@inneranimalmedia/agentsam-sdk 2.6.8 → 2.6.9

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 (104) hide show
  1. package/apps/README.md +25 -0
  2. package/apps/cad-creator/shared/cad/package.json +1 -1
  3. package/apps/client-cms-editor/backend/package.json +4 -6
  4. package/apps/client-cms-editor/frontend/package.json +6 -8
  5. package/apps/client-cms-editor/frontend/tsconfig.json +27 -8
  6. package/apps/client-cms-editor/package.json +1 -1
  7. package/apps/client-cms-editor/shared/cms/package.json +4 -6
  8. package/docs/LOCAL_ENGINE_COMPUTE_AUDIT_PLAN.md +299 -0
  9. package/package.json +18 -7
  10. package/packages/agentsam-brand/package.json +1 -1
  11. package/packages/agentsam-contracts/package.json +71 -20
  12. package/packages/agentsam-contracts/scripts/build.mjs +18 -0
  13. package/packages/agentsam-contracts/test/contracts.test.mjs +4 -1
  14. package/packages/agentsam-contracts/test/repository.test.mjs +4 -1
  15. package/packages/agentsam-contracts/tsconfig.build.json +10 -0
  16. package/packages/agentsam-errors/package.json +3 -2
  17. package/packages/agentsam-goap/package.json +2 -2
  18. package/packages/agentsam-knowledge/package.json +3 -2
  19. package/packages/agentsam-nav/package.json +1 -1
  20. package/packages/agentsam-queue-control/package.json +1 -1
  21. package/packages/agentsam-rapid-rust/src/main.rs +1 -0
  22. package/packages/agentsam-repository/package.json +3 -2
  23. package/packages/agentsam-vault/package.json +8 -5
  24. package/packages/agentsam-workbench/package.json +44 -17
  25. package/packages/connectors/cloudflare/package.json +1 -1
  26. package/packages/heuristic-theme/package.json +1 -1
  27. package/packages/identity/package.json +4 -3
  28. package/packages/identity/tests/lane1-packed-consumer.test.mjs +9 -1
  29. package/packages/providers/completeful/package.json +1 -1
  30. package/packages/theme-church-site/README.md +2 -2
  31. package/packages/theme-church-site/package.json +3 -4
  32. package/packages/theme-church-site/src/index.js +1 -1
  33. package/packages/theme-church-site/test/theme.test.mjs +1 -1
  34. package/packages/theme-companions-site/README.md +2 -2
  35. package/packages/theme-companions-site/package.json +3 -4
  36. package/packages/theme-companions-site/src/index.js +1 -1
  37. package/packages/theme-companions-site/test/theme.test.mjs +1 -1
  38. package/packages/theme-floors-site/README.md +2 -2
  39. package/packages/theme-floors-site/package.json +3 -4
  40. package/packages/theme-floors-site/src/index.js +1 -1
  41. package/packages/theme-floors-site/test/theme.test.mjs +1 -1
  42. package/packages/theme-fuelnfree-site/README.md +2 -2
  43. package/packages/theme-fuelnfree-site/package.json +3 -4
  44. package/packages/theme-fuelnfree-site/src/index.js +1 -1
  45. package/packages/theme-fuelnfree-site/test/theme.test.mjs +1 -1
  46. package/packages/theme-handyman-site/README.md +2 -2
  47. package/packages/theme-handyman-site/package.json +3 -4
  48. package/packages/theme-handyman-site/src/index.js +1 -1
  49. package/packages/theme-handyman-site/test/theme.test.mjs +1 -1
  50. package/packages/theme-insurance-site/README.md +2 -2
  51. package/packages/theme-insurance-site/package.json +3 -4
  52. package/packages/theme-insurance-site/src/index.js +1 -1
  53. package/packages/theme-insurance-site/test/theme.test.mjs +1 -1
  54. package/packages/theme-scenes/package.json +1 -1
  55. package/packages/theme-shinshu-site/README.md +2 -2
  56. package/packages/theme-shinshu-site/package.json +3 -4
  57. package/packages/theme-shinshu-site/src/index.js +1 -1
  58. package/packages/theme-shinshu-site/test/theme.test.mjs +1 -1
  59. package/packages/work-graph/package.json +3 -2
  60. package/protocol/local-engine/benchmark.v1.schema.json +14 -0
  61. package/protocol/local-engine/engine.v1.schema.json +17 -0
  62. package/src/cli/command-catalog.js +1 -0
  63. package/src/cli.js +10 -0
  64. package/src/commands/app.js +10 -0
  65. package/src/commands/engine.js +10 -0
  66. package/src/commands/machine.js +71 -38
  67. package/src/commands/package.js +10 -3
  68. package/src/commands/runtime.js +25 -10
  69. package/src/commands/setup.js +30 -6
  70. package/src/commands/terminal.js +20 -8
  71. package/src/errors/contract.js +4 -4
  72. package/src/errors/diagnostic.js +1 -1
  73. package/src/errors/index.js +1 -1
  74. package/src/index.js +20 -0
  75. package/src/indexing/ingest/materials.js +237 -30
  76. package/src/lib/setup/runtime.js +4 -2
  77. package/src/local-engine/assets.js +37 -0
  78. package/src/local-engine/audit.js +133 -0
  79. package/src/local-engine/benchmark.js +26 -0
  80. package/src/local-engine/contracts.js +101 -0
  81. package/src/local-engine/engines.js +142 -0
  82. package/src/local-engine/index.js +18 -0
  83. package/src/local-engine/runtime.js +39 -0
  84. package/packages/agentsam-nav/dist/Nav.d.ts +0 -43
  85. package/packages/agentsam-nav/dist/NavCollapsible.d.ts +0 -10
  86. package/packages/agentsam-nav/dist/NavContent.d.ts +0 -2
  87. package/packages/agentsam-nav/dist/NavFooter.d.ts +0 -2
  88. package/packages/agentsam-nav/dist/NavGroup.d.ts +0 -3
  89. package/packages/agentsam-nav/dist/NavHeader.d.ts +0 -2
  90. package/packages/agentsam-nav/dist/NavMenu.d.ts +0 -22
  91. package/packages/agentsam-nav/dist/NavProvider.d.ts +0 -35
  92. package/packages/agentsam-nav/dist/NavTrigger.d.ts +0 -2
  93. package/packages/agentsam-nav/dist/Surfaces.d.ts +0 -41
  94. package/packages/agentsam-nav/dist/browser.d.ts +0 -11
  95. package/packages/agentsam-nav/dist/browser.global.js +0 -135
  96. package/packages/agentsam-nav/dist/browser.js +0 -7290
  97. package/packages/agentsam-nav/dist/contracts.d.ts +0 -76
  98. package/packages/agentsam-nav/dist/index.d.ts +0 -11
  99. package/packages/agentsam-nav/dist/index.js +0 -7291
  100. package/packages/agentsam-nav/dist/legacy.css +0 -83
  101. package/packages/agentsam-nav/dist/shell.css +0 -113
  102. package/packages/agentsam-nav/dist/theme.css +0 -113
  103. package/packages/agentsam-nav/dist/utils.d.ts +0 -1
  104. package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +0 -670
@@ -139,7 +139,7 @@ async function runSetupRuntime(argv = [], options = {}) {
139
139
  if (dryRun || !yes) {
140
140
  writeLine(write, ` Plan receipt ${planPath}`);
141
141
  if (!yes) {
142
- writeLine(write, ' Pass --yes to install agentsamd (when profile needs it) and write install receipt.');
142
+ writeLine(write, ' Pass --yes to apply the selected runtime profile; healthy agentsamd installs are reused.');
143
143
  }
144
144
  writeLine(write, '');
145
145
  return 0;
@@ -154,9 +154,26 @@ async function runSetupRuntime(argv = [], options = {}) {
154
154
 
155
155
  let installResult = null;
156
156
  if (recommended.target.runtime_adapter === 'agentsamd') {
157
- const code = await runRuntime(['install', '--yes'], { write, home });
158
- installResult = { code, adapter: 'agentsamd' };
159
- if (code !== 0) return code;
157
+ if (plan.facts.agentsamd.installed) {
158
+ const doctorCode = await runRuntime(['doctor', '--json'], { write: () => {}, home });
159
+ if (doctorCode === 0) {
160
+ installResult = { code: 0, adapter: 'agentsamd', skipped: true, reason: 'already_installed_healthy' };
161
+ writeLine(write, ' ✓ agentsamd already installed and healthy; reusing current runtime');
162
+ } else {
163
+ const startCode = await runRuntime(['start'], { write, home });
164
+ if (startCode === 0) {
165
+ installResult = { code: 0, adapter: 'agentsamd', skipped: true, reason: 'existing_runtime_restarted' };
166
+ } else {
167
+ const code = await runRuntime(['install', '--yes'], { write, home });
168
+ installResult = { code, adapter: 'agentsamd', repaired: true };
169
+ if (code !== 0) return code;
170
+ }
171
+ }
172
+ } else {
173
+ const code = await runRuntime(['install', '--yes'], { write, home });
174
+ installResult = { code, adapter: 'agentsamd' };
175
+ if (code !== 0) return code;
176
+ }
160
177
  }
161
178
 
162
179
  const receiptPath = writeRuntimeInstallReceipt({
@@ -164,7 +181,12 @@ async function runSetupRuntime(argv = [], options = {}) {
164
181
  profile_id: recommended.profile.id,
165
182
  target: recommended.target,
166
183
  goap: recommended.goap,
167
- install: installResult,
184
+ goap_execution: {
185
+ status: 'planned_only',
186
+ executed_action_ids: [],
187
+ pending_action_ids: recommended.goap.action_ids || [],
188
+ },
189
+ adapter_apply: installResult,
168
190
  plan_receipt: planPath,
169
191
  }, home);
170
192
 
@@ -175,7 +197,9 @@ async function runSetupRuntime(argv = [], options = {}) {
175
197
  plan,
176
198
  plan_receipt: planPath,
177
199
  install_receipt: receiptPath,
178
- executed: true,
200
+ adapter_applied: true,
201
+ goap_execution: 'planned_only',
202
+ executed: false,
179
203
  }, null, 2) + '\n');
180
204
  }
181
205
  return 0;
@@ -30,6 +30,21 @@ function printIdentity(identity, write) {
30
30
  write(`hw_model: ${identity.model || 'unknown'}`);
31
31
  }
32
32
 
33
+ function terminalUsage() {
34
+ return [
35
+ 'agentsam terminal — device identity and ExecOS enrollment',
36
+ '',
37
+ 'Usage:',
38
+ ' agentsam terminal identity [--json]',
39
+ ' agentsam terminal enroll --instance <id> [--endpoint <url>] [--pair] [--json]',
40
+ ' agentsam terminal --help',
41
+ '',
42
+ 'Identity lanes:',
43
+ ' AGENTSAM_API_KEY account/CLI identity used to mint enrollment',
44
+ ' AGENTSAM_BRIDGE_KEY one terminal_connection credential written by --pair',
45
+ ].join('\n');
46
+ }
47
+
33
48
  function defaultExecosEnrollBin() {
34
49
  return path.join(homedir(), 'ExecOS', 'bin', 'enroll.mjs');
35
50
  }
@@ -52,6 +67,10 @@ function pairWithExecos(enrollmentToken, options = {}) {
52
67
 
53
68
  export async function runTerminal(argv = [], options = {}) {
54
69
  const write = options.write || ((line) => console.log(line));
70
+ if (argv.includes('--help') || argv.includes('-h') || argv.includes('help')) {
71
+ write(terminalUsage());
72
+ return { ok: true, help: true };
73
+ }
55
74
  const sub = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'identity';
56
75
  const json = argv.includes('--json') || options.json === true;
57
76
  const pair = argv.includes('--pair') || options.pair === true;
@@ -70,14 +89,7 @@ export async function runTerminal(argv = [], options = {}) {
70
89
  }
71
90
 
72
91
  if (sub !== 'enroll') {
73
- write([
74
- 'Usage: agentsam terminal identity [--json]',
75
- ' agentsam terminal enroll --instance <id> [--endpoint <url>] [--pair] [--json]',
76
- '',
77
- 'Lanes:',
78
- ' AGENTSAM_API_KEY account (CLI / whoami / mint enrollment) — required for this command',
79
- ' AGENTSAM_BRIDGE_KEY one terminal_connection secret (written by --pair into ExecOS profile)',
80
- ].join('\n'));
92
+ write(terminalUsage());
81
93
  return { ok: false, error: 'terminal_usage' };
82
94
  }
83
95
 
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  ERROR_CODE,
4
4
  ERROR_REASON,
5
- } from '../../packages/agentsam-errors/src/vocabulary.js';
6
- import { canonicalCodeFromHttpStatus } from '../../packages/agentsam-errors/src/envelope.js';
5
+ } from '@inneranimalmedia/agentsam-errors';
6
+ import { canonicalCodeFromHttpStatus } from '@inneranimalmedia/agentsam-errors';
7
7
 
8
8
  export {
9
9
  ERROR_SCHEMA_VERSION,
@@ -15,7 +15,7 @@ export {
15
15
  ERROR_DOMAIN,
16
16
  REMEDIATION_ACTION,
17
17
  GRPC_STATUS,
18
- } from '../../packages/agentsam-errors/src/vocabulary.js';
18
+ } from '@inneranimalmedia/agentsam-errors';
19
19
 
20
20
  export {
21
21
  canonicalCodeFromHttpStatus,
@@ -27,7 +27,7 @@ export {
27
27
  assertErrorEnvelope,
28
28
  serializeError,
29
29
  parseError,
30
- } from '../../packages/agentsam-errors/src/envelope.js';
30
+ } from '@inneranimalmedia/agentsam-errors';
31
31
 
32
32
  // Legacy compact classifier shapes remain public here; richer envelope classifiers
33
33
  // live in packages/agentsam-errors and are available via the other adapter exports.
@@ -6,7 +6,7 @@ import {
6
6
  normalizeError,
7
7
  redactErrorValue,
8
8
  redactString,
9
- } from '../../packages/agentsam-errors/src/index.js';
9
+ } from '@inneranimalmedia/agentsam-errors';
10
10
 
11
11
  const DEFAULT_MAX_DETAIL_CHARS = 12_000;
12
12
  function clean(value) { return value == null ? '' : String(value).trim(); }
@@ -1,4 +1,4 @@
1
- export * from '../../packages/agentsam-errors/src/index.js';
1
+ export * from '@inneranimalmedia/agentsam-errors';
2
2
 
3
3
  export {
4
4
  AgentSamDiagnosticError,
package/src/index.js CHANGED
@@ -77,6 +77,26 @@ export {
77
77
  terminalExec,
78
78
  } from './capabilities/index.js';
79
79
  export { getPreset, listPresets, resolvePreset, getAddon, listAddons } from './presets/index.js';
80
+ export {
81
+ LOCAL_ENGINE_SCHEMA,
82
+ LOCAL_ENGINE_BENCHMARK_SCHEMA,
83
+ ENGINE_CAPABILITIES,
84
+ LocalEngineRegistry,
85
+ createLocalEngineRegistry,
86
+ createOllamaEngine,
87
+ createMlxLmEngine,
88
+ createLlamaCppEngine,
89
+ auditLocalCompute,
90
+ auditHost,
91
+ auditAppleHost,
92
+ auditTools,
93
+ auditAssets,
94
+ adoptAsset,
95
+ readAssetManifest,
96
+ assetManifestPath,
97
+ benchmarkEngine,
98
+ validateStructuredOutput,
99
+ } from './local-engine/index.js';
80
100
  export {
81
101
  COMPANY_REPOSITORY_GRAPH_SCHEMA_VERSION,
82
102
  REPOSITORY_STATUSES,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Material intake for codebaseindex.ingest — paths pasted/dropped into the CLI.
3
- * Handles archives (tar/zip), sites/builds, HTML, images, GLB, and mixed trees.
3
+ * Archives are inspected first and selectively mined instead of blindly unpacked.
4
4
  */
5
5
 
6
6
  import fs from 'node:fs';
@@ -12,6 +12,20 @@ import { isCodeSourceExtension, isDocumentSourceExtension } from '../../../packa
12
12
  const ARCHIVE_EXT = new Set(['.zip', '.tar', '.tgz', '.gz', '.tar.gz', '.tbz2', '.tar.bz2']);
13
13
  const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif']);
14
14
  const MODEL_3D_EXT = new Set(['.glb', '.gltf', '.obj', '.fbx', '.stl', '.usdz']);
15
+ const VIDEO_EXT = new Set(['.mp4', '.mov', '.m4v', '.webm', '.mkv', '.avi', '.mpeg', '.mpg']);
16
+ const AUDIO_EXT = new Set(['.mp3', '.wav', '.m4a', '.aac', '.ogg', '.flac']);
17
+ const TEXT_BASENAMES = new Set([
18
+ 'readme', 'license', 'licence', 'dockerfile', 'makefile', 'procfile', 'gemfile', 'rakefile',
19
+ 'cargo.lock', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', '.gitignore', '.npmrc', '.editorconfig',
20
+ ]);
21
+ const DEFAULT_ARCHIVE_POLICY = Object.freeze({
22
+ maxEntries: 10_000,
23
+ maxFiles: 2_000,
24
+ maxEntryBytes: 8 * 1024 * 1024,
25
+ maxTotalBytes: 128 * 1024 * 1024,
26
+ includeAssets: false,
27
+ });
28
+
15
29
  function clean(value) {
16
30
  return value == null ? '' : String(value).trim();
17
31
  }
@@ -43,9 +57,7 @@ export function parsePastedPaths(text) {
43
57
  return [...new Set(paths)];
44
58
  }
45
59
 
46
- /**
47
- * @param {string} filePath
48
- */
60
+ /** @param {string} filePath */
49
61
  export function classifyMaterial(filePath) {
50
62
  const resolved = path.resolve(filePath);
51
63
  const ext = extOf(resolved);
@@ -79,37 +91,237 @@ function hashFile(filePath) {
79
91
 
80
92
  function hasBin(name) {
81
93
  const result = spawnSync(name, ['--help'], { stdio: 'ignore' });
82
- // Some tools exit non-zero on --help; presence of spawn failure matters more.
83
94
  return result.error == null;
84
95
  }
85
96
 
97
+ function archiveFormat(archivePath) {
98
+ const lower = archivePath.toLowerCase();
99
+ if (lower.endsWith('.zip')) return 'zip';
100
+ if (lower.endsWith('.tar') || lower.endsWith('.tar.gz') || lower.endsWith('.tgz')
101
+ || lower.endsWith('.tar.bz2') || lower.endsWith('.tbz2')) return 'tar';
102
+ if (lower.endsWith('.gz')) return 'gzip';
103
+ return null;
104
+ }
105
+
106
+ function archiveEntryKind(entryPath) {
107
+ const normalized = String(entryPath || '').replace(/\\/g, '/');
108
+ const ext = path.posix.extname(normalized.toLowerCase());
109
+ const base = path.posix.basename(normalized).toLowerCase();
110
+ if (IMAGE_EXT.has(ext)) return 'image';
111
+ if (MODEL_3D_EXT.has(ext)) return 'model3d';
112
+ if (VIDEO_EXT.has(ext)) return 'video';
113
+ if (AUDIO_EXT.has(ext)) return 'audio';
114
+ if (isDocumentSourceExtension(ext) || TEXT_BASENAMES.has(base)) return 'document';
115
+ if (isCodeSourceExtension(ext)) return 'code';
116
+ return 'file';
117
+ }
118
+
119
+ function isSafeArchivePath(entryPath) {
120
+ const raw = String(entryPath || '').replace(/\\/g, '/');
121
+ if (!raw || raw.includes('\0') || raw.startsWith('/') || /^[a-zA-Z]:\//.test(raw)) return false;
122
+ const parts = raw.split('/').filter((part) => part && part !== '.');
123
+ return !parts.includes('..');
124
+ }
125
+
126
+ function shouldMineArchiveEntry(entry, policy) {
127
+ if (!entry.safe) return { selected: false, reason: 'unsafe_path' };
128
+ if (entry.link) return { selected: false, reason: 'link_entry' };
129
+ if (entry.directory) return { selected: false, reason: 'directory' };
130
+ if (Number.isFinite(entry.bytes) && entry.bytes > policy.maxEntryBytes) {
131
+ return { selected: false, reason: 'entry_too_large' };
132
+ }
133
+ if (entry.kind === 'code' || entry.kind === 'document') return { selected: true, reason: 'indexable_text' };
134
+ if (policy.includeAssets && ['image', 'model3d'].includes(entry.kind)) return { selected: true, reason: 'requested_asset' };
135
+ return { selected: false, reason: ['image', 'model3d', 'video', 'audio'].includes(entry.kind) ? 'asset_inventory_only' : 'non_indexable' };
136
+ }
137
+
138
+ function commandText(command, args, maxBuffer = 16 * 1024 * 1024) {
139
+ return execFileSync(command, args, {
140
+ encoding: 'utf8',
141
+ maxBuffer,
142
+ env: { ...process.env, LC_ALL: 'C' },
143
+ stdio: ['ignore', 'pipe', 'pipe'],
144
+ });
145
+ }
146
+
147
+ function zipSizeMap(archivePath) {
148
+ if (!hasBin('zipinfo')) return new Map();
149
+ const map = new Map();
150
+ const lines = commandText('zipinfo', ['-l', archivePath], 32 * 1024 * 1024).split(/\r?\n/);
151
+ for (const line of lines) {
152
+ const match = line.match(/^(\S+)\s+\S+\s+\S+\s+(\d+)\s+\S+\s+\d+\s+\S+\s+\S+\s+\S+\s+(.*)$/);
153
+ if (!match) continue;
154
+ map.set(match[3], { bytes: Number(match[2]), link: match[1].startsWith('l') });
155
+ }
156
+ return map;
157
+ }
158
+
86
159
  /**
87
- * Extract archive into destDir. Uses host tar/unzip (no extra npm deps).
160
+ * Inspect an archive without expanding it to disk.
161
+ * @param {string} archivePath
162
+ * @param {Partial<typeof DEFAULT_ARCHIVE_POLICY>} [options]
163
+ */
164
+ export function inspectArchive(archivePath, options = {}) {
165
+ const resolved = path.resolve(archivePath);
166
+ const policy = { ...DEFAULT_ARCHIVE_POLICY, ...options };
167
+ const format = archiveFormat(resolved);
168
+ if (!format) throw new Error(`unsupported_archive:${path.basename(resolved)}`);
169
+ if (!fs.existsSync(resolved)) throw new Error(`archive_missing:${resolved}`);
170
+
171
+ let names = [];
172
+ let zipMeta = new Map();
173
+ let tarLinks = new Set();
174
+ if (format === 'zip') {
175
+ if (!hasBin('unzip')) throw new Error('unzip_unavailable: install unzip to inspect .zip materials');
176
+ names = commandText('unzip', ['-Z1', resolved], 32 * 1024 * 1024).split(/\r?\n/).filter(Boolean);
177
+ zipMeta = zipSizeMap(resolved);
178
+ } else if (format === 'tar') {
179
+ if (!hasBin('tar')) throw new Error('tar_unavailable: install tar to inspect tar materials');
180
+ names = commandText('tar', ['-tf', resolved], 32 * 1024 * 1024).split(/\r?\n/).filter(Boolean);
181
+ const verbose = commandText('tar', ['-tvf', resolved], 32 * 1024 * 1024).split(/\r?\n/);
182
+ for (const line of verbose) {
183
+ if (!(line.startsWith('l') || line.startsWith('h'))) continue;
184
+ const marker = line.includes(' -> ') ? line.split(' -> ')[0] : line;
185
+ const name = names.find((candidate) => marker.endsWith(candidate));
186
+ if (name) tarLinks.add(name);
187
+ }
188
+ } else {
189
+ if (!hasBin('gzip')) throw new Error('gzip_unavailable: install gzip to inspect .gz materials');
190
+ names = [path.basename(resolved, '.gz') || 'payload'];
191
+ }
192
+
193
+ if (names.length > policy.maxEntries) {
194
+ const error = new Error(`archive_entry_limit_exceeded:${names.length}>${policy.maxEntries}`);
195
+ error.code = 'AGENTSAM_ARCHIVE_ENTRY_LIMIT';
196
+ throw error;
197
+ }
198
+
199
+ const entries = names.map((name) => {
200
+ const meta = zipMeta.get(name) || {};
201
+ const directory = name.endsWith('/');
202
+ const entry = {
203
+ path: name,
204
+ bytes: Number.isFinite(meta.bytes) ? meta.bytes : null,
205
+ kind: directory ? 'directory' : archiveEntryKind(name),
206
+ directory,
207
+ link: Boolean(meta.link || tarLinks.has(name)),
208
+ safe: isSafeArchivePath(name),
209
+ };
210
+ return { ...entry, ...shouldMineArchiveEntry(entry, policy) };
211
+ });
212
+
213
+ const unsafe = entries.filter((entry) => !entry.safe);
214
+ if (unsafe.length) {
215
+ const error = new Error(`archive_unsafe_paths:${unsafe.slice(0, 5).map((entry) => entry.path).join(',')}`);
216
+ error.code = 'AGENTSAM_ARCHIVE_UNSAFE_PATH';
217
+ throw error;
218
+ }
219
+
220
+ const selected = entries.filter((entry) => entry.selected);
221
+ const selectedKnownBytes = selected.reduce((sum, entry) => sum + (entry.bytes || 0), 0);
222
+ const byKind = {};
223
+ for (const entry of entries) byKind[entry.kind] = (byKind[entry.kind] || 0) + 1;
224
+
225
+ return {
226
+ schema: 'agentsam.archive.inspect.v1',
227
+ archive: resolved,
228
+ format,
229
+ compressed_bytes: fs.statSync(resolved).size,
230
+ entries_total: entries.length,
231
+ selected_total: selected.length,
232
+ selected_known_bytes: selectedKnownBytes,
233
+ by_kind: byKind,
234
+ policy,
235
+ entries,
236
+ };
237
+ }
238
+
239
+ function readArchiveEntry(archivePath, format, entry, maxBytes) {
240
+ const maxBuffer = Math.max(1024 * 1024, maxBytes + 1);
241
+ if (format === 'zip') {
242
+ return execFileSync('unzip', ['-p', archivePath, entry], { encoding: null, maxBuffer, stdio: ['ignore', 'pipe', 'pipe'] });
243
+ }
244
+ if (format === 'tar') {
245
+ return execFileSync('tar', ['-xOf', archivePath, entry], { encoding: null, maxBuffer, stdio: ['ignore', 'pipe', 'pipe'] });
246
+ }
247
+ return execFileSync('gzip', ['-dc', archivePath], { encoding: null, maxBuffer, stdio: ['ignore', 'pipe', 'pipe'] });
248
+ }
249
+
250
+ /**
251
+ * Selectively mine indexable content from an archive into destDir.
252
+ * Large/binary/media members remain represented in the receipt but are not copied.
88
253
  * @param {string} archivePath
89
254
  * @param {string} destDir
255
+ * @param {Partial<typeof DEFAULT_ARCHIVE_POLICY>} [options]
90
256
  */
91
- export function extractArchive(archivePath, destDir) {
257
+ export function extractArchive(archivePath, destDir, options = {}) {
258
+ const policy = { ...DEFAULT_ARCHIVE_POLICY, ...options };
259
+ const inspection = inspectArchive(archivePath, policy);
260
+ fs.rmSync(destDir, { recursive: true, force: true });
92
261
  fs.mkdirSync(destDir, { recursive: true });
93
- const lower = archivePath.toLowerCase();
94
- if (lower.endsWith('.zip')) {
95
- if (!hasBin('unzip')) throw new Error('unzip_unavailable: install unzip to extract .zip materials');
96
- execFileSync('unzip', ['-q', '-o', archivePath, '-d', destDir], { stdio: ['ignore', 'pipe', 'pipe'] });
97
- return { tool: 'unzip', dest: destDir };
98
- }
99
- if (lower.endsWith('.tar') || lower.endsWith('.tar.gz') || lower.endsWith('.tgz')
100
- || lower.endsWith('.tar.bz2') || lower.endsWith('.tbz2') || lower.endsWith('.gz')) {
101
- if (!hasBin('tar')) throw new Error('tar_unavailable: install tar to extract archive materials');
102
- execFileSync('tar', ['-xf', archivePath, '-C', destDir], { stdio: ['ignore', 'pipe', 'pipe'] });
103
- return { tool: 'tar', dest: destDir };
262
+
263
+ const selected = inspection.entries.filter((entry) => entry.selected);
264
+ const mined = [];
265
+ const skipped = inspection.entries.filter((entry) => !entry.selected).map((entry) => ({
266
+ path: entry.path, kind: entry.kind, bytes: entry.bytes, reason: entry.reason,
267
+ }));
268
+ let totalBytes = 0;
269
+
270
+ for (const entry of selected) {
271
+ if (mined.length >= policy.maxFiles) {
272
+ skipped.push({ path: entry.path, kind: entry.kind, bytes: entry.bytes, reason: 'file_limit' });
273
+ continue;
274
+ }
275
+ if (Number.isFinite(entry.bytes) && totalBytes + entry.bytes > policy.maxTotalBytes) {
276
+ skipped.push({ path: entry.path, kind: entry.kind, bytes: entry.bytes, reason: 'total_byte_limit' });
277
+ continue;
278
+ }
279
+
280
+ let payload;
281
+ try {
282
+ payload = readArchiveEntry(inspection.archive, inspection.format, entry.path, policy.maxEntryBytes);
283
+ } catch (cause) {
284
+ skipped.push({ path: entry.path, kind: entry.kind, bytes: entry.bytes, reason: 'extract_error', error: String(cause?.message || cause).slice(0, 240) });
285
+ continue;
286
+ }
287
+ if (payload.length > policy.maxEntryBytes || totalBytes + payload.length > policy.maxTotalBytes) {
288
+ skipped.push({ path: entry.path, kind: entry.kind, bytes: payload.length, reason: payload.length > policy.maxEntryBytes ? 'entry_too_large' : 'total_byte_limit' });
289
+ continue;
290
+ }
291
+
292
+ const normalized = entry.path.replace(/\\/g, '/').replace(/^\.\//, '');
293
+ const output = path.resolve(destDir, ...normalized.split('/'));
294
+ const root = path.resolve(destDir);
295
+ if (!(output === root || output.startsWith(`${root}${path.sep}`))) {
296
+ throw new Error(`archive_unsafe_output:${entry.path}`);
297
+ }
298
+ fs.mkdirSync(path.dirname(output), { recursive: true });
299
+ fs.writeFileSync(output, payload);
300
+ totalBytes += payload.length;
301
+ mined.push({ path: entry.path, kind: entry.kind, bytes: payload.length });
104
302
  }
105
- throw new Error(`unsupported_archive:${path.basename(archivePath)}`);
303
+
304
+ return {
305
+ schema: 'agentsam.archive.mine.v1',
306
+ format: inspection.format,
307
+ dest: destDir,
308
+ entries_total: inspection.entries_total,
309
+ mined_files: mined.length,
310
+ mined_bytes: totalBytes,
311
+ skipped_files: skipped.length,
312
+ by_kind: inspection.by_kind,
313
+ policy,
314
+ mined,
315
+ skipped: skipped.slice(0, 200),
316
+ skipped_truncated: skipped.length > 200,
317
+ };
106
318
  }
107
319
 
108
320
  /**
109
321
  * Stage pasted/dropped materials under `.agentsam/ingest/<id>/`.
110
- * Archives are extracted; files/dirs are copied or linked by relative include paths.
322
+ * Archives are inspected + selectively mined; files/dirs are copied or linked by relative include paths.
111
323
  *
112
- * @param {{ root: string, materials: string[], ingestId?: string }} opts
324
+ * @param {{ root: string, materials: string[], ingestId?: string, archivePolicy?: object }} opts
113
325
  */
114
326
  export function stageMaterials(opts) {
115
327
  const root = path.resolve(opts.root);
@@ -117,11 +329,8 @@ export function stageMaterials(opts) {
117
329
  const stageRoot = path.join(root, '.agentsam', 'ingest', ingestId);
118
330
  fs.mkdirSync(stageRoot, { recursive: true });
119
331
 
120
- /** @type {object[]} */
121
332
  const items = [];
122
- /** @type {string[]} */
123
333
  const include = [];
124
- /** @type {object[]} */
125
334
  const assets = [];
126
335
 
127
336
  for (const raw of opts.materials) {
@@ -133,11 +342,10 @@ export function stageMaterials(opts) {
133
342
 
134
343
  if (classified.kind === 'archive') {
135
344
  const dest = path.join(stageRoot, 'archives', path.basename(classified.path, classified.ext) || 'archive');
136
- fs.mkdirSync(dest, { recursive: true });
137
- const extracted = extractArchive(classified.path, dest);
345
+ const extracted = extractArchive(classified.path, dest, opts.archivePolicy || {});
138
346
  const rel = path.relative(root, dest);
139
- include.push(rel);
140
- items.push({ ...classified, status: 'extracted', stage: dest, extract: extracted });
347
+ if (extracted.mined_files > 0) include.push(rel);
348
+ items.push({ ...classified, status: 'mined', stage: dest, extract: extracted });
141
349
  continue;
142
350
  }
143
351
 
@@ -158,7 +366,6 @@ export function stageMaterials(opts) {
158
366
  continue;
159
367
  }
160
368
 
161
- // Single file
162
369
  const destDir = path.join(stageRoot, classified.kind === 'image' ? 'images'
163
370
  : classified.kind === 'model3d' ? 'models3d'
164
371
  : classified.kind === 'document' ? 'documents'
@@ -180,7 +387,7 @@ export function stageMaterials(opts) {
180
387
 
181
388
  const uniqueInclude = [...new Set(include.map((p) => p.replace(/\\/g, '/').replace(/\/$/, '') || '.'))].sort();
182
389
  const manifest = {
183
- schema: 'agentsam.ingest.materials.v1',
390
+ schema: 'agentsam.ingest.materials.v2',
184
391
  ingest_id: ingestId,
185
392
  staged_at: new Date().toISOString(),
186
393
  stage_root: path.relative(root, stageRoot),
@@ -380,14 +380,16 @@ export function renderRuntimePlan(plan) {
380
380
  lines.push(` missing tools: ${c.constraints.missing_tools.join(', ')}`);
381
381
  }
382
382
  } else if (c.goap.action_ids.length) {
383
- lines.push(` plan: ${c.goap.action_ids.join(' → ')}`);
383
+ lines.push(` target plan: ${c.goap.action_ids.join(' → ')}`);
384
384
  }
385
385
  }
386
386
  lines.push('');
387
387
  if (plan.recommended) {
388
388
  lines.push(` Recommended ${plan.recommended.profile.label} (${plan.recommended.profile.id})`);
389
389
  lines.push(` Next agentsam setup runtime --profile ${plan.recommended.profile.id} --yes`);
390
- lines.push(` agentsam runtime install`);
390
+ lines.push(plan.facts.agentsamd.installed
391
+ ? ' agentsam runtime status'
392
+ : ' agentsam runtime install');
391
393
  }
392
394
  lines.push('');
393
395
  return `${lines.join('\n')}\n`;
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+
6
+ export const ASSET_MANIFEST_SCHEMA = 'agentsam.local-model-assets.v1';
7
+ export function agentsamHome(env = process.env) {
8
+ const configured = String(env.AGENTSAM_HOME || '').trim();
9
+ return configured ? path.resolve(configured.replace(/^~(?=\/|$)/, os.homedir())) : path.join(os.homedir(), '.agentsam');
10
+ }
11
+ export function assetManifestPath(env = process.env) { return path.join(agentsamHome(env), 'models', 'manifest.v1.json'); }
12
+ function safeStat(file) { try { return fs.statSync(file); } catch { return null; } }
13
+ function fileId(stat) { return stat ? `${stat.dev}:${stat.ino}` : null; }
14
+ export function readAssetManifest(env = process.env) {
15
+ const file = assetManifestPath(env);
16
+ if (!fs.existsSync(file)) return { schema_version: ASSET_MANIFEST_SCHEMA, generated_at: null, assets: [] };
17
+ try { const parsed = JSON.parse(fs.readFileSync(file, 'utf8')); return parsed?.schema_version === ASSET_MANIFEST_SCHEMA ? parsed : { schema_version: ASSET_MANIFEST_SCHEMA, generated_at: null, assets: [] }; } catch { return { schema_version: ASSET_MANIFEST_SCHEMA, generated_at: null, assets: [] }; }
18
+ }
19
+ function writeManifest(manifest, env = process.env) {
20
+ const file = assetManifestPath(env); fs.mkdirSync(path.dirname(file), { recursive: true });
21
+ const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, JSON.stringify(manifest, null, 2) + '\n', { mode: 0o600 }); fs.renameSync(temp, file); return file;
22
+ }
23
+ export function adoptAsset(file, metadata = {}, options = {}) {
24
+ const absolute = path.resolve(file); const stat = safeStat(absolute);
25
+ if (!stat || !stat.isFile()) throw new Error(`model asset is not a regular file: ${absolute}`);
26
+ const env = options.env || process.env; const manifest = readAssetManifest(env);
27
+ const asset = { id: metadata.id || crypto.createHash('sha256').update(`${absolute}:${stat.size}:${stat.mtimeMs}`).digest('hex').slice(0, 16), engine_id: metadata.engine_id || null, model: metadata.model || path.basename(absolute), format: metadata.format || path.extname(absolute).slice(1) || 'unknown', canonical_path: absolute, reference: 'external', size_bytes: stat.size, file_id: fileId(stat), digest: metadata.digest || null, seen_at: new Date().toISOString() };
28
+ const assets = manifest.assets.filter((row) => row.canonical_path !== absolute); assets.push(asset);
29
+ const next = { ...manifest, schema_version: ASSET_MANIFEST_SCHEMA, generated_at: new Date().toISOString(), assets }; const manifestPath = writeManifest(next, env);
30
+ return { asset, manifest_path: manifestPath, copied: false };
31
+ }
32
+ export function auditAssets(options = {}) {
33
+ const env = options.env || process.env; const manifest = readAssetManifest(env);
34
+ const assets = manifest.assets.map((asset) => { const stat = safeStat(asset.canonical_path); return { ...asset, exists: Boolean(stat), current_size_bytes: stat?.size || null, current_file_id: fileId(stat), stale: !stat || stat.size !== asset.size_bytes }; });
35
+ const byFile = new Map(); for (const asset of assets) if (asset.file_id) byFile.set(asset.file_id, [...(byFile.get(asset.file_id) || []), asset.id]);
36
+ return { schema_version: ASSET_MANIFEST_SCHEMA, generated_at: new Date().toISOString(), root: path.join(agentsamHome(env), 'models'), assets, duplicates: [...byFile.entries()].filter(([, ids]) => ids.length > 1).map(([file_id, ids]) => ({ file_id, asset_ids: ids })) };
37
+ }