diff --git a/NOTES.md b/NOTES.md
index a9d2d3e558706776c3e48a594edb1740be23fa5c..25b966959058313cddea018ca9dfac2ee59dee44 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -34,6 +34,8 @@ sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain
     files?  How would it work?
   * Cleanup help output.
     * Remove the node related flags.
+  * Database
+    * trigger to limit values for 'visibility';
 
 ## Module links
 
diff --git a/dclish/dclish.go b/dclish/dclish.go
index 19c93fe4f09966690007736b3a358f9a8872206e..0302781f228afc4fcd836574770c2a08d8d540eb 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -4,6 +4,7 @@ package dclish
 import (
 	"fmt"
 	"strings"
+	"unicode"
 )
 
 // ActionFunc is the function that a command runs.
@@ -11,10 +12,10 @@ type ActionFunc func(*Command) error
 
 // Flag is a flag for a command.
 type Flag struct {
+	OptArg      bool
 	Value       string
 	Default     string
 	Description string
-	// TODO: Toggle bool
 }
 
 // Flags is the list of flags.
@@ -34,10 +35,81 @@ type Command struct {
 // Commands is the full list of commands.
 type Commands map[string]*Command
 
+func split(line string) []string {
+	words := []string{}
+	buf := strings.Builder{}
+	state := "start"
+	for _, c := range line {
+		switch state {
+		case "start":
+			if unicode.IsSpace(c) {
+				continue
+			}
+			if c == '"' {
+				state = "dquote"
+			} else if c == '\'' {
+				state = "squote"
+			} else {
+				state = "raw"
+				buf.WriteRune(c)
+			}
+		case "dquote":
+			if c == '"' {
+				words = append(words, buf.String())
+				buf.Reset()
+				state = "start"
+			} else {
+				buf.WriteRune(c)
+			}
+		case "squote":
+			if c == '\'' {
+				words = append(words, buf.String())
+				buf.Reset()
+				state = "start"
+			} else {
+				buf.WriteRune(c)
+			}
+		case "dquote-raw":
+			if c == '"' {
+				state = "raw"
+			}
+			buf.WriteRune(c)
+		case "squote-raw":
+			if c == '\'' {
+				state = "raw"
+			}
+			buf.WriteRune(c)
+		case "raw":
+			if unicode.IsSpace(c) {
+				words = append(words, buf.String())
+				buf.Reset()
+				state = "start"
+			} else if c == '/' {
+				words = append(words, buf.String())
+				buf.Reset()
+				state = "raw"
+				buf.WriteRune(c)
+			} else if c == '"' {
+				state = "dquote-raw"
+				buf.WriteRune(c)
+			} else if c == '\'' {
+				state = "squote-raw"
+				buf.WriteRune(c)
+			} else {
+				buf.WriteRune(c)
+			}
+		}
+	}
+	if len(buf.String()) > 0 {
+		words = append(words, buf.String())
+	}
+	return words
+}
+
 // ParseAndRun parses a command line and runs the command.
 func (c Commands) ParseAndRun(line string) error {
 	// TODO: this doesn't handle a DCL command line completely.
-	words := strings.Fields(line)
+	words := split(line)
 	cmd, ok := c[strings.ToUpper(words[0])]
 	if !ok {
 		wordup := strings.ToUpper(words[0])
@@ -75,27 +147,33 @@ func (c Commands) ParseAndRun(line string) error {
 	for i := range args {
 		if strings.HasPrefix(args[i], "/") {
 			flag, val, assigned := strings.Cut(args[i], "=")
+			var wordup string
 			if assigned {
-				wordup := strings.ToUpper(flag)
-				flg, ok := cmd.Flags[wordup]
-				if !ok {
-					fmt.Printf("ERROR: Flag '%s' not recognised.\n", args[i])
-					return nil
-				}
-				flg.Value = val
+				wordup = strings.ToUpper(flag)
 			} else {
-				wordup := strings.ToUpper(args[i])
-				value := "true"
-				if strings.HasPrefix(wordup, "/NO") {
-					wordup = strings.Replace(wordup, "/NO", "/", 1)
-					value = "false"
-				}
-				flg, ok := cmd.Flags[wordup]
+				wordup = args[i]
+			}
+			toggleValue := "true"
+			flg, ok := cmd.Flags[wordup]
+			if !ok {
+				wordup = strings.Replace(wordup, "/NO", "/", 1)
+				flg, ok = cmd.Flags[wordup]
 				if !ok {
 					fmt.Printf("ERROR: Flag '%s' not recognised.\n", args[i])
 					return nil
 				}
-				flg.Value = value
+				toggleValue = "false"
+			}
+			if !flg.OptArg && assigned {
+				fmt.Printf("ERROR: Flag '%s' is a toggle.\n", args[i])
+				return nil
+			}
+			if flg.OptArg {
+				if assigned {
+					flg.Value = strings.Trim(val, "\"'")
+				}
+			} else {
+				flg.Value = toggleValue
 			}
 		} else {
 			if len(cmd.Args) == cmd.MaxArgs {
diff --git a/folders/folders.go b/folders/folders.go
index 2fd334bf4aad947db4f3fdce6adaa8cba8a31e51..039ff10e22cce3dfba66266d75cde67ab62e82b9 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -2,7 +2,6 @@
 package folders
 
 import (
-	"database/sql"
 	"embed"
 	"errors"
 	"os"
@@ -11,6 +10,7 @@ import (
 	"github.com/adrg/xdg"
 	"github.com/golang-migrate/migrate/v4"
 	"github.com/golang-migrate/migrate/v4/source/iofs"
+	"github.com/jmoiron/sqlx"
 
 	// Included to connect to sqlite.
 	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
@@ -23,7 +23,7 @@ var fs embed.FS
 // Store is the store for folders.
 type Store struct {
 	user string
-	db   *sql.DB
+	db   *sqlx.DB
 }
 
 // Open opens the folders database.
@@ -35,6 +35,7 @@ func Open(user string) (*Store, error) {
 	}
 	fdb := path.Join(fdir, "bboard.db")
 
+	// Run db migrations if needed.
 	sqldir, err := iofs.New(fs, "sql")
 	if err != nil {
 		return nil, err
@@ -50,7 +51,7 @@ func Open(user string) (*Store, error) {
 	m.Close()
 
 	store := &Store{user: user}
-	store.db, err = sql.Open("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)")
+	store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)")
 	if err != nil {
 		return nil, errors.New("bulletin database problem")
 	}
diff --git a/folders/manage-folders.go b/folders/manage-folders.go
new file mode 100644
index 0000000000000000000000000000000000000000..8beaf58b7eca969165b011972630b007591dee37
--- /dev/null
+++ b/folders/manage-folders.go
@@ -0,0 +1,75 @@
+// Package folders are all the routines and sql for managing folders.
+package folders
+
+import (
+	"errors"
+	"fmt"
+)
+
+// FolderVisibility is the folder visibility level.
+type FolderVisibility string
+
+// Values for FolderVisibility.
+const (
+	FolderPublic      FolderVisibility = "public"
+	FolderSemiPrivate                  = "semi-private"
+	FolderPrivate                      = "private"
+)
+
+// FolderOptions are a list of folder options.
+type FolderOptions struct {
+	Always      int
+	Brief       int
+	Description string
+	Notify      int
+	Owner       string
+	Readnew     int
+	Shownew     int
+	System      int
+	Expire      int
+	Visibility  FolderVisibility
+}
+
+// CreateFolder creates a new folder.
+func (s *Store) CreateFolder(name string, options FolderOptions) error {
+	_, err := s.db.Exec(
+		`INSERT INTO folders
+			(name, always, brief, description, notify, owner, readnew,
+			 shownew, system, expire, visibility)
+			VALUES
+			($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
+		name,
+		options.Always,
+		options.Brief,
+		options.Description,
+		options.Notify,
+		options.Owner,
+		options.Readnew,
+		options.Shownew,
+		options.System,
+		options.Expire,
+		options.Visibility,
+	)
+	// TODO: process this error a bit more to give a better error message.
+	return err
+}
+
+// DeleteFolder creates a new folder.
+func (s *Store) DeleteFolder(name string) error {
+	results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", name)
+	// TODO: process this error a bit more to give a better error message.
+	if err != nil {
+		return err
+	}
+	rows, err := results.RowsAffected()
+	if err != nil {
+		return err
+	}
+	if rows == 0 {
+		return errors.New("No such folder found")
+	}
+	if rows != 1 {
+		return fmt.Errorf("Unexpected number (%d) of folders removed", rows)
+	}
+	return nil
+}
diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql
index 6094a1cdb7c4f74ebb93615ae3e20e45ec1e146e..02b78c886f35f3f59b0c5e6159d98bbd65e10695 100644
--- a/folders/sql/1_create_table.up.sql
+++ b/folders/sql/1_create_table.up.sql
@@ -86,7 +86,7 @@ INSERT INTO folders (name, description, system, shownew, owner)
        VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1, 'SYSTEM');
 
 CREATE TABLE co_owners (
-  folder      VARCHAR(25)  REFERENCES folders(name) ON UPDATE CASCADE,
+  folder      VARCHAR(25)  REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
   owner       VARCHAR(25)  REFERENCES users(login) ON UPDATE CASCADE,
   create_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
   update_at   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
diff --git a/go.mod b/go.mod
index 4f17e6ab7216c567010304d9a3b4881b16543ed8..bb985d3bddcf100c541110f0c5b6753cc3efd8bd 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,9 @@ go 1.24.2
 require (
 	github.com/adrg/xdg v0.5.3
 	github.com/chzyer/readline v1.5.1
+	github.com/davecgh/go-spew v1.1.1
 	github.com/golang-migrate/migrate/v4 v4.18.3
+	github.com/jmoiron/sqlx v1.4.0
 	github.com/urfave/cli/v3 v3.3.2
 	modernc.org/sqlite v1.37.0
 )
@@ -23,7 +25,7 @@ require (
 	go.uber.org/atomic v1.11.0 // indirect
 	golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
 	golang.org/x/sys v0.33.0 // indirect
-	modernc.org/libc v1.65.0 // indirect
+	modernc.org/libc v1.65.1 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.10.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 95baed5b57cce81c2990d3cd0c5a588e636f8cf9..7ea4c6a9a3de68b90931c7d940137c6e08b13f19 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,15 @@
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
 github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
 github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
 github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
 github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
 github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
 github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
@@ -18,8 +21,12 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -40,6 +47,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y=
 modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs=
+modernc.org/libc v1.65.1 h1:EwykJ3C7c5pCiZTU3dLkgRm3VdFGNFc8UXXzhhEZvbQ=
+modernc.org/libc v1.65.1/go.mod h1:+LU/iIPTqxVVdAl3E++KC9npafs4zI4pkLiolMVDatc=
 modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
 modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
 modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
diff --git a/main.go b/main.go
index 2ea43e3f0713cecaaf725b47d31290fff1d286dd..caeb3111336ca16b504e799e53b45e62683adf39 100644
--- a/main.go
+++ b/main.go
@@ -43,6 +43,6 @@ func main() {
 
 	err := cmd.Run(context.Background(), os.Args)
 	if err != nil {
-		fmt.Println(err)
+		fmt.Printf("ERROR: %s.", err)
 	}
 }
diff --git a/repl/command.go b/repl/command.go
index 77f5d1bf9a2d8ea90ad86092bd3a1de5b084fe1d..f913639f04d16faef1041d61ae7480c463542d95 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -33,12 +33,6 @@ A node which does not have BULLCP running cannot have a message
 broadcasted to it, (even though it is able to create a remote folder).
 
 See also /ALL and /BELL.`,
-			},
-			"/CLUSTER": {
-				Description: `/[NO]CLUSTER
-
-This option specifies that broadcasted messages should be sent to all
-nodes in the cluster.  /CLUSTER is the default.`,
 			},
 			"/EDIT": {
 				Description: `/[NO]EDIT
@@ -52,6 +46,7 @@ BULLETIN command line.`,
 Specifies the time at which the message is to expire.  Either absolute
 time: [dd-mmm-yyyy] hh:mm:ss, or delta time: dddd [hh:mm:ss] can be
 used.`,
+				OptArg: true,
 			},
 			"/EXTRACT": {
 				Description: `Specifies that the text of the previously read message should be included
@@ -82,36 +77,21 @@ ALL_FOLDERS after /FOLDER=.  Note that the quotation marks are required.
 When using /FOLDER for remote nodes, proxy logins are used to  determine
 if privileged options are allowed.  If they are not allowed, the message
 will still be added, but without the privileged settings.`,
+				OptArg: true,
 			},
-			"/LOCAL": {
-				Description: `Specifies  that  when  /BROADCAST  is specified for a remote folder, the
-message is broadcasted ONLY on the local node.`,
-			},
-			"/NODES": {
-				Description: `/NODES=(nodes[,...])
-
-Specifies  to send the message to the listed DECNET nodes.  The BULLETIN
-utility  must  be  installed  properly  on  the   other   nodes.    (See
-installation  notes). You can specify a different username to use at the
-other nodes by either using the USERNAME qualifier, or by specifying the
-nodename   with   2   semi-colons   followed   by   the  username,  i.e.
-nodename::username.  If you specify a username, you will be prompted for
-the password of the account on the other nodes.
+			"/INDENT": {
+				Description: `See /EXTRACT for information on this qualifier.
 
-Additionally,  you  can  specify logical names which translate to one or
-more node names.  I.e.  $ DEFINE ALL_NODES  "VAX1,VAX2,VAX3",  and  then
-specify /NODES=ALL_NODES.  Note that the quotation marks are required.
-
-NOTE:  It  is  preferable  to  use /FOLDER instead of /NODE if possible,
-since adding messages via /FOLDER is much quicker.`,
-			},
-			"/NOINDENT": {
-				Description: `See /EXTRACT for information on this qualifier.`,
+Defaults to set - use /NOINDENT to suppress.`,
+				Default: "true",
 			},
-			"/NOSIGNATURE": {
-				Description: `Specifies to suppress the automatically appended signature, if one exists.
+			"/SIGNATURE": {
+				Description: `Specifies to automatically appended signature, if one exists.
 Signatures are appended for postings to mailing lists and to responds.
-See the help topic POST Signature_file for signature information.`,
+See the help topic POST Signature_file for signature information.
+
+Defaults to set - use /NOSIGNATURE to suppress.`,
+				Default: "true",
 			},
 			"/PERMANENT": {
 				Description: `If specified, message will be a permanent message and will never expire.
@@ -122,9 +102,10 @@ user has privileges.`,
 				Description: `/SUBJECT=description
 
 Specifies the subject of the message to be added.`,
+				OptArg: true,
 			},
 			"/SHUTDOWN": {
-				Description: `/SHUTDOWN[=nodename]
+				Description: `/SHUTDOWN
 This option is restricted to privileged users.  If specified, message
 will be automatically deleted after a computer shutdown has occurred.
 This option is restricted to SYSTEM folders.
@@ -137,6 +118,7 @@ node reboots, you have the option of specifying that node name.
 NOTE: If the folder is a remote folder, the message will be deleted
 after the remote node reboots, not the node from which the message was
 added.  The nodename cannot be specified with a remote folder.`,
+				OptArg: true,
 			},
 			"/SYSTEM": {
 				Description: `This option is restricted to privileged users.  If specified, message
@@ -145,10 +127,6 @@ when a user logs in.  System messages should be as brief as possible to
 avoid the possibility that system messages could scroll off the screen.
 This option is restricted to SYSTEM folders.`,
 			},
-			"/USERNAME": {
-				Description: `Specifies username to be used at remote DECNET nodes when adding messages
-to DECNET nodes via the /NODE qualifier.`,
-			},
 		},
 	},
 	"BACK": {
@@ -167,14 +145,6 @@ further reads in the selected folder.  The default is /HEADER.`,
 			},
 		},
 	},
-	"BULLETIN": {
-		Description: `The BULLETIN utility permits a user to create a message for reading by
-all users.  Users are notified upon logging in that new messages have
-been added, and what the topic of the messages are.  Actual reading of
-the messages is optional. (See the command SET READNEW for info on
-automatic reading.)  Messages are automatically deleted when their
-expiration date has passed.`,
-	},
 	"CHANGE": {
 		Description: `Replaces or modifies existing stored message.  This is for changing part
 or all of a message without causing users who have already seen the
@@ -207,6 +177,7 @@ added /EDIT to your BULLETIN command line.`,
 Specifies the time at which the message is to expire.  Either absolute
 time: [dd-mmm-yyyy] hh:mm:ss, or delta time: dddd [hh:mm:ss] can be
 used.  If no time is specified, you will be prompted for the time.`,
+				OptArg: true,
 			},
 			"/GENERAL": {
 				Description: `Specifies that the message is to be converted from a SYSTEM message to
@@ -231,11 +202,12 @@ date and message headers can be changed if a range is specified.
 
 The key words CURRENT and LAST can also be specified in the range,
 in place of an actual number, i.e. CURRENT-LAST, 1-CURRENT, etc.`,
+				OptArg: true,
 			},
 			"/PERMANENT": {
 				Description: `Specifies that the message is to be made permanent.`,
 			},
-			"/SHUTDOWN[=nodename]": {
+			"/SHUTDOWN": {
 				Description: `Specifies that the message is to expire after the next computer
 shutdown.  This option is restricted to SYSTEM folders.`,
 			},
@@ -243,6 +215,7 @@ shutdown.  This option is restricted to SYSTEM folders.`,
 				Description: `/SUBJECT=description
 
 Specifies the subject of the message to be added.`,
+				OptArg: true,
 			},
 			"/SYSTEM": {
 				Description: `Specifies that the message is to be made a SYSTEM message.  This is a
@@ -353,42 +326,24 @@ feature in order to respond to NEWS messages).  The default protocol is
 IN%.  If desired, you can specify the protocol with the address, i.e.
 
               INFOVAX MAILING LIST <IN%"INFO-VAX@KL.SRI.COM">`,
+				OptArg: true,
+			},
+			"/EXPIRE": {
+				Description: `/EXPIRE=days
+
+Sets the default number of days for messages to expire.
+
+Default value is 14.`,
+				OptArg:  true,
+				Default: "14",
 			},
 			"/ID": {
 				Description: `Designates that the name specified as the owner name is a rights
 identifier.  The creator's process must have the identifier presently
 assigned to it.  Any process which has that identifier assigned to it
 will be able to control the folder as if it were the folder's owner.
-This is used to allow more than one use to control a folder.
-
-Note: This feature will not work during remote access to the folder.`,
-			},
-			"/NODE": {
-				Description: `/NODE=node
-
-Specifies that the folder is a remote folder at the specified node.
-A remote folder is a folder in which the messages are actually stored
-on a folder at a remote DECNET node.  The specified node is checked to
-see if a folder of the same name is located on that node.  If so, the
-folder will then be modified to point to that folder.  For example if
-there was a folder on node A with name INFO, and you issued the command:
-                        CREATE INFO/NODE=A
-from node B, then if INFO is selected on node B, you will actually
-obtain the folder INFO on node A.  In this manner, a folder can be shared
-between more than one node. This capability is only present if the BULLCP
-process is running on the remote node via the BULL/STARTUP command.
-If the remote folder name is different from the local folder name, the
-remote folder name is specified using the /REMOTENAME qualifier.
-
-NOTE: If a message is added to a remote node, the message is stored
-immediately.  However, a user logging into another node might not be
-immediately alerted that the message is present.  That information is
-only updated every 15 minutes (same algorithm for updating BBOARD
-messages), or if a user accesses that folder.  Thus, if the folder is
-located on node A, and the message is added from node B, and a user logs
-in to node C, the BULLETIN login notification might not notify the user
-of the message.  However, if the message is added with /BROADCAST, the
-message will be broadcasted immediately to all nodes.`,
+This is used to allow more than one use to control a folder.`,
+				OptArg: true,
 			},
 			"/NOTIFY": {
 				Description: `Specifies that all users automatically have NOTIFY set for this folder.
@@ -399,6 +354,7 @@ more information.)`,
 				Description: `/OWNER=username
 Specifies the owner of the folder.  This is a privileged command.
 See also /ID.`,
+				OptArg: true,
 			},
 			"/PRIVATE": {
 				Description: `Specifies that the folder can only be accessed by users who have been
@@ -413,12 +369,6 @@ compilation of this program).  NOTE: See HELP SET ACCESS for more info.`,
 				Description: `Specifies that all users automatically have READNEW set for this folder.
 Only a privileged user can use this qualifier.  (See HELP SET READNEW for
 more information.)`,
-			},
-			"/REMOTENAME": {
-				Description: `/REMOTENAME=foldername
-Valid only if /NODE is present, i.e. that the folder is a remote folder.
-Specifies the name of the remote folder name.  If not specified, it is
-assumed that the remote name is the same as the local name.`,
 			},
 			"/SHOWNEW": {
 				Description: `Specifies that all users automatically have SHOWNEW set for this folder.
@@ -489,22 +439,6 @@ remote folder at a time.`,
 			"/IMMEDIATE": {
 				Description: `Specifies that the message is to be deleted immediately.`,
 			},
-			"/NODES": {
-				Description: `/NODES=(nodes[,...])
-
-Specifies to delete the message at the listed DECNET nodes.  The BULLETIN
-utility must be installed properly on the other nodes.  You can specify
-a different username to use at the other nodes by either using the
-USERNAME qualifier, or by specifying the nodename with 2 semi-colons
-followed by the username, i.e. nodename::username.  If you specify a
-username, you will be prompted for the password of the account on the
-other nodes.  The /SUBJECT must be specified to identify the specific
-message that is to be deleted.
-
-Additionally, you can specify logical names which translate to one or
-more node names.  I.e.  $ DEFINE ALL_NODES "VAX1,VAX2,VAX3", and then
-specify /NODES=ALL_NODES.  Note that the quotation marks are required.`,
-			},
 			"/SUBJECT": {
 				Description: `/SUBJECT=subject
 
@@ -514,10 +448,7 @@ The specified subject need not be the exact subject of the message.
 It can be a substring of the subject.  This is in case you have forgotten
 the exact subject that was specified.  Case is not critical either.
 You will be notified if the deletion was successful.`,
-			},
-			"/USERNAME": {
-				Description: `Specifies username to be used at remote DECNET nodes when deleting messages
-on other DECNET nodes via the /NODE qualifier.`,
+				OptArg: true,
 			},
 		},
 	},
@@ -552,6 +483,7 @@ folder.`,
 				Description: `/END=message_number
 
 Indicates the last message number you want to display.`,
+				OptArg: true,
 			},
 			"/FOLDERS": {
 				Description: `Lists the available message folders.  Shows last message date and number
@@ -604,12 +536,14 @@ are to be displayed.  This cannot be used in conjunction with /MARKED.`,
 Specifies that only messages which contain the specified string are
 to be displayed.  This cannot be used in conjunction with /MARKED.
 If no string is specified, the previously specified string is used.`,
+				OptArg: true,
 			},
 			"/SINCE": {
 				Description: `/SINCE=date
 
 Displays a listing of all the messages created on or after the
 specified date.  If no date is specified, the default is TODAY.`,
+				OptArg: true,
 			},
 			"/START": {
 				Description: `/START=message_number
@@ -617,6 +551,7 @@ specified date.  If no date is specified, the default is TODAY.`,
 Indicates the first message number you want to display.  For example,
 to  display  all the messages beginning with number three, enter the
 command line DIRECTORY/START=3.  Not valid with /FOLDER.`,
+				OptArg: true,
 			},
 			"/SUBJECT": {
 				Description: `/SUBJECT=[string]
@@ -625,6 +560,7 @@ Specifies that only messages which contain the specified string in it's
 subject header are to be displayed.  This cannot be used in conjunction
 with /MARKED.  If no string is specified, the previously specified string
 is used.`,
+				OptArg: true,
 			},
 		},
 	},
@@ -808,6 +744,7 @@ than one word, enclose the text in quotation marks (").
 
 If you omit this qualifier, the description of the message will be used
 as the subject.`,
+				OptArg: true,
 			},
 		},
 	},
@@ -879,6 +816,7 @@ Note: This feature will not work during remote access to the folder.`,
 				Description: `/NAME=foldername
 
 Specifies a new name for the folder.`,
+				OptArg: true,
 			},
 			"/OWNER": {
 				Description: `/OWNER=username
@@ -886,6 +824,7 @@ Specifies a new name for the folder.`,
 Specifies a new owner for the folder.  If the owner does not have
 privileges, BULLETIN will prompt for the password of the new owner
 account in order to okay the modification.  See also /ID.`,
+				OptArg: true,
 			},
 		},
 	},
@@ -995,6 +934,7 @@ message is printed at the beginning. The default is to write the header.`,
 Indicates that you will be notified by a broadcast message  when  the
 file or files have been printed.  If /NONOTIFY is specified, there
 is no notification.  The default is /NOTIFY.`,
+				Default: "true",
 			},
 			"/NOW": {
 				Description: `Sends all messages that have been queued for printing with the PRINT
@@ -1005,6 +945,7 @@ command during this session to the printer.`,
 
 The name of the queue to which a message is to be sent.  If the /QUEUE
 qualifier  is  not  specified,  the message is queued to SYS$PRINT.`,
+				OptArg: true,
 			},
 		},
 	},
@@ -1091,6 +1032,7 @@ the contents of the terminal's memory.`,
 
 Specifies to read the first message created on or after the specified
 date.  If no date is specified, the default is TODAY.`,
+				OptArg: true,
 			},
 		},
 	},
@@ -1102,6 +1044,7 @@ remove the folder.
     REMOVE folder-name`,
 		MinArgs: 1,
 		MaxArgs: 1,
+		Action:  ActionRemove,
 	},
 	"REPLY": {
 		Description: `Adds message with subject of message being the subject of the  currently
@@ -1112,14 +1055,21 @@ the same as the ADD command except for /NOINDENT and /EXTRACT.
     REPLY [file-name]`,
 		MaxArgs: 1,
 		Flags: dclish.Flags{
+			"/EDIT": {
+				Description: `Specifies that the editor is to be used for creating the reply
+message.`,
+			},
 			"/EXTRACT": {
 				Description: `Specifies that the text of the message should be included in the reply
-mail message.  This qualifier is valid only when used with /EDIT.  The
+message.  This qualifier is valid only when used with /EDIT.  The
 text of the message is indented with > at the beginning of each line.
 This can be suppressed with /NOINDENT.`,
 			},
-			"/NOINDENT": {
-				Description: `See /EXTRACT for information on this qualifier.`,
+			"/INDENT": {
+				Description: `See /EXTRACT for information on this qualifier.
+
+Defaults to set - use /NOINDENT to suppress.`,
+				Default: "true",
 			},
 		},
 	},
@@ -1139,6 +1089,7 @@ of the message.`,
 			"/CC": {
 				Description: `/CC=user[s]
 Specifies additional users that should receive the reply.`,
+				OptArg: true,
 			},
 			"/EDIT": {
 				Description: `Specifies that the editor is to be used for creating the reply mail
@@ -1156,13 +1107,19 @@ associated with the folder.  The mailing list address should be stored
 in the folder description.  See CREATE/DESCRIPTION or MODIFY/DESCRIPTION
 for more informaton.`,
 			},
-			"/NOINDENT": {
-				Description: `See /EXTRACT for information on this qualifier.`,
+			"/INDENT": {
+				Description: `See /EXTRACT for information on this qualifier.
+
+Defaults to set - use /NOINDENT to suppress.`,
+				Default: "true",
 			},
-			"/NOSIGNATURE": {
-				Description: `Specifies to suppress the automatically appended signature, if one exists.
+			"/SIGNATURE": {
+				Description: `Specifies to automatically appended signature, if one exists.
 Signatures are appended for postings to mailing lists and to responds.
-See the help topic POST Signature_file for signature information.`,
+See the help topic POST Signature_file for signature information.
+
+Defaults to set - use /NOSIGNATURE to suppress.`,
+				Default: "true",
 			},
 			"/SUBJECT": {
 				Description: `/SUBJECT=text
@@ -1172,6 +1129,7 @@ than one word, enclose the text in quotation marks (").
 
 If you omit this qualifier, the description of the message will be used
 as the subject preceeded by "RE: ".`,
+				OptArg: true,
 			},
 		},
 	},
@@ -1208,6 +1166,7 @@ a match.  If, during a search, no more matches or messages are found,
 the next folder in the list is automatically selected.  The presently
 selected folder can be included in the search by specifying "" as the
 first folder in the list.`,
+				OptArg: true,
 			},
 			"/REPLY": {
 				Description: `Specifies that messages are to be searched for that are replies to the
@@ -1223,6 +1182,7 @@ message.`,
 				Description: `/START=message_number
 
 Specifies the message number to start the search at.`,
+				OptArg: true,
 			},
 			"/SUBJECT": {
 				Description: `Specifies that only the subject of the messages are to be searched.`,
diff --git a/repl/folders.go b/repl/folders.go
index 7611b81c7305ffd4cd0d33ffa2bdad60b10fe2fd..4dbbdade011eed3434455b00a69ba1bcd97ff296 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -2,11 +2,14 @@
 package repl
 
 import (
+	"errors"
 	"fmt"
+	"strconv"
 	"strings"
 
 	"git.lyda.ie/kevin/bulletin/accounts"
 	"git.lyda.ie/kevin/bulletin/dclish"
+	"git.lyda.ie/kevin/bulletin/folders"
 )
 
 // ActionDirectory handles the `DIRECTORY` command.  This lists all the
@@ -30,8 +33,55 @@ func ActionIndex(cmd *dclish.Command) error {
 
 // ActionCreate handles the `CREATE` command.  This creates a folder.
 func ActionCreate(cmd *dclish.Command) error {
-	fmt.Printf("TODO: implement CREATE:\n%s\n\n", cmd.Description)
-	return nil
+	options := folders.FolderOptions{}
+	if cmd.Flags["/ALWAYS"].Value == "true" {
+		options.Always = 1
+	}
+	if cmd.Flags["/BRIEF"].Value == "true" {
+		options.Brief = 1
+	}
+	if cmd.Flags["/DESCRIPTION"].Value == "" {
+		return errors.New("Description is required - use /DESCRIPTION")
+	}
+	options.Description = cmd.Flags["/DESCRIPTION"].Value
+	if cmd.Flags["/NOTIFY"].Value == "true" {
+		options.Notify = 1
+	}
+	if cmd.Flags["/OWNER"].Value != "" {
+		options.Owner = cmd.Flags["/OWNER"].Value
+	} else {
+		options.Owner = accounts.User.Account
+	}
+	if cmd.Flags["/READNEW"].Value == "true" {
+		options.Readnew = 1
+	}
+	if cmd.Flags["/SHOWNEW"].Value == "true" {
+		options.Shownew = 1
+	}
+	if cmd.Flags["/SYSTEM"].Value == "true" {
+		options.System = 1
+	}
+	if cmd.Flags["/EXPIRE"].Value != "" {
+		expire, err := strconv.Atoi(cmd.Flags["/EXPIRE"].Value)
+		if err != nil {
+			return fmt.Errorf("Invalid expiry value '%s'", cmd.Flags["/EXPIRE"].Value)
+		}
+		options.Expire = expire
+	}
+	options.Visibility = folders.FolderPublic
+	if cmd.Flags["/PRIVATE"].Value == "true" && cmd.Flags["/SEMIPRIVATE"].Value == "true" {
+		return errors.New("Private or semi-private - pick one")
+	}
+	if cmd.Flags["/PRIVATE"].Value == "true" {
+		options.Visibility = folders.FolderPrivate
+	}
+	if cmd.Flags["/SEMIPRIVATE"].Value == "true" {
+		options.Visibility = folders.FolderSemiPrivate
+	}
+
+	err := accounts.User.Folders.CreateFolder(cmd.Args[0], options)
+	// TODO: handle the /ID flag.
+	return err
 }
 
 // ActionSelect handles the `SELECT` command.  This selects a folder.
@@ -45,3 +95,12 @@ func ActionModify(cmd *dclish.Command) error {
 	fmt.Printf("TODO: implement MODIFY:\n%s\n\n", cmd.Description)
 	return nil
 }
+
+// ActionRemove handles the `REMOVE` command.  This modifies a folder.
+func ActionRemove(cmd *dclish.Command) error {
+	err := accounts.User.Folders.DeleteFolder(cmd.Args[0])
+	if err == nil {
+		fmt.Println("Folder removed.")
+	}
+	return err
+}
diff --git a/repl/help.go b/repl/help.go
index d3dc916d1b3e5757455edaa0b3de6a653e7a9f51..e7b6ae555d0cfbfc16f51a76b3558631a1d822ac 100644
--- a/repl/help.go
+++ b/repl/help.go
@@ -10,6 +10,12 @@ import (
 )
 
 var helpmap = map[string]string{
+	"BULLETIN": `The BULLETIN utility permits a user to create a message for reading by
+all users.  Users are notified upon logging in that new messages have
+been added, and what the topic of the messages are.  Actual reading of
+the messages is optional. (See the command SET READNEW for info on
+automatic reading.)  Messages are automatically deleted when their
+expiration date has passed.`,
 	"FOLDERS": `All messages are divided into separate folders.  The default folder is
 GENERAL.  New folders can be created by any user.  As an example, the
 following creates a folder for GAMES related messages: 
diff --git a/repl/misc.go b/repl/misc.go
index a5b62f330ab8cca7e7b9d70b0e8d970d73fd51a0..0bb3a730a1739c9e7427221f2b61eda1cc483b24 100644
--- a/repl/misc.go
+++ b/repl/misc.go
@@ -2,7 +2,8 @@
 package repl
 
 import (
-	"errors"
+	"fmt"
+	"os"
 
 	"git.lyda.ie/kevin/bulletin/accounts"
 	"git.lyda.ie/kevin/bulletin/dclish"
@@ -12,12 +13,16 @@ import (
 func ActionQuit(_ *dclish.Command) error {
 	accounts.User.Close()
 	// TODO: IIRC, quit should not update unread data.  Check old code to confirm.
-	return errors.New("QUIT")
+	fmt.Println("QUIT")
+	os.Exit(0)
+	return nil
 }
 
 // ActionExit handles the `EXIT` command.
 func ActionExit(_ *dclish.Command) error {
 	accounts.User.Close()
 	// TODO: update unread data.
-	return errors.New("EXIT")
+	fmt.Println("EXIT")
+	os.Exit(0)
+	return nil
 }
diff --git a/repl/repl.go b/repl/repl.go
index 0f3536642795c308ecc81df622652c00864cbf0d..fea8113eb9b94f495449022a4acbec5b5d1e9029 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -41,7 +41,7 @@ func Loop(user string) error {
 		}
 		err = commands.ParseAndRun(line)
 		if err != nil {
-			return err
+			fmt.Printf("ERROR: %s.\n", err)
 		}
 	}
 }