@inneranimalmedia/agentsam-sdk 2.6.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 (124) hide show
  1. package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
  2. package/docs/RELEASES.md +7 -7
  3. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  4. package/docs/TEST_TIERS.md +26 -0
  5. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  6. package/package.json +28 -7
  7. package/packages/agentsam-repository/README.md +15 -0
  8. package/packages/agentsam-repository/package.json +25 -0
  9. package/packages/agentsam-repository/src/contracts.js +113 -0
  10. package/packages/agentsam-repository/src/index.js +3 -0
  11. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  12. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  13. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  14. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  15. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  16. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  17. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  18. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  19. package/packages/identity/package.json +1 -1
  20. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  21. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  22. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  23. package/protocol/repository/repository-contract.schema.json +24 -0
  24. package/protocol/repository/repository-dependency.schema.json +24 -0
  25. package/protocol/repository/repository-identity.schema.json +17 -0
  26. package/protocol/rpc/v1/common.proto +16 -0
  27. package/protocol/rpc/v1/errors.proto +35 -0
  28. package/protocol/rpc/v1/knowledge.proto +77 -0
  29. package/services/knowledge/package-lock.json +333 -0
  30. package/services/knowledge/package.json +5 -1
  31. package/src/agent/responses-runner.js +63 -35
  32. package/src/capabilities/repository-snapshot.js +3 -3
  33. package/src/cli.js +23 -6
  34. package/src/commands/context-economics.js +17 -2
  35. package/src/commands/context.js +1 -1
  36. package/src/commands/db.js +20 -3
  37. package/src/commands/env.js +90 -0
  38. package/src/commands/knowledge.js +12 -4
  39. package/src/commands/merkle-persist.js +30 -11
  40. package/src/commands/merkle.js +1 -1
  41. package/src/commands/models.js +123 -65
  42. package/src/commands/ollama.js +26 -0
  43. package/src/commands/preferences.js +53 -26
  44. package/src/commands/shell.js +236 -48
  45. package/src/errors/contract.js +236 -0
  46. package/src/errors/index.js +14 -0
  47. package/src/index.js +13 -1
  48. package/src/knowledge/service/auth.js +13 -0
  49. package/src/knowledge/service/grpc-client.js +115 -0
  50. package/src/knowledge/service/grpc-codec.js +237 -0
  51. package/src/knowledge/service/grpc-server.js +83 -0
  52. package/src/knowledge/service/job-engine.js +248 -0
  53. package/src/knowledge/service/server.js +87 -135
  54. package/src/knowledge/source.js +1 -1
  55. package/src/lib/cli-preferences.js +31 -4
  56. package/src/lib/deploy-receipt/index.js +2 -2
  57. package/src/lib/knowledge-docker.js +6 -3
  58. package/src/lib/local-sessions.js +23 -2
  59. package/src/lib/local-status.js +1 -1
  60. package/src/lib/project-config.js +1 -1
  61. package/src/lib/provider-credentials.js +105 -5
  62. package/src/lib/slash-commands.js +4 -3
  63. package/src/local/migrations.js +93 -0
  64. package/src/local/runtime-store.js +141 -0
  65. package/src/local/sqlite.js +2 -0
  66. package/src/local-pty/server.js +113 -51
  67. package/src/models/discovery.js +292 -0
  68. package/src/providers/anthropic-messages.js +192 -0
  69. package/src/providers/cloudflare-chat.js +183 -0
  70. package/src/providers/factory.js +69 -0
  71. package/src/providers/gemini-generate-content.js +208 -0
  72. package/src/providers/index.js +5 -0
  73. package/src/providers/ollama-chat.js +148 -0
  74. package/src/providers/openai-responses.js +226 -75
  75. package/src/repository/index.js +14 -2
  76. package/src/rpc/generated/common_grpc_pb.js +1 -0
  77. package/src/rpc/generated/common_pb.js +536 -0
  78. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  79. package/src/rpc/generated/errors_pb.js +482 -0
  80. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  81. package/src/rpc/generated/knowledge_pb.js +2168 -0
  82. package/src/rpc/generated/package.json +3 -0
  83. package/src/security/trust-boundary.js +2 -2
  84. package/src/telemetry/events.js +4 -1
  85. package/src/ui/cli/activity.js +76 -0
  86. package/src/ui/cli/compaction.js +15 -0
  87. package/src/ui/cli/footer.js +39 -0
  88. package/src/ui/cli/help.js +192 -0
  89. package/src/ui/cli/plan.js +20 -0
  90. package/src/ui/cli/runtime-events.js +110 -0
  91. package/src/ui/cli/waiting.js +16 -0
  92. package/src/ui/merkle/render.js +1 -1
  93. package/test/cli/preferences-runtime.test.mjs +11 -0
  94. package/test/cli/runtime-ui.test.mjs +74 -0
  95. package/test/error-diagnostics.test.mjs +57 -1
  96. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  97. package/test/integration/cli-help.test.mjs +37 -0
  98. package/test/integration/knowledge-rpc.test.mjs +112 -0
  99. package/test/integration/merkle-cli.test.mjs +61 -0
  100. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  101. package/test/integration/provider-env-cli.test.mjs +49 -0
  102. package/test/integration/provider-factory.test.mjs +197 -0
  103. package/test/integration/repository-company-graph.test.mjs +90 -0
  104. package/test/integration/runtime-migrations.test.mjs +82 -0
  105. package/test/knowledge-service.test.mjs +5 -0
  106. package/test/knowledge.test.mjs +16 -0
  107. package/test/live/terminal-transport.live.test.mjs +24 -0
  108. package/test/local-sessions.test.mjs +7 -1
  109. package/test/models.test.mjs +101 -4
  110. package/test/ollama.test.mjs +21 -0
  111. package/test/portable-context.test.mjs +1 -1
  112. package/test/provider-credentials.test.mjs +45 -1
  113. package/test/release-hygiene.test.mjs +13 -5
  114. package/test/responses-runner.test.mjs +3 -1
  115. package/test/shell.test.mjs +50 -8
  116. package/test/terminal/local-pty.mock.test.mjs +151 -0
  117. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  118. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  119. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  120. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  121. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  122. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  123. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  124. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import { getProjectName, tryReadProjectConfig } from './project-config.js';
5
5
 
6
- export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v2';
7
- export const LEGACY_CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v1';
6
+ export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v3';
7
+ export const LEGACY_CLI_PREFERENCES_SCHEMAS = new Set(['agentsam-cli-preferences-v1', 'agentsam-cli-preferences-v2']);
8
8
 
9
9
  function readJson(filename) {
10
10
  try { return JSON.parse(fs.readFileSync(filename, 'utf8')); }
@@ -43,16 +43,43 @@ export function detectCliProject(startDir = process.cwd()) {
43
43
 
44
44
  export function cliPreferencesPath(root) { return path.join(path.resolve(root), '.agentsam', 'cli.json'); }
45
45
 
46
+ function safeModelSnapshot(value) {
47
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
48
+ const provider = String(value.provider || '').trim();
49
+ const providerModelId = String(value.provider_model_id || '').trim();
50
+ if (!provider || !providerModelId) return null;
51
+ return {
52
+ model_key: String(value.model_key || `${provider}:${providerModelId}`),
53
+ provider,
54
+ provider_model_id: providerModelId,
55
+ label: String(value.label || providerModelId),
56
+ availability: value.availability === 'available' ? 'available' : 'unverified',
57
+ availability_source: String(value.availability_source || ''),
58
+ context_window: Number.isFinite(Number(value.context_window)) && Number(value.context_window) > 0 ? Number(value.context_window) : null,
59
+ context_window_source: String(value.context_window_source || 'unknown'),
60
+ max_output_tokens: Number.isFinite(Number(value.max_output_tokens)) && Number(value.max_output_tokens) > 0 ? Number(value.max_output_tokens) : null,
61
+ max_output_tokens_source: String(value.max_output_tokens_source || 'unknown'),
62
+ reasoning_efforts: Array.isArray(value.reasoning_efforts) && value.reasoning_efforts.length ? value.reasoning_efforts.map(String) : ['auto'],
63
+ service_tiers: Array.isArray(value.service_tiers) && value.service_tiers.length ? value.service_tiers.map(String) : ['default'],
64
+ capabilities: value.capabilities && typeof value.capabilities === 'object' ? { ...value.capabilities } : {},
65
+ pricing: value.pricing && typeof value.pricing === 'object' ? { ...value.pricing } : null,
66
+ context_policy: value.context_policy && typeof value.context_policy === 'object' ? { ...value.context_policy } : null,
67
+ source: value.source && typeof value.source === 'object' ? { ...value.source } : null,
68
+ };
69
+ }
70
+
46
71
  function normalizePreferences(value = {}) {
72
+ const modelSnapshot = safeModelSnapshot(value.modelSnapshot);
47
73
  return {
48
74
  schemaVersion: CLI_PREFERENCES_SCHEMA,
49
75
  trustedDirectory: value.trustedDirectory === true,
50
76
  runtime: value.runtime || 'local',
51
77
  terminal: value.terminal || '',
52
78
  modelPreference: value.modelPreference || 'auto',
79
+ modelSnapshot,
53
80
  reasoningEffort: value.reasoningEffort || 'auto',
54
81
  serviceTier: value.serviceTier || 'default',
55
- modelAuthority: 'preference-only',
82
+ modelAuthority: modelSnapshot?.availability === 'available' ? 'provider-verified' : 'preference-only',
56
83
  updatedAt: value.updatedAt || null,
57
84
  };
58
85
  }
@@ -60,7 +87,7 @@ function normalizePreferences(value = {}) {
60
87
  export function readCliPreferences(root) {
61
88
  const value = readJson(cliPreferencesPath(root));
62
89
  if (!value) return null;
63
- if (value.schemaVersion !== CLI_PREFERENCES_SCHEMA && value.schemaVersion !== LEGACY_CLI_PREFERENCES_SCHEMA) return null;
90
+ if (value.schemaVersion !== CLI_PREFERENCES_SCHEMA && !LEGACY_CLI_PREFERENCES_SCHEMAS.has(value.schemaVersion)) return null;
64
91
  return normalizePreferences(value);
65
92
  }
66
93
 
@@ -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',
@@ -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;
@@ -7,10 +7,16 @@ const PROVIDER_CREDENTIALS = Object.freeze({
7
7
  gemini: Object.freeze({ env: 'GEMINI_API_KEY', files: ['gemini.env'] }),
8
8
  anthropic: Object.freeze({ env: 'ANTHROPIC_API_KEY', files: ['anthropic.env'] }),
9
9
  grok: Object.freeze({ env: 'XAI_API_KEY', files: ['grok.env', 'xai.env'] }),
10
- cloudflare: Object.freeze({ env: 'CLOUDFLARE_API_TOKEN', files: ['cloudflare.env'] }),
10
+ cloudflare: Object.freeze({ env: 'CLOUDFLARE_API_TOKEN', files: ['cloudflare.env'], accountEnv: ['ACCOUNT_ID', 'CLOUDFLARE_ACCOUNT_ID'] }),
11
11
  });
12
12
 
13
13
  function clean(value) { return value == null ? '' : String(value).trim(); }
14
+ function normalizeCloudflareAccountId(value) {
15
+ const id = clean(value);
16
+ if (!id) return '';
17
+ if (!/^[a-f0-9]{32}$/i.test(id)) throw new Error('invalid_cloudflare_account_id');
18
+ return id;
19
+ }
14
20
 
15
21
  function homeDirectory(options = {}) {
16
22
  return path.resolve(clean(options.home) || clean(options.env?.HOME) || clean(options.env?.USERPROFILE) || os.homedir());
@@ -36,6 +42,96 @@ function secureFile(filename) {
36
42
  return { ok: true, mode: stat.mode & 0o777 };
37
43
  }
38
44
 
45
+ function firstEnvValue(source, names = []) {
46
+ for (const name of names) {
47
+ const value = clean(parseEnvValue(source, name));
48
+ if (value) return value;
49
+ }
50
+ return '';
51
+ }
52
+
53
+ function firstRuntimeValue(env, names = []) {
54
+ for (const name of names) {
55
+ const value = clean(env?.[name]);
56
+ if (value) return value;
57
+ }
58
+ return '';
59
+ }
60
+
61
+ export function agentEnvDirectory(options = {}) {
62
+ return path.join(homeDirectory(options), '.agentsam', 'env.d');
63
+ }
64
+
65
+ export function agentEnvLoaderPath(options = {}) {
66
+ return path.join(homeDirectory(options), '.agentsam', 'load-agent-env.sh');
67
+ }
68
+
69
+ export function ensureAgentEnvLoader(options = {}) {
70
+ const filename = agentEnvLoaderPath(options);
71
+ fs.mkdirSync(path.dirname(filename), { recursive: true, mode: 0o700 });
72
+ const source = `# AgentSam provider environment loader. Source this file; do not execute it.
73
+ _agentsam_profile=\"\${1:-}\"
74
+ case \"\$_agentsam_profile\" in
75
+ openai|anthropic|gemini|grok|cloudflare) ;;
76
+ *) echo \"usage: source ~/.agentsam/load-agent-env.sh <openai|anthropic|gemini|grok|cloudflare>\" >&2; return 2 2>/dev/null || exit 2 ;;
77
+ esac
78
+ _agentsam_file=\"\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env\"
79
+ if [ ! -f \"\$_agentsam_file\" ]; then
80
+ echo \"AgentSam provider profile not found: \$_agentsam_file\" >&2
81
+ return 1 2>/dev/null || exit 1
82
+ fi
83
+ set -a
84
+ . \"\$_agentsam_file\"
85
+ set +a
86
+ if [ \"\$_agentsam_profile\" = cloudflare ]; then
87
+ if [ -z \"\${ACCOUNT_ID:-}\" ] && [ -n \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ]; then export ACCOUNT_ID=\"\$CLOUDFLARE_ACCOUNT_ID\"; fi
88
+ if [ -z \"\${CLOUDFLARE_ACCOUNT_ID:-}\" ] && [ -n \"\${ACCOUNT_ID:-}\" ]; then export CLOUDFLARE_ACCOUNT_ID=\"\$ACCOUNT_ID\"; fi
89
+ fi
90
+ unset _agentsam_file _agentsam_profile
91
+ `;
92
+ fs.writeFileSync(filename, source, { mode: 0o700 });
93
+ if (process.platform !== 'win32') fs.chmodSync(filename, 0o700);
94
+ return filename;
95
+ }
96
+
97
+ function profileTemplate(provider, options = {}) {
98
+ if (provider === 'cloudflare') {
99
+ const accountId = normalizeCloudflareAccountId(options.accountId);
100
+ return `# AgentSam Cloudflare profile\n# ACCOUNT_ID is your Cloudflare account identifier; it is not a secret.\nexport ACCOUNT_ID=\"${accountId}\"\nexport CLOUDFLARE_API_TOKEN=\"\"\n`;
101
+ }
102
+ const spec = PROVIDER_CREDENTIALS[provider];
103
+ if (!spec) throw new Error(`unsupported_provider:${provider}`);
104
+ return `# AgentSam ${provider} provider profile\nexport ${spec.env}=\"\"\n`;
105
+ }
106
+
107
+ export function ensureProviderEnvProfile(provider, options = {}) {
108
+ const spec = providerCredentialSpec(provider);
109
+ if (!spec) throw new Error(`unsupported_provider:${clean(provider).toLowerCase()}`);
110
+ const dir = agentEnvDirectory(options);
111
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
112
+ if (process.platform !== 'win32') fs.chmodSync(dir, 0o700);
113
+ const filename = path.join(dir, spec.files[0]);
114
+ let created = false;
115
+ if (!fs.existsSync(filename)) {
116
+ fs.writeFileSync(filename, profileTemplate(spec.provider, options), { mode: 0o600 });
117
+ created = true;
118
+ } else if (spec.provider === 'cloudflare' && clean(options.accountId)) {
119
+ const source = fs.readFileSync(filename, 'utf8');
120
+ const current = firstEnvValue(source, spec.accountEnv || []);
121
+ if (!current) {
122
+ const accountId = normalizeCloudflareAccountId(options.accountId);
123
+ const line = `export ACCOUNT_ID=\"${accountId}\"`;
124
+ const next = /^(?:export\s+)?ACCOUNT_ID=.*$/m.test(source)
125
+ ? source.replace(/^(?:export\s+)?ACCOUNT_ID=.*$/m, line)
126
+ : `${line}\n${source}`;
127
+ fs.writeFileSync(filename, next, { mode: 0o600 });
128
+ }
129
+ }
130
+ if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
131
+ const loader = ensureAgentEnvLoader(options);
132
+ return Object.freeze({ provider: spec.provider, file: filename, loader, created, source_command: `source ~/.agentsam/load-agent-env.sh ${spec.provider}` });
133
+ }
134
+
39
135
  export function providerCredentialSpec(provider) {
40
136
  const id = clean(provider).toLowerCase();
41
137
  const spec = PROVIDER_CREDENTIALS[id];
@@ -46,18 +142,21 @@ export function resolveProviderCredential(provider, options = {}) {
46
142
  const spec = providerCredentialSpec(provider);
47
143
  if (!spec) return Object.freeze({ provider: clean(provider).toLowerCase(), configured: false, source: null, error: 'unsupported_provider', value: '' });
48
144
  const env = options.env || process.env;
145
+ const accountIdFromEnv = firstRuntimeValue(env, spec.accountEnv || []);
49
146
  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 });
147
+ if (fromEnv) return Object.freeze({ provider: spec.provider, configured: true, source: 'environment', env: spec.env, file: null, error: null, value: fromEnv, account_id: accountIdFromEnv || null });
51
148
 
52
- const dir = path.join(homeDirectory({ ...options, env }), '.agentsam', 'env.d');
149
+ const dir = agentEnvDirectory({ ...options, env });
53
150
  for (const basename of spec.files) {
54
151
  const filename = path.join(dir, basename);
55
152
  if (!fs.existsSync(filename)) continue;
56
153
  try {
57
154
  const safety = secureFile(filename);
58
155
  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 });
156
+ const source = fs.readFileSync(filename, 'utf8');
157
+ const value = clean(parseEnvValue(source, spec.env));
158
+ const accountId = accountIdFromEnv || firstEnvValue(source, spec.accountEnv || []);
159
+ if (value) return Object.freeze({ provider: spec.provider, configured: true, source: 'agentsam_env_file', env: spec.env, file: filename, error: null, value, account_id: accountId || null });
61
160
  return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: 'credential_variable_missing', value: '' });
62
161
  } catch (error) {
63
162
  return Object.freeze({ provider: spec.provider, configured: false, source: 'agentsam_env_file', env: spec.env, file: filename, error: error?.message || String(error), value: '' });
@@ -75,6 +174,7 @@ export function describeProviderCredential(provider, options = {}) {
75
174
  env: resolved.env || null,
76
175
  file: resolved.file || null,
77
176
  error: resolved.error || null,
177
+ account_id: resolved.account_id || null,
78
178
  });
79
179
  }
80
180
 
@@ -11,10 +11,11 @@ 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: '/login', description: 'Sign in to Inner Animal Media and save the machine-local Agent Sam session', lane: 'identity' },
15
+ { cmd: '/logout', description: 'Sign out locally without deleting provider credentials', lane: 'identity' },
16
+ { cmd: '/whoami', description: 'Show authenticated account identity and safe credential status', lane: 'identity' },
17
17
  { cmd: '/session', description: 'Show current session usage, cost, and resume receipt', lane: 'observability' },
18
+ { cmd: '/usage', description: 'Show token usage, spend breakdown, and resume command', lane: 'observability' },
18
19
  { cmd: '/cf', description: 'Cloudflare native reads, Wrangler status, and CPU profile analysis', lane: 'cloudflare' },
19
20
  { cmd: '/settings', description: 'Choose project, runtime, terminal, and model policy' },
20
21
  { cmd: '/pwd', description: 'Print working directory', lane: 'terminal' },
@@ -0,0 +1,93 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const DEFAULT_MIGRATIONS_DIR = path.resolve(
6
+ path.dirname(fileURLToPath(import.meta.url)),
7
+ '..',
8
+ '..',
9
+ 'migrations',
10
+ 'runtime',
11
+ );
12
+
13
+ function migrationFiles(dir) {
14
+ if (!fs.existsSync(dir)) return [];
15
+ return fs.readdirSync(dir, { withFileTypes: true })
16
+ .filter((entry) => entry.isFile() && /^\d+.*\.sql$/i.test(entry.name))
17
+ .map((entry) => entry.name)
18
+ .sort((a, b) => a.localeCompare(b));
19
+ }
20
+
21
+ function ensureLedger(db) {
22
+ db.exec(`
23
+ CREATE TABLE IF NOT EXISTS agentsam_schema_migrations (
24
+ id TEXT PRIMARY KEY,
25
+ applied_at_unix INTEGER NOT NULL DEFAULT (unixepoch()),
26
+ checksum TEXT
27
+ );
28
+ `);
29
+ }
30
+
31
+ function checksumSource(source) {
32
+ let hash = 2166136261;
33
+ for (let i = 0; i < source.length; i += 1) {
34
+ hash ^= source.charCodeAt(i);
35
+ hash = Math.imul(hash, 16777619);
36
+ }
37
+ return 'fnv1a32:' + (hash >>> 0).toString(16).padStart(8, '0');
38
+ }
39
+
40
+ export async function listAppliedMigrations(db) {
41
+ ensureLedger(db);
42
+ const result = await db.prepare(
43
+ 'SELECT id, applied_at_unix, checksum FROM agentsam_schema_migrations ORDER BY id'
44
+ ).all();
45
+ return result.results || [];
46
+ }
47
+
48
+ export async function applyRuntimeMigrations(db, options = {}) {
49
+ const dir = path.resolve(options.migrationsDir || DEFAULT_MIGRATIONS_DIR);
50
+ ensureLedger(db);
51
+ const appliedRows = await listAppliedMigrations(db);
52
+ const applied = new Map(appliedRows.map((row) => [String(row.id), row]));
53
+ const results = [];
54
+
55
+ for (const filename of migrationFiles(dir)) {
56
+ const id = filename.replace(/\.sql$/i, '');
57
+ const source = fs.readFileSync(path.join(dir, filename), 'utf8');
58
+ const checksum = checksumSource(source);
59
+ const previous = applied.get(id);
60
+
61
+ if (previous) {
62
+ if (previous.checksum && previous.checksum !== checksum) {
63
+ throw new Error('migration_checksum_mismatch:' + id);
64
+ }
65
+ results.push({ id, filename, status: 'already_applied', checksum });
66
+ continue;
67
+ }
68
+
69
+ db.exec('BEGIN IMMEDIATE');
70
+ try {
71
+ db.exec(source);
72
+ await db.prepare(
73
+ 'INSERT INTO agentsam_schema_migrations (id, checksum) VALUES (?, ?)'
74
+ ).bind(id, checksum).run();
75
+ db.exec('COMMIT');
76
+ results.push({ id, filename, status: 'applied', checksum });
77
+ } catch (error) {
78
+ try { db.exec('ROLLBACK'); } catch {}
79
+ throw new Error('migration_failed:' + id + ':' + (error?.message || error));
80
+ }
81
+ }
82
+
83
+ return Object.freeze({
84
+ migrationsDir: dir,
85
+ total: results.length,
86
+ applied: results.filter((row) => row.status === 'applied').length,
87
+ results: Object.freeze(results),
88
+ });
89
+ }
90
+
91
+ export function runtimeMigrationsDirectory() {
92
+ return DEFAULT_MIGRATIONS_DIR;
93
+ }
@@ -0,0 +1,141 @@
1
+ import path from 'node:path';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { createLocalSqliteDatabase } from './sqlite.js';
4
+ import { applyRuntimeMigrations } from './migrations.js';
5
+
6
+ function clean(value) { return value == null ? '' : String(value).trim(); }
7
+ function hash(value) { return createHash('sha256').update(String(value || '')).digest('hex'); }
8
+
9
+ export function runtimeDatabasePath(cwd = process.cwd()) {
10
+ return path.join(path.resolve(cwd), '.agentsam', 'data', 'agentsam.sqlite');
11
+ }
12
+
13
+ async function withStore(cwd, fn) {
14
+ const db = await createLocalSqliteDatabase(runtimeDatabasePath(cwd));
15
+ try {
16
+ await applyRuntimeMigrations(db);
17
+ return await fn(db);
18
+ } finally {
19
+ db.close();
20
+ }
21
+ }
22
+
23
+ export function createRuntimeRunId() {
24
+ return `arun_${randomUUID()}`;
25
+ }
26
+
27
+ export async function startRuntimeRun(value = {}) {
28
+ const id = clean(value.id) || createRuntimeRunId();
29
+ await withStore(value.cwd, async (db) => {
30
+ await db.prepare(`
31
+ INSERT INTO agentsam_agent_run (
32
+ id, account_id, source_client, surface, mode, model_key,
33
+ reasoning_effort, requested_service_tier, status, started_at_unix, updated_at_unix
34
+ ) VALUES (?, ?, 'agentsam-cli', 'cli', ?, ?, ?, ?, 'running', unixepoch(), unixepoch())
35
+ `).bind(
36
+ id,
37
+ clean(value.account_id) || null,
38
+ clean(value.mode) || 'agent',
39
+ clean(value.model_key) || null,
40
+ clean(value.reasoning_effort) || null,
41
+ clean(value.service_tier) || null,
42
+ ).run();
43
+ });
44
+ return id;
45
+ }
46
+
47
+ export async function finishRuntimeRun(value = {}) {
48
+ if (!clean(value.id)) return;
49
+ await withStore(value.cwd, async (db) => {
50
+ const usage = value.usage || {};
51
+ await db.prepare(`
52
+ UPDATE agentsam_agent_run
53
+ SET status = ?,
54
+ actual_service_tier = COALESCE(?, actual_service_tier),
55
+ input_tokens = ?,
56
+ cached_input_tokens = ?,
57
+ output_tokens = ?,
58
+ reasoning_tokens = ?,
59
+ cost_usd = ?,
60
+ error_code = ?,
61
+ error_message = ?,
62
+ completed_at_unix = unixepoch(),
63
+ updated_at_unix = unixepoch(),
64
+ latency_ms = ?
65
+ WHERE id = ?
66
+ `).bind(
67
+ clean(value.status) || 'completed',
68
+ clean(value.actual_service_tier) || null,
69
+ Number(usage.input_tokens || 0),
70
+ Number(usage.cached_input_tokens || 0),
71
+ Number(usage.output_tokens || 0),
72
+ Number(usage.reasoning_tokens || 0),
73
+ Math.max(0, Number(value.cost_usd || 0)),
74
+ clean(value.error_code) || null,
75
+ clean(value.error_message) || null,
76
+ Number.isFinite(Number(value.latency_ms)) ? Math.max(0, Math.round(Number(value.latency_ms))) : null,
77
+ value.id,
78
+ ).run();
79
+ });
80
+ }
81
+
82
+ export async function recordRuntimeCompaction(value = {}) {
83
+ if (!clean(value.agent_run_id)) return null;
84
+ const id = clean(value.id) || `cmp_${randomUUID()}`;
85
+ const summary = clean(value.summary_text);
86
+ const before = Math.max(0, Math.round(Number(value.tokens_before || 0)));
87
+ const after = Math.max(0, Math.round(Number(value.tokens_after || 0)));
88
+ const sourceHash = hash(summary || JSON.stringify(value.metadata || {}));
89
+
90
+ await withStore(value.cwd, async (db) => {
91
+ await db.prepare(`
92
+ INSERT INTO agentsam_compaction_events (
93
+ id, account_id, agent_run_id, compaction_type, compaction_scope,
94
+ compaction_strategy, source_kind, content_hash, provider, model_key,
95
+ tokens_before, tokens_after, status, summary_text, summary_json,
96
+ metrics_json, metadata_json, source_stored
97
+ ) VALUES (?, ?, ?, 'context_summary', 'agent_run', 'summarize', ?, ?, ?, ?, ?, ?, 'completed', ?, ?, ?, ?, ?)
98
+ `).bind(
99
+ id,
100
+ clean(value.account_id) || null,
101
+ value.agent_run_id,
102
+ clean(value.source_kind) || 'api',
103
+ sourceHash,
104
+ clean(value.provider),
105
+ clean(value.model_key),
106
+ before,
107
+ after,
108
+ summary,
109
+ JSON.stringify({ summary }),
110
+ JSON.stringify({ tokens_before: before, tokens_after: after, tokens_saved: before - after }),
111
+ JSON.stringify(value.metadata || {}),
112
+ clean(value.source_stored) || 'local:agentsam_compaction_events',
113
+ ).run();
114
+
115
+ if (summary) {
116
+ const digestId = `ctx_${randomUUID()}`;
117
+ const digestHash = hash(`${value.agent_run_id}:${id}:${summary}`);
118
+ await db.prepare(`
119
+ INSERT INTO agentsam_context_digest (
120
+ id, account_id, digest_type, agent_run_id, session_id,
121
+ source_hash, digest_hash, raw_size_bytes, reduced_size_bytes,
122
+ token_count, digest_text, generation_model, compaction_event_id
123
+ ) VALUES (?, ?, 'session', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
124
+ `).bind(
125
+ digestId,
126
+ clean(value.account_id) || null,
127
+ value.agent_run_id,
128
+ clean(value.session_id) || null,
129
+ sourceHash,
130
+ digestHash,
131
+ Number(value.raw_size_bytes || 0) || null,
132
+ Buffer.byteLength(summary, 'utf8'),
133
+ after || Math.ceil(summary.length / 4),
134
+ summary,
135
+ clean(value.model_key) || null,
136
+ id,
137
+ ).run();
138
+ }
139
+ });
140
+ return id;
141
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { applyRuntimeMigrations } from './migrations.js';
3
4
 
4
5
  async function loadSqlite() {
5
6
  try {
@@ -77,6 +78,7 @@ export async function initializeLocalSqlite({
77
78
  const db = await createLocalSqliteDatabase(resolvedDb);
78
79
  try {
79
80
  db.exec(schema);
81
+ await applyRuntimeMigrations(db);
80
82
  } finally {
81
83
  db.close();
82
84
  }