@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.
- package/README.md +9 -7
- package/dist/bridge/pi-model.js +44 -0
- package/dist/bridge/switchyard-bridge.js +24 -2
- package/dist/bridge/switchyard-terminal.js +146 -4
- package/dist/switchyard.mjs +1301 -700
- package/package.json +1 -1
- package/web-dist/assets/AutomationView-BIW_pO0M.js +2 -0
- package/web-dist/assets/AutomationView-BIW_pO0M.js.map +1 -0
- package/web-dist/assets/Badge-BiDp7NKI.js +2 -0
- package/web-dist/assets/Badge-BiDp7NKI.js.map +1 -0
- package/web-dist/assets/InteractionCard-CWxyDv81.js +6 -0
- package/web-dist/assets/InteractionCard-CWxyDv81.js.map +1 -0
- package/web-dist/assets/{Menu-C4GUY_ym.js → Menu-BDt1bxwz.js} +2 -2
- package/web-dist/assets/Menu-BDt1bxwz.js.map +1 -0
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js → SegmentedControl-D-2BCZzd.js} +2 -2
- package/web-dist/assets/{SegmentedControl-ChdVilXb.js.map → SegmentedControl-D-2BCZzd.js.map} +1 -1
- package/web-dist/assets/Tabs-ByiPklAw.js +184 -0
- package/web-dist/assets/Tabs-ByiPklAw.js.map +1 -0
- package/web-dist/assets/TextArea-BYr88SnV.js +2 -0
- package/web-dist/assets/TextArea-BYr88SnV.js.map +1 -0
- package/web-dist/assets/attention-Bya8XHKI.js +2 -0
- package/web-dist/assets/attention-Bya8XHKI.js.map +1 -0
- package/web-dist/assets/automations-DiXKRcnE.js +2 -0
- package/web-dist/assets/automations-DiXKRcnE.js.map +1 -0
- package/web-dist/assets/board-BrLaJwLJ.js +10 -0
- package/web-dist/assets/board-BrLaJwLJ.js.map +1 -0
- package/web-dist/assets/c-Cz4nF6n_.js +1 -0
- package/web-dist/assets/c-SSSlmE39.js +2 -0
- package/web-dist/assets/c-SSSlmE39.js.map +1 -0
- package/web-dist/assets/code-DKLO8bzz.js +11 -0
- package/web-dist/assets/code-DKLO8bzz.js.map +1 -0
- package/web-dist/assets/cpp-Cts1PyIS.js +2 -0
- package/web-dist/assets/cpp-Cts1PyIS.js.map +1 -0
- package/web-dist/assets/cpp-DzjeI0uH.js +1 -0
- package/web-dist/assets/css-BGIxQoYs.js +2 -0
- package/web-dist/assets/css-BGIxQoYs.js.map +1 -0
- package/web-dist/assets/css-DgaLhXQH.js +1 -0
- package/web-dist/assets/describe-DnvH6_fl.js +5 -0
- package/web-dist/assets/describe-DnvH6_fl.js.map +1 -0
- package/web-dist/assets/diff-woXpYk--.js +2 -0
- package/web-dist/assets/diff-woXpYk--.js.map +1 -0
- package/web-dist/assets/dist-BE_V9dwJ.js +56 -0
- package/web-dist/assets/dist-BE_V9dwJ.js.map +1 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js +2 -0
- package/web-dist/assets/dockerfile-IyjqRm3v.js.map +1 -0
- package/web-dist/assets/enroll-DUDpbJoB.js +2 -0
- package/web-dist/assets/enroll-DUDpbJoB.js.map +1 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js +2 -0
- package/web-dist/assets/github-dark-C-LZuMrd.js.map +1 -0
- package/web-dist/assets/github-light-EUqPIrTm.js +2 -0
- package/web-dist/assets/github-light-EUqPIrTm.js.map +1 -0
- package/web-dist/assets/go-rLFTqkRN.js +2 -0
- package/web-dist/assets/go-rLFTqkRN.js.map +1 -0
- package/web-dist/assets/html-BMn1ogTm.js +2 -0
- package/web-dist/assets/html-BMn1ogTm.js.map +1 -0
- package/web-dist/assets/html-BWDvXlHg.js +1 -0
- package/web-dist/assets/index-DvRcMGEe.css +2 -0
- package/web-dist/assets/index-vIzH1m8u.js +11 -0
- package/web-dist/assets/index-vIzH1m8u.js.map +1 -0
- package/web-dist/assets/java-BtIQNnN0.js +2 -0
- package/web-dist/assets/java-BtIQNnN0.js.map +1 -0
- package/web-dist/assets/java-_ATy9zCa.js +1 -0
- package/web-dist/assets/javascript-BzbQS41l.js +2 -0
- package/web-dist/assets/javascript-BzbQS41l.js.map +1 -0
- package/web-dist/assets/javascript-CpIGwZ_Y.js +1 -0
- package/web-dist/assets/json-qhed-kSA.js +2 -0
- package/web-dist/assets/json-qhed-kSA.js.map +1 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js +2 -0
- package/web-dist/assets/jsonc-CYpm1nAK.js.map +1 -0
- package/web-dist/assets/jsx-C4yDKi0A.js +1 -0
- package/web-dist/assets/jsx-D3KKoC5L.js +2 -0
- package/web-dist/assets/jsx-D3KKoC5L.js.map +1 -0
- package/web-dist/assets/logic-xb-QF00E.js +2 -0
- package/web-dist/assets/logic-xb-QF00E.js.map +1 -0
- package/web-dist/assets/lua-DjuecEsk.js +2 -0
- package/web-dist/assets/lua-DjuecEsk.js.map +1 -0
- package/web-dist/assets/lua-fPRBWD-V.js +1 -0
- package/web-dist/assets/{more-syTnSpW3.js → more-BP5x45cs.js} +2 -2
- package/web-dist/assets/{more-syTnSpW3.js.map → more-BP5x45cs.js.map} +1 -1
- package/web-dist/assets/now--XjJC37b.js +2 -0
- package/web-dist/assets/now--XjJC37b.js.map +1 -0
- package/web-dist/assets/python-gzcpVVnB.js +2 -0
- package/web-dist/assets/python-gzcpVVnB.js.map +1 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js +2 -0
- package/web-dist/assets/ruby-Bx-U2qjP.js.map +1 -0
- package/web-dist/assets/rust-DpiIf5JO.js +2 -0
- package/web-dist/assets/rust-DpiIf5JO.js.map +1 -0
- package/web-dist/assets/scss-8bKmll0B.js +2 -0
- package/web-dist/assets/scss-8bKmll0B.js.map +1 -0
- package/web-dist/assets/send-BKjswHxB.js +2 -0
- package/web-dist/assets/send-BKjswHxB.js.map +1 -0
- package/web-dist/assets/settings-D8T1esWq.js +7 -0
- package/web-dist/assets/settings-D8T1esWq.js.map +1 -0
- package/web-dist/assets/shared-CZ7fcu_B.js +3 -0
- package/web-dist/assets/shared-CZ7fcu_B.js.map +1 -0
- package/web-dist/assets/shellscript-CVTB3cr6.js +1 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js +2 -0
- package/web-dist/assets/shellscript-CqHdAlCp.js.map +1 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js +2 -0
- package/web-dist/assets/sliders-horizontal-DAoirila.js.map +1 -0
- package/web-dist/assets/sql-CNu3Or2W.js +2 -0
- package/web-dist/assets/sql-CNu3Or2W.js.map +1 -0
- package/web-dist/assets/sql-CfM2IJm3.js +1 -0
- package/web-dist/assets/states--geW2Ntl.js +2 -0
- package/web-dist/assets/states--geW2Ntl.js.map +1 -0
- package/web-dist/assets/swift-CyEgAFGc.js +2 -0
- package/web-dist/assets/swift-CyEgAFGc.js.map +1 -0
- package/web-dist/assets/terminal-login-BJxV78au.js +2 -0
- package/web-dist/assets/terminal-login-BJxV78au.js.map +1 -0
- package/web-dist/assets/toml-CcmNWLt0.js +2 -0
- package/web-dist/assets/toml-CcmNWLt0.js.map +1 -0
- package/web-dist/assets/tsx-BOvsFOI6.js +2 -0
- package/web-dist/assets/tsx-BOvsFOI6.js.map +1 -0
- package/web-dist/assets/tsx-CGosfAUg.js +1 -0
- package/web-dist/assets/typescript-Bb1J8qyy.js +1 -0
- package/web-dist/assets/typescript-j_1H8WHN.js +2 -0
- package/web-dist/assets/typescript-j_1H8WHN.js.map +1 -0
- package/web-dist/assets/useNow-jxdoOC_1.js +2 -0
- package/web-dist/assets/{useNow-BQpDYijR.js.map → useNow-jxdoOC_1.js.map} +1 -1
- package/web-dist/assets/xml-DuKfbPI6.js +1 -0
- package/web-dist/assets/xml-WY0zc4PG.js +2 -0
- package/web-dist/assets/xml-WY0zc4PG.js.map +1 -0
- package/web-dist/assets/yaml-DDA_XMNu.js +1 -0
- package/web-dist/assets/yaml-DijMHRDA.js +2 -0
- package/web-dist/assets/yaml-DijMHRDA.js.map +1 -0
- package/web-dist/index.html +4 -3
- package/web-dist/sw.js +1 -1
- package/web-dist/sw.js.map +1 -1
- package/web-dist/assets/AutomationView-CFohp0SQ.js +0 -2
- package/web-dist/assets/AutomationView-CFohp0SQ.js.map +0 -1
- package/web-dist/assets/InteractionCard-B-xmpTNG.js +0 -4
- package/web-dist/assets/InteractionCard-B-xmpTNG.js.map +0 -1
- package/web-dist/assets/Markdown-jXkvZeRp.js +0 -31
- package/web-dist/assets/Markdown-jXkvZeRp.js.map +0 -1
- package/web-dist/assets/Menu-C4GUY_ym.js.map +0 -1
- package/web-dist/assets/Overlay--v0ur2lj.js +0 -2
- package/web-dist/assets/Overlay--v0ur2lj.js.map +0 -1
- package/web-dist/assets/TextArea-BsQHOQMe.js +0 -2
- package/web-dist/assets/TextArea-BsQHOQMe.js.map +0 -1
- package/web-dist/assets/attention-q6_xlnT4.js +0 -2
- package/web-dist/assets/attention-q6_xlnT4.js.map +0 -1
- package/web-dist/assets/automations-CyA83OAD.js +0 -2
- package/web-dist/assets/automations-CyA83OAD.js.map +0 -1
- package/web-dist/assets/board-BFvNcFhk.js +0 -10
- package/web-dist/assets/board-BFvNcFhk.js.map +0 -1
- package/web-dist/assets/code-DavnmlQN.js +0 -8
- package/web-dist/assets/code-DavnmlQN.js.map +0 -1
- package/web-dist/assets/describe-BirsjlUl.js +0 -5
- package/web-dist/assets/describe-BirsjlUl.js.map +0 -1
- package/web-dist/assets/dist-C-h8FC-n.js +0 -56
- package/web-dist/assets/dist-C-h8FC-n.js.map +0 -1
- package/web-dist/assets/enroll-C7UBC20s.js +0 -2
- package/web-dist/assets/enroll-C7UBC20s.js.map +0 -1
- package/web-dist/assets/index-ByiuDoG0.css +0 -2
- package/web-dist/assets/index-IdzOP9GE.js +0 -11
- package/web-dist/assets/index-IdzOP9GE.js.map +0 -1
- package/web-dist/assets/logic-D_Eq9DMW.js +0 -2
- package/web-dist/assets/logic-D_Eq9DMW.js.map +0 -1
- package/web-dist/assets/now-BqsLS-tK.js +0 -2
- package/web-dist/assets/now-BqsLS-tK.js.map +0 -1
- package/web-dist/assets/settings-DRj7UPtP.js +0 -8
- package/web-dist/assets/settings-DRj7UPtP.js.map +0 -1
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js +0 -2
- package/web-dist/assets/sliders-horizontal-S3tmwxsP.js.map +0 -1
- package/web-dist/assets/states-DrTQuYXw.js +0 -2
- package/web-dist/assets/states-DrTQuYXw.js.map +0 -1
- package/web-dist/assets/useNow-BQpDYijR.js +0 -2
package/dist/switchyard.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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/
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
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
|
-
|
|
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}/
|
|
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).
|
|
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:
|
|
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({
|
|
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(`/
|
|
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
|
|
9117
|
+
function seedDefaultProfiles(ctx) {
|
|
9137
9118
|
ctx.store.tx(() => {
|
|
9138
|
-
if (ctx.store.profiles.count()
|
|
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.
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
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
|
-
|
|
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.
|
|
9481
|
+
app.get("/api/sessions/:id/models", async (req) => {
|
|
9476
9482
|
const s = get(idOf(req));
|
|
9477
|
-
|
|
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(
|
|
9677
|
-
let s = this.cache.get(
|
|
9729
|
+
get(sql4) {
|
|
9730
|
+
let s = this.cache.get(sql4);
|
|
9678
9731
|
if (!s) {
|
|
9679
|
-
s = this.db.prepare(
|
|
9680
|
-
this.cache.set(
|
|
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
|
|
9712
|
-
return this.st.get(
|
|
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
|
|
9716
|
-
return this.st.get(
|
|
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
|
|
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 ?? [
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
10679
|
-
log.info("
|
|
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("
|
|
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
|
|
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
|
|
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 :
|
|
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 (!
|
|
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) =>
|
|
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 (
|
|
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
|
|
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 =
|
|
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:
|
|
11201
|
-
webRendererPath: manifest.webRenderer ?
|
|
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
|
|
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
|
|
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 (!
|
|
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 && !
|
|
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 (!
|
|
11748
|
+
if (!existsSync7(root))
|
|
11718
11749
|
return [];
|
|
11719
|
-
if (
|
|
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) =>
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
12971
|
-
import { homedir as homedir4, hostname
|
|
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
|
-
|
|
13047
|
+
mkdirSync4(d, { recursive: true, mode: 448 });
|
|
13007
13048
|
return p;
|
|
13008
13049
|
}
|
|
13009
13050
|
function loadConfig(path) {
|
|
13010
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
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() ||
|
|
13153
|
+
const name = env.SWITCHYARD_HOST_NAME?.trim() || hostname();
|
|
13113
13154
|
for (; ; ) {
|
|
13114
13155
|
if (signal?.aborted)
|
|
13115
13156
|
throw abortError(signal);
|
|
13116
|
-
if (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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(
|
|
13327
|
-
const r = this.db.prepare(
|
|
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(
|
|
13331
|
-
return this.db.prepare(
|
|
13371
|
+
get(sql4, params = []) {
|
|
13372
|
+
return this.db.prepare(sql4).get(...params);
|
|
13332
13373
|
}
|
|
13333
|
-
all(
|
|
13334
|
-
return this.db.prepare(
|
|
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(
|
|
13345
|
-
const r = this.get(
|
|
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(
|
|
13349
|
-
return this.all(
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
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 && !
|
|
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
|
-
/**
|
|
15345
|
-
async
|
|
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: "
|
|
15361
|
-
});
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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 &&
|
|
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:
|
|
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
|
-
|
|
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 (!
|
|
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,
|
|
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
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
log.
|
|
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
|
-
|
|
17346
|
-
} catch
|
|
17347
|
-
|
|
18025
|
+
return new URL(url).origin;
|
|
18026
|
+
} catch {
|
|
18027
|
+
return url;
|
|
17348
18028
|
}
|
|
17349
|
-
|
|
17350
|
-
|
|
17351
|
-
|
|
17352
|
-
|
|
17353
|
-
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
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
|
-
|
|
17361
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
17551
|
-
import { closeSync as closeSync2, existsSync as
|
|
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,
|
|
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
|
|
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 (
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
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
|
|
19436
|
-
import { hostname as
|
|
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
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 ??
|
|
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 (!
|
|
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 (!
|
|
20416
|
+
if (!existsSync21(src))
|
|
19683
20417
|
throw new CliError(`No host database at ${src}`);
|
|
19684
20418
|
const dest = resolve12(out);
|
|
19685
|
-
if (
|
|
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 (!
|
|
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:
|
|
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("
|
|
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("
|
|
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) => {
|