@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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time migration of relay state into the OAR state root (~/.oar).
|
|
3
|
+
*
|
|
4
|
+
* Global npm installs cannot keep state under the package directory — `npm i -g`
|
|
5
|
+
* replaces the tree on every update, database included. This service copies the
|
|
6
|
+
* old state (a git-checkout's server/ tree, and/or the legacy managed config
|
|
7
|
+
* dir) into the state root exactly once, guarded by a marker file.
|
|
8
|
+
*
|
|
9
|
+
* Contract (docs/plans/oar-rebrand-and-release.md §5.2):
|
|
10
|
+
* - refuse to run while a relay holds the source lock (live pid);
|
|
11
|
+
* - checkpoint the WAL before copying, then copy db + -wal + -shm together;
|
|
12
|
+
* - copy, never move — the source stays untouched as the rollback path;
|
|
13
|
+
* - verify integrity and row counts before writing the marker;
|
|
14
|
+
* - on any mismatch remove the partial target and leave the marker unwritten.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
|
|
21
|
+
export const MIGRATION_MARKER = '.migrated';
|
|
22
|
+
const DB_FILE = 'copilot.db';
|
|
23
|
+
const MAX_LOGS_BYTES = 50 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
export function resolveOarRoot(env = process.env, platform = process.platform) {
|
|
26
|
+
const override = String(env.OAR_STATE_ROOT || '').trim();
|
|
27
|
+
if (override) return path.resolve(override);
|
|
28
|
+
if (platform === 'win32') {
|
|
29
|
+
const appData = String(env.APPDATA || '').trim();
|
|
30
|
+
if (appData) return path.join(appData, 'oar');
|
|
31
|
+
const profile = String(env.USERPROFILE || '').trim();
|
|
32
|
+
return path.join(profile || os.homedir(), 'AppData', 'Roaming', 'oar');
|
|
33
|
+
}
|
|
34
|
+
return path.join(os.homedir(), '.oar');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function defaultIsProcessAlive(pid) {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(pid, 0);
|
|
40
|
+
return true;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return error?.code === 'EPERM';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readLockPid(lockPath) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
49
|
+
const pid = Number.parseInt(String(parsed?.pid ?? ''), 10);
|
|
50
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function directorySizeBytes(dir) {
|
|
57
|
+
let total = 0;
|
|
58
|
+
const stack = [dir];
|
|
59
|
+
while (stack.length) {
|
|
60
|
+
const current = stack.pop();
|
|
61
|
+
let entries = [];
|
|
62
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const full = path.join(current, entry.name);
|
|
65
|
+
if (entry.isDirectory()) stack.push(full);
|
|
66
|
+
else if (entry.isFile()) {
|
|
67
|
+
try { total += fs.statSync(full).size; } catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return total;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function copyIfExists(source, target, copied) {
|
|
75
|
+
if (!source || !fs.existsSync(source)) return false;
|
|
76
|
+
const stat = fs.statSync(source);
|
|
77
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
78
|
+
if (stat.isDirectory()) fs.cpSync(source, target, { recursive: true });
|
|
79
|
+
else fs.copyFileSync(source, target);
|
|
80
|
+
copied.push({ from: source, to: target, bytes: stat.isDirectory() ? directorySizeBytes(source) : stat.size });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadDatabase(DatabaseImpl) {
|
|
85
|
+
if (DatabaseImpl) return DatabaseImpl;
|
|
86
|
+
const mod = await import('better-sqlite3');
|
|
87
|
+
return mod.default;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readDbCounts(Database, dbPath, { checkpoint = false } = {}) {
|
|
91
|
+
const db = new Database(dbPath, checkpoint ? {} : { readonly: true });
|
|
92
|
+
try {
|
|
93
|
+
if (checkpoint) db.pragma('wal_checkpoint(TRUNCATE)');
|
|
94
|
+
const integrity = String(db.pragma('integrity_check', { simple: true }) || '');
|
|
95
|
+
const tableExists = (name) => !!db
|
|
96
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?")
|
|
97
|
+
.get(name);
|
|
98
|
+
const countOf = (table) => (tableExists(table)
|
|
99
|
+
? Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get()?.n || 0)
|
|
100
|
+
: null);
|
|
101
|
+
return {
|
|
102
|
+
integrity,
|
|
103
|
+
conversations: countOf('conversations'),
|
|
104
|
+
messages: countOf('messages'),
|
|
105
|
+
};
|
|
106
|
+
} finally {
|
|
107
|
+
db.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns { status, ... } — statuses: 'already-migrated', 'blocked-live-relay',
|
|
113
|
+
* 'nothing-to-migrate', 'migrated'. Throws only on verification failure or an
|
|
114
|
+
* unexpected copy error, after removing the partial target state.
|
|
115
|
+
*/
|
|
116
|
+
export async function migrateStateToOarRoot({
|
|
117
|
+
targetRoot,
|
|
118
|
+
repoServerDir = null,
|
|
119
|
+
managedConfigDir = null,
|
|
120
|
+
isProcessAliveImpl = defaultIsProcessAlive,
|
|
121
|
+
DatabaseImpl = null,
|
|
122
|
+
now = () => new Date().toISOString(),
|
|
123
|
+
logger = console,
|
|
124
|
+
} = {}) {
|
|
125
|
+
const root = path.resolve(String(targetRoot || ''));
|
|
126
|
+
if (!root) throw new Error('targetRoot is required');
|
|
127
|
+
const markerPath = path.join(root, MIGRATION_MARKER);
|
|
128
|
+
if (fs.existsSync(markerPath)) return { status: 'already-migrated', root };
|
|
129
|
+
|
|
130
|
+
const sourceData = repoServerDir ? path.join(repoServerDir, 'data') : null;
|
|
131
|
+
const sourceDb = sourceData ? path.join(sourceData, DB_FILE) : null;
|
|
132
|
+
const sourceRepoConfig = repoServerDir ? path.join(repoServerDir, 'config.json') : null;
|
|
133
|
+
const sourceManagedConfig = managedConfigDir ? path.join(managedConfigDir, 'config.json') : null;
|
|
134
|
+
|
|
135
|
+
const lockPath = sourceData ? path.join(sourceData, 'relay-server.lock') : null;
|
|
136
|
+
if (lockPath && fs.existsSync(lockPath)) {
|
|
137
|
+
const pid = readLockPid(lockPath);
|
|
138
|
+
if (pid && isProcessAliveImpl(pid)) {
|
|
139
|
+
return {
|
|
140
|
+
status: 'blocked-live-relay',
|
|
141
|
+
root,
|
|
142
|
+
pid,
|
|
143
|
+
error: `A relay is still running (pid=${pid}, lock at ${lockPath}). Stop it, then run the migration again.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const hasDb = !!(sourceDb && fs.existsSync(sourceDb));
|
|
149
|
+
const hasAnyConfig = !!((sourceRepoConfig && fs.existsSync(sourceRepoConfig))
|
|
150
|
+
|| (sourceManagedConfig && fs.existsSync(sourceManagedConfig)));
|
|
151
|
+
if (!hasDb && !hasAnyConfig) {
|
|
152
|
+
fs.mkdirSync(root, { recursive: true });
|
|
153
|
+
fs.writeFileSync(markerPath, `${now()} nothing-to-migrate\n`);
|
|
154
|
+
return { status: 'nothing-to-migrate', root };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const copied = [];
|
|
158
|
+
let sourceCounts = null;
|
|
159
|
+
try {
|
|
160
|
+
fs.mkdirSync(path.join(root, 'data'), { recursive: true });
|
|
161
|
+
|
|
162
|
+
if (hasDb) {
|
|
163
|
+
const Database = await loadDatabase(DatabaseImpl);
|
|
164
|
+
// Checkpoint first so recent writes leave the WAL, then copy all three
|
|
165
|
+
// files anyway — a hot -wal must never be dropped on the floor.
|
|
166
|
+
sourceCounts = readDbCounts(Database, sourceDb, { checkpoint: true });
|
|
167
|
+
copyIfExists(sourceDb, path.join(root, 'data', DB_FILE), copied);
|
|
168
|
+
copyIfExists(`${sourceDb}-wal`, path.join(root, 'data', `${DB_FILE}-wal`), copied);
|
|
169
|
+
copyIfExists(`${sourceDb}-shm`, path.join(root, 'data', `${DB_FILE}-shm`), copied);
|
|
170
|
+
copyIfExists(path.join(sourceData, 'cursor-agents'), path.join(root, 'data', 'cursor-agents'), copied);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Repo config wins over the legacy managed config when both exist.
|
|
174
|
+
const configSource = (sourceRepoConfig && fs.existsSync(sourceRepoConfig))
|
|
175
|
+
? sourceRepoConfig
|
|
176
|
+
: sourceManagedConfig;
|
|
177
|
+
copyIfExists(configSource, path.join(root, 'config.json'), copied);
|
|
178
|
+
|
|
179
|
+
if (repoServerDir) {
|
|
180
|
+
copyIfExists(path.join(repoServerDir, 'uploads'), path.join(root, 'uploads'), copied);
|
|
181
|
+
const repoLogs = path.join(repoServerDir, 'logs');
|
|
182
|
+
if (fs.existsSync(repoLogs) && directorySizeBytes(repoLogs) <= MAX_LOGS_BYTES) {
|
|
183
|
+
copyIfExists(repoLogs, path.join(root, 'logs'), copied);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (managedConfigDir) {
|
|
187
|
+
const managedLogs = path.join(managedConfigDir, 'logs');
|
|
188
|
+
if (fs.existsSync(managedLogs) && directorySizeBytes(managedLogs) <= MAX_LOGS_BYTES) {
|
|
189
|
+
copyIfExists(managedLogs, path.join(root, 'logs'), copied);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (hasDb) {
|
|
194
|
+
const Database = await loadDatabase(DatabaseImpl);
|
|
195
|
+
const targetCounts = readDbCounts(Database, path.join(root, 'data', DB_FILE));
|
|
196
|
+
const mismatch = targetCounts.integrity !== 'ok'
|
|
197
|
+
|| targetCounts.conversations !== sourceCounts.conversations
|
|
198
|
+
|| targetCounts.messages !== sourceCounts.messages;
|
|
199
|
+
if (mismatch) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Migrated database failed verification (integrity=${targetCounts.integrity}, `
|
|
202
|
+
+ `conversations ${targetCounts.conversations} vs ${sourceCounts.conversations}, `
|
|
203
|
+
+ `messages ${targetCounts.messages} vs ${sourceCounts.messages}).`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Leave no partial target behind: the marker was never written, so the next
|
|
209
|
+
// attempt starts clean; the source was never touched.
|
|
210
|
+
try { fs.rmSync(root, { recursive: true, force: true }); } catch {}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const backupsDir = path.join(root, 'backups');
|
|
215
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
216
|
+
const receipt = [
|
|
217
|
+
'# OAR state migration',
|
|
218
|
+
'',
|
|
219
|
+
`- when: ${now()}`,
|
|
220
|
+
`- repo server dir: ${repoServerDir || '(none)'}`,
|
|
221
|
+
`- managed config dir: ${managedConfigDir || '(none)'}`,
|
|
222
|
+
...(sourceCounts
|
|
223
|
+
? [`- verified: integrity ok, ${sourceCounts.conversations} conversations, ${sourceCounts.messages} messages`]
|
|
224
|
+
: ['- verified: no database in source']),
|
|
225
|
+
'',
|
|
226
|
+
'Copied (sources left untouched — delete them manually once satisfied):',
|
|
227
|
+
...copied.map((c) => `- ${c.from} -> ${c.to} (${c.bytes} bytes)`),
|
|
228
|
+
'',
|
|
229
|
+
].join('\n');
|
|
230
|
+
fs.writeFileSync(path.join(backupsDir, 'MIGRATION.md'), receipt);
|
|
231
|
+
fs.writeFileSync(markerPath, `${now()} migrated\n`);
|
|
232
|
+
|
|
233
|
+
if (process.platform !== 'win32') {
|
|
234
|
+
try {
|
|
235
|
+
fs.chmodSync(root, 0o700);
|
|
236
|
+
const configTarget = path.join(root, 'config.json');
|
|
237
|
+
if (fs.existsSync(configTarget)) fs.chmodSync(configTarget, 0o600);
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
logger.log?.(`[oar] Migrated relay state into ${root} (${copied.length} items; sources untouched).`);
|
|
242
|
+
return { status: 'migrated', root, copied: copied.length, counts: sourceCounts };
|
|
243
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude subscription plan usage.
|
|
5
|
+
*
|
|
6
|
+
* Source is the Agent SDK's structured `/usage` control response
|
|
7
|
+
* (`SDKControlGetUsageResponse`), which is explicitly marked EXPERIMENTAL by
|
|
8
|
+
* the SDK. Everything here is therefore written defensively: each window is
|
|
9
|
+
* optional, the whole `rate_limits` block can be null (API-key, Bedrock and
|
|
10
|
+
* Vertex sessions have no plan limits at all), and a shape change must degrade
|
|
11
|
+
* to "fewer meters" rather than throw.
|
|
12
|
+
*
|
|
13
|
+
* When the experimental call is unavailable the worker still reports the
|
|
14
|
+
* stable `modelUsage` / `total_cost_usd` result fields, which render as session
|
|
15
|
+
* cost details with no plan meters.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
SOURCE_CACHE,
|
|
20
|
+
SOURCE_WORKER,
|
|
21
|
+
STATUS_NOT_CONFIGURED,
|
|
22
|
+
STATUS_OK,
|
|
23
|
+
STATUS_PARTIAL,
|
|
24
|
+
STATUS_UNAVAILABLE,
|
|
25
|
+
buildDetailSection,
|
|
26
|
+
buildMeter,
|
|
27
|
+
buildProviderCard,
|
|
28
|
+
buildUnavailableCard,
|
|
29
|
+
clampPercent,
|
|
30
|
+
roundCurrency,
|
|
31
|
+
toFiniteNumber,
|
|
32
|
+
toIsoTimestamp,
|
|
33
|
+
toTrimmedString,
|
|
34
|
+
} from './plan-usage-contract.mjs';
|
|
35
|
+
|
|
36
|
+
export const CLAUDE_PROVIDER_ID = 'claude';
|
|
37
|
+
export const CLAUDE_LABEL = 'Claude';
|
|
38
|
+
|
|
39
|
+
const CLAUDE_USAGE_URL = 'https://claude.ai/settings/usage';
|
|
40
|
+
|
|
41
|
+
// Fixed windows, in display order. Model-scoped windows are appended after
|
|
42
|
+
// these from the server-supplied `model_scoped[]` array.
|
|
43
|
+
const WINDOW_LABELS = [
|
|
44
|
+
['five_hour', 'Current session (5 h)', 'primary'],
|
|
45
|
+
['seven_day', 'Weekly limit', 'primary'],
|
|
46
|
+
['seven_day_sonnet', 'Weekly Sonnet', 'secondary'],
|
|
47
|
+
['seven_day_opus', 'Weekly Opus', 'secondary'],
|
|
48
|
+
['seven_day_oauth_apps', 'Weekly (OAuth apps)', 'secondary'],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function normalizeWindow(raw) {
|
|
52
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
53
|
+
const utilization = clampPercent(raw.utilization);
|
|
54
|
+
const resetsAt = toIsoTimestamp(raw.resets_at);
|
|
55
|
+
if (utilization === null && !resetsAt) return null;
|
|
56
|
+
return { utilization, resetsAt };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeModelUsageMap(rawModelUsage) {
|
|
60
|
+
if (!rawModelUsage || typeof rawModelUsage !== 'object') return [];
|
|
61
|
+
return Object.entries(rawModelUsage)
|
|
62
|
+
.map(([model, usage]) => {
|
|
63
|
+
if (!usage || typeof usage !== 'object') return null;
|
|
64
|
+
return {
|
|
65
|
+
model: toTrimmedString(model),
|
|
66
|
+
inputTokens: toFiniteNumber(usage.inputTokens),
|
|
67
|
+
outputTokens: toFiniteNumber(usage.outputTokens),
|
|
68
|
+
cacheReadTokens: toFiniteNumber(usage.cacheReadInputTokens),
|
|
69
|
+
cacheWriteTokens: toFiniteNumber(usage.cacheCreationInputTokens),
|
|
70
|
+
webSearchRequests: toFiniteNumber(usage.webSearchRequests),
|
|
71
|
+
costUsd: toFiniteNumber(usage.costUSD),
|
|
72
|
+
contextWindow: toFiniteNumber(usage.contextWindow),
|
|
73
|
+
};
|
|
74
|
+
})
|
|
75
|
+
.filter((entry) => entry && entry.model);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeAttribution(list) {
|
|
79
|
+
return (Array.isArray(list) ? list : [])
|
|
80
|
+
.map((entry) => {
|
|
81
|
+
const name = toTrimmedString(entry?.name) || toTrimmedString(entry?.key);
|
|
82
|
+
const pct = clampPercent(entry?.pct);
|
|
83
|
+
if (!name || pct === null) return null;
|
|
84
|
+
return { name, pct, count: toFiniteNumber(entry?.count) };
|
|
85
|
+
})
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.sort((a, b) => b.pct - a.pct);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeBehaviorWindow(raw) {
|
|
91
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
92
|
+
const window = {
|
|
93
|
+
requestCount: toFiniteNumber(raw.request_count),
|
|
94
|
+
sessionCount: toFiniteNumber(raw.session_count),
|
|
95
|
+
behaviors: normalizeAttribution(raw.behaviors),
|
|
96
|
+
agents: normalizeAttribution(raw.agents),
|
|
97
|
+
skills: normalizeAttribution(raw.skills),
|
|
98
|
+
plugins: normalizeAttribution(raw.plugins),
|
|
99
|
+
mcpServers: normalizeAttribution(raw.mcp_servers),
|
|
100
|
+
};
|
|
101
|
+
const hasSignal = window.requestCount !== null
|
|
102
|
+
|| window.sessionCount !== null
|
|
103
|
+
|| window.behaviors.length
|
|
104
|
+
|| window.agents.length
|
|
105
|
+
|| window.skills.length
|
|
106
|
+
|| window.plugins.length
|
|
107
|
+
|| window.mcpServers.length;
|
|
108
|
+
return hasSignal ? window : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Normalize the raw experimental response into the payload the relay persists.
|
|
113
|
+
* Returns null when nothing usable is present, so callers never store noise.
|
|
114
|
+
*/
|
|
115
|
+
export function normalizeClaudePlanUsage(raw) {
|
|
116
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
117
|
+
|
|
118
|
+
const rawSession = raw.session && typeof raw.session === 'object' ? raw.session : null;
|
|
119
|
+
const session = rawSession
|
|
120
|
+
? {
|
|
121
|
+
totalCostUsd: toFiniteNumber(rawSession.total_cost_usd),
|
|
122
|
+
totalApiDurationMs: toFiniteNumber(rawSession.total_api_duration_ms),
|
|
123
|
+
totalDurationMs: toFiniteNumber(rawSession.total_duration_ms),
|
|
124
|
+
totalLinesAdded: toFiniteNumber(rawSession.total_lines_added),
|
|
125
|
+
totalLinesRemoved: toFiniteNumber(rawSession.total_lines_removed),
|
|
126
|
+
modelUsage: normalizeModelUsageMap(rawSession.model_usage),
|
|
127
|
+
}
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
const rawLimits = raw.rate_limits && typeof raw.rate_limits === 'object' ? raw.rate_limits : null;
|
|
131
|
+
const windows = [];
|
|
132
|
+
if (rawLimits) {
|
|
133
|
+
for (const [key, label, emphasis] of WINDOW_LABELS) {
|
|
134
|
+
const normalized = normalizeWindow(rawLimits[key]);
|
|
135
|
+
if (normalized) windows.push({ id: key, label, emphasis, ...normalized });
|
|
136
|
+
}
|
|
137
|
+
for (const entry of (Array.isArray(rawLimits.model_scoped) ? rawLimits.model_scoped : [])) {
|
|
138
|
+
const normalized = normalizeWindow(entry);
|
|
139
|
+
const displayName = toTrimmedString(entry?.display_name);
|
|
140
|
+
if (!normalized || !displayName) continue;
|
|
141
|
+
windows.push({
|
|
142
|
+
id: `model_scoped:${displayName.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
|
|
143
|
+
label: `Weekly ${displayName}`,
|
|
144
|
+
emphasis: 'secondary',
|
|
145
|
+
...normalized,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const rawExtra = rawLimits?.extra_usage && typeof rawLimits.extra_usage === 'object'
|
|
151
|
+
? rawLimits.extra_usage
|
|
152
|
+
: null;
|
|
153
|
+
const extraUsage = rawExtra
|
|
154
|
+
? {
|
|
155
|
+
isEnabled: rawExtra.is_enabled === true,
|
|
156
|
+
monthlyLimit: toFiniteNumber(rawExtra.monthly_limit),
|
|
157
|
+
usedCredits: toFiniteNumber(rawExtra.used_credits),
|
|
158
|
+
utilization: clampPercent(rawExtra.utilization),
|
|
159
|
+
currency: toTrimmedString(rawExtra.currency),
|
|
160
|
+
}
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
const rawBehaviors = raw.behaviors && typeof raw.behaviors === 'object' ? raw.behaviors : null;
|
|
164
|
+
const behaviors = rawBehaviors
|
|
165
|
+
? {
|
|
166
|
+
day: normalizeBehaviorWindow(rawBehaviors.day),
|
|
167
|
+
week: normalizeBehaviorWindow(rawBehaviors.week),
|
|
168
|
+
}
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
const payload = {
|
|
172
|
+
subscriptionType: toTrimmedString(raw.subscription_type),
|
|
173
|
+
rateLimitsAvailable: raw.rate_limits_available === true,
|
|
174
|
+
windows,
|
|
175
|
+
extraUsage,
|
|
176
|
+
session,
|
|
177
|
+
behaviors: behaviors && (behaviors.day || behaviors.week) ? behaviors : null,
|
|
178
|
+
};
|
|
179
|
+
const hasSignal = payload.windows.length
|
|
180
|
+
|| payload.extraUsage
|
|
181
|
+
|| payload.session
|
|
182
|
+
|| payload.behaviors
|
|
183
|
+
|| payload.subscriptionType;
|
|
184
|
+
return hasSignal ? payload : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Build the fallback payload from the stable result-message fields. */
|
|
188
|
+
export function claudePlanUsageFromResult({ modelUsage = null, totalCostUsd = null } = {}) {
|
|
189
|
+
const models = normalizeModelUsageMap(modelUsage);
|
|
190
|
+
const cost = toFiniteNumber(totalCostUsd);
|
|
191
|
+
if (!models.length && cost === null) return null;
|
|
192
|
+
return {
|
|
193
|
+
subscriptionType: null,
|
|
194
|
+
rateLimitsAvailable: false,
|
|
195
|
+
windows: [],
|
|
196
|
+
extraUsage: null,
|
|
197
|
+
session: {
|
|
198
|
+
totalCostUsd: cost,
|
|
199
|
+
totalApiDurationMs: null,
|
|
200
|
+
totalDurationMs: null,
|
|
201
|
+
totalLinesAdded: null,
|
|
202
|
+
totalLinesRemoved: null,
|
|
203
|
+
modelUsage: models,
|
|
204
|
+
},
|
|
205
|
+
behaviors: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatDuration(ms) {
|
|
210
|
+
const value = toFiniteNumber(ms);
|
|
211
|
+
if (value === null || value < 0) return null;
|
|
212
|
+
const totalSeconds = Math.round(value / 1000);
|
|
213
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
214
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
215
|
+
const seconds = totalSeconds % 60;
|
|
216
|
+
if (minutes < 60) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
217
|
+
const hours = Math.floor(minutes / 60);
|
|
218
|
+
return `${hours}h ${minutes % 60}m`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function formatTokens(value) {
|
|
222
|
+
const numeric = toFiniteNumber(value);
|
|
223
|
+
if (numeric === null) return null;
|
|
224
|
+
const abs = Math.abs(numeric);
|
|
225
|
+
if (abs < 1000) return String(Math.round(numeric));
|
|
226
|
+
if (abs < 1_000_000) return `${(numeric / 1000).toFixed(1)}k`;
|
|
227
|
+
return `${(numeric / 1_000_000).toFixed(1)}M`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatUsd(value) {
|
|
231
|
+
const numeric = roundCurrency(value);
|
|
232
|
+
return numeric === null ? null : `$${numeric.toFixed(2)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function attributionRows(entries, limit = 6) {
|
|
236
|
+
return entries.slice(0, limit).map((entry) => ({
|
|
237
|
+
label: entry.name,
|
|
238
|
+
value: `${entry.pct.toFixed(1)}%`,
|
|
239
|
+
hint: entry.count === null ? null : `${entry.count} requests`,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function behaviorSections(behaviors) {
|
|
244
|
+
if (!behaviors) return [];
|
|
245
|
+
const sections = [];
|
|
246
|
+
for (const [key, window, windowLabel] of [
|
|
247
|
+
['day', behaviors.day, 'Last 24 hours'],
|
|
248
|
+
['week', behaviors.week, 'Last 7 days'],
|
|
249
|
+
]) {
|
|
250
|
+
if (!window) continue;
|
|
251
|
+
const rows = [];
|
|
252
|
+
if (window.requestCount !== null) rows.push({ label: 'API requests', value: String(window.requestCount) });
|
|
253
|
+
if (window.sessionCount !== null) rows.push({ label: 'Sessions', value: String(window.sessionCount) });
|
|
254
|
+
for (const entry of window.behaviors.slice(0, 6)) {
|
|
255
|
+
rows.push({ label: entry.name, value: `${entry.pct.toFixed(1)}%`, hint: entry.count === null ? null : `${entry.count} requests` });
|
|
256
|
+
}
|
|
257
|
+
sections.push(buildDetailSection({
|
|
258
|
+
id: `claude-behaviors-${key}`,
|
|
259
|
+
label: `What is driving usage — ${windowLabel}`,
|
|
260
|
+
// The SDK is explicit that this comes from a local transcript scan, so it
|
|
261
|
+
// must not be presented as an account-wide figure.
|
|
262
|
+
note: 'Approximate, from local transcripts on this machine only. Categories overlap.',
|
|
263
|
+
rows,
|
|
264
|
+
}));
|
|
265
|
+
|
|
266
|
+
for (const [attrKey, attrLabel, list] of [
|
|
267
|
+
['agents', 'Agents', window.agents],
|
|
268
|
+
['skills', 'Skills', window.skills],
|
|
269
|
+
['plugins', 'Plugins', window.plugins],
|
|
270
|
+
['mcp', 'MCP servers', window.mcpServers],
|
|
271
|
+
]) {
|
|
272
|
+
if (!list.length) continue;
|
|
273
|
+
sections.push(buildDetailSection({
|
|
274
|
+
id: `claude-${attrKey}-${key}`,
|
|
275
|
+
label: `${attrLabel} — ${windowLabel}`,
|
|
276
|
+
rows: attributionRows(list),
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return sections;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function buildClaudePlanCard({
|
|
284
|
+
usage = null,
|
|
285
|
+
capturedAt = null,
|
|
286
|
+
stale = false,
|
|
287
|
+
configured = true,
|
|
288
|
+
message = null,
|
|
289
|
+
} = {}) {
|
|
290
|
+
if (!configured) {
|
|
291
|
+
return buildUnavailableCard({
|
|
292
|
+
provider: CLAUDE_PROVIDER_ID,
|
|
293
|
+
label: CLAUDE_LABEL,
|
|
294
|
+
status: STATUS_NOT_CONFIGURED,
|
|
295
|
+
message: 'Claude is not enabled in provider settings.',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
if (!usage) {
|
|
299
|
+
return buildUnavailableCard({
|
|
300
|
+
provider: CLAUDE_PROVIDER_ID,
|
|
301
|
+
label: CLAUDE_LABEL,
|
|
302
|
+
status: STATUS_UNAVAILABLE,
|
|
303
|
+
message: toTrimmedString(message)
|
|
304
|
+
|| 'No Claude usage captured yet. Run a Claude turn — usage is read from the live session and never from a hidden extra turn.',
|
|
305
|
+
links: [{ label: 'Claude usage settings', url: CLAUDE_USAGE_URL }],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const meters = [];
|
|
310
|
+
for (const window of (Array.isArray(usage.windows) ? usage.windows : [])) {
|
|
311
|
+
meters.push(buildMeter({
|
|
312
|
+
id: `claude-${window.id}`,
|
|
313
|
+
label: window.label,
|
|
314
|
+
unit: 'percent',
|
|
315
|
+
utilization: window.utilization,
|
|
316
|
+
resetAt: window.resetsAt,
|
|
317
|
+
emphasis: window.emphasis === 'secondary' ? 'secondary' : 'primary',
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const extra = usage.extraUsage;
|
|
322
|
+
if (extra && (extra.isEnabled || extra.usedCredits !== null || extra.monthlyLimit !== null)) {
|
|
323
|
+
meters.push(buildMeter({
|
|
324
|
+
id: 'claude-extra-usage',
|
|
325
|
+
label: 'Extra usage credits',
|
|
326
|
+
unit: extra.currency && extra.currency.toUpperCase() !== 'USD' ? 'credits' : 'usd',
|
|
327
|
+
used: extra.usedCredits,
|
|
328
|
+
allowance: extra.monthlyLimit,
|
|
329
|
+
utilization: extra.utilization,
|
|
330
|
+
note: extra.isEnabled ? null : 'Extra usage is disabled for this account',
|
|
331
|
+
emphasis: 'secondary',
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const details = [];
|
|
336
|
+
const session = usage.session;
|
|
337
|
+
if (session) {
|
|
338
|
+
const rows = [];
|
|
339
|
+
const cost = formatUsd(session.totalCostUsd);
|
|
340
|
+
if (cost) rows.push({ label: 'Session cost (estimate)', value: cost, hint: 'Client-side estimate, not a billing statement' });
|
|
341
|
+
const apiDuration = formatDuration(session.totalApiDurationMs);
|
|
342
|
+
if (apiDuration) rows.push({ label: 'API time', value: apiDuration });
|
|
343
|
+
const wallDuration = formatDuration(session.totalDurationMs);
|
|
344
|
+
if (wallDuration) rows.push({ label: 'Wall-clock time', value: wallDuration });
|
|
345
|
+
if (session.totalLinesAdded !== null) rows.push({ label: 'Lines added', value: String(session.totalLinesAdded) });
|
|
346
|
+
if (session.totalLinesRemoved !== null) rows.push({ label: 'Lines removed', value: String(session.totalLinesRemoved) });
|
|
347
|
+
const sessionSection = buildDetailSection({ id: 'claude-session', label: 'Latest session totals', rows });
|
|
348
|
+
if (sessionSection) details.push(sessionSection);
|
|
349
|
+
|
|
350
|
+
const modelRows = (session.modelUsage || []).map((entry) => ({
|
|
351
|
+
label: entry.model,
|
|
352
|
+
value: formatUsd(entry.costUsd) ?? '—',
|
|
353
|
+
hint: [
|
|
354
|
+
formatTokens(entry.inputTokens) ? `in ${formatTokens(entry.inputTokens)}` : null,
|
|
355
|
+
formatTokens(entry.outputTokens) ? `out ${formatTokens(entry.outputTokens)}` : null,
|
|
356
|
+
formatTokens(entry.cacheReadTokens) ? `cache r ${formatTokens(entry.cacheReadTokens)}` : null,
|
|
357
|
+
formatTokens(entry.cacheWriteTokens) ? `cache w ${formatTokens(entry.cacheWriteTokens)}` : null,
|
|
358
|
+
].filter(Boolean).join(' · ') || null,
|
|
359
|
+
}));
|
|
360
|
+
const modelSection = buildDetailSection({ id: 'claude-models', label: 'By model (session)', rows: modelRows });
|
|
361
|
+
if (modelSection) details.push(modelSection);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
details.push(...behaviorSections(usage.behaviors).filter(Boolean));
|
|
365
|
+
|
|
366
|
+
const resolvedMeters = meters.filter(Boolean);
|
|
367
|
+
const planLimitsMissing = !resolvedMeters.length;
|
|
368
|
+
return buildProviderCard({
|
|
369
|
+
provider: CLAUDE_PROVIDER_ID,
|
|
370
|
+
label: CLAUDE_LABEL,
|
|
371
|
+
status: planLimitsMissing ? STATUS_PARTIAL : STATUS_OK,
|
|
372
|
+
planName: usage.subscriptionType
|
|
373
|
+
? `${usage.subscriptionType.charAt(0).toUpperCase()}${usage.subscriptionType.slice(1)}`
|
|
374
|
+
: null,
|
|
375
|
+
message: planLimitsMissing
|
|
376
|
+
? (usage.rateLimitsAvailable
|
|
377
|
+
? 'Plan rate limits were not reported for this session.'
|
|
378
|
+
: 'This session authenticates without claude.ai plan limits (API key or third-party provider), so only session cost is available.')
|
|
379
|
+
: null,
|
|
380
|
+
source: stale ? SOURCE_CACHE : SOURCE_WORKER,
|
|
381
|
+
stale,
|
|
382
|
+
capturedAt,
|
|
383
|
+
meters: resolvedMeters,
|
|
384
|
+
details,
|
|
385
|
+
links: [{ label: 'Claude usage settings', url: CLAUDE_USAGE_URL }],
|
|
386
|
+
});
|
|
387
|
+
}
|