npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/cli

To 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 result

A 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 -i

Interactive task is also the default when no command is provided:

entrydesk

Authentication

entrydesk login    # OAuth device flow — prints a verification URL + code to enter
entrydesk status
entrydesk logout
entrydesk token    # Print a fresh access token for scripts

Scripting 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 connectors

Piping input and output:

echo "Hello" | entrydesk task --model <model>
entrydesk task -m "Hi" --model <model> --plain
entrydesk task -m "Hi" --model <model> --output stream-json

With --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 with Start-Process is 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 messages

The 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 self

Moving 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-14

context-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.md

What 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 prompt

create 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_agents

tool 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 format

Budget 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 format

Usage 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 format

Agents 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 recording

Workflows

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 history

Webhooks

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> --yes

Write 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-all

Marketplace

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 by

search 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 10

Billing 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 apiUrl

When 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 \
  --login

If 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 --json

Note 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 staging

Development

pnpm --filter @entrydesk/cli dev
pnpm --filter @entrydesk/cli lint
pnpm --filter @entrydesk/cli typecheck