@inneranimalmedia/agentsam-sdk 2.4.0 → 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 (162) 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 -5
  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/cad.js +56 -6
  66. package/src/commands/context.js +14 -2
  67. package/src/commands/db.js +4 -7
  68. package/src/commands/deploy.js +17 -31
  69. package/src/commands/interactive.js +21 -0
  70. package/src/commands/knowledge.js +27 -4
  71. package/src/commands/merkle-persist.js +118 -0
  72. package/src/commands/merkle.js +32 -17
  73. package/src/commands/models.js +107 -0
  74. package/src/commands/ollama.js +259 -0
  75. package/src/commands/preferences.js +102 -0
  76. package/src/commands/product.js +86 -16
  77. package/src/commands/security.js +3 -3
  78. package/src/commands/shell.js +71 -27
  79. package/src/commands/skills.js +66 -0
  80. package/src/commands/start-local.js +1 -1
  81. package/src/commands/tunnel.js +5 -4
  82. package/src/context/budget.js +58 -0
  83. package/src/context/compact.js +28 -0
  84. package/src/context/index.js +5 -0
  85. package/src/context/resolve.js +84 -0
  86. package/src/context/result-policy.js +66 -0
  87. package/src/index.js +16 -0
  88. package/src/indexing/execution-boundary.js +144 -0
  89. package/src/indexing/index.js +8 -0
  90. package/src/indexing/provider.js +41 -0
  91. package/src/knowledge/config.js +2 -2
  92. package/src/knowledge/context-pack.js +12 -1
  93. package/src/knowledge/contracts.js +9 -4
  94. package/src/knowledge/engine.js +7 -3
  95. package/src/knowledge/service/server.js +1 -1
  96. package/src/lib/auth.js +10 -19
  97. package/src/lib/bridge-client.js +7 -5
  98. package/src/lib/cli-preferences.js +74 -0
  99. package/src/lib/core-client.js +8 -8
  100. package/src/lib/deploy-receipt/index.js +5 -2
  101. package/src/lib/detect-context.js +6 -5
  102. package/src/lib/identity-scaffold.js +1 -1
  103. package/src/lib/local-scaffold.js +35 -33
  104. package/src/lib/local-status.js +9 -17
  105. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  106. package/src/lib/merkle/filemeta.js +43 -0
  107. package/src/lib/merkle/git-ignore.js +24 -0
  108. package/src/lib/merkle/hash.js +1 -0
  109. package/src/lib/merkle/index.js +22 -0
  110. package/src/lib/merkle/persistence.js +72 -0
  111. package/src/lib/merkle/semantic.js +359 -0
  112. package/src/lib/merkle/snapshot.js +9 -3
  113. package/src/lib/merkle/tree.js +11 -6
  114. package/src/lib/open-url.js +66 -0
  115. package/src/lib/project-config.js +227 -0
  116. package/src/lib/project-rules.js +68 -0
  117. package/src/lib/save-sdk-token.js +1 -1
  118. package/src/lib/slash-commands.js +2 -1
  119. package/src/lib/tools.js +11 -5
  120. package/src/security/index.js +1 -0
  121. package/src/security/inventory.js +4 -1
  122. package/src/security/render.js +27 -5
  123. package/src/security/scan.js +24 -9
  124. package/src/security/trust-boundary.js +24 -0
  125. package/src/skills/index.js +64 -0
  126. package/src/tools/index.js +1 -0
  127. package/src/tools/search.js +70 -0
  128. package/src/ui/ansi.js +1 -1
  129. package/src/ui/boot.js +56 -0
  130. package/src/ui/merkle/render.js +1 -0
  131. package/src/ui/runtime-activity.js +192 -0
  132. package/src/ui/theme.js +19 -18
  133. package/test/app-building-skills.test.mjs +61 -0
  134. package/test/apps-scaffold-contract.test.mjs +56 -0
  135. package/test/capabilities.test.mjs +53 -4
  136. package/test/cli-preferences.test.mjs +25 -0
  137. package/test/context.test.mjs +95 -0
  138. package/test/indexing-provider.test.mjs +29 -0
  139. package/test/jr-dev-skill.test.mjs +26 -0
  140. package/test/knowledge-context-pack.test.mjs +19 -0
  141. package/test/knowledge.test.mjs +1 -1
  142. package/test/merkle-persistence.test.mjs +91 -0
  143. package/test/merkle.test.mjs +77 -3
  144. package/test/models.test.mjs +37 -0
  145. package/test/ollama.test.mjs +94 -0
  146. package/test/open-url.test.mjs +64 -0
  147. package/test/project-config.test.mjs +81 -0
  148. package/test/project-rules.test.mjs +44 -0
  149. package/test/release-hygiene.test.mjs +34 -0
  150. package/test/repository-snapshot-view.test.mjs +112 -0
  151. package/test/runtime-activity.test.mjs +98 -0
  152. package/test/sdk-worker-contract.test.mjs +68 -0
  153. package/test/security.test.mjs +46 -0
  154. package/test/shell.test.mjs +8 -1
  155. package/test/skills.test.mjs +22 -0
  156. package/test/smoke.mjs +2 -2
  157. package/test/theme-portability.test.mjs +14 -0
  158. package/test/tools-search.test.mjs +27 -0
  159. package/examples/agentsam-tui-ansi.mjs +0 -149
  160. package/src/commands/tui.js +0 -120
  161. package/src/ui/splash-xterm.js +0 -290
  162. package/src/ui/splash.js +0 -426
@@ -0,0 +1,102 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts';
5
+ import { collectModelsStatus } from './models.js';
6
+ import { detectCliProject, readCliPreferences, writeCliPreferences } from '../lib/cli-preferences.js';
7
+
8
+ function stopIfCancelled(value) {
9
+ if (!isCancel(value)) return value;
10
+ cancel('Agent Sam setup cancelled.');
11
+ const error = new Error('setup_cancelled');
12
+ error.code = 'AGENTSAM_SETUP_CANCELLED';
13
+ throw error;
14
+ }
15
+
16
+ function commandExists(command) {
17
+ const probe = process.platform === 'win32' ? ['where', [command]] : ['sh', ['-lc', `command -v ${command}`]];
18
+ return spawnSync(probe[0], probe[1], { stdio: 'ignore' }).status === 0;
19
+ }
20
+
21
+ export function availableShells(env = process.env) {
22
+ const values = [];
23
+ const detected = path.basename(env.SHELL || env.ComSpec || '').trim();
24
+ if (detected) values.push(detected);
25
+ for (const shell of ['zsh', 'bash', 'fish', 'pwsh']) {
26
+ if (!values.includes(shell) && commandExists(shell)) values.push(shell);
27
+ }
28
+ return values.length ? values : ['shell'];
29
+ }
30
+
31
+ function modelOptions(status) {
32
+ const options = [{ value: 'auto', label: 'Automatic', hint: 'connected host/runtime decides' }];
33
+ if (status.local?.online) {
34
+ for (const row of status.local.models || []) {
35
+ if (row?.name) options.push({ value: `ollama:${row.name}`, label: `Ollama · ${row.name}`, hint: 'local' });
36
+ }
37
+ }
38
+ for (const provider of status.providers || []) {
39
+ if (provider.configured) options.push({ value: `${provider.id}:auto`, label: provider.label, hint: 'provider configured' });
40
+ }
41
+ return options;
42
+ }
43
+
44
+ export async function configureCliPreferences({ cwd = process.cwd(), firstRun = false } = {}) {
45
+ let identity = detectCliProject(cwd);
46
+ const existing = readCliPreferences(identity.root) || {};
47
+ intro(firstRun ? 'Welcome to Agent Sam' : 'Agent Sam settings');
48
+
49
+ const projectChoice = stopIfCancelled(await select({
50
+ message: 'Where should Agent Sam work?',
51
+ initialValue: 'current',
52
+ options: [
53
+ { value: 'current', label: `This project · ${identity.project}`, hint: identity.root },
54
+ { value: 'other', label: 'Choose another folder' },
55
+ ],
56
+ }));
57
+
58
+ if (projectChoice === 'other') {
59
+ const folder = stopIfCancelled(await text({
60
+ message: 'Project folder',
61
+ placeholder: identity.root,
62
+ defaultValue: identity.root,
63
+ validate(value) {
64
+ const resolved = path.resolve(String(value || ''));
65
+ if (!fs.existsSync(resolved)) return 'Folder does not exist';
66
+ if (!fs.statSync(resolved).isDirectory()) return 'Path is not a directory';
67
+ },
68
+ }));
69
+ identity = detectCliProject(path.resolve(String(folder)));
70
+ }
71
+
72
+ const runtime = stopIfCancelled(await select({
73
+ message: 'Runtime',
74
+ initialValue: existing.runtime || 'local',
75
+ options: [
76
+ { value: 'local', label: 'Local machine', hint: 'real shell + filesystem' },
77
+ { value: 'remote', label: 'Remote', hint: 'connected remote runtime' },
78
+ { value: 'sandbox', label: 'Sandbox', hint: 'isolated disposable runtime' },
79
+ ],
80
+ }));
81
+
82
+ const shells = availableShells();
83
+ const terminal = stopIfCancelled(await select({
84
+ message: 'Terminal',
85
+ initialValue: existing.terminal && shells.includes(existing.terminal) ? existing.terminal : shells[0],
86
+ options: shells.map((shell) => ({ value: shell, label: shell, hint: shell === shells[0] ? 'detected/default' : undefined })),
87
+ }));
88
+
89
+ let status = { providers: [], local: { online: false, models: [] } };
90
+ try { status = await collectModelsStatus(); } catch { /* inventory is best-effort */ }
91
+ const models = modelOptions(status);
92
+ const initialModel = models.some((row) => row.value === existing.modelPreference) ? existing.modelPreference : 'auto';
93
+ const modelPreference = stopIfCancelled(await select({
94
+ message: 'Model preference',
95
+ initialValue: initialModel,
96
+ options: models,
97
+ }));
98
+
99
+ const preferences = writeCliPreferences(identity.root, { runtime, terminal, modelPreference });
100
+ outro(`Ready · ${identity.project}`);
101
+ return { identity, preferences };
102
+ }
@@ -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`);
@@ -3,16 +3,16 @@ import { scanProjectSecurity, reportExitCode, repairProject, formatSecurityRepor
3
3
  import { runProcess } from '../security/process.js';
4
4
 
5
5
  const help = [
6
- 'Agent Sam · dependency health',
6
+ 'Agent Sam · security',
7
7
  ' agentsam security scan [--path DIR] [--offline] [--json]',
8
8
  ' agentsam security check --log FILE [--path DIR] [--json]',
9
9
  ' agentsam security run [--path DIR] [--json] -- COMMAND ARGS...',
10
10
  ' agentsam security repair [--path DIR] [--log FILE] [--apply] [--verify SCRIPT] [--json]',
11
11
  ' agentsam sca ... alias',
12
12
  '',
13
- 'scan/check: exact npm lockfile versions, full OSV advisories, deprecations and log triage.',
13
+ 'scan/check: npm/OSV evidence plus AST+Merkle client/server trust-boundary contradictions.',
14
14
  '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.',
15
+ 'repair: plans dependency + trust-boundary repairs; --apply automates dependency-only repairs in an isolated worktree.',
16
16
  'Online lookup sends package names/versions to OSV, never source code or deployment logs.',
17
17
  'Offline is inventory only and exits 2. pnpm/Yarn/Bun graphs are explicitly unsupported.',
18
18
  'Exit: 0 clean/verified, 1 unresolved findings or command failure, 2 incomplete/error.',
@@ -1,18 +1,40 @@
1
1
  import readline from 'node:readline';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import os from 'node:os';
4
5
  import { spawnSync } from 'node:child_process';
5
6
  import { SLASH_COMMANDS, SHELL_PHASES } from '../lib/slash-commands.js';
6
7
  import { runContext } from './context.js';
7
8
  import { runDb } from './db.js';
8
9
  import { runDeploy } from './deploy.js';
9
10
  import { runStatus } from './status.js';
10
- import { runTui } from './tui.js';
11
+ import { runModels } from './models.js';
12
+ import { configureCliPreferences } from './preferences.js';
13
+ import { createRuntimeActivity } from '../ui/runtime-activity.js';
11
14
 
12
15
  function writeLine(write, value = '') {
13
16
  write(`${value}\n`);
14
17
  }
15
18
 
19
+ export function compactCwd(value, home = process.env.HOME || process.env.USERPROFILE || '') {
20
+ const cwd = path.resolve(value);
21
+ const resolvedHome = home ? path.resolve(home) : '';
22
+ if (!resolvedHome) return cwd;
23
+ if (cwd === resolvedHome) return '~';
24
+ return cwd.startsWith(`${resolvedHome}${path.sep}`) ? `~${cwd.slice(resolvedHome.length)}` : cwd;
25
+ }
26
+
27
+ export function shellUsername(env = process.env) {
28
+ const fromEnv = String(env.USER || env.USERNAME || '').trim();
29
+ if (fromEnv) return fromEnv;
30
+ try { return String(os.userInfo().username || '').trim() || 'user'; }
31
+ catch { return 'user'; }
32
+ }
33
+
34
+ export function renderShellPrompt(cwd, env = process.env) {
35
+ return `${shellUsername(env)} ${compactCwd(cwd, env.HOME || env.USERPROFILE || '')} > `;
36
+ }
37
+
16
38
  export function tokenizeShellLine(input = '') {
17
39
  const source = String(input);
18
40
  const tokens = [];
@@ -63,9 +85,7 @@ export function renderShellCatalog() {
63
85
  ╚════════════════════════════════╝
64
86
 
65
87
  Local PTY agentsam start-local ws://127.0.0.1:3099
66
- ANSI TUI agentsam tui zero-dependency Node UI
67
- Rich TUI agentsam tui rich optional richer Python UI
68
- agentsam tui rich --install
88
+ Models agentsam models providers + local model inventory
69
89
  DB agentsam db status local SQLite
70
90
 
71
91
  Current milestone: ${next?.label ?? 'local terminal experience'}
@@ -86,35 +106,44 @@ function parseDeployOptions(args, cwd) {
86
106
  return opts;
87
107
  }
88
108
 
89
- async function withProcessCwd(cwd, fn) {
90
- const previous = process.cwd();
91
- process.chdir(cwd);
92
- try {
93
- return await fn();
94
- } finally {
95
- process.chdir(previous);
96
- }
97
- }
98
109
 
99
- async function runLocalAgent(goal, write) {
110
+ export async function runLocalAgent(goal, write, options = {}) {
100
111
  if (!goal) {
101
112
  writeLine(write, ' Usage: /agent <goal>');
102
113
  writeLine(write, ' Requires the local Agent Sam dev server (default http://127.0.0.1:8787).');
103
114
  return;
104
115
  }
105
116
  const base = String(process.env.AGENTSAM_LOCAL_URL || 'http://127.0.0.1:8787').replace(/\/$/, '');
117
+ const fetchImpl = options.fetchImpl || fetch;
118
+ const activity = options.activity || createRuntimeActivity({
119
+ write,
120
+ phase: 'thinking',
121
+ interactive: options.interactive,
122
+ });
106
123
  let response;
124
+ activity.start('thinking');
107
125
  try {
108
- response = await fetch(`${base}/api/agentsam/message`, {
126
+ response = await fetchImpl(`${base}/api/agentsam/message`, {
109
127
  method: 'POST',
110
128
  headers: { 'content-type': 'application/json' },
111
129
  body: JSON.stringify({ message: goal }),
112
130
  });
113
131
  } catch (error) {
132
+ activity.fail('unavailable');
114
133
  throw new Error(`local Agent Sam unavailable at ${base} — run \`npm run dev\` first (${error?.message || error})`);
115
134
  }
116
- const text = await response.text();
117
- if (!response.ok) throw new Error(`local Agent Sam returned HTTP ${response.status}: ${text.slice(0, 400)}`);
135
+ let text;
136
+ try {
137
+ text = await response.text();
138
+ } catch (error) {
139
+ activity.fail('response error');
140
+ throw new Error(`local Agent Sam response could not be read: ${error?.message || error}`);
141
+ }
142
+ if (!response.ok) {
143
+ activity.fail(`HTTP ${response.status}`);
144
+ throw new Error(`local Agent Sam returned HTTP ${response.status}: ${text.slice(0, 400)}`);
145
+ }
146
+ activity.succeed('done');
118
147
  try {
119
148
  writeLine(write, JSON.stringify(JSON.parse(text), null, 2));
120
149
  } catch {
@@ -192,14 +221,19 @@ export async function dispatchShellLine(line, state = {}) {
192
221
  await runDb(args.length ? args : ['status'], { cwd: state.cwd });
193
222
  break;
194
223
  case '/agent':
195
- await runLocalAgent(args.join(' '), write);
224
+ await runLocalAgent(args.join(' '), write, { interactive: state.interactive });
225
+ break;
226
+ case '/models':
227
+ await runModels(args, { cwd: state.cwd, write });
196
228
  break;
229
+ case '/settings': {
230
+ const configured = await configureCliPreferences({ cwd: state.cwd, firstRun: false });
231
+ state.cwd = configured.identity.root;
232
+ break;
233
+ }
197
234
  case '/logs':
198
235
  await showLocalLogs(state.cwd, write);
199
236
  break;
200
- case '/tui':
201
- await withProcessCwd(state.cwd, () => runTui(args));
202
- break;
203
237
  case '/deploy':
204
238
  await runDeploy(parseDeployOptions(args, state.cwd));
205
239
  break;
@@ -217,7 +251,7 @@ export async function dispatchShellLine(line, state = {}) {
217
251
 
218
252
  export async function runShell(argv = [], options = {}) {
219
253
  const write = options.write || ((text) => process.stdout.write(text));
220
- const state = { cwd: path.resolve(options.cwd || process.cwd()), write };
254
+ const state = { cwd: path.resolve(options.cwd || process.cwd()), write, interactive: options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY) };
221
255
  const sub = argv[0] || '';
222
256
 
223
257
  if (sub === 'list' || sub === 'status') {
@@ -232,13 +266,20 @@ export async function runShell(argv = [], options = {}) {
232
266
  }
233
267
  if (sub) throw new Error(`unknown shell option: ${sub}`);
234
268
 
235
- write(renderShellCatalog());
236
- writeLine(write, ' Interactive shell ready. Type /help for commands; /exit to return to your host shell.');
237
- writeLine(write, '');
269
+ if (options.intro !== 'quiet') {
270
+ write(renderShellCatalog());
271
+ writeLine(write, ' Interactive shell ready. Type /help for commands; /exit to return to your host shell.');
272
+ writeLine(write, '');
273
+ }
238
274
 
239
275
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
276
+ const promptText = () => {
277
+ if (typeof options.prompt === 'function') return options.prompt(state);
278
+ if (typeof options.prompt === 'string' && options.prompt) return options.prompt;
279
+ return renderShellPrompt(state.cwd);
280
+ };
240
281
  if (rl.terminal) {
241
- rl.setPrompt('agentsam> ');
282
+ rl.setPrompt(promptText());
242
283
  rl.prompt();
243
284
  }
244
285
 
@@ -248,6 +289,9 @@ export async function runShell(argv = [], options = {}) {
248
289
  rl.close();
249
290
  break;
250
291
  }
251
- if (rl.terminal) rl.prompt();
292
+ if (rl.terminal) {
293
+ rl.setPrompt(promptText());
294
+ rl.prompt();
295
+ }
252
296
  }
253
297
  }
@@ -0,0 +1,66 @@
1
+ import { getSkill, listSkills, loadSkill } from '../skills/index.js';
2
+
3
+ function parse(argv = []) {
4
+ const out = { id: '', json: false, references: false };
5
+ for (const arg of argv) {
6
+ if (arg === '--json') out.json = true;
7
+ else if (arg === '--references' || arg === '--full') out.references = true;
8
+ else if (arg === '--help' || arg === '-h') out.help = true;
9
+ else if (arg.startsWith('-')) throw new Error(`Unknown skills option: ${arg}`);
10
+ else if (!out.id) out.id = arg;
11
+ else throw new Error('Only one skill id or alias may be supplied.');
12
+ }
13
+ return out;
14
+ }
15
+
16
+ function help() {
17
+ return [
18
+ 'Agent Sam · skills',
19
+ ' agentsam skills list portable skills',
20
+ ' agentsam skills <id-or-alias> print compact skill instructions',
21
+ ' agentsam skills <id-or-alias> --references include on-demand references',
22
+ ' agentsam skills <id-or-alias> --json machine-readable skill payload',
23
+ '',
24
+ 'Examples:',
25
+ ' agentsam skills quick-bytes',
26
+ ' agentsam skills no-regress --references',
27
+ '',
28
+ ].join('\n');
29
+ }
30
+
31
+ export function runSkills(argv = []) {
32
+ const opts = parse(argv);
33
+ if (opts.help) {
34
+ process.stdout.write(help());
35
+ return null;
36
+ }
37
+
38
+ if (!opts.id) {
39
+ const skills = listSkills();
40
+ if (opts.json) process.stdout.write(`${JSON.stringify(skills, null, 2)}\n`);
41
+ else {
42
+ process.stdout.write('Agent Sam · portable skills\n\n');
43
+ for (const skill of skills) {
44
+ const aliases = (skill.aliases || []).length ? ` (${skill.aliases.join(', ')})` : '';
45
+ process.stdout.write(` ${skill.id}${aliases}\n ${skill.description}\n`);
46
+ }
47
+ process.stdout.write('\n');
48
+ }
49
+ return skills;
50
+ }
51
+
52
+ if (!getSkill(opts.id)) throw new Error(`Unknown skill: ${opts.id}`);
53
+ const loaded = loadSkill(opts.id, { references: opts.references });
54
+ if (opts.json) {
55
+ process.stdout.write(`${JSON.stringify(loaded, null, 2)}\n`);
56
+ return loaded;
57
+ }
58
+
59
+ process.stdout.write(`${loaded.instructions.trimEnd()}\n`);
60
+ if (loaded.references?.length) {
61
+ for (const reference of loaded.references) {
62
+ process.stdout.write(`\n---\nReference: ${reference.path}\n---\n\n${reference.content.trimEnd()}\n`);
63
+ }
64
+ }
65
+ return loaded;
66
+ }
@@ -50,7 +50,7 @@ export async function runStartLocal(opts = {}) {
50
50
  Project services:
51
51
  npm run dev → local Agent Sam API http://127.0.0.1:${config?.dev_port ?? 8787}
52
52
  npm run db:status → inspect local SQLite
53
- npm run tui → ANSI Agent Sam dashboard
53
+ npx agentsam → enter Agent Sam
54
54
 
55
55
  Press Ctrl+C to stop.
56
56
  `);
@@ -7,6 +7,7 @@
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
8
  import { authenticateViaBrowser } from '../lib/auth.js';
9
9
  import { postJson } from '../lib/core-client.js';
10
+ import { resolveSdkKey } from '../../packages/identity/src/contracts/auth-config.js';
10
11
 
11
12
  const DEFAULT_PORT = 3099;
12
13
 
@@ -32,7 +33,7 @@ function parseArgs(argv) {
32
33
  else if (a === '--platform') opts.platform = argv[++i] || opts.platform;
33
34
  else if (a === '--shell') opts.shell = argv[++i] || opts.shell;
34
35
  else if (a === '--token' && argv[i + 1]) {
35
- process.env.AGENTSAM_SDK_TOKEN = argv[++i];
36
+ process.env.AGENTSAM_SDK_KEY = argv[++i];
36
37
  }
37
38
  }
38
39
  return opts;
@@ -50,12 +51,12 @@ function ensureCloudflared() {
50
51
  }
51
52
 
52
53
  async function resolveToken() {
53
- const existing = String(process.env.AGENTSAM_SDK_TOKEN || '').trim();
54
+ const existing = resolveSdkKey(process.env);
54
55
  if (existing.startsWith('sdk_')) return existing;
55
56
  const session = await authenticateViaBrowser();
56
57
  const tok = String(session?.access_token || '').trim();
57
- if (!tok.startsWith('sdk_')) throw new Error('IAM auth did not return an sdk_ bearer token');
58
- process.env.AGENTSAM_SDK_TOKEN = tok;
58
+ if (!tok.startsWith('sdk_')) throw new Error('IAM auth did not return an sdk_ bearer key');
59
+ process.env.AGENTSAM_SDK_KEY = tok;
59
60
  return tok;
60
61
  }
61
62
 
@@ -0,0 +1,58 @@
1
+ export const DEFAULT_CONTEXT_RATIOS = Object.freeze({
2
+ targetInputRatio: 0.60,
3
+ hardInputRatio: 0.85,
4
+ charsPerToken: 4,
5
+ });
6
+
7
+ function ratio(value, label) {
8
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) throw new RangeError(`${label} must be > 0 and <= 1`);
9
+ return value;
10
+ }
11
+
12
+ function positiveNumber(value, label) {
13
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) throw new RangeError(`${label} must be positive`);
14
+ return value;
15
+ }
16
+
17
+ function positiveInteger(value, label) {
18
+ if (!Number.isInteger(value) || value < 1) throw new RangeError(`${label} must be a positive integer`);
19
+ return value;
20
+ }
21
+
22
+ export function createContextBudget(options = {}) {
23
+ const windowTokens = positiveInteger(options.windowTokens, 'windowTokens');
24
+ const charsPerToken = positiveNumber(options.charsPerToken ?? DEFAULT_CONTEXT_RATIOS.charsPerToken, 'charsPerToken');
25
+ const targetInputRatio = ratio(options.targetInputRatio ?? DEFAULT_CONTEXT_RATIOS.targetInputRatio, 'targetInputRatio');
26
+ const hardInputRatio = ratio(options.hardInputRatio ?? DEFAULT_CONTEXT_RATIOS.hardInputRatio, 'hardInputRatio');
27
+ if (targetInputRatio >= hardInputRatio) throw new RangeError('targetInputRatio must be lower than hardInputRatio');
28
+
29
+ const windowChars = Math.floor(windowTokens * charsPerToken);
30
+ const targetInputTokens = Math.floor(windowTokens * targetInputRatio);
31
+ const hardInputTokens = Math.floor(windowTokens * hardInputRatio);
32
+ const targetInputChars = Math.floor(windowChars * targetInputRatio);
33
+ const hardInputChars = Math.floor(windowChars * hardInputRatio);
34
+
35
+ return Object.freeze({
36
+ windowTokens,
37
+ windowChars,
38
+ charsPerToken,
39
+ targetInputRatio,
40
+ hardInputRatio,
41
+ targetInputTokens,
42
+ hardInputTokens,
43
+ targetInputChars,
44
+ hardInputChars,
45
+ maxSystemChars: positiveInteger(options.maxSystemChars ?? Math.max(4_000, Math.min(60_000, Math.floor(windowChars * 0.05))), 'maxSystemChars'),
46
+ maxToolSchemaChars: positiveInteger(options.maxToolSchemaChars ?? Math.max(4_000, Math.min(40_000, Math.floor(windowChars * 0.04))), 'maxToolSchemaChars'),
47
+ maxEvidenceChars: positiveInteger(options.maxEvidenceChars ?? Math.max(8_000, Math.min(100_000, Math.floor(targetInputChars * 0.25))), 'maxEvidenceChars'),
48
+ maxFileCharsPerRead: positiveInteger(options.maxFileCharsPerRead ?? 32_768, 'maxFileCharsPerRead'),
49
+ maxFileCharsPerTurn: positiveInteger(options.maxFileCharsPerTurn ?? Math.max(32_768, Math.min(131_072, Math.floor(targetInputChars * 0.25))), 'maxFileCharsPerTurn'),
50
+ maxToolResultChars: positiveInteger(options.maxToolResultChars ?? 24_000, 'maxToolResultChars'),
51
+ });
52
+ }
53
+
54
+ export function estimateContextTokens(chars, charsPerToken = DEFAULT_CONTEXT_RATIOS.charsPerToken) {
55
+ if (!Number.isFinite(chars) || chars < 0) throw new RangeError('chars must be a non-negative number');
56
+ if (!Number.isFinite(charsPerToken) || charsPerToken <= 0) throw new RangeError('charsPerToken must be positive');
57
+ return Math.ceil(chars / charsPerToken);
58
+ }
@@ -0,0 +1,28 @@
1
+ import { normalizeContextItem } from './resolve.js';
2
+
3
+ export const DEFAULT_CONSUMED_CONTEXT_CHARS = 4_000;
4
+
5
+ export function compactContextItem(value, options = {}) {
6
+ const item = normalizeContextItem(value);
7
+ const maxChars = Number(options.maxChars ?? DEFAULT_CONSUMED_CONTEXT_CHARS);
8
+ if (!Number.isInteger(maxChars) || maxChars < 1) throw new RangeError('compact maxChars must be a positive integer');
9
+ if (item.content.length <= maxChars) return Object.freeze({ ...item, compacted: false });
10
+ const content = `${item.content.slice(0, Math.max(0, maxChars - 1))}…`;
11
+ return Object.freeze({
12
+ ...item,
13
+ content,
14
+ chars: content.length,
15
+ compacted: true,
16
+ source_chars: item.content.length,
17
+ });
18
+ }
19
+
20
+ export function compactConsumedToolResult(value, options = {}) {
21
+ const ref = String(options.ref || value?.ref || 'tool_result:consumed').trim();
22
+ const hash = options.hash || value?.hash;
23
+ const priority = Number.isFinite(options.priority) ? options.priority : Number(value?.priority || 0);
24
+ const content = typeof value === 'string'
25
+ ? value
26
+ : String(value?.content ?? JSON.stringify(value?.content ?? value ?? null));
27
+ return compactContextItem({ ref, kind: 'tool_result', hash, priority, content }, options);
28
+ }
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_CONTEXT_RATIOS, createContextBudget, estimateContextTokens } from './budget.js';
2
+ export { DEFAULT_RESULT_POLICY, RESULT_DETAIL_ORDER, normalizeResultPolicy, truncateResultText, boundResultItems } from './result-policy.js';
3
+ export { CONTEXT_ITEM_KINDS, normalizeContextItem, resolveContext, resolveProjectContext } from './resolve.js';
4
+ export { PROJECT_RULES_FILENAME, DEFAULT_PROJECT_RULES_MAX_CHARS, defaultProjectRules, ensureProjectRules, findProjectRules, loadProjectRules } from '../lib/project-rules.js';
5
+ export { DEFAULT_CONSUMED_CONTEXT_CHARS, compactContextItem, compactConsumedToolResult } from './compact.js';