@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
@@ -33,6 +33,8 @@ import { sanitizeRelayQuestionContext } from './relay-question-context.mjs';
33
33
 
34
34
  const LOG_PREFIX = '[remote-relays]';
35
35
  const APPROVAL_HEADER = 'Remote relay';
36
+ // The once-per-conversation card for sessions an agent starts on this relay.
37
+ const LOCAL_SESSIONS_HEADER = 'Agent sessions';
36
38
  const APPROVAL_ALLOW = 'Allow';
37
39
  const APPROVAL_DENY = 'Deny';
38
40
  const EXCERPT_MAX = 200;
@@ -115,21 +117,45 @@ function describeRemoteTurn(action, args, caller) {
115
117
  return parts.join(' · ');
116
118
  }
117
119
 
120
+ /**
121
+ * What a session an agent starts on this relay works on, for the card: the
122
+ * repository (and branch) of a Claude Cloud session, else the folder, else
123
+ * the relay's default folder.
124
+ */
125
+ function describeLocalSessionTarget(args) {
126
+ const repo = toText(args.repo).replace(/^https?:\/\/(?:www\.)?github\.com\//i, '');
127
+ if (repo) return toText(args.branch) ? `${excerpt(repo)}, branch ${excerpt(args.branch)}` : excerpt(repo);
128
+ return toText(args.cwd) ? excerpt(args.cwd) : 'the default folder';
129
+ }
130
+
118
131
  /**
119
132
  * The card text: what the agent wants to do where, how the remote turn would
120
133
  * run, then the prompt (or the answer) it wants to send, so the user decides
121
134
  * on the actual words. `caller` is the calling session's context.
135
+ *
136
+ * `localSessions` is the once-per-conversation card of the local target: the
137
+ * agent wants to start sessions on this relay itself. Allowing it covers the
138
+ * later ones of that conversation, so the card says what they cost.
122
139
  */
123
- export function formatRemoteRelayApprovalPrompt({ relay, action, args = {}, caller = null, agentPrompts = 0 } = {}) {
140
+ export function formatRemoteRelayApprovalPrompt({ relay, action, args = {}, caller = null, agentPrompts = 0, localSessions = false } = {}) {
141
+ const local = relay?.local === true;
124
142
  const name = toText(relay?.name) || 'a remote relay';
143
+ const where = local ? 'this relay' : `relay "${name}"`;
125
144
  const verb = APPROVAL_VERBS[action] || toText(action).replace(/_/g, ' ') || 'act';
126
145
  const target = args.session ? ` (session ${shortId(args.session)})` : '';
127
146
  const lines = [];
128
147
  // The loop guard's card: agents that prompt each other without the user.
129
148
  if (agentPrompts > 0) {
130
- lines.push(`The agent has sent ${agentPrompts} prompts to agents on other relays since your last message. Let it go on?`);
149
+ lines.push(`The agent has sent ${agentPrompts} prompts to ${local ? 'other agents' : 'agents on other relays'} since your last message. Let it go on?`);
150
+ }
151
+ if (localSessions) {
152
+ const requested = toText(args.provider).toLowerCase();
153
+ const callerProvider = toText(caller?.provider).toLowerCase();
154
+ const provider = requested || (REMOTE_RELAY_PROVIDERS.includes(callerProvider) ? callerProvider : 'github');
155
+ lines.push(`This agent wants to start sessions on this relay (${provider}, ${describeLocalSessionTarget(args)}). Sessions it starts use this relay's accounts.`);
156
+ } else {
157
+ lines.push(`Allow the agent to ${verb} on ${where}${target}?`);
131
158
  }
132
- lines.push(`Allow the agent to ${verb} on relay "${name}"${target}?`);
133
159
  if (action === 'send' || action === 'create_session') {
134
160
  const details = describeRemoteTurn(action, args, caller);
135
161
  if (details) lines.push(details);
@@ -288,31 +314,44 @@ export function createRemoteRelayCallerContext({
288
314
  * `agentPrompts` (the loop guard) is how many prompts the agent has sent to
289
315
  * other relays' agents since the user last wrote; the card then asks for
290
316
  * that, in any relay mode.
317
+ * `localSessions` makes it the card for starting sessions on this relay
318
+ * itself, asked once per conversation (the dispatcher remembers the Allow).
291
319
  */
292
- async function requestApproval({ conversationId, callerContext, relay, action, args = {}, signal, agentPrompts = 0 } = {}) {
320
+ async function requestApproval({ conversationId, callerContext, relay, action, args = {}, signal, agentPrompts = 0, localSessions = false } = {}) {
293
321
  const rowId = toText(callerContext?.processingRowId);
294
322
  const row = rowId ? getQueueRow.get(rowId) : null;
295
323
  if (!row || row.status !== 'processing') {
296
324
  return {
297
325
  approved: false,
298
326
  code: REMOTE_RELAY_ERROR_CODES.noTurn,
299
- error: 'This write action needs the user\'s approval, but the conversation has no running turn to ask on.',
327
+ error: localSessions
328
+ ? 'Starting sessions on this relay needs the user\'s approval once per conversation, but the conversation has no running turn to ask on.'
329
+ : 'This write action needs the user\'s approval, but the conversation has no running turn to ask on.',
300
330
  };
301
331
  }
302
332
 
303
333
  const createdAt = now().toISOString();
304
334
  const questionId = String(uuid());
305
335
  const relayMode = normalizeMode(row.relay_mode);
306
- const prompt = formatRemoteRelayApprovalPrompt({ relay, action, args, caller: callerContext, agentPrompts });
336
+ const prompt = formatRemoteRelayApprovalPrompt({ relay, action, args, caller: callerContext, agentPrompts, localSessions });
337
+ const local = relay?.local === true;
338
+ let rationale = `Write actions on ${local ? 'other sessions of this relay' : 'other relays'} need approval in ${relayMode} mode.`;
339
+ if (agentPrompts > 0) {
340
+ rationale = local
341
+ ? 'Agents that keep prompting each other need your go from time to time.'
342
+ : 'Agents on different relays that keep prompting each other need your go from time to time.';
343
+ } else if (localSessions) {
344
+ rationale = 'Allow covers every session this conversation\'s agent starts on this relay; Deny refuses this one.';
345
+ }
346
+ // The same source for every card of this tool: it is what keeps agents
347
+ // (other relays' and this relay's own) from seeing or answering one.
307
348
  const context = sanitizeRelayQuestionContext({
308
349
  source: REMOTE_RELAY_APPROVAL_SOURCE,
309
- rationale: agentPrompts > 0
310
- ? 'Agents on different relays that keep prompting each other need your go from time to time.'
311
- : `Write actions on other relays need approval in ${relayMode} mode.`,
350
+ rationale,
312
351
  queueMessageId: row.id,
313
352
  conversationId: toText(conversationId) || row.conversation_id,
314
353
  relayMode,
315
- header: APPROVAL_HEADER,
354
+ header: localSessions ? LOCAL_SESSIONS_HEADER : APPROVAL_HEADER,
316
355
  }, { normalizeRelayMode: normalizeMode, defaultRelayMode });
317
356
  questionStatements.insertQuestion.run(
318
357
  questionId,
@@ -121,6 +121,15 @@ function networkError(relay, error, timeoutMs) {
121
121
  );
122
122
  }
123
123
 
124
+ /**
125
+ * The error for a relay that answered with a non-2xx status and (maybe) a JSON
126
+ * body. Shared with the in-process loopback, so a refusal of this relay's own
127
+ * routes reads exactly like a paired relay's.
128
+ */
129
+ export function remoteRelayHttpError(relay, status, body) {
130
+ return httpError(relay, status, body);
131
+ }
132
+
124
133
  function httpError(relay, status, body) {
125
134
  const remoteError = isPlainObject(body) && toText(body.error) ? toText(body.error).slice(0, REMOTE_ERROR_TEXT_MAX) : null;
126
135
  const remoteCode = isPlainObject(body) && toText(body.code) ? toText(body.code).slice(0, 100) : null;