@inneranimalmedia/agentsam-sdk 1.2.0 → 1.5.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.
package/README.md CHANGED
@@ -22,24 +22,42 @@ It is not a wrapper around a chat API. It is a command fabric with a conversatio
22
22
 
23
23
  ---
24
24
 
25
- ## Quickstart
25
+ ## Quickstart (local-first — Node only)
26
26
 
27
27
  ```bash
28
28
  npx @inneranimalmedia/agentsam-sdk init
29
29
  ```
30
30
 
31
- Non-interactive (CI / scripts):
31
+ Default path: **localhost**. No IAM login, no Cloudflare OAuth, no accounts. Under 2 minutes with Node 20+ installed.
32
+
33
+ ```bash
34
+ cd my-project
35
+ npm install
36
+ npm run smoke
37
+ npx agentsam start-local # local PTY on ws://127.0.0.1:3099
38
+ npm run dev # http://127.0.0.1:8787
39
+ npm run db:migrate # local D1 schema
40
+ ```
41
+
42
+ When you're ready to ship to **your** Cloudflare account:
43
+
44
+ ```bash
45
+ npx agentsam deploy
46
+ ```
47
+
48
+ Cloudflare OAuth is prompted **only at deploy** — not at init.
49
+
50
+ Non-interactive init:
32
51
 
33
52
  ```bash
34
53
  npx @inneranimalmedia/agentsam-sdk init \
35
54
  --name my-agent \
36
55
  --lane fullstack \
37
- --provider cloudflare \
38
- --agent orchestrator \
56
+ --run-target local \
39
57
  --yes
40
58
  ```
41
59
 
42
- Scaffolded workers expose `GET /health` and `POST /chat` immediately (stub mode without an API key).
60
+ Run targets at init: `local` (default) · `cloudflare` · `gcp` all scaffold locally first; cloud credentials come at deploy.
43
61
 
44
62
  See [DEVELOPMENT.md](./DEVELOPMENT.md) for linking the SDK into Inner Animal Media locally.
45
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inneranimalmedia/agentsam-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.5.1",
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",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "src",
16
+ "templates",
16
17
  "docs",
17
18
  "examples",
18
19
  "test",
@@ -28,6 +29,10 @@
28
29
  "engines": {
29
30
  "node": ">=20"
30
31
  },
32
+ "dependencies": {
33
+ "node-pty": "^1.0.0",
34
+ "ws": "^8.18.0"
35
+ },
31
36
  "publishConfig": {
32
37
  "access": "public"
33
38
  },
package/src/cli.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import readline from 'readline';
4
3
  import pkg from '../package.json' with { type: 'json' };
5
- import { scaffoldProject } from './lib/scaffold.js';
4
+ import readline from 'readline';
5
+ import { buildLocalScaffoldMeta, LANE_KEYS, RUN_TARGETS } from './lib/local-scaffold.js';
6
+ import { writeScaffoldFiles } from './lib/write-files.js';
7
+ import { copyGorillaTemplate } from './lib/gorilla-template.js';
8
+ import { printContextSummary } from './lib/detect-context.js';
9
+ import { promptOptionalByokKeys } from './lib/prompt-byok.js';
10
+ import { runStartLocal } from './commands/start-local.js';
11
+ import { runDeploy } from './commands/deploy.js';
6
12
  import { SLASH_COMMANDS, SHELL_PHASES } from './lib/slash-commands.js';
7
13
 
8
14
  const VERSION = pkg.version;
@@ -15,150 +21,96 @@ function createPrompt() {
15
21
  };
16
22
  }
17
23
 
18
- const LANES = {
19
- '1': 'Full Stack',
20
- '2': 'CMS',
21
- '3': 'Data Solutions',
22
- '4': 'Customer Management',
23
- '5': 'Creative & Design',
24
- };
25
-
26
- const LANE_BY_SLUG = {
27
- fullstack: 'Full Stack',
28
- cms: 'CMS',
29
- data: 'Data Solutions',
30
- crm: 'Customer Management',
31
- creative: 'Creative & Design',
32
- };
33
-
34
- const PROVIDERS = {
35
- '1': 'Cloudflare Workers',
36
- '2': 'GitHub + Cloudflare',
37
- '3': 'Local / Self-hosted',
38
- };
39
-
40
- const PROVIDER_BY_SLUG = {
41
- cloudflare: 'Cloudflare Workers',
42
- github: 'GitHub + Cloudflare',
43
- local: 'Local / Self-hosted',
44
- };
45
-
46
- const AGENTS = {
47
- '1': 'orchestrator',
48
- '2': 'cms',
49
- '3': 'data',
50
- '4': 'crm',
51
- '5': 'creative',
52
- };
53
-
54
- function laneKeyFromLabel(label) {
55
- const entry = Object.entries(LANES).find(([, v]) => v === label);
56
- return entry?.[0] ?? '1';
57
- }
58
-
59
24
  function printHelp() {
60
25
  console.log(`
61
26
  Agent Sam SDK — CLI v${VERSION}
62
27
 
63
28
  Usage:
64
- agentsam init [options] Scaffold a new Agent Sam project
65
- agentsam shell CLI shell UX info + slash commands (Gorilla Shell)
66
- agentsam --version Print version
67
- agentsam --help Show this help
68
-
69
- Init options (non-interactive):
70
- --name <name> Project directory name
29
+ agentsam init Local-first project scaffold (default: localhost, no accounts)
30
+ agentsam start-local Local PTY on ws://127.0.0.1:3099 (no tunnel, no Cloudflare)
31
+ agentsam deploy Graduate to Cloudflare / GCP when ready
32
+ agentsam shell Slash commands + shell UX info
33
+ agentsam --version
34
+ agentsam --help
35
+
36
+ Init is completable with Node only — no IAM login, no OAuth, no Cloudflare.
37
+ Prove locally first; deploy prompts for accounts only when you choose to ship.
38
+
39
+ Init options:
40
+ --name <name> Project directory name
71
41
  --lane <fullstack|cms|data|crm|creative>
72
- --provider <cloudflare|github|local>
73
- --agent <orchestrator|cms|data|crm|creative>
74
- --cf-account <id> Cloudflare account ID
75
- --yes Skip confirmation prompt
42
+ --run-target <local|cloudflare|gcp> Default: local
43
+ --yes Skip confirmation
76
44
  `);
77
45
  }
78
46
 
79
47
  function parseInitArgs(argv) {
80
48
  const opts = {
81
49
  projectName: '',
82
- lane: '',
83
- provider: '',
84
- agent: '',
85
- cfAccountId: '',
50
+ lane: 'fullstack',
51
+ runTarget: 'local',
86
52
  yes: false,
87
53
  };
88
-
89
54
  for (let i = 0; i < argv.length; i += 1) {
90
55
  const arg = argv[i];
91
56
  if (arg === '--yes' || arg === '-y') opts.yes = true;
92
57
  else if (arg === '--name') opts.projectName = argv[++i] || '';
93
- else if (arg === '--lane') {
94
- const slug = argv[++i] || '';
95
- opts.lane = LANE_BY_SLUG[slug] || slug;
96
- } else if (arg === '--provider') {
97
- const slug = argv[++i] || '';
98
- opts.provider = PROVIDER_BY_SLUG[slug] || slug;
99
- } else if (arg === '--agent') opts.agent = argv[++i] || '';
100
- else if (arg === '--cf-account') opts.cfAccountId = argv[++i] || '';
58
+ else if (arg === '--lane') opts.lane = argv[++i] || 'fullstack';
59
+ else if (arg === '--run-target' || arg === '--target') opts.runTarget = argv[++i] || 'local';
101
60
  }
61
+ return opts;
62
+ }
102
63
 
64
+ function parseDeployArgs(argv) {
65
+ const opts = { target: '', accountId: '' };
66
+ for (let i = 0; i < argv.length; i += 1) {
67
+ const arg = argv[i];
68
+ if (arg === '--target') opts.target = argv[++i] || '';
69
+ else if (arg === '--account-id') opts.accountId = argv[++i] || '';
70
+ }
103
71
  return opts;
104
72
  }
105
73
 
106
- async function runInit(config) {
107
- const {
108
- projectName,
109
- lane,
110
- provider,
111
- agent,
112
- cfAccountId = '',
113
- yes = false,
114
- prompt = null,
115
- } = config;
74
+ async function runLocalInit(config) {
75
+ const { projectName, lane, runTarget, prompt } = config;
76
+
77
+ const meta = buildLocalScaffoldMeta(
78
+ { projectName, lane, runTarget },
79
+ VERSION,
80
+ );
116
81
 
117
- const safeDir = projectName.trim();
118
82
  console.log(`
119
83
  ┌─────────────────────────────────────┐
120
- │ Agent Sam Project Config
84
+ │ Agent Sam local-first scaffold
121
85
  ├─────────────────────────────────────┤
122
- Project: ${safeDir.padEnd(25)}│
123
- │ Lane: ${lane.padEnd(25)}│
124
- Provider: ${provider.padEnd(25)}│
125
- │ Agent: ${agent.padEnd(25)}│
126
- │ CF Acct: ${(cfAccountId || 'not set').padEnd(25)}│
86
+ Name: ${meta.projectName.padEnd(25)}│
87
+ │ Lane: ${meta.laneKey.padEnd(25)}│
88
+ Run: ${meta.runTarget.padEnd(25)}│
127
89
  └─────────────────────────────────────┘
128
90
  `);
129
91
 
130
- let confirm = 'y';
131
- if (!yes && prompt) {
132
- confirm = await prompt.ask(' Scaffold project? (y/n): ');
133
- }
92
+ const dir = writeScaffoldFiles(meta.projectName, meta.files);
93
+ copyGorillaTemplate(dir, meta);
134
94
 
135
- if (confirm.toLowerCase() === 'y') {
136
- try {
137
- const dir = scaffoldProject({
138
- projectName: safeDir,
139
- lane,
140
- provider,
141
- agent,
142
- cfAccountId,
143
- sdkVersion: VERSION,
144
- });
145
- console.log(`
146
- ✓ Project created at ${dir}
95
+ console.log(`
96
+ Project ready: ${dir}
97
+ Gorilla Mode UI → gorilla/ (http://localhost:5173 after npm run dev)
147
98
 
148
- Next steps:
149
- cd ${dir.split('/').pop()}
150
- cp .env.example .env
151
- npm install
152
- npm run smoke
153
- npm run dev
154
- `);
155
- } catch (error) {
156
- console.error(`\n ✗ ${error.message}\n`);
157
- process.exitCode = 1;
158
- }
159
- } else {
160
- console.log('\n Cancelled.\n');
99
+ Next steps:`);
100
+ for (const step of meta.next_steps) {
101
+ console.log(` ${step}`);
102
+ }
103
+
104
+ if (prompt && process.env.AGENTSAM_SDK_TOKEN) {
105
+ console.log('\n Optional — BYOK keys for IAM dashboard Agent Sam (skip with Enter):\n');
106
+ await promptOptionalByokKeys(process.env.AGENTSAM_SDK_TOKEN, prompt);
161
107
  }
108
+
109
+ console.log(`
110
+ Local in ~60 seconds:
111
+ cd ${meta.projectName} && npm install && npm run smoke && npm run dev
112
+ open http://localhost:5173
113
+ `);
162
114
  }
163
115
 
164
116
  async function initInteractive(partial = {}) {
@@ -166,66 +118,41 @@ async function initInteractive(partial = {}) {
166
118
 
167
119
  console.log(`
168
120
  ╔═══════════════════════════════════╗
169
- Agent Sam SDK — Init
121
+ Agent Sam SDK — Init
122
+ ║ Local-first · Node only ║
170
123
  ╚═══════════════════════════════════╝
171
124
  `);
172
125
 
173
- const projectName = partial.projectName || (await prompt.ask(' Project name: '));
126
+ printContextSummary(await import('./lib/detect-context.js').then((m) => m.detectContext()));
127
+
128
+ const projectName =
129
+ partial.projectName ||
130
+ (await prompt.ask(' 1) Project name: '));
174
131
 
175
132
  if (!partial.lane) {
176
133
  console.log(`
177
- Select a lane:
178
- 1) Full Stack
179
- 2) CMS
180
- 3) Data Solutions
181
- 4) Customer Management
182
- 5) Creative & Design
134
+ 2) Lane:
135
+ 1) Full Stack 2) CMS 3) Data 4) CRM 5) Creative
183
136
  `);
184
137
  }
185
- const laneKey = partial.lane ? laneKeyFromLabel(partial.lane) : await prompt.ask(' Lane [1-5]: ');
186
- const lane = partial.lane || LANES[laneKey] || 'Full Stack';
187
-
188
- console.log(`
189
- Select a provider:
190
- 1) Cloudflare Workers
191
- 2) GitHub + Cloudflare
192
- 3) Local / Self-hosted
193
- `);
194
- const providerKey = partial.provider
195
- ? Object.entries(PROVIDERS).find(([, v]) => v === partial.provider)?.[0] || '1'
196
- : await prompt.ask(' Provider [1-3]: ');
197
- const provider = partial.provider || PROVIDERS[providerKey] || 'Cloudflare Workers';
138
+ const laneKey = partial.lane
139
+ ? partial.lane
140
+ : LANE_KEYS[await prompt.ask(' Pick lane [1-5]: ')]?.key || 'fullstack';
198
141
 
199
- if (!partial.agent) {
142
+ if (!partial.runTarget) {
200
143
  console.log(`
201
- Select your default Agent Sam:
202
- 1) Orchestrator — general purpose, routes to all lanes
203
- 2) CMS Agent pages, sections, assets, publishing workflows
204
- 3) Data Agent database ops, migrations, queries
205
- 4) CRM Agent — customer management, contacts, billing
206
- 5) Creative Agent — design, 3D, media, content
144
+ 3) Where do you want to run your project?
145
+
146
+ 1) Local (localhost start here, no accounts needed)
147
+ 2) Cloudflare (Workers, D1, R2 deploy when ready)
148
+ 3) GCP (your own Google Cloud project)
207
149
  `);
208
150
  }
209
- const agentKey = partial.agent
210
- ? Object.entries(AGENTS).find(([, v]) => v === partial.agent)?.[0] || '1'
211
- : await prompt.ask(' Agent [1-5]: ');
212
- const agent = partial.agent || AGENTS[agentKey] || 'orchestrator';
213
-
214
- const cfAccountId =
215
- partial.cfAccountId !== undefined && partial.cfAccountId !== ''
216
- ? partial.cfAccountId
217
- : await prompt.ask(' Cloudflare Account ID (enter to skip): ');
218
-
219
- await runInit({
220
- projectName,
221
- lane,
222
- provider,
223
- agent,
224
- cfAccountId,
225
- yes: partial.yes,
226
- prompt,
227
- });
151
+ const runTarget = partial.runTarget
152
+ ? partial.runTarget
153
+ : RUN_TARGETS[await prompt.ask(' Select [1]: ')] || 'local';
228
154
 
155
+ await runLocalInit({ projectName, lane: laneKey, runTarget, prompt });
229
156
  prompt.close();
230
157
  }
231
158
 
@@ -235,15 +162,7 @@ async function initFromArgs(argv) {
235
162
  console.error('\n ✗ --name is required for non-interactive init.\n');
236
163
  process.exit(1);
237
164
  }
238
- await runInit({
239
- projectName: opts.projectName,
240
- lane: opts.lane || 'Full Stack',
241
- provider: opts.provider || 'Cloudflare Workers',
242
- agent: opts.agent || 'orchestrator',
243
- cfAccountId: opts.cfAccountId,
244
- yes: true,
245
- prompt: null,
246
- });
165
+ await runLocalInit({ ...opts, prompt: null });
247
166
  }
248
167
 
249
168
  async function runShellInfo() {
@@ -253,21 +172,14 @@ async function runShellInfo() {
253
172
  ║ Agent Sam Shell (Gorilla) ║
254
173
  ╚═══════════════════════════════════╝
255
174
 
256
- Game-feel terminal UX for SDK installs — HUD, themes, slash commands, PTY bridge.
257
-
258
- Phase 0 prototype: examples/gorilla-shell/
259
- Docs: docs/CLI_SHELL.md
260
- Next milestone: ${next?.label ?? 'PTY connection'}
175
+ Local PTY: agentsam start-local (ws://127.0.0.1:3099)
176
+ Next milestone: ${next?.label ?? 'dashboard bridge after deploy'}
261
177
 
262
178
  Slash commands (${SLASH_COMMANDS.length} registered):
263
179
  `);
264
180
  for (const row of SLASH_COMMANDS) {
265
181
  console.log(` ${row.cmd.padEnd(14)} ${row.description}`);
266
182
  }
267
- console.log(`
268
- Run the visual prototype:
269
- cd examples/gorilla-shell && npm install && npm run dev
270
- `);
271
183
  }
272
184
 
273
185
  const command = process.argv[2];
@@ -279,6 +191,15 @@ if (command === '--version' || command === '-v') {
279
191
  printHelp();
280
192
  } else if (command === 'shell') {
281
193
  await runShellInfo();
194
+ } else if (command === 'start-local') {
195
+ await runStartLocal({});
196
+ } else if (command === 'deploy') {
197
+ try {
198
+ await runDeploy(parseDeployArgs(rest));
199
+ } catch (e) {
200
+ console.error(`\n ✗ ${e?.message || e}\n`);
201
+ process.exit(1);
202
+ }
282
203
  } else if (command === 'init') {
283
204
  const hasFlags = rest.some((a) => a.startsWith('--'));
284
205
  if (hasFlags) {
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Graduate local project to Cloudflare (or GCP hints) — OAuth only here, not at init.
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import readline from 'node:readline';
7
+ import { authenticateViaBrowser } from '../lib/auth.js';
8
+ import { getJson, streamScaffold } from '../lib/core-client.js';
9
+
10
+ function readConfig(cwd) {
11
+ const configPath = path.join(cwd, '.agentsam', 'config.json');
12
+ if (!fs.existsSync(configPath)) {
13
+ throw new Error('Not an Agent Sam project — run agentsam init first.');
14
+ }
15
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
16
+ }
17
+
18
+ function writeConfig(cwd, config) {
19
+ const configPath = path.join(cwd, '.agentsam', 'config.json');
20
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
21
+ }
22
+
23
+ function patchWranglerToml(cwd, cf) {
24
+ const tomlPath = path.join(cwd, 'wrangler.toml');
25
+ if (!fs.existsSync(tomlPath)) return;
26
+ let text = fs.readFileSync(tomlPath, 'utf8');
27
+ if (cf.account_id) {
28
+ if (/^account_id\s*=/m.test(text)) {
29
+ text = text.replace(/^account_id\s*=.*$/m, `account_id = "${cf.account_id}"`);
30
+ } else {
31
+ text = text.replace(/(compatibility_date\s*=.*\n)/, `$1account_id = "${cf.account_id}"\n`);
32
+ }
33
+ }
34
+ if (cf.d1_database_id) {
35
+ text = text.replace(/^database_id\s*=.*$/m, `database_id = "${cf.d1_database_id}"`);
36
+ }
37
+ if (cf.kv_namespace_id) {
38
+ text = text.replace(/^id\s*=.*$/m, `id = "${cf.kv_namespace_id}"`);
39
+ }
40
+ fs.writeFileSync(tomlPath, text, 'utf8');
41
+ }
42
+
43
+ function ask(question) {
44
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
45
+ return new Promise((resolve) => {
46
+ rl.question(question, (answer) => {
47
+ rl.close();
48
+ resolve(answer);
49
+ });
50
+ });
51
+ }
52
+
53
+ async function runCloudflareDeploy(cwd, config, accountId) {
54
+ console.log('\n Cloudflare deploy — browser sign-in + resource provisioning…\n');
55
+
56
+ let token = process.env.AGENTSAM_SDK_TOKEN || '';
57
+ if (!token) {
58
+ const session = await authenticateViaBrowser();
59
+ token = session.access_token;
60
+ console.log(`\n ✓ Signed in (${session.user_id})\n`);
61
+ }
62
+
63
+ const ctx = await getJson('/api/sdk/context', token);
64
+ if (!ctx?.cloudflare?.ok) {
65
+ throw new Error('Connect Cloudflare in IAM Integrations during browser sign-in, then retry.');
66
+ }
67
+
68
+ let complete = null;
69
+ await streamScaffold(
70
+ {
71
+ project_name: config.project,
72
+ lane: config.lane,
73
+ hosting: 'cloudflare',
74
+ provision_only: true,
75
+ account_id: accountId || undefined,
76
+ workspace_id: ctx.workspace_id,
77
+ },
78
+ token,
79
+ async (evt) => {
80
+ if (evt.type === 'log') console.log(` · ${evt.message}`);
81
+ else if (evt.type === 'warn') console.log(` ⚠ ${evt.message}`);
82
+ else if (evt.type === 'account_selection_required') {
83
+ console.log('\n Multiple Cloudflare accounts — re-run with: agentsam deploy --account-id <id>\n');
84
+ for (const a of evt.accounts || []) console.log(` ${a.id} ${a.name || ''}`);
85
+ throw new Error('cloudflare_account_selection_required');
86
+ } else if (evt.type === 'error') throw new Error(evt.error || 'deploy failed');
87
+ else if (evt.type === 'complete') complete = evt;
88
+ },
89
+ );
90
+
91
+ if (!complete?.cloudflare) throw new Error('deploy incomplete — no cloudflare ids returned');
92
+
93
+ patchWranglerToml(cwd, complete.cloudflare);
94
+ writeConfig(cwd, {
95
+ ...config,
96
+ deploy_target: 'cloudflare',
97
+ cloudflare: complete.cloudflare,
98
+ deployed_at: new Date().toISOString(),
99
+ });
100
+
101
+ console.log(`
102
+ ✓ Cloudflare resources provisioned in YOUR account
103
+ ✓ wrangler.toml updated
104
+
105
+ Next:
106
+ npx wrangler deploy
107
+ npm run db:migrate -- --remote # when ready for remote D1
108
+ `);
109
+ }
110
+
111
+ /**
112
+ * @param {{ cwd?: string, target?: string, accountId?: string }} [opts]
113
+ */
114
+ export async function runDeploy(opts = {}) {
115
+ const cwd = path.resolve(opts.cwd || process.cwd());
116
+ const config = readConfig(cwd);
117
+
118
+ let target = opts.target || config.deploy_target || 'cloudflare';
119
+ if (!opts.target && !config.deploy_target) {
120
+ console.log(`
121
+ Where do you want to deploy?
122
+
123
+ 1) Cloudflare (Workers, D1, R2)
124
+ 2) GCP (your Google Cloud project — manual wrangler/container step after)
125
+ `);
126
+ const pick = (await ask(' Select [1]: ')).trim() || '1';
127
+ target = pick === '2' ? 'gcp' : 'cloudflare';
128
+ }
129
+
130
+ if (target === 'gcp') {
131
+ console.log(`
132
+ GCP deploy path:
133
+
134
+ gcloud auth login --no-browser
135
+ gcloud config set project YOUR_PROJECT_ID
136
+ export USER_GCP_PROJECT=YOUR_PROJECT_ID
137
+
138
+ Your local project keeps running with npm run dev.
139
+ Container/Worker deploy scripts are project-specific — add when ready.
140
+ `);
141
+ writeConfig(cwd, { ...config, deploy_target: 'gcp' });
142
+ return;
143
+ }
144
+
145
+ await runCloudflareDeploy(cwd, config, opts.accountId || '');
146
+ }
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { startLocalPtyServer } from '../local-pty/server.js';
4
+
5
+ function readConfig(cwd) {
6
+ const configPath = path.join(cwd, '.agentsam', 'config.json');
7
+ if (!fs.existsSync(configPath)) return null;
8
+ try {
9
+ return JSON.parse(fs.readFileSync(configPath, 'utf8'));
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function findProjectRoot(startDir) {
16
+ let dir = path.resolve(startDir);
17
+ for (let i = 0; i < 12; i += 1) {
18
+ if (fs.existsSync(path.join(dir, '.agentsam', 'config.json'))) return dir;
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir) break;
21
+ dir = parent;
22
+ }
23
+ return path.resolve(startDir);
24
+ }
25
+
26
+ /**
27
+ * @param {{ cwd?: string, port?: number }} [opts]
28
+ */
29
+ export async function runStartLocal(opts = {}) {
30
+ const root = findProjectRoot(opts.cwd || process.cwd());
31
+ const config = readConfig(root);
32
+ const port = opts.port ?? config?.pty_port ?? 3099;
33
+
34
+ console.log(`
35
+ Agent Sam — local PTY
36
+ No Cloudflare · no tunnel · no IAM login
37
+ `);
38
+
39
+ const server = await startLocalPtyServer({ cwd: root, port });
40
+
41
+ console.log(` ✓ PTY listening ${server.url}`);
42
+ console.log(` ✓ Health ${server.healthUrl}`);
43
+ console.log(` ✓ Project root ${server.cwd}`);
44
+ console.log(` ✓ Shell ${server.shell}`);
45
+ console.log(`
46
+ Next:
47
+ npm run dev → http://127.0.0.1:${config?.dev_port ?? 8787}
48
+ npm run db:migrate → local D1 schema (first run)
49
+
50
+ Press Ctrl+C to stop.
51
+ `);
52
+
53
+ const shutdown = async () => {
54
+ await server.close().catch(() => {});
55
+ process.exit(0);
56
+ };
57
+ process.on('SIGINT', shutdown);
58
+ process.on('SIGTERM', shutdown);
59
+
60
+ await new Promise(() => {});
61
+ }