@spencerboucher/switchyard 0.5.0 → 0.7.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.
Files changed (167) hide show
  1. package/README.md +9 -7
  2. package/dist/bridge/pi-model.js +44 -0
  3. package/dist/bridge/switchyard-bridge.js +24 -2
  4. package/dist/bridge/switchyard-terminal.js +146 -4
  5. package/dist/switchyard.mjs +1301 -700
  6. package/package.json +1 -1
  7. package/web-dist/assets/AutomationView-BIW_pO0M.js +2 -0
  8. package/web-dist/assets/AutomationView-BIW_pO0M.js.map +1 -0
  9. package/web-dist/assets/Badge-BiDp7NKI.js +2 -0
  10. package/web-dist/assets/Badge-BiDp7NKI.js.map +1 -0
  11. package/web-dist/assets/InteractionCard-CWxyDv81.js +6 -0
  12. package/web-dist/assets/InteractionCard-CWxyDv81.js.map +1 -0
  13. package/web-dist/assets/{Menu-C4GUY_ym.js → Menu-BDt1bxwz.js} +2 -2
  14. package/web-dist/assets/Menu-BDt1bxwz.js.map +1 -0
  15. package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-D-2BCZzd.js} +2 -2
  16. package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-D-2BCZzd.js.map} +1 -1
  17. package/web-dist/assets/Tabs-ByiPklAw.js +184 -0
  18. package/web-dist/assets/Tabs-ByiPklAw.js.map +1 -0
  19. package/web-dist/assets/TextArea-BYr88SnV.js +2 -0
  20. package/web-dist/assets/TextArea-BYr88SnV.js.map +1 -0
  21. package/web-dist/assets/attention-Bya8XHKI.js +2 -0
  22. package/web-dist/assets/attention-Bya8XHKI.js.map +1 -0
  23. package/web-dist/assets/automations-DiXKRcnE.js +2 -0
  24. package/web-dist/assets/automations-DiXKRcnE.js.map +1 -0
  25. package/web-dist/assets/board-BrLaJwLJ.js +10 -0
  26. package/web-dist/assets/board-BrLaJwLJ.js.map +1 -0
  27. package/web-dist/assets/c-Cz4nF6n_.js +1 -0
  28. package/web-dist/assets/c-SSSlmE39.js +2 -0
  29. package/web-dist/assets/c-SSSlmE39.js.map +1 -0
  30. package/web-dist/assets/code-DKLO8bzz.js +11 -0
  31. package/web-dist/assets/code-DKLO8bzz.js.map +1 -0
  32. package/web-dist/assets/cpp-Cts1PyIS.js +2 -0
  33. package/web-dist/assets/cpp-Cts1PyIS.js.map +1 -0
  34. package/web-dist/assets/cpp-DzjeI0uH.js +1 -0
  35. package/web-dist/assets/css-BGIxQoYs.js +2 -0
  36. package/web-dist/assets/css-BGIxQoYs.js.map +1 -0
  37. package/web-dist/assets/css-DgaLhXQH.js +1 -0
  38. package/web-dist/assets/describe-DnvH6_fl.js +5 -0
  39. package/web-dist/assets/describe-DnvH6_fl.js.map +1 -0
  40. package/web-dist/assets/diff-woXpYk--.js +2 -0
  41. package/web-dist/assets/diff-woXpYk--.js.map +1 -0
  42. package/web-dist/assets/dist-BE_V9dwJ.js +56 -0
  43. package/web-dist/assets/dist-BE_V9dwJ.js.map +1 -0
  44. package/web-dist/assets/dockerfile-IyjqRm3v.js +2 -0
  45. package/web-dist/assets/dockerfile-IyjqRm3v.js.map +1 -0
  46. package/web-dist/assets/enroll-DUDpbJoB.js +2 -0
  47. package/web-dist/assets/enroll-DUDpbJoB.js.map +1 -0
  48. package/web-dist/assets/github-dark-C-LZuMrd.js +2 -0
  49. package/web-dist/assets/github-dark-C-LZuMrd.js.map +1 -0
  50. package/web-dist/assets/github-light-EUqPIrTm.js +2 -0
  51. package/web-dist/assets/github-light-EUqPIrTm.js.map +1 -0
  52. package/web-dist/assets/go-rLFTqkRN.js +2 -0
  53. package/web-dist/assets/go-rLFTqkRN.js.map +1 -0
  54. package/web-dist/assets/html-BMn1ogTm.js +2 -0
  55. package/web-dist/assets/html-BMn1ogTm.js.map +1 -0
  56. package/web-dist/assets/html-BWDvXlHg.js +1 -0
  57. package/web-dist/assets/index-DvRcMGEe.css +2 -0
  58. package/web-dist/assets/index-vIzH1m8u.js +11 -0
  59. package/web-dist/assets/index-vIzH1m8u.js.map +1 -0
  60. package/web-dist/assets/java-BtIQNnN0.js +2 -0
  61. package/web-dist/assets/java-BtIQNnN0.js.map +1 -0
  62. package/web-dist/assets/java-_ATy9zCa.js +1 -0
  63. package/web-dist/assets/javascript-BzbQS41l.js +2 -0
  64. package/web-dist/assets/javascript-BzbQS41l.js.map +1 -0
  65. package/web-dist/assets/javascript-CpIGwZ_Y.js +1 -0
  66. package/web-dist/assets/json-qhed-kSA.js +2 -0
  67. package/web-dist/assets/json-qhed-kSA.js.map +1 -0
  68. package/web-dist/assets/jsonc-CYpm1nAK.js +2 -0
  69. package/web-dist/assets/jsonc-CYpm1nAK.js.map +1 -0
  70. package/web-dist/assets/jsx-C4yDKi0A.js +1 -0
  71. package/web-dist/assets/jsx-D3KKoC5L.js +2 -0
  72. package/web-dist/assets/jsx-D3KKoC5L.js.map +1 -0
  73. package/web-dist/assets/logic-xb-QF00E.js +2 -0
  74. package/web-dist/assets/logic-xb-QF00E.js.map +1 -0
  75. package/web-dist/assets/lua-DjuecEsk.js +2 -0
  76. package/web-dist/assets/lua-DjuecEsk.js.map +1 -0
  77. package/web-dist/assets/lua-fPRBWD-V.js +1 -0
  78. package/web-dist/assets/{more-syTnSpW3.js → more-BP5x45cs.js} +2 -2
  79. package/web-dist/assets/{more-syTnSpW3.js.map → more-BP5x45cs.js.map} +1 -1
  80. package/web-dist/assets/now--XjJC37b.js +2 -0
  81. package/web-dist/assets/now--XjJC37b.js.map +1 -0
  82. package/web-dist/assets/python-gzcpVVnB.js +2 -0
  83. package/web-dist/assets/python-gzcpVVnB.js.map +1 -0
  84. package/web-dist/assets/ruby-Bx-U2qjP.js +2 -0
  85. package/web-dist/assets/ruby-Bx-U2qjP.js.map +1 -0
  86. package/web-dist/assets/rust-DpiIf5JO.js +2 -0
  87. package/web-dist/assets/rust-DpiIf5JO.js.map +1 -0
  88. package/web-dist/assets/scss-8bKmll0B.js +2 -0
  89. package/web-dist/assets/scss-8bKmll0B.js.map +1 -0
  90. package/web-dist/assets/send-BKjswHxB.js +2 -0
  91. package/web-dist/assets/send-BKjswHxB.js.map +1 -0
  92. package/web-dist/assets/settings-D8T1esWq.js +7 -0
  93. package/web-dist/assets/settings-D8T1esWq.js.map +1 -0
  94. package/web-dist/assets/shared-CZ7fcu_B.js +3 -0
  95. package/web-dist/assets/shared-CZ7fcu_B.js.map +1 -0
  96. package/web-dist/assets/shellscript-CVTB3cr6.js +1 -0
  97. package/web-dist/assets/shellscript-CqHdAlCp.js +2 -0
  98. package/web-dist/assets/shellscript-CqHdAlCp.js.map +1 -0
  99. package/web-dist/assets/sliders-horizontal-DAoirila.js +2 -0
  100. package/web-dist/assets/sliders-horizontal-DAoirila.js.map +1 -0
  101. package/web-dist/assets/sql-CNu3Or2W.js +2 -0
  102. package/web-dist/assets/sql-CNu3Or2W.js.map +1 -0
  103. package/web-dist/assets/sql-CfM2IJm3.js +1 -0
  104. package/web-dist/assets/states--geW2Ntl.js +2 -0
  105. package/web-dist/assets/states--geW2Ntl.js.map +1 -0
  106. package/web-dist/assets/swift-CyEgAFGc.js +2 -0
  107. package/web-dist/assets/swift-CyEgAFGc.js.map +1 -0
  108. package/web-dist/assets/terminal-login-BJxV78au.js +2 -0
  109. package/web-dist/assets/terminal-login-BJxV78au.js.map +1 -0
  110. package/web-dist/assets/toml-CcmNWLt0.js +2 -0
  111. package/web-dist/assets/toml-CcmNWLt0.js.map +1 -0
  112. package/web-dist/assets/tsx-BOvsFOI6.js +2 -0
  113. package/web-dist/assets/tsx-BOvsFOI6.js.map +1 -0
  114. package/web-dist/assets/tsx-CGosfAUg.js +1 -0
  115. package/web-dist/assets/typescript-Bb1J8qyy.js +1 -0
  116. package/web-dist/assets/typescript-j_1H8WHN.js +2 -0
  117. package/web-dist/assets/typescript-j_1H8WHN.js.map +1 -0
  118. package/web-dist/assets/useNow-jxdoOC_1.js +2 -0
  119. package/web-dist/assets/{useNow-BQpDYijR.js.map → useNow-jxdoOC_1.js.map} +1 -1
  120. package/web-dist/assets/xml-DuKfbPI6.js +1 -0
  121. package/web-dist/assets/xml-WY0zc4PG.js +2 -0
  122. package/web-dist/assets/xml-WY0zc4PG.js.map +1 -0
  123. package/web-dist/assets/yaml-DDA_XMNu.js +1 -0
  124. package/web-dist/assets/yaml-DijMHRDA.js +2 -0
  125. package/web-dist/assets/yaml-DijMHRDA.js.map +1 -0
  126. package/web-dist/index.html +4 -3
  127. package/web-dist/sw.js +1 -1
  128. package/web-dist/sw.js.map +1 -1
  129. package/web-dist/assets/AutomationView-CFohp0SQ.js +0 -2
  130. package/web-dist/assets/AutomationView-CFohp0SQ.js.map +0 -1
  131. package/web-dist/assets/InteractionCard-B-xmpTNG.js +0 -4
  132. package/web-dist/assets/InteractionCard-B-xmpTNG.js.map +0 -1
  133. package/web-dist/assets/Markdown-jXkvZeRp.js +0 -31
  134. package/web-dist/assets/Markdown-jXkvZeRp.js.map +0 -1
  135. package/web-dist/assets/Menu-C4GUY_ym.js.map +0 -1
  136. package/web-dist/assets/Overlay--v0ur2lj.js +0 -2
  137. package/web-dist/assets/Overlay--v0ur2lj.js.map +0 -1
  138. package/web-dist/assets/TextArea-BsQHOQMe.js +0 -2
  139. package/web-dist/assets/TextArea-BsQHOQMe.js.map +0 -1
  140. package/web-dist/assets/attention-q6_xlnT4.js +0 -2
  141. package/web-dist/assets/attention-q6_xlnT4.js.map +0 -1
  142. package/web-dist/assets/automations-CyA83OAD.js +0 -2
  143. package/web-dist/assets/automations-CyA83OAD.js.map +0 -1
  144. package/web-dist/assets/board-BFvNcFhk.js +0 -10
  145. package/web-dist/assets/board-BFvNcFhk.js.map +0 -1
  146. package/web-dist/assets/code-DavnmlQN.js +0 -8
  147. package/web-dist/assets/code-DavnmlQN.js.map +0 -1
  148. package/web-dist/assets/describe-BirsjlUl.js +0 -5
  149. package/web-dist/assets/describe-BirsjlUl.js.map +0 -1
  150. package/web-dist/assets/dist-C-h8FC-n.js +0 -56
  151. package/web-dist/assets/dist-C-h8FC-n.js.map +0 -1
  152. package/web-dist/assets/enroll-C7UBC20s.js +0 -2
  153. package/web-dist/assets/enroll-C7UBC20s.js.map +0 -1
  154. package/web-dist/assets/index-ByiuDoG0.css +0 -2
  155. package/web-dist/assets/index-IdzOP9GE.js +0 -11
  156. package/web-dist/assets/index-IdzOP9GE.js.map +0 -1
  157. package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
  158. package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
  159. package/web-dist/assets/now-BqsLS-tK.js +0 -2
  160. package/web-dist/assets/now-BqsLS-tK.js.map +0 -1
  161. package/web-dist/assets/settings-DRj7UPtP.js +0 -8
  162. package/web-dist/assets/settings-DRj7UPtP.js.map +0 -1
  163. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
  164. package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
  165. package/web-dist/assets/states-DrTQuYXw.js +0 -2
  166. package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
  167. package/web-dist/assets/useNow-BQpDYijR.js +0 -2
@@ -283,6 +283,8 @@ var RuntimeCapabilities = z3.object({
283
283
  costAccounting: z3.boolean(),
284
284
  /** The host runs sessions as interactive Pi in a terminal that clients can attach to. */
285
285
  terminal: z3.boolean().optional(),
286
+ /** The host answers session.models, session.commands, session.setModel, session.setThinking and session.compact. */
287
+ sessionControls: z3.boolean().optional(),
286
288
  unsupported: z3.array(z3.object({ capability: z3.string(), reason: z3.string() }))
287
289
  });
288
290
  var ProjectScan = z3.object({
@@ -364,6 +366,28 @@ var Workspace = z3.object({
364
366
  updatedAt: Iso
365
367
  });
366
368
  var ThinkingLevel = z3.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
369
+ var SessionModel = z3.object({
370
+ provider: z3.string(),
371
+ id: z3.string(),
372
+ name: z3.string().nullable(),
373
+ contextWindow: z3.number().nullable(),
374
+ reasoning: z3.boolean(),
375
+ /** In Pi's scoped (favorite) model list; set only when Pi has one. */
376
+ scoped: z3.boolean().optional(),
377
+ /** Effort levels this model accepts, in Pi's order; ["off"] without reasoning. */
378
+ thinkingLevels: z3.array(ThinkingLevel)
379
+ });
380
+ var SlashCommand = z3.object({
381
+ name: z3.string().min(1),
382
+ description: z3.string().nullable(),
383
+ source: z3.enum(["extension", "prompt", "skill"])
384
+ });
385
+ var ContextUsage = z3.object({
386
+ /** Estimated tokens in context; null right after compaction until the next reply. */
387
+ tokens: z3.number().nullable(),
388
+ window: z3.number(),
389
+ percent: z3.number().nullable()
390
+ });
367
391
  var FORBIDDEN_EXTRA_ARGS = ["--mode", "--fork", "--no-session", "-e", "--extension"];
368
392
  var ExtraArg = z3.string().refine((a) => a.startsWith("--") || !a.startsWith("-"), "extra args must be long flags").refine((a) => !FORBIDDEN_EXTRA_ARGS.includes(a.split("=")[0]) && !a.startsWith("--session"), "this flag is managed by Switchyard");
369
393
  var Profile = z3.object({
@@ -373,7 +397,9 @@ var Profile = z3.object({
373
397
  model: z3.string().nullable(),
374
398
  thinking: ThinkingLevel.nullable(),
375
399
  projectTrust: z3.enum(["ask", "approve", "ignore"]),
376
- extraArgs: z3.array(ExtraArg)
400
+ extraArgs: z3.array(ExtraArg),
401
+ /** "vanilla": --no-extensions --no-skills --no-prompt-templates. Undefined means "all". */
402
+ resources: z3.enum(["all", "vanilla"]).optional()
377
403
  });
378
404
  var RuntimeState = z3.enum([
379
405
  "starting",
@@ -387,7 +413,8 @@ var RuntimeState = z3.enum([
387
413
  var NativeHandle = z3.object({ sessionId: z3.string(), sessionFile: z3.string() });
388
414
  var SessionStats = z3.object({
389
415
  tokens: z3.number().nullable(),
390
- costUsd: z3.number().nullable()
416
+ costUsd: z3.number().nullable(),
417
+ context: ContextUsage.nullable().optional()
391
418
  });
392
419
  var Session = z3.object({
393
420
  id: Id,
@@ -410,6 +437,12 @@ var Session = z3.object({
410
437
  archivedAt: Iso.nullable(),
411
438
  queue: z3.object({ steering: z3.array(z3.string()), followUp: z3.array(z3.string()) }),
412
439
  stats: SessionStats.nullable(),
440
+ /** The live model (host-owned; last known when stopped). */
441
+ model: SessionModel.nullable().optional(),
442
+ /** The live effort (Pi's thinking level). */
443
+ thinking: ThinkingLevel.nullable().optional(),
444
+ /** Pi is compacting the context. */
445
+ compacting: z3.boolean().optional(),
413
446
  lastAssistantText: z3.string().nullable(),
414
447
  createdAt: Iso,
415
448
  /** A live terminal session: `switchyard tui` can attach to its Pi terminal. */
@@ -650,7 +683,7 @@ var Device = z3.object({
650
683
  permission: z3.enum(["granted", "denied", "default", "unknown"])
651
684
  }),
652
685
  preferredMobile: z3.boolean(),
653
- enrolledVia: z3.enum(["enrollment", "passkey", "oidc", "admin"]).default("enrollment")
686
+ enrolledVia: z3.enum(["enrollment", "passkey", "oidc", "admin", "approval"]).default("enrollment")
654
687
  });
655
688
  var ToolItemData = z3.object({
656
689
  toolCallId: z3.string(),
@@ -848,7 +881,8 @@ var ProfileInput = z4.object({
848
881
  model: z4.string().nullable().default(null),
849
882
  thinking: ThinkingLevel.nullable().default(null),
850
883
  projectTrust: z4.enum(["ask", "approve", "ignore"]).default("ask"),
851
- extraArgs: z4.array(ExtraArg).default([])
884
+ extraArgs: z4.array(ExtraArg).default([]),
885
+ resources: z4.enum(["all", "vanilla"]).default("all")
852
886
  });
853
887
  var CreateWorkspaceRequest = z4.object({
854
888
  checkoutId: z4.string(),
@@ -879,6 +913,18 @@ var PromptRequest = z4.object({
879
913
  clientMessageId: z4.string().optional()
880
914
  });
881
915
  var RenameRequest = z4.object({ title: z4.string().min(1).max(200) });
916
+ var SetModelRequest = z4.object({
917
+ provider: z4.string().min(1),
918
+ modelId: z4.string().min(1)
919
+ });
920
+ var SetThinkingRequest = z4.object({ level: ThinkingLevel });
921
+ var CompactRequest = z4.object({ instructions: z4.string().max(4e3).optional() });
922
+ var SessionModelsResult = z4.object({ models: z4.array(SessionModel) });
923
+ var SessionCommandsResult = z4.object({ commands: z4.array(SlashCommand) });
924
+ var ModelStateResult = z4.object({
925
+ model: SessionModel.nullable(),
926
+ thinking: ThinkingLevel.nullable()
927
+ });
882
928
  var ImportSessionRequest = z4.object({
883
929
  hostId: z4.string(),
884
930
  sessionFile: z4.string(),
@@ -1003,7 +1049,6 @@ var PushPayload = z4.union([
1003
1049
  z4.object({ type: z4.literal("clear"), tag: z4.string() })
1004
1050
  ]);
1005
1051
  var SetupState = z4.object({
1006
- initialized: z4.boolean(),
1007
1052
  passkey: z4.object({
1008
1053
  available: z4.boolean(),
1009
1054
  registered: z4.boolean(),
@@ -1011,10 +1056,6 @@ var SetupState = z4.object({
1011
1056
  }),
1012
1057
  oidc: z4.object({ label: z4.string() }).nullable()
1013
1058
  });
1014
- var InitResponse = z4.object({
1015
- alreadyInitialized: z4.boolean(),
1016
- enrollment: CreateEnrollmentResponse
1017
- });
1018
1059
  var PasskeyRegisterOptionsResponse = z4.object({
1019
1060
  challengeId: z4.string(),
1020
1061
  options: z4.unknown()
@@ -1050,6 +1091,31 @@ var OidcClaimResponse = z4.union([
1050
1091
  z4.object({ status: z4.literal("pending") }),
1051
1092
  z4.object({ status: z4.literal("done"), deviceId: z4.string() })
1052
1093
  ]);
1094
+ var TerminalLoginStartRequest = z4.object({ name: z4.string().min(1).max(80) });
1095
+ var TerminalLoginStartResponse = z4.object({
1096
+ userCode: z4.string(),
1097
+ pollToken: z4.string(),
1098
+ verifyUrl: z4.string(),
1099
+ expiresAt: z4.string(),
1100
+ intervalMs: z4.number().int()
1101
+ });
1102
+ var TerminalLoginClaimRequest = z4.object({
1103
+ userCode: z4.string().min(1),
1104
+ pollToken: z4.string().min(1)
1105
+ });
1106
+ var TerminalLoginClaimResponse = z4.union([
1107
+ z4.object({ status: z4.literal("pending") }),
1108
+ z4.object({ status: z4.literal("denied") }),
1109
+ z4.object({ status: z4.literal("done"), deviceId: z4.string(), credential: z4.string() })
1110
+ ]);
1111
+ var TerminalLoginRequest = z4.object({
1112
+ userCode: z4.string(),
1113
+ name: z4.string(),
1114
+ requestedFrom: z4.string(),
1115
+ createdAt: z4.string(),
1116
+ expiresAt: z4.string(),
1117
+ status: z4.enum(["pending", "approved", "denied"])
1118
+ });
1053
1119
  var AuthMethods = z4.object({
1054
1120
  passkeys: z4.array(Passkey),
1055
1121
  passkeyAvailability: z4.object({ available: z4.boolean(), reason: z4.string().nullable() }),
@@ -1253,6 +1319,20 @@ var HostCommand = z8.discriminatedUnion("type", [
1253
1319
  z8.object({ type: z8.literal("session.importable") }),
1254
1320
  z8.object({ type: z8.literal("session.readNative"), sessionFile: z8.string() }),
1255
1321
  z8.object({ type: z8.literal("session.snapshot"), sessionId: z8.string() }),
1322
+ z8.object({ type: z8.literal("session.models"), sessionId: z8.string() }),
1323
+ z8.object({ type: z8.literal("session.commands"), sessionId: z8.string() }),
1324
+ z8.object({
1325
+ type: z8.literal("session.setModel"),
1326
+ sessionId: z8.string(),
1327
+ provider: z8.string().min(1),
1328
+ modelId: z8.string().min(1)
1329
+ }),
1330
+ z8.object({ type: z8.literal("session.setThinking"), sessionId: z8.string(), level: ThinkingLevel }),
1331
+ z8.object({
1332
+ type: z8.literal("session.compact"),
1333
+ sessionId: z8.string(),
1334
+ instructions: z8.string().nullable()
1335
+ }),
1256
1336
  z8.object({ type: z8.literal("checkout.inspect"), path: z8.string() }),
1257
1337
  z8.object({ type: z8.literal("projects.scan"), folder: z8.string().min(1) }),
1258
1338
  z8.object({
@@ -1794,7 +1874,7 @@ var ApiClient = class {
1794
1874
  constructor(opts) {
1795
1875
  this.baseUrl = opts.coordinatorUrl.replace(/\/+$/, "");
1796
1876
  this.credential = opts.credential;
1797
- this.doFetch = opts.fetch ?? ((input, init2) => fetch(input, init2));
1877
+ this.doFetch = opts.fetch ?? ((input, init) => fetch(input, init));
1798
1878
  }
1799
1879
  async request(method, path, body) {
1800
1880
  const headers = { accept: "application/json" };
@@ -2260,8 +2340,8 @@ function runRawTerminal(opts) {
2260
2340
  import { Command, Option } from "commander";
2261
2341
 
2262
2342
  // packages/cli/dist/commands/auth.js
2263
- import { hostname } from "node:os";
2264
- import QRCode from "qrcode";
2343
+ import { hostname as hostname3 } from "node:os";
2344
+ import QRCode2 from "qrcode";
2265
2345
 
2266
2346
  // packages/cli/dist/config.js
2267
2347
  import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -2423,121 +2503,10 @@ function trimUrl(url) {
2423
2503
  return url.trim().replace(/\/+$/, "");
2424
2504
  }
2425
2505
 
2426
- // packages/cli/dist/commands/auth.js
2427
- function adminApi(ctx, flags) {
2428
- const dataDir = flags.dataDir ?? ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
2429
- const token = readAdminToken(dataDir, ctx.env);
2430
- if (!token) {
2431
- throw new CliError(`No admin token: run this on the coordinator machine (looked in ${dataDir}/admin.token) or set SWITCHYARD_ADMIN_TOKEN`);
2432
- }
2433
- const url = trimUrl(flags.coordinator ?? readEndpoint(dataDir)?.localUrl ?? DEFAULT_LOCAL_URL);
2434
- return new ApiClient({ coordinatorUrl: url, credential: token, fetch: ctx.fetch });
2435
- }
2436
- function clockTime(iso) {
2437
- const d = new Date(iso);
2438
- return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
2439
- }
2440
- async function printDeviceLink(ctx, url) {
2441
- const qr = await QRCode.toString(url, { type: "terminal", small: true });
2442
- ctx.out("");
2443
- ctx.out(` ${url}`);
2444
- ctx.out("");
2445
- ctx.out(qr.trimEnd());
2446
- ctx.out("");
2447
- }
2448
- async function printInit(ctx, r) {
2449
- if (r.alreadyInitialized) {
2450
- ctx.out("Already set up. New single-use sign-in link for one device:");
2451
- await printDeviceLink(ctx, r.enrollment.url);
2452
- return;
2453
- }
2454
- ctx.out(`Switchyard is set up. Open this single-use link on your first device (expires ${clockTime(r.enrollment.expiresAt)}):`);
2455
- await printDeviceLink(ctx, r.enrollment.url);
2456
- ctx.out("Then add a passkey in Settings \u203A Sign-in so other devices can sign in without a link.");
2457
- }
2458
- async function init(ctx, flags) {
2459
- const api = adminApi(ctx, flags);
2460
- const r = await api.post("/api/admin/init", {});
2461
- if (flags.json) {
2462
- ctx.out(JSON.stringify(r, null, 2));
2463
- return r;
2464
- }
2465
- await printInit(ctx, r);
2466
- return r;
2467
- }
2468
- async function pair(ctx, flags) {
2469
- const api = adminApi(ctx, flags);
2470
- const kind = flags.host ? "host" : "device";
2471
- const e = await api.post("/api/enrollments", { kind });
2472
- if (flags.json) {
2473
- ctx.out(JSON.stringify(e, null, 2));
2474
- return e;
2475
- }
2476
- const qr = await QRCode.toString(e.url, { type: "terminal", small: true });
2477
- if (kind === "host") {
2478
- ctx.out(`Host enrollment (single use, expires ${e.expiresAt}). On the host, run:`);
2479
- ctx.out("");
2480
- ctx.out(` ${e.command ?? `switchyard host enroll --coordinator <url> --token ${e.token}`}`);
2481
- ctx.out("");
2482
- ctx.out(`Token: ${e.token}`);
2483
- } else {
2484
- ctx.out(`Open this link on the device to enroll it (single use, expires ${e.expiresAt}):`);
2485
- ctx.out("");
2486
- ctx.out(` ${e.url}`);
2487
- ctx.out("");
2488
- ctx.out(qr.trimEnd());
2489
- ctx.out("");
2490
- ctx.out(`Terminal: switchyard login --coordinator ${originOf(e.url)} --token ${e.token}`);
2491
- }
2492
- ctx.out("The link does not provide network access: the device must already reach this URL.");
2493
- return e;
2494
- }
2495
- function originOf(url) {
2496
- try {
2497
- return new URL(url).origin;
2498
- } catch {
2499
- return url;
2500
- }
2501
- }
2502
- function parseEnrollmentInput(token, coordinator) {
2503
- const t = token.trim();
2504
- if (/^https?:\/\//.test(t)) {
2505
- const u = new URL(t);
2506
- const params = new URLSearchParams(u.hash.replace(/^#/, ""));
2507
- const fromUrl = params.get("token") ?? u.searchParams.get("token");
2508
- if (!fromUrl)
2509
- throw new CliError("That URL has no enrollment token");
2510
- return { url: trimUrl(coordinator ?? u.origin), token: fromUrl };
2511
- }
2512
- if (!coordinator)
2513
- throw new CliError("--coordinator <url> is required");
2514
- return { url: trimUrl(coordinator), token: t };
2515
- }
2516
- async function login(ctx, flags) {
2517
- const { url, token } = parseEnrollmentInput(flags.token, flags.coordinator);
2518
- const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
2519
- const name = flags.name ?? `${hostname().replace(/\.local$/, "")} terminal`;
2520
- const res = await api.post("/api/enroll", {
2521
- token,
2522
- name,
2523
- kind: "terminal"
2524
- });
2525
- if (!res.credential)
2526
- throw new CliError("The coordinator returned no credential");
2527
- const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
2528
- writeClientConfig(path, {
2529
- coordinatorUrl: url,
2530
- credential: res.credential,
2531
- deviceId: res.deviceId
2532
- });
2533
- ctx.out(`Enrolled "${name}" (${res.deviceId}) with ${url}.`);
2534
- ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
2535
- return { path, deviceId: res.deviceId };
2536
- }
2537
-
2538
- // packages/cli/dist/commands/coordinator.js
2539
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "node:fs";
2540
- import { resolve as resolve4 } from "node:path";
2506
+ // packages/cli/dist/commands/up.js
2507
+ import { spawn as spawn4 } from "node:child_process";
2508
+ import { existsSync as existsSync14 } from "node:fs";
2509
+ import { hostname as hostname2 } from "node:os";
2541
2510
 
2542
2511
  // packages/coordinator/dist/attention/router.js
2543
2512
  var DESKTOP_RECENT_MS = 5 * 6e4;
@@ -2835,12 +2804,10 @@ var Auth = class {
2835
2804
  store;
2836
2805
  config;
2837
2806
  hub;
2838
- owner;
2839
- constructor(store, config, hub, owner) {
2807
+ constructor(store, config, hub) {
2840
2808
  this.store = store;
2841
2809
  this.config = config;
2842
2810
  this.hub = hub;
2843
- this.owner = owner;
2844
2811
  }
2845
2812
  get adminTokenPath() {
2846
2813
  return join3(this.config.dataDir, "admin.token");
@@ -2880,14 +2847,13 @@ var Auth = class {
2880
2847
  return this.store.devices.count("is_admin = 0 AND revoked_at IS NULL");
2881
2848
  }
2882
2849
  enrollmentUrl(token, kind) {
2883
- return kind === "device" ? `${this.config.publicUrl}/enroll#token=${token}` : `${this.config.publicUrl}/enroll#token=${token}&kind=host`;
2850
+ return kind === "device" ? `${this.config.publicUrl}/sign-in#token=${token}` : `${this.config.publicUrl}/sign-in#token=${token}&kind=host`;
2884
2851
  }
2885
2852
  hostEnrollCommand(token) {
2886
2853
  return `switchyard host enroll --coordinator ${this.config.publicUrl} --token ${token}`;
2887
2854
  }
2888
2855
  /**
2889
- * Create a single-use enrollment (device 10 min, host 30 min by default). An enrollment created
2890
- * by a device (the local admin included) also marks the server set up.
2856
+ * Create a single-use enrollment (device 10 min, host 30 min by default).
2891
2857
  */
2892
2858
  createEnrollment(kind, createdBy, ttlMs = kind === "device" ? DEVICE_ENROLLMENT_TTL_MS : HOST_ENROLLMENT_TTL_MS) {
2893
2859
  const token = newToken();
@@ -2895,8 +2861,6 @@ var Auth = class {
2895
2861
  const now = this.store.now();
2896
2862
  const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
2897
2863
  this.store.tx(() => {
2898
- if (createdBy !== null)
2899
- this.owner.markInitialized();
2900
2864
  this.store.insertEnrollment({
2901
2865
  id,
2902
2866
  kind,
@@ -4278,7 +4242,7 @@ function resolveProfile(ctx, profileId, project) {
4278
4242
  const id = profileId ?? project?.defaultProfileId ?? null;
4279
4243
  if (id)
4280
4244
  return ctx.store.profiles.require(id);
4281
- const first = ctx.store.profiles.list(void 0, [], "created_at")[0];
4245
+ const first = ctx.store.profiles.list(void 0, [], "created_at, rowid")[0];
4282
4246
  if (!first)
4283
4247
  throw new SwitchyardError("invalid_request", "No Pi profile configured");
4284
4248
  return first;
@@ -4399,7 +4363,7 @@ function createSession(ctx, input) {
4399
4363
  nativeHandle: null,
4400
4364
  profileSnapshot: profile,
4401
4365
  runtime: "starting",
4402
- runtimeDetail: null,
4366
+ runtimeDetail: workspace ? "Starting Pi\u2026" : "Creating worktree\u2026",
4403
4367
  connectivity: host.connectivity,
4404
4368
  runId: null,
4405
4369
  taskId: null,
@@ -4415,19 +4379,27 @@ function createSession(ctx, input) {
4415
4379
  ctx.store.putSession(session);
4416
4380
  const choice = input.workspace;
4417
4381
  const started = (async () => {
4382
+ const t0 = Date.now();
4418
4383
  try {
4419
4384
  let s = session;
4420
4385
  if (!("existingId" in choice)) {
4421
4386
  const ws = await createWorkspace(ctx, project, checkout, choice.newBranch, choice.base);
4387
+ ctx.log.info({ sessionId: session.id, ms: Date.now() - t0 }, "session start: worktree ready");
4422
4388
  s = ctx.store.tx(() => {
4423
4389
  const cur = ctx.store.sessions.require(session.id);
4424
- return ctx.store.putSession({ ...cur, workspaceId: ws.id, cwd: ws.path });
4390
+ return ctx.store.putSession({
4391
+ ...cur,
4392
+ workspaceId: ws.id,
4393
+ cwd: ws.path,
4394
+ runtimeDetail: "Starting Pi\u2026"
4395
+ });
4425
4396
  });
4426
4397
  }
4427
4398
  await startOnHost(ctx, s, {
4428
4399
  initialPrompt: input.prompt.trim() ? input.prompt : null,
4429
4400
  nativeHandle: null
4430
4401
  });
4402
+ ctx.log.info({ sessionId: session.id, ms: Date.now() - t0 }, "session start: started on host");
4431
4403
  } catch (err) {
4432
4404
  const message = err instanceof Error ? err.message : String(err);
4433
4405
  ctx.log.warn({ err, sessionId: session.id }, "session start failed");
@@ -7268,10 +7240,23 @@ INSERT OR IGNORE INTO settings(key, value)
7268
7240
  WHERE EXISTS (SELECT 1 FROM devices WHERE is_admin = 0 AND revoked_at IS NULL);
7269
7241
  `;
7270
7242
 
7243
+ // packages/coordinator/dist/db/migrations/003_terminal_login.js
7244
+ var sql3 = `
7245
+ DROP TABLE auth_flows;
7246
+
7247
+ CREATE TABLE auth_flows (
7248
+ id TEXT PRIMARY KEY,
7249
+ kind TEXT NOT NULL CHECK (kind IN ('passkey_register','passkey_auth','oidc','terminal_login')),
7250
+ data TEXT NOT NULL,
7251
+ expires_at TEXT NOT NULL
7252
+ );
7253
+ `;
7254
+
7271
7255
  // packages/coordinator/dist/db/index.js
7272
7256
  var MIGRATIONS = [
7273
7257
  { version: 1, name: "001_init", sql },
7274
- { version: 2, name: "002_owner_auth", sql: sql2 }
7258
+ { version: 2, name: "002_owner_auth", sql: sql2 },
7259
+ { version: 3, name: "003_terminal_login", sql: sql3 }
7275
7260
  ];
7276
7261
  var LATEST_MIGRATION = MIGRATIONS[MIGRATIONS.length - 1].version;
7277
7262
  function openDb(path) {
@@ -7437,6 +7422,9 @@ var HOST_SESSION_FIELDS = [
7437
7422
  "runtimeDetail",
7438
7423
  "queue",
7439
7424
  "stats",
7425
+ "model",
7426
+ "thinking",
7427
+ "compacting",
7440
7428
  "lastAssistantText",
7441
7429
  "lastActivityAt",
7442
7430
  "terminal"
@@ -7955,7 +7943,7 @@ var HostHub = class {
7955
7943
  projectId: ws?.projectId ?? null,
7956
7944
  cwd: "",
7957
7945
  nativeHandle: null,
7958
- profileSnapshot: this.store.profiles.list(void 0, [], "created_at")[0] ?? FALLBACK_PROFILE,
7946
+ profileSnapshot: this.store.profiles.list(void 0, [], "created_at, rowid")[0] ?? FALLBACK_PROFILE,
7959
7947
  runtime: "idle",
7960
7948
  runtimeDetail: null,
7961
7949
  runId: null,
@@ -8108,8 +8096,11 @@ import * as client from "openid-client";
8108
8096
  // packages/coordinator/dist/owner.js
8109
8097
  import { randomBytes as randomBytes2 } from "node:crypto";
8110
8098
  import { isIP } from "node:net";
8111
- var OWNER_INITIALIZED_KEY = "owner.initialized_at";
8112
8099
  var OWNER_WEBAUTHN_USER_KEY = "owner.webauthn_user_id";
8100
+ function isLoopbackUrl(url) {
8101
+ const host = new URL(url).hostname.replace(/^\[|\]$/g, "");
8102
+ return host === "localhost" || host === "::1" || /^127\.\d+\.\d+\.\d+$/.test(host);
8103
+ }
8113
8104
  var Owner = class {
8114
8105
  store;
8115
8106
  config;
@@ -8117,18 +8108,6 @@ var Owner = class {
8117
8108
  this.store = store;
8118
8109
  this.config = config;
8119
8110
  }
8120
- isInitialized() {
8121
- return this.config.oidc !== null || this.store.getSetting(OWNER_INITIALIZED_KEY, null) !== null;
8122
- }
8123
- /** Mark the server set up. Returns true when this call set it. */
8124
- markInitialized() {
8125
- return this.store.tx(() => {
8126
- if (this.store.getSetting(OWNER_INITIALIZED_KEY, null) !== null)
8127
- return false;
8128
- this.store.setSetting(OWNER_INITIALIZED_KEY, this.store.nowIso());
8129
- return true;
8130
- });
8131
- }
8132
8111
  /** Stable random WebAuthn user handle for the owner (16 bytes, created lazily). */
8133
8112
  webauthnUserId() {
8134
8113
  return this.store.tx(() => {
@@ -8788,53 +8767,6 @@ function registerAttentionRoutes(app, ctx) {
8788
8767
  app.post("/api/attention/:id/acknowledge", async (req) => ctx.attention.acknowledge(req.params.id));
8789
8768
  }
8790
8769
 
8791
- // packages/coordinator/dist/routes/devices.js
8792
- import QRCode2 from "qrcode";
8793
- import { z as z14 } from "zod";
8794
- var UpdateDeviceRequest = z14.object({
8795
- name: z14.string().min(1).max(80).optional(),
8796
- preferredMobile: z14.boolean().optional()
8797
- });
8798
- async function enrollmentResponse(ctx, kind, createdBy) {
8799
- const e = ctx.auth.createEnrollment(kind, createdBy);
8800
- const qrSvg = await QRCode2.toString(e.url, { type: "svg", margin: 1 });
8801
- return { token: e.token, url: e.url, expiresAt: e.expiresAt, qrSvg, command: e.command };
8802
- }
8803
- function publicDevice(ctx, id) {
8804
- return ctx.store.devices.require(id);
8805
- }
8806
- function registerDeviceRoutes(app, ctx) {
8807
- app.post("/api/enrollments", async (req, reply) => {
8808
- const body = parse(CreateEnrollmentRequest, req.body);
8809
- return reply.code(201).send(await enrollmentResponse(ctx, body.kind, deviceOf(req).id));
8810
- });
8811
- app.post("/api/devices/enrollments", async (req, reply) => reply.code(201).send(await enrollmentResponse(ctx, "device", deviceOf(req).id)));
8812
- app.get("/api/devices", async () => ctx.store.devices.list("is_admin = 0", [], "created_at"));
8813
- app.get("/api/devices/:id", async (req) => publicDevice(ctx, req.params.id));
8814
- app.patch("/api/devices/:id", async (req) => {
8815
- const { id } = req.params;
8816
- const body = parse(UpdateDeviceRequest, req.body);
8817
- return ctx.store.tx(() => {
8818
- const d = ctx.store.devices.require(id);
8819
- if (body.preferredMobile) {
8820
- for (const other of ctx.store.devices.list("is_admin = 0")) {
8821
- if (other.id !== id && other.preferredMobile) {
8822
- ctx.store.putDevice({ ...other, preferredMobile: false });
8823
- }
8824
- }
8825
- }
8826
- return ctx.store.putDevice({
8827
- ...d,
8828
- name: body.name ?? d.name,
8829
- preferredMobile: body.preferredMobile ?? d.preferredMobile
8830
- });
8831
- });
8832
- });
8833
- const revoke = async (req) => ctx.auth.revokeDevice(req.params.id);
8834
- app.post("/api/devices/:id/revoke", revoke);
8835
- app.delete("/api/devices/:id", revoke);
8836
- }
8837
-
8838
8770
  // packages/coordinator/dist/routes/auth.js
8839
8771
  var HANDOFF_PAGE = `<!doctype html>
8840
8772
  <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
@@ -8845,7 +8777,6 @@ var HANDOFF_PAGE = `<!doctype html>
8845
8777
  function setupState(ctx) {
8846
8778
  const availability = ctx.owner.passkeyAvailability();
8847
8779
  return {
8848
- initialized: ctx.owner.isInitialized(),
8849
8780
  passkey: {
8850
8781
  available: availability.available,
8851
8782
  registered: ctx.passkeys.count() > 0,
@@ -8925,6 +8856,18 @@ function registerAuthRoutes(app, ctx) {
8925
8856
  ctx.auth.setDeviceCookie(reply, r.credential);
8926
8857
  return { status: "done", deviceId: r.deviceId };
8927
8858
  });
8859
+ app.post("/api/auth/terminal/start", async (req, reply) => {
8860
+ limitAuth(req);
8861
+ const body = parse(TerminalLoginStartRequest, req.body);
8862
+ return reply.code(201).send(ctx.terminalLogins.start(body.name, req.ip));
8863
+ });
8864
+ app.post("/api/auth/terminal/claim", async (req) => {
8865
+ const body = parse(TerminalLoginClaimRequest, req.body);
8866
+ return ctx.terminalLogins.claim(body.userCode, body.pollToken);
8867
+ });
8868
+ app.get("/api/auth/terminal/requests/:code", async (req) => ctx.terminalLogins.describe(req.params.code));
8869
+ app.post("/api/auth/terminal/requests/:code/approve", async (req) => ctx.terminalLogins.approve(req.params.code));
8870
+ app.post("/api/auth/terminal/requests/:code/deny", async (req) => ctx.terminalLogins.deny(req.params.code));
8928
8871
  app.get("/api/auth/methods", async () => {
8929
8872
  const availability = ctx.owner.passkeyAvailability();
8930
8873
  return {
@@ -8943,15 +8886,6 @@ function registerAuthRoutes(app, ctx) {
8943
8886
  ctx.passkeys.remove(req.params.id);
8944
8887
  return { ok: true };
8945
8888
  });
8946
- app.post("/api/admin/init", async (req) => {
8947
- const device = deviceOf(req);
8948
- if (!device.isAdmin || device.id !== ADMIN_DEVICE_ID) {
8949
- throw new SwitchyardError("forbidden", "Only the server's local admin token can initialize");
8950
- }
8951
- const alreadyInitialized = !ctx.owner.markInitialized();
8952
- const enrollment = await enrollmentResponse(ctx, "device", device.id);
8953
- return { alreadyInitialized, enrollment };
8954
- });
8955
8889
  }
8956
8890
  function redirectError(ctx, req, reply, err) {
8957
8891
  const code = err instanceof SwitchyardError ? err.code : "oidc_failed";
@@ -8961,7 +8895,54 @@ function redirectError(ctx, req, reply, err) {
8961
8895
  else if (err.code === "forbidden")
8962
8896
  ctx.log.info({ detail }, "OIDC sign-in refused");
8963
8897
  const qs = new URLSearchParams({ error: code, detail });
8964
- return reply.redirect(`/enroll?${qs.toString()}`, 302);
8898
+ return reply.redirect(`/sign-in?${qs.toString()}`, 302);
8899
+ }
8900
+
8901
+ // packages/coordinator/dist/routes/devices.js
8902
+ import QRCode from "qrcode";
8903
+ import { z as z14 } from "zod";
8904
+ var UpdateDeviceRequest = z14.object({
8905
+ name: z14.string().min(1).max(80).optional(),
8906
+ preferredMobile: z14.boolean().optional()
8907
+ });
8908
+ async function enrollmentResponse(ctx, kind, createdBy) {
8909
+ const e = ctx.auth.createEnrollment(kind, createdBy);
8910
+ const qrSvg = await QRCode.toString(e.url, { type: "svg", margin: 1 });
8911
+ return { token: e.token, url: e.url, expiresAt: e.expiresAt, qrSvg, command: e.command };
8912
+ }
8913
+ function publicDevice(ctx, id) {
8914
+ return ctx.store.devices.require(id);
8915
+ }
8916
+ function registerDeviceRoutes(app, ctx) {
8917
+ app.post("/api/enrollments", async (req, reply) => {
8918
+ const body = parse(CreateEnrollmentRequest, req.body);
8919
+ return reply.code(201).send(await enrollmentResponse(ctx, body.kind, deviceOf(req).id));
8920
+ });
8921
+ app.post("/api/devices/enrollments", async (req, reply) => reply.code(201).send(await enrollmentResponse(ctx, "device", deviceOf(req).id)));
8922
+ app.get("/api/devices", async () => ctx.store.devices.list("is_admin = 0", [], "created_at"));
8923
+ app.get("/api/devices/:id", async (req) => publicDevice(ctx, req.params.id));
8924
+ app.patch("/api/devices/:id", async (req) => {
8925
+ const { id } = req.params;
8926
+ const body = parse(UpdateDeviceRequest, req.body);
8927
+ return ctx.store.tx(() => {
8928
+ const d = ctx.store.devices.require(id);
8929
+ if (body.preferredMobile) {
8930
+ for (const other of ctx.store.devices.list("is_admin = 0")) {
8931
+ if (other.id !== id && other.preferredMobile) {
8932
+ ctx.store.putDevice({ ...other, preferredMobile: false });
8933
+ }
8934
+ }
8935
+ }
8936
+ return ctx.store.putDevice({
8937
+ ...d,
8938
+ name: body.name ?? d.name,
8939
+ preferredMobile: body.preferredMobile ?? d.preferredMobile
8940
+ });
8941
+ });
8942
+ });
8943
+ const revoke = async (req) => ctx.auth.revokeDevice(req.params.id);
8944
+ app.post("/api/devices/:id/revoke", revoke);
8945
+ app.delete("/api/devices/:id", revoke);
8965
8946
  }
8966
8947
 
8967
8948
  // packages/coordinator/dist/routes/hosts.js
@@ -9133,23 +9114,39 @@ function registerOpsRoutes(app, ctx) {
9133
9114
 
9134
9115
  // packages/coordinator/dist/routes/profiles.js
9135
9116
  var ProfilePatch = Profile.omit({ id: true }).partial();
9136
- function seedDefaultProfile(ctx) {
9117
+ function seedDefaultProfiles(ctx) {
9137
9118
  ctx.store.tx(() => {
9138
- if (ctx.store.profiles.count() > 0)
9119
+ if (ctx.store.profiles.count() === 0) {
9120
+ ctx.store.putProfile({
9121
+ id: newId("prf"),
9122
+ name: "Default",
9123
+ provider: null,
9124
+ model: null,
9125
+ thinking: null,
9126
+ projectTrust: "ask",
9127
+ extraArgs: []
9128
+ });
9129
+ }
9130
+ if (ctx.store.getSetting("profiles.vanillaSeeded", false))
9139
9131
  return;
9140
- ctx.store.putProfile({
9141
- id: newId("prf"),
9142
- name: "Default",
9143
- provider: null,
9144
- model: null,
9145
- thinking: null,
9146
- projectTrust: "ask",
9147
- extraArgs: []
9148
- });
9132
+ const has = ctx.store.profiles.list().some((p) => p.resources === "vanilla");
9133
+ if (!has) {
9134
+ ctx.store.putProfile({
9135
+ id: newId("prf"),
9136
+ name: "Vanilla",
9137
+ provider: null,
9138
+ model: null,
9139
+ thinking: null,
9140
+ projectTrust: "ask",
9141
+ extraArgs: [],
9142
+ resources: "vanilla"
9143
+ });
9144
+ }
9145
+ ctx.store.setSetting("profiles.vanillaSeeded", true);
9149
9146
  });
9150
9147
  }
9151
9148
  function registerProfileRoutes(app, ctx) {
9152
- app.get("/api/profiles", async () => ctx.store.profiles.list(void 0, [], "created_at"));
9149
+ app.get("/api/profiles", async () => ctx.store.profiles.list(void 0, [], "created_at, rowid"));
9153
9150
  app.get("/api/profiles/:id", async (req) => ctx.store.profiles.require(req.params.id));
9154
9151
  app.post("/api/profiles", async (req, reply) => {
9155
9152
  const body = parse(ProfileInput, req.body);
@@ -9316,6 +9313,20 @@ function registerProjectRoutes(app, ctx) {
9316
9313
 
9317
9314
  // packages/coordinator/dist/routes/sessions.js
9318
9315
  import { z as z16 } from "zod";
9316
+ function requireLiveSession(ctx, s) {
9317
+ if (s.readOnly)
9318
+ throw new SwitchyardError("forbidden", SUBAGENT_READ_ONLY);
9319
+ if (!LIVE_RUNTIMES2.includes(s.runtime) || s.runtime === "starting") {
9320
+ throw new SwitchyardError("conflict", s.runtime === "starting" ? "Session is still starting" : `Session ${s.runtime}: Resume to send`);
9321
+ }
9322
+ requireConnected(ctx, s.hostId);
9323
+ }
9324
+ function requireSessionControls(ctx, s) {
9325
+ requireLiveSession(ctx, s);
9326
+ if (ctx.store.hosts.get(s.hostId)?.capabilities?.sessionControls !== true) {
9327
+ throw new SwitchyardError("invalid_request", "Update Switchyard on this host to change the model, effort or commands from here");
9328
+ }
9329
+ }
9319
9330
  var ListQuery5 = z16.object({
9320
9331
  projectId: z16.string().optional(),
9321
9332
  hostId: z16.string().optional(),
@@ -9449,12 +9460,7 @@ function registerSessionRoutes(app, ctx) {
9449
9460
  app.post("/api/sessions/:id/prompt", async (req) => {
9450
9461
  const s = get(idOf(req));
9451
9462
  const body = parse(PromptRequest, req.body);
9452
- if (s.readOnly)
9453
- throw new SwitchyardError("forbidden", SUBAGENT_READ_ONLY);
9454
- if (!LIVE_RUNTIMES2.includes(s.runtime) || s.runtime === "starting") {
9455
- throw new SwitchyardError("conflict", s.runtime === "starting" ? "Session is still starting" : `Session ${s.runtime}: Resume to send`);
9456
- }
9457
- requireConnected(ctx, s.hostId);
9463
+ requireLiveSession(ctx, s);
9458
9464
  const run = ctx.store.runForSession(s);
9459
9465
  const result = await ctx.hostHub.request(s.hostId, {
9460
9466
  type: "session.prompt",
@@ -9472,9 +9478,56 @@ function registerSessionRoutes(app, ctx) {
9472
9478
  automationActive: run?.controlMode === "automation"
9473
9479
  };
9474
9480
  });
9475
- app.post("/api/sessions/:id/abort", async (req) => {
9481
+ app.get("/api/sessions/:id/models", async (req) => {
9476
9482
  const s = get(idOf(req));
9477
- const body = parse(AbortRequest, req.body);
9483
+ requireSessionControls(ctx, s);
9484
+ return SessionModelsResult.parse(await ctx.hostHub.request(s.hostId, { type: "session.models", sessionId: s.id }));
9485
+ });
9486
+ app.get("/api/sessions/:id/commands", async (req) => {
9487
+ const s = get(idOf(req));
9488
+ requireSessionControls(ctx, s);
9489
+ return SessionCommandsResult.parse(await ctx.hostHub.request(s.hostId, { type: "session.commands", sessionId: s.id }));
9490
+ });
9491
+ app.post("/api/sessions/:id/model", async (req) => {
9492
+ const s = get(idOf(req));
9493
+ const body = parse(SetModelRequest, req.body);
9494
+ requireSessionControls(ctx, s);
9495
+ return ModelStateResult.parse(await ctx.hostHub.request(s.hostId, {
9496
+ type: "session.setModel",
9497
+ sessionId: s.id,
9498
+ provider: body.provider,
9499
+ modelId: body.modelId
9500
+ }));
9501
+ });
9502
+ app.post("/api/sessions/:id/thinking", async (req) => {
9503
+ const s = get(idOf(req));
9504
+ const body = parse(SetThinkingRequest, req.body);
9505
+ requireSessionControls(ctx, s);
9506
+ return ModelStateResult.parse(await ctx.hostHub.request(s.hostId, {
9507
+ type: "session.setThinking",
9508
+ sessionId: s.id,
9509
+ level: body.level
9510
+ }));
9511
+ });
9512
+ app.post("/api/sessions/:id/compact", async (req) => {
9513
+ const s = get(idOf(req));
9514
+ const body = parse(CompactRequest, req.body ?? {});
9515
+ requireSessionControls(ctx, s);
9516
+ if (s.runtime !== "idle") {
9517
+ throw new SwitchyardError("conflict", "Wait for the run to finish before compacting");
9518
+ }
9519
+ if (s.compacting)
9520
+ throw new SwitchyardError("conflict", "Compaction is already running");
9521
+ await ctx.hostHub.request(s.hostId, {
9522
+ type: "session.compact",
9523
+ sessionId: s.id,
9524
+ instructions: body.instructions?.trim() || null
9525
+ });
9526
+ return { ok: true };
9527
+ });
9528
+ app.post("/api/sessions/:id/abort", async (req) => {
9529
+ const s = get(idOf(req));
9530
+ const body = parse(AbortRequest, req.body);
9478
9531
  requireConnected(ctx, s.hostId);
9479
9532
  await ctx.hostHub.request(s.hostId, {
9480
9533
  type: "session.abort",
@@ -9673,11 +9726,11 @@ var Statements = class {
9673
9726
  constructor(db) {
9674
9727
  this.db = db;
9675
9728
  }
9676
- get(sql3) {
9677
- let s = this.cache.get(sql3);
9729
+ get(sql4) {
9730
+ let s = this.cache.get(sql4);
9678
9731
  if (!s) {
9679
- s = this.db.prepare(sql3);
9680
- this.cache.set(sql3, s);
9732
+ s = this.db.prepare(sql4);
9733
+ this.cache.set(sql4, s);
9681
9734
  }
9682
9735
  return s;
9683
9736
  }
@@ -9708,12 +9761,12 @@ var JsonRepo = class {
9708
9761
  }
9709
9762
  /** List with an optional SQL `WHERE` fragment over the indexed columns. */
9710
9763
  list(where, params = [], orderBy = "rowid") {
9711
- const sql3 = `SELECT json FROM ${this.table}${where ? ` WHERE ${where}` : ""} ORDER BY ${orderBy}`;
9712
- return this.st.get(sql3).all(...params).map((r) => JSON.parse(r.json));
9764
+ const sql4 = `SELECT json FROM ${this.table}${where ? ` WHERE ${where}` : ""} ORDER BY ${orderBy}`;
9765
+ return this.st.get(sql4).all(...params).map((r) => JSON.parse(r.json));
9713
9766
  }
9714
9767
  count(where, params = []) {
9715
- const sql3 = `SELECT COUNT(*) AS n FROM ${this.table}${where ? ` WHERE ${where}` : ""}`;
9716
- return this.st.get(sql3).get(...params).n;
9768
+ const sql4 = `SELECT COUNT(*) AS n FROM ${this.table}${where ? ` WHERE ${where}` : ""}`;
9769
+ return this.st.get(sql4).get(...params).n;
9717
9770
  }
9718
9771
  /**
9719
9772
  * Update by id, or insert when missing. `extra` columns are written in both cases when given
@@ -10356,7 +10409,7 @@ var Store2 = class {
10356
10409
  projects: this.projects.list(void 0, [], "created_at"),
10357
10410
  checkouts: this.checkouts.list(),
10358
10411
  workspaces: this.workspaces.list("archived_at IS NULL"),
10359
- profiles: this.profiles.list(void 0, [], "created_at"),
10412
+ profiles: this.profiles.list(void 0, [], "created_at, rowid"),
10360
10413
  sessions,
10361
10414
  tasks,
10362
10415
  runs,
@@ -10459,6 +10512,126 @@ var TerminalHub = class {
10459
10512
  }
10460
10513
  };
10461
10514
 
10515
+ // packages/coordinator/dist/terminal-login.js
10516
+ import { randomInt } from "node:crypto";
10517
+ var TERMINAL_LOGIN_TTL_MS = 10 * 6e4;
10518
+ var TERMINAL_LOGIN_POLL_MS = 2e3;
10519
+ var CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ";
10520
+ function normalizeUserCode(s) {
10521
+ return s.toUpperCase().replace(/[^A-Z]/g, "");
10522
+ }
10523
+ function formatUserCode(n) {
10524
+ return `${n.slice(0, 4)}-${n.slice(4)}`;
10525
+ }
10526
+ var TerminalLogins = class {
10527
+ store;
10528
+ config;
10529
+ auth;
10530
+ flows;
10531
+ constructor(store, config, auth) {
10532
+ this.store = store;
10533
+ this.config = config;
10534
+ this.auth = auth;
10535
+ this.flows = new AuthFlows(store);
10536
+ }
10537
+ load(code) {
10538
+ const id = normalizeUserCode(code);
10539
+ const flow = this.flows.get("terminal_login", id);
10540
+ if (!flow) {
10541
+ throw new SwitchyardError("not_found", "This sign-in request expired or does not exist. Run `switchyard login` again.");
10542
+ }
10543
+ return { id, flow };
10544
+ }
10545
+ view(id, flow) {
10546
+ return {
10547
+ userCode: formatUserCode(id),
10548
+ name: flow.name,
10549
+ requestedFrom: flow.requestedFrom,
10550
+ createdAt: flow.createdAt,
10551
+ expiresAt: flow.expiresAt,
10552
+ status: flow.status
10553
+ };
10554
+ }
10555
+ start(name, requestedFrom) {
10556
+ let id = "";
10557
+ for (let attempt = 0; attempt < 5 && !id; attempt++) {
10558
+ let candidate = "";
10559
+ for (let i = 0; i < 8; i++)
10560
+ candidate += CODE_ALPHABET[randomInt(0, CODE_ALPHABET.length)];
10561
+ if (!this.flows.get("terminal_login", candidate))
10562
+ id = candidate;
10563
+ }
10564
+ if (!id)
10565
+ throw new SwitchyardError("internal", "Could not allocate a sign-in code");
10566
+ const pollToken = newToken();
10567
+ const now = this.store.now();
10568
+ const createdAt = now.toISOString();
10569
+ const expiresAt = new Date(now.getTime() + TERMINAL_LOGIN_TTL_MS).toISOString();
10570
+ const flow = {
10571
+ name,
10572
+ requestedFrom,
10573
+ createdAt,
10574
+ expiresAt,
10575
+ pollTokenHash: sha256(pollToken),
10576
+ status: "pending",
10577
+ result: null
10578
+ };
10579
+ this.flows.put("terminal_login", id, flow, TERMINAL_LOGIN_TTL_MS);
10580
+ const userCode = formatUserCode(id);
10581
+ return {
10582
+ userCode,
10583
+ pollToken,
10584
+ verifyUrl: `${this.config.publicUrl}/terminal-login?code=${userCode}`,
10585
+ expiresAt,
10586
+ intervalMs: TERMINAL_LOGIN_POLL_MS
10587
+ };
10588
+ }
10589
+ describe(code) {
10590
+ const { id, flow } = this.load(code);
10591
+ return this.view(id, flow);
10592
+ }
10593
+ approve(code) {
10594
+ return this.store.tx(() => {
10595
+ const { id, flow } = this.load(code);
10596
+ if (flow.status !== "pending") {
10597
+ throw new SwitchyardError("invalid_request", "This sign-in was already approved or denied");
10598
+ }
10599
+ const { device, credential } = this.auth.createDevice(flow.name, "terminal", "approval");
10600
+ const next = {
10601
+ ...flow,
10602
+ status: "approved",
10603
+ result: { deviceId: device.id, credential }
10604
+ };
10605
+ this.flows.update(id, next);
10606
+ return this.view(id, next);
10607
+ });
10608
+ }
10609
+ deny(code) {
10610
+ return this.store.tx(() => {
10611
+ const { id, flow } = this.load(code);
10612
+ if (flow.status !== "pending") {
10613
+ throw new SwitchyardError("invalid_request", "This sign-in was already approved or denied");
10614
+ }
10615
+ const next = { ...flow, status: "denied" };
10616
+ this.flows.update(id, next);
10617
+ return this.view(id, next);
10618
+ });
10619
+ }
10620
+ claim(code, pollToken) {
10621
+ const id = normalizeUserCode(code);
10622
+ const flow = this.flows.get("terminal_login", id);
10623
+ if (!flow || !safeEqualHex(sha256(pollToken), flow.pollTokenHash)) {
10624
+ throw new SwitchyardError("unauthorized", "Unknown or expired sign-in");
10625
+ }
10626
+ if (flow.status === "pending")
10627
+ return { status: "pending" };
10628
+ this.flows.delete(id);
10629
+ if (flow.status === "denied" || !flow.result)
10630
+ return { status: "denied" };
10631
+ return { status: "done", deviceId: flow.result.deviceId, credential: flow.result.credential };
10632
+ }
10633
+ };
10634
+
10462
10635
  // packages/coordinator/dist/server.js
10463
10636
  var MIN_HOST_ENROLL_SECRET_LENGTH = 32;
10464
10637
  var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
@@ -10469,6 +10642,8 @@ var PUBLIC_ROUTES = /* @__PURE__ */ new Set([
10469
10642
  "/api/auth/oidc/begin",
10470
10643
  "/api/auth/oidc/callback",
10471
10644
  "/api/auth/oidc/claim",
10645
+ "/api/auth/terminal/start",
10646
+ "/api/auth/terminal/claim",
10472
10647
  "/api/hosts/enroll",
10473
10648
  "/api/hooks/:automationId",
10474
10649
  "/healthz",
@@ -10490,13 +10665,16 @@ function isApiLikePath(url) {
10490
10665
  }
10491
10666
  return path.startsWith("/api/") || path.startsWith("/ws/") || path.startsWith("/adapter-frames/");
10492
10667
  }
10493
- function originOf2(url) {
10668
+ function originOf(url) {
10494
10669
  return new URL(url).origin;
10495
10670
  }
10496
10671
  function validateAuthOptions(opts) {
10497
10672
  if (opts.hostEnrollSecret !== void 0 && opts.hostEnrollSecret.length < MIN_HOST_ENROLL_SECRET_LENGTH) {
10498
10673
  throw new SwitchyardError("invalid_request", `SWITCHYARD_HOST_ENROLL_SECRET must be at least ${MIN_HOST_ENROLL_SECRET_LENGTH} characters`);
10499
10674
  }
10675
+ if (!opts.oidc && opts.publicUrl && !isLoopbackUrl(opts.publicUrl)) {
10676
+ throw new SwitchyardError("invalid_request", `Single sign-on is required for ${opts.publicUrl}: set SWITCHYARD_OIDC_ISSUER, SWITCHYARD_OIDC_CLIENT_ID and an owner (SWITCHYARD_OIDC_OWNER_*), see docs/auth.md. Without single sign-on, Switchyard runs only at a localhost public URL.`);
10677
+ }
10500
10678
  if (opts.oidc && !hasOwnerMatch(opts.oidc.owner)) {
10501
10679
  throw new SwitchyardError("invalid_request", "OIDC needs an owner match: set SWITCHYARD_OIDC_OWNER_SUBJECT, SWITCHYARD_OIDC_OWNER_EMAIL or SWITCHYARD_OIDC_OWNER_GROUP");
10502
10680
  }
@@ -10521,7 +10699,7 @@ function createCoordinator(opts) {
10521
10699
  const config = {
10522
10700
  dataDir,
10523
10701
  publicUrl,
10524
- trustedOrigins: opts.trustedOrigins ?? [originOf2(publicUrl)],
10702
+ trustedOrigins: opts.trustedOrigins ?? [originOf(publicUrl)],
10525
10703
  webDist: opts.webDist ? resolve3(opts.webDist) : null,
10526
10704
  secureCookies: publicUrl.startsWith("https:"),
10527
10705
  unreachableAfterMs: opts.unreachableAfterMs ?? 6e4,
@@ -10534,19 +10712,18 @@ function createCoordinator(opts) {
10534
10712
  hub.onListenerError = (name, err) => log.error({ err, event: name }, "event listener failed");
10535
10713
  const store = new Store2(db, hub, opts.clock);
10536
10714
  const owner = new Owner(store, config);
10537
- const auth = new Auth(store, config, hub, owner);
10715
+ const auth = new Auth(store, config, hub);
10538
10716
  const adminToken = auth.ensureAdminToken();
10539
- if (config.oidc)
10540
- owner.markInitialized();
10541
10717
  const passkeys = new Passkeys(store, owner, config, auth);
10542
10718
  const oidc = config.oidc ? new Oidc(store, owner, config, auth) : null;
10719
+ const terminalLogins = new TerminalLogins(store, config, auth);
10543
10720
  const limits = opts.rateLimits === false ? null : opts.rateLimits ?? {};
10544
10721
  const storeClock = () => store.now().getTime();
10545
10722
  const limiters = {
10546
10723
  auth: new RateLimiter(limits ? limits.auth ?? AUTH_RATE_LIMIT : Number.POSITIVE_INFINITY, 6e4, storeClock),
10547
10724
  hosts: new RateLimiter(limits ? limits.hosts ?? HOSTS_RATE_LIMIT : Number.POSITIVE_INFINITY, 6e4, storeClock)
10548
10725
  };
10549
- seedDefaultProfile({ store });
10726
+ seedDefaultProfiles({ store });
10550
10727
  const attention = new AttentionService(store, hub);
10551
10728
  const hostHub = new HostHub(store, hub, attention, log.child({ mod: "host-hub" }), {
10552
10729
  unreachableAfterMs: config.unreachableAfterMs,
@@ -10568,6 +10745,7 @@ function createCoordinator(opts) {
10568
10745
  owner,
10569
10746
  passkeys,
10570
10747
  oidc,
10748
+ terminalLogins,
10571
10749
  limiters,
10572
10750
  log,
10573
10751
  services: /* @__PURE__ */ new Map()
@@ -10675,10 +10853,10 @@ function createCoordinator(opts) {
10675
10853
  sweep();
10676
10854
  retentionTimer = setInterval(sweep, opts.retentionIntervalMs ?? 36e5);
10677
10855
  retentionTimer.unref();
10678
- if (!owner.isInitialized()) {
10679
- log.info("not set up yet: run `switchyard init` on this server");
10856
+ if (!config.oidc && auth.activeDeviceCount() === 0 && passkeys.count() === 0) {
10857
+ log.info("no device signed in yet: run `switchyard pair` on this machine");
10680
10858
  if (opts.setupHint !== false) {
10681
- process.stderr.write("Switchyard is not set up yet. Run `switchyard init` on this server to create the first sign-in link.\n");
10859
+ process.stderr.write("No device is signed in yet. Run `switchyard pair` on this machine for a single-use sign-in link.\n");
10682
10860
  }
10683
10861
  }
10684
10862
  },
@@ -10699,162 +10877,11 @@ function createCoordinator(opts) {
10699
10877
  return coordinator;
10700
10878
  }
10701
10879
 
10702
- // packages/cli/dist/commands/coordinator.js
10703
- function parseListen(v) {
10704
- const s = v.trim();
10705
- let host = "0.0.0.0";
10706
- let portStr = s;
10707
- const v6 = s.match(/^\[([^\]]+)\]:(\d+)$/);
10708
- if (v6) {
10709
- host = v6[1];
10710
- portStr = v6[2];
10711
- } else if (s.includes(":")) {
10712
- const i = s.lastIndexOf(":");
10713
- host = s.slice(0, i) || "0.0.0.0";
10714
- portStr = s.slice(i + 1);
10715
- } else if (!/^\d+$/.test(s)) {
10716
- host = s;
10717
- portStr = String(DEFAULT_PORT);
10718
- }
10719
- const port = Number(portStr);
10720
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
10721
- throw new CliError(`Invalid listen address "${v}" (expected host:port)`);
10722
- }
10723
- return { host, port };
10724
- }
10725
- function parseOrigins(v) {
10726
- if (!v?.trim())
10727
- return void 0;
10728
- return v.split(",").map((o) => o.trim().replace(/\/+$/, "")).filter(Boolean);
10729
- }
10730
- var DEFAULT_OIDC_LABEL = "Sign in with single sign-on";
10731
- var DEFAULT_OIDC_SCOPES = "openid profile email groups";
10732
- function envValue(env, key) {
10733
- const v = env[key]?.trim();
10734
- return v ? v : void 0;
10735
- }
10736
- function authOptionsFromEnv(env) {
10737
- const out = {};
10738
- const secret = envValue(env, "SWITCHYARD_HOST_ENROLL_SECRET");
10739
- if (secret)
10740
- out.hostEnrollSecret = secret;
10741
- const proxies = parseOrigins(env.SWITCHYARD_TRUSTED_PROXIES);
10742
- if (proxies)
10743
- out.trustedProxies = proxies;
10744
- const issuer = envValue(env, "SWITCHYARD_OIDC_ISSUER");
10745
- const clientId = envValue(env, "SWITCHYARD_OIDC_CLIENT_ID");
10746
- if (issuer && clientId) {
10747
- if (!/^https?:\/\//.test(issuer)) {
10748
- throw new CliError(`SWITCHYARD_OIDC_ISSUER must be an http(s) URL, got "${issuer}"`);
10749
- }
10750
- out.oidc = {
10751
- issuer,
10752
- clientId,
10753
- clientSecret: envValue(env, "SWITCHYARD_OIDC_CLIENT_SECRET") ?? null,
10754
- label: envValue(env, "SWITCHYARD_OIDC_LABEL") ?? DEFAULT_OIDC_LABEL,
10755
- scopes: envValue(env, "SWITCHYARD_OIDC_SCOPES") ?? DEFAULT_OIDC_SCOPES,
10756
- owner: {
10757
- subject: envValue(env, "SWITCHYARD_OIDC_OWNER_SUBJECT") ?? null,
10758
- email: envValue(env, "SWITCHYARD_OIDC_OWNER_EMAIL") ?? null,
10759
- group: envValue(env, "SWITCHYARD_OIDC_OWNER_GROUP") ?? null
10760
- }
10761
- };
10762
- } else if (issuer || clientId) {
10763
- throw new CliError("Single sign-on needs both SWITCHYARD_OIDC_ISSUER and SWITCHYARD_OIDC_CLIENT_ID");
10764
- }
10765
- return out;
10766
- }
10767
- function coordinatorOptions(flags, env) {
10768
- const dataDir = flags.dataDir ? resolve4(flags.dataDir) : defaultDataDir(env);
10769
- const listen = parseListen(flags.listen ?? env.SWITCHYARD_LISTEN ?? `0.0.0.0:${DEFAULT_PORT}`);
10770
- const publicUrl = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim() || void 0;
10771
- if (publicUrl && !/^https?:\/\//.test(publicUrl)) {
10772
- throw new CliError(`Public URL must start with http:// or https://, got "${publicUrl}"`);
10773
- }
10774
- const trustedOrigins = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
10775
- return {
10776
- dataDir,
10777
- listen,
10778
- publicUrl,
10779
- trustedOrigins,
10780
- webDist: resolveWebDist(flags.webDist, env),
10781
- ...authOptionsFromEnv(env)
10782
- };
10783
- }
10784
- function waitForSignal() {
10785
- return new Promise((res) => {
10786
- const on = (sig) => {
10787
- process.off("SIGINT", on);
10788
- process.off("SIGTERM", on);
10789
- res(sig);
10790
- };
10791
- process.on("SIGINT", on);
10792
- process.on("SIGTERM", on);
10793
- });
10794
- }
10795
- async function coordinatorStart(ctx, flags) {
10796
- const opts = coordinatorOptions(flags, ctx.env);
10797
- const c = createCoordinator(opts);
10798
- await c.start();
10799
- writeEndpoint(opts.dataDir, {
10800
- localUrl: localUrlFor(opts.listen, c.port),
10801
- publicUrl: c.config.publicUrl
10802
- });
10803
- ctx.err(`Switchyard coordinator listening on ${opts.listen.host}:${c.port} (public URL ${c.config.publicUrl}; data ${opts.dataDir}${opts.webDist ? "" : "; web app not found, API only"})`);
10804
- const sig = await waitForSignal();
10805
- ctx.err(`${sig}: stopping coordinator`);
10806
- await c.stop();
10807
- }
10808
- function localUrlFor(listen, port) {
10809
- const h = listen.host ?? "0.0.0.0";
10810
- const host = h === "0.0.0.0" || h === "::" || h === "" ? "127.0.0.1" : h.includes(":") ? `[${h}]` : h;
10811
- return `http://${host}:${port}`;
10812
- }
10813
- function dataDirOf(flags, env) {
10814
- return flags.dataDir ? resolve4(flags.dataDir) : defaultDataDir(env);
10815
- }
10816
- function coordinatorBackup(ctx, out, flags) {
10817
- const r = backup(dataDirOf(flags, ctx.env), out);
10818
- ctx.out(`Backed up to ${r.file} (schema ${r.migrationLevel})`);
10819
- }
10820
- function coordinatorRestore(ctx, file, flags) {
10821
- const r = restore(dataDirOf(flags, ctx.env), file);
10822
- ctx.out(`Restored ${r.restoredFrom} (schema ${r.migrationLevel}).`);
10823
- if (r.previous)
10824
- ctx.out(`The previous database was kept as ${r.previous}`);
10825
- if (r.migrationLevel < LATEST_MIGRATION) {
10826
- ctx.out("Migrations to the current schema run when the coordinator next starts.");
10827
- }
10828
- }
10829
- function coordinatorMigrate(ctx, flags) {
10830
- const dataDir = dataDirOf(flags, ctx.env);
10831
- const holder = lockHolder(dataDir);
10832
- if (holder !== null) {
10833
- throw new CliError(`Coordinator (pid ${holder}) is running on ${dataDir}; it migrated at start`);
10834
- }
10835
- const path = dbPath(dataDir);
10836
- const existed = existsSync6(path);
10837
- mkdirSync4(dataDir, { recursive: true });
10838
- const db = openDb(path);
10839
- try {
10840
- const before = existed ? migrationLevel(db) : 0;
10841
- migrate(db);
10842
- const after = migrationLevel(db);
10843
- ctx.out(before === after ? `Schema is current (level ${after}) at ${path}` : `Migrated ${path} from level ${before} to ${after}`);
10844
- } finally {
10845
- db.close();
10846
- }
10847
- }
10848
-
10849
- // packages/cli/dist/commands/doctor.js
10850
- import { execFile as execFile3 } from "node:child_process";
10851
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "node:fs";
10852
-
10853
10880
  // packages/adapter-sdk/dist/discovery.js
10854
- import { existsSync as existsSync7 } from "node:fs";
10881
+ import { existsSync as existsSync6 } from "node:fs";
10855
10882
  import { readFile } from "node:fs/promises";
10856
10883
  import { homedir as homedir2 } from "node:os";
10857
- import { isAbsolute, join as join6, resolve as resolve5 } from "node:path";
10884
+ import { isAbsolute, join as join6, resolve as resolve4 } from "node:path";
10858
10885
  function piAgentDir(env = process.env, home = homedir2()) {
10859
10886
  const fromEnv = env.PI_CODING_AGENT_DIR?.trim();
10860
10887
  if (fromEnv)
@@ -10926,7 +10953,7 @@ function piPackageDirCandidates(source, agentDir, opts = {}) {
10926
10953
  return [join6(agentDir, "git", parsed.host, parsed.path)];
10927
10954
  case "local": {
10928
10955
  const p = expandHome(parsed.path, home);
10929
- return [isAbsolute(p) ? p : resolve5(agentDir, p)];
10956
+ return [isAbsolute(p) ? p : resolve4(agentDir, p)];
10930
10957
  }
10931
10958
  }
10932
10959
  }
@@ -10952,7 +10979,7 @@ async function discoverPiPackages(opts = {}) {
10952
10979
  };
10953
10980
  const diag = (level, message) => out.diagnostics.push({ sessionId: null, adapterId: null, level, message });
10954
10981
  const settingsPath = join6(agentDir, "settings.json");
10955
- if (!existsSync7(settingsPath))
10982
+ if (!existsSync6(settingsPath))
10956
10983
  return out;
10957
10984
  let settings;
10958
10985
  try {
@@ -10974,14 +11001,14 @@ async function discoverPiPackages(opts = {}) {
10974
11001
  home,
10975
11002
  globalNpmRoot: opts.globalNpmRoot
10976
11003
  });
10977
- const dir = candidates.find((c) => existsSync7(c));
11004
+ const dir = candidates.find((c) => existsSync6(c));
10978
11005
  if (!dir) {
10979
11006
  diag("warning", `Pi package ${source} could not be resolved (looked in ${candidates.join(", ")}); any adapter it provides is not loaded.`);
10980
11007
  continue;
10981
11008
  }
10982
11009
  let pkg = null;
10983
11010
  const pkgPath = join6(dir, "package.json");
10984
- if (existsSync7(pkgPath)) {
11011
+ if (existsSync6(pkgPath)) {
10985
11012
  try {
10986
11013
  pkg = await readJson(pkgPath);
10987
11014
  } catch (err) {
@@ -11133,7 +11160,7 @@ function genericWidgetPanel(lines) {
11133
11160
 
11134
11161
  // packages/adapter-sdk/dist/manifest.js
11135
11162
  import { readFile as readFile2 } from "node:fs/promises";
11136
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve6 } from "node:path";
11163
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, resolve as resolve5 } from "node:path";
11137
11164
  import { validRange } from "semver";
11138
11165
  import { z as z18 } from "zod";
11139
11166
  var MANIFEST_FILE = "switchyard-adapter.json";
@@ -11177,7 +11204,7 @@ function parseManifest(input, path = null) {
11177
11204
  throw new ManifestError(`invalid adapter manifest: ${issues}`, path);
11178
11205
  }
11179
11206
  async function loadManifest(pathOrDir) {
11180
- const abs = resolve6(pathOrDir);
11207
+ const abs = resolve5(pathOrDir);
11181
11208
  const manifestPath = abs.endsWith(".json") ? abs : join7(abs, MANIFEST_FILE);
11182
11209
  let raw;
11183
11210
  try {
@@ -11197,8 +11224,8 @@ async function loadManifest(pathOrDir) {
11197
11224
  manifest,
11198
11225
  manifestPath,
11199
11226
  dir,
11200
- serverPath: resolve6(dir, manifest.server),
11201
- webRendererPath: manifest.webRenderer ? resolve6(dir, manifest.webRenderer) : null
11227
+ serverPath: resolve5(dir, manifest.server),
11228
+ webRendererPath: manifest.webRenderer ? resolve5(dir, manifest.webRenderer) : null
11202
11229
  };
11203
11230
  }
11204
11231
 
@@ -11253,9 +11280,9 @@ function responseFitsRequest(req, resp) {
11253
11280
  }
11254
11281
 
11255
11282
  // packages/adapter-sdk/dist/registry.js
11256
- import { existsSync as existsSync8 } from "node:fs";
11283
+ import { existsSync as existsSync7 } from "node:fs";
11257
11284
  import { readdir, readFile as readFile3 } from "node:fs/promises";
11258
- import { join as join8, resolve as resolve7 } from "node:path";
11285
+ import { join as join8, resolve as resolve6 } from "node:path";
11259
11286
  import { pathToFileURL } from "node:url";
11260
11287
  import { satisfies } from "semver";
11261
11288
 
@@ -11339,6 +11366,10 @@ var AdapterSession = class {
11339
11366
  this.activeTools.set(e.toolCallId, e);
11340
11367
  await this.callAll("onToolStart", e);
11341
11368
  }
11369
+ /** `tool_execution_update`: runs `onToolUpdate` hooks with the partial result. */
11370
+ async onToolUpdate(e) {
11371
+ await this.callAll("onToolUpdate", e);
11372
+ }
11342
11373
  /** `tool_execution_end`: runs `onToolEnd` hooks, then expires answers queued for the call. */
11343
11374
  async onToolEnd(e) {
11344
11375
  try {
@@ -11676,7 +11707,7 @@ function isServerAdapter(x) {
11676
11707
  async function loadAdapterFromDir(pathOrDir, tier) {
11677
11708
  const m = await loadManifest(pathOrDir);
11678
11709
  const warnings = [];
11679
- if (!existsSync8(m.serverPath)) {
11710
+ if (!existsSync7(m.serverPath)) {
11680
11711
  throw new AdapterLoadError(`server module ${m.serverPath} does not exist`, m.manifest);
11681
11712
  }
11682
11713
  let mod;
@@ -11697,7 +11728,7 @@ async function loadAdapterFromDir(pathOrDir, tier) {
11697
11728
  throw new AdapterLoadError(`${m.serverPath} must default-export a ServerAdapter object or factory`, m.manifest);
11698
11729
  }
11699
11730
  let webRendererPath = m.webRendererPath;
11700
- if (webRendererPath && !existsSync8(webRendererPath)) {
11731
+ if (webRendererPath && !existsSync7(webRendererPath)) {
11701
11732
  warnings.push(`web renderer ${webRendererPath} does not exist; portable panels are used`);
11702
11733
  webRendererPath = null;
11703
11734
  }
@@ -11714,12 +11745,12 @@ async function loadAdapterFromDir(pathOrDir, tier) {
11714
11745
  };
11715
11746
  }
11716
11747
  async function subdirsWithManifest(root) {
11717
- if (!existsSync8(root))
11748
+ if (!existsSync7(root))
11718
11749
  return [];
11719
- if (existsSync8(join8(root, MANIFEST_FILE)))
11750
+ if (existsSync7(join8(root, MANIFEST_FILE)))
11720
11751
  return [root];
11721
11752
  const entries = await readdir(root, { withFileTypes: true });
11722
- return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => join8(root, e.name)).filter((d) => existsSync8(join8(d, MANIFEST_FILE))).sort();
11753
+ return entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => join8(root, e.name)).filter((d) => existsSync7(join8(d, MANIFEST_FILE))).sort();
11723
11754
  }
11724
11755
  var AdapterRegistry = class {
11725
11756
  opts;
@@ -11737,7 +11768,7 @@ var AdapterRegistry = class {
11737
11768
  this.diags = [];
11738
11769
  const candidates = [];
11739
11770
  if (opts.projectRoot) {
11740
- for (const d of await this.safeList(resolve7(opts.projectRoot, PROJECT_ADAPTER_DIR))) {
11771
+ for (const d of await this.safeList(resolve6(opts.projectRoot, PROJECT_ADAPTER_DIR))) {
11741
11772
  candidates.push({ tier: "project", path: d });
11742
11773
  }
11743
11774
  }
@@ -11752,7 +11783,7 @@ var AdapterRegistry = class {
11752
11783
  this.diagnostic("warning", null, `Cannot read ${pkgDir}/package.json: ${err.message}`);
11753
11784
  }
11754
11785
  for (const e of entries)
11755
- candidates.push({ tier: "plugin", path: resolve7(pkgDir, e) });
11786
+ candidates.push({ tier: "plugin", path: resolve6(pkgDir, e) });
11756
11787
  }
11757
11788
  const loaded = [];
11758
11789
  for (const c of candidates) {
@@ -12023,7 +12054,7 @@ async function runChecks(path, commands, opts = {}) {
12023
12054
 
12024
12055
  // packages/host-agent/dist/files.js
12025
12056
  import { lstat, readdir as readdir2, readFile as readFile4, realpath, stat } from "node:fs/promises";
12026
- import { join as join9, relative, resolve as resolve8, sep } from "node:path";
12057
+ import { join as join9, relative, resolve as resolve7, sep } from "node:path";
12027
12058
  var MAX_READ_BYTES = 1024 * 1024;
12028
12059
  var BINARY_SNIFF_BYTES = 8e3;
12029
12060
  function isInside(root, target) {
@@ -12047,7 +12078,7 @@ async function resolveInside(root, rel) {
12047
12078
  if (rel.includes("\0"))
12048
12079
  throw new SwitchyardError("invalid_request", "Invalid path");
12049
12080
  const rootReal = await realRoot(root);
12050
- const target = resolve8(rootReal, rel.replace(/^[/\\]+/, ""));
12081
+ const target = resolve7(rootReal, rel.replace(/^[/\\]+/, ""));
12051
12082
  if (!isInside(rootReal, target)) {
12052
12083
  throw new SwitchyardError("forbidden", "Path is outside the workspace");
12053
12084
  }
@@ -12126,7 +12157,7 @@ async function readWorkspaceFile(root, file) {
12126
12157
  import { execFile } from "node:child_process";
12127
12158
  import { appendFile, lstat as lstat2, mkdir, readFile as readFile5, realpath as realpath2, stat as stat2 } from "node:fs/promises";
12128
12159
  import { tmpdir } from "node:os";
12129
- import { join as join10, resolve as resolve9, sep as sep2 } from "node:path";
12160
+ import { join as join10, resolve as resolve8, sep as sep2 } from "node:path";
12130
12161
  var MAX_BUFFER = 64 * 1024 * 1024;
12131
12162
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
12132
12163
  var MAX_PATCH_BYTES = 2 * 1024 * 1024;
@@ -12203,7 +12234,7 @@ async function realpathOr(path) {
12203
12234
  }
12204
12235
  async function commonDirOf(path) {
12205
12236
  const { stdout } = await runGit(path, ["rev-parse", "--git-common-dir"]);
12206
- return realpathOr(resolve9(path, stdout.trim()));
12237
+ return realpathOr(resolve8(path, stdout.trim()));
12207
12238
  }
12208
12239
  function notARepo(path) {
12209
12240
  return new SwitchyardError("not_a_git_repo", `${path} is not a git repository`);
@@ -12776,7 +12807,7 @@ function toErrorBody(err) {
12776
12807
  function queueKey(command) {
12777
12808
  if (command.type === "checks.run")
12778
12809
  return `checks:${command.sessionId}`;
12779
- if (command.type === "session.answer" || command.type === "session.abort" || command.type === "session.setControl" || command.type === "session.adapterAction" || command.type === "session.snapshot") {
12810
+ if (command.type === "session.answer" || command.type === "session.abort" || command.type === "session.setControl" || command.type === "session.adapterAction" || command.type === "session.snapshot" || command.type === "session.models" || command.type === "session.commands") {
12780
12811
  return `control:${command.sessionId}`;
12781
12812
  }
12782
12813
  if ("sessionId" in command && typeof command.sessionId === "string") {
@@ -12931,6 +12962,16 @@ var CommandRouter = class {
12931
12962
  return sup.readNative(c.sessionFile);
12932
12963
  case "session.snapshot":
12933
12964
  return sup.snapshot(c.sessionId);
12965
+ case "session.models":
12966
+ return sup.models(c.sessionId);
12967
+ case "session.commands":
12968
+ return sup.commands(c.sessionId);
12969
+ case "session.setModel":
12970
+ return sup.setModel(c.sessionId, c.provider, c.modelId);
12971
+ case "session.setThinking":
12972
+ return sup.setThinking(c.sessionId, c.level);
12973
+ case "session.compact":
12974
+ return sup.compact(c.sessionId, c.instructions);
12934
12975
  case "checkout.inspect":
12935
12976
  return inspectCheckout2(c.path);
12936
12977
  case "projects.scan":
@@ -12967,8 +13008,8 @@ var CommandRouter = class {
12967
13008
  };
12968
13009
 
12969
13010
  // packages/host-agent/dist/config.js
12970
- import { chmodSync as chmodSync3, existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
12971
- import { homedir as homedir4, hostname as hostname2 } from "node:os";
13011
+ import { chmodSync as chmodSync3, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
13012
+ import { homedir as homedir4, hostname } from "node:os";
12972
13013
  import { dirname as dirname5, join as join12 } from "node:path";
12973
13014
  import { z as z21 } from "zod";
12974
13015
  var HostConfigSchema = z21.object({
@@ -13003,11 +13044,11 @@ function statePaths(stateDir) {
13003
13044
  logs: join12(stateDir, "logs")
13004
13045
  };
13005
13046
  for (const d of [p.root, p.runtime, p.logs])
13006
- mkdirSync5(d, { recursive: true, mode: 448 });
13047
+ mkdirSync4(d, { recursive: true, mode: 448 });
13007
13048
  return p;
13008
13049
  }
13009
13050
  function loadConfig(path) {
13010
- if (!existsSync9(path)) {
13051
+ if (!existsSync8(path)) {
13011
13052
  throw new SwitchyardError("not_found", `Host config ${path} not found; enroll this host first (switchyard host enroll)`);
13012
13053
  }
13013
13054
  let raw;
@@ -13026,7 +13067,7 @@ function loadConfig(path) {
13026
13067
  }
13027
13068
  function saveConfig(path, config) {
13028
13069
  const cfg = HostConfigSchema.parse(config);
13029
- mkdirSync5(dirname5(path), { recursive: true, mode: 448 });
13070
+ mkdirSync4(dirname5(path), { recursive: true, mode: 448 });
13030
13071
  const tmp = `${path}.${process.pid}.tmp`;
13031
13072
  writeFileSync4(tmp, `${JSON.stringify(cfg, null, 2)}
13032
13073
  `, { mode: 384 });
@@ -13104,16 +13145,16 @@ function abortError(signal) {
13104
13145
  async function waitForConfig(opts) {
13105
13146
  const { configPath, env, log, signal } = opts;
13106
13147
  const t = { ...DEFAULT_TIMING, ...opts.timing };
13107
- if (existsSync9(configPath))
13148
+ if (existsSync8(configPath))
13108
13149
  return loadConfig(configPath);
13109
13150
  const secret = env.SWITCHYARD_HOST_ENROLL_SECRET?.trim();
13110
13151
  const coordinatorUrl = env.SWITCHYARD_COORDINATOR_URL?.trim();
13111
13152
  if (secret && coordinatorUrl) {
13112
- const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname2();
13153
+ const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname();
13113
13154
  for (; ; ) {
13114
13155
  if (signal?.aborted)
13115
13156
  throw abortError(signal);
13116
- if (existsSync9(configPath))
13157
+ if (existsSync8(configPath))
13117
13158
  return loadConfig(configPath);
13118
13159
  try {
13119
13160
  const enrolled = await enroll({
@@ -13143,7 +13184,7 @@ async function waitForConfig(opts) {
13143
13184
  for (; ; ) {
13144
13185
  if (signal?.aborted)
13145
13186
  throw abortError(signal);
13146
- if (existsSync9(configPath)) {
13187
+ if (existsSync8(configPath)) {
13147
13188
  try {
13148
13189
  return loadConfig(configPath);
13149
13190
  } catch (err) {
@@ -13163,7 +13204,7 @@ async function waitForConfig(opts) {
13163
13204
  }
13164
13205
 
13165
13206
  // packages/host-agent/dist/db/index.js
13166
- import { mkdirSync as mkdirSync6 } from "node:fs";
13207
+ import { mkdirSync as mkdirSync5 } from "node:fs";
13167
13208
  import { dirname as dirname6 } from "node:path";
13168
13209
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
13169
13210
 
@@ -13247,7 +13288,7 @@ var HostDb = class {
13247
13288
  constructor(path) {
13248
13289
  this.path = path;
13249
13290
  if (path !== ":memory:")
13250
- mkdirSync6(dirname6(path), { recursive: true });
13291
+ mkdirSync5(dirname6(path), { recursive: true });
13251
13292
  this.db = new DatabaseSync3(path);
13252
13293
  this.db.exec("PRAGMA journal_mode = WAL");
13253
13294
  this.db.exec("PRAGMA foreign_keys = ON");
@@ -13323,15 +13364,15 @@ var HostDb = class {
13323
13364
  else
13324
13365
  fn();
13325
13366
  }
13326
- run(sql3, params = []) {
13327
- const r = this.db.prepare(sql3).run(...params);
13367
+ run(sql4, params = []) {
13368
+ const r = this.db.prepare(sql4).run(...params);
13328
13369
  return { changes: Number(r.changes), lastInsertRowid: Number(r.lastInsertRowid) };
13329
13370
  }
13330
- get(sql3, params = []) {
13331
- return this.db.prepare(sql3).get(...params);
13371
+ get(sql4, params = []) {
13372
+ return this.db.prepare(sql4).get(...params);
13332
13373
  }
13333
- all(sql3, params = []) {
13334
- return this.db.prepare(sql3).all(...params);
13374
+ all(sql4, params = []) {
13375
+ return this.db.prepare(sql4).all(...params);
13335
13376
  }
13336
13377
  // ---- meta ----
13337
13378
  getMeta(key) {
@@ -13341,12 +13382,12 @@ var HostDb = class {
13341
13382
  this.run("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", [key, value]);
13342
13383
  }
13343
13384
  // ---- JSON row helpers ----
13344
- getJson(sql3, params = []) {
13345
- const r = this.get(sql3, params);
13385
+ getJson(sql4, params = []) {
13386
+ const r = this.get(sql4, params);
13346
13387
  return r ? JSON.parse(r.json) : void 0;
13347
13388
  }
13348
- allJson(sql3, params = []) {
13349
- return this.all(sql3, params).map((r) => JSON.parse(r.json));
13389
+ allJson(sql4, params = []) {
13390
+ return this.all(sql4, params).map((r) => JSON.parse(r.json));
13350
13391
  }
13351
13392
  close() {
13352
13393
  try {
@@ -13871,6 +13912,45 @@ function createLogger2(opts = {}) {
13871
13912
  // packages/host-agent/dist/main.js
13872
13913
  import { readFileSync as readFileSync8 } from "node:fs";
13873
13914
 
13915
+ // packages/pi-runtime/dist/bridge/pi-model.js
13916
+ var PI_THINKING_LEVELS = [
13917
+ "off",
13918
+ "minimal",
13919
+ "low",
13920
+ "medium",
13921
+ "high",
13922
+ "xhigh",
13923
+ "max"
13924
+ ];
13925
+ function supportedThinkingLevels(model) {
13926
+ const known = PI_THINKING_LEVELS;
13927
+ if (Array.isArray(model?.thinkingLevels)) {
13928
+ return model.thinkingLevels.filter((l) => typeof l === "string" && known.includes(l));
13929
+ }
13930
+ if (!model?.reasoning)
13931
+ return ["off"];
13932
+ return PI_THINKING_LEVELS.filter((level) => {
13933
+ const mapped = model.thinkingLevelMap?.[level];
13934
+ if (mapped === null)
13935
+ return false;
13936
+ if (level === "xhigh" || level === "max")
13937
+ return mapped !== void 0;
13938
+ return true;
13939
+ });
13940
+ }
13941
+ function modelInfo(model) {
13942
+ if (!model || typeof model.provider !== "string" || typeof model.id !== "string")
13943
+ return null;
13944
+ return {
13945
+ provider: model.provider,
13946
+ id: model.id,
13947
+ name: typeof model.name === "string" && model.name ? model.name : null,
13948
+ contextWindow: typeof model.contextWindow === "number" ? model.contextWindow : null,
13949
+ reasoning: model.reasoning === true,
13950
+ thinkingLevels: supportedThinkingLevels(model)
13951
+ };
13952
+ }
13953
+
13874
13954
  // packages/pi-runtime/dist/capabilities.js
13875
13955
  import { execFile as execFile2 } from "node:child_process";
13876
13956
  import semver from "semver";
@@ -13889,6 +13969,7 @@ var PI_RPC_CAPABILITIES = {
13889
13969
  widgetsComponent: false,
13890
13970
  tokenAccounting: true,
13891
13971
  costAccounting: true,
13972
+ sessionControls: true,
13892
13973
  unsupported: [
13893
13974
  { capability: "customUi", reason: "ctx.ui.custom() returns undefined in RPC mode" },
13894
13975
  {
@@ -13912,6 +13993,7 @@ function noCapabilities(reason) {
13912
13993
  widgetsComponent: false,
13913
13994
  tokenAccounting: false,
13914
13995
  costAccounting: false,
13996
+ sessionControls: false,
13915
13997
  unsupported: [{ capability: "all", reason }]
13916
13998
  };
13917
13999
  }
@@ -13964,7 +14046,7 @@ ${stderr}`));
13964
14046
  import { accessSync, constants, readFileSync as readFileSync6, realpathSync } from "node:fs";
13965
14047
  import { open, readdir as readdir4, readFile as readFile6, stat as stat4 } from "node:fs/promises";
13966
14048
  import { homedir as homedir5 } from "node:os";
13967
- import { delimiter, dirname as dirname7, join as join13, resolve as resolve10 } from "node:path";
14049
+ import { delimiter, dirname as dirname7, join as join13, resolve as resolve9 } from "node:path";
13968
14050
  import { pathToFileURL as pathToFileURL2 } from "node:url";
13969
14051
 
13970
14052
  // packages/pi-runtime/dist/transcript.js
@@ -14227,7 +14309,7 @@ async function listImportable(sessionsRoot = defaultSessionsRoot(), now = Date.n
14227
14309
  }
14228
14310
  function findOnPath(bin) {
14229
14311
  if (bin.includes("/"))
14230
- return resolve10(bin);
14312
+ return resolve9(bin);
14231
14313
  for (const dir of (process.env.PATH ?? "").split(delimiter)) {
14232
14314
  if (!dir)
14233
14315
  continue;
@@ -14490,14 +14572,14 @@ function writeJsonl(stream, value) {
14490
14572
  import { spawn as spawn2 } from "node:child_process";
14491
14573
  import { randomUUID } from "node:crypto";
14492
14574
  import { EventEmitter as EventEmitter3 } from "node:events";
14493
- import { chmodSync as chmodSync4, existsSync as existsSync10, mkdirSync as mkdirSync7, rmSync as rmSync2 } from "node:fs";
14575
+ import { chmodSync as chmodSync4, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "node:fs";
14494
14576
  import { createServer } from "node:net";
14495
14577
  import { tmpdir as tmpdir2 } from "node:os";
14496
14578
  import { join as join14 } from "node:path";
14497
14579
  import { fileURLToPath as fileURLToPath2 } from "node:url";
14498
14580
  function bridgeExtensionPath() {
14499
14581
  const sibling = fileURLToPath2(new URL("./bridge/switchyard-bridge.js", import.meta.url));
14500
- if (existsSync10(sibling))
14582
+ if (existsSync9(sibling))
14501
14583
  return sibling;
14502
14584
  return fileURLToPath2(new URL("../dist/bridge/switchyard-bridge.js", import.meta.url));
14503
14585
  }
@@ -14516,7 +14598,7 @@ function shortSocketPath(runtimeDir, prefix) {
14516
14598
  if (join14(dir, name).length > MAX_SOCKET_PATH) {
14517
14599
  dir = join14("/tmp", `switchyard-${process.getuid?.() ?? "u"}`);
14518
14600
  }
14519
- mkdirSync7(dir, { recursive: true, mode: 448 });
14601
+ mkdirSync6(dir, { recursive: true, mode: 448 });
14520
14602
  return join14(dir, name);
14521
14603
  }
14522
14604
  var PiRpcProcess = class _PiRpcProcess extends EventEmitter3 {
@@ -14755,12 +14837,12 @@ var PiRpcProcess = class _PiRpcProcess extends EventEmitter3 {
14755
14837
  // packages/pi-runtime/dist/session-runtime.js
14756
14838
  import { randomUUID as randomUUID2 } from "node:crypto";
14757
14839
  import { EventEmitter as EventEmitter5 } from "node:events";
14758
- import { existsSync as existsSync12 } from "node:fs";
14840
+ import { existsSync as existsSync11 } from "node:fs";
14759
14841
 
14760
14842
  // packages/pi-runtime/dist/terminal-process.js
14761
14843
  import { spawn as spawn3 } from "node:child_process";
14762
14844
  import { EventEmitter as EventEmitter4 } from "node:events";
14763
- import { closeSync, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync7 } from "node:fs";
14845
+ import { closeSync, existsSync as existsSync10, mkdirSync as mkdirSync7, openSync, readFileSync as readFileSync7 } from "node:fs";
14764
14846
  import { createRequire } from "node:module";
14765
14847
  import { createConnection } from "node:net";
14766
14848
  import { tmpdir as tmpdir3 } from "node:os";
@@ -14768,7 +14850,7 @@ import { basename as basename2, dirname as dirname8, join as join15 } from "node
14768
14850
  import { fileURLToPath as fileURLToPath3 } from "node:url";
14769
14851
  function resolveDist(rel) {
14770
14852
  const sibling = fileURLToPath3(new URL(`./${rel}`, import.meta.url));
14771
- if (existsSync11(sibling))
14853
+ if (existsSync10(sibling))
14772
14854
  return sibling;
14773
14855
  return fileURLToPath3(new URL(`../dist/${rel}`, import.meta.url));
14774
14856
  }
@@ -14785,7 +14867,7 @@ function terminalSupport() {
14785
14867
  return { ok: false, reason: `node-pty is not available: ${err.message}` };
14786
14868
  }
14787
14869
  const holder = holderScriptPath();
14788
- if (!existsSync11(holder))
14870
+ if (!existsSync10(holder))
14789
14871
  return { ok: false, reason: `Terminal holder not built: ${holder}` };
14790
14872
  return { ok: true };
14791
14873
  }
@@ -14872,7 +14954,7 @@ var PiTerminalProcess = class _PiTerminalProcess extends EventEmitter4 {
14872
14954
  rows: opts.rows ?? 40
14873
14955
  };
14874
14956
  const logPath = opts.logPath ?? join15(dirname8(socketPath), `${basename2(socketPath, ".sock")}.log`);
14875
- mkdirSync8(dirname8(logPath), { recursive: true, mode: 448 });
14957
+ mkdirSync7(dirname8(logPath), { recursive: true, mode: 448 });
14876
14958
  const fd = openSync(logPath, "a", 384);
14877
14959
  try {
14878
14960
  const child = spawn3(process.execPath, [holderScriptPath()], {
@@ -15024,6 +15106,7 @@ var PiTerminalProcess = class _PiTerminalProcess extends EventEmitter4 {
15024
15106
  type: rec.event === "ui_prompt_end" ? "ui_prompt_end" : "ui_prompt_start",
15025
15107
  kind: typeof rec.kind === "string" ? rec.kind : "unknown",
15026
15108
  title: typeof rec.title === "string" ? rec.title : null,
15109
+ remoteBacked: rec.remoteBacked === true,
15027
15110
  seq: ++this.seq,
15028
15111
  at: (/* @__PURE__ */ new Date()).toISOString(),
15029
15112
  pid: this.pid ?? 0
@@ -15099,6 +15182,11 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15099
15182
  items = /* @__PURE__ */ new Map();
15100
15183
  partialToolOutput = /* @__PURE__ */ new Map();
15101
15184
  runAssistantText = null;
15185
+ _model = null;
15186
+ scoped = null;
15187
+ _thinking = null;
15188
+ _compacting = false;
15189
+ lastModelJson = "";
15102
15190
  constructor(opts) {
15103
15191
  super();
15104
15192
  this.opts = opts;
@@ -15149,6 +15237,12 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15149
15237
  get remoteDialogs() {
15150
15238
  return this.proc instanceof PiTerminalProcess ? this.proc.remoteDialogs : true;
15151
15239
  }
15240
+ get modelState() {
15241
+ return { model: this._model ? { ...this._model } : null, thinking: this._thinking };
15242
+ }
15243
+ get compacting() {
15244
+ return this._compacting;
15245
+ }
15152
15246
  get isAlive() {
15153
15247
  return this.proc !== null && this.proc.exited === null;
15154
15248
  }
@@ -15175,6 +15269,9 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15175
15269
  args.push("--approve");
15176
15270
  else if (profile.projectTrust === "ignore")
15177
15271
  args.push("--no-approve");
15272
+ if (profile.resources === "vanilla") {
15273
+ args.push("--no-extensions", "--no-skills", "--no-prompt-templates");
15274
+ }
15178
15275
  args.push(...profile.extraArgs);
15179
15276
  args.push(...this.opts.extraArgs ?? []);
15180
15277
  for (const ext of this.opts.extensions ?? [])
@@ -15190,7 +15287,7 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15190
15287
  await this.attachTerminal(this.opts.terminalSocket);
15191
15288
  return;
15192
15289
  }
15193
- if (this._native && !existsSync12(this._native.sessionFile)) {
15290
+ if (this._native && !existsSync11(this._native.sessionFile)) {
15194
15291
  const message = `Native session file not found: ${this._native.sessionFile}`;
15195
15292
  this.fail(message);
15196
15293
  throw new SwitchyardError("native_session_missing", message, {
@@ -15245,6 +15342,9 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15245
15342
  this._native = { sessionId: state.sessionId, sessionFile: state.sessionFile };
15246
15343
  this.started = true;
15247
15344
  this.busy = state.isStreaming;
15345
+ this.applyModel(state.model);
15346
+ this.applyThinking(state.thinkingLevel);
15347
+ this.emitModelIfChanged();
15248
15348
  this.emit("native", { ...this._native });
15249
15349
  this.setRuntime();
15250
15350
  }
@@ -15272,6 +15372,9 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15272
15372
  }
15273
15373
  this.started = true;
15274
15374
  this.busy = state.isStreaming;
15375
+ this.applyModel(state.model);
15376
+ this.applyThinking(state.thinkingLevel);
15377
+ this.emitModelIfChanged();
15275
15378
  this.setRuntime();
15276
15379
  }
15277
15380
  /** Terminal transport: let go of the holder; Pi keeps running and no exit is reported. */
@@ -15341,29 +15444,134 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15341
15444
  async rename(title) {
15342
15445
  await this.call(this.requireAlive(), { type: "set_session_name", name: title });
15343
15446
  }
15344
- /** Token and cost totals (`get_session_stats`). */
15345
- async stats() {
15346
- const raw = await this.call(this.requireAlive(), { type: "get_session_stats" });
15347
- return {
15348
- tokens: typeof raw?.tokens?.total === "number" ? raw.tokens.total : null,
15349
- costUsd: typeof raw?.cost === "number" ? raw.cost : null,
15350
- raw
15351
- };
15352
- }
15353
- /** `get_state` passthrough. */
15354
- async state() {
15355
- return this.call(this.requireAlive(), { type: "get_state" });
15356
- }
15357
- /** Map `get_messages` to transcript items (orders from `startOrder`, default 0; live counter untouched). */
15358
- async history(startOrder = 0) {
15447
+ /** Models Pi can use right now. */
15448
+ async models() {
15359
15449
  const data = await this.call(this.requireAlive(), {
15360
- type: "get_messages"
15361
- });
15362
- return messagesToItems(data?.messages ?? [], {
15363
- sessionId: this.sessionId,
15364
- startOrder,
15365
- newItemId: this.newItemId,
15366
- fallbackAt: this.now()
15450
+ type: "get_available_models"
15451
+ });
15452
+ const out = [];
15453
+ for (const raw of data?.models ?? []) {
15454
+ const m = this.toSessionModel(raw);
15455
+ if (!m)
15456
+ continue;
15457
+ if (this.scoped && this.scoped.size > 0)
15458
+ m.scoped = this.scoped.has(`${m.provider}/${m.id}`);
15459
+ out.push(m);
15460
+ }
15461
+ return out.sort((a, b) => a.provider.localeCompare(b.provider) || (a.name ?? a.id).localeCompare(b.name ?? b.id));
15462
+ }
15463
+ /** Slash commands Pi knows (extension commands, prompt templates, skills). */
15464
+ async commands() {
15465
+ const data = await this.call(this.requireAlive(), {
15466
+ type: "get_commands"
15467
+ });
15468
+ const out = [];
15469
+ for (const c of data?.commands ?? []) {
15470
+ if (typeof c?.name !== "string" || !c.name)
15471
+ continue;
15472
+ if (c.source !== "extension" && c.source !== "prompt" && c.source !== "skill")
15473
+ continue;
15474
+ out.push({
15475
+ name: c.name,
15476
+ description: typeof c.description === "string" ? c.description : null,
15477
+ source: c.source
15478
+ });
15479
+ }
15480
+ return out.sort((a, b) => a.name.localeCompare(b.name));
15481
+ }
15482
+ async setModel(provider, modelId) {
15483
+ await this.call(this.requireAlive(), { type: "set_model", provider, modelId });
15484
+ await this.refreshModelState();
15485
+ return this.modelState;
15486
+ }
15487
+ async setThinking(level) {
15488
+ await this.call(this.requireAlive(), { type: "set_thinking_level", level });
15489
+ await this.refreshModelState();
15490
+ return this.modelState;
15491
+ }
15492
+ /** Start a manual compaction. Terminal sessions answer at once; Pi's RPC answers when done. */
15493
+ async compact(instructions) {
15494
+ const proc = this.requireAlive();
15495
+ const cmd = { type: "compact", ...instructions ? { customInstructions: instructions } : {} };
15496
+ if (this.transport === "terminal") {
15497
+ await this.call(proc, cmd, 1e4);
15498
+ } else {
15499
+ this.call(proc, cmd, 30 * 6e4).catch(() => {
15500
+ });
15501
+ }
15502
+ }
15503
+ async refreshModelState() {
15504
+ const s = await this.state();
15505
+ this.applyModel(s.model);
15506
+ this.applyThinking(s.thinkingLevel);
15507
+ this.emitModelIfChanged();
15508
+ }
15509
+ toSessionModel(raw) {
15510
+ const info = modelInfo(raw);
15511
+ if (!info)
15512
+ return null;
15513
+ return {
15514
+ ...info,
15515
+ thinkingLevels: info.thinkingLevels.flatMap((l) => {
15516
+ const r = ThinkingLevel.safeParse(l);
15517
+ return r.success ? [r.data] : [];
15518
+ })
15519
+ };
15520
+ }
15521
+ applyScoped(raw) {
15522
+ if (Array.isArray(raw))
15523
+ this.scoped = new Set(raw.filter((x) => typeof x === "string"));
15524
+ }
15525
+ applyModel(raw) {
15526
+ this._model = this.toSessionModel(raw);
15527
+ }
15528
+ applyThinking(raw) {
15529
+ const r = ThinkingLevel.safeParse(raw);
15530
+ this._thinking = r.success ? r.data : null;
15531
+ }
15532
+ emitModelIfChanged() {
15533
+ const json = JSON.stringify(this.modelState);
15534
+ if (json === this.lastModelJson)
15535
+ return;
15536
+ this.lastModelJson = json;
15537
+ this.emit("model", this.modelState);
15538
+ }
15539
+ setCompacting(v) {
15540
+ if (this._compacting === v)
15541
+ return;
15542
+ this._compacting = v;
15543
+ this.emit("compacting", v);
15544
+ }
15545
+ /** Token and cost totals (`get_session_stats`). */
15546
+ async stats() {
15547
+ const raw = await this.call(this.requireAlive(), { type: "get_session_stats" });
15548
+ const cu = raw?.contextUsage;
15549
+ const context = cu && typeof cu.contextWindow === "number" ? {
15550
+ tokens: typeof cu.tokens === "number" ? cu.tokens : null,
15551
+ window: cu.contextWindow,
15552
+ percent: typeof cu.percent === "number" ? cu.percent : null
15553
+ } : null;
15554
+ return {
15555
+ context,
15556
+ tokens: typeof raw?.tokens?.total === "number" ? raw.tokens.total : null,
15557
+ costUsd: typeof raw?.cost === "number" ? raw.cost : null,
15558
+ raw
15559
+ };
15560
+ }
15561
+ /** `get_state` passthrough. */
15562
+ async state() {
15563
+ return this.call(this.requireAlive(), { type: "get_state" });
15564
+ }
15565
+ /** Map `get_messages` to transcript items (orders from `startOrder`, default 0; live counter untouched). */
15566
+ async history(startOrder = 0) {
15567
+ const data = await this.call(this.requireAlive(), {
15568
+ type: "get_messages"
15569
+ });
15570
+ return messagesToItems(data?.messages ?? [], {
15571
+ sessionId: this.sessionId,
15572
+ startOrder,
15573
+ newItemId: this.newItemId,
15574
+ fallbackAt: this.now()
15367
15575
  });
15368
15576
  }
15369
15577
  /** Current state for a client (re)attaching mid-stream. */
@@ -15478,6 +15686,12 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15478
15686
  this.emit("bridge", msg);
15479
15687
  if (msg.type === "ui_prompt_end")
15480
15688
  this.onPromptEnd(msg);
15689
+ if (msg.type === "model_state") {
15690
+ this.applyScoped(msg.scoped);
15691
+ this.applyModel(msg.model);
15692
+ this.applyThinking(msg.thinkingLevel);
15693
+ this.emitModelIfChanged();
15694
+ }
15481
15695
  });
15482
15696
  proc.on("stderr", (text) => this.emit("stderr", text));
15483
15697
  proc.on("protocol_error", (err, line) => {
@@ -15520,6 +15734,7 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15520
15734
  this.emit("ui_resolved", { id, by: "exit", request: req });
15521
15735
  this.pending.clear();
15522
15736
  this.current = null;
15737
+ this.setCompacting(false);
15523
15738
  const wasBusy = this.busy;
15524
15739
  this.busy = false;
15525
15740
  if (this.terminal === "failed") {
@@ -15686,15 +15901,29 @@ var ManagedSession = class _ManagedSession extends EventEmitter5 {
15686
15901
  this.notice("error", `Retry failed: ${ev.finalError ?? "unknown error"}`);
15687
15902
  break;
15688
15903
  case "compaction_start":
15904
+ this.setCompacting(true);
15689
15905
  this.notice("info", `Compacting context (${ev.reason ?? "manual"})`);
15690
15906
  break;
15691
15907
  case "compaction_end":
15908
+ this.setCompacting(false);
15692
15909
  if (ev.result)
15693
15910
  this.notice("info", "Context compacted");
15694
15911
  else if (ev.aborted)
15695
15912
  this.notice("warning", "Compaction aborted");
15696
- else
15697
- this.notice("error", `Compaction failed: ${ev.errorMessage ?? "unknown error"}`);
15913
+ else {
15914
+ const msg = String(ev.errorMessage ?? "unknown error").replace(/^Compaction failed:\s*/, "");
15915
+ this.notice("error", `Compaction failed: ${msg}`);
15916
+ }
15917
+ break;
15918
+ case "model_state":
15919
+ this.applyScoped(ev.scoped);
15920
+ this.applyModel(ev.model);
15921
+ this.applyThinking(ev.thinkingLevel);
15922
+ this.emitModelIfChanged();
15923
+ break;
15924
+ case "thinking_level_changed":
15925
+ this.applyThinking(ev.level);
15926
+ this.emitModelIfChanged();
15698
15927
  break;
15699
15928
  case "extension_error":
15700
15929
  this.emit("diagnostic", {
@@ -15848,7 +16077,7 @@ ${output}`.trimEnd() }));
15848
16077
  };
15849
16078
 
15850
16079
  // packages/host-agent/dist/supervisor.js
15851
- import { existsSync as existsSync13, realpathSync as realpathSync2, statSync } from "node:fs";
16080
+ import { existsSync as existsSync12, realpathSync as realpathSync2, statSync } from "node:fs";
15852
16081
  import { join as join16, sep as sep3 } from "node:path";
15853
16082
  var RESTART_NOTICE = "Host agent restarted; resume to continue";
15854
16083
  var QUEUED_ANSWER_SUMMARY = "Answered from your earlier form";
@@ -15987,6 +16216,7 @@ var SessionSupervisor = class {
15987
16216
  markLost(rec, expireSummary) {
15988
16217
  rec.runtime = "interrupted";
15989
16218
  rec.runtimeDetail = "Host agent restarted";
16219
+ rec.compacting = false;
15990
16220
  rec.queue = { steering: [], followUp: [] };
15991
16221
  rec.terminal = null;
15992
16222
  this.expirePending(rec.id, expireSummary);
@@ -16116,6 +16346,9 @@ var SessionSupervisor = class {
16116
16346
  archivedAt: null,
16117
16347
  queue: rec.queue,
16118
16348
  stats: rec.stats,
16349
+ model: rec.model ?? null,
16350
+ thinking: rec.thinking ?? null,
16351
+ compacting: rec.compacting ?? false,
16119
16352
  lastAssistantText: rec.lastAssistantText,
16120
16353
  createdAt: rec.createdAt,
16121
16354
  terminal: rec.terminal != null
@@ -16186,7 +16419,7 @@ var SessionSupervisor = class {
16186
16419
  if (!pi.compatible) {
16187
16420
  throw new SwitchyardError("pi_incompatible", pi.reason ?? "Pi is not available on this host");
16188
16421
  }
16189
- if (!existsSync13(p.workspacePath) || !statSync(p.workspacePath).isDirectory()) {
16422
+ if (!existsSync12(p.workspacePath) || !statSync(p.workspacePath).isDirectory()) {
16190
16423
  throw new SwitchyardError("not_found", `Workspace ${p.workspacePath} does not exist`);
16191
16424
  }
16192
16425
  const prev = this.records.get(p.sessionId);
@@ -16201,7 +16434,7 @@ var SessionSupervisor = class {
16201
16434
  }
16202
16435
  }
16203
16436
  let nextOrder = prev?.nextOrder ?? 0;
16204
- if (!prev && nativeHandle && existsSync13(nativeHandle.sessionFile)) {
16437
+ if (!prev && nativeHandle && existsSync12(nativeHandle.sessionFile)) {
16205
16438
  try {
16206
16439
  const native = await readNativeTranscript(nativeHandle.sessionFile, {
16207
16440
  piBin: this.opts.piBin
@@ -16211,6 +16444,8 @@ var SessionSupervisor = class {
16211
16444
  nextOrder = 0;
16212
16445
  }
16213
16446
  }
16447
+ const t0 = Date.now();
16448
+ let adaptersMs = 0;
16214
16449
  const now = nowIso();
16215
16450
  let control = { controlRevision: p.controlRevision, controlMode: p.controlMode };
16216
16451
  for (const c of [prev, this.pendingControl.get(p.sessionId)]) {
@@ -16232,7 +16467,7 @@ var SessionSupervisor = class {
16232
16467
  profile: p.profile,
16233
16468
  nativeHandle,
16234
16469
  runtime: "starting",
16235
- runtimeDetail: null,
16470
+ runtimeDetail: "Starting Pi\u2026",
16236
16471
  controlRevision: control.controlRevision,
16237
16472
  controlMode: control.controlMode,
16238
16473
  origin: p.origin,
@@ -16241,6 +16476,9 @@ var SessionSupervisor = class {
16241
16476
  nextOrder,
16242
16477
  queue: { steering: [], followUp: [] },
16243
16478
  stats: prev?.stats ?? null,
16479
+ model: prev?.model ?? null,
16480
+ thinking: prev?.thinking ?? null,
16481
+ compacting: false,
16244
16482
  lastAssistantText: prev?.lastAssistantText ?? null,
16245
16483
  lastActivityAt: now,
16246
16484
  createdAt: prev?.createdAt ?? now
@@ -16249,7 +16487,11 @@ var SessionSupervisor = class {
16249
16487
  const l = this.newLive(rec);
16250
16488
  this.live.set(rec.id, l);
16251
16489
  this.db.tx(() => this.saveAndEmit(rec));
16252
- await this.openAdapters(l, p.workspacePath);
16490
+ const adaptersStart = Date.now();
16491
+ this.enqueue(l, async () => {
16492
+ await this.openAdapters(l, p.workspacePath);
16493
+ adaptersMs = Date.now() - adaptersStart;
16494
+ });
16253
16495
  const ms = new ManagedSession({
16254
16496
  sessionId: rec.id,
16255
16497
  cwd: p.workspacePath,
@@ -16265,8 +16507,16 @@ var SessionSupervisor = class {
16265
16507
  });
16266
16508
  l.ms = ms;
16267
16509
  this.wire(l, ms);
16510
+ const piStart = Date.now();
16268
16511
  try {
16269
16512
  await ms.start();
16513
+ this.opts.log.info({
16514
+ sessionId: rec.id,
16515
+ piStartMs: Date.now() - piStart,
16516
+ adaptersMs,
16517
+ totalMs: Date.now() - t0,
16518
+ transport: ms.transport
16519
+ }, "session started");
16270
16520
  const socketPath = ms.terminalSocket;
16271
16521
  rec.terminal = socketPath ? { socketPath } : null;
16272
16522
  this.db.tx(() => this.saveAndEmit(rec));
@@ -16331,6 +16581,28 @@ var SessionSupervisor = class {
16331
16581
  this.db.tx(() => this.saveAndEmit(rec));
16332
16582
  return { title };
16333
16583
  }
16584
+ async models(sessionId) {
16585
+ return { models: await this.requireLive(sessionId).models() };
16586
+ }
16587
+ async commands(sessionId) {
16588
+ return { commands: await this.requireLive(sessionId).commands() };
16589
+ }
16590
+ async setModel(sessionId, provider, modelId) {
16591
+ return this.requireLive(sessionId).setModel(provider, modelId);
16592
+ }
16593
+ async setThinking(sessionId, level) {
16594
+ return this.requireLive(sessionId).setThinking(level);
16595
+ }
16596
+ async compact(sessionId, instructions) {
16597
+ const ms = this.requireLive(sessionId);
16598
+ if (ms.compacting)
16599
+ throw new SwitchyardError("conflict", "Compaction is already running");
16600
+ if (ms.runtime !== "idle") {
16601
+ throw new SwitchyardError("conflict", "Wait for the run to finish before compacting");
16602
+ }
16603
+ await ms.compact(instructions);
16604
+ return { ok: true };
16605
+ }
16334
16606
  async adapterAction(sessionId, adapterId, actionId, payload) {
16335
16607
  const l = this.live.get(sessionId);
16336
16608
  if (!l?.adapters) {
@@ -16394,7 +16666,7 @@ var SessionSupervisor = class {
16394
16666
  };
16395
16667
  }
16396
16668
  async readNative(sessionFile) {
16397
- if (!existsSync13(sessionFile)) {
16669
+ if (!existsSync12(sessionFile)) {
16398
16670
  throw new SwitchyardError("native_session_missing", `Native session file not found: ${sessionFile}`);
16399
16671
  }
16400
16672
  return readNativeTranscript(sessionFile, { piBin: this.opts.piBin });
@@ -16585,6 +16857,18 @@ var SessionSupervisor = class {
16585
16857
  this.db.tx(() => this.saveAndEmit(rec));
16586
16858
  }
16587
16859
  });
16860
+ ms.on("model", (m) => {
16861
+ rec.model = m.model;
16862
+ rec.thinking = m.thinking;
16863
+ this.db.tx(() => this.saveAndEmit(rec));
16864
+ void this.refreshStats(l);
16865
+ });
16866
+ ms.on("compacting", (c) => {
16867
+ rec.compacting = c;
16868
+ this.db.tx(() => this.saveAndEmit(rec));
16869
+ if (!c)
16870
+ void this.refreshStats(l);
16871
+ });
16588
16872
  ms.on("diagnostic", (d) => this.diagnostic(rec.id, null, d.level, d.message));
16589
16873
  ms.on("stderr", (text) => this.opts.log.debug({ sessionId: rec.id, stderr: text.slice(0, 2e3) }, "pi stderr"));
16590
16874
  ms.on("ui_request", (req) => this.enqueue(l, () => this.onUiRequest(l, req)));
@@ -16619,7 +16903,7 @@ var SessionSupervisor = class {
16619
16903
  return;
16620
16904
  try {
16621
16905
  const s = await ms.stats();
16622
- const stats = { tokens: s.tokens, costUsd: s.costUsd };
16906
+ const stats = { tokens: s.tokens, costUsd: s.costUsd, context: s.context };
16623
16907
  if (JSON.stringify(stats) === JSON.stringify(l.rec.stats))
16624
16908
  return;
16625
16909
  l.rec.stats = stats;
@@ -16640,6 +16924,21 @@ var SessionSupervisor = class {
16640
16924
  await this.applyToolCard(l, e);
16641
16925
  return;
16642
16926
  }
16927
+ case "tool_execution_update": {
16928
+ if (ev.parentToolCallId || !adapters)
16929
+ return;
16930
+ const id = String(ev.toolCallId);
16931
+ const pr = ev.partialResult;
16932
+ const details = pr && typeof pr === "object" && "details" in pr ? pr.details : void 0;
16933
+ await adapters.onToolUpdate({
16934
+ toolCallId: id,
16935
+ name: String(ev.toolName),
16936
+ args: ms?.toolItem(id)?.data.args ?? {},
16937
+ partialResult: pr,
16938
+ details
16939
+ });
16940
+ return;
16941
+ }
16643
16942
  case "tool_execution_end": {
16644
16943
  if (ev.parentToolCallId || !adapters)
16645
16944
  return;
@@ -16664,6 +16963,8 @@ var SessionSupervisor = class {
16664
16963
  return;
16665
16964
  case "message_end": {
16666
16965
  const m = ev.message;
16966
+ if (m?.role === "assistant")
16967
+ void this.refreshStats(l);
16667
16968
  if (adapters && m?.role === "custom" && typeof m.customType === "string") {
16668
16969
  await adapters.onCustomMessage({
16669
16970
  customType: m.customType,
@@ -16840,6 +17141,8 @@ var SessionSupervisor = class {
16840
17141
  */
16841
17142
  onTerminalPrompt(l, b) {
16842
17143
  if (b.type === "ui_prompt_start") {
17144
+ if (b.kind === "custom" && b.remoteBacked)
17145
+ return;
16843
17146
  if (b.kind !== "custom" && l.ms?.remoteDialogs)
16844
17147
  return;
16845
17148
  if (l.terminalPrompt)
@@ -17166,9 +17469,10 @@ var WorkspaceService = class {
17166
17469
  }
17167
17470
  async create(checkoutPath, branch, base, setupCommand, worktreeDir) {
17168
17471
  const wt = await createWorktree(checkoutPath, branch, base, worktreeDir);
17169
- const git = await worktreeStatus(wt.path, wt.baseBranch).catch(() => null);
17170
17472
  const initial = setupCommand ? { state: "running", log: "" } : { state: "none", log: "" };
17171
- this.emitUpdate(wt.path, { branch: wt.branch, baseBranch: wt.baseBranch, git, setup: initial });
17473
+ this.emitUpdate(wt.path, { branch: wt.branch, baseBranch: wt.baseBranch, setup: initial });
17474
+ void worktreeStatus(wt.path, wt.baseBranch).then((git) => this.emitUpdate(wt.path, { git }), () => {
17475
+ });
17172
17476
  let setupDone = Promise.resolve(initial);
17173
17477
  if (setupCommand) {
17174
17478
  setupDone = runSetup(wt.path, setupCommand, (setup) => this.emitUpdate(wt.path, { setup }), {
@@ -17190,7 +17494,7 @@ var WorkspaceService = class {
17190
17494
  branch: wt.branch,
17191
17495
  baseBranch: wt.baseBranch,
17192
17496
  provenance: "managed",
17193
- git,
17497
+ git: null,
17194
17498
  setup: initial,
17195
17499
  setupDone
17196
17500
  };
@@ -17335,59 +17639,489 @@ async function runHostAgent(opts = {}) {
17335
17639
  if (interruptedOnStart.length > 0 || failedDispatches > 0) {
17336
17640
  log.warn({ sessions: interruptedOnStart.length, dispatches: failedDispatches }, "recovered after restart");
17337
17641
  }
17338
- router.pruneReceipts();
17339
- const reattach = await supervisor.reattachTerminals();
17340
- interruptedOnStart.push(...reattach.lost);
17341
- if (reattach.reattached.length > 0 || reattach.lost.length > 0) {
17342
- log.info({ reattached: reattach.reattached.length, lost: reattach.lost.length }, "terminal sessions after restart");
17642
+ router.pruneReceipts();
17643
+ try {
17644
+ await catalog.init();
17645
+ } catch (err) {
17646
+ log.warn({ err: err.message }, "adapter discovery failed; Pi package adapters are not loaded");
17647
+ }
17648
+ const reattach = await supervisor.reattachTerminals();
17649
+ interruptedOnStart.push(...reattach.lost);
17650
+ if (reattach.reattached.length > 0 || reattach.lost.length > 0) {
17651
+ log.info({ reattached: reattach.reattached.length, lost: reattach.lost.length }, "terminal sessions after restart");
17652
+ }
17653
+ supervisor.emitAdapters();
17654
+ link.start();
17655
+ let stopping = null;
17656
+ const onSignal = (signal) => {
17657
+ log.info({ signal }, "stopping host agent");
17658
+ void handle.stop().finally(() => process.exit(0));
17659
+ };
17660
+ if (opts.handleSignals !== false) {
17661
+ process.once("SIGTERM", onSignal);
17662
+ process.once("SIGINT", onSignal);
17663
+ }
17664
+ const handle = {
17665
+ hostId: config.hostId,
17666
+ config,
17667
+ db,
17668
+ link,
17669
+ supervisor,
17670
+ router,
17671
+ dispatcher,
17672
+ pi,
17673
+ interruptedOnStart,
17674
+ terminalSessions: terminalEnabled,
17675
+ get spawnedPiProcesses() {
17676
+ return supervisor.spawnedProcesses;
17677
+ },
17678
+ stop(stopOpts = {}) {
17679
+ stopping ??= (async () => {
17680
+ process.off("SIGTERM", onSignal);
17681
+ process.off("SIGINT", onSignal);
17682
+ relay.closeAll();
17683
+ await supervisor.stopAll(stopOpts);
17684
+ await new Promise((r) => setTimeout(r, 50));
17685
+ await link.stop();
17686
+ db.close();
17687
+ })();
17688
+ return stopping;
17689
+ }
17690
+ };
17691
+ return handle;
17692
+ }
17693
+
17694
+ // packages/cli/dist/commands/coordinator.js
17695
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "node:fs";
17696
+ import { resolve as resolve10 } from "node:path";
17697
+ function parseListen(v) {
17698
+ const s = v.trim();
17699
+ let host = "0.0.0.0";
17700
+ let portStr = s;
17701
+ const v6 = s.match(/^\[([^\]]+)\]:(\d+)$/);
17702
+ if (v6) {
17703
+ host = v6[1];
17704
+ portStr = v6[2];
17705
+ } else if (s.includes(":")) {
17706
+ const i = s.lastIndexOf(":");
17707
+ host = s.slice(0, i) || "0.0.0.0";
17708
+ portStr = s.slice(i + 1);
17709
+ } else if (!/^\d+$/.test(s)) {
17710
+ host = s;
17711
+ portStr = String(DEFAULT_PORT);
17712
+ }
17713
+ const port = Number(portStr);
17714
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
17715
+ throw new CliError(`Invalid listen address "${v}" (expected host:port)`);
17716
+ }
17717
+ return { host, port };
17718
+ }
17719
+ function parseOrigins(v) {
17720
+ if (!v?.trim())
17721
+ return void 0;
17722
+ return v.split(",").map((o) => o.trim().replace(/\/+$/, "")).filter(Boolean);
17723
+ }
17724
+ var DEFAULT_OIDC_LABEL = "Sign in with single sign-on";
17725
+ var DEFAULT_OIDC_SCOPES = "openid profile email groups";
17726
+ function envValue(env, key) {
17727
+ const v = env[key]?.trim();
17728
+ return v ? v : void 0;
17729
+ }
17730
+ function authOptionsFromEnv(env) {
17731
+ const out = {};
17732
+ const secret = envValue(env, "SWITCHYARD_HOST_ENROLL_SECRET");
17733
+ if (secret)
17734
+ out.hostEnrollSecret = secret;
17735
+ const proxies = parseOrigins(env.SWITCHYARD_TRUSTED_PROXIES);
17736
+ if (proxies)
17737
+ out.trustedProxies = proxies;
17738
+ const issuer = envValue(env, "SWITCHYARD_OIDC_ISSUER");
17739
+ const clientId = envValue(env, "SWITCHYARD_OIDC_CLIENT_ID");
17740
+ if (issuer && clientId) {
17741
+ if (!/^https?:\/\//.test(issuer)) {
17742
+ throw new CliError(`SWITCHYARD_OIDC_ISSUER must be an http(s) URL, got "${issuer}"`);
17743
+ }
17744
+ out.oidc = {
17745
+ issuer,
17746
+ clientId,
17747
+ clientSecret: envValue(env, "SWITCHYARD_OIDC_CLIENT_SECRET") ?? null,
17748
+ label: envValue(env, "SWITCHYARD_OIDC_LABEL") ?? DEFAULT_OIDC_LABEL,
17749
+ scopes: envValue(env, "SWITCHYARD_OIDC_SCOPES") ?? DEFAULT_OIDC_SCOPES,
17750
+ owner: {
17751
+ subject: envValue(env, "SWITCHYARD_OIDC_OWNER_SUBJECT") ?? null,
17752
+ email: envValue(env, "SWITCHYARD_OIDC_OWNER_EMAIL") ?? null,
17753
+ group: envValue(env, "SWITCHYARD_OIDC_OWNER_GROUP") ?? null
17754
+ }
17755
+ };
17756
+ } else if (issuer || clientId) {
17757
+ throw new CliError("Single sign-on needs both SWITCHYARD_OIDC_ISSUER and SWITCHYARD_OIDC_CLIENT_ID");
17758
+ }
17759
+ return out;
17760
+ }
17761
+ function coordinatorOptions(flags, env) {
17762
+ const dataDir = flags.dataDir ? resolve10(flags.dataDir) : defaultDataDir(env);
17763
+ const listen = parseListen(flags.listen ?? env.SWITCHYARD_LISTEN ?? `0.0.0.0:${DEFAULT_PORT}`);
17764
+ const publicUrl = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim() || void 0;
17765
+ if (publicUrl && !/^https?:\/\//.test(publicUrl)) {
17766
+ throw new CliError(`Public URL must start with http:// or https://, got "${publicUrl}"`);
17767
+ }
17768
+ const trustedOrigins = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
17769
+ return {
17770
+ dataDir,
17771
+ listen,
17772
+ publicUrl,
17773
+ trustedOrigins,
17774
+ webDist: resolveWebDist(flags.webDist, env),
17775
+ ...authOptionsFromEnv(env)
17776
+ };
17777
+ }
17778
+ function waitForSignal() {
17779
+ return new Promise((res) => {
17780
+ const on = (sig) => {
17781
+ process.off("SIGINT", on);
17782
+ process.off("SIGTERM", on);
17783
+ res(sig);
17784
+ };
17785
+ process.on("SIGINT", on);
17786
+ process.on("SIGTERM", on);
17787
+ });
17788
+ }
17789
+ async function coordinatorStart(ctx, flags) {
17790
+ const opts = coordinatorOptions(flags, ctx.env);
17791
+ const c = createCoordinator(opts);
17792
+ await c.start();
17793
+ writeEndpoint(opts.dataDir, {
17794
+ localUrl: localUrlFor(opts.listen, c.port),
17795
+ publicUrl: c.config.publicUrl
17796
+ });
17797
+ ctx.err(`Switchyard coordinator listening on ${opts.listen.host}:${c.port} (public URL ${c.config.publicUrl}; data ${opts.dataDir}${opts.webDist ? "" : "; web app not found, API only"})`);
17798
+ const sig = await waitForSignal();
17799
+ ctx.err(`${sig}: stopping coordinator`);
17800
+ await c.stop();
17801
+ }
17802
+ function localUrlFor(listen, port) {
17803
+ const h = listen.host ?? "0.0.0.0";
17804
+ const host = h === "0.0.0.0" || h === "::" || h === "" ? "127.0.0.1" : h.includes(":") ? `[${h}]` : h;
17805
+ return `http://${host}:${port}`;
17806
+ }
17807
+ function dataDirOf(flags, env) {
17808
+ return flags.dataDir ? resolve10(flags.dataDir) : defaultDataDir(env);
17809
+ }
17810
+ function coordinatorBackup(ctx, out, flags) {
17811
+ const r = backup(dataDirOf(flags, ctx.env), out);
17812
+ ctx.out(`Backed up to ${r.file} (schema ${r.migrationLevel})`);
17813
+ }
17814
+ function coordinatorRestore(ctx, file, flags) {
17815
+ const r = restore(dataDirOf(flags, ctx.env), file);
17816
+ ctx.out(`Restored ${r.restoredFrom} (schema ${r.migrationLevel}).`);
17817
+ if (r.previous)
17818
+ ctx.out(`The previous database was kept as ${r.previous}`);
17819
+ if (r.migrationLevel < LATEST_MIGRATION) {
17820
+ ctx.out("Migrations to the current schema run when the coordinator next starts.");
17821
+ }
17822
+ }
17823
+ function coordinatorMigrate(ctx, flags) {
17824
+ const dataDir = dataDirOf(flags, ctx.env);
17825
+ const holder = lockHolder(dataDir);
17826
+ if (holder !== null) {
17827
+ throw new CliError(`Coordinator (pid ${holder}) is running on ${dataDir}; it migrated at start`);
17828
+ }
17829
+ const path = dbPath(dataDir);
17830
+ const existed = existsSync13(path);
17831
+ mkdirSync8(dataDir, { recursive: true });
17832
+ const db = openDb(path);
17833
+ try {
17834
+ const before = existed ? migrationLevel(db) : 0;
17835
+ migrate(db);
17836
+ const after = migrationLevel(db);
17837
+ ctx.out(before === after ? `Schema is current (level ${after}) at ${path}` : `Migrated ${path} from level ${before} to ${after}`);
17838
+ } finally {
17839
+ db.close();
17840
+ }
17841
+ }
17842
+
17843
+ // packages/cli/dist/commands/up.js
17844
+ var DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
17845
+ function upOptions(flags, env) {
17846
+ const port = flags.port === void 0 ? DEFAULT_PORT : Number(flags.port);
17847
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
17848
+ throw new CliError(`Invalid port "${flags.port}"`);
17849
+ }
17850
+ const listenHost = flags.host ?? "0.0.0.0";
17851
+ const dataDir = flags.dataDir ?? defaultDataDir(env);
17852
+ const explicitPublic = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim();
17853
+ const publicUrl = explicitPublic || port !== 0 ? (explicitPublic || `http://localhost:${port}`).replace(/\/+$/, "") : void 0;
17854
+ let trusted = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
17855
+ if (!trusted && publicUrl) {
17856
+ trusted = [new URL(publicUrl).origin];
17857
+ if (!explicitPublic)
17858
+ trusted.push(`http://127.0.0.1:${port}`);
17859
+ }
17860
+ if (flags.dev && trusted)
17861
+ trusted = [.../* @__PURE__ */ new Set([...trusted, ...DEV_ORIGINS])];
17862
+ return {
17863
+ dataDir,
17864
+ listen: { host: listenHost, port },
17865
+ publicUrl,
17866
+ trustedOrigins: trusted,
17867
+ webDist: resolveWebDist(flags.webDist, env),
17868
+ // Same server env as `coordinator start`: shared host secret, trusted proxies and OIDC.
17869
+ ...authOptionsFromEnv(env)
17870
+ };
17871
+ }
17872
+ async function ensureLocalHost(coordinator, localUrl, configPath, name, fetchImpl) {
17873
+ if (existsSync14(configPath)) {
17874
+ const cfg2 = loadConfig(configPath);
17875
+ const known = coordinator.store.hosts.get(cfg2.hostId);
17876
+ if (known && !known.revokedAt) {
17877
+ return { config: { ...cfg2, coordinatorUrl: localUrl }, enrolled: false };
17878
+ }
17879
+ }
17880
+ const api = new ApiClient({
17881
+ coordinatorUrl: localUrl,
17882
+ credential: coordinator.adminToken,
17883
+ fetch: fetchImpl
17884
+ });
17885
+ const e = await api.post("/api/hosts/enrollments", {});
17886
+ const cfg = await enroll({
17887
+ coordinatorUrl: localUrl,
17888
+ token: e.token,
17889
+ name,
17890
+ configPath,
17891
+ fetch: fetchImpl
17892
+ });
17893
+ const { configPath: _p, ...config } = cfg;
17894
+ return { config, enrolled: true };
17895
+ }
17896
+ function openBrowser(url, platform = process.platform, spawnFn = spawn4) {
17897
+ const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
17898
+ if (!cmd)
17899
+ return;
17900
+ try {
17901
+ const child = spawnFn(cmd, [url], { detached: true, stdio: "ignore" });
17902
+ child.on("error", () => {
17903
+ });
17904
+ child.unref();
17905
+ } catch {
17906
+ }
17907
+ }
17908
+ async function firstSignInLink(ctx, coordinator, open2, opener) {
17909
+ if (coordinator.config.oidc || coordinator.auth.activeDeviceCount() > 0)
17910
+ return null;
17911
+ const e = coordinator.auth.createEnrollment("device", ADMIN_DEVICE_ID);
17912
+ await printSignInLink(ctx, e.url, e.expiresAt);
17913
+ if (open2)
17914
+ opener(e.url);
17915
+ return e.url;
17916
+ }
17917
+ async function startStandalone(ctx, flags, deps = {}) {
17918
+ if (flags.dev && !ctx.env.SWITCHYARD_LOG_LEVEL)
17919
+ process.env.SWITCHYARD_LOG_LEVEL = "debug";
17920
+ const opts = upOptions(flags, ctx.env);
17921
+ const coordinator = createCoordinator({ ...opts, setupHint: false });
17922
+ await coordinator.start();
17923
+ let firstLink;
17924
+ try {
17925
+ firstLink = await firstSignInLink(ctx, coordinator, flags.open ?? process.stdout.isTTY === true, deps.open ?? ((url) => openBrowser(url)));
17926
+ } catch (err) {
17927
+ await coordinator.stop();
17928
+ throw err;
17929
+ }
17930
+ const localUrl = localUrlFor(opts.listen, coordinator.port);
17931
+ writeEndpoint(opts.dataDir, { localUrl, publicUrl: coordinator.config.publicUrl });
17932
+ let host;
17933
+ try {
17934
+ const paths = standaloneHostPaths(opts.dataDir);
17935
+ const { config, enrolled } = await ensureLocalHost(coordinator, localUrl, paths.configPath, flags.name ?? hostname2().replace(/\.local$/, ""));
17936
+ if (enrolled)
17937
+ ctx.err(`Enrolled the local host "${config.name}" (${config.hostId})`);
17938
+ host = await runHostAgent({ config, stateDir: paths.stateDir, handleSignals: false });
17939
+ } catch (err) {
17940
+ await coordinator.stop();
17941
+ throw err;
17942
+ }
17943
+ return {
17944
+ coordinator,
17945
+ host,
17946
+ localUrl,
17947
+ firstLink,
17948
+ async stop() {
17949
+ await host.stop();
17950
+ await coordinator.stop();
17951
+ }
17952
+ };
17953
+ }
17954
+ async function up(ctx, flags) {
17955
+ const s = await startStandalone(ctx, flags);
17956
+ const c = s.coordinator;
17957
+ ctx.err([
17958
+ `Switchyard is up: ${c.config.publicUrl} (listening on port ${c.port})`,
17959
+ `Data: ${c.config.dataDir}`,
17960
+ c.config.webDist ? null : "Web app bundle not found: API only (run `pnpm build`)",
17961
+ c.config.oidc ? `Sign in with single sign-on at ${c.config.publicUrl} Terminal: switchyard login --coordinator ${c.config.publicUrl}` : `Another browser: switchyard pair Terminal: switchyard login --coordinator ${c.config.publicUrl}`,
17962
+ "Schedules run only while this process is running."
17963
+ ].filter(Boolean).join("\n"));
17964
+ const sig = await waitForSignal();
17965
+ ctx.err(`${sig}: stopping (Pi sessions on this host are stopped and can be resumed later)`);
17966
+ await s.stop();
17967
+ }
17968
+
17969
+ // packages/cli/dist/commands/auth.js
17970
+ function adminApi(ctx, flags) {
17971
+ const dataDir = flags.dataDir ?? ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
17972
+ const token = readAdminToken(dataDir, ctx.env);
17973
+ if (!token) {
17974
+ throw new CliError(`No admin token: run this on the coordinator machine (looked in ${dataDir}/admin.token) or set SWITCHYARD_ADMIN_TOKEN`);
17975
+ }
17976
+ const url = trimUrl(flags.coordinator ?? readEndpoint(dataDir)?.localUrl ?? DEFAULT_LOCAL_URL);
17977
+ return new ApiClient({ coordinatorUrl: url, credential: token, fetch: ctx.fetch });
17978
+ }
17979
+ function clockTime(iso) {
17980
+ const d = new Date(iso);
17981
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
17982
+ }
17983
+ async function printDeviceLink(ctx, url) {
17984
+ const qr = await QRCode2.toString(url, { type: "terminal", small: true });
17985
+ ctx.out("");
17986
+ ctx.out(` ${url}`);
17987
+ ctx.out("");
17988
+ ctx.out(qr.trimEnd());
17989
+ ctx.out("");
17990
+ }
17991
+ async function printSignInLink(ctx, url, expiresAt) {
17992
+ ctx.out(`Open this single-use sign-in link in your browser (expires ${clockTime(expiresAt)}):`);
17993
+ await printDeviceLink(ctx, url);
17994
+ ctx.out("Then add a passkey in Settings \u203A Sign-in as a backup, or run `switchyard pair` for another browser.");
17995
+ }
17996
+ async function pair(ctx, flags) {
17997
+ const api = adminApi(ctx, flags);
17998
+ const kind = flags.host ? "host" : "device";
17999
+ const e = await api.post("/api/enrollments", { kind });
18000
+ if (flags.json) {
18001
+ ctx.out(JSON.stringify(e, null, 2));
18002
+ return e;
18003
+ }
18004
+ const qr = await QRCode2.toString(e.url, { type: "terminal", small: true });
18005
+ if (kind === "host") {
18006
+ ctx.out(`Host enrollment (single use, expires ${e.expiresAt}). On the host, run:`);
18007
+ ctx.out("");
18008
+ ctx.out(` ${e.command ?? `switchyard host enroll --coordinator <url> --token ${e.token}`}`);
18009
+ ctx.out("");
18010
+ ctx.out(`Token: ${e.token}`);
18011
+ } else {
18012
+ ctx.out(`Open this link in a browser to sign it in (single use, expires ${e.expiresAt}):`);
18013
+ ctx.out("");
18014
+ ctx.out(` ${e.url}`);
18015
+ ctx.out("");
18016
+ ctx.out(qr.trimEnd());
18017
+ ctx.out("");
18018
+ ctx.out(`Terminal: switchyard login --coordinator ${originOf2(e.url)} --token ${e.token}`);
17343
18019
  }
18020
+ ctx.out("The link does not provide network access: the device must already reach this URL.");
18021
+ return e;
18022
+ }
18023
+ function originOf2(url) {
17344
18024
  try {
17345
- await catalog.init();
17346
- } catch (err) {
17347
- log.warn({ err: err.message }, "adapter discovery failed; Pi package adapters are not loaded");
18025
+ return new URL(url).origin;
18026
+ } catch {
18027
+ return url;
17348
18028
  }
17349
- supervisor.emitAdapters();
17350
- link.start();
17351
- let stopping = null;
17352
- const onSignal = (signal) => {
17353
- log.info({ signal }, "stopping host agent");
17354
- void handle.stop().finally(() => process.exit(0));
17355
- };
17356
- if (opts.handleSignals !== false) {
17357
- process.once("SIGTERM", onSignal);
17358
- process.once("SIGINT", onSignal);
18029
+ }
18030
+ function parseEnrollmentInput(token, coordinator) {
18031
+ const t = token.trim();
18032
+ if (/^https?:\/\//.test(t)) {
18033
+ const u = new URL(t);
18034
+ const params = new URLSearchParams(u.hash.replace(/^#/, ""));
18035
+ const fromUrl = params.get("token") ?? u.searchParams.get("token");
18036
+ if (!fromUrl)
18037
+ throw new CliError("That URL has no enrollment token");
18038
+ return { url: trimUrl(coordinator ?? u.origin), token: fromUrl };
17359
18039
  }
17360
- const handle = {
17361
- hostId: config.hostId,
17362
- config,
17363
- db,
17364
- link,
17365
- supervisor,
17366
- router,
17367
- dispatcher,
17368
- pi,
17369
- interruptedOnStart,
17370
- terminalSessions: terminalEnabled,
17371
- get spawnedPiProcesses() {
17372
- return supervisor.spawnedProcesses;
17373
- },
17374
- stop(stopOpts = {}) {
17375
- stopping ??= (async () => {
17376
- process.off("SIGTERM", onSignal);
17377
- process.off("SIGINT", onSignal);
17378
- relay.closeAll();
17379
- await supervisor.stopAll(stopOpts);
17380
- await new Promise((r) => setTimeout(r, 50));
17381
- await link.stop();
17382
- db.close();
17383
- })();
17384
- return stopping;
18040
+ if (!coordinator)
18041
+ throw new CliError("--coordinator <url> is required");
18042
+ return { url: trimUrl(coordinator), token: t };
18043
+ }
18044
+ async function loginWithToken(ctx, flags) {
18045
+ const { url, token } = parseEnrollmentInput(flags.token, flags.coordinator);
18046
+ const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
18047
+ const name = flags.name ?? `${hostname3().replace(/\.local$/, "")} terminal`;
18048
+ const res = await api.post("/api/enroll", {
18049
+ token,
18050
+ name,
18051
+ kind: "terminal"
18052
+ });
18053
+ if (!res.credential)
18054
+ throw new CliError("The coordinator returned no credential");
18055
+ const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
18056
+ writeClientConfig(path, {
18057
+ coordinatorUrl: url,
18058
+ credential: res.credential,
18059
+ deviceId: res.deviceId
18060
+ });
18061
+ ctx.out(`Enrolled "${name}" (${res.deviceId}) with ${url}.`);
18062
+ ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
18063
+ return { path, deviceId: res.deviceId };
18064
+ }
18065
+ var EXPIRED2 = "The sign-in expired before it was approved. Run `switchyard login` again.";
18066
+ async function login(ctx, flags, deps = {}) {
18067
+ if (flags.token)
18068
+ return loginWithToken(ctx, { ...flags, token: flags.token });
18069
+ const path = ctx.paths?.clientConfig ?? clientConfigPath(ctx.env);
18070
+ const raw = flags.coordinator ?? ctx.env.SWITCHYARD_URL ?? readClientConfig(path)?.coordinatorUrl;
18071
+ if (!raw)
18072
+ throw new CliError("--coordinator <url> is required");
18073
+ const url = trimUrl(raw);
18074
+ const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
18075
+ const name = flags.name ?? `${hostname3().replace(/\.local$/, "")} terminal`;
18076
+ const started = await api.post("/api/auth/terminal/start", { name });
18077
+ ctx.out("To sign in this terminal, open this link in a browser where you use Switchyard:");
18078
+ ctx.out("");
18079
+ ctx.out(` ${started.verifyUrl}`);
18080
+ ctx.out("");
18081
+ ctx.out(`Check that it shows the code ${started.userCode}. Waiting for approval (expires ${clockTime(started.expiresAt)})\u2026`);
18082
+ const interactive = process.stdout.isTTY === true && !ctx.env.SSH_CONNECTION && !ctx.env.SSH_TTY;
18083
+ if (flags.open ?? interactive)
18084
+ (deps.open ?? ((u) => openBrowser(u)))(started.verifyUrl);
18085
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18086
+ const now = deps.now ?? Date.now;
18087
+ const deadline = Date.parse(started.expiresAt);
18088
+ for (; ; ) {
18089
+ await sleep3(started.intervalMs);
18090
+ let r;
18091
+ try {
18092
+ r = await api.post("/api/auth/terminal/claim", {
18093
+ userCode: started.userCode,
18094
+ pollToken: started.pollToken
18095
+ });
18096
+ } catch (err) {
18097
+ if (err instanceof ApiRequestError) {
18098
+ if (err.status === 401)
18099
+ throw new CliError(EXPIRED2);
18100
+ if (err.status === 0 && now() < deadline)
18101
+ continue;
18102
+ }
18103
+ throw err;
17385
18104
  }
17386
- };
17387
- return handle;
18105
+ if (r.status === "denied")
18106
+ throw new CliError("The sign-in was denied in the browser.");
18107
+ if (r.status === "done") {
18108
+ writeClientConfig(path, {
18109
+ coordinatorUrl: url,
18110
+ credential: r.credential,
18111
+ deviceId: r.deviceId
18112
+ });
18113
+ ctx.out(`Signed in "${name}" (${r.deviceId}) to ${url}.`);
18114
+ ctx.out(`Credential saved to ${path} (mode 0600). Open a session's Pi terminal with \`switchyard tui <session>\`.`);
18115
+ return { path, deviceId: r.deviceId };
18116
+ }
18117
+ if (now() >= deadline)
18118
+ throw new CliError(EXPIRED2);
18119
+ }
17388
18120
  }
17389
18121
 
17390
18122
  // packages/cli/dist/commands/doctor.js
18123
+ import { execFile as execFile3 } from "node:child_process";
18124
+ import { existsSync as existsSync15, readFileSync as readFileSync9 } from "node:fs";
17391
18125
  function defaultDoctorDeps(ctx) {
17392
18126
  const dataDir = ctx.paths?.dataDir ?? defaultDataDir(ctx.env);
17393
18127
  return {
@@ -17395,7 +18129,7 @@ function defaultDoctorDeps(ctx) {
17395
18129
  detectPi: () => detectPi(ctx.env.SWITCHYARD_PI_BIN ?? "pi"),
17396
18130
  gitVersion: () => new Promise((resolve13) => execFile3("git", ["--version"], (err, stdout) => resolve13(err ? null : stdout.trim()))),
17397
18131
  hostConfigPaths: [defaultConfigPath(ctx.env), standaloneHostPaths(dataDir).configPath],
17398
- readFile: (p) => existsSync14(p) ? readFileSync9(p, "utf8") : null
18132
+ readFile: (p) => existsSync15(p) ? readFileSync9(p, "utf8") : null
17399
18133
  };
17400
18134
  }
17401
18135
  function findHostConfig(deps) {
@@ -17525,13 +18259,13 @@ async function checkSignIn(ctx, url, add) {
17525
18259
  try {
17526
18260
  const api = new ApiClient({ coordinatorUrl: url, credential: null, fetch: ctx.fetch });
17527
18261
  const s = await api.get("/api/setup");
17528
- if (!s.initialized) {
17529
- add("FAIL", "Sign-in", "not set up: run `switchyard init` on the server");
17530
- return;
17531
- }
17532
18262
  const passkey = !s.passkey.available ? `unavailable, ${s.passkey.reason ?? "unsupported"}` : s.passkey.registered ? "registered" : "none registered yet";
17533
18263
  const sso = s.oidc ? `on (${s.oidc.label})` : "off";
17534
- add("PASS", "Sign-in", `set up (passkey: ${passkey}, single sign-on: ${sso})`);
18264
+ if (!s.oidc && !s.passkey.registered) {
18265
+ add("WARN", "Sign-in", "no single sign-on and no passkey: sign in with a link from `switchyard pair` on the server");
18266
+ return;
18267
+ }
18268
+ add("PASS", "Sign-in", `passkey: ${passkey}, single sign-on: ${sso}`);
17535
18269
  } catch (err) {
17536
18270
  add("WARN", "Sign-in", `could not read the setup state: ${err.message}`);
17537
18271
  }
@@ -17547,8 +18281,8 @@ function printDoctor(ctx, results) {
17547
18281
  }
17548
18282
 
17549
18283
  // packages/cli/dist/commands/herdr.js
17550
- import { spawn as spawn4 } from "node:child_process";
17551
- import { closeSync as closeSync2, existsSync as existsSync18, openSync as openSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
18284
+ import { spawn as spawn5 } from "node:child_process";
18285
+ import { closeSync as closeSync2, existsSync as existsSync19, openSync as openSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
17552
18286
  import { join as join21 } from "node:path";
17553
18287
 
17554
18288
  // packages/cli/dist/context.js
@@ -17559,7 +18293,7 @@ function defaultCtx() {
17559
18293
  err: (l) => process.stderr.write(`${l}
17560
18294
  `),
17561
18295
  env: process.env,
17562
- fetch: (input, init2) => fetch(input, init2)
18296
+ fetch: (input, init) => fetch(input, init)
17563
18297
  };
17564
18298
  }
17565
18299
  function connect(ctx, flags) {
@@ -17600,7 +18334,7 @@ function ago(iso, now = Date.now()) {
17600
18334
  }
17601
18335
 
17602
18336
  // packages/cli/dist/herdr/client.js
17603
- import { existsSync as existsSync15, readdirSync } from "node:fs";
18337
+ import { existsSync as existsSync16, readdirSync } from "node:fs";
17604
18338
  import { createConnection as createConnection3 } from "node:net";
17605
18339
  import { homedir as homedir6 } from "node:os";
17606
18340
  import { join as join17 } from "node:path";
@@ -17625,11 +18359,11 @@ function herdrSocketPath(env = process.env, home = homedir6()) {
17625
18359
  if (env.HERDR_SESSION)
17626
18360
  return join17(base, "sessions", env.HERDR_SESSION, "herdr.sock");
17627
18361
  const dflt = join17(base, "herdr.sock");
17628
- if (existsSync15(dflt))
18362
+ if (existsSync16(dflt))
17629
18363
  return dflt;
17630
18364
  let live = [];
17631
18365
  try {
17632
- live = readdirSync(join17(base, "sessions")).map((s) => join17(base, "sessions", s, "herdr.sock")).filter((p) => existsSync15(p));
18366
+ live = readdirSync(join17(base, "sessions")).map((s) => join17(base, "sessions", s, "herdr.sock")).filter((p) => existsSync16(p));
17633
18367
  } catch {
17634
18368
  live = [];
17635
18369
  }
@@ -17742,7 +18476,7 @@ function nextSeq() {
17742
18476
  }
17743
18477
 
17744
18478
  // packages/cli/dist/herdr/daemon.js
17745
- import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
18479
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
17746
18480
  import { join as join19 } from "node:path";
17747
18481
 
17748
18482
  // packages/cli/dist/herdr/mirror.js
@@ -18029,7 +18763,7 @@ function planMirror(st, h, mem, now) {
18029
18763
  }
18030
18764
 
18031
18765
  // packages/cli/dist/herdr/util.js
18032
- import { chmodSync as chmodSync5, existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "node:fs";
18766
+ import { chmodSync as chmodSync5, existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "node:fs";
18033
18767
  import { homedir as homedir7 } from "node:os";
18034
18768
  import { join as join18 } from "node:path";
18035
18769
  function herdrStateDir(env = process.env, home = homedir7()) {
@@ -18059,7 +18793,7 @@ exec sh '${pluginRoot}/switchyard-herdr.sh' "$@"
18059
18793
  return path;
18060
18794
  }
18061
18795
  function launcherExists(env = process.env) {
18062
- return existsSync16(stableLauncherPath(env));
18796
+ return existsSync17(stableLauncherPath(env));
18063
18797
  }
18064
18798
  async function isAtShell(call, paneId) {
18065
18799
  const info = (await call("pane.process_info", { pane_id: paneId }))?.process_info ?? {};
@@ -18258,7 +18992,7 @@ var MirrorDaemon = class {
18258
18992
  const titleOf = (sid) => st.sessions.find((s) => s.id === sid)?.title ?? sid;
18259
18993
  const launcher = this.deps.launcherPath;
18260
18994
  const paneCmd = (sid) => launcherCommand(launcher, ["pane", sid]);
18261
- const exists2 = this.deps.exists ?? existsSync17;
18995
+ const exists2 = this.deps.exists ?? existsSync18;
18262
18996
  const createdNow = {};
18263
18997
  const fallbackCwd = () => {
18264
18998
  const dir = join19(this.deps.stateDir, "spaces");
@@ -19364,13 +20098,13 @@ async function herdrDaemon(ctx, flags) {
19364
20098
  }
19365
20099
  return;
19366
20100
  }
19367
- const running = existsSync18(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : 0;
20101
+ const running = existsSync19(pidFile) ? Number(readFileSync12(pidFile, "utf8").trim()) : 0;
19368
20102
  if (flags.ensure) {
19369
20103
  if (running && pidAlive2(running))
19370
20104
  return;
19371
20105
  const fd = openSync2(logFile, "a", 384);
19372
20106
  try {
19373
- const child = spawn4(process.execPath, [process.argv[1], "herdr", "daemon"], {
20107
+ const child = spawn5(process.execPath, [process.argv[1], "herdr", "daemon"], {
19374
20108
  detached: true,
19375
20109
  stdio: ["ignore", fd, fd],
19376
20110
  env: { ...process.env, SWITCHYARD_HERDR_LAUNCHER: launcherPath }
@@ -19432,14 +20166,14 @@ async function herdrStatus(ctx, flags) {
19432
20166
  }
19433
20167
 
19434
20168
  // packages/cli/dist/commands/host.js
19435
- import { existsSync as existsSync20, mkdirSync as mkdirSync12 } from "node:fs";
19436
- import { hostname as hostname3 } from "node:os";
20169
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12 } from "node:fs";
20170
+ import { hostname as hostname4 } from "node:os";
19437
20171
  import { dirname as dirname10, join as join23, resolve as resolve12 } from "node:path";
19438
20172
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
19439
20173
 
19440
20174
  // packages/cli/dist/commands/service.js
19441
20175
  import { execFile as execFile4 } from "node:child_process";
19442
- import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
20176
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
19443
20177
  import { homedir as homedir8 } from "node:os";
19444
20178
  import { dirname as dirname9, join as join22, resolve as resolve11 } from "node:path";
19445
20179
  var LAUNCHD_LABEL = "com.switchyard.host";
@@ -19531,7 +20265,7 @@ function systemdEscape(s, exec = true) {
19531
20265
  }
19532
20266
  function loadTemplate(kind, dir = deployDir()) {
19533
20267
  const p = join22(dir, TEMPLATE_FILES[kind]);
19534
- return existsSync19(p) ? readFileSync13(p, "utf8") : TEMPLATES[kind];
20268
+ return existsSync20(p) ? readFileSync13(p, "utf8") : TEMPLATES[kind];
19535
20269
  }
19536
20270
  function renderService(kind, vars, template = TEMPLATES[kind]) {
19537
20271
  for (const [k, v] of Object.entries(vars)) {
@@ -19575,7 +20309,7 @@ function loadCommands(kind, file, uid, instance) {
19575
20309
  ["systemctl", "--user", "restart", unit]
19576
20310
  ];
19577
20311
  }
19578
- function stableNodePath(execPath, exists2 = existsSync19) {
20312
+ function stableNodePath(execPath, exists2 = existsSync20) {
19579
20313
  const m = execPath.match(/^(.*)\/Cellar\/node(?:@[^/]+)?\/[^/]+\/bin\/node$/);
19580
20314
  if (m) {
19581
20315
  const candidate = `${m[1]}/bin/node`;
@@ -19599,7 +20333,7 @@ async function installService(ctx, flags, deps = defaultInstallDeps()) {
19599
20333
  throw new CliError("--instance needs --config and --state-dir, so the second host agent keeps its own enrollment and state");
19600
20334
  }
19601
20335
  const config = flags.config ?? defaultConfigPath(ctx.env, deps.home);
19602
- if (!existsSync19(config) && !flags.print) {
20336
+ if (!existsSync20(config) && !flags.print) {
19603
20337
  throw new CliError(`No host config at ${config}. Enroll first: switchyard host enroll --coordinator <url> --token <t>`);
19604
20338
  }
19605
20339
  const stateDir = flags.stateDir ? resolve11(flags.stateDir) : defaultStateDir(ctx.env, deps.platform, deps.home);
@@ -19649,7 +20383,7 @@ async function hostEnroll(ctx, flags) {
19649
20383
  if (!flags.token === !flags.secret) {
19650
20384
  throw new CliError("Pass exactly one of --token or --secret");
19651
20385
  }
19652
- const name = flags.name ?? hostname3().replace(/\.local$/, "");
20386
+ const name = flags.name ?? hostname4().replace(/\.local$/, "");
19653
20387
  const cfg = await enroll({
19654
20388
  coordinatorUrl: flags.coordinator,
19655
20389
  ...flags.token ? { token: flags.token } : { secret: flags.secret },
@@ -19663,7 +20397,7 @@ async function hostEnroll(ctx, flags) {
19663
20397
  }
19664
20398
  async function hostRun(ctx, flags) {
19665
20399
  const configPath = flags.config ?? defaultConfigPath(ctx.env);
19666
- if (!existsSync20(configPath)) {
20400
+ if (!existsSync21(configPath)) {
19667
20401
  ctx.err(`No host config at ${configPath} yet: waiting for it (with SWITCHYARD_HOST_ENROLL_SECRET and SWITCHYARD_COORDINATOR_URL set, the host enrolls itself).`);
19668
20402
  }
19669
20403
  const agent = await runHostAgent({
@@ -19679,10 +20413,10 @@ async function hostRun(ctx, flags) {
19679
20413
  function hostBackup(ctx, out, flags) {
19680
20414
  const stateDir = flags.stateDir ?? defaultStateDir(ctx.env);
19681
20415
  const src = join23(stateDir, "host.db");
19682
- if (!existsSync20(src))
20416
+ if (!existsSync21(src))
19683
20417
  throw new CliError(`No host database at ${src}`);
19684
20418
  const dest = resolve12(out);
19685
- if (existsSync20(dest))
20419
+ if (existsSync21(dest))
19686
20420
  throw new CliError(`${dest} already exists`);
19687
20421
  mkdirSync12(dirname10(dest), { recursive: true });
19688
20422
  const db = new DatabaseSync4(src);
@@ -19699,7 +20433,7 @@ async function hostStatus(ctx, flags) {
19699
20433
  const configPath = flags.config ?? defaultConfigPath(ctx.env);
19700
20434
  const stateDir = flags.stateDir ?? defaultStateDir(ctx.env);
19701
20435
  const status = { configPath, stateDir };
19702
- if (!existsSync20(configPath)) {
20436
+ if (!existsSync21(configPath)) {
19703
20437
  status.enrolled = false;
19704
20438
  } else {
19705
20439
  const cfg = loadConfig(configPath);
@@ -19732,7 +20466,7 @@ async function hostStatus(ctx, flags) {
19732
20466
  try {
19733
20467
  const kind = serviceKindFor(process.platform);
19734
20468
  const file = serviceFilePath(kind);
19735
- status.service = { kind, file, installed: existsSync20(file) };
20469
+ status.service = { kind, file, installed: existsSync21(file) };
19736
20470
  } catch {
19737
20471
  status.service = null;
19738
20472
  }
@@ -19884,136 +20618,6 @@ async function tuiCommand(ctx, sessionId, flags, run = runRawTerminal) {
19884
20618
  throw new CliError(result.message);
19885
20619
  }
19886
20620
 
19887
- // packages/cli/dist/commands/up.js
19888
- import { spawn as spawn5 } from "node:child_process";
19889
- import { existsSync as existsSync21 } from "node:fs";
19890
- import { hostname as hostname4 } from "node:os";
19891
- var DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
19892
- function upOptions(flags, env) {
19893
- const port = flags.port === void 0 ? DEFAULT_PORT : Number(flags.port);
19894
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
19895
- throw new CliError(`Invalid port "${flags.port}"`);
19896
- }
19897
- const listenHost = flags.host ?? "0.0.0.0";
19898
- const dataDir = flags.dataDir ?? defaultDataDir(env);
19899
- const explicitPublic = (flags.publicUrl ?? env.SWITCHYARD_PUBLIC_URL)?.trim();
19900
- const publicUrl = explicitPublic || port !== 0 ? (explicitPublic || `http://localhost:${port}`).replace(/\/+$/, "") : void 0;
19901
- let trusted = parseOrigins(flags.trustedOrigins ?? env.SWITCHYARD_TRUSTED_ORIGINS);
19902
- if (!trusted && publicUrl) {
19903
- trusted = [new URL(publicUrl).origin];
19904
- if (!explicitPublic)
19905
- trusted.push(`http://127.0.0.1:${port}`);
19906
- }
19907
- if (flags.dev && trusted)
19908
- trusted = [.../* @__PURE__ */ new Set([...trusted, ...DEV_ORIGINS])];
19909
- return {
19910
- dataDir,
19911
- listen: { host: listenHost, port },
19912
- publicUrl,
19913
- trustedOrigins: trusted,
19914
- webDist: resolveWebDist(flags.webDist, env),
19915
- // Same server env as `coordinator start`: shared host secret, trusted proxies and OIDC.
19916
- ...authOptionsFromEnv(env)
19917
- };
19918
- }
19919
- async function ensureLocalHost(coordinator, localUrl, configPath, name, fetchImpl) {
19920
- if (existsSync21(configPath)) {
19921
- const cfg2 = loadConfig(configPath);
19922
- const known = coordinator.store.hosts.get(cfg2.hostId);
19923
- if (known && !known.revokedAt) {
19924
- return { config: { ...cfg2, coordinatorUrl: localUrl }, enrolled: false };
19925
- }
19926
- }
19927
- const api = new ApiClient({
19928
- coordinatorUrl: localUrl,
19929
- credential: coordinator.adminToken,
19930
- fetch: fetchImpl
19931
- });
19932
- const e = await api.post("/api/hosts/enrollments", {});
19933
- const cfg = await enroll({
19934
- coordinatorUrl: localUrl,
19935
- token: e.token,
19936
- name,
19937
- configPath,
19938
- fetch: fetchImpl
19939
- });
19940
- const { configPath: _p, ...config } = cfg;
19941
- return { config, enrolled: true };
19942
- }
19943
- function openBrowser(url, platform = process.platform, spawnFn = spawn5) {
19944
- const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
19945
- if (!cmd)
19946
- return;
19947
- try {
19948
- const child = spawnFn(cmd, [url], { detached: true, stdio: "ignore" });
19949
- child.on("error", () => {
19950
- });
19951
- child.unref();
19952
- } catch {
19953
- }
19954
- }
19955
- async function initializeLocally(ctx, coordinator, open2, opener) {
19956
- if (coordinator.owner.isInitialized())
19957
- return null;
19958
- coordinator.owner.markInitialized();
19959
- const e = coordinator.auth.createEnrollment("device", ADMIN_DEVICE_ID);
19960
- await printInit(ctx, { alreadyInitialized: false, enrollment: e });
19961
- if (open2)
19962
- opener(e.url);
19963
- return e.url;
19964
- }
19965
- async function startStandalone(ctx, flags, deps = {}) {
19966
- if (flags.dev && !ctx.env.SWITCHYARD_LOG_LEVEL)
19967
- process.env.SWITCHYARD_LOG_LEVEL = "debug";
19968
- const opts = upOptions(flags, ctx.env);
19969
- const coordinator = createCoordinator({ ...opts, setupHint: false });
19970
- await coordinator.start();
19971
- let firstLink;
19972
- try {
19973
- firstLink = await initializeLocally(ctx, coordinator, flags.open ?? process.stdout.isTTY === true, deps.open ?? ((url) => openBrowser(url)));
19974
- } catch (err) {
19975
- await coordinator.stop();
19976
- throw err;
19977
- }
19978
- const localUrl = localUrlFor(opts.listen, coordinator.port);
19979
- writeEndpoint(opts.dataDir, { localUrl, publicUrl: coordinator.config.publicUrl });
19980
- let host;
19981
- try {
19982
- const paths = standaloneHostPaths(opts.dataDir);
19983
- const { config, enrolled } = await ensureLocalHost(coordinator, localUrl, paths.configPath, flags.name ?? hostname4().replace(/\.local$/, ""));
19984
- if (enrolled)
19985
- ctx.err(`Enrolled the local host "${config.name}" (${config.hostId})`);
19986
- host = await runHostAgent({ config, stateDir: paths.stateDir, handleSignals: false });
19987
- } catch (err) {
19988
- await coordinator.stop();
19989
- throw err;
19990
- }
19991
- return {
19992
- coordinator,
19993
- host,
19994
- localUrl,
19995
- firstLink,
19996
- async stop() {
19997
- await host.stop();
19998
- await coordinator.stop();
19999
- }
20000
- };
20001
- }
20002
- async function up(ctx, flags) {
20003
- const s = await startStandalone(ctx, flags);
20004
- const c = s.coordinator;
20005
- ctx.err([
20006
- `Switchyard is up: ${c.config.publicUrl} (listening on port ${c.port})`,
20007
- `Data: ${c.config.dataDir}`,
20008
- c.config.webDist ? null : "Web app bundle not found: API only (run `pnpm build`)",
20009
- "Another device: switchyard pair Terminal: switchyard tui <session>",
20010
- "Schedules run only while this process is running."
20011
- ].filter(Boolean).join("\n"));
20012
- const sig = await waitForSignal();
20013
- ctx.err(`${sig}: stopping (Pi sessions on this host are stopped and can be resumed later)`);
20014
- await s.stop();
20015
- }
20016
-
20017
20621
  // packages/cli/dist/commands/work.js
20018
20622
  function byId2(list) {
20019
20623
  return new Map(list.map((x) => [x.id, x]));
@@ -20221,13 +20825,10 @@ function buildProgram(ctx = defaultCtx()) {
20221
20825
  });
20222
20826
  host.command("backup <out>").description("Consistent backup of the host database").option("--state-dir <dir>", "state dir (env SWITCHYARD_STATE_DIR)").action((out, o) => hostBackup(ctx, out, o));
20223
20827
  host.command("status").description("Show this host's enrollment, link and Pi status").option("--config <path>", "host.json path").option("--state-dir <dir>", "state dir").option("--json", "JSON output").action((o) => hostStatus(ctx, o));
20224
- program.command("init").description("Set up this server and print a single-use sign-in link for your first device").option("--coordinator <url>", "local coordinator URL (default: the running one)").option("--data-dir <dir>", "data dir holding admin.token").option("--json", "JSON output").action(async (o) => {
20225
- await init(ctx, o);
20226
- });
20227
- program.command("pair").description("Create a single-use link and QR for another device (or --host); also the recovery path when every device is lost").option("--host", "enroll a host instead of a device").option("--coordinator <url>", "local coordinator URL (default: the running one)").option("--data-dir <dir>", "data dir holding admin.token").option("--json", "JSON output").action(async (o) => {
20828
+ program.command("pair").description("Print a single-use sign-in link and QR for a browser (or --host for a host token), using the admin token: for local servers and recovery").option("--host", "enroll a host instead of a device").option("--coordinator <url>", "local coordinator URL (default: the running one)").option("--data-dir <dir>", "data dir holding admin.token").option("--json", "JSON output").action(async (o) => {
20228
20829
  await pair(ctx, o);
20229
20830
  });
20230
- program.command("login").description("Enroll this terminal as a device (writes ~/.config/switchyard/client.json)").option("--coordinator <url>", "coordinator URL").requiredOption("--token <token>", "device enrollment token or link (switchyard pair)").option("--name <name>", "device name").action(async (o) => {
20831
+ program.command("login").description("Sign this terminal in by approving it in a browser (writes ~/.config/switchyard/client.json)").option("--coordinator <url>", "coordinator URL (default: SWITCHYARD_URL or client.json)").option("--name <name>", "device name").option("--token <token>", "single-use sign-in link or token from `switchyard pair`, instead of browser approval").option("--open", "open the approval page in a browser (default: when interactive and not over SSH)").option("--no-open", "never open a browser").action(async (o) => {
20231
20832
  await login(ctx, o);
20232
20833
  });
20233
20834
  connectionOptions(program.command("doctor").description("Check Node, Pi, git, coordinator, credentials, host link and push")).action(async (o) => {