@lore-co/cli 0.2.22 → 0.2.24

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 (73) hide show
  1. package/README.md +109 -13
  2. package/dist/approach-guidance.js +2 -2
  3. package/dist/approach-guidance.js.map +1 -1
  4. package/dist/approach.d.ts +1 -1
  5. package/dist/approach.d.ts.map +1 -1
  6. package/dist/approach.js +7 -0
  7. package/dist/approach.js.map +1 -1
  8. package/dist/ask.d.ts +2 -6
  9. package/dist/ask.d.ts.map +1 -1
  10. package/dist/ask.js +4 -21
  11. package/dist/ask.js.map +1 -1
  12. package/dist/chat.d.ts +15 -0
  13. package/dist/chat.d.ts.map +1 -0
  14. package/dist/chat.js +286 -0
  15. package/dist/chat.js.map +1 -0
  16. package/dist/cli.d.ts +80 -7
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +626 -142
  19. package/dist/cli.js.map +1 -1
  20. package/dist/command-help.d.ts +4 -1
  21. package/dist/command-help.d.ts.map +1 -1
  22. package/dist/command-help.js +9 -0
  23. package/dist/command-help.js.map +1 -1
  24. package/dist/devin-capture.d.ts +6 -0
  25. package/dist/devin-capture.d.ts.map +1 -1
  26. package/dist/devin-capture.js +34 -3
  27. package/dist/devin-capture.js.map +1 -1
  28. package/dist/devin-cli.d.ts +7 -2
  29. package/dist/devin-cli.d.ts.map +1 -1
  30. package/dist/devin-cli.js +81 -8
  31. package/dist/devin-cli.js.map +1 -1
  32. package/dist/generated-assets.d.ts +8 -6
  33. package/dist/generated-assets.d.ts.map +1 -1
  34. package/dist/generated-assets.js +8 -6
  35. package/dist/generated-assets.js.map +1 -1
  36. package/dist/generic-mcp.d.ts +35 -0
  37. package/dist/generic-mcp.d.ts.map +1 -0
  38. package/dist/generic-mcp.js +83 -0
  39. package/dist/generic-mcp.js.map +1 -0
  40. package/dist/learnings.d.ts +20 -0
  41. package/dist/learnings.d.ts.map +1 -1
  42. package/dist/learnings.js +25 -16
  43. package/dist/learnings.js.map +1 -1
  44. package/dist/lore-agent.d.ts +50 -0
  45. package/dist/lore-agent.d.ts.map +1 -0
  46. package/dist/lore-agent.js +299 -0
  47. package/dist/lore-agent.js.map +1 -0
  48. package/dist/mcp-config.d.ts +46 -0
  49. package/dist/mcp-config.d.ts.map +1 -0
  50. package/dist/mcp-config.js +196 -0
  51. package/dist/mcp-config.js.map +1 -0
  52. package/dist/owned-files.d.ts +21 -0
  53. package/dist/owned-files.d.ts.map +1 -0
  54. package/dist/owned-files.js +88 -0
  55. package/dist/owned-files.js.map +1 -0
  56. package/dist/recovery-cloud.d.ts +1 -1
  57. package/dist/review.d.ts +30 -0
  58. package/dist/review.d.ts.map +1 -0
  59. package/dist/review.js +276 -0
  60. package/dist/review.js.map +1 -0
  61. package/dist/runtime.d.ts.map +1 -1
  62. package/dist/runtime.js +50 -19
  63. package/dist/runtime.js.map +1 -1
  64. package/dist/self-host.d.ts +1 -1
  65. package/dist/self-host.js +2 -2
  66. package/dist/skills.d.ts +2 -0
  67. package/dist/skills.d.ts.map +1 -1
  68. package/dist/skills.js +46 -97
  69. package/dist/skills.js.map +1 -1
  70. package/dist/update.d.ts.map +1 -1
  71. package/dist/update.js +39 -4
  72. package/dist/update.js.map +1 -1
  73. package/package.json +7 -4
@@ -0,0 +1,196 @@
1
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
2
+ /**
3
+ * The full Lore MCP server entry that `lore connect` registers next to an
4
+ * agent's native hooks. Hooks deliver context and capture turns on their own;
5
+ * the server gives the agent explicit tools for "remember this", "what did we
6
+ * decide", "share this", and "make this repeatable".
7
+ */
8
+ export const LORE_MCP_SERVER_NAME = "lore";
9
+ /** Agents whose hooks or plugin are paired with the Lore MCP server. */
10
+ export const MCP_HOST_AGENTS = ["claude", "codex", "cursor", "copilot-cli", "copilot-vscode"];
11
+ const CODEX_MCP_START = "# >>> Lore MCP server >>>";
12
+ const CODEX_MCP_END = "# <<< Lore MCP server <<<";
13
+ function isObject(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function stableJson(value) {
17
+ if (Array.isArray(value))
18
+ return `[${value.map(stableJson).join(",")}]`;
19
+ if (isObject(value)) {
20
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
21
+ }
22
+ return JSON.stringify(value);
23
+ }
24
+ /**
25
+ * Build the server entry. `invocation` is the executable plus any script path
26
+ * the CLI needs (empty for the standalone binary); the stored home keeps the
27
+ * server on the same credentials and folder rules as the hooks, and the host
28
+ * name attributes saves whose caller passes no `agent`.
29
+ */
30
+ export function loreMcpServerEntry(invocation, home, agent) {
31
+ const [command, ...prefix] = invocation;
32
+ if (command === undefined)
33
+ throw new Error("The Lore MCP server needs an executable");
34
+ const entry = {
35
+ type: "stdio",
36
+ command,
37
+ args: [...prefix, "mcp", "--owner", "lore"],
38
+ // Writes stay proposals that owners and admins approve; hooks already
39
+ // deliver context per prompt, so the server skips its own delivery tool.
40
+ env: { LORE_HOME: home, LORE_MCP_WRITE_TOOLS: "true", LORE_MCP_HOST_CONTEXT: "hooks", LORE_MCP_AGENT: agent },
41
+ };
42
+ // Copilot CLI calls stdio servers "local" and enables tools by name.
43
+ if (agent === "copilot-cli")
44
+ return { ...entry, type: "local", tools: ["*"] };
45
+ return entry;
46
+ }
47
+ export function isLoreMcpServer(value) {
48
+ return isObject(value) && Array.isArray(value.args) && value.args.includes("mcp")
49
+ && !value.args.includes("cursor-mcp") && value.args.includes("--owner") && value.args.includes("lore");
50
+ }
51
+ /** Merge the entry into a JSON host config that lists servers under `mcpServers`. */
52
+ export function mergeLoreMcpServers(config, entry, agent) {
53
+ if (config.mcpServers !== undefined && !isObject(config.mcpServers)) {
54
+ throw new Error(`${agent} mcpServers must be an object`);
55
+ }
56
+ const servers = isObject(config.mcpServers) ? config.mcpServers : {};
57
+ const existing = servers[LORE_MCP_SERVER_NAME];
58
+ if (existing !== undefined && !isLoreMcpServer(existing)) {
59
+ throw new Error(`${agent} MCP name "${LORE_MCP_SERVER_NAME}" is already owned by another server; rename it before connecting.`);
60
+ }
61
+ return { ...config, mcpServers: { ...servers, [LORE_MCP_SERVER_NAME]: entry } };
62
+ }
63
+ export function removeLoreMcpServers(config) {
64
+ if (!isObject(config.mcpServers) || !isLoreMcpServer(config.mcpServers[LORE_MCP_SERVER_NAME]))
65
+ return config;
66
+ const { [LORE_MCP_SERVER_NAME]: _lore, ...rest } = config.mcpServers;
67
+ return { ...config, mcpServers: rest };
68
+ }
69
+ /**
70
+ * VS Code reads `.vscode/mcp.json` in the repository, with servers under
71
+ * `servers` in the same stdio shape (type, command, args, env) the shared
72
+ * entry already has, so the entry is stored as built.
73
+ */
74
+ export function mergeLoreVsCodeMcp(config, entry) {
75
+ if (config.servers !== undefined && !isObject(config.servers)) {
76
+ throw new Error('VS Code configuration field "servers" must be a JSON object');
77
+ }
78
+ const servers = isObject(config.servers) ? config.servers : {};
79
+ const existing = servers[LORE_MCP_SERVER_NAME];
80
+ if (existing !== undefined && !isLoreMcpServer(existing)) {
81
+ throw new Error(`VS Code MCP name "${LORE_MCP_SERVER_NAME}" is already owned by another server; rename it before connecting.`);
82
+ }
83
+ return { ...config, servers: { ...servers, [LORE_MCP_SERVER_NAME]: entry } };
84
+ }
85
+ export function removeLoreVsCodeMcp(config) {
86
+ if (!isObject(config.servers) || !isLoreMcpServer(config.servers[LORE_MCP_SERVER_NAME]))
87
+ return config;
88
+ const { [LORE_MCP_SERVER_NAME]: _lore, ...rest } = config.servers;
89
+ return { ...config, servers: rest };
90
+ }
91
+ /** The Lore entry in a VS Code `servers` map, or undefined when the map holds none. */
92
+ export function loreVsCodeMcpServer(config) {
93
+ return isObject(config) && isObject(config.servers) ? config.servers[LORE_MCP_SERVER_NAME] : undefined;
94
+ }
95
+ /** OpenCode lists servers under `mcp`, with the executable and arguments as one array. */
96
+ export function openCodeMcpEntry(entry) {
97
+ return {
98
+ type: "local",
99
+ command: [entry.command, ...(Array.isArray(entry.args) ? entry.args : [])],
100
+ environment: entry.env,
101
+ enabled: true,
102
+ };
103
+ }
104
+ export function isLoreOpenCodeMcp(value) {
105
+ return isObject(value) && Array.isArray(value.command) && value.command.includes("mcp")
106
+ && value.command.includes("--owner") && value.command.includes("lore");
107
+ }
108
+ export function mergeLoreOpenCodeMcp(config, entry) {
109
+ if (config.mcp !== undefined && !isObject(config.mcp))
110
+ throw new Error('OpenCode configuration field "mcp" must be a JSON object');
111
+ const servers = isObject(config.mcp) ? config.mcp : {};
112
+ const existing = servers[LORE_MCP_SERVER_NAME];
113
+ if (existing !== undefined && !isLoreOpenCodeMcp(existing)) {
114
+ throw new Error(`OpenCode MCP name "${LORE_MCP_SERVER_NAME}" is already owned by another server; rename it before connecting.`);
115
+ }
116
+ return { ...config, mcp: { ...servers, [LORE_MCP_SERVER_NAME]: openCodeMcpEntry(entry) } };
117
+ }
118
+ export function removeLoreOpenCodeMcp(config) {
119
+ if (!isObject(config.mcp) || !isLoreOpenCodeMcp(config.mcp[LORE_MCP_SERVER_NAME]))
120
+ return config;
121
+ const { [LORE_MCP_SERVER_NAME]: _lore, ...rest } = config.mcp;
122
+ return Object.keys(rest).length === 0 ? Object.fromEntries(Object.entries(config).filter(([key]) => key !== "mcp")) : { ...config, mcp: rest };
123
+ }
124
+ /** Codex lists servers in TOML; it has no `type` field. */
125
+ function codexMcpTable(entry) {
126
+ const { type: _type, ...table } = entry;
127
+ return table;
128
+ }
129
+ function codexMcpBlock(entry) {
130
+ const body = stringifyToml({ mcp_servers: { [LORE_MCP_SERVER_NAME]: codexMcpTable(entry) } }).trim();
131
+ return `${CODEX_MCP_START}\n${body}\n${CODEX_MCP_END}\n`;
132
+ }
133
+ function stripCodexMcpBlocks(text) {
134
+ const lines = text.split("\n");
135
+ const kept = [];
136
+ let blocks = 0;
137
+ let inside = false;
138
+ for (const line of lines) {
139
+ const trimmed = line.trim();
140
+ if (!inside && trimmed === CODEX_MCP_START) {
141
+ inside = true;
142
+ blocks += 1;
143
+ continue;
144
+ }
145
+ if (inside) {
146
+ if (trimmed === CODEX_MCP_END)
147
+ inside = false;
148
+ continue;
149
+ }
150
+ kept.push(line);
151
+ }
152
+ if (inside)
153
+ throw new Error("The Lore block in Codex config.toml has no closing marker; repair it before connecting.");
154
+ return { text: kept.join("\n"), blocks };
155
+ }
156
+ function parseCodexConfig(text) {
157
+ try {
158
+ return parseToml(text);
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ }
164
+ function foreignCodexLoreServer(text, parsed) {
165
+ if (parsed !== null) {
166
+ return isObject(parsed.mcp_servers) && parsed.mcp_servers[LORE_MCP_SERVER_NAME] !== undefined;
167
+ }
168
+ // Unparsable by this CLI: fall back to a textual check so the user's own
169
+ // entry is never shadowed by a second table of the same name.
170
+ return /^\s*\[\s*mcp_servers\.lore\s*[\].]/mu.test(text)
171
+ || (/^\s*\[\s*mcp_servers\s*\]/mu.test(text) && /^\s*lore\s*[.=]/mu.test(text));
172
+ }
173
+ /** Add or refresh the Lore block without rewriting the rest of the user's file. */
174
+ export function mergeLoreCodexMcp(text, entry) {
175
+ const stripped = stripCodexMcpBlocks(text).text;
176
+ if (foreignCodexLoreServer(stripped, parseCodexConfig(stripped))) {
177
+ throw new Error(`Codex MCP server "${LORE_MCP_SERVER_NAME}" is already defined in config.toml; rename it before connecting.`);
178
+ }
179
+ const base = stripped.replace(/\s+$/u, "");
180
+ return `${base === "" ? "" : `${base}\n\n`}${codexMcpBlock(entry)}`;
181
+ }
182
+ export function removeLoreCodexMcp(text) {
183
+ const { text: stripped, blocks } = stripCodexMcpBlocks(text);
184
+ if (blocks === 0)
185
+ return text;
186
+ const base = stripped.replace(/\s+$/u, "");
187
+ return base === "" ? "" : `${base}\n`;
188
+ }
189
+ /** How many Lore blocks the file holds and whether the parsed entry matches. */
190
+ export function inspectCodexMcp(text, entry) {
191
+ const { blocks } = stripCodexMcpBlocks(text);
192
+ const parsed = parseCodexConfig(text);
193
+ const value = parsed !== null && isObject(parsed.mcp_servers) ? parsed.mcp_servers[LORE_MCP_SERVER_NAME] : undefined;
194
+ return { owned: blocks, exact: blocks === 1 && stableJson(value) === stableJson(codexMcpTable(entry)) };
195
+ }
196
+ //# sourceMappingURL=mcp-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-config.js","sourceRoot":"","sources":["../src/mcp-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Files Lore writes into an agent's own directories (skills, agent
3
+ * definitions) and must later remove without touching the customer's edits.
4
+ * Ownership is a manifest of file path to content digest; a file is only
5
+ * rewritten or deleted while its content still matches the recorded digest.
6
+ */
7
+ export declare const digest: (text: string) => string;
8
+ export type OwnedManifest = Record<string, string>;
9
+ /** True when no component of the path is a symlink. Lore never follows links into other files. */
10
+ export declare function regularPath(path: string): Promise<boolean>;
11
+ export declare function readOwnedManifest(file: string, subject: string): Promise<OwnedManifest>;
12
+ export declare function atomicWrite(file: string, text: string): Promise<void>;
13
+ export declare function readIfPresent(file: string): Promise<string | null>;
14
+ /**
15
+ * Delete the file when the manifest owns it and its content is unchanged;
16
+ * then drop an empty parent directory and the manifest entry. Returns false
17
+ * when the file was left alone.
18
+ */
19
+ export declare function removeOwnedFile(file: string, manifest: OwnedManifest, removeEmptyDirectory?: boolean): Promise<boolean>;
20
+ export declare function ownedFileHint(subject: string, file: string, error: unknown, consequence: string): string;
21
+ //# sourceMappingURL=owned-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-files.d.ts","sourceRoot":"","sources":["../src/owned-files.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,KAAG,MAAyD,CAAC;AAEhG,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEnD,kGAAkG;AAClG,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAWhE;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAU7F;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK3E;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKxE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,oBAAoB,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAe1H;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAIxG"}
@@ -0,0 +1,88 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ /**
5
+ * Files Lore writes into an agent's own directories (skills, agent
6
+ * definitions) and must later remove without touching the customer's edits.
7
+ * Ownership is a manifest of file path to content digest; a file is only
8
+ * rewritten or deleted while its content still matches the recorded digest.
9
+ */
10
+ export const digest = (text) => createHash("sha256").update(text).digest("hex");
11
+ /** True when no component of the path is a symlink. Lore never follows links into other files. */
12
+ export async function regularPath(path) {
13
+ let current = path;
14
+ while (dirname(current) !== current) {
15
+ const stat = await lstat(current).catch((error) => {
16
+ if (error.code !== "ENOENT")
17
+ throw error;
18
+ return null;
19
+ });
20
+ if (stat?.isSymbolicLink())
21
+ return false;
22
+ current = dirname(current);
23
+ }
24
+ return true;
25
+ }
26
+ export async function readOwnedManifest(file, subject) {
27
+ if (!await regularPath(file))
28
+ throw new Error(`${subject} ownership file is a symlink.`);
29
+ try {
30
+ const value = JSON.parse(await readFile(file, "utf8"));
31
+ if (!value || typeof value !== "object" || Array.isArray(value))
32
+ throw new Error(`Invalid ${subject.toLowerCase()} ownership file.`);
33
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string" && /^[a-f0-9]{64}$/u.test(entry[1])));
34
+ }
35
+ catch (error) {
36
+ if (error.code === "ENOENT")
37
+ return {};
38
+ throw error;
39
+ }
40
+ }
41
+ export async function atomicWrite(file, text) {
42
+ await mkdir(dirname(file), { recursive: true });
43
+ const temporary = `${file}.${randomUUID()}.tmp`;
44
+ try {
45
+ await writeFile(temporary, text, { flag: "wx", mode: 0o600 });
46
+ await rename(temporary, file);
47
+ }
48
+ finally {
49
+ await rm(temporary, { force: true });
50
+ }
51
+ }
52
+ export async function readIfPresent(file) {
53
+ return readFile(file, "utf8").catch((error) => {
54
+ if (error.code !== "ENOENT")
55
+ throw error;
56
+ return null;
57
+ });
58
+ }
59
+ /**
60
+ * Delete the file when the manifest owns it and its content is unchanged;
61
+ * then drop an empty parent directory and the manifest entry. Returns false
62
+ * when the file was left alone.
63
+ */
64
+ export async function removeOwnedFile(file, manifest, removeEmptyDirectory = true) {
65
+ if (!manifest[file] || !await regularPath(file))
66
+ return false;
67
+ const existing = await readIfPresent(file);
68
+ if (existing !== null && digest(existing) !== manifest[file])
69
+ return false;
70
+ if (existing !== null) {
71
+ await rm(file);
72
+ // Remove only an empty directory; preserve any customer resources.
73
+ if (removeEmptyDirectory) {
74
+ await rmdir(dirname(file)).catch((error) => {
75
+ if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
76
+ throw error;
77
+ });
78
+ }
79
+ }
80
+ delete manifest[file];
81
+ return true;
82
+ }
83
+ export function ownedFileHint(subject, file, error, consequence) {
84
+ const code = error.code;
85
+ const reason = code === "EACCES" || code === "EPERM" ? "permission denied" : (code ?? error.message);
86
+ return `${subject} ${file} (${reason}). ${consequence}`;
87
+ }
88
+ //# sourceMappingURL=owned-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-files.js","sourceRoot":"","sources":["../src/owned-files.ts"],"names":[],"mappings":"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"}
@@ -11,9 +11,9 @@ declare const Runner: z.ZodObject<{
11
11
  agents: z.ZodArray<z.ZodEnum<{
12
12
  claude: "claude";
13
13
  "copilot-cli": "copilot-cli";
14
+ codex: "codex";
14
15
  cursor: "cursor";
15
16
  devin: "devin";
16
- codex: "codex";
17
17
  opencode: "opencode";
18
18
  polytoken: "polytoken";
19
19
  }>>;
@@ -0,0 +1,30 @@
1
+ import type { Memory, ProposalDetailResponse, ReviewProposalResponse } from "@lore-co/core";
2
+ import type { ConnectorConfig } from "./cli.js";
3
+ import { type RepositorySource } from "./learnings.js";
4
+ export type ReviewArguments = {
5
+ command: "list";
6
+ repo?: string;
7
+ allRepositories: boolean;
8
+ json: boolean;
9
+ } | {
10
+ command: "show";
11
+ id: string;
12
+ json: boolean;
13
+ } | {
14
+ command: "approve" | "decline";
15
+ id: string;
16
+ reason?: string;
17
+ json: boolean;
18
+ };
19
+ export declare function parseReviewArguments(args: readonly string[]): ReviewArguments | null;
20
+ export declare function formatProposalList(listing: {
21
+ repo: string | null;
22
+ proposals: readonly Memory[];
23
+ }, repoSource: RepositorySource): string;
24
+ export declare function formatProposal(proposal: ProposalDetailResponse): string;
25
+ export declare function formatReviewResult(command: "approve" | "decline", result: ReviewProposalResponse): string;
26
+ export declare function runReviewCommand(args: readonly string[], config: ConnectorConfig | null, options?: {
27
+ cwd?: string;
28
+ fetcher?: typeof fetch;
29
+ }): Promise<void>;
30
+ //# sourceMappingURL=review.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,sBAAsB,EAEtB,sBAAsB,EACvB,MAAM,eAAe,CAAC;AAOvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAOL,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAwCxB,MAAM,MAAM,eAAe,GACvB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC3E;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC9C;IAAE,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEnF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,GAAG,IAAI,CA2DpF;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAC9D,UAAU,EAAE,gBAAgB,GAC3B,MAAM,CAmBR;AAWD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,sBAAsB,GAAG,MAAM,CAoCvE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAQzG;AAkDD,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,EAAE,eAAe,GAAG,IAAI,EAC9B,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CAAO,GACrD,OAAO,CAAC,IAAI,CAAC,CA+Cf"}
package/dist/review.js ADDED
@@ -0,0 +1,276 @@
1
+ import { LoreTransportError, SharedMemoryApiError, SharedMemoryClient, } from "@lore-co/sdk";
2
+ import { ZodError } from "zod";
3
+ import { formatDate, loadAllLearnings, resolveListingRepository, scopeLabel, singleLine, table, } from "./learnings.js";
4
+ const REVIEW_HELP = `lore review
5
+ Review learning proposals from the terminal: list what is waiting, read one,
6
+ and approve or decline it.
7
+
8
+ Usage:
9
+ lore review [list] [options]
10
+ lore review show <id> [--json]
11
+ lore review approve <id> [--reason <text>] [--json]
12
+ lore review decline <id> [--reason <text>] [--json]
13
+
14
+ Options:
15
+ --repo <repo> List proposals for this repository: its own plus
16
+ workspace-wide ones (default: the Git repository of
17
+ the current folder)
18
+ --all-repositories List every pending proposal in the workspace
19
+ --reason <text> Record why the proposal was approved or declined
20
+ --json Print machine-readable JSON
21
+ --help Show this command's help
22
+
23
+ Proposals come from automatic extraction, lore github harvest, agent saves,
24
+ and members' submissions. Approving activates the learning; declining retires
25
+ it. Only a workspace owner or admin can decide, and the connection must
26
+ identify you: connect with lore connect --login. A proposal with an open
27
+ conflict against an existing learning is decided in the dashboard, where the
28
+ current learning can be replaced or kept alongside.
29
+
30
+ Examples:
31
+ lore review --repo acme/app
32
+ lore review show 6f1c2a3e-...
33
+ lore review approve 6f1c2a3e-... --reason "Matches the deployment runbook."
34
+ lore review decline 6f1c2a3e-... --reason "Superseded by the new lint rule."
35
+ lore review --all-repositories --json
36
+ `;
37
+ const REVIEW_COMMANDS = ["list", "show", "approve", "decline"];
38
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
39
+ export function parseReviewArguments(args) {
40
+ if (args.some((argument) => argument === "--help" || argument === "-h")) {
41
+ process.stdout.write(REVIEW_HELP);
42
+ return null;
43
+ }
44
+ const first = args[0];
45
+ const named = first !== undefined && REVIEW_COMMANDS.includes(first);
46
+ if (first !== undefined && !named && !first.startsWith("-")) {
47
+ throw new Error(`Unknown review command: ${first}\nTry: lore review --help`);
48
+ }
49
+ const command = named ? first : "list";
50
+ const rest = named ? args.slice(1) : args;
51
+ const options = { allRepositories: false, json: false };
52
+ const positionals = [];
53
+ for (let index = 0; index < rest.length; index += 1) {
54
+ const argument = rest[index];
55
+ if (argument === "--json") {
56
+ options.json = true;
57
+ }
58
+ else if (argument === "--all-repositories") {
59
+ options.allRepositories = true;
60
+ }
61
+ else if (argument === "--repo" || argument === "--reason") {
62
+ const value = rest[index + 1]?.trim();
63
+ if (value === undefined || value === "" || value.startsWith("-")) {
64
+ throw new Error(`Missing value for ${argument}`);
65
+ }
66
+ index += 1;
67
+ if (argument === "--repo")
68
+ options.repo = value;
69
+ else
70
+ options.reason = value;
71
+ }
72
+ else if (argument.startsWith("-")) {
73
+ throw new Error(`Unknown review option: ${argument}`);
74
+ }
75
+ else {
76
+ positionals.push(argument);
77
+ }
78
+ }
79
+ if (command === "list") {
80
+ if (positionals.length > 0)
81
+ throw new Error(`Unexpected argument: ${positionals[0]}`);
82
+ if (options.reason !== undefined)
83
+ throw new Error("--reason applies to approve and decline.");
84
+ if (options.repo !== undefined && options.allRepositories) {
85
+ throw new Error("Use --repo or --all-repositories, not both.");
86
+ }
87
+ return {
88
+ command,
89
+ ...(options.repo === undefined ? {} : { repo: options.repo }),
90
+ allRepositories: options.allRepositories,
91
+ json: options.json,
92
+ };
93
+ }
94
+ if (options.repo !== undefined || options.allRepositories) {
95
+ throw new Error(`--repo and --all-repositories apply to lore review list, not ${command}.`);
96
+ }
97
+ const id = positionals[0];
98
+ if (id === undefined)
99
+ throw new Error(`Usage: lore review ${command} <id>`);
100
+ if (positionals.length > 1)
101
+ throw new Error(`Unexpected argument: ${positionals[1]}`);
102
+ if (!UUID.test(id))
103
+ throw new Error(`Not a proposal ID: ${id}. Copy the ID from lore review.`);
104
+ if (command === "show") {
105
+ if (options.reason !== undefined)
106
+ throw new Error("--reason applies to approve and decline.");
107
+ return { command, id, json: options.json };
108
+ }
109
+ return { command, id, ...(options.reason === undefined ? {} : { reason: options.reason }), json: options.json };
110
+ }
111
+ export function formatProposalList(listing, repoSource) {
112
+ const header = repoSource === "all"
113
+ ? "Repository: all repositories in the workspace\n"
114
+ : `Repository: ${listing.repo} (${repoSource === "flag" ? "from --repo" : "detected from the current folder"})\n`;
115
+ const count = listing.proposals.length;
116
+ const summary = `${count} pending proposal${count === 1 ? "" : "s"}\n`;
117
+ if (count === 0)
118
+ return `${header}${summary}`;
119
+ const showScope = repoSource === "all";
120
+ const headers = ["ID", "CATEGORY", "PROPOSED", "AGENT", ...(showScope ? ["SCOPE"] : []), "CONTENT"];
121
+ const rows = listing.proposals.map((memory) => [
122
+ memory.id,
123
+ memory.category,
124
+ formatDate(memory.createdAt),
125
+ memory.source.agent,
126
+ ...(showScope ? [singleLine(scopeLabel(memory), 40)] : []),
127
+ singleLine(memory.content, 100),
128
+ ]);
129
+ return `${header}${summary}\n${table(rows, headers)}\n\nNext: lore review show <id>, then lore review approve <id> or lore review decline <id>.\n`;
130
+ }
131
+ function decisionLabel(decision) {
132
+ switch (decision) {
133
+ case "approve": return "approved";
134
+ case "reject": return "declined";
135
+ case "use_proposal": return "approved, replacing the conflicting learning";
136
+ case "keep_both": return "approved alongside the conflicting learning";
137
+ }
138
+ }
139
+ export function formatProposal(proposal) {
140
+ const { memory, metadata, conflicts, conflictTargets } = proposal;
141
+ const source = [
142
+ memory.source.agent,
143
+ ...(memory.source.sessionId === undefined ? [] : [`session ${memory.source.sessionId}`]),
144
+ ].join(", ");
145
+ const decision = metadata.decision === null
146
+ ? "pending"
147
+ : `${decisionLabel(metadata.decision)} on ${formatDate(metadata.decidedAt ?? metadata.proposedAt)}${metadata.decisionReason === null ? "" : `: ${metadata.decisionReason}`}`;
148
+ const lines = [
149
+ `Proposal ${memory.id}`,
150
+ `Status: ${memory.status}`,
151
+ `Category: ${memory.category}`,
152
+ `Scope: ${scopeLabel(memory)}`,
153
+ `Source: ${source}`,
154
+ `Proposed: ${formatDate(metadata.proposedAt)} (${metadata.reason})`,
155
+ `Decision: ${decision}`,
156
+ "",
157
+ memory.content.trim(),
158
+ ];
159
+ if (conflicts.length > 0) {
160
+ const targets = new Map(conflictTargets.map((target) => [target.id, target]));
161
+ lines.push("", `Conflicts (${conflicts.length}):`);
162
+ for (const conflict of conflicts) {
163
+ const state = conflict.resolution === null ? "open" : decisionLabel(conflict.resolution);
164
+ lines.push(` ${conflict.severity} ${conflict.detector} conflict with ${conflict.targetMemoryId} (${state})`);
165
+ lines.push(` Evidence: ${singleLine(conflict.evidence.summary, 100)}`);
166
+ const target = targets.get(conflict.targetMemoryId);
167
+ if (target !== undefined)
168
+ lines.push(` Current: ${singleLine(target.content, 100)}`);
169
+ }
170
+ if (conflicts.some((conflict) => conflict.resolution === null)) {
171
+ lines.push("", "Open conflicts are decided in the dashboard, where the current learning can be replaced or kept alongside.");
172
+ }
173
+ }
174
+ return `${lines.join("\n")}\n`;
175
+ }
176
+ export function formatReviewResult(command, result) {
177
+ const { proposal } = result;
178
+ if (command === "approve") {
179
+ return proposal.status === "active"
180
+ ? `Approved ${proposal.id}. The learning is now active${proposal.scope.repo === undefined ? " across the workspace" : ` for ${proposal.scope.repo}`}.\n`
181
+ : `Reviewed ${proposal.id}. Learning status: ${proposal.status}.\n`;
182
+ }
183
+ return `Declined ${proposal.id}. The proposal will not become a learning.\n`;
184
+ }
185
+ function detailMessage(error) {
186
+ const details = error.details;
187
+ if (typeof details === "object" && details !== null && "message" in details) {
188
+ const message = details.message;
189
+ if (typeof message === "string")
190
+ return message;
191
+ if (Array.isArray(message))
192
+ return message.map(String).join("; ");
193
+ }
194
+ return typeof details === "string" && details !== "" ? details : undefined;
195
+ }
196
+ function describeFailure(error, id) {
197
+ if (error instanceof SharedMemoryApiError) {
198
+ const detail = detailMessage(error);
199
+ if (error.status === 401) {
200
+ return new Error("Reviewing proposals needs a connection that identifies you. Reconnect with: lore connect --login");
201
+ }
202
+ if (error.status === 403) {
203
+ return new Error(detail ?? "Only a workspace owner or admin can review proposals.");
204
+ }
205
+ if (error.status === 404) {
206
+ return detail !== undefined && /not found/iu.test(detail)
207
+ ? new Error(`Proposal ${id ?? ""} was not found in this workspace. It may already be decided; check lore review.`.replace(" ", " "))
208
+ : new Error("This Lore server does not support reviewing proposals from the CLI. Upgrade the server to use lore review.");
209
+ }
210
+ if (error.status === 409) {
211
+ return new Error(`${detail ?? "Proposal cannot be resolved with this decision"}. Run lore review show ${id ?? "<id>"}: an open conflict or an earlier decision is handled in the dashboard.`);
212
+ }
213
+ if (error.status === 400 && detail !== undefined)
214
+ return new Error(detail);
215
+ return new Error(`Lore review failed with HTTP ${error.status}.`);
216
+ }
217
+ if (error instanceof ZodError ||
218
+ error instanceof SyntaxError ||
219
+ (error instanceof LoreTransportError && error.code === "response_too_large")) {
220
+ return new Error("Lore server returned an incompatible review response. Upgrade the server and CLI.");
221
+ }
222
+ return new Error("Lore API is unreachable. Run lore doctor to diagnose connectivity.");
223
+ }
224
+ export async function runReviewCommand(args, config, options = {}) {
225
+ const parsed = parseReviewArguments(args);
226
+ if (parsed === null)
227
+ return;
228
+ if (config === null) {
229
+ throw new Error("Lore is not connected. Run: lore connect --login");
230
+ }
231
+ const fetcher = options.fetcher ?? fetch;
232
+ const client = new SharedMemoryClient({
233
+ baseUrl: config.apiUrl,
234
+ headers: { authorization: `Bearer ${config.token}` },
235
+ fetch: fetcher,
236
+ });
237
+ if (parsed.command === "list") {
238
+ const { repo, repoSource } = await resolveListingRepository(parsed, config, { ...options, fetcher });
239
+ let proposals;
240
+ try {
241
+ proposals = await loadAllLearnings(client, { ...(repo === undefined ? {} : { repo }), statuses: ["proposed"] });
242
+ }
243
+ catch (error) {
244
+ throw describeFailure(error);
245
+ }
246
+ if (parsed.json) {
247
+ process.stdout.write(`${JSON.stringify({ repo: repo ?? null, total: proposals.length, proposals }, null, 2)}\n`);
248
+ return;
249
+ }
250
+ process.stdout.write(formatProposalList({ repo: repo ?? null, proposals }, repoSource));
251
+ return;
252
+ }
253
+ if (parsed.command === "show") {
254
+ let proposal;
255
+ try {
256
+ proposal = await client.getProposal(parsed.id);
257
+ }
258
+ catch (error) {
259
+ throw describeFailure(error, parsed.id);
260
+ }
261
+ process.stdout.write(parsed.json ? `${JSON.stringify(proposal, null, 2)}\n` : formatProposal(proposal));
262
+ return;
263
+ }
264
+ let result;
265
+ try {
266
+ result = await client.reviewProposal(parsed.id, {
267
+ decision: parsed.command === "approve" ? "approve" : "reject",
268
+ ...(parsed.reason === undefined ? {} : { reason: parsed.reason }),
269
+ });
270
+ }
271
+ catch (error) {
272
+ throw describeFailure(error, parsed.id);
273
+ }
274
+ process.stdout.write(parsed.json ? `${JSON.stringify(result, null, 2)}\n` : formatReviewResult(parsed.command, result));
275
+ }
276
+ //# sourceMappingURL=review.js.map