@inneranimalmedia/agentsam-sdk 2.6.3 → 2.6.4

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 +6 -0
  2. package/README.md +16 -1
  3. package/bin/agentsam +16 -1
  4. package/docs/BRAND_INTELLIGENCE.md +1 -1
  5. package/docs/architecture/AGENTSAM_DISTRIBUTION_OWNERSHIP.md +176 -0
  6. package/docs/architecture/AGENTSAM_GO_RUNTIME.md +282 -0
  7. package/docs/architecture/CODEBASEINDEX_GUIDED_PATH.md +202 -0
  8. package/docs/architecture/FS_E2E_CLOSURE_RECEIPT.md +59 -0
  9. package/docs/architecture/LOCAL_FS_AUTHORITY.md +38 -0
  10. package/docs/architecture/LOCAL_STUDIO_CLOUDFLARE_OAUTH.md +28 -0
  11. package/docs/architecture/PLAN_CLI_AND_LOCAL_STUDIO_DESKTOP.md +96 -0
  12. package/docs/architecture/SAM_ACTIVITY_RECOVERY_RECEIPT.md +39 -0
  13. package/docs/architecture/SAM_DECISION_WORK_RECEIPT.md +54 -0
  14. package/docs/architecture/SAM_KERNEL.md +181 -0
  15. package/docs/architecture/SAM_MACHINE_NORMALIZATION_PRECOMMIT_REPORT.md +208 -0
  16. package/docs/architecture/SLASH_SKILLS_PORTABLE.md +116 -0
  17. package/docs/architecture/fs-e2e-receipt.latest.json +42 -0
  18. package/docs/architecture/previews/codebaseindex-guided-path/CODEBASEINDEX_GUIDED_PATH.md +202 -0
  19. package/docs/architecture/previews/codebaseindex-guided-path/index.html +321 -0
  20. package/migrations/d1/0010_portable_tickets_memory.sql +140 -0
  21. package/migrations/d1/0011_agentsam_skill_v2.sql +185 -0
  22. package/migrations/d1/0011b_agentsam_skill_v2_cutover.sql +22 -0
  23. package/migrations/d1/0011c_agentsam_skill_v2_backfill.sql +76 -0
  24. package/migrations/d1/0011d_agentsam_skill_v2_retrieval_revisions.sql +50 -0
  25. package/migrations/d1/0012_agentsam_tools_required_seed.sql +67 -0
  26. package/migrations/d1/0013_identity_oauth_states.sql +15 -0
  27. package/migrations/d1/0014_auth_event_log.sql +20 -0
  28. package/migrations/d1/0015_identity_oauth_state_app_id.sql +3 -0
  29. package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +16 -0
  30. package/migrations/sqlite/agentsam_skill_retrieval.portable.sql +42 -0
  31. package/package.json +19 -4
  32. package/packages/agentsam-contracts/src/errors.ts +16 -0
  33. package/packages/agentsam-errors/src/envelope.js +53 -0
  34. package/packages/agentsam-errors/src/index.js +1 -0
  35. package/packages/agentsam-errors/src/recovery.js +301 -0
  36. package/packages/agentsam-knowledge/src/providers/index.js +18 -6
  37. package/packages/connectors/cloudflare/src/routes.js +9 -0
  38. package/packages/connectors/cloudflare/tests/connector.test.mjs +26 -1
  39. package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
  40. package/packages/identity/.agentsam/features/oauth-login-portal/routes.json +11 -2
  41. package/packages/identity/docs/PORTABLE_IDENTITY_ARCHITECTURE.md +50 -0
  42. package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +31 -0
  43. package/packages/identity/migrations/sqlite/001_identity_core.sql +99 -0
  44. package/packages/identity/migrations/sqlite/002_identity_oauth_client.sql +38 -0
  45. package/packages/identity/migrations/sqlite/003_identity_oauth_server.sql +64 -0
  46. package/packages/identity/package.json +2 -2
  47. package/packages/identity/src/adapters/cloudflare-d1/index.js +122 -22
  48. package/packages/identity/src/adapters/sqlite/index.js +319 -0
  49. package/packages/identity/src/app/verify-app.js +95 -0
  50. package/packages/identity/src/contracts/identity-store.js +115 -0
  51. package/packages/identity/src/contracts/route-ids.js +30 -0
  52. package/packages/identity/src/contracts/route-projection.js +218 -0
  53. package/packages/identity/src/contracts/routes.js +11 -0
  54. package/packages/identity/src/core/browser-paths.js +4 -5
  55. package/packages/identity/src/core/constants.js +13 -8
  56. package/packages/identity/src/core/session-policy.js +32 -0
  57. package/packages/identity/src/frontend/auth-portal/pages/login.html +10 -10
  58. package/packages/identity/src/frontend/auth-portal/pages/reset.html +3 -3
  59. package/packages/identity/src/frontend/auth-portal/pages/signup.html +3 -3
  60. package/packages/identity/src/frontend/auth-portal/preview/dashboard-stub.html +1 -1
  61. package/packages/identity/src/index.js +25 -0
  62. package/packages/identity/src/oauth/README.md +10 -4
  63. package/packages/identity/src/oauth/credentials.js +20 -13
  64. package/packages/identity/src/oauth/finalize-inbound.js +1 -1
  65. package/packages/identity/src/oauth/iam-platform.js +8 -7
  66. package/packages/identity/src/oauth/redirect-paths.js +27 -36
  67. package/packages/identity/src/server/identity-service.js +30 -13
  68. package/packages/identity/src/server/mount-policy.js +30 -0
  69. package/packages/identity/src/server/post-auth.js +79 -0
  70. package/packages/identity/src/server/worker-router.js +104 -72
  71. package/packages/identity/tests/finalize-inbound-oauth.test.mjs +6 -6
  72. package/packages/identity/tests/iam-provider.test.mjs +1 -1
  73. package/packages/identity/tests/identity-service.test.mjs +36 -5
  74. package/packages/identity/tests/oauth-credentials.test.mjs +3 -1
  75. package/packages/identity/tests/portable-identity-architecture.test.mjs +157 -0
  76. package/packages/identity/tests/session-routes-policy.test.mjs +21 -0
  77. package/packages/theme-church-site/package.json +2 -1
  78. package/packages/theme-church-site/src/index.js +1 -0
  79. package/packages/theme-companions-site/package.json +2 -1
  80. package/packages/theme-companions-site/src/index.js +1 -0
  81. package/packages/theme-floors-site/package.json +2 -1
  82. package/packages/theme-floors-site/src/index.js +1 -0
  83. package/packages/theme-fuelnfree-site/package.json +2 -1
  84. package/packages/theme-fuelnfree-site/src/index.js +1 -0
  85. package/packages/theme-handyman-site/package.json +2 -1
  86. package/packages/theme-handyman-site/src/index.js +1 -0
  87. package/packages/theme-insurance-site/package.json +2 -1
  88. package/packages/theme-insurance-site/src/index.js +1 -0
  89. package/packages/theme-shinshu-site/package.json +2 -1
  90. package/packages/theme-shinshu-site/src/index.js +1 -0
  91. package/protocol/apps/agentsam.app.v1.schema.json +51 -0
  92. package/protocol/brand/brandpack.v1.schema.json +43 -0
  93. package/protocol/credentials/issue.v1.schema.json +38 -0
  94. package/protocol/database/connection.v1.schema.json +41 -0
  95. package/protocol/embeddings/embedding-profile.v1.schema.json +20 -0
  96. package/protocol/errors/error-envelope.schema.json +135 -1
  97. package/protocol/errors/recovery.v1.schema.json +50 -0
  98. package/protocol/runtime/workspace-fs.v1.schema.json +71 -0
  99. package/protocol/sam/activity.v1.schema.json +48 -0
  100. package/protocol/sam/answer.v1.schema.json +35 -0
  101. package/protocol/sam/calibration.v1.schema.json +21 -0
  102. package/protocol/sam/decision-receipt.v1.schema.json +29 -0
  103. package/protocol/sam/evaluation.v1.schema.json +19 -0
  104. package/protocol/sam/operation.schema.json +66 -0
  105. package/protocol/sam/outcome.v1.schema.json +36 -0
  106. package/protocol/sam/question.v1.schema.json +26 -0
  107. package/protocol/sam/registry.seed.json +153 -0
  108. package/protocol/sam/result.schema.json +53 -0
  109. package/protocol/sam/state.v1.schema.json +23 -0
  110. package/protocol/skills/agentsam.interaction.v1.schema.json +50 -0
  111. package/protocol/skills/agentsam.skill.v1.schema.json +57 -0
  112. package/protocol/ui/icon-registry.mjs +226 -0
  113. package/protocol/ui/icon.v1.schema.json +51 -0
  114. package/skills/README.md +22 -9
  115. package/skills/agentsam-codebaseindex/SKILL.md +225 -0
  116. package/skills/catalog.json +14 -0
  117. package/src/cli/command-catalog.js +130 -0
  118. package/src/cli/dispatch.js +48 -0
  119. package/src/cli.js +43 -1
  120. package/src/commands/api-key.js +244 -0
  121. package/src/commands/app.js +60 -28
  122. package/src/commands/brand.js +17 -19
  123. package/src/commands/codebaseindex.js +688 -0
  124. package/src/commands/env.js +152 -25
  125. package/src/commands/go.js +366 -53
  126. package/src/commands/interaction-clack.js +115 -0
  127. package/src/commands/models.js +1 -1
  128. package/src/commands/providers.js +62 -14
  129. package/src/commands/shell.js +43 -2
  130. package/src/commands/skill.js +248 -0
  131. package/src/commands/skills.js +1 -1
  132. package/src/commands/start-local.js +4 -0
  133. package/src/commands/whoami.js +90 -18
  134. package/src/go/build.js +229 -39
  135. package/src/go/cloudflare.js +506 -105
  136. package/src/go/container.js +120 -0
  137. package/src/go/contract.js +9 -4
  138. package/src/go/discover.js +149 -33
  139. package/src/go/index.js +15 -3
  140. package/src/go/native-probe-runner.mjs +119 -0
  141. package/src/go/{registry.js → official-registry.js} +64 -7
  142. package/src/go/official-release.js +10 -0
  143. package/src/go/receipts.js +77 -10
  144. package/src/go/verify.js +9 -2
  145. package/src/index.js +27 -0
  146. package/src/indexing/ingest/discover-models.js +298 -0
  147. package/src/indexing/ingest/inventory.js +243 -0
  148. package/src/indexing/ingest/job-graph.js +181 -0
  149. package/src/indexing/ingest/materials.js +210 -0
  150. package/src/lib/provider-credentials.js +63 -21
  151. package/src/lib/slash-commands.js +1 -0
  152. package/src/local-fs/capability.js +121 -0
  153. package/src/local-fs/freshness.js +75 -0
  154. package/src/local-fs/index.js +385 -0
  155. package/src/local-fs/paths.js +100 -0
  156. package/src/local-pty/server.js +295 -19
  157. package/src/mcp/client.js +2 -2
  158. package/src/models/ai-access-onboarding.js +112 -0
  159. package/src/models/discovery.js +10 -2
  160. package/src/models/inventory-core.js +9 -1
  161. package/src/sam/activity/index.js +183 -0
  162. package/src/sam/client.js +252 -0
  163. package/src/sam/decision/calibration.js +109 -0
  164. package/src/sam/decision/confidence.js +126 -0
  165. package/src/sam/decision/evaluate.js +157 -0
  166. package/src/sam/decision/evaluators/deterministic.js +341 -0
  167. package/src/sam/decision/evaluators/heuristic.js +61 -0
  168. package/src/sam/decision/evaluators/select.js +50 -0
  169. package/src/sam/decision/evaluators/semantic.js +149 -0
  170. package/src/sam/decision/hierarchical.js +61 -0
  171. package/src/sam/decision/index.js +53 -0
  172. package/src/sam/decision/policy.js +86 -0
  173. package/src/sam/decision/questions.js +120 -0
  174. package/src/sam/decision/receipt.js +148 -0
  175. package/src/sam/decision/state.js +117 -0
  176. package/src/sam/decision/types.js +22 -0
  177. package/src/sam/decision/validate.js +200 -0
  178. package/src/sam/define.js +51 -0
  179. package/src/sam/index.js +65 -0
  180. package/src/sam/operations/brand-scan.js +62 -0
  181. package/src/sam/operations/cad-blender-inspect.js +36 -0
  182. package/src/sam/operations/codebaseindex-ingest.js +49 -0
  183. package/src/sam/operations/decision-evaluate.js +59 -0
  184. package/src/sam/operations/planning-astar.js +77 -0
  185. package/src/sam/operations/planning-goap.js +60 -0
  186. package/src/sam/operations/repository-inspect.js +72 -0
  187. package/src/sam/operations/security-scan.js +33 -0
  188. package/src/sam/operations/terminal-exec.js +29 -0
  189. package/src/sam/planning/astar.js +311 -0
  190. package/src/sam/planning/goap.js +177 -0
  191. package/src/sam/planning/index.js +21 -0
  192. package/src/sam/planning/state.js +84 -0
  193. package/src/sam/registry.js +48 -0
  194. package/src/sam/result.js +77 -0
  195. package/src/sam/seed.js +44 -0
  196. package/src/sam/types.js +91 -0
  197. package/src/skills/catalog.js +64 -0
  198. package/src/skills/content-resolver.js +124 -0
  199. package/src/skills/hosted-store.js +37 -0
  200. package/src/skills/index.js +29 -64
  201. package/src/skills/interaction.js +102 -0
  202. package/src/skills/local-store.js +228 -0
  203. package/src/skills/manifest.js +104 -0
  204. package/src/skills/metrics.js +31 -0
  205. package/src/skills/registry.js +184 -0
  206. package/src/skills/runtime.js +287 -0
  207. package/src/skills/slash.js +44 -0
  208. package/src/ui/cli/help.js +94 -101
  209. package/test/cli/api-key-env-whoami.test.mjs +129 -0
  210. package/test/cli/codebaseindex-plan-ux.test.mjs +37 -0
  211. package/test/cli/go.test.mjs +62 -5
  212. package/test/cli/skill-npm-and-env.test.mjs +52 -0
  213. package/test/cli/wireframes-go-registry.test.mjs +87 -2
  214. package/test/go/build-source-identity.test.mjs +31 -0
  215. package/test/go/cloudflare-probe.test.mjs +274 -10
  216. package/test/go/distribution.test.mjs +28 -0
  217. package/test/integration/ai-access-onboarding.test.mjs +47 -0
  218. package/test/integration/cli-help.test.mjs +1 -1
  219. package/test/integration/cms-site-tenancy-contract.test.mjs +6 -6
  220. package/test/integration/codebaseindex-ingest.test.mjs +166 -0
  221. package/test/integration/icon-registry.test.mjs +67 -0
  222. package/test/integration/ingest-discover-models.test.mjs +30 -0
  223. package/test/integration/install-script.test.mjs +12 -9
  224. package/test/integration/local-fs.test.mjs +113 -0
  225. package/test/integration/provider-env-cli.test.mjs +2 -1
  226. package/test/integration/sam-activity-recovery.test.mjs +147 -0
  227. package/test/integration/sam-decision.test.mjs +584 -0
  228. package/test/integration/sam-kernel.test.mjs +99 -0
  229. package/test/integration/sam-planning-astar.test.mjs +279 -0
  230. package/test/integration/skill-runtime.test.mjs +240 -0
  231. package/test/integration/studio-fs-pty-e2e.test.mjs +294 -0
  232. package/test/models.test.mjs +14 -7
  233. package/test/shell.test.mjs +4 -4
@@ -0,0 +1,166 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it, before } from 'node:test';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import {
8
+ getCliCommand,
9
+ listCliCommands,
10
+ printAssistTip,
11
+ suggestCliCommands,
12
+ } from '../../src/cli/command-catalog.js';
13
+ import { parsePastedPaths, classifyMaterial, stageMaterials } from '../../src/indexing/ingest/materials.js';
14
+ import { ensureSeedOperations, getSamOperation, AgentSamClient } from '../../src/sam/index.js';
15
+ import { getSkill, loadSkill } from '../../src/skills/index.js';
16
+ import { resolveHelpTopic, renderHelpOverview } from '../../src/ui/cli/help.js';
17
+ import { runCodebaseindexIngest } from '../../src/commands/codebaseindex.js';
18
+
19
+ describe('CLI command catalog assist', () => {
20
+ it('maps ingest alias to codebaseindex with skill tip', () => {
21
+ const entry = getCliCommand('ingest');
22
+ assert.equal(entry?.id, 'codebaseindex');
23
+ assert.equal(entry?.skill, 'agentsam-codebaseindex');
24
+ assert.equal(entry?.operation, 'codebaseindex.ingest');
25
+ });
26
+
27
+ it('prints tip: use skill for every catalogued command', () => {
28
+ const chunks = [];
29
+ for (const entry of listCliCommands()) {
30
+ if (!entry.skill) continue;
31
+ printAssistTip(entry, { write: (s) => chunks.push(s) });
32
+ }
33
+ const text = chunks.join('');
34
+ assert.match(text, /tip: use skill agentsam-codebaseindex/);
35
+ assert.match(text, /tip: use skill agentsam-app-fundamentals/);
36
+ });
37
+
38
+ it('suggests commands for typos', () => {
39
+ const hits = suggestCliCommands('codebase');
40
+ assert.ok(hits.some((h) => h.id === 'codebaseindex'));
41
+ });
42
+ });
43
+
44
+ describe('material paste/drop intake', () => {
45
+ it('parses pasted multi-path text', () => {
46
+ const paths = parsePastedPaths('/tmp/a.tar.gz\n"./site build"/index.html screenshot.png');
47
+ assert.ok(paths.includes('/tmp/a.tar.gz'));
48
+ assert.ok(paths.some((p) => p.includes('index.html') || p.endsWith('screenshot.png')));
49
+ });
50
+
51
+ it('stages html and classifies archives', () => {
52
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-ingest-'));
53
+ const html = path.join(root, 'page.html');
54
+ fs.writeFileSync(html, '<html><body>hi</body></html>');
55
+ const classified = classifyMaterial(html);
56
+ assert.equal(classified.kind, 'document');
57
+
58
+ const tarPath = path.join(root, 'site.tar');
59
+ const bundle = path.join(root, 'bundle');
60
+ fs.mkdirSync(bundle);
61
+ fs.writeFileSync(path.join(bundle, 'index.html'), '<html>build</html>');
62
+ execFileSync('tar', ['-cf', tarPath, '-C', bundle, '.']);
63
+
64
+ const staged = stageMaterials({ root, materials: [html, tarPath] });
65
+ assert.equal(staged.schema, 'agentsam.ingest.materials.v1');
66
+ assert.ok(staged.include.length >= 1);
67
+ assert.ok(staged.assets.some((a) => a.kind === 'document') || staged.items.some((i) => i.kind === 'archive'));
68
+ assert.ok(fs.existsSync(path.join(root, staged.stage_root, 'manifest.json')));
69
+ });
70
+ });
71
+
72
+ describe('codebaseindex SAM primitive', () => {
73
+ before(() => ensureSeedOperations());
74
+
75
+ it('registers codebaseindex.ingest', () => {
76
+ const op = getSamOperation('codebaseindex.ingest');
77
+ assert.ok(op);
78
+ assert.equal(op.execution.model, 'optional');
79
+ assert.deepEqual(op.cli.command[0], ['codebaseindex']);
80
+ });
81
+
82
+ it('skill agentsam-codebaseindex loads', () => {
83
+ assert.ok(getSkill('ingest'));
84
+ const loaded = loadSkill('agentsam-codebaseindex');
85
+ assert.match(loaded.instructions, /codebaseindex\.ingest/);
86
+ assert.match(loaded.instructions, /sam\.codebaseindex\.index\.run/);
87
+ });
88
+
89
+ it('describe + discover surface ingest', async () => {
90
+ const sam = new AgentSamClient();
91
+ const info = await sam.describe('codebaseindex.ingest');
92
+ assert.equal(info.ok, true);
93
+ const found = await sam.discover({ query: 'ingest archive embedding' });
94
+ assert.ok(found.operations.some((c) => c.id === 'codebaseindex.ingest'));
95
+ });
96
+
97
+ it('plan-only ingest via invoke (repo allowlist)', async () => {
98
+ const sam = new AgentSamClient();
99
+ const result = await sam.invoke('codebaseindex.ingest', {
100
+ root: process.cwd(),
101
+ include: ['src/sam'],
102
+ exclude: ['node_modules', '.git'],
103
+ embeddingChoice: 'none',
104
+ planOnly: true,
105
+ });
106
+ assert.equal(result.ok, true);
107
+ assert.equal(result.data?.pipeline, 'sam.codebaseindex.index.run');
108
+ assert.equal(result.data?.mode, 'plan');
109
+ });
110
+ });
111
+
112
+ describe('codebaseindex real execution', () => {
113
+ it('publishes, smoke-searches, completes the graph, and returns portable paths', async () => {
114
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-codebaseindex-run-'));
115
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
116
+ fs.writeFileSync(
117
+ path.join(root, 'src', 'demo.js'),
118
+ 'export function hello() { return "hello AgentSam"; }\n',
119
+ );
120
+
121
+ const result = await runCodebaseindexIngest({
122
+ root,
123
+ include: ['src'],
124
+ exclude: [],
125
+ embeddingChoice: 'none',
126
+ embed: false,
127
+ planOnly: false,
128
+ });
129
+
130
+ assert.equal(result.mode, 'run');
131
+ assert.equal(result.root, '.');
132
+ assert.equal(result.inventory.root, '.');
133
+
134
+ assert.equal(result.index.published, true);
135
+ assert.equal(result.job_graph.status, 'completed');
136
+ assert.equal(result.job_graph.current, null);
137
+ assert.equal(
138
+ result.job_graph.nodes.find((node) => node.id === 'embedding.generate')?.status,
139
+ 'skipped',
140
+ );
141
+ assert.equal(
142
+ result.job_graph.nodes.find((node) => node.id === 'search.smoke')?.status,
143
+ 'done',
144
+ );
145
+
146
+ assert.equal(
147
+ result.search_smoke.generation_id,
148
+ result.index.generation_id,
149
+ );
150
+ assert.ok(result.search_smoke.sources_included >= 1);
151
+ });
152
+ });
153
+
154
+ describe('catalog-driven help', () => {
155
+ it('resolves ingest topic from catalog', () => {
156
+ const topic = resolveHelpTopic('ingest');
157
+ assert.ok(topic);
158
+ assert.ok(topic.rows.some(([cmd]) => cmd.includes('codebaseindex')));
159
+ });
160
+
161
+ it('overview mentions skill baseline', () => {
162
+ const text = renderHelpOverview('2.6.3');
163
+ assert.match(text, /tip: use skill/);
164
+ assert.match(text, /command catalog/i);
165
+ });
166
+ });
@@ -0,0 +1,67 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import {
4
+ AGENTSAM_ICON_KEYS,
5
+ normalizeIconKey,
6
+ isAgentsamIconKey,
7
+ resolveCliIconGlyph,
8
+ createIconRenderer,
9
+ ICON_ALIASES,
10
+ } from '../../protocol/ui/icon-registry.mjs';
11
+ import iconSchema from '../../protocol/ui/icon.v1.schema.json' with { type: 'json' };
12
+
13
+ describe('agentsam.icon.v1', () => {
14
+ it('schema enum matches registry keys', () => {
15
+ assert.deepEqual([...(iconSchema.enum || [])].sort(), [...AGENTSAM_ICON_KEYS].sort());
16
+ });
17
+
18
+ it('includes generic fallback and first-class AgentSam concepts', () => {
19
+ for (const key of [
20
+ 'generic',
21
+ 'database',
22
+ 'vector',
23
+ 'identity',
24
+ 'key',
25
+ 'brand',
26
+ 'theme',
27
+ 'app',
28
+ 'repository',
29
+ 'connection',
30
+ 'metrics',
31
+ ]) {
32
+ assert.equal(isAgentsamIconKey(key), true, key);
33
+ }
34
+ });
35
+
36
+ it('normalizes aliases and rejects lucide/emoji-like values to generic', () => {
37
+ assert.equal(normalizeIconKey('db'), 'database');
38
+ assert.equal(normalizeIconKey('repo'), 'repository');
39
+ assert.equal(normalizeIconKey('lucide-database'), 'database');
40
+ assert.equal(normalizeIconKey('🗄️'), 'generic');
41
+ assert.equal(normalizeIconKey(''), 'generic');
42
+ assert.equal(normalizeIconKey('unknown-future-key'), 'generic');
43
+ });
44
+
45
+ it('createIconRenderer falls back to generic without throwing', () => {
46
+ const render = createIconRenderer(
47
+ { database: 'DB', generic: 'G', identity: 'ID' },
48
+ 'FALLBACK',
49
+ );
50
+ assert.equal(render('database'), 'DB');
51
+ assert.equal(render('db'), 'DB');
52
+ assert.equal(render('not-a-real-icon'), 'G');
53
+ assert.equal(render('identity'), 'ID');
54
+ });
55
+
56
+ it('CLI glyphs always resolve', () => {
57
+ assert.ok(resolveCliIconGlyph('database'));
58
+ assert.equal(resolveCliIconGlyph('totally-unknown'), resolveCliIconGlyph('generic'));
59
+ });
60
+
61
+ it('aliases only map to canonical vocabulary', () => {
62
+ for (const [from, to] of Object.entries(ICON_ALIASES)) {
63
+ assert.equal(isAgentsamIconKey(to), true, `${from} → ${to}`);
64
+ assert.equal(normalizeIconKey(from), to);
65
+ }
66
+ });
67
+ });
@@ -0,0 +1,30 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import {
4
+ encodeEmbeddingChoice,
5
+ parseEmbeddingChoice,
6
+ looksLikeEmbedModel,
7
+ } from '../../src/indexing/ingest/discover-models.js';
8
+
9
+ describe('ingest model discovery helpers', () => {
10
+ it('encodes/decodes embedding choices without breaking model ids that contain colons', () => {
11
+ const value = encodeEmbeddingChoice('ollama', 'mxbai-embed-large:latest', 1024);
12
+ assert.equal(value, 'ollama|mxbai-embed-large:latest|1024');
13
+ const parsed = parseEmbeddingChoice(value);
14
+ assert.equal(parsed.provider, 'ollama');
15
+ assert.equal(parsed.model, 'mxbai-embed-large:latest');
16
+ assert.equal(parsed.dimensions, 1024);
17
+ });
18
+
19
+ it('treats none as disabled embedding', () => {
20
+ const parsed = parseEmbeddingChoice('none');
21
+ assert.equal(parsed.provider, 'none');
22
+ assert.equal(parsed.dimensions, 0);
23
+ });
24
+
25
+ it('classifies ollama embed vs chat tags', () => {
26
+ assert.equal(looksLikeEmbedModel('mxbai-embed-large:latest'), true);
27
+ assert.equal(looksLikeEmbedModel('nomic-embed-text'), true);
28
+ assert.equal(looksLikeEmbedModel('qwen2.5-coder:latest'), false);
29
+ });
30
+ });
@@ -15,7 +15,9 @@ test('install.sh is a bash npm bootstrap with platform detection', () => {
15
15
  assert.match(source, /npm install --global/);
16
16
  assert.match(source, /Darwin-arm64/);
17
17
  assert.match(source, /--version/);
18
+ assert.match(source, /--app-id/);
18
19
  assert.match(source, /--app/);
20
+ assert.doesNotMatch(source, /APP_SELECTOR="\$\{AGENTSAM_DEFAULT_APP/);
19
21
  assert.match(source, /standalone_ready/);
20
22
  assert.match(source, /checksum_contract/);
21
23
  });
@@ -25,11 +27,12 @@ test('install.sh passes bash syntax validation', () => {
25
27
  assert.equal(result.status, 0, result.stderr);
26
28
  });
27
29
 
28
- test('install.sh maps --app aliases to executable launchers', () => {
30
+ test('install.sh maps --app-id and legacy --app aliases to executable launchers', () => {
29
31
  const cases = [
30
32
  ['cad', 'cad-creator', 'agentsam-cad-creator'],
31
33
  ['cms', 'client-cms-editor', 'agentsam-cms'],
32
34
  ['studio', 'local-studio', 'agentsam-studio'],
35
+ ['database-editor', 'database-editor', 'agentsam-database-editor'],
33
36
  ];
34
37
 
35
38
  for (const [alias, appId, executable] of cases) {
@@ -57,7 +60,8 @@ test('install.sh maps --app aliases to executable launchers', () => {
57
60
  );
58
61
 
59
62
  try {
60
- const result = spawnSync('bash', [installScript, '--app', alias, '--prefix', binDir], {
63
+ const flag = alias === 'database-editor' ? '--app-id' : '--app';
64
+ const result = spawnSync('bash', [installScript, flag, alias, '--prefix', binDir], {
61
65
  encoding: 'utf8',
62
66
  env: {
63
67
  ...process.env,
@@ -74,6 +78,7 @@ test('install.sh maps --app aliases to executable launchers', () => {
74
78
  assert.ok((fs.statSync(launcherPath).mode & 0o111) !== 0);
75
79
 
76
80
  const receipt = JSON.parse(fs.readFileSync(path.join(installRoot, 'install-receipt.json'), 'utf8'));
81
+ assert.equal(receipt.app_id, appId);
77
82
  assert.equal(receipt.app, appId);
78
83
  } finally {
79
84
  fs.rmSync(temp, { recursive: true, force: true });
@@ -81,13 +86,13 @@ test('install.sh maps --app aliases to executable launchers', () => {
81
86
  }
82
87
  });
83
88
 
84
- test('install.sh rejects unknown --app values before installation', () => {
85
- const result = spawnSync('bash', [installScript, '--app', 'unknown'], { encoding: 'utf8' });
89
+ test('install.sh rejects unknown --app-id values before installation', () => {
90
+ const result = spawnSync('bash', [installScript, '--app-id', 'unknown'], { encoding: 'utf8' });
86
91
  assert.equal(result.status, 2);
87
- assert.match(result.stderr, /expected cad, cms, or studio/);
92
+ assert.match(result.stderr, /unknown app/);
88
93
  });
89
94
 
90
- test('Local Studio Worker serves standalone installer routes', () => {
95
+ test('Local Studio Worker serves installer without AGENTSAM_DEFAULT_APP mutation', () => {
91
96
  const worker = fs.readFileSync(
92
97
  path.join(root, 'apps/local-studio/backend/worker/index.js'),
93
98
  'utf8',
@@ -98,9 +103,7 @@ test('Local Studio Worker serves standalone installer routes', () => {
98
103
  );
99
104
 
100
105
  assert.match(worker, /import installScript from "\.\.\/\.\.\/\.\.\/\.\.\/scripts\/install\.sh"/);
101
- assert.match(worker, /"\/install\/cad": "cad-creator"/);
102
- assert.match(worker, /"\/install\/cms": "client-cms-editor"/);
103
- assert.match(worker, /"\/install\/studio": "local-studio"/);
106
+ assert.doesNotMatch(worker, /APP_SELECTOR="\\\$\{AGENTSAM_DEFAULT_APP/);
104
107
  assert.match(worker, /text\/x-shellscript/);
105
108
  assert.match(wrangler, /"type": "Text"/);
106
109
  assert.match(wrangler, /"\*\*\/\*\.sh"/);
@@ -0,0 +1,113 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { describe, it, before, after } from 'node:test';
5
+ import {
6
+ createLocalFilesystem,
7
+ createTempWorkspaceRoot,
8
+ LOCAL_FS_ENGINE,
9
+ } from '../../src/local-fs/index.js';
10
+ import { resolveContainedPath } from '../../src/local-fs/paths.js';
11
+
12
+ describe('local-fs path containment', () => {
13
+ /** @type {string} */
14
+ let root;
15
+ before(() => {
16
+ root = createTempWorkspaceRoot();
17
+ fs.writeFileSync(path.join(root, 'ok.txt'), 'hello\n');
18
+ fs.mkdirSync(path.join(root, 'sub'));
19
+ });
20
+ after(() => {
21
+ fs.rmSync(root, { recursive: true, force: true });
22
+ });
23
+
24
+ it('resolves relative paths inside root', () => {
25
+ const r = resolveContainedPath(root, 'ok.txt');
26
+ assert.equal(r.ok, true);
27
+ assert.equal(r.rel, 'ok.txt');
28
+ });
29
+
30
+ it('rejects traversal', () => {
31
+ const r = resolveContainedPath(root, '../outside.txt');
32
+ assert.equal(r.ok, false);
33
+ assert.equal(r.code, 'path_escape');
34
+ });
35
+
36
+ it('rejects NUL', () => {
37
+ const r = resolveContainedPath(root, 'a\0b.txt');
38
+ assert.equal(r.ok, false);
39
+ assert.equal(r.code, 'invalid_path');
40
+ });
41
+
42
+ it('rejects symlink escape when possible', () => {
43
+ const outside = createTempWorkspaceRoot('agentsam-fs-out-');
44
+ try {
45
+ fs.writeFileSync(path.join(outside, 'secret.txt'), 'nope');
46
+ const link = path.join(root, 'escape-link');
47
+ try {
48
+ fs.symlinkSync(path.join(outside, 'secret.txt'), link);
49
+ } catch {
50
+ // Windows may lack symlink privilege — skip
51
+ return;
52
+ }
53
+ const r = resolveContainedPath(root, 'escape-link');
54
+ assert.equal(r.ok, false);
55
+ assert.equal(r.code, 'path_escape');
56
+ } finally {
57
+ fs.rmSync(outside, { recursive: true, force: true });
58
+ }
59
+ });
60
+ });
61
+
62
+ describe('local-fs operations', () => {
63
+ /** @type {string} */
64
+ let root;
65
+ /** @type {ReturnType<typeof createLocalFilesystem>} */
66
+ let fsApi;
67
+
68
+ before(() => {
69
+ root = createTempWorkspaceRoot();
70
+ fsApi = createLocalFilesystem(root);
71
+ });
72
+ after(() => {
73
+ fs.rmSync(root, { recursive: true, force: true });
74
+ });
75
+
76
+ it('create / read / write with version conflict detection', () => {
77
+ assert.equal(fsApi.engine, LOCAL_FS_ENGINE);
78
+ const created = fsApi.create('README.md', '# one\n');
79
+ assert.equal(created.ok, true);
80
+ const v1 = created.version;
81
+
82
+ const read1 = fsApi.read('README.md');
83
+ assert.equal(read1.ok, true);
84
+ assert.equal(read1.content, '# one\n');
85
+ assert.equal(read1.version, v1);
86
+
87
+ const conflict = fsApi.write('README.md', '# stale\n', { expectedVersion: 'sha256:dead' });
88
+ assert.equal(conflict.ok, false);
89
+ assert.equal(conflict.code, 'version_conflict');
90
+
91
+ const wrote = fsApi.write('README.md', '# two\n', { expectedVersion: v1 });
92
+ assert.equal(wrote.ok, true);
93
+ assert.notEqual(wrote.version, v1);
94
+ assert.equal(fs.readFileSync(path.join(root, 'README.md'), 'utf8'), '# two\n');
95
+ });
96
+
97
+ it('list / rename / remove', () => {
98
+ fsApi.create('a.txt', 'a');
99
+ fsApi.mkdir('dir');
100
+ const listed = fsApi.list('.');
101
+ assert.equal(listed.ok, true);
102
+ assert.ok(listed.entries.some((e) => e.path === 'a.txt'));
103
+
104
+ const renamed = fsApi.rename('a.txt', 'b.txt');
105
+ assert.equal(renamed.ok, true);
106
+ assert.ok(fs.existsSync(path.join(root, 'b.txt')));
107
+ assert.equal(fs.existsSync(path.join(root, 'a.txt')), false);
108
+
109
+ const removed = fsApi.remove('b.txt');
110
+ assert.equal(removed.ok, true);
111
+ assert.equal(fs.existsSync(path.join(root, 'b.txt')), false);
112
+ });
113
+ });
@@ -13,7 +13,8 @@ test('agentsam env init creates provider profile and reusable source loader', {
13
13
  const env = { ...process.env, HOME: home };
14
14
  const result = spawnSync(process.execPath, ['src/cli.js', 'env', 'init', 'openai'], { cwd: repoRoot, env, encoding: 'utf8' });
15
15
  assert.equal(result.status, 0, result.stderr);
16
- assert.match(result.stdout, /source ~\/.agentsam\/load-agent-env\.sh openai/);
16
+ assert.match(result.stdout, /source ~\/.agentsam\/load-agent-env\.sh(?:\s|$)/);
17
+ assert.doesNotMatch(result.stdout, /source ~\/.agentsam\/load-agent-env\.sh openai/);
17
18
  assert.equal(fs.statSync(path.join(home, '.agentsam', 'env.d', 'openai.env')).mode & 0o777, 0o600);
18
19
  assert.equal(fs.statSync(path.join(home, '.agentsam', 'load-agent-env.sh')).mode & 0o777, 0o700);
19
20
  });
@@ -0,0 +1,147 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import {
4
+ evaluateDecisions as evaluate,
5
+ appendOutcome,
6
+ attachAction,
7
+ createActivityStore,
8
+ activityFromDecision,
9
+ DECISION_ACTIVITY_LABELS,
10
+ } from '../../src/sam/index.js';
11
+ import {
12
+ createErrorEnvelope,
13
+ planRecovery,
14
+ inferSideEffectState,
15
+ SIDE_EFFECT_STATE,
16
+ FAILURE_CLASS,
17
+ } from '../../packages/agentsam-errors/src/index.js';
18
+
19
+ describe('SAM decision → activity → outcome linkage', () => {
20
+ it('emits user-facing decision.made events without choose/score/check as primary label', async () => {
21
+ const activity = createActivityStore('run_test_1');
22
+ const seen = [];
23
+ activity.on((e) => seen.push(e));
24
+
25
+ const result = await evaluate({
26
+ state: {
27
+ constraints: { requires_local_files: true },
28
+ runtime: { terminal_lanes: { local: true, remote: true, sandbox: true } },
29
+ },
30
+ questions: ['terminal_lane', 'requires_approval'],
31
+ run_id: 'run_test_1',
32
+ step_id: 'step_plan',
33
+ activity,
34
+ });
35
+
36
+ assert.ok(result.receipt.run_id === 'run_test_1');
37
+ assert.ok(result.receipt.step_id === 'step_plan');
38
+ assert.ok(seen.some((e) => e.event === 'decision.made'));
39
+ const lane = seen.find((e) => e.evidence?.question_id === 'terminal_lane');
40
+ assert.equal(lane.label, DECISION_ACTIVITY_LABELS.terminal_lane);
41
+ assert.equal(lane.label.includes('choose'), false);
42
+ assert.ok(lane.evidence.decision_id);
43
+ assert.equal(lane.detail, 'local');
44
+ });
45
+
46
+ it('records supervised human corrections distinctly from weak passive accept', async () => {
47
+ const result = await evaluate({
48
+ state: { constraints: { requires_local_files: true }, runtime: { terminal_lanes: { local: true } } },
49
+ questions: ['terminal_lane'],
50
+ run_id: 'run_corr',
51
+ });
52
+ let receipt = attachAction(result.receipt, { kind: 'terminal.attach', summary: 'attach local pty' });
53
+ assert.ok(receipt.action_id);
54
+
55
+ receipt = appendOutcome(receipt, {
56
+ success: false,
57
+ disposition: 'corrected_to',
58
+ human_corrected: true,
59
+ corrected_to: 'sandbox',
60
+ user_override: true,
61
+ duration_ms: 1200,
62
+ retries: 0,
63
+ });
64
+ assert.equal(receipt.outcome.label_strength, 'supervised');
65
+ assert.equal(receipt.outcome.corrected_to, 'sandbox');
66
+ assert.ok(receipt.outcome_id);
67
+
68
+ const passive = appendOutcome(result.receipt, { success: true });
69
+ assert.equal(passive.outcome.label_strength, 'weak_passive');
70
+ });
71
+ });
72
+
73
+ describe('ErrorEnvelope recovery semantics', () => {
74
+ it('refuses blind retry when side effect is unknown', () => {
75
+ const error = createErrorEnvelope({
76
+ reason: 'transport_timeout',
77
+ message: 'deployment create timed out',
78
+ code: 'DEADLINE_EXCEEDED',
79
+ severity: 'transient',
80
+ retryable: true,
81
+ domain: 'deployment',
82
+ operation: {
83
+ kind: 'deployment.create',
84
+ idempotent: false,
85
+ read_only: false,
86
+ side_effect_state: SIDE_EFFECT_STATE.UNKNOWN,
87
+ },
88
+ });
89
+ assert.equal(inferSideEffectState(error), SIDE_EFFECT_STATE.UNKNOWN);
90
+ const plan = planRecovery(error);
91
+ assert.equal(plan.disposition, 'reconcile');
92
+ assert.ok(plan.evidence.some((e) => /reconcile_before_retry/.test(e)));
93
+ });
94
+
95
+ it('retries transport failures with budget when side effect is known not applied', () => {
96
+ const error = createErrorEnvelope({
97
+ reason: 'connection_reset',
98
+ message: 'reset',
99
+ code: 'UNAVAILABLE',
100
+ severity: 'transient',
101
+ retryable: true,
102
+ domain: 'transport',
103
+ operation: {
104
+ kind: 'fs.read',
105
+ read_only: true,
106
+ side_effect_state: SIDE_EFFECT_STATE.NONE,
107
+ },
108
+ });
109
+ const plan = planRecovery(error, { attempt: 1, max_attempts: 4 });
110
+ assert.ok(plan.disposition === 'retry' || plan.disposition === 'reconnect');
111
+ assert.ok(plan.delay_ms == null || plan.delay_ms >= 0);
112
+ });
113
+
114
+ it('treats cancellation as abort without operator incident', () => {
115
+ const error = createErrorEnvelope({
116
+ reason: 'cancelled_by_user',
117
+ message: 'Stopped',
118
+ code: 'CANCELLED',
119
+ severity: 'blocking_user_fixable',
120
+ retryable: false,
121
+ domain: 'runtime',
122
+ resolution_owner: 'user',
123
+ remediation: { action: 'none' },
124
+ });
125
+ // cancelled may not be in policy — use unknown path via custom
126
+ const plan = planRecovery({
127
+ ...error,
128
+ reason: 'cancelled_by_user',
129
+ failure_class: FAILURE_CLASS.CANCELLED,
130
+ fingerprint: error.fingerprint,
131
+ });
132
+ assert.equal(plan.disposition, 'abort');
133
+ assert.equal(plan.notify.operator, 'never');
134
+ });
135
+
136
+ it('activityFromDecision maps score to non-numeric user detail', () => {
137
+ const ev = activityFromDecision({
138
+ run_id: 'r1',
139
+ question_id: 'change_risk',
140
+ answer: { type: 'score', value: 1.7, question_id: 'change_risk' },
141
+ decision_id: 'dec_x',
142
+ });
143
+ assert.equal(ev.label, 'Assessing change risk');
144
+ assert.equal(ev.detail, 'Bounded');
145
+ assert.equal(ev.evidence.question_type, 'score');
146
+ });
147
+ });