@lore-co/cli 0.1.2 → 0.1.4
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 +72 -24
- package/dist/cli.d.ts +12 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +300 -78
- package/dist/cli.js.map +1 -1
- package/dist/demo.d.ts +24 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +402 -0
- package/dist/demo.js.map +1 -0
- 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/github.d.ts.map +1 -1
- package/dist/github.js +2 -4
- package/dist/github.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/repository.d.ts +1 -0
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +6 -0
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts +15 -7
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +271 -44
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +7 -0
- package/dist/self-host.d.ts.map +1 -0
- package/dist/self-host.js +981 -0
- package/dist/self-host.js.map +1 -0
- package/dist/update.js +3 -3
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -5,13 +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";
|
|
13
|
+
import { runDemoCommand } from "./demo.js";
|
|
12
14
|
import { updateCommand } from "./update.js";
|
|
15
|
+
import { runSelfHostCommand } from "./self-host.js";
|
|
13
16
|
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
14
17
|
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
18
|
+
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
19
|
+
const CONFIGURED_AGENT_NAMES = ["claude", "codex", "opencode"];
|
|
15
20
|
const HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionEnd"];
|
|
16
21
|
const ROOT_HELP = `lore
|
|
17
22
|
Connect local coding agents to Lore shared engineering memory.
|
|
@@ -20,51 +25,56 @@ Usage:
|
|
|
20
25
|
lore <command> [options]
|
|
21
26
|
|
|
22
27
|
Commands:
|
|
23
|
-
connect Configure Lore and install native agent
|
|
28
|
+
connect Configure Lore and install native agent integrations
|
|
24
29
|
github Prepare/post GitHub reviews and observe corrections
|
|
25
30
|
devin Start and manage Lore-enabled Devin sessions
|
|
26
31
|
host Call auditable APIs from external agent hosts
|
|
27
|
-
status Show connector and
|
|
28
|
-
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
|
|
35
|
+
demo Prove a file-scoped Claude-to-Codex handoff
|
|
29
36
|
update Install the latest Lore CLI binary
|
|
30
|
-
disconnect Remove Lore-owned
|
|
37
|
+
disconnect Remove Lore-owned integrations and local credentials
|
|
31
38
|
hook Internal native hook handler
|
|
32
39
|
|
|
33
40
|
Discover:
|
|
34
41
|
lore connect --help
|
|
35
42
|
lore status --help
|
|
36
43
|
lore doctor --help
|
|
44
|
+
lore self-host --help
|
|
37
45
|
lore disconnect --help
|
|
38
46
|
lore host --help
|
|
39
47
|
|
|
40
48
|
Examples:
|
|
41
|
-
lore connect --url https://lore.example.com --token "$
|
|
49
|
+
lore connect --url https://lore.example.com --token "$LORE_WORKSPACE_TOKEN"
|
|
42
50
|
lore connect github --repo owner/repository
|
|
43
51
|
lore status --json
|
|
44
52
|
lore doctor
|
|
53
|
+
lore demo
|
|
45
54
|
lore update
|
|
46
55
|
lore devin --help
|
|
47
56
|
`;
|
|
48
57
|
const CONNECT_HELP = `lore connect
|
|
49
|
-
Store a workspace credential and idempotently install
|
|
58
|
+
Store a workspace credential and idempotently install agent integrations.
|
|
50
59
|
|
|
51
60
|
Usage:
|
|
52
61
|
lore connect --url <url> --token <token> [options]
|
|
53
62
|
|
|
54
63
|
Options:
|
|
55
64
|
--url <url> Lore API base URL (or LORE_API_URL)
|
|
56
|
-
--
|
|
57
|
-
--
|
|
65
|
+
--dashboard-url <url> Lore dashboard URL used for receipt links
|
|
66
|
+
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
67
|
+
--agent <name> claude, codex, or opencode; repeat to override auto-detection
|
|
58
68
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
59
69
|
--json Print machine-readable output
|
|
60
70
|
--help Show this command's help
|
|
61
71
|
|
|
62
72
|
Examples:
|
|
63
|
-
lore connect --url https://lore.example.com --token "$
|
|
73
|
+
lore connect --url https://lore.example.com --token "$LORE_WORKSPACE_TOKEN"
|
|
64
74
|
lore connect --url http://localhost:3004 --token dev-token --agent codex
|
|
65
75
|
`;
|
|
66
76
|
const STATUS_HELP = `lore status
|
|
67
|
-
Show whether Lore is configured and
|
|
77
|
+
Show whether Lore is configured and each native integration is installed.
|
|
68
78
|
|
|
69
79
|
Usage:
|
|
70
80
|
lore status [--json]
|
|
@@ -74,7 +84,7 @@ Examples:
|
|
|
74
84
|
lore status --json
|
|
75
85
|
`;
|
|
76
86
|
const DOCTOR_HELP = `lore doctor
|
|
77
|
-
Check local security, runtime,
|
|
87
|
+
Check local security, runtime, agent integrations, binaries, and Lore health.
|
|
78
88
|
|
|
79
89
|
Usage:
|
|
80
90
|
lore doctor [--json]
|
|
@@ -84,8 +94,8 @@ Examples:
|
|
|
84
94
|
lore doctor --json
|
|
85
95
|
`;
|
|
86
96
|
const DISCONNECT_HELP = `lore disconnect
|
|
87
|
-
Remove only Lore-owned
|
|
88
|
-
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.
|
|
89
99
|
|
|
90
100
|
Usage:
|
|
91
101
|
lore disconnect [--json]
|
|
@@ -114,11 +124,18 @@ export function getLorePaths(home) {
|
|
|
114
124
|
queue: resolve(loreDirectory, "queue"),
|
|
115
125
|
codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
|
|
116
126
|
claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
|
|
127
|
+
openCodeConfig: resolve(resolvedHome, ".config", "opencode", "opencode.json"),
|
|
117
128
|
};
|
|
118
129
|
}
|
|
119
130
|
function shellQuote(value) {
|
|
120
131
|
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
121
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
|
+
}
|
|
122
139
|
function hookCommand(agent, paths) {
|
|
123
140
|
if (IS_STANDALONE_BINARY) {
|
|
124
141
|
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
@@ -223,6 +240,41 @@ export function countLoreHooks(input) {
|
|
|
223
240
|
}
|
|
224
241
|
return count;
|
|
225
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
|
+
}
|
|
226
278
|
async function readJsonDocument(path) {
|
|
227
279
|
try {
|
|
228
280
|
const [raw, metadata] = await Promise.all([
|
|
@@ -292,16 +344,20 @@ function parseConnectorConfig(value) {
|
|
|
292
344
|
typeof value.connectedAt !== "string") {
|
|
293
345
|
return null;
|
|
294
346
|
}
|
|
295
|
-
const agents = value.agents.filter((agent) => agent
|
|
347
|
+
const agents = value.agents.filter((agent) => isConfiguredAgent(agent));
|
|
296
348
|
const timeoutMs = typeof value.timeoutMs === "number" &&
|
|
297
349
|
Number.isInteger(value.timeoutMs) &&
|
|
298
350
|
value.timeoutMs >= 250 &&
|
|
299
351
|
value.timeoutMs <= 10_000
|
|
300
352
|
? value.timeoutMs
|
|
301
353
|
: 2_500;
|
|
354
|
+
const dashboardUrl = typeof value.dashboardUrl === "string"
|
|
355
|
+
? normalizeApiUrl(value.dashboardUrl)
|
|
356
|
+
: undefined;
|
|
302
357
|
return {
|
|
303
358
|
version: 1,
|
|
304
359
|
apiUrl: value.apiUrl,
|
|
360
|
+
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
305
361
|
token: value.token,
|
|
306
362
|
agents,
|
|
307
363
|
connectedAt: value.connectedAt,
|
|
@@ -322,7 +378,7 @@ function normalizeApiUrl(value) {
|
|
|
322
378
|
parsed = new URL(value);
|
|
323
379
|
}
|
|
324
380
|
catch {
|
|
325
|
-
throw new Error(
|
|
381
|
+
throw new Error("Lore API URL is invalid");
|
|
326
382
|
}
|
|
327
383
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
328
384
|
throw new Error("Lore API URL must use http or https");
|
|
@@ -330,10 +386,79 @@ function normalizeApiUrl(value) {
|
|
|
330
386
|
if (parsed.username !== "" || parsed.password !== "") {
|
|
331
387
|
throw new Error("Lore API URL must not contain credentials");
|
|
332
388
|
}
|
|
333
|
-
parsed.hash
|
|
334
|
-
|
|
389
|
+
if (parsed.hash !== "" || parsed.search !== "") {
|
|
390
|
+
throw new Error("Lore API URL must not contain a query or fragment");
|
|
391
|
+
}
|
|
335
392
|
return parsed.href.replace(/\/+$/u, "");
|
|
336
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
|
+
}
|
|
337
462
|
async function isExecutable(path) {
|
|
338
463
|
try {
|
|
339
464
|
await access(path, fsConstants.X_OK);
|
|
@@ -355,14 +480,28 @@ async function commandExists(command) {
|
|
|
355
480
|
}
|
|
356
481
|
return false;
|
|
357
482
|
}
|
|
358
|
-
async function
|
|
359
|
-
|
|
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([
|
|
360
494
|
commandExists("codex"),
|
|
361
495
|
commandExists("claude"),
|
|
496
|
+
commandExists("opencode"),
|
|
497
|
+
pathExists(paths.openCodeConfig),
|
|
362
498
|
]);
|
|
363
499
|
return [
|
|
364
500
|
...(codex ? ["codex"] : []),
|
|
365
501
|
...(claude ? ["claude"] : []),
|
|
502
|
+
...(openCodeExecutable || openCodeConfig
|
|
503
|
+
? ["opencode"]
|
|
504
|
+
: []),
|
|
366
505
|
];
|
|
367
506
|
}
|
|
368
507
|
async function installRuntime(paths) {
|
|
@@ -388,6 +527,26 @@ async function installRuntime(paths) {
|
|
|
388
527
|
function hookPath(agent, paths) {
|
|
389
528
|
return agent === "codex" ? paths.codexHooks : paths.claudeSettings;
|
|
390
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
|
+
}
|
|
391
550
|
function parseInteger(value, flag) {
|
|
392
551
|
const parsed = Number(value);
|
|
393
552
|
if (!Number.isInteger(parsed)) {
|
|
@@ -415,6 +574,7 @@ function parseConnectArguments(args) {
|
|
|
415
574
|
continue;
|
|
416
575
|
}
|
|
417
576
|
if (argument !== "--url" &&
|
|
577
|
+
argument !== "--dashboard-url" &&
|
|
418
578
|
argument !== "--token" &&
|
|
419
579
|
argument !== "--agent" &&
|
|
420
580
|
argument !== "--timeout-ms") {
|
|
@@ -425,6 +585,9 @@ function parseConnectArguments(args) {
|
|
|
425
585
|
if (argument === "--url") {
|
|
426
586
|
parsed.apiUrl = value;
|
|
427
587
|
}
|
|
588
|
+
else if (argument === "--dashboard-url") {
|
|
589
|
+
parsed.dashboardUrl = value;
|
|
590
|
+
}
|
|
428
591
|
else if (argument === "--token") {
|
|
429
592
|
parsed.token = value;
|
|
430
593
|
}
|
|
@@ -435,11 +598,11 @@ function parseConnectArguments(args) {
|
|
|
435
598
|
}
|
|
436
599
|
parsed.timeoutMs = timeoutMs;
|
|
437
600
|
}
|
|
438
|
-
else if (value
|
|
601
|
+
else if (isConfiguredAgent(value)) {
|
|
439
602
|
parsed.agents.push(value);
|
|
440
603
|
}
|
|
441
604
|
else {
|
|
442
|
-
throw new Error("--agent must be codex or
|
|
605
|
+
throw new Error("--agent must be claude, codex, or opencode");
|
|
443
606
|
}
|
|
444
607
|
}
|
|
445
608
|
return parsed;
|
|
@@ -468,7 +631,7 @@ async function connectCommand(args) {
|
|
|
468
631
|
return;
|
|
469
632
|
}
|
|
470
633
|
if (platform() !== "darwin" && platform() !== "linux") {
|
|
471
|
-
throw new Error("Lore
|
|
634
|
+
throw new Error("Lore agent integrations currently support macOS and Linux");
|
|
472
635
|
}
|
|
473
636
|
const paths = getLorePaths();
|
|
474
637
|
const existing = await readConnectorConfig(paths);
|
|
@@ -476,14 +639,20 @@ async function connectCommand(args) {
|
|
|
476
639
|
process.env.LORE_API_URL ??
|
|
477
640
|
process.env.LORE_BASE_URL ??
|
|
478
641
|
existing?.apiUrl;
|
|
479
|
-
const token = parsed.token
|
|
642
|
+
const token = configuredToken(parsed.token, existing);
|
|
643
|
+
const dashboardUrlValue = parsed.dashboardUrl ??
|
|
644
|
+
process.env.LORE_DASHBOARD_URL ??
|
|
645
|
+
existing?.dashboardUrl;
|
|
480
646
|
if (apiUrlValue === undefined || apiUrlValue.trim() === "") {
|
|
481
647
|
throw new Error("Lore API URL is required. Use --url <url> or LORE_API_URL.");
|
|
482
648
|
}
|
|
483
649
|
if (token === undefined || token.trim() === "") {
|
|
484
|
-
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.");
|
|
485
651
|
}
|
|
486
|
-
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) : [];
|
|
487
656
|
const agents = [
|
|
488
657
|
...new Set([
|
|
489
658
|
...(existing?.agents ?? []),
|
|
@@ -492,17 +661,19 @@ async function connectCommand(args) {
|
|
|
492
661
|
]),
|
|
493
662
|
].sort();
|
|
494
663
|
if (agents.length === 0) {
|
|
495
|
-
throw new Error("No Codex or
|
|
664
|
+
throw new Error("No Claude, Codex, or OpenCode installation detected. Use --agent <name>.");
|
|
496
665
|
}
|
|
497
666
|
const now = new Date();
|
|
498
667
|
const documents = new Map();
|
|
499
668
|
const mergedDocuments = new Map();
|
|
500
669
|
for (const agent of agents) {
|
|
501
|
-
const document = await readJsonDocument(
|
|
670
|
+
const document = await readJsonDocument(agentConfigPath(agent, paths));
|
|
502
671
|
documents.set(agent, document);
|
|
503
|
-
mergedDocuments.set(agent,
|
|
672
|
+
mergedDocuments.set(agent, mergeAgentConfig(document.value, agent, paths));
|
|
673
|
+
}
|
|
674
|
+
if (agents.some(isCommandHookAgent)) {
|
|
675
|
+
await installRuntime(paths);
|
|
504
676
|
}
|
|
505
|
-
await installRuntime(paths);
|
|
506
677
|
const changedHookFiles = [];
|
|
507
678
|
const backups = [];
|
|
508
679
|
for (const agent of agents) {
|
|
@@ -511,9 +682,9 @@ async function connectCommand(args) {
|
|
|
511
682
|
if (document === undefined || merged === undefined) {
|
|
512
683
|
continue;
|
|
513
684
|
}
|
|
514
|
-
const result = await writeMergedJson(
|
|
685
|
+
const result = await writeMergedJson(agentConfigPath(agent, paths), document, merged, now);
|
|
515
686
|
if (result.changed) {
|
|
516
|
-
changedHookFiles.push(
|
|
687
|
+
changedHookFiles.push(agentConfigPath(agent, paths));
|
|
517
688
|
}
|
|
518
689
|
if (result.backup !== undefined) {
|
|
519
690
|
backups.push(result.backup);
|
|
@@ -521,16 +692,20 @@ async function connectCommand(args) {
|
|
|
521
692
|
}
|
|
522
693
|
const config = {
|
|
523
694
|
version: 1,
|
|
524
|
-
apiUrl
|
|
695
|
+
apiUrl,
|
|
696
|
+
...(dashboardUrlValue === undefined
|
|
697
|
+
? {}
|
|
698
|
+
: { dashboardUrl: normalizeApiUrl(dashboardUrlValue) }),
|
|
525
699
|
token: token.trim(),
|
|
526
700
|
agents,
|
|
527
701
|
connectedAt: existing?.connectedAt ?? now.toISOString(),
|
|
528
|
-
timeoutMs
|
|
702
|
+
timeoutMs,
|
|
529
703
|
};
|
|
530
704
|
await atomicWrite(paths.config, `${JSON.stringify(config, null, 2)}\n`, 0o600);
|
|
531
705
|
const result = {
|
|
532
706
|
connected: true,
|
|
533
707
|
apiUrl: config.apiUrl,
|
|
708
|
+
identity,
|
|
534
709
|
agents,
|
|
535
710
|
config: paths.config,
|
|
536
711
|
changedHookFiles,
|
|
@@ -547,21 +722,23 @@ async function queueCount(paths) {
|
|
|
547
722
|
}
|
|
548
723
|
}
|
|
549
724
|
async function getAgentStatus(agent, config, paths) {
|
|
550
|
-
const path =
|
|
551
|
-
let
|
|
725
|
+
const path = agentConfigPath(agent, paths);
|
|
726
|
+
let installed = 0;
|
|
552
727
|
try {
|
|
553
|
-
|
|
728
|
+
installed = countAgentIntegrations((await readJsonDocument(path)).value, agent);
|
|
554
729
|
}
|
|
555
730
|
catch {
|
|
556
|
-
|
|
731
|
+
installed = 0;
|
|
557
732
|
}
|
|
558
733
|
return {
|
|
559
734
|
agent,
|
|
560
735
|
configured: config?.agents.includes(agent) ?? false,
|
|
561
736
|
executable: await commandExists(agent),
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
737
|
+
configExists: await pathExists(path),
|
|
738
|
+
configFile: path,
|
|
739
|
+
integration: agent === "opencode" ? "plugin" : "hooks",
|
|
740
|
+
installed,
|
|
741
|
+
expected: agent === "opencode" ? 1 : HOOK_EVENTS.length,
|
|
565
742
|
};
|
|
566
743
|
}
|
|
567
744
|
async function statusData(paths) {
|
|
@@ -573,27 +750,32 @@ async function statusData(paths) {
|
|
|
573
750
|
catch {
|
|
574
751
|
// Missing configuration is represented as disconnected.
|
|
575
752
|
}
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
access(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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,
|
|
585
765
|
queueCount(paths),
|
|
586
|
-
getAgentStatus("codex", config, paths),
|
|
587
766
|
getAgentStatus("claude", config, paths),
|
|
767
|
+
getAgentStatus("codex", config, paths),
|
|
768
|
+
getAgentStatus("opencode", config, paths),
|
|
588
769
|
]);
|
|
589
770
|
return {
|
|
590
771
|
connected: config !== null,
|
|
591
772
|
apiUrl: config?.apiUrl ?? null,
|
|
592
773
|
config: paths.config,
|
|
593
774
|
configMode,
|
|
775
|
+
runtimeRequired,
|
|
594
776
|
runtimeInstalled,
|
|
595
777
|
queuedTurns,
|
|
596
|
-
agents: [codex,
|
|
778
|
+
agents: [claude, codex, opencode],
|
|
597
779
|
};
|
|
598
780
|
}
|
|
599
781
|
async function statusCommand(args) {
|
|
@@ -603,9 +785,9 @@ async function statusCommand(args) {
|
|
|
603
785
|
}
|
|
604
786
|
const data = await statusData(getLorePaths());
|
|
605
787
|
const agentLines = data.agents
|
|
606
|
-
.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"}`)
|
|
607
789
|
.join("\n");
|
|
608
|
-
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`);
|
|
609
791
|
}
|
|
610
792
|
async function disconnectCommand(args) {
|
|
611
793
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -616,13 +798,13 @@ async function disconnectCommand(args) {
|
|
|
616
798
|
const now = new Date();
|
|
617
799
|
const changedHookFiles = [];
|
|
618
800
|
const backups = [];
|
|
619
|
-
for (const agent of
|
|
620
|
-
const path =
|
|
801
|
+
for (const agent of CONFIGURED_AGENT_NAMES) {
|
|
802
|
+
const path = agentConfigPath(agent, paths);
|
|
621
803
|
const document = await readJsonDocument(path);
|
|
622
804
|
if (!document.exists) {
|
|
623
805
|
continue;
|
|
624
806
|
}
|
|
625
|
-
const result = await writeMergedJson(path, document,
|
|
807
|
+
const result = await writeMergedJson(path, document, removeAgentConfig(document.value, agent), now);
|
|
626
808
|
if (result.changed) {
|
|
627
809
|
changedHookFiles.push(path);
|
|
628
810
|
}
|
|
@@ -641,28 +823,60 @@ async function disconnectCommand(args) {
|
|
|
641
823
|
const result = { connected: false, changedHookFiles, backups };
|
|
642
824
|
writeResult(result, parsed.json, `disconnected: yes\nchanged_hook_files: ${changedHookFiles.length}\n`);
|
|
643
825
|
}
|
|
644
|
-
async function
|
|
645
|
-
const
|
|
826
|
+
async function apiChecks(config) {
|
|
827
|
+
const readinessUrl = `${config.apiUrl}/health/ready`;
|
|
828
|
+
let readiness;
|
|
646
829
|
try {
|
|
647
|
-
|
|
648
|
-
headers: { authorization: `Bearer ${config.token}` },
|
|
830
|
+
readiness = await fetch(readinessUrl, {
|
|
649
831
|
signal: AbortSignal.timeout(3_000),
|
|
650
832
|
});
|
|
651
|
-
|
|
652
|
-
|
|
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
|
+
}
|
|
653
855
|
: {
|
|
654
|
-
name: "api",
|
|
856
|
+
name: "api-readiness",
|
|
655
857
|
status: "error",
|
|
656
|
-
detail:
|
|
657
|
-
}
|
|
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
|
+
});
|
|
658
868
|
}
|
|
659
869
|
catch (error) {
|
|
660
|
-
|
|
661
|
-
|
|
870
|
+
const detail = error instanceof Error ? error.message : "Identity check failed";
|
|
871
|
+
checks.push({
|
|
872
|
+
name: "api-identity",
|
|
662
873
|
status: "error",
|
|
663
|
-
detail:
|
|
664
|
-
|
|
874
|
+
detail: detail.startsWith("Lore authentication failed")
|
|
875
|
+
? "unauthorized: the configured credential was rejected"
|
|
876
|
+
: detail,
|
|
877
|
+
});
|
|
665
878
|
}
|
|
879
|
+
return checks;
|
|
666
880
|
}
|
|
667
881
|
async function doctorCommand(args) {
|
|
668
882
|
const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
|
|
@@ -699,11 +913,13 @@ async function doctorCommand(args) {
|
|
|
699
913
|
status: status.configMode === "600" ? "ok" : "error",
|
|
700
914
|
detail: status.configMode ?? "missing",
|
|
701
915
|
});
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
+
}
|
|
707
923
|
for (const agent of status.agents.filter((item) => item.configured)) {
|
|
708
924
|
checks.push({
|
|
709
925
|
name: `${agent.agent}-executable`,
|
|
@@ -711,9 +927,9 @@ async function doctorCommand(args) {
|
|
|
711
927
|
detail: agent.executable ? "found on PATH" : "not found on PATH",
|
|
712
928
|
});
|
|
713
929
|
checks.push({
|
|
714
|
-
name: `${agent.agent}
|
|
715
|
-
status: agent.
|
|
716
|
-
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}`,
|
|
717
933
|
});
|
|
718
934
|
}
|
|
719
935
|
checks.push({
|
|
@@ -722,7 +938,7 @@ async function doctorCommand(args) {
|
|
|
722
938
|
detail: `${status.queuedTurns} queued turn(s)`,
|
|
723
939
|
});
|
|
724
940
|
if (config !== null) {
|
|
725
|
-
checks.push(await
|
|
941
|
+
checks.push(...(await apiChecks(config)));
|
|
726
942
|
}
|
|
727
943
|
const errors = checks.filter((check) => check.status === "error").length;
|
|
728
944
|
const warnings = checks.filter((check) => check.status === "warning").length;
|
|
@@ -766,6 +982,12 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
766
982
|
case "doctor":
|
|
767
983
|
await doctorCommand(commandArgs);
|
|
768
984
|
return;
|
|
985
|
+
case "self-host":
|
|
986
|
+
await runSelfHostCommand(commandArgs);
|
|
987
|
+
return;
|
|
988
|
+
case "demo":
|
|
989
|
+
await runDemoCommand(commandArgs, await readConnectorConfig(getLorePaths()));
|
|
990
|
+
return;
|
|
769
991
|
case "update":
|
|
770
992
|
await updateCommand(commandArgs);
|
|
771
993
|
return;
|