@oar-sh/oar 0.9.3 → 0.9.4
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/extension.mjs +6 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
- package/CHANGELOG.md +67 -0
- package/README.md +62 -13
- package/package.json +5 -2
- package/server/README.md +43 -3
- package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
- package/server/claude-worker/claude-session-process.mjs +92 -25
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +12 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +83 -9
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
- package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
- package/server/db-schema.mjs +8 -0
- package/server/grok-worker/acp-client.mjs +92 -6
- package/server/grok-worker/grok-turn-runner.mjs +59 -8
- package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
- package/server/mcp/oar-mcp-server.mjs +473 -0
- package/server/migrations/0005-remote-relays.mjs +90 -0
- package/server/public/app/action-confirmations.js +141 -13
- package/server/public/app/api-client.js +103 -0
- package/server/public/app/bootstrap.js +21 -6
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-view.js +98 -7
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +4 -1
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/relay-prompt-sanitizer.mjs +48 -5
- package/server/public/app/remote-relay-origin-view.mjs +76 -0
- package/server/public/app/remote-relay-shared.mjs +85 -0
- package/server/public/app/remote-relays-settings.js +618 -0
- package/server/public/app/remote-relays-store.mjs +126 -0
- package/server/public/app/settings-modal.js +4 -2
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +18 -1
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +4 -0
- package/server/public/index.html +257 -8
- package/server/relay-tools.md +5 -1
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/question-repository.mjs +3 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +6 -1
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +90 -16
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +80 -29
- package/server/server-runtime.mjs +238 -0
- package/server/services/github-ci-activity-service.mjs +199 -0
- package/server/services/host-suspend-command.mjs +38 -0
- package/server/services/host-suspend-service.mjs +254 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/remote-relay-caller-context.mjs +341 -0
- package/server/services/remote-relay-client.mjs +332 -0
- package/server/services/remote-relay-dispatcher.mjs +1500 -0
- package/server/services/remote-relay-inbound.mjs +187 -0
- package/server/services/remote-relay-pairing.mjs +189 -0
- package/server/services/remote-relay-registry-service.mjs +581 -0
- package/server/services/session-worker-launch-service.mjs +131 -12
- package/shared/remote-relay-contract.mjs +539 -0
- package/shared/remote-relay-mentions.mjs +129 -0
- package/shared/remote-relay-tool-core.mjs +314 -0
- package/shared/worker-runtime/api-client.mjs +112 -22
|
@@ -6,6 +6,16 @@ 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
|
+
|
|
17
|
+
export { isOarMcpServerDisabled };
|
|
18
|
+
|
|
9
19
|
function shellQuote(value) {
|
|
10
20
|
return `'${String(value || '').replace(/'/g, `'\\''`)}'`;
|
|
11
21
|
}
|
|
@@ -302,7 +312,7 @@ function resolveNodeWorkerDescriptor(env = {}) {
|
|
|
302
312
|
};
|
|
303
313
|
}
|
|
304
314
|
|
|
305
|
-
function buildPosixWorkerLaunchCommand(targetSessionId, env = {}) {
|
|
315
|
+
function buildPosixWorkerLaunchCommand(targetSessionId, env = {}, extraCliArgs = []) {
|
|
306
316
|
const nodeWorker = resolveNodeWorkerDescriptor(env);
|
|
307
317
|
if (nodeWorker) {
|
|
308
318
|
const nodeExecutable = normalizeText(env?.COPILOT_WEB_RELAY_NODE) || 'node';
|
|
@@ -312,7 +322,88 @@ function buildPosixWorkerLaunchCommand(targetSessionId, env = {}) {
|
|
|
312
322
|
|| normalizeText(env?.COPILOT_CLI_EXECUTABLE)
|
|
313
323
|
|| normalizeText(env?.COPILOT_CLI_PATH)
|
|
314
324
|
|| 'copilot';
|
|
315
|
-
|
|
325
|
+
const extra = (Array.isArray(extraCliArgs) ? extraCliArgs : []).map((arg) => ` ${shellQuote(arg)}`).join('');
|
|
326
|
+
return `${shellQuote(cliExecutable)} --allow-all --session-id ${shellQuote(targetSessionId)}${extra}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── The OAR MCP server for the extension engine ─────────────────────────────
|
|
330
|
+
//
|
|
331
|
+
// The Copilot CLI takes extra MCP servers per run through
|
|
332
|
+
// `--additional-mcp-config <json>`, where `<json>` may be `@<file>` (CLI
|
|
333
|
+
// 1.0.83 `--help`: "JSON string or file path (prefix with @)"; the file is
|
|
334
|
+
// read as UTF-8 and merged over ~/.copilot/mcp-config.json for that run
|
|
335
|
+
// only). The config goes to a file rather than inline: the argument then
|
|
336
|
+
// crosses cmd.exe/`start` on Windows and `sh -lc` + `script -c` on the tmux
|
|
337
|
+
// path as one plain path, with no JSON to escape through two shells.
|
|
338
|
+
//
|
|
339
|
+
// Server entry shape (the CLI's own `copilot mcp add` writes the same):
|
|
340
|
+
// `{ type: 'local', command, args, env, tools: ['*'], timeout }`. `timeout` is
|
|
341
|
+
// the per-call limit in ms; without it the CLI cuts a tool call off at 180 s,
|
|
342
|
+
// shorter than one remote_relay wait.
|
|
343
|
+
|
|
344
|
+
export function resolveOarMcpServerScriptPath(env = {}, { pathImpl = path } = {}) {
|
|
345
|
+
const repoRoot = normalizeText(env?.COPILOT_WEB_RELAY_ROOT);
|
|
346
|
+
if (repoRoot) return pathImpl.join(repoRoot, 'server', 'mcp', 'oar-mcp-server.mjs');
|
|
347
|
+
const serverDir = normalizeText(env?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
348
|
+
if (serverDir) return pathImpl.join(serverDir, 'mcp', 'oar-mcp-server.mjs');
|
|
349
|
+
return OAR_MCP_SERVER_SCRIPT_PATH;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The `--additional-mcp-config` document for one extension-engine session. */
|
|
353
|
+
export function buildCopilotOarMcpConfig(targetSessionId, env = {}, {
|
|
354
|
+
nodePath = normalizeText(env?.COPILOT_WEB_RELAY_NODE) || process.execPath,
|
|
355
|
+
pathImpl = path,
|
|
356
|
+
} = {}) {
|
|
357
|
+
const launch = buildOarMcpServerLaunch({
|
|
358
|
+
nodePath,
|
|
359
|
+
conversationId: targetSessionId,
|
|
360
|
+
env,
|
|
361
|
+
scriptPath: resolveOarMcpServerScriptPath(env, { pathImpl }),
|
|
362
|
+
});
|
|
363
|
+
return {
|
|
364
|
+
mcpServers: {
|
|
365
|
+
[OAR_MCP_SERVER_NAME]: {
|
|
366
|
+
type: 'local',
|
|
367
|
+
command: launch.command,
|
|
368
|
+
args: launch.args,
|
|
369
|
+
env: launch.env,
|
|
370
|
+
tools: ['*'],
|
|
371
|
+
timeout: OAR_MCP_TOOL_TIMEOUT_MS,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Writes the session's MCP config next to its worker log and returns the file
|
|
379
|
+
* path, or null — best-effort like the log itself: a session that cannot get
|
|
380
|
+
* the file still starts, just without the OAR tools. No secrets go in: the
|
|
381
|
+
* server reads the token from the relay config the file points at.
|
|
382
|
+
*/
|
|
383
|
+
export function prepareOarMcpConfigFile(targetSessionId, launchEnv = {}, {
|
|
384
|
+
fsImpl = fs,
|
|
385
|
+
pathImpl = path,
|
|
386
|
+
nodePath = undefined,
|
|
387
|
+
} = {}) {
|
|
388
|
+
if (isOarMcpServerDisabled(launchEnv)) return null;
|
|
389
|
+
try {
|
|
390
|
+
const config = buildCopilotOarMcpConfig(targetSessionId, launchEnv, {
|
|
391
|
+
pathImpl,
|
|
392
|
+
...(nodePath ? { nodePath } : {}),
|
|
393
|
+
});
|
|
394
|
+
const baseDir = resolveWorkerLogDir(launchEnv, { pathImpl });
|
|
395
|
+
fsImpl.mkdirSync(baseDir, { recursive: true });
|
|
396
|
+
const filePath = pathImpl.join(baseDir, `worker-${safeWorkerFileId(targetSessionId)}.mcp.json`);
|
|
397
|
+
fsImpl.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
398
|
+
return filePath;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function oarMcpCliArgs(configFilePath) {
|
|
405
|
+
const filePath = normalizeText(configFilePath);
|
|
406
|
+
return filePath ? ['--additional-mcp-config', `@${filePath}`] : [];
|
|
316
407
|
}
|
|
317
408
|
|
|
318
409
|
export function normalizeTmuxSessionName(targetSessionId) {
|
|
@@ -418,20 +509,28 @@ export function createWorkerSecretEnvFile(env = {}, {
|
|
|
418
509
|
* incident undiagnosable. Best-effort by design: a log problem must never
|
|
419
510
|
* block a worker spawn. Naive rotation: >10 MB rolls to `<file>.1`.
|
|
420
511
|
*/
|
|
512
|
+
function resolveWorkerLogDir(launchEnv = {}, { pathImpl = path } = {}) {
|
|
513
|
+
const explicitDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_LOG_DIR);
|
|
514
|
+
const serverDir = normalizeText(launchEnv?.COPILOT_WEB_RELAY_SERVER_DIR);
|
|
515
|
+
// `new URL(...).pathname` yields '/C:/git/...' on Windows, which pathImpl.join
|
|
516
|
+
// then mangles into '\C:\git\...'; fileURLToPath decodes it to a real host path.
|
|
517
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
518
|
+
return explicitDir
|
|
519
|
+
|| (serverDir ? pathImpl.join(serverDir, 'logs') : pathImpl.join(moduleDir, '..', 'logs'));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function safeWorkerFileId(targetSessionId) {
|
|
523
|
+
return String(targetSessionId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64) || 'worker';
|
|
524
|
+
}
|
|
525
|
+
|
|
421
526
|
export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
|
|
422
527
|
fsImpl = fs,
|
|
423
528
|
pathImpl = path,
|
|
424
529
|
} = {}) {
|
|
425
530
|
try {
|
|
426
|
-
const
|
|
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'));
|
|
531
|
+
const baseDir = resolveWorkerLogDir(launchEnv, { pathImpl });
|
|
433
532
|
fsImpl.mkdirSync(baseDir, { recursive: true });
|
|
434
|
-
const safeId =
|
|
533
|
+
const safeId = safeWorkerFileId(targetSessionId);
|
|
435
534
|
const logPath = pathImpl.join(baseDir, `worker-${safeId}.log`);
|
|
436
535
|
try {
|
|
437
536
|
const stats = fsImpl.statSync(logPath);
|
|
@@ -446,6 +545,9 @@ export function prepareWorkerLogFile(targetSessionId, launchEnv = {}, {
|
|
|
446
545
|
export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
447
546
|
secretEnvFilePath = '',
|
|
448
547
|
workerLogPath = '',
|
|
548
|
+
// Extension engine only: appended to the CLI command, each shell-quoted
|
|
549
|
+
// (and quoted again with it for `script -c`).
|
|
550
|
+
extraCliArgs = [],
|
|
449
551
|
} = {}) {
|
|
450
552
|
const launchEnv = {
|
|
451
553
|
...env,
|
|
@@ -503,6 +605,10 @@ export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
|
503
605
|
'GROK_RELAY_MODEL',
|
|
504
606
|
'GROK_CLI_COMMAND',
|
|
505
607
|
'GROK_ALWAYS_APPROVE',
|
|
608
|
+
// The OAR MCP server kill switch (the Grok worker reads it too), and the
|
|
609
|
+
// extension engine's "it is attached" marker.
|
|
610
|
+
'OAR_MCP_SERVER',
|
|
611
|
+
'OAR_MCP_SERVER_ATTACHED',
|
|
506
612
|
'SESSION_ID',
|
|
507
613
|
'COPILOT_WORKSPACE_ROOT',
|
|
508
614
|
'INIT_CWD',
|
|
@@ -515,7 +621,7 @@ export function buildTmuxWorkerShellCommand(targetSessionId, env = {}, {
|
|
|
515
621
|
const secretPrefix = normalizedSecretEnvFilePath
|
|
516
622
|
? `. ${shellQuote(normalizedSecretEnvFilePath)} || exit $?; rm -f ${shellQuote(normalizedSecretEnvFilePath)}; rmdir ${shellQuote(path.dirname(normalizedSecretEnvFilePath))}; `
|
|
517
623
|
: '';
|
|
518
|
-
const workerCommand = buildPosixWorkerLaunchCommand(targetSessionId, launchEnv);
|
|
624
|
+
const workerCommand = buildPosixWorkerLaunchCommand(targetSessionId, launchEnv, extraCliArgs);
|
|
519
625
|
if (resolveNodeWorkerDescriptor(launchEnv)) {
|
|
520
626
|
// Node workers (Claude, Cursor, Grok) are plain processes; no pseudo-TTY
|
|
521
627
|
// needed. Their output is teed to the worker log so a crash leaves a
|
|
@@ -574,6 +680,9 @@ export async function launchSessionCli({
|
|
|
574
680
|
detachedPollDelayMs = 200,
|
|
575
681
|
allowProcessReuse = true,
|
|
576
682
|
createSecretEnvFileImpl = createWorkerSecretEnvFile,
|
|
683
|
+
// Extension engine: writes the session's OAR MCP config, returns its path
|
|
684
|
+
// (or null to launch without it).
|
|
685
|
+
prepareOarMcpConfigImpl = prepareOarMcpConfigFile,
|
|
577
686
|
} = {}) {
|
|
578
687
|
const target = String(targetSessionId || '').trim();
|
|
579
688
|
if (!target) throw new Error('missing-target-session-id');
|
|
@@ -610,6 +719,14 @@ export async function launchSessionCli({
|
|
|
610
719
|
...launchEnv,
|
|
611
720
|
SESSION_ID: target,
|
|
612
721
|
};
|
|
722
|
+
// The extension engine gets the OAR MCP server (remote_relay + preview) the
|
|
723
|
+
// node workers carry in-process; every launch path appends the same args.
|
|
724
|
+
const extensionCliArgs = resolveNodeWorkerDescriptor(launchSessionEnv)
|
|
725
|
+
? []
|
|
726
|
+
: oarMcpCliArgs(prepareOarMcpConfigImpl(target, launchSessionEnv));
|
|
727
|
+
// Tells the in-CLI web-relay extension that `preview` is a real tool in this
|
|
728
|
+
// session, so its prompt context need not teach the HTTP fallback.
|
|
729
|
+
if (extensionCliArgs.length) launchSessionEnv.OAR_MCP_SERVER_ATTACHED = '1';
|
|
613
730
|
|
|
614
731
|
if (isTmuxAvailable({ platform, execFileSyncImpl })) {
|
|
615
732
|
const sessionName = normalizeTmuxSessionName(target);
|
|
@@ -647,6 +764,7 @@ export async function launchSessionCli({
|
|
|
647
764
|
buildTmuxWorkerShellCommand(target, launchSessionEnv, {
|
|
648
765
|
secretEnvFilePath: secretEnvFile?.filePath,
|
|
649
766
|
workerLogPath: tmuxWorkerLogPath || '',
|
|
767
|
+
extraCliArgs: extensionCliArgs,
|
|
650
768
|
}),
|
|
651
769
|
], {
|
|
652
770
|
env: tmuxEnv,
|
|
@@ -717,10 +835,11 @@ export async function launchSessionCli({
|
|
|
717
835
|
'--allow-all',
|
|
718
836
|
'--session-id',
|
|
719
837
|
target,
|
|
838
|
+
...extensionCliArgs,
|
|
720
839
|
])
|
|
721
840
|
: (nodeWorker
|
|
722
841
|
? [nodeWorker.scriptPath, '--session-id', target]
|
|
723
|
-
: ['--allow-all', '--session-id', target]);
|
|
842
|
+
: ['--allow-all', '--session-id', target, ...extensionCliArgs]);
|
|
724
843
|
// POSIX node workers tee stdout/stderr into the worker log (a crash must
|
|
725
844
|
// leave a trail). On win32 the `start` intermediary opens its own console,
|
|
726
845
|
// so fd inheritance cannot reach the worker — its window shows the output.
|