@entrydesk/cli
v2.2.0
Published
EntryDesk CLI - Interact with EntryDesk from your terminal
Readme
EntryDesk CLI
Command-line client for EntryDesk. Includes interactive task, tool output rendering, and scripting-friendly JSON output.
Installation
Requires Node.js 24 or newer (engines.node in package.json is the source
of truth; it tracks the monorepo root). On an older Node the CLI refuses to run
and says which version it needs, rather than starting and then failing
mid-command.
npm install -g @entrydesk/cliTo update an existing installation to the latest published version:
entrydesk update # check + install
entrydesk update --check # only check, don't install
entrydesk update --json # machine-readable resultA stable install compares itself with the latest dist-tag. A beta install
(npm install -g @entrydesk/cli@beta, any prerelease version) compares itself
with both beta and latest and installs whichever is newer, so it moves to
the stable release once that overtakes the beta. update never installs a
version older than the one you have. --check and --json show the channel
(latest or beta) and the version each dist-tag points to: in --json,
candidates holds the dist-tags and latest is the newest of them, the version
update installs.
Quick Start
Build and run from the repo:
pnpm --filter @entrydesk/cli build
pnpm --filter @entrydesk/cli start -- task -iInteractive task is also the default when no command is provided:
entrydeskAuthentication
entrydesk login # OAuth device flow — prints a verification URL + code to enter
entrydesk status
entrydesk logout
entrydesk token # Print a fresh access token for scriptsScripting with the access token
entrydesk token prints a valid access token and nothing else, refreshing the
stored session first when it is about to expire, so scripts and CI can call the
API directly without reading the keychain:
entrydesk token --json # {"token":"…","expiresAt":"…","workspaceId":"…"}
WORKSPACE_ID=$(entrydesk token --json | jq -r .workspaceId)
curl -H "Authorization: Bearer $(entrydesk token)" \
"https://api.entrydesk.com/v1/workspaces/$WORKSPACE_ID/agents"Call workspace-scoped endpoints (/v1/workspaces/<id>/...) with the printed
token: a personal access token (ENTRYDESK_TOKEN) is only accepted there, and
account-level endpoints such as /v1/workspaces reject it with 403.
expiresAt is read from the token itself and omitted when it carries none;
when ENTRYDESK_TOKEN is set, that token is printed as-is. The token is a
bearer credential for your account — keep it out of logs and shared output.
Credential Storage
By default, EntryDesk CLI stores credentials in the OS keychain when available.
If the keychain is unavailable, it falls back to an encrypted file at
~/.entrydesk/profiles/<profile>/credentials.json.
To force file storage (for example, in CI), set
ENTRYDESK_FORCE_FILE_STORAGE=true.
Task
entrydesk task -m "Hello" --model <model>
entrydesk task -a <agent>
entrydesk task -c 1 -m "Follow up"
entrydesk task -i
entrydesk task -i --full-output # Show full tool outputs
entrydesk task -i --no-file-system # Disable local file operations
entrydesk task -i --connectors id1,id2 # Preload tools from specific connectorsPiping input and output:
echo "Hello" | entrydesk task --model <model>
entrydesk task -m "Hi" --model <model> --plain
entrydesk task -m "Hi" --model <model> --output stream-jsonWith --output stream-json, the last line is {"event":"done","data":{"taskId":"…","text":"…"}}: text is the final answer, without the progress notes the agent writes between tool calls.
Stopping a task:
entrydesk tasks stop <taskId> # Or a 1-based index into `entrydesk tasks`
entrydesk tasks stop 1 --json # {"taskId":"…","status":"stopped"}A one-shot entrydesk task prints its task id before sending: Task: <id>
on stderr, or a first {"event":"task","data":{"taskId":"…"}} line under
--output stream-json. Ctrl+C, SIGTERM or SIGHUP (the terminal closing)
stops the task on the server and ends the local command it is running, then
exits with 130, 143 or 129; a second signal exits at once. A signal before the
message is sent stops nothing. If the stop cannot be confirmed, the CLI prints
the entrydesk tasks stop command to retry. tasks stop exits 0 when the task stopped or was not running, and 1
when the task does not exist or is not yours. Stopping cancels the task's
active run, so with -c it also stops a run of that task started from Hub once
this message has been sent.
A local command ends with everything it started, whether it outlives its
timeout or the task is stopped: on macOS and Linux the command runs in a
process group of its own, which gets SIGTERM and, two seconds later,
SIGKILL; on Windows taskkill /T ends its process tree. A second signal
skips the two seconds and kills it before the CLI exits. Interactive mode
(-i) does the same on SIGTERM or SIGHUP (the terminal closing), then
exits with 143 or 129 without stopping the task on the server. Some processes
get away, by design:
- Work a command leaves in the background after it finished on its own
(
nohup … &) keeps running, as it would in your terminal. - A process that leaves the group (
setsid, a double fork) or, on Windows, one started withStart-Processis out of reach. - A command has no terminal, so one that prompts on
/dev/tty(sudo, an ssh passphrase) fails instead of waiting for input.
Browse Resources
entrydesk workspaces
entrydesk workspaces switch <workspaceId> # Switch to a workspace
entrydesk models
entrydesk agents
entrydesk connectors
entrydesk skills
entrydesk tasks
entrydesk tasks 1
entrydesk tasks <taskId> --limit 20 --next-key <cursor> # An older page of its messagesThe task list (tasks --json and project tasks --json, without --search)
prints { "tasks": [...], "nextKey": "..." }, tasks --search <query> --json
prints { "results": [...], "nextKey": "..." }, and tasks <id> --json prints
{ "messages": [...], "nextKey": "..." }; pass nextKey back as --next-key
for the next page. Under --output stream-json the closing done event carries
it instead, as { "event": "done", "data": { "nextKey": "..." } }, and has no
data on the last page. Viewing a task reads its newest 50 messages unless
--limit says otherwise, and names the command for older ones when there are
more. That command uses the task's id even when you opened it as tasks 1: the
recent list can shift between calls, and a cursor read against another task
returns the wrong messages.
Project
Projects group sources (the files and links you put in one), the tasks that belong to it, and the assets those tasks produce. Its instruction is the standing brief every task in the project starts from.
entrydesk project # List projects you belong to
entrydesk project list --filter mine # all | mine | shared
entrydesk project get 1 # …by its number in that list
entrydesk project get <projectId> # …or by id
entrydesk project instruction 1 # Plain text, ready to pipe
entrydesk project edit 1 --name "Q4 Launch" --description "Launch coordination"A <project> number always means a position in a plain project list. A
--filter, --sort-by, --order or --all listing renumbers from 1, so those
print no position column — pass an id there.
A plain listing is in creation order, oldest first, and the number is resolved
against that listing as it stands at that moment. A project created after you
looked takes the next number, so the ones you saw keep theirs; only a deletion
shifts the numbers after it. entrydesk tasks <n> numbers newest first, so a
new task does shift its positions. Numbers are for typing by hand; scripts, and
anything that has to keep pointing at one project across deletions, should use
the id.
--sort-by lastActivityAt --order desc shows the most recently active project first, the
order the Hub uses; that listing prints ids instead of positions.
The instruction is plain text on stdout with nothing around it, so it pipes and redirects cleanly, and a project without one prints nothing and exits 0:
entrydesk project instruction 1 > brief.md
cat brief.md | entrydesk project edit 1 --instruction-file -
entrydesk project edit 1 --clear-instruction--instruction, --instruction-file and --clear-instruction are mutually
exclusive. --instruction-file - reads stdin, and stores exactly what it is
given — leading and trailing whitespace included, the same as a file path — so
the round-trip above returns the instruction unchanged.
Project text that another member wrote (names, descriptions, the instruction as
project get renders it) has terminal control characters stripped before it is
displayed. project instruction writes the raw bytes when its output is piped
or redirected, and filters them only when it is writing to your terminal.
These commands read and edit the project itself; none of them talks to the AI.
To work with the AI inside a project, pass --project to the task commands.
task --project creates its task in the project, so it starts from the project
instruction and the AI answers as it always does; entrydesk project note is
the other path, filing a work note with no AI reply. tasks --project lists or
searches only the tasks you can see in that project:
entrydesk task --project 2 -m "List the open questions in the spec"
entrydesk task --project 2 -i # Interactive, same project
entrydesk tasks --project 2 # Tasks you can see in project 2
entrydesk tasks --project 2 -q "deploy" # …searched within it--project also decides what the numbers mean. entrydesk tasks and a bare
-c <n> never list a project's tasks, so inside a project both count down
entrydesk tasks --project <project> instead:
entrydesk tasks --project 2 # 1 is the newest task in project 2
entrydesk task --project 2 -c 1 -m "Follow up"The same holds for /tasks in an interactive --project session: it offers
that project's tasks only, so picking one cannot move the session out of it.
A piped task gets both halves of what you gave it: -m is your instruction
and stdin is the material to work on, so this reaches the AI as one turn
carrying the file and the request.
entrydesk project context-read 2 file <id> \
| entrydesk task --project 2 -m "Turn this into a checklist"stdin here is content from elsewhere, so it is handed over as material to read
rather than as instructions to follow, and anything inside it that looks like a
command is not one. Piped stdin on its own is still the message itself, exactly
as before, and -i never reads stdin.
A piped stdin is always read to its end, with or without -m, so a producer
that takes seconds to answer is waited for instead of being dropped:
curl -s https://example.com/spec.md | entrydesk task --project 2 -m "列出待辦"The cost is the other side of the same rule: a command spawned with an stdin it
inherited but never sends on waits for that pipe to close. Close it explicitly
when you do not mean to send anything — entrydesk task -m "…" </dev/null.
Work notes
A work note is the other direction: writing down what you did without asking
the AI anything. project note creates a task under your name in the project,
private until you share it, and no AI reply is triggered. The content comes
from -m, then -f, then stdin — the first one given wins, and all three are
equivalent:
entrydesk project note 2 -m "Shipped the retry fix"
entrydesk project note 2 -f ./summary.md
git log -1 | entrydesk project note 2 --title "Fixed the login race"
entrydesk project note 2 -m "Audit: 3 docs lack an owner" --visibility project--visibility project shares the note with every current project member as it
is filed and notifies them. The workspace sharing policy and the
sensitive-content check still apply; if either blocks the share, the note is
still filed privately and the output says why.
It prints the new task's id and a Hub link; --json returns
{ "taskId", "projectId", "visibility", "url" }, plus shareLink when the
note was shared, or shareFailure (the server's error code) when it was not.
Reach for task --project above whenever you want the AI to answer instead.
Tasks in a project
project tasks is the project's own listing — the same tasks tasks --project
shows, printed with their ids so they can be handed to the commands below. The
other three do what the Hub's task menu does: move a task of yours in or out,
and share it inside the project. Only the task's owner can do any of this; the
API refuses everyone else with Task not found, the same answer as for a task
that does not exist.
entrydesk project tasks 1 # Tasks you can see in project 1
entrydesk project tasks 1 -q "deploy" # …searched within it
entrydesk project task-add 1 <taskId> # Move your task in
entrydesk project task-add 1 <taskId> --confirm-asset-sharing
entrydesk project task-remove 1 <taskId> # Move it out; its assets stay
entrydesk project task-share 1 <taskId> --add-user <userId>
entrydesk project task-share 1 <taskId> --remove-user <userId>
entrydesk project task-share 1 <taskId> --add-team "Design"
entrydesk project task-share 1 <taskId> --remove-team <teamId>
entrydesk project task-share 1 <taskId> --visibility project
entrydesk project task-share 1 <taskId> --visibility selfMoving a task in also files the assets it produced with the project, where every
member can view and use them. When the task carries assets the project does not
hold yet, task-add stops and says so instead of sharing them on its own —
re-run it with --confirm-asset-sharing to go ahead. Under --json that
refusal is the API's own asset_sharing_confirmation_required code on stderr.
Moving a task out leaves its assets in the project along with their version
history; only the task moves.
task-share does one thing per call and never prompts. --add-user and
--remove-user name one project member by id (project get <project> --json
lists the members with theirs); --add-team and --remove-team name one team
that is already in the project, by id or by name (project members <project>
lists them), and everyone in that team can view the task, read-only;
--visibility project opens the task to every member and --visibility self
closes it again, keeping whoever you named. The five flags are mutually
exclusive and each needs its value; with none of them the command prints the
usage and exits 1. A task has to be in the project you
name — task-share and task-remove refuse one that is elsewhere rather than
acting on the project it is actually in.
project tasks reads with tasks:read; the three that write need
tasks:write, and task-remove and task-share also read the task first with
tasks:read. --add-team and --remove-team also read the project's teams,
which needs projects:read. A project you are not a member of answers
Project not found from all four.
Sources and context
Sources are the files and links you put in a project — the same list the
Hub's Sources tab shows, with each one's retrieval description and whether it
can be searched: processing while it is being indexed, ready, partial
with the pages, slides or rows that could not be indexed (missingUnits), or
failed with the reason (knowledgeIssue). A link reports its snapshot first —
pending while it is being fetched, failed with the fetch error — and its
index status once the snapshot is in. Context is
everything the AI knows in the project, so searching and reading across the
tasks you can see and those sources hangs off that word instead:
entrydesk project sources 1
entrydesk project context-search 1 "login race" --type task --limit 5
entrydesk project context-read 1 file <fileId> # A file's stored text
entrydesk project context-read 1 task <taskId> # A task's summary
entrydesk project context-read 1 task <taskId> --full # …its whole history
entrydesk project context-read 1 file <fileId> --evidence <evidenceId> # One search hit
entrydesk project context-read 1 file <fileId> --start 12 --end 14 # Pages 12-14context-search looks across tasks and sources at once; --type task, --type
file or --type link narrows it to one kind. Assets are not on this surface —
view them in the Hub — and asking for one fails the same way every other
project command fails, JSON on stderr included.
sources lists what the API serves in one shot, which is the newest 200 files
and the newest 200 links. A project holding more says so on stderr, and under
--json the payload carries truncated: true beside the rows.
The sources-* commands maintain that list the way the Hub's Sources tab does
— upload a file, add a link, edit a description, re-fetch a link, remove one.
sources-add takes a local file path, or a link when the argument starts with
http:// or https://; the file is uploaded and then attached to the project.
<file|link> and <id> are exactly what project sources prints for a row:
entrydesk project sources-add 1 ./brief.pdf --description "The launch brief"
entrydesk project sources-add 1 https://example.com/spec
entrydesk project sources-edit 1 file <fileId> --description "Signed-off scope"
entrydesk project sources-edit 1 file <fileId> --title "Launch brief" # How the AI names it
entrydesk project sources-refetch 1 <linkId> # Snapshot goes back to pending
entrydesk project sources-replace 1 <fileId> ./brief.md --expected-version 3
entrydesk project sources-rm 1 link <linkId>sources-replace swaps a file's content in place: its id, title and
description stay, and its version number goes up by one. Pass the version
project sources showed when you read the file. If someone replaced it since,
nothing changes: the command exits 1 and names the current version, who saved
it and a URL to read it from. Under --json the error carries
code: "project_file_version_conflict" and the same details under data. The
CLI never retries a replace on its own, so a conflict always means someone
else's write. Merge your change into that content and replace again with that
version. The replaced content is kept as an earlier version, up to the latest 20.
A link starts fetching its snapshot as soon as it is added, and sources-refetch
starts that over, so both report it as pending; project sources shows when
it is ready. Re-fetching a link whose fetch is still in flight is refused
rather than queued twice. Assets are removed in the Hub, so sources-rm takes
only file or link. --json prints the source the API returned with a
type field beside it, and sources-rm --json prints { "type", "id",
"removed": true }.
Uploading a file, attaching it and saving its --description is one request,
declared on projects:write like every other sources-* call; nothing is
left behind when the project refuses it. A token short of that scope is told so.
context-read writes the source's text and nothing else to stdout, so it pipes
straight into a task in the same project:
entrydesk project context-read 1 file <fileId> \
| entrydesk task --project 1 -m "List the work this implies"Everything that is not text stays off stdout, so a pipe never carries an
explanation where the content should be: a file whose text has not been
extracted (still processing, unsupported or failed — a PDF, slide deck,
spreadsheet or image with extracted text reads like any text file), a link
whose snapshot is still being fetched, and one whose fetch failed each print
their reason on stderr and exit non-zero. With --json that reason is the
{ "status": … } object from the API — download with a short-lived
downloadUrl, pending, or failed with the error — again on stderr.
A long source comes back one capped page at a time. A read that is cut short
says so on stderr, never on stdout, and names the --offset to continue from;
with --json the same facts are the truncated and nextOffset fields of the
object on stdout. Repeat with the reported offset until the read is no longer
truncated — only ever pass back what the server reported:
entrydesk project context-read 1 file <fileId> > brief.md
entrydesk project context-read 1 file <fileId> --offset 50000 --revision <rev> >> brief.mdWhat the offset counts depends on the source. For a task it is the number of
newest messages to skip, so --offset implies --full — a summary is not
paged. A file or a link snapshot has one depth, --full changes nothing, and
the offset is a position in its stored text. It comes with a --revision
naming the stored text it was measured against: a snapshot refetched between
two pages fails the next read instead of quietly serving new bytes at the old
offset, so start again from the beginning when that happens.
A file or a link can also be read by its indexed passages instead of its
stored text. Each context-search hit on a source prints its Evidence id and
its location; --evidence <id> reads that passage with the ones around it, and
--start <n> / --end <n> read the passages overlapping a page, slide or row
range (1-based, inclusive). The text still goes to stdout alone; stderr lists
each passage's evidence id and location, and a range cut short names the
--start to continue from — passages and nextStart under --json.
--evidence and a range cannot be combined, neither works with --offset or
--revision, and tasks have no passages; the CLI refuses those before sending
a request. An evidence id lasts until its source is re-indexed, so search again
when a read says it is gone.
Governance
A project can be created, deleted and staffed without opening the Hub, under the same rules the Hub applies. Whoever creates it is its owner:
entrydesk project create --name "Q4 Launch" --description "Launch coordination"
cat brief.md | entrydesk project create --name "Q4 Launch" --instruction-file -
entrydesk project members 1 # Each member, role and id, then each team
entrydesk project member-add 1 <userId> # Takes effect at once
entrydesk project member-role 1 <userId> --role owner
entrydesk project member-remove 1 <userId> # Your own id leaves the project
entrydesk project team-add 1 --team "Design" # Everyone in the team, now and later
entrydesk project team-remove 1 --team <teamId>
entrydesk project delete 1 # Lists what goes, then asks
entrydesk project delete 1 -y # No promptcreate prints the new id and its Hub address, and with --json the payload
carries both. --instruction and --instruction-file are the same options
edit takes, with the same limit.
Only an owner can delete a project, add or remove someone else, or change a
role; a member can leave. A user id is the UUID project members prints beside
each member — a workspace member who is not in the project yet is found in the
Hub's member picker. The last owner cannot leave or step down to member: promote
someone else first. Each refusal comes back as its own message, and with
--json under its own code (project_owner_required,
project_last_owner_required, project_member_already_exists,
project_invitee_not_in_workspace, project_member_not_found). A project you
are not in is Project not found for every one of these commands, exactly as
it is for get.
A team in a project makes everyone in it a member, and anyone who joins the team
later too; the team is always a member, never an owner. project members lists
the teams after the members, each with its size, visibility and id, and
--json carries them as teams[] beside members[]. A hidden team you are not
in shows as locked: its name and size, never who is in it. team-add and
team-remove take --team as an id or a name. A name has to match exactly,
case included — team names are unique in a workspace, so an exact name is
never ambiguous. A name that only matches in a different case is never picked:
the command fails with project_team_not_found and lists the similar names
with their ids as hints. team-add checks the project's own teams first, so a
name already in the project is project_team_already_added. Only an owner can add or remove a
team, and a hidden team can only be added by one of its own members
(project_team_not_found otherwise, and for a team that is already gone);
adding one twice is project_team_already_added. Both need projects:write;
team-remove, and team-add given a name, also read with projects:read.
Deleting a project detaches its tasks, which return to their owners' main
lists, and deletes its sources, its assets and every version in their history.
Without -y the command prints those effects for the project in question and
waits for a y; with no terminal to ask on it refuses instead (exit code 1,
JSON on stderr under --json), so a stray id in a script cannot delete
anything.
A project command fails in one of three ways, each with its own message: the
server has projects turned off, your token is missing a scope the command
needs (the message names each missing scope — run entrydesk login again, or
issue a PAT that grants it), or the project is not one you can see — reported
as Project not found whether it is someone else's or does not exist. With
--json the error is JSON on stderr and the exit code is non-zero.
Commands that only read or edit the project itself need projects:read or
projects:write; the task commands need tasks:read or tasks:write.
project note, context-search and context-read reach both a project's
sources and its tasks, so each requires both families at once: project note
needs projects:write and tasks:write, and the two context commands need
projects:read and tasks:read. entrydesk login grants all four; a PAT has
to be issued with them.
Tools
entrydesk tool search "send a group chat message" --json
entrydesk tool search "read a spreadsheet" --namespace google_drive --json
entrydesk tool list # List directly exposed tools
entrydesk tool list --prefix agent # Filter the direct list by prefix
entrydesk tool get agent__search_agents --json # Get the input schema
entrydesk tool call agent__search_agents --input '{"limit": 5}'
entrydesk tool call agent__search_agents --input-file input.json
echo '{"limit": 5}' | entrydesk tool call agent__search_agentstool list is the server's direct tool list, not a complete connector catalog.
Connector tools can be discovered and called even when they are absent from this
list. Use tool search with an English description of the operation, including
the service name when known. --namespace restricts connector discovery to exact
namespaces copied from previous results; omit it when you do not know them.
If you already know the exact name, tool get <name> --json retrieves its input
schema, including for tools absent from the direct list. Read the schema, prepare
JSON in a file, then use tool call <name> --input-file input.json --json in your
script. The file or stdin keeps message bodies and other source text out of shell
quoting. Run entrydesk tool call --help for input options.
list --json returns an array, get --json returns a tool object, and
call --json returns the MCP result. search --json returns the discovery object
with primaryTools, optional relatedTools, toolSchemas, authStatuses, and
error. Search matches are not an exhaustive list. Check authStatuses before
calling a connector that needs authorization.
Discovery errors use TOOL_DISCOVERY_FAILED, preserving the server's explanation.
If lookup returns no schema, the CLI suggests checking the name and connector
connection: the server may report a name missing when its connector is unavailable,
so that response does not prove the tool does not exist.
Errors go to stderr with exit status 1. A partially failed search still writes its
results to stdout and exits with status 1, so scripts can retain successful matches
without treating the search as complete. JSON stdout contains no help text.
Updating the sandbox CLI
The CLI in a sandbox comes from its image, independently of the API release.
After publishing a CLI version containing these commands, update the exact
@entrydesk/cli pin in both apps/entrydesk/sandbox/Dockerfile and
apps/entrydesk/sandbox/Dockerfile.kubernetes, then rebuild and release the affected
sandbox images. If an environment still uses E2B, update its separate template
as well. Do not pin an unpublished version.
Verify a fresh sandbox with entrydesk --version, entrydesk tool call --help,
and entrydesk tool search --help. Against the target workspace, use a known
connector tool name absent from tool list and confirm tool get <name> --json
returns its schema. These checks do not execute the connector operation. Existing
sandboxes may retain their old CLI until they are recreated.
MCP Server
entrydesk mcp-server # Stdio mode (for MCP clients)
entrydesk mcp-server --http --port 3100 # HTTP mode (for debugging)Skills Management
entrydesk skills # List all available skills
entrydesk skills --me # List only skills created by you
entrydesk skills upload my-skill.zip # Upload a skill zip file
entrydesk skills upload ./my-skill # Upload a skill folder (auto-zipped)
entrydesk skills delete skill-123 # Delete a skill you own
entrydesk skills --json # Output in JSON formatBudget Management
entrydesk budget # Show current workspace budget
entrydesk budget get # Get workspace budget
entrydesk budget set 100.00 # Set workspace budget to $100
entrydesk budget clear # Remove workspace budget
entrydesk budget --json # Output in JSON formatUsage Statistics
entrydesk usage # Show usage by all users, sorted by cost
entrydesk usage --limit 10 # Show top 10 users
entrydesk usage --sort-by operations # Sort by operation count
entrydesk usage --json # Output in JSON formatAgents Management
entrydesk agents # List available agents
entrydesk agents --json # Output in JSON format
entrydesk agents create --name "My Agent" --description "..." --system-prompt "..." --model-id <modelId>
entrydesk agents update <agentId> --name "New Name" --description "..." --system-prompt "..." --model-id <modelId>
entrydesk agents delete <agentId>
# Icons: give an agentId to apply the icon to that agent, or omit it to only print the iconId and URL
entrydesk agents icon generate <agentId> # AI icon from the agent's name and description
entrydesk agents icon generate --name "Bot" --description "..." # Icon for --icon-id on create/update
entrydesk agents icon upload ./icon.png <agentId> # PNG, JPEG, GIF, or WebP, up to 10MB
# generate falls back to the system prompt when the agent has no description.
# Applying an icon fails instead of overwriting if someone edits the agent meanwhile;
# the error prints the `agents update <agentId> --icon-id <iconId>` command to apply it.Meetings and Recordings
entrydesk meeting # List meetings
entrydesk meeting record https://meet.google.com/abc-defg-hij # Send a recording bot into the call now
entrydesk meeting get <meetingId> # Details and its recordings
entrydesk meeting summary <meetingId> # Print the summary as markdown
entrydesk meeting transcript <recordingId> # Print a transcript (--raw for timestamps)
entrydesk meeting share <meetingId> --add-user <userId> --role viewer
entrydesk recordings # List recordings
entrydesk recordings import https://example.com/audio.mp3 --title "Standup"
entrydesk recordings audio-url <recordingId> # Signed URL to stream the audio
entrydesk recordings retry <recordingId> # Retry a failed recordingWorkflows
entrydesk workflow # List workflows
entrydesk workflow create --name "Nightly report" --file ./report.js
entrydesk workflow run <workflowId> --args '{"date":"2026-06-20"}' --wait
entrydesk workflow runs <workflowId>
entrydesk workflow run-get <workflowId> <runId>Schedules
entrydesk schedules # List schedules
entrydesk schedules create --name "Daily digest" --prompt "Summarize yesterday" \
--agent-id <agentId> --starts-at 2026-07-01T09:00:00Z \
--repeat-type days --every 1 --time 09:00
entrydesk schedules run <scheduleId> # Run now; prints the created taskId
entrydesk schedules pause <scheduleId>
entrydesk schedules runs <scheduleId> # Run historyWebhooks
Outgoing webhook destinations, for environments where an operator has enabled them. Managing them needs the Manage workspace permission.
entrydesk webhook # List destinations
entrydesk webhook event-types # Event types you can subscribe to
entrydesk webhook create --url https://hooks.example.com/entrydesk --name BPM \
--events budget.limit_reached,budget.threshold_reached # or --all-events
entrydesk webhook test <id> # Send destination.test_sent now
entrydesk webhook deliveries <id> --status failed
entrydesk webhook delivery <id> <deliveryId>
entrydesk webhook resend <id> <deliveryId>
entrydesk webhook rotate-secret <id> --expire-previous 24h # or now
entrydesk webhook update <id> --name "BPM (prod)"
entrydesk webhook disable <id> # enable <id> resumes
entrydesk webhook delete <id> --yesWrite commands need the workspaces:write scope; a session from before this release lacks it, so run entrydesk login once. create and rotate-secret print the signing secret only once. Each request carries X-ED-Timestamp and X-ED-Signature: v1=<hex>, where the hex is HMAC-SHA256 of "{X-ED-Timestamp}.{raw body}".
Notifications
entrydesk notifications # List notifications
entrydesk notifications --tab action # Only items that need action
entrydesk notifications unread # Unread count
entrydesk notifications read <notificationId>
entrydesk notifications read-allMarketplace
entrydesk marketplace search # Browse agents and skills
entrydesk marketplace search --type agent
entrydesk marketplace show <slug>
entrydesk marketplace install <slug> --type agent
entrydesk marketplace roles # Role tags you can filter bysearch and show are public; install requires login and
CreateAgent/CreateSkill permission in the current workspace.
Billing
entrydesk billing # Plan, seats, and extra-usage balance
entrydesk billing invoices --limit 10Billing endpoints require an interactive login — a workspace PAT
(ENTRYDESK_TOKEN) cannot read them.
Configuration
Server URLs live in the profile. profile set writes the current profile;
add --profile <name> to write another one without switching. The older
entrydesk config list|get|set|unset still works but is deprecated: it prints a
warning naming the profile command to use and will be removed in a future
release.
entrydesk profile show
entrydesk profile set apiUrl http://localhost:3200
entrydesk profile set hubUrl http://localhost:3000 --profile local
entrydesk profile get apiUrl
entrydesk profile unset apiUrlWhen only hubUrl is set, the CLI asks that Hub where its API lives
(GET <hubUrl>/.well-known/entrydesk/client) the first time a command sends an
API request, and caches the answer per Hub in ~/.entrydesk/hub-discovery.json
for five minutes; the profile config is left untouched. Commands that never
call the API (profile, config, version) do not contact the Hub, and
entrydesk status shows the API as resolved on first request until then.
entrydesk logout does reach the API to revoke the session, so it resolves
the URL, and it clears the local credentials either way.
ENTRYDESK_API_URL and a configured apiUrl both take precedence and skip
discovery. If a Hub has no discovery endpoint, the CLI falls back to the
matching API for app.entrydesk.com, entrydesk.dcard.io and stage.gntc.io,
and fails the request with an error for any other Hub instead of sending it to
the production API — set apiUrl explicitly in that case. With neither URL
set, the CLI uses https://app.entrydesk.com and https://api.entrydesk.com.
Running Against a Local API
Point the CLI at a locally running API for one command, or create a profile
for it. A profile of its own keeps local credentials apart from the ones you
use against production, and create switches to it and logs in in one go:
ENTRYDESK_API_URL=http://localhost:3200 entrydesk status # one invocation
entrydesk profile create local \
--api-url http://localhost:3200 \
--hub-url http://localhost:3000 \
--loginIf the local API serves a self-signed certificate, add
ENTRYDESK_IGNORE_SSL_ERRORS=true. It turns off TLS verification for the whole
process, so use it only against a local endpoint.
Tool Call Timeouts
Tool calls (entrydesk tool call, and every call proxied by entrydesk mcp-server)
wait up to 5 minutes for a response, and a server that reports progress keeps
resetting that window up to an absolute cap of 30 minutes. Raise either limit
in milliseconds:
ENTRYDESK_MCP_TIMEOUT=900000 entrydesk tool call slow__tool -i '{}'
ENTRYDESK_MCP_MAX_TIMEOUT=3600000 entrydesk tool call slow__tool -i '{}'For entrydesk mcp-server, set them in the env block of the MCP server entry so
the proxy never times out before the client that launched it does.
These are client-side limits. A tool served by a workspace connector is also
capped at 5 minutes by the API, so raising ENTRYDESK_MCP_TIMEOUT beyond that
only extends how long the CLI waits on EntryDesk's own long-running tools.
Environment Variables
Environment variables take precedence over stored profile config, so a value set for a single invocation always wins.
| Variable | Purpose | Expected value |
| -------------------------------- | ----------------------------------------------------------- | ------------------- |
| ENTRYDESK_API_URL | Override the API base URL | URL |
| ENTRYDESK_HUB_URL | Override the Hub URL used for login and share links | URL |
| ENTRYDESK_TOKEN | Personal access token for non-interactive auth | token |
| ENTRYDESK_WORKSPACE_ID | Workspace the PAT acts in (required with ENTRYDESK_TOKEN) | workspace id |
| ENTRYDESK_FORCE_FILE_STORAGE | Skip the OS keychain, store credentials in a file | true |
| ENTRYDESK_IGNORE_SSL_ERRORS | Disable TLS verification (local development only) | true |
| ENTRYDESK_MCP_TIMEOUT | Per-tool-call timeout (see Tool Call Timeouts above) | milliseconds |
| ENTRYDESK_MCP_MAX_TIMEOUT | Absolute cap for progress-extended tool calls | milliseconds |
| ENTRYDESK_SANDBOX | Enable sandbox authentication | true |
| ENTRYDESK_SANDBOX_ACCESS_TOKEN | Sandbox credential (required when sandbox is enabled) | token |
| ENTRYDESK_SANDBOX_WORKSPACE_ID | Sandbox workspace (required when sandbox is enabled) | workspace id |
| ENTRYDESK_DEBUG | Log credential-storage/keychain decisions | true |
| DEBUG | Verbose request, stream, and stack-trace output | any non-empty value |
| NO_COLOR | Disable colored tool output | any non-empty value |
Non-Interactive Authentication
For CI and automation, authenticate with a personal access token instead of
entrydesk login. Both variables are required — setting only the token fails
with an explicit error:
export ENTRYDESK_TOKEN=<personal-access-token>
export ENTRYDESK_WORKSPACE_ID=<workspace-id>
entrydesk tasks --jsonNote that billing endpoints are not readable with a PAT; they require an
interactive login. A PAT for the project commands needs projects:read,
projects:write, tasks:read and tasks:write — project note,
context-search and context-read each require a projects:* and a tasks:*
scope together, and refuse with the missing ones named otherwise.
Profiles
entrydesk profile # List profiles
entrydesk profile create staging # Create a profile and switch to it
entrydesk profile create staging --hub-url https://stage.gntc.io --login
entrydesk profile create staging --no-use # Create without switching
entrydesk profile create staging --no-use --login # Log in to it, stay on the current one
entrydesk profile use staging # Switch active profile
entrydesk profile show staging # Show profile details (URLs, model, login)
entrydesk profile set apiUrl <url> # Set a value on the current profile
entrydesk profile clone default staging # Clone config defaults (no credentials)
entrydesk profile rename staging qa # Rename a profile (credentials follow)
entrydesk profile delete staging
# Run a single command against a specific profile without switching
entrydesk status --profile staging
entrydesk task -i --profile stagingDevelopment
pnpm --filter @entrydesk/cli dev
pnpm --filter @entrydesk/cli lint
pnpm --filter @entrydesk/cli typecheck