@inneranimalmedia/agentsam-sdk 2.0.0 → 2.2.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 (61) hide show
  1. package/README.md +28 -15
  2. package/docs/CAPABILITIES.md +93 -0
  3. package/docs/DEPLOY_RECEIPTS.md +83 -0
  4. package/docs/RECON.md +165 -0
  5. package/docs/RELEASES.md +4 -3
  6. package/docs/portable-knowledge.md +4 -4
  7. package/docs/sdk-2.0-release.md +19 -21
  8. package/package.json +8 -1
  9. package/packages/identity/package.json +1 -1
  10. package/packages/identity/src/frontend/auth-portal/README.md +1 -1
  11. package/packages/identity/src/index.js +2 -0
  12. package/protocol/capabilities/capability-manifest.schema.json +36 -0
  13. package/protocol/capabilities/manifest.json +179 -0
  14. package/protocol/capabilities/repository-audit-input.schema.json +14 -0
  15. package/protocol/capabilities/repository-audit.schema.json +30 -0
  16. package/protocol/capabilities/repository-snapshot-input.schema.json +11 -0
  17. package/protocol/capabilities/repository-snapshot.schema.json +20 -0
  18. package/protocol/knowledge/chunk.schema.json +15 -73
  19. package/protocol/knowledge/document.schema.json +10 -48
  20. package/protocol/knowledge/index-config.schema.json +2 -2
  21. package/protocol/knowledge/repository.schema.json +11 -52
  22. package/protocol/knowledge/retrieval-query.schema.json +13 -63
  23. package/protocol/knowledge/source.schema.json +9 -43
  24. package/protocol/presets/catalog.json +48 -0
  25. package/protocol/recon/README.md +19 -0
  26. package/protocol/recon/finding-report.schema.json +46 -0
  27. package/protocol/recon/task-packet.schema.json +79 -0
  28. package/python/agentsam_sdk/knowledge/models.py +19 -7
  29. package/python/agentsam_sdk/repository/__main__.py +2 -2
  30. package/python/agentsam_sdk/repository/recon/__init__.py +28 -0
  31. package/python/agentsam_sdk/repository/recon/__main__.py +3 -0
  32. package/python/agentsam_sdk/repository/recon/cli.py +178 -0
  33. package/python/agentsam_sdk/repository/recon/packet.py +294 -0
  34. package/python/agentsam_sdk/repository/recon/validate.py +76 -0
  35. package/python/tests/test_knowledge_models.py +6 -2
  36. package/python/tests/test_recon.py +257 -0
  37. package/src/agent/capability-adapter.js +50 -0
  38. package/src/agent/index.js +2 -0
  39. package/src/agent/repository-audit.js +188 -0
  40. package/src/capabilities/index.js +7 -0
  41. package/src/capabilities/manifest.js +22 -0
  42. package/src/capabilities/repository-snapshot.js +180 -0
  43. package/src/cli.js +68 -8
  44. package/src/commands/deploy-receipt.js +129 -0
  45. package/src/commands/deploy.js +0 -1
  46. package/src/commands/knowledge.js +5 -6
  47. package/src/commands/product.js +119 -0
  48. package/src/commands/recon.js +71 -0
  49. package/src/index.js +17 -0
  50. package/src/knowledge/config.js +12 -6
  51. package/src/knowledge/contracts.js +1 -1
  52. package/src/knowledge/engine.js +1 -1
  53. package/src/knowledge/service/server.js +2 -2
  54. package/src/lib/deploy-receipt/index.js +246 -0
  55. package/src/lib/git-context.js +3 -1
  56. package/src/presets/index.js +20 -0
  57. package/src/repository/index.js +4 -0
  58. package/test/agent-capabilities.test.mjs +67 -0
  59. package/test/capabilities.test.mjs +84 -0
  60. package/test/deploy-receipt.test.mjs +91 -0
  61. package/test/portable-context.test.mjs +11 -1
@@ -0,0 +1,50 @@
1
+ import { getCapability, listCapabilities } from '../capabilities/manifest.js';
2
+ import { repositorySnapshot } from '../capabilities/repository-snapshot.js';
3
+ import { runRepositoryAudit } from './repository-audit.js';
4
+
5
+ export function createCapabilityAdapter({ handlers = {}, reasoner } = {}) {
6
+ const executable = new Map([
7
+ ['repository.snapshot', (input) => repositorySnapshot(input)],
8
+ ...Object.entries(handlers),
9
+ ]);
10
+ if (typeof reasoner === 'function') {
11
+ executable.set('repository.audit', (input = {}) => runRepositoryAudit({ ...input, reasoner }));
12
+ }
13
+
14
+ function describe(id) {
15
+ const capability = getCapability(id);
16
+ if (!capability) throw new Error(`unknown_capability:${id}`);
17
+ return capability;
18
+ }
19
+
20
+ return Object.freeze({
21
+ list(options = {}) {
22
+ return listCapabilities(options);
23
+ },
24
+ describe,
25
+ toolDescriptors({ domain, kind, includeUnavailable = false } = {}) {
26
+ return listCapabilities({ domain, kind }).filter((row) => includeUnavailable || executable.has(row.id)).map((row) => ({
27
+ name: row.id,
28
+ description: row.description,
29
+ input_schema: row.input_schema || null,
30
+ side_effects: row.side_effects,
31
+ deterministic: row.deterministic,
32
+ model_required: row.model_required,
33
+ }));
34
+ },
35
+ canInvoke(id) {
36
+ return executable.has(String(id || '').trim());
37
+ },
38
+ async invoke(id, input = {}) {
39
+ const capability = describe(id);
40
+ const handler = executable.get(capability.id);
41
+ if (!handler) throw new Error(`capability_handler_unavailable:${capability.id}`);
42
+ const value = await handler(input);
43
+ return {
44
+ capability_id: capability.id,
45
+ capability_version: capability.version,
46
+ result: value,
47
+ };
48
+ },
49
+ });
50
+ }
@@ -0,0 +1,2 @@
1
+ export { createCapabilityAdapter } from './capability-adapter.js';
2
+ export { buildRepositoryAuditPacket, renderRepositoryAuditMarkdown, runRepositoryAudit } from './repository-audit.js';
@@ -0,0 +1,188 @@
1
+ import { repositorySnapshot } from '../capabilities/repository-snapshot.js';
2
+
3
+ const DEPTHS = new Set(['quick', 'standard', 'deep']);
4
+ const FORBIDDEN_OUTPUT_KEYS = new Set(['jobs', 'executions', 'commits', 'deployments', 'mutations', 'patches']);
5
+ const ARRAY_FIELDS = [
6
+ 'packages',
7
+ 'binaries',
8
+ 'commands',
9
+ 'capabilities',
10
+ 'services',
11
+ 'protocols',
12
+ 'notable_combinations',
13
+ 'findings',
14
+ 'cleanup_candidates',
15
+ 'recommended_routes',
16
+ 'evidence_refs',
17
+ 'artifacts',
18
+ ];
19
+
20
+ function boundedStrings(values = [], max = 32, field = 'value') {
21
+ if (!Array.isArray(values)) throw new TypeError(`${field} must be an array`);
22
+ return values.slice(0, max).map((value) => String(value).trim()).filter(Boolean);
23
+ }
24
+
25
+ function shrinkEvidence(evidence, characterBudget) {
26
+ const out = structuredClone(evidence);
27
+ const shrinkable = [
28
+ ['pressure_points', 12, 6, 3],
29
+ ['top_level', 20, 10, 5],
30
+ ['file_paths', 400, 200, 100],
31
+ ['packages', 40, 20, 10],
32
+ ['languages', 30, 15, 8],
33
+ ['manifests', 40, 20, 10],
34
+ ];
35
+ for (const [key, ...limits] of shrinkable) {
36
+ const original = Array.isArray(out[key]) ? out[key] : [];
37
+ for (const limit of limits) {
38
+ if (JSON.stringify(out).length <= characterBudget) break;
39
+ out[key] = original.slice(0, limit);
40
+ }
41
+ }
42
+ if (JSON.stringify(out).length > characterBudget) {
43
+ out.top_level = [];
44
+ out.pressure_points = [];
45
+ }
46
+ if (JSON.stringify(out).length > characterBudget) {
47
+ out.file_paths = [];
48
+ out.packages = [];
49
+ out.languages = [];
50
+ out.manifests = [];
51
+ }
52
+ return out;
53
+ }
54
+
55
+ export function buildRepositoryAuditPacket({
56
+ snapshot,
57
+ focus = [],
58
+ depth = 'standard',
59
+ requestedSections = [],
60
+ evidenceBudget = 8000,
61
+ } = {}) {
62
+ if (snapshot?.capability !== 'repository.snapshot' || !snapshot.snapshot_id) {
63
+ throw new TypeError('repository.audit requires a repository.snapshot result');
64
+ }
65
+ if (!DEPTHS.has(depth)) throw new RangeError(`depth must be one of: ${[...DEPTHS].join(', ')}`);
66
+ if (!Number.isInteger(evidenceBudget) || evidenceBudget < 1000 || evidenceBudget > 64000) {
67
+ throw new RangeError('evidenceBudget must be an integer from 1000..64000');
68
+ }
69
+
70
+ const evidence = shrinkEvidence({
71
+ repository: snapshot.repository,
72
+ tree: { merkle_root: snapshot.tree?.merkle_root, stats: snapshot.tree?.stats },
73
+ file_paths: snapshot.tree?.paths || [],
74
+ summary: snapshot.intelligence?.summary || {},
75
+ languages: snapshot.intelligence?.languages || [],
76
+ manifests: snapshot.intelligence?.manifests || [],
77
+ top_level: snapshot.intelligence?.top_level || [],
78
+ pressure_points: snapshot.intelligence?.pressure_points || [],
79
+ packages: snapshot.packages || [],
80
+ knowledge: snapshot.knowledge || { configured: false },
81
+ deploy: snapshot.deploy || null,
82
+ }, evidenceBudget * 4);
83
+
84
+ const prefix = `snapshot:${snapshot.snapshot_id}`;
85
+ return Object.freeze({
86
+ schema_version: 1,
87
+ primitive: 'repository.audit',
88
+ snapshot_id: snapshot.snapshot_id,
89
+ snapshot_content_hash: snapshot.content_hash,
90
+ focus: boundedStrings(focus, 24, 'focus'),
91
+ depth,
92
+ requested_sections: boundedStrings(requestedSections, 24, 'requestedSections'),
93
+ evidence_budget: evidenceBudget,
94
+ evidence,
95
+ evidence_index: Object.freeze({
96
+ repository: `${prefix}#repository`,
97
+ tree: `${prefix}#tree`,
98
+ file_paths: `${prefix}#tree.paths`,
99
+ summary: `${prefix}#intelligence.summary`,
100
+ languages: `${prefix}#intelligence.languages`,
101
+ manifests: `${prefix}#intelligence.manifests`,
102
+ packages: `${prefix}#packages`,
103
+ knowledge: `${prefix}#knowledge`,
104
+ deploy: `${prefix}#deploy`,
105
+ }),
106
+ rules: Object.freeze({
107
+ read_only: true,
108
+ may_edit: false,
109
+ may_deploy: false,
110
+ may_create_jobs: false,
111
+ evidence_must_come_from_packet: true,
112
+ }),
113
+ });
114
+ }
115
+
116
+ function normalizeAnalysis(value, snapshotId) {
117
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('repository.audit reasoner must return an object');
118
+ for (const key of FORBIDDEN_OUTPUT_KEYS) {
119
+ if (Object.hasOwn(value, key)) throw new Error(`repository.audit output may not contain mutation field: ${key}`);
120
+ }
121
+ if (typeof value.summary !== 'string' || !value.summary.trim()) throw new TypeError('repository.audit output requires summary');
122
+ const prefix = `snapshot:${snapshotId}#`;
123
+ const result = {
124
+ summary: value.summary.trim(),
125
+ repository_map: value.repository_map ?? null,
126
+ file_tree: value.file_tree ?? null,
127
+ };
128
+ for (const field of ARRAY_FIELDS) result[field] = Array.isArray(value[field]) ? structuredClone(value[field]) : [];
129
+ const validateRef = (ref) => {
130
+ if (typeof ref !== 'string' || !ref.startsWith(prefix)) throw new Error(`repository.audit invalid evidence ref: ${ref}`);
131
+ };
132
+ for (const ref of result.evidence_refs) validateRef(ref);
133
+ for (const finding of result.findings) {
134
+ if (finding && typeof finding === 'object' && Array.isArray(finding.evidence_refs)) {
135
+ for (const ref of finding.evidence_refs) validateRef(ref);
136
+ }
137
+ }
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * Optional AgentSam/LLM layer over deterministic repository evidence.
143
+ * The SDK owns evidence collection and output validation; the caller injects reasoning.
144
+ */
145
+ export async function runRepositoryAudit({
146
+ cwd = process.cwd(),
147
+ snapshot,
148
+ focus = [],
149
+ depth = 'standard',
150
+ requestedSections = [],
151
+ evidenceBudget = 8000,
152
+ reasoner,
153
+ } = {}) {
154
+ if (typeof reasoner !== 'function') throw new TypeError('repository.audit requires an injected reasoner(packet) function');
155
+ const resolvedSnapshot = snapshot || await repositorySnapshot({ cwd });
156
+ const packet = buildRepositoryAuditPacket({ snapshot: resolvedSnapshot, focus, depth, requestedSections, evidenceBudget });
157
+ const analysis = normalizeAnalysis(await reasoner(structuredClone(packet)), resolvedSnapshot.snapshot_id);
158
+ return {
159
+ schema_version: 1,
160
+ primitive: 'repository.audit',
161
+ snapshot_id: resolvedSnapshot.snapshot_id,
162
+ snapshot_content_hash: resolvedSnapshot.content_hash,
163
+ generated_at: new Date().toISOString(),
164
+ ...analysis,
165
+ };
166
+ }
167
+
168
+ export function renderRepositoryAuditMarkdown(audit) {
169
+ if (audit?.primitive !== 'repository.audit') throw new TypeError('repository.audit result required');
170
+ const lines = [`# Repository audit`, '', audit.summary, '', `Snapshot: \`${audit.snapshot_id}\``];
171
+ const sections = [
172
+ ['Packages', audit.packages],
173
+ ['Commands', audit.commands],
174
+ ['Capabilities', audit.capabilities],
175
+ ['Services', audit.services],
176
+ ['Protocols', audit.protocols],
177
+ ['Notable combinations', audit.notable_combinations],
178
+ ['Findings', audit.findings],
179
+ ['Cleanup candidates', audit.cleanup_candidates],
180
+ ['Recommended routes', audit.recommended_routes],
181
+ ];
182
+ for (const [title, rows] of sections) {
183
+ if (!Array.isArray(rows) || rows.length === 0) continue;
184
+ lines.push('', `## ${title}`, '');
185
+ for (const row of rows) lines.push(`- ${typeof row === 'string' ? row : JSON.stringify(row)}`);
186
+ }
187
+ return `${lines.join('\n')}\n`;
188
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ CAPABILITY_MANIFEST_VERSION,
3
+ getCapability,
4
+ getCapabilityManifest,
5
+ listCapabilities,
6
+ } from './manifest.js';
7
+ export { repositorySnapshot } from './repository-snapshot.js';
@@ -0,0 +1,22 @@
1
+ import manifest from '../../protocol/capabilities/manifest.json' with { type: 'json' };
2
+
3
+ export const CAPABILITY_MANIFEST_VERSION = manifest.schema_version;
4
+
5
+ function rows() {
6
+ return Object.values(manifest.capabilities);
7
+ }
8
+
9
+ export function listCapabilities({ domain, status = 'stable', kind } = {}) {
10
+ return rows()
11
+ .filter((row) => (!domain || row.domain === domain) && (!status || row.status === status) && (!kind || row.kind === kind))
12
+ .map((row) => structuredClone(row));
13
+ }
14
+
15
+ export function getCapability(id) {
16
+ const row = manifest.capabilities[String(id || '').trim()];
17
+ return row ? structuredClone(row) : null;
18
+ }
19
+
20
+ export function getCapabilityManifest() {
21
+ return structuredClone(manifest);
22
+ }
@@ -0,0 +1,180 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { createHash } from 'node:crypto';
7
+ import { resolveGitContext } from '../lib/git-context.js';
8
+ import { buildMerkleTree } from '../lib/merkle/index.js';
9
+ import { showLatestDeployReceipt } from '../lib/deploy-receipt/index.js';
10
+ import { CONFIG_PATH, canonical, readConfig, scopeKey } from '../knowledge/config.js';
11
+ import { openSqliteStore } from '../knowledge/stores/sqlite.js';
12
+
13
+ const execute = promisify(execFile);
14
+
15
+ function sha256(value) {
16
+ return createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex');
17
+ }
18
+
19
+ function providerForHost(host) {
20
+ const value = String(host || '').toLowerCase();
21
+ if (value === 'github.com') return 'github';
22
+ if (value === 'gitlab.com') return 'gitlab';
23
+ if (value === 'bitbucket.org') return 'bitbucket';
24
+ return value ? 'git' : null;
25
+ }
26
+
27
+ async function gitIgnoredPaths(root) {
28
+ const result = await execute('git', ['status', '--porcelain=v1', '--ignored=matching'], {
29
+ cwd: root,
30
+ maxBuffer: 16 * 1024 * 1024,
31
+ });
32
+ return [...new Set(result.stdout.split('\n')
33
+ .filter((line) => line.startsWith('!! '))
34
+ .map((line) => line.slice(3).trim().replace(/\/$/, ''))
35
+ .filter((value) => value && !value.includes('\\') && !value.split('/').includes('..')))];
36
+ }
37
+
38
+ async function runRepositoryIntelligence(root, churnDays) {
39
+ const pythonRoot = fileURLToPath(new URL('../../python', import.meta.url));
40
+ const result = await execute(process.platform === 'win32' ? 'python' : 'python3', [
41
+ '-B', '-m', 'agentsam_sdk.repository.intelligence', '--repo-root', root,
42
+ '--churn-days', String(churnDays), '--json',
43
+ ], {
44
+ env: { ...process.env, PYTHONPATH: [pythonRoot, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter) },
45
+ maxBuffer: 64 * 1024 * 1024,
46
+ });
47
+ return JSON.parse(result.stdout);
48
+ }
49
+
50
+ function readPackageInventory(root, manifests = []) {
51
+ const packages = [];
52
+ for (const row of manifests) {
53
+ if (row.kind !== 'node' || path.basename(row.path) !== 'package.json') continue;
54
+ const filename = path.join(root, row.path);
55
+ try {
56
+ const parsed = JSON.parse(fs.readFileSync(filename, 'utf8'));
57
+ packages.push({
58
+ path: row.path,
59
+ name: parsed.name || null,
60
+ version: parsed.version || null,
61
+ private: parsed.private === true,
62
+ type: parsed.type || null,
63
+ bins: parsed.bin || null,
64
+ exports: parsed.exports && typeof parsed.exports === 'object' ? Object.keys(parsed.exports) : [],
65
+ scripts: parsed.scripts && typeof parsed.scripts === 'object' ? Object.keys(parsed.scripts).sort() : [],
66
+ dependencies: parsed.dependencies && typeof parsed.dependencies === 'object' ? Object.keys(parsed.dependencies).sort() : [],
67
+ dev_dependencies: parsed.devDependencies && typeof parsed.devDependencies === 'object' ? Object.keys(parsed.devDependencies).sort() : [],
68
+ workspaces: parsed.workspaces || null,
69
+ });
70
+ } catch {
71
+ packages.push({ path: row.path, invalid: true });
72
+ }
73
+ }
74
+ return packages.sort((a, b) => a.path.localeCompare(b.path));
75
+ }
76
+
77
+ async function readKnowledgeState(root) {
78
+ const filename = path.join(root, CONFIG_PATH);
79
+ if (!fs.existsSync(filename)) return { configured: false };
80
+ try {
81
+ const config = readConfig(root);
82
+ const state = {
83
+ configured: true,
84
+ repository_id: config.repository_id,
85
+ scope: config.scope,
86
+ storage: config.storage.driver,
87
+ embedding_profile: config.embedding,
88
+ legacy_workspace_id: config.workspace_id || null,
89
+ };
90
+ if (config.storage.driver !== 'sqlite') return state;
91
+ const store = await openSqliteStore(path.join(root, '.agentsam', 'knowledge', 'index.sqlite'), { readOnly: true });
92
+ if (!store) return { ...state, indexed: false };
93
+ try {
94
+ const generation = await store.active(scopeKey(config));
95
+ if (!generation) return { ...state, indexed: false };
96
+ return {
97
+ ...state,
98
+ indexed: true,
99
+ generation_id: generation.id,
100
+ created_at: generation.created_at,
101
+ source_hash: generation.source_hash,
102
+ profile_id: generation.profile_id || null,
103
+ receipt: generation.receipt || null,
104
+ };
105
+ } finally {
106
+ await store.close();
107
+ }
108
+ } catch (error) {
109
+ return { configured: true, error: error?.message || String(error) };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Canonical deterministic repository evidence snapshot.
115
+ * Read-only: no source/config/index/deploy state is created or modified.
116
+ */
117
+ export async function repositorySnapshot({ cwd = process.cwd(), churnDays = 30 } = {}) {
118
+ if (!Number.isInteger(churnDays) || churnDays < 1 || churnDays > 3650) {
119
+ throw new RangeError('churnDays must be an integer from 1..3650');
120
+ }
121
+ const git = resolveGitContext({ cwd });
122
+ const root = git.root;
123
+ const ignored = await gitIgnoredPaths(root);
124
+ const [intelligence, merkle, knowledge, deployReceipt] = await Promise.all([
125
+ runRepositoryIntelligence(root, churnDays),
126
+ buildMerkleTree(root, { exclude: ignored }),
127
+ readKnowledgeState(root),
128
+ showLatestDeployReceipt({ root }),
129
+ ]);
130
+ const provider = providerForHost(git.remoteHost);
131
+ const portableRepositoryId = provider && git.repoFullName ? `${provider}:${git.repoFullName}` : null;
132
+ const repositoryId = knowledge.repository_id || portableRepositoryId;
133
+ const packages = readPackageInventory(root, intelligence.manifests || []);
134
+
135
+ const evidence = {
136
+ repository: {
137
+ repository_id: repositoryId,
138
+ identity_source: knowledge.repository_id ? 'knowledge-config' : portableRepositoryId ? 'git-remote' : 'unresolved',
139
+ provider,
140
+ full_name: git.repoFullName,
141
+ remote_url: git.remoteUrl || null,
142
+ branch: git.branch,
143
+ revision_sha: git.revisionSha,
144
+ dirty: git.dirty,
145
+ },
146
+ tree: {
147
+ merkle_root: merkle.rootHash,
148
+ stats: merkle.stats,
149
+ paths: merkle.entries
150
+ .filter((entry) => entry.type === 'file' || entry.type === 'symlink')
151
+ .map((entry) => entry.path),
152
+ },
153
+ intelligence: {
154
+ summary: intelligence.summary,
155
+ languages: intelligence.languages || [],
156
+ manifests: intelligence.manifests || [],
157
+ top_level: intelligence.top_level || [],
158
+ pressure_points: intelligence.pressure_points || [],
159
+ },
160
+ packages,
161
+ knowledge,
162
+ deploy: deployReceipt ? {
163
+ status: deployReceipt.status,
164
+ root_hash: deployReceipt.root_hash,
165
+ git_sha: deployReceipt.git_sha,
166
+ deployment_id: deployReceipt.deployment_id || null,
167
+ worker_version_id: deployReceipt.worker_version_id || null,
168
+ completed_at: deployReceipt.completed_at || null,
169
+ } : null,
170
+ };
171
+ const contentHash = sha256(evidence);
172
+ return {
173
+ schema_version: 1,
174
+ capability: 'repository.snapshot',
175
+ snapshot_id: `rsnap_${contentHash.slice(0, 24)}`,
176
+ created_at: new Date().toISOString(),
177
+ content_hash: `sha256:${contentHash}`,
178
+ ...evidence,
179
+ };
180
+ }
package/src/cli.js CHANGED
@@ -21,7 +21,11 @@ import { runTui } from './commands/tui.js';
21
21
  import { runDockerize } from './commands/dockerize.js';
22
22
  import { runMini } from './commands/mini.js';
23
23
  import { runMerkle } from './commands/merkle.js';
24
+ import { runDeployReceipt } from './commands/deploy-receipt.js';
24
25
  import { runSecurity } from './commands/security.js';
26
+ import { runRecon } from './commands/recon.js';
27
+ import { applyPresetSelection, runAdd, runCapabilities, runDev, runInspect } from './commands/product.js';
28
+ import { listPresets, resolvePreset } from './presets/index.js';
25
29
  import { SLASH_COMMANDS, SHELL_PHASES } from './lib/slash-commands.js';
26
30
  import fs from 'node:fs';
27
31
  import { repositoryRoot } from './knowledge/config.js';
@@ -40,7 +44,17 @@ function printHelp() {
40
44
  console.log(`
41
45
  Agent Sam SDK — CLI v${VERSION}
42
46
 
43
- Usage:
47
+ Product UX:
48
+ agentsam create <name> --preset <fullstack|cms|prototype|data>
49
+ agentsam add <auth|cms|knowledge|agent|deploy-cloudflare>
50
+ agentsam dev Run this project's existing npm dev script
51
+ agentsam inspect [--json] Canonical deterministic repository.snapshot
52
+ agentsam deploy Graduate an AgentSam project intentionally
53
+
54
+ Capability discovery:
55
+ agentsam capabilities [capability-id] [--json]
56
+
57
+ Power-user UX:
44
58
  agentsam context [--json] Git repo/revision + bridge configuration from any repo
45
59
  agentsam init Configure knowledge in this repo; --name scaffolds a new project
46
60
  agentsam index Plan/run incremental AST and optional embeddings (--help)
@@ -48,6 +62,8 @@ function printHelp() {
48
62
  agentsam repo snapshot Git composition/churn; --save retains observations
49
63
  agentsam mini <name> Create and preview a small local gadget (--help for options)
50
64
  agentsam merkle File integrity, snapshots, comparisons, and TUI (--help)
65
+ agentsam deploy-receipt Merkle deploy/checkpoint capture + promote/failure receipts (--help)
66
+ agentsam recon Bounded-worker task packets + finding-report validation (--help)
51
67
  agentsam security Dependency scan, log triage, and verified repair (--help)
52
68
  agentsam status [--json] Live local Git + DB + API + PTY status
53
69
  agentsam db init|status Manage the project-local SQLite database
@@ -112,6 +128,21 @@ function parseDeployArgs(argv) {
112
128
  return opts;
113
129
  }
114
130
 
131
+ function parseCreateArgs(argv) {
132
+ const opts = { projectName: '', preset: 'fullstack', runTarget: 'local', help: false };
133
+ for (let i = 0; i < argv.length; i += 1) {
134
+ const arg = argv[i];
135
+ if (arg === '--help' || arg === '-h') opts.help = true;
136
+ else if (arg === '--preset') opts.preset = argv[++i] || 'fullstack';
137
+ else if (arg === '--run-target' || arg === '--target') opts.runTarget = argv[++i] || 'local';
138
+ else if (arg === '--yes' || arg === '-y') continue;
139
+ else if (arg.startsWith('-')) throw new Error(`unknown create option: ${arg}`);
140
+ else if (!opts.projectName) opts.projectName = arg;
141
+ else throw new Error(`unexpected create argument: ${arg}`);
142
+ }
143
+ return opts;
144
+ }
145
+
115
146
  async function runLocalInit(config) {
116
147
  const { projectName, lane, runTarget, prompt } = config;
117
148
 
@@ -121,13 +152,13 @@ async function runLocalInit(config) {
121
152
  );
122
153
 
123
154
  console.log(`
124
- ┌─────────────────────────────────────┐
155
+ ┌──────────────────────────────────────┐
125
156
  │ Agent Sam — local-first scaffold │
126
- ├─────────────────────────────────────┤
157
+ ├──────────────────────────────────────┤
127
158
  │ Name: ${meta.projectName.padEnd(25)}│
128
159
  │ Lane: ${meta.laneKey.padEnd(25)}│
129
160
  │ Run: ${meta.runTarget.padEnd(25)}│
130
- └─────────────────────────────────────┘
161
+ └──────────────────────────────────────┘
131
162
  `);
132
163
 
133
164
  const dir = writeScaffoldFiles(meta.projectName, meta.files);
@@ -159,16 +190,17 @@ async function runLocalInit(config) {
159
190
  console.log(`
160
191
  Local means local: no Worker, tunnel, IAM login, or cloud database is required.
161
192
  `);
193
+ return { dir, meta };
162
194
  }
163
195
 
164
196
  async function initInteractive(partial = {}) {
165
197
  const prompt = createPrompt();
166
198
 
167
199
  console.log(`
168
- ╔═══════════════════════════════════╗
200
+ ╔════════════════════════════════╗
169
201
  ║ Agent Sam SDK — Init ║
170
202
  ║ Local-first · Node only ║
171
- ╚═══════════════════════════════════╝
203
+ ╚════════════════════════════════╝
172
204
  `);
173
205
 
174
206
  const projectName =
@@ -241,9 +273,9 @@ async function runShellInfo(argv = []) {
241
273
 
242
274
  const next = SHELL_PHASES.find((p) => p.status === 'next' || p.status === 'current');
243
275
  console.log(`
244
- ╔═══════════════════════════════════╗
276
+ ╔═══════════════════════════════╗
245
277
  ║ Agent Sam Terminal ║
246
- ╚═══════════════════════════════════╝
278
+ ╚════════════════════════════════╝
247
279
 
248
280
  Local PTY agentsam start-local ws://127.0.0.1:3099
249
281
  ANSI TUI agentsam tui zero-dependency Node UI
@@ -267,6 +299,30 @@ if (command === '--version' || command === '-v') {
267
299
  console.log(VERSION);
268
300
  } else if (command === '--help' || command === '-h' || !command) {
269
301
  printHelp();
302
+ } else if (command === 'create') {
303
+ try {
304
+ const opts = parseCreateArgs(rest);
305
+ if (opts.help || !opts.projectName) {
306
+ console.log(`agentsam create <name> --preset <${listPresets().map((row) => row.id).join('|')}> [--target local|cloudflare|gcp]`);
307
+ } else {
308
+ const preset = resolvePreset(opts.preset);
309
+ const created = await runLocalInit({ projectName: opts.projectName, lane: preset.lane, runTarget: opts.runTarget, prompt: null });
310
+ applyPresetSelection(created.dir, preset);
311
+ console.log(` ✓ Preset ${preset.id}\n ✓ Features ${preset.features.join(', ') || 'none'}\n ✓ Capabilities ${preset.capabilities.length}\n`);
312
+ }
313
+ } catch (e) { console.error(`\n ✗ ${e?.message || e}\n`); process.exitCode = 1; }
314
+ } else if (command === 'add') {
315
+ try { await runAdd(rest); }
316
+ catch (e) { console.error(`\n ✗ ${e?.message || e}\n`); process.exitCode = 1; }
317
+ } else if (command === 'dev') {
318
+ try { await runDev(rest); }
319
+ catch (e) { console.error(`\n ✗ ${e?.message || e}\n`); process.exitCode = 1; }
320
+ } else if (command === 'inspect') {
321
+ try { await runInspect(rest); }
322
+ catch (e) { console.error(`\n ✗ ${e?.message || e}\n`); process.exitCode = 1; }
323
+ } else if (command === 'capabilities') {
324
+ try { await runCapabilities(rest); }
325
+ catch (e) { console.error(`\n ✗ ${e?.message || e}\n`); process.exitCode = 1; }
270
326
  } else if (command === 'context') {
271
327
  try {
272
328
  await runContext(rest);
@@ -329,6 +385,10 @@ if (command === '--version' || command === '-v') {
329
385
  await runSecurity(rest);
330
386
  } else if (command === 'merkle') {
331
387
  await runMerkle(rest);
388
+ } else if (command === 'deploy-receipt') {
389
+ await runDeployReceipt(rest);
390
+ } else if (command === 'recon') {
391
+ await runRecon(rest);
332
392
  } else if (command === 'mini') {
333
393
  try {
334
394
  await runMini(rest);