@inneranimalmedia/agentsam-sdk 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/CLI_SHELL.md CHANGED
@@ -21,7 +21,15 @@ Runs the optional Python Rich renderer. `--install` creates an isolated `.agents
21
21
  agentsam shell
22
22
  ```
23
23
 
24
- Shows the command catalog and local PTY / DB / TUI surfaces.
24
+ Starts the interactive Agent Sam slash-command shell. Once the `agentsam>` prompt is visible, commands such as `/help`, `/status`, `/pwd`, and `/git` are handled by Agent Sam instead of the host shell (PowerShell, bash, or zsh). Use `/exit` to return to the host terminal.
25
+
26
+ Do not type `/help` directly at a PowerShell/bash prompt; enter `agentsam shell` first.
27
+
28
+ For scripts and regression tests, a single slash command can be dispatched without opening the REPL:
29
+
30
+ ```bash
31
+ agentsam shell --command /help
32
+ ```
25
33
 
26
34
  ## Architecture
27
35
 
@@ -92,6 +100,7 @@ Gorilla is intentionally not scaffolded by default.
92
100
  /logs local execution events
93
101
  /tui terminal presentation
94
102
  /deploy intentionally add a cloud adapter
103
+ /exit exit Agent Sam shell and return to the host terminal
95
104
  ```
96
105
 
97
106
  Provider-specific commands such as `/claude` or `/codex` are not part of the generic shell contract. Model routing belongs behind Agent Sam.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneranimalmedia/agentsam-sdk",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Portable AgentSam SDK and CLI kits for local scaffolding, repository intelligence, incremental indexing, identity adapters, and verified dependency maintenance.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneranimalmedia/agentsam-sdk-identity",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Identity module for @inneranimalmedia/agentsam-sdk (workspace — publish via root SDK)",
package/src/cli.js CHANGED
@@ -18,6 +18,7 @@ import { runContext } from './commands/context.js';
18
18
  import { runDb } from './commands/db.js';
19
19
  import { runStatus } from './commands/status.js';
20
20
  import { runTui } from './commands/tui.js';
21
+ import { runShell } from './commands/shell.js';
21
22
  import { runDockerize } from './commands/dockerize.js';
22
23
  import { runMini } from './commands/mini.js';
23
24
  import { runMerkle } from './commands/merkle.js';
@@ -26,7 +27,6 @@ import { runSecurity } from './commands/security.js';
26
27
  import { runRecon } from './commands/recon.js';
27
28
  import { applyPresetSelection, runAdd, runCapabilities, runDev, runInspect } from './commands/product.js';
28
29
  import { listPresets, resolvePreset } from './presets/index.js';
29
- import { SLASH_COMMANDS, SHELL_PHASES } from './lib/slash-commands.js';
30
30
  import fs from 'node:fs';
31
31
  import { repositoryRoot } from './knowledge/config.js';
32
32
 
@@ -70,7 +70,7 @@ function printHelp() {
70
70
  agentsam tui Zero-dependency ANSI Agent Sam dashboard
71
71
  agentsam tui rich Optional Python Rich dashboard (--install for local venv)
72
72
  agentsam start-local Local PTY on ws://127.0.0.1:3099 (no tunnel, no Cloudflare)
73
- agentsam shell Terminal commands + presentation catalog
73
+ agentsam shell Interactive Agent Sam slash-command shell
74
74
  agentsam tunnel Explicitly expose local PTY when remote access is wanted
75
75
  agentsam deploy Graduate to Cloudflare / GCP when ready
76
76
  agentsam dockerize Build/run app, knowledge, or CAD containers (--help)
@@ -257,41 +257,6 @@ async function initFromArgs(argv) {
257
257
  await runLocalInit({ ...opts, prompt: null });
258
258
  }
259
259
 
260
- async function runShellInfo(argv = []) {
261
- const sub = argv[0] || 'list';
262
- if (sub === 'demo' || sub === 'ansi') {
263
- await runTui(['ansi', ...argv.slice(1)]);
264
- return;
265
- }
266
- if (sub === 'rich') {
267
- await runTui(['rich', ...argv.slice(1)]);
268
- return;
269
- }
270
- if (sub !== 'list' && sub !== 'status') {
271
- throw new Error(`unknown shell command: ${sub}`);
272
- }
273
-
274
- const next = SHELL_PHASES.find((p) => p.status === 'next' || p.status === 'current');
275
- console.log(`
276
- ╔═══════════════════════════════╗
277
- ║ Agent Sam Terminal ║
278
- ╚════════════════════════════════╝
279
-
280
- Local PTY agentsam start-local ws://127.0.0.1:3099
281
- ANSI TUI agentsam tui zero-dependency Node UI
282
- Rich TUI agentsam tui rich optional richer Python UI
283
- agentsam tui rich --install
284
- DB agentsam db status local SQLite
285
-
286
- Current milestone: ${next?.label ?? 'local terminal experience'}
287
-
288
- Slash commands (${SLASH_COMMANDS.length} registered):
289
- `);
290
- for (const row of SLASH_COMMANDS) {
291
- console.log(` ${row.cmd.padEnd(14)} ${row.description}`);
292
- }
293
- }
294
-
295
260
  const command = process.argv[2];
296
261
  const rest = process.argv.slice(3);
297
262
 
@@ -353,7 +318,7 @@ if (command === '--version' || command === '-v') {
353
318
  }
354
319
  } else if (command === 'shell') {
355
320
  try {
356
- await runShellInfo(rest);
321
+ await runShell(rest);
357
322
  } catch (e) {
358
323
  console.error(`\n ✗ ${e?.message || e}\n`);
359
324
  process.exit(1);
@@ -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
+ }
@@ -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,60 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import test from 'node:test';
7
+ import { dispatchShellLine, renderShellCatalog, tokenizeShellLine } from '../src/commands/shell.js';
8
+
9
+ const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
10
+
11
+ test('shell tokenizer preserves Windows paths and quoted arguments', () => {
12
+ assert.deepEqual(tokenizeShellLine('/cd C:\\Users\\conno\\fuelnreetime'), ['/cd', 'C:\\Users\\conno\\fuelnreetime']);
13
+ assert.deepEqual(tokenizeShellLine('/cd "C:\\Users\\Connor Smith\\repo"'), ['/cd', 'C:\\Users\\Connor Smith\\repo']);
14
+ assert.deepEqual(tokenizeShellLine('/agent "inspect this repo"'), ['/agent', 'inspect this repo']);
15
+ });
16
+
17
+ test('shell catalog advertises commands that the REPL owns', () => {
18
+ const catalog = renderShellCatalog();
19
+ assert.match(catalog, /\/help\s+Show Agent Sam commands/);
20
+ assert.match(catalog, /\/status\s+Local project/);
21
+ assert.match(catalog, /\/exit\s+Exit Agent Sam shell/);
22
+ });
23
+
24
+ test('dispatch handles help, pwd, cd, and exit without falling through to host shell', async () => {
25
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-'));
26
+ const child = path.join(root, 'child folder');
27
+ fs.mkdirSync(child);
28
+ let output = '';
29
+ const state = { cwd: root, write: (text) => { output += text; } };
30
+
31
+ let result = await dispatchShellLine('/help', state);
32
+ assert.equal(result.handled, true);
33
+ assert.equal(result.exit, false);
34
+ assert.match(output, /Slash commands/);
35
+
36
+ output = '';
37
+ result = await dispatchShellLine('/pwd', state);
38
+ assert.equal(result.handled, true);
39
+ assert.equal(output.trim(), root);
40
+
41
+ output = '';
42
+ result = await dispatchShellLine('/cd "child folder"', state);
43
+ assert.equal(result.handled, true);
44
+ assert.equal(state.cwd, child);
45
+ assert.equal(output.trim(), child);
46
+
47
+ result = await dispatchShellLine('/exit', state);
48
+ assert.equal(result.exit, true);
49
+ });
50
+
51
+ test('CLI supports a deterministic one-shot slash command for regression tests', () => {
52
+ const result = spawnSync(process.execPath, ['src/cli.js', 'shell', '--command', '/help'], {
53
+ cwd: repoRoot,
54
+ encoding: 'utf8',
55
+ });
56
+ assert.equal(result.status, 0, result.stderr);
57
+ assert.match(result.stdout, /Agent Sam Terminal/);
58
+ assert.match(result.stdout, /\/help/);
59
+ assert.match(result.stdout, /\/exit/);
60
+ });
package/test/smoke.mjs CHANGED
@@ -57,8 +57,8 @@ assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/db'));
57
57
  assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/agent'));
58
58
  assert.deepEqual(
59
59
  listSlashCommands({ lane: 'deploy' }).map(({ cmd }) => cmd),
60
- ['/help', '/deploy'],
61
- 'deploy lane keeps global help and excludes commands from other lanes',
60
+ ['/help', '/deploy', '/exit'],
61
+ 'deploy lane keeps global shell controls and excludes commands from other lanes',
62
62
  );
63
63
 
64
64
  printContextSummary({