@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/AGENTSAM.md +55 -0
  2. package/README.md +17 -12
  3. package/bin/agentsam +2 -0
  4. package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
  5. package/docs/AGENTSAM_WORKBENCH.md +30 -0
  6. package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
  7. package/docs/CAPABILITIES.md +5 -3
  8. package/docs/CLI_SHELL.md +167 -80
  9. package/docs/CMS_STUDIO.md +101 -0
  10. package/docs/CONTEXT.md +170 -0
  11. package/docs/LOCAL_OLLAMA.md +58 -0
  12. package/docs/MERKLE.md +19 -19
  13. package/docs/PORTABLE_CONTEXT.md +4 -3
  14. package/docs/PROJECT_CONFIG.md +72 -0
  15. package/docs/RELEASES.md +35 -2
  16. package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
  17. package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
  18. package/docs/SDK_WORKER.md +86 -0
  19. package/docs/SECURITY.md +59 -22
  20. package/docs/client-cms-editor.md +15 -0
  21. package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
  22. package/package.json +35 -10
  23. package/packages/connectors/cloudflare/package.json +10 -0
  24. package/packages/connectors/cloudflare/src/index.js +127 -0
  25. package/packages/connectors/cloudflare/src/owner.js +76 -0
  26. package/packages/connectors/cloudflare/src/routes.js +223 -0
  27. package/packages/connectors/cloudflare/src/vault.js +80 -0
  28. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  29. package/packages/identity/package.json +10 -1
  30. package/packages/identity/src/contracts/auth-config.js +109 -0
  31. package/packages/identity/src/index.js +1 -0
  32. package/packages/identity/src/oauth/README.md +2 -2
  33. package/packages/identity/src/oauth/credentials.js +11 -4
  34. package/packages/identity/src/oauth/iam-platform.js +3 -3
  35. package/packages/identity/src/providers/iam/index.js +7 -7
  36. package/packages/identity/src/providers/iam/oauth.js +6 -4
  37. package/packages/identity/src/providers/iam/profile.js +5 -5
  38. package/packages/identity/tests/auth-config.test.mjs +61 -0
  39. package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
  40. package/protocol/FILEMETA_V1.md +95 -0
  41. package/protocol/INSPECT_VIEWS_V1.md +40 -0
  42. package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
  43. package/protocol/MERKLE_V1.md +3 -1
  44. package/protocol/README.md +1 -0
  45. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  46. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  47. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  48. package/protocol/capabilities/manifest.json +79 -3
  49. package/protocol/capabilities/repository-snapshot.schema.json +1 -0
  50. package/protocol/context/context-budget.schema.json +36 -0
  51. package/protocol/context/context-item.schema.json +19 -0
  52. package/protocol/context/resolved-context-pack.schema.json +47 -0
  53. package/protocol/context/result-policy.schema.json +17 -0
  54. package/protocol/knowledge/context-pack.schema.json +33 -0
  55. package/protocol/knowledge/retrieval-query.schema.json +68 -13
  56. package/protocol/models/README.md +373 -0
  57. package/protocol/models/model-inventory-v2.schema.json +212 -0
  58. package/python/README.md +15 -7
  59. package/python/agentsam_sdk/cli.py +0 -21
  60. package/python/agentsam_sdk/tui/README.md +17 -12
  61. package/python/agentsam_sdk/tui/bootstrap.py +2 -2
  62. package/python/agentsam_sdk/tui/demo.py +25 -10
  63. package/python/agentsam_sdk/tui/onboarding.py +208 -0
  64. package/python/tests/test_tui_cli.py +7 -6
  65. package/skills/README.md +21 -0
  66. package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
  67. package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
  68. package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
  69. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  70. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  71. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  72. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  73. package/skills/agentsam-jr-dev/SKILL.md +232 -0
  74. package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
  75. package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
  76. package/skills/agentsam-progression-guard/SKILL.md +197 -0
  77. package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
  78. package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
  79. package/skills/catalog.json +71 -0
  80. package/src/agent/capability-adapter.js +25 -13
  81. package/src/agent/index.js +1 -0
  82. package/src/agent/responses-runner.js +325 -0
  83. package/src/capabilities/index.js +7 -0
  84. package/src/capabilities/repository-snapshot-view.js +238 -0
  85. package/src/capabilities/repository-snapshot.js +28 -14
  86. package/src/cli.js +198 -76
  87. package/src/cloudflare/cpu-profile.js +115 -0
  88. package/src/cloudflare/index.js +14 -0
  89. package/src/cloudflare/wrangler.js +132 -0
  90. package/src/commands/account-auth.js +47 -0
  91. package/src/commands/cloudflare.js +58 -0
  92. package/src/commands/connections.js +93 -0
  93. package/src/commands/context-economics.js +114 -0
  94. package/src/commands/context.js +14 -2
  95. package/src/commands/db.js +4 -7
  96. package/src/commands/deploy.js +54 -32
  97. package/src/commands/eval.js +63 -0
  98. package/src/commands/interactive.js +18 -0
  99. package/src/commands/knowledge.js +27 -4
  100. package/src/commands/merkle-persist.js +118 -0
  101. package/src/commands/merkle.js +32 -17
  102. package/src/commands/models.js +152 -0
  103. package/src/commands/ollama.js +259 -0
  104. package/src/commands/preferences.js +144 -0
  105. package/src/commands/product.js +86 -16
  106. package/src/commands/resume.js +67 -0
  107. package/src/commands/security.js +8 -6
  108. package/src/commands/shell.js +405 -100
  109. package/src/commands/skills.js +66 -0
  110. package/src/commands/start-local.js +1 -1
  111. package/src/commands/tunnel.js +5 -4
  112. package/src/commands/whoami.js +86 -0
  113. package/src/context/budget.js +120 -0
  114. package/src/context/compact.js +28 -0
  115. package/src/context/index.js +7 -0
  116. package/src/context/rehydrate.js +35 -0
  117. package/src/context/resolve.js +116 -0
  118. package/src/context/result-policy.js +66 -0
  119. package/src/errors/diagnostic.js +160 -0
  120. package/src/errors/index.js +9 -0
  121. package/src/eval/context.js +191 -0
  122. package/src/eval/index.js +1 -0
  123. package/src/index.js +70 -0
  124. package/src/indexing/execution-boundary.js +144 -0
  125. package/src/indexing/index.js +8 -0
  126. package/src/indexing/provider.js +41 -0
  127. package/src/knowledge/config.js +2 -2
  128. package/src/knowledge/context-pack.js +12 -1
  129. package/src/knowledge/contracts.js +9 -4
  130. package/src/knowledge/engine.js +7 -3
  131. package/src/knowledge/service/server.js +1 -1
  132. package/src/lib/account-session.js +98 -0
  133. package/src/lib/agent-instructions.js +73 -0
  134. package/src/lib/auth.js +4 -0
  135. package/src/lib/bridge-client.js +7 -5
  136. package/src/lib/cli-preferences.js +78 -0
  137. package/src/lib/core-client.js +8 -8
  138. package/src/lib/deploy/git-guard.js +69 -0
  139. package/src/lib/deploy/health.js +57 -0
  140. package/src/lib/deploy/local-studio.js +283 -0
  141. package/src/lib/deploy/secret-scan.js +65 -0
  142. package/src/lib/deploy-receipt/index.js +5 -2
  143. package/src/lib/detect-context.js +6 -5
  144. package/src/lib/execution-approvals.js +59 -0
  145. package/src/lib/identity-scaffold.js +1 -1
  146. package/src/lib/local-scaffold.js +35 -33
  147. package/src/lib/local-sessions.js +127 -0
  148. package/src/lib/local-status.js +9 -17
  149. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  150. package/src/lib/merkle/filemeta.js +43 -0
  151. package/src/lib/merkle/git-ignore.js +24 -0
  152. package/src/lib/merkle/hash.js +1 -0
  153. package/src/lib/merkle/index.js +22 -0
  154. package/src/lib/merkle/persistence.js +72 -0
  155. package/src/lib/merkle/semantic.js +359 -0
  156. package/src/lib/merkle/snapshot.js +9 -3
  157. package/src/lib/merkle/tree.js +11 -6
  158. package/src/lib/project-config.js +227 -0
  159. package/src/lib/project-rules.js +68 -0
  160. package/src/lib/provider-credentials.js +83 -0
  161. package/src/lib/save-sdk-token.js +1 -1
  162. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  163. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  164. package/src/lib/slash-commands.js +22 -15
  165. package/src/lib/tools.js +11 -5
  166. package/src/models/catalog.js +135 -0
  167. package/src/models/index.js +7 -0
  168. package/src/providers/index.js +5 -0
  169. package/src/providers/openai-responses.js +275 -0
  170. package/src/security/index.js +1 -0
  171. package/src/security/inventory.js +4 -1
  172. package/src/security/process.js +35 -9
  173. package/src/security/render.js +27 -5
  174. package/src/security/scan.js +24 -9
  175. package/src/security/trust-boundary.js +24 -0
  176. package/src/skills/index.js +64 -0
  177. package/src/telemetry/contracts.js +203 -0
  178. package/src/telemetry/events.js +48 -0
  179. package/src/telemetry/index.js +8 -0
  180. package/src/tools/hydrate.js +35 -0
  181. package/src/tools/index.js +2 -0
  182. package/src/tools/search.js +70 -0
  183. package/src/ui/ansi.js +1 -1
  184. package/src/ui/boot.js +54 -0
  185. package/src/ui/merkle/render.js +1 -0
  186. package/src/ui/runtime-activity.js +192 -0
  187. package/src/ui/theme.js +19 -18
  188. package/test/account-session.test.mjs +36 -0
  189. package/test/app-building-skills.test.mjs +61 -0
  190. package/test/apps-scaffold-contract.test.mjs +56 -0
  191. package/test/capabilities.test.mjs +53 -4
  192. package/test/cli-preferences.test.mjs +46 -0
  193. package/test/cloudflare-connector.test.mjs +96 -0
  194. package/test/cloudflare-runtime.test.mjs +75 -0
  195. package/test/context.test.mjs +144 -0
  196. package/test/deploy-health-scan.test.mjs +67 -0
  197. package/test/error-diagnostics.test.mjs +59 -0
  198. package/test/eval-context.test.mjs +37 -0
  199. package/test/execution-approvals.test.mjs +27 -0
  200. package/test/indexing-provider.test.mjs +29 -0
  201. package/test/jr-dev-skill.test.mjs +26 -0
  202. package/test/knowledge-context-pack.test.mjs +19 -0
  203. package/test/knowledge.test.mjs +1 -1
  204. package/test/local-sessions.test.mjs +42 -0
  205. package/test/local-studio-deploy.test.mjs +83 -0
  206. package/test/merkle-persistence.test.mjs +91 -0
  207. package/test/merkle.test.mjs +77 -3
  208. package/test/model-catalog.test.mjs +43 -0
  209. package/test/models.test.mjs +51 -0
  210. package/test/npm10-lock.test.mjs +29 -0
  211. package/test/ollama.test.mjs +94 -0
  212. package/test/openai-responses.test.mjs +95 -0
  213. package/test/project-config.test.mjs +81 -0
  214. package/test/project-rules.test.mjs +44 -0
  215. package/test/provider-credentials.test.mjs +52 -0
  216. package/test/rehydrate.test.mjs +25 -0
  217. package/test/release-hygiene.test.mjs +34 -0
  218. package/test/repository-snapshot-view.test.mjs +112 -0
  219. package/test/responses-runner.test.mjs +148 -0
  220. package/test/runtime-activity.test.mjs +98 -0
  221. package/test/sdk-worker-contract.test.mjs +68 -0
  222. package/test/security.test.mjs +46 -0
  223. package/test/shell.test.mjs +55 -21
  224. package/test/skills.test.mjs +22 -0
  225. package/test/smoke.mjs +5 -2
  226. package/test/telemetry.test.mjs +79 -0
  227. package/test/theme-portability.test.mjs +14 -0
  228. package/test/tools-search.test.mjs +40 -0
  229. package/test/whoami-resume.test.mjs +56 -0
  230. package/examples/agentsam-tui-ansi.mjs +0 -149
  231. package/src/commands/tui.js +0 -120
  232. package/src/ui/splash-xterm.js +0 -290
  233. package/src/ui/splash.js +0 -426
@@ -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
+ }
@@ -0,0 +1,144 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { cancel, confirm, intro, isCancel, outro, select, text } from '@clack/prompts';
5
+ import { collectModelsStatus } from './models.js';
6
+ import { getModelRecord } from '../models/index.js';
7
+ import { detectCliProject, readCliPreferences, writeCliPreferences } from '../lib/cli-preferences.js';
8
+
9
+ function stopIfCancelled(value) {
10
+ if (!isCancel(value)) return value;
11
+ cancel('Agent Sam setup cancelled.');
12
+ const error = new Error('setup_cancelled');
13
+ error.code = 'AGENTSAM_SETUP_CANCELLED';
14
+ throw error;
15
+ }
16
+
17
+ function commandExists(command) {
18
+ const probe = process.platform === 'win32' ? ['where', [command]] : ['sh', ['-lc', `command -v ${command}`]];
19
+ return spawnSync(probe[0], probe[1], { stdio: 'ignore' }).status === 0;
20
+ }
21
+
22
+ export function availableShells(env = process.env) {
23
+ const values = [];
24
+ const detected = path.basename(env.SHELL || env.ComSpec || '').trim();
25
+ if (detected) values.push(detected);
26
+ for (const shell of ['zsh', 'bash', 'fish', 'pwsh']) if (!values.includes(shell) && commandExists(shell)) values.push(shell);
27
+ return values.length ? values : ['shell'];
28
+ }
29
+
30
+ export function modelOptions(status) {
31
+ const options = [{ value: 'auto', label: 'Automatic', hint: 'runtime chooses; exact economics unavailable until resolved' }];
32
+ for (const model of status.availableModels || []) {
33
+ options.push({ value: model.model_key, label: model.label, hint: `${model.provider_model_id} · provider verified` });
34
+ }
35
+ if (status.local?.online) {
36
+ for (const row of status.local.models || []) if (row?.name) options.push({ value: `ollama:${row.name}`, label: `Ollama · ${row.name}`, hint: 'local' });
37
+ }
38
+ for (const provider of status.providers || []) {
39
+ if (provider.configured && !options.some((row) => row.value.startsWith(`${provider.id}:`) && row.value !== `${provider.id}:auto`)) {
40
+ options.push({ value: `${provider.id}:auto`, label: `${provider.label} · automatic`, hint: 'credential proven; exact model not verified' });
41
+ }
42
+ }
43
+ return options;
44
+ }
45
+
46
+ function reasoningOptions(modelPreference) {
47
+ const record = getModelRecord(modelPreference);
48
+ if (!record) return [{ value: 'auto', label: 'Automatic', hint: 'runtime/provider default' }];
49
+ return record.reasoning_efforts.map((value) => ({
50
+ value,
51
+ label: value === 'xhigh' ? 'Extra high' : value === 'max' ? 'Max' : value[0].toUpperCase() + value.slice(1),
52
+ hint: value === 'low' ? 'lighter reasoning' : value === 'max' ? 'highest supported reasoning depth' : undefined,
53
+ }));
54
+ }
55
+
56
+ function serviceTierOptions(modelPreference) {
57
+ const record = getModelRecord(modelPreference);
58
+ if (!record) return [{ value: 'default', label: 'Standard', hint: 'default provider processing' }];
59
+ return record.service_tiers.map((value) => {
60
+ if (value === 'fast') return { value, label: 'Fast', hint: 'lower latency · 2× applicable token rates for Astra' };
61
+ if (value === 'flex') return { value, label: 'Flex', hint: 'slower / capacity-sensitive · 50% of Standard for Astra' };
62
+ return { value, label: 'Standard', hint: 'standard pricing and latency' };
63
+ });
64
+ }
65
+
66
+ async function promptModelPreferences(identity, existing, options = {}) {
67
+ let status = { providers: [], availableModels: [], local: { online: false, models: [] } };
68
+ try { status = await collectModelsStatus(options.modelStatusOptions || {}); } catch { /* inventory remains best-effort */ }
69
+ const models = modelOptions(status);
70
+ const initialModel = models.some((row) => row.value === existing.modelPreference) ? existing.modelPreference : 'auto';
71
+ const modelPreference = stopIfCancelled(await select({ message: 'Model', initialValue: initialModel, options: models }));
72
+
73
+ const reasoning = reasoningOptions(modelPreference);
74
+ const initialReasoning = reasoning.some((row) => row.value === existing.reasoningEffort) ? existing.reasoningEffort : reasoning[0].value;
75
+ const reasoningEffort = stopIfCancelled(await select({ message: 'Reasoning level', initialValue: initialReasoning, options: reasoning }));
76
+
77
+ const tiers = serviceTierOptions(modelPreference);
78
+ const initialTier = tiers.some((row) => row.value === existing.serviceTier) ? existing.serviceTier : tiers[0].value;
79
+ const serviceTier = stopIfCancelled(await select({ message: 'Processing', initialValue: initialTier, options: tiers }));
80
+
81
+ return { modelPreference, reasoningEffort, serviceTier };
82
+ }
83
+
84
+ export async function configureCliPreferences({ cwd = process.cwd(), firstRun = false, section = 'all', modelStatusOptions } = {}) {
85
+ let identity = detectCliProject(cwd);
86
+ const existing = readCliPreferences(identity.root) || {};
87
+ intro(firstRun ? `You are in ${identity.root}` : section === 'model' ? 'Agent Sam model' : 'Agent Sam settings');
88
+
89
+ let trustedDirectory = existing.trustedDirectory === true;
90
+ if (firstRun && !trustedDirectory) {
91
+ trustedDirectory = stopIfCancelled(await confirm({
92
+ message: 'Do you trust the contents of this directory? Project-local instructions, hooks, and execution policy may load.',
93
+ initialValue: false,
94
+ }));
95
+ if (!trustedDirectory) {
96
+ cancel('Directory not trusted. Agent Sam did not load project-local configuration.');
97
+ const error = new Error('directory_not_trusted');
98
+ error.code = 'AGENTSAM_SETUP_CANCELLED';
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ let runtime = existing.runtime || 'local';
104
+ let terminal = existing.terminal || availableShells()[0];
105
+ if (!firstRun && section === 'all') {
106
+ const projectChoice = stopIfCancelled(await select({
107
+ message: 'Where should Agent Sam work?',
108
+ initialValue: 'current',
109
+ options: [
110
+ { value: 'current', label: `This project · ${identity.project}`, hint: identity.root },
111
+ { value: 'other', label: 'Choose another folder' },
112
+ ],
113
+ }));
114
+ if (projectChoice === 'other') {
115
+ const folder = stopIfCancelled(await text({
116
+ message: 'Project folder', placeholder: identity.root, defaultValue: identity.root,
117
+ validate(value) {
118
+ const resolved = path.resolve(String(value || ''));
119
+ if (!fs.existsSync(resolved)) return 'Folder does not exist';
120
+ if (!fs.statSync(resolved).isDirectory()) return 'Path is not a directory';
121
+ },
122
+ }));
123
+ identity = detectCliProject(path.resolve(String(folder)));
124
+ }
125
+ runtime = stopIfCancelled(await select({
126
+ message: 'Runtime', initialValue: runtime,
127
+ options: [
128
+ { value: 'local', label: 'Local machine', hint: 'real shell + filesystem' },
129
+ { value: 'remote', label: 'Remote', hint: 'connected remote runtime' },
130
+ { value: 'sandbox', label: 'Sandbox', hint: 'isolated disposable runtime' },
131
+ ],
132
+ }));
133
+ const shells = availableShells();
134
+ terminal = stopIfCancelled(await select({
135
+ message: 'Terminal', initialValue: terminal && shells.includes(terminal) ? terminal : shells[0],
136
+ options: shells.map((shell) => ({ value: shell, label: shell, hint: shell === shells[0] ? 'detected/default' : undefined })),
137
+ }));
138
+ }
139
+
140
+ const model = await promptModelPreferences(identity, existing, { modelStatusOptions });
141
+ const preferences = writeCliPreferences(identity.root, { trustedDirectory, runtime, terminal, ...model });
142
+ outro(`Ready · ${identity.project}`);
143
+ return { identity, preferences };
144
+ }
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { spawn } from 'node:child_process';
4
- import { getCapability, getCapabilityManifest, listCapabilities, repositorySnapshot } from '../capabilities/index.js';
4
+ import { getCapability, getCapabilityManifest, listCapabilities, projectRepositorySnapshot, repositorySnapshot } from '../capabilities/index.js';
5
5
  import { getAddon, listAddons, listPresets } from '../presets/index.js';
6
+ import { projectConfigPath, readProjectConfig, setDeployTarget, setProductPreset, writeProjectConfig } from '../lib/project-config.js';
6
7
 
7
8
  function parseCommon(argv = []) {
8
9
  const out = { json: false, cwd: process.cwd(), positionals: [] };
@@ -18,24 +19,94 @@ function parseCommon(argv = []) {
18
19
  export async function runInspect(argv = []) {
19
20
  const opts = parseCommon(argv);
20
21
  let churnDays = 30;
22
+ let view = 'index';
23
+ let viewExplicit = false;
24
+ let limit = 50;
25
+ let facetLimit = 48;
26
+ let pretty = false;
27
+ let snapshotFile = '';
28
+ let saveSnapshot = '';
29
+ const filters = {};
30
+ const addFilter = (field, value) => {
31
+ const clean = String(value || '').trim();
32
+ if (!clean) throw new Error(`inspect_${field}_value_required`);
33
+ (filters[field] ||= []).push(clean);
34
+ };
35
+ const filterFlags = new Map([
36
+ ['--system', 'system'], ['--package', 'package'], ['--category', 'category'], ['--layer', 'layer'],
37
+ ['--kind', 'kind'], ['--language', 'language'], ['--role', 'role'], ['--execution-domain', 'execution_domain'], ['--tag', 'tag'],
38
+ ['--path', 'path'], ['--symbol', 'symbol'], ['--import', 'import'], ['--match', 'match'],
39
+ ]);
21
40
  for (let i = 0; i < opts.positionals.length; i += 1) {
22
- if (opts.positionals[i] === '--churn-days') churnDays = Number(opts.positionals[++i] || 30);
23
- else if (opts.positionals[i] === 'repository') continue;
24
- else throw new Error(`unknown inspect option: ${opts.positionals[i]}`);
41
+ const arg = opts.positionals[i];
42
+ if (arg === '--churn-days') churnDays = Number(opts.positionals[++i] || 30);
43
+ else if (arg === '--view') { view = String(opts.positionals[++i] || '').trim().toLowerCase(); viewExplicit = true; }
44
+ else if (arg === '--limit') limit = Number(opts.positionals[++i] || 50);
45
+ else if (arg === '--facet-limit') facetLimit = Number(opts.positionals[++i] || 48);
46
+ else if (arg === '--pretty') pretty = true;
47
+ else if (arg === '--full') { view = 'full'; viewExplicit = true; }
48
+ else if (arg === '--index') { view = 'index'; viewExplicit = true; }
49
+ else if (arg === '--snapshot-file') snapshotFile = String(opts.positionals[++i] || '').trim();
50
+ else if (arg === '--save-snapshot') saveSnapshot = String(opts.positionals[++i] || '').trim();
51
+ else if (filterFlags.has(arg)) addFilter(filterFlags.get(arg), opts.positionals[++i]);
52
+ else if (arg === 'repository') continue;
53
+ else throw new Error(`unknown inspect option: ${arg}`);
25
54
  }
26
- const result = await repositorySnapshot({ cwd: opts.cwd, churnDays });
27
- if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
28
- else {
55
+ if (!['full', 'index', 'files'].includes(view)) throw new Error(`invalid inspect view: ${view}`);
56
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error('inspect limit must be an integer from 1..500');
57
+ if (!Number.isInteger(facetLimit) || facetLimit < 1 || facetLimit > 200) throw new Error('inspect facet limit must be an integer from 1..200');
58
+ if (Object.keys(filters).length && !viewExplicit) view = 'files';
59
+ else if (Object.keys(filters).length && view === 'full') view = 'files';
60
+ if (argv.includes('--snapshot-file') && !snapshotFile) throw new Error('inspect_snapshot_file_value_required');
61
+ if (argv.includes('--save-snapshot') && !saveSnapshot) throw new Error('inspect_save_snapshot_value_required');
62
+
63
+ const result = snapshotFile
64
+ ? JSON.parse(fs.readFileSync(path.resolve(opts.cwd, snapshotFile), 'utf8'))
65
+ : await repositorySnapshot({ cwd: opts.cwd, churnDays });
66
+ if (!result || result.capability !== 'repository.snapshot' || !result.snapshot_id || !result.tree) {
67
+ throw new Error('inspect_snapshot_file_invalid');
68
+ }
69
+ if (saveSnapshot) {
70
+ const filename = path.resolve(opts.cwd, saveSnapshot);
71
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
72
+ fs.writeFileSync(filename, `${JSON.stringify(result)}\n`, 'utf8');
73
+ }
74
+ const output = view === 'full' ? result : projectRepositorySnapshot(result, { view, filters, limit, facetLimit });
75
+ if (opts.json) process.stdout.write(`${JSON.stringify(output, null, pretty ? 2 : 0)}\n`);
76
+ else if (view !== 'full') {
77
+ console.log(`\nRepository snapshot ${result.snapshot_id} · ${view}`);
78
+ console.log(` repo ${result.repository.full_name || result.repository.repository_id || '(local)'}`);
79
+ console.log(` revision ${result.repository.revision_sha}`);
80
+ console.log(` merkle ${result.tree.merkle_root}`);
81
+ console.log(` metadata ${result.tree.metadata_root}`);
82
+ console.log(` matched ${output.projection.matched}${output.projection.truncated ? ` (showing ${output.projection.returned})` : ''}`);
83
+ const systems = output.facets.systems.slice(0, 12).map((row) => `${row.value}:${row.count}`).join(', ');
84
+ if (systems) console.log(` systems ${systems}`);
85
+ const trust = output.analysis?.trust_boundary;
86
+ if (trust) {
87
+ console.log(` trust ${trust.status}${trust.finding_count ? ` · ${trust.finding_count} contradiction${trust.finding_count === 1 ? '' : 's'}` : ''}`);
88
+ for (const finding of trust.findings || []) {
89
+ console.log(` ! ${finding.kind}: ${finding.source}${finding.target ? ` → ${finding.target}` : finding.specifier ? ` · ${finding.specifier}` : finding.env ? ` · ${finding.env}` : ''}`);
90
+ if (finding.repair_action) console.log(` repair: ${finding.repair_action}`);
91
+ }
92
+ if (trust.findings_truncated) console.log(` … ${trust.finding_count - trust.findings_returned} more contradiction(s); use --view full --json for full evidence`);
93
+ }
94
+ if (view === 'files') for (const file of output.files) console.log(` ${file.path} [${file.system || '-'} / ${file.category || '-'} / ${file.execution_domain || 'unknown'}]`);
95
+ console.log('');
96
+ } else {
29
97
  console.log(`\nRepository snapshot ${result.snapshot_id}`);
30
98
  console.log(` repo ${result.repository.full_name || result.repository.repository_id || '(local)'}`);
31
99
  console.log(` revision ${result.repository.revision_sha}`);
32
100
  console.log(` merkle ${result.tree.merkle_root}`);
101
+ console.log(` metadata ${result.tree.metadata_root}`);
33
102
  console.log(` files ${result.intelligence.summary?.file_count ?? result.tree.stats?.files ?? 'unknown'}`);
34
103
  console.log(` knowledge ${result.knowledge?.indexed ? result.knowledge.generation_id : result.knowledge?.configured ? 'configured / not indexed' : 'not configured'}`);
104
+ const trust = result.analysis?.trust_boundary;
105
+ if (trust) console.log(` trust ${trust.status}${trust.findings?.length ? ` · ${trust.findings.length} contradiction${trust.findings.length === 1 ? '' : 's'}` : ''}`);
35
106
  console.log(` deploy ${result.deploy?.status || 'no trusted receipt'}`);
36
107
  console.log(` content ${result.content_hash}\n`);
37
108
  }
38
- return result;
109
+ return output;
39
110
  }
40
111
 
41
112
  export async function runCapabilities(argv = []) {
@@ -53,16 +124,15 @@ export async function runCapabilities(argv = []) {
53
124
  return result;
54
125
  }
55
126
 
56
- function projectConfigPath(cwd) { return path.join(cwd, '.agentsam', 'config.json'); }
57
127
  function featureStatePath(cwd) { return path.join(cwd, '.agentsam', 'features.json'); }
58
128
 
59
129
  export function applyPresetSelection(cwd, preset) {
60
130
  const filename = projectConfigPath(cwd);
61
131
  if (!fs.existsSync(filename)) throw new Error('not_agentsam_project');
62
- const config = JSON.parse(fs.readFileSync(filename, 'utf8'));
63
- const next = { ...config, preset: preset.id, features: [...preset.features], capabilities: [...preset.capabilities] };
64
- fs.writeFileSync(filename, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
65
- return next;
132
+ const config = readProjectConfig(cwd);
133
+ setProductPreset(config, preset);
134
+ writeProjectConfig(cwd, config);
135
+ return config;
66
136
  }
67
137
 
68
138
  export async function runAdd(argv = []) {
@@ -84,9 +154,9 @@ export async function runAdd(argv = []) {
84
154
  state.features[addon.id] = { selected: true, capabilities: addon.capabilities, selected_at: new Date().toISOString() };
85
155
  fs.writeFileSync(featureStatePath(cwd), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
86
156
  if (addon.id === 'deploy-cloudflare') {
87
- const config = JSON.parse(fs.readFileSync(projectConfigPath(cwd), 'utf8'));
88
- config.deploy_target = 'cloudflare';
89
- fs.writeFileSync(projectConfigPath(cwd), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
157
+ const config = readProjectConfig(cwd);
158
+ setDeployTarget(config, 'cloudflare');
159
+ writeProjectConfig(cwd, config);
90
160
  }
91
161
  const result = { ok: true, feature: addon.id, capabilities: addon.capabilities, description: addon.description, state_file: '.agentsam/features.json' };
92
162
  if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import { isCancel, select } from '@clack/prompts';
3
+ import { listLocalSessions, loadLocalSession } from '../lib/local-sessions.js';
4
+ import { runShell } from './shell.js';
5
+
6
+ function writeLine(write, value = '') { write(`${value}\n`); }
7
+ function formatWhen(value) {
8
+ const date = new Date(value);
9
+ return Number.isNaN(date.getTime()) ? String(value || '') : date.toLocaleString();
10
+ }
11
+
12
+ export function renderSessionList(sessions = []) {
13
+ if (!sessions.length) return '\n No resumable Agent Sam sessions found.\n';
14
+ const lines = ['', ' Agent Sam · sessions', ''];
15
+ for (const row of sessions) {
16
+ lines.push(` ${row.id} ${row.title}`);
17
+ lines.push(` ${formatWhen(row.updated_at)} · ${row.model_key || 'no model'} · ${row.cwd}`);
18
+ }
19
+ lines.push('');
20
+ return lines.join('\n');
21
+ }
22
+
23
+ export async function runResume(argv = [], options = {}) {
24
+ const write = options.write || ((text) => process.stdout.write(text));
25
+ const json = argv.includes('--json');
26
+ const listOnly = argv.includes('--list');
27
+ const positional = argv.filter((arg) => !arg.startsWith('-'));
28
+ const unknown = argv.filter((arg) => arg.startsWith('-') && !['--json', '--list'].includes(arg));
29
+ if (unknown.length) throw new Error(`unknown resume option: ${unknown[0]}`);
30
+ if (positional.length > 1) throw new Error('agentsam resume accepts at most one session id');
31
+
32
+ if (listOnly || (json && !positional[0])) {
33
+ const sessions = listLocalSessions({ home: options.home, limit: 30 });
34
+ if (json) writeLine(write, JSON.stringify(sessions, null, 2));
35
+ else write(renderSessionList(sessions));
36
+ return sessions;
37
+ }
38
+
39
+ let sessionId = positional[0] || '';
40
+ if (!sessionId) {
41
+ const sessions = listLocalSessions({ home: options.home, limit: 30 });
42
+ if (!sessions.length) { write(renderSessionList([])); return null; }
43
+ const interactive = options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
44
+ if (!interactive) { write(renderSessionList(sessions)); return null; }
45
+ const choice = await select({
46
+ message: 'Resume Agent Sam session',
47
+ options: sessions.map((row) => ({
48
+ value: row.id,
49
+ label: row.title,
50
+ hint: `${path.basename(row.cwd)} · ${formatWhen(row.updated_at)}`,
51
+ })),
52
+ });
53
+ if (isCancel(choice)) return null;
54
+ sessionId = choice;
55
+ }
56
+
57
+ const session = loadLocalSession(sessionId, { home: options.home });
58
+ if (!session) throw new Error(`session_not_found:${sessionId}`);
59
+ if (json) { writeLine(write, JSON.stringify(session, null, 2)); return session; }
60
+
61
+ writeLine(write, `\n Resuming ${session.id}`);
62
+ writeLine(write, ` ${session.title}`);
63
+ writeLine(write, ` ${session.cwd}\n`);
64
+ const runner = options.runShellImpl || runShell;
65
+ await runner([], { cwd: session.cwd, intro: 'quiet', session, home: options.home });
66
+ return session;
67
+ }
@@ -1,18 +1,19 @@
1
1
  import fs from 'node:fs';
2
2
  import { scanProjectSecurity, reportExitCode, repairProject, formatSecurityReport } from '../security/index.js';
3
3
  import { runProcess } from '../security/process.js';
4
+ import { diagnosticFromError, renderDiagnosticError } from '../errors/index.js';
4
5
 
5
6
  const help = [
6
- 'Agent Sam · dependency health',
7
+ 'Agent Sam · security',
7
8
  ' agentsam security scan [--path DIR] [--offline] [--json]',
8
9
  ' agentsam security check --log FILE [--path DIR] [--json]',
9
10
  ' agentsam security run [--path DIR] [--json] -- COMMAND ARGS...',
10
11
  ' agentsam security repair [--path DIR] [--log FILE] [--apply] [--verify SCRIPT] [--json]',
11
12
  ' agentsam sca ... alias',
12
13
  '',
13
- 'scan/check: exact npm lockfile versions, full OSV advisories, deprecations and log triage.',
14
+ 'scan/check: npm/OSV evidence plus AST+Merkle client/server trust-boundary contradictions.',
14
15
  'run: execute an explicitly supplied install/build command, then check its logs and dependencies.',
15
- 'repair: plan by default; --apply updates an isolated Git worktree, installs, verifies and rescans.',
16
+ 'repair: plans dependency + trust-boundary repairs; --apply automates dependency-only repairs in an isolated worktree.',
16
17
  'Online lookup sends package names/versions to OSV, never source code or deployment logs.',
17
18
  'Offline is inventory only and exits 2. pnpm/Yarn/Bun graphs are explicitly unsupported.',
18
19
  'Exit: 0 clean/verified, 1 unresolved findings or command failure, 2 incomplete/error.',
@@ -63,9 +64,10 @@ export async function runSecurity(argv) {
63
64
  }
64
65
  process.stdout.write(options.json ? JSON.stringify(report, null, 2) + '\n' : formatSecurityReport(report, { color: Boolean(process.stdout.isTTY && !process.env.NO_COLOR) }));
65
66
  process.exitCode = reportExitCode(report);
66
- } catch {
67
- const report = { schema_version: 1, ok: false, complete: false, status: 'error', error: 'Security operation failed. Check command options, manifest/log validity, network access, and Git prerequisites. Use --help.' };
68
- process.stdout.write(options.json ? JSON.stringify(report) + '\n' : report.error + '\n');
67
+ } catch (error) {
68
+ const diagnostic = diagnosticFromError(error, { source: 'security', kind: 'security_operation_error' });
69
+ const report = { schema_version: 1, ok: false, complete: false, status: 'error', error: diagnostic };
70
+ process.stdout.write(options.json ? JSON.stringify(report) + '\n' : `${renderDiagnosticError(error)}\n`);
69
71
  process.exitCode = 2;
70
72
  }
71
73
  }