crewship chat
A chat session is the thread that backs everycrewship 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.
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.
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.
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.
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 answers404 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/ndjsonprint the response verbatim — pagination cursors,has_more, job states — and a204 No Contentsuccess prints{"status":"ok"}. The default human format renders the same envelope as key/value rows, withidfirst.--format quietprints the first row’s value only: the bare id forcreate,direct,get,sendandcontinue, but forlist,messages,jobs,participants list,agents list,activityand the204commands it is the value of the first key in sorted order (agents,conversations,issues,jobs,messages,participants,status) — use--format jsonto 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-agentqueues work. Plain@namein the text does nothing. - Retries are by id.
sendandcontinuetake a--client-idyou 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 with409.
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>
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>
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>
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
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
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
Print a transcript
List an agent’s recent chats
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:
--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
--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
(chat, message, emoji, user) — add and remove can be called blindly without first checking list.
Manage group-chat participants
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 attachauto-lookup walked every visible agent and didn’t find the chat. Pass--agentto 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 attachcouldn’t open the local file.
See also
crewship run— start an agent run (creates the chat).crewship ask— one-shot prompt (also creates a chat, taggedorigin=CLI).crewship session— browser session management, unrelated noun.- Conversations API — the underlying endpoints.
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
--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
{shares: [...]}, including expiry and revocation, without
tokens. Requires the same current sharing authority as creation.
crewship chat share revoke
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.