@lore-co/cli 0.1.18 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/cli.d.ts +37 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +593 -49
- package/dist/cli.js.map +1 -1
- package/dist/cursor-mcp.d.ts +5 -0
- package/dist/cursor-mcp.d.ts.map +1 -0
- package/dist/cursor-mcp.js +41 -0
- package/dist/cursor-mcp.js.map +1 -0
- package/dist/devin-client.d.ts.map +1 -1
- package/dist/devin-client.js +6 -3
- package/dist/devin-client.js.map +1 -1
- package/dist/generated-assets.d.ts +4 -4
- package/dist/generated-assets.d.ts.map +1 -1
- package/dist/generated-assets.js +4 -4
- package/dist/generated-assets.js.map +1 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +31 -1
- package/dist/guard.js.map +1 -1
- package/dist/remember.d.ts +3 -0
- package/dist/remember.d.ts.map +1 -0
- package/dist/remember.js +159 -0
- package/dist/remember.js.map +1 -0
- package/dist/runtime.d.ts +15 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +319 -110
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +15 -2
- package/dist/self-host.d.ts.map +1 -1
- package/dist/self-host.js +55 -8
- package/dist/self-host.js.map +1 -1
- package/dist/update.js +3 -3
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
import { delimiter, dirname, resolve } from "node:path";
|
|
6
|
-
import { homedir, platform } from "node:os";
|
|
7
|
+
import { homedir, hostname, platform } from "node:os";
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, CURSOR_RELIABILITY_INTEGRATION, NATIVE_CODING_AGENT_NAMES, OPENCODE_RELIABILITY_INTEGRATION, OperationalMetricsResponseSchema, POLYTOKEN_RELIABILITY_INTEGRATION, RELIABILITY_INTEGRATION_CONTRACT_VERSION, ReliabilityInvocationRecordSchema, WorkspaceIdentityResponseSchema, canonicalJson, deriveReliabilityInvocationHealth, isNativeCodingAgent, } from "@lore-co/core";
|
|
10
|
+
import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, COPILOT_CLI_RELIABILITY_INTEGRATION, COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION, COPILOT_VSCODE_PREVIEW_RELIABILITY_INTEGRATION, CreateDeviceAuthorizationResponseSchema, CURSOR_RELIABILITY_INTEGRATION, NATIVE_CODING_AGENT_NAMES, OPENCODE_RELIABILITY_INTEGRATION, OperationalMetricsResponseSchema, PollDeviceAuthorizationResponseSchema, POLYTOKEN_RELIABILITY_INTEGRATION, RELIABILITY_INTEGRATION_CONTRACT_VERSION, ReliabilityInvocationRecordSchema, WorkspaceIdentityResponseSchema, canonicalJson, deriveReliabilityInvocationHealth, isNativeCodingAgent, } from "@lore-co/core";
|
|
9
11
|
import { runHook, } from "./runtime.js";
|
|
10
12
|
import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
|
|
11
13
|
import { runAskCommand } from "./ask.js";
|
|
14
|
+
import { runRememberCommand } from "./remember.js";
|
|
12
15
|
import { runGuardCommand } from "./guard.js";
|
|
13
16
|
import { runContinueCommand, runResumeCommand } from "./handoff.js";
|
|
14
17
|
import { runAttachCommand, runLiveHostCommand, runLiveRunCommand, } from "./live.js";
|
|
@@ -33,8 +36,9 @@ const HOOK_EVENTS = [
|
|
|
33
36
|
"SessionEnd",
|
|
34
37
|
];
|
|
35
38
|
/** Tools the Context Guard PreToolUse hook cares about (regex matcher). */
|
|
36
|
-
const GUARD_TOOL_MATCHER = "Edit|Write|MultiEdit|NotebookEdit|ApplyPatch|apply_patch|Bash";
|
|
39
|
+
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";
|
|
37
40
|
const CURSOR_HOOK_EVENTS = [
|
|
41
|
+
"sessionStart",
|
|
38
42
|
"beforeSubmitPrompt",
|
|
39
43
|
"beforeShellExecution",
|
|
40
44
|
"afterAgentResponse",
|
|
@@ -43,10 +47,28 @@ const CURSOR_HOOK_EVENTS = [
|
|
|
43
47
|
const POLYTOKEN_HOOK_EVENTS = [
|
|
44
48
|
"pre_user_prompt",
|
|
45
49
|
"post_model_turn",
|
|
50
|
+
"pre_tool_use",
|
|
51
|
+
];
|
|
52
|
+
const COPILOT_CLI_HOOK_EVENTS = [
|
|
53
|
+
"userPromptTransformed",
|
|
54
|
+
"preToolUse",
|
|
55
|
+
"agentStop",
|
|
56
|
+
"sessionEnd",
|
|
57
|
+
];
|
|
58
|
+
const EXTERNAL_SURFACES = [
|
|
59
|
+
{
|
|
60
|
+
surface: "copilot-cloud-mcp",
|
|
61
|
+
integrationId: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.integrationId,
|
|
62
|
+
installation: "repository_template",
|
|
63
|
+
locallyInspectable: false,
|
|
64
|
+
capabilities: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.capabilities,
|
|
65
|
+
},
|
|
46
66
|
];
|
|
47
67
|
const AGENT_MANIFESTS = {
|
|
48
68
|
claude: CLAUDE_RELIABILITY_INTEGRATION,
|
|
49
69
|
codex: CODEX_RELIABILITY_INTEGRATION,
|
|
70
|
+
"copilot-cli": COPILOT_CLI_RELIABILITY_INTEGRATION,
|
|
71
|
+
"copilot-vscode": COPILOT_VSCODE_PREVIEW_RELIABILITY_INTEGRATION,
|
|
50
72
|
cursor: CURSOR_RELIABILITY_INTEGRATION,
|
|
51
73
|
opencode: OPENCODE_RELIABILITY_INTEGRATION,
|
|
52
74
|
polytoken: POLYTOKEN_RELIABILITY_INTEGRATION,
|
|
@@ -60,6 +82,7 @@ Usage:
|
|
|
60
82
|
Commands:
|
|
61
83
|
connect Configure Lore and install native agent integrations
|
|
62
84
|
ask Query shared knowledge and prior session context
|
|
85
|
+
remember Save a durable fact, convention, correction, or decision
|
|
63
86
|
guard Control Context Guard (off, assist, enforce)
|
|
64
87
|
resume Continue previous work with saved Lore context
|
|
65
88
|
continue Hand the current work to a different agent (--on <agent>)
|
|
@@ -79,6 +102,7 @@ Commands:
|
|
|
79
102
|
Discover:
|
|
80
103
|
lore connect --help
|
|
81
104
|
lore ask --help
|
|
105
|
+
lore remember --help
|
|
82
106
|
lore guard --help
|
|
83
107
|
lore resume --help
|
|
84
108
|
lore continue --help
|
|
@@ -92,6 +116,7 @@ Discover:
|
|
|
92
116
|
Examples:
|
|
93
117
|
lore connect --token "$LORE_WORKSPACE_TOKEN"
|
|
94
118
|
lore ask "How should payment retries work?"
|
|
119
|
+
lore remember "Use pnpm for this repository." --repo acme/app
|
|
95
120
|
lore resume
|
|
96
121
|
lore continue --on codex
|
|
97
122
|
lore connect github --repo owner/repository
|
|
@@ -102,21 +127,26 @@ Examples:
|
|
|
102
127
|
lore devin --help
|
|
103
128
|
`;
|
|
104
129
|
const CONNECT_HELP = `lore connect
|
|
105
|
-
|
|
130
|
+
Authorize this device and idempotently install agent integrations.
|
|
106
131
|
|
|
107
132
|
Usage:
|
|
133
|
+
lore connect --login [options]
|
|
108
134
|
lore connect --token <token> [options]
|
|
109
135
|
|
|
110
136
|
Options:
|
|
111
137
|
--url <url> Lore API base URL (default: https://api.uselore.co)
|
|
112
138
|
--dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
|
|
139
|
+
--login Approve access securely in the Lore dashboard
|
|
140
|
+
--no-open Print the approval URL without opening a browser
|
|
113
141
|
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
114
|
-
--agent <name> claude, codex, cursor, opencode, polytoken, or t3code
|
|
142
|
+
--agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code
|
|
115
143
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
116
144
|
--json Print machine-readable output
|
|
117
145
|
--help Show this command's help
|
|
118
146
|
|
|
119
147
|
Examples:
|
|
148
|
+
lore connect --login --agent claude
|
|
149
|
+
lore connect --login --agent cowork
|
|
120
150
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
|
|
121
151
|
lore connect --url http://localhost:3004 --token dev-token --agent codex
|
|
122
152
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
|
|
@@ -159,7 +189,7 @@ function isObject(value) {
|
|
|
159
189
|
function cloneObject(value) {
|
|
160
190
|
return structuredClone(value);
|
|
161
191
|
}
|
|
162
|
-
export function getLorePaths(home, environment = process.env) {
|
|
192
|
+
export function getLorePaths(home, environment = process.env, cwd = process.cwd()) {
|
|
163
193
|
const resolvedHome = resolve(home ?? environment.HOME ?? homedir());
|
|
164
194
|
const loreDirectory = resolve(resolvedHome, ".lore");
|
|
165
195
|
const xdgConfigHome = environment.XDG_CONFIG_HOME?.trim() === undefined ||
|
|
@@ -167,6 +197,7 @@ export function getLorePaths(home, environment = process.env) {
|
|
|
167
197
|
? resolve(resolvedHome, ".config")
|
|
168
198
|
: resolve(environment.XDG_CONFIG_HOME);
|
|
169
199
|
const t3Home = resolve(environment.T3CODE_HOME?.trim() || resolve(resolvedHome, ".t3"));
|
|
200
|
+
const copilotHome = resolve(environment.COPILOT_HOME?.trim() || resolve(resolvedHome, ".copilot"));
|
|
170
201
|
return {
|
|
171
202
|
home: resolvedHome,
|
|
172
203
|
loreDirectory,
|
|
@@ -183,8 +214,12 @@ export function getLorePaths(home, environment = process.env) {
|
|
|
183
214
|
state: resolve(loreDirectory, "state"),
|
|
184
215
|
queue: resolve(loreDirectory, "queue"),
|
|
185
216
|
codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
|
|
217
|
+
copilotCliHooks: resolve(copilotHome, "hooks", "lore.json"),
|
|
218
|
+
copilotVscodeHooks: resolve(environment.LORE_COPILOT_VSCODE_HOOKS?.trim() ||
|
|
219
|
+
resolve(cwd, ".github", "hooks", "lore-vscode.json")),
|
|
186
220
|
claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
|
|
187
221
|
cursorHooks: resolve(resolvedHome, ".cursor", "hooks.json"),
|
|
222
|
+
cursorMcp: resolve(resolvedHome, ".cursor", "mcp.json"),
|
|
188
223
|
cursorUserData: platform() === "darwin"
|
|
189
224
|
? resolve(resolvedHome, "Library", "Application Support", "Cursor")
|
|
190
225
|
: resolve(resolvedHome, ".config", "Cursor"),
|
|
@@ -200,14 +235,18 @@ function isConfiguredAgent(value) {
|
|
|
200
235
|
return typeof value === "string" && isNativeCodingAgent(value);
|
|
201
236
|
}
|
|
202
237
|
function isConnectAgent(value) {
|
|
203
|
-
return isConfiguredAgent(value) ||
|
|
238
|
+
return (isConfiguredAgent(value) ||
|
|
239
|
+
value === "copilot" ||
|
|
240
|
+
value === "cowork" ||
|
|
241
|
+
value === "t3code");
|
|
204
242
|
}
|
|
205
|
-
function hookCommand(agent, paths) {
|
|
243
|
+
function hookCommand(agent, paths, event) {
|
|
206
244
|
const loreHome = `LORE_HOME=${shellQuote(paths.home)}`;
|
|
245
|
+
const hookEvent = event === undefined ? "" : ` LORE_HOOK_EVENT=${shellQuote(event)}`;
|
|
207
246
|
if (IS_STANDALONE_BINARY) {
|
|
208
|
-
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
247
|
+
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome}${hookEvent} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
209
248
|
}
|
|
210
|
-
return `env ${loreHome} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
249
|
+
return `env ${loreHome}${hookEvent} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
211
250
|
}
|
|
212
251
|
function isLoreHook(value) {
|
|
213
252
|
if (!isObject(value) || value.type !== "command") {
|
|
@@ -333,6 +372,35 @@ function cursorEventHandler(event, paths) {
|
|
|
333
372
|
: 25,
|
|
334
373
|
};
|
|
335
374
|
}
|
|
375
|
+
const CURSOR_MCP_NAME = "lore-native";
|
|
376
|
+
function cursorMcpServer(paths) {
|
|
377
|
+
return {
|
|
378
|
+
type: "stdio", command: process.execPath,
|
|
379
|
+
args: [...(IS_STANDALONE_BINARY ? [] : [fileURLToPath(import.meta.url)]), "cursor-mcp", "--owner", "lore"],
|
|
380
|
+
env: { LORE_HOME: paths.home },
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function isLoreCursorMcp(value) {
|
|
384
|
+
return isObject(value) && Array.isArray(value.args) && value.args.includes("cursor-mcp")
|
|
385
|
+
&& value.args.includes("--owner") && value.args.includes("lore");
|
|
386
|
+
}
|
|
387
|
+
export function mergeLoreCursorMcp(input, paths) {
|
|
388
|
+
const config = objectConfiguration(input, "cursor");
|
|
389
|
+
if (config.mcpServers !== undefined && !isObject(config.mcpServers))
|
|
390
|
+
throw new Error("Cursor mcpServers must be an object");
|
|
391
|
+
const servers = isObject(config.mcpServers) ? config.mcpServers : {};
|
|
392
|
+
if (servers[CURSOR_MCP_NAME] !== undefined && !isLoreCursorMcp(servers[CURSOR_MCP_NAME])) {
|
|
393
|
+
throw new Error('Cursor MCP name "lore-native" is already owned by another server; rename it before connecting.');
|
|
394
|
+
}
|
|
395
|
+
return { ...config, mcpServers: { ...servers, [CURSOR_MCP_NAME]: cursorMcpServer(paths) } };
|
|
396
|
+
}
|
|
397
|
+
export function removeLoreCursorMcp(input) {
|
|
398
|
+
const config = objectConfiguration(input, "cursor");
|
|
399
|
+
if (!isObject(config.mcpServers) || !isLoreCursorMcp(config.mcpServers[CURSOR_MCP_NAME]))
|
|
400
|
+
return config;
|
|
401
|
+
const { [CURSOR_MCP_NAME]: _lore, ...rest } = config.mcpServers;
|
|
402
|
+
return { ...config, mcpServers: rest };
|
|
403
|
+
}
|
|
336
404
|
function stripLoreFromCursorEvent(value) {
|
|
337
405
|
return Array.isArray(value) ? value.filter((hook) => !isLoreHook(hook)) : [];
|
|
338
406
|
}
|
|
@@ -396,6 +464,169 @@ export function countLoreCursorHooks(input) {
|
|
|
396
464
|
return count + hooks[event].filter(isLoreHook).length;
|
|
397
465
|
}, 0);
|
|
398
466
|
}
|
|
467
|
+
function copilotCliEventHandler(event, paths) {
|
|
468
|
+
return {
|
|
469
|
+
type: "command",
|
|
470
|
+
bash: hookCommand("copilot-cli", paths, event),
|
|
471
|
+
timeoutSec: event === "sessionEnd"
|
|
472
|
+
? 2
|
|
473
|
+
: event === "agentStop"
|
|
474
|
+
? 3
|
|
475
|
+
: event === "preToolUse"
|
|
476
|
+
? 10
|
|
477
|
+
: 25,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function isLoreCopilotHook(value) {
|
|
481
|
+
if (!isObject(value) || value.type !== "command") {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const command = typeof value.bash === "string"
|
|
485
|
+
? value.bash
|
|
486
|
+
: typeof value.command === "string"
|
|
487
|
+
? value.command
|
|
488
|
+
: "";
|
|
489
|
+
return (/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
|
|
490
|
+
command.includes("/.lore/bin/lore-hook.mjs"));
|
|
491
|
+
}
|
|
492
|
+
function stripLoreFromCopilotEvent(value) {
|
|
493
|
+
return Array.isArray(value)
|
|
494
|
+
? value.filter((hook) => !isLoreCopilotHook(hook))
|
|
495
|
+
: [];
|
|
496
|
+
}
|
|
497
|
+
export function mergeLoreCopilotCliHooks(input, paths) {
|
|
498
|
+
const result = cloneObject(input);
|
|
499
|
+
if (result.version !== undefined && result.version !== 1) {
|
|
500
|
+
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
501
|
+
}
|
|
502
|
+
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
503
|
+
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
504
|
+
}
|
|
505
|
+
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
506
|
+
for (const event of COPILOT_CLI_HOOK_EVENTS) {
|
|
507
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
508
|
+
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
509
|
+
}
|
|
510
|
+
hooks[event] = [
|
|
511
|
+
...stripLoreFromCopilotEvent(hooks[event]),
|
|
512
|
+
copilotCliEventHandler(event, paths),
|
|
513
|
+
];
|
|
514
|
+
}
|
|
515
|
+
result.version = 1;
|
|
516
|
+
result.hooks = hooks;
|
|
517
|
+
return result;
|
|
518
|
+
}
|
|
519
|
+
export function removeLoreCopilotCliHooks(input) {
|
|
520
|
+
const result = cloneObject(input);
|
|
521
|
+
if (!isObject(result.hooks)) {
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
const hooks = { ...result.hooks };
|
|
525
|
+
for (const event of COPILOT_CLI_HOOK_EVENTS) {
|
|
526
|
+
if (!Array.isArray(hooks[event])) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const remaining = stripLoreFromCopilotEvent(hooks[event]);
|
|
530
|
+
if (remaining.length === 0) {
|
|
531
|
+
delete hooks[event];
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
hooks[event] = remaining;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (Object.keys(hooks).length === 0) {
|
|
538
|
+
delete result.hooks;
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
result.hooks = hooks;
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
export function countLoreCopilotCliHooks(input) {
|
|
546
|
+
if (!isObject(input.hooks)) {
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
return COPILOT_CLI_HOOK_EVENTS.reduce((count, event) => {
|
|
550
|
+
const hooks = input.hooks;
|
|
551
|
+
if (!isObject(hooks) || !Array.isArray(hooks[event])) {
|
|
552
|
+
return count;
|
|
553
|
+
}
|
|
554
|
+
return count + hooks[event].filter(isLoreCopilotHook).length;
|
|
555
|
+
}, 0);
|
|
556
|
+
}
|
|
557
|
+
function copilotVscodeEventHandler(event, paths) {
|
|
558
|
+
return {
|
|
559
|
+
type: "command",
|
|
560
|
+
command: hookCommand("copilot-vscode", paths, event),
|
|
561
|
+
timeout: event === "SessionEnd"
|
|
562
|
+
? 2
|
|
563
|
+
: event === "Stop"
|
|
564
|
+
? 3
|
|
565
|
+
: event === "PreToolUse"
|
|
566
|
+
? 10
|
|
567
|
+
: 25,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
export function mergeLoreCopilotVscodeHooks(input, paths) {
|
|
571
|
+
const result = cloneObject(input);
|
|
572
|
+
if (result.version !== undefined && result.version !== 1) {
|
|
573
|
+
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
574
|
+
}
|
|
575
|
+
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
576
|
+
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
577
|
+
}
|
|
578
|
+
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
579
|
+
for (const event of HOOK_EVENTS) {
|
|
580
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
581
|
+
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
582
|
+
}
|
|
583
|
+
hooks[event] = [
|
|
584
|
+
...stripLoreFromCursorEvent(hooks[event]),
|
|
585
|
+
copilotVscodeEventHandler(event, paths),
|
|
586
|
+
];
|
|
587
|
+
}
|
|
588
|
+
result.version = 1;
|
|
589
|
+
result.hooks = hooks;
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
export function removeLoreCopilotVscodeHooks(input) {
|
|
593
|
+
const result = cloneObject(input);
|
|
594
|
+
if (!isObject(result.hooks)) {
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
const hooks = { ...result.hooks };
|
|
598
|
+
for (const event of HOOK_EVENTS) {
|
|
599
|
+
if (!Array.isArray(hooks[event])) {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const remaining = stripLoreFromCursorEvent(hooks[event]);
|
|
603
|
+
if (remaining.length === 0) {
|
|
604
|
+
delete hooks[event];
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
hooks[event] = remaining;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (Object.keys(hooks).length === 0) {
|
|
611
|
+
delete result.hooks;
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
result.hooks = hooks;
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
export function countLoreCopilotVscodeHooks(input) {
|
|
619
|
+
if (!isObject(input.hooks)) {
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
return HOOK_EVENTS.reduce((count, event) => {
|
|
623
|
+
const hooks = input.hooks;
|
|
624
|
+
if (!isObject(hooks) || !Array.isArray(hooks[event])) {
|
|
625
|
+
return count;
|
|
626
|
+
}
|
|
627
|
+
return count + hooks[event].filter(isLoreHook).length;
|
|
628
|
+
}, 0);
|
|
629
|
+
}
|
|
399
630
|
function polytokenEventHandler(event, paths) {
|
|
400
631
|
return {
|
|
401
632
|
name: `lore-${event.replaceAll("_", "-")}`,
|
|
@@ -536,6 +767,54 @@ function cursorInstallationUnits(input, paths, index, total) {
|
|
|
536
767
|
};
|
|
537
768
|
});
|
|
538
769
|
}
|
|
770
|
+
function copilotCliInstallationUnits(input, paths, index, total) {
|
|
771
|
+
const configuration = objectConfiguration(input, "copilot-cli");
|
|
772
|
+
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
773
|
+
throw new Error('Copilot hook configuration field "version" must be 1');
|
|
774
|
+
}
|
|
775
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
776
|
+
throw new Error('Copilot hook configuration field "hooks" must be an object');
|
|
777
|
+
}
|
|
778
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
779
|
+
return COPILOT_CLI_HOOK_EVENTS.map((event) => {
|
|
780
|
+
const values = hooks[event];
|
|
781
|
+
if (values !== undefined && !Array.isArray(values)) {
|
|
782
|
+
throw new Error(`Copilot hook event "${event}" must be an array`);
|
|
783
|
+
}
|
|
784
|
+
const candidates = Array.isArray(values) ? values : [];
|
|
785
|
+
const expected = copilotCliEventHandler(event, paths);
|
|
786
|
+
const ownedCount = candidates.filter(isLoreCopilotHook).length;
|
|
787
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
788
|
+
return {
|
|
789
|
+
id: installationUnitId(event, index, total),
|
|
790
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
791
|
+
};
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
function copilotVscodeInstallationUnits(input, paths, index, total) {
|
|
795
|
+
const configuration = objectConfiguration(input, "copilot-vscode");
|
|
796
|
+
if (configuration.version !== undefined && configuration.version !== 1) {
|
|
797
|
+
throw new Error('VS Code Copilot hook field "version" must be 1');
|
|
798
|
+
}
|
|
799
|
+
if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
|
|
800
|
+
throw new Error('VS Code Copilot hook field "hooks" must be an object');
|
|
801
|
+
}
|
|
802
|
+
const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
|
|
803
|
+
return HOOK_EVENTS.map((event) => {
|
|
804
|
+
const values = hooks[event];
|
|
805
|
+
if (values !== undefined && !Array.isArray(values)) {
|
|
806
|
+
throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
|
|
807
|
+
}
|
|
808
|
+
const candidates = Array.isArray(values) ? values : [];
|
|
809
|
+
const expected = copilotVscodeEventHandler(event, paths);
|
|
810
|
+
const ownedCount = candidates.filter(isLoreHook).length;
|
|
811
|
+
const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
|
|
812
|
+
return {
|
|
813
|
+
id: installationUnitId(event, index, total),
|
|
814
|
+
state: installationUnitState(ownedCount, exactCount),
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
}
|
|
539
818
|
function polytokenInstallationUnits(input, paths, index, total) {
|
|
540
819
|
if (!Array.isArray(input)) {
|
|
541
820
|
throw new Error("Polytoken hooks must contain a JSON array");
|
|
@@ -574,6 +853,10 @@ function inspectInstallationUnits(input, agent, paths, index, total) {
|
|
|
574
853
|
case "claude":
|
|
575
854
|
case "codex":
|
|
576
855
|
return commandHookInstallationUnits(input, agent, paths, index, total);
|
|
856
|
+
case "copilot-cli":
|
|
857
|
+
return copilotCliInstallationUnits(input, paths, index, total);
|
|
858
|
+
case "copilot-vscode":
|
|
859
|
+
return copilotVscodeInstallationUnits(input, paths, index, total);
|
|
577
860
|
case "cursor":
|
|
578
861
|
return cursorInstallationUnits(input, paths, index, total);
|
|
579
862
|
case "polytoken":
|
|
@@ -883,9 +1166,33 @@ const AGENT_TARGETS = {
|
|
|
883
1166
|
remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
|
|
884
1167
|
count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
|
|
885
1168
|
},
|
|
1169
|
+
"copilot-cli": {
|
|
1170
|
+
integration: "hooks",
|
|
1171
|
+
expected: COPILOT_CLI_HOOK_EVENTS.length,
|
|
1172
|
+
runtimeRequired: true,
|
|
1173
|
+
emptyValue: () => ({}),
|
|
1174
|
+
configPath: (paths) => paths.copilotCliHooks,
|
|
1175
|
+
detect: async (paths) => (await commandExists("copilot")) || pathExists(paths.copilotCliHooks),
|
|
1176
|
+
executable: async () => commandExists("copilot"),
|
|
1177
|
+
merge: (input, paths) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths),
|
|
1178
|
+
remove: (input) => removeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1179
|
+
count: (input) => countLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
|
|
1180
|
+
},
|
|
1181
|
+
"copilot-vscode": {
|
|
1182
|
+
integration: "hooks",
|
|
1183
|
+
expected: HOOK_EVENTS.length,
|
|
1184
|
+
runtimeRequired: true,
|
|
1185
|
+
emptyValue: () => ({}),
|
|
1186
|
+
configPath: (paths) => paths.copilotVscodeHooks,
|
|
1187
|
+
detect: async (paths) => pathExists(paths.copilotVscodeHooks),
|
|
1188
|
+
executable: async () => commandExists("code"),
|
|
1189
|
+
merge: (input, paths) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths),
|
|
1190
|
+
remove: (input) => removeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1191
|
+
count: (input) => countLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
|
|
1192
|
+
},
|
|
886
1193
|
cursor: {
|
|
887
1194
|
integration: "hooks",
|
|
888
|
-
expected: CURSOR_HOOK_EVENTS.length,
|
|
1195
|
+
expected: CURSOR_HOOK_EVENTS.length + 1,
|
|
889
1196
|
runtimeRequired: true,
|
|
890
1197
|
emptyValue: () => ({}),
|
|
891
1198
|
configPath: (paths) => paths.cursorHooks,
|
|
@@ -928,7 +1235,10 @@ async function detectAgents(paths) {
|
|
|
928
1235
|
agent,
|
|
929
1236
|
detected: await targetFor(agent).detect(paths),
|
|
930
1237
|
})));
|
|
931
|
-
|
|
1238
|
+
const agents = detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
|
|
1239
|
+
return agents.includes("copilot-cli")
|
|
1240
|
+
? agents.filter((agent) => agent !== "copilot-vscode")
|
|
1241
|
+
: agents;
|
|
932
1242
|
}
|
|
933
1243
|
function installationKey(installation) {
|
|
934
1244
|
return `${installation.agent}\0${resolve(installation.configPath)}`;
|
|
@@ -1088,7 +1398,14 @@ async function readT3CodeProviderInstallations(paths) {
|
|
|
1088
1398
|
}
|
|
1089
1399
|
}
|
|
1090
1400
|
async function expandConnectInstallations(requested, paths) {
|
|
1091
|
-
const direct = requested.flatMap((agent) =>
|
|
1401
|
+
const direct = requested.flatMap((agent) => {
|
|
1402
|
+
if (agent === "cowork" || agent === "t3code") {
|
|
1403
|
+
return [];
|
|
1404
|
+
}
|
|
1405
|
+
return [
|
|
1406
|
+
defaultInstallation(agent === "copilot" ? "copilot-cli" : agent, paths),
|
|
1407
|
+
];
|
|
1408
|
+
});
|
|
1092
1409
|
if (!requested.includes("t3code")) {
|
|
1093
1410
|
return { installations: direct, warnings: [] };
|
|
1094
1411
|
}
|
|
@@ -1117,6 +1434,7 @@ async function installRuntime(paths) {
|
|
|
1117
1434
|
rm(paths.runtimeReliabilityStore, { force: true }),
|
|
1118
1435
|
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
1119
1436
|
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
1437
|
+
rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
|
|
1120
1438
|
rm(paths.runtimeContextFallback, { force: true }),
|
|
1121
1439
|
rm(paths.runtimeInvocationHealthWriter, { force: true }),
|
|
1122
1440
|
rm(paths.runtimeVersion, { force: true }),
|
|
@@ -1126,6 +1444,7 @@ async function installRuntime(paths) {
|
|
|
1126
1444
|
}
|
|
1127
1445
|
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
1128
1446
|
const canonicalJsonModule = fileURLToPath(import.meta.resolve("@lore-co/core/canonical-json"));
|
|
1447
|
+
const guardEvaluation = await readFile(fileURLToPath(import.meta.resolve("@lore-co/core/guard-evaluation")), "utf8");
|
|
1129
1448
|
const [runtime, repository, reliabilityStore, signedSnapshot, canonicalJson, contextFallback, invocationHealthWriter,] = await Promise.all([
|
|
1130
1449
|
readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
|
|
1131
1450
|
readFile(resolve(sourceDirectory, "repository.js"), "utf8"),
|
|
@@ -1141,7 +1460,8 @@ async function installRuntime(paths) {
|
|
|
1141
1460
|
}
|
|
1142
1461
|
const runtimeVersion = `export const RUNTIME_VERSION = ${JSON.stringify(LORE_VERSION)};\n`;
|
|
1143
1462
|
const runtimeFiles = {
|
|
1144
|
-
"lore-hook.mjs": runtime,
|
|
1463
|
+
"lore-hook.mjs": runtime.replaceAll("@lore-co/core/guard-evaluation", "./guard-evaluation.js"),
|
|
1464
|
+
"guard-evaluation.js": guardEvaluation,
|
|
1145
1465
|
"repository.js": repository,
|
|
1146
1466
|
"reliability-store.js": reliabilityStore,
|
|
1147
1467
|
"signed-snapshot.js": standaloneSignedSnapshot,
|
|
@@ -1161,6 +1481,7 @@ async function installRuntime(paths) {
|
|
|
1161
1481
|
},
|
|
1162
1482
|
};
|
|
1163
1483
|
await Promise.all([
|
|
1484
|
+
atomicWrite(resolve(dirname(paths.runtime), "guard-evaluation.js"), guardEvaluation, 0o600),
|
|
1164
1485
|
atomicWrite(paths.runtimeRepository, repository, 0o600),
|
|
1165
1486
|
atomicWrite(paths.runtimeReliabilityStore, reliabilityStore, 0o600),
|
|
1166
1487
|
atomicWrite(paths.runtimeSignedSnapshot, standaloneSignedSnapshot, 0o600),
|
|
@@ -1172,7 +1493,7 @@ async function installRuntime(paths) {
|
|
|
1172
1493
|
]);
|
|
1173
1494
|
// The hook entrypoint is the generation activation marker. Updating it last
|
|
1174
1495
|
// prevents a new runtime from loading before all of its local modules exist.
|
|
1175
|
-
await atomicWrite(paths.runtime,
|
|
1496
|
+
await atomicWrite(paths.runtime, runtimeFiles["lore-hook.mjs"], 0o700);
|
|
1176
1497
|
}
|
|
1177
1498
|
function agentConfigPath(agent, paths) {
|
|
1178
1499
|
return targetFor(agent).configPath(paths);
|
|
@@ -1203,7 +1524,12 @@ function valueAfter(args, index, flag) {
|
|
|
1203
1524
|
return [value, index + 1];
|
|
1204
1525
|
}
|
|
1205
1526
|
function parseConnectArguments(args) {
|
|
1206
|
-
const parsed = {
|
|
1527
|
+
const parsed = {
|
|
1528
|
+
agents: [],
|
|
1529
|
+
json: false,
|
|
1530
|
+
login: false,
|
|
1531
|
+
openBrowser: true,
|
|
1532
|
+
};
|
|
1207
1533
|
for (let index = 0; index < args.length; index += 1) {
|
|
1208
1534
|
const argument = args[index];
|
|
1209
1535
|
if (argument === "--help" || argument === "-h") {
|
|
@@ -1214,6 +1540,14 @@ function parseConnectArguments(args) {
|
|
|
1214
1540
|
parsed.json = true;
|
|
1215
1541
|
continue;
|
|
1216
1542
|
}
|
|
1543
|
+
if (argument === "--login") {
|
|
1544
|
+
parsed.login = true;
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (argument === "--no-open") {
|
|
1548
|
+
parsed.openBrowser = false;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1217
1551
|
if (argument !== "--url" &&
|
|
1218
1552
|
argument !== "--dashboard-url" &&
|
|
1219
1553
|
argument !== "--token" &&
|
|
@@ -1243,9 +1577,17 @@ function parseConnectArguments(args) {
|
|
|
1243
1577
|
parsed.agents.push(value);
|
|
1244
1578
|
}
|
|
1245
1579
|
else {
|
|
1246
|
-
throw new Error("--agent must be claude, codex, cursor, opencode, polytoken, or t3code");
|
|
1580
|
+
throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code");
|
|
1247
1581
|
}
|
|
1248
1582
|
}
|
|
1583
|
+
if (parsed.login && parsed.token !== undefined) {
|
|
1584
|
+
throw new Error("--login and --token cannot be used together");
|
|
1585
|
+
}
|
|
1586
|
+
if ((parsed.agents.includes("copilot-cli") ||
|
|
1587
|
+
parsed.agents.includes("copilot")) &&
|
|
1588
|
+
parsed.agents.includes("copilot-vscode")) {
|
|
1589
|
+
throw new Error("Install either copilot-cli or copilot-vscode in one connection. Both hosts load repository hooks and would invoke Lore twice.");
|
|
1590
|
+
}
|
|
1249
1591
|
return parsed;
|
|
1250
1592
|
}
|
|
1251
1593
|
function parseOutputArguments(args, help, command) {
|
|
@@ -1266,38 +1608,26 @@ function parseOutputArguments(args, help, command) {
|
|
|
1266
1608
|
function writeResult(value, json, text) {
|
|
1267
1609
|
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : text);
|
|
1268
1610
|
}
|
|
1269
|
-
async function
|
|
1270
|
-
const parsed = parseConnectArguments(args);
|
|
1271
|
-
if (parsed === null) {
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1611
|
+
export async function connectWithCredential(options) {
|
|
1274
1612
|
if (platform() !== "darwin" && platform() !== "linux") {
|
|
1275
1613
|
throw new Error("Lore agent integrations currently support macOS and Linux");
|
|
1276
1614
|
}
|
|
1277
|
-
const paths = getLorePaths();
|
|
1615
|
+
const paths = getLorePaths(options.home, options.home === undefined ? process.env : {}, options.cwd);
|
|
1278
1616
|
const existing = await readConnectorConfig(paths);
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
: { dashboardUrl: parsed.dashboardUrl }),
|
|
1287
|
-
environment: process.env,
|
|
1288
|
-
existing,
|
|
1289
|
-
preferHostedDefault: hasProvidedToken,
|
|
1290
|
-
});
|
|
1291
|
-
const token = configuredToken(parsed.token, existing);
|
|
1292
|
-
if (token === undefined || token.trim() === "") {
|
|
1293
|
-
throw new Error("Workspace token is required. Use --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
|
|
1617
|
+
const apiUrl = normalizeApiUrl(options.apiUrl);
|
|
1618
|
+
const dashboardUrl = options.dashboardUrl === undefined
|
|
1619
|
+
? undefined
|
|
1620
|
+
: normalizeApiUrl(options.dashboardUrl);
|
|
1621
|
+
const token = options.token.trim();
|
|
1622
|
+
if (token === "") {
|
|
1623
|
+
throw new Error("Workspace token cannot be empty");
|
|
1294
1624
|
}
|
|
1295
|
-
const timeoutMs =
|
|
1625
|
+
const timeoutMs = options.timeoutMs ?? existing?.timeoutMs ?? 2_500;
|
|
1296
1626
|
const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
|
|
1297
|
-
const requestedExpansion =
|
|
1627
|
+
const requestedExpansion = options.agents.length === 0
|
|
1298
1628
|
? { installations: [], warnings: [] }
|
|
1299
|
-
: await expandConnectInstallations(
|
|
1300
|
-
const detectedAgents =
|
|
1629
|
+
: await expandConnectInstallations(options.agents, paths);
|
|
1630
|
+
const detectedAgents = options.agents.length === 0 ? await detectAgents(paths) : [];
|
|
1301
1631
|
const installations = uniqueInstallations([
|
|
1302
1632
|
...configuredInstallations(existing, paths),
|
|
1303
1633
|
...requestedExpansion.installations,
|
|
@@ -1306,23 +1636,37 @@ async function connectCommand(args) {
|
|
|
1306
1636
|
const agents = [
|
|
1307
1637
|
...new Set(installations.map((installation) => installation.agent)),
|
|
1308
1638
|
].sort();
|
|
1309
|
-
|
|
1310
|
-
|
|
1639
|
+
const credentialOnlyCowork = options.agents.includes("cowork");
|
|
1640
|
+
if (agents.length === 0 && !credentialOnlyCowork) {
|
|
1641
|
+
throw new Error("No Claude, Codex, Copilot CLI, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
|
|
1311
1642
|
}
|
|
1312
1643
|
const now = new Date();
|
|
1313
1644
|
const documents = new Map();
|
|
1314
1645
|
const mergedDocuments = new Map();
|
|
1646
|
+
const cursorMcpDocuments = new Map();
|
|
1315
1647
|
for (const installation of installations) {
|
|
1316
1648
|
const key = installationKey(installation);
|
|
1317
1649
|
const document = await readJsonDocument(installation.configPath);
|
|
1318
1650
|
documents.set(key, document);
|
|
1319
1651
|
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths));
|
|
1652
|
+
if (installation.agent === "cursor") {
|
|
1653
|
+
const mcpPath = resolve(dirname(installation.configPath), "mcp.json");
|
|
1654
|
+
const mcp = await readJsonDocument(mcpPath);
|
|
1655
|
+
cursorMcpDocuments.set(mcpPath, { document: mcp, merged: mergeLoreCursorMcp(mcp.exists ? mcp.value : {}, paths) });
|
|
1656
|
+
}
|
|
1320
1657
|
}
|
|
1321
1658
|
if (installations.some((installation) => targetFor(installation.agent).runtimeRequired)) {
|
|
1322
1659
|
await installRuntime(paths);
|
|
1323
1660
|
}
|
|
1324
1661
|
const changedHookFiles = [];
|
|
1325
1662
|
const backups = [];
|
|
1663
|
+
for (const [path, { document, merged }] of cursorMcpDocuments) {
|
|
1664
|
+
const result = await writeMergedJson(path, document, merged, now);
|
|
1665
|
+
if (result.changed)
|
|
1666
|
+
changedHookFiles.push(path);
|
|
1667
|
+
if (result.backup)
|
|
1668
|
+
backups.push(result.backup);
|
|
1669
|
+
}
|
|
1326
1670
|
for (const installation of installations) {
|
|
1327
1671
|
const key = installationKey(installation);
|
|
1328
1672
|
const document = documents.get(key);
|
|
@@ -1369,17 +1713,154 @@ async function connectCommand(args) {
|
|
|
1369
1713
|
await credentialStore.transferPendingTo(reliabilityStore);
|
|
1370
1714
|
}
|
|
1371
1715
|
await reliabilityStore.releaseAuthBlocked(now);
|
|
1372
|
-
|
|
1716
|
+
return {
|
|
1373
1717
|
connected: true,
|
|
1374
1718
|
apiUrl: config.apiUrl,
|
|
1375
1719
|
identity,
|
|
1376
|
-
agents,
|
|
1720
|
+
agents: credentialOnlyCowork ? [...agents, "cowork"] : agents,
|
|
1377
1721
|
config: paths.config,
|
|
1378
1722
|
changedHookFiles,
|
|
1379
1723
|
backups,
|
|
1380
1724
|
warnings: requestedExpansion.warnings,
|
|
1381
1725
|
};
|
|
1382
|
-
|
|
1726
|
+
}
|
|
1727
|
+
async function requestDeviceAuthorization(input) {
|
|
1728
|
+
let response;
|
|
1729
|
+
try {
|
|
1730
|
+
response = await fetch(`${input.apiUrl}/v1/device-authorizations`, {
|
|
1731
|
+
method: "POST",
|
|
1732
|
+
headers: {
|
|
1733
|
+
accept: "application/json",
|
|
1734
|
+
"content-type": "application/json",
|
|
1735
|
+
},
|
|
1736
|
+
body: JSON.stringify({
|
|
1737
|
+
clientName: `Lore CLI on ${hostname()}`,
|
|
1738
|
+
agents: input.agents,
|
|
1739
|
+
expiresInDays: 90,
|
|
1740
|
+
}),
|
|
1741
|
+
signal: AbortSignal.timeout(10_000),
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
catch {
|
|
1745
|
+
throw new Error("Lore device authorization is unreachable. Verify --url and retry.");
|
|
1746
|
+
}
|
|
1747
|
+
if (!response.ok) {
|
|
1748
|
+
throw new Error(response.status === 404
|
|
1749
|
+
? "This Lore server does not support browser approval. Upgrade the server or use --token."
|
|
1750
|
+
: "Lore could not start browser approval. Retry in a moment.");
|
|
1751
|
+
}
|
|
1752
|
+
const authorization = CreateDeviceAuthorizationResponseSchema.safeParse(await response.json().catch(() => null));
|
|
1753
|
+
if (!authorization.success) {
|
|
1754
|
+
throw new Error("Lore returned an incompatible device authorization response.");
|
|
1755
|
+
}
|
|
1756
|
+
const serverApprovalUrl = new URL(authorization.data.verificationUriComplete);
|
|
1757
|
+
const approvalUrl = input.dashboardUrl === undefined
|
|
1758
|
+
? serverApprovalUrl.toString()
|
|
1759
|
+
: new URL(`${serverApprovalUrl.pathname}${serverApprovalUrl.search}`, `${input.dashboardUrl}/`).toString();
|
|
1760
|
+
process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approvalUrl}\n`);
|
|
1761
|
+
if (input.openBrowser) {
|
|
1762
|
+
const command = platform() === "darwin" ? "open" : "xdg-open";
|
|
1763
|
+
const child = spawn(command, [approvalUrl], {
|
|
1764
|
+
detached: true,
|
|
1765
|
+
stdio: "ignore",
|
|
1766
|
+
});
|
|
1767
|
+
child.once("error", () => {
|
|
1768
|
+
// The printed URL remains the safe fallback.
|
|
1769
|
+
});
|
|
1770
|
+
child.unref();
|
|
1771
|
+
}
|
|
1772
|
+
const expiresAt = Date.parse(authorization.data.expiresAt);
|
|
1773
|
+
const intervalMs = authorization.data.intervalSeconds * 1_000;
|
|
1774
|
+
while (Date.now() < expiresAt) {
|
|
1775
|
+
await delay(intervalMs);
|
|
1776
|
+
let pollResponse;
|
|
1777
|
+
try {
|
|
1778
|
+
pollResponse = await fetch(`${input.apiUrl}/v1/device-authorizations/poll`, {
|
|
1779
|
+
method: "POST",
|
|
1780
|
+
headers: {
|
|
1781
|
+
accept: "application/json",
|
|
1782
|
+
"content-type": "application/json",
|
|
1783
|
+
},
|
|
1784
|
+
body: JSON.stringify({
|
|
1785
|
+
deviceCode: authorization.data.deviceCode,
|
|
1786
|
+
}),
|
|
1787
|
+
signal: AbortSignal.timeout(10_000),
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
if (pollResponse.status === 429) {
|
|
1794
|
+
const retryAfter = Number(pollResponse.headers.get("retry-after"));
|
|
1795
|
+
if (Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
1796
|
+
await delay(retryAfter * 1_000);
|
|
1797
|
+
}
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
if (!pollResponse.ok) {
|
|
1801
|
+
throw new Error("Lore browser approval failed. Run connect --login again.");
|
|
1802
|
+
}
|
|
1803
|
+
const result = PollDeviceAuthorizationResponseSchema.safeParse(await pollResponse.json().catch(() => null));
|
|
1804
|
+
if (!result.success) {
|
|
1805
|
+
throw new Error("Lore returned an incompatible device authorization response.");
|
|
1806
|
+
}
|
|
1807
|
+
if (result.data.status === "pending") {
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (result.data.status === "approved") {
|
|
1811
|
+
return result.data.token;
|
|
1812
|
+
}
|
|
1813
|
+
if (result.data.status === "denied") {
|
|
1814
|
+
throw new Error("Lore access was denied in the dashboard.");
|
|
1815
|
+
}
|
|
1816
|
+
throw new Error("Lore browser approval expired. Run connect --login again.");
|
|
1817
|
+
}
|
|
1818
|
+
throw new Error("Lore browser approval expired. Run connect --login again.");
|
|
1819
|
+
}
|
|
1820
|
+
async function connectCommand(args) {
|
|
1821
|
+
const parsed = parseConnectArguments(args);
|
|
1822
|
+
if (parsed === null) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const paths = getLorePaths();
|
|
1826
|
+
const existing = await readConnectorConfig(paths);
|
|
1827
|
+
const hasProvidedToken = parsed.token !== undefined ||
|
|
1828
|
+
(process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
|
|
1829
|
+
(process.env.LORE_TOKEN?.trim() ?? "") !== "";
|
|
1830
|
+
const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
|
|
1831
|
+
...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
|
|
1832
|
+
...(parsed.dashboardUrl === undefined
|
|
1833
|
+
? {}
|
|
1834
|
+
: { dashboardUrl: parsed.dashboardUrl }),
|
|
1835
|
+
environment: process.env,
|
|
1836
|
+
existing,
|
|
1837
|
+
preferHostedDefault: parsed.login || hasProvidedToken,
|
|
1838
|
+
});
|
|
1839
|
+
const configured = parsed.login
|
|
1840
|
+
? undefined
|
|
1841
|
+
: configuredToken(parsed.token, existing);
|
|
1842
|
+
if (parsed.login && hasProvidedToken) {
|
|
1843
|
+
throw new Error("--login cannot be combined with an explicit or environment workspace token. Unset it or use the token directly.");
|
|
1844
|
+
}
|
|
1845
|
+
const token = parsed.login
|
|
1846
|
+
? await requestDeviceAuthorization({
|
|
1847
|
+
apiUrl,
|
|
1848
|
+
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
1849
|
+
agents: parsed.agents,
|
|
1850
|
+
openBrowser: parsed.openBrowser,
|
|
1851
|
+
})
|
|
1852
|
+
: configured;
|
|
1853
|
+
if (token === undefined || token.trim() === "") {
|
|
1854
|
+
throw new Error("Workspace token is required. Use --login, --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
|
|
1855
|
+
}
|
|
1856
|
+
const result = await connectWithCredential({
|
|
1857
|
+
apiUrl,
|
|
1858
|
+
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
1859
|
+
token,
|
|
1860
|
+
agents: parsed.agents,
|
|
1861
|
+
...(parsed.timeoutMs === undefined ? {} : { timeoutMs: parsed.timeoutMs }),
|
|
1862
|
+
});
|
|
1863
|
+
writeResult(result, parsed.json, `connected: ${result.agents.join(", ")}\napi_url: ${result.apiUrl}\nconfig: ${result.config}\n${result.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
|
|
1383
1864
|
}
|
|
1384
1865
|
async function queueCount(paths) {
|
|
1385
1866
|
try {
|
|
@@ -1462,6 +1943,7 @@ async function inspectRuntimeInstallation(required, paths) {
|
|
|
1462
1943
|
}
|
|
1463
1944
|
const runtimeFiles = {
|
|
1464
1945
|
"lore-hook.mjs": paths.runtime,
|
|
1946
|
+
"guard-evaluation.js": resolve(dirname(paths.runtime), "guard-evaluation.js"),
|
|
1465
1947
|
"repository.js": paths.runtimeRepository,
|
|
1466
1948
|
"reliability-store.js": paths.runtimeReliabilityStore,
|
|
1467
1949
|
"signed-snapshot.js": paths.runtimeSignedSnapshot,
|
|
@@ -1575,6 +2057,14 @@ async function getAgentStatus(agent, config, paths, runtime) {
|
|
|
1575
2057
|
units.push(...inspectInstallationUnits(document.value === undefined
|
|
1576
2058
|
? target.emptyValue()
|
|
1577
2059
|
: document.value, agent, paths, index, configPaths.length));
|
|
2060
|
+
if (agent === "cursor") {
|
|
2061
|
+
const mcp = await readJsonDocument(resolve(dirname(path), "mcp.json"));
|
|
2062
|
+
const value = isObject(mcp.value) && isObject(mcp.value.mcpServers) ? mcp.value.mcpServers[CURSOR_MCP_NAME] : undefined;
|
|
2063
|
+
const owned = Number(isLoreCursorMcp(value));
|
|
2064
|
+
installed += owned;
|
|
2065
|
+
units.push({ id: installationUnitId("mcp", index, configPaths.length),
|
|
2066
|
+
state: installationUnitState(owned, Number(exactConfiguration(value, cursorMcpServer(paths)))) });
|
|
2067
|
+
}
|
|
1578
2068
|
}
|
|
1579
2069
|
catch {
|
|
1580
2070
|
invalidConfiguration = true;
|
|
@@ -1665,6 +2155,7 @@ async function statusData(paths) {
|
|
|
1665
2155
|
reliability: reliability.inspection,
|
|
1666
2156
|
reliabilityError: reliability.error,
|
|
1667
2157
|
agents,
|
|
2158
|
+
externalSurfaces: EXTERNAL_SURFACES,
|
|
1668
2159
|
};
|
|
1669
2160
|
}
|
|
1670
2161
|
async function statusCommand(args) {
|
|
@@ -1676,10 +2167,13 @@ async function statusCommand(args) {
|
|
|
1676
2167
|
const agentLines = data.agents
|
|
1677
2168
|
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, installation ${agent.installation.state}, invocation ${agent.invocation.state}${agent.invocationError === null ? "" : ` (${agent.invocationError})`}, capture ${agent.capabilities.captureDurability}, guard ${agent.capabilities.guardBoundaryCoverage}/${agent.capabilities.enforcement}`)
|
|
1678
2169
|
.join("\n");
|
|
2170
|
+
const externalSurfaceLines = data.externalSurfaces
|
|
2171
|
+
.map((surface) => `${surface.surface}: repository template, locally inspectable no, capture ${surface.capabilities.captureDurability}, guard ${surface.capabilities.guardBoundaryCoverage}/${surface.capabilities.enforcement}`)
|
|
2172
|
+
.join("\n");
|
|
1679
2173
|
const metricRate = (value) => value === null || value === undefined
|
|
1680
2174
|
? "-"
|
|
1681
2175
|
: `${(value * 100).toFixed(1)}%`;
|
|
1682
|
-
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\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${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n`);
|
|
2176
|
+
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\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${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
|
|
1683
2177
|
}
|
|
1684
2178
|
async function disconnectCommand(args) {
|
|
1685
2179
|
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
@@ -1697,6 +2191,17 @@ async function disconnectCommand(args) {
|
|
|
1697
2191
|
]);
|
|
1698
2192
|
for (const installation of installations) {
|
|
1699
2193
|
const path = installation.configPath;
|
|
2194
|
+
if (installation.agent === "cursor") {
|
|
2195
|
+
const mcpPath = resolve(dirname(path), "mcp.json");
|
|
2196
|
+
const mcp = await readJsonDocument(mcpPath);
|
|
2197
|
+
if (mcp.exists) {
|
|
2198
|
+
const result = await writeMergedJson(mcpPath, mcp, removeLoreCursorMcp(mcp.value), now);
|
|
2199
|
+
if (result.changed)
|
|
2200
|
+
changedHookFiles.push(mcpPath);
|
|
2201
|
+
if (result.backup)
|
|
2202
|
+
backups.push(result.backup);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
1700
2205
|
const document = await readJsonDocument(path);
|
|
1701
2206
|
if (!document.exists) {
|
|
1702
2207
|
continue;
|
|
@@ -1717,6 +2222,7 @@ async function disconnectCommand(args) {
|
|
|
1717
2222
|
rm(paths.runtimeSignedSnapshot, { force: true }),
|
|
1718
2223
|
rm(paths.runtimeCanonicalJson, { force: true }),
|
|
1719
2224
|
rm(paths.runtimeContextFallback, { force: true }),
|
|
2225
|
+
rm(resolve(dirname(paths.runtime), "guard-evaluation.js"), { force: true }),
|
|
1720
2226
|
rm(paths.runtimeInvocationHealthWriter, { force: true }),
|
|
1721
2227
|
rm(paths.runtimeVersion, { force: true }),
|
|
1722
2228
|
rm(paths.runtimePackage, { force: true }),
|
|
@@ -1821,6 +2327,19 @@ async function apiChecks(config) {
|
|
|
1821
2327
|
}
|
|
1822
2328
|
return checks;
|
|
1823
2329
|
}
|
|
2330
|
+
async function verifyConnectedInstallation() {
|
|
2331
|
+
const paths = getLorePaths();
|
|
2332
|
+
const config = await readConnectorConfig(paths);
|
|
2333
|
+
if (config === null) {
|
|
2334
|
+
return false;
|
|
2335
|
+
}
|
|
2336
|
+
const status = await statusData(paths);
|
|
2337
|
+
if (status.configMode !== "600" ||
|
|
2338
|
+
status.agents.some((agent) => agent.configured && agent.installation.state !== "complete")) {
|
|
2339
|
+
return false;
|
|
2340
|
+
}
|
|
2341
|
+
return (await apiChecks(config)).every((check) => check.status !== "error");
|
|
2342
|
+
}
|
|
1824
2343
|
async function doctorCommand(args) {
|
|
1825
2344
|
const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
|
|
1826
2345
|
if (parsed === null) {
|
|
@@ -1899,6 +2418,13 @@ async function doctorCommand(args) {
|
|
|
1899
2418
|
: "invocation health is unavailable",
|
|
1900
2419
|
});
|
|
1901
2420
|
}
|
|
2421
|
+
for (const surface of status.externalSurfaces) {
|
|
2422
|
+
checks.push({
|
|
2423
|
+
name: `${surface.surface}-capability`,
|
|
2424
|
+
status: "ok",
|
|
2425
|
+
detail: "surface-specific repository configuration; local installation is not claimed",
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
1902
2428
|
checks.push({
|
|
1903
2429
|
name: "reliability-store",
|
|
1904
2430
|
status: status.reliabilityError !== null
|
|
@@ -1979,6 +2505,9 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
1979
2505
|
case "ask":
|
|
1980
2506
|
await runAskCommand(commandArgs, await readConnectorConfig(getLorePaths()));
|
|
1981
2507
|
return;
|
|
2508
|
+
case "remember":
|
|
2509
|
+
await runRememberCommand(commandArgs, await readConnectorConfig(getLorePaths()));
|
|
2510
|
+
return;
|
|
1982
2511
|
case "guard":
|
|
1983
2512
|
await runGuardCommand(commandArgs, await readConnectorConfig(getLorePaths()));
|
|
1984
2513
|
return;
|
|
@@ -2016,7 +2545,19 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
2016
2545
|
await doctorCommand(commandArgs);
|
|
2017
2546
|
return;
|
|
2018
2547
|
case "self-host":
|
|
2019
|
-
await runSelfHostCommand(commandArgs
|
|
2548
|
+
await runSelfHostCommand(commandArgs, process.env, {
|
|
2549
|
+
connect: async (request) => {
|
|
2550
|
+
const connected = await connectWithCredential({
|
|
2551
|
+
apiUrl: request.apiUrl,
|
|
2552
|
+
token: request.token,
|
|
2553
|
+
agents: request.agents,
|
|
2554
|
+
});
|
|
2555
|
+
return {
|
|
2556
|
+
agents: connected.agents,
|
|
2557
|
+
verified: await verifyConnectedInstallation(),
|
|
2558
|
+
};
|
|
2559
|
+
},
|
|
2560
|
+
});
|
|
2020
2561
|
return;
|
|
2021
2562
|
case "demo":
|
|
2022
2563
|
await runDemoCommand(commandArgs, await readConnectorConfig(getLorePaths()));
|
|
@@ -2030,6 +2571,9 @@ export async function runCli(args = process.argv.slice(2)) {
|
|
|
2030
2571
|
case "hook":
|
|
2031
2572
|
await runHook(commandArgs);
|
|
2032
2573
|
return;
|
|
2574
|
+
case "cursor-mcp":
|
|
2575
|
+
await (await import("./cursor-mcp.js")).runCursorMcpServer();
|
|
2576
|
+
return;
|
|
2033
2577
|
case "internal-health-write": {
|
|
2034
2578
|
const encoded = commandArgs[0] ?? (await readInvocationHealthWriteInput());
|
|
2035
2579
|
await runInvocationHealthWrite(encoded);
|