@oar-sh/oar 0.9.6 → 0.9.8

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/README.md +222 -40
  3. package/bin/oar.js +320 -18
  4. package/package.json +1 -1
  5. package/server/README.md +174 -7
  6. package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
  7. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
  8. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
  9. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  10. package/server/claude-worker/claude-sdk-adapter.mjs +18 -13
  11. package/server/claude-worker/claude-session-process.mjs +225 -24
  12. package/server/claude-worker/claude-session-worker.mjs +6 -0
  13. package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
  14. package/server/db-schema.mjs +17 -0
  15. package/server/migrations/0006-workspace-root-settings.mjs +67 -0
  16. package/server/migrations/0007-claude-cloud.mjs +82 -0
  17. package/server/public/app/activity-replay-state.mjs +68 -5
  18. package/server/public/app/agent-sessions-settings-ui.js +197 -0
  19. package/server/public/app/agent-sessions-settings.mjs +134 -0
  20. package/server/public/app/api-client.js +149 -6
  21. package/server/public/app/attachments-view.js +20 -1
  22. package/server/public/app/attribution-control.mjs +46 -0
  23. package/server/public/app/bootstrap.js +468 -88
  24. package/server/public/app/claude-cloud-conversation-ui.js +110 -0
  25. package/server/public/app/claude-cloud-settings-ui.js +374 -0
  26. package/server/public/app/claude-cloud-ui.mjs +611 -0
  27. package/server/public/app/composer-control-state.mjs +11 -0
  28. package/server/public/app/context-menu.mjs +140 -0
  29. package/server/public/app/context-tier-options.mjs +4 -0
  30. package/server/public/app/context-usage-view.mjs +127 -8
  31. package/server/public/app/conversation-origin-ui.js +27 -0
  32. package/server/public/app/conversation-provider-indicator.mjs +22 -4
  33. package/server/public/app/conversation-view.js +145 -11
  34. package/server/public/app/cwd-picker.js +12 -0
  35. package/server/public/app/journal-view.js +698 -28
  36. package/server/public/app/math-dollars.mjs +89 -0
  37. package/server/public/app/model-selector-options.mjs +1 -1
  38. package/server/public/app/plan-usage-view.mjs +36 -8
  39. package/server/public/app/pwa-install.js +5 -0
  40. package/server/public/app/relay-error-ctas.mjs +16 -2
  41. package/server/public/app/remote-relay-origin-view.mjs +120 -4
  42. package/server/public/app/router.js +49 -12
  43. package/server/public/app/scrollbar-hover.mjs +78 -0
  44. package/server/public/app/settings-modal.js +37 -3
  45. package/server/public/app/settings-tabs.js +1 -1
  46. package/server/public/app/socket-handlers.js +53 -1
  47. package/server/public/app/store.js +26 -0
  48. package/server/public/app/transcript-separators.mjs +48 -11
  49. package/server/public/app-icon-192.png +0 -0
  50. package/server/public/app-icon-512.png +0 -0
  51. package/server/public/app-icon-light-192.png +0 -0
  52. package/server/public/app-icon-light-512.png +0 -0
  53. package/server/public/app-icon-light-maskable-512.png +0 -0
  54. package/server/public/app-icon-light.svg +4 -0
  55. package/server/public/app-icon-maskable-512.png +0 -0
  56. package/server/public/app-icon.svg +3 -3
  57. package/server/public/favicon.ico +0 -0
  58. package/server/public/index.html +564 -20
  59. package/server/public/manifest.webmanifest +9 -3
  60. package/server/public/sw.js +6 -1
  61. package/server/relay-tools.md +13 -1
  62. package/server/repositories/remote-relay-repository.mjs +42 -1
  63. package/server/repositories/session-repository.mjs +47 -1
  64. package/server/routes/claude-cloud-routes.mjs +140 -0
  65. package/server/routes/git-routes.mjs +37 -0
  66. package/server/routes/messages-routes.mjs +123 -16
  67. package/server/routes/remote-relay-routes.mjs +15 -1
  68. package/server/routes/sessions-routes.mjs +517 -74
  69. package/server/server-runtime.mjs +268 -33
  70. package/server/services/claude-account-usage-service.mjs +176 -0
  71. package/server/services/claude-cloud-repo-service.mjs +408 -0
  72. package/server/services/claude-cloud-session-service.mjs +326 -0
  73. package/server/services/claude-cloud-settings-service.mjs +269 -0
  74. package/server/services/claude-context-usage.mjs +51 -3
  75. package/server/services/git-remote-service.mjs +115 -0
  76. package/server/services/oar-cli-helpers.mjs +67 -1
  77. package/server/services/plan-usage-claude-cloud.mjs +211 -0
  78. package/server/services/plan-usage-claude.mjs +202 -40
  79. package/server/services/plan-usage-contract.mjs +39 -3
  80. package/server/services/plan-usage-service.mjs +82 -2
  81. package/server/services/provider-reasoning-effort.mjs +3 -0
  82. package/server/services/pwa-manifest-theme.mjs +30 -0
  83. package/server/services/remote-relay-caller-context.mjs +49 -10
  84. package/server/services/remote-relay-client.mjs +9 -0
  85. package/server/services/remote-relay-dispatcher.mjs +383 -35
  86. package/server/services/remote-relay-inbound.mjs +49 -2
  87. package/server/services/remote-relay-loopback.mjs +245 -0
  88. package/server/services/remote-relay-registry-service.mjs +52 -0
  89. package/server/services/session-worker-launch-service.mjs +100 -15
  90. package/server/services/session-worker-process-service.mjs +348 -129
  91. package/server/services/session-worker-registry-service.mjs +18 -1
  92. package/server/services/session-worker-stop-service.mjs +24 -13
  93. package/server/services/session-worker-supervisor-service.mjs +56 -1
  94. package/server/services/update-install-service.mjs +9 -3
  95. package/server/services/usage-limit-pause-service.mjs +3 -1
  96. package/server/services/worker-launch-note-service.mjs +151 -0
  97. package/shared/claude-attribution.mjs +117 -0
  98. package/shared/claude-cloud/api-client.mjs +809 -0
  99. package/shared/claude-cloud/base-url.mjs +47 -0
  100. package/shared/claude-cloud/credentials.mjs +221 -0
  101. package/shared/claude-cloud/repo-url.mjs +111 -0
  102. package/shared/provider-routing.mjs +4 -2
  103. package/shared/remote-relay-contract.mjs +153 -22
  104. package/shared/remote-relay-tool-core.mjs +11 -8
  105. package/shared/steer-settle-failure.mjs +1 -0
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ // Claude Cloud session worker: a per-conversation Node process that speaks
3
+ // the same relay contracts as the Claude/Cursor/Grok workers, but runs no
4
+ // agent itself. The agent and its clone live in a Claude Code cloud session;
5
+ // this process sends the user's messages there and follows the session's
6
+ // event stream (claude-cloud-session-process.mjs).
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ import {
11
+ loadTokenFromConfig,
12
+ resolveRelayServerUrl,
13
+ } from '../../shared/worker-runtime/config-loader.mjs';
14
+ import { createApiClient } from '../../shared/worker-runtime/api-client.mjs';
15
+ import { createWorkerWebSocketLink } from '../../shared/worker-runtime/worker-websocket-link.mjs';
16
+ import { createHeartbeatController } from '../../shared/worker-runtime/heartbeat.mjs';
17
+ import { installWorkerLogFile } from '../../shared/worker-runtime/worker-log-file.mjs';
18
+ import { createControlPoller } from '../../shared/control-poller.mjs';
19
+ import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
20
+ import { createClaudeCloudCredentials } from '../../shared/claude-cloud/credentials.mjs';
21
+ import { createClaudeCloudClient } from '../../shared/claude-cloud/api-client.mjs';
22
+ import { resolveClaudeCloudBaseUrl } from '../../shared/claude-cloud/base-url.mjs';
23
+ import { createClaudeCloudSessionRunner } from './claude-cloud-session-process.mjs';
24
+
25
+ // Windows console launch: the launcher names the worker log, this copies
26
+ // stdout/stderr into it. A no-op wherever the launcher redirects instead.
27
+ installWorkerLogFile();
28
+
29
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
+ const HEARTBEAT_MS = 10_000;
31
+
32
+ function parseSessionIdArg(argv = process.argv) {
33
+ const index = argv.indexOf('--session-id');
34
+ if (index !== -1 && argv[index + 1]) return String(argv[index + 1]).trim();
35
+ const inline = argv.find((arg) => String(arg || '').startsWith('--session-id='));
36
+ if (inline) return String(inline.split('=')[1] || '').trim();
37
+ return String(process.env.SESSION_ID || '').trim();
38
+ }
39
+
40
+ function dbg(...parts) {
41
+ const timestamp = new Date().toISOString();
42
+ console.log(`[claude-cloud-worker ${timestamp}]`, ...parts);
43
+ }
44
+
45
+ async function main() {
46
+ const sdkSessionId = parseSessionIdArg();
47
+ if (!sdkSessionId) {
48
+ console.error('claude-cloud-session-worker: missing --session-id');
49
+ process.exit(2);
50
+ }
51
+
52
+ const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
53
+ || path.resolve(__dirname, '..', 'config.json');
54
+ const serverUrl = resolveRelayServerUrl({ configPath });
55
+ const token = loadTokenFromConfig(configPath);
56
+ const defaultModel = String(process.env.CLAUDE_CLOUD_RELAY_MODEL || '').trim();
57
+
58
+ const api = createApiClient({
59
+ serverUrl,
60
+ token,
61
+ getHeaders: () => ({
62
+ 'X-Relay-Process-Pid': String(process.pid),
63
+ 'X-Relay-Parent-Pid': String(process.ppid),
64
+ 'X-Relay-Session-Id': sdkSessionId,
65
+ 'X-Relay-Conversation-Id': sdkSessionId,
66
+ }),
67
+ });
68
+
69
+ // The Claude CLI's login, read by the client's own module (the credentials
70
+ // file, or CLAUDE_CODE_OAUTH_TOKEN where the relay runs on one): this
71
+ // process never logs it. A login that has run out is nudged through the
72
+ // relay, which can run the CLI (`claude auth status` refreshes a login
73
+ // whose refresh token is still good); the file is read again afterwards.
74
+ // The base URL is the Anthropic API unless a test names a fake one on this
75
+ // machine (base-url.mjs takes nothing else).
76
+ const cloud = createClaudeCloudClient({
77
+ credentials: createClaudeCloudCredentials({
78
+ nudge: async () => {
79
+ const answer = await api('POST', '/api/claude-cloud/login-nudge', {});
80
+ dbg('login nudge', answer?.nudged ? 'ran' : 'skipped', answer?.expired ? 'still expired' : `expires ${answer?.expiresAt || '?'}`);
81
+ },
82
+ }),
83
+ baseUrl: resolveClaudeCloudBaseUrl(process.env),
84
+ });
85
+
86
+ const controlPoller = createControlPoller({
87
+ api,
88
+ sdkSessionId,
89
+ abortAckNote: 'cloud turn interrupted',
90
+ dbg,
91
+ });
92
+ const runner = createClaudeCloudSessionRunner({
93
+ api,
94
+ cloud,
95
+ sdkSessionId,
96
+ defaultModel,
97
+ controlPoller,
98
+ dbg,
99
+ });
100
+
101
+ let heartbeatTimer = null;
102
+ const heartbeat = createHeartbeatController({
103
+ api,
104
+ pollMs: HEARTBEAT_MS,
105
+ getSessionReady: () => true,
106
+ getHeartbeatTimer: () => heartbeatTimer,
107
+ setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
108
+ getActiveQueueMessageId: () => runner.getActiveQueueMessageId(),
109
+ });
110
+
111
+ const wsLink = createWorkerWebSocketLink({
112
+ serverUrl,
113
+ token,
114
+ dbg,
115
+ getSessionReady: () => true,
116
+ getSessionId: () => sdkSessionId,
117
+ getPid: () => process.pid,
118
+ onDeliver: async (pending, reason) => {
119
+ dbg('queue.deliver received', `reason=${reason}`, `msgId=${pending?.message?.id || 'none'}`);
120
+ try {
121
+ return await runner.handlePendingPayload(pending);
122
+ } catch (error) {
123
+ dbg('turn handling failed', error?.message || String(error));
124
+ return false;
125
+ }
126
+ },
127
+ });
128
+
129
+ const shutdown = async (signal) => {
130
+ dbg(`shutting down (${signal})`);
131
+ try { wsLink.stop(); } catch {}
132
+ try { heartbeat.stopHeartbeat(); } catch {}
133
+ try { controlPoller.stop(); } catch {}
134
+ // The cloud session is left as it is: a turn in flight keeps running
135
+ // there, and the next worker finds it through the stored sequence number.
136
+ try { await runner.dispose(); } catch {}
137
+ process.exit(0);
138
+ };
139
+ process.on('SIGTERM', () => { shutdown('SIGTERM'); });
140
+ process.on('SIGINT', () => { shutdown('SIGINT'); });
141
+ installWorkerCrashGuard({
142
+ api,
143
+ workerName: 'claude-cloud-session-worker',
144
+ getActiveQueueMessageIds: () => [runner.getActiveQueueAttempt()],
145
+ });
146
+
147
+ dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} model=${defaultModel || 'default'}`);
148
+ heartbeat.startHeartbeat();
149
+ wsLink.start();
150
+ }
151
+
152
+ main().catch((error) => {
153
+ console.error('claude-cloud-session-worker fatal:', error?.stack || error?.message || error);
154
+ process.exit(1);
155
+ });
@@ -62,6 +62,8 @@ export const REMOTE_RELAY_TOOL_ZOD_SHAPE = {
62
62
  provider: choice('provider', REMOTE_RELAY_PROVIDERS),
63
63
  model: text('model'),
64
64
  cwd: text('cwd'),
65
+ repo: text('repo'),
66
+ branch: text('branch'),
65
67
  mode: choice('mode', REMOTE_RELAY_MODES),
66
68
  effort: text('effort'),
67
69
  title: text('title'),
@@ -55,17 +55,6 @@ export function claudeUltracodeFlagSettings(effort) {
55
55
  return { ultracode: null, enableWorkflows: null, effortLevel: effort || null };
56
56
  }
57
57
 
58
- /**
59
- * The flag-settings payload that moves a live session onto another auto-compact
60
- * window. `null` clears the flag layer back to the CLI's model-tuned default,
61
- * which is exactly what "Auto" means — leaving the previous window pinned would
62
- * make the setting one-way.
63
- */
64
- export function claudeAutoCompactFlagSettings(autoCompactWindow) {
65
- const window = normalizeAutoCompactWindow(autoCompactWindow);
66
- return { autoCompactWindow: window };
67
- }
68
-
69
58
  /**
70
59
  * The flag-settings payload that pins (or clears) `alwaysThinkingEnabled` on a
71
60
  * live session.
@@ -111,7 +100,7 @@ export function normalizeAutoCompactWindow(value) {
111
100
  * spreads of `settings` in the options literal would silently clobber each
112
101
  * other. Returns null when nothing needs to be set.
113
102
  */
114
- export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = null, thinkingEnabled = null } = {}) {
103
+ export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = null, thinkingEnabled = null, attribution = null } = {}) {
115
104
  const window = normalizeAutoCompactWindow(autoCompactWindow);
116
105
  const thinking = strictThinkingEnabled(thinkingEnabled);
117
106
  const settings = {
@@ -122,10 +111,25 @@ export function claudeSpawnSettings({ ultracode = false, autoCompactWindow = nul
122
111
  // present; the omit path exists for direct callers that mean to say
123
112
  // nothing.
124
113
  ...(thinking !== null ? { alwaysThinkingEnabled: thinking } : {}),
114
+ // What commits and PRs say (shared/claude-attribution.mjs). null is
115
+ // vanilla: the key is left out and the CLI uses its own.
116
+ ...(attribution && typeof attribution === 'object' ? { attribution } : {}),
125
117
  };
126
118
  return Object.keys(settings).length ? settings : null;
127
119
  }
128
120
 
121
+ /**
122
+ * Live twin of the attribution spawn setting, for a value (OAR or off). A
123
+ * running CLI takes a new text through the flag layer (it arrives as a fresh
124
+ * system-reminder on the next user turn) but cannot UNSET a value it was
125
+ * spawned with — `{}`, `null` and `true` all leave the spawn-time text in
126
+ * force (wire-logged probe, 2026-10-01). Back to vanilla therefore means a
127
+ * fresh process without the key; the session runner does that.
128
+ */
129
+ export function claudeAttributionFlagSettings(attribution) {
130
+ return { attribution: attribution && typeof attribution === 'object' ? attribution : { commit: '', pr: '', sessionUrl: false } };
131
+ }
132
+
129
133
  export function permissionModeForRelayMode(relayMode) {
130
134
  const mode = String(relayMode || 'agent').trim().toLowerCase();
131
135
  if (mode === 'plan') return 'plan';
@@ -228,6 +232,7 @@ export function startClaudeSession({
228
232
  reasoningEffort = '',
229
233
  autoCompactWindow = null,
230
234
  thinkingEnabled = null,
235
+ attribution = null,
231
236
  thinkingDisplay = '',
232
237
  abortController,
233
238
  canUseTool,
@@ -248,7 +253,7 @@ export function startClaudeSession({
248
253
  // only way to hand a fresh CLI the session-scoped ultracode flag, the
249
254
  // auto-compact window and the thinking pin, and all three share one
250
255
  // `settings` object.
251
- const spawnSettings = claudeSpawnSettings({ ultracode, autoCompactWindow, thinkingEnabled });
256
+ const spawnSettings = claudeSpawnSettings({ ultracode, autoCompactWindow, thinkingEnabled, attribution });
252
257
  // Relay tools need the worker's authenticated API helper; a caller that
253
258
  // passes none (tests, probes) gets a session without them.
254
259
  const relayMcpServer = typeof api === 'function'