@lore-co/cli 0.1.0
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/LICENSE +110 -0
- package/README.md +218 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +792 -0
- package/dist/cli.js.map +1 -0
- package/dist/devin-client.d.ts +63 -0
- package/dist/devin-client.d.ts.map +1 -0
- package/dist/devin-client.js +231 -0
- package/dist/devin-client.js.map +1 -0
- package/dist/devin.d.ts +2 -0
- package/dist/devin.d.ts.map +1 -0
- package/dist/devin.js +479 -0
- package/dist/devin.js.map +1 -0
- package/dist/generated-assets.d.ts +7 -0
- package/dist/generated-assets.d.ts.map +1 -0
- package/dist/generated-assets.js +8 -0
- package/dist/generated-assets.js.map +1 -0
- package/dist/github.d.ts +13 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +559 -0
- package/dist/github.js.map +1 -0
- package/dist/host.d.ts +24 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +337 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +10 -0
- package/dist/main.js.map +1 -0
- package/dist/repository.d.ts +3 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +60 -0
- package/dist/repository.js.map +1 -0
- package/dist/review-output.d.ts +59 -0
- package/dist/review-output.d.ts.map +1 -0
- package/dist/review-output.js +112 -0
- package/dist/review-output.js.map +1 -0
- package/dist/runtime.d.ts +63 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +612 -0
- package/dist/runtime.js.map +1 -0
- package/dist/update.d.ts +2 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +97 -0
- package/dist/update.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +47 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access, chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants, realpathSync } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { delimiter, dirname, resolve } from "node:path";
|
|
6
|
+
import { homedir, platform } from "node:os";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { runHook, } from "./runtime.js";
|
|
9
|
+
import { connectGithub, runGithubCommand } from "./github.js";
|
|
10
|
+
import { runDevinCommand } from "./devin.js";
|
|
11
|
+
import { runHostCommand } from "./host.js";
|
|
12
|
+
import { updateCommand } from "./update.js";
|
|
13
|
+
import { IS_STANDALONE_BINARY, LORE_VERSION } from "./version.js";
|
|
14
|
+
const LORE_OWNER_ARGUMENT = "--owner lore";
|
|
15
|
+
const HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionEnd"];
|
|
16
|
+
const ROOT_HELP = `lore
|
|
17
|
+
Connect local coding agents to Lore shared engineering memory.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
lore <command> [options]
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
connect Configure Lore and install native agent hooks
|
|
24
|
+
github Prepare/post GitHub reviews and observe corrections
|
|
25
|
+
devin Start and manage Lore-enabled Devin sessions
|
|
26
|
+
host Call auditable APIs from external agent hosts
|
|
27
|
+
status Show connector and hook state
|
|
28
|
+
doctor Diagnose configuration, hooks, and API reachability
|
|
29
|
+
update Install the latest Lore CLI binary
|
|
30
|
+
disconnect Remove Lore-owned hooks and local credentials
|
|
31
|
+
hook Internal native hook handler
|
|
32
|
+
|
|
33
|
+
Discover:
|
|
34
|
+
lore connect --help
|
|
35
|
+
lore status --help
|
|
36
|
+
lore doctor --help
|
|
37
|
+
lore disconnect --help
|
|
38
|
+
lore host --help
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
lore connect --url https://lore.example.com --token "$LORE_TOKEN"
|
|
42
|
+
lore connect github --repo owner/repository
|
|
43
|
+
lore status --json
|
|
44
|
+
lore doctor
|
|
45
|
+
lore update
|
|
46
|
+
lore devin --help
|
|
47
|
+
`;
|
|
48
|
+
const CONNECT_HELP = `lore connect
|
|
49
|
+
Store a workspace credential and idempotently install Codex and Claude hooks.
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
lore connect --url <url> --token <token> [options]
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
--url <url> Lore API base URL (or LORE_API_URL)
|
|
56
|
+
--token <token> Workspace bearer token (or LORE_TOKEN)
|
|
57
|
+
--agent <name> codex or claude; repeat to override auto-detection
|
|
58
|
+
--timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
|
|
59
|
+
--json Print machine-readable output
|
|
60
|
+
--help Show this command's help
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
lore connect --url https://lore.example.com --token "$LORE_TOKEN"
|
|
64
|
+
lore connect --url http://localhost:3004 --token dev-token --agent codex
|
|
65
|
+
`;
|
|
66
|
+
const STATUS_HELP = `lore status
|
|
67
|
+
Show whether Lore is configured and its native hooks are installed.
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
lore status [--json]
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
lore status
|
|
74
|
+
lore status --json
|
|
75
|
+
`;
|
|
76
|
+
const DOCTOR_HELP = `lore doctor
|
|
77
|
+
Check local security, runtime, native hooks, agent binaries, and Lore health.
|
|
78
|
+
|
|
79
|
+
Usage:
|
|
80
|
+
lore doctor [--json]
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
lore doctor
|
|
84
|
+
lore doctor --json
|
|
85
|
+
`;
|
|
86
|
+
const DISCONNECT_HELP = `lore disconnect
|
|
87
|
+
Remove only Lore-owned native hooks, credentials, runtime, state, and retry queue.
|
|
88
|
+
Unrelated Codex and Claude settings and Lore-created backups are retained.
|
|
89
|
+
|
|
90
|
+
Usage:
|
|
91
|
+
lore disconnect [--json]
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
lore disconnect
|
|
95
|
+
lore disconnect --json
|
|
96
|
+
`;
|
|
97
|
+
function isObject(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function cloneObject(value) {
|
|
101
|
+
return structuredClone(value);
|
|
102
|
+
}
|
|
103
|
+
export function getLorePaths(home) {
|
|
104
|
+
const resolvedHome = resolve(home ?? process.env.HOME ?? homedir());
|
|
105
|
+
const loreDirectory = resolve(resolvedHome, ".lore");
|
|
106
|
+
return {
|
|
107
|
+
home: resolvedHome,
|
|
108
|
+
loreDirectory,
|
|
109
|
+
config: resolve(loreDirectory, "config.json"),
|
|
110
|
+
runtime: resolve(loreDirectory, "bin", "lore-hook.mjs"),
|
|
111
|
+
runtimeRepository: resolve(loreDirectory, "bin", "repository.js"),
|
|
112
|
+
runtimePackage: resolve(loreDirectory, "bin", "package.json"),
|
|
113
|
+
state: resolve(loreDirectory, "state"),
|
|
114
|
+
queue: resolve(loreDirectory, "queue"),
|
|
115
|
+
codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
|
|
116
|
+
claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function shellQuote(value) {
|
|
120
|
+
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
121
|
+
}
|
|
122
|
+
function hookCommand(agent, paths) {
|
|
123
|
+
if (IS_STANDALONE_BINARY) {
|
|
124
|
+
return `env -u BUN_OPTIONS -u BUN_BE_BUN ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
125
|
+
}
|
|
126
|
+
return `${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
|
|
127
|
+
}
|
|
128
|
+
function isLoreHook(value) {
|
|
129
|
+
if (!isObject(value) || value.type !== "command") {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const command = value.command;
|
|
133
|
+
return (typeof command === "string" &&
|
|
134
|
+
(/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
|
|
135
|
+
command.includes("/.lore/bin/lore-hook.mjs")));
|
|
136
|
+
}
|
|
137
|
+
function stripLoreFromEvent(value) {
|
|
138
|
+
if (!Array.isArray(value)) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
const groups = [];
|
|
142
|
+
for (const candidate of value) {
|
|
143
|
+
if (!isObject(candidate) || !Array.isArray(candidate.hooks)) {
|
|
144
|
+
groups.push(candidate);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const hooks = candidate.hooks.filter((hook) => !isLoreHook(hook));
|
|
148
|
+
if (hooks.length === 0 && candidate.hooks.some(isLoreHook)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
groups.push({ ...candidate, hooks });
|
|
152
|
+
}
|
|
153
|
+
return groups;
|
|
154
|
+
}
|
|
155
|
+
function eventHandler(agent, event, paths) {
|
|
156
|
+
return {
|
|
157
|
+
type: "command",
|
|
158
|
+
command: hookCommand(agent, paths),
|
|
159
|
+
timeout: event === "SessionEnd" ? 2 : event === "Stop" ? 3 : 25,
|
|
160
|
+
...(event === "UserPromptSubmit"
|
|
161
|
+
? { statusMessage: "Loading Lore context" }
|
|
162
|
+
: {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function mergeLoreHooks(input, agent, paths) {
|
|
166
|
+
const result = cloneObject(input);
|
|
167
|
+
if (result.hooks !== undefined && !isObject(result.hooks)) {
|
|
168
|
+
throw new Error("Agent configuration field \"hooks\" must be a JSON object");
|
|
169
|
+
}
|
|
170
|
+
const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
|
|
171
|
+
for (const event of HOOK_EVENTS) {
|
|
172
|
+
if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
|
|
173
|
+
throw new Error(`Agent hook event "${event}" must be a JSON array`);
|
|
174
|
+
}
|
|
175
|
+
hooks[event] = [
|
|
176
|
+
...stripLoreFromEvent(hooks[event]),
|
|
177
|
+
{ hooks: [eventHandler(agent, event, paths)] },
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
result.hooks = hooks;
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
export function removeLoreHooks(input) {
|
|
184
|
+
const result = cloneObject(input);
|
|
185
|
+
if (!isObject(result.hooks)) {
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
const hooks = { ...result.hooks };
|
|
189
|
+
for (const event of HOOK_EVENTS) {
|
|
190
|
+
if (Array.isArray(hooks[event])) {
|
|
191
|
+
const groups = stripLoreFromEvent(hooks[event]);
|
|
192
|
+
if (groups.length === 0) {
|
|
193
|
+
delete hooks[event];
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
hooks[event] = groups;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (Object.keys(hooks).length === 0) {
|
|
201
|
+
delete result.hooks;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
result.hooks = hooks;
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
export function countLoreHooks(input) {
|
|
209
|
+
if (!isObject(input.hooks)) {
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
let count = 0;
|
|
213
|
+
for (const event of HOOK_EVENTS) {
|
|
214
|
+
const groups = input.hooks[event];
|
|
215
|
+
if (!Array.isArray(groups)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
for (const group of groups) {
|
|
219
|
+
if (isObject(group) && Array.isArray(group.hooks)) {
|
|
220
|
+
count += group.hooks.filter(isLoreHook).length;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return count;
|
|
225
|
+
}
|
|
226
|
+
async function readJsonDocument(path) {
|
|
227
|
+
try {
|
|
228
|
+
const [raw, metadata] = await Promise.all([
|
|
229
|
+
readFile(path, "utf8"),
|
|
230
|
+
stat(path),
|
|
231
|
+
]);
|
|
232
|
+
const parsed = JSON.parse(raw);
|
|
233
|
+
if (!isObject(parsed)) {
|
|
234
|
+
throw new Error(`Expected a JSON object in ${path}`);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
exists: true,
|
|
238
|
+
value: parsed,
|
|
239
|
+
mode: metadata.mode & 0o777,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
244
|
+
? error.code
|
|
245
|
+
: undefined;
|
|
246
|
+
if (code === "ENOENT") {
|
|
247
|
+
return { exists: false, value: {}, mode: 0o600 };
|
|
248
|
+
}
|
|
249
|
+
if (error instanceof SyntaxError) {
|
|
250
|
+
throw new Error(`Refusing to overwrite invalid JSON in ${path}`, {
|
|
251
|
+
cause: error,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function atomicWrite(path, content, mode) {
|
|
258
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
259
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
260
|
+
await writeFile(temporary, content, {
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
mode,
|
|
263
|
+
flag: "wx",
|
|
264
|
+
});
|
|
265
|
+
await rename(temporary, path);
|
|
266
|
+
await chmod(path, mode);
|
|
267
|
+
}
|
|
268
|
+
function backupTimestamp(now) {
|
|
269
|
+
return now.toISOString().replaceAll(":", "").replaceAll(".", "-");
|
|
270
|
+
}
|
|
271
|
+
async function writeMergedJson(path, document, value, now) {
|
|
272
|
+
if (JSON.stringify(document.value) === JSON.stringify(value)) {
|
|
273
|
+
return { changed: false };
|
|
274
|
+
}
|
|
275
|
+
let backup;
|
|
276
|
+
if (document.exists) {
|
|
277
|
+
backup = `${path}.lore-backup-${backupTimestamp(now)}-${randomUUID().slice(0, 8)}`;
|
|
278
|
+
await copyFile(path, backup, fsConstants.COPYFILE_EXCL);
|
|
279
|
+
}
|
|
280
|
+
await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`, document.mode);
|
|
281
|
+
return {
|
|
282
|
+
changed: true,
|
|
283
|
+
...(backup === undefined ? {} : { backup }),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function parseConnectorConfig(value) {
|
|
287
|
+
if (!isObject(value) ||
|
|
288
|
+
value.version !== 1 ||
|
|
289
|
+
typeof value.apiUrl !== "string" ||
|
|
290
|
+
typeof value.token !== "string" ||
|
|
291
|
+
!Array.isArray(value.agents) ||
|
|
292
|
+
typeof value.connectedAt !== "string") {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const agents = value.agents.filter((agent) => agent === "codex" || agent === "claude");
|
|
296
|
+
const timeoutMs = typeof value.timeoutMs === "number" &&
|
|
297
|
+
Number.isInteger(value.timeoutMs) &&
|
|
298
|
+
value.timeoutMs >= 250 &&
|
|
299
|
+
value.timeoutMs <= 10_000
|
|
300
|
+
? value.timeoutMs
|
|
301
|
+
: 2_500;
|
|
302
|
+
return {
|
|
303
|
+
version: 1,
|
|
304
|
+
apiUrl: value.apiUrl,
|
|
305
|
+
token: value.token,
|
|
306
|
+
agents,
|
|
307
|
+
connectedAt: value.connectedAt,
|
|
308
|
+
timeoutMs,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
async function readConnectorConfig(paths) {
|
|
312
|
+
try {
|
|
313
|
+
return parseConnectorConfig(JSON.parse(await readFile(paths.config, "utf8")));
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function normalizeApiUrl(value) {
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = new URL(value);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
throw new Error(`Invalid Lore API URL: ${value}`);
|
|
326
|
+
}
|
|
327
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
328
|
+
throw new Error("Lore API URL must use http or https");
|
|
329
|
+
}
|
|
330
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
331
|
+
throw new Error("Lore API URL must not contain credentials");
|
|
332
|
+
}
|
|
333
|
+
parsed.hash = "";
|
|
334
|
+
parsed.search = "";
|
|
335
|
+
return parsed.href.replace(/\/+$/u, "");
|
|
336
|
+
}
|
|
337
|
+
async function isExecutable(path) {
|
|
338
|
+
try {
|
|
339
|
+
await access(path, fsConstants.X_OK);
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function commandExists(command) {
|
|
347
|
+
const path = process.env.PATH;
|
|
348
|
+
if (path === undefined) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
for (const directory of path.split(delimiter)) {
|
|
352
|
+
if (directory !== "" && (await isExecutable(resolve(directory, command)))) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
async function detectAgents() {
|
|
359
|
+
const [codex, claude] = await Promise.all([
|
|
360
|
+
commandExists("codex"),
|
|
361
|
+
commandExists("claude"),
|
|
362
|
+
]);
|
|
363
|
+
return [
|
|
364
|
+
...(codex ? ["codex"] : []),
|
|
365
|
+
...(claude ? ["claude"] : []),
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
async function installRuntime(paths) {
|
|
369
|
+
if (IS_STANDALONE_BINARY) {
|
|
370
|
+
await Promise.all([
|
|
371
|
+
rm(paths.runtime, { force: true }),
|
|
372
|
+
rm(paths.runtimeRepository, { force: true }),
|
|
373
|
+
rm(paths.runtimePackage, { force: true }),
|
|
374
|
+
]);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
378
|
+
const [runtime, repository] = await Promise.all([
|
|
379
|
+
readFile(resolve(sourceDirectory, "runtime.js"), "utf8"),
|
|
380
|
+
readFile(resolve(sourceDirectory, "repository.js"), "utf8"),
|
|
381
|
+
]);
|
|
382
|
+
await Promise.all([
|
|
383
|
+
atomicWrite(paths.runtime, runtime, 0o700),
|
|
384
|
+
atomicWrite(paths.runtimeRepository, repository, 0o600),
|
|
385
|
+
atomicWrite(paths.runtimePackage, '{"type":"module"}\n', 0o600),
|
|
386
|
+
]);
|
|
387
|
+
}
|
|
388
|
+
function hookPath(agent, paths) {
|
|
389
|
+
return agent === "codex" ? paths.codexHooks : paths.claudeSettings;
|
|
390
|
+
}
|
|
391
|
+
function parseInteger(value, flag) {
|
|
392
|
+
const parsed = Number(value);
|
|
393
|
+
if (!Number.isInteger(parsed)) {
|
|
394
|
+
throw new Error(`${flag} requires an integer`);
|
|
395
|
+
}
|
|
396
|
+
return parsed;
|
|
397
|
+
}
|
|
398
|
+
function valueAfter(args, index, flag) {
|
|
399
|
+
const value = args[index + 1];
|
|
400
|
+
if (value === undefined) {
|
|
401
|
+
throw new Error(`Missing value for ${flag}`);
|
|
402
|
+
}
|
|
403
|
+
return [value, index + 1];
|
|
404
|
+
}
|
|
405
|
+
function parseConnectArguments(args) {
|
|
406
|
+
const parsed = { agents: [], json: false };
|
|
407
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
408
|
+
const argument = args[index];
|
|
409
|
+
if (argument === "--help" || argument === "-h") {
|
|
410
|
+
process.stdout.write(CONNECT_HELP);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
if (argument === "--json") {
|
|
414
|
+
parsed.json = true;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (argument !== "--url" &&
|
|
418
|
+
argument !== "--token" &&
|
|
419
|
+
argument !== "--agent" &&
|
|
420
|
+
argument !== "--timeout-ms") {
|
|
421
|
+
throw new Error(`Unknown connect option: ${argument ?? ""}`);
|
|
422
|
+
}
|
|
423
|
+
const [value, valueIndex] = valueAfter(args, index, argument);
|
|
424
|
+
index = valueIndex;
|
|
425
|
+
if (argument === "--url") {
|
|
426
|
+
parsed.apiUrl = value;
|
|
427
|
+
}
|
|
428
|
+
else if (argument === "--token") {
|
|
429
|
+
parsed.token = value;
|
|
430
|
+
}
|
|
431
|
+
else if (argument === "--timeout-ms") {
|
|
432
|
+
const timeoutMs = parseInteger(value, "--timeout-ms");
|
|
433
|
+
if (timeoutMs < 250 || timeoutMs > 10_000) {
|
|
434
|
+
throw new Error("--timeout-ms must be between 250 and 10000");
|
|
435
|
+
}
|
|
436
|
+
parsed.timeoutMs = timeoutMs;
|
|
437
|
+
}
|
|
438
|
+
else if (value === "codex" || value === "claude") {
|
|
439
|
+
parsed.agents.push(value);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
throw new Error("--agent must be codex or claude");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return parsed;
|
|
446
|
+
}
|
|
447
|
+
function parseOutputArguments(args, help, command) {
|
|
448
|
+
let json = false;
|
|
449
|
+
for (const argument of args) {
|
|
450
|
+
if (argument === "--help" || argument === "-h") {
|
|
451
|
+
process.stdout.write(help);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
if (argument === "--json") {
|
|
455
|
+
json = true;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
throw new Error(`Unknown ${command} option: ${argument}`);
|
|
459
|
+
}
|
|
460
|
+
return { json };
|
|
461
|
+
}
|
|
462
|
+
function writeResult(value, json, text) {
|
|
463
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : text);
|
|
464
|
+
}
|
|
465
|
+
async function connectCommand(args) {
|
|
466
|
+
const parsed = parseConnectArguments(args);
|
|
467
|
+
if (parsed === null) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (platform() !== "darwin" && platform() !== "linux") {
|
|
471
|
+
throw new Error("Lore hooks currently support macOS and Linux");
|
|
472
|
+
}
|
|
473
|
+
const paths = getLorePaths();
|
|
474
|
+
const existing = await readConnectorConfig(paths);
|
|
475
|
+
const apiUrlValue = parsed.apiUrl ??
|
|
476
|
+
process.env.LORE_API_URL ??
|
|
477
|
+
process.env.LORE_BASE_URL ??
|
|
478
|
+
existing?.apiUrl;
|
|
479
|
+
const token = parsed.token ?? process.env.LORE_TOKEN ?? existing?.token;
|
|
480
|
+
if (apiUrlValue === undefined || apiUrlValue.trim() === "") {
|
|
481
|
+
throw new Error("Lore API URL is required. Use --url <url> or LORE_API_URL.");
|
|
482
|
+
}
|
|
483
|
+
if (token === undefined || token.trim() === "") {
|
|
484
|
+
throw new Error("Workspace token is required. Use --token <token> or LORE_TOKEN.");
|
|
485
|
+
}
|
|
486
|
+
const detected = parsed.agents.length === 0 ? await detectAgents() : [];
|
|
487
|
+
const agents = [
|
|
488
|
+
...new Set([
|
|
489
|
+
...(existing?.agents ?? []),
|
|
490
|
+
...parsed.agents,
|
|
491
|
+
...detected,
|
|
492
|
+
]),
|
|
493
|
+
].sort();
|
|
494
|
+
if (agents.length === 0) {
|
|
495
|
+
throw new Error("No Codex or Claude executable detected. Use --agent codex or --agent claude.");
|
|
496
|
+
}
|
|
497
|
+
const now = new Date();
|
|
498
|
+
const documents = new Map();
|
|
499
|
+
const mergedDocuments = new Map();
|
|
500
|
+
for (const agent of agents) {
|
|
501
|
+
const document = await readJsonDocument(hookPath(agent, paths));
|
|
502
|
+
documents.set(agent, document);
|
|
503
|
+
mergedDocuments.set(agent, mergeLoreHooks(document.value, agent, paths));
|
|
504
|
+
}
|
|
505
|
+
await installRuntime(paths);
|
|
506
|
+
const changedHookFiles = [];
|
|
507
|
+
const backups = [];
|
|
508
|
+
for (const agent of agents) {
|
|
509
|
+
const document = documents.get(agent);
|
|
510
|
+
const merged = mergedDocuments.get(agent);
|
|
511
|
+
if (document === undefined || merged === undefined) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const result = await writeMergedJson(hookPath(agent, paths), document, merged, now);
|
|
515
|
+
if (result.changed) {
|
|
516
|
+
changedHookFiles.push(hookPath(agent, paths));
|
|
517
|
+
}
|
|
518
|
+
if (result.backup !== undefined) {
|
|
519
|
+
backups.push(result.backup);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const config = {
|
|
523
|
+
version: 1,
|
|
524
|
+
apiUrl: normalizeApiUrl(apiUrlValue),
|
|
525
|
+
token: token.trim(),
|
|
526
|
+
agents,
|
|
527
|
+
connectedAt: existing?.connectedAt ?? now.toISOString(),
|
|
528
|
+
timeoutMs: parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500,
|
|
529
|
+
};
|
|
530
|
+
await atomicWrite(paths.config, `${JSON.stringify(config, null, 2)}\n`, 0o600);
|
|
531
|
+
const result = {
|
|
532
|
+
connected: true,
|
|
533
|
+
apiUrl: config.apiUrl,
|
|
534
|
+
agents,
|
|
535
|
+
config: paths.config,
|
|
536
|
+
changedHookFiles,
|
|
537
|
+
backups,
|
|
538
|
+
};
|
|
539
|
+
writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n`);
|
|
540
|
+
}
|
|
541
|
+
async function queueCount(paths) {
|
|
542
|
+
try {
|
|
543
|
+
return (await readdir(paths.queue)).filter((entry) => entry.endsWith(".json")).length;
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function getAgentStatus(agent, config, paths) {
|
|
550
|
+
const path = hookPath(agent, paths);
|
|
551
|
+
let installedHooks = 0;
|
|
552
|
+
try {
|
|
553
|
+
installedHooks = countLoreHooks((await readJsonDocument(path)).value);
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
installedHooks = 0;
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
agent,
|
|
560
|
+
configured: config?.agents.includes(agent) ?? false,
|
|
561
|
+
executable: await commandExists(agent),
|
|
562
|
+
hookFile: path,
|
|
563
|
+
installedHooks,
|
|
564
|
+
expectedHooks: HOOK_EVENTS.length,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
async function statusData(paths) {
|
|
568
|
+
const config = await readConnectorConfig(paths);
|
|
569
|
+
let configMode = null;
|
|
570
|
+
try {
|
|
571
|
+
configMode = (await stat(paths.config)).mode.toString(8).slice(-3);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
// Missing configuration is represented as disconnected.
|
|
575
|
+
}
|
|
576
|
+
const runtimeChecks = IS_STANDALONE_BINARY
|
|
577
|
+
? [access(process.execPath, fsConstants.R_OK | fsConstants.X_OK)]
|
|
578
|
+
: [
|
|
579
|
+
access(paths.runtime, fsConstants.R_OK | fsConstants.X_OK),
|
|
580
|
+
access(paths.runtimeRepository, fsConstants.R_OK),
|
|
581
|
+
access(paths.runtimePackage, fsConstants.R_OK),
|
|
582
|
+
];
|
|
583
|
+
const [runtimeInstalled, queuedTurns, codex, claude] = await Promise.all([
|
|
584
|
+
Promise.all(runtimeChecks).then(() => true, () => false),
|
|
585
|
+
queueCount(paths),
|
|
586
|
+
getAgentStatus("codex", config, paths),
|
|
587
|
+
getAgentStatus("claude", config, paths),
|
|
588
|
+
]);
|
|
589
|
+
return {
|
|
590
|
+
connected: config !== null,
|
|
591
|
+
apiUrl: config?.apiUrl ?? null,
|
|
592
|
+
config: paths.config,
|
|
593
|
+
configMode,
|
|
594
|
+
runtimeInstalled,
|
|
595
|
+
queuedTurns,
|
|
596
|
+
agents: [codex, claude],
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
async function statusCommand(args) {
|
|
600
|
+
const parsed = parseOutputArguments(args, STATUS_HELP, "status");
|
|
601
|
+
if (parsed === null) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const data = await statusData(getLorePaths());
|
|
605
|
+
const agentLines = data.agents
|
|
606
|
+
.map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, hooks ${agent.installedHooks}/${agent.expectedHooks}, executable ${agent.executable ? "yes" : "no"}`)
|
|
607
|
+
.join("\n");
|
|
608
|
+
writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeInstalled ? "installed" : "missing"}\nqueued_turns: ${data.queuedTurns}\n${agentLines}\n`);
|
|
609
|
+
}
|
|
610
|
+
async function disconnectCommand(args) {
|
|
611
|
+
const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
|
|
612
|
+
if (parsed === null) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const paths = getLorePaths();
|
|
616
|
+
const now = new Date();
|
|
617
|
+
const changedHookFiles = [];
|
|
618
|
+
const backups = [];
|
|
619
|
+
for (const agent of ["codex", "claude"]) {
|
|
620
|
+
const path = hookPath(agent, paths);
|
|
621
|
+
const document = await readJsonDocument(path);
|
|
622
|
+
if (!document.exists) {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const result = await writeMergedJson(path, document, removeLoreHooks(document.value), now);
|
|
626
|
+
if (result.changed) {
|
|
627
|
+
changedHookFiles.push(path);
|
|
628
|
+
}
|
|
629
|
+
if (result.backup !== undefined) {
|
|
630
|
+
backups.push(result.backup);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
await Promise.all([
|
|
634
|
+
rm(paths.config, { force: true }),
|
|
635
|
+
rm(paths.runtime, { force: true }),
|
|
636
|
+
rm(paths.runtimeRepository, { force: true }),
|
|
637
|
+
rm(paths.runtimePackage, { force: true }),
|
|
638
|
+
rm(paths.state, { recursive: true, force: true }),
|
|
639
|
+
rm(paths.queue, { recursive: true, force: true }),
|
|
640
|
+
]);
|
|
641
|
+
const result = { connected: false, changedHookFiles, backups };
|
|
642
|
+
writeResult(result, parsed.json, `disconnected: yes\nchanged_hook_files: ${changedHookFiles.length}\n`);
|
|
643
|
+
}
|
|
644
|
+
async function apiHealth(config) {
|
|
645
|
+
const url = `${config.apiUrl.replace(/\/+$/u, "")}/health`;
|
|
646
|
+
try {
|
|
647
|
+
const response = await fetch(url, {
|
|
648
|
+
headers: { authorization: `Bearer ${config.token}` },
|
|
649
|
+
signal: AbortSignal.timeout(3_000),
|
|
650
|
+
});
|
|
651
|
+
return response.ok
|
|
652
|
+
? { name: "api", status: "ok", detail: `${url} returned ${response.status}` }
|
|
653
|
+
: {
|
|
654
|
+
name: "api",
|
|
655
|
+
status: "error",
|
|
656
|
+
detail: `${url} returned ${response.status}`,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
return {
|
|
661
|
+
name: "api",
|
|
662
|
+
status: "error",
|
|
663
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function doctorCommand(args) {
|
|
668
|
+
const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
|
|
669
|
+
if (parsed === null) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const paths = getLorePaths();
|
|
673
|
+
const config = await readConnectorConfig(paths);
|
|
674
|
+
const status = await statusData(paths);
|
|
675
|
+
const checks = [];
|
|
676
|
+
checks.push({
|
|
677
|
+
name: "platform",
|
|
678
|
+
status: platform() === "darwin" || platform() === "linux" ? "ok" : "error",
|
|
679
|
+
detail: `${platform()} ${process.arch}`,
|
|
680
|
+
});
|
|
681
|
+
checks.push(IS_STANDALONE_BINARY
|
|
682
|
+
? {
|
|
683
|
+
name: "binary",
|
|
684
|
+
status: "ok",
|
|
685
|
+
detail: `Lore ${LORE_VERSION} at ${process.execPath}`,
|
|
686
|
+
}
|
|
687
|
+
: {
|
|
688
|
+
name: "node",
|
|
689
|
+
status: Number(process.versions.node.split(".")[0]) >= 22 ? "ok" : "error",
|
|
690
|
+
detail: process.versions.node,
|
|
691
|
+
});
|
|
692
|
+
checks.push({
|
|
693
|
+
name: "config",
|
|
694
|
+
status: config === null ? "error" : "ok",
|
|
695
|
+
detail: config === null ? `missing or invalid: ${paths.config}` : paths.config,
|
|
696
|
+
});
|
|
697
|
+
checks.push({
|
|
698
|
+
name: "config-permissions",
|
|
699
|
+
status: status.configMode === "600" ? "ok" : "error",
|
|
700
|
+
detail: status.configMode ?? "missing",
|
|
701
|
+
});
|
|
702
|
+
checks.push({
|
|
703
|
+
name: "runtime",
|
|
704
|
+
status: status.runtimeInstalled ? "ok" : "error",
|
|
705
|
+
detail: IS_STANDALONE_BINARY ? process.execPath : paths.runtime,
|
|
706
|
+
});
|
|
707
|
+
for (const agent of status.agents.filter((item) => item.configured)) {
|
|
708
|
+
checks.push({
|
|
709
|
+
name: `${agent.agent}-executable`,
|
|
710
|
+
status: agent.executable ? "ok" : "warning",
|
|
711
|
+
detail: agent.executable ? "found on PATH" : "not found on PATH",
|
|
712
|
+
});
|
|
713
|
+
checks.push({
|
|
714
|
+
name: `${agent.agent}-hooks`,
|
|
715
|
+
status: agent.installedHooks === agent.expectedHooks ? "ok" : "error",
|
|
716
|
+
detail: `${agent.installedHooks}/${agent.expectedHooks} Lore hooks in ${agent.hookFile}`,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
checks.push({
|
|
720
|
+
name: "retry-queue",
|
|
721
|
+
status: status.queuedTurns === 0 ? "ok" : "warning",
|
|
722
|
+
detail: `${status.queuedTurns} queued turn(s)`,
|
|
723
|
+
});
|
|
724
|
+
if (config !== null) {
|
|
725
|
+
checks.push(await apiHealth(config));
|
|
726
|
+
}
|
|
727
|
+
const errors = checks.filter((check) => check.status === "error").length;
|
|
728
|
+
const warnings = checks.filter((check) => check.status === "warning").length;
|
|
729
|
+
const result = { ok: errors === 0, errors, warnings, checks };
|
|
730
|
+
writeResult(result, parsed.json, `${checks.map((check) => `[${check.status.toUpperCase()}] ${check.name}: ${check.detail}`).join("\n")}\nsummary: ${errors} error(s), ${warnings} warning(s)\n`);
|
|
731
|
+
if (errors > 0) {
|
|
732
|
+
process.exitCode = 1;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
export async function runCli(args = process.argv.slice(2)) {
|
|
736
|
+
const command = args[0];
|
|
737
|
+
if (command === undefined || command === "--help" || command === "-h") {
|
|
738
|
+
process.stdout.write(ROOT_HELP);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (command === "--version" || command === "-V") {
|
|
742
|
+
process.stdout.write(`${LORE_VERSION}\n`);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const commandArgs = args.slice(1);
|
|
746
|
+
switch (command) {
|
|
747
|
+
case "connect":
|
|
748
|
+
if (commandArgs[0] === "github") {
|
|
749
|
+
await connectGithub(commandArgs.slice(1));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
await connectCommand(commandArgs);
|
|
753
|
+
return;
|
|
754
|
+
case "github":
|
|
755
|
+
await runGithubCommand(commandArgs);
|
|
756
|
+
return;
|
|
757
|
+
case "devin":
|
|
758
|
+
await runDevinCommand(commandArgs);
|
|
759
|
+
return;
|
|
760
|
+
case "host":
|
|
761
|
+
await runHostCommand(commandArgs);
|
|
762
|
+
return;
|
|
763
|
+
case "status":
|
|
764
|
+
await statusCommand(commandArgs);
|
|
765
|
+
return;
|
|
766
|
+
case "doctor":
|
|
767
|
+
await doctorCommand(commandArgs);
|
|
768
|
+
return;
|
|
769
|
+
case "update":
|
|
770
|
+
await updateCommand(commandArgs);
|
|
771
|
+
return;
|
|
772
|
+
case "disconnect":
|
|
773
|
+
await disconnectCommand(commandArgs);
|
|
774
|
+
return;
|
|
775
|
+
case "hook":
|
|
776
|
+
await runHook(commandArgs);
|
|
777
|
+
return;
|
|
778
|
+
default:
|
|
779
|
+
throw new Error(`Unknown command: ${command}\nTry: lore --help`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const entryPath = process.argv[1];
|
|
783
|
+
if (!IS_STANDALONE_BINARY &&
|
|
784
|
+
entryPath !== undefined &&
|
|
785
|
+
realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(entryPath))) {
|
|
786
|
+
runCli().catch((error) => {
|
|
787
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
788
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
789
|
+
process.exitCode = 1;
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
//# sourceMappingURL=cli.js.map
|