Files
llmgw/internal/cli/threads.go
Ilker ManapandClaude Opus 5.5 3104f4b706 Manage people and the tenancy tree, from the web and the CLI
Nothing in the tree could be renamed, moved or removed, and an approved
person stayed in the project they were first put in — from the web or
the CLI. Now:

- /admin/users is the tree, each project opening onto its people and
  keys, with approve, move, suspend, reinstate and remove for people,
  revoke for keys, and add, rename, move and remove at every level,
  plus a search over every account. Forms post and get the page back,
  so it works with no script; htmx 2.0.11 (0BSD, embedded, checked
  against npm's integrity hash) swaps only the changed part, keeping
  open projects open.
- llmgw org tree/add/rename/move/remove, users move/delete, and
  keys list --all.
- Projects, teams and organizations are named by id, path
  (Acme/platform/gateway) or unique name; users by id or email. With a
  single project --project can be left out, instead of defaulting to
  "default", which a tree built from the web does not have.

Moving a person moves the keys they made; operator keys stay with the
project. Only empty things are removed, and never a project that had a
key, since usage history refers to it.

Also: keys init no longer reparents an existing team or project;
reinstating an account with no project is refused (a rejected signup
used to come back active and fail at its first key); rejecting a
signup removes it; keys limits --budget takes whole units like the
quotas page.

Checked in headless Chrome: a move from an open project updates the
page in place, keeps the project open, and the search still filters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:26:39 +02:00

76 lines
2.5 KiB
Go

package cli
import (
"fmt"
"github.com/spf13/cobra"
)
// newThreadsCmd turns conversation storage on for a project, or off.
//
// A command rather than a configuration key because it is a decision about
// data, not about deployment: someone has to choose to keep what a team types,
// and choosing it should look like an act rather than like a default nobody
// read.
//
// Until it is run for a project, llmgw stores no message content for that
// project — usage rows carry token counts and costs, never text — and that is
// how every project starts.
func newThreadsCmd(open opener) *cobra.Command {
var (
project string
hours int
off bool
)
cmd := &cobra.Command{
Use: "threads",
Short: "Keep conversations for a project, so a thread can move between models",
Long: "With this on, a client sends x-llmgw-thread and only its new turn, and\n" +
"llmgw replays the conversation to whichever model serves — including a\n" +
"different one from the one it started on.\n\n" +
"Off for every project until this is run. llmgw otherwise stores no\n" +
"message content anywhere, which is a much easier thing to have deployed\n" +
"in front of a team, so turning it on is deliberate.\n\n" +
"The retention is required rather than optional: a conversation store with\n" +
"no expiry is a transcript of everything a team ever typed, kept forever\n" +
"because nobody chose a number.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if !off && hours <= 0 {
return fmt.Errorf("--retain-hours is required: say how long conversations are kept, or pass --off")
}
s, err := open(cmd.Context())
if err != nil {
return err
}
defer s.Close()
keep := hours
if off {
keep = 0
}
p, err := resolveProject(cmd.Context(), s, project)
if err != nil {
return err
}
project = p.Path
if err := s.SetThreadRetention(cmd.Context(), p.ID, keep); err != nil {
return err
}
if keep == 0 {
fmt.Fprintf(cmd.OutOrStdout(),
"%s keeps no conversations. Existing ones are removed at the next prune.\n", project)
return nil
}
fmt.Fprintf(cmd.OutOrStdout(),
"%s keeps conversations for %d hours, measured from the last turn.\n", project, keep)
return nil
},
}
cmd.Flags().StringVar(&project, "project", "", "project: id, path or unique name")
cmd.Flags().IntVar(&hours, "retain-hours", 0, "how long a conversation is kept after its last turn")
cmd.Flags().BoolVar(&off, "off", false, "stop keeping conversations for this project")
return cmd
}