agent-relay 15.0.0-next.6 → 15.0.0-next.8
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/dist/cli/commands/init.d.ts +4 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +215 -45
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/workspace.d.ts.map +1 -1
- package/dist/cli/commands/workspace.js +26 -1
- package/dist/cli/commands/workspace.js.map +1 -1
- package/dist/cli/lib/agent-registration.d.ts +7 -0
- package/dist/cli/lib/agent-registration.d.ts.map +1 -1
- package/dist/cli/lib/agent-registration.js +9 -0
- package/dist/cli/lib/agent-registration.js.map +1 -1
- package/dist/cli/lib/init-leftovers.d.ts +36 -1
- package/dist/cli/lib/init-leftovers.d.ts.map +1 -1
- package/dist/cli/lib/init-leftovers.js +52 -1
- package/dist/cli/lib/init-leftovers.js.map +1 -1
- package/dist/cli/lib/probe-core.d.ts +4 -2
- package/dist/cli/lib/probe-core.d.ts.map +1 -1
- package/dist/cli/lib/probe-core.js +22 -1
- package/dist/cli/lib/probe-core.js.map +1 -1
- package/package.json +10 -10
|
@@ -68,8 +68,10 @@ export declare function withInitDefaults(overrides?: Partial<InitCommandDependen
|
|
|
68
68
|
* machine by default, the hosted service, or your own server), then connect
|
|
69
69
|
* your agent tools. By default the Agent Relay core injects messages into
|
|
70
70
|
* their running sessions; `--delivery mcp` (or a machine the core cannot run
|
|
71
|
-
* on) uses the Relay MCP server instead.
|
|
72
|
-
*
|
|
71
|
+
* on) uses the Relay MCP server instead. It asks nothing: flags, then
|
|
72
|
+
* defaults (this machine, every installed tool, global scope), and a summary
|
|
73
|
+
* of what it chose. `--interactive` asks each step in a terminal; `--yes` is
|
|
74
|
+
* accepted and changes nothing.
|
|
73
75
|
*
|
|
74
76
|
* @param program - The commander program to add the command to.
|
|
75
77
|
* @param overrides - Dependencies to replace, typically in tests.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASzC,OAAO,EAkBL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,KAAK,WAAW,EAIjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAA4B,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAErF,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAML,KAAK,YAAY,EACjB,KAAK,cAAc,EACpB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAiBL,KAAK,UAAU,EAKhB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/init.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASzC,OAAO,EAkBL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,KAAK,WAAW,EAIjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAA4B,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAErF,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAML,KAAK,YAAY,EACjB,KAAK,cAAc,EACpB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAiBL,KAAK,UAAU,EAKhB,MAAM,sBAAsB,CAAC;AAyB9B,oGAAoG;AACpG,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAC7C,eAAO,MAAM,oBAAoB,6BAA8C,CAAC;AAKhF,8DAA8D;AAC9D,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,wDAAwD;IACxD,WAAW,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC;IACnF,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAClC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,iEAAiE;IACjE,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,QAAQ,EAAE,MAAM,YAAY,CAAC;IAC7B,iFAAiF;IACjF,WAAW,EAAE,MAAM,WAAW,CAAC;IAC/B,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,aAAa,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,YAAY,KAAK,SAAS,CAAC;IACpE;;;OAGG;IACH,UAAU,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,iFAAiF;IACjF,cAAc,EAAE,YAAY,CAAC;IAC7B,2EAA2E;IAC3E,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACtD,6EAA6E;IAC7E,cAAc,EAAE,MAAM,cAAc,CAAC;IACrC,4CAA4C;IAC5C,UAAU,EAAE,MAAM,MAAM,CAAC;IACzB,oEAAoE;IACpE,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IACjE,gDAAgD;IAChD,SAAS,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC;IAClF,sEAAsE;IACtE,aAAa,EAAE,YAAY,CAAC;IAC5B,4EAA4E;IAC5E,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACtD;AAgJD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAAG,uBAAuB,CAwB1G;AA6oDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,IAAI,CAwDN"}
|
|
@@ -10,8 +10,8 @@ import { sessionBaseName } from '../lib/mcp-session-name.js';
|
|
|
10
10
|
import { runSdk, withSdkDefaults } from '../lib/sdk-command.js';
|
|
11
11
|
import { currentEnginePlatform, ensureReleaseBinary, ensureStandaloneEngine, fetchDownload, readEngineVersion, } from '../lib/relaycast-engine-install.js';
|
|
12
12
|
import { AUTO_ACTIVATE, CORE_START_TIMEOUT_MS, PROBE_DELIVERY, PROBE_VERSION, connectCore, coreSocketPath, deliveryKeys, disableDelivery, enableDelivery, probeAssetName, probeAssetUrl, readCoreState, readDeliveryRecord, readProbeVersion, startCoreProcess, writePrivateJson, } from '../lib/probe-core.js';
|
|
13
|
-
import { shellQuote } from '../lib/agent-registration.js';
|
|
14
|
-
import { claudeFileStates, openCodeConfigDir, openCodeState, tidyClaudeSettings, tidyOpenCodePlugin, } from '../lib/init-leftovers.js';
|
|
13
|
+
import { shellQuote, shellWord } from '../lib/agent-registration.js';
|
|
14
|
+
import { claudeFileStates, openCodeConfigDir, openCodeState, piAgentDir, piState, tidyClaudeSettings, tidyOpenCodePlugin, tidyPiExtension, } from '../lib/init-leftovers.js';
|
|
15
15
|
import { persistWorkspaceSession } from '../lib/workspace-session.js';
|
|
16
16
|
import { readWorkspaceStore } from '../lib/workspace-store.js';
|
|
17
17
|
import { getProjectPaths } from '@agent-relay/config';
|
|
@@ -286,22 +286,22 @@ async function verifyWorkspace(deps, origin, key) {
|
|
|
286
286
|
}
|
|
287
287
|
if (!response.ok)
|
|
288
288
|
throw new Error(`${origin} answered ${response.status} when checking the workspace.`);
|
|
289
|
-
return
|
|
289
|
+
return workspaceInfoIn(response);
|
|
290
290
|
}
|
|
291
|
-
/** The `id` of a `GET /v1/workspace` answer (`{ok, data: {id}}`), read leniently. */
|
|
292
|
-
async function
|
|
291
|
+
/** The `id` and `name` of a `GET /v1/workspace` answer (`{ok, data: {id, name}}`), read leniently. */
|
|
292
|
+
async function workspaceInfoIn(response) {
|
|
293
|
+
const text = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
293
294
|
try {
|
|
294
295
|
const body = (await response.json());
|
|
295
|
-
|
|
296
|
-
return typeof id === 'string' && id.trim() ? id.trim() : undefined;
|
|
296
|
+
return { id: text(body?.data?.id ?? body?.id), name: text(body?.data?.name ?? body?.name) };
|
|
297
297
|
}
|
|
298
298
|
catch {
|
|
299
|
-
return
|
|
299
|
+
return {};
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
/** The workspace id a key belongs to, when its server can say; never throws. */
|
|
303
303
|
async function workspaceIdOf(deps, origin, key) {
|
|
304
|
-
return verifyWorkspace(deps, origin, key).
|
|
304
|
+
return verifyWorkspace(deps, origin, key).then((workspace) => workspace.id, () => undefined);
|
|
305
305
|
}
|
|
306
306
|
function parsePort(value) {
|
|
307
307
|
const port = Number.parseInt(value, 10);
|
|
@@ -322,14 +322,45 @@ function parseTools(value) {
|
|
|
322
322
|
throw new InvalidArgumentError(error.message);
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
|
-
/** A word as the shell reads it: quoted unless it is plainly safe to paste. */
|
|
326
|
-
function shellWord(value) {
|
|
327
|
-
return /^[A-Za-z0-9._@%+=:,/-]+$/.test(value) ? value : shellQuote(value);
|
|
328
|
-
}
|
|
329
325
|
/** The command that makes `name` the machine-wide active workspace. */
|
|
330
326
|
function switchCommand(name) {
|
|
331
327
|
return `relay workspace switch ${shellWord(name)}`;
|
|
332
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* The npm package spec a teammate's `npx` runs: the `next` dist-tag for a
|
|
331
|
+
* `-next` prerelease (or a build that is not a published version), else
|
|
332
|
+
* this exact version.
|
|
333
|
+
*/
|
|
334
|
+
function teammatePackage(version) {
|
|
335
|
+
const published = version?.trim();
|
|
336
|
+
if (!published || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(published) || published.includes('-next')) {
|
|
337
|
+
return 'agent-relay@next';
|
|
338
|
+
}
|
|
339
|
+
return `agent-relay@${published}`;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The one command a teammate runs in their project to join a hosted
|
|
343
|
+
* workspace, Relay installed or not: it saves the workspace (named as the
|
|
344
|
+
* server names it), starts the Agent Relay core and turns on message
|
|
345
|
+
* injection for their agent tools. `relay workspace join` only saves it.
|
|
346
|
+
*/
|
|
347
|
+
function joinCommand(key, version) {
|
|
348
|
+
return `npx -y ${teammatePackage(version)} init --key ${shellQuote(key)}`;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* A free name for a workspace joined by key: `name`, unless a saved
|
|
352
|
+
* workspace under it holds another key, then `name-2` and on.
|
|
353
|
+
*/
|
|
354
|
+
function freeWorkspaceName(name, key) {
|
|
355
|
+
const saved = readWorkspaceStore().workspaces;
|
|
356
|
+
const free = (candidate) => !saved[candidate] || saved[candidate]?.key === key;
|
|
357
|
+
if (free(name))
|
|
358
|
+
return name;
|
|
359
|
+
for (let n = 2;; n += 1) {
|
|
360
|
+
if (free(`${name}-${n}`))
|
|
361
|
+
return `${name}-${n}`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
333
364
|
/**
|
|
334
365
|
* Save the workspace and bind it to this project. It becomes the machine-wide
|
|
335
366
|
* active workspace only when no other one is: a machine already serving a
|
|
@@ -362,22 +393,79 @@ function globalScopeNote(setup, scope, installed) {
|
|
|
362
393
|
return (`In projects without their own workspace, globally set up agent tools use the active workspace "${setup.keptActive}". ` +
|
|
363
394
|
`To make "${setup.workspace}" the machine-wide fallback, run: ${switchCommand(setup.workspace)}`);
|
|
364
395
|
}
|
|
396
|
+
/** A workspace name part: lowercase letters, digits and single dashes. */
|
|
397
|
+
function nameSlug(value) {
|
|
398
|
+
return value
|
|
399
|
+
.toLowerCase()
|
|
400
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
401
|
+
.replace(/^-+|-+$/g, '');
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The name `relay init --hosted` gives a new workspace without `--name`:
|
|
405
|
+
* the person and the project folder (`alice-api`), with a numeric suffix
|
|
406
|
+
* when a saved workspace already has that name, so a second hosted
|
|
407
|
+
* workspace never replaces the first in the store.
|
|
408
|
+
*/
|
|
409
|
+
function defaultHostedName(projectRoot) {
|
|
410
|
+
let user = '';
|
|
411
|
+
try {
|
|
412
|
+
user = os.userInfo().username;
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// No passwd entry (some containers): the folder alone.
|
|
416
|
+
}
|
|
417
|
+
const base = [nameSlug(user), nameSlug(path.basename(projectRoot))].filter(Boolean).join('-').slice(0, 40) || 'relay';
|
|
418
|
+
const taken = new Set(Object.keys(readWorkspaceStore().workspaces));
|
|
419
|
+
if (!taken.has(base))
|
|
420
|
+
return base;
|
|
421
|
+
for (let n = 2;; n += 1) {
|
|
422
|
+
if (!taken.has(`${base}-${n}`))
|
|
423
|
+
return `${base}-${n}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
365
426
|
/**
|
|
366
427
|
* Connect to (or start) the server and save its workspace for this project.
|
|
367
428
|
* This is the original `relay init`; the agent-tool setup comes after it.
|
|
368
429
|
*/
|
|
369
430
|
async function setupServer(deps, choice) {
|
|
370
431
|
if (choice.mode === 'own') {
|
|
371
|
-
const {
|
|
372
|
-
if (!
|
|
373
|
-
throw new Error('Pass --
|
|
432
|
+
const { key } = choice;
|
|
433
|
+
if (!key)
|
|
434
|
+
throw new Error('Pass --key with --url for your own server.');
|
|
435
|
+
// `--key` alone joins the hosted service. A key already bound to a
|
|
436
|
+
// self-hosted server is never sent there: only an explicit --url moves it.
|
|
437
|
+
const url = choice.url ?? HOSTED_ORIGIN;
|
|
438
|
+
const bound = choice.url ? undefined : boundOrigin(key);
|
|
439
|
+
if (bound && new URL(bound).origin !== HOSTED_ORIGIN) {
|
|
440
|
+
throw new Error(`This workspace key belongs to ${new URL(bound).origin}, so relay init will not send it to the hosted ` +
|
|
441
|
+
`service. To use it there, run: relay init --url ${shellWord(new URL(bound).origin)} --key <key>`);
|
|
442
|
+
}
|
|
374
443
|
const origin = configuredOrigin(url, '--url');
|
|
375
444
|
if (!origin)
|
|
376
445
|
throw new Error('--url is empty.');
|
|
377
|
-
const
|
|
446
|
+
const workspace = await verifyWorkspace(deps, origin, key);
|
|
447
|
+
const workspaceId = workspace.id;
|
|
378
448
|
guardCredentialOrigin(key, url);
|
|
379
|
-
const
|
|
449
|
+
const hosted = origin === HOSTED_ORIGIN;
|
|
450
|
+
// Named as the person says, else as the server names the workspace.
|
|
451
|
+
const name = choice.name ??
|
|
452
|
+
freeWorkspaceName(workspace.name ??
|
|
453
|
+
(hosted ? defaultHostedName(deps.toolContext().projectRoot) : new URL(origin).hostname), key);
|
|
380
454
|
const { lines, ...saved } = saveWorkspace(name, key);
|
|
455
|
+
// A teammate joining a hosted workspace by key: the hosted service, not
|
|
456
|
+
// a server of their own. It stays an `own` setup: init did not create
|
|
457
|
+
// the workspace, so its name never stands in for a workspace id when
|
|
458
|
+
// checking which workspace the core serves (`sameWorkspace`).
|
|
459
|
+
if (hosted) {
|
|
460
|
+
return {
|
|
461
|
+
...saved,
|
|
462
|
+
workspaceId,
|
|
463
|
+
mode: 'own',
|
|
464
|
+
origin,
|
|
465
|
+
headline: `Joined hosted workspace "${name}" (${origin}).`,
|
|
466
|
+
details: ['Your key is only ever sent to the hosted service.', ...lines],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
381
469
|
return {
|
|
382
470
|
...saved,
|
|
383
471
|
workspaceId,
|
|
@@ -388,7 +476,7 @@ async function setupServer(deps, choice) {
|
|
|
388
476
|
};
|
|
389
477
|
}
|
|
390
478
|
if (choice.mode === 'hosted') {
|
|
391
|
-
const name = choice.name ??
|
|
479
|
+
const name = choice.name ?? defaultHostedName(deps.toolContext().projectRoot);
|
|
392
480
|
const relay = await deps.createWorkspace(name, HOSTED_ORIGIN);
|
|
393
481
|
if (!relay.workspaceKey)
|
|
394
482
|
throw new Error('The hosted service did not return a workspace key.');
|
|
@@ -400,9 +488,11 @@ async function setupServer(deps, choice) {
|
|
|
400
488
|
origin: HOSTED_ORIGIN,
|
|
401
489
|
headline: `Created hosted workspace "${name}".`,
|
|
402
490
|
details: [
|
|
491
|
+
...(choice.name ? [] : [`Named after you and this folder; pass --name to choose another.`]),
|
|
403
492
|
...lines,
|
|
404
|
-
'On another machine, join it
|
|
405
|
-
|
|
493
|
+
'On another machine, run this in the project to join it and connect its agent tools ' +
|
|
494
|
+
'(treat this key like a password):',
|
|
495
|
+
` ${joinCommand(relay.workspaceKey, deps.cliVersion)}`,
|
|
406
496
|
],
|
|
407
497
|
};
|
|
408
498
|
}
|
|
@@ -416,7 +506,7 @@ async function setupServer(deps, choice) {
|
|
|
416
506
|
// Only a key the server refuses is replaced. A timeout or server
|
|
417
507
|
// error is reported instead, so a hiccup never swaps the workspace
|
|
418
508
|
// (and its agents and messages) for a new one.
|
|
419
|
-
workspaceId = await verifyWorkspace(deps, origin, workspaceKey).
|
|
509
|
+
workspaceId = await verifyWorkspace(deps, origin, workspaceKey).then((workspace) => workspace.id, (error) => {
|
|
420
510
|
if (!(error instanceof WorkspaceKeyRejectedError))
|
|
421
511
|
throw error;
|
|
422
512
|
workspaceKey = undefined;
|
|
@@ -726,10 +816,12 @@ async function setupInjection(deps, tools, setup) {
|
|
|
726
816
|
// included, instead of the hold the core writes on each login.
|
|
727
817
|
const before = tools.includes('claude') ? claudeInboundValues(ctx) : {};
|
|
728
818
|
// The files the core is about to edit, as they are now: what --uninstall
|
|
729
|
-
// tidies back to (a settings.json or
|
|
819
|
+
// tidies back to (a settings.json, plugins or extensions folder, or off
|
|
820
|
+
// marker init created).
|
|
730
821
|
const files = {
|
|
731
822
|
claudeFiles: tools.includes('claude') ? claudeFileStates(claudeLoginSettings(ctx)) : undefined,
|
|
732
823
|
openCode: tools.includes('opencode') ? openCodeState(openCodeConfigDir(ctx.env, ctx.home)) : undefined,
|
|
824
|
+
pi: tools.includes('pi') ? piState(piAgentDir(ctx.env, ctx.home), ctx.home) : undefined,
|
|
733
825
|
};
|
|
734
826
|
const { results, turnedOn, turnedOff } = await enableDelivery(session.client, tools, record.turnedOn);
|
|
735
827
|
// Applied to the record as it is now, not as it was read: another
|
|
@@ -756,6 +848,7 @@ async function setupInjection(deps, tools, setup) {
|
|
|
756
848
|
openCode: touched.includes('opencode_plugin')
|
|
757
849
|
? (current.openCode ?? files.openCode)
|
|
758
850
|
: current.openCode,
|
|
851
|
+
pi: touched.includes('pi_extension') ? (current.pi ?? files.pi) : current.pi,
|
|
759
852
|
});
|
|
760
853
|
});
|
|
761
854
|
try {
|
|
@@ -787,6 +880,31 @@ async function setupInjection(deps, tools, setup) {
|
|
|
787
880
|
session.client.close();
|
|
788
881
|
}
|
|
789
882
|
}
|
|
883
|
+
/** The bytes of the regular file `file` is or links to; undefined when there is none. */
|
|
884
|
+
function readIfFile(file) {
|
|
885
|
+
let fd;
|
|
886
|
+
try {
|
|
887
|
+
// One descriptor for the check and the read, so they see the same file.
|
|
888
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
889
|
+
return fs.fstatSync(fd).isFile() ? fs.readFileSync(fd) : undefined;
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
finally {
|
|
895
|
+
if (fd !== undefined)
|
|
896
|
+
fs.closeSync(fd);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/** Puts `bytes` back in `file`; a failure leaves the core's marker, which means the same. */
|
|
900
|
+
function restoreBytes(file, bytes) {
|
|
901
|
+
try {
|
|
902
|
+
fs.writeFileSync(file, bytes);
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
// The core's own marker stays: Pi is still off.
|
|
906
|
+
}
|
|
907
|
+
}
|
|
790
908
|
/** Settings init turned on that are neither recorded nor turned off: setup stops. */
|
|
791
909
|
class InjectionStuckError extends Error {
|
|
792
910
|
}
|
|
@@ -809,6 +927,9 @@ async function undoUnrecorded(client, turnedOn, before, files, cause) {
|
|
|
809
927
|
if (turnedOn.includes('opencode_plugin') && !failed('opencode_plugin') && files.openCode) {
|
|
810
928
|
tidyOpenCodePlugin(files.openCode);
|
|
811
929
|
}
|
|
930
|
+
if (turnedOn.includes('pi_extension') && !failed('pi_extension') && files.pi) {
|
|
931
|
+
tidyPiExtension(files.pi);
|
|
932
|
+
}
|
|
812
933
|
if (failures.length > 0) {
|
|
813
934
|
throw new InjectionStuckError(`Could not save ${deliveryRecordPath()} (${cause.message}), and these settings relay init turned on ` +
|
|
814
935
|
`could not be turned off again: ${failures.join('; ')}. Turn them off with ` +
|
|
@@ -1000,13 +1121,14 @@ const CLAUDE_FILE_KEYS = ['direct_delivery', 'claude_session_hook'];
|
|
|
1000
1121
|
/**
|
|
1001
1122
|
* Once nothing init turned on edits a tool's files any more, tidies them
|
|
1002
1123
|
* back to how init found them (a Claude settings.json init created, the
|
|
1003
|
-
* OpenCode plugin's off
|
|
1124
|
+
* OpenCode plugin's and Pi extension's off markers) and drops their
|
|
1125
|
+
* recorded state. Run after
|
|
1004
1126
|
* the core turned the settings off and the original crossSessionInbound is
|
|
1005
1127
|
* back.
|
|
1006
1128
|
* @returns The record without the states it used
|
|
1007
1129
|
*/
|
|
1008
1130
|
function tidyLeftovers(record) {
|
|
1009
|
-
let { claudeFiles, openCode } = record;
|
|
1131
|
+
let { claudeFiles, openCode, pi } = record;
|
|
1010
1132
|
if (claudeFiles && !record.turnedOn.some((key) => CLAUDE_FILE_KEYS.includes(key))) {
|
|
1011
1133
|
tidyClaudeSettings(claudeFiles);
|
|
1012
1134
|
claudeFiles = undefined;
|
|
@@ -1015,11 +1137,16 @@ function tidyLeftovers(record) {
|
|
|
1015
1137
|
tidyOpenCodePlugin(openCode);
|
|
1016
1138
|
openCode = undefined;
|
|
1017
1139
|
}
|
|
1140
|
+
if (pi && !record.turnedOn.includes('pi_extension')) {
|
|
1141
|
+
tidyPiExtension(pi);
|
|
1142
|
+
pi = undefined;
|
|
1143
|
+
}
|
|
1018
1144
|
return {
|
|
1019
1145
|
turnedOn: record.turnedOn,
|
|
1020
1146
|
claudeInbound: record.claudeInbound,
|
|
1021
1147
|
...(claudeFiles ? { claudeFiles } : {}),
|
|
1022
1148
|
...(openCode ? { openCode } : {}),
|
|
1149
|
+
...(pi ? { pi } : {}),
|
|
1023
1150
|
};
|
|
1024
1151
|
}
|
|
1025
1152
|
/**
|
|
@@ -1040,6 +1167,7 @@ function writeDeliveryRecord(record) {
|
|
|
1040
1167
|
...(Object.keys(record.claudeInbound).length > 0 ? { claudeInbound: record.claudeInbound } : {}),
|
|
1041
1168
|
...(record.claudeFiles ? { claudeFiles: record.claudeFiles } : {}),
|
|
1042
1169
|
...(record.openCode ? { openCode: record.openCode } : {}),
|
|
1170
|
+
...(record.pi ? { pi: record.pi } : {}),
|
|
1043
1171
|
});
|
|
1044
1172
|
}
|
|
1045
1173
|
/**
|
|
@@ -1070,7 +1198,17 @@ async function turnOffInjection(deps, keys) {
|
|
|
1070
1198
|
throw new Error(`Could not reach the Agent Relay core to turn off message injection: ${session.reason}`);
|
|
1071
1199
|
}
|
|
1072
1200
|
try {
|
|
1201
|
+
// A Pi off marker already there was the person's own opt-out after init
|
|
1202
|
+
// (the desktop app's switch), not this run's: it is kept, byte for byte,
|
|
1203
|
+
// though turning pi_extension off rewrites it.
|
|
1204
|
+
const pi = readDeliveryRecord(deliveryRecordPath()).pi;
|
|
1205
|
+
const piOptedOut = keys.includes('pi_extension') &&
|
|
1206
|
+
pi !== undefined &&
|
|
1207
|
+
fs.lstatSync(pi.marker, { throwIfNoEntry: false }) !== undefined;
|
|
1208
|
+
const piMarker = piOptedOut && pi ? readIfFile(pi.marker) : undefined;
|
|
1073
1209
|
const failures = await disableDelivery(session.client, keys);
|
|
1210
|
+
if (pi && piMarker)
|
|
1211
|
+
restoreBytes(pi.marker, piMarker);
|
|
1074
1212
|
const done = keys.filter((key) => !failures.some((failure) => failure.startsWith(`${key}:`)));
|
|
1075
1213
|
const ctx = deps.toolContext();
|
|
1076
1214
|
updateDeliveryRecord((current) => {
|
|
@@ -1078,6 +1216,8 @@ async function turnOffInjection(deps, keys) {
|
|
|
1078
1216
|
restoreClaudeInbound(recordedClaudeFiles(current, ctx));
|
|
1079
1217
|
return tidyLeftovers({
|
|
1080
1218
|
...current,
|
|
1219
|
+
// Kept as if it had been there before init; an extensions folder init created still goes.
|
|
1220
|
+
...(piOptedOut && current.pi ? { pi: { ...current.pi, off: true } } : {}),
|
|
1081
1221
|
turnedOn: current.turnedOn.filter((key) => !done.includes(key)),
|
|
1082
1222
|
claudeInbound: done.includes('direct_delivery') ? {} : current.claudeInbound,
|
|
1083
1223
|
});
|
|
@@ -1132,11 +1272,12 @@ async function stopCore(deps, quiet = false) {
|
|
|
1132
1272
|
fs.rmSync(coreStatePath(), { force: true });
|
|
1133
1273
|
}
|
|
1134
1274
|
function serverChoiceFromFlags(o) {
|
|
1135
|
-
if (
|
|
1136
|
-
throw new Error('Pass --
|
|
1275
|
+
if (o.url && !o.key)
|
|
1276
|
+
throw new Error('Pass --key with --url for your own server.');
|
|
1137
1277
|
if (o.url && o.hosted)
|
|
1138
1278
|
throw new Error('Choose either --hosted or --url/--key.');
|
|
1139
|
-
|
|
1279
|
+
// A key joins its workspace: on --url's server, else on the hosted service.
|
|
1280
|
+
if (o.key)
|
|
1140
1281
|
return { mode: 'own', url: o.url, key: o.key, name: o.name, port: o.port };
|
|
1141
1282
|
if (o.hosted)
|
|
1142
1283
|
return { mode: 'hosted', name: o.name, port: o.port };
|
|
@@ -1231,7 +1372,7 @@ async function runPlain(deps, o) {
|
|
|
1231
1372
|
deps.log(line);
|
|
1232
1373
|
const ctx = deps.toolContext();
|
|
1233
1374
|
const tools = o.tools ?? detectTools(ctx).map((tool) => tool.id);
|
|
1234
|
-
const scope = o.scope ?? '
|
|
1375
|
+
const scope = o.scope ?? 'global';
|
|
1235
1376
|
const plan = await planDelivery(deps, tools, o.delivery ?? 'inject', setup, ctx);
|
|
1236
1377
|
if (plan.injection) {
|
|
1237
1378
|
deps.log('');
|
|
@@ -1279,6 +1420,28 @@ async function runPlain(deps, o) {
|
|
|
1279
1420
|
deps.log(` ${line}`);
|
|
1280
1421
|
for (const line of CLI_FALLBACK)
|
|
1281
1422
|
deps.log(` ${line}`);
|
|
1423
|
+
deps.log('');
|
|
1424
|
+
for (const line of choicesSummary(o, setup, tools, scope))
|
|
1425
|
+
deps.log(line);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* What a `relay init` that asked nothing chose, each with the flag that
|
|
1429
|
+
* changes it, so the defaults are never a surprise.
|
|
1430
|
+
*/
|
|
1431
|
+
function choicesSummary(o, setup, tools, scope) {
|
|
1432
|
+
const where = setup.mode === 'local'
|
|
1433
|
+
? 'this machine (--hosted for agents on other machines, or --url and --key for your own server)'
|
|
1434
|
+
: setup.mode === 'hosted' && !o.url
|
|
1435
|
+
? `hosted workspace "${setup.workspace}"${o.name ? '' : ' (--name to name it)'}`
|
|
1436
|
+
: `workspace "${setup.workspace}" on ${setup.origin}`;
|
|
1437
|
+
const toolList = tools.length > 0 ? tools.join(', ') : 'none';
|
|
1438
|
+
return [
|
|
1439
|
+
'Chosen without asking:',
|
|
1440
|
+
` Server: ${where}`,
|
|
1441
|
+
` Tools: ${toolList}${o.tools ? '' : ' (the ones installed here; --tools to choose)'}`,
|
|
1442
|
+
` Scope: ${scope}${o.scope ? '' : ' (--scope project for this project only)'}`,
|
|
1443
|
+
'To choose each step yourself, run: relay init --interactive',
|
|
1444
|
+
];
|
|
1282
1445
|
}
|
|
1283
1446
|
/**
|
|
1284
1447
|
* Codex reads a project's .codex/config.toml only once that project is
|
|
@@ -1425,11 +1588,10 @@ async function runGuided(deps, o) {
|
|
|
1425
1588
|
p.note(summary.join('\n'), 'Installation Summary');
|
|
1426
1589
|
if (delivery === 'mcp' && launch.warning)
|
|
1427
1590
|
p.warn(launch.warning);
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
}
|
|
1591
|
+
// `--interactive` asks to the end; `--yes` changes nothing here either.
|
|
1592
|
+
const proceed = await p.confirm({ message: 'Proceed with installation?', initialValue: true });
|
|
1593
|
+
if (proceed === CANCELLED || !proceed)
|
|
1594
|
+
return cancelled();
|
|
1433
1595
|
const injecting = p.spinner();
|
|
1434
1596
|
if (delivery === 'inject')
|
|
1435
1597
|
injecting.start('Setting up message injection');
|
|
@@ -1483,8 +1645,10 @@ function parseDelivery(value) {
|
|
|
1483
1645
|
* machine by default, the hosted service, or your own server), then connect
|
|
1484
1646
|
* your agent tools. By default the Agent Relay core injects messages into
|
|
1485
1647
|
* their running sessions; `--delivery mcp` (or a machine the core cannot run
|
|
1486
|
-
* on) uses the Relay MCP server instead.
|
|
1487
|
-
*
|
|
1648
|
+
* on) uses the Relay MCP server instead. It asks nothing: flags, then
|
|
1649
|
+
* defaults (this machine, every installed tool, global scope), and a summary
|
|
1650
|
+
* of what it chose. `--interactive` asks each step in a terminal; `--yes` is
|
|
1651
|
+
* accepted and changes nothing.
|
|
1488
1652
|
*
|
|
1489
1653
|
* @param program - The commander program to add the command to.
|
|
1490
1654
|
* @param overrides - Dependencies to replace, typically in tests.
|
|
@@ -1493,19 +1657,21 @@ export function registerInitCommand(program, overrides = {}) {
|
|
|
1493
1657
|
const deps = withInitDefaults(overrides);
|
|
1494
1658
|
program
|
|
1495
1659
|
.command('init')
|
|
1496
|
-
.description('Set up Relay and connect your agent tools: a private server on this machine (default),
|
|
1497
|
-
'service (--hosted), or your own server (--url and --key)
|
|
1498
|
-
'into running Claude Code, Codex, OpenCode, Grok, Pi and Devin
|
|
1660
|
+
.description('Set up Relay and connect your agent tools without asking: a private server on this machine (default), ' +
|
|
1661
|
+
'the hosted service (--hosted), or your own server (--url and --key), every installed agent tool, and ' +
|
|
1662
|
+
'the Agent Relay core injecting messages into running Claude Code, Codex, OpenCode, Grok, Pi and Devin ' +
|
|
1663
|
+
'sessions. --interactive asks each step instead')
|
|
1499
1664
|
.option('--hosted', 'Use the hosted service so agents on other machines can join')
|
|
1500
|
-
.option('--url <url>', 'Your own Relaycast server')
|
|
1501
|
-
.option('--key <key>', '
|
|
1502
|
-
.option('--name <name>', 'Workspace name to save this as')
|
|
1665
|
+
.option('--url <url>', 'Your own Relaycast server (with --key)')
|
|
1666
|
+
.option('--key <key>', 'Join the workspace this key belongs to: hosted, or on --url')
|
|
1667
|
+
.option('--name <name>', 'Workspace name to save this as (default: the one its server names)')
|
|
1503
1668
|
.option('--port <port>', 'Port for the local server', parsePort, DEFAULT_LOCAL_PORT)
|
|
1504
1669
|
.option('--tools <tools>', `Agent tools to connect: ${TOOL_IDS.join(',')}, all, or none (default: the ones installed here)`, parseTools)
|
|
1505
1670
|
.option('--delivery <delivery>', 'inject (default): the Agent Relay core types messages into running sessions; mcp: the Relay MCP ' +
|
|
1506
1671
|
'server, whose inbox agents check', parseDelivery)
|
|
1507
|
-
.option('--scope <scope>', 'Where the Relay MCP server goes:
|
|
1508
|
-
.option('-
|
|
1672
|
+
.option('--scope <scope>', 'Where the Relay MCP server goes: global (default) or project (injection is always per user)', parseScope)
|
|
1673
|
+
.option('-i, --interactive', 'Ask where agents talk, which agent tools to connect and where the MCP server goes')
|
|
1674
|
+
.option('-y, --yes', 'Accepted for older scripts: relay init asks nothing unless --interactive')
|
|
1509
1675
|
.option('--uninstall', 'Remove the Relay MCP entries relay init added (both scopes unless --scope) and turn off the injection it turned on')
|
|
1510
1676
|
.option('--stop', 'Stop the local server and the Agent Relay core started by relay init')
|
|
1511
1677
|
.action(async (o) => {
|
|
@@ -1514,8 +1680,12 @@ export function registerInitCommand(program, overrides = {}) {
|
|
|
1514
1680
|
return stopLocalServer(deps);
|
|
1515
1681
|
if (o.uninstall)
|
|
1516
1682
|
return uninstall(deps, o);
|
|
1517
|
-
if (
|
|
1683
|
+
if (o.interactive) {
|
|
1684
|
+
if (!deps.isInteractive()) {
|
|
1685
|
+
throw new Error('relay init --interactive needs a terminal; run relay init without it to use the defaults.');
|
|
1686
|
+
}
|
|
1518
1687
|
return runGuided(deps, o);
|
|
1688
|
+
}
|
|
1519
1689
|
return runPlain(deps, o);
|
|
1520
1690
|
});
|
|
1521
1691
|
});
|