ds-harness-remote 0.5.4 → 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 (67) 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 +268 -0
  6. package/dist/client-locale.d.ts.map +1 -0
  7. package/dist/client-locale.js +521 -0
  8. package/dist/client-locale.js.map +1 -0
  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 +273 -53
  15. package/dist/client.js +273 -53
  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-api-bridge.d.ts.map +1 -1
  23. package/dist/harness-api-bridge.js +6 -2
  24. package/dist/harness-api-bridge.js.map +1 -1
  25. package/dist/harness-remote-bridge.d.ts +1 -1
  26. package/dist/harness-remote-bridge.d.ts.map +1 -1
  27. package/dist/harness-remote-bridge.js +9 -3
  28. package/dist/harness-remote-bridge.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +393 -63
  31. package/dist/index.js.map +4 -4
  32. package/dist/plugin-route-bridge.d.ts +23 -0
  33. package/dist/plugin-route-bridge.d.ts.map +1 -0
  34. package/dist/plugin-route-bridge.js +82 -0
  35. package/dist/plugin-route-bridge.js.map +1 -0
  36. package/dist/plugin-route-client.d.ts +20 -0
  37. package/dist/plugin-route-client.d.ts.map +1 -0
  38. package/dist/plugin-route-client.js +242 -0
  39. package/dist/plugin-route-client.js.map +1 -0
  40. package/dist/plugin-route-dispatcher.d.ts +10 -0
  41. package/dist/plugin-route-dispatcher.d.ts.map +1 -0
  42. package/dist/plugin-route-dispatcher.js +61 -0
  43. package/dist/plugin-route-dispatcher.js.map +1 -0
  44. package/dist/rpc-router.d.ts +4 -1
  45. package/dist/rpc-router.d.ts.map +1 -1
  46. package/dist/rpc-router.js +11 -1
  47. package/dist/rpc-router.js.map +1 -1
  48. package/dist/server-api.d.ts +9 -1
  49. package/dist/server-api.d.ts.map +1 -1
  50. package/dist/server-api.js +61 -2
  51. package/dist/server-api.js.map +1 -1
  52. package/dist/service.d.ts +5 -2
  53. package/dist/service.d.ts.map +1 -1
  54. package/dist/service.js +18 -2
  55. package/dist/service.js.map +1 -1
  56. package/dist/tui-command.d.ts.map +1 -1
  57. package/dist/tui-command.js +58 -18
  58. package/dist/tui-command.js.map +1 -1
  59. package/dist/typert-gateway-contract.d.ts +4 -0
  60. package/dist/typert-gateway-contract.d.ts.map +1 -1
  61. package/dist/typert-gateway-switch.d.ts.map +1 -1
  62. package/dist/typert-gateway-switch.js +16 -1
  63. package/dist/typert-gateway-switch.js.map +1 -1
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/dsh-plugin.json +1 -1
  67. 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.4";
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) {
@@ -31120,7 +31329,11 @@ var HarnessApiBridge = class {
31120
31329
  continue;
31121
31330
  }
31122
31331
  this.trackRespondable(frame);
31123
- await this.publish("harness.api.frame", { streamId, frame });
31332
+ const data2 = { streamId, frame };
31333
+ if (encodeMessage(createEvent("harness.api.frame", data2)).byteLength > MAX_SECURE_MESSAGE_BYTES) {
31334
+ throw new Error("Oversized Harness API frame");
31335
+ }
31336
+ await this.publish("harness.api.frame", data2);
31124
31337
  }
31125
31338
  if (signal.aborted) reason = "cancelled";
31126
31339
  } catch {
@@ -31452,7 +31665,7 @@ function concatChunks3(chunks, totalBytes) {
31452
31665
 
31453
31666
  // src/harness-remote-bridge.ts
31454
31667
  var endpointSchema = external_exports.string().min(1).max(128).regex(/^(?:\$events(?:\/result)?|[A-Za-z0-9_$.-]+\/[A-Za-z0-9_$.-]+)$/);
31455
- 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();
31456
31669
  var streamOpenSchema4 = external_exports.object({
31457
31670
  streamId: external_exports.string().min(1).max(128),
31458
31671
  endpoint: endpointSchema,
@@ -31527,6 +31740,8 @@ var HARNESS_REMOTE_ALLOWLIST = [
31527
31740
  "messageFeedback/put",
31528
31741
  "pluginInventory/list",
31529
31742
  "permissionPresets/catalog",
31743
+ "schedule/list",
31744
+ "schedule/catalog",
31530
31745
  "session/attachment",
31531
31746
  "session/cancel",
31532
31747
  "session/canOpenWorkspacePath",
@@ -31593,7 +31808,7 @@ var HarnessRemoteBridge = class {
31593
31808
  incomingTransfers = /* @__PURE__ */ new Map();
31594
31809
  outgoingTransfers = /* @__PURE__ */ new Map();
31595
31810
  async call(input2) {
31596
- const params = callSchema3.parse(input2);
31811
+ const params = callSchema4.parse(input2);
31597
31812
  this.assertAllowed(params.endpoint);
31598
31813
  if (TERMINAL_STREAMS.has(params.endpoint)) throw new RpcError("METHOD_NOT_ALLOWED", "Use a stream for this terminal endpoint.");
31599
31814
  if (["settings/update", "settings/replace", "settings/mutate"].includes(params.endpoint)) {
@@ -31713,7 +31928,7 @@ var HarnessRemoteBridge = class {
31713
31928
  } catch {
31714
31929
  throw new RpcError("INVALID_MESSAGE", "The Harness Remote transfer does not contain a valid request.");
31715
31930
  }
31716
- const response = await this.call(callSchema3.parse(request));
31931
+ const response = await this.call(callSchema4.parse(request));
31717
31932
  const responseBytes = new TextEncoder().encode(JSON.stringify(response));
31718
31933
  if (responseBytes.byteLength <= INLINE_TRANSFER_RESPONSE_BYTES4) return { kind: "inline", response };
31719
31934
  if (responseBytes.byteLength > MAX_HARNESS_API_TRANSFER_BYTES) {
@@ -31810,11 +32025,15 @@ var HarnessRemoteBridge = class {
31810
32025
  try {
31811
32026
  for await (const value of source) {
31812
32027
  if (signal.aborted) break;
31813
- await this.publish("harness.remote.frame", {
32028
+ const frame = {
31814
32029
  streamId,
31815
32030
  hasValue: true,
31816
32031
  ...value === void 0 ? {} : { value }
31817
- });
32032
+ };
32033
+ if (encodeMessage(createEvent("harness.remote.frame", frame)).byteLength > MAX_SECURE_MESSAGE_BYTES) {
32034
+ throw new Error("Oversized Harness Remote frame");
32035
+ }
32036
+ await this.publish("harness.remote.frame", frame);
31818
32037
  }
31819
32038
  if (signal.aborted) reason = "cancelled";
31820
32039
  } catch (error) {
@@ -31971,7 +32190,7 @@ var CodexWorkspaceBridge = class extends WorkspaceBridge {
31971
32190
 
31972
32191
  // src/service.ts
31973
32192
  var HostPluginRuntime = class {
31974
- constructor(config, identities, apiProxy, logger, localGateway, fileViewerHost, terminalSpawner) {
32193
+ constructor(config, identities, apiProxy, logger, localGateway, fileViewerHost, terminalSpawner, pluginRoutes) {
31975
32194
  this.config = config;
31976
32195
  this.identities = identities;
31977
32196
  this.apiProxy = apiProxy;
@@ -31979,6 +32198,7 @@ var HostPluginRuntime = class {
31979
32198
  this.localGateway = localGateway;
31980
32199
  this.fileViewerHost = fileViewerHost;
31981
32200
  this.terminalSpawner = terminalSpawner;
32201
+ this.pluginRoutes = pluginRoutes;
31982
32202
  this.terminalEnabled = config.terminal.enabled;
31983
32203
  this.loopbackPorts = [...config.loopback.ports];
31984
32204
  this.codex = new CodexRemoteDomain(config.codex, logger);
@@ -32017,6 +32237,8 @@ var HostPluginRuntime = class {
32017
32237
  context,
32018
32238
  (event, data2) => send(createEvent(event, data2))
32019
32239
  );
32240
+ const dispatcher = this.pluginRoutes?.();
32241
+ const pluginRoute = dispatcher === void 0 ? void 0 : new PluginRouteBridge(dispatcher, this.logger);
32020
32242
  return new RpcRouter(
32021
32243
  harnessApi,
32022
32244
  void 0,
@@ -32028,7 +32250,8 @@ var HostPluginRuntime = class {
32028
32250
  cursor2,
32029
32251
  // Handles and their lifetime belong to this connection; only policy is shared.
32030
32252
  this.createLoopbackHost(),
32031
- () => this.hostWorkspaceTypes()
32253
+ () => this.hostWorkspaceTypes(),
32254
+ pluginRoute
32032
32255
  );
32033
32256
  }, this.logger);
32034
32257
  if (config.serverUrl !== void 0) {
@@ -32142,6 +32365,12 @@ var HostPluginRuntime = class {
32142
32365
  }
32143
32366
  this.serverConnection.reconnect();
32144
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
+ }
32145
32374
  async startHostOAuthQrLogin(provider) {
32146
32375
  if (this.serverApi === void 0) {
32147
32376
  throw new ServerApiError("SERVER_NOT_CONFIGURED", "Remote Host Server is unavailable.", false);
@@ -32348,6 +32577,7 @@ var HostPluginRuntime = class {
32348
32577
  capabilities.push("harness.api.v1", "harness.api.transfer.v1");
32349
32578
  }
32350
32579
  if (this.fileViewerHost?.() !== void 0) capabilities.push("fileviewer.read.v1");
32580
+ if (this.pluginRoutes?.() !== void 0) capabilities.push("plugin.route.v1");
32351
32581
  if (this.codex.isAvailable()) capabilities.push("codex.appserver.v1", "codex.appserver.transfer.v1");
32352
32582
  if (this.acp.enabledBackends().length > 0) {
32353
32583
  capabilities.push("agent.acp.v1", "agent.acp.transfer.v1", "agent.acp.workspace-files.v1");
@@ -32428,6 +32658,61 @@ function isPlainRecord(value) {
32428
32658
  return typeof value === "object" && value !== null && !Array.isArray(value);
32429
32659
  }
32430
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
+
32431
32716
  // src/cli.ts
32432
32717
  import { stat as stat7 } from "node:fs/promises";
32433
32718
  import { hostname as hostname3 } from "node:os";
@@ -32465,16 +32750,20 @@ async function register(args, runtime) {
32465
32750
  return 0;
32466
32751
  }
32467
32752
  async function login(args, runtime) {
32468
- if (args.length > 1) throw new CliUsageError("Usage: ds-harness-remote login [github|zhihu]");
32469
- const provider = args[0] ?? "zhihu";
32470
- if (provider !== "github" && provider !== "zhihu") {
32471
- throw new CliUsageError("Login provider must be github or zhihu.");
32472
- }
32753
+ if (args.length > 1) throw new CliUsageError("Usage: ds-harness-remote login [provider|list]");
32473
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;
32474
32764
  const session = await context.api.startOAuthQrLogin(provider);
32475
32765
  const qr = await runtime.renderQr(session.scanUrl);
32476
- const providerName = provider === "github" ? "GitHub" : "Zhihu";
32477
- 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.
32478
32767
  `);
32479
32768
  write(runtime.stdout, "Scan this QR code to authorize this Host:\n\n");
32480
32769
  write(runtime.stdout, `${qr.trimEnd()}
@@ -32652,9 +32941,6 @@ async function renderCompactTerminalQr(url) {
32652
32941
  function authorizedMessage(authorization) {
32653
32942
  return authorization.account === void 0 ? "Remote Host login complete." : `Remote Host login complete for ${authorization.account}.`;
32654
32943
  }
32655
- function alternativeProviderHint(provider) {
32656
- return provider === "zhihu" ? "GitHub is also supported: ds-harness-remote login github" : "Zhihu is also supported: ds-harness-remote login zhihu";
32657
- }
32658
32944
  function authorizationLabel(method) {
32659
32945
  if (method === "host_registration_code") return "Host registration code";
32660
32946
  if (method === "owned_device") return "owned device";
@@ -32671,13 +32957,13 @@ function cliErrorMessage(error) {
32671
32957
  function helpText() {
32672
32958
  return [
32673
32959
  "Usage:",
32674
- " ds-harness-remote login [github|zhihu]",
32960
+ " ds-harness-remote login [provider|list]",
32675
32961
  " ds-harness-remote register <server-token>",
32676
32962
  " ds-harness-remote status",
32677
32963
  " ds-harness-remote logout",
32678
32964
  "",
32679
32965
  "Inside dsh-TUI, use /remote login, /remote status, or /remote logout.",
32680
- "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.",
32681
32967
  `The Server is ${DEFAULT_REMOTE_SERVER_URL}.`,
32682
32968
  "Host configuration is not exposed by this CLI yet. Restart dsh-tui after login or logout.",
32683
32969
  ""
@@ -32720,7 +33006,7 @@ function installTuiRemoteCommand(ctx, resolveTarget) {
32720
33006
  const definition = {
32721
33007
  name: "remote",
32722
33008
  description: "Manage Remote Host login and connection status",
32723
- input: { hint: "[status | login [github|zhihu] | logout]" },
33009
+ input: { hint: "[status | login [provider|list] | logout]" },
32724
33010
  recordInput: false,
32725
33011
  handler: async ({ rawInput }) => {
32726
33012
  const args = rawInput.trim().split(/\s+/u).filter(Boolean);
@@ -32730,9 +33016,19 @@ function installTuiRemoteCommand(ctx, resolveTarget) {
32730
33016
  return { kind: "success", text: formatRemoteStatusInline(resolveTarget()) };
32731
33017
  }
32732
33018
  if (command === "login" && args.length <= 2) {
32733
- const provider = args[1] ?? "zhihu";
32734
- if (provider !== "github" && provider !== "zhihu") {
32735
- 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]" };
32736
33032
  }
32737
33033
  if (scenes === void 0) {
32738
33034
  return {
@@ -32776,7 +33072,7 @@ function installTuiRemoteCommand(ctx, resolveTarget) {
32776
33072
  en: "Manage Remote Host login and connection status",
32777
33073
  zh: "\u7BA1\u7406 Remote Host \u767B\u5F55\u548C\u8FDE\u63A5\u72B6\u6001"
32778
33074
  },
32779
- children: remoteCommandChildren
33075
+ children: (path) => remoteCommandChildren(path, login2.completionProviders())
32780
33076
  }));
32781
33077
  const disposeScene = registerOptional(tuiContext, "login scene", () => scenes?.register({
32782
33078
  id: REMOTE_LOGIN_SCENE_ID,
@@ -32820,7 +33116,7 @@ function registerOptional(ctx, feature, register2) {
32820
33116
  };
32821
33117
  }
32822
33118
  }
32823
- function remoteCommandChildren(canonicalPath) {
33119
+ function remoteCommandChildren(canonicalPath, providers) {
32824
33120
  if (canonicalPath.length === 1 && canonicalPath[0] === "remote") {
32825
33121
  return [
32826
33122
  {
@@ -32830,8 +33126,8 @@ function remoteCommandChildren(canonicalPath) {
32830
33126
  },
32831
33127
  {
32832
33128
  name: "login",
32833
- description: "Authorize this Host with a GitHub or Zhihu QR code",
32834
- 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" }
32835
33131
  },
32836
33132
  {
32837
33133
  name: "logout",
@@ -32842,8 +33138,12 @@ function remoteCommandChildren(canonicalPath) {
32842
33138
  }
32843
33139
  if (canonicalPath.length === 2 && canonicalPath[0] === "remote" && canonicalPath[1] === "login") {
32844
33140
  return [
32845
- { name: "zhihu", description: "Sign in with Zhihu (default)", descriptions: { zh: "\u4F7F\u7528\u77E5\u4E4E\u767B\u5F55\uFF08\u9ED8\u8BA4\uFF09" } },
32846
- { 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
+ }))
32847
33147
  ];
32848
33148
  }
32849
33149
  return [];
@@ -32866,13 +33166,13 @@ function remoteStatusLines(target2) {
32866
33166
  `Authorization: ${status2.authorized ? status2.account === void 0 ? "logged in" : `logged in (${status2.account})` : "logged out"}`,
32867
33167
  `Server connection: ${connection}`,
32868
33168
  ...status2.error === void 0 ? [] : [`Connection error: ${status2.error}`],
32869
- ...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]."] : [],
32870
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"}`,
32871
33171
  `Remote clients: ${diagnostics.activeConnections}`,
32872
33172
  `Codex Remote: ${codex.enabled ? codex.state : "disabled"}`,
32873
33173
  `Agent ACP: ${cursor2.enabled ? cursor2.state : "disabled"}`,
32874
33174
  "",
32875
- "Commands: /remote login [github|zhihu] \xB7 /remote status \xB7 /remote logout"
33175
+ "Commands: /remote login [provider|list] \xB7 /remote status \xB7 /remote logout"
32876
33176
  ];
32877
33177
  }
32878
33178
  function formatRemoteStatusInline(target2) {
@@ -32883,7 +33183,7 @@ function remoteCommandUsage() {
32883
33183
  "Usage:",
32884
33184
  " /remote",
32885
33185
  " /remote status",
32886
- " /remote login [github|zhihu]",
33186
+ " /remote login [provider|list]",
32887
33187
  " /remote logout"
32888
33188
  ].join("\n");
32889
33189
  }
@@ -32900,7 +33200,29 @@ var RemoteLoginController = class {
32900
33200
  current = { phase: "idle", provider: "zhihu" };
32901
33201
  listeners = /* @__PURE__ */ new Set();
32902
33202
  attempt = 0;
33203
+ providers = [];
33204
+ providersLoading = false;
33205
+ providersCheckedAt = 0;
33206
+ providersServerUrl;
32903
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
+ }
32904
33226
  subscribe = (listener) => {
32905
33227
  this.listeners.add(listener);
32906
33228
  return () => {
@@ -32919,6 +33241,11 @@ var RemoteLoginController = class {
32919
33241
  try {
32920
33242
  const target2 = this.resolveTarget();
32921
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");
32922
33249
  const session = await target2.runtime.startHostOAuthQrLogin(provider);
32923
33250
  const [qr, compactQr] = await Promise.all([
32924
33251
  renderTerminalQr(session.scanUrl),
@@ -32972,11 +33299,10 @@ function createRemoteLoginScene(controller) {
32972
33299
  close();
32973
33300
  }
32974
33301
  });
32975
- const provider = snapshot.provider === "github" ? "GitHub" : "Zhihu";
32976
- const other = snapshot.provider === "github" ? "/remote login zhihu" : "/remote login github";
33302
+ const provider = controller.providerName(snapshot.provider);
32977
33303
  const children = [
32978
33304
  React.createElement(ui.Text, { key: "title", bold: true, color: "accent" }, "Remote Host login"),
32979
- 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`)
32980
33306
  ];
32981
33307
  if (snapshot.phase === "loading") {
32982
33308
  children.push(React.createElement(ui.Text, { key: "loading", color: "subtle" }, "Creating authorization QR code\u2026"));
@@ -33271,7 +33597,11 @@ async function activate(ctx, readConfig, entryId, tuiBinding) {
33271
33597
  logger,
33272
33598
  localTypertGateway,
33273
33599
  () => ctx.get("fileViewerHost"),
33274
- 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
+ )
33275
33605
  );
33276
33606
  let clientRuntime;
33277
33607
  const hostControl = runtime;