@inneranimalmedia/agentsam-sdk 2.5.0 → 2.6.0

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 (112) 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/RELEASES.md +16 -7
  7. package/package.json +20 -8
  8. package/packages/connectors/cloudflare/package.json +10 -0
  9. package/packages/connectors/cloudflare/src/index.js +127 -0
  10. package/packages/connectors/cloudflare/src/owner.js +76 -0
  11. package/packages/connectors/cloudflare/src/routes.js +223 -0
  12. package/packages/connectors/cloudflare/src/vault.js +80 -0
  13. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  14. package/packages/identity/package.json +2 -2
  15. package/packages/identity/src/contracts/auth-config.js +18 -7
  16. package/packages/identity/tests/auth-config.test.mjs +9 -5
  17. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  18. package/protocol/README.md +1 -0
  19. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  20. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  21. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  22. package/protocol/capabilities/manifest.json +47 -0
  23. package/protocol/context/context-budget.schema.json +10 -15
  24. package/protocol/context/context-item.schema.json +4 -5
  25. package/protocol/context/resolved-context-pack.schema.json +19 -14
  26. package/protocol/models/README.md +373 -0
  27. package/protocol/models/model-inventory-v2.schema.json +212 -0
  28. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  29. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  30. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  31. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  32. package/skills/catalog.json +18 -0
  33. package/src/agent/capability-adapter.js +25 -13
  34. package/src/agent/index.js +1 -0
  35. package/src/agent/responses-runner.js +325 -0
  36. package/src/cli.js +98 -28
  37. package/src/cloudflare/cpu-profile.js +115 -0
  38. package/src/cloudflare/index.js +14 -0
  39. package/src/cloudflare/wrangler.js +132 -0
  40. package/src/commands/account-auth.js +47 -0
  41. package/src/commands/cloudflare.js +58 -0
  42. package/src/commands/connections.js +93 -0
  43. package/src/commands/context-economics.js +114 -0
  44. package/src/commands/deploy.js +39 -3
  45. package/src/commands/eval.js +63 -0
  46. package/src/commands/interactive.js +2 -5
  47. package/src/commands/models.js +85 -40
  48. package/src/commands/preferences.js +101 -59
  49. package/src/commands/resume.js +67 -0
  50. package/src/commands/security.js +5 -3
  51. package/src/commands/shell.js +370 -109
  52. package/src/commands/tunnel.js +2 -2
  53. package/src/commands/whoami.js +86 -0
  54. package/src/context/budget.js +68 -6
  55. package/src/context/index.js +3 -1
  56. package/src/context/rehydrate.js +35 -0
  57. package/src/context/resolve.js +44 -12
  58. package/src/errors/diagnostic.js +160 -0
  59. package/src/errors/index.js +9 -0
  60. package/src/eval/context.js +191 -0
  61. package/src/eval/index.js +1 -0
  62. package/src/index.js +55 -1
  63. package/src/lib/account-session.js +98 -0
  64. package/src/lib/agent-instructions.js +73 -0
  65. package/src/lib/auth.js +4 -0
  66. package/src/lib/cli-preferences.js +28 -24
  67. package/src/lib/deploy/git-guard.js +69 -0
  68. package/src/lib/deploy/health.js +57 -0
  69. package/src/lib/deploy/local-studio.js +283 -0
  70. package/src/lib/deploy/secret-scan.js +65 -0
  71. package/src/lib/detect-context.js +2 -2
  72. package/src/lib/execution-approvals.js +59 -0
  73. package/src/lib/local-sessions.js +127 -0
  74. package/src/lib/provider-credentials.js +83 -0
  75. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  76. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  77. package/src/lib/slash-commands.js +22 -16
  78. package/src/models/catalog.js +135 -0
  79. package/src/models/index.js +7 -0
  80. package/src/providers/index.js +5 -0
  81. package/src/providers/openai-responses.js +275 -0
  82. package/src/security/process.js +35 -9
  83. package/src/telemetry/contracts.js +203 -0
  84. package/src/telemetry/events.js +48 -0
  85. package/src/telemetry/index.js +8 -0
  86. package/src/tools/hydrate.js +35 -0
  87. package/src/tools/index.js +1 -0
  88. package/src/ui/boot.js +15 -17
  89. package/test/account-session.test.mjs +36 -0
  90. package/test/cli-preferences.test.mjs +26 -5
  91. package/test/cloudflare-connector.test.mjs +96 -0
  92. package/test/cloudflare-runtime.test.mjs +75 -0
  93. package/test/context.test.mjs +61 -12
  94. package/test/deploy-health-scan.test.mjs +67 -0
  95. package/test/error-diagnostics.test.mjs +59 -0
  96. package/test/eval-context.test.mjs +37 -0
  97. package/test/execution-approvals.test.mjs +27 -0
  98. package/test/local-sessions.test.mjs +42 -0
  99. package/test/local-studio-deploy.test.mjs +83 -0
  100. package/test/model-catalog.test.mjs +43 -0
  101. package/test/models.test.mjs +30 -16
  102. package/test/npm10-lock.test.mjs +29 -0
  103. package/test/openai-responses.test.mjs +95 -0
  104. package/test/provider-credentials.test.mjs +52 -0
  105. package/test/rehydrate.test.mjs +25 -0
  106. package/test/release-hygiene.test.mjs +4 -4
  107. package/test/responses-runner.test.mjs +148 -0
  108. package/test/shell.test.mjs +47 -20
  109. package/test/smoke.mjs +4 -1
  110. package/test/telemetry.test.mjs +79 -0
  111. package/test/tools-search.test.mjs +14 -1
  112. package/test/whoami-resume.test.mjs +56 -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,20 @@ 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';
14
+ import { runCloudflare } from './cloudflare.js';
13
15
  import { createRuntimeActivity } from '../ui/runtime-activity.js';
16
+ import { diagnosticFromError, renderDiagnosticError } from '../errors/index.js';
17
+ import { getModelRecord } from '../models/index.js';
18
+ import { readCliPreferences, updateCliPreferences } from '../lib/cli-preferences.js';
19
+ import { buildContextEconomicsReport, renderContextEconomics } from './context-economics.js';
20
+ import { createOpenAIResponsesAdapter } from '../providers/index.js';
21
+ import { createCapabilityAdapter, runResponsesAgent } from '../agent/index.js';
22
+ import { resolveProviderCredential } from '../lib/provider-credentials.js';
23
+ import { createLocalSession, saveLocalSession, sessionTitleFromInput } from '../lib/local-sessions.js';
24
+ import { grantExecutionApproval, isExecutionApproved, toolApprovalKey } from '../lib/execution-approvals.js';
25
+ import { runWhoami } from './whoami.js';
14
26
 
15
- function writeLine(write, value = '') {
16
- write(`${value}\n`);
17
- }
27
+ function writeLine(write, value = '') { write(`${value}\n`); }
18
28
 
19
29
  export function compactCwd(value, home = process.env.HOME || process.env.USERPROFILE || '') {
20
30
  const cwd = path.resolve(value);
@@ -41,34 +51,22 @@ export function tokenizeShellLine(input = '') {
41
51
  let token = '';
42
52
  let quote = '';
43
53
  let started = false;
44
-
45
54
  const flush = () => {
46
55
  if (!started) return;
47
56
  tokens.push(token);
48
57
  token = '';
49
58
  started = false;
50
59
  };
51
-
52
60
  for (let i = 0; i < source.length; i += 1) {
53
61
  const ch = source[i];
54
62
  if (quote) {
55
- if (ch === quote) {
56
- quote = '';
57
- } else {
58
- token += ch;
59
- }
63
+ if (ch === quote) quote = '';
64
+ else token += ch;
60
65
  started = true;
61
66
  continue;
62
67
  }
63
- if (ch === '"' || ch === "'") {
64
- quote = ch;
65
- started = true;
66
- continue;
67
- }
68
- if (/\s/.test(ch)) {
69
- flush();
70
- continue;
71
- }
68
+ if (ch === '"' || ch === "'") { quote = ch; started = true; continue; }
69
+ if (/\s/.test(ch)) { flush(); continue; }
72
70
  token += ch;
73
71
  started = true;
74
72
  }
@@ -84,13 +82,10 @@ export function renderShellCatalog() {
84
82
  ║ Agent Sam Terminal ║
85
83
  ╚════════════════════════════════╝
86
84
 
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'}
85
+ Current milestone: ${next?.label ?? 'interactive runtime'}
86
+ Type / and press Enter for the scrollable command picker.
92
87
 
93
- Slash commands (${SLASH_COMMANDS.length} registered):
88
+ Slash commands (${SLASH_COMMANDS.length} implemented):
94
89
  ${rows}
95
90
  `;
96
91
  }
@@ -106,6 +101,63 @@ function parseDeployOptions(args, cwd) {
106
101
  return opts;
107
102
  }
108
103
 
104
+ function spawnGit(cwd, args) {
105
+ const result = spawnSync('git', args, { cwd, stdio: 'inherit', shell: false });
106
+ if (result.error) throw result.error;
107
+ if (result.status !== 0) throw new Error(`git exited ${result.status}`);
108
+ }
109
+
110
+ function selectedModel(cwd) {
111
+ const preferences = readCliPreferences(cwd) || {};
112
+ const model = getModelRecord(preferences.modelPreference);
113
+ if (!model) throw new Error('Select an exact provider-verified model with /model first so Agent Sam can verify supported runtime controls.');
114
+ return { preferences, model };
115
+ }
116
+
117
+ async function chooseReasoning(cwd, args, state) {
118
+ const { preferences, model } = selectedModel(cwd);
119
+ let effort = String(args[0] || '').trim().toLowerCase();
120
+ if (!effort) {
121
+ if (!state.interactive) {
122
+ writeLine(state.write, ` ${model.provider_model_id} reasoning: ${model.reasoning_efforts.join(' | ')}`);
123
+ writeLine(state.write, ` current: ${preferences.reasoningEffort || 'auto'}`);
124
+ return;
125
+ }
126
+ const choice = await select({
127
+ message: `Reasoning level · ${model.provider_model_id}`,
128
+ initialValue: model.reasoning_efforts.includes(preferences.reasoningEffort) ? preferences.reasoningEffort : model.reasoning_efforts[0],
129
+ options: model.reasoning_efforts.map((value) => ({ value, label: value === 'xhigh' ? 'Extra high' : value === 'max' ? 'Max' : value[0].toUpperCase() + value.slice(1) })),
130
+ });
131
+ if (isCancel(choice)) return;
132
+ effort = choice;
133
+ }
134
+ if (!model.reasoning_efforts.includes(effort)) throw new Error(`unsupported reasoning level for ${model.provider_model_id}: ${effort}`);
135
+ updateCliPreferences(cwd, { reasoningEffort: effort });
136
+ writeLine(state.write, ` reasoning → ${effort}`);
137
+ }
138
+
139
+ function setServiceTier(cwd, tier, write) {
140
+ const { model } = selectedModel(cwd);
141
+ if (!model.service_tiers.includes(tier)) throw new Error(`${model.provider_model_id} does not declare ${tier} processing support`);
142
+ updateCliPreferences(cwd, { serviceTier: tier });
143
+ const label = tier === 'default' ? 'standard' : tier;
144
+ writeLine(write, ` processing → ${label}`);
145
+ if (tier === 'fast') writeLine(write, ' Fast is a paid latency choice; Agent Sam will account for its model-specific pricing multiplier.');
146
+ if (tier === 'flex') writeLine(write, ' Flex trades latency/capacity availability for lower cost; it is not Batch.');
147
+ }
148
+
149
+ async function showCommandPicker(state) {
150
+ if (!state.interactive) {
151
+ state.write(renderShellCatalog());
152
+ return;
153
+ }
154
+ const choice = await select({
155
+ message: 'Agent Sam commands',
156
+ options: SLASH_COMMANDS.map((row) => ({ value: row.cmd, label: row.cmd, hint: row.description })),
157
+ });
158
+ if (isCancel(choice)) return;
159
+ await dispatchShellLine(choice, state);
160
+ }
109
161
 
110
162
  export async function runLocalAgent(goal, write, options = {}) {
111
163
  if (!goal) {
@@ -115,90 +167,280 @@ export async function runLocalAgent(goal, write, options = {}) {
115
167
  }
116
168
  const base = String(process.env.AGENTSAM_LOCAL_URL || 'http://127.0.0.1:8787').replace(/\/$/, '');
117
169
  const fetchImpl = options.fetchImpl || fetch;
118
- const activity = options.activity || createRuntimeActivity({
119
- write,
120
- phase: 'thinking',
121
- interactive: options.interactive,
122
- });
170
+ const activity = options.activity || createRuntimeActivity({ write, phase: 'thinking', interactive: options.interactive });
123
171
  let response;
124
172
  activity.start('thinking');
125
173
  try {
126
174
  response = await fetchImpl(`${base}/api/agentsam/message`, {
127
- method: 'POST',
128
- headers: { 'content-type': 'application/json' },
129
- body: JSON.stringify({ message: goal }),
175
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ message: goal }),
130
176
  });
131
177
  } catch (error) {
132
178
  activity.fail('unavailable');
133
179
  throw new Error(`local Agent Sam unavailable at ${base} — run \`npm run dev\` first (${error?.message || error})`);
134
180
  }
135
181
  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
- }
182
+ try { text = await response.text(); }
183
+ catch (error) { activity.fail('response error'); throw new Error(`local Agent Sam response could not be read: ${error?.message || error}`); }
184
+ if (!response.ok) { activity.fail(`HTTP ${response.status}`); throw new Error(`local Agent Sam returned HTTP ${response.status}: ${text.slice(0, 400)}`); }
146
185
  activity.succeed('done');
147
- try {
148
- writeLine(write, JSON.stringify(JSON.parse(text), null, 2));
149
- } catch {
150
- writeLine(write, text);
151
- }
186
+ try { writeLine(write, JSON.stringify(JSON.parse(text), null, 2)); }
187
+ catch { writeLine(write, text); }
152
188
  }
153
189
 
154
190
  async function showLocalLogs(cwd, write) {
155
191
  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
- }
192
+ if (!fs.existsSync(dbPath)) { writeLine(write, ' No local Agent Sam DB found. Run `agentsam init . --yes` first.'); return; }
160
193
  const { createLocalSqliteDatabase } = await import('../local/sqlite.js');
161
194
  const db = await createLocalSqliteDatabase(dbPath);
162
195
  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
- }
170
- 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}`);
174
- }
196
+ 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();
197
+ if (!calls.results.length) { writeLine(write, ' No local Agent Sam tool-call events yet.'); return; }
198
+ writeLine(write, '\n Recent Agent Sam tool calls');
199
+ for (const row of calls.results) writeLine(write, ` ${String(row.created_at || '').padEnd(20)} ${String(row.status || '').padEnd(10)} ${row.tool_name}`);
175
200
  writeLine(write, '');
176
- } finally {
177
- db.close();
201
+ } finally { db.close(); }
202
+ }
203
+
204
+ function formatCount(value) {
205
+ return Math.max(0, Number(value || 0)).toLocaleString('en-US');
206
+ }
207
+
208
+ function formatUsd(value) {
209
+ const amount = Number(value || 0);
210
+ if (!Number.isFinite(amount)) return 'unavailable';
211
+ const currency = String.fromCharCode(36);
212
+ if (amount === 0) return currency + '0.000000';
213
+ return currency + (amount < 0.01 ? amount.toFixed(6) : amount.toFixed(4));
214
+ }
215
+
216
+ export function renderSessionReceipt(session) {
217
+ if (!session) return '';
218
+ const usage = session.cumulative_usage || {};
219
+ const input = Number(usage.input_tokens || 0);
220
+ const output = Number(usage.output_tokens || 0);
221
+ const cached = Number(usage.cached_input_tokens || 0);
222
+ const reasoning = Number(usage.reasoning_tokens || 0);
223
+ const total = input + output;
224
+ const active = Number(session.usage_snapshot?.current_context?.input_tokens || 0);
225
+ const lines = [
226
+ '',
227
+ `Token usage: total=${formatCount(total)} input=${formatCount(input)}${cached ? ` (+ ${formatCount(cached)} cached)` : ''} output=${formatCount(output)}${reasoning ? ` reasoning=${formatCount(reasoning)}` : ''}`,
228
+ `Cost: ${formatUsd(session.total_cost_usd)} · ${session.model_key || 'model unavailable'}${session.actual_service_tier ? ` · ${session.actual_service_tier}` : ''}`,
229
+ ];
230
+ if (active) lines.push(`Active context: ${formatCount(active)} tokens`);
231
+ lines.push('', 'To continue this session, run:', ` agentsam resume ${session.id}`, '', 'Or run:', ' agentsam resume', '', 'and select:', ` ${session.title || 'this session'}`, '');
232
+ return lines.join('\n');
233
+ }
234
+
235
+ function persistSession(state, patch = {}) {
236
+ if (!state.session) return null;
237
+ state.session = saveLocalSession({ ...state.session, ...patch }, { home: state.home });
238
+ return state.session;
239
+ }
240
+
241
+ function recordSessionInput(state, input) {
242
+ const value = String(input || '').trim();
243
+ if (!state.session || !value) return;
244
+ const housekeeping = new Set(['/exit', '/quit', '/session', '/help', '/', '/menu', '/clear']);
245
+ if (housekeeping.has(value.toLowerCase())) return;
246
+ persistSession(state, {
247
+ status: 'active',
248
+ last_input: value,
249
+ title: sessionTitleFromInput(value),
250
+ });
251
+ }
252
+
253
+ function safeToolInput(value, depth = 0) {
254
+ if (depth > 3) return '[nested]';
255
+ if (Array.isArray(value)) return value.slice(0, 12).map((row) => safeToolInput(row, depth + 1));
256
+ if (!value || typeof value !== 'object') return value;
257
+ const output = {};
258
+ for (const [key, item] of Object.entries(value)) {
259
+ if (/token|secret|password|api[_-]?key|authorization/i.test(key)) output[key] = '[REDACTED]';
260
+ else output[key] = safeToolInput(item, depth + 1);
261
+ }
262
+ return output;
263
+ }
264
+
265
+ async function approveModelRequest(preflight, state) {
266
+ const approvedCeiling = Number(state.session?.approved_projected_call_cost_usd || 0);
267
+ if (approvedCeiling > 0 && preflight.projected_max_call_cost_usd <= approvedCeiling) return true;
268
+ if (!state.interactive) return false;
269
+ writeLine(state.write, '');
270
+ writeLine(state.write, ' Model request');
271
+ writeLine(state.write, ` model ${preflight.model}`);
272
+ writeLine(state.write, ` reasoning ${preflight.reasoning_effort}`);
273
+ writeLine(state.write, ` processing ${preflight.service_tier}`);
274
+ writeLine(state.write, ` context ~${formatCount(preflight.estimated_input_tokens)} input tokens`);
275
+ writeLine(state.write, ` max call ${formatUsd(preflight.projected_max_call_cost_usd)} conservative ceiling`);
276
+ if (Number.isFinite(preflight.tokens_until_pricing_threshold)) writeLine(state.write, ` price cliff ${formatCount(preflight.tokens_until_pricing_threshold)} tokens headroom`);
277
+ const approved = await confirm({ message: 'Send this request?', initialValue: true });
278
+ if (isCancel(approved) || approved !== true) return false;
279
+ persistSession(state, { approved_projected_call_cost_usd: Math.max(approvedCeiling, preflight.projected_max_call_cost_usd) });
280
+ return true;
281
+ }
282
+
283
+ async function approveToolExecution(request, state) {
284
+ const sideEffects = request?.descriptor?.side_effects || 'none';
285
+ if (sideEffects === 'none') return true;
286
+ const key = toolApprovalKey(request.capability_id, request.input);
287
+ if (isExecutionApproved({ cwd: state.cwd, key }, { home: state.home })) return true;
288
+ if (!state.interactive) return false;
289
+
290
+ const summary = JSON.stringify(safeToolInput(request.input || {}));
291
+ writeLine(state.write, '');
292
+ writeLine(state.write, ' Agent Sam needs execution permission');
293
+ writeLine(state.write, ` action ${key}`);
294
+ writeLine(state.write, ` target local runtime · ${compactCwd(state.cwd)}`);
295
+ writeLine(state.write, ` effect ${sideEffects}`);
296
+ if (summary && summary !== '{}') writeLine(state.write, ` input ${summary.length > 500 ? `${summary.slice(0, 497)}...` : summary}`);
297
+ writeLine(state.write, ' secrets remain runtime-owned and are not included in the model-visible result.');
298
+
299
+ const choice = await select({
300
+ message: `Allow ${key}?`,
301
+ options: [
302
+ { value: 'once', label: 'Allow once' },
303
+ { value: 'always', label: `Always allow ${key} in this project` },
304
+ { value: 'deny', label: 'Deny' },
305
+ ],
306
+ });
307
+ if (isCancel(choice) || choice === 'deny') return false;
308
+ if (choice === 'always') grantExecutionApproval({ cwd: state.cwd, key, label: key }, { home: state.home });
309
+ return true;
310
+ }
311
+
312
+ async function runInteractiveModelTurn(prompt, state) {
313
+ const { preferences, model } = selectedModel(state.cwd);
314
+ if (model.provider !== 'openai') throw new Error(`interactive_provider_not_implemented:${model.provider}`);
315
+ const credential = resolveProviderCredential(model.provider, { home: state.home });
316
+ if (!credential.configured) throw new Error(`provider_credential_unavailable:${model.provider}:${credential.error || credential.env || 'not_configured'}`);
317
+
318
+ const activity = createRuntimeActivity({ write: state.write, phase: 'thinking', interactive: state.interactive });
319
+ const provider = createOpenAIResponsesAdapter({ apiKey: credential.value });
320
+ const capabilityAdapter = createCapabilityAdapter();
321
+ const samePolicy = state.session
322
+ && state.session.model_key === model.model_key
323
+ && state.session.reasoning_effort === preferences.reasoningEffort
324
+ && state.session.requested_service_tier === preferences.serviceTier;
325
+ const previousResponseId = samePolicy ? state.session?.provider_state?.previous_response_id : null;
326
+ const previousUsageSnapshot = samePolicy ? state.session?.usage_snapshot : null;
327
+
328
+ activity.start('thinking');
329
+ let result;
330
+ try {
331
+ result = await runResponsesAgent({
332
+ provider,
333
+ capabilityAdapter,
334
+ cwd: state.cwd,
335
+ prompt,
336
+ model: model.model_key,
337
+ reasoningEffort: preferences.reasoningEffort,
338
+ serviceTier: preferences.serviceTier,
339
+ previousResponseId,
340
+ previousUsageSnapshot,
341
+ cumulativeUsage: state.session?.cumulative_usage || null,
342
+ promptCacheKey: state.session?.id || undefined,
343
+ runId: state.session?.id || undefined,
344
+ beforeRequest: (preflight) => approveModelRequest(preflight, state),
345
+ beforeTool: (request) => approveToolExecution(request, state),
346
+ emit(event) {
347
+ if (event?.type === 'usage.snapshot') state.usageSnapshot = event.payload;
348
+ },
349
+ });
350
+ } catch (error) {
351
+ activity.fail('failed');
352
+ throw error;
353
+ }
354
+ activity.succeed('done');
355
+ if (result.output_text) {
356
+ writeLine(state.write, '');
357
+ writeLine(state.write, result.output_text);
358
+ writeLine(state.write, '');
359
+ }
360
+ state.usageSnapshot = result.usage_snapshot;
361
+ if (state.session) {
362
+ persistSession(state, {
363
+ status: 'active',
364
+ model_key: model.model_key,
365
+ provider_model_id: result.model,
366
+ reasoning_effort: result.reasoning_effort,
367
+ requested_service_tier: result.requested_service_tier,
368
+ actual_service_tier: result.actual_service_tier,
369
+ provider_state: { provider: model.provider, previous_response_id: result.response_id },
370
+ usage_snapshot: result.usage_snapshot,
371
+ cumulative_usage: result.cumulative_usage,
372
+ total_cost_usd: Number(state.session.total_cost_usd || 0) + Number(result.total_cost_usd || 0),
373
+ last_error: null,
374
+ });
178
375
  }
376
+ return result;
179
377
  }
180
378
 
181
379
  export async function dispatchShellLine(line, state = {}) {
182
380
  const tokens = tokenizeShellLine(line);
183
381
  const write = state.write || ((text) => process.stdout.write(text));
382
+ state.write = write;
184
383
  state.cwd = path.resolve(state.cwd || process.cwd());
185
384
  if (!tokens.length) return { handled: true, exit: false, cwd: state.cwd };
186
-
187
385
  const [command, ...args] = tokens;
188
386
  try {
189
387
  switch (command.toLowerCase()) {
388
+ case '/':
389
+ case '/menu':
390
+ await showCommandPicker(state);
391
+ break;
190
392
  case '/help':
191
393
  write(renderShellCatalog());
192
- return { handled: true, exit: false, cwd: state.cwd };
394
+ break;
193
395
  case '/exit':
194
396
  case '/quit':
195
397
  return { handled: true, exit: true, cwd: state.cwd };
398
+ case '/model': {
399
+ const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false, section: 'model' });
400
+ state.cwd = configured.identity.root;
401
+ break;
402
+ }
403
+ case '/reasoning':
404
+ await chooseReasoning(state.cwd, args, state);
405
+ break;
406
+ case '/fast':
407
+ setServiceTier(state.cwd, 'fast', write);
408
+ break;
409
+ case '/flex':
410
+ setServiceTier(state.cwd, 'flex', write);
411
+ break;
412
+ case '/standard':
413
+ setServiceTier(state.cwd, 'default', write);
414
+ break;
415
+ case '/context':
416
+ if (args[0] === 'repo' || args[0] === 'git') await runContext(['--cwd', state.cwd, ...args.slice(1)]);
417
+ else write(renderContextEconomics(buildContextEconomicsReport(state.cwd, {
418
+ activeInputTokens: state.usageSnapshot?.current_context?.input_tokens,
419
+ estimateKind: state.usageSnapshot?.estimate_kind,
420
+ })));
421
+ break;
196
422
  case '/status':
197
423
  await runStatus(args, { cwd: state.cwd });
198
424
  break;
199
- case '/context':
200
- await runContext(['--cwd', state.cwd, ...args]);
425
+ case '/models':
426
+ await runModels(args, { cwd: state.cwd, write, home: state.home });
427
+ break;
428
+ case '/whoami':
429
+ await runWhoami(args, { write, home: state.home });
430
+ break;
431
+ case '/session':
432
+ if (state.session) write(renderSessionReceipt(state.session));
433
+ else writeLine(write, ' No persistent session is active in this shell invocation.');
434
+ break;
435
+ case '/cf':
436
+ case '/cloudflare':
437
+ await runCloudflare(args.length ? args : ['commands'], { cwd: state.cwd, write });
438
+ break;
439
+ case '/settings': {
440
+ const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false });
441
+ state.cwd = configured.identity.root;
201
442
  break;
443
+ }
202
444
  case '/pwd':
203
445
  writeLine(write, state.cwd);
204
446
  break;
@@ -210,54 +452,59 @@ export async function dispatchShellLine(line, state = {}) {
210
452
  writeLine(write, state.cwd);
211
453
  break;
212
454
  }
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}`);
455
+ case '/git':
456
+ spawnGit(state.cwd, args.length ? args : ['status', '--short', '--branch']);
457
+ break;
458
+ case '/diff':
459
+ spawnGit(state.cwd, ['diff', ...args]);
218
460
  break;
219
- }
220
461
  case '/db':
221
462
  await runDb(args.length ? args : ['status'], { cwd: state.cwd });
222
463
  break;
223
464
  case '/agent':
224
465
  await runLocalAgent(args.join(' '), write, { interactive: state.interactive });
225
466
  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
467
  case '/logs':
235
468
  await showLocalLogs(state.cwd, write);
236
469
  break;
237
470
  case '/deploy':
238
471
  await runDeploy(parseDeployOptions(args, state.cwd));
239
472
  break;
473
+ case '/clear':
474
+ write('\x1b[2J\x1b[H');
475
+ break;
240
476
  default:
477
+ if (!command.startsWith('/')) {
478
+ await runInteractiveModelTurn(line, state);
479
+ break;
480
+ }
241
481
  writeLine(write, ` Unknown Agent Sam command: ${command}`);
242
- writeLine(write, ' Type /help for available commands.');
482
+ writeLine(write, ' Type / or /help for available commands.');
243
483
  return { handled: false, exit: false, cwd: state.cwd };
244
484
  }
245
485
  } catch (error) {
246
- writeLine(write, ` ✗ ${error?.message || error}`);
486
+ if (state.session) {
487
+ persistSession(state, { last_error: diagnosticFromError(error, { source: 'shell', kind: 'interactive_error' }) });
488
+ }
489
+ if (!error?.reported) {
490
+ for (const line of renderDiagnosticError(error).split('\n')) writeLine(write, ` ${line}`);
491
+ }
247
492
  }
248
-
249
493
  return { handled: true, exit: false, cwd: state.cwd };
250
494
  }
251
495
 
252
496
  export async function runShell(argv = [], options = {}) {
253
497
  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) };
498
+ const state = {
499
+ cwd: path.resolve(options.cwd || process.cwd()),
500
+ write,
501
+ interactive: options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY),
502
+ usageSnapshot: options.usageSnapshot || options.session?.usage_snapshot || null,
503
+ session: options.session || null,
504
+ home: options.home,
505
+ };
255
506
  const sub = argv[0] || '';
256
-
257
- if (sub === 'list' || sub === 'status') {
258
- write(renderShellCatalog());
259
- return;
260
- }
507
+ if (sub === 'list' || sub === 'status') { write(renderShellCatalog()); return; }
261
508
  if (sub === '--command' || sub === '--once') {
262
509
  const line = argv.slice(1).join(' ');
263
510
  if (!line) throw new Error(`${sub} requires a slash command`);
@@ -266,32 +513,46 @@ export async function runShell(argv = [], options = {}) {
266
513
  }
267
514
  if (sub) throw new Error(`unknown shell option: ${sub}`);
268
515
 
516
+ if (state.session) {
517
+ state.cwd = path.resolve(state.session.cwd || state.cwd);
518
+ state.session = saveLocalSession({ ...state.session, status: 'active', cwd: state.cwd }, { home: state.home });
519
+ state.usageSnapshot = state.session.usage_snapshot || state.usageSnapshot;
520
+ } else {
521
+ const preferences = readCliPreferences(state.cwd) || {};
522
+ state.session = createLocalSession({
523
+ cwd: state.cwd,
524
+ status: 'active',
525
+ model_key: preferences.modelPreference !== 'auto' ? preferences.modelPreference : null,
526
+ reasoning_effort: preferences.reasoningEffort !== 'auto' ? preferences.reasoningEffort : null,
527
+ requested_service_tier: preferences.serviceTier || 'default',
528
+ }, { home: state.home });
529
+ }
530
+
269
531
  if (options.intro !== 'quiet') {
270
532
  write(renderShellCatalog());
271
- writeLine(write, ' Interactive shell ready. Type /help for commands; /exit to return to your host shell.');
272
- writeLine(write, '');
533
+ writeLine(write, ' Interactive shell ready. Type / for the command picker; /exit to return to your host shell.\n');
273
534
  }
274
-
275
535
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
536
+ let interrupted = false;
537
+ rl.on('SIGINT', () => {
538
+ interrupted = true;
539
+ rl.close();
540
+ });
276
541
  const promptText = () => {
277
542
  if (typeof options.prompt === 'function') return options.prompt(state);
278
543
  if (typeof options.prompt === 'string' && options.prompt) return options.prompt;
279
544
  return renderShellPrompt(state.cwd);
280
545
  };
281
- if (rl.terminal) {
282
- rl.setPrompt(promptText());
283
- rl.prompt();
284
- }
285
-
546
+ if (rl.terminal) { rl.setPrompt(promptText()); rl.prompt(); }
286
547
  for await (const line of rl) {
548
+ recordSessionInput(state, line);
287
549
  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
- }
550
+ if (result.exit) { rl.close(); break; }
551
+ if (rl.terminal) { rl.setPrompt(promptText()); rl.prompt(); }
552
+ }
553
+ if (state.session) {
554
+ persistSession(state, { status: interrupted ? 'interrupted' : 'paused', cwd: state.cwd });
555
+ if (options.receipt !== false) write(renderSessionReceipt(state.session));
296
556
  }
557
+ return state.session;
297
558
  }
@@ -7,7 +7,7 @@
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
8
  import { authenticateViaBrowser } from '../lib/auth.js';
9
9
  import { postJson } from '../lib/core-client.js';
10
- import { resolveSdkKey } from '../../packages/identity/src/contracts/auth-config.js';
10
+ import { resolveAccountSdkKey } from '../lib/account-session.js';
11
11
 
12
12
  const DEFAULT_PORT = 3099;
13
13
 
@@ -51,7 +51,7 @@ function ensureCloudflared() {
51
51
  }
52
52
 
53
53
  async function resolveToken() {
54
- const existing = resolveSdkKey(process.env);
54
+ const existing = resolveAccountSdkKey({ env: process.env }).value;
55
55
  if (existing.startsWith('sdk_')) return existing;
56
56
  const session = await authenticateViaBrowser();
57
57
  const tok = String(session?.access_token || '').trim();