@reticlehq/server 3.8.2 → 3.9.0
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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input.d.ts +15 -0
- package/dist/portal/input/real-input.js +59 -3
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +10 -10
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +13 -9
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { mcpLauncher } from './mcp-launcher.js';
|
|
11
12
|
import { planAgentConfigs, platformKey } from './agent-configs.js';
|
|
12
13
|
import { AppShape, describeShape, readShape } from './desktop-shape.js';
|
|
13
14
|
import { devConfigText, expectedDevPort, loneLoopbackOnDevPort, pinnedDevPort, } from './probe/dev-port.js';
|
|
@@ -73,8 +74,9 @@ export function installCrashGuard() {
|
|
|
73
74
|
function hasElectronDependency(dir) {
|
|
74
75
|
try {
|
|
75
76
|
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
76
|
-
if ('object' !== typeof pkg || null === pkg)
|
|
77
|
+
if ('object' !== typeof pkg || null === pkg) {
|
|
77
78
|
return false;
|
|
79
|
+
}
|
|
78
80
|
const p = pkg;
|
|
79
81
|
return undefined !== { ...p.dependencies, ...p.devDependencies }['electron'];
|
|
80
82
|
}
|
|
@@ -95,7 +97,13 @@ const agentWord = (n) => (1 === n ? 'agent' : 'agents');
|
|
|
95
97
|
export function registerOtherAgents(print) {
|
|
96
98
|
const platform = platformKey(process.platform);
|
|
97
99
|
const home = homedir();
|
|
98
|
-
const results = applyAgentPlan(planAgentConfigs({
|
|
100
|
+
const results = applyAgentPlan(planAgentConfigs({
|
|
101
|
+
home,
|
|
102
|
+
platform,
|
|
103
|
+
exists: agentIo.exists,
|
|
104
|
+
readFile: agentIo.readFile,
|
|
105
|
+
launcher: mcpLauncher(),
|
|
106
|
+
}), agentIo);
|
|
99
107
|
const wrote = results.filter((r) => 'created' === r.action || 'merged' === r.action);
|
|
100
108
|
if (0 < wrote.length) {
|
|
101
109
|
print(` ✓ ${String(wrote.length)} more ${agentWord(wrote.length)}: ${wrote.map((r) => r.name).join(', ')}`);
|
|
@@ -115,8 +123,9 @@ export function registerOtherAgents(print) {
|
|
|
115
123
|
}
|
|
116
124
|
rememberSaid(stateHome, fresh.map((n) => noticeKey(n)));
|
|
117
125
|
const skills = applyAgentSkills(agentIo, { home, platform });
|
|
118
|
-
if (0 < skills.length)
|
|
126
|
+
if (0 < skills.length) {
|
|
119
127
|
print(` ✓ the /reticle skill, for ${String(skills.length)} ${agentWord(skills.length)}`);
|
|
128
|
+
}
|
|
120
129
|
// Registration alone still leaves an Accept dialog in front of every call, and a verification run
|
|
121
130
|
// makes dozens of them: the loop is only autonomous once the tools are pre-approved.
|
|
122
131
|
const approvals = grantAutoApproval(agentIo, { home, platform });
|
|
@@ -173,8 +182,9 @@ async function restartHandedOverServer(appDir) {
|
|
|
173
182
|
*/
|
|
174
183
|
async function afterConnect(input, url, sessionId, outcome, print) {
|
|
175
184
|
const notes = [...outcome.notes];
|
|
176
|
-
if (input.firstFlow === undefined)
|
|
185
|
+
if (input.firstFlow === undefined) {
|
|
177
186
|
return { flowSaved: false, notes };
|
|
187
|
+
}
|
|
178
188
|
const { flowSaved } = await input.firstFlow({
|
|
179
189
|
appDir: input.appDir,
|
|
180
190
|
bridgePort: input.bridgePort,
|
|
@@ -192,8 +202,9 @@ async function afterConnect(input, url, sessionId, outcome, print) {
|
|
|
192
202
|
return { flowSaved, notes };
|
|
193
203
|
}
|
|
194
204
|
export async function runSetupCommand(input, print) {
|
|
195
|
-
if (input.registerAgents)
|
|
205
|
+
if (input.registerAgents) {
|
|
196
206
|
registerOtherAgents(print);
|
|
207
|
+
}
|
|
197
208
|
// A moved port first: this project's daemon on the OLD port would otherwise be the one the page
|
|
198
209
|
// finds and dials, while everything below waits on the new one.
|
|
199
210
|
const retired = await retireMovedDaemons(readProjectId(input.appDir), input.bridgePort, nodeRetireDeps());
|
|
@@ -209,8 +220,9 @@ export async function runSetupCommand(input, print) {
|
|
|
209
220
|
// Before the app is booted, because the app's whole job from here is to dial this port. Without
|
|
210
221
|
// it the phases wait out their budget and then report the SDK as the thing that failed.
|
|
211
222
|
const daemon = await ensureDaemon(input.bridgePort, nodeEnsureDaemonDeps((status) => undefined !== daemonSkew(status)));
|
|
212
|
-
if (undefined !== daemon.message)
|
|
223
|
+
if (undefined !== daemon.message) {
|
|
213
224
|
print(daemon.message);
|
|
225
|
+
}
|
|
214
226
|
if (EnsureDaemon.SKEWED_IN_USE === daemon.state) {
|
|
215
227
|
// Stop here: every hello from the page would be refused, and waiting out the connect budget
|
|
216
228
|
// only to blame the page is the run this exists to prevent.
|
|
@@ -235,8 +247,9 @@ export async function runSetupCommand(input, print) {
|
|
|
235
247
|
hasTauriConf: existsSync(join(input.appDir, 'src-tauri', 'tauri.conf.json')),
|
|
236
248
|
hasElectronDep: hasElectronDependency(input.appDir),
|
|
237
249
|
});
|
|
238
|
-
if (AppShape.WEB !== shape)
|
|
250
|
+
if (AppShape.WEB !== shape) {
|
|
239
251
|
print(`detected ${describeShape(shape)}`);
|
|
252
|
+
}
|
|
240
253
|
const server = new OwnedDevServer();
|
|
241
254
|
const effects = {
|
|
242
255
|
startDevServer: (command, cwd) => {
|
|
@@ -261,8 +274,9 @@ export async function runSetupCommand(input, print) {
|
|
|
261
274
|
},
|
|
262
275
|
})
|
|
263
276
|
: {};
|
|
264
|
-
if (SigningEnv.IDENTITY in signing)
|
|
277
|
+
if (SigningEnv.IDENTITY in signing) {
|
|
265
278
|
print(`signing the dev build as ${String(signing[SigningEnv.IDENTITY])}, so macOS does not ask for the keychain password`);
|
|
279
|
+
}
|
|
266
280
|
server.start(command, cwd, { ...input.env, ...signing });
|
|
267
281
|
return Promise.resolve();
|
|
268
282
|
},
|
|
@@ -288,8 +302,9 @@ export async function runSetupCommand(input, print) {
|
|
|
288
302
|
probePage,
|
|
289
303
|
openBrowser: async (url) => {
|
|
290
304
|
const failure = await openInBrowser(url);
|
|
291
|
-
if (null === failure)
|
|
305
|
+
if (null === failure) {
|
|
292
306
|
return undefined;
|
|
307
|
+
}
|
|
293
308
|
// It used to end "nothing else here will open one", which stopped being true once a
|
|
294
309
|
// Reticle-owned browser could always launch: run-setup opens a lease on this failure, so the
|
|
295
310
|
// reader is told what happens next rather than sent to find a browser by hand.
|
|
@@ -310,8 +325,9 @@ export async function runSetupCommand(input, print) {
|
|
|
310
325
|
// A daemon too old to report the short one falls back to the long one, because saying the
|
|
311
326
|
// right thing at length beats saying nothing.
|
|
312
327
|
const lead = status.whyLead ?? full;
|
|
313
|
-
if (undefined === lead)
|
|
328
|
+
if (undefined === lead) {
|
|
314
329
|
return undefined;
|
|
330
|
+
}
|
|
315
331
|
return undefined === full ? { lead } : { lead, full };
|
|
316
332
|
},
|
|
317
333
|
now: () => Date.now(),
|
|
@@ -352,11 +368,13 @@ export async function runSetupCommand(input, print) {
|
|
|
352
368
|
// Its output is on a file now, not this terminal, so say where: a compile error that used to
|
|
353
369
|
// scroll past here is the first thing somebody will look for.
|
|
354
370
|
const log = server.logPath();
|
|
355
|
-
if (undefined !== log)
|
|
371
|
+
if (undefined !== log) {
|
|
356
372
|
print(`The dev server keeps running; its output goes to ${log}`);
|
|
373
|
+
}
|
|
357
374
|
}
|
|
358
|
-
if (!outcome.ok || undefined === outcome.url || undefined === outcome.sessionId)
|
|
375
|
+
if (!outcome.ok || undefined === outcome.url || undefined === outcome.sessionId) {
|
|
359
376
|
return outcome;
|
|
377
|
+
}
|
|
360
378
|
return {
|
|
361
379
|
...outcome,
|
|
362
380
|
...(await afterConnect(input, outcome.url, outcome.sessionId, outcome, print)),
|
|
@@ -17,6 +17,8 @@ export interface SetupMcpIo {
|
|
|
17
17
|
writeFile(path: string, contents: string): void;
|
|
18
18
|
homeDir(): string;
|
|
19
19
|
print(line: string): void;
|
|
20
|
+
/** The installed bin to register (`InitHost.mcpLauncher`). Absent: the pinned npx command. */
|
|
21
|
+
readonly launcher?: string | undefined;
|
|
20
22
|
/** Register with a client that owns its own registration (Claude Code). Returns whether it took. */
|
|
21
23
|
runCli(command: string, args: readonly string[]): boolean;
|
|
22
24
|
/**
|
|
@@ -46,6 +48,12 @@ export interface SetupMcpResult {
|
|
|
46
48
|
* and a pre-seeded `~/.codex/config.toml`; the file came back byte-identical both times.
|
|
47
49
|
*/
|
|
48
50
|
readonly manual: readonly ManualClient[];
|
|
51
|
+
/** Clients whose Reticle plugin brings the server, so nothing was registered for them. */
|
|
52
|
+
readonly pluginActive: readonly string[];
|
|
53
|
+
/** Clients with the plugin AND a user-scope `reticle` entry: two servers. Named, never removed. */
|
|
54
|
+
readonly duplicates: readonly string[];
|
|
55
|
+
/** Clients whose `reticle` entry is the floating npx form. Named with the fix, never replaced. */
|
|
56
|
+
readonly stale: readonly string[];
|
|
49
57
|
}
|
|
50
58
|
/** A client the caller has to tell somebody about, with the two facts they need to act. */
|
|
51
59
|
export interface ManualClient {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe, claudeHasReticle, codexAddCommand, codexAvailableProbe, codexNamesOurServer, detectMcpClients, mergeClientConfig, clientSpec, ClientMergeStatus, } from '@reticlehq/init';
|
|
2
|
-
import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
|
|
1
|
+
import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe, claudeHasReticle, claudePluginInstalled, claudeUserEntryIsFloating, codexAddCommand, codexAvailableProbe, codexNamesOurServer, detectMcpClients, mergeClientConfig, clientSpec, ClientMergeStatus, } from '@reticlehq/init';
|
|
2
|
+
import { OnboardingPhase, OnboardingStepStatus, OnboardingStop, } from '@reticlehq/core/telemetry';
|
|
3
3
|
/**
|
|
4
4
|
* Codex registers through `codex mcp add`, which does the TOML editing we refuse to do (#1238).
|
|
5
5
|
* `undefined` when that does not apply, and the caller reports a hand edit exactly as before: not
|
|
@@ -12,9 +12,10 @@ function registerWithCodexCli(io, client, serversKey) {
|
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
14
|
const probe = codexAvailableProbe();
|
|
15
|
-
if (!io.runCli(probe.command, probe.args))
|
|
15
|
+
if (!io.runCli(probe.command, probe.args)) {
|
|
16
16
|
return undefined;
|
|
17
|
-
|
|
17
|
+
}
|
|
18
|
+
const cmd = codexAddCommand(io.launcher);
|
|
18
19
|
return io.runCli(cmd.command, cmd.args);
|
|
19
20
|
}
|
|
20
21
|
export function setupMcp(io) {
|
|
@@ -23,6 +24,9 @@ export function setupMcp(io) {
|
|
|
23
24
|
const alreadyThere = [];
|
|
24
25
|
const failed = [];
|
|
25
26
|
const manual = [];
|
|
27
|
+
const pluginActive = [];
|
|
28
|
+
const duplicates = [];
|
|
29
|
+
const stale = [];
|
|
26
30
|
/*
|
|
27
31
|
* Claude Code first, because nothing else can find it.
|
|
28
32
|
*
|
|
@@ -41,21 +45,34 @@ export function setupMcp(io) {
|
|
|
41
45
|
if (io.runCli(available.command, available.args)) {
|
|
42
46
|
detected.push(McpClient.CLAUDE_CODE);
|
|
43
47
|
// Read from Claude's config: `claude mcp get` health-checks by launching the server.
|
|
44
|
-
|
|
48
|
+
const hasEntry = claudeHasReticle(io, undefined);
|
|
49
|
+
if (claudePluginInstalled(io)) {
|
|
50
|
+
// The plugin brings its own server. A user-scope entry next to it is a second one.
|
|
51
|
+
pluginActive.push(McpClient.CLAUDE_CODE);
|
|
52
|
+
if (hasEntry) {
|
|
53
|
+
duplicates.push(McpClient.CLAUDE_CODE);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (hasEntry) {
|
|
45
57
|
alreadyThere.push(McpClient.CLAUDE_CODE);
|
|
58
|
+
if (claudeUserEntryIsFloating(io)) {
|
|
59
|
+
stale.push(McpClient.CLAUDE_CODE);
|
|
60
|
+
}
|
|
46
61
|
}
|
|
47
62
|
else {
|
|
48
|
-
const cmd = claudeAddCommand();
|
|
49
|
-
if (io.runCli(cmd.command, cmd.args))
|
|
63
|
+
const cmd = claudeAddCommand(io.launcher);
|
|
64
|
+
if (io.runCli(cmd.command, cmd.args)) {
|
|
50
65
|
registered.push(McpClient.CLAUDE_CODE);
|
|
51
|
-
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
52
68
|
failed.push(McpClient.CLAUDE_CODE);
|
|
69
|
+
}
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
72
|
for (const client of detectMcpClients(io)) {
|
|
56
73
|
detected.push(client.id);
|
|
57
74
|
const spec = clientSpec(client.id);
|
|
58
|
-
const merged = mergeClientConfig(spec, client.existing);
|
|
75
|
+
const merged = mergeClientConfig(spec, client.existing, io.launcher);
|
|
59
76
|
if (ClientMergeStatus.ALREADY === merged.status) {
|
|
60
77
|
alreadyThere.push(client.id);
|
|
61
78
|
continue;
|
|
@@ -65,12 +82,15 @@ export function setupMcp(io) {
|
|
|
65
82
|
// sends somebody to an agent that has no Reticle tools in it.
|
|
66
83
|
if (ClientMergeStatus.MANUAL === merged.status) {
|
|
67
84
|
const viaCli = registerWithCodexCli(io, client, spec.serversKey);
|
|
68
|
-
if (true === viaCli)
|
|
85
|
+
if (true === viaCli) {
|
|
69
86
|
registered.push(client.id);
|
|
70
|
-
|
|
87
|
+
}
|
|
88
|
+
else if (false === viaCli) {
|
|
71
89
|
failed.push(client.id);
|
|
72
|
-
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
73
92
|
manual.push({ id: client.id, configPath: client.configPath, docs: spec.docs });
|
|
93
|
+
}
|
|
74
94
|
continue;
|
|
75
95
|
}
|
|
76
96
|
try {
|
|
@@ -93,6 +113,7 @@ export function setupMcp(io) {
|
|
|
93
113
|
step: 'agents_detected',
|
|
94
114
|
status: OnboardingStepStatus.COMPLETED,
|
|
95
115
|
});
|
|
116
|
+
const mcpStatus = mcpRegisteredStatus(detected.length, registered.length, alreadyThere.length + pluginActive.length);
|
|
96
117
|
io.reportStep({
|
|
97
118
|
phase: OnboardingPhase.INSTALL,
|
|
98
119
|
step: 'mcp_registered',
|
|
@@ -107,15 +128,21 @@ export function setupMcp(io) {
|
|
|
107
128
|
* for. Registering some and not others is still COMPLETED: the agent they are using may well be
|
|
108
129
|
* one that worked, and a partial win is not a failed step.
|
|
109
130
|
*/
|
|
110
|
-
status:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
: 0 === registered.length && 0 === alreadyThere.length
|
|
115
|
-
? OnboardingStepStatus.FAILED
|
|
116
|
-
: OnboardingStepStatus.COMPLETED,
|
|
131
|
+
status: mcpStatus,
|
|
132
|
+
...(OnboardingStepStatus.FAILED === mcpStatus
|
|
133
|
+
? { stop: OnboardingStop.MCP_NOT_REGISTERED }
|
|
134
|
+
: {}),
|
|
117
135
|
});
|
|
118
|
-
return { detected, registered, alreadyThere, failed, manual };
|
|
136
|
+
return { detected, registered, alreadyThere, failed, manual, pluginActive, duplicates, stale };
|
|
137
|
+
}
|
|
138
|
+
function mcpRegisteredStatus(detected, registered, alreadyThere) {
|
|
139
|
+
if (0 === detected || (0 === registered && 0 < alreadyThere)) {
|
|
140
|
+
return OnboardingStepStatus.SKIPPED;
|
|
141
|
+
}
|
|
142
|
+
if (0 === registered) {
|
|
143
|
+
return OnboardingStepStatus.FAILED;
|
|
144
|
+
}
|
|
145
|
+
return OnboardingStepStatus.COMPLETED;
|
|
119
146
|
}
|
|
120
147
|
/** Everything this machine could be asked about, for the report when nothing was found. */
|
|
121
148
|
export function knownClientLabels() {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The url it serves and how to stop it, or why it did not serve. */
|
|
2
|
+
export type StartedDevServer = {
|
|
3
|
+
url: string;
|
|
4
|
+
stop: () => void;
|
|
5
|
+
} | {
|
|
6
|
+
failed: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function startDevServerAndWait(command: string, dir: string, budgetMs: number): Promise<StartedDevServer>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start a project's dev script and wait until it serves a page, for a command that is not `init`.
|
|
3
|
+
*
|
|
4
|
+
* The same pieces `init` uses: the supervised `OwnedDevServer`, the URL it announces or the port its
|
|
5
|
+
* process tree binds, and `judgeWait` for ready, dead or hung. `init` keeps its own loop because it
|
|
6
|
+
* also reports progress and hands the server over; this one stops the server when the caller is done.
|
|
7
|
+
*/
|
|
8
|
+
import { OwnedDevServer, probePage } from './node-effects.js';
|
|
9
|
+
import { WaitVerdict, devServerDiedMessage, judgeWait, portBusyMessage, urlToWatch, } from './bringup/dev-server-wait.js';
|
|
10
|
+
const POLL_MS = 500;
|
|
11
|
+
const MS_PER_S = 1000;
|
|
12
|
+
export async function startDevServerAndWait(command, dir, budgetMs) {
|
|
13
|
+
const server = new OwnedDevServer();
|
|
14
|
+
server.start(command, dir, {});
|
|
15
|
+
const startedAt = Date.now();
|
|
16
|
+
for (;;) {
|
|
17
|
+
const watching = urlToWatch(server.output(), server.listeningPorts());
|
|
18
|
+
const probe = watching === undefined ? undefined : await probePage(watching);
|
|
19
|
+
const verdict = judgeWait({
|
|
20
|
+
output: server.output(),
|
|
21
|
+
launcherExited: server.exited(),
|
|
22
|
+
serving: true === probe?.served,
|
|
23
|
+
quietForMs: server.quietForMs(),
|
|
24
|
+
elapsedMs: Date.now() - startedAt,
|
|
25
|
+
budgetMs,
|
|
26
|
+
});
|
|
27
|
+
if (WaitVerdict.READY === verdict && watching !== undefined) {
|
|
28
|
+
return { url: probe?.reachedUrl ?? watching, stop: () => server.stop() };
|
|
29
|
+
}
|
|
30
|
+
if (WaitVerdict.DEAD === verdict) {
|
|
31
|
+
const failed = portBusyMessage(server.output()) ?? devServerDiedMessage(server.output());
|
|
32
|
+
server.stop();
|
|
33
|
+
return { failed };
|
|
34
|
+
}
|
|
35
|
+
if (WaitVerdict.HUNG === verdict) {
|
|
36
|
+
server.stop();
|
|
37
|
+
return {
|
|
38
|
+
failed: `The dev server did not serve a page in ${String(budgetMs / MS_PER_S)}s.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -5,3 +5,5 @@ export declare const SERVER_VERSION: string;
|
|
|
5
5
|
* drift. Self-update installs THIS (never `@reticlehq/core`, which is schema-only and has no bin).
|
|
6
6
|
*/
|
|
7
7
|
export declare const RETICLE_NPM_PACKAGE: string;
|
|
8
|
+
/** The Node.js range this package declares in `engines`, such as `>=20.11`. */
|
|
9
|
+
export declare const SERVER_NODE_RANGE: string | undefined;
|
|
@@ -48,3 +48,5 @@ export const SERVER_VERSION = _pkg.version;
|
|
|
48
48
|
* drift. Self-update installs THIS (never `@reticlehq/core`, which is schema-only and has no bin).
|
|
49
49
|
*/
|
|
50
50
|
export const RETICLE_NPM_PACKAGE = _pkg.name;
|
|
51
|
+
/** The Node.js range this package declares in `engines`, such as `>=20.11`. */
|
|
52
|
+
export const SERVER_NODE_RANGE = _pkg.engines?.node;
|
|
@@ -106,6 +106,13 @@ export declare function compareVersions(a: string | undefined, b: string): numbe
|
|
|
106
106
|
export declare function sdkIsBehind(sdk: string | undefined, daemon: string): boolean;
|
|
107
107
|
/** The same fact for the person looking at the page, on its overlay. */
|
|
108
108
|
export declare const SDK_BEHIND_NARRATION: (sdk: string, daemon: string) => string;
|
|
109
|
+
/**
|
|
110
|
+
* The sentence for a screenshot the page cannot draw because its SDK is behind this daemon.
|
|
111
|
+
*
|
|
112
|
+
* `render` is one of the things a behind page does not have. The refusal from the page names the
|
|
113
|
+
* missing command and nothing else; this is the same update the overlay already shows.
|
|
114
|
+
*/
|
|
115
|
+
export declare function sdkBehindNote(sdk: string | undefined): string | undefined;
|
|
109
116
|
/**
|
|
110
117
|
* A page whose SDK is a minor release or more behind this daemon.
|
|
111
118
|
*
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* a human acts on.
|
|
24
24
|
*/
|
|
25
25
|
import { RETIRED_WIRE_NAMES } from '@reticlehq/core';
|
|
26
|
+
import { SERVER_VERSION } from './identity/server-version.js';
|
|
26
27
|
import { resolveSdkFix } from './sdk-fix.js';
|
|
27
28
|
/** Which pair disagreed. Named so the nudge can report each independently. */
|
|
28
29
|
export const SkewPair = {
|
|
@@ -177,9 +178,19 @@ export function sdkIsBehind(sdk, daemon) {
|
|
|
177
178
|
const order = compareVersions(minorOf(sdk), minorOf(daemon));
|
|
178
179
|
return order !== undefined && order < 0;
|
|
179
180
|
}
|
|
181
|
+
/** What to run so the page picks up the daemon's SDK. */
|
|
182
|
+
const UPDATE_THE_SDK = "Run `reticle update` in the app's folder, then restart its dev server.";
|
|
180
183
|
/** The same fact for the person looking at the page, on its overlay. */
|
|
181
|
-
export const SDK_BEHIND_NARRATION = (sdk, daemon) => `This app runs Reticle SDK ${sdk}, older than Reticle ${daemon}.
|
|
182
|
-
|
|
184
|
+
export const SDK_BEHIND_NARRATION = (sdk, daemon) => `This app runs Reticle SDK ${sdk}, older than Reticle ${daemon}. ${UPDATE_THE_SDK}`;
|
|
185
|
+
/**
|
|
186
|
+
* The sentence for a screenshot the page cannot draw because its SDK is behind this daemon.
|
|
187
|
+
*
|
|
188
|
+
* `render` is one of the things a behind page does not have. The refusal from the page names the
|
|
189
|
+
* missing command and nothing else; this is the same update the overlay already shows.
|
|
190
|
+
*/
|
|
191
|
+
export function sdkBehindNote(sdk) {
|
|
192
|
+
return describeSdkBehind(sdk, SERVER_VERSION, () => UPDATE_THE_SDK);
|
|
193
|
+
}
|
|
183
194
|
/**
|
|
184
195
|
* A page whose SDK is a minor release or more behind this daemon.
|
|
185
196
|
*
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* License — see./LICENSE), not the FSL that covers the rest of the server.
|
|
2
|
+
* License-key verification for `reticle license` and the telemetry `licenseStatus` field.
|
|
4
3
|
*
|
|
5
|
-
* Offline
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* caller with requireLicense:true must present a valid, unexpired key. The clock is injected (rule 7).
|
|
9
|
-
*
|
|
10
|
-
* Rules for this directory: the free server must never import from ee/; ee/ depends only on protocol +
|
|
11
|
-
* node + zod. This file imports neither server core nor any sibling outside ee/.
|
|
4
|
+
* Offline, with no phone-home: a key is a signed payload that Reticle issues with its private key.
|
|
5
|
+
* This file verifies it against the issuer's PUBLIC key with Ed25519. Nothing in this package is
|
|
6
|
+
* gated on a key. The clock is injected (rule 7).
|
|
12
7
|
*/
|
|
13
8
|
import { type KeyObject } from 'node:crypto';
|
|
14
9
|
import { z } from 'zod';
|
|
@@ -23,8 +18,8 @@ export declare const LicenseStatus: {
|
|
|
23
18
|
};
|
|
24
19
|
export type LicenseStatus = (typeof LicenseStatus)[keyof typeof LicenseStatus];
|
|
25
20
|
/**
|
|
26
|
-
* The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes
|
|
27
|
-
*
|
|
21
|
+
* The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes the features that
|
|
22
|
+
* the key covers.
|
|
28
23
|
*
|
|
29
24
|
* `lid` is the stable licence id and the only safe identity: `org` is a display name a human typed at
|
|
30
25
|
* signing time, so two organisations with the same name collapse into one and a rename splits one into
|
|
@@ -61,48 +56,14 @@ export declare function signLicenseKey(payload: LicensePayload, privateKey: KeyO
|
|
|
61
56
|
/** Verify a key against the issuer public key. Never throws — returns a structured status. */
|
|
62
57
|
export declare function verifyLicenseKey(key: string | undefined, publicKey: KeyObject, now: number): LicenseCheck;
|
|
63
58
|
/**
|
|
64
|
-
* Where to get a key. A named constant because
|
|
65
|
-
*
|
|
66
|
-
* that reaches nobody.
|
|
59
|
+
* Where to get a key. A named constant because `reticle license` prints it on every branch, and a
|
|
60
|
+
* contact address that drifts is a support request that reaches nobody.
|
|
67
61
|
*/
|
|
68
62
|
export declare const LICENSE_CONTACT = "hey@reticle.sh";
|
|
69
|
-
/**
|
|
70
|
-
* The features a licence gates in THIS build. The single source: `assertEnterprise` callers name a
|
|
71
|
-
* member rather than a string literal, and `reticle license` reports the list, so what the CLI claims
|
|
72
|
-
* is gated is derived from what is actually gated rather than a second list somebody maintains.
|
|
73
|
-
*
|
|
74
|
-
* It is deliberately short. Reticle's roadmap for enterprise is longer (SSO, SCIM, RBAC, multi-org,
|
|
75
|
-
* policy gates), and none of it is here yet — printing a roadmap as though it were shipped is how a
|
|
76
|
-
* buyer discovers on day two that the thing they paid for does not exist. What is listed is what the
|
|
77
|
-
* running build will actually refuse without a key.
|
|
78
|
-
*/
|
|
79
|
-
export declare const EnterpriseFeature: {
|
|
80
|
-
/** Recording an audit event. */
|
|
81
|
-
readonly AUDIT_LOG: "audit-log";
|
|
82
|
-
};
|
|
83
|
-
export type EnterpriseFeature = (typeof EnterpriseFeature)[keyof typeof EnterpriseFeature];
|
|
84
|
-
/** Thrown when a production-mode enterprise feature is used without a valid license. */
|
|
85
|
-
export declare class EnterpriseLicenseError extends Error {
|
|
86
|
-
readonly feature: string;
|
|
87
|
-
readonly reason: string;
|
|
88
|
-
constructor(feature: string, reason: string);
|
|
89
|
-
}
|
|
90
|
-
/** Context for the gate. requireLicense:false ⇒ dev/eval no-op. publicKey injectable for tests. */
|
|
91
|
-
export interface GateContext {
|
|
92
|
-
requireLicense: boolean;
|
|
93
|
-
now: () => number;
|
|
94
|
-
key?: string;
|
|
95
|
-
publicKey?: KeyObject;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Gate an enterprise feature. No-op in dev/eval (requireLicense:false). In production it throws
|
|
99
|
-
* EnterpriseLicenseError unless a valid, unexpired key that covers `feature` is present.
|
|
100
|
-
*/
|
|
101
|
-
export declare function assertEnterprise(feature: string, ctx: GateContext, env?: NodeJS.ProcessEnv, baked?: string): void;
|
|
102
63
|
/** Env names that carry the activation: the operator's key, and the issuer public key baked at release. */
|
|
103
64
|
export declare const LICENSE_KEY_ENV = "RETICLE_LICENSE_KEY";
|
|
104
65
|
export declare const LICENSE_PUBLIC_KEY_ENV = "RETICLE_LICENSE_PUBLIC_KEY";
|
|
105
|
-
/** The human-facing state of
|
|
66
|
+
/** The human-facing state of license activation on this machine (what `reticle license` shows). */
|
|
106
67
|
interface LicenseReport {
|
|
107
68
|
/**
|
|
108
69
|
* Typed against core's closed list rather than re-listing the strings here: this status is reported
|
|
@@ -112,18 +73,7 @@ interface LicenseReport {
|
|
|
112
73
|
status: LicenseActivation;
|
|
113
74
|
/** The stable license id — what usage is attributed to. Present only when `status` is `active`. */
|
|
114
75
|
licenseId?: string;
|
|
115
|
-
/**
|
|
116
|
-
* What a licence unlocks in this build, reported alongside where to get one — being told to set a
|
|
117
|
-
* variable with no idea what it unlocks is a dead end at the moment somebody is asking. Omitted
|
|
118
|
-
* once a licence is `active`, where the reader already has it.
|
|
119
|
-
*/
|
|
120
|
-
gated?: readonly string[];
|
|
121
76
|
contact: string;
|
|
122
|
-
/**
|
|
123
|
-
* The key is valid, and scoped to features this build does not gate, so it unlocks nothing here.
|
|
124
|
-
* Present only when true: a `false` on every healthy licence is noise on the common case.
|
|
125
|
-
*/
|
|
126
|
-
coversNothingHere?: boolean;
|
|
127
77
|
org?: string;
|
|
128
78
|
plan?: string;
|
|
129
79
|
expiresAt?: number;
|
|
@@ -132,18 +82,8 @@ interface LicenseReport {
|
|
|
132
82
|
}
|
|
133
83
|
/**
|
|
134
84
|
* Resolve activation entirely from the environment — the install mechanism: the release bakes the
|
|
135
|
-
* issuer public key, the operator sets RETICLE_LICENSE_KEY. No public key configured ⇒ evaluation mode
|
|
136
|
-
*
|
|
85
|
+
* issuer public key, the operator sets RETICLE_LICENSE_KEY. No public key configured ⇒ evaluation mode.
|
|
86
|
+
* Offline, no phone-home.
|
|
137
87
|
*/
|
|
138
88
|
export declare function describeLicense(now: number, env?: NodeJS.ProcessEnv, baked?: string): LicenseReport;
|
|
139
|
-
/**
|
|
140
|
-
* Gate an enterprise feature using env-resolved activation. Enforcement is ON when the issuer public key
|
|
141
|
-
* is configured (a real release). Without it, behaviour splits on runtime:
|
|
142
|
-
* - dev/eval (NODE_ENV ≠ production): features run FREE — never blocks a contributor or CI.
|
|
143
|
-
* - production (NODE_ENV = production): FAIL CLOSED — a release that reaches production with no
|
|
144
|
-
* resolvable issuer key was mis-built (the baked key wasn't stamped in, BAKED_ISSUER_PUBLIC_KEY_PEM
|
|
145
|
-
* is still ''), and silently unlocking every ee feature with no key and no warning is the worst
|
|
146
|
-
* outcome. Deny instead, with a distinct reason so the misconfiguration is obvious.
|
|
147
|
-
*/
|
|
148
|
-
export declare function assertEnterpriseFromEnv(feature: string, now: number, env?: NodeJS.ProcessEnv, baked?: string): void;
|
|
149
89
|
export {};
|