@odla-ai/cli 0.38.2 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.cjs CHANGED
@@ -347,45 +347,15 @@ var init_handshake_state = __esm({
347
347
  }
348
348
  });
349
349
 
350
- // src/cached-credential.ts
351
- function noteCachedCredential(tokenFile) {
352
- noted = tokenFile;
353
- }
354
- function isCredentialRejection(error) {
355
- const message2 = error instanceof Error ? error.message : String(error ?? "");
356
- return /\((401|403)\)\s*$/.test(message2.trim());
357
- }
358
- function explainRejectedCredential(error) {
359
- const tokenFile = noted;
360
- if (!tokenFile || !isCredentialRejection(error)) return null;
361
- noted = null;
362
- (0, import_node_fs5.rmSync)(tokenFile, { force: true });
363
- return [
364
- "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
365
- " The usual cause is a newer sign-in for this project: collecting a handshake retires the",
366
- " principal's other credentials, so a second terminal or worktree supersedes this one.",
367
- ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`
368
- ].join("\n");
369
- }
370
- var import_node_fs5, noted;
371
- var init_cached_credential = __esm({
372
- "src/cached-credential.ts"() {
373
- "use strict";
374
- init_cjs_shims();
375
- import_node_fs5 = require("fs");
376
- noted = null;
377
- }
378
- });
379
-
380
350
  // src/device-session.ts
381
351
  function deviceCredentialPath(env = import_node_process4.default.env) {
382
352
  return env.ODLA_DEVICE_CREDENTIAL ?? (0, import_node_path3.join)(env.HOME ?? (0, import_node_os.homedir)(), ".odla", "device.json");
383
353
  }
384
354
  function readDeviceCredential(platform, env = import_node_process4.default.env) {
385
355
  const path = deviceCredentialPath(env);
386
- if (!(0, import_node_fs6.existsSync)(path)) return null;
356
+ if (!(0, import_node_fs5.existsSync)(path)) return null;
387
357
  try {
388
- const parsed = JSON.parse((0, import_node_fs6.readFileSync)(path, "utf8"));
358
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(path, "utf8"));
389
359
  if (typeof parsed.token !== "string" || !parsed.token.startsWith("odla_device_")) return null;
390
360
  if (parsed.platform !== platform) return null;
391
361
  return { ...parsed, token: parsed.token, platform: parsed.platform };
@@ -407,20 +377,86 @@ async function mintDeviceSession(platformUrl, credential2, doFetch) {
407
377
  `device session failed: ${detail} (${response2.status})` + (revocable ? " \u2014 if this machine's enrollment was revoked or has expired, enroll it again in Studio" : "")
408
378
  );
409
379
  }
410
- return { token: body.token, expiresAt: body.expiresAt ?? Date.now() };
380
+ return {
381
+ token: body.token,
382
+ expiresAt: body.expiresAt ?? Date.now(),
383
+ // Absent from a registry that predates rolling expiry and scoped devices.
384
+ // Left undefined rather than defaulted, so a caller can tell "the platform
385
+ // did not say" from "the platform said none".
386
+ ...typeof body.deviceExpiresAt === "number" ? { deviceExpiresAt: body.deviceExpiresAt } : {},
387
+ ...Array.isArray(body.appIds) ? { appIds: body.appIds } : {},
388
+ ...Array.isArray(body.capabilities) ? { capabilities: body.capabilities } : {},
389
+ ...Array.isArray(body.scopes) ? { scopes: body.scopes } : {}
390
+ };
411
391
  }
412
- var import_node_fs6, import_node_os, import_node_path3, import_node_process4;
392
+ var import_node_fs5, import_node_os, import_node_path3, import_node_process4;
413
393
  var init_device_session = __esm({
414
394
  "src/device-session.ts"() {
415
395
  "use strict";
416
396
  init_cjs_shims();
417
- import_node_fs6 = require("fs");
397
+ import_node_fs5 = require("fs");
418
398
  import_node_os = require("os");
419
399
  import_node_path3 = require("path");
420
400
  import_node_process4 = __toESM(require("process"), 1);
421
401
  }
422
402
  });
423
403
 
404
+ // src/odla-home.ts
405
+ function odlaHome(env = import_node_process5.default.env) {
406
+ return env.ODLA_HOME ?? (0, import_node_path4.join)(env.HOME ?? (0, import_node_os2.homedir)(), ".odla");
407
+ }
408
+ function odlaHomePath(segments, env = import_node_process5.default.env) {
409
+ return (0, import_node_path4.join)(odlaHome(env), ...segments);
410
+ }
411
+ function identityFile(env) {
412
+ return odlaHomePath(["identity.json"], env);
413
+ }
414
+ function deviceSessionFile(env) {
415
+ return odlaHomePath(["session.json"], env);
416
+ }
417
+ function appTokenFile(appId, env) {
418
+ return odlaHomePath(["apps", safeSegment(appId), "dev-token.json"], env);
419
+ }
420
+ function appCredentialsFile(appId, env) {
421
+ return odlaHomePath(["apps", safeSegment(appId), "credentials.json"], env);
422
+ }
423
+ function scopedTokenFile(env) {
424
+ return odlaHomePath(["admin-token.local.json"], env);
425
+ }
426
+ function pmContextFile(env) {
427
+ return odlaHomePath(["pm-context.json"], env);
428
+ }
429
+ function adoptRepoLocalCache(legacyPath, machinePath, out) {
430
+ if (!(0, import_node_fs6.existsSync)(legacyPath) || legacyPath === machinePath) return false;
431
+ const superseded = (0, import_node_fs6.existsSync)(machinePath);
432
+ if (!superseded) {
433
+ (0, import_node_fs6.mkdirSync)((0, import_node_path4.dirname)(machinePath), { recursive: true });
434
+ (0, import_node_fs6.copyFileSync)(legacyPath, machinePath);
435
+ (0, import_node_fs6.chmodSync)(machinePath, 384);
436
+ }
437
+ (0, import_node_fs6.rmSync)(legacyPath, { force: true });
438
+ out?.error(
439
+ superseded ? `auth: removed superseded ${legacyPath}; this machine's credentials live in ${odlaHome()}` : `auth: moved ${legacyPath} into ${machinePath}; credentials are per machine now, not per worktree`
440
+ );
441
+ return true;
442
+ }
443
+ function safeSegment(value2) {
444
+ const clean4 = value2.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
445
+ if (!clean4) throw new Error(`"${value2}" is not a usable app id`);
446
+ return clean4;
447
+ }
448
+ var import_node_fs6, import_node_os2, import_node_path4, import_node_process5;
449
+ var init_odla_home = __esm({
450
+ "src/odla-home.ts"() {
451
+ "use strict";
452
+ init_cjs_shims();
453
+ import_node_fs6 = require("fs");
454
+ import_node_os2 = require("os");
455
+ import_node_path4 = require("path");
456
+ import_node_process5 = __toESM(require("process"), 1);
457
+ }
458
+ });
459
+
424
460
  // src/local.ts
425
461
  function readJsonFile(path) {
426
462
  try {
@@ -467,7 +503,7 @@ function mergeCredential(current, update) {
467
503
  return next;
468
504
  }
469
505
  function ensureGitignore(rootDir, localPaths = []) {
470
- const path = (0, import_node_path4.resolve)(rootDir, ".gitignore");
506
+ const path = (0, import_node_path5.resolve)(rootDir, ".gitignore");
471
507
  const existing = (0, import_node_fs7.existsSync)(path) ? (0, import_node_fs7.readFileSync)(path, "utf8") : "";
472
508
  const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
473
509
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
@@ -488,7 +524,7 @@ function o11yDevVars(cfg) {
488
524
  function resolveWriteDevVarsTarget(cfg, requested) {
489
525
  if (!requested) return null;
490
526
  if (requested === true) return cfg.local.devVarsFile;
491
- return (0, import_node_path4.resolve)((0, import_node_path4.dirname)(cfg.configPath), requested);
527
+ return (0, import_node_path5.resolve)((0, import_node_path5.dirname)(cfg.configPath), requested);
492
528
  }
493
529
  function writeDevVars(path, credentials, env, o11y) {
494
530
  const entry = credentials.envs[env];
@@ -516,28 +552,28 @@ function isManagedDevVar(line2) {
516
552
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
517
553
  }
518
554
  function writePrivateText(path, text3) {
519
- (0, import_node_fs7.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
555
+ (0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)(path), { recursive: true });
520
556
  const temporary = `${path}.tmp-${process.pid}-${Date.now()}`;
521
557
  (0, import_node_fs7.writeFileSync)(temporary, text3, { mode: 384 });
522
558
  (0, import_node_fs7.chmodSync)(temporary, 384);
523
559
  (0, import_node_fs7.renameSync)(temporary, path);
524
560
  }
525
561
  function gitignoreEntry(rootDir, path) {
526
- const rel = (0, import_node_path4.relative)((0, import_node_path4.resolve)(rootDir), (0, import_node_path4.resolve)(path));
527
- if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path4.isAbsolute)(rel)) return null;
562
+ const rel = (0, import_node_path5.relative)((0, import_node_path5.resolve)(rootDir), (0, import_node_path5.resolve)(path));
563
+ if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || (0, import_node_path5.isAbsolute)(rel)) return null;
528
564
  return rel.replaceAll("\\", "/");
529
565
  }
530
566
  function displayPath(path, rootDir = process.cwd()) {
531
- const rel = (0, import_node_path4.relative)(rootDir, path);
567
+ const rel = (0, import_node_path5.relative)(rootDir, path);
532
568
  return rel && !rel.startsWith("..") ? rel : path;
533
569
  }
534
- var import_node_fs7, import_node_path4, GITIGNORE_LINES, MANAGED_DEV_VARS;
570
+ var import_node_fs7, import_node_path5, GITIGNORE_LINES, MANAGED_DEV_VARS;
535
571
  var init_local = __esm({
536
572
  "src/local.ts"() {
537
573
  "use strict";
538
574
  init_cjs_shims();
539
575
  import_node_fs7 = require("fs");
540
- import_node_path4 = require("path");
576
+ import_node_path5 = require("path");
541
577
  GITIGNORE_LINES = [".odla/*.local.json", ".odla/dev-token.json", ".dev.vars"];
542
578
  MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
543
579
  "ODLA_PLATFORM",
@@ -554,6 +590,133 @@ var init_local = __esm({
554
590
  }
555
591
  });
556
592
 
593
+ // src/auth-guidance.ts
594
+ function machineAuthState(audience, env = import_node_process6.default.env) {
595
+ const device = readDeviceCredential(audience, env);
596
+ if (!device) return { enrolled: false };
597
+ const session = readJsonFile(deviceSessionFile(env));
598
+ const current = session?.platform === audience && session.deviceId === device.deviceId ? session : void 0;
599
+ return {
600
+ enrolled: true,
601
+ ...device.name ? { deviceName: device.name } : {},
602
+ ...current?.appIds ? { appIds: current.appIds } : {},
603
+ ...current?.capabilities ? { capabilities: current.capabilities } : {},
604
+ ...current?.scopes ? { scopes: current.scopes } : {},
605
+ ...current?.deviceExpiresAt ? { lapsesAt: current.deviceExpiresAt } : {}
606
+ };
607
+ }
608
+ function scopeInterruptionNotice(scope, state2) {
609
+ const platformScope = scope.startsWith("platform:");
610
+ const reason = !state2.enrolled ? "this machine is not enrolled, so every privileged command needs its own browser approval" : `this machine is enrolled but its approval did not include "${scope}"`;
611
+ return [
612
+ `odla: ${reason}.`,
613
+ ` Approve this one now, then end the interruptions with:`,
614
+ ` ${platformScope ? ENROL_PLATFORM_WIDE : ENROL_EVERYTHING}`,
615
+ platformScope ? " A platform scope needs an administrator's approval; an app owner's cannot carry it." : " One approval, every app you own, and it rolls forward while you keep working."
616
+ ].join("\n");
617
+ }
618
+ function lapseNotice(state2, now = Date.now()) {
619
+ if (!state2.enrolled || !state2.lapsesAt) return null;
620
+ const days = Math.floor((state2.lapsesAt - now) / (24 * 60 * 60 * 1e3));
621
+ if (days < 0) return "this machine's enrollment has lapsed; the next command will ask for approval";
622
+ return `idle for ${days} more day${days === 1 ? "" : "s"} before this machine needs approving again (using it resets the clock)`;
623
+ }
624
+ var import_node_process6, ENROL_EVERYTHING, ENROL_PLATFORM_WIDE;
625
+ var init_auth_guidance = __esm({
626
+ "src/auth-guidance.ts"() {
627
+ "use strict";
628
+ init_cjs_shims();
629
+ import_node_process6 = __toESM(require("process"), 1);
630
+ init_device_session();
631
+ init_odla_home();
632
+ init_local();
633
+ ENROL_EVERYTHING = "npx odla-ai device enroll --all-apps --capability all --no-open --wait 600";
634
+ ENROL_PLATFORM_WIDE = "npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600";
635
+ }
636
+ });
637
+
638
+ // src/cached-credential.ts
639
+ function noteCachedCredential(tokenFile) {
640
+ noted = tokenFile;
641
+ }
642
+ function isCredentialRejection(error) {
643
+ const message2 = error instanceof Error ? error.message : String(error ?? "");
644
+ return /\((401|403)\)\s*$/.test(message2.trim());
645
+ }
646
+ function explainRejectedCredential(error) {
647
+ const tokenFile = noted;
648
+ if (!tokenFile || !isCredentialRejection(error)) return null;
649
+ noted = null;
650
+ (0, import_node_fs8.rmSync)(tokenFile, { force: true });
651
+ return [
652
+ "auth: the cached credential was rejected by odla, so it was revoked before its cached expiry.",
653
+ " The usual cause is a newer sign-in for this account: collecting a handshake retires the",
654
+ " principal's other collected credentials, so a second machine supersedes this one.",
655
+ ` Discarded ${tokenFile}; re-run this command to request a fresh approval.`,
656
+ ` To stop needing one: ${ENROL_EVERYTHING}`
657
+ ].join("\n");
658
+ }
659
+ var import_node_fs8, noted;
660
+ var init_cached_credential = __esm({
661
+ "src/cached-credential.ts"() {
662
+ "use strict";
663
+ init_cjs_shims();
664
+ import_node_fs8 = require("fs");
665
+ init_auth_guidance();
666
+ noted = null;
667
+ }
668
+ });
669
+
670
+ // src/device-session-cache.ts
671
+ async function deviceSessionToken(platformUrl, audience, credential2, doFetch, env = import_node_process7.default.env) {
672
+ const path = deviceSessionFile(env);
673
+ const cached = readJsonFile(path);
674
+ if (cached?.token && cached.platform === audience && cached.deviceId === credential2.deviceId && (cached.expiresAt ?? 0) > Date.now() + SKEW_MS) return cached;
675
+ const minted = await mintDeviceSession(platformUrl, credential2, doFetch);
676
+ const session = {
677
+ ...minted,
678
+ platform: audience,
679
+ ...credential2.deviceId ? { deviceId: credential2.deviceId } : {}
680
+ };
681
+ writePrivateJson(path, session);
682
+ return session;
683
+ }
684
+ var import_node_process7, SKEW_MS;
685
+ var init_device_session_cache = __esm({
686
+ "src/device-session-cache.ts"() {
687
+ "use strict";
688
+ init_cjs_shims();
689
+ import_node_process7 = __toESM(require("process"), 1);
690
+ init_odla_home();
691
+ init_local();
692
+ init_device_session();
693
+ SKEW_MS = 6e4;
694
+ }
695
+ });
696
+
697
+ // src/machine-identity.ts
698
+ function readMachineIdentity(audience, env = import_node_process8.default.env) {
699
+ const stored = readJsonFile(identityFile(env));
700
+ if (!stored || typeof stored.email !== "string" || !stored.email) return null;
701
+ return stored.platform === audience ? { platform: audience, email: stored.email } : null;
702
+ }
703
+ function rememberMachineIdentity(audience, email, env = import_node_process8.default.env) {
704
+ if (!email) return;
705
+ const existing = readMachineIdentity(audience, env);
706
+ if (existing?.email === email) return;
707
+ writePrivateJson(identityFile(env), { platform: audience, email });
708
+ }
709
+ var import_node_process8;
710
+ var init_machine_identity = __esm({
711
+ "src/machine-identity.ts"() {
712
+ "use strict";
713
+ init_cjs_shims();
714
+ import_node_process8 = __toESM(require("process"), 1);
715
+ init_odla_home();
716
+ init_local();
717
+ }
718
+ });
719
+
557
720
  // src/token.ts
558
721
  async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {}) {
559
722
  const audience = platformAudience(cfg.platformUrl);
@@ -562,19 +725,19 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
562
725
  const cached = readJsonFile(cfg.local.tokenFile);
563
726
  if (!grantRequest.forceReview && !grantRequest.freshLogin) {
564
727
  if (options.token) return options.token;
565
- if (import_node_process5.default.env.ODLA_DEV_TOKEN) {
566
- const declared = import_node_process5.default.env.ODLA_DEV_TOKEN_AUDIENCE;
728
+ if (import_node_process9.default.env.ODLA_DEV_TOKEN) {
729
+ const declared = import_node_process9.default.env.ODLA_DEV_TOKEN_AUDIENCE;
567
730
  if (declared) {
568
731
  if (platformAudience(declared) !== audience) throw new Error("ODLA_DEV_TOKEN_AUDIENCE does not match the configured platform");
569
732
  } else if (audience !== "https://odla.ai") {
570
733
  throw new Error("ODLA_DEV_TOKEN_AUDIENCE is required for a non-default platform");
571
734
  }
572
- return import_node_process5.default.env.ODLA_DEV_TOKEN;
735
+ return import_node_process9.default.env.ODLA_DEV_TOKEN;
573
736
  }
574
737
  const device = readDeviceCredential(audience);
575
738
  if (device) {
576
- const session = await mintDeviceSession(cfg.platformUrl, device, doFetch);
577
- out.error(`auth: session minted by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
739
+ const session = await deviceSessionToken(cfg.platformUrl, audience, device, doFetch);
740
+ out.error(`auth: session held by this enrolled device (${displayPath(deviceCredentialPath(), cfg.rootDir)})`);
578
741
  return session.token;
579
742
  }
580
743
  if (cached?.token && cached.platform === audience && (cached.expiresAt ?? 0) > Date.now() + 6e4 && cachedGrantCovers(cached, grantIntent)) {
@@ -594,7 +757,10 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
594
757
  doFetch,
595
758
  out,
596
759
  audience,
597
- email: handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0),
760
+ email: handshakeEmail(
761
+ options.email,
762
+ (cached?.platform === audience ? cached.email : void 0) ?? readMachineIdentity(audience)?.email
763
+ ),
598
764
  pendingFile: handshakeFile(cfg),
599
765
  grantIntent
600
766
  };
@@ -611,6 +777,7 @@ async function getDeveloperToken(cfg, options, doFetch, out, grantRequest = {})
611
777
  expiresAt
612
778
  });
613
779
  out.error(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
780
+ rememberMachineIdentity(audience, ctx.email);
614
781
  return token;
615
782
  }
616
783
  async function freshHandshake(ctx, waitMs) {
@@ -680,7 +847,7 @@ function stillPending(pending, email) {
680
847
  );
681
848
  }
682
849
  function handshakeEmail(value2, cached) {
683
- const email = (value2 ?? import_node_process5.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
850
+ const email = (value2 ?? import_node_process9.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
684
851
  if (/@users\.noreply\.github\.com$/i.test(email)) {
685
852
  throw new Error(
686
853
  `"${email}" is a GitHub commit identity, not an odla account email; use --email <signed-in-odla-account> or ODLA_USER_EMAIL`
@@ -709,18 +876,20 @@ function platformAudience(value2) {
709
876
  }
710
877
  return url.origin;
711
878
  }
712
- var import_db, import_node_crypto, import_node_process5;
879
+ var import_db, import_node_crypto, import_node_process9;
713
880
  var init_token = __esm({
714
881
  "src/token.ts"() {
715
882
  "use strict";
716
883
  init_cjs_shims();
717
884
  import_db = require("@odla-ai/db");
718
885
  import_node_crypto = require("crypto");
719
- import_node_process5 = __toESM(require("process"), 1);
886
+ import_node_process9 = __toESM(require("process"), 1);
720
887
  init_handshake_approval();
721
888
  init_handshake_state();
722
889
  init_cached_credential();
723
890
  init_device_session();
891
+ init_device_session_cache();
892
+ init_machine_identity();
724
893
  init_local();
725
894
  }
726
895
  });
@@ -729,7 +898,7 @@ var init_token = __esm({
729
898
  async function secretInputValue(options, kind = "credential") {
730
899
  if (options.fromEnv && options.stdin) throw new Error("choose exactly one of --from-env or --stdin");
731
900
  let value2;
732
- if (options.fromEnv) value2 = import_node_process6.default.env[options.fromEnv];
901
+ if (options.fromEnv) value2 = import_node_process10.default.env[options.fromEnv];
733
902
  else if (options.stdin) value2 = await (options.readStdin ?? (() => readSecretStream(kind)))();
734
903
  else throw new Error(`${kind} input required: use --from-env <NAME> or --stdin; values are never accepted as arguments`);
735
904
  value2 = value2?.replace(/[\r\n]+$/, "");
@@ -737,7 +906,7 @@ async function secretInputValue(options, kind = "credential") {
737
906
  if (new TextEncoder().encode(value2).byteLength > MAX_BYTES) throw new Error(`${kind} exceeds 64 KiB`);
738
907
  return value2;
739
908
  }
740
- async function readSecretStream(kind, stream = import_node_process6.default.stdin) {
909
+ async function readSecretStream(kind, stream = import_node_process10.default.stdin) {
741
910
  let value2 = "";
742
911
  for await (const chunk of stream) {
743
912
  value2 += String(chunk);
@@ -745,12 +914,12 @@ async function readSecretStream(kind, stream = import_node_process6.default.stdi
745
914
  }
746
915
  return value2;
747
916
  }
748
- var import_node_process6, MAX_BYTES;
917
+ var import_node_process10, MAX_BYTES;
749
918
  var init_secret_input = __esm({
750
919
  "src/secret-input.ts"() {
751
920
  "use strict";
752
921
  init_cjs_shims();
753
- import_node_process6 = __toESM(require("process"), 1);
922
+ import_node_process10 = __toESM(require("process"), 1);
754
923
  MAX_BYTES = 64 * 1024;
755
924
  }
756
925
  });
@@ -762,7 +931,7 @@ async function getScopedPlatformToken(options) {
762
931
  async function resolveAdminPlatformToken(options) {
763
932
  const audience = platformAudience(options.platform);
764
933
  if (options.token) return options.token;
765
- const fromEnv = import_node_process7.default.env.ODLA_ADMIN_TOKEN;
934
+ const fromEnv = import_node_process11.default.env.ODLA_ADMIN_TOKEN;
766
935
  if (fromEnv) return audienceBoundEnvToken(fromEnv, audience);
767
936
  return scopedToken(
768
937
  audience,
@@ -774,7 +943,7 @@ async function resolveAdminPlatformToken(options) {
774
943
  }
775
944
  function audienceBoundEnvToken(token, platform) {
776
945
  const audience = platformAudience(platform);
777
- const declared = import_node_process7.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
946
+ const declared = import_node_process11.default.env.ODLA_ADMIN_TOKEN_AUDIENCE;
778
947
  if (declared) {
779
948
  if (platformAudience(declared) !== audience) throw new Error("ODLA_ADMIN_TOKEN_AUDIENCE does not match the configured platform");
780
949
  } else if (audience !== "https://odla.ai") {
@@ -784,15 +953,28 @@ function audienceBoundEnvToken(token, platform) {
784
953
  }
785
954
  async function scopedToken(platform, scope, options, doFetch, out) {
786
955
  const audience = platformAudience(platform);
787
- const rootDir = options.rootDir ?? import_node_process7.default.cwd();
788
- const tokenFile = options.tokenFile ?? (0, import_node_path5.join)(rootDir, ".odla/admin-token.local.json");
956
+ const rootDir = options.rootDir ?? import_node_process11.default.cwd();
957
+ const tokenFile = options.tokenFile ?? scopedTokenFile();
958
+ adoptRepoLocalCache((0, import_node_path6.join)(rootDir, ".odla/admin-token.local.json"), tokenFile, out);
959
+ const device = readDeviceCredential(audience);
960
+ if (device && options.cache !== false) {
961
+ const session = await deviceSessionToken(platform, audience, device, doFetch);
962
+ if (session.scopes?.includes(scope)) {
963
+ out.error(`auth: ${scope} held by this enrolled device`);
964
+ return session.token;
965
+ }
966
+ }
789
967
  const cache2 = options.cache === false ? null : readJsonFile(tokenFile);
790
968
  const cached = cache2?.platform === audience ? cache2.tokens?.[scope] : void 0;
791
969
  if (cached?.token && (cached.expiresAt ?? 0) > Date.now() + 6e4) {
792
970
  out.error(`auth: using cached ${scope} grant (${tokenFile})`);
793
971
  return cached.token;
794
972
  }
795
- const email = handshakeEmail(options.email, cache2?.platform === audience ? cache2.email : void 0);
973
+ out.error(scopeInterruptionNotice(scope, machineAuthState(audience)));
974
+ const email = handshakeEmail(
975
+ options.email,
976
+ (cache2?.platform === audience ? cache2.email : void 0) ?? readMachineIdentity(audience)?.email
977
+ );
796
978
  const { token, expiresAt } = await (0, import_db2.requestToken)({
797
979
  endpoint: audience,
798
980
  email,
@@ -812,26 +994,30 @@ async function scopedToken(platform, scope, options, doFetch, out) {
812
994
  if (options.cache !== false) {
813
995
  const tokens = cache2?.platform === audience ? { ...cache2.tokens ?? {} } : {};
814
996
  tokens[scope] = { token, expiresAt };
815
- if ((0, import_node_fs8.existsSync)((0, import_node_path5.join)(rootDir, ".git"))) ensureGitignore(rootDir, [tokenFile]);
816
997
  writePrivateJson(tokenFile, { platform: audience, email, tokens });
998
+ rememberMachineIdentity(audience, email);
817
999
  out.error(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
818
1000
  } else {
819
1001
  out.error(`auth: ${scope} grant is in memory only; its credential record remains in odla-ai/db`);
820
1002
  }
821
1003
  return token;
822
1004
  }
823
- var import_node_fs8, import_node_path5, import_node_process7, import_db2, SCOPE_PURPOSE;
1005
+ var import_node_path6, import_node_process11, import_db2, SCOPE_PURPOSE;
824
1006
  var init_admin_ai_auth = __esm({
825
1007
  "src/admin-ai-auth.ts"() {
826
1008
  "use strict";
827
1009
  init_cjs_shims();
828
- import_node_fs8 = require("fs");
829
- import_node_path5 = require("path");
830
- import_node_process7 = __toESM(require("process"), 1);
1010
+ import_node_path6 = require("path");
1011
+ import_node_process11 = __toESM(require("process"), 1);
831
1012
  import_db2 = require("@odla-ai/db");
832
1013
  init_local();
833
1014
  init_handshake_approval();
834
1015
  init_token();
1016
+ init_auth_guidance();
1017
+ init_device_session_cache();
1018
+ init_device_session();
1019
+ init_machine_identity();
1020
+ init_odla_home();
835
1021
  SCOPE_PURPOSE = {
836
1022
  "platform:status:read": "read the platform fleet health and deployment snapshot",
837
1023
  "app:config:read": "compare checked-in intent with an exact-id app Registry configuration",
@@ -1047,7 +1233,7 @@ var init_admin_ai_usage = __esm({
1047
1233
 
1048
1234
  // src/admin-ai.ts
1049
1235
  async function adminAi(options) {
1050
- const platform = platformAudience(options.platform ?? import_node_process8.default.env.ODLA_PLATFORM ?? "https://odla.ai");
1236
+ const platform = platformAudience(options.platform ?? import_node_process12.default.env.ODLA_PLATFORM ?? "https://odla.ai");
1051
1237
  const doFetch = options.fetch ?? fetch;
1052
1238
  const out = options.stdout ?? console;
1053
1239
  const usageQuery = options.action === "usage" ? adminAiUsageQuery(options) : void 0;
@@ -1225,12 +1411,12 @@ function apiError3(action2, status, body) {
1225
1411
  function isRecord3(value2) {
1226
1412
  return Boolean(value2) && typeof value2 === "object" && !Array.isArray(value2);
1227
1413
  }
1228
- var import_node_process8;
1414
+ var import_node_process12;
1229
1415
  var init_admin_ai = __esm({
1230
1416
  "src/admin-ai.ts"() {
1231
1417
  "use strict";
1232
1418
  init_cjs_shims();
1233
- import_node_process8 = __toESM(require("process"), 1);
1419
+ import_node_process12 = __toESM(require("process"), 1);
1234
1420
  init_token();
1235
1421
  init_secret_input();
1236
1422
  init_admin_ai_auth();
@@ -1812,12 +1998,12 @@ var init_monitoring_validation = __esm({
1812
1998
 
1813
1999
  // src/config.ts
1814
2000
  async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1815
- const resolved = (0, import_node_path6.resolve)(configPath);
2001
+ const resolved = (0, import_node_path7.resolve)(configPath);
1816
2002
  if (!(0, import_node_fs9.existsSync)(resolved)) {
1817
2003
  throw new Error(`config not found: ${configPath}. Run "odla-ai init" first or pass --config.`);
1818
2004
  }
1819
2005
  const raw = await loadConfigModule(resolved);
1820
- const rootDir = (0, import_node_path6.dirname)(resolved);
2006
+ const rootDir = (0, import_node_path7.dirname)(resolved);
1821
2007
  validateRawConfig(raw, resolved);
1822
2008
  const platformUrl = trimSlash(process.env.ODLA_PLATFORM_URL || raw.platformUrl || DEFAULT_PLATFORM);
1823
2009
  const dbEndpoint = trimSlash(process.env.ODLA_DB_ENDPOINT || raw.dbEndpoint || platformUrl);
@@ -1827,11 +2013,14 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1827
2013
  validateCalendarConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1828
2014
  validateMonitoringConfig(raw, unique3([...envs, ...options.additionalEnvs ?? []]), services, resolved);
1829
2015
  const local = {
1830
- tokenFile: (0, import_node_path6.resolve)(rootDir, raw.local?.tokenFile ?? ".odla/dev-token.json"),
1831
- credentialsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.credentialsFile ?? ".odla/credentials.local.json"),
1832
- devVarsFile: (0, import_node_path6.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
2016
+ tokenFile: raw.local?.tokenFile ? (0, import_node_path7.resolve)(rootDir, raw.local.tokenFile) : appTokenFile(raw.app.id),
2017
+ credentialsFile: raw.local?.credentialsFile ? (0, import_node_path7.resolve)(rootDir, raw.local.credentialsFile) : appCredentialsFile(raw.app.id),
2018
+ devVarsFile: (0, import_node_path7.resolve)(rootDir, raw.local?.devVarsFile ?? ".dev.vars"),
1833
2019
  gitignore: raw.local?.gitignore ?? true
1834
2020
  };
2021
+ adoptRepoLocalCache((0, import_node_path7.resolve)(rootDir, ".odla/dev-token.json"), local.tokenFile, stderr);
2022
+ adoptRepoLocalCache((0, import_node_path7.resolve)(rootDir, ".odla/credentials.local.json"), local.credentialsFile, stderr);
2023
+ (0, import_node_fs9.rmSync)((0, import_node_path7.resolve)(rootDir, ".odla/handshake.local.json"), { force: true });
1835
2024
  return {
1836
2025
  ...raw,
1837
2026
  configPath: resolved,
@@ -1846,7 +2035,7 @@ async function loadProjectConfig(configPath = "odla.config.mjs", options = {}) {
1846
2035
  async function resolveDataExport(cfg, value2, names) {
1847
2036
  if (value2 === void 0 || value2 === null || value2 === false) return void 0;
1848
2037
  if (typeof value2 !== "string") return value2;
1849
- const target = (0, import_node_path6.isAbsolute)(value2) ? value2 : (0, import_node_path6.resolve)(cfg.rootDir, value2);
2038
+ const target = (0, import_node_path7.isAbsolute)(value2) ? value2 : (0, import_node_path7.resolve)(cfg.rootDir, value2);
1850
2039
  if (target.endsWith(".json")) {
1851
2040
  return JSON.parse((0, import_node_fs9.readFileSync)(target, "utf8"));
1852
2041
  }
@@ -1937,37 +2126,42 @@ function trimSlash(value2) {
1937
2126
  function unique3(values) {
1938
2127
  return [...new Set(values.filter(Boolean))];
1939
2128
  }
1940
- var import_node_fs9, import_node_path6, import_node_url, import_apps, DEFAULT_PLATFORM, DEFAULT_ENVS, DEFAULT_SERVICES, configImportSerial, GOOGLE_CALENDAR_EVENTS_SCOPE;
2129
+ var import_node_fs9, import_node_path7, import_node_url, import_apps, DEFAULT_PLATFORM, DEFAULT_ENVS, DEFAULT_SERVICES, configImportSerial, GOOGLE_CALENDAR_EVENTS_SCOPE, stderr;
1941
2130
  var init_config = __esm({
1942
2131
  "src/config.ts"() {
1943
2132
  "use strict";
1944
2133
  init_cjs_shims();
1945
2134
  import_node_fs9 = require("fs");
1946
- import_node_path6 = require("path");
2135
+ import_node_path7 = require("path");
1947
2136
  import_node_url = require("url");
1948
2137
  import_apps = require("@odla-ai/apps");
1949
2138
  init_ai_config_validation();
1950
2139
  init_calendar_config();
1951
2140
  init_integration_validation();
1952
2141
  init_monitoring_validation();
2142
+ init_odla_home();
1953
2143
  init_calendar_config();
1954
2144
  DEFAULT_PLATFORM = "https://odla.ai";
1955
2145
  DEFAULT_ENVS = ["dev"];
1956
2146
  DEFAULT_SERVICES = ["db", "ai"];
1957
2147
  configImportSerial = 0;
1958
2148
  GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events";
2149
+ stderr = { error: (message2) => {
2150
+ process.stderr.write(`${message2}
2151
+ `);
2152
+ } };
1959
2153
  }
1960
2154
  });
1961
2155
 
1962
2156
  // src/operator-profiles.ts
1963
2157
  function operatorProfileFile() {
1964
- return (0, import_node_path7.resolve)(
1965
- clean(import_node_process9.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".odla", "contexts.json")
2158
+ return (0, import_node_path8.resolve)(
2159
+ clean(import_node_process13.default.env.ODLA_CONTEXT_FILE) ?? (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".odla", "contexts.json")
1966
2160
  );
1967
2161
  }
1968
2162
  function resolveOperatorProfile(parsed) {
1969
2163
  const fromFlag = clean(stringOpt(parsed.options.context));
1970
- const fromEnvironment = clean(import_node_process9.default.env.ODLA_CONTEXT);
2164
+ const fromEnvironment = clean(import_node_process13.default.env.ODLA_CONTEXT);
1971
2165
  const name = fromFlag ?? fromEnvironment ?? null;
1972
2166
  const file = operatorProfileFile();
1973
2167
  if (!name) {
@@ -2007,10 +2201,10 @@ function removeOperatorProfile(name, file = operatorProfileFile()) {
2007
2201
  return true;
2008
2202
  }
2009
2203
  function operatorCredentialFiles(selection) {
2010
- const base = selection.name ? (0, import_node_path7.join)((0, import_node_path7.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".odla");
2204
+ const base = selection.name ? (0, import_node_path8.join)((0, import_node_path8.dirname)(selection.file), "profiles", selection.name) : (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".odla");
2011
2205
  return {
2012
- developer: (0, import_node_path7.join)(base, "dev-token.json"),
2013
- scoped: (0, import_node_path7.join)(base, "admin-token.local.json")
2206
+ developer: (0, import_node_path8.join)(base, "dev-token.json"),
2207
+ scoped: (0, import_node_path8.join)(base, "admin-token.local.json")
2014
2208
  };
2015
2209
  }
2016
2210
  function assertOperatorName(value2, label) {
@@ -2082,15 +2276,15 @@ function clean(value2) {
2082
2276
  const normalized = value2?.trim();
2083
2277
  return normalized || void 0;
2084
2278
  }
2085
- var import_node_fs10, import_node_os2, import_node_path7, import_node_process9;
2279
+ var import_node_fs10, import_node_os3, import_node_path8, import_node_process13;
2086
2280
  var init_operator_profiles = __esm({
2087
2281
  "src/operator-profiles.ts"() {
2088
2282
  "use strict";
2089
2283
  init_cjs_shims();
2090
2284
  import_node_fs10 = require("fs");
2091
- import_node_os2 = require("os");
2092
- import_node_path7 = require("path");
2093
- import_node_process9 = __toESM(require("process"), 1);
2285
+ import_node_os3 = require("os");
2286
+ import_node_path8 = require("path");
2287
+ import_node_process13 = __toESM(require("process"), 1);
2094
2288
  init_argv();
2095
2289
  init_local();
2096
2290
  init_token();
@@ -2101,7 +2295,7 @@ var init_operator_profiles = __esm({
2101
2295
  async function resolveOperatorContext(parsed, options = {}) {
2102
2296
  const profile = resolveOperatorProfile(parsed);
2103
2297
  const configArgument = stringOpt(parsed.options.config) ?? "odla.config.mjs";
2104
- const configPath = (0, import_node_path8.resolve)(configArgument);
2298
+ const configPath = (0, import_node_path9.resolve)(configArgument);
2105
2299
  const explicitConfig = parsed.options.config !== void 0;
2106
2300
  const hasConfig = (0, import_node_fs11.existsSync)(configPath);
2107
2301
  if (!hasConfig && (!options.allowMissingConfig || explicitConfig)) {
@@ -2109,13 +2303,13 @@ async function resolveOperatorContext(parsed, options = {}) {
2109
2303
  }
2110
2304
  const loaded = hasConfig ? await loadProjectConfig(configArgument) : void 0;
2111
2305
  const platformFlag = clean2(stringOpt(parsed.options.platform));
2112
- const platformEnvironment = clean2(import_node_process10.default.env.ODLA_PLATFORM_URL);
2306
+ const platformEnvironment = clean2(import_node_process14.default.env.ODLA_PLATFORM_URL);
2113
2307
  const platformValue = platformAudience(
2114
2308
  platformFlag ?? platformEnvironment ?? profile.value?.platform ?? loaded?.platformUrl ?? DEFAULT_PLATFORM2
2115
2309
  );
2116
2310
  const platformSource = platformFlag ? "flag" : platformEnvironment ? "environment" : profile.value ? "profile" : loaded ? "config" : "default";
2117
2311
  const appFlag = clean2(stringOpt(parsed.options.app));
2118
- const appEnvironment = clean2(import_node_process10.default.env.ODLA_APP_ID);
2312
+ const appEnvironment = clean2(import_node_process14.default.env.ODLA_APP_ID);
2119
2313
  const appValue = appFlag ?? appEnvironment ?? profile.value?.app ?? loaded?.app.id ?? null;
2120
2314
  const appSource = appFlag ? "flag" : appEnvironment ? "environment" : profile.value?.app ? "profile" : loaded ? "config" : "unresolved";
2121
2315
  if (appValue) {
@@ -2129,16 +2323,16 @@ async function resolveOperatorContext(parsed, options = {}) {
2129
2323
  );
2130
2324
  }
2131
2325
  const envFlag = clean2(stringOpt(parsed.options.env));
2132
- const envEnvironment = clean2(import_node_process10.default.env.ODLA_ENV);
2326
+ const envEnvironment = clean2(import_node_process14.default.env.ODLA_ENV);
2133
2327
  const environmentValue = envFlag ?? envEnvironment ?? profile.value?.environment ?? options.defaultEnvironment ?? null;
2134
2328
  const environmentSource = envFlag ? "flag" : envEnvironment ? "environment" : profile.value?.environment ? "profile" : options.defaultEnvironment ? "default" : "unresolved";
2135
2329
  if (environmentValue) {
2136
2330
  assertOperatorName(environmentValue, "environment");
2137
2331
  }
2138
- const rootDir = loaded?.rootDir ?? import_node_process10.default.cwd();
2332
+ const rootDir = loaded?.rootDir ?? import_node_process14.default.cwd();
2139
2333
  const profileCredentials = operatorCredentialFiles(profile);
2140
- const tokenFile = clean2(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process10.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
2141
- const scopedTokenFile = clean2(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path8.resolve)(import_node_process10.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path8.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
2334
+ const tokenFile = clean2(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) ? (0, import_node_path9.resolve)(import_node_process14.default.env.ODLA_DEV_TOKEN_FILE) : profile.name ? profileCredentials.developer : loaded?.local.tokenFile ?? profileCredentials.developer;
2335
+ const scopedTokenFile2 = clean2(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) ? (0, import_node_path9.resolve)(import_node_process14.default.env.ODLA_ADMIN_TOKEN_FILE) : profile.name ? profileCredentials.scoped : loaded ? (0, import_node_path9.join)(loaded.rootDir, ".odla", "admin-token.local.json") : profileCredentials.scoped;
2142
2336
  const cfg = loaded ? {
2143
2337
  ...loaded,
2144
2338
  platformUrl: platformValue,
@@ -2160,8 +2354,8 @@ async function resolveOperatorContext(parsed, options = {}) {
2160
2354
  services: [],
2161
2355
  local: {
2162
2356
  tokenFile,
2163
- credentialsFile: (0, import_node_path8.join)(rootDir, ".odla", "credentials.local.json"),
2164
- devVarsFile: (0, import_node_path8.join)(rootDir, ".dev.vars"),
2357
+ credentialsFile: (0, import_node_path9.join)(rootDir, ".odla", "credentials.local.json"),
2358
+ devVarsFile: (0, import_node_path9.join)(rootDir, ".dev.vars"),
2165
2359
  gitignore: true
2166
2360
  }
2167
2361
  };
@@ -2185,7 +2379,7 @@ async function resolveOperatorContext(parsed, options = {}) {
2185
2379
  },
2186
2380
  credentials: {
2187
2381
  developerTokenFile: tokenFile,
2188
- scopedTokenFile
2382
+ scopedTokenFile: scopedTokenFile2
2189
2383
  }
2190
2384
  };
2191
2385
  }
@@ -2193,14 +2387,14 @@ function clean2(value2) {
2193
2387
  const normalized = value2?.trim();
2194
2388
  return normalized || void 0;
2195
2389
  }
2196
- var import_node_fs11, import_node_path8, import_node_process10, DEFAULT_PLATFORM2;
2390
+ var import_node_fs11, import_node_path9, import_node_process14, DEFAULT_PLATFORM2;
2197
2391
  var init_operator_context = __esm({
2198
2392
  "src/operator-context.ts"() {
2199
2393
  "use strict";
2200
2394
  init_cjs_shims();
2201
2395
  import_node_fs11 = require("fs");
2202
- import_node_path8 = require("path");
2203
- import_node_process10 = __toESM(require("process"), 1);
2396
+ import_node_path9 = require("path");
2397
+ import_node_process14 = __toESM(require("process"), 1);
2204
2398
  init_argv();
2205
2399
  init_config();
2206
2400
  init_operator_profiles();
@@ -2442,6 +2636,7 @@ async function whoamiCommand(parsed, deps = {}) {
2442
2636
  } else {
2443
2637
  out.log("projects: (none \u2014 every pm and discuss call will be refused)");
2444
2638
  }
2639
+ printMachineBlock(cfg.platformUrl, out);
2445
2640
  if (!identity.admin) {
2446
2641
  if (identity.scopes.includes("platform:runbook:write")) {
2447
2642
  out.log("\nThis exact scope can read and edit all platform runbook content.");
@@ -2452,6 +2647,21 @@ async function whoamiCommand(parsed, deps = {}) {
2452
2647
  }
2453
2648
  }
2454
2649
  }
2650
+ function printMachineBlock(platformUrl, out) {
2651
+ const state2 = machineAuthState(platformAudience(platformUrl));
2652
+ if (!state2.enrolled) {
2653
+ out.log("\nmachine: not enrolled \u2014 every privileged command needs its own browser approval.");
2654
+ out.log(` End that with:
2655
+ ${ENROL_EVERYTHING}`);
2656
+ return;
2657
+ }
2658
+ const reach = state2.appIds?.includes("*") ? "every app you own" : state2.appIds?.join(", ");
2659
+ out.log(`
2660
+ machine: enrolled${state2.deviceName ? ` as "${state2.deviceName}"` : ""}${reach ? ` for ${reach}` : ""}`);
2661
+ if (state2.scopes?.length) out.log(` carrying ${state2.scopes.join(", ")}`);
2662
+ const lapse = lapseNotice(state2);
2663
+ if (lapse) out.log(` ${lapse}`);
2664
+ }
2455
2665
  var text2;
2456
2666
  var init_whoami_command = __esm({
2457
2667
  "src/whoami-command.ts"() {
@@ -2460,6 +2670,8 @@ var init_whoami_command = __esm({
2460
2670
  init_argv();
2461
2671
  init_operator_context();
2462
2672
  init_token();
2673
+ init_auth_guidance();
2674
+ init_token();
2463
2675
  text2 = (value2) => typeof value2 === "string" && value2.trim() ? value2.trim() : null;
2464
2676
  }
2465
2677
  });
@@ -2487,7 +2699,7 @@ async function authCommand(parsed, deps = {}) {
2487
2699
  const { cfg } = context;
2488
2700
  const out = deps.stdout ?? console;
2489
2701
  const doFetch = deps.fetch ?? fetch;
2490
- const email = stringOpt(parsed.options.email) ?? import_node_process11.default.env.ODLA_USER_EMAIL?.trim();
2702
+ const email = stringOpt(parsed.options.email) ?? import_node_process15.default.env.ODLA_USER_EMAIL?.trim();
2491
2703
  if (!email) {
2492
2704
  throw new Error(
2493
2705
  "auth login requires --email <odla-account> or ODLA_USER_EMAIL; confirm the signed-in odla email instead of using git or GitHub identity"
@@ -2521,12 +2733,12 @@ async function authCommand(parsed, deps = {}) {
2521
2733
  out.log(`Authorized ${identity.displayName}${handle} for ${cfg.app.id}.`);
2522
2734
  out.log(`odla account: ${identity.email ?? "not returned"}`);
2523
2735
  }
2524
- var import_node_process11;
2736
+ var import_node_process15;
2525
2737
  var init_auth_command = __esm({
2526
2738
  "src/auth-command.ts"() {
2527
2739
  "use strict";
2528
2740
  init_cjs_shims();
2529
- import_node_process11 = __toESM(require("process"), 1);
2741
+ import_node_process15 = __toESM(require("process"), 1);
2530
2742
  init_argv();
2531
2743
  init_operator_context();
2532
2744
  init_token();
@@ -3043,15 +3255,15 @@ var init_brand_design_unpack = __esm({
3043
3255
 
3044
3256
  // src/brand-command.ts
3045
3257
  async function readBundle(source, deps) {
3046
- if (source !== "-") return (0, import_promises.readFile)((0, import_node_path9.resolve)(source), "utf8");
3258
+ if (source !== "-") return (0, import_promises.readFile)((0, import_node_path10.resolve)(source), "utf8");
3047
3259
  const readStdin = deps.readStdin;
3048
3260
  if (!readStdin) throw new Error("reading a bundle from stdin is not supported here");
3049
3261
  return readStdin();
3050
3262
  }
3051
3263
  async function writeAll(result, outDir) {
3052
3264
  for (const file of result.files) {
3053
- const target = (0, import_node_path9.resolve)(outDir, file.path);
3054
- await (0, import_promises.mkdir)((0, import_node_path9.dirname)(target), { recursive: true });
3265
+ const target = (0, import_node_path10.resolve)(outDir, file.path);
3266
+ await (0, import_promises.mkdir)((0, import_node_path10.dirname)(target), { recursive: true });
3055
3267
  await (0, import_promises.writeFile)(target, file.bytes);
3056
3268
  }
3057
3269
  }
@@ -3059,7 +3271,7 @@ async function designUnpack(parsed, deps) {
3059
3271
  assertArgs(parsed, ["out", "json"], 4);
3060
3272
  const source = parsed.positionals[3];
3061
3273
  if (!source) throw new Error(USAGE);
3062
- const outDir = (0, import_node_path9.resolve)(stringOpt(parsed.options.out) ?? "design");
3274
+ const outDir = (0, import_node_path10.resolve)(stringOpt(parsed.options.out) ?? "design");
3063
3275
  const result = unpackDesign(await readBundle(source, deps));
3064
3276
  await writeAll(result, outDir);
3065
3277
  const out = deps.stdout ?? console;
@@ -3085,13 +3297,13 @@ async function brandCommand(parsed, deps) {
3085
3297
  }
3086
3298
  throw new Error(USAGE);
3087
3299
  }
3088
- var import_promises, import_node_path9, USAGE;
3300
+ var import_promises, import_node_path10, USAGE;
3089
3301
  var init_brand_command = __esm({
3090
3302
  "src/brand-command.ts"() {
3091
3303
  "use strict";
3092
3304
  init_cjs_shims();
3093
3305
  import_promises = require("fs/promises");
3094
- import_node_path9 = require("path");
3306
+ import_node_path10 = require("path");
3095
3307
  init_argv();
3096
3308
  init_brand_design_unpack();
3097
3309
  USAGE = "usage: odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]";
@@ -4139,7 +4351,7 @@ async function operationClient(cfg, options, purpose) {
4139
4351
  platform: cfg.platformUrl,
4140
4352
  scope: "app:config:write",
4141
4353
  token: options.token,
4142
- tokenFile: (0, import_node_path10.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4354
+ tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4143
4355
  rootDir: cfg.rootDir,
4144
4356
  email: options.email,
4145
4357
  open: options.open,
@@ -4191,13 +4403,13 @@ function normalizeRequestError(error) {
4191
4403
  function record4(value2) {
4192
4404
  return !!value2 && typeof value2 === "object" && !Array.isArray(value2);
4193
4405
  }
4194
- var import_apps6, import_node_path10, IDEMPOTENCY_KEY, DEFAULT_WAIT_SECONDS, DEFAULT_INTERVAL_SECONDS;
4406
+ var import_apps6, import_node_path11, IDEMPOTENCY_KEY, DEFAULT_WAIT_SECONDS, DEFAULT_INTERVAL_SECONDS;
4195
4407
  var init_config_operation_command = __esm({
4196
4408
  "src/config-operation-command.ts"() {
4197
4409
  "use strict";
4198
4410
  init_cjs_shims();
4199
4411
  import_apps6 = require("@odla-ai/apps");
4200
- import_node_path10 = require("path");
4412
+ import_node_path11 = require("path");
4201
4413
  init_admin_ai_auth();
4202
4414
  init_version();
4203
4415
  init_config();
@@ -4520,7 +4732,7 @@ async function inspectConfig(options) {
4520
4732
  platform: cfg.platformUrl,
4521
4733
  scope: "app:config:read",
4522
4734
  token: options.token,
4523
- tokenFile: (0, import_node_path11.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4735
+ tokenFile: (0, import_node_path12.join)(cfg.rootDir, ".odla", "admin-token.local.json"),
4524
4736
  rootDir: cfg.rootDir,
4525
4737
  email: options.email,
4526
4738
  open: options.open,
@@ -4649,13 +4861,13 @@ function studioSettingsUrl(reconciliation) {
4649
4861
  function quoteArg2(value2) {
4650
4862
  return `'${value2.replace(/'/g, `'\\''`)}'`;
4651
4863
  }
4652
- var import_apps8, import_node_path11;
4864
+ var import_apps8, import_node_path12;
4653
4865
  var init_config_reconcile_command = __esm({
4654
4866
  "src/config-reconcile-command.ts"() {
4655
4867
  "use strict";
4656
4868
  init_cjs_shims();
4657
4869
  import_apps8 = require("@odla-ai/apps");
4658
- import_node_path11 = require("path");
4870
+ import_node_path12 = require("path");
4659
4871
  init_admin_ai_auth();
4660
4872
  init_config();
4661
4873
  init_config_reconcile_digest();
@@ -4668,7 +4880,7 @@ var init_config_reconcile_command = __esm({
4668
4880
  // src/wrangler.ts
4669
4881
  function findWranglerConfig(rootDir) {
4670
4882
  for (const name of WRANGLER_CONFIG_FILES) {
4671
- const path = (0, import_node_path12.join)(rootDir, name);
4883
+ const path = (0, import_node_path13.join)(rootDir, name);
4672
4884
  if ((0, import_node_fs14.existsSync)(path)) return path;
4673
4885
  }
4674
4886
  return null;
@@ -4780,22 +4992,22 @@ function wranglerBulkSecrets(run, opts) {
4780
4992
  ];
4781
4993
  return run("npx", args, { input: JSON.stringify(opts.secrets), cwd: opts.cwd });
4782
4994
  }
4783
- var import_node_child_process2, import_node_fs14, import_node_path12, defaultRunner, WRANGLER_CONFIG_FILES;
4995
+ var import_node_child_process2, import_node_fs14, import_node_path13, defaultRunner, WRANGLER_CONFIG_FILES;
4784
4996
  var init_wrangler = __esm({
4785
4997
  "src/wrangler.ts"() {
4786
4998
  "use strict";
4787
4999
  init_cjs_shims();
4788
5000
  import_node_child_process2 = require("child_process");
4789
5001
  import_node_fs14 = require("fs");
4790
- import_node_path12 = require("path");
5002
+ import_node_path13 = require("path");
4791
5003
  defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
4792
5004
  const child = (0, import_node_child_process2.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
4793
5005
  let stdout = "";
4794
- let stderr = "";
5006
+ let stderr2 = "";
4795
5007
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
4796
- child.stderr.on("data", (chunk) => stderr += chunk.toString());
5008
+ child.stderr.on("data", (chunk) => stderr2 += chunk.toString());
4797
5009
  child.on("error", reject);
4798
- child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr }));
5010
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout, stderr: stderr2 }));
4799
5011
  child.stdin.end(opts?.input ?? "");
4800
5012
  });
4801
5013
  WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
@@ -4839,21 +5051,21 @@ function wranglerWarnings(rootDir) {
4839
5051
  const blocks = [{ label: "", block: config }];
4840
5052
  const envs = config.env;
4841
5053
  if (envs && typeof envs === "object") {
4842
- for (const [name, block] of Object.entries(envs)) {
4843
- if (block && typeof block === "object") blocks.push({ label: `env.${name}.`, block });
5054
+ for (const [name, block2] of Object.entries(envs)) {
5055
+ if (block2 && typeof block2 === "object") blocks.push({ label: `env.${name}.`, block: block2 });
4844
5056
  }
4845
5057
  }
4846
- for (const { label, block } of blocks) {
4847
- const assets = block.assets;
5058
+ for (const { label, block: block2 } of blocks) {
5059
+ const assets = block2.assets;
4848
5060
  if (assets?.directory) {
4849
- const dir = (0, import_node_path13.resolve)(rootDir, assets.directory);
4850
- if (dir === (0, import_node_path13.resolve)(rootDir)) {
5061
+ const dir = (0, import_node_path14.resolve)(rootDir, assets.directory);
5062
+ if (dir === (0, import_node_path14.resolve)(rootDir)) {
4851
5063
  warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
4852
- } else if ((0, import_node_fs15.existsSync)((0, import_node_path13.join)(dir, "node_modules"))) {
5064
+ } else if ((0, import_node_fs15.existsSync)((0, import_node_path14.join)(dir, "node_modules"))) {
4853
5065
  warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
4854
5066
  }
4855
5067
  }
4856
- const vars = block.vars;
5068
+ const vars = block2.vars;
4857
5069
  if (vars && typeof vars === "object") {
4858
5070
  for (const [name, value2] of Object.entries(vars)) {
4859
5071
  if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value2 === "string" && looksSecret(value2)) {
@@ -4884,7 +5096,7 @@ function o11yProjectWarnings(rootDir) {
4884
5096
  warnings.push("cannot verify o11y Worker instrumentation \u2014 add a parseable wrangler.jsonc/json config");
4885
5097
  return warnings;
4886
5098
  }
4887
- const main = typeof config.main === "string" ? (0, import_node_path13.resolve)(rootDir, config.main) : null;
5099
+ const main = typeof config.main === "string" ? (0, import_node_path14.resolve)(rootDir, config.main) : null;
4888
5100
  if (!main || !(0, import_node_fs15.existsSync)(main)) {
4889
5101
  warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
4890
5102
  } else {
@@ -4914,19 +5126,19 @@ function calendarProjectWarnings(rootDir) {
4914
5126
  }
4915
5127
  function readPackageJson(rootDir) {
4916
5128
  try {
4917
- return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path13.join)(rootDir, "package.json"), "utf8"));
5129
+ return JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path14.join)(rootDir, "package.json"), "utf8"));
4918
5130
  } catch {
4919
5131
  return null;
4920
5132
  }
4921
5133
  }
4922
- var import_node_child_process3, import_node_fs15, import_node_path13, defaultExec;
5134
+ var import_node_child_process3, import_node_fs15, import_node_path14, defaultExec;
4923
5135
  var init_doctor_checks = __esm({
4924
5136
  "src/doctor-checks.ts"() {
4925
5137
  "use strict";
4926
5138
  init_cjs_shims();
4927
5139
  import_node_child_process3 = require("child_process");
4928
5140
  import_node_fs15 = require("fs");
4929
- import_node_path13 = require("path");
5141
+ import_node_path14 = require("path");
4930
5142
  init_redact();
4931
5143
  init_local();
4932
5144
  init_wrangler();
@@ -5263,8 +5475,8 @@ var init_harness_options = __esm({
5263
5475
  // src/init.ts
5264
5476
  function initProject(options) {
5265
5477
  const out = options.stdout ?? console;
5266
- const rootDir = (0, import_node_path14.resolve)(options.rootDir ?? process.cwd());
5267
- const configPath = (0, import_node_path14.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
5478
+ const rootDir = (0, import_node_path15.resolve)(options.rootDir ?? process.cwd());
5479
+ const configPath = (0, import_node_path15.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
5268
5480
  if ((0, import_node_fs16.existsSync)(configPath) && !options.force) {
5269
5481
  throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
5270
5482
  }
@@ -5281,12 +5493,12 @@ function initProject(options) {
5281
5493
  }
5282
5494
  }
5283
5495
  const aiProvider = options.aiProvider;
5284
- (0, import_node_fs16.mkdirSync)((0, import_node_path14.dirname)(configPath), { recursive: true });
5285
- (0, import_node_fs16.mkdirSync)((0, import_node_path14.resolve)(rootDir, "src/odla"), { recursive: true });
5286
- (0, import_node_fs16.mkdirSync)((0, import_node_path14.resolve)(rootDir, ".odla"), { recursive: true });
5496
+ (0, import_node_fs16.mkdirSync)((0, import_node_path15.dirname)(configPath), { recursive: true });
5497
+ (0, import_node_fs16.mkdirSync)((0, import_node_path15.resolve)(rootDir, "src/odla"), { recursive: true });
5498
+ (0, import_node_fs16.mkdirSync)((0, import_node_path15.resolve)(rootDir, ".odla"), { recursive: true });
5287
5499
  (0, import_node_fs16.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
5288
- writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
5289
- writeIfMissing((0, import_node_path14.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
5500
+ writeIfMissing((0, import_node_path15.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
5501
+ writeIfMissing((0, import_node_path15.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
5290
5502
  ensureGitignore(rootDir);
5291
5503
  out.log(`created ${relativeDisplay(configPath, rootDir)}`);
5292
5504
  out.log("created src/odla/schema.mjs and src/odla/rules.mjs");
@@ -5353,8 +5565,10 @@ ${calendar}
5353
5565
  // prod: "https://example.com",
5354
5566
  },
5355
5567
  local: {
5356
- tokenFile: ".odla/dev-token.json",
5357
- credentialsFile: ".odla/credentials.local.json",
5568
+ // Credentials live in ~/.odla, per machine, so every worktree of this app
5569
+ // shares one approval instead of asking for its own. Pinning tokenFile or
5570
+ // credentialsFile here still works and still overrides that \u2014 it just puts
5571
+ // this checkout back on its own island.
5358
5572
  devVarsFile: ".dev.vars",
5359
5573
  },
5360
5574
  };
@@ -5397,13 +5611,13 @@ function defaultKeyEnv(provider) {
5397
5611
  function relativeDisplay(path, rootDir) {
5398
5612
  return path.startsWith(rootDir) ? path.slice(rootDir.length + 1) : path;
5399
5613
  }
5400
- var import_node_fs16, import_node_path14, import_apps9;
5614
+ var import_node_fs16, import_node_path15, import_apps9;
5401
5615
  var init_init = __esm({
5402
5616
  "src/init.ts"() {
5403
5617
  "use strict";
5404
5618
  init_cjs_shims();
5405
5619
  import_node_fs16 = require("fs");
5406
- import_node_path14 = require("path");
5620
+ import_node_path15 = require("path");
5407
5621
  import_apps9 = require("@odla-ai/apps");
5408
5622
  init_local();
5409
5623
  }
@@ -5736,8 +5950,8 @@ function installSkill(options = {}) {
5736
5950
  const files = listFiles(sourceDir);
5737
5951
  if (files.length === 0) throw new Error(`no bundled skills found at ${sourceDir}`);
5738
5952
  const harnesses = normalizeHarnesses(options.harnesses, options.global === true);
5739
- const root = (0, import_node_path15.resolve)(options.dir ?? process.cwd());
5740
- const home = (0, import_node_path15.resolve)(options.homeDir ?? (0, import_node_os3.homedir)());
5953
+ const root = (0, import_node_path16.resolve)(options.dir ?? process.cwd());
5954
+ const home = (0, import_node_path16.resolve)(options.homeDir ?? (0, import_node_os4.homedir)());
5741
5955
  const plans = /* @__PURE__ */ new Map();
5742
5956
  const targets = /* @__PURE__ */ new Map();
5743
5957
  const rememberTarget = (harness, target) => {
@@ -5751,48 +5965,48 @@ function installSkill(options = {}) {
5751
5965
  plans.set(target, { target, content: content2, boundary, managedMerge });
5752
5966
  };
5753
5967
  const planSkillTree = (targetDir2, boundary = root) => {
5754
- for (const rel of files) plan((0, import_node_path15.join)(targetDir2, rel), (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(sourceDir, rel), "utf8"), false, boundary);
5968
+ for (const rel of files) plan((0, import_node_path16.join)(targetDir2, rel), (0, import_node_fs17.readFileSync)((0, import_node_path16.join)(sourceDir, rel), "utf8"), false, boundary);
5755
5969
  };
5756
5970
  let targetDir;
5757
5971
  if (options.global) {
5758
- const claudeRoot = (0, import_node_path15.join)(home, ".claude", "skills");
5759
- const codexRoot = (0, import_node_path15.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path15.join)(home, ".codex"), "skills");
5972
+ const claudeRoot = (0, import_node_path16.join)(home, ".claude", "skills");
5973
+ const codexRoot = (0, import_node_path16.resolve)(options.codexHomeDir ?? process.env.CODEX_HOME ?? (0, import_node_path16.join)(home, ".codex"), "skills");
5760
5974
  targetDir = harnesses[0] === "codex" ? codexRoot : claudeRoot;
5761
5975
  for (const harness of harnesses) {
5762
5976
  const skillRoot = harness === "claude" ? claudeRoot : codexRoot;
5763
- planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path15.dirname)((0, import_node_path15.dirname)(codexRoot)));
5977
+ planSkillTree(skillRoot, harness === "claude" ? home : (0, import_node_path16.dirname)((0, import_node_path16.dirname)(codexRoot)));
5764
5978
  rememberTarget(harness, skillRoot);
5765
5979
  }
5766
5980
  } else {
5767
- const sharedRoot = (0, import_node_path15.join)(root, ".agents", "skills");
5981
+ const sharedRoot = (0, import_node_path16.join)(root, ".agents", "skills");
5768
5982
  planSkillTree(sharedRoot);
5769
- const claudeRoot = (0, import_node_path15.join)(root, ".claude", "skills");
5983
+ const claudeRoot = (0, import_node_path16.join)(root, ".claude", "skills");
5770
5984
  targetDir = harnesses.includes("claude") ? claudeRoot : sharedRoot;
5771
5985
  for (const harness of harnesses) rememberTarget(harness, sharedRoot);
5772
5986
  if (harnesses.includes("claude")) {
5773
5987
  for (const skill of skillNames(files)) {
5774
- const canonical2 = (0, import_node_fs17.readFileSync)((0, import_node_path15.join)(sourceDir, skill, "SKILL.md"), "utf8");
5775
- plan((0, import_node_path15.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5988
+ const canonical2 = (0, import_node_fs17.readFileSync)((0, import_node_path16.join)(sourceDir, skill, "SKILL.md"), "utf8");
5989
+ plan((0, import_node_path16.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical2));
5776
5990
  }
5777
5991
  rememberTarget("claude", claudeRoot);
5778
5992
  }
5779
5993
  if (harnesses.includes("cursor")) {
5780
- const cursorRule = (0, import_node_path15.join)(root, ".cursor", "rules", "odla.mdc");
5994
+ const cursorRule = (0, import_node_path16.join)(root, ".cursor", "rules", "odla.mdc");
5781
5995
  plan(cursorRule, CURSOR_RULE);
5782
5996
  rememberTarget("cursor", cursorRule);
5783
5997
  }
5784
5998
  if (harnesses.includes("agents")) {
5785
- const agentsFile = (0, import_node_path15.join)(root, "AGENTS.md");
5999
+ const agentsFile = (0, import_node_path16.join)(root, "AGENTS.md");
5786
6000
  plan(agentsFile, managedFileContent(agentsFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5787
6001
  rememberTarget("agents", agentsFile);
5788
6002
  }
5789
6003
  if (harnesses.includes("copilot")) {
5790
- const copilotFile = (0, import_node_path15.join)(root, ".github", "copilot-instructions.md");
6004
+ const copilotFile = (0, import_node_path16.join)(root, ".github", "copilot-instructions.md");
5791
6005
  plan(copilotFile, managedFileContent(copilotFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5792
6006
  rememberTarget("copilot", copilotFile);
5793
6007
  }
5794
6008
  if (harnesses.includes("gemini")) {
5795
- const geminiFile = (0, import_node_path15.join)(root, "GEMINI.md");
6009
+ const geminiFile = (0, import_node_path16.join)(root, "GEMINI.md");
5796
6010
  plan(geminiFile, managedFileContent(geminiFile, PROJECT_INSTRUCTIONS, options.force === true, root), true);
5797
6011
  rememberTarget("gemini", geminiFile);
5798
6012
  }
@@ -5828,7 +6042,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5828
6042
  }
5829
6043
  for (const file of plans.values()) {
5830
6044
  if (!(0, import_node_fs17.existsSync)(file.target) || (0, import_node_fs17.readFileSync)(file.target, "utf8") !== file.content) {
5831
- (0, import_node_fs17.mkdirSync)((0, import_node_path15.dirname)(file.target), { recursive: true });
6045
+ (0, import_node_fs17.mkdirSync)((0, import_node_path16.dirname)(file.target), { recursive: true });
5832
6046
  (0, import_node_fs17.writeFileSync)(file.target, file.content);
5833
6047
  }
5834
6048
  }
@@ -5848,7 +6062,7 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
5848
6062
  };
5849
6063
  }
5850
6064
  function pathsUnder(root, paths) {
5851
- return [...paths].map((path) => (0, import_node_path15.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path15.sep}`) && !(0, import_node_path15.isAbsolute)(path)).sort();
6065
+ return [...paths].map((path) => (0, import_node_path16.relative)(root, path)).filter((path) => path !== ".." && !path.startsWith(`..${import_node_path16.sep}`) && !(0, import_node_path16.isAbsolute)(path)).sort();
5852
6066
  }
5853
6067
  function normalizeHarnesses(values, global) {
5854
6068
  const requested = values?.length ? values : ["claude"];
@@ -5867,10 +6081,10 @@ function normalizeHarnesses(values, global) {
5867
6081
  }
5868
6082
  return expanded;
5869
6083
  }
5870
- function managedFileContent(path, block, force, boundary) {
6084
+ function managedFileContent(path, block2, force, boundary) {
5871
6085
  const symlink = symlinkedComponent(boundary, path);
5872
6086
  if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
5873
- if (!(0, import_node_fs17.existsSync)(path)) return `${block}
6087
+ if (!(0, import_node_fs17.existsSync)(path)) return `${block2}
5874
6088
  `;
5875
6089
  const current = (0, import_node_fs17.readFileSync)(path, "utf8");
5876
6090
  const start = "<!-- odla-ai agent setup:start -->";
@@ -5882,24 +6096,24 @@ function managedFileContent(path, block, force, boundary) {
5882
6096
  }
5883
6097
  if (startAt === -1) {
5884
6098
  const separator = current.length === 0 || current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
5885
- return `${current}${separator}${block}
6099
+ return `${current}${separator}${block2}
5886
6100
  `;
5887
6101
  }
5888
6102
  const afterEnd = endAt + end.length;
5889
6103
  const existing = current.slice(startAt, afterEnd);
5890
- if (existing !== block && !force) {
6104
+ if (existing !== block2 && !force) {
5891
6105
  throw new Error(`odla-managed section modified locally in ${path}; re-run with --force to replace that section`);
5892
6106
  }
5893
- return `${current.slice(0, startAt)}${block}${current.slice(afterEnd)}`;
6107
+ return `${current.slice(0, startAt)}${block2}${current.slice(afterEnd)}`;
5894
6108
  }
5895
6109
  function symlinkedComponent(boundary, target) {
5896
- const rel = (0, import_node_path15.relative)(boundary, target);
5897
- if (rel === ".." || rel.startsWith(`..${import_node_path15.sep}`) || (0, import_node_path15.isAbsolute)(rel)) {
6110
+ const rel = (0, import_node_path16.relative)(boundary, target);
6111
+ if (rel === ".." || rel.startsWith(`..${import_node_path16.sep}`) || (0, import_node_path16.isAbsolute)(rel)) {
5898
6112
  throw new Error(`agent setup target escapes its install root: ${target}`);
5899
6113
  }
5900
6114
  let current = boundary;
5901
- for (const part of rel.split(import_node_path15.sep).filter(Boolean)) {
5902
- current = (0, import_node_path15.join)(current, part);
6115
+ for (const part of rel.split(import_node_path16.sep).filter(Boolean)) {
6116
+ current = (0, import_node_path16.join)(current, part);
5903
6117
  try {
5904
6118
  if ((0, import_node_fs17.lstatSync)(current).isSymbolicLink()) return current;
5905
6119
  } catch (error) {
@@ -5916,22 +6130,22 @@ function listFiles(dir) {
5916
6130
  const results = [];
5917
6131
  const walk = (current) => {
5918
6132
  for (const entry of (0, import_node_fs17.readdirSync)(current, { withFileTypes: true })) {
5919
- const path = (0, import_node_path15.join)(current, entry.name);
6133
+ const path = (0, import_node_path16.join)(current, entry.name);
5920
6134
  if (entry.isDirectory()) walk(path);
5921
- else results.push((0, import_node_path15.relative)(dir, path));
6135
+ else results.push((0, import_node_path16.relative)(dir, path));
5922
6136
  }
5923
6137
  };
5924
6138
  walk(dir);
5925
6139
  return results.sort();
5926
6140
  }
5927
- var import_node_fs17, import_node_os3, import_node_path15, import_node_url2, AGENT_HARNESSES;
6141
+ var import_node_fs17, import_node_os4, import_node_path16, import_node_url2, AGENT_HARNESSES;
5928
6142
  var init_skill = __esm({
5929
6143
  "src/skill.ts"() {
5930
6144
  "use strict";
5931
6145
  init_cjs_shims();
5932
6146
  import_node_fs17 = require("fs");
5933
- import_node_os3 = require("os");
5934
- import_node_path15 = require("path");
6147
+ import_node_os4 = require("os");
6148
+ import_node_path16 = require("path");
5935
6149
  import_node_url2 = require("url");
5936
6150
  init_skill_adapters();
5937
6151
  AGENT_HARNESSES = ["claude", "codex", "cursor", "copilot", "gemini", "agents"];
@@ -6405,7 +6619,7 @@ function allowedWorkspacePath(relativePath) {
6405
6619
  async function gitOutput(cwd, args, maxBytes) {
6406
6620
  const child = (0, import_child_process2.spawn)("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], shell: false });
6407
6621
  const stdout = [];
6408
- const stderr = [];
6622
+ const stderr2 = [];
6409
6623
  let bytes = 0;
6410
6624
  child.stdout.on("data", (chunk) => {
6411
6625
  bytes += chunk.byteLength;
@@ -6413,20 +6627,20 @@ async function gitOutput(cwd, args, maxBytes) {
6413
6627
  else stdout.push(chunk);
6414
6628
  });
6415
6629
  child.stderr.on("data", (chunk) => {
6416
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6630
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6417
6631
  });
6418
6632
  const code = await new Promise((accept, reject) => {
6419
6633
  child.once("error", reject);
6420
6634
  child.once("exit", accept);
6421
6635
  });
6422
6636
  if (bytes > maxBytes) throw new Error(`git output exceeds ${maxBytes} bytes`);
6423
- if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6637
+ if (code !== 0) throw new Error(`git command failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6424
6638
  return Buffer.concat(stdout);
6425
6639
  }
6426
6640
  async function gitBlobs(cwd, entries, maxBytes) {
6427
6641
  const child = (0, import_child_process2.spawn)("git", ["cat-file", "--batch"], { cwd, stdio: ["pipe", "pipe", "pipe"], shell: false });
6428
6642
  const stdout = [];
6429
- const stderr = [];
6643
+ const stderr2 = [];
6430
6644
  let bytes = 0;
6431
6645
  child.stdout.on("data", (chunk) => {
6432
6646
  bytes += chunk.byteLength;
@@ -6434,7 +6648,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
6434
6648
  else stdout.push(chunk);
6435
6649
  });
6436
6650
  child.stderr.on("data", (chunk) => {
6437
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6651
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6438
6652
  });
6439
6653
  child.stdin.end(`${entries.map((entry) => entry.hash).join("\n")}
6440
6654
  `);
@@ -6443,7 +6657,7 @@ async function gitBlobs(cwd, entries, maxBytes) {
6443
6657
  child.once("exit", accept);
6444
6658
  });
6445
6659
  if (bytes > maxBytes + entries.length * 100) throw new Error(`Git tree exceeds ${maxBytes} bytes`);
6446
- if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6660
+ if (code !== 0) throw new Error(`git object read failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6447
6661
  const output = Buffer.concat(stdout);
6448
6662
  const blobs = [];
6449
6663
  let offset = 0;
@@ -6540,7 +6754,7 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
6540
6754
  shell: false
6541
6755
  });
6542
6756
  const stdout = [];
6543
- const stderr = [];
6757
+ const stderr2 = [];
6544
6758
  let outputBytes = 0;
6545
6759
  child.stdout.on("data", (chunk) => {
6546
6760
  outputBytes += chunk.byteLength;
@@ -6548,14 +6762,14 @@ async function gitSourceFiles(sourceDir, maxFiles, maxBytes) {
6548
6762
  else stdout.push(chunk);
6549
6763
  });
6550
6764
  child.stderr.on("data", (chunk) => {
6551
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6765
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6552
6766
  });
6553
6767
  const code = await new Promise((accept, reject) => {
6554
6768
  child.once("error", reject);
6555
6769
  child.once("exit", accept);
6556
6770
  });
6557
6771
  if (outputBytes > 8 * 1024 * 1024) throw new Error("git file inventory exceeds 8 MiB");
6558
- if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6772
+ if (code !== 0) throw new Error(`git file inventory failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6559
6773
  const paths = Buffer.concat(stdout).toString("utf8").split("\0").filter(Boolean).sort();
6560
6774
  if (paths.length > maxFiles) throw new Error(`workspace exceeds ${maxFiles} files`);
6561
6775
  const root = (0, import_path4.resolve)(sourceDir);
@@ -6600,7 +6814,7 @@ async function captureGitDiff(root, maxBytes) {
6600
6814
  "workspace"
6601
6815
  ], { cwd: root, stdio: ["ignore", "pipe", "pipe"], shell: false });
6602
6816
  const stdout = [];
6603
- const stderr = [];
6817
+ const stderr2 = [];
6604
6818
  let bytes = 0;
6605
6819
  child.stdout.on("data", (chunk) => {
6606
6820
  bytes += chunk.byteLength;
@@ -6608,7 +6822,7 @@ async function captureGitDiff(root, maxBytes) {
6608
6822
  else stdout.push(chunk);
6609
6823
  });
6610
6824
  child.stderr.on("data", (chunk) => {
6611
- if (stderr.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr.push(chunk);
6825
+ if (stderr2.reduce((sum, value2) => sum + value2.byteLength, 0) < 16384) stderr2.push(chunk);
6612
6826
  });
6613
6827
  const code = await new Promise((accept, reject) => {
6614
6828
  child.once("error", reject);
@@ -6616,7 +6830,7 @@ async function captureGitDiff(root, maxBytes) {
6616
6830
  });
6617
6831
  if (bytes > maxBytes) throw new Error(`patch exceeds ${maxBytes} bytes`);
6618
6832
  if (code !== 0 && code !== 1) {
6619
- throw new Error(`git diff failed: ${Buffer.concat(stderr).toString("utf8").slice(0, 1e3)}`);
6833
+ throw new Error(`git diff failed: ${Buffer.concat(stderr2).toString("utf8").slice(0, 1e3)}`);
6620
6834
  }
6621
6835
  return Buffer.concat(stdout).toString("utf8").replaceAll("a/baseline/", "a/").replaceAll("a/workspace/", "a/").replaceAll("b/baseline/", "b/").replaceAll("b/workspace/", "b/").replaceAll("--- a/baseline", "--- a").replaceAll("+++ b/workspace", "+++ b");
6622
6836
  }
@@ -7504,11 +7718,11 @@ var init_dist2 = __esm({
7504
7718
  });
7505
7719
 
7506
7720
  // ../graph/dist/code/index.js
7507
- function dirname9(path) {
7721
+ function dirname10(path) {
7508
7722
  const at = path.lastIndexOf("/");
7509
7723
  return at <= 0 ? "." : path.slice(0, at);
7510
7724
  }
7511
- function join13(base, specifier) {
7725
+ function join14(base, specifier) {
7512
7726
  const parts = [];
7513
7727
  const segments = `${base === "." ? "" : `${base}/`}${specifier}`.split("/");
7514
7728
  for (const segment of segments) {
@@ -7520,7 +7734,7 @@ function join13(base, specifier) {
7520
7734
  }
7521
7735
  function resolveImport(fromPath, specifier, known) {
7522
7736
  if (!specifier.startsWith(".")) return null;
7523
- const base = join13(dirname9(fromPath), specifier);
7737
+ const base = join14(dirname10(fromPath), specifier);
7524
7738
  const candidates = [
7525
7739
  base,
7526
7740
  base.replace(/\.js$/, ".ts"),
@@ -8023,13 +8237,13 @@ function gitApply(cwd, patch2, check) {
8023
8237
  stdio: ["pipe", "ignore", "pipe"],
8024
8238
  env: { PATH: process.env.PATH ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }
8025
8239
  });
8026
- let stderr = "";
8240
+ let stderr2 = "";
8027
8241
  child.stderr.setEncoding("utf8");
8028
8242
  child.stderr.on("data", (text3) => {
8029
- if (stderr.length < 4e3) stderr += text3.slice(0, 4e3);
8243
+ if (stderr2.length < 4e3) stderr2 += text3.slice(0, 4e3);
8030
8244
  });
8031
8245
  child.once("error", reject);
8032
- child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr.trim().slice(0, 500)))));
8246
+ child.once("exit", (code) => code === 0 ? accept() : reject(new TypeError(describePatchFailure(patch2, stderr2.trim().slice(0, 500)))));
8033
8247
  child.stdin.end(patch2);
8034
8248
  });
8035
8249
  }
@@ -8140,7 +8354,7 @@ function execute(engine, args, name, recipe2, signal) {
8140
8354
  const started = Date.now();
8141
8355
  const child = (0, import_child_process5.spawn)(engine, args, { shell: false, stdio: ["ignore", "pipe", "pipe"] });
8142
8356
  const stdout = [];
8143
- const stderr = [];
8357
+ const stderr2 = [];
8144
8358
  let bytes = 0;
8145
8359
  let outputLimitExceeded = false;
8146
8360
  let timedOut = false;
@@ -8161,7 +8375,7 @@ function execute(engine, args, name, recipe2, signal) {
8161
8375
  else target.push(chunk);
8162
8376
  };
8163
8377
  child.stdout.on("data", collect(stdout));
8164
- child.stderr.on("data", collect(stderr));
8378
+ child.stderr.on("data", collect(stderr2));
8165
8379
  const abort = () => stop("abort");
8166
8380
  signal?.addEventListener("abort", abort, { once: true });
8167
8381
  if (signal?.aborted) abort();
@@ -8177,7 +8391,7 @@ function execute(engine, args, name, recipe2, signal) {
8177
8391
  accept({
8178
8392
  exitCode: code ?? 1,
8179
8393
  stdout: Buffer.concat(stdout).toString("utf8"),
8180
- stderr: Buffer.concat(stderr).toString("utf8"),
8394
+ stderr: Buffer.concat(stderr2).toString("utf8"),
8181
8395
  durationMs: Date.now() - started,
8182
8396
  outputLimitExceeded,
8183
8397
  timedOut
@@ -8323,11 +8537,11 @@ function checkedResult(result, maximumOutputBytes) {
8323
8537
  }
8324
8538
  function boundedLogs(result, maximum) {
8325
8539
  const stdout = Buffer.from(result.stdout);
8326
- const stderr = Buffer.from(result.stderr);
8540
+ const stderr2 = Buffer.from(result.stderr);
8327
8541
  const first = stdout.subarray(0, maximum);
8328
8542
  return {
8329
8543
  stdout: first.toString("utf8"),
8330
- stderr: stderr.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
8544
+ stderr: stderr2.subarray(0, Math.max(0, maximum - first.byteLength)).toString("utf8")
8331
8545
  };
8332
8546
  }
8333
8547
  function digestPolicy(policy) {
@@ -10461,7 +10675,7 @@ var init_code_runtime_config = __esm({
10461
10675
  // src/code-connect.ts
10462
10676
  async function codeConnect(options) {
10463
10677
  const cwd = options.cwd ?? process.cwd();
10464
- const configPath = (0, import_node_path16.resolve)(cwd, options.configPath);
10678
+ const configPath = (0, import_node_path17.resolve)(cwd, options.configPath);
10465
10679
  const cfg = (0, import_node_fs18.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
10466
10680
  const requestedAppId = options.appId?.trim();
10467
10681
  if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
@@ -10491,7 +10705,7 @@ async function codeConnect(options) {
10491
10705
  const doFetch = options.fetch ?? fetch;
10492
10706
  const engine = await (options.selectEngine ?? selectContainerEngine)(options.engine ?? "auto");
10493
10707
  const hostPlatform = process.platform === "darwin" ? "macos" : "linux";
10494
- const hostName = (options.name ?? (0, import_node_os4.hostname)()).trim();
10708
+ const hostName = (options.name ?? (0, import_node_os5.hostname)()).trim();
10495
10709
  if (!hostName || hostName.length > 120) throw new Error("--name must contain 1 to 120 characters");
10496
10710
  const repository = await inferGitHubRepository(cwd, options.readGitOrigin);
10497
10711
  const localSource = await (options.prepareLocalSource ?? prepareCodeLocalSource)(
@@ -10501,7 +10715,8 @@ async function codeConnect(options) {
10501
10715
  );
10502
10716
  try {
10503
10717
  const descriptor2 = localSource.descriptor;
10504
- const approval = await (options.getToken ?? getScopedPlatformToken)({
10718
+ const device = options.getToken ? null : readDeviceCredential(platform);
10719
+ const authorization = device ? (await mintDeviceSession(platform, device, doFetch)).token : await (options.getToken ?? getScopedPlatformToken)({
10505
10720
  platform,
10506
10721
  scope: "app:code:host:connect",
10507
10722
  email: options.email,
@@ -10515,7 +10730,7 @@ async function codeConnect(options) {
10515
10730
  const target = appId ? { appId } : { repository };
10516
10731
  const response2 = await doFetch(`${platform}/registry/code/hosts/connect`, {
10517
10732
  method: "POST",
10518
- headers: { authorization: `Bearer ${approval}`, "content-type": "application/json" },
10733
+ headers: { authorization: `Bearer ${authorization}`, "content-type": "application/json" },
10519
10734
  body: JSON.stringify({ ...target, env: appEnv, name: hostName, platform: hostPlatform, slots }),
10520
10735
  redirect: "error",
10521
10736
  signal: options.signal
@@ -10528,8 +10743,8 @@ async function codeConnect(options) {
10528
10743
  platform: hostPlatform,
10529
10744
  arch: process.arch,
10530
10745
  engines: [engine],
10531
- cpuCount: (0, import_node_os4.cpus)().length,
10532
- memoryBytes: (0, import_node_os4.totalmem)(),
10746
+ cpuCount: (0, import_node_os5.cpus)().length,
10747
+ memoryBytes: (0, import_node_os5.totalmem)(),
10533
10748
  source: descriptor2,
10534
10749
  images: {
10535
10750
  ready: true,
@@ -10626,17 +10841,18 @@ function apiFailure(action2, status, value2) {
10626
10841
  function record6(value2) {
10627
10842
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
10628
10843
  }
10629
- var import_node_fs18, import_node_os4, import_node_path16;
10844
+ var import_node_fs18, import_node_os5, import_node_path17;
10630
10845
  var init_code_connect = __esm({
10631
10846
  "src/code-connect.ts"() {
10632
10847
  "use strict";
10633
10848
  init_cjs_shims();
10634
10849
  import_node_fs18 = require("fs");
10635
- import_node_os4 = require("os");
10636
- import_node_path16 = require("path");
10850
+ import_node_os5 = require("os");
10851
+ import_node_path17 = require("path");
10637
10852
  init_node();
10638
10853
  init_admin_ai_auth();
10639
10854
  init_config();
10855
+ init_device_session();
10640
10856
  init_version();
10641
10857
  init_security_hosted_github();
10642
10858
  init_code_local_source();
@@ -10950,7 +11166,7 @@ function developerTokenStatus(context, parsed, now = Date.now()) {
10950
11166
  const cacheStatus = !cached?.token ? "missing" : cached.platform !== context.platform.value ? "other-platform" : (cached.expiresAt ?? 0) <= now + 6e4 ? "expired" : "valid";
10951
11167
  const source = clean3(
10952
11168
  stringOpt(parsed.options.token)
10953
- ) ? "flag" : clean3(import_node_process12.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
11169
+ ) ? "flag" : clean3(import_node_process16.default.env.ODLA_DEV_TOKEN) ? "environment" : cacheStatus === "valid" ? "cache" : "missing";
10954
11170
  return {
10955
11171
  source,
10956
11172
  cacheFile: context.cfg.local.tokenFile,
@@ -10961,12 +11177,12 @@ function clean3(value2) {
10961
11177
  const normalized = value2?.trim();
10962
11178
  return normalized || void 0;
10963
11179
  }
10964
- var import_node_process12;
11180
+ var import_node_process16;
10965
11181
  var init_operator_credentials = __esm({
10966
11182
  "src/operator-credentials.ts"() {
10967
11183
  "use strict";
10968
11184
  init_cjs_shims();
10969
- import_node_process12 = __toESM(require("process"), 1);
11185
+ import_node_process16 = __toESM(require("process"), 1);
10970
11186
  init_argv();
10971
11187
  init_local();
10972
11188
  }
@@ -11174,11 +11390,32 @@ var init_credential_command = __esm({
11174
11390
  });
11175
11391
 
11176
11392
  // src/help-usage.ts
11177
- var USAGE_SECTION;
11393
+ var AUTH_SECTION, USAGE_SECTION;
11178
11394
  var init_help_usage = __esm({
11179
11395
  "src/help-usage.ts"() {
11180
11396
  "use strict";
11181
11397
  init_cjs_shims();
11398
+ AUTH_SECTION = `
11399
+ Enrol this machine once, then stop asking:
11400
+ npx odla-ai device enroll --all-apps --capability all --no-open --wait 600
11401
+ One browser approval. Afterwards every worktree on this machine mints its
11402
+ own short-lived credentials with nobody's attention, for every app you
11403
+ own \u2014 including apps you create later. The window rolls forward each time
11404
+ you use it, so continuous work never interrupts anyone; only a real gap
11405
+ does. Give the human the printed /studio?code= URL, keep the process
11406
+ alive, and wait on it.
11407
+
11408
+ npx odla-ai device enroll --platform-wide --device-ttl 6w --no-open --wait 600
11409
+ The same thing across all of odla, for weeks. Needs a platform
11410
+ administrator's approval \u2014 an app owner's cannot carry platform scopes.
11411
+
11412
+ npx odla-ai whoami what this machine holds and when it lapses
11413
+ npx odla-ai device list every machine you have enrolled
11414
+
11415
+ Enrollment is the only human decision here. Revoking a machine, purging an app,
11416
+ transferring ownership, and rotating credentials still need a signed-in human in
11417
+ Studio, and no machine credential can do them however wide its approval was.
11418
+ `;
11182
11419
  USAGE_SECTION = `
11183
11420
  Start here:
11184
11421
  odla-ai runbook ask "<question>" The current procedure, from odla's own
@@ -11217,7 +11454,7 @@ Usage:
11217
11454
  odla-ai brand design unpack <bundle.html|-> [--out <dir>] [--json]
11218
11455
  odla-ai pm project list [--app <product-id>] [--status <s>] [--json]
11219
11456
  odla-ai pm project add --app <product-id> --name <name> [--description <text>] [--json]
11220
- odla-ai pm project use <project-id> [--json] [saved locally in this worktree]
11457
+ odla-ai pm project use <project-id> [--json] [saved for this app, on this machine]
11221
11458
  odla-ai pm goal list [--app <id>] [--project <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
11222
11459
  odla-ai pm task list [--app <id>] [--column <backlog|ready|doing|review|done>] [--goal <id>] [--assignee <id>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
11223
11460
  odla-ai pm decision list [--app <id>] [--status <s>] [--q <text>] [--limit <n>] [--offset <n>] [--json]
@@ -11309,7 +11546,8 @@ Usage:
11309
11546
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
11310
11547
  odla-ai security run [target] --self --ack-redacted-source
11311
11548
  odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
11312
- odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
11549
+ odla-ai device enroll [--all-apps|--app <id>[,<id>...]] [--capability all|<c>[,<c>...]] [--platform-wide]
11550
+ [--name <label>] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--wait <seconds>] [--json]
11313
11551
  odla-ai device list [--email <odla-account>] [--json]
11314
11552
  odla-ai device revoke <device-id> [--email <odla-account>] [--json]
11315
11553
  odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
@@ -11325,8 +11563,11 @@ Usage:
11325
11563
 
11326
11564
  // src/help.ts
11327
11565
  function printHelp(output = console) {
11328
- output.log(`odla-ai
11329
- ${USAGE_SECTION}
11566
+ output.log(helpText());
11567
+ }
11568
+ function helpText() {
11569
+ return `odla-ai
11570
+ ${AUTH_SECTION}${USAGE_SECTION}
11330
11571
  Commands:
11331
11572
  auth Start a fresh, exact-project agent authorization for human review.
11332
11573
  The email is the signed-in odla account, never git or GitHub
@@ -11399,8 +11640,9 @@ Commands:
11399
11640
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
11400
11641
  pm Project management (via @odla-ai/pm): Products contain Projects;
11401
11642
  projects contain goals, kanban tasks, decisions, and bugs. Use
11402
- "pm project list|add|use" to select worktree-local context, or
11403
- pass --app/--project explicitly. Same device-grant auth as "app".
11643
+ "pm project list|add|use" selects a project for this app on this
11644
+ machine \u2014 every worktree shares it \u2014 or pass --app/--project
11645
+ explicitly. Same device-grant auth as "app".
11404
11646
  Status changes and comments post to each item's @odla-ai/chat
11405
11647
  discussion thread.
11406
11648
  NOTE: "--column ready" is OWNER-ONLY. Creating a task in Ready
@@ -11429,9 +11671,20 @@ Commands:
11429
11671
  platform Read canonical fleet health, releases, provider load/freshness,
11430
11672
  explicit unknowns, and next actions through a read-only grant.
11431
11673
  device Enrol THIS machine once, then stop asking. A human approves the
11432
- enrollment in the browser; from then on this terminal mints its
11433
- own short-lived credentials for the named projects with nobody's
11434
- attention, until the device expires or is revoked.
11674
+ enrollment in the browser; from then on EVERY worktree on this
11675
+ machine mints its own short-lived credentials with nobody's
11676
+ attention, until the device is revoked or goes unused.
11677
+ "--all-apps" covers every app you own, now and later, so creating
11678
+ an app costs no new approval. "--capability all" takes everything
11679
+ that approval is allowed to carry, so a capability you did not
11680
+ think to name is not a 403 next week. "--platform-wide" is the
11681
+ administrator's version, across all of odla.
11682
+ The expiry is a GAP, not a clock: each use rolls it forward, so
11683
+ only going quiet brings a human back into the loop \u2014 which is
11684
+ where anything that changed can be explained.
11685
+ "device list" shows what each machine holds and when it lapses;
11686
+ revoking one takes down every credential it ever minted, and is
11687
+ deliberately a signed-in human's decision in Studio.
11435
11688
  provision Register services, compose integrations, persist credentials, optionally push secrets.
11436
11689
  "provision --live --yes" initializes only the live instance of
11437
11690
  an existing sandbox app and enables every configured service;
@@ -11501,10 +11754,12 @@ Safety:
11501
11754
  release. A confirmed stale client stops with a safe npx rerun command; a
11502
11755
  workspace-linked client also identifies the worktree that must be updated.
11503
11756
  Run Code from a GitHub checkout already connected to an app in Studio; an
11504
- odla.config.mjs may select the app explicitly but is not required. Code host
11505
- approval and credential hashes live in odla-ai/db. The host
11506
- credential is never written under .odla/; it exists only in the foreground
11507
- "code connect" process and is rotated by the next approved connection.
11757
+ odla.config.mjs may select the app explicitly but is not required. With an
11758
+ enrolled code.session device, the Studio repository selection authorizes the
11759
+ host's first connection and reconnects without another human approval. Without
11760
+ an enrolled device, "code connect" falls back to the reviewed handshake. Host
11761
+ credential hashes live in odla-ai/db; the credential itself is never written
11762
+ under .odla/, exists only in the foreground process, and rotates on reconnect.
11508
11763
  "code repository bind" takes owner/name and resolves the two GitHub integers the
11509
11764
  bind route wants across every installation you have, refusing an ambiguous match
11510
11765
  rather than choosing one \u2014 the same repository name under two organizations is
@@ -11525,7 +11780,7 @@ Safety:
11525
11780
  Run security plan first to inspect the admin-selected providers, models,
11526
11781
  per-route bounds, credential readiness, retention, no-execution boundary,
11527
11782
  and digest that binds consent to that exact plan.
11528
- `);
11783
+ `;
11529
11784
  }
11530
11785
  var init_help = __esm({
11531
11786
  "src/help.ts"() {
@@ -11535,6 +11790,53 @@ var init_help = __esm({
11535
11790
  }
11536
11791
  });
11537
11792
 
11793
+ // src/help-command.ts
11794
+ function printCommandHelp(command, output = console) {
11795
+ const lines = helpText().split("\n");
11796
+ const usage = allBlocks(lines, new RegExp(`^ odla-ai ${escapeRe(command)}(\\s|$)`));
11797
+ const prose = block(lines, (line2) => new RegExp(`^ ${escapeRe(command)}\\s\\s+\\S`).test(line2));
11798
+ if (usage.length === 0 && prose.length === 0) {
11799
+ output.log(`odla-ai: no command "${command}". Run "odla-ai help" for all of them.`);
11800
+ return;
11801
+ }
11802
+ output.log([
11803
+ ...prose.length ? [prose.join("\n"), ""] : [],
11804
+ ...usage.length ? ["Usage:", ...usage, ""] : [],
11805
+ AUTH_SECTION.trimEnd()
11806
+ ].join("\n"));
11807
+ }
11808
+ function allBlocks(lines, pattern) {
11809
+ const out = [];
11810
+ for (let i = 0; i < lines.length; i++) {
11811
+ if (!pattern.test(lines[i])) continue;
11812
+ out.push(...block(lines.slice(i), (line2) => line2 === lines[i]));
11813
+ }
11814
+ return out;
11815
+ }
11816
+ function block(lines, starts) {
11817
+ const first = lines.findIndex(starts);
11818
+ if (first === -1) return [];
11819
+ const indent = lines[first].length - lines[first].trimStart().length;
11820
+ const out = [lines[first]];
11821
+ for (const line2 of lines.slice(first + 1)) {
11822
+ if (!line2.trim()) break;
11823
+ if (line2.length - line2.trimStart().length <= indent) break;
11824
+ out.push(line2);
11825
+ }
11826
+ return out;
11827
+ }
11828
+ function escapeRe(value2) {
11829
+ return value2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11830
+ }
11831
+ var init_help_command = __esm({
11832
+ "src/help-command.ts"() {
11833
+ "use strict";
11834
+ init_cjs_shims();
11835
+ init_help_usage();
11836
+ init_help();
11837
+ }
11838
+ });
11839
+
11538
11840
  // src/discuss-principals.ts
11539
11841
  function mergeDiscussPrincipals(target, source) {
11540
11842
  Object.assign(target.authors, source.authors ?? {});
@@ -12800,20 +13102,43 @@ var init_pm_watch = __esm({
12800
13102
 
12801
13103
  // src/pm-project-context.ts
12802
13104
  function readPmProjectContext(rootDir) {
12803
- const value2 = readJsonFile(pmProjectContextFile(rootDir));
12804
- return value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" ? value2 : null;
13105
+ adoptLegacySelection(rootDir);
13106
+ const entries = Object.values(readSelections()).filter(isSelection);
13107
+ return entries.sort((a, b) => b.selectedAt.localeCompare(a.selectedAt))[0] ?? null;
12805
13108
  }
12806
13109
  function writePmProjectContext(rootDir, value2) {
12807
- writePrivateJson(pmProjectContextFile(rootDir), { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() });
13110
+ adoptLegacySelection(rootDir);
13111
+ writePrivateJson(pmProjectContextFile(), {
13112
+ ...readSelections(),
13113
+ [value2.appId]: { ...value2, selectedAt: (/* @__PURE__ */ new Date()).toISOString() }
13114
+ });
13115
+ }
13116
+ function adoptLegacySelection(rootDir) {
13117
+ const legacy = (0, import_node_path18.resolve)(rootDir, ".odla", "pm-project.local.json");
13118
+ if (!(0, import_node_fs19.existsSync)(legacy)) return;
13119
+ const previous = readJsonFile(legacy);
13120
+ (0, import_node_fs19.rmSync)(legacy, { force: true });
13121
+ if (!isSelection(previous)) return;
13122
+ const selections = readSelections();
13123
+ if (selections[previous.appId]) return;
13124
+ writePrivateJson(pmProjectContextFile(), { ...selections, [previous.appId]: previous });
12808
13125
  }
12809
- var import_node_path17, pmProjectContextFile;
13126
+ function readSelections() {
13127
+ return readJsonFile(pmProjectContextFile()) ?? {};
13128
+ }
13129
+ function isSelection(value2) {
13130
+ return !!value2 && typeof value2.appId === "string" && typeof value2.projectId === "string" && typeof value2.selectedAt === "string";
13131
+ }
13132
+ var import_node_fs19, import_node_path18, pmProjectContextFile;
12810
13133
  var init_pm_project_context = __esm({
12811
13134
  "src/pm-project-context.ts"() {
12812
13135
  "use strict";
12813
13136
  init_cjs_shims();
12814
- import_node_path17 = require("path");
13137
+ import_node_fs19 = require("fs");
13138
+ import_node_path18 = require("path");
12815
13139
  init_local();
12816
- pmProjectContextFile = (rootDir) => (0, import_node_path17.resolve)(rootDir, ".odla", "pm-project.local.json");
13140
+ init_odla_home();
13141
+ pmProjectContextFile = () => pmContextFile();
12817
13142
  }
12818
13143
  });
12819
13144
 
@@ -14387,7 +14712,7 @@ async function provision(options) {
14387
14712
  await provisionIntegrationSeeds(doFetch, cfg.dbEndpoint, tenantId, dbKey, database.integrations, env, out);
14388
14713
  }
14389
14714
  if (cfg.services.includes("ai") && cfg.ai?.provider && cfg.ai.keyEnv) {
14390
- const key = import_node_process13.default.env[cfg.ai.keyEnv];
14715
+ const key = import_node_process17.default.env[cfg.ai.keyEnv];
14391
14716
  if (key) {
14392
14717
  const secretName = cfg.ai.secretName ?? defaultSecretName(cfg.ai.provider);
14393
14718
  await (0, import_ai5.putSecret)({ endpoint: cfg.dbEndpoint, token, fetch: doFetch }, tenantId, secretName, key);
@@ -14426,14 +14751,14 @@ async function provision(options) {
14426
14751
  }
14427
14752
  }
14428
14753
  }
14429
- var import_apps13, import_ai5, import_node_process13;
14754
+ var import_apps13, import_ai5, import_node_process17;
14430
14755
  var init_provision = __esm({
14431
14756
  "src/provision.ts"() {
14432
14757
  "use strict";
14433
14758
  init_cjs_shims();
14434
14759
  import_apps13 = require("@odla-ai/apps");
14435
14760
  import_ai5 = require("@odla-ai/ai");
14436
- import_node_process13 = __toESM(require("process"), 1);
14761
+ import_node_process17 = __toESM(require("process"), 1);
14437
14762
  init_config();
14438
14763
  init_calendar();
14439
14764
  init_calendar_errors();
@@ -14628,7 +14953,7 @@ var init_surface = __esm({
14628
14953
 
14629
14954
  // src/record.ts
14630
14955
  function recordInvocation(parsed) {
14631
- const file = import_node_process14.default.env.ODLA_CLI_RECORD;
14956
+ const file = import_node_process18.default.env.ODLA_CLI_RECORD;
14632
14957
  if (!file) return;
14633
14958
  try {
14634
14959
  const entry = {
@@ -14636,18 +14961,18 @@ function recordInvocation(parsed) {
14636
14961
  options: Object.entries(parsed.options).map(([name, value2]) => value2 === false ? `no-${name}` : name).sort()
14637
14962
  };
14638
14963
  if (!entry.path.length) return;
14639
- (0, import_node_fs19.appendFileSync)(file, `${JSON.stringify(entry)}
14964
+ (0, import_node_fs20.appendFileSync)(file, `${JSON.stringify(entry)}
14640
14965
  `);
14641
14966
  } catch {
14642
14967
  }
14643
14968
  }
14644
- var import_node_fs19, import_node_process14;
14969
+ var import_node_fs20, import_node_process18;
14645
14970
  var init_record = __esm({
14646
14971
  "src/record.ts"() {
14647
14972
  "use strict";
14648
14973
  init_cjs_shims();
14649
- import_node_fs19 = require("fs");
14650
- import_node_process14 = __toESM(require("process"), 1);
14974
+ import_node_fs20 = require("fs");
14975
+ import_node_process18 = __toESM(require("process"), 1);
14651
14976
  init_surface();
14652
14977
  }
14653
14978
  });
@@ -14709,6 +15034,22 @@ var init_device_ttl = __esm({
14709
15034
 
14710
15035
  // src/device-command.ts
14711
15036
  async function deviceCommand(parsed, deps) {
15037
+ assertArgs(parsed, [
15038
+ "app",
15039
+ "all-apps",
15040
+ "platform-wide",
15041
+ "name",
15042
+ "capability",
15043
+ "device-ttl",
15044
+ "email",
15045
+ "open",
15046
+ "json",
15047
+ "config",
15048
+ "token",
15049
+ "context",
15050
+ "platform",
15051
+ "wait"
15052
+ ], 3);
14712
15053
  const action2 = parsed.positionals[1] ?? "";
14713
15054
  const out = deps.stdout ?? console;
14714
15055
  const doFetch = deps.fetch ?? fetch;
@@ -14721,10 +15062,12 @@ async function deviceCommand(parsed, deps) {
14721
15062
  }
14722
15063
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
14723
15064
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
14724
- const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
14725
- if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
15065
+ const platformWide = parsed.options["platform-wide"] === true;
15066
+ const apps = platformWide || parsed.options["all-apps"] === true ? [import_db4.ALL_OWNED_APPS] : (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
15067
+ if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026], or --all-apps");
14726
15068
  const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
14727
- const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
15069
+ const extended = platformWide || deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
15070
+ const { capabilities, scopes } = requestedEnvelope(parsed, platformWide);
14728
15071
  const token = await scopedToken2(
14729
15072
  parsed,
14730
15073
  deps,
@@ -14739,9 +15082,10 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14739
15082
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
14740
15083
  body: JSON.stringify({
14741
15084
  name,
14742
- platform: import_node_process15.default.platform,
15085
+ platform: import_node_process19.default.platform,
14743
15086
  appIds: apps,
14744
- ...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
15087
+ ...capabilities ? { capabilities } : {},
15088
+ ...scopes ? { scopes } : {},
14745
15089
  ...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
14746
15090
  })
14747
15091
  });
@@ -14750,19 +15094,52 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
14750
15094
  throw new Error(`device enroll failed: ${body.error?.message ?? `registry returned ${response2.status}`} (${response2.status})`);
14751
15095
  }
14752
15096
  const path = deviceCredentialPath();
14753
- (0, import_node_fs20.mkdirSync)((0, import_node_path18.dirname)(path), { recursive: true });
14754
- (0, import_node_fs20.writeFileSync)(path, JSON.stringify({
15097
+ (0, import_node_fs21.mkdirSync)((0, import_node_path19.dirname)(path), { recursive: true });
15098
+ (0, import_node_fs21.writeFileSync)(path, JSON.stringify({
14755
15099
  token: body.token,
14756
15100
  platform: cfg.platformUrl.replace(/\/$/, ""),
14757
15101
  deviceId: body.device.deviceId,
14758
15102
  name
14759
15103
  }, null, 2));
14760
- (0, import_node_fs20.chmodSync)(path, 384);
14761
- out.error(`device: enrolled "${name}" for ${body.device.appIds.join(", ")}; credential written to ${path}`);
14762
- out.error("device: this terminal will mint its own credentials from now on \u2014 no further approvals.");
15104
+ (0, import_node_fs21.chmodSync)(path, 384);
15105
+ rememberMachineIdentity(cfg.platformUrl.replace(/\/$/, ""), stringOpt(parsed.options.email));
15106
+ const reach = body.device.appIds.includes(import_db4.ALL_OWNED_APPS) ? "every app you own, now and later" : body.device.appIds.join(", ");
15107
+ out.error(`device: enrolled "${name}" for ${reach}; credential written to ${path}`);
15108
+ out.error(
15109
+ "device: every worktree on this machine mints its own credentials from now on \u2014 no further approvals,"
15110
+ );
15111
+ out.error(
15112
+ `device: and the clock resets each time you use it. Going quiet for ${describeWindow(body.device.expiresAt)} is what ends it.`
15113
+ );
14763
15114
  if (json) {
14764
- out.log(JSON.stringify({ deviceId: body.device.deviceId, name, appIds: body.device.appIds, expiresAt: body.device.expiresAt }, null, 2));
15115
+ out.log(JSON.stringify({
15116
+ deviceId: body.device.deviceId,
15117
+ name,
15118
+ appIds: body.device.appIds,
15119
+ capabilities: body.device.capabilities ?? [],
15120
+ scopes: body.device.scopes ?? [],
15121
+ expiresAt: body.device.expiresAt,
15122
+ hardExpiresAt: body.device.hardExpiresAt ?? null
15123
+ }, null, 2));
15124
+ }
15125
+ }
15126
+ function requestedEnvelope(parsed, platformWide) {
15127
+ const raw = stringOpt(parsed.options.capability);
15128
+ const everything = platformWide || raw?.trim().toLowerCase() === "all";
15129
+ if (everything) {
15130
+ return {
15131
+ capabilities: [...import_db4.OPTIONAL_AGENT_PROJECT_CAPABILITIES],
15132
+ scopes: platformWide ? [...import_db4.ADMIN_DEVICE_SCOPES] : [...import_db4.OWNER_DEVICE_SCOPES]
15133
+ };
14765
15134
  }
15135
+ const named = raw?.split(",").map((c) => c.trim()).filter(Boolean);
15136
+ return named?.length ? { capabilities: named } : {};
15137
+ }
15138
+ function describeWindow(expiresAt, now = Date.now()) {
15139
+ const days = Math.max(1, Math.round((expiresAt - now) / (24 * 60 * 60 * 1e3)));
15140
+ if (days >= 365) return `${Math.round(days / 365)} year${days >= 730 ? "s" : ""}`;
15141
+ if (days % 7 === 0) return `${days / 7} week${days > 7 ? "s" : ""}`;
15142
+ return `${days} day${days === 1 ? "" : "s"}`;
14766
15143
  }
14767
15144
  async function list2(parsed, deps, cfg, doFetch, out, json) {
14768
15145
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device list)");
@@ -14777,12 +15154,17 @@ async function list2(parsed, deps, cfg, doFetch, out, json) {
14777
15154
  if (body.devices.length === 0) return out.log("no enrolled devices");
14778
15155
  for (const device of body.devices) {
14779
15156
  const state2 = device.revokedAt ? "revoked" : device.expiresAt <= Date.now() ? "expired" : "active";
14780
- out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${device.appIds.join(", ")}]`);
15157
+ const reach = device.appIds.includes("*") ? "every app you own" : device.appIds.join(", ");
15158
+ const gap = state2 === "active" ? ` idle ${describeWindow(device.expiresAt)} left` : "";
15159
+ out.log(`${device.deviceId} ${state2.padEnd(7)} ${device.name} [${reach}]${gap}`);
14781
15160
  }
14782
15161
  }
14783
15162
  async function revoke(parsed, deps, cfg, doFetch, out, json) {
14784
15163
  const deviceId = parsed.positionals[2];
14785
15164
  if (!deviceId) throw new Error("device revoke needs the device id from `odla-ai device list`");
15165
+ out.error(
15166
+ `device: revoking is a signed-in human's decision; if this is refused, open ${cfg.platformUrl}/studio and revoke it there.`
15167
+ );
14786
15168
  const token = await scopedToken2(parsed, deps, cfg, doFetch, out, "odla CLI (device revoke)");
14787
15169
  const response2 = await doFetch(`${cfg.platformUrl}/registry/devices/${encodeURIComponent(deviceId)}/revoke`, {
14788
15170
  method: "POST",
@@ -14801,7 +15183,7 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
14801
15183
  // A device is granted the apps named in ONE approval, so --app is a list here.
14802
15184
  allowAppList: true
14803
15185
  });
14804
- const scopedTokenFile = credentials.scopedTokenFile;
15186
+ const scopedTokenFile2 = credentials.scopedTokenFile;
14805
15187
  return getScopedPlatformToken({
14806
15188
  platform: cfg.platformUrl,
14807
15189
  scope,
@@ -14812,22 +15194,24 @@ async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app
14812
15194
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
14813
15195
  openApprovalUrl: deps.openUrl,
14814
15196
  rootDir: cfg.rootDir,
14815
- tokenFile: scopedTokenFile,
15197
+ tokenFile: scopedTokenFile2,
14816
15198
  ...stringOpt(parsed.options.token) ? { token: stringOpt(parsed.options.token) } : {}
14817
15199
  });
14818
15200
  }
14819
15201
  function defaultDeviceName() {
14820
- return `${import_node_process15.default.env.HOSTNAME ?? import_node_process15.default.env.HOST ?? "machine"}-${import_node_process15.default.platform}`;
15202
+ return `${import_node_process19.default.env.HOSTNAME ?? import_node_process19.default.env.HOST ?? "machine"}-${import_node_process19.default.platform}`;
14821
15203
  }
14822
- var import_node_fs20, import_node_path18, import_node_process15;
15204
+ var import_db4, import_node_fs21, import_node_path19, import_node_process19;
14823
15205
  var init_device_command = __esm({
14824
15206
  "src/device-command.ts"() {
14825
15207
  "use strict";
14826
15208
  init_cjs_shims();
15209
+ import_db4 = require("@odla-ai/db");
14827
15210
  init_device_ttl();
14828
- import_node_fs20 = require("fs");
14829
- import_node_path18 = require("path");
14830
- import_node_process15 = __toESM(require("process"), 1);
15211
+ init_machine_identity();
15212
+ import_node_fs21 = require("fs");
15213
+ import_node_path19 = require("path");
15214
+ import_node_process19 = __toESM(require("process"), 1);
14831
15215
  init_argv();
14832
15216
  init_admin_ai_auth();
14833
15217
  init_device_session();
@@ -14882,7 +15266,7 @@ async function bySlug(ctx, slug) {
14882
15266
  function readBody(file, inline) {
14883
15267
  if (inline !== void 0) return inline;
14884
15268
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
14885
- return (0, import_node_fs21.readFileSync)(file === "-" ? 0 : file, "utf8");
15269
+ return (0, import_node_fs22.readFileSync)(file === "-" ? 0 : file, "utf8");
14886
15270
  }
14887
15271
  async function runbookList(ctx, all, query) {
14888
15272
  const params = new URLSearchParams();
@@ -14971,12 +15355,12 @@ async function runbookRemove(ctx, slug) {
14971
15355
  await call2(ctx, "DELETE", `/runbook/${encodeURIComponent(runbook.id)}`);
14972
15356
  ctx.out.log(`removed ${slug}`);
14973
15357
  }
14974
- var import_node_fs21, PLATFORM_SCOPE, stamp;
15358
+ var import_node_fs22, PLATFORM_SCOPE, stamp;
14975
15359
  var init_runbook_actions = __esm({
14976
15360
  "src/runbook-actions.ts"() {
14977
15361
  "use strict";
14978
15362
  init_cjs_shims();
14979
- import_node_fs21 = require("fs");
15363
+ import_node_fs22 = require("fs");
14980
15364
  init_version();
14981
15365
  init_runbook_requires();
14982
15366
  PLATFORM_SCOPE = "$platform";
@@ -15009,12 +15393,12 @@ function parseRunbook(text3, slug) {
15009
15393
  };
15010
15394
  }
15011
15395
  function readRunbookDir(dir) {
15012
- if (!(0, import_node_fs22.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
15013
- const files = (0, import_node_fs22.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
15396
+ if (!(0, import_node_fs23.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
15397
+ const files = (0, import_node_fs23.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
15014
15398
  if (!files.length) throw new Error(`no .md files in ${dir}`);
15015
15399
  return files.map((file) => {
15016
- const slug = (0, import_node_path19.basename)(file, ".md");
15017
- const parsed = parseRunbook((0, import_node_fs22.readFileSync)((0, import_node_path19.join)(dir, file), "utf8"), slug);
15400
+ const slug = (0, import_node_path20.basename)(file, ".md");
15401
+ const parsed = parseRunbook((0, import_node_fs23.readFileSync)((0, import_node_path20.join)(dir, file), "utf8"), slug);
15018
15402
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
15019
15403
  });
15020
15404
  }
@@ -15079,13 +15463,13 @@ async function upsert(ctx, r, visibility) {
15079
15463
  );
15080
15464
  return "updated";
15081
15465
  }
15082
- var import_node_fs22, import_node_path19;
15466
+ var import_node_fs23, import_node_path20;
15083
15467
  var init_runbook_import = __esm({
15084
15468
  "src/runbook-import.ts"() {
15085
15469
  "use strict";
15086
15470
  init_cjs_shims();
15087
- import_node_fs22 = require("fs");
15088
- import_node_path19 = require("path");
15471
+ import_node_fs23 = require("fs");
15472
+ import_node_path20 = require("path");
15089
15473
  init_runbook_actions();
15090
15474
  }
15091
15475
  });
@@ -15263,10 +15647,10 @@ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
15263
15647
  }
15264
15648
  function manifestLabeller(root) {
15265
15649
  return (workspace) => {
15266
- const manifest = (0, import_node_path20.join)(root, workspace, "package.json");
15267
- if (!(0, import_node_fs23.existsSync)(manifest)) return void 0;
15650
+ const manifest = (0, import_node_path21.join)(root, workspace, "package.json");
15651
+ if (!(0, import_node_fs24.existsSync)(manifest)) return void 0;
15268
15652
  try {
15269
- const name = JSON.parse((0, import_node_fs23.readFileSync)(manifest, "utf8")).name;
15653
+ const name = JSON.parse((0, import_node_fs24.readFileSync)(manifest, "utf8")).name;
15270
15654
  return typeof name === "string" ? name : void 0;
15271
15655
  } catch {
15272
15656
  return void 0;
@@ -15332,7 +15716,7 @@ function report4(ctx, impacts) {
15332
15716
  async function runbookImpact(ctx, options, deps = {}) {
15333
15717
  const cwd = deps.cwd ?? process.cwd();
15334
15718
  const runGit = deps.runGit ?? gitRunner(cwd);
15335
- const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs23.readFileSync)((0, import_node_path20.join)(cwd, path), "utf8"));
15719
+ const read3 = deps.readRepoFile ?? ((path) => (0, import_node_fs24.readFileSync)((0, import_node_path21.join)(cwd, path), "utf8"));
15336
15720
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read3), manifestLabeller(cwd));
15337
15721
  if (!surfaces.length) {
15338
15722
  return ctx.out.log(
@@ -15343,14 +15727,14 @@ async function runbookImpact(ctx, options, deps = {}) {
15343
15727
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
15344
15728
  report4(ctx, impacts);
15345
15729
  }
15346
- var import_node_child_process6, import_node_fs23, import_node_path20, SOURCE3, editHint;
15730
+ var import_node_child_process6, import_node_fs24, import_node_path21, SOURCE3, editHint;
15347
15731
  var init_runbook_impact = __esm({
15348
15732
  "src/runbook-impact.ts"() {
15349
15733
  "use strict";
15350
15734
  init_cjs_shims();
15351
15735
  import_node_child_process6 = require("child_process");
15352
- import_node_fs23 = require("fs");
15353
- import_node_path20 = require("path");
15736
+ import_node_fs24 = require("fs");
15737
+ import_node_path21 = require("path");
15354
15738
  init_runbook_impact_scan();
15355
15739
  init_runbook_actions();
15356
15740
  SOURCE3 = /\.(ts|tsx|js|jsx|mts|cts)$/;
@@ -15490,7 +15874,7 @@ var init_runbook_search_command = __esm({
15490
15874
  });
15491
15875
 
15492
15876
  // src/runbook-editor.ts
15493
- function resolveEditor(env = import_node_process16.default.env) {
15877
+ function resolveEditor(env = import_node_process20.default.env) {
15494
15878
  for (const name of EDITOR_ENV) {
15495
15879
  const value2 = env[name];
15496
15880
  if (value2 && value2.trim()) return value2.trim();
@@ -15504,8 +15888,8 @@ function defaultRun(command, path) {
15504
15888
  return result.status ?? 0;
15505
15889
  }
15506
15890
  function editText(initial, slug, deps = {}) {
15507
- const env = deps.env ?? import_node_process16.default.env;
15508
- const interactive = deps.interactive ?? (() => Boolean(import_node_process16.default.stdin.isTTY));
15891
+ const env = deps.env ?? import_node_process20.default.env;
15892
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process20.default.stdin.isTTY));
15509
15893
  const editor = resolveEditor(env);
15510
15894
  if (!editor)
15511
15895
  throw new Error(
@@ -15513,28 +15897,28 @@ function editText(initial, slug, deps = {}) {
15513
15897
  );
15514
15898
  if (!interactive())
15515
15899
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
15516
- const dir = (0, import_node_fs24.mkdtempSync)((0, import_node_path21.join)((0, import_node_os5.tmpdir)(), "odla-runbook-"));
15517
- const file = (0, import_node_path21.join)(dir, `${slug}.md`);
15900
+ const dir = (0, import_node_fs25.mkdtempSync)((0, import_node_path22.join)((0, import_node_os6.tmpdir)(), "odla-runbook-"));
15901
+ const file = (0, import_node_path22.join)(dir, `${slug}.md`);
15518
15902
  try {
15519
- (0, import_node_fs24.writeFileSync)(file, initial, { mode: 384 });
15903
+ (0, import_node_fs25.writeFileSync)(file, initial, { mode: 384 });
15520
15904
  const code = defaultRunOrInjected(deps)(editor, file);
15521
15905
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
15522
- const edited = (0, import_node_fs24.readFileSync)(file, "utf8");
15906
+ const edited = (0, import_node_fs25.readFileSync)(file, "utf8");
15523
15907
  return edited === initial ? null : edited;
15524
15908
  } finally {
15525
- (0, import_node_fs24.rmSync)(dir, { recursive: true, force: true });
15909
+ (0, import_node_fs25.rmSync)(dir, { recursive: true, force: true });
15526
15910
  }
15527
15911
  }
15528
- var import_node_child_process7, import_node_fs24, import_node_os5, import_node_path21, import_node_process16, EDITOR_ENV, defaultRunOrInjected;
15912
+ var import_node_child_process7, import_node_fs25, import_node_os6, import_node_path22, import_node_process20, EDITOR_ENV, defaultRunOrInjected;
15529
15913
  var init_runbook_editor = __esm({
15530
15914
  "src/runbook-editor.ts"() {
15531
15915
  "use strict";
15532
15916
  init_cjs_shims();
15533
15917
  import_node_child_process7 = require("child_process");
15534
- import_node_fs24 = require("fs");
15535
- import_node_os5 = require("os");
15536
- import_node_path21 = require("path");
15537
- import_node_process16 = __toESM(require("process"), 1);
15918
+ import_node_fs25 = require("fs");
15919
+ import_node_os6 = require("os");
15920
+ import_node_path22 = require("path");
15921
+ import_node_process20 = __toESM(require("process"), 1);
15538
15922
  EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
15539
15923
  defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
15540
15924
  }
@@ -15923,9 +16307,9 @@ async function runHostedSecurity(options) {
15923
16307
  const appId = selfAudit ? "odla-ai" : cfg.app.id;
15924
16308
  const env = selfAudit ? "prod" : selectEnv(options.env, cfg.envs, cfg.configPath, cfg.rootDir);
15925
16309
  const platform = options.platform ?? cfg?.platformUrl ?? "https://odla.ai";
15926
- const target = (0, import_node_path22.resolve)(options.target ?? cfg?.rootDir ?? ".");
15927
- const output = (0, import_node_path22.resolve)(options.out ?? (0, import_node_path22.resolve)(target, ".odla/security/hosted"));
15928
- const outputRelative = (0, import_node_path22.relative)(target, output).split(import_node_path22.sep).join("/");
16310
+ const target = (0, import_node_path23.resolve)(options.target ?? cfg?.rootDir ?? ".");
16311
+ const output = (0, import_node_path23.resolve)(options.out ?? (0, import_node_path23.resolve)(target, ".odla/security/hosted"));
16312
+ const outputRelative = (0, import_node_path23.relative)(target, output).split(import_node_path23.sep).join("/");
15929
16313
  if (!outputRelative) throw new Error("Hosted security output cannot be the repository root");
15930
16314
  const profile = profileFor(options.profile ?? "odla", options.maxHuntTasks ?? 12);
15931
16315
  const tokenRequest = {
@@ -15937,7 +16321,7 @@ async function runHostedSecurity(options) {
15937
16321
  };
15938
16322
  const token = await injectedToken(options, tokenRequest);
15939
16323
  const snapshot = await (0, import_node3.snapshotDirectory)(target, {
15940
- exclude: !outputRelative.startsWith("../") && !(0, import_node_path22.isAbsolute)(outputRelative) ? [outputRelative] : []
16324
+ exclude: !outputRelative.startsWith("../") && !(0, import_node_path23.isAbsolute)(outputRelative) ? [outputRelative] : []
15941
16325
  });
15942
16326
  const hosted = await (0, import_security.createPlatformSecurityReasoners)({
15943
16327
  platform,
@@ -15955,7 +16339,7 @@ async function runHostedSecurity(options) {
15955
16339
  });
15956
16340
  const harness = (0, import_security.createSecurityHarness)({
15957
16341
  profile,
15958
- store: new import_node3.FileRunStore((0, import_node_path22.resolve)(output, "state")),
16342
+ store: new import_node3.FileRunStore((0, import_node_path23.resolve)(output, "state")),
15959
16343
  discoveryReasoner: hosted.discoveryReasoner,
15960
16344
  validationReasoner: hosted.validationReasoner,
15961
16345
  policy: {
@@ -15979,7 +16363,7 @@ async function runHostedSecurity(options) {
15979
16363
  function selectEnv(requested, declared, configPath, rootDir) {
15980
16364
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
15981
16365
  if (!env || !declared.includes(env)) {
15982
- const shown = (0, import_node_path22.relative)(rootDir, configPath) || configPath;
16366
+ const shown = (0, import_node_path23.relative)(rootDir, configPath) || configPath;
15983
16367
  throw new Error(`env "${env ?? ""}" is not declared in ${shown}`);
15984
16368
  }
15985
16369
  return env;
@@ -16008,17 +16392,17 @@ function printSummary(out, appId, env, run, report5, output) {
16008
16392
  out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
16009
16393
  if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
16010
16394
  out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
16011
- out.log(` report: ${(0, import_node_path22.resolve)(output, "REPORT.md")}`);
16395
+ out.log(` report: ${(0, import_node_path23.resolve)(output, "REPORT.md")}`);
16012
16396
  }
16013
16397
  function formatBudget(usage) {
16014
16398
  return usage ? `${usage.usedCalls}/${usage.maxCalls} skipped=${usage.skippedCalls}` : "caller-managed";
16015
16399
  }
16016
- var import_node_path22, import_security, import_node3;
16400
+ var import_node_path23, import_security, import_node3;
16017
16401
  var init_security = __esm({
16018
16402
  "src/security.ts"() {
16019
16403
  "use strict";
16020
16404
  init_cjs_shims();
16021
- import_node_path22 = require("path");
16405
+ import_node_path23 = require("path");
16022
16406
  import_security = require("@odla-ai/security");
16023
16407
  import_node3 = require("@odla-ai/security/node");
16024
16408
  init_config();
@@ -16536,8 +16920,10 @@ async function dispatchCli(argv2, dependencies) {
16536
16920
  return;
16537
16921
  }
16538
16922
  if (command === "help" || command === "--help" || command === "-h") {
16539
- assertArgs(parsed, ["help"], 1);
16540
- printHelp(runtime.stdout);
16923
+ assertArgs(parsed, ["help"], 2);
16924
+ const topic = parsed.positionals[1];
16925
+ if (topic) printCommandHelp(topic, runtime.stdout);
16926
+ else printHelp(runtime.stdout);
16541
16927
  return;
16542
16928
  }
16543
16929
  if (command === "whoami") {
@@ -16708,6 +17094,7 @@ var init_cli = __esm({
16708
17094
  init_context_command();
16709
17095
  init_credential_command();
16710
17096
  init_help();
17097
+ init_help_command();
16711
17098
  init_version();
16712
17099
  init_discuss_command();
16713
17100
  init_pm_command();