@oar-sh/oar 0.9.21 → 0.9.23
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/CHANGELOG.md +206 -0
- package/README.md +9 -9
- package/bin/oar.js +28 -0
- package/package.json +13 -14
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +9 -4
- package/server/claude-worker/claude-session-worker-main.mjs +10 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
- package/server/cursor-worker/cursor-session-worker.mjs +9 -1
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +1 -0
- package/server/grok-worker/grok-session-worker.mjs +10 -1
- package/server/grok-worker/grok-turn-runner.mjs +1 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +7 -0
- package/server/public/app/ask-user-view.js +4 -3
- package/server/public/app/bootstrap.js +62 -4
- package/server/public/app/chat-actions-menu-state.mjs +2 -0
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-view.js +306 -49
- package/server/public/app/header-priority-actions.mjs +5 -1
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/inbox-ui.js +70 -79
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +17 -0
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/session-history-ui.js +1 -0
- package/server/public/app/settings-modal.js +6 -0
- package/server/public/app/socket-handlers.js +42 -11
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +8 -6
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/index.html +391 -214
- package/server/repositories/message-repository.mjs +10 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/messages-routes.mjs +46 -34
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +77 -6
- package/server/server-runtime.mjs +199 -171
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/earlier-database-service.mjs +1 -1
- package/server/services/file-serving.mjs +13 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +7 -4
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-console-log.mjs +2 -3
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -4
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +143 -15
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/update-install-service.mjs +32 -1
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +26 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `preinstall` of @oar-sh/oar. Nothing of OAR loads before the `try`:
|
|
3
|
-
* a module of this package that cannot load (a broken file, a missing one)
|
|
4
|
-
* must not fail the install on any platform. Only a copy of an old
|
|
5
|
-
* database that failed (RescueCopyError) exits 1, so npm stops the update
|
|
6
|
-
* and puts the old package, data and all, back in place; anything else is
|
|
7
|
-
* one line and exit 0, as this script only ever copies. See
|
|
8
|
-
* package-data-rescue.mjs.
|
|
9
|
-
*/
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const { rescuePackageData } = await import('./package-data-rescue.mjs');
|
|
14
|
-
rescuePackageData({ packageRoot: fileURLToPath(new URL('../..', import.meta.url)) });
|
|
15
|
-
} catch (error) {
|
|
16
|
-
if (error?.rescueCopyFailed === true) {
|
|
17
|
-
console.error(`[oar] ${error.message}`);
|
|
18
|
-
process.exitCode = 1;
|
|
19
|
-
} else {
|
|
20
|
-
console.error(`[oar] Did not copy the data of the version being replaced: ${error?.message || error}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The preinstall's work (package-data-rescue-preinstall.mjs), on every
|
|
3
|
-
* platform. Relays before 0.9.14 could run on the database and uploads
|
|
4
|
-
* inside the package (Windows autostart, a plain `gh copilot`), and
|
|
5
|
-
* `npm i -g` deletes the old package. While install scripts run, npm keeps
|
|
6
|
-
* it as its retired folder (npmRetiredPath; a scan for npm-shaped names only
|
|
7
|
-
* when that is missing): its database (-wal, -shm), Cursor stores, uploads
|
|
8
|
-
* and config.json are copied, copy only, to
|
|
9
|
-
* `<data folder>/rescued-package-data/<time>/` (written as `.partial`, then
|
|
10
|
-
* renamed), and the relay takes them over at its next start. Any of them
|
|
11
|
-
* makes the old package a source (its uploads beyond the README.md and
|
|
12
|
-
* image_input.md it ships). Nothing to copy, or not an installed package:
|
|
13
|
-
* nothing happens, nothing is printed. A failed copy throws
|
|
14
|
-
* RescueCopyError: exit 1, npm puts the old package back. npm hides a successful script's output, hence the README.txt.
|
|
15
|
-
*/
|
|
16
|
-
import crypto from 'crypto';
|
|
17
|
-
import fs from 'fs';
|
|
18
|
-
import path from 'path';
|
|
19
|
-
|
|
20
|
-
import { DB_FILE_NAMES, sideFiles } from './database-file.mjs';
|
|
21
|
-
import { RESCUE_DIR_NAME, copyMissing, isRetiredName } from './earlier-database-service.mjs';
|
|
22
|
-
import { resolveRelayStateDirs, resolveStateLayout } from './oar-state-layout.mjs';
|
|
23
|
-
|
|
24
|
-
// Old packages only ever held the name before 0.9.20; a later one may hold oar.db.
|
|
25
|
-
const DB_FILES = DB_FILE_NAMES.flatMap(sideFiles);
|
|
26
|
-
|
|
27
|
-
// What a relay left in a package's server folder; a folder that cannot be
|
|
28
|
-
// read throws (no copy failure: nothing was detected yet).
|
|
29
|
-
function hasRelayData(fsImpl, serverDir) {
|
|
30
|
-
const has = (...parts) => fsImpl.existsSync(path.join(serverDir, ...parts));
|
|
31
|
-
const anyUpload = (...parts) => has(...parts) && fsImpl.readdirSync(path.join(serverDir, ...parts)).some((name) => /^[a-f0-9]{64}$/.test(name));
|
|
32
|
-
return DB_FILE_NAMES.some((name) => has('data', name)) || has('data', 'cursor-agents') || has('config.json') || anyUpload('uploads') || anyUpload('data', 'uploads');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Where npm's reify moves a package it replaces (@npmcli/arborist retire-path.js). */
|
|
36
|
-
export function npmRetiredPath(packageDir) {
|
|
37
|
-
const hash = crypto.createHash('sha1').update(packageDir).digest('base64').replace(/[^a-zA-Z0-9]+/g, '').slice(0, 8);
|
|
38
|
-
return path.resolve(path.dirname(packageDir), `.${path.basename(packageDir)}-${hash}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** A copy of an old database failed: the one error that stops the install. */
|
|
42
|
-
export class RescueCopyError extends Error {
|
|
43
|
-
constructor(message, options) {
|
|
44
|
-
super(message, options);
|
|
45
|
-
this.name = 'RescueCopyError';
|
|
46
|
-
this.rescueCopyFailed = true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Returns { status: 'not-installed' | 'nothing' | 'rescued', rescued: [{ from,
|
|
52
|
-
* to }] }; throws a RescueCopyError, naming what to save, when a copy fails.
|
|
53
|
-
*/
|
|
54
|
-
export function rescuePackageData({
|
|
55
|
-
packageRoot,
|
|
56
|
-
env = process.env,
|
|
57
|
-
platform = process.platform,
|
|
58
|
-
now = () => new Date(),
|
|
59
|
-
fsImpl = fs,
|
|
60
|
-
logger = console,
|
|
61
|
-
} = {}) {
|
|
62
|
-
const resolvedRoot = path.resolve(String(packageRoot || ''));
|
|
63
|
-
const scope = path.dirname(resolvedRoot);
|
|
64
|
-
if (path.basename(scope) !== '@oar-sh' || path.basename(path.dirname(scope)) !== 'node_modules') {
|
|
65
|
-
return { status: 'not-installed', rescued: [] };
|
|
66
|
-
}
|
|
67
|
-
// Detection: npm's retired name, else a scan for npm-shaped names (a path
|
|
68
|
-
// in another case hashes differently). Failing here is no copy failure.
|
|
69
|
-
let sources;
|
|
70
|
-
try {
|
|
71
|
-
const exact = npmRetiredPath(resolvedRoot);
|
|
72
|
-
const retired = fsImpl.existsSync(exact)
|
|
73
|
-
? [exact]
|
|
74
|
-
: fsImpl.readdirSync(scope).filter((name) => isRetiredName(name, path.basename(resolvedRoot))).map((name) => path.join(scope, name));
|
|
75
|
-
sources = retired.filter((dir) => hasRelayData(fsImpl, path.join(dir, 'server')));
|
|
76
|
-
} catch (error) {
|
|
77
|
-
throw new Error(`Could not look for the data of the version being replaced: ${error?.message || error}`, { cause: error });
|
|
78
|
-
}
|
|
79
|
-
if (!sources.length) return { status: 'nothing', rescued: [] };
|
|
80
|
-
|
|
81
|
-
// Old data is there: any failure from here stops the install.
|
|
82
|
-
const exists = (file) => fsImpl.existsSync(file);
|
|
83
|
-
const kept = path.join(resolvedRoot, 'server');
|
|
84
|
-
const copyFailed = (from, to, error) => new RescueCopyError(
|
|
85
|
-
`Could not copy the data of the version being replaced (${from}) to ${to}: ${error?.message || error}. `
|
|
86
|
-
+ `The update stops and the version installed stays as it is; copy ${path.join(kept, 'data')} and ${path.join(kept, 'uploads')} somewhere safe by hand, then update again.`,
|
|
87
|
-
{ cause: error },
|
|
88
|
-
);
|
|
89
|
-
let rescueRoot;
|
|
90
|
-
try {
|
|
91
|
-
const layout = resolveStateLayout({ packageRoot: resolvedRoot, env, platform });
|
|
92
|
-
rescueRoot = path.join(resolveRelayStateDirs({ packageRoot: resolvedRoot, env, platform, layout }).dataDir, RESCUE_DIR_NAME);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
throw copyFailed(path.join(sources[0], 'server'), 'the OAR data folder', error);
|
|
95
|
-
}
|
|
96
|
-
const stamp = now().toISOString().replace(/[:.]/g, '-');
|
|
97
|
-
const rescued = [];
|
|
98
|
-
for (const [index, source] of sources.entries()) {
|
|
99
|
-
const serverDir = path.join(source, 'server');
|
|
100
|
-
let target = path.join(rescueRoot, sources.length > 1 ? `${stamp}-${index}` : stamp);
|
|
101
|
-
// Written under a .partial name and renamed when complete: a half copy
|
|
102
|
-
// is never taken for a rescue.
|
|
103
|
-
try {
|
|
104
|
-
for (let n = 1; exists(target) || exists(`${target}.partial`); n += 1) target = `${path.join(rescueRoot, stamp)}-${n}`;
|
|
105
|
-
const partial = `${target}.partial`;
|
|
106
|
-
fsImpl.mkdirSync(rescueRoot, { recursive: true });
|
|
107
|
-
fsImpl.mkdirSync(partial);
|
|
108
|
-
fsImpl.mkdirSync(path.join(partial, 'data'));
|
|
109
|
-
for (const name of DB_FILES) {
|
|
110
|
-
const from = path.join(serverDir, 'data', name);
|
|
111
|
-
if (exists(from)) fsImpl.copyFileSync(from, path.join(partial, 'data', name), fs.constants.COPYFILE_EXCL);
|
|
112
|
-
}
|
|
113
|
-
if (exists(path.join(serverDir, 'data', 'cursor-agents'))) {
|
|
114
|
-
copyMissing(fsImpl, path.join(serverDir, 'data', 'cursor-agents'), path.join(partial, 'data', 'cursor-agents'));
|
|
115
|
-
}
|
|
116
|
-
// Uploads of server/uploads and server/data/uploads, content-addressed.
|
|
117
|
-
for (const uploads of [path.join(serverDir, 'uploads'), path.join(serverDir, 'data', 'uploads')]) {
|
|
118
|
-
if (exists(uploads)) copyMissing(fsImpl, uploads, path.join(partial, 'uploads'));
|
|
119
|
-
}
|
|
120
|
-
if (exists(path.join(serverDir, 'config.json'))) {
|
|
121
|
-
fsImpl.copyFileSync(path.join(serverDir, 'config.json'), path.join(partial, 'config.json'), fs.constants.COPYFILE_EXCL);
|
|
122
|
-
}
|
|
123
|
-
fsImpl.writeFileSync(path.join(partial, 'README.txt'), [
|
|
124
|
-
`Copied by the @oar-sh/oar update on ${now().toISOString()} from`,
|
|
125
|
-
`${serverDir}`,
|
|
126
|
-
'before npm deleted the version it replaced. The relay takes the',
|
|
127
|
-
'database in data\\ over at its next start if it is newer than the',
|
|
128
|
-
'one it has. Nothing here is deleted by OAR.',
|
|
129
|
-
'',
|
|
130
|
-
].join('\r\n'));
|
|
131
|
-
fsImpl.renameSync(partial, target);
|
|
132
|
-
} catch (error) {
|
|
133
|
-
throw copyFailed(serverDir, target, error);
|
|
134
|
-
}
|
|
135
|
-
rescued.push({ from: serverDir, to: target });
|
|
136
|
-
logger.log?.(`[oar] Copied the data of the version being replaced from ${serverDir} to ${target}; the relay takes it over at its next start.`);
|
|
137
|
-
}
|
|
138
|
-
return { status: 'rescued', rescued };
|
|
139
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
|
-
|
|
5
|
-
function existingDirectories(paths = []) {
|
|
6
|
-
const out = [];
|
|
7
|
-
for (const candidate of paths) {
|
|
8
|
-
const normalized = String(candidate || '').trim();
|
|
9
|
-
if (!normalized) continue;
|
|
10
|
-
try {
|
|
11
|
-
if (fs.statSync(normalized).isDirectory()) out.push(normalized);
|
|
12
|
-
} catch {}
|
|
13
|
-
}
|
|
14
|
-
return out;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function resolveTtyConsoleRoot() {
|
|
18
|
-
const require = createRequire(import.meta.url);
|
|
19
|
-
return path.dirname(require.resolve('tty-console'));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function maybeStartTtyConsole({
|
|
23
|
-
serverDir,
|
|
24
|
-
logsDir,
|
|
25
|
-
logger = console,
|
|
26
|
-
} = {}) {
|
|
27
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
28
|
-
|
|
29
|
-
const resolvedServerDir = path.resolve(String(serverDir || process.cwd()).trim() || process.cwd());
|
|
30
|
-
const resolvedLogsDir = path.resolve(String(logsDir || path.join(resolvedServerDir, 'logs')).trim() || path.join(resolvedServerDir, 'logs'));
|
|
31
|
-
fs.mkdirSync(resolvedLogsDir, { recursive: true });
|
|
32
|
-
|
|
33
|
-
let ttyConsoleModule;
|
|
34
|
-
try {
|
|
35
|
-
ttyConsoleModule = await import('tty-console');
|
|
36
|
-
} catch (error) {
|
|
37
|
-
const code = String(error?.code || '').trim();
|
|
38
|
-
if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
|
|
39
|
-
logger?.log?.('[server] tty-console not installed; continuing without terminal console.');
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
logger?.warn?.(`[server] tty-console load failed: ${error?.message || error}`);
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const ttyConsoleApi = ttyConsoleModule?.default || ttyConsoleModule;
|
|
47
|
-
const ConsoleCtor = ttyConsoleApi?.Console;
|
|
48
|
-
if (typeof ConsoleCtor !== 'function') {
|
|
49
|
-
logger?.warn?.('[server] tty-console loaded but Console export was unavailable.');
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const ttyConsoleRoot = resolveTtyConsoleRoot();
|
|
54
|
-
const bundledCommandsDir = path.join(ttyConsoleRoot, 'commands');
|
|
55
|
-
const localCommandsDir = path.join(resolvedServerDir, 'commands');
|
|
56
|
-
const commandsDirs = existingDirectories([bundledCommandsDir, localCommandsDir]);
|
|
57
|
-
const configPath = path.join(resolvedLogsDir, 'console_config.json');
|
|
58
|
-
const historyPath = path.join(resolvedLogsDir, 'console_history.json');
|
|
59
|
-
|
|
60
|
-
const consoleOptions = {
|
|
61
|
-
configPath,
|
|
62
|
-
historyPath,
|
|
63
|
-
exitOnStop: false,
|
|
64
|
-
};
|
|
65
|
-
if (commandsDirs.length === 1) {
|
|
66
|
-
consoleOptions.commandsDir = commandsDirs[0];
|
|
67
|
-
} else if (commandsDirs.length > 1) {
|
|
68
|
-
consoleOptions.commandsDir = commandsDirs;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const runtime = new ConsoleCtor(consoleOptions);
|
|
72
|
-
runtime.start?.();
|
|
73
|
-
logger?.log?.(`[server] tty-console started (${commandsDirs.join(', ')})`);
|
|
74
|
-
return runtime;
|
|
75
|
-
}
|