@lore-co/cli 0.2.26 → 0.2.28
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 +67 -20
- package/dist/agents.d.ts +9 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +19 -6
- package/dist/agents.js.map +1 -1
- package/dist/api.d.ts +2 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +15 -2
- package/dist/api.js.map +1 -1
- package/dist/approach-guidance.d.ts +8 -1
- package/dist/approach-guidance.d.ts.map +1 -1
- package/dist/approach-guidance.js +13 -3
- package/dist/approach-guidance.js.map +1 -1
- package/dist/approach.d.ts.map +1 -1
- package/dist/approach.js +2 -2
- package/dist/approach.js.map +1 -1
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +2 -2
- package/dist/ask.js.map +1 -1
- package/dist/capture-repair.d.ts +11 -0
- package/dist/capture-repair.d.ts.map +1 -0
- package/dist/capture-repair.js +94 -0
- package/dist/capture-repair.js.map +1 -0
- package/dist/capture-scope.d.ts.map +1 -1
- package/dist/capture-scope.js +2 -1
- package/dist/capture-scope.js.map +1 -1
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/chat.js.map +1 -1
- package/dist/cli.d.ts +114 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +628 -127
- package/dist/cli.js.map +1 -1
- package/dist/codex-hook-approval.d.ts.map +1 -1
- package/dist/codex-hook-approval.js +9 -2
- package/dist/codex-hook-approval.js.map +1 -1
- package/dist/command-help.d.ts +3 -2
- package/dist/command-help.d.ts.map +1 -1
- package/dist/command-help.js +11 -5
- package/dist/command-help.js.map +1 -1
- package/dist/config-command.js +2 -2
- package/dist/config-command.js.map +1 -1
- package/dist/connect-project.d.ts.map +1 -1
- package/dist/connect-project.js +2 -1
- package/dist/connect-project.js.map +1 -1
- package/dist/console-keys.d.ts +11 -0
- package/dist/console-keys.d.ts.map +1 -0
- package/dist/console-keys.js +24 -0
- package/dist/console-keys.js.map +1 -0
- package/dist/cursor-mcp.d.ts.map +1 -1
- package/dist/cursor-mcp.js +4 -0
- package/dist/cursor-mcp.js.map +1 -1
- package/dist/demo-process.js +3 -3
- package/dist/demo-process.js.map +1 -1
- package/dist/devin-capture.d.ts.map +1 -1
- package/dist/devin-capture.js +2 -2
- package/dist/devin-capture.js.map +1 -1
- package/dist/devin-cli.d.ts.map +1 -1
- package/dist/devin-cli.js +23 -13
- package/dist/devin-cli.js.map +1 -1
- package/dist/devin-client.d.ts +3 -0
- package/dist/devin-client.d.ts.map +1 -1
- package/dist/devin-client.js +5 -0
- package/dist/devin-client.js.map +1 -1
- package/dist/devin.d.ts.map +1 -1
- package/dist/devin.js +29 -9
- package/dist/devin.js.map +1 -1
- package/dist/drafts.d.ts +8 -0
- package/dist/drafts.d.ts.map +1 -0
- package/dist/drafts.js +173 -0
- package/dist/drafts.js.map +1 -0
- package/dist/generated-assets.d.ts +9 -9
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +9 -9
- package/dist/generated-assets.js.map +1 -1
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +46 -14
- package/dist/github.js.map +1 -1
- package/dist/guard.d.ts +1 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +42 -14
- package/dist/guard.js.map +1 -1
- package/dist/handoff.js +2 -2
- package/dist/handoff.js.map +1 -1
- package/dist/host.js +4 -4
- package/dist/host.js.map +1 -1
- package/dist/image-capture.d.ts.map +1 -1
- package/dist/image-capture.js +5 -4
- package/dist/image-capture.js.map +1 -1
- package/dist/learnings.d.ts.map +1 -1
- package/dist/learnings.js +3 -3
- package/dist/learnings.js.map +1 -1
- package/dist/live.d.ts +7 -0
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +11 -6
- package/dist/live.js.map +1 -1
- package/dist/lore-agent.d.ts +6 -1
- package/dist/lore-agent.d.ts.map +1 -1
- package/dist/lore-agent.js +29 -7
- package/dist/lore-agent.js.map +1 -1
- package/dist/mcp-config.d.ts +7 -0
- package/dist/mcp-config.d.ts.map +1 -1
- package/dist/mcp-config.js +19 -3
- package/dist/mcp-config.js.map +1 -1
- package/dist/npm-installation.js +2 -2
- package/dist/npm-installation.js.map +1 -1
- package/dist/owned-files.d.ts +14 -4
- package/dist/owned-files.d.ts.map +1 -1
- package/dist/owned-files.js +24 -9
- package/dist/owned-files.js.map +1 -1
- package/dist/platform.d.ts +26 -3
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +87 -20
- package/dist/platform.js.map +1 -1
- package/dist/pty.d.ts +2 -1
- package/dist/pty.d.ts.map +1 -1
- package/dist/pty.js +20 -7
- package/dist/pty.js.map +1 -1
- package/dist/recovery-cloud-receipt.d.ts.map +1 -1
- package/dist/recovery-cloud-receipt.js +2 -1
- package/dist/recovery-cloud-receipt.js.map +1 -1
- package/dist/recovery-cloud-workspace.d.ts.map +1 -1
- package/dist/recovery-cloud-workspace.js +2 -1
- package/dist/recovery-cloud-workspace.js.map +1 -1
- package/dist/recovery-cloud.d.ts.map +1 -1
- package/dist/recovery-cloud.js +8 -9
- package/dist/recovery-cloud.js.map +1 -1
- package/dist/recovery-copilot.d.ts.map +1 -1
- package/dist/recovery-copilot.js +3 -2
- package/dist/recovery-copilot.js.map +1 -1
- package/dist/recovery-desktop.d.ts.map +1 -1
- package/dist/recovery-desktop.js +3 -2
- package/dist/recovery-desktop.js.map +1 -1
- package/dist/recovery-hooks.js +2 -2
- package/dist/recovery-hooks.js.map +1 -1
- package/dist/recovery-hosts.d.ts.map +1 -1
- package/dist/recovery-hosts.js +53 -6
- package/dist/recovery-hosts.js.map +1 -1
- package/dist/recovery-polytoken.d.ts +2 -2
- package/dist/recovery-polytoken.d.ts.map +1 -1
- package/dist/recovery-polytoken.js +13 -8
- package/dist/recovery-polytoken.js.map +1 -1
- package/dist/recovery-remote.js +3 -3
- package/dist/recovery-remote.js.map +1 -1
- package/dist/recovery-state.d.ts +22 -1
- package/dist/recovery-state.d.ts.map +1 -1
- package/dist/recovery-state.js +167 -31
- package/dist/recovery-state.js.map +1 -1
- package/dist/recovery-transfer.d.ts.map +1 -1
- package/dist/recovery-transfer.js +8 -4
- package/dist/recovery-transfer.js.map +1 -1
- package/dist/recovery.d.ts +5 -2
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +37 -14
- package/dist/recovery.js.map +1 -1
- package/dist/reliability-store.d.ts +13 -0
- package/dist/reliability-store.d.ts.map +1 -1
- package/dist/reliability-store.js +110 -8
- package/dist/reliability-store.js.map +1 -1
- package/dist/remember.d.ts.map +1 -1
- package/dist/remember.js +32 -3
- package/dist/remember.js.map +1 -1
- package/dist/retained-context.d.ts.map +1 -1
- package/dist/retained-context.js +3 -2
- package/dist/retained-context.js.map +1 -1
- package/dist/retrieval-task.d.ts +30 -0
- package/dist/retrieval-task.d.ts.map +1 -1
- package/dist/retrieval-task.js +98 -2
- package/dist/retrieval-task.js.map +1 -1
- package/dist/review.d.ts +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -7
- package/dist/review.js.map +1 -1
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +147 -97
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +1 -1
- package/dist/self-host.d.ts.map +1 -1
- package/dist/self-host.js +105 -23
- package/dist/self-host.js.map +1 -1
- package/dist/share.d.ts.map +1 -1
- package/dist/share.js +2 -1
- package/dist/share.js.map +1 -1
- package/dist/signed-snapshot.d.ts +7 -0
- package/dist/signed-snapshot.d.ts.map +1 -1
- package/dist/signed-snapshot.js +27 -0
- package/dist/signed-snapshot.js.map +1 -1
- package/dist/skill-command.d.ts +5 -2
- package/dist/skill-command.d.ts.map +1 -1
- package/dist/skill-command.js +73 -10
- package/dist/skill-command.js.map +1 -1
- package/dist/skill-guidance.js +1 -1
- package/dist/skill-guidance.js.map +1 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +9 -7
- package/dist/skills.js.map +1 -1
- package/dist/status-health.d.ts +26 -0
- package/dist/status-health.d.ts.map +1 -0
- package/dist/status-health.js +49 -0
- package/dist/status-health.js.map +1 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +2 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/terminal-output.d.ts +76 -0
- package/dist/terminal-output.d.ts.map +1 -0
- package/dist/terminal-output.js +151 -0
- package/dist/terminal-output.js.map +1 -0
- package/dist/uninstall.d.ts.map +1 -1
- package/dist/uninstall.js +3 -1
- package/dist/uninstall.js.map +1 -1
- package/dist/update.d.ts +3 -0
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +40 -14
- package/dist/update.js.map +1 -1
- package/dist/windows-permissions.d.ts +6 -0
- package/dist/windows-permissions.d.ts.map +1 -1
- package/dist/windows-permissions.js +22 -2
- package/dist/windows-permissions.js.map +1 -1
- package/dist/windows-pty.d.ts.map +1 -1
- package/dist/windows-pty.js +4 -3
- package/dist/windows-pty.js.map +1 -1
- 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,
|
|
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";
|
|
@@ -29,10 +30,13 @@ import { runChatCommand } from "./chat.js";
|
|
|
29
30
|
import { runLearningsCommand } from "./learnings.js";
|
|
30
31
|
import { runReviewCommand } from "./review.js";
|
|
31
32
|
import { runEmbeddingsCommand } from "./embeddings.js";
|
|
33
|
+
import { CAPTURE_REPAIR_HELP, runCaptureRepairCommand } from "./capture-repair.js";
|
|
32
34
|
import { runRememberCommand } from "./remember.js";
|
|
33
|
-
import {
|
|
35
|
+
import { runDraftsCommand } from "./drafts.js";
|
|
36
|
+
import { installLoreSkill, loreSkillFiles, recapSkillFiles, removeLoreSkill } from "./skills.js";
|
|
34
37
|
import { GENERIC_AGENT, genericInstructions, genericMcpServers, parseGenericAgentId, readGenericAgents, recordGenericAgent, removeGenericAgents } from "./generic-mcp.js";
|
|
35
|
-
import { installLoreAgent, removeLoreAgent } from "./lore-agent.js";
|
|
38
|
+
import { installLoreAgent, loreAgentFiles, removeLoreAgent } from "./lore-agent.js";
|
|
39
|
+
import { regularPath } from "./owned-files.js";
|
|
36
40
|
import { runApproachCommand } from "./approach.js";
|
|
37
41
|
import { runSkillCommand } from "./skill-command.js";
|
|
38
42
|
import { runGuardCommand } from "./guard.js";
|
|
@@ -46,10 +50,12 @@ import { runDevinCommand } from "./devin.js";
|
|
|
46
50
|
import { runHostCommand } from "./host.js";
|
|
47
51
|
import { runDemoCommand } from "./demo.js";
|
|
48
52
|
import { updateCommand } from "./update.js";
|
|
53
|
+
import { Output, PLAIN_OUTPUT, detectOutputStyle, listText } from "./terminal-output.js";
|
|
49
54
|
import { UNINSTALL_HELP, uninstallCommand } from "./uninstall.js";
|
|
50
55
|
import { runSelfHostCommand } from "./self-host.js";
|
|
51
56
|
import { ReliabilityStore, ReliabilityStoreError, integrationInvocationStateKey, } from "./reliability-store.js";
|
|
52
57
|
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
58
|
+
import { openNoFollow } from "@lore-co/core/no-follow";
|
|
53
59
|
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
54
60
|
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
55
61
|
export const DEFAULT_HOSTED_API_URL = "https://api.uselore.co";
|
|
@@ -141,7 +147,8 @@ Options:
|
|
|
141
147
|
--write With --agent generic: let the agent save through MCP (saves stay proposals)
|
|
142
148
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
143
149
|
--fallback <agent> claude, codex, opencode, devin, cursor, copilot-cli, polytoken
|
|
144
|
-
--no-recovery Disable automatic handoff setup (otherwise
|
|
150
|
+
--no-recovery Disable automatic handoff setup (otherwise set up between the agents you connect)
|
|
151
|
+
--verbose Also list the files written and the connection details
|
|
145
152
|
--json Print machine-readable output
|
|
146
153
|
--help Show this command's help
|
|
147
154
|
|
|
@@ -161,14 +168,18 @@ Examples:
|
|
|
161
168
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
|
|
162
169
|
`;
|
|
163
170
|
const STATUS_HELP = `lore status
|
|
164
|
-
Show
|
|
171
|
+
Show whether Lore is connected, whether it records in this folder, and which agents have used it.
|
|
165
172
|
|
|
166
173
|
Usage:
|
|
167
|
-
lore status [--json]
|
|
174
|
+
lore status [--verbose] [--json]
|
|
175
|
+
|
|
176
|
+
Options:
|
|
177
|
+
--verbose Also show installation, capture queue, cache, and Guard details
|
|
178
|
+
--json Print every detail as JSON
|
|
168
179
|
|
|
169
180
|
Examples:
|
|
170
181
|
lore status
|
|
171
|
-
lore status --
|
|
182
|
+
lore status --verbose
|
|
172
183
|
`;
|
|
173
184
|
const DOCTOR_HELP = `lore doctor
|
|
174
185
|
Check local security, exact wiring, invocation health, binaries, and Lore health.
|
|
@@ -187,16 +198,26 @@ Usage:
|
|
|
187
198
|
lore capture status [--json]
|
|
188
199
|
lore capture off [--json]
|
|
189
200
|
lore capture on [--json]
|
|
201
|
+
lore capture reminder off|on [--json]
|
|
202
|
+
lore capture repair [--json]
|
|
190
203
|
|
|
191
204
|
With capture off, Lore installs no Stop or SessionEnd hooks and never sends a
|
|
192
205
|
prompt or response as a turn. Context injection, Guard, lore ask, lore remember,
|
|
193
206
|
and the lore skill's "remember this session" keep working. Folder rules still
|
|
194
207
|
decide where Lore runs at all.
|
|
195
208
|
|
|
209
|
+
While nothing is recorded, Lore asks your agent to suggest /recap once in a
|
|
210
|
+
session where you confirm a decision. lore capture reminder off stops that.
|
|
211
|
+
|
|
212
|
+
lore capture repair retries captures the server dead-lettered while its
|
|
213
|
+
extraction provider was down. See lore help capture repair.
|
|
214
|
+
|
|
196
215
|
Examples:
|
|
197
216
|
lore capture off
|
|
198
217
|
lore capture status
|
|
218
|
+
lore capture reminder off
|
|
199
219
|
lore capture on
|
|
220
|
+
lore capture repair
|
|
200
221
|
`;
|
|
201
222
|
const DISCONNECT_HELP = `lore disconnect
|
|
202
223
|
Remove Lore-owned hooks/plugin, credentials, runtime, and transient state.
|
|
@@ -317,17 +338,25 @@ function stripLoreFromEvent(value) {
|
|
|
317
338
|
}
|
|
318
339
|
return groups;
|
|
319
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Seconds a host waits for a Lore hook before stopping it. On Windows each
|
|
343
|
+
* hook starts PowerShell and lore.exe, and a reply hook took 3.6 s on
|
|
344
|
+
* windows-latest, so the 2 and 3 second limits would drop the capture there.
|
|
345
|
+
*/
|
|
346
|
+
function hookTimeout(seconds) {
|
|
347
|
+
return isWindows() ? Math.max(seconds, 10) : seconds;
|
|
348
|
+
}
|
|
320
349
|
function eventHandler(agent, event, paths) {
|
|
321
350
|
return {
|
|
322
351
|
type: "command",
|
|
323
352
|
command: hookCommand(agent, paths),
|
|
324
|
-
timeout: event === "SessionEnd"
|
|
353
|
+
timeout: hookTimeout(event === "SessionEnd"
|
|
325
354
|
? 2
|
|
326
355
|
: event === "Stop"
|
|
327
356
|
? 3
|
|
328
357
|
: event === "PreToolUse"
|
|
329
358
|
? 10
|
|
330
|
-
: 25,
|
|
359
|
+
: 25),
|
|
331
360
|
...(event === "UserPromptSubmit"
|
|
332
361
|
? { statusMessage: "Loading Lore context" }
|
|
333
362
|
: {}),
|
|
@@ -410,13 +439,13 @@ function cursorEventHandler(event, paths) {
|
|
|
410
439
|
return {
|
|
411
440
|
type: "command",
|
|
412
441
|
command: hookCommand("cursor", paths),
|
|
413
|
-
timeout: event === "sessionEnd"
|
|
442
|
+
timeout: hookTimeout(event === "sessionEnd"
|
|
414
443
|
? 2
|
|
415
444
|
: event === "afterAgentResponse"
|
|
416
445
|
? 3
|
|
417
446
|
: event === "beforeShellExecution"
|
|
418
447
|
? 10
|
|
419
|
-
: 25,
|
|
448
|
+
: 25),
|
|
420
449
|
};
|
|
421
450
|
}
|
|
422
451
|
const CURSOR_MCP_NAME = "lore-native";
|
|
@@ -424,7 +453,7 @@ function cursorMcpServer(paths) {
|
|
|
424
453
|
return {
|
|
425
454
|
type: "stdio", command: process.execPath,
|
|
426
455
|
args: [...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)]), "cursor-mcp", "--owner", "lore"],
|
|
427
|
-
env: { LORE_HOME: paths.home },
|
|
456
|
+
env: { LORE_HOME: paths.home, ...CURSOR_LORE_DISABLE_ENV },
|
|
428
457
|
};
|
|
429
458
|
}
|
|
430
459
|
function isLoreCursorMcp(value) {
|
|
@@ -480,6 +509,36 @@ export function mcpConfigPath(agent, hooksPath) {
|
|
|
480
509
|
return resolve(directory, "..", "..", ".vscode", "mcp.json");
|
|
481
510
|
}
|
|
482
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* The hooks and MCP files of an agent that keeps them inside a project, where
|
|
514
|
+
* a repository can commit them; null for agents configured in their own folders.
|
|
515
|
+
*/
|
|
516
|
+
function projectConfigFiles(installation) {
|
|
517
|
+
if (installation.agent !== "copilot-vscode")
|
|
518
|
+
return null;
|
|
519
|
+
return {
|
|
520
|
+
project: resolve(dirname(installation.configPath), "..", ".."),
|
|
521
|
+
files: [installation.configPath, mcpConfigPath(installation.agent, installation.configPath)],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/** The first project config file that is, or sits in a folder that is, a symlink. */
|
|
525
|
+
async function linkedProjectConfigFile(installation, paths) {
|
|
526
|
+
const project = projectConfigFiles(installation);
|
|
527
|
+
if (project === null)
|
|
528
|
+
return null;
|
|
529
|
+
for (const file of project.files) {
|
|
530
|
+
// A link could point at any file of the user's, such as Lore's own token.
|
|
531
|
+
if (!await regularPath(file, paths.home, project.project))
|
|
532
|
+
return file;
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
async function assertProjectConfigNotLinked(installation, paths) {
|
|
537
|
+
const file = await linkedProjectConfigFile(installation, paths);
|
|
538
|
+
if (file !== null) {
|
|
539
|
+
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.`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
483
542
|
export function mergeLoreMcp(input, agent, paths) {
|
|
484
543
|
const config = objectConfiguration(input, agent);
|
|
485
544
|
// VS Code keeps its servers under `servers`; the other JSON hosts use `mcpServers`.
|
|
@@ -563,13 +622,13 @@ function copilotCliEventHandler(event, paths) {
|
|
|
563
622
|
return {
|
|
564
623
|
type: "command",
|
|
565
624
|
[isWindows() ? "powershell" : "bash"]: hookCommand("copilot-cli", paths, event),
|
|
566
|
-
timeoutSec: event === "sessionEnd"
|
|
625
|
+
timeoutSec: hookTimeout(event === "sessionEnd"
|
|
567
626
|
? 2
|
|
568
627
|
: event === "agentStop"
|
|
569
628
|
? 3
|
|
570
629
|
: event === "preToolUse"
|
|
571
630
|
? 10
|
|
572
|
-
: 25,
|
|
631
|
+
: 25),
|
|
573
632
|
};
|
|
574
633
|
}
|
|
575
634
|
function isLoreCopilotHook(value) {
|
|
@@ -659,13 +718,13 @@ function copilotVscodeEventHandler(event, paths) {
|
|
|
659
718
|
return {
|
|
660
719
|
type: "command",
|
|
661
720
|
command: hookCommand("copilot-vscode", paths, event),
|
|
662
|
-
timeout: event === "SessionEnd"
|
|
721
|
+
timeout: hookTimeout(event === "SessionEnd"
|
|
663
722
|
? 2
|
|
664
723
|
: event === "Stop"
|
|
665
724
|
? 3
|
|
666
725
|
: event === "PreToolUse"
|
|
667
726
|
? 10
|
|
668
|
-
: 25,
|
|
727
|
+
: 25),
|
|
669
728
|
};
|
|
670
729
|
}
|
|
671
730
|
export function mergeLoreCopilotVscodeHooks(input, paths, capture = true) {
|
|
@@ -993,6 +1052,7 @@ async function readJsonDocument(path) {
|
|
|
993
1052
|
return {
|
|
994
1053
|
exists: true,
|
|
995
1054
|
value: parsed,
|
|
1055
|
+
raw,
|
|
996
1056
|
mode: metadata.mode & 0o777,
|
|
997
1057
|
};
|
|
998
1058
|
}
|
|
@@ -1032,7 +1092,7 @@ async function writeMergedJson(path, document, value, now) {
|
|
|
1032
1092
|
let backup;
|
|
1033
1093
|
if (document.exists) {
|
|
1034
1094
|
backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
|
|
1035
|
-
await
|
|
1095
|
+
await writeFile(backup, document.raw ?? "", { flag: "wx", mode: document.mode });
|
|
1036
1096
|
}
|
|
1037
1097
|
await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`, document.mode);
|
|
1038
1098
|
return {
|
|
@@ -1065,7 +1125,7 @@ async function writeMergedText(path, document, value, now) {
|
|
|
1065
1125
|
let backup;
|
|
1066
1126
|
if (document.exists) {
|
|
1067
1127
|
backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
|
|
1068
|
-
await
|
|
1128
|
+
await writeFile(backup, document.value, { flag: "wx", mode: document.mode });
|
|
1069
1129
|
}
|
|
1070
1130
|
await atomicWrite(path, value, document.mode);
|
|
1071
1131
|
return {
|
|
@@ -1133,6 +1193,7 @@ function parseConnectorConfig(value) {
|
|
|
1133
1193
|
? {}
|
|
1134
1194
|
: { agentConfigPaths }),
|
|
1135
1195
|
...(value.capture === "off" ? { capture: "off" } : {}),
|
|
1196
|
+
...(value.recapReminder === "off" ? { recapReminder: "off" } : {}),
|
|
1136
1197
|
connectedAt: value.connectedAt,
|
|
1137
1198
|
timeoutMs,
|
|
1138
1199
|
};
|
|
@@ -1600,9 +1661,13 @@ async function installRuntime(paths) {
|
|
|
1600
1661
|
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
1601
1662
|
rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
|
|
1602
1663
|
rm(resolve(dirname(paths.runtime), "capture-conflict.js"), { force: true }),
|
|
1664
|
+
rm(resolve(dirname(paths.runtime), "skill-context.js"), { force: true }),
|
|
1665
|
+
rm(resolve(dirname(paths.runtime), "learning-line.js"), { force: true }),
|
|
1603
1666
|
rm(resolve(dirname(paths.runtime), "image-bytes.js"), { force: true }),
|
|
1604
1667
|
rm(resolve(dirname(paths.runtime), "image-capture.js"), { force: true }),
|
|
1605
1668
|
rm(resolve(dirname(paths.runtime), "retained-context.js"), { force: true }),
|
|
1669
|
+
rm(resolve(dirname(paths.runtime), "no-follow.js"), { force: true }),
|
|
1670
|
+
rm(resolve(dirname(paths.runtime), "system-command.js"), { force: true }),
|
|
1606
1671
|
rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
|
|
1607
1672
|
rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
|
|
1608
1673
|
rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
|
|
@@ -1620,7 +1685,10 @@ async function installRuntime(paths) {
|
|
|
1620
1685
|
const canonicalJsonModule = fileURLToPath(import.meta.resolve("@lore-co/core/canonical-json"));
|
|
1621
1686
|
const guardEvaluation = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/guard-evaluation")), "utf8");
|
|
1622
1687
|
const imageBytes = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/image-bytes")), "utf8");
|
|
1623
|
-
|
|
1688
|
+
// Hook modules refuse symbolic links through this dependency-free module.
|
|
1689
|
+
const noFollow = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/no-follow")), "utf8");
|
|
1690
|
+
const localNoFollow = (source) => source.replaceAll("@lore-co/core/no-follow", "./no-follow.js");
|
|
1691
|
+
const imageCapture = localNoFollow((await readFile(resolve(sourceDirectory, "image-capture.js"), "utf8")).replaceAll("@lore-co/core/image-bytes", "./image-bytes.js"));
|
|
1624
1692
|
const [runtime, repository, reliabilityStore, signedSnapshot, canonicalJson, contextFallback, invocationHealthWriter,] = await Promise.all([
|
|
1625
1693
|
readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
|
|
1626
1694
|
readFile(fileURLToPath(import.meta.resolve("@lore-co/core/repository-location")), "utf8"),
|
|
@@ -1636,12 +1704,19 @@ async function installRuntime(paths) {
|
|
|
1636
1704
|
}
|
|
1637
1705
|
const runtimeVersion = `export const RUNTIME_VERSION = ${JSON.stringify(LORE_VERSION)};\n`;
|
|
1638
1706
|
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"),
|
|
1707
|
+
"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
1708
|
"guard-evaluation.js": guardEvaluation,
|
|
1641
1709
|
"capture-conflict.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/capture-conflict")), "utf8"),
|
|
1710
|
+
// Fits a redacted skill pack back within its cap; it has no imports.
|
|
1711
|
+
"skill-context.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/skill-context")), "utf8"),
|
|
1712
|
+
// Formats Guard items the way every integration does; imports only repository-address.js.
|
|
1713
|
+
"learning-line.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/learning-line")), "utf8"),
|
|
1642
1714
|
"image-bytes.js": imageBytes,
|
|
1643
1715
|
"image-capture.js": imageCapture,
|
|
1644
|
-
"retained-context.js": await readFile(resolve(sourceDirectory, "retained-context.js"), "utf8"),
|
|
1716
|
+
"retained-context.js": localNoFollow(await readFile(resolve(sourceDirectory, "retained-context.js"), "utf8")),
|
|
1717
|
+
"no-follow.js": noFollow,
|
|
1718
|
+
// repository.js, directory-policy.js and the hook start git through this dependency-free module.
|
|
1719
|
+
"system-command.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/system-command")), "utf8"),
|
|
1645
1720
|
"retrieval-task.js": (await readFile(resolve(sourceDirectory, "retrieval-task.js"), "utf8")).replaceAll("@lore-co/core/user-message", "./user-message.js"),
|
|
1646
1721
|
"runtime-redaction.js": await readFile(resolve(sourceDirectory, "runtime-redaction.js"), "utf8"),
|
|
1647
1722
|
"user-message.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/user-message")), "utf8"),
|
|
@@ -1675,9 +1750,13 @@ async function installRuntime(paths) {
|
|
|
1675
1750
|
await Promise.all([
|
|
1676
1751
|
atomicWrite(resolve(dirname(paths.runtime), "guard-evaluation.js"), guardEvaluation, 0o600),
|
|
1677
1752
|
atomicWrite(resolve(dirname(paths.runtime), "capture-conflict.js"), runtimeFiles["capture-conflict.js"], 0o600),
|
|
1753
|
+
atomicWrite(resolve(dirname(paths.runtime), "skill-context.js"), runtimeFiles["skill-context.js"], 0o600),
|
|
1754
|
+
atomicWrite(resolve(dirname(paths.runtime), "learning-line.js"), runtimeFiles["learning-line.js"], 0o600),
|
|
1678
1755
|
atomicWrite(resolve(dirname(paths.runtime), "image-bytes.js"), imageBytes, 0o600),
|
|
1679
1756
|
atomicWrite(resolve(dirname(paths.runtime), "image-capture.js"), imageCapture, 0o600),
|
|
1680
1757
|
atomicWrite(resolve(dirname(paths.runtime), "retained-context.js"), runtimeFiles["retained-context.js"], 0o600),
|
|
1758
|
+
atomicWrite(resolve(dirname(paths.runtime), "no-follow.js"), noFollow, 0o600),
|
|
1759
|
+
atomicWrite(resolve(dirname(paths.runtime), "system-command.js"), runtimeFiles["system-command.js"], 0o600),
|
|
1681
1760
|
atomicWrite(resolve(dirname(paths.runtime), "retrieval-task.js"), runtimeFiles["retrieval-task.js"], 0o600),
|
|
1682
1761
|
atomicWrite(resolve(dirname(paths.runtime), "runtime-redaction.js"), runtimeFiles["runtime-redaction.js"], 0o600),
|
|
1683
1762
|
atomicWrite(resolve(dirname(paths.runtime), "user-message.js"), runtimeFiles["user-message.js"], 0o600),
|
|
@@ -1721,6 +1800,7 @@ async function prepareAgentEntries(installations, paths, capture) {
|
|
|
1721
1800
|
const mcpDocuments = new Map();
|
|
1722
1801
|
for (const installation of installations) {
|
|
1723
1802
|
const key = installationKey(installation);
|
|
1803
|
+
await assertProjectConfigNotLinked(installation, paths);
|
|
1724
1804
|
const document = await readJsonDocument(installation.configPath);
|
|
1725
1805
|
documents.set(key, document);
|
|
1726
1806
|
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths, capture));
|
|
@@ -1794,7 +1874,24 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
|
|
|
1794
1874
|
const generic = (await readGenericAgents(paths.loreDirectory)).length > 0;
|
|
1795
1875
|
const skills = await installLoreSkill(paths, generic ? [...installations, { agent: GENERIC_AGENT, configPath: paths.config }] : installations);
|
|
1796
1876
|
const agent = await installLoreAgent(paths, installations);
|
|
1877
|
+
const changed = new Set([...written.changedFiles, ...skills.changed, ...agent.changed]);
|
|
1878
|
+
const agents = new Map();
|
|
1879
|
+
for (const installation of installations) {
|
|
1880
|
+
// Every file this agent reads: hooks, MCP config, skills, and the Lore agent.
|
|
1881
|
+
const files = [
|
|
1882
|
+
installation.configPath,
|
|
1883
|
+
...(isMcpHostAgent(installation.agent) ? [mcpConfigPath(installation.agent, installation.configPath)] : []),
|
|
1884
|
+
...loreSkillFiles(paths, [installation]),
|
|
1885
|
+
...recapSkillFiles(paths, [installation]),
|
|
1886
|
+
...loreAgentFiles([installation]).map((entry) => entry.file),
|
|
1887
|
+
];
|
|
1888
|
+
const entry = agents.get(installation.agent) ?? { agent: installation.agent, changed: false, hooksChanged: false };
|
|
1889
|
+
entry.changed ||= files.some((file) => changed.has(file));
|
|
1890
|
+
entry.hooksChanged ||= written.changedFiles.includes(installation.configPath);
|
|
1891
|
+
agents.set(installation.agent, entry);
|
|
1892
|
+
}
|
|
1797
1893
|
return {
|
|
1894
|
+
agents: [...agents.values()],
|
|
1798
1895
|
// Same order the files are written in, so the two update lines match.
|
|
1799
1896
|
configFiles: [
|
|
1800
1897
|
...entries.mcpDocuments.keys(),
|
|
@@ -1809,6 +1906,35 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
|
|
|
1809
1906
|
};
|
|
1810
1907
|
});
|
|
1811
1908
|
}
|
|
1909
|
+
/**
|
|
1910
|
+
* The refresh after `lore update`, by agent: which agents got new Lore files
|
|
1911
|
+
* and what the customer must do, or that nothing changed. --verbose adds the
|
|
1912
|
+
* per-file report.
|
|
1913
|
+
*/
|
|
1914
|
+
export function formatRefreshSummary(result, style = PLAIN_OUTPUT) {
|
|
1915
|
+
const out = new Output(style);
|
|
1916
|
+
const updated = result.agents.filter((entry) => entry.changed).map((entry) => agentDisplayName(entry.agent));
|
|
1917
|
+
if (updated.length === 0)
|
|
1918
|
+
out.note("Your agents are up to date.");
|
|
1919
|
+
else
|
|
1920
|
+
out.outcome("ok", `Updated Lore for ${listText([...new Set(updated)])}`);
|
|
1921
|
+
if (result.warnings.length)
|
|
1922
|
+
out.blank();
|
|
1923
|
+
for (const warning of result.warnings)
|
|
1924
|
+
out.outcome("warning", tildePaths(warning, style.home));
|
|
1925
|
+
out.steps("Next", [
|
|
1926
|
+
...(updated.length ? ["Restart your agents."] : []),
|
|
1927
|
+
...(result.agents.some((entry) => entry.agent === "codex" && entry.hooksChanged) ? [`In Codex, approve Lore's updated hooks with ${out.code("/hooks")}.`] : []),
|
|
1928
|
+
]);
|
|
1929
|
+
if (style.verbose) {
|
|
1930
|
+
out.blank().verbatim(out.bold("Details"));
|
|
1931
|
+
for (const line of formatRefreshReport(result).trimEnd().split("\n")) {
|
|
1932
|
+
if (!line.startsWith("warning: "))
|
|
1933
|
+
out.text(tildePaths(line, style.home));
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return out.toString();
|
|
1937
|
+
}
|
|
1812
1938
|
/** What a refresh rewrote and what was already at this version, one line each. */
|
|
1813
1939
|
export function formatRefreshReport(result) {
|
|
1814
1940
|
const lines = [
|
|
@@ -1837,9 +1963,11 @@ export function formatRefreshReport(result) {
|
|
|
1837
1963
|
async function runInternalCommand(args) {
|
|
1838
1964
|
if (args.length !== 1 || args[0] !== "refresh-integrations")
|
|
1839
1965
|
throw new Error("Usage: lore internal refresh-integrations");
|
|
1840
|
-
const
|
|
1966
|
+
const paths = getLorePaths();
|
|
1967
|
+
const refreshed = await refreshAgentEntries(paths);
|
|
1968
|
+
// `lore update` runs this with piped stdout and says how to draw through LORE_OUTPUT_*.
|
|
1841
1969
|
if (refreshed !== null)
|
|
1842
|
-
process.stdout.write(
|
|
1970
|
+
process.stdout.write(formatRefreshSummary(refreshed, detectOutputStyle({ home: paths.home })));
|
|
1843
1971
|
}
|
|
1844
1972
|
function countAgentIntegrations(input, agent) {
|
|
1845
1973
|
const target = targetFor(agent);
|
|
@@ -1876,6 +2004,10 @@ function parseConnectArguments(args) {
|
|
|
1876
2004
|
parsed.json = true;
|
|
1877
2005
|
continue;
|
|
1878
2006
|
}
|
|
2007
|
+
if (argument === "--verbose") {
|
|
2008
|
+
parsed.verbose = true;
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
1879
2011
|
if (argument === "--login") {
|
|
1880
2012
|
parsed.login = true;
|
|
1881
2013
|
continue;
|
|
@@ -2040,9 +2172,13 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
2040
2172
|
const recoveryEnv = options.home === undefined ? process.env : { ...process.env, ZDOTDIR: paths.home };
|
|
2041
2173
|
let recoveryWarning;
|
|
2042
2174
|
const externalRecoveryHost = options.agents.some(agent => ["t3code", "cowork", "copilot-vscode"].includes(agent));
|
|
2175
|
+
// Named agents limit recovery to themselves; auto-detection already installs every detected agent.
|
|
2176
|
+
const namedRecoveryAgents = options.agents.length === 0 ? undefined
|
|
2177
|
+
: options.agents.map(agent => agent === "copilot" ? "copilot-cli" : agent);
|
|
2178
|
+
const namedSources = namedRecoveryAgents?.filter((agent) => isRecoverySource(agent) && agent !== options.fallback);
|
|
2043
2179
|
const installRecovery = options.fallback === undefined ? options.recovery === true ?
|
|
2044
|
-
await prepareAutomaticRecovery(paths, recoveryEnv).catch((error) => { recoveryWarning = `Automatic recovery setup: ${error.message}`; return undefined; }) : undefined :
|
|
2045
|
-
await prepareRecoveryInstallation(paths, options.fallback, recoveryEnv).catch((error) => {
|
|
2180
|
+
await prepareAutomaticRecovery(paths, recoveryEnv, namedRecoveryAgents).catch((error) => { recoveryWarning = `Automatic recovery setup: ${error.message}`; return undefined; }) : undefined :
|
|
2181
|
+
await prepareRecoveryInstallation(paths, options.fallback, recoveryEnv, namedSources?.length ? namedSources : undefined).catch((error) => {
|
|
2046
2182
|
// A desktop host can be the source even when only its destination is on PATH.
|
|
2047
2183
|
if (externalRecoveryHost && error.message.startsWith("No supported source client is installed."))
|
|
2048
2184
|
return undefined;
|
|
@@ -2108,6 +2244,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
2108
2244
|
agents,
|
|
2109
2245
|
agentConfigPaths,
|
|
2110
2246
|
...(capture ? {} : { capture: "off" }),
|
|
2247
|
+
...(existing?.recapReminder === "off" ? { recapReminder: "off" } : {}),
|
|
2111
2248
|
connectedAt: connection.profiles.find(value => value.connectionId === connection.fields.connectionId)?.connectedAt ?? now.toISOString(),
|
|
2112
2249
|
timeoutMs,
|
|
2113
2250
|
};
|
|
@@ -2180,16 +2317,36 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
2180
2317
|
keptAgents,
|
|
2181
2318
|
alreadyConnected,
|
|
2182
2319
|
...(knownDashboardUrl === undefined ? {} : { dashboardUrl: knownDashboardUrl }),
|
|
2183
|
-
folder: options.cwd ?? process.cwd(),
|
|
2320
|
+
folder: await firstMessageFolder(options.cwd ?? process.cwd(), config.directories, paths.home),
|
|
2184
2321
|
config: paths.config,
|
|
2185
2322
|
changedHookFiles,
|
|
2186
2323
|
backups,
|
|
2187
2324
|
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
|
-
? [
|
|
2325
|
+
? [CODEX_APPROVAL_WARNING]
|
|
2189
2326
|
: [])],
|
|
2190
2327
|
...(recoveryNotice ? { recoveryNotice } : {}),
|
|
2191
2328
|
};
|
|
2192
2329
|
}
|
|
2330
|
+
/**
|
|
2331
|
+
* The browser approval link, and whether Lore may open it: only a page on the
|
|
2332
|
+
* dashboard (or, without one, the API server) the person chose. With a
|
|
2333
|
+
* dashboard, only the server's path and query are kept; setting them rather
|
|
2334
|
+
* than resolving a reference keeps a `//host` path on the dashboard.
|
|
2335
|
+
*/
|
|
2336
|
+
export function deviceApprovalUrl(verificationUriComplete, input) {
|
|
2337
|
+
const server = new URL(verificationUriComplete);
|
|
2338
|
+
const url = input.dashboardUrl === undefined ? server : new URL(`${input.dashboardUrl}/`);
|
|
2339
|
+
if (input.dashboardUrl !== undefined) {
|
|
2340
|
+
url.pathname = server.pathname;
|
|
2341
|
+
url.search = server.search;
|
|
2342
|
+
}
|
|
2343
|
+
const expectedOrigin = new URL(input.dashboardUrl ?? input.apiUrl).origin;
|
|
2344
|
+
return {
|
|
2345
|
+
url: url.toString(),
|
|
2346
|
+
trusted: ["http:", "https:"].includes(url.protocol) && url.origin === expectedOrigin,
|
|
2347
|
+
expectedOrigin,
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2193
2350
|
async function requestDeviceAuthorization(input) {
|
|
2194
2351
|
let response;
|
|
2195
2352
|
try {
|
|
@@ -2219,13 +2376,13 @@ async function requestDeviceAuthorization(input) {
|
|
|
2219
2376
|
if (!authorization.success) {
|
|
2220
2377
|
throw new Error("Lore returned an incompatible device authorization response.");
|
|
2221
2378
|
}
|
|
2222
|
-
const
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
if (input.openBrowser) {
|
|
2228
|
-
|
|
2379
|
+
const approval = deviceApprovalUrl(authorization.data.verificationUriComplete, input);
|
|
2380
|
+
process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approval.url}\n`);
|
|
2381
|
+
if (input.openBrowser && approval.trusted) {
|
|
2382
|
+
openBrowser(approval.url);
|
|
2383
|
+
}
|
|
2384
|
+
else if (input.openBrowser) {
|
|
2385
|
+
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
2386
|
}
|
|
2230
2387
|
const expiresAt = Date.parse(authorization.data.expiresAt);
|
|
2231
2388
|
const intervalMs = authorization.data.intervalSeconds * 1_000;
|
|
@@ -2340,48 +2497,182 @@ export function connectNextStep(agent) {
|
|
|
2340
2497
|
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
2498
|
}
|
|
2342
2499
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2500
|
+
/** A folder Lore records in: the current one when it is inside the capture folders, else the first capture folder. */
|
|
2501
|
+
export async function firstMessageFolder(cwd, directories, home) {
|
|
2502
|
+
const first = directories?.include[0];
|
|
2503
|
+
if (first === undefined || (await checkDirectoryRules(directories, cwd, home)).allowed)
|
|
2504
|
+
return cwd;
|
|
2505
|
+
const expanded = first.startsWith("~/") || first.startsWith("~\\") ? resolve(home, first.slice(2)) : first;
|
|
2506
|
+
const parts = resolve(expanded).split(sep);
|
|
2507
|
+
const wildcard = parts.findIndex((part) => /[*?]/u.test(part));
|
|
2508
|
+
return wildcard < 0 ? resolve(expanded) : parts.slice(0, wildcard).join(sep) || sep;
|
|
2509
|
+
}
|
|
2510
|
+
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.";
|
|
2511
|
+
/** Product names for human output; --json keeps the agent ids. */
|
|
2512
|
+
export function agentDisplayName(agent) {
|
|
2513
|
+
switch (agent) {
|
|
2514
|
+
case "claude": return "Claude Code";
|
|
2515
|
+
case "codex": return "Codex";
|
|
2516
|
+
case "copilot":
|
|
2517
|
+
case "copilot-cli": return "Copilot CLI";
|
|
2518
|
+
case "copilot-vscode": return "Copilot in VS Code";
|
|
2519
|
+
case "cursor": return "Cursor";
|
|
2520
|
+
case "opencode": return "OpenCode";
|
|
2521
|
+
case "polytoken": return "Polytoken";
|
|
2522
|
+
case "devin": return "Devin CLI";
|
|
2523
|
+
case "cowork": return "Claude Cowork";
|
|
2524
|
+
case "t3code": return "T3 Code";
|
|
2525
|
+
case "generic": return "MCP agent";
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
/** What one connected agent still needs, for its row in the connect summary. */
|
|
2529
|
+
function connectAgentNote(agent, out) {
|
|
2530
|
+
switch (agent) {
|
|
2531
|
+
case "claude": return "ready after restart, allow the lore server when asked";
|
|
2532
|
+
case "codex": return `ready after restart, approve hooks with ${out.code("/hooks")}`;
|
|
2533
|
+
case "cursor": return "ready after restart, enable lore-native and lore";
|
|
2534
|
+
case "copilot-vscode": return "ready after you restart the VS Code session";
|
|
2535
|
+
case "devin": return `run ${out.code("lore devin cli setup")}, then open a new terminal`;
|
|
2536
|
+
case "generic": return "add the MCP entry and instructions below, then restart it";
|
|
2537
|
+
default: return "ready after restart";
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* The human summary of a connect run: the outcome, where Lore records, one
|
|
2542
|
+
* row per agent, and the numbered steps left for the customer. Paths and
|
|
2543
|
+
* per-file detail appear with --verbose; --json prints the result itself.
|
|
2544
|
+
*/
|
|
2545
|
+
export function formatConnectResult(result, options = {}) {
|
|
2546
|
+
const style = options.style ?? { ...PLAIN_OUTPUT, home: dirname(dirname(result.config)) };
|
|
2547
|
+
const out = new Output(style);
|
|
2347
2548
|
const generic = result.genericAgent;
|
|
2549
|
+
const genericName = generic?.id ?? agentDisplayName("generic");
|
|
2550
|
+
const name = (agent) => agent === GENERIC_AGENT ? genericName : agentDisplayName(agent);
|
|
2348
2551
|
// With only an MCP agent connected, nothing on this machine records turns.
|
|
2349
2552
|
const nothingRecords = result.capture.mode === "off" || (generic !== undefined && result.agents.every((agent) => agent === GENERIC_AGENT));
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2553
|
+
const pausedBy = result.capture.mode === "on" ? options.capturePausedBy ?? null : null;
|
|
2554
|
+
const folders = result.capture.folders;
|
|
2555
|
+
const shownFolders = folders === null ? "all projects the workspace allows"
|
|
2556
|
+
: folders.length === 0 ? "no folders yet"
|
|
2557
|
+
: listText(folders.length > 3 ? [...folders.slice(0, 2).map((folder) => out.path(folder)), `${folders.length - 2} more`] : folders.map((folder) => out.path(folder)));
|
|
2558
|
+
const excluded = result.capture.excludedFolders.length ? `, except ${listText(result.capture.excludedFolders.map((folder) => out.path(folder)))}` : "";
|
|
2559
|
+
const mode = generic !== undefined && nothingRecords && result.capture.mode === "on" ? "MCP only, saves what you ask"
|
|
2560
|
+
: result.capture.mode === "off" ? "capture off"
|
|
2561
|
+
: pausedBy === "workspace" ? "paused by workspace"
|
|
2562
|
+
: pausedBy === "member" ? "paused by your setting"
|
|
2563
|
+
: "capture on";
|
|
2564
|
+
const records = mode === "capture on";
|
|
2565
|
+
out.outcome("ok", result.alreadyConnected
|
|
2566
|
+
? `Already connected to ${result.identity.workspaceName}, nothing changed`
|
|
2567
|
+
: `Connected to ${result.identity.workspaceName}`);
|
|
2568
|
+
out.note(`${records ? "Recording in" : "Lore runs in"} ${shownFolders}${excluded}${out.separator}${mode}`);
|
|
2569
|
+
if (generic !== undefined && !nothingRecords)
|
|
2570
|
+
out.note(`${generic.id} is MCP-only: Lore saves only what you ask it to.`);
|
|
2571
|
+
// One row per agent: set up in this run first, then the ones kept as they were.
|
|
2572
|
+
const rows = [];
|
|
2573
|
+
const seen = new Set();
|
|
2574
|
+
for (const [agent, mark, note] of [
|
|
2575
|
+
...result.installedAgents.map((agent) => [agent, out.mark("ok"), connectAgentNote(agent, out)]),
|
|
2576
|
+
...result.keptAgents.map((agent) => [agent, "", out.dim("already set up")]),
|
|
2577
|
+
]) {
|
|
2578
|
+
if (seen.has(name(agent)))
|
|
2579
|
+
continue;
|
|
2580
|
+
seen.add(name(agent));
|
|
2581
|
+
rows.push([name(agent), mark, note]);
|
|
2582
|
+
}
|
|
2583
|
+
if (rows.length) {
|
|
2584
|
+
out.blank();
|
|
2585
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
2586
|
+
for (const [label, mark, note] of rows)
|
|
2587
|
+
out.row(label, width, note, mark);
|
|
2588
|
+
}
|
|
2589
|
+
const steps = [];
|
|
2590
|
+
const restart = result.installedAgents.filter((agent) => agent !== "devin" && agent !== GENERIC_AGENT);
|
|
2591
|
+
if (restart.length) {
|
|
2592
|
+
steps.push(restart.length === rows.length ? "Restart the agents above." : `Restart ${listText([...new Set(restart.map(name))])}.`);
|
|
2593
|
+
}
|
|
2594
|
+
if (result.installedAgents.includes("devin"))
|
|
2595
|
+
steps.push(`Run ${out.code("lore devin cli setup")}, then open a new terminal.`);
|
|
2596
|
+
if (generic !== undefined)
|
|
2597
|
+
steps.push(`Add the MCP entry and instructions below to ${generic.id}, then restart it.`);
|
|
2598
|
+
const warnings = result.warnings.filter((warning) => warning !== CODEX_APPROVAL_WARNING);
|
|
2599
|
+
if (warnings.length !== result.warnings.length)
|
|
2600
|
+
steps.push(`In Codex, approve Lore's updated hooks with ${out.code("/hooks")}.`);
|
|
2601
|
+
const notes = [];
|
|
2602
|
+
for (const line of result.recoveryNotice?.split("\n") ?? []) {
|
|
2603
|
+
const shell = /^Open a new terminal or run: (.+)$/u.exec(line);
|
|
2604
|
+
if (!result.recoveryNotice.startsWith("Recovery enabled:"))
|
|
2605
|
+
warnings.push(line);
|
|
2606
|
+
else if (shell)
|
|
2607
|
+
steps.push(`Open a new terminal, or run ${out.code(shell[1])}.`);
|
|
2608
|
+
else if (line.startsWith("Codex requires one-time approval")) {
|
|
2609
|
+
if (!result.installedAgents.includes("codex"))
|
|
2610
|
+
steps.push(`In Codex, approve Lore's hooks with ${out.code("/hooks")}.`);
|
|
2611
|
+
}
|
|
2612
|
+
else
|
|
2613
|
+
notes.push(style.rich ? line : line.replaceAll("→", "->"));
|
|
2614
|
+
}
|
|
2615
|
+
const folder = out.path(result.folder);
|
|
2616
|
+
const ask = "\"Lore, has anything like this come up before?\"";
|
|
2617
|
+
if (folders === null || folders.length > 0) {
|
|
2618
|
+
steps.push(generic !== undefined && nothingRecords
|
|
2619
|
+
? `In ${folder}, ask your agent ${ask}, then run ${out.code("lore doctor")}.`
|
|
2620
|
+
: !records
|
|
2621
|
+
? `In ${folder}, ask your agent anything, then run ${out.code("lore doctor")} to check that context arrives.`
|
|
2365
2622
|
: result.dashboardUrl === undefined
|
|
2366
|
-
? `
|
|
2367
|
-
: `
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2623
|
+
? `In ${folder}, ask your agent anything, then run ${out.code("lore doctor")} to confirm recording.`
|
|
2624
|
+
: `In ${folder}, ask your agent anything, then check ${out.code(`${result.dashboardUrl}/connect`)}.`);
|
|
2625
|
+
}
|
|
2626
|
+
if (notes.length)
|
|
2627
|
+
out.blank();
|
|
2628
|
+
for (const note of notes)
|
|
2629
|
+
out.note(note);
|
|
2630
|
+
if (warnings.length)
|
|
2631
|
+
out.blank();
|
|
2632
|
+
for (const warning of warnings)
|
|
2633
|
+
out.outcome("warning", tildePaths(warning, style.home));
|
|
2634
|
+
out.steps("Next", steps);
|
|
2635
|
+
const recap = result.skillFiles.some((file) => basename(dirname(file)) === "recap");
|
|
2636
|
+
if (result.skillFiles.length || result.agentFiles.length) {
|
|
2637
|
+
out.blank().text(`Try: ${ask}${recap ? `${style.rich ? " · " : " or "}${out.code("/recap")} to save a session` : ""}`, 0);
|
|
2638
|
+
}
|
|
2639
|
+
if (style.verbose) {
|
|
2640
|
+
const details = [
|
|
2641
|
+
["Workspace", `${result.identity.workspaceName} (${result.identity.workspaceId})`],
|
|
2642
|
+
["API", result.apiUrl],
|
|
2643
|
+
...(result.dashboardUrl === undefined ? [] : [["Dashboard", result.dashboardUrl]]),
|
|
2644
|
+
["Config", out.path(result.config)],
|
|
2645
|
+
["Capture", captureSummary(result.capture.mode, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })],
|
|
2646
|
+
["Folders", folders === null ? "all projects allowed by the workspace" : folders.map((value) => out.path(value)).join(", ") || "none"],
|
|
2647
|
+
...(result.capture.excludedFolders.length ? [["Excluded", result.capture.excludedFolders.map((value) => out.path(value)).join(", ")]] : []),
|
|
2648
|
+
["Agents", result.agents.join(", ")],
|
|
2649
|
+
...(result.keptAgents.length ? [["Kept", result.keptAgents.join(", ")]] : []),
|
|
2650
|
+
["Changed", result.changedHookFiles.map((file) => out.path(file)).join(", ") || "nothing"],
|
|
2651
|
+
...(result.backups.length ? [["Backups", result.backups.map((file) => out.path(file)).join(", ")]] : []),
|
|
2652
|
+
...(result.skillFiles.length ? [["Skills", result.skillFiles.map((file) => out.path(file)).join(", ")]] : []),
|
|
2653
|
+
...(result.agentFiles.length ? [["Lore agent", result.agentFiles.map((file) => out.path(file)).join(", ")]] : []),
|
|
2654
|
+
];
|
|
2655
|
+
out.blank().verbatim(out.bold("Details"));
|
|
2656
|
+
const width = Math.max(...details.map(([label]) => label.length));
|
|
2657
|
+
for (const [label, value] of details)
|
|
2658
|
+
out.row(label, width, value);
|
|
2659
|
+
}
|
|
2660
|
+
if (generic !== undefined) {
|
|
2661
|
+
out.blank().note(generic.writeTools
|
|
2662
|
+
? `Saves from ${generic.id} are proposals credited to ${generic.id}; review them in Learnings.`
|
|
2663
|
+
: `${generic.id} can read Lore; it saves through the lore CLI. Reconnect with --write to let it save through MCP (saves stay proposals).`, 0);
|
|
2664
|
+
// Copied into the agent's own settings: printed exactly, never styled or wrapped.
|
|
2665
|
+
out.blank().verbatim(`MCP entry for ${generic.id} (merge into its mcpServers settings):`)
|
|
2666
|
+
.verbatim(JSON.stringify({ mcpServers: generic.mcpServers }, null, 2))
|
|
2667
|
+
.blank().verbatim("Instructions for AGENTS.md, GEMINI.md, or the agent's rules file:")
|
|
2668
|
+
.verbatim(generic.instructions);
|
|
2669
|
+
}
|
|
2670
|
+
return out.toString();
|
|
2671
|
+
}
|
|
2672
|
+
/** Show the home folder as ~ inside a message that names paths. */
|
|
2673
|
+
function tildePaths(text, home) {
|
|
2674
|
+
const prefix = home.endsWith(sep) ? home : `${home}${sep}`;
|
|
2675
|
+
return text.replaceAll(prefix, `~${sep}`);
|
|
2385
2676
|
}
|
|
2386
2677
|
async function connectCommand(args) {
|
|
2387
2678
|
const parsed = parseConnectArguments(args);
|
|
@@ -2438,7 +2729,18 @@ async function connectCommand(args) {
|
|
|
2438
2729
|
});
|
|
2439
2730
|
if (authorization)
|
|
2440
2731
|
await acknowledgeDeviceAuthorization(apiUrl, authorization);
|
|
2441
|
-
|
|
2732
|
+
if (parsed.json) {
|
|
2733
|
+
writeResult(result, true, "");
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
// The summary says when a dashboard switch pauses recording; the policy read is best-effort.
|
|
2737
|
+
const capturePausedBy = result.capture.mode === "on"
|
|
2738
|
+
? (await checkWorkspaceRepository({ apiUrl: result.apiUrl, token, timeoutMs: parsed.timeoutMs ?? 2_500 }, undefined).catch(() => undefined))?.capturePausedBy ?? null
|
|
2739
|
+
: null;
|
|
2740
|
+
process.stdout.write(formatConnectResult(result, {
|
|
2741
|
+
style: detectOutputStyle({ verbose: parsed.verbose === true, home: getLorePaths().home }),
|
|
2742
|
+
capturePausedBy,
|
|
2743
|
+
}));
|
|
2442
2744
|
}
|
|
2443
2745
|
async function queueCount(paths) {
|
|
2444
2746
|
try {
|
|
@@ -2501,7 +2803,7 @@ async function inspectReliabilityStore(config, paths) {
|
|
|
2501
2803
|
const MAX_RUNTIME_MODULE_BYTES = 2 * 1024 * 1024;
|
|
2502
2804
|
const MAX_RUNTIME_MANIFEST_BYTES = 64 * 1024;
|
|
2503
2805
|
async function readRuntimeFile(path, maxBytes, executable = false) {
|
|
2504
|
-
const handle = await
|
|
2806
|
+
const handle = await openNoFollow(path, fsConstants.O_RDONLY);
|
|
2505
2807
|
try {
|
|
2506
2808
|
const metadata = await handle.stat();
|
|
2507
2809
|
if (!metadata.isFile() ||
|
|
@@ -2544,9 +2846,13 @@ async function inspectRuntimeInstallation(required, paths) {
|
|
|
2544
2846
|
"lore-hook.mjs": paths.runtime,
|
|
2545
2847
|
"guard-evaluation.js": resolve(dirname(paths.runtime), "guard-evaluation.js"),
|
|
2546
2848
|
"capture-conflict.js": resolve(dirname(paths.runtime), "capture-conflict.js"),
|
|
2849
|
+
"skill-context.js": resolve(dirname(paths.runtime), "skill-context.js"),
|
|
2850
|
+
"learning-line.js": resolve(dirname(paths.runtime), "learning-line.js"),
|
|
2547
2851
|
"image-bytes.js": resolve(dirname(paths.runtime), "image-bytes.js"),
|
|
2548
2852
|
"image-capture.js": resolve(dirname(paths.runtime), "image-capture.js"),
|
|
2549
2853
|
"retained-context.js": resolve(dirname(paths.runtime), "retained-context.js"),
|
|
2854
|
+
"no-follow.js": resolve(dirname(paths.runtime), "no-follow.js"),
|
|
2855
|
+
"system-command.js": resolve(dirname(paths.runtime), "system-command.js"),
|
|
2550
2856
|
"retrieval-task.js": resolve(dirname(paths.runtime), "retrieval-task.js"),
|
|
2551
2857
|
"runtime-redaction.js": resolve(dirname(paths.runtime), "runtime-redaction.js"),
|
|
2552
2858
|
"user-message.js": resolve(dirname(paths.runtime), "user-message.js"),
|
|
@@ -2665,6 +2971,7 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
2665
2971
|
const units = [];
|
|
2666
2972
|
for (const [index, path] of configPaths.entries()) {
|
|
2667
2973
|
try {
|
|
2974
|
+
await assertProjectConfigNotLinked({ agent, configPath: path }, paths);
|
|
2668
2975
|
const document = await readJsonDocument(path);
|
|
2669
2976
|
installed += countAgentIntegrations(document.value, agent);
|
|
2670
2977
|
existing += document.exists ? 1 : 0;
|
|
@@ -2819,12 +3126,99 @@ function formatRetrievalFailureDiagnostics(retrieval) {
|
|
|
2819
3126
|
return (`local_retrieval_failure_reasons: ${reasons.length === 0 ? "-" : reasons.join(" ")}\n` +
|
|
2820
3127
|
`last_retrieval_failure: ${last === null ? "-" : `${last.reason} (${last.path} request, agent ${last.agent}, at ${last.at})`}\n`);
|
|
2821
3128
|
}
|
|
3129
|
+
/**
|
|
3130
|
+
* The plain lines at the top of `lore status`: live context over the last
|
|
3131
|
+
* days on this machine, then a server behind this CLI and out-of-date Lore
|
|
3132
|
+
* files, each only when true.
|
|
3133
|
+
*/
|
|
3134
|
+
function statusHealth(data, serverVersion) {
|
|
3135
|
+
const context = data.reliability?.operational.retrieval.recent ?? null;
|
|
3136
|
+
const staleFiles = [];
|
|
3137
|
+
if (data.runtimeRequired && !data.runtimeInstalled && data.runtimeVersion !== null) {
|
|
3138
|
+
staleFiles.push({ name: "Lore hooks", version: data.runtimeVersion === LORE_VERSION ? null : data.runtimeVersion });
|
|
3139
|
+
}
|
|
3140
|
+
for (const agent of data.agents) {
|
|
3141
|
+
// Hook agents share the runtime above; a plugin carries its own copy.
|
|
3142
|
+
if (agent.installation.runtime !== "stale" || targetFor(agent.agent).runtimeRequired)
|
|
3143
|
+
continue;
|
|
3144
|
+
staleFiles.push({ name: `${agentDisplayName(agent.agent)} plugin`, version: agent.installation.runtimeVersion });
|
|
3145
|
+
}
|
|
3146
|
+
const lines = [
|
|
3147
|
+
contextHealthLine(context ?? undefined),
|
|
3148
|
+
serverBehindLine(serverVersion, LORE_VERSION),
|
|
3149
|
+
staleFilesLine(staleFiles),
|
|
3150
|
+
].filter((line) => line !== null);
|
|
3151
|
+
return { context, serverVersion, staleFiles, lines };
|
|
3152
|
+
}
|
|
3153
|
+
/** The server's release, from the identity endpoint; null when unreachable. */
|
|
3154
|
+
async function connectedServerVersion(config) {
|
|
3155
|
+
try {
|
|
3156
|
+
const response = await fetch(`${config.apiUrl}/v1/workspace/identity`, {
|
|
3157
|
+
headers: { accept: "application/json", authorization: `Bearer ${config.token}`, "x-lore-connection-identity": "1" },
|
|
3158
|
+
signal: AbortSignal.timeout(Math.min(config.timeoutMs ?? 2_000, 2_000)),
|
|
3159
|
+
});
|
|
3160
|
+
if (!response.ok)
|
|
3161
|
+
return null;
|
|
3162
|
+
const parsed = WorkspaceIdentityResponseSchema.safeParse(await response.json());
|
|
3163
|
+
return parsed.success ? parsed.data.server.version : null;
|
|
3164
|
+
}
|
|
3165
|
+
catch {
|
|
3166
|
+
return null;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
/** The default `lore status`: what someone needs to know, and the one next step. */
|
|
3170
|
+
export function statusSummary(data, healthLines, workspaceName) {
|
|
3171
|
+
if (!data.connected)
|
|
3172
|
+
return "Lore isn't connected on this machine.\nNext: lore connect --login\n";
|
|
3173
|
+
const lines = [...healthLines, workspaceName ? `Connected to ${workspaceName}.` : "Connected."];
|
|
3174
|
+
lines.push(!data.folderAccess.allowed
|
|
3175
|
+
? `This folder: Lore is off (${data.folderAccess.reason}).`
|
|
3176
|
+
: data.capture === "off" ? "This folder: context and Guard on; recording off."
|
|
3177
|
+
: data.captureSwitches.pausedBy !== null ? `This folder: context and Guard on; ${capturePauseMessage(data.captureSwitches.pausedBy).toLowerCase()}.`
|
|
3178
|
+
: "This folder: recording on.");
|
|
3179
|
+
const agents = data.agents.filter((agent) => agent.configured);
|
|
3180
|
+
const day = (value) => value === null ? "" : new Date(value).toISOString().slice(0, 10);
|
|
3181
|
+
let next = null;
|
|
3182
|
+
if (agents.length > 0) {
|
|
3183
|
+
const width = Math.max(...agents.map((agent) => agentDisplayName(agent.agent).length));
|
|
3184
|
+
lines.push("Agents:");
|
|
3185
|
+
for (const agent of agents) {
|
|
3186
|
+
const broken = agent.installation.state !== "complete" || agent.invocationError !== null
|
|
3187
|
+
|| agent.invocation.state === "failed" || agent.invocation.state === "incomplete";
|
|
3188
|
+
if (broken)
|
|
3189
|
+
next ??= "lore doctor";
|
|
3190
|
+
const state = agent.installation.state !== "complete" ? "setup incomplete"
|
|
3191
|
+
: agent.invocationError !== null || agent.invocation.state === "failed" || agent.invocation.state === "incomplete" ? "last run failed"
|
|
3192
|
+
: agent.invocation.state === "successful" ? `last used ${day(agent.invocation.lastSuccessfulAt)}`
|
|
3193
|
+
: "not used yet";
|
|
3194
|
+
lines.push(` ${agentDisplayName(agent.agent).padEnd(width)} ${state}`);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
if (next !== null)
|
|
3198
|
+
lines.push(`Next: ${next}`);
|
|
3199
|
+
lines.push("Details: lore status --verbose");
|
|
3200
|
+
return `${lines.join("\n")}\n`;
|
|
3201
|
+
}
|
|
2822
3202
|
async function statusCommand(args) {
|
|
2823
|
-
const
|
|
3203
|
+
const verbose = args.includes("--verbose");
|
|
3204
|
+
const parsed = parseOutputArguments(args.filter((argument) => argument !== "--verbose"), STATUS_HELP, "status");
|
|
2824
3205
|
if (parsed === null) {
|
|
2825
3206
|
return;
|
|
2826
3207
|
}
|
|
2827
|
-
const
|
|
3208
|
+
const paths = getLorePaths();
|
|
3209
|
+
const config = await diagnosticConfig(paths, process.cwd());
|
|
3210
|
+
const [statusResult, serverVersion] = await Promise.all([
|
|
3211
|
+
statusData(paths, process.cwd()),
|
|
3212
|
+
config === null ? null : connectedServerVersion(config),
|
|
3213
|
+
]);
|
|
3214
|
+
const health = statusHealth(statusResult, serverVersion);
|
|
3215
|
+
const data = { ...statusResult, health };
|
|
3216
|
+
if (!parsed.json && loreDisabled(process.env))
|
|
3217
|
+
process.stdout.write(`${LORE_DISABLED_MESSAGE}\n`);
|
|
3218
|
+
if (!parsed.json && !verbose) {
|
|
3219
|
+
process.stdout.write(statusSummary(data, health.lines, config?.workspaceName));
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
2828
3222
|
if (!parsed.json && data.connection)
|
|
2829
3223
|
process.stdout.write(`connection: ${data.connection}\n`);
|
|
2830
3224
|
const agentLines = data.agents
|
|
@@ -2837,7 +3231,7 @@ async function statusCommand(args) {
|
|
|
2837
3231
|
? "-"
|
|
2838
3232
|
: `${(value * 100).toFixed(1)}%`;
|
|
2839
3233
|
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`);
|
|
3234
|
+
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
3235
|
}
|
|
2842
3236
|
async function disconnectCommand(args) {
|
|
2843
3237
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -2860,6 +3254,9 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2860
3254
|
]);
|
|
2861
3255
|
for (const installation of installations) {
|
|
2862
3256
|
const path = installation.configPath;
|
|
3257
|
+
// Leave linked project files alone; disconnecting everything else still matters.
|
|
3258
|
+
if (await linkedProjectConfigFile(installation, paths) !== null)
|
|
3259
|
+
continue;
|
|
2863
3260
|
if (isMcpHostAgent(installation.agent)) {
|
|
2864
3261
|
const mcpPath = mcpConfigPath(installation.agent, path);
|
|
2865
3262
|
if (installation.agent === "codex") {
|
|
@@ -2914,9 +3311,12 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2914
3311
|
rm(paths.runtimeContextFallback, { force: true }),
|
|
2915
3312
|
rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
|
|
2916
3313
|
rm(resolve(dirname(paths.runtime), "capture-conflict.js"), { force: true }),
|
|
3314
|
+
rm(resolve(dirname(paths.runtime), "skill-context.js"), { force: true }),
|
|
3315
|
+
rm(resolve(dirname(paths.runtime), "learning-line.js"), { force: true }),
|
|
2917
3316
|
rm(resolve(dirname(paths.runtime), "image-bytes.js"), { force: true }),
|
|
2918
3317
|
rm(resolve(dirname(paths.runtime), "image-capture.js"), { force: true }),
|
|
2919
3318
|
rm(resolve(dirname(paths.runtime), "retained-context.js"), { force: true }),
|
|
3319
|
+
rm(resolve(dirname(paths.runtime), "no-follow.js"), { force: true }),
|
|
2920
3320
|
rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
|
|
2921
3321
|
rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
|
|
2922
3322
|
rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
|
|
@@ -2974,17 +3374,18 @@ export function captureModeCheck(capture, switches = CAPTURE_SWITCHES_UNCHECKED)
|
|
|
2974
3374
|
};
|
|
2975
3375
|
}
|
|
2976
3376
|
/**
|
|
2977
|
-
* Server-side queue health. Dead
|
|
2978
|
-
*
|
|
2979
|
-
* for the reconciler's backoff.
|
|
3377
|
+
* Server-side queue health. Dead jobs name the operator fix: once the provider
|
|
3378
|
+
* is repaired, `lore capture repair` and `lore embeddings repair` retry them
|
|
3379
|
+
* without waiting for a new capture or the reconciler's backoff.
|
|
2980
3380
|
*/
|
|
2981
3381
|
export function operationalMetricsCheck(metrics) {
|
|
2982
3382
|
const captureBacklog = metrics.captureReplay.captureExtraction.pending +
|
|
2983
3383
|
metrics.captureReplay.captureExtraction.running;
|
|
2984
3384
|
const embeddingBacklog = metrics.captureReplay.memoryEmbedding.pending +
|
|
2985
3385
|
metrics.captureReplay.memoryEmbedding.running;
|
|
3386
|
+
const captureDeadLetters = metrics.captureReplay.captureExtraction.dead;
|
|
2986
3387
|
const embeddingDeadLetters = metrics.captureReplay.memoryEmbedding.dead;
|
|
2987
|
-
const deadLetters =
|
|
3388
|
+
const deadLetters = captureDeadLetters + embeddingDeadLetters;
|
|
2988
3389
|
const detectedFailures = metrics.silentFailures.count +
|
|
2989
3390
|
metrics.guard.failures +
|
|
2990
3391
|
deadLetters;
|
|
@@ -2998,11 +3399,44 @@ export function operationalMetricsCheck(metrics) {
|
|
|
2998
3399
|
? "warning"
|
|
2999
3400
|
: "ok",
|
|
3000
3401
|
detail: `${detectedFailures} detected failure(s), ${captureBacklog} capture and ${embeddingBacklog} embedding job(s) in backlog; ${metrics.guard.latency.samples} Guard latency sample(s)` +
|
|
3402
|
+
(captureDeadLetters > 0
|
|
3403
|
+
? `; ${captureDeadLetters} dead capture job(s): once the extraction provider is back, run lore capture repair`
|
|
3404
|
+
: "") +
|
|
3001
3405
|
(embeddingDeadLetters > 0
|
|
3002
3406
|
? `; ${embeddingDeadLetters} dead embedding job(s): fix the embedding provider, then run lore embeddings repair`
|
|
3003
3407
|
: ""),
|
|
3004
3408
|
};
|
|
3005
3409
|
}
|
|
3410
|
+
export const SEMANTIC_SEARCH_WINDOW_DAYS = 7;
|
|
3411
|
+
const SEMANTIC_SEARCH_MINIMUM_ATTEMPTS = 10;
|
|
3412
|
+
/**
|
|
3413
|
+
* Embedding failures over the last week. The server records `semantic_unavailable`
|
|
3414
|
+
* only when embeddings are configured and the request failed, and `lexical_only`
|
|
3415
|
+
* when none are configured, so a self-hosted server without an embedding key
|
|
3416
|
+
* never warns. Context still arrives during an outage, so nothing else shows it.
|
|
3417
|
+
*/
|
|
3418
|
+
export function semanticSearchCheck(retrieval) {
|
|
3419
|
+
const period = `in the last ${SEMANTIC_SEARCH_WINDOW_DAYS} days`;
|
|
3420
|
+
const attempts = retrieval.hybrid + retrieval.semanticUnavailable;
|
|
3421
|
+
const failed = retrieval.semanticUnavailable;
|
|
3422
|
+
if (attempts >= SEMANTIC_SEARCH_MINIMUM_ATTEMPTS && failed * 2 > attempts) {
|
|
3423
|
+
return {
|
|
3424
|
+
name: "semantic-search",
|
|
3425
|
+
status: "warning",
|
|
3426
|
+
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.`,
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
return {
|
|
3430
|
+
name: "semantic-search",
|
|
3431
|
+
status: "ok",
|
|
3432
|
+
detail: attempts === 0
|
|
3433
|
+
? retrieval.lexicalOnly > 0
|
|
3434
|
+
? "keyword search only; embeddings are not configured on the server"
|
|
3435
|
+
: `no prompts with context ${period}`
|
|
3436
|
+
: `semantic search on ${retrieval.hybrid} of ${attempts} prompts ${period}` +
|
|
3437
|
+
(failed > 0 ? `; ${failed} fell back to keywords` : ""),
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3006
3440
|
/**
|
|
3007
3441
|
* Local outbox health. Dead letters that can never be replayed (an old snapshot
|
|
3008
3442
|
* format or a permanent rejection) are a note, not an error: replaying them
|
|
@@ -3079,28 +3513,41 @@ async function apiChecks(config) {
|
|
|
3079
3513
|
: detail,
|
|
3080
3514
|
});
|
|
3081
3515
|
}
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
throw new Error("Operational metrics request failed");
|
|
3092
|
-
}
|
|
3093
|
-
checks.push(operationalMetricsCheck(OperationalMetricsResponseSchema.parse(await response.json())));
|
|
3094
|
-
}
|
|
3095
|
-
catch {
|
|
3096
|
-
checks.push({
|
|
3516
|
+
const now = Date.now();
|
|
3517
|
+
const [daily, weekly] = await Promise.allSettled([
|
|
3518
|
+
// The server's default window is the last 24 hours.
|
|
3519
|
+
operationalMetrics(config),
|
|
3520
|
+
operationalMetrics(config, new Date(now - SEMANTIC_SEARCH_WINDOW_DAYS * 24 * 60 * 60_000)),
|
|
3521
|
+
]);
|
|
3522
|
+
checks.push(daily.status === "fulfilled"
|
|
3523
|
+
? operationalMetricsCheck(daily.value)
|
|
3524
|
+
: {
|
|
3097
3525
|
name: "server-operational-metrics",
|
|
3098
3526
|
status: "warning",
|
|
3099
3527
|
detail: "authenticated operational metrics are unavailable",
|
|
3100
3528
|
});
|
|
3529
|
+
// Without the weekly metrics, the warning above already says they are unavailable.
|
|
3530
|
+
if (weekly.status === "fulfilled") {
|
|
3531
|
+
checks.push(semanticSearchCheck(weekly.value.retrieval));
|
|
3101
3532
|
}
|
|
3102
3533
|
return checks;
|
|
3103
3534
|
}
|
|
3535
|
+
async function operationalMetrics(config, from) {
|
|
3536
|
+
const url = new URL(`${config.apiUrl}/v1/reliability/metrics`);
|
|
3537
|
+
if (from !== undefined)
|
|
3538
|
+
url.searchParams.set("from", from.toISOString());
|
|
3539
|
+
const response = await fetch(url, {
|
|
3540
|
+
headers: {
|
|
3541
|
+
authorization: `Bearer ${config.token}`,
|
|
3542
|
+
"user-agent": `lore-cli/${LORE_VERSION}`,
|
|
3543
|
+
},
|
|
3544
|
+
signal: AbortSignal.timeout(3_000),
|
|
3545
|
+
});
|
|
3546
|
+
if (!response.ok) {
|
|
3547
|
+
throw new Error("Operational metrics request failed");
|
|
3548
|
+
}
|
|
3549
|
+
return OperationalMetricsResponseSchema.parse(await response.json());
|
|
3550
|
+
}
|
|
3104
3551
|
async function verifyConnectedInstallation() {
|
|
3105
3552
|
const paths = getLorePaths();
|
|
3106
3553
|
const config = await readConnectorConfig(paths);
|
|
@@ -3119,6 +3566,7 @@ function formatCaptureResult(result) {
|
|
|
3119
3566
|
const lines = [
|
|
3120
3567
|
`capture: ${captureSummary(result.capture, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })}`,
|
|
3121
3568
|
...(result.capture === "on" && pausedBy !== null ? [captureResumeHint(pausedBy)] : []),
|
|
3569
|
+
...(result.recapReminder === undefined ? [] : [result.recapReminder === "on" ? "recap reminder: on; lore capture reminder off stops it" : "recap reminder: off"]),
|
|
3122
3570
|
...(result.changed
|
|
3123
3571
|
? [
|
|
3124
3572
|
`updated: ${result.configFiles.join(", ")}`,
|
|
@@ -3146,7 +3594,35 @@ export async function setCaptureMode(capture, paths = getLorePaths()) {
|
|
|
3146
3594
|
const devinFiles = await refreshDevinHooks(paths.home, capture === "on");
|
|
3147
3595
|
return { capture, changed: true, configFiles: [...refreshed?.configFiles ?? [], ...devinFiles] };
|
|
3148
3596
|
}
|
|
3597
|
+
/** The once-per-session /recap reminder agents get while nothing is recorded. Hooks read it on each prompt. */
|
|
3598
|
+
export async function setRecapReminder(reminder, paths = getLorePaths()) {
|
|
3599
|
+
const config = await readConnectorConfig(paths);
|
|
3600
|
+
if (config === null)
|
|
3601
|
+
throw new Error("Lore is not connected. Run lore connect.");
|
|
3602
|
+
if ((config.recapReminder ?? "on") === reminder)
|
|
3603
|
+
return { recapReminder: reminder, changed: false };
|
|
3604
|
+
await withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
|
|
3605
|
+
const { recapReminder: _previous, ...rest } = config;
|
|
3606
|
+
await atomicWriteJson(paths.config, { ...rest, ...(reminder === "off" ? { recapReminder: reminder } : {}) }, 65536);
|
|
3607
|
+
});
|
|
3608
|
+
return { recapReminder: reminder, changed: true };
|
|
3609
|
+
}
|
|
3149
3610
|
async function captureCommand(args) {
|
|
3611
|
+
if (args[0] === "repair") {
|
|
3612
|
+
await runCaptureRepairCommand(args.slice(1), await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
if (args[0] === "reminder") {
|
|
3616
|
+
const json = args.includes("--json");
|
|
3617
|
+
const [reminder, ...extra] = args.slice(1).filter((argument) => argument !== "--json");
|
|
3618
|
+
if ((reminder !== "on" && reminder !== "off") || extra.length > 0)
|
|
3619
|
+
throw new Error("Usage: lore capture reminder off|on [--json]");
|
|
3620
|
+
const result = await setRecapReminder(reminder);
|
|
3621
|
+
writeResult(result, json, reminder === "on"
|
|
3622
|
+
? "recap reminder: on. While nothing is recorded, your agent suggests /recap once in a session where you confirm a decision.\n"
|
|
3623
|
+
: "recap reminder: off. Your agent no longer suggests /recap; you can still run it any time.\n");
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3150
3626
|
let mode;
|
|
3151
3627
|
let json = false;
|
|
3152
3628
|
for (const argument of args) {
|
|
@@ -3162,7 +3638,7 @@ async function captureCommand(args) {
|
|
|
3162
3638
|
mode = argument;
|
|
3163
3639
|
continue;
|
|
3164
3640
|
}
|
|
3165
|
-
throw new Error("Usage: lore capture [status|on|off] [--json]");
|
|
3641
|
+
throw new Error("Usage: lore capture [status|on|off|reminder off|reminder on|repair] [--json]");
|
|
3166
3642
|
}
|
|
3167
3643
|
const paths = getLorePaths();
|
|
3168
3644
|
if (mode === undefined || mode === "status") {
|
|
@@ -3172,7 +3648,8 @@ async function captureCommand(args) {
|
|
|
3172
3648
|
const capture = captureEnabled(config) ? "on" : "off";
|
|
3173
3649
|
// Same reading as lore status and lore doctor: the folder's connection and its dashboard switches.
|
|
3174
3650
|
const switches = capture === "on" ? await captureSwitches(await diagnosticConfig(paths, process.cwd()), paths.home, process.cwd()) : CAPTURE_SWITCHES_UNCHECKED;
|
|
3175
|
-
const
|
|
3651
|
+
const recorded = capture === "on" && switches.pausedBy === null;
|
|
3652
|
+
const result = { capture, pausedBy: switches.pausedBy, ...(recorded ? {} : { recapReminder: config.recapReminder ?? "on" }), changed: false, configFiles: [] };
|
|
3176
3653
|
writeResult(result, json, formatCaptureResult(result));
|
|
3177
3654
|
return;
|
|
3178
3655
|
}
|
|
@@ -3219,6 +3696,8 @@ async function doctorCommand(args) {
|
|
|
3219
3696
|
if (genericChecks.length > 0 && !status.agents.some((agent) => agent.configured)) {
|
|
3220
3697
|
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
3698
|
}
|
|
3699
|
+
if (loreDisabled(process.env))
|
|
3700
|
+
checks.unshift({ name: "lore-disable", status: "warning", detail: LORE_DISABLED_MESSAGE });
|
|
3222
3701
|
checks.push({
|
|
3223
3702
|
name: "platform",
|
|
3224
3703
|
status: platform() === "darwin" || platform() === "linux" || isWindows() ? "ok" : "error",
|
|
@@ -3238,13 +3717,18 @@ async function doctorCommand(args) {
|
|
|
3238
3717
|
checks.push({
|
|
3239
3718
|
name: "config",
|
|
3240
3719
|
status: config === null ? "error" : "ok",
|
|
3241
|
-
detail: config === null ? `
|
|
3242
|
-
});
|
|
3243
|
-
checks.push({
|
|
3244
|
-
name: "config-permissions",
|
|
3245
|
-
status: ["600", "private-acl"].includes(status.configMode ?? "") ? "ok" : "error",
|
|
3246
|
-
detail: status.configMode ?? "missing",
|
|
3720
|
+
detail: config === null ? `not connected (${paths.config} is missing or invalid). Run lore connect --login` : paths.config,
|
|
3247
3721
|
});
|
|
3722
|
+
// Without a config there is nothing to protect yet; the check above says what to do.
|
|
3723
|
+
if (config !== null)
|
|
3724
|
+
checks.push({
|
|
3725
|
+
name: "config-permissions",
|
|
3726
|
+
status: ["600", "private-acl"].includes(status.configMode ?? "") ? "ok" : "error",
|
|
3727
|
+
detail: status.configMode ?? "missing",
|
|
3728
|
+
});
|
|
3729
|
+
// A fresh connection has not been used yet; that is expected for a day, not a problem.
|
|
3730
|
+
const connectedAt = config === null ? Number.NaN : Date.parse(config.connectedAt);
|
|
3731
|
+
const recentlyConnected = Number.isFinite(connectedAt) && Date.now() - connectedAt < 24 * 60 * 60 * 1000;
|
|
3248
3732
|
if (status.runtimeRequired) {
|
|
3249
3733
|
checks.push({
|
|
3250
3734
|
name: "runtime",
|
|
@@ -3272,7 +3756,9 @@ async function doctorCommand(args) {
|
|
|
3272
3756
|
: agent.invocation.state === "failed" ||
|
|
3273
3757
|
agent.invocation.state === "incomplete"
|
|
3274
3758
|
? "error"
|
|
3275
|
-
: "
|
|
3759
|
+
: agent.invocation.state === "never_observed" && recentlyConnected
|
|
3760
|
+
? "ok"
|
|
3761
|
+
: "warning",
|
|
3276
3762
|
detail: agent.invocationError !== null
|
|
3277
3763
|
? agent.invocationError === "runtime_version_mismatch"
|
|
3278
3764
|
? `observed Lore ${agent.invocation.runtimeVersion}; expected ${LORE_VERSION}`
|
|
@@ -3284,7 +3770,9 @@ async function doctorCommand(args) {
|
|
|
3284
3770
|
: agent.invocation.state === "incomplete"
|
|
3285
3771
|
? `last invocation did not complete (${agent.invocation.lastAttemptedAt})`
|
|
3286
3772
|
: agent.invocation.state === "never_observed"
|
|
3287
|
-
?
|
|
3773
|
+
? recentlyConnected
|
|
3774
|
+
? "installed, not used yet"
|
|
3775
|
+
: `installed, but not used since connecting. Start a session in ${agentDisplayName(agent.agent)}, then run lore doctor again`
|
|
3288
3776
|
: "invocation health is unavailable",
|
|
3289
3777
|
});
|
|
3290
3778
|
if (agent.agent === "codex" && agent.executable) {
|
|
@@ -3308,23 +3796,28 @@ async function doctorCommand(args) {
|
|
|
3308
3796
|
detail: "surface-specific repository configuration; local installation is not claimed",
|
|
3309
3797
|
});
|
|
3310
3798
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
status.
|
|
3320
|
-
|
|
3321
|
-
|
|
3799
|
+
if (config !== null)
|
|
3800
|
+
checks.push({
|
|
3801
|
+
name: "reliability-store",
|
|
3802
|
+
status: status.reliabilityError !== null
|
|
3803
|
+
? "error"
|
|
3804
|
+
: status.reliability === null
|
|
3805
|
+
? "warning"
|
|
3806
|
+
: "ok",
|
|
3807
|
+
detail: status.reliabilityError ??
|
|
3808
|
+
status.reliability?.directory ??
|
|
3809
|
+
"not initialized",
|
|
3810
|
+
});
|
|
3322
3811
|
if (status.reliability !== null) {
|
|
3323
3812
|
checks.push(durableOutboxCheck(status.reliability.outbox, prunedDeadLetters));
|
|
3813
|
+
// Keys are pinned on the first delivery, so an unused connection has none yet.
|
|
3814
|
+
const unused = status.agents.every((agent) => !agent.configured || agent.invocation.state === "never_observed");
|
|
3324
3815
|
checks.push({
|
|
3325
3816
|
name: "verified-snapshot-cache",
|
|
3326
|
-
status: status.reliability.trustKeys > 0 ? "ok" : "warning",
|
|
3327
|
-
detail:
|
|
3817
|
+
status: status.reliability.trustKeys > 0 || unused ? "ok" : "warning",
|
|
3818
|
+
detail: status.reliability.trustKeys === 0 && unused
|
|
3819
|
+
? "empty until the first prompt"
|
|
3820
|
+
: `${status.reliability.cache.context} context, ${status.reliability.cache.policy} policy, ${status.reliability.trustKeys} pinned key(s)`,
|
|
3328
3821
|
});
|
|
3329
3822
|
const operational = status.reliability.operational;
|
|
3330
3823
|
const detectedFailures = operational.guard.failures + operational.retrieval.failed;
|
|
@@ -3389,7 +3882,7 @@ async function printCommandHelp(path) {
|
|
|
3389
3882
|
process.stdout.write(DOCTOR_HELP);
|
|
3390
3883
|
return;
|
|
3391
3884
|
case "capture":
|
|
3392
|
-
process.stdout.write(CAPTURE_HELP);
|
|
3885
|
+
process.stdout.write(topic.subcommand === "repair" ? CAPTURE_REPAIR_HELP : CAPTURE_HELP);
|
|
3393
3886
|
return;
|
|
3394
3887
|
case "embeddings":
|
|
3395
3888
|
await runEmbeddingsCommand(args, null);
|
|
@@ -3418,6 +3911,9 @@ async function printCommandHelp(path) {
|
|
|
3418
3911
|
case "remember":
|
|
3419
3912
|
await runRememberCommand(args, null);
|
|
3420
3913
|
return;
|
|
3914
|
+
case "drafts":
|
|
3915
|
+
await runDraftsCommand(args, null);
|
|
3916
|
+
return;
|
|
3421
3917
|
case "approach":
|
|
3422
3918
|
await runApproachCommand(args, null);
|
|
3423
3919
|
return;
|
|
@@ -3512,7 +4008,7 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3512
4008
|
return;
|
|
3513
4009
|
}
|
|
3514
4010
|
const commandArgs = args.slice(1);
|
|
3515
|
-
if (["ask", "chat", "learnings", "review", "remember", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
|
|
4011
|
+
if (["ask", "chat", "learnings", "review", "remember", "drafts", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
|
|
3516
4012
|
const decision = await checkDirectory(process.cwd(), getLorePaths(["approach", "skill"].includes(command) ? process.env.LORE_HOME : undefined).home);
|
|
3517
4013
|
if (!decision.allowed)
|
|
3518
4014
|
throw new Error(`Lore is off in this folder. ${decision.reason}`);
|
|
@@ -3567,15 +4063,20 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3567
4063
|
case "remember":
|
|
3568
4064
|
await runRememberCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3569
4065
|
return;
|
|
4066
|
+
case "drafts":
|
|
4067
|
+
await runDraftsCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
4068
|
+
return;
|
|
3570
4069
|
case "approach":
|
|
3571
4070
|
await runApproachCommand(commandArgs, await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()));
|
|
3572
4071
|
return;
|
|
3573
4072
|
case "skill":
|
|
3574
|
-
await runSkillCommand(commandArgs, commandArgs[0] === "discover" ? null : await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()));
|
|
4073
|
+
await runSkillCommand(commandArgs, commandArgs[0] === "discover" ? null : await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()), getLorePaths(process.env.LORE_HOME).loreDirectory);
|
|
3575
4074
|
return;
|
|
3576
|
-
case "guard":
|
|
3577
|
-
|
|
4075
|
+
case "guard": {
|
|
4076
|
+
const paths = getLorePaths();
|
|
4077
|
+
await runGuardCommand(commandArgs, await readConnectorConfig(paths, process.cwd()), paths.home);
|
|
3578
4078
|
return;
|
|
4079
|
+
}
|
|
3579
4080
|
case "resume":
|
|
3580
4081
|
await runResumeCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()), getLorePaths());
|
|
3581
4082
|
return;
|