@lore-co/cli 0.1.9 → 0.1.11
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 +45 -23
- package/dist/cli.d.ts +27 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +542 -101
- 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.d.ts.map +1 -1
- package/dist/generated-assets.js +4 -4
- package/dist/generated-assets.js.map +1 -1
- 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.d.ts.map +1 -1
- package/dist/self-host.js +2 -2
- package/dist/self-host.js.map +1 -1
- package/dist/update.js +3 -3
- package/dist/update.js.map +1 -1
- 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";
|
|
@@ -16,8 +16,19 @@ import { runSelfHostCommand } from "./self-host.js";
|
|
|
16
16
|
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
17
17
|
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
18
18
|
export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
|
|
19
|
-
const
|
|
19
|
+
export const DEFAULT_HOSTED_API_URL = "https://api.uselore.co";
|
|
20
|
+
export const DEFAULT_HOSTED_DASHBOARD_URL = "https://uselore.co";
|
|
21
|
+
const CONFIGURED_AGENT_NAMES = NATIVE_CODING_AGENT_NAMES;
|
|
20
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
|
+
];
|
|
21
32
|
const ROOT_HELP = `lore
|
|
22
33
|
Connect local coding agents to Lore shared engineering memory.
|
|
23
34
|
|
|
@@ -46,7 +57,7 @@ Discover:
|
|
|
46
57
|
lore host --help
|
|
47
58
|
|
|
48
59
|
Examples:
|
|
49
|
-
lore connect --
|
|
60
|
+
lore connect --token "$LORE_WORKSPACE_TOKEN"
|
|
50
61
|
lore connect github --repo owner/repository
|
|
51
62
|
lore status --json
|
|
52
63
|
lore doctor
|
|
@@ -58,20 +69,21 @@ const CONNECT_HELP = `lore connect
|
|
|
58
69
|
Store a workspace credential and idempotently install agent integrations.
|
|
59
70
|
|
|
60
71
|
Usage:
|
|
61
|
-
lore connect --
|
|
72
|
+
lore connect --token <token> [options]
|
|
62
73
|
|
|
63
74
|
Options:
|
|
64
|
-
--url <url> Lore API base URL (
|
|
65
|
-
--dashboard-url <url> Lore dashboard URL
|
|
75
|
+
--url <url> Lore API base URL (default: https://api.uselore.co)
|
|
76
|
+
--dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
|
|
66
77
|
--token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
|
|
67
|
-
--agent <name> claude, codex, or
|
|
78
|
+
--agent <name> claude, codex, cursor, opencode, polytoken, or t3code; repeat to override auto-detection
|
|
68
79
|
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
69
80
|
--json Print machine-readable output
|
|
70
81
|
--help Show this command's help
|
|
71
82
|
|
|
72
83
|
Examples:
|
|
73
|
-
lore connect --
|
|
84
|
+
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
|
|
74
85
|
lore connect --url http://localhost:3004 --token dev-token --agent codex
|
|
86
|
+
lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
|
|
75
87
|
`;
|
|
76
88
|
const STATUS_HELP = `lore status
|
|
77
89
|
Show whether Lore is configured and each native integration is installed.
|
|
@@ -110,9 +122,14 @@ function isObject(value) {
|
|
|
110
122
|
function cloneObject(value) {
|
|
111
123
|
return structuredClone(value);
|
|
112
124
|
}
|
|
113
|
-
export function getLorePaths(home) {
|
|
114
|
-
const resolvedHome = resolve(home ??
|
|
125
|
+
export function getLorePaths(home, environment = process.env) {
|
|
126
|
+
const resolvedHome = resolve(home ?? environment.HOME ?? homedir());
|
|
115
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"));
|
|
116
133
|
return {
|
|
117
134
|
home: resolvedHome,
|
|
118
135
|
loreDirectory,
|
|
@@ -124,23 +141,30 @@ export function getLorePaths(home) {
|
|
|
124
141
|
queue: resolve(loreDirectory, "queue"),
|
|
125
142
|
codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
|
|
126
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"),
|
|
127
148
|
openCodeConfig: resolve(resolvedHome, ".config", "opencode", "opencode.json"),
|
|
149
|
+
polytokenHooks: resolve(xdgConfigHome, "polytoken", "hooks.json"),
|
|
150
|
+
t3Settings: resolve(t3Home, "userdata", "settings.json"),
|
|
128
151
|
};
|
|
129
152
|
}
|
|
130
153
|
function shellQuote(value) {
|
|
131
154
|
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
132
155
|
}
|
|
133
156
|
function isConfiguredAgent(value) {
|
|
134
|
-
return
|
|
157
|
+
return typeof value === "string" && isNativeCodingAgent(value);
|
|
135
158
|
}
|
|
136
|
-
function
|
|
137
|
-
return
|
|
159
|
+
function isConnectAgent(value) {
|
|
160
|
+
return isConfiguredAgent(value) || value === "t3code";
|
|
138
161
|
}
|
|
139
162
|
function hookCommand(agent, paths) {
|
|
163
|
+
const loreHome = `LORE_HOME=${shellQuote(paths.home)}`;
|
|
140
164
|
if (IS_STANDALONE_BINARY) {
|
|
141
|
-
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}`;
|
|
142
166
|
}
|
|
143
|
-
return
|
|
167
|
+
return `env ${loreHome} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
144
168
|
}
|
|
145
169
|
function isLoreHook(value) {
|
|
146
170
|
if (!isObject(value) || value.type !== "command") {
|
|
@@ -240,6 +264,113 @@ export function countLoreHooks(input) {
|
|
|
240
264
|
}
|
|
241
265
|
return count;
|
|
242
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
|
+
}
|
|
243
374
|
function isLoreOpenCodePlugin(value) {
|
|
244
375
|
return (typeof value === "string" &&
|
|
245
376
|
/^@lore-co\/(?:opencode|opencode-plugin)(?:@[^\s]+)?$/u.test(value));
|
|
@@ -282,9 +413,6 @@ async function readJsonDocument(path) {
|
|
|
282
413
|
stat(path),
|
|
283
414
|
]);
|
|
284
415
|
const parsed = JSON.parse(raw);
|
|
285
|
-
if (!isObject(parsed)) {
|
|
286
|
-
throw new Error(`Expected a JSON object in ${path}`);
|
|
287
|
-
}
|
|
288
416
|
return {
|
|
289
417
|
exists: true,
|
|
290
418
|
value: parsed,
|
|
@@ -296,7 +424,7 @@ async function readJsonDocument(path) {
|
|
|
296
424
|
? error.code
|
|
297
425
|
: undefined;
|
|
298
426
|
if (code === "ENOENT") {
|
|
299
|
-
return { exists: false, value:
|
|
427
|
+
return { exists: false, value: undefined, mode: 0o600 };
|
|
300
428
|
}
|
|
301
429
|
if (error instanceof SyntaxError) {
|
|
302
430
|
throw new Error(`Refusing to overwrite invalid JSON in ${path}`, {
|
|
@@ -345,6 +473,25 @@ function parseConnectorConfig(value) {
|
|
|
345
473
|
return null;
|
|
346
474
|
}
|
|
347
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;
|
|
348
495
|
const timeoutMs = typeof value.timeoutMs === "number" &&
|
|
349
496
|
Number.isInteger(value.timeoutMs) &&
|
|
350
497
|
value.timeoutMs >= 250 &&
|
|
@@ -360,6 +507,10 @@ function parseConnectorConfig(value) {
|
|
|
360
507
|
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
361
508
|
token: value.token,
|
|
362
509
|
agents,
|
|
510
|
+
...(agentConfigPaths === undefined ||
|
|
511
|
+
Object.keys(agentConfigPaths).length === 0
|
|
512
|
+
? {}
|
|
513
|
+
: { agentConfigPaths }),
|
|
363
514
|
connectedAt: value.connectedAt,
|
|
364
515
|
timeoutMs,
|
|
365
516
|
};
|
|
@@ -391,6 +542,33 @@ function normalizeApiUrl(value) {
|
|
|
391
542
|
}
|
|
392
543
|
return parsed.href.replace(/\/+$/u, "");
|
|
393
544
|
}
|
|
545
|
+
export function resolveConnectEndpoints(options = {}) {
|
|
546
|
+
const environment = options.environment ?? process.env;
|
|
547
|
+
const configuredApiUrl = options.apiUrl ??
|
|
548
|
+
environment.LORE_API_URL ??
|
|
549
|
+
environment.LORE_BASE_URL;
|
|
550
|
+
const apiUrl = normalizeApiUrl(configuredApiUrl ??
|
|
551
|
+
(options.preferHostedDefault
|
|
552
|
+
? DEFAULT_HOSTED_API_URL
|
|
553
|
+
: (options.existing?.apiUrl ?? DEFAULT_HOSTED_API_URL)));
|
|
554
|
+
const configuredDashboardUrl = options.dashboardUrl ?? environment.LORE_DASHBOARD_URL;
|
|
555
|
+
const dashboardUrlValue = options.preferHostedDefault
|
|
556
|
+
? (configuredDashboardUrl ??
|
|
557
|
+
(apiUrl === DEFAULT_HOSTED_API_URL
|
|
558
|
+
? DEFAULT_HOSTED_DASHBOARD_URL
|
|
559
|
+
: undefined))
|
|
560
|
+
: (configuredDashboardUrl ??
|
|
561
|
+
options.existing?.dashboardUrl ??
|
|
562
|
+
(apiUrl === DEFAULT_HOSTED_API_URL
|
|
563
|
+
? DEFAULT_HOSTED_DASHBOARD_URL
|
|
564
|
+
: undefined));
|
|
565
|
+
return {
|
|
566
|
+
apiUrl,
|
|
567
|
+
...(dashboardUrlValue === undefined
|
|
568
|
+
? {}
|
|
569
|
+
: { dashboardUrl: normalizeApiUrl(dashboardUrlValue) }),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
394
572
|
function configuredToken(explicit, existing) {
|
|
395
573
|
if (explicit !== undefined) {
|
|
396
574
|
return explicit;
|
|
@@ -489,20 +667,266 @@ async function pathExists(path) {
|
|
|
489
667
|
return false;
|
|
490
668
|
}
|
|
491
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
|
+
}
|
|
492
745
|
async function detectAgents(paths) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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,
|
|
498
925
|
]);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
? ["opencode"]
|
|
504
|
-
: []),
|
|
505
|
-
];
|
|
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 };
|
|
506
930
|
}
|
|
507
931
|
async function installRuntime(paths) {
|
|
508
932
|
if (IS_STANDALONE_BINARY) {
|
|
@@ -524,28 +948,19 @@ async function installRuntime(paths) {
|
|
|
524
948
|
atomicWrite(paths.runtimePackage, '{"type":"module"}\n', 0o600),
|
|
525
949
|
]);
|
|
526
950
|
}
|
|
527
|
-
function hookPath(agent, paths) {
|
|
528
|
-
return agent === "codex" ? paths.codexHooks : paths.claudeSettings;
|
|
529
|
-
}
|
|
530
951
|
function agentConfigPath(agent, paths) {
|
|
531
|
-
return agent
|
|
532
|
-
? paths.openCodeConfig
|
|
533
|
-
: hookPath(agent, paths);
|
|
952
|
+
return targetFor(agent).configPath(paths);
|
|
534
953
|
}
|
|
535
954
|
function mergeAgentConfig(input, agent, paths) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
: mergeLoreHooks(input, agent, paths);
|
|
955
|
+
const target = targetFor(agent);
|
|
956
|
+
return target.merge(input === undefined ? target.emptyValue() : input, paths);
|
|
539
957
|
}
|
|
540
958
|
function removeAgentConfig(input, agent) {
|
|
541
|
-
return agent
|
|
542
|
-
? removeLoreOpenCodePlugin(input)
|
|
543
|
-
: removeLoreHooks(input);
|
|
959
|
+
return targetFor(agent).remove(input);
|
|
544
960
|
}
|
|
545
961
|
function countAgentIntegrations(input, agent) {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
: countLoreHooks(input);
|
|
962
|
+
const target = targetFor(agent);
|
|
963
|
+
return target.count(input === undefined ? target.emptyValue() : input);
|
|
549
964
|
}
|
|
550
965
|
function parseInteger(value, flag) {
|
|
551
966
|
const parsed = Number(value);
|
|
@@ -598,11 +1013,11 @@ function parseConnectArguments(args) {
|
|
|
598
1013
|
}
|
|
599
1014
|
parsed.timeoutMs = timeoutMs;
|
|
600
1015
|
}
|
|
601
|
-
else if (
|
|
1016
|
+
else if (isConnectAgent(value)) {
|
|
602
1017
|
parsed.agents.push(value);
|
|
603
1018
|
}
|
|
604
1019
|
else {
|
|
605
|
-
throw new Error("--agent must be claude, codex, or
|
|
1020
|
+
throw new Error("--agent must be claude, codex, cursor, opencode, polytoken, or t3code");
|
|
606
1021
|
}
|
|
607
1022
|
}
|
|
608
1023
|
return parsed;
|
|
@@ -635,69 +1050,81 @@ async function connectCommand(args) {
|
|
|
635
1050
|
}
|
|
636
1051
|
const paths = getLorePaths();
|
|
637
1052
|
const existing = await readConnectorConfig(paths);
|
|
638
|
-
const
|
|
639
|
-
process.env.
|
|
640
|
-
process.env.
|
|
641
|
-
|
|
1053
|
+
const hasProvidedToken = parsed.token !== undefined ||
|
|
1054
|
+
(process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
|
|
1055
|
+
(process.env.LORE_TOKEN?.trim() ?? "") !== "";
|
|
1056
|
+
const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
|
|
1057
|
+
...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
|
|
1058
|
+
...(parsed.dashboardUrl === undefined
|
|
1059
|
+
? {}
|
|
1060
|
+
: { dashboardUrl: parsed.dashboardUrl }),
|
|
1061
|
+
environment: process.env,
|
|
1062
|
+
existing,
|
|
1063
|
+
preferHostedDefault: hasProvidedToken,
|
|
1064
|
+
});
|
|
642
1065
|
const token = configuredToken(parsed.token, existing);
|
|
643
|
-
const dashboardUrlValue = parsed.dashboardUrl ??
|
|
644
|
-
process.env.LORE_DASHBOARD_URL ??
|
|
645
|
-
existing?.dashboardUrl;
|
|
646
|
-
if (apiUrlValue === undefined || apiUrlValue.trim() === "") {
|
|
647
|
-
throw new Error("Lore API URL is required. Use --url <url> or LORE_API_URL.");
|
|
648
|
-
}
|
|
649
1066
|
if (token === undefined || token.trim() === "") {
|
|
650
1067
|
throw new Error("Workspace token is required. Use --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
|
|
651
1068
|
}
|
|
652
|
-
const apiUrl = normalizeApiUrl(apiUrlValue);
|
|
653
1069
|
const timeoutMs = parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500;
|
|
654
1070
|
const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
|
|
655
|
-
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
|
+
]);
|
|
656
1080
|
const agents = [
|
|
657
|
-
...new Set(
|
|
658
|
-
...(existing?.agents ?? []),
|
|
659
|
-
...parsed.agents,
|
|
660
|
-
...detected,
|
|
661
|
-
]),
|
|
1081
|
+
...new Set(installations.map((installation) => installation.agent)),
|
|
662
1082
|
].sort();
|
|
663
1083
|
if (agents.length === 0) {
|
|
664
|
-
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.");
|
|
665
1085
|
}
|
|
666
1086
|
const now = new Date();
|
|
667
1087
|
const documents = new Map();
|
|
668
1088
|
const mergedDocuments = new Map();
|
|
669
|
-
for (const
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
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));
|
|
673
1094
|
}
|
|
674
|
-
if (
|
|
1095
|
+
if (installations.some((installation) => targetFor(installation.agent).runtimeRequired)) {
|
|
675
1096
|
await installRuntime(paths);
|
|
676
1097
|
}
|
|
677
1098
|
const changedHookFiles = [];
|
|
678
1099
|
const backups = [];
|
|
679
|
-
for (const
|
|
680
|
-
const
|
|
681
|
-
const
|
|
1100
|
+
for (const installation of installations) {
|
|
1101
|
+
const key = installationKey(installation);
|
|
1102
|
+
const document = documents.get(key);
|
|
1103
|
+
const merged = mergedDocuments.get(key);
|
|
682
1104
|
if (document === undefined || merged === undefined) {
|
|
683
1105
|
continue;
|
|
684
1106
|
}
|
|
685
|
-
const result = await writeMergedJson(
|
|
1107
|
+
const result = await writeMergedJson(installation.configPath, document, merged, now);
|
|
686
1108
|
if (result.changed) {
|
|
687
|
-
changedHookFiles.push(
|
|
1109
|
+
changedHookFiles.push(installation.configPath);
|
|
688
1110
|
}
|
|
689
1111
|
if (result.backup !== undefined) {
|
|
690
1112
|
backups.push(result.backup);
|
|
691
1113
|
}
|
|
692
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
|
+
}
|
|
693
1121
|
const config = {
|
|
694
1122
|
version: 1,
|
|
695
1123
|
apiUrl,
|
|
696
|
-
...(
|
|
697
|
-
? {}
|
|
698
|
-
: { dashboardUrl: normalizeApiUrl(dashboardUrlValue) }),
|
|
1124
|
+
...(dashboardUrl === undefined ? {} : { dashboardUrl }),
|
|
699
1125
|
token: token.trim(),
|
|
700
1126
|
agents,
|
|
1127
|
+
agentConfigPaths,
|
|
701
1128
|
connectedAt: existing?.connectedAt ?? now.toISOString(),
|
|
702
1129
|
timeoutMs,
|
|
703
1130
|
};
|
|
@@ -710,8 +1137,9 @@ async function connectCommand(args) {
|
|
|
710
1137
|
config: paths.config,
|
|
711
1138
|
changedHookFiles,
|
|
712
1139
|
backups,
|
|
1140
|
+
warnings: requestedExpansion.warnings,
|
|
713
1141
|
};
|
|
714
|
-
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("")}`);
|
|
715
1143
|
}
|
|
716
1144
|
async function queueCount(paths) {
|
|
717
1145
|
try {
|
|
@@ -722,23 +1150,33 @@ async function queueCount(paths) {
|
|
|
722
1150
|
}
|
|
723
1151
|
}
|
|
724
1152
|
async function getAgentStatus(agent, config, paths) {
|
|
725
|
-
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)];
|
|
726
1158
|
let installed = 0;
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
}
|
|
732
1169
|
}
|
|
733
1170
|
return {
|
|
734
1171
|
agent,
|
|
735
1172
|
configured: config?.agents.includes(agent) ?? false,
|
|
736
|
-
executable: await
|
|
737
|
-
configExists:
|
|
738
|
-
configFile:
|
|
739
|
-
|
|
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,
|
|
740
1178
|
installed,
|
|
741
|
-
expected:
|
|
1179
|
+
expected: target.expected * configPaths.length,
|
|
742
1180
|
};
|
|
743
1181
|
}
|
|
744
1182
|
async function statusData(paths) {
|
|
@@ -750,7 +1188,7 @@ async function statusData(paths) {
|
|
|
750
1188
|
catch {
|
|
751
1189
|
// Missing configuration is represented as disconnected.
|
|
752
1190
|
}
|
|
753
|
-
const runtimeRequired = config?.agents.some(
|
|
1191
|
+
const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
|
|
754
1192
|
const runtimeInstalledCheck = runtimeRequired
|
|
755
1193
|
? Promise.all(IS_STANDALONE_BINARY
|
|
756
1194
|
? [access(process.execPath, fsConstants.R_OK | fsConstants.X_OK)]
|
|
@@ -760,12 +1198,10 @@ async function statusData(paths) {
|
|
|
760
1198
|
access(paths.runtimePackage, fsConstants.R_OK),
|
|
761
1199
|
]).then(() => true, () => false)
|
|
762
1200
|
: Promise.resolve(false);
|
|
763
|
-
const [runtimeInstalled, queuedTurns,
|
|
1201
|
+
const [runtimeInstalled, queuedTurns, agents] = await Promise.all([
|
|
764
1202
|
runtimeInstalledCheck,
|
|
765
1203
|
queueCount(paths),
|
|
766
|
-
getAgentStatus(
|
|
767
|
-
getAgentStatus("codex", config, paths),
|
|
768
|
-
getAgentStatus("opencode", config, paths),
|
|
1204
|
+
Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths))),
|
|
769
1205
|
]);
|
|
770
1206
|
return {
|
|
771
1207
|
connected: config !== null,
|
|
@@ -775,7 +1211,7 @@ async function statusData(paths) {
|
|
|
775
1211
|
runtimeRequired,
|
|
776
1212
|
runtimeInstalled,
|
|
777
1213
|
queuedTurns,
|
|
778
|
-
agents
|
|
1214
|
+
agents,
|
|
779
1215
|
};
|
|
780
1216
|
}
|
|
781
1217
|
async function statusCommand(args) {
|
|
@@ -795,16 +1231,21 @@ async function disconnectCommand(args) {
|
|
|
795
1231
|
return;
|
|
796
1232
|
}
|
|
797
1233
|
const paths = getLorePaths();
|
|
1234
|
+
const config = await readConnectorConfig(paths);
|
|
798
1235
|
const now = new Date();
|
|
799
1236
|
const changedHookFiles = [];
|
|
800
1237
|
const backups = [];
|
|
801
|
-
|
|
802
|
-
|
|
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;
|
|
803
1244
|
const document = await readJsonDocument(path);
|
|
804
1245
|
if (!document.exists) {
|
|
805
1246
|
continue;
|
|
806
1247
|
}
|
|
807
|
-
const result = await writeMergedJson(path, document, removeAgentConfig(document.value, agent), now);
|
|
1248
|
+
const result = await writeMergedJson(path, document, removeAgentConfig(document.value, installation.agent), now);
|
|
808
1249
|
if (result.changed) {
|
|
809
1250
|
changedHookFiles.push(path);
|
|
810
1251
|
}
|
|
@@ -924,7 +1365,7 @@ async function doctorCommand(args) {
|
|
|
924
1365
|
checks.push({
|
|
925
1366
|
name: `${agent.agent}-executable`,
|
|
926
1367
|
status: agent.executable ? "ok" : "warning",
|
|
927
|
-
detail: agent.executable ? "
|
|
1368
|
+
detail: agent.executable ? "detected" : "not detected",
|
|
928
1369
|
});
|
|
929
1370
|
checks.push({
|
|
930
1371
|
name: `${agent.agent}-${agent.integration}`,
|