@inneranimalmedia/agentsam-sdk 2.4.1 → 2.5.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 (158) hide show
  1. package/README.md +7 -6
  2. package/docs/AGENTSAM_WORKBENCH.md +30 -0
  3. package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
  4. package/docs/CAPABILITIES.md +5 -3
  5. package/docs/CLI_SHELL.md +57 -80
  6. package/docs/CMS_STUDIO.md +101 -0
  7. package/docs/CONTEXT.md +170 -0
  8. package/docs/LOCAL_OLLAMA.md +58 -0
  9. package/docs/MERKLE.md +19 -19
  10. package/docs/PORTABLE_CONTEXT.md +4 -3
  11. package/docs/PROJECT_CONFIG.md +72 -0
  12. package/docs/RELEASES.md +26 -2
  13. package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
  14. package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
  15. package/docs/SDK_WORKER.md +86 -0
  16. package/docs/SECURITY.md +59 -22
  17. package/docs/client-cms-editor.md +15 -0
  18. package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
  19. package/package.json +17 -4
  20. package/packages/identity/package.json +11 -2
  21. package/packages/identity/src/contracts/auth-config.js +98 -0
  22. package/packages/identity/src/index.js +1 -0
  23. package/packages/identity/src/oauth/README.md +2 -2
  24. package/packages/identity/src/oauth/credentials.js +11 -4
  25. package/packages/identity/src/oauth/iam-platform.js +3 -3
  26. package/packages/identity/src/providers/iam/index.js +7 -7
  27. package/packages/identity/src/providers/iam/oauth.js +6 -4
  28. package/packages/identity/src/providers/iam/profile.js +5 -5
  29. package/packages/identity/tests/auth-config.test.mjs +57 -0
  30. package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
  31. package/protocol/FILEMETA_V1.md +95 -0
  32. package/protocol/INSPECT_VIEWS_V1.md +40 -0
  33. package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
  34. package/protocol/MERKLE_V1.md +3 -1
  35. package/protocol/capabilities/manifest.json +32 -3
  36. package/protocol/capabilities/repository-snapshot.schema.json +1 -0
  37. package/protocol/context/context-budget.schema.json +41 -0
  38. package/protocol/context/context-item.schema.json +20 -0
  39. package/protocol/context/resolved-context-pack.schema.json +42 -0
  40. package/protocol/context/result-policy.schema.json +17 -0
  41. package/protocol/knowledge/context-pack.schema.json +33 -0
  42. package/protocol/knowledge/retrieval-query.schema.json +68 -13
  43. package/python/README.md +15 -7
  44. package/python/agentsam_sdk/cli.py +0 -21
  45. package/python/agentsam_sdk/tui/README.md +17 -12
  46. package/python/agentsam_sdk/tui/bootstrap.py +2 -2
  47. package/python/agentsam_sdk/tui/demo.py +25 -10
  48. package/python/agentsam_sdk/tui/onboarding.py +208 -0
  49. package/python/tests/test_tui_cli.py +7 -6
  50. package/skills/README.md +21 -0
  51. package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
  52. package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
  53. package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
  54. package/skills/agentsam-jr-dev/SKILL.md +232 -0
  55. package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
  56. package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
  57. package/skills/agentsam-progression-guard/SKILL.md +197 -0
  58. package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
  59. package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
  60. package/skills/catalog.json +53 -0
  61. package/src/capabilities/index.js +7 -0
  62. package/src/capabilities/repository-snapshot-view.js +238 -0
  63. package/src/capabilities/repository-snapshot.js +28 -14
  64. package/src/cli.js +108 -56
  65. package/src/commands/context.js +14 -2
  66. package/src/commands/db.js +4 -7
  67. package/src/commands/deploy.js +17 -31
  68. package/src/commands/interactive.js +21 -0
  69. package/src/commands/knowledge.js +27 -4
  70. package/src/commands/merkle-persist.js +118 -0
  71. package/src/commands/merkle.js +32 -17
  72. package/src/commands/models.js +107 -0
  73. package/src/commands/ollama.js +259 -0
  74. package/src/commands/preferences.js +102 -0
  75. package/src/commands/product.js +86 -16
  76. package/src/commands/security.js +3 -3
  77. package/src/commands/shell.js +71 -27
  78. package/src/commands/skills.js +66 -0
  79. package/src/commands/start-local.js +1 -1
  80. package/src/commands/tunnel.js +5 -4
  81. package/src/context/budget.js +58 -0
  82. package/src/context/compact.js +28 -0
  83. package/src/context/index.js +5 -0
  84. package/src/context/resolve.js +84 -0
  85. package/src/context/result-policy.js +66 -0
  86. package/src/index.js +16 -0
  87. package/src/indexing/execution-boundary.js +144 -0
  88. package/src/indexing/index.js +8 -0
  89. package/src/indexing/provider.js +41 -0
  90. package/src/knowledge/config.js +2 -2
  91. package/src/knowledge/context-pack.js +12 -1
  92. package/src/knowledge/contracts.js +9 -4
  93. package/src/knowledge/engine.js +7 -3
  94. package/src/knowledge/service/server.js +1 -1
  95. package/src/lib/bridge-client.js +7 -5
  96. package/src/lib/cli-preferences.js +74 -0
  97. package/src/lib/core-client.js +8 -8
  98. package/src/lib/deploy-receipt/index.js +5 -2
  99. package/src/lib/detect-context.js +6 -5
  100. package/src/lib/identity-scaffold.js +1 -1
  101. package/src/lib/local-scaffold.js +35 -33
  102. package/src/lib/local-status.js +9 -17
  103. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  104. package/src/lib/merkle/filemeta.js +43 -0
  105. package/src/lib/merkle/git-ignore.js +24 -0
  106. package/src/lib/merkle/hash.js +1 -0
  107. package/src/lib/merkle/index.js +22 -0
  108. package/src/lib/merkle/persistence.js +72 -0
  109. package/src/lib/merkle/semantic.js +359 -0
  110. package/src/lib/merkle/snapshot.js +9 -3
  111. package/src/lib/merkle/tree.js +11 -6
  112. package/src/lib/project-config.js +227 -0
  113. package/src/lib/project-rules.js +68 -0
  114. package/src/lib/save-sdk-token.js +1 -1
  115. package/src/lib/slash-commands.js +2 -1
  116. package/src/lib/tools.js +11 -5
  117. package/src/security/index.js +1 -0
  118. package/src/security/inventory.js +4 -1
  119. package/src/security/render.js +27 -5
  120. package/src/security/scan.js +24 -9
  121. package/src/security/trust-boundary.js +24 -0
  122. package/src/skills/index.js +64 -0
  123. package/src/tools/index.js +1 -0
  124. package/src/tools/search.js +70 -0
  125. package/src/ui/ansi.js +1 -1
  126. package/src/ui/boot.js +56 -0
  127. package/src/ui/merkle/render.js +1 -0
  128. package/src/ui/runtime-activity.js +192 -0
  129. package/src/ui/theme.js +19 -18
  130. package/test/app-building-skills.test.mjs +61 -0
  131. package/test/apps-scaffold-contract.test.mjs +56 -0
  132. package/test/capabilities.test.mjs +53 -4
  133. package/test/cli-preferences.test.mjs +25 -0
  134. package/test/context.test.mjs +95 -0
  135. package/test/indexing-provider.test.mjs +29 -0
  136. package/test/jr-dev-skill.test.mjs +26 -0
  137. package/test/knowledge-context-pack.test.mjs +19 -0
  138. package/test/knowledge.test.mjs +1 -1
  139. package/test/merkle-persistence.test.mjs +91 -0
  140. package/test/merkle.test.mjs +77 -3
  141. package/test/models.test.mjs +37 -0
  142. package/test/ollama.test.mjs +94 -0
  143. package/test/project-config.test.mjs +81 -0
  144. package/test/project-rules.test.mjs +44 -0
  145. package/test/release-hygiene.test.mjs +34 -0
  146. package/test/repository-snapshot-view.test.mjs +112 -0
  147. package/test/runtime-activity.test.mjs +98 -0
  148. package/test/sdk-worker-contract.test.mjs +68 -0
  149. package/test/security.test.mjs +46 -0
  150. package/test/shell.test.mjs +8 -1
  151. package/test/skills.test.mjs +22 -0
  152. package/test/smoke.mjs +2 -2
  153. package/test/theme-portability.test.mjs +14 -0
  154. package/test/tools-search.test.mjs +27 -0
  155. package/examples/agentsam-tui-ansi.mjs +0 -149
  156. package/src/commands/tui.js +0 -120
  157. package/src/ui/splash-xterm.js +0 -290
  158. package/src/ui/splash.js +0 -426
@@ -0,0 +1,118 @@
1
+ import path from 'node:path';
2
+ import { readSnapshot } from '../lib/merkle/snapshot.js';
3
+ import {
4
+ buildMerklePersistencePlan,
5
+ persistMerkleSnapshotCloudflare,
6
+ resolveWranglerMerklePersistence,
7
+ } from '../lib/merkle/cloudflare-persistence.js';
8
+
9
+ function value(args, index, flag) {
10
+ const next = args[index + 1];
11
+ if (!next || next.startsWith('--')) throw new Error(`Missing value for ${flag}`);
12
+ return next;
13
+ }
14
+
15
+ export function printMerklePersistHelp() {
16
+ console.log(`
17
+ agentsam merkle persist <snapshot.json> — publish a saved Merkle snapshot through host bindings
18
+
19
+ --wrangler-config <file> Worker config containing WEBSITE_ASSETS and optionally DB
20
+ --owner-user-id <id> Snapshot owner authority (or AGENTSAM_OWNER_USER_ID)
21
+ --repo-id <id> Canonical repo id; inferred for GitHub/GitLab/Bitbucket when possible
22
+ --capture-kind <kind> deploy|manual|agent|index (default manual)
23
+ --connection-id <id> Execution provenance for non-deploy captures
24
+ --runtime-lease-id <id> Alternative execution provenance for non-deploy captures
25
+ --deployment-id <id> Optional deployment linkage
26
+ --worker-version <id> Optional provider worker version linkage
27
+ --reference-label <label> Optional human/audit label
28
+ --source <source> github|gitlab|bitbucket|local|upload (default local)
29
+ --prefix <prefix> Object prefix (default agentsam_fs_merkle_snapshots)
30
+ --r2-binding <name> Logical R2 role (default WEBSITE_ASSETS)
31
+ --d1-binding <name> Logical D1 role (default DB)
32
+ --environment <name> Wrangler environment
33
+ --wrangler-bin <path> Wrangler executable (default WRANGLER_BIN or wrangler)
34
+ --r2-only Upload object without indexing in D1
35
+ --dry-run Resolve bindings and emit the exact storage/index plan without writes
36
+ --json Machine-readable output
37
+
38
+ The SDK never infers owner identity. Hosts should pass authenticated owner_user_id.
39
+ Physical bucket/database names come from Wrangler bindings, so customer installs keep
40
+ WEBSITE_ASSETS/DB while selecting their own storage resources.
41
+ `);
42
+ }
43
+
44
+ function parse(args) {
45
+ if (!args.length || args.includes('--help') || args.includes('-h')) return { help: true };
46
+ const opts = {
47
+ snapshotPath: '', json: false, dryRun: false, r2Only: false,
48
+ captureKind: 'manual', source: 'local', r2Binding: 'WEBSITE_ASSETS', d1Binding: 'DB',
49
+ storagePrefix: 'agentsam_fs_merkle_snapshots',
50
+ };
51
+ for (let i = 0; i < args.length; i += 1) {
52
+ const arg = args[i];
53
+ if (!arg.startsWith('-') && !opts.snapshotPath) { opts.snapshotPath = arg; continue; }
54
+ if (arg === '--json') { opts.json = true; continue; }
55
+ if (arg === '--dry-run') { opts.dryRun = true; continue; }
56
+ if (arg === '--r2-only') { opts.r2Only = true; continue; }
57
+ const map = {
58
+ '--wrangler-config': 'wranglerConfig', '--owner-user-id': 'ownerUserId', '--repo-id': 'repoId',
59
+ '--capture-kind': 'captureKind', '--connection-id': 'connectionId', '--runtime-lease-id': 'runtimeLeaseId',
60
+ '--deployment-id': 'deploymentId', '--worker-version': 'workerVersionId', '--reference-label': 'referenceLabel',
61
+ '--source': 'source', '--prefix': 'storagePrefix', '--r2-binding': 'r2Binding', '--d1-binding': 'd1Binding',
62
+ '--environment': 'environment', '--wrangler-bin': 'wranglerBin', '--root': 'root',
63
+ };
64
+ if (map[arg]) { opts[map[arg]] = value(args, i, arg); i += 1; continue; }
65
+ throw new Error(`Unknown merkle persist option: ${arg}`);
66
+ }
67
+ if (!opts.snapshotPath) throw new Error('snapshot_file_required');
68
+ opts.ownerUserId ||= process.env.AGENTSAM_OWNER_USER_ID || '';
69
+ opts.wranglerConfig ||= process.env.AGENTSAM_WRANGLER_CONFIG || '';
70
+ opts.connectionId ||= process.env.AGENTSAM_CONNECTION_ID || '';
71
+ opts.runtimeLeaseId ||= process.env.AGENTSAM_RUNTIME_LEASE_ID || '';
72
+ return opts;
73
+ }
74
+
75
+ export async function runMerklePersist(args = []) {
76
+ const opts = parse(args);
77
+ if (opts.help) { printMerklePersistHelp(); return null; }
78
+ const snapshotPath = path.resolve(opts.snapshotPath);
79
+ const snapshot = await readSnapshot(snapshotPath);
80
+ const root = path.resolve(opts.root || snapshot.rootPath || process.cwd());
81
+ const wrangler = resolveWranglerMerklePersistence({
82
+ configPath: opts.wranglerConfig,
83
+ environment: opts.environment || null,
84
+ r2Binding: opts.r2Binding,
85
+ d1Binding: opts.d1Binding,
86
+ r2Only: opts.r2Only,
87
+ });
88
+ const plan = buildMerklePersistencePlan({
89
+ snapshot,
90
+ root,
91
+ ownerUserId: opts.ownerUserId,
92
+ repoId: opts.repoId,
93
+ source: opts.source,
94
+ captureKind: opts.captureKind,
95
+ connectionId: opts.connectionId,
96
+ runtimeLeaseId: opts.runtimeLeaseId,
97
+ deploymentId: opts.deploymentId,
98
+ workerVersionId: opts.workerVersionId,
99
+ referenceLabel: opts.referenceLabel,
100
+ storagePrefix: opts.storagePrefix,
101
+ wrangler,
102
+ });
103
+ const result = persistMerkleSnapshotCloudflare({
104
+ snapshotPath,
105
+ snapshot,
106
+ plan,
107
+ wranglerBin: opts.wranglerBin,
108
+ r2Only: opts.r2Only,
109
+ dryRun: opts.dryRun,
110
+ });
111
+ if (opts.json) process.stdout.write(`${JSON.stringify(result)}\n`);
112
+ else {
113
+ console.log(`Persisted ${result.snapshot_id}${result.dry_run ? ' (dry run)' : ''}`);
114
+ console.log(` R2 ${result.binding} -> ${result.storage_bucket}/${result.storage_key}`);
115
+ if (result.table) console.log(` D1 ${result.database_binding} -> ${result.database_name}.${result.table}`);
116
+ }
117
+ return result;
118
+ }
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { buildMerkleTree, saveSnapshot, readSnapshot, diffTrees, normalizePolicy } from '../lib/merkle/index.js';
4
4
  import { renderSummary, safeText } from '../ui/merkle/render.js';
5
5
  import { runMerkleExplorer } from '../ui/merkle/explorer.js';
6
+ import { runMerklePersist } from './merkle-persist.js';
6
7
 
7
8
  export function printMerkleHelp() {
8
9
  console.log(`
@@ -13,24 +14,26 @@ export function printMerkleHelp() {
13
14
  verify <snapshot> Check current files against a saved manifest
14
15
  diff <a> <b> Compare two directories or snapshot files
15
16
  inspect [path] Show every path and hash (also accepts a snapshot)
16
- tui [path] Open the interactive tree explorer
17
+ persist <snapshot> Publish a saved snapshot through WEBSITE_ASSETS/DB bindings
18
+ explore [path] Open the interactive tree explorer
17
19
 
18
20
  --out <file> Snapshot destination
19
21
  --force Replace an existing snapshot file
22
+ --semantic Add agentsam-filemeta classification + AST metadata
20
23
  --root <path> Verify a snapshot against a different directory
21
24
  --include <default-rule> Include an ignored category, e.g. --include dist
22
25
  --exclude <path-or-name> Ignore an additional name or relative subtree
23
- --tui Explore root/inspect/verify/diff interactively
24
- --json Machine-readable output; never opens a TUI
26
+ --interactive Explore root/inspect/verify/diff interactively
27
+ --json Machine-readable output; never opens the interactive explorer
25
28
 
26
29
  Examples:
27
30
  agentsam merkle root .
28
- agentsam merkle snapshot . --out .agentsam/merkle.json
29
- agentsam merkle verify .agentsam/merkle.json --tui
30
- agentsam merkle diff ./mac-copy ./vm-copy --tui
31
- agentsam tui merkle .
31
+ agentsam merkle snapshot . --semantic --out .agentsam/merkle.json
32
+ agentsam merkle verify .agentsam/merkle.json --interactive
33
+ agentsam merkle diff ./mac-copy ./vm-copy --interactive
34
+ agentsam merkle explore .
32
35
 
33
- TUI: arrows or j/k to move; Enter/right to expand; left to collapse;
36
+ Explorer: arrows or j/k to move; Enter/right to expand; left to collapse;
34
37
  c to filter changes; r to rescan; q/Esc/Ctrl+C to close.
35
38
  Exit codes: 0 success/match, 1 differences, 2 error, 130 interrupted scan.
36
39
  Includes/excludes are literal rules, not globs or .gitignore syntax.
@@ -39,14 +42,16 @@ export function printMerkleHelp() {
39
42
 
40
43
  function parse(argv) {
41
44
  const [command, ...args] = argv;
42
- if (!['root', 'snapshot', 'verify', 'diff', 'inspect', 'tui'].includes(command)) throw new Error(`Unknown Merkle command: ${command}`);
43
- const opts = { command, paths: [], include: [], exclude: [], tui: command === 'tui', json: false, force: false };
45
+ if (!['root', 'snapshot', 'verify', 'diff', 'inspect', 'explore', 'tui'].includes(command)) throw new Error(`Unknown Merkle command: ${command}`);
46
+ const publicCommand = command === 'tui' ? 'explore' : command;
47
+ const opts = { command: publicCommand, paths: [], include: [], exclude: [], tui: publicCommand === 'explore', json: false, force: false, semantic: false };
44
48
  let positionalOnly = false;
45
49
  for (let i = 0; i < args.length; i++) {
46
50
  const arg = args[i];
47
51
  if (arg === '--' && !positionalOnly) { positionalOnly = true; continue; }
48
52
  if (positionalOnly || !arg.startsWith('-')) opts.paths.push(arg);
49
- else if (['--tui', '--json', '--force'].includes(arg)) opts[arg.slice(2)] = true;
53
+ else if (arg === '--interactive' || arg === '--tui') opts.tui = true;
54
+ else if (['--json', '--force', '--semantic'].includes(arg)) opts[arg.slice(2)] = true;
50
55
  else if (['--out', '--root', '--include', '--exclude'].includes(arg)) {
51
56
  const value = args[++i];
52
57
  if (!value || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
@@ -59,7 +64,8 @@ function parse(argv) {
59
64
  if (!count && !opts.paths.length) opts.paths.push('.');
60
65
  if ((opts.out || opts.force) && command !== 'snapshot') throw new Error('--out and --force apply only to snapshot.');
61
66
  if (opts.root && command !== 'verify') throw new Error('--root applies only to verify.');
62
- if (command === 'snapshot' && opts.tui) throw new Error('Use inspect --tui after saving a snapshot.');
67
+ if (command === 'snapshot' && opts.tui) throw new Error('Use inspect --interactive after saving a snapshot.');
68
+ if (opts.semantic && !['root', 'snapshot', 'inspect'].includes(command)) throw new Error('--semantic applies only to root, snapshot, or inspect.');
63
69
  if (command === 'verify' && (opts.include.length || opts.exclude.length)) throw new Error('Verification uses the snapshot\'s saved ignore rules.');
64
70
  opts.policy = normalizePolicy(opts);
65
71
  return opts;
@@ -78,6 +84,14 @@ async function comparison(opts, progress) {
78
84
  }
79
85
 
80
86
  export async function runMerkle(argv = []) {
87
+ if (argv[0] === 'persist') {
88
+ try { return await runMerklePersist(argv.slice(1)); }
89
+ catch (error) {
90
+ process.exitCode = 2;
91
+ process.stderr.write(argv.includes('--json') ? JSON.stringify({ error: error.message }) + '\n' : `Merkle persist: ${safeText(error.message)}\n`);
92
+ return null;
93
+ }
94
+ }
81
95
  if (!argv.length || argv.includes('--help') || argv.includes('-h')) { printMerkleHelp(); return; }
82
96
  let opts;
83
97
  try {
@@ -91,12 +105,12 @@ export async function runMerkle(argv = []) {
91
105
  }
92
106
  if (opts.command === 'diff') return comparison(opts, progress);
93
107
  if (opts.command === 'snapshot') {
94
- const { snapshot: tree, output } = await saveSnapshot(target, { ...progress, policy: opts.policy, out: opts.out, force: opts.force });
108
+ const { snapshot: tree, output } = await saveSnapshot(target, { ...progress, policy: opts.policy, out: opts.out, force: opts.force, semantic: opts.semantic });
95
109
  return { title: 'Merkle snapshot', tree, output };
96
110
  }
97
- const isSnapshot = ['inspect', 'tui'].includes(opts.command) && !(await fs.stat(target)).isDirectory();
111
+ const isSnapshot = ['inspect', 'explore'].includes(opts.command) && !(await fs.stat(target)).isDirectory();
98
112
  if (isSnapshot && (opts.include.length || opts.exclude.length)) throw new Error('Snapshot inspection uses saved ignore rules.');
99
- const tree = isSnapshot ? await readSnapshot(target) : await buildMerkleTree(target, { ...progress, policy: opts.policy });
113
+ const tree = isSnapshot ? await readSnapshot(target) : await buildMerkleTree(target, { ...progress, policy: opts.policy, semantic: opts.semantic });
100
114
  return { title: isSnapshot ? 'Saved Merkle tree' : 'Merkle root', tree };
101
115
  };
102
116
  let result;
@@ -108,9 +122,10 @@ export async function runMerkle(argv = []) {
108
122
  try { result = await load({ signal: controller.signal }); }
109
123
  finally { process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); }
110
124
  if (opts.json) {
111
- const { rootPath, rootHash, stats, policy } = result.tree;
125
+ const { rootPath, rootHash, stats, policy, policyHash, semantic } = result.tree;
126
+ const semanticSummary = semantic ? { metadataRoot: semantic.rootHash, classifier: semantic.classifier, semanticStats: semantic.stats } : {};
112
127
  const value = result.diff ? { ...result.diff, rootPath, policy } : opts.command === 'snapshot' ? { ...result.tree, output: result.output }
113
- : opts.command === 'root' ? { rootPath, rootHash, stats, policy } : result.tree;
128
+ : opts.command === 'root' ? { rootPath, rootHash, stats, policy, policyHash, ...semanticSummary } : result.tree;
114
129
  process.stdout.write(JSON.stringify(value, null, 2) + '\n');
115
130
  } else process.stdout.write(renderSummary(result, { inspect: opts.command === 'inspect', color: process.stdout.isTTY && !Object.hasOwn(process.env, 'NO_COLOR') }));
116
131
  }
@@ -0,0 +1,107 @@
1
+ import pc from 'picocolors';
2
+ import { probeOllama, resolveOllamaConfig } from './ollama.js';
3
+
4
+ const API_PROVIDERS = Object.freeze([
5
+ { id: 'openai', label: 'OpenAI', credential: 'OPENAI_API_KEY' },
6
+ { id: 'gemini', label: 'Gemini', credential: 'GEMINI_API_KEY' },
7
+ { id: 'grok', label: 'Grok', credential: 'XAI_API_KEY' },
8
+ ]);
9
+
10
+ function clean(value) {
11
+ return value == null ? '' : String(value).trim();
12
+ }
13
+
14
+ function configured(value) {
15
+ return Boolean(clean(value));
16
+ }
17
+
18
+ export async function collectModelsStatus(options = {}) {
19
+ const env = options.env || process.env;
20
+ const fetchImpl = options.fetchImpl || fetch;
21
+ const ollamaConfig = resolveOllamaConfig({}, env);
22
+ const ollama = await probeOllama(ollamaConfig, fetchImpl);
23
+
24
+ return {
25
+ schemaVersion: 'agentsam-model-inventory-v1',
26
+ providers: API_PROVIDERS.map((provider) => ({
27
+ ...provider,
28
+ configured: configured(env[provider.credential]),
29
+ source: 'environment',
30
+ })),
31
+ local: {
32
+ provider: 'ollama',
33
+ configured: ollama.online,
34
+ online: ollama.online,
35
+ endpoint: ollama.endpoint,
36
+ chatModel: ollamaConfig.model,
37
+ embedModel: ollamaConfig.embedModel,
38
+ models: ollama.models || [],
39
+ error: ollama.error || null,
40
+ },
41
+ };
42
+ }
43
+
44
+ function statusMark(ok) {
45
+ return ok ? pc.green('●') : pc.dim('○');
46
+ }
47
+
48
+ function writeLine(write, value = '') {
49
+ write(`${value}\n`);
50
+ }
51
+
52
+ export function renderModelsStatus(status) {
53
+ const lines = [];
54
+ lines.push('');
55
+ lines.push(` ${pc.bold('Agent Sam · models')}`);
56
+ lines.push(` ${pc.dim('Detected from this terminal session. Secrets are never printed.')}`);
57
+ lines.push('');
58
+
59
+ for (const provider of status.providers) {
60
+ const state = provider.configured ? pc.green('configured') : pc.dim('not configured');
61
+ const detail = provider.configured ? 'credential available' : provider.credential;
62
+ lines.push(` ${statusMark(provider.configured)} ${pc.cyan(provider.label.padEnd(10))} ${state.padEnd(20)} ${pc.dim(detail)}`);
63
+ }
64
+
65
+ const local = status.local;
66
+ const localState = local.online ? pc.green('online') : pc.dim('offline');
67
+ lines.push(` ${statusMark(local.online)} ${pc.cyan('Ollama'.padEnd(10))} ${localState.padEnd(20)} ${pc.dim('local only')}`);
68
+
69
+ if (local.online) {
70
+ const names = local.models.map((row) => row.name).filter(Boolean);
71
+ lines.push('');
72
+ lines.push(` ${pc.dim('local models')}`);
73
+ if (names.length) {
74
+ for (const name of names) lines.push(` ${pc.green('•')} ${name}`);
75
+ } else {
76
+ lines.push(` ${pc.dim('no models reported')}`);
77
+ }
78
+ lines.push('');
79
+ lines.push(` ${pc.dim('chat default')} ${local.chatModel}`);
80
+ lines.push(` ${pc.dim('embed default')} ${local.embedModel}`);
81
+ }
82
+
83
+ lines.push('');
84
+ lines.push(` ${pc.dim('Provider model catalogs and model selection belong to the connected host/runtime;')}`);
85
+ lines.push(` ${pc.dim('this command only reports what this local CLI can prove is configured or available.')}`);
86
+ lines.push('');
87
+ return lines.join('\n');
88
+ }
89
+
90
+ export async function runModels(argv = [], options = {}) {
91
+ if (argv.some((arg) => arg === '--help' || arg === '-h')) {
92
+ const text = 'agentsam models [--json]\n\nShow model providers configured in this terminal session plus models reported by local Ollama.\n';
93
+ (options.write || process.stdout.write.bind(process.stdout))(text);
94
+ return;
95
+ }
96
+ const unknown = argv.filter((arg) => arg !== '--json');
97
+ if (unknown.length) throw new Error(`unknown models option: ${unknown[0]}`);
98
+
99
+ const status = await collectModelsStatus(options);
100
+ const write = options.write || ((text) => process.stdout.write(text));
101
+ if (argv.includes('--json')) {
102
+ writeLine(write, JSON.stringify(status, null, 2));
103
+ } else {
104
+ write(renderModelsStatus(status));
105
+ }
106
+ return status;
107
+ }
@@ -0,0 +1,259 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+ import { readProjectConfig, setLocalModelCapability, writeProjectConfig } from '../lib/project-config.js';
5
+
6
+ export const OLLAMA_DEFAULTS = Object.freeze({
7
+ baseUrl: 'http://127.0.0.1:11434',
8
+ model: 'qwen2.5-coder',
9
+ embedModel: 'mxbai-embed-large',
10
+ });
11
+
12
+ function clean(value) {
13
+ return value == null ? '' : String(value).trim();
14
+ }
15
+
16
+ function boolFlag(argv, name) {
17
+ return argv.includes(name);
18
+ }
19
+
20
+ export function parseOllamaArgs(argv = []) {
21
+ const first = argv[0] && !String(argv[0]).startsWith('-') ? String(argv[0]) : 'status';
22
+ const opts = {
23
+ command: first,
24
+ cwd: process.cwd(),
25
+ envFile: '',
26
+ baseUrl: '',
27
+ model: '',
28
+ embedModel: '',
29
+ install: boolFlag(argv, '--install'),
30
+ start: boolFlag(argv, '--start'),
31
+ pull: boolFlag(argv, '--pull'),
32
+ json: boolFlag(argv, '--json'),
33
+ };
34
+
35
+ for (let i = first === argv[0] ? 1 : 0; i < argv.length; i += 1) {
36
+ const arg = argv[i];
37
+ if (['--install', '--start', '--pull', '--json'].includes(arg)) continue;
38
+ if (arg === '--cwd') opts.cwd = argv[++i] || opts.cwd;
39
+ else if (arg === '--env-file') opts.envFile = argv[++i] || '';
40
+ else if (arg === '--base-url') opts.baseUrl = argv[++i] || '';
41
+ else if (arg === '--model') opts.model = argv[++i] || '';
42
+ else if (arg === '--embed-model') opts.embedModel = argv[++i] || '';
43
+ else if (!String(arg).startsWith('-') && opts.command === 'pull') {
44
+ opts.model = opts.model || arg;
45
+ } else if (arg === '--help' || arg === '-h') opts.command = 'help';
46
+ else throw new Error(`unknown ollama option: ${arg}`);
47
+ }
48
+ return opts;
49
+ }
50
+
51
+ export function resolveOllamaConfig(opts = {}, env = process.env) {
52
+ return {
53
+ baseUrl: clean(opts.baseUrl || env.OLLAMA_BASE_URL || OLLAMA_DEFAULTS.baseUrl).replace(/\/+$/, ''),
54
+ model: clean(opts.model || env.OLLAMA_MODEL || OLLAMA_DEFAULTS.model),
55
+ embedModel: clean(opts.embedModel || env.OLLAMA_EMBED_MODEL || OLLAMA_DEFAULTS.embedModel),
56
+ };
57
+ }
58
+
59
+ function replaceEnvLine(text, key, value) {
60
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
61
+ const line = `${key}=${value}`;
62
+ const re = new RegExp(`^${escaped}=.*$`, 'm');
63
+ if (re.test(text)) return text.replace(re, line);
64
+ const prefix = text && !text.endsWith('\n') ? `${text}\n` : text;
65
+ return `${prefix}${line}\n`;
66
+ }
67
+
68
+ export function upsertOllamaEnvFile(filename, config) {
69
+ const target = path.resolve(filename);
70
+ fs.mkdirSync(path.dirname(target), { recursive: true });
71
+ let text = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : '';
72
+ text = replaceEnvLine(text, 'OLLAMA_BASE_URL', config.baseUrl);
73
+ text = replaceEnvLine(text, 'OLLAMA_MODEL', config.model);
74
+ text = replaceEnvLine(text, 'OLLAMA_EMBED_MODEL', config.embedModel);
75
+ fs.writeFileSync(target, text, 'utf8');
76
+ return target;
77
+ }
78
+
79
+ export function updateProjectOllamaConfig(cwd, config) {
80
+ const root = path.resolve(cwd);
81
+ const filename = path.join(root, '.agentsam', 'config.json');
82
+ if (!fs.existsSync(filename)) return null;
83
+ const project = readProjectConfig(root);
84
+ setLocalModelCapability(project, 'ollama');
85
+ writeProjectConfig(root, project);
86
+ return filename;
87
+ }
88
+
89
+ function commandResult(command, args, options = {}) {
90
+ return spawnSync(command, args, {
91
+ encoding: 'utf8',
92
+ stdio: options.inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
93
+ timeout: options.timeout || 15_000,
94
+ ...options,
95
+ });
96
+ }
97
+
98
+ export function commandAvailable(command, spawnSyncImpl = spawnSync) {
99
+ const finder = process.platform === 'win32' ? 'where' : 'which';
100
+ const result = spawnSyncImpl(finder, [command], { encoding: 'utf8', stdio: 'ignore' });
101
+ return result.status === 0;
102
+ }
103
+
104
+ export function ollamaInstallPlan(platform = process.platform, available = {}) {
105
+ if ((platform === 'darwin' || platform === 'linux') && available.brew) {
106
+ return { command: 'brew', args: ['install', 'ollama'], manager: 'homebrew' };
107
+ }
108
+ if (platform === 'win32' && available.winget) {
109
+ return {
110
+ command: 'winget',
111
+ args: ['install', '--id', 'Ollama.Ollama', '--exact', '--accept-source-agreements', '--accept-package-agreements'],
112
+ manager: 'winget',
113
+ };
114
+ }
115
+ return null;
116
+ }
117
+
118
+ function installOllama() {
119
+ const available = {
120
+ brew: commandAvailable('brew'),
121
+ winget: commandAvailable('winget'),
122
+ };
123
+ const plan = ollamaInstallPlan(process.platform, available);
124
+ if (!plan) {
125
+ throw new Error('automatic Ollama install is unavailable on this host; install Ollama with your platform package manager, then rerun `agentsam ollama setup`');
126
+ }
127
+ const result = commandResult(plan.command, plan.args, { inherit: true, timeout: 20 * 60_000 });
128
+ if (result.status !== 0) throw new Error(`Ollama install failed via ${plan.manager}`);
129
+ return plan.manager;
130
+ }
131
+
132
+ function startOllama() {
133
+ if (process.platform === 'darwin' && commandAvailable('brew')) {
134
+ const result = commandResult('brew', ['services', 'start', 'ollama'], { inherit: true, timeout: 60_000 });
135
+ if (result.status === 0) return 'homebrew-service';
136
+ }
137
+ const child = spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' });
138
+ child.unref();
139
+ return 'ollama-serve';
140
+ }
141
+
142
+ function normalizeModelName(value) {
143
+ return clean(value).toLowerCase().replace(/:latest$/, '');
144
+ }
145
+
146
+ function hasModel(models, desired) {
147
+ const want = normalizeModelName(desired);
148
+ return models.some((row) => normalizeModelName(row.name || row.model) === want);
149
+ }
150
+
151
+ export async function probeOllama(config, fetchImpl = fetch) {
152
+ const endpoint = `${config.baseUrl}/api/tags`;
153
+ try {
154
+ const response = await fetchImpl(endpoint, { signal: AbortSignal.timeout(2000) });
155
+ if (!response.ok) return { online: false, status: response.status, endpoint, models: [] };
156
+ const body = await response.json();
157
+ const models = Array.isArray(body?.models) ? body.models.map((row) => ({ name: row.name || row.model || '' })).filter((row) => row.name) : [];
158
+ return {
159
+ online: true,
160
+ status: response.status,
161
+ endpoint,
162
+ models,
163
+ chat_ready: hasModel(models, config.model),
164
+ embed_ready: hasModel(models, config.embedModel),
165
+ };
166
+ } catch (error) {
167
+ return { online: false, status: null, endpoint, models: [], error: error?.message || String(error) };
168
+ }
169
+ }
170
+
171
+ function binaryInfo() {
172
+ if (!commandAvailable('ollama')) return { installed: false, version: null };
173
+ const result = commandResult('ollama', ['--version']);
174
+ return {
175
+ installed: result.status === 0,
176
+ version: result.status === 0 ? clean(result.stdout || result.stderr) : null,
177
+ };
178
+ }
179
+
180
+ function pullModel(model) {
181
+ const result = commandResult('ollama', ['pull', model], { inherit: true, timeout: 60 * 60_000 });
182
+ if (result.status !== 0) throw new Error(`ollama pull failed for ${model}`);
183
+ }
184
+
185
+ function help() {
186
+ return `agentsam ollama <setup|status|list|pull> [options]\n\nLocal-only Ollama development kit. Nothing here configures an edge model.\n\n setup Write OLLAMA_* values into the local project .env\n --install Explicitly install Ollama with a supported package manager\n --start Start the local Ollama service\n --pull Pull both configured chat + embedding models\n status Probe the local Ollama API and configured models\n list Alias for status with model inventory\n pull [model] Pull one model, or both configured defaults when omitted\n\n --base-url <url> Default http://127.0.0.1:11434\n --model <name> Default qwen2.5-coder\n --embed-model <name> Default mxbai-embed-large\n --cwd <path> Project root (default current directory)\n --env-file <path> Env file to update (default <cwd>/.env)\n --json Machine-readable output\n\nRemote AgentSam sessions should use the existing user-hosted terminal tunnel to execute this CLI on the local machine; the Worker should not try to reach 127.0.0.1.\n`;
187
+ }
188
+
189
+ function printResult(result, json) {
190
+ if (json) {
191
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
192
+ return;
193
+ }
194
+ console.log(`Ollama local development`);
195
+ console.log(` base URL ${result.config.baseUrl}`);
196
+ console.log(` chat model ${result.config.model}`);
197
+ console.log(` embed model ${result.config.embedModel}`);
198
+ console.log(` binary ${result.binary.installed ? result.binary.version || 'installed' : 'not installed'}`);
199
+ console.log(` API ${result.api.online ? 'online' : 'offline'}`);
200
+ if (result.env_file) console.log(` env ${result.env_file}`);
201
+ if (result.project_config) console.log(` project ${result.project_config}`);
202
+ if (result.api.online) {
203
+ console.log(` chat ready ${result.api.chat_ready ? 'yes' : 'no'}`);
204
+ console.log(` embed ready ${result.api.embed_ready ? 'yes' : 'no'}`);
205
+ }
206
+ if (!result.binary.installed) console.log(` next agentsam ollama setup --install --start --pull`);
207
+ else if (!result.api.online) console.log(` next agentsam ollama setup --start`);
208
+ else if (!result.api.chat_ready || !result.api.embed_ready) console.log(` next agentsam ollama pull`);
209
+ }
210
+
211
+ export async function runOllama(argv = []) {
212
+ const opts = parseOllamaArgs(argv);
213
+ if (opts.command === 'help') {
214
+ process.stdout.write(help());
215
+ return;
216
+ }
217
+ if (!['setup', 'status', 'list', 'pull'].includes(opts.command)) {
218
+ throw new Error(`unknown ollama command: ${opts.command}`);
219
+ }
220
+
221
+ const root = path.resolve(opts.cwd || process.cwd());
222
+ const config = resolveOllamaConfig(opts);
223
+ let envFile = null;
224
+ let projectConfig = null;
225
+ let installedBy = null;
226
+ let startedBy = null;
227
+
228
+ if (opts.command === 'setup') {
229
+ envFile = upsertOllamaEnvFile(opts.envFile || path.join(root, '.env'), config);
230
+ projectConfig = updateProjectOllamaConfig(root, config);
231
+ if (!commandAvailable('ollama') && opts.install) installedBy = installOllama();
232
+ if (commandAvailable('ollama') && opts.start) startedBy = startOllama();
233
+ }
234
+
235
+ if (opts.command === 'pull') {
236
+ if (!commandAvailable('ollama')) throw new Error('Ollama is not installed; run `agentsam ollama setup --install` first');
237
+ const requested = opts.model && opts.model !== OLLAMA_DEFAULTS.model ? [opts.model] : [config.model, config.embedModel];
238
+ for (const model of [...new Set(requested.filter(Boolean))]) pullModel(model);
239
+ } else if (opts.command === 'setup' && opts.pull) {
240
+ if (!commandAvailable('ollama')) throw new Error('Ollama is not installed; rerun setup with --install before --pull');
241
+ for (const model of [...new Set([config.model, config.embedModel])]) pullModel(model);
242
+ }
243
+
244
+ if (startedBy) await new Promise((resolve) => setTimeout(resolve, 600));
245
+ const result = {
246
+ schemaVersion: 'agentsam-ollama-local-v1',
247
+ local_only: true,
248
+ edge_model: false,
249
+ config,
250
+ env_file: envFile,
251
+ project_config: projectConfig,
252
+ installed_by: installedBy,
253
+ started_by: startedBy,
254
+ binary: binaryInfo(),
255
+ api: await probeOllama(config),
256
+ };
257
+ printResult(result, opts.json);
258
+ return result;
259
+ }