@lore-co/cli 0.2.23 → 0.2.25
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 +57 -9
- 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 +59 -10
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +419 -97
- package/dist/cli.js.map +1 -1
- package/dist/command-help.d.ts +3 -1
- package/dist/command-help.d.ts.map +1 -1
- package/dist/command-help.js +5 -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/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 +21 -4
- package/dist/mcp-config.d.ts.map +1 -1
- package/dist/mcp-config.js +66 -6
- package/dist/mcp-config.js.map +1 -1
- 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/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 +7 -4
- package/dist/update.js.map +1 -1
- package/package.json +6 -5
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,11 +8,11 @@ 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";
|
|
15
|
-
import { LORE_MCP_SERVER_NAME, MCP_HOST_AGENTS, inspectCodexMcp, isLoreMcpServer, loreMcpServerEntry, mergeLoreCodexMcp, mergeLoreMcpServers, removeLoreCodexMcp, removeLoreMcpServers, } from "./mcp-config.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";
|
|
16
16
|
import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
17
17
|
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
18
18
|
import { spawn, isWindows, windowsCommand, commandText, findExecutable, openBrowser } from "./platform.js";
|
|
@@ -25,11 +25,14 @@ import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, COPILOT_
|
|
|
25
25
|
import { runHook, } from "./runtime.js";
|
|
26
26
|
import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
|
|
27
27
|
import { runAskCommand } from "./ask.js";
|
|
28
|
+
import { runChatCommand } from "./chat.js";
|
|
28
29
|
import { runLearningsCommand } from "./learnings.js";
|
|
29
30
|
import { runReviewCommand } from "./review.js";
|
|
30
31
|
import { runEmbeddingsCommand } from "./embeddings.js";
|
|
31
32
|
import { runRememberCommand } from "./remember.js";
|
|
32
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";
|
|
33
36
|
import { runApproachCommand } from "./approach.js";
|
|
34
37
|
import { runSkillCommand } from "./skill-command.js";
|
|
35
38
|
import { runGuardCommand } from "./guard.js";
|
|
@@ -60,6 +63,20 @@ const HOOK_EVENTS = [
|
|
|
60
63
|
"Stop",
|
|
61
64
|
"SessionEnd",
|
|
62
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
|
+
}
|
|
63
80
|
/** Tools the Context Guard PreToolUse hook cares about (regex matcher). */
|
|
64
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";
|
|
65
82
|
const CURSOR_HOOK_EVENTS = [
|
|
@@ -112,12 +129,16 @@ Options:
|
|
|
112
129
|
--repository <repo> Verify the selected folder matches this GitHub repository
|
|
113
130
|
--folder <path> Record only in this folder and subfolders (repeatable)
|
|
114
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
|
|
115
134
|
--url <url> Lore API base URL (default: https://api.uselore.co)
|
|
116
135
|
--dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
|
|
117
136
|
--login Approve access securely in the Lore dashboard
|
|
118
137
|
--no-open Print the approval URL without opening a browser
|
|
119
138
|
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
120
|
-
--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)
|
|
121
142
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
122
143
|
--fallback <agent> claude, codex, opencode, devin, cursor, copilot-cli, polytoken
|
|
123
144
|
--no-recovery Disable automatic handoff setup (otherwise enabled with two installed clients)
|
|
@@ -125,11 +146,14 @@ Options:
|
|
|
125
146
|
--help Show this command's help
|
|
126
147
|
|
|
127
148
|
New connections stay paused until you choose a project, a folder, or --all-projects.
|
|
128
|
-
Reconnecting without a capture option keeps your saved folder rules.
|
|
129
|
-
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.
|
|
130
153
|
|
|
131
154
|
Examples:
|
|
132
155
|
lore connect --login --agent claude --folder .
|
|
156
|
+
lore connect --login --agent claude --folder . --no-capture
|
|
133
157
|
lore connect --login --agent cowork
|
|
134
158
|
lore connect --login --agent devin --folder .
|
|
135
159
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
|
|
@@ -156,6 +180,24 @@ Examples:
|
|
|
156
180
|
lore doctor
|
|
157
181
|
lore doctor --json
|
|
158
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
|
+
`;
|
|
159
201
|
const DISCONNECT_HELP = `lore disconnect
|
|
160
202
|
Remove Lore-owned hooks/plugin, credentials, runtime, and transient state.
|
|
161
203
|
Durable captures, dead letters, verified snapshots, unrelated agent settings,
|
|
@@ -224,6 +266,7 @@ function isConnectAgent(value) {
|
|
|
224
266
|
return (isConfiguredAgent(value) ||
|
|
225
267
|
value === "copilot" ||
|
|
226
268
|
isCredentialOnlyAgent(value) ||
|
|
269
|
+
value === "generic" ||
|
|
227
270
|
value === "t3code");
|
|
228
271
|
}
|
|
229
272
|
function isCredentialOnlyAgent(value) {
|
|
@@ -293,24 +336,29 @@ function eventHandler(agent, event, paths) {
|
|
|
293
336
|
: {}),
|
|
294
337
|
};
|
|
295
338
|
}
|
|
296
|
-
export function mergeLoreHooks(input, agent, paths) {
|
|
339
|
+
export function mergeLoreHooks(input, agent, paths, capture = true) {
|
|
297
340
|
const result = cloneObject(input);
|
|
298
341
|
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
299
342
|
throw new Error("Agent configuration field \"hooks\" must be a JSON object");
|
|
300
343
|
}
|
|
301
344
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
345
|
+
const installed = installedEvents(HOOK_EVENTS, capture);
|
|
302
346
|
for (const event of HOOK_EVENTS) {
|
|
303
347
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
304
348
|
throw new Error(`Agent hook event "${event}" must be a JSON array`);
|
|
305
349
|
}
|
|
306
|
-
|
|
350
|
+
const groups = [
|
|
307
351
|
...stripLoreFromEvent(hooks[event]),
|
|
308
|
-
{
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
}] : []),
|
|
313
357
|
];
|
|
358
|
+
if (groups.length === 0)
|
|
359
|
+
delete hooks[event];
|
|
360
|
+
else
|
|
361
|
+
hooks[event] = groups;
|
|
314
362
|
}
|
|
315
363
|
result.hooks = hooks;
|
|
316
364
|
return result;
|
|
@@ -404,8 +452,12 @@ function isMcpHostAgent(agent) {
|
|
|
404
452
|
return MCP_HOST_AGENTS.includes(agent);
|
|
405
453
|
}
|
|
406
454
|
/** The full Lore MCP server, run by this CLI so it shares the hooks' credentials and version. */
|
|
407
|
-
export function loreMcpServer(paths) {
|
|
408
|
-
return loreMcpServerEntry(
|
|
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)])];
|
|
409
461
|
}
|
|
410
462
|
/** Where each agent lists MCP servers, located from the hooks file Lore already manages. */
|
|
411
463
|
export function mcpConfigPath(agent, hooksPath) {
|
|
@@ -420,18 +472,29 @@ export function mcpConfigPath(agent, hooksPath) {
|
|
|
420
472
|
return resolve(directory, "config.toml");
|
|
421
473
|
case "cursor":
|
|
422
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");
|
|
423
481
|
}
|
|
424
482
|
}
|
|
425
483
|
export function mergeLoreMcp(input, agent, paths) {
|
|
426
|
-
|
|
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);
|
|
427
489
|
}
|
|
428
490
|
export function removeLoreMcp(input, agent) {
|
|
429
|
-
|
|
491
|
+
const config = objectConfiguration(input, agent);
|
|
492
|
+
return agent === "copilot-vscode" ? removeLoreVsCodeMcp(config) : removeLoreMcpServers(config);
|
|
430
493
|
}
|
|
431
494
|
function stripLoreFromCursorEvent(value) {
|
|
432
495
|
return Array.isArray(value) ? value.filter((hook) => !isLoreHook(hook)) : [];
|
|
433
496
|
}
|
|
434
|
-
export function mergeLoreCursorHooks(input, paths) {
|
|
497
|
+
export function mergeLoreCursorHooks(input, paths, capture = true) {
|
|
435
498
|
const result = cloneObject(input);
|
|
436
499
|
if (result.version !== undefined && result.version !== 1) {
|
|
437
500
|
throw new Error('Cursor configuration field "version" must be 1');
|
|
@@ -440,14 +503,19 @@ export function mergeLoreCursorHooks(input, paths) {
|
|
|
440
503
|
throw new Error('Cursor configuration field "hooks" must be a JSON object');
|
|
441
504
|
}
|
|
442
505
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
506
|
+
const installed = installedEvents(CURSOR_HOOK_EVENTS, capture);
|
|
443
507
|
for (const event of CURSOR_HOOK_EVENTS) {
|
|
444
508
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
445
509
|
throw new Error(`Cursor hook event "${event}" must be a JSON array`);
|
|
446
510
|
}
|
|
447
|
-
|
|
511
|
+
const entries = [
|
|
448
512
|
...stripLoreFromCursorEvent(hooks[event]),
|
|
449
|
-
cursorEventHandler(event, paths),
|
|
513
|
+
...(installed.includes(event) ? [cursorEventHandler(event, paths)] : []),
|
|
450
514
|
];
|
|
515
|
+
if (entries.length === 0)
|
|
516
|
+
delete hooks[event];
|
|
517
|
+
else
|
|
518
|
+
hooks[event] = entries;
|
|
451
519
|
}
|
|
452
520
|
result.version = 1;
|
|
453
521
|
result.hooks = hooks;
|
|
@@ -522,7 +590,7 @@ function stripLoreFromCopilotEvent(value) {
|
|
|
522
590
|
? value.filter((hook) => !isLoreCopilotHook(hook))
|
|
523
591
|
: [];
|
|
524
592
|
}
|
|
525
|
-
export function mergeLoreCopilotCliHooks(input, paths) {
|
|
593
|
+
export function mergeLoreCopilotCliHooks(input, paths, capture = true) {
|
|
526
594
|
const result = cloneObject(input);
|
|
527
595
|
if (result.version !== undefined && result.version !== 1) {
|
|
528
596
|
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
@@ -531,14 +599,19 @@ export function mergeLoreCopilotCliHooks(input, paths) {
|
|
|
531
599
|
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
532
600
|
}
|
|
533
601
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
602
|
+
const installed = installedEvents(COPILOT_CLI_HOOK_EVENTS, capture);
|
|
534
603
|
for (const event of COPILOT_CLI_HOOK_EVENTS) {
|
|
535
604
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
536
605
|
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
537
606
|
}
|
|
538
|
-
|
|
607
|
+
const entries = [
|
|
539
608
|
...stripLoreFromCopilotEvent(hooks[event]),
|
|
540
|
-
copilotCliEventHandler(event, paths),
|
|
609
|
+
...(installed.includes(event) ? [copilotCliEventHandler(event, paths)] : []),
|
|
541
610
|
];
|
|
611
|
+
if (entries.length === 0)
|
|
612
|
+
delete hooks[event];
|
|
613
|
+
else
|
|
614
|
+
hooks[event] = entries;
|
|
542
615
|
}
|
|
543
616
|
result.version = 1;
|
|
544
617
|
result.hooks = hooks;
|
|
@@ -595,7 +668,7 @@ function copilotVscodeEventHandler(event, paths) {
|
|
|
595
668
|
: 25,
|
|
596
669
|
};
|
|
597
670
|
}
|
|
598
|
-
export function mergeLoreCopilotVscodeHooks(input, paths) {
|
|
671
|
+
export function mergeLoreCopilotVscodeHooks(input, paths, capture = true) {
|
|
599
672
|
const result = cloneObject(input);
|
|
600
673
|
if (result.version !== undefined && result.version !== 1) {
|
|
601
674
|
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
@@ -604,14 +677,19 @@ export function mergeLoreCopilotVscodeHooks(input, paths) {
|
|
|
604
677
|
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
605
678
|
}
|
|
606
679
|
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
680
|
+
const installed = installedEvents(HOOK_EVENTS, capture);
|
|
607
681
|
for (const event of HOOK_EVENTS) {
|
|
608
682
|
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
609
683
|
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
610
684
|
}
|
|
611
|
-
|
|
685
|
+
const entries = [
|
|
612
686
|
...stripLoreFromCursorEvent(hooks[event]),
|
|
613
|
-
copilotVscodeEventHandler(event, paths),
|
|
687
|
+
...(installed.includes(event) ? [copilotVscodeEventHandler(event, paths)] : []),
|
|
614
688
|
];
|
|
689
|
+
if (entries.length === 0)
|
|
690
|
+
delete hooks[event];
|
|
691
|
+
else
|
|
692
|
+
hooks[event] = entries;
|
|
615
693
|
}
|
|
616
694
|
result.version = 1;
|
|
617
695
|
result.hooks = hooks;
|
|
@@ -671,14 +749,14 @@ function isLorePolytokenHook(value) {
|
|
|
671
749
|
(/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
|
|
672
750
|
command.includes("/.lore/bin/lore-hook.mjs")));
|
|
673
751
|
}
|
|
674
|
-
export function mergeLorePolytokenHooks(input, paths) {
|
|
752
|
+
export function mergeLorePolytokenHooks(input, paths, capture = true) {
|
|
675
753
|
if (!Array.isArray(input)) {
|
|
676
754
|
throw new Error("Polytoken hooks must contain a JSON array");
|
|
677
755
|
}
|
|
678
756
|
const retained = input.filter((hook) => !isLorePolytokenHook(hook));
|
|
679
757
|
return [
|
|
680
758
|
...structuredClone(retained),
|
|
681
|
-
...POLYTOKEN_HOOK_EVENTS.map((event) => polytokenEventHandler(event, paths)),
|
|
759
|
+
...installedEvents(POLYTOKEN_HOOK_EVENTS, capture).map((event) => polytokenEventHandler(event, paths)),
|
|
682
760
|
];
|
|
683
761
|
}
|
|
684
762
|
export function removeLorePolytokenHooks(input) {
|
|
@@ -727,6 +805,9 @@ export function countLoreOpenCodePlugins(input) {
|
|
|
727
805
|
? input.plugin.filter(isLoreOpenCodePlugin).length
|
|
728
806
|
: 0;
|
|
729
807
|
}
|
|
808
|
+
function countLoreOpenCodeMcp(input) {
|
|
809
|
+
return isObject(input.mcp) && isLoreOpenCodeMcp(input.mcp[LORE_MCP_SERVER_NAME]) ? 1 : 0;
|
|
810
|
+
}
|
|
730
811
|
function installationUnitId(event, index, total) {
|
|
731
812
|
return total === 1 ? event : `installation-${index + 1}/${event}`;
|
|
732
813
|
}
|
|
@@ -742,13 +823,13 @@ function installationUnitState(ownedCount, exactCount) {
|
|
|
742
823
|
function exactConfiguration(left, right) {
|
|
743
824
|
return canonicalJson(left) === canonicalJson(right);
|
|
744
825
|
}
|
|
745
|
-
function commandHookInstallationUnits(input, agent, paths, index, total) {
|
|
826
|
+
function commandHookInstallationUnits(input, agent, paths, index, total, capture) {
|
|
746
827
|
const configuration = objectConfiguration(input, agent);
|
|
747
828
|
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
748
829
|
throw new Error(`${agent} configuration field "hooks" must be an object`);
|
|
749
830
|
}
|
|
750
831
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
751
|
-
return HOOK_EVENTS.map((event) => {
|
|
832
|
+
return installedEvents(HOOK_EVENTS, capture).map((event) => {
|
|
752
833
|
const groups = hooks[event];
|
|
753
834
|
if (groups !== undefined && !Array.isArray(groups)) {
|
|
754
835
|
throw new Error(`${agent} hook event "${event}" must be an array`);
|
|
@@ -771,7 +852,7 @@ function commandHookInstallationUnits(input, agent, paths, index, total) {
|
|
|
771
852
|
};
|
|
772
853
|
});
|
|
773
854
|
}
|
|
774
|
-
function cursorInstallationUnits(input, paths, index, total) {
|
|
855
|
+
function cursorInstallationUnits(input, paths, index, total, capture) {
|
|
775
856
|
const configuration = objectConfiguration(input, "cursor");
|
|
776
857
|
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
777
858
|
throw new Error('Cursor configuration field "version" must be 1');
|
|
@@ -780,7 +861,7 @@ function cursorInstallationUnits(input, paths, index, total) {
|
|
|
780
861
|
throw new Error('Cursor configuration field "hooks" must be an object');
|
|
781
862
|
}
|
|
782
863
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
783
|
-
return CURSOR_HOOK_EVENTS.map((event) => {
|
|
864
|
+
return installedEvents(CURSOR_HOOK_EVENTS, capture).map((event) => {
|
|
784
865
|
const values = hooks[event];
|
|
785
866
|
if (values !== undefined && !Array.isArray(values)) {
|
|
786
867
|
throw new Error(`Cursor hook event "${event}" must be an array`);
|
|
@@ -795,7 +876,7 @@ function cursorInstallationUnits(input, paths, index, total) {
|
|
|
795
876
|
};
|
|
796
877
|
});
|
|
797
878
|
}
|
|
798
|
-
function copilotCliInstallationUnits(input, paths, index, total) {
|
|
879
|
+
function copilotCliInstallationUnits(input, paths, index, total, capture) {
|
|
799
880
|
const configuration = objectConfiguration(input, "copilot-cli");
|
|
800
881
|
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
801
882
|
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
@@ -804,7 +885,7 @@ function copilotCliInstallationUnits(input, paths, index, total) {
|
|
|
804
885
|
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
805
886
|
}
|
|
806
887
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
807
|
-
return COPILOT_CLI_HOOK_EVENTS.map((event) => {
|
|
888
|
+
return installedEvents(COPILOT_CLI_HOOK_EVENTS, capture).map((event) => {
|
|
808
889
|
const values = hooks[event];
|
|
809
890
|
if (values !== undefined && !Array.isArray(values)) {
|
|
810
891
|
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
@@ -819,7 +900,7 @@ function copilotCliInstallationUnits(input, paths, index, total) {
|
|
|
819
900
|
};
|
|
820
901
|
});
|
|
821
902
|
}
|
|
822
|
-
function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
903
|
+
function copilotVscodeInstallationUnits(input, paths, index, total, capture) {
|
|
823
904
|
const configuration = objectConfiguration(input, "copilot-vscode");
|
|
824
905
|
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
825
906
|
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
@@ -828,7 +909,7 @@ function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
|
828
909
|
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
829
910
|
}
|
|
830
911
|
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
831
|
-
return HOOK_EVENTS.map((event) => {
|
|
912
|
+
return installedEvents(HOOK_EVENTS, capture).map((event) => {
|
|
832
913
|
const values = hooks[event];
|
|
833
914
|
if (values !== undefined && !Array.isArray(values)) {
|
|
834
915
|
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
@@ -843,11 +924,11 @@ function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
|
843
924
|
};
|
|
844
925
|
});
|
|
845
926
|
}
|
|
846
|
-
function polytokenInstallationUnits(input, paths, index, total) {
|
|
927
|
+
function polytokenInstallationUnits(input, paths, index, total, capture) {
|
|
847
928
|
if (!Array.isArray(input)) {
|
|
848
929
|
throw new Error("Polytoken hooks must contain a JSON array");
|
|
849
930
|
}
|
|
850
|
-
return POLYTOKEN_HOOK_EVENTS.map((event) => {
|
|
931
|
+
return installedEvents(POLYTOKEN_HOOK_EVENTS, capture).map((event) => {
|
|
851
932
|
const expected = polytokenEventHandler(event, paths);
|
|
852
933
|
const owned = input.filter((candidate) => isLorePolytokenHook(candidate) &&
|
|
853
934
|
(!isObject(candidate) || candidate.event === event));
|
|
@@ -858,7 +939,7 @@ function polytokenInstallationUnits(input, paths, index, total) {
|
|
|
858
939
|
};
|
|
859
940
|
});
|
|
860
941
|
}
|
|
861
|
-
function openCodeInstallationUnits(input, index, total) {
|
|
942
|
+
function openCodeInstallationUnits(input, paths, index, total) {
|
|
862
943
|
const configuration = objectConfiguration(input, "opencode");
|
|
863
944
|
if (configuration.plugin !== undefined &&
|
|
864
945
|
!Array.isArray(configuration.plugin)) {
|
|
@@ -869,28 +950,37 @@ function openCodeInstallationUnits(input, index, total) {
|
|
|
869
950
|
: [];
|
|
870
951
|
const owned = plugins.filter(isLoreOpenCodePlugin);
|
|
871
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));
|
|
872
958
|
return [
|
|
873
959
|
{
|
|
874
960
|
id: installationUnitId("plugin", index, total),
|
|
875
961
|
state: installationUnitState(owned.length, exactCount),
|
|
876
962
|
},
|
|
963
|
+
{
|
|
964
|
+
id: installationUnitId("mcp-tools", index, total),
|
|
965
|
+
state: installationUnitState(ownedServer, Number(exactConfiguration(server, openCodeMcpEntry(loreMcpServer(paths, "opencode"))))),
|
|
966
|
+
},
|
|
877
967
|
];
|
|
878
968
|
}
|
|
879
|
-
function inspectInstallationUnits(input, agent, paths, index, total) {
|
|
969
|
+
function inspectInstallationUnits(input, agent, paths, index, total, capture) {
|
|
880
970
|
switch (agent) {
|
|
881
971
|
case "claude":
|
|
882
972
|
case "codex":
|
|
883
|
-
return commandHookInstallationUnits(input, agent, paths, index, total);
|
|
973
|
+
return commandHookInstallationUnits(input, agent, paths, index, total, capture);
|
|
884
974
|
case "copilot-cli":
|
|
885
|
-
return copilotCliInstallationUnits(input, paths, index, total);
|
|
975
|
+
return copilotCliInstallationUnits(input, paths, index, total, capture);
|
|
886
976
|
case "copilot-vscode":
|
|
887
|
-
return copilotVscodeInstallationUnits(input, paths, index, total);
|
|
977
|
+
return copilotVscodeInstallationUnits(input, paths, index, total, capture);
|
|
888
978
|
case "cursor":
|
|
889
|
-
return cursorInstallationUnits(input, paths, index, total);
|
|
979
|
+
return cursorInstallationUnits(input, paths, index, total, capture);
|
|
890
980
|
case "polytoken":
|
|
891
|
-
return polytokenInstallationUnits(input, paths, index, total);
|
|
981
|
+
return polytokenInstallationUnits(input, paths, index, total, capture);
|
|
892
982
|
case "opencode":
|
|
893
|
-
return openCodeInstallationUnits(input, index, total);
|
|
983
|
+
return openCodeInstallationUnits(input, paths, index, total);
|
|
894
984
|
}
|
|
895
985
|
}
|
|
896
986
|
async function readJsonDocument(path) {
|
|
@@ -1042,6 +1132,7 @@ function parseConnectorConfig(value) {
|
|
|
1042
1132
|
Object.keys(agentConfigPaths).length === 0
|
|
1043
1133
|
? {}
|
|
1044
1134
|
: { agentConfigPaths }),
|
|
1135
|
+
...(value.capture === "off" ? { capture: "off" } : {}),
|
|
1045
1136
|
connectedAt: value.connectedAt,
|
|
1046
1137
|
timeoutMs,
|
|
1047
1138
|
};
|
|
@@ -1209,80 +1300,83 @@ const AGENT_TARGETS = {
|
|
|
1209
1300
|
claude: {
|
|
1210
1301
|
integration: "hooks",
|
|
1211
1302
|
// Native hooks plus the Lore MCP server entry.
|
|
1212
|
-
expected: HOOK_EVENTS.length + 1,
|
|
1303
|
+
expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
|
|
1213
1304
|
runtimeRequired: true,
|
|
1214
1305
|
emptyValue: () => ({}),
|
|
1215
1306
|
configPath: (paths) => paths.claudeSettings,
|
|
1216
1307
|
detect: async () => commandExists("claude"),
|
|
1217
1308
|
executable: async () => commandExists("claude"),
|
|
1218
|
-
merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths),
|
|
1309
|
+
merge: (input, paths, capture) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths, capture),
|
|
1219
1310
|
remove: (input) => removeLoreHooks(objectConfiguration(input, "claude")),
|
|
1220
1311
|
count: (input) => countLoreHooks(objectConfiguration(input, "claude")),
|
|
1221
1312
|
},
|
|
1222
1313
|
codex: {
|
|
1223
1314
|
integration: "hooks",
|
|
1224
|
-
expected: HOOK_EVENTS.length + 1,
|
|
1315
|
+
expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
|
|
1225
1316
|
runtimeRequired: true,
|
|
1226
1317
|
emptyValue: () => ({}),
|
|
1227
1318
|
configPath: (paths) => paths.codexHooks,
|
|
1228
1319
|
detect: async () => commandExists("codex"),
|
|
1229
1320
|
executable: async () => commandExists("codex"),
|
|
1230
|
-
merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths),
|
|
1321
|
+
merge: (input, paths, capture) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths, capture),
|
|
1231
1322
|
remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
|
|
1232
1323
|
count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
|
|
1233
1324
|
},
|
|
1234
1325
|
"copilot-cli": {
|
|
1235
1326
|
integration: "hooks",
|
|
1236
|
-
|
|
1327
|
+
// Native hooks plus the Lore MCP server entry.
|
|
1328
|
+
expected: (capture) => installedEvents(COPILOT_CLI_HOOK_EVENTS, capture).length + 1,
|
|
1237
1329
|
runtimeRequired: true,
|
|
1238
1330
|
emptyValue: () => ({}),
|
|
1239
1331
|
configPath: (paths) => paths.copilotCliHooks,
|
|
1240
1332
|
detect: async (paths) => (await commandExists("copilot")) || pathExists(paths.copilotCliHooks),
|
|
1241
1333
|
executable: async () => commandExists("copilot"),
|
|
1242
|
-
merge: (input, paths) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths),
|
|
1334
|
+
merge: (input, paths, capture) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths, capture),
|
|
1243
1335
|
remove: (input) => removeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1244
1336
|
count: (input) => countLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1245
1337
|
},
|
|
1246
1338
|
"copilot-vscode": {
|
|
1247
1339
|
integration: "hooks",
|
|
1248
|
-
|
|
1340
|
+
// Repository hooks plus the Lore MCP server entry in .vscode/mcp.json.
|
|
1341
|
+
expected: (capture) => installedEvents(HOOK_EVENTS, capture).length + 1,
|
|
1249
1342
|
runtimeRequired: true,
|
|
1250
1343
|
emptyValue: () => ({}),
|
|
1251
1344
|
configPath: (paths) => paths.copilotVscodeHooks,
|
|
1252
1345
|
detect: async (paths) => pathExists(paths.copilotVscodeHooks),
|
|
1253
1346
|
executable: async () => commandExists("code"),
|
|
1254
|
-
merge: (input, paths) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths),
|
|
1347
|
+
merge: (input, paths, capture) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths, capture),
|
|
1255
1348
|
remove: (input) => removeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1256
1349
|
count: (input) => countLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1257
1350
|
},
|
|
1258
1351
|
cursor: {
|
|
1259
1352
|
integration: "hooks",
|
|
1260
1353
|
// Hooks, the capture bridge, and the Lore MCP server entry.
|
|
1261
|
-
expected: CURSOR_HOOK_EVENTS.length + 2,
|
|
1354
|
+
expected: (capture) => installedEvents(CURSOR_HOOK_EVENTS, capture).length + 2,
|
|
1262
1355
|
runtimeRequired: true,
|
|
1263
1356
|
emptyValue: () => ({}),
|
|
1264
1357
|
configPath: (paths) => paths.cursorHooks,
|
|
1265
1358
|
detect: async (paths) => (await cursorAvailable(paths)) || pathExists(paths.cursorHooks),
|
|
1266
1359
|
executable: cursorAvailable,
|
|
1267
|
-
merge: (input, paths) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths),
|
|
1360
|
+
merge: (input, paths, capture) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths, capture),
|
|
1268
1361
|
remove: (input) => removeLoreCursorHooks(objectConfiguration(input, "cursor")),
|
|
1269
1362
|
count: (input) => countLoreCursorHooks(objectConfiguration(input, "cursor")),
|
|
1270
1363
|
},
|
|
1271
1364
|
opencode: {
|
|
1272
1365
|
integration: "plugin",
|
|
1273
|
-
|
|
1366
|
+
// The plugin entry plus the Lore MCP server entry, both in opencode.json.
|
|
1367
|
+
expected: () => 2,
|
|
1274
1368
|
runtimeRequired: false,
|
|
1275
1369
|
emptyValue: () => ({}),
|
|
1276
1370
|
configPath: (paths) => paths.openCodeConfig,
|
|
1277
1371
|
detect: async (paths) => (await commandExists("opencode")) || pathExists(paths.openCodeConfig),
|
|
1278
1372
|
executable: async () => commandExists("opencode"),
|
|
1279
|
-
merge: (input) => mergeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
|
|
1280
|
-
remove: (input) => removeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
|
|
1281
|
-
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")),
|
|
1282
1376
|
},
|
|
1283
1377
|
polytoken: {
|
|
1284
1378
|
integration: "hooks",
|
|
1285
|
-
expected: POLYTOKEN_HOOK_EVENTS.length,
|
|
1379
|
+
expected: (capture) => installedEvents(POLYTOKEN_HOOK_EVENTS, capture).length,
|
|
1286
1380
|
runtimeRequired: true,
|
|
1287
1381
|
emptyValue: () => [],
|
|
1288
1382
|
configPath: (paths) => paths.polytokenHooks,
|
|
@@ -1465,7 +1559,7 @@ async function readT3CodeProviderInstallations(paths) {
|
|
|
1465
1559
|
}
|
|
1466
1560
|
async function expandConnectInstallations(requested, paths) {
|
|
1467
1561
|
const direct = requested.flatMap((agent) => {
|
|
1468
|
-
if (isCredentialOnlyAgent(agent) || agent === "t3code") {
|
|
1562
|
+
if (isCredentialOnlyAgent(agent) || agent === "t3code" || agent === GENERIC_AGENT) {
|
|
1469
1563
|
return [];
|
|
1470
1564
|
}
|
|
1471
1565
|
return [
|
|
@@ -1498,6 +1592,7 @@ async function installRuntime(paths) {
|
|
|
1498
1592
|
rm(paths.runtime, { force: true }),
|
|
1499
1593
|
rm(paths.runtimeRepository, { force: true }),
|
|
1500
1594
|
rm(resolve(dirname(paths.runtime), "repository-address.js"), { force: true }),
|
|
1595
|
+
rm(resolve(dirname(paths.runtime), "capture-policy.js"), { force: true }),
|
|
1501
1596
|
rm(resolve(dirname(paths.runtime), "directory-policy.js"), { force: true }),
|
|
1502
1597
|
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
1503
1598
|
rm(resolve(dirname(paths.runtime), "windows-permissions.js"), { force: true }),
|
|
@@ -1556,6 +1651,8 @@ async function installRuntime(paths) {
|
|
|
1556
1651
|
"skill-guidance.js": await readFile(resolve(sourceDirectory, "skill-guidance.js"), "utf8"),
|
|
1557
1652
|
"repository.js": repository,
|
|
1558
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"),
|
|
1559
1656
|
"directory-policy.js": (await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/local-scope")), "utf8")).replaceAll("./repository-location.js", "./repository.js"),
|
|
1560
1657
|
"reliability-store.js": reliabilityStore,
|
|
1561
1658
|
"windows-permissions.js": await readFile(resolve(sourceDirectory, "windows-permissions.js"), "utf8"),
|
|
@@ -1589,6 +1686,7 @@ async function installRuntime(paths) {
|
|
|
1589
1686
|
atomicWrite(resolve(dirname(paths.runtime), "skill-guidance.js"), runtimeFiles["skill-guidance.js"], 0o600),
|
|
1590
1687
|
atomicWrite(paths.runtimeRepository, repository, 0o600),
|
|
1591
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),
|
|
1592
1690
|
atomicWrite(resolve(dirname(paths.runtime), "directory-policy.js"), runtimeFiles["directory-policy.js"], 0o600),
|
|
1593
1691
|
atomicWrite(paths.runtimeReliabilityStore, reliabilityStore, 0o600),
|
|
1594
1692
|
atomicWrite(resolve(dirname(paths.runtime), "windows-permissions.js"), runtimeFiles["windows-permissions.js"], 0o600),
|
|
@@ -1606,9 +1704,9 @@ async function installRuntime(paths) {
|
|
|
1606
1704
|
function agentConfigPath(agent, paths) {
|
|
1607
1705
|
return targetFor(agent).configPath(paths);
|
|
1608
1706
|
}
|
|
1609
|
-
function mergeAgentConfig(input, agent, paths) {
|
|
1707
|
+
function mergeAgentConfig(input, agent, paths, capture) {
|
|
1610
1708
|
const target = targetFor(agent);
|
|
1611
|
-
return target.merge(input === undefined ? target.emptyValue() : input, paths);
|
|
1709
|
+
return target.merge(input === undefined ? target.emptyValue() : input, paths, capture);
|
|
1612
1710
|
}
|
|
1613
1711
|
function removeAgentConfig(input, agent) {
|
|
1614
1712
|
return targetFor(agent).remove(input);
|
|
@@ -1617,7 +1715,7 @@ function removeAgentConfig(input, agent) {
|
|
|
1617
1715
|
* Read each installation's hooks file and, for MCP hosts, its MCP config, and
|
|
1618
1716
|
* merge the Lore entries this CLI version writes. Nothing touches disk yet.
|
|
1619
1717
|
*/
|
|
1620
|
-
async function prepareAgentEntries(installations, paths) {
|
|
1718
|
+
async function prepareAgentEntries(installations, paths, capture) {
|
|
1621
1719
|
const documents = new Map();
|
|
1622
1720
|
const mergedDocuments = new Map();
|
|
1623
1721
|
const mcpDocuments = new Map();
|
|
@@ -1625,12 +1723,12 @@ async function prepareAgentEntries(installations, paths) {
|
|
|
1625
1723
|
const key = installationKey(installation);
|
|
1626
1724
|
const document = await readJsonDocument(installation.configPath);
|
|
1627
1725
|
documents.set(key, document);
|
|
1628
|
-
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths));
|
|
1726
|
+
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths, capture));
|
|
1629
1727
|
if (isMcpHostAgent(installation.agent)) {
|
|
1630
1728
|
const mcpPath = mcpConfigPath(installation.agent, installation.configPath);
|
|
1631
1729
|
if (installation.agent === "codex") {
|
|
1632
1730
|
const text = await readTextDocument(mcpPath);
|
|
1633
|
-
mcpDocuments.set(mcpPath, { kind: "text", document: text, merged: mergeLoreCodexMcp(text.value, loreMcpServer(paths)) });
|
|
1731
|
+
mcpDocuments.set(mcpPath, { kind: "text", document: text, merged: mergeLoreCodexMcp(text.value, loreMcpServer(paths, "codex")) });
|
|
1634
1732
|
}
|
|
1635
1733
|
else {
|
|
1636
1734
|
const mcp = await readJsonDocument(mcpPath);
|
|
@@ -1691,9 +1789,11 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
|
|
|
1691
1789
|
return null;
|
|
1692
1790
|
const installations = uniqueInstallations(configuredInstallations(config, paths));
|
|
1693
1791
|
return withDirectoryLock(resolve(paths.loreDirectory, "connections.lock"), async () => {
|
|
1694
|
-
const entries = await prepareAgentEntries(installations, paths);
|
|
1792
|
+
const entries = await prepareAgentEntries(installations, paths, captureEnabled(config));
|
|
1695
1793
|
const written = await writeAgentEntries(entries, paths, new Date());
|
|
1696
|
-
const
|
|
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);
|
|
1697
1797
|
return {
|
|
1698
1798
|
// Same order the files are written in, so the two update lines match.
|
|
1699
1799
|
configFiles: [
|
|
@@ -1702,7 +1802,8 @@ export async function refreshAgentEntries(paths = getLorePaths()) {
|
|
|
1702
1802
|
],
|
|
1703
1803
|
...written,
|
|
1704
1804
|
skillFiles: skills.files,
|
|
1705
|
-
|
|
1805
|
+
agentFiles: agent.files,
|
|
1806
|
+
warnings: [...skills.warnings, ...agent.warnings],
|
|
1706
1807
|
};
|
|
1707
1808
|
});
|
|
1708
1809
|
}
|
|
@@ -1753,6 +1854,14 @@ function parseConnectArguments(args) {
|
|
|
1753
1854
|
parsed.recovery = false;
|
|
1754
1855
|
continue;
|
|
1755
1856
|
}
|
|
1857
|
+
if (argument === "--no-capture") {
|
|
1858
|
+
parsed.capture = false;
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (argument === "--capture") {
|
|
1862
|
+
parsed.capture = true;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1756
1865
|
if (argument === "--current-project") {
|
|
1757
1866
|
parsed.currentProject = true;
|
|
1758
1867
|
continue;
|
|
@@ -1765,6 +1874,10 @@ function parseConnectArguments(args) {
|
|
|
1765
1874
|
parsed.allProjects = true;
|
|
1766
1875
|
continue;
|
|
1767
1876
|
}
|
|
1877
|
+
if (argument === "--write") {
|
|
1878
|
+
parsed.writeTools = true;
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1768
1881
|
if (argument !== "--repository" &&
|
|
1769
1882
|
argument !== "--folder" &&
|
|
1770
1883
|
argument !== "--name" &&
|
|
@@ -1772,6 +1885,7 @@ function parseConnectArguments(args) {
|
|
|
1772
1885
|
argument !== "--dashboard-url" &&
|
|
1773
1886
|
argument !== "--token" &&
|
|
1774
1887
|
argument !== "--agent" &&
|
|
1888
|
+
argument !== "--agent-id" &&
|
|
1775
1889
|
argument !== "--fallback" &&
|
|
1776
1890
|
argument !== "--timeout-ms") {
|
|
1777
1891
|
throw new Error(`Unknown connect option: ${argument ?? ""}`);
|
|
@@ -1787,6 +1901,9 @@ function parseConnectArguments(args) {
|
|
|
1787
1901
|
else if (argument === "--name") {
|
|
1788
1902
|
parsed.connectionName = value;
|
|
1789
1903
|
}
|
|
1904
|
+
else if (argument === "--agent-id") {
|
|
1905
|
+
parsed.agentId = parseGenericAgentId(value);
|
|
1906
|
+
}
|
|
1790
1907
|
else if (argument === "--fallback") {
|
|
1791
1908
|
if (!isLaunchable(value))
|
|
1792
1909
|
throw new Error(`--fallback must be ${LAUNCHABLE_AGENTS.join(", ")}`);
|
|
@@ -1812,10 +1929,13 @@ function parseConnectArguments(args) {
|
|
|
1812
1929
|
parsed.agents.push(value);
|
|
1813
1930
|
}
|
|
1814
1931
|
else {
|
|
1815
|
-
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");
|
|
1816
1933
|
}
|
|
1817
1934
|
}
|
|
1818
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
|
+
}
|
|
1819
1939
|
if (parsed.login && parsed.token !== undefined) {
|
|
1820
1940
|
throw new Error("--login and --token cannot be used together");
|
|
1821
1941
|
}
|
|
@@ -1913,7 +2033,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1913
2033
|
...new Set([
|
|
1914
2034
|
...requestedExpansion.installations.map((installation) => installation.agent),
|
|
1915
2035
|
...detectedAgents,
|
|
1916
|
-
...options.agents.filter((agent) => isCredentialOnlyAgent(agent) || agent === "t3code"),
|
|
2036
|
+
...options.agents.filter((agent) => isCredentialOnlyAgent(agent) || agent === "t3code" || agent === GENERIC_AGENT),
|
|
1917
2037
|
]),
|
|
1918
2038
|
].sort();
|
|
1919
2039
|
const keptAgents = [
|
|
@@ -1927,13 +2047,17 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1927
2047
|
// Cowork and the Devin CLI read the saved credential from ~/.lore/config.json
|
|
1928
2048
|
// and need no hooks, so they connect without a native agent installation.
|
|
1929
2049
|
const credentialOnlyAgents = CREDENTIAL_ONLY_AGENTS.filter((agent) => options.agents.includes(agent));
|
|
1930
|
-
|
|
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) {
|
|
1931
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.");
|
|
1932
2054
|
}
|
|
1933
2055
|
const now = new Date();
|
|
2056
|
+
// Reconnecting without a capture option keeps the saved mode, like folder rules.
|
|
2057
|
+
const capture = options.capture ?? captureEnabled(existing);
|
|
1934
2058
|
// Read and merge before anything is persisted so an unreadable agent file
|
|
1935
2059
|
// stops the run without a half-written connection.
|
|
1936
|
-
const entries = await prepareAgentEntries(installations, paths);
|
|
2060
|
+
const entries = await prepareAgentEntries(installations, paths, capture);
|
|
1937
2061
|
const agentConfigPaths = {};
|
|
1938
2062
|
for (const agent of agents) {
|
|
1939
2063
|
agentConfigPaths[agent] = installations
|
|
@@ -1949,6 +2073,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1949
2073
|
token: token.trim(),
|
|
1950
2074
|
agents,
|
|
1951
2075
|
agentConfigPaths,
|
|
2076
|
+
...(capture ? {} : { capture: "off" }),
|
|
1952
2077
|
connectedAt: connection.profiles.find(value => value.connectionId === connection.fields.connectionId)?.connectedAt ?? now.toISOString(),
|
|
1953
2078
|
timeoutMs,
|
|
1954
2079
|
};
|
|
@@ -1960,6 +2085,9 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1960
2085
|
// Folder restrictions must be durable before any new hook can run.
|
|
1961
2086
|
await persistConnection(paths, config, connection.profiles);
|
|
1962
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(() => []));
|
|
1963
2091
|
if (existing && !existing.connectionId && existing.apiUrl === apiUrl && existing.token === token) {
|
|
1964
2092
|
await new ReliabilityStore(identity.workspaceId, { home: paths.home }).transferPendingTo(reliabilityStore);
|
|
1965
2093
|
await reliabilityStore.migrateLegacyQueue(paths.queue);
|
|
@@ -1979,7 +2107,10 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1979
2107
|
.catch((error) => [`Host recovery setup: ${error.message}`]) : [];
|
|
1980
2108
|
if (options.recovery === false)
|
|
1981
2109
|
await disableRecovery(paths.home);
|
|
1982
|
-
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);
|
|
1983
2114
|
// An unchanged reconnect: same server, credential, and workspace, every
|
|
1984
2115
|
// configured agent was already configured, and no agent file was rewritten.
|
|
1985
2116
|
const alreadyConnected = existing !== null &&
|
|
@@ -1988,6 +2119,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1988
2119
|
existing.workspaceId === identity.workspaceId &&
|
|
1989
2120
|
existing.connectionId === config.connectionId &&
|
|
1990
2121
|
agents.every((agent) => existing.agents.includes(agent)) &&
|
|
2122
|
+
captureEnabled(existing) === capture &&
|
|
1991
2123
|
changedHookFiles.length === 0 &&
|
|
1992
2124
|
JSON.stringify(existing.directories ?? null) === JSON.stringify(config.directories ?? null);
|
|
1993
2125
|
// A Codex set up in this run gets the approval step in its next-step line;
|
|
@@ -1998,12 +2130,18 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
1998
2130
|
const codexNeedsApproval = !installedAgents.includes("codex") &&
|
|
1999
2131
|
changedHookFiles.some((path) => codexHookPaths.includes(path));
|
|
2000
2132
|
return {
|
|
2001
|
-
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 ?? [] },
|
|
2002
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
|
+
} } : {}),
|
|
2003
2141
|
connected: true,
|
|
2004
2142
|
apiUrl: config.apiUrl,
|
|
2005
2143
|
identity,
|
|
2006
|
-
agents: [...agents, ...credentialOnlyAgents],
|
|
2144
|
+
agents: [...agents, ...credentialOnlyAgents, ...(genericAgent ? [GENERIC_AGENT] : [])],
|
|
2007
2145
|
installedAgents,
|
|
2008
2146
|
keptAgents,
|
|
2009
2147
|
alreadyConnected,
|
|
@@ -2012,7 +2150,7 @@ async function connectWithCredentialLocked(options, identity) {
|
|
|
2012
2150
|
config: paths.config,
|
|
2013
2151
|
changedHookFiles,
|
|
2014
2152
|
backups,
|
|
2015
|
-
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
|
|
2016
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."]
|
|
2017
2155
|
: [])],
|
|
2018
2156
|
...(recoveryNotice ? { recoveryNotice } : {}),
|
|
@@ -2164,12 +2302,17 @@ export function connectNextStep(agent) {
|
|
|
2164
2302
|
case "cowork":
|
|
2165
2303
|
case "t3code":
|
|
2166
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.";
|
|
2167
2307
|
}
|
|
2168
2308
|
}
|
|
2169
2309
|
export function formatConnectResult(result) {
|
|
2170
|
-
const
|
|
2310
|
+
const folderSummary = result.capture.folders === null ? "all projects allowed by the workspace"
|
|
2171
2311
|
: result.capture.folders.length ? result.capture.folders.join(", ") : "paused — choose folders with --folder <path>";
|
|
2172
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));
|
|
2173
2316
|
const lines = [
|
|
2174
2317
|
result.alreadyConnected
|
|
2175
2318
|
? `Already connected to ${result.identity.workspaceName} for ${installed}; nothing changed.`
|
|
@@ -2177,14 +2320,32 @@ export function formatConnectResult(result) {
|
|
|
2177
2320
|
...(result.keptAgents.length ? [`kept: ${result.keptAgents.join(", ")}`] : []),
|
|
2178
2321
|
`api_url: ${result.apiUrl}`,
|
|
2179
2322
|
`config: ${result.config}`,
|
|
2180
|
-
|
|
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(", ")}` : ""}`,
|
|
2181
2326
|
...result.installedAgents.map((agent) => `Next step for ${agent}: ${connectNextStep(agent)}`),
|
|
2182
|
-
|
|
2183
|
-
? `
|
|
2184
|
-
:
|
|
2185
|
-
|
|
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."] : []),
|
|
2186
2336
|
...(result.recoveryNotice ? [result.recoveryNotice] : []),
|
|
2187
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
|
+
] : []),
|
|
2188
2349
|
];
|
|
2189
2350
|
return `${lines.join("\n")}\n`;
|
|
2190
2351
|
}
|
|
@@ -2228,13 +2389,17 @@ async function connectCommand(args) {
|
|
|
2228
2389
|
}
|
|
2229
2390
|
const result = await connectWithCredential({
|
|
2230
2391
|
...capture,
|
|
2392
|
+
...(parsed.capture === undefined ? {} : { capture: parsed.capture }),
|
|
2231
2393
|
...(parsed.connectionName === undefined ? {} : { connectionName: parsed.connectionName }),
|
|
2232
2394
|
...(parsed.fallback === undefined ? {} : { fallback: parsed.fallback }),
|
|
2233
|
-
|
|
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)),
|
|
2234
2398
|
apiUrl,
|
|
2235
2399
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
2236
2400
|
token,
|
|
2237
2401
|
agents: parsed.agents,
|
|
2402
|
+
...(parsed.agents.includes(GENERIC_AGENT) ? { genericAgent: { id: parsed.agentId ?? GENERIC_AGENT, writeTools: parsed.writeTools === true } } : {}),
|
|
2238
2403
|
...(parsed.timeoutMs === undefined ? {} : { timeoutMs: parsed.timeoutMs }),
|
|
2239
2404
|
});
|
|
2240
2405
|
if (authorization)
|
|
@@ -2356,6 +2521,7 @@ async function inspectRuntimeInstallation(required, paths) {
|
|
|
2356
2521
|
"skill-guidance.js": resolve(dirname(paths.runtime), "skill-guidance.js"),
|
|
2357
2522
|
"repository.js": paths.runtimeRepository,
|
|
2358
2523
|
"repository-address.js": resolve(dirname(paths.runtime), "repository-address.js"),
|
|
2524
|
+
"capture-policy.js": resolve(dirname(paths.runtime), "capture-policy.js"),
|
|
2359
2525
|
"directory-policy.js": resolve(dirname(paths.runtime), "directory-policy.js"),
|
|
2360
2526
|
"reliability-store.js": paths.runtimeReliabilityStore,
|
|
2361
2527
|
"windows-permissions.js": resolve(dirname(paths.runtime), "windows-permissions.js"),
|
|
@@ -2470,11 +2636,11 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
2470
2636
|
existing += document.exists ? 1 : 0;
|
|
2471
2637
|
units.push(...inspectInstallationUnits(document.value === undefined
|
|
2472
2638
|
? target.emptyValue()
|
|
2473
|
-
: document.value, agent, paths, index, configPaths.length));
|
|
2639
|
+
: document.value, agent, paths, index, configPaths.length, captureEnabled(config)));
|
|
2474
2640
|
if (isMcpHostAgent(agent)) {
|
|
2475
2641
|
const mcpPath = mcpConfigPath(agent, path);
|
|
2476
2642
|
if (agent === "codex") {
|
|
2477
|
-
const state = inspectCodexMcp((await readTextDocument(mcpPath)).value, loreMcpServer(paths));
|
|
2643
|
+
const state = inspectCodexMcp((await readTextDocument(mcpPath)).value, loreMcpServer(paths, "codex"));
|
|
2478
2644
|
installed += state.owned;
|
|
2479
2645
|
units.push({ id: installationUnitId("mcp-tools", index, configPaths.length),
|
|
2480
2646
|
state: installationUnitState(state.owned, Number(state.exact)) });
|
|
@@ -2489,11 +2655,11 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
2489
2655
|
units.push({ id: installationUnitId("mcp", index, configPaths.length),
|
|
2490
2656
|
state: installationUnitState(owned, Number(exactConfiguration(bridge, cursorMcpServer(paths)))) });
|
|
2491
2657
|
}
|
|
2492
|
-
const value = servers[LORE_MCP_SERVER_NAME];
|
|
2658
|
+
const value = agent === "copilot-vscode" ? loreVsCodeMcpServer(mcp.value) : servers[LORE_MCP_SERVER_NAME];
|
|
2493
2659
|
const owned = Number(isLoreMcpServer(value));
|
|
2494
2660
|
installed += owned;
|
|
2495
2661
|
units.push({ id: installationUnitId("mcp-tools", index, configPaths.length),
|
|
2496
|
-
state: installationUnitState(owned, Number(exactConfiguration(value, loreMcpServer(paths)))) });
|
|
2662
|
+
state: installationUnitState(owned, Number(exactConfiguration(value, loreMcpServer(paths, agent)))) });
|
|
2497
2663
|
}
|
|
2498
2664
|
}
|
|
2499
2665
|
}
|
|
@@ -2515,7 +2681,7 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
2515
2681
|
state: "not_required",
|
|
2516
2682
|
version: null,
|
|
2517
2683
|
};
|
|
2518
|
-
const expected = target.expected * configPaths.length;
|
|
2684
|
+
const expected = target.expected(captureEnabled(config)) * configPaths.length;
|
|
2519
2685
|
const exactUnits = units.filter((unit) => unit.state === "exact").length;
|
|
2520
2686
|
const installationState = invalidConfiguration
|
|
2521
2687
|
? "invalid"
|
|
@@ -2578,13 +2744,17 @@ async function statusData(paths, cwd) {
|
|
|
2578
2744
|
}
|
|
2579
2745
|
const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
|
|
2580
2746
|
const runtime = await inspectRuntimeInstallation(runtimeRequired, paths);
|
|
2581
|
-
const [legacyQueuedTurns, reliability, agents] = await Promise.all([
|
|
2747
|
+
const [legacyQueuedTurns, reliability, agents, switches] = await Promise.all([
|
|
2582
2748
|
queueCount(paths),
|
|
2583
2749
|
inspectReliabilityStore(config, paths),
|
|
2584
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,
|
|
2585
2753
|
]);
|
|
2586
2754
|
return {
|
|
2587
2755
|
folderAccess: await checkDirectory(cwd, paths.home, config ?? undefined),
|
|
2756
|
+
capture: captureEnabled(config) ? "on" : "off",
|
|
2757
|
+
captureSwitches: switches,
|
|
2588
2758
|
schemaVersion: "lore-cli-status-v1",
|
|
2589
2759
|
integrationContractVersion: RELIABILITY_INTEGRATION_CONTRACT_VERSION,
|
|
2590
2760
|
connected: config !== null,
|
|
@@ -2633,7 +2803,7 @@ async function statusCommand(args) {
|
|
|
2633
2803
|
? "-"
|
|
2634
2804
|
: `${(value * 100).toFixed(1)}%`;
|
|
2635
2805
|
const retrievalFailureLines = formatRetrievalFailureDiagnostics(data.reliability?.operational.retrieval);
|
|
2636
|
-
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`);
|
|
2637
2807
|
}
|
|
2638
2808
|
async function disconnectCommand(args) {
|
|
2639
2809
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -2693,13 +2863,16 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2693
2863
|
backups.push(result.backup);
|
|
2694
2864
|
}
|
|
2695
2865
|
}
|
|
2696
|
-
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);
|
|
2697
2869
|
await Promise.all([
|
|
2698
2870
|
rm(paths.config, { force: true }),
|
|
2699
2871
|
rm(resolve(paths.loreDirectory, "connections"), { recursive: true, force: true }),
|
|
2700
2872
|
rm(paths.runtime, { force: true }),
|
|
2701
2873
|
rm(paths.runtimeRepository, { force: true }),
|
|
2702
2874
|
rm(resolve(dirname(paths.runtime), "repository-address.js"), { force: true }),
|
|
2875
|
+
rm(resolve(dirname(paths.runtime), "capture-policy.js"), { force: true }),
|
|
2703
2876
|
rm(resolve(dirname(paths.runtime), "directory-policy.js"), { force: true }),
|
|
2704
2877
|
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
2705
2878
|
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
@@ -2725,6 +2898,47 @@ export async function disconnectInstallation(paths = getLorePaths(), preservePen
|
|
|
2725
2898
|
]);
|
|
2726
2899
|
return { connected: false, changedHookFiles, backups };
|
|
2727
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
|
+
}
|
|
2728
2942
|
/**
|
|
2729
2943
|
* Server-side queue health. Dead embedding jobs name the operator fix: once the
|
|
2730
2944
|
* provider is repaired, `lore embeddings repair` retries them without waiting
|
|
@@ -2866,6 +3080,93 @@ async function verifyConnectedInstallation() {
|
|
|
2866
3080
|
}
|
|
2867
3081
|
return (await apiChecks(config)).every((check) => check.status !== "error");
|
|
2868
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
|
+
}
|
|
2869
3170
|
async function doctorCommand(args) {
|
|
2870
3171
|
const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
|
|
2871
3172
|
if (parsed === null) {
|
|
@@ -2875,7 +3176,15 @@ async function doctorCommand(args) {
|
|
|
2875
3176
|
const config = await diagnosticConfig(paths, process.cwd());
|
|
2876
3177
|
const prunedDeadLetters = await pruneUnrecoverableDeadLetters(config, paths);
|
|
2877
3178
|
const status = await statusData(paths, process.cwd());
|
|
2878
|
-
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
|
+
}
|
|
2879
3188
|
checks.push({
|
|
2880
3189
|
name: "platform",
|
|
2881
3190
|
status: platform() === "darwin" || platform() === "linux" || isWindows() ? "ok" : "error",
|
|
@@ -2957,6 +3266,7 @@ async function doctorCommand(args) {
|
|
|
2957
3266
|
});
|
|
2958
3267
|
}
|
|
2959
3268
|
}
|
|
3269
|
+
checks.push(...genericChecks);
|
|
2960
3270
|
for (const surface of status.externalSurfaces) {
|
|
2961
3271
|
checks.push({
|
|
2962
3272
|
name: `${surface.surface}-capability`,
|
|
@@ -3044,6 +3354,9 @@ async function printCommandHelp(path) {
|
|
|
3044
3354
|
case "doctor":
|
|
3045
3355
|
process.stdout.write(DOCTOR_HELP);
|
|
3046
3356
|
return;
|
|
3357
|
+
case "capture":
|
|
3358
|
+
process.stdout.write(CAPTURE_HELP);
|
|
3359
|
+
return;
|
|
3047
3360
|
case "embeddings":
|
|
3048
3361
|
await runEmbeddingsCommand(args, null);
|
|
3049
3362
|
return;
|
|
@@ -3059,6 +3372,9 @@ async function printCommandHelp(path) {
|
|
|
3059
3372
|
case "ask":
|
|
3060
3373
|
await runAskCommand(args, null);
|
|
3061
3374
|
return;
|
|
3375
|
+
case "chat":
|
|
3376
|
+
await runChatCommand(args, null);
|
|
3377
|
+
return;
|
|
3062
3378
|
case "learnings":
|
|
3063
3379
|
await runLearningsCommand(args, null);
|
|
3064
3380
|
return;
|
|
@@ -3149,7 +3465,7 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3149
3465
|
const helpIndex = args.findIndex((value, index) => index > 0 && (separator < 0 || index < separator) && (value === "--help" || value === "-h"));
|
|
3150
3466
|
if (helpIndex > 0) {
|
|
3151
3467
|
const subcommand = args[1];
|
|
3152
|
-
const grouped = ["connect", "config", "embeddings", "guard", "recovery", "github", "devin", "host", "self-host", "scope", "review"].includes(command ?? "");
|
|
3468
|
+
const grouped = ["connect", "config", "capture", "embeddings", "guard", "recovery", "github", "devin", "host", "self-host", "scope", "review"].includes(command ?? "");
|
|
3153
3469
|
await printCommandHelp([command, ...(grouped && subcommand && !subcommand.startsWith("-") ? [subcommand] : [])]);
|
|
3154
3470
|
return;
|
|
3155
3471
|
}
|
|
@@ -3162,7 +3478,7 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3162
3478
|
return;
|
|
3163
3479
|
}
|
|
3164
3480
|
const commandArgs = args.slice(1);
|
|
3165
|
-
if (["ask", "learnings", "review", "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")) {
|
|
3166
3482
|
const decision = await checkDirectory(process.cwd(), getLorePaths(["approach", "skill"].includes(command) ? process.env.LORE_HOME : undefined).home);
|
|
3167
3483
|
if (!decision.allowed)
|
|
3168
3484
|
throw new Error(`Lore is off in this folder. ${decision.reason}`);
|
|
@@ -3176,6 +3492,9 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3176
3492
|
case "config":
|
|
3177
3493
|
await runConfigCommand(commandArgs, getLorePaths().config);
|
|
3178
3494
|
return;
|
|
3495
|
+
case "capture":
|
|
3496
|
+
await captureCommand(commandArgs);
|
|
3497
|
+
return;
|
|
3179
3498
|
case "scope": {
|
|
3180
3499
|
if (commandArgs[0] !== "check" || commandArgs.length > 2)
|
|
3181
3500
|
throw new Error("Usage: lore scope check [folder]");
|
|
@@ -3202,6 +3521,9 @@ async function executeCli(args = process.argv.slice(2)) {
|
|
|
3202
3521
|
case "ask":
|
|
3203
3522
|
await runAskCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3204
3523
|
return;
|
|
3524
|
+
case "chat":
|
|
3525
|
+
await runChatCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3526
|
+
return;
|
|
3205
3527
|
case "learnings":
|
|
3206
3528
|
await runLearningsCommand(commandArgs, await readConnectorConfig(getLorePaths(), process.cwd()));
|
|
3207
3529
|
return;
|