@inneranimalmedia/agentsam-sdk 2.6.0 → 2.6.2

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 (145) hide show
  1. package/docs/AUTH_IDENTITY_CONTRACT.md +25 -27
  2. package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
  3. package/docs/RELEASES.md +7 -7
  4. package/docs/SDK_WORKER.md +4 -3
  5. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  6. package/docs/TEST_TIERS.md +26 -0
  7. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  8. package/package.json +28 -7
  9. package/packages/agentsam-repository/README.md +15 -0
  10. package/packages/agentsam-repository/package.json +25 -0
  11. package/packages/agentsam-repository/src/contracts.js +113 -0
  12. package/packages/agentsam-repository/src/index.js +3 -0
  13. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  14. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  15. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  16. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  17. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  18. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  19. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  20. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  21. package/packages/identity/package.json +2 -2
  22. package/packages/identity/src/contracts/auth-config.js +23 -40
  23. package/packages/identity/tests/auth-config.test.mjs +14 -23
  24. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  25. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  26. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  27. package/protocol/repository/repository-contract.schema.json +24 -0
  28. package/protocol/repository/repository-dependency.schema.json +24 -0
  29. package/protocol/repository/repository-identity.schema.json +17 -0
  30. package/protocol/rpc/v1/common.proto +16 -0
  31. package/protocol/rpc/v1/errors.proto +35 -0
  32. package/protocol/rpc/v1/knowledge.proto +77 -0
  33. package/services/knowledge/package-lock.json +333 -0
  34. package/services/knowledge/package.json +5 -1
  35. package/src/agent/responses-runner.js +63 -35
  36. package/src/capabilities/repository-snapshot.js +3 -3
  37. package/src/cli.js +36 -29
  38. package/src/commands/account-auth.js +1 -1
  39. package/src/commands/context-economics.js +17 -2
  40. package/src/commands/context.js +1 -1
  41. package/src/commands/db.js +20 -3
  42. package/src/commands/deploy.js +2 -2
  43. package/src/commands/env.js +90 -0
  44. package/src/commands/knowledge.js +12 -4
  45. package/src/commands/merkle-persist.js +30 -11
  46. package/src/commands/merkle.js +1 -1
  47. package/src/commands/models.js +123 -65
  48. package/src/commands/ollama.js +26 -0
  49. package/src/commands/preferences.js +53 -26
  50. package/src/commands/providers.js +308 -0
  51. package/src/commands/shell.js +240 -48
  52. package/src/commands/tunnel.js +8 -8
  53. package/src/commands/whoami.js +42 -21
  54. package/src/errors/contract.js +236 -0
  55. package/src/errors/diagnostic.js +1 -0
  56. package/src/errors/index.js +14 -0
  57. package/src/index.js +13 -1
  58. package/src/knowledge/service/auth.js +13 -0
  59. package/src/knowledge/service/grpc-client.js +115 -0
  60. package/src/knowledge/service/grpc-codec.js +237 -0
  61. package/src/knowledge/service/grpc-server.js +83 -0
  62. package/src/knowledge/service/job-engine.js +248 -0
  63. package/src/knowledge/service/server.js +87 -135
  64. package/src/knowledge/source.js +1 -1
  65. package/src/lib/account-session.js +94 -25
  66. package/src/lib/auth.js +314 -51
  67. package/src/lib/cli-preferences.js +31 -4
  68. package/src/lib/core-client.js +75 -30
  69. package/src/lib/deploy-receipt/index.js +2 -2
  70. package/src/lib/detect-context.js +14 -16
  71. package/src/lib/knowledge-docker.js +6 -3
  72. package/src/lib/local-sessions.js +23 -2
  73. package/src/lib/local-status.js +1 -1
  74. package/src/lib/project-config.js +1 -1
  75. package/src/lib/provider-credentials.js +230 -14
  76. package/src/lib/slash-commands.js +5 -3
  77. package/src/local/migrations.js +93 -0
  78. package/src/local/runtime-store.js +141 -0
  79. package/src/local/sqlite.js +2 -0
  80. package/src/local-pty/server.js +113 -51
  81. package/src/models/discovery.js +327 -0
  82. package/src/providers/anthropic-messages.js +192 -0
  83. package/src/providers/cloudflare-chat.js +183 -0
  84. package/src/providers/factory.js +69 -0
  85. package/src/providers/gemini-generate-content.js +208 -0
  86. package/src/providers/index.js +5 -0
  87. package/src/providers/ollama-chat.js +148 -0
  88. package/src/providers/openai-responses.js +226 -75
  89. package/src/repository/index.js +14 -2
  90. package/src/rpc/generated/common_grpc_pb.js +1 -0
  91. package/src/rpc/generated/common_pb.js +536 -0
  92. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  93. package/src/rpc/generated/errors_pb.js +482 -0
  94. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  95. package/src/rpc/generated/knowledge_pb.js +2168 -0
  96. package/src/rpc/generated/package.json +3 -0
  97. package/src/security/trust-boundary.js +2 -2
  98. package/src/telemetry/events.js +4 -1
  99. package/src/ui/cli/activity.js +76 -0
  100. package/src/ui/cli/compaction.js +15 -0
  101. package/src/ui/cli/footer.js +39 -0
  102. package/src/ui/cli/help.js +194 -0
  103. package/src/ui/cli/plan.js +20 -0
  104. package/src/ui/cli/runtime-events.js +110 -0
  105. package/src/ui/cli/waiting.js +16 -0
  106. package/src/ui/merkle/render.js +1 -1
  107. package/test/account-session.test.mjs +48 -11
  108. package/test/apps-scaffold-contract.test.mjs +43 -0
  109. package/test/cli/preferences-runtime.test.mjs +11 -0
  110. package/test/cli/runtime-ui.test.mjs +74 -0
  111. package/test/error-diagnostics.test.mjs +57 -1
  112. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  113. package/test/integration/cli-help.test.mjs +37 -0
  114. package/test/integration/knowledge-rpc.test.mjs +112 -0
  115. package/test/integration/merkle-cli.test.mjs +61 -0
  116. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  117. package/test/integration/provider-env-cli.test.mjs +49 -0
  118. package/test/integration/provider-factory.test.mjs +197 -0
  119. package/test/integration/repository-company-graph.test.mjs +90 -0
  120. package/test/integration/runtime-migrations.test.mjs +82 -0
  121. package/test/knowledge-service.test.mjs +5 -0
  122. package/test/knowledge.test.mjs +16 -0
  123. package/test/live/terminal-transport.live.test.mjs +24 -0
  124. package/test/local-sessions.test.mjs +7 -1
  125. package/test/models.test.mjs +101 -4
  126. package/test/ollama.test.mjs +21 -0
  127. package/test/portable-context.test.mjs +1 -1
  128. package/test/provider-credentials.test.mjs +45 -1
  129. package/test/release-hygiene.test.mjs +13 -5
  130. package/test/responses-runner.test.mjs +3 -1
  131. package/test/sdk-worker-contract.test.mjs +5 -1
  132. package/test/shell.test.mjs +50 -8
  133. package/test/smoke.mjs +2 -1
  134. package/test/terminal/local-pty.mock.test.mjs +151 -0
  135. package/test/whoami-resume.test.mjs +6 -5
  136. package/src/lib/prompt-byok.js +0 -57
  137. package/src/lib/save-sdk-token.js +0 -19
  138. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  139. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  140. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  141. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  142. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  143. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  144. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  145. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -1,27 +1,67 @@
1
1
  import { resolveIamOrigin } from '../../packages/identity/src/contracts/auth-config.js';
2
+ import { resolveAccountAuthority } from './auth.js';
2
3
 
3
4
  /**
4
5
  * IAM CORE client — SDK is a delivery mechanism; intelligence lives server-side.
5
6
  *
6
- * IAM_ORIGIN is the canonical platform authority/API origin. During the
7
- * migration window IAM_CORE_URL / AGENTSAM_CORE_URL remain compatibility-only
8
- * fallbacks. Without a CORE backend, consumers get scaffold/local CLI only.
7
+ * IAM_OAUTH_ISSUER is the canonical platform authority/API origin. IAM_ORIGIN,
8
+ * IAM_CORE_URL and AGENTSAM_CORE_URL remain compatibility fallbacks.
9
9
  */
10
-
11
10
  export function coreBaseUrl(env = process.env) {
12
- const explicit = env?.IAM_ORIGIN || env?.IAM_CORE_URL || env?.AGENTSAM_CORE_URL || '';
11
+ const explicit = env?.IAM_OAUTH_ISSUER || env?.IAM_ORIGIN || env?.IAM_CORE_URL || env?.AGENTSAM_CORE_URL || '';
13
12
  return resolveIamOrigin(env, explicit);
14
13
  }
15
14
 
16
- export async function postJson(path, body, token) {
17
- const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
18
- if (token) headers.Authorization = `Bearer ${token}`;
19
- const res = await fetch(`${coreBaseUrl()}${path}`, {
15
+ function authOptions(options) {
16
+ if (typeof options === 'string') return { explicit: options };
17
+ return options && typeof options === 'object' ? options : {};
18
+ }
19
+
20
+ export async function resolveCoreAuthority(options = {}) {
21
+ const resolvedOptions = authOptions(options);
22
+ const resolver = resolvedOptions.resolveAuthorityImpl || resolveAccountAuthority;
23
+ const authority = await resolver({
24
+ env: resolvedOptions.env || process.env,
25
+ home: resolvedOptions.home,
26
+ explicit: resolvedOptions.explicit || resolvedOptions.token || '',
27
+ fetchImpl: resolvedOptions.fetchImpl,
28
+ issuer: resolvedOptions.issuer,
29
+ signal: resolvedOptions.signal,
30
+ nowMs: resolvedOptions.nowMs,
31
+ skewMs: resolvedOptions.skewMs,
32
+ refreshImpl: resolvedOptions.refreshImpl,
33
+ });
34
+ if (authority?.error) throw new Error(authority.error);
35
+ if (!authority?.value) throw new Error('account_auth_required');
36
+ return authority;
37
+ }
38
+
39
+ async function authorizedHeaders(headers, options = {}) {
40
+ const authority = await resolveCoreAuthority(options);
41
+ return {
42
+ ...headers,
43
+ Authorization: `Bearer ${authority.value}`,
44
+ };
45
+ }
46
+
47
+ async function responseJson(res) {
48
+ return res.json().catch(() => ({}));
49
+ }
50
+
51
+ export async function postJson(path, body, options = {}) {
52
+ const resolvedOptions = authOptions(options);
53
+ const fetchImpl = resolvedOptions.fetchImpl || fetch;
54
+ const headers = await authorizedHeaders({
55
+ 'Content-Type': 'application/json',
56
+ Accept: 'application/json',
57
+ }, resolvedOptions);
58
+ const res = await fetchImpl(`${coreBaseUrl(resolvedOptions.env || process.env)}${path}`, {
20
59
  method: 'POST',
21
60
  headers,
22
61
  body: JSON.stringify(body ?? {}),
62
+ signal: resolvedOptions.signal,
23
63
  });
24
- const data = await res.json().catch(() => ({}));
64
+ const data = await responseJson(res);
25
65
  if (!res.ok) {
26
66
  const msg = data?.error || data?.message || `HTTP ${res.status}`;
27
67
  throw new Error(String(msg));
@@ -29,11 +69,15 @@ export async function postJson(path, body, token) {
29
69
  return data;
30
70
  }
31
71
 
32
- export async function getJson(path, token) {
33
- const headers = { Accept: 'application/json' };
34
- if (token) headers.Authorization = `Bearer ${token}`;
35
- const res = await fetch(`${coreBaseUrl()}${path}`, { headers });
36
- const data = await res.json().catch(() => ({}));
72
+ export async function getJson(path, options = {}) {
73
+ const resolvedOptions = authOptions(options);
74
+ const fetchImpl = resolvedOptions.fetchImpl || fetch;
75
+ const headers = await authorizedHeaders({ Accept: 'application/json' }, resolvedOptions);
76
+ const res = await fetchImpl(`${coreBaseUrl(resolvedOptions.env || process.env)}${path}`, {
77
+ headers,
78
+ signal: resolvedOptions.signal,
79
+ });
80
+ const data = await responseJson(res);
37
81
  if (!res.ok) {
38
82
  const msg = data?.error || data?.message || `HTTP ${res.status}`;
39
83
  throw new Error(String(msg));
@@ -44,18 +88,21 @@ export async function getJson(path, token) {
44
88
  /**
45
89
  * Stream NDJSON from POST /api/sdk/scaffold — calls onEvent for each line.
46
90
  */
47
- export async function streamScaffold(body, token, onEvent) {
48
- const res = await fetch(`${coreBaseUrl()}/api/sdk/scaffold`, {
91
+ export async function streamScaffold(body, onEvent, options = {}) {
92
+ const resolvedOptions = authOptions(options);
93
+ const fetchImpl = resolvedOptions.fetchImpl || fetch;
94
+ const headers = await authorizedHeaders({
95
+ 'Content-Type': 'application/json',
96
+ Accept: 'application/x-ndjson',
97
+ }, resolvedOptions);
98
+ const res = await fetchImpl(`${coreBaseUrl(resolvedOptions.env || process.env)}/api/sdk/scaffold`, {
49
99
  method: 'POST',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- Accept: 'application/x-ndjson',
53
- Authorization: `Bearer ${token}`,
54
- },
100
+ headers,
55
101
  body: JSON.stringify(body),
102
+ signal: resolvedOptions.signal,
56
103
  });
57
104
  if (!res.ok) {
58
- const data = await res.json().catch(() => ({}));
105
+ const data = await responseJson(res);
59
106
  throw new Error(data?.error || `scaffold HTTP ${res.status}`);
60
107
  }
61
108
  if (!res.body) throw new Error('scaffold stream missing');
@@ -71,15 +118,13 @@ export async function streamScaffold(body, token, onEvent) {
71
118
  const lines = buf.split('\n');
72
119
  buf = lines.pop() || '';
73
120
  for (const line of lines) {
74
- const t = line.trim();
75
- if (!t) continue;
76
- let evt;
121
+ const text = line.trim();
122
+ if (!text) continue;
77
123
  try {
78
- evt = JSON.parse(t);
124
+ await onEvent(JSON.parse(text));
79
125
  } catch {
80
- continue;
126
+ /* ignore malformed stream lines */
81
127
  }
82
- await onEvent(evt);
83
128
  }
84
129
  }
85
130
  const tail = buf.trim();
@@ -87,7 +132,7 @@ export async function streamScaffold(body, token, onEvent) {
87
132
  try {
88
133
  await onEvent(JSON.parse(tail));
89
134
  } catch {
90
- /* ignore */
135
+ /* ignore malformed trailing line */
91
136
  }
92
137
  }
93
138
  }
@@ -2,8 +2,8 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { randomBytes } from 'node:crypto';
5
- import { buildMerkleTree, diffTrees, readSnapshot, validateSnapshot } from '../merkle/index.js';
6
- import { normalizePolicy } from '../merkle/policy.js';
5
+ import { buildMerkleTree, diffTrees, readSnapshot, validateSnapshot } from '../../../packages/agentsam-repository/src/merkle/index.js';
6
+ import { normalizePolicy } from '../../../packages/agentsam-repository/src/merkle/policy.js';
7
7
 
8
8
  export const DEFAULT_DEPLOY_EXCLUDES = Object.freeze([
9
9
  '.agentsam/deploy-merkle',
@@ -5,7 +5,7 @@
5
5
  import { execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { coreBaseUrl } from './core-client.js';
8
- import { resolveAccountSdkKey } from './account-session.js';
8
+ import { resolveAccountAuth } from './account-session.js';
9
9
 
10
10
  const execFileAsync = promisify(execFile);
11
11
 
@@ -167,11 +167,9 @@ async function detectCloudflare() {
167
167
  };
168
168
  }
169
169
 
170
- async function probeSdkBearer(token) {
170
+ async function probeAccountBearer(token) {
171
171
  const t = String(token || '').trim();
172
- if (!t || !t.startsWith('sdk_')) {
173
- return { valid: false, error: 'not_sdk_bearer' };
174
- }
172
+ if (!t) return { valid: false, error: 'account_auth_required' };
175
173
  try {
176
174
  const res = await fetch(`${coreBaseUrl()}/api/sdk/context`, {
177
175
  headers: { Accept: 'application/json', Authorization: `Bearer ${t}` },
@@ -197,7 +195,7 @@ function envPresent(name) {
197
195
  return v != null && String(v).trim() !== '';
198
196
  }
199
197
 
200
- /** IAM auth for SDK init — only verified sdk_* bearer counts as ready. */
198
+ /** IAM auth for SDK init — accepts a verified account API key or browser session credential. */
201
199
  async function detectIam(explicitToken = '') {
202
200
  const aux = [];
203
201
 
@@ -210,9 +208,9 @@ async function detectIam(explicitToken = '') {
210
208
  });
211
209
  }
212
210
 
213
- const workerKey = envPresent('IAM_API_KEY') || envPresent('AGENTSAM_API_KEY');
211
+ const workerKey = envPresent('IAM_API_KEY');
214
212
  if (workerKey) {
215
- const name = envPresent('IAM_API_KEY') ? 'IAM_API_KEY' : 'AGENTSAM_API_KEY';
213
+ const name = 'IAM_API_KEY';
216
214
  aux.push({
217
215
  var: name,
218
216
  role: 'Worker runtime secret',
@@ -221,22 +219,22 @@ async function detectIam(explicitToken = '') {
221
219
  });
222
220
  }
223
221
 
224
- const sdkToken = resolveAccountSdkKey({ env: process.env, explicit: explicitToken }).value;
225
- if (sdkToken.trim()) {
226
- const probe = await probeSdkBearer(sdkToken);
222
+ const accountAuth = resolveAccountAuth({ env: process.env, explicit: explicitToken });
223
+ if (accountAuth.value) {
224
+ const probe = await probeAccountBearer(accountAuth.value);
227
225
  if (probe.valid) {
228
226
  return {
229
- source: 'sdk-key',
227
+ source: accountAuth.kind || 'account-auth',
230
228
  ready: true,
231
- detail: `AGENTSAM_SDK_KEY · user ${probe.user_id || '?'}`,
229
+ detail: `${accountAuth.kind === 'api_key' ? 'AGENTSAM_API_KEY' : 'browser session'} · user ${probe.user_id || '?'}`,
232
230
  probe,
233
231
  aux,
234
232
  };
235
233
  }
236
234
  return {
237
- source: 'sdk-key',
235
+ source: accountAuth.kind || 'account-auth',
238
236
  ready: false,
239
- detail: `AGENTSAM_SDK_KEY invalid (${probe.error}) → will open browser`,
237
+ detail: `${accountAuth.kind === 'api_key' ? 'AGENTSAM_API_KEY' : 'browser session'} invalid (${probe.error}) → will open browser`,
240
238
  probe,
241
239
  aux,
242
240
  };
@@ -247,7 +245,7 @@ async function detectIam(explicitToken = '') {
247
245
  return {
248
246
  source: 'execos-env',
249
247
  ready: false,
250
- detail: `IAM_PTY_USER_ID set (ExecOS identity — SDK bearer still needed)`,
248
+ detail: `IAM_PTY_USER_ID set (ExecOS identity — account auth still needed)`,
251
249
  aux,
252
250
  };
253
251
  }
@@ -7,12 +7,15 @@ import { CONFIG_PATH, defaultConfig, readConfig } from '../knowledge/config.js';
7
7
 
8
8
  const sdkRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
9
9
  const runtimeFiles = () => {
10
- const files = ['services/knowledge/package.json', 'services/knowledge/package-lock.json', 'src/lib/merkle/hash.js'];
10
+ const files = ['services/knowledge/package.json', 'services/knowledge/package-lock.json', 'packages/agentsam-repository/src/merkle/hash.js', 'src/errors/contract.js'];
11
11
  const walk = dir => { for (const entry of fs.readdirSync(path.join(sdkRoot, dir), { withFileTypes: true })) {
12
12
  const file = `${dir}/${entry.name}`;
13
- if (entry.isDirectory()) walk(file); else if (entry.isFile() && /\.(js|sql)$/.test(entry.name)) files.push(file);
13
+ if (entry.isDirectory()) walk(file);
14
+ else if (entry.isFile() && (/\.(js|sql)$/.test(entry.name) || file === 'src/rpc/generated/package.json')) files.push(file);
14
15
  } };
15
- walk('src/knowledge'); return files.sort();
16
+ walk('src/knowledge');
17
+ walk('src/rpc/generated');
18
+ return files.sort();
16
19
  };
17
20
 
18
21
  export function generateKnowledgeDocker(opts = {}) {
@@ -52,17 +52,37 @@ function normalizeUsage(value = {}) {
52
52
  };
53
53
  }
54
54
 
55
+ function normalizeCostBreakdown(value = {}) {
56
+ return {
57
+ input: Number(value.input || 0),
58
+ cached_input: Number(value.cached_input || 0),
59
+ cache_write: Number(value.cache_write || 0),
60
+ output: Number(value.output || 0),
61
+ };
62
+ }
63
+
64
+ export function localSessionElapsedMs(session = {}, at = Date.now()) {
65
+ const accumulated = Math.max(0, Number(session.active_elapsed_ms || 0));
66
+ const startedAt = Date.parse(clean(session.active_started_at));
67
+ if (clean(session.status) !== 'active' || !Number.isFinite(startedAt)) return accumulated;
68
+ return accumulated + Math.max(0, Number(at) - startedAt);
69
+ }
70
+
55
71
  export function normalizeLocalSession(value = {}) {
56
72
  const createdAt = clean(value.created_at) || now();
73
+ const status = clean(value.status) || 'active';
74
+ const updatedAt = clean(value.updated_at) || createdAt;
57
75
  return {
58
76
  schema_version: LOCAL_SESSION_SCHEMA,
59
77
  id: validateSessionId(value.id || createLocalSessionId()),
60
- status: clean(value.status) || 'active',
78
+ status,
61
79
  cwd: path.resolve(clean(value.cwd) || process.cwd()),
62
80
  title: clean(value.title) || sessionTitleFromInput(value.last_input),
63
81
  last_input: clean(value.last_input) || null,
64
82
  created_at: createdAt,
65
- updated_at: clean(value.updated_at) || createdAt,
83
+ updated_at: updatedAt,
84
+ active_elapsed_ms: Math.max(0, Number(value.active_elapsed_ms || 0)),
85
+ active_started_at: clean(value.active_started_at) || (status === 'active' ? updatedAt : null),
66
86
  model_key: clean(value.model_key) || null,
67
87
  provider_model_id: clean(value.provider_model_id) || null,
68
88
  reasoning_effort: clean(value.reasoning_effort) || null,
@@ -72,6 +92,7 @@ export function normalizeLocalSession(value = {}) {
72
92
  usage_snapshot: value.usage_snapshot && typeof value.usage_snapshot === 'object' ? structuredClone(value.usage_snapshot) : null,
73
93
  cumulative_usage: normalizeUsage(value.cumulative_usage || {}),
74
94
  total_cost_usd: Number(value.total_cost_usd || 0),
95
+ cost_breakdown_usd: normalizeCostBreakdown(value.cost_breakdown_usd || {}),
75
96
  approved_projected_call_cost_usd: Number(value.approved_projected_call_cost_usd || 0),
76
97
  last_error: value.last_error && typeof value.last_error === 'object' ? structuredClone(value.last_error) : null,
77
98
  };
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { tryResolveGitContext } from './git-context.js';
3
+ import { tryResolveGitContext } from '../../packages/agentsam-repository/src/git-context.js';
4
4
  import { inspectLocalSqlite } from '../local/sqlite.js';
5
5
  import { getCreatedWithVersion, getDefaultProfile, getDeployTarget, getLocalDatabasePath, getProjectName, getProjectPreset, getRepositoryId, tryReadProjectConfig } from './project-config.js';
6
6
 
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { tryResolveGitContext } from './git-context.js';
4
+ import { tryResolveGitContext } from '../../packages/agentsam-repository/src/git-context.js';
5
5
 
6
6
  export const PROJECT_CONFIG_PATH = '.agentsam/config.json';
7
7
  export const PROJECT_CONFIG_SCHEMA_VERSION = 2;
@@ -2,16 +2,58 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
- const PROVIDER_CREDENTIALS = Object.freeze({
6
- openai: Object.freeze({ env: 'OPENAI_API_KEY', files: ['openai.env'] }),
7
- gemini: Object.freeze({ env: 'GEMINI_API_KEY', files: ['gemini.env'] }),
8
- anthropic: Object.freeze({ env: 'ANTHROPIC_API_KEY', files: ['anthropic.env'] }),
9
- grok: Object.freeze({ env: 'XAI_API_KEY', files: ['grok.env', 'xai.env'] }),
10
- cloudflare: Object.freeze({ env: 'CLOUDFLARE_API_TOKEN', files: ['cloudflare.env'] }),
5
+ export const PROVIDER_CREDENTIALS = Object.freeze({
6
+ openai: Object.freeze({ label: 'OpenAI', env: 'OPENAI_API_KEY', files: ['openai.env'], modelProvider: 'openai' }),
7
+ anthropic: Object.freeze({ label: 'Anthropic', env: 'ANTHROPIC_API_KEY', files: ['anthropic.env'], modelProvider: 'anthropic' }),
8
+ gemini: Object.freeze({ label: 'Gemini', env: 'GEMINI_API_KEY', files: ['gemini.env'], modelProvider: 'gemini' }),
9
+ cursor: Object.freeze({ label: 'Cursor', env: 'CURSOR_API_KEY', files: ['cursor.env'], modelProvider: 'cursor' }),
10
+ xai: Object.freeze({ label: 'xAI', env: 'XAI_API_KEY', files: ['xai.env', 'grok.env'], modelProvider: 'xai' }),
11
+ cloudflare: Object.freeze({
12
+ label: 'Cloudflare',
13
+ env: 'CLOUDFLARE_API_TOKEN',
14
+ files: ['cloudflare.env'],
15
+ modelProvider: 'cloudflare',
16
+ accountEnv: ['ACCOUNT_ID', 'CLOUDFLARE_ACCOUNT_ID'],
17
+ }),
18
+ inneranimalmedia: Object.freeze({
19
+ label: 'InnerAnimalMedia',
20
+ env: 'AGENTSAM_API_KEY',
21
+ files: ['inneranimalmedia.env'],
22
+ tokenPrefix: 'aak_',
23
+ platformCredential: true,
24
+ }),
25
+ });
26
+
27
+ const PROVIDER_ALIASES = Object.freeze({
28
+ grok: 'xai',
29
+ iam: 'inneranimalmedia',
30
+ inneranimal: 'inneranimalmedia',
11
31
  });
12
32
 
13
33
  function clean(value) { return value == null ? '' : String(value).trim(); }
14
34
 
35
+ export function normalizeProviderId(provider) {
36
+ const id = clean(provider).toLowerCase();
37
+ return PROVIDER_ALIASES[id] || id;
38
+ }
39
+
40
+ function normalizeCloudflareAccountId(value) {
41
+ const id = clean(value);
42
+ if (!id) return '';
43
+ if (!/^[a-f0-9]{32}$/i.test(id)) throw new Error('invalid_cloudflare_account_id');
44
+ return id;
45
+ }
46
+
47
+ function validateCredentialValue(spec, value) {
48
+ const secret = clean(value);
49
+ if (!secret) throw new Error('credential_required');
50
+ if (/[\r\n\0]/.test(secret)) throw new Error('credential_must_be_single_line');
51
+ if (spec.tokenPrefix && !secret.startsWith(spec.tokenPrefix)) {
52
+ throw new Error(`credential_prefix_required:${spec.tokenPrefix}`);
53
+ }
54
+ return secret;
55
+ }
56
+
15
57
  function homeDirectory(options = {}) {
16
58
  return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
17
59
  }
@@ -36,45 +78,219 @@ function secureFile(filename) {
36
78
  return { ok: true, mode: stat.mode & 0o777 };
37
79
  }
38
80
 
81
+ function firstEnvValue(source, names = []) {
82
+ for (const name of names) {
83
+ const value = clean(parseEnvValue(source, name));
84
+ if (value) return value;
85
+ }
86
+ return '';
87
+ }
88
+
89
+ function firstRuntimeValue(env, names = []) {
90
+ for (const name of names) {
91
+ const value = clean(env?.[name]);
92
+ if (value) return value;
93
+ }
94
+ return '';
95
+ }
96
+
97
+ function atomicWrite(filename, source, mode = 0o600) {
98
+ const dir = path.dirname(filename);
99
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
100
+ if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
101
+ const temp = `${filename}.${process.pid}.${Date.now()}.tmp`;
102
+ fs.writeFileSync(temp, source, { mode });
103
+ if (process.platform !== 'win32') fs.chmodSync(temp, mode);
104
+ fs.renameSync(temp, filename);
105
+ if (process.platform !== 'win32') fs.chmodSync(filename, mode);
106
+ }
107
+
108
+ function envLiteral(value) {
109
+ const text = String(value ?? '');
110
+ if (/['"\r\n\0]/.test(text)) throw new Error('credential_contains_unsupported_profile_character');
111
+ return `"${text}"`;
112
+ }
113
+
114
+ export function agentEnvDirectory(options = {}) {
115
+ return path.join(homeDirectory(options), '.agentsam', 'env.d');
116
+ }
117
+
118
+ export function agentEnvLoaderPath(options = {}) {
119
+ return path.join(homeDirectory(options), '.agentsam', 'load-agent-env.sh');
120
+ }
121
+
122
+ export function ensureAgentEnvLoader(options = {}) {
123
+ const filename = agentEnvLoaderPath(options);
124
+ const supported = Object.keys(PROVIDER_CREDENTIALS).join('|');
125
+ const source = `# AgentSam provider environment loader. Source this file; do not execute it.
126
+ if [ "$#" -eq 0 ]; then
127
+ echo "usage: source ~/.agentsam/load-agent-env.sh <profile> [profile ...]" >&2
128
+ return 2 2>/dev/null || exit 2
129
+ fi
130
+ for _agentsam_profile in "$@"; do
131
+ case "$_agentsam_profile" in
132
+ grok) _agentsam_profile="xai" ;;
133
+ ${supported}) ;;
134
+ *) echo "unknown AgentSam provider profile: $_agentsam_profile" >&2; return 2 2>/dev/null || exit 2 ;;
135
+ esac
136
+ _agentsam_file="\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env"
137
+ if [ ! -f "$_agentsam_file" ]; then
138
+ echo "AgentSam provider profile not found: $_agentsam_file" >&2
139
+ return 1 2>/dev/null || exit 1
140
+ fi
141
+ set -a
142
+ . "$_agentsam_file"
143
+ set +a
144
+ if [ "$_agentsam_profile" = cloudflare ]; then
145
+ if [ -z "\${ACCOUNT_ID:-}" ] && [ -n "\${CLOUDFLARE_ACCOUNT_ID:-}" ]; then export ACCOUNT_ID="$CLOUDFLARE_ACCOUNT_ID"; fi
146
+ if [ -z "\${CLOUDFLARE_ACCOUNT_ID:-}" ] && [ -n "\${ACCOUNT_ID:-}" ]; then export CLOUDFLARE_ACCOUNT_ID="$ACCOUNT_ID"; fi
147
+ fi
148
+ done
149
+ unset _agentsam_file _agentsam_profile
150
+ `;
151
+ atomicWrite(filename, source, 0o700);
152
+ return filename;
153
+ }
154
+
155
+ function profileSource(provider, credential = '', options = {}) {
156
+ const spec = providerCredentialSpec(provider);
157
+ if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
158
+ const lines = [`# AgentSam ${spec.label} provider profile`];
159
+ if (spec.provider === 'cloudflare') {
160
+ lines.push('# ACCOUNT_ID is your Cloudflare account identifier; it is not a secret.');
161
+ lines.push(`export ACCOUNT_ID=${envLiteral(normalizeCloudflareAccountId(options.accountId))}`);
162
+ }
163
+ lines.push(`export ${spec.env}=${envLiteral(credential)}`);
164
+ return `${lines.join('\n')}\n`;
165
+ }
166
+
39
167
  export function providerCredentialSpec(provider) {
40
- const id = clean(provider).toLowerCase();
168
+ const id = normalizeProviderId(provider);
41
169
  const spec = PROVIDER_CREDENTIALS[id];
42
170
  return spec ? Object.freeze({ provider: id, ...spec }) : null;
43
171
  }
44
172
 
173
+ export function ensureProviderEnvProfile(provider, options = {}) {
174
+ const spec = providerCredentialSpec(provider);
175
+ if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
176
+ const dir = agentEnvDirectory(options);
177
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
178
+ if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
179
+ const filename = path.join(dir, spec.files[0]);
180
+ let created = false;
181
+ if (!fs.existsSync(filename)) {
182
+ atomicWrite(filename, profileSource(spec.provider, '', options), 0o600);
183
+ created = true;
184
+ } else {
185
+ const safety = secureFile(filename);
186
+ if (!safety.ok) throw new Error(safety.error);
187
+ if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
188
+ if (spec.provider === 'cloudflare' && clean(options.accountId)) {
189
+ const source = fs.readFileSync(filename, 'utf8');
190
+ const current = firstEnvValue(source, spec.accountEnv || []);
191
+ if (!current) {
192
+ const accountId = normalizeCloudflareAccountId(options.accountId);
193
+ const line = `export ACCOUNT_ID=${envLiteral(accountId)}`;
194
+ const next = /^(?:export\s+)?ACCOUNT_ID=.*$/m.test(source)
195
+ ? source.replace(/^(?:export\s+)?ACCOUNT_ID=.*$/m, line)
196
+ : `${line}\n${source}`;
197
+ atomicWrite(filename, next, 0o600);
198
+ }
199
+ }
200
+ }
201
+ const loader = ensureAgentEnvLoader(options);
202
+ return Object.freeze({
203
+ provider: spec.provider,
204
+ file: filename,
205
+ loader,
206
+ created,
207
+ source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}`,
208
+ });
209
+ }
210
+
211
+ export function setProviderCredential(provider, credential, options = {}) {
212
+ const spec = providerCredentialSpec(provider);
213
+ if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
214
+ const value = validateCredentialValue(spec, credential);
215
+ const filename = path.join(agentEnvDirectory(options), spec.files[0]);
216
+ atomicWrite(filename, profileSource(spec.provider, value, options), 0o600);
217
+ const loader = ensureAgentEnvLoader(options);
218
+ return Object.freeze({
219
+ provider: spec.provider,
220
+ file: filename,
221
+ loader,
222
+ source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}`,
223
+ });
224
+ }
225
+
226
+ export function removeProviderCredential(provider, options = {}) {
227
+ const spec = providerCredentialSpec(provider);
228
+ if (!spec) throw new Error(`unsupported_provider:${normalizeProviderId(provider)}`);
229
+ const dir = agentEnvDirectory(options);
230
+ let removed = false;
231
+ for (const basename of spec.files) {
232
+ const filename = path.join(dir, basename);
233
+ if (!fs.existsSync(filename)) continue;
234
+ const safety = secureFile(filename);
235
+ if (!safety.ok) throw new Error(safety.error);
236
+ fs.rmSync(filename, { force: true });
237
+ removed = true;
238
+ }
239
+ return Object.freeze({ provider: spec.provider, removed });
240
+ }
241
+
45
242
  export function resolveProviderCredential(provider, options = {}) {
46
243
  const spec = providerCredentialSpec(provider);
47
- if (!spec) return Object.freeze({ provider: clean(provider).toLowerCase(), configured: false, source: null, error: 'unsupported_provider', value: '' });
244
+ if (!spec) return Object.freeze({ provider: normalizeProviderId(provider), configured: false, source: null, error: 'unsupported_provider', value: '' });
48
245
  const env = options.env || process.env;
246
+ const accountIdFromEnv = firstRuntimeValue(env, spec.accountEnv || []);
49
247
  const fromEnv = clean(env?.[spec.env]);
50
- if (fromEnv) return Object.freeze({ provider: spec.provider, configured: true, source: 'environment', env: spec.env, file: null, error: null, value: fromEnv });
248
+ if (fromEnv) {
249
+ const prefixError = spec.tokenPrefix && !fromEnv.startsWith(spec.tokenPrefix) ? `credential_prefix_required:${spec.tokenPrefix}` : null;
250
+ return Object.freeze({
251
+ provider: spec.provider,
252
+ configured: !prefixError,
253
+ source: 'environment',
254
+ env: spec.env,
255
+ file: null,
256
+ error: prefixError,
257
+ value: prefixError ? '' : fromEnv,
258
+ account_id: accountIdFromEnv || null,
259
+ });
260
+ }
51
261
 
52
- const dir = path.join(homeDirectory({ ...options, env }), '.agentsam', 'env.d');
262
+ const dir = agentEnvDirectory({ ...options, env });
53
263
  for (const basename of spec.files) {
54
264
  const filename = path.join(dir, basename);
55
265
  if (!fs.existsSync(filename)) continue;
56
266
  try {
57
267
  const safety = secureFile(filename);
58
268
  if (!safety.ok) return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: safety.error, value: '' });
59
- const value = clean(parseEnvValue(fs.readFileSync(filename, 'utf8'), spec.env));
60
- if (value) return Object.freeze({ provider: spec.provider, configured: true, source: 'agentsam_env_file', env: spec.env, file: filename, error: null, value });
61
- return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: 'credential_variable_missing', value: '' });
269
+ const source = fs.readFileSync(filename, 'utf8');
270
+ const value = clean(parseEnvValue(source, spec.env));
271
+ const accountId = accountIdFromEnv || firstEnvValue(source, spec.accountEnv || []);
272
+ const prefixError = spec.tokenPrefix && value && !value.startsWith(spec.tokenPrefix) ? `credential_prefix_required:${spec.tokenPrefix}` : null;
273
+ if (value && !prefixError) return Object.freeze({ provider: spec.provider, configured: true, source: 'agentsam_env_file', env: spec.env, file: filename, error: null, value, account_id: accountId || null });
274
+ return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: prefixError || 'credential_variable_missing', value: '' });
62
275
  } catch (error) {
63
276
  return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: error?.message || String(error), value: '' });
64
277
  }
65
278
  }
66
- return Object.freeze({ provider: spec.provider, configured: false, source: null, env: spec.env, file: null, error: null, value: '' });
279
+ return Object.freeze({ provider: spec.provider, configured: false, source: null, env: spec.env, file: null, error: null, value: '', account_id: accountIdFromEnv || null });
67
280
  }
68
281
 
69
282
  export function describeProviderCredential(provider, options = {}) {
70
283
  const resolved = resolveProviderCredential(provider, options);
284
+ const spec = providerCredentialSpec(provider);
71
285
  return Object.freeze({
72
286
  provider: resolved.provider,
287
+ label: spec?.label || resolved.provider,
73
288
  configured: resolved.configured,
74
289
  source: resolved.source,
75
290
  env: resolved.env || null,
76
291
  file: resolved.file || null,
77
292
  error: resolved.error || null,
293
+ account_id: resolved.account_id || null,
78
294
  });
79
295
  }
80
296
 
@@ -11,10 +11,12 @@ export const SLASH_COMMANDS = [
11
11
  { cmd: '/context', description: 'Show model context economics; add repo for Git bridge context', lane: 'context' },
12
12
  { cmd: '/status', description: 'Local project, DB, Git, and PTY health', lane: 'local' },
13
13
  { cmd: '/models', description: 'Probe providers and provider-verified known models', lane: 'model' },
14
- { cmd: '/login', description: 'Authenticate IAM and save the machine-local Agent Sam session', lane: 'identity' },
15
- { cmd: '/logout', description: 'Remove the local IAM session without deleting provider keys', lane: 'identity' },
16
- { cmd: '/whoami', description: 'Show authenticated IAM identity and safe credential status', lane: 'identity' },
14
+ { cmd: '/providers', description: 'Configure and verify machine provider credentials', lane: 'model' },
15
+ { cmd: '/login', description: 'Sign in to Inner Animal Media and save the machine-local Agent Sam session', lane: 'identity' },
16
+ { cmd: '/logout', description: 'Sign out locally without deleting provider credentials', lane: 'identity' },
17
+ { cmd: '/whoami', description: 'Show authenticated account identity and safe credential status', lane: 'identity' },
17
18
  { cmd: '/session', description: 'Show current session usage, cost, and resume receipt', lane: 'observability' },
19
+ { cmd: '/usage', description: 'Show token usage, spend breakdown, and resume command', lane: 'observability' },
18
20
  { cmd: '/cf', description: 'Cloudflare native reads, Wrangler status, and CPU profile analysis', lane: 'cloudflare' },
19
21
  { cmd: '/settings', description: 'Choose project, runtime, terminal, and model policy' },
20
22
  { cmd: '/pwd', description: 'Print working directory', lane: 'terminal' },