@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/README.md CHANGED
@@ -426,12 +426,16 @@ 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
+ Studio shows the immutable project and capability request without grant
430
+ checkboxes. The owner approves that exact request or declines it with an
431
+ explanation which the CLI returns to the calling agent; different access
432
+ requires a new request.
433
+ Every real CLI handshake prints exactly one canonical `/studio?code=…` URL
434
+ and attempts to open it including from CI, SSH, display-less, scripted,
435
+ and agent-driven shells. Only `--no-open` suppresses the attempt. Browser
436
+ launch is best-effort: an agent with browser control must open that exact URL
437
+ immediately; otherwise it gives the URL to the human verbatim. The started
438
+ handshake is persisted privately under `.odla/`, so a
435
439
  run killed before approval loses nothing: rerunning resumes the same code.
436
440
  Outside an interactive terminal the approval wait is capped (90 seconds by
437
441
  default; `--wait <seconds>` overrides), and a still-pending handshake exits
package/REQUIREMENTS.md CHANGED
@@ -38,13 +38,19 @@ 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.
45
+ - Studio must display the complete immutable request without grant-editing
46
+ controls. Approval accepts the request digest exactly; denial requires a
47
+ bounded, non-secret explanation returned to the polling agent. Any changed
48
+ scope, project, or capability requires a fresh request.
44
49
  - Only one claimed pending or approved-but-uncollected request may be active per
45
50
  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.
51
+ - Every real CLI handshake must attempt to open that canonical approval URL,
52
+ including in CI, SSH, display-less, and agent-driven shells. `--no-open` is
53
+ the only production suppression; failures must visibly repeat the manual action.
48
54
  - Cached developer tokens must be mode `0600`, gitignored, and reused until
49
55
  expiry.
50
56
  - 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 {
@@ -341,8 +389,8 @@ async function freshHandshake(ctx, waitMs) {
341
389
  projectIds: [ctx.cfg.app.id],
342
390
  fetch: ctx.doFetch,
343
391
  waitMs,
344
- onCode: async ({ userCode, deviceCode, expiresIn, interval, verificationUriComplete }) => {
345
- const approvalUrl = verificationUriComplete ?? handshakeUrl(ctx.cfg.platformUrl, userCode);
392
+ onCode: async ({ userCode, deviceCode, expiresIn, interval }) => {
393
+ const approvalUrl = handshakeUrl(ctx.audience, userCode);
346
394
  started = {
347
395
  platform: ctx.audience,
348
396
  email: ctx.email,
@@ -353,15 +401,12 @@ async function freshHandshake(ctx, waitMs) {
353
401
  expiresAt: Date.now() + expiresIn * 1e3
354
402
  };
355
403
  writePendingHandshake(ctx.pendingFile, started);
356
- printApproval(ctx.out, {
404
+ await presentHandshakeApproval(ctx.out, {
357
405
  userCode,
358
406
  approvalUrl,
359
407
  minutesLeft: Math.floor(expiresIn / 60),
360
- purpose: `sign this terminal in as ${ctx.email}`,
361
- browserAttempted: ctx.browser.open,
362
- browserSkipped: ctx.browser.reason
363
- });
364
- await launchApproval(ctx, approvalUrl);
408
+ purpose: `sign this terminal in as ${ctx.email}`
409
+ }, ctx.options);
365
410
  stopReminder = approvalReminder(ctx.out, started);
366
411
  }
367
412
  });
@@ -376,18 +421,6 @@ async function freshHandshake(ctx, waitMs) {
376
421
  stopReminder?.();
377
422
  }
378
423
  }
379
- async function launchApproval(ctx, approvalUrl) {
380
- if (ctx.browser.open) {
381
- try {
382
- await (ctx.options.openApprovalUrl ?? openUrl)(approvalUrl);
383
- 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`);
384
- } catch (err) {
385
- ctx.out.error(`auth: could not open browser (${err instanceof Error ? err.message : String(err)})`);
386
- }
387
- } else if (ctx.browser.reason) {
388
- ctx.out.error(`auth: browser launch skipped (${ctx.browser.reason}) \u2014 show the human the URL above`);
389
- }
390
- }
391
424
  function stillPending(pending, email) {
392
425
  return new import_db.OdlaError(
393
426
  "handshake_pending",
@@ -396,30 +429,12 @@ function stillPending(pending, email) {
396
429
  );
397
430
  }
398
431
  function handshakeEmail(value2, cached) {
399
- 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();
400
433
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
401
434
  throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
402
435
  }
403
436
  return email;
404
437
  }
405
- function approvalBrowser(options, host = {}) {
406
- if (options.open === true) return { open: true, mode: "forced" };
407
- if (options.open === false) return { open: false, reason: "disabled by --no-open" };
408
- const env = host.env ?? import_node_process3.default.env;
409
- if (env.VITEST || env.NODE_ENV === "test") return { open: false, reason: "test environment" };
410
- if (env.CI) return { open: false, reason: "CI environment" };
411
- if (env.SSH_CONNECTION || env.SSH_TTY) return { open: false, reason: "SSH session; pass --open to force" };
412
- const platform = host.platform ?? import_node_process3.default.platform;
413
- if (platform !== "darwin" && platform !== "win32" && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
414
- return { open: false, reason: "no graphical display; pass --open to force" };
415
- }
416
- return { open: true, mode: "auto" };
417
- }
418
- function handshakeUrl(platformUrl, userCode) {
419
- const url = new URL("/studio", platformUrl);
420
- url.searchParams.set("code", userCode);
421
- return url.toString();
422
- }
423
438
  function platformAudience(value2) {
424
439
  let url;
425
440
  try {
@@ -438,12 +453,12 @@ function platformAudience(value2) {
438
453
  }
439
454
 
440
455
  // src/secret-input.ts
441
- var import_node_process4 = __toESM(require("process"), 1);
456
+ var import_node_process5 = __toESM(require("process"), 1);
442
457
  var MAX_BYTES = 64 * 1024;
443
458
  async function secretInputValue(options, kind = "credential") {
444
459
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
445
460
  let value2;
446
- if (options.fromEnv) value2 = import_node_process4.default.env[options.fromEnv];
461
+ if (options.fromEnv) value2 = import_node_process5.default.env[options.fromEnv];
447
462
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
448
463
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
449
464
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -451,7 +466,7 @@ async function secretInputValue(options, kind = "credential") {
451
466
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
452
467
  return value2;
453
468
  }
454
- async function readSecretStream(kind, stream = import_node_process4.default.stdin) {
469
+ async function readSecretStream(kind, stream = import_node_process5.default.stdin) {
455
470
  let value2 = "";
456
471
  for await (const chunk of stream) {
457
472
  value2 += String(chunk);
@@ -463,7 +478,7 @@ async function readSecretStream(kind, stream = import_node_process4.default.stdi
463
478
  // src/admin-ai-auth.ts
464
479
  var import_node_fs3 = require("fs");
465
480
  var import_node_path3 = require("path");
466
- var import_node_process5 = __toESM(require("process"), 1);
481
+ var import_node_process6 = __toESM(require("process"), 1);
467
482
  var import_db2 = require("@odla-ai/db");
468
483
  async function getScopedPlatformToken(options) {
469
484
  return resolveAdminPlatformToken(options);
@@ -471,7 +486,7 @@ async function getScopedPlatformToken(options) {
471
486
  async function resolveAdminPlatformToken(options) {
472
487
  const audience = platformAudience(options.platform);
473
488
  if (options.token) return options.token;
474
- const fromEnv = import_node_process5.default.env.ODLA_ADMIN_TOKEN;
489
+ const fromEnv = import_node_process6.default.env.ODLA_ADMIN_TOKEN;
475
490
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
476
491
  return scopedToken(
477
492
  audience,
@@ -483,7 +498,7 @@ async function resolveAdminPlatformToken(options) {
483
498
  }
484
499
  function audienceBoundEnvToken(token, platform) {
485
500
  const audience = platformAudience(platform);
486
- const declared = import_node_process5.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
501
+ const declared = import_node_process6.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
487
502
  if (declared) {
488
503
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
489
504
  } else if (audience !== "https://odla.ai") {
@@ -508,7 +523,7 @@ var SCOPE_PURPOSE = {
508
523
  };
509
524
  async function scopedToken(platform, scope, options, doFetch, out) {
510
525
  const audience = platformAudience(platform);
511
- const rootDir = options.rootDir ?? import_node_process5.default.cwd();
526
+ const rootDir = options.rootDir ?? import_node_process6.default.cwd();
512
527
  const tokenFile = options.tokenFile ?? (0, import_node_path3.join)(rootDir, ".odla/admin-token.local.json");
513
528
  const cache = options.cache === false ? null : readJsonFile(tokenFile);
514
529
  const cached = cache?.platform === audience ? cache.tokens?.[scope] : void 0;
@@ -523,21 +538,14 @@ async function scopedToken(platform, scope, options, doFetch, out) {
523
538
  label: options.label ?? `odla CLI (${scope})`,
524
539
  scopes: [scope],
525
540
  fetch: doFetch,
526
- onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
527
- const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
528
- const browser = approvalBrowser({ open: options.open });
529
- printApproval(out, {
541
+ onCode: async ({ userCode, expiresIn }) => {
542
+ await presentHandshakeApproval(out, {
530
543
  userCode,
531
- approvalUrl,
544
+ approvalUrl: handshakeUrl(audience, userCode),
532
545
  minutesLeft: Math.floor(expiresIn / 60),
533
546
  purpose: SCOPE_PURPOSE[scope] ?? `use ${scope}`,
534
- approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin",
535
- browserAttempted: browser.open,
536
- browserSkipped: browser.reason
537
- });
538
- if (browser.open) {
539
- await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
540
- }
547
+ approver: scope.startsWith("app:") ? "A signed-in app owner" : "A signed-in odla platform admin"
548
+ }, options);
541
549
  }
542
550
  });
543
551
  if (options.cache !== false) {
@@ -720,7 +728,7 @@ function isRecord2(value2) {
720
728
 
721
729
  // src/admin-ai.ts
722
730
  async function adminAi(options) {
723
- 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");
724
732
  const doFetch = options.fetch ?? fetch;
725
733
  const out = options.stdout ?? console;
726
734
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -977,7 +985,7 @@ function addOption(options, name, value2) {
977
985
  // src/operator-context.ts
978
986
  var import_node_fs6 = require("fs");
979
987
  var import_node_path6 = require("path");
980
- var import_node_process8 = __toESM(require("process"), 1);
988
+ var import_node_process9 = __toESM(require("process"), 1);
981
989
 
982
990
  // src/config.ts
983
991
  var import_node_fs4 = require("fs");
@@ -1283,15 +1291,15 @@ function unique2(values) {
1283
1291
  var import_node_fs5 = require("fs");
1284
1292
  var import_node_os = require("os");
1285
1293
  var import_node_path5 = require("path");
1286
- var import_node_process7 = __toESM(require("process"), 1);
1294
+ var import_node_process8 = __toESM(require("process"), 1);
1287
1295
  function operatorProfileFile() {
1288
1296
  return (0, import_node_path5.resolve)(
1289
- 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")
1290
1298
  );
1291
1299
  }
1292
1300
  function resolveOperatorProfile(parsed) {
1293
1301
  const fromFlag = clean(stringOpt(parsed.options.context));
1294
- const fromEnvironment = clean(import_node_process7.default.env.ODLA_CONTEXT);
1302
+ const fromEnvironment = clean(import_node_process8.default.env.ODLA_CONTEXT);
1295
1303
  const name = fromFlag ?? fromEnvironment ?? null;
1296
1304
  const file = operatorProfileFile();
1297
1305
  if (!name) {
@@ -1420,13 +1428,13 @@ async function resolveOperatorContext(parsed, options = {}) {
1420
1428
  }
1421
1429
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
1422
1430
  const platformFlag = clean2(stringOpt(parsed.options.platform));
1423
- const platformEnvironment = clean2(import_node_process8.default.env.ODLA_PLATFORM_URL);
1431
+ const platformEnvironment = clean2(import_node_process9.default.env.ODLA_PLATFORM_URL);
1424
1432
  const platformValue = platformAudience(
1425
1433
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
1426
1434
  );
1427
1435
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
1428
1436
  const appFlag = clean2(stringOpt(parsed.options.app));
1429
- const appEnvironment = clean2(import_node_process8.default.env.ODLA_APP_ID);
1437
+ const appEnvironment = clean2(import_node_process9.default.env.ODLA_APP_ID);
1430
1438
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
1431
1439
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
1432
1440
  if (appValue) assertOperatorName(appValue, "app");
@@ -1436,16 +1444,16 @@ async function resolveOperatorContext(parsed, options = {}) {
1436
1444
  );
1437
1445
  }
1438
1446
  const envFlag = clean2(stringOpt(parsed.options.env));
1439
- const envEnvironment = clean2(import_node_process8.default.env.ODLA_ENV);
1447
+ const envEnvironment = clean2(import_node_process9.default.env.ODLA_ENV);
1440
1448
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
1441
1449
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
1442
1450
  if (environmentValue) {
1443
1451
  assertOperatorName(environmentValue, "environment");
1444
1452
  }
1445
- const rootDir = loaded?.rootDir ?? import_node_process8.default.cwd();
1453
+ const rootDir = loaded?.rootDir ?? import_node_process9.default.cwd();
1446
1454
  const profileCredentials = operatorCredentialFiles(profile);
1447
- 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;
1448
- 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;
1449
1457
  const cfg = loaded ? {
1450
1458
  ...loaded,
1451
1459
  platformUrl: platformValue,
@@ -8497,13 +8505,13 @@ async function codeCommand(parsed, dependencies) {
8497
8505
  }
8498
8506
 
8499
8507
  // src/operator-credentials.ts
8500
- var import_node_process9 = __toESM(require("process"), 1);
8508
+ var import_node_process10 = __toESM(require("process"), 1);
8501
8509
  function developerTokenStatus(context, parsed, now = Date.now()) {
8502
8510
  const cached = readJsonFile(context.cfg.local.tokenFile);
8503
8511
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
8504
8512
  const source = clean3(
8505
8513
  stringOpt(parsed.options.token)
8506
- ) ? "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";
8507
8515
  return {
8508
8516
  source,
8509
8517
  cacheFile: context.cfg.local.tokenFile,
@@ -8877,15 +8885,16 @@ Safety:
8877
8885
  the metadata file. Flags and specific ODLA_* scope variables beat a selected
8878
8886
  context, which beats project config. There is no ambient current context.
8879
8887
  "context show" reports only provenance and cache state and never authenticates.
8880
- Provision opens the approval page in your browser automatically whenever the
8881
- machine can show one, including agent-driven runs; only CI, SSH, and
8882
- display-less hosts skip it. Use --open to force or --no-open to suppress.
8883
- Browser launch is best-effort: the printed approval URL is authoritative and
8884
- 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
8885
8893
  under .odla/, so a command killed mid-wait loses nothing \u2014 rerunning resumes
8886
8894
  the same code. Outside an interactive terminal the wait is capped (90s by
8887
8895
  default, --wait <seconds> to change); a still-pending handshake then exits
8888
- 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.
8889
8898
  A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
8890
8899
  The email is a non-secret identity hint: never provide a password or session
8891
8900
  token. The matching account must already exist, be signed in, explicitly
@@ -10294,7 +10303,7 @@ async function read2(url, headers, doFetch) {
10294
10303
  // src/provision.ts
10295
10304
  var import_apps12 = require("@odla-ai/apps");
10296
10305
  var import_ai3 = require("@odla-ai/ai");
10297
- var import_node_process10 = __toESM(require("process"), 1);
10306
+ var import_node_process11 = __toESM(require("process"), 1);
10298
10307
 
10299
10308
  // src/integration-provision.ts
10300
10309
  var import_db3 = require("@odla-ai/db");
@@ -10601,7 +10610,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10601
10610
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
10602
10611
  }
10603
10612
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
10604
- const key = import_node_process10.default.env[cfg.ai.keyEnv];
10613
+ const key = import_node_process11.default.env[cfg.ai.keyEnv];
10605
10614
  if (key) {
10606
10615
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
10607
10616
  await (0, import_ai3.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -10643,7 +10652,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
10643
10652
 
10644
10653
  // src/record.ts
10645
10654
  var import_node_fs16 = require("fs");
10646
- var import_node_process11 = __toESM(require("process"), 1);
10655
+ var import_node_process12 = __toESM(require("process"), 1);
10647
10656
 
10648
10657
  // src/surface.ts
10649
10658
  var PM_ACTIONS = {
@@ -10792,7 +10801,7 @@ function invocationPath(words2) {
10792
10801
 
10793
10802
  // src/record.ts
10794
10803
  function recordInvocation(parsed) {
10795
- const file = import_node_process11.default.env.ODLA_CLI_RECORD;
10804
+ const file = import_node_process12.default.env.ODLA_CLI_RECORD;
10796
10805
  if (!file) return;
10797
10806
  try {
10798
10807
  const entry = {
@@ -11464,9 +11473,9 @@ var import_node_child_process8 = require("child_process");
11464
11473
  var import_node_fs20 = require("fs");
11465
11474
  var import_node_os5 = require("os");
11466
11475
  var import_node_path18 = require("path");
11467
- var import_node_process12 = __toESM(require("process"), 1);
11476
+ var import_node_process13 = __toESM(require("process"), 1);
11468
11477
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
11469
- function resolveEditor(env = import_node_process12.default.env) {
11478
+ function resolveEditor(env = import_node_process13.default.env) {
11470
11479
  for (const name of EDITOR_ENV) {
11471
11480
  const value2 = env[name];
11472
11481
  if (value2 && value2.trim()) return value2.trim();
@@ -11480,8 +11489,8 @@ function defaultRun(command, path) {
11480
11489
  return result.status ?? 0;
11481
11490
  }
11482
11491
  function editText(initial, slug, deps = {}) {
11483
- const env = deps.env ?? import_node_process12.default.env;
11484
- 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));
11485
11494
  const editor = resolveEditor(env);
11486
11495
  if (!editor)
11487
11496
  throw new Error(