@lore-co/cli 0.1.17 → 0.1.19
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 +99 -17
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +3 -26
- package/dist/ask.js.map +1 -1
- package/dist/cli.d.ts +37 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1071 -80
- package/dist/cli.js.map +1 -1
- package/dist/context-fallback.d.ts +37 -0
- package/dist/context-fallback.d.ts.map +1 -0
- package/dist/context-fallback.js +259 -0
- package/dist/context-fallback.js.map +1 -0
- package/dist/generated-assets.d.ts +4 -4
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +4 -4
- package/dist/generated-assets.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/invocation-health-writer.d.ts +19 -0
- package/dist/invocation-health-writer.d.ts.map +1 -0
- package/dist/invocation-health-writer.js +131 -0
- package/dist/invocation-health-writer.js.map +1 -0
- package/dist/reliability-store.d.ts +283 -0
- package/dist/reliability-store.d.ts.map +1 -0
- package/dist/reliability-store.js +1913 -0
- package/dist/reliability-store.js.map +1 -0
- package/dist/runtime-version.d.ts +2 -0
- package/dist/runtime-version.d.ts.map +1 -0
- package/dist/runtime-version.js +5 -0
- package/dist/runtime-version.js.map +1 -0
- package/dist/runtime.d.ts +9 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1113 -153
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +15 -2
- package/dist/self-host.d.ts.map +1 -1
- package/dist/self-host.js +55 -8
- package/dist/self-host.js.map +1 -1
- package/dist/signed-snapshot.d.ts +59 -0
- package/dist/signed-snapshot.d.ts.map +1 -0
- package/dist/signed-snapshot.js +303 -0
- package/dist/signed-snapshot.js.map +1 -0
- package/dist/update.js +3 -3
- package/package.json +19 -3
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { access, chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
import { delimiter, dirname, resolve } from "node:path";
|
|
6
|
-
import { homedir, platform } from "node:os";
|
|
7
|
+
import { homedir, hostname, platform } from "node:os";
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { NATIVE_CODING_AGENT_NAMES, WorkspaceIdentityResponseSchema, isNativeCodingAgent, } from "@lore-co/core";
|
|
10
|
+
import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, COPILOT_CLI_RELIABILITY_INTEGRATION, COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION, COPILOT_VSCODE_PREVIEW_RELIABILITY_INTEGRATION, CreateDeviceAuthorizationResponseSchema, CURSOR_RELIABILITY_INTEGRATION, NATIVE_CODING_AGENT_NAMES, OPENCODE_RELIABILITY_INTEGRATION, OperationalMetricsResponseSchema, PollDeviceAuthorizationResponseSchema, POLYTOKEN_RELIABILITY_INTEGRATION, RELIABILITY_INTEGRATION_CONTRACT_VERSION, ReliabilityInvocationRecordSchema, WorkspaceIdentityResponseSchema, canonicalJson, deriveReliabilityInvocationHealth, isNativeCodingAgent, } from "@lore-co/core";
|
|
9
11
|
import { runHook, } from "./runtime.js";
|
|
12
|
+
import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
|
|
10
13
|
import { runAskCommand } from "./ask.js";
|
|
11
14
|
import { runGuardCommand } from "./guard.js";
|
|
12
15
|
import { runContinueCommand, runResumeCommand } from "./handoff.js";
|
|
@@ -18,6 +21,7 @@ import { runHostCommand } from "./host.js";
|
|
|
18
21
|
import { runDemoCommand } from "./demo.js";
|
|
19
22
|
import { updateCommand } from "./update.js";
|
|
20
23
|
import { runSelfHostCommand } from "./self-host.js";
|
|
24
|
+
import { ReliabilityStore, ReliabilityStoreError, integrationInvocationStateKey, } from "./reliability-store.js";
|
|
21
25
|
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
22
26
|
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
23
27
|
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
@@ -42,6 +46,30 @@ const POLYTOKEN_HOOK_EVENTS = [
|
|
|
42
46
|
"pre_user_prompt",
|
|
43
47
|
"post_model_turn",
|
|
44
48
|
];
|
|
49
|
+
const COPILOT_CLI_HOOK_EVENTS = [
|
|
50
|
+
"userPromptTransformed",
|
|
51
|
+
"preToolUse",
|
|
52
|
+
"agentStop",
|
|
53
|
+
"sessionEnd",
|
|
54
|
+
];
|
|
55
|
+
const EXTERNAL_SURFACES = [
|
|
56
|
+
{
|
|
57
|
+
surface: "copilot-cloud-mcp",
|
|
58
|
+
integrationId: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.integrationId,
|
|
59
|
+
installation: "repository_template",
|
|
60
|
+
locallyInspectable: false,
|
|
61
|
+
capabilities: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.capabilities,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const AGENT_MANIFESTS = {
|
|
65
|
+
claude: CLAUDE_RELIABILITY_INTEGRATION,
|
|
66
|
+
codex: CODEX_RELIABILITY_INTEGRATION,
|
|
67
|
+
"copilot-cli": COPILOT_CLI_RELIABILITY_INTEGRATION,
|
|
68
|
+
"copilot-vscode": COPILOT_VSCODE_PREVIEW_RELIABILITY_INTEGRATION,
|
|
69
|
+
cursor: CURSOR_RELIABILITY_INTEGRATION,
|
|
70
|
+
opencode: OPENCODE_RELIABILITY_INTEGRATION,
|
|
71
|
+
polytoken: POLYTOKEN_RELIABILITY_INTEGRATION,
|
|
72
|
+
};
|
|
45
73
|
const ROOT_HELP = `lore
|
|
46
74
|
Connect local coding agents to Lore shared engineering memory.
|
|
47
75
|
|
|
@@ -93,27 +121,32 @@ Examples:
|
|
|
93
121
|
lore devin --help
|
|
94
122
|
`;
|
|
95
123
|
const CONNECT_HELP = `lore connect
|
|
96
|
-
|
|
124
|
+
Authorize this device and idempotently install agent integrations.
|
|
97
125
|
|
|
98
126
|
Usage:
|
|
127
|
+
lore connect --login [options]
|
|
99
128
|
lore connect --token <token> [options]
|
|
100
129
|
|
|
101
130
|
Options:
|
|
102
131
|
--url <url> Lore API base URL (default: https://api.uselore.co)
|
|
103
132
|
--dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
|
|
133
|
+
--login Approve access securely in the Lore dashboard
|
|
134
|
+
--no-open Print the approval URL without opening a browser
|
|
104
135
|
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
105
|
-
--agent <name> claude, codex, cursor, opencode, polytoken, or t3code
|
|
136
|
+
--agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code
|
|
106
137
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
107
138
|
--json Print machine-readable output
|
|
108
139
|
--help Show this command's help
|
|
109
140
|
|
|
110
141
|
Examples:
|
|
142
|
+
lore connect --login --agent claude
|
|
143
|
+
lore connect --login --agent cowork
|
|
111
144
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
|
|
112
145
|
lore connect --url http://localhost:3004 --token dev-token --agent codex
|
|
113
146
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
|
|
114
147
|
`;
|
|
115
148
|
const STATUS_HELP = `lore status
|
|
116
|
-
Show
|
|
149
|
+
Show exact installation, declared capabilities, and last invocation health.
|
|
117
150
|
|
|
118
151
|
Usage:
|
|
119
152
|
lore status [--json]
|
|
@@ -123,7 +156,7 @@ Examples:
|
|
|
123
156
|
lore status --json
|
|
124
157
|
`;
|
|
125
158
|
const DOCTOR_HELP = `lore doctor
|
|
126
|
-
Check local security,
|
|
159
|
+
Check local security, exact wiring, invocation health, binaries, and Lore health.
|
|
127
160
|
|
|
128
161
|
Usage:
|
|
129
162
|
lore doctor [--json]
|
|
@@ -133,8 +166,9 @@ Examples:
|
|
|
133
166
|
lore doctor --json
|
|
134
167
|
`;
|
|
135
168
|
const DISCONNECT_HELP = `lore disconnect
|
|
136
|
-
Remove
|
|
137
|
-
|
|
169
|
+
Remove Lore-owned hooks/plugin, credentials, runtime, and transient state.
|
|
170
|
+
Durable captures, dead letters, verified snapshots, unrelated agent settings,
|
|
171
|
+
plugins, and Lore-created backups are retained.
|
|
138
172
|
|
|
139
173
|
Usage:
|
|
140
174
|
lore disconnect [--json]
|
|
@@ -157,16 +191,26 @@ export function getLorePaths(home, environment = process.env) {
|
|
|
157
191
|
? resolve(resolvedHome, ".config")
|
|
158
192
|
: resolve(environment.XDG_CONFIG_HOME);
|
|
159
193
|
const t3Home = resolve(environment.T3CODE_HOME?.trim() || resolve(resolvedHome, ".t3"));
|
|
194
|
+
const copilotHome = resolve(environment.COPILOT_HOME?.trim() || resolve(resolvedHome, ".copilot"));
|
|
160
195
|
return {
|
|
161
196
|
home: resolvedHome,
|
|
162
197
|
loreDirectory,
|
|
163
198
|
config: resolve(loreDirectory, "config.json"),
|
|
164
199
|
runtime: resolve(loreDirectory, "bin", "lore-hook.mjs"),
|
|
165
200
|
runtimeRepository: resolve(loreDirectory, "bin", "repository.js"),
|
|
201
|
+
runtimeReliabilityStore: resolve(loreDirectory, "bin", "reliability-store.js"),
|
|
202
|
+
runtimeSignedSnapshot: resolve(loreDirectory, "bin", "signed-snapshot.js"),
|
|
203
|
+
runtimeCanonicalJson: resolve(loreDirectory, "bin", "canonical-json.js"),
|
|
204
|
+
runtimeContextFallback: resolve(loreDirectory, "bin", "context-fallback.js"),
|
|
205
|
+
runtimeInvocationHealthWriter: resolve(loreDirectory, "bin", "invocation-health-writer.js"),
|
|
206
|
+
runtimeVersion: resolve(loreDirectory, "bin", "runtime-version.js"),
|
|
166
207
|
runtimePackage: resolve(loreDirectory, "bin", "package.json"),
|
|
167
208
|
state: resolve(loreDirectory, "state"),
|
|
168
209
|
queue: resolve(loreDirectory, "queue"),
|
|
169
210
|
codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
|
|
211
|
+
copilotCliHooks: resolve(copilotHome, "hooks", "lore.json"),
|
|
212
|
+
copilotVscodeHooks: resolve(environment.LORE_COPILOT_VSCODE_HOOKS?.trim() ||
|
|
213
|
+
resolve(process.cwd(), ".github", "hooks", "lore-vscode.json")),
|
|
170
214
|
claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
|
|
171
215
|
cursorHooks: resolve(resolvedHome, ".cursor", "hooks.json"),
|
|
172
216
|
cursorUserData: platform() === "darwin"
|
|
@@ -184,14 +228,18 @@ function isConfiguredAgent(value) {
|
|
|
184
228
|
return typeof value === "string" && isNativeCodingAgent(value);
|
|
185
229
|
}
|
|
186
230
|
function isConnectAgent(value) {
|
|
187
|
-
return isConfiguredAgent(value) ||
|
|
231
|
+
return (isConfiguredAgent(value) ||
|
|
232
|
+
value === "copilot" ||
|
|
233
|
+
value === "cowork" ||
|
|
234
|
+
value === "t3code");
|
|
188
235
|
}
|
|
189
|
-
function hookCommand(agent, paths) {
|
|
236
|
+
function hookCommand(agent, paths, event) {
|
|
190
237
|
const loreHome = `LORE_HOME=${shellQuote(paths.home)}`;
|
|
238
|
+
const hookEvent = event === undefined ? "" : ` LORE_HOOK_EVENT=${shellQuote(event)}`;
|
|
191
239
|
if (IS_STANDALONE_BINARY) {
|
|
192
|
-
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
240
|
+
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome}${hookEvent} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
193
241
|
}
|
|
194
|
-
return `env ${loreHome} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
242
|
+
return `env ${loreHome}${hookEvent} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
195
243
|
}
|
|
196
244
|
function isLoreHook(value) {
|
|
197
245
|
if (!isObject(value) || value.type !== "command") {
|
|
@@ -380,6 +428,169 @@ export function countLoreCursorHooks(input) {
|
|
|
380
428
|
return count + hooks[event].filter(isLoreHook).length;
|
|
381
429
|
}, 0);
|
|
382
430
|
}
|
|
431
|
+
function copilotCliEventHandler(event, paths) {
|
|
432
|
+
return {
|
|
433
|
+
type: "command",
|
|
434
|
+
bash: hookCommand("copilot-cli", paths, event),
|
|
435
|
+
timeoutSec: event === "sessionEnd"
|
|
436
|
+
? 2
|
|
437
|
+
: event === "agentStop"
|
|
438
|
+
? 3
|
|
439
|
+
: event === "preToolUse"
|
|
440
|
+
? 10
|
|
441
|
+
: 25,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function isLoreCopilotHook(value) {
|
|
445
|
+
if (!isObject(value) || value.type !== "command") {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const command = typeof value.bash === "string"
|
|
449
|
+
? value.bash
|
|
450
|
+
: typeof value.command === "string"
|
|
451
|
+
? value.command
|
|
452
|
+
: "";
|
|
453
|
+
return (/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
|
|
454
|
+
command.includes("/.lore/bin/lore-hook.mjs"));
|
|
455
|
+
}
|
|
456
|
+
function stripLoreFromCopilotEvent(value) {
|
|
457
|
+
return Array.isArray(value)
|
|
458
|
+
? value.filter((hook) => !isLoreCopilotHook(hook))
|
|
459
|
+
: [];
|
|
460
|
+
}
|
|
461
|
+
export function mergeLoreCopilotCliHooks(input, paths) {
|
|
462
|
+
const result = cloneObject(input);
|
|
463
|
+
if (result.version !== undefined && result.version !== 1) {
|
|
464
|
+
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
465
|
+
}
|
|
466
|
+
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
467
|
+
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
468
|
+
}
|
|
469
|
+
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
470
|
+
for (const event of COPILOT_CLI_HOOK_EVENTS) {
|
|
471
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
472
|
+
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
473
|
+
}
|
|
474
|
+
hooks[event] = [
|
|
475
|
+
...stripLoreFromCopilotEvent(hooks[event]),
|
|
476
|
+
copilotCliEventHandler(event, paths),
|
|
477
|
+
];
|
|
478
|
+
}
|
|
479
|
+
result.version = 1;
|
|
480
|
+
result.hooks = hooks;
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
export function removeLoreCopilotCliHooks(input) {
|
|
484
|
+
const result = cloneObject(input);
|
|
485
|
+
if (!isObject(result.hooks)) {
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
const hooks = { ...result.hooks };
|
|
489
|
+
for (const event of COPILOT_CLI_HOOK_EVENTS) {
|
|
490
|
+
if (!Array.isArray(hooks[event])) {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const remaining = stripLoreFromCopilotEvent(hooks[event]);
|
|
494
|
+
if (remaining.length === 0) {
|
|
495
|
+
delete hooks[event];
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
hooks[event] = remaining;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (Object.keys(hooks).length === 0) {
|
|
502
|
+
delete result.hooks;
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
result.hooks = hooks;
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
export function countLoreCopilotCliHooks(input) {
|
|
510
|
+
if (!isObject(input.hooks)) {
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
return COPILOT_CLI_HOOK_EVENTS.reduce((count, event) => {
|
|
514
|
+
const hooks = input.hooks;
|
|
515
|
+
if (!isObject(hooks) || !Array.isArray(hooks[event])) {
|
|
516
|
+
return count;
|
|
517
|
+
}
|
|
518
|
+
return count + hooks[event].filter(isLoreCopilotHook).length;
|
|
519
|
+
}, 0);
|
|
520
|
+
}
|
|
521
|
+
function copilotVscodeEventHandler(event, paths) {
|
|
522
|
+
return {
|
|
523
|
+
type: "command",
|
|
524
|
+
command: hookCommand("copilot-vscode", paths, event),
|
|
525
|
+
timeout: event === "SessionEnd"
|
|
526
|
+
? 2
|
|
527
|
+
: event === "Stop"
|
|
528
|
+
? 3
|
|
529
|
+
: event === "PreToolUse"
|
|
530
|
+
? 10
|
|
531
|
+
: 25,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
export function mergeLoreCopilotVscodeHooks(input, paths) {
|
|
535
|
+
const result = cloneObject(input);
|
|
536
|
+
if (result.version !== undefined && result.version !== 1) {
|
|
537
|
+
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
538
|
+
}
|
|
539
|
+
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
540
|
+
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
541
|
+
}
|
|
542
|
+
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
543
|
+
for (const event of HOOK_EVENTS) {
|
|
544
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
545
|
+
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
546
|
+
}
|
|
547
|
+
hooks[event] = [
|
|
548
|
+
...stripLoreFromCursorEvent(hooks[event]),
|
|
549
|
+
copilotVscodeEventHandler(event, paths),
|
|
550
|
+
];
|
|
551
|
+
}
|
|
552
|
+
result.version = 1;
|
|
553
|
+
result.hooks = hooks;
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
export function removeLoreCopilotVscodeHooks(input) {
|
|
557
|
+
const result = cloneObject(input);
|
|
558
|
+
if (!isObject(result.hooks)) {
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
const hooks = { ...result.hooks };
|
|
562
|
+
for (const event of HOOK_EVENTS) {
|
|
563
|
+
if (!Array.isArray(hooks[event])) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const remaining = stripLoreFromCursorEvent(hooks[event]);
|
|
567
|
+
if (remaining.length === 0) {
|
|
568
|
+
delete hooks[event];
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
hooks[event] = remaining;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (Object.keys(hooks).length === 0) {
|
|
575
|
+
delete result.hooks;
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
result.hooks = hooks;
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
export function countLoreCopilotVscodeHooks(input) {
|
|
583
|
+
if (!isObject(input.hooks)) {
|
|
584
|
+
return 0;
|
|
585
|
+
}
|
|
586
|
+
return HOOK_EVENTS.reduce((count, event) => {
|
|
587
|
+
const hooks = input.hooks;
|
|
588
|
+
if (!isObject(hooks) || !Array.isArray(hooks[event])) {
|
|
589
|
+
return count;
|
|
590
|
+
}
|
|
591
|
+
return count + hooks[event].filter(isLoreHook).length;
|
|
592
|
+
}, 0);
|
|
593
|
+
}
|
|
383
594
|
function polytokenEventHandler(event, paths) {
|
|
384
595
|
return {
|
|
385
596
|
name: `lore-${event.replaceAll("_", "-")}`,
|
|
@@ -452,6 +663,172 @@ export function countLoreOpenCodePlugins(input) {
|
|
|
452
663
|
? input.plugin.filter(isLoreOpenCodePlugin).length
|
|
453
664
|
: 0;
|
|
454
665
|
}
|
|
666
|
+
function installationUnitId(event, index, total) {
|
|
667
|
+
return total === 1 ? event : `installation-${index + 1}/${event}`;
|
|
668
|
+
}
|
|
669
|
+
function installationUnitState(ownedCount, exactCount) {
|
|
670
|
+
if (ownedCount === 0) {
|
|
671
|
+
return "missing";
|
|
672
|
+
}
|
|
673
|
+
if (ownedCount > 1 || exactCount > 1) {
|
|
674
|
+
return "duplicate";
|
|
675
|
+
}
|
|
676
|
+
return exactCount === 1 ? "exact" : "drifted";
|
|
677
|
+
}
|
|
678
|
+
function exactConfiguration(left, right) {
|
|
679
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
680
|
+
}
|
|
681
|
+
function commandHookInstallationUnits(input, agent, paths, index, total) {
|
|
682
|
+
const configuration = objectConfiguration(input, agent);
|
|
683
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
684
|
+
throw new Error(`${agent} configuration field "hooks" must be an object`);
|
|
685
|
+
}
|
|
686
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
687
|
+
return HOOK_EVENTS.map((event) => {
|
|
688
|
+
const groups = hooks[event];
|
|
689
|
+
if (groups !== undefined && !Array.isArray(groups)) {
|
|
690
|
+
throw new Error(`${agent} hook event "${event}" must be an array`);
|
|
691
|
+
}
|
|
692
|
+
const candidates = Array.isArray(groups) ? groups : [];
|
|
693
|
+
const expected = {
|
|
694
|
+
...(event === "PreToolUse" ? { matcher: GUARD_TOOL_MATCHER } : {}),
|
|
695
|
+
hooks: [eventHandler(agent, event, paths)],
|
|
696
|
+
};
|
|
697
|
+
const ownedCount = candidates.reduce((count, candidate) => {
|
|
698
|
+
if (!isObject(candidate) || !Array.isArray(candidate.hooks)) {
|
|
699
|
+
return count;
|
|
700
|
+
}
|
|
701
|
+
return count + candidate.hooks.filter(isLoreHook).length;
|
|
702
|
+
}, 0);
|
|
703
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
704
|
+
return {
|
|
705
|
+
id: installationUnitId(event, index, total),
|
|
706
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
function cursorInstallationUnits(input, paths, index, total) {
|
|
711
|
+
const configuration = objectConfiguration(input, "cursor");
|
|
712
|
+
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
713
|
+
throw new Error('Cursor configuration field "version" must be 1');
|
|
714
|
+
}
|
|
715
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
716
|
+
throw new Error('Cursor configuration field "hooks" must be an object');
|
|
717
|
+
}
|
|
718
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
719
|
+
return CURSOR_HOOK_EVENTS.map((event) => {
|
|
720
|
+
const values = hooks[event];
|
|
721
|
+
if (values !== undefined && !Array.isArray(values)) {
|
|
722
|
+
throw new Error(`Cursor hook event "${event}" must be an array`);
|
|
723
|
+
}
|
|
724
|
+
const candidates = Array.isArray(values) ? values : [];
|
|
725
|
+
const expected = cursorEventHandler(event, paths);
|
|
726
|
+
const ownedCount = candidates.filter(isLoreHook).length;
|
|
727
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
728
|
+
return {
|
|
729
|
+
id: installationUnitId(event, index, total),
|
|
730
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function copilotCliInstallationUnits(input, paths, index, total) {
|
|
735
|
+
const configuration = objectConfiguration(input, "copilot-cli");
|
|
736
|
+
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
737
|
+
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
738
|
+
}
|
|
739
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
740
|
+
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
741
|
+
}
|
|
742
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
743
|
+
return COPILOT_CLI_HOOK_EVENTS.map((event) => {
|
|
744
|
+
const values = hooks[event];
|
|
745
|
+
if (values !== undefined && !Array.isArray(values)) {
|
|
746
|
+
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
747
|
+
}
|
|
748
|
+
const candidates = Array.isArray(values) ? values : [];
|
|
749
|
+
const expected = copilotCliEventHandler(event, paths);
|
|
750
|
+
const ownedCount = candidates.filter(isLoreCopilotHook).length;
|
|
751
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
752
|
+
return {
|
|
753
|
+
id: installationUnitId(event, index, total),
|
|
754
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
759
|
+
const configuration = objectConfiguration(input, "copilot-vscode");
|
|
760
|
+
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
761
|
+
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
762
|
+
}
|
|
763
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
764
|
+
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
765
|
+
}
|
|
766
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
767
|
+
return HOOK_EVENTS.map((event) => {
|
|
768
|
+
const values = hooks[event];
|
|
769
|
+
if (values !== undefined && !Array.isArray(values)) {
|
|
770
|
+
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
771
|
+
}
|
|
772
|
+
const candidates = Array.isArray(values) ? values : [];
|
|
773
|
+
const expected = copilotVscodeEventHandler(event, paths);
|
|
774
|
+
const ownedCount = candidates.filter(isLoreHook).length;
|
|
775
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
776
|
+
return {
|
|
777
|
+
id: installationUnitId(event, index, total),
|
|
778
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
779
|
+
};
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function polytokenInstallationUnits(input, paths, index, total) {
|
|
783
|
+
if (!Array.isArray(input)) {
|
|
784
|
+
throw new Error("Polytoken hooks must contain a JSON array");
|
|
785
|
+
}
|
|
786
|
+
return POLYTOKEN_HOOK_EVENTS.map((event) => {
|
|
787
|
+
const expected = polytokenEventHandler(event, paths);
|
|
788
|
+
const owned = input.filter((candidate) => isLorePolytokenHook(candidate) &&
|
|
789
|
+
(!isObject(candidate) || candidate.event === event));
|
|
790
|
+
const exactCount = owned.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
791
|
+
return {
|
|
792
|
+
id: installationUnitId(event, index, total),
|
|
793
|
+
state: installationUnitState(owned.length, exactCount),
|
|
794
|
+
};
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function openCodeInstallationUnits(input, index, total) {
|
|
798
|
+
const configuration = objectConfiguration(input, "opencode");
|
|
799
|
+
if (configuration.plugin !== undefined &&
|
|
800
|
+
!Array.isArray(configuration.plugin)) {
|
|
801
|
+
throw new Error('OpenCode configuration field "plugin" must be an array');
|
|
802
|
+
}
|
|
803
|
+
const plugins = Array.isArray(configuration.plugin)
|
|
804
|
+
? configuration.plugin
|
|
805
|
+
: [];
|
|
806
|
+
const owned = plugins.filter(isLoreOpenCodePlugin);
|
|
807
|
+
const exactCount = owned.filter((candidate) => candidate === LORE_OPENCODE_PLUGIN).length;
|
|
808
|
+
return [
|
|
809
|
+
{
|
|
810
|
+
id: installationUnitId("plugin", index, total),
|
|
811
|
+
state: installationUnitState(owned.length, exactCount),
|
|
812
|
+
},
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
function inspectInstallationUnits(input, agent, paths, index, total) {
|
|
816
|
+
switch (agent) {
|
|
817
|
+
case "claude":
|
|
818
|
+
case "codex":
|
|
819
|
+
return commandHookInstallationUnits(input, agent, paths, index, total);
|
|
820
|
+
case "copilot-cli":
|
|
821
|
+
return copilotCliInstallationUnits(input, paths, index, total);
|
|
822
|
+
case "copilot-vscode":
|
|
823
|
+
return copilotVscodeInstallationUnits(input, paths, index, total);
|
|
824
|
+
case "cursor":
|
|
825
|
+
return cursorInstallationUnits(input, paths, index, total);
|
|
826
|
+
case "polytoken":
|
|
827
|
+
return polytokenInstallationUnits(input, paths, index, total);
|
|
828
|
+
case "opencode":
|
|
829
|
+
return openCodeInstallationUnits(input, index, total);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
455
832
|
async function readJsonDocument(path) {
|
|
456
833
|
try {
|
|
457
834
|
const [raw, metadata] = await Promise.all([
|
|
@@ -511,7 +888,7 @@ async function writeMergedJson(path, document, value, now) {
|
|
|
511
888
|
}
|
|
512
889
|
function parseConnectorConfig(value) {
|
|
513
890
|
if (!isObject(value) ||
|
|
514
|
-
value.version !== 1 ||
|
|
891
|
+
(value.version !== 1 && value.version !== 2) ||
|
|
515
892
|
typeof value.apiUrl !== "string" ||
|
|
516
893
|
typeof value.token !== "string" ||
|
|
517
894
|
!Array.isArray(value.agents) ||
|
|
@@ -547,10 +924,15 @@ function parseConnectorConfig(value) {
|
|
|
547
924
|
const dashboardUrl = typeof value.dashboardUrl === "string"
|
|
548
925
|
? normalizeApiUrl(value.dashboardUrl)
|
|
549
926
|
: undefined;
|
|
927
|
+
const parsedWorkspaceId = WorkspaceIdentityResponseSchema.shape.workspaceId.safeParse(value.workspaceId);
|
|
928
|
+
const workspaceId = parsedWorkspaceId.success
|
|
929
|
+
? parsedWorkspaceId.data
|
|
930
|
+
: undefined;
|
|
550
931
|
return {
|
|
551
|
-
version:
|
|
932
|
+
version: 2,
|
|
552
933
|
apiUrl: value.apiUrl,
|
|
553
934
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
935
|
+
...(workspaceId === undefined ? {} : { workspaceId }),
|
|
554
936
|
token: value.token,
|
|
555
937
|
agents,
|
|
556
938
|
...(agentConfigPaths === undefined ||
|
|
@@ -748,6 +1130,30 @@ const AGENT_TARGETS = {
|
|
|
748
1130
|
remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
|
|
749
1131
|
count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
|
|
750
1132
|
},
|
|
1133
|
+
"copilot-cli": {
|
|
1134
|
+
integration: "hooks",
|
|
1135
|
+
expected: COPILOT_CLI_HOOK_EVENTS.length,
|
|
1136
|
+
runtimeRequired: true,
|
|
1137
|
+
emptyValue: () => ({}),
|
|
1138
|
+
configPath: (paths) => paths.copilotCliHooks,
|
|
1139
|
+
detect: async (paths) => (await commandExists("copilot")) || pathExists(paths.copilotCliHooks),
|
|
1140
|
+
executable: async () => commandExists("copilot"),
|
|
1141
|
+
merge: (input, paths) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths),
|
|
1142
|
+
remove: (input) => removeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1143
|
+
count: (input) => countLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1144
|
+
},
|
|
1145
|
+
"copilot-vscode": {
|
|
1146
|
+
integration: "hooks",
|
|
1147
|
+
expected: HOOK_EVENTS.length,
|
|
1148
|
+
runtimeRequired: true,
|
|
1149
|
+
emptyValue: () => ({}),
|
|
1150
|
+
configPath: (paths) => paths.copilotVscodeHooks,
|
|
1151
|
+
detect: async (paths) => pathExists(paths.copilotVscodeHooks),
|
|
1152
|
+
executable: async () => commandExists("code"),
|
|
1153
|
+
merge: (input, paths) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths),
|
|
1154
|
+
remove: (input) => removeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1155
|
+
count: (input) => countLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1156
|
+
},
|
|
751
1157
|
cursor: {
|
|
752
1158
|
integration: "hooks",
|
|
753
1159
|
expected: CURSOR_HOOK_EVENTS.length,
|
|
@@ -793,7 +1199,10 @@ async function detectAgents(paths) {
|
|
|
793
1199
|
agent,
|
|
794
1200
|
detected: await targetFor(agent).detect(paths),
|
|
795
1201
|
})));
|
|
796
|
-
|
|
1202
|
+
const agents = detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
|
|
1203
|
+
return agents.includes("copilot-cli")
|
|
1204
|
+
? agents.filter((agent) => agent !== "copilot-vscode")
|
|
1205
|
+
: agents;
|
|
797
1206
|
}
|
|
798
1207
|
function installationKey(installation) {
|
|
799
1208
|
return `${installation.agent}\0${resolve(installation.configPath)}`;
|
|
@@ -953,7 +1362,14 @@ async function readT3CodeProviderInstallations(paths) {
|
|
|
953
1362
|
}
|
|
954
1363
|
}
|
|
955
1364
|
async function expandConnectInstallations(requested, paths) {
|
|
956
|
-
const direct = requested.flatMap((agent) =>
|
|
1365
|
+
const direct = requested.flatMap((agent) => {
|
|
1366
|
+
if (agent === "cowork" || agent === "t3code") {
|
|
1367
|
+
return [];
|
|
1368
|
+
}
|
|
1369
|
+
return [
|
|
1370
|
+
defaultInstallation(agent === "copilot" ? "copilot-cli" : agent, paths),
|
|
1371
|
+
];
|
|
1372
|
+
});
|
|
957
1373
|
if (!requested.includes("t3code")) {
|
|
958
1374
|
return { installations: direct, warnings: [] };
|
|
959
1375
|
}
|
|
@@ -979,20 +1395,65 @@ async function installRuntime(paths) {
|
|
|
979
1395
|
await Promise.all([
|
|
980
1396
|
rm(paths.runtime, { force: true }),
|
|
981
1397
|
rm(paths.runtimeRepository, { force: true }),
|
|
1398
|
+
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
1399
|
+
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
1400
|
+
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
1401
|
+
rm(paths.runtimeContextFallback, { force: true }),
|
|
1402
|
+
rm(paths.runtimeInvocationHealthWriter, { force: true }),
|
|
1403
|
+
rm(paths.runtimeVersion, { force: true }),
|
|
982
1404
|
rm(paths.runtimePackage, { force: true }),
|
|
983
1405
|
]);
|
|
984
1406
|
return;
|
|
985
1407
|
}
|
|
986
1408
|
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
987
|
-
const
|
|
1409
|
+
const canonicalJsonModule = fileURLToPath(import.meta.resolve("@lore-co/core/canonical-json"));
|
|
1410
|
+
const [runtime, repository, reliabilityStore, signedSnapshot, canonicalJson, contextFallback, invocationHealthWriter,] = await Promise.all([
|
|
988
1411
|
readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
|
|
989
1412
|
readFile(resolve(sourceDirectory, "repository.js"), "utf8"),
|
|
1413
|
+
readFile(resolve(sourceDirectory, "reliability-store.js"), "utf8"),
|
|
1414
|
+
readFile(resolve(sourceDirectory, "signed-snapshot.js"), "utf8"),
|
|
1415
|
+
readFile(canonicalJsonModule, "utf8"),
|
|
1416
|
+
readFile(resolve(sourceDirectory, "context-fallback.js"), "utf8"),
|
|
1417
|
+
readFile(resolve(sourceDirectory, "invocation-health-writer.js"), "utf8"),
|
|
990
1418
|
]);
|
|
1419
|
+
const standaloneSignedSnapshot = signedSnapshot.replaceAll("@lore-co/core/canonical-json", "./canonical-json.js");
|
|
1420
|
+
if (standaloneSignedSnapshot === signedSnapshot) {
|
|
1421
|
+
throw new Error("Signed snapshot runtime dependency could not be localized");
|
|
1422
|
+
}
|
|
1423
|
+
const runtimeVersion = `export const RUNTIME_VERSION = ${JSON.stringify(LORE_VERSION)};\n`;
|
|
1424
|
+
const runtimeFiles = {
|
|
1425
|
+
"lore-hook.mjs": runtime,
|
|
1426
|
+
"repository.js": repository,
|
|
1427
|
+
"reliability-store.js": reliabilityStore,
|
|
1428
|
+
"signed-snapshot.js": standaloneSignedSnapshot,
|
|
1429
|
+
"canonical-json.js": canonicalJson,
|
|
1430
|
+
"context-fallback.js": contextFallback,
|
|
1431
|
+
"invocation-health-writer.js": invocationHealthWriter,
|
|
1432
|
+
"runtime-version.js": runtimeVersion,
|
|
1433
|
+
};
|
|
1434
|
+
const runtimeManifest = {
|
|
1435
|
+
type: "module",
|
|
1436
|
+
loreRuntime: {
|
|
1437
|
+
version: LORE_VERSION,
|
|
1438
|
+
files: Object.fromEntries(Object.entries(runtimeFiles).map(([name, contents]) => [
|
|
1439
|
+
name,
|
|
1440
|
+
createHash("sha256").update(contents).digest("hex"),
|
|
1441
|
+
])),
|
|
1442
|
+
},
|
|
1443
|
+
};
|
|
991
1444
|
await Promise.all([
|
|
992
|
-
atomicWrite(paths.runtime, runtime, 0o700),
|
|
993
1445
|
atomicWrite(paths.runtimeRepository, repository, 0o600),
|
|
994
|
-
atomicWrite(paths.
|
|
1446
|
+
atomicWrite(paths.runtimeReliabilityStore, reliabilityStore, 0o600),
|
|
1447
|
+
atomicWrite(paths.runtimeSignedSnapshot, standaloneSignedSnapshot, 0o600),
|
|
1448
|
+
atomicWrite(paths.runtimeCanonicalJson, canonicalJson, 0o600),
|
|
1449
|
+
atomicWrite(paths.runtimeContextFallback, contextFallback, 0o600),
|
|
1450
|
+
atomicWrite(paths.runtimeInvocationHealthWriter, invocationHealthWriter, 0o600),
|
|
1451
|
+
atomicWrite(paths.runtimeVersion, runtimeVersion, 0o600),
|
|
1452
|
+
atomicWrite(paths.runtimePackage, `${JSON.stringify(runtimeManifest, null, 2)}\n`, 0o600),
|
|
995
1453
|
]);
|
|
1454
|
+
// The hook entrypoint is the generation activation marker. Updating it last
|
|
1455
|
+
// prevents a new runtime from loading before all of its local modules exist.
|
|
1456
|
+
await atomicWrite(paths.runtime, runtime, 0o700);
|
|
996
1457
|
}
|
|
997
1458
|
function agentConfigPath(agent, paths) {
|
|
998
1459
|
return targetFor(agent).configPath(paths);
|
|
@@ -1023,7 +1484,12 @@ function valueAfter(args, index, flag) {
|
|
|
1023
1484
|
return [value, index + 1];
|
|
1024
1485
|
}
|
|
1025
1486
|
function parseConnectArguments(args) {
|
|
1026
|
-
const parsed = {
|
|
1487
|
+
const parsed = {
|
|
1488
|
+
agents: [],
|
|
1489
|
+
json: false,
|
|
1490
|
+
login: false,
|
|
1491
|
+
openBrowser: true,
|
|
1492
|
+
};
|
|
1027
1493
|
for (let index = 0; index < args.length; index += 1) {
|
|
1028
1494
|
const argument = args[index];
|
|
1029
1495
|
if (argument === "--help" || argument === "-h") {
|
|
@@ -1034,6 +1500,14 @@ function parseConnectArguments(args) {
|
|
|
1034
1500
|
parsed.json = true;
|
|
1035
1501
|
continue;
|
|
1036
1502
|
}
|
|
1503
|
+
if (argument === "--login") {
|
|
1504
|
+
parsed.login = true;
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (argument === "--no-open") {
|
|
1508
|
+
parsed.openBrowser = false;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1037
1511
|
if (argument !== "--url" &&
|
|
1038
1512
|
argument !== "--dashboard-url" &&
|
|
1039
1513
|
argument !== "--token" &&
|
|
@@ -1063,9 +1537,17 @@ function parseConnectArguments(args) {
|
|
|
1063
1537
|
parsed.agents.push(value);
|
|
1064
1538
|
}
|
|
1065
1539
|
else {
|
|
1066
|
-
throw new Error("--agent must be claude, codex, cursor, opencode, polytoken, or t3code");
|
|
1540
|
+
throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code");
|
|
1067
1541
|
}
|
|
1068
1542
|
}
|
|
1543
|
+
if (parsed.login && parsed.token !== undefined) {
|
|
1544
|
+
throw new Error("--login and --token cannot be used together");
|
|
1545
|
+
}
|
|
1546
|
+
if ((parsed.agents.includes("copilot-cli") ||
|
|
1547
|
+
parsed.agents.includes("copilot")) &&
|
|
1548
|
+
parsed.agents.includes("copilot-vscode")) {
|
|
1549
|
+
throw new Error("Install either copilot-cli or copilot-vscode in one connection. Both hosts load repository hooks and would invoke Lore twice.");
|
|
1550
|
+
}
|
|
1069
1551
|
return parsed;
|
|
1070
1552
|
}
|
|
1071
1553
|
function parseOutputArguments(args, help, command) {
|
|
@@ -1086,38 +1568,26 @@ function parseOutputArguments(args, help, command) {
|
|
|
1086
1568
|
function writeResult(value, json, text) {
|
|
1087
1569
|
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : text);
|
|
1088
1570
|
}
|
|
1089
|
-
async function
|
|
1090
|
-
const parsed = parseConnectArguments(args);
|
|
1091
|
-
if (parsed === null) {
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1571
|
+
export async function connectWithCredential(options) {
|
|
1094
1572
|
if (platform() !== "darwin" && platform() !== "linux") {
|
|
1095
1573
|
throw new Error("Lore agent integrations currently support macOS and Linux");
|
|
1096
1574
|
}
|
|
1097
1575
|
const paths = getLorePaths();
|
|
1098
1576
|
const existing = await readConnectorConfig(paths);
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
: { dashboardUrl: parsed.dashboardUrl }),
|
|
1107
|
-
environment: process.env,
|
|
1108
|
-
existing,
|
|
1109
|
-
preferHostedDefault: hasProvidedToken,
|
|
1110
|
-
});
|
|
1111
|
-
const token = configuredToken(parsed.token, existing);
|
|
1112
|
-
if (token === undefined || token.trim() === "") {
|
|
1113
|
-
throw new Error("Workspace token is required. Use --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
|
|
1577
|
+
const apiUrl = normalizeApiUrl(options.apiUrl);
|
|
1578
|
+
const dashboardUrl = options.dashboardUrl === undefined
|
|
1579
|
+
? undefined
|
|
1580
|
+
: normalizeApiUrl(options.dashboardUrl);
|
|
1581
|
+
const token = options.token.trim();
|
|
1582
|
+
if (token === "") {
|
|
1583
|
+
throw new Error("Workspace token cannot be empty");
|
|
1114
1584
|
}
|
|
1115
|
-
const timeoutMs =
|
|
1585
|
+
const timeoutMs = options.timeoutMs ?? existing?.timeoutMs ?? 2_500;
|
|
1116
1586
|
const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
|
|
1117
|
-
const requestedExpansion =
|
|
1587
|
+
const requestedExpansion = options.agents.length === 0
|
|
1118
1588
|
? { installations: [], warnings: [] }
|
|
1119
|
-
: await expandConnectInstallations(
|
|
1120
|
-
const detectedAgents =
|
|
1589
|
+
: await expandConnectInstallations(options.agents, paths);
|
|
1590
|
+
const detectedAgents = options.agents.length === 0 ? await detectAgents(paths) : [];
|
|
1121
1591
|
const installations = uniqueInstallations([
|
|
1122
1592
|
...configuredInstallations(existing, paths),
|
|
1123
1593
|
...requestedExpansion.installations,
|
|
@@ -1126,8 +1596,9 @@ async function connectCommand(args) {
|
|
|
1126
1596
|
const agents = [
|
|
1127
1597
|
...new Set(installations.map((installation) => installation.agent)),
|
|
1128
1598
|
].sort();
|
|
1129
|
-
|
|
1130
|
-
|
|
1599
|
+
const credentialOnlyCowork = options.agents.includes("cowork");
|
|
1600
|
+
if (agents.length === 0 && !credentialOnlyCowork) {
|
|
1601
|
+
throw new Error("No Claude, Codex, Copilot CLI, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
|
|
1131
1602
|
}
|
|
1132
1603
|
const now = new Date();
|
|
1133
1604
|
const documents = new Map();
|
|
@@ -1165,27 +1636,178 @@ async function connectCommand(args) {
|
|
|
1165
1636
|
.map((installation) => installation.configPath);
|
|
1166
1637
|
}
|
|
1167
1638
|
const config = {
|
|
1168
|
-
version:
|
|
1639
|
+
version: 2,
|
|
1169
1640
|
apiUrl,
|
|
1170
1641
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
1642
|
+
workspaceId: identity.workspaceId,
|
|
1171
1643
|
token: token.trim(),
|
|
1172
1644
|
agents,
|
|
1173
1645
|
agentConfigPaths,
|
|
1174
1646
|
connectedAt: existing?.connectedAt ?? now.toISOString(),
|
|
1175
1647
|
timeoutMs,
|
|
1176
1648
|
};
|
|
1649
|
+
const reliabilityStore = new ReliabilityStore(identity.workspaceId, {
|
|
1650
|
+
home: paths.home,
|
|
1651
|
+
});
|
|
1652
|
+
await reliabilityStore.initialize();
|
|
1653
|
+
await reliabilityStore.migrateLegacyQueue(paths.queue);
|
|
1177
1654
|
await atomicWrite(paths.config, `${JSON.stringify(config, null, 2)}\n`, 0o600);
|
|
1178
|
-
|
|
1655
|
+
if (existing !== null &&
|
|
1656
|
+
existing.workspaceId === undefined &&
|
|
1657
|
+
existing.apiUrl === apiUrl &&
|
|
1658
|
+
existing.token === token.trim()) {
|
|
1659
|
+
const credentialStore = new ReliabilityStore(credentialReliabilityWorkspaceKey(apiUrl, token.trim()), { home: paths.home });
|
|
1660
|
+
await credentialStore.transferPendingTo(reliabilityStore);
|
|
1661
|
+
}
|
|
1662
|
+
await reliabilityStore.releaseAuthBlocked(now);
|
|
1663
|
+
return {
|
|
1179
1664
|
connected: true,
|
|
1180
1665
|
apiUrl: config.apiUrl,
|
|
1181
1666
|
identity,
|
|
1182
|
-
agents,
|
|
1667
|
+
agents: credentialOnlyCowork ? [...agents, "cowork"] : agents,
|
|
1183
1668
|
config: paths.config,
|
|
1184
1669
|
changedHookFiles,
|
|
1185
1670
|
backups,
|
|
1186
1671
|
warnings: requestedExpansion.warnings,
|
|
1187
1672
|
};
|
|
1188
|
-
|
|
1673
|
+
}
|
|
1674
|
+
async function requestDeviceAuthorization(input) {
|
|
1675
|
+
let response;
|
|
1676
|
+
try {
|
|
1677
|
+
response = await fetch(`${input.apiUrl}/v1/device-authorizations`, {
|
|
1678
|
+
method: "POST",
|
|
1679
|
+
headers: {
|
|
1680
|
+
accept: "application/json",
|
|
1681
|
+
"content-type": "application/json",
|
|
1682
|
+
},
|
|
1683
|
+
body: JSON.stringify({
|
|
1684
|
+
clientName: `Lore CLI on ${hostname()}`,
|
|
1685
|
+
agents: input.agents,
|
|
1686
|
+
expiresInDays: 90,
|
|
1687
|
+
}),
|
|
1688
|
+
signal: AbortSignal.timeout(10_000),
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
catch {
|
|
1692
|
+
throw new Error("Lore device authorization is unreachable. Verify --url and retry.");
|
|
1693
|
+
}
|
|
1694
|
+
if (!response.ok) {
|
|
1695
|
+
throw new Error(response.status === 404
|
|
1696
|
+
? "This Lore server does not support browser approval. Upgrade the server or use --token."
|
|
1697
|
+
: "Lore could not start browser approval. Retry in a moment.");
|
|
1698
|
+
}
|
|
1699
|
+
const authorization = CreateDeviceAuthorizationResponseSchema.safeParse(await response.json().catch(() => null));
|
|
1700
|
+
if (!authorization.success) {
|
|
1701
|
+
throw new Error("Lore returned an incompatible device authorization response.");
|
|
1702
|
+
}
|
|
1703
|
+
const serverApprovalUrl = new URL(authorization.data.verificationUriComplete);
|
|
1704
|
+
const approvalUrl = input.dashboardUrl === undefined
|
|
1705
|
+
? serverApprovalUrl.toString()
|
|
1706
|
+
: new URL(`${serverApprovalUrl.pathname}${serverApprovalUrl.search}`, `${input.dashboardUrl}/`).toString();
|
|
1707
|
+
process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approvalUrl}\n`);
|
|
1708
|
+
if (input.openBrowser) {
|
|
1709
|
+
const command = platform() === "darwin" ? "open" : "xdg-open";
|
|
1710
|
+
const child = spawn(command, [approvalUrl], {
|
|
1711
|
+
detached: true,
|
|
1712
|
+
stdio: "ignore",
|
|
1713
|
+
});
|
|
1714
|
+
child.once("error", () => {
|
|
1715
|
+
// The printed URL remains the safe fallback.
|
|
1716
|
+
});
|
|
1717
|
+
child.unref();
|
|
1718
|
+
}
|
|
1719
|
+
const expiresAt = Date.parse(authorization.data.expiresAt);
|
|
1720
|
+
const intervalMs = authorization.data.intervalSeconds * 1_000;
|
|
1721
|
+
while (Date.now() < expiresAt) {
|
|
1722
|
+
await delay(intervalMs);
|
|
1723
|
+
let pollResponse;
|
|
1724
|
+
try {
|
|
1725
|
+
pollResponse = await fetch(`${input.apiUrl}/v1/device-authorizations/poll`, {
|
|
1726
|
+
method: "POST",
|
|
1727
|
+
headers: {
|
|
1728
|
+
accept: "application/json",
|
|
1729
|
+
"content-type": "application/json",
|
|
1730
|
+
},
|
|
1731
|
+
body: JSON.stringify({
|
|
1732
|
+
deviceCode: authorization.data.deviceCode,
|
|
1733
|
+
}),
|
|
1734
|
+
signal: AbortSignal.timeout(10_000),
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
catch {
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
if (pollResponse.status === 429) {
|
|
1741
|
+
const retryAfter = Number(pollResponse.headers.get("retry-after"));
|
|
1742
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
1743
|
+
await delay(retryAfter * 1_000);
|
|
1744
|
+
}
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
if (!pollResponse.ok) {
|
|
1748
|
+
throw new Error("Lore browser approval failed. Run connect --login again.");
|
|
1749
|
+
}
|
|
1750
|
+
const result = PollDeviceAuthorizationResponseSchema.safeParse(await pollResponse.json().catch(() => null));
|
|
1751
|
+
if (!result.success) {
|
|
1752
|
+
throw new Error("Lore returned an incompatible device authorization response.");
|
|
1753
|
+
}
|
|
1754
|
+
if (result.data.status === "pending") {
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
if (result.data.status === "approved") {
|
|
1758
|
+
return result.data.token;
|
|
1759
|
+
}
|
|
1760
|
+
if (result.data.status === "denied") {
|
|
1761
|
+
throw new Error("Lore access was denied in the dashboard.");
|
|
1762
|
+
}
|
|
1763
|
+
throw new Error("Lore browser approval expired. Run connect --login again.");
|
|
1764
|
+
}
|
|
1765
|
+
throw new Error("Lore browser approval expired. Run connect --login again.");
|
|
1766
|
+
}
|
|
1767
|
+
async function connectCommand(args) {
|
|
1768
|
+
const parsed = parseConnectArguments(args);
|
|
1769
|
+
if (parsed === null) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const paths = getLorePaths();
|
|
1773
|
+
const existing = await readConnectorConfig(paths);
|
|
1774
|
+
const hasProvidedToken = parsed.token !== undefined ||
|
|
1775
|
+
(process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
|
|
1776
|
+
(process.env.LORE_TOKEN?.trim() ?? "") !== "";
|
|
1777
|
+
const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
|
|
1778
|
+
...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
|
|
1779
|
+
...(parsed.dashboardUrl === undefined
|
|
1780
|
+
? {}
|
|
1781
|
+
: { dashboardUrl: parsed.dashboardUrl }),
|
|
1782
|
+
environment: process.env,
|
|
1783
|
+
existing,
|
|
1784
|
+
preferHostedDefault: parsed.login || hasProvidedToken,
|
|
1785
|
+
});
|
|
1786
|
+
const configured = parsed.login
|
|
1787
|
+
? undefined
|
|
1788
|
+
: configuredToken(parsed.token, existing);
|
|
1789
|
+
if (parsed.login && hasProvidedToken) {
|
|
1790
|
+
throw new Error("--login cannot be combined with an explicit or environment workspace token. Unset it or use the token directly.");
|
|
1791
|
+
}
|
|
1792
|
+
const token = parsed.login
|
|
1793
|
+
? await requestDeviceAuthorization({
|
|
1794
|
+
apiUrl,
|
|
1795
|
+
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
1796
|
+
agents: parsed.agents,
|
|
1797
|
+
openBrowser: parsed.openBrowser,
|
|
1798
|
+
})
|
|
1799
|
+
: configured;
|
|
1800
|
+
if (token === undefined || token.trim() === "") {
|
|
1801
|
+
throw new Error("Workspace token is required. Use --login, --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
|
|
1802
|
+
}
|
|
1803
|
+
const result = await connectWithCredential({
|
|
1804
|
+
apiUrl,
|
|
1805
|
+
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
1806
|
+
token,
|
|
1807
|
+
agents: parsed.agents,
|
|
1808
|
+
...(parsed.timeoutMs === undefined ? {} : { timeoutMs: parsed.timeoutMs }),
|
|
1809
|
+
});
|
|
1810
|
+
writeResult(result, parsed.json, `connected: ${result.agents.join(", ")}\napi_url: ${result.apiUrl}\nconfig: ${result.config}\n${result.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
|
|
1189
1811
|
}
|
|
1190
1812
|
async function queueCount(paths) {
|
|
1191
1813
|
try {
|
|
@@ -1195,34 +1817,250 @@ async function queueCount(paths) {
|
|
|
1195
1817
|
return 0;
|
|
1196
1818
|
}
|
|
1197
1819
|
}
|
|
1198
|
-
|
|
1820
|
+
function reliabilityWorkspaceKey(config) {
|
|
1821
|
+
return (config.workspaceId ??
|
|
1822
|
+
credentialReliabilityWorkspaceKey(config.apiUrl, config.token));
|
|
1823
|
+
}
|
|
1824
|
+
function credentialReliabilityWorkspaceKey(apiUrl, token) {
|
|
1825
|
+
return `credential-${createHash("sha256")
|
|
1826
|
+
.update(`${apiUrl}\0${token}`)
|
|
1827
|
+
.digest("hex")
|
|
1828
|
+
.slice(0, 32)}`;
|
|
1829
|
+
}
|
|
1830
|
+
async function inspectReliabilityStore(config, paths) {
|
|
1831
|
+
if (config === null) {
|
|
1832
|
+
return { inspection: null, error: null };
|
|
1833
|
+
}
|
|
1834
|
+
try {
|
|
1835
|
+
const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
|
|
1836
|
+
home: paths.home,
|
|
1837
|
+
});
|
|
1838
|
+
return { inspection: await store.inspectExisting(), error: null };
|
|
1839
|
+
}
|
|
1840
|
+
catch (error) {
|
|
1841
|
+
return {
|
|
1842
|
+
inspection: null,
|
|
1843
|
+
error: error instanceof Error
|
|
1844
|
+
? error.message
|
|
1845
|
+
: "Local reliability store is unavailable",
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
const MAX_RUNTIME_MODULE_BYTES = 2 * 1024 * 1024;
|
|
1850
|
+
const MAX_RUNTIME_MANIFEST_BYTES = 64 * 1024;
|
|
1851
|
+
async function readRuntimeFile(path, maxBytes, executable = false) {
|
|
1852
|
+
const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
1853
|
+
try {
|
|
1854
|
+
const metadata = await handle.stat();
|
|
1855
|
+
if (!metadata.isFile() ||
|
|
1856
|
+
metadata.size > maxBytes ||
|
|
1857
|
+
(executable && (metadata.mode & 0o111) === 0)) {
|
|
1858
|
+
throw new Error("Runtime file is unsafe or exceeds its size limit");
|
|
1859
|
+
}
|
|
1860
|
+
const buffer = Buffer.allocUnsafe(maxBytes + 1);
|
|
1861
|
+
let bytes = 0;
|
|
1862
|
+
while (bytes < buffer.length) {
|
|
1863
|
+
const result = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
1864
|
+
if (result.bytesRead === 0) {
|
|
1865
|
+
break;
|
|
1866
|
+
}
|
|
1867
|
+
bytes += result.bytesRead;
|
|
1868
|
+
}
|
|
1869
|
+
if (bytes > maxBytes) {
|
|
1870
|
+
throw new Error("Runtime file exceeds its size limit");
|
|
1871
|
+
}
|
|
1872
|
+
return buffer.subarray(0, bytes).toString("utf8");
|
|
1873
|
+
}
|
|
1874
|
+
finally {
|
|
1875
|
+
await handle.close();
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async function inspectRuntimeInstallation(required, paths) {
|
|
1879
|
+
if (!required) {
|
|
1880
|
+
return { state: "not_required", version: null };
|
|
1881
|
+
}
|
|
1882
|
+
if (IS_STANDALONE_BINARY) {
|
|
1883
|
+
try {
|
|
1884
|
+
await access(process.execPath, fsConstants.R_OK | fsConstants.X_OK);
|
|
1885
|
+
return { state: "installed", version: LORE_VERSION };
|
|
1886
|
+
}
|
|
1887
|
+
catch {
|
|
1888
|
+
return { state: "missing", version: null };
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const runtimeFiles = {
|
|
1892
|
+
"lore-hook.mjs": paths.runtime,
|
|
1893
|
+
"repository.js": paths.runtimeRepository,
|
|
1894
|
+
"reliability-store.js": paths.runtimeReliabilityStore,
|
|
1895
|
+
"signed-snapshot.js": paths.runtimeSignedSnapshot,
|
|
1896
|
+
"canonical-json.js": paths.runtimeCanonicalJson,
|
|
1897
|
+
"context-fallback.js": paths.runtimeContextFallback,
|
|
1898
|
+
"invocation-health-writer.js": paths.runtimeInvocationHealthWriter,
|
|
1899
|
+
"runtime-version.js": paths.runtimeVersion,
|
|
1900
|
+
};
|
|
1901
|
+
let contents;
|
|
1902
|
+
let runtimePackage;
|
|
1903
|
+
try {
|
|
1904
|
+
[contents, runtimePackage] = await Promise.all([
|
|
1905
|
+
Promise.all(Object.entries(runtimeFiles).map(async ([name, path]) => {
|
|
1906
|
+
return [
|
|
1907
|
+
name,
|
|
1908
|
+
await readRuntimeFile(path, MAX_RUNTIME_MODULE_BYTES, name === "lore-hook.mjs"),
|
|
1909
|
+
];
|
|
1910
|
+
})),
|
|
1911
|
+
readRuntimeFile(paths.runtimePackage, MAX_RUNTIME_MANIFEST_BYTES),
|
|
1912
|
+
]);
|
|
1913
|
+
}
|
|
1914
|
+
catch (error) {
|
|
1915
|
+
const missing = typeof error === "object" &&
|
|
1916
|
+
error !== null &&
|
|
1917
|
+
"code" in error &&
|
|
1918
|
+
error.code === "ENOENT";
|
|
1919
|
+
return { state: missing ? "missing" : "stale", version: null };
|
|
1920
|
+
}
|
|
1921
|
+
let parsed;
|
|
1922
|
+
try {
|
|
1923
|
+
parsed = JSON.parse(runtimePackage);
|
|
1924
|
+
}
|
|
1925
|
+
catch {
|
|
1926
|
+
return { state: "stale", version: null };
|
|
1927
|
+
}
|
|
1928
|
+
const loreRuntime = isObject(parsed) && isObject(parsed.loreRuntime)
|
|
1929
|
+
? parsed.loreRuntime
|
|
1930
|
+
: null;
|
|
1931
|
+
const hashes = loreRuntime !== null && isObject(loreRuntime.files)
|
|
1932
|
+
? loreRuntime.files
|
|
1933
|
+
: null;
|
|
1934
|
+
const version = loreRuntime !== null && typeof loreRuntime.version === "string"
|
|
1935
|
+
? loreRuntime.version
|
|
1936
|
+
: null;
|
|
1937
|
+
const hashEntries = hashes === null ? [] : Object.keys(hashes).sort();
|
|
1938
|
+
const expectedEntries = Object.keys(runtimeFiles).sort();
|
|
1939
|
+
const hashesValid = hashes !== null &&
|
|
1940
|
+
canonicalJson(hashEntries) === canonicalJson(expectedEntries) &&
|
|
1941
|
+
contents.every(([name, content]) => hashes[name] ===
|
|
1942
|
+
createHash("sha256").update(content).digest("hex"));
|
|
1943
|
+
return version === LORE_VERSION && hashesValid
|
|
1944
|
+
? { state: "installed", version }
|
|
1945
|
+
: { state: "stale", version };
|
|
1946
|
+
}
|
|
1947
|
+
async function invocationHealth(config, paths, manifest) {
|
|
1948
|
+
if (config === null) {
|
|
1949
|
+
return {
|
|
1950
|
+
health: deriveReliabilityInvocationHealth(null, manifest.integrationId),
|
|
1951
|
+
error: null,
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
|
|
1956
|
+
home: paths.home,
|
|
1957
|
+
});
|
|
1958
|
+
const record = await store.readStateExisting(integrationInvocationStateKey(manifest.integrationId));
|
|
1959
|
+
const parsed = ReliabilityInvocationRecordSchema.safeParse(record);
|
|
1960
|
+
if (record !== null &&
|
|
1961
|
+
(!parsed.success ||
|
|
1962
|
+
parsed.data.integrationId !== manifest.integrationId)) {
|
|
1963
|
+
return {
|
|
1964
|
+
health: deriveReliabilityInvocationHealth(undefined, manifest.integrationId),
|
|
1965
|
+
error: "corrupt_record",
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
const health = deriveReliabilityInvocationHealth(record, manifest.integrationId);
|
|
1969
|
+
return {
|
|
1970
|
+
health,
|
|
1971
|
+
error: manifest.integrationId === "plugin/opencode" &&
|
|
1972
|
+
health.runtimeVersion !== null &&
|
|
1973
|
+
health.runtimeVersion !== LORE_VERSION
|
|
1974
|
+
? "runtime_version_mismatch"
|
|
1975
|
+
: null,
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
catch (error) {
|
|
1979
|
+
return {
|
|
1980
|
+
health: deriveReliabilityInvocationHealth(undefined, manifest.integrationId),
|
|
1981
|
+
error: error instanceof ReliabilityStoreError
|
|
1982
|
+
? error.code.toLowerCase()
|
|
1983
|
+
: "unavailable",
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
async function getAgentStatus(agent, config, paths, runtime) {
|
|
1199
1988
|
const target = targetFor(agent);
|
|
1989
|
+
const manifest = AGENT_MANIFESTS[agent];
|
|
1200
1990
|
const configuredPaths = config?.agentConfigPaths?.[agent];
|
|
1201
1991
|
const configPaths = config?.agents.includes(agent) === true && configuredPaths !== undefined
|
|
1202
1992
|
? configuredPaths
|
|
1203
1993
|
: [agentConfigPath(agent, paths)];
|
|
1204
1994
|
let installed = 0;
|
|
1205
1995
|
let existing = 0;
|
|
1206
|
-
|
|
1996
|
+
let invalidConfiguration = false;
|
|
1997
|
+
const units = [];
|
|
1998
|
+
for (const [index, path] of configPaths.entries()) {
|
|
1207
1999
|
try {
|
|
1208
2000
|
const document = await readJsonDocument(path);
|
|
1209
2001
|
installed += countAgentIntegrations(document.value, agent);
|
|
1210
2002
|
existing += document.exists ? 1 : 0;
|
|
2003
|
+
units.push(...inspectInstallationUnits(document.value === undefined
|
|
2004
|
+
? target.emptyValue()
|
|
2005
|
+
: document.value, agent, paths, index, configPaths.length));
|
|
1211
2006
|
}
|
|
1212
2007
|
catch {
|
|
1213
|
-
|
|
2008
|
+
invalidConfiguration = true;
|
|
1214
2009
|
}
|
|
1215
2010
|
}
|
|
2011
|
+
const invocation = await invocationHealth(config, paths, manifest);
|
|
2012
|
+
const stalePluginRuntime = agent === "opencode" &&
|
|
2013
|
+
invocation.error === "runtime_version_mismatch";
|
|
2014
|
+
const agentRuntime = target.runtimeRequired
|
|
2015
|
+
? runtime
|
|
2016
|
+
: stalePluginRuntime
|
|
2017
|
+
? {
|
|
2018
|
+
state: "stale",
|
|
2019
|
+
version: invocation.health.runtimeVersion,
|
|
2020
|
+
}
|
|
2021
|
+
: {
|
|
2022
|
+
state: "not_required",
|
|
2023
|
+
version: null,
|
|
2024
|
+
};
|
|
2025
|
+
const expected = target.expected * configPaths.length;
|
|
2026
|
+
const exactUnits = units.filter((unit) => unit.state === "exact").length;
|
|
2027
|
+
const installationState = invalidConfiguration
|
|
2028
|
+
? "invalid"
|
|
2029
|
+
: installed === 0
|
|
2030
|
+
? "missing"
|
|
2031
|
+
: agentRuntime.state === "stale"
|
|
2032
|
+
? "stale"
|
|
2033
|
+
: exactUnits === expected &&
|
|
2034
|
+
agentRuntime.state !== "missing"
|
|
2035
|
+
? "complete"
|
|
2036
|
+
: "partial";
|
|
2037
|
+
const executable = await target.executable(paths);
|
|
1216
2038
|
return {
|
|
1217
2039
|
agent,
|
|
2040
|
+
integrationId: manifest.integrationId,
|
|
1218
2041
|
configured: config?.agents.includes(agent) ?? false,
|
|
1219
|
-
executable
|
|
2042
|
+
executable,
|
|
1220
2043
|
configExists: existing === configPaths.length,
|
|
1221
2044
|
configFile: configPaths[0] ?? agentConfigPath(agent, paths),
|
|
1222
2045
|
...(configPaths.length > 1 ? { configFiles: configPaths } : {}),
|
|
1223
2046
|
integration: target.integration,
|
|
1224
2047
|
installed,
|
|
1225
|
-
expected
|
|
2048
|
+
expected,
|
|
2049
|
+
capabilities: manifest.capabilities,
|
|
2050
|
+
installation: {
|
|
2051
|
+
state: installationState,
|
|
2052
|
+
configured: config?.agents.includes(agent) ?? false,
|
|
2053
|
+
executable,
|
|
2054
|
+
installedUnits: exactUnits,
|
|
2055
|
+
expectedUnits: expected,
|
|
2056
|
+
units,
|
|
2057
|
+
runtime: agentRuntime.state,
|
|
2058
|
+
runtimeVersion: agentRuntime.version,
|
|
2059
|
+
expectedRuntimeVersion: target.runtimeRequired || stalePluginRuntime ? LORE_VERSION : null,
|
|
2060
|
+
issue: invalidConfiguration ? "invalid_configuration" : null,
|
|
2061
|
+
},
|
|
2062
|
+
invocation: invocation.health,
|
|
2063
|
+
invocationError: invocation.error,
|
|
1226
2064
|
};
|
|
1227
2065
|
}
|
|
1228
2066
|
async function statusData(paths) {
|
|
@@ -1235,29 +2073,27 @@ async function statusData(paths) {
|
|
|
1235
2073
|
// Missing configuration is represented as disconnected.
|
|
1236
2074
|
}
|
|
1237
2075
|
const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1240
|
-
? [access(process.execPath, fsConstants.R_OK | fsConstants.X_OK)]
|
|
1241
|
-
: [
|
|
1242
|
-
access(paths.runtime, fsConstants.R_OK | fsConstants.X_OK),
|
|
1243
|
-
access(paths.runtimeRepository, fsConstants.R_OK),
|
|
1244
|
-
access(paths.runtimePackage, fsConstants.R_OK),
|
|
1245
|
-
]).then(() => true, () => false)
|
|
1246
|
-
: Promise.resolve(false);
|
|
1247
|
-
const [runtimeInstalled, queuedTurns, agents] = await Promise.all([
|
|
1248
|
-
runtimeInstalledCheck,
|
|
2076
|
+
const runtime = await inspectRuntimeInstallation(runtimeRequired, paths);
|
|
2077
|
+
const [legacyQueuedTurns, reliability, agents] = await Promise.all([
|
|
1249
2078
|
queueCount(paths),
|
|
1250
|
-
|
|
2079
|
+
inspectReliabilityStore(config, paths),
|
|
2080
|
+
Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths, runtime))),
|
|
1251
2081
|
]);
|
|
1252
2082
|
return {
|
|
2083
|
+
schemaVersion: "lore-cli-status-v1",
|
|
2084
|
+
integrationContractVersion: RELIABILITY_INTEGRATION_CONTRACT_VERSION,
|
|
1253
2085
|
connected: config !== null,
|
|
1254
2086
|
apiUrl: config?.apiUrl ?? null,
|
|
1255
2087
|
config: paths.config,
|
|
1256
2088
|
configMode,
|
|
1257
2089
|
runtimeRequired,
|
|
1258
|
-
runtimeInstalled,
|
|
1259
|
-
|
|
2090
|
+
runtimeInstalled: runtime.state === "installed",
|
|
2091
|
+
runtimeVersion: runtime.version,
|
|
2092
|
+
queuedTurns: legacyQueuedTurns + (reliability.inspection?.outbox.total ?? 0),
|
|
2093
|
+
reliability: reliability.inspection,
|
|
2094
|
+
reliabilityError: reliability.error,
|
|
1260
2095
|
agents,
|
|
2096
|
+
externalSurfaces: EXTERNAL_SURFACES,
|
|
1261
2097
|
};
|
|
1262
2098
|
}
|
|
1263
2099
|
async function statusCommand(args) {
|
|
@@ -1267,9 +2103,15 @@ async function statusCommand(args) {
|
|
|
1267
2103
|
}
|
|
1268
2104
|
const data = await statusData(getLorePaths());
|
|
1269
2105
|
const agentLines = data.agents
|
|
1270
|
-
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, ${agent.
|
|
2106
|
+
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, installation ${agent.installation.state}, invocation ${agent.invocation.state}${agent.invocationError === null ? "" : ` (${agent.invocationError})`}, capture ${agent.capabilities.captureDurability}, guard ${agent.capabilities.guardBoundaryCoverage}/${agent.capabilities.enforcement}`)
|
|
1271
2107
|
.join("\n");
|
|
1272
|
-
|
|
2108
|
+
const externalSurfaceLines = data.externalSurfaces
|
|
2109
|
+
.map((surface) => `${surface.surface}: repository template, locally inspectable no, capture ${surface.capabilities.captureDurability}, guard ${surface.capabilities.guardBoundaryCoverage}/${surface.capabilities.enforcement}`)
|
|
2110
|
+
.join("\n");
|
|
2111
|
+
const metricRate = (value) => value === null || value === undefined
|
|
2112
|
+
? "-"
|
|
2113
|
+
: `${(value * 100).toFixed(1)}%`;
|
|
2114
|
+
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? `installed (${data.runtimeVersion})` : data.runtimeVersion === null ? "missing" : `stale (${data.runtimeVersion})`) : "not required"}\ndurable_outbox: ${data.reliability?.outbox.total ?? 0} ready=${data.reliability?.outbox.ready ?? 0} auth_blocked=${data.reliability?.outbox.authBlocked ?? 0} dead=${data.reliability?.outbox.dead ?? 0}\noldest_capture: ${data.reliability?.outbox.oldestEnqueuedAt ?? "-"}\ncontext_snapshots: ${data.reliability?.cache.context ?? 0}\noldest_context_snapshot: ${data.reliability?.cache.oldestContextWrittenAt ?? "-"}\npolicy_snapshots: ${data.reliability?.cache.policy ?? 0}\nlocal_context_fallback_rate: ${metricRate(data.reliability?.operational.retrieval.fallbackRate)}\nlocal_guard_coverage: ${metricRate(data.reliability?.operational.guard.coverageRate)}\nlocal_guard_failures: ${data.reliability?.operational.guard.failures ?? 0}\nlocal_retrieval_failures: ${data.reliability?.operational.retrieval.failed ?? 0}\n${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
|
|
1273
2115
|
}
|
|
1274
2116
|
async function disconnectCommand(args) {
|
|
1275
2117
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -1303,6 +2145,12 @@ async function disconnectCommand(args) {
|
|
|
1303
2145
|
rm(paths.config, { force: true }),
|
|
1304
2146
|
rm(paths.runtime, { force: true }),
|
|
1305
2147
|
rm(paths.runtimeRepository, { force: true }),
|
|
2148
|
+
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
2149
|
+
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
2150
|
+
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
2151
|
+
rm(paths.runtimeContextFallback, { force: true }),
|
|
2152
|
+
rm(paths.runtimeInvocationHealthWriter, { force: true }),
|
|
2153
|
+
rm(paths.runtimeVersion, { force: true }),
|
|
1306
2154
|
rm(paths.runtimePackage, { force: true }),
|
|
1307
2155
|
rm(paths.state, { recursive: true, force: true }),
|
|
1308
2156
|
rm(paths.queue, { recursive: true, force: true }),
|
|
@@ -1363,8 +2211,61 @@ async function apiChecks(config) {
|
|
|
1363
2211
|
: detail,
|
|
1364
2212
|
});
|
|
1365
2213
|
}
|
|
2214
|
+
try {
|
|
2215
|
+
const response = await fetch(`${config.apiUrl}/v1/reliability/metrics`, {
|
|
2216
|
+
headers: {
|
|
2217
|
+
authorization: `Bearer ${config.token}`,
|
|
2218
|
+
"user-agent": `lore-cli/${LORE_VERSION}`,
|
|
2219
|
+
},
|
|
2220
|
+
signal: AbortSignal.timeout(3_000),
|
|
2221
|
+
});
|
|
2222
|
+
if (!response.ok) {
|
|
2223
|
+
throw new Error("Operational metrics request failed");
|
|
2224
|
+
}
|
|
2225
|
+
const metrics = OperationalMetricsResponseSchema.parse(await response.json());
|
|
2226
|
+
const captureBacklog = metrics.captureReplay.captureExtraction.pending +
|
|
2227
|
+
metrics.captureReplay.captureExtraction.running;
|
|
2228
|
+
const embeddingBacklog = metrics.captureReplay.memoryEmbedding.pending +
|
|
2229
|
+
metrics.captureReplay.memoryEmbedding.running;
|
|
2230
|
+
const deadLetters = metrics.captureReplay.captureExtraction.dead +
|
|
2231
|
+
metrics.captureReplay.memoryEmbedding.dead;
|
|
2232
|
+
const detectedFailures = metrics.silentFailures.count +
|
|
2233
|
+
metrics.guard.failures +
|
|
2234
|
+
deadLetters;
|
|
2235
|
+
checks.push({
|
|
2236
|
+
name: "server-operational-metrics",
|
|
2237
|
+
status: metrics.silentFailures.count > 0 || deadLetters > 0
|
|
2238
|
+
? "error"
|
|
2239
|
+
: metrics.guard.failures > 0 ||
|
|
2240
|
+
captureBacklog > 0 ||
|
|
2241
|
+
embeddingBacklog > 0
|
|
2242
|
+
? "warning"
|
|
2243
|
+
: "ok",
|
|
2244
|
+
detail: `${detectedFailures} detected failure(s), ${captureBacklog} capture and ${embeddingBacklog} embedding job(s) in backlog; ${metrics.guard.latency.samples} Guard latency sample(s)`,
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
catch {
|
|
2248
|
+
checks.push({
|
|
2249
|
+
name: "server-operational-metrics",
|
|
2250
|
+
status: "warning",
|
|
2251
|
+
detail: "authenticated operational metrics are unavailable",
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
1366
2254
|
return checks;
|
|
1367
2255
|
}
|
|
2256
|
+
async function verifyConnectedInstallation() {
|
|
2257
|
+
const paths = getLorePaths();
|
|
2258
|
+
const config = await readConnectorConfig(paths);
|
|
2259
|
+
if (config === null) {
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
const status = await statusData(paths);
|
|
2263
|
+
if (status.configMode !== "600" ||
|
|
2264
|
+
status.agents.some((agent) => agent.configured && agent.installation.state !== "complete")) {
|
|
2265
|
+
return false;
|
|
2266
|
+
}
|
|
2267
|
+
return (await apiChecks(config)).every((check) => check.status !== "error");
|
|
2268
|
+
}
|
|
1368
2269
|
async function doctorCommand(args) {
|
|
1369
2270
|
const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
|
|
1370
2271
|
if (parsed === null) {
|
|
@@ -1415,14 +2316,87 @@ async function doctorCommand(args) {
|
|
|
1415
2316
|
});
|
|
1416
2317
|
checks.push({
|
|
1417
2318
|
name: `${agent.agent}-${agent.integration}`,
|
|
1418
|
-
status: agent.
|
|
1419
|
-
detail: `${agent.
|
|
2319
|
+
status: agent.installation.state === "complete" ? "ok" : "error",
|
|
2320
|
+
detail: `${agent.installation.state}: ${agent.installation.installedUnits}/${agent.installation.expectedUnits} exact Lore ${agent.integration} units`,
|
|
2321
|
+
});
|
|
2322
|
+
checks.push({
|
|
2323
|
+
name: `${agent.agent}-invocation`,
|
|
2324
|
+
status: agent.invocationError !== null
|
|
2325
|
+
? "error"
|
|
2326
|
+
: agent.invocation.state === "successful"
|
|
2327
|
+
? "ok"
|
|
2328
|
+
: agent.invocation.state === "failed" ||
|
|
2329
|
+
agent.invocation.state === "incomplete"
|
|
2330
|
+
? "error"
|
|
2331
|
+
: "warning",
|
|
2332
|
+
detail: agent.invocationError !== null
|
|
2333
|
+
? agent.invocationError === "runtime_version_mismatch"
|
|
2334
|
+
? `observed Lore ${agent.invocation.runtimeVersion}; expected ${LORE_VERSION}`
|
|
2335
|
+
: `health record unavailable (${agent.invocationError})`
|
|
2336
|
+
: agent.invocation.state === "successful"
|
|
2337
|
+
? `last completed ${agent.invocation.lastSuccessfulAt}`
|
|
2338
|
+
: agent.invocation.state === "failed"
|
|
2339
|
+
? `last invocation failed (${agent.invocation.failureCode})`
|
|
2340
|
+
: agent.invocation.state === "incomplete"
|
|
2341
|
+
? `last invocation did not complete (${agent.invocation.lastAttemptedAt})`
|
|
2342
|
+
: agent.invocation.state === "never_observed"
|
|
2343
|
+
? "installed, but no invocation has been observed yet"
|
|
2344
|
+
: "invocation health is unavailable",
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
for (const surface of status.externalSurfaces) {
|
|
2348
|
+
checks.push({
|
|
2349
|
+
name: `${surface.surface}-capability`,
|
|
2350
|
+
status: "ok",
|
|
2351
|
+
detail: "surface-specific repository configuration; local installation is not claimed",
|
|
1420
2352
|
});
|
|
1421
2353
|
}
|
|
1422
2354
|
checks.push({
|
|
1423
|
-
name: "
|
|
1424
|
-
status: status.
|
|
1425
|
-
|
|
2355
|
+
name: "reliability-store",
|
|
2356
|
+
status: status.reliabilityError !== null
|
|
2357
|
+
? "error"
|
|
2358
|
+
: status.reliability === null
|
|
2359
|
+
? "warning"
|
|
2360
|
+
: "ok",
|
|
2361
|
+
detail: status.reliabilityError ??
|
|
2362
|
+
status.reliability?.directory ??
|
|
2363
|
+
"not initialized",
|
|
2364
|
+
});
|
|
2365
|
+
if (status.reliability !== null) {
|
|
2366
|
+
checks.push({
|
|
2367
|
+
name: "durable-capture-outbox",
|
|
2368
|
+
status: status.reliability.outbox.authBlocked > 0 ||
|
|
2369
|
+
status.reliability.outbox.dead > 0
|
|
2370
|
+
? "error"
|
|
2371
|
+
: status.reliability.outbox.ready > 0 ||
|
|
2372
|
+
status.reliability.outbox.inFlight > 0
|
|
2373
|
+
? "warning"
|
|
2374
|
+
: "ok",
|
|
2375
|
+
detail: `${status.reliability.outbox.total} total; ${status.reliability.outbox.ready} ready, ${status.reliability.outbox.authBlocked} authentication-blocked, ${status.reliability.outbox.dead} dead-letter`,
|
|
2376
|
+
});
|
|
2377
|
+
checks.push({
|
|
2378
|
+
name: "verified-snapshot-cache",
|
|
2379
|
+
status: status.reliability.trustKeys > 0 ? "ok" : "warning",
|
|
2380
|
+
detail: `${status.reliability.cache.context} context, ${status.reliability.cache.policy} policy, ${status.reliability.trustKeys} pinned key(s)`,
|
|
2381
|
+
});
|
|
2382
|
+
const operational = status.reliability.operational;
|
|
2383
|
+
const detectedFailures = operational.guard.failures + operational.retrieval.failed;
|
|
2384
|
+
checks.push({
|
|
2385
|
+
name: "local-operational-metrics",
|
|
2386
|
+
status: detectedFailures > 0 ? "warning" : "ok",
|
|
2387
|
+
detail: `${detectedFailures} detected failure(s); Guard coverage ${operational.guard.coverageRate === null ? "not yet sampled" : `${(operational.guard.coverageRate * 100).toFixed(1)}%`}, context fallback ${operational.retrieval.fallbackRate === null ? "not yet sampled" : `${(operational.retrieval.fallbackRate * 100).toFixed(1)}%`}`,
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
const incompleteInvocations = status.agents.filter((agent) => agent.configured && agent.invocation.state === "incomplete").length;
|
|
2391
|
+
const failedInvocations = status.agents.filter((agent) => agent.configured && agent.invocation.state === "failed").length;
|
|
2392
|
+
checks.push({
|
|
2393
|
+
name: "integration-invocation-completion",
|
|
2394
|
+
status: incompleteInvocations > 0
|
|
2395
|
+
? "error"
|
|
2396
|
+
: failedInvocations > 0
|
|
2397
|
+
? "warning"
|
|
2398
|
+
: "ok",
|
|
2399
|
+
detail: `${incompleteInvocations} incomplete and ${failedInvocations} explicitly failed latest invocation(s); target 0`,
|
|
1426
2400
|
});
|
|
1427
2401
|
if (config !== null) {
|
|
1428
2402
|
checks.push(...(await apiChecks(config)));
|
|
@@ -1494,7 +2468,19 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
1494
2468
|
await doctorCommand(commandArgs);
|
|
1495
2469
|
return;
|
|
1496
2470
|
case "self-host":
|
|
1497
|
-
await runSelfHostCommand(commandArgs
|
|
2471
|
+
await runSelfHostCommand(commandArgs, process.env, {
|
|
2472
|
+
connect: async (request) => {
|
|
2473
|
+
const connected = await connectWithCredential({
|
|
2474
|
+
apiUrl: request.apiUrl,
|
|
2475
|
+
token: request.token,
|
|
2476
|
+
agents: request.agents,
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
agents: connected.agents,
|
|
2480
|
+
verified: await verifyConnectedInstallation(),
|
|
2481
|
+
};
|
|
2482
|
+
},
|
|
2483
|
+
});
|
|
1498
2484
|
return;
|
|
1499
2485
|
case "demo":
|
|
1500
2486
|
await runDemoCommand(commandArgs, await readConnectorConfig(getLorePaths()));
|
|
@@ -1508,6 +2494,11 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
1508
2494
|
case "hook":
|
|
1509
2495
|
await runHook(commandArgs);
|
|
1510
2496
|
return;
|
|
2497
|
+
case "internal-health-write": {
|
|
2498
|
+
const encoded = commandArgs[0] ?? (await readInvocationHealthWriteInput());
|
|
2499
|
+
await runInvocationHealthWrite(encoded);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
1511
2502
|
default:
|
|
1512
2503
|
throw new Error(`Unknown command: ${command}\nTry: lore --help`);
|
|
1513
2504
|
}
|