@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,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
export function safeUsageNumber(value) {
|
|
4
|
+
if (value === null || value === undefined || value === '') return null;
|
|
5
|
+
const numeric = Number(value);
|
|
6
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function computeUsedPercent(remaining, entitlement, percentRemaining) {
|
|
10
|
+
const explicitRemainingPct = safeUsageNumber(percentRemaining);
|
|
11
|
+
if (explicitRemainingPct != null) return Math.max(0, Math.min(100, 100 - explicitRemainingPct));
|
|
12
|
+
const rem = safeUsageNumber(remaining);
|
|
13
|
+
const ent = safeUsageNumber(entitlement);
|
|
14
|
+
if (rem == null || ent == null || ent <= 0) return null;
|
|
15
|
+
return Math.max(0, Math.min(100, ((ent - rem) / ent) * 100));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function computePercentRemaining(remaining, entitlement, percentRemaining) {
|
|
19
|
+
const rem = safeUsageNumber(remaining);
|
|
20
|
+
const ent = safeUsageNumber(entitlement);
|
|
21
|
+
const explicit = safeUsageNumber(percentRemaining);
|
|
22
|
+
if (explicit != null) {
|
|
23
|
+
// Some API payloads report 0 with no denominator context; treat that as unknown.
|
|
24
|
+
const hasDenominatorContext = (ent != null && ent > 0) || rem != null;
|
|
25
|
+
if (!hasDenominatorContext && explicit === 0) return null;
|
|
26
|
+
return Math.max(0, Math.min(100, explicit));
|
|
27
|
+
}
|
|
28
|
+
if (rem == null || ent == null || ent <= 0) return null;
|
|
29
|
+
return Math.max(0, Math.min(100, (rem / ent) * 100));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function computeDeltaMonthlyPercent(currentBucket = {}, previousBucket = {}) {
|
|
33
|
+
const currentUsed = safeUsageNumber(currentBucket.usedPercent);
|
|
34
|
+
const previousUsed = safeUsageNumber(previousBucket.usedPercent);
|
|
35
|
+
if (currentUsed != null && previousUsed != null) {
|
|
36
|
+
const delta = currentUsed - previousUsed;
|
|
37
|
+
return Number.isFinite(delta) ? Math.max(0, delta) : null;
|
|
38
|
+
}
|
|
39
|
+
const deltaUsed = safeUsageNumber(currentBucket.deltaUsed);
|
|
40
|
+
const entitlement = safeUsageNumber(currentBucket.entitlement);
|
|
41
|
+
if (deltaUsed != null && entitlement != null && entitlement > 0) {
|
|
42
|
+
return Math.max(0, (deltaUsed / entitlement) * 100);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeUsageBucket(bucket = {}, previousBucket = {}) {
|
|
48
|
+
const remaining = safeUsageNumber(bucket?.remaining);
|
|
49
|
+
const entitlement = safeUsageNumber(bucket?.entitlement);
|
|
50
|
+
const usedPercent = computeUsedPercent(remaining, entitlement, bucket?.percentRemaining);
|
|
51
|
+
const percentRemaining = computePercentRemaining(remaining, entitlement, bucket?.percentRemaining);
|
|
52
|
+
const previousRemaining = safeUsageNumber(previousBucket?.remaining);
|
|
53
|
+
const deltaUsed = (remaining != null && previousRemaining != null) ? (previousRemaining - remaining) : null;
|
|
54
|
+
return { remaining, entitlement, usedPercent, percentRemaining, deltaUsed };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function usageSnapshotFromSummary(summary = {}, previousSnapshot = null, {
|
|
58
|
+
source = 'live',
|
|
59
|
+
stale = false,
|
|
60
|
+
capturedAt = new Date().toISOString(),
|
|
61
|
+
} = {}) {
|
|
62
|
+
const previous = previousSnapshot || {};
|
|
63
|
+
const premiumBase = normalizeUsageBucket(summary?.premiumInteractions || {}, previous.premium || {});
|
|
64
|
+
const chat = normalizeUsageBucket(summary?.chat || {}, previous.chat || {});
|
|
65
|
+
const planBase = normalizeUsageBucket(summary?.planQuota || {}, previous.plan || {});
|
|
66
|
+
const premium = {
|
|
67
|
+
...premiumBase,
|
|
68
|
+
deltaCredits: premiumBase.deltaUsed,
|
|
69
|
+
};
|
|
70
|
+
const plan = {
|
|
71
|
+
...planBase,
|
|
72
|
+
deltaMonthlyPercent: computeDeltaMonthlyPercent(planBase, previous.plan || {}),
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
source,
|
|
76
|
+
stale,
|
|
77
|
+
capturedAt,
|
|
78
|
+
premium,
|
|
79
|
+
chat,
|
|
80
|
+
plan,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function usageSnapshotFromRow(row = null) {
|
|
85
|
+
if (!row) return null;
|
|
86
|
+
const premiumRemaining = safeUsageNumber(row.premium_remaining);
|
|
87
|
+
const premiumEntitlement = safeUsageNumber(row.premium_entitlement);
|
|
88
|
+
const chatRemaining = safeUsageNumber(row.chat_remaining);
|
|
89
|
+
const chatEntitlement = safeUsageNumber(row.chat_entitlement);
|
|
90
|
+
const planRemaining = safeUsageNumber(row.plan_remaining);
|
|
91
|
+
const planEntitlement = safeUsageNumber(row.plan_entitlement);
|
|
92
|
+
return {
|
|
93
|
+
premium: {
|
|
94
|
+
remaining: premiumRemaining,
|
|
95
|
+
entitlement: premiumEntitlement,
|
|
96
|
+
usedPercent: safeUsageNumber(row.premium_used_percent),
|
|
97
|
+
percentRemaining: computePercentRemaining(premiumRemaining, premiumEntitlement, null),
|
|
98
|
+
},
|
|
99
|
+
chat: {
|
|
100
|
+
remaining: chatRemaining,
|
|
101
|
+
entitlement: chatEntitlement,
|
|
102
|
+
usedPercent: safeUsageNumber(row.chat_used_percent),
|
|
103
|
+
percentRemaining: computePercentRemaining(chatRemaining, chatEntitlement, null),
|
|
104
|
+
},
|
|
105
|
+
plan: {
|
|
106
|
+
remaining: planRemaining,
|
|
107
|
+
entitlement: planEntitlement,
|
|
108
|
+
usedPercent: safeUsageNumber(row.plan_used_percent),
|
|
109
|
+
percentRemaining: computePercentRemaining(planRemaining, planEntitlement, null),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function staleUsageSnapshotFromRow(row = null, {
|
|
115
|
+
source = 'stale-cache',
|
|
116
|
+
capturedAt = new Date().toISOString(),
|
|
117
|
+
} = {}) {
|
|
118
|
+
const previous = usageSnapshotFromRow(row);
|
|
119
|
+
if (!previous) return null;
|
|
120
|
+
return {
|
|
121
|
+
source,
|
|
122
|
+
stale: true,
|
|
123
|
+
capturedAt,
|
|
124
|
+
premium: {
|
|
125
|
+
remaining: previous.premium.remaining,
|
|
126
|
+
entitlement: previous.premium.entitlement,
|
|
127
|
+
usedPercent: computeUsedPercent(previous.premium.remaining, previous.premium.entitlement, null),
|
|
128
|
+
percentRemaining: computePercentRemaining(previous.premium.remaining, previous.premium.entitlement, null),
|
|
129
|
+
deltaUsed: null,
|
|
130
|
+
deltaCredits: null,
|
|
131
|
+
},
|
|
132
|
+
chat: {
|
|
133
|
+
remaining: previous.chat.remaining,
|
|
134
|
+
entitlement: previous.chat.entitlement,
|
|
135
|
+
usedPercent: computeUsedPercent(previous.chat.remaining, previous.chat.entitlement, null),
|
|
136
|
+
percentRemaining: computePercentRemaining(previous.chat.remaining, previous.chat.entitlement, null),
|
|
137
|
+
deltaUsed: null,
|
|
138
|
+
},
|
|
139
|
+
plan: {
|
|
140
|
+
remaining: previous.plan.remaining,
|
|
141
|
+
entitlement: previous.plan.entitlement,
|
|
142
|
+
usedPercent: computeUsedPercent(previous.plan.remaining, previous.plan.entitlement, null),
|
|
143
|
+
percentRemaining: computePercentRemaining(previous.plan.remaining, previous.plan.entitlement, null),
|
|
144
|
+
deltaUsed: null,
|
|
145
|
+
deltaMonthlyPercent: null,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function mapUsageSnapshotRow(row) {
|
|
151
|
+
if (!row) return null;
|
|
152
|
+
const premiumRemaining = safeUsageMetric(row.premium_remaining);
|
|
153
|
+
const premiumEntitlement = safeUsageMetric(row.premium_entitlement);
|
|
154
|
+
const planRemaining = safeUsageMetric(row.plan_remaining);
|
|
155
|
+
const planEntitlement = safeUsageMetric(row.plan_entitlement);
|
|
156
|
+
const planDeltaUsed = safeUsageMetric(row.plan_delta_used);
|
|
157
|
+
const premiumDeltaUsed = safeUsageMetric(row.premium_delta_used);
|
|
158
|
+
const planDeltaMonthlyPercent = planDeltaUsed != null && planEntitlement != null && planEntitlement > 0
|
|
159
|
+
? Math.max(0, (planDeltaUsed / planEntitlement) * 100)
|
|
160
|
+
: null;
|
|
161
|
+
return {
|
|
162
|
+
source: row.source || 'live',
|
|
163
|
+
stale: !!row.stale,
|
|
164
|
+
capturedAt: row.captured_at || null,
|
|
165
|
+
premium: {
|
|
166
|
+
remaining: premiumRemaining,
|
|
167
|
+
entitlement: premiumEntitlement,
|
|
168
|
+
usedPercent: safeUsageMetric(row.premium_used_percent),
|
|
169
|
+
percentRemaining: computePercentRemaining(premiumRemaining, premiumEntitlement, null),
|
|
170
|
+
deltaUsed: premiumDeltaUsed,
|
|
171
|
+
deltaCredits: premiumDeltaUsed,
|
|
172
|
+
},
|
|
173
|
+
chat: {
|
|
174
|
+
remaining: safeUsageMetric(row.chat_remaining),
|
|
175
|
+
entitlement: safeUsageMetric(row.chat_entitlement),
|
|
176
|
+
usedPercent: safeUsageMetric(row.chat_used_percent),
|
|
177
|
+
percentRemaining: computePercentRemaining(
|
|
178
|
+
safeUsageMetric(row.chat_remaining),
|
|
179
|
+
safeUsageMetric(row.chat_entitlement),
|
|
180
|
+
null,
|
|
181
|
+
),
|
|
182
|
+
deltaUsed: safeUsageMetric(row.chat_delta_used),
|
|
183
|
+
},
|
|
184
|
+
plan: {
|
|
185
|
+
remaining: planRemaining,
|
|
186
|
+
entitlement: planEntitlement,
|
|
187
|
+
usedPercent: safeUsageMetric(row.plan_used_percent),
|
|
188
|
+
percentRemaining: computePercentRemaining(planRemaining, planEntitlement, null),
|
|
189
|
+
deltaUsed: planDeltaUsed,
|
|
190
|
+
deltaMonthlyPercent: planDeltaMonthlyPercent,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function safeUsageMetric(value) {
|
|
196
|
+
return safeUsageNumber(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function fetchUsageSummaryPromise(fetchUsageSummary) {
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
fetchUsageSummary((error, summary) => {
|
|
202
|
+
if (error) {
|
|
203
|
+
reject(error);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
resolve(summary || {});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
|
|
5
|
+
export const WINDOWS_AUTOSTART_FILENAME = 'copilot-remote-web-relay.cmd';
|
|
6
|
+
|
|
7
|
+
function escapeBatchValue(value) {
|
|
8
|
+
return String(value || '').replaceAll('%', '%%');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveWindowsStartupDirectory(env = process.env, pathImpl = path) {
|
|
12
|
+
const appData = String(env?.APPDATA || '').trim();
|
|
13
|
+
if (!appData) {
|
|
14
|
+
throw new Error('Windows autostart requires the current user APPDATA directory.');
|
|
15
|
+
}
|
|
16
|
+
return pathImpl.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildWindowsAutostartScript({
|
|
20
|
+
packageRoot,
|
|
21
|
+
nodePath = process.execPath,
|
|
22
|
+
configPath = '',
|
|
23
|
+
pathImpl = path,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const resolvedPackageRoot = pathImpl.resolve(String(packageRoot || ''));
|
|
26
|
+
if (!String(packageRoot || '').trim()) {
|
|
27
|
+
throw new Error('Windows autostart requires the copilot-remote package root.');
|
|
28
|
+
}
|
|
29
|
+
const resolvedNodePath = pathImpl.resolve(String(nodePath || ''));
|
|
30
|
+
if (!String(nodePath || '').trim()) {
|
|
31
|
+
throw new Error('Windows autostart requires the Node.js executable path.');
|
|
32
|
+
}
|
|
33
|
+
const serverPath = pathImpl.join(resolvedPackageRoot, 'server', 'server.js');
|
|
34
|
+
const lines = [
|
|
35
|
+
'@echo off',
|
|
36
|
+
'title Copilot Remote Web Relay',
|
|
37
|
+
];
|
|
38
|
+
if (String(configPath || '').trim()) {
|
|
39
|
+
lines.push(`set "COPILOT_WEB_RELAY_CONFIG=${escapeBatchValue(pathImpl.resolve(configPath))}"`);
|
|
40
|
+
}
|
|
41
|
+
lines.push(
|
|
42
|
+
`cd /d "${escapeBatchValue(resolvedPackageRoot)}"`,
|
|
43
|
+
`"${escapeBatchValue(resolvedNodePath)}" "${escapeBatchValue(serverPath)}"`,
|
|
44
|
+
'',
|
|
45
|
+
);
|
|
46
|
+
return lines.join('\r\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createWindowsAutostartService({
|
|
50
|
+
platform = process.platform,
|
|
51
|
+
env = process.env,
|
|
52
|
+
packageRoot,
|
|
53
|
+
nodePath = process.execPath,
|
|
54
|
+
configPath = '',
|
|
55
|
+
fsImpl = fs,
|
|
56
|
+
pathImpl = path,
|
|
57
|
+
} = {}) {
|
|
58
|
+
const supported = platform === 'win32';
|
|
59
|
+
|
|
60
|
+
function entryPath() {
|
|
61
|
+
return pathImpl.join(
|
|
62
|
+
resolveWindowsStartupDirectory(env, pathImpl),
|
|
63
|
+
WINDOWS_AUTOSTART_FILENAME,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getState() {
|
|
68
|
+
return {
|
|
69
|
+
supported,
|
|
70
|
+
enabled: supported ? fsImpl.existsSync(entryPath()) : false,
|
|
71
|
+
platform,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setEnabled(enabled) {
|
|
76
|
+
if (!supported) {
|
|
77
|
+
throw new Error('Windows autostart is only available on Windows.');
|
|
78
|
+
}
|
|
79
|
+
if (typeof enabled !== 'boolean') {
|
|
80
|
+
throw new TypeError('Windows autostart enabled state must be a boolean.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const targetPath = entryPath();
|
|
84
|
+
const existed = fsImpl.existsSync(targetPath);
|
|
85
|
+
if (!enabled) {
|
|
86
|
+
if (existed) fsImpl.unlinkSync(targetPath);
|
|
87
|
+
return { ...getState(), changed: existed };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const script = buildWindowsAutostartScript({
|
|
91
|
+
packageRoot,
|
|
92
|
+
nodePath,
|
|
93
|
+
configPath,
|
|
94
|
+
pathImpl,
|
|
95
|
+
});
|
|
96
|
+
if (existed && fsImpl.readFileSync(targetPath, 'utf8') === script) {
|
|
97
|
+
return { ...getState(), changed: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fsImpl.mkdirSync(pathImpl.dirname(targetPath), { recursive: true });
|
|
101
|
+
const temporaryPath = `${targetPath}.${randomUUID()}.tmp`;
|
|
102
|
+
try {
|
|
103
|
+
fsImpl.writeFileSync(temporaryPath, script, { encoding: 'utf8', flag: 'wx' });
|
|
104
|
+
fsImpl.renameSync(temporaryPath, targetPath);
|
|
105
|
+
} finally {
|
|
106
|
+
if (fsImpl.existsSync(temporaryPath)) fsImpl.unlinkSync(temporaryPath);
|
|
107
|
+
}
|
|
108
|
+
return { ...getState(), changed: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
getState,
|
|
113
|
+
setEnabled,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function resolveDefaultSessionWorkspaceRootState({
|
|
2
|
+
storedPath = '',
|
|
3
|
+
normalizePath = null,
|
|
4
|
+
} = {}) {
|
|
5
|
+
const stored = String(storedPath || '').trim();
|
|
6
|
+
if (!stored) return { path: null, warning: null };
|
|
7
|
+
const normalized = typeof normalizePath === 'function' ? normalizePath(stored) : stored;
|
|
8
|
+
const normalizedText = String(normalized || '').trim();
|
|
9
|
+
if (normalizedText) {
|
|
10
|
+
return {
|
|
11
|
+
path: normalizedText,
|
|
12
|
+
warning: null,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
path: null,
|
|
17
|
+
warning: 'Saved default CWD is unavailable. Update it in Settings.',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveLaunchWorkspaceRootPath({
|
|
22
|
+
configuredWorkspaceRootPath = '',
|
|
23
|
+
pendingSessionWorkspaceRootPath = '',
|
|
24
|
+
defaultSessionWorkspaceRootPath = '',
|
|
25
|
+
workspaceRootPath = '',
|
|
26
|
+
} = {}) {
|
|
27
|
+
const configured = String(configuredWorkspaceRootPath || '').trim();
|
|
28
|
+
if (configured) return configured;
|
|
29
|
+
const pending = String(pendingSessionWorkspaceRootPath || '').trim();
|
|
30
|
+
if (pending) return pending;
|
|
31
|
+
const configuredDefault = String(defaultSessionWorkspaceRootPath || '').trim();
|
|
32
|
+
if (configuredDefault) return configuredDefault;
|
|
33
|
+
return String(workspaceRootPath || '').trim();
|
|
34
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Workspace-root path policy
|
|
6
|
+
//
|
|
7
|
+
// The canonical home for CWD path normalization and for the *strict* validation
|
|
8
|
+
// applied to the REST endpoints that let a client choose a session's launch
|
|
9
|
+
// directory. Deliberately separate from server-runtime's lenient
|
|
10
|
+
// normalizeWorkspaceRootPath(), which must keep accepting whatever a CLI
|
|
11
|
+
// reports about itself.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
// Characters rejected outright. Mirrors parseCdCommandTarget in workspace-root.mjs
|
|
15
|
+
// so the REST endpoints are no laxer than the chat `cd` command.
|
|
16
|
+
const REJECTED_PATH_CHARS = /[;&|\0\r\n]/;
|
|
17
|
+
|
|
18
|
+
function pathFor(platform) {
|
|
19
|
+
return platform === 'win32' ? path.win32 : path.posix;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* "D:" -> "D:\". Without the trailing separator, path.resolve("D:") returns the
|
|
24
|
+
* process's remembered per-drive CWD rather than the drive root.
|
|
25
|
+
*
|
|
26
|
+
* This is the canonical copy; server-runtime.mjs, workspace-root.mjs and
|
|
27
|
+
* drives-path-helpers.mjs all delegate here. The browser has its own copy in
|
|
28
|
+
* public/app/cwd-picker.js because server/ modules are not served to the client.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeDriveLetterOnlyPath(value) {
|
|
31
|
+
const stripped = String(value ?? '').trim();
|
|
32
|
+
if (/^[A-Za-z]:$/.test(stripped)) return `${stripped}\\`;
|
|
33
|
+
return stripped;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A comparison key for two paths that name the same directory.
|
|
38
|
+
* Windows is case-insensitive across every segment, not just the drive letter,
|
|
39
|
+
* so the whole string folds there. POSIX paths are left alone.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeWorkspaceRootKey(value, platform = process.platform) {
|
|
42
|
+
let stripped = String(value ?? '').trim();
|
|
43
|
+
if (!stripped) return '';
|
|
44
|
+
// Keep the separator that *is* the root ("C:\", "/").
|
|
45
|
+
const trimmed = stripped.replace(/[\\/]+$/, '');
|
|
46
|
+
stripped = trimmed || stripped.slice(0, 1);
|
|
47
|
+
if (/^[A-Za-z]:$/.test(stripped)) stripped = `${stripped}\\`;
|
|
48
|
+
return platform === 'win32' ? stripped.toLowerCase() : stripped;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Segment-boundary containment check. Never a bare startsWith: the prefix
|
|
53
|
+
* "C:\work" must not admit "C:\work-secrets".
|
|
54
|
+
*/
|
|
55
|
+
export function isWithinAllowedPrefix(candidatePath, prefixes = [], platform = process.platform) {
|
|
56
|
+
const list = Array.isArray(prefixes) ? prefixes : [];
|
|
57
|
+
if (!list.length) return true;
|
|
58
|
+
const sep = platform === 'win32' ? '\\' : '/';
|
|
59
|
+
const key = normalizeWorkspaceRootKey(candidatePath, platform);
|
|
60
|
+
if (!key) return false;
|
|
61
|
+
return list.some((prefix) => {
|
|
62
|
+
const prefixKey = normalizeWorkspaceRootKey(prefix, platform);
|
|
63
|
+
if (!prefixKey) return false;
|
|
64
|
+
if (key === prefixKey) return true;
|
|
65
|
+
// A root prefix ("C:\", "/") already ends in the separator.
|
|
66
|
+
const withSep = prefixKey.endsWith(sep) ? prefixKey : `${prefixKey}${sep}`;
|
|
67
|
+
return key.startsWith(withSep);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Strict validation for client-supplied launch directories.
|
|
73
|
+
* Returns { ok, path, realPath } or { ok: false, code, error }.
|
|
74
|
+
*
|
|
75
|
+
* Codes: missing-root-path | invalid-root-path | relative-root-path
|
|
76
|
+
* | root-path-not-found | root-path-not-allowed
|
|
77
|
+
*/
|
|
78
|
+
export function validateRequestedWorkspaceRoot(candidatePath, {
|
|
79
|
+
platform = process.platform,
|
|
80
|
+
allowList = [],
|
|
81
|
+
statSyncImpl = fs.statSync,
|
|
82
|
+
realpathSyncImpl = fs.realpathSync.native,
|
|
83
|
+
} = {}) {
|
|
84
|
+
const pathApi = pathFor(platform);
|
|
85
|
+
const raw = String(candidatePath ?? '').trim();
|
|
86
|
+
if (!raw) {
|
|
87
|
+
return { ok: false, code: 'missing-root-path', error: 'Missing rootPath' };
|
|
88
|
+
}
|
|
89
|
+
if (REJECTED_PATH_CHARS.test(raw)) {
|
|
90
|
+
return { ok: false, code: 'invalid-root-path', error: 'The path contains characters that are not allowed.' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const withDriveRoot = normalizeDriveLetterOnlyPath(raw);
|
|
94
|
+
if (platform === 'win32') {
|
|
95
|
+
// Drive-relative ("C:foo") resolves against a per-drive CWD we do not control.
|
|
96
|
+
if (/^[A-Za-z]:[^\\/]/.test(withDriveRoot)) {
|
|
97
|
+
return { ok: false, code: 'relative-root-path', error: 'Use an absolute path (for example C:\\projects\\app).' };
|
|
98
|
+
}
|
|
99
|
+
// Extended-length and device namespaces bypass normalization entirely.
|
|
100
|
+
if (/^\\\\[?.]\\/.test(withDriveRoot)) {
|
|
101
|
+
return { ok: false, code: 'invalid-root-path', error: 'Extended-length and device paths are not supported.' };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!pathApi.isAbsolute(withDriveRoot)) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
code: 'relative-root-path',
|
|
108
|
+
error: platform === 'win32'
|
|
109
|
+
? 'Use an absolute path (for example C:\\projects\\app).'
|
|
110
|
+
: 'Use an absolute path (for example /home/user/projects/app).',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const resolved = pathApi.resolve(withDriveRoot);
|
|
115
|
+
try {
|
|
116
|
+
if (!statSyncImpl(resolved).isDirectory()) {
|
|
117
|
+
return { ok: false, code: 'root-path-not-found', error: `Not a directory: ${resolved}` };
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
return { ok: false, code: 'root-path-not-found', error: `Directory not found: ${resolved}` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Resolve symlinks before the allowlist check so an alias cannot smuggle a
|
|
124
|
+
// request past it, and so the recent-CWD list stops collecting aliases.
|
|
125
|
+
let realPath = resolved;
|
|
126
|
+
try {
|
|
127
|
+
realPath = realpathSyncImpl(resolved);
|
|
128
|
+
if (!statSyncImpl(realPath).isDirectory()) {
|
|
129
|
+
return { ok: false, code: 'root-path-not-found', error: `Not a directory: ${realPath}` };
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
realPath = resolved;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!isWithinAllowedPrefix(realPath, allowList, platform)) {
|
|
136
|
+
return { ok: false, code: 'root-path-not-allowed', error: 'That directory is outside the configured workspace allow list.' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ok: true, path: resolved, realPath };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Config/env parsing for the opt-in allow list.
|
|
144
|
+
* Absent, null, empty or malformed input disables the allow list entirely, so
|
|
145
|
+
* deployments that never set it behave exactly as they did before.
|
|
146
|
+
* Entries that do not resolve to a directory are dropped with a warning rather
|
|
147
|
+
* than failing closed — a typo must not brick a relay.
|
|
148
|
+
*/
|
|
149
|
+
export function normalizeWorkspaceRootAllowList(value, {
|
|
150
|
+
statSyncImpl = fs.statSync,
|
|
151
|
+
realpathSyncImpl = fs.realpathSync.native,
|
|
152
|
+
warn = console.warn,
|
|
153
|
+
} = {}) {
|
|
154
|
+
let entries = [];
|
|
155
|
+
if (Array.isArray(value)) entries = value;
|
|
156
|
+
else if (typeof value === 'string' && value.trim()) entries = value.split(path.delimiter);
|
|
157
|
+
else return [];
|
|
158
|
+
|
|
159
|
+
const normalized = [];
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const raw = normalizeDriveLetterOnlyPath(entry);
|
|
162
|
+
if (!raw) continue;
|
|
163
|
+
let resolved = '';
|
|
164
|
+
try {
|
|
165
|
+
resolved = path.resolve(raw);
|
|
166
|
+
if (!statSyncImpl(resolved).isDirectory()) {
|
|
167
|
+
warn(`[workspace-root] ignoring allow-list entry (not a directory): ${raw}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
resolved = realpathSyncImpl(resolved);
|
|
172
|
+
} catch {
|
|
173
|
+
// Keep the resolved path when realpath is unavailable.
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
warn(`[workspace-root] ignoring allow-list entry (not found): ${raw}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!normalized.includes(resolved)) normalized.push(resolved);
|
|
180
|
+
}
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Shared body-alias reader so both CWD endpoints accept the same payload shape.
|
|
186
|
+
* `||` rather than `??` preserves the existing fall-through-on-empty-string.
|
|
187
|
+
*/
|
|
188
|
+
export function readWorkspaceRootPathFromBody(body) {
|
|
189
|
+
return String(
|
|
190
|
+
body?.rootPath
|
|
191
|
+
|| body?.workspaceRootPath
|
|
192
|
+
|| body?.workspace_root_path
|
|
193
|
+
|| body?.cwd
|
|
194
|
+
|| '',
|
|
195
|
+
).trim();
|
|
196
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { normalizeWorkspaceRootKey } from './workspace-root-path-policy.mjs';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Workspace-root relaunch policy
|
|
5
|
+
//
|
|
6
|
+
// Pure, express-free helpers behind POST /api/conversation/:id/relaunch-with-workspace-root.
|
|
7
|
+
// Kept separate from the route so the concurrency and reporting rules can be
|
|
8
|
+
// tested without an HTTP server.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
// Mirrors DUPLICATE_USER_MESSAGE_WINDOW_MS in routes/messages-routes.mjs: a
|
|
12
|
+
// repeated request inside this window is treated as the same user intent.
|
|
13
|
+
export const RELAUNCH_COALESCE_WINDOW_MS = 5_000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decide whether a relaunch may proceed and whether the running CLI must be stopped first.
|
|
17
|
+
*
|
|
18
|
+
* `workerPidAlive` / `liveProcessDetected` matter because a session whose status
|
|
19
|
+
* is anything but 'ready' (e.g. 'error', or missing from the registry entirely)
|
|
20
|
+
* can still have a live process. Without them the route would skip the stop and
|
|
21
|
+
* the launch would silently reuse that process — in the *old* directory.
|
|
22
|
+
*
|
|
23
|
+
* The return shape is deliberately just { ok, stopWorker }.
|
|
24
|
+
*/
|
|
25
|
+
export function evaluateWorkspaceRootRelaunch({
|
|
26
|
+
workerStatus = '',
|
|
27
|
+
activeQueueCount = 0,
|
|
28
|
+
workerPidAlive = false,
|
|
29
|
+
liveProcessDetected = false,
|
|
30
|
+
} = {}) {
|
|
31
|
+
const normalizedStatus = String(workerStatus || '').trim().toLowerCase();
|
|
32
|
+
if (Number(activeQueueCount) > 0 || normalizedStatus === 'processing' || normalizedStatus === 'starting') {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
statusCode: 409,
|
|
36
|
+
error: 'Wait for the active turn to finish before changing CWD.',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
stopWorker: normalizedStatus === 'ready' || workerPidAlive === true || liveProcessDetected === true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Identity of a relaunch request. Two requests with the same key are the same
|
|
47
|
+
* user intent and may share one result; two with different keys must not.
|
|
48
|
+
*/
|
|
49
|
+
export function buildRelaunchRequestKey({
|
|
50
|
+
conversationId = '',
|
|
51
|
+
rootPath = '',
|
|
52
|
+
idempotencyKey = '',
|
|
53
|
+
platform = process.platform,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const explicit = String(idempotencyKey || '').trim();
|
|
56
|
+
if (explicit) return `idem:${explicit}`;
|
|
57
|
+
const conv = String(conversationId || '').trim();
|
|
58
|
+
return `${conv}|${normalizeWorkspaceRootKey(rootPath, platform)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Per-session request coalescer.
|
|
63
|
+
*
|
|
64
|
+
* - same key, in flight -> await and return the same body
|
|
65
|
+
* - same key, just settled -> replay the cached body
|
|
66
|
+
* - different key, in flight -> busy
|
|
67
|
+
*/
|
|
68
|
+
export function createRelaunchCoalescer({
|
|
69
|
+
nowMs = Date.now,
|
|
70
|
+
windowMs = RELAUNCH_COALESCE_WINDOW_MS,
|
|
71
|
+
} = {}) {
|
|
72
|
+
/** @type {Map<string, { key: string, promise: Promise<unknown>|null, result: unknown, settledAtMs: number }>} */
|
|
73
|
+
const bySession = new Map();
|
|
74
|
+
|
|
75
|
+
function prune(reference = nowMs()) {
|
|
76
|
+
for (const [sessionId, entry] of bySession) {
|
|
77
|
+
if (entry.promise) continue;
|
|
78
|
+
if (reference - entry.settledAtMs > windowMs) bySession.delete(sessionId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function peek(sessionId, key) {
|
|
83
|
+
prune();
|
|
84
|
+
const entry = bySession.get(String(sessionId || ''));
|
|
85
|
+
if (!entry) return { state: 'idle' };
|
|
86
|
+
if (entry.key !== key) {
|
|
87
|
+
return entry.promise ? { state: 'busy' } : { state: 'idle' };
|
|
88
|
+
}
|
|
89
|
+
if (entry.promise) return { state: 'in-flight', promise: entry.promise };
|
|
90
|
+
if (nowMs() - entry.settledAtMs <= windowMs) return { state: 'cached', result: entry.result };
|
|
91
|
+
return { state: 'idle' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function begin(sessionId, key) {
|
|
95
|
+
const sid = String(sessionId || '');
|
|
96
|
+
let settle;
|
|
97
|
+
const promise = new Promise((resolve) => { settle = resolve; });
|
|
98
|
+
bySession.set(sid, { key, promise, result: undefined, settledAtMs: 0 });
|
|
99
|
+
return { settle };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function settle(sessionId, key, result) {
|
|
103
|
+
const sid = String(sessionId || '');
|
|
104
|
+
const entry = bySession.get(sid);
|
|
105
|
+
if (!entry || entry.key !== key) return;
|
|
106
|
+
entry.promise = null;
|
|
107
|
+
entry.result = result;
|
|
108
|
+
entry.settledAtMs = nowMs();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function forget(sessionId) {
|
|
112
|
+
bySession.delete(String(sessionId || ''));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { peek, begin, settle, forget, prune, size: () => bySession.size };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Did a relaunch actually apply the requested directory?
|
|
120
|
+
* `comparable` is false when the running CLI's directory is unknown, in which
|
|
121
|
+
* case the caller must not claim success either way.
|
|
122
|
+
*/
|
|
123
|
+
export function evaluateReuseCwdMismatch({
|
|
124
|
+
requestedRootPath = '',
|
|
125
|
+
observedRootPath = '',
|
|
126
|
+
platform = process.platform,
|
|
127
|
+
} = {}) {
|
|
128
|
+
const requested = normalizeWorkspaceRootKey(requestedRootPath, platform);
|
|
129
|
+
const observed = normalizeWorkspaceRootKey(observedRootPath, platform);
|
|
130
|
+
if (!requested || !observed) return { comparable: false, mismatch: false };
|
|
131
|
+
return { comparable: true, mismatch: requested !== observed };
|
|
132
|
+
}
|