ds-harness-remote 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/cli.d.ts +2 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +16 -14
  4. package/dist/cli.js.map +1 -1
  5. package/dist/client-locale.d.ts +5 -0
  6. package/dist/client-locale.d.ts.map +1 -1
  7. package/dist/client-locale.js +10 -0
  8. package/dist/client-locale.js.map +1 -1
  9. package/dist/client-runtime.d.ts +2 -0
  10. package/dist/client-runtime.d.ts.map +1 -1
  11. package/dist/client-runtime.js +26 -2
  12. package/dist/client-runtime.js.map +1 -1
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.github.js +242 -30
  15. package/dist/client.js +242 -30
  16. package/dist/client.js.map +1 -1
  17. package/dist/codex/app-server.js +2 -0
  18. package/dist/codex/app-server.js.map +1 -1
  19. package/dist/codex/domain.d.ts.map +1 -1
  20. package/dist/codex/domain.js +29 -7
  21. package/dist/codex/domain.js.map +1 -1
  22. package/dist/harness-remote-bridge.d.ts +1 -1
  23. package/dist/harness-remote-bridge.d.ts.map +1 -1
  24. package/dist/harness-remote-bridge.js +2 -0
  25. package/dist/harness-remote-bridge.js.map +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +382 -60
  28. package/dist/index.js.map +4 -4
  29. package/dist/plugin-route-bridge.d.ts +23 -0
  30. package/dist/plugin-route-bridge.d.ts.map +1 -0
  31. package/dist/plugin-route-bridge.js +82 -0
  32. package/dist/plugin-route-bridge.js.map +1 -0
  33. package/dist/plugin-route-client.d.ts +20 -0
  34. package/dist/plugin-route-client.d.ts.map +1 -0
  35. package/dist/plugin-route-client.js +242 -0
  36. package/dist/plugin-route-client.js.map +1 -0
  37. package/dist/plugin-route-dispatcher.d.ts +10 -0
  38. package/dist/plugin-route-dispatcher.d.ts.map +1 -0
  39. package/dist/plugin-route-dispatcher.js +61 -0
  40. package/dist/plugin-route-dispatcher.js.map +1 -0
  41. package/dist/rpc-router.d.ts +4 -1
  42. package/dist/rpc-router.d.ts.map +1 -1
  43. package/dist/rpc-router.js +11 -1
  44. package/dist/rpc-router.js.map +1 -1
  45. package/dist/server-api.d.ts +9 -1
  46. package/dist/server-api.d.ts.map +1 -1
  47. package/dist/server-api.js +61 -2
  48. package/dist/server-api.js.map +1 -1
  49. package/dist/service.d.ts +5 -2
  50. package/dist/service.d.ts.map +1 -1
  51. package/dist/service.js +18 -2
  52. package/dist/service.js.map +1 -1
  53. package/dist/tui-command.d.ts.map +1 -1
  54. package/dist/tui-command.js +58 -18
  55. package/dist/tui-command.js.map +1 -1
  56. package/dist/typert-gateway-contract.d.ts +4 -0
  57. package/dist/typert-gateway-contract.d.ts.map +1 -1
  58. package/dist/typert-gateway-switch.d.ts.map +1 -1
  59. package/dist/typert-gateway-switch.js +16 -1
  60. package/dist/typert-gateway-switch.js.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/dist/version.js +1 -1
  63. package/dsh-plugin.json +1 -1
  64. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4185,7 +4185,8 @@ var rpcMethods = [
4185
4185
  "acp.session.prompt",
4186
4186
  "acp.session.respond_permission",
4187
4187
  "acp.session.cancel",
4188
- "acp.session.set_mode"
4188
+ "acp.session.set_mode",
4189
+ "plugin.route.call"
4189
4190
  ];
4190
4191
  var errorCodes = [
4191
4192
  // Protocol / Version
@@ -6549,6 +6550,7 @@ var RtcDataChannelTransport = class {
6549
6550
  bytesReceived = 0;
6550
6551
  selected;
6551
6552
  selectedMode;
6553
+ selectedRelayUrlValue;
6552
6554
  selectedPathTelemetry;
6553
6555
  localCandidateTelemetry = emptyCandidateTelemetry();
6554
6556
  remoteCandidateTelemetry = emptyCandidateTelemetry();
@@ -6646,6 +6648,9 @@ var RtcDataChannelTransport = class {
6646
6648
  selectedTransport() {
6647
6649
  return this.selected;
6648
6650
  }
6651
+ selectedRelayUrl() {
6652
+ return this.selectedRelayUrlValue;
6653
+ }
6649
6654
  selectedPathMode() {
6650
6655
  return this.selectedMode;
6651
6656
  }
@@ -6856,6 +6861,7 @@ var RtcDataChannelTransport = class {
6856
6861
  }
6857
6862
  this.selected = selected?.transport;
6858
6863
  this.selectedMode = selected?.mode;
6864
+ this.selectedRelayUrlValue = selected?.relayUrl;
6859
6865
  this.selectedPathTelemetry = selectedPathTelemetry(selected);
6860
6866
  this.emitDiagnostic({
6861
6867
  type: "selected-path",
@@ -7022,6 +7028,7 @@ function inspectSelectedPath(stats) {
7022
7028
  let selected;
7023
7029
  if (localType === "relay" || remoteType === "relay")
7024
7030
  selected = { transport: "turn", mode: "TURN" };
7031
+ const relayUrl = localType === "relay" ? stringStat(local, "url") : void 0;
7025
7032
  if (local === void 0 && remote === void 0)
7026
7033
  continue;
7027
7034
  const lan = isLanCandidatePair(localType, remoteType, localAddressScope, remoteAddressScope);
@@ -7029,6 +7036,7 @@ function inspectSelectedPath(stats) {
7029
7036
  const currentRoundTripTime = numberStat(pair, "currentRoundTripTime");
7030
7037
  return {
7031
7038
  ...selected,
7039
+ ...relayUrl === void 0 ? {} : { relayUrl },
7032
7040
  localCandidateType: stringStat(local, "candidateType"),
7033
7041
  remoteCandidateType: stringStat(remote, "candidateType"),
7034
7042
  localAddressScope,
@@ -7181,6 +7189,7 @@ function selectedPathTelemetry(details) {
7181
7189
  return {
7182
7190
  ...details.transport === void 0 ? {} : { transport: details.transport },
7183
7191
  ...details.mode === void 0 ? {} : { mode: details.mode },
7192
+ ...details.relayUrl === void 0 ? {} : { relayUrl: details.relayUrl },
7184
7193
  ...details.localCandidateType === void 0 ? {} : { localCandidateType: details.localCandidateType },
7185
7194
  ...details.remoteCandidateType === void 0 ? {} : { remoteCandidateType: details.remoteCandidateType },
7186
7195
  ...details.localAddressScope === void 0 ? {} : { localAddressScope: details.localAddressScope },
@@ -7460,7 +7469,7 @@ var AdaptiveTransport = class extends BaseTransport {
7460
7469
  this.lastRtcDiagnostics = void 0;
7461
7470
  if (wantWebRtc) {
7462
7471
  try {
7463
- const rtcSelected = await this.tryWebRtc();
7472
+ const rtcSelected = (await this.tryWebRtc()).transport;
7464
7473
  const wireSelected = rtcSelected === "lan" && !preferred.includes("lan") && preferred.includes("p2p") ? "p2p" : rtcSelected;
7465
7474
  if (!preferred.includes(wireSelected)) {
7466
7475
  await this.rtc?.close();
@@ -7494,10 +7503,12 @@ var AdaptiveTransport = class extends BaseTransport {
7494
7503
  selected = "relay";
7495
7504
  }
7496
7505
  this.selected = selected;
7506
+ const turnNode = selected === "turn" ? this.rtc?.selectedRelayUrl() : void 0;
7497
7507
  this.sendControl("transport.selected", {
7498
7508
  connectionId: this.connectionId,
7499
7509
  targetDeviceId: this.options.targetDeviceId,
7500
- transport: selected
7510
+ transport: selected,
7511
+ ...turnNode === void 0 ? {} : { turnNode }
7501
7512
  });
7502
7513
  this.finishConnection();
7503
7514
  }
@@ -7558,7 +7569,7 @@ var AdaptiveTransport = class extends BaseTransport {
7558
7569
  throw error;
7559
7570
  }
7560
7571
  this.lastRtcDiagnostics = rtc.diagnostics();
7561
- return rtc.selectedTransport() ?? "p2p";
7572
+ return { transport: rtc.selectedTransport() ?? "p2p", relayUrl: rtc.selectedRelayUrl() };
7562
7573
  }
7563
7574
  sendRtcSignal(signal) {
7564
7575
  if (this.connectionId === void 0)
@@ -15090,6 +15101,97 @@ async function readHarnessDistributionVersion(entrypoint = process.argv[1]) {
15090
15101
  }
15091
15102
  }
15092
15103
 
15104
+ // src/plugin-route-bridge.ts
15105
+ var PLUGIN_ROUTE_ALLOWLIST = ["api/bot"];
15106
+ var BOT_REMOTE_OPERATIONS = [
15107
+ "state",
15108
+ "models",
15109
+ "efforts",
15110
+ "exchange",
15111
+ "earlier",
15112
+ "usage",
15113
+ "room-progress",
15114
+ "schedules",
15115
+ "create-bot",
15116
+ "duplicate-bot",
15117
+ "update-bot",
15118
+ "delete-bot",
15119
+ "set-main",
15120
+ "set-flags",
15121
+ "create-room",
15122
+ "update-room",
15123
+ "delete-room",
15124
+ "add-agent",
15125
+ "agent-session",
15126
+ "agent-adopt",
15127
+ "agent-archive",
15128
+ "agent-rename",
15129
+ "remove-agent",
15130
+ "send",
15131
+ "answer-question",
15132
+ "dismiss-question",
15133
+ "approval-answer",
15134
+ "stop-member-work"
15135
+ ];
15136
+ function isAllowedPluginRoute(route) {
15137
+ return PLUGIN_ROUTE_ALLOWLIST.some((allowed) => allowed === route);
15138
+ }
15139
+ var callSchema = external_exports.object({
15140
+ route: external_exports.enum(PLUGIN_ROUTE_ALLOWLIST),
15141
+ body: external_exports.object({
15142
+ endpoint: external_exports.enum(BOT_REMOTE_OPERATIONS),
15143
+ payload: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional()
15144
+ }).strict().superRefine((body, ctx) => {
15145
+ if (body.endpoint === "approval-answer" && body.payload?.choice !== "allow" && body.payload?.choice !== "reject") {
15146
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, message: "Only one-time approval or rejection is supported." });
15147
+ }
15148
+ })
15149
+ }).strict();
15150
+ var PLUGIN_ROUTE_TIMEOUT_MS = 3e4;
15151
+ var PluginRouteBridge = class {
15152
+ constructor(dispatch, logger) {
15153
+ this.dispatch = dispatch;
15154
+ this.logger = logger;
15155
+ }
15156
+ async call(input2) {
15157
+ let params;
15158
+ try {
15159
+ params = callSchema.parse(input2);
15160
+ } catch (error) {
15161
+ if (error instanceof external_exports.ZodError) {
15162
+ throw new RpcError("INVALID_MESSAGE", "The plugin route call is invalid for the remote channel.");
15163
+ }
15164
+ throw error;
15165
+ }
15166
+ if (serializedBytes(params) > MAX_SECURE_MESSAGE_BYTES) {
15167
+ throw new RpcError("INVALID_MESSAGE", `The plugin route call exceeds the ${MAX_SECURE_MESSAGE_BYTES}-byte remote limit.`);
15168
+ }
15169
+ const startedAt = performance.now();
15170
+ let result;
15171
+ try {
15172
+ result = await this.dispatch(params.route, params.body, AbortSignal.timeout(PLUGIN_ROUTE_TIMEOUT_MS));
15173
+ } catch (error) {
15174
+ this.logger?.warn("plugin route call failed", {
15175
+ route: params.route,
15176
+ durationMs: Math.round(performance.now() - startedAt)
15177
+ });
15178
+ throw error;
15179
+ }
15180
+ if (serializedBytes(result) > MAX_SECURE_MESSAGE_BYTES) {
15181
+ throw new RpcError("RESPONSE_TOO_LARGE", "The plugin route response exceeds the bounded remote limit.");
15182
+ }
15183
+ this.logger?.debug("plugin route call ok", {
15184
+ route: params.route,
15185
+ status: result.status,
15186
+ durationMs: Math.round(performance.now() - startedAt)
15187
+ });
15188
+ return { status: result.status, json: result.json };
15189
+ }
15190
+ };
15191
+ function serializedBytes(value) {
15192
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
15193
+ }
15194
+
15093
15195
  // src/remote-api-proxy.ts
15094
15196
  var DIRECT_API_CALL_BYTES = 2 * 1024 * 1024;
15095
15197
  var AGENT_PRESET_SETTINGS_NS = "agent-presets";
@@ -20243,7 +20345,7 @@ function normalizeServerUrl(value) {
20243
20345
  }
20244
20346
 
20245
20347
  // src/version.ts
20246
- var PLUGIN_VERSION = "0.5.5";
20348
+ var PLUGIN_VERSION = "0.5.7";
20247
20349
 
20248
20350
  // src/server-api.ts
20249
20351
  var TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/u;
@@ -20324,13 +20426,54 @@ var HostServerApi = class {
20324
20426
  isAdmin: login2.isAdmin
20325
20427
  };
20326
20428
  }
20429
+ async oauthProviders() {
20430
+ let body;
20431
+ try {
20432
+ body = await this.publicRequest("/api/v1/auth/oauth/providers", { method: "GET" });
20433
+ } catch (error) {
20434
+ if (!(error instanceof ServerApiError) || error.status !== 404) throw error;
20435
+ const legacy = [
20436
+ { id: "zhihu", displayName: "Zhihu", startUrl: "/api/v1/auth/oauth/start", statusUrl: "/api/v1/auth/oauth/status" },
20437
+ { id: "github", displayName: "GitHub", startUrl: "/api/v1/auth/oauth/github/start", statusUrl: "/api/v1/auth/oauth/github/status" }
20438
+ ];
20439
+ const items = await Promise.all(legacy.map(async ({ statusUrl, ...provider }) => {
20440
+ try {
20441
+ const status2 = requireRecord(await this.publicRequest(statusUrl, { method: "GET" }), "OAuth status");
20442
+ if (typeof status2.configured !== "boolean") throw invalidOAuthProviders();
20443
+ return status2.configured ? [{ ...provider, qrStartUrl: `/api/v1/auth/oauth/qr/start?provider=${provider.id}` }] : [];
20444
+ } catch (reason) {
20445
+ if (reason instanceof ServerApiError && reason.status === 404) return [];
20446
+ throw reason;
20447
+ }
20448
+ }));
20449
+ return items.flat();
20450
+ }
20451
+ const value = requireRecord(body, "OAuth providers");
20452
+ if (!Array.isArray(value.items)) throw invalidOAuthProviders();
20453
+ const ids = /* @__PURE__ */ new Set();
20454
+ return value.items.map((item) => {
20455
+ const provider = requireRecord(item, "OAuth provider");
20456
+ if (typeof provider.id !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(provider.id) || ids.has(provider.id) || typeof provider.displayName !== "string" || !provider.displayName.trim() || provider.displayName.length > 128 || TERMINAL_CONTROL_CHARACTERS.test(provider.displayName) || !isOAuthEntryPath(provider.startUrl) || provider.qrStartUrl != null && !isOAuthEntryPath(provider.qrStartUrl)) throw invalidOAuthProviders();
20457
+ ids.add(provider.id);
20458
+ return {
20459
+ id: provider.id,
20460
+ displayName: provider.displayName,
20461
+ startUrl: provider.startUrl,
20462
+ ...typeof provider.qrStartUrl === "string" ? { qrStartUrl: provider.qrStartUrl } : {}
20463
+ };
20464
+ });
20465
+ }
20327
20466
  async startOAuthQrLogin(provider = "zhihu") {
20328
- const value = requireRecord(await this.publicRequest(`/api/v1/auth/oauth/qr/start?provider=${provider}`, {
20467
+ const entry = (await this.oauthProviders()).find((item) => item.id === provider);
20468
+ if (entry?.qrStartUrl == null) {
20469
+ throw new ServerApiError("OAUTH_PROVIDER_UNAVAILABLE", "This OAuth provider is unavailable for QR login.", false);
20470
+ }
20471
+ const value = requireRecord(await this.publicRequest(entry.qrStartUrl, {
20329
20472
  method: "POST",
20330
20473
  body: "{}"
20331
20474
  }), "QR login");
20332
20475
  const scanUrl = normalizeOAuthScanUrl(value.scanUrl, this.baseUrl);
20333
- if (typeof value.qrId !== "string" || value.qrId.length < 20 || scanUrl === void 0 || !Number.isSafeInteger(value.expiresIn) || provider === "github" && value.provider !== "github" || value.provider !== void 0 && value.provider !== provider) {
20476
+ if (typeof value.qrId !== "string" || value.qrId.length < 20 || scanUrl === void 0 || !Number.isSafeInteger(value.expiresIn) || provider !== "zhihu" && value.provider !== provider || value.provider !== void 0 && value.provider !== provider) {
20334
20477
  throw new ServerApiError("INVALID_MESSAGE", "The Server returned an invalid QR login session.", false);
20335
20478
  }
20336
20479
  return { qrId: value.qrId, scanUrl, expiresIn: value.expiresIn };
@@ -20566,6 +20709,12 @@ var HostServerApi = class {
20566
20709
  return this.identity;
20567
20710
  }
20568
20711
  };
20712
+ function isOAuthEntryPath(value) {
20713
+ return typeof value === "string" && /^\/api\/v1\/auth\/[^\s\\#]*$/.test(value) && !TERMINAL_CONTROL_CHARACTERS.test(value) && !value.split("?")[0]?.split("/").some((segment) => segment === "." || segment === ".." || /%/u.test(segment));
20714
+ }
20715
+ function invalidOAuthProviders() {
20716
+ return new ServerApiError("INVALID_MESSAGE", "The Server returned an invalid OAuth provider list.", false);
20717
+ }
20569
20718
  function normalizeOAuthScanUrl(value, baseUrl) {
20570
20719
  if (typeof value !== "string" || TERMINAL_CONTROL_CHARACTERS.test(value)) return void 0;
20571
20720
  try {
@@ -20750,7 +20899,10 @@ var TypertGatewaySwitch = class {
20750
20899
  this.runtime.stream = (request) => this.remoteTarget === void 0 || isLocalOnlyEndpoint(endpointOf(request)) ? this.localStream(request) : this.remoteTarget.open(endpointOf(request), { args: request.args }, request.signal ?? new AbortController().signal);
20751
20900
  }
20752
20901
  if (this.originalDispatch !== void 0) {
20753
- this.runtime.dispatchRpc = (endpoint, payload, signal) => this.remoteTarget === void 0 || isLocalOnlyEndpoint(endpoint) ? this.localDispatch(endpoint, payload, signal) : this.remoteTarget.dispatch(endpoint, payload, signal);
20902
+ this.runtime.dispatchRpc = (endpoint, payload, signal) => this.remoteTarget === void 0 || isLocalOnlyEndpoint(endpoint) ? this.localDispatch(endpoint, payload, signal) : this.remoteTarget.dispatch(endpoint, payload, signal).then(
20903
+ (result) => workspaceConnectionResult(endpoint, result),
20904
+ (error) => ({ ok: false, error: this.failure(error) })
20905
+ );
20754
20906
  }
20755
20907
  if (this.originalOpen !== void 0) {
20756
20908
  const open = this.originalOpen;
@@ -20826,6 +20978,18 @@ var TypertGatewaySwitch = class {
20826
20978
  return { code, message: source.message, details };
20827
20979
  }
20828
20980
  };
20981
+ function workspaceConnectionResult(endpoint, result) {
20982
+ if (endpoint !== "workspaceFiles/readBytes" && endpoint !== "officeToPdf/render") return result;
20983
+ if (!result.ok || !isRecord9(result.value) || !(result.value.data instanceof Uint8Array)) return result;
20984
+ return {
20985
+ ...result,
20986
+ value: { ...result.value, data: null },
20987
+ attachments: [
20988
+ ...(result.attachments ?? []).filter((attachment) => attachment.path.length !== 1 || attachment.path[0] !== "data"),
20989
+ { path: ["data"], bytes: result.value.data }
20990
+ ]
20991
+ };
20992
+ }
20829
20993
  function usesRc1Arity(open) {
20830
20994
  return open.length >= 5;
20831
20995
  }
@@ -22414,6 +22578,16 @@ var ClientModeRuntime = class {
22414
22578
  }
22415
22579
  return remote.client.rpc("fileviewer.call", { endpoint, payload }, signal);
22416
22580
  }
22581
+ async pluginRoute(route, body, signal) {
22582
+ const remote = this.connected;
22583
+ if (remote === void 0 || this.status().mode !== "remote") {
22584
+ throw new ClientModeError("REMOTE_NOT_CONNECTED", "No Remote Host is selected.", true);
22585
+ }
22586
+ if (!remote.features.pluginRoute) {
22587
+ throw new ClientModeError("FEATURE_NOT_SUPPORTED", "The connected Host cannot forward plugin routes.");
22588
+ }
22589
+ return remote.client.rpc("plugin.route.call", { route, body }, signal);
22590
+ }
22417
22591
  activeRemote() {
22418
22592
  return this.connected;
22419
22593
  }
@@ -22793,6 +22967,7 @@ var ClientModeRuntime = class {
22793
22967
  try {
22794
22968
  if (endpoint === "status") return ok2(await this.detailedStatus());
22795
22969
  if (endpoint === "devices") return ok2(await this.devices());
22970
+ if (endpoint === "client.account.providers") return ok2(await this.server.oauthProviders());
22796
22971
  if (endpoint === "client.account.login") {
22797
22972
  const value = record5(payload);
22798
22973
  if (typeof value.email !== "string" || typeof value.password !== "string") {
@@ -22803,8 +22978,8 @@ var ClientModeRuntime = class {
22803
22978
  if (endpoint === "client.account.qr.start") {
22804
22979
  const value = record5(payload);
22805
22980
  const provider = value.provider ?? "zhihu";
22806
- if (provider !== "zhihu" && provider !== "github") {
22807
- throw new ClientModeError("INVALID_MESSAGE", "A supported OAuth provider is required.");
22981
+ if (typeof provider !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(provider)) {
22982
+ throw new ClientModeError("INVALID_MESSAGE", "An OAuth provider ID is required.");
22808
22983
  }
22809
22984
  return ok2(await this.startClientOAuthQrLogin(provider));
22810
22985
  }
@@ -22946,6 +23121,14 @@ var ClientModeRuntime = class {
22946
23121
  const host = this.requireLocalCodex();
22947
23122
  return ok2(await host.codexRespond(value, signal));
22948
23123
  }
23124
+ if (endpoint === "plugin.route") {
23125
+ const value = record5(payload);
23126
+ if (typeof value.route !== "string" || !isAllowedPluginRoute(value.route)) {
23127
+ throw new ClientModeError("INVALID_MESSAGE", "A valid plugin route is required.");
23128
+ }
23129
+ if (!isRecord10(value.body)) throw new ClientModeError("INVALID_MESSAGE", "A plugin route body object is required.");
23130
+ return ok2(await this.pluginRoute(value.route, value.body, signal));
23131
+ }
22949
23132
  if (endpoint === "codex.stream.open") return ok2(await this.openCodexStream(payload, signal));
22950
23133
  if (endpoint === "codex.stream.next") return ok2(await this.nextCodexFrames(payload, signal));
22951
23134
  if (endpoint === "codex.stream.close") return ok2(await this.closeCodexStream(payload));
@@ -23128,7 +23311,8 @@ function remoteHostFeatures(clientVersion) {
23128
23311
  remoteGateway: false,
23129
23312
  codex: false,
23130
23313
  cursor: false,
23131
- antigravity: false
23314
+ antigravity: false,
23315
+ pluginRoute: false
23132
23316
  };
23133
23317
  }
23134
23318
  async function probeRemoteHostFeatures(client, clientVersion) {
@@ -23172,7 +23356,8 @@ async function probeRemoteHostFeatures(client, clientVersion) {
23172
23356
  cursor: cursor2,
23173
23357
  antigravity,
23174
23358
  ...capabilities.has("agent.acp.antigravity.model-load.v1") ? { acpModelLoadsSession: true } : {},
23175
- ...workspaceTypes === void 0 ? {} : { workspaceTypes }
23359
+ ...workspaceTypes === void 0 ? {} : { workspaceTypes },
23360
+ pluginRoute: capabilities.has("plugin.route.v1")
23176
23361
  };
23177
23362
  }
23178
23363
  async function waitForCodexFrames(stream, signal) {
@@ -23794,6 +23979,7 @@ function safeUpstreamError(value) {
23794
23979
  if (!isRecord11(value) || typeof value.message !== "string") return "Codex App Server rejected the request.";
23795
23980
  const message = value.message.toLowerCase();
23796
23981
  if (message.includes("active writer")) return "Codex thread already has an active writer.";
23982
+ if (message.includes("no rollout found")) return "Codex thread state is not persisted yet.";
23797
23983
  return message.includes("not initialized") ? "Codex App Server is not initialized." : "Codex App Server rejected the request.";
23798
23984
  }
23799
23985
  function isRecord11(value) {
@@ -24634,13 +24820,23 @@ var CodexRemoteDomain = class {
24634
24820
  try {
24635
24821
  await this.callUpstream("thread/settings/update", { threadId, ...settings });
24636
24822
  result = { thread: allowed.thread, ...settings, sandbox: settings.sandboxPolicy };
24637
- } catch {
24638
- result = await this.callUpstream("thread/resume", {
24639
- ...permission.params,
24640
- ...allowed.cwd === void 0 ? {} : { cwd: allowed.cwd },
24641
- ...codexThreadPermissionParams(permission),
24642
- excludeTurns: true
24643
- });
24823
+ } catch (error) {
24824
+ if (isUnpersistedThreadError(error)) {
24825
+ return { thread: allowed.thread, ...settings, sandbox: settings.sandboxPolicy };
24826
+ }
24827
+ try {
24828
+ result = await this.callUpstream("thread/resume", {
24829
+ ...permission.params,
24830
+ ...allowed.cwd === void 0 ? {} : { cwd: allowed.cwd },
24831
+ ...codexThreadPermissionParams(permission),
24832
+ excludeTurns: true
24833
+ });
24834
+ } catch (resumeError) {
24835
+ if (isUnpersistedThreadError(resumeError)) {
24836
+ return { thread: allowed.thread, ...settings, sandbox: settings.sandboxPolicy };
24837
+ }
24838
+ throw resumeError;
24839
+ }
24644
24840
  await this.assertResultThreadAllowed(result);
24645
24841
  if (codexPermissionPresetFromResponse(result) !== params.permissionPreset) {
24646
24842
  await this.callUpstream("thread/settings/update", { threadId, ...settings });
@@ -25324,6 +25520,9 @@ function maskId(value) {
25324
25520
  function isActiveWriterMessage(message) {
25325
25521
  return message.toLowerCase().includes("active writer");
25326
25522
  }
25523
+ function isUnpersistedThreadError(error) {
25524
+ return error instanceof RpcError && error.code === "CODEX_UPSTREAM_ERROR" && error.message.toLowerCase().includes("not persisted yet");
25525
+ }
25327
25526
 
25328
25527
  // src/workspace-bridge.ts
25329
25528
  import { spawn as spawn3 } from "node:child_process";
@@ -29345,7 +29544,8 @@ var apiMethods = /* @__PURE__ */ new Set([
29345
29544
  "agent.acp.transfer.chunk",
29346
29545
  "agent.acp.transfer.commit",
29347
29546
  "agent.acp.transfer.read",
29348
- "agent.acp.transfer.close"
29547
+ "agent.acp.transfer.close",
29548
+ "plugin.route.call"
29349
29549
  ]);
29350
29550
  var HOST_CAPABILITIES = [
29351
29551
  "harness.api.v1",
@@ -29359,7 +29559,7 @@ var HOST_CAPABILITIES = [
29359
29559
  "agent.acp.transfer.v1"
29360
29560
  ];
29361
29561
  var RpcRouter = class {
29362
- constructor(harnessApi, maxPending = 128, logger, fileViewer, harnessRemote, capabilities = () => HOST_CAPABILITIES, codex, acp, loopback, workspaceTypes) {
29562
+ constructor(harnessApi, maxPending = 128, logger, fileViewer, harnessRemote, capabilities = () => HOST_CAPABILITIES, codex, acp, loopback, workspaceTypes, pluginRoute) {
29363
29563
  this.harnessApi = harnessApi;
29364
29564
  this.maxPending = maxPending;
29365
29565
  this.logger = logger;
@@ -29370,6 +29570,7 @@ var RpcRouter = class {
29370
29570
  this.acp = acp;
29371
29571
  this.loopback = loopback;
29372
29572
  this.workspaceTypes = workspaceTypes;
29573
+ this.pluginRoute = pluginRoute;
29373
29574
  }
29374
29575
  active = 0;
29375
29576
  async closePeerStreams() {
@@ -29518,6 +29719,8 @@ var RpcRouter = class {
29518
29719
  return this.requireAcp().readTransfer(params);
29519
29720
  case "agent.acp.transfer.close":
29520
29721
  return this.requireAcp().closeTransfer(params);
29722
+ case "plugin.route.call":
29723
+ return this.requirePluginRoute().call(params);
29521
29724
  default:
29522
29725
  throw new RpcError("METHOD_NOT_FOUND", "The requested method does not exist.");
29523
29726
  }
@@ -29546,6 +29749,12 @@ var RpcRouter = class {
29546
29749
  }
29547
29750
  return this.acp;
29548
29751
  }
29752
+ requirePluginRoute() {
29753
+ if (this.pluginRoute === void 0) {
29754
+ throw new RpcError("FEATURE_NOT_SUPPORTED", "This Host cannot forward local plugin routes.");
29755
+ }
29756
+ return this.pluginRoute;
29757
+ }
29549
29758
  };
29550
29759
  function errorResponse2(requestId, error) {
29551
29760
  const code = safeErrorCode(error);
@@ -29564,7 +29773,7 @@ var MAX_REMOTE_DIRECTORY_ENTRIES = 1e3;
29564
29773
 
29565
29774
  // src/file-viewer-bridge.ts
29566
29775
  var locatorSchema = external_exports.string().min(1).max(MAX_REMOTE_FILE_LOCATOR_CHARS);
29567
- var callSchema = external_exports.discriminatedUnion("endpoint", [
29776
+ var callSchema2 = external_exports.discriminatedUnion("endpoint", [
29568
29777
  external_exports.object({
29569
29778
  endpoint: external_exports.literal("stat"),
29570
29779
  payload: external_exports.object({ path: locatorSchema }).strict()
@@ -29615,7 +29824,7 @@ var RemoteFileViewerBridge = class {
29615
29824
  this.logger = logger;
29616
29825
  }
29617
29826
  async call(input2) {
29618
- const params = callSchema.parse(input2);
29827
+ const params = callSchema2.parse(input2);
29619
29828
  const service = this.service();
29620
29829
  if (service === void 0) {
29621
29830
  throw new RpcError("FILE_VIEWER_UNAVAILABLE", "The Remote Host does not have DSH File Viewer available.");
@@ -30685,7 +30894,7 @@ function historyRequestPayload(payload, maxMessages) {
30685
30894
  }
30686
30895
 
30687
30896
  // src/harness-api-bridge.ts
30688
- var callSchema2 = external_exports.object({
30897
+ var callSchema3 = external_exports.object({
30689
30898
  method: external_exports.string().min(1).max(80),
30690
30899
  rpcId: external_exports.string().min(1).max(128),
30691
30900
  payload: external_exports.unknown()
@@ -30876,7 +31085,7 @@ var HarnessApiBridge = class {
30876
31085
  host;
30877
31086
  answer;
30878
31087
  async call(input2) {
30879
- const params = callSchema2.parse(input2);
31088
+ const params = callSchema3.parse(input2);
30880
31089
  const signal = AbortSignal.timeout(NATIVE_CALL_TIMEOUT_MS);
30881
31090
  const method = this.methods.get(params.method);
30882
31091
  if (method === void 0) {
@@ -31010,7 +31219,7 @@ var HarnessApiBridge = class {
31010
31219
  } catch {
31011
31220
  throw new RpcError("INVALID_MESSAGE", "The Harness API transfer does not contain a valid request.");
31012
31221
  }
31013
- const nativeRequest = callSchema2.parse(request);
31222
+ const nativeRequest = callSchema3.parse(request);
31014
31223
  const response = await this.call(nativeRequest);
31015
31224
  const responseBytes = new TextEncoder().encode(JSON.stringify(response));
31016
31225
  if (responseBytes.byteLength <= INLINE_TRANSFER_RESPONSE_BYTES3) {
@@ -31456,7 +31665,7 @@ function concatChunks3(chunks, totalBytes) {
31456
31665
 
31457
31666
  // src/harness-remote-bridge.ts
31458
31667
  var endpointSchema = external_exports.string().min(1).max(128).regex(/^(?:\$events(?:\/result)?|[A-Za-z0-9_$.-]+\/[A-Za-z0-9_$.-]+)$/);
31459
- var callSchema3 = external_exports.object({ endpoint: endpointSchema, payload: external_exports.unknown() }).strict();
31668
+ var callSchema4 = external_exports.object({ endpoint: endpointSchema, payload: external_exports.unknown() }).strict();
31460
31669
  var streamOpenSchema4 = external_exports.object({
31461
31670
  streamId: external_exports.string().min(1).max(128),
31462
31671
  endpoint: endpointSchema,
@@ -31531,6 +31740,8 @@ var HARNESS_REMOTE_ALLOWLIST = [
31531
31740
  "messageFeedback/put",
31532
31741
  "pluginInventory/list",
31533
31742
  "permissionPresets/catalog",
31743
+ "schedule/list",
31744
+ "schedule/catalog",
31534
31745
  "session/attachment",
31535
31746
  "session/cancel",
31536
31747
  "session/canOpenWorkspacePath",
@@ -31597,7 +31808,7 @@ var HarnessRemoteBridge = class {
31597
31808
  incomingTransfers = /* @__PURE__ */ new Map();
31598
31809
  outgoingTransfers = /* @__PURE__ */ new Map();
31599
31810
  async call(input2) {
31600
- const params = callSchema3.parse(input2);
31811
+ const params = callSchema4.parse(input2);
31601
31812
  this.assertAllowed(params.endpoint);
31602
31813
  if (TERMINAL_STREAMS.has(params.endpoint)) throw new RpcError("METHOD_NOT_ALLOWED", "Use a stream for this terminal endpoint.");
31603
31814
  if (["settings/update", "settings/replace", "settings/mutate"].includes(params.endpoint)) {
@@ -31717,7 +31928,7 @@ var HarnessRemoteBridge = class {
31717
31928
  } catch {
31718
31929
  throw new RpcError("INVALID_MESSAGE", "The Harness Remote transfer does not contain a valid request.");
31719
31930
  }
31720
- const response = await this.call(callSchema3.parse(request));
31931
+ const response = await this.call(callSchema4.parse(request));
31721
31932
  const responseBytes = new TextEncoder().encode(JSON.stringify(response));
31722
31933
  if (responseBytes.byteLength <= INLINE_TRANSFER_RESPONSE_BYTES4) return { kind: "inline", response };
31723
31934
  if (responseBytes.byteLength > MAX_HARNESS_API_TRANSFER_BYTES) {
@@ -31979,7 +32190,7 @@ var CodexWorkspaceBridge = class extends WorkspaceBridge {
31979
32190
 
31980
32191
  // src/service.ts
31981
32192
  var HostPluginRuntime = class {
31982
- constructor(config, identities, apiProxy, logger, localGateway, fileViewerHost, terminalSpawner) {
32193
+ constructor(config, identities, apiProxy, logger, localGateway, fileViewerHost, terminalSpawner, pluginRoutes) {
31983
32194
  this.config = config;
31984
32195
  this.identities = identities;
31985
32196
  this.apiProxy = apiProxy;
@@ -31987,6 +32198,7 @@ var HostPluginRuntime = class {
31987
32198
  this.localGateway = localGateway;
31988
32199
  this.fileViewerHost = fileViewerHost;
31989
32200
  this.terminalSpawner = terminalSpawner;
32201
+ this.pluginRoutes = pluginRoutes;
31990
32202
  this.terminalEnabled = config.terminal.enabled;
31991
32203
  this.loopbackPorts = [...config.loopback.ports];
31992
32204
  this.codex = new CodexRemoteDomain(config.codex, logger);
@@ -32025,6 +32237,8 @@ var HostPluginRuntime = class {
32025
32237
  context,
32026
32238
  (event, data2) => send(createEvent(event, data2))
32027
32239
  );
32240
+ const dispatcher = this.pluginRoutes?.();
32241
+ const pluginRoute = dispatcher === void 0 ? void 0 : new PluginRouteBridge(dispatcher, this.logger);
32028
32242
  return new RpcRouter(
32029
32243
  harnessApi,
32030
32244
  void 0,
@@ -32036,7 +32250,8 @@ var HostPluginRuntime = class {
32036
32250
  cursor2,
32037
32251
  // Handles and their lifetime belong to this connection; only policy is shared.
32038
32252
  this.createLoopbackHost(),
32039
- () => this.hostWorkspaceTypes()
32253
+ () => this.hostWorkspaceTypes(),
32254
+ pluginRoute
32040
32255
  );
32041
32256
  }, this.logger);
32042
32257
  if (config.serverUrl !== void 0) {
@@ -32150,6 +32365,12 @@ var HostPluginRuntime = class {
32150
32365
  }
32151
32366
  this.serverConnection.reconnect();
32152
32367
  }
32368
+ async hostOAuthProviders() {
32369
+ if (this.serverApi === void 0) {
32370
+ throw new ServerApiError("SERVER_NOT_CONFIGURED", "Remote Host Server is unavailable.", false);
32371
+ }
32372
+ return this.serverApi.oauthProviders();
32373
+ }
32153
32374
  async startHostOAuthQrLogin(provider) {
32154
32375
  if (this.serverApi === void 0) {
32155
32376
  throw new ServerApiError("SERVER_NOT_CONFIGURED", "Remote Host Server is unavailable.", false);
@@ -32356,6 +32577,7 @@ var HostPluginRuntime = class {
32356
32577
  capabilities.push("harness.api.v1", "harness.api.transfer.v1");
32357
32578
  }
32358
32579
  if (this.fileViewerHost?.() !== void 0) capabilities.push("fileviewer.read.v1");
32580
+ if (this.pluginRoutes?.() !== void 0) capabilities.push("plugin.route.v1");
32359
32581
  if (this.codex.isAvailable()) capabilities.push("codex.appserver.v1", "codex.appserver.transfer.v1");
32360
32582
  if (this.acp.enabledBackends().length > 0) {
32361
32583
  capabilities.push("agent.acp.v1", "agent.acp.transfer.v1", "agent.acp.workspace-files.v1");
@@ -32436,6 +32658,61 @@ function isPlainRecord(value) {
32436
32658
  return typeof value === "object" && value !== null && !Array.isArray(value);
32437
32659
  }
32438
32660
 
32661
+ // src/plugin-route-dispatcher.ts
32662
+ function createPluginRouteDispatcher(readConnection, readPort) {
32663
+ let cookie;
32664
+ return async (route, body, signal) => {
32665
+ signal.throwIfAborted();
32666
+ const connection = readConnection();
32667
+ const handler = connection?.createSharedFetchHandler?.("/api");
32668
+ let response;
32669
+ if (handler !== void 0) {
32670
+ response = await withSignal(handler.fetch(new Request(`http://127.0.0.1/${route}`, {
32671
+ method: "POST",
32672
+ headers: { "content-type": "application/json" },
32673
+ body: JSON.stringify(body ?? {}),
32674
+ signal
32675
+ })), signal);
32676
+ } else {
32677
+ const port = readPort();
32678
+ if (connection?.authenticatedUrl === void 0 || typeof port !== "number") {
32679
+ return { status: 503, json: { ok: false, error: { code: "gateway/unavailable", message: "The local plugin route carrier is unavailable." } } };
32680
+ }
32681
+ const origin = `http://127.0.0.1:${port}`;
32682
+ if (cookie === void 0) {
32683
+ const launch = await fetch(connection.authenticatedUrl(`${origin}/`), { redirect: "manual", signal });
32684
+ const setCookie = typeof launch.headers.getSetCookie === "function" ? launch.headers.getSetCookie() : [];
32685
+ cookie = setCookie.map((item) => item.split(";", 1)[0]).join("; ") || void 0;
32686
+ }
32687
+ response = await fetch(`${origin}/${route}`, {
32688
+ method: "POST",
32689
+ headers: { "content-type": "application/json", ...cookie === void 0 ? {} : { cookie } },
32690
+ body: JSON.stringify(body ?? {}),
32691
+ signal
32692
+ });
32693
+ if (response.status === 401) cookie = void 0;
32694
+ }
32695
+ const text = await response.text();
32696
+ let json = null;
32697
+ if (text !== "") {
32698
+ try {
32699
+ json = JSON.parse(text);
32700
+ } catch {
32701
+ json = text;
32702
+ }
32703
+ }
32704
+ return { status: response.status, json };
32705
+ };
32706
+ }
32707
+ function withSignal(pending, signal) {
32708
+ return new Promise((resolve6, reject) => {
32709
+ const onAbort = () => reject(signal.reason);
32710
+ signal.addEventListener("abort", onAbort, { once: true });
32711
+ if (signal.aborted) onAbort();
32712
+ void pending.then(resolve6, reject).finally(() => signal.removeEventListener("abort", onAbort));
32713
+ });
32714
+ }
32715
+
32439
32716
  // src/cli.ts
32440
32717
  import { stat as stat7 } from "node:fs/promises";
32441
32718
  import { hostname as hostname3 } from "node:os";
@@ -32473,16 +32750,20 @@ async function register(args, runtime) {
32473
32750
  return 0;
32474
32751
  }
32475
32752
  async function login(args, runtime) {
32476
- if (args.length > 1) throw new CliUsageError("Usage: ds-harness-remote login [github|zhihu]");
32477
- const provider = args[0] ?? "zhihu";
32478
- if (provider !== "github" && provider !== "zhihu") {
32479
- throw new CliUsageError("Login provider must be github or zhihu.");
32480
- }
32753
+ if (args.length > 1) throw new CliUsageError("Usage: ds-harness-remote login [provider|list]");
32481
32754
  const context = await hostContext(runtime);
32755
+ const providers = (await context.api.oauthProviders()).filter((item) => item.qrStartUrl != null);
32756
+ if (args[0] === "list") {
32757
+ write(runtime.stdout, providers.length === 0 ? "No QR sign-in providers are available.\n" : providers.map((item) => `${item.displayName}: ds-harness-remote login ${item.id}
32758
+ `).join(""));
32759
+ return 0;
32760
+ }
32761
+ const selected = args[0] === void 0 ? providers.find((item) => item.id === "zhihu") ?? providers[0] : providers.find((item) => item.id === args[0]);
32762
+ if (selected === void 0) throw new CliUsageError('OAuth provider unavailable. Run "ds-harness-remote login list" to see available providers.');
32763
+ const provider = selected.id;
32482
32764
  const session = await context.api.startOAuthQrLogin(provider);
32483
32765
  const qr = await runtime.renderQr(session.scanUrl);
32484
- const providerName = provider === "github" ? "GitHub" : "Zhihu";
32485
- write(runtime.stdout, `Using ${providerName} QR login. ${alternativeProviderHint(provider)}
32766
+ write(runtime.stdout, `Using ${selected.displayName} QR login. Run "ds-harness-remote login list" for available providers.
32486
32767
  `);
32487
32768
  write(runtime.stdout, "Scan this QR code to authorize this Host:\n\n");
32488
32769
  write(runtime.stdout, `${qr.trimEnd()}
@@ -32660,9 +32941,6 @@ async function renderCompactTerminalQr(url) {
32660
32941
  function authorizedMessage(authorization) {
32661
32942
  return authorization.account === void 0 ? "Remote Host login complete." : `Remote Host login complete for ${authorization.account}.`;
32662
32943
  }
32663
- function alternativeProviderHint(provider) {
32664
- return provider === "zhihu" ? "GitHub is also supported: ds-harness-remote login github" : "Zhihu is also supported: ds-harness-remote login zhihu";
32665
- }
32666
32944
  function authorizationLabel(method) {
32667
32945
  if (method === "host_registration_code") return "Host registration code";
32668
32946
  if (method === "owned_device") return "owned device";
@@ -32679,13 +32957,13 @@ function cliErrorMessage(error) {
32679
32957
  function helpText() {
32680
32958
  return [
32681
32959
  "Usage:",
32682
- " ds-harness-remote login [github|zhihu]",
32960
+ " ds-harness-remote login [provider|list]",
32683
32961
  " ds-harness-remote register <server-token>",
32684
32962
  " ds-harness-remote status",
32685
32963
  " ds-harness-remote logout",
32686
32964
  "",
32687
32965
  "Inside dsh-TUI, use /remote login, /remote status, or /remote logout.",
32688
- "login defaults to Zhihu and authorizes this computer as a Remote Host with a terminal QR code.",
32966
+ "login uses an available QR provider (preferring Zhihu). Use login list to see available providers.",
32689
32967
  `The Server is ${DEFAULT_REMOTE_SERVER_URL}.`,
32690
32968
  "Host configuration is not exposed by this CLI yet. Restart dsh-tui after login or logout.",
32691
32969
  ""
@@ -32728,7 +33006,7 @@ function installTuiRemoteCommand(ctx, resolveTarget) {
32728
33006
  const definition = {
32729
33007
  name: "remote",
32730
33008
  description: "Manage Remote Host login and connection status",
32731
- input: { hint: "[status | login [github|zhihu] | logout]" },
33009
+ input: { hint: "[status | login [provider|list] | logout]" },
32732
33010
  recordInput: false,
32733
33011
  handler: async ({ rawInput }) => {
32734
33012
  const args = rawInput.trim().split(/\s+/u).filter(Boolean);
@@ -32738,9 +33016,19 @@ function installTuiRemoteCommand(ctx, resolveTarget) {
32738
33016
  return { kind: "success", text: formatRemoteStatusInline(resolveTarget()) };
32739
33017
  }
32740
33018
  if (command === "login" && args.length <= 2) {
32741
- const provider = args[1] ?? "zhihu";
32742
- if (provider !== "github" && provider !== "zhihu") {
32743
- return { kind: "error", text: "Usage: /remote login [github|zhihu]" };
33019
+ const provider = args[1] ?? "";
33020
+ if (provider === "list") {
33021
+ const target2 = resolveTarget();
33022
+ if (target2 === void 0) return remoteNotReady();
33023
+ try {
33024
+ const providers = await target2.runtime.hostOAuthProviders();
33025
+ return { kind: "success", text: providers.filter((item) => item.qrStartUrl != null).map((item) => `${item.displayName}: /remote login ${item.id}`).join("\n") || "No QR sign-in providers are available." };
33026
+ } catch (error) {
33027
+ return { kind: "error", text: errorCode5(error) };
33028
+ }
33029
+ }
33030
+ if (provider !== "" && !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(provider)) {
33031
+ return { kind: "error", text: "Usage: /remote login [provider|list]" };
32744
33032
  }
32745
33033
  if (scenes === void 0) {
32746
33034
  return {
@@ -32784,7 +33072,7 @@ function installTuiRemoteCommand(ctx, resolveTarget) {
32784
33072
  en: "Manage Remote Host login and connection status",
32785
33073
  zh: "\u7BA1\u7406 Remote Host \u767B\u5F55\u548C\u8FDE\u63A5\u72B6\u6001"
32786
33074
  },
32787
- children: remoteCommandChildren
33075
+ children: (path) => remoteCommandChildren(path, login2.completionProviders())
32788
33076
  }));
32789
33077
  const disposeScene = registerOptional(tuiContext, "login scene", () => scenes?.register({
32790
33078
  id: REMOTE_LOGIN_SCENE_ID,
@@ -32828,7 +33116,7 @@ function registerOptional(ctx, feature, register2) {
32828
33116
  };
32829
33117
  }
32830
33118
  }
32831
- function remoteCommandChildren(canonicalPath) {
33119
+ function remoteCommandChildren(canonicalPath, providers) {
32832
33120
  if (canonicalPath.length === 1 && canonicalPath[0] === "remote") {
32833
33121
  return [
32834
33122
  {
@@ -32838,8 +33126,8 @@ function remoteCommandChildren(canonicalPath) {
32838
33126
  },
32839
33127
  {
32840
33128
  name: "login",
32841
- description: "Authorize this Host with a GitHub or Zhihu QR code",
32842
- descriptions: { zh: "\u4F7F\u7528 GitHub \u6216\u77E5\u4E4E\u4E8C\u7EF4\u7801\u6388\u6743\u5F53\u524D Host" }
33129
+ description: "Authorize this Host with an available OAuth QR provider",
33130
+ descriptions: { zh: "\u4F7F\u7528\u670D\u52A1\u7AEF\u63D0\u4F9B\u7684 OAuth \u4E8C\u7EF4\u7801\u6388\u6743\u5F53\u524D Host" }
32843
33131
  },
32844
33132
  {
32845
33133
  name: "logout",
@@ -32850,8 +33138,12 @@ function remoteCommandChildren(canonicalPath) {
32850
33138
  }
32851
33139
  if (canonicalPath.length === 2 && canonicalPath[0] === "remote" && canonicalPath[1] === "login") {
32852
33140
  return [
32853
- { name: "zhihu", description: "Sign in with Zhihu (default)", descriptions: { zh: "\u4F7F\u7528\u77E5\u4E4E\u767B\u5F55\uFF08\u9ED8\u8BA4\uFF09" } },
32854
- { name: "github", description: "Sign in with GitHub", descriptions: { zh: "\u4F7F\u7528 GitHub \u767B\u5F55" } }
33141
+ { name: "list", description: "List available sign-in providers", descriptions: { zh: "\u5217\u51FA\u53EF\u7528\u767B\u5F55\u65B9\u5F0F" } },
33142
+ ...providers.filter((item) => item.qrStartUrl != null).map((item) => ({
33143
+ name: item.id,
33144
+ description: `Sign in with ${item.displayName}`,
33145
+ descriptions: { zh: `\u4F7F\u7528 ${item.displayName} \u767B\u5F55` }
33146
+ }))
32855
33147
  ];
32856
33148
  }
32857
33149
  return [];
@@ -32874,13 +33166,13 @@ function remoteStatusLines(target2) {
32874
33166
  `Authorization: ${status2.authorized ? status2.account === void 0 ? "logged in" : `logged in (${status2.account})` : "logged out"}`,
32875
33167
  `Server connection: ${connection}`,
32876
33168
  ...status2.error === void 0 ? [] : [`Connection error: ${status2.error}`],
32877
- ...status2.error === "CONNECTION_REPLACED" ? ["Another instance is using this Host identity. Stop it or use a separate DSH_HOME before reconnecting."] : status2.error === "SERVER_CREDENTIALS_BUSY" ? ["Credential refresh is locked. Stop all instances before removing an orphaned server-credentials.json.refresh-lock and authorizing again."] : status2.accountRequired ? ["Authorize again with /remote login [github|zhihu]."] : [],
33169
+ ...status2.error === "CONNECTION_REPLACED" ? ["Another instance is using this Host identity. Stop it or use a separate DSH_HOME before reconnecting."] : status2.error === "SERVER_CREDENTIALS_BUSY" ? ["Credential refresh is locked. Stop all instances before removing an orphaned server-credentials.json.refresh-lock and authorizing again."] : status2.accountRequired ? ["Authorize again with /remote login [provider|list]."] : [],
32878
33170
  `Harness Remote API: ${capabilities.has("harness.api.v1") ? "available (ApiProxy)" : capabilities.has("harness.remote.v3") ? "available (Typert Remote Session V3)" : capabilities.has("harness.remote.v1") ? "available (Typert Remote)" : "unavailable"}`,
32879
33171
  `Remote clients: ${diagnostics.activeConnections}`,
32880
33172
  `Codex Remote: ${codex.enabled ? codex.state : "disabled"}`,
32881
33173
  `Agent ACP: ${cursor2.enabled ? cursor2.state : "disabled"}`,
32882
33174
  "",
32883
- "Commands: /remote login [github|zhihu] \xB7 /remote status \xB7 /remote logout"
33175
+ "Commands: /remote login [provider|list] \xB7 /remote status \xB7 /remote logout"
32884
33176
  ];
32885
33177
  }
32886
33178
  function formatRemoteStatusInline(target2) {
@@ -32891,7 +33183,7 @@ function remoteCommandUsage() {
32891
33183
  "Usage:",
32892
33184
  " /remote",
32893
33185
  " /remote status",
32894
- " /remote login [github|zhihu]",
33186
+ " /remote login [provider|list]",
32895
33187
  " /remote logout"
32896
33188
  ].join("\n");
32897
33189
  }
@@ -32908,7 +33200,29 @@ var RemoteLoginController = class {
32908
33200
  current = { phase: "idle", provider: "zhihu" };
32909
33201
  listeners = /* @__PURE__ */ new Set();
32910
33202
  attempt = 0;
33203
+ providers = [];
33204
+ providersLoading = false;
33205
+ providersCheckedAt = 0;
33206
+ providersServerUrl;
32911
33207
  snapshot = () => this.current;
33208
+ completionProviders() {
33209
+ const target2 = this.resolveTarget();
33210
+ if (!this.providersLoading && target2 !== void 0 && (target2.config.serverUrl !== this.providersServerUrl || Date.now() - this.providersCheckedAt > 3e4)) {
33211
+ this.providersLoading = true;
33212
+ this.providersServerUrl = target2.config.serverUrl;
33213
+ this.providersCheckedAt = Date.now();
33214
+ this.providers = [];
33215
+ void target2.runtime.hostOAuthProviders().then((items) => {
33216
+ this.providers = items;
33217
+ }).catch(() => void 0).finally(() => {
33218
+ this.providersLoading = false;
33219
+ });
33220
+ }
33221
+ return this.providers;
33222
+ }
33223
+ providerName(provider) {
33224
+ return this.providers.find((item) => item.id === provider)?.displayName ?? provider;
33225
+ }
32912
33226
  subscribe = (listener) => {
32913
33227
  this.listeners.add(listener);
32914
33228
  return () => {
@@ -32927,6 +33241,11 @@ var RemoteLoginController = class {
32927
33241
  try {
32928
33242
  const target2 = this.resolveTarget();
32929
33243
  if (target2 === void 0) throw new Error("REMOTE_NOT_READY");
33244
+ this.providers = await target2.runtime.hostOAuthProviders();
33245
+ if (attempt !== this.attempt) return;
33246
+ const available = this.providers.filter((item) => item.qrStartUrl != null);
33247
+ if (provider === "") provider = (available.find((item) => item.id === "zhihu") ?? available[0])?.id ?? "";
33248
+ if (!available.some((item) => item.id === provider)) throw new Error("OAUTH_PROVIDER_UNAVAILABLE");
32930
33249
  const session = await target2.runtime.startHostOAuthQrLogin(provider);
32931
33250
  const [qr, compactQr] = await Promise.all([
32932
33251
  renderTerminalQr(session.scanUrl),
@@ -32980,11 +33299,10 @@ function createRemoteLoginScene(controller) {
32980
33299
  close();
32981
33300
  }
32982
33301
  });
32983
- const provider = snapshot.provider === "github" ? "GitHub" : "Zhihu";
32984
- const other = snapshot.provider === "github" ? "/remote login zhihu" : "/remote login github";
33302
+ const provider = controller.providerName(snapshot.provider);
32985
33303
  const children = [
32986
33304
  React.createElement(ui.Text, { key: "title", bold: true, color: "accent" }, "Remote Host login"),
32987
- React.createElement(ui.Text, { key: "provider" }, `Provider: ${provider} \xB7 You can also use ${other}`)
33305
+ React.createElement(ui.Text, { key: "provider" }, `Provider: ${provider} \xB7 Available providers: /remote login list`)
32988
33306
  ];
32989
33307
  if (snapshot.phase === "loading") {
32990
33308
  children.push(React.createElement(ui.Text, { key: "loading", color: "subtle" }, "Creating authorization QR code\u2026"));
@@ -33279,7 +33597,11 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
33279
33597
  logger,
33280
33598
  localTypertGateway,
33281
33599
  () => ctx.get("fileViewerHost"),
33282
- subprocess === void 0 ? void 0 : subprocessTerminalSpawner(subprocess)
33600
+ subprocess === void 0 ? void 0 : subprocessTerminalSpawner(subprocess),
33601
+ () => createPluginRouteDispatcher(
33602
+ () => ctx.get("connection"),
33603
+ () => ctx.get("webServer")?.port
33604
+ )
33283
33605
  );
33284
33606
  let clientRuntime;
33285
33607
  const hostControl = runtime;