@lore-co/cli 0.2.20 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/approach-guidance.d.ts.map +1 -1
- package/dist/approach-guidance.js +1 -0
- package/dist/approach-guidance.js.map +1 -1
- package/dist/approach.d.ts +1 -1
- package/dist/approach.d.ts.map +1 -1
- package/dist/approach.js +11 -3
- package/dist/approach.js.map +1 -1
- package/dist/ask.d.ts +9 -1
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +32 -3
- package/dist/ask.js.map +1 -1
- package/dist/cli.d.ts +36 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +225 -48
- package/dist/cli.js.map +1 -1
- package/dist/command-help.d.ts +3 -1
- package/dist/command-help.d.ts.map +1 -1
- package/dist/command-help.js +13 -1
- package/dist/command-help.js.map +1 -1
- package/dist/devin-cli.js +2 -2
- package/dist/devin-cli.js.map +1 -1
- package/dist/embeddings.d.ts +12 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +105 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/generated-assets.d.ts +5 -4
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +5 -4
- package/dist/generated-assets.js.map +1 -1
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +242 -4
- package/dist/github.js.map +1 -1
- package/dist/guard.js +2 -2
- package/dist/guard.js.map +1 -1
- package/dist/handoff.js +2 -2
- package/dist/handoff.js.map +1 -1
- package/dist/learnings.d.ts +31 -0
- package/dist/learnings.d.ts.map +1 -0
- package/dist/learnings.js +265 -0
- package/dist/learnings.js.map +1 -0
- package/dist/platform.d.ts +7 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +25 -1
- package/dist/platform.js.map +1 -1
- package/dist/reliability-store.d.ts +31 -0
- package/dist/reliability-store.d.ts.map +1 -1
- package/dist/reliability-store.js +89 -0
- package/dist/reliability-store.js.map +1 -1
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +119 -28
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +2 -2
- package/dist/self-host.d.ts.map +1 -1
- package/dist/self-host.js +4 -3
- package/dist/self-host.js.map +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +24 -4
- package/dist/skills.js.map +1 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/dist/uninstall.js +4 -3
- package/dist/uninstall.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +4 -23
- package/dist/update.js.map +1 -1
- 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
|
|
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
|
|
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
|
|
1743
|
-
|
|
1744
|
-
|
|
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:
|
|
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, ...(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|