@odla-ai/cli 0.27.0 → 0.27.2

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
@@ -426,12 +426,12 @@ shown-once credential.
426
426
  `--email <account>` or `ODLA_USER_EMAIL`; the matching existing account must
427
427
  sign in, review the exact code, and approve it. Opening the URL alone does
428
428
  not claim the request.
429
- When the code is displayed it opens the approval page in your browser —
430
- including from scripted and agent-driven shells; only CI, SSH sessions, and
431
- display-less hosts skip the launch. Pass `--open` to force it anyway, or
432
- `--no-open` to suppress it. Browser launch is best-effort; the printed URL
433
- and code always remain the fallback and agents must relay that URL to the
434
- human. The started handshake is persisted privately under `.odla/`, so a
429
+ Every real CLI handshake prints exactly one canonical `/studio?code=…` URL
430
+ and attempts to open it including from CI, SSH, display-less, scripted,
431
+ and agent-driven shells. Only `--no-open` suppresses the attempt. Browser
432
+ launch is best-effort: an agent with browser control must open that exact URL
433
+ immediately; otherwise it gives the URL to the human verbatim. The started
434
+ handshake is persisted privately under `.odla/`, so a
435
435
  run killed before approval loses nothing: rerunning resumes the same code.
436
436
  Outside an interactive terminal the approval wait is capped (90 seconds by
437
437
  default; `--wait <seconds>` overrides), and a still-pending handshake exits
package/REQUIREMENTS.md CHANGED
@@ -38,13 +38,15 @@ Agnacl, but none should mention or special-case Agnacl.
38
38
  credential; agents must not request either from the human.
39
39
  - Unknown and never-signed-in accounts must not yield a claimable request, but
40
40
  the public response must not disclose account existence.
41
- - The approval code must be printed in a copyable, server-supplied verification
42
- URL. Opening it must not claim the request; the matching signed-in user must
41
+ - The approval code must be printed in the one canonical, copyable
42
+ `<platform>/studio?code=…` URL. Optional server URL hints must not create a
43
+ second CLI path. Opening it must not claim the request; the matching user must
43
44
  explicitly review that exact code before approval controls appear.
44
45
  - Only one claimed pending or approved-but-uncollected request may be active per
45
46
  user. Unclaimed issued codes must not occupy that slot.
46
- - Interactive provisioning should launch the browser to the approval page when
47
- possible. `--open` should force launch; `--no-open` should suppress it.
47
+ - Every real CLI handshake must attempt to open that canonical approval URL,
48
+ including in CI, SSH, display-less, and agent-driven shells. `--no-open` is
49
+ the only production suppression; failures must visibly repeat the manual action.
48
50
  - Cached developer tokens must be mode `0600`, gitignored, and reused until
49
51
  expiry.
50
52
  - Minted developer tokens must have a metadata-only inventory (owner, label,
package/dist/bin.cjs CHANGED
@@ -28,18 +28,22 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
28
28
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
29
 
30
30
  // src/admin-ai.ts
31
- var import_node_process6 = __toESM(require("process"), 1);
31
+ var import_node_process7 = __toESM(require("process"), 1);
32
32
 
33
33
  // src/token.ts
34
34
  var import_db = require("@odla-ai/db");
35
- var import_node_process3 = __toESM(require("process"), 1);
35
+ var import_node_process4 = __toESM(require("process"), 1);
36
+
37
+ // src/handshake-approval.ts
38
+ var import_node_process2 = __toESM(require("process"), 1);
36
39
 
37
40
  // src/approval-prompt.ts
38
41
  function approvalLines(prompt) {
39
42
  const lines = [""];
40
43
  lines.push(
41
- `\u23F8 Human approval required${prompt.purpose ? ` to ${prompt.purpose}` : ""} \u2014 code ${prompt.userCode}, ${prompt.minutesLeft}m left.`
44
+ `\u23F8 AUTHORIZATION REQUIRED${prompt.purpose ? ` to ${prompt.purpose}` : ""} \u2014 code ${prompt.userCode}, ${prompt.minutesLeft}m left.`
42
45
  );
46
+ lines.push(" OPEN THIS URL IN THE APPROVER'S SIGNED-IN BROWSER NOW:");
43
47
  lines.push("");
44
48
  lines.push(` ${prompt.approvalUrl}`);
45
49
  lines.push("");
@@ -53,8 +57,9 @@ function approvalLines(prompt) {
53
57
  lines.push(` No browser was opened (${prompt.browserSkipped}).`);
54
58
  }
55
59
  lines.push("");
56
- lines.push(" AGENTS: relay the URL above to the human verbatim, now, before waiting.");
57
- lines.push(" You cannot approve it yourself \u2014 the grant is issued to you, not by you.");
60
+ lines.push(" AGENTS: use browser control to open the URL above now; do not wait silently.");
61
+ lines.push(" If browser control is unavailable, give the exact URL to the human verbatim.");
62
+ lines.push(" You cannot approve it yourself, retry it away, or start a substitute handshake.");
58
63
  lines.push("");
59
64
  return lines;
60
65
  }
@@ -68,10 +73,69 @@ function reminderLines(prompt) {
68
73
  ];
69
74
  }
70
75
 
76
+ // src/open.ts
77
+ var import_node_child_process = require("child_process");
78
+ var import_node_process = __toESM(require("process"), 1);
79
+ async function openUrl(url, options = {}) {
80
+ const command = openerFor(options.platform ?? import_node_process.default.platform);
81
+ const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
82
+ await new Promise((resolve13, reject) => {
83
+ const child = doSpawn(command.cmd, [...command.args, url], {
84
+ stdio: "ignore",
85
+ detached: true
86
+ });
87
+ child.once("error", reject);
88
+ child.once("spawn", () => {
89
+ child.unref();
90
+ resolve13();
91
+ });
92
+ });
93
+ }
94
+ function openerFor(platform) {
95
+ if (platform === "darwin") return { cmd: "open", args: [] };
96
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
97
+ return { cmd: "xdg-open", args: [] };
98
+ }
99
+
100
+ // src/handshake-approval.ts
101
+ function approvalBrowser(options, host = {}) {
102
+ if (options.open === false) return { open: false, reason: "disabled by --no-open" };
103
+ if (options.open === true) return { open: true, mode: "forced" };
104
+ const env = host.env ?? import_node_process2.default.env;
105
+ if (env.VITEST || env.NODE_ENV === "test") {
106
+ return { open: false, reason: "test environment" };
107
+ }
108
+ return { open: true, mode: "auto" };
109
+ }
110
+ async function presentHandshakeApproval(out, prompt, options) {
111
+ const browser = approvalBrowser(options);
112
+ printApproval(out, {
113
+ ...prompt,
114
+ browserAttempted: browser.open,
115
+ browserSkipped: browser.reason
116
+ });
117
+ if (!browser.open) {
118
+ out.error(`auth: browser launch skipped (${browser.reason}) \u2014 open the URL above manually`);
119
+ return;
120
+ }
121
+ try {
122
+ await (options.openApprovalUrl ?? openUrl)(prompt.approvalUrl);
123
+ out.error("auth: browser open requested for the approval URL above");
124
+ } catch (error) {
125
+ const reason = error instanceof Error ? error.message : String(error);
126
+ out.error(`auth: browser did not open (${reason}) \u2014 OPEN THE URL ABOVE NOW`);
127
+ }
128
+ }
129
+ function handshakeUrl(platformUrl, userCode) {
130
+ const url = new URL("/studio", platformUrl);
131
+ url.searchParams.set("code", userCode);
132
+ return url.toString();
133
+ }
134
+
71
135
  // src/handshake-state.ts
72
136
  var import_node_fs2 = require("fs");
73
137
  var import_node_path2 = require("path");
74
- var import_node_process = __toESM(require("process"), 1);
138
+ var import_node_process3 = __toESM(require("process"), 1);
75
139
 
76
140
  // src/local.ts
77
141
  var import_node_fs = require("fs");
@@ -207,9 +271,13 @@ var RESUME_MARGIN_MS = 5e3;
207
271
  function readPendingHandshake(path, platform, email) {
208
272
  const pending = readJsonFile(path);
209
273
  if (!pending || pending.platform !== platform || pending.email !== email) return null;
210
- if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string" || typeof pending.approvalUrl !== "string") return null;
274
+ if (typeof pending.userCode !== "string" || typeof pending.deviceCode !== "string") return null;
211
275
  if (typeof pending.expiresAt !== "number" || pending.expiresAt <= Date.now() + RESUME_MARGIN_MS) return null;
212
- return { interval: 3, ...pending };
276
+ return {
277
+ interval: typeof pending.interval === "number" ? pending.interval : 3,
278
+ ...pending,
279
+ approvalUrl: handshakeUrl(platform, pending.userCode)
280
+ };
213
281
  }
214
282
  function writePendingHandshake(path, pending) {
215
283
  writePrivateJson(path, pending);
@@ -235,47 +303,23 @@ function approvalReminder(out, pending, periodMs = 3e4) {
235
303
  timer.unref?.();
236
304
  return () => clearInterval(timer);
237
305
  }
238
- function handshakeWaitMs(waitSeconds, interactive = import_node_process.default.stdout.isTTY === true) {
306
+ function handshakeWaitMs(waitSeconds, interactive = import_node_process3.default.stdout.isTTY === true) {
239
307
  if (waitSeconds !== void 0) return waitSeconds * 1e3;
240
308
  return interactive ? void 0 : 9e4;
241
309
  }
242
310
 
243
- // src/open.ts
244
- var import_node_child_process = require("child_process");
245
- var import_node_process2 = __toESM(require("process"), 1);
246
- async function openUrl(url, options = {}) {
247
- const command = openerFor(options.platform ?? import_node_process2.default.platform);
248
- const doSpawn = options.spawnImpl ?? import_node_child_process.spawn;
249
- await new Promise((resolve13, reject) => {
250
- const child = doSpawn(command.cmd, [...command.args, url], {
251
- stdio: "ignore",
252
- detached: true
253
- });
254
- child.once("error", reject);
255
- child.once("spawn", () => {
256
- child.unref();
257
- resolve13();
258
- });
259
- });
260
- }
261
- function openerFor(platform) {
262
- if (platform === "darwin") return { cmd: "open", args: [] };
263
- if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
264
- return { cmd: "xdg-open", args: [] };
265
- }
266
-
267
311
  // src/token.ts
268
312
  async function getDeveloperToken(cfg, options, doFetch, out) {
269
313
  if (options.token) return options.token;
270
314
  const audience = platformAudience(cfg.platformUrl);
271
- if (import_node_process3.default.env.ODLA_DEV_TOKEN) {
272
- const declared = import_node_process3.default.env.ODLA_DEV_TOKEN_AUDIENCE;
315
+ if (import_node_process4.default.env.ODLA_DEV_TOKEN) {
316
+ const declared = import_node_process4.default.env.ODLA_DEV_TOKEN_AUDIENCE;
273
317
  if (declared) {
274
318
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
275
319
  } else if (audience !== "https://odla.ai") {
276
320
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
277
321
  }
278
- return import_node_process3.default.env.ODLA_DEV_TOKEN;
322
+ return import_node_process4.default.env.ODLA_DEV_TOKEN;
279
323
  }
280
324
  const cached = readJsonFile(cfg.local.tokenFile);
281
325
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
@@ -288,7 +332,6 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
288
332
  doFetch,
289
333
  out,
290
334
  audience,
291
- browser: approvalBrowser(options),
292
335
  email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
293
336
  pendingFile: handshakeFile(cfg)
294
337
  };
@@ -304,7 +347,12 @@ async function resumePendingHandshake(ctx, waitMs) {
304
347
  if (!pending) return null;
305
348
  ctx.out.error("");
306
349
  ctx.out.error(`auth: resuming pending handshake \u2014 ${approvalHint(pending)}`);
307
- await launchApproval(ctx, pending.approvalUrl);
350
+ await presentHandshakeApproval(ctx.out, {
351
+ userCode: pending.userCode,
352
+ approvalUrl: pending.approvalUrl,
353
+ minutesLeft: Math.max(1, Math.floor((pending.expiresAt - Date.now()) / 6e4)),
354
+ purpose: `sign this terminal in as ${ctx.email}`
355
+ }, ctx.options);
308
356
  ctx.out.error("");
309
357
  const stopReminder = approvalReminder(ctx.out, pending);
310
358
  try {
@@ -338,10 +386,11 @@ async function freshHandshake(ctx, waitMs) {
338
386
  endpoint: ctx.cfg.platformUrl,
339
387
  email: ctx.email,
340
388
  label: `${ctx.cfg.app.id} provisioner`,
389
+ projectIds: [ctx.cfg.app.id],
341
390
  fetch: ctx.doFetch,
342
391
  waitMs,
343
- onCode: async ({ userCode, deviceCode, expiresIn, interval, verificationUriComplete }) => {
344
- const approvalUrl = verificationUriComplete ?? handshakeUrl(ctx.cfg.platformUrl, userCode);
392
+ onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
393
+ const approvalUrl = handshakeUrl(ctx.audience, userCode);
345
394
  started = {
346
395
  platform: ctx.audience,
347
396
  email: ctx.email,
@@ -352,15 +401,12 @@ async function freshHandshake(ctx, waitMs) {
352
401
  expiresAt: Date.now() + expiresIn * 1e3
353
402
  };
354
403
  writePendingHandshake(ctx.pendingFile, started);
355
- printApproval(ctx.out, {
404
+ await presentHandshakeApproval(ctx.out, {
356
405
  userCode,
357
406
  approvalUrl,
358
407
  minutesLeft: Math.floor(expiresIn / 60),
359
- purpose: `sign this terminal in as ${ctx.email}`,
360
- browserAttempted: ctx.browser.open,
361
- browserSkipped: ctx.browser.reason
362
- });
363
- await launchApproval(ctx, approvalUrl);
408
+ purpose: `sign this terminal in as ${ctx.email}`
409
+ }, ctx.options);
364
410
  stopReminder = approvalReminder(ctx.out, started);
365
411
  }
366
412
  });
@@ -375,18 +421,6 @@ async function freshHandshake(ctx, waitMs) {
375
421
  stopReminder?.();
376
422
  }
377
423
  }
378
- async function launchApproval(ctx, approvalUrl) {
379
- if (ctx.browser.open) {
380
- try {
381
- await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
382
- 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`);
383
- } catch (err) {
384
- ctx.out.error(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
385
- }
386
- } else if (ctx.browser.reason) {
387
- ctx.out.error(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
388
- }
389
- }
390
424
  function stillPending(pending, email) {
391
425
  return new import_db.OdlaError(
392
426
  "handshake_pending",
@@ -395,30 +429,12 @@ function stillPending(pending, email) {
395
429
  );
396
430
  }
397
431
  function handshakeEmail(value2, cached) {
398
- const email = (value2 ?? import_node_process3.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
432
+ const email = (value2 ?? import_node_process4.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
399
433
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
400
434
  throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
401
435
  }
402
436
  return email;
403
437
  }
404
- function approvalBrowser(options, host = {}) {
405
- if (options.open === true) return { open: true, mode: "forced" };
406
- if (options.open === false) return { open: false, reason: "disabled by --no-open" };
407
- const env = host.env ?? import_node_process3.default.env;
408
- if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
409
- if (env.CI) return { open: false, reason: "CI environment" };
410
- if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
411
- const platform = host.platform ?? import_node_process3.default.platform;
412
- if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
413
- return { open: false, reason: "no graphical display; pass --open to force" };
414
- }
415
- return { open: true, mode: "auto" };
416
- }
417
- function handshakeUrl(platformUrl, userCode) {
418
- const url = new URL("/studio", platformUrl);
419
- url.searchParams.set("code", userCode);
420
- return url.toString();
421
- }
422
438
  function platformAudience(value2) {
423
439
  let url;
424
440
  try {
@@ -437,12 +453,12 @@ function platformAudience(value2) {
437
453
  }
438
454
 
439
455
  // src/secret-input.ts
440
- var import_node_process4 = __toESM(require("process"), 1);
456
+ var import_node_process5 = __toESM(require("process"), 1);
441
457
  var MAX_BYTES = 64 * 1024;
442
458
  async function secretInputValue(options, kind = "credential") {
443
459
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
444
460
  let value2;
445
- if (options.fromEnv) value2 = import_node_process4.default.env[options.fromEnv];
461
+ if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
446
462
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
447
463
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
448
464
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -450,7 +466,7 @@ async function secretInputValue(options, kind = "credential") {
450
466
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
451
467
  return value2;
452
468
  }
453
- async function readSecretStream(kind, stream = import_node_process4.default.stdin) {
469
+ async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
454
470
  let value2 = "";
455
471
  for await (const chunk of stream) {
456
472
  value2 += String(chunk);
@@ -462,7 +478,7 @@ async function readSecretStream(kind, stream = import_node_process4.default.stdi
462
478
  // src/admin-ai-auth.ts
463
479
  var import_node_fs3 = require("fs");
464
480
  var import_node_path3 = require("path");
465
- var import_node_process5 = __toESM(require("process"), 1);
481
+ var import_node_process6 = __toESM(require("process"), 1);
466
482
  var import_db2 = require("@odla-ai/db");
467
483
  async function getScopedPlatformToken(options) {
468
484
  return resolveAdminPlatformToken(options);
@@ -470,7 +486,7 @@ async function getScopedPlatformToken(options) {
470
486
  async function resolveAdminPlatformToken(options) {
471
487
  const audience = platformAudience(options.platform);
472
488
  if (options.token) return options.token;
473
- const fromEnv = import_node_process5.default.env.ODLA_ADMIN_TOKEN;
489
+ const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
474
490
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
475
491
  return scopedToken(
476
492
  audience,
@@ -482,7 +498,7 @@ async function resolveAdminPlatformToken(options) {
482
498
  }
483
499
  function audienceBoundEnvToken(token, platform) {
484
500
  const audience = platformAudience(platform);
485
- const declared = import_node_process5.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
501
+ const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
486
502
  if (declared) {
487
503
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
488
504
  } else if (audience !== "https://odla.ai") {
@@ -507,7 +523,7 @@ var SCOPE_PURPOSE = {
507
523
  };
508
524
  async function scopedToken(platform, scope, options, doFetch, out) {
509
525
  const audience = platformAudience(platform);
510
- const rootDir = options.rootDir ?? import_node_process5.default.cwd();
526
+ const rootDir = options.rootDir ?? import_node_process6.default.cwd();
511
527
  const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
512
528
  const cache = options.cache === false ? null : readJsonFile(tokenFile);
513
529
  const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
@@ -522,21 +538,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
522
538
  label: options.label ?? `odla CLI (${scope})`,
523
539
  scopes: [scope],
524
540
  fetch: doFetch,
525
- onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
526
- const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
527
- const browser = approvalBrowser({ open: options.open });
528
- printApproval(out, {
541
+ onCode: async ({ userCode, expiresIn }) => {
542
+ await presentHandshakeApproval(out, {
529
543
  userCode,
530
- approvalUrl,
544
+ approvalUrl: handshakeUrl(audience, userCode),
531
545
  minutesLeft: Math.floor(expiresIn / 60),
532
546
  purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
533
- approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
534
- browserAttempted: browser.open,
535
- browserSkipped: browser.reason
536
- });
537
- if (browser.open) {
538
- await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
539
- }
547
+ approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
548
+ }, options);
540
549
  }
541
550
  });
542
551
  if (options.cache !== false) {
@@ -719,7 +728,7 @@ function isRecord2(value2) {
719
728
 
720
729
  // src/admin-ai.ts
721
730
  async function adminAi(options) {
722
- const platform = platformAudience(options.platform ?? import_node_process6.default.env.ODLA_PLATFORM ?? "https://odla.ai");
731
+ const platform = platformAudience(options.platform ?? import_node_process7.default.env.ODLA_PLATFORM ?? "https://odla.ai");
723
732
  const doFetch = options.fetch ?? fetch;
724
733
  const out = options.stdout ?? console;
725
734
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -976,7 +985,7 @@ function addOption(options, name, value2) {
976
985
  // src/operator-context.ts
977
986
  var import_node_fs6 = require("fs");
978
987
  var import_node_path6 = require("path");
979
- var import_node_process8 = __toESM(require("process"), 1);
988
+ var import_node_process9 = __toESM(require("process"), 1);
980
989
 
981
990
  // src/config.ts
982
991
  var import_node_fs4 = require("fs");
@@ -1282,15 +1291,15 @@ function unique2(values) {
1282
1291
  var import_node_fs5 = require("fs");
1283
1292
  var import_node_os = require("os");
1284
1293
  var import_node_path5 = require("path");
1285
- var import_node_process7 = __toESM(require("process"), 1);
1294
+ var import_node_process8 = __toESM(require("process"), 1);
1286
1295
  function operatorProfileFile() {
1287
1296
  return (0, import_node_path5.resolve)(
1288
- clean(import_node_process7.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1297
+ clean(import_node_process8.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path5.join)((0, import_node_os.homedir)(), ".odla", "contexts.json")
1289
1298
  );
1290
1299
  }
1291
1300
  function resolveOperatorProfile(parsed) {
1292
1301
  const fromFlag = clean(stringOpt(parsed.options.context));
1293
- const fromEnvironment = clean(import_node_process7.default.env.ODLA_CONTEXT);
1302
+ const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
1294
1303
  const name = fromFlag ?? fromEnvironment ?? null;
1295
1304
  const file = operatorProfileFile();
1296
1305
  if (!name) {
@@ -1419,13 +1428,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1419
1428
  }
1420
1429
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1421
1430
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1422
- const platformEnvironment = clean2(import_node_process8.default.env.ODLA_PLATFORM_URL);
1431
+ const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
1423
1432
  const platformValue = platformAudience(
1424
1433
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1425
1434
  );
1426
1435
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1427
1436
  const appFlag = clean2(stringOpt(parsed.options.app));
1428
- const appEnvironment = clean2(import_node_process8.default.env.ODLA_APP_ID);
1437
+ const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
1429
1438
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1430
1439
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1431
1440
  if (appValue) assertOperatorName(appValue, "app");
@@ -1435,16 +1444,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1435
1444
  );
1436
1445
  }
1437
1446
  const envFlag = clean2(stringOpt(parsed.options.env));
1438
- const envEnvironment = clean2(import_node_process8.default.env.ODLA_ENV);
1447
+ const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
1439
1448
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1440
1449
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1441
1450
  if (environmentValue) {
1442
1451
  assertOperatorName(environmentValue, "environment");
1443
1452
  }
1444
- const rootDir = loaded?.rootDir ?? import_node_process8.default.cwd();
1453
+ const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
1445
1454
  const profileCredentials = operatorCredentialFiles(profile);
1446
- 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;
1447
- 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;
1455
+ 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;
1456
+ 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;
1448
1457
  const cfg = loaded ? {
1449
1458
  ...loaded,
1450
1459
  platformUrl: platformValue,
@@ -8496,13 +8505,13 @@ async function codeCommand(parsed, dependencies) {
8496
8505
  }
8497
8506
 
8498
8507
  // src/operator-credentials.ts
8499
- var import_node_process9 = __toESM(require("process"), 1);
8508
+ var import_node_process10 = __toESM(require("process"), 1);
8500
8509
  function developerTokenStatus(context, parsed, now = Date.now()) {
8501
8510
  const cached = readJsonFile(context.cfg.local.tokenFile);
8502
8511
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8503
8512
  const source = clean3(
8504
8513
  stringOpt(parsed.options.token)
8505
- ) ? "flag" : clean3(import_node_process9.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8514
+ ) ? "flag" : clean3(import_node_process10.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
8506
8515
  return {
8507
8516
  source,
8508
8517
  cacheFile: context.cfg.local.tokenFile,
@@ -8876,15 +8885,16 @@ Safety:
8876
8885
  the metadata file. Flags and specific ODLA_* scope variables beat a selected
8877
8886
  context, which beats project config. There is no ambient current context.
8878
8887
  "context show" reports only provenance and cache state and never authenticates.
8879
- Provision opens the approval page in your browser automatically whenever the
8880
- machine can show one, including agent-driven runs; only CI, SSH, and
8881
- display-less hosts skip it. Use --open to force or --no-open to suppress.
8882
- Browser launch is best-effort: the printed approval URL is authoritative and
8883
- agents must relay it to the human verbatim. A started handshake is persisted
8888
+ Every real CLI handshake prints one canonical /studio?code= approval URL and
8889
+ attempts to open it \u2014 including in CI, SSH, display-less, and agent-driven
8890
+ shells. Only --no-open suppresses the attempt. Browser launch is best-effort:
8891
+ agents with browser control must open that exact URL immediately; otherwise
8892
+ they must give it to the human verbatim. A started handshake is persisted
8884
8893
  under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
8885
8894
  the same code. Outside an interactive terminal the wait is capped (90s by
8886
8895
  default, --wait <seconds> to change); a still-pending handshake then exits
8887
- with code 75: relay the URL, wait for approval, and re-run to collect.
8896
+ with code 75: open the same URL (or relay it if browser control is unavailable),
8897
+ wait for approval, and re-run to collect.
8888
8898
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8889
8899
  The email is a non-secret identity hint: never provide a password or session
8890
8900
  token. The matching account must already exist, be signed in, explicitly
@@ -10293,7 +10303,7 @@ async function read2(url, headers, doFetch) {
10293
10303
  // src/provision.ts
10294
10304
  var import_apps12 = require("@odla-ai/apps");
10295
10305
  var import_ai3 = require("@odla-ai/ai");
10296
- var import_node_process10 = __toESM(require("process"), 1);
10306
+ var import_node_process11 = __toESM(require("process"), 1);
10297
10307
 
10298
10308
  // src/integration-provision.ts
10299
10309
  var import_db3 = require("@odla-ai/db");
@@ -10600,7 +10610,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10600
10610
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10601
10611
  }
10602
10612
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10603
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10613
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
10604
10614
  if (key) {
10605
10615
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10606
10616
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10642,7 +10652,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10642
10652
 
10643
10653
  // src/record.ts
10644
10654
  var import_node_fs16 = require("fs");
10645
- var import_node_process11 = __toESM(require("process"), 1);
10655
+ var import_node_process12 = __toESM(require("process"), 1);
10646
10656
 
10647
10657
  // src/surface.ts
10648
10658
  var PM_ACTIONS = {
@@ -10791,7 +10801,7 @@ function invocationPath(words2) {
10791
10801
 
10792
10802
  // src/record.ts
10793
10803
  function recordInvocation(parsed) {
10794
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10804
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10795
10805
  if (!file) return;
10796
10806
  try {
10797
10807
  const entry = {
@@ -11463,9 +11473,9 @@ var import_node_child_process8 = require("child_process");
11463
11473
  var import_node_fs20 = require("fs");
11464
11474
  var import_node_os5 = require("os");
11465
11475
  var import_node_path18 = require("path");
11466
- var import_node_process12 = __toESM(require("process"), 1);
11476
+ var import_node_process13 = __toESM(require("process"), 1);
11467
11477
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11468
- function resolveEditor(env = import_node_process12.default.env) {
11478
+ function resolveEditor(env = import_node_process13.default.env) {
11469
11479
  for (const name of EDITOR_ENV) {
11470
11480
  const value2 = env[name];
11471
11481
  if (value2 && value2.trim()) return value2.trim();
@@ -11479,8 +11489,8 @@ function defaultRun(command, path) {
11479
11489
  return result.status ?? 0;
11480
11490
  }
11481
11491
  function editText(initial, slug, deps = {}) {
11482
- const env = deps.env ?? import_node_process12.default.env;
11483
- const interactive = deps.interactive ?? (() => Boolean(import_node_process12.default.stdin.isTTY));
11492
+ const env = deps.env ?? import_node_process13.default.env;
11493
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process13.default.stdin.isTTY));
11484
11494
  const editor = resolveEditor(env);
11485
11495
  if (!editor)
11486
11496
  throw new Error(