@inneranimalmedia/agentsam-sdk 1.5.1 → 1.7.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/README.md CHANGED
@@ -35,10 +35,12 @@ cd my-project
35
35
  npm install
36
36
  npm run smoke
37
37
  npx agentsam start-local # local PTY on ws://127.0.0.1:3099
38
+ npx agentsam tunnel # cloudflared → register with IAM (dashboard Local lane)
38
39
  npm run dev # http://127.0.0.1:8787
39
40
  npm run db:migrate # local D1 schema
40
41
  ```
41
42
 
43
+ `agentsam tunnel` (default `--quick`) starts a Cloudflare quick tunnel to `:3099` and POSTs the `wss://` URL to `/api/sdk/terminal/register-local` so `agentsam_terminal_local` can reach your machine. Use `--named --tunnel-name … --hostname … --zone-id …` for a stable BYOK named tunnel.
42
44
  When you're ready to ship to **your** Cloudflare account:
43
45
 
44
46
  ```bash
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agentsam scaffold
4
+ * Usage:
5
+ * npx @inneranimalmedia/agentsam-sdk scaffold
6
+ * npx @inneranimalmedia/agentsam-sdk scaffold cms
7
+ * npx @inneranimalmedia/agentsam-sdk scaffold worker-api
8
+ */
9
+
10
+ import { runScaffold } from '../src/lib/scaffold/index.js';
11
+
12
+ const type = process.argv[2] ?? null;
13
+
14
+ runScaffold(type).catch((err) => {
15
+ console.error(err);
16
+ process.exit(1);
17
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneranimalmedia/agentsam-sdk",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Agent Sam is a full-stack AI agent SDK for autonomous task execution — covering data management, creative workflows, design commands, and multi-step agentic pipelines.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -9,7 +9,8 @@
9
9
  "./package.json": "./package.json"
10
10
  },
11
11
  "bin": {
12
- "agentsam": "src/cli.js"
12
+ "agentsam": "src/cli.js",
13
+ "agentsam-scaffold": "bin/scaffold.mjs"
13
14
  },
14
15
  "files": [
15
16
  "src",
@@ -19,7 +20,8 @@
19
20
  "test",
20
21
  "README.md",
21
22
  "LICENSE",
22
- "DEVELOPMENT.md"
23
+ "DEVELOPMENT.md",
24
+ "bin"
23
25
  ],
24
26
  "scripts": {
25
27
  "test": "node test/smoke.mjs",
@@ -30,7 +32,9 @@
30
32
  "node": ">=20"
31
33
  },
32
34
  "dependencies": {
35
+ "@clack/prompts": "^1.7.0",
33
36
  "node-pty": "^1.0.0",
37
+ "picocolors": "^1.1.1",
34
38
  "ws": "^8.18.0"
35
39
  },
36
40
  "publishConfig": {
@@ -54,5 +58,8 @@
54
58
  "bugs": {
55
59
  "url": "https://github.com/SamPrimeaux/agentsam-sdk/issues"
56
60
  },
57
- "homepage": "https://github.com/SamPrimeaux/agentsam-sdk#readme"
61
+ "homepage": "https://github.com/SamPrimeaux/agentsam-sdk#readme",
62
+ "allowScripts": {
63
+ "node-pty@1.1.0": true
64
+ }
58
65
  }
package/src/cli.js CHANGED
@@ -8,6 +8,7 @@ import { copyGorillaTemplate } from './lib/gorilla-template.js';
8
8
  import { printContextSummary } from './lib/detect-context.js';
9
9
  import { promptOptionalByokKeys } from './lib/prompt-byok.js';
10
10
  import { runStartLocal } from './commands/start-local.js';
11
+ import { runTunnel } from './commands/tunnel.js';
11
12
  import { runDeploy } from './commands/deploy.js';
12
13
  import { SLASH_COMMANDS, SHELL_PHASES } from './lib/slash-commands.js';
13
14
 
@@ -28,6 +29,7 @@ function printHelp() {
28
29
  Usage:
29
30
  agentsam init Local-first project scaffold (default: localhost, no accounts)
30
31
  agentsam start-local Local PTY on ws://127.0.0.1:3099 (no tunnel, no Cloudflare)
32
+ agentsam tunnel Expose local PTY to IAM (cloudflared + register)
31
33
  agentsam deploy Graduate to Cloudflare / GCP when ready
32
34
  agentsam shell Slash commands + shell UX info
33
35
  agentsam --version
@@ -36,6 +38,12 @@ function printHelp() {
36
38
  Init is completable with Node only — no IAM login, no OAuth, no Cloudflare.
37
39
  Prove locally first; deploy prompts for accounts only when you choose to ship.
38
40
 
41
+ Tunnel options:
42
+ --quick Quick tunnel (default) — trycloudflare.com URL
43
+ --named Named CF tunnel (needs --tunnel-name --hostname --zone-id)
44
+ --port <n> Local PTY port (default 3099)
45
+ --token <sdk_…> Use existing AGENTSAM_SDK_TOKEN (skip browser auth)
46
+
39
47
  Init options:
40
48
  --name <name> Project directory name
41
49
  --lane <fullstack|cms|data|crm|creative>
@@ -193,6 +201,13 @@ if (command === '--version' || command === '-v') {
193
201
  await runShellInfo();
194
202
  } else if (command === 'start-local') {
195
203
  await runStartLocal({});
204
+ } else if (command === 'tunnel') {
205
+ try {
206
+ await runTunnel(rest);
207
+ } catch (e) {
208
+ console.error(`\n ✗ ${e?.message || e}\n`);
209
+ process.exit(1);
210
+ }
196
211
  } else if (command === 'deploy') {
197
212
  try {
198
213
  await runDeploy(parseDeployArgs(rest));
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AgentSam HTTP API server for containerized deployments
4
+ * Exposes the AgentSam SDK as HTTP endpoints (compatible with Cloudflare Workers pattern)
5
+ */
6
+ import http from 'node:http';
7
+ import { AgentSam } from './AgentSam.js';
8
+
9
+ const PORT = process.env.PORT || 8080;
10
+ const HOST = process.env.HOST || '0.0.0.0';
11
+
12
+ const agentSam = new AgentSam({
13
+ env: process.env,
14
+ agent: process.env.AGENT || 'orchestrator',
15
+ lane: process.env.LANE || 'fullstack',
16
+ project: process.env.PROJECT || 'agentsam-docker',
17
+ });
18
+
19
+ /**
20
+ * Adapt Web API Request to Node HTTP
21
+ */
22
+ class NodeRequest {
23
+ constructor(req, body) {
24
+ this.url = `http://${req.headers.host || 'localhost'}${req.url}`;
25
+ this.method = req.method;
26
+ this.headers = req.headers;
27
+ this.body = body;
28
+ }
29
+
30
+ async json() {
31
+ if (!this.body) return {};
32
+ try {
33
+ return JSON.parse(this.body);
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+ }
39
+
40
+ const server = http.createServer(async (req, res) => {
41
+ let body = '';
42
+
43
+ // Read request body
44
+ req.on('data', (chunk) => {
45
+ body += chunk.toString();
46
+ });
47
+
48
+ req.on('end', async () => {
49
+ try {
50
+ const request = new NodeRequest(req, body);
51
+ const response = await agentSam.handle(request);
52
+
53
+ // Write response headers
54
+ const headers = {};
55
+ if (response.headers && response.headers.entries) {
56
+ for (const [key, value] of response.headers.entries()) {
57
+ headers[key] = value;
58
+ }
59
+ }
60
+ res.writeHead(response.status, headers);
61
+
62
+ // Write response body
63
+ const text = await response.text();
64
+ res.end(text);
65
+ } catch (error) {
66
+ console.error('Error handling request:', error);
67
+ res.writeHead(500, { 'Content-Type': 'application/json' });
68
+ res.end(JSON.stringify({ ok: false, error: error.message }));
69
+ }
70
+ });
71
+ });
72
+
73
+ server.listen(PORT, HOST, () => {
74
+ console.log(`
75
+ ✓ Agent Sam API Server listening on http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}
76
+
77
+ Routes:
78
+ GET /api/health → service health
79
+ GET /api/agentsam/info → agent capabilities
80
+ POST /api/agentsam/session → create session
81
+ GET /api/agentsam/session/:id → get session
82
+ POST /api/agentsam/message → route message
83
+
84
+ Environment: AGENT=${process.env.AGENT || 'orchestrator'} LANE=${process.env.LANE || 'fullstack'}
85
+ `);
86
+ });
87
+
88
+ // Graceful shutdown
89
+ const shutdown = async () => {
90
+ console.log('\nShutting down...');
91
+ server.close(() => {
92
+ process.exit(0);
93
+ });
94
+ setTimeout(() => process.exit(1), 5000);
95
+ };
96
+ process.on('SIGINT', shutdown);
97
+ process.on('SIGTERM', shutdown);
@@ -0,0 +1,232 @@
1
+ /**
2
+ * agentsam tunnel — expose local PTY (:3099) to the IAM platform.
3
+ *
4
+ * Default (--quick): cloudflared quick tunnel → register ws_url via SDK API.
5
+ * Named (--named): platform provisions CF named tunnel + DNS; run with --token.
6
+ */
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+ import { authenticateViaBrowser } from '../lib/auth.js';
9
+ import { postJson } from '../lib/core-client.js';
10
+
11
+ const DEFAULT_PORT = 3099;
12
+
13
+ function parseArgs(argv) {
14
+ const opts = {
15
+ mode: 'quick',
16
+ port: DEFAULT_PORT,
17
+ tunnelName: '',
18
+ hostname: '',
19
+ zoneId: '',
20
+ platform: process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux',
21
+ shell: process.platform === 'win32' ? 'powershell' : process.env.SHELL || '/bin/zsh',
22
+ skipAuth: false,
23
+ };
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const a = argv[i];
26
+ if (a === '--quick') opts.mode = 'quick';
27
+ else if (a === '--named') opts.mode = 'named';
28
+ else if (a === '--port') opts.port = Number(argv[++i]) || DEFAULT_PORT;
29
+ else if (a === '--tunnel-name') opts.tunnelName = argv[++i] || '';
30
+ else if (a === '--hostname') opts.hostname = argv[++i] || '';
31
+ else if (a === '--zone-id') opts.zoneId = argv[++i] || '';
32
+ else if (a === '--platform') opts.platform = argv[++i] || opts.platform;
33
+ else if (a === '--shell') opts.shell = argv[++i] || opts.shell;
34
+ else if (a === '--token' && argv[i + 1]) {
35
+ process.env.AGENTSAM_SDK_TOKEN = argv[++i];
36
+ }
37
+ }
38
+ return opts;
39
+ }
40
+
41
+ function ensureCloudflared() {
42
+ const which = spawnSync(process.platform === 'win32' ? 'where' : 'which', ['cloudflared'], {
43
+ encoding: 'utf8',
44
+ });
45
+ if (which.status !== 0) {
46
+ throw new Error(
47
+ 'cloudflared not found. Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/',
48
+ );
49
+ }
50
+ }
51
+
52
+ async function resolveToken() {
53
+ const existing = String(process.env.AGENTSAM_SDK_TOKEN || '').trim();
54
+ if (existing.startsWith('sdk_')) return existing;
55
+ const session = await authenticateViaBrowser();
56
+ const tok = String(session?.access_token || '').trim();
57
+ if (!tok.startsWith('sdk_')) throw new Error('IAM auth did not return an sdk_ bearer token');
58
+ process.env.AGENTSAM_SDK_TOKEN = tok;
59
+ return tok;
60
+ }
61
+
62
+ async function assertLocalPty(port) {
63
+ const url = `http://127.0.0.1:${port}/health`;
64
+ try {
65
+ const res = await fetch(url, { signal: AbortSignal.timeout(2500) });
66
+ if (!res.ok) throw new Error(`health ${res.status}`);
67
+ return true;
68
+ } catch {
69
+ throw new Error(
70
+ `Local PTY not reachable at ${url}. In another terminal run: npx agentsam start-local`,
71
+ );
72
+ }
73
+ }
74
+
75
+ function httpsToWss(url) {
76
+ const u = String(url || '').trim();
77
+ if (!u) return '';
78
+ if (u.startsWith('wss://') || u.startsWith('ws://')) return u.replace(/\/$/, '');
79
+ if (u.startsWith('https://')) return `wss://${u.slice(8)}`.replace(/\/$/, '');
80
+ if (u.startsWith('http://')) return `ws://${u.slice(7)}`.replace(/\/$/, '');
81
+ return `wss://${u.replace(/^\/+/, '')}`.replace(/\/$/, '');
82
+ }
83
+
84
+ /**
85
+ * Parse trycloudflare.com URL from cloudflared stderr/stdout.
86
+ * @param {import('node:child_process').ChildProcessWithoutNullStreams} child
87
+ * @returns {Promise<string>}
88
+ */
89
+ function waitForQuickTunnelUrl(child) {
90
+ return new Promise((resolve, reject) => {
91
+ let buf = '';
92
+ const timer = setTimeout(() => {
93
+ reject(new Error('Timed out waiting for cloudflared quick tunnel URL (60s)'));
94
+ }, 60_000);
95
+
96
+ const onData = (chunk) => {
97
+ const text = chunk.toString();
98
+ buf += text;
99
+ process.stderr.write(text);
100
+ const m =
101
+ buf.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i) ||
102
+ buf.match(/https:\/\/[a-z0-9.-]+\.cfargotunnel\.com/i);
103
+ if (m) {
104
+ clearTimeout(timer);
105
+ child.stdout?.off('data', onData);
106
+ child.stderr?.off('data', onData);
107
+ resolve(m[0]);
108
+ }
109
+ };
110
+
111
+ child.stdout?.on('data', onData);
112
+ child.stderr?.on('data', onData);
113
+ child.on('error', (e) => {
114
+ clearTimeout(timer);
115
+ reject(e);
116
+ });
117
+ child.on('exit', (code) => {
118
+ clearTimeout(timer);
119
+ reject(new Error(`cloudflared exited early (code ${code})`));
120
+ });
121
+ });
122
+ }
123
+
124
+ async function runQuick(opts, token) {
125
+ await assertLocalPty(opts.port);
126
+ ensureCloudflared();
127
+
128
+ console.log(`
129
+ Agent Sam — tunnel (quick)
130
+ Local PTY http://127.0.0.1:${opts.port}
131
+ Mode cloudflared quick tunnel → IAM register-local
132
+ `);
133
+
134
+ const child = spawn(
135
+ 'cloudflared',
136
+ ['tunnel', '--url', `http://127.0.0.1:${opts.port}`, '--no-autoupdate'],
137
+ { stdio: ['ignore', 'pipe', 'pipe'] },
138
+ );
139
+
140
+ const publicUrl = await waitForQuickTunnelUrl(child);
141
+ const wsUrl = httpsToWss(publicUrl);
142
+ console.log(`\n ✓ Public URL ${publicUrl}`);
143
+ console.log(` ✓ Registering ${wsUrl}\n`);
144
+
145
+ const registered = await postJson(
146
+ '/api/sdk/terminal/register-local',
147
+ {
148
+ ws_url: wsUrl,
149
+ platform: opts.platform,
150
+ shell: opts.shell,
151
+ },
152
+ token,
153
+ );
154
+
155
+ console.log(` ✓ IAM local lane active`);
156
+ if (registered?.connection?.id) {
157
+ console.log(` ✓ connection ${registered.connection.id}`);
158
+ }
159
+ console.log(`
160
+ Keep this process running. In the dashboard: Terminal → Local.
161
+
162
+ Ctrl+C stops the tunnel.
163
+ `);
164
+
165
+ await new Promise((resolve) => {
166
+ child.on('exit', resolve);
167
+ process.on('SIGINT', () => {
168
+ child.kill('SIGINT');
169
+ });
170
+ process.on('SIGTERM', () => {
171
+ child.kill('SIGTERM');
172
+ });
173
+ });
174
+ }
175
+
176
+ async function runNamed(opts, token) {
177
+ if (!opts.tunnelName || !opts.hostname || !opts.zoneId) {
178
+ throw new Error(
179
+ 'Named mode requires --tunnel-name, --hostname, and --zone-id (from your CF zone).',
180
+ );
181
+ }
182
+ await assertLocalPty(opts.port);
183
+ ensureCloudflared();
184
+
185
+ console.log(`
186
+ Agent Sam — tunnel (named)
187
+ Provisioning Cloudflare tunnel ${opts.tunnelName} → ${opts.hostname}
188
+ `);
189
+
190
+ const provisioned = await postJson(
191
+ '/api/sdk/terminal/tunnel/provision',
192
+ {
193
+ tunnel_name: opts.tunnelName,
194
+ hostname: opts.hostname,
195
+ zone_id: opts.zoneId,
196
+ port: opts.port,
197
+ platform: opts.platform,
198
+ shell: opts.shell,
199
+ },
200
+ token,
201
+ );
202
+
203
+ const runToken = String(provisioned?.run_token || '').trim();
204
+ if (!runToken) throw new Error('Platform did not return a cloudflared run_token');
205
+
206
+ console.log(` ✓ ws_url ${provisioned.ws_url || `wss://${opts.hostname}`}`);
207
+ console.log(` ✓ Starting cloudflared tunnel run --token …\n`);
208
+
209
+ const child = spawn('cloudflared', ['tunnel', 'run', '--token', runToken, '--no-autoupdate'], {
210
+ stdio: 'inherit',
211
+ });
212
+
213
+ await new Promise((resolve, reject) => {
214
+ child.on('error', reject);
215
+ child.on('exit', resolve);
216
+ process.on('SIGINT', () => child.kill('SIGINT'));
217
+ });
218
+ }
219
+
220
+ /**
221
+ * @param {string[]} [argv]
222
+ */
223
+ export async function runTunnel(argv = []) {
224
+ const opts = parseArgs(argv);
225
+ const token = await resolveToken();
226
+
227
+ if (opts.mode === 'named') {
228
+ await runNamed(opts, token);
229
+ } else {
230
+ await runQuick(opts, token);
231
+ }
232
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @inneranimalmedia/agentsam-sdk — scaffold system
3
+ * Entry point for all guided scaffold wizards.
4
+ *
5
+ * Usage:
6
+ * npx @inneranimalmedia/agentsam-sdk scaffold
7
+ * npx @inneranimalmedia/agentsam-sdk scaffold cms
8
+ * npx @inneranimalmedia/agentsam-sdk scaffold worker-api
9
+ */
10
+
11
+ import { intro, outro, select, cancel, isCancel, note } from '@clack/prompts';
12
+ import { runCmsWizard } from './wizards/cms.js';
13
+ import { runWorkerApiWizard } from './wizards/worker-api.js';
14
+ import pc from 'picocolors';
15
+
16
+ const SCAFFOLDS = {
17
+ cms: {
18
+ label: 'CMS Site',
19
+ description: 'Cloudflare Worker + D1 + R2 with nav, pages, and reusable templates',
20
+ run: runCmsWizard,
21
+ },
22
+ 'worker-api': {
23
+ label: 'Worker API',
24
+ description: 'Bare Cloudflare Worker with typed route handlers and D1 binding',
25
+ run: runWorkerApiWizard,
26
+ },
27
+ };
28
+
29
+ export async function runScaffold(type) {
30
+ intro(pc.bgCyan(pc.black(' Agent Sam Scaffold ')));
31
+
32
+ // If a type was passed directly (e.g. `scaffold cms`), run it
33
+ if (type && SCAFFOLDS[type]) {
34
+ await SCAFFOLDS[type].run();
35
+ outro(pc.green('Done. Files written — check the output above.'));
36
+ return;
37
+ }
38
+
39
+ if (type && !SCAFFOLDS[type]) {
40
+ note(`Unknown scaffold type: "${type}"\nAvailable: ${Object.keys(SCAFFOLDS).join(', ')}`, 'Error');
41
+ process.exit(1);
42
+ }
43
+
44
+ // No type passed — show picker
45
+ const choice = await select({
46
+ message: 'What do you want to scaffold?',
47
+ options: Object.entries(SCAFFOLDS).map(([value, { label, description }]) => ({
48
+ value,
49
+ label,
50
+ hint: description,
51
+ })),
52
+ });
53
+
54
+ if (isCancel(choice)) {
55
+ cancel('Cancelled.');
56
+ process.exit(0);
57
+ }
58
+
59
+ await SCAFFOLDS[choice].run();
60
+ outro(pc.green('Done. Files written — check the output above.'));
61
+ }