@lore-co/cli 0.1.3 → 0.1.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/README.md +87 -24
- package/dist/cli.d.ts +11 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +279 -78
- package/dist/cli.js.map +1 -1
- package/dist/demo.d.ts +9 -2
- package/dist/demo.d.ts.map +1 -1
- package/dist/demo.js +64 -22
- package/dist/demo.js.map +1 -1
- package/dist/generated-assets.d.ts +4 -3
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +4 -3
- package/dist/generated-assets.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +7 -5
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -8
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +6 -0
- package/dist/self-host.d.ts.map +1 -0
- package/dist/self-host.js +925 -0
- package/dist/self-host.js.map +1 -0
- package/dist/update.d.ts +21 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +277 -53
- package/dist/update.js.map +1 -1
- package/package.json +10 -4
package/dist/cli.js
CHANGED
|
@@ -5,14 +5,18 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { delimiter, dirname, resolve } from "node:path";
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { WorkspaceIdentityResponseSchema, } from "@lore-co/core";
|
|
8
9
|
import { runHook, } from "./runtime.js";
|
|
9
10
|
import { connectGithub, runGithubCommand } from "./github.js";
|
|
10
11
|
import { runDevinCommand } from "./devin.js";
|
|
11
12
|
import { runHostCommand } from "./host.js";
|
|
12
13
|
import { runDemoCommand } from "./demo.js";
|
|
13
14
|
import { updateCommand } from "./update.js";
|
|
15
|
+
import { runSelfHostCommand } from "./self-host.js";
|
|
14
16
|
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
15
17
|
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
18
|
+
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
19
|
+
const CONFIGURED_AGENT_NAMES = ["claude", "codex", "opencode"];
|
|
16
20
|
const HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionEnd"];
|
|
17
21
|
const ROOT_HELP = `lore
|
|
18
22
|
Connect local coding agents to Lore shared engineering memory.
|
|
@@ -21,26 +25,28 @@ Usage:
|
|
|
21
25
|
lore <command> [options]
|
|
22
26
|
|
|
23
27
|
Commands:
|
|
24
|
-
connect Configure Lore and install native agent
|
|
28
|
+
connect Configure Lore and install native agent integrations
|
|
25
29
|
github Prepare/post GitHub reviews and observe corrections
|
|
26
30
|
devin Start and manage Lore-enabled Devin sessions
|
|
27
31
|
host Call auditable APIs from external agent hosts
|
|
28
|
-
status Show connector and
|
|
29
|
-
doctor Diagnose configuration,
|
|
32
|
+
status Show connector and agent integration state
|
|
33
|
+
doctor Diagnose configuration, integrations, and API reachability
|
|
34
|
+
self-host Run a version-pinned local Docker Compose deployment
|
|
30
35
|
demo Prove a file-scoped Claude-to-Codex handoff
|
|
31
36
|
update Install the latest Lore CLI binary
|
|
32
|
-
disconnect Remove Lore-owned
|
|
37
|
+
disconnect Remove Lore-owned integrations and local credentials
|
|
33
38
|
hook Internal native hook handler
|
|
34
39
|
|
|
35
40
|
Discover:
|
|
36
41
|
lore connect --help
|
|
37
42
|
lore status --help
|
|
38
43
|
lore doctor --help
|
|
44
|
+
lore self-host --help
|
|
39
45
|
lore disconnect --help
|
|
40
46
|
lore host --help
|
|
41
47
|
|
|
42
48
|
Examples:
|
|
43
|
-
lore connect --url https://lore.example.com --token "$
|
|
49
|
+
lore connect --url https://lore.example.com --token "$LORE_WORKSPACE_TOKEN"
|
|
44
50
|
lore connect github --repo owner/repository
|
|
45
51
|
lore status --json
|
|
46
52
|
lore doctor
|
|
@@ -49,7 +55,7 @@ Examples:
|
|
|
49
55
|
lore devin --help
|
|
50
56
|
`;
|
|
51
57
|
const CONNECT_HELP = `lore connect
|
|
52
|
-
Store a workspace credential and idempotently install
|
|
58
|
+
Store a workspace credential and idempotently install agent integrations.
|
|
53
59
|
|
|
54
60
|
Usage:
|
|
55
61
|
lore connect --url <url> --token <token> [options]
|
|
@@ -57,18 +63,18 @@ Usage:
|
|
|
57
63
|
Options:
|
|
58
64
|
--url <url> Lore API base URL (or LORE_API_URL)
|
|
59
65
|
--dashboard-url <url> Lore dashboard URL used for receipt links
|
|
60
|
-
--token <token> Workspace bearer token (or LORE_TOKEN)
|
|
61
|
-
--agent <name> codex or
|
|
66
|
+
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
67
|
+
--agent <name> claude, codex, or opencode; repeat to override auto-detection
|
|
62
68
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
63
69
|
--json Print machine-readable output
|
|
64
70
|
--help Show this command's help
|
|
65
71
|
|
|
66
72
|
Examples:
|
|
67
|
-
lore connect --url https://lore.example.com --token "$
|
|
73
|
+
lore connect --url https://lore.example.com --token "$LORE_WORKSPACE_TOKEN"
|
|
68
74
|
lore connect --url http://localhost:3004 --token dev-token --agent codex
|
|
69
75
|
`;
|
|
70
76
|
const STATUS_HELP = `lore status
|
|
71
|
-
Show whether Lore is configured and
|
|
77
|
+
Show whether Lore is configured and each native integration is installed.
|
|
72
78
|
|
|
73
79
|
Usage:
|
|
74
80
|
lore status [--json]
|
|
@@ -78,7 +84,7 @@ Examples:
|
|
|
78
84
|
lore status --json
|
|
79
85
|
`;
|
|
80
86
|
const DOCTOR_HELP = `lore doctor
|
|
81
|
-
Check local security, runtime,
|
|
87
|
+
Check local security, runtime, agent integrations, binaries, and Lore health.
|
|
82
88
|
|
|
83
89
|
Usage:
|
|
84
90
|
lore doctor [--json]
|
|
@@ -88,8 +94,8 @@ Examples:
|
|
|
88
94
|
lore doctor --json
|
|
89
95
|
`;
|
|
90
96
|
const DISCONNECT_HELP = `lore disconnect
|
|
91
|
-
Remove only Lore-owned
|
|
92
|
-
Unrelated
|
|
97
|
+
Remove only Lore-owned hooks/plugin, credentials, runtime, state, and retry queue.
|
|
98
|
+
Unrelated agent settings, plugins, and Lore-created backups are retained.
|
|
93
99
|
|
|
94
100
|
Usage:
|
|
95
101
|
lore disconnect [--json]
|
|
@@ -118,11 +124,18 @@ export function getLorePaths(home) {
|
|
|
118
124
|
queue: resolve(loreDirectory, "queue"),
|
|
119
125
|
codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
|
|
120
126
|
claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
|
|
127
|
+
openCodeConfig: resolve(resolvedHome, ".config", "opencode", "opencode.json"),
|
|
121
128
|
};
|
|
122
129
|
}
|
|
123
130
|
function shellQuote(value) {
|
|
124
131
|
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
125
132
|
}
|
|
133
|
+
function isConfiguredAgent(value) {
|
|
134
|
+
return (value === "claude" || value === "codex" || value === "opencode");
|
|
135
|
+
}
|
|
136
|
+
function isCommandHookAgent(agent) {
|
|
137
|
+
return agent === "claude" || agent === "codex";
|
|
138
|
+
}
|
|
126
139
|
function hookCommand(agent, paths) {
|
|
127
140
|
if (IS_STANDALONE_BINARY) {
|
|
128
141
|
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
@@ -227,6 +240,41 @@ export function countLoreHooks(input) {
|
|
|
227
240
|
}
|
|
228
241
|
return count;
|
|
229
242
|
}
|
|
243
|
+
function isLoreOpenCodePlugin(value) {
|
|
244
|
+
return (typeof value === "string" &&
|
|
245
|
+
/^@lore-co\/(?:opencode|opencode-plugin)(?:@[^\s]+)?$/u.test(value));
|
|
246
|
+
}
|
|
247
|
+
export function mergeLoreOpenCodePlugin(input) {
|
|
248
|
+
const result = cloneObject(input);
|
|
249
|
+
if (result.plugin !== undefined && !Array.isArray(result.plugin)) {
|
|
250
|
+
throw new Error('OpenCode configuration field "plugin" must be a JSON array');
|
|
251
|
+
}
|
|
252
|
+
const plugins = Array.isArray(result.plugin) ? result.plugin : [];
|
|
253
|
+
result.plugin = [
|
|
254
|
+
...plugins.filter((plugin) => !isLoreOpenCodePlugin(plugin)),
|
|
255
|
+
LORE_OPENCODE_PLUGIN,
|
|
256
|
+
];
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
export function removeLoreOpenCodePlugin(input) {
|
|
260
|
+
const result = cloneObject(input);
|
|
261
|
+
if (!Array.isArray(result.plugin)) {
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
const plugins = result.plugin.filter((plugin) => !isLoreOpenCodePlugin(plugin));
|
|
265
|
+
if (plugins.length === 0) {
|
|
266
|
+
delete result.plugin;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
result.plugin = plugins;
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
export function countLoreOpenCodePlugins(input) {
|
|
274
|
+
return Array.isArray(input.plugin)
|
|
275
|
+
? input.plugin.filter(isLoreOpenCodePlugin).length
|
|
276
|
+
: 0;
|
|
277
|
+
}
|
|
230
278
|
async function readJsonDocument(path) {
|
|
231
279
|
try {
|
|
232
280
|
const [raw, metadata] = await Promise.all([
|
|
@@ -296,7 +344,7 @@ function parseConnectorConfig(value) {
|
|
|
296
344
|
typeof value.connectedAt !== "string") {
|
|
297
345
|
return null;
|
|
298
346
|
}
|
|
299
|
-
const agents = value.agents.filter((agent) => agent
|
|
347
|
+
const agents = value.agents.filter((agent) => isConfiguredAgent(agent));
|
|
300
348
|
const timeoutMs = typeof value.timeoutMs === "number" &&
|
|
301
349
|
Number.isInteger(value.timeoutMs) &&
|
|
302
350
|
value.timeoutMs >= 250 &&
|
|
@@ -330,7 +378,7 @@ function normalizeApiUrl(value) {
|
|
|
330
378
|
parsed = new URL(value);
|
|
331
379
|
}
|
|
332
380
|
catch {
|
|
333
|
-
throw new Error(
|
|
381
|
+
throw new Error("Lore API URL is invalid");
|
|
334
382
|
}
|
|
335
383
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
336
384
|
throw new Error("Lore API URL must use http or https");
|
|
@@ -338,10 +386,79 @@ function normalizeApiUrl(value) {
|
|
|
338
386
|
if (parsed.username !== "" || parsed.password !== "") {
|
|
339
387
|
throw new Error("Lore API URL must not contain credentials");
|
|
340
388
|
}
|
|
341
|
-
parsed.hash
|
|
342
|
-
|
|
389
|
+
if (parsed.hash !== "" || parsed.search !== "") {
|
|
390
|
+
throw new Error("Lore API URL must not contain a query or fragment");
|
|
391
|
+
}
|
|
343
392
|
return parsed.href.replace(/\/+$/u, "");
|
|
344
393
|
}
|
|
394
|
+
function configuredToken(explicit, existing) {
|
|
395
|
+
if (explicit !== undefined) {
|
|
396
|
+
return explicit;
|
|
397
|
+
}
|
|
398
|
+
const workspaceToken = process.env.LORE_WORKSPACE_TOKEN?.trim();
|
|
399
|
+
const legacyToken = process.env.LORE_TOKEN?.trim();
|
|
400
|
+
if (workspaceToken !== undefined &&
|
|
401
|
+
workspaceToken !== "" &&
|
|
402
|
+
legacyToken !== undefined &&
|
|
403
|
+
legacyToken !== "" &&
|
|
404
|
+
workspaceToken !== legacyToken) {
|
|
405
|
+
throw new Error("LORE_WORKSPACE_TOKEN and LORE_TOKEN disagree. Set only one credential.");
|
|
406
|
+
}
|
|
407
|
+
return workspaceToken || legacyToken || existing?.token;
|
|
408
|
+
}
|
|
409
|
+
function semverCompatibility(version) {
|
|
410
|
+
const match = /^(\d+)\.(\d+)\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(version);
|
|
411
|
+
if (match?.[1] === undefined || match[2] === undefined) {
|
|
412
|
+
throw new Error("Invalid semantic version");
|
|
413
|
+
}
|
|
414
|
+
return { major: Number(match[1]), minor: Number(match[2]) };
|
|
415
|
+
}
|
|
416
|
+
function isCompatibleServerVersion(serverVersion) {
|
|
417
|
+
const cli = semverCompatibility(LORE_VERSION);
|
|
418
|
+
const server = semverCompatibility(serverVersion);
|
|
419
|
+
return (cli.major === server.major &&
|
|
420
|
+
(cli.major !== 0 || cli.minor === server.minor));
|
|
421
|
+
}
|
|
422
|
+
async function authenticatedIdentity(apiUrl, token, timeoutMs) {
|
|
423
|
+
const url = `${apiUrl}/v1/workspace/identity`;
|
|
424
|
+
let response;
|
|
425
|
+
try {
|
|
426
|
+
response = await fetch(url, {
|
|
427
|
+
headers: {
|
|
428
|
+
accept: "application/json",
|
|
429
|
+
authorization: `Bearer ${token}`,
|
|
430
|
+
},
|
|
431
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
if (error instanceof Error &&
|
|
436
|
+
(error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
437
|
+
throw new Error(`Lore preflight timed out after ${timeoutMs}ms. Verify the URL and server availability.`);
|
|
438
|
+
}
|
|
439
|
+
throw new Error("Lore API is unreachable. Verify --url and the server's network availability.");
|
|
440
|
+
}
|
|
441
|
+
if (response.status === 401 || response.status === 403) {
|
|
442
|
+
throw new Error("Lore authentication failed. Verify the workspace token is active and belongs to this server.");
|
|
443
|
+
}
|
|
444
|
+
if (!response.ok) {
|
|
445
|
+
throw new Error(response.status === 404
|
|
446
|
+
? "Lore server is incompatible with this CLI. Upgrade the server before connecting."
|
|
447
|
+
: `Lore connector preflight failed with HTTP ${response.status}. Check server readiness and retry.`);
|
|
448
|
+
}
|
|
449
|
+
let body;
|
|
450
|
+
try {
|
|
451
|
+
body = JSON.parse(await response.text());
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
throw new Error("Lore server returned an incompatible identity response. Upgrade the server and CLI.");
|
|
455
|
+
}
|
|
456
|
+
const parsed = WorkspaceIdentityResponseSchema.safeParse(body);
|
|
457
|
+
if (!parsed.success || !isCompatibleServerVersion(parsed.data.server.version)) {
|
|
458
|
+
throw new Error("Lore server is incompatible with this CLI. Upgrade the server and CLI to compatible versions.");
|
|
459
|
+
}
|
|
460
|
+
return parsed.data;
|
|
461
|
+
}
|
|
345
462
|
async function isExecutable(path) {
|
|
346
463
|
try {
|
|
347
464
|
await access(path, fsConstants.X_OK);
|
|
@@ -363,14 +480,28 @@ async function commandExists(command) {
|
|
|
363
480
|
}
|
|
364
481
|
return false;
|
|
365
482
|
}
|
|
366
|
-
async function
|
|
367
|
-
|
|
483
|
+
async function pathExists(path) {
|
|
484
|
+
try {
|
|
485
|
+
await access(path, fsConstants.F_OK);
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function detectAgents(paths) {
|
|
493
|
+
const [codex, claude, openCodeExecutable, openCodeConfig] = await Promise.all([
|
|
368
494
|
commandExists("codex"),
|
|
369
495
|
commandExists("claude"),
|
|
496
|
+
commandExists("opencode"),
|
|
497
|
+
pathExists(paths.openCodeConfig),
|
|
370
498
|
]);
|
|
371
499
|
return [
|
|
372
500
|
...(codex ? ["codex"] : []),
|
|
373
501
|
...(claude ? ["claude"] : []),
|
|
502
|
+
...(openCodeExecutable || openCodeConfig
|
|
503
|
+
? ["opencode"]
|
|
504
|
+
: []),
|
|
374
505
|
];
|
|
375
506
|
}
|
|
376
507
|
async function installRuntime(paths) {
|
|
@@ -396,6 +527,26 @@ async function installRuntime(paths) {
|
|
|
396
527
|
function hookPath(agent, paths) {
|
|
397
528
|
return agent === "codex" ? paths.codexHooks : paths.claudeSettings;
|
|
398
529
|
}
|
|
530
|
+
function agentConfigPath(agent, paths) {
|
|
531
|
+
return agent === "opencode"
|
|
532
|
+
? paths.openCodeConfig
|
|
533
|
+
: hookPath(agent, paths);
|
|
534
|
+
}
|
|
535
|
+
function mergeAgentConfig(input, agent, paths) {
|
|
536
|
+
return agent === "opencode"
|
|
537
|
+
? mergeLoreOpenCodePlugin(input)
|
|
538
|
+
: mergeLoreHooks(input, agent, paths);
|
|
539
|
+
}
|
|
540
|
+
function removeAgentConfig(input, agent) {
|
|
541
|
+
return agent === "opencode"
|
|
542
|
+
? removeLoreOpenCodePlugin(input)
|
|
543
|
+
: removeLoreHooks(input);
|
|
544
|
+
}
|
|
545
|
+
function countAgentIntegrations(input, agent) {
|
|
546
|
+
return agent === "opencode"
|
|
547
|
+
? countLoreOpenCodePlugins(input)
|
|
548
|
+
: countLoreHooks(input);
|
|
549
|
+
}
|
|
399
550
|
function parseInteger(value, flag) {
|
|
400
551
|
const parsed = Number(value);
|
|
401
552
|
if (!Number.isInteger(parsed)) {
|
|
@@ -447,11 +598,11 @@ function parseConnectArguments(args) {
|
|
|
447
598
|
}
|
|
448
599
|
parsed.timeoutMs = timeoutMs;
|
|
449
600
|
}
|
|
450
|
-
else if (value
|
|
601
|
+
else if (isConfiguredAgent(value)) {
|
|
451
602
|
parsed.agents.push(value);
|
|
452
603
|
}
|
|
453
604
|
else {
|
|
454
|
-
throw new Error("--agent must be codex or
|
|
605
|
+
throw new Error("--agent must be claude, codex, or opencode");
|
|
455
606
|
}
|
|
456
607
|
}
|
|
457
608
|
return parsed;
|
|
@@ -480,7 +631,7 @@ async function connectCommand(args) {
|
|
|
480
631
|
return;
|
|
481
632
|
}
|
|
482
633
|
if (platform() !== "darwin" && platform() !== "linux") {
|
|
483
|
-
throw new Error("Lore
|
|
634
|
+
throw new Error("Lore agent integrations currently support macOS and Linux");
|
|
484
635
|
}
|
|
485
636
|
const paths = getLorePaths();
|
|
486
637
|
const existing = await readConnectorConfig(paths);
|
|
@@ -488,7 +639,7 @@ async function connectCommand(args) {
|
|
|
488
639
|
process.env.LORE_API_URL ??
|
|
489
640
|
process.env.LORE_BASE_URL ??
|
|
490
641
|
existing?.apiUrl;
|
|
491
|
-
const token = parsed.token
|
|
642
|
+
const token = configuredToken(parsed.token, existing);
|
|
492
643
|
const dashboardUrlValue = parsed.dashboardUrl ??
|
|
493
644
|
process.env.LORE_DASHBOARD_URL ??
|
|
494
645
|
existing?.dashboardUrl;
|
|
@@ -496,9 +647,12 @@ async function connectCommand(args) {
|
|
|
496
647
|
throw new Error("Lore API URL is required. Use --url <url> or LORE_API_URL.");
|
|
497
648
|
}
|
|
498
649
|
if (token === undefined || token.trim() === "") {
|
|
499
|
-
throw new Error("Workspace token is required. Use --token <token
|
|
650
|
+
throw new Error("Workspace token is required. Use --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
|
|
500
651
|
}
|
|
501
|
-
const
|
|
652
|
+
const apiUrl = normalizeApiUrl(apiUrlValue);
|
|
653
|
+
const timeoutMs = parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500;
|
|
654
|
+
const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
|
|
655
|
+
const detected = parsed.agents.length === 0 ? await detectAgents(paths) : [];
|
|
502
656
|
const agents = [
|
|
503
657
|
...new Set([
|
|
504
658
|
...(existing?.agents ?? []),
|
|
@@ -507,17 +661,19 @@ async function connectCommand(args) {
|
|
|
507
661
|
]),
|
|
508
662
|
].sort();
|
|
509
663
|
if (agents.length === 0) {
|
|
510
|
-
throw new Error("No Codex or
|
|
664
|
+
throw new Error("No Claude, Codex, or OpenCode installation detected. Use --agent <name>.");
|
|
511
665
|
}
|
|
512
666
|
const now = new Date();
|
|
513
667
|
const documents = new Map();
|
|
514
668
|
const mergedDocuments = new Map();
|
|
515
669
|
for (const agent of agents) {
|
|
516
|
-
const document = await readJsonDocument(
|
|
670
|
+
const document = await readJsonDocument(agentConfigPath(agent, paths));
|
|
517
671
|
documents.set(agent, document);
|
|
518
|
-
mergedDocuments.set(agent,
|
|
672
|
+
mergedDocuments.set(agent, mergeAgentConfig(document.value, agent, paths));
|
|
673
|
+
}
|
|
674
|
+
if (agents.some(isCommandHookAgent)) {
|
|
675
|
+
await installRuntime(paths);
|
|
519
676
|
}
|
|
520
|
-
await installRuntime(paths);
|
|
521
677
|
const changedHookFiles = [];
|
|
522
678
|
const backups = [];
|
|
523
679
|
for (const agent of agents) {
|
|
@@ -526,9 +682,9 @@ async function connectCommand(args) {
|
|
|
526
682
|
if (document === undefined || merged === undefined) {
|
|
527
683
|
continue;
|
|
528
684
|
}
|
|
529
|
-
const result = await writeMergedJson(
|
|
685
|
+
const result = await writeMergedJson(agentConfigPath(agent, paths), document, merged, now);
|
|
530
686
|
if (result.changed) {
|
|
531
|
-
changedHookFiles.push(
|
|
687
|
+
changedHookFiles.push(agentConfigPath(agent, paths));
|
|
532
688
|
}
|
|
533
689
|
if (result.backup !== undefined) {
|
|
534
690
|
backups.push(result.backup);
|
|
@@ -536,19 +692,20 @@ async function connectCommand(args) {
|
|
|
536
692
|
}
|
|
537
693
|
const config = {
|
|
538
694
|
version: 1,
|
|
539
|
-
apiUrl
|
|
695
|
+
apiUrl,
|
|
540
696
|
...(dashboardUrlValue === undefined
|
|
541
697
|
? {}
|
|
542
698
|
: { dashboardUrl: normalizeApiUrl(dashboardUrlValue) }),
|
|
543
699
|
token: token.trim(),
|
|
544
700
|
agents,
|
|
545
701
|
connectedAt: existing?.connectedAt ?? now.toISOString(),
|
|
546
|
-
timeoutMs
|
|
702
|
+
timeoutMs,
|
|
547
703
|
};
|
|
548
704
|
await atomicWrite(paths.config, `${JSON.stringify(config, null, 2)}\n`, 0o600);
|
|
549
705
|
const result = {
|
|
550
706
|
connected: true,
|
|
551
707
|
apiUrl: config.apiUrl,
|
|
708
|
+
identity,
|
|
552
709
|
agents,
|
|
553
710
|
config: paths.config,
|
|
554
711
|
changedHookFiles,
|
|
@@ -565,21 +722,23 @@ async function queueCount(paths) {
|
|
|
565
722
|
}
|
|
566
723
|
}
|
|
567
724
|
async function getAgentStatus(agent, config, paths) {
|
|
568
|
-
const path =
|
|
569
|
-
let
|
|
725
|
+
const path = agentConfigPath(agent, paths);
|
|
726
|
+
let installed = 0;
|
|
570
727
|
try {
|
|
571
|
-
|
|
728
|
+
installed = countAgentIntegrations((await readJsonDocument(path)).value, agent);
|
|
572
729
|
}
|
|
573
730
|
catch {
|
|
574
|
-
|
|
731
|
+
installed = 0;
|
|
575
732
|
}
|
|
576
733
|
return {
|
|
577
734
|
agent,
|
|
578
735
|
configured: config?.agents.includes(agent) ?? false,
|
|
579
736
|
executable: await commandExists(agent),
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
737
|
+
configExists: await pathExists(path),
|
|
738
|
+
configFile: path,
|
|
739
|
+
integration: agent === "opencode" ? "plugin" : "hooks",
|
|
740
|
+
installed,
|
|
741
|
+
expected: agent === "opencode" ? 1 : HOOK_EVENTS.length,
|
|
583
742
|
};
|
|
584
743
|
}
|
|
585
744
|
async function statusData(paths) {
|
|
@@ -591,27 +750,32 @@ async function statusData(paths) {
|
|
|
591
750
|
catch {
|
|
592
751
|
// Missing configuration is represented as disconnected.
|
|
593
752
|
}
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
access(
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
753
|
+
const runtimeRequired = config?.agents.some(isCommandHookAgent) ?? false;
|
|
754
|
+
const runtimeInstalledCheck = runtimeRequired
|
|
755
|
+
? Promise.all(IS_STANDALONE_BINARY
|
|
756
|
+
? [access(process.execPath, fsConstants.R_OK | fsConstants.X_OK)]
|
|
757
|
+
: [
|
|
758
|
+
access(paths.runtime, fsConstants.R_OK | fsConstants.X_OK),
|
|
759
|
+
access(paths.runtimeRepository, fsConstants.R_OK),
|
|
760
|
+
access(paths.runtimePackage, fsConstants.R_OK),
|
|
761
|
+
]).then(() => true, () => false)
|
|
762
|
+
: Promise.resolve(false);
|
|
763
|
+
const [runtimeInstalled, queuedTurns, claude, codex, opencode] = await Promise.all([
|
|
764
|
+
runtimeInstalledCheck,
|
|
603
765
|
queueCount(paths),
|
|
604
|
-
getAgentStatus("codex", config, paths),
|
|
605
766
|
getAgentStatus("claude", config, paths),
|
|
767
|
+
getAgentStatus("codex", config, paths),
|
|
768
|
+
getAgentStatus("opencode", config, paths),
|
|
606
769
|
]);
|
|
607
770
|
return {
|
|
608
771
|
connected: config !== null,
|
|
609
772
|
apiUrl: config?.apiUrl ?? null,
|
|
610
773
|
config: paths.config,
|
|
611
774
|
configMode,
|
|
775
|
+
runtimeRequired,
|
|
612
776
|
runtimeInstalled,
|
|
613
777
|
queuedTurns,
|
|
614
|
-
agents: [codex,
|
|
778
|
+
agents: [claude, codex, opencode],
|
|
615
779
|
};
|
|
616
780
|
}
|
|
617
781
|
async function statusCommand(args) {
|
|
@@ -621,9 +785,9 @@ async function statusCommand(args) {
|
|
|
621
785
|
}
|
|
622
786
|
const data = await statusData(getLorePaths());
|
|
623
787
|
const agentLines = data.agents
|
|
624
|
-
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"},
|
|
788
|
+
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, ${agent.integration} ${agent.installed}/${agent.expected}, executable ${agent.executable ? "yes" : "no"}, config ${agent.configExists ? "yes" : "no"}`)
|
|
625
789
|
.join("\n");
|
|
626
|
-
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeInstalled ? "installed" : "missing"}\nqueued_turns: ${data.queuedTurns}\n${agentLines}\n`);
|
|
790
|
+
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? "installed" : "missing") : "not required"}\nqueued_turns: ${data.queuedTurns}\n${agentLines}\n`);
|
|
627
791
|
}
|
|
628
792
|
async function disconnectCommand(args) {
|
|
629
793
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -634,13 +798,13 @@ async function disconnectCommand(args) {
|
|
|
634
798
|
const now = new Date();
|
|
635
799
|
const changedHookFiles = [];
|
|
636
800
|
const backups = [];
|
|
637
|
-
for (const agent of
|
|
638
|
-
const path =
|
|
801
|
+
for (const agent of CONFIGURED_AGENT_NAMES) {
|
|
802
|
+
const path = agentConfigPath(agent, paths);
|
|
639
803
|
const document = await readJsonDocument(path);
|
|
640
804
|
if (!document.exists) {
|
|
641
805
|
continue;
|
|
642
806
|
}
|
|
643
|
-
const result = await writeMergedJson(path, document,
|
|
807
|
+
const result = await writeMergedJson(path, document, removeAgentConfig(document.value, agent), now);
|
|
644
808
|
if (result.changed) {
|
|
645
809
|
changedHookFiles.push(path);
|
|
646
810
|
}
|
|
@@ -659,28 +823,60 @@ async function disconnectCommand(args) {
|
|
|
659
823
|
const result = { connected: false, changedHookFiles, backups };
|
|
660
824
|
writeResult(result, parsed.json, `disconnected: yes\nchanged_hook_files: ${changedHookFiles.length}\n`);
|
|
661
825
|
}
|
|
662
|
-
async function
|
|
663
|
-
const
|
|
826
|
+
async function apiChecks(config) {
|
|
827
|
+
const readinessUrl = `${config.apiUrl}/health/ready`;
|
|
828
|
+
let readiness;
|
|
664
829
|
try {
|
|
665
|
-
|
|
666
|
-
headers: { authorization: `Bearer ${config.token}` },
|
|
830
|
+
readiness = await fetch(readinessUrl, {
|
|
667
831
|
signal: AbortSignal.timeout(3_000),
|
|
668
832
|
});
|
|
669
|
-
|
|
670
|
-
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
return [
|
|
836
|
+
{
|
|
837
|
+
name: "api-readiness",
|
|
838
|
+
status: "error",
|
|
839
|
+
detail: `unreachable: ${readinessUrl}`,
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
name: "api-identity",
|
|
843
|
+
status: "warning",
|
|
844
|
+
detail: "not checked because the API is unreachable",
|
|
845
|
+
},
|
|
846
|
+
];
|
|
847
|
+
}
|
|
848
|
+
const checks = [
|
|
849
|
+
readiness.ok
|
|
850
|
+
? {
|
|
851
|
+
name: "api-readiness",
|
|
852
|
+
status: "ok",
|
|
853
|
+
detail: `ready: ${readinessUrl}`,
|
|
854
|
+
}
|
|
671
855
|
: {
|
|
672
|
-
name: "api",
|
|
856
|
+
name: "api-readiness",
|
|
673
857
|
status: "error",
|
|
674
|
-
detail:
|
|
675
|
-
}
|
|
858
|
+
detail: `unready: ${readinessUrl} returned ${readiness.status}`,
|
|
859
|
+
},
|
|
860
|
+
];
|
|
861
|
+
try {
|
|
862
|
+
const identity = await authenticatedIdentity(config.apiUrl, config.token, 3_000);
|
|
863
|
+
checks.push({
|
|
864
|
+
name: "api-identity",
|
|
865
|
+
status: "ok",
|
|
866
|
+
detail: `authorized for ${identity.organization}/${identity.workspaceName} as ${identity.credentialType}; server ${identity.server.version}`,
|
|
867
|
+
});
|
|
676
868
|
}
|
|
677
869
|
catch (error) {
|
|
678
|
-
|
|
679
|
-
|
|
870
|
+
const detail = error instanceof Error ? error.message : "Identity check failed";
|
|
871
|
+
checks.push({
|
|
872
|
+
name: "api-identity",
|
|
680
873
|
status: "error",
|
|
681
|
-
detail:
|
|
682
|
-
|
|
874
|
+
detail: detail.startsWith("Lore authentication failed")
|
|
875
|
+
? "unauthorized: the configured credential was rejected"
|
|
876
|
+
: detail,
|
|
877
|
+
});
|
|
683
878
|
}
|
|
879
|
+
return checks;
|
|
684
880
|
}
|
|
685
881
|
async function doctorCommand(args) {
|
|
686
882
|
const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
|
|
@@ -717,11 +913,13 @@ async function doctorCommand(args) {
|
|
|
717
913
|
status: status.configMode === "600" ? "ok" : "error",
|
|
718
914
|
detail: status.configMode ?? "missing",
|
|
719
915
|
});
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
916
|
+
if (status.runtimeRequired) {
|
|
917
|
+
checks.push({
|
|
918
|
+
name: "runtime",
|
|
919
|
+
status: status.runtimeInstalled ? "ok" : "error",
|
|
920
|
+
detail: IS_STANDALONE_BINARY ? process.execPath : paths.runtime,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
725
923
|
for (const agent of status.agents.filter((item) => item.configured)) {
|
|
726
924
|
checks.push({
|
|
727
925
|
name: `${agent.agent}-executable`,
|
|
@@ -729,9 +927,9 @@ async function doctorCommand(args) {
|
|
|
729
927
|
detail: agent.executable ? "found on PATH" : "not found on PATH",
|
|
730
928
|
});
|
|
731
929
|
checks.push({
|
|
732
|
-
name: `${agent.agent}
|
|
733
|
-
status: agent.
|
|
734
|
-
detail: `${agent.
|
|
930
|
+
name: `${agent.agent}-${agent.integration}`,
|
|
931
|
+
status: agent.installed === agent.expected ? "ok" : "error",
|
|
932
|
+
detail: `${agent.installed}/${agent.expected} Lore ${agent.integration} in ${agent.configFile}`,
|
|
735
933
|
});
|
|
736
934
|
}
|
|
737
935
|
checks.push({
|
|
@@ -740,7 +938,7 @@ async function doctorCommand(args) {
|
|
|
740
938
|
detail: `${status.queuedTurns} queued turn(s)`,
|
|
741
939
|
});
|
|
742
940
|
if (config !== null) {
|
|
743
|
-
checks.push(await
|
|
941
|
+
checks.push(...(await apiChecks(config)));
|
|
744
942
|
}
|
|
745
943
|
const errors = checks.filter((check) => check.status === "error").length;
|
|
746
944
|
const warnings = checks.filter((check) => check.status === "warning").length;
|
|
@@ -784,6 +982,9 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
784
982
|
case "doctor":
|
|
785
983
|
await doctorCommand(commandArgs);
|
|
786
984
|
return;
|
|
985
|
+
case "self-host":
|
|
986
|
+
await runSelfHostCommand(commandArgs);
|
|
987
|
+
return;
|
|
787
988
|
case "demo":
|
|
788
989
|
await runDemoCommand(commandArgs, await readConnectorConfig(getLorePaths()));
|
|
789
990
|
return;
|