@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,269 @@
|
|
|
1
|
+
// Normalize Claude CLI workflow progress into the bounded digest the relay
|
|
2
|
+
// ships on a background-task row (`workflowProgress`).
|
|
3
|
+
//
|
|
4
|
+
// Two sources feed the digest (verified against CLI 2.1.226):
|
|
5
|
+
//
|
|
6
|
+
// - `<sessionDir>/workflows/<runId>.json` — the run record, written only at
|
|
7
|
+
// workflow COMPLETION. Its `workflowProgress` array mixes
|
|
8
|
+
// `{type:'workflow_phase'}` and `{type:'workflow_agent'}` entries and is the
|
|
9
|
+
// authoritative tree once it exists.
|
|
10
|
+
// - `<sessionDir>/subagents/workflows/<runId>/journal.jsonl` — the live
|
|
11
|
+
// append-only source while the run is in flight: `{type:'started', key,
|
|
12
|
+
// agentId}` then `{type:'result', key, agentId, result}` per agent.
|
|
13
|
+
// started-without-result = running; result = done.
|
|
14
|
+
//
|
|
15
|
+
// The digest shape is a contract shared with the relay sanitizer and the
|
|
16
|
+
// client tree renderer — keep it EXACTLY this:
|
|
17
|
+
//
|
|
18
|
+
// { runId, workflowName, status, agentCount, totalTokens, durationMs,
|
|
19
|
+
// phases: [{index, title}], logs: [string], // last 5, ≤300 chars
|
|
20
|
+
// agents: [{index, label, phaseIndex, phaseTitle, model, state, attempt,
|
|
21
|
+
// lastToolName, tokens, toolCalls, durationMs, startedAt}],
|
|
22
|
+
// agentsOmitted } // agents ≤100, order kept
|
|
23
|
+
//
|
|
24
|
+
// Both file formats are undocumented CLI internals, so every field is
|
|
25
|
+
// presence-checked and clamped; malformed input degrades to omitted fields or
|
|
26
|
+
// a null digest — these functions NEVER throw.
|
|
27
|
+
|
|
28
|
+
const MAX_AGENTS = 100;
|
|
29
|
+
const MAX_PHASES = 50;
|
|
30
|
+
const MAX_LOGS = 5;
|
|
31
|
+
const MAX_LOG_CHARS = 300;
|
|
32
|
+
const MAX_LABEL_CHARS = 160;
|
|
33
|
+
const MAX_TOOL_NAME_CHARS = 160;
|
|
34
|
+
const MAX_TITLE_CHARS = 120;
|
|
35
|
+
const MAX_MODEL_CHARS = 80;
|
|
36
|
+
const MAX_STATE_CHARS = 32;
|
|
37
|
+
const MAX_RUN_ID_CHARS = 64;
|
|
38
|
+
const MAX_WORKFLOW_NAME_CHARS = 120;
|
|
39
|
+
|
|
40
|
+
/** Trimmed string clamped to `max` chars, or null for anything non-string. */
|
|
41
|
+
function clampString(value, max) {
|
|
42
|
+
if (typeof value !== 'string') return null;
|
|
43
|
+
const text = value.trim();
|
|
44
|
+
if (!text) return null;
|
|
45
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A finite number, or null — never NaN/Infinity/strings coerced. */
|
|
49
|
+
function finiteOrNull(value) {
|
|
50
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Digest a completed (or partial) run record. Returns null when `record` is
|
|
55
|
+
* not an object carrying a usable `runId` — the one field every real record
|
|
56
|
+
* has and the digest cannot identify a run without.
|
|
57
|
+
*/
|
|
58
|
+
export function digestFromRunRecord(record) {
|
|
59
|
+
try {
|
|
60
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) return null;
|
|
61
|
+
const runId = clampString(record.runId, MAX_RUN_ID_CHARS);
|
|
62
|
+
if (!runId) return null;
|
|
63
|
+
|
|
64
|
+
const progress = Array.isArray(record.workflowProgress) ? record.workflowProgress : [];
|
|
65
|
+
const phases = [];
|
|
66
|
+
const agents = [];
|
|
67
|
+
let agentTotal = 0;
|
|
68
|
+
for (const entry of progress) {
|
|
69
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
70
|
+
if (entry.type === 'workflow_phase') {
|
|
71
|
+
if (phases.length < MAX_PHASES) {
|
|
72
|
+
phases.push({
|
|
73
|
+
index: finiteOrNull(entry.index) ?? phases.length + 1,
|
|
74
|
+
title: clampString(entry.title, MAX_TITLE_CHARS),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (entry.type !== 'workflow_agent') continue;
|
|
80
|
+
agentTotal += 1;
|
|
81
|
+
if (agents.length >= MAX_AGENTS) continue;
|
|
82
|
+
// promptPreview/resultPreview are dropped deliberately: they are the
|
|
83
|
+
// multi-hundred-char fields that would balloon a 2s-cadence broadcast.
|
|
84
|
+
agents.push({
|
|
85
|
+
index: finiteOrNull(entry.index) ?? agentTotal,
|
|
86
|
+
label: clampString(entry.label, MAX_LABEL_CHARS),
|
|
87
|
+
phaseIndex: finiteOrNull(entry.phaseIndex),
|
|
88
|
+
phaseTitle: clampString(entry.phaseTitle, MAX_TITLE_CHARS),
|
|
89
|
+
model: clampString(entry.model, MAX_MODEL_CHARS),
|
|
90
|
+
state: clampString(entry.state, MAX_STATE_CHARS),
|
|
91
|
+
attempt: finiteOrNull(entry.attempt),
|
|
92
|
+
lastToolName: clampString(entry.lastToolName, MAX_TOOL_NAME_CHARS),
|
|
93
|
+
tokens: finiteOrNull(entry.tokens),
|
|
94
|
+
toolCalls: finiteOrNull(entry.toolCalls),
|
|
95
|
+
durationMs: finiteOrNull(entry.durationMs),
|
|
96
|
+
startedAt: finiteOrNull(entry.startedAt),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The record also carries top-level `phases: [{title}]`; used only when
|
|
101
|
+
// workflowProgress had no phase entries to derive titles from.
|
|
102
|
+
if (!phases.length && Array.isArray(record.phases)) {
|
|
103
|
+
for (const phase of record.phases) {
|
|
104
|
+
if (phases.length >= MAX_PHASES) break;
|
|
105
|
+
if (!phase || typeof phase !== 'object') continue;
|
|
106
|
+
phases.push({ index: phases.length + 1, title: clampString(phase.title, MAX_TITLE_CHARS) });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const logs = (Array.isArray(record.logs) ? record.logs : [])
|
|
111
|
+
.filter((line) => typeof line === 'string' && line.trim())
|
|
112
|
+
.slice(-MAX_LOGS)
|
|
113
|
+
.map((line) => line.slice(0, MAX_LOG_CHARS));
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
runId,
|
|
117
|
+
workflowName: clampString(record.workflowName, MAX_WORKFLOW_NAME_CHARS),
|
|
118
|
+
status: clampString(record.status, MAX_STATE_CHARS),
|
|
119
|
+
agentCount: finiteOrNull(record.agentCount) ?? (agentTotal || null),
|
|
120
|
+
totalTokens: finiteOrNull(record.totalTokens),
|
|
121
|
+
// Top-level run duration: only the completed run record knows it. The
|
|
122
|
+
// finished-task card renders it; renderers ignore it when null.
|
|
123
|
+
durationMs: finiteOrNull(record.durationMs),
|
|
124
|
+
phases,
|
|
125
|
+
logs,
|
|
126
|
+
agents,
|
|
127
|
+
agentsOmitted: Math.max(0, agentTotal - agents.length),
|
|
128
|
+
};
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Shared-prefix stripping only kicks in when the common prefix is long enough
|
|
135
|
+
// to be boilerplate; short shared openers ("review: ") are kept as-is.
|
|
136
|
+
const MIN_COMMON_PREFIX_CHARS = 24;
|
|
137
|
+
// Leftovers a stripped tail may start with (the prefix often ends mid-
|
|
138
|
+
// separator): whitespace, dashes, and common punctuation.
|
|
139
|
+
const LABEL_LEADING_NOISE = /^[\s.,;:!?·…"'()[\]{}<>|/\\`~*_#=–—-]+/;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Strip the longest common prefix shared by the given labels. Live labels are
|
|
143
|
+
* prompt previews, and workflows often prefix every agent's prompt with the
|
|
144
|
+
* same boilerplate — N indistinguishable labels. When ≥2 non-empty string
|
|
145
|
+
* entries share a prefix of ≥24 chars, that prefix is stripped from each and
|
|
146
|
+
* the tail (trimmed of leading whitespace/punctuation, clamped to the label
|
|
147
|
+
* limit) becomes the label. Non-string entries pass through untouched and do
|
|
148
|
+
* not join the prefix computation; a label that was nothing but the shared
|
|
149
|
+
* prefix keeps its original text (an empty label would be strictly worse).
|
|
150
|
+
* Single label or short prefix ⇒ unchanged. Pure and throw-free.
|
|
151
|
+
*/
|
|
152
|
+
export function stripCommonPromptPrefix(labels) {
|
|
153
|
+
try {
|
|
154
|
+
if (!Array.isArray(labels)) return [];
|
|
155
|
+
const strings = labels.filter((label) => typeof label === 'string' && label.length > 0);
|
|
156
|
+
if (strings.length < 2) return labels.slice();
|
|
157
|
+
let prefix = strings[0];
|
|
158
|
+
for (const label of strings) {
|
|
159
|
+
let shared = 0;
|
|
160
|
+
const limit = Math.min(prefix.length, label.length);
|
|
161
|
+
while (shared < limit && prefix[shared] === label[shared]) shared += 1;
|
|
162
|
+
prefix = prefix.slice(0, shared);
|
|
163
|
+
if (!prefix) break;
|
|
164
|
+
}
|
|
165
|
+
if (prefix.length < MIN_COMMON_PREFIX_CHARS) return labels.slice();
|
|
166
|
+
return labels.map((label) => {
|
|
167
|
+
if (typeof label !== 'string' || label.length === 0) return label;
|
|
168
|
+
const tail = label.slice(prefix.length).replace(LABEL_LEADING_NOISE, '').slice(0, MAX_LABEL_CHARS);
|
|
169
|
+
return tail || label;
|
|
170
|
+
});
|
|
171
|
+
} catch {
|
|
172
|
+
return Array.isArray(labels) ? labels.slice() : [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Label lookup tolerant of a Map or a plain object (own keys only). */
|
|
177
|
+
function labelFor(labelsByAgentId, agentId) {
|
|
178
|
+
try {
|
|
179
|
+
if (!labelsByAgentId) return null;
|
|
180
|
+
if (typeof labelsByAgentId.get === 'function') return labelsByAgentId.get(agentId) ?? null;
|
|
181
|
+
if (typeof labelsByAgentId === 'object' && Object.hasOwn(labelsByAgentId, agentId)) {
|
|
182
|
+
return labelsByAgentId[agentId];
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// fall through to null
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Digest the live journal while the run record does not exist yet. `entries`
|
|
192
|
+
* are the parsed journal lines in file order; `labelsByAgentId` (Map or plain
|
|
193
|
+
* object) supplies per-agent labels read from the first line of each
|
|
194
|
+
* `agent-<agentId>.jsonl`, falling back to `agent <n>`. The journal carries no
|
|
195
|
+
* phases, tokens, or timings, so those digest fields are empty/null; `runId`
|
|
196
|
+
* is optional (the caller knows it from the run directory's name).
|
|
197
|
+
* Returns null when no agent can be derived — an empty digest has nothing to
|
|
198
|
+
* render and would only churn the publish path.
|
|
199
|
+
*/
|
|
200
|
+
export function digestFromJournal({ entries, labelsByAgentId, workflowName, runId } = {}) {
|
|
201
|
+
try {
|
|
202
|
+
if (!Array.isArray(entries)) return null;
|
|
203
|
+
const stateByAgentId = new Map();
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
206
|
+
const agentId = clampString(entry.agentId, 256);
|
|
207
|
+
if (!agentId) continue;
|
|
208
|
+
if (entry.type === 'started') {
|
|
209
|
+
if (!stateByAgentId.has(agentId)) stateByAgentId.set(agentId, 'running');
|
|
210
|
+
} else if (entry.type === 'result') {
|
|
211
|
+
// A result without a prior `started` still proves the agent ran.
|
|
212
|
+
stateByAgentId.set(agentId, 'done');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!stateByAgentId.size) return null;
|
|
216
|
+
|
|
217
|
+
// Live labels are prompt previews; when the workflow prefixed every
|
|
218
|
+
// agent's prompt with the same boilerplate, strip the shared prefix so
|
|
219
|
+
// each label shows the distinguishing tail instead of N identical strings.
|
|
220
|
+
// The strip runs on the UNCLAMPED strings — boilerplate longer than the
|
|
221
|
+
// label limit would otherwise clamp every label to the same prefix and
|
|
222
|
+
// leave nothing to distinguish — and the label clamp is applied to the
|
|
223
|
+
// results (stripped tails come back pre-clamped, but the keep-original
|
|
224
|
+
// degenerate paths do not).
|
|
225
|
+
const agentEntries = [...stateByAgentId];
|
|
226
|
+
const labels = stripCommonPromptPrefix(
|
|
227
|
+
agentEntries.map(([agentId]) => {
|
|
228
|
+
const label = labelFor(labelsByAgentId, agentId);
|
|
229
|
+
return typeof label === 'string' ? label.trim() || null : null;
|
|
230
|
+
}),
|
|
231
|
+
).map((label) => clampString(label, MAX_LABEL_CHARS));
|
|
232
|
+
|
|
233
|
+
const agents = [];
|
|
234
|
+
let index = 0;
|
|
235
|
+
for (const [, state] of agentEntries) {
|
|
236
|
+
index += 1;
|
|
237
|
+
if (agents.length >= MAX_AGENTS) continue;
|
|
238
|
+
agents.push({
|
|
239
|
+
index,
|
|
240
|
+
label: (typeof labels[index - 1] === 'string' && labels[index - 1]) || `agent ${index}`,
|
|
241
|
+
phaseIndex: null,
|
|
242
|
+
phaseTitle: null,
|
|
243
|
+
model: null,
|
|
244
|
+
state,
|
|
245
|
+
attempt: null,
|
|
246
|
+
lastToolName: null,
|
|
247
|
+
tokens: null,
|
|
248
|
+
toolCalls: null,
|
|
249
|
+
durationMs: null,
|
|
250
|
+
startedAt: null,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
runId: clampString(runId, MAX_RUN_ID_CHARS),
|
|
256
|
+
workflowName: clampString(workflowName, MAX_WORKFLOW_NAME_CHARS),
|
|
257
|
+
status: 'running',
|
|
258
|
+
agentCount: stateByAgentId.size,
|
|
259
|
+
totalTokens: null,
|
|
260
|
+
durationMs: null,
|
|
261
|
+
phases: [],
|
|
262
|
+
logs: [],
|
|
263
|
+
agents,
|
|
264
|
+
agentsOmitted: Math.max(0, stateByAgentId.size - agents.length),
|
|
265
|
+
};
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { pathToFileURL } from 'url';
|
|
7
|
+
|
|
8
|
+
function dedupePaths(paths) {
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
return paths.filter((value) => {
|
|
11
|
+
const candidate = String(value || '').trim();
|
|
12
|
+
if (!candidate) return false;
|
|
13
|
+
const normalized = path.normalize(candidate);
|
|
14
|
+
if (seen.has(normalized)) return false;
|
|
15
|
+
seen.add(normalized);
|
|
16
|
+
return true;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getCopilotBaseDirs({ env = process.env, platform = process.platform, homeDir = os.homedir() } = {}) {
|
|
21
|
+
const configured = String(env.COPILOT_PKG_DIR || '').trim();
|
|
22
|
+
if (platform === 'win32') {
|
|
23
|
+
return dedupePaths([configured, path.join(env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), 'copilot', 'pkg')]);
|
|
24
|
+
}
|
|
25
|
+
if (platform === 'darwin') {
|
|
26
|
+
return dedupePaths([configured, path.join(homeDir, 'Library', 'Application Support', 'copilot', 'pkg')]);
|
|
27
|
+
}
|
|
28
|
+
return dedupePaths([
|
|
29
|
+
configured,
|
|
30
|
+
path.join(env.XDG_CACHE_HOME || path.join(homeDir, '.cache'), 'copilot', 'pkg'),
|
|
31
|
+
path.join(env.XDG_DATA_HOME || path.join(homeDir, '.local', 'share'), 'copilot', 'pkg'),
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The runtime entry point inside a Copilot CLI version directory.
|
|
37
|
+
*
|
|
38
|
+
* It is `app.js` and never the sibling `index.js`: `index.js` is a
|
|
39
|
+
* self-updating launcher that resolves the newest version in the cache and
|
|
40
|
+
* `exec`s THAT version's `app.js`. Spawning it silently defeats pinning the
|
|
41
|
+
* runtime to the SDK bundle's own version, which matters because the event
|
|
42
|
+
* shapes the workers normalize are per-version. Exported so every caller pins
|
|
43
|
+
* the same file.
|
|
44
|
+
*/
|
|
45
|
+
export function copilotRuntimeEntry(versionDir) {
|
|
46
|
+
return path.join(versionDir, 'app.js');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveInstalledCopilotPaths({ config = {}, env = process.env, platform = process.platform, arch = process.arch } = {}) {
|
|
50
|
+
if (config.sdkPath && config.cliPath) {
|
|
51
|
+
return { sdkPath: path.resolve(config.sdkPath), cliPath: path.resolve(config.cliPath), version: 'configured' };
|
|
52
|
+
}
|
|
53
|
+
const requestedVersion = String(config.sdkVersion || '').trim();
|
|
54
|
+
const subdirs = platform === 'universal' ? ['universal'] : [`${platform}-${arch}`, 'universal'];
|
|
55
|
+
const candidates = [];
|
|
56
|
+
for (const [baseDirIdx, baseDir] of getCopilotBaseDirs({ env, platform }).entries()) {
|
|
57
|
+
for (const [subdirIdx, subdir] of subdirs.entries()) {
|
|
58
|
+
let entries = [];
|
|
59
|
+
try { entries = fs.readdirSync(path.join(baseDir, subdir)); } catch { continue; }
|
|
60
|
+
for (const version of entries) {
|
|
61
|
+
if (!/^\d+\.\d+\.\d+$/.test(version) || (requestedVersion && version !== requestedVersion)) continue;
|
|
62
|
+
const versionDir = path.join(baseDir, subdir, version);
|
|
63
|
+
const sdkPath = path.join(versionDir, 'copilot-sdk', 'index.js');
|
|
64
|
+
const cliPath = copilotRuntimeEntry(versionDir);
|
|
65
|
+
if (fs.existsSync(sdkPath) && fs.existsSync(cliPath)) {
|
|
66
|
+
candidates.push({ version, baseDirIdx, subdirIdx, sdkPath, cliPath });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
candidates.sort((a, b) => {
|
|
72
|
+
const aVersion = a.version.split('.').map(Number);
|
|
73
|
+
const bVersion = b.version.split('.').map(Number);
|
|
74
|
+
return bVersion[0] - aVersion[0] || bVersion[1] - aVersion[1] || bVersion[2] - aVersion[2]
|
|
75
|
+
|| a.baseDirIdx - b.baseDirIdx || a.subdirIdx - b.subdirIdx;
|
|
76
|
+
});
|
|
77
|
+
if (candidates.length) return candidates[0];
|
|
78
|
+
const scope = requestedVersion ? `version ${requestedVersion}` : 'a compatible version';
|
|
79
|
+
throw new Error(`Copilot SDK runtime not found (${scope}). Install Copilot CLI or configure sdkPath and cliPath.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildInstalledCopilotClientOptions({
|
|
83
|
+
cliPath,
|
|
84
|
+
cwd,
|
|
85
|
+
baseDirectory,
|
|
86
|
+
logLevel = 'debug',
|
|
87
|
+
} = {}) {
|
|
88
|
+
return {
|
|
89
|
+
connection: {
|
|
90
|
+
kind: 'stdio',
|
|
91
|
+
path: cliPath,
|
|
92
|
+
},
|
|
93
|
+
mode: 'empty',
|
|
94
|
+
baseDirectory,
|
|
95
|
+
useLoggedInUser: true,
|
|
96
|
+
logLevel,
|
|
97
|
+
workingDirectory: cwd,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function createInstalledCopilotClient({
|
|
102
|
+
config = {},
|
|
103
|
+
cwd,
|
|
104
|
+
baseDirectory,
|
|
105
|
+
logLevel = 'debug',
|
|
106
|
+
} = {}) {
|
|
107
|
+
const paths = resolveInstalledCopilotPaths({ config });
|
|
108
|
+
const sdk = await import(pathToFileURL(paths.sdkPath).href);
|
|
109
|
+
if (typeof sdk.CopilotClient !== 'function') throw new Error('CopilotClient not found in installed Copilot SDK exports');
|
|
110
|
+
const client = new sdk.CopilotClient(buildInstalledCopilotClientOptions({
|
|
111
|
+
cliPath: paths.cliPath,
|
|
112
|
+
cwd,
|
|
113
|
+
baseDirectory,
|
|
114
|
+
logLevel,
|
|
115
|
+
}));
|
|
116
|
+
await client.start();
|
|
117
|
+
return {
|
|
118
|
+
client,
|
|
119
|
+
approveAll: sdk.approveAll,
|
|
120
|
+
paths,
|
|
121
|
+
async dispose() {
|
|
122
|
+
await client.stop?.();
|
|
123
|
+
await client.dispose?.();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
// Very large base64 payloads risk rejection by the runtime; larger images fall
|
|
4
|
+
// back to a file reference so the agent can read them from disk instead. Same
|
|
5
|
+
// threshold the Claude and Cursor workers use.
|
|
6
|
+
export const MAX_INLINE_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
function isImageAttachment(att) {
|
|
9
|
+
return String(att?.type || '').toLowerCase().startsWith('image/');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function imageFromDataUrl(att) {
|
|
13
|
+
const dataUrl = String(att?.dataUrl || '').trim();
|
|
14
|
+
if (!dataUrl.startsWith('data:')) return null;
|
|
15
|
+
const match = dataUrl.match(/^data:([^;,]+);base64,(.+)$/i);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
const mimeType = String(match[1] || '').trim().toLowerCase();
|
|
18
|
+
const data = String(match[2] || '').trim();
|
|
19
|
+
if (!mimeType.startsWith('image/') || !data) return null;
|
|
20
|
+
return { data, mimeType };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the `MessageOptions` payload for `session.send()` from a relay
|
|
25
|
+
* message.
|
|
26
|
+
*
|
|
27
|
+
* Mirrors `buildCursorUserMessage` / `buildClaudeUserContent`: images small
|
|
28
|
+
* enough to embed travel inline, everything else becomes a reference the
|
|
29
|
+
* agent's file tools can open, and the server-provided
|
|
30
|
+
* `attachmentPromptContext` is appended to the prompt. What differs is the
|
|
31
|
+
* carrier — the Copilot SDK's `MessageOptions.attachments` is a typed union
|
|
32
|
+
* rather than content blocks:
|
|
33
|
+
*
|
|
34
|
+
* image, embeddable → `{ type: 'blob', data, mimeType, displayName }`
|
|
35
|
+
* image, too large → `{ type: 'file', path, displayName }`
|
|
36
|
+
* any other file → `{ type: 'file', path, displayName }`
|
|
37
|
+
*
|
|
38
|
+
* The note lines are kept even though the attachments are structured, because
|
|
39
|
+
* they are what tells the model the absolute path of a file it may want to
|
|
40
|
+
* re-read with its own tools — the same reason the siblings emit them.
|
|
41
|
+
*
|
|
42
|
+
* Text composition order (identical to the siblings): user text, note lines,
|
|
43
|
+
* `attachmentPromptContext`, joined by blank lines.
|
|
44
|
+
*/
|
|
45
|
+
export function buildCopilotMessageOptions(message, {
|
|
46
|
+
fsImpl = fs,
|
|
47
|
+
maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES,
|
|
48
|
+
} = {}) {
|
|
49
|
+
const text = String(message?.text || '').trim();
|
|
50
|
+
const input = Array.isArray(message?.attachments) ? message.attachments : [];
|
|
51
|
+
const attachments = [];
|
|
52
|
+
const noteLines = [];
|
|
53
|
+
|
|
54
|
+
for (const att of input) {
|
|
55
|
+
if (!att || typeof att !== 'object') continue;
|
|
56
|
+
const name = String(att.name || 'attachment').trim() || 'attachment';
|
|
57
|
+
const mime = String(att.type || 'application/octet-stream').trim() || 'application/octet-stream';
|
|
58
|
+
const filePath = String(att.path || '').trim();
|
|
59
|
+
|
|
60
|
+
if (isImageAttachment(att)) {
|
|
61
|
+
let blob = null;
|
|
62
|
+
if (filePath && fsImpl.existsSync(filePath)) {
|
|
63
|
+
try {
|
|
64
|
+
const bytes = fsImpl.readFileSync(filePath);
|
|
65
|
+
if (Buffer.isBuffer(bytes) && bytes.length && bytes.length <= maxInlineImageBytes) {
|
|
66
|
+
blob = {
|
|
67
|
+
data: bytes.toString('base64'),
|
|
68
|
+
mimeType: mime.toLowerCase().startsWith('image/') ? mime.toLowerCase() : 'image/png',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
blob = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!blob) blob = imageFromDataUrl(att);
|
|
76
|
+
if (blob) {
|
|
77
|
+
attachments.push({ type: 'blob', data: blob.data, mimeType: blob.mimeType, displayName: name });
|
|
78
|
+
noteLines.push(`Attached image "${name}" (${mime}) is embedded in this message.`);
|
|
79
|
+
} else if (filePath) {
|
|
80
|
+
attachments.push({ type: 'file', path: filePath, displayName: name });
|
|
81
|
+
noteLines.push(`Attached image "${name}" (${mime}): ${filePath}`);
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (filePath && fsImpl.existsSync(filePath)) {
|
|
87
|
+
attachments.push({ type: 'file', path: filePath, displayName: name });
|
|
88
|
+
noteLines.push(`Attached file "${name}" (${mime}): ${filePath}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const attachmentPromptContext = String(message?.attachmentPromptContext || '').trim();
|
|
93
|
+
const textParts = [text];
|
|
94
|
+
if (noteLines.length) textParts.push(noteLines.join('\n'));
|
|
95
|
+
if (attachmentPromptContext) textParts.push(attachmentPromptContext);
|
|
96
|
+
return { prompt: textParts.filter(Boolean).join('\n\n'), attachments };
|
|
97
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// BYOK (bring-your-own-key) provider config for the SDK worker.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists at all: on the EXTENSION path the `COPILOT_PROVIDER_*`
|
|
4
|
+
// environment variables are read by the `copilot` CLI's own startup layer,
|
|
5
|
+
// which turns them into the process-wide custom provider. Sessions this worker
|
|
6
|
+
// creates through the SDK never go through that layer — a live probe confirmed
|
|
7
|
+
// the runtime IGNORES those variables for SDK-created sessions — so the same
|
|
8
|
+
// configuration has to be re-expressed in-process as `SessionConfig.provider`
|
|
9
|
+
// (`ProviderConfig`, verified against the bundled runtime 1.0.82 types).
|
|
10
|
+
//
|
|
11
|
+
// The variable names are deliberately NOT re-invented here: they are exactly
|
|
12
|
+
// the ones `applyOpenAIProviderEnvironment()`
|
|
13
|
+
// (server/services/session-worker-launch-service.mjs) writes today, so one
|
|
14
|
+
// relay-side OpenAI configuration drives both engines and the existing
|
|
15
|
+
// secret-env-file plumbing (`WORKER_SECRET_ENV_VARS` already carries
|
|
16
|
+
// `COPILOT_PROVIDER_API_KEY`) keeps working untouched.
|
|
17
|
+
//
|
|
18
|
+
// Note on cost: BYOK usage events report `cost: 0` (the field is the premium
|
|
19
|
+
// multiplier, not money), so nothing here feeds a spend display.
|
|
20
|
+
|
|
21
|
+
import { resolveModelTokenCeilings } from '../../shared/context-window-fallbacks.mjs';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
|
|
24
|
+
|
|
25
|
+
function text(value) {
|
|
26
|
+
return String(value ?? '').trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A token ceiling override read from the environment.
|
|
31
|
+
*
|
|
32
|
+
* `parseInt` is deliberately NOT used: it truncates at the first character it
|
|
33
|
+
* cannot read, so `1e6` would silently become **1** and `90000.5` would become
|
|
34
|
+
* 90000 — an escape hatch that quietly does the wrong thing is worse than one
|
|
35
|
+
* that refuses. An unusable value is logged and IGNORED, falling through to the
|
|
36
|
+
* shared table and then to the runtime's own default, rather than being
|
|
37
|
+
* truncated into a number nobody asked for.
|
|
38
|
+
*/
|
|
39
|
+
function tokenCeilingOverride(name, raw, dbg) {
|
|
40
|
+
const value = text(raw);
|
|
41
|
+
if (!value) return null;
|
|
42
|
+
const numeric = Number(value);
|
|
43
|
+
if (!Number.isInteger(numeric) || numeric <= 0) {
|
|
44
|
+
dbg(`ignoring ${name}: expected a positive whole number of tokens, got ${JSON.stringify(value)}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return numeric;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prompt/output token ceilings for the model this session will call.
|
|
52
|
+
*
|
|
53
|
+
* The runtime resolves these from its own model catalog and falls back to a
|
|
54
|
+
* synthesized 128k context for a model it does not recognise, which is wrong in
|
|
55
|
+
* the expensive direction for a large-context model: a long relay conversation
|
|
56
|
+
* would lose history it never needed to lose.
|
|
57
|
+
*
|
|
58
|
+
* The numbers themselves live in `shared/context-window-fallbacks.mjs` — the
|
|
59
|
+
* same module the Copilot snapshot service and the Cursor worker read — so this
|
|
60
|
+
* worker cannot disagree with the rest of the relay about how big a model's
|
|
61
|
+
* window is. The prompt ceiling is derived there as `window − output`.
|
|
62
|
+
*
|
|
63
|
+
* An unknown model is left UNSET on purpose: the runtime's own default is a
|
|
64
|
+
* safe answer, whereas a guessed ceiling that is too HIGH turns early
|
|
65
|
+
* compaction into hard API errors.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveOpenAiModelTokenLimits(model) {
|
|
68
|
+
const { maxPromptTokens = null, maxOutputTokens = null } = resolveModelTokenCeilings(model);
|
|
69
|
+
if (maxPromptTokens === null || maxOutputTokens === null) return {};
|
|
70
|
+
return { maxPromptTokens, maxOutputTokens };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build `SessionConfig.provider` from the worker's environment, or null when
|
|
75
|
+
* this session is not a BYOK session.
|
|
76
|
+
*
|
|
77
|
+
* The signal is the same one the extension path uses: the launch service only
|
|
78
|
+
* writes `COPILOT_PROVIDER_TYPE` for a session whose relay provider is
|
|
79
|
+
* `openai`, and deletes the whole family otherwise. An API key is required
|
|
80
|
+
* because a provider config without credentials would silently fail every
|
|
81
|
+
* model call instead of failing to start.
|
|
82
|
+
*
|
|
83
|
+
* `modelId` is deliberately left UNSET. `ProviderConfig.modelId` falls back to
|
|
84
|
+
* `SessionConfig.model`, so leaving it out is what keeps `session.setModel()`
|
|
85
|
+
* authoritative for the rest of the session; pinning it here would freeze the
|
|
86
|
+
* session on the model it was created with and make every later switch a
|
|
87
|
+
* silent no-op.
|
|
88
|
+
*
|
|
89
|
+
* The ceilings are resolved against the model passed in, so a caller that
|
|
90
|
+
* switches models must rebuild this config — `SessionConfig.provider` is fixed
|
|
91
|
+
* at session creation and runtime 1.0.82 exposes no way to update it
|
|
92
|
+
* mid-session (see `copilot-sdk-session-process.mjs`).
|
|
93
|
+
*/
|
|
94
|
+
export function resolveCopilotProviderConfig({ env = process.env, model = '', dbg = () => {} } = {}) {
|
|
95
|
+
const type = text(env.COPILOT_PROVIDER_TYPE).toLowerCase();
|
|
96
|
+
if (type !== 'openai') return null;
|
|
97
|
+
const apiKey = text(env.COPILOT_PROVIDER_API_KEY);
|
|
98
|
+
if (!apiKey) return null;
|
|
99
|
+
// The session's live model wins over the configured default: the token
|
|
100
|
+
// ceilings below have to describe the model that will actually be called.
|
|
101
|
+
const effectiveModel = text(model) || text(env.COPILOT_MODEL);
|
|
102
|
+
const wireApi = text(env.COPILOT_PROVIDER_WIRE_API).toLowerCase();
|
|
103
|
+
const limits = resolveOpenAiModelTokenLimits(effectiveModel);
|
|
104
|
+
const maxPromptTokens = tokenCeilingOverride(
|
|
105
|
+
'COPILOT_PROVIDER_MAX_PROMPT_TOKENS', env.COPILOT_PROVIDER_MAX_PROMPT_TOKENS, dbg,
|
|
106
|
+
) ?? limits.maxPromptTokens ?? null;
|
|
107
|
+
const maxOutputTokens = tokenCeilingOverride(
|
|
108
|
+
'COPILOT_PROVIDER_MAX_OUTPUT_TOKENS', env.COPILOT_PROVIDER_MAX_OUTPUT_TOKENS, dbg,
|
|
109
|
+
) ?? limits.maxOutputTokens ?? null;
|
|
110
|
+
return {
|
|
111
|
+
type: 'openai',
|
|
112
|
+
baseUrl: text(env.COPILOT_PROVIDER_BASE_URL) || DEFAULT_BASE_URL,
|
|
113
|
+
apiKey,
|
|
114
|
+
// Only the two documented values are forwarded; anything else is a typo in
|
|
115
|
+
// the relay config and the runtime's own default ("completions") is a
|
|
116
|
+
// better answer than a rejected session config.
|
|
117
|
+
...(wireApi === 'responses' || wireApi === 'completions' ? { wireApi } : {}),
|
|
118
|
+
...(maxPromptTokens ? { maxPromptTokens } : {}),
|
|
119
|
+
...(maxOutputTokens ? { maxOutputTokens } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|