@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,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Preview management API. Lives on the *relay* port behind `auth` — the preview
|
|
4
|
+
// listener itself has no routes and no way to register anything. Creating or
|
|
5
|
+
// closing a preview therefore always requires the relay token, even though the
|
|
6
|
+
// resulting URL is public.
|
|
7
|
+
|
|
8
|
+
import { validateStaticRoot } from '../services/preview-static-handler.mjs';
|
|
9
|
+
|
|
10
|
+
export function registerPreviewRoutes(app, deps) {
|
|
11
|
+
const {
|
|
12
|
+
auth,
|
|
13
|
+
previewRegistry,
|
|
14
|
+
previewHealthProbe = null,
|
|
15
|
+
// (conversationId) => absolute workspace root or ''. Static previews are
|
|
16
|
+
// jailed to it; without one they are refused rather than served unscoped.
|
|
17
|
+
resolvePreviewWorkspaceRoot = () => '',
|
|
18
|
+
// (conversationId, preview) => void. Pins a transcript card when a turn is
|
|
19
|
+
// in flight for the conversation; a no-op otherwise (manual /preview
|
|
20
|
+
// publishes stay panel-only — the transcript records the conversation, not
|
|
21
|
+
// panel actions).
|
|
22
|
+
recordPreviewCard = () => {},
|
|
23
|
+
} = deps;
|
|
24
|
+
|
|
25
|
+
function laneUnavailable(res) {
|
|
26
|
+
const settings = previewRegistry?.settings;
|
|
27
|
+
if (!settings) return res.status(503).json({ error: 'Preview lane is unavailable' });
|
|
28
|
+
// The interlock errors are the actionable part of a disabled lane ("you set
|
|
29
|
+
// publicBaseUrl to the relay's hostname"), so they are handed back verbatim
|
|
30
|
+
// rather than collapsed into a generic 503.
|
|
31
|
+
return res.status(503).json({
|
|
32
|
+
error: settings.requested
|
|
33
|
+
? 'Preview lane failed its startup checks'
|
|
34
|
+
: 'Preview lane is disabled (set previews.enabled and previews.publicBaseUrl)',
|
|
35
|
+
details: settings.errors || [],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// GET /api/previews — every live preview, or one conversation's when filtered.
|
|
40
|
+
// Unfiltered is what the settings "Live previews" list uses: nothing expires
|
|
41
|
+
// on its own, so there has to be one place that shows everything.
|
|
42
|
+
app.get('/api/previews', auth, (req, res) => {
|
|
43
|
+
const conversationId = String(req.query?.conversationId || '').trim();
|
|
44
|
+
const previews = conversationId
|
|
45
|
+
? previewRegistry.listForConversation(conversationId)
|
|
46
|
+
: previewRegistry.list();
|
|
47
|
+
res.json({
|
|
48
|
+
previews,
|
|
49
|
+
enabled: previewRegistry.settings?.enabled === true,
|
|
50
|
+
publicBaseUrl: previewRegistry.settings?.publicBaseUrl || '',
|
|
51
|
+
errors: previewRegistry.settings?.errors || [],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// POST /api/previews — publish an already-listening local port, or a
|
|
56
|
+
// directory the lane serves itself (static mode).
|
|
57
|
+
app.post('/api/previews', auth, (req, res) => {
|
|
58
|
+
if (previewRegistry.settings?.enabled !== true) return laneUnavailable(res);
|
|
59
|
+
|
|
60
|
+
const dir = String(req.body?.dir || '').trim();
|
|
61
|
+
const hasPort = req.body?.port !== undefined && req.body?.port !== null && String(req.body.port).trim() !== '';
|
|
62
|
+
if (dir && hasPort) {
|
|
63
|
+
return res.status(400).json({ error: 'Pass either port or dir, not both' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let created;
|
|
67
|
+
if (dir) {
|
|
68
|
+
const conversationId = String(req.body?.conversationId || '').trim();
|
|
69
|
+
let workspaceRoot = '';
|
|
70
|
+
try {
|
|
71
|
+
workspaceRoot = String(resolvePreviewWorkspaceRoot(conversationId) || '');
|
|
72
|
+
} catch {}
|
|
73
|
+
const jailed = validateStaticRoot(dir, { workspaceRoot });
|
|
74
|
+
if (!jailed.ok) return res.status(400).json({ error: jailed.error });
|
|
75
|
+
created = previewRegistry.createStatic({
|
|
76
|
+
conversationId,
|
|
77
|
+
rootDir: jailed.rootDir,
|
|
78
|
+
label: req.body?.label,
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
const port = Number(req.body?.port);
|
|
82
|
+
if (!Number.isFinite(port)) return res.status(400).json({ error: 'Missing or invalid port (or pass dir for a static preview)' });
|
|
83
|
+
created = previewRegistry.create({
|
|
84
|
+
conversationId: req.body?.conversationId,
|
|
85
|
+
port,
|
|
86
|
+
host: req.body?.host,
|
|
87
|
+
label: req.body?.label,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!created.ok) return res.status(created.status || 400).json({ error: created.error });
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
recordPreviewCard(String(req.body?.conversationId || '').trim(), created.preview);
|
|
94
|
+
} catch {}
|
|
95
|
+
// Probe immediately so the card does not sit badge-less until the next
|
|
96
|
+
// scheduled sweep — and so a typo'd port shows as offline right away.
|
|
97
|
+
previewHealthProbe?.probeNow?.(created.preview.token);
|
|
98
|
+
res.json({ ok: true, preview: created.preview, url: created.preview.url, basePath: created.preview.basePath });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// DELETE /api/previews/:token — the link stops resolving immediately. The dev
|
|
102
|
+
// server behind it is untouched; it is a separate background task.
|
|
103
|
+
app.delete('/api/previews/:token', auth, (req, res) => {
|
|
104
|
+
const closed = previewRegistry.close(req.params?.token);
|
|
105
|
+
if (!closed.ok) return res.status(closed.status || 404).json({ error: closed.error });
|
|
106
|
+
res.json({ ok: true, preview: closed.preview });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Web Push subscription management. All endpoints sit behind the token auth
|
|
5
|
+
* middleware, so shared read-only viewers (who authenticate with a share
|
|
6
|
+
* token, not the relay token) cannot reach them.
|
|
7
|
+
*/
|
|
8
|
+
export function registerPushRoutes(app, deps) {
|
|
9
|
+
const {
|
|
10
|
+
auth,
|
|
11
|
+
pushDispatchService,
|
|
12
|
+
getPushVapidPublicKey,
|
|
13
|
+
} = deps;
|
|
14
|
+
|
|
15
|
+
app.get('/api/push/vapid-public-key', auth, (req, res) => {
|
|
16
|
+
const publicKey = String(getPushVapidPublicKey() || '').trim();
|
|
17
|
+
if (!publicKey) return res.status(503).json({ error: 'Push is not configured' });
|
|
18
|
+
res.json({ publicKey });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.get('/api/push/devices', auth, (req, res) => {
|
|
22
|
+
res.json({ devices: pushDispatchService.listDevices() });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
app.post('/api/push/subscribe', auth, (req, res) => {
|
|
26
|
+
const { deviceId, label, subscription, preferences } = req.body || {};
|
|
27
|
+
const endpoint = String(subscription?.endpoint || '').trim();
|
|
28
|
+
const keys = subscription?.keys && typeof subscription.keys === 'object' ? subscription.keys : null;
|
|
29
|
+
if (!String(deviceId || '').trim() || !endpoint || !keys?.p256dh || !keys?.auth) {
|
|
30
|
+
return res.status(400).json({ error: 'Missing deviceId or subscription' });
|
|
31
|
+
}
|
|
32
|
+
const result = pushDispatchService.upsertSubscription({
|
|
33
|
+
deviceId,
|
|
34
|
+
label,
|
|
35
|
+
endpoint,
|
|
36
|
+
keys,
|
|
37
|
+
preferences,
|
|
38
|
+
userAgent: req.headers['user-agent'] || null,
|
|
39
|
+
});
|
|
40
|
+
if (!result.ok) return res.status(400).json({ error: result.error || 'Invalid subscription' });
|
|
41
|
+
res.json({ device: result.device });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
app.patch('/api/push/devices/:id', auth, (req, res) => {
|
|
45
|
+
const { label, preferences } = req.body || {};
|
|
46
|
+
if (label === undefined && preferences === undefined) {
|
|
47
|
+
return res.status(400).json({ error: 'Nothing to update' });
|
|
48
|
+
}
|
|
49
|
+
const result = pushDispatchService.updateDevice(req.params.id, { label, preferences });
|
|
50
|
+
if (!result.ok) return res.status(404).json({ error: result.error || 'Not found' });
|
|
51
|
+
res.json({ device: result.device });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.delete('/api/push/devices/:id', auth, (req, res) => {
|
|
55
|
+
const result = pushDispatchService.deleteDevice(req.params.id);
|
|
56
|
+
if (!result.ok) return res.status(404).json({ error: 'Not found' });
|
|
57
|
+
res.json({ ok: true });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function normalizeId(value) {
|
|
4
|
+
const text = String(value || '').trim();
|
|
5
|
+
return text || null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeBoardText(value, fallback = '') {
|
|
9
|
+
const text = String(value || '').trim();
|
|
10
|
+
if (!text) return fallback;
|
|
11
|
+
return text.slice(0, 12_000);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeBoardType(value) {
|
|
15
|
+
const text = String(value || '').trim().toLowerCase();
|
|
16
|
+
if (!text) return 'generic';
|
|
17
|
+
return text.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'generic';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function labelForAction(actionId) {
|
|
21
|
+
const id = String(actionId || '').trim().toLowerCase();
|
|
22
|
+
if (id === 'autopilot') return 'Implement in autopilot';
|
|
23
|
+
if (id === 'autopilot_fleet') return 'Implement with autopilot fleet';
|
|
24
|
+
if (id === 'interactive') return 'Stop here and prompt myself';
|
|
25
|
+
if (id === 'exit_only') return 'Stop here';
|
|
26
|
+
return id.replace(/[_-]+/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) || 'Select';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultPromptForAction(actionId, board = null) {
|
|
30
|
+
const summary = String(board?.body || '').trim();
|
|
31
|
+
if (actionId === 'autopilot') {
|
|
32
|
+
return `Please implement the approved plan now.\n\nPlan summary:\n${summary}`;
|
|
33
|
+
}
|
|
34
|
+
if (actionId === 'autopilot_fleet') {
|
|
35
|
+
return `Please implement the approved plan using autopilot_fleet where useful.\n\nPlan summary:\n${summary}`;
|
|
36
|
+
}
|
|
37
|
+
if (actionId === 'interactive') {
|
|
38
|
+
return `I approved the plan. Let's proceed interactively with implementation.\n\nPlan summary:\n${summary}`;
|
|
39
|
+
}
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function modeForAction(actionId, fallbackMode) {
|
|
44
|
+
if (actionId === 'autopilot' || actionId === 'autopilot_fleet') return 'autopilot';
|
|
45
|
+
if (actionId === 'interactive') return 'agent';
|
|
46
|
+
return fallbackMode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeBoardActions(rawActions) {
|
|
50
|
+
const input = Array.isArray(rawActions) ? rawActions : [];
|
|
51
|
+
const normalized = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (const entry of input) {
|
|
54
|
+
const item = (typeof entry === 'string')
|
|
55
|
+
? { id: entry }
|
|
56
|
+
: (entry && typeof entry === 'object' ? entry : null);
|
|
57
|
+
if (!item) continue;
|
|
58
|
+
const id = String(item.id || item.actionId || item.value || '').trim().toLowerCase();
|
|
59
|
+
if (!id || seen.has(id)) continue;
|
|
60
|
+
seen.add(id);
|
|
61
|
+
const label = String(item.label || item.title || labelForAction(id)).trim().slice(0, 120) || labelForAction(id);
|
|
62
|
+
const mode = normalizeId(item.mode);
|
|
63
|
+
const prompt = normalizeBoardText(item.prompt || '', '');
|
|
64
|
+
normalized.push({
|
|
65
|
+
id,
|
|
66
|
+
label,
|
|
67
|
+
mode: mode || null,
|
|
68
|
+
prompt: prompt || null,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return normalized.slice(0, 8);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseContext(value) {
|
|
75
|
+
if (!value) return null;
|
|
76
|
+
if (typeof value === 'object') return value;
|
|
77
|
+
if (typeof value !== 'string') return null;
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(value);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function registerRelayBoardRoutes(app, deps) {
|
|
86
|
+
const {
|
|
87
|
+
auth,
|
|
88
|
+
io,
|
|
89
|
+
stmts,
|
|
90
|
+
uuidv4,
|
|
91
|
+
normalizeRelayMode,
|
|
92
|
+
resolveRequestedModel,
|
|
93
|
+
DEFAULT_RELAY_MODE,
|
|
94
|
+
DEFAULT_MODEL,
|
|
95
|
+
formatRelayBoardRow,
|
|
96
|
+
pushDispatchService,
|
|
97
|
+
} = deps;
|
|
98
|
+
|
|
99
|
+
app.get('/api/relay-boards', auth, (req, res) => {
|
|
100
|
+
const conversationId = req.query.conversationId ? String(req.query.conversationId) : null;
|
|
101
|
+
const status = String(req.query.status || 'pending').trim() || 'pending';
|
|
102
|
+
const rows = stmts.listBoards.all(status, conversationId, conversationId);
|
|
103
|
+
res.json({ boards: rows.map(formatRelayBoardRow).filter(Boolean) });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.get('/api/relay-board/:id', auth, (req, res) => {
|
|
107
|
+
const row = stmts.getBoard.get(req.params.id);
|
|
108
|
+
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
109
|
+
res.json({ board: formatRelayBoardRow(row) });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
app.post('/api/relay-board', auth, (req, res) => {
|
|
113
|
+
const {
|
|
114
|
+
queueId,
|
|
115
|
+
messageId,
|
|
116
|
+
conversationId,
|
|
117
|
+
mode,
|
|
118
|
+
boardType,
|
|
119
|
+
title,
|
|
120
|
+
body,
|
|
121
|
+
actions,
|
|
122
|
+
recommendedAction,
|
|
123
|
+
context,
|
|
124
|
+
} = req.body || {};
|
|
125
|
+
const q = stmts.findQById.get(queueId || messageId);
|
|
126
|
+
if (!q || q.status !== 'processing') {
|
|
127
|
+
return res.status(409).json({ error: 'No active relay turn' });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const effectiveMessageId = normalizeId(messageId) || q.id;
|
|
131
|
+
const normalizedBoardType = normalizeBoardType(boardType || 'generic');
|
|
132
|
+
const existing = stmts.findBoardByMessageType.get(effectiveMessageId, normalizedBoardType);
|
|
133
|
+
if (existing) {
|
|
134
|
+
return res.json({ board: formatRelayBoardRow(existing), reused: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const now = new Date().toISOString();
|
|
138
|
+
const boardId = uuidv4();
|
|
139
|
+
const relayMode = normalizeRelayMode(mode || q.relay_mode) || DEFAULT_RELAY_MODE;
|
|
140
|
+
const normalizedActions = normalizeBoardActions(actions);
|
|
141
|
+
const normalizedRecommended = normalizeId(recommendedAction);
|
|
142
|
+
const boardContext = parseContext(context);
|
|
143
|
+
|
|
144
|
+
stmts.insertBoard.run(
|
|
145
|
+
boardId,
|
|
146
|
+
q.id,
|
|
147
|
+
normalizeId(conversationId) || q.conversation_id,
|
|
148
|
+
effectiveMessageId,
|
|
149
|
+
normalizedBoardType,
|
|
150
|
+
relayMode,
|
|
151
|
+
normalizeBoardText(title, 'Plan ready for review'),
|
|
152
|
+
normalizeBoardText(body, ''),
|
|
153
|
+
normalizedActions.length ? JSON.stringify(normalizedActions) : null,
|
|
154
|
+
normalizedRecommended,
|
|
155
|
+
boardContext ? JSON.stringify(boardContext) : null,
|
|
156
|
+
now,
|
|
157
|
+
now,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const board = formatRelayBoardRow(stmts.getBoard.get(boardId));
|
|
161
|
+
io.emit('relay_board', { board });
|
|
162
|
+
void pushDispatchService?.notifyBoard?.(board);
|
|
163
|
+
io.emit('relay_board_changed', { conversationId: board?.conversationId || null, boardId });
|
|
164
|
+
return res.json({ board });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
app.post('/api/relay-board/:id/action', auth, (req, res) => {
|
|
168
|
+
const boardId = normalizeId(req.params.id);
|
|
169
|
+
const actionId = normalizeId(req.body?.actionId || req.body?.action_id);
|
|
170
|
+
const clientId = normalizeId(req.body?.clientId || req.body?.client_id);
|
|
171
|
+
if (!boardId || !actionId) return res.status(400).json({ error: 'Missing board/action id' });
|
|
172
|
+
|
|
173
|
+
const row = stmts.getBoard.get(boardId);
|
|
174
|
+
if (!row) return res.status(404).json({ error: 'Not found' });
|
|
175
|
+
if (String(row.status || '').trim().toLowerCase() !== 'pending') {
|
|
176
|
+
return res.status(409).json({ error: `Board already ${row.status}` });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const board = formatRelayBoardRow(row);
|
|
180
|
+
const actions = Array.isArray(board?.actions) ? board.actions : [];
|
|
181
|
+
const selected = actions.find((item) => String(item?.id || '').trim().toLowerCase() === actionId.toLowerCase());
|
|
182
|
+
if (!selected) return res.status(400).json({ error: 'Unknown board action' });
|
|
183
|
+
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const shouldDismiss = actionId.toLowerCase() === 'exit_only';
|
|
186
|
+
if (shouldDismiss) {
|
|
187
|
+
stmts.dismissBoard.run(actionId, now, now, boardId);
|
|
188
|
+
} else {
|
|
189
|
+
stmts.markBoardAction.run(actionId, now, now, boardId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let queuedMessageId = null;
|
|
193
|
+
if (!shouldDismiss) {
|
|
194
|
+
const followupPrompt = normalizeBoardText(selected.prompt || defaultPromptForAction(actionId.toLowerCase(), board), '');
|
|
195
|
+
if (followupPrompt) {
|
|
196
|
+
const relayMode = normalizeRelayMode(selected.mode || modeForAction(actionId.toLowerCase(), board.mode)) || DEFAULT_RELAY_MODE;
|
|
197
|
+
const convId = String(board?.conversationId || '').trim();
|
|
198
|
+
const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(convId) || null;
|
|
199
|
+
const latestModel = stmts.getLatestConversationModel?.get?.(convId)?.model || null;
|
|
200
|
+
const modelResolution = typeof resolveRequestedModel === 'function'
|
|
201
|
+
? resolveRequestedModel(latestModel || DEFAULT_MODEL)
|
|
202
|
+
: { ok: true, model: latestModel || DEFAULT_MODEL, modelVariantId: latestModel || DEFAULT_MODEL, reasoningEffort: null };
|
|
203
|
+
const resolvedBaseModel = modelResolution?.ok ? String(modelResolution.model || '').trim() : (latestModel || DEFAULT_MODEL);
|
|
204
|
+
const resolvedVariantModel = modelResolution?.ok
|
|
205
|
+
? String(modelResolution.modelVariantId || latestModel || resolvedBaseModel).trim()
|
|
206
|
+
: String(latestModel || resolvedBaseModel).trim();
|
|
207
|
+
const resolvedReasoningEffort = modelResolution?.ok
|
|
208
|
+
? String(modelResolution.reasoningEffort || '').trim() || null
|
|
209
|
+
: null;
|
|
210
|
+
queuedMessageId = uuidv4();
|
|
211
|
+
stmts.insertMsg.run(
|
|
212
|
+
queuedMessageId,
|
|
213
|
+
convId,
|
|
214
|
+
'user',
|
|
215
|
+
followupPrompt,
|
|
216
|
+
resolvedVariantModel,
|
|
217
|
+
relayMode,
|
|
218
|
+
null,
|
|
219
|
+
now,
|
|
220
|
+
resolvedVariantModel || null,
|
|
221
|
+
null,
|
|
222
|
+
String(resolvedVariantModel || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual',
|
|
223
|
+
);
|
|
224
|
+
stmts.updateConvTime.run(now, convId);
|
|
225
|
+
stmts.insertQ.run(
|
|
226
|
+
queuedMessageId,
|
|
227
|
+
convId,
|
|
228
|
+
runtimeSession?.id || null,
|
|
229
|
+
0,
|
|
230
|
+
resolvedBaseModel,
|
|
231
|
+
resolvedVariantModel,
|
|
232
|
+
resolvedReasoningEffort,
|
|
233
|
+
relayMode,
|
|
234
|
+
followupPrompt,
|
|
235
|
+
null,
|
|
236
|
+
now,
|
|
237
|
+
null,
|
|
238
|
+
null,
|
|
239
|
+
null,
|
|
240
|
+
null,
|
|
241
|
+
);
|
|
242
|
+
io.emit('user_message', {
|
|
243
|
+
conversationId: convId,
|
|
244
|
+
messageId: queuedMessageId,
|
|
245
|
+
senderClientId: clientId || null,
|
|
246
|
+
message: {
|
|
247
|
+
role: 'user',
|
|
248
|
+
text: followupPrompt,
|
|
249
|
+
model: resolvedVariantModel,
|
|
250
|
+
modelOrigin: String(resolvedVariantModel || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual',
|
|
251
|
+
mode: relayMode,
|
|
252
|
+
attachments: [],
|
|
253
|
+
timestamp: now,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const updated = formatRelayBoardRow(stmts.getBoard.get(boardId));
|
|
260
|
+
io.emit('relay_board_updated', { board: updated });
|
|
261
|
+
io.emit('relay_board_changed', { conversationId: updated?.conversationId || null, boardId });
|
|
262
|
+
return res.json({
|
|
263
|
+
ok: true,
|
|
264
|
+
board: updated,
|
|
265
|
+
queuedMessageId,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|