@inneranimalmedia/agentsam-sdk 2.5.0 → 2.6.1

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 (211) hide show
  1. package/AGENTSAM.md +55 -0
  2. package/README.md +12 -8
  3. package/bin/agentsam +2 -0
  4. package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
  5. package/docs/CLI_SHELL.md +163 -53
  6. package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
  7. package/docs/RELEASES.md +16 -7
  8. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  9. package/docs/TEST_TIERS.md +26 -0
  10. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  11. package/package.json +45 -12
  12. package/packages/agentsam-repository/README.md +15 -0
  13. package/packages/agentsam-repository/package.json +25 -0
  14. package/packages/agentsam-repository/src/contracts.js +113 -0
  15. package/packages/agentsam-repository/src/index.js +3 -0
  16. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  17. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  18. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  19. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  20. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  21. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  22. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  23. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  24. package/packages/connectors/cloudflare/package.json +10 -0
  25. package/packages/connectors/cloudflare/src/index.js +127 -0
  26. package/packages/connectors/cloudflare/src/owner.js +76 -0
  27. package/packages/connectors/cloudflare/src/routes.js +223 -0
  28. package/packages/connectors/cloudflare/src/vault.js +80 -0
  29. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  30. package/packages/identity/package.json +2 -2
  31. package/packages/identity/src/contracts/auth-config.js +18 -7
  32. package/packages/identity/tests/auth-config.test.mjs +9 -5
  33. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  34. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  35. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  36. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  37. package/protocol/README.md +1 -0
  38. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  39. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  40. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  41. package/protocol/capabilities/manifest.json +47 -0
  42. package/protocol/context/context-budget.schema.json +10 -15
  43. package/protocol/context/context-item.schema.json +4 -5
  44. package/protocol/context/resolved-context-pack.schema.json +19 -14
  45. package/protocol/models/README.md +373 -0
  46. package/protocol/models/model-inventory-v2.schema.json +212 -0
  47. package/protocol/repository/repository-contract.schema.json +24 -0
  48. package/protocol/repository/repository-dependency.schema.json +24 -0
  49. package/protocol/repository/repository-identity.schema.json +17 -0
  50. package/protocol/rpc/v1/common.proto +16 -0
  51. package/protocol/rpc/v1/errors.proto +35 -0
  52. package/protocol/rpc/v1/knowledge.proto +77 -0
  53. package/services/knowledge/package-lock.json +333 -0
  54. package/services/knowledge/package.json +5 -1
  55. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  56. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  57. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  58. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  59. package/skills/catalog.json +18 -0
  60. package/src/agent/capability-adapter.js +25 -13
  61. package/src/agent/index.js +1 -0
  62. package/src/agent/responses-runner.js +353 -0
  63. package/src/capabilities/repository-snapshot.js +3 -3
  64. package/src/cli.js +118 -31
  65. package/src/cloudflare/cpu-profile.js +115 -0
  66. package/src/cloudflare/index.js +14 -0
  67. package/src/cloudflare/wrangler.js +132 -0
  68. package/src/commands/account-auth.js +47 -0
  69. package/src/commands/cloudflare.js +58 -0
  70. package/src/commands/connections.js +93 -0
  71. package/src/commands/context-economics.js +129 -0
  72. package/src/commands/context.js +1 -1
  73. package/src/commands/db.js +20 -3
  74. package/src/commands/deploy.js +39 -3
  75. package/src/commands/env.js +90 -0
  76. package/src/commands/eval.js +63 -0
  77. package/src/commands/interactive.js +2 -5
  78. package/src/commands/knowledge.js +12 -4
  79. package/src/commands/merkle-persist.js +30 -11
  80. package/src/commands/merkle.js +1 -1
  81. package/src/commands/models.js +149 -46
  82. package/src/commands/ollama.js +26 -0
  83. package/src/commands/preferences.js +130 -61
  84. package/src/commands/resume.js +67 -0
  85. package/src/commands/security.js +5 -3
  86. package/src/commands/shell.js +568 -119
  87. package/src/commands/tunnel.js +2 -2
  88. package/src/commands/whoami.js +86 -0
  89. package/src/context/budget.js +68 -6
  90. package/src/context/index.js +3 -1
  91. package/src/context/rehydrate.js +35 -0
  92. package/src/context/resolve.js +44 -12
  93. package/src/errors/contract.js +236 -0
  94. package/src/errors/diagnostic.js +160 -0
  95. package/src/errors/index.js +23 -0
  96. package/src/eval/context.js +191 -0
  97. package/src/eval/index.js +1 -0
  98. package/src/index.js +68 -2
  99. package/src/knowledge/service/auth.js +13 -0
  100. package/src/knowledge/service/grpc-client.js +115 -0
  101. package/src/knowledge/service/grpc-codec.js +237 -0
  102. package/src/knowledge/service/grpc-server.js +83 -0
  103. package/src/knowledge/service/job-engine.js +248 -0
  104. package/src/knowledge/service/server.js +87 -135
  105. package/src/knowledge/source.js +1 -1
  106. package/src/lib/account-session.js +98 -0
  107. package/src/lib/agent-instructions.js +73 -0
  108. package/src/lib/auth.js +4 -0
  109. package/src/lib/cli-preferences.js +55 -24
  110. package/src/lib/deploy/git-guard.js +69 -0
  111. package/src/lib/deploy/health.js +57 -0
  112. package/src/lib/deploy/local-studio.js +283 -0
  113. package/src/lib/deploy/secret-scan.js +65 -0
  114. package/src/lib/deploy-receipt/index.js +2 -2
  115. package/src/lib/detect-context.js +2 -2
  116. package/src/lib/execution-approvals.js +59 -0
  117. package/src/lib/knowledge-docker.js +6 -3
  118. package/src/lib/local-sessions.js +148 -0
  119. package/src/lib/local-status.js +1 -1
  120. package/src/lib/project-config.js +1 -1
  121. package/src/lib/provider-credentials.js +183 -0
  122. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  123. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  124. package/src/lib/slash-commands.js +23 -16
  125. package/src/local/migrations.js +93 -0
  126. package/src/local/runtime-store.js +141 -0
  127. package/src/local/sqlite.js +2 -0
  128. package/src/local-pty/server.js +113 -51
  129. package/src/models/catalog.js +135 -0
  130. package/src/models/discovery.js +292 -0
  131. package/src/models/index.js +7 -0
  132. package/src/providers/anthropic-messages.js +192 -0
  133. package/src/providers/cloudflare-chat.js +183 -0
  134. package/src/providers/factory.js +69 -0
  135. package/src/providers/gemini-generate-content.js +208 -0
  136. package/src/providers/index.js +10 -0
  137. package/src/providers/ollama-chat.js +148 -0
  138. package/src/providers/openai-responses.js +426 -0
  139. package/src/repository/index.js +14 -2
  140. package/src/rpc/generated/common_grpc_pb.js +1 -0
  141. package/src/rpc/generated/common_pb.js +536 -0
  142. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  143. package/src/rpc/generated/errors_pb.js +482 -0
  144. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  145. package/src/rpc/generated/knowledge_pb.js +2168 -0
  146. package/src/rpc/generated/package.json +3 -0
  147. package/src/security/process.js +35 -9
  148. package/src/security/trust-boundary.js +2 -2
  149. package/src/telemetry/contracts.js +203 -0
  150. package/src/telemetry/events.js +51 -0
  151. package/src/telemetry/index.js +8 -0
  152. package/src/tools/hydrate.js +35 -0
  153. package/src/tools/index.js +1 -0
  154. package/src/ui/boot.js +15 -17
  155. package/src/ui/cli/activity.js +76 -0
  156. package/src/ui/cli/compaction.js +15 -0
  157. package/src/ui/cli/footer.js +39 -0
  158. package/src/ui/cli/help.js +192 -0
  159. package/src/ui/cli/plan.js +20 -0
  160. package/src/ui/cli/runtime-events.js +110 -0
  161. package/src/ui/cli/waiting.js +16 -0
  162. package/src/ui/merkle/render.js +1 -1
  163. package/test/account-session.test.mjs +36 -0
  164. package/test/cli/preferences-runtime.test.mjs +11 -0
  165. package/test/cli/runtime-ui.test.mjs +74 -0
  166. package/test/cli-preferences.test.mjs +26 -5
  167. package/test/cloudflare-connector.test.mjs +96 -0
  168. package/test/cloudflare-runtime.test.mjs +75 -0
  169. package/test/context.test.mjs +61 -12
  170. package/test/deploy-health-scan.test.mjs +67 -0
  171. package/test/error-diagnostics.test.mjs +115 -0
  172. package/test/eval-context.test.mjs +37 -0
  173. package/test/execution-approvals.test.mjs +27 -0
  174. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  175. package/test/integration/cli-help.test.mjs +37 -0
  176. package/test/integration/knowledge-rpc.test.mjs +112 -0
  177. package/test/integration/merkle-cli.test.mjs +61 -0
  178. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  179. package/test/integration/provider-env-cli.test.mjs +49 -0
  180. package/test/integration/provider-factory.test.mjs +197 -0
  181. package/test/integration/repository-company-graph.test.mjs +90 -0
  182. package/test/integration/runtime-migrations.test.mjs +82 -0
  183. package/test/knowledge-service.test.mjs +5 -0
  184. package/test/knowledge.test.mjs +16 -0
  185. package/test/live/terminal-transport.live.test.mjs +24 -0
  186. package/test/local-sessions.test.mjs +48 -0
  187. package/test/local-studio-deploy.test.mjs +83 -0
  188. package/test/model-catalog.test.mjs +43 -0
  189. package/test/models.test.mjs +127 -16
  190. package/test/npm10-lock.test.mjs +29 -0
  191. package/test/ollama.test.mjs +21 -0
  192. package/test/openai-responses.test.mjs +95 -0
  193. package/test/portable-context.test.mjs +1 -1
  194. package/test/provider-credentials.test.mjs +96 -0
  195. package/test/rehydrate.test.mjs +25 -0
  196. package/test/release-hygiene.test.mjs +13 -5
  197. package/test/responses-runner.test.mjs +150 -0
  198. package/test/shell.test.mjs +92 -23
  199. package/test/smoke.mjs +4 -1
  200. package/test/telemetry.test.mjs +79 -0
  201. package/test/terminal/local-pty.mock.test.mjs +151 -0
  202. package/test/tools-search.test.mjs +14 -1
  203. package/test/whoami-resume.test.mjs +56 -0
  204. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  205. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  206. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  207. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  208. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  209. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  210. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  211. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
5
  import { spawnSync } from 'node:child_process';
6
+ import { confirm, isCancel, select } from '@clack/prompts';
6
7
  import { SLASH_COMMANDS, SHELL_PHASES } from '../lib/slash-commands.js';
7
8
  import { runContext } from './context.js';
8
9
  import { runDb } from './db.js';
@@ -10,11 +11,28 @@ import { runDeploy } from './deploy.js';
10
11
  import { runStatus } from './status.js';
11
12
  import { runModels } from './models.js';
12
13
  import { configureCliPreferences } from './preferences.js';
13
- import { createRuntimeActivity } from '../ui/runtime-activity.js';
14
+ import { runCloudflare } from './cloudflare.js';
15
+ import { probeOllamaModel, resolveOllamaConfig } from './ollama.js';
16
+ import { createInlineActivity } from '../ui/cli/activity.js';
17
+ import { createCliRuntimePresenter } from '../ui/cli/runtime-events.js';
18
+ import { renderCliFooter } from '../ui/cli/footer.js';
19
+ import { diagnosticFromError, renderDiagnosticError } from '../errors/index.js';
20
+ import { getModelRecord } from '../models/index.js';
21
+ import { discoverProviderModels } from '../models/discovery.js';
22
+ import { readCliPreferences, updateCliPreferences } from '../lib/cli-preferences.js';
23
+ import { buildContextEconomicsReport, renderContextEconomics } from './context-economics.js';
24
+ import { createProviderAdapter } from '../providers/index.js';
25
+ import { createCapabilityAdapter, runResponsesAgent } from '../agent/index.js';
26
+ import { resolveProviderCredential } from '../lib/provider-credentials.js';
27
+ import { createLocalSession, saveLocalSession, sessionTitleFromInput, localSessionElapsedMs } from '../lib/local-sessions.js';
28
+ import { grantExecutionApproval, isExecutionApproved, toolApprovalKey } from '../lib/execution-approvals.js';
29
+ import { runWhoami } from './whoami.js';
30
+ import { runLogin, runLogout } from './account-auth.js';
31
+ import { runHelp } from '../ui/cli/help.js';
32
+ import { readAccountSession } from '../lib/account-session.js';
33
+ import { startRuntimeRun, finishRuntimeRun, recordRuntimeCompaction } from '../local/runtime-store.js';
14
34
 
15
- function writeLine(write, value = '') {
16
- write(`${value}\n`);
17
- }
35
+ function writeLine(write, value = '') { write(`${value}\n`); }
18
36
 
19
37
  export function compactCwd(value, home = process.env.HOME || process.env.USERPROFILE || '') {
20
38
  const cwd = path.resolve(value);
@@ -41,34 +59,22 @@ export function tokenizeShellLine(input = '') {
41
59
  let token = '';
42
60
  let quote = '';
43
61
  let started = false;
44
-
45
62
  const flush = () => {
46
63
  if (!started) return;
47
64
  tokens.push(token);
48
65
  token = '';
49
66
  started = false;
50
67
  };
51
-
52
68
  for (let i = 0; i < source.length; i += 1) {
53
69
  const ch = source[i];
54
70
  if (quote) {
55
- if (ch === quote) {
56
- quote = '';
57
- } else {
58
- token += ch;
59
- }
60
- started = true;
61
- continue;
62
- }
63
- if (ch === '"' || ch === "'") {
64
- quote = ch;
71
+ if (ch === quote) quote = '';
72
+ else token += ch;
65
73
  started = true;
66
74
  continue;
67
75
  }
68
- if (/\s/.test(ch)) {
69
- flush();
70
- continue;
71
- }
76
+ if (ch === '"' || ch === "'") { quote = ch; started = true; continue; }
77
+ if (/\s/.test(ch)) { flush(); continue; }
72
78
  token += ch;
73
79
  started = true;
74
80
  }
@@ -78,21 +84,20 @@ export function tokenizeShellLine(input = '') {
78
84
 
79
85
  export function renderShellCatalog() {
80
86
  const next = SHELL_PHASES.find((phase) => phase.status === 'next' || phase.status === 'current');
81
- const rows = SLASH_COMMANDS.map((row) => ` ${row.cmd.padEnd(14)} ${row.description}`).join('\n');
82
- return `
83
- ╔════════════════════════════════╗
84
- ║ Agent Sam Terminal ║
85
- ╚════════════════════════════════╝
86
-
87
- Local PTY agentsam start-local ws://127.0.0.1:3099
88
- Models agentsam models providers + local model inventory
89
- DB agentsam db status local SQLite
90
-
91
- Current milestone: ${next?.label ?? 'local terminal experience'}
92
-
93
- Slash commands (${SLASH_COMMANDS.length} registered):
94
- ${rows}
95
- `;
87
+ return [
88
+ '',
89
+ ' Agent Sam',
90
+ ' Type normally to work with the selected model.',
91
+ '',
92
+ ' / command picker',
93
+ ' /model model · reasoning · processing',
94
+ ' /usage tokens · cost · resume receipt',
95
+ ' /help focused help',
96
+ ' /exit return to host shell',
97
+ '',
98
+ ` ${next?.label ?? 'interactive runtime'} · ${SLASH_COMMANDS.length} commands available through /`,
99
+ '',
100
+ ].join('\n');
96
101
  }
97
102
 
98
103
  function parseDeployOptions(args, cwd) {
@@ -106,6 +111,105 @@ function parseDeployOptions(args, cwd) {
106
111
  return opts;
107
112
  }
108
113
 
114
+ function spawnGit(cwd, args) {
115
+ const result = spawnSync('git', args, { cwd, stdio: 'inherit', shell: false });
116
+ if (result.error) throw result.error;
117
+ if (result.status !== 0) throw new Error(`git exited ${result.status}`);
118
+ }
119
+
120
+ function selectedModel(cwd) {
121
+ const preferences = readCliPreferences(cwd) || {};
122
+ const snapshot = preferences.modelSnapshot?.model_key === preferences.modelPreference
123
+ ? preferences.modelSnapshot
124
+ : null;
125
+ const model = snapshot || getModelRecord(preferences.modelPreference);
126
+ if (!model) throw new Error('Select an exact provider-verified model with /model first so Agent Sam can verify supported runtime controls.');
127
+ return { preferences, model };
128
+ }
129
+
130
+ async function resolveModelForTurn(cwd, state) {
131
+ const selected = selectedModel(cwd);
132
+ const { preferences } = selected;
133
+ let model = selected.model;
134
+
135
+ if (model.provider === 'ollama') {
136
+ const config = resolveOllamaConfig({}, process.env);
137
+ const probe = await probeOllamaModel(model.provider_model_id, config, state.providerFetchImpl || fetch);
138
+ if (!probe.ok) throw new Error(`ollama_model_probe_failed:${model.provider_model_id}:${probe.error || 'unknown'}`);
139
+ model = {
140
+ ...model,
141
+ context_window: probe.context_window,
142
+ context_window_source: probe.context_window_source || 'unknown',
143
+ capabilities: { ...(model.capabilities || {}), local_runtime: true, ...(Object.fromEntries((probe.capabilities || []).map((name) => [name, true]))) },
144
+ };
145
+ updateCliPreferences(cwd, { modelPreference: model.model_key, modelSnapshot: model });
146
+ return { preferences: readCliPreferences(cwd) || preferences, model, credential: null, verification: 'local_runtime' };
147
+ }
148
+
149
+ const credential = resolveProviderCredential(model.provider, { home: state.home });
150
+ if (!credential.configured) {
151
+ throw new Error(`provider_credential_unavailable:${model.provider}:${credential.error || credential.env || 'not_configured'}`);
152
+ }
153
+
154
+ const discovery = await discoverProviderModels(model.provider, credential, { fetchImpl: state.providerFetchImpl || fetch });
155
+ if (discovery.ok) {
156
+ const live = discovery.models.find((row) => row.provider_model_id === model.provider_model_id || row.model_key === model.model_key);
157
+ if (!live) throw new Error(`selected_model_not_available_for_credential:${model.provider}:${model.provider_model_id}`);
158
+ model = live;
159
+ updateCliPreferences(cwd, { modelPreference: live.model_key, modelSnapshot: live });
160
+ return { preferences: readCliPreferences(cwd) || preferences, model, credential, verification: 'provider_api' };
161
+ }
162
+
163
+ if (preferences.modelSnapshot?.availability === 'available') {
164
+ return { preferences, model, credential, verification: 'cached_provider_snapshot', discoveryError: discovery.error || null };
165
+ }
166
+ throw new Error(`provider_model_discovery_failed:${model.provider}:${discovery.error || 'unknown'}`);
167
+ }
168
+
169
+ async function chooseReasoning(cwd, args, state) {
170
+ const { preferences, model } = selectedModel(cwd);
171
+ let effort = String(args[0] || '').trim().toLowerCase();
172
+ if (!effort) {
173
+ if (!state.interactive) {
174
+ writeLine(state.write, ` ${model.provider_model_id} reasoning: ${model.reasoning_efforts.join(' | ')}`);
175
+ writeLine(state.write, ` current: ${preferences.reasoningEffort || 'auto'}`);
176
+ return;
177
+ }
178
+ const choice = await select({
179
+ message: `Reasoning level · ${model.provider_model_id}`,
180
+ initialValue: model.reasoning_efforts.includes(preferences.reasoningEffort) ? preferences.reasoningEffort : model.reasoning_efforts[0],
181
+ options: model.reasoning_efforts.map((value) => ({ value, label: value === 'xhigh' ? 'Extra high' : value === 'max' ? 'Max' : value[0].toUpperCase() + value.slice(1) })),
182
+ });
183
+ if (isCancel(choice)) return;
184
+ effort = choice;
185
+ }
186
+ if (!model.reasoning_efforts.includes(effort)) throw new Error(`unsupported reasoning level for ${model.provider_model_id}: ${effort}`);
187
+ updateCliPreferences(cwd, { reasoningEffort: effort });
188
+ writeLine(state.write, ` reasoning → ${effort}`);
189
+ }
190
+
191
+ function setServiceTier(cwd, tier, write) {
192
+ const { model } = selectedModel(cwd);
193
+ if (!model.service_tiers.includes(tier)) throw new Error(`${model.provider_model_id} does not declare ${tier} processing support`);
194
+ updateCliPreferences(cwd, { serviceTier: tier });
195
+ const label = tier === 'default' ? 'standard' : tier;
196
+ writeLine(write, ` processing → ${label}`);
197
+ if (tier === 'fast') writeLine(write, ' Fast is a paid latency choice; Agent Sam will account for its model-specific pricing multiplier.');
198
+ if (tier === 'flex') writeLine(write, ' Flex trades latency/capacity availability for lower cost; it is not Batch.');
199
+ }
200
+
201
+ async function showCommandPicker(state) {
202
+ if (!state.interactive) {
203
+ state.write(renderShellCatalog());
204
+ return;
205
+ }
206
+ const choice = await select({
207
+ message: 'Agent Sam commands',
208
+ options: SLASH_COMMANDS.map((row) => ({ value: row.cmd, label: row.cmd, hint: row.description })),
209
+ });
210
+ if (isCancel(choice)) return;
211
+ await dispatchShellLine(choice, state);
212
+ }
109
213
 
110
214
  export async function runLocalAgent(goal, write, options = {}) {
111
215
  if (!goal) {
@@ -115,90 +219,404 @@ export async function runLocalAgent(goal, write, options = {}) {
115
219
  }
116
220
  const base = String(process.env.AGENTSAM_LOCAL_URL || 'http://127.0.0.1:8787').replace(/\/$/, '');
117
221
  const fetchImpl = options.fetchImpl || fetch;
118
- const activity = options.activity || createRuntimeActivity({
119
- write,
120
- phase: 'thinking',
121
- interactive: options.interactive,
122
- });
222
+ const activity = options.activity || createInlineActivity({ write, label: 'Thinking', interactive: options.interactive });
123
223
  let response;
124
224
  activity.start('thinking');
125
225
  try {
126
226
  response = await fetchImpl(`${base}/api/agentsam/message`, {
127
- method: 'POST',
128
- headers: { 'content-type': 'application/json' },
129
- body: JSON.stringify({ message: goal }),
227
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message: goal }),
130
228
  });
131
229
  } catch (error) {
132
230
  activity.fail('unavailable');
133
231
  throw new Error(`local Agent Sam unavailable at ${base} — run \`npm run dev\` first (${error?.message || error})`);
134
232
  }
135
233
  let text;
136
- try {
137
- text = await response.text();
138
- } catch (error) {
139
- activity.fail('response error');
140
- throw new Error(`local Agent Sam response could not be read: ${error?.message || error}`);
141
- }
142
- if (!response.ok) {
143
- activity.fail(`HTTP ${response.status}`);
144
- throw new Error(`local Agent Sam returned HTTP ${response.status}: ${text.slice(0, 400)}`);
145
- }
234
+ try { text = await response.text(); }
235
+ catch (error) { activity.fail('response error'); throw new Error(`local Agent Sam response could not be read: ${error?.message || error}`); }
236
+ if (!response.ok) { activity.fail(`HTTP ${response.status}`); throw new Error(`local Agent Sam returned HTTP ${response.status}: ${text.slice(0, 400)}`); }
146
237
  activity.succeed('done');
147
- try {
148
- writeLine(write, JSON.stringify(JSON.parse(text), null, 2));
149
- } catch {
150
- writeLine(write, text);
151
- }
238
+ try { writeLine(write, JSON.stringify(JSON.parse(text), null, 2)); }
239
+ catch { writeLine(write, text); }
152
240
  }
153
241
 
154
242
  async function showLocalLogs(cwd, write) {
155
243
  const dbPath = path.join(cwd, '.agentsam', 'data', 'agentsam.sqlite');
156
- if (!fs.existsSync(dbPath)) {
157
- writeLine(write, ' No local Agent Sam DB found. Run `agentsam init . --yes` first.');
158
- return;
159
- }
244
+ if (!fs.existsSync(dbPath)) { writeLine(write, ' No local Agent Sam DB found. Run `agentsam init . --yes` first.'); return; }
160
245
  const { createLocalSqliteDatabase } = await import('../local/sqlite.js');
161
246
  const db = await createLocalSqliteDatabase(dbPath);
162
247
  try {
163
- const calls = await db
164
- .prepare('SELECT id, session_id, tool_name, status, created_at, completed_at FROM agent_tool_calls ORDER BY created_at DESC LIMIT 20')
165
- .all();
166
- if (!calls.results.length) {
167
- writeLine(write, ' No local Agent Sam tool-call events yet.');
168
- return;
169
- }
248
+ const calls = await db.prepare('SELECT id, session_id, tool_name, status, created_at, completed_at FROM agent_tool_calls ORDER BY created_at DESC LIMIT 20').all();
249
+ if (!calls.results.length) { writeLine(write, ' No local Agent Sam tool-call events yet.'); return; }
250
+ writeLine(write, '\n Recent Agent Sam tool calls');
251
+ for (const row of calls.results) writeLine(write, ` ${String(row.created_at || '').padEnd(20)} ${String(row.status || '').padEnd(10)} ${row.tool_name}`);
170
252
  writeLine(write, '');
171
- writeLine(write, ' Recent Agent Sam tool calls');
172
- for (const row of calls.results) {
173
- writeLine(write, ` ${String(row.created_at || '').padEnd(20)} ${String(row.status || '').padEnd(10)} ${row.tool_name}`);
253
+ } finally { db.close(); }
254
+ }
255
+
256
+ function formatCount(value) {
257
+ return Math.max(0, Number(value || 0)).toLocaleString('en-US');
258
+ }
259
+
260
+ function formatUsd(value) {
261
+ const amount = Number(value || 0);
262
+ if (!Number.isFinite(amount)) return 'unavailable';
263
+ const currency = String.fromCharCode(36);
264
+ if (amount === 0) return currency + '0.000000';
265
+ return currency + (amount < 0.01 ? amount.toFixed(6) : amount.toFixed(4));
266
+ }
267
+
268
+ function formatElapsed(ms) {
269
+ const totalSeconds = Math.max(0, Math.round(Number(ms || 0) / 1000));
270
+ const hours = Math.floor(totalSeconds / 3600);
271
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
272
+ const seconds = totalSeconds % 60;
273
+ if (hours) return `${hours}h ${minutes}m ${seconds}s`;
274
+ if (minutes) return `${minutes}m ${seconds}s`;
275
+ return `${seconds}s`;
276
+ }
277
+
278
+ export function renderSessionReceipt(session) {
279
+ if (!session) return '';
280
+ const usage = session.cumulative_usage || {};
281
+ const input = Number(usage.input_tokens || 0);
282
+ const output = Number(usage.output_tokens || 0);
283
+ const cached = Number(usage.cached_input_tokens || 0);
284
+ const cacheWrite = Number(usage.cache_write_tokens || 0);
285
+ const reasoning = Number(usage.reasoning_tokens || 0);
286
+ const total = input + output;
287
+ const active = Number(session.usage_snapshot?.current_context?.input_tokens || 0);
288
+ const model = session.provider_model_id || session.model_key || 'model unavailable';
289
+ const breakdown = session.cost_breakdown_usd || {};
290
+ const componentTotal = ['input', 'cached_input', 'cache_write', 'output'].reduce((sum, key) => sum + Number(breakdown[key] || 0), 0);
291
+ const elapsed = localSessionElapsedMs(session);
292
+ const lines = [
293
+ '',
294
+ 'Session summary',
295
+ `Token usage: total=${formatCount(total)} input=${formatCount(input)}${cached ? ` (+ ${formatCount(cached)} cached)` : ''}${cacheWrite ? ` (+ ${formatCount(cacheWrite)} cache write)` : ''} output=${formatCount(output)}${reasoning ? ` reasoning=${formatCount(reasoning)}` : ''}`,
296
+ `Spent: ${formatUsd(session.total_cost_usd)} · ${model}${session.actual_service_tier ? ` · ${session.actual_service_tier}` : ''}`,
297
+ `Elapsed: ${formatElapsed(elapsed)}`,
298
+ ];
299
+ if (componentTotal > 0) {
300
+ lines.push(`Cost breakdown: input ${formatUsd(breakdown.input)} · cached ${formatUsd(breakdown.cached_input)} · cache write ${formatUsd(breakdown.cache_write)} · output ${formatUsd(breakdown.output)}`);
301
+ }
302
+ if (active) lines.push(`Active context: ${formatCount(active)} tokens`);
303
+ lines.push('', 'To continue this session, run:', ` agentsam resume ${session.id}`, '', 'Or run:', ' agentsam resume', '', 'and select:', ` ${session.title || 'this session'}`, '');
304
+ return lines.join('\n');
305
+ }
306
+
307
+ function persistSession(state, patch = {}) {
308
+ if (!state.session) return null;
309
+ state.session = saveLocalSession({ ...state.session, ...patch }, { home: state.home });
310
+ return state.session;
311
+ }
312
+
313
+ function recordSessionInput(state, input) {
314
+ const value = String(input || '').trim();
315
+ if (!state.session || !value) return;
316
+ const housekeeping = new Set(['/exit', '/quit', '/session', '/help', '/', '/menu', '/clear']);
317
+ if (housekeeping.has(value.toLowerCase())) return;
318
+ persistSession(state, {
319
+ status: 'active',
320
+ last_input: value,
321
+ title: sessionTitleFromInput(value),
322
+ });
323
+ }
324
+
325
+ function safeToolInput(value, depth = 0) {
326
+ if (depth > 3) return '[nested]';
327
+ if (Array.isArray(value)) return value.slice(0, 12).map((row) => safeToolInput(row, depth + 1));
328
+ if (!value || typeof value !== 'object') return value;
329
+ const output = {};
330
+ for (const [key, item] of Object.entries(value)) {
331
+ if (/token|secret|password|api[_-]?key|authorization/i.test(key)) output[key] = '[REDACTED]';
332
+ else output[key] = safeToolInput(item, depth + 1);
333
+ }
334
+ return output;
335
+ }
336
+
337
+ async function approveModelRequest(preflight, state) {
338
+ const projected = Number(preflight.projected_max_call_cost_usd);
339
+ const threshold = Number(process.env.AGENTSAM_CONFIRM_CALL_COST_USD || 0);
340
+ if (!(Number.isFinite(projected) && projected >= 0)) return true;
341
+ if (!(Number.isFinite(threshold) && threshold > 0) || projected <= threshold) return true;
342
+
343
+ const approvedCeiling = Number(state.session?.approved_projected_call_cost_usd || 0);
344
+ if (approvedCeiling > 0 && projected <= approvedCeiling) return true;
345
+ if (!state.interactive) return false;
346
+
347
+ state.activity?.clear?.();
348
+ writeLine(state.write, '');
349
+ writeLine(state.write, ' ◆ Model cost approval');
350
+ writeLine(state.write, ` model ${preflight.model}`);
351
+ writeLine(state.write, ` reasoning ${preflight.reasoning_effort}`);
352
+ writeLine(state.write, ` processing ${preflight.service_tier}`);
353
+ writeLine(state.write, ` context ~${formatCount(preflight.estimated_input_tokens)} input tokens`);
354
+ writeLine(state.write, ` max call ${formatUsd(projected)} conservative ceiling`);
355
+ if (Number.isFinite(preflight.tokens_until_pricing_threshold)) writeLine(state.write, ` headroom ${formatCount(preflight.tokens_until_pricing_threshold)} tokens`);
356
+ const approved = await confirm({ message: 'Allow this call?', initialValue: false });
357
+ if (isCancel(approved) || approved !== true) return false;
358
+ persistSession(state, { approved_projected_call_cost_usd: Math.max(approvedCeiling, projected) });
359
+ state.activity?.start?.('Working');
360
+ return true;
361
+ }
362
+
363
+ async function approveToolExecution(request, state) {
364
+ const sideEffects = request?.descriptor?.side_effects || 'none';
365
+ if (sideEffects === 'none') return true;
366
+ const key = toolApprovalKey(request.capability_id, request.input);
367
+ if (isExecutionApproved({ cwd: state.cwd, key }, { home: state.home })) return true;
368
+ if (!state.interactive) return false;
369
+
370
+ const summary = JSON.stringify(safeToolInput(request.input || {}));
371
+ state.activity?.clear?.();
372
+ writeLine(state.write, '');
373
+ writeLine(state.write, ' Agent Sam needs execution permission');
374
+ writeLine(state.write, ` action ${key}`);
375
+ writeLine(state.write, ` target local runtime · ${compactCwd(state.cwd)}`);
376
+ writeLine(state.write, ` effect ${sideEffects}`);
377
+ if (summary && summary !== '{}') writeLine(state.write, ` input ${summary.length > 500 ? `${summary.slice(0, 497)}...` : summary}`);
378
+ writeLine(state.write, ' secrets remain runtime-owned and are not included in the model-visible result.');
379
+
380
+ const choice = await select({
381
+ message: `Allow ${key}?`,
382
+ options: [
383
+ { value: 'once', label: 'Allow once' },
384
+ { value: 'always', label: `Always allow ${key} in this project` },
385
+ { value: 'deny', label: 'Deny' },
386
+ ],
387
+ });
388
+ if (isCancel(choice) || choice === 'deny') return false;
389
+ if (choice === 'always') grantExecutionApproval({ cwd: state.cwd, key, label: key }, { home: state.home });
390
+ state.activity?.start?.(`Working · ${request.capability_id}`);
391
+ return true;
392
+ }
393
+
394
+ async function runInteractiveModelTurn(prompt, state) {
395
+ const resolved = await resolveModelForTurn(state.cwd, state);
396
+ const { preferences, model, credential } = resolved;
397
+ const provider = createProviderAdapter({
398
+ modelRecord: model,
399
+ credential,
400
+ endpoint: model.provider === 'ollama' ? process.env.OLLAMA_BASE_URL : undefined,
401
+ fetchImpl: state.providerFetchImpl,
402
+ });
403
+ const capabilityAdapter = createCapabilityAdapter();
404
+
405
+ const samePolicy = state.session
406
+ && state.session.model_key === model.model_key
407
+ && state.session.reasoning_effort === preferences.reasoningEffort
408
+ && state.session.requested_service_tier === preferences.serviceTier;
409
+ const previousProviderState = samePolicy ? state.session?.provider_state : null;
410
+ const previousUsageSnapshot = samePolicy ? state.session?.usage_snapshot : null;
411
+ const accountId = readAccountSession({ home: state.home })?.account_id || null;
412
+ let runtimeRunId = null;
413
+ try {
414
+ runtimeRunId = await startRuntimeRun({
415
+ cwd: state.cwd,
416
+ account_id: accountId,
417
+ mode: 'agent',
418
+ model_key: model.model_key,
419
+ reasoning_effort: preferences.reasoningEffort,
420
+ service_tier: preferences.serviceTier,
421
+ });
422
+ } catch {
423
+ runtimeRunId = null;
424
+ }
425
+
426
+ const activity = createInlineActivity({
427
+ write: state.write,
428
+ interactive: state.interactive,
429
+ label: `Thinking · ${model.provider_model_id}`,
430
+ });
431
+ state.activity = activity;
432
+ const presenter = createCliRuntimePresenter({ activity, write: state.write, state });
433
+ const startedAt = Date.now();
434
+ activity.start(`Thinking · ${model.provider_model_id}`);
435
+
436
+ let result;
437
+ try {
438
+ result = await runResponsesAgent({
439
+ provider,
440
+ capabilityAdapter,
441
+ cwd: state.cwd,
442
+ prompt,
443
+ model: model.model_key,
444
+ modelRecord: model,
445
+ reasoningEffort: preferences.reasoningEffort,
446
+ serviceTier: preferences.serviceTier,
447
+ previousProviderState,
448
+ previousUsageSnapshot,
449
+ cumulativeUsage: state.session?.cumulative_usage || null,
450
+ promptCacheKey: state.session?.id || undefined,
451
+ runId: runtimeRunId || state.session?.id || undefined,
452
+ beforeRequest: (preflight) => approveModelRequest(preflight, state),
453
+ beforeTool: (request) => approveToolExecution(request, state),
454
+ emit(event) {
455
+ presenter.handle(event);
456
+ if (event?.type === 'usage.snapshot') state.usageSnapshot = event.payload;
457
+ },
458
+ });
459
+ } catch (error) {
460
+ activity.fail('Failed');
461
+ state.activity = null;
462
+ if (runtimeRunId) {
463
+ try {
464
+ await finishRuntimeRun({
465
+ cwd: state.cwd,
466
+ id: runtimeRunId,
467
+ status: 'failed',
468
+ error_code: error?.code || 'interactive_error',
469
+ error_message: error?.message || String(error),
470
+ latency_ms: Date.now() - startedAt,
471
+ });
472
+ } catch { /* execution result remains primary */ }
174
473
  }
175
- writeLine(write, '');
176
- } finally {
177
- db.close();
474
+ throw error;
178
475
  }
476
+
477
+ activity.succeed(result.tool_receipts?.length ? `Done · ${result.tool_receipts.length} tool call${result.tool_receipts.length === 1 ? '' : 's'}` : 'Done');
478
+ state.activity = null;
479
+
480
+ const compacted = presenter.compactionReceipt();
481
+ if (compacted) writeLine(state.write, compacted);
482
+
483
+ if (result.output_text) {
484
+ writeLine(state.write, '');
485
+ writeLine(state.write, result.output_text);
486
+ writeLine(state.write, '');
487
+ }
488
+
489
+ state.usageSnapshot = result.usage_snapshot;
490
+ if (runtimeRunId) {
491
+ try {
492
+ await finishRuntimeRun({
493
+ cwd: state.cwd,
494
+ id: runtimeRunId,
495
+ status: 'completed',
496
+ actual_service_tier: result.actual_service_tier,
497
+ usage: result.cumulative_usage || result.usage_snapshot?.cumulative || {},
498
+ cost_usd: result.total_cost_usd || 0,
499
+ latency_ms: Date.now() - startedAt,
500
+ });
501
+ if (presenter.state.lastCompaction) {
502
+ await recordRuntimeCompaction({
503
+ cwd: state.cwd,
504
+ account_id: accountId,
505
+ agent_run_id: runtimeRunId,
506
+ session_id: state.session?.id,
507
+ provider: model.provider,
508
+ model_key: model.model_key,
509
+ tokens_before: presenter.state.lastCompaction.tokens_before,
510
+ tokens_after: presenter.state.lastCompaction.tokens_after,
511
+ summary_text: presenter.state.lastCompaction.summary_text || '',
512
+ source_kind: model.provider === 'ollama' ? 'filesystem' : 'api',
513
+ metadata: { verification: resolved.verification, compaction_id: presenter.state.lastCompaction.compaction_id || null },
514
+ });
515
+ }
516
+ } catch { /* local telemetry persistence is best-effort */ }
517
+ }
518
+ if (state.session) {
519
+ persistSession(state, {
520
+ status: 'active',
521
+ model_key: model.model_key,
522
+ provider_model_id: result.model,
523
+ reasoning_effort: result.reasoning_effort,
524
+ requested_service_tier: result.requested_service_tier,
525
+ actual_service_tier: result.actual_service_tier,
526
+ provider_state: { provider: model.provider, ...(result.provider_state || {}) },
527
+ usage_snapshot: result.usage_snapshot,
528
+ cumulative_usage: result.cumulative_usage,
529
+ total_cost_usd: Number(state.session.total_cost_usd || 0) + Number(result.total_cost_usd || 0),
530
+ cost_breakdown_usd: Object.fromEntries(['input', 'cached_input', 'cache_write', 'output'].map((key) => [
531
+ key, Number(state.session.cost_breakdown_usd?.[key] || 0) + Number(result.cost_breakdown_usd?.[key] || 0),
532
+ ])),
533
+ last_error: null,
534
+ });
535
+ }
536
+
537
+ writeLine(state.write, renderCliFooter({
538
+ model: result.model || model.provider_model_id,
539
+ usageSnapshot: result.usage_snapshot,
540
+ tier: result.actual_service_tier,
541
+ elapsedMs: Date.now() - startedAt,
542
+ }));
543
+ writeLine(state.write, '');
544
+ return result;
179
545
  }
180
546
 
181
547
  export async function dispatchShellLine(line, state = {}) {
182
548
  const tokens = tokenizeShellLine(line);
183
549
  const write = state.write || ((text) => process.stdout.write(text));
550
+ state.write = write;
184
551
  state.cwd = path.resolve(state.cwd || process.cwd());
185
552
  if (!tokens.length) return { handled: true, exit: false, cwd: state.cwd };
186
-
187
553
  const [command, ...args] = tokens;
188
554
  try {
189
555
  switch (command.toLowerCase()) {
556
+ case '/':
557
+ case '/menu':
558
+ await showCommandPicker(state);
559
+ break;
190
560
  case '/help':
191
- write(renderShellCatalog());
192
- return { handled: true, exit: false, cwd: state.cwd };
561
+ await runHelp(args, { write, interactive: state.interactive });
562
+ break;
193
563
  case '/exit':
194
564
  case '/quit':
195
565
  return { handled: true, exit: true, cwd: state.cwd };
566
+ case '/model': {
567
+ const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false, section: 'model', home: state.home });
568
+ state.cwd = configured.identity.root;
569
+ break;
570
+ }
571
+ case '/reasoning':
572
+ await chooseReasoning(state.cwd, args, state);
573
+ break;
574
+ case '/fast':
575
+ setServiceTier(state.cwd, 'fast', write);
576
+ break;
577
+ case '/flex':
578
+ setServiceTier(state.cwd, 'flex', write);
579
+ break;
580
+ case '/standard':
581
+ setServiceTier(state.cwd, 'default', write);
582
+ break;
583
+ case '/context':
584
+ if (args[0] === 'repo' || args[0] === 'git') await runContext(['--cwd', state.cwd, ...args.slice(1)]);
585
+ else write(renderContextEconomics(buildContextEconomicsReport(state.cwd, {
586
+ activeInputTokens: state.usageSnapshot?.current_context?.input_tokens,
587
+ estimateKind: state.usageSnapshot?.estimate_kind,
588
+ })));
589
+ break;
196
590
  case '/status':
197
591
  await runStatus(args, { cwd: state.cwd });
198
592
  break;
199
- case '/context':
200
- await runContext(['--cwd', state.cwd, ...args]);
593
+ case '/models':
594
+ await runModels(args, { cwd: state.cwd, write, home: state.home });
201
595
  break;
596
+ case '/login':
597
+ await runLogin(args, { write, home: state.home });
598
+ break;
599
+ case '/logout':
600
+ runLogout(args, { write, home: state.home });
601
+ if (state.session) write(renderSessionReceipt(state.session));
602
+ break;
603
+ case '/whoami':
604
+ await runWhoami(args, { write, home: state.home });
605
+ break;
606
+ case '/session':
607
+ case '/usage':
608
+ if (state.session) write(renderSessionReceipt(state.session));
609
+ else writeLine(write, ' No persistent session is active in this shell invocation.');
610
+ break;
611
+ case '/cf':
612
+ case '/cloudflare':
613
+ await runCloudflare(args.length ? args : ['commands'], { cwd: state.cwd, write });
614
+ break;
615
+ case '/settings': {
616
+ const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false, home: state.home });
617
+ state.cwd = configured.identity.root;
618
+ break;
619
+ }
202
620
  case '/pwd':
203
621
  writeLine(write, state.cwd);
204
622
  break;
@@ -210,54 +628,60 @@ export async function dispatchShellLine(line, state = {}) {
210
628
  writeLine(write, state.cwd);
211
629
  break;
212
630
  }
213
- case '/git': {
214
- const gitArgs = args.length ? args : ['status', '--short', '--branch'];
215
- const result = spawnSync('git', gitArgs, { cwd: state.cwd, stdio: 'inherit', shell: false });
216
- if (result.error) throw result.error;
217
- if (result.status !== 0) throw new Error(`git exited ${result.status}`);
631
+ case '/git':
632
+ spawnGit(state.cwd, args.length ? args : ['status', '--short', '--branch']);
633
+ break;
634
+ case '/diff':
635
+ spawnGit(state.cwd, ['diff', ...args]);
218
636
  break;
219
- }
220
637
  case '/db':
221
638
  await runDb(args.length ? args : ['status'], { cwd: state.cwd });
222
639
  break;
223
640
  case '/agent':
224
641
  await runLocalAgent(args.join(' '), write, { interactive: state.interactive });
225
642
  break;
226
- case '/models':
227
- await runModels(args, { cwd: state.cwd, write });
228
- break;
229
- case '/settings': {
230
- const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false });
231
- state.cwd = configured.identity.root;
232
- break;
233
- }
234
643
  case '/logs':
235
644
  await showLocalLogs(state.cwd, write);
236
645
  break;
237
646
  case '/deploy':
238
647
  await runDeploy(parseDeployOptions(args, state.cwd));
239
648
  break;
649
+ case '/clear':
650
+ write('\x1b[2J\x1b[H');
651
+ break;
240
652
  default:
653
+ if (!command.startsWith('/')) {
654
+ await runInteractiveModelTurn(line, state);
655
+ break;
656
+ }
241
657
  writeLine(write, ` Unknown Agent Sam command: ${command}`);
242
- writeLine(write, ' Type /help for available commands.');
658
+ writeLine(write, ' Type / or /help for available commands.');
243
659
  return { handled: false, exit: false, cwd: state.cwd };
244
660
  }
245
661
  } catch (error) {
246
- writeLine(write, ` ✗ ${error?.message || error}`);
662
+ if (state.session) {
663
+ persistSession(state, { last_error: diagnosticFromError(error, { source: 'shell', kind: 'interactive_error' }) });
664
+ }
665
+ if (!error?.reported) {
666
+ for (const line of renderDiagnosticError(error).split('\n')) writeLine(write, ` ${line}`);
667
+ }
247
668
  }
248
-
249
669
  return { handled: true, exit: false, cwd: state.cwd };
250
670
  }
251
671
 
252
672
  export async function runShell(argv = [], options = {}) {
253
673
  const write = options.write || ((text) => process.stdout.write(text));
254
- const state = { cwd: path.resolve(options.cwd || process.cwd()), write, interactive: options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY) };
674
+ const state = {
675
+ cwd: path.resolve(options.cwd || process.cwd()),
676
+ write,
677
+ interactive: options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY),
678
+ usageSnapshot: options.usageSnapshot || options.session?.usage_snapshot || null,
679
+ session: options.session || null,
680
+ home: options.home,
681
+ providerFetchImpl: options.providerFetchImpl,
682
+ };
255
683
  const sub = argv[0] || '';
256
-
257
- if (sub === 'list' || sub === 'status') {
258
- write(renderShellCatalog());
259
- return;
260
- }
684
+ if (sub === 'list' || sub === 'status') { write(renderShellCatalog()); return; }
261
685
  if (sub === '--command' || sub === '--once') {
262
686
  const line = argv.slice(1).join(' ');
263
687
  if (!line) throw new Error(`${sub} requires a slash command`);
@@ -266,32 +690,57 @@ export async function runShell(argv = [], options = {}) {
266
690
  }
267
691
  if (sub) throw new Error(`unknown shell option: ${sub}`);
268
692
 
693
+ if (state.session) {
694
+ state.cwd = path.resolve(state.session.cwd || state.cwd);
695
+ state.session = saveLocalSession({
696
+ ...state.session,
697
+ status: 'active',
698
+ cwd: state.cwd,
699
+ active_started_at: new Date().toISOString(),
700
+ }, { home: state.home });
701
+ state.usageSnapshot = state.session.usage_snapshot || state.usageSnapshot;
702
+ } else {
703
+ const preferences = readCliPreferences(state.cwd) || {};
704
+ state.session = createLocalSession({
705
+ cwd: state.cwd,
706
+ status: 'active',
707
+ model_key: preferences.modelPreference !== 'auto' ? preferences.modelPreference : null,
708
+ reasoning_effort: preferences.reasoningEffort !== 'auto' ? preferences.reasoningEffort : null,
709
+ requested_service_tier: preferences.serviceTier || 'default',
710
+ }, { home: state.home });
711
+ }
712
+
269
713
  if (options.intro !== 'quiet') {
270
714
  write(renderShellCatalog());
271
- writeLine(write, ' Interactive shell ready. Type /help for commands; /exit to return to your host shell.');
272
- writeLine(write, '');
715
+ writeLine(write, ' Interactive shell ready. Type / for the command picker; /exit to return to your host shell.\n');
273
716
  }
274
-
275
717
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
718
+ let interrupted = false;
719
+ rl.on('SIGINT', () => {
720
+ interrupted = true;
721
+ rl.close();
722
+ });
276
723
  const promptText = () => {
277
724
  if (typeof options.prompt === 'function') return options.prompt(state);
278
725
  if (typeof options.prompt === 'string' && options.prompt) return options.prompt;
279
726
  return renderShellPrompt(state.cwd);
280
727
  };
281
- if (rl.terminal) {
282
- rl.setPrompt(promptText());
283
- rl.prompt();
284
- }
285
-
728
+ if (rl.terminal) { rl.setPrompt(promptText()); rl.prompt(); }
286
729
  for await (const line of rl) {
730
+ recordSessionInput(state, line);
287
731
  const result = await dispatchShellLine(line, state);
288
- if (result.exit) {
289
- rl.close();
290
- break;
291
- }
292
- if (rl.terminal) {
293
- rl.setPrompt(promptText());
294
- rl.prompt();
295
- }
732
+ if (result.exit) { rl.close(); break; }
733
+ if (rl.terminal) { rl.setPrompt(promptText()); rl.prompt(); }
734
+ }
735
+ if (state.session) {
736
+ const activeElapsedMs = localSessionElapsedMs(state.session);
737
+ persistSession(state, {
738
+ status: interrupted ? 'interrupted' : 'paused',
739
+ cwd: state.cwd,
740
+ active_elapsed_ms: activeElapsedMs,
741
+ active_started_at: null,
742
+ });
743
+ if (options.receipt !== false) write(renderSessionReceipt(state.session));
296
744
  }
745
+ return state.session;
297
746
  }