@lore-co/cli 0.1.18 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
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 { spawn } from "node:child_process";
4
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 { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, CURSOR_RELIABILITY_INTEGRATION, NATIVE_CODING_AGENT_NAMES, OPENCODE_RELIABILITY_INTEGRATION, OperationalMetricsResponseSchema, POLYTOKEN_RELIABILITY_INTEGRATION, RELIABILITY_INTEGRATION_CONTRACT_VERSION, ReliabilityInvocationRecordSchema, WorkspaceIdentityResponseSchema, canonicalJson, deriveReliabilityInvocationHealth, 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";
10
12
  import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
11
13
  import { runAskCommand } from "./ask.js";
14
+ import { runRememberCommand } from "./remember.js";
12
15
  import { runGuardCommand } from "./guard.js";
13
16
  import { runContinueCommand, runResumeCommand } from "./handoff.js";
14
17
  import { runAttachCommand, runLiveHostCommand, runLiveRunCommand, } from "./live.js";
@@ -33,8 +36,9 @@ const HOOK_EVENTS = [
33
36
  "SessionEnd",
34
37
  ];
35
38
  /** Tools the Context Guard PreToolUse hook cares about (regex matcher). */
36
- const GUARD_TOOL_MATCHER = "Edit|Write|MultiEdit|NotebookEdit|ApplyPatch|apply_patch|Bash";
39
+ const GUARD_TOOL_MATCHER = "Edit|Write|MultiEdit|NotebookEdit|ApplyPatch|apply_patch|edit|write|patch|str_replace_editor|Bash|bash|Shell|shell|exec_command|shell_command|run_terminal_cmd";
37
40
  const CURSOR_HOOK_EVENTS = [
41
+ "sessionStart",
38
42
  "beforeSubmitPrompt",
39
43
  "beforeShellExecution",
40
44
  "afterAgentResponse",
@@ -43,10 +47,28 @@ const CURSOR_HOOK_EVENTS = [
43
47
  const POLYTOKEN_HOOK_EVENTS = [
44
48
  "pre_user_prompt",
45
49
  "post_model_turn",
50
+ "pre_tool_use",
51
+ ];
52
+ const COPILOT_CLI_HOOK_EVENTS = [
53
+ "userPromptTransformed",
54
+ "preToolUse",
55
+ "agentStop",
56
+ "sessionEnd",
57
+ ];
58
+ const EXTERNAL_SURFACES = [
59
+ {
60
+ surface: "copilot-cloud-mcp",
61
+ integrationId: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.integrationId,
62
+ installation: "repository_template",
63
+ locallyInspectable: false,
64
+ capabilities: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.capabilities,
65
+ },
46
66
  ];
47
67
  const AGENT_MANIFESTS = {
48
68
  claude: CLAUDE_RELIABILITY_INTEGRATION,
49
69
  codex: CODEX_RELIABILITY_INTEGRATION,
70
+ "copilot-cli": COPILOT_CLI_RELIABILITY_INTEGRATION,
71
+ "copilot-vscode": COPILOT_VSCODE_PREVIEW_RELIABILITY_INTEGRATION,
50
72
  cursor: CURSOR_RELIABILITY_INTEGRATION,
51
73
  opencode: OPENCODE_RELIABILITY_INTEGRATION,
52
74
  polytoken: POLYTOKEN_RELIABILITY_INTEGRATION,
@@ -60,6 +82,7 @@ Usage:
60
82
  Commands:
61
83
  connect Configure Lore and install native agent integrations
62
84
  ask Query shared knowledge and prior session context
85
+ remember Save a durable fact, convention, correction, or decision
63
86
  guard Control Context Guard (off, assist, enforce)
64
87
  resume Continue previous work with saved Lore context
65
88
  continue Hand the current work to a different agent (--on <agent>)
@@ -79,6 +102,7 @@ Commands:
79
102
  Discover:
80
103
  lore connect --help
81
104
  lore ask --help
105
+ lore remember --help
82
106
  lore guard --help
83
107
  lore resume --help
84
108
  lore continue --help
@@ -92,6 +116,7 @@ Discover:
92
116
  Examples:
93
117
  lore connect --token "$LORE_WORKSPACE_TOKEN"
94
118
  lore ask "How should payment retries work?"
119
+ lore remember "Use pnpm for this repository." --repo acme/app
95
120
  lore resume
96
121
  lore continue --on codex
97
122
  lore connect github --repo owner/repository
@@ -102,21 +127,26 @@ Examples:
102
127
  lore devin --help
103
128
  `;
104
129
  const CONNECT_HELP = `lore connect
105
- Store a workspace credential and idempotently install agent integrations.
130
+ Authorize this device and idempotently install agent integrations.
106
131
 
107
132
  Usage:
133
+ lore connect --login [options]
108
134
  lore connect --token <token> [options]
109
135
 
110
136
  Options:
111
137
  --url <url> Lore API base URL (default: https://api.uselore.co)
112
138
  --dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
139
+ --login Approve access securely in the Lore dashboard
140
+ --no-open Print the approval URL without opening a browser
113
141
  --token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
114
- --agent <name> claude, codex, cursor, opencode, polytoken, or t3code; repeat to override auto-detection
142
+ --agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code
115
143
  --timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
116
144
  --json Print machine-readable output
117
145
  --help Show this command's help
118
146
 
119
147
  Examples:
148
+ lore connect --login --agent claude
149
+ lore connect --login --agent cowork
120
150
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
121
151
  lore connect --url http://localhost:3004 --token dev-token --agent codex
122
152
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
@@ -159,7 +189,7 @@ function isObject(value) {
159
189
  function cloneObject(value) {
160
190
  return structuredClone(value);
161
191
  }
162
- export function getLorePaths(home, environment = process.env) {
192
+ export function getLorePaths(home, environment = process.env, cwd = process.cwd()) {
163
193
  const resolvedHome = resolve(home ?? environment.HOME ?? homedir());
164
194
  const loreDirectory = resolve(resolvedHome, ".lore");
165
195
  const xdgConfigHome = environment.XDG_CONFIG_HOME?.trim() === undefined ||
@@ -167,6 +197,7 @@ export function getLorePaths(home, environment = process.env) {
167
197
  ? resolve(resolvedHome, ".config")
168
198
  : resolve(environment.XDG_CONFIG_HOME);
169
199
  const t3Home = resolve(environment.T3CODE_HOME?.trim() || resolve(resolvedHome, ".t3"));
200
+ const copilotHome = resolve(environment.COPILOT_HOME?.trim() || resolve(resolvedHome, ".copilot"));
170
201
  return {
171
202
  home: resolvedHome,
172
203
  loreDirectory,
@@ -183,8 +214,12 @@ export function getLorePaths(home, environment = process.env) {
183
214
  state: resolve(loreDirectory, "state"),
184
215
  queue: resolve(loreDirectory, "queue"),
185
216
  codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
217
+ copilotCliHooks: resolve(copilotHome, "hooks", "lore.json"),
218
+ copilotVscodeHooks: resolve(environment.LORE_COPILOT_VSCODE_HOOKS?.trim() ||
219
+ resolve(cwd, ".github", "hooks", "lore-vscode.json")),
186
220
  claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
187
221
  cursorHooks: resolve(resolvedHome, ".cursor", "hooks.json"),
222
+ cursorMcp: resolve(resolvedHome, ".cursor", "mcp.json"),
188
223
  cursorUserData: platform() === "darwin"
189
224
  ? resolve(resolvedHome, "Library", "Application Support", "Cursor")
190
225
  : resolve(resolvedHome, ".config", "Cursor"),
@@ -200,14 +235,18 @@ function isConfiguredAgent(value) {
200
235
  return typeof value === "string" && isNativeCodingAgent(value);
201
236
  }
202
237
  function isConnectAgent(value) {
203
- return isConfiguredAgent(value) || value === "t3code";
238
+ return (isConfiguredAgent(value) ||
239
+ value === "copilot" ||
240
+ value === "cowork" ||
241
+ value === "t3code");
204
242
  }
205
- function hookCommand(agent, paths) {
243
+ function hookCommand(agent, paths, event) {
206
244
  const loreHome = `LORE_HOME=${shellQuote(paths.home)}`;
245
+ const hookEvent = event === undefined ? "" : ` LORE_HOOK_EVENT=${shellQuote(event)}`;
207
246
  if (IS_STANDALONE_BINARY) {
208
- return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
247
+ return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome}${hookEvent} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
209
248
  }
210
- return `env ${loreHome} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
249
+ return `env ${loreHome}${hookEvent} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
211
250
  }
212
251
  function isLoreHook(value) {
213
252
  if (!isObject(value) || value.type !== "command") {
@@ -333,6 +372,35 @@ function cursorEventHandler(event, paths) {
333
372
  : 25,
334
373
  };
335
374
  }
375
+ const CURSOR_MCP_NAME = "lore-native";
376
+ function cursorMcpServer(paths) {
377
+ return {
378
+ type: "stdio", command: process.execPath,
379
+ args: [...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)]), "cursor-mcp", "--owner", "lore"],
380
+ env: { LORE_HOME: paths.home },
381
+ };
382
+ }
383
+ function isLoreCursorMcp(value) {
384
+ return isObject(value) && Array.isArray(value.args) && value.args.includes("cursor-mcp")
385
+ && value.args.includes("--owner") && value.args.includes("lore");
386
+ }
387
+ export function mergeLoreCursorMcp(input, paths) {
388
+ const config = objectConfiguration(input, "cursor");
389
+ if (config.mcpServers !== undefined && !isObject(config.mcpServers))
390
+ throw new Error("Cursor mcpServers must be an object");
391
+ const servers = isObject(config.mcpServers) ? config.mcpServers : {};
392
+ if (servers[CURSOR_MCP_NAME] !== undefined && !isLoreCursorMcp(servers[CURSOR_MCP_NAME])) {
393
+ throw new Error('Cursor MCP name "lore-native" is already owned by another server; rename it before connecting.');
394
+ }
395
+ return { ...config, mcpServers: { ...servers, [CURSOR_MCP_NAME]: cursorMcpServer(paths) } };
396
+ }
397
+ export function removeLoreCursorMcp(input) {
398
+ const config = objectConfiguration(input, "cursor");
399
+ if (!isObject(config.mcpServers) || !isLoreCursorMcp(config.mcpServers[CURSOR_MCP_NAME]))
400
+ return config;
401
+ const { [CURSOR_MCP_NAME]: _lore, ...rest } = config.mcpServers;
402
+ return { ...config, mcpServers: rest };
403
+ }
336
404
  function stripLoreFromCursorEvent(value) {
337
405
  return Array.isArray(value) ? value.filter((hook) => !isLoreHook(hook)) : [];
338
406
  }
@@ -396,6 +464,169 @@ export function countLoreCursorHooks(input) {
396
464
  return count + hooks[event].filter(isLoreHook).length;
397
465
  }, 0);
398
466
  }
467
+ function copilotCliEventHandler(event, paths) {
468
+ return {
469
+ type: "command",
470
+ bash: hookCommand("copilot-cli", paths, event),
471
+ timeoutSec: event === "sessionEnd"
472
+ ? 2
473
+ : event === "agentStop"
474
+ ? 3
475
+ : event === "preToolUse"
476
+ ? 10
477
+ : 25,
478
+ };
479
+ }
480
+ function isLoreCopilotHook(value) {
481
+ if (!isObject(value) || value.type !== "command") {
482
+ return false;
483
+ }
484
+ const command = typeof value.bash === "string"
485
+ ? value.bash
486
+ : typeof value.command === "string"
487
+ ? value.command
488
+ : "";
489
+ return (/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
490
+ command.includes("/.lore/bin/lore-hook.mjs"));
491
+ }
492
+ function stripLoreFromCopilotEvent(value) {
493
+ return Array.isArray(value)
494
+ ? value.filter((hook) => !isLoreCopilotHook(hook))
495
+ : [];
496
+ }
497
+ export function mergeLoreCopilotCliHooks(input, paths) {
498
+ const result = cloneObject(input);
499
+ if (result.version !== undefined && result.version !== 1) {
500
+ throw new Error('Copilot hook configuration field "version" must be 1');
501
+ }
502
+ if (result.hooks !== undefined && !isObject(result.hooks)) {
503
+ throw new Error('Copilot hook configuration field "hooks" must be an object');
504
+ }
505
+ const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
506
+ for (const event of COPILOT_CLI_HOOK_EVENTS) {
507
+ if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
508
+ throw new Error(`Copilot hook event "${event}" must be an array`);
509
+ }
510
+ hooks[event] = [
511
+ ...stripLoreFromCopilotEvent(hooks[event]),
512
+ copilotCliEventHandler(event, paths),
513
+ ];
514
+ }
515
+ result.version = 1;
516
+ result.hooks = hooks;
517
+ return result;
518
+ }
519
+ export function removeLoreCopilotCliHooks(input) {
520
+ const result = cloneObject(input);
521
+ if (!isObject(result.hooks)) {
522
+ return result;
523
+ }
524
+ const hooks = { ...result.hooks };
525
+ for (const event of COPILOT_CLI_HOOK_EVENTS) {
526
+ if (!Array.isArray(hooks[event])) {
527
+ continue;
528
+ }
529
+ const remaining = stripLoreFromCopilotEvent(hooks[event]);
530
+ if (remaining.length === 0) {
531
+ delete hooks[event];
532
+ }
533
+ else {
534
+ hooks[event] = remaining;
535
+ }
536
+ }
537
+ if (Object.keys(hooks).length === 0) {
538
+ delete result.hooks;
539
+ }
540
+ else {
541
+ result.hooks = hooks;
542
+ }
543
+ return result;
544
+ }
545
+ export function countLoreCopilotCliHooks(input) {
546
+ if (!isObject(input.hooks)) {
547
+ return 0;
548
+ }
549
+ return COPILOT_CLI_HOOK_EVENTS.reduce((count, event) => {
550
+ const hooks = input.hooks;
551
+ if (!isObject(hooks) || !Array.isArray(hooks[event])) {
552
+ return count;
553
+ }
554
+ return count + hooks[event].filter(isLoreCopilotHook).length;
555
+ }, 0);
556
+ }
557
+ function copilotVscodeEventHandler(event, paths) {
558
+ return {
559
+ type: "command",
560
+ command: hookCommand("copilot-vscode", paths, event),
561
+ timeout: event === "SessionEnd"
562
+ ? 2
563
+ : event === "Stop"
564
+ ? 3
565
+ : event === "PreToolUse"
566
+ ? 10
567
+ : 25,
568
+ };
569
+ }
570
+ export function mergeLoreCopilotVscodeHooks(input, paths) {
571
+ const result = cloneObject(input);
572
+ if (result.version !== undefined && result.version !== 1) {
573
+ throw new Error('VS Code Copilot hook field "version" must be 1');
574
+ }
575
+ if (result.hooks !== undefined && !isObject(result.hooks)) {
576
+ throw new Error('VS Code Copilot hook field "hooks" must be an object');
577
+ }
578
+ const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
579
+ for (const event of HOOK_EVENTS) {
580
+ if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
581
+ throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
582
+ }
583
+ hooks[event] = [
584
+ ...stripLoreFromCursorEvent(hooks[event]),
585
+ copilotVscodeEventHandler(event, paths),
586
+ ];
587
+ }
588
+ result.version = 1;
589
+ result.hooks = hooks;
590
+ return result;
591
+ }
592
+ export function removeLoreCopilotVscodeHooks(input) {
593
+ const result = cloneObject(input);
594
+ if (!isObject(result.hooks)) {
595
+ return result;
596
+ }
597
+ const hooks = { ...result.hooks };
598
+ for (const event of HOOK_EVENTS) {
599
+ if (!Array.isArray(hooks[event])) {
600
+ continue;
601
+ }
602
+ const remaining = stripLoreFromCursorEvent(hooks[event]);
603
+ if (remaining.length === 0) {
604
+ delete hooks[event];
605
+ }
606
+ else {
607
+ hooks[event] = remaining;
608
+ }
609
+ }
610
+ if (Object.keys(hooks).length === 0) {
611
+ delete result.hooks;
612
+ }
613
+ else {
614
+ result.hooks = hooks;
615
+ }
616
+ return result;
617
+ }
618
+ export function countLoreCopilotVscodeHooks(input) {
619
+ if (!isObject(input.hooks)) {
620
+ return 0;
621
+ }
622
+ return HOOK_EVENTS.reduce((count, event) => {
623
+ const hooks = input.hooks;
624
+ if (!isObject(hooks) || !Array.isArray(hooks[event])) {
625
+ return count;
626
+ }
627
+ return count + hooks[event].filter(isLoreHook).length;
628
+ }, 0);
629
+ }
399
630
  function polytokenEventHandler(event, paths) {
400
631
  return {
401
632
  name: `lore-${event.replaceAll("_", "-")}`,
@@ -536,6 +767,54 @@ function cursorInstallationUnits(input, paths, index, total) {
536
767
  };
537
768
  });
538
769
  }
770
+ function copilotCliInstallationUnits(input, paths, index, total) {
771
+ const configuration = objectConfiguration(input, "copilot-cli");
772
+ if (configuration.version !== undefined && configuration.version !== 1) {
773
+ throw new Error('Copilot hook configuration field "version" must be 1');
774
+ }
775
+ if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
776
+ throw new Error('Copilot hook configuration field "hooks" must be an object');
777
+ }
778
+ const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
779
+ return COPILOT_CLI_HOOK_EVENTS.map((event) => {
780
+ const values = hooks[event];
781
+ if (values !== undefined && !Array.isArray(values)) {
782
+ throw new Error(`Copilot hook event "${event}" must be an array`);
783
+ }
784
+ const candidates = Array.isArray(values) ? values : [];
785
+ const expected = copilotCliEventHandler(event, paths);
786
+ const ownedCount = candidates.filter(isLoreCopilotHook).length;
787
+ const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
788
+ return {
789
+ id: installationUnitId(event, index, total),
790
+ state: installationUnitState(ownedCount, exactCount),
791
+ };
792
+ });
793
+ }
794
+ function copilotVscodeInstallationUnits(input, paths, index, total) {
795
+ const configuration = objectConfiguration(input, "copilot-vscode");
796
+ if (configuration.version !== undefined && configuration.version !== 1) {
797
+ throw new Error('VS Code Copilot hook field "version" must be 1');
798
+ }
799
+ if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
800
+ throw new Error('VS Code Copilot hook field "hooks" must be an object');
801
+ }
802
+ const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
803
+ return HOOK_EVENTS.map((event) => {
804
+ const values = hooks[event];
805
+ if (values !== undefined && !Array.isArray(values)) {
806
+ throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
807
+ }
808
+ const candidates = Array.isArray(values) ? values : [];
809
+ const expected = copilotVscodeEventHandler(event, paths);
810
+ const ownedCount = candidates.filter(isLoreHook).length;
811
+ const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
812
+ return {
813
+ id: installationUnitId(event, index, total),
814
+ state: installationUnitState(ownedCount, exactCount),
815
+ };
816
+ });
817
+ }
539
818
  function polytokenInstallationUnits(input, paths, index, total) {
540
819
  if (!Array.isArray(input)) {
541
820
  throw new Error("Polytoken hooks must contain a JSON array");
@@ -574,6 +853,10 @@ function inspectInstallationUnits(input, agent, paths, index, total) {
574
853
  case "claude":
575
854
  case "codex":
576
855
  return commandHookInstallationUnits(input, agent, paths, index, total);
856
+ case "copilot-cli":
857
+ return copilotCliInstallationUnits(input, paths, index, total);
858
+ case "copilot-vscode":
859
+ return copilotVscodeInstallationUnits(input, paths, index, total);
577
860
  case "cursor":
578
861
  return cursorInstallationUnits(input, paths, index, total);
579
862
  case "polytoken":
@@ -883,9 +1166,33 @@ const AGENT_TARGETS = {
883
1166
  remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
884
1167
  count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
885
1168
  },
1169
+ "copilot-cli": {
1170
+ integration: "hooks",
1171
+ expected: COPILOT_CLI_HOOK_EVENTS.length,
1172
+ runtimeRequired: true,
1173
+ emptyValue: () => ({}),
1174
+ configPath: (paths) => paths.copilotCliHooks,
1175
+ detect: async (paths) => (await commandExists("copilot")) || pathExists(paths.copilotCliHooks),
1176
+ executable: async () => commandExists("copilot"),
1177
+ merge: (input, paths) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths),
1178
+ remove: (input) => removeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
1179
+ count: (input) => countLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
1180
+ },
1181
+ "copilot-vscode": {
1182
+ integration: "hooks",
1183
+ expected: HOOK_EVENTS.length,
1184
+ runtimeRequired: true,
1185
+ emptyValue: () => ({}),
1186
+ configPath: (paths) => paths.copilotVscodeHooks,
1187
+ detect: async (paths) => pathExists(paths.copilotVscodeHooks),
1188
+ executable: async () => commandExists("code"),
1189
+ merge: (input, paths) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths),
1190
+ remove: (input) => removeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
1191
+ count: (input) => countLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
1192
+ },
886
1193
  cursor: {
887
1194
  integration: "hooks",
888
- expected: CURSOR_HOOK_EVENTS.length,
1195
+ expected: CURSOR_HOOK_EVENTS.length + 1,
889
1196
  runtimeRequired: true,
890
1197
  emptyValue: () => ({}),
891
1198
  configPath: (paths) => paths.cursorHooks,
@@ -928,7 +1235,10 @@ async function detectAgents(paths) {
928
1235
  agent,
929
1236
  detected: await targetFor(agent).detect(paths),
930
1237
  })));
931
- return detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
1238
+ const agents = detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
1239
+ return agents.includes("copilot-cli")
1240
+ ? agents.filter((agent) => agent !== "copilot-vscode")
1241
+ : agents;
932
1242
  }
933
1243
  function installationKey(installation) {
934
1244
  return `${installation.agent}\0${resolve(installation.configPath)}`;
@@ -1088,7 +1398,14 @@ async function readT3CodeProviderInstallations(paths) {
1088
1398
  }
1089
1399
  }
1090
1400
  async function expandConnectInstallations(requested, paths) {
1091
- const direct = requested.flatMap((agent) => agent === "t3code" ? [] : [defaultInstallation(agent, paths)]);
1401
+ const direct = requested.flatMap((agent) => {
1402
+ if (agent === "cowork" || agent === "t3code") {
1403
+ return [];
1404
+ }
1405
+ return [
1406
+ defaultInstallation(agent === "copilot" ? "copilot-cli" : agent, paths),
1407
+ ];
1408
+ });
1092
1409
  if (!requested.includes("t3code")) {
1093
1410
  return { installations: direct, warnings: [] };
1094
1411
  }
@@ -1117,6 +1434,7 @@ async function installRuntime(paths) {
1117
1434
  rm(paths.runtimeReliabilityStore, { force: true }),
1118
1435
  rm(paths.runtimeSignedSnapshot, { force: true }),
1119
1436
  rm(paths.runtimeCanonicalJson, { force: true }),
1437
+ rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
1120
1438
  rm(paths.runtimeContextFallback, { force: true }),
1121
1439
  rm(paths.runtimeInvocationHealthWriter, { force: true }),
1122
1440
  rm(paths.runtimeVersion, { force: true }),
@@ -1126,6 +1444,7 @@ async function installRuntime(paths) {
1126
1444
  }
1127
1445
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
1128
1446
  const canonicalJsonModule = fileURLToPath(import.meta.resolve("@lore-co/core/canonical-json"));
1447
+ const guardEvaluation = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/guard-evaluation")), "utf8");
1129
1448
  const [runtime, repository, reliabilityStore, signedSnapshot, canonicalJson, contextFallback, invocationHealthWriter,] = await Promise.all([
1130
1449
  readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
1131
1450
  readFile(resolve(sourceDirectory, "repository.js"), "utf8"),
@@ -1141,7 +1460,8 @@ async function installRuntime(paths) {
1141
1460
  }
1142
1461
  const runtimeVersion = `export const RUNTIME_VERSION = ${JSON.stringify(LORE_VERSION)};\n`;
1143
1462
  const runtimeFiles = {
1144
- "lore-hook.mjs": runtime,
1463
+ "lore-hook.mjs": runtime.replaceAll("@lore-co/core/guard-evaluation", "./guard-evaluation.js"),
1464
+ "guard-evaluation.js": guardEvaluation,
1145
1465
  "repository.js": repository,
1146
1466
  "reliability-store.js": reliabilityStore,
1147
1467
  "signed-snapshot.js": standaloneSignedSnapshot,
@@ -1161,6 +1481,7 @@ async function installRuntime(paths) {
1161
1481
  },
1162
1482
  };
1163
1483
  await Promise.all([
1484
+ atomicWrite(resolve(dirname(paths.runtime), "guard-evaluation.js"), guardEvaluation, 0o600),
1164
1485
  atomicWrite(paths.runtimeRepository, repository, 0o600),
1165
1486
  atomicWrite(paths.runtimeReliabilityStore, reliabilityStore, 0o600),
1166
1487
  atomicWrite(paths.runtimeSignedSnapshot, standaloneSignedSnapshot, 0o600),
@@ -1172,7 +1493,7 @@ async function installRuntime(paths) {
1172
1493
  ]);
1173
1494
  // The hook entrypoint is the generation activation marker. Updating it last
1174
1495
  // prevents a new runtime from loading before all of its local modules exist.
1175
- await atomicWrite(paths.runtime, runtime, 0o700);
1496
+ await atomicWrite(paths.runtime, runtimeFiles["lore-hook.mjs"], 0o700);
1176
1497
  }
1177
1498
  function agentConfigPath(agent, paths) {
1178
1499
  return targetFor(agent).configPath(paths);
@@ -1203,7 +1524,12 @@ function valueAfter(args, index, flag) {
1203
1524
  return [value, index + 1];
1204
1525
  }
1205
1526
  function parseConnectArguments(args) {
1206
- const parsed = { agents: [], json: false };
1527
+ const parsed = {
1528
+ agents: [],
1529
+ json: false,
1530
+ login: false,
1531
+ openBrowser: true,
1532
+ };
1207
1533
  for (let index = 0; index < args.length; index += 1) {
1208
1534
  const argument = args[index];
1209
1535
  if (argument === "--help" || argument === "-h") {
@@ -1214,6 +1540,14 @@ function parseConnectArguments(args) {
1214
1540
  parsed.json = true;
1215
1541
  continue;
1216
1542
  }
1543
+ if (argument === "--login") {
1544
+ parsed.login = true;
1545
+ continue;
1546
+ }
1547
+ if (argument === "--no-open") {
1548
+ parsed.openBrowser = false;
1549
+ continue;
1550
+ }
1217
1551
  if (argument !== "--url" &&
1218
1552
  argument !== "--dashboard-url" &&
1219
1553
  argument !== "--token" &&
@@ -1243,9 +1577,17 @@ function parseConnectArguments(args) {
1243
1577
  parsed.agents.push(value);
1244
1578
  }
1245
1579
  else {
1246
- throw new Error("--agent must be claude, codex, cursor, opencode, polytoken, or t3code");
1580
+ throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code");
1247
1581
  }
1248
1582
  }
1583
+ if (parsed.login && parsed.token !== undefined) {
1584
+ throw new Error("--login and --token cannot be used together");
1585
+ }
1586
+ if ((parsed.agents.includes("copilot-cli") ||
1587
+ parsed.agents.includes("copilot")) &&
1588
+ parsed.agents.includes("copilot-vscode")) {
1589
+ throw new Error("Install either copilot-cli or copilot-vscode in one connection. Both hosts load repository hooks and would invoke Lore twice.");
1590
+ }
1249
1591
  return parsed;
1250
1592
  }
1251
1593
  function parseOutputArguments(args, help, command) {
@@ -1266,38 +1608,26 @@ function parseOutputArguments(args, help, command) {
1266
1608
  function writeResult(value, json, text) {
1267
1609
  process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : text);
1268
1610
  }
1269
- async function connectCommand(args) {
1270
- const parsed = parseConnectArguments(args);
1271
- if (parsed === null) {
1272
- return;
1273
- }
1611
+ export async function connectWithCredential(options) {
1274
1612
  if (platform() !== "darwin" && platform() !== "linux") {
1275
1613
  throw new Error("Lore agent integrations currently support macOS and Linux");
1276
1614
  }
1277
- const paths = getLorePaths();
1615
+ const paths = getLorePaths(options.home, options.home === undefined ? process.env : {}, options.cwd);
1278
1616
  const existing = await readConnectorConfig(paths);
1279
- const hasProvidedToken = parsed.token !== undefined ||
1280
- (process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
1281
- (process.env.LORE_TOKEN?.trim() ?? "") !== "";
1282
- const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
1283
- ...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
1284
- ...(parsed.dashboardUrl === undefined
1285
- ? {}
1286
- : { dashboardUrl: parsed.dashboardUrl }),
1287
- environment: process.env,
1288
- existing,
1289
- preferHostedDefault: hasProvidedToken,
1290
- });
1291
- const token = configuredToken(parsed.token, existing);
1292
- if (token === undefined || token.trim() === "") {
1293
- throw new Error("Workspace token is required. Use --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
1617
+ const apiUrl = normalizeApiUrl(options.apiUrl);
1618
+ const dashboardUrl = options.dashboardUrl === undefined
1619
+ ? undefined
1620
+ : normalizeApiUrl(options.dashboardUrl);
1621
+ const token = options.token.trim();
1622
+ if (token === "") {
1623
+ throw new Error("Workspace token cannot be empty");
1294
1624
  }
1295
- const timeoutMs = parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500;
1625
+ const timeoutMs = options.timeoutMs ?? existing?.timeoutMs ?? 2_500;
1296
1626
  const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
1297
- const requestedExpansion = parsed.agents.length === 0
1627
+ const requestedExpansion = options.agents.length === 0
1298
1628
  ? { installations: [], warnings: [] }
1299
- : await expandConnectInstallations(parsed.agents, paths);
1300
- const detectedAgents = parsed.agents.length === 0 ? await detectAgents(paths) : [];
1629
+ : await expandConnectInstallations(options.agents, paths);
1630
+ const detectedAgents = options.agents.length === 0 ? await detectAgents(paths) : [];
1301
1631
  const installations = uniqueInstallations([
1302
1632
  ...configuredInstallations(existing, paths),
1303
1633
  ...requestedExpansion.installations,
@@ -1306,23 +1636,37 @@ async function connectCommand(args) {
1306
1636
  const agents = [
1307
1637
  ...new Set(installations.map((installation) => installation.agent)),
1308
1638
  ].sort();
1309
- if (agents.length === 0) {
1310
- throw new Error("No Claude, Codex, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
1639
+ const credentialOnlyCowork = options.agents.includes("cowork");
1640
+ if (agents.length === 0 && !credentialOnlyCowork) {
1641
+ throw new Error("No Claude, Codex, Copilot CLI, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
1311
1642
  }
1312
1643
  const now = new Date();
1313
1644
  const documents = new Map();
1314
1645
  const mergedDocuments = new Map();
1646
+ const cursorMcpDocuments = new Map();
1315
1647
  for (const installation of installations) {
1316
1648
  const key = installationKey(installation);
1317
1649
  const document = await readJsonDocument(installation.configPath);
1318
1650
  documents.set(key, document);
1319
1651
  mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths));
1652
+ if (installation.agent === "cursor") {
1653
+ const mcpPath = resolve(dirname(installation.configPath), "mcp.json");
1654
+ const mcp = await readJsonDocument(mcpPath);
1655
+ cursorMcpDocuments.set(mcpPath, { document: mcp, merged: mergeLoreCursorMcp(mcp.exists ? mcp.value : {}, paths) });
1656
+ }
1320
1657
  }
1321
1658
  if (installations.some((installation) => targetFor(installation.agent).runtimeRequired)) {
1322
1659
  await installRuntime(paths);
1323
1660
  }
1324
1661
  const changedHookFiles = [];
1325
1662
  const backups = [];
1663
+ for (const [path, { document, merged }] of cursorMcpDocuments) {
1664
+ const result = await writeMergedJson(path, document, merged, now);
1665
+ if (result.changed)
1666
+ changedHookFiles.push(path);
1667
+ if (result.backup)
1668
+ backups.push(result.backup);
1669
+ }
1326
1670
  for (const installation of installations) {
1327
1671
  const key = installationKey(installation);
1328
1672
  const document = documents.get(key);
@@ -1369,17 +1713,154 @@ async function connectCommand(args) {
1369
1713
  await credentialStore.transferPendingTo(reliabilityStore);
1370
1714
  }
1371
1715
  await reliabilityStore.releaseAuthBlocked(now);
1372
- const result = {
1716
+ return {
1373
1717
  connected: true,
1374
1718
  apiUrl: config.apiUrl,
1375
1719
  identity,
1376
- agents,
1720
+ agents: credentialOnlyCowork ? [...agents, "cowork"] : agents,
1377
1721
  config: paths.config,
1378
1722
  changedHookFiles,
1379
1723
  backups,
1380
1724
  warnings: requestedExpansion.warnings,
1381
1725
  };
1382
- writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n${requestedExpansion.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
1726
+ }
1727
+ async function requestDeviceAuthorization(input) {
1728
+ let response;
1729
+ try {
1730
+ response = await fetch(`${input.apiUrl}/v1/device-authorizations`, {
1731
+ method: "POST",
1732
+ headers: {
1733
+ accept: "application/json",
1734
+ "content-type": "application/json",
1735
+ },
1736
+ body: JSON.stringify({
1737
+ clientName: `Lore CLI on ${hostname()}`,
1738
+ agents: input.agents,
1739
+ expiresInDays: 90,
1740
+ }),
1741
+ signal: AbortSignal.timeout(10_000),
1742
+ });
1743
+ }
1744
+ catch {
1745
+ throw new Error("Lore device authorization is unreachable. Verify --url and retry.");
1746
+ }
1747
+ if (!response.ok) {
1748
+ throw new Error(response.status === 404
1749
+ ? "This Lore server does not support browser approval. Upgrade the server or use --token."
1750
+ : "Lore could not start browser approval. Retry in a moment.");
1751
+ }
1752
+ const authorization = CreateDeviceAuthorizationResponseSchema.safeParse(await response.json().catch(() => null));
1753
+ if (!authorization.success) {
1754
+ throw new Error("Lore returned an incompatible device authorization response.");
1755
+ }
1756
+ const serverApprovalUrl = new URL(authorization.data.verificationUriComplete);
1757
+ const approvalUrl = input.dashboardUrl === undefined
1758
+ ? serverApprovalUrl.toString()
1759
+ : new URL(`${serverApprovalUrl.pathname}${serverApprovalUrl.search}`, `${input.dashboardUrl}/`).toString();
1760
+ process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approvalUrl}\n`);
1761
+ if (input.openBrowser) {
1762
+ const command = platform() === "darwin" ? "open" : "xdg-open";
1763
+ const child = spawn(command, [approvalUrl], {
1764
+ detached: true,
1765
+ stdio: "ignore",
1766
+ });
1767
+ child.once("error", () => {
1768
+ // The printed URL remains the safe fallback.
1769
+ });
1770
+ child.unref();
1771
+ }
1772
+ const expiresAt = Date.parse(authorization.data.expiresAt);
1773
+ const intervalMs = authorization.data.intervalSeconds * 1_000;
1774
+ while (Date.now() < expiresAt) {
1775
+ await delay(intervalMs);
1776
+ let pollResponse;
1777
+ try {
1778
+ pollResponse = await fetch(`${input.apiUrl}/v1/device-authorizations/poll`, {
1779
+ method: "POST",
1780
+ headers: {
1781
+ accept: "application/json",
1782
+ "content-type": "application/json",
1783
+ },
1784
+ body: JSON.stringify({
1785
+ deviceCode: authorization.data.deviceCode,
1786
+ }),
1787
+ signal: AbortSignal.timeout(10_000),
1788
+ });
1789
+ }
1790
+ catch {
1791
+ continue;
1792
+ }
1793
+ if (pollResponse.status === 429) {
1794
+ const retryAfter = Number(pollResponse.headers.get("retry-after"));
1795
+ if (Number.isFinite(retryAfter) && retryAfter > 0) {
1796
+ await delay(retryAfter * 1_000);
1797
+ }
1798
+ continue;
1799
+ }
1800
+ if (!pollResponse.ok) {
1801
+ throw new Error("Lore browser approval failed. Run connect --login again.");
1802
+ }
1803
+ const result = PollDeviceAuthorizationResponseSchema.safeParse(await pollResponse.json().catch(() => null));
1804
+ if (!result.success) {
1805
+ throw new Error("Lore returned an incompatible device authorization response.");
1806
+ }
1807
+ if (result.data.status === "pending") {
1808
+ continue;
1809
+ }
1810
+ if (result.data.status === "approved") {
1811
+ return result.data.token;
1812
+ }
1813
+ if (result.data.status === "denied") {
1814
+ throw new Error("Lore access was denied in the dashboard.");
1815
+ }
1816
+ throw new Error("Lore browser approval expired. Run connect --login again.");
1817
+ }
1818
+ throw new Error("Lore browser approval expired. Run connect --login again.");
1819
+ }
1820
+ async function connectCommand(args) {
1821
+ const parsed = parseConnectArguments(args);
1822
+ if (parsed === null) {
1823
+ return;
1824
+ }
1825
+ const paths = getLorePaths();
1826
+ const existing = await readConnectorConfig(paths);
1827
+ const hasProvidedToken = parsed.token !== undefined ||
1828
+ (process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
1829
+ (process.env.LORE_TOKEN?.trim() ?? "") !== "";
1830
+ const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
1831
+ ...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
1832
+ ...(parsed.dashboardUrl === undefined
1833
+ ? {}
1834
+ : { dashboardUrl: parsed.dashboardUrl }),
1835
+ environment: process.env,
1836
+ existing,
1837
+ preferHostedDefault: parsed.login || hasProvidedToken,
1838
+ });
1839
+ const configured = parsed.login
1840
+ ? undefined
1841
+ : configuredToken(parsed.token, existing);
1842
+ if (parsed.login && hasProvidedToken) {
1843
+ throw new Error("--login cannot be combined with an explicit or environment workspace token. Unset it or use the token directly.");
1844
+ }
1845
+ const token = parsed.login
1846
+ ? await requestDeviceAuthorization({
1847
+ apiUrl,
1848
+ ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
1849
+ agents: parsed.agents,
1850
+ openBrowser: parsed.openBrowser,
1851
+ })
1852
+ : configured;
1853
+ if (token === undefined || token.trim() === "") {
1854
+ throw new Error("Workspace token is required. Use --login, --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
1855
+ }
1856
+ const result = await connectWithCredential({
1857
+ apiUrl,
1858
+ ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
1859
+ token,
1860
+ agents: parsed.agents,
1861
+ ...(parsed.timeoutMs === undefined ? {} : { timeoutMs: parsed.timeoutMs }),
1862
+ });
1863
+ writeResult(result, parsed.json, `connected: ${result.agents.join(", ")}\napi_url: ${result.apiUrl}\nconfig: ${result.config}\n${result.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
1383
1864
  }
1384
1865
  async function queueCount(paths) {
1385
1866
  try {
@@ -1462,6 +1943,7 @@ async function inspectRuntimeInstallation(required, paths) {
1462
1943
  }
1463
1944
  const runtimeFiles = {
1464
1945
  "lore-hook.mjs": paths.runtime,
1946
+ "guard-evaluation.js": resolve(dirname(paths.runtime), "guard-evaluation.js"),
1465
1947
  "repository.js": paths.runtimeRepository,
1466
1948
  "reliability-store.js": paths.runtimeReliabilityStore,
1467
1949
  "signed-snapshot.js": paths.runtimeSignedSnapshot,
@@ -1575,6 +2057,14 @@ async function getAgentStatus(agent, config, paths, runtime) {
1575
2057
  units.push(...inspectInstallationUnits(document.value === undefined
1576
2058
  ? target.emptyValue()
1577
2059
  : document.value, agent, paths, index, configPaths.length));
2060
+ if (agent === "cursor") {
2061
+ const mcp = await readJsonDocument(resolve(dirname(path), "mcp.json"));
2062
+ const value = isObject(mcp.value) && isObject(mcp.value.mcpServers) ? mcp.value.mcpServers[CURSOR_MCP_NAME] : undefined;
2063
+ const owned = Number(isLoreCursorMcp(value));
2064
+ installed += owned;
2065
+ units.push({ id: installationUnitId("mcp", index, configPaths.length),
2066
+ state: installationUnitState(owned, Number(exactConfiguration(value, cursorMcpServer(paths)))) });
2067
+ }
1578
2068
  }
1579
2069
  catch {
1580
2070
  invalidConfiguration = true;
@@ -1665,6 +2155,7 @@ async function statusData(paths) {
1665
2155
  reliability: reliability.inspection,
1666
2156
  reliabilityError: reliability.error,
1667
2157
  agents,
2158
+ externalSurfaces: EXTERNAL_SURFACES,
1668
2159
  };
1669
2160
  }
1670
2161
  async function statusCommand(args) {
@@ -1676,10 +2167,13 @@ async function statusCommand(args) {
1676
2167
  const agentLines = data.agents
1677
2168
  .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}`)
1678
2169
  .join("\n");
2170
+ const externalSurfaceLines = data.externalSurfaces
2171
+ .map((surface) => `${surface.surface}: repository template, locally inspectable no, capture ${surface.capabilities.captureDurability}, guard ${surface.capabilities.guardBoundaryCoverage}/${surface.capabilities.enforcement}`)
2172
+ .join("\n");
1679
2173
  const metricRate = (value) => value === null || value === undefined
1680
2174
  ? "-"
1681
2175
  : `${(value * 100).toFixed(1)}%`;
1682
- 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`);
2176
+ 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`);
1683
2177
  }
1684
2178
  async function disconnectCommand(args) {
1685
2179
  const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
@@ -1697,6 +2191,17 @@ async function disconnectCommand(args) {
1697
2191
  ]);
1698
2192
  for (const installation of installations) {
1699
2193
  const path = installation.configPath;
2194
+ if (installation.agent === "cursor") {
2195
+ const mcpPath = resolve(dirname(path), "mcp.json");
2196
+ const mcp = await readJsonDocument(mcpPath);
2197
+ if (mcp.exists) {
2198
+ const result = await writeMergedJson(mcpPath, mcp, removeLoreCursorMcp(mcp.value), now);
2199
+ if (result.changed)
2200
+ changedHookFiles.push(mcpPath);
2201
+ if (result.backup)
2202
+ backups.push(result.backup);
2203
+ }
2204
+ }
1700
2205
  const document = await readJsonDocument(path);
1701
2206
  if (!document.exists) {
1702
2207
  continue;
@@ -1717,6 +2222,7 @@ async function disconnectCommand(args) {
1717
2222
  rm(paths.runtimeSignedSnapshot, { force: true }),
1718
2223
  rm(paths.runtimeCanonicalJson, { force: true }),
1719
2224
  rm(paths.runtimeContextFallback, { force: true }),
2225
+ rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
1720
2226
  rm(paths.runtimeInvocationHealthWriter, { force: true }),
1721
2227
  rm(paths.runtimeVersion, { force: true }),
1722
2228
  rm(paths.runtimePackage, { force: true }),
@@ -1821,6 +2327,19 @@ async function apiChecks(config) {
1821
2327
  }
1822
2328
  return checks;
1823
2329
  }
2330
+ async function verifyConnectedInstallation() {
2331
+ const paths = getLorePaths();
2332
+ const config = await readConnectorConfig(paths);
2333
+ if (config === null) {
2334
+ return false;
2335
+ }
2336
+ const status = await statusData(paths);
2337
+ if (status.configMode !== "600" ||
2338
+ status.agents.some((agent) => agent.configured && agent.installation.state !== "complete")) {
2339
+ return false;
2340
+ }
2341
+ return (await apiChecks(config)).every((check) => check.status !== "error");
2342
+ }
1824
2343
  async function doctorCommand(args) {
1825
2344
  const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
1826
2345
  if (parsed === null) {
@@ -1899,6 +2418,13 @@ async function doctorCommand(args) {
1899
2418
  : "invocation health is unavailable",
1900
2419
  });
1901
2420
  }
2421
+ for (const surface of status.externalSurfaces) {
2422
+ checks.push({
2423
+ name: `${surface.surface}-capability`,
2424
+ status: "ok",
2425
+ detail: "surface-specific repository configuration; local installation is not claimed",
2426
+ });
2427
+ }
1902
2428
  checks.push({
1903
2429
  name: "reliability-store",
1904
2430
  status: status.reliabilityError !== null
@@ -1979,6 +2505,9 @@ export async function runCli(args = process.argv.slice(2)) {
1979
2505
  case "ask":
1980
2506
  await runAskCommand(commandArgs, await readConnectorConfig(getLorePaths()));
1981
2507
  return;
2508
+ case "remember":
2509
+ await runRememberCommand(commandArgs, await readConnectorConfig(getLorePaths()));
2510
+ return;
1982
2511
  case "guard":
1983
2512
  await runGuardCommand(commandArgs, await readConnectorConfig(getLorePaths()));
1984
2513
  return;
@@ -2016,7 +2545,19 @@ export async function runCli(args = process.argv.slice(2)) {
2016
2545
  await doctorCommand(commandArgs);
2017
2546
  return;
2018
2547
  case "self-host":
2019
- await runSelfHostCommand(commandArgs);
2548
+ await runSelfHostCommand(commandArgs, process.env, {
2549
+ connect: async (request) => {
2550
+ const connected = await connectWithCredential({
2551
+ apiUrl: request.apiUrl,
2552
+ token: request.token,
2553
+ agents: request.agents,
2554
+ });
2555
+ return {
2556
+ agents: connected.agents,
2557
+ verified: await verifyConnectedInstallation(),
2558
+ };
2559
+ },
2560
+ });
2020
2561
  return;
2021
2562
  case "demo":
2022
2563
  await runDemoCommand(commandArgs, await readConnectorConfig(getLorePaths()));
@@ -2030,6 +2571,9 @@ export async function runCli(args = process.argv.slice(2)) {
2030
2571
  case "hook":
2031
2572
  await runHook(commandArgs);
2032
2573
  return;
2574
+ case "cursor-mcp":
2575
+ await (await import("./cursor-mcp.js")).runCursorMcpServer();
2576
+ return;
2033
2577
  case "internal-health-write": {
2034
2578
  const encoded = commandArgs[0] ?? (await readInvocationHealthWriteInput());
2035
2579
  await runInvocationHealthWrite(encoded);