@lore-co/cli 0.1.10 → 0.1.12
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 +41 -22
- package/dist/cli.d.ts +18 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +495 -82
- package/dist/cli.js.map +1 -1
- package/dist/demo.d.ts +2 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/demo.js.map +1 -1
- package/dist/devin.d.ts.map +1 -1
- package/dist/devin.js +4 -3
- package/dist/devin.js.map +1 -1
- package/dist/generated-assets.d.ts +4 -4
- package/dist/generated-assets.js +4 -4
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +11 -7
- package/dist/github.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +15 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +167 -26
- package/dist/runtime.js.map +1 -1
- package/dist/self-host.d.ts +1 -1
- package/dist/self-host.js +2 -2
- package/dist/update.js +3 -3
- package/package.json +7 -4
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { delimiter, dirname, resolve } from "node:path";
|
|
6
6
|
import { homedir, platform } from "node:os";
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { WorkspaceIdentityResponseSchema, } from "@lore-co/core";
|
|
8
|
+
import { NATIVE_CODING_AGENT_NAMES, WorkspaceIdentityResponseSchema, isNativeCodingAgent, } from "@lore-co/core";
|
|
9
9
|
import { runHook, } from "./runtime.js";
|
|
10
10
|
import { connectGithub, runGithubCommand } from "./github.js";
|
|
11
11
|
import { runDevinCommand } from "./devin.js";
|
|
@@ -18,8 +18,17 @@ const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
|
18
18
|
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
19
19
|
export const DEFAULT_HOSTED_API_URL = "https://api.uselore.co";
|
|
20
20
|
export const DEFAULT_HOSTED_DASHBOARD_URL = "https://uselore.co";
|
|
21
|
-
const CONFIGURED_AGENT_NAMES =
|
|
21
|
+
const CONFIGURED_AGENT_NAMES = NATIVE_CODING_AGENT_NAMES;
|
|
22
22
|
const HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionEnd"];
|
|
23
|
+
const CURSOR_HOOK_EVENTS = [
|
|
24
|
+
"beforeSubmitPrompt",
|
|
25
|
+
"afterAgentResponse",
|
|
26
|
+
"sessionEnd",
|
|
27
|
+
];
|
|
28
|
+
const POLYTOKEN_HOOK_EVENTS = [
|
|
29
|
+
"pre_user_prompt",
|
|
30
|
+
"post_model_turn",
|
|
31
|
+
];
|
|
23
32
|
const ROOT_HELP = `lore
|
|
24
33
|
Connect local coding agents to Lore shared engineering memory.
|
|
25
34
|
|
|
@@ -66,7 +75,7 @@ Options:
|
|
|
66
75
|
--url <url> Lore API base URL (default: https://api.uselore.co)
|
|
67
76
|
--dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
|
|
68
77
|
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
69
|
-
--agent <name> claude, codex, or
|
|
78
|
+
--agent <name> claude, codex, cursor, opencode, polytoken, or t3code; repeat to override auto-detection
|
|
70
79
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
71
80
|
--json Print machine-readable output
|
|
72
81
|
--help Show this command's help
|
|
@@ -74,6 +83,7 @@ Options:
|
|
|
74
83
|
Examples:
|
|
75
84
|
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
|
|
76
85
|
lore connect --url http://localhost:3004 --token dev-token --agent codex
|
|
86
|
+
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
|
|
77
87
|
`;
|
|
78
88
|
const STATUS_HELP = `lore status
|
|
79
89
|
Show whether Lore is configured and each native integration is installed.
|
|
@@ -112,9 +122,14 @@ function isObject(value) {
|
|
|
112
122
|
function cloneObject(value) {
|
|
113
123
|
return structuredClone(value);
|
|
114
124
|
}
|
|
115
|
-
export function getLorePaths(home) {
|
|
116
|
-
const resolvedHome = resolve(home ??
|
|
125
|
+
export function getLorePaths(home, environment = process.env) {
|
|
126
|
+
const resolvedHome = resolve(home ?? environment.HOME ?? homedir());
|
|
117
127
|
const loreDirectory = resolve(resolvedHome, ".lore");
|
|
128
|
+
const xdgConfigHome = environment.XDG_CONFIG_HOME?.trim() === undefined ||
|
|
129
|
+
environment.XDG_CONFIG_HOME.trim() === ""
|
|
130
|
+
? resolve(resolvedHome, ".config")
|
|
131
|
+
: resolve(environment.XDG_CONFIG_HOME);
|
|
132
|
+
const t3Home = resolve(environment.T3CODE_HOME?.trim() || resolve(resolvedHome, ".t3"));
|
|
118
133
|
return {
|
|
119
134
|
home: resolvedHome,
|
|
120
135
|
loreDirectory,
|
|
@@ -126,23 +141,30 @@ export function getLorePaths(home) {
|
|
|
126
141
|
queue: resolve(loreDirectory, "queue"),
|
|
127
142
|
codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
|
|
128
143
|
claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
|
|
144
|
+
cursorHooks: resolve(resolvedHome, ".cursor", "hooks.json"),
|
|
145
|
+
cursorUserData: platform() === "darwin"
|
|
146
|
+
? resolve(resolvedHome, "Library", "Application Support", "Cursor")
|
|
147
|
+
: resolve(resolvedHome, ".config", "Cursor"),
|
|
129
148
|
openCodeConfig: resolve(resolvedHome, ".config", "opencode", "opencode.json"),
|
|
149
|
+
polytokenHooks: resolve(xdgConfigHome, "polytoken", "hooks.json"),
|
|
150
|
+
t3Settings: resolve(t3Home, "userdata", "settings.json"),
|
|
130
151
|
};
|
|
131
152
|
}
|
|
132
153
|
function shellQuote(value) {
|
|
133
154
|
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
134
155
|
}
|
|
135
156
|
function isConfiguredAgent(value) {
|
|
136
|
-
return
|
|
157
|
+
return typeof value === "string" && isNativeCodingAgent(value);
|
|
137
158
|
}
|
|
138
|
-
function
|
|
139
|
-
return
|
|
159
|
+
function isConnectAgent(value) {
|
|
160
|
+
return isConfiguredAgent(value) || value === "t3code";
|
|
140
161
|
}
|
|
141
162
|
function hookCommand(agent, paths) {
|
|
163
|
+
const loreHome = `LORE_HOME=${shellQuote(paths.home)}`;
|
|
142
164
|
if (IS_STANDALONE_BINARY) {
|
|
143
|
-
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
165
|
+
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
144
166
|
}
|
|
145
|
-
return
|
|
167
|
+
return `env ${loreHome} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
146
168
|
}
|
|
147
169
|
function isLoreHook(value) {
|
|
148
170
|
if (!isObject(value) || value.type !== "command") {
|
|
@@ -242,6 +264,113 @@ export function countLoreHooks(input) {
|
|
|
242
264
|
}
|
|
243
265
|
return count;
|
|
244
266
|
}
|
|
267
|
+
function cursorEventHandler(event, paths) {
|
|
268
|
+
return {
|
|
269
|
+
type: "command",
|
|
270
|
+
command: hookCommand("cursor", paths),
|
|
271
|
+
timeout: event === "sessionEnd" ? 2 : event === "afterAgentResponse" ? 3 : 25,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function stripLoreFromCursorEvent(value) {
|
|
275
|
+
return Array.isArray(value) ? value.filter((hook) => !isLoreHook(hook)) : [];
|
|
276
|
+
}
|
|
277
|
+
export function mergeLoreCursorHooks(input, paths) {
|
|
278
|
+
const result = cloneObject(input);
|
|
279
|
+
if (result.version !== undefined && result.version !== 1) {
|
|
280
|
+
throw new Error('Cursor configuration field "version" must be 1');
|
|
281
|
+
}
|
|
282
|
+
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
283
|
+
throw new Error('Cursor configuration field "hooks" must be a JSON object');
|
|
284
|
+
}
|
|
285
|
+
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
286
|
+
for (const event of CURSOR_HOOK_EVENTS) {
|
|
287
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
288
|
+
throw new Error(`Cursor hook event "${event}" must be a JSON array`);
|
|
289
|
+
}
|
|
290
|
+
hooks[event] = [
|
|
291
|
+
...stripLoreFromCursorEvent(hooks[event]),
|
|
292
|
+
cursorEventHandler(event, paths),
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
result.version = 1;
|
|
296
|
+
result.hooks = hooks;
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
export function removeLoreCursorHooks(input) {
|
|
300
|
+
const result = cloneObject(input);
|
|
301
|
+
if (!isObject(result.hooks)) {
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
const hooks = { ...result.hooks };
|
|
305
|
+
for (const event of CURSOR_HOOK_EVENTS) {
|
|
306
|
+
if (!Array.isArray(hooks[event])) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const remaining = stripLoreFromCursorEvent(hooks[event]);
|
|
310
|
+
if (remaining.length === 0) {
|
|
311
|
+
delete hooks[event];
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
hooks[event] = remaining;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (Object.keys(hooks).length === 0) {
|
|
318
|
+
delete result.hooks;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
result.hooks = hooks;
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
export function countLoreCursorHooks(input) {
|
|
326
|
+
if (!isObject(input.hooks)) {
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
return CURSOR_HOOK_EVENTS.reduce((count, event) => {
|
|
330
|
+
const hooks = input.hooks;
|
|
331
|
+
if (!isObject(hooks) || !Array.isArray(hooks[event])) {
|
|
332
|
+
return count;
|
|
333
|
+
}
|
|
334
|
+
return count + hooks[event].filter(isLoreHook).length;
|
|
335
|
+
}, 0);
|
|
336
|
+
}
|
|
337
|
+
function polytokenEventHandler(event, paths) {
|
|
338
|
+
return {
|
|
339
|
+
name: `lore-${event.replaceAll("_", "-")}`,
|
|
340
|
+
event,
|
|
341
|
+
handler: { bash: hookCommand("polytoken", paths) },
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function isLorePolytokenHook(value) {
|
|
345
|
+
if (!isObject(value) || !isObject(value.handler)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
const command = value.handler.bash;
|
|
349
|
+
return (typeof command === "string" &&
|
|
350
|
+
(/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
|
|
351
|
+
command.includes("/.lore/bin/lore-hook.mjs")));
|
|
352
|
+
}
|
|
353
|
+
export function mergeLorePolytokenHooks(input, paths) {
|
|
354
|
+
if (!Array.isArray(input)) {
|
|
355
|
+
throw new Error("Polytoken hooks must contain a JSON array");
|
|
356
|
+
}
|
|
357
|
+
const retained = input.filter((hook) => !isLorePolytokenHook(hook));
|
|
358
|
+
return [
|
|
359
|
+
...structuredClone(retained),
|
|
360
|
+
...POLYTOKEN_HOOK_EVENTS.map((event) => polytokenEventHandler(event, paths)),
|
|
361
|
+
];
|
|
362
|
+
}
|
|
363
|
+
export function removeLorePolytokenHooks(input) {
|
|
364
|
+
if (!Array.isArray(input)) {
|
|
365
|
+
throw new Error("Polytoken hooks must contain a JSON array");
|
|
366
|
+
}
|
|
367
|
+
return structuredClone(input.filter((hook) => !isLorePolytokenHook(hook)));
|
|
368
|
+
}
|
|
369
|
+
export function countLorePolytokenHooks(input) {
|
|
370
|
+
return Array.isArray(input)
|
|
371
|
+
? input.filter((hook) => isLorePolytokenHook(hook)).length
|
|
372
|
+
: 0;
|
|
373
|
+
}
|
|
245
374
|
function isLoreOpenCodePlugin(value) {
|
|
246
375
|
return (typeof value === "string" &&
|
|
247
376
|
/^@lore-co\/(?:opencode|opencode-plugin)(?:@[^\s]+)?$/u.test(value));
|
|
@@ -284,9 +413,6 @@ async function readJsonDocument(path) {
|
|
|
284
413
|
stat(path),
|
|
285
414
|
]);
|
|
286
415
|
const parsed = JSON.parse(raw);
|
|
287
|
-
if (!isObject(parsed)) {
|
|
288
|
-
throw new Error(`Expected a JSON object in ${path}`);
|
|
289
|
-
}
|
|
290
416
|
return {
|
|
291
417
|
exists: true,
|
|
292
418
|
value: parsed,
|
|
@@ -298,7 +424,7 @@ async function readJsonDocument(path) {
|
|
|
298
424
|
? error.code
|
|
299
425
|
: undefined;
|
|
300
426
|
if (code === "ENOENT") {
|
|
301
|
-
return { exists: false, value:
|
|
427
|
+
return { exists: false, value: undefined, mode: 0o600 };
|
|
302
428
|
}
|
|
303
429
|
if (error instanceof SyntaxError) {
|
|
304
430
|
throw new Error(`Refusing to overwrite invalid JSON in ${path}`, {
|
|
@@ -347,6 +473,25 @@ function parseConnectorConfig(value) {
|
|
|
347
473
|
return null;
|
|
348
474
|
}
|
|
349
475
|
const agents = value.agents.filter((agent) => isConfiguredAgent(agent));
|
|
476
|
+
const parsedAgentConfigPaths = {};
|
|
477
|
+
if (isObject(value.agentConfigPaths)) {
|
|
478
|
+
for (const agent of CONFIGURED_AGENT_NAMES) {
|
|
479
|
+
const paths = value.agentConfigPaths[agent];
|
|
480
|
+
if (!Array.isArray(paths)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const valid = paths.flatMap((path) => {
|
|
484
|
+
const normalized = typeof path === "string" ? path.trim() : "";
|
|
485
|
+
return normalized === "" ? [] : [resolve(normalized)];
|
|
486
|
+
});
|
|
487
|
+
if (valid.length > 0) {
|
|
488
|
+
parsedAgentConfigPaths[agent] = [...new Set(valid)];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const agentConfigPaths = Object.keys(parsedAgentConfigPaths).length === 0
|
|
493
|
+
? undefined
|
|
494
|
+
: parsedAgentConfigPaths;
|
|
350
495
|
const timeoutMs = typeof value.timeoutMs === "number" &&
|
|
351
496
|
Number.isInteger(value.timeoutMs) &&
|
|
352
497
|
value.timeoutMs >= 250 &&
|
|
@@ -362,6 +507,10 @@ function parseConnectorConfig(value) {
|
|
|
362
507
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
363
508
|
token: value.token,
|
|
364
509
|
agents,
|
|
510
|
+
...(agentConfigPaths === undefined ||
|
|
511
|
+
Object.keys(agentConfigPaths).length === 0
|
|
512
|
+
? {}
|
|
513
|
+
: { agentConfigPaths }),
|
|
365
514
|
connectedAt: value.connectedAt,
|
|
366
515
|
timeoutMs,
|
|
367
516
|
};
|
|
@@ -518,20 +667,266 @@ async function pathExists(path) {
|
|
|
518
667
|
return false;
|
|
519
668
|
}
|
|
520
669
|
}
|
|
670
|
+
async function cursorAvailable(paths) {
|
|
671
|
+
return ((await commandExists("cursor")) ||
|
|
672
|
+
(await pathExists(paths.cursorUserData)));
|
|
673
|
+
}
|
|
674
|
+
function objectConfiguration(input, agent) {
|
|
675
|
+
if (!isObject(input)) {
|
|
676
|
+
throw new Error(`${agent} configuration must contain a JSON object`);
|
|
677
|
+
}
|
|
678
|
+
return input;
|
|
679
|
+
}
|
|
680
|
+
const AGENT_TARGETS = {
|
|
681
|
+
claude: {
|
|
682
|
+
integration: "hooks",
|
|
683
|
+
expected: HOOK_EVENTS.length,
|
|
684
|
+
runtimeRequired: true,
|
|
685
|
+
emptyValue: () => ({}),
|
|
686
|
+
configPath: (paths) => paths.claudeSettings,
|
|
687
|
+
detect: async () => commandExists("claude"),
|
|
688
|
+
executable: async () => commandExists("claude"),
|
|
689
|
+
merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths),
|
|
690
|
+
remove: (input) => removeLoreHooks(objectConfiguration(input, "claude")),
|
|
691
|
+
count: (input) => countLoreHooks(objectConfiguration(input, "claude")),
|
|
692
|
+
},
|
|
693
|
+
codex: {
|
|
694
|
+
integration: "hooks",
|
|
695
|
+
expected: HOOK_EVENTS.length,
|
|
696
|
+
runtimeRequired: true,
|
|
697
|
+
emptyValue: () => ({}),
|
|
698
|
+
configPath: (paths) => paths.codexHooks,
|
|
699
|
+
detect: async () => commandExists("codex"),
|
|
700
|
+
executable: async () => commandExists("codex"),
|
|
701
|
+
merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths),
|
|
702
|
+
remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
|
|
703
|
+
count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
|
|
704
|
+
},
|
|
705
|
+
cursor: {
|
|
706
|
+
integration: "hooks",
|
|
707
|
+
expected: CURSOR_HOOK_EVENTS.length,
|
|
708
|
+
runtimeRequired: true,
|
|
709
|
+
emptyValue: () => ({}),
|
|
710
|
+
configPath: (paths) => paths.cursorHooks,
|
|
711
|
+
detect: async (paths) => (await cursorAvailable(paths)) || pathExists(paths.cursorHooks),
|
|
712
|
+
executable: cursorAvailable,
|
|
713
|
+
merge: (input, paths) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths),
|
|
714
|
+
remove: (input) => removeLoreCursorHooks(objectConfiguration(input, "cursor")),
|
|
715
|
+
count: (input) => countLoreCursorHooks(objectConfiguration(input, "cursor")),
|
|
716
|
+
},
|
|
717
|
+
opencode: {
|
|
718
|
+
integration: "plugin",
|
|
719
|
+
expected: 1,
|
|
720
|
+
runtimeRequired: false,
|
|
721
|
+
emptyValue: () => ({}),
|
|
722
|
+
configPath: (paths) => paths.openCodeConfig,
|
|
723
|
+
detect: async (paths) => (await commandExists("opencode")) || pathExists(paths.openCodeConfig),
|
|
724
|
+
executable: async () => commandExists("opencode"),
|
|
725
|
+
merge: (input) => mergeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
|
|
726
|
+
remove: (input) => removeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
|
|
727
|
+
count: (input) => countLoreOpenCodePlugins(objectConfiguration(input, "opencode")),
|
|
728
|
+
},
|
|
729
|
+
polytoken: {
|
|
730
|
+
integration: "hooks",
|
|
731
|
+
expected: POLYTOKEN_HOOK_EVENTS.length,
|
|
732
|
+
runtimeRequired: true,
|
|
733
|
+
emptyValue: () => [],
|
|
734
|
+
configPath: (paths) => paths.polytokenHooks,
|
|
735
|
+
detect: async (paths) => (await commandExists("polytoken")) || pathExists(paths.polytokenHooks),
|
|
736
|
+
executable: async () => commandExists("polytoken"),
|
|
737
|
+
merge: mergeLorePolytokenHooks,
|
|
738
|
+
remove: removeLorePolytokenHooks,
|
|
739
|
+
count: countLorePolytokenHooks,
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
function targetFor(agent) {
|
|
743
|
+
return AGENT_TARGETS[agent];
|
|
744
|
+
}
|
|
521
745
|
async function detectAgents(paths) {
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
746
|
+
const detected = await Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => ({
|
|
747
|
+
agent,
|
|
748
|
+
detected: await targetFor(agent).detect(paths),
|
|
749
|
+
})));
|
|
750
|
+
return detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
|
|
751
|
+
}
|
|
752
|
+
function installationKey(installation) {
|
|
753
|
+
return `${installation.agent}\0${resolve(installation.configPath)}`;
|
|
754
|
+
}
|
|
755
|
+
function uniqueInstallations(installations) {
|
|
756
|
+
const byKey = new Map();
|
|
757
|
+
for (const installation of installations) {
|
|
758
|
+
const normalized = {
|
|
759
|
+
agent: installation.agent,
|
|
760
|
+
configPath: resolve(installation.configPath),
|
|
761
|
+
};
|
|
762
|
+
byKey.set(installationKey(normalized), normalized);
|
|
763
|
+
}
|
|
764
|
+
return [...byKey.values()].sort((left, right) => installationKey(left).localeCompare(installationKey(right)));
|
|
765
|
+
}
|
|
766
|
+
function defaultInstallation(agent, paths) {
|
|
767
|
+
return { agent, configPath: targetFor(agent).configPath(paths) };
|
|
768
|
+
}
|
|
769
|
+
function configuredInstallations(config, paths) {
|
|
770
|
+
return (config?.agents.flatMap((agent) => {
|
|
771
|
+
const configuredPaths = config.agentConfigPaths?.[agent];
|
|
772
|
+
return configuredPaths === undefined
|
|
773
|
+
? [defaultInstallation(agent, paths)]
|
|
774
|
+
: configuredPaths.map((configPath) => ({ agent, configPath }));
|
|
775
|
+
}) ?? []);
|
|
776
|
+
}
|
|
777
|
+
function nestedObject(input, key) {
|
|
778
|
+
return isObject(input[key]) ? input[key] : {};
|
|
779
|
+
}
|
|
780
|
+
function firstString(...values) {
|
|
781
|
+
for (const value of values) {
|
|
782
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
783
|
+
return value.trim();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return undefined;
|
|
787
|
+
}
|
|
788
|
+
function t3ProviderAgent(key, input) {
|
|
789
|
+
const config = nestedObject(input, "config");
|
|
790
|
+
const signal = [
|
|
791
|
+
key,
|
|
792
|
+
firstString(input.driver),
|
|
793
|
+
firstString(input.provider),
|
|
794
|
+
firstString(input.type),
|
|
795
|
+
firstString(config.driver),
|
|
796
|
+
firstString(config.provider),
|
|
797
|
+
firstString(config.type),
|
|
798
|
+
]
|
|
799
|
+
.filter((value) => value !== undefined)
|
|
800
|
+
.join(" ")
|
|
801
|
+
.toLocaleLowerCase();
|
|
802
|
+
if (signal.includes("opencode")) {
|
|
803
|
+
return "opencode";
|
|
804
|
+
}
|
|
805
|
+
if (signal.includes("cursor")) {
|
|
806
|
+
return "cursor";
|
|
807
|
+
}
|
|
808
|
+
if (signal.includes("claude")) {
|
|
809
|
+
return "claude";
|
|
810
|
+
}
|
|
811
|
+
if (signal.includes("codex")) {
|
|
812
|
+
return "codex";
|
|
813
|
+
}
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
function t3ProviderConfigPath(agent, input, paths) {
|
|
817
|
+
const config = nestedObject(input, "config");
|
|
818
|
+
const environment = nestedObject(input, "environment");
|
|
819
|
+
const configEnvironment = nestedObject(config, "environment");
|
|
820
|
+
const home = nestedObject(input, "home");
|
|
821
|
+
const configHome = nestedObject(config, "home");
|
|
822
|
+
const environmentValue = (key) => environment[key] ?? configEnvironment[key];
|
|
823
|
+
const homePath = firstString(input.homePath, config.homePath, home.path, configHome.path);
|
|
824
|
+
if (agent === "codex") {
|
|
825
|
+
const root = firstString(homePath, environmentValue("CODEX_HOME"));
|
|
826
|
+
return root === undefined
|
|
827
|
+
? paths.codexHooks
|
|
828
|
+
: resolve(root, "hooks.json");
|
|
829
|
+
}
|
|
830
|
+
if (agent === "claude") {
|
|
831
|
+
const root = firstString(homePath, environmentValue("CLAUDE_CONFIG_DIR"));
|
|
832
|
+
return root === undefined
|
|
833
|
+
? paths.claudeSettings
|
|
834
|
+
: resolve(root, "settings.json");
|
|
835
|
+
}
|
|
836
|
+
if (agent === "cursor") {
|
|
837
|
+
return paths.cursorHooks;
|
|
838
|
+
}
|
|
839
|
+
const serverUrl = firstString(input.serverUrl, config.serverUrl);
|
|
840
|
+
if (serverUrl !== undefined) {
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
const explicitConfig = firstString(input.configPath, config.configPath, environmentValue("OPENCODE_CONFIG"));
|
|
844
|
+
if (explicitConfig !== undefined) {
|
|
845
|
+
return resolve(explicitConfig);
|
|
846
|
+
}
|
|
847
|
+
const configDirectory = firstString(input.configDirectory, config.configDirectory, environmentValue("OPENCODE_CONFIG_DIR"));
|
|
848
|
+
if (configDirectory !== undefined) {
|
|
849
|
+
return resolve(configDirectory, "opencode.json");
|
|
850
|
+
}
|
|
851
|
+
const xdgConfigHome = firstString(environmentValue("XDG_CONFIG_HOME"));
|
|
852
|
+
return xdgConfigHome === undefined
|
|
853
|
+
? paths.openCodeConfig
|
|
854
|
+
: resolve(xdgConfigHome, "opencode", "opencode.json");
|
|
855
|
+
}
|
|
856
|
+
function discoverT3CodeProviders(settings, paths) {
|
|
857
|
+
if (!isObject(settings)) {
|
|
858
|
+
throw new Error("T3 Code settings must contain a JSON object");
|
|
859
|
+
}
|
|
860
|
+
const records = [];
|
|
861
|
+
for (const field of ["providerInstances", "providers"]) {
|
|
862
|
+
const collection = settings[field];
|
|
863
|
+
if (!isObject(collection)) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
for (const [key, value] of Object.entries(collection)) {
|
|
867
|
+
if (isObject(value)) {
|
|
868
|
+
records.push([key, value]);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const warnings = [];
|
|
873
|
+
const installations = uniqueInstallations(records.flatMap(([key, value]) => {
|
|
874
|
+
if (value.enabled === false || value.disabled === true) {
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
const agent = t3ProviderAgent(key, value);
|
|
878
|
+
if (agent === undefined) {
|
|
879
|
+
return [];
|
|
880
|
+
}
|
|
881
|
+
const configPath = t3ProviderConfigPath(agent, value, paths);
|
|
882
|
+
if (agent === "opencode" && configPath === undefined) {
|
|
883
|
+
warnings.push(`T3 Code provider "${key}" uses an external OpenCode server; install and configure Lore on that server.`);
|
|
884
|
+
}
|
|
885
|
+
return configPath === undefined ? [] : [{ agent, configPath }];
|
|
886
|
+
}));
|
|
887
|
+
return { installations, warnings };
|
|
888
|
+
}
|
|
889
|
+
export function t3CodeProviderInstallations(settings, paths) {
|
|
890
|
+
return discoverT3CodeProviders(settings, paths).installations;
|
|
891
|
+
}
|
|
892
|
+
async function readT3CodeProviderInstallations(paths) {
|
|
893
|
+
try {
|
|
894
|
+
return discoverT3CodeProviders(JSON.parse(await readFile(paths.t3Settings, "utf8")), paths);
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
898
|
+
? error.code
|
|
899
|
+
: undefined;
|
|
900
|
+
if (code === "ENOENT") {
|
|
901
|
+
return { installations: [], warnings: [] };
|
|
902
|
+
}
|
|
903
|
+
if (error instanceof SyntaxError) {
|
|
904
|
+
throw new Error(`Refusing to use invalid T3 Code settings in ${paths.t3Settings}`, { cause: error });
|
|
905
|
+
}
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async function expandConnectInstallations(requested, paths) {
|
|
910
|
+
const direct = requested.flatMap((agent) => agent === "t3code" ? [] : [defaultInstallation(agent, paths)]);
|
|
911
|
+
if (!requested.includes("t3code")) {
|
|
912
|
+
return { installations: direct, warnings: [] };
|
|
913
|
+
}
|
|
914
|
+
const configured = await readT3CodeProviderInstallations(paths);
|
|
915
|
+
const t3Providers = ["claude", "codex", "cursor", "opencode"];
|
|
916
|
+
const detected = await Promise.all(t3Providers.map(async (agent) => ({
|
|
917
|
+
agent,
|
|
918
|
+
available: await targetFor(agent).detect(paths),
|
|
919
|
+
})));
|
|
920
|
+
const routed = detected.flatMap(({ agent, available }) => available ? [defaultInstallation(agent, paths)] : []);
|
|
921
|
+
const installations = uniqueInstallations([
|
|
922
|
+
...direct,
|
|
923
|
+
...configured.installations,
|
|
924
|
+
...routed,
|
|
527
925
|
]);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
? ["opencode"]
|
|
533
|
-
: []),
|
|
534
|
-
];
|
|
926
|
+
if (installations.length === 0) {
|
|
927
|
+
throw new Error(`T3 Code delegates to provider CLIs, but no supported local Claude, Codex, Cursor, or OpenCode provider was found in ${paths.t3Settings}. Install a provider, set T3CODE_HOME, or connect it explicitly.`);
|
|
928
|
+
}
|
|
929
|
+
return { installations, warnings: configured.warnings };
|
|
535
930
|
}
|
|
536
931
|
async function installRuntime(paths) {
|
|
537
932
|
if (IS_STANDALONE_BINARY) {
|
|
@@ -553,28 +948,19 @@ async function installRuntime(paths) {
|
|
|
553
948
|
atomicWrite(paths.runtimePackage, '{"type":"module"}\n', 0o600),
|
|
554
949
|
]);
|
|
555
950
|
}
|
|
556
|
-
function hookPath(agent, paths) {
|
|
557
|
-
return agent === "codex" ? paths.codexHooks : paths.claudeSettings;
|
|
558
|
-
}
|
|
559
951
|
function agentConfigPath(agent, paths) {
|
|
560
|
-
return agent
|
|
561
|
-
? paths.openCodeConfig
|
|
562
|
-
: hookPath(agent, paths);
|
|
952
|
+
return targetFor(agent).configPath(paths);
|
|
563
953
|
}
|
|
564
954
|
function mergeAgentConfig(input, agent, paths) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
: mergeLoreHooks(input, agent, paths);
|
|
955
|
+
const target = targetFor(agent);
|
|
956
|
+
return target.merge(input === undefined ? target.emptyValue() : input, paths);
|
|
568
957
|
}
|
|
569
958
|
function removeAgentConfig(input, agent) {
|
|
570
|
-
return agent
|
|
571
|
-
? removeLoreOpenCodePlugin(input)
|
|
572
|
-
: removeLoreHooks(input);
|
|
959
|
+
return targetFor(agent).remove(input);
|
|
573
960
|
}
|
|
574
961
|
function countAgentIntegrations(input, agent) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
: countLoreHooks(input);
|
|
962
|
+
const target = targetFor(agent);
|
|
963
|
+
return target.count(input === undefined ? target.emptyValue() : input);
|
|
578
964
|
}
|
|
579
965
|
function parseInteger(value, flag) {
|
|
580
966
|
const parsed = Number(value);
|
|
@@ -627,11 +1013,11 @@ function parseConnectArguments(args) {
|
|
|
627
1013
|
}
|
|
628
1014
|
parsed.timeoutMs = timeoutMs;
|
|
629
1015
|
}
|
|
630
|
-
else if (
|
|
1016
|
+
else if (isConnectAgent(value)) {
|
|
631
1017
|
parsed.agents.push(value);
|
|
632
1018
|
}
|
|
633
1019
|
else {
|
|
634
|
-
throw new Error("--agent must be claude, codex, or
|
|
1020
|
+
throw new Error("--agent must be claude, codex, cursor, opencode, polytoken, or t3code");
|
|
635
1021
|
}
|
|
636
1022
|
}
|
|
637
1023
|
return parsed;
|
|
@@ -682,50 +1068,63 @@ async function connectCommand(args) {
|
|
|
682
1068
|
}
|
|
683
1069
|
const timeoutMs = parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500;
|
|
684
1070
|
const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
|
|
685
|
-
const
|
|
1071
|
+
const requestedExpansion = parsed.agents.length === 0
|
|
1072
|
+
? { installations: [], warnings: [] }
|
|
1073
|
+
: await expandConnectInstallations(parsed.agents, paths);
|
|
1074
|
+
const detectedAgents = parsed.agents.length === 0 ? await detectAgents(paths) : [];
|
|
1075
|
+
const installations = uniqueInstallations([
|
|
1076
|
+
...configuredInstallations(existing, paths),
|
|
1077
|
+
...requestedExpansion.installations,
|
|
1078
|
+
...detectedAgents.map((agent) => defaultInstallation(agent, paths)),
|
|
1079
|
+
]);
|
|
686
1080
|
const agents = [
|
|
687
|
-
...new Set(
|
|
688
|
-
...(existing?.agents ?? []),
|
|
689
|
-
...parsed.agents,
|
|
690
|
-
...detected,
|
|
691
|
-
]),
|
|
1081
|
+
...new Set(installations.map((installation) => installation.agent)),
|
|
692
1082
|
].sort();
|
|
693
1083
|
if (agents.length === 0) {
|
|
694
|
-
throw new Error("No Claude, Codex,
|
|
1084
|
+
throw new Error("No Claude, Codex, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
|
|
695
1085
|
}
|
|
696
1086
|
const now = new Date();
|
|
697
1087
|
const documents = new Map();
|
|
698
1088
|
const mergedDocuments = new Map();
|
|
699
|
-
for (const
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
1089
|
+
for (const installation of installations) {
|
|
1090
|
+
const key = installationKey(installation);
|
|
1091
|
+
const document = await readJsonDocument(installation.configPath);
|
|
1092
|
+
documents.set(key, document);
|
|
1093
|
+
mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths));
|
|
703
1094
|
}
|
|
704
|
-
if (
|
|
1095
|
+
if (installations.some((installation) => targetFor(installation.agent).runtimeRequired)) {
|
|
705
1096
|
await installRuntime(paths);
|
|
706
1097
|
}
|
|
707
1098
|
const changedHookFiles = [];
|
|
708
1099
|
const backups = [];
|
|
709
|
-
for (const
|
|
710
|
-
const
|
|
711
|
-
const
|
|
1100
|
+
for (const installation of installations) {
|
|
1101
|
+
const key = installationKey(installation);
|
|
1102
|
+
const document = documents.get(key);
|
|
1103
|
+
const merged = mergedDocuments.get(key);
|
|
712
1104
|
if (document === undefined || merged === undefined) {
|
|
713
1105
|
continue;
|
|
714
1106
|
}
|
|
715
|
-
const result = await writeMergedJson(
|
|
1107
|
+
const result = await writeMergedJson(installation.configPath, document, merged, now);
|
|
716
1108
|
if (result.changed) {
|
|
717
|
-
changedHookFiles.push(
|
|
1109
|
+
changedHookFiles.push(installation.configPath);
|
|
718
1110
|
}
|
|
719
1111
|
if (result.backup !== undefined) {
|
|
720
1112
|
backups.push(result.backup);
|
|
721
1113
|
}
|
|
722
1114
|
}
|
|
1115
|
+
const agentConfigPaths = {};
|
|
1116
|
+
for (const agent of agents) {
|
|
1117
|
+
agentConfigPaths[agent] = installations
|
|
1118
|
+
.filter((installation) => installation.agent === agent)
|
|
1119
|
+
.map((installation) => installation.configPath);
|
|
1120
|
+
}
|
|
723
1121
|
const config = {
|
|
724
1122
|
version: 1,
|
|
725
1123
|
apiUrl,
|
|
726
1124
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
727
1125
|
token: token.trim(),
|
|
728
1126
|
agents,
|
|
1127
|
+
agentConfigPaths,
|
|
729
1128
|
connectedAt: existing?.connectedAt ?? now.toISOString(),
|
|
730
1129
|
timeoutMs,
|
|
731
1130
|
};
|
|
@@ -738,8 +1137,9 @@ async function connectCommand(args) {
|
|
|
738
1137
|
config: paths.config,
|
|
739
1138
|
changedHookFiles,
|
|
740
1139
|
backups,
|
|
1140
|
+
warnings: requestedExpansion.warnings,
|
|
741
1141
|
};
|
|
742
|
-
writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n`);
|
|
1142
|
+
writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n${requestedExpansion.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
|
|
743
1143
|
}
|
|
744
1144
|
async function queueCount(paths) {
|
|
745
1145
|
try {
|
|
@@ -750,23 +1150,33 @@ async function queueCount(paths) {
|
|
|
750
1150
|
}
|
|
751
1151
|
}
|
|
752
1152
|
async function getAgentStatus(agent, config, paths) {
|
|
753
|
-
const
|
|
1153
|
+
const target = targetFor(agent);
|
|
1154
|
+
const configuredPaths = config?.agentConfigPaths?.[agent];
|
|
1155
|
+
const configPaths = config?.agents.includes(agent) === true && configuredPaths !== undefined
|
|
1156
|
+
? configuredPaths
|
|
1157
|
+
: [agentConfigPath(agent, paths)];
|
|
754
1158
|
let installed = 0;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1159
|
+
let existing = 0;
|
|
1160
|
+
for (const path of configPaths) {
|
|
1161
|
+
try {
|
|
1162
|
+
const document = await readJsonDocument(path);
|
|
1163
|
+
installed += countAgentIntegrations(document.value, agent);
|
|
1164
|
+
existing += document.exists ? 1 : 0;
|
|
1165
|
+
}
|
|
1166
|
+
catch {
|
|
1167
|
+
// A malformed target config is reported as missing integration state.
|
|
1168
|
+
}
|
|
760
1169
|
}
|
|
761
1170
|
return {
|
|
762
1171
|
agent,
|
|
763
1172
|
configured: config?.agents.includes(agent) ?? false,
|
|
764
|
-
executable: await
|
|
765
|
-
configExists:
|
|
766
|
-
configFile:
|
|
767
|
-
|
|
1173
|
+
executable: await target.executable(paths),
|
|
1174
|
+
configExists: existing === configPaths.length,
|
|
1175
|
+
configFile: configPaths[0] ?? agentConfigPath(agent, paths),
|
|
1176
|
+
...(configPaths.length > 1 ? { configFiles: configPaths } : {}),
|
|
1177
|
+
integration: target.integration,
|
|
768
1178
|
installed,
|
|
769
|
-
expected:
|
|
1179
|
+
expected: target.expected * configPaths.length,
|
|
770
1180
|
};
|
|
771
1181
|
}
|
|
772
1182
|
async function statusData(paths) {
|
|
@@ -778,7 +1188,7 @@ async function statusData(paths) {
|
|
|
778
1188
|
catch {
|
|
779
1189
|
// Missing configuration is represented as disconnected.
|
|
780
1190
|
}
|
|
781
|
-
const runtimeRequired = config?.agents.some(
|
|
1191
|
+
const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
|
|
782
1192
|
const runtimeInstalledCheck = runtimeRequired
|
|
783
1193
|
? Promise.all(IS_STANDALONE_BINARY
|
|
784
1194
|
? [access(process.execPath, fsConstants.R_OK | fsConstants.X_OK)]
|
|
@@ -788,12 +1198,10 @@ async function statusData(paths) {
|
|
|
788
1198
|
access(paths.runtimePackage, fsConstants.R_OK),
|
|
789
1199
|
]).then(() => true, () => false)
|
|
790
1200
|
: Promise.resolve(false);
|
|
791
|
-
const [runtimeInstalled, queuedTurns,
|
|
1201
|
+
const [runtimeInstalled, queuedTurns, agents] = await Promise.all([
|
|
792
1202
|
runtimeInstalledCheck,
|
|
793
1203
|
queueCount(paths),
|
|
794
|
-
getAgentStatus(
|
|
795
|
-
getAgentStatus("codex", config, paths),
|
|
796
|
-
getAgentStatus("opencode", config, paths),
|
|
1204
|
+
Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths))),
|
|
797
1205
|
]);
|
|
798
1206
|
return {
|
|
799
1207
|
connected: config !== null,
|
|
@@ -803,7 +1211,7 @@ async function statusData(paths) {
|
|
|
803
1211
|
runtimeRequired,
|
|
804
1212
|
runtimeInstalled,
|
|
805
1213
|
queuedTurns,
|
|
806
|
-
agents
|
|
1214
|
+
agents,
|
|
807
1215
|
};
|
|
808
1216
|
}
|
|
809
1217
|
async function statusCommand(args) {
|
|
@@ -823,16 +1231,21 @@ async function disconnectCommand(args) {
|
|
|
823
1231
|
return;
|
|
824
1232
|
}
|
|
825
1233
|
const paths = getLorePaths();
|
|
1234
|
+
const config = await readConnectorConfig(paths);
|
|
826
1235
|
const now = new Date();
|
|
827
1236
|
const changedHookFiles = [];
|
|
828
1237
|
const backups = [];
|
|
829
|
-
|
|
830
|
-
|
|
1238
|
+
const installations = uniqueInstallations([
|
|
1239
|
+
...CONFIGURED_AGENT_NAMES.map((agent) => defaultInstallation(agent, paths)),
|
|
1240
|
+
...configuredInstallations(config, paths),
|
|
1241
|
+
]);
|
|
1242
|
+
for (const installation of installations) {
|
|
1243
|
+
const path = installation.configPath;
|
|
831
1244
|
const document = await readJsonDocument(path);
|
|
832
1245
|
if (!document.exists) {
|
|
833
1246
|
continue;
|
|
834
1247
|
}
|
|
835
|
-
const result = await writeMergedJson(path, document, removeAgentConfig(document.value, agent), now);
|
|
1248
|
+
const result = await writeMergedJson(path, document, removeAgentConfig(document.value, installation.agent), now);
|
|
836
1249
|
if (result.changed) {
|
|
837
1250
|
changedHookFiles.push(path);
|
|
838
1251
|
}
|
|
@@ -952,7 +1365,7 @@ async function doctorCommand(args) {
|
|
|
952
1365
|
checks.push({
|
|
953
1366
|
name: `${agent.agent}-executable`,
|
|
954
1367
|
status: agent.executable ? "ok" : "warning",
|
|
955
|
-
detail: agent.executable ? "
|
|
1368
|
+
detail: agent.executable ? "detected" : "not detected",
|
|
956
1369
|
});
|
|
957
1370
|
checks.push({
|
|
958
1371
|
name: `${agent.agent}-${agent.integration}`,
|