@lore-co/cli 0.2.22 → 0.2.24
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 +109 -13
- package/dist/approach-guidance.js +2 -2
- package/dist/approach-guidance.js.map +1 -1
- package/dist/approach.d.ts +1 -1
- package/dist/approach.d.ts.map +1 -1
- package/dist/approach.js +7 -0
- package/dist/approach.js.map +1 -1
- package/dist/ask.d.ts +2 -6
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +4 -21
- package/dist/ask.js.map +1 -1
- package/dist/chat.d.ts +15 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +286 -0
- package/dist/chat.js.map +1 -0
- package/dist/cli.d.ts +80 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +626 -142
- package/dist/cli.js.map +1 -1
- package/dist/command-help.d.ts +4 -1
- package/dist/command-help.d.ts.map +1 -1
- package/dist/command-help.js +9 -0
- package/dist/command-help.js.map +1 -1
- package/dist/devin-capture.d.ts +6 -0
- package/dist/devin-capture.d.ts.map +1 -1
- package/dist/devin-capture.js +34 -3
- package/dist/devin-capture.js.map +1 -1
- package/dist/devin-cli.d.ts +7 -2
- package/dist/devin-cli.d.ts.map +1 -1
- package/dist/devin-cli.js +81 -8
- package/dist/devin-cli.js.map +1 -1
- package/dist/generated-assets.d.ts +8 -6
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +8 -6
- package/dist/generated-assets.js.map +1 -1
- package/dist/generic-mcp.d.ts +35 -0
- package/dist/generic-mcp.d.ts.map +1 -0
- package/dist/generic-mcp.js +83 -0
- package/dist/generic-mcp.js.map +1 -0
- package/dist/learnings.d.ts +20 -0
- package/dist/learnings.d.ts.map +1 -1
- package/dist/learnings.js +25 -16
- package/dist/learnings.js.map +1 -1
- package/dist/lore-agent.d.ts +50 -0
- package/dist/lore-agent.d.ts.map +1 -0
- package/dist/lore-agent.js +299 -0
- package/dist/lore-agent.js.map +1 -0
- package/dist/mcp-config.d.ts +46 -0
- package/dist/mcp-config.d.ts.map +1 -0
- package/dist/mcp-config.js +196 -0
- package/dist/mcp-config.js.map +1 -0
- package/dist/owned-files.d.ts +21 -0
- package/dist/owned-files.d.ts.map +1 -0
- package/dist/owned-files.js +88 -0
- package/dist/owned-files.js.map +1 -0
- package/dist/recovery-cloud.d.ts +1 -1
- package/dist/review.d.ts +30 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +276 -0
- package/dist/review.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +50 -19
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +1 -1
- package/dist/self-host.js +2 -2
- package/dist/skills.d.ts +2 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +46 -97
- package/dist/skills.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +39 -4
- package/dist/update.js.map +1 -1
- package/package.json +7 -4
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseDirectoryRules, checkDirectory, checkLoreAccess } from "./directory-policy.js";
|
|
2
|
+
import { parseDirectoryRules, checkDirectory, checkLoreAccess, capturePauseMessage } 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";
|
|
@@ -8,15 +8,16 @@ import { resolveConnectProject, validateConnectProjectOptions } from "./connect-
|
|
|
8
8
|
import { captureDirectories } from "./capture-scope.js";
|
|
9
9
|
import { prepareConnection, persistConnection, listConnections } from "./connections.js";
|
|
10
10
|
import { connectionReferences, resolveLocalConnection } from "./directory-policy.js";
|
|
11
|
-
import { withDirectoryLock } from "./reliability-store.js";
|
|
11
|
+
import { atomicWriteJson, withDirectoryLock } from "./reliability-store.js";
|
|
12
12
|
import { runConfigCommand } from "./config-command.js";
|
|
13
13
|
import { codexApprovalDetail, inspectCodexHookApproval } from "./codex-hook-approval.js";
|
|
14
|
-
import { claudeOnlyCommand, disableDevinCli, runDevinCliHook } from "./devin-cli.js";
|
|
14
|
+
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";
|
|
15
16
|
import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
16
17
|
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
17
18
|
import { spawn, isWindows, windowsCommand, commandText, findExecutable, openBrowser } from "./platform.js";
|
|
18
19
|
import { createHash, randomUUID } from "node:crypto";
|
|
19
|
-
import { dirname, resolve } from "node:path";
|
|
20
|
+
import { basename, dirname, resolve } from "node:path";
|
|
20
21
|
import { homedir, hostname, platform } from "node:os";
|
|
21
22
|
import { setTimeout as delay } from "node:timers/promises";
|
|
22
23
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -24,10 +25,14 @@ import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, COPILOT_
|
|
|
24
25
|
import { runHook, } from "./runtime.js";
|
|
25
26
|
import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
|
|
26
27
|
import { runAskCommand } from "./ask.js";
|
|
28
|
+
import { runChatCommand } from "./chat.js";
|
|
27
29
|
import { runLearningsCommand } from "./learnings.js";
|
|
30
|
+
import { runReviewCommand } from "./review.js";
|
|
28
31
|
import { runEmbeddingsCommand } from "./embeddings.js";
|
|
29
32
|
import { runRememberCommand } from "./remember.js";
|
|
30
33
|
import { installLoreSkill, removeLoreSkill } from "./skills.js";
|
|
34
|
+
import { GENERIC_AGENT, genericInstructions, genericMcpServers, parseGenericAgentId, readGenericAgents, recordGenericAgent, removeGenericAgents } from "./generic-mcp.js";
|
|
35
|
+
import { installLoreAgent, removeLoreAgent } from "./lore-agent.js";
|
|
31
36
|
import { runApproachCommand } from "./approach.js";
|
|
32
37
|
import { runSkillCommand } from "./skill-command.js";
|
|
33
38
|
import { runGuardCommand } from "./guard.js";
|
|
@@ -58,6 +63,20 @@ const HOOK_EVENTS = [
|
|
|
58
63
|
"Stop",
|
|
59
64
|
"SessionEnd",
|
|
60
65
|
];
|
|
66
|
+
/**
|
|
67
|
+
* Hook events that exist only to record turns. Retrieval-only mode leaves them
|
|
68
|
+
* uninstalled, so the agent never hands a response to Lore at all.
|
|
69
|
+
*/
|
|
70
|
+
const CAPTURE_HOOK_EVENTS = new Set([
|
|
71
|
+
"Stop", "SessionEnd", "afterAgentResponse", "sessionEnd", "agentStop", "post_model_turn",
|
|
72
|
+
]);
|
|
73
|
+
function installedEvents(events, capture) {
|
|
74
|
+
return capture ? [...events] : events.filter((event) => !CAPTURE_HOOK_EVENTS.has(event));
|
|
75
|
+
}
|
|
76
|
+
/** Capture mode of a connection: absent means on. */
|
|
77
|
+
export function captureEnabled(config) {
|
|
78
|
+
return config?.capture !== "off";
|
|
79
|
+
}
|
|
61
80
|
/** Tools the Context Guard PreToolUse hook cares about (regex matcher). */
|
|
62
81
|
const GUARD_TOOL_MATCHER = "Edit|Write|MultiEdit|NotebookEdit|ApplyPatch|apply_patch|edit|write|patch|str_replace_editor|Bash|bash|Shell|shell|exec_command|shell_command|run_terminal_cmd";
|
|
63
82
|
const CURSOR_HOOK_EVENTS = [
|
|
@@ -110,12 +129,16 @@ Options:
|
|
|
110
129
|
--repository <repo> Verify the selected folder matches this GitHub repository
|
|
111
130
|
--folder <path> Record only in this folder and subfolders (repeatable)
|
|
112
131
|
--all-projects Record in all projects allowed by your workspace
|
|
132
|
+
--no-capture Retrieval-only mode: context, guard, and manual saves, no turn recording
|
|
133
|
+
--capture Turn automatic turn recording back on
|
|
113
134
|
--url <url> Lore API base URL (default: https://api.uselore.co)
|
|
114
135
|
--dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
|
|
115
136
|
--login Approve access securely in the Lore dashboard
|
|
116
137
|
--no-open Print the approval URL without opening a browser
|
|
117
138
|
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
118
|
-
--agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, opencode, polytoken, or t3code
|
|
139
|
+
--agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, generic, opencode, polytoken, or t3code
|
|
140
|
+
--agent-id <id> With --agent generic: credit MCP saves to this agent (default: generic)
|
|
141
|
+
--write With --agent generic: let the agent save through MCP (saves stay proposals)
|
|
119
142
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
120
143
|
--fallback <agent> claude, codex, opencode, devin, cursor, copilot-cli, polytoken
|
|
121
144
|
--no-recovery Disable automatic handoff setup (otherwise enabled with two installed clients)
|
|
@@ -123,11 +146,14 @@ Options:
|
|
|
123
146
|
--help Show this command's help
|
|
124
147
|
|
|
125
148
|
New connections stay paused until you choose a project, a folder, or --all-projects.
|
|
126
|
-
Reconnecting without a capture option keeps your saved folder rules.
|
|
127
|
-
Folder choices control
|
|
149
|
+
Reconnecting without a capture option keeps your saved folder rules and capture mode.
|
|
150
|
+
Folder choices control where Lore runs; learning scope controls where knowledge applies.
|
|
151
|
+
With --no-capture, Lore installs no Stop or SessionEnd hooks and sends no turns.
|
|
152
|
+
Context injection, Guard, lore ask, and lore remember keep working. See lore capture.
|
|
128
153
|
|
|
129
154
|
Examples:
|
|
130
155
|
lore connect --login --agent claude --folder .
|
|
156
|
+
lore connect --login --agent claude --folder . --no-capture
|
|
131
157
|
lore connect --login --agent cowork
|
|
132
158
|
lore connect --login --agent devin --folder .
|
|
133
159
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
|
|
@@ -154,6 +180,24 @@ Examples:
|
|
|
154
180
|
lore doctor
|
|
155
181
|
lore doctor --json
|
|
156
182
|
`;
|
|
183
|
+
const CAPTURE_HELP = `lore capture
|
|
184
|
+
Choose whether this machine records agent turns automatically.
|
|
185
|
+
|
|
186
|
+
Usage:
|
|
187
|
+
lore capture status [--json]
|
|
188
|
+
lore capture off [--json]
|
|
189
|
+
lore capture on [--json]
|
|
190
|
+
|
|
191
|
+
With capture off, Lore installs no Stop or SessionEnd hooks and never sends a
|
|
192
|
+
prompt or response as a turn. Context injection, Guard, lore ask, lore remember,
|
|
193
|
+
and the lore skill's "remember this session" keep working. Folder rules still
|
|
194
|
+
decide where Lore runs at all.
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
lore capture off
|
|
198
|
+
lore capture status
|
|
199
|
+
lore capture on
|
|
200
|
+
`;
|
|
157
201
|
const DISCONNECT_HELP = `lore disconnect
|
|
158
202
|
Remove Lore-owned hooks/plugin, credentials, runtime, and transient state.
|
|
159
203
|
Durable captures, dead letters, verified snapshots, unrelated agent settings,
|
|
@@ -222,6 +266,7 @@ function isConnectAgent(value) {
|
|
|
222
266
|
return (isConfiguredAgent(value) ||
|
|
223
267
|
value === "copilot" ||
|
|
224
268
|
isCredentialOnlyAgent(value) ||
|
|
269
|
+
value === "generic" ||
|
|
225
270
|
value === "t3code");
|
|
226
271
|
}
|
|
227
272
|
function isCredentialOnlyAgent(value) {
|
|
@@ -291,24 +336,29 @@ function eventHandler(agent, event, paths) {
|
|
|
291
336
|
: {}),
|
|
292
337
|
};
|
|
293
338
|
}
|
|
294
|
-
export function mergeLoreHooks(input, agent, paths) {
|
|
339
|
+
export function mergeLoreHooks(input, agent, paths, capture = true) {
|
|
295
340
|
const result = cloneObject(input);
|
|
296
341
|
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
297
342
|
throw new Error("Agent configuration field \"hooks\" must be a JSON object");
|
|
298
343
|
}
|
|
299
344
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
345
|
+
const installed = installedEvents(HOOK_EVENTS, capture);
|
|
300
346
|
for (const event of HOOK_EVENTS) {
|
|
301
347
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
302
348
|
throw new Error(`Agent hook event "${event}" must be a JSON array`);
|
|
303
349
|
}
|
|
304
|
-
|
|
350
|
+
const groups = [
|
|
305
351
|
...stripLoreFromEvent(hooks[event]),
|
|
306
|
-
{
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
352
|
+
...(installed.includes(event) ? [{
|
|
353
|
+
// Guard checks run only for mutation-shaped tools.
|
|
354
|
+
...(event === "PreToolUse" ? { matcher: GUARD_TOOL_MATCHER } : {}),
|
|
355
|
+
hooks: [eventHandler(agent, event, paths)],
|
|
356
|
+
}] : []),
|
|
311
357
|
];
|
|
358
|
+
if (groups.length === 0)
|
|
359
|
+
delete hooks[event];
|
|
360
|
+
else
|
|
361
|
+
hooks[event] = groups;
|
|
312
362
|
}
|
|
313
363
|
result.hooks = hooks;
|
|
314
364
|
return result;
|
|
@@ -398,10 +448,53 @@ export function removeLoreCursorMcp(input) {
|
|
|
398
448
|
const { [CURSOR_MCP_NAME]: _lore, ...rest } = config.mcpServers;
|
|
399
449
|
return { ...config, mcpServers: rest };
|
|
400
450
|
}
|
|
451
|
+
function isMcpHostAgent(agent) {
|
|
452
|
+
return MCP_HOST_AGENTS.includes(agent);
|
|
453
|
+
}
|
|
454
|
+
/** The full Lore MCP server, run by this CLI so it shares the hooks' credentials and version. */
|
|
455
|
+
export function loreMcpServer(paths, agent) {
|
|
456
|
+
return loreMcpServerEntry(loreMcpInvocation(), paths.home, agent);
|
|
457
|
+
}
|
|
458
|
+
/** This CLI's executable plus its script path (none for the standalone binary). */
|
|
459
|
+
function loreMcpInvocation() {
|
|
460
|
+
return [process.execPath, ...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)])];
|
|
461
|
+
}
|
|
462
|
+
/** Where each agent lists MCP servers, located from the hooks file Lore already manages. */
|
|
463
|
+
export function mcpConfigPath(agent, hooksPath) {
|
|
464
|
+
const directory = dirname(hooksPath);
|
|
465
|
+
switch (agent) {
|
|
466
|
+
case "claude":
|
|
467
|
+
// ~/.claude/settings.json pairs with ~/.claude.json; a CLAUDE_CONFIG_DIR keeps both inside it.
|
|
468
|
+
return basename(directory) === ".claude"
|
|
469
|
+
? resolve(directory, "..", ".claude.json")
|
|
470
|
+
: resolve(directory, ".claude.json");
|
|
471
|
+
case "codex":
|
|
472
|
+
return resolve(directory, "config.toml");
|
|
473
|
+
case "cursor":
|
|
474
|
+
return resolve(directory, "mcp.json");
|
|
475
|
+
case "copilot-cli":
|
|
476
|
+
// $COPILOT_HOME/hooks/lore.json pairs with $COPILOT_HOME/mcp-config.json.
|
|
477
|
+
return resolve(directory, "..", "mcp-config.json");
|
|
478
|
+
case "copilot-vscode":
|
|
479
|
+
// <repo>/.github/hooks/lore-vscode.json pairs with <repo>/.vscode/mcp.json.
|
|
480
|
+
return resolve(directory, "..", "..", ".vscode", "mcp.json");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
export function mergeLoreMcp(input, agent, paths) {
|
|
484
|
+
const config = objectConfiguration(input, agent);
|
|
485
|
+
// VS Code keeps its servers under `servers`; the other JSON hosts use `mcpServers`.
|
|
486
|
+
return agent === "copilot-vscode"
|
|
487
|
+
? mergeLoreVsCodeMcp(config, loreMcpServer(paths, agent))
|
|
488
|
+
: mergeLoreMcpServers(config, loreMcpServer(paths, agent), agent);
|
|
489
|
+
}
|
|
490
|
+
export function removeLoreMcp(input, agent) {
|
|
491
|
+
const config = objectConfiguration(input, agent);
|
|
492
|
+
return agent === "copilot-vscode" ? removeLoreVsCodeMcp(config) : removeLoreMcpServers(config);
|
|
493
|
+
}
|
|
401
494
|
function stripLoreFromCursorEvent(value) {
|
|
402
495
|
return Array.isArray(value) ? value.filter((hook) => !isLoreHook(hook)) : [];
|
|
403
496
|
}
|
|
404
|
-
export function mergeLoreCursorHooks(input, paths) {
|
|
497
|
+
export function mergeLoreCursorHooks(input, paths, capture = true) {
|
|
405
498
|
const result = cloneObject(input);
|
|
406
499
|
if (result.version !== undefined && result.version !== 1) {
|
|
407
500
|
throw new Error('Cursor configuration field "version" must be 1');
|
|
@@ -410,14 +503,19 @@ export function mergeLoreCursorHooks(input, paths) {
|
|
|
410
503
|
throw new Error('Cursor configuration field "hooks" must be a JSON object');
|
|
411
504
|
}
|
|
412
505
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
506
|
+
const installed = installedEvents(CURSOR_HOOK_EVENTS, capture);
|
|
413
507
|
for (const event of CURSOR_HOOK_EVENTS) {
|
|
414
508
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
415
509
|
throw new Error(`Cursor hook event "${event}" must be a JSON array`);
|
|
416
510
|
}
|
|
417
|
-
|
|
511
|
+
const entries = [
|
|
418
512
|
...stripLoreFromCursorEvent(hooks[event]),
|
|
419
|
-
cursorEventHandler(event, paths),
|
|
513
|
+
...(installed.includes(event) ? [cursorEventHandler(event, paths)] : []),
|
|
420
514
|
];
|
|
515
|
+
if (entries.length === 0)
|
|
516
|
+
delete hooks[event];
|
|
517
|
+
else
|
|
518
|
+
hooks[event] = entries;
|
|
421
519
|
}
|
|
422
520
|
result.version = 1;
|
|
423
521
|
result.hooks = hooks;
|
|
@@ -492,7 +590,7 @@ function stripLoreFromCopilotEvent(value) {
|
|
|
492
590
|
? value.filter((hook) => !isLoreCopilotHook(hook))
|
|
493
591
|
: [];
|
|
494
592
|
}
|
|
495
|
-
export function mergeLoreCopilotCliHooks(input, paths) {
|
|
593
|
+
export function mergeLoreCopilotCliHooks(input, paths, capture = true) {
|
|
496
594
|
const result = cloneObject(input);
|
|
497
595
|
if (result.version !== undefined && result.version !== 1) {
|
|
498
596
|
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
@@ -501,14 +599,19 @@ export function mergeLoreCopilotCliHooks(input, paths) {
|
|
|
501
599
|
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
502
600
|
}
|
|
503
601
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
602
|
+
const installed = installedEvents(COPILOT_CLI_HOOK_EVENTS, capture);
|
|
504
603
|
for (const event of COPILOT_CLI_HOOK_EVENTS) {
|
|
505
604
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
506
605
|
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
507
606
|
}
|
|
508
|
-
|
|
607
|
+
const entries = [
|
|
509
608
|
...stripLoreFromCopilotEvent(hooks[event]),
|
|
510
|
-
copilotCliEventHandler(event, paths),
|
|
609
|
+
...(installed.includes(event) ? [copilotCliEventHandler(event, paths)] : []),
|
|
511
610
|
];
|
|
611
|
+
if (entries.length === 0)
|
|
612
|
+
delete hooks[event];
|
|
613
|
+
else
|
|
614
|
+
hooks[event] = entries;
|
|
512
615
|
}
|
|
513
616
|
result.version = 1;
|
|
514
617
|
result.hooks = hooks;
|
|
@@ -565,7 +668,7 @@ function copilotVscodeEventHandler(event, paths) {
|
|
|
565
668
|
: 25,
|
|
566
669
|
};
|
|
567
670
|
}
|
|
568
|
-
export function mergeLoreCopilotVscodeHooks(input, paths) {
|
|
671
|
+
export function mergeLoreCopilotVscodeHooks(input, paths, capture = true) {
|
|
569
672
|
const result = cloneObject(input);
|
|
570
673
|
if (result.version !== undefined && result.version !== 1) {
|
|
571
674
|
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
@@ -574,14 +677,19 @@ export function mergeLoreCopilotVscodeHooks(input, paths) {
|
|
|
574
677
|
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
575
678
|
}
|
|
576
679
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
680
|
+
const installed = installedEvents(HOOK_EVENTS, capture);
|
|
577
681
|
for (const event of HOOK_EVENTS) {
|
|
578
682
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
579
683
|
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
580
684
|
}
|
|
581
|
-
|
|
685
|
+
const entries = [
|
|
582
686
|
...stripLoreFromCursorEvent(hooks[event]),
|
|
583
|
-
copilotVscodeEventHandler(event, paths),
|
|
687
|
+
...(installed.includes(event) ? [copilotVscodeEventHandler(event, paths)] : []),
|
|
584
688
|
];
|
|
689
|
+
if (entries.length === 0)
|
|
690
|
+
delete hooks[event];
|
|
691
|
+
else
|
|
692
|
+
hooks[event] = entries;
|
|
585
693
|
}
|
|
586
694
|
result.version = 1;
|
|
587
695
|
result.hooks = hooks;
|
|
@@ -641,14 +749,14 @@ function isLorePolytokenHook(value) {
|
|
|
641
749
|
(/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
|
|
642
750
|
command.includes("/.lore/bin/lore-hook.mjs")));
|
|
643
751
|
}
|
|
644
|
-
export function mergeLorePolytokenHooks(input, paths) {
|
|
752
|
+
export function mergeLorePolytokenHooks(input, paths, capture = true) {
|
|
645
753
|
if (!Array.isArray(input)) {
|
|
646
754
|
throw new Error("Polytoken hooks must contain a JSON array");
|
|
647
755
|
}
|
|
648
756
|
const retained = input.filter((hook) => !isLorePolytokenHook(hook));
|
|
649
757
|
return [
|
|
650
758
|
...structuredClone(retained),
|
|
651
|
-
...POLYTOKEN_HOOK_EVENTS.map((event) => polytokenEventHandler(event, paths)),
|
|
759
|
+
...installedEvents(POLYTOKEN_HOOK_EVENTS, capture).map((event) => polytokenEventHandler(event, paths)),
|
|
652
760
|
];
|
|
653
761
|
}
|
|
654
762
|
export function removeLorePolytokenHooks(input) {
|
|
@@ -697,6 +805,9 @@ export function countLoreOpenCodePlugins(input) {
|
|
|
697
805
|
? input.plugin.filter(isLoreOpenCodePlugin).length
|
|
698
806
|
: 0;
|
|
699
807
|
}
|
|
808
|
+
function countLoreOpenCodeMcp(input) {
|
|
809
|
+
return isObject(input.mcp) && isLoreOpenCodeMcp(input.mcp[LORE_MCP_SERVER_NAME]) ? 1 : 0;
|
|
810
|
+
}
|
|
700
811
|
function installationUnitId(event, index, total) {
|
|
701
812
|
return total === 1 ? event : `installation-${index + 1}/${event}`;
|
|
702
813
|
}
|
|
@@ -712,13 +823,13 @@ function installationUnitState(ownedCount, exactCount) {
|
|
|
712
823
|
function exactConfiguration(left, right) {
|
|
713
824
|
return canonicalJson(left) === canonicalJson(right);
|
|
714
825
|
}
|
|
715
|
-
function commandHookInstallationUnits(input, agent, paths, index, total) {
|
|
826
|
+
function commandHookInstallationUnits(input, agent, paths, index, total, capture) {
|
|
716
827
|
const configuration = objectConfiguration(input, agent);
|
|
717
828
|
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
718
829
|
throw new Error(`${agent} configuration field "hooks" must be an object`);
|
|
719
830
|
}
|
|
720
831
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
721
|
-
return HOOK_EVENTS.map((event) => {
|
|
832
|
+
return installedEvents(HOOK_EVENTS, capture).map((event) => {
|
|
722
833
|
const groups = hooks[event];
|
|
723
834
|
if (groups !== undefined && !Array.isArray(groups)) {
|
|
724
835
|
throw new Error(`${agent} hook event "${event}" must be an array`);
|
|
@@ -741,7 +852,7 @@ function commandHookInstallationUnits(input, agent, paths, index, total) {
|
|
|
741
852
|
};
|
|
742
853
|
});
|
|
743
854
|
}
|
|
744
|
-
function cursorInstallationUnits(input, paths, index, total) {
|
|
855
|
+
function cursorInstallationUnits(input, paths, index, total, capture) {
|
|
745
856
|
const configuration = objectConfiguration(input, "cursor");
|
|
746
857
|
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
747
858
|
throw new Error('Cursor configuration field "version" must be 1');
|
|
@@ -750,7 +861,7 @@ function cursorInstallationUnits(input, paths, index, total) {
|
|
|
750
861
|
throw new Error('Cursor configuration field "hooks" must be an object');
|
|
751
862
|
}
|
|
752
863
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
753
|
-
return CURSOR_HOOK_EVENTS.map((event) => {
|
|
864
|
+
return installedEvents(CURSOR_HOOK_EVENTS, capture).map((event) => {
|
|
754
865
|
const values = hooks[event];
|
|
755
866
|
if (values !== undefined && !Array.isArray(values)) {
|
|
756
867
|
throw new Error(`Cursor hook event "${event}" must be an array`);
|
|
@@ -765,7 +876,7 @@ function cursorInstallationUnits(input, paths, index, total) {
|
|
|
765
876
|
};
|
|
766
877
|
});
|
|
767
878
|
}
|
|
768
|
-
function copilotCliInstallationUnits(input, paths, index, total) {
|
|
879
|
+
function copilotCliInstallationUnits(input, paths, index, total, capture) {
|
|
769
880
|
const configuration = objectConfiguration(input, "copilot-cli");
|
|
770
881
|
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
771
882
|
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
@@ -774,7 +885,7 @@ function copilotCliInstallationUnits(input, paths, index, total) {
|
|
|
774
885
|
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
775
886
|
}
|
|
776
887
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
777
|
-
return COPILOT_CLI_HOOK_EVENTS.map((event) => {
|
|
888
|
+
return installedEvents(COPILOT_CLI_HOOK_EVENTS, capture).map((event) => {
|
|
778
889
|
const values = hooks[event];
|
|
779
890
|
if (values !== undefined && !Array.isArray(values)) {
|
|
780
891
|
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
@@ -789,7 +900,7 @@ function copilotCliInstallationUnits(input, paths, index, total) {
|
|
|
789
900
|
};
|
|
790
901
|
});
|
|
791
902
|
}
|
|
792
|
-
function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
903
|
+
function copilotVscodeInstallationUnits(input, paths, index, total, capture) {
|
|
793
904
|
const configuration = objectConfiguration(input, "copilot-vscode");
|
|
794
905
|
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
795
906
|
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
@@ -798,7 +909,7 @@ function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
|
798
909
|
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
799
910
|
}
|
|
800
911
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
801
|
-
return HOOK_EVENTS.map((event) => {
|
|
912
|
+
return installedEvents(HOOK_EVENTS, capture).map((event) => {
|
|
802
913
|
const values = hooks[event];
|
|
803
914
|
if (values !== undefined && !Array.isArray(values)) {
|
|
804
915
|
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
@@ -813,11 +924,11 @@ function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
|
813
924
|
};
|
|
814
925
|
});
|
|
815
926
|
}
|
|
816
|
-
function polytokenInstallationUnits(input, paths, index, total) {
|
|
927
|
+
function polytokenInstallationUnits(input, paths, index, total, capture) {
|
|
817
928
|
if (!Array.isArray(input)) {
|
|
818
929
|
throw new Error("Polytoken hooks must contain a JSON array");
|
|
819
930
|
}
|
|
820
|
-
return POLYTOKEN_HOOK_EVENTS.map((event) => {
|
|
931
|
+
return installedEvents(POLYTOKEN_HOOK_EVENTS, capture).map((event) => {
|
|
821
932
|
const expected = polytokenEventHandler(event, paths);
|
|
822
933
|
const owned = input.filter((candidate) => isLorePolytokenHook(candidate) &&
|
|
823
934
|
(!isObject(candidate) || candidate.event === event));
|
|
@@ -828,7 +939,7 @@ function polytokenInstallationUnits(input, paths, index, total) {
|
|
|
828
939
|
};
|
|
829
940
|
});
|
|
830
941
|
}
|
|
831
|
-
function openCodeInstallationUnits(input, index, total) {
|
|
942
|
+
function openCodeInstallationUnits(input, paths, index, total) {
|
|
832
943
|
const configuration = objectConfiguration(input, "opencode");
|
|
833
944
|
if (configuration.plugin !== undefined &&
|
|
834
945
|
!Array.isArray(configuration.plugin)) {
|
|
@@ -839,28 +950,37 @@ function openCodeInstallationUnits(input, index, total) {
|
|
|
839
950
|
: [];
|
|
840
951
|
const owned = plugins.filter(isLoreOpenCodePlugin);
|
|
841
952
|
const exactCount = owned.filter((candidate) => candidate === LORE_OPENCODE_PLUGIN).length;
|
|
953
|
+
if (configuration.mcp !== undefined && !isObject(configuration.mcp)) {
|
|
954
|
+
throw new Error('OpenCode configuration field "mcp" must be a JSON object');
|
|
955
|
+
}
|
|
956
|
+
const server = isObject(configuration.mcp) ? configuration.mcp[LORE_MCP_SERVER_NAME] : undefined;
|
|
957
|
+
const ownedServer = Number(isLoreOpenCodeMcp(server));
|
|
842
958
|
return [
|
|
843
959
|
{
|
|
844
960
|
id: installationUnitId("plugin", index, total),
|
|
845
961
|
state: installationUnitState(owned.length, exactCount),
|
|
846
962
|
},
|
|
963
|
+
{
|
|
964
|
+
id: installationUnitId("mcp-tools", index, total),
|
|
965
|
+
state: installationUnitState(ownedServer, Number(exactConfiguration(server, openCodeMcpEntry(loreMcpServer(paths, "opencode"))))),
|
|
966
|
+
},
|
|
847
967
|
];
|
|
848
968
|
}
|
|
849
|
-
function inspectInstallationUnits(input, agent, paths, index, total) {
|
|
969
|
+
function inspectInstallationUnits(input, agent, paths, index, total, capture) {
|
|
850
970
|
switch (agent) {
|
|
851
971
|
case "claude":
|
|
852
972
|
case "codex":
|
|
853
|
-
return commandHookInstallationUnits(input, agent, paths, index, total);
|
|
973
|
+
return commandHookInstallationUnits(input, agent, paths, index, total, capture);
|
|
854
974
|
case "copilot-cli":
|
|
855
|
-
return copilotCliInstallationUnits(input, paths, index, total);
|
|
975
|
+
return copilotCliInstallationUnits(input, paths, index, total, capture);
|
|
856
976
|
case "copilot-vscode":
|
|
857
|
-
return copilotVscodeInstallationUnits(input, paths, index, total);
|
|
977
|
+
return copilotVscodeInstallationUnits(input, paths, index, total, capture);
|
|
858
978
|
case "cursor":
|
|
859
|
-
return cursorInstallationUnits(input, paths, index, total);
|
|
979
|
+
return cursorInstallationUnits(input, paths, index, total, capture);
|
|
860
980
|
case "polytoken":
|
|
861
|
-
return polytokenInstallationUnits(input, paths, index, total);
|
|
981
|
+
return polytokenInstallationUnits(input, paths, index, total, capture);
|
|
862
982
|
case "opencode":
|
|
863
|
-
return openCodeInstallationUnits(input, index, total);
|
|
983
|
+
return openCodeInstallationUnits(input, paths, index, total);
|
|
864
984
|
}
|
|
865
985
|
}
|
|
866
986
|
async function readJsonDocument(path) {
|
|
@@ -920,6 +1040,39 @@ async function writeMergedJson(path, document, value, now) {
|
|
|
920
1040
|
...(backup === undefined ? {} : { backup }),
|
|
921
1041
|
};
|
|
922
1042
|
}
|
|
1043
|
+
async function readTextDocument(path) {
|
|
1044
|
+
try {
|
|
1045
|
+
const [value, metadata] = await Promise.all([
|
|
1046
|
+
readFile(path, "utf8"),
|
|
1047
|
+
stat(path),
|
|
1048
|
+
]);
|
|
1049
|
+
return { exists: true, value, mode: metadata.mode & 0o777 };
|
|
1050
|
+
}
|
|
1051
|
+
catch (error) {
|
|
1052
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
1053
|
+
? error.code
|
|
1054
|
+
: undefined;
|
|
1055
|
+
if (code === "ENOENT") {
|
|
1056
|
+
return { exists: false, value: "", mode: 0o600 };
|
|
1057
|
+
}
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
async function writeMergedText(path, document, value, now) {
|
|
1062
|
+
if (document.exists && document.value === value) {
|
|
1063
|
+
return { changed: false };
|
|
1064
|
+
}
|
|
1065
|
+
let backup;
|
|
1066
|
+
if (document.exists) {
|
|
1067
|
+
backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
|
|
1068
|
+
await copyFile(path, backup, fsConstants.COPYFILE_EXCL);
|
|
1069
|
+
}
|
|
1070
|
+
await atomicWrite(path, value, document.mode);
|
|
1071
|
+
return {
|
|
1072
|
+
changed: true,
|
|
1073
|
+
...(backup === undefined ? {} : { backup }),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
923
1076
|
function parseConnectorConfig(value) {
|
|
924
1077
|
if (!isObject(value) ||
|
|
925
1078
|
(value.version !== 1 && value.version !== 2) ||
|
|
@@ -979,6 +1132,7 @@ function parseConnectorConfig(value) {
|
|
|
979
1132
|
Object.keys(agentConfigPaths).length === 0
|
|
980
1133
|
? {}
|
|
981
1134
|
: { agentConfigPaths }),
|
|
1135
|
+
...(value.capture === "off" ? { capture: "off" } : {}),
|
|
982
1136
|
connectedAt: value.connectedAt,
|
|
983
1137
|
timeoutMs,
|
|
984
1138
|
};
|
|
@@ -1145,79 +1299,84 @@ function objectConfiguration(input, agent) {
|
|
|
1145
1299
|
const AGENT_TARGETS = {
|
|
1146
1300
|
claude: {
|
|
1147
1301
|
integration: "hooks",
|
|
1148
|
-
|
|
1302
|
+
// Native hooks plus the Lore MCP server entry.
|
|
1303
|
+
expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
|
|
1149
1304
|
runtimeRequired: true,
|
|
1150
1305
|
emptyValue: () => ({}),
|
|
1151
1306
|
configPath: (paths) => paths.claudeSettings,
|
|
1152
1307
|
detect: async () => commandExists("claude"),
|
|
1153
1308
|
executable: async () => commandExists("claude"),
|
|
1154
|
-
merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths),
|
|
1309
|
+
merge: (input, paths, capture) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths, capture),
|
|
1155
1310
|
remove: (input) => removeLoreHooks(objectConfiguration(input, "claude")),
|
|
1156
1311
|
count: (input) => countLoreHooks(objectConfiguration(input, "claude")),
|
|
1157
1312
|
},
|
|
1158
1313
|
codex: {
|
|
1159
1314
|
integration: "hooks",
|
|
1160
|
-
expected: HOOK_EVENTS.length,
|
|
1315
|
+
expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
|
|
1161
1316
|
runtimeRequired: true,
|
|
1162
1317
|
emptyValue: () => ({}),
|
|
1163
1318
|
configPath: (paths) => paths.codexHooks,
|
|
1164
1319
|
detect: async () => commandExists("codex"),
|
|
1165
1320
|
executable: async () => commandExists("codex"),
|
|
1166
|
-
merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths),
|
|
1321
|
+
merge: (input, paths, capture) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths, capture),
|
|
1167
1322
|
remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
|
|
1168
1323
|
count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
|
|
1169
1324
|
},
|
|
1170
1325
|
"copilot-cli": {
|
|
1171
1326
|
integration: "hooks",
|
|
1172
|
-
|
|
1327
|
+
// Native hooks plus the Lore MCP server entry.
|
|
1328
|
+
expected: (capture) => installedEvents(COPILOT_CLI_HOOK_EVENTS, capture).length + 1,
|
|
1173
1329
|
runtimeRequired: true,
|
|
1174
1330
|
emptyValue: () => ({}),
|
|
1175
1331
|
configPath: (paths) => paths.copilotCliHooks,
|
|
1176
1332
|
detect: async (paths) => (await commandExists("copilot")) || pathExists(paths.copilotCliHooks),
|
|
1177
1333
|
executable: async () => commandExists("copilot"),
|
|
1178
|
-
merge: (input, paths) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths),
|
|
1334
|
+
merge: (input, paths, capture) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths, capture),
|
|
1179
1335
|
remove: (input) => removeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1180
1336
|
count: (input) => countLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1181
1337
|
},
|
|
1182
1338
|
"copilot-vscode": {
|
|
1183
1339
|
integration: "hooks",
|
|
1184
|
-
|
|
1340
|
+
// Repository hooks plus the Lore MCP server entry in .vscode/mcp.json.
|
|
1341
|
+
expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
|
|
1185
1342
|
runtimeRequired: true,
|
|
1186
1343
|
emptyValue: () => ({}),
|
|
1187
1344
|
configPath: (paths) => paths.copilotVscodeHooks,
|
|
1188
1345
|
detect: async (paths) => pathExists(paths.copilotVscodeHooks),
|
|
1189
1346
|
executable: async () => commandExists("code"),
|
|
1190
|
-
merge: (input, paths) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths),
|
|
1347
|
+
merge: (input, paths, capture) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths, capture),
|
|
1191
1348
|
remove: (input) => removeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1192
1349
|
count: (input) => countLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1193
1350
|
},
|
|
1194
1351
|
cursor: {
|
|
1195
1352
|
integration: "hooks",
|
|
1196
|
-
|
|
1353
|
+
// Hooks, the capture bridge, and the Lore MCP server entry.
|
|
1354
|
+
expected: (capture) => installedEvents(CURSOR_HOOK_EVENTS, capture).length + 2,
|
|
1197
1355
|
runtimeRequired: true,
|
|
1198
1356
|
emptyValue: () => ({}),
|
|
1199
1357
|
configPath: (paths) => paths.cursorHooks,
|
|
1200
1358
|
detect: async (paths) => (await cursorAvailable(paths)) || pathExists(paths.cursorHooks),
|
|
1201
1359
|
executable: cursorAvailable,
|
|
1202
|
-
merge: (input, paths) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths),
|
|
1360
|
+
merge: (input, paths, capture) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths, capture),
|
|
1203
1361
|
remove: (input) => removeLoreCursorHooks(objectConfiguration(input, "cursor")),
|
|
1204
1362
|
count: (input) => countLoreCursorHooks(objectConfiguration(input, "cursor")),
|
|
1205
1363
|
},
|
|
1206
1364
|
opencode: {
|
|
1207
1365
|
integration: "plugin",
|
|
1208
|
-
|
|
1366
|
+
// The plugin entry plus the Lore MCP server entry, both in opencode.json.
|
|
1367
|
+
expected: () => 2,
|
|
1209
1368
|
runtimeRequired: false,
|
|
1210
1369
|
emptyValue: () => ({}),
|
|
1211
1370
|
configPath: (paths) => paths.openCodeConfig,
|
|
1212
1371
|
detect: async (paths) => (await commandExists("opencode")) || pathExists(paths.openCodeConfig),
|
|
1213
1372
|
executable: async () => commandExists("opencode"),
|
|
1214
|
-
merge: (input) => mergeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
|
|
1215
|
-
remove: (input) => removeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
|
|
1216
|
-
count: (input) => countLoreOpenCodePlugins(objectConfiguration(input, "opencode")),
|
|
1373
|
+
merge: (input, paths) => mergeLoreOpenCodeMcp(mergeLoreOpenCodePlugin(objectConfiguration(input, "opencode")), loreMcpServer(paths, "opencode")),
|
|
1374
|
+
remove: (input) => removeLoreOpenCodeMcp(removeLoreOpenCodePlugin(objectConfiguration(input, "opencode"))),
|
|
1375
|
+
count: (input) => countLoreOpenCodePlugins(objectConfiguration(input, "opencode")) + countLoreOpenCodeMcp(objectConfiguration(input, "opencode")),
|
|
1217
1376
|
},
|
|
1218
1377
|
polytoken: {
|
|
1219
1378
|
integration: "hooks",
|
|
1220
|
-
expected: POLYTOKEN_HOOK_EVENTS.length,
|
|
1379
|
+
expected: (capture) => installedEvents(POLYTOKEN_HOOK_EVENTS, capture).length,
|
|
1221
1380
|
runtimeRequired: true,
|
|
1222
1381
|
emptyValue: () => [],
|
|
1223
1382
|
configPath: (paths) => paths.polytokenHooks,
|
|
@@ -1400,7 +1559,7 @@ async function readT3CodeProviderInstallations(paths) {
|
|
|
1400
1559
|
}
|
|
1401
1560
|
async function expandConnectInstallations(requested, paths) {
|
|
1402
1561
|
const direct = requested.flatMap((agent) => {
|
|
1403
|
-
if (isCredentialOnlyAgent(agent) || agent === "t3code") {
|
|
1562
|
+
if (isCredentialOnlyAgent(agent) || agent === "t3code" || agent === GENERIC_AGENT) {
|
|
1404
1563
|
return [];
|
|
1405
1564
|
}
|
|
1406
1565
|
return [
|
|
@@ -1433,6 +1592,7 @@ async function installRuntime(paths) {
|
|
|
1433
1592
|
rm(paths.runtime, { force: true }),
|
|
1434
1593
|
rm(paths.runtimeRepository, { force: true }),
|
|
1435
1594
|
rm(resolve(dirname(paths.runtime), "repository-address.js"), { force: true }),
|
|
1595
|
+
rm(resolve(dirname(paths.runtime), "capture-policy.js"), { force: true }),
|
|
1436
1596
|
rm(resolve(dirname(paths.runtime), "directory-policy.js"), { force: true }),
|
|
1437
1597
|
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
1438
1598
|
rm(resolve(dirname(paths.runtime), "windows-permissions.js"), { force: true }),
|
|
@@ -1491,6 +1651,8 @@ async function installRuntime(paths) {
|
|
|
1491
1651
|
"skill-guidance.js": await readFile(resolve(sourceDirectory, "skill-guidance.js"), "utf8"),
|
|
1492
1652
|
"repository.js": repository,
|
|
1493
1653
|
"repository-address.js": await readFile(resolve(dirname(fileURLToPath(import.meta.resolve("@lore-co/core/repository-location"))), "repository-address.js"), "utf8"),
|
|
1654
|
+
// directory-policy.js reads the live capture switches through this dependency-free module.
|
|
1655
|
+
"capture-policy.js": await readFile(resolve(dirname(fileURLToPath(import.meta.resolve("@lore-co/core/local-scope"))), "capture-policy.js"), "utf8"),
|
|
1494
1656
|
"directory-policy.js": (await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/local-scope")), "utf8")).replaceAll("./repository-location.js", "./repository.js"),
|
|
1495
1657
|
"reliability-store.js": reliabilityStore,
|
|
1496
1658
|
"windows-permissions.js": await readFile(resolve(sourceDirectory, "windows-permissions.js"), "utf8"),
|
|
@@ -1524,6 +1686,7 @@ async function installRuntime(paths) {
|
|
|
1524
1686
|
atomicWrite(resolve(dirname(paths.runtime), "skill-guidance.js"), runtimeFiles["skill-guidance.js"], 0o600),
|
|
1525
1687
|
atomicWrite(paths.runtimeRepository, repository, 0o600),
|
|
1526
1688
|
atomicWrite(resolve(dirname(paths.runtime), "repository-address.js"), runtimeFiles["repository-address.js"], 0o600),
|
|
1689
|
+
atomicWrite(resolve(dirname(paths.runtime), "capture-policy.js"), runtimeFiles["capture-policy.js"], 0o600),
|
|
1527
1690
|
atomicWrite(resolve(dirname(paths.runtime), "directory-policy.js"), runtimeFiles["directory-policy.js"], 0o600),
|
|
1528
1691
|
atomicWrite(paths.runtimeReliabilityStore, reliabilityStore, 0o600),
|
|
1529
1692
|
atomicWrite(resolve(dirname(paths.runtime), "windows-permissions.js"), runtimeFiles["windows-permissions.js"], 0o600),
|
|
@@ -1541,13 +1704,109 @@ async function installRuntime(paths) {
|
|
|
1541
1704
|
function agentConfigPath(agent, paths) {
|
|
1542
1705
|
return targetFor(agent).configPath(paths);
|
|
1543
1706
|
}
|
|
1544
|
-
function mergeAgentConfig(input, agent, paths) {
|
|
1707
|
+
function mergeAgentConfig(input, agent, paths, capture) {
|
|
1545
1708
|
const target = targetFor(agent);
|
|
1546
|
-
return target.merge(input === undefined ? target.emptyValue() : input, paths);
|
|
1709
|
+
return target.merge(input === undefined ? target.emptyValue() : input, paths, capture);
|
|
1547
1710
|
}
|
|
1548
1711
|
function removeAgentConfig(input, agent) {
|
|
1549
1712
|
return targetFor(agent).remove(input);
|
|
1550
1713
|
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Read each installation's hooks file and, for MCP hosts, its MCP config, and
|
|
1716
|
+
* merge the Lore entries this CLI version writes. Nothing touches disk yet.
|
|
1717
|
+
*/
|
|
1718
|
+
async function prepareAgentEntries(installations, paths, capture) {
|
|
1719
|
+
const documents = new Map();
|
|
1720
|
+
const mergedDocuments = new Map();
|
|
1721
|
+
const mcpDocuments = new Map();
|
|
1722
|
+
for (const installation of installations) {
|
|
1723
|
+
const key = installationKey(installation);
|
|
1724
|
+
const document = await readJsonDocument(installation.configPath);
|
|
1725
|
+
documents.set(key, document);
|
|
1726
|
+
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths, capture));
|
|
1727
|
+
if (isMcpHostAgent(installation.agent)) {
|
|
1728
|
+
const mcpPath = mcpConfigPath(installation.agent, installation.configPath);
|
|
1729
|
+
if (installation.agent === "codex") {
|
|
1730
|
+
const text = await readTextDocument(mcpPath);
|
|
1731
|
+
mcpDocuments.set(mcpPath, { kind: "text", document: text, merged: mergeLoreCodexMcp(text.value, loreMcpServer(paths, "codex")) });
|
|
1732
|
+
}
|
|
1733
|
+
else {
|
|
1734
|
+
const mcp = await readJsonDocument(mcpPath);
|
|
1735
|
+
const base = mcp.exists ? mcp.value : {};
|
|
1736
|
+
// Cursor keeps its capture bridge next to the shared tool server.
|
|
1737
|
+
const withBridge = installation.agent === "cursor" ? mergeLoreCursorMcp(base, paths) : base;
|
|
1738
|
+
mcpDocuments.set(mcpPath, { kind: "json", document: mcp, merged: mergeLoreMcp(withBridge, installation.agent, paths) });
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return { installations: [...installations], documents, mergedDocuments, mcpDocuments };
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Install the hook runtime when an installation needs it, then write the
|
|
1746
|
+
* merged files. Unchanged files are left alone; changed ones get a backup.
|
|
1747
|
+
*/
|
|
1748
|
+
async function writeAgentEntries(entries, paths, now) {
|
|
1749
|
+
if (entries.installations.some((installation) => targetFor(installation.agent).runtimeRequired)) {
|
|
1750
|
+
await installRuntime(paths);
|
|
1751
|
+
}
|
|
1752
|
+
const changedFiles = [];
|
|
1753
|
+
const backups = [];
|
|
1754
|
+
for (const [path, entry] of entries.mcpDocuments) {
|
|
1755
|
+
const result = entry.kind === "json"
|
|
1756
|
+
? await writeMergedJson(path, entry.document, entry.merged, now)
|
|
1757
|
+
: await writeMergedText(path, entry.document, entry.merged, now);
|
|
1758
|
+
if (result.changed)
|
|
1759
|
+
changedFiles.push(path);
|
|
1760
|
+
if (result.backup)
|
|
1761
|
+
backups.push(result.backup);
|
|
1762
|
+
}
|
|
1763
|
+
for (const installation of entries.installations) {
|
|
1764
|
+
const key = installationKey(installation);
|
|
1765
|
+
const document = entries.documents.get(key);
|
|
1766
|
+
const merged = entries.mergedDocuments.get(key);
|
|
1767
|
+
if (document === undefined || merged === undefined) {
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
const result = await writeMergedJson(installation.configPath, document, merged, now);
|
|
1771
|
+
if (result.changed) {
|
|
1772
|
+
changedFiles.push(installation.configPath);
|
|
1773
|
+
}
|
|
1774
|
+
if (result.backup !== undefined) {
|
|
1775
|
+
backups.push(result.backup);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
return { changedFiles, backups };
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Re-apply the Lore-owned hook and MCP entries and the `lore` skill for the
|
|
1782
|
+
* agents recorded in the stored connection, for example after `lore update`
|
|
1783
|
+
* replaced the binary. Nothing is authorized again and folder rules are
|
|
1784
|
+
* untouched. Resolves to null when no connection is stored.
|
|
1785
|
+
*/
|
|
1786
|
+
export async function refreshAgentEntries(paths = getLorePaths()) {
|
|
1787
|
+
const config = await readConnectorConfig(paths);
|
|
1788
|
+
if (config === null)
|
|
1789
|
+
return null;
|
|
1790
|
+
const installations = uniqueInstallations(configuredInstallations(config, paths));
|
|
1791
|
+
return withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
|
|
1792
|
+
const entries = await prepareAgentEntries(installations, paths, captureEnabled(config));
|
|
1793
|
+
const written = await writeAgentEntries(entries, paths, new Date());
|
|
1794
|
+
const generic = (await readGenericAgents(paths.loreDirectory)).length > 0;
|
|
1795
|
+
const skills = await installLoreSkill(paths, generic ? [...installations, { agent: GENERIC_AGENT, configPath: paths.config }] : installations);
|
|
1796
|
+
const agent = await installLoreAgent(paths, installations);
|
|
1797
|
+
return {
|
|
1798
|
+
// Same order the files are written in, so the two update lines match.
|
|
1799
|
+
configFiles: [
|
|
1800
|
+
...entries.mcpDocuments.keys(),
|
|
1801
|
+
...installations.map((installation) => installation.configPath),
|
|
1802
|
+
],
|
|
1803
|
+
...written,
|
|
1804
|
+
skillFiles: skills.files,
|
|
1805
|
+
agentFiles: agent.files,
|
|
1806
|
+
warnings: [...skills.warnings, ...agent.warnings],
|
|
1807
|
+
};
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1551
1810
|
function countAgentIntegrations(input, agent) {
|
|
1552
1811
|
const target = targetFor(agent);
|
|
1553
1812
|
return target.count(input === undefined ? target.emptyValue() : input);
|
|
@@ -1595,6 +1854,14 @@ function parseConnectArguments(args) {
|
|
|
1595
1854
|
parsed.recovery = false;
|
|
1596
1855
|
continue;
|
|
1597
1856
|
}
|
|
1857
|
+
if (argument === "--no-capture") {
|
|
1858
|
+
parsed.capture = false;
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (argument === "--capture") {
|
|
1862
|
+
parsed.capture = true;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1598
1865
|
if (argument === "--current-project") {
|
|
1599
1866
|
parsed.currentProject = true;
|
|
1600
1867
|
continue;
|
|
@@ -1607,6 +1874,10 @@ function parseConnectArguments(args) {
|
|
|
1607
1874
|
parsed.allProjects = true;
|
|
1608
1875
|
continue;
|
|
1609
1876
|
}
|
|
1877
|
+
if (argument === "--write") {
|
|
1878
|
+
parsed.writeTools = true;
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1610
1881
|
if (argument !== "--repository" &&
|
|
1611
1882
|
argument !== "--folder" &&
|
|
1612
1883
|
argument !== "--name" &&
|
|
@@ -1614,6 +1885,7 @@ function parseConnectArguments(args) {
|
|
|
1614
1885
|
argument !== "--dashboard-url" &&
|
|
1615
1886
|
argument !== "--token" &&
|
|
1616
1887
|
argument !== "--agent" &&
|
|
1888
|
+
argument !== "--agent-id" &&
|
|
1617
1889
|
argument !== "--fallback" &&
|
|
1618
1890
|
argument !== "--timeout-ms") {
|
|
1619
1891
|
throw new Error(`Unknown connect option: ${argument ?? ""}`);
|
|
@@ -1629,6 +1901,9 @@ function parseConnectArguments(args) {
|
|
|
1629
1901
|
else if (argument === "--name") {
|
|
1630
1902
|
parsed.connectionName = value;
|
|
1631
1903
|
}
|
|
1904
|
+
else if (argument === "--agent-id") {
|
|
1905
|
+
parsed.agentId = parseGenericAgentId(value);
|
|
1906
|
+
}
|
|
1632
1907
|
else if (argument === "--fallback") {
|
|
1633
1908
|
if (!isLaunchable(value))
|
|
1634
1909
|
throw new Error(`--fallback must be ${LAUNCHABLE_AGENTS.join(", ")}`);
|
|
@@ -1654,10 +1929,13 @@ function parseConnectArguments(args) {
|
|
|
1654
1929
|
parsed.agents.push(value);
|
|
1655
1930
|
}
|
|
1656
1931
|
else {
|
|
1657
|
-
throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, opencode, polytoken, or t3code");
|
|
1932
|
+
throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, devin, generic, opencode, polytoken, or t3code");
|
|
1658
1933
|
}
|
|
1659
1934
|
}
|
|
1660
1935
|
validateConnectProjectOptions(parsed);
|
|
1936
|
+
if ((parsed.agentId !== undefined || parsed.writeTools) && !parsed.agents.includes(GENERIC_AGENT)) {
|
|
1937
|
+
throw new Error("--agent-id and --write apply to --agent generic.");
|
|
1938
|
+
}
|
|
1661
1939
|
if (parsed.login && parsed.token !== undefined) {
|
|
1662
1940
|
throw new Error("--login and --token cannot be used together");
|
|
1663
1941
|
}
|
|
@@ -1755,7 +2033,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1755
2033
|
...new Set([
|
|
1756
2034
|
...requestedExpansion.installations.map((installation) => installation.agent),
|
|
1757
2035
|
...detectedAgents,
|
|
1758
|
-
...options.agents.filter((agent) => isCredentialOnlyAgent(agent) || agent === "t3code"),
|
|
2036
|
+
...options.agents.filter((agent) => isCredentialOnlyAgent(agent) || agent === "t3code" || agent === GENERIC_AGENT),
|
|
1759
2037
|
]),
|
|
1760
2038
|
].sort();
|
|
1761
2039
|
const keptAgents = [
|
|
@@ -1769,24 +2047,17 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1769
2047
|
// Cowork and the Devin CLI read the saved credential from ~/.lore/config.json
|
|
1770
2048
|
// and need no hooks, so they connect without a native agent installation.
|
|
1771
2049
|
const credentialOnlyAgents = CREDENTIAL_ONLY_AGENTS.filter((agent) => options.agents.includes(agent));
|
|
1772
|
-
|
|
2050
|
+
// An MCP-only agent has no hooks or settings Lore can edit; it gets the skills and a printed entry.
|
|
2051
|
+
const genericAgent = options.agents.includes(GENERIC_AGENT) ? options.genericAgent ?? { id: GENERIC_AGENT, writeTools: false } : undefined;
|
|
2052
|
+
if (agents.length === 0 && credentialOnlyAgents.length === 0 && genericAgent === undefined) {
|
|
1773
2053
|
throw new Error("No Claude, Codex, Copilot CLI, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code, and Devin CLI users --agent devin.");
|
|
1774
2054
|
}
|
|
1775
2055
|
const now = new Date();
|
|
1776
|
-
|
|
1777
|
-
const
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const document = await readJsonDocument(installation.configPath);
|
|
1782
|
-
documents.set(key, document);
|
|
1783
|
-
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths));
|
|
1784
|
-
if (installation.agent === "cursor") {
|
|
1785
|
-
const mcpPath = resolve(dirname(installation.configPath), "mcp.json");
|
|
1786
|
-
const mcp = await readJsonDocument(mcpPath);
|
|
1787
|
-
cursorMcpDocuments.set(mcpPath, { document: mcp, merged: mergeLoreCursorMcp(mcp.exists ? mcp.value : {}, paths) });
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
2056
|
+
// Reconnecting without a capture option keeps the saved mode, like folder rules.
|
|
2057
|
+
const capture = options.capture ?? captureEnabled(existing);
|
|
2058
|
+
// Read and merge before anything is persisted so an unreadable agent file
|
|
2059
|
+
// stops the run without a half-written connection.
|
|
2060
|
+
const entries = await prepareAgentEntries(installations, paths, capture);
|
|
1790
2061
|
const agentConfigPaths = {};
|
|
1791
2062
|
for (const agent of agents) {
|
|
1792
2063
|
agentConfigPaths[agent] = installations
|
|
@@ -1802,6 +2073,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1802
2073
|
token: token.trim(),
|
|
1803
2074
|
agents,
|
|
1804
2075
|
agentConfigPaths,
|
|
2076
|
+
...(capture ? {} : { capture: "off" }),
|
|
1805
2077
|
connectedAt: connection.profiles.find(value => value.connectionId === connection.fields.connectionId)?.connectedAt ?? now.toISOString(),
|
|
1806
2078
|
timeoutMs,
|
|
1807
2079
|
};
|
|
@@ -1812,33 +2084,10 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1812
2084
|
await reliabilityStore.initialize();
|
|
1813
2085
|
// Folder restrictions must be durable before any new hook can run.
|
|
1814
2086
|
await persistConnection(paths, config, connection.profiles);
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const backups = [];
|
|
1820
|
-
for (const [path, { document, merged }] of cursorMcpDocuments) {
|
|
1821
|
-
const result = await writeMergedJson(path, document, merged, now);
|
|
1822
|
-
if (result.changed)
|
|
1823
|
-
changedHookFiles.push(path);
|
|
1824
|
-
if (result.backup)
|
|
1825
|
-
backups.push(result.backup);
|
|
1826
|
-
}
|
|
1827
|
-
for (const installation of installations) {
|
|
1828
|
-
const key = installationKey(installation);
|
|
1829
|
-
const document = documents.get(key);
|
|
1830
|
-
const merged = mergedDocuments.get(key);
|
|
1831
|
-
if (document === undefined || merged === undefined) {
|
|
1832
|
-
continue;
|
|
1833
|
-
}
|
|
1834
|
-
const result = await writeMergedJson(installation.configPath, document, merged, now);
|
|
1835
|
-
if (result.changed) {
|
|
1836
|
-
changedHookFiles.push(installation.configPath);
|
|
1837
|
-
}
|
|
1838
|
-
if (result.backup !== undefined) {
|
|
1839
|
-
backups.push(result.backup);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
2087
|
+
const { changedFiles: changedHookFiles, backups } = await writeAgentEntries(entries, paths, now);
|
|
2088
|
+
// The Devin wrapper and hook check the capture mode when they run, so stale
|
|
2089
|
+
// Devin hooks record nothing; an unreadable Devin config must not fail connect.
|
|
2090
|
+
changedHookFiles.push(...await refreshDevinHooks(paths.home, capture).catch(() => []));
|
|
1842
2091
|
if (existing && !existing.connectionId && existing.apiUrl === apiUrl && existing.token === token) {
|
|
1843
2092
|
await new ReliabilityStore(identity.workspaceId, { home: paths.home }).transferPendingTo(reliabilityStore);
|
|
1844
2093
|
await reliabilityStore.migrateLegacyQueue(paths.queue);
|
|
@@ -1858,7 +2107,10 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1858
2107
|
.catch((error) => [`Host recovery setup: ${error.message}`]) : [];
|
|
1859
2108
|
if (options.recovery === false)
|
|
1860
2109
|
await disableRecovery(paths.home);
|
|
1861
|
-
const skills = await installLoreSkill(paths, installations);
|
|
2110
|
+
const skills = await installLoreSkill(paths, genericAgent ? [...installations, { agent: GENERIC_AGENT, configPath: paths.config }] : installations);
|
|
2111
|
+
const loreAgent = await installLoreAgent(paths, installations);
|
|
2112
|
+
if (genericAgent)
|
|
2113
|
+
await recordGenericAgent(paths.loreDirectory, genericAgent, now);
|
|
1862
2114
|
// An unchanged reconnect: same server, credential, and workspace, every
|
|
1863
2115
|
// configured agent was already configured, and no agent file was rewritten.
|
|
1864
2116
|
const alreadyConnected = existing !== null &&
|
|
@@ -1867,6 +2119,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1867
2119
|
existing.workspaceId === identity.workspaceId &&
|
|
1868
2120
|
existing.connectionId === config.connectionId &&
|
|
1869
2121
|
agents.every((agent) => existing.agents.includes(agent)) &&
|
|
2122
|
+
captureEnabled(existing) === capture &&
|
|
1870
2123
|
changedHookFiles.length === 0 &&
|
|
1871
2124
|
JSON.stringify(existing.directories ?? null) === JSON.stringify(config.directories ?? null);
|
|
1872
2125
|
// A Codex set up in this run gets the approval step in its next-step line;
|
|
@@ -1877,12 +2130,18 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1877
2130
|
const codexNeedsApproval = !installedAgents.includes("codex") &&
|
|
1878
2131
|
changedHookFiles.some((path) => codexHookPaths.includes(path));
|
|
1879
2132
|
return {
|
|
1880
|
-
capture: { folders: config.directories?.include ?? null, excludedFolders: config.directories?.exclude ?? [] },
|
|
2133
|
+
capture: { mode: capture ? "on" : "off", folders: config.directories?.include ?? null, excludedFolders: config.directories?.exclude ?? [] },
|
|
1881
2134
|
skillFiles: skills.files,
|
|
2135
|
+
agentFiles: loreAgent.files,
|
|
2136
|
+
...(genericAgent ? { genericAgent: {
|
|
2137
|
+
...genericAgent,
|
|
2138
|
+
mcpServers: genericMcpServers(loreMcpInvocation(), paths.home, genericAgent).mcpServers,
|
|
2139
|
+
instructions: genericInstructions(genericAgent.id, skills.files.filter((file) => file.startsWith(resolve(paths.home, ".agents", "skills")))),
|
|
2140
|
+
} } : {}),
|
|
1882
2141
|
connected: true,
|
|
1883
2142
|
apiUrl: config.apiUrl,
|
|
1884
2143
|
identity,
|
|
1885
|
-
agents: [...agents, ...credentialOnlyAgents],
|
|
2144
|
+
agents: [...agents, ...credentialOnlyAgents, ...(genericAgent ? [GENERIC_AGENT] : [])],
|
|
1886
2145
|
installedAgents,
|
|
1887
2146
|
keptAgents,
|
|
1888
2147
|
alreadyConnected,
|
|
@@ -1891,7 +2150,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1891
2150
|
config: paths.config,
|
|
1892
2151
|
changedHookFiles,
|
|
1893
2152
|
backups,
|
|
1894
|
-
warnings: [...skills.warnings, ...(connection.needsFolders ? [`Lore is off for ${config.connectionName} until you choose where to record. Reconnect with --folder <path> or --all-projects, or run lore config open --connection ${config.connectionName}.`] : []), ...requestedExpansion.warnings, ...hostRecoveryWarnings, ...(codexNeedsApproval
|
|
2153
|
+
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
|
|
1895
2154
|
? ["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."]
|
|
1896
2155
|
: [])],
|
|
1897
2156
|
...(recoveryNotice ? { recoveryNotice } : {}),
|
|
@@ -2024,11 +2283,11 @@ async function acknowledgeDeviceAuthorization(apiUrl, authorization) {
|
|
|
2024
2283
|
export function connectNextStep(agent) {
|
|
2025
2284
|
switch (agent) {
|
|
2026
2285
|
case "claude":
|
|
2027
|
-
return "Restart Claude Code.";
|
|
2286
|
+
return "Restart Claude Code and allow the lore MCP server when it asks.";
|
|
2028
2287
|
case "codex":
|
|
2029
2288
|
return "Restart Codex, open /hooks, approve Lore's hooks, then start a fresh session.";
|
|
2030
2289
|
case "cursor":
|
|
2031
|
-
return "Restart Cursor and enable the lore-native
|
|
2290
|
+
return "Restart Cursor and enable the lore-native and lore MCP servers.";
|
|
2032
2291
|
case "opencode":
|
|
2033
2292
|
return "Restart OpenCode.";
|
|
2034
2293
|
case "copilot":
|
|
@@ -2043,12 +2302,17 @@ export function connectNextStep(agent) {
|
|
|
2043
2302
|
case "cowork":
|
|
2044
2303
|
case "t3code":
|
|
2045
2304
|
return "Restart your agent and complete a turn.";
|
|
2305
|
+
case "generic":
|
|
2306
|
+
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.";
|
|
2046
2307
|
}
|
|
2047
2308
|
}
|
|
2048
2309
|
export function formatConnectResult(result) {
|
|
2049
|
-
const
|
|
2310
|
+
const folderSummary = result.capture.folders === null ? "all projects allowed by the workspace"
|
|
2050
2311
|
: result.capture.folders.length ? result.capture.folders.join(", ") : "paused — choose folders with --folder <path>";
|
|
2051
2312
|
const installed = result.installedAgents.join(", ");
|
|
2313
|
+
const generic = result.genericAgent;
|
|
2314
|
+
// With only an MCP agent connected, nothing on this machine records turns.
|
|
2315
|
+
const nothingRecords = result.capture.mode === "off" || (generic !== undefined && result.agents.every((agent) => agent === GENERIC_AGENT));
|
|
2052
2316
|
const lines = [
|
|
2053
2317
|
result.alreadyConnected
|
|
2054
2318
|
? `Already connected to ${result.identity.workspaceName} for ${installed}; nothing changed.`
|
|
@@ -2056,14 +2320,32 @@ export function formatConnectResult(result) {
|
|
|
2056
2320
|
...(result.keptAgents.length ? [`kept: ${result.keptAgents.join(", ")}`] : []),
|
|
2057
2321
|
`api_url: ${result.apiUrl}`,
|
|
2058
2322
|
`config: ${result.config}`,
|
|
2059
|
-
|
|
2323
|
+
...(result.capture.mode === "off" ? [`capture: ${captureSummary("off")}`] : []),
|
|
2324
|
+
...(generic ? [`capture for ${generic.id}: off; an MCP-only agent has no hooks, so Lore saves only what you ask it to`] : []),
|
|
2325
|
+
`${nothingRecords ? "folders" : "capture"}: ${folderSummary}${result.capture.excludedFolders.length ? `; excluded: ${result.capture.excludedFolders.join(", ")}` : ""}`,
|
|
2060
2326
|
...result.installedAgents.map((agent) => `Next step for ${agent}: ${connectNextStep(agent)}`),
|
|
2061
|
-
|
|
2062
|
-
? `
|
|
2063
|
-
:
|
|
2064
|
-
|
|
2327
|
+
generic !== undefined && nothingRecords
|
|
2328
|
+
? `Ask your agent "Lore, has anything like this come up before?" in ${result.folder}, then run lore doctor.`
|
|
2329
|
+
: result.capture.mode === "off"
|
|
2330
|
+
? `Send one message in ${result.folder}, then run lore doctor to confirm context delivery.`
|
|
2331
|
+
: result.dashboardUrl === undefined
|
|
2332
|
+
? `Send one message in ${result.folder}, then run lore doctor to confirm recording.`
|
|
2333
|
+
: `Send one message in ${result.folder}, then open ${result.dashboardUrl}/connect to confirm recording.`,
|
|
2334
|
+
...(result.skillFiles.length ? ["Choose lore in your agent’s skills to find knowledge or remember this session, or recap to write a session up by hand."] : []),
|
|
2335
|
+
...(result.agentFiles.length ? ["Delegate to the lore agent (\"use the lore agent to review my pending learnings\") for recall, proposal review, or a recap with a fresh context."] : []),
|
|
2065
2336
|
...(result.recoveryNotice ? [result.recoveryNotice] : []),
|
|
2066
2337
|
...result.warnings.map((warning) => `warning: ${warning}`),
|
|
2338
|
+
...(generic ? [
|
|
2339
|
+
generic.writeTools
|
|
2340
|
+
? `Saves from ${generic.id} are proposals credited to ${generic.id}; review them in Learnings.`
|
|
2341
|
+
: `${generic.id} can read Lore; it saves through the lore CLI. Reconnect with --write to let it save through MCP (saves stay proposals).`,
|
|
2342
|
+
"",
|
|
2343
|
+
`MCP entry for ${generic.id} (merge into its mcpServers settings):`,
|
|
2344
|
+
JSON.stringify({ mcpServers: generic.mcpServers }, null, 2),
|
|
2345
|
+
"",
|
|
2346
|
+
"Instructions for AGENTS.md, GEMINI.md, or the agent's rules file:",
|
|
2347
|
+
generic.instructions,
|
|
2348
|
+
] : []),
|
|
2067
2349
|
];
|
|
2068
2350
|
return `${lines.join("\n")}\n`;
|
|
2069
2351
|
}
|
|
@@ -2107,13 +2389,17 @@ async function connectCommand(args) {
|
|
|
2107
2389
|
}
|
|
2108
2390
|
const result = await connectWithCredential({
|
|
2109
2391
|
...capture,
|
|
2392
|
+
...(parsed.capture === undefined ? {} : { capture: parsed.capture }),
|
|
2110
2393
|
...(parsed.connectionName === undefined ? {} : { connectionName: parsed.connectionName }),
|
|
2111
2394
|
...(parsed.fallback === undefined ? {} : { fallback: parsed.fallback }),
|
|
2112
|
-
|
|
2395
|
+
// Automatic recovery installs hooks for other agents on PATH; an MCP-only
|
|
2396
|
+
// connection must not start recording them as a side effect.
|
|
2397
|
+
recovery: parsed.recovery ?? !(parsed.agents.length > 0 && parsed.agents.every((agent) => agent === GENERIC_AGENT)),
|
|
2113
2398
|
apiUrl,
|
|
2114
2399
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
2115
2400
|
token,
|
|
2116
2401
|
agents: parsed.agents,
|
|
2402
|
+
...(parsed.agents.includes(GENERIC_AGENT) ? { genericAgent: { id: parsed.agentId ?? GENERIC_AGENT, writeTools: parsed.writeTools === true } } : {}),
|
|
2117
2403
|
...(parsed.timeoutMs === undefined ? {} : { timeoutMs: parsed.timeoutMs }),
|
|
2118
2404
|
});
|
|
2119
2405
|
if (authorization)
|
|
@@ -2235,6 +2521,7 @@ async function inspectRuntimeInstallation(required, paths) {
|
|
|
2235
2521
|
"skill-guidance.js": resolve(dirname(paths.runtime), "skill-guidance.js"),
|
|
2236
2522
|
"repository.js": paths.runtimeRepository,
|
|
2237
2523
|
"repository-address.js": resolve(dirname(paths.runtime), "repository-address.js"),
|
|
2524
|
+
"capture-policy.js": resolve(dirname(paths.runtime), "capture-policy.js"),
|
|
2238
2525
|
"directory-policy.js": resolve(dirname(paths.runtime), "directory-policy.js"),
|
|
2239
2526
|
"reliability-store.js": paths.runtimeReliabilityStore,
|
|
2240
2527
|
"windows-permissions.js": resolve(dirname(paths.runtime), "windows-permissions.js"),
|
|
@@ -2349,14 +2636,31 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
2349
2636
|
existing += document.exists ? 1 : 0;
|
|
2350
2637
|
units.push(...inspectInstallationUnits(document.value === undefined
|
|
2351
2638
|
? target.emptyValue()
|
|
2352
|
-
: document.value, agent, paths, index, configPaths.length));
|
|
2353
|
-
if (agent
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2639
|
+
: document.value, agent, paths, index, configPaths.length, captureEnabled(config)));
|
|
2640
|
+
if (isMcpHostAgent(agent)) {
|
|
2641
|
+
const mcpPath = mcpConfigPath(agent, path);
|
|
2642
|
+
if (agent === "codex") {
|
|
2643
|
+
const state = inspectCodexMcp((await readTextDocument(mcpPath)).value, loreMcpServer(paths, "codex"));
|
|
2644
|
+
installed += state.owned;
|
|
2645
|
+
units.push({ id: installationUnitId("mcp-tools", index, configPaths.length),
|
|
2646
|
+
state: installationUnitState(state.owned, Number(state.exact)) });
|
|
2647
|
+
}
|
|
2648
|
+
else {
|
|
2649
|
+
const mcp = await readJsonDocument(mcpPath);
|
|
2650
|
+
const servers = isObject(mcp.value) && isObject(mcp.value.mcpServers) ? mcp.value.mcpServers : {};
|
|
2651
|
+
if (agent === "cursor") {
|
|
2652
|
+
const bridge = servers[CURSOR_MCP_NAME];
|
|
2653
|
+
const owned = Number(isLoreCursorMcp(bridge));
|
|
2654
|
+
installed += owned;
|
|
2655
|
+
units.push({ id: installationUnitId("mcp", index, configPaths.length),
|
|
2656
|
+
state: installationUnitState(owned, Number(exactConfiguration(bridge, cursorMcpServer(paths)))) });
|
|
2657
|
+
}
|
|
2658
|
+
const value = agent === "copilot-vscode" ? loreVsCodeMcpServer(mcp.value) : servers[LORE_MCP_SERVER_NAME];
|
|
2659
|
+
const owned = Number(isLoreMcpServer(value));
|
|
2660
|
+
installed += owned;
|
|
2661
|
+
units.push({ id: installationUnitId("mcp-tools", index, configPaths.length),
|
|
2662
|
+
state: installationUnitState(owned, Number(exactConfiguration(value, loreMcpServer(paths, agent)))) });
|
|
2663
|
+
}
|
|
2360
2664
|
}
|
|
2361
2665
|
}
|
|
2362
2666
|
catch {
|
|
@@ -2377,7 +2681,7 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
2377
2681
|
state: "not_required",
|
|
2378
2682
|
version: null,
|
|
2379
2683
|
};
|
|
2380
|
-
const expected = target.expected * configPaths.length;
|
|
2684
|
+
const expected = target.expected(captureEnabled(config)) * configPaths.length;
|
|
2381
2685
|
const exactUnits = units.filter((unit) => unit.state === "exact").length;
|
|
2382
2686
|
const installationState = invalidConfiguration
|
|
2383
2687
|
? "invalid"
|
|
@@ -2440,13 +2744,17 @@ async function statusData(paths, cwd) {
|
|
|
2440
2744
|
}
|
|
2441
2745
|
const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
|
|
2442
2746
|
const runtime = await inspectRuntimeInstallation(runtimeRequired, paths);
|
|
2443
|
-
const [legacyQueuedTurns, reliability, agents] = await Promise.all([
|
|
2747
|
+
const [legacyQueuedTurns, reliability, agents, switches] = await Promise.all([
|
|
2444
2748
|
queueCount(paths),
|
|
2445
2749
|
inspectReliabilityStore(config, paths),
|
|
2446
2750
|
Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths, runtime))),
|
|
2751
|
+
// Only a machine that records turns needs the dashboard switches.
|
|
2752
|
+
captureEnabled(config) ? captureSwitches(config, paths.home, cwd) : CAPTURE_SWITCHES_UNCHECKED,
|
|
2447
2753
|
]);
|
|
2448
2754
|
return {
|
|
2449
2755
|
folderAccess: await checkDirectory(cwd, paths.home, config ?? undefined),
|
|
2756
|
+
capture: captureEnabled(config) ? "on" : "off",
|
|
2757
|
+
captureSwitches: switches,
|
|
2450
2758
|
schemaVersion: "lore-cli-status-v1",
|
|
2451
2759
|
integrationContractVersion: RELIABILITY_INTEGRATION_CONTRACT_VERSION,
|
|
2452
2760
|
connected: config !== null,
|
|
@@ -2495,7 +2803,7 @@ async function statusCommand(args) {
|
|
|
2495
2803
|
? "-"
|
|
2496
2804
|
: `${(value * 100).toFixed(1)}%`;
|
|
2497
2805
|
const retrievalFailureLines = formatRetrievalFailureDiagnostics(data.reliability?.operational.retrieval);
|
|
2498
|
-
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\ncurrent_folder: ${data.folderAccess.allowed ? "allowed" : "off"} (${data.folderAccess.reason})\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? `installed (${data.runtimeVersion})` : data.runtimeVersion === null ? "missing" : `stale (${data.runtimeVersion})`) : "not required"}\ndurable_outbox: ${data.reliability?.outbox.total ?? 0} ready=${data.reliability?.outbox.ready ?? 0} auth_blocked=${data.reliability?.outbox.authBlocked ?? 0} dead=${data.reliability?.outbox.dead ?? 0}\noldest_capture: ${data.reliability?.outbox.oldestEnqueuedAt ?? "-"}\ncontext_snapshots: ${data.reliability?.cache.context ?? 0}\noldest_context_snapshot: ${data.reliability?.cache.oldestContextWrittenAt ?? "-"}\npolicy_snapshots: ${data.reliability?.cache.policy ?? 0}\nlocal_context_fallback_rate: ${metricRate(data.reliability?.operational.retrieval.fallbackRate)}\nlocal_guard_coverage: ${metricRate(data.reliability?.operational.guard.coverageRate)}\nlocal_guard_failures: ${data.reliability?.operational.guard.failures ?? 0}\nlocal_retrieval_failures: ${data.reliability?.operational.retrieval.failed ?? 0}\n${retrievalFailureLines}${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
|
|
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`);
|
|
2499
2807
|
}
|
|
2500
2808
|
async function disconnectCommand(args) {
|
|
2501
2809
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -2518,15 +2826,29 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2518
2826
|
]);
|
|
2519
2827
|
for (const installation of installations) {
|
|
2520
2828
|
const path = installation.configPath;
|
|
2521
|
-
if (installation.agent
|
|
2522
|
-
const mcpPath =
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2829
|
+
if (isMcpHostAgent(installation.agent)) {
|
|
2830
|
+
const mcpPath = mcpConfigPath(installation.agent, path);
|
|
2831
|
+
if (installation.agent === "codex") {
|
|
2832
|
+
const text = await readTextDocument(mcpPath);
|
|
2833
|
+
if (text.exists) {
|
|
2834
|
+
const result = await writeMergedText(mcpPath, text, removeLoreCodexMcp(text.value), now);
|
|
2835
|
+
if (result.changed)
|
|
2836
|
+
changedHookFiles.push(mcpPath);
|
|
2837
|
+
if (result.backup)
|
|
2838
|
+
backups.push(result.backup);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
else {
|
|
2842
|
+
const mcp = await readJsonDocument(mcpPath);
|
|
2843
|
+
if (mcp.exists) {
|
|
2844
|
+
const withoutTools = removeLoreMcp(mcp.value, installation.agent);
|
|
2845
|
+
const cleaned = installation.agent === "cursor" ? removeLoreCursorMcp(withoutTools) : withoutTools;
|
|
2846
|
+
const result = await writeMergedJson(mcpPath, mcp, cleaned, now);
|
|
2847
|
+
if (result.changed)
|
|
2848
|
+
changedHookFiles.push(mcpPath);
|
|
2849
|
+
if (result.backup)
|
|
2850
|
+
backups.push(result.backup);
|
|
2851
|
+
}
|
|
2530
2852
|
}
|
|
2531
2853
|
}
|
|
2532
2854
|
const document = await readJsonDocument(path);
|
|
@@ -2541,13 +2863,16 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2541
2863
|
backups.push(result.backup);
|
|
2542
2864
|
}
|
|
2543
2865
|
}
|
|
2544
|
-
await removeLoreSkill(paths, installations);
|
|
2866
|
+
await removeLoreSkill(paths, [...installations, { agent: GENERIC_AGENT, configPath: paths.config }]);
|
|
2867
|
+
await removeLoreAgent(paths, installations);
|
|
2868
|
+
await removeGenericAgents(paths.loreDirectory);
|
|
2545
2869
|
await Promise.all([
|
|
2546
2870
|
rm(paths.config, { force: true }),
|
|
2547
2871
|
rm(resolve(paths.loreDirectory, "connections"), { recursive: true, force: true }),
|
|
2548
2872
|
rm(paths.runtime, { force: true }),
|
|
2549
2873
|
rm(paths.runtimeRepository, { force: true }),
|
|
2550
2874
|
rm(resolve(dirname(paths.runtime), "repository-address.js"), { force: true }),
|
|
2875
|
+
rm(resolve(dirname(paths.runtime), "capture-policy.js"), { force: true }),
|
|
2551
2876
|
rm(resolve(dirname(paths.runtime), "directory-policy.js"), { force: true }),
|
|
2552
2877
|
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
2553
2878
|
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
@@ -2573,6 +2898,47 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2573
2898
|
]);
|
|
2574
2899
|
return { connected: false, changedHookFiles, backups };
|
|
2575
2900
|
}
|
|
2901
|
+
const CAPTURE_SWITCHES_UNCHECKED = { pausedBy: null, checked: false, reason: "not connected" };
|
|
2902
|
+
async function captureSwitches(config, home, cwd) {
|
|
2903
|
+
if (config === null)
|
|
2904
|
+
return CAPTURE_SWITCHES_UNCHECKED;
|
|
2905
|
+
const access = await checkLoreAccess(cwd, home, config);
|
|
2906
|
+
// A folder outside the local rules never reaches the workspace policy.
|
|
2907
|
+
if (access.failure !== undefined || !access.reason.startsWith("Repository"))
|
|
2908
|
+
return { pausedBy: null, checked: false, reason: access.reason };
|
|
2909
|
+
return { pausedBy: access.capturePausedBy ?? null, checked: true, reason: access.capturePausedBy ? capturePauseMessage(access.capturePausedBy) : "on" };
|
|
2910
|
+
}
|
|
2911
|
+
/** How the dashboard switches are resumed, for status and doctor wording. */
|
|
2912
|
+
function captureResumeHint(pausedBy) {
|
|
2913
|
+
return pausedBy === "workspace"
|
|
2914
|
+
? "An owner or admin can resume it under Settings, Session capture."
|
|
2915
|
+
: "Resume it under Settings, Session capture.";
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* The effective capture state for `lore capture status` and `lore status`:
|
|
2919
|
+
* the machine's own choice, then the dashboard switch that pauses it, worded
|
|
2920
|
+
* as `lore doctor` words it.
|
|
2921
|
+
*/
|
|
2922
|
+
export function captureSummary(capture, switches = CAPTURE_SWITCHES_UNCHECKED) {
|
|
2923
|
+
if (capture === "off")
|
|
2924
|
+
return "off by choice; context and guard active";
|
|
2925
|
+
if (switches.pausedBy === null)
|
|
2926
|
+
return "on; turns are recorded in allowed folders";
|
|
2927
|
+
// "Capture paused by workspace setting" -> "on locally, paused by workspace setting"
|
|
2928
|
+
return `on locally, ${capturePauseMessage(switches.pausedBy).replace(/^Capture p/u, "p")}; context and guard active`;
|
|
2929
|
+
}
|
|
2930
|
+
/** Retrieval-only mode and a paused switch are chosen configurations, never warnings. */
|
|
2931
|
+
export function captureModeCheck(capture, switches = CAPTURE_SWITCHES_UNCHECKED) {
|
|
2932
|
+
return {
|
|
2933
|
+
name: "capture-mode",
|
|
2934
|
+
status: "ok",
|
|
2935
|
+
detail: capture === "off"
|
|
2936
|
+
? "Capture off by choice; context and guard active. lore remember still saves what you choose. Run lore capture on to record turns again."
|
|
2937
|
+
: switches.pausedBy !== null
|
|
2938
|
+
? `${capturePauseMessage(switches.pausedBy)}; context and guard active. Saved learnings and /recap still work. ${captureResumeHint(switches.pausedBy)}`
|
|
2939
|
+
: "on: turns are recorded in allowed folders",
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2576
2942
|
/**
|
|
2577
2943
|
* Server-side queue health. Dead embedding jobs name the operator fix: once the
|
|
2578
2944
|
* provider is repaired, `lore embeddings repair` retries them without waiting
|
|
@@ -2714,6 +3080,93 @@ async function verifyConnectedInstallation() {
|
|
|
2714
3080
|
}
|
|
2715
3081
|
return (await apiChecks(config)).every((check) => check.status !== "error");
|
|
2716
3082
|
}
|
|
3083
|
+
function formatCaptureResult(result) {
|
|
3084
|
+
const pausedBy = result.pausedBy ?? null;
|
|
3085
|
+
const lines = [
|
|
3086
|
+
`capture: ${captureSummary(result.capture, { ...CAPTURE_SWITCHES_UNCHECKED, pausedBy })}`,
|
|
3087
|
+
...(result.capture === "on" && pausedBy !== null ? [captureResumeHint(pausedBy)] : []),
|
|
3088
|
+
...(result.changed
|
|
3089
|
+
? [
|
|
3090
|
+
`updated: ${result.configFiles.join(", ")}`,
|
|
3091
|
+
result.capture === "on"
|
|
3092
|
+
? "Restart your agents, then send one message and run lore doctor to confirm recording."
|
|
3093
|
+
: "Restart your agents. lore remember and the lore skill still save what you choose.",
|
|
3094
|
+
]
|
|
3095
|
+
: []),
|
|
3096
|
+
];
|
|
3097
|
+
return `${lines.join("\n")}\n`;
|
|
3098
|
+
}
|
|
3099
|
+
/** Switch retrieval-only mode and rewrite the agent hooks to match, without re-authorizing. */
|
|
3100
|
+
export async function setCaptureMode(capture, paths = getLorePaths()) {
|
|
3101
|
+
const config = await readConnectorConfig(paths);
|
|
3102
|
+
if (config === null)
|
|
3103
|
+
throw new Error("Lore is not connected. Run lore connect.");
|
|
3104
|
+
const current = captureEnabled(config) ? "on" : "off";
|
|
3105
|
+
if (current === capture)
|
|
3106
|
+
return { capture, changed: false, configFiles: [] };
|
|
3107
|
+
await withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
|
|
3108
|
+
const { capture: _previous, ...rest } = config;
|
|
3109
|
+
await atomicWriteJson(paths.config, { ...rest, ...(capture === "off" ? { capture } : {}) }, 65536);
|
|
3110
|
+
});
|
|
3111
|
+
const refreshed = await refreshAgentEntries(paths);
|
|
3112
|
+
const devinFiles = await refreshDevinHooks(paths.home, capture === "on");
|
|
3113
|
+
return { capture, changed: true, configFiles: [...refreshed?.configFiles ?? [], ...devinFiles] };
|
|
3114
|
+
}
|
|
3115
|
+
async function captureCommand(args) {
|
|
3116
|
+
let mode;
|
|
3117
|
+
let json = false;
|
|
3118
|
+
for (const argument of args) {
|
|
3119
|
+
if (argument === "--help" || argument === "-h") {
|
|
3120
|
+
process.stdout.write(CAPTURE_HELP);
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
if (argument === "--json") {
|
|
3124
|
+
json = true;
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
if ((argument === "status" || argument === "on" || argument === "off") && mode === undefined) {
|
|
3128
|
+
mode = argument;
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
throw new Error("Usage: lore capture [status|on|off] [--json]");
|
|
3132
|
+
}
|
|
3133
|
+
const paths = getLorePaths();
|
|
3134
|
+
if (mode === undefined || mode === "status") {
|
|
3135
|
+
const config = await readConnectorConfig(paths);
|
|
3136
|
+
if (config === null)
|
|
3137
|
+
throw new Error("Lore is not connected. Run lore connect.");
|
|
3138
|
+
const capture = captureEnabled(config) ? "on" : "off";
|
|
3139
|
+
// Same reading as lore status and lore doctor: the folder's connection and its dashboard switches.
|
|
3140
|
+
const switches = capture === "on" ? await captureSwitches(await diagnosticConfig(paths, process.cwd()), paths.home, process.cwd()) : CAPTURE_SWITCHES_UNCHECKED;
|
|
3141
|
+
const result = { capture, pausedBy: switches.pausedBy, changed: false, configFiles: [] };
|
|
3142
|
+
writeResult(result, json, formatCaptureResult(result));
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
const result = await setCaptureMode(mode, paths);
|
|
3146
|
+
writeResult(result, json, formatCaptureResult(result));
|
|
3147
|
+
}
|
|
3148
|
+
/** MCP-only agents from `lore connect --agent generic`: their skills, and that nothing records them. */
|
|
3149
|
+
export async function genericAgentChecks(paths) {
|
|
3150
|
+
const agents = await readGenericAgents(paths.loreDirectory);
|
|
3151
|
+
if (agents.length === 0)
|
|
3152
|
+
return [];
|
|
3153
|
+
const skills = (await Promise.all(["lore", "recap"].map(async (name) => {
|
|
3154
|
+
const file = resolve(paths.home, ".agents", "skills", name, "SKILL.md");
|
|
3155
|
+
return await access(file).then(() => null, () => file);
|
|
3156
|
+
}))).filter((file) => file !== null);
|
|
3157
|
+
return agents.flatMap((agent) => [
|
|
3158
|
+
{
|
|
3159
|
+
name: `${agent.id}-mcp`,
|
|
3160
|
+
status: "ok",
|
|
3161
|
+
detail: `MCP only, ${agent.writeTools ? "read-write (saves are proposals credited to " + agent.id + ")" : "read-only (saves go through the lore CLI)"}. Capture off: no hooks, so this agent's turns are never recorded. Lore cannot inspect the agent's own MCP settings; ask it "Lore, has anything like this come up before?" to confirm.`,
|
|
3162
|
+
},
|
|
3163
|
+
{
|
|
3164
|
+
name: `${agent.id}-skills`,
|
|
3165
|
+
status: skills.length === 0 ? "ok" : "warning",
|
|
3166
|
+
detail: skills.length === 0 ? resolve(paths.home, ".agents", "skills") : `missing ${skills.join(", ")}; run lore connect --agent generic --agent-id ${agent.id} again`,
|
|
3167
|
+
},
|
|
3168
|
+
]);
|
|
3169
|
+
}
|
|
2717
3170
|
async function doctorCommand(args) {
|
|
2718
3171
|
const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
|
|
2719
3172
|
if (parsed === null) {
|
|
@@ -2723,7 +3176,15 @@ async function doctorCommand(args) {
|
|
|
2723
3176
|
const config = await diagnosticConfig(paths, process.cwd());
|
|
2724
3177
|
const prunedDeadLetters = await pruneUnrecoverableDeadLetters(config, paths);
|
|
2725
3178
|
const status = await statusData(paths, process.cwd());
|
|
2726
|
-
const checks = [
|
|
3179
|
+
const checks = [
|
|
3180
|
+
{ name: "capture-folder", status: status.folderAccess.allowed ? "ok" : "warning", detail: status.folderAccess.allowed ? status.folderAccess.reason : `${status.capture === "off" ? "Lore" : "Recording"} is off here: ${status.folderAccess.reason}. Run lore scope check <folder> to verify a selected folder.` },
|
|
3181
|
+
captureModeCheck(status.capture, status.captureSwitches),
|
|
3182
|
+
];
|
|
3183
|
+
const genericChecks = await genericAgentChecks(paths);
|
|
3184
|
+
// With only MCP agents connected there are no hooks, so nothing records turns.
|
|
3185
|
+
if (genericChecks.length > 0 && !status.agents.some((agent) => agent.configured)) {
|
|
3186
|
+
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
|
+
}
|
|
2727
3188
|
checks.push({
|
|
2728
3189
|
name: "platform",
|
|
2729
3190
|
status: platform() === "darwin" || platform() === "linux" || isWindows() ? "ok" : "error",
|
|
@@ -2805,6 +3266,7 @@ async function doctorCommand(args) {
|
|
|
2805
3266
|
});
|
|
2806
3267
|
}
|
|
2807
3268
|
}
|
|
3269
|
+
checks.push(...genericChecks);
|
|
2808
3270
|
for (const surface of status.externalSurfaces) {
|
|
2809
3271
|
checks.push({
|
|
2810
3272
|
name: `${surface.surface}-capability`,
|
|
@@ -2892,6 +3354,9 @@ async function printCommandHelp(path) {
|
|
|
2892
3354
|
case "doctor":
|
|
2893
3355
|
process.stdout.write(DOCTOR_HELP);
|
|
2894
3356
|
return;
|
|
3357
|
+
case "capture":
|
|
3358
|
+
process.stdout.write(CAPTURE_HELP);
|
|
3359
|
+
return;
|
|
2895
3360
|
case "embeddings":
|
|
2896
3361
|
await runEmbeddingsCommand(args, null);
|
|
2897
3362
|
return;
|
|
@@ -2907,9 +3372,15 @@ async function printCommandHelp(path) {
|
|
|
2907
3372
|
case "ask":
|
|
2908
3373
|
await runAskCommand(args, null);
|
|
2909
3374
|
return;
|
|
3375
|
+
case "chat":
|
|
3376
|
+
await runChatCommand(args, null);
|
|
3377
|
+
return;
|
|
2910
3378
|
case "learnings":
|
|
2911
3379
|
await runLearningsCommand(args, null);
|
|
2912
3380
|
return;
|
|
3381
|
+
case "review":
|
|
3382
|
+
await runReviewCommand(args, null);
|
|
3383
|
+
return;
|
|
2913
3384
|
case "remember":
|
|
2914
3385
|
await runRememberCommand(args, null);
|
|
2915
3386
|
return;
|
|
@@ -2994,7 +3465,7 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
2994
3465
|
const helpIndex = args.findIndex((value, index) => index > 0 && (separator < 0 || index < separator) && (value === "--help" || value === "-h"));
|
|
2995
3466
|
if (helpIndex > 0) {
|
|
2996
3467
|
const subcommand = args[1];
|
|
2997
|
-
const grouped = ["connect", "config", "embeddings", "guard", "recovery", "github", "devin", "host", "self-host", "scope"].includes(command ?? "");
|
|
3468
|
+
const grouped = ["connect", "config", "capture", "embeddings", "guard", "recovery", "github", "devin", "host", "self-host", "scope", "review"].includes(command ?? "");
|
|
2998
3469
|
await printCommandHelp([command, ...(grouped && subcommand && !subcommand.startsWith("-") ? [subcommand] : [])]);
|
|
2999
3470
|
return;
|
|
3000
3471
|
}
|
|
@@ -3007,7 +3478,7 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3007
3478
|
return;
|
|
3008
3479
|
}
|
|
3009
3480
|
const commandArgs = args.slice(1);
|
|
3010
|
-
if (["ask", "learnings", "remember", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
|
|
3481
|
+
if (["ask", "chat", "learnings", "review", "remember", "share", "resume", "continue", "approach", "skill"].includes(command) && !(command === "skill" && commandArgs[0] === "discover")) {
|
|
3011
3482
|
const decision = await checkDirectory(process.cwd(), getLorePaths(["approach", "skill"].includes(command) ? process.env.LORE_HOME : undefined).home);
|
|
3012
3483
|
if (!decision.allowed)
|
|
3013
3484
|
throw new Error(`Lore is off in this folder. ${decision.reason}`);
|
|
@@ -3021,6 +3492,9 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3021
3492
|
case "config":
|
|
3022
3493
|
await runConfigCommand(commandArgs, getLorePaths().config);
|
|
3023
3494
|
return;
|
|
3495
|
+
case "capture":
|
|
3496
|
+
await captureCommand(commandArgs);
|
|
3497
|
+
return;
|
|
3024
3498
|
case "scope": {
|
|
3025
3499
|
if (commandArgs[0] !== "check" || commandArgs.length > 2)
|
|
3026
3500
|
throw new Error("Usage: lore scope check [folder]");
|
|
@@ -3047,9 +3521,15 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3047
3521
|
case "ask":
|
|
3048
3522
|
await runAskCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3049
3523
|
return;
|
|
3524
|
+
case "chat":
|
|
3525
|
+
await runChatCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3526
|
+
return;
|
|
3050
3527
|
case "learnings":
|
|
3051
3528
|
await runLearningsCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3052
3529
|
return;
|
|
3530
|
+
case "review":
|
|
3531
|
+
await runReviewCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3532
|
+
return;
|
|
3053
3533
|
case "remember":
|
|
3054
3534
|
await runRememberCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3055
3535
|
return;
|
|
@@ -3164,6 +3644,10 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3164
3644
|
case "cursor-mcp":
|
|
3165
3645
|
await (await import("./cursor-mcp.js")).runCursorMcpServer();
|
|
3166
3646
|
return;
|
|
3647
|
+
case "mcp":
|
|
3648
|
+
// The full Lore MCP server, registered by connect for hook-based agents.
|
|
3649
|
+
await (await import("@lore-co/mcp")).runStdioServer();
|
|
3650
|
+
return;
|
|
3167
3651
|
case "internal-health-write": {
|
|
3168
3652
|
const encoded = commandArgs[0] ?? (await readInvocationHealthWriteInput());
|
|
3169
3653
|
await runInvocationHealthWrite(encoded);
|