Skip to main content

crewship chat

A chat session is the thread that backs every crewship run or crewship ask. The top-level chat <chat-id> command prints the whole transcript; the subcommands let you list recent chats per agent, upload attachments, and add emoji reactions. Defined in cmd/crewship/cmd_chat.go.
Auth: commands require an authenticated session (crewship login) except chat share read, which uses a dedicated share token. Authenticated commands also require a selected workspace except chat stream, which resolves tenancy from the chat row itself and so needs only a token and a chat id. The transcript hits GET /api/v1/chats/{id}/messages?limit=500 — the cap is server-side and not paged today.

Subcommands

Flags

chat <chat-id>

The transcript renders assistant / model messages through the markdown ANSI renderer; user and system messages stay plain. Each block carries a coloured role · timestamp header (yellow for user, green for assistant, dim for system).

chat attach <chat-id> <path>

The upload is one multipart POST to POST /api/v1/agents/{agentId}/chats/{chatId}/attachments. The server caps the body at 25 MB; the CLI assembles the multipart body in memory rather than streaming because the bound is small. The crew has to be running. The file lands in the agent’s /output tree, which a provisioned crew owns as uid 1001 — the server cannot create attachments/<chat-id>/ there itself, so it writes through the crew container as that uid. On a stopped crew the upload returns 409 with failed to save attachment: the agent's output directory is owned by the crew runtime; … start the crew and retry. Start the crew and re-run the attach; nothing partial is left behind, and no attachment metadata row is recorded for an upload that did not land. Each upload gets its own directory. The file lands at attachments/<chat-id>/<attachment-id>/<filename> — the readable filename is still the last segment, and the id in front of it is what makes the location unique. Attaching two different files called evidence.pdf gives you two attachments and two blobs, each with its own checksum; neither replaces the other. Attaching the same file twice is a retry and resolves to the attachment that already exists. Both facts are printed by chat attachments list, which is also where you get the id that chat attachments delete takes.

chat attachments list <chat-id>

Calls GET /api/v1/agents/{agentId}/chats/{chatId}/attachments. Columns are ID / FILENAME / SIZE / SHA256 (first 12 hex characters — the full digest is in --format json) / AGENT PATH / CREATED. An upload that did not finish is never listed.

chat attachments delete <chat-id> <attachment-id>

Calls DELETE /api/v1/agents/{agentId}/chats/{chatId}/attachments/{attachmentId}. Removes the stored file and then the metadata row. Idempotent: deleting an attachment that is already gone succeeds, because what you asked for is already true. This is the only way to reclaim a chat attachment’s bytes short of deleting the whole chat — chat blobs are not content-addressed (the path is what the agent is told to open), so they sit outside the background reclaim sweep that collects issue attachments. Like the upload, the removal goes through the crew container, so a stopped crew refuses it with failed to delete attachment: …. Nothing is lost — the row is kept and re-running the command after starting the crew completes the deletion.

chat list <agent-slug-or-id>

See List an agent’s recent chats below for what the rows mean and how to read KIND first.

chat read <chat-id>

Calls PUT /api/v1/agents/{agentId}/chats/{chatId}/read. Your read cursor advances to now: UNREAD in chat list drops to 0 for the session, and the “agent replied” inbox item for it flips to read. Read state is per user — marking a shared group chat read doesn’t touch anyone else’s badge.

chat rename <chat-id> <title>

Calls PATCH /api/v1/agents/{agentId}/chats/{chatId} with {"title": "..."}. Quote the title — it is one argument, not the rest of the line. The server normalises what you send before storing it: the whole title is folded onto one line (newlines and tab runs become a single space), control characters and invisible formatting characters are stripped, and the result is trimmed and capped at 200 characters (characters, not bytes — an emoji or a Czech title gets the same length as an English one). A title that is empty after that, or longer than the cap, is refused with 400 rather than silently truncated. Who may rename: the chat’s creator always, and anyone who can edit the agent for anyone else’s chats — the same gate as chat delete.
Human output is Chat <chat-id> renamed to "<title>". — the title in the message is the stored one, so normalisation is visible. --format json / yaml / ndjson emit {"id":"<chat-id>","title":"<stored title>"}.

chat steer <chat-id>

The message POSTs to POST /api/v1/chats/{chatId}/steer. If a run is in flight, the message is held and applied on the next turn rather than interrupting the running one (live mid-turn injection is a planned follow-up).

chat stream <chat-id>

Reads GET /api/v1/chats/{chatId}/stream. The run’s text lands on stdout and nothing else does, so crewship chat stream c_abc123 > reply.md captures the reply and only the reply; thinking, tool calls and stream notices go to stderr. A run that ends in an error exits non-zero. Only a workspace-scoped token is needed — not a selected workspace. The route resolves tenancy from the chat itself, so CREWSHIP_TOKEN plus a chat id is enough. Any run that has a chat can be watched this way — a chat message, crewship run, crewship ask, a routine’s agent_run step, a webhook trigger or a scheduled agent. A run executed with no chat row has no session channel to attach to, and a delegated sub-agent’s output is reported as part of the delegating agent’s turn rather than as a stream of its own. --format ndjson (or json) switches to passthrough: every server frame is printed verbatim, one JSON object per line. That is the mode to script against.
If the connection drops mid-run the command reconnects on a bounded backoff and resumes from the last sequence number it printed, so a blip neither loses nor duplicates output. Full frame reference: Watching agent runs.

chat participants add <chat-id> <user-id>

crewship chat create <agent-slug-or-id>

Create a chat session on an agent without sending it a message. Calls POST /api/v1/agents/{agentId}/chats with origin: CLI; the session starts empty and ACTIVE. Every other way to get a session — ask, run, routine iterate — creates it on the way to a model call, so none of them can produce one on an instance with no provider credential. chat create needs none, which makes it the way to set up scripted fixtures and to exercise chat stream’s idle contract (stream.open → stream.end / no_active_run) on a workspace where nothing has ever run. The chat id goes to stdout in every format, so it can be captured directly:

crewship chat delete <chat-id>

Delete a chat session, its messages, and read cursors. The operation is irreversible and requires confirmation unless --yes is supplied. The agent is resolved automatically; use --agent to select it explicitly.
Human output is Chat <chat-id> deleted. Machine output is {"chat_id":"<chat-id>","status":"deleted"} with --format json (or the equivalent YAML object with --format yaml).

crewship chat react remove <chat-id> <message-id> <emoji>

Remove your emoji reaction from a message. Aliases are rm and delete.
The command takes exactly the chat ID, message ID, and emoji; it has no command-specific flags. On success it prints Removed <emoji> from message <message-id>. Repeating the operation is safe because reactions are idempotent per chat, message, emoji, and user.

crewship chat room

Workspace rooms are the human side of Chat: a DM is a fixed pair of people, a private group is a roster of people, and a channel is visible to the whole workspace and may have agents joined to it. They live on the /api/v1/conversations routes and their ids and message sequences are separate from the agent sessions every other chat subcommand takes — a room id is not a chat id. chat room and chat rooms are the same group. Rules that hold for every room command:
  • Auth: a login plus a selected workspace, like the rest of chat. Private groups and DMs also require membership; a room you cannot reach answers 404 conversation not found, including the creator-only mutations, so a refusal never reveals who else is in it.
  • Machine output is the server’s envelope. --format json / yaml / ndjson print the response verbatim — pagination cursors, has_more, job states — and a 204 No Content success prints {"status":"ok"}. The default human format renders the same envelope as key/value rows, with id first. --format quiet prints the first row’s value only: the bare id for create, direct, get, send and continue, but for list, messages, jobs, participants list, agents list, activity and the 204 commands it is the value of the first key in sorted order (agents, conversations, issues, jobs, messages, participants, status) — use --format json to script against those.
  • Agents run only when mentioned. Joining an agent to a channel gives it the history when it is asked; only send --mention-agent queues work. Plain @name in the text does nothing.
  • Retries are by id. send and continue take a --client-id you choose; repeating the same request with the same id returns the original result (200) instead of a duplicate, and the same id with different content is refused with 409.

chat room list

Calls GET /api/v1/conversations. Only rooms you can reach are listed: your DMs, the private groups you are in, and every channel in the workspace.

chat room create

Calls POST /api/v1/conversations and prints the new room (201). A member who is not in the workspace is refused with 404 — the same answer as a room you cannot reach, so the refusal does not confirm who is in the workspace. Creating a channel does not invoke any agent; use chat room agents add and then chat room send --mention-agent, both below.

chat room direct <user-id>

No command-specific flags. Calls POST /api/v1/conversations/direct. The first call creates the pair’s DM (201); every later call from either side returns the same room (200) — there is never a second DM for the same two people, and its roster cannot be edited (participants add on a DM is a 400; use chat room continue, below, to bring in a third person without exposing the DM’s history). The target must be a different current workspace member.

chat room get <room-id>

No command-specific flags. Calls GET /api/v1/conversations/{conversationId} and prints the room: id, kind, title, created_by, is_direct, last_sequence, plus the caller’s own last_read_sequence, unread_count and muted. A DM adds direct_user_id, direct_user_name and direct_avatar_url for the other person.

chat room messages <room-id>

Calls GET /api/v1/conversations/{conversationId}/messages. The two cursor flags are mutually exclusive — the CLI refuses both before making a request. Messages within a page are in ascending sequence order and carry the server id, sequence, author, content, timestamp, retry id, structured mentions and kind; a system message (an agent joining, an activity event) is marked source_kind=activity.

chat room send <room-id>

Calls POST /api/v1/conversations/{conversationId}/messages and prints the persisted message (201; a retry prints the original, 200). Two different refusals: mentioning any agent in a group or DM, which cannot hold agents, is a 400; mentioning an agent that is not joined to the channel is a 404 (the agent is treated as unreachable, like a room you are not in). The message is accepted before the agent has answered: watch chat room jobs for the reply’s progress.

chat room read <room-id>

Calls POST /api/v1/conversations/{conversationId}/read. Your own cursor only, and it never moves backwards; nobody else’s unread count changes. A sequence beyond the room’s last_sequence is refused with 400 — the cursor can only point at a message that exists.

chat room mute <room-id>

Calls POST /api/v1/conversations/{conversationId}/mute. A notification preference, not membership — the room keeps counting your unread messages.

chat room jobs <room-id>

No command-specific flags. Calls GET /api/v1/conversations/{conversationId}/agent-jobs and prints {"jobs":[…]}: one entry per mention with id, message_id, agent_id, state (pending until the server picks it up), assignment_id once the work is running and error when it failed. This is how you tell “accepted” from “answered”.

chat room participants — list, add, remove

No command-specific flags. list calls GET /api/v1/conversations/{conversationId}/participants and prints {"participants":[…]} with user_id, name, avatar_url, joined_at and an informational role; for a channel this is the explicit roster, not a limit on who can read it. add and remove (POST …/participants, DELETE …/participants/{userId}) are creator-only — anyone else gets 404. add grants the room’s existing history and is idempotent. A DM refuses both (400), and the creator cannot be removed. Channel removal changes membership without revoking workspace-wide access. Reading, sending or muting a channel does not rejoin it.

chat room agents — list, add, remove

No command-specific flags. list calls GET /api/v1/conversations/{conversationId}/agents and prints {"agents":[…]} with agent_id, name, slug, joined_at and avatar fields. add (POST …/agents) and remove (DELETE …/agents/{agentId}) are creator-only and channels only — a group or DM answers 400. Joining posts a visible notice and gives the agent access to the channel’s history when it is mentioned; it starts no work. Queued mention jobs re-check access before they run, so a removed agent does not answer from the queue.

chat room activity <room-id>

With neither flag the command reads the settings (GET /api/v1/conversations/{conversationId}/activity); any workspace member who can see the channel may. To change them (PUT, creator-only) both flags must be given explicitly — setting only one is refused before any request is made. Both default to false on the server. Enabling starts with future events: nothing is backfilled, and the system messages it posts never queue agent jobs. A private group or DM has no activity settings (400).

chat room continue <room-id>

Calls POST /api/v1/conversations/{conversationId}/continue and prints the new room (201; the same request retried prints the same room, 200). The source is left exactly as it was and its messages are never copied: a group continuation starts empty, a channel continuation holds only the agent-joined notice. Inviting an agent into what started as a private discussion therefore always means choosing a new, workspace-visible channel on purpose. Mixing the two shapes — an --agent on a group, or no --agent on a channel — is refused before any request is made.

Examples

List an agent’s recent chats

Rows are ordered by LAST ACTIVITY (newest message, not creation time). UNREAD counts messages you haven’t seen — your own don’t count; - means fully read. KIND is the one to read first, because an agent’s chats are not all the same thing. Four writers land in this list: On a busy agent the routine rows outnumber the rest by an order of magnitude — a five-step nightly routine writes 150 a month — so --kind is usually what you want:
The filter runs in the query, before the server’s page limit, which is the point of it: narrowing after the fact narrows a page routine rows have already filled. Omitted, every kind is listed — --kind narrows, it never reorders. Casing and stray separators are forgiven (--kind ALL, --kind "direct,"), but a value with no kind in it at all — --kind , — is an error rather than silently meaning “everything”. A filter that fails open hands back the mixed list you were trying to escape while you believe you narrowed it. Naming all alongside a kind is refused for the same reason. The raw provenance token is still on the wire as origin (UI, CLI, WEBHOOK, CRON, ROUTINE, AGENT); --output json carries both it and kind. Use origin when you specifically want to tell a crewship run session from a browser one — handy when grep-ing for terminal sessions to attach a file to.

Catch up on a reply you missed

Attach a file

List and remove a chat’s files

Two files with the same name are two attachments with two checksums and two paths — which is the point of the id segment. Use --format json when you want the full digest to compare against what an agent reported reading. Pass --agent when the auto-lookup is ambiguous (the chat lives on more than one agent in your visible set) or when you’ve already pre-resolved the slug.

React to a message

Server-side reactions are idempotent per (chat, message, emoji, user) — add and remove can be called blindly without first checking list.

Manage group-chat participants

Adding the first participant flips the chat to group visibility, after which the agent only replies when @mentioned. Mutations require the caller to be the chat owner or a workspace OWNER/ADMIN — otherwise the server returns 403. Adding a user who isn’t in the workspace returns 400 user is not a member of this workspace, and removing the chat owner returns 400 cannot remove the chat owner.

Common errors

  • chat <id> not found in any agent's recent sessions — chat attach auto-lookup walked every visible agent and didn’t find the chat. Pass --agent to short-circuit the search.
  • bad --since: ... — the value isn’t a Go duration (1h, 24h) or an RFC 3339 timestamp.
  • open <path>: no such file or directory — chat attach couldn’t open the local file.

See also

crewship chat share

Read-only sharing of one direct human chat. A share grants ongoing user/assistant text, including future messages until expiry or revocation. It does not grant workspace membership, files, tool output, streams, or agent execution.

crewship chat share create

The chat creator or current workspace OWNER/ADMIN can create a share. --ttl is a Go duration between 1s and 168h, default 24h. JSON output is {share, token} (YAML is also supported). The token is shown only once. Keep it private and send it separately from the share ID through a trusted channel.

crewship chat share list

Lists metadata as {shares: [...]}, including expiry and revocation, without tokens. Requires the same current sharing authority as creation.

crewship chat share revoke

Invalidates the token on the next read request. Already received copies cannot be recalled. Removing the issuer’s membership also invalidates future reads.

crewship chat share read

--token-stdin is required: provide the token on standard input, then end input. An explicit --server is required. This command ignores the normal profile’s credential and workspace. Do not put tokens in command arguments or URLs. Output is {messages: [...]} as JSON (or YAML). Only user/assistant text is included. Recipients can alternatively enter the ID and token at /shared-chat. The management API is GET/POST /api/v1/agents/{agentId}/chats/{chatId}/shares and DELETE /api/v1/agents/{agentId}/chats/{chatId}/shares/{shareId}. The reader uses GET /api/v1/shared-chats/{shareId}/messages with a dedicated share bearer.