@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,24 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execute = promisify(execFile);
5
+
6
+ /**
7
+ * Resolve Git-ignored paths using Git itself so every Merkle-backed consumer can
8
+ * build evidence from the same checkout policy. Non-Git directories simply
9
+ * return an empty list and still receive the protocol's built-in excludes.
10
+ */
11
+ export async function gitIgnoredPaths(root) {
12
+ try {
13
+ const result = await execute('git', ['status', '--porcelain=v1', '--ignored=matching'], {
14
+ cwd: root,
15
+ maxBuffer: 16 * 1024 * 1024,
16
+ });
17
+ return [...new Set(result.stdout.split('\n')
18
+ .filter((line) => line.startsWith('!! '))
19
+ .map((line) => line.slice(3).trim().replace(/\/$/, ''))
20
+ .filter((value) => value && !value.includes('\\') && !value.split('/').includes('..')))];
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
@@ -4,6 +4,7 @@ export const comparePaths = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from
4
4
  export const digest = (type, value) => 'sha256:' + createHash('sha256')
5
5
  .update(`agentsam-merkle:${type}:v1\0`).update(value).digest('hex');
6
6
  export const linkHash = (target) => digest('symlink', target);
7
+ export const policyHash = (policy) => digest('policy', JSON.stringify(policy));
7
8
  export const directoryHash = (children) => digest('directory', JSON.stringify(
8
9
  [...children].sort((a, b) => comparePaths(a.name, b.name)).map(({ name, type, hash }) => [name, type, hash]),
9
10
  ));
@@ -2,3 +2,25 @@ export { buildMerkleTree } from './tree.js';
2
2
  export { saveSnapshot, readSnapshot, validateSnapshot } from './snapshot.js';
3
3
  export { diffTrees } from './diff.js';
4
4
  export { DEFAULT_IGNORES, normalizePolicy } from './policy.js';
5
+ export { policyHash } from './hash.js';
6
+ export async function buildSemanticMetadata(...args) {
7
+ const semantic = await import('./semantic.js');
8
+ return semantic.buildSemanticMetadata(...args);
9
+ }
10
+ export { validateSemanticMetadata, metadataRoot, FILEMETA_FORMAT, FILEMETA_VERSION } from './filemeta.js';
11
+
12
+ export {
13
+ MERKLE_SNAPSHOT_SCHEMA_SQL,
14
+ MERKLE_SNAPSHOT_STORAGE_PREFIX,
15
+ MERKLE_SNAPSHOT_TABLE,
16
+ MERKLE_WEBSITE_ASSETS_BINDING,
17
+ merkleSnapshotStorageKey,
18
+ normalizeMerkleStoragePrefix,
19
+ } from './persistence.js';
20
+ export {
21
+ buildMerklePersistencePlan,
22
+ merklePersistenceUpsertSql,
23
+ persistMerkleSnapshotCloudflare,
24
+ readWranglerPersistenceBindings,
25
+ resolveWranglerMerklePersistence,
26
+ } from './cloudflare-persistence.js';
@@ -0,0 +1,72 @@
1
+ export const MERKLE_SNAPSHOT_TABLE = 'agentsam_fs_merkle_snapshots';
2
+ export const MERKLE_SNAPSHOT_STORAGE_PREFIX = 'agentsam_fs_merkle_snapshots';
3
+ export const MERKLE_WEBSITE_ASSETS_BINDING = 'WEBSITE_ASSETS';
4
+
5
+ function clean(value) {
6
+ return value == null ? '' : String(value).trim();
7
+ }
8
+
9
+ function safeSegment(value, name) {
10
+ const segment = clean(value);
11
+ if (!segment) throw new Error(`${name}_required`);
12
+ if (segment === '.' || segment === '..' || /[\u0000-\u001f\u007f]/.test(segment)) {
13
+ throw new Error(`${name}_invalid`);
14
+ }
15
+ return encodeURIComponent(segment);
16
+ }
17
+
18
+ export function normalizeMerkleStoragePrefix(value = MERKLE_SNAPSHOT_STORAGE_PREFIX) {
19
+ const prefix = clean(value || MERKLE_SNAPSHOT_STORAGE_PREFIX).replace(/^\/+|\/+$/g, '');
20
+ if (!prefix || prefix.split('/').some((part) => !part || part === '.' || part === '..')) {
21
+ throw new Error('merkle_storage_prefix_invalid');
22
+ }
23
+ return prefix;
24
+ }
25
+
26
+ /**
27
+ * Provider-neutral object key. The host chooses which physical bucket is bound
28
+ * to the logical WEBSITE_ASSETS role; the SDK never owns cloud credentials.
29
+ */
30
+ export function merkleSnapshotStorageKey({ ownerUserId, repoId, snapshotId, prefix = MERKLE_SNAPSHOT_STORAGE_PREFIX } = {}) {
31
+ return `${normalizeMerkleStoragePrefix(prefix)}/${safeSegment(ownerUserId, 'owner_user_id')}/${safeSegment(repoId, 'repo_id')}/${safeSegment(snapshotId, 'snapshot_id')}.json`;
32
+ }
33
+
34
+ /** Portable D1/SQLite schema for hosts that opt into persisted Merkle snapshots. */
35
+ export const MERKLE_SNAPSHOT_SCHEMA_SQL = `CREATE TABLE IF NOT EXISTS ${MERKLE_SNAPSHOT_TABLE} (
36
+ snapshot_id TEXT PRIMARY KEY NOT NULL,
37
+ owner_user_id TEXT NOT NULL,
38
+ repo_id TEXT NOT NULL,
39
+ repository TEXT,
40
+ source TEXT NOT NULL CHECK (source IN ('github','gitlab','bitbucket','local','upload')),
41
+ manifest_format TEXT NOT NULL DEFAULT 'agentsam-merkle',
42
+ manifest_version INTEGER NOT NULL DEFAULT 1,
43
+ hash_algorithm TEXT NOT NULL DEFAULT 'sha256',
44
+ root_hash TEXT NOT NULL,
45
+ policy_hash TEXT,
46
+ resolved_commit_sha TEXT,
47
+ resolved_tree_sha TEXT,
48
+ git_branch TEXT,
49
+ working_tree_dirty INTEGER NOT NULL DEFAULT 0,
50
+ connection_id TEXT,
51
+ runtime_lease_id TEXT,
52
+ storage_backend TEXT NOT NULL DEFAULT 'r2' CHECK (storage_backend IN ('r2','local_fs','inline','none')),
53
+ storage_bucket TEXT,
54
+ storage_key TEXT,
55
+ entry_count INTEGER,
56
+ file_count INTEGER,
57
+ directory_count INTEGER,
58
+ symlink_count INTEGER,
59
+ total_bytes INTEGER,
60
+ capture_kind TEXT NOT NULL DEFAULT 'manual' CHECK (capture_kind IN ('deploy','manual','agent','index')),
61
+ deployment_id TEXT,
62
+ worker_version_id TEXT,
63
+ reference_label TEXT,
64
+ created_at INTEGER NOT NULL,
65
+ persisted_at INTEGER,
66
+ metadata_root TEXT,
67
+ classifier_format TEXT,
68
+ classifier_version INTEGER,
69
+ classifier_source TEXT,
70
+ CHECK (storage_backend IN ('inline','none') OR (storage_bucket IS NOT NULL AND storage_key IS NOT NULL)),
71
+ CHECK (connection_id IS NOT NULL OR runtime_lease_id IS NOT NULL OR capture_kind = 'deploy')
72
+ );`;
@@ -0,0 +1,359 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import ts from 'typescript';
4
+ import { comparePaths, fileHasher } from './hash.js';
5
+ import { FILEMETA_FORMAT, FILEMETA_VERSION, metadataRoot } from './filemeta.js';
6
+ const MAX_AST_BYTES = 2 * 1024 * 1024;
7
+
8
+ const LANGUAGE_BY_EXTENSION = Object.freeze({
9
+ '.c': 'c', '.cc': 'cpp', '.cpp': 'cpp', '.cxx': 'cpp', '.css': 'css', '.go': 'go',
10
+ '.h': 'c', '.hpp': 'cpp', '.html': 'html', '.java': 'java', '.js': 'javascript',
11
+ '.jsx': 'javascript', '.json': 'json', '.md': 'markdown', '.mjs': 'javascript',
12
+ '.cjs': 'javascript', '.py': 'python', '.rs': 'rust', '.scss': 'scss', '.sql': 'sql',
13
+ '.ts': 'typescript', '.tsx': 'typescript', '.yaml': 'yaml', '.yml': 'yaml',
14
+ });
15
+ const AST_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx']);
16
+ const ASSET_EXTENSIONS = new Set([
17
+ '.avif', '.eot', '.gif', '.ico', '.jpeg', '.jpg', '.mp3', '.mp4', '.ogg', '.otf', '.pdf',
18
+ '.png', '.svg', '.ttf', '.wav', '.webm', '.webp', '.woff', '.woff2',
19
+ ]);
20
+ const CONFIG_NAMES = new Set([
21
+ 'package.json', 'tsconfig.json', 'wrangler.json', 'wrangler.jsonc', 'wrangler.toml',
22
+ 'vite.config.js', 'vite.config.mjs', 'vite.config.ts', 'eslint.config.js', '.gitignore',
23
+ ]);
24
+
25
+ function systemName(value) {
26
+ return String(value || '')
27
+ .replace(/^@[^/]+\//, '')
28
+ .replace(/^agentsam-sdk-/, '')
29
+ .replace(/^agentsam-/, '')
30
+ .replace(/[^a-zA-Z0-9_-]+/g, '-')
31
+ .replace(/^-+|-+$/g, '')
32
+ .toLowerCase() || null;
33
+ }
34
+
35
+ function extensionOf(relative) {
36
+ const lower = relative.toLowerCase();
37
+ if (lower.endsWith('.d.ts')) return '.ts';
38
+ return path.posix.extname(lower);
39
+ }
40
+
41
+ function nearestPackage(relative, packages) {
42
+ let best = null;
43
+ for (const pkg of packages) {
44
+ if (!pkg.root || relative === pkg.root || relative.startsWith(pkg.root + '/')) {
45
+ if (!best || pkg.root.length > best.root.length) best = pkg;
46
+ }
47
+ }
48
+ return best;
49
+ }
50
+
51
+ function inferLayer(relative, packageRoot) {
52
+ const local = packageRoot && relative.startsWith(packageRoot + '/') ? relative.slice(packageRoot.length + 1) : relative;
53
+ const parts = local.split('/');
54
+ for (const name of ['frontend', 'backend', 'shared']) if (parts.includes(name)) return name;
55
+ const src = parts.indexOf('src');
56
+ if (src >= 0 && parts[src + 1] && parts[src + 1].includes('.') === false) return parts[src + 1];
57
+ for (const name of ['test', 'tests', 'docs', 'scripts', 'protocol', 'templates', 'migrations']) if (parts[0] === name || parts.includes(name)) return name === 'tests' ? 'test' : name;
58
+ return parts.length > 1 ? parts[0] : 'root';
59
+ }
60
+
61
+ function inferCategory(relative) {
62
+ const filename = path.posix.basename(relative);
63
+ const ext = path.posix.extname(filename);
64
+ let stem = ext ? filename.slice(0, -ext.length) : filename;
65
+ if (stem === 'index' || stem === 'README') stem = path.posix.basename(path.posix.dirname(relative));
66
+ return stem.toLowerCase().replace(/[^a-z0-9_-]+/g, '-') || 'root';
67
+ }
68
+
69
+ function inferKind(relative, language) {
70
+ const lower = relative.toLowerCase();
71
+ const ext = extensionOf(relative);
72
+ if (/(^|\/)(__tests__|tests?|fixtures?)(\/|$)/.test(lower) || /\.(test|spec)\.[^.]+$/.test(lower)) return 'test';
73
+ if (ASSET_EXTENSIONS.has(ext)) return 'asset';
74
+ if (ext === '.md' || /(^|\/)docs?(\/|$)/.test(lower) || path.posix.basename(relative).toLowerCase().startsWith('readme')) return 'documentation';
75
+ if (ext === '.sql' || /(^|\/)migrations?(\/|$)/.test(lower)) return 'migration';
76
+ if (CONFIG_NAMES.has(path.posix.basename(relative)) || /(^|\/)(config|configs)(\/|$)/.test(lower)) return 'config';
77
+ if (language && !['json', 'yaml'].includes(language)) return 'source';
78
+ if (language) return 'data';
79
+ return 'file';
80
+ }
81
+
82
+ function inferRole(relative, layer, kind) {
83
+ const lower = relative.toLowerCase();
84
+ if (kind === 'test') return 'test';
85
+ if (kind === 'documentation') return 'documentation';
86
+ if (kind === 'migration') return 'database';
87
+ if (kind === 'config') return 'configuration';
88
+ if (/(^|\/)contracts?(\/|$)/.test(lower)) return 'contract';
89
+ if (/(^|\/)adapters?(\/|$)/.test(lower)) return 'adapter';
90
+ if (/(^|\/)providers?(\/|$)/.test(lower)) return 'provider';
91
+ if (/(^|\/)(routes?|api|server)(\/|$)/.test(lower) || ['routes', 'api', 'server'].includes(layer)) return 'transport';
92
+ if (layer === 'frontend') return 'ui';
93
+ if (layer === 'core') return 'business-logic';
94
+ if (layer === 'scripts') return 'tooling';
95
+ return kind === 'source' ? 'runtime' : kind;
96
+ }
97
+
98
+ function inferExecutionDomain(relative, layer, role, kind) {
99
+ const lower = relative.toLowerCase();
100
+ const parts = lower.split('/');
101
+ const filename = parts.at(-1) || '';
102
+ if (kind === 'test') return 'test';
103
+ if (/^(?:vite|vitest|eslint|tailwind|postcss|playwright|next|nuxt|astro)\.config\.[cm]?[jt]s$/.test(filename)) return 'tooling';
104
+ if (/\.gen\.[cm]?[jt]sx?$/.test(filename) || filename.endsWith('.generated.ts') || filename.endsWith('.generated.js')) return 'framework';
105
+ if (/\.server\.[cm]?[jt]sx?$/.test(filename) || /^(server|worker)\.[cm]?[jt]s$/.test(filename) || filename.includes('server-only')) return 'server';
106
+ if (parts.includes('backend') || parts.includes('worker') || /(^|\/)routes?\/api(\/|$)/.test(lower) || /(^|\/)api(\/|$)/.test(lower)) return 'server';
107
+ if (/\.client\.[cm]?[jt]sx?$/.test(filename)) return 'browser';
108
+ if (parts.includes('shared') || layer === 'shared' || role === 'contract') return 'shared';
109
+ if (parts.includes('frontend') || layer === 'frontend') return 'browser';
110
+ if (layer === 'backend' || layer === 'server' || layer === 'api') return 'server';
111
+ return 'unknown';
112
+ }
113
+
114
+ function globRegex(glob) {
115
+ let source = '^';
116
+ for (let i = 0; i < glob.length; i++) {
117
+ const char = glob[i];
118
+ if (char === '*') {
119
+ if (glob[i + 1] === '*') { source += '.*'; i++; }
120
+ else source += '[^/]*';
121
+ } else if (char === '?') source += '[^/]';
122
+ else source += /[\\^$.*+?()[\]{}|]/.test(char) ? `\\${char}` : char;
123
+ }
124
+ return new RegExp(source + '$');
125
+ }
126
+
127
+ function packageRule(localPath, pkg) {
128
+ const rules = Array.isArray(pkg?.agentsam?.classify) ? pkg.agentsam.classify : [];
129
+ const out = {};
130
+ const tags = new Set();
131
+ for (const rule of rules) {
132
+ if (!rule || typeof rule.glob !== 'string' || !globRegex(rule.glob).test(localPath)) continue;
133
+ for (const key of ['system', 'category', 'layer', 'kind', 'language', 'role', 'execution_domain']) if (typeof rule[key] === 'string' && rule[key]) out[key] = rule[key];
134
+ if (Array.isArray(rule.tags)) for (const tag of rule.tags) if (typeof tag === 'string' && tag) tags.add(tag);
135
+ }
136
+ if (tags.size) out.tags = [...tags].sort();
137
+ return out;
138
+ }
139
+
140
+ function scriptKind(ext) {
141
+ if (ext === '.tsx') return ts.ScriptKind.TSX;
142
+ if (ext === '.jsx') return ts.ScriptKind.JSX;
143
+ if (ext === '.ts') return ts.ScriptKind.TS;
144
+ return ts.ScriptKind.JS;
145
+ }
146
+
147
+ function declarationNames(node) {
148
+ const out = [];
149
+ const addBinding = (name) => {
150
+ if (!name) return;
151
+ if (ts.isIdentifier(name)) out.push(name.text);
152
+ else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) for (const element of name.elements) if (ts.isBindingElement(element)) addBinding(element.name);
153
+ };
154
+ if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) && node.name) out.push(node.name.text);
155
+ else if (ts.isVariableStatement(node)) for (const declaration of node.declarationList.declarations) addBinding(declaration.name);
156
+ return out;
157
+ }
158
+
159
+ function astMetadata(relative, source) {
160
+ const ext = extensionOf(relative);
161
+ const file = ts.createSourceFile(relative, source, ts.ScriptTarget.Latest, true, scriptKind(ext));
162
+ const symbols = new Set();
163
+ const imports = new Set();
164
+ const envAccesses = new Set();
165
+ for (const statement of file.statements) {
166
+ for (const name of declarationNames(statement)) symbols.add(name);
167
+ if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) {
168
+ const value = statement.moduleSpecifier;
169
+ if (value && ts.isStringLiteralLike(value)) imports.add(value.text);
170
+ }
171
+ }
172
+ const recordEnvAccess = (node) => {
173
+ if (!ts.isPropertyAccessExpression(node) && !ts.isElementAccessExpression(node)) return;
174
+ const text = node.getText(file);
175
+ let match = text.match(/^(process\.env|import\.meta\.env)\.([A-Za-z_][A-Za-z0-9_]*)$/);
176
+ if (!match) match = text.match(/^(process\.env|import\.meta\.env)\[['\"]([A-Za-z_][A-Za-z0-9_]*)['\"]\]$/);
177
+ if (match) envAccesses.add(`${match[1]}:${match[2]}`);
178
+ };
179
+ const visit = (node) => {
180
+ recordEnvAccess(node);
181
+ if (ts.isCallExpression(node) && node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0])) {
182
+ if (ts.isIdentifier(node.expression) && node.expression.text === 'require') imports.add(node.arguments[0].text);
183
+ else if (node.expression.kind === ts.SyntaxKind.ImportKeyword) imports.add(node.arguments[0].text);
184
+ }
185
+ ts.forEachChild(node, visit);
186
+ };
187
+ visit(file);
188
+ const symbolList = [...symbols].sort();
189
+ const importList = [...imports].sort();
190
+ const envList = [...envAccesses].sort().map((value) => {
191
+ const split = value.indexOf(':');
192
+ return { source: value.slice(0, split), name: value.slice(split + 1) };
193
+ });
194
+ return {
195
+ symbols: symbolList,
196
+ imports: importList,
197
+ env_accesses: envList,
198
+ ast: {
199
+ indexed: true,
200
+ parser: 'typescript',
201
+ symbol_count: symbolList.length,
202
+ dependency_count: importList.length,
203
+ env_access_count: envList.length,
204
+ parse_error_count: Array.isArray(file.parseDiagnostics) ? file.parseDiagnostics.length : 0,
205
+ },
206
+ };
207
+ }
208
+
209
+ function resolveLocalImport(sourcePath, specifier, files) {
210
+ if (!specifier.startsWith('.')) return null;
211
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), specifier));
212
+ const candidates = [base];
213
+ const ext = path.posix.extname(base);
214
+ if (!ext) {
215
+ for (const suffix of ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json']) candidates.push(base + suffix);
216
+ for (const suffix of ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json']) candidates.push(path.posix.join(base, 'index' + suffix));
217
+ } else if (['.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
218
+ const stem = base.slice(0, -ext.length);
219
+ for (const suffix of ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']) candidates.push(stem + suffix);
220
+ }
221
+ return candidates.find((candidate) => files.has(candidate)) || null;
222
+ }
223
+
224
+ function attachResolvedImports(entries) {
225
+ const files = new Set(entries.filter((entry) => entry.type === 'file').map((entry) => entry.path));
226
+ for (const entry of entries) {
227
+ if (!Array.isArray(entry.imports) || !entry.imports.length) continue;
228
+ entry.resolved_imports = entry.imports.map((specifier) => {
229
+ const target = resolveLocalImport(entry.path, specifier, files);
230
+ return target ? { specifier, target } : { specifier };
231
+ });
232
+ }
233
+ }
234
+
235
+ async function readVerifiedFile(rootPath, entry) {
236
+ const filename = path.join(rootPath, ...entry.path.split('/'));
237
+ const before = await fs.lstat(filename, { bigint: true });
238
+ if (!before.isFile()) throw new Error(`Semantic index expected a file: ${entry.path}`);
239
+ if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`File is too large: ${entry.path}`);
240
+ const buffer = await fs.readFile(filename);
241
+ const after = await fs.lstat(filename, { bigint: true });
242
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs) {
243
+ throw new Error(`File changed while building semantic metadata: ${entry.path}`);
244
+ }
245
+ const hash = fileHasher();
246
+ hash.update(buffer);
247
+ const verified = 'sha256:' + hash.digest('hex');
248
+ if (verified !== entry.hash || buffer.length !== entry.size) throw new Error(`Merkle/semantic content mismatch: ${entry.path}`);
249
+ return { buffer, mode: Number(after.mode & 0o7777n) };
250
+ }
251
+
252
+ async function readPackages(rootPath, tree) {
253
+ const manifests = tree.entries.filter((entry) => entry.type === 'file' && path.posix.basename(entry.path) === 'package.json');
254
+ const packages = [];
255
+ for (const entry of manifests) {
256
+ const { buffer } = await readVerifiedFile(rootPath, entry);
257
+ let parsed = {};
258
+ try { parsed = JSON.parse(buffer.toString('utf8')); } catch { parsed = {}; }
259
+ const agentsam = parsed.agentsam && typeof parsed.agentsam === 'object' && !Array.isArray(parsed.agentsam) ? parsed.agentsam : {};
260
+ packages.push({
261
+ root: path.posix.dirname(entry.path) === '.' ? '' : path.posix.dirname(entry.path),
262
+ name: typeof parsed.name === 'string' ? parsed.name : null,
263
+ agentsam,
264
+ });
265
+ }
266
+ return packages.sort((a, b) => comparePaths(a.root, b.root));
267
+ }
268
+
269
+ function summarize(entries, packages) {
270
+ const bySystem = {};
271
+ const byLanguage = {};
272
+ const byExecutionDomain = {};
273
+ let astIndexed = 0;
274
+ for (const entry of entries) {
275
+ if (entry.system) bySystem[entry.system] = (bySystem[entry.system] || 0) + 1;
276
+ if (entry.language) byLanguage[entry.language] = (byLanguage[entry.language] || 0) + 1;
277
+ if (entry.execution_domain) byExecutionDomain[entry.execution_domain] = (byExecutionDomain[entry.execution_domain] || 0) + 1;
278
+ if (entry.ast?.indexed) astIndexed++;
279
+ }
280
+ return {
281
+ entries: entries.length,
282
+ files: entries.filter((entry) => entry.type === 'file').length,
283
+ symlinks: entries.filter((entry) => entry.type === 'symlink').length,
284
+ packages: packages.length,
285
+ ast_indexed: astIndexed,
286
+ by_system: Object.fromEntries(Object.entries(bySystem).sort()),
287
+ by_language: Object.fromEntries(Object.entries(byLanguage).sort()),
288
+ by_execution_domain: Object.fromEntries(Object.entries(byExecutionDomain).sort()),
289
+ };
290
+ }
291
+
292
+ export async function buildSemanticMetadata(rootPath, tree) {
293
+ const packages = await readPackages(rootPath, tree);
294
+ const classifier = {
295
+ format: FILEMETA_FORMAT,
296
+ version: FILEMETA_VERSION,
297
+ source: 'path+package+ast+execution-boundary',
298
+ trust_boundary: 'execution-domain-v1',
299
+ ast_parser: 'typescript',
300
+ ast_parser_version: ts.version,
301
+ };
302
+ const entries = [];
303
+ for (const contentEntry of tree.entries) {
304
+ if (contentEntry.type === 'directory') continue;
305
+ const pkg = nearestPackage(contentEntry.path, packages);
306
+ const localPath = pkg?.root && contentEntry.path.startsWith(pkg.root + '/') ? contentEntry.path.slice(pkg.root.length + 1) : contentEntry.path;
307
+ const ext = extensionOf(contentEntry.path);
308
+ const language = LANGUAGE_BY_EXTENSION[ext] || null;
309
+ const layer = inferLayer(contentEntry.path, pkg?.root || '');
310
+ const kind = contentEntry.type === 'symlink' ? 'symlink' : inferKind(contentEntry.path, language);
311
+ const structuralSystem = (contentEntry.path.startsWith('packages/') || contentEntry.path.startsWith('apps/')) ? contentEntry.path.split('/')[1] : null;
312
+ const baseSystem = pkg?.agentsam?.system || pkg?.name || structuralSystem || contentEntry.path.split('/')[0];
313
+ const explicit = packageRule(localPath, pkg);
314
+ const tags = new Set([...(Array.isArray(pkg?.agentsam?.tags) ? pkg.agentsam.tags : []), ...(explicit.tags || [])].filter((tag) => typeof tag === 'string' && tag));
315
+ let mode = contentEntry.mode;
316
+ let sourceBuffer = null;
317
+ if (contentEntry.type === 'file' && AST_EXTENSIONS.has(ext) && contentEntry.size <= MAX_AST_BYTES) {
318
+ const verified = await readVerifiedFile(rootPath, contentEntry);
319
+ if (mode != null && verified.mode !== mode) throw new Error(`File mode changed while building semantic metadata: ${contentEntry.path}`);
320
+ mode = verified.mode;
321
+ sourceBuffer = verified.buffer;
322
+ }
323
+ if (mode == null) {
324
+ const filename = path.join(rootPath, ...contentEntry.path.split('/'));
325
+ mode = Number((await fs.lstat(filename, { bigint: true })).mode & 0o7777n);
326
+ }
327
+ const record = {
328
+ path: contentEntry.path,
329
+ type: contentEntry.type,
330
+ ...(contentEntry.type === 'file' ? { size: contentEntry.size } : {}),
331
+ mode,
332
+ hash: contentEntry.hash,
333
+ ...(pkg?.name ? { package: pkg.name } : {}),
334
+ ...(pkg ? { package_root: pkg.root || '.' } : {}),
335
+ ...(pkg?.agentsam?.kind ? { package_kind: pkg.agentsam.kind } : {}),
336
+ system: explicit.system || systemName(baseSystem),
337
+ category: explicit.category || inferCategory(contentEntry.path),
338
+ layer: explicit.layer || layer,
339
+ kind: explicit.kind || kind,
340
+ ...(explicit.language || language ? { language: explicit.language || language } : {}),
341
+ role: explicit.role || inferRole(contentEntry.path, explicit.layer || layer, explicit.kind || kind),
342
+ execution_domain: explicit.execution_domain || inferExecutionDomain(contentEntry.path, explicit.layer || layer, explicit.role || inferRole(contentEntry.path, explicit.layer || layer, explicit.kind || kind), explicit.kind || kind),
343
+ tags: [...tags].sort(),
344
+ };
345
+ if (sourceBuffer) Object.assign(record, astMetadata(contentEntry.path, sourceBuffer.toString('utf8')));
346
+ entries.push(record);
347
+ }
348
+ attachResolvedImports(entries);
349
+ entries.sort((a, b) => comparePaths(a.path, b.path));
350
+ return {
351
+ format: FILEMETA_FORMAT,
352
+ version: FILEMETA_VERSION,
353
+ algorithm: 'sha256',
354
+ classifier,
355
+ rootHash: metadataRoot(entries, classifier),
356
+ stats: summarize(entries, packages),
357
+ entries,
358
+ };
359
+ }
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { randomBytes } from 'node:crypto';
4
- import { directoryHash, linkHash, comparePaths } from './hash.js';
4
+ import { directoryHash, linkHash, comparePaths, policyHash } from './hash.js';
5
5
  import { validPath, normalizePolicy, isIgnored } from './policy.js';
6
6
  import { buildMerkleTree } from './tree.js';
7
+ import { validateSemanticMetadata } from './filemeta.js';
7
8
 
8
9
  export function validateSnapshot(value) {
9
10
  const fail = (message) => { throw new Error(`Invalid Merkle snapshot: ${message}`); };
@@ -11,12 +12,15 @@ export function validateSnapshot(value) {
11
12
  if (typeof value.rootPath !== 'string' || !value.rootPath || value.rootPath.includes('\0')) fail('missing root path');
12
13
  if (!value.policy || !Array.isArray(value.entries) || value.entries.length === 0) fail('missing entries/policy');
13
14
  const policy = normalizePolicy(value.policy);
15
+ const normalizedPolicyHash = policyHash(policy);
16
+ if (value.policyHash != null && value.policyHash !== normalizedPolicyHash) fail('policy hash mismatch');
14
17
  const nodes = new Map();
15
18
  const children = new Map();
16
19
  const stats = { files: 0, symlinks: 0, directories: 0, bytes: 0 };
17
20
  for (const entry of value.entries) {
18
21
  if (!entry || !validPath(entry.path, true) || entry.path.split('/').length > 257 || nodes.has(entry.path)) fail('invalid/duplicate path');
19
22
  if (!/^sha256:[a-f0-9]{64}$/.test(entry.hash)) fail('invalid hash');
23
+ if (entry.mode != null && (!Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777)) fail('invalid mode');
20
24
  if (entry.path && isIgnored(entry.path, policy)) fail('entry contradicts ignore policy');
21
25
  if (entry.type === 'file') {
22
26
  if (!Number.isSafeInteger(entry.size) || entry.size < 0) fail('invalid file size');
@@ -43,9 +47,11 @@ export function validateSnapshot(value) {
43
47
  }
44
48
  }
45
49
  if (value.rootHash !== nodes.get('').hash) fail('root hash mismatch');
50
+ const entries = [...nodes.values()].sort((a, b) => comparePaths(a.path, b.path));
51
+ const semantic = value.semantic ? validateSemanticMetadata(value.semantic, entries) : null;
46
52
  return { format: value.format, version: 1, algorithm: 'sha256', rootPath: value.rootPath,
47
- createdAt: value.createdAt, policy, rootHash: value.rootHash, stats,
48
- entries: [...nodes.values()].sort((a, b) => comparePaths(a.path, b.path)) };
53
+ createdAt: value.createdAt, policy, policyHash: normalizedPolicyHash, rootHash: value.rootHash, stats,
54
+ entries, ...(semantic ? { semantic } : {}) };
49
55
  }
50
56
 
51
57
  export async function readSnapshot(filename) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { constants } from 'node:fs';
3
3
  import path from 'node:path';
4
- import { directoryHash, fileHasher, linkHash, comparePaths } from './hash.js';
4
+ import { directoryHash, fileHasher, linkHash, comparePaths, policyHash } from './hash.js';
5
5
  import { normalizePolicy, isIgnored, validPath } from './policy.js';
6
6
 
7
7
  const unchanged = (a, b) => ['dev', 'ino', 'size', 'mtimeNs', 'ctimeNs'].every((key) => a[key] === b[key]);
@@ -49,10 +49,10 @@ export async function buildMerkleTree(root = '.', options = {}) {
49
49
  const target = raw.toString('utf8');
50
50
  if (!Buffer.from(target).equals(raw)) throw new Error(`Symlink target is not UTF-8: ${relative}`);
51
51
  if (!unchanged(stat, await fs.lstat(absolute, { bigint: true }))) throw new Error(`Symlink changed while scanning: ${relative}`);
52
- entry = { path: relative, type: 'symlink', hash: linkHash(target), target };
52
+ entry = { path: relative, type: 'symlink', hash: linkHash(target), target, mode: Number(stat.mode & 0o7777n) };
53
53
  stats.symlinks++;
54
54
  } else if (stat.isFile()) {
55
- entry = { path: relative, type: 'file', ...await hashFile(absolute, stat, signal) };
55
+ entry = { path: relative, type: 'file', ...await hashFile(absolute, stat, signal), mode: Number(stat.mode & 0o7777n) };
56
56
  stats.files++; stats.bytes += entry.size;
57
57
  } else if (stat.isDirectory()) {
58
58
  const names = await namesAt(absolute, relative);
@@ -68,7 +68,7 @@ export async function buildMerkleTree(root = '.', options = {}) {
68
68
  }
69
69
  // Empty directories have no file content identity, including snapshot-only parents.
70
70
  if (relative && children.length === 0) return null;
71
- entry = { path: relative, type: 'directory', hash: directoryHash(children) };
71
+ entry = { path: relative, type: 'directory', hash: directoryHash(children), mode: Number(stat.mode & 0o7777n) };
72
72
  stats.directories++;
73
73
  } else throw new Error(`Unsupported filesystem entry: ${relative} (only files, directories, and symlinks are supported)`);
74
74
  entries.push(entry);
@@ -76,7 +76,12 @@ export async function buildMerkleTree(root = '.', options = {}) {
76
76
  return entry;
77
77
  }
78
78
  const rootEntry = await walk('');
79
- return { format: 'agentsam-merkle', version: 1, algorithm: 'sha256', rootPath,
80
- createdAt: new Date().toISOString(), policy, rootHash: rootEntry.hash, stats,
79
+ const tree = { format: 'agentsam-merkle', version: 1, algorithm: 'sha256', rootPath,
80
+ createdAt: new Date().toISOString(), policy, policyHash: policyHash(policy), rootHash: rootEntry.hash, stats,
81
81
  entries: entries.sort((a, b) => comparePaths(a.path, b.path)) };
82
+ if (options.semantic) {
83
+ const { buildSemanticMetadata } = await import('./semantic.js');
84
+ tree.semantic = await buildSemanticMetadata(rootPath, tree);
85
+ }
86
+ return tree;
82
87
  }
@@ -0,0 +1,66 @@
1
+ import readline from 'node:readline';
2
+ import { spawn } from 'node:child_process';
3
+
4
+ function normalizeHttpUrl(value) {
5
+ const raw = String(value || '').trim();
6
+ if (!raw) throw new Error('URL is required');
7
+ let parsed;
8
+ try { parsed = new URL(raw); }
9
+ catch { throw new Error(`Invalid URL: ${raw}`); }
10
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
11
+ throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
12
+ }
13
+ return parsed.toString();
14
+ }
15
+
16
+ export function browserCommand(url, platform = process.platform) {
17
+ const normalized = normalizeHttpUrl(url);
18
+ if (platform === 'darwin') return { command: 'open', args: [normalized] };
19
+ if (platform === 'win32') return { command: 'cmd', args: ['/c', 'start', '', normalized] };
20
+ return { command: 'xdg-open', args: [normalized] };
21
+ }
22
+
23
+ export function openExternalUrl(url, {
24
+ platform = process.platform,
25
+ spawnImpl = spawn,
26
+ } = {}) {
27
+ const invocation = browserCommand(url, platform);
28
+ const child = spawnImpl(invocation.command, invocation.args, {
29
+ stdio: 'ignore',
30
+ detached: true,
31
+ });
32
+ child.unref?.();
33
+ return invocation;
34
+ }
35
+
36
+ export async function promptToOpenUrl(url, {
37
+ heading = 'Open in your browser:',
38
+ prompt = 'Press ENTER to open in the browser, or copy the URL above.',
39
+ input = process.stdin,
40
+ output = process.stdout,
41
+ openImpl = openExternalUrl,
42
+ } = {}) {
43
+ const normalized = normalizeHttpUrl(url);
44
+ output.write(`\n${heading}\n${normalized}\n`);
45
+
46
+ if (!input?.isTTY || !output?.isTTY) {
47
+ output.write('Open the URL above in a browser to continue.\n\n');
48
+ return { url: normalized, opened: false, interactive: false };
49
+ }
50
+
51
+ const rl = readline.createInterface({ input, output });
52
+ try {
53
+ await new Promise(resolve => rl.question(`\n${prompt}\n`, resolve));
54
+ } finally {
55
+ rl.close();
56
+ }
57
+
58
+ try {
59
+ openImpl(normalized);
60
+ output.write('\nBrowser opened. Complete the step there, then return here.\n\n');
61
+ return { url: normalized, opened: true, interactive: true };
62
+ } catch (error) {
63
+ output.write(`\nCould not open the browser automatically: ${error.message}\nOpen the URL above manually.\n\n`);
64
+ return { url: normalized, opened: false, interactive: true, error: error.message };
65
+ }
66
+ }