@inneranimalmedia/agentsam-sdk 1.2.0 → 1.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.
@@ -26,7 +26,7 @@ export const SLASH_COMMANDS = [
26
26
  /** Shell UX rollout phases (gorilla-shell → SDK default CLI experience). */
27
27
  export const SHELL_PHASES = [
28
28
  { id: '0-prototype', label: 'Visual prototype + demo scenarios', status: 'complete' },
29
- { id: 'pty-connection', label: 'ExecOS / iam-pty WebSocket bridge', status: 'next' },
29
+ { id: 'pty-connection', label: 'Local PTY via agentsam start-local', status: 'current' },
30
30
  { id: 'hud-layer', label: 'Quest log, tool gate, XP HUD', status: 'planned' },
31
31
  { id: 'buddy-system', label: 'In-shell Agent Sam via MCP', status: 'planned' },
32
32
  { id: 'dashboard-embed', label: 'Embeddable shell for IAM dashboard', status: 'planned' },
@@ -0,0 +1,21 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Write Agent Sam scaffold files to the user's machine (their repo, not IAM's).
6
+ * @param {string} targetDir
7
+ * @param {{ path: string, content: string }[]} files
8
+ */
9
+ export function writeScaffoldFiles(targetDir, files) {
10
+ const root = path.resolve(targetDir);
11
+ if (fs.existsSync(root)) {
12
+ throw new Error(`Directory already exists: ${root}`);
13
+ }
14
+ fs.mkdirSync(root, { recursive: true });
15
+ for (const file of files) {
16
+ const dest = path.join(root, file.path);
17
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
18
+ fs.writeFileSync(dest, file.content, 'utf8');
19
+ }
20
+ return root;
21
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Agent Sam local PTY — localhost WebSocket shell, no tunnel, no IAM.
3
+ * Compatible with iam-pty wire format (raw bytes + JSON resize/slash).
4
+ */
5
+ import http from 'node:http';
6
+ import { WebSocketServer } from 'ws';
7
+
8
+ const DEFAULT_PORT = 3099;
9
+
10
+ function parsePort(value, fallback) {
11
+ const n = Number.parseInt(String(value || ''), 10);
12
+ return Number.isFinite(n) && n > 0 ? n : fallback;
13
+ }
14
+
15
+ function shellForPlatform() {
16
+ if (process.platform === 'win32') return process.env.COMSPEC || 'powershell.exe';
17
+ return process.env.SHELL || '/bin/zsh';
18
+ }
19
+
20
+ async function loadPty() {
21
+ try {
22
+ const mod = await import('node-pty');
23
+ return mod.default || mod;
24
+ } catch (e) {
25
+ throw new Error(
26
+ `node-pty is required for start-local. Run: npm install node-pty\n${e?.message || e}`,
27
+ );
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @param {{ cwd?: string, port?: number, host?: string }} [opts]
33
+ */
34
+ export async function startLocalPtyServer(opts = {}) {
35
+ const pty = await loadPty();
36
+ const cwd = opts.cwd || process.cwd();
37
+ const port = parsePort(opts.port ?? process.env.PTY_PORT, DEFAULT_PORT);
38
+ const host = opts.host || '127.0.0.1';
39
+ const shell = shellForPlatform();
40
+
41
+ const httpServer = http.createServer((req, res) => {
42
+ const path = (req.url || '/').split('?')[0];
43
+ if (path === '/health') {
44
+ res.writeHead(200, { 'Content-Type': 'application/json' });
45
+ res.end(
46
+ JSON.stringify({
47
+ ok: true,
48
+ service: 'agentsam-local-pty',
49
+ cwd,
50
+ port,
51
+ shell,
52
+ }),
53
+ );
54
+ return;
55
+ }
56
+ res.writeHead(404);
57
+ res.end('not found');
58
+ });
59
+
60
+ const wss = new WebSocketServer({ server: httpServer });
61
+
62
+ wss.on('connection', (ws, req) => {
63
+ const url = new URL(req.url || '/', `http://${host}`);
64
+ const sessionCwd = url.searchParams.get('cwd')?.trim() || cwd;
65
+ const cols = parsePort(url.searchParams.get('cols'), 80);
66
+ const rows = parsePort(url.searchParams.get('rows'), 24);
67
+
68
+ const term = pty.spawn(shell, [], {
69
+ name: 'xterm-256color',
70
+ cols,
71
+ rows,
72
+ cwd: sessionCwd,
73
+ env: { ...process.env, TERM: 'xterm-256color', AGENTSAM_LOCAL_PTY: '1' },
74
+ });
75
+
76
+ const sessionId = `local_${Date.now().toString(36)}`;
77
+ ws.send(JSON.stringify({ type: 'session_id', session_id: sessionId }));
78
+
79
+ term.onData((data) => {
80
+ if (ws.readyState === ws.OPEN) ws.send(data);
81
+ });
82
+
83
+ ws.on('message', (raw) => {
84
+ const text = raw.toString();
85
+ try {
86
+ const msg = JSON.parse(text);
87
+ if (msg.type === 'resize' && msg.cols && msg.rows) {
88
+ term.resize(msg.cols, msg.rows);
89
+ return;
90
+ }
91
+ if (msg.type === 'slash' && msg.line) {
92
+ term.write(`${msg.line}\r`);
93
+ return;
94
+ }
95
+ } catch {
96
+ /* raw PTY input */
97
+ }
98
+ term.write(text);
99
+ });
100
+
101
+ const cleanup = () => {
102
+ try {
103
+ term.kill();
104
+ } catch {
105
+ /* ignore */
106
+ }
107
+ };
108
+ term.onExit(() => {
109
+ if (ws.readyState === ws.OPEN) ws.close();
110
+ });
111
+ ws.on('close', cleanup);
112
+ ws.on('error', cleanup);
113
+ });
114
+
115
+ await new Promise((resolve) => {
116
+ httpServer.listen(port, host, resolve);
117
+ });
118
+
119
+ return {
120
+ port,
121
+ host,
122
+ cwd,
123
+ shell,
124
+ url: `ws://${host}:${port}`,
125
+ healthUrl: `http://${host}:${port}/health`,
126
+ close: () =>
127
+ new Promise((resolve, reject) => {
128
+ wss.close(() => {
129
+ httpServer.close((err) => (err ? reject(err) : resolve()));
130
+ });
131
+ }),
132
+ };
133
+ }
package/test/smoke.mjs CHANGED
@@ -3,7 +3,9 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { AgentSam, routeIntent, getToolCatalog } from '../src/index.js';
6
- import { scaffoldProject } from '../src/lib/scaffold.js';
6
+ import { buildLocalScaffoldMeta } from '../src/lib/local-scaffold.js';
7
+ import { writeScaffoldFiles } from '../src/lib/write-files.js';
8
+ import { printContextSummary, missingForInit } from '../src/lib/detect-context.js';
7
9
 
8
10
  const app = new AgentSam({ project: 'smoke', lane: 'cms', agent: 'cms' });
9
11
  let res = await app.handle(new Request('https://example.com/api/health'));
@@ -27,10 +29,11 @@ assert.equal(routeIntent({ message: 'drop table users' }).requires_approval, tru
27
29
  assert.ok(getToolCatalog('Data Solutions').some((tool) => tool.name === 'query'));
28
30
 
29
31
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-sdk-test-'));
30
- const cwd = process.cwd();
31
- process.chdir(tmp);
32
- const dir = scaffoldProject({ projectName: 'CMS Demo', lane: 'CMS', provider: 'Cloudflare Workers', agent: 'cms' });
33
- process.chdir(cwd);
32
+ const dir = writeScaffoldFiles(path.join(tmp, 'demo-project'), [
33
+ { path: 'src/index.js', content: 'export default {};\n' },
34
+ { path: 'migrations/0001_agentsam_core.sql', content: 'CREATE TABLE cms_pages (id TEXT);\n' },
35
+ { path: 'package.json', content: '{"name":"demo","devDependencies":{"wrangler":"^4"}}\n' },
36
+ ]);
34
37
  assert.ok(fs.existsSync(path.join(dir, 'src/index.js')));
35
38
  assert.ok(fs.readFileSync(path.join(dir, 'migrations/0001_agentsam_core.sql'), 'utf8').includes('cms_pages'));
36
39
  assert.ok(fs.readFileSync(path.join(dir, 'package.json'), 'utf8').includes('wrangler'));
@@ -39,4 +42,20 @@ import { SLASH_COMMANDS, listSlashCommands } from '../src/lib/slash-commands.js'
39
42
  assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/deploy'));
40
43
  assert.equal(listSlashCommands({ lane: 'deploy' }).length, 2);
41
44
 
45
+ printContextSummary({
46
+ iam: { ready: true, source: 'sdk-token', detail: 'AGENTSAM_SDK_TOKEN' },
47
+ gcp: { source: 'vm-metadata', email: 'execos@project.iam.gserviceaccount.com' },
48
+ gcp_vm: true,
49
+ github: { source: 'gh-cli', account: 'connor@example.com' },
50
+ cloudflare: { source: 'wrangler', account: 'connor@cloudflare.test' },
51
+ });
52
+ assert.deepEqual(missingForInit({ iam: { ready: false } }, '', { runTarget: 'local' }), []);
53
+ assert.deepEqual(missingForInit({ iam: { ready: false } }, '', { runTarget: 'cloudflare' }), ['iam']);
54
+
55
+ const localMeta = buildLocalScaffoldMeta({ projectName: 'demo', lane: 'cms', runTarget: 'local' });
56
+ assert.equal(localMeta.laneKey, 'cms');
57
+ assert.ok(localMeta.files.some((f) => f.path === '.agentsam/start-local.md'));
58
+ assert.ok(localMeta.files.some((f) => f.path === 'wrangler.toml'));
59
+ assert.ok(!localMeta.files.some((f) => f.path.includes('execos')));
60
+
42
61
  console.log('SDK smoke tests passed');