@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.
Files changed (55) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +218 -0
  3. package/dist/cli.d.ts +28 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +792 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/devin-client.d.ts +63 -0
  8. package/dist/devin-client.d.ts.map +1 -0
  9. package/dist/devin-client.js +231 -0
  10. package/dist/devin-client.js.map +1 -0
  11. package/dist/devin.d.ts +2 -0
  12. package/dist/devin.d.ts.map +1 -0
  13. package/dist/devin.js +479 -0
  14. package/dist/devin.js.map +1 -0
  15. package/dist/generated-assets.d.ts +7 -0
  16. package/dist/generated-assets.d.ts.map +1 -0
  17. package/dist/generated-assets.js +8 -0
  18. package/dist/generated-assets.js.map +1 -0
  19. package/dist/github.d.ts +13 -0
  20. package/dist/github.d.ts.map +1 -0
  21. package/dist/github.js +559 -0
  22. package/dist/github.js.map +1 -0
  23. package/dist/host.d.ts +24 -0
  24. package/dist/host.d.ts.map +1 -0
  25. package/dist/host.js +337 -0
  26. package/dist/host.js.map +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +8 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/main.d.ts +2 -0
  32. package/dist/main.d.ts.map +1 -0
  33. package/dist/main.js +10 -0
  34. package/dist/main.js.map +1 -0
  35. package/dist/repository.d.ts +3 -0
  36. package/dist/repository.d.ts.map +1 -0
  37. package/dist/repository.js +60 -0
  38. package/dist/repository.js.map +1 -0
  39. package/dist/review-output.d.ts +59 -0
  40. package/dist/review-output.d.ts.map +1 -0
  41. package/dist/review-output.js +112 -0
  42. package/dist/review-output.js.map +1 -0
  43. package/dist/runtime.d.ts +63 -0
  44. package/dist/runtime.d.ts.map +1 -0
  45. package/dist/runtime.js +612 -0
  46. package/dist/runtime.js.map +1 -0
  47. package/dist/update.d.ts +2 -0
  48. package/dist/update.d.ts.map +1 -0
  49. package/dist/update.js +97 -0
  50. package/dist/update.js.map +1 -0
  51. package/dist/version.d.ts +3 -0
  52. package/dist/version.d.ts.map +1 -0
  53. package/dist/version.js +6 -0
  54. package/dist/version.js.map +1 -0
  55. 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