@reticlehq/server 3.8.0 → 3.8.2
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/SKILL.md +2 -2
- package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
- package/dist/command/cli/answers/gate-hook-message.js +5 -1
- package/dist/command/cli/auth/cloud-session.d.ts +1 -19
- package/dist/command/cli/auth/device-flow.d.ts +3 -37
- package/dist/command/cli/cli-doctor.js +30 -23
- package/dist/command/cli/cli-flow-commands.js +7 -1
- package/dist/command/cli/cli-parse-serve.d.ts +24 -0
- package/dist/command/cli/cli-parse-serve.js +124 -0
- package/dist/command/cli/cli-parse.d.ts +1 -10
- package/dist/command/cli/cli-parse.js +62 -121
- package/dist/command/cli/cli-usage.js +5 -2
- package/dist/command/cli/cloud-link.d.ts +1 -20
- package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
- package/dist/command/cli/ports/port-holder.js +6 -1
- package/dist/command/cli/project-name.d.ts +1 -7
- package/dist/command/cli/project-name.js +1 -0
- package/dist/command/daemon/daemon.d.ts +6 -0
- package/dist/command/daemon/daemon.js +52 -2
- package/dist/command/daemon/win32-escape.d.ts +32 -0
- package/dist/command/daemon/win32-escape.js +119 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
- package/dist/command/setup/bringup/dev-server-wait.js +5 -1
- package/dist/command/version/version-skew.d.ts +2 -0
- package/dist/command/version/version-skew.js +7 -3
- package/dist/features/crawl/crawl-tools.js +2 -2
- package/dist/features/exhaust/ledger.d.ts +15 -96
- package/dist/features/exhaust/ledger.js +3 -3
- package/dist/features/license/license.d.ts +2 -14
- package/dist/hooks/hook-commands.js +19 -9
- package/dist/judgement/capsule/capsule-store.d.ts +2 -39
- package/dist/language/flows/annotate-tools.js +1 -1
- package/dist/language/flows/fields/flow-secret-field.js +1 -1
- package/dist/language/flows/flow-expect-grammar.js +14 -6
- package/dist/language/flows/flow-lease.d.ts +27 -0
- package/dist/language/flows/flow-lease.js +59 -0
- package/dist/language/flows/flow-replay-run.d.ts +1 -87
- package/dist/language/flows/flow-replay-run.js +88 -283
- package/dist/language/flows/flow-replayable.d.ts +1 -1
- package/dist/language/flows/flow-start-path.d.ts +108 -0
- package/dist/language/flows/flow-start-path.js +313 -0
- package/dist/language/flows/flow-tools.d.ts +2 -23
- package/dist/language/flows/flow-tools.js +34 -66
- package/dist/language/flows/recording/flake.d.ts +3 -27
- package/dist/language/flows/recording/flake.js +1 -1
- package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
- package/dist/machine/git-tracks.js +1 -0
- package/dist/memory/cloud/cloud-sync.d.ts +2 -24
- package/dist/memory/intent/intent-shard.d.ts +41 -266
- package/dist/memory/journal/envelope-store.js +1 -1
- package/dist/memory/journal/envelope.d.ts +6 -154
- package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
- package/dist/memory/journal/on-disk/ambient-file.js +1 -1
- package/dist/memory/journal/self-observation.js +3 -1
- package/dist/memory/journal/session-journal.js +39 -20
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +6 -23
- package/dist/portal/input/real-input.js +95 -192
- package/dist/portal/input/viewport-tools.d.ts +19 -15
- package/dist/portal/pool/browser-pool.d.ts +5 -55
- package/dist/portal/pool/browser-pool.js +121 -100
- package/dist/portal/pool/pool-lease.d.ts +128 -0
- package/dist/portal/pool/pool-lease.js +49 -0
- package/dist/portal/production/production-activation.d.ts +5 -27
- package/dist/portal/session/control-envelope.d.ts +1 -1
- package/dist/portal/session/zero-install-first-call.d.ts +3 -0
- package/dist/portal/session/zero-install-first-call.js +33 -12
- package/dist/surface/mcp/mcp.d.ts +3 -3
- package/dist/surface/mcp/mcp.js +4 -6
- package/dist/surface/tools/act/act-danger.js +32 -17
- package/dist/surface/tools/act/durable.js +1 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -3
- package/dist/surface/tools/act-tools.js +15 -16
- package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.d.ts +8 -0
- package/dist/surface/tools/dynamic-tools.js +36 -16
- package/dist/surface/tools/empty-session-list.d.ts +5 -29
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/gap-novelty.d.ts +1 -1
- package/dist/surface/tools/gaps/capability-absences.js +4 -3
- package/dist/surface/tools/harness-gate.d.ts +1 -1
- package/dist/surface/tools/harness-toolset.js +3 -4
- package/dist/surface/tools/lease-permissions.d.ts +1 -1
- package/dist/surface/tools/lease-readiness.d.ts +23 -1
- package/dist/surface/tools/lease-readiness.js +75 -9
- package/dist/surface/tools/lease-tools.d.ts +1 -52
- package/dist/surface/tools/lease-tools.js +79 -142
- package/dist/surface/tools/observe-tools.js +3 -1
- package/dist/surface/tools/read-tools.js +5 -5
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/session-ports.d.ts +57 -0
- package/dist/surface/tools/session-ports.js +102 -0
- package/dist/surface/tools/tool-kit.d.ts +5 -3
- package/dist/surface/tools/tools.js +1 -1
- package/dist/telemetry/daemon-telemetry.d.ts +3 -1
- package/dist/telemetry/daemon-telemetry.js +9 -2
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +2 -0
- package/docs/agent-cheatsheet.md +12 -0
- package/docs/cli/gate.mdx +1 -1
- package/docs/cli/verify.mdx +8 -1
- package/docs/first-drive.md +7 -0
- package/docs/multi-agent-testing.md +2 -0
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/predicates.mdx +6 -0
- package/docs/skill.md +3 -3
- package/docs/troubleshooting.mdx +10 -0
- package/docs/usage.md +1 -1
- package/package.json +8 -9
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* and re-exports this surface.
|
|
5
5
|
*/
|
|
6
6
|
import { parseFeedbackArgs } from './cli-parse-feedback.js';
|
|
7
|
+
import { HEADLESS_FLAG, parseServeFlags } from './cli-parse-serve.js';
|
|
7
8
|
import { parseVerifySuffix } from './cli-parse-verify.js';
|
|
8
9
|
import { HEADED_FLAG, PORT_FLAG, VERIFY_COMMAND, missingOperand, missingValue, notANumber, retiredFlag, unknownArgument, CLOUD_COMMANDS, REMOVED_COMMANDS, removedNote, } from './cli-parse-grammar.js';
|
|
9
10
|
// Re-exported because the daemon's own argv builder reaches for these through this module, which is
|
|
10
11
|
// where the grammar has always lived as far as every caller is concerned.
|
|
11
12
|
export { HEADED_FLAG, PORT_FLAG };
|
|
13
|
+
export { DRIVE_FLAG, HEADLESS_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, } from './cli-parse-serve.js';
|
|
12
14
|
import { FORCE_FLAG } from './cli-kill.js';
|
|
13
15
|
// Re-exported so every existing importer of these flags is unaffected by the file split.
|
|
14
16
|
export { AGENT_FLAG, BUG_FLAG, CONTACT_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
|
|
@@ -122,24 +124,19 @@ const COMMAND_FLAG_ALIASES = new Map([
|
|
|
122
124
|
*/
|
|
123
125
|
function numberFlag(args, name) {
|
|
124
126
|
const at = args.indexOf(name);
|
|
125
|
-
if (at < 0)
|
|
127
|
+
if (at < 0) {
|
|
126
128
|
return 0;
|
|
129
|
+
}
|
|
127
130
|
const raw = Number(args[at + 1]);
|
|
128
131
|
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 0;
|
|
129
132
|
}
|
|
130
133
|
/** The subcommand name if we recognize it, else `unknown`. Bare `reticle` reports `help`. */
|
|
131
134
|
export function knownCommand(arg) {
|
|
132
|
-
if (arg === undefined || '' === arg)
|
|
135
|
+
if (arg === undefined || '' === arg) {
|
|
133
136
|
return HELP_COMMAND;
|
|
137
|
+
}
|
|
134
138
|
return KNOWN_COMMANDS.has(arg) ? arg : (COMMAND_FLAG_ALIASES.get(arg) ?? UNKNOWN_COMMAND);
|
|
135
139
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Force a hidden browser. The default is now HEADED: a run nobody can see is a run nobody trusts,
|
|
138
|
-
* and every "did it actually do anything?" question cost a round-trip. CI passes this (or just sets
|
|
139
|
-
* CI, which flips the default) because there is no display there to be headed on.
|
|
140
|
-
*/
|
|
141
|
-
export const HEADLESS_FLAG = '--headless';
|
|
142
|
-
export const DRIVE_FLAG = '--drive';
|
|
143
140
|
const QUIET_FLAG = '--quiet';
|
|
144
141
|
const DRY_RUN_FLAG = '--dry-run';
|
|
145
142
|
const YES_FLAG = '--yes';
|
|
@@ -211,9 +208,6 @@ const LICENSE_FLAG = '--license';
|
|
|
211
208
|
const NO_INSTALL_FLAG = '--no-install';
|
|
212
209
|
/** Give this checkout a project id of its own: a copied repository keeps the original's. */
|
|
213
210
|
const NEW_ID_FLAG = '--new-id';
|
|
214
|
-
export const HTTP_FLAG = '--http';
|
|
215
|
-
export const HTTP_PORT_FLAG = '--http-port';
|
|
216
|
-
export const HTTP_TOKEN_FLAG = '--http-token';
|
|
217
211
|
/**
|
|
218
212
|
* A usage error names the thing that was wrong.
|
|
219
213
|
*
|
|
@@ -226,98 +220,19 @@ export const HTTP_TOKEN_FLAG = '--http-token';
|
|
|
226
220
|
* The help text is still shown — cli.ts renders it as readable text underneath. It is just not the
|
|
227
221
|
* message any more.
|
|
228
222
|
*/
|
|
229
|
-
const requiresHttp = (flag) => ({
|
|
230
|
-
kind: 'error',
|
|
231
|
-
message: `${flag} requires ${HTTP_FLAG} — it configures the verify endpoint ${HTTP_FLAG} starts`,
|
|
232
|
-
});
|
|
233
223
|
const unknownCommand = (command) => ({
|
|
234
224
|
kind: 'error',
|
|
235
225
|
message: `unknown command '${command}'`,
|
|
236
226
|
});
|
|
237
|
-
function parseServeFlags(args, defaultPort, defaultHeadless) {
|
|
238
|
-
let port = defaultPort;
|
|
239
|
-
let driveUrl;
|
|
240
|
-
let headless = defaultHeadless;
|
|
241
|
-
let http = false;
|
|
242
|
-
let httpPort;
|
|
243
|
-
let httpToken;
|
|
244
|
-
let i = 0;
|
|
245
|
-
while (i < args.length) {
|
|
246
|
-
const arg = args[i];
|
|
247
|
-
// `i < args.length` already guarantees this, but the index signature does not say so and the
|
|
248
|
-
// unknown-argument error needs a real string to name.
|
|
249
|
-
if (arg === undefined)
|
|
250
|
-
break;
|
|
251
|
-
if (arg === PORT_FLAG) {
|
|
252
|
-
i++;
|
|
253
|
-
const n = args[i];
|
|
254
|
-
if (n === undefined)
|
|
255
|
-
return missingValue(PORT_FLAG);
|
|
256
|
-
const parsed = parseInt(n, 10);
|
|
257
|
-
if (isNaN(parsed))
|
|
258
|
-
return notANumber(PORT_FLAG, n);
|
|
259
|
-
port = parsed;
|
|
260
|
-
}
|
|
261
|
-
else if (arg === DRIVE_FLAG) {
|
|
262
|
-
i++;
|
|
263
|
-
driveUrl = args[i];
|
|
264
|
-
if (driveUrl === undefined)
|
|
265
|
-
return missingValue(DRIVE_FLAG);
|
|
266
|
-
}
|
|
267
|
-
else if (arg === HEADED_FLAG) {
|
|
268
|
-
headless = false;
|
|
269
|
-
}
|
|
270
|
-
else if (arg === HEADLESS_FLAG) {
|
|
271
|
-
headless = true;
|
|
272
|
-
}
|
|
273
|
-
else if (arg === HTTP_FLAG) {
|
|
274
|
-
http = true;
|
|
275
|
-
}
|
|
276
|
-
else if (arg === HTTP_PORT_FLAG) {
|
|
277
|
-
i++;
|
|
278
|
-
const n = args[i];
|
|
279
|
-
if (n === undefined)
|
|
280
|
-
return missingValue(HTTP_PORT_FLAG);
|
|
281
|
-
const parsed = parseInt(n, 10);
|
|
282
|
-
if (isNaN(parsed))
|
|
283
|
-
return notANumber(HTTP_PORT_FLAG, n);
|
|
284
|
-
httpPort = parsed;
|
|
285
|
-
}
|
|
286
|
-
else if (arg === HTTP_TOKEN_FLAG) {
|
|
287
|
-
i++;
|
|
288
|
-
httpToken = args[i];
|
|
289
|
-
if (httpToken === undefined)
|
|
290
|
-
return missingValue(HTTP_TOKEN_FLAG);
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
return unknownArgument(arg);
|
|
294
|
-
}
|
|
295
|
-
i++;
|
|
296
|
-
}
|
|
297
|
-
// Without `--http` these flags configure an endpoint nothing starts, and they were accepted and
|
|
298
|
-
// silently ignored — worse than being rejected, because the whole reason to pass them is to be
|
|
299
|
-
// honoured (#687).
|
|
300
|
-
if (!http && httpPort !== undefined)
|
|
301
|
-
return requiresHttp(HTTP_PORT_FLAG);
|
|
302
|
-
if (!http && httpToken !== undefined)
|
|
303
|
-
return requiresHttp(HTTP_TOKEN_FLAG);
|
|
304
|
-
return {
|
|
305
|
-
kind: 'ok',
|
|
306
|
-
port,
|
|
307
|
-
headless,
|
|
308
|
-
http,
|
|
309
|
-
...(driveUrl !== undefined ? { driveUrl } : {}),
|
|
310
|
-
...(httpPort !== undefined ? { httpPort } : {}),
|
|
311
|
-
...(httpToken !== undefined ? { httpToken } : {}),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
227
|
function parsePortFlag(args, defaultPort) {
|
|
315
228
|
const idx = args.indexOf(PORT_FLAG);
|
|
316
|
-
if (-1 === idx)
|
|
229
|
+
if (-1 === idx) {
|
|
317
230
|
return defaultPort;
|
|
231
|
+
}
|
|
318
232
|
const n = args[idx + 1];
|
|
319
|
-
if (n === undefined)
|
|
233
|
+
if (n === undefined) {
|
|
320
234
|
return defaultPort;
|
|
235
|
+
}
|
|
321
236
|
const parsed = parseInt(n, 10);
|
|
322
237
|
return isNaN(parsed) ? defaultPort : parsed;
|
|
323
238
|
}
|
|
@@ -341,8 +256,9 @@ function parseDriveSuffix(args, port, defaultHeadless) {
|
|
|
341
256
|
return unknownArgument(arg);
|
|
342
257
|
}
|
|
343
258
|
}
|
|
344
|
-
if (driveUrl === undefined)
|
|
259
|
+
if (driveUrl === undefined) {
|
|
345
260
|
return missingOperand(DRIVE_COMMAND, 'a url');
|
|
261
|
+
}
|
|
346
262
|
return { kind: 'ok', port, driveUrl, headless };
|
|
347
263
|
}
|
|
348
264
|
function parseInitFlags(args) {
|
|
@@ -372,23 +288,27 @@ function parseInitFlags(args) {
|
|
|
372
288
|
const arg = args[i];
|
|
373
289
|
// `i < args.length` already guarantees this, but the index signature does not say so and the
|
|
374
290
|
// unknown-argument error needs a real string to name.
|
|
375
|
-
if (arg === undefined)
|
|
291
|
+
if (arg === undefined) {
|
|
376
292
|
break;
|
|
293
|
+
}
|
|
377
294
|
if (arg === PORT_FLAG) {
|
|
378
295
|
i++;
|
|
379
296
|
const n = args[i];
|
|
380
|
-
if (n === undefined)
|
|
297
|
+
if (n === undefined) {
|
|
381
298
|
return missingValue(PORT_FLAG);
|
|
299
|
+
}
|
|
382
300
|
const parsed = parseInt(n, 10);
|
|
383
|
-
if (isNaN(parsed))
|
|
301
|
+
if (isNaN(parsed)) {
|
|
384
302
|
return notANumber(PORT_FLAG, n);
|
|
303
|
+
}
|
|
385
304
|
port = parsed;
|
|
386
305
|
}
|
|
387
306
|
else if (arg === APP_FLAG) {
|
|
388
307
|
i++;
|
|
389
308
|
const value = args[i];
|
|
390
|
-
if (value === undefined)
|
|
309
|
+
if (value === undefined) {
|
|
391
310
|
return missingValue(APP_FLAG);
|
|
311
|
+
}
|
|
392
312
|
app = value;
|
|
393
313
|
}
|
|
394
314
|
else if (arg === FLOW_FLAG || arg === NO_DRIVE_FLAG || arg === DRIVE_MODEL_FLAG) {
|
|
@@ -397,8 +317,9 @@ function parseInitFlags(args) {
|
|
|
397
317
|
else if (arg === ENV_FLAG) {
|
|
398
318
|
i++;
|
|
399
319
|
const value = args[i];
|
|
400
|
-
if (value === undefined)
|
|
320
|
+
if (value === undefined) {
|
|
401
321
|
return missingValue(ENV_FLAG);
|
|
322
|
+
}
|
|
402
323
|
env.push(value);
|
|
403
324
|
}
|
|
404
325
|
else if (arg === FILES_ONLY_FLAG) {
|
|
@@ -434,25 +355,29 @@ function parseInitFlags(args) {
|
|
|
434
355
|
else if (arg === URL_FLAG) {
|
|
435
356
|
i++;
|
|
436
357
|
const value = args[i];
|
|
437
|
-
if (value === undefined)
|
|
358
|
+
if (value === undefined) {
|
|
438
359
|
return missingValue(URL_FLAG);
|
|
360
|
+
}
|
|
439
361
|
url = value;
|
|
440
362
|
}
|
|
441
363
|
else if (arg === LICENSE_FLAG) {
|
|
442
364
|
i++;
|
|
443
365
|
const value = args[i];
|
|
444
|
-
if (value === undefined)
|
|
366
|
+
if (value === undefined) {
|
|
445
367
|
return missingValue(LICENSE_FLAG);
|
|
368
|
+
}
|
|
446
369
|
licenseKey = value;
|
|
447
370
|
}
|
|
448
371
|
else if (arg === TIMEOUT_FLAG_INIT) {
|
|
449
372
|
i++;
|
|
450
373
|
const value = args[i];
|
|
451
|
-
if (value === undefined)
|
|
374
|
+
if (value === undefined) {
|
|
452
375
|
return missingValue(TIMEOUT_FLAG_INIT);
|
|
376
|
+
}
|
|
453
377
|
const seconds = parseInt(value, 10);
|
|
454
|
-
if (isNaN(seconds))
|
|
378
|
+
if (isNaN(seconds)) {
|
|
455
379
|
return notANumber(TIMEOUT_FLAG_INIT, value);
|
|
380
|
+
}
|
|
456
381
|
timeoutSeconds = seconds;
|
|
457
382
|
}
|
|
458
383
|
else if (arg === NO_MCP_FLAG) {
|
|
@@ -527,8 +452,9 @@ function parseTargetArgs(rest) {
|
|
|
527
452
|
i += 1;
|
|
528
453
|
continue;
|
|
529
454
|
}
|
|
530
|
-
if (arg !== undefined && !arg.startsWith('-'))
|
|
455
|
+
if (arg !== undefined && !arg.startsWith('-')) {
|
|
531
456
|
files.push(arg);
|
|
457
|
+
}
|
|
532
458
|
}
|
|
533
459
|
return since === undefined ? { files } : { files, since };
|
|
534
460
|
}
|
|
@@ -538,8 +464,9 @@ function parseTargetArgs(rest) {
|
|
|
538
464
|
* workspace itself, and a tutorial only dials one when asked to run.
|
|
539
465
|
*/
|
|
540
466
|
export function dialsTheDaemon(parsed) {
|
|
541
|
-
if ('init' === parsed.kind)
|
|
467
|
+
if ('init' === parsed.kind) {
|
|
542
468
|
return false;
|
|
469
|
+
}
|
|
543
470
|
return 'port' in parsed;
|
|
544
471
|
}
|
|
545
472
|
/**
|
|
@@ -550,16 +477,19 @@ export function dialsTheDaemon(parsed) {
|
|
|
550
477
|
*/
|
|
551
478
|
export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
552
479
|
// Bare `reticle` is `serve`.
|
|
553
|
-
if (0 === argv.length)
|
|
480
|
+
if (0 === argv.length) {
|
|
554
481
|
return { kind: 'serve', port: defaultPort, headless: defaultHeadless, http: false };
|
|
482
|
+
}
|
|
555
483
|
const [cmd, ...rest] = argv;
|
|
556
484
|
// Guaranteed by the bare-argv check above; restated so the unknown-command error can name it.
|
|
557
|
-
if (cmd === undefined)
|
|
485
|
+
if (cmd === undefined) {
|
|
558
486
|
return { kind: 'error', message: CLI_USAGE };
|
|
487
|
+
}
|
|
559
488
|
// `version` (or the conventional -v/--version flags) prints the running version — the diagnostic the
|
|
560
489
|
// troubleshooting docs lean on to confirm which npx-resolved build is actually executing.
|
|
561
|
-
if (cmd === VERSION_COMMAND || '--version' === cmd || '-v' === cmd)
|
|
490
|
+
if (cmd === VERSION_COMMAND || '--version' === cmd || '-v' === cmd) {
|
|
562
491
|
return { kind: 'version' };
|
|
492
|
+
}
|
|
563
493
|
// `help` (and the conventional -h/--help) print usage to stdout and exit 0 — the universal first move
|
|
564
494
|
// for a new user, which otherwise fell through to a JSON error with exit 1.
|
|
565
495
|
//
|
|
@@ -573,23 +503,27 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
573
503
|
// Removed commands answer before help does: `reticle hunt --help` should say it is gone, not
|
|
574
504
|
// describe something that no longer runs.
|
|
575
505
|
const removed = REMOVED_COMMANDS.get(cmd);
|
|
576
|
-
if (removed !== undefined)
|
|
506
|
+
if (removed !== undefined) {
|
|
577
507
|
return { kind: 'removed', message: removedNote(cmd, removed) };
|
|
508
|
+
}
|
|
578
509
|
if (HELP_COMMAND === cmd || HELP_FLAGS.has(cmd)) {
|
|
579
510
|
// `reticle help all`, `reticle --help all`, `reticle help init`.
|
|
580
511
|
const topic = rest.find((arg) => !HELP_FLAGS.has(arg));
|
|
581
512
|
return topic === undefined ? { kind: 'help' } : { kind: 'help', topic };
|
|
582
513
|
}
|
|
583
|
-
if (argv.some((arg) => HELP_FLAGS.has(arg)))
|
|
514
|
+
if (argv.some((arg) => HELP_FLAGS.has(arg))) {
|
|
584
515
|
return { kind: 'help', topic: cmd };
|
|
516
|
+
}
|
|
585
517
|
// The list is the gate, so a command cannot be routed without also being named in telemetry.
|
|
586
|
-
if (!LOCAL_COMMAND_SET.has(cmd))
|
|
518
|
+
if (!LOCAL_COMMAND_SET.has(cmd)) {
|
|
587
519
|
return unknownCommand(cmd);
|
|
520
|
+
}
|
|
588
521
|
switch (cmd) {
|
|
589
522
|
case INIT_COMMAND: {
|
|
590
523
|
const r = parseInitFlags(rest);
|
|
591
|
-
if ('error' === r.kind)
|
|
524
|
+
if ('error' === r.kind) {
|
|
592
525
|
return r;
|
|
526
|
+
}
|
|
593
527
|
return {
|
|
594
528
|
kind: 'init',
|
|
595
529
|
port: r.port,
|
|
@@ -615,8 +549,9 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
615
549
|
}
|
|
616
550
|
case SERVE_COMMAND: {
|
|
617
551
|
const r = parseServeFlags(rest, defaultPort, defaultHeadless);
|
|
618
|
-
if ('error' === r.kind)
|
|
552
|
+
if ('error' === r.kind) {
|
|
619
553
|
return r;
|
|
554
|
+
}
|
|
620
555
|
return {
|
|
621
556
|
kind: 'serve',
|
|
622
557
|
port: r.port,
|
|
@@ -631,8 +566,9 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
631
566
|
const port = parsePortFlag(rest, defaultPort);
|
|
632
567
|
// `stop --force` is what `kill` was: free the port by its listener, which must still be a
|
|
633
568
|
// Reticle daemon. Overriding that refusal stays on `restart --force` and the old `kill --force`.
|
|
634
|
-
if (rest.includes(FORCE_FLAG))
|
|
569
|
+
if (rest.includes(FORCE_FLAG)) {
|
|
635
570
|
return { kind: 'kill', port, force: false };
|
|
571
|
+
}
|
|
636
572
|
const quiet = rest.includes(QUIET_FLAG);
|
|
637
573
|
return { kind: 'stop', port, quiet };
|
|
638
574
|
}
|
|
@@ -687,8 +623,9 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
687
623
|
}
|
|
688
624
|
// Only `mcp` today. A bare `reticle setup` falls to help rather than guessing, because the
|
|
689
625
|
// next subcommand here will not be a synonym for this one.
|
|
690
|
-
if (what !== 'mcp')
|
|
626
|
+
if (what !== 'mcp') {
|
|
691
627
|
return { kind: 'help', topic: SETUP_COMMAND };
|
|
628
|
+
}
|
|
692
629
|
return { kind: 'setup-mcp' };
|
|
693
630
|
}
|
|
694
631
|
case LICENSE_COMMAND:
|
|
@@ -715,14 +652,16 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
715
652
|
}
|
|
716
653
|
case DRIVE_COMMAND: {
|
|
717
654
|
const r = parseDriveSuffix(rest, defaultPort, defaultHeadless);
|
|
718
|
-
if ('error' === r.kind)
|
|
655
|
+
if ('error' === r.kind) {
|
|
719
656
|
return r;
|
|
657
|
+
}
|
|
720
658
|
return { kind: 'drive', port: r.port, driveUrl: r.driveUrl, headless: r.headless };
|
|
721
659
|
}
|
|
722
660
|
case DAEMON_INNER_COMMAND: {
|
|
723
661
|
const r = parseServeFlags(rest, defaultPort, defaultHeadless);
|
|
724
|
-
if ('error' === r.kind)
|
|
662
|
+
if ('error' === r.kind) {
|
|
725
663
|
return r;
|
|
664
|
+
}
|
|
726
665
|
return {
|
|
727
666
|
kind: '_daemon',
|
|
728
667
|
port: r.port,
|
|
@@ -735,8 +674,9 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
735
674
|
}
|
|
736
675
|
case VERIFY_COMMAND: {
|
|
737
676
|
const r = parseVerifySuffix(rest, defaultPort);
|
|
738
|
-
if ('error' === r.kind)
|
|
677
|
+
if ('error' === r.kind) {
|
|
739
678
|
return r;
|
|
679
|
+
}
|
|
740
680
|
return {
|
|
741
681
|
kind: 'verify',
|
|
742
682
|
url: r.url,
|
|
@@ -797,8 +737,9 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
797
737
|
return { kind: 'rollback' };
|
|
798
738
|
case MCP_COMMAND: {
|
|
799
739
|
const r = parseServeFlags(rest, defaultPort, defaultHeadless);
|
|
800
|
-
if ('error' === r.kind)
|
|
740
|
+
if ('error' === r.kind) {
|
|
801
741
|
return r;
|
|
742
|
+
}
|
|
802
743
|
return {
|
|
803
744
|
kind: 'mcp',
|
|
804
745
|
port: r.port,
|
|
@@ -56,6 +56,9 @@ const VERIFY_DETAIL = `Drives the URL and verifies the saved flows. Exit 0 means
|
|
|
56
56
|
https://docs.reticle.sh/http-transport.md`;
|
|
57
57
|
const BROWSER_NOTE = `The browser Reticle opens for your agent is shown so you can watch the run. --headless hides
|
|
58
58
|
it; CI, RETICLE_HEADLESS=1, or a Linux machine with no display hides it too.`;
|
|
59
|
+
/** What `--http` starts. MCP is already on the bridge port; this flag is the verify endpoint. */
|
|
60
|
+
const HTTP_HELP = '--http starts the verify endpoint. MCP is already served on the bridge port.';
|
|
61
|
+
const DAEMON_DETAIL = `${BROWSER_NOTE}\n\n${HTTP_HELP}`;
|
|
59
62
|
const COMMANDS = [
|
|
60
63
|
{
|
|
61
64
|
name: 'init',
|
|
@@ -225,14 +228,14 @@ const COMMANDS = [
|
|
|
225
228
|
synopsis: 'mcp [--port N] [--drive <url>] [--headless] [--headed] [--http] [--http-port N] [--http-token T]',
|
|
226
229
|
summary: 'The MCP stdio server your agent runs; starts the daemon if needed',
|
|
227
230
|
group: HelpGroup.MORE,
|
|
228
|
-
detail:
|
|
231
|
+
detail: DAEMON_DETAIL,
|
|
229
232
|
},
|
|
230
233
|
{
|
|
231
234
|
name: 'serve',
|
|
232
235
|
synopsis: 'serve [--port N] [--drive <url>] [--headless] [--headed] [--http] [--http-port N] [--http-token T]',
|
|
233
236
|
summary: 'Start the daemon in the background; returns once it answers',
|
|
234
237
|
group: HelpGroup.MORE,
|
|
235
|
-
detail:
|
|
238
|
+
detail: DAEMON_DETAIL,
|
|
236
239
|
},
|
|
237
240
|
{
|
|
238
241
|
name: 'drive',
|
|
@@ -10,28 +10,9 @@ export declare const validateKey: (url: string, key: string) => Promise<z.infer<
|
|
|
10
10
|
declare const WhoamiSchema: z.ZodObject<{
|
|
11
11
|
projectId: z.ZodString;
|
|
12
12
|
projectName: z.ZodString;
|
|
13
|
-
/**
|
|
14
|
-
* Which tenant the key belongs to. Optional for the same reason `dashboardUrl` is — an older
|
|
15
|
-
* cloud does not send it — and when it is missing a stored key cannot be proved to be ours, which
|
|
16
|
-
* is why the reuse path below mints instead of guessing.
|
|
17
|
-
*/
|
|
18
13
|
orgId: z.ZodOptional<z.ZodString>;
|
|
19
|
-
/**
|
|
20
|
-
* Where this project's dashboard lives. Optional because an older cloud does not send it, and a
|
|
21
|
-
* CLI that refused to link against one would break the thing it is supposed to connect.
|
|
22
|
-
*/
|
|
23
14
|
dashboardUrl: z.ZodOptional<z.ZodString>;
|
|
24
|
-
},
|
|
25
|
-
projectId: string;
|
|
26
|
-
projectName: string;
|
|
27
|
-
orgId?: string | undefined;
|
|
28
|
-
dashboardUrl?: string | undefined;
|
|
29
|
-
}, {
|
|
30
|
-
projectId: string;
|
|
31
|
-
projectName: string;
|
|
32
|
-
orgId?: string | undefined;
|
|
33
|
-
dashboardUrl?: string | undefined;
|
|
34
|
-
}>;
|
|
15
|
+
}, z.core.$strip>;
|
|
35
16
|
export interface PlatformLinked {
|
|
36
17
|
via: PlatformLinkVia;
|
|
37
18
|
/** The project this folder was bound to before, if any: a re-run against it is not a new link. */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent link row of `reticle doctor`, as one pure decision (reticlehq/reticle#1710).
|
|
3
|
+
*
|
|
4
|
+
* The row used to be built inline from the attach record alone: once a client had attached,
|
|
5
|
+
* enumerated and called, it stayed green forever, and a client that had started but never listed
|
|
6
|
+
* was told to restart the client. Both answers are wrong when the port is held but `/status`
|
|
7
|
+
* does not answer: the record cannot say whether the link works now, and the daemon row above
|
|
8
|
+
* already names the one fix for the held port. So in that state this row only points back at
|
|
9
|
+
* the daemon row. It never prints a second command, and it never says "the daemon", because
|
|
10
|
+
* this state also covers a port held by a different program.
|
|
11
|
+
*
|
|
12
|
+
* The record and action strings below are copied verbatim from the inline row this replaces;
|
|
13
|
+
* this module changes their priority, not their words.
|
|
14
|
+
*/
|
|
15
|
+
export interface AgentLinkInput {
|
|
16
|
+
/** The split-brain sentence, when this project is served by another daemon. */
|
|
17
|
+
split: string | undefined;
|
|
18
|
+
/** The attach-record action, or undefined when the record says the link worked. */
|
|
19
|
+
attachAction: string | undefined;
|
|
20
|
+
/** True when the port is held but `/status` does not answer. */
|
|
21
|
+
foreign: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function agentLinkText(input: AgentLinkInput): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent link row of `reticle doctor`, as one pure decision (reticlehq/reticle#1710).
|
|
3
|
+
*
|
|
4
|
+
* The row used to be built inline from the attach record alone: once a client had attached,
|
|
5
|
+
* enumerated and called, it stayed green forever, and a client that had started but never listed
|
|
6
|
+
* was told to restart the client. Both answers are wrong when the port is held but `/status`
|
|
7
|
+
* does not answer: the record cannot say whether the link works now, and the daemon row above
|
|
8
|
+
* already names the one fix for the held port. So in that state this row only points back at
|
|
9
|
+
* the daemon row. It never prints a second command, and it never says "the daemon", because
|
|
10
|
+
* this state also covers a port held by a different program.
|
|
11
|
+
*
|
|
12
|
+
* The record and action strings below are copied verbatim from the inline row this replaces;
|
|
13
|
+
* this module changes their priority, not their words.
|
|
14
|
+
*/
|
|
15
|
+
/** The healthy-record sentence, word for word as doctor printed it before. */
|
|
16
|
+
const RECORD_HEALTHY = '✓ an MCP client attached here and called tools before (a record, not a live check)';
|
|
17
|
+
export function agentLinkText(input) {
|
|
18
|
+
if (input.split !== undefined) {
|
|
19
|
+
return `✗ ${input.split}`;
|
|
20
|
+
}
|
|
21
|
+
if (input.foreign) {
|
|
22
|
+
return '✗ the agent link is not checked live here — follow the daemon row above.';
|
|
23
|
+
}
|
|
24
|
+
if (input.attachAction === undefined) {
|
|
25
|
+
return RECORD_HEALTHY;
|
|
26
|
+
}
|
|
27
|
+
return `✗ ${input.attachAction}`;
|
|
28
|
+
}
|
|
@@ -27,7 +27,12 @@ const LOOKUP_TIMEOUT_MS = 2_000;
|
|
|
27
27
|
*/
|
|
28
28
|
export function captureLookup(command, args, timeoutMs = LOOKUP_TIMEOUT_MS) {
|
|
29
29
|
try {
|
|
30
|
-
|
|
30
|
+
// The daemon has no console: without windowsHide, each lookup opens a visible window on Windows.
|
|
31
|
+
const result = spawnSync(command, [...args], {
|
|
32
|
+
encoding: 'utf8',
|
|
33
|
+
timeout: timeoutMs,
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
});
|
|
31
36
|
return 0 === result.status && 'string' === typeof result.stdout ? result.stdout : null;
|
|
32
37
|
}
|
|
33
38
|
catch {
|
|
@@ -34,13 +34,7 @@ localProjectId?: string) => string;
|
|
|
34
34
|
export declare const CreatedProjectSchema: z.ZodObject<{
|
|
35
35
|
projectId: z.ZodString;
|
|
36
36
|
name: z.ZodString;
|
|
37
|
-
},
|
|
38
|
-
name: string;
|
|
39
|
-
projectId: string;
|
|
40
|
-
}, {
|
|
41
|
-
name: string;
|
|
42
|
-
projectId: string;
|
|
43
|
-
}>;
|
|
37
|
+
}, z.core.$strip>;
|
|
44
38
|
/** ` in workspace "<name>"`, or nothing when no name is known. */
|
|
45
39
|
export declare const inWorkspace: (name: string | undefined) => string;
|
|
46
40
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type EscapeRequest, type EscapeResult } from './win32-escape.js';
|
|
1
2
|
export { MAX_DAEMON_LOG_BYTES, recoverOversizedLog } from './lifetime/log-cap.js';
|
|
2
3
|
/** Env override for the whole state directory — see ReticleEnv.STATE_DIR. */
|
|
3
4
|
export declare const STATE_DIR_ENV: "RETICLE_STATE_DIR";
|
|
@@ -72,8 +73,13 @@ export interface SpawnDaemonDeps {
|
|
|
72
73
|
spawnChild(command: string, args: readonly string[], options: {
|
|
73
74
|
detached: boolean;
|
|
74
75
|
stdio: readonly ('ignore' | number)[];
|
|
76
|
+
env?: Record<string, string | undefined>;
|
|
75
77
|
}): SpawnedChild;
|
|
76
78
|
pidAlive(pid: number): boolean;
|
|
79
|
+
/** `process.platform`. Optional so a test that does not care runs the detached start. */
|
|
80
|
+
readonly platform?: string;
|
|
81
|
+
/** Windows only: start the daemon outside the Job Object of this process. See win32-escape.ts. */
|
|
82
|
+
readonly escapeJob?: (request: EscapeRequest) => EscapeResult;
|
|
77
83
|
/**
|
|
78
84
|
* A predecessor died without a clean stop; `ageMs` is how long it had run. Optional so a test that
|
|
79
85
|
* does not care reports nothing. The default sends `daemon_died_unclean`.
|
|
@@ -2,11 +2,12 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, statSync, renameSync, writeSync, } from 'node:fs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
-
import { daemonRegistryFileName, ReticleEnv } from '@reticlehq/core';
|
|
5
|
+
import { daemonRegistryFileName, DaemonSpawnMethod, ReticleEnv, } from '@reticlehq/core';
|
|
6
6
|
import { log } from '../../log.js';
|
|
7
7
|
import { uncleanPredecessor } from './lifetime/unclean-predecessor.js';
|
|
8
8
|
import { reportDaemonDiedUnclean } from '../../telemetry/proxy-telemetry.js';
|
|
9
9
|
import { MAX_DAEMON_LOG_BYTES, pruneOldDaemonLogs } from './lifetime/log-cap.js';
|
|
10
|
+
import { escapeJobViaWmi } from './win32-escape.js';
|
|
10
11
|
// Re-exported from their owner so callers outside this directory — the proxy log, the rotation
|
|
11
12
|
// tests — keep reaching `daemon` rather than `lifetime`, which is a sink. See lifetime/log-cap.ts.
|
|
12
13
|
export { MAX_DAEMON_LOG_BYTES, recoverOversizedLog } from './lifetime/log-cap.js';
|
|
@@ -197,8 +198,14 @@ function defaultSpawnDaemonDeps() {
|
|
|
197
198
|
closeFile: closeSync,
|
|
198
199
|
fileSize: (path) => (existsSync(path) ? statSync(path).size : 0),
|
|
199
200
|
renameFile: renameSync,
|
|
200
|
-
spawnChild: (command, args, options) => spawn(command, [...args], {
|
|
201
|
+
spawnChild: (command, args, options) => spawn(command, [...args], {
|
|
202
|
+
detached: options.detached,
|
|
203
|
+
stdio: [...options.stdio],
|
|
204
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
205
|
+
}),
|
|
201
206
|
pidAlive: isAlive,
|
|
207
|
+
platform: process.platform,
|
|
208
|
+
escapeJob: escapeJobViaWmi,
|
|
202
209
|
onUncleanDeath: (ageMs) => void reportDaemonDiedUnclean(ageMs),
|
|
203
210
|
};
|
|
204
211
|
}
|
|
@@ -215,6 +222,24 @@ function reportUncleanAge(pidFilePath, deps) {
|
|
|
215
222
|
// An unreadable pidfile has no age to report.
|
|
216
223
|
}
|
|
217
224
|
}
|
|
225
|
+
const WINDOWS = 'win32';
|
|
226
|
+
/** The WMI start must never stop a daemon start: a throw reads as "nothing launched". */
|
|
227
|
+
function tryEscapeJob(escape, request) {
|
|
228
|
+
try {
|
|
229
|
+
return escape(request);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return { launched: false };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function removeQuietly(path) {
|
|
236
|
+
try {
|
|
237
|
+
unlinkSync(path);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// racing another reclaimer — fine
|
|
241
|
+
}
|
|
242
|
+
}
|
|
218
243
|
/**
|
|
219
244
|
* Spawn the reticle daemon as a detached background process, redirecting output to the log file.
|
|
220
245
|
* Writes the PID file from the parent before returning so callers can call isRunning
|
|
@@ -307,11 +332,36 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
|
|
|
307
332
|
// A log we cannot write is not a reason to refuse to start the daemon.
|
|
308
333
|
}
|
|
309
334
|
}
|
|
335
|
+
let method = DaemonSpawnMethod.DETACHED;
|
|
336
|
+
if (WINDOWS === deps.platform && deps.escapeJob !== undefined) {
|
|
337
|
+
const escaped = tryEscapeJob(deps.escapeJob, {
|
|
338
|
+
command: nodeExec,
|
|
339
|
+
args: [scriptPath, ...args],
|
|
340
|
+
cwd: process.cwd(),
|
|
341
|
+
env: { ...process.env, [ReticleEnv.SPAWN_METHOD]: DaemonSpawnMethod.WMI },
|
|
342
|
+
logPath: logFilePath,
|
|
343
|
+
specPath: join(deps.home, `daemon-${port}.spawn.json`),
|
|
344
|
+
});
|
|
345
|
+
if (escaped.launched) {
|
|
346
|
+
deps.closeFile(logFd);
|
|
347
|
+
// No pid in time: release the lock, so the pidfile never names a process that is not ours.
|
|
348
|
+
if (escaped.pid === undefined) {
|
|
349
|
+
deps.closeFile(lockFd);
|
|
350
|
+
removeQuietly(pidFilePath);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
writeFileSync(lockFd, String(escaped.pid), 'utf8');
|
|
354
|
+
deps.closeFile(lockFd);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
method = DaemonSpawnMethod.WMI_FALLBACK;
|
|
358
|
+
}
|
|
310
359
|
let child;
|
|
311
360
|
try {
|
|
312
361
|
child = deps.spawnChild(nodeExec, [scriptPath, ...args], {
|
|
313
362
|
detached: true,
|
|
314
363
|
stdio: ['ignore', logFd, logFd],
|
|
364
|
+
env: { ...process.env, [ReticleEnv.SPAWN_METHOD]: method },
|
|
315
365
|
});
|
|
316
366
|
}
|
|
317
367
|
catch {
|