@lore-co/cli 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -18
- 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-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 +66 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +547 -91
- 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.map +1 -1
- package/dist/command-help.js +7 -3
- 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/generated-assets.d.ts +8 -8
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +8 -8
- package/dist/generated-assets.js.map +1 -1
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +38 -7
- 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 +8 -1
- package/dist/lore-agent.d.ts.map +1 -1
- package/dist/lore-agent.js +33 -11
- 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.d.ts +30 -0
- package/dist/npm-installation.d.ts.map +1 -0
- package/dist/npm-installation.js +46 -0
- package/dist/npm-installation.js.map +1 -0
- 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.map +1 -1
- package/dist/recovery.js +30 -11
- 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 +2 -2
- 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 +5 -2
- 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 +138 -91
- 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 +2 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +16 -11
- 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 +14 -32
- package/dist/uninstall.js.map +1 -1
- package/dist/update.d.ts +17 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +150 -51
- 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";
|
|
@@ -30,9 +31,10 @@ import { runLearningsCommand } from "./learnings.js";
|
|
|
30
31
|
import { runReviewCommand } from "./review.js";
|
|
31
32
|
import { runEmbeddingsCommand } from "./embeddings.js";
|
|
32
33
|
import { runRememberCommand } from "./remember.js";
|
|
33
|
-
import { installLoreSkill, removeLoreSkill } from "./skills.js";
|
|
34
|
+
import { installLoreSkill, loreSkillFiles, recapSkillFiles, removeLoreSkill } from "./skills.js";
|
|
34
35
|
import { GENERIC_AGENT, genericInstructions, genericMcpServers, parseGenericAgentId, readGenericAgents, recordGenericAgent, removeGenericAgents } from "./generic-mcp.js";
|
|
35
|
-
import { installLoreAgent, removeLoreAgent } from "./lore-agent.js";
|
|
36
|
+
import { installLoreAgent, loreAgentFiles, removeLoreAgent } from "./lore-agent.js";
|
|
37
|
+
import { regularPath } from "./owned-files.js";
|
|
36
38
|
import { runApproachCommand } from "./approach.js";
|
|
37
39
|
import { runSkillCommand } from "./skill-command.js";
|
|
38
40
|
import { runGuardCommand } from "./guard.js";
|
|
@@ -46,10 +48,12 @@ import { runDevinCommand } from "./devin.js";
|
|
|
46
48
|
import { runHostCommand } from "./host.js";
|
|
47
49
|
import { runDemoCommand } from "./demo.js";
|
|
48
50
|
import { updateCommand } from "./update.js";
|
|
51
|
+
import { Output, PLAIN_OUTPUT, detectOutputStyle, listText } from "./terminal-output.js";
|
|
49
52
|
import { UNINSTALL_HELP, uninstallCommand } from "./uninstall.js";
|
|
50
53
|
import { runSelfHostCommand } from "./self-host.js";
|
|
51
54
|
import { ReliabilityStore, ReliabilityStoreError, integrationInvocationStateKey, } from "./reliability-store.js";
|
|
52
55
|
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
56
|
+
import { openNoFollow } from "@lore-co/core/no-follow";
|
|
53
57
|
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
54
58
|
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
55
59
|
export const DEFAULT_HOSTED_API_URL = "https://api.uselore.co";
|
|
@@ -142,6 +146,7 @@ Options:
|
|
|
142
146
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
143
147
|
--fallback <agent> claude, codex, opencode, devin, cursor, copilot-cli, polytoken
|
|
144
148
|
--no-recovery Disable automatic handoff setup (otherwise enabled with two installed clients)
|
|
149
|
+
--verbose Also list the files written and the connection details
|
|
145
150
|
--json Print machine-readable output
|
|
146
151
|
--help Show this command's help
|
|
147
152
|
|
|
@@ -187,15 +192,20 @@ Usage:
|
|
|
187
192
|
lore capture status [--json]
|
|
188
193
|
lore capture off [--json]
|
|
189
194
|
lore capture on [--json]
|
|
195
|
+
lore capture reminder off|on [--json]
|
|
190
196
|
|
|
191
197
|
With capture off, Lore installs no Stop or SessionEnd hooks and never sends a
|
|
192
198
|
prompt or response as a turn. Context injection, Guard, lore ask, lore remember,
|
|
193
199
|
and the lore skill's "remember this session" keep working. Folder rules still
|
|
194
200
|
decide where Lore runs at all.
|
|
195
201
|
|
|
202
|
+
While nothing is recorded, Lore asks your agent to suggest /recap once in a
|
|
203
|
+
session where you confirm a decision. lore capture reminder off stops that.
|
|
204
|
+
|
|
196
205
|
Examples:
|
|
197
206
|
lore capture off
|
|
198
207
|
lore capture status
|
|
208
|
+
lore capture reminder off
|
|
199
209
|
lore capture on
|
|
200
210
|
`;
|
|
201
211
|
const DISCONNECT_HELP = `lore disconnect
|
|
@@ -317,17 +327,25 @@ function stripLoreFromEvent(value) {
|
|
|
317
327
|
}
|
|
318
328
|
return groups;
|
|
319
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Seconds a host waits for a Lore hook before stopping it. On Windows each
|
|
332
|
+
* hook starts PowerShell and lore.exe, and a reply hook took 3.6 s on
|
|
333
|
+
* windows-latest, so the 2 and 3 second limits would drop the capture there.
|
|
334
|
+
*/
|
|
335
|
+
function hookTimeout(seconds) {
|
|
336
|
+
return isWindows() ? Math.max(seconds, 10) : seconds;
|
|
337
|
+
}
|
|
320
338
|
function eventHandler(agent, event, paths) {
|
|
321
339
|
return {
|
|
322
340
|
type: "command",
|
|
323
341
|
command: hookCommand(agent, paths),
|
|
324
|
-
timeout: event === "SessionEnd"
|
|
342
|
+
timeout: hookTimeout(event === "SessionEnd"
|
|
325
343
|
? 2
|
|
326
344
|
: event === "Stop"
|
|
327
345
|
? 3
|
|
328
346
|
: event === "PreToolUse"
|
|
329
347
|
? 10
|
|
330
|
-
: 25,
|
|
348
|
+
: 25),
|
|
331
349
|
...(event === "UserPromptSubmit"
|
|
332
350
|
? { statusMessage: "Loading Lore context" }
|
|
333
351
|
: {}),
|
|
@@ -410,13 +428,13 @@ function cursorEventHandler(event, paths) {
|
|
|
410
428
|
return {
|
|
411
429
|
type: "command",
|
|
412
430
|
command: hookCommand("cursor", paths),
|
|
413
|
-
timeout: event === "sessionEnd"
|
|
431
|
+
timeout: hookTimeout(event === "sessionEnd"
|
|
414
432
|
? 2
|
|
415
433
|
: event === "afterAgentResponse"
|
|
416
434
|
? 3
|
|
417
435
|
: event === "beforeShellExecution"
|
|
418
436
|
? 10
|
|
419
|
-
: 25,
|
|
437
|
+
: 25),
|
|
420
438
|
};
|
|
421
439
|
}
|
|
422
440
|
const CURSOR_MCP_NAME = "lore-native";
|
|
@@ -424,7 +442,7 @@ function cursorMcpServer(paths) {
|
|
|
424
442
|
return {
|
|
425
443
|
type: "stdio", command: process.execPath,
|
|
426
444
|
args: [...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)]), "cursor-mcp", "--owner", "lore"],
|
|
427
|
-
env: { LORE_HOME: paths.home },
|
|
445
|
+
env: { LORE_HOME: paths.home, ...CURSOR_LORE_DISABLE_ENV },
|
|
428
446
|
};
|
|
429
447
|
}
|
|
430
448
|
function isLoreCursorMcp(value) {
|
|
@@ -480,6 +498,36 @@ export function mcpConfigPath(agent, hooksPath) {
|
|
|
480
498
|
return resolve(directory, "..", "..", ".vscode", "mcp.json");
|
|
481
499
|
}
|
|
482
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* The hooks and MCP files of an agent that keeps them inside a project, where
|
|
503
|
+
* a repository can commit them; null for agents configured in their own folders.
|
|
504
|
+
*/
|
|
505
|
+
function projectConfigFiles(installation) {
|
|
506
|
+
if (installation.agent !== "copilot-vscode")
|
|
507
|
+
return null;
|
|
508
|
+
return {
|
|
509
|
+
project: resolve(dirname(installation.configPath), "..", ".."),
|
|
510
|
+
files: [installation.configPath, mcpConfigPath(installation.agent, installation.configPath)],
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/** The first project config file that is, or sits in a folder that is, a symlink. */
|
|
514
|
+
async function linkedProjectConfigFile(installation, paths) {
|
|
515
|
+
const project = projectConfigFiles(installation);
|
|
516
|
+
if (project === null)
|
|
517
|
+
return null;
|
|
518
|
+
for (const file of project.files) {
|
|
519
|
+
// A link could point at any file of the user's, such as Lore's own token.
|
|
520
|
+
if (!await regularPath(file, paths.home, project.project))
|
|
521
|
+
return file;
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
async function assertProjectConfigNotLinked(installation, paths) {
|
|
526
|
+
const file = await linkedProjectConfigFile(installation, paths);
|
|
527
|
+
if (file !== null) {
|
|
528
|
+
throw new Error(`Refusing to edit ${file}: it or a folder above it in the project is a symlink. Replace the link with a regular file, then run this again.`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
483
531
|
export function mergeLoreMcp(input, agent, paths) {
|
|
484
532
|
const config = objectConfiguration(input, agent);
|
|
485
533
|
// VS Code keeps its servers under `servers`; the other JSON hosts use `mcpServers`.
|
|
@@ -563,13 +611,13 @@ function copilotCliEventHandler(event, paths) {
|
|
|
563
611
|
return {
|
|
564
612
|
type: "command",
|
|
565
613
|
[isWindows() ? "powershell" : "bash"]: hookCommand("copilot-cli", paths, event),
|
|
566
|
-
timeoutSec: event === "sessionEnd"
|
|
614
|
+
timeoutSec: hookTimeout(event === "sessionEnd"
|
|
567
615
|
? 2
|
|
568
616
|
: event === "agentStop"
|
|
569
617
|
? 3
|
|
570
618
|
: event === "preToolUse"
|
|
571
619
|
? 10
|
|
572
|
-
: 25,
|
|
620
|
+
: 25),
|
|
573
621
|
};
|
|
574
622
|
}
|
|
575
623
|
function isLoreCopilotHook(value) {
|
|
@@ -659,13 +707,13 @@ function copilotVscodeEventHandler(event, paths) {
|
|
|
659
707
|
return {
|
|
660
708
|
type: "command",
|
|
661
709
|
command: hookCommand("copilot-vscode", paths, event),
|
|
662
|
-
timeout: event === "SessionEnd"
|
|
710
|
+
timeout: hookTimeout(event === "SessionEnd"
|
|
663
711
|
? 2
|
|
664
712
|
: event === "Stop"
|
|
665
713
|
? 3
|
|
666
714
|
: event === "PreToolUse"
|
|
667
715
|
? 10
|
|
668
|
-
: 25,
|
|
716
|
+
: 25),
|
|
669
717
|
};
|
|
670
718
|
}
|
|
671
719
|
export function mergeLoreCopilotVscodeHooks(input, paths, capture = true) {
|
|
@@ -993,6 +1041,7 @@ async function readJsonDocument(path) {
|
|
|
993
1041
|
return {
|
|
994
1042
|
exists: true,
|
|
995
1043
|
value: parsed,
|
|
1044
|
+
raw,
|
|
996
1045
|
mode: metadata.mode & 0o777,
|
|
997
1046
|
};
|
|
998
1047
|
}
|
|
@@ -1032,7 +1081,7 @@ async function writeMergedJson(path, document, value, now) {
|
|
|
1032
1081
|
let backup;
|
|
1033
1082
|
if (document.exists) {
|
|
1034
1083
|
backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
|
|
1035
|
-
await
|
|
1084
|
+
await writeFile(backup, document.raw ?? "", { flag: "wx", mode: document.mode });
|
|
1036
1085
|
}
|
|
1037
1086
|
await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`, document.mode);
|
|
1038
1087
|
return {
|
|
@@ -1065,7 +1114,7 @@ async function writeMergedText(path, document, value, now) {
|
|
|
1065
1114
|
let backup;
|
|
1066
1115
|
if (document.exists) {
|
|
1067
1116
|
backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
|
|
1068
|
-
await
|
|
1117
|
+
await writeFile(backup, document.value, { flag: "wx", mode: document.mode });
|
|
1069
1118
|
}
|
|
1070
1119
|
await atomicWrite(path, value, document.mode);
|
|
1071
1120
|
return {
|
|
@@ -1133,6 +1182,7 @@ function parseConnectorConfig(value) {
|
|
|
1133
1182
|
? {}
|
|
1134
1183
|
: { agentConfigPaths }),
|
|
1135
1184
|
...(value.capture === "off" ? { capture: "off" } : {}),
|
|
1185
|
+
...(value.recapReminder === "off" ? { recapReminder: "off" } : {}),
|
|
1136
1186
|
connectedAt: value.connectedAt,
|
|
1137
1187
|
timeoutMs,
|
|
1138
1188
|
};
|
|
@@ -1600,9 +1650,13 @@ async function installRuntime(paths) {
|
|
|
1600
1650
|
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
1601
1651
|
rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
|
|
1602
1652
|
rm(resolve(dirname(paths.runtime), "capture-conflict.js"), { force: true }),
|
|
1653
|
+
rm(resolve(dirname(paths.runtime), "skill-context.js"), { force: true }),
|
|
1654
|
+
rm(resolve(dirname(paths.runtime), "learning-line.js"), { force: true }),
|
|
1603
1655
|
rm(resolve(dirname(paths.runtime), "image-bytes.js"), { force: true }),
|
|
1604
1656
|
rm(resolve(dirname(paths.runtime), "image-capture.js"), { force: true }),
|
|
1605
1657
|
rm(resolve(dirname(paths.runtime), "retained-context.js"), { force: true }),
|
|
1658
|
+
rm(resolve(dirname(paths.runtime), "no-follow.js"), { force: true }),
|
|
1659
|
+
rm(resolve(dirname(paths.runtime), "system-command.js"), { force: true }),
|
|
1606
1660
|
rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
|
|
1607
1661
|
rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
|
|
1608
1662
|
rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
|
|
@@ -1620,7 +1674,10 @@ async function installRuntime(paths) {
|
|
|
1620
1674
|
const canonicalJsonModule = fileURLToPath(import.meta.resolve("@lore-co/core/canonical-json"));
|
|
1621
1675
|
const guardEvaluation = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/guard-evaluation")), "utf8");
|
|
1622
1676
|
const imageBytes = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/image-bytes")), "utf8");
|
|
1623
|
-
|
|
1677
|
+
// Hook modules refuse symbolic links through this dependency-free module.
|
|
1678
|
+
const noFollow = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/no-follow")), "utf8");
|
|
1679
|
+
const localNoFollow = (source) => source.replaceAll("@lore-co/core/no-follow", "./no-follow.js");
|
|
1680
|
+
const imageCapture = localNoFollow((await readFile(resolve(sourceDirectory, "image-capture.js"), "utf8")).replaceAll("@lore-co/core/image-bytes", "./image-bytes.js"));
|
|
1624
1681
|
const [runtime, repository, reliabilityStore, signedSnapshot, canonicalJson, contextFallback, invocationHealthWriter,] = await Promise.all([
|
|
1625
1682
|
readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
|
|
1626
1683
|
readFile(fileURLToPath(import.meta.resolve("@lore-co/core/repository-location")), "utf8"),
|
|
@@ -1636,12 +1693,19 @@ async function installRuntime(paths) {
|
|
|
1636
1693
|
}
|
|
1637
1694
|
const runtimeVersion = `export const RUNTIME_VERSION = ${JSON.stringify(LORE_VERSION)};\n`;
|
|
1638
1695
|
const runtimeFiles = {
|
|
1639
|
-
"lore-hook.mjs": runtime.replaceAll("@lore-co/core/guard-evaluation", "./guard-evaluation.js").replaceAll("@lore-co/core/capture-conflict", "./capture-conflict.js"),
|
|
1696
|
+
"lore-hook.mjs": runtime.replaceAll("@lore-co/core/guard-evaluation", "./guard-evaluation.js").replaceAll("@lore-co/core/capture-conflict", "./capture-conflict.js").replaceAll("@lore-co/core/learning-line", "./learning-line.js").replaceAll("@lore-co/core/skill-context", "./skill-context.js").replaceAll("@lore-co/core/system-command", "./system-command.js"),
|
|
1640
1697
|
"guard-evaluation.js": guardEvaluation,
|
|
1641
1698
|
"capture-conflict.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/capture-conflict")), "utf8"),
|
|
1699
|
+
// Fits a redacted skill pack back within its cap; it has no imports.
|
|
1700
|
+
"skill-context.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/skill-context")), "utf8"),
|
|
1701
|
+
// Formats Guard items the way every integration does; imports only repository-address.js.
|
|
1702
|
+
"learning-line.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/learning-line")), "utf8"),
|
|
1642
1703
|
"image-bytes.js": imageBytes,
|
|
1643
1704
|
"image-capture.js": imageCapture,
|
|
1644
|
-
"retained-context.js": await readFile(resolve(sourceDirectory, "retained-context.js"), "utf8"),
|
|
1705
|
+
"retained-context.js": localNoFollow(await readFile(resolve(sourceDirectory, "retained-context.js"), "utf8")),
|
|
1706
|
+
"no-follow.js": noFollow,
|
|
1707
|
+
// repository.js, directory-policy.js and the hook start git through this dependency-free module.
|
|
1708
|
+
"system-command.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/system-command")), "utf8"),
|
|
1645
1709
|
"retrieval-task.js": (await readFile(resolve(sourceDirectory, "retrieval-task.js"), "utf8")).replaceAll("@lore-co/core/user-message", "./user-message.js"),
|
|
1646
1710
|
"runtime-redaction.js": await readFile(resolve(sourceDirectory, "runtime-redaction.js"), "utf8"),
|
|
1647
1711
|
"user-message.js": await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/user-message")), "utf8"),
|
|
@@ -1675,9 +1739,13 @@ async function installRuntime(paths) {
|
|
|
1675
1739
|
await Promise.all([
|
|
1676
1740
|
atomicWrite(resolve(dirname(paths.runtime), "guard-evaluation.js"), guardEvaluation, 0o600),
|
|
1677
1741
|
atomicWrite(resolve(dirname(paths.runtime), "capture-conflict.js"), runtimeFiles["capture-conflict.js"], 0o600),
|
|
1742
|
+
atomicWrite(resolve(dirname(paths.runtime), "skill-context.js"), runtimeFiles["skill-context.js"], 0o600),
|
|
1743
|
+
atomicWrite(resolve(dirname(paths.runtime), "learning-line.js"), runtimeFiles["learning-line.js"], 0o600),
|
|
1678
1744
|
atomicWrite(resolve(dirname(paths.runtime), "image-bytes.js"), imageBytes, 0o600),
|
|
1679
1745
|
atomicWrite(resolve(dirname(paths.runtime), "image-capture.js"), imageCapture, 0o600),
|
|
1680
1746
|
atomicWrite(resolve(dirname(paths.runtime), "retained-context.js"), runtimeFiles["retained-context.js"], 0o600),
|
|
1747
|
+
atomicWrite(resolve(dirname(paths.runtime), "no-follow.js"), noFollow, 0o600),
|
|
1748
|
+
atomicWrite(resolve(dirname(paths.runtime), "system-command.js"), runtimeFiles["system-command.js"], 0o600),
|
|
1681
1749
|
atomicWrite(resolve(dirname(paths.runtime), "retrieval-task.js"), runtimeFiles["retrieval-task.js"], 0o600),
|
|
1682
1750
|
atomicWrite(resolve(dirname(paths.runtime), "runtime-redaction.js"), runtimeFiles["runtime-redaction.js"], 0o600),
|
|
1683
1751
|
atomicWrite(resolve(dirname(paths.runtime), "user-message.js"), runtimeFiles["user-message.js"], 0o600),
|
|
@@ -1721,6 +1789,7 @@ async function prepareAgentEntries(installations, paths, capture) {
|
|
|
1721
1789
|
const mcpDocuments = new Map();
|
|
1722
1790
|
for (const installation of installations) {
|
|
1723
1791
|
const key = installationKey(installation);
|
|
1792
|
+
await assertProjectConfigNotLinked(installation, paths);
|
|
1724
1793
|
const document = await readJsonDocument(installation.configPath);
|
|
1725
1794
|
documents.set(key, document);
|
|
1726
1795
|
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths, capture));
|
|
@@ -1794,7 +1863,24 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
|
|
|
1794
1863
|
const generic = (await readGenericAgents(paths.loreDirectory)).length > 0;
|
|
1795
1864
|
const skills = await installLoreSkill(paths, generic ? [...installations, { agent: GENERIC_AGENT, configPath: paths.config }] : installations);
|
|
1796
1865
|
const agent = await installLoreAgent(paths, installations);
|
|
1866
|
+
const changed = new Set([...written.changedFiles, ...skills.changed, ...agent.changed]);
|
|
1867
|
+
const agents = new Map();
|
|
1868
|
+
for (const installation of installations) {
|
|
1869
|
+
// Every file this agent reads: hooks, MCP config, skills, and the Lore agent.
|
|
1870
|
+
const files = [
|
|
1871
|
+
installation.configPath,
|
|
1872
|
+
...(isMcpHostAgent(installation.agent) ? [mcpConfigPath(installation.agent, installation.configPath)] : []),
|
|
1873
|
+
...loreSkillFiles(paths, [installation]),
|
|
1874
|
+
...recapSkillFiles(paths, [installation]),
|
|
1875
|
+
...loreAgentFiles([installation]).map((entry) => entry.file),
|
|
1876
|
+
];
|
|
1877
|
+
const entry = agents.get(installation.agent) ?? { agent: installation.agent, changed: false, hooksChanged: false };
|
|
1878
|
+
entry.changed ||= files.some((file) => changed.has(file));
|
|
1879
|
+
entry.hooksChanged ||= written.changedFiles.includes(installation.configPath);
|
|
1880
|
+
agents.set(installation.agent, entry);
|
|
1881
|
+
}
|
|
1797
1882
|
return {
|
|
1883
|
+
agents: [...agents.values()],
|
|
1798
1884
|
// Same order the files are written in, so the two update lines match.
|
|
1799
1885
|
configFiles: [
|
|
1800
1886
|
...entries.mcpDocuments.keys(),
|
|
@@ -1802,11 +1888,76 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
|
|
|
1802
1888
|
],
|
|
1803
1889
|
...written,
|
|
1804
1890
|
skillFiles: skills.files,
|
|
1891
|
+
changedSkillFiles: skills.changed,
|
|
1805
1892
|
agentFiles: agent.files,
|
|
1893
|
+
changedAgentFiles: agent.changed,
|
|
1806
1894
|
warnings: [...skills.warnings, ...agent.warnings],
|
|
1807
1895
|
};
|
|
1808
1896
|
});
|
|
1809
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* The refresh after `lore update`, by agent: which agents got new Lore files
|
|
1900
|
+
* and what the customer must do, or that nothing changed. --verbose adds the
|
|
1901
|
+
* per-file report.
|
|
1902
|
+
*/
|
|
1903
|
+
export function formatRefreshSummary(result, style = PLAIN_OUTPUT) {
|
|
1904
|
+
const out = new Output(style);
|
|
1905
|
+
const updated = result.agents.filter((entry) => entry.changed).map((entry) => agentDisplayName(entry.agent));
|
|
1906
|
+
if (updated.length === 0)
|
|
1907
|
+
out.note("Your agents are up to date.");
|
|
1908
|
+
else
|
|
1909
|
+
out.outcome("ok", `Updated Lore for ${listText([...new Set(updated)])}`);
|
|
1910
|
+
if (result.warnings.length)
|
|
1911
|
+
out.blank();
|
|
1912
|
+
for (const warning of result.warnings)
|
|
1913
|
+
out.outcome("warning", tildePaths(warning, style.home));
|
|
1914
|
+
out.steps("Next", [
|
|
1915
|
+
...(updated.length ? ["Restart your agents."] : []),
|
|
1916
|
+
...(result.agents.some((entry) => entry.agent === "codex" && entry.hooksChanged) ? [`In Codex, approve Lore's updated hooks with ${out.code("/hooks")}.`] : []),
|
|
1917
|
+
]);
|
|
1918
|
+
if (style.verbose) {
|
|
1919
|
+
out.blank().verbatim(out.bold("Details"));
|
|
1920
|
+
for (const line of formatRefreshReport(result).trimEnd().split("\n")) {
|
|
1921
|
+
if (!line.startsWith("warning: "))
|
|
1922
|
+
out.text(tildePaths(line, style.home));
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
return out.toString();
|
|
1926
|
+
}
|
|
1927
|
+
/** What a refresh rewrote and what was already at this version, one line each. */
|
|
1928
|
+
export function formatRefreshReport(result) {
|
|
1929
|
+
const lines = [
|
|
1930
|
+
result.changedFiles.length > 0
|
|
1931
|
+
? `Refreshed Lore entries in ${result.changedFiles.join(", ")}`
|
|
1932
|
+
: `Lore entries already current in ${result.configFiles.join(", ")}`,
|
|
1933
|
+
];
|
|
1934
|
+
const report = (files, changed, one, many) => {
|
|
1935
|
+
const current = files.filter((file) => !changed.includes(file));
|
|
1936
|
+
if (changed.length > 0)
|
|
1937
|
+
lines.push(`Refreshed ${changed.length > 1 ? many : one} at ${changed.join(", ")}`);
|
|
1938
|
+
if (current.length > 0)
|
|
1939
|
+
lines.push(`${current.length > 1 ? many : one} current at ${current.join(", ")}`);
|
|
1940
|
+
};
|
|
1941
|
+
report(result.skillFiles, result.changedSkillFiles, "Lore skill", "Lore skills");
|
|
1942
|
+
report(result.agentFiles, result.changedAgentFiles, "Lore agent", "Lore agents");
|
|
1943
|
+
for (const warning of result.warnings)
|
|
1944
|
+
lines.push(`warning: ${warning}`);
|
|
1945
|
+
return `${lines.join("\n")}\n`;
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* `lore internal refresh-integrations`: the refresh `lore update` runs through
|
|
1949
|
+
* the binary it just installed, so the files written and the "current" checks
|
|
1950
|
+
* come from that version's bundled assets rather than the updater's.
|
|
1951
|
+
*/
|
|
1952
|
+
async function runInternalCommand(args) {
|
|
1953
|
+
if (args.length !== 1 || args[0] !== "refresh-integrations")
|
|
1954
|
+
throw new Error("Usage: lore internal refresh-integrations");
|
|
1955
|
+
const paths = getLorePaths();
|
|
1956
|
+
const refreshed = await refreshAgentEntries(paths);
|
|
1957
|
+
// `lore update` runs this with piped stdout and says how to draw through LORE_OUTPUT_*.
|
|
1958
|
+
if (refreshed !== null)
|
|
1959
|
+
process.stdout.write(formatRefreshSummary(refreshed, detectOutputStyle({ home: paths.home })));
|
|
1960
|
+
}
|
|
1810
1961
|
function countAgentIntegrations(input, agent) {
|
|
1811
1962
|
const target = targetFor(agent);
|
|
1812
1963
|
return target.count(input === undefined ? target.emptyValue() : input);
|
|
@@ -1842,6 +1993,10 @@ function parseConnectArguments(args) {
|
|
|
1842
1993
|
parsed.json = true;
|
|
1843
1994
|
continue;
|
|
1844
1995
|
}
|
|
1996
|
+
if (argument === "--verbose") {
|
|
1997
|
+
parsed.verbose = true;
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
1845
2000
|
if (argument === "--login") {
|
|
1846
2001
|
parsed.login = true;
|
|
1847
2002
|
continue;
|
|
@@ -2074,6 +2229,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
2074
2229
|
agents,
|
|
2075
2230
|
agentConfigPaths,
|
|
2076
2231
|
...(capture ? {} : { capture: "off" }),
|
|
2232
|
+
...(existing?.recapReminder === "off" ? { recapReminder: "off" } : {}),
|
|
2077
2233
|
connectedAt: connection.profiles.find(value => value.connectionId === connection.fields.connectionId)?.connectedAt ?? now.toISOString(),
|
|
2078
2234
|
timeoutMs,
|
|
2079
2235
|
};
|
|
@@ -2146,16 +2302,36 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
2146
2302
|
keptAgents,
|
|
2147
2303
|
alreadyConnected,
|
|
2148
2304
|
...(knownDashboardUrl === undefined ? {} : { dashboardUrl: knownDashboardUrl }),
|
|
2149
|
-
folder: options.cwd ?? process.cwd(),
|
|
2305
|
+
folder: await firstMessageFolder(options.cwd ?? process.cwd(), config.directories, paths.home),
|
|
2150
2306
|
config: paths.config,
|
|
2151
2307
|
changedHookFiles,
|
|
2152
2308
|
backups,
|
|
2153
2309
|
warnings: [...skills.warnings, ...loreAgent.warnings, ...(connection.needsFolders ? [`Lore is off for ${config.connectionName} until you choose where to record. Reconnect with --folder <path> or --all-projects, or run lore config open --connection ${config.connectionName}.`] : []), ...requestedExpansion.warnings, ...hostRecoveryWarnings, ...(codexNeedsApproval
|
|
2154
|
-
? [
|
|
2310
|
+
? [CODEX_APPROVAL_WARNING]
|
|
2155
2311
|
: [])],
|
|
2156
2312
|
...(recoveryNotice ? { recoveryNotice } : {}),
|
|
2157
2313
|
};
|
|
2158
2314
|
}
|
|
2315
|
+
/**
|
|
2316
|
+
* The browser approval link, and whether Lore may open it: only a page on the
|
|
2317
|
+
* dashboard (or, without one, the API server) the person chose. With a
|
|
2318
|
+
* dashboard, only the server's path and query are kept; setting them rather
|
|
2319
|
+
* than resolving a reference keeps a `//host` path on the dashboard.
|
|
2320
|
+
*/
|
|
2321
|
+
export function deviceApprovalUrl(verificationUriComplete, input) {
|
|
2322
|
+
const server = new URL(verificationUriComplete);
|
|
2323
|
+
const url = input.dashboardUrl === undefined ? server : new URL(`${input.dashboardUrl}/`);
|
|
2324
|
+
if (input.dashboardUrl !== undefined) {
|
|
2325
|
+
url.pathname = server.pathname;
|
|
2326
|
+
url.search = server.search;
|
|
2327
|
+
}
|
|
2328
|
+
const expectedOrigin = new URL(input.dashboardUrl ?? input.apiUrl).origin;
|
|
2329
|
+
return {
|
|
2330
|
+
url: url.toString(),
|
|
2331
|
+
trusted: ["http:", "https:"].includes(url.protocol) && url.origin === expectedOrigin,
|
|
2332
|
+
expectedOrigin,
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2159
2335
|
async function requestDeviceAuthorization(input) {
|
|
2160
2336
|
let response;
|
|
2161
2337
|
try {
|
|
@@ -2185,13 +2361,13 @@ async function requestDeviceAuthorization(input) {
|
|
|
2185
2361
|
if (!authorization.success) {
|
|
2186
2362
|
throw new Error("Lore returned an incompatible device authorization response.");
|
|
2187
2363
|
}
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
if (input.openBrowser) {
|
|
2194
|
-
|
|
2364
|
+
const approval = deviceApprovalUrl(authorization.data.verificationUriComplete, input);
|
|
2365
|
+
process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approval.url}\n`);
|
|
2366
|
+
if (input.openBrowser && approval.trusted) {
|
|
2367
|
+
openBrowser(approval.url);
|
|
2368
|
+
}
|
|
2369
|
+
else if (input.openBrowser) {
|
|
2370
|
+
process.stderr.write(`Lore did not open your browser because this link is not on ${approval.expectedOrigin}. Open it yourself if you expected it, or pass --dashboard-url.\n`);
|
|
2195
2371
|
}
|
|
2196
2372
|
const expiresAt = Date.parse(authorization.data.expiresAt);
|
|
2197
2373
|
const intervalMs = authorization.data.intervalSeconds * 1_000;
|
|
@@ -2306,48 +2482,182 @@ export function connectNextStep(agent) {
|
|
|
2306
2482
|
return "Add the MCP entry below to your agent's MCP settings and the instructions to its AGENTS.md (or GEMINI.md or rules file), then restart it.";
|
|
2307
2483
|
}
|
|
2308
2484
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2485
|
+
/** A folder Lore records in: the current one when it is inside the capture folders, else the first capture folder. */
|
|
2486
|
+
export async function firstMessageFolder(cwd, directories, home) {
|
|
2487
|
+
const first = directories?.include[0];
|
|
2488
|
+
if (first === undefined || (await checkDirectoryRules(directories, cwd, home)).allowed)
|
|
2489
|
+
return cwd;
|
|
2490
|
+
const expanded = first.startsWith("~/") || first.startsWith("~\\") ? resolve(home, first.slice(2)) : first;
|
|
2491
|
+
const parts = resolve(expanded).split(sep);
|
|
2492
|
+
const wildcard = parts.findIndex((part) => /[*?]/u.test(part));
|
|
2493
|
+
return wildcard < 0 ? resolve(expanded) : parts.slice(0, wildcard).join(sep) || sep;
|
|
2494
|
+
}
|
|
2495
|
+
const CODEX_APPROVAL_WARNING = "Codex requires hook approval. Open /hooks in Codex and review any new or changed Lore hooks, then start a fresh session. Run lore doctor to verify approval and the first invocation.";
|
|
2496
|
+
/** Product names for human output; --json keeps the agent ids. */
|
|
2497
|
+
export function agentDisplayName(agent) {
|
|
2498
|
+
switch (agent) {
|
|
2499
|
+
case "claude": return "Claude Code";
|
|
2500
|
+
case "codex": return "Codex";
|
|
2501
|
+
case "copilot":
|
|
2502
|
+
case "copilot-cli": return "Copilot CLI";
|
|
2503
|
+
case "copilot-vscode": return "Copilot in VS Code";
|
|
2504
|
+
case "cursor": return "Cursor";
|
|
2505
|
+
case "opencode": return "OpenCode";
|
|
2506
|
+
case "polytoken": return "Polytoken";
|
|
2507
|
+
case "devin": return "Devin CLI";
|
|
2508
|
+
case "cowork": return "Claude Cowork";
|
|
2509
|
+
case "t3code": return "T3 Code";
|
|
2510
|
+
case "generic": return "MCP agent";
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
/** What one connected agent still needs, for its row in the connect summary. */
|
|
2514
|
+
function connectAgentNote(agent, out) {
|
|
2515
|
+
switch (agent) {
|
|
2516
|
+
case "claude": return "ready after restart, allow the lore server when asked";
|
|
2517
|
+
case "codex": return `ready after restart, approve hooks with ${out.code("/hooks")}`;
|
|
2518
|
+
case "cursor": return "ready after restart, enable lore-native and lore";
|
|
2519
|
+
case "copilot-vscode": return "ready after you restart the VS Code session";
|
|
2520
|
+
case "devin": return `run ${out.code("lore devin cli setup")}, then open a new terminal`;
|
|
2521
|
+
case "generic": return "add the MCP entry and instructions below, then restart it";
|
|
2522
|
+
default: return "ready after restart";
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* The human summary of a connect run: the outcome, where Lore records, one
|
|
2527
|
+
* row per agent, and the numbered steps left for the customer. Paths and
|
|
2528
|
+
* per-file detail appear with --verbose; --json prints the result itself.
|
|
2529
|
+
*/
|
|
2530
|
+
export function formatConnectResult(result, options = {}) {
|
|
2531
|
+
const style = options.style ?? { ...PLAIN_OUTPUT, home: dirname(dirname(result.config)) };
|
|
2532
|
+
const out = new Output(style);
|
|
2313
2533
|
const generic = result.genericAgent;
|
|
2534
|
+
const genericName = generic?.id ?? agentDisplayName("generic");
|
|
2535
|
+
const name = (agent) => agent === GENERIC_AGENT ? genericName : agentDisplayName(agent);
|
|
2314
2536
|
// With only an MCP agent connected, nothing on this machine records turns.
|
|
2315
2537
|
const nothingRecords = result.capture.mode === "off" || (generic !== undefined && result.agents.every((agent) => agent === GENERIC_AGENT));
|
|
2316
|
-
const
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2538
|
+
const pausedBy = result.capture.mode === "on" ? options.capturePausedBy ?? null : null;
|
|
2539
|
+
const folders = result.capture.folders;
|
|
2540
|
+
const shownFolders = folders === null ? "all projects the workspace allows"
|
|
2541
|
+
: folders.length === 0 ? "no folders yet"
|
|
2542
|
+
: listText(folders.length > 3 ? [...folders.slice(0, 2).map((folder) => out.path(folder)), `${folders.length - 2} more`] : folders.map((folder) => out.path(folder)));
|
|
2543
|
+
const excluded = result.capture.excludedFolders.length ? `, except ${listText(result.capture.excludedFolders.map((folder) => out.path(folder)))}` : "";
|
|
2544
|
+
const mode = generic !== undefined && nothingRecords && result.capture.mode === "on" ? "MCP only, saves what you ask"
|
|
2545
|
+
: result.capture.mode === "off" ? "capture off"
|
|
2546
|
+
: pausedBy === "workspace" ? "paused by workspace"
|
|
2547
|
+
: pausedBy === "member" ? "paused by your setting"
|
|
2548
|
+
: "capture on";
|
|
2549
|
+
const records = mode === "capture on";
|
|
2550
|
+
out.outcome("ok", result.alreadyConnected
|
|
2551
|
+
? `Already connected to ${result.identity.workspaceName}, nothing changed`
|
|
2552
|
+
: `Connected to ${result.identity.workspaceName}`);
|
|
2553
|
+
out.note(`${records ? "Recording in" : "Lore runs in"} ${shownFolders}${excluded}${out.separator}${mode}`);
|
|
2554
|
+
if (generic !== undefined && !nothingRecords)
|
|
2555
|
+
out.note(`${generic.id} is MCP-only: Lore saves only what you ask it to.`);
|
|
2556
|
+
// One row per agent: set up in this run first, then the ones kept as they were.
|
|
2557
|
+
const rows = [];
|
|
2558
|
+
const seen = new Set();
|
|
2559
|
+
for (const [agent, mark, note] of [
|
|
2560
|
+
...result.installedAgents.map((agent) => [agent, out.mark("ok"), connectAgentNote(agent, out)]),
|
|
2561
|
+
...result.keptAgents.map((agent) => [agent, "", out.dim("already set up")]),
|
|
2562
|
+
]) {
|
|
2563
|
+
if (seen.has(name(agent)))
|
|
2564
|
+
continue;
|
|
2565
|
+
seen.add(name(agent));
|
|
2566
|
+
rows.push([name(agent), mark, note]);
|
|
2567
|
+
}
|
|
2568
|
+
if (rows.length) {
|
|
2569
|
+
out.blank();
|
|
2570
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
2571
|
+
for (const [label, mark, note] of rows)
|
|
2572
|
+
out.row(label, width, note, mark);
|
|
2573
|
+
}
|
|
2574
|
+
const steps = [];
|
|
2575
|
+
const restart = result.installedAgents.filter((agent) => agent !== "devin" && agent !== GENERIC_AGENT);
|
|
2576
|
+
if (restart.length) {
|
|
2577
|
+
steps.push(restart.length === rows.length ? "Restart the agents above." : `Restart ${listText([...new Set(restart.map(name))])}.`);
|
|
2578
|
+
}
|
|
2579
|
+
if (result.installedAgents.includes("devin"))
|
|
2580
|
+
steps.push(`Run ${out.code("lore devin cli setup")}, then open a new terminal.`);
|
|
2581
|
+
if (generic !== undefined)
|
|
2582
|
+
steps.push(`Add the MCP entry and instructions below to ${generic.id}, then restart it.`);
|
|
2583
|
+
const warnings = result.warnings.filter((warning) => warning !== CODEX_APPROVAL_WARNING);
|
|
2584
|
+
if (warnings.length !== result.warnings.length)
|
|
2585
|
+
steps.push(`In Codex, approve Lore's updated hooks with ${out.code("/hooks")}.`);
|
|
2586
|
+
const notes = [];
|
|
2587
|
+
for (const line of result.recoveryNotice?.split("\n") ?? []) {
|
|
2588
|
+
const shell = /^Open a new terminal or run: (.+)$/u.exec(line);
|
|
2589
|
+
if (!result.recoveryNotice.startsWith("Recovery enabled:"))
|
|
2590
|
+
warnings.push(line);
|
|
2591
|
+
else if (shell)
|
|
2592
|
+
steps.push(`Open a new terminal, or run ${out.code(shell[1])}.`);
|
|
2593
|
+
else if (line.startsWith("Codex requires one-time approval")) {
|
|
2594
|
+
if (!result.installedAgents.includes("codex"))
|
|
2595
|
+
steps.push(`In Codex, approve Lore's hooks with ${out.code("/hooks")}.`);
|
|
2596
|
+
}
|
|
2597
|
+
else
|
|
2598
|
+
notes.push(style.rich ? line : line.replaceAll("→", "->"));
|
|
2599
|
+
}
|
|
2600
|
+
const folder = out.path(result.folder);
|
|
2601
|
+
const ask = "\"Lore, has anything like this come up before?\"";
|
|
2602
|
+
if (folders === null || folders.length > 0) {
|
|
2603
|
+
steps.push(generic !== undefined && nothingRecords
|
|
2604
|
+
? `In ${folder}, ask your agent ${ask}, then run ${out.code("lore doctor")}.`
|
|
2605
|
+
: !records
|
|
2606
|
+
? `In ${folder}, ask your agent anything, then run ${out.code("lore doctor")} to check that context arrives.`
|
|
2331
2607
|
: result.dashboardUrl === undefined
|
|
2332
|
-
? `
|
|
2333
|
-
: `
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2608
|
+
? `In ${folder}, ask your agent anything, then run ${out.code("lore doctor")} to confirm recording.`
|
|
2609
|
+
: `In ${folder}, ask your agent anything, then check ${out.code(`${result.dashboardUrl}/connect`)}.`);
|
|
2610
|
+
}
|
|
2611
|
+
if (notes.length)
|
|
2612
|
+
out.blank();
|
|
2613
|
+
for (const note of notes)
|
|
2614
|
+
out.note(note);
|
|
2615
|
+
if (warnings.length)
|
|
2616
|
+
out.blank();
|
|
2617
|
+
for (const warning of warnings)
|
|
2618
|
+
out.outcome("warning", tildePaths(warning, style.home));
|
|
2619
|
+
out.steps("Next", steps);
|
|
2620
|
+
const recap = result.skillFiles.some((file) => basename(dirname(file)) === "recap");
|
|
2621
|
+
if (result.skillFiles.length || result.agentFiles.length) {
|
|
2622
|
+
out.blank().text(`Try: ${ask}${recap ? `${style.rich ? " · " : " or "}${out.code("/recap")} to save a session` : ""}`, 0);
|
|
2623
|
+
}
|
|
2624
|
+
if (style.verbose) {
|
|
2625
|
+
const details = [
|
|
2626
|
+
["Workspace", `${result.identity.workspaceName} (${result.identity.workspaceId})`],
|
|
2627
|
+
["API", result.apiUrl],
|
|
2628
|
+
...(result.dashboardUrl === undefined ? [] : [["Dashboard", result.dashboardUrl]]),
|
|
2629
|
+
["Config", out.path(result.config)],
|
|
2630
|
+
["Capture", captureSummary(result.capture.mode, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })],
|
|
2631
|
+
["Folders", folders === null ? "all projects allowed by the workspace" : folders.map((value) => out.path(value)).join(", ") || "none"],
|
|
2632
|
+
...(result.capture.excludedFolders.length ? [["Excluded", result.capture.excludedFolders.map((value) => out.path(value)).join(", ")]] : []),
|
|
2633
|
+
["Agents", result.agents.join(", ")],
|
|
2634
|
+
...(result.keptAgents.length ? [["Kept", result.keptAgents.join(", ")]] : []),
|
|
2635
|
+
["Changed", result.changedHookFiles.map((file) => out.path(file)).join(", ") || "nothing"],
|
|
2636
|
+
...(result.backups.length ? [["Backups", result.backups.map((file) => out.path(file)).join(", ")]] : []),
|
|
2637
|
+
...(result.skillFiles.length ? [["Skills", result.skillFiles.map((file) => out.path(file)).join(", ")]] : []),
|
|
2638
|
+
...(result.agentFiles.length ? [["Lore agent", result.agentFiles.map((file) => out.path(file)).join(", ")]] : []),
|
|
2639
|
+
];
|
|
2640
|
+
out.blank().verbatim(out.bold("Details"));
|
|
2641
|
+
const width = Math.max(...details.map(([label]) => label.length));
|
|
2642
|
+
for (const [label, value] of details)
|
|
2643
|
+
out.row(label, width, value);
|
|
2644
|
+
}
|
|
2645
|
+
if (generic !== undefined) {
|
|
2646
|
+
out.blank().note(generic.writeTools
|
|
2647
|
+
? `Saves from ${generic.id} are proposals credited to ${generic.id}; review them in Learnings.`
|
|
2648
|
+
: `${generic.id} can read Lore; it saves through the lore CLI. Reconnect with --write to let it save through MCP (saves stay proposals).`, 0);
|
|
2649
|
+
// Copied into the agent's own settings: printed exactly, never styled or wrapped.
|
|
2650
|
+
out.blank().verbatim(`MCP entry for ${generic.id} (merge into its mcpServers settings):`)
|
|
2651
|
+
.verbatim(JSON.stringify({ mcpServers: generic.mcpServers }, null, 2))
|
|
2652
|
+
.blank().verbatim("Instructions for AGENTS.md, GEMINI.md, or the agent's rules file:")
|
|
2653
|
+
.verbatim(generic.instructions);
|
|
2654
|
+
}
|
|
2655
|
+
return out.toString();
|
|
2656
|
+
}
|
|
2657
|
+
/** Show the home folder as ~ inside a message that names paths. */
|
|
2658
|
+
function tildePaths(text, home) {
|
|
2659
|
+
const prefix = home.endsWith(sep) ? home : `${home}${sep}`;
|
|
2660
|
+
return text.replaceAll(prefix, `~${sep}`);
|
|
2351
2661
|
}
|
|
2352
2662
|
async function connectCommand(args) {
|
|
2353
2663
|
const parsed = parseConnectArguments(args);
|
|
@@ -2404,7 +2714,18 @@ async function connectCommand(args) {
|
|
|
2404
2714
|
});
|
|
2405
2715
|
if (authorization)
|
|
2406
2716
|
await acknowledgeDeviceAuthorization(apiUrl, authorization);
|
|
2407
|
-
|
|
2717
|
+
if (parsed.json) {
|
|
2718
|
+
writeResult(result, true, "");
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
// The summary says when a dashboard switch pauses recording; the policy read is best-effort.
|
|
2722
|
+
const capturePausedBy = result.capture.mode === "on"
|
|
2723
|
+
? (await checkWorkspaceRepository({ apiUrl: result.apiUrl, token, timeoutMs: parsed.timeoutMs ?? 2_500 }, undefined).catch(() => undefined))?.capturePausedBy ?? null
|
|
2724
|
+
: null;
|
|
2725
|
+
process.stdout.write(formatConnectResult(result, {
|
|
2726
|
+
style: detectOutputStyle({ verbose: parsed.verbose === true, home: getLorePaths().home }),
|
|
2727
|
+
capturePausedBy,
|
|
2728
|
+
}));
|
|
2408
2729
|
}
|
|
2409
2730
|
async function queueCount(paths) {
|
|
2410
2731
|
try {
|
|
@@ -2467,7 +2788,7 @@ async function inspectReliabilityStore(config, paths) {
|
|
|
2467
2788
|
const MAX_RUNTIME_MODULE_BYTES = 2 * 1024 * 1024;
|
|
2468
2789
|
const MAX_RUNTIME_MANIFEST_BYTES = 64 * 1024;
|
|
2469
2790
|
async function readRuntimeFile(path, maxBytes, executable = false) {
|
|
2470
|
-
const handle = await
|
|
2791
|
+
const handle = await openNoFollow(path, fsConstants.O_RDONLY);
|
|
2471
2792
|
try {
|
|
2472
2793
|
const metadata = await handle.stat();
|
|
2473
2794
|
if (!metadata.isFile() ||
|
|
@@ -2510,9 +2831,13 @@ async function inspectRuntimeInstallation(required, paths) {
|
|
|
2510
2831
|
"lore-hook.mjs": paths.runtime,
|
|
2511
2832
|
"guard-evaluation.js": resolve(dirname(paths.runtime), "guard-evaluation.js"),
|
|
2512
2833
|
"capture-conflict.js": resolve(dirname(paths.runtime), "capture-conflict.js"),
|
|
2834
|
+
"skill-context.js": resolve(dirname(paths.runtime), "skill-context.js"),
|
|
2835
|
+
"learning-line.js": resolve(dirname(paths.runtime), "learning-line.js"),
|
|
2513
2836
|
"image-bytes.js": resolve(dirname(paths.runtime), "image-bytes.js"),
|
|
2514
2837
|
"image-capture.js": resolve(dirname(paths.runtime), "image-capture.js"),
|
|
2515
2838
|
"retained-context.js": resolve(dirname(paths.runtime), "retained-context.js"),
|
|
2839
|
+
"no-follow.js": resolve(dirname(paths.runtime), "no-follow.js"),
|
|
2840
|
+
"system-command.js": resolve(dirname(paths.runtime), "system-command.js"),
|
|
2516
2841
|
"retrieval-task.js": resolve(dirname(paths.runtime), "retrieval-task.js"),
|
|
2517
2842
|
"runtime-redaction.js": resolve(dirname(paths.runtime), "runtime-redaction.js"),
|
|
2518
2843
|
"user-message.js": resolve(dirname(paths.runtime), "user-message.js"),
|
|
@@ -2631,6 +2956,7 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
2631
2956
|
const units = [];
|
|
2632
2957
|
for (const [index, path] of configPaths.entries()) {
|
|
2633
2958
|
try {
|
|
2959
|
+
await assertProjectConfigNotLinked({ agent, configPath: path }, paths);
|
|
2634
2960
|
const document = await readJsonDocument(path);
|
|
2635
2961
|
installed += countAgentIntegrations(document.value, agent);
|
|
2636
2962
|
existing += document.exists ? 1 : 0;
|
|
@@ -2785,12 +3111,60 @@ function formatRetrievalFailureDiagnostics(retrieval) {
|
|
|
2785
3111
|
return (`local_retrieval_failure_reasons: ${reasons.length === 0 ? "-" : reasons.join(" ")}\n` +
|
|
2786
3112
|
`last_retrieval_failure: ${last === null ? "-" : `${last.reason} (${last.path} request, agent ${last.agent}, at ${last.at})`}\n`);
|
|
2787
3113
|
}
|
|
3114
|
+
/**
|
|
3115
|
+
* The plain lines at the top of `lore status`: live context over the last
|
|
3116
|
+
* days on this machine, then a server behind this CLI and out-of-date Lore
|
|
3117
|
+
* files, each only when true.
|
|
3118
|
+
*/
|
|
3119
|
+
function statusHealth(data, serverVersion) {
|
|
3120
|
+
const context = data.reliability?.operational.retrieval.recent ?? null;
|
|
3121
|
+
const staleFiles = [];
|
|
3122
|
+
if (data.runtimeRequired && !data.runtimeInstalled && data.runtimeVersion !== null) {
|
|
3123
|
+
staleFiles.push({ name: "Lore hooks", version: data.runtimeVersion === LORE_VERSION ? null : data.runtimeVersion });
|
|
3124
|
+
}
|
|
3125
|
+
for (const agent of data.agents) {
|
|
3126
|
+
// Hook agents share the runtime above; a plugin carries its own copy.
|
|
3127
|
+
if (agent.installation.runtime !== "stale" || targetFor(agent.agent).runtimeRequired)
|
|
3128
|
+
continue;
|
|
3129
|
+
staleFiles.push({ name: `${agentDisplayName(agent.agent)} plugin`, version: agent.installation.runtimeVersion });
|
|
3130
|
+
}
|
|
3131
|
+
const lines = [
|
|
3132
|
+
contextHealthLine(context ?? undefined),
|
|
3133
|
+
serverBehindLine(serverVersion, LORE_VERSION),
|
|
3134
|
+
staleFilesLine(staleFiles),
|
|
3135
|
+
].filter((line) => line !== null);
|
|
3136
|
+
return { context, serverVersion, staleFiles, lines };
|
|
3137
|
+
}
|
|
3138
|
+
/** The server's release, from the identity endpoint; null when unreachable. */
|
|
3139
|
+
async function connectedServerVersion(config) {
|
|
3140
|
+
try {
|
|
3141
|
+
const response = await fetch(`${config.apiUrl}/v1/workspace/identity`, {
|
|
3142
|
+
headers: { accept: "application/json", authorization: `Bearer ${config.token}`, "x-lore-connection-identity": "1" },
|
|
3143
|
+
signal: AbortSignal.timeout(Math.min(config.timeoutMs ?? 2_000, 2_000)),
|
|
3144
|
+
});
|
|
3145
|
+
if (!response.ok)
|
|
3146
|
+
return null;
|
|
3147
|
+
const parsed = WorkspaceIdentityResponseSchema.safeParse(await response.json());
|
|
3148
|
+
return parsed.success ? parsed.data.server.version : null;
|
|
3149
|
+
}
|
|
3150
|
+
catch {
|
|
3151
|
+
return null;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
2788
3154
|
async function statusCommand(args) {
|
|
2789
3155
|
const parsed = parseOutputArguments(args, STATUS_HELP, "status");
|
|
2790
3156
|
if (parsed === null) {
|
|
2791
3157
|
return;
|
|
2792
3158
|
}
|
|
2793
|
-
const
|
|
3159
|
+
const paths = getLorePaths();
|
|
3160
|
+
const [statusResult, serverVersion] = await Promise.all([
|
|
3161
|
+
statusData(paths, process.cwd()),
|
|
3162
|
+
diagnosticConfig(paths, process.cwd()).then((config) => config === null ? null : connectedServerVersion(config)),
|
|
3163
|
+
]);
|
|
3164
|
+
const health = statusHealth(statusResult, serverVersion);
|
|
3165
|
+
const data = { ...statusResult, health };
|
|
3166
|
+
if (!parsed.json && loreDisabled(process.env))
|
|
3167
|
+
process.stdout.write(`${LORE_DISABLED_MESSAGE}\n`);
|
|
2794
3168
|
if (!parsed.json && data.connection)
|
|
2795
3169
|
process.stdout.write(`connection: ${data.connection}\n`);
|
|
2796
3170
|
const agentLines = data.agents
|
|
@@ -2803,7 +3177,7 @@ async function statusCommand(args) {
|
|
|
2803
3177
|
? "-"
|
|
2804
3178
|
: `${(value * 100).toFixed(1)}%`;
|
|
2805
3179
|
const retrievalFailureLines = formatRetrievalFailureDiagnostics(data.reliability?.operational.retrieval);
|
|
2806
|
-
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\ncurrent_folder: ${data.folderAccess.allowed ? "allowed" : "off"} (${data.folderAccess.reason})\ncapture: ${captureSummary(data.capture, data.captureSwitches)}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? `installed (${data.runtimeVersion})` : data.runtimeVersion === null ? "missing" : `stale (${data.runtimeVersion})`) : "not required"}\ndurable_outbox: ${data.reliability?.outbox.total ?? 0} ready=${data.reliability?.outbox.ready ?? 0} auth_blocked=${data.reliability?.outbox.authBlocked ?? 0} dead=${data.reliability?.outbox.dead ?? 0}\noldest_capture: ${data.reliability?.outbox.oldestEnqueuedAt ?? "-"}\ncontext_snapshots: ${data.reliability?.cache.context ?? 0}\noldest_context_snapshot: ${data.reliability?.cache.oldestContextWrittenAt ?? "-"}\npolicy_snapshots: ${data.reliability?.cache.policy ?? 0}\nlocal_context_fallback_rate: ${metricRate(data.reliability?.operational.retrieval.fallbackRate)}\nlocal_guard_coverage: ${metricRate(data.reliability?.operational.guard.coverageRate)}\nlocal_guard_failures: ${data.reliability?.operational.guard.failures ?? 0}\nlocal_retrieval_failures: ${data.reliability?.operational.retrieval.failed ?? 0}\n${retrievalFailureLines}${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
|
|
3180
|
+
writeResult(data, parsed.json, `${health.lines.map((line) => `${line}\n`).join("")}connected: ${data.connected ? "yes" : "no"}\ncurrent_folder: ${data.folderAccess.allowed ? "allowed" : "off"} (${data.folderAccess.reason})\ncapture: ${captureSummary(data.capture, data.captureSwitches)}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? `installed (${data.runtimeVersion})` : data.runtimeVersion === null ? "missing" : `stale (${data.runtimeVersion})`) : "not required"}\ndurable_outbox: ${data.reliability?.outbox.total ?? 0} ready=${data.reliability?.outbox.ready ?? 0} auth_blocked=${data.reliability?.outbox.authBlocked ?? 0} dead=${data.reliability?.outbox.dead ?? 0}\noldest_capture: ${data.reliability?.outbox.oldestEnqueuedAt ?? "-"}\ncontext_snapshots: ${data.reliability?.cache.context ?? 0}\noldest_context_snapshot: ${data.reliability?.cache.oldestContextWrittenAt ?? "-"}\npolicy_snapshots: ${data.reliability?.cache.policy ?? 0}\nlocal_context_fallback_rate: ${metricRate(data.reliability?.operational.retrieval.fallbackRate)}\nlocal_guard_coverage: ${metricRate(data.reliability?.operational.guard.coverageRate)}\nlocal_guard_failures: ${data.reliability?.operational.guard.failures ?? 0}\nlocal_retrieval_failures: ${data.reliability?.operational.retrieval.failed ?? 0}\n${retrievalFailureLines}${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
|
|
2807
3181
|
}
|
|
2808
3182
|
async function disconnectCommand(args) {
|
|
2809
3183
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -2826,6 +3200,9 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2826
3200
|
]);
|
|
2827
3201
|
for (const installation of installations) {
|
|
2828
3202
|
const path = installation.configPath;
|
|
3203
|
+
// Leave linked project files alone; disconnecting everything else still matters.
|
|
3204
|
+
if (await linkedProjectConfigFile(installation, paths) !== null)
|
|
3205
|
+
continue;
|
|
2829
3206
|
if (isMcpHostAgent(installation.agent)) {
|
|
2830
3207
|
const mcpPath = mcpConfigPath(installation.agent, path);
|
|
2831
3208
|
if (installation.agent === "codex") {
|
|
@@ -2880,9 +3257,12 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2880
3257
|
rm(paths.runtimeContextFallback, { force: true }),
|
|
2881
3258
|
rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
|
|
2882
3259
|
rm(resolve(dirname(paths.runtime), "capture-conflict.js"), { force: true }),
|
|
3260
|
+
rm(resolve(dirname(paths.runtime), "skill-context.js"), { force: true }),
|
|
3261
|
+
rm(resolve(dirname(paths.runtime), "learning-line.js"), { force: true }),
|
|
2883
3262
|
rm(resolve(dirname(paths.runtime), "image-bytes.js"), { force: true }),
|
|
2884
3263
|
rm(resolve(dirname(paths.runtime), "image-capture.js"), { force: true }),
|
|
2885
3264
|
rm(resolve(dirname(paths.runtime), "retained-context.js"), { force: true }),
|
|
3265
|
+
rm(resolve(dirname(paths.runtime), "no-follow.js"), { force: true }),
|
|
2886
3266
|
rm(resolve(dirname(paths.runtime), "retrieval-task.js"), { force: true }),
|
|
2887
3267
|
rm(resolve(dirname(paths.runtime), "runtime-redaction.js"), { force: true }),
|
|
2888
3268
|
rm(resolve(dirname(paths.runtime), "user-message.js"), { force: true }),
|
|
@@ -2969,6 +3349,36 @@ export function operationalMetricsCheck(metrics) {
|
|
|
2969
3349
|
: ""),
|
|
2970
3350
|
};
|
|
2971
3351
|
}
|
|
3352
|
+
export const SEMANTIC_SEARCH_WINDOW_DAYS = 7;
|
|
3353
|
+
const SEMANTIC_SEARCH_MINIMUM_ATTEMPTS = 10;
|
|
3354
|
+
/**
|
|
3355
|
+
* Embedding failures over the last week. The server records `semantic_unavailable`
|
|
3356
|
+
* only when embeddings are configured and the request failed, and `lexical_only`
|
|
3357
|
+
* when none are configured, so a self-hosted server without an embedding key
|
|
3358
|
+
* never warns. Context still arrives during an outage, so nothing else shows it.
|
|
3359
|
+
*/
|
|
3360
|
+
export function semanticSearchCheck(retrieval) {
|
|
3361
|
+
const period = `in the last ${SEMANTIC_SEARCH_WINDOW_DAYS} days`;
|
|
3362
|
+
const attempts = retrieval.hybrid + retrieval.semanticUnavailable;
|
|
3363
|
+
const failed = retrieval.semanticUnavailable;
|
|
3364
|
+
if (attempts >= SEMANTIC_SEARCH_MINIMUM_ATTEMPTS && failed * 2 > attempts) {
|
|
3365
|
+
return {
|
|
3366
|
+
name: "semantic-search",
|
|
3367
|
+
status: "warning",
|
|
3368
|
+
detail: `Most prompts ${period} got keyword-only context because embedding requests are failing (${failed} of ${attempts} failed). Check the server's embedding key and provider.`,
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
return {
|
|
3372
|
+
name: "semantic-search",
|
|
3373
|
+
status: "ok",
|
|
3374
|
+
detail: attempts === 0
|
|
3375
|
+
? retrieval.lexicalOnly > 0
|
|
3376
|
+
? "keyword search only; embeddings are not configured on the server"
|
|
3377
|
+
: `no prompts with context ${period}`
|
|
3378
|
+
: `semantic search on ${retrieval.hybrid} of ${attempts} prompts ${period}` +
|
|
3379
|
+
(failed > 0 ? `; ${failed} fell back to keywords` : ""),
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
2972
3382
|
/**
|
|
2973
3383
|
* Local outbox health. Dead letters that can never be replayed (an old snapshot
|
|
2974
3384
|
* format or a permanent rejection) are a note, not an error: replaying them
|
|
@@ -3045,28 +3455,41 @@ async function apiChecks(config) {
|
|
|
3045
3455
|
: detail,
|
|
3046
3456
|
});
|
|
3047
3457
|
}
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
throw new Error("Operational metrics request failed");
|
|
3058
|
-
}
|
|
3059
|
-
checks.push(operationalMetricsCheck(OperationalMetricsResponseSchema.parse(await response.json())));
|
|
3060
|
-
}
|
|
3061
|
-
catch {
|
|
3062
|
-
checks.push({
|
|
3458
|
+
const now = Date.now();
|
|
3459
|
+
const [daily, weekly] = await Promise.allSettled([
|
|
3460
|
+
// The server's default window is the last 24 hours.
|
|
3461
|
+
operationalMetrics(config),
|
|
3462
|
+
operationalMetrics(config, new Date(now - SEMANTIC_SEARCH_WINDOW_DAYS * 24 * 60 * 60_000)),
|
|
3463
|
+
]);
|
|
3464
|
+
checks.push(daily.status === "fulfilled"
|
|
3465
|
+
? operationalMetricsCheck(daily.value)
|
|
3466
|
+
: {
|
|
3063
3467
|
name: "server-operational-metrics",
|
|
3064
3468
|
status: "warning",
|
|
3065
3469
|
detail: "authenticated operational metrics are unavailable",
|
|
3066
3470
|
});
|
|
3471
|
+
// Without the weekly metrics, the warning above already says they are unavailable.
|
|
3472
|
+
if (weekly.status === "fulfilled") {
|
|
3473
|
+
checks.push(semanticSearchCheck(weekly.value.retrieval));
|
|
3067
3474
|
}
|
|
3068
3475
|
return checks;
|
|
3069
3476
|
}
|
|
3477
|
+
async function operationalMetrics(config, from) {
|
|
3478
|
+
const url = new URL(`${config.apiUrl}/v1/reliability/metrics`);
|
|
3479
|
+
if (from !== undefined)
|
|
3480
|
+
url.searchParams.set("from", from.toISOString());
|
|
3481
|
+
const response = await fetch(url, {
|
|
3482
|
+
headers: {
|
|
3483
|
+
authorization: `Bearer ${config.token}`,
|
|
3484
|
+
"user-agent": `lore-cli/${LORE_VERSION}`,
|
|
3485
|
+
},
|
|
3486
|
+
signal: AbortSignal.timeout(3_000),
|
|
3487
|
+
});
|
|
3488
|
+
if (!response.ok) {
|
|
3489
|
+
throw new Error("Operational metrics request failed");
|
|
3490
|
+
}
|
|
3491
|
+
return OperationalMetricsResponseSchema.parse(await response.json());
|
|
3492
|
+
}
|
|
3070
3493
|
async function verifyConnectedInstallation() {
|
|
3071
3494
|
const paths = getLorePaths();
|
|
3072
3495
|
const config = await readConnectorConfig(paths);
|
|
@@ -3085,6 +3508,7 @@ function formatCaptureResult(result) {
|
|
|
3085
3508
|
const lines = [
|
|
3086
3509
|
`capture: ${captureSummary(result.capture, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })}`,
|
|
3087
3510
|
...(result.capture === "on" && pausedBy !== null ? [captureResumeHint(pausedBy)] : []),
|
|
3511
|
+
...(result.recapReminder === undefined ? [] : [result.recapReminder === "on" ? "recap reminder: on; lore capture reminder off stops it" : "recap reminder: off"]),
|
|
3088
3512
|
...(result.changed
|
|
3089
3513
|
? [
|
|
3090
3514
|
`updated: ${result.configFiles.join(", ")}`,
|
|
@@ -3112,7 +3536,31 @@ export async function setCaptureMode(capture, paths = getLorePaths()) {
|
|
|
3112
3536
|
const devinFiles = await refreshDevinHooks(paths.home, capture === "on");
|
|
3113
3537
|
return { capture, changed: true, configFiles: [...refreshed?.configFiles ?? [], ...devinFiles] };
|
|
3114
3538
|
}
|
|
3539
|
+
/** The once-per-session /recap reminder agents get while nothing is recorded. Hooks read it on each prompt. */
|
|
3540
|
+
export async function setRecapReminder(reminder, paths = getLorePaths()) {
|
|
3541
|
+
const config = await readConnectorConfig(paths);
|
|
3542
|
+
if (config === null)
|
|
3543
|
+
throw new Error("Lore is not connected. Run lore connect.");
|
|
3544
|
+
if ((config.recapReminder ?? "on") === reminder)
|
|
3545
|
+
return { recapReminder: reminder, changed: false };
|
|
3546
|
+
await withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
|
|
3547
|
+
const { recapReminder: _previous, ...rest } = config;
|
|
3548
|
+
await atomicWriteJson(paths.config, { ...rest, ...(reminder === "off" ? { recapReminder: reminder } : {}) }, 65536);
|
|
3549
|
+
});
|
|
3550
|
+
return { recapReminder: reminder, changed: true };
|
|
3551
|
+
}
|
|
3115
3552
|
async function captureCommand(args) {
|
|
3553
|
+
if (args[0] === "reminder") {
|
|
3554
|
+
const json = args.includes("--json");
|
|
3555
|
+
const [reminder, ...extra] = args.slice(1).filter((argument) => argument !== "--json");
|
|
3556
|
+
if ((reminder !== "on" && reminder !== "off") || extra.length > 0)
|
|
3557
|
+
throw new Error("Usage: lore capture reminder off|on [--json]");
|
|
3558
|
+
const result = await setRecapReminder(reminder);
|
|
3559
|
+
writeResult(result, json, reminder === "on"
|
|
3560
|
+
? "recap reminder: on. While nothing is recorded, your agent suggests /recap once in a session where you confirm a decision.\n"
|
|
3561
|
+
: "recap reminder: off. Your agent no longer suggests /recap; you can still run it any time.\n");
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3116
3564
|
let mode;
|
|
3117
3565
|
let json = false;
|
|
3118
3566
|
for (const argument of args) {
|
|
@@ -3128,7 +3576,7 @@ async function captureCommand(args) {
|
|
|
3128
3576
|
mode = argument;
|
|
3129
3577
|
continue;
|
|
3130
3578
|
}
|
|
3131
|
-
throw new Error("Usage: lore capture [status|on|off] [--json]");
|
|
3579
|
+
throw new Error("Usage: lore capture [status|on|off|reminder off|reminder on] [--json]");
|
|
3132
3580
|
}
|
|
3133
3581
|
const paths = getLorePaths();
|
|
3134
3582
|
if (mode === undefined || mode === "status") {
|
|
@@ -3138,7 +3586,8 @@ async function captureCommand(args) {
|
|
|
3138
3586
|
const capture = captureEnabled(config) ? "on" : "off";
|
|
3139
3587
|
// Same reading as lore status and lore doctor: the folder's connection and its dashboard switches.
|
|
3140
3588
|
const switches = capture === "on" ? await captureSwitches(await diagnosticConfig(paths, process.cwd()), paths.home, process.cwd()) : CAPTURE_SWITCHES_UNCHECKED;
|
|
3141
|
-
const
|
|
3589
|
+
const recorded = capture === "on" && switches.pausedBy === null;
|
|
3590
|
+
const result = { capture, pausedBy: switches.pausedBy, ...(recorded ? {} : { recapReminder: config.recapReminder ?? "on" }), changed: false, configFiles: [] };
|
|
3142
3591
|
writeResult(result, json, formatCaptureResult(result));
|
|
3143
3592
|
return;
|
|
3144
3593
|
}
|
|
@@ -3185,6 +3634,8 @@ async function doctorCommand(args) {
|
|
|
3185
3634
|
if (genericChecks.length > 0 && !status.agents.some((agent) => agent.configured)) {
|
|
3186
3635
|
checks[1] = { name: "capture-mode", status: "ok", detail: "off: only MCP agents are connected, and they have no hooks; Lore saves only what you ask them to" };
|
|
3187
3636
|
}
|
|
3637
|
+
if (loreDisabled(process.env))
|
|
3638
|
+
checks.unshift({ name: "lore-disable", status: "warning", detail: LORE_DISABLED_MESSAGE });
|
|
3188
3639
|
checks.push({
|
|
3189
3640
|
name: "platform",
|
|
3190
3641
|
status: platform() === "darwin" || platform() === "linux" || isWindows() ? "ok" : "error",
|
|
@@ -3537,11 +3988,13 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3537
3988
|
await runApproachCommand(commandArgs, await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()));
|
|
3538
3989
|
return;
|
|
3539
3990
|
case "skill":
|
|
3540
|
-
await runSkillCommand(commandArgs, commandArgs[0] === "discover" ? null : await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()));
|
|
3991
|
+
await runSkillCommand(commandArgs, commandArgs[0] === "discover" ? null : await readConnectorConfig(getLorePaths(process.env.LORE_HOME), process.cwd()), getLorePaths(process.env.LORE_HOME).loreDirectory);
|
|
3541
3992
|
return;
|
|
3542
|
-
case "guard":
|
|
3543
|
-
|
|
3993
|
+
case "guard": {
|
|
3994
|
+
const paths = getLorePaths();
|
|
3995
|
+
await runGuardCommand(commandArgs, await readConnectorConfig(paths, process.cwd()), paths.home);
|
|
3544
3996
|
return;
|
|
3997
|
+
}
|
|
3545
3998
|
case "resume":
|
|
3546
3999
|
await runResumeCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()), getLorePaths());
|
|
3547
4000
|
return;
|
|
@@ -3619,6 +4072,9 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3619
4072
|
case "update":
|
|
3620
4073
|
await updateCommand(commandArgs);
|
|
3621
4074
|
return;
|
|
4075
|
+
case "internal":
|
|
4076
|
+
await runInternalCommand(commandArgs);
|
|
4077
|
+
return;
|
|
3622
4078
|
case "disconnect":
|
|
3623
4079
|
await disconnectCommand(commandArgs);
|
|
3624
4080
|
return;
|