@oar-sh/oar 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { execFileSync, spawn } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
function shellQuote(value) {
|
|
10
|
+
return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parsePositiveInt(value) {
|
|
14
|
+
const num = Number.parseInt(String(value || ''), 10);
|
|
15
|
+
return Number.isInteger(num) && num > 0 ? num : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isPidAlive(pidValue) {
|
|
19
|
+
const pid = Number(pidValue);
|
|
20
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const code = String(error?.code || '').trim().toUpperCase();
|
|
26
|
+
return code === 'EPERM';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeText(value) {
|
|
31
|
+
const text = String(value || '').trim();
|
|
32
|
+
return text || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveOpenAIWireApi(model = '') {
|
|
36
|
+
const normalized = String(model || '').trim().toLowerCase().replace(/^openai\//, '');
|
|
37
|
+
if (
|
|
38
|
+
normalized.startsWith('gpt-5')
|
|
39
|
+
|| normalized.startsWith('codex-')
|
|
40
|
+
|| normalized === 'o1'
|
|
41
|
+
|| normalized.startsWith('o1-')
|
|
42
|
+
|| normalized === 'o3'
|
|
43
|
+
|| normalized.startsWith('o3-')
|
|
44
|
+
|| normalized.startsWith('o4-')
|
|
45
|
+
) {
|
|
46
|
+
return 'responses';
|
|
47
|
+
}
|
|
48
|
+
return 'completions';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function applyOpenAIProviderEnvironment(env = {}, {
|
|
52
|
+
enabled = false,
|
|
53
|
+
apiKey = '',
|
|
54
|
+
model = '',
|
|
55
|
+
baseUrl = 'https://api.openai.com/v1',
|
|
56
|
+
wireApi = '',
|
|
57
|
+
} = {}) {
|
|
58
|
+
const next = { ...env };
|
|
59
|
+
for (const key of [
|
|
60
|
+
'COPILOT_PROVIDER_TYPE',
|
|
61
|
+
'COPILOT_PROVIDER_BASE_URL',
|
|
62
|
+
'COPILOT_PROVIDER_API_KEY',
|
|
63
|
+
'COPILOT_PROVIDER_WIRE_API',
|
|
64
|
+
'COPILOT_MODEL',
|
|
65
|
+
]) {
|
|
66
|
+
delete next[key];
|
|
67
|
+
}
|
|
68
|
+
if (!enabled) return next;
|
|
69
|
+
const normalizedApiKey = normalizeText(apiKey);
|
|
70
|
+
const normalizedModel = normalizeText(model);
|
|
71
|
+
if (!normalizedApiKey) throw new Error('openai-api-key-not-configured');
|
|
72
|
+
if (!normalizedModel) throw new Error('openai-model-not-configured');
|
|
73
|
+
next.COPILOT_PROVIDER_TYPE = 'openai';
|
|
74
|
+
next.COPILOT_PROVIDER_BASE_URL = normalizeText(baseUrl) || 'https://api.openai.com/v1';
|
|
75
|
+
next.COPILOT_PROVIDER_API_KEY = normalizedApiKey;
|
|
76
|
+
next.COPILOT_PROVIDER_WIRE_API = normalizeText(wireApi) || resolveOpenAIWireApi(normalizedModel);
|
|
77
|
+
next.COPILOT_MODEL = normalizedModel;
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The worker kind an environment names, or `copilot` (the extension engine,
|
|
83
|
+
* which is the absence of a kind) for anything unrecognised.
|
|
84
|
+
*
|
|
85
|
+
* Validated against `NODE_WORKER_DESCRIPTORS` rather than a parallel list of
|
|
86
|
+
* kind names: a new node worker is then one descriptor entry, and there is no
|
|
87
|
+
* second place that can be forgotten and silently route the new kind back to
|
|
88
|
+
* the Copilot CLI. Referenced lazily — this is only ever called at request
|
|
89
|
+
* time, long after the descriptor table is initialised.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveWorkerKind(env = {}) {
|
|
92
|
+
const kind = String(env?.COPILOT_WEB_RELAY_WORKER_KIND || '').trim().toLowerCase();
|
|
93
|
+
return Object.hasOwn(NODE_WORKER_DESCRIPTORS, kind) ? kind : 'copilot';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function applyClaudeProviderEnvironment(env = {}, {
|
|
97
|
+
enabled = false,
|
|
98
|
+
model = '',
|
|
99
|
+
} = {}) {
|
|
100
|
+
const next = { ...env };
|
|
101
|
+
// Only clear the worker kind this provider owns so clear chains stay
|
|
102
|
+
// order-independent when multiple provider appliers run back to back.
|
|
103
|
+
if (resolveWorkerKind(next) === 'claude') delete next.COPILOT_WEB_RELAY_WORKER_KIND;
|
|
104
|
+
delete next.CLAUDE_RELAY_MODEL;
|
|
105
|
+
if (!enabled) return next;
|
|
106
|
+
// The Claude worker authenticates through the host's logged-in Claude
|
|
107
|
+
// credentials, so no API key flows through the environment here.
|
|
108
|
+
next.COPILOT_WEB_RELAY_WORKER_KIND = 'claude';
|
|
109
|
+
const normalizedModel = normalizeText(model);
|
|
110
|
+
if (normalizedModel) next.CLAUDE_RELAY_MODEL = normalizedModel;
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function applyCursorProviderEnvironment(env = {}, {
|
|
115
|
+
enabled = false,
|
|
116
|
+
model = '',
|
|
117
|
+
apiKey = '',
|
|
118
|
+
} = {}) {
|
|
119
|
+
const next = { ...env };
|
|
120
|
+
if (resolveWorkerKind(next) === 'cursor') delete next.COPILOT_WEB_RELAY_WORKER_KIND;
|
|
121
|
+
delete next.CURSOR_RELAY_MODEL;
|
|
122
|
+
delete next.CURSOR_API_KEY;
|
|
123
|
+
if (!enabled) return next;
|
|
124
|
+
const normalizedApiKey = normalizeText(apiKey);
|
|
125
|
+
if (!normalizedApiKey) throw new Error('cursor-api-key-not-configured');
|
|
126
|
+
next.COPILOT_WEB_RELAY_WORKER_KIND = 'cursor';
|
|
127
|
+
next.CURSOR_API_KEY = normalizedApiKey;
|
|
128
|
+
const normalizedModel = normalizeText(model);
|
|
129
|
+
if (normalizedModel) next.CURSOR_RELAY_MODEL = normalizedModel;
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function applyGrokProviderEnvironment(env = {}, {
|
|
134
|
+
enabled = false,
|
|
135
|
+
model = '',
|
|
136
|
+
command = '',
|
|
137
|
+
} = {}) {
|
|
138
|
+
const next = { ...env };
|
|
139
|
+
// Host-login provider (like Claude): no API key is required in the relay.
|
|
140
|
+
if (resolveWorkerKind(next) === 'grok') delete next.COPILOT_WEB_RELAY_WORKER_KIND;
|
|
141
|
+
delete next.GROK_RELAY_MODEL;
|
|
142
|
+
delete next.GROK_CLI_COMMAND;
|
|
143
|
+
if (!enabled) return next;
|
|
144
|
+
next.COPILOT_WEB_RELAY_WORKER_KIND = 'grok';
|
|
145
|
+
const normalizedModel = normalizeText(model);
|
|
146
|
+
if (normalizedModel) next.GROK_RELAY_MODEL = normalizedModel;
|
|
147
|
+
const normalizedCommand = normalizeText(command);
|
|
148
|
+
if (normalizedCommand) next.GROK_CLI_COMMAND = normalizedCommand;
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The Copilot SDK engine: the same `github`/`openai` providers the extension
|
|
154
|
+
* serves, run as a plain Node worker against the CLI's headless runtime.
|
|
155
|
+
*
|
|
156
|
+
* Unlike the other appliers this one configures no credentials of its own. The
|
|
157
|
+
* SDK worker authenticates exactly like the TUI does (the host's `copilot`
|
|
158
|
+
* login), and the one path it needs — `COPILOT_SDK_PATH` — is already resolved
|
|
159
|
+
* once into the base launch environment for every session, extension or SDK.
|
|
160
|
+
* (It needs no CLI executable: the runtime entry point is derived from the SDK
|
|
161
|
+
* bundle's own version directory, so the two can never drift apart.) This
|
|
162
|
+
* applier's whole job is selecting the worker kind and handing over the model.
|
|
163
|
+
*
|
|
164
|
+
* BYOK is deliberately NOT re-implemented here: an `openai` session is composed
|
|
165
|
+
* as `applyCopilotSdkProviderEnvironment(applyOpenAIProviderEnvironment(...))`,
|
|
166
|
+
* so one OpenAI configuration drives both engines and the secret-env-file
|
|
167
|
+
* plumbing for `COPILOT_PROVIDER_API_KEY` is untouched. The SDK worker reads
|
|
168
|
+
* that family in-process (`copilot-byok-provider.mjs`) because the runtime
|
|
169
|
+
* ignores it for SDK-created sessions.
|
|
170
|
+
*
|
|
171
|
+
* `COPILOT_SDK_PATH` is required rather than optional: it is resolved from the
|
|
172
|
+
* installed CLI's own bundle, so its absence means there is no CLI to run and
|
|
173
|
+
* failing here names the problem instead of spawning a worker that dies on its
|
|
174
|
+
* first import.
|
|
175
|
+
*/
|
|
176
|
+
export function applyCopilotSdkProviderEnvironment(env = {}, {
|
|
177
|
+
enabled = false,
|
|
178
|
+
model = '',
|
|
179
|
+
} = {}) {
|
|
180
|
+
const next = { ...env };
|
|
181
|
+
// Only clear the worker kind this provider owns so clear chains stay
|
|
182
|
+
// order-independent when multiple provider appliers run back to back.
|
|
183
|
+
if (resolveWorkerKind(next) === 'copilot-sdk') delete next.COPILOT_WEB_RELAY_WORKER_KIND;
|
|
184
|
+
delete next.COPILOT_RELAY_MODEL;
|
|
185
|
+
if (!enabled) return next;
|
|
186
|
+
if (!normalizeText(next.COPILOT_SDK_PATH)) throw new Error('copilot-sdk-path-not-resolved');
|
|
187
|
+
next.COPILOT_WEB_RELAY_WORKER_KIND = 'copilot-sdk';
|
|
188
|
+
const normalizedModel = normalizeText(model);
|
|
189
|
+
if (normalizedModel) next.COPILOT_RELAY_MODEL = normalizedModel;
|
|
190
|
+
return next;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Why the Copilot SDK engine cannot be selected right now, or null when it can.
|
|
195
|
+
*
|
|
196
|
+
* The engine toggle is a promise the UI makes on the relay's behalf ("new
|
|
197
|
+
* Copilot conversations run headless"), and there are two ways to save it and
|
|
198
|
+
* have precisely nothing happen — both invisible from the settings panel, and
|
|
199
|
+
* both indistinguishable from the feature simply not working:
|
|
200
|
+
*
|
|
201
|
+
* - `COPILOT_SDK_PATH` never resolved. The worker's very first act is to
|
|
202
|
+
* import the SDK from it, so every conversation would die on spawn.
|
|
203
|
+
* - Session-worker routing is off. No worker of any node kind is ever spawned,
|
|
204
|
+
* so the setting is a no-op while the panel asserts the opposite.
|
|
205
|
+
*
|
|
206
|
+
* Refusing the save with the reason is strictly better than accepting it: the
|
|
207
|
+
* user finds out at the moment they can still act on it.
|
|
208
|
+
*/
|
|
209
|
+
export function copilotSdkEngineUnavailableReason({ env = {}, routingEnabled = false } = {}) {
|
|
210
|
+
if (!normalizeText(env?.COPILOT_SDK_PATH)) {
|
|
211
|
+
// Worth spelling out: the launch environment is built ONCE at boot, so
|
|
212
|
+
// installing the CLI now and re-saving without a restart fails identically.
|
|
213
|
+
return 'The Copilot SDK was not found when the relay started (COPILOT_SDK_PATH did not resolve). '
|
|
214
|
+
+ 'Install or upgrade the GitHub Copilot CLI, then restart the relay — the launch environment is '
|
|
215
|
+
+ 'snapshotted at startup, so a CLI installed since then is not visible yet.';
|
|
216
|
+
}
|
|
217
|
+
if (routingEnabled !== true) {
|
|
218
|
+
return 'The SDK engine requires session worker routing, which is disabled on this relay '
|
|
219
|
+
+ '(SESSION_WORKER_ROUTING_ENABLED). With routing off no SDK worker is ever spawned, so the engine '
|
|
220
|
+
+ 'setting would have no effect.';
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function isClaudeWorkerEnvironment(env = {}) {
|
|
226
|
+
return String(env?.COPILOT_WEB_RELAY_WORKER_KIND || '').trim().toLowerCase() === 'claude';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function isCursorWorkerEnvironment(env = {}) {
|
|
230
|
+
return resolveWorkerKind(env) === 'cursor';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function isGrokWorkerEnvironment(env = {}) {
|
|
234
|
+
return resolveWorkerKind(env) === 'grok';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function isCopilotSdkWorkerEnvironment(env = {}) {
|
|
238
|
+
return resolveWorkerKind(env) === 'copilot-sdk';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function resolveClaudeWorkerScriptPath(env = {}) {
|
|
242
|
+
const explicit = normalizeText(env?.COPILOT_WEB_RELAY_CLAUDE_WORKER_PATH);
|
|
243
|
+
if (explicit) return explicit;
|
|
244
|
+
const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
|
|
245
|
+
if (repoRoot) return path.join(repoRoot, 'server', 'claude-worker', 'claude-session-worker.mjs');
|
|
246
|
+
const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
247
|
+
if (serverDir) return path.join(serverDir, 'claude-worker', 'claude-session-worker.mjs');
|
|
248
|
+
return path.join(process.cwd(), 'server', 'claude-worker', 'claude-session-worker.mjs');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function resolveCursorWorkerScriptPath(env = {}) {
|
|
252
|
+
const explicit = normalizeText(env?.COPILOT_WEB_RELAY_CURSOR_WORKER_PATH);
|
|
253
|
+
if (explicit) return explicit;
|
|
254
|
+
const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
|
|
255
|
+
if (repoRoot) return path.join(repoRoot, 'server', 'cursor-worker', 'cursor-session-worker.mjs');
|
|
256
|
+
const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
257
|
+
if (serverDir) return path.join(serverDir, 'cursor-worker', 'cursor-session-worker.mjs');
|
|
258
|
+
return path.join(process.cwd(), 'server', 'cursor-worker', 'cursor-session-worker.mjs');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function resolveGrokWorkerScriptPath(env = {}) {
|
|
262
|
+
const explicit = normalizeText(env?.COPILOT_WEB_RELAY_GROK_WORKER_PATH);
|
|
263
|
+
if (explicit) return explicit;
|
|
264
|
+
const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
|
|
265
|
+
if (repoRoot) return path.join(repoRoot, 'server', 'grok-worker', 'grok-session-worker.mjs');
|
|
266
|
+
const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
267
|
+
if (serverDir) return path.join(serverDir, 'grok-worker', 'grok-session-worker.mjs');
|
|
268
|
+
return path.join(process.cwd(), 'server', 'grok-worker', 'grok-session-worker.mjs');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function resolveCopilotSdkWorkerScriptPath(env = {}) {
|
|
272
|
+
const explicit = normalizeText(env?.COPILOT_WEB_RELAY_COPILOT_SDK_WORKER_PATH);
|
|
273
|
+
if (explicit) return explicit;
|
|
274
|
+
const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
|
|
275
|
+
if (repoRoot) return path.join(repoRoot, 'server', 'copilot-worker', 'copilot-sdk-session-worker.mjs');
|
|
276
|
+
const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
277
|
+
if (serverDir) return path.join(serverDir, 'copilot-worker', 'copilot-sdk-session-worker.mjs');
|
|
278
|
+
return path.join(process.cwd(), 'server', 'copilot-worker', 'copilot-sdk-session-worker.mjs');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Workers that run as plain Node processes (no CLI, no pseudo-TTY). The
|
|
282
|
+
// `copilot` kind is intentionally absent: that is the extension engine, whose
|
|
283
|
+
// launch path (the real TUI under a `script` pseudo-TTY) must stay exactly
|
|
284
|
+
// as-is. `copilot-sdk` is the SAME provider on the headless SDK engine, and it
|
|
285
|
+
// belongs here with Claude/Cursor/Grok.
|
|
286
|
+
const NODE_WORKER_DESCRIPTORS = Object.freeze({
|
|
287
|
+
claude: Object.freeze({ resolveScriptPath: resolveClaudeWorkerScriptPath, windowsTitle: 'Claude Worker' }),
|
|
288
|
+
cursor: Object.freeze({ resolveScriptPath: resolveCursorWorkerScriptPath, windowsTitle: 'Cursor Worker' }),
|
|
289
|
+
grok: Object.freeze({ resolveScriptPath: resolveGrokWorkerScriptPath, windowsTitle: 'Grok Worker' }),
|
|
290
|
+
'copilot-sdk': Object.freeze({
|
|
291
|
+
resolveScriptPath: resolveCopilotSdkWorkerScriptPath,
|
|
292
|
+
windowsTitle: 'Copilot SDK Worker',
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
function resolveNodeWorkerDescriptor(env = {}) {
|
|
297
|
+
const descriptor = NODE_WORKER_DESCRIPTORS[resolveWorkerKind(env)];
|
|
298
|
+
if (!descriptor) return null;
|
|
299
|
+
return {
|
|
300
|
+
scriptPath: descriptor.resolveScriptPath(env),
|
|
301
|
+
windowsTitle: descriptor.windowsTitle,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildPosixWorkerLaunchCommand(targetSessionId, env = {}) {
|
|
306
|
+
const nodeWorker = resolveNodeWorkerDescriptor(env);
|
|
307
|
+
if (nodeWorker) {
|
|
308
|
+
const nodeExecutable = normalizeText(env?.COPILOT_WEB_RELAY_NODE) || 'node';
|
|
309
|
+
return `${shellQuote(nodeExecutable)} ${shellQuote(nodeWorker.scriptPath)} --session-id ${shellQuote(targetSessionId)}`;
|
|
310
|
+
}
|
|
311
|
+
const cliExecutable = normalizeText(env?.COPILOT_WEB_RELAY_CLI_EXECUTABLE)
|
|
312
|
+
|| normalizeText(env?.COPILOT_CLI_EXECUTABLE)
|
|
313
|
+
|| normalizeText(env?.COPILOT_CLI_PATH)
|
|
314
|
+
|| 'copilot';
|
|
315
|
+
return `${shellQuote(cliExecutable)} --allow-all --session-id ${shellQuote(targetSessionId)}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function normalizeTmuxSessionName(targetSessionId) {
|
|
319
|
+
const text = String(targetSessionId || '').trim();
|
|
320
|
+
if (!text) throw new Error('missing-target-session-id');
|
|
321
|
+
if (!/^[A-Za-z0-9_-]+$/.test(text)) {
|
|
322
|
+
throw new Error(`invalid-tmux-session-name:${text}`);
|
|
323
|
+
}
|
|
324
|
+
return text;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function isTmuxAvailable({
|
|
328
|
+
platform = process.platform,
|
|
329
|
+
execFileSyncImpl = execFileSync,
|
|
330
|
+
} = {}) {
|
|
331
|
+
if (platform === 'win32') return false;
|
|
332
|
+
try {
|
|
333
|
+
execFileSyncImpl('tmux', ['-V'], {
|
|
334
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
335
|
+
});
|
|
336
|
+
return true;
|
|
337
|
+
} catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function tmuxSessionExists(sessionName, {
|
|
343
|
+
execFileSyncImpl = execFileSync,
|
|
344
|
+
} = {}) {
|
|
345
|
+
try {
|
|
346
|
+
execFileSyncImpl('tmux', ['has-session', '-t', sessionName], {
|
|
347
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
348
|
+
});
|
|
349
|
+
return true;
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function killTmuxSession(sessionName, {
|
|
356
|
+
execFileSyncImpl = execFileSync,
|
|
357
|
+
} = {}) {
|
|
358
|
+
if (!tmuxSessionExists(sessionName, { execFileSyncImpl })) return false;
|
|
359
|
+
try {
|
|
360
|
+
execFileSyncImpl('tmux', ['kill-session', '-t', sessionName], {
|
|
361
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
362
|
+
});
|
|
363
|
+
return true;
|
|
364
|
+
} catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function getTmuxPanePid(sessionName, {
|
|
370
|
+
execFileSyncImpl = execFileSync,
|
|
371
|
+
} = {}) {
|
|
372
|
+
try {
|
|
373
|
+
const output = execFileSyncImpl('tmux', ['list-panes', '-t', sessionName, '-F', '#{pane_pid}'], {
|
|
374
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
375
|
+
});
|
|
376
|
+
const firstLine = String(output || '').trim().split(/\r?\n/).find(Boolean) || '';
|
|
377
|
+
return parsePositiveInt(firstLine);
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export const WORKER_SECRET_ENV_VARS = Object.freeze(['COPILOT_PROVIDER_API_KEY', 'CURSOR_API_KEY']);
|
|
384
|
+
|
|
385
|
+
export function createWorkerSecretEnvFile(env = {}, {
|
|
386
|
+
fsImpl = fs,
|
|
387
|
+
tempRoot = os.tmpdir(),
|
|
388
|
+
} = {}) {
|
|
389
|
+
const secretLines = [];
|
|
390
|
+
for (const key of WORKER_SECRET_ENV_VARS) {
|
|
391
|
+
const value = String(env?.[key] || '').trim();
|
|
392
|
+
if (!value) continue;
|
|
393
|
+
secretLines.push(`export ${key}=${shellQuote(value)}\n`);
|
|
394
|
+
}
|
|
395
|
+
if (!secretLines.length) return null;
|
|
396
|
+
const directoryPath = fsImpl.mkdtempSync(path.join(tempRoot, 'copilot-relay-worker-'));
|
|
397
|
+
fsImpl.chmodSync(directoryPath, 0o700);
|
|
398
|
+
const filePath = path.join(directoryPath, 'provider.env');
|
|
399
|
+
fsImpl.writeFileSync(
|
|
400
|
+
filePath,
|
|
401
|
+
secretLines.join(''),
|
|
402
|
+
{ encoding: 'utf8', mode: 0o600 },
|
|
403
|
+
);
|
|
404
|
+
fsImpl.chmodSync(filePath, 0o600);
|
|
405
|
+
return {
|
|
406
|
+
filePath,
|
|
407
|
+
cleanup() {
|
|
408
|
+
fsImpl.rmSync(filePath, { force: true });
|
|
409
|
+
fsImpl.rmdirSync(directoryPath);
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Where a worker's stdout/stderr land. Before this existed workers ran with
|
|
416
|
+
* their output discarded (`stdio: 'ignore'` / bare tmux exec), so a crash
|
|
417
|
+
* left zero forensic trail — the failure mode that made the 2026-08-11
|
|
418
|
+
* incident undiagnosable. Best-effort by design: a log problem must never
|
|
419
|
+
* block a worker spawn. Naive rotation: >10 MB rolls to `<file>.1`.
|
|
420
|
+
*/
|
|
421
|
+
export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
|
|
422
|
+
fsImpl = fs,
|
|
423
|
+
pathImpl = path,
|
|
424
|
+
} = {}) {
|
|
425
|
+
try {
|
|
426
|
+
const explicitDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_LOG_DIR);
|
|
427
|
+
const serverDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
428
|
+
// `new URL(...).pathname` yields '/C:/git/...' on Windows, which pathImpl.join
|
|
429
|
+
// then mangles into '\C:\git\...'; fileURLToPath decodes it to a real host path.
|
|
430
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
431
|
+
const baseDir = explicitDir
|
|
432
|
+
|| (serverDir ? pathImpl.join(serverDir, 'logs') : pathImpl.join(moduleDir, '..', 'logs'));
|
|
433
|
+
fsImpl.mkdirSync(baseDir, { recursive: true });
|
|
434
|
+
const safeId = String(targetSessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || 'worker';
|
|
435
|
+
const logPath = pathImpl.join(baseDir, `worker-${safeId}.log`);
|
|
436
|
+
try {
|
|
437
|
+
const stats = fsImpl.statSync(logPath);
|
|
438
|
+
if (stats.size > 10 * 1024 * 1024) fsImpl.renameSync(logPath, `${logPath}.1`);
|
|
439
|
+
} catch {}
|
|
440
|
+
return logPath;
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
447
|
+
secretEnvFilePath = '',
|
|
448
|
+
workerLogPath = '',
|
|
449
|
+
} = {}) {
|
|
450
|
+
const launchEnv = {
|
|
451
|
+
...env,
|
|
452
|
+
SESSION_ID: String(targetSessionId || '').trim() || String(env?.SESSION_ID || '').trim(),
|
|
453
|
+
};
|
|
454
|
+
const hasSecretEnvValue = WORKER_SECRET_ENV_VARS
|
|
455
|
+
.some((key) => String(launchEnv[key] || '').trim());
|
|
456
|
+
const normalizedSecretEnvFilePath = String(secretEnvFilePath || '').trim();
|
|
457
|
+
if (hasSecretEnvValue && !normalizedSecretEnvFilePath) {
|
|
458
|
+
throw new Error('worker-secret-env-file-required');
|
|
459
|
+
}
|
|
460
|
+
const exports = [];
|
|
461
|
+
for (const key of [
|
|
462
|
+
'COPILOT_ALLOW_ALL',
|
|
463
|
+
'GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS',
|
|
464
|
+
'COPILOT_WEB_RELAY_ROOT',
|
|
465
|
+
'COPILOT_WEB_RELAY_SERVER_DIR',
|
|
466
|
+
'COPILOT_WEB_RELAY_CONFIG',
|
|
467
|
+
'COPILOT_WEB_RELAY_TOOLS',
|
|
468
|
+
'COPILOT_WEB_RELAY_LOG_DIR',
|
|
469
|
+
'COPILOT_WEB_RELAY_CLI_EXECUTABLE',
|
|
470
|
+
'COPILOT_WEB_RELAY_EXTENSION_BOOTSTRAP_PATH',
|
|
471
|
+
'COPILOT_SDK_PATH',
|
|
472
|
+
'EXTENSION_PATH',
|
|
473
|
+
'COPILOT_PROVIDER_TYPE',
|
|
474
|
+
'COPILOT_PROVIDER_BASE_URL',
|
|
475
|
+
'COPILOT_PROVIDER_WIRE_API',
|
|
476
|
+
'COPILOT_MODEL',
|
|
477
|
+
'COPILOT_WEB_RELAY_WORKER_KIND',
|
|
478
|
+
// The SDK worker's own model and BYOK context overrides. `COPILOT_MODEL`
|
|
479
|
+
// above is the extension engine's model variable and is read by the CLI's
|
|
480
|
+
// startup layer; the SDK worker takes its default model from
|
|
481
|
+
// `COPILOT_RELAY_MODEL`, matching every other Node worker's
|
|
482
|
+
// `*_RELAY_MODEL`. Without it here the model would silently drop to the
|
|
483
|
+
// runtime default on the tmux path while working on the detached path,
|
|
484
|
+
// which inherits the whole environment.
|
|
485
|
+
'COPILOT_RELAY_MODEL',
|
|
486
|
+
'COPILOT_PROVIDER_MAX_PROMPT_TOKENS',
|
|
487
|
+
'COPILOT_PROVIDER_MAX_OUTPUT_TOKENS',
|
|
488
|
+
// Without this the tmux path would fall back to ~/.claude while the relay
|
|
489
|
+
// (and the detached spawn path, which inherits the full environment) uses
|
|
490
|
+
// the override — worker and relay would read different config roots.
|
|
491
|
+
'CLAUDE_CONFIG_DIR',
|
|
492
|
+
// Same divergence, one variable over: the relay binds a resolved `claude`
|
|
493
|
+
// binary onto its own environment (cli-install-service.mjs §4.3), and the
|
|
494
|
+
// detached path inherits it for free. Without it here a bound binary would
|
|
495
|
+
// be silently dropped on the tmux path and the worker would fall back to
|
|
496
|
+
// whatever `claude` PATH happens to resolve — exactly the failure mode
|
|
497
|
+
// CLAUDE_CONFIG_DIR above was added for. GROK_CLI_COMMAND, its Grok
|
|
498
|
+
// counterpart, has always been on this list.
|
|
499
|
+
'CLAUDE_CODE_EXECUTABLE',
|
|
500
|
+
'CLAUDE_RELAY_MODEL',
|
|
501
|
+
'CURSOR_RELAY_MODEL',
|
|
502
|
+
'CURSOR_AGENT_STORE_DIR',
|
|
503
|
+
'GROK_RELAY_MODEL',
|
|
504
|
+
'GROK_CLI_COMMAND',
|
|
505
|
+
'GROK_ALWAYS_APPROVE',
|
|
506
|
+
'SESSION_ID',
|
|
507
|
+
'COPILOT_WORKSPACE_ROOT',
|
|
508
|
+
'INIT_CWD',
|
|
509
|
+
]) {
|
|
510
|
+
const value = String(launchEnv?.[key] || '').trim();
|
|
511
|
+
if (!value) continue;
|
|
512
|
+
exports.push(`${key}=${shellQuote(value)}`);
|
|
513
|
+
}
|
|
514
|
+
const prefix = exports.length ? `${exports.map((entry) => `export ${entry};`).join(' ')} ` : '';
|
|
515
|
+
const secretPrefix = normalizedSecretEnvFilePath
|
|
516
|
+
? `. ${shellQuote(normalizedSecretEnvFilePath)} || exit $?; rm -f ${shellQuote(normalizedSecretEnvFilePath)}; rmdir ${shellQuote(path.dirname(normalizedSecretEnvFilePath))}; `
|
|
517
|
+
: '';
|
|
518
|
+
const workerCommand = buildPosixWorkerLaunchCommand(targetSessionId, launchEnv);
|
|
519
|
+
if (resolveNodeWorkerDescriptor(launchEnv)) {
|
|
520
|
+
// Node workers (Claude, Cursor, Grok) are plain processes; no pseudo-TTY
|
|
521
|
+
// needed. Their output is teed to the worker log so a crash leaves a
|
|
522
|
+
// trail (the Copilot CLI keeps its own logs and draws a TUI, so it is
|
|
523
|
+
// deliberately not teed).
|
|
524
|
+
const teeSuffix = String(workerLogPath || '').trim()
|
|
525
|
+
? ` >> ${shellQuote(workerLogPath)} 2>&1`
|
|
526
|
+
: '';
|
|
527
|
+
return `${prefix}${secretPrefix}exec ${workerCommand}${teeSuffix}`;
|
|
528
|
+
}
|
|
529
|
+
// Use script to create a pseudo-TTY without GH_FORCE_TTY so the CLI routes
|
|
530
|
+
// ask_user requests through the SDK's onUserInputRequest handler instead of
|
|
531
|
+
// drawing terminal prompts.
|
|
532
|
+
return `${prefix}${secretPrefix}exec script -q -c ${shellQuote(workerCommand)} /dev/null`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function sleep(ms) {
|
|
536
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function buildWorkerLaunchEnv({ processCwd, workspaceRoot, env = process.env } = {}) {
|
|
540
|
+
const launchProcessCwd = String(processCwd || '').trim();
|
|
541
|
+
const launchWorkspaceRoot = String(workspaceRoot || '').trim() || launchProcessCwd;
|
|
542
|
+
if (!launchProcessCwd && !launchWorkspaceRoot) return env;
|
|
543
|
+
const launchEnv = {
|
|
544
|
+
...env,
|
|
545
|
+
COPILOT_ALLOW_ALL: 'true',
|
|
546
|
+
GITHUB_COPILOT_PROMPT_MODE_EXTENSIONS: 'true',
|
|
547
|
+
...(launchWorkspaceRoot ? {
|
|
548
|
+
COPILOT_WORKSPACE_ROOT: launchWorkspaceRoot,
|
|
549
|
+
INIT_CWD: launchWorkspaceRoot,
|
|
550
|
+
} : {}),
|
|
551
|
+
...(launchProcessCwd ? { PWD: launchProcessCwd } : {}),
|
|
552
|
+
};
|
|
553
|
+
// COPILOT_ALLOW_ALL matches the --allow-all launch flag and lets headless
|
|
554
|
+
// workers pass folder trust before extension activation. The global wrapper
|
|
555
|
+
// handles outside-package workers and defers to the project extension inside
|
|
556
|
+
// this package, so workers must not force both.
|
|
557
|
+
delete launchEnv.COPILOT_WEB_RELAY_FORCE_GLOBAL_EXTENSION;
|
|
558
|
+
return launchEnv;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export async function launchSessionCli({
|
|
562
|
+
targetSessionId,
|
|
563
|
+
cwd,
|
|
564
|
+
processCwd = '',
|
|
565
|
+
workspaceRoot = '',
|
|
566
|
+
env = process.env,
|
|
567
|
+
platform = process.platform,
|
|
568
|
+
spawnImpl = spawn,
|
|
569
|
+
execFileSyncImpl = execFileSync,
|
|
570
|
+
processInspector = null,
|
|
571
|
+
tmuxPollAttempts = 4,
|
|
572
|
+
tmuxPollDelayMs = 200,
|
|
573
|
+
detachedPollAttempts = 10,
|
|
574
|
+
detachedPollDelayMs = 200,
|
|
575
|
+
allowProcessReuse = true,
|
|
576
|
+
createSecretEnvFileImpl = createWorkerSecretEnvFile,
|
|
577
|
+
} = {}) {
|
|
578
|
+
const target = String(targetSessionId || '').trim();
|
|
579
|
+
if (!target) throw new Error('missing-target-session-id');
|
|
580
|
+
|
|
581
|
+
// Kill switch for test/e2e servers: a relay started with this env var must
|
|
582
|
+
// never spawn real session workers (Copilot CLI clients or Claude workers).
|
|
583
|
+
if (String(env?.COPILOT_WEB_RELAY_DISABLE_CLI_SPAWN || '').trim()) {
|
|
584
|
+
throw new Error('cli-spawn-disabled');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (allowProcessReuse) {
|
|
588
|
+
const liveProcess = typeof processInspector?.findProcessForSession === 'function'
|
|
589
|
+
? processInspector.findProcessForSession(target)
|
|
590
|
+
: null;
|
|
591
|
+
const liveProcessPid = parsePositiveInt(liveProcess?.processId);
|
|
592
|
+
if (liveProcessPid && isPidAlive(liveProcessPid)) {
|
|
593
|
+
return {
|
|
594
|
+
pid: liveProcessPid,
|
|
595
|
+
reused: true,
|
|
596
|
+
launchMode: platform === 'win32' ? 'detached' : 'existing',
|
|
597
|
+
tmuxSessionName: null,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const launchWorkspaceRoot = String(workspaceRoot || cwd || process.cwd());
|
|
603
|
+
const launchProcessCwd = String(processCwd || cwd || process.cwd());
|
|
604
|
+
const launchEnv = buildWorkerLaunchEnv({
|
|
605
|
+
processCwd: launchProcessCwd,
|
|
606
|
+
workspaceRoot: launchWorkspaceRoot,
|
|
607
|
+
env,
|
|
608
|
+
});
|
|
609
|
+
const launchSessionEnv = {
|
|
610
|
+
...launchEnv,
|
|
611
|
+
SESSION_ID: target,
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
if (isTmuxAvailable({ platform, execFileSyncImpl })) {
|
|
615
|
+
const sessionName = normalizeTmuxSessionName(target);
|
|
616
|
+
const tmuxEnv = { ...launchSessionEnv };
|
|
617
|
+
delete tmuxEnv.TMUX;
|
|
618
|
+
delete tmuxEnv.TMUX_PANE;
|
|
619
|
+
delete tmuxEnv.COPILOT_PROVIDER_API_KEY;
|
|
620
|
+
delete tmuxEnv.CURSOR_API_KEY;
|
|
621
|
+
if (allowProcessReuse) {
|
|
622
|
+
const existingPanePid = getTmuxPanePid(sessionName, { execFileSyncImpl });
|
|
623
|
+
if (existingPanePid && isPidAlive(existingPanePid)) {
|
|
624
|
+
return {
|
|
625
|
+
pid: existingPanePid,
|
|
626
|
+
reused: true,
|
|
627
|
+
launchMode: 'tmux',
|
|
628
|
+
tmuxSessionName: sessionName,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
killTmuxSession(sessionName, { execFileSyncImpl });
|
|
633
|
+
const secretEnvFile = createSecretEnvFileImpl(launchSessionEnv);
|
|
634
|
+
const tmuxWorkerLogPath = resolveNodeWorkerDescriptor(launchSessionEnv)
|
|
635
|
+
? prepareWorkerLogFile(target, launchSessionEnv)
|
|
636
|
+
: null;
|
|
637
|
+
try {
|
|
638
|
+
execFileSyncImpl('tmux', [
|
|
639
|
+
'new-session',
|
|
640
|
+
'-d',
|
|
641
|
+
'-s',
|
|
642
|
+
sessionName,
|
|
643
|
+
'-c',
|
|
644
|
+
launchProcessCwd,
|
|
645
|
+
'sh',
|
|
646
|
+
'-lc',
|
|
647
|
+
buildTmuxWorkerShellCommand(target, launchSessionEnv, {
|
|
648
|
+
secretEnvFilePath: secretEnvFile?.filePath,
|
|
649
|
+
workerLogPath: tmuxWorkerLogPath || '',
|
|
650
|
+
}),
|
|
651
|
+
], {
|
|
652
|
+
env: tmuxEnv,
|
|
653
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
654
|
+
});
|
|
655
|
+
} catch (error) {
|
|
656
|
+
secretEnvFile?.cleanup?.();
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
const attempts = Math.max(1, Number(tmuxPollAttempts) || 1);
|
|
660
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
661
|
+
await sleep(Math.max(50, Number(tmuxPollDelayMs) || 200));
|
|
662
|
+
const processMatch = typeof processInspector?.findProcessForSession === 'function'
|
|
663
|
+
? processInspector.findProcessForSession(target)
|
|
664
|
+
: null;
|
|
665
|
+
if (processMatch?.processId) {
|
|
666
|
+
return {
|
|
667
|
+
pid: Number(processMatch.processId),
|
|
668
|
+
reused: false,
|
|
669
|
+
launchMode: 'tmux',
|
|
670
|
+
tmuxSessionName: sessionName,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const panePid = getTmuxPanePid(sessionName, { execFileSyncImpl });
|
|
674
|
+
if (panePid && isPidAlive(panePid)) {
|
|
675
|
+
return {
|
|
676
|
+
pid: panePid,
|
|
677
|
+
reused: false,
|
|
678
|
+
launchMode: 'tmux',
|
|
679
|
+
tmuxSessionName: sessionName,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
throw new Error('worker-spawn-unhealthy:tmux-pane-missing');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const nodeWorker = resolveNodeWorkerDescriptor(launchSessionEnv);
|
|
687
|
+
const nodeWorkerExecutable = normalizeText(launchSessionEnv.COPILOT_WEB_RELAY_NODE) || 'node';
|
|
688
|
+
const posixCliExecutable = normalizeText(launchSessionEnv.COPILOT_WEB_RELAY_CLI_EXECUTABLE)
|
|
689
|
+
|| normalizeText(launchSessionEnv.COPILOT_CLI_EXECUTABLE)
|
|
690
|
+
|| normalizeText(launchSessionEnv.COPILOT_CLI_PATH)
|
|
691
|
+
|| 'copilot';
|
|
692
|
+
const spawnCommand = platform === 'win32'
|
|
693
|
+
? (launchSessionEnv.ComSpec || process.env.ComSpec || 'cmd.exe')
|
|
694
|
+
: (nodeWorker ? nodeWorkerExecutable : posixCliExecutable);
|
|
695
|
+
const spawnArgs = platform === 'win32'
|
|
696
|
+
? (nodeWorker
|
|
697
|
+
? [
|
|
698
|
+
'/d',
|
|
699
|
+
'/s',
|
|
700
|
+
'/c',
|
|
701
|
+
'start',
|
|
702
|
+
`${nodeWorker.windowsTitle} ${target.slice(0, 8)}`,
|
|
703
|
+
nodeWorkerExecutable,
|
|
704
|
+
nodeWorker.scriptPath,
|
|
705
|
+
'--session-id',
|
|
706
|
+
target,
|
|
707
|
+
]
|
|
708
|
+
: [
|
|
709
|
+
'/d',
|
|
710
|
+
'/s',
|
|
711
|
+
'/c',
|
|
712
|
+
'start',
|
|
713
|
+
`Copilot Worker ${target.slice(0, 8)}`,
|
|
714
|
+
'gh',
|
|
715
|
+
'copilot',
|
|
716
|
+
'--',
|
|
717
|
+
'--allow-all',
|
|
718
|
+
'--session-id',
|
|
719
|
+
target,
|
|
720
|
+
])
|
|
721
|
+
: (nodeWorker
|
|
722
|
+
? [nodeWorker.scriptPath, '--session-id', target]
|
|
723
|
+
: ['--allow-all', '--session-id', target]);
|
|
724
|
+
// POSIX node workers tee stdout/stderr into the worker log (a crash must
|
|
725
|
+
// leave a trail). On win32 the `start` intermediary opens its own console,
|
|
726
|
+
// so fd inheritance cannot reach the worker — its window shows the output.
|
|
727
|
+
let detachedStdio = 'ignore';
|
|
728
|
+
if (platform !== 'win32' && nodeWorker) {
|
|
729
|
+
const workerLogPath = prepareWorkerLogFile(target, launchSessionEnv);
|
|
730
|
+
if (workerLogPath) {
|
|
731
|
+
try {
|
|
732
|
+
const logFd = fs.openSync(workerLogPath, 'a');
|
|
733
|
+
detachedStdio = ['ignore', logFd, logFd];
|
|
734
|
+
} catch {
|
|
735
|
+
detachedStdio = 'ignore';
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const child = spawnImpl(spawnCommand, spawnArgs, {
|
|
740
|
+
cwd: launchProcessCwd,
|
|
741
|
+
env: launchSessionEnv,
|
|
742
|
+
detached: true,
|
|
743
|
+
stdio: detachedStdio,
|
|
744
|
+
windowsHide: platform !== 'win32',
|
|
745
|
+
});
|
|
746
|
+
child.unref?.();
|
|
747
|
+
if (Array.isArray(detachedStdio) && typeof detachedStdio[1] === 'number') {
|
|
748
|
+
// The child inherited the descriptor; the parent's copy must not leak.
|
|
749
|
+
try { fs.closeSync(detachedStdio[1]); } catch {}
|
|
750
|
+
}
|
|
751
|
+
if (platform === 'win32') {
|
|
752
|
+
const attempts = Math.max(1, Number(detachedPollAttempts) || 1);
|
|
753
|
+
for (let index = 0; index < attempts; index += 1) {
|
|
754
|
+
await sleep(Math.max(50, Number(detachedPollDelayMs) || 200));
|
|
755
|
+
const processMatch = typeof processInspector?.findProcessForSession === 'function'
|
|
756
|
+
? processInspector.findProcessForSession(target)
|
|
757
|
+
: null;
|
|
758
|
+
const processPid = parsePositiveInt(processMatch?.processId);
|
|
759
|
+
if (processPid && isPidAlive(processPid)) {
|
|
760
|
+
return {
|
|
761
|
+
pid: processPid,
|
|
762
|
+
reused: false,
|
|
763
|
+
launchMode: 'console',
|
|
764
|
+
tmuxSessionName: null,
|
|
765
|
+
child,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
pid: null,
|
|
771
|
+
reused: false,
|
|
772
|
+
launchMode: 'console',
|
|
773
|
+
tmuxSessionName: null,
|
|
774
|
+
child,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const pid = parsePositiveInt(child?.pid);
|
|
778
|
+
if (!pid) throw new Error('worker-spawn-unhealthy:missing-pid');
|
|
779
|
+
return {
|
|
780
|
+
pid,
|
|
781
|
+
reused: false,
|
|
782
|
+
launchMode: 'detached',
|
|
783
|
+
tmuxSessionName: null,
|
|
784
|
+
child,
|
|
785
|
+
};
|
|
786
|
+
}
|