@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,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { parseAutoCompactWindow } from '../../shared/auto-compact-window.mjs';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_THINKING_DISPLAY,
|
|
6
|
+
DEFAULT_THINKING_ENABLED,
|
|
7
|
+
parseThinkingDisplay,
|
|
8
|
+
parseThinkingEnabled,
|
|
9
|
+
} from '../../shared/claude-thinking.mjs';
|
|
10
|
+
|
|
11
|
+
function normalizeModeWith(value, normalizeMode) {
|
|
12
|
+
if (typeof normalizeMode === 'function') {
|
|
13
|
+
const normalized = normalizeMode(value);
|
|
14
|
+
const text = String(normalized || '').trim();
|
|
15
|
+
return text || null;
|
|
16
|
+
}
|
|
17
|
+
const text = String(value || '').trim();
|
|
18
|
+
return text || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizePreferredModel(value) {
|
|
22
|
+
return String(value || '').trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizePreferredReasoningEffort(value) {
|
|
26
|
+
return String(value || '').trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function persistConversationPreferences({
|
|
30
|
+
db,
|
|
31
|
+
stmts,
|
|
32
|
+
conversationId = '',
|
|
33
|
+
preferredRelayMode = '',
|
|
34
|
+
preferredModel = '',
|
|
35
|
+
preferredReasoningEffort = '',
|
|
36
|
+
// `undefined` means "not part of this write" — the composer PATCHes the same
|
|
37
|
+
// route without ever mentioning the window, and must not clear it.
|
|
38
|
+
autoCompactWindow = undefined,
|
|
39
|
+
// Same contract for the thinking control: `undefined` leaves the stored
|
|
40
|
+
// value alone. Anything else is resolved through the shared parsers, whose
|
|
41
|
+
// fallback is the relay default (on / summarized).
|
|
42
|
+
thinkingEnabled = undefined,
|
|
43
|
+
thinkingDisplay = undefined,
|
|
44
|
+
updatedAt = new Date().toISOString(),
|
|
45
|
+
createIfMissing = false,
|
|
46
|
+
createTitle = 'Session',
|
|
47
|
+
tolerateMissingColumns = false,
|
|
48
|
+
} = {}) {
|
|
49
|
+
const convId = String(conversationId || '').trim();
|
|
50
|
+
const mode = String(preferredRelayMode || '').trim();
|
|
51
|
+
const model = normalizePreferredModel(preferredModel);
|
|
52
|
+
const reasoningEffort = normalizePreferredReasoningEffort(preferredReasoningEffort);
|
|
53
|
+
const writesAutoCompactWindow = autoCompactWindow !== undefined;
|
|
54
|
+
const window = writesAutoCompactWindow ? parseAutoCompactWindow(autoCompactWindow) : null;
|
|
55
|
+
const writesThinkingEnabled = thinkingEnabled !== undefined;
|
|
56
|
+
const enabled = writesThinkingEnabled ? parseThinkingEnabled(thinkingEnabled) : DEFAULT_THINKING_ENABLED;
|
|
57
|
+
const writesThinkingDisplay = thinkingDisplay !== undefined;
|
|
58
|
+
const display = writesThinkingDisplay ? parseThinkingDisplay(thinkingDisplay) : null;
|
|
59
|
+
if (!db || !stmts || !convId || !mode) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
created: false,
|
|
63
|
+
preferredRelayMode: mode,
|
|
64
|
+
preferredModel: '',
|
|
65
|
+
preferredReasoningEffort: '',
|
|
66
|
+
autoCompactWindow: null,
|
|
67
|
+
thinkingEnabled: DEFAULT_THINKING_ENABLED,
|
|
68
|
+
thinkingDisplay: DEFAULT_THINKING_DISPLAY,
|
|
69
|
+
updatedAt,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const safeTitle = String(createTitle || '').trim() || 'Session';
|
|
73
|
+
const writePreferences = db.transaction(() => {
|
|
74
|
+
const existing = typeof stmts.getConvAnyStatus?.get === 'function'
|
|
75
|
+
? (stmts.getConvAnyStatus.get(convId) || null)
|
|
76
|
+
: null;
|
|
77
|
+
if (!existing && createIfMissing && typeof stmts.insertConv?.run === 'function') {
|
|
78
|
+
stmts.insertConv.run(convId, safeTitle, updatedAt, updatedAt);
|
|
79
|
+
}
|
|
80
|
+
// Extra per-conversation setting columns this write mentions. The prepared
|
|
81
|
+
// statement can't carry them, and these writes are rare (one control
|
|
82
|
+
// change each) — the dynamic form keeps the hot composer path on the
|
|
83
|
+
// prepared statement.
|
|
84
|
+
const extraSets = [];
|
|
85
|
+
const extraValues = [];
|
|
86
|
+
if (writesAutoCompactWindow) {
|
|
87
|
+
extraSets.push('auto_compact_window = ?');
|
|
88
|
+
extraValues.push(window);
|
|
89
|
+
}
|
|
90
|
+
// Both axes store the RESOLVED value, never NULL-as-default: NULL means
|
|
91
|
+
// "never set" and is read back as the relay default, so canonicalizing a
|
|
92
|
+
// default choice to NULL would silently re-point those rows if the relay
|
|
93
|
+
// default ever changed.
|
|
94
|
+
if (writesThinkingEnabled) {
|
|
95
|
+
extraSets.push('thinking_enabled = ?');
|
|
96
|
+
extraValues.push(enabled ? 1 : 0);
|
|
97
|
+
}
|
|
98
|
+
if (writesThinkingDisplay) {
|
|
99
|
+
extraSets.push('thinking_display = ?');
|
|
100
|
+
extraValues.push(display);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
if (extraSets.length) {
|
|
104
|
+
db.prepare(`
|
|
105
|
+
UPDATE conversations
|
|
106
|
+
SET preferred_relay_mode = ?, preferred_model = ?, preferred_reasoning_effort = ?,
|
|
107
|
+
${extraSets.join(', ')}, updated_at = ?
|
|
108
|
+
WHERE id = ?
|
|
109
|
+
`).run(mode, model || null, reasoningEffort || null, ...extraValues, updatedAt, convId);
|
|
110
|
+
} else if (typeof stmts.updateConvPreferences?.run === 'function') {
|
|
111
|
+
stmts.updateConvPreferences.run(mode, model || null, reasoningEffort || null, updatedAt, convId);
|
|
112
|
+
} else {
|
|
113
|
+
db.prepare(`
|
|
114
|
+
UPDATE conversations
|
|
115
|
+
SET preferred_relay_mode = ?, preferred_model = ?, preferred_reasoning_effort = ?, updated_at = ?
|
|
116
|
+
WHERE id = ?
|
|
117
|
+
`).run(mode, model || null, reasoningEffort || null, updatedAt, convId);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!tolerateMissingColumns) throw error;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
created: !existing,
|
|
125
|
+
preferredRelayMode: mode,
|
|
126
|
+
preferredModel: model,
|
|
127
|
+
preferredReasoningEffort: reasoningEffort,
|
|
128
|
+
// Always echoed, so a write that didn't touch the window still tells the
|
|
129
|
+
// client what the stored value is.
|
|
130
|
+
autoCompactWindow: writesAutoCompactWindow
|
|
131
|
+
? window
|
|
132
|
+
: parseAutoCompactWindow(existing?.auto_compact_window),
|
|
133
|
+
thinkingEnabled: writesThinkingEnabled
|
|
134
|
+
? enabled
|
|
135
|
+
: parseThinkingEnabled(existing?.thinking_enabled),
|
|
136
|
+
thinkingDisplay: writesThinkingDisplay
|
|
137
|
+
? display
|
|
138
|
+
: parseThinkingDisplay(existing?.thinking_display),
|
|
139
|
+
updatedAt,
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
return writePreferences();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function persistConversationModelPreference({
|
|
146
|
+
db,
|
|
147
|
+
stmts,
|
|
148
|
+
conversationId = '',
|
|
149
|
+
relayMode = '',
|
|
150
|
+
model = '',
|
|
151
|
+
reasoningEffort = '',
|
|
152
|
+
normalizeMode,
|
|
153
|
+
fallbackRelayMode = 'agent',
|
|
154
|
+
updatedAt = new Date().toISOString(),
|
|
155
|
+
createIfMissing = false,
|
|
156
|
+
createTitle = 'Session',
|
|
157
|
+
tolerateMissingColumns = false,
|
|
158
|
+
} = {}) {
|
|
159
|
+
const convId = String(conversationId || '').trim();
|
|
160
|
+
const modelText = normalizePreferredModel(model);
|
|
161
|
+
const normalizedRelayMode = normalizeModeWith(relayMode, normalizeMode);
|
|
162
|
+
const fallbackMode = normalizeModeWith(fallbackRelayMode, normalizeMode) || String(fallbackRelayMode || '').trim() || 'agent';
|
|
163
|
+
const mode = normalizedRelayMode || fallbackMode;
|
|
164
|
+
const effortText = normalizePreferredReasoningEffort(reasoningEffort);
|
|
165
|
+
if (!db || !stmts || !convId || !mode || !modelText) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
created: false,
|
|
169
|
+
preferredRelayMode: mode || fallbackMode,
|
|
170
|
+
preferredModel: '',
|
|
171
|
+
preferredReasoningEffort: '',
|
|
172
|
+
updatedAt,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const safeTitle = String(createTitle || '').trim() || 'Session';
|
|
177
|
+
const writeModelPreference = db.transaction(() => {
|
|
178
|
+
const existing = typeof stmts.getConvAnyStatus?.get === 'function'
|
|
179
|
+
? (stmts.getConvAnyStatus.get(convId) || null)
|
|
180
|
+
: null;
|
|
181
|
+
if (!existing && createIfMissing && typeof stmts.insertConv?.run === 'function') {
|
|
182
|
+
stmts.insertConv.run(convId, safeTitle, updatedAt, updatedAt);
|
|
183
|
+
}
|
|
184
|
+
const effort = effortText || normalizePreferredReasoningEffort(existing?.preferred_reasoning_effort);
|
|
185
|
+
try {
|
|
186
|
+
if (typeof stmts.updateConvPreferences?.run === 'function') {
|
|
187
|
+
stmts.updateConvPreferences.run(mode, modelText, effort || null, updatedAt, convId);
|
|
188
|
+
} else {
|
|
189
|
+
db.prepare(`
|
|
190
|
+
UPDATE conversations
|
|
191
|
+
SET preferred_relay_mode = ?, preferred_model = ?, preferred_reasoning_effort = ?, updated_at = ?
|
|
192
|
+
WHERE id = ?
|
|
193
|
+
`).run(mode, modelText, effort || null, updatedAt, convId);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (!tolerateMissingColumns) throw error;
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
created: !existing,
|
|
201
|
+
preferredRelayMode: mode,
|
|
202
|
+
preferredModel: modelText,
|
|
203
|
+
preferredReasoningEffort: effort,
|
|
204
|
+
updatedAt,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
return writeModelPreference();
|
|
208
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Live Cursor plan quota for the Check Usage card.
|
|
5
|
+
*
|
|
6
|
+
* The Cursor API key exposes no plan/usage surface (`/v1/agents/:id/usage` is
|
|
7
|
+
* feature-gated, everything else 404s). The percentages shown on
|
|
8
|
+
* cursor.com/dashboard ("Included in Ultra": Total / Auto / API) come from the
|
|
9
|
+
* dashboard's own web API, which authenticates with the browser session
|
|
10
|
+
* cookie:
|
|
11
|
+
*
|
|
12
|
+
* POST https://cursor.com/api/dashboard/get-current-period-usage
|
|
13
|
+
* Cookie: WorkosCursorSessionToken=<token>
|
|
14
|
+
* Origin: https://cursor.com
|
|
15
|
+
*
|
|
16
|
+
* The token resolves automatically from the relay host's Cursor IDE login
|
|
17
|
+
* (state.vscdb `cursorAuth/accessToken`, same host-login trust model as the
|
|
18
|
+
* Grok CLI's auth.json) — the cookie is `<sub-user-id>::<access JWT>`. A
|
|
19
|
+
* manually pasted cookie (DevTools → Application → Cookies → cursor.com)
|
|
20
|
+
* takes precedence for headless hosts without an IDE install. On 401/expiry
|
|
21
|
+
* the card silently degrades to the local estimated view. Tokens never leave
|
|
22
|
+
* the relay and are never echoed back to clients.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import os from 'node:os';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
|
|
30
|
+
const requireModule = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
export const CURSOR_DASHBOARD_USAGE_URL = 'https://cursor.com/api/dashboard/get-current-period-usage';
|
|
33
|
+
const FETCH_TIMEOUT_MS = 6_000;
|
|
34
|
+
// The IDE access token lives for months; re-reading the multi-MB state.vscdb
|
|
35
|
+
// on every Check Usage open would be wasteful.
|
|
36
|
+
const IDE_TOKEN_CACHE_MS = 5 * 60_000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default locations of the Cursor IDE's state database per platform.
|
|
40
|
+
*/
|
|
41
|
+
export function defaultCursorStateDbPath({ platform = process.platform, env = process.env, homeDir = os.homedir() } = {}) {
|
|
42
|
+
if (platform === 'win32') {
|
|
43
|
+
const appData = String(env.APPDATA || '').trim() || path.join(homeDir, 'AppData', 'Roaming');
|
|
44
|
+
return path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
45
|
+
}
|
|
46
|
+
if (platform === 'darwin') {
|
|
47
|
+
return path.join(homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
48
|
+
}
|
|
49
|
+
return path.join(homeDir, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the dashboard session cookie value from an IDE access token:
|
|
54
|
+
* `WorkosCursorSessionToken=<userId>%3A%3A<jwt>` where userId is the JWT
|
|
55
|
+
* `sub` claim with its provider prefix stripped. Pure; null when the token
|
|
56
|
+
* is malformed or expired.
|
|
57
|
+
*/
|
|
58
|
+
export function sessionTokenFromAccessToken(accessToken, { now = () => Date.now() } = {}) {
|
|
59
|
+
const token = String(accessToken || '').trim();
|
|
60
|
+
const parts = token.split('.');
|
|
61
|
+
if (parts.length !== 3) return null;
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const sub = String(payload?.sub || '').trim();
|
|
69
|
+
if (!sub) return null;
|
|
70
|
+
const exp = Number(payload?.exp);
|
|
71
|
+
if (Number.isFinite(exp) && exp * 1000 <= now()) return null;
|
|
72
|
+
const userId = sub.includes('|') ? sub.split('|').pop() : sub;
|
|
73
|
+
return `${userId}%3A%3A${token}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read the Cursor IDE's login from state.vscdb and build the dashboard
|
|
78
|
+
* session token from it — the same host-login trust model as the Grok CLI's
|
|
79
|
+
* auth.json. The db is opened READ-ONLY in place: it can be multiple GB, so
|
|
80
|
+
* copying it is not an option, and SQLite's WAL mode supports concurrent
|
|
81
|
+
* readers next to the running IDE. Never throws; null when the IDE is
|
|
82
|
+
* absent, logged out, or the db is momentarily locked.
|
|
83
|
+
*/
|
|
84
|
+
export function readCursorIdeSessionToken({
|
|
85
|
+
stateDbPath = defaultCursorStateDbPath(),
|
|
86
|
+
fsImpl = fs,
|
|
87
|
+
loadDatabase = null,
|
|
88
|
+
now = () => Date.now(),
|
|
89
|
+
} = {}) {
|
|
90
|
+
try {
|
|
91
|
+
if (!fsImpl.existsSync(stateDbPath)) return null;
|
|
92
|
+
// Lazy-required so importing this module never pays the native-module cost.
|
|
93
|
+
const openDatabase = loadDatabase
|
|
94
|
+
|| ((file) => requireModule('better-sqlite3')(file, { readonly: true, fileMustExist: true, timeout: 1500 }));
|
|
95
|
+
const db = openDatabase(stateDbPath);
|
|
96
|
+
try {
|
|
97
|
+
const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'").get();
|
|
98
|
+
return sessionTokenFromAccessToken(row?.value, { now });
|
|
99
|
+
} finally {
|
|
100
|
+
try { db.close(); } catch { /* ignore */ }
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toPercent(value) {
|
|
108
|
+
const numeric = Number(value);
|
|
109
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toCents(value) {
|
|
113
|
+
const numeric = Number(value);
|
|
114
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toIso(value) {
|
|
118
|
+
if (value === null || value === undefined || value === '') return null;
|
|
119
|
+
// The endpoint has shipped both ISO strings and unix-ms strings.
|
|
120
|
+
const asNumber = Number(value);
|
|
121
|
+
const timestamp = Number.isFinite(asNumber) && asNumber > 10_000_000_000
|
|
122
|
+
? asNumber
|
|
123
|
+
: Date.parse(String(value));
|
|
124
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Normalize the dashboard payload into the fields the plan card needs.
|
|
129
|
+
* Handles both the flat `planUsage` shape and the older
|
|
130
|
+
* `individualUsage.plan` shape from /api/usage-summary. Pure; null when
|
|
131
|
+
* nothing usable is present.
|
|
132
|
+
*/
|
|
133
|
+
export function normalizeCursorDashboardUsage(payload = null) {
|
|
134
|
+
if (!payload || typeof payload !== 'object') return null;
|
|
135
|
+
const plan = (payload.planUsage && typeof payload.planUsage === 'object')
|
|
136
|
+
? payload.planUsage
|
|
137
|
+
: (payload.individualUsage?.plan && typeof payload.individualUsage.plan === 'object'
|
|
138
|
+
? payload.individualUsage.plan
|
|
139
|
+
: null);
|
|
140
|
+
if (!plan) return null;
|
|
141
|
+
|
|
142
|
+
const normalized = {
|
|
143
|
+
totalPercentUsed: toPercent(plan.totalPercentUsed),
|
|
144
|
+
autoPercentUsed: toPercent(plan.autoPercentUsed),
|
|
145
|
+
apiPercentUsed: toPercent(plan.apiPercentUsed),
|
|
146
|
+
includedSpendCents: toCents(plan.includedSpend ?? plan.totalSpend),
|
|
147
|
+
limitCents: toCents(plan.limit),
|
|
148
|
+
remainingCents: toCents(plan.remaining),
|
|
149
|
+
billingCycleStart: toIso(payload.billingCycleStart),
|
|
150
|
+
billingCycleEnd: toIso(payload.billingCycleEnd),
|
|
151
|
+
membershipType: String(payload.membershipType || '').trim() || null,
|
|
152
|
+
displayMessage: String(payload.displayMessage || '').trim() || null,
|
|
153
|
+
onDemand: (() => {
|
|
154
|
+
const spend = payload.spendLimitUsage && typeof payload.spendLimitUsage === 'object'
|
|
155
|
+
? payload.spendLimitUsage
|
|
156
|
+
: (payload.individualUsage?.onDemand && typeof payload.individualUsage.onDemand === 'object'
|
|
157
|
+
? payload.individualUsage.onDemand
|
|
158
|
+
: null);
|
|
159
|
+
if (!spend) return null;
|
|
160
|
+
const usedCents = toCents(spend.individualUsed ?? spend.totalSpend ?? spend.used);
|
|
161
|
+
const limitCents = toCents(spend.individualLimit ?? spend.limit);
|
|
162
|
+
if (usedCents === null && limitCents === null) return null;
|
|
163
|
+
return { usedCents, limitCents };
|
|
164
|
+
})(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const hasSignal = normalized.totalPercentUsed !== null
|
|
168
|
+
|| normalized.autoPercentUsed !== null
|
|
169
|
+
|| normalized.apiPercentUsed !== null
|
|
170
|
+
|| normalized.includedSpendCents !== null;
|
|
171
|
+
return hasSignal ? normalized : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build the best-effort fetcher the usage route calls. Token resolution: a
|
|
176
|
+
* manually stored session token wins (headless hosts), otherwise the host's
|
|
177
|
+
* Cursor IDE login is used automatically. Errors, missing or expired tokens
|
|
178
|
+
* yield `null` so the card degrades to the estimated view.
|
|
179
|
+
*/
|
|
180
|
+
export function createCursorDashboardUsageFetcher({
|
|
181
|
+
getSessionToken = () => '',
|
|
182
|
+
readIdeTokenImpl = readCursorIdeSessionToken,
|
|
183
|
+
fetchImpl = globalThis.fetch,
|
|
184
|
+
url = CURSOR_DASHBOARD_USAGE_URL,
|
|
185
|
+
timeoutMs = FETCH_TIMEOUT_MS,
|
|
186
|
+
now = () => Date.now(),
|
|
187
|
+
} = {}) {
|
|
188
|
+
let cachedIdeToken = null;
|
|
189
|
+
let cachedIdeTokenAt = 0;
|
|
190
|
+
const resolveIdeToken = () => {
|
|
191
|
+
if (cachedIdeToken && now() - cachedIdeTokenAt < IDE_TOKEN_CACHE_MS) return cachedIdeToken;
|
|
192
|
+
cachedIdeToken = readIdeTokenImpl() || null;
|
|
193
|
+
cachedIdeTokenAt = now();
|
|
194
|
+
return cachedIdeToken;
|
|
195
|
+
};
|
|
196
|
+
return async function fetchCursorDashboardUsage() {
|
|
197
|
+
if (typeof fetchImpl !== 'function') return null;
|
|
198
|
+
const token = String(getSessionToken() || '').trim() || resolveIdeToken();
|
|
199
|
+
if (!token) return null;
|
|
200
|
+
try {
|
|
201
|
+
const response = await fetchImpl(url, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: {
|
|
204
|
+
Cookie: `WorkosCursorSessionToken=${token}`,
|
|
205
|
+
Origin: 'https://cursor.com',
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
},
|
|
208
|
+
body: '{}',
|
|
209
|
+
signal: AbortSignal.timeout?.(timeoutMs),
|
|
210
|
+
});
|
|
211
|
+
if (!response?.ok) return null;
|
|
212
|
+
return normalizeCursorDashboardUsage(await response.json());
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import nodeFs from 'fs';
|
|
4
|
+
import nodePath from 'path';
|
|
5
|
+
|
|
6
|
+
// Resolve the on-disk folder the Cursor SDK integration keeps for one session,
|
|
7
|
+
// so the file explorer's Session root works on `cursor`-provider conversations.
|
|
8
|
+
//
|
|
9
|
+
// The Cursor worker roots its per-session agent store at
|
|
10
|
+
// `<storeDir>/<sdkSessionId>/` (see cursor-session-worker.mjs / the storePath
|
|
11
|
+
// in cursor-sdk-adapter.mjs), where storeDir is `$CURSOR_AGENT_STORE_DIR` or
|
|
12
|
+
// `<serverDir>/data/cursor-agents`. The directory is created on the session's
|
|
13
|
+
// first turn, so a missing directory means "nothing to browse yet" rather than
|
|
14
|
+
// an error — the resolver returns null and the Session button stays disabled
|
|
15
|
+
// until the first turn lands.
|
|
16
|
+
|
|
17
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
18
|
+
|
|
19
|
+
function normalizeText(value) {
|
|
20
|
+
const text = String(value || '').trim();
|
|
21
|
+
return text || '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveCursorAgentStoreDir({
|
|
25
|
+
env = process.env,
|
|
26
|
+
serverDir = '',
|
|
27
|
+
path = nodePath,
|
|
28
|
+
} = {}) {
|
|
29
|
+
const override = normalizeText(env?.CURSOR_AGENT_STORE_DIR);
|
|
30
|
+
if (override) return override;
|
|
31
|
+
const base = normalizeText(serverDir);
|
|
32
|
+
if (!base) return '';
|
|
33
|
+
return path.join(base, 'data', 'cursor-agents');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createCursorSessionRootResolver({
|
|
37
|
+
fs = nodeFs,
|
|
38
|
+
path = nodePath,
|
|
39
|
+
env = process.env,
|
|
40
|
+
serverDir = '',
|
|
41
|
+
} = {}) {
|
|
42
|
+
/**
|
|
43
|
+
* @returns {{ sessionRootPath: string, sessionRootName: string } | null}
|
|
44
|
+
*/
|
|
45
|
+
function resolveCursorSessionRoot({ sdkSessionId = '' } = {}) {
|
|
46
|
+
const sessionId = normalizeText(sdkSessionId);
|
|
47
|
+
// The id is joined into a filesystem path, so it is validated before any
|
|
48
|
+
// syscall (mirrors the Claude session-root resolver).
|
|
49
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) return null;
|
|
50
|
+
const storeDir = resolveCursorAgentStoreDir({ env, serverDir, path });
|
|
51
|
+
if (!storeDir) return null;
|
|
52
|
+
const sessionRootPath = path.join(storeDir, sessionId);
|
|
53
|
+
try {
|
|
54
|
+
if (!fs.statSync(sessionRootPath).isDirectory()) return null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return { sessionRootPath, sessionRootName: 'Session' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { resolveCursorSessionRoot };
|
|
62
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const DEFAULT_DELETE_RETRIES = 3;
|
|
2
|
+
const RETRY_DELAYS_MS = [250, 500];
|
|
3
|
+
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeConversationId(value) {
|
|
9
|
+
return String(value || '').trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createSdkDeleteSessionCaller(sdkClient) {
|
|
13
|
+
if (sdkClient && typeof sdkClient.deleteSession === 'function') {
|
|
14
|
+
return sdkClient.deleteSession.bind(sdkClient);
|
|
15
|
+
}
|
|
16
|
+
let warned = false;
|
|
17
|
+
return async (sessionId) => {
|
|
18
|
+
if (!warned) {
|
|
19
|
+
warned = true;
|
|
20
|
+
console.warn(`[delete-archive] SDK deleteSession() is unavailable; skipping remote delete lifecycle.`);
|
|
21
|
+
}
|
|
22
|
+
return { ok: true, skipped: true, sessionId };
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
import { cleanupGeneratedImagesForConversation } from './generated-image-cleanup-service.mjs';
|
|
27
|
+
|
|
28
|
+
export function createDeleteArchiveService(db, sdkClient, { resolveSessionStateRoot = null } = {}) {
|
|
29
|
+
const callDeleteSession = createSdkDeleteSessionCaller(sdkClient);
|
|
30
|
+
|
|
31
|
+
const stmts = {
|
|
32
|
+
getConversation: db.prepare(`
|
|
33
|
+
SELECT c.id, c.status, c.sdk_session_id
|
|
34
|
+
FROM conversations c
|
|
35
|
+
WHERE c.id = ?
|
|
36
|
+
LIMIT 1
|
|
37
|
+
`),
|
|
38
|
+
markDeleted: db.prepare(`UPDATE conversations SET status = 'deleted', updated_at = ? WHERE id = ?`),
|
|
39
|
+
markArchived: db.prepare(`UPDATE conversations SET archived = 1, updated_at = ? WHERE id = ?`),
|
|
40
|
+
listDeletedWithSdkSession: db.prepare(`
|
|
41
|
+
SELECT c.id, c.sdk_session_id
|
|
42
|
+
FROM conversations c
|
|
43
|
+
WHERE c.status = 'deleted'
|
|
44
|
+
ORDER BY c.updated_at ASC
|
|
45
|
+
`),
|
|
46
|
+
listConversationMessages: db.prepare(`
|
|
47
|
+
SELECT attachments
|
|
48
|
+
FROM messages
|
|
49
|
+
WHERE conversation_id = ?
|
|
50
|
+
ORDER BY timestamp ASC
|
|
51
|
+
`),
|
|
52
|
+
hardDeleteConversation: db.transaction((conversationId) => {
|
|
53
|
+
db.prepare(`DELETE FROM relay_questions WHERE conversation_id = ?`).run(conversationId);
|
|
54
|
+
db.prepare(`DELETE FROM relay_boards WHERE conversation_id = ?`).run(conversationId);
|
|
55
|
+
db.prepare(`DELETE FROM queue WHERE conversation_id = ?`).run(conversationId);
|
|
56
|
+
db.prepare(`DELETE FROM messages WHERE conversation_id = ?`).run(conversationId);
|
|
57
|
+
db.prepare(`DELETE FROM runtime_sessions WHERE conversation_id = ?`).run(conversationId);
|
|
58
|
+
db.prepare(`DELETE FROM conversations WHERE id = ?`).run(conversationId);
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
async function deleteSdkSessionWithRetries(sdkSessionId, conversationId) {
|
|
63
|
+
let lastError = null;
|
|
64
|
+
for (let attempt = 1; attempt <= DEFAULT_DELETE_RETRIES; attempt += 1) {
|
|
65
|
+
try {
|
|
66
|
+
await callDeleteSession(sdkSessionId);
|
|
67
|
+
return { ok: true };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
lastError = error;
|
|
70
|
+
const suffix = attempt >= DEFAULT_DELETE_RETRIES ? ' (no more retries)' : '';
|
|
71
|
+
console.warn(
|
|
72
|
+
`[delete-archive] SDK delete failed for conversation=${conversationId}, session=${sdkSessionId}, attempt=${attempt}/${DEFAULT_DELETE_RETRIES}${suffix}: ${error?.message || error}`
|
|
73
|
+
);
|
|
74
|
+
if (attempt < DEFAULT_DELETE_RETRIES) {
|
|
75
|
+
await sleep(RETRY_DELAYS_MS[attempt - 1] || RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1] || 0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { ok: false, error: lastError };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function hardDeleteConversation(conversationId) {
|
|
83
|
+
const row = stmts.getConversation.get(conversationId);
|
|
84
|
+
cleanupGeneratedImagesForConversation({
|
|
85
|
+
conversationId,
|
|
86
|
+
sdkSessionId: String(row?.sdk_session_id || '').trim() || null,
|
|
87
|
+
messageRows: stmts.listConversationMessages.all(conversationId),
|
|
88
|
+
resolveSessionStateRoot,
|
|
89
|
+
});
|
|
90
|
+
stmts.hardDeleteConversation(conversationId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function deleteConversation(conversationId) {
|
|
94
|
+
const id = normalizeConversationId(conversationId);
|
|
95
|
+
if (!id) return { ok: false, error: 'Missing conversation id' };
|
|
96
|
+
|
|
97
|
+
const row = stmts.getConversation.get(id);
|
|
98
|
+
if (!row) return { ok: true, alreadyDeleted: true };
|
|
99
|
+
if (row.status === 'deleted') return { ok: true, alreadyDeleted: true, tombstoned: true };
|
|
100
|
+
|
|
101
|
+
stmts.markDeleted.run(new Date().toISOString(), id);
|
|
102
|
+
|
|
103
|
+
const sdkSessionId = String(row.sdk_session_id || '').trim();
|
|
104
|
+
|
|
105
|
+
if (!sdkSessionId) {
|
|
106
|
+
await hardDeleteConversation(id);
|
|
107
|
+
return { ok: true, deleted: true, tombstoned: true };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sdkDeleteResult = await deleteSdkSessionWithRetries(sdkSessionId, id);
|
|
111
|
+
if (sdkDeleteResult.ok) {
|
|
112
|
+
await hardDeleteConversation(id);
|
|
113
|
+
return { ok: true, deleted: true, tombstoned: true, sdkDeleted: true };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.warn(`[delete-archive] Leaving tombstoned conversation=${id}; SDK delete failed. Will retry on next startup.`);
|
|
117
|
+
return { ok: true, deleted: false, tombstoned: true, sdkDeleted: false };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function archiveConversation(conversationId) {
|
|
121
|
+
const id = normalizeConversationId(conversationId);
|
|
122
|
+
if (!id) return { ok: false, error: 'Missing conversation id' };
|
|
123
|
+
|
|
124
|
+
const row = stmts.getConversation.get(id);
|
|
125
|
+
if (!row) return { ok: true, alreadyDeleted: true };
|
|
126
|
+
if (row.status === 'deleted') return { ok: true, alreadyDeleted: true, tombstoned: true };
|
|
127
|
+
|
|
128
|
+
stmts.markArchived.run(new Date().toISOString(), id);
|
|
129
|
+
return { ok: true, archived: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function retryPendingDeletesOnStartup() {
|
|
133
|
+
const pendingRows = stmts.listDeletedWithSdkSession.all();
|
|
134
|
+
for (const row of pendingRows) {
|
|
135
|
+
if (!row || !row.id) continue;
|
|
136
|
+
const id = String(row.id).trim();
|
|
137
|
+
if (!id) continue;
|
|
138
|
+
const sdkSessionId = String(row.sdk_session_id || '').trim();
|
|
139
|
+
if (!sdkSessionId) {
|
|
140
|
+
await hardDeleteConversation(id);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const sdkDeleteResult = await deleteSdkSessionWithRetries(sdkSessionId, id);
|
|
144
|
+
if (sdkDeleteResult.ok) {
|
|
145
|
+
await hardDeleteConversation(id);
|
|
146
|
+
} else {
|
|
147
|
+
console.warn(`[delete-archive] Startup retry failed for conversation=${id}; tombstone retained for next restart.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, pendingCount: pendingRows.length };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { deleteConversation, archiveConversation, retryPendingDeletesOnStartup };
|
|
154
|
+
}
|