@lore-co/cli 0.2.20 → 0.2.22

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 (67) hide show
  1. package/README.md +18 -2
  2. package/dist/approach-guidance.d.ts.map +1 -1
  3. package/dist/approach-guidance.js +1 -0
  4. package/dist/approach-guidance.js.map +1 -1
  5. package/dist/approach.d.ts +1 -1
  6. package/dist/approach.d.ts.map +1 -1
  7. package/dist/approach.js +11 -3
  8. package/dist/approach.js.map +1 -1
  9. package/dist/ask.d.ts +9 -1
  10. package/dist/ask.d.ts.map +1 -1
  11. package/dist/ask.js +32 -3
  12. package/dist/ask.js.map +1 -1
  13. package/dist/cli.d.ts +36 -2
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +225 -48
  16. package/dist/cli.js.map +1 -1
  17. package/dist/command-help.d.ts +3 -1
  18. package/dist/command-help.d.ts.map +1 -1
  19. package/dist/command-help.js +13 -1
  20. package/dist/command-help.js.map +1 -1
  21. package/dist/devin-cli.js +2 -2
  22. package/dist/devin-cli.js.map +1 -1
  23. package/dist/embeddings.d.ts +12 -0
  24. package/dist/embeddings.d.ts.map +1 -0
  25. package/dist/embeddings.js +105 -0
  26. package/dist/embeddings.js.map +1 -0
  27. package/dist/generated-assets.d.ts +5 -4
  28. package/dist/generated-assets.d.ts.map +1 -1
  29. package/dist/generated-assets.js +5 -4
  30. package/dist/generated-assets.js.map +1 -1
  31. package/dist/github.d.ts.map +1 -1
  32. package/dist/github.js +242 -4
  33. package/dist/github.js.map +1 -1
  34. package/dist/guard.js +2 -2
  35. package/dist/guard.js.map +1 -1
  36. package/dist/handoff.js +2 -2
  37. package/dist/handoff.js.map +1 -1
  38. package/dist/learnings.d.ts +31 -0
  39. package/dist/learnings.d.ts.map +1 -0
  40. package/dist/learnings.js +265 -0
  41. package/dist/learnings.js.map +1 -0
  42. package/dist/platform.d.ts +7 -0
  43. package/dist/platform.d.ts.map +1 -1
  44. package/dist/platform.js +25 -1
  45. package/dist/platform.js.map +1 -1
  46. package/dist/reliability-store.d.ts +31 -0
  47. package/dist/reliability-store.d.ts.map +1 -1
  48. package/dist/reliability-store.js +89 -0
  49. package/dist/reliability-store.js.map +1 -1
  50. package/dist/runtime.d.ts +13 -0
  51. package/dist/runtime.d.ts.map +1 -1
  52. package/dist/runtime.js +119 -28
  53. package/dist/runtime.js.map +1 -1
  54. package/dist/self-host.d.ts +2 -2
  55. package/dist/self-host.d.ts.map +1 -1
  56. package/dist/self-host.js +4 -3
  57. package/dist/self-host.js.map +1 -1
  58. package/dist/skills.d.ts.map +1 -1
  59. package/dist/skills.js +24 -4
  60. package/dist/skills.js.map +1 -1
  61. package/dist/uninstall.d.ts.map +1 -1
  62. package/dist/uninstall.js +4 -3
  63. package/dist/uninstall.js.map +1 -1
  64. package/dist/update.d.ts.map +1 -1
  65. package/dist/update.js +4 -23
  66. package/dist/update.js.map +1 -1
  67. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -24,6 +24,8 @@ import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, COPILOT_
24
24
  import { runHook, } from "./runtime.js";
25
25
  import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
26
26
  import { runAskCommand } from "./ask.js";
27
+ import { runLearningsCommand } from "./learnings.js";
28
+ import { runEmbeddingsCommand } from "./embeddings.js";
27
29
  import { runRememberCommand } from "./remember.js";
28
30
  import { installLoreSkill, removeLoreSkill } from "./skills.js";
29
31
  import { runApproachCommand } from "./approach.js";
@@ -48,6 +50,8 @@ export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
48
50
  export const DEFAULT_HOSTED_API_URL = "https://api.uselore.co";
49
51
  export const DEFAULT_HOSTED_DASHBOARD_URL = "https://uselore.co";
50
52
  const CONFIGURED_AGENT_NAMES = NATIVE_CODING_AGENT_NAMES;
53
+ /** Agents that only need the saved credential: no hooks or agent config are written. */
54
+ const CREDENTIAL_ONLY_AGENTS = ["cowork", "devin"];
51
55
  const HOOK_EVENTS = [
52
56
  "UserPromptSubmit",
53
57
  "PreToolUse",
@@ -111,7 +115,7 @@ Options:
111
115
  --login Approve access securely in the Lore dashboard
112
116
  --no-open Print the approval URL without opening a browser
113
117
  --token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
114
- --agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code
118
+ --agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, opencode, polytoken, or t3code
115
119
  --timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
116
120
  --fallback <agent> claude, codex, opencode, devin, cursor, copilot-cli, polytoken
117
121
  --no-recovery Disable automatic handoff setup (otherwise enabled with two installed clients)
@@ -125,6 +129,7 @@ Folder choices control capture; learning scope controls where knowledge applies.
125
129
  Examples:
126
130
  lore connect --login --agent claude --folder .
127
131
  lore connect --login --agent cowork
132
+ lore connect --login --agent devin --folder .
128
133
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
129
134
  lore connect --url http://localhost:3004 --token dev-token --agent codex
130
135
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
@@ -216,9 +221,12 @@ function isConfiguredAgent(value) {
216
221
  function isConnectAgent(value) {
217
222
  return (isConfiguredAgent(value) ||
218
223
  value === "copilot" ||
219
- value === "cowork" ||
224
+ isCredentialOnlyAgent(value) ||
220
225
  value === "t3code");
221
226
  }
227
+ function isCredentialOnlyAgent(value) {
228
+ return CREDENTIAL_ONLY_AGENTS.includes(value);
229
+ }
222
230
  function hookCommand(agent, paths, event) {
223
231
  if (isWindows()) {
224
232
  const invocation = IS_STANDALONE_BINARY ? [process.execPath, "hook"] : [process.execPath, paths.runtime];
@@ -1392,7 +1400,7 @@ async function readT3CodeProviderInstallations(paths) {
1392
1400
  }
1393
1401
  async function expandConnectInstallations(requested, paths) {
1394
1402
  const direct = requested.flatMap((agent) => {
1395
- if (agent === "cowork" || agent === "t3code") {
1403
+ if (isCredentialOnlyAgent(agent) || agent === "t3code") {
1396
1404
  return [];
1397
1405
  }
1398
1406
  return [
@@ -1438,6 +1446,7 @@ async function installRuntime(paths) {
1438
1446
  rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
1439
1447
  rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
1440
1448
  rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
1449
+ rm(resolve(dirname(paths.runtime), "host-envelopes.js"), { force: true }),
1441
1450
  rm(resolve(dirname(paths.runtime), "approach-guidance.js"), { force: true }),
1442
1451
  rm(resolve(dirname(paths.runtime), "skill-guidance.js"), { force: true }),
1443
1452
  rm(paths.runtimeContextFallback, { force: true }),
@@ -1476,6 +1485,8 @@ async function installRuntime(paths) {
1476
1485
  "retrieval-task.js": (await readFile(resolve(sourceDirectory, "retrieval-task.js"), "utf8")).replaceAll("@lore-co/core/user-message", "./user-message.js"),
1477
1486
  "runtime-redaction.js": await readFile(resolve(sourceDirectory, "runtime-redaction.js"), "utf8"),
1478
1487
  "user-message.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/user-message")), "utf8"),
1488
+ // user-message.js imports the host-envelope registry it derives from.
1489
+ "host-envelopes.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/host-envelopes")), "utf8"),
1479
1490
  "approach-guidance.js": await readFile(resolve(sourceDirectory, "approach-guidance.js"), "utf8"),
1480
1491
  "skill-guidance.js": await readFile(resolve(sourceDirectory, "skill-guidance.js"), "utf8"),
1481
1492
  "repository.js": repository,
@@ -1508,6 +1519,7 @@ async function installRuntime(paths) {
1508
1519
  atomicWrite(resolve(dirname(paths.runtime), "retrieval-task.js"), runtimeFiles["retrieval-task.js"], 0o600),
1509
1520
  atomicWrite(resolve(dirname(paths.runtime), "runtime-redaction.js"), runtimeFiles["runtime-redaction.js"], 0o600),
1510
1521
  atomicWrite(resolve(dirname(paths.runtime), "user-message.js"), runtimeFiles["user-message.js"], 0o600),
1522
+ atomicWrite(resolve(dirname(paths.runtime), "host-envelopes.js"), runtimeFiles["host-envelopes.js"], 0o600),
1511
1523
  atomicWrite(resolve(dirname(paths.runtime), "approach-guidance.js"), runtimeFiles["approach-guidance.js"], 0o600),
1512
1524
  atomicWrite(resolve(dirname(paths.runtime), "skill-guidance.js"), runtimeFiles["skill-guidance.js"], 0o600),
1513
1525
  atomicWrite(paths.runtimeRepository, repository, 0o600),
@@ -1642,7 +1654,7 @@ function parseConnectArguments(args) {
1642
1654
  parsed.agents.push(value);
1643
1655
  }
1644
1656
  else {
1645
- throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code");
1657
+ throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, opencode, polytoken, or t3code");
1646
1658
  }
1647
1659
  }
1648
1660
  validateConnectProjectOptions(parsed);
@@ -1739,9 +1751,26 @@ async function connectWithCredentialLocked(options, identity) {
1739
1751
  const agents = [
1740
1752
  ...new Set(installations.map((installation) => installation.agent)),
1741
1753
  ].sort();
1742
- const credentialOnlyCowork = options.agents.includes("cowork");
1743
- if (agents.length === 0 && !credentialOnlyCowork) {
1744
- throw new Error("No Claude, Codex, Copilot CLI, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
1754
+ const installedAgents = [
1755
+ ...new Set([
1756
+ ...requestedExpansion.installations.map((installation) => installation.agent),
1757
+ ...detectedAgents,
1758
+ ...options.agents.filter((agent) => isCredentialOnlyAgent(agent) || agent === "t3code"),
1759
+ ]),
1760
+ ].sort();
1761
+ const keptAgents = [
1762
+ ...new Set(configuredInstallations(existing, paths).map((installation) => installation.agent)),
1763
+ ]
1764
+ .filter((agent) => !installedAgents.includes(agent))
1765
+ .sort();
1766
+ const knownDashboardUrl = dashboardUrl ??
1767
+ (existing?.apiUrl === apiUrl ? existing.dashboardUrl : undefined) ??
1768
+ (apiUrl === DEFAULT_HOSTED_API_URL ? DEFAULT_HOSTED_DASHBOARD_URL : undefined);
1769
+ // Cowork and the Devin CLI read the saved credential from ~/.lore/config.json
1770
+ // and need no hooks, so they connect without a native agent installation.
1771
+ const credentialOnlyAgents = CREDENTIAL_ONLY_AGENTS.filter((agent) => options.agents.includes(agent));
1772
+ if (agents.length === 0 && credentialOnlyAgents.length === 0) {
1773
+ 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.");
1745
1774
  }
1746
1775
  const now = new Date();
1747
1776
  const documents = new Map();
@@ -1830,17 +1859,39 @@ async function connectWithCredentialLocked(options, identity) {
1830
1859
  if (options.recovery === false)
1831
1860
  await disableRecovery(paths.home);
1832
1861
  const skills = await installLoreSkill(paths, installations);
1862
+ // An unchanged reconnect: same server, credential, and workspace, every
1863
+ // configured agent was already configured, and no agent file was rewritten.
1864
+ const alreadyConnected = existing !== null &&
1865
+ existing.apiUrl === apiUrl &&
1866
+ existing.token === token &&
1867
+ existing.workspaceId === identity.workspaceId &&
1868
+ existing.connectionId === config.connectionId &&
1869
+ agents.every((agent) => existing.agents.includes(agent)) &&
1870
+ changedHookFiles.length === 0 &&
1871
+ JSON.stringify(existing.directories ?? null) === JSON.stringify(config.directories ?? null);
1872
+ // A Codex set up in this run gets the approval step in its next-step line;
1873
+ // the warning covers a kept Codex whose hooks file was rewritten anyway.
1874
+ const codexHookPaths = installations
1875
+ .filter((installation) => installation.agent === "codex")
1876
+ .map((installation) => installation.configPath);
1877
+ const codexNeedsApproval = !installedAgents.includes("codex") &&
1878
+ changedHookFiles.some((path) => codexHookPaths.includes(path));
1833
1879
  return {
1834
1880
  capture: { folders: config.directories?.include ?? null, excludedFolders: config.directories?.exclude ?? [] },
1835
1881
  skillFiles: skills.files,
1836
1882
  connected: true,
1837
1883
  apiUrl: config.apiUrl,
1838
1884
  identity,
1839
- agents: credentialOnlyCowork ? [...agents, "cowork"] : agents,
1885
+ agents: [...agents, ...credentialOnlyAgents],
1886
+ installedAgents,
1887
+ keptAgents,
1888
+ alreadyConnected,
1889
+ ...(knownDashboardUrl === undefined ? {} : { dashboardUrl: knownDashboardUrl }),
1890
+ folder: options.cwd ?? process.cwd(),
1840
1891
  config: paths.config,
1841
1892
  changedHookFiles,
1842
1893
  backups,
1843
- 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, ...(agents.includes("codex")
1894
+ 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
1844
1895
  ? ["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."]
1845
1896
  : [])],
1846
1897
  ...(recoveryNotice ? { recoveryNotice } : {}),
@@ -1969,6 +2020,53 @@ async function acknowledgeDeviceAuthorization(apiUrl, authorization) {
1969
2020
  }
1970
2021
  }
1971
2022
  }
2023
+ /** The exact step each agent needs before Lore records its first turn. */
2024
+ export function connectNextStep(agent) {
2025
+ switch (agent) {
2026
+ case "claude":
2027
+ return "Restart Claude Code.";
2028
+ case "codex":
2029
+ return "Restart Codex, open /hooks, approve Lore's hooks, then start a fresh session.";
2030
+ case "cursor":
2031
+ return "Restart Cursor and enable the lore-native MCP server (lore_context and lore_complete).";
2032
+ case "opencode":
2033
+ return "Restart OpenCode.";
2034
+ case "copilot":
2035
+ case "copilot-cli":
2036
+ return "Restart Copilot CLI.";
2037
+ case "copilot-vscode":
2038
+ return "Restart the VS Code session.";
2039
+ case "polytoken":
2040
+ return "Restart Polytoken.";
2041
+ case "devin":
2042
+ return "Run lore devin cli setup, then open a new terminal.";
2043
+ case "cowork":
2044
+ case "t3code":
2045
+ return "Restart your agent and complete a turn.";
2046
+ }
2047
+ }
2048
+ export function formatConnectResult(result) {
2049
+ const captureSummary = result.capture.folders === null ? "all projects allowed by the workspace"
2050
+ : result.capture.folders.length ? result.capture.folders.join(", ") : "paused — choose folders with --folder <path>";
2051
+ const installed = result.installedAgents.join(", ");
2052
+ const lines = [
2053
+ result.alreadyConnected
2054
+ ? `Already connected to ${result.identity.workspaceName} for ${installed}; nothing changed.`
2055
+ : `access authorized; installed: ${installed}`,
2056
+ ...(result.keptAgents.length ? [`kept: ${result.keptAgents.join(", ")}`] : []),
2057
+ `api_url: ${result.apiUrl}`,
2058
+ `config: ${result.config}`,
2059
+ `capture: ${captureSummary}${result.capture.excludedFolders.length ? `; excluded: ${result.capture.excludedFolders.join(", ")}` : ""}`,
2060
+ ...result.installedAgents.map((agent) => `Next step for ${agent}: ${connectNextStep(agent)}`),
2061
+ result.dashboardUrl === undefined
2062
+ ? `Send one message in ${result.folder}, then run lore doctor to confirm recording.`
2063
+ : `Send one message in ${result.folder}, then open ${result.dashboardUrl}/connect to confirm recording.`,
2064
+ ...(result.skillFiles.length ? ["Choose lore in your agent’s skills to find knowledge or remember this session."] : []),
2065
+ ...(result.recoveryNotice ? [result.recoveryNotice] : []),
2066
+ ...result.warnings.map((warning) => `warning: ${warning}`),
2067
+ ];
2068
+ return `${lines.join("\n")}\n`;
2069
+ }
1972
2070
  async function connectCommand(args) {
1973
2071
  const parsed = parseConnectArguments(args);
1974
2072
  if (parsed === null) {
@@ -2020,9 +2118,7 @@ async function connectCommand(args) {
2020
2118
  });
2021
2119
  if (authorization)
2022
2120
  await acknowledgeDeviceAuthorization(apiUrl, authorization);
2023
- const captureSummary = result.capture.folders === null ? "all projects allowed by the workspace"
2024
- : result.capture.folders.length ? result.capture.folders.join(", ") : "paused — choose folders with --folder <path>";
2025
- writeResult(result, parsed.json, `access authorized; integrations installed: ${result.agents.join(", ")}\napi_url: ${result.apiUrl}\nconfig: ${result.config}\ncapture: ${captureSummary}${result.capture.excludedFolders.length ? `; excluded: ${result.capture.excludedFolders.join(", ")}` : ""}\nRestart your agent and complete a turn, then run lore doctor to verify recording.\n${result.skillFiles.length ? 'Choose lore in your agent’s skills to find knowledge or remember this session.\n' : ''}${result.recoveryNotice ? `${result.recoveryNotice}\n` : ""}${result.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
2121
+ writeResult(result, parsed.json, formatConnectResult(result));
2026
2122
  }
2027
2123
  async function queueCount(paths) {
2028
2124
  try {
@@ -2042,6 +2138,26 @@ export function credentialReliabilityWorkspaceKey(apiUrl, token) {
2042
2138
  .digest("hex")
2043
2139
  .slice(0, 32)}`;
2044
2140
  }
2141
+ /** Unrecoverable dead letters are kept this long for diagnosis, then pruned by doctor. */
2142
+ export const DEAD_LETTER_RETENTION_MS = 14 * 24 * 60 * 60_000;
2143
+ async function pruneUnrecoverableDeadLetters(config, paths) {
2144
+ if (config === null) {
2145
+ return 0;
2146
+ }
2147
+ try {
2148
+ const store = new ReliabilityStore(reliabilityWorkspaceKey(config), {
2149
+ home: paths.home,
2150
+ ...(config.connectionId ? { connectionId: config.connectionId } : {}),
2151
+ });
2152
+ return await store.pruneDeadLetters({
2153
+ olderThan: new Date(Date.now() - DEAD_LETTER_RETENTION_MS),
2154
+ });
2155
+ }
2156
+ catch {
2157
+ // The reliability-store check reports an unusable store.
2158
+ return 0;
2159
+ }
2160
+ }
2045
2161
  async function inspectReliabilityStore(config, paths) {
2046
2162
  if (config === null) {
2047
2163
  return { inspection: null, error: null };
@@ -2114,6 +2230,7 @@ async function inspectRuntimeInstallation(required, paths) {
2114
2230
  "retrieval-task.js": resolve(dirname(paths.runtime), "retrieval-task.js"),
2115
2231
  "runtime-redaction.js": resolve(dirname(paths.runtime), "runtime-redaction.js"),
2116
2232
  "user-message.js": resolve(dirname(paths.runtime), "user-message.js"),
2233
+ "host-envelopes.js": resolve(dirname(paths.runtime), "host-envelopes.js"),
2117
2234
  "approach-guidance.js": resolve(dirname(paths.runtime), "approach-guidance.js"),
2118
2235
  "skill-guidance.js": resolve(dirname(paths.runtime), "skill-guidance.js"),
2119
2236
  "repository.js": paths.runtimeRepository,
@@ -2347,6 +2464,19 @@ async function statusData(paths, cwd) {
2347
2464
  externalSurfaces: EXTERNAL_SURFACES,
2348
2465
  };
2349
2466
  }
2467
+ /**
2468
+ * Per-reason breakdown of local context-retrieval failures and the most
2469
+ * recent one. Reasons are a fixed runtime vocabulary (timeout, http_<status>,
2470
+ * network, snapshot_unverified, binding_mismatch, ...) and never prompt text.
2471
+ */
2472
+ function formatRetrievalFailureDiagnostics(retrieval) {
2473
+ const reasons = Object.entries(retrieval?.failureReasons ?? {})
2474
+ .sort(([reasonA, countA], [reasonB, countB]) => countB - countA || reasonA.localeCompare(reasonB))
2475
+ .map(([reason, count]) => `${reason}=${count}`);
2476
+ const last = retrieval?.lastFailure ?? null;
2477
+ return (`local_retrieval_failure_reasons: ${reasons.length === 0 ? "-" : reasons.join(" ")}\n` +
2478
+ `last_retrieval_failure: ${last === null ? "-" : `${last.reason} (${last.path} request, agent ${last.agent}, at ${last.at})`}\n`);
2479
+ }
2350
2480
  async function statusCommand(args) {
2351
2481
  const parsed = parseOutputArguments(args, STATUS_HELP, "status");
2352
2482
  if (parsed === null) {
@@ -2364,7 +2494,8 @@ async function statusCommand(args) {
2364
2494
  const metricRate = (value) => value === null || value === undefined
2365
2495
  ? "-"
2366
2496
  : `${(value * 100).toFixed(1)}%`;
2367
- 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${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
2497
+ const retrievalFailureLines = formatRetrievalFailureDiagnostics(data.reliability?.operational.retrieval);
2498
+ 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`);
2368
2499
  }
2369
2500
  async function disconnectCommand(args) {
2370
2501
  const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
@@ -2430,6 +2561,7 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
2430
2561
  rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
2431
2562
  rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
2432
2563
  rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
2564
+ rm(resolve(dirname(paths.runtime), "host-envelopes.js"), { force: true }),
2433
2565
  rm(resolve(dirname(paths.runtime), "skill-guidance.js"), { force: true }),
2434
2566
  rm(paths.runtimeInvocationHealthWriter, { force: true }),
2435
2567
  rm(paths.runtimeVersion, { force: true }),
@@ -2441,6 +2573,59 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
2441
2573
  ]);
2442
2574
  return { connected: false, changedHookFiles, backups };
2443
2575
  }
2576
+ /**
2577
+ * Server-side queue health. Dead embedding jobs name the operator fix: once the
2578
+ * provider is repaired, `lore embeddings repair` retries them without waiting
2579
+ * for the reconciler's backoff.
2580
+ */
2581
+ export function operationalMetricsCheck(metrics) {
2582
+ const captureBacklog = metrics.captureReplay.captureExtraction.pending +
2583
+ metrics.captureReplay.captureExtraction.running;
2584
+ const embeddingBacklog = metrics.captureReplay.memoryEmbedding.pending +
2585
+ metrics.captureReplay.memoryEmbedding.running;
2586
+ const embeddingDeadLetters = metrics.captureReplay.memoryEmbedding.dead;
2587
+ const deadLetters = metrics.captureReplay.captureExtraction.dead + embeddingDeadLetters;
2588
+ const detectedFailures = metrics.silentFailures.count +
2589
+ metrics.guard.failures +
2590
+ deadLetters;
2591
+ return {
2592
+ name: "server-operational-metrics",
2593
+ status: metrics.silentFailures.count > 0 || deadLetters > 0
2594
+ ? "error"
2595
+ : metrics.guard.failures > 0 ||
2596
+ captureBacklog > 0 ||
2597
+ embeddingBacklog > 0
2598
+ ? "warning"
2599
+ : "ok",
2600
+ detail: `${detectedFailures} detected failure(s), ${captureBacklog} capture and ${embeddingBacklog} embedding job(s) in backlog; ${metrics.guard.latency.samples} Guard latency sample(s)` +
2601
+ (embeddingDeadLetters > 0
2602
+ ? `; ${embeddingDeadLetters} dead embedding job(s): fix the embedding provider, then run lore embeddings repair`
2603
+ : ""),
2604
+ };
2605
+ }
2606
+ /**
2607
+ * Local outbox health. Dead letters that can never be replayed (an old snapshot
2608
+ * format or a permanent rejection) are a note, not an error: replaying them
2609
+ * cannot help, and doctor prunes them after the retention window.
2610
+ */
2611
+ export function durableOutboxCheck(outbox, pruned = 0) {
2612
+ const unexplained = outbox.dead - outbox.unrecoverable;
2613
+ const retentionDays = Math.round(DEAD_LETTER_RETENTION_MS / (24 * 60 * 60_000));
2614
+ return {
2615
+ name: "durable-capture-outbox",
2616
+ status: outbox.authBlocked > 0 || unexplained > 0
2617
+ ? "error"
2618
+ : outbox.ready > 0 || outbox.inFlight > 0 || outbox.unrecoverable > 0
2619
+ ? "warning"
2620
+ : "ok",
2621
+ detail: `${outbox.total} total; ${outbox.ready} ready, ${outbox.authBlocked} authentication-blocked, ${outbox.dead} dead-letter` +
2622
+ (outbox.unrecoverable > 0
2623
+ ? `; note: ${outbox.unrecoverable} dead letter(s) cannot be replayed (old snapshot format or permanent rejection) and are pruned after ${retentionDays} days`
2624
+ : "") +
2625
+ (pruned > 0 ? `; pruned ${pruned} unrecoverable dead letter(s) older than ${retentionDays} days` : "") +
2626
+ (outbox.oldestFailure ? `; oldest failure (${outbox.oldestFailure.attempts} attempts): ${outbox.oldestFailure.message}` : ""),
2627
+ };
2628
+ }
2444
2629
  async function apiChecks(config) {
2445
2630
  const readinessUrl = `${config.apiUrl}/health/ready`;
2446
2631
  let readiness;
@@ -2505,27 +2690,7 @@ async function apiChecks(config) {
2505
2690
  if (!response.ok) {
2506
2691
  throw new Error("Operational metrics request failed");
2507
2692
  }
2508
- const metrics = OperationalMetricsResponseSchema.parse(await response.json());
2509
- const captureBacklog = metrics.captureReplay.captureExtraction.pending +
2510
- metrics.captureReplay.captureExtraction.running;
2511
- const embeddingBacklog = metrics.captureReplay.memoryEmbedding.pending +
2512
- metrics.captureReplay.memoryEmbedding.running;
2513
- const deadLetters = metrics.captureReplay.captureExtraction.dead +
2514
- metrics.captureReplay.memoryEmbedding.dead;
2515
- const detectedFailures = metrics.silentFailures.count +
2516
- metrics.guard.failures +
2517
- deadLetters;
2518
- checks.push({
2519
- name: "server-operational-metrics",
2520
- status: metrics.silentFailures.count > 0 || deadLetters > 0
2521
- ? "error"
2522
- : metrics.guard.failures > 0 ||
2523
- captureBacklog > 0 ||
2524
- embeddingBacklog > 0
2525
- ? "warning"
2526
- : "ok",
2527
- detail: `${detectedFailures} detected failure(s), ${captureBacklog} capture and ${embeddingBacklog} embedding job(s) in backlog; ${metrics.guard.latency.samples} Guard latency sample(s)`,
2528
- });
2693
+ checks.push(operationalMetricsCheck(OperationalMetricsResponseSchema.parse(await response.json())));
2529
2694
  }
2530
2695
  catch {
2531
2696
  checks.push({
@@ -2556,6 +2721,7 @@ async function doctorCommand(args) {
2556
2721
  }
2557
2722
  const paths = getLorePaths();
2558
2723
  const config = await diagnosticConfig(paths, process.cwd());
2724
+ const prunedDeadLetters = await pruneUnrecoverableDeadLetters(config, paths);
2559
2725
  const status = await statusData(paths, process.cwd());
2560
2726
  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.` }];
2561
2727
  checks.push({
@@ -2658,18 +2824,7 @@ async function doctorCommand(args) {
2658
2824
  "not initialized",
2659
2825
  });
2660
2826
  if (status.reliability !== null) {
2661
- checks.push({
2662
- name: "durable-capture-outbox",
2663
- status: status.reliability.outbox.authBlocked > 0 ||
2664
- status.reliability.outbox.dead > 0
2665
- ? "error"
2666
- : status.reliability.outbox.ready > 0 ||
2667
- status.reliability.outbox.inFlight > 0
2668
- ? "warning"
2669
- : "ok",
2670
- detail: `${status.reliability.outbox.total} total; ${status.reliability.outbox.ready} ready, ${status.reliability.outbox.authBlocked} authentication-blocked, ${status.reliability.outbox.dead} dead-letter` +
2671
- (status.reliability.outbox.oldestFailure ? `; oldest failure (${status.reliability.outbox.oldestFailure.attempts} attempts): ${status.reliability.outbox.oldestFailure.message}` : ""),
2672
- });
2827
+ checks.push(durableOutboxCheck(status.reliability.outbox, prunedDeadLetters));
2673
2828
  checks.push({
2674
2829
  name: "verified-snapshot-cache",
2675
2830
  status: status.reliability.trustKeys > 0 ? "ok" : "warning",
@@ -2682,6 +2837,16 @@ async function doctorCommand(args) {
2682
2837
  status: detectedFailures > 0 ? "warning" : "ok",
2683
2838
  detail: `${detectedFailures} detected failure(s); Guard coverage ${operational.guard.coverageRate === null ? "not yet sampled" : `${(operational.guard.coverageRate * 100).toFixed(1)}%`}, context fallback ${operational.retrieval.fallbackRate === null ? "not yet sampled" : `${(operational.retrieval.fallbackRate * 100).toFixed(1)}%`}`,
2684
2839
  });
2840
+ const retrievalReasons = Object.entries(operational.retrieval.failureReasons)
2841
+ .sort(([reasonA, countA], [reasonB, countB]) => countB - countA || reasonA.localeCompare(reasonB));
2842
+ const lastRetrievalFailure = operational.retrieval.lastFailure;
2843
+ checks.push({
2844
+ name: "context-retrieval-failures",
2845
+ status: retrievalReasons.length > 0 ? "warning" : "ok",
2846
+ detail: retrievalReasons.length === 0
2847
+ ? "no live retrieval failures recorded"
2848
+ : `${retrievalReasons.map(([reason, count]) => `${reason}=${count}`).join(", ")}; last ${lastRetrievalFailure === null ? "unknown" : `${lastRetrievalFailure.reason} on ${lastRetrievalFailure.path} request (${lastRetrievalFailure.agent}) at ${lastRetrievalFailure.at}`}`,
2849
+ });
2685
2850
  }
2686
2851
  const incompleteInvocations = status.agents.filter((agent) => agent.configured && agent.invocation.state === "incomplete").length;
2687
2852
  const failedInvocations = status.agents.filter((agent) => agent.configured && agent.invocation.state === "failed").length;
@@ -2727,6 +2892,9 @@ async function printCommandHelp(path) {
2727
2892
  case "doctor":
2728
2893
  process.stdout.write(DOCTOR_HELP);
2729
2894
  return;
2895
+ case "embeddings":
2896
+ await runEmbeddingsCommand(args, null);
2897
+ return;
2730
2898
  case "disconnect":
2731
2899
  process.stdout.write(DISCONNECT_HELP);
2732
2900
  return;
@@ -2739,6 +2907,9 @@ async function printCommandHelp(path) {
2739
2907
  case "ask":
2740
2908
  await runAskCommand(args, null);
2741
2909
  return;
2910
+ case "learnings":
2911
+ await runLearningsCommand(args, null);
2912
+ return;
2742
2913
  case "remember":
2743
2914
  await runRememberCommand(args, null);
2744
2915
  return;
@@ -2823,7 +2994,7 @@ async function executeCli(args = process.argv.slice(2)) {
2823
2994
  const helpIndex = args.findIndex((value, index) => index > 0 && (separator < 0 || index < separator) && (value === "--help" || value === "-h"));
2824
2995
  if (helpIndex > 0) {
2825
2996
  const subcommand = args[1];
2826
- const grouped = ["connect", "config", "guard", "recovery", "github", "devin", "host", "self-host", "scope"].includes(command ?? "");
2997
+ const grouped = ["connect", "config", "embeddings", "guard", "recovery", "github", "devin", "host", "self-host", "scope"].includes(command ?? "");
2827
2998
  await printCommandHelp([command, ...(grouped && subcommand && !subcommand.startsWith("-") ? [subcommand] : [])]);
2828
2999
  return;
2829
3000
  }
@@ -2836,7 +3007,7 @@ async function executeCli(args = process.argv.slice(2)) {
2836
3007
  return;
2837
3008
  }
2838
3009
  const commandArgs = args.slice(1);
2839
- if (["ask", "remember", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
3010
+ if (["ask", "learnings", "remember", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
2840
3011
  const decision = await checkDirectory(process.cwd(), getLorePaths(["approach", "skill"].includes(command) ? process.env.LORE_HOME : undefined).home);
2841
3012
  if (!decision.allowed)
2842
3013
  throw new Error(`Lore is off in this folder. ${decision.reason}`);
@@ -2876,6 +3047,9 @@ async function executeCli(args = process.argv.slice(2)) {
2876
3047
  case "ask":
2877
3048
  await runAskCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
2878
3049
  return;
3050
+ case "learnings":
3051
+ await runLearningsCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
3052
+ return;
2879
3053
  case "remember":
2880
3054
  await runRememberCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
2881
3055
  return;
@@ -2940,6 +3114,9 @@ async function executeCli(args = process.argv.slice(2)) {
2940
3114
  case "doctor":
2941
3115
  await doctorCommand(commandArgs);
2942
3116
  return;
3117
+ case "embeddings":
3118
+ await runEmbeddingsCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
3119
+ return;
2943
3120
  case "self-host":
2944
3121
  await runSelfHostCommand(commandArgs, process.env, {
2945
3122
  connect: async (request) => {