@inneranimalmedia/agentsam-sdk 2.4.1 → 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 (158) 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 -4
  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/context.js +14 -2
  66. package/src/commands/db.js +4 -7
  67. package/src/commands/deploy.js +17 -31
  68. package/src/commands/interactive.js +21 -0
  69. package/src/commands/knowledge.js +27 -4
  70. package/src/commands/merkle-persist.js +118 -0
  71. package/src/commands/merkle.js +32 -17
  72. package/src/commands/models.js +107 -0
  73. package/src/commands/ollama.js +259 -0
  74. package/src/commands/preferences.js +102 -0
  75. package/src/commands/product.js +86 -16
  76. package/src/commands/security.js +3 -3
  77. package/src/commands/shell.js +71 -27
  78. package/src/commands/skills.js +66 -0
  79. package/src/commands/start-local.js +1 -1
  80. package/src/commands/tunnel.js +5 -4
  81. package/src/context/budget.js +58 -0
  82. package/src/context/compact.js +28 -0
  83. package/src/context/index.js +5 -0
  84. package/src/context/resolve.js +84 -0
  85. package/src/context/result-policy.js +66 -0
  86. package/src/index.js +16 -0
  87. package/src/indexing/execution-boundary.js +144 -0
  88. package/src/indexing/index.js +8 -0
  89. package/src/indexing/provider.js +41 -0
  90. package/src/knowledge/config.js +2 -2
  91. package/src/knowledge/context-pack.js +12 -1
  92. package/src/knowledge/contracts.js +9 -4
  93. package/src/knowledge/engine.js +7 -3
  94. package/src/knowledge/service/server.js +1 -1
  95. package/src/lib/bridge-client.js +7 -5
  96. package/src/lib/cli-preferences.js +74 -0
  97. package/src/lib/core-client.js +8 -8
  98. package/src/lib/deploy-receipt/index.js +5 -2
  99. package/src/lib/detect-context.js +6 -5
  100. package/src/lib/identity-scaffold.js +1 -1
  101. package/src/lib/local-scaffold.js +35 -33
  102. package/src/lib/local-status.js +9 -17
  103. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  104. package/src/lib/merkle/filemeta.js +43 -0
  105. package/src/lib/merkle/git-ignore.js +24 -0
  106. package/src/lib/merkle/hash.js +1 -0
  107. package/src/lib/merkle/index.js +22 -0
  108. package/src/lib/merkle/persistence.js +72 -0
  109. package/src/lib/merkle/semantic.js +359 -0
  110. package/src/lib/merkle/snapshot.js +9 -3
  111. package/src/lib/merkle/tree.js +11 -6
  112. package/src/lib/project-config.js +227 -0
  113. package/src/lib/project-rules.js +68 -0
  114. package/src/lib/save-sdk-token.js +1 -1
  115. package/src/lib/slash-commands.js +2 -1
  116. package/src/lib/tools.js +11 -5
  117. package/src/security/index.js +1 -0
  118. package/src/security/inventory.js +4 -1
  119. package/src/security/render.js +27 -5
  120. package/src/security/scan.js +24 -9
  121. package/src/security/trust-boundary.js +24 -0
  122. package/src/skills/index.js +64 -0
  123. package/src/tools/index.js +1 -0
  124. package/src/tools/search.js +70 -0
  125. package/src/ui/ansi.js +1 -1
  126. package/src/ui/boot.js +56 -0
  127. package/src/ui/merkle/render.js +1 -0
  128. package/src/ui/runtime-activity.js +192 -0
  129. package/src/ui/theme.js +19 -18
  130. package/test/app-building-skills.test.mjs +61 -0
  131. package/test/apps-scaffold-contract.test.mjs +56 -0
  132. package/test/capabilities.test.mjs +53 -4
  133. package/test/cli-preferences.test.mjs +25 -0
  134. package/test/context.test.mjs +95 -0
  135. package/test/indexing-provider.test.mjs +29 -0
  136. package/test/jr-dev-skill.test.mjs +26 -0
  137. package/test/knowledge-context-pack.test.mjs +19 -0
  138. package/test/knowledge.test.mjs +1 -1
  139. package/test/merkle-persistence.test.mjs +91 -0
  140. package/test/merkle.test.mjs +77 -3
  141. package/test/models.test.mjs +37 -0
  142. package/test/ollama.test.mjs +94 -0
  143. package/test/project-config.test.mjs +81 -0
  144. package/test/project-rules.test.mjs +44 -0
  145. package/test/release-hygiene.test.mjs +34 -0
  146. package/test/repository-snapshot-view.test.mjs +112 -0
  147. package/test/runtime-activity.test.mjs +98 -0
  148. package/test/sdk-worker-contract.test.mjs +68 -0
  149. package/test/security.test.mjs +46 -0
  150. package/test/shell.test.mjs +8 -1
  151. package/test/skills.test.mjs +22 -0
  152. package/test/smoke.mjs +2 -2
  153. package/test/theme-portability.test.mjs +14 -0
  154. package/test/tools-search.test.mjs +27 -0
  155. package/examples/agentsam-tui-ansi.mjs +0 -149
  156. package/src/commands/tui.js +0 -120
  157. package/src/ui/splash-xterm.js +0 -290
  158. package/src/ui/splash.js +0 -426
@@ -0,0 +1,84 @@
1
+ import { createContextBudget, estimateContextTokens } from './budget.js';
2
+ import { loadProjectRules } from '../lib/project-rules.js';
3
+
4
+ export const CONTEXT_ITEM_KINDS = Object.freeze(['file', 'symbol', 'memory', 'tool_result', 'repo', 'artifact']);
5
+
6
+ function clean(value) { return value == null ? '' : String(value).trim(); }
7
+
8
+ export function normalizeContextItem(value = {}) {
9
+ const ref = clean(value.ref);
10
+ if (!ref) throw new TypeError('ContextItem.ref is required');
11
+ const kind = clean(value.kind);
12
+ if (!CONTEXT_ITEM_KINDS.includes(kind)) throw new RangeError(`ContextItem.kind must be one of: ${CONTEXT_ITEM_KINDS.join(', ')}`);
13
+ const content = value.content == null ? '' : String(value.content);
14
+ const chars = Number.isInteger(value.chars) && value.chars >= 0 ? value.chars : content.length;
15
+ const priority = Number.isFinite(value.priority) ? Number(value.priority) : 0;
16
+ return Object.freeze({ ref, kind, chars, hash: clean(value.hash) || undefined, priority, content });
17
+ }
18
+
19
+ function capForKind(item, budget) {
20
+ if (item.kind === 'file') return budget.maxFileCharsPerRead;
21
+ if (item.kind === 'tool_result') return budget.maxToolResultChars;
22
+ return budget.maxEvidenceChars;
23
+ }
24
+
25
+ function truncate(content, maxChars) {
26
+ if (content.length <= maxChars) return { content, truncated: false };
27
+ return { content: `${content.slice(0, Math.max(0, maxChars - 1))}…`, truncated: true };
28
+ }
29
+
30
+ export function resolveContext(options = {}) {
31
+ const objective = clean(options.objective);
32
+ if (!objective) throw new TypeError('context.resolve objective is required');
33
+ const budget = options.budget?.windowTokens ? options.budget : createContextBudget(options.budget || {});
34
+ const refs = [...new Set((options.refs || []).map(clean).filter(Boolean))];
35
+ const candidates = (options.items || []).map((item, index) => ({ item: normalizeContextItem(item), index }));
36
+ candidates.sort((a, b) => (b.item.priority - a.item.priority) || (a.index - b.index));
37
+
38
+ let evidenceChars = 0;
39
+ let fileChars = 0;
40
+ const included = [];
41
+ const deferred = [];
42
+
43
+ for (const row of candidates) {
44
+ const item = row.item;
45
+ const remainingEvidence = budget.maxEvidenceChars - evidenceChars;
46
+ if (remainingEvidence <= 0) { deferred.push(item.ref); continue; }
47
+ const kindCap = capForKind(item, budget);
48
+ const remainingFile = item.kind === 'file' ? budget.maxFileCharsPerTurn - fileChars : Number.POSITIVE_INFINITY;
49
+ const allowed = Math.floor(Math.min(kindCap, remainingEvidence, remainingFile));
50
+ if (allowed <= 0) { deferred.push(item.ref); continue; }
51
+ const clipped = truncate(item.content, allowed);
52
+ const next = Object.freeze({ ...item, content: clipped.content, chars: clipped.content.length, truncated: clipped.truncated || item.chars > clipped.content.length });
53
+ included.push(next);
54
+ evidenceChars += next.chars;
55
+ if (item.kind === 'file') fileChars += next.chars;
56
+ if (clipped.truncated) deferred.push(`${item.ref}#remainder`);
57
+ }
58
+
59
+ const systemChars = options.rules?.content ? String(options.rules.content).length : 0;
60
+ const totalChars = systemChars + evidenceChars;
61
+ return Object.freeze({
62
+ objective,
63
+ refs: Object.freeze(refs),
64
+ rules: options.rules || null,
65
+ items: Object.freeze(included),
66
+ budget,
67
+ receipt: Object.freeze({
68
+ chars: totalChars,
69
+ evidence_chars: evidenceChars,
70
+ system_chars: systemChars,
71
+ estimated_tokens: estimateContextTokens(totalChars, budget.charsPerToken),
72
+ sources_considered: candidates.length,
73
+ sources_included: included.length,
74
+ sources_deferred: deferred.length,
75
+ deferred_refs: Object.freeze(deferred),
76
+ }),
77
+ });
78
+ }
79
+
80
+ export function resolveProjectContext(options = {}) {
81
+ const budget = options.budget?.windowTokens ? options.budget : createContextBudget(options.budget || {});
82
+ const rules = loadProjectRules(options.cwd || process.cwd(), { maxChars: Math.min(24_000, budget.maxSystemChars) });
83
+ return resolveContext({ ...options, budget, rules });
84
+ }
@@ -0,0 +1,66 @@
1
+ export const RESULT_DETAIL_ORDER = Object.freeze(['metadata', 'card', 'excerpt', 'range', 'full']);
2
+
3
+ export const DEFAULT_RESULT_POLICY = Object.freeze({
4
+ max_items: 8,
5
+ max_chars: 24_000,
6
+ detail: 'excerpt',
7
+ });
8
+
9
+ function positiveInteger(value, label) {
10
+ if (!Number.isInteger(value) || value < 1) throw new RangeError(`${label} must be a positive integer`);
11
+ return value;
12
+ }
13
+
14
+ function detailRank(value) {
15
+ const index = RESULT_DETAIL_ORDER.indexOf(value);
16
+ if (index === -1) throw new RangeError(`detail must be one of: ${RESULT_DETAIL_ORDER.join(', ')}`);
17
+ return index;
18
+ }
19
+
20
+ export function normalizeResultPolicy(value = {}, options = {}) {
21
+ const ceiling = { ...DEFAULT_RESULT_POLICY, ...(options.ceiling || {}) };
22
+ const requested = {
23
+ max_items: positiveInteger(value.max_items ?? ceiling.max_items, 'max_items'),
24
+ max_chars: positiveInteger(value.max_chars ?? ceiling.max_chars, 'max_chars'),
25
+ detail: value.detail ?? ceiling.detail,
26
+ };
27
+ detailRank(requested.detail);
28
+ detailRank(ceiling.detail);
29
+
30
+ const higherDetailOperation = options.operation === 'higher-detail';
31
+ if (!higherDetailOperation) {
32
+ if (requested.max_items > ceiling.max_items) throw new RangeError('result_policy_higher_detail_required:max_items');
33
+ if (requested.max_chars > ceiling.max_chars) throw new RangeError('result_policy_higher_detail_required:max_chars');
34
+ if (detailRank(requested.detail) > detailRank(ceiling.detail)) throw new RangeError('result_policy_higher_detail_required:detail');
35
+ }
36
+ return Object.freeze(requested);
37
+ }
38
+
39
+ export function truncateResultText(value, maxChars = DEFAULT_RESULT_POLICY.max_chars) {
40
+ const text = String(value ?? '');
41
+ positiveInteger(maxChars, 'maxChars');
42
+ if (text.length <= maxChars) return Object.freeze({ text, chars: text.length, source_chars: text.length, truncated: false });
43
+ const bounded = `${text.slice(0, Math.max(0, maxChars - 1))}…`;
44
+ return Object.freeze({ text: bounded, chars: bounded.length, source_chars: text.length, truncated: true });
45
+ }
46
+
47
+ export function boundResultItems(items = [], requested = {}, options = {}) {
48
+ if (!Array.isArray(items)) throw new TypeError('items must be an array');
49
+ const policy = normalizeResultPolicy(requested, options);
50
+ const selected = items.slice(0, policy.max_items);
51
+ const serialized = JSON.stringify(selected);
52
+ const bounded = truncateResultText(serialized, policy.max_chars);
53
+ return Object.freeze({
54
+ items: Object.freeze(selected),
55
+ policy,
56
+ receipt: Object.freeze({
57
+ source_items: items.length,
58
+ returned_items: selected.length,
59
+ omitted_items: Math.max(0, items.length - selected.length),
60
+ serialized_chars: serialized.length,
61
+ max_chars: policy.max_chars,
62
+ detail: policy.detail,
63
+ serialized_overflow: bounded.truncated,
64
+ }),
65
+ });
66
+ }
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ import pkg from '../package.json' with { type: 'json' };
4
4
 
5
5
  export { AgentSam } from './AgentSam.js';
6
6
  export { routeIntent } from './lib/router.js';
7
+ export { searchToolCards, toToolCard } from './tools/index.js';
7
8
  export { getToolCatalog } from './lib/tools.js';
8
9
  export { scaffoldProject } from './lib/scaffold.js';
9
10
  export {
@@ -39,6 +40,21 @@ export {
39
40
  repositorySnapshot,
40
41
  } from './capabilities/index.js';
41
42
  export { getPreset, listPresets, resolvePreset, getAddon, listAddons } from './presets/index.js';
43
+ export {
44
+ DEFAULT_CONTEXT_RATIOS,
45
+ DEFAULT_RESULT_POLICY,
46
+ createContextBudget,
47
+ normalizeResultPolicy,
48
+ resolveContext,
49
+ resolveProjectContext,
50
+ compactConsumedToolResult,
51
+ loadProjectRules,
52
+ } from './context/index.js';
53
+ export {
54
+ assertRepositoryKnowledgeProvider,
55
+ createRepositoryKnowledgeClient,
56
+ describeRepositoryKnowledgeProvider,
57
+ } from './indexing/index.js';
42
58
 
43
59
  export {
44
60
  createIdentityClient,
@@ -0,0 +1,144 @@
1
+ import { builtinModules } from 'node:module';
2
+
3
+ const NODE_BUILTINS = new Set(builtinModules.flatMap((name) => [name, name.replace(/^node:/, '')]));
4
+ const SERVER_PACKAGES = new Set([
5
+ 'pg', 'postgres', 'mysql', 'mysql2', 'better-sqlite3', 'sqlite3', 'tedious', 'oracledb',
6
+ ]);
7
+ const PUBLIC_ENV_PREFIX = /^(?:VITE_|NEXT_PUBLIC_|PUBLIC_|REACT_APP_)/;
8
+ const SECRETISH_ENV = /(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|DATABASE_URL|API_KEY|ACCESS_KEY|CLIENT_SECRET|SIGNING_KEY|ENCRYPTION_KEY)/i;
9
+
10
+ function packageRoot(specifier) {
11
+ if (!specifier || specifier.startsWith('.') || specifier.startsWith('/')) return null;
12
+ const clean = specifier.replace(/^node:/, '');
13
+ if (clean.startsWith('@')) return clean.split('/').slice(0, 2).join('/');
14
+ return clean.split('/')[0];
15
+ }
16
+
17
+ function finding(kind, entry, extra = {}) {
18
+ return {
19
+ kind,
20
+ severity: 'high',
21
+ source: entry.path,
22
+ source_hash: entry.hash,
23
+ execution_domain: entry.execution_domain,
24
+ ...extra,
25
+ };
26
+ }
27
+
28
+ export function repairSuggestionForBoundaryFinding(item) {
29
+ const base = { finding: item.kind, automatic: false, evidence: { source: item.source, target: item.target || null } };
30
+ if (item.kind === 'browser-server-import') return {
31
+ ...base,
32
+ action: 'move-server-call-behind-api-boundary',
33
+ summary: 'Keep the browser-facing contract client-safe; move the privileged implementation behind a server/API boundary and call it over HTTP/RPC.',
34
+ };
35
+ if (item.kind === 'browser-server-runtime-import') return {
36
+ ...base,
37
+ action: 'move-server-dependency-out-of-browser-graph',
38
+ summary: `Move ${item.specifier} to server-owned code; expose only a bounded request/response contract to the browser.`,
39
+ };
40
+ if (item.kind === 'browser-private-env-access' || item.kind === 'browser-public-secret-name') return {
41
+ ...base,
42
+ action: 'move-secret-to-server-runtime',
43
+ summary: `Treat ${item.env} as server-owned. Browser code should receive only non-secret derived data from an authenticated server endpoint.`,
44
+ };
45
+ if (item.kind === 'shared-server-env') return {
46
+ ...base,
47
+ action: 'split-shared-contract-from-server-implementation',
48
+ summary: 'Keep shared code deterministic and secret-free; move server environment access into a server adapter and retain only types/schemas/contracts in shared.',
49
+ };
50
+ return { ...base, action: 'review-trust-boundary', summary: 'Review the client/server execution boundary using the attached AST/Merkle evidence.' };
51
+ }
52
+
53
+ export function analyzeExecutionBoundaries(semantic) {
54
+ if (!semantic || !Array.isArray(semantic.entries)) throw new TypeError('agentsam-filemeta semantic metadata is required');
55
+ const byPath = new Map(semantic.entries.map((entry) => [entry.path, entry]));
56
+ const browserRoots = semantic.entries.filter((entry) => entry.type === 'file' && entry.execution_domain === 'browser');
57
+ const reachable = new Map();
58
+ const queue = browserRoots.map((entry) => ({ path: entry.path, root: entry.path }));
59
+ for (const item of queue) reachable.set(item.path, item.root);
60
+ const findings = [];
61
+ const dedupe = new Set();
62
+ const add = (item) => {
63
+ const key = JSON.stringify([item.kind, item.source, item.target || '', item.specifier || '', item.env || '']);
64
+ if (dedupe.has(key)) return;
65
+ dedupe.add(key);
66
+ findings.push({ ...item, repair: repairSuggestionForBoundaryFinding(item) });
67
+ };
68
+
69
+ for (let i = 0; i < queue.length; i++) {
70
+ const current = queue[i];
71
+ const entry = byPath.get(current.path);
72
+ if (!entry) continue;
73
+
74
+ for (const edge of entry.resolved_imports || []) {
75
+ if (!edge.target) continue;
76
+ const target = byPath.get(edge.target);
77
+ if (!target) continue;
78
+ if (target.execution_domain === 'server') {
79
+ add(finding('browser-server-import', entry, {
80
+ target: target.path,
81
+ target_hash: target.hash,
82
+ specifier: edge.specifier,
83
+ browser_root: current.root,
84
+ }));
85
+ continue;
86
+ }
87
+ if (target.execution_domain === 'framework') {
88
+ if (!reachable.has(target.path)) reachable.set(target.path, current.root);
89
+ continue;
90
+ }
91
+ if (!reachable.has(target.path)) {
92
+ reachable.set(target.path, current.root);
93
+ queue.push({ path: target.path, root: current.root });
94
+ }
95
+ }
96
+
97
+ for (const specifier of entry.imports || []) {
98
+ if (specifier.startsWith('.')) continue;
99
+ const root = packageRoot(specifier);
100
+ const normalized = specifier.replace(/^node:/, '');
101
+ if (NODE_BUILTINS.has(normalized) || SERVER_PACKAGES.has(root)) {
102
+ add(finding('browser-server-runtime-import', entry, { specifier, browser_root: current.root }));
103
+ }
104
+ }
105
+
106
+ for (const access of entry.env_accesses || []) {
107
+ const env = access.name;
108
+ const publicName = PUBLIC_ENV_PREFIX.test(env);
109
+ if (publicName && SECRETISH_ENV.test(env)) {
110
+ add(finding('browser-public-secret-name', entry, { env, env_source: access.source, browser_root: current.root }));
111
+ } else if (access.source === 'process.env' && !publicName) {
112
+ add(finding('browser-private-env-access', entry, { env, env_source: access.source, browser_root: current.root }));
113
+ }
114
+ }
115
+ }
116
+
117
+ for (const entry of semantic.entries) {
118
+ if (entry.execution_domain !== 'shared') continue;
119
+ for (const access of entry.env_accesses || []) {
120
+ if (access.source === 'process.env' || SECRETISH_ENV.test(access.name)) {
121
+ add(finding('shared-server-env', entry, { env: access.name, env_source: access.source }));
122
+ }
123
+ }
124
+ }
125
+
126
+ findings.sort((a, b) => [a.source, a.kind, a.target || '', a.specifier || '', a.env || ''].join('\0').localeCompare([b.source, b.kind, b.target || '', b.specifier || '', b.env || ''].join('\0')));
127
+ const astEntries = semantic.entries.filter((entry) => entry.ast?.indexed);
128
+ const parseErrors = astEntries.filter((entry) => Number(entry.ast?.parse_error_count || 0) > 0).map((entry) => entry.path);
129
+ return {
130
+ schema_version: 1,
131
+ analyzer: 'agentsam-execution-boundary',
132
+ metadata_root: semantic.rootHash || null,
133
+ classifier: semantic.classifier || null,
134
+ complete: parseErrors.length === 0,
135
+ status: parseErrors.length ? 'incomplete' : findings.length ? 'action-required' : 'clean',
136
+ ok: parseErrors.length === 0 && findings.length === 0,
137
+ browser_roots: browserRoots.length,
138
+ browser_reachable_files: reachable.size,
139
+ ast_files: astEntries.length,
140
+ parse_errors: parseErrors,
141
+ findings,
142
+ repair_suggestions: findings.map((item) => item.repair),
143
+ };
144
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ REPOSITORY_KNOWLEDGE_PROVIDER_METHODS,
3
+ assertRepositoryKnowledgeProvider,
4
+ createRepositoryKnowledgeClient,
5
+ describeRepositoryKnowledgeProvider,
6
+ } from './provider.js';
7
+
8
+ export { analyzeExecutionBoundaries, repairSuggestionForBoundaryFinding } from './execution-boundary.js';
@@ -0,0 +1,41 @@
1
+ export const REPOSITORY_KNOWLEDGE_PROVIDER_METHODS = Object.freeze([
2
+ 'status',
3
+ 'refresh',
4
+ 'findSymbol',
5
+ 'graph',
6
+ 'retrieve',
7
+ 'snapshot',
8
+ ]);
9
+
10
+ export function assertRepositoryKnowledgeProvider(provider) {
11
+ if (!provider || typeof provider !== 'object') throw new TypeError('RepositoryKnowledgeProvider must be an object');
12
+ for (const method of REPOSITORY_KNOWLEDGE_PROVIDER_METHODS) {
13
+ if (typeof provider[method] !== 'function') throw new TypeError(`RepositoryKnowledgeProvider.${method}() is required`);
14
+ }
15
+ return provider;
16
+ }
17
+
18
+ export function createRepositoryKnowledgeClient(provider) {
19
+ const impl = assertRepositoryKnowledgeProvider(provider);
20
+ const call = (method, input = {}) => Promise.resolve(impl[method](input));
21
+ return Object.freeze({
22
+ status: (input) => call('status', input),
23
+ refresh: (input) => call('refresh', input),
24
+ findSymbol: (input) => call('findSymbol', input),
25
+ graph: (input) => call('graph', input),
26
+ retrieve: (input) => call('retrieve', input),
27
+ snapshot: (input) => call('snapshot', input),
28
+ });
29
+ }
30
+
31
+ export function describeRepositoryKnowledgeProvider(capabilities = {}) {
32
+ return Object.freeze({
33
+ structure: capabilities.structure || 'unknown',
34
+ lexical: capabilities.lexical !== false,
35
+ semantic: Boolean(capabilities.semantic),
36
+ graph: Boolean(capabilities.graph),
37
+ history: Boolean(capabilities.history),
38
+ evidence: capabilities.evidence || 'unknown',
39
+ provider: capabilities.provider || 'custom',
40
+ });
41
+ }
@@ -50,9 +50,9 @@ export function validateConfig(input) {
50
50
  if (c.storage.driver === 'postgres' && !/^[A-Z_][A-Z0-9_]*$/.test(c.storage.connection_env || '')) throw new Error('Postgres requires a connection_env name, never a connection string in config.');
51
51
  return c;
52
52
  }
53
- export function defaultConfig({ include = ['.'], exclude = [], scope = 'default', target = 'local', dimensions = 768 } = {}) {
53
+ export function defaultConfig({ include = ['.'], exclude = [], scope = 'default', target = 'local', dimensions = 768, repositoryId = '' } = {}) {
54
54
  if (!['local', 'production'].includes(target)) throw new Error('target must be local or production.');
55
- return validateConfig({ version: 1, repository_id: randomUUID(),
55
+ return validateConfig({ version: 1, repository_id: String(repositoryId || '').trim() || randomUUID(),
56
56
  scope: { name: scope, include, exclude }, chunking: { max_chars: 4000 },
57
57
  embedding: { provider: 'gemini', model: 'gemini-embedding-2', revision: '1', dimensions, parameters: { task: 'code retrieval' } },
58
58
  storage: target === 'local' ? { driver: 'sqlite' } : { driver: 'postgres', connection_env: 'AGENTSAM_DATABASE_URL' } });
@@ -1,12 +1,23 @@
1
1
  export function createContextPack({ queryId, query, hits = [], diagnostics = {}, confidence = "unknown" }) {
2
2
  const normalizedHits = hits.map((hit) => Object.freeze({ stale: false, reasons: [], metadata: {}, ...hit }));
3
+ const estimatedTokens = estimateTokens(normalizedHits);
4
+ const chars = normalizedHits.reduce((sum, hit) => sum + String(hit.content || "").length, 0);
5
+ const considered = Number.isInteger(diagnostics.sources_considered) ? diagnostics.sources_considered : normalizedHits.length;
6
+ const deferred = Number.isInteger(diagnostics.sources_deferred) ? diagnostics.sources_deferred : Math.max(0, considered - normalizedHits.length);
3
7
  return Object.freeze({
4
8
  query_id: queryId,
5
9
  query,
6
10
  hits: normalizedHits,
7
- estimated_tokens: estimateTokens(normalizedHits),
11
+ estimated_tokens: estimatedTokens,
8
12
  confidence,
9
13
  diagnostics: Object.freeze({ ...diagnostics }),
14
+ receipt: Object.freeze({
15
+ chars,
16
+ estimated_tokens: estimatedTokens,
17
+ sources_considered: considered,
18
+ sources_included: normalizedHits.length,
19
+ sources_deferred: deferred,
20
+ }),
10
21
  });
11
22
  }
12
23
 
@@ -1,5 +1,7 @@
1
1
  /** Provider-neutral AgentSam Knowledge SDK contracts and validators. */
2
2
 
3
+ import { normalizeResultPolicy } from '../context/result-policy.js';
4
+
3
5
  export const KNOWLEDGE_OPERATIONS = Object.freeze({
4
6
  RETRIEVE: "knowledge.retrieve",
5
7
  INDEX: "knowledge.index",
@@ -9,10 +11,13 @@ export function assertRetrievalQuery(value) {
9
11
  if (!value || typeof value !== "object") throw new TypeError("RetrievalQuery must be an object");
10
12
  if (!String(value.text || "").trim()) throw new TypeError("RetrievalQuery.text is required");
11
13
  if (value.workspace_id != null && !String(value.workspace_id).trim()) throw new TypeError("RetrievalQuery.workspace_id must be non-empty when present");
12
- const topK = value.top_k ?? 12;
13
- if (!Number.isInteger(topK) || topK < 1 || topK > 100) throw new RangeError("top_k must be 1..100");
14
- if (!Number.isInteger(value.token_budget ?? 8000) || (value.token_budget ?? 8000) < 256) throw new RangeError("token_budget must be at least 256");
15
- return { ...value, top_k: topK, token_budget: value.token_budget ?? 8000 };
14
+ const resultPolicy = normalizeResultPolicy(value.result_policy || {});
15
+ const topK = value.top_k ?? resultPolicy.max_items;
16
+ if (!Number.isInteger(topK) || topK < 1 || topK > resultPolicy.max_items) throw new RangeError(`top_k must be 1..${resultPolicy.max_items} under the active result policy`);
17
+ const defaultTokenBudget = Math.max(256, Math.floor(resultPolicy.max_chars / 4));
18
+ const tokenBudget = value.token_budget ?? defaultTokenBudget;
19
+ if (!Number.isInteger(tokenBudget) || tokenBudget < 256 || tokenBudget > defaultTokenBudget) throw new RangeError(`token_budget must be 256..${defaultTokenBudget} under the active result policy`);
20
+ return { ...value, result_policy: resultPolicy, top_k: topK, token_budget: tokenBudget };
16
21
  }
17
22
 
18
23
  export function assertContextPack(value) {
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { validateConfig, fingerprint, scopeKey, cacheNamespace, gitEvidence } from './config.js';
3
3
  import { PARSER, inventory, readSource, parseSource } from './source.js';
4
4
  import { createContextPack } from './context-pack.js';
5
+ import { normalizeResultPolicy } from '../context/result-policy.js';
5
6
 
6
7
  export const embeddingProfileId = profile => fingerprint({ ...profile, input_format: 'agentsam-code-content:1' });
7
8
  export function validateVector(vector, dimensions) {
@@ -72,10 +73,13 @@ export async function runIndex({ root, config, store, embedder, embed = false, m
72
73
  return { ...receipt, generation_id: generation.id, published: true };
73
74
  }
74
75
 
75
- export async function retrieve({ store, config: input, text, semantic = false, embedder, topK = 8, tokenBudget = 8000, generationId }) {
76
+ export async function retrieve({ store, config: input, text, semantic = false, embedder, topK = 8, tokenBudget = 6000, generationId, resultPolicy: requestedResultPolicy }) {
76
77
  const config = validateConfig(input);
77
78
  if (typeof text !== 'string' || !text.trim()) throw new Error('A nonempty query is required.');
78
- if (!Number.isInteger(topK) || topK < 1 || topK > 100 || !Number.isInteger(tokenBudget) || tokenBudget < 256) throw new Error('topK must be 1..100 and tokenBudget at least 256.');
79
+ const resultPolicy = normalizeResultPolicy(requestedResultPolicy || {});
80
+ if (!Number.isInteger(topK) || topK < 1 || topK > resultPolicy.max_items) throw new Error(`topK must be 1..${resultPolicy.max_items} under the active result policy.`);
81
+ const maxTokenBudget = Math.max(256, Math.floor(resultPolicy.max_chars / 4));
82
+ if (!Number.isInteger(tokenBudget) || tokenBudget < 256 || tokenBudget > maxTokenBudget) throw new Error(`tokenBudget must be 256..${maxTokenBudget} under the active result policy.`);
79
83
  const generation = generationId ? await store?.getGeneration(scopeKey(config), generationId) : await store?.active(scopeKey(config));
80
84
  if (!generation) throw new Error('No generation found for this repository/scope. Run agentsam index run.');
81
85
  let queryVector;
@@ -113,6 +117,6 @@ export async function retrieve({ store, config: input, text, semantic = false, e
113
117
  metadata: { generation_id: generation.id, symbol: hit.symbol, content_hash: hit.content_hash, freshness: 'working tree not checked' } }); estimatedTokens += tokens;
114
118
  if (hits.length === topK) break;
115
119
  }
116
- return createContextPack({ queryId: randomUUID(), query: { text, top_k: topK, token_budget: tokenBudget }, hits,
120
+ return createContextPack({ queryId: randomUUID(), query: { text, top_k: topK, token_budget: tokenBudget, result_policy: resultPolicy }, hits,
117
121
  diagnostics: { generation_id: generation.id, source_hash: generation.source_hash, scope: generation.config.scope, mode: semantic ? 'semantic-exact' : 'lexical', freshness: 'snapshot; working tree not checked' } });
118
122
  }
@@ -30,7 +30,7 @@ function normalizeRequest(body, repositories, allowEmbeddings) {
30
30
  };
31
31
  const request = { ...body, embed: body.embed ?? false, semantic: body.semantic ?? false,
32
32
  max_inputs: bounded('max_inputs', 100, 0, 1000), max_characters: bounded('max_characters', 200000, 0, 2000000),
33
- top_k: bounded('top_k', 8, 1, 100), token_budget: bounded('token_budget', 8000, 256, 32000) };
33
+ top_k: bounded('top_k', 8, 1, 8), token_budget: bounded('token_budget', 6000, 256, 6000) };
34
34
  const config = structuredClone(repo.config);
35
35
  if (body.scope !== undefined) config.scope.name = body.scope;
36
36
  if (body.include !== undefined) config.scope.include = body.include;
@@ -1,17 +1,19 @@
1
- const DEFAULT_BASE_URL = 'https://inneranimalmedia.com';
1
+ import {
2
+ resolveBridgeKey as resolveContractBridgeKey,
3
+ resolveIamOrigin,
4
+ } from '../../packages/identity/src/contracts/auth-config.js';
2
5
 
3
6
  function clean(value) {
4
7
  return value == null ? '' : String(value).trim();
5
8
  }
6
9
 
7
10
  export function resolveAgentSamBaseUrl(env = process.env, explicit = '') {
8
- return clean(
9
- explicit || env.AGENTSAM_BASE_URL || env.AGENTSAM_CORE_URL || env.IAM_CORE_URL || DEFAULT_BASE_URL,
10
- ).replace(/\/$/, '');
11
+ const compatibilityBase = env?.AGENTSAM_BASE_URL || env?.AGENTSAM_CORE_URL || env?.IAM_CORE_URL || '';
12
+ return resolveIamOrigin(env, explicit || env?.IAM_ORIGIN || compatibilityBase);
11
13
  }
12
14
 
13
15
  export function resolveBridgeKey(env = process.env, explicit = '') {
14
- return clean(explicit || env.AGENTSAM_BRIDGE_KEY);
16
+ return resolveContractBridgeKey(env, explicit);
15
17
  }
16
18
 
17
19
  /**
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { getProjectName, tryReadProjectConfig } from './project-config.js';
5
+
6
+ export const CLI_PREFERENCES_SCHEMA = 'agentsam-cli-preferences-v1';
7
+
8
+ function readJson(filename) {
9
+ try {
10
+ return JSON.parse(fs.readFileSync(filename, 'utf8'));
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ function gitValue(cwd, args) {
17
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
18
+ return result.status === 0 ? String(result.stdout || '').trim() : '';
19
+ }
20
+
21
+ export function findCliProjectRoot(startDir = process.cwd()) {
22
+ const cwd = path.resolve(startDir);
23
+ const gitRoot = gitValue(cwd, ['rev-parse', '--show-toplevel']);
24
+ if (gitRoot) return path.resolve(gitRoot);
25
+
26
+ let dir = cwd;
27
+ for (let i = 0; i < 16; i += 1) {
28
+ if (
29
+ fs.existsSync(path.join(dir, '.agentsam', 'cli.json')) ||
30
+ fs.existsSync(path.join(dir, '.agentsam', 'config.json')) ||
31
+ fs.existsSync(path.join(dir, 'package.json'))
32
+ ) return dir;
33
+ const parent = path.dirname(dir);
34
+ if (parent === dir) break;
35
+ dir = parent;
36
+ }
37
+ return cwd;
38
+ }
39
+
40
+ export function detectCliProject(startDir = process.cwd()) {
41
+ const root = findCliProjectRoot(startDir);
42
+ const config = tryReadProjectConfig(root) || {};
43
+ const pkg = readJson(path.join(root, 'package.json')) || {};
44
+ const branch = gitValue(root, ['branch', '--show-current']);
45
+ const remote = gitValue(root, ['config', '--get', 'remote.origin.url']);
46
+ const project = String(getProjectName(config, pkg.name || path.basename(root)));
47
+ const website = String(pkg.homepage || '').trim();
48
+ return { root, project, branch, remote, website, configured: Boolean(getProjectName(config)) };
49
+ }
50
+
51
+ export function cliPreferencesPath(root) {
52
+ return path.join(path.resolve(root), '.agentsam', 'cli.json');
53
+ }
54
+
55
+ export function readCliPreferences(root) {
56
+ const value = readJson(cliPreferencesPath(root));
57
+ if (!value || value.schemaVersion !== CLI_PREFERENCES_SCHEMA) return null;
58
+ return value;
59
+ }
60
+
61
+ export function writeCliPreferences(root, value = {}) {
62
+ const filename = cliPreferencesPath(root);
63
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
64
+ const next = {
65
+ schemaVersion: CLI_PREFERENCES_SCHEMA,
66
+ runtime: value.runtime || 'local',
67
+ terminal: value.terminal || '',
68
+ modelPreference: value.modelPreference || 'auto',
69
+ modelAuthority: 'preference-only',
70
+ updatedAt: new Date().toISOString(),
71
+ };
72
+ fs.writeFileSync(filename, `${JSON.stringify(next, null, 2)}\n`);
73
+ return next;
74
+ }
@@ -1,16 +1,16 @@
1
+ import { resolveIamOrigin } from '../../packages/identity/src/contracts/auth-config.js';
2
+
1
3
  /**
2
4
  * IAM CORE client — SDK is a delivery mechanism; intelligence lives server-side.
3
5
  *
4
- * Default base URL https://inneranimalmedia.com is CORRECT-AS-DESIGNED for
5
- * multi-tenant client serving (thin client → IAM CORE). Override with
6
- * IAM_CORE_URL / AGENTSAM_CORE_URL. Without a CORE backend, consumers get
7
- * scaffold/local CLI only.
6
+ * IAM_ORIGIN is the canonical platform authority/API origin. During the
7
+ * migration window IAM_CORE_URL / AGENTSAM_CORE_URL remain compatibility-only
8
+ * fallbacks. Without a CORE backend, consumers get scaffold/local CLI only.
8
9
  */
9
10
 
10
- const DEFAULT_CORE = 'https://inneranimalmedia.com';
11
-
12
- export function coreBaseUrl() {
13
- return (process.env.IAM_CORE_URL || process.env.AGENTSAM_CORE_URL || DEFAULT_CORE).replace(/\/$/, '');
11
+ export function coreBaseUrl(env = process.env) {
12
+ const explicit = env?.IAM_ORIGIN || env?.IAM_CORE_URL || env?.AGENTSAM_CORE_URL || '';
13
+ return resolveIamOrigin(env, explicit);
14
14
  }
15
15
 
16
16
  export async function postJson(path, body, token) {
@@ -146,7 +146,7 @@ export async function captureDeployReceipt({
146
146
  }
147
147
  }
148
148
 
149
- const tree = await buildMerkleTree(rootPath, { policy });
149
+ const tree = await buildMerkleTree(rootPath, { policy, semantic: true });
150
150
  await atomicJson(state.pendingSnapshot, tree);
151
151
 
152
152
  let diff = null;
@@ -163,7 +163,7 @@ export async function captureDeployReceipt({
163
163
  const receipt = {
164
164
  format: 'agentsam-deploy-merkle',
165
165
  version: 1,
166
- engine: 'agentsam-merkle-v1',
166
+ engine: tree.semantic ? 'agentsam-merkle-v1+agentsam-filemeta-v1' : 'agentsam-merkle-v1',
167
167
  status: 'captured',
168
168
  project: project || path.basename(rootPath),
169
169
  repository: gitInfo.repository || null,
@@ -171,6 +171,9 @@ export async function captureDeployReceipt({
171
171
  git_branch: gitInfo.git_branch || null,
172
172
  captured_at: new Date().toISOString(),
173
173
  root_hash: tree.rootHash,
174
+ policy_hash: tree.policyHash || null,
175
+ metadata_root: tree.semantic?.rootHash || null,
176
+ classifier: tree.semantic?.classifier || null,
174
177
  previous_root_hash: baseline?.rootHash || null,
175
178
  has_baseline: Boolean(baseline),
176
179
  baseline_source: resolvedBaselineSource,