@bastani/atomic 0.9.25-alpha.1 → 0.9.25-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/builtin/intercom/package.json +2 -2
  3. package/dist/builtin/mcp/CHANGELOG.md +13 -0
  4. package/dist/builtin/mcp/index.bundle.mjs +489 -208
  5. package/dist/builtin/mcp/package.json +2 -2
  6. package/dist/builtin/subagents/package.json +3 -3
  7. package/dist/builtin/web-access/package.json +2 -2
  8. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  9. package/dist/builtin/workflows/builtin/{chunk-e1254zvd.js → chunk-kfsnh3w7.js} +40 -12
  10. package/dist/builtin/workflows/builtin/{chunk-cpw5gtzg.js → chunk-rqmasd3d.js} +1 -1
  11. package/dist/builtin/workflows/builtin/{chunk-qy6g0baf.js → chunk-wz783pvd.js} +1 -1
  12. package/dist/builtin/workflows/builtin/goal.js +2 -2
  13. package/dist/builtin/workflows/builtin/index.js +3 -3
  14. package/dist/builtin/workflows/builtin/ralph.js +2 -2
  15. package/dist/builtin/workflows/package.json +2 -2
  16. package/dist/builtin/workflows/src/extension/index.bundle.mjs +472 -88
  17. package/dist/builtin/workflows/src/index.js +65 -28
  18. package/dist/core/agent-session-accessors.d.ts.map +1 -1
  19. package/dist/core/agent-session-accessors.js +7 -0
  20. package/dist/core/agent-session-accessors.js.map +1 -1
  21. package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
  22. package/dist/core/agent-session-extension-bindings.js +16 -2
  23. package/dist/core/agent-session-extension-bindings.js.map +1 -1
  24. package/dist/core/agent-session-methods.d.ts +7 -1
  25. package/dist/core/agent-session-methods.d.ts.map +1 -1
  26. package/dist/core/agent-session-methods.js.map +1 -1
  27. package/dist/core/agent-session-state.d.ts.map +1 -1
  28. package/dist/core/agent-session-state.js +3 -2
  29. package/dist/core/agent-session-state.js.map +1 -1
  30. package/dist/core/agent-session-types.d.ts +1 -0
  31. package/dist/core/agent-session-types.d.ts.map +1 -1
  32. package/dist/core/agent-session-types.js.map +1 -1
  33. package/dist/core/agent-session.d.ts +3 -0
  34. package/dist/core/agent-session.d.ts.map +1 -1
  35. package/dist/core/agent-session.js +6 -0
  36. package/dist/core/agent-session.js.map +1 -1
  37. package/dist/core/builtin-resource-loader.d.ts.map +1 -1
  38. package/dist/core/builtin-resource-loader.js +13 -1
  39. package/dist/core/builtin-resource-loader.js.map +1 -1
  40. package/dist/core/extensions/api-types.d.ts +3 -0
  41. package/dist/core/extensions/api-types.d.ts.map +1 -1
  42. package/dist/core/extensions/api-types.js.map +1 -1
  43. package/dist/core/extensions/index.d.ts +2 -0
  44. package/dist/core/extensions/index.d.ts.map +1 -1
  45. package/dist/core/extensions/index.js +1 -0
  46. package/dist/core/extensions/index.js.map +1 -1
  47. package/dist/core/extensions/loader-api.d.ts.map +1 -1
  48. package/dist/core/extensions/loader-api.js +11 -0
  49. package/dist/core/extensions/loader-api.js.map +1 -1
  50. package/dist/core/extensions/loader-runtime.d.ts.map +1 -1
  51. package/dist/core/extensions/loader-runtime.js +3 -0
  52. package/dist/core/extensions/loader-runtime.js.map +1 -1
  53. package/dist/core/extensions/mcp-server-registry.d.ts.map +1 -1
  54. package/dist/core/extensions/mcp-server-registry.js +4 -0
  55. package/dist/core/extensions/mcp-server-registry.js.map +1 -1
  56. package/dist/core/extensions/runner.d.ts +3 -0
  57. package/dist/core/extensions/runner.d.ts.map +1 -1
  58. package/dist/core/extensions/runner.js +4 -0
  59. package/dist/core/extensions/runner.js.map +1 -1
  60. package/dist/core/extensions/runtime-types.d.ts +3 -0
  61. package/dist/core/extensions/runtime-types.d.ts.map +1 -1
  62. package/dist/core/extensions/runtime-types.js.map +1 -1
  63. package/dist/core/extensions/tool-types.d.ts +1 -0
  64. package/dist/core/extensions/tool-types.d.ts.map +1 -1
  65. package/dist/core/extensions/tool-types.js.map +1 -1
  66. package/dist/core/extensions/workflow-run-control-hub.d.ts +23 -0
  67. package/dist/core/extensions/workflow-run-control-hub.d.ts.map +1 -0
  68. package/dist/core/extensions/workflow-run-control-hub.js +57 -0
  69. package/dist/core/extensions/workflow-run-control-hub.js.map +1 -0
  70. package/dist/core/extensions/workflow-run-control.d.ts +163 -0
  71. package/dist/core/extensions/workflow-run-control.d.ts.map +1 -0
  72. package/dist/core/extensions/workflow-run-control.js +73 -0
  73. package/dist/core/extensions/workflow-run-control.js.map +1 -0
  74. package/dist/core/mandatory-resource-loader.d.ts.map +1 -1
  75. package/dist/core/mandatory-resource-loader.js +9 -0
  76. package/dist/core/mandatory-resource-loader.js.map +1 -1
  77. package/dist/core/mcp-servers.d.ts +4 -1
  78. package/dist/core/mcp-servers.d.ts.map +1 -1
  79. package/dist/core/mcp-servers.js.map +1 -1
  80. package/dist/core/oauth-provider-metadata.js +1 -1
  81. package/dist/core/oauth-provider-metadata.js.map +1 -1
  82. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  83. package/dist/core/remote-catalog-provider.js +4 -9
  84. package/dist/core/remote-catalog-provider.js.map +1 -1
  85. package/dist/core/resource-loader-types.d.ts +4 -0
  86. package/dist/core/resource-loader-types.d.ts.map +1 -1
  87. package/dist/core/resource-loader-types.js.map +1 -1
  88. package/dist/core/sdk.js +1 -0
  89. package/dist/core/sdk.js.map +1 -1
  90. package/dist/core/structured-output/index.d.ts.map +1 -1
  91. package/dist/core/structured-output/index.js +3 -1
  92. package/dist/core/structured-output/index.js.map +1 -1
  93. package/dist/core/tool-loadout.d.ts +1 -0
  94. package/dist/core/tool-loadout.d.ts.map +1 -1
  95. package/dist/core/tool-loadout.js +3 -0
  96. package/dist/core/tool-loadout.js.map +1 -1
  97. package/dist/core/tools/bash.d.ts.map +1 -1
  98. package/dist/core/tools/bash.js +11 -30
  99. package/dist/core/tools/bash.js.map +1 -1
  100. package/dist/extensions/codemode/execute.d.ts.map +1 -1
  101. package/dist/extensions/codemode/execute.js +26 -0
  102. package/dist/extensions/codemode/execute.js.map +1 -1
  103. package/dist/extensions/codemode/renderer.d.ts.map +1 -1
  104. package/dist/extensions/codemode/renderer.js +38 -23
  105. package/dist/extensions/codemode/renderer.js.map +1 -1
  106. package/dist/extensions/codemode/tool.d.ts.map +1 -1
  107. package/dist/extensions/codemode/tool.js +22 -10
  108. package/dist/extensions/codemode/tool.js.map +1 -1
  109. package/dist/extensions/tool-search/tool.d.ts +1 -1
  110. package/dist/extensions/tool-search/tool.d.ts.map +1 -1
  111. package/dist/extensions/tool-search/tool.js +3 -13
  112. package/dist/extensions/tool-search/tool.js.map +1 -1
  113. package/dist/index-extensions.d.ts +2 -1
  114. package/dist/index-extensions.d.ts.map +1 -1
  115. package/dist/index-extensions.js +1 -1
  116. package/dist/index-extensions.js.map +1 -1
  117. package/dist/modes/interactive/components/visual-truncate.d.ts +27 -3
  118. package/dist/modes/interactive/components/visual-truncate.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/visual-truncate.js +39 -6
  120. package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
  121. package/docs/extensions/events.md +2 -0
  122. package/docs/mcp-servers.md +47 -0
  123. package/docs/providers.md +9 -1
  124. package/docs/sdk/reference.md +22 -0
  125. package/docs/sdk/structured-decisions.md +2 -0
  126. package/docs/sdk.md +67 -1
  127. package/docs/settings.md +2 -0
  128. package/docs/tools.md +6 -0
  129. package/docs/workflows/operations.md +3 -1
  130. package/examples/extensions/built-in-tool-renderer.ts +16 -40
  131. package/examples/extensions/minimal-mode.ts +28 -108
  132. package/npm-shrinkwrap.json +69 -69
  133. package/package.json +8 -8
@@ -7,6 +7,12 @@ description: "Configure MCP servers, discover tools, and authenticate connection
7
7
 
8
8
  Atomic includes MCP support in both npm and binary installations. No separate extension install is needed. Use `/mcp` to inspect servers or `/mcp setup` to configure them.
9
9
 
10
+ ## Quick setup
11
+
12
+ Run `atomic`, then use `/mcp setup` to create configuration or preview imports from another client. For manual setup, add a local server's `command` and `args`, or a remote server's `url`, to one of the configuration files below. Restart Atomic after editing a file manually.
13
+
14
+ Use `/mcp` to inspect connections and `/mcp tools` to list tools. Servers connect lazily by default; discovery or a tool call can establish the connection. Use `/mcp reconnect my-server` to reconnect a configured server. For a server that requires OAuth, run `/mcp-auth my-server` before unattended work.
15
+
10
16
  ## Configure a server
11
17
 
12
18
  Put shared project configuration in `.mcp.json` at your project root:
@@ -55,6 +61,17 @@ The Atomic agent directory can be relocated with `ATOMIC_CODING_AGENT_DIR`. Use
55
61
 
56
62
  Servers connect lazily by default. Adding a server does not require an immediate connection at startup.
57
63
 
64
+ ### Configuration rules
65
+
66
+ - `command` is one executable and `args` contains its arguments, not one shell command string. Use `cwd` for the server's working directory and `env` for its environment.
67
+ - Remote servers use `url` and optional `headers`. Keep credentials out of shared files; use environment variable references or `bearerTokenEnv` for bearer authentication.
68
+ - Put personal servers and credentials in user-level configuration. Use project configuration only for servers the project requires, in trusted projects.
69
+ - Use `timeoutMs` for the per-request inactivity timeout. Progress notifications reset it.
70
+
71
+ ### Import configuration from another client
72
+
73
+ Clients that use a top-level `mcpServers` object can share the same server entries. Copy compatible entries into `.mcp.json`, or use `/mcp setup` to preview detected imports before saving. Check executable paths, working directories, and required environment variables on the current machine. Restart Atomic after manual changes, then use `/mcp` to diagnose connection errors.
74
+
58
75
  ## Servers from packages and extensions
59
76
 
60
77
  Installed [packages](/packages/authoring#mcp-servers) and extensions can contribute MCP servers. Contributed servers sit below all four configuration files: a server with the same name in any of them replaces the contributed entry completely. To turn a contributed server off, give its name an entry with only `disabled`:
@@ -86,6 +103,8 @@ mcp({ tool: "my_server_search", args: '{"query":"example"}' })
86
103
 
87
104
  Use the tool names returned by discovery. `args` is a JSON string, not an object. Search may connect configured servers when their metadata has not yet been cached.
88
105
 
106
+ Tool names replace punctuation with `_` and receive a leading `_` if they start with a digit. Names longer than 64 characters and tools whose names collide after this conversion receive a deterministic hash suffix. Both gateway discovery and direct tools use these names; call the returned name rather than constructing one yourself. Server names that differ only in `-` and `_` are rejected within one configuration file. A higher-precedence file replaces a contributed or lower-precedence server with the same normalized name.
107
+
89
108
  To expose a server's tools directly in the agent's tool list, add `"directTools": true` to that server's configuration. To expose only selected tools, set `directTools` to an array of the original MCP tool names. The default is gateway-only access.
90
109
 
91
110
  In a headless SDK session, cached direct tools are available at startup, but discovery does not connect uncached lazy servers. Call the `mcp` gateway when you need them. Set the server's `lifecycle` to `"eager"` or `"keep-alive"` if it must connect during startup.
@@ -94,8 +113,36 @@ In a headless SDK session, cached direct tools are available at startup, but dis
94
113
 
95
114
  For an OAuth server, run `/mcp-auth my-server` in an interactive session. You can also select the server in `/mcp` and press Enter or `Ctrl+A`. Run `/mcp logout my-server` to remove stored OAuth credentials and disconnect.
96
115
 
116
+ While sign-in waits for browser approval, Atomic displays the authorization URL as a terminal hyperlink with a Cmd/Ctrl+click hint. You can use it if the browser did not open automatically.
117
+
97
118
  Automatic OAuth is opt-in through `settings.autoAuth`. Browser-based authorization requires an interactive session; authenticate before running unattended work.
98
119
 
120
+ Atomic registers OAuth clients as `atomic`. If a server requires a known client name, configure it with `oauth.clientName`:
121
+
122
+ ```json
123
+ { "mcpServers": { "figma": { "url": "https://mcp.figma.com/mcp", "oauth": { "clientName": "Claude Code" } } } }
124
+ ```
125
+
126
+ The name is sent only during dynamic client registration. Run `/mcp logout figma` before signing in again to register under a changed name.
127
+
128
+ ### Authenticate with a provider login
129
+
130
+ An HTTP server can use the token of a provider you have signed in to instead of MCP OAuth:
131
+
132
+ ```json
133
+ { "mcpServers": { "radius": { "url": "https://radius.example/mcp", "auth": { "provider": "radius" } } } }
134
+ ```
135
+
136
+ Atomic reads the provider's current token for each request, so refreshes apply. MCP does not copy or store the token. If the server rejects it or the provider has no token, the connection fails with a message to run `/login <provider>`.
137
+
138
+ After signing in with `/login <provider>`, retry the MCP gateway call. Atomic reconnects with the current token, including for servers with cached tools; you do not need to run `/mcp reconnect`.
139
+
140
+ Because the token goes to the server's `url`, `auth.provider` has limits:
141
+
142
+ - It is accepted in the global `mcp.json` files and in servers registered by extensions. It is ignored, with a warning, in project `.mcp.json` and `.atomic/mcp.json`, in project-relative imports such as `.vscode/mcp.json`, and in package manifest servers.
143
+ - The URL must use `https`, or `http` on `localhost`, `127.0.0.1`, or `[::1]`.
144
+ - Atomic sends the token only to the server's origin. It follows `307` and `308` redirects within that origin and refuses every other redirect.
145
+
99
146
  ## Troubleshooting
100
147
 
101
148
  - Run `/mcp` to check server status and `/mcp tools` to list available tools.
package/docs/providers.md CHANGED
@@ -61,7 +61,7 @@ Checks refresh expired OAuth credentials by default through the ordinary locked
61
61
 
62
62
  Run `/login openai`, then choose **API key** or **Sign in with ChatGPT**. The ChatGPT option authenticates the `openai` provider for requests to OpenAI's Responses API; it is separate from `/login openai-codex`, which uses the Codex backend. Choose the provider/model identity appropriate to the account and endpoint you intend to use.
63
63
 
64
- Complete the browser callback, or paste the full callback URL when prompted on a remote machine. Atomic creates a global device identity on first use of this sign-in; project settings do not override it. A saved subscription credential does not verify model entitlement or remaining usage.
64
+ Complete the browser callback, or paste the full callback URL into the login dialog on a remote machine. If your browser cannot reach `127.0.0.1:1455`, copy the final URL from its address bar, including `code`, `state`, and `client_id`, and paste it into Atomic. Atomic creates a global device identity on first use of this sign-in; project settings do not override it. A saved subscription credential does not verify model entitlement or remaining usage.
65
65
 
66
66
  ### OpenAI Codex
67
67
 
@@ -124,12 +124,20 @@ Anthropic fast mode delivers up to 2.5x higher output tokens per second at twice
124
124
 
125
125
  Anthropic subscription auth is active for Claude Pro/Max accounts. Third-party harness usage draws from [extra usage](https://claude.ai/settings/usage) and is billed per token, not against Claude plan limits.
126
126
 
127
+ Run `/login anthropic` and choose subscription authentication. **Browser login (default)** uses a local callback and still accepts a pasted redirect URL. Choose **Copy code login (headless)** when your browser runs on another machine: complete sign-in in that browser, then paste the `code#state` value Anthropic displays into Atomic. This method does not need a reachable local callback.
128
+
127
129
  For gateway-issued Anthropic bearer credentials, set `ANTHROPIC_AUTH_TOKEN` without `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN`. A populated bearer token counts as configured Anthropic authentication, so `/model`, saved/default selection, cycling, RPC catalogs, and isolated model pickers keep Anthropic models available. Atomic sends it as `Authorization: Bearer …` for normal turns, branch summaries, and Verbatim Compaction without replacing caller-supplied custom headers.
128
130
 
129
131
  Claude Opus 5 is available from the bundled/dynamic Anthropic and Amazon Bedrock catalogs. With bearer-only Anthropic auth, select the exact `anthropic/claude-opus-5-*` entry through `/model`; Bedrock uses its catalog-advertised inference profile. `xhigh` appears only when the chosen entry advertises it. Bedrock requests retain adaptive thinking, prompt caching, and AWS validation/error details from the provider runtime.
130
132
 
131
133
  `ANTHROPIC_AUTH_TOKEN` is specifically for Anthropic-compatible gateways that require a bearer header. It does not synthesize an API key or `x-api-key`, and callers may still add independent custom headers/base URLs through `models.json` or an extension. Empty environment variables do not count as configured. If token and API-key sources are both configured, normal credential resolution rules apply; avoid setting both accidentally.
132
134
 
135
+ ### Anthropic workload identity federation
136
+
137
+ With no Anthropic key, bearer token, or login configured, Atomic uses workload identity federation when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, and `ANTHROPIC_IDENTITY_TOKEN_FILE` are all set. The Anthropic SDK exchanges the identity token for a short-lived access token and refreshes it, re-reading the token file each time, so keep that file fresh in long sessions. `ANTHROPIC_SERVICE_ACCOUNT_ID` and `ANTHROPIC_WORKSPACE_ID` are sent when set.
138
+
139
+ Keys, `ANTHROPIC_AUTH_TOKEN`, and custom authorization headers take precedence. Federation applies only to the `anthropic` provider, not to other Anthropic-compatible providers.
140
+
133
141
  ### GitHub Copilot
134
142
 
135
143
  - Press Enter for github.com, or enter your GitHub Enterprise Server domain
@@ -666,6 +666,12 @@ const themes = loader.getThemes();
666
666
  const contextFiles = loader.getAgentsFiles().agentsFiles;
667
667
  ```
668
668
 
669
+ Package workflows returned by `DefaultResourceLoader` are available through the SDK session's `workflow` tool, even when the factory adds the shipped workflows extension. Use the tool's `reload` action to pick up added or removed package workflows without recreating the session. Project packages still require project trust; workflow reload does not grant it.
670
+
671
+ Custom `ResourceLoader` implementations can optionally provide `getWorkflowResources()` returning `ResolvedResource[]` and `refreshWorkflowResources()` returning `Promise<ResolvedResource[]>`. Refresh should return the current trust-filtered resources. If refresh is omitted, workflow reload uses the getter's current snapshot. If both are omitted, shipped workflows and normal workflow-directory discovery remain available.
672
+
673
+ Package MCP servers from `DefaultResourceLoader` remain available through the session's `mcp` tool when the factory adds the shipped MCP extension. Custom loaders can provide `getMcpServerContributions()` returning `McpServerContribution[]`; return only contributions allowed by your loader's trust policy. Normal MCP configuration overrides still apply. See [MCP servers](/mcp-servers) for connection and tool invocation.
674
+
669
675
  ## Return Value
670
676
 
671
677
  `createAgentSession()` returns:
@@ -843,6 +849,22 @@ type EditDiffResult
843
849
  SessionManager
844
850
  SettingsManager
845
851
 
852
+ // Workflow run control (session.workflows)
853
+ type SessionWorkflows
854
+ type WorkflowRunSummary
855
+ type WorkflowRunDetail
856
+ type WorkflowRunControlOutcome
857
+ type WorkflowRunControlFailedRun
858
+ WorkflowRunControlError
859
+ WorkflowRunNotFoundError
860
+ WorkflowRunOwnershipError
861
+ WorkflowRunNotResumableError
862
+ WorkflowStageNotFoundError
863
+ WorkflowStageAmbiguousError
864
+ WorkflowStageResumeUnsupportedError
865
+ WorkflowRunDatabaseError
866
+ WorkflowRunControlUnavailableError
867
+
846
868
  // Tool factories
847
869
  createCodingTools
848
870
  createReadOnlyTools
@@ -73,6 +73,8 @@ Automatic routing builds its own finite Choice questions from eligible execution
73
73
 
74
74
  Chat candidates receive one internal result tool whose parameters are the registered schema. Atomic uses provider-aware serialization, requests strict sampling where supported, and validates the returned arguments without coercing values or removing extra fields. Use `additionalProperties: false` for closed objects. A prose-only response, extra tool call, truncated response, or invalid result fails that candidate and advances the chain. The public `structured_output` tool is separate: its parameters are `instructions`, `state`, and optional model selectors, and its result is the inferred value.
75
75
 
76
+ Use `Type.Optional(...)` for result fields that may be omitted. When a strict provider represents an omitted optional object property as `null`, Atomic removes that placeholder if the property's schema rejects null, including declared properties inside nested objects and array items. If null is a meaningful result, explicitly allow it in the property schema; Atomic preserves those null values. Required properties and undeclared fields still follow the original schema.
77
+
76
78
  Chat decisions use HTTP/SSE rather than WebSocket transport fallback and disable configured Anthropic server-side fallbacks for this request only. The chat decision layer retries transient failures according to the request's retry settings. Classifier calls use the registered provider's `classify` operation and forward the retry limit and cancellation signal; provider implementations own their transport retries.
77
79
 
78
80
  A registered classifier receives shared state and choice questions through the generic classify operation. [TypeSafe Jev](/providers#typesafe-jev) is one such classifier. Router classification is one classify request. A provider that rejects the request size or context fails that candidate; routing then uses the current chat model when one exists.
package/docs/sdk.md CHANGED
@@ -210,7 +210,7 @@ To continue a saved run in another session:
210
210
 
211
211
  1. Keep its definition and durable storage available.
212
212
  2. Bind the new host.
213
- 3. Use `/workflow resume <run-id>` or the workflow tool's `resume` action.
213
+ 3. Use `session.workflows.resume(runId)` ([Workflow run control](#workflow-run-control)), `/workflow resume <run-id>`, or the workflow tool's `resume` action.
214
214
 
215
215
  Rebinding alone does not reopen a saved run. See [workflow operations](/workflows/operations) for inspection, graceful quit, and resume.
216
216
 
@@ -226,6 +226,69 @@ Rebinding host callbacks preserves pending MCP OAuth ownership. Authorization st
226
226
 
227
227
  Web provider configuration caches are session-local. Configuration discovery and environment keys are unchanged; create a new session to pick up changed cached settings. GitHub extraction returns the owned clone path for use with file tools, so callers need not predict its directory.
228
228
 
229
+ ### Workflow run control
230
+
231
+ `session.workflows` manages the workflow runs that this session owns, without slash commands or the model-facing `workflow` tool. It uses the same run-control service as the tool, so ownership and durability rules are identical: a run owned by another session, or executing in another live Atomic process, stays read-only here.
232
+
233
+ ```typescript
234
+ interface SessionWorkflows {
235
+ listRuns(filter?: { status?: WorkflowRunFilterStatus }): Promise<readonly WorkflowRunSummary[]>;
236
+ getRun(runId: string): Promise<WorkflowRunDetail>;
237
+ getStages(runId: string, filter?: { status?: WorkflowStageStatus }): Promise<readonly WorkflowRunStageSummary[]>;
238
+ pause(runId: string, options?: { stageId?: string }): Promise<WorkflowRunControlOutcome>;
239
+ pause(target: { all: true }): Promise<WorkflowRunControlOutcome>;
240
+ quit(target: string | { all: true }): Promise<WorkflowRunControlOutcome>;
241
+ resume(runId: string, options?: { stageId?: string; message?: string }): Promise<WorkflowRunControlOutcome>;
242
+ }
243
+ ```
244
+
245
+ `listRuns()` returns the same per-run data as `workflow status`: id, workflow name, status, active stages, and unanswered prompts (`awaitingInput`). Run IDs accept the full UUID or a unique 8-character prefix. `getRun()`, `getStages()`, `pause()` and `quit()` resolve a prefix against the runs this session holds and reject it at once when none matches, so pass the full UUID for a run recorded by an earlier process. `pause`, `quit` and `resume` resolve with the tool's acknowledged outcome: `action`, `runId` (`--all` for a batch), a `status` of `ok`, `running`, `paused`, `partial`, `noop` or `cancelled`, and a human-readable `message`. A `noop` means the request was understood and found nothing to change, such as pausing a run that already ended. A `partial` outcome means part of the request took effect. For `pause({ all: true })` and `quit({ all: true })`, `partial` means this call stopped at least one run and others are still active: `failedRuns` lists each run that is still active with its `runId`, a `reason` (for example `pause_failed` or `no_active_stages`) and a `message`, so check `outcome.status === "partial"` before treating a batch stop as complete. If active runs could not be stopped and this call stopped none, the call rejects with `WorkflowRunControlError` (`WORKFLOW_RUN_CONTROL_FAILED`) whose `failedRuns` carries the same list, even when other runs were already paused. Runs that are already paused or already ended are not failures, and a batch with no active run left to stop resolves as a `noop`. Call `getRun()` before retrying.
246
+
247
+ Pausing a run doesn't park a stage that is waiting on a prompt. Such a run reports `paused` and counts as already paused, because nothing in it advances on its own. If your host then answers that prompt, the stage can continue its turn before you call `resume`. To keep a paused run fully stopped, hold prompt answers until you resume it.
248
+
249
+ Requests that cannot be carried out reject with a subclass of `WorkflowRunControlError`. Branch on `code` or use `instanceof`:
250
+
251
+ | Class | `code` | Meaning |
252
+ | --- | --- | --- |
253
+ | `WorkflowRunNotFoundError` | `WORKFLOW_RUN_NOT_FOUND` | The run ID is unknown or malformed, or a prefix is ambiguous or matches no run in this session. |
254
+ | `WorkflowRunOwnershipError` | `WORKFLOW_RUN_OWNED_ELSEWHERE` | The run belongs to another session, or is running in another live Atomic process. Control it from its owner. |
255
+ | `WorkflowRunNotResumableError` | `WORKFLOW_RUN_NOT_RESUMABLE` | The run completed, was killed, or has no durable progress to resume. |
256
+ | `WorkflowStageNotFoundError` | `WORKFLOW_STAGE_NOT_FOUND` | `stageId` matches no stage of the run, or the stage has no control to pause. |
257
+ | `WorkflowStageAmbiguousError` | `WORKFLOW_STAGE_AMBIGUOUS` | `stageId` matches more than one stage; pass a more specific identifier. |
258
+ | `WorkflowStageResumeUnsupportedError` | `WORKFLOW_STAGE_RESUME_UNSUPPORTED` | A durable run resumes as a whole; call `resume()` without `stageId`. |
259
+ | `WorkflowRunDatabaseError` | `WORKFLOW_RUN_DATABASE` | Workflow durability is unavailable, including when the database cannot be inspected to check a run's owner during `pause()` or `quit()`. Restore the database, then inspect the run before retrying. |
260
+ | `WorkflowRunControlError` | `WORKFLOW_RUN_CONTROL_FAILED` | Another failure; `message` explains it. |
261
+ | `WorkflowRunControlUnavailableError` | `WORKFLOW_RUN_CONTROL_UNAVAILABLE` | The workflows package is disabled, or the session was disposed. |
262
+
263
+ Each error carries the `runId` when one applies. A refused batch also carries `failedRuns`.
264
+
265
+ A host with a stop button can pause its session's runs and resume the same runs later:
266
+
267
+ ```typescript
268
+ import { createAgentSession, WorkflowRunControlError } from "@bastani/atomic";
269
+
270
+ const { session } = await createAgentSession();
271
+
272
+ async function onStopClicked() {
273
+ const outcome = await session.workflows.pause({ all: true });
274
+ showStatus(outcome.message);
275
+ }
276
+
277
+ async function onResumeClicked() {
278
+ for (const run of await session.workflows.listRuns({ status: "paused" })) {
279
+ try {
280
+ const outcome = await session.workflows.resume(run.runId);
281
+ showStatus(`${run.name}: ${outcome.message}`);
282
+ } catch (error) {
283
+ if (!(error instanceof WorkflowRunControlError)) throw error;
284
+ showStatus(`${run.name}: ${error.code}: ${error.message}`);
285
+ }
286
+ }
287
+ }
288
+ ```
289
+
290
+ `pause` holds the live run and keeps it resumable in this process; `quit` retires the executor at a durability boundary so a later process can resume it. After `quit`, or when resuming a run from a previous process, `resume` follows the durable resume path: keep the workflow definition and durable storage available. See [pausing, quitting, and resuming](/workflows/operations#pausing-quitting-and-resuming) for what each action guarantees. To watch runs change instead of polling `listRuns()`, use the observation contract in [Workflow activity and lifecycle hooks](/extensions/events#workflow-activity-and-lifecycle-hooks).
291
+
229
292
  ### Workflow and subagent children
230
293
 
231
294
  Children inherit the invoking session's model/auth runtime, settings, agent directory, human-input callbacks, and diagnostic sink. Choosing a child model or fallback does not switch to global credentials.
@@ -275,6 +338,9 @@ interface AgentSession {
275
338
  sessionFile: string | undefined;
276
339
  sessionId: string;
277
340
 
341
+ // Typed control of the workflow runs this session owns
342
+ readonly workflows: SessionWorkflows;
343
+
278
344
  // Model and thinking control
279
345
  setModel(model: Model): Promise<void>;
280
346
  setThinkingLevel(level: ThinkingLevel): void;
package/docs/settings.md CHANGED
@@ -476,6 +476,8 @@ A list containing only modifiers starts from Atomic's standard defaults. Modifie
476
476
 
477
477
  Plain names establish a replacement list before its modifiers apply. A project list of only modifiers layers over the global selection. A project list with plain names, or an empty list, replaces the global selection. For example, global `["read", "bash"]` plus project `["-bash", "+ls"]` resolves to `["read", "ls"]`.
478
478
 
479
+ `/reload` enables tools newly added to `defaultTools`. It does not disable removed tools or re-enable unchanged tools you turned off during the session. Explicit `--tools`, `--no-tools`, and `--no-builtin-tools` choices override the setting on reload too; excluded tools remain excluded.
480
+
479
481
  `codemode` and `tool_search` are built-in extension tools registered inactive. Add `"+codemode"` or `"+tool_search"` to `defaultTools` to activate them alongside ordinary defaults. See [codemode](/tools#codemode) and [tool search](/tools#tool_search) for usage and safety boundaries.
480
482
 
481
483
  ### Sessions
package/docs/tools.md CHANGED
@@ -41,16 +41,22 @@ Use `text(value)`, `image(dataUrlOrImageContent)`, `console.log(...)`, or a top-
41
41
  return await tools.read({ path: "README.md" });
42
42
  ```
43
43
 
44
+ `image()` accepts base64 PNG, JPEG, GIF, or WebP data URLs and MCP image content. It detects the MIME type from the data, ignores an incorrect supplied type, and rejects malformed base64 or unsupported formats before returning an image. Remote HTTP image URLs are not supported; fetch the image through a permitted tool first.
45
+
44
46
  There is no default deadline. Text output defaults to 10,000 estimated tokens; longer output keeps its start and end and, when saving succeeds, names a temporary file containing the full text. Failed scripts retain partial output and report the error. Unawaited calls are cancelled when a script ends; await work you need to finish.
45
47
 
46
48
  Tools with `outputSchema` return their `structuredContent` to scripts, including structured error results. Other tools return text and throw on errors. Completed `bash` calls provide structured output as described in the [SDK reference](/sdk/reference#bash-tool-behavior).
47
49
 
48
50
  `ALL_TOOLS`, `searchTools(query, { limit, namespace })`, and `describeTool(name)` discover the currently permitted tools. `store(key, value)` and `load(key)` preserve JSON values across successful calls on the current session branch, including after resume. Failed scripts discard store writes, not tool side effects. `models.getModelsOfType`, `getAvailableOfType`, and `getModelOfType` expose public catalog metadata; `models.classify(model, context)` calls a registered classifier with host-resolved credentials, at most four concurrently. Reported classifier usage contributes to the script's session cost.
49
51
 
52
+ Use `await describeNamespace(name)` to retrieve a callable tool namespace's `{ name, description?, instructions?, tools }`, or `undefined` when it is unavailable. Namespace instructions are returned on request rather than included in inline listings. `searchTools()` also searches those instructions; pass `{ namespace: name }` to restrict the search.
53
+
50
54
  Set [codemode settings](/settings#tools) to control inline declaration size and whether ordinary direct tool declarations remain visible. Atomic's existing [MCP host](/mcp-servers) remains the only MCP connection/configuration owner: codemode calls the permitted gateway or direct tools, not a second MCP host. Use MCP discovery for server tools not yet registered in the session.
51
55
 
52
56
  ## `tool_search`
53
57
 
58
+ Deferred tools and their namespaces are omitted from codemode's inline catalog. Discover them with `searchTools()`, `describeNamespace()`, or `ALL_TOOLS`; `tool_search` can load matching registered tools for direct calls.
59
+
54
60
  `tool_search` is inactive by default. Enable it with `"defaultTools": ["+tool_search"]` or include it in `--tools`. It ranks currently registered `codemode` and `deferred` tools by their metadata and activates matching tools for the next model call. It does not bypass session tool restrictions or replace MCP server discovery.
55
61
 
56
62
  ## `code_search`
@@ -372,6 +372,8 @@ This reopens only the conversation. The workflow DAG and terminal stage snapshot
372
372
 
373
373
  Workflow stages and their subagent transcripts are excluded from ordinary `/resume`, `atomic -r`, `--continue`, and global history. Use workflow inspection and resume commands for stages; terminal subagents remain transcript artifacts, not resumable children. An explicit `--session` file path can still open a stage transcript. Older unmarked workflow sessions may remain in ordinary history.
374
374
 
375
+ Embedding hosts can run the same pause, quit, and resume actions programmatically with `session.workflows`; see [Workflow run control](/sdk#workflow-run-control).
376
+
375
377
  ## Workflow activity for extensions
376
378
 
377
379
  Extensions can subscribe with `ctx.observeWorkflowActivity(observer)` and use the typed `workflow_lifecycle`, `workflow_activity_changed`, `workflow_stage_completed`, and `workflow_heartbeat` hooks. See [Workflow activity and lifecycle hooks](/extensions/events#workflow-activity-and-lifecycle-hooks) for the public types and subscription example.
@@ -470,7 +472,7 @@ If a resume continuation reports `cleanup skipped: database admission unavailabl
470
472
 
471
473
  While root registration is pending, status shows `pending` with phase `starting`, not healthy execution. Status listing and exact-run detail include `phase`, `phaseAgeMs`, `dependencyError`, and `lastProgressAt`. Last progress records admission or a tracked node starting/settling, not polling or a heartbeat. Database failures report `blocked_dependency`; a paused run still reports `paused`. Starting and dependency-blocked runs do not emit "still running" heartbeats.
472
474
 
473
- For an admitted live executor with no active stage or tool, pause installs a local barrier immediately and acknowledges within 500ms of starting database settlement. The acknowledgement and status distinguish `observed` from `durable` control persistence. If confirmation is still pending at acknowledgement, settlement continues in the background for up to 10 seconds total. Status becomes `durable` on success; only a settlement failure or deadline reports `blocked_dependency`. `observed` is not proof of a saved pause and does not guarantee cross-process resume. During registration, pause acknowledges immediately while database settlement continues in the background. After registration, resume allows up to 10 seconds for database confirmation before releasing the barrier. A failed resume leaves the executor paused; restore database availability and retry the same run. These limits do not cover stage cancellation or arbitrary external I/O.
475
+ For an admitted live executor with no active stage or tool, pause installs a local barrier immediately and acknowledges within 500ms of starting database settlement. The acknowledgement and status distinguish `observed` from `durable` control persistence. If confirmation is still pending at acknowledgement, settlement continues in the background for up to 10 seconds total. Status becomes `durable` on success; only a settlement failure or deadline reports `blocked_dependency`. `observed` is not proof of a saved pause and does not guarantee cross-process resume. During registration, pause acknowledges immediately while database settlement continues in the background. After registration, resume allows up to 10 seconds for database confirmation before releasing the barrier, and a resume requested while the pause's confirmation is still pending waits for it within that same 10 seconds instead of cancelling it. A failed resume leaves the executor paused; restore database availability and retry the same run. These limits do not cover stage cancellation or arbitrary external I/O.
474
476
 
475
477
  If registration times out or reports an unavailable database while the run is paused, restore the existing database without resetting data, then resume that same run in the same Atomic process. Resume retries registration with the original identity and releases the existing executor once; repeated concurrent resume requests share the retry and do not start another executor. If the run was not paused, admission failure ends the attempt. A non-retryable admission rejection, such as a permission error, is reported on resume instead of retrying the live executor. Retain the full run ID and inspect it before retrying. Completed checkpoints remain reusable, but an external side effect whose checkpoint was never confirmed can still repeat after a crash; make such operations idempotent.
476
478
 
@@ -12,8 +12,8 @@
12
12
  *
13
13
  * How it works:
14
14
  * - registerTool() with the same name as a built-in replaces it entirely
15
- * - We create instances of the original tools via createReadTool(), etc.
16
- * and delegate execute() to them
15
+ * - We create the original definitions via createReadToolDefinition(), etc.
16
+ * and override only their renderers
17
17
  * - renderCall() controls what's shown when the tool is invoked
18
18
  * - renderResult() controls what's shown after execution completes
19
19
  * - renderShell: "self" lets a tool render its own outer shell instead of
@@ -26,23 +26,21 @@
26
26
  */
27
27
 
28
28
  import type { BashToolDetails, EditToolDetails, ExtensionAPI, ReadToolDetails } from "@bastani/atomic";
29
- import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@bastani/atomic";
29
+ import {
30
+ createBashToolDefinition,
31
+ createEditToolDefinition,
32
+ createReadToolDefinition,
33
+ createWriteToolDefinition,
34
+ } from "@bastani/atomic";
30
35
  import { Text } from "@earendil-works/pi-tui";
31
36
 
32
37
  export default function (pi: ExtensionAPI) {
33
38
  const cwd = process.cwd();
34
39
 
35
40
  // --- Read tool: show path and line count ---
36
- const originalRead = createReadTool(cwd);
41
+ const originalRead = createReadToolDefinition(cwd);
37
42
  pi.registerTool({
38
- name: "read",
39
- label: "read",
40
- description: originalRead.description,
41
- parameters: originalRead.parameters,
42
-
43
- async execute(toolCallId, params, signal, onUpdate) {
44
- return originalRead.execute(toolCallId, params, signal, onUpdate);
45
- },
43
+ ...originalRead,
46
44
 
47
45
  renderCall(args, theme, _context) {
48
46
  let text = theme.fg("toolTitle", theme.bold("read "));
@@ -92,16 +90,9 @@ export default function (pi: ExtensionAPI) {
92
90
  });
93
91
 
94
92
  // --- Bash tool: show command and exit code ---
95
- const originalBash = createBashTool(cwd);
93
+ const originalBash = createBashToolDefinition(cwd);
96
94
  pi.registerTool({
97
- name: "bash",
98
- label: "bash",
99
- description: originalBash.description,
100
- parameters: originalBash.parameters,
101
-
102
- async execute(toolCallId, params, signal, onUpdate) {
103
- return originalBash.execute(toolCallId, params, signal, onUpdate);
104
- },
95
+ ...originalBash,
105
96
 
106
97
  renderCall(args, theme, _context) {
107
98
  let text = theme.fg("toolTitle", theme.bold("$ "));
@@ -151,17 +142,9 @@ export default function (pi: ExtensionAPI) {
151
142
  });
152
143
 
153
144
  // --- Edit tool: show path and diff stats ---
154
- const originalEdit = createEditTool(cwd);
145
+ const originalEdit = createEditToolDefinition(cwd);
155
146
  pi.registerTool({
156
- name: "edit",
157
- label: "edit",
158
- description: originalEdit.description,
159
- parameters: originalEdit.parameters,
160
- renderShell: "self",
161
-
162
- async execute(toolCallId, params, signal, onUpdate) {
163
- return originalEdit.execute(toolCallId, params, signal, onUpdate);
164
- },
147
+ ...originalEdit,
165
148
 
166
149
  renderCall(args, theme, _context) {
167
150
  let text = theme.fg("toolTitle", theme.bold("edit "));
@@ -216,16 +199,9 @@ export default function (pi: ExtensionAPI) {
216
199
  });
217
200
 
218
201
  // --- Write tool: show path and size ---
219
- const originalWrite = createWriteTool(cwd);
202
+ const originalWrite = createWriteToolDefinition(cwd);
220
203
  pi.registerTool({
221
- name: "write",
222
- label: "write",
223
- description: originalWrite.description,
224
- parameters: originalWrite.parameters,
225
-
226
- async execute(toolCallId, params, signal, onUpdate) {
227
- return originalWrite.execute(toolCallId, params, signal, onUpdate);
228
- },
204
+ ...originalWrite,
229
205
 
230
206
  renderCall(args, theme, _context) {
231
207
  let text = theme.fg("toolTitle", theme.bold("write "));
@@ -18,13 +18,13 @@
18
18
 
19
19
  import type { ExtensionAPI } from "@bastani/atomic";
20
20
  import {
21
- createBashTool,
22
- createEditTool,
23
- createFindTool,
24
- createGrepTool,
25
- createLsTool,
26
- createReadTool,
27
- createWriteTool,
21
+ createBashToolDefinition,
22
+ createEditToolDefinition,
23
+ createFindToolDefinition,
24
+ createLsToolDefinition,
25
+ createReadToolDefinition,
26
+ createSearchToolDefinition,
27
+ createWriteToolDefinition,
28
28
  } from "@bastani/atomic";
29
29
  import { Text } from "@earendil-works/pi-tui";
30
30
  import { homedir } from "os";
@@ -40,45 +40,26 @@ function shortenPath(path: string): string {
40
40
  return path;
41
41
  }
42
42
 
43
- // Cache for built-in tools by cwd
44
- const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
45
-
46
43
  function createBuiltInTools(cwd: string) {
47
44
  return {
48
- read: createReadTool(cwd),
49
- bash: createBashTool(cwd),
50
- edit: createEditTool(cwd),
51
- write: createWriteTool(cwd),
52
- find: createFindTool(cwd),
53
- grep: createGrepTool(cwd),
54
- ls: createLsTool(cwd),
45
+ read: createReadToolDefinition(cwd),
46
+ bash: createBashToolDefinition(cwd),
47
+ edit: createEditToolDefinition(cwd),
48
+ write: createWriteToolDefinition(cwd),
49
+ find: createFindToolDefinition(cwd),
50
+ search: createSearchToolDefinition(cwd),
51
+ ls: createLsToolDefinition(cwd),
55
52
  };
56
53
  }
57
54
 
58
- function getBuiltInTools(cwd: string) {
59
- let tools = toolCache.get(cwd);
60
- if (!tools) {
61
- tools = createBuiltInTools(cwd);
62
- toolCache.set(cwd, tools);
63
- }
64
- return tools;
65
- }
66
-
67
55
  export default function (pi: ExtensionAPI) {
56
+ const tools = createBuiltInTools(process.cwd());
57
+
68
58
  // =========================================================================
69
59
  // Read Tool
70
60
  // =========================================================================
71
61
  pi.registerTool({
72
- name: "read",
73
- label: "read",
74
- description:
75
- "Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files.",
76
- parameters: getBuiltInTools(process.cwd()).read.parameters,
77
-
78
- async execute(toolCallId, params, signal, onUpdate, ctx) {
79
- const tools = getBuiltInTools(ctx.cwd);
80
- return tools.read.execute(toolCallId, params, signal, onUpdate);
81
- },
62
+ ...tools.read,
82
63
 
83
64
  renderCall(args, theme, _context) {
84
65
  const path = shortenPath(args.path || "");
@@ -116,16 +97,7 @@ export default function (pi: ExtensionAPI) {
116
97
  // Bash Tool
117
98
  // =========================================================================
118
99
  pi.registerTool({
119
- name: "bash",
120
- label: "bash",
121
- description:
122
- "Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first).",
123
- parameters: getBuiltInTools(process.cwd()).bash.parameters,
124
-
125
- async execute(toolCallId, params, signal, onUpdate, ctx) {
126
- const tools = getBuiltInTools(ctx.cwd);
127
- return tools.bash.execute(toolCallId, params, signal, onUpdate);
128
- },
100
+ ...tools.bash,
129
101
 
130
102
  renderCall(args, theme, _context) {
131
103
  const command = args.command || "...";
@@ -165,16 +137,7 @@ export default function (pi: ExtensionAPI) {
165
137
  // Write Tool
166
138
  // =========================================================================
167
139
  pi.registerTool({
168
- name: "write",
169
- label: "write",
170
- description:
171
- "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
172
- parameters: getBuiltInTools(process.cwd()).write.parameters,
173
-
174
- async execute(toolCallId, params, signal, onUpdate, ctx) {
175
- const tools = getBuiltInTools(ctx.cwd);
176
- return tools.write.execute(toolCallId, params, signal, onUpdate);
177
- },
140
+ ...tools.write,
178
141
 
179
142
  renderCall(args, theme, _context) {
180
143
  const path = shortenPath(args.path || "");
@@ -207,16 +170,8 @@ export default function (pi: ExtensionAPI) {
207
170
  // Edit Tool
208
171
  // =========================================================================
209
172
  pi.registerTool({
210
- name: "edit",
211
- label: "edit",
212
- description:
213
- "Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
214
- parameters: getBuiltInTools(process.cwd()).edit.parameters,
215
-
216
- async execute(toolCallId, params, signal, onUpdate, ctx) {
217
- const tools = getBuiltInTools(ctx.cwd);
218
- return tools.edit.execute(toolCallId, params, signal, onUpdate);
219
- },
173
+ ...tools.edit,
174
+ renderShell: "default",
220
175
 
221
176
  renderCall(args, theme, _context) {
222
177
  const path = shortenPath(args.path || "");
@@ -252,16 +207,7 @@ export default function (pi: ExtensionAPI) {
252
207
  // Find Tool
253
208
  // =========================================================================
254
209
  pi.registerTool({
255
- name: "find",
256
- label: "find",
257
- description:
258
- "Find files by name pattern (glob). Searches recursively from the specified path. Output limited to 200 results.",
259
- parameters: getBuiltInTools(process.cwd()).find.parameters,
260
-
261
- async execute(toolCallId, params, signal, onUpdate, ctx) {
262
- const tools = getBuiltInTools(ctx.cwd);
263
- return tools.find.execute(toolCallId, params, signal, onUpdate);
264
- },
210
+ ...tools.find,
265
211
 
266
212
  renderCall(args, theme, _context) {
267
213
  const pattern = args.pattern || "";
@@ -307,34 +253,17 @@ export default function (pi: ExtensionAPI) {
307
253
  });
308
254
 
309
255
  // =========================================================================
310
- // Grep Tool
256
+ // Search Tool
311
257
  // =========================================================================
312
258
  pi.registerTool({
313
- name: "grep",
314
- label: "grep",
315
- description:
316
- "Search file contents by regex pattern. Uses ripgrep for fast searching. Output limited to 200 matches.",
317
- parameters: getBuiltInTools(process.cwd()).grep.parameters,
318
-
319
- async execute(toolCallId, params, signal, onUpdate, ctx) {
320
- const tools = getBuiltInTools(ctx.cwd);
321
- return tools.grep.execute(toolCallId, params, signal, onUpdate);
322
- },
259
+ ...tools.search,
323
260
 
324
261
  renderCall(args, theme, _context) {
325
262
  const pattern = args.pattern || "";
326
- const path = shortenPath(args.path || ".");
327
- const glob = args.glob;
328
- const limit = args.limit;
263
+ const paths = typeof args.paths === "string" ? args.paths : args.paths?.join(", ") || ".";
329
264
 
330
- let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern}/`)}`;
331
- text += theme.fg("toolOutput", ` in ${path}`);
332
- if (glob) {
333
- text += theme.fg("toolOutput", ` (${glob})`);
334
- }
335
- if (limit !== undefined) {
336
- text += theme.fg("toolOutput", ` limit ${limit}`);
337
- }
265
+ let text = `${theme.fg("toolTitle", theme.bold("search"))} ${theme.fg("accent", `/${pattern}/`)}`;
266
+ text += theme.fg("toolOutput", ` in ${shortenPath(paths)}`);
338
267
 
339
268
  return new Text(text, 0, 0);
340
269
  },
@@ -372,16 +301,7 @@ export default function (pi: ExtensionAPI) {
372
301
  // Ls Tool
373
302
  // =========================================================================
374
303
  pi.registerTool({
375
- name: "ls",
376
- label: "ls",
377
- description:
378
- "List directory contents with file sizes. Shows files and directories with their sizes. Output limited to 500 entries.",
379
- parameters: getBuiltInTools(process.cwd()).ls.parameters,
380
-
381
- async execute(toolCallId, params, signal, onUpdate, ctx) {
382
- const tools = getBuiltInTools(ctx.cwd);
383
- return tools.ls.execute(toolCallId, params, signal, onUpdate);
384
- },
304
+ ...tools.ls,
385
305
 
386
306
  renderCall(args, theme, _context) {
387
307
  const path = shortenPath(args.path || ".");