@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,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maps the Claude Agent SDK's context-usage control response onto the relay's
|
|
5
|
+
* existing context-snapshot contract.
|
|
6
|
+
*
|
|
7
|
+
* Claude sessions have no Copilot `events.jsonl` to tail, so this is the only
|
|
8
|
+
* source of context data for them. The SDK hands back the same breakdown that
|
|
9
|
+
* powers `/context` in Claude Code — see `SDKControlGetContextUsageResponse` in
|
|
10
|
+
* `@anthropic-ai/claude-agent-sdk`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Category names as the SDK reports them, mapped onto the snapshot's coarser
|
|
14
|
+
// system/tools/messages buckets. Matched case-insensitively; unknown categories
|
|
15
|
+
// are still carried in the view payload, they just don't fold into a bucket.
|
|
16
|
+
const SYSTEM_CATEGORY_NAMES = new Set(['system prompt', 'memory files', 'custom agents']);
|
|
17
|
+
const TOOLS_CATEGORY_NAMES = new Set(['system tools', 'tools', 'mcp tools', 'skills', 'slash commands']);
|
|
18
|
+
const MESSAGES_CATEGORY_NAMES = new Set(['messages', 'conversation']);
|
|
19
|
+
const FREE_CATEGORY_NAMES = new Set(['free space', 'free', 'autocompact buffer']);
|
|
20
|
+
|
|
21
|
+
function toNullableInt(value) {
|
|
22
|
+
if (value === null || value === undefined || value === '') return null;
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
if (!Number.isFinite(n)) return null;
|
|
25
|
+
return Math.round(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toNullablePercent(value) {
|
|
29
|
+
if (value === null || value === undefined || value === '') return null;
|
|
30
|
+
const n = Number(value);
|
|
31
|
+
if (!Number.isFinite(n)) return null;
|
|
32
|
+
return Math.round(n * 100) / 100;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeText(value) {
|
|
36
|
+
return String(value || '').trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isFreeSpaceCategoryName(name) {
|
|
40
|
+
return FREE_CATEGORY_NAMES.has(normalizeText(name).toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validate and normalize a raw SDK context-usage response. Returns null for
|
|
45
|
+
* anything that isn't usable, so callers never have to guard field-by-field.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizeClaudeContextUsage(raw) {
|
|
48
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
49
|
+
|
|
50
|
+
const totalTokens = toNullableInt(raw.totalTokens);
|
|
51
|
+
const maxTokens = toNullableInt(raw.maxTokens) ?? toNullableInt(raw.rawMaxTokens);
|
|
52
|
+
if (totalTokens === null && maxTokens === null) return null;
|
|
53
|
+
|
|
54
|
+
const categories = (Array.isArray(raw.categories) ? raw.categories : [])
|
|
55
|
+
.map((entry) => {
|
|
56
|
+
const name = normalizeText(entry?.name);
|
|
57
|
+
const tokens = toNullableInt(entry?.tokens);
|
|
58
|
+
if (!name || tokens === null) return null;
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
tokens,
|
|
62
|
+
color: normalizeText(entry?.color) || null,
|
|
63
|
+
isDeferred: entry?.isDeferred === true,
|
|
64
|
+
};
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
// The SDK sometimes includes free space as a category; the view layer
|
|
68
|
+
// derives that from the total instead, so drop it here to avoid double
|
|
69
|
+
// counting it in the bar.
|
|
70
|
+
.filter((entry) => !isFreeSpaceCategoryName(entry.name));
|
|
71
|
+
|
|
72
|
+
const apiUsage = raw.apiUsage && typeof raw.apiUsage === 'object' ? raw.apiUsage : null;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
model: normalizeText(raw.model) || null,
|
|
76
|
+
totalTokens,
|
|
77
|
+
maxTokens,
|
|
78
|
+
percentage: toNullablePercent(raw.percentage),
|
|
79
|
+
// Set by the Cursor worker when occupancy is derived rather than measured
|
|
80
|
+
// (e.g. 'cursor-per-call-average'); absent from Claude SDK payloads.
|
|
81
|
+
estimateKind: normalizeText(raw.estimateKind) || null,
|
|
82
|
+
categories,
|
|
83
|
+
skills: raw.skills && typeof raw.skills === 'object'
|
|
84
|
+
? {
|
|
85
|
+
totalSkills: toNullableInt(raw.skills.totalSkills),
|
|
86
|
+
includedSkills: toNullableInt(raw.skills.includedSkills),
|
|
87
|
+
tokens: toNullableInt(raw.skills.tokens),
|
|
88
|
+
}
|
|
89
|
+
: null,
|
|
90
|
+
// NOT the model's own limit: probed on claude-opus-5[1m], it follows the
|
|
91
|
+
// ACTIVE auto-compact window (no setting → 1000000, setting 100000 →
|
|
92
|
+
// 100000). Recorded because the SDK reports it, and used as the fallback
|
|
93
|
+
// for a missing maxTokens above — never to say what a model can hold. The
|
|
94
|
+
// modal annotated slider stops from it until 2026-08-21 and so told users
|
|
95
|
+
// who had just narrowed the window that widening it again was pointless.
|
|
96
|
+
rawMaxTokens: toNullableInt(raw.rawMaxTokens),
|
|
97
|
+
// A TOKEN COUNT, not a percent: the CLI compacts once the conversation
|
|
98
|
+
// crosses it (measured: 967000 on a 1M-window model). It was run through a
|
|
99
|
+
// percent normalizer here until 2026-08-20, which is why the derived
|
|
100
|
+
// buffer below was always 0.
|
|
101
|
+
autoCompactThreshold: toNullableInt(raw.autoCompactThreshold),
|
|
102
|
+
// Where that threshold came from: 'auto' | 'model-default' | 'settings' |
|
|
103
|
+
// 'env' | 'clientdata' (the CLI's own vocabulary). The modal renders it so
|
|
104
|
+
// a user can tell a default from their own setting.
|
|
105
|
+
autocompactSource: normalizeText(raw.autocompactSource) || null,
|
|
106
|
+
isAutoCompactEnabled: raw.isAutoCompactEnabled === true,
|
|
107
|
+
apiUsage: apiUsage
|
|
108
|
+
? {
|
|
109
|
+
inputTokens: toNullableInt(apiUsage.input_tokens),
|
|
110
|
+
outputTokens: toNullableInt(apiUsage.output_tokens),
|
|
111
|
+
cacheReadTokens: toNullableInt(apiUsage.cache_read_input_tokens),
|
|
112
|
+
cacheWriteTokens: toNullableInt(apiUsage.cache_creation_input_tokens),
|
|
113
|
+
}
|
|
114
|
+
: null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `modelUsage` is keyed by the raw model string. Prefer the requested model,
|
|
120
|
+
* fall back to the only entry when there is exactly one (the common case), so a
|
|
121
|
+
* model-id spelling mismatch doesn't lose the context window.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveModelUsageEntry(modelUsage, model) {
|
|
124
|
+
if (!modelUsage || typeof modelUsage !== 'object') return null;
|
|
125
|
+
const wanted = normalizeText(model).toLowerCase();
|
|
126
|
+
const entries = Object.entries(modelUsage).filter(([, value]) => value && typeof value === 'object');
|
|
127
|
+
if (!entries.length) return null;
|
|
128
|
+
if (wanted) {
|
|
129
|
+
const match = entries.find(([key]) => normalizeText(key).toLowerCase() === wanted);
|
|
130
|
+
if (match) return match[1];
|
|
131
|
+
}
|
|
132
|
+
return entries.length === 1 ? entries[0][1] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sumCategories(categories, names) {
|
|
136
|
+
let total = null;
|
|
137
|
+
for (const entry of categories) {
|
|
138
|
+
if (!names.has(entry.name.toLowerCase())) continue;
|
|
139
|
+
total = (total || 0) + entry.tokens;
|
|
140
|
+
}
|
|
141
|
+
return total;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build a snapshot in the same shape `context-snapshot-service` produces for
|
|
146
|
+
* Copilot, so the indicator line and the existing text renderer work unchanged.
|
|
147
|
+
*/
|
|
148
|
+
export function buildClaudeContextSnapshot({
|
|
149
|
+
contextUsage,
|
|
150
|
+
modelUsage = null,
|
|
151
|
+
model = '',
|
|
152
|
+
runtimeSessionId = null,
|
|
153
|
+
sdkSessionId = null,
|
|
154
|
+
capturedAt = null,
|
|
155
|
+
} = {}) {
|
|
156
|
+
const usage = normalizeClaudeContextUsage(contextUsage);
|
|
157
|
+
const modelUsageEntry = resolveModelUsageEntry(modelUsage, model || usage?.model);
|
|
158
|
+
const contextWindow = toNullableInt(modelUsageEntry?.contextWindow);
|
|
159
|
+
|
|
160
|
+
const maxContextTokens = usage?.maxTokens ?? contextWindow;
|
|
161
|
+
const usedTotalTokens = usage?.totalTokens ?? null;
|
|
162
|
+
if (usedTotalTokens === null && maxContextTokens === null) return null;
|
|
163
|
+
|
|
164
|
+
const usedPercent = usage?.percentage
|
|
165
|
+
?? ((usedTotalTokens !== null && maxContextTokens !== null && maxContextTokens > 0)
|
|
166
|
+
? Math.round((usedTotalTokens / maxContextTokens) * 10000) / 100
|
|
167
|
+
: null);
|
|
168
|
+
const freeTokens = (usedTotalTokens !== null && maxContextTokens !== null)
|
|
169
|
+
? Math.max(0, maxContextTokens - usedTotalTokens)
|
|
170
|
+
: null;
|
|
171
|
+
|
|
172
|
+
const categories = usage?.categories || [];
|
|
173
|
+
const systemTokens = sumCategories(categories, SYSTEM_CATEGORY_NAMES);
|
|
174
|
+
const toolsTokens = sumCategories(categories, TOOLS_CATEGORY_NAMES);
|
|
175
|
+
const messagesTokens = sumCategories(categories, MESSAGES_CATEGORY_NAMES);
|
|
176
|
+
const systemToolsTokens = (systemTokens !== null || toolsTokens !== null)
|
|
177
|
+
? Number(systemTokens || 0) + Number(toolsTokens || 0)
|
|
178
|
+
: null;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
runtime_session_id: runtimeSessionId || null,
|
|
182
|
+
copilot_session_id: sdkSessionId || null,
|
|
183
|
+
model: usage?.model || normalizeText(model) || null,
|
|
184
|
+
used_total_tokens: usedTotalTokens,
|
|
185
|
+
max_context_tokens: maxContextTokens,
|
|
186
|
+
used_percent: usedPercent,
|
|
187
|
+
free_tokens: freeTokens,
|
|
188
|
+
// Derived from `max - used`, so it still spans the buffer below; the view
|
|
189
|
+
// layer splits the two before rendering them side by side.
|
|
190
|
+
free_tokens_includes_buffer: freeTokens !== null,
|
|
191
|
+
// Everything above the auto-compact threshold is context Claude will never
|
|
192
|
+
// let the conversation occupy, so it is the closest analogue to Copilot's
|
|
193
|
+
// buffer. Both values are token counts.
|
|
194
|
+
buffer_tokens: (usage?.isAutoCompactEnabled && usage?.autoCompactThreshold != null
|
|
195
|
+
&& maxContextTokens !== null)
|
|
196
|
+
? Math.max(0, maxContextTokens - usage.autoCompactThreshold)
|
|
197
|
+
: null,
|
|
198
|
+
system_tokens: systemTokens,
|
|
199
|
+
messages_tokens: messagesTokens,
|
|
200
|
+
tools_tokens: toolsTokens,
|
|
201
|
+
system_tools_tokens: systemToolsTokens,
|
|
202
|
+
used_prompt_tokens: usage?.apiUsage?.inputTokens ?? null,
|
|
203
|
+
used_completion_tokens: usage?.apiUsage?.outputTokens ?? null,
|
|
204
|
+
reasoning_tokens: null,
|
|
205
|
+
cache_read_tokens: usage?.apiUsage?.cacheReadTokens ?? null,
|
|
206
|
+
cache_write_tokens: usage?.apiUsage?.cacheWriteTokens ?? null,
|
|
207
|
+
captured_at: normalizeText(capturedAt) || null,
|
|
208
|
+
estimate_kind: usage?.estimateKind ?? null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parse the JSON blob persisted on `runtime_sessions` and rebuild both the
|
|
214
|
+
* snapshot and the normalized usage the view layer renders.
|
|
215
|
+
*/
|
|
216
|
+
export function readStoredClaudeContextUsage(row) {
|
|
217
|
+
const rawJson = normalizeText(row?.context_usage_json);
|
|
218
|
+
if (!rawJson) return { snapshot: null, contextUsage: null };
|
|
219
|
+
|
|
220
|
+
let parsed = null;
|
|
221
|
+
try {
|
|
222
|
+
parsed = JSON.parse(rawJson);
|
|
223
|
+
} catch {
|
|
224
|
+
return { snapshot: null, contextUsage: null };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const capturedAt = normalizeText(row?.context_usage_captured_at) || null;
|
|
228
|
+
const snapshot = buildClaudeContextSnapshot({
|
|
229
|
+
contextUsage: parsed?.contextUsage,
|
|
230
|
+
modelUsage: parsed?.modelUsage,
|
|
231
|
+
model: parsed?.model || row?.provider_model || row?.model || '',
|
|
232
|
+
runtimeSessionId: row?.id || null,
|
|
233
|
+
sdkSessionId: row?.sdk_session_id || null,
|
|
234
|
+
capturedAt,
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
snapshot,
|
|
238
|
+
contextUsage: normalizeClaudeContextUsage(parsed?.contextUsage),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import nodeFs from 'fs';
|
|
4
|
+
import nodePath from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
// Resolve the on-disk folder the Claude Agent SDK keeps for one session, so the
|
|
8
|
+
// file explorer's Session root works on `claude`-provider conversations.
|
|
9
|
+
//
|
|
10
|
+
// The Copilot CLI creates `~/.copilot/session-state/<sdkSessionId>/` as a side
|
|
11
|
+
// effect of running; the Claude worker never touches that path, which is why the
|
|
12
|
+
// Session button was permanently disabled for Claude conversations. The SDK's
|
|
13
|
+
// own equivalent is:
|
|
14
|
+
//
|
|
15
|
+
// <configRoot>/projects/<slug(cwd)>/<nativeSessionId>/
|
|
16
|
+
//
|
|
17
|
+
// with the sibling `<nativeSessionId>.jsonl` transcript one level up. Only the
|
|
18
|
+
// per-session directory is exposed — the project directory holds every session
|
|
19
|
+
// for that workspace plus `memory/`, so pointing the browser at it would leak
|
|
20
|
+
// unrelated conversations into a "Session" view.
|
|
21
|
+
//
|
|
22
|
+
// The transcript, not the directory, is what anchors a session here. The SDK
|
|
23
|
+
// writes the `.jsonl` from the first turn but creates the directory lazily, only
|
|
24
|
+
// once the session produces `subagents/` or `tool-results/` files — so requiring
|
|
25
|
+
// the directory left the Session button dark for every conversation that had not
|
|
26
|
+
// yet spawned a subagent. `sessionRootPath` is therefore returned for a
|
|
27
|
+
// transcript-only session too, with `sessionRootExists: false` so the caller can
|
|
28
|
+
// serve it as an empty folder rather than a 404.
|
|
29
|
+
|
|
30
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
31
|
+
const PROJECT_SLUG_MAX_LENGTH = 200;
|
|
32
|
+
const DEFAULT_MISS_TTL_MS = 5_000;
|
|
33
|
+
const DEFAULT_MAX_PROJECT_DIRS_SCANNED = 500;
|
|
34
|
+
const DEFAULT_MAX_CACHE_ENTRIES = 256;
|
|
35
|
+
|
|
36
|
+
function normalizeText(value) {
|
|
37
|
+
const text = String(value || '').trim();
|
|
38
|
+
return text || '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function slugHashSuffix(value) {
|
|
42
|
+
let hash = 0;
|
|
43
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
44
|
+
hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0;
|
|
45
|
+
}
|
|
46
|
+
return Math.abs(hash).toString(36);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mirrors the directory-name sanitizer the Agent SDK applies to a session's cwd.
|
|
51
|
+
* Reimplemented rather than imported: `claude-sdk-adapter.mjs` is deliberately
|
|
52
|
+
* the only module in this repo that pulls in `@anthropic-ai/claude-agent-sdk`,
|
|
53
|
+
* and the relay server must not load it.
|
|
54
|
+
*/
|
|
55
|
+
export function claudeProjectDirSlug(cwd) {
|
|
56
|
+
const value = String(cwd || '');
|
|
57
|
+
const sanitized = value.replace(/[^a-zA-Z0-9]/g, '-');
|
|
58
|
+
if (sanitized.length <= PROJECT_SLUG_MAX_LENGTH) return sanitized;
|
|
59
|
+
return `${sanitized.slice(0, PROJECT_SLUG_MAX_LENGTH)}-${slugHashSuffix(value)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Candidate `<configRoot>/projects` directories, most specific first.
|
|
64
|
+
*
|
|
65
|
+
* Both `$CLAUDE_CONFIG_DIR` and `~/.claude` are probed because a session may
|
|
66
|
+
* predate the override: the tmux launch path now forwards CLAUDE_CONFIG_DIR in
|
|
67
|
+
* its env allowlist (matching the detached spawn path, which inherits the full
|
|
68
|
+
* environment), but transcripts written before that fix still live under
|
|
69
|
+
* `~/.claude`.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveClaudeProjectsRoots({ env = process.env, homedir = os.homedir, path = nodePath } = {}) {
|
|
72
|
+
const roots = [];
|
|
73
|
+
const configDirOverride = normalizeText(env?.CLAUDE_CONFIG_DIR);
|
|
74
|
+
if (configDirOverride) roots.push(path.join(configDirOverride, 'projects'));
|
|
75
|
+
let home = '';
|
|
76
|
+
try {
|
|
77
|
+
home = normalizeText(typeof homedir === 'function' ? homedir() : homedir);
|
|
78
|
+
} catch {
|
|
79
|
+
home = '';
|
|
80
|
+
}
|
|
81
|
+
if (!home) home = normalizeText(env?.HOME) || normalizeText(env?.USERPROFILE);
|
|
82
|
+
if (home) roots.push(path.join(home, '.claude', 'projects'));
|
|
83
|
+
return [...new Set(roots.map((root) => root.normalize('NFC')))];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createClaudeSessionRootResolver({
|
|
87
|
+
fs = nodeFs,
|
|
88
|
+
path = nodePath,
|
|
89
|
+
env = process.env,
|
|
90
|
+
homedir = os.homedir,
|
|
91
|
+
now = () => Date.now(),
|
|
92
|
+
missTtlMs = DEFAULT_MISS_TTL_MS,
|
|
93
|
+
maxProjectDirsScanned = DEFAULT_MAX_PROJECT_DIRS_SCANNED,
|
|
94
|
+
maxCacheEntries = DEFAULT_MAX_CACHE_ENTRIES,
|
|
95
|
+
} = {}) {
|
|
96
|
+
// nativeSessionId -> the project directory that contains it.
|
|
97
|
+
const projectDirCache = new Map();
|
|
98
|
+
// nativeSessionId -> timestamp after which it is worth scanning again.
|
|
99
|
+
const missCache = new Map();
|
|
100
|
+
|
|
101
|
+
function isDirectory(candidate) {
|
|
102
|
+
try {
|
|
103
|
+
return fs.statSync(candidate).isDirectory();
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isFile(candidate) {
|
|
110
|
+
try {
|
|
111
|
+
return fs.statSync(candidate).isFile();
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The session's on-disk footprint inside one project directory, or null when
|
|
119
|
+
* the session does not live there at all.
|
|
120
|
+
*
|
|
121
|
+
* @returns {{ sessionRootPath: string, transcriptPath: string, sessionRootExists: boolean } | null}
|
|
122
|
+
*/
|
|
123
|
+
function sessionAnchorsIn(projectDir, nativeSessionId) {
|
|
124
|
+
const sessionRootPath = path.join(projectDir, nativeSessionId);
|
|
125
|
+
const transcriptPath = path.join(projectDir, `${nativeSessionId}.jsonl`);
|
|
126
|
+
const sessionRootExists = isDirectory(sessionRootPath);
|
|
127
|
+
const transcriptExists = isFile(transcriptPath);
|
|
128
|
+
if (!sessionRootExists && !transcriptExists) return null;
|
|
129
|
+
return {
|
|
130
|
+
sessionRootPath,
|
|
131
|
+
transcriptPath: transcriptExists ? transcriptPath : '',
|
|
132
|
+
sessionRootExists,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findProjectDir(nativeSessionId, workspaceRootPath) {
|
|
137
|
+
const roots = resolveClaudeProjectsRoots({ env, homedir, path });
|
|
138
|
+
const workspaceRoot = normalizeText(workspaceRootPath);
|
|
139
|
+
|
|
140
|
+
// Fast path: derive the project dir straight from the session's workspace.
|
|
141
|
+
if (workspaceRoot) {
|
|
142
|
+
const slug = claudeProjectDirSlug(workspaceRoot);
|
|
143
|
+
for (const root of roots) {
|
|
144
|
+
const projectDir = path.join(root, slug);
|
|
145
|
+
if (sessionAnchorsIn(projectDir, nativeSessionId)) return projectDir;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fallback: the slug can miss (symlinked or since-changed workspace root, a
|
|
150
|
+
// git worktree, an SDK sanitizer change), so scan the project dirs. Bounded
|
|
151
|
+
// and never recursive — one readdir per root plus two stats per project.
|
|
152
|
+
for (const root of roots) {
|
|
153
|
+
let entries = [];
|
|
154
|
+
try {
|
|
155
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
156
|
+
} catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
let scanned = 0;
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (!entry.isDirectory()) continue;
|
|
162
|
+
if (scanned >= maxProjectDirsScanned) break;
|
|
163
|
+
scanned += 1;
|
|
164
|
+
const projectDir = path.join(root, entry.name);
|
|
165
|
+
if (sessionAnchorsIn(projectDir, nativeSessionId)) return projectDir;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function rememberProjectDir(nativeSessionId, projectDir) {
|
|
172
|
+
if (projectDirCache.size >= maxCacheEntries) projectDirCache.clear();
|
|
173
|
+
projectDirCache.set(nativeSessionId, projectDir);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @returns {{
|
|
178
|
+
* sessionRootPath: string,
|
|
179
|
+
* sessionRootName: string,
|
|
180
|
+
* projectDirPath: string,
|
|
181
|
+
* transcriptPath: string,
|
|
182
|
+
* sessionRootExists: boolean,
|
|
183
|
+
* } | null}
|
|
184
|
+
*/
|
|
185
|
+
function resolveClaudeSessionRoot({ claudeNativeSessionId = '', workspaceRootPath = '' } = {}) {
|
|
186
|
+
const nativeSessionId = normalizeText(claudeNativeSessionId);
|
|
187
|
+
// The id is joined into a filesystem path, so it is validated before any
|
|
188
|
+
// syscall rather than trusted the way the Copilot branch trusts its own.
|
|
189
|
+
if (!SESSION_ID_PATTERN.test(nativeSessionId)) return null;
|
|
190
|
+
|
|
191
|
+
const cachedProjectDir = projectDirCache.get(nativeSessionId);
|
|
192
|
+
if (cachedProjectDir) {
|
|
193
|
+
// Re-probed rather than assumed: the directory appears partway through a
|
|
194
|
+
// session's life, so a cache hit must still report the current state.
|
|
195
|
+
const anchors = sessionAnchorsIn(cachedProjectDir, nativeSessionId);
|
|
196
|
+
if (anchors) {
|
|
197
|
+
return { ...anchors, sessionRootName: 'Session', projectDirPath: cachedProjectDir };
|
|
198
|
+
}
|
|
199
|
+
projectDirCache.delete(nativeSessionId);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The client re-polls conversation detail roughly every second while a turn
|
|
203
|
+
// runs, so a miss must not mean a directory scan every time.
|
|
204
|
+
const missUntil = missCache.get(nativeSessionId);
|
|
205
|
+
if (missUntil !== undefined && missUntil > now()) return null;
|
|
206
|
+
|
|
207
|
+
const projectDir = findProjectDir(nativeSessionId, workspaceRootPath);
|
|
208
|
+
if (!projectDir) {
|
|
209
|
+
if (missCache.size >= maxCacheEntries) missCache.clear();
|
|
210
|
+
missCache.set(nativeSessionId, now() + missTtlMs);
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
// Re-read rather than reuse what the scan matched on: the session can be
|
|
214
|
+
// deleted between the two, and a partial result would be worse than a miss.
|
|
215
|
+
const anchors = sessionAnchorsIn(projectDir, nativeSessionId);
|
|
216
|
+
if (!anchors) return null;
|
|
217
|
+
missCache.delete(nativeSessionId);
|
|
218
|
+
rememberProjectDir(nativeSessionId, projectDir);
|
|
219
|
+
return { ...anchors, sessionRootName: 'Session', projectDirPath: projectDir };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function clearCache() {
|
|
223
|
+
projectDirCache.clear();
|
|
224
|
+
missCache.clear();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { resolveClaudeSessionRoot, clearCache };
|
|
228
|
+
}
|