@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTSAM.md +55 -0
- package/README.md +17 -12
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/AGENTSAM_WORKBENCH.md +30 -0
- package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
- package/docs/CAPABILITIES.md +5 -3
- package/docs/CLI_SHELL.md +167 -80
- package/docs/CMS_STUDIO.md +101 -0
- package/docs/CONTEXT.md +170 -0
- package/docs/LOCAL_OLLAMA.md +58 -0
- package/docs/MERKLE.md +19 -19
- package/docs/PORTABLE_CONTEXT.md +4 -3
- package/docs/PROJECT_CONFIG.md +72 -0
- package/docs/RELEASES.md +35 -2
- package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
- package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
- package/docs/SDK_WORKER.md +86 -0
- package/docs/SECURITY.md +59 -22
- package/docs/client-cms-editor.md +15 -0
- package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
- package/package.json +35 -10
- package/packages/connectors/cloudflare/package.json +10 -0
- package/packages/connectors/cloudflare/src/index.js +127 -0
- package/packages/connectors/cloudflare/src/owner.js +76 -0
- package/packages/connectors/cloudflare/src/routes.js +223 -0
- package/packages/connectors/cloudflare/src/vault.js +80 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
- package/packages/identity/package.json +10 -1
- package/packages/identity/src/contracts/auth-config.js +109 -0
- package/packages/identity/src/index.js +1 -0
- package/packages/identity/src/oauth/README.md +2 -2
- package/packages/identity/src/oauth/credentials.js +11 -4
- package/packages/identity/src/oauth/iam-platform.js +3 -3
- package/packages/identity/src/providers/iam/index.js +7 -7
- package/packages/identity/src/providers/iam/oauth.js +6 -4
- package/packages/identity/src/providers/iam/profile.js +5 -5
- package/packages/identity/tests/auth-config.test.mjs +61 -0
- package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
- package/protocol/FILEMETA_V1.md +95 -0
- package/protocol/INSPECT_VIEWS_V1.md +40 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
- package/protocol/MERKLE_V1.md +3 -1
- package/protocol/README.md +1 -0
- package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
- package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
- package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
- package/protocol/capabilities/manifest.json +79 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +36 -0
- package/protocol/context/context-item.schema.json +19 -0
- package/protocol/context/resolved-context-pack.schema.json +47 -0
- package/protocol/context/result-policy.schema.json +17 -0
- package/protocol/knowledge/context-pack.schema.json +33 -0
- package/protocol/knowledge/retrieval-query.schema.json +68 -13
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- package/python/README.md +15 -7
- package/python/agentsam_sdk/cli.py +0 -21
- package/python/agentsam_sdk/tui/README.md +17 -12
- package/python/agentsam_sdk/tui/bootstrap.py +2 -2
- package/python/agentsam_sdk/tui/demo.py +25 -10
- package/python/agentsam_sdk/tui/onboarding.py +208 -0
- package/python/tests/test_tui_cli.py +7 -6
- package/skills/README.md +21 -0
- package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
- package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
- package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
- package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
- package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
- package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
- package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
- package/skills/agentsam-jr-dev/SKILL.md +232 -0
- package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
- package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
- package/skills/agentsam-progression-guard/SKILL.md +197 -0
- package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
- package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
- package/skills/catalog.json +71 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +325 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/repository-snapshot-view.js +238 -0
- package/src/capabilities/repository-snapshot.js +28 -14
- package/src/cli.js +198 -76
- package/src/cloudflare/cpu-profile.js +115 -0
- package/src/cloudflare/index.js +14 -0
- package/src/cloudflare/wrangler.js +132 -0
- package/src/commands/account-auth.js +47 -0
- package/src/commands/cloudflare.js +58 -0
- package/src/commands/connections.js +93 -0
- package/src/commands/context-economics.js +114 -0
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +54 -32
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +18 -0
- package/src/commands/knowledge.js +27 -4
- package/src/commands/merkle-persist.js +118 -0
- package/src/commands/merkle.js +32 -17
- package/src/commands/models.js +152 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +144 -0
- package/src/commands/product.js +86 -16
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +8 -6
- package/src/commands/shell.js +405 -100
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +120 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +7 -0
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +116 -0
- package/src/context/result-policy.js +66 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +9 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +70 -0
- package/src/indexing/execution-boundary.js +144 -0
- package/src/indexing/index.js +8 -0
- package/src/indexing/provider.js +41 -0
- package/src/knowledge/config.js +2 -2
- package/src/knowledge/context-pack.js +12 -1
- package/src/knowledge/contracts.js +9 -4
- package/src/knowledge/engine.js +7 -3
- package/src/knowledge/service/server.js +1 -1
- package/src/lib/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +78 -0
- package/src/lib/core-client.js +8 -8
- package/src/lib/deploy/git-guard.js +69 -0
- package/src/lib/deploy/health.js +57 -0
- package/src/lib/deploy/local-studio.js +283 -0
- package/src/lib/deploy/secret-scan.js +65 -0
- package/src/lib/deploy-receipt/index.js +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- package/src/lib/local-sessions.js +127 -0
- package/src/lib/local-status.js +9 -17
- package/src/lib/merkle/cloudflare-persistence.js +321 -0
- package/src/lib/merkle/filemeta.js +43 -0
- package/src/lib/merkle/git-ignore.js +24 -0
- package/src/lib/merkle/hash.js +1 -0
- package/src/lib/merkle/index.js +22 -0
- package/src/lib/merkle/persistence.js +72 -0
- package/src/lib/merkle/semantic.js +359 -0
- package/src/lib/merkle/snapshot.js +9 -3
- package/src/lib/merkle/tree.js +11 -6
- package/src/lib/project-config.js +227 -0
- package/src/lib/project-rules.js +68 -0
- package/src/lib/provider-credentials.js +83 -0
- package/src/lib/save-sdk-token.js +1 -1
- package/src/lib/scaffold/templates/worker-api/index.js +101 -20
- package/src/lib/scaffold/wizards/worker-api.js +27 -11
- package/src/lib/slash-commands.js +22 -15
- package/src/lib/tools.js +11 -5
- package/src/models/catalog.js +135 -0
- package/src/models/index.js +7 -0
- package/src/providers/index.js +5 -0
- package/src/providers/openai-responses.js +275 -0
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- package/src/security/process.js +35 -9
- package/src/security/render.js +27 -5
- package/src/security/scan.js +24 -9
- package/src/security/trust-boundary.js +24 -0
- package/src/skills/index.js +64 -0
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +48 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +2 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +54 -0
- package/src/ui/merkle/render.js +1 -0
- package/src/ui/runtime-activity.js +192 -0
- package/src/ui/theme.js +19 -18
- package/test/account-session.test.mjs +36 -0
- package/test/app-building-skills.test.mjs +61 -0
- package/test/apps-scaffold-contract.test.mjs +56 -0
- package/test/capabilities.test.mjs +53 -4
- package/test/cli-preferences.test.mjs +46 -0
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +144 -0
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +59 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- package/test/indexing-provider.test.mjs +29 -0
- package/test/jr-dev-skill.test.mjs +26 -0
- package/test/knowledge-context-pack.test.mjs +19 -0
- package/test/knowledge.test.mjs +1 -1
- package/test/local-sessions.test.mjs +42 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +51 -0
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +94 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/provider-credentials.test.mjs +52 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -0
- package/test/responses-runner.test.mjs +148 -0
- package/test/runtime-activity.test.mjs +98 -0
- package/test/sdk-worker-contract.test.mjs +68 -0
- package/test/security.test.mjs +46 -0
- package/test/shell.test.mjs +55 -21
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +5 -2
- package/test/telemetry.test.mjs +79 -0
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +40 -0
- package/test/whoami-resume.test.mjs +56 -0
- package/examples/agentsam-tui-ansi.mjs +0 -149
- package/src/commands/tui.js +0 -120
- package/src/ui/splash-xterm.js +0 -290
- package/src/ui/splash.js +0 -426
|
@@ -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
|
|
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) {
|
package/src/lib/merkle/tree.js
CHANGED
|
@@ -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
|
-
|
|
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,227 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { tryResolveGitContext } from './git-context.js';
|
|
5
|
+
|
|
6
|
+
export const PROJECT_CONFIG_PATH = '.agentsam/config.json';
|
|
7
|
+
export const PROJECT_CONFIG_SCHEMA_VERSION = 2;
|
|
8
|
+
|
|
9
|
+
function providerForHost(host) {
|
|
10
|
+
const value = String(host || '').toLowerCase();
|
|
11
|
+
if (value === 'github.com') return 'github';
|
|
12
|
+
if (value === 'gitlab.com') return 'gitlab';
|
|
13
|
+
if (value === 'bitbucket.org') return 'bitbucket';
|
|
14
|
+
return value ? 'git' : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clean(value) {
|
|
18
|
+
return value == null ? '' : String(value).trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function portableRepositoryIdFromGit(cwd = process.cwd()) {
|
|
22
|
+
const git = tryResolveGitContext({ cwd });
|
|
23
|
+
if (!git) return null;
|
|
24
|
+
const provider = providerForHost(git.remoteHost);
|
|
25
|
+
if (!provider || !git.repoFullName) return null;
|
|
26
|
+
const fullName = provider === 'github' ? git.repoFullName.toLowerCase() : git.repoFullName;
|
|
27
|
+
return `${provider}:${fullName}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function newLocalRepositoryId() {
|
|
31
|
+
return `local:${randomUUID()}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createProjectManifest({
|
|
35
|
+
projectName,
|
|
36
|
+
preset = null,
|
|
37
|
+
profile = 'default',
|
|
38
|
+
runTarget = 'local',
|
|
39
|
+
sdkVersion = null,
|
|
40
|
+
repositoryId = null,
|
|
41
|
+
repositoryRemote = 'origin',
|
|
42
|
+
} = {}) {
|
|
43
|
+
const name = clean(projectName);
|
|
44
|
+
if (!name) throw new Error('project_name_required');
|
|
45
|
+
const repoId = clean(repositoryId) || newLocalRepositoryId();
|
|
46
|
+
const target = clean(runTarget) || 'local';
|
|
47
|
+
return {
|
|
48
|
+
schema_version: PROJECT_CONFIG_SCHEMA_VERSION,
|
|
49
|
+
project: {
|
|
50
|
+
name,
|
|
51
|
+
},
|
|
52
|
+
repository: {
|
|
53
|
+
id: repoId,
|
|
54
|
+
remote: clean(repositoryRemote) || 'origin',
|
|
55
|
+
},
|
|
56
|
+
product: {
|
|
57
|
+
preset: preset ? clean(preset) : null,
|
|
58
|
+
features: [],
|
|
59
|
+
capabilities: [],
|
|
60
|
+
},
|
|
61
|
+
defaults: {
|
|
62
|
+
mode: 'agent',
|
|
63
|
+
profile: clean(profile) || 'default',
|
|
64
|
+
runtime: 'local',
|
|
65
|
+
model: 'auto',
|
|
66
|
+
deploy_target: target === 'local' ? null : target,
|
|
67
|
+
},
|
|
68
|
+
merkle: {
|
|
69
|
+
enabled: true,
|
|
70
|
+
semantic: true,
|
|
71
|
+
persistence: 'auto',
|
|
72
|
+
},
|
|
73
|
+
rules: {
|
|
74
|
+
file: '.agentsamrules',
|
|
75
|
+
},
|
|
76
|
+
knowledge: {
|
|
77
|
+
config: '.agentsam/knowledge.json',
|
|
78
|
+
},
|
|
79
|
+
local: {
|
|
80
|
+
database: '.agentsam/data/agentsam.sqlite',
|
|
81
|
+
schema: 'db/schema.sql',
|
|
82
|
+
},
|
|
83
|
+
sdk: {
|
|
84
|
+
created_with: sdkVersion || null,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function projectConfigPath(root) {
|
|
90
|
+
return path.join(path.resolve(root), PROJECT_CONFIG_PATH);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readProjectConfig(root) {
|
|
94
|
+
return JSON.parse(fs.readFileSync(projectConfigPath(root), 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function tryReadProjectConfig(root) {
|
|
98
|
+
try {
|
|
99
|
+
return readProjectConfig(root);
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function writeProjectConfig(root, config) {
|
|
106
|
+
const filename = projectConfigPath(root);
|
|
107
|
+
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
|
108
|
+
fs.writeFileSync(filename, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
109
|
+
return filename;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isProjectManifestV2(config) {
|
|
113
|
+
return Number(config?.schema_version) === PROJECT_CONFIG_SCHEMA_VERSION && Boolean(config?.project?.name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Accessors deliberately understand the old flat scaffold so existing projects keep working
|
|
117
|
+
// while new projects use the committed portable v2 manifest.
|
|
118
|
+
export function getProjectName(config, fallback = '') {
|
|
119
|
+
return clean(config?.project?.name || config?.project || fallback);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getProjectPreset(config) {
|
|
123
|
+
return clean(config?.product?.preset || config?.preset || config?.lane) || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getDefaultProfile(config) {
|
|
127
|
+
return clean(config?.defaults?.profile || config?.agent) || 'default';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getDefaultRuntime(config) {
|
|
131
|
+
return clean(config?.defaults?.runtime || config?.run_target) || 'local';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getDefaultModel(config) {
|
|
135
|
+
return clean(config?.defaults?.model) || 'auto';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getDeployTarget(config) {
|
|
139
|
+
return clean(config?.defaults?.deploy_target || config?.deploy_target) || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getRepositoryId(config) {
|
|
143
|
+
return clean(config?.repository?.id) || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getProjectRulesPath(config) {
|
|
147
|
+
return clean(config?.rules?.file) || '.agentsamrules';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getKnowledgeConfigPath(config) {
|
|
151
|
+
return clean(config?.knowledge?.config) || '.agentsam/knowledge.json';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getLocalDatabasePath(config) {
|
|
155
|
+
return clean(config?.local?.database || config?.db_path) || '.agentsam/data/agentsam.sqlite';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getLocalSchemaPath(config) {
|
|
159
|
+
return clean(config?.local?.schema || config?.db_schema) || 'db/schema.sql';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getCreatedWithVersion(config) {
|
|
163
|
+
return clean(config?.sdk?.created_with || config?.scaffold_version) || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function setDeployTarget(config, target) {
|
|
167
|
+
const value = clean(target) || null;
|
|
168
|
+
if (isProjectManifestV2(config)) {
|
|
169
|
+
config.defaults ||= {};
|
|
170
|
+
config.defaults.deploy_target = value;
|
|
171
|
+
} else {
|
|
172
|
+
config.deploy_target = value;
|
|
173
|
+
}
|
|
174
|
+
return config;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function setProductPreset(config, preset) {
|
|
178
|
+
if (isProjectManifestV2(config)) {
|
|
179
|
+
config.product ||= { preset: null, features: [], capabilities: [] };
|
|
180
|
+
config.product.preset = clean(preset?.id) || null;
|
|
181
|
+
config.product.features = [...(preset?.features || [])];
|
|
182
|
+
config.product.capabilities = [...(preset?.capabilities || [])];
|
|
183
|
+
} else {
|
|
184
|
+
config.preset = clean(preset?.id) || null;
|
|
185
|
+
config.features = [...(preset?.features || [])];
|
|
186
|
+
config.capabilities = [...(preset?.capabilities || [])];
|
|
187
|
+
}
|
|
188
|
+
return config;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function setLocalModelCapability(config, provider = 'ollama') {
|
|
192
|
+
if (isProjectManifestV2(config)) {
|
|
193
|
+
config.models ||= { default: getDefaultModel(config) };
|
|
194
|
+
config.models.local = {
|
|
195
|
+
provider,
|
|
196
|
+
base_url_env: 'OLLAMA_BASE_URL',
|
|
197
|
+
model_env: 'OLLAMA_MODEL',
|
|
198
|
+
embed_model_env: 'OLLAMA_EMBED_MODEL',
|
|
199
|
+
};
|
|
200
|
+
} else {
|
|
201
|
+
config.local_model = {
|
|
202
|
+
provider,
|
|
203
|
+
local_only: true,
|
|
204
|
+
base_url_env: 'OLLAMA_BASE_URL',
|
|
205
|
+
model_env: 'OLLAMA_MODEL',
|
|
206
|
+
embed_model_env: 'OLLAMA_EMBED_MODEL',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return config;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function ensureProjectManifest(root, options = {}) {
|
|
213
|
+
const existing = tryReadProjectConfig(root);
|
|
214
|
+
if (existing) return existing;
|
|
215
|
+
const gitId = portableRepositoryIdFromGit(root);
|
|
216
|
+
const manifest = createProjectManifest({
|
|
217
|
+
projectName: options.projectName || path.basename(path.resolve(root)),
|
|
218
|
+
preset: options.preset ?? null,
|
|
219
|
+
profile: options.profile || 'default',
|
|
220
|
+
runTarget: options.runTarget || 'local',
|
|
221
|
+
sdkVersion: options.sdkVersion || null,
|
|
222
|
+
repositoryId: options.repositoryId || gitId || newLocalRepositoryId(),
|
|
223
|
+
repositoryRemote: options.repositoryRemote || 'origin',
|
|
224
|
+
});
|
|
225
|
+
writeProjectConfig(root, manifest);
|
|
226
|
+
return manifest;
|
|
227
|
+
}
|