@oar-sh/oar 0.9.2 → 0.9.3
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 +269 -0
- package/README.md +407 -424
- package/bin/oar.js +25 -10
- package/package.json +7 -4
- package/server/README.md +1 -1
- package/server/claude-worker/claude-session-process.mjs +981 -44
- package/server/claude-worker/claude-session-worker.mjs +45 -4
- package/server/claude-worker/claude-turn-publisher.mjs +81 -20
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +34 -12
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +101 -12
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2269 -492
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +82 -20
- package/server/db-schema.mjs +20 -0
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/public/app/api-client.js +22 -2
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +37 -2
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +33 -18
- package/server/public/app/bootstrap.js +34 -8
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +919 -194
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/journal-view.js +145 -5
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +17 -2
- package/server/public/app/settings-modal.js +8 -6
- package/server/public/app/socket-handlers.js +19 -0
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +126 -15
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/tool-activity-emoji.mjs +36 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/index.html +206 -9
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +2 -0
- package/server/repositories/message-repository.mjs +61 -18
- package/server/repositories/question-repository.mjs +15 -0
- package/server/repositories/session-repository.mjs +7 -31
- package/server/routes/messages-routes.mjs +488 -46
- package/server/routes/sessions-routes.mjs +298 -55
- package/server/server-runtime.mjs +65 -25
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/relay-prompt-sanitizer.mjs +17 -2
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/sdk-session-import-service.mjs +49 -7
- package/server/services/session-worker-registry-service.mjs +75 -0
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +23 -1
- package/server/services/steer-resend.mjs +45 -0
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/control-poller.mjs +4 -1
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- package/shared/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/worker-crash-guard.mjs +40 -25
- package/shared/worker-runtime/heartbeat.mjs +55 -1
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +119 -8
- package/server/claude-worker/claude-session-test-harness.mjs +0 -250
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -382
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.cmd +0 -1
- package/server/services/fixtures/claude-auth-stub.mjs +0 -76
- package/server/services/fixtures/claude-auth-stub.sh +0 -4
- package/server/services/fixtures/cli-install-stub.cmd +0 -1
- package/server/services/fixtures/cli-install-stub.mjs +0 -98
- package/server/services/fixtures/cli-install-stub.sh +0 -4
- package/server/services/fixtures/grok-stub.cmd +0 -1
- package/server/services/fixtures/grok-stub.mjs +0 -89
- package/server/services/fixtures/grok-stub.sh +0 -4
- package/shared/fixtures/copilot-catalog-raw-2026-09-07.json +0 -2363
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// Shared scaffolding for the messages-routes test suites and the claude-worker
|
|
2
|
-
// routes-integration test: the route-capturing fake express app, the res stub,
|
|
3
|
-
// and the baseline deps stand-ins.
|
|
4
|
-
//
|
|
5
|
-
// server-runtime.mjs boots a live server on import, so the deps it injects are
|
|
6
|
-
// reproduced here rather than imported. makeRouteDeps carries only the minimal
|
|
7
|
-
// working defaults every suite shares; each suite supplies its own db/stmts —
|
|
8
|
-
// the stub suites keep their hand-rolled fakes and prepared-statement mirrors,
|
|
9
|
-
// the integration test passes a real better-sqlite3 database and the real
|
|
10
|
-
// repositories. The harness owns the shape, the suite owns the fakes.
|
|
11
|
-
import assert from 'node:assert/strict';
|
|
12
|
-
|
|
13
|
-
import { registerMessagesRoutes } from './messages-routes.mjs';
|
|
14
|
-
|
|
15
|
-
// Baseline deps: enough for registerMessagesRoutes to register every route
|
|
16
|
-
// without touching a real server. Registration eagerly prepares statements for
|
|
17
|
-
// many routes the suites never exercise, against tables they do not create —
|
|
18
|
-
// the db stub absorbs those; suites that need real statement/transaction
|
|
19
|
-
// semantics override db/stmts.
|
|
20
|
-
export function makeRouteDeps(overrides = {}) {
|
|
21
|
-
return {
|
|
22
|
-
auth: (_req, _res, next) => next(),
|
|
23
|
-
db: { prepare: () => ({ run() {}, get: () => null, all: () => [] }) },
|
|
24
|
-
stmts: {},
|
|
25
|
-
MAX_UPLOAD_BYTES: 1024 * 1024,
|
|
26
|
-
touchCli: () => {},
|
|
27
|
-
ensureSessionId: () => 'session-1',
|
|
28
|
-
normalizeRelayMode: (value) => String(value || '').trim().toLowerCase() || null,
|
|
29
|
-
DEFAULT_RELAY_MODE: 'default',
|
|
30
|
-
DEFAULT_MODEL: 'gpt-5',
|
|
31
|
-
normalizeAttachments: () => [],
|
|
32
|
-
collectReferenceAttachmentsFromText: () => ({ attachments: [] }),
|
|
33
|
-
mergeMessageAttachments: (left, right) => [...(left || []), ...(right || [])],
|
|
34
|
-
resolveRequestedModel: (model) => ({
|
|
35
|
-
ok: false,
|
|
36
|
-
error: `Model "${String(model || '')}" is not supported`,
|
|
37
|
-
available: [],
|
|
38
|
-
}),
|
|
39
|
-
getOpenAIProviderSettings: () => ({ configured: false, enabled: false, model: '', models: [] }),
|
|
40
|
-
getCursorProviderSettings: () => ({ enabled: false, model: '', models: [] }),
|
|
41
|
-
featureFlags: {},
|
|
42
|
-
...overrides,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Registers the routes on a fake express app and returns the captured handlers
|
|
47
|
-
// as a Map keyed 'METHOD /path' (last handler wins, matching how the suites
|
|
48
|
-
// always grabbed the terminal handler after the auth middleware).
|
|
49
|
-
export function captureRoutes(deps, registerFn = registerMessagesRoutes) {
|
|
50
|
-
const routes = new Map();
|
|
51
|
-
const register = (method) => (routePath, ...handlers) => {
|
|
52
|
-
routes.set(`${method} ${routePath}`, handlers[handlers.length - 1]);
|
|
53
|
-
};
|
|
54
|
-
const app = {
|
|
55
|
-
post: register('POST'),
|
|
56
|
-
get: register('GET'),
|
|
57
|
-
patch: register('PATCH'),
|
|
58
|
-
put: register('PUT'),
|
|
59
|
-
delete: register('DELETE'),
|
|
60
|
-
use() {},
|
|
61
|
-
};
|
|
62
|
-
registerFn(app, deps);
|
|
63
|
-
return routes;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Express res stub capturing status/body.
|
|
67
|
-
export function makeRes() {
|
|
68
|
-
const captured = { status: 200, body: null };
|
|
69
|
-
const res = {
|
|
70
|
-
setHeader() {},
|
|
71
|
-
status(code) { captured.status = code; return res; },
|
|
72
|
-
json(payload) { captured.body = payload; return res; },
|
|
73
|
-
};
|
|
74
|
-
return { res, captured };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Dispatches into a captured handler and returns { status, body }.
|
|
78
|
-
export async function invokeRoute(routes, method, routePath, { body = {}, headers = {}, query = {}, params = {}, socket = {} } = {}) {
|
|
79
|
-
const handler = routes.get(`${String(method).toUpperCase()} ${routePath}`);
|
|
80
|
-
assert.ok(handler, `${routePath} should be registered`);
|
|
81
|
-
const { res, captured } = makeRes();
|
|
82
|
-
await handler({ body, headers, query, params, socket }, res);
|
|
83
|
-
return captured;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Registers once and returns an invoker for one POST route — for suites that
|
|
87
|
-
// drive the same handler repeatedly against one database.
|
|
88
|
-
export function postHandler(routePath, deps) {
|
|
89
|
-
const routes = captureRoutes(deps);
|
|
90
|
-
assert.ok(routes.has(`POST ${routePath}`), `${routePath} should be registered`);
|
|
91
|
-
return (body) => invokeRoute(routes, 'POST', routePath, { body });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// One-shot register + POST, the common form in the stub suites.
|
|
95
|
-
export function invokePost(routePath, deps, body) {
|
|
96
|
-
return postHandler(routePath, deps)(body);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// The worker runner's `api(method, path, body)` against the captured handlers:
|
|
100
|
-
// dispatch with a fixed header set (e.g. the bridge identity header the live
|
|
101
|
-
// worker sends) and surface non-2xx as a rejection like the worker's HTTP
|
|
102
|
-
// client does.
|
|
103
|
-
export function makeApi(routes, { headers = {} } = {}) {
|
|
104
|
-
return async (method, routePath, body) => {
|
|
105
|
-
const { status, body: responseBody } = await invokeRoute(routes, method, routePath, { body: body || {}, headers });
|
|
106
|
-
if (status >= 400) {
|
|
107
|
-
throw new Error(`${method} ${routePath} -> ${status}: ${JSON.stringify(responseBody)}`);
|
|
108
|
-
}
|
|
109
|
-
return responseBody;
|
|
110
|
-
};
|
|
111
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@node "%~dp0claude-auth-stub.mjs" %*
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Stand-in for the `claude` CLI auth subcommands, injected through
|
|
2
|
-
// COPILOT_WEB_RELAY_CLAUDE_AUTH_BIN so the auth service can be exercised without
|
|
3
|
-
// touching the host's real Claude login.
|
|
4
|
-
//
|
|
5
|
-
// Node port of claude-auth-stub.sh so Windows can run it too; the platform
|
|
6
|
-
// wrappers next to it (`claude-auth-stub.sh`, `claude-auth-stub.cmd`) just exec
|
|
7
|
-
// this file.
|
|
8
|
-
//
|
|
9
|
-
// Mirrors the shapes observed on CLI 2.1.247:
|
|
10
|
-
// auth status --json -> one line of JSON
|
|
11
|
-
// auth login -> browser notice, an OSC-8 + SGR wrapped authorize URL,
|
|
12
|
-
// then `Paste code here if prompted > ` and a blocking read
|
|
13
|
-
// auth logout -> removes the credentials file
|
|
14
|
-
//
|
|
15
|
-
// Fake credentials file path comes from CLAUDE_AUTH_STUB_CRED_FILE; "logged in"
|
|
16
|
-
// means that file exists.
|
|
17
|
-
|
|
18
|
-
import fs from 'node:fs';
|
|
19
|
-
import process from 'node:process';
|
|
20
|
-
import readline from 'node:readline';
|
|
21
|
-
|
|
22
|
-
const CRED_FILE = process.env.CLAUDE_AUTH_STUB_CRED_FILE || '';
|
|
23
|
-
const AUTH_URL = process.env.CLAUDE_AUTH_STUB_URL
|
|
24
|
-
|| 'https://claude.com/oauth/authorize?code=true&client_id=stub-client&code_challenge=Xk3nQ7pLm2vB8sT1wY6zR0aC5dF9gH4jK7lN2oP3qS8&redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback';
|
|
25
|
-
|
|
26
|
-
const [first = '', second = ''] = process.argv.slice(2);
|
|
27
|
-
const command = `${first} ${second}`;
|
|
28
|
-
const out = (text) => process.stdout.write(text);
|
|
29
|
-
const err = (text) => process.stderr.write(text);
|
|
30
|
-
|
|
31
|
-
if (command === 'auth status') {
|
|
32
|
-
if (CRED_FILE && fs.existsSync(CRED_FILE)) {
|
|
33
|
-
out('{"loggedIn":true,"authMethod":"claudeai","apiProvider":null,"email":"stub@example.com","orgId":"org_stub","orgName":"Stub Org","subscriptionType":"max"}\n');
|
|
34
|
-
} else {
|
|
35
|
-
out('{"loggedIn":false,"authMethod":null,"apiProvider":null,"email":null,"orgId":null,"orgName":null,"subscriptionType":null}\n');
|
|
36
|
-
}
|
|
37
|
-
process.exit(0);
|
|
38
|
-
} else if (command === 'auth login') {
|
|
39
|
-
out('Opening browser to sign in…\n');
|
|
40
|
-
// OSC-8 hyperlink (target + visible label) wrapped in SGR colour codes, the
|
|
41
|
-
// exact shape the real CLI emits under a PTY.
|
|
42
|
-
out(`If the browser did not open, visit: \x1b]8;;${AUTH_URL}\x1b\\\x1b[4;34m${AUTH_URL}\x1b[0m\x1b]8;;\x1b\\\n`);
|
|
43
|
-
out('\x1b[2mPaste code here if prompted > \x1b[0m');
|
|
44
|
-
const rl = readline.createInterface({ input: process.stdin });
|
|
45
|
-
let submitted = null;
|
|
46
|
-
rl.on('line', (line) => {
|
|
47
|
-
submitted = line;
|
|
48
|
-
rl.close();
|
|
49
|
-
});
|
|
50
|
-
rl.on('close', () => {
|
|
51
|
-
if (submitted === null) {
|
|
52
|
-
out('\nAborted.\n');
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
if (submitted === 'goodcode') {
|
|
56
|
-
if (CRED_FILE) {
|
|
57
|
-
// Deliberately short fake tokens: anything with 20+ characters after
|
|
58
|
-
// `sk-ant-` trips the secret scanner in server/test-hygiene.test.mjs.
|
|
59
|
-
fs.writeFileSync(CRED_FILE, '{"claudeAiOauth":{"accessToken":"sk-ant-oat01-stub","refreshToken":"sk-ant-ort01-stub"}}\n');
|
|
60
|
-
}
|
|
61
|
-
out('\nLogin successful. Logged in as stub@example.com\n');
|
|
62
|
-
process.exit(0);
|
|
63
|
-
}
|
|
64
|
-
err('\nOAuth error: invalid_grant - the authorization code is invalid or has expired.\n');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
});
|
|
67
|
-
} else if (command === 'auth logout') {
|
|
68
|
-
if (CRED_FILE) {
|
|
69
|
-
fs.rmSync(CRED_FILE, { force: true });
|
|
70
|
-
}
|
|
71
|
-
out('Logged out.\n');
|
|
72
|
-
process.exit(0);
|
|
73
|
-
} else {
|
|
74
|
-
err(`claude-auth-stub: unsupported command: ${process.argv.slice(2).join(' ')}\n`);
|
|
75
|
-
process.exit(2);
|
|
76
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@node "%~dp0cli-install-stub.mjs" %*
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// Stand-in for a vendor CLI installer, injected through
|
|
2
|
-
// COPILOT_WEB_RELAY_CLI_INSTALL_COMMAND so the install flow can be exercised end
|
|
3
|
-
// to end without ever fetching from x.ai / claude.ai or writing to the host's
|
|
4
|
-
// real bin directories.
|
|
5
|
-
//
|
|
6
|
-
// Node port of cli-install-stub.sh so Windows can run it too; the platform
|
|
7
|
-
// wrappers next to it (`cli-install-stub.sh`, `cli-install-stub.cmd`) just exec
|
|
8
|
-
// this file.
|
|
9
|
-
//
|
|
10
|
-
// The install service calls it as: cli-install-stub <provider> <action>
|
|
11
|
-
// (both values come from the frozen descriptor table, never from a request
|
|
12
|
-
// body). It prints installer-shaped progress, then writes a fake binary into
|
|
13
|
-
// CLI_INSTALL_STUB_BIN_DIR — which the relay is pointed at through
|
|
14
|
-
// COPILOT_WEB_RELAY_CLI_BIN_DIR — so the resolve -> bind -> broadcast chain runs
|
|
15
|
-
// against something real. The fake binary is itself a Node script behind a
|
|
16
|
-
// platform launcher: a bare `#!/bin/sh` file on POSIX, a `<provider>.cmd` shim
|
|
17
|
-
// on Windows (which also exercises the services' batch-launcher spawn path,
|
|
18
|
-
// the same shape a real npm-installed CLI has there).
|
|
19
|
-
//
|
|
20
|
-
// CLI_INSTALL_STUB_BIN_DIR where the fake binary lands (required)
|
|
21
|
-
// CLI_INSTALL_STUB_VERSION version the fake binary reports (default 9.9.9)
|
|
22
|
-
// CLI_INSTALL_STUB_FAIL non-empty -> print an error and exit 1
|
|
23
|
-
// CLI_INSTALL_STUB_SLEEP seconds to stall mid-install (cancel/timeout tests)
|
|
24
|
-
|
|
25
|
-
import fs from 'node:fs';
|
|
26
|
-
import path from 'node:path';
|
|
27
|
-
import process from 'node:process';
|
|
28
|
-
|
|
29
|
-
const [provider = '', action = ''] = process.argv.slice(2);
|
|
30
|
-
const binDir = process.env.CLI_INSTALL_STUB_BIN_DIR || '';
|
|
31
|
-
const version = process.env.CLI_INSTALL_STUB_VERSION || '9.9.9';
|
|
32
|
-
const out = (text) => process.stdout.write(text);
|
|
33
|
-
const err = (text) => process.stderr.write(text);
|
|
34
|
-
|
|
35
|
-
out(`Installing ${provider} (${action})…\n`);
|
|
36
|
-
out('Resolving latest release for linux-x86_64\n');
|
|
37
|
-
|
|
38
|
-
const stallSeconds = Number(process.env.CLI_INSTALL_STUB_SLEEP || '0');
|
|
39
|
-
if (stallSeconds > 0) {
|
|
40
|
-
await new Promise((resolve) => setTimeout(resolve, stallSeconds * 1000));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (process.env.CLI_INSTALL_STUB_FAIL) {
|
|
44
|
-
err('error: download failed (stub)\n');
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!binDir) {
|
|
49
|
-
err('cli-install-stub: CLI_INSTALL_STUB_BIN_DIR is not set\n');
|
|
50
|
-
process.exit(2);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
54
|
-
|
|
55
|
-
// The fake binary answers exactly the probes cli-install-service runs: the
|
|
56
|
-
// version banner in each descriptor's shape, `update --check --json` for Grok,
|
|
57
|
-
// and `doctor` for Claude. The answering logic is one Node script; only the
|
|
58
|
-
// launcher in front of it differs per platform.
|
|
59
|
-
// host-platform: the launcher must match the platform the relay under test
|
|
60
|
-
// runs on — an extensionless shebang file on POSIX, a .cmd shim on Windows.
|
|
61
|
-
const windows = process.platform === 'win32';
|
|
62
|
-
const target = path.join(binDir, windows ? `${provider}.cmd` : provider);
|
|
63
|
-
|
|
64
|
-
const implPath = path.join(binDir, `${provider}-fake-cli.mjs`);
|
|
65
|
-
const impl = `import process from 'node:process';
|
|
66
|
-
const [first = '', second = ''] = process.argv.slice(2);
|
|
67
|
-
const command = \`\${first} \${second}\`.trimEnd();
|
|
68
|
-
if (command === '--version') {
|
|
69
|
-
const banners = {
|
|
70
|
-
grok: 'grok ${version} (stubcommit) [stable]\\n',
|
|
71
|
-
claude: '${version} (Claude Code)\\n',
|
|
72
|
-
};
|
|
73
|
-
process.stdout.write(banners['${provider}'] || '${version}\\n');
|
|
74
|
-
process.exit(0);
|
|
75
|
-
}
|
|
76
|
-
if (command === 'update --check') {
|
|
77
|
-
process.stdout.write('{"currentVersion":"${version}","latestVersion":"${version}","updateAvailable":false,"installer":"internal","channel":"stable","autoUpdate":null,"error":null}\\n');
|
|
78
|
-
process.exit(0);
|
|
79
|
-
}
|
|
80
|
-
if (command === 'doctor') {
|
|
81
|
-
process.stdout.write('Running: native (${version})\\n');
|
|
82
|
-
process.stdout.write('Path: ${JSON.stringify(target).slice(1, -1)}\\n');
|
|
83
|
-
process.stdout.write('Auto-updates: enabled\\n');
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
process.stderr.write(\`cli-stub: unsupported command: \${process.argv.slice(2).join(' ')}\\n\`);
|
|
87
|
-
process.exit(2);
|
|
88
|
-
`;
|
|
89
|
-
fs.writeFileSync(implPath, impl);
|
|
90
|
-
if (windows) {
|
|
91
|
-
fs.writeFileSync(target, `@node "%~dp0${provider}-fake-cli.mjs" %*\r\n`);
|
|
92
|
-
} else {
|
|
93
|
-
fs.writeFileSync(target, `#!/bin/sh\nexec node "$(dirname "$0")/${provider}-fake-cli.mjs" "$@"\n`);
|
|
94
|
-
fs.chmodSync(target, 0o755);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
out(`Installed ${provider} ${version} to ${target}\n`);
|
|
98
|
-
process.exit(0);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@node "%~dp0grok-stub.mjs" %*
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Stand-in for the `grok` CLI, injected through GROK_CLI_COMMAND so the auth
|
|
2
|
-
// service (and the CLI install/probe path) can be exercised without touching the
|
|
3
|
-
// host's real Grok login.
|
|
4
|
-
//
|
|
5
|
-
// Node port of grok-stub.sh so Windows can run it too (no shebang support, .sh
|
|
6
|
-
// not in PATHEXT); the platform wrappers next to it (`grok-stub.sh`,
|
|
7
|
-
// `grok-stub.cmd`) just exec this file. Batch was rejected as the port target
|
|
8
|
-
// because the poll interval is sub-second and `timeout /t` can't sleep below 1s.
|
|
9
|
-
//
|
|
10
|
-
// Mirrors the shapes probed live on Grok Build 1.0.13 (2026-08-31):
|
|
11
|
-
// --version -> `grok 1.0.13 (5e9a58528b76)`
|
|
12
|
-
// update --check --json-> the machine-readable update payload
|
|
13
|
-
// login --device-auth -> a device URL carrying the code, the code again on its
|
|
14
|
-
// own line, one grey (SGR 90) warning line, then
|
|
15
|
-
// `Waiting for authorization...` and a poll loop. No
|
|
16
|
-
// TTY, no stdin read: the real CLI ignores stdin and
|
|
17
|
-
// exits by itself once the browser authorises.
|
|
18
|
-
// logout -> removes the fake auth.json
|
|
19
|
-
//
|
|
20
|
-
// The poll loop stands in for the browser: it exits 0 when GROK_STUB_SUCCESS_SENTINEL
|
|
21
|
-
// appears (writing the fake auth store) and non-zero when GROK_STUB_FAILURE_SENTINEL
|
|
22
|
-
// does. "Logged in" means GROK_STUB_AUTH_FILE exists, matching what
|
|
23
|
-
// readGrokCliAuthKey() actually looks at.
|
|
24
|
-
|
|
25
|
-
import fs from 'node:fs';
|
|
26
|
-
import process from 'node:process';
|
|
27
|
-
|
|
28
|
-
const env = process.env;
|
|
29
|
-
const VERSION = env.GROK_STUB_VERSION || '1.0.13';
|
|
30
|
-
const AUTH_FILE = env.GROK_STUB_AUTH_FILE || '';
|
|
31
|
-
const DEVICE_CODE = env.GROK_STUB_DEVICE_CODE || 'D7SV-M4TR';
|
|
32
|
-
const DEVICE_URL = env.GROK_STUB_DEVICE_URL
|
|
33
|
-
|| `https://accounts.x.ai/oauth2/device?user_code=${DEVICE_CODE}`;
|
|
34
|
-
const SUCCESS_SENTINEL = env.GROK_STUB_SUCCESS_SENTINEL || '';
|
|
35
|
-
const FAILURE_SENTINEL = env.GROK_STUB_FAILURE_SENTINEL || '';
|
|
36
|
-
const TIMEOUT_SECONDS = Number(env.GROK_STUB_TIMEOUT_SECONDS || '60');
|
|
37
|
-
const POLL_SECONDS = Number(env.GROK_STUB_POLL_SECONDS || '0.2');
|
|
38
|
-
const UPDATE_AVAILABLE = env.GROK_STUB_UPDATE_AVAILABLE || 'false';
|
|
39
|
-
const LATEST_VERSION = env.GROK_STUB_LATEST_VERSION || VERSION;
|
|
40
|
-
|
|
41
|
-
const [first = '', second = ''] = process.argv.slice(2);
|
|
42
|
-
const out = (text) => process.stdout.write(text);
|
|
43
|
-
const err = (text) => process.stderr.write(text);
|
|
44
|
-
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
45
|
-
|
|
46
|
-
if (first === '--version') {
|
|
47
|
-
out(`grok ${VERSION} (5e9a58528b76)\n`);
|
|
48
|
-
process.exit(0);
|
|
49
|
-
} else if (first === 'update' && second === '--check') {
|
|
50
|
-
out(`{"currentVersion":"${VERSION}","latestVersion":"${LATEST_VERSION}","updateAvailable":${UPDATE_AVAILABLE},"installer":"internal","channel":"stable","autoUpdate":null,"error":null}\n`);
|
|
51
|
-
process.exit(0);
|
|
52
|
-
} else if (first === 'login' && second === '--device-auth') {
|
|
53
|
-
// Byte-for-byte the banner the real CLI printed under piped stdio, including
|
|
54
|
-
// the blank lines and the one grey warning line — the escape-stripping in the
|
|
55
|
-
// URL/code scrape is tested against exactly this.
|
|
56
|
-
out(`\nTo sign in, open this URL in your browser:\n\n ${DEVICE_URL}\n\n`);
|
|
57
|
-
out(' (Could not open browser automatically — open the URL above manually.)\n\n');
|
|
58
|
-
out(`Confirm this code in your browser:\n\n ${DEVICE_CODE}\n\n`);
|
|
59
|
-
out('\x1b[90mOnly continue with a code you requested. Do not share it with anyone.\x1b[0m\n\n');
|
|
60
|
-
out('Waiting for authorization...\n');
|
|
61
|
-
const deadline = Date.now() + TIMEOUT_SECONDS * 1000;
|
|
62
|
-
while (Date.now() <= deadline) {
|
|
63
|
-
if (FAILURE_SENTINEL && fs.existsSync(FAILURE_SENTINEL)) {
|
|
64
|
-
err('\nAuthorization failed: the device code expired.\n');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
if (SUCCESS_SENTINEL && fs.existsSync(SUCCESS_SENTINEL)) {
|
|
68
|
-
if (AUTH_FILE) {
|
|
69
|
-
// Deliberately short fake key: anything 32+ characters long trips the
|
|
70
|
-
// secret scanner in server/test-hygiene.test.mjs.
|
|
71
|
-
fs.writeFileSync(AUTH_FILE, '{"https://auth.x.ai::stub":{"key":"grok-stub-key","create_time":"2026-08-31T00:00:00Z","expires_at":"2099-01-01T00:00:00Z"}}\n');
|
|
72
|
-
}
|
|
73
|
-
out('\nSigned in.\n');
|
|
74
|
-
process.exit(0);
|
|
75
|
-
}
|
|
76
|
-
await sleep(POLL_SECONDS);
|
|
77
|
-
}
|
|
78
|
-
err('\nTimed out waiting for authorization.\n');
|
|
79
|
-
process.exit(1);
|
|
80
|
-
} else if (first === 'logout') {
|
|
81
|
-
if (AUTH_FILE) {
|
|
82
|
-
fs.rmSync(AUTH_FILE, { force: true });
|
|
83
|
-
}
|
|
84
|
-
out('Signed out.\n');
|
|
85
|
-
process.exit(0);
|
|
86
|
-
} else {
|
|
87
|
-
err(`grok-stub: unsupported command: ${process.argv.slice(2).join(' ')}\n`);
|
|
88
|
-
process.exit(2);
|
|
89
|
-
}
|