@inneranimalmedia/agentsam-sdk 2.1.0 → 2.2.1

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 (50) hide show
  1. package/README.md +27 -15
  2. package/docs/CAPABILITIES.md +93 -0
  3. package/docs/CLI_SHELL.md +10 -1
  4. package/docs/RELEASES.md +2 -1
  5. package/docs/portable-knowledge.md +4 -4
  6. package/package.json +7 -1
  7. package/packages/identity/package.json +1 -1
  8. package/packages/identity/src/frontend/auth-portal/README.md +1 -1
  9. package/packages/identity/src/index.js +2 -0
  10. package/protocol/capabilities/capability-manifest.schema.json +36 -0
  11. package/protocol/capabilities/manifest.json +179 -0
  12. package/protocol/capabilities/repository-audit-input.schema.json +14 -0
  13. package/protocol/capabilities/repository-audit.schema.json +30 -0
  14. package/protocol/capabilities/repository-snapshot-input.schema.json +11 -0
  15. package/protocol/capabilities/repository-snapshot.schema.json +20 -0
  16. package/protocol/knowledge/chunk.schema.json +15 -73
  17. package/protocol/knowledge/document.schema.json +10 -48
  18. package/protocol/knowledge/index-config.schema.json +2 -2
  19. package/protocol/knowledge/repository.schema.json +11 -52
  20. package/protocol/knowledge/retrieval-query.schema.json +13 -63
  21. package/protocol/knowledge/source.schema.json +9 -43
  22. package/protocol/presets/catalog.json +48 -0
  23. package/python/agentsam_sdk/knowledge/models.py +19 -7
  24. package/python/agentsam_sdk/repository/__main__.py +2 -2
  25. package/python/tests/test_knowledge_models.py +6 -2
  26. package/src/agent/capability-adapter.js +50 -0
  27. package/src/agent/index.js +2 -0
  28. package/src/agent/repository-audit.js +188 -0
  29. package/src/capabilities/index.js +7 -0
  30. package/src/capabilities/manifest.js +22 -0
  31. package/src/capabilities/repository-snapshot.js +180 -0
  32. package/src/cli.js +56 -39
  33. package/src/commands/deploy.js +0 -1
  34. package/src/commands/knowledge.js +5 -6
  35. package/src/commands/product.js +119 -0
  36. package/src/commands/shell.js +253 -0
  37. package/src/index.js +9 -0
  38. package/src/knowledge/config.js +12 -6
  39. package/src/knowledge/contracts.js +1 -1
  40. package/src/knowledge/engine.js +1 -1
  41. package/src/knowledge/service/server.js +2 -2
  42. package/src/lib/git-context.js +3 -1
  43. package/src/lib/slash-commands.js +2 -1
  44. package/src/presets/index.js +20 -0
  45. package/src/repository/index.js +4 -0
  46. package/test/agent-capabilities.test.mjs +67 -0
  47. package/test/capabilities.test.mjs +84 -0
  48. package/test/portable-context.test.mjs +11 -1
  49. package/test/shell.test.mjs +60 -0
  50. package/test/smoke.mjs +2 -2
@@ -0,0 +1,119 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { getCapability, getCapabilityManifest, listCapabilities, repositorySnapshot } from '../capabilities/index.js';
5
+ import { getAddon, listAddons, listPresets } from '../presets/index.js';
6
+
7
+ function parseCommon(argv = []) {
8
+ const out = { json: false, cwd: process.cwd(), positionals: [] };
9
+ for (let i = 0; i < argv.length; i += 1) {
10
+ const arg = argv[i];
11
+ if (arg === '--json') out.json = true;
12
+ else if (arg === '--cwd') out.cwd = argv[++i] || out.cwd;
13
+ else out.positionals.push(arg);
14
+ }
15
+ return out;
16
+ }
17
+
18
+ export async function runInspect(argv = []) {
19
+ const opts = parseCommon(argv);
20
+ let churnDays = 30;
21
+ for (let i = 0; i < opts.positionals.length; i += 1) {
22
+ if (opts.positionals[i] === '--churn-days') churnDays = Number(opts.positionals[++i] || 30);
23
+ else if (opts.positionals[i] === 'repository') continue;
24
+ else throw new Error(`unknown inspect option: ${opts.positionals[i]}`);
25
+ }
26
+ const result = await repositorySnapshot({ cwd: opts.cwd, churnDays });
27
+ if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
28
+ else {
29
+ console.log(`\nRepository snapshot ${result.snapshot_id}`);
30
+ console.log(` repo ${result.repository.full_name || result.repository.repository_id || '(local)'}`);
31
+ console.log(` revision ${result.repository.revision_sha}`);
32
+ console.log(` merkle ${result.tree.merkle_root}`);
33
+ console.log(` files ${result.intelligence.summary?.file_count ?? result.tree.stats?.files ?? 'unknown'}`);
34
+ console.log(` knowledge ${result.knowledge?.indexed ? result.knowledge.generation_id : result.knowledge?.configured ? 'configured / not indexed' : 'not configured'}`);
35
+ console.log(` deploy ${result.deploy?.status || 'no trusted receipt'}`);
36
+ console.log(` content ${result.content_hash}\n`);
37
+ }
38
+ return result;
39
+ }
40
+
41
+ export async function runCapabilities(argv = []) {
42
+ const opts = parseCommon(argv);
43
+ const id = opts.positionals[0] || '';
44
+ const result = id ? getCapability(id) : getCapabilityManifest();
45
+ if (id && !result) throw new Error(`unknown_capability:${id}`);
46
+ if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
47
+ else if (id) console.log(`${result.id}\n ${result.description}\n cli: ${result.cli || '(library only)'}\n model required: ${result.model_required ? 'yes' : 'no'}`);
48
+ else {
49
+ console.log('\nAgentSam deterministic capabilities\n');
50
+ for (const row of listCapabilities()) console.log(` ${row.id.padEnd(26)} ${row.description}`);
51
+ console.log('');
52
+ }
53
+ return result;
54
+ }
55
+
56
+ function projectConfigPath(cwd) { return path.join(cwd, '.agentsam', 'config.json'); }
57
+ function featureStatePath(cwd) { return path.join(cwd, '.agentsam', 'features.json'); }
58
+
59
+ export function applyPresetSelection(cwd, preset) {
60
+ const filename = projectConfigPath(cwd);
61
+ if (!fs.existsSync(filename)) throw new Error('not_agentsam_project');
62
+ const config = JSON.parse(fs.readFileSync(filename, 'utf8'));
63
+ const next = { ...config, preset: preset.id, features: [...preset.features], capabilities: [...preset.capabilities] };
64
+ fs.writeFileSync(filename, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
65
+ return next;
66
+ }
67
+
68
+ export async function runAdd(argv = []) {
69
+ const opts = parseCommon(argv);
70
+ const id = opts.positionals[0];
71
+ if (!id || id === '--help') {
72
+ const rows = listAddons();
73
+ console.log(`agentsam add <${rows.map(x => x.id).join('|')}> [--cwd PATH] [--json]`);
74
+ return null;
75
+ }
76
+ const addon = getAddon(id);
77
+ if (!addon) throw new Error(`unknown_addon:${id}`);
78
+ const cwd = path.resolve(opts.cwd);
79
+ if (!fs.existsSync(projectConfigPath(cwd))) throw new Error('not_agentsam_project');
80
+ fs.mkdirSync(path.dirname(featureStatePath(cwd)), { recursive: true });
81
+ let state = { schema_version: 1, features: {} };
82
+ if (fs.existsSync(featureStatePath(cwd))) state = JSON.parse(fs.readFileSync(featureStatePath(cwd), 'utf8'));
83
+ state.features ||= {};
84
+ state.features[addon.id] = { selected: true, capabilities: addon.capabilities, selected_at: new Date().toISOString() };
85
+ fs.writeFileSync(featureStatePath(cwd), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
86
+ if (addon.id === 'deploy-cloudflare') {
87
+ const config = JSON.parse(fs.readFileSync(projectConfigPath(cwd), 'utf8'));
88
+ config.deploy_target = 'cloudflare';
89
+ fs.writeFileSync(projectConfigPath(cwd), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
90
+ }
91
+ const result = { ok: true, feature: addon.id, capabilities: addon.capabilities, description: addon.description, state_file: '.agentsam/features.json' };
92
+ if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
93
+ else console.log(`\nAdded ${addon.id}\n ${addon.description}\n state: ${result.state_file}\n`);
94
+ return result;
95
+ }
96
+
97
+ export async function runDev(argv = []) {
98
+ const opts = parseCommon(argv);
99
+ if (opts.positionals.length) throw new Error(`unknown dev option: ${opts.positionals[0]}`);
100
+ const cwd = path.resolve(opts.cwd);
101
+ const pkgFile = path.join(cwd, 'package.json');
102
+ if (!fs.existsSync(pkgFile)) throw new Error('package_json_not_found');
103
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
104
+ if (!pkg.scripts?.dev) throw new Error('dev_script_not_found');
105
+ if (/\bagentsam\s+dev\b/.test(pkg.scripts.dev)) throw new Error('recursive_agentsam_dev_script');
106
+ const child = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'dev'], { cwd, stdio: 'inherit', env: process.env });
107
+ return new Promise((resolve, reject) => {
108
+ child.once('error', reject);
109
+ child.once('exit', (code, signal) => {
110
+ if (signal) return reject(new Error(`dev_process_signal:${signal}`));
111
+ process.exitCode = code || 0;
112
+ resolve(code || 0);
113
+ });
114
+ });
115
+ }
116
+
117
+ export function printProductCatalog() {
118
+ return { presets: listPresets(), addons: listAddons(), capabilities: listCapabilities() };
119
+ }
@@ -0,0 +1,253 @@
1
+ import readline from 'node:readline';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { SLASH_COMMANDS, SHELL_PHASES } from '../lib/slash-commands.js';
6
+ import { runContext } from './context.js';
7
+ import { runDb } from './db.js';
8
+ import { runDeploy } from './deploy.js';
9
+ import { runStatus } from './status.js';
10
+ import { runTui } from './tui.js';
11
+
12
+ function writeLine(write, value = '') {
13
+ write(`${value}\n`);
14
+ }
15
+
16
+ export function tokenizeShellLine(input = '') {
17
+ const source = String(input);
18
+ const tokens = [];
19
+ let token = '';
20
+ let quote = '';
21
+ let started = false;
22
+
23
+ const flush = () => {
24
+ if (!started) return;
25
+ tokens.push(token);
26
+ token = '';
27
+ started = false;
28
+ };
29
+
30
+ for (let i = 0; i < source.length; i += 1) {
31
+ const ch = source[i];
32
+ if (quote) {
33
+ if (ch === quote) {
34
+ quote = '';
35
+ } else {
36
+ token += ch;
37
+ }
38
+ started = true;
39
+ continue;
40
+ }
41
+ if (ch === '"' || ch === "'") {
42
+ quote = ch;
43
+ started = true;
44
+ continue;
45
+ }
46
+ if (/\s/.test(ch)) {
47
+ flush();
48
+ continue;
49
+ }
50
+ token += ch;
51
+ started = true;
52
+ }
53
+ flush();
54
+ return tokens;
55
+ }
56
+
57
+ export function renderShellCatalog() {
58
+ const next = SHELL_PHASES.find((phase) => phase.status === 'next' || phase.status === 'current');
59
+ const rows = SLASH_COMMANDS.map((row) => ` ${row.cmd.padEnd(14)} ${row.description}`).join('\n');
60
+ return `
61
+ ╔════════════════════════════════╗
62
+ ║ Agent Sam Terminal ║
63
+ ╚════════════════════════════════╝
64
+
65
+ Local PTY agentsam start-local ws://127.0.0.1:3099
66
+ ANSI TUI agentsam tui zero-dependency Node UI
67
+ Rich TUI agentsam tui rich optional richer Python UI
68
+ agentsam tui rich --install
69
+ DB agentsam db status local SQLite
70
+
71
+ Current milestone: ${next?.label ?? 'local terminal experience'}
72
+
73
+ Slash commands (${SLASH_COMMANDS.length} registered):
74
+ ${rows}
75
+ `;
76
+ }
77
+
78
+ function parseDeployOptions(args, cwd) {
79
+ const opts = { cwd, target: '', accountId: '' };
80
+ for (let i = 0; i < args.length; i += 1) {
81
+ const arg = args[i];
82
+ if (arg === '--target') opts.target = args[++i] || '';
83
+ else if (arg === '--account-id') opts.accountId = args[++i] || '';
84
+ else throw new Error(`unknown /deploy option: ${arg}`);
85
+ }
86
+ return opts;
87
+ }
88
+
89
+ async function withProcessCwd(cwd, fn) {
90
+ const previous = process.cwd();
91
+ process.chdir(cwd);
92
+ try {
93
+ return await fn();
94
+ } finally {
95
+ process.chdir(previous);
96
+ }
97
+ }
98
+
99
+ async function runLocalAgent(goal, write) {
100
+ if (!goal) {
101
+ writeLine(write, ' Usage: /agent <goal>');
102
+ writeLine(write, ' Requires the local Agent Sam dev server (default http://127.0.0.1:8787).');
103
+ return;
104
+ }
105
+ const base = String(process.env.AGENTSAM_LOCAL_URL || 'http://127.0.0.1:8787').replace(/\/$/, '');
106
+ let response;
107
+ try {
108
+ response = await fetch(`${base}/api/agentsam/message`, {
109
+ method: 'POST',
110
+ headers: { 'content-type': 'application/json' },
111
+ body: JSON.stringify({ message: goal }),
112
+ });
113
+ } catch (error) {
114
+ throw new Error(`local Agent Sam unavailable at ${base} — run \`npm run dev\` first (${error?.message || error})`);
115
+ }
116
+ const text = await response.text();
117
+ if (!response.ok) throw new Error(`local Agent Sam returned HTTP ${response.status}: ${text.slice(0, 400)}`);
118
+ try {
119
+ writeLine(write, JSON.stringify(JSON.parse(text), null, 2));
120
+ } catch {
121
+ writeLine(write, text);
122
+ }
123
+ }
124
+
125
+ async function showLocalLogs(cwd, write) {
126
+ const dbPath = path.join(cwd, '.agentsam', 'data', 'agentsam.sqlite');
127
+ if (!fs.existsSync(dbPath)) {
128
+ writeLine(write, ' No local Agent Sam DB found. Run `agentsam init . --yes` first.');
129
+ return;
130
+ }
131
+ const { createLocalSqliteDatabase } = await import('../local/sqlite.js');
132
+ const db = await createLocalSqliteDatabase(dbPath);
133
+ try {
134
+ const calls = await db
135
+ .prepare('SELECT id, session_id, tool_name, status, created_at, completed_at FROM agent_tool_calls ORDER BY created_at DESC LIMIT 20')
136
+ .all();
137
+ if (!calls.results.length) {
138
+ writeLine(write, ' No local Agent Sam tool-call events yet.');
139
+ return;
140
+ }
141
+ writeLine(write, '');
142
+ writeLine(write, ' Recent Agent Sam tool calls');
143
+ for (const row of calls.results) {
144
+ writeLine(write, ` ${String(row.created_at || '').padEnd(20)} ${String(row.status || '').padEnd(10)} ${row.tool_name}`);
145
+ }
146
+ writeLine(write, '');
147
+ } finally {
148
+ db.close();
149
+ }
150
+ }
151
+
152
+ export async function dispatchShellLine(line, state = {}) {
153
+ const tokens = tokenizeShellLine(line);
154
+ const write = state.write || ((text) => process.stdout.write(text));
155
+ state.cwd = path.resolve(state.cwd || process.cwd());
156
+ if (!tokens.length) return { handled: true, exit: false, cwd: state.cwd };
157
+
158
+ const [command, ...args] = tokens;
159
+ try {
160
+ switch (command.toLowerCase()) {
161
+ case '/help':
162
+ write(renderShellCatalog());
163
+ return { handled: true, exit: false, cwd: state.cwd };
164
+ case '/exit':
165
+ case '/quit':
166
+ return { handled: true, exit: true, cwd: state.cwd };
167
+ case '/status':
168
+ await runStatus(args, { cwd: state.cwd });
169
+ break;
170
+ case '/context':
171
+ await runContext(['--cwd', state.cwd, ...args]);
172
+ break;
173
+ case '/pwd':
174
+ writeLine(write, state.cwd);
175
+ break;
176
+ case '/cd': {
177
+ const destination = args.length ? args.join(' ') : process.env.HOME || process.env.USERPROFILE || state.cwd;
178
+ const next = path.resolve(state.cwd, destination);
179
+ if (!fs.existsSync(next) || !fs.statSync(next).isDirectory()) throw new Error(`directory not found: ${next}`);
180
+ state.cwd = next;
181
+ writeLine(write, state.cwd);
182
+ break;
183
+ }
184
+ case '/git': {
185
+ const gitArgs = args.length ? args : ['status', '--short', '--branch'];
186
+ const result = spawnSync('git', gitArgs, { cwd: state.cwd, stdio: 'inherit', shell: false });
187
+ if (result.error) throw result.error;
188
+ if (result.status !== 0) throw new Error(`git exited ${result.status}`);
189
+ break;
190
+ }
191
+ case '/db':
192
+ await runDb(args.length ? args : ['status'], { cwd: state.cwd });
193
+ break;
194
+ case '/agent':
195
+ await runLocalAgent(args.join(' '), write);
196
+ break;
197
+ case '/logs':
198
+ await showLocalLogs(state.cwd, write);
199
+ break;
200
+ case '/tui':
201
+ await withProcessCwd(state.cwd, () => runTui(args));
202
+ break;
203
+ case '/deploy':
204
+ await runDeploy(parseDeployOptions(args, state.cwd));
205
+ break;
206
+ default:
207
+ writeLine(write, ` Unknown Agent Sam command: ${command}`);
208
+ writeLine(write, ' Type /help for available commands.');
209
+ return { handled: false, exit: false, cwd: state.cwd };
210
+ }
211
+ } catch (error) {
212
+ writeLine(write, ` ✗ ${error?.message || error}`);
213
+ }
214
+
215
+ return { handled: true, exit: false, cwd: state.cwd };
216
+ }
217
+
218
+ export async function runShell(argv = [], options = {}) {
219
+ const write = options.write || ((text) => process.stdout.write(text));
220
+ const state = { cwd: path.resolve(options.cwd || process.cwd()), write };
221
+ const sub = argv[0] || '';
222
+
223
+ if (sub === 'list' || sub === 'status') {
224
+ write(renderShellCatalog());
225
+ return;
226
+ }
227
+ if (sub === '--command' || sub === '--once') {
228
+ const line = argv.slice(1).join(' ');
229
+ if (!line) throw new Error(`${sub} requires a slash command`);
230
+ await dispatchShellLine(line, state);
231
+ return;
232
+ }
233
+ if (sub) throw new Error(`unknown shell option: ${sub}`);
234
+
235
+ write(renderShellCatalog());
236
+ writeLine(write, ' Interactive shell ready. Type /help for commands; /exit to return to your host shell.');
237
+ writeLine(write, '');
238
+
239
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
240
+ if (rl.terminal) {
241
+ rl.setPrompt('agentsam> ');
242
+ rl.prompt();
243
+ }
244
+
245
+ for await (const line of rl) {
246
+ const result = await dispatchShellLine(line, state);
247
+ if (result.exit) {
248
+ rl.close();
249
+ break;
250
+ }
251
+ if (rl.terminal) rl.prompt();
252
+ }
253
+ }
package/src/index.js CHANGED
@@ -31,12 +31,21 @@ export {
31
31
  SHELL_PHASES,
32
32
  listSlashCommands,
33
33
  } from './lib/slash-commands.js';
34
+ export {
35
+ CAPABILITY_MANIFEST_VERSION,
36
+ getCapability,
37
+ getCapabilityManifest,
38
+ listCapabilities,
39
+ repositorySnapshot,
40
+ } from './capabilities/index.js';
41
+ export { getPreset, listPresets, resolvePreset, getAddon, listAddons } from './presets/index.js';
34
42
 
35
43
  export {
36
44
  createIdentityClient,
37
45
  createIdentity,
38
46
  GoogleProvider,
39
47
  GithubProvider,
48
+ IamProvider,
40
49
  GcpProvider,
41
50
  EmailProvider,
42
51
  getIdentityProvider,
@@ -37,7 +37,8 @@ export function validateConfig(input) {
37
37
  allowed(c.embedding, ['provider', 'model', 'revision', 'dimensions', 'parameters'], 'embedding');
38
38
  allowed(c.storage, ['driver', 'connection_env'], 'storage');
39
39
  if (c.version !== 1) throw new Error('Unsupported knowledge configuration version.');
40
- for (const key of ['repository_id', 'workspace_id']) if (typeof c[key] !== 'string' || !c[key].trim()) throw new Error(`${key} is required.`);
40
+ if (typeof c.repository_id !== 'string' || !c.repository_id.trim()) throw new Error('repository_id is required.');
41
+ if (c.workspace_id != null && (typeof c.workspace_id !== 'string' || !c.workspace_id.trim())) throw new Error('workspace_id must be a non-empty string when present.');
41
42
  if (typeof c.scope?.name !== 'string' || !c.scope.name.trim() || !Array.isArray(c.scope.include) || !c.scope.include.length) throw new Error('A named scope with at least one include path is required.');
42
43
  c.scope.include = [...new Set(c.scope.include.map(relativeSelection))].sort();
43
44
  c.scope.exclude = [...new Set((c.scope.exclude || []).map(relativeSelection))].sort();
@@ -49,10 +50,9 @@ export function validateConfig(input) {
49
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.');
50
51
  return c;
51
52
  }
52
- export function defaultConfig({ include = ['.'], exclude = [], scope = 'default', workspace = 'local', target = 'local', dimensions = 768 } = {}) {
53
+ export function defaultConfig({ include = ['.'], exclude = [], scope = 'default', target = 'local', dimensions = 768 } = {}) {
53
54
  if (!['local', 'production'].includes(target)) throw new Error('target must be local or production.');
54
- if (target === 'production' && workspace === 'local') throw new Error('Production requires an explicit workspace identifier.');
55
- return validateConfig({ version: 1, repository_id: randomUUID(), workspace_id: workspace,
55
+ return validateConfig({ version: 1, repository_id: 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' } });
@@ -65,5 +65,11 @@ export function initRepository(root, options = {}) {
65
65
  fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
66
66
  return config;
67
67
  }
68
- export const scopeKey = config => fingerprint([config.workspace_id, config.repository_id, config.scope.name]);
69
- export const cacheNamespace = config => fingerprint([config.workspace_id, config.repository_id]);
68
+ // New portable configs are repository-scoped. Legacy configs that still carry
69
+ // workspace_id retain their original namespace so existing local generations stay readable.
70
+ export const scopeKey = config => config.workspace_id
71
+ ? fingerprint([config.workspace_id, config.repository_id, config.scope.name])
72
+ : fingerprint([config.repository_id, config.scope.name]);
73
+ export const cacheNamespace = config => config.workspace_id
74
+ ? fingerprint([config.workspace_id, config.repository_id])
75
+ : fingerprint([config.repository_id]);
@@ -8,7 +8,7 @@ export const KNOWLEDGE_OPERATIONS = Object.freeze({
8
8
  export function assertRetrievalQuery(value) {
9
9
  if (!value || typeof value !== "object") throw new TypeError("RetrievalQuery must be an object");
10
10
  if (!String(value.text || "").trim()) throw new TypeError("RetrievalQuery.text is required");
11
- if (!String(value.workspace_id || "").trim()) throw new TypeError("RetrievalQuery.workspace_id is required");
11
+ if (value.workspace_id != null && !String(value.workspace_id).trim()) throw new TypeError("RetrievalQuery.workspace_id must be non-empty when present");
12
12
  const topK = value.top_k ?? 12;
13
13
  if (!Number.isInteger(topK) || topK < 1 || topK > 100) throw new RangeError("top_k must be 1..100");
14
14
  if (!Number.isInteger(value.token_budget ?? 8000) || (value.token_budget ?? 8000) < 256) throw new RangeError("token_budget must be at least 256");
@@ -113,6 +113,6 @@ export async function retrieve({ store, config: input, text, semantic = false, e
113
113
  metadata: { generation_id: generation.id, symbol: hit.symbol, content_hash: hit.content_hash, freshness: 'working tree not checked' } }); estimatedTokens += tokens;
114
114
  if (hits.length === topK) break;
115
115
  }
116
- return createContextPack({ queryId: randomUUID(), query: { text, workspace_id: config.workspace_id, top_k: topK, token_budget: tokenBudget }, hits,
116
+ return createContextPack({ queryId: randomUUID(), query: { text, top_k: topK, token_budget: tokenBudget }, hits,
117
117
  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
118
  }
@@ -100,7 +100,7 @@ export async function startKnowledgeService({ stateDir, repositories, token, por
100
100
  });
101
101
  // Re-resolve registry on replay. Removed repositories do not get resumed.
102
102
  const payload = JSON.parse(row.payload), registered = repositories[payload.request.repository];
103
- if (!registered || registered.config.repository_id !== payload.config.repository_id || registered.config.workspace_id !== payload.config.workspace_id || fingerprint(registered.config.scope) !== payload.registered_scope || (!allowEmbeddings && ((payload.request.embed && payload.request.operation !== 'plan') || payload.request.semantic))) {
103
+ if (!registered || registered.config.repository_id !== payload.config.repository_id || fingerprint(registered.config.scope) !== payload.registered_scope || (!allowEmbeddings && ((payload.request.embed && payload.request.operation !== 'plan') || payload.request.semantic))) {
104
104
  response = { ok: false, error: 'Repository registration changed; resubmit this job.' }; child.kill(); return;
105
105
  }
106
106
  child.send({ ...payload, root: registered.root, filename: path.join(stateDir, 'knowledge.sqlite'), maxFiles });
@@ -124,7 +124,7 @@ export async function startKnowledgeService({ stateDir, repositories, token, por
124
124
  payload.registered_scope = fingerprint(repositories[payload.request.repository].config.scope);
125
125
  const key = req.headers['idempotency-key'];
126
126
  if (key !== undefined && (typeof key !== 'string' || !/^[\w:.-]{1,128}$/.test(key))) throw fail(400, 'Invalid Idempotency-Key.');
127
- const digest = fingerprint(payload), idem = key ? fingerprint([payload.config.workspace_id, payload.config.repository_id, key]) : null;
127
+ const digest = fingerprint(payload), idem = key ? fingerprint([payload.config.repository_id, payload.registered_scope, key]) : null;
128
128
  const previous = idem && db.prepare('SELECT * FROM jobs WHERE idem=?').get(idem);
129
129
  if (previous) {
130
130
  if (previous.digest !== digest) throw fail(409, 'Idempotency-Key already used for a different job.');
@@ -66,7 +66,9 @@ export function resolveGitContext(options = {}) {
66
66
  ? preferredRemote
67
67
  : availableRemotes[0] || preferredRemote;
68
68
  const remoteUrl = runGit(root, ['remote', 'get-url', remote], { required: false });
69
- const revisionSha = runGit(root, ['rev-parse', 'HEAD']);
69
+ // A newly initialized repository can have an unborn HEAD. Repository identity
70
+ // and local inspection must still work before the first commit.
71
+ const revisionSha = runGit(root, ['rev-parse', 'HEAD'], { required: false }) || null;
70
72
  const branch = runGit(root, ['branch', '--show-current'], { required: false }) || null;
71
73
  const status = runGit(root, ['status', '--porcelain=v1'], { required: false });
72
74
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Canonical slash-command surface for Agent Sam SDK CLI / shell UX.
3
- * Consumed by the gorilla-shell example and future `agentsam shell` PTY bridge.
3
+ * Consumed by the interactive `agentsam shell` REPL and presentation layers.
4
4
  */
5
5
 
6
6
  export const SHELL_THEMES = ['NIGHT', 'DAY', 'LAVA', 'VOID'];
@@ -18,6 +18,7 @@ export const SLASH_COMMANDS = [
18
18
  { cmd: '/logs', description: 'Show local Agent Sam execution events', lane: 'observability' },
19
19
  { cmd: '/tui', description: 'Switch or preview terminal presentation', lane: 'terminal' },
20
20
  { cmd: '/deploy', description: 'Add a cloud adapter and deploy intentionally', lane: 'deploy' },
21
+ { cmd: '/exit', description: 'Exit Agent Sam shell and return to the host terminal' },
21
22
  ];
22
23
 
23
24
  /** Shell UX rollout phases (gorilla-shell → SDK default CLI experience). */
@@ -0,0 +1,20 @@
1
+ import catalog from '../../protocol/presets/catalog.json' with { type: 'json' };
2
+
3
+ export const PRESET_CATALOG_VERSION = catalog.schema_version;
4
+
5
+ export function getPresetCatalog() { return structuredClone(catalog); }
6
+ export function listPresets() { return Object.values(catalog.presets).map((row) => structuredClone(row)); }
7
+ export function getPreset(id) {
8
+ const row = catalog.presets[String(id || '').trim().toLowerCase()];
9
+ return row ? structuredClone(row) : null;
10
+ }
11
+ export function resolvePreset(id = 'fullstack') {
12
+ const preset = getPreset(id);
13
+ if (!preset) throw new Error(`unknown_preset:${id}; expected ${Object.keys(catalog.presets).join(',')}`);
14
+ return preset;
15
+ }
16
+ export function listAddons() { return Object.values(catalog.addons).map((row) => structuredClone(row)); }
17
+ export function getAddon(id) {
18
+ const row = catalog.addons[String(id || '').trim().toLowerCase()];
19
+ return row ? structuredClone(row) : null;
20
+ }
@@ -0,0 +1,4 @@
1
+ export { repositorySnapshot } from '../capabilities/repository-snapshot.js';
2
+ export { buildMerkleTree, diffTrees, readSnapshot, saveSnapshot, validateSnapshot } from '../lib/merkle/index.js';
3
+ export { resolveGitContext, tryResolveGitContext, normalizeGitRemote } from '../lib/git-context.js';
4
+ export { buildRetrievalPlan, createContextPack } from '../knowledge/index.js';
@@ -0,0 +1,67 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createCapabilityAdapter } from '../src/agent/index.js';
4
+ import { buildRepositoryAuditPacket, runRepositoryAudit } from '../src/agent/repository-audit.js';
5
+
6
+ const snapshot = {
7
+ schema_version: 1,
8
+ capability: 'repository.snapshot',
9
+ snapshot_id: 'rsnap_0123456789abcdef01234567',
10
+ content_hash: `sha256:${'a'.repeat(64)}`,
11
+ repository: { repository_id: 'github:owner/repo', provider: 'github', full_name: 'owner/repo', branch: 'main', revision_sha: 'b'.repeat(40), dirty: false },
12
+ tree: { merkle_root: `sha256:${'c'.repeat(64)}`, stats: { files: 10 } },
13
+ intelligence: {
14
+ summary: { file_count: 10, total_lines: 500 },
15
+ languages: [{ language: 'JavaScript', files: 7 }],
16
+ manifests: [{ path: 'package.json', kind: 'node' }],
17
+ top_level: [{ path: 'src', files: 7 }],
18
+ pressure_points: [{ path: 'src', pressure_score: 70 }],
19
+ },
20
+ packages: [{ path: 'package.json', name: 'demo', version: '1.0.0' }],
21
+ knowledge: { configured: false },
22
+ deploy: null,
23
+ };
24
+
25
+ test('repository audit packet is bounded read-only evidence', () => {
26
+ const packet = buildRepositoryAuditPacket({ snapshot, focus: ['packages'], evidenceBudget: 1000 });
27
+ assert.equal(packet.primitive, 'repository.audit');
28
+ assert.equal(packet.rules.read_only, true);
29
+ assert.equal(packet.rules.may_edit, false);
30
+ assert.equal(packet.snapshot_id, snapshot.snapshot_id);
31
+ assert.ok(JSON.stringify(packet.evidence).length <= 4000);
32
+ });
33
+
34
+ test('repository audit uses injected reasoning and validates read-only output', async () => {
35
+ const audit = await runRepositoryAudit({
36
+ snapshot,
37
+ reasoner: async packet => ({
38
+ summary: `Audited ${packet.evidence.repository.full_name}`,
39
+ packages: packet.evidence.packages,
40
+ notable_combinations: ['snapshot + audit'],
41
+ findings: [{ severity: 'low', finding: 'example' }],
42
+ recommended_routes: ['inspect capability registry'],
43
+ evidence_refs: [packet.evidence_index.packages],
44
+ }),
45
+ });
46
+ assert.equal(audit.primitive, 'repository.audit');
47
+ assert.equal(audit.summary, 'Audited owner/repo');
48
+ assert.equal(audit.packages.length, 1);
49
+ assert.deepEqual(audit.commands, []);
50
+
51
+ await assert.rejects(() => runRepositoryAudit({ snapshot, reasoner: async () => ({ summary: 'bad', jobs: [] }) }), /mutation field: jobs/);
52
+ });
53
+
54
+ test('agent capability adapter exposes only executable tools by default', async () => {
55
+ const adapter = createCapabilityAdapter({
56
+ reasoner: async packet => ({ summary: 'ok', evidence_refs: [packet.evidence_index.summary] }),
57
+ handlers: { 'knowledge.search': async input => ({ query: input.text, hits: [] }) },
58
+ });
59
+ const names = adapter.toolDescriptors().map(row => row.name);
60
+ assert.ok(names.includes('repository.snapshot'));
61
+ assert.ok(names.includes('repository.audit'));
62
+ assert.ok(names.includes('knowledge.search'));
63
+ assert.equal(names.includes('knowledge.index'), false);
64
+ const result = await adapter.invoke('knowledge.search', { text: 'hello' });
65
+ assert.equal(result.capability_id, 'knowledge.search');
66
+ assert.deepEqual(result.result.hits, []);
67
+ });