@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.
Files changed (46) hide show
  1. package/README.md +99 -17
  2. package/dist/ask.d.ts.map +1 -1
  3. package/dist/ask.js +3 -26
  4. package/dist/ask.js.map +1 -1
  5. package/dist/cli.d.ts +37 -2
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +1071 -80
  8. package/dist/cli.js.map +1 -1
  9. package/dist/context-fallback.d.ts +37 -0
  10. package/dist/context-fallback.d.ts.map +1 -0
  11. package/dist/context-fallback.js +259 -0
  12. package/dist/context-fallback.js.map +1 -0
  13. package/dist/generated-assets.d.ts +4 -4
  14. package/dist/generated-assets.d.ts.map +1 -1
  15. package/dist/generated-assets.js +4 -4
  16. package/dist/generated-assets.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/invocation-health-writer.d.ts +19 -0
  22. package/dist/invocation-health-writer.d.ts.map +1 -0
  23. package/dist/invocation-health-writer.js +131 -0
  24. package/dist/invocation-health-writer.js.map +1 -0
  25. package/dist/reliability-store.d.ts +283 -0
  26. package/dist/reliability-store.d.ts.map +1 -0
  27. package/dist/reliability-store.js +1913 -0
  28. package/dist/reliability-store.js.map +1 -0
  29. package/dist/runtime-version.d.ts +2 -0
  30. package/dist/runtime-version.d.ts.map +1 -0
  31. package/dist/runtime-version.js +5 -0
  32. package/dist/runtime-version.js.map +1 -0
  33. package/dist/runtime.d.ts +9 -1
  34. package/dist/runtime.d.ts.map +1 -1
  35. package/dist/runtime.js +1113 -153
  36. package/dist/runtime.js.map +1 -1
  37. package/dist/self-host.d.ts +15 -2
  38. package/dist/self-host.d.ts.map +1 -1
  39. package/dist/self-host.js +55 -8
  40. package/dist/self-host.js.map +1 -1
  41. package/dist/signed-snapshot.d.ts +59 -0
  42. package/dist/signed-snapshot.d.ts.map +1 -0
  43. package/dist/signed-snapshot.js +303 -0
  44. package/dist/signed-snapshot.js.map +1 -0
  45. package/dist/update.js +3 -3
  46. 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 { randomUUID } from "node:crypto";
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
- Store a workspace credential and idempotently install agent integrations.
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; repeat to override auto-detection
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 whether Lore is configured and each native integration is installed.
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, runtime, agent integrations, binaries, and Lore health.
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 only Lore-owned hooks/plugin, credentials, runtime, state, and retry queue.
137
- Unrelated agent settings, plugins, and Lore-created backups are retained.
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) || value === "t3code";
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: 1,
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
- return detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
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) => agent === "t3code" ? [] : [defaultInstallation(agent, paths)]);
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 [runtime, repository] = await Promise.all([
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.runtimePackage, '{"type":"module"}\n', 0o600),
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 = { agents: [], json: false };
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 connectCommand(args) {
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 hasProvidedToken = parsed.token !== undefined ||
1100
- (process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
1101
- (process.env.LORE_TOKEN?.trim() ?? "") !== "";
1102
- const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
1103
- ...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
1104
- ...(parsed.dashboardUrl === undefined
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 = parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500;
1585
+ const timeoutMs = options.timeoutMs ?? existing?.timeoutMs ?? 2_500;
1116
1586
  const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
1117
- const requestedExpansion = parsed.agents.length === 0
1587
+ const requestedExpansion = options.agents.length === 0
1118
1588
  ? { installations: [], warnings: [] }
1119
- : await expandConnectInstallations(parsed.agents, paths);
1120
- const detectedAgents = parsed.agents.length === 0 ? await detectAgents(paths) : [];
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
- if (agents.length === 0) {
1130
- throw new Error("No Claude, Codex, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
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: 1,
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
- const result = {
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
- writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n${requestedExpansion.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
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
- async function getAgentStatus(agent, config, paths) {
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
- for (const path of configPaths) {
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
- // A malformed target config is reported as missing integration state.
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: await target.executable(paths),
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: target.expected * configPaths.length,
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 runtimeInstalledCheck = runtimeRequired
1239
- ? Promise.all(IS_STANDALONE_BINARY
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
- Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths))),
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
- queuedTurns,
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.integration} ${agent.installed}/${agent.expected}, executable ${agent.executable ? "yes" : "no"}, config ${agent.configExists ? "yes" : "no"}`)
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
- 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`);
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.installed === agent.expected ? "ok" : "error",
1419
- detail: `${agent.installed}/${agent.expected} Lore ${agent.integration} in ${agent.configFile}`,
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: "retry-queue",
1424
- status: status.queuedTurns === 0 ? "ok" : "warning",
1425
- detail: `${status.queuedTurns} queued turn(s)`,
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
  }