@inneranimalmedia/agentsam-sdk 1.1.1 → 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.
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 { printContextSummary } from './lib/detect-context.js';
8
+ import { promptOptionalByokKeys } from './lib/prompt-byok.js';
9
+ import { runStartLocal } from './commands/start-local.js';
10
+ import { runDeploy } from './commands/deploy.js';
11
+ import { SLASH_COMMANDS, SHELL_PHASES } from './lib/slash-commands.js';
6
12
 
7
13
  const VERSION = pkg.version;
8
14
 
@@ -14,149 +20,95 @@ function createPrompt() {
14
20
  };
15
21
  }
16
22
 
17
- const LANES = {
18
- '1': 'Full Stack',
19
- '2': 'CMS',
20
- '3': 'Data Solutions',
21
- '4': 'Customer Management',
22
- '5': 'Creative & Design',
23
- };
24
-
25
- const LANE_BY_SLUG = {
26
- fullstack: 'Full Stack',
27
- cms: 'CMS',
28
- data: 'Data Solutions',
29
- crm: 'Customer Management',
30
- creative: 'Creative & Design',
31
- };
32
-
33
- const PROVIDERS = {
34
- '1': 'Cloudflare Workers',
35
- '2': 'GitHub + Cloudflare',
36
- '3': 'Local / Self-hosted',
37
- };
38
-
39
- const PROVIDER_BY_SLUG = {
40
- cloudflare: 'Cloudflare Workers',
41
- github: 'GitHub + Cloudflare',
42
- local: 'Local / Self-hosted',
43
- };
44
-
45
- const AGENTS = {
46
- '1': 'orchestrator',
47
- '2': 'cms',
48
- '3': 'data',
49
- '4': 'crm',
50
- '5': 'creative',
51
- };
52
-
53
- function laneKeyFromLabel(label) {
54
- const entry = Object.entries(LANES).find(([, v]) => v === label);
55
- return entry?.[0] ?? '1';
56
- }
57
-
58
23
  function printHelp() {
59
24
  console.log(`
60
25
  Agent Sam SDK — CLI v${VERSION}
61
26
 
62
27
  Usage:
63
- agentsam init [options] Scaffold a new Agent Sam project
64
- agentsam --version Print version
65
- agentsam --help Show this help
66
-
67
- Init options (non-interactive):
68
- --name <name> Project directory name
28
+ agentsam init Local-first project scaffold (default: localhost, no accounts)
29
+ agentsam start-local Local PTY on ws://127.0.0.1:3099 (no tunnel, no Cloudflare)
30
+ agentsam deploy Graduate to Cloudflare / GCP when ready
31
+ agentsam shell Slash commands + shell UX info
32
+ agentsam --version
33
+ agentsam --help
34
+
35
+ Init is completable with Node only — no IAM login, no OAuth, no Cloudflare.
36
+ Prove locally first; deploy prompts for accounts only when you choose to ship.
37
+
38
+ Init options:
39
+ --name <name> Project directory name
69
40
  --lane <fullstack|cms|data|crm|creative>
70
- --provider <cloudflare|github|local>
71
- --agent <orchestrator|cms|data|crm|creative>
72
- --cf-account <id> Cloudflare account ID
73
- --yes Skip confirmation prompt
41
+ --run-target <local|cloudflare|gcp> Default: local
42
+ --yes Skip confirmation
74
43
  `);
75
44
  }
76
45
 
77
46
  function parseInitArgs(argv) {
78
47
  const opts = {
79
48
  projectName: '',
80
- lane: '',
81
- provider: '',
82
- agent: '',
83
- cfAccountId: '',
49
+ lane: 'fullstack',
50
+ runTarget: 'local',
84
51
  yes: false,
85
52
  };
86
-
87
53
  for (let i = 0; i < argv.length; i += 1) {
88
54
  const arg = argv[i];
89
55
  if (arg === '--yes' || arg === '-y') opts.yes = true;
90
56
  else if (arg === '--name') opts.projectName = argv[++i] || '';
91
- else if (arg === '--lane') {
92
- const slug = argv[++i] || '';
93
- opts.lane = LANE_BY_SLUG[slug] || slug;
94
- } else if (arg === '--provider') {
95
- const slug = argv[++i] || '';
96
- opts.provider = PROVIDER_BY_SLUG[slug] || slug;
97
- } else if (arg === '--agent') opts.agent = argv[++i] || '';
98
- else if (arg === '--cf-account') opts.cfAccountId = argv[++i] || '';
57
+ else if (arg === '--lane') opts.lane = argv[++i] || 'fullstack';
58
+ else if (arg === '--run-target' || arg === '--target') opts.runTarget = argv[++i] || 'local';
99
59
  }
60
+ return opts;
61
+ }
100
62
 
63
+ function parseDeployArgs(argv) {
64
+ const opts = { target: '', accountId: '' };
65
+ for (let i = 0; i < argv.length; i += 1) {
66
+ const arg = argv[i];
67
+ if (arg === '--target') opts.target = argv[++i] || '';
68
+ else if (arg === '--account-id') opts.accountId = argv[++i] || '';
69
+ }
101
70
  return opts;
102
71
  }
103
72
 
104
- async function runInit(config) {
105
- const {
106
- projectName,
107
- lane,
108
- provider,
109
- agent,
110
- cfAccountId = '',
111
- yes = false,
112
- prompt = null,
113
- } = config;
73
+ async function runLocalInit(config) {
74
+ const { projectName, lane, runTarget, prompt } = config;
75
+
76
+ const meta = buildLocalScaffoldMeta(
77
+ { projectName, lane, runTarget },
78
+ VERSION,
79
+ );
114
80
 
115
- const safeDir = projectName.trim();
116
81
  console.log(`
117
82
  ┌─────────────────────────────────────┐
118
- │ Agent Sam Project Config │
83
+ │ Agent Sam — local-first scaffold │
119
84
  ├─────────────────────────────────────┤
120
- │ Project: ${safeDir.padEnd(25)}│
121
- │ Lane: ${lane.padEnd(25)}│
122
- │ Provider: ${provider.padEnd(25)}│
123
- │ Agent: ${agent.padEnd(25)}│
124
- │ CF Acct: ${(cfAccountId || 'not set').padEnd(25)}│
85
+ │ Name: ${meta.projectName.padEnd(25)}│
86
+ │ Lane: ${meta.laneKey.padEnd(25)}│
87
+ │ Run: ${meta.runTarget.padEnd(25)}│
125
88
  └─────────────────────────────────────┘
126
89
  `);
127
90
 
128
- let confirm = 'y';
129
- if (!yes && prompt) {
130
- confirm = await prompt.ask(' Scaffold project? (y/n): ');
91
+ const dir = writeScaffoldFiles(meta.projectName, meta.files);
92
+
93
+ console.log(`
94
+ ✓ Project ready: ${dir}
95
+
96
+ Next steps:`);
97
+ for (const step of meta.next_steps) {
98
+ console.log(` ${step}`);
131
99
  }
132
100
 
133
- if (confirm.toLowerCase() === 'y') {
134
- try {
135
- const dir = scaffoldProject({
136
- projectName: safeDir,
137
- lane,
138
- provider,
139
- agent,
140
- cfAccountId,
141
- sdkVersion: VERSION,
142
- });
143
- console.log(`
144
- ✓ Project created at ${dir}
101
+ if (prompt && process.env.AGENTSAM_SDK_TOKEN) {
102
+ console.log('\n Optional — BYOK keys for IAM dashboard Agent Sam (skip with Enter):\n');
103
+ await promptOptionalByokKeys(process.env.AGENTSAM_SDK_TOKEN, prompt);
104
+ }
145
105
 
146
- Next steps:
147
- cd ${dir.split('/').pop()}
148
- cp .env.example .env
149
- npm install
150
- npm run smoke
106
+ console.log(`
107
+ Local in ~60 seconds:
108
+ cd ${meta.projectName} && npm install && npm run smoke
109
+ npx agentsam start-local
151
110
  npm run dev
152
- `);
153
- } catch (error) {
154
- console.error(`\n ✗ ${error.message}\n`);
155
- process.exitCode = 1;
156
- }
157
- } else {
158
- console.log('\n Cancelled.\n');
159
- }
111
+ `);
160
112
  }
161
113
 
162
114
  async function initInteractive(partial = {}) {
@@ -164,66 +116,41 @@ async function initInteractive(partial = {}) {
164
116
 
165
117
  console.log(`
166
118
  ╔═══════════════════════════════════╗
167
- ║ Agent Sam SDK — Init ║
119
+ ║ Agent Sam SDK — Init ║
120
+ ║ Local-first · Node only ║
168
121
  ╚═══════════════════════════════════╝
169
122
  `);
170
123
 
171
- const projectName = partial.projectName || (await prompt.ask(' Project name: '));
124
+ printContextSummary(await import('./lib/detect-context.js').then((m) => m.detectContext()));
125
+
126
+ const projectName =
127
+ partial.projectName ||
128
+ (await prompt.ask(' 1) Project name: '));
172
129
 
173
130
  if (!partial.lane) {
174
131
  console.log(`
175
- Select a lane:
176
- 1) Full Stack
177
- 2) CMS
178
- 3) Data Solutions
179
- 4) Customer Management
180
- 5) Creative & Design
132
+ 2) Lane:
133
+ 1) Full Stack 2) CMS 3) Data 4) CRM 5) Creative
181
134
  `);
182
135
  }
183
- const laneKey = partial.lane ? laneKeyFromLabel(partial.lane) : await prompt.ask(' Lane [1-5]: ');
184
- const lane = partial.lane || LANES[laneKey] || 'Full Stack';
136
+ const laneKey = partial.lane
137
+ ? partial.lane
138
+ : LANE_KEYS[await prompt.ask(' Pick lane [1-5]: ')]?.key || 'fullstack';
185
139
 
186
- console.log(`
187
- Select a provider:
188
- 1) Cloudflare Workers
189
- 2) GitHub + Cloudflare
190
- 3) Local / Self-hosted
191
- `);
192
- const providerKey = partial.provider
193
- ? Object.entries(PROVIDERS).find(([, v]) => v === partial.provider)?.[0] || '1'
194
- : await prompt.ask(' Provider [1-3]: ');
195
- const provider = partial.provider || PROVIDERS[providerKey] || 'Cloudflare Workers';
196
-
197
- if (!partial.agent) {
140
+ if (!partial.runTarget) {
198
141
  console.log(`
199
- Select your default Agent Sam:
200
- 1) Orchestrator — general purpose, routes to all lanes
201
- 2) CMS Agent — pages, sections, assets, publishing workflows
202
- 3) Data Agent — database ops, migrations, queries
203
- 4) CRM Agent — customer management, contacts, billing
204
- 5) Creative Agent — design, 3D, media, content
142
+ 3) Where do you want to run your project?
143
+
144
+ 1) Local (localhost — start here, no accounts needed)
145
+ 2) Cloudflare (Workers, D1, R2 — deploy when ready)
146
+ 3) GCP (your own Google Cloud project)
205
147
  `);
206
148
  }
207
- const agentKey = partial.agent
208
- ? Object.entries(AGENTS).find(([, v]) => v === partial.agent)?.[0] || '1'
209
- : await prompt.ask(' Agent [1-5]: ');
210
- const agent = partial.agent || AGENTS[agentKey] || 'orchestrator';
211
-
212
- const cfAccountId =
213
- partial.cfAccountId !== undefined && partial.cfAccountId !== ''
214
- ? partial.cfAccountId
215
- : await prompt.ask(' Cloudflare Account ID (enter to skip): ');
216
-
217
- await runInit({
218
- projectName,
219
- lane,
220
- provider,
221
- agent,
222
- cfAccountId,
223
- yes: partial.yes,
224
- prompt,
225
- });
149
+ const runTarget = partial.runTarget
150
+ ? partial.runTarget
151
+ : RUN_TARGETS[await prompt.ask(' Select [1]: ')] || 'local';
226
152
 
153
+ await runLocalInit({ projectName, lane: laneKey, runTarget, prompt });
227
154
  prompt.close();
228
155
  }
229
156
 
@@ -233,15 +160,24 @@ async function initFromArgs(argv) {
233
160
  console.error('\n ✗ --name is required for non-interactive init.\n');
234
161
  process.exit(1);
235
162
  }
236
- await runInit({
237
- projectName: opts.projectName,
238
- lane: opts.lane || 'Full Stack',
239
- provider: opts.provider || 'Cloudflare Workers',
240
- agent: opts.agent || 'orchestrator',
241
- cfAccountId: opts.cfAccountId,
242
- yes: true,
243
- prompt: null,
244
- });
163
+ await runLocalInit({ ...opts, prompt: null });
164
+ }
165
+
166
+ async function runShellInfo() {
167
+ const next = SHELL_PHASES.find((p) => p.status === 'next');
168
+ console.log(`
169
+ ╔═══════════════════════════════════╗
170
+ ║ Agent Sam Shell (Gorilla) ║
171
+ ╚═══════════════════════════════════╝
172
+
173
+ Local PTY: agentsam start-local (ws://127.0.0.1:3099)
174
+ Next milestone: ${next?.label ?? 'dashboard bridge after deploy'}
175
+
176
+ Slash commands (${SLASH_COMMANDS.length} registered):
177
+ `);
178
+ for (const row of SLASH_COMMANDS) {
179
+ console.log(` ${row.cmd.padEnd(14)} ${row.description}`);
180
+ }
245
181
  }
246
182
 
247
183
  const command = process.argv[2];
@@ -251,6 +187,17 @@ if (command === '--version' || command === '-v') {
251
187
  console.log(VERSION);
252
188
  } else if (command === '--help' || command === '-h' || !command) {
253
189
  printHelp();
190
+ } else if (command === 'shell') {
191
+ await runShellInfo();
192
+ } else if (command === 'start-local') {
193
+ await runStartLocal({});
194
+ } else if (command === 'deploy') {
195
+ try {
196
+ await runDeploy(parseDeployArgs(rest));
197
+ } catch (e) {
198
+ console.error(`\n ✗ ${e?.message || e}\n`);
199
+ process.exit(1);
200
+ }
254
201
  } else if (command === 'init') {
255
202
  const hasFlags = rest.some((a) => a.startsWith('--'));
256
203
  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
+ }
package/src/index.js CHANGED
@@ -6,6 +6,12 @@ export { AgentSam } from './AgentSam.js';
6
6
  export { routeIntent } from './lib/router.js';
7
7
  export { getToolCatalog } from './lib/tools.js';
8
8
  export { scaffoldProject } from './lib/scaffold.js';
9
+ export {
10
+ SLASH_COMMANDS,
11
+ SHELL_THEMES,
12
+ SHELL_PHASES,
13
+ listSlashCommands,
14
+ } from './lib/slash-commands.js';
9
15
 
10
16
  export const version = pkg.version;
11
17
  export const name = pkg.name;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Browser OAuth for SDK init — one click IAM login + Cloudflare connect.
3
+ */
4
+ import http from 'http';
5
+ import { randomBytes } from 'crypto';
6
+ import { postJson } from './core-client.js';
7
+
8
+ function randomState() {
9
+ return randomBytes(16).toString('hex');
10
+ }
11
+
12
+ function openBrowser(url) {
13
+ const start =
14
+ process.platform === 'darwin'
15
+ ? ['open', url]
16
+ : process.platform === 'win32'
17
+ ? ['cmd', '/c', 'start', '', url]
18
+ : ['xdg-open', url];
19
+ import('child_process').then(({ spawn }) => {
20
+ spawn(start[0], start.slice(1), { stdio: 'ignore', detached: true }).unref();
21
+ }).catch(() => {
22
+ console.log(`\n Open in browser:\n ${url}\n`);
23
+ });
24
+ }
25
+
26
+ /**
27
+ * @returns {Promise<{ access_token: string, user_id: string, workspace_id: string, tenant_id: string }>}
28
+ */
29
+ export async function authenticateViaBrowser() {
30
+ const state = randomState();
31
+ const port = 8791 + (randomBytes(1)[0] % 20);
32
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
33
+
34
+ const { auth_url: authUrl } = await postJson('/api/sdk/auth/start', {
35
+ redirect_uri: redirectUri,
36
+ state,
37
+ });
38
+
39
+ const codePromise = new Promise((resolve, reject) => {
40
+ const server = http.createServer((req, res) => {
41
+ try {
42
+ const u = new URL(req.url || '/', `http://127.0.0.1:${port}`);
43
+ if (u.pathname !== '/callback') {
44
+ res.writeHead(404);
45
+ res.end('Not found');
46
+ return;
47
+ }
48
+ const code = u.searchParams.get('code');
49
+ const gotState = u.searchParams.get('state');
50
+ if (!code || gotState !== state) {
51
+ res.writeHead(400);
52
+ res.end('Invalid callback');
53
+ reject(new Error('auth callback invalid'));
54
+ server.close();
55
+ return;
56
+ }
57
+ res.writeHead(200, { 'Content-Type': 'text/html' });
58
+ res.end('<html><body style="font-family:system-ui"><h1>Agent Sam</h1><p>You can close this tab.</p></body></html>');
59
+ resolve(code);
60
+ server.close();
61
+ } catch (e) {
62
+ reject(e);
63
+ server.close();
64
+ }
65
+ });
66
+ server.on('error', reject);
67
+ server.listen(port, '127.0.0.1');
68
+ });
69
+
70
+ console.log('\n Opening browser for IAM sign-in + Cloudflare connect…\n');
71
+ openBrowser(authUrl);
72
+
73
+ const code = await codePromise;
74
+ const session = await postJson('/api/sdk/auth/exchange', { code, state });
75
+ return session;
76
+ }