@lore-co/cli 0.2.23 → 0.2.25

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 (59) hide show
  1. package/README.md +57 -9
  2. package/dist/ask.d.ts +2 -6
  3. package/dist/ask.d.ts.map +1 -1
  4. package/dist/ask.js +4 -21
  5. package/dist/ask.js.map +1 -1
  6. package/dist/chat.d.ts +15 -0
  7. package/dist/chat.d.ts.map +1 -0
  8. package/dist/chat.js +286 -0
  9. package/dist/chat.js.map +1 -0
  10. package/dist/cli.d.ts +59 -10
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +419 -97
  13. package/dist/cli.js.map +1 -1
  14. package/dist/command-help.d.ts +3 -1
  15. package/dist/command-help.d.ts.map +1 -1
  16. package/dist/command-help.js +5 -0
  17. package/dist/command-help.js.map +1 -1
  18. package/dist/devin-capture.d.ts +6 -0
  19. package/dist/devin-capture.d.ts.map +1 -1
  20. package/dist/devin-capture.js +34 -3
  21. package/dist/devin-capture.js.map +1 -1
  22. package/dist/devin-cli.d.ts +7 -2
  23. package/dist/devin-cli.d.ts.map +1 -1
  24. package/dist/devin-cli.js +81 -8
  25. package/dist/devin-cli.js.map +1 -1
  26. package/dist/generated-assets.d.ts +8 -6
  27. package/dist/generated-assets.d.ts.map +1 -1
  28. package/dist/generated-assets.js +8 -6
  29. package/dist/generated-assets.js.map +1 -1
  30. package/dist/generic-mcp.d.ts +35 -0
  31. package/dist/generic-mcp.d.ts.map +1 -0
  32. package/dist/generic-mcp.js +83 -0
  33. package/dist/generic-mcp.js.map +1 -0
  34. package/dist/lore-agent.d.ts +50 -0
  35. package/dist/lore-agent.d.ts.map +1 -0
  36. package/dist/lore-agent.js +299 -0
  37. package/dist/lore-agent.js.map +1 -0
  38. package/dist/mcp-config.d.ts +21 -4
  39. package/dist/mcp-config.d.ts.map +1 -1
  40. package/dist/mcp-config.js +66 -6
  41. package/dist/mcp-config.js.map +1 -1
  42. package/dist/owned-files.d.ts +21 -0
  43. package/dist/owned-files.d.ts.map +1 -0
  44. package/dist/owned-files.js +88 -0
  45. package/dist/owned-files.js.map +1 -0
  46. package/dist/recovery-cloud.d.ts +1 -1
  47. package/dist/runtime.d.ts.map +1 -1
  48. package/dist/runtime.js +50 -19
  49. package/dist/runtime.js.map +1 -1
  50. package/dist/self-host.d.ts +1 -1
  51. package/dist/self-host.js +2 -2
  52. package/dist/skills.d.ts +2 -0
  53. package/dist/skills.d.ts.map +1 -1
  54. package/dist/skills.js +46 -97
  55. package/dist/skills.js.map +1 -1
  56. package/dist/update.d.ts.map +1 -1
  57. package/dist/update.js +7 -4
  58. package/dist/update.js.map +1 -1
  59. package/package.json +6 -5
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { parseDirectoryRules, checkDirectory, checkLoreAccess } from "./directory-policy.js";
2
+ import { parseDirectoryRules, checkDirectory, checkLoreAccess, capturePauseMessage } from "./directory-policy.js";
3
3
  import { reportCommand, runTelemetryCommand, telemetryEnabled, trackedCommand, TELEMETRY_HELP } from "./telemetry.js";
4
4
  import { secureWindowsDirectory, windowsPermissionsArePrivate } from "./windows-permissions.js";
5
5
  import { COMMAND_GUIDES, ROOT_HELP, helpTopic } from "./command-help.js";
@@ -8,11 +8,11 @@ import { resolveConnectProject, validateConnectProjectOptions } from "./connect-
8
8
  import { captureDirectories } from "./capture-scope.js";
9
9
  import { prepareConnection, persistConnection, listConnections } from "./connections.js";
10
10
  import { connectionReferences, resolveLocalConnection } from "./directory-policy.js";
11
- import { withDirectoryLock } from "./reliability-store.js";
11
+ import { atomicWriteJson, withDirectoryLock } from "./reliability-store.js";
12
12
  import { runConfigCommand } from "./config-command.js";
13
13
  import { codexApprovalDetail, inspectCodexHookApproval } from "./codex-hook-approval.js";
14
- import { claudeOnlyCommand, disableDevinCli, runDevinCliHook } from "./devin-cli.js";
15
- import { LORE_MCP_SERVER_NAME, MCP_HOST_AGENTS, inspectCodexMcp, isLoreMcpServer, loreMcpServerEntry, mergeLoreCodexMcp, mergeLoreMcpServers, removeLoreCodexMcp, removeLoreMcpServers, } from "./mcp-config.js";
14
+ import { claudeOnlyCommand, disableDevinCli, refreshDevinHooks, runDevinCliHook } from "./devin-cli.js";
15
+ import { LORE_MCP_SERVER_NAME, MCP_HOST_AGENTS, inspectCodexMcp, isLoreMcpServer, isLoreOpenCodeMcp, loreMcpServerEntry, loreVsCodeMcpServer, mergeLoreCodexMcp, mergeLoreMcpServers, mergeLoreOpenCodeMcp, mergeLoreVsCodeMcp, openCodeMcpEntry, removeLoreCodexMcp, removeLoreMcpServers, removeLoreOpenCodeMcp, removeLoreVsCodeMcp, } from "./mcp-config.js";
16
16
  import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
17
17
  import { constants as fsConstants, realpathSync } from "node:fs";
18
18
  import { spawn, isWindows, windowsCommand, commandText, findExecutable, openBrowser } from "./platform.js";
@@ -25,11 +25,14 @@ import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, COPILOT_
25
25
  import { runHook, } from "./runtime.js";
26
26
  import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
27
27
  import { runAskCommand } from "./ask.js";
28
+ import { runChatCommand } from "./chat.js";
28
29
  import { runLearningsCommand } from "./learnings.js";
29
30
  import { runReviewCommand } from "./review.js";
30
31
  import { runEmbeddingsCommand } from "./embeddings.js";
31
32
  import { runRememberCommand } from "./remember.js";
32
33
  import { installLoreSkill, removeLoreSkill } from "./skills.js";
34
+ import { GENERIC_AGENT, genericInstructions, genericMcpServers, parseGenericAgentId, readGenericAgents, recordGenericAgent, removeGenericAgents } from "./generic-mcp.js";
35
+ import { installLoreAgent, removeLoreAgent } from "./lore-agent.js";
33
36
  import { runApproachCommand } from "./approach.js";
34
37
  import { runSkillCommand } from "./skill-command.js";
35
38
  import { runGuardCommand } from "./guard.js";
@@ -60,6 +63,20 @@ const HOOK_EVENTS = [
60
63
  "Stop",
61
64
  "SessionEnd",
62
65
  ];
66
+ /**
67
+ * Hook events that exist only to record turns. Retrieval-only mode leaves them
68
+ * uninstalled, so the agent never hands a response to Lore at all.
69
+ */
70
+ const CAPTURE_HOOK_EVENTS = new Set([
71
+ "Stop", "SessionEnd", "afterAgentResponse", "sessionEnd", "agentStop", "post_model_turn",
72
+ ]);
73
+ function installedEvents(events, capture) {
74
+ return capture ? [...events] : events.filter((event) => !CAPTURE_HOOK_EVENTS.has(event));
75
+ }
76
+ /** Capture mode of a connection: absent means on. */
77
+ export function captureEnabled(config) {
78
+ return config?.capture !== "off";
79
+ }
63
80
  /** Tools the Context Guard PreToolUse hook cares about (regex matcher). */
64
81
  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";
65
82
  const CURSOR_HOOK_EVENTS = [
@@ -112,12 +129,16 @@ Options:
112
129
  --repository <repo> Verify the selected folder matches this GitHub repository
113
130
  --folder <path> Record only in this folder and subfolders (repeatable)
114
131
  --all-projects Record in all projects allowed by your workspace
132
+ --no-capture Retrieval-only mode: context, guard, and manual saves, no turn recording
133
+ --capture Turn automatic turn recording back on
115
134
  --url <url> Lore API base URL (default: https://api.uselore.co)
116
135
  --dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
117
136
  --login Approve access securely in the Lore dashboard
118
137
  --no-open Print the approval URL without opening a browser
119
138
  --token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
120
- --agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, opencode, polytoken, or t3code
139
+ --agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, generic, opencode, polytoken, or t3code
140
+ --agent-id <id> With --agent generic: credit MCP saves to this agent (default: generic)
141
+ --write With --agent generic: let the agent save through MCP (saves stay proposals)
121
142
  --timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
122
143
  --fallback <agent> claude, codex, opencode, devin, cursor, copilot-cli, polytoken
123
144
  --no-recovery Disable automatic handoff setup (otherwise enabled with two installed clients)
@@ -125,11 +146,14 @@ Options:
125
146
  --help Show this command's help
126
147
 
127
148
  New connections stay paused until you choose a project, a folder, or --all-projects.
128
- Reconnecting without a capture option keeps your saved folder rules.
129
- Folder choices control capture; learning scope controls where knowledge applies.
149
+ Reconnecting without a capture option keeps your saved folder rules and capture mode.
150
+ Folder choices control where Lore runs; learning scope controls where knowledge applies.
151
+ With --no-capture, Lore installs no Stop or SessionEnd hooks and sends no turns.
152
+ Context injection, Guard, lore ask, and lore remember keep working. See lore capture.
130
153
 
131
154
  Examples:
132
155
  lore connect --login --agent claude --folder .
156
+ lore connect --login --agent claude --folder . --no-capture
133
157
  lore connect --login --agent cowork
134
158
  lore connect --login --agent devin --folder .
135
159
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
@@ -156,6 +180,24 @@ Examples:
156
180
  lore doctor
157
181
  lore doctor --json
158
182
  `;
183
+ const CAPTURE_HELP = `lore capture
184
+ Choose whether this machine records agent turns automatically.
185
+
186
+ Usage:
187
+ lore capture status [--json]
188
+ lore capture off [--json]
189
+ lore capture on [--json]
190
+
191
+ With capture off, Lore installs no Stop or SessionEnd hooks and never sends a
192
+ prompt or response as a turn. Context injection, Guard, lore ask, lore remember,
193
+ and the lore skill's "remember this session" keep working. Folder rules still
194
+ decide where Lore runs at all.
195
+
196
+ Examples:
197
+ lore capture off
198
+ lore capture status
199
+ lore capture on
200
+ `;
159
201
  const DISCONNECT_HELP = `lore disconnect
160
202
  Remove Lore-owned hooks/plugin, credentials, runtime, and transient state.
161
203
  Durable captures, dead letters, verified snapshots, unrelated agent settings,
@@ -224,6 +266,7 @@ function isConnectAgent(value) {
224
266
  return (isConfiguredAgent(value) ||
225
267
  value === "copilot" ||
226
268
  isCredentialOnlyAgent(value) ||
269
+ value === "generic" ||
227
270
  value === "t3code");
228
271
  }
229
272
  function isCredentialOnlyAgent(value) {
@@ -293,24 +336,29 @@ function eventHandler(agent, event, paths) {
293
336
  : {}),
294
337
  };
295
338
  }
296
- export function mergeLoreHooks(input, agent, paths) {
339
+ export function mergeLoreHooks(input, agent, paths, capture = true) {
297
340
  const result = cloneObject(input);
298
341
  if (result.hooks !== undefined && !isObject(result.hooks)) {
299
342
  throw new Error("Agent configuration field \"hooks\" must be a JSON object");
300
343
  }
301
344
  const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
345
+ const installed = installedEvents(HOOK_EVENTS, capture);
302
346
  for (const event of HOOK_EVENTS) {
303
347
  if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
304
348
  throw new Error(`Agent hook event "${event}" must be a JSON array`);
305
349
  }
306
- hooks[event] = [
350
+ const groups = [
307
351
  ...stripLoreFromEvent(hooks[event]),
308
- {
309
- // Guard checks run only for mutation-shaped tools.
310
- ...(event === "PreToolUse" ? { matcher: GUARD_TOOL_MATCHER } : {}),
311
- hooks: [eventHandler(agent, event, paths)],
312
- },
352
+ ...(installed.includes(event) ? [{
353
+ // Guard checks run only for mutation-shaped tools.
354
+ ...(event === "PreToolUse" ? { matcher: GUARD_TOOL_MATCHER } : {}),
355
+ hooks: [eventHandler(agent, event, paths)],
356
+ }] : []),
313
357
  ];
358
+ if (groups.length === 0)
359
+ delete hooks[event];
360
+ else
361
+ hooks[event] = groups;
314
362
  }
315
363
  result.hooks = hooks;
316
364
  return result;
@@ -404,8 +452,12 @@ function isMcpHostAgent(agent) {
404
452
  return MCP_HOST_AGENTS.includes(agent);
405
453
  }
406
454
  /** The full Lore MCP server, run by this CLI so it shares the hooks' credentials and version. */
407
- export function loreMcpServer(paths) {
408
- return loreMcpServerEntry([process.execPath, ...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)])], paths.home);
455
+ export function loreMcpServer(paths, agent) {
456
+ return loreMcpServerEntry(loreMcpInvocation(), paths.home, agent);
457
+ }
458
+ /** This CLI's executable plus its script path (none for the standalone binary). */
459
+ function loreMcpInvocation() {
460
+ return [process.execPath, ...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)])];
409
461
  }
410
462
  /** Where each agent lists MCP servers, located from the hooks file Lore already manages. */
411
463
  export function mcpConfigPath(agent, hooksPath) {
@@ -420,18 +472,29 @@ export function mcpConfigPath(agent, hooksPath) {
420
472
  return resolve(directory, "config.toml");
421
473
  case "cursor":
422
474
  return resolve(directory, "mcp.json");
475
+ case "copilot-cli":
476
+ // $COPILOT_HOME/hooks/lore.json pairs with $COPILOT_HOME/mcp-config.json.
477
+ return resolve(directory, "..", "mcp-config.json");
478
+ case "copilot-vscode":
479
+ // <repo>/.github/hooks/lore-vscode.json pairs with <repo>/.vscode/mcp.json.
480
+ return resolve(directory, "..", "..", ".vscode", "mcp.json");
423
481
  }
424
482
  }
425
483
  export function mergeLoreMcp(input, agent, paths) {
426
- return mergeLoreMcpServers(objectConfiguration(input, agent), loreMcpServer(paths), agent);
484
+ const config = objectConfiguration(input, agent);
485
+ // VS Code keeps its servers under `servers`; the other JSON hosts use `mcpServers`.
486
+ return agent === "copilot-vscode"
487
+ ? mergeLoreVsCodeMcp(config, loreMcpServer(paths, agent))
488
+ : mergeLoreMcpServers(config, loreMcpServer(paths, agent), agent);
427
489
  }
428
490
  export function removeLoreMcp(input, agent) {
429
- return removeLoreMcpServers(objectConfiguration(input, agent));
491
+ const config = objectConfiguration(input, agent);
492
+ return agent === "copilot-vscode" ? removeLoreVsCodeMcp(config) : removeLoreMcpServers(config);
430
493
  }
431
494
  function stripLoreFromCursorEvent(value) {
432
495
  return Array.isArray(value) ? value.filter((hook) => !isLoreHook(hook)) : [];
433
496
  }
434
- export function mergeLoreCursorHooks(input, paths) {
497
+ export function mergeLoreCursorHooks(input, paths, capture = true) {
435
498
  const result = cloneObject(input);
436
499
  if (result.version !== undefined && result.version !== 1) {
437
500
  throw new Error('Cursor configuration field "version" must be 1');
@@ -440,14 +503,19 @@ export function mergeLoreCursorHooks(input, paths) {
440
503
  throw new Error('Cursor configuration field "hooks" must be a JSON object');
441
504
  }
442
505
  const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
506
+ const installed = installedEvents(CURSOR_HOOK_EVENTS, capture);
443
507
  for (const event of CURSOR_HOOK_EVENTS) {
444
508
  if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
445
509
  throw new Error(`Cursor hook event "${event}" must be a JSON array`);
446
510
  }
447
- hooks[event] = [
511
+ const entries = [
448
512
  ...stripLoreFromCursorEvent(hooks[event]),
449
- cursorEventHandler(event, paths),
513
+ ...(installed.includes(event) ? [cursorEventHandler(event, paths)] : []),
450
514
  ];
515
+ if (entries.length === 0)
516
+ delete hooks[event];
517
+ else
518
+ hooks[event] = entries;
451
519
  }
452
520
  result.version = 1;
453
521
  result.hooks = hooks;
@@ -522,7 +590,7 @@ function stripLoreFromCopilotEvent(value) {
522
590
  ? value.filter((hook) => !isLoreCopilotHook(hook))
523
591
  : [];
524
592
  }
525
- export function mergeLoreCopilotCliHooks(input, paths) {
593
+ export function mergeLoreCopilotCliHooks(input, paths, capture = true) {
526
594
  const result = cloneObject(input);
527
595
  if (result.version !== undefined && result.version !== 1) {
528
596
  throw new Error('Copilot hook configuration field "version" must be 1');
@@ -531,14 +599,19 @@ export function mergeLoreCopilotCliHooks(input, paths) {
531
599
  throw new Error('Copilot hook configuration field "hooks" must be an object');
532
600
  }
533
601
  const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
602
+ const installed = installedEvents(COPILOT_CLI_HOOK_EVENTS, capture);
534
603
  for (const event of COPILOT_CLI_HOOK_EVENTS) {
535
604
  if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
536
605
  throw new Error(`Copilot hook event "${event}" must be an array`);
537
606
  }
538
- hooks[event] = [
607
+ const entries = [
539
608
  ...stripLoreFromCopilotEvent(hooks[event]),
540
- copilotCliEventHandler(event, paths),
609
+ ...(installed.includes(event) ? [copilotCliEventHandler(event, paths)] : []),
541
610
  ];
611
+ if (entries.length === 0)
612
+ delete hooks[event];
613
+ else
614
+ hooks[event] = entries;
542
615
  }
543
616
  result.version = 1;
544
617
  result.hooks = hooks;
@@ -595,7 +668,7 @@ function copilotVscodeEventHandler(event, paths) {
595
668
  : 25,
596
669
  };
597
670
  }
598
- export function mergeLoreCopilotVscodeHooks(input, paths) {
671
+ export function mergeLoreCopilotVscodeHooks(input, paths, capture = true) {
599
672
  const result = cloneObject(input);
600
673
  if (result.version !== undefined && result.version !== 1) {
601
674
  throw new Error('VS Code Copilot hook field "version" must be 1');
@@ -604,14 +677,19 @@ export function mergeLoreCopilotVscodeHooks(input, paths) {
604
677
  throw new Error('VS Code Copilot hook field "hooks" must be an object');
605
678
  }
606
679
  const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
680
+ const installed = installedEvents(HOOK_EVENTS, capture);
607
681
  for (const event of HOOK_EVENTS) {
608
682
  if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
609
683
  throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
610
684
  }
611
- hooks[event] = [
685
+ const entries = [
612
686
  ...stripLoreFromCursorEvent(hooks[event]),
613
- copilotVscodeEventHandler(event, paths),
687
+ ...(installed.includes(event) ? [copilotVscodeEventHandler(event, paths)] : []),
614
688
  ];
689
+ if (entries.length === 0)
690
+ delete hooks[event];
691
+ else
692
+ hooks[event] = entries;
615
693
  }
616
694
  result.version = 1;
617
695
  result.hooks = hooks;
@@ -671,14 +749,14 @@ function isLorePolytokenHook(value) {
671
749
  (/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
672
750
  command.includes("/.lore/bin/lore-hook.mjs")));
673
751
  }
674
- export function mergeLorePolytokenHooks(input, paths) {
752
+ export function mergeLorePolytokenHooks(input, paths, capture = true) {
675
753
  if (!Array.isArray(input)) {
676
754
  throw new Error("Polytoken hooks must contain a JSON array");
677
755
  }
678
756
  const retained = input.filter((hook) => !isLorePolytokenHook(hook));
679
757
  return [
680
758
  ...structuredClone(retained),
681
- ...POLYTOKEN_HOOK_EVENTS.map((event) => polytokenEventHandler(event, paths)),
759
+ ...installedEvents(POLYTOKEN_HOOK_EVENTS, capture).map((event) => polytokenEventHandler(event, paths)),
682
760
  ];
683
761
  }
684
762
  export function removeLorePolytokenHooks(input) {
@@ -727,6 +805,9 @@ export function countLoreOpenCodePlugins(input) {
727
805
  ? input.plugin.filter(isLoreOpenCodePlugin).length
728
806
  : 0;
729
807
  }
808
+ function countLoreOpenCodeMcp(input) {
809
+ return isObject(input.mcp) && isLoreOpenCodeMcp(input.mcp[LORE_MCP_SERVER_NAME]) ? 1 : 0;
810
+ }
730
811
  function installationUnitId(event, index, total) {
731
812
  return total === 1 ? event : `installation-${index + 1}/${event}`;
732
813
  }
@@ -742,13 +823,13 @@ function installationUnitState(ownedCount, exactCount) {
742
823
  function exactConfiguration(left, right) {
743
824
  return canonicalJson(left) === canonicalJson(right);
744
825
  }
745
- function commandHookInstallationUnits(input, agent, paths, index, total) {
826
+ function commandHookInstallationUnits(input, agent, paths, index, total, capture) {
746
827
  const configuration = objectConfiguration(input, agent);
747
828
  if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
748
829
  throw new Error(`${agent} configuration field "hooks" must be an object`);
749
830
  }
750
831
  const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
751
- return HOOK_EVENTS.map((event) => {
832
+ return installedEvents(HOOK_EVENTS, capture).map((event) => {
752
833
  const groups = hooks[event];
753
834
  if (groups !== undefined && !Array.isArray(groups)) {
754
835
  throw new Error(`${agent} hook event "${event}" must be an array`);
@@ -771,7 +852,7 @@ function commandHookInstallationUnits(input, agent, paths, index, total) {
771
852
  };
772
853
  });
773
854
  }
774
- function cursorInstallationUnits(input, paths, index, total) {
855
+ function cursorInstallationUnits(input, paths, index, total, capture) {
775
856
  const configuration = objectConfiguration(input, "cursor");
776
857
  if (configuration.version !== undefined && configuration.version !== 1) {
777
858
  throw new Error('Cursor configuration field "version" must be 1');
@@ -780,7 +861,7 @@ function cursorInstallationUnits(input, paths, index, total) {
780
861
  throw new Error('Cursor configuration field "hooks" must be an object');
781
862
  }
782
863
  const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
783
- return CURSOR_HOOK_EVENTS.map((event) => {
864
+ return installedEvents(CURSOR_HOOK_EVENTS, capture).map((event) => {
784
865
  const values = hooks[event];
785
866
  if (values !== undefined && !Array.isArray(values)) {
786
867
  throw new Error(`Cursor hook event "${event}" must be an array`);
@@ -795,7 +876,7 @@ function cursorInstallationUnits(input, paths, index, total) {
795
876
  };
796
877
  });
797
878
  }
798
- function copilotCliInstallationUnits(input, paths, index, total) {
879
+ function copilotCliInstallationUnits(input, paths, index, total, capture) {
799
880
  const configuration = objectConfiguration(input, "copilot-cli");
800
881
  if (configuration.version !== undefined && configuration.version !== 1) {
801
882
  throw new Error('Copilot hook configuration field "version" must be 1');
@@ -804,7 +885,7 @@ function copilotCliInstallationUnits(input, paths, index, total) {
804
885
  throw new Error('Copilot hook configuration field "hooks" must be an object');
805
886
  }
806
887
  const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
807
- return COPILOT_CLI_HOOK_EVENTS.map((event) => {
888
+ return installedEvents(COPILOT_CLI_HOOK_EVENTS, capture).map((event) => {
808
889
  const values = hooks[event];
809
890
  if (values !== undefined && !Array.isArray(values)) {
810
891
  throw new Error(`Copilot hook event "${event}" must be an array`);
@@ -819,7 +900,7 @@ function copilotCliInstallationUnits(input, paths, index, total) {
819
900
  };
820
901
  });
821
902
  }
822
- function copilotVscodeInstallationUnits(input, paths, index, total) {
903
+ function copilotVscodeInstallationUnits(input, paths, index, total, capture) {
823
904
  const configuration = objectConfiguration(input, "copilot-vscode");
824
905
  if (configuration.version !== undefined && configuration.version !== 1) {
825
906
  throw new Error('VS Code Copilot hook field "version" must be 1');
@@ -828,7 +909,7 @@ function copilotVscodeInstallationUnits(input, paths, index, total) {
828
909
  throw new Error('VS Code Copilot hook field "hooks" must be an object');
829
910
  }
830
911
  const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
831
- return HOOK_EVENTS.map((event) => {
912
+ return installedEvents(HOOK_EVENTS, capture).map((event) => {
832
913
  const values = hooks[event];
833
914
  if (values !== undefined && !Array.isArray(values)) {
834
915
  throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
@@ -843,11 +924,11 @@ function copilotVscodeInstallationUnits(input, paths, index, total) {
843
924
  };
844
925
  });
845
926
  }
846
- function polytokenInstallationUnits(input, paths, index, total) {
927
+ function polytokenInstallationUnits(input, paths, index, total, capture) {
847
928
  if (!Array.isArray(input)) {
848
929
  throw new Error("Polytoken hooks must contain a JSON array");
849
930
  }
850
- return POLYTOKEN_HOOK_EVENTS.map((event) => {
931
+ return installedEvents(POLYTOKEN_HOOK_EVENTS, capture).map((event) => {
851
932
  const expected = polytokenEventHandler(event, paths);
852
933
  const owned = input.filter((candidate) => isLorePolytokenHook(candidate) &&
853
934
  (!isObject(candidate) || candidate.event === event));
@@ -858,7 +939,7 @@ function polytokenInstallationUnits(input, paths, index, total) {
858
939
  };
859
940
  });
860
941
  }
861
- function openCodeInstallationUnits(input, index, total) {
942
+ function openCodeInstallationUnits(input, paths, index, total) {
862
943
  const configuration = objectConfiguration(input, "opencode");
863
944
  if (configuration.plugin !== undefined &&
864
945
  !Array.isArray(configuration.plugin)) {
@@ -869,28 +950,37 @@ function openCodeInstallationUnits(input, index, total) {
869
950
  : [];
870
951
  const owned = plugins.filter(isLoreOpenCodePlugin);
871
952
  const exactCount = owned.filter((candidate) => candidate === LORE_OPENCODE_PLUGIN).length;
953
+ if (configuration.mcp !== undefined && !isObject(configuration.mcp)) {
954
+ throw new Error('OpenCode configuration field "mcp" must be a JSON object');
955
+ }
956
+ const server = isObject(configuration.mcp) ? configuration.mcp[LORE_MCP_SERVER_NAME] : undefined;
957
+ const ownedServer = Number(isLoreOpenCodeMcp(server));
872
958
  return [
873
959
  {
874
960
  id: installationUnitId("plugin", index, total),
875
961
  state: installationUnitState(owned.length, exactCount),
876
962
  },
963
+ {
964
+ id: installationUnitId("mcp-tools", index, total),
965
+ state: installationUnitState(ownedServer, Number(exactConfiguration(server, openCodeMcpEntry(loreMcpServer(paths, "opencode"))))),
966
+ },
877
967
  ];
878
968
  }
879
- function inspectInstallationUnits(input, agent, paths, index, total) {
969
+ function inspectInstallationUnits(input, agent, paths, index, total, capture) {
880
970
  switch (agent) {
881
971
  case "claude":
882
972
  case "codex":
883
- return commandHookInstallationUnits(input, agent, paths, index, total);
973
+ return commandHookInstallationUnits(input, agent, paths, index, total, capture);
884
974
  case "copilot-cli":
885
- return copilotCliInstallationUnits(input, paths, index, total);
975
+ return copilotCliInstallationUnits(input, paths, index, total, capture);
886
976
  case "copilot-vscode":
887
- return copilotVscodeInstallationUnits(input, paths, index, total);
977
+ return copilotVscodeInstallationUnits(input, paths, index, total, capture);
888
978
  case "cursor":
889
- return cursorInstallationUnits(input, paths, index, total);
979
+ return cursorInstallationUnits(input, paths, index, total, capture);
890
980
  case "polytoken":
891
- return polytokenInstallationUnits(input, paths, index, total);
981
+ return polytokenInstallationUnits(input, paths, index, total, capture);
892
982
  case "opencode":
893
- return openCodeInstallationUnits(input, index, total);
983
+ return openCodeInstallationUnits(input, paths, index, total);
894
984
  }
895
985
  }
896
986
  async function readJsonDocument(path) {
@@ -1042,6 +1132,7 @@ function parseConnectorConfig(value) {
1042
1132
  Object.keys(agentConfigPaths).length === 0
1043
1133
  ? {}
1044
1134
  : { agentConfigPaths }),
1135
+ ...(value.capture === "off" ? { capture: "off" } : {}),
1045
1136
  connectedAt: value.connectedAt,
1046
1137
  timeoutMs,
1047
1138
  };
@@ -1209,80 +1300,83 @@ const AGENT_TARGETS = {
1209
1300
  claude: {
1210
1301
  integration: "hooks",
1211
1302
  // Native hooks plus the Lore MCP server entry.
1212
- expected: HOOK_EVENTS.length + 1,
1303
+ expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
1213
1304
  runtimeRequired: true,
1214
1305
  emptyValue: () => ({}),
1215
1306
  configPath: (paths) => paths.claudeSettings,
1216
1307
  detect: async () => commandExists("claude"),
1217
1308
  executable: async () => commandExists("claude"),
1218
- merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths),
1309
+ merge: (input, paths, capture) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths, capture),
1219
1310
  remove: (input) => removeLoreHooks(objectConfiguration(input, "claude")),
1220
1311
  count: (input) => countLoreHooks(objectConfiguration(input, "claude")),
1221
1312
  },
1222
1313
  codex: {
1223
1314
  integration: "hooks",
1224
- expected: HOOK_EVENTS.length + 1,
1315
+ expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
1225
1316
  runtimeRequired: true,
1226
1317
  emptyValue: () => ({}),
1227
1318
  configPath: (paths) => paths.codexHooks,
1228
1319
  detect: async () => commandExists("codex"),
1229
1320
  executable: async () => commandExists("codex"),
1230
- merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths),
1321
+ merge: (input, paths, capture) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths, capture),
1231
1322
  remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
1232
1323
  count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
1233
1324
  },
1234
1325
  "copilot-cli": {
1235
1326
  integration: "hooks",
1236
- expected: COPILOT_CLI_HOOK_EVENTS.length,
1327
+ // Native hooks plus the Lore MCP server entry.
1328
+ expected: (capture) => installedEvents(COPILOT_CLI_HOOK_EVENTS, capture).length + 1,
1237
1329
  runtimeRequired: true,
1238
1330
  emptyValue: () => ({}),
1239
1331
  configPath: (paths) => paths.copilotCliHooks,
1240
1332
  detect: async (paths) => (await commandExists("copilot")) || pathExists(paths.copilotCliHooks),
1241
1333
  executable: async () => commandExists("copilot"),
1242
- merge: (input, paths) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths),
1334
+ merge: (input, paths, capture) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths, capture),
1243
1335
  remove: (input) => removeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
1244
1336
  count: (input) => countLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
1245
1337
  },
1246
1338
  "copilot-vscode": {
1247
1339
  integration: "hooks",
1248
- expected: HOOK_EVENTS.length,
1340
+ // Repository hooks plus the Lore MCP server entry in .vscode/mcp.json.
1341
+ expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
1249
1342
  runtimeRequired: true,
1250
1343
  emptyValue: () => ({}),
1251
1344
  configPath: (paths) => paths.copilotVscodeHooks,
1252
1345
  detect: async (paths) => pathExists(paths.copilotVscodeHooks),
1253
1346
  executable: async () => commandExists("code"),
1254
- merge: (input, paths) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths),
1347
+ merge: (input, paths, capture) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths, capture),
1255
1348
  remove: (input) => removeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
1256
1349
  count: (input) => countLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
1257
1350
  },
1258
1351
  cursor: {
1259
1352
  integration: "hooks",
1260
1353
  // Hooks, the capture bridge, and the Lore MCP server entry.
1261
- expected: CURSOR_HOOK_EVENTS.length + 2,
1354
+ expected: (capture) => installedEvents(CURSOR_HOOK_EVENTS, capture).length + 2,
1262
1355
  runtimeRequired: true,
1263
1356
  emptyValue: () => ({}),
1264
1357
  configPath: (paths) => paths.cursorHooks,
1265
1358
  detect: async (paths) => (await cursorAvailable(paths)) || pathExists(paths.cursorHooks),
1266
1359
  executable: cursorAvailable,
1267
- merge: (input, paths) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths),
1360
+ merge: (input, paths, capture) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths, capture),
1268
1361
  remove: (input) => removeLoreCursorHooks(objectConfiguration(input, "cursor")),
1269
1362
  count: (input) => countLoreCursorHooks(objectConfiguration(input, "cursor")),
1270
1363
  },
1271
1364
  opencode: {
1272
1365
  integration: "plugin",
1273
- expected: 1,
1366
+ // The plugin entry plus the Lore MCP server entry, both in opencode.json.
1367
+ expected: () => 2,
1274
1368
  runtimeRequired: false,
1275
1369
  emptyValue: () => ({}),
1276
1370
  configPath: (paths) => paths.openCodeConfig,
1277
1371
  detect: async (paths) => (await commandExists("opencode")) || pathExists(paths.openCodeConfig),
1278
1372
  executable: async () => commandExists("opencode"),
1279
- merge: (input) => mergeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
1280
- remove: (input) => removeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
1281
- count: (input) => countLoreOpenCodePlugins(objectConfiguration(input, "opencode")),
1373
+ merge: (input, paths) => mergeLoreOpenCodeMcp(mergeLoreOpenCodePlugin(objectConfiguration(input, "opencode")), loreMcpServer(paths, "opencode")),
1374
+ remove: (input) => removeLoreOpenCodeMcp(removeLoreOpenCodePlugin(objectConfiguration(input, "opencode"))),
1375
+ count: (input) => countLoreOpenCodePlugins(objectConfiguration(input, "opencode")) + countLoreOpenCodeMcp(objectConfiguration(input, "opencode")),
1282
1376
  },
1283
1377
  polytoken: {
1284
1378
  integration: "hooks",
1285
- expected: POLYTOKEN_HOOK_EVENTS.length,
1379
+ expected: (capture) => installedEvents(POLYTOKEN_HOOK_EVENTS, capture).length,
1286
1380
  runtimeRequired: true,
1287
1381
  emptyValue: () => [],
1288
1382
  configPath: (paths) => paths.polytokenHooks,
@@ -1465,7 +1559,7 @@ async function readT3CodeProviderInstallations(paths) {
1465
1559
  }
1466
1560
  async function expandConnectInstallations(requested, paths) {
1467
1561
  const direct = requested.flatMap((agent) => {
1468
- if (isCredentialOnlyAgent(agent) || agent === "t3code") {
1562
+ if (isCredentialOnlyAgent(agent) || agent === "t3code" || agent === GENERIC_AGENT) {
1469
1563
  return [];
1470
1564
  }
1471
1565
  return [
@@ -1498,6 +1592,7 @@ async function installRuntime(paths) {
1498
1592
  rm(paths.runtime, { force: true }),
1499
1593
  rm(paths.runtimeRepository, { force: true }),
1500
1594
  rm(resolve(dirname(paths.runtime), "repository-address.js"), { force: true }),
1595
+ rm(resolve(dirname(paths.runtime), "capture-policy.js"), { force: true }),
1501
1596
  rm(resolve(dirname(paths.runtime), "directory-policy.js"), { force: true }),
1502
1597
  rm(paths.runtimeReliabilityStore, { force: true }),
1503
1598
  rm(resolve(dirname(paths.runtime), "windows-permissions.js"), { force: true }),
@@ -1556,6 +1651,8 @@ async function installRuntime(paths) {
1556
1651
  "skill-guidance.js": await readFile(resolve(sourceDirectory, "skill-guidance.js"), "utf8"),
1557
1652
  "repository.js": repository,
1558
1653
  "repository-address.js": await readFile(resolve(dirname(fileURLToPath(import.meta.resolve("@lore-co/core/repository-location"))), "repository-address.js"), "utf8"),
1654
+ // directory-policy.js reads the live capture switches through this dependency-free module.
1655
+ "capture-policy.js": await readFile(resolve(dirname(fileURLToPath(import.meta.resolve("@lore-co/core/local-scope"))), "capture-policy.js"), "utf8"),
1559
1656
  "directory-policy.js": (await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/local-scope")), "utf8")).replaceAll("./repository-location.js", "./repository.js"),
1560
1657
  "reliability-store.js": reliabilityStore,
1561
1658
  "windows-permissions.js": await readFile(resolve(sourceDirectory, "windows-permissions.js"), "utf8"),
@@ -1589,6 +1686,7 @@ async function installRuntime(paths) {
1589
1686
  atomicWrite(resolve(dirname(paths.runtime), "skill-guidance.js"), runtimeFiles["skill-guidance.js"], 0o600),
1590
1687
  atomicWrite(paths.runtimeRepository, repository, 0o600),
1591
1688
  atomicWrite(resolve(dirname(paths.runtime), "repository-address.js"), runtimeFiles["repository-address.js"], 0o600),
1689
+ atomicWrite(resolve(dirname(paths.runtime), "capture-policy.js"), runtimeFiles["capture-policy.js"], 0o600),
1592
1690
  atomicWrite(resolve(dirname(paths.runtime), "directory-policy.js"), runtimeFiles["directory-policy.js"], 0o600),
1593
1691
  atomicWrite(paths.runtimeReliabilityStore, reliabilityStore, 0o600),
1594
1692
  atomicWrite(resolve(dirname(paths.runtime), "windows-permissions.js"), runtimeFiles["windows-permissions.js"], 0o600),
@@ -1606,9 +1704,9 @@ async function installRuntime(paths) {
1606
1704
  function agentConfigPath(agent, paths) {
1607
1705
  return targetFor(agent).configPath(paths);
1608
1706
  }
1609
- function mergeAgentConfig(input, agent, paths) {
1707
+ function mergeAgentConfig(input, agent, paths, capture) {
1610
1708
  const target = targetFor(agent);
1611
- return target.merge(input === undefined ? target.emptyValue() : input, paths);
1709
+ return target.merge(input === undefined ? target.emptyValue() : input, paths, capture);
1612
1710
  }
1613
1711
  function removeAgentConfig(input, agent) {
1614
1712
  return targetFor(agent).remove(input);
@@ -1617,7 +1715,7 @@ function removeAgentConfig(input, agent) {
1617
1715
  * Read each installation's hooks file and, for MCP hosts, its MCP config, and
1618
1716
  * merge the Lore entries this CLI version writes. Nothing touches disk yet.
1619
1717
  */
1620
- async function prepareAgentEntries(installations, paths) {
1718
+ async function prepareAgentEntries(installations, paths, capture) {
1621
1719
  const documents = new Map();
1622
1720
  const mergedDocuments = new Map();
1623
1721
  const mcpDocuments = new Map();
@@ -1625,12 +1723,12 @@ async function prepareAgentEntries(installations, paths) {
1625
1723
  const key = installationKey(installation);
1626
1724
  const document = await readJsonDocument(installation.configPath);
1627
1725
  documents.set(key, document);
1628
- mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths));
1726
+ mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths, capture));
1629
1727
  if (isMcpHostAgent(installation.agent)) {
1630
1728
  const mcpPath = mcpConfigPath(installation.agent, installation.configPath);
1631
1729
  if (installation.agent === "codex") {
1632
1730
  const text = await readTextDocument(mcpPath);
1633
- mcpDocuments.set(mcpPath, { kind: "text", document: text, merged: mergeLoreCodexMcp(text.value, loreMcpServer(paths)) });
1731
+ mcpDocuments.set(mcpPath, { kind: "text", document: text, merged: mergeLoreCodexMcp(text.value, loreMcpServer(paths, "codex")) });
1634
1732
  }
1635
1733
  else {
1636
1734
  const mcp = await readJsonDocument(mcpPath);
@@ -1691,9 +1789,11 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
1691
1789
  return null;
1692
1790
  const installations = uniqueInstallations(configuredInstallations(config, paths));
1693
1791
  return withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
1694
- const entries = await prepareAgentEntries(installations, paths);
1792
+ const entries = await prepareAgentEntries(installations, paths, captureEnabled(config));
1695
1793
  const written = await writeAgentEntries(entries, paths, new Date());
1696
- const skills = await installLoreSkill(paths, installations);
1794
+ const generic = (await readGenericAgents(paths.loreDirectory)).length > 0;
1795
+ const skills = await installLoreSkill(paths, generic ? [...installations, { agent: GENERIC_AGENT, configPath: paths.config }] : installations);
1796
+ const agent = await installLoreAgent(paths, installations);
1697
1797
  return {
1698
1798
  // Same order the files are written in, so the two update lines match.
1699
1799
  configFiles: [
@@ -1702,7 +1802,8 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
1702
1802
  ],
1703
1803
  ...written,
1704
1804
  skillFiles: skills.files,
1705
- warnings: skills.warnings,
1805
+ agentFiles: agent.files,
1806
+ warnings: [...skills.warnings, ...agent.warnings],
1706
1807
  };
1707
1808
  });
1708
1809
  }
@@ -1753,6 +1854,14 @@ function parseConnectArguments(args) {
1753
1854
  parsed.recovery = false;
1754
1855
  continue;
1755
1856
  }
1857
+ if (argument === "--no-capture") {
1858
+ parsed.capture = false;
1859
+ continue;
1860
+ }
1861
+ if (argument === "--capture") {
1862
+ parsed.capture = true;
1863
+ continue;
1864
+ }
1756
1865
  if (argument === "--current-project") {
1757
1866
  parsed.currentProject = true;
1758
1867
  continue;
@@ -1765,6 +1874,10 @@ function parseConnectArguments(args) {
1765
1874
  parsed.allProjects = true;
1766
1875
  continue;
1767
1876
  }
1877
+ if (argument === "--write") {
1878
+ parsed.writeTools = true;
1879
+ continue;
1880
+ }
1768
1881
  if (argument !== "--repository" &&
1769
1882
  argument !== "--folder" &&
1770
1883
  argument !== "--name" &&
@@ -1772,6 +1885,7 @@ function parseConnectArguments(args) {
1772
1885
  argument !== "--dashboard-url" &&
1773
1886
  argument !== "--token" &&
1774
1887
  argument !== "--agent" &&
1888
+ argument !== "--agent-id" &&
1775
1889
  argument !== "--fallback" &&
1776
1890
  argument !== "--timeout-ms") {
1777
1891
  throw new Error(`Unknown connect option: ${argument ?? ""}`);
@@ -1787,6 +1901,9 @@ function parseConnectArguments(args) {
1787
1901
  else if (argument === "--name") {
1788
1902
  parsed.connectionName = value;
1789
1903
  }
1904
+ else if (argument === "--agent-id") {
1905
+ parsed.agentId = parseGenericAgentId(value);
1906
+ }
1790
1907
  else if (argument === "--fallback") {
1791
1908
  if (!isLaunchable(value))
1792
1909
  throw new Error(`--fallback must be ${LAUNCHABLE_AGENTS.join(", ")}`);
@@ -1812,10 +1929,13 @@ function parseConnectArguments(args) {
1812
1929
  parsed.agents.push(value);
1813
1930
  }
1814
1931
  else {
1815
- throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, opencode, polytoken, or t3code");
1932
+ throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, generic, opencode, polytoken, or t3code");
1816
1933
  }
1817
1934
  }
1818
1935
  validateConnectProjectOptions(parsed);
1936
+ if ((parsed.agentId !== undefined || parsed.writeTools) && !parsed.agents.includes(GENERIC_AGENT)) {
1937
+ throw new Error("--agent-id and --write apply to --agent generic.");
1938
+ }
1819
1939
  if (parsed.login && parsed.token !== undefined) {
1820
1940
  throw new Error("--login and --token cannot be used together");
1821
1941
  }
@@ -1913,7 +2033,7 @@ async function connectWithCredentialLocked(options, identity) {
1913
2033
  ...new Set([
1914
2034
  ...requestedExpansion.installations.map((installation) => installation.agent),
1915
2035
  ...detectedAgents,
1916
- ...options.agents.filter((agent) => isCredentialOnlyAgent(agent) || agent === "t3code"),
2036
+ ...options.agents.filter((agent) => isCredentialOnlyAgent(agent) || agent === "t3code" || agent === GENERIC_AGENT),
1917
2037
  ]),
1918
2038
  ].sort();
1919
2039
  const keptAgents = [
@@ -1927,13 +2047,17 @@ async function connectWithCredentialLocked(options, identity) {
1927
2047
  // Cowork and the Devin CLI read the saved credential from ~/.lore/config.json
1928
2048
  // and need no hooks, so they connect without a native agent installation.
1929
2049
  const credentialOnlyAgents = CREDENTIAL_ONLY_AGENTS.filter((agent) => options.agents.includes(agent));
1930
- if (agents.length === 0 && credentialOnlyAgents.length === 0) {
2050
+ // An MCP-only agent has no hooks or settings Lore can edit; it gets the skills and a printed entry.
2051
+ const genericAgent = options.agents.includes(GENERIC_AGENT) ? options.genericAgent ?? { id: GENERIC_AGENT, writeTools: false } : undefined;
2052
+ if (agents.length === 0 && credentialOnlyAgents.length === 0 && genericAgent === undefined) {
1931
2053
  throw new Error("No Claude, Codex, Copilot CLI, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code, and Devin CLI users --agent devin.");
1932
2054
  }
1933
2055
  const now = new Date();
2056
+ // Reconnecting without a capture option keeps the saved mode, like folder rules.
2057
+ const capture = options.capture ?? captureEnabled(existing);
1934
2058
  // Read and merge before anything is persisted so an unreadable agent file
1935
2059
  // stops the run without a half-written connection.
1936
- const entries = await prepareAgentEntries(installations, paths);
2060
+ const entries = await prepareAgentEntries(installations, paths, capture);
1937
2061
  const agentConfigPaths = {};
1938
2062
  for (const agent of agents) {
1939
2063
  agentConfigPaths[agent] = installations
@@ -1949,6 +2073,7 @@ async function connectWithCredentialLocked(options, identity) {
1949
2073
  token: token.trim(),
1950
2074
  agents,
1951
2075
  agentConfigPaths,
2076
+ ...(capture ? {} : { capture: "off" }),
1952
2077
  connectedAt: connection.profiles.find(value => value.connectionId === connection.fields.connectionId)?.connectedAt ?? now.toISOString(),
1953
2078
  timeoutMs,
1954
2079
  };
@@ -1960,6 +2085,9 @@ async function connectWithCredentialLocked(options, identity) {
1960
2085
  // Folder restrictions must be durable before any new hook can run.
1961
2086
  await persistConnection(paths, config, connection.profiles);
1962
2087
  const { changedFiles: changedHookFiles, backups } = await writeAgentEntries(entries, paths, now);
2088
+ // The Devin wrapper and hook check the capture mode when they run, so stale
2089
+ // Devin hooks record nothing; an unreadable Devin config must not fail connect.
2090
+ changedHookFiles.push(...await refreshDevinHooks(paths.home, capture).catch(() => []));
1963
2091
  if (existing && !existing.connectionId && existing.apiUrl === apiUrl && existing.token === token) {
1964
2092
  await new ReliabilityStore(identity.workspaceId, { home: paths.home }).transferPendingTo(reliabilityStore);
1965
2093
  await reliabilityStore.migrateLegacyQueue(paths.queue);
@@ -1979,7 +2107,10 @@ async function connectWithCredentialLocked(options, identity) {
1979
2107
  .catch((error) => [`Host recovery setup: ${error.message}`]) : [];
1980
2108
  if (options.recovery === false)
1981
2109
  await disableRecovery(paths.home);
1982
- const skills = await installLoreSkill(paths, installations);
2110
+ const skills = await installLoreSkill(paths, genericAgent ? [...installations, { agent: GENERIC_AGENT, configPath: paths.config }] : installations);
2111
+ const loreAgent = await installLoreAgent(paths, installations);
2112
+ if (genericAgent)
2113
+ await recordGenericAgent(paths.loreDirectory, genericAgent, now);
1983
2114
  // An unchanged reconnect: same server, credential, and workspace, every
1984
2115
  // configured agent was already configured, and no agent file was rewritten.
1985
2116
  const alreadyConnected = existing !== null &&
@@ -1988,6 +2119,7 @@ async function connectWithCredentialLocked(options, identity) {
1988
2119
  existing.workspaceId === identity.workspaceId &&
1989
2120
  existing.connectionId === config.connectionId &&
1990
2121
  agents.every((agent) => existing.agents.includes(agent)) &&
2122
+ captureEnabled(existing) === capture &&
1991
2123
  changedHookFiles.length === 0 &&
1992
2124
  JSON.stringify(existing.directories ?? null) === JSON.stringify(config.directories ?? null);
1993
2125
  // A Codex set up in this run gets the approval step in its next-step line;
@@ -1998,12 +2130,18 @@ async function connectWithCredentialLocked(options, identity) {
1998
2130
  const codexNeedsApproval = !installedAgents.includes("codex") &&
1999
2131
  changedHookFiles.some((path) => codexHookPaths.includes(path));
2000
2132
  return {
2001
- capture: { folders: config.directories?.include ?? null, excludedFolders: config.directories?.exclude ?? [] },
2133
+ capture: { mode: capture ? "on" : "off", folders: config.directories?.include ?? null, excludedFolders: config.directories?.exclude ?? [] },
2002
2134
  skillFiles: skills.files,
2135
+ agentFiles: loreAgent.files,
2136
+ ...(genericAgent ? { genericAgent: {
2137
+ ...genericAgent,
2138
+ mcpServers: genericMcpServers(loreMcpInvocation(), paths.home, genericAgent).mcpServers,
2139
+ instructions: genericInstructions(genericAgent.id, skills.files.filter((file) => file.startsWith(resolve(paths.home, ".agents", "skills")))),
2140
+ } } : {}),
2003
2141
  connected: true,
2004
2142
  apiUrl: config.apiUrl,
2005
2143
  identity,
2006
- agents: [...agents, ...credentialOnlyAgents],
2144
+ agents: [...agents, ...credentialOnlyAgents, ...(genericAgent ? [GENERIC_AGENT] : [])],
2007
2145
  installedAgents,
2008
2146
  keptAgents,
2009
2147
  alreadyConnected,
@@ -2012,7 +2150,7 @@ async function connectWithCredentialLocked(options, identity) {
2012
2150
  config: paths.config,
2013
2151
  changedHookFiles,
2014
2152
  backups,
2015
- warnings: [...skills.warnings, ...(connection.needsFolders ? [`Lore is off for ${config.connectionName} until you choose where to record. Reconnect with --folder <path> or --all-projects, or run lore config open --connection ${config.connectionName}.`] : []), ...requestedExpansion.warnings, ...hostRecoveryWarnings, ...(codexNeedsApproval
2153
+ warnings: [...skills.warnings, ...loreAgent.warnings, ...(connection.needsFolders ? [`Lore is off for ${config.connectionName} until you choose where to record. Reconnect with --folder <path> or --all-projects, or run lore config open --connection ${config.connectionName}.`] : []), ...requestedExpansion.warnings, ...hostRecoveryWarnings, ...(codexNeedsApproval
2016
2154
  ? ["Codex requires hook approval. Open /hooks in Codex and review any new or changed Lore hooks, then start a fresh session. Run lore doctor to verify approval and the first invocation."]
2017
2155
  : [])],
2018
2156
  ...(recoveryNotice ? { recoveryNotice } : {}),
@@ -2164,12 +2302,17 @@ export function connectNextStep(agent) {
2164
2302
  case "cowork":
2165
2303
  case "t3code":
2166
2304
  return "Restart your agent and complete a turn.";
2305
+ case "generic":
2306
+ return "Add the MCP entry below to your agent's MCP settings and the instructions to its AGENTS.md (or GEMINI.md or rules file), then restart it.";
2167
2307
  }
2168
2308
  }
2169
2309
  export function formatConnectResult(result) {
2170
- const captureSummary = result.capture.folders === null ? "all projects allowed by the workspace"
2310
+ const folderSummary = result.capture.folders === null ? "all projects allowed by the workspace"
2171
2311
  : result.capture.folders.length ? result.capture.folders.join(", ") : "paused — choose folders with --folder <path>";
2172
2312
  const installed = result.installedAgents.join(", ");
2313
+ const generic = result.genericAgent;
2314
+ // With only an MCP agent connected, nothing on this machine records turns.
2315
+ const nothingRecords = result.capture.mode === "off" || (generic !== undefined && result.agents.every((agent) => agent === GENERIC_AGENT));
2173
2316
  const lines = [
2174
2317
  result.alreadyConnected
2175
2318
  ? `Already connected to ${result.identity.workspaceName} for ${installed}; nothing changed.`
@@ -2177,14 +2320,32 @@ export function formatConnectResult(result) {
2177
2320
  ...(result.keptAgents.length ? [`kept: ${result.keptAgents.join(", ")}`] : []),
2178
2321
  `api_url: ${result.apiUrl}`,
2179
2322
  `config: ${result.config}`,
2180
- `capture: ${captureSummary}${result.capture.excludedFolders.length ? `; excluded: ${result.capture.excludedFolders.join(", ")}` : ""}`,
2323
+ ...(result.capture.mode === "off" ? [`capture: ${captureSummary("off")}`] : []),
2324
+ ...(generic ? [`capture for ${generic.id}: off; an MCP-only agent has no hooks, so Lore saves only what you ask it to`] : []),
2325
+ `${nothingRecords ? "folders" : "capture"}: ${folderSummary}${result.capture.excludedFolders.length ? `; excluded: ${result.capture.excludedFolders.join(", ")}` : ""}`,
2181
2326
  ...result.installedAgents.map((agent) => `Next step for ${agent}: ${connectNextStep(agent)}`),
2182
- result.dashboardUrl === undefined
2183
- ? `Send one message in ${result.folder}, then run lore doctor to confirm recording.`
2184
- : `Send one message in ${result.folder}, then open ${result.dashboardUrl}/connect to confirm recording.`,
2185
- ...(result.skillFiles.length ? ["Choose lore in your agent’s skills to find knowledge or remember this session."] : []),
2327
+ generic !== undefined && nothingRecords
2328
+ ? `Ask your agent "Lore, has anything like this come up before?" in ${result.folder}, then run lore doctor.`
2329
+ : result.capture.mode === "off"
2330
+ ? `Send one message in ${result.folder}, then run lore doctor to confirm context delivery.`
2331
+ : result.dashboardUrl === undefined
2332
+ ? `Send one message in ${result.folder}, then run lore doctor to confirm recording.`
2333
+ : `Send one message in ${result.folder}, then open ${result.dashboardUrl}/connect to confirm recording.`,
2334
+ ...(result.skillFiles.length ? ["Choose lore in your agent’s skills to find knowledge or remember this session, or recap to write a session up by hand."] : []),
2335
+ ...(result.agentFiles.length ? ["Delegate to the lore agent (\"use the lore agent to review my pending learnings\") for recall, proposal review, or a recap with a fresh context."] : []),
2186
2336
  ...(result.recoveryNotice ? [result.recoveryNotice] : []),
2187
2337
  ...result.warnings.map((warning) => `warning: ${warning}`),
2338
+ ...(generic ? [
2339
+ generic.writeTools
2340
+ ? `Saves from ${generic.id} are proposals credited to ${generic.id}; review them in Learnings.`
2341
+ : `${generic.id} can read Lore; it saves through the lore CLI. Reconnect with --write to let it save through MCP (saves stay proposals).`,
2342
+ "",
2343
+ `MCP entry for ${generic.id} (merge into its mcpServers settings):`,
2344
+ JSON.stringify({ mcpServers: generic.mcpServers }, null, 2),
2345
+ "",
2346
+ "Instructions for AGENTS.md, GEMINI.md, or the agent's rules file:",
2347
+ generic.instructions,
2348
+ ] : []),
2188
2349
  ];
2189
2350
  return `${lines.join("\n")}\n`;
2190
2351
  }
@@ -2228,13 +2389,17 @@ async function connectCommand(args) {
2228
2389
  }
2229
2390
  const result = await connectWithCredential({
2230
2391
  ...capture,
2392
+ ...(parsed.capture === undefined ? {} : { capture: parsed.capture }),
2231
2393
  ...(parsed.connectionName === undefined ? {} : { connectionName: parsed.connectionName }),
2232
2394
  ...(parsed.fallback === undefined ? {} : { fallback: parsed.fallback }),
2233
- recovery: parsed.recovery ?? true,
2395
+ // Automatic recovery installs hooks for other agents on PATH; an MCP-only
2396
+ // connection must not start recording them as a side effect.
2397
+ recovery: parsed.recovery ?? !(parsed.agents.length > 0 && parsed.agents.every((agent) => agent === GENERIC_AGENT)),
2234
2398
  apiUrl,
2235
2399
  ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
2236
2400
  token,
2237
2401
  agents: parsed.agents,
2402
+ ...(parsed.agents.includes(GENERIC_AGENT) ? { genericAgent: { id: parsed.agentId ?? GENERIC_AGENT, writeTools: parsed.writeTools === true } } : {}),
2238
2403
  ...(parsed.timeoutMs === undefined ? {} : { timeoutMs: parsed.timeoutMs }),
2239
2404
  });
2240
2405
  if (authorization)
@@ -2356,6 +2521,7 @@ async function inspectRuntimeInstallation(required, paths) {
2356
2521
  "skill-guidance.js": resolve(dirname(paths.runtime), "skill-guidance.js"),
2357
2522
  "repository.js": paths.runtimeRepository,
2358
2523
  "repository-address.js": resolve(dirname(paths.runtime), "repository-address.js"),
2524
+ "capture-policy.js": resolve(dirname(paths.runtime), "capture-policy.js"),
2359
2525
  "directory-policy.js": resolve(dirname(paths.runtime), "directory-policy.js"),
2360
2526
  "reliability-store.js": paths.runtimeReliabilityStore,
2361
2527
  "windows-permissions.js": resolve(dirname(paths.runtime), "windows-permissions.js"),
@@ -2470,11 +2636,11 @@ async function getAgentStatus(agent, config, paths, runtime) {
2470
2636
  existing += document.exists ? 1 : 0;
2471
2637
  units.push(...inspectInstallationUnits(document.value === undefined
2472
2638
  ? target.emptyValue()
2473
- : document.value, agent, paths, index, configPaths.length));
2639
+ : document.value, agent, paths, index, configPaths.length, captureEnabled(config)));
2474
2640
  if (isMcpHostAgent(agent)) {
2475
2641
  const mcpPath = mcpConfigPath(agent, path);
2476
2642
  if (agent === "codex") {
2477
- const state = inspectCodexMcp((await readTextDocument(mcpPath)).value, loreMcpServer(paths));
2643
+ const state = inspectCodexMcp((await readTextDocument(mcpPath)).value, loreMcpServer(paths, "codex"));
2478
2644
  installed += state.owned;
2479
2645
  units.push({ id: installationUnitId("mcp-tools", index, configPaths.length),
2480
2646
  state: installationUnitState(state.owned, Number(state.exact)) });
@@ -2489,11 +2655,11 @@ async function getAgentStatus(agent, config, paths, runtime) {
2489
2655
  units.push({ id: installationUnitId("mcp", index, configPaths.length),
2490
2656
  state: installationUnitState(owned, Number(exactConfiguration(bridge, cursorMcpServer(paths)))) });
2491
2657
  }
2492
- const value = servers[LORE_MCP_SERVER_NAME];
2658
+ const value = agent === "copilot-vscode" ? loreVsCodeMcpServer(mcp.value) : servers[LORE_MCP_SERVER_NAME];
2493
2659
  const owned = Number(isLoreMcpServer(value));
2494
2660
  installed += owned;
2495
2661
  units.push({ id: installationUnitId("mcp-tools", index, configPaths.length),
2496
- state: installationUnitState(owned, Number(exactConfiguration(value, loreMcpServer(paths)))) });
2662
+ state: installationUnitState(owned, Number(exactConfiguration(value, loreMcpServer(paths, agent)))) });
2497
2663
  }
2498
2664
  }
2499
2665
  }
@@ -2515,7 +2681,7 @@ async function getAgentStatus(agent, config, paths, runtime) {
2515
2681
  state: "not_required",
2516
2682
  version: null,
2517
2683
  };
2518
- const expected = target.expected * configPaths.length;
2684
+ const expected = target.expected(captureEnabled(config)) * configPaths.length;
2519
2685
  const exactUnits = units.filter((unit) => unit.state === "exact").length;
2520
2686
  const installationState = invalidConfiguration
2521
2687
  ? "invalid"
@@ -2578,13 +2744,17 @@ async function statusData(paths, cwd) {
2578
2744
  }
2579
2745
  const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
2580
2746
  const runtime = await inspectRuntimeInstallation(runtimeRequired, paths);
2581
- const [legacyQueuedTurns, reliability, agents] = await Promise.all([
2747
+ const [legacyQueuedTurns, reliability, agents, switches] = await Promise.all([
2582
2748
  queueCount(paths),
2583
2749
  inspectReliabilityStore(config, paths),
2584
2750
  Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths, runtime))),
2751
+ // Only a machine that records turns needs the dashboard switches.
2752
+ captureEnabled(config) ? captureSwitches(config, paths.home, cwd) : CAPTURE_SWITCHES_UNCHECKED,
2585
2753
  ]);
2586
2754
  return {
2587
2755
  folderAccess: await checkDirectory(cwd, paths.home, config ?? undefined),
2756
+ capture: captureEnabled(config) ? "on" : "off",
2757
+ captureSwitches: switches,
2588
2758
  schemaVersion: "lore-cli-status-v1",
2589
2759
  integrationContractVersion: RELIABILITY_INTEGRATION_CONTRACT_VERSION,
2590
2760
  connected: config !== null,
@@ -2633,7 +2803,7 @@ async function statusCommand(args) {
2633
2803
  ? "-"
2634
2804
  : `${(value * 100).toFixed(1)}%`;
2635
2805
  const retrievalFailureLines = formatRetrievalFailureDiagnostics(data.reliability?.operational.retrieval);
2636
- writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\ncurrent_folder: ${data.folderAccess.allowed ? "allowed" : "off"} (${data.folderAccess.reason})\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${retrievalFailureLines}${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
2806
+ writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\ncurrent_folder: ${data.folderAccess.allowed ? "allowed" : "off"} (${data.folderAccess.reason})\ncapture: ${captureSummary(data.capture, data.captureSwitches)}\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${retrievalFailureLines}${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
2637
2807
  }
2638
2808
  async function disconnectCommand(args) {
2639
2809
  const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
@@ -2693,13 +2863,16 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
2693
2863
  backups.push(result.backup);
2694
2864
  }
2695
2865
  }
2696
- await removeLoreSkill(paths, installations);
2866
+ await removeLoreSkill(paths, [...installations, { agent: GENERIC_AGENT, configPath: paths.config }]);
2867
+ await removeLoreAgent(paths, installations);
2868
+ await removeGenericAgents(paths.loreDirectory);
2697
2869
  await Promise.all([
2698
2870
  rm(paths.config, { force: true }),
2699
2871
  rm(resolve(paths.loreDirectory, "connections"), { recursive: true, force: true }),
2700
2872
  rm(paths.runtime, { force: true }),
2701
2873
  rm(paths.runtimeRepository, { force: true }),
2702
2874
  rm(resolve(dirname(paths.runtime), "repository-address.js"), { force: true }),
2875
+ rm(resolve(dirname(paths.runtime), "capture-policy.js"), { force: true }),
2703
2876
  rm(resolve(dirname(paths.runtime), "directory-policy.js"), { force: true }),
2704
2877
  rm(paths.runtimeReliabilityStore, { force: true }),
2705
2878
  rm(paths.runtimeSignedSnapshot, { force: true }),
@@ -2725,6 +2898,47 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
2725
2898
  ]);
2726
2899
  return { connected: false, changedHookFiles, backups };
2727
2900
  }
2901
+ const CAPTURE_SWITCHES_UNCHECKED = { pausedBy: null, checked: false, reason: "not connected" };
2902
+ async function captureSwitches(config, home, cwd) {
2903
+ if (config === null)
2904
+ return CAPTURE_SWITCHES_UNCHECKED;
2905
+ const access = await checkLoreAccess(cwd, home, config);
2906
+ // A folder outside the local rules never reaches the workspace policy.
2907
+ if (access.failure !== undefined || !access.reason.startsWith("Repository"))
2908
+ return { pausedBy: null, checked: false, reason: access.reason };
2909
+ return { pausedBy: access.capturePausedBy ?? null, checked: true, reason: access.capturePausedBy ? capturePauseMessage(access.capturePausedBy) : "on" };
2910
+ }
2911
+ /** How the dashboard switches are resumed, for status and doctor wording. */
2912
+ function captureResumeHint(pausedBy) {
2913
+ return pausedBy === "workspace"
2914
+ ? "An owner or admin can resume it under Settings, Session capture."
2915
+ : "Resume it under Settings, Session capture.";
2916
+ }
2917
+ /**
2918
+ * The effective capture state for `lore capture status` and `lore status`:
2919
+ * the machine's own choice, then the dashboard switch that pauses it, worded
2920
+ * as `lore doctor` words it.
2921
+ */
2922
+ export function captureSummary(capture, switches = CAPTURE_SWITCHES_UNCHECKED) {
2923
+ if (capture === "off")
2924
+ return "off by choice; context and guard active";
2925
+ if (switches.pausedBy === null)
2926
+ return "on; turns are recorded in allowed folders";
2927
+ // "Capture paused by workspace setting" -> "on locally, paused by workspace setting"
2928
+ return `on locally, ${capturePauseMessage(switches.pausedBy).replace(/^Capture p/u, "p")}; context and guard active`;
2929
+ }
2930
+ /** Retrieval-only mode and a paused switch are chosen configurations, never warnings. */
2931
+ export function captureModeCheck(capture, switches = CAPTURE_SWITCHES_UNCHECKED) {
2932
+ return {
2933
+ name: "capture-mode",
2934
+ status: "ok",
2935
+ detail: capture === "off"
2936
+ ? "Capture off by choice; context and guard active. lore remember still saves what you choose. Run lore capture on to record turns again."
2937
+ : switches.pausedBy !== null
2938
+ ? `${capturePauseMessage(switches.pausedBy)}; context and guard active. Saved learnings and /recap still work. ${captureResumeHint(switches.pausedBy)}`
2939
+ : "on: turns are recorded in allowed folders",
2940
+ };
2941
+ }
2728
2942
  /**
2729
2943
  * Server-side queue health. Dead embedding jobs name the operator fix: once the
2730
2944
  * provider is repaired, `lore embeddings repair` retries them without waiting
@@ -2866,6 +3080,93 @@ async function verifyConnectedInstallation() {
2866
3080
  }
2867
3081
  return (await apiChecks(config)).every((check) => check.status !== "error");
2868
3082
  }
3083
+ function formatCaptureResult(result) {
3084
+ const pausedBy = result.pausedBy ?? null;
3085
+ const lines = [
3086
+ `capture: ${captureSummary(result.capture, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })}`,
3087
+ ...(result.capture === "on" && pausedBy !== null ? [captureResumeHint(pausedBy)] : []),
3088
+ ...(result.changed
3089
+ ? [
3090
+ `updated: ${result.configFiles.join(", ")}`,
3091
+ result.capture === "on"
3092
+ ? "Restart your agents, then send one message and run lore doctor to confirm recording."
3093
+ : "Restart your agents. lore remember and the lore skill still save what you choose.",
3094
+ ]
3095
+ : []),
3096
+ ];
3097
+ return `${lines.join("\n")}\n`;
3098
+ }
3099
+ /** Switch retrieval-only mode and rewrite the agent hooks to match, without re-authorizing. */
3100
+ export async function setCaptureMode(capture, paths = getLorePaths()) {
3101
+ const config = await readConnectorConfig(paths);
3102
+ if (config === null)
3103
+ throw new Error("Lore is not connected. Run lore connect.");
3104
+ const current = captureEnabled(config) ? "on" : "off";
3105
+ if (current === capture)
3106
+ return { capture, changed: false, configFiles: [] };
3107
+ await withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
3108
+ const { capture: _previous, ...rest } = config;
3109
+ await atomicWriteJson(paths.config, { ...rest, ...(capture === "off" ? { capture } : {}) }, 65536);
3110
+ });
3111
+ const refreshed = await refreshAgentEntries(paths);
3112
+ const devinFiles = await refreshDevinHooks(paths.home, capture === "on");
3113
+ return { capture, changed: true, configFiles: [...refreshed?.configFiles ?? [], ...devinFiles] };
3114
+ }
3115
+ async function captureCommand(args) {
3116
+ let mode;
3117
+ let json = false;
3118
+ for (const argument of args) {
3119
+ if (argument === "--help" || argument === "-h") {
3120
+ process.stdout.write(CAPTURE_HELP);
3121
+ return;
3122
+ }
3123
+ if (argument === "--json") {
3124
+ json = true;
3125
+ continue;
3126
+ }
3127
+ if ((argument === "status" || argument === "on" || argument === "off") && mode === undefined) {
3128
+ mode = argument;
3129
+ continue;
3130
+ }
3131
+ throw new Error("Usage: lore capture [status|on|off] [--json]");
3132
+ }
3133
+ const paths = getLorePaths();
3134
+ if (mode === undefined || mode === "status") {
3135
+ const config = await readConnectorConfig(paths);
3136
+ if (config === null)
3137
+ throw new Error("Lore is not connected. Run lore connect.");
3138
+ const capture = captureEnabled(config) ? "on" : "off";
3139
+ // Same reading as lore status and lore doctor: the folder's connection and its dashboard switches.
3140
+ const switches = capture === "on" ? await captureSwitches(await diagnosticConfig(paths, process.cwd()), paths.home, process.cwd()) : CAPTURE_SWITCHES_UNCHECKED;
3141
+ const result = { capture, pausedBy: switches.pausedBy, changed: false, configFiles: [] };
3142
+ writeResult(result, json, formatCaptureResult(result));
3143
+ return;
3144
+ }
3145
+ const result = await setCaptureMode(mode, paths);
3146
+ writeResult(result, json, formatCaptureResult(result));
3147
+ }
3148
+ /** MCP-only agents from `lore connect --agent generic`: their skills, and that nothing records them. */
3149
+ export async function genericAgentChecks(paths) {
3150
+ const agents = await readGenericAgents(paths.loreDirectory);
3151
+ if (agents.length === 0)
3152
+ return [];
3153
+ const skills = (await Promise.all(["lore", "recap"].map(async (name) => {
3154
+ const file = resolve(paths.home, ".agents", "skills", name, "SKILL.md");
3155
+ return await access(file).then(() => null, () => file);
3156
+ }))).filter((file) => file !== null);
3157
+ return agents.flatMap((agent) => [
3158
+ {
3159
+ name: `${agent.id}-mcp`,
3160
+ status: "ok",
3161
+ detail: `MCP only, ${agent.writeTools ? "read-write (saves are proposals credited to " + agent.id + ")" : "read-only (saves go through the lore CLI)"}. Capture off: no hooks, so this agent's turns are never recorded. Lore cannot inspect the agent's own MCP settings; ask it "Lore, has anything like this come up before?" to confirm.`,
3162
+ },
3163
+ {
3164
+ name: `${agent.id}-skills`,
3165
+ status: skills.length === 0 ? "ok" : "warning",
3166
+ detail: skills.length === 0 ? resolve(paths.home, ".agents", "skills") : `missing ${skills.join(", ")}; run lore connect --agent generic --agent-id ${agent.id} again`,
3167
+ },
3168
+ ]);
3169
+ }
2869
3170
  async function doctorCommand(args) {
2870
3171
  const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
2871
3172
  if (parsed === null) {
@@ -2875,7 +3176,15 @@ async function doctorCommand(args) {
2875
3176
  const config = await diagnosticConfig(paths, process.cwd());
2876
3177
  const prunedDeadLetters = await pruneUnrecoverableDeadLetters(config, paths);
2877
3178
  const status = await statusData(paths, process.cwd());
2878
- const checks = [{ name: "capture-folder", status: status.folderAccess.allowed ? "ok" : "warning", detail: status.folderAccess.allowed ? status.folderAccess.reason : `Recording is off here: ${status.folderAccess.reason}. Run lore scope check <folder> to verify a selected folder.` }];
3179
+ const checks = [
3180
+ { name: "capture-folder", status: status.folderAccess.allowed ? "ok" : "warning", detail: status.folderAccess.allowed ? status.folderAccess.reason : `${status.capture === "off" ? "Lore" : "Recording"} is off here: ${status.folderAccess.reason}. Run lore scope check <folder> to verify a selected folder.` },
3181
+ captureModeCheck(status.capture, status.captureSwitches),
3182
+ ];
3183
+ const genericChecks = await genericAgentChecks(paths);
3184
+ // With only MCP agents connected there are no hooks, so nothing records turns.
3185
+ if (genericChecks.length > 0 && !status.agents.some((agent) => agent.configured)) {
3186
+ checks[1] = { name: "capture-mode", status: "ok", detail: "off: only MCP agents are connected, and they have no hooks; Lore saves only what you ask them to" };
3187
+ }
2879
3188
  checks.push({
2880
3189
  name: "platform",
2881
3190
  status: platform() === "darwin" || platform() === "linux" || isWindows() ? "ok" : "error",
@@ -2957,6 +3266,7 @@ async function doctorCommand(args) {
2957
3266
  });
2958
3267
  }
2959
3268
  }
3269
+ checks.push(...genericChecks);
2960
3270
  for (const surface of status.externalSurfaces) {
2961
3271
  checks.push({
2962
3272
  name: `${surface.surface}-capability`,
@@ -3044,6 +3354,9 @@ async function printCommandHelp(path) {
3044
3354
  case "doctor":
3045
3355
  process.stdout.write(DOCTOR_HELP);
3046
3356
  return;
3357
+ case "capture":
3358
+ process.stdout.write(CAPTURE_HELP);
3359
+ return;
3047
3360
  case "embeddings":
3048
3361
  await runEmbeddingsCommand(args, null);
3049
3362
  return;
@@ -3059,6 +3372,9 @@ async function printCommandHelp(path) {
3059
3372
  case "ask":
3060
3373
  await runAskCommand(args, null);
3061
3374
  return;
3375
+ case "chat":
3376
+ await runChatCommand(args, null);
3377
+ return;
3062
3378
  case "learnings":
3063
3379
  await runLearningsCommand(args, null);
3064
3380
  return;
@@ -3149,7 +3465,7 @@ async function executeCli(args = process.argv.slice(2)) {
3149
3465
  const helpIndex = args.findIndex((value, index) => index > 0 && (separator < 0 || index < separator) && (value === "--help" || value === "-h"));
3150
3466
  if (helpIndex > 0) {
3151
3467
  const subcommand = args[1];
3152
- const grouped = ["connect", "config", "embeddings", "guard", "recovery", "github", "devin", "host", "self-host", "scope", "review"].includes(command ?? "");
3468
+ const grouped = ["connect", "config", "capture", "embeddings", "guard", "recovery", "github", "devin", "host", "self-host", "scope", "review"].includes(command ?? "");
3153
3469
  await printCommandHelp([command, ...(grouped && subcommand && !subcommand.startsWith("-") ? [subcommand] : [])]);
3154
3470
  return;
3155
3471
  }
@@ -3162,7 +3478,7 @@ async function executeCli(args = process.argv.slice(2)) {
3162
3478
  return;
3163
3479
  }
3164
3480
  const commandArgs = args.slice(1);
3165
- if (["ask", "learnings", "review", "remember", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
3481
+ if (["ask", "chat", "learnings", "review", "remember", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
3166
3482
  const decision = await checkDirectory(process.cwd(), getLorePaths(["approach", "skill"].includes(command) ? process.env.LORE_HOME : undefined).home);
3167
3483
  if (!decision.allowed)
3168
3484
  throw new Error(`Lore is off in this folder. ${decision.reason}`);
@@ -3176,6 +3492,9 @@ async function executeCli(args = process.argv.slice(2)) {
3176
3492
  case "config":
3177
3493
  await runConfigCommand(commandArgs, getLorePaths().config);
3178
3494
  return;
3495
+ case "capture":
3496
+ await captureCommand(commandArgs);
3497
+ return;
3179
3498
  case "scope": {
3180
3499
  if (commandArgs[0] !== "check" || commandArgs.length > 2)
3181
3500
  throw new Error("Usage: lore scope check [folder]");
@@ -3202,6 +3521,9 @@ async function executeCli(args = process.argv.slice(2)) {
3202
3521
  case "ask":
3203
3522
  await runAskCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
3204
3523
  return;
3524
+ case "chat":
3525
+ await runChatCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
3526
+ return;
3205
3527
  case "learnings":
3206
3528
  await runLearningsCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
3207
3529
  return;