@oar-sh/oar 0.9.3 → 0.9.5

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 (76) hide show
  1. package/.github/extensions/web-relay/extension.mjs +6 -1
  2. package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
  3. package/CHANGELOG.md +98 -0
  4. package/README.md +76 -14
  5. package/package.json +5 -2
  6. package/server/README.md +43 -3
  7. package/server/claude-worker/claude-remote-relay-tool.mjs +100 -0
  8. package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
  9. package/server/claude-worker/claude-session-process.mjs +385 -43
  10. package/server/claude-worker/claude-session-worker.mjs +5 -0
  11. package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
  12. package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
  13. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +12 -2
  14. package/server/copilot-worker/copilot-sdk-session-process.mjs +83 -9
  15. package/server/copilot-worker/copilot-sdk-session-worker.mjs +5 -0
  16. package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
  17. package/server/cursor-worker/cursor-session-worker.mjs +5 -0
  18. package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
  19. package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
  20. package/server/db-schema.mjs +8 -0
  21. package/server/grok-worker/acp-client.mjs +92 -6
  22. package/server/grok-worker/grok-session-worker.mjs +5 -0
  23. package/server/grok-worker/grok-turn-runner.mjs +59 -8
  24. package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
  25. package/server/mcp/oar-mcp-server.mjs +473 -0
  26. package/server/migrations/0005-remote-relays.mjs +90 -0
  27. package/server/public/app/action-confirmations.js +141 -13
  28. package/server/public/app/api-client.js +103 -0
  29. package/server/public/app/bootstrap.js +21 -6
  30. package/server/public/app/conversation-deep-link.mjs +42 -0
  31. package/server/public/app/conversation-view.js +100 -7
  32. package/server/public/app/host-suspend-ui.js +272 -0
  33. package/server/public/app/journal-view.js +4 -1
  34. package/server/public/app/mention-autocomplete.mjs +298 -0
  35. package/server/public/app/push-settings.js +1 -0
  36. package/server/public/app/pwa-install.js +8 -3
  37. package/server/public/app/relay-prompt-sanitizer.mjs +48 -5
  38. package/server/public/app/remote-relay-origin-view.mjs +76 -0
  39. package/server/public/app/remote-relay-shared.mjs +85 -0
  40. package/server/public/app/remote-relays-settings.js +618 -0
  41. package/server/public/app/remote-relays-store.mjs +126 -0
  42. package/server/public/app/settings-modal.js +4 -2
  43. package/server/public/app/settings-tabs.js +1 -1
  44. package/server/public/app/socket-handlers.js +18 -1
  45. package/server/public/app/steered-turn-merge.mjs +10 -1
  46. package/server/public/app/thinking-follow.js +260 -0
  47. package/server/public/app/tool-activity-emoji.mjs +4 -0
  48. package/server/public/index.html +264 -11
  49. package/server/relay-tools.md +5 -1
  50. package/server/repositories/message-repository.mjs +7 -0
  51. package/server/repositories/question-repository.mjs +3 -0
  52. package/server/repositories/remote-relay-repository.mjs +75 -0
  53. package/server/repositories/session-repository.mjs +6 -1
  54. package/server/routes/ask-user-routes.mjs +42 -2
  55. package/server/routes/messages-routes.mjs +98 -19
  56. package/server/routes/remote-relay-routes.mjs +133 -0
  57. package/server/routes/sessions-routes.mjs +84 -31
  58. package/server/server-runtime.mjs +238 -0
  59. package/server/services/github-ci-activity-service.mjs +199 -0
  60. package/server/services/host-suspend-command.mjs +38 -0
  61. package/server/services/host-suspend-service.mjs +254 -0
  62. package/server/services/push-dispatch-service.mjs +37 -4
  63. package/server/services/remote-relay-caller-context.mjs +346 -0
  64. package/server/services/remote-relay-client.mjs +332 -0
  65. package/server/services/remote-relay-dispatcher.mjs +1743 -0
  66. package/server/services/remote-relay-inbound.mjs +187 -0
  67. package/server/services/remote-relay-pairing.mjs +189 -0
  68. package/server/services/remote-relay-registry-service.mjs +581 -0
  69. package/server/services/session-worker-launch-service.mjs +148 -14
  70. package/shared/remote-relay-contract.mjs +556 -0
  71. package/shared/remote-relay-mentions.mjs +129 -0
  72. package/shared/remote-relay-tool-core.mjs +314 -0
  73. package/shared/steer-settle-failure.mjs +6 -0
  74. package/shared/steer-settle-markers.mjs +16 -0
  75. package/shared/worker-runtime/api-client.mjs +112 -22
  76. package/shared/worker-runtime/worker-log-file.mjs +97 -0
@@ -6,6 +6,17 @@ import os from 'os';
6
6
  import path from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
 
9
+ import {
10
+ OAR_MCP_SERVER_NAME,
11
+ OAR_MCP_SERVER_SCRIPT_PATH,
12
+ OAR_MCP_TOOL_TIMEOUT_MS,
13
+ buildOarMcpServerLaunch,
14
+ isOarMcpServerDisabled,
15
+ } from '../mcp/oar-mcp-server.mjs';
16
+ import { WORKER_LOG_FILE_ENV } from '../../shared/worker-runtime/worker-log-file.mjs';
17
+
18
+ export { isOarMcpServerDisabled };
19
+
9
20
  function shellQuote(value) {
10
21
  return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
11
22
  }
@@ -302,7 +313,7 @@ function resolveNodeWorkerDescriptor(env = {}) {
302
313
  };
303
314
  }
304
315
 
305
- function buildPosixWorkerLaunchCommand(targetSessionId, env = {}) {
316
+ function buildPosixWorkerLaunchCommand(targetSessionId, env = {}, extraCliArgs = []) {
306
317
  const nodeWorker = resolveNodeWorkerDescriptor(env);
307
318
  if (nodeWorker) {
308
319
  const nodeExecutable = normalizeText(env?.COPILOT_WEB_RELAY_NODE) || 'node';
@@ -312,7 +323,88 @@ function buildPosixWorkerLaunchCommand(targetSessionId, env = {}) {
312
323
  || normalizeText(env?.COPILOT_CLI_EXECUTABLE)
313
324
  || normalizeText(env?.COPILOT_CLI_PATH)
314
325
  || 'copilot';
315
- return `${shellQuote(cliExecutable)} --allow-all --session-id ${shellQuote(targetSessionId)}`;
326
+ const extra = (Array.isArray(extraCliArgs) ? extraCliArgs : []).map((arg) => ` ${shellQuote(arg)}`).join('');
327
+ return `${shellQuote(cliExecutable)} --allow-all --session-id ${shellQuote(targetSessionId)}${extra}`;
328
+ }
329
+
330
+ // ─── The OAR MCP server for the extension engine ─────────────────────────────
331
+ //
332
+ // The Copilot CLI takes extra MCP servers per run through
333
+ // `--additional-mcp-config <json>`, where `<json>` may be `@<file>` (CLI
334
+ // 1.0.83 `--help`: "JSON string or file path (prefix with @)"; the file is
335
+ // read as UTF-8 and merged over ~/.copilot/mcp-config.json for that run
336
+ // only). The config goes to a file rather than inline: the argument then
337
+ // crosses cmd.exe/`start` on Windows and `sh -lc` + `script -c` on the tmux
338
+ // path as one plain path, with no JSON to escape through two shells.
339
+ //
340
+ // Server entry shape (the CLI's own `copilot mcp add` writes the same):
341
+ // `{ type: 'local', command, args, env, tools: ['*'], timeout }`. `timeout` is
342
+ // the per-call limit in ms; without it the CLI cuts a tool call off at 180 s,
343
+ // shorter than one remote_relay wait.
344
+
345
+ export function resolveOarMcpServerScriptPath(env = {}, { pathImpl = path } = {}) {
346
+ const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
347
+ if (repoRoot) return pathImpl.join(repoRoot, 'server', 'mcp', 'oar-mcp-server.mjs');
348
+ const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
349
+ if (serverDir) return pathImpl.join(serverDir, 'mcp', 'oar-mcp-server.mjs');
350
+ return OAR_MCP_SERVER_SCRIPT_PATH;
351
+ }
352
+
353
+ /** The `--additional-mcp-config` document for one extension-engine session. */
354
+ export function buildCopilotOarMcpConfig(targetSessionId, env = {}, {
355
+ nodePath = normalizeText(env?.COPILOT_WEB_RELAY_NODE) || process.execPath,
356
+ pathImpl = path,
357
+ } = {}) {
358
+ const launch = buildOarMcpServerLaunch({
359
+ nodePath,
360
+ conversationId: targetSessionId,
361
+ env,
362
+ scriptPath: resolveOarMcpServerScriptPath(env, { pathImpl }),
363
+ });
364
+ return {
365
+ mcpServers: {
366
+ [OAR_MCP_SERVER_NAME]: {
367
+ type: 'local',
368
+ command: launch.command,
369
+ args: launch.args,
370
+ env: launch.env,
371
+ tools: ['*'],
372
+ timeout: OAR_MCP_TOOL_TIMEOUT_MS,
373
+ },
374
+ },
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Writes the session's MCP config next to its worker log and returns the file
380
+ * path, or null — best-effort like the log itself: a session that cannot get
381
+ * the file still starts, just without the OAR tools. No secrets go in: the
382
+ * server reads the token from the relay config the file points at.
383
+ */
384
+ export function prepareOarMcpConfigFile(targetSessionId, launchEnv = {}, {
385
+ fsImpl = fs,
386
+ pathImpl = path,
387
+ nodePath = undefined,
388
+ } = {}) {
389
+ if (isOarMcpServerDisabled(launchEnv)) return null;
390
+ try {
391
+ const config = buildCopilotOarMcpConfig(targetSessionId, launchEnv, {
392
+ pathImpl,
393
+ ...(nodePath ? { nodePath } : {}),
394
+ });
395
+ const baseDir = resolveWorkerLogDir(launchEnv, { pathImpl });
396
+ fsImpl.mkdirSync(baseDir, { recursive: true });
397
+ const filePath = pathImpl.join(baseDir, `worker-${safeWorkerFileId(targetSessionId)}.mcp.json`);
398
+ fsImpl.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
399
+ return filePath;
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+
405
+ function oarMcpCliArgs(configFilePath) {
406
+ const filePath = normalizeText(configFilePath);
407
+ return filePath ? ['--additional-mcp-config', `@${filePath}`] : [];
316
408
  }
317
409
 
318
410
  export function normalizeTmuxSessionName(targetSessionId) {
@@ -416,22 +508,31 @@ export function createWorkerSecretEnvFile(env = {}, {
416
508
  * their output discarded (`stdio: 'ignore'` / bare tmux exec), so a crash
417
509
  * left zero forensic trail — the failure mode that made the 2026-08-11
418
510
  * incident undiagnosable. Best-effort by design: a log problem must never
419
- * block a worker spawn. Naive rotation: >10 MB rolls to `<file>.1`.
511
+ * block a worker spawn. Naive rotation: >10 MB rolls to `<file>.1`, checked
512
+ * at each launch on every platform.
420
513
  */
514
+ function resolveWorkerLogDir(launchEnv = {}, { pathImpl = path } = {}) {
515
+ const explicitDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_LOG_DIR);
516
+ const serverDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_SERVER_DIR);
517
+ // `new URL(...).pathname` yields '/C:/git/...' on Windows, which pathImpl.join
518
+ // then mangles into '\C:\git\...'; fileURLToPath decodes it to a real host path.
519
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
520
+ return explicitDir
521
+ || (serverDir ? pathImpl.join(serverDir, 'logs') : pathImpl.join(moduleDir, '..', 'logs'));
522
+ }
523
+
524
+ function safeWorkerFileId(targetSessionId) {
525
+ return String(targetSessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || 'worker';
526
+ }
527
+
421
528
  export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
422
529
  fsImpl = fs,
423
530
  pathImpl = path,
424
531
  } = {}) {
425
532
  try {
426
- const explicitDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_LOG_DIR);
427
- const serverDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_SERVER_DIR);
428
- // `new URL(...).pathname` yields '/C:/git/...' on Windows, which pathImpl.join
429
- // then mangles into '\C:\git\...'; fileURLToPath decodes it to a real host path.
430
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
431
- const baseDir = explicitDir
432
- || (serverDir ? pathImpl.join(serverDir, 'logs') : pathImpl.join(moduleDir, '..', 'logs'));
533
+ const baseDir = resolveWorkerLogDir(launchEnv, { pathImpl });
433
534
  fsImpl.mkdirSync(baseDir, { recursive: true });
434
- const safeId = String(targetSessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || 'worker';
535
+ const safeId = safeWorkerFileId(targetSessionId);
435
536
  const logPath = pathImpl.join(baseDir, `worker-${safeId}.log`);
436
537
  try {
437
538
  const stats = fsImpl.statSync(logPath);
@@ -446,6 +547,9 @@ export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
446
547
  export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
447
548
  secretEnvFilePath = '',
448
549
  workerLogPath = '',
550
+ // Extension engine only: appended to the CLI command, each shell-quoted
551
+ // (and quoted again with it for `script -c`).
552
+ extraCliArgs = [],
449
553
  } = {}) {
450
554
  const launchEnv = {
451
555
  ...env,
@@ -503,6 +607,10 @@ export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
503
607
  'GROK_RELAY_MODEL',
504
608
  'GROK_CLI_COMMAND',
505
609
  'GROK_ALWAYS_APPROVE',
610
+ // The OAR MCP server kill switch (the Grok worker reads it too), and the
611
+ // extension engine's "it is attached" marker.
612
+ 'OAR_MCP_SERVER',
613
+ 'OAR_MCP_SERVER_ATTACHED',
506
614
  'SESSION_ID',
507
615
  'COPILOT_WORKSPACE_ROOT',
508
616
  'INIT_CWD',
@@ -515,7 +623,7 @@ export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
515
623
  const secretPrefix = normalizedSecretEnvFilePath
516
624
  ? `. ${shellQuote(normalizedSecretEnvFilePath)} || exit $?; rm -f ${shellQuote(normalizedSecretEnvFilePath)}; rmdir ${shellQuote(path.dirname(normalizedSecretEnvFilePath))}; `
517
625
  : '';
518
- const workerCommand = buildPosixWorkerLaunchCommand(targetSessionId, launchEnv);
626
+ const workerCommand = buildPosixWorkerLaunchCommand(targetSessionId, launchEnv, extraCliArgs);
519
627
  if (resolveNodeWorkerDescriptor(launchEnv)) {
520
628
  // Node workers (Claude, Cursor, Grok) are plain processes; no pseudo-TTY
521
629
  // needed. Their output is teed to the worker log so a crash leaves a
@@ -574,6 +682,12 @@ export async function launchSessionCli({
574
682
  detachedPollDelayMs = 200,
575
683
  allowProcessReuse = true,
576
684
  createSecretEnvFileImpl = createWorkerSecretEnvFile,
685
+ // Extension engine: writes the session's OAR MCP config, returns its path
686
+ // (or null to launch without it).
687
+ prepareOarMcpConfigImpl = prepareOarMcpConfigFile,
688
+ // Windows console launch: resolves (and rotates) the worker log the worker
689
+ // then writes itself; returns its path or null.
690
+ prepareWorkerLogFileImpl = prepareWorkerLogFile,
577
691
  } = {}) {
578
692
  const target = String(targetSessionId || '').trim();
579
693
  if (!target) throw new Error('missing-target-session-id');
@@ -610,6 +724,17 @@ export async function launchSessionCli({
610
724
  ...launchEnv,
611
725
  SESSION_ID: target,
612
726
  };
727
+ // Set per launch below, never inherited: a relay started from inside a
728
+ // worker session would otherwise hand its workers that session's log.
729
+ delete launchSessionEnv[WORKER_LOG_FILE_ENV];
730
+ // The extension engine gets the OAR MCP server (remote_relay + preview) the
731
+ // node workers carry in-process; every launch path appends the same args.
732
+ const extensionCliArgs = resolveNodeWorkerDescriptor(launchSessionEnv)
733
+ ? []
734
+ : oarMcpCliArgs(prepareOarMcpConfigImpl(target, launchSessionEnv));
735
+ // Tells the in-CLI web-relay extension that `preview` is a real tool in this
736
+ // session, so its prompt context need not teach the HTTP fallback.
737
+ if (extensionCliArgs.length) launchSessionEnv.OAR_MCP_SERVER_ATTACHED = '1';
613
738
 
614
739
  if (isTmuxAvailable({ platform, execFileSyncImpl })) {
615
740
  const sessionName = normalizeTmuxSessionName(target);
@@ -647,6 +772,7 @@ export async function launchSessionCli({
647
772
  buildTmuxWorkerShellCommand(target, launchSessionEnv, {
648
773
  secretEnvFilePath: secretEnvFile?.filePath,
649
774
  workerLogPath: tmuxWorkerLogPath || '',
775
+ extraCliArgs: extensionCliArgs,
650
776
  }),
651
777
  ], {
652
778
  env: tmuxEnv,
@@ -717,14 +843,22 @@ export async function launchSessionCli({
717
843
  '--allow-all',
718
844
  '--session-id',
719
845
  target,
846
+ ...extensionCliArgs,
720
847
  ])
721
848
  : (nodeWorker
722
849
  ? [nodeWorker.scriptPath, '--session-id', target]
723
- : ['--allow-all', '--session-id', target]);
850
+ : ['--allow-all', '--session-id', target, ...extensionCliArgs]);
724
851
  // POSIX node workers tee stdout/stderr into the worker log (a crash must
725
852
  // leave a trail). On win32 the `start` intermediary opens its own console,
726
- // so fd inheritance cannot reach the worker — its window shows the output.
853
+ // so fd inheritance cannot reach the worker, and a shell redirect would
854
+ // leave its window empty: the worker is told the file and copies its own
855
+ // output into it (shared/worker-runtime/worker-log-file.mjs). Only the path
856
+ // travels; nothing of the environment or command line is written to the log.
727
857
  let detachedStdio = 'ignore';
858
+ if (platform === 'win32' && nodeWorker) {
859
+ const workerLogPath = prepareWorkerLogFileImpl(target, launchSessionEnv);
860
+ if (workerLogPath) launchSessionEnv[WORKER_LOG_FILE_ENV] = workerLogPath;
861
+ }
728
862
  if (platform !== 'win32' && nodeWorker) {
729
863
  const workerLogPath = prepareWorkerLogFile(target, launchSessionEnv);
730
864
  if (workerLogPath) {