@odla-ai/cli 0.27.1 → 0.27.3

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/dist/index.cjs CHANGED
@@ -96,18 +96,22 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
96
96
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
97
97
 
98
98
  // src/admin-ai.ts
99
- var import_node_process6 = __toESM(require("process"), 1);
99
+ var import_node_process7 = __toESM(require("process"), 1);
100
100
 
101
101
  // src/token.ts
102
102
  var import_db = require("@odla-ai/db");
103
- var import_node_process3 = __toESM(require("process"), 1);
103
+ var import_node_process4 = __toESM(require("process"), 1);
104
+
105
+ // src/handshake-approval.ts
106
+ var import_node_process2 = __toESM(require("process"), 1);
104
107
 
105
108
  // src/approval-prompt.ts
106
109
  function approvalLines(prompt) {
107
110
  const lines = [""];
108
111
  lines.push(
109
- `\u23F8 Human approval required${prompt.purpose ? ` to ${prompt.purpose}` : ""} \u2014 code ${prompt.userCode}, ${prompt.minutesLeft}m left.`
112
+ `\u23F8 AUTHORIZATION REQUIRED${prompt.purpose ? ` to ${prompt.purpose}` : ""} \u2014 code ${prompt.userCode}, ${prompt.minutesLeft}m left.`
110
113
  );
114
+ lines.push(" OPEN THIS URL IN THE APPROVER'S SIGNED-IN BROWSER NOW:");
111
115
  lines.push("");
112
116
  lines.push(` ${prompt.approvalUrl}`);
113
117
  lines.push("");
@@ -121,8 +125,9 @@ function approvalLines(prompt) {
121
125
  lines.push(` No browser was opened (${prompt.browserSkipped}).`);
122
126
  }
123
127
  lines.push("");
124
- lines.push(" AGENTS: relay the URL above to the human verbatim, now, before waiting.");
125
- lines.push(" You cannot approve it yourself \u2014 the grant is issued to you, not by you.");
128
+ lines.push(" AGENTS: use browser control to open the URL above now; do not wait silently.");
129
+ lines.push(" If browser control is unavailable, give the exact URL to the human verbatim.");
130
+ lines.push(" You cannot approve it yourself, retry it away, or start a substitute handshake.");
126
131
  lines.push("");
127
132
  return lines;
128
133
  }
@@ -136,10 +141,69 @@ function reminderLines(prompt) {
136
141
  ];
137
142
  }
138
143
 
144
+ // src/open.ts
145
+ var import_node_child_process = require("child_process");
146
+ var import_node_process = __toESM(require("process"), 1);
147
+ async function openUrl(url, options = {}) {
148
+ const command = openerFor(options.platform ?? import_node_process.default.platform);
149
+ const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
150
+ await new Promise((resolve13, reject) => {
151
+ const child = doSpawn(command.cmd, [...command.args, url], {
152
+ stdio: "ignore",
153
+ detached: true
154
+ });
155
+ child.once("error", reject);
156
+ child.once("spawn", () => {
157
+ child.unref();
158
+ resolve13();
159
+ });
160
+ });
161
+ }
162
+ function openerFor(platform) {
163
+ if (platform === "darwin") return { cmd: "open", args: [] };
164
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
165
+ return { cmd: "xdg-open", args: [] };
166
+ }
167
+
168
+ // src/handshake-approval.ts
169
+ function approvalBrowser(options, host = {}) {
170
+ if (options.open === false) return { open: false, reason: "disabled by --no-open" };
171
+ if (options.open === true) return { open: true, mode: "forced" };
172
+ const env = host.env ?? import_node_process2.default.env;
173
+ if (env.VITEST || env.NODE_ENV === "test") {
174
+ return { open: false, reason: "test environment" };
175
+ }
176
+ return { open: true, mode: "auto" };
177
+ }
178
+ async function presentHandshakeApproval(out, prompt, options) {
179
+ const browser = approvalBrowser(options);
180
+ printApproval(out, {
181
+ ...prompt,
182
+ browserAttempted: browser.open,
183
+ browserSkipped: browser.reason
184
+ });
185
+ if (!browser.open) {
186
+ out.error(`auth: browser launch skipped (${browser.reason}) \u2014 open the URL above manually`);
187
+ return;
188
+ }
189
+ try {
190
+ await (options.openApprovalUrl ?? openUrl)(prompt.approvalUrl);
191
+ out.error("auth: browser open requested for the approval URL above");
192
+ } catch (error) {
193
+ const reason = error instanceof Error ? error.message : String(error);
194
+ out.error(`auth: browser did not open (${reason}) \u2014 OPEN THE URL ABOVE NOW`);
195
+ }
196
+ }
197
+ function handshakeUrl(platformUrl, userCode) {
198
+ const url = new URL("/studio", platformUrl);
199
+ url.searchParams.set("code", userCode);
200
+ return url.toString();
201
+ }
202
+
139
203
  // src/handshake-state.ts
140
204
  var import_node_fs2 = require("fs");
141
205
  var import_node_path2 = require("path");
142
- var import_node_process = __toESM(require("process"), 1);
206
+ var import_node_process3 = __toESM(require("process"), 1);
143
207
 
144
208
  // src/local.ts
145
209
  var import_node_fs = require("fs");
@@ -275,9 +339,13 @@ var RESUME_MARGIN_MS = 5e3;
275
339
  function readPendingHandshake(path, platform, email) {
276
340
  const pending = readJsonFile(path);
277
341
  if (!pending || pending.platform !== platform || pending.email !== email) return null;
278
- if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string" || typeof pending.approvalUrl !== "string") return null;
342
+ if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
279
343
  if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
280
- return { interval: 3, ...pending };
344
+ return {
345
+ interval: typeof pending.interval === "number" ? pending.interval : 3,
346
+ ...pending,
347
+ approvalUrl: handshakeUrl(platform, pending.userCode)
348
+ };
281
349
  }
282
350
  function writePendingHandshake(path, pending) {
283
351
  writePrivateJson(path, pending);
@@ -303,47 +371,23 @@ function approvalReminder(out, pending, periodMs = 3e4) {
303
371
  timer.unref?.();
304
372
  return () => clearInterval(timer);
305
373
  }
306
- function handshakeWaitMs(waitSeconds, interactive = import_node_process.default.stdout.isTTY === true) {
374
+ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
307
375
  if (waitSeconds !== void 0) return waitSeconds * 1e3;
308
376
  return interactive ? void 0 : 9e4;
309
377
  }
310
378
 
311
- // src/open.ts
312
- var import_node_child_process = require("child_process");
313
- var import_node_process2 = __toESM(require("process"), 1);
314
- async function openUrl(url, options = {}) {
315
- const command = openerFor(options.platform ?? import_node_process2.default.platform);
316
- const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
317
- await new Promise((resolve13, reject) => {
318
- const child = doSpawn(command.cmd, [...command.args, url], {
319
- stdio: "ignore",
320
- detached: true
321
- });
322
- child.once("error", reject);
323
- child.once("spawn", () => {
324
- child.unref();
325
- resolve13();
326
- });
327
- });
328
- }
329
- function openerFor(platform) {
330
- if (platform === "darwin") return { cmd: "open", args: [] };
331
- if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
332
- return { cmd: "xdg-open", args: [] };
333
- }
334
-
335
379
  // src/token.ts
336
380
  async function getDeveloperToken(cfg, options, doFetch, out) {
337
381
  if (options.token) return options.token;
338
382
  const audience = platformAudience(cfg.platformUrl);
339
- if (import_node_process3.default.env.ODLA_DEV_TOKEN) {
340
- const declared = import_node_process3.default.env.ODLA_DEV_TOKEN_AUDIENCE;
383
+ if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
384
+ const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
341
385
  if (declared) {
342
386
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
343
387
  } else if (audience !== "https://odla.ai") {
344
388
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
345
389
  }
346
- return import_node_process3.default.env.ODLA_DEV_TOKEN;
390
+ return import_node_process4.default.env.ODLA_DEV_TOKEN;
347
391
  }
348
392
  const cached = readJsonFile(cfg.local.tokenFile);
349
393
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -356,7 +400,6 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
356
400
  doFetch,
357
401
  out,
358
402
  audience,
359
- browser: approvalBrowser(options),
360
403
  email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
361
404
  pendingFile: handshakeFile(cfg)
362
405
  };
@@ -372,7 +415,12 @@ async function resumePendingHandshake(ctx, waitMs) {
372
415
  if (!pending) return null;
373
416
  ctx.out.error("");
374
417
  ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
375
- await launchApproval(ctx, pending.approvalUrl);
418
+ await presentHandshakeApproval(ctx.out, {
419
+ userCode: pending.userCode,
420
+ approvalUrl: pending.approvalUrl,
421
+ minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
422
+ purpose: `sign this terminal in as ${ctx.email}`
423
+ }, ctx.options);
376
424
  ctx.out.error("");
377
425
  const stopReminder = approvalReminder(ctx.out, pending);
378
426
  try {
@@ -409,8 +457,8 @@ async function freshHandshake(ctx, waitMs) {
409
457
  projectIds: [ctx.cfg.app.id],
410
458
  fetch: ctx.doFetch,
411
459
  waitMs,
412
- onCode: async ({ userCode, deviceCode, expiresIn, interval, verificationUriComplete }) => {
413
- const approvalUrl = verificationUriComplete ?? handshakeUrl(ctx.cfg.platformUrl, userCode);
460
+ onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
461
+ const approvalUrl = handshakeUrl(ctx.audience, userCode);
414
462
  started = {
415
463
  platform: ctx.audience,
416
464
  email: ctx.email,
@@ -421,15 +469,12 @@ async function freshHandshake(ctx, waitMs) {
421
469
  expiresAt: Date.now() + expiresIn * 1e3
422
470
  };
423
471
  writePendingHandshake(ctx.pendingFile, started);
424
- printApproval(ctx.out, {
472
+ await presentHandshakeApproval(ctx.out, {
425
473
  userCode,
426
474
  approvalUrl,
427
475
  minutesLeft: Math.floor(expiresIn / 60),
428
- purpose: `sign this terminal in as ${ctx.email}`,
429
- browserAttempted: ctx.browser.open,
430
- browserSkipped: ctx.browser.reason
431
- });
432
- await launchApproval(ctx, approvalUrl);
476
+ purpose: `sign this terminal in as ${ctx.email}`
477
+ }, ctx.options);
433
478
  stopReminder = approvalReminder(ctx.out, started);
434
479
  }
435
480
  });
@@ -444,18 +489,6 @@ async function freshHandshake(ctx, waitMs) {
444
489
  stopReminder?.();
445
490
  }
446
491
  }
447
- async function launchApproval(ctx, approvalUrl) {
448
- if (ctx.browser.open) {
449
- try {
450
- await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
451
- ctx.out.error(`auth: asked the OS to open a browser${ctx.browser.mode === "auto" ? " (auto)" : ""} \u2014 if no tab appeared, use the URL above`);
452
- } catch (err) {
453
- ctx.out.error(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
454
- }
455
- } else if (ctx.browser.reason) {
456
- ctx.out.error(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
457
- }
458
- }
459
492
  function stillPending(pending, email) {
460
493
  return new import_db.OdlaError(
461
494
  "handshake_pending",
@@ -464,30 +497,12 @@ function stillPending(pending, email) {
464
497
  );
465
498
  }
466
499
  function handshakeEmail(value2, cached) {
467
- const email = (value2 ?? import_node_process3.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
500
+ const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
468
501
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
469
502
  throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
470
503
  }
471
504
  return email;
472
505
  }
473
- function approvalBrowser(options, host = {}) {
474
- if (options.open === true) return { open: true, mode: "forced" };
475
- if (options.open === false) return { open: false, reason: "disabled by --no-open" };
476
- const env = host.env ?? import_node_process3.default.env;
477
- if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
478
- if (env.CI) return { open: false, reason: "CI environment" };
479
- if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
480
- const platform = host.platform ?? import_node_process3.default.platform;
481
- if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
482
- return { open: false, reason: "no graphical display; pass --open to force" };
483
- }
484
- return { open: true, mode: "auto" };
485
- }
486
- function handshakeUrl(platformUrl, userCode) {
487
- const url = new URL("/studio", platformUrl);
488
- url.searchParams.set("code", userCode);
489
- return url.toString();
490
- }
491
506
  function platformAudience(value2) {
492
507
  let url;
493
508
  try {
@@ -506,12 +521,12 @@ function platformAudience(value2) {
506
521
  }
507
522
 
508
523
  // src/secret-input.ts
509
- var import_node_process4 = __toESM(require("process"), 1);
524
+ var import_node_process5 = __toESM(require("process"), 1);
510
525
  var MAX_BYTES = 64 * 1024;
511
526
  async function secretInputValue(options, kind = "credential") {
512
527
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
513
528
  let value2;
514
- if (options.fromEnv) value2 = import_node_process4.default.env[options.fromEnv];
529
+ if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
515
530
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
516
531
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
517
532
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -519,7 +534,7 @@ async function secretInputValue(options, kind = "credential") {
519
534
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
520
535
  return value2;
521
536
  }
522
- async function readSecretStream(kind, stream = import_node_process4.default.stdin) {
537
+ async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
523
538
  let value2 = "";
524
539
  for await (const chunk of stream) {
525
540
  value2 += String(chunk);
@@ -531,7 +546,7 @@ async function readSecretStream(kind, stream = import_node_process4.default.stdi
531
546
  // src/admin-ai-auth.ts
532
547
  var import_node_fs3 = require("fs");
533
548
  var import_node_path3 = require("path");
534
- var import_node_process5 = __toESM(require("process"), 1);
549
+ var import_node_process6 = __toESM(require("process"), 1);
535
550
  var import_db2 = require("@odla-ai/db");
536
551
  async function getScopedPlatformToken(options) {
537
552
  return resolveAdminPlatformToken(options);
@@ -539,7 +554,7 @@ async function getScopedPlatformToken(options) {
539
554
  async function resolveAdminPlatformToken(options) {
540
555
  const audience = platformAudience(options.platform);
541
556
  if (options.token) return options.token;
542
- const fromEnv = import_node_process5.default.env.ODLA_ADMIN_TOKEN;
557
+ const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
543
558
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
544
559
  return scopedToken(
545
560
  audience,
@@ -551,7 +566,7 @@ async function resolveAdminPlatformToken(options) {
551
566
  }
552
567
  function audienceBoundEnvToken(token, platform) {
553
568
  const audience = platformAudience(platform);
554
- const declared = import_node_process5.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
569
+ const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
555
570
  if (declared) {
556
571
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
557
572
  } else if (audience !== "https://odla.ai") {
@@ -576,7 +591,7 @@ var SCOPE_PURPOSE = {
576
591
  };
577
592
  async function scopedToken(platform, scope, options, doFetch, out) {
578
593
  const audience = platformAudience(platform);
579
- const rootDir = options.rootDir ?? import_node_process5.default.cwd();
594
+ const rootDir = options.rootDir ?? import_node_process6.default.cwd();
580
595
  const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
581
596
  const cache = options.cache === false ? null : readJsonFile(tokenFile);
582
597
  const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
@@ -591,21 +606,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
591
606
  label: options.label ?? `odla CLI (${scope})`,
592
607
  scopes: [scope],
593
608
  fetch: doFetch,
594
- onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
595
- const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
596
- const browser = approvalBrowser({ open: options.open });
597
- printApproval(out, {
609
+ onCode: async ({ userCode, expiresIn }) => {
610
+ await presentHandshakeApproval(out, {
598
611
  userCode,
599
- approvalUrl,
612
+ approvalUrl: handshakeUrl(audience, userCode),
600
613
  minutesLeft: Math.floor(expiresIn / 60),
601
614
  purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
602
- approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
603
- browserAttempted: browser.open,
604
- browserSkipped: browser.reason
605
- });
606
- if (browser.open) {
607
- await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
608
- }
615
+ approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
616
+ }, options);
609
617
  }
610
618
  });
611
619
  if (options.cache !== false) {
@@ -788,7 +796,7 @@ function isRecord2(value2) {
788
796
 
789
797
  // src/admin-ai.ts
790
798
  async function adminAi(options) {
791
- const platform = platformAudience(options.platform ?? import_node_process6.default.env.ODLA_PLATFORM ?? "https://odla.ai");
799
+ const platform = platformAudience(options.platform ?? import_node_process7.default.env.ODLA_PLATFORM ?? "https://odla.ai");
792
800
  const doFetch = options.fetch ?? fetch;
793
801
  const out = options.stdout ?? console;
794
802
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1045,7 +1053,7 @@ function addOption(options, name, value2) {
1045
1053
  // src/operator-context.ts
1046
1054
  var import_node_fs6 = require("fs");
1047
1055
  var import_node_path6 = require("path");
1048
- var import_node_process8 = __toESM(require("process"), 1);
1056
+ var import_node_process9 = __toESM(require("process"), 1);
1049
1057
 
1050
1058
  // src/config.ts
1051
1059
  var import_node_fs4 = require("fs");
@@ -1351,15 +1359,15 @@ function unique2(values) {
1351
1359
  var import_node_fs5 = require("fs");
1352
1360
  var import_node_os = require("os");
1353
1361
  var import_node_path5 = require("path");
1354
- var import_node_process7 = __toESM(require("process"), 1);
1362
+ var import_node_process8 = __toESM(require("process"), 1);
1355
1363
  function operatorProfileFile() {
1356
1364
  return (0, import_node_path5.resolve)(
1357
- clean(import_node_process7.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1365
+ clean(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1358
1366
  );
1359
1367
  }
1360
1368
  function resolveOperatorProfile(parsed) {
1361
1369
  const fromFlag = clean(stringOpt(parsed.options.context));
1362
- const fromEnvironment = clean(import_node_process7.default.env.ODLA_CONTEXT);
1370
+ const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
1363
1371
  const name = fromFlag ?? fromEnvironment ?? null;
1364
1372
  const file = operatorProfileFile();
1365
1373
  if (!name) {
@@ -1488,13 +1496,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1488
1496
  }
1489
1497
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1490
1498
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1491
- const platformEnvironment = clean2(import_node_process8.default.env.ODLA_PLATFORM_URL);
1499
+ const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
1492
1500
  const platformValue = platformAudience(
1493
1501
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1494
1502
  );
1495
1503
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1496
1504
  const appFlag = clean2(stringOpt(parsed.options.app));
1497
- const appEnvironment = clean2(import_node_process8.default.env.ODLA_APP_ID);
1505
+ const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
1498
1506
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1499
1507
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1500
1508
  if (appValue) assertOperatorName(appValue, "app");
@@ -1504,16 +1512,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1504
1512
  );
1505
1513
  }
1506
1514
  const envFlag = clean2(stringOpt(parsed.options.env));
1507
- const envEnvironment = clean2(import_node_process8.default.env.ODLA_ENV);
1515
+ const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
1508
1516
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1509
1517
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1510
1518
  if (environmentValue) {
1511
1519
  assertOperatorName(environmentValue, "environment");
1512
1520
  }
1513
- const rootDir = loaded?.rootDir ?? import_node_process8.default.cwd();
1521
+ const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
1514
1522
  const profileCredentials = operatorCredentialFiles(profile);
1515
- const tokenFile = clean2(import_node_process8.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process8.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1516
- const scopedTokenFile = clean2(import_node_process8.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process8.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path6.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1523
+ const tokenFile = clean2(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process9.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
1524
+ const scopedTokenFile = clean2(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path6.resolve)(import_node_process9.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path6.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
1517
1525
  const cfg = loaded ? {
1518
1526
  ...loaded,
1519
1527
  platformUrl: platformValue,
@@ -8565,13 +8573,13 @@ async function codeCommand(parsed, dependencies) {
8565
8573
  }
8566
8574
 
8567
8575
  // src/operator-credentials.ts
8568
- var import_node_process9 = __toESM(require("process"), 1);
8576
+ var import_node_process10 = __toESM(require("process"), 1);
8569
8577
  function developerTokenStatus(context, parsed, now = Date.now()) {
8570
8578
  const cached = readJsonFile(context.cfg.local.tokenFile);
8571
8579
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8572
8580
  const source = clean3(
8573
8581
  stringOpt(parsed.options.token)
8574
- ) ? "flag" : clean3(import_node_process9.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8582
+ ) ? "flag" : clean3(import_node_process10.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8575
8583
  return {
8576
8584
  source,
8577
8585
  cacheFile: context.cfg.local.tokenFile,
@@ -8945,15 +8953,16 @@ Safety:
8945
8953
  the metadata file. Flags and specific ODLA_* scope variables beat a selected
8946
8954
  context, which beats project config. There is no ambient current context.
8947
8955
  "context show" reports only provenance and cache state and never authenticates.
8948
- Provision opens the approval page in your browser automatically whenever the
8949
- machine can show one, including agent-driven runs; only CI, SSH, and
8950
- display-less hosts skip it. Use --open to force or --no-open to suppress.
8951
- Browser launch is best-effort: the printed approval URL is authoritative and
8952
- agents must relay it to the human verbatim. A started handshake is persisted
8956
+ Every real CLI handshake prints one canonical /studio?code= approval URL and
8957
+ attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
8958
+ shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
8959
+ agents with browser control must open that exact URL immediately; otherwise
8960
+ they must give it to the human verbatim. A started handshake is persisted
8953
8961
  under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
8954
8962
  the same code. Outside an interactive terminal the wait is capped (90s by
8955
8963
  default, --wait <seconds> to change); a still-pending handshake then exits
8956
- with code 75: relay the URL, wait for approval, and re-run to collect.
8964
+ with code 75: open the same URL (or relay it if browser control is unavailable),
8965
+ wait for approval, and re-run to collect.
8957
8966
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8958
8967
  The email is a non-secret identity hint: never provide a password or session
8959
8968
  token. The matching account must already exist, be signed in, explicitly
@@ -10362,7 +10371,7 @@ async function read2(url, headers, doFetch) {
10362
10371
  // src/provision.ts
10363
10372
  var import_apps12 = require("@odla-ai/apps");
10364
10373
  var import_ai3 = require("@odla-ai/ai");
10365
- var import_node_process10 = __toESM(require("process"), 1);
10374
+ var import_node_process11 = __toESM(require("process"), 1);
10366
10375
 
10367
10376
  // src/integration-provision.ts
10368
10377
  var import_db3 = require("@odla-ai/db");
@@ -10669,7 +10678,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10669
10678
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10670
10679
  }
10671
10680
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10672
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10681
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
10673
10682
  if (key) {
10674
10683
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10675
10684
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10711,7 +10720,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10711
10720
 
10712
10721
  // src/record.ts
10713
10722
  var import_node_fs16 = require("fs");
10714
- var import_node_process11 = __toESM(require("process"), 1);
10723
+ var import_node_process12 = __toESM(require("process"), 1);
10715
10724
 
10716
10725
  // src/surface.ts
10717
10726
  var PM_ACTIONS = {
@@ -10869,7 +10878,7 @@ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
10869
10878
 
10870
10879
  // src/record.ts
10871
10880
  function recordInvocation(parsed) {
10872
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10881
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10873
10882
  if (!file) return;
10874
10883
  try {
10875
10884
  const entry = {
@@ -11541,9 +11550,9 @@ var import_node_child_process8 = require("child_process");
11541
11550
  var import_node_fs20 = require("fs");
11542
11551
  var import_node_os5 = require("os");
11543
11552
  var import_node_path18 = require("path");
11544
- var import_node_process12 = __toESM(require("process"), 1);
11553
+ var import_node_process13 = __toESM(require("process"), 1);
11545
11554
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11546
- function resolveEditor(env = import_node_process12.default.env) {
11555
+ function resolveEditor(env = import_node_process13.default.env) {
11547
11556
  for (const name of EDITOR_ENV) {
11548
11557
  const value2 = env[name];
11549
11558
  if (value2 && value2.trim()) return value2.trim();
@@ -11557,8 +11566,8 @@ function defaultRun(command, path) {
11557
11566
  return result.status ?? 0;
11558
11567
  }
11559
11568
  function editText(initial, slug, deps = {}) {
11560
- const env = deps.env ?? import_node_process12.default.env;
11561
- const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
11569
+ const env = deps.env ?? import_node_process13.default.env;
11570
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
11562
11571
  const editor = resolveEditor(env);
11563
11572
  if (!editor)
11564
11573
  throw new Error(