@lore-co/cli 0.2.26 → 0.2.27

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 (213) hide show
  1. package/README.md +62 -16
  2. package/dist/agents.d.ts +9 -0
  3. package/dist/agents.d.ts.map +1 -1
  4. package/dist/agents.js +19 -6
  5. package/dist/agents.js.map +1 -1
  6. package/dist/api.d.ts +2 -1
  7. package/dist/api.d.ts.map +1 -1
  8. package/dist/api.js +15 -2
  9. package/dist/api.js.map +1 -1
  10. package/dist/approach-guidance.d.ts +8 -1
  11. package/dist/approach-guidance.d.ts.map +1 -1
  12. package/dist/approach-guidance.js +13 -3
  13. package/dist/approach-guidance.js.map +1 -1
  14. package/dist/approach.d.ts.map +1 -1
  15. package/dist/approach.js +2 -2
  16. package/dist/approach.js.map +1 -1
  17. package/dist/ask.d.ts.map +1 -1
  18. package/dist/ask.js +2 -2
  19. package/dist/ask.js.map +1 -1
  20. package/dist/capture-scope.d.ts.map +1 -1
  21. package/dist/capture-scope.js +2 -1
  22. package/dist/capture-scope.js.map +1 -1
  23. package/dist/chat.d.ts.map +1 -1
  24. package/dist/chat.js +2 -2
  25. package/dist/chat.js.map +1 -1
  26. package/dist/cli.d.ts +60 -2
  27. package/dist/cli.d.ts.map +1 -1
  28. package/dist/cli.js +512 -93
  29. package/dist/cli.js.map +1 -1
  30. package/dist/codex-hook-approval.d.ts.map +1 -1
  31. package/dist/codex-hook-approval.js +9 -2
  32. package/dist/codex-hook-approval.js.map +1 -1
  33. package/dist/command-help.d.ts.map +1 -1
  34. package/dist/command-help.js +7 -3
  35. package/dist/command-help.js.map +1 -1
  36. package/dist/config-command.js +2 -2
  37. package/dist/config-command.js.map +1 -1
  38. package/dist/connect-project.d.ts.map +1 -1
  39. package/dist/connect-project.js +2 -1
  40. package/dist/connect-project.js.map +1 -1
  41. package/dist/console-keys.d.ts +11 -0
  42. package/dist/console-keys.d.ts.map +1 -0
  43. package/dist/console-keys.js +24 -0
  44. package/dist/console-keys.js.map +1 -0
  45. package/dist/cursor-mcp.d.ts.map +1 -1
  46. package/dist/cursor-mcp.js +4 -0
  47. package/dist/cursor-mcp.js.map +1 -1
  48. package/dist/demo-process.js +3 -3
  49. package/dist/demo-process.js.map +1 -1
  50. package/dist/devin-capture.d.ts.map +1 -1
  51. package/dist/devin-capture.js +2 -2
  52. package/dist/devin-capture.js.map +1 -1
  53. package/dist/devin-cli.d.ts.map +1 -1
  54. package/dist/devin-cli.js +23 -13
  55. package/dist/devin-cli.js.map +1 -1
  56. package/dist/devin-client.d.ts +3 -0
  57. package/dist/devin-client.d.ts.map +1 -1
  58. package/dist/devin-client.js +5 -0
  59. package/dist/devin-client.js.map +1 -1
  60. package/dist/devin.d.ts.map +1 -1
  61. package/dist/devin.js +29 -9
  62. package/dist/devin.js.map +1 -1
  63. package/dist/generated-assets.d.ts +8 -8
  64. package/dist/generated-assets.d.ts.map +1 -1
  65. package/dist/generated-assets.js +8 -8
  66. package/dist/generated-assets.js.map +1 -1
  67. package/dist/github.d.ts.map +1 -1
  68. package/dist/github.js +38 -7
  69. package/dist/github.js.map +1 -1
  70. package/dist/guard.d.ts +1 -1
  71. package/dist/guard.d.ts.map +1 -1
  72. package/dist/guard.js +42 -14
  73. package/dist/guard.js.map +1 -1
  74. package/dist/handoff.js +2 -2
  75. package/dist/handoff.js.map +1 -1
  76. package/dist/host.js +4 -4
  77. package/dist/host.js.map +1 -1
  78. package/dist/image-capture.d.ts.map +1 -1
  79. package/dist/image-capture.js +5 -4
  80. package/dist/image-capture.js.map +1 -1
  81. package/dist/learnings.d.ts.map +1 -1
  82. package/dist/learnings.js +3 -3
  83. package/dist/learnings.js.map +1 -1
  84. package/dist/live.d.ts +7 -0
  85. package/dist/live.d.ts.map +1 -1
  86. package/dist/live.js +11 -6
  87. package/dist/live.js.map +1 -1
  88. package/dist/lore-agent.d.ts +6 -1
  89. package/dist/lore-agent.d.ts.map +1 -1
  90. package/dist/lore-agent.js +26 -7
  91. package/dist/lore-agent.js.map +1 -1
  92. package/dist/mcp-config.d.ts +7 -0
  93. package/dist/mcp-config.d.ts.map +1 -1
  94. package/dist/mcp-config.js +19 -3
  95. package/dist/mcp-config.js.map +1 -1
  96. package/dist/npm-installation.js +2 -2
  97. package/dist/npm-installation.js.map +1 -1
  98. package/dist/owned-files.d.ts +14 -4
  99. package/dist/owned-files.d.ts.map +1 -1
  100. package/dist/owned-files.js +24 -9
  101. package/dist/owned-files.js.map +1 -1
  102. package/dist/platform.d.ts +26 -3
  103. package/dist/platform.d.ts.map +1 -1
  104. package/dist/platform.js +87 -20
  105. package/dist/platform.js.map +1 -1
  106. package/dist/pty.d.ts +2 -1
  107. package/dist/pty.d.ts.map +1 -1
  108. package/dist/pty.js +20 -7
  109. package/dist/pty.js.map +1 -1
  110. package/dist/recovery-cloud-receipt.d.ts.map +1 -1
  111. package/dist/recovery-cloud-receipt.js +2 -1
  112. package/dist/recovery-cloud-receipt.js.map +1 -1
  113. package/dist/recovery-cloud-workspace.d.ts.map +1 -1
  114. package/dist/recovery-cloud-workspace.js +2 -1
  115. package/dist/recovery-cloud-workspace.js.map +1 -1
  116. package/dist/recovery-cloud.d.ts.map +1 -1
  117. package/dist/recovery-cloud.js +8 -9
  118. package/dist/recovery-cloud.js.map +1 -1
  119. package/dist/recovery-copilot.d.ts.map +1 -1
  120. package/dist/recovery-copilot.js +3 -2
  121. package/dist/recovery-copilot.js.map +1 -1
  122. package/dist/recovery-desktop.d.ts.map +1 -1
  123. package/dist/recovery-desktop.js +3 -2
  124. package/dist/recovery-desktop.js.map +1 -1
  125. package/dist/recovery-hooks.js +2 -2
  126. package/dist/recovery-hooks.js.map +1 -1
  127. package/dist/recovery-hosts.d.ts.map +1 -1
  128. package/dist/recovery-hosts.js +53 -6
  129. package/dist/recovery-hosts.js.map +1 -1
  130. package/dist/recovery-polytoken.d.ts +2 -2
  131. package/dist/recovery-polytoken.d.ts.map +1 -1
  132. package/dist/recovery-polytoken.js +13 -8
  133. package/dist/recovery-polytoken.js.map +1 -1
  134. package/dist/recovery-remote.js +3 -3
  135. package/dist/recovery-remote.js.map +1 -1
  136. package/dist/recovery-state.d.ts +22 -1
  137. package/dist/recovery-state.d.ts.map +1 -1
  138. package/dist/recovery-state.js +167 -31
  139. package/dist/recovery-state.js.map +1 -1
  140. package/dist/recovery-transfer.d.ts.map +1 -1
  141. package/dist/recovery-transfer.js +8 -4
  142. package/dist/recovery-transfer.js.map +1 -1
  143. package/dist/recovery.d.ts.map +1 -1
  144. package/dist/recovery.js +30 -11
  145. package/dist/recovery.js.map +1 -1
  146. package/dist/reliability-store.d.ts +13 -0
  147. package/dist/reliability-store.d.ts.map +1 -1
  148. package/dist/reliability-store.js +110 -8
  149. package/dist/reliability-store.js.map +1 -1
  150. package/dist/remember.d.ts.map +1 -1
  151. package/dist/remember.js +2 -2
  152. package/dist/remember.js.map +1 -1
  153. package/dist/retained-context.d.ts.map +1 -1
  154. package/dist/retained-context.js +3 -2
  155. package/dist/retained-context.js.map +1 -1
  156. package/dist/retrieval-task.d.ts +30 -0
  157. package/dist/retrieval-task.d.ts.map +1 -1
  158. package/dist/retrieval-task.js +98 -2
  159. package/dist/retrieval-task.js.map +1 -1
  160. package/dist/review.d.ts +1 -1
  161. package/dist/review.d.ts.map +1 -1
  162. package/dist/review.js +5 -2
  163. package/dist/review.js.map +1 -1
  164. package/dist/runtime.d.ts +4 -1
  165. package/dist/runtime.d.ts.map +1 -1
  166. package/dist/runtime.js +138 -91
  167. package/dist/runtime.js.map +1 -1
  168. package/dist/self-host.d.ts +1 -1
  169. package/dist/self-host.d.ts.map +1 -1
  170. package/dist/self-host.js +105 -23
  171. package/dist/self-host.js.map +1 -1
  172. package/dist/share.d.ts.map +1 -1
  173. package/dist/share.js +2 -1
  174. package/dist/share.js.map +1 -1
  175. package/dist/signed-snapshot.d.ts +7 -0
  176. package/dist/signed-snapshot.d.ts.map +1 -1
  177. package/dist/signed-snapshot.js +27 -0
  178. package/dist/signed-snapshot.js.map +1 -1
  179. package/dist/skill-command.d.ts +5 -2
  180. package/dist/skill-command.d.ts.map +1 -1
  181. package/dist/skill-command.js +73 -10
  182. package/dist/skill-command.js.map +1 -1
  183. package/dist/skill-guidance.js +1 -1
  184. package/dist/skill-guidance.js.map +1 -1
  185. package/dist/skills.d.ts.map +1 -1
  186. package/dist/skills.js +9 -7
  187. package/dist/skills.js.map +1 -1
  188. package/dist/status-health.d.ts +26 -0
  189. package/dist/status-health.d.ts.map +1 -0
  190. package/dist/status-health.js +49 -0
  191. package/dist/status-health.js.map +1 -0
  192. package/dist/telemetry.d.ts.map +1 -1
  193. package/dist/telemetry.js +2 -2
  194. package/dist/telemetry.js.map +1 -1
  195. package/dist/terminal-output.d.ts +76 -0
  196. package/dist/terminal-output.d.ts.map +1 -0
  197. package/dist/terminal-output.js +151 -0
  198. package/dist/terminal-output.js.map +1 -0
  199. package/dist/uninstall.d.ts.map +1 -1
  200. package/dist/uninstall.js +3 -1
  201. package/dist/uninstall.js.map +1 -1
  202. package/dist/update.d.ts +3 -0
  203. package/dist/update.d.ts.map +1 -1
  204. package/dist/update.js +40 -14
  205. package/dist/update.js.map +1 -1
  206. package/dist/windows-permissions.d.ts +6 -0
  207. package/dist/windows-permissions.d.ts.map +1 -1
  208. package/dist/windows-permissions.js +22 -2
  209. package/dist/windows-permissions.js.map +1 -1
  210. package/dist/windows-pty.d.ts.map +1 -1
  211. package/dist/windows-pty.js +4 -3
  212. package/dist/windows-pty.js.map +1 -1
  213. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { parseDirectoryRules, checkDirectory, checkLoreAccess, capturePauseMessage } from "./directory-policy.js";
2
+ import { parseDirectoryRules, checkDirectory, checkDirectoryRules, checkLoreAccess, checkWorkspaceRepository, capturePauseMessage, LORE_DISABLED_MESSAGE, loreDisabled } 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";
@@ -10,14 +10,15 @@ import { prepareConnection, persistConnection, listConnections } from "./connect
10
10
  import { connectionReferences, resolveLocalConnection } from "./directory-policy.js";
11
11
  import { atomicWriteJson, withDirectoryLock } from "./reliability-store.js";
12
12
  import { runConfigCommand } from "./config-command.js";
13
+ import { contextHealthLine, serverBehindLine, staleFilesLine } from "./status-health.js";
13
14
  import { codexApprovalDetail, inspectCodexHookApproval } from "./codex-hook-approval.js";
14
15
  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
- import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
16
+ import { CURSOR_LORE_DISABLE_ENV, 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";
17
+ import { access, chmod, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
17
18
  import { constants as fsConstants, realpathSync } from "node:fs";
18
19
  import { spawn, isWindows, windowsCommand, commandText, findExecutable, openBrowser } from "./platform.js";
19
20
  import { createHash, randomUUID } from "node:crypto";
20
- import { basename, dirname, resolve } from "node:path";
21
+ import { basename, dirname, resolve, sep } from "node:path";
21
22
  import { homedir, hostname, platform } from "node:os";
22
23
  import { setTimeout as delay } from "node:timers/promises";
23
24
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -30,9 +31,10 @@ import { runLearningsCommand } from "./learnings.js";
30
31
  import { runReviewCommand } from "./review.js";
31
32
  import { runEmbeddingsCommand } from "./embeddings.js";
32
33
  import { runRememberCommand } from "./remember.js";
33
- import { installLoreSkill, removeLoreSkill } from "./skills.js";
34
+ import { installLoreSkill, loreSkillFiles, recapSkillFiles, removeLoreSkill } from "./skills.js";
34
35
  import { GENERIC_AGENT, genericInstructions, genericMcpServers, parseGenericAgentId, readGenericAgents, recordGenericAgent, removeGenericAgents } from "./generic-mcp.js";
35
- import { installLoreAgent, removeLoreAgent } from "./lore-agent.js";
36
+ import { installLoreAgent, loreAgentFiles, removeLoreAgent } from "./lore-agent.js";
37
+ import { regularPath } from "./owned-files.js";
36
38
  import { runApproachCommand } from "./approach.js";
37
39
  import { runSkillCommand } from "./skill-command.js";
38
40
  import { runGuardCommand } from "./guard.js";
@@ -46,10 +48,12 @@ import { runDevinCommand } from "./devin.js";
46
48
  import { runHostCommand } from "./host.js";
47
49
  import { runDemoCommand } from "./demo.js";
48
50
  import { updateCommand } from "./update.js";
51
+ import { Output, PLAIN_OUTPUT, detectOutputStyle, listText } from "./terminal-output.js";
49
52
  import { UNINSTALL_HELP, uninstallCommand } from "./uninstall.js";
50
53
  import { runSelfHostCommand } from "./self-host.js";
51
54
  import { ReliabilityStore, ReliabilityStoreError, integrationInvocationStateKey, } from "./reliability-store.js";
52
55
  import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
56
+ import { openNoFollow } from "@lore-co/core/no-follow";
53
57
  const LORE_OWNER_ARGUMENT = "--owner lore";
54
58
  export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
55
59
  export const DEFAULT_HOSTED_API_URL = "https://api.uselore.co";
@@ -142,6 +146,7 @@ Options:
142
146
  --timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
143
147
  --fallback <agent> claude, codex, opencode, devin, cursor, copilot-cli, polytoken
144
148
  --no-recovery Disable automatic handoff setup (otherwise enabled with two installed clients)
149
+ --verbose Also list the files written and the connection details
145
150
  --json Print machine-readable output
146
151
  --help Show this command's help
147
152
 
@@ -187,15 +192,20 @@ Usage:
187
192
  lore capture status [--json]
188
193
  lore capture off [--json]
189
194
  lore capture on [--json]
195
+ lore capture reminder off|on [--json]
190
196
 
191
197
  With capture off, Lore installs no Stop or SessionEnd hooks and never sends a
192
198
  prompt or response as a turn. Context injection, Guard, lore ask, lore remember,
193
199
  and the lore skill's "remember this session" keep working. Folder rules still
194
200
  decide where Lore runs at all.
195
201
 
202
+ While nothing is recorded, Lore asks your agent to suggest /recap once in a
203
+ session where you confirm a decision. lore capture reminder off stops that.
204
+
196
205
  Examples:
197
206
  lore capture off
198
207
  lore capture status
208
+ lore capture reminder off
199
209
  lore capture on
200
210
  `;
201
211
  const DISCONNECT_HELP = `lore disconnect
@@ -317,17 +327,25 @@ function stripLoreFromEvent(value) {
317
327
  }
318
328
  return groups;
319
329
  }
330
+ /**
331
+ * Seconds a host waits for a Lore hook before stopping it. On Windows each
332
+ * hook starts PowerShell and lore.exe, and a reply hook took 3.6 s on
333
+ * windows-latest, so the 2 and 3 second limits would drop the capture there.
334
+ */
335
+ function hookTimeout(seconds) {
336
+ return isWindows() ? Math.max(seconds, 10) : seconds;
337
+ }
320
338
  function eventHandler(agent, event, paths) {
321
339
  return {
322
340
  type: "command",
323
341
  command: hookCommand(agent, paths),
324
- timeout: event === "SessionEnd"
342
+ timeout: hookTimeout(event === "SessionEnd"
325
343
  ? 2
326
344
  : event === "Stop"
327
345
  ? 3
328
346
  : event === "PreToolUse"
329
347
  ? 10
330
- : 25,
348
+ : 25),
331
349
  ...(event === "UserPromptSubmit"
332
350
  ? { statusMessage: "Loading Lore context" }
333
351
  : {}),
@@ -410,13 +428,13 @@ function cursorEventHandler(event, paths) {
410
428
  return {
411
429
  type: "command",
412
430
  command: hookCommand("cursor", paths),
413
- timeout: event === "sessionEnd"
431
+ timeout: hookTimeout(event === "sessionEnd"
414
432
  ? 2
415
433
  : event === "afterAgentResponse"
416
434
  ? 3
417
435
  : event === "beforeShellExecution"
418
436
  ? 10
419
- : 25,
437
+ : 25),
420
438
  };
421
439
  }
422
440
  const CURSOR_MCP_NAME = "lore-native";
@@ -424,7 +442,7 @@ function cursorMcpServer(paths) {
424
442
  return {
425
443
  type: "stdio", command: process.execPath,
426
444
  args: [...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)]), "cursor-mcp", "--owner", "lore"],
427
- env: { LORE_HOME: paths.home },
445
+ env: { LORE_HOME: paths.home, ...CURSOR_LORE_DISABLE_ENV },
428
446
  };
429
447
  }
430
448
  function isLoreCursorMcp(value) {
@@ -480,6 +498,36 @@ export function mcpConfigPath(agent, hooksPath) {
480
498
  return resolve(directory, "..", "..", ".vscode", "mcp.json");
481
499
  }
482
500
  }
501
+ /**
502
+ * The hooks and MCP files of an agent that keeps them inside a project, where
503
+ * a repository can commit them; null for agents configured in their own folders.
504
+ */
505
+ function projectConfigFiles(installation) {
506
+ if (installation.agent !== "copilot-vscode")
507
+ return null;
508
+ return {
509
+ project: resolve(dirname(installation.configPath), "..", ".."),
510
+ files: [installation.configPath, mcpConfigPath(installation.agent, installation.configPath)],
511
+ };
512
+ }
513
+ /** The first project config file that is, or sits in a folder that is, a symlink. */
514
+ async function linkedProjectConfigFile(installation, paths) {
515
+ const project = projectConfigFiles(installation);
516
+ if (project === null)
517
+ return null;
518
+ for (const file of project.files) {
519
+ // A link could point at any file of the user's, such as Lore's own token.
520
+ if (!await regularPath(file, paths.home, project.project))
521
+ return file;
522
+ }
523
+ return null;
524
+ }
525
+ async function assertProjectConfigNotLinked(installation, paths) {
526
+ const file = await linkedProjectConfigFile(installation, paths);
527
+ if (file !== null) {
528
+ throw new Error(`Refusing to edit ${file}: it or a folder above it in the project is a symlink. Replace the link with a regular file, then run this again.`);
529
+ }
530
+ }
483
531
  export function mergeLoreMcp(input, agent, paths) {
484
532
  const config = objectConfiguration(input, agent);
485
533
  // VS Code keeps its servers under `servers`; the other JSON hosts use `mcpServers`.
@@ -563,13 +611,13 @@ function copilotCliEventHandler(event, paths) {
563
611
  return {
564
612
  type: "command",
565
613
  [isWindows() ? "powershell" : "bash"]: hookCommand("copilot-cli", paths, event),
566
- timeoutSec: event === "sessionEnd"
614
+ timeoutSec: hookTimeout(event === "sessionEnd"
567
615
  ? 2
568
616
  : event === "agentStop"
569
617
  ? 3
570
618
  : event === "preToolUse"
571
619
  ? 10
572
- : 25,
620
+ : 25),
573
621
  };
574
622
  }
575
623
  function isLoreCopilotHook(value) {
@@ -659,13 +707,13 @@ function copilotVscodeEventHandler(event, paths) {
659
707
  return {
660
708
  type: "command",
661
709
  command: hookCommand("copilot-vscode", paths, event),
662
- timeout: event === "SessionEnd"
710
+ timeout: hookTimeout(event === "SessionEnd"
663
711
  ? 2
664
712
  : event === "Stop"
665
713
  ? 3
666
714
  : event === "PreToolUse"
667
715
  ? 10
668
- : 25,
716
+ : 25),
669
717
  };
670
718
  }
671
719
  export function mergeLoreCopilotVscodeHooks(input, paths, capture = true) {
@@ -993,6 +1041,7 @@ async function readJsonDocument(path) {
993
1041
  return {
994
1042
  exists: true,
995
1043
  value: parsed,
1044
+ raw,
996
1045
  mode: metadata.mode & 0o777,
997
1046
  };
998
1047
  }
@@ -1032,7 +1081,7 @@ async function writeMergedJson(path, document, value, now) {
1032
1081
  let backup;
1033
1082
  if (document.exists) {
1034
1083
  backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
1035
- await copyFile(path, backup, fsConstants.COPYFILE_EXCL);
1084
+ await writeFile(backup, document.raw ?? "", { flag: "wx", mode: document.mode });
1036
1085
  }
1037
1086
  await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`, document.mode);
1038
1087
  return {
@@ -1065,7 +1114,7 @@ async function writeMergedText(path, document, value, now) {
1065
1114
  let backup;
1066
1115
  if (document.exists) {
1067
1116
  backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
1068
- await copyFile(path, backup, fsConstants.COPYFILE_EXCL);
1117
+ await writeFile(backup, document.value, { flag: "wx", mode: document.mode });
1069
1118
  }
1070
1119
  await atomicWrite(path, value, document.mode);
1071
1120
  return {
@@ -1133,6 +1182,7 @@ function parseConnectorConfig(value) {
1133
1182
  ? {}
1134
1183
  : { agentConfigPaths }),
1135
1184
  ...(value.capture === "off" ? { capture: "off" } : {}),
1185
+ ...(value.recapReminder === "off" ? { recapReminder: "off" } : {}),
1136
1186
  connectedAt: value.connectedAt,
1137
1187
  timeoutMs,
1138
1188
  };
@@ -1600,9 +1650,13 @@ async function installRuntime(paths) {
1600
1650
  rm(paths.runtimeCanonicalJson, { force: true }),
1601
1651
  rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
1602
1652
  rm(resolve(dirname(paths.runtime), "capture-conflict.js"), { force: true }),
1653
+ rm(resolve(dirname(paths.runtime), "skill-context.js"), { force: true }),
1654
+ rm(resolve(dirname(paths.runtime), "learning-line.js"), { force: true }),
1603
1655
  rm(resolve(dirname(paths.runtime), "image-bytes.js"), { force: true }),
1604
1656
  rm(resolve(dirname(paths.runtime), "image-capture.js"), { force: true }),
1605
1657
  rm(resolve(dirname(paths.runtime), "retained-context.js"), { force: true }),
1658
+ rm(resolve(dirname(paths.runtime), "no-follow.js"), { force: true }),
1659
+ rm(resolve(dirname(paths.runtime), "system-command.js"), { force: true }),
1606
1660
  rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
1607
1661
  rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
1608
1662
  rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
@@ -1620,7 +1674,10 @@ async function installRuntime(paths) {
1620
1674
  const canonicalJsonModule = fileURLToPath(import.meta.resolve("@lore-co/core/canonical-json"));
1621
1675
  const guardEvaluation = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/guard-evaluation")), "utf8");
1622
1676
  const imageBytes = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/image-bytes")), "utf8");
1623
- const imageCapture = (await readFile(resolve(sourceDirectory, "image-capture.js"), "utf8")).replaceAll("@lore-co/core/image-bytes", "./image-bytes.js");
1677
+ // Hook modules refuse symbolic links through this dependency-free module.
1678
+ const noFollow = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/no-follow")), "utf8");
1679
+ const localNoFollow = (source) => source.replaceAll("@lore-co/core/no-follow", "./no-follow.js");
1680
+ const imageCapture = localNoFollow((await readFile(resolve(sourceDirectory, "image-capture.js"), "utf8")).replaceAll("@lore-co/core/image-bytes", "./image-bytes.js"));
1624
1681
  const [runtime, repository, reliabilityStore, signedSnapshot, canonicalJson, contextFallback, invocationHealthWriter,] = await Promise.all([
1625
1682
  readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
1626
1683
  readFile(fileURLToPath(import.meta.resolve("@lore-co/core/repository-location")), "utf8"),
@@ -1636,12 +1693,19 @@ async function installRuntime(paths) {
1636
1693
  }
1637
1694
  const runtimeVersion = `export const RUNTIME_VERSION = ${JSON.stringify(LORE_VERSION)};\n`;
1638
1695
  const runtimeFiles = {
1639
- "lore-hook.mjs": runtime.replaceAll("@lore-co/core/guard-evaluation", "./guard-evaluation.js").replaceAll("@lore-co/core/capture-conflict", "./capture-conflict.js"),
1696
+ "lore-hook.mjs": runtime.replaceAll("@lore-co/core/guard-evaluation", "./guard-evaluation.js").replaceAll("@lore-co/core/capture-conflict", "./capture-conflict.js").replaceAll("@lore-co/core/learning-line", "./learning-line.js").replaceAll("@lore-co/core/skill-context", "./skill-context.js").replaceAll("@lore-co/core/system-command", "./system-command.js"),
1640
1697
  "guard-evaluation.js": guardEvaluation,
1641
1698
  "capture-conflict.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/capture-conflict")), "utf8"),
1699
+ // Fits a redacted skill pack back within its cap; it has no imports.
1700
+ "skill-context.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/skill-context")), "utf8"),
1701
+ // Formats Guard items the way every integration does; imports only repository-address.js.
1702
+ "learning-line.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/learning-line")), "utf8"),
1642
1703
  "image-bytes.js": imageBytes,
1643
1704
  "image-capture.js": imageCapture,
1644
- "retained-context.js": await readFile(resolve(sourceDirectory, "retained-context.js"), "utf8"),
1705
+ "retained-context.js": localNoFollow(await readFile(resolve(sourceDirectory, "retained-context.js"), "utf8")),
1706
+ "no-follow.js": noFollow,
1707
+ // repository.js, directory-policy.js and the hook start git through this dependency-free module.
1708
+ "system-command.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/system-command")), "utf8"),
1645
1709
  "retrieval-task.js": (await readFile(resolve(sourceDirectory, "retrieval-task.js"), "utf8")).replaceAll("@lore-co/core/user-message", "./user-message.js"),
1646
1710
  "runtime-redaction.js": await readFile(resolve(sourceDirectory, "runtime-redaction.js"), "utf8"),
1647
1711
  "user-message.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/user-message")), "utf8"),
@@ -1675,9 +1739,13 @@ async function installRuntime(paths) {
1675
1739
  await Promise.all([
1676
1740
  atomicWrite(resolve(dirname(paths.runtime), "guard-evaluation.js"), guardEvaluation, 0o600),
1677
1741
  atomicWrite(resolve(dirname(paths.runtime), "capture-conflict.js"), runtimeFiles["capture-conflict.js"], 0o600),
1742
+ atomicWrite(resolve(dirname(paths.runtime), "skill-context.js"), runtimeFiles["skill-context.js"], 0o600),
1743
+ atomicWrite(resolve(dirname(paths.runtime), "learning-line.js"), runtimeFiles["learning-line.js"], 0o600),
1678
1744
  atomicWrite(resolve(dirname(paths.runtime), "image-bytes.js"), imageBytes, 0o600),
1679
1745
  atomicWrite(resolve(dirname(paths.runtime), "image-capture.js"), imageCapture, 0o600),
1680
1746
  atomicWrite(resolve(dirname(paths.runtime), "retained-context.js"), runtimeFiles["retained-context.js"], 0o600),
1747
+ atomicWrite(resolve(dirname(paths.runtime), "no-follow.js"), noFollow, 0o600),
1748
+ atomicWrite(resolve(dirname(paths.runtime), "system-command.js"), runtimeFiles["system-command.js"], 0o600),
1681
1749
  atomicWrite(resolve(dirname(paths.runtime), "retrieval-task.js"), runtimeFiles["retrieval-task.js"], 0o600),
1682
1750
  atomicWrite(resolve(dirname(paths.runtime), "runtime-redaction.js"), runtimeFiles["runtime-redaction.js"], 0o600),
1683
1751
  atomicWrite(resolve(dirname(paths.runtime), "user-message.js"), runtimeFiles["user-message.js"], 0o600),
@@ -1721,6 +1789,7 @@ async function prepareAgentEntries(installations, paths, capture) {
1721
1789
  const mcpDocuments = new Map();
1722
1790
  for (const installation of installations) {
1723
1791
  const key = installationKey(installation);
1792
+ await assertProjectConfigNotLinked(installation, paths);
1724
1793
  const document = await readJsonDocument(installation.configPath);
1725
1794
  documents.set(key, document);
1726
1795
  mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths, capture));
@@ -1794,7 +1863,24 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
1794
1863
  const generic = (await readGenericAgents(paths.loreDirectory)).length > 0;
1795
1864
  const skills = await installLoreSkill(paths, generic ? [...installations, { agent: GENERIC_AGENT, configPath: paths.config }] : installations);
1796
1865
  const agent = await installLoreAgent(paths, installations);
1866
+ const changed = new Set([...written.changedFiles, ...skills.changed, ...agent.changed]);
1867
+ const agents = new Map();
1868
+ for (const installation of installations) {
1869
+ // Every file this agent reads: hooks, MCP config, skills, and the Lore agent.
1870
+ const files = [
1871
+ installation.configPath,
1872
+ ...(isMcpHostAgent(installation.agent) ? [mcpConfigPath(installation.agent, installation.configPath)] : []),
1873
+ ...loreSkillFiles(paths, [installation]),
1874
+ ...recapSkillFiles(paths, [installation]),
1875
+ ...loreAgentFiles([installation]).map((entry) => entry.file),
1876
+ ];
1877
+ const entry = agents.get(installation.agent) ?? { agent: installation.agent, changed: false, hooksChanged: false };
1878
+ entry.changed ||= files.some((file) => changed.has(file));
1879
+ entry.hooksChanged ||= written.changedFiles.includes(installation.configPath);
1880
+ agents.set(installation.agent, entry);
1881
+ }
1797
1882
  return {
1883
+ agents: [...agents.values()],
1798
1884
  // Same order the files are written in, so the two update lines match.
1799
1885
  configFiles: [
1800
1886
  ...entries.mcpDocuments.keys(),
@@ -1809,6 +1895,35 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
1809
1895
  };
1810
1896
  });
1811
1897
  }
1898
+ /**
1899
+ * The refresh after `lore update`, by agent: which agents got new Lore files
1900
+ * and what the customer must do, or that nothing changed. --verbose adds the
1901
+ * per-file report.
1902
+ */
1903
+ export function formatRefreshSummary(result, style = PLAIN_OUTPUT) {
1904
+ const out = new Output(style);
1905
+ const updated = result.agents.filter((entry) => entry.changed).map((entry) => agentDisplayName(entry.agent));
1906
+ if (updated.length === 0)
1907
+ out.note("Your agents are up to date.");
1908
+ else
1909
+ out.outcome("ok", `Updated Lore for ${listText([...new Set(updated)])}`);
1910
+ if (result.warnings.length)
1911
+ out.blank();
1912
+ for (const warning of result.warnings)
1913
+ out.outcome("warning", tildePaths(warning, style.home));
1914
+ out.steps("Next", [
1915
+ ...(updated.length ? ["Restart your agents."] : []),
1916
+ ...(result.agents.some((entry) => entry.agent === "codex" && entry.hooksChanged) ? [`In Codex, approve Lore's updated hooks with ${out.code("/hooks")}.`] : []),
1917
+ ]);
1918
+ if (style.verbose) {
1919
+ out.blank().verbatim(out.bold("Details"));
1920
+ for (const line of formatRefreshReport(result).trimEnd().split("\n")) {
1921
+ if (!line.startsWith("warning: "))
1922
+ out.text(tildePaths(line, style.home));
1923
+ }
1924
+ }
1925
+ return out.toString();
1926
+ }
1812
1927
  /** What a refresh rewrote and what was already at this version, one line each. */
1813
1928
  export function formatRefreshReport(result) {
1814
1929
  const lines = [
@@ -1837,9 +1952,11 @@ export function formatRefreshReport(result) {
1837
1952
  async function runInternalCommand(args) {
1838
1953
  if (args.length !== 1 || args[0] !== "refresh-integrations")
1839
1954
  throw new Error("Usage: lore internal refresh-integrations");
1840
- const refreshed = await refreshAgentEntries(getLorePaths());
1955
+ const paths = getLorePaths();
1956
+ const refreshed = await refreshAgentEntries(paths);
1957
+ // `lore update` runs this with piped stdout and says how to draw through LORE_OUTPUT_*.
1841
1958
  if (refreshed !== null)
1842
- process.stdout.write(formatRefreshReport(refreshed));
1959
+ process.stdout.write(formatRefreshSummary(refreshed, detectOutputStyle({ home: paths.home })));
1843
1960
  }
1844
1961
  function countAgentIntegrations(input, agent) {
1845
1962
  const target = targetFor(agent);
@@ -1876,6 +1993,10 @@ function parseConnectArguments(args) {
1876
1993
  parsed.json = true;
1877
1994
  continue;
1878
1995
  }
1996
+ if (argument === "--verbose") {
1997
+ parsed.verbose = true;
1998
+ continue;
1999
+ }
1879
2000
  if (argument === "--login") {
1880
2001
  parsed.login = true;
1881
2002
  continue;
@@ -2108,6 +2229,7 @@ async function connectWithCredentialLocked(options, identity) {
2108
2229
  agents,
2109
2230
  agentConfigPaths,
2110
2231
  ...(capture ? {} : { capture: "off" }),
2232
+ ...(existing?.recapReminder === "off" ? { recapReminder: "off" } : {}),
2111
2233
  connectedAt: connection.profiles.find(value => value.connectionId === connection.fields.connectionId)?.connectedAt ?? now.toISOString(),
2112
2234
  timeoutMs,
2113
2235
  };
@@ -2180,16 +2302,36 @@ async function connectWithCredentialLocked(options, identity) {
2180
2302
  keptAgents,
2181
2303
  alreadyConnected,
2182
2304
  ...(knownDashboardUrl === undefined ? {} : { dashboardUrl: knownDashboardUrl }),
2183
- folder: options.cwd ?? process.cwd(),
2305
+ folder: await firstMessageFolder(options.cwd ?? process.cwd(), config.directories, paths.home),
2184
2306
  config: paths.config,
2185
2307
  changedHookFiles,
2186
2308
  backups,
2187
2309
  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
2188
- ? ["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."]
2310
+ ? [CODEX_APPROVAL_WARNING]
2189
2311
  : [])],
2190
2312
  ...(recoveryNotice ? { recoveryNotice } : {}),
2191
2313
  };
2192
2314
  }
2315
+ /**
2316
+ * The browser approval link, and whether Lore may open it: only a page on the
2317
+ * dashboard (or, without one, the API server) the person chose. With a
2318
+ * dashboard, only the server's path and query are kept; setting them rather
2319
+ * than resolving a reference keeps a `//host` path on the dashboard.
2320
+ */
2321
+ export function deviceApprovalUrl(verificationUriComplete, input) {
2322
+ const server = new URL(verificationUriComplete);
2323
+ const url = input.dashboardUrl === undefined ? server : new URL(`${input.dashboardUrl}/`);
2324
+ if (input.dashboardUrl !== undefined) {
2325
+ url.pathname = server.pathname;
2326
+ url.search = server.search;
2327
+ }
2328
+ const expectedOrigin = new URL(input.dashboardUrl ?? input.apiUrl).origin;
2329
+ return {
2330
+ url: url.toString(),
2331
+ trusted: ["http:", "https:"].includes(url.protocol) && url.origin === expectedOrigin,
2332
+ expectedOrigin,
2333
+ };
2334
+ }
2193
2335
  async function requestDeviceAuthorization(input) {
2194
2336
  let response;
2195
2337
  try {
@@ -2219,13 +2361,13 @@ async function requestDeviceAuthorization(input) {
2219
2361
  if (!authorization.success) {
2220
2362
  throw new Error("Lore returned an incompatible device authorization response.");
2221
2363
  }
2222
- const serverApprovalUrl = new URL(authorization.data.verificationUriComplete);
2223
- const approvalUrl = input.dashboardUrl === undefined
2224
- ? serverApprovalUrl.toString()
2225
- : new URL(`${serverApprovalUrl.pathname}${serverApprovalUrl.search}`, `${input.dashboardUrl}/`).toString();
2226
- process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approvalUrl}\n`);
2227
- if (input.openBrowser) {
2228
- openBrowser(approvalUrl);
2364
+ const approval = deviceApprovalUrl(authorization.data.verificationUriComplete, input);
2365
+ process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approval.url}\n`);
2366
+ if (input.openBrowser && approval.trusted) {
2367
+ openBrowser(approval.url);
2368
+ }
2369
+ else if (input.openBrowser) {
2370
+ process.stderr.write(`Lore did not open your browser because this link is not on ${approval.expectedOrigin}. Open it yourself if you expected it, or pass --dashboard-url.\n`);
2229
2371
  }
2230
2372
  const expiresAt = Date.parse(authorization.data.expiresAt);
2231
2373
  const intervalMs = authorization.data.intervalSeconds * 1_000;
@@ -2340,48 +2482,182 @@ export function connectNextStep(agent) {
2340
2482
  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.";
2341
2483
  }
2342
2484
  }
2343
- export function formatConnectResult(result) {
2344
- const folderSummary = result.capture.folders === null ? "all projects allowed by the workspace"
2345
- : result.capture.folders.length ? result.capture.folders.join(", ") : "paused — choose folders with --folder <path>";
2346
- const installed = result.installedAgents.join(", ");
2485
+ /** A folder Lore records in: the current one when it is inside the capture folders, else the first capture folder. */
2486
+ export async function firstMessageFolder(cwd, directories, home) {
2487
+ const first = directories?.include[0];
2488
+ if (first === undefined || (await checkDirectoryRules(directories, cwd, home)).allowed)
2489
+ return cwd;
2490
+ const expanded = first.startsWith("~/") || first.startsWith("~\\") ? resolve(home, first.slice(2)) : first;
2491
+ const parts = resolve(expanded).split(sep);
2492
+ const wildcard = parts.findIndex((part) => /[*?]/u.test(part));
2493
+ return wildcard < 0 ? resolve(expanded) : parts.slice(0, wildcard).join(sep) || sep;
2494
+ }
2495
+ const CODEX_APPROVAL_WARNING = "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.";
2496
+ /** Product names for human output; --json keeps the agent ids. */
2497
+ export function agentDisplayName(agent) {
2498
+ switch (agent) {
2499
+ case "claude": return "Claude Code";
2500
+ case "codex": return "Codex";
2501
+ case "copilot":
2502
+ case "copilot-cli": return "Copilot CLI";
2503
+ case "copilot-vscode": return "Copilot in VS Code";
2504
+ case "cursor": return "Cursor";
2505
+ case "opencode": return "OpenCode";
2506
+ case "polytoken": return "Polytoken";
2507
+ case "devin": return "Devin CLI";
2508
+ case "cowork": return "Claude Cowork";
2509
+ case "t3code": return "T3 Code";
2510
+ case "generic": return "MCP agent";
2511
+ }
2512
+ }
2513
+ /** What one connected agent still needs, for its row in the connect summary. */
2514
+ function connectAgentNote(agent, out) {
2515
+ switch (agent) {
2516
+ case "claude": return "ready after restart, allow the lore server when asked";
2517
+ case "codex": return `ready after restart, approve hooks with ${out.code("/hooks")}`;
2518
+ case "cursor": return "ready after restart, enable lore-native and lore";
2519
+ case "copilot-vscode": return "ready after you restart the VS Code session";
2520
+ case "devin": return `run ${out.code("lore devin cli setup")}, then open a new terminal`;
2521
+ case "generic": return "add the MCP entry and instructions below, then restart it";
2522
+ default: return "ready after restart";
2523
+ }
2524
+ }
2525
+ /**
2526
+ * The human summary of a connect run: the outcome, where Lore records, one
2527
+ * row per agent, and the numbered steps left for the customer. Paths and
2528
+ * per-file detail appear with --verbose; --json prints the result itself.
2529
+ */
2530
+ export function formatConnectResult(result, options = {}) {
2531
+ const style = options.style ?? { ...PLAIN_OUTPUT, home: dirname(dirname(result.config)) };
2532
+ const out = new Output(style);
2347
2533
  const generic = result.genericAgent;
2534
+ const genericName = generic?.id ?? agentDisplayName("generic");
2535
+ const name = (agent) => agent === GENERIC_AGENT ? genericName : agentDisplayName(agent);
2348
2536
  // With only an MCP agent connected, nothing on this machine records turns.
2349
2537
  const nothingRecords = result.capture.mode === "off" || (generic !== undefined && result.agents.every((agent) => agent === GENERIC_AGENT));
2350
- const lines = [
2351
- result.alreadyConnected
2352
- ? `Already connected to ${result.identity.workspaceName} for ${installed}; nothing changed.`
2353
- : `access authorized; installed: ${installed}`,
2354
- ...(result.keptAgents.length ? [`kept: ${result.keptAgents.join(", ")}`] : []),
2355
- `api_url: ${result.apiUrl}`,
2356
- `config: ${result.config}`,
2357
- ...(result.capture.mode === "off" ? [`capture: ${captureSummary("off")}`] : []),
2358
- ...(generic ? [`capture for ${generic.id}: off; an MCP-only agent has no hooks, so Lore saves only what you ask it to`] : []),
2359
- `${nothingRecords ? "folders" : "capture"}: ${folderSummary}${result.capture.excludedFolders.length ? `; excluded: ${result.capture.excludedFolders.join(", ")}` : ""}`,
2360
- ...result.installedAgents.map((agent) => `Next step for ${agent}: ${connectNextStep(agent)}`),
2361
- generic !== undefined && nothingRecords
2362
- ? `Ask your agent "Lore, has anything like this come up before?" in ${result.folder}, then run lore doctor.`
2363
- : result.capture.mode === "off"
2364
- ? `Send one message in ${result.folder}, then run lore doctor to confirm context delivery.`
2538
+ const pausedBy = result.capture.mode === "on" ? options.capturePausedBy ?? null : null;
2539
+ const folders = result.capture.folders;
2540
+ const shownFolders = folders === null ? "all projects the workspace allows"
2541
+ : folders.length === 0 ? "no folders yet"
2542
+ : listText(folders.length > 3 ? [...folders.slice(0, 2).map((folder) => out.path(folder)), `${folders.length - 2} more`] : folders.map((folder) => out.path(folder)));
2543
+ const excluded = result.capture.excludedFolders.length ? `, except ${listText(result.capture.excludedFolders.map((folder) => out.path(folder)))}` : "";
2544
+ const mode = generic !== undefined && nothingRecords && result.capture.mode === "on" ? "MCP only, saves what you ask"
2545
+ : result.capture.mode === "off" ? "capture off"
2546
+ : pausedBy === "workspace" ? "paused by workspace"
2547
+ : pausedBy === "member" ? "paused by your setting"
2548
+ : "capture on";
2549
+ const records = mode === "capture on";
2550
+ out.outcome("ok", result.alreadyConnected
2551
+ ? `Already connected to ${result.identity.workspaceName}, nothing changed`
2552
+ : `Connected to ${result.identity.workspaceName}`);
2553
+ out.note(`${records ? "Recording in" : "Lore runs in"} ${shownFolders}${excluded}${out.separator}${mode}`);
2554
+ if (generic !== undefined && !nothingRecords)
2555
+ out.note(`${generic.id} is MCP-only: Lore saves only what you ask it to.`);
2556
+ // One row per agent: set up in this run first, then the ones kept as they were.
2557
+ const rows = [];
2558
+ const seen = new Set();
2559
+ for (const [agent, mark, note] of [
2560
+ ...result.installedAgents.map((agent) => [agent, out.mark("ok"), connectAgentNote(agent, out)]),
2561
+ ...result.keptAgents.map((agent) => [agent, "", out.dim("already set up")]),
2562
+ ]) {
2563
+ if (seen.has(name(agent)))
2564
+ continue;
2565
+ seen.add(name(agent));
2566
+ rows.push([name(agent), mark, note]);
2567
+ }
2568
+ if (rows.length) {
2569
+ out.blank();
2570
+ const width = Math.max(...rows.map(([label]) => label.length));
2571
+ for (const [label, mark, note] of rows)
2572
+ out.row(label, width, note, mark);
2573
+ }
2574
+ const steps = [];
2575
+ const restart = result.installedAgents.filter((agent) => agent !== "devin" && agent !== GENERIC_AGENT);
2576
+ if (restart.length) {
2577
+ steps.push(restart.length === rows.length ? "Restart the agents above." : `Restart ${listText([...new Set(restart.map(name))])}.`);
2578
+ }
2579
+ if (result.installedAgents.includes("devin"))
2580
+ steps.push(`Run ${out.code("lore devin cli setup")}, then open a new terminal.`);
2581
+ if (generic !== undefined)
2582
+ steps.push(`Add the MCP entry and instructions below to ${generic.id}, then restart it.`);
2583
+ const warnings = result.warnings.filter((warning) => warning !== CODEX_APPROVAL_WARNING);
2584
+ if (warnings.length !== result.warnings.length)
2585
+ steps.push(`In Codex, approve Lore's updated hooks with ${out.code("/hooks")}.`);
2586
+ const notes = [];
2587
+ for (const line of result.recoveryNotice?.split("\n") ?? []) {
2588
+ const shell = /^Open a new terminal or run: (.+)$/u.exec(line);
2589
+ if (!result.recoveryNotice.startsWith("Recovery enabled:"))
2590
+ warnings.push(line);
2591
+ else if (shell)
2592
+ steps.push(`Open a new terminal, or run ${out.code(shell[1])}.`);
2593
+ else if (line.startsWith("Codex requires one-time approval")) {
2594
+ if (!result.installedAgents.includes("codex"))
2595
+ steps.push(`In Codex, approve Lore's hooks with ${out.code("/hooks")}.`);
2596
+ }
2597
+ else
2598
+ notes.push(style.rich ? line : line.replaceAll("→", "->"));
2599
+ }
2600
+ const folder = out.path(result.folder);
2601
+ const ask = "\"Lore, has anything like this come up before?\"";
2602
+ if (folders === null || folders.length > 0) {
2603
+ steps.push(generic !== undefined && nothingRecords
2604
+ ? `In ${folder}, ask your agent ${ask}, then run ${out.code("lore doctor")}.`
2605
+ : !records
2606
+ ? `In ${folder}, ask your agent anything, then run ${out.code("lore doctor")} to check that context arrives.`
2365
2607
  : result.dashboardUrl === undefined
2366
- ? `Send one message in ${result.folder}, then run lore doctor to confirm recording.`
2367
- : `Send one message in ${result.folder}, then open ${result.dashboardUrl}/connect to confirm recording.`,
2368
- ...(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."] : []),
2369
- ...(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."] : []),
2370
- ...(result.recoveryNotice ? [result.recoveryNotice] : []),
2371
- ...result.warnings.map((warning) => `warning: ${warning}`),
2372
- ...(generic ? [
2373
- generic.writeTools
2374
- ? `Saves from ${generic.id} are proposals credited to ${generic.id}; review them in Learnings.`
2375
- : `${generic.id} can read Lore; it saves through the lore CLI. Reconnect with --write to let it save through MCP (saves stay proposals).`,
2376
- "",
2377
- `MCP entry for ${generic.id} (merge into its mcpServers settings):`,
2378
- JSON.stringify({ mcpServers: generic.mcpServers }, null, 2),
2379
- "",
2380
- "Instructions for AGENTS.md, GEMINI.md, or the agent's rules file:",
2381
- generic.instructions,
2382
- ] : []),
2383
- ];
2384
- return `${lines.join("\n")}\n`;
2608
+ ? `In ${folder}, ask your agent anything, then run ${out.code("lore doctor")} to confirm recording.`
2609
+ : `In ${folder}, ask your agent anything, then check ${out.code(`${result.dashboardUrl}/connect`)}.`);
2610
+ }
2611
+ if (notes.length)
2612
+ out.blank();
2613
+ for (const note of notes)
2614
+ out.note(note);
2615
+ if (warnings.length)
2616
+ out.blank();
2617
+ for (const warning of warnings)
2618
+ out.outcome("warning", tildePaths(warning, style.home));
2619
+ out.steps("Next", steps);
2620
+ const recap = result.skillFiles.some((file) => basename(dirname(file)) === "recap");
2621
+ if (result.skillFiles.length || result.agentFiles.length) {
2622
+ out.blank().text(`Try: ${ask}${recap ? `${style.rich ? " · " : " or "}${out.code("/recap")} to save a session` : ""}`, 0);
2623
+ }
2624
+ if (style.verbose) {
2625
+ const details = [
2626
+ ["Workspace", `${result.identity.workspaceName} (${result.identity.workspaceId})`],
2627
+ ["API", result.apiUrl],
2628
+ ...(result.dashboardUrl === undefined ? [] : [["Dashboard", result.dashboardUrl]]),
2629
+ ["Config", out.path(result.config)],
2630
+ ["Capture", captureSummary(result.capture.mode, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })],
2631
+ ["Folders", folders === null ? "all projects allowed by the workspace" : folders.map((value) => out.path(value)).join(", ") || "none"],
2632
+ ...(result.capture.excludedFolders.length ? [["Excluded", result.capture.excludedFolders.map((value) => out.path(value)).join(", ")]] : []),
2633
+ ["Agents", result.agents.join(", ")],
2634
+ ...(result.keptAgents.length ? [["Kept", result.keptAgents.join(", ")]] : []),
2635
+ ["Changed", result.changedHookFiles.map((file) => out.path(file)).join(", ") || "nothing"],
2636
+ ...(result.backups.length ? [["Backups", result.backups.map((file) => out.path(file)).join(", ")]] : []),
2637
+ ...(result.skillFiles.length ? [["Skills", result.skillFiles.map((file) => out.path(file)).join(", ")]] : []),
2638
+ ...(result.agentFiles.length ? [["Lore agent", result.agentFiles.map((file) => out.path(file)).join(", ")]] : []),
2639
+ ];
2640
+ out.blank().verbatim(out.bold("Details"));
2641
+ const width = Math.max(...details.map(([label]) => label.length));
2642
+ for (const [label, value] of details)
2643
+ out.row(label, width, value);
2644
+ }
2645
+ if (generic !== undefined) {
2646
+ out.blank().note(generic.writeTools
2647
+ ? `Saves from ${generic.id} are proposals credited to ${generic.id}; review them in Learnings.`
2648
+ : `${generic.id} can read Lore; it saves through the lore CLI. Reconnect with --write to let it save through MCP (saves stay proposals).`, 0);
2649
+ // Copied into the agent's own settings: printed exactly, never styled or wrapped.
2650
+ out.blank().verbatim(`MCP entry for ${generic.id} (merge into its mcpServers settings):`)
2651
+ .verbatim(JSON.stringify({ mcpServers: generic.mcpServers }, null, 2))
2652
+ .blank().verbatim("Instructions for AGENTS.md, GEMINI.md, or the agent's rules file:")
2653
+ .verbatim(generic.instructions);
2654
+ }
2655
+ return out.toString();
2656
+ }
2657
+ /** Show the home folder as ~ inside a message that names paths. */
2658
+ function tildePaths(text, home) {
2659
+ const prefix = home.endsWith(sep) ? home : `${home}${sep}`;
2660
+ return text.replaceAll(prefix, `~${sep}`);
2385
2661
  }
2386
2662
  async function connectCommand(args) {
2387
2663
  const parsed = parseConnectArguments(args);
@@ -2438,7 +2714,18 @@ async function connectCommand(args) {
2438
2714
  });
2439
2715
  if (authorization)
2440
2716
  await acknowledgeDeviceAuthorization(apiUrl, authorization);
2441
- writeResult(result, parsed.json, formatConnectResult(result));
2717
+ if (parsed.json) {
2718
+ writeResult(result, true, "");
2719
+ return;
2720
+ }
2721
+ // The summary says when a dashboard switch pauses recording; the policy read is best-effort.
2722
+ const capturePausedBy = result.capture.mode === "on"
2723
+ ? (await checkWorkspaceRepository({ apiUrl: result.apiUrl, token, timeoutMs: parsed.timeoutMs ?? 2_500 }, undefined).catch(() => undefined))?.capturePausedBy ?? null
2724
+ : null;
2725
+ process.stdout.write(formatConnectResult(result, {
2726
+ style: detectOutputStyle({ verbose: parsed.verbose === true, home: getLorePaths().home }),
2727
+ capturePausedBy,
2728
+ }));
2442
2729
  }
2443
2730
  async function queueCount(paths) {
2444
2731
  try {
@@ -2501,7 +2788,7 @@ async function inspectReliabilityStore(config, paths) {
2501
2788
  const MAX_RUNTIME_MODULE_BYTES = 2 * 1024 * 1024;
2502
2789
  const MAX_RUNTIME_MANIFEST_BYTES = 64 * 1024;
2503
2790
  async function readRuntimeFile(path, maxBytes, executable = false) {
2504
- const handle = await open(path, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
2791
+ const handle = await openNoFollow(path, fsConstants.O_RDONLY);
2505
2792
  try {
2506
2793
  const metadata = await handle.stat();
2507
2794
  if (!metadata.isFile() ||
@@ -2544,9 +2831,13 @@ async function inspectRuntimeInstallation(required, paths) {
2544
2831
  "lore-hook.mjs": paths.runtime,
2545
2832
  "guard-evaluation.js": resolve(dirname(paths.runtime), "guard-evaluation.js"),
2546
2833
  "capture-conflict.js": resolve(dirname(paths.runtime), "capture-conflict.js"),
2834
+ "skill-context.js": resolve(dirname(paths.runtime), "skill-context.js"),
2835
+ "learning-line.js": resolve(dirname(paths.runtime), "learning-line.js"),
2547
2836
  "image-bytes.js": resolve(dirname(paths.runtime), "image-bytes.js"),
2548
2837
  "image-capture.js": resolve(dirname(paths.runtime), "image-capture.js"),
2549
2838
  "retained-context.js": resolve(dirname(paths.runtime), "retained-context.js"),
2839
+ "no-follow.js": resolve(dirname(paths.runtime), "no-follow.js"),
2840
+ "system-command.js": resolve(dirname(paths.runtime), "system-command.js"),
2550
2841
  "retrieval-task.js": resolve(dirname(paths.runtime), "retrieval-task.js"),
2551
2842
  "runtime-redaction.js": resolve(dirname(paths.runtime), "runtime-redaction.js"),
2552
2843
  "user-message.js": resolve(dirname(paths.runtime), "user-message.js"),
@@ -2665,6 +2956,7 @@ async function getAgentStatus(agent, config, paths, runtime) {
2665
2956
  const units = [];
2666
2957
  for (const [index, path] of configPaths.entries()) {
2667
2958
  try {
2959
+ await assertProjectConfigNotLinked({ agent, configPath: path }, paths);
2668
2960
  const document = await readJsonDocument(path);
2669
2961
  installed += countAgentIntegrations(document.value, agent);
2670
2962
  existing += document.exists ? 1 : 0;
@@ -2819,12 +3111,60 @@ function formatRetrievalFailureDiagnostics(retrieval) {
2819
3111
  return (`local_retrieval_failure_reasons: ${reasons.length === 0 ? "-" : reasons.join(" ")}\n` +
2820
3112
  `last_retrieval_failure: ${last === null ? "-" : `${last.reason} (${last.path} request, agent ${last.agent}, at ${last.at})`}\n`);
2821
3113
  }
3114
+ /**
3115
+ * The plain lines at the top of `lore status`: live context over the last
3116
+ * days on this machine, then a server behind this CLI and out-of-date Lore
3117
+ * files, each only when true.
3118
+ */
3119
+ function statusHealth(data, serverVersion) {
3120
+ const context = data.reliability?.operational.retrieval.recent ?? null;
3121
+ const staleFiles = [];
3122
+ if (data.runtimeRequired && !data.runtimeInstalled && data.runtimeVersion !== null) {
3123
+ staleFiles.push({ name: "Lore hooks", version: data.runtimeVersion === LORE_VERSION ? null : data.runtimeVersion });
3124
+ }
3125
+ for (const agent of data.agents) {
3126
+ // Hook agents share the runtime above; a plugin carries its own copy.
3127
+ if (agent.installation.runtime !== "stale" || targetFor(agent.agent).runtimeRequired)
3128
+ continue;
3129
+ staleFiles.push({ name: `${agentDisplayName(agent.agent)} plugin`, version: agent.installation.runtimeVersion });
3130
+ }
3131
+ const lines = [
3132
+ contextHealthLine(context ?? undefined),
3133
+ serverBehindLine(serverVersion, LORE_VERSION),
3134
+ staleFilesLine(staleFiles),
3135
+ ].filter((line) => line !== null);
3136
+ return { context, serverVersion, staleFiles, lines };
3137
+ }
3138
+ /** The server's release, from the identity endpoint; null when unreachable. */
3139
+ async function connectedServerVersion(config) {
3140
+ try {
3141
+ const response = await fetch(`${config.apiUrl}/v1/workspace/identity`, {
3142
+ headers: { accept: "application/json", authorization: `Bearer ${config.token}`, "x-lore-connection-identity": "1" },
3143
+ signal: AbortSignal.timeout(Math.min(config.timeoutMs ?? 2_000, 2_000)),
3144
+ });
3145
+ if (!response.ok)
3146
+ return null;
3147
+ const parsed = WorkspaceIdentityResponseSchema.safeParse(await response.json());
3148
+ return parsed.success ? parsed.data.server.version : null;
3149
+ }
3150
+ catch {
3151
+ return null;
3152
+ }
3153
+ }
2822
3154
  async function statusCommand(args) {
2823
3155
  const parsed = parseOutputArguments(args, STATUS_HELP, "status");
2824
3156
  if (parsed === null) {
2825
3157
  return;
2826
3158
  }
2827
- const data = await statusData(getLorePaths(), process.cwd());
3159
+ const paths = getLorePaths();
3160
+ const [statusResult, serverVersion] = await Promise.all([
3161
+ statusData(paths, process.cwd()),
3162
+ diagnosticConfig(paths, process.cwd()).then((config) => config === null ? null : connectedServerVersion(config)),
3163
+ ]);
3164
+ const health = statusHealth(statusResult, serverVersion);
3165
+ const data = { ...statusResult, health };
3166
+ if (!parsed.json && loreDisabled(process.env))
3167
+ process.stdout.write(`${LORE_DISABLED_MESSAGE}\n`);
2828
3168
  if (!parsed.json && data.connection)
2829
3169
  process.stdout.write(`connection: ${data.connection}\n`);
2830
3170
  const agentLines = data.agents
@@ -2837,7 +3177,7 @@ async function statusCommand(args) {
2837
3177
  ? "-"
2838
3178
  : `${(value * 100).toFixed(1)}%`;
2839
3179
  const retrievalFailureLines = formatRetrievalFailureDiagnostics(data.reliability?.operational.retrieval);
2840
- 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`);
3180
+ writeResult(data, parsed.json, `${health.lines.map((line) => `${line}\n`).join("")}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`);
2841
3181
  }
2842
3182
  async function disconnectCommand(args) {
2843
3183
  const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
@@ -2860,6 +3200,9 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
2860
3200
  ]);
2861
3201
  for (const installation of installations) {
2862
3202
  const path = installation.configPath;
3203
+ // Leave linked project files alone; disconnecting everything else still matters.
3204
+ if (await linkedProjectConfigFile(installation, paths) !== null)
3205
+ continue;
2863
3206
  if (isMcpHostAgent(installation.agent)) {
2864
3207
  const mcpPath = mcpConfigPath(installation.agent, path);
2865
3208
  if (installation.agent === "codex") {
@@ -2914,9 +3257,12 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
2914
3257
  rm(paths.runtimeContextFallback, { force: true }),
2915
3258
  rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
2916
3259
  rm(resolve(dirname(paths.runtime), "capture-conflict.js"), { force: true }),
3260
+ rm(resolve(dirname(paths.runtime), "skill-context.js"), { force: true }),
3261
+ rm(resolve(dirname(paths.runtime), "learning-line.js"), { force: true }),
2917
3262
  rm(resolve(dirname(paths.runtime), "image-bytes.js"), { force: true }),
2918
3263
  rm(resolve(dirname(paths.runtime), "image-capture.js"), { force: true }),
2919
3264
  rm(resolve(dirname(paths.runtime), "retained-context.js"), { force: true }),
3265
+ rm(resolve(dirname(paths.runtime), "no-follow.js"), { force: true }),
2920
3266
  rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
2921
3267
  rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
2922
3268
  rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
@@ -3003,6 +3349,36 @@ export function operationalMetricsCheck(metrics) {
3003
3349
  : ""),
3004
3350
  };
3005
3351
  }
3352
+ export const SEMANTIC_SEARCH_WINDOW_DAYS = 7;
3353
+ const SEMANTIC_SEARCH_MINIMUM_ATTEMPTS = 10;
3354
+ /**
3355
+ * Embedding failures over the last week. The server records `semantic_unavailable`
3356
+ * only when embeddings are configured and the request failed, and `lexical_only`
3357
+ * when none are configured, so a self-hosted server without an embedding key
3358
+ * never warns. Context still arrives during an outage, so nothing else shows it.
3359
+ */
3360
+ export function semanticSearchCheck(retrieval) {
3361
+ const period = `in the last ${SEMANTIC_SEARCH_WINDOW_DAYS} days`;
3362
+ const attempts = retrieval.hybrid + retrieval.semanticUnavailable;
3363
+ const failed = retrieval.semanticUnavailable;
3364
+ if (attempts >= SEMANTIC_SEARCH_MINIMUM_ATTEMPTS && failed * 2 > attempts) {
3365
+ return {
3366
+ name: "semantic-search",
3367
+ status: "warning",
3368
+ detail: `Most prompts ${period} got keyword-only context because embedding requests are failing (${failed} of ${attempts} failed). Check the server's embedding key and provider.`,
3369
+ };
3370
+ }
3371
+ return {
3372
+ name: "semantic-search",
3373
+ status: "ok",
3374
+ detail: attempts === 0
3375
+ ? retrieval.lexicalOnly > 0
3376
+ ? "keyword search only; embeddings are not configured on the server"
3377
+ : `no prompts with context ${period}`
3378
+ : `semantic search on ${retrieval.hybrid} of ${attempts} prompts ${period}` +
3379
+ (failed > 0 ? `; ${failed} fell back to keywords` : ""),
3380
+ };
3381
+ }
3006
3382
  /**
3007
3383
  * Local outbox health. Dead letters that can never be replayed (an old snapshot
3008
3384
  * format or a permanent rejection) are a note, not an error: replaying them
@@ -3079,28 +3455,41 @@ async function apiChecks(config) {
3079
3455
  : detail,
3080
3456
  });
3081
3457
  }
3082
- try {
3083
- const response = await fetch(`${config.apiUrl}/v1/reliability/metrics`, {
3084
- headers: {
3085
- authorization: `Bearer ${config.token}`,
3086
- "user-agent": `lore-cli/${LORE_VERSION}`,
3087
- },
3088
- signal: AbortSignal.timeout(3_000),
3089
- });
3090
- if (!response.ok) {
3091
- throw new Error("Operational metrics request failed");
3092
- }
3093
- checks.push(operationalMetricsCheck(OperationalMetricsResponseSchema.parse(await response.json())));
3094
- }
3095
- catch {
3096
- checks.push({
3458
+ const now = Date.now();
3459
+ const [daily, weekly] = await Promise.allSettled([
3460
+ // The server's default window is the last 24 hours.
3461
+ operationalMetrics(config),
3462
+ operationalMetrics(config, new Date(now - SEMANTIC_SEARCH_WINDOW_DAYS * 24 * 60 * 60_000)),
3463
+ ]);
3464
+ checks.push(daily.status === "fulfilled"
3465
+ ? operationalMetricsCheck(daily.value)
3466
+ : {
3097
3467
  name: "server-operational-metrics",
3098
3468
  status: "warning",
3099
3469
  detail: "authenticated operational metrics are unavailable",
3100
3470
  });
3471
+ // Without the weekly metrics, the warning above already says they are unavailable.
3472
+ if (weekly.status === "fulfilled") {
3473
+ checks.push(semanticSearchCheck(weekly.value.retrieval));
3101
3474
  }
3102
3475
  return checks;
3103
3476
  }
3477
+ async function operationalMetrics(config, from) {
3478
+ const url = new URL(`${config.apiUrl}/v1/reliability/metrics`);
3479
+ if (from !== undefined)
3480
+ url.searchParams.set("from", from.toISOString());
3481
+ const response = await fetch(url, {
3482
+ headers: {
3483
+ authorization: `Bearer ${config.token}`,
3484
+ "user-agent": `lore-cli/${LORE_VERSION}`,
3485
+ },
3486
+ signal: AbortSignal.timeout(3_000),
3487
+ });
3488
+ if (!response.ok) {
3489
+ throw new Error("Operational metrics request failed");
3490
+ }
3491
+ return OperationalMetricsResponseSchema.parse(await response.json());
3492
+ }
3104
3493
  async function verifyConnectedInstallation() {
3105
3494
  const paths = getLorePaths();
3106
3495
  const config = await readConnectorConfig(paths);
@@ -3119,6 +3508,7 @@ function formatCaptureResult(result) {
3119
3508
  const lines = [
3120
3509
  `capture: ${captureSummary(result.capture, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })}`,
3121
3510
  ...(result.capture === "on" && pausedBy !== null ? [captureResumeHint(pausedBy)] : []),
3511
+ ...(result.recapReminder === undefined ? [] : [result.recapReminder === "on" ? "recap reminder: on; lore capture reminder off stops it" : "recap reminder: off"]),
3122
3512
  ...(result.changed
3123
3513
  ? [
3124
3514
  `updated: ${result.configFiles.join(", ")}`,
@@ -3146,7 +3536,31 @@ export async function setCaptureMode(capture, paths = getLorePaths()) {
3146
3536
  const devinFiles = await refreshDevinHooks(paths.home, capture === "on");
3147
3537
  return { capture, changed: true, configFiles: [...refreshed?.configFiles ?? [], ...devinFiles] };
3148
3538
  }
3539
+ /** The once-per-session /recap reminder agents get while nothing is recorded. Hooks read it on each prompt. */
3540
+ export async function setRecapReminder(reminder, paths = getLorePaths()) {
3541
+ const config = await readConnectorConfig(paths);
3542
+ if (config === null)
3543
+ throw new Error("Lore is not connected. Run lore connect.");
3544
+ if ((config.recapReminder ?? "on") === reminder)
3545
+ return { recapReminder: reminder, changed: false };
3546
+ await withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
3547
+ const { recapReminder: _previous, ...rest } = config;
3548
+ await atomicWriteJson(paths.config, { ...rest, ...(reminder === "off" ? { recapReminder: reminder } : {}) }, 65536);
3549
+ });
3550
+ return { recapReminder: reminder, changed: true };
3551
+ }
3149
3552
  async function captureCommand(args) {
3553
+ if (args[0] === "reminder") {
3554
+ const json = args.includes("--json");
3555
+ const [reminder, ...extra] = args.slice(1).filter((argument) => argument !== "--json");
3556
+ if ((reminder !== "on" && reminder !== "off") || extra.length > 0)
3557
+ throw new Error("Usage: lore capture reminder off|on [--json]");
3558
+ const result = await setRecapReminder(reminder);
3559
+ writeResult(result, json, reminder === "on"
3560
+ ? "recap reminder: on. While nothing is recorded, your agent suggests /recap once in a session where you confirm a decision.\n"
3561
+ : "recap reminder: off. Your agent no longer suggests /recap; you can still run it any time.\n");
3562
+ return;
3563
+ }
3150
3564
  let mode;
3151
3565
  let json = false;
3152
3566
  for (const argument of args) {
@@ -3162,7 +3576,7 @@ async function captureCommand(args) {
3162
3576
  mode = argument;
3163
3577
  continue;
3164
3578
  }
3165
- throw new Error("Usage: lore capture [status|on|off] [--json]");
3579
+ throw new Error("Usage: lore capture [status|on|off|reminder off|reminder on] [--json]");
3166
3580
  }
3167
3581
  const paths = getLorePaths();
3168
3582
  if (mode === undefined || mode === "status") {
@@ -3172,7 +3586,8 @@ async function captureCommand(args) {
3172
3586
  const capture = captureEnabled(config) ? "on" : "off";
3173
3587
  // Same reading as lore status and lore doctor: the folder's connection and its dashboard switches.
3174
3588
  const switches = capture === "on" ? await captureSwitches(await diagnosticConfig(paths, process.cwd()), paths.home, process.cwd()) : CAPTURE_SWITCHES_UNCHECKED;
3175
- const result = { capture, pausedBy: switches.pausedBy, changed: false, configFiles: [] };
3589
+ const recorded = capture === "on" && switches.pausedBy === null;
3590
+ const result = { capture, pausedBy: switches.pausedBy, ...(recorded ? {} : { recapReminder: config.recapReminder ?? "on" }), changed: false, configFiles: [] };
3176
3591
  writeResult(result, json, formatCaptureResult(result));
3177
3592
  return;
3178
3593
  }
@@ -3219,6 +3634,8 @@ async function doctorCommand(args) {
3219
3634
  if (genericChecks.length > 0 && !status.agents.some((agent) => agent.configured)) {
3220
3635
  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" };
3221
3636
  }
3637
+ if (loreDisabled(process.env))
3638
+ checks.unshift({ name: "lore-disable", status: "warning", detail: LORE_DISABLED_MESSAGE });
3222
3639
  checks.push({
3223
3640
  name: "platform",
3224
3641
  status: platform() === "darwin" || platform() === "linux" || isWindows() ? "ok" : "error",
@@ -3571,11 +3988,13 @@ async function executeCli(args = process.argv.slice(2)) {
3571
3988
  await runApproachCommand(commandArgs, await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()));
3572
3989
  return;
3573
3990
  case "skill":
3574
- await runSkillCommand(commandArgs, commandArgs[0] === "discover" ? null : await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()));
3991
+ await runSkillCommand(commandArgs, commandArgs[0] === "discover" ? null : await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()), getLorePaths(process.env.LORE_HOME).loreDirectory);
3575
3992
  return;
3576
- case "guard":
3577
- await runGuardCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
3993
+ case "guard": {
3994
+ const paths = getLorePaths();
3995
+ await runGuardCommand(commandArgs, await readConnectorConfig(paths, process.cwd()), paths.home);
3578
3996
  return;
3997
+ }
3579
3998
  case "resume":
3580
3999
  await runResumeCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()), getLorePaths());
3581
4000
  return;