mulch-cli 0.5.0 → 0.6.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 (196) hide show
  1. package/README.md +12 -1
  2. package/package.json +11 -16
  3. package/src/api.ts +310 -0
  4. package/src/cli.ts +54 -0
  5. package/src/commands/add.ts +61 -0
  6. package/src/commands/compact.ts +924 -0
  7. package/src/commands/delete.ts +103 -0
  8. package/src/commands/diff.ts +209 -0
  9. package/src/commands/doctor.ts +586 -0
  10. package/src/commands/edit.ts +253 -0
  11. package/src/commands/init.ts +33 -0
  12. package/src/commands/learn.ts +170 -0
  13. package/src/commands/onboard.ts +362 -0
  14. package/src/commands/prime.ts +327 -0
  15. package/src/commands/prune.ts +128 -0
  16. package/src/commands/query.ts +177 -0
  17. package/src/commands/ready.ts +194 -0
  18. package/src/commands/record.ts +959 -0
  19. package/src/commands/search.ts +234 -0
  20. package/src/commands/setup.ts +823 -0
  21. package/src/commands/status.ts +83 -0
  22. package/src/commands/sync.ts +224 -0
  23. package/src/commands/update.ts +112 -0
  24. package/src/commands/validate.ts +107 -0
  25. package/src/index.ts +50 -0
  26. package/src/schemas/config.ts +31 -0
  27. package/src/schemas/index.ts +18 -0
  28. package/src/schemas/record-schema.ts +177 -0
  29. package/src/schemas/record.ts +83 -0
  30. package/src/utils/bm25.ts +243 -0
  31. package/src/utils/budget.ts +157 -0
  32. package/src/utils/config.ts +117 -0
  33. package/src/utils/expertise.ts +379 -0
  34. package/src/utils/format.ts +767 -0
  35. package/src/utils/git.ts +89 -0
  36. package/src/utils/index.ts +54 -0
  37. package/src/utils/json-output.ts +13 -0
  38. package/src/utils/lock.ts +82 -0
  39. package/src/utils/markers.ts +51 -0
  40. package/{dist/utils/scoring.d.ts → src/utils/scoring.ts} +53 -9
  41. package/src/utils/version.ts +46 -0
  42. package/dist/api.d.ts +0 -65
  43. package/dist/api.d.ts.map +0 -1
  44. package/dist/api.js +0 -196
  45. package/dist/api.js.map +0 -1
  46. package/dist/cli.d.ts +0 -3
  47. package/dist/cli.d.ts.map +0 -1
  48. package/dist/cli.js +0 -50
  49. package/dist/cli.js.map +0 -1
  50. package/dist/commands/add.d.ts +0 -3
  51. package/dist/commands/add.d.ts.map +0 -1
  52. package/dist/commands/add.js +0 -47
  53. package/dist/commands/add.js.map +0 -1
  54. package/dist/commands/compact.d.ts +0 -5
  55. package/dist/commands/compact.d.ts.map +0 -1
  56. package/dist/commands/compact.js +0 -709
  57. package/dist/commands/compact.js.map +0 -1
  58. package/dist/commands/delete.d.ts +0 -3
  59. package/dist/commands/delete.d.ts.map +0 -1
  60. package/dist/commands/delete.js +0 -82
  61. package/dist/commands/delete.js.map +0 -1
  62. package/dist/commands/diff.d.ts +0 -11
  63. package/dist/commands/diff.d.ts.map +0 -1
  64. package/dist/commands/diff.js +0 -170
  65. package/dist/commands/diff.js.map +0 -1
  66. package/dist/commands/doctor.d.ts +0 -3
  67. package/dist/commands/doctor.d.ts.map +0 -1
  68. package/dist/commands/doctor.js +0 -391
  69. package/dist/commands/doctor.js.map +0 -1
  70. package/dist/commands/edit.d.ts +0 -3
  71. package/dist/commands/edit.d.ts.map +0 -1
  72. package/dist/commands/edit.js +0 -198
  73. package/dist/commands/edit.js.map +0 -1
  74. package/dist/commands/init.d.ts +0 -3
  75. package/dist/commands/init.d.ts.map +0 -1
  76. package/dist/commands/init.js +0 -30
  77. package/dist/commands/init.js.map +0 -1
  78. package/dist/commands/learn.d.ts +0 -12
  79. package/dist/commands/learn.d.ts.map +0 -1
  80. package/dist/commands/learn.js +0 -130
  81. package/dist/commands/learn.js.map +0 -1
  82. package/dist/commands/onboard.d.ts +0 -10
  83. package/dist/commands/onboard.d.ts.map +0 -1
  84. package/dist/commands/onboard.js +0 -286
  85. package/dist/commands/onboard.js.map +0 -1
  86. package/dist/commands/prime.d.ts +0 -3
  87. package/dist/commands/prime.d.ts.map +0 -1
  88. package/dist/commands/prime.js +0 -242
  89. package/dist/commands/prime.js.map +0 -1
  90. package/dist/commands/prune.d.ts +0 -8
  91. package/dist/commands/prune.d.ts.map +0 -1
  92. package/dist/commands/prune.js +0 -90
  93. package/dist/commands/prune.js.map +0 -1
  94. package/dist/commands/query.d.ts +0 -3
  95. package/dist/commands/query.d.ts.map +0 -1
  96. package/dist/commands/query.js +0 -133
  97. package/dist/commands/query.js.map +0 -1
  98. package/dist/commands/ready.d.ts +0 -3
  99. package/dist/commands/ready.d.ts.map +0 -1
  100. package/dist/commands/ready.js +0 -160
  101. package/dist/commands/ready.js.map +0 -1
  102. package/dist/commands/record.d.ts +0 -13
  103. package/dist/commands/record.d.ts.map +0 -1
  104. package/dist/commands/record.js +0 -689
  105. package/dist/commands/record.js.map +0 -1
  106. package/dist/commands/search.d.ts +0 -3
  107. package/dist/commands/search.d.ts.map +0 -1
  108. package/dist/commands/search.js +0 -163
  109. package/dist/commands/search.js.map +0 -1
  110. package/dist/commands/setup.d.ts +0 -29
  111. package/dist/commands/setup.d.ts.map +0 -1
  112. package/dist/commands/setup.js +0 -548
  113. package/dist/commands/setup.js.map +0 -1
  114. package/dist/commands/status.d.ts +0 -3
  115. package/dist/commands/status.d.ts.map +0 -1
  116. package/dist/commands/status.js +0 -61
  117. package/dist/commands/status.js.map +0 -1
  118. package/dist/commands/sync.d.ts +0 -3
  119. package/dist/commands/sync.d.ts.map +0 -1
  120. package/dist/commands/sync.js +0 -176
  121. package/dist/commands/sync.js.map +0 -1
  122. package/dist/commands/update.d.ts +0 -3
  123. package/dist/commands/update.d.ts.map +0 -1
  124. package/dist/commands/update.js +0 -72
  125. package/dist/commands/update.js.map +0 -1
  126. package/dist/commands/validate.d.ts +0 -3
  127. package/dist/commands/validate.d.ts.map +0 -1
  128. package/dist/commands/validate.js +0 -86
  129. package/dist/commands/validate.js.map +0 -1
  130. package/dist/index.d.ts +0 -9
  131. package/dist/index.d.ts.map +0 -1
  132. package/dist/index.js +0 -10
  133. package/dist/index.js.map +0 -1
  134. package/dist/schemas/config.d.ts +0 -17
  135. package/dist/schemas/config.d.ts.map +0 -1
  136. package/dist/schemas/config.js +0 -16
  137. package/dist/schemas/config.js.map +0 -1
  138. package/dist/schemas/index.d.ts +0 -5
  139. package/dist/schemas/index.d.ts.map +0 -1
  140. package/dist/schemas/index.js +0 -3
  141. package/dist/schemas/index.js.map +0 -1
  142. package/dist/schemas/record-schema.d.ts +0 -403
  143. package/dist/schemas/record-schema.d.ts.map +0 -1
  144. package/dist/schemas/record-schema.js +0 -150
  145. package/dist/schemas/record-schema.js.map +0 -1
  146. package/dist/schemas/record.d.ts +0 -62
  147. package/dist/schemas/record.d.ts.map +0 -1
  148. package/dist/schemas/record.js +0 -2
  149. package/dist/schemas/record.js.map +0 -1
  150. package/dist/utils/bm25.d.ts +0 -39
  151. package/dist/utils/bm25.d.ts.map +0 -1
  152. package/dist/utils/bm25.js +0 -171
  153. package/dist/utils/bm25.js.map +0 -1
  154. package/dist/utils/budget.d.ts +0 -35
  155. package/dist/utils/budget.d.ts.map +0 -1
  156. package/dist/utils/budget.js +0 -114
  157. package/dist/utils/budget.js.map +0 -1
  158. package/dist/utils/config.d.ts +0 -12
  159. package/dist/utils/config.d.ts.map +0 -1
  160. package/dist/utils/config.js +0 -89
  161. package/dist/utils/config.js.map +0 -1
  162. package/dist/utils/expertise.d.ts +0 -57
  163. package/dist/utils/expertise.d.ts.map +0 -1
  164. package/dist/utils/expertise.js +0 -276
  165. package/dist/utils/expertise.js.map +0 -1
  166. package/dist/utils/format.d.ts +0 -31
  167. package/dist/utils/format.d.ts.map +0 -1
  168. package/dist/utils/format.js +0 -562
  169. package/dist/utils/format.js.map +0 -1
  170. package/dist/utils/git.d.ts +0 -6
  171. package/dist/utils/git.d.ts.map +0 -1
  172. package/dist/utils/git.js +0 -81
  173. package/dist/utils/git.js.map +0 -1
  174. package/dist/utils/index.d.ts +0 -8
  175. package/dist/utils/index.d.ts.map +0 -1
  176. package/dist/utils/index.js +0 -8
  177. package/dist/utils/index.js.map +0 -1
  178. package/dist/utils/json-output.d.ts +0 -8
  179. package/dist/utils/json-output.d.ts.map +0 -1
  180. package/dist/utils/json-output.js +0 -7
  181. package/dist/utils/json-output.js.map +0 -1
  182. package/dist/utils/lock.d.ts +0 -6
  183. package/dist/utils/lock.d.ts.map +0 -1
  184. package/dist/utils/lock.js +0 -70
  185. package/dist/utils/lock.js.map +0 -1
  186. package/dist/utils/markers.d.ts +0 -22
  187. package/dist/utils/markers.d.ts.map +0 -1
  188. package/dist/utils/markers.js +0 -42
  189. package/dist/utils/markers.js.map +0 -1
  190. package/dist/utils/scoring.d.ts.map +0 -1
  191. package/dist/utils/scoring.js +0 -80
  192. package/dist/utils/scoring.js.map +0 -1
  193. package/dist/utils/version.d.ts +0 -15
  194. package/dist/utils/version.d.ts.map +0 -1
  195. package/dist/utils/version.js +0 -48
  196. package/dist/utils/version.js.map +0 -1
@@ -0,0 +1,253 @@
1
+ import Ajv from "ajv";
2
+ import chalk from "chalk";
3
+ import { type Command, Option } from "commander";
4
+ import { recordSchema } from "../schemas/record-schema.ts";
5
+ import type { Classification, Outcome } from "../schemas/record.ts";
6
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
7
+ import {
8
+ readExpertiseFile,
9
+ resolveRecordId,
10
+ writeExpertiseFile,
11
+ } from "../utils/expertise.ts";
12
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
13
+ import { withFileLock } from "../utils/lock.ts";
14
+
15
+ export function registerEditCommand(program: Command): void {
16
+ program
17
+ .command("edit")
18
+ .argument("<domain>", "expertise domain")
19
+ .argument("<id>", "record ID (e.g. mx-abc123, abc123, or abc)")
20
+ .description("Edit an existing expertise record")
21
+ .addOption(
22
+ new Option(
23
+ "--classification <classification>",
24
+ "update classification",
25
+ ).choices(["foundational", "tactical", "observational"]),
26
+ )
27
+ .option("--content <content>", "update content (convention)")
28
+ .option("--name <name>", "update name (pattern)")
29
+ .option("--description <description>", "update description")
30
+ .option("--resolution <resolution>", "update resolution (failure)")
31
+ .option("--title <title>", "update title (decision)")
32
+ .option("--rationale <rationale>", "update rationale (decision)")
33
+ .option("--files <files>", "update related files (comma-separated)")
34
+ .option("--relates-to <ids>", "update linked record IDs (comma-separated)")
35
+ .option(
36
+ "--supersedes <ids>",
37
+ "update superseded record IDs (comma-separated)",
38
+ )
39
+ .addOption(
40
+ new Option("--outcome-status <status>", "set outcome status").choices([
41
+ "success",
42
+ "failure",
43
+ "partial",
44
+ ]),
45
+ )
46
+ .option("--outcome-duration <ms>", "set outcome duration in milliseconds")
47
+ .option("--outcome-test-results <text>", "set outcome test results summary")
48
+ .option("--outcome-agent <agent>", "set outcome agent name")
49
+ .action(
50
+ async (domain: string, id: string, options: Record<string, unknown>) => {
51
+ const jsonMode = program.opts().json === true;
52
+ try {
53
+ const config = await readConfig();
54
+
55
+ if (!config.domains.includes(domain)) {
56
+ if (jsonMode) {
57
+ outputJsonError(
58
+ "edit",
59
+ `Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`,
60
+ );
61
+ } else {
62
+ console.error(
63
+ chalk.red(`Error: domain "${domain}" not found in config.`),
64
+ );
65
+ console.error(
66
+ chalk.red(
67
+ `Available domains: ${config.domains.join(", ") || "(none)"}`,
68
+ ),
69
+ );
70
+ }
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+
75
+ const filePath = getExpertisePath(domain);
76
+ await withFileLock(filePath, async () => {
77
+ const records = await readExpertiseFile(filePath);
78
+
79
+ const resolved = resolveRecordId(records, id);
80
+ if (!resolved.ok) {
81
+ if (jsonMode) {
82
+ outputJsonError("edit", resolved.error);
83
+ } else {
84
+ console.error(chalk.red(`Error: ${resolved.error}`));
85
+ }
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ const targetIndex = resolved.index;
90
+
91
+ const record = { ...records[targetIndex] };
92
+
93
+ // Apply updates based on record type
94
+ if (options.classification) {
95
+ record.classification = options.classification as Classification;
96
+ }
97
+ if (typeof options.relatesTo === "string") {
98
+ record.relates_to = options.relatesTo
99
+ .split(",")
100
+ .map((id: string) => id.trim())
101
+ .filter(Boolean);
102
+ }
103
+ if (typeof options.supersedes === "string") {
104
+ record.supersedes = options.supersedes
105
+ .split(",")
106
+ .map((id: string) => id.trim())
107
+ .filter(Boolean);
108
+ }
109
+ if (options.outcomeStatus) {
110
+ const o: Outcome = {
111
+ status: options.outcomeStatus as
112
+ | "success"
113
+ | "failure"
114
+ | "partial",
115
+ };
116
+ if (options.outcomeDuration !== undefined) {
117
+ o.duration = Number.parseFloat(
118
+ options.outcomeDuration as string,
119
+ );
120
+ }
121
+ if (options.outcomeTestResults) {
122
+ o.test_results = options.outcomeTestResults as string;
123
+ }
124
+ if (options.outcomeAgent) {
125
+ o.agent = options.outcomeAgent as string;
126
+ }
127
+ record.outcomes = [...(record.outcomes ?? []), o];
128
+ }
129
+
130
+ switch (record.type) {
131
+ case "convention":
132
+ if (options.content) {
133
+ record.content = options.content as string;
134
+ }
135
+ break;
136
+ case "pattern":
137
+ if (options.name) {
138
+ record.name = options.name as string;
139
+ }
140
+ if (options.description) {
141
+ record.description = options.description as string;
142
+ }
143
+ if (typeof options.files === "string") {
144
+ record.files = (options.files as string).split(",");
145
+ }
146
+ break;
147
+ case "failure":
148
+ if (options.description) {
149
+ record.description = options.description as string;
150
+ }
151
+ if (options.resolution) {
152
+ record.resolution = options.resolution as string;
153
+ }
154
+ break;
155
+ case "decision":
156
+ if (options.title) {
157
+ record.title = options.title as string;
158
+ }
159
+ if (options.rationale) {
160
+ record.rationale = options.rationale as string;
161
+ }
162
+ break;
163
+ case "reference":
164
+ if (options.name) {
165
+ record.name = options.name as string;
166
+ }
167
+ if (options.description) {
168
+ record.description = options.description as string;
169
+ }
170
+ if (typeof options.files === "string") {
171
+ record.files = (options.files as string).split(",");
172
+ }
173
+ break;
174
+ case "guide":
175
+ if (options.name) {
176
+ record.name = options.name as string;
177
+ }
178
+ if (options.description) {
179
+ record.description = options.description as string;
180
+ }
181
+ break;
182
+ }
183
+
184
+ // Validate the updated record
185
+ const ajv = new Ajv();
186
+ const validate = ajv.compile(recordSchema);
187
+ if (!validate(record)) {
188
+ const errors = (validate.errors ?? []).map(
189
+ (err) => `${err.instancePath} ${err.message}`,
190
+ );
191
+ if (jsonMode) {
192
+ outputJsonError(
193
+ "edit",
194
+ `Updated record failed schema validation: ${errors.join("; ")}`,
195
+ );
196
+ } else {
197
+ console.error(
198
+ chalk.red("Error: updated record failed schema validation:"),
199
+ );
200
+ for (const err of validate.errors ?? []) {
201
+ console.error(
202
+ chalk.red(` ${err.instancePath} ${err.message}`),
203
+ );
204
+ }
205
+ }
206
+ process.exitCode = 1;
207
+ return;
208
+ }
209
+
210
+ records[targetIndex] = record;
211
+ await writeExpertiseFile(filePath, records);
212
+
213
+ if (jsonMode) {
214
+ outputJson({
215
+ success: true,
216
+ command: "edit",
217
+ domain,
218
+ id: record.id ?? null,
219
+ type: record.type,
220
+ record,
221
+ });
222
+ } else {
223
+ console.log(
224
+ chalk.green(
225
+ `\u2714 Updated ${record.type} ${record.id ?? ""} in ${domain}`,
226
+ ),
227
+ );
228
+ }
229
+ });
230
+ } catch (err) {
231
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
232
+ if (jsonMode) {
233
+ outputJsonError(
234
+ "edit",
235
+ "No .mulch/ directory found. Run `mulch init` first.",
236
+ );
237
+ } else {
238
+ console.error(
239
+ "Error: No .mulch/ directory found. Run `mulch init` first.",
240
+ );
241
+ }
242
+ } else {
243
+ if (jsonMode) {
244
+ outputJsonError("edit", (err as Error).message);
245
+ } else {
246
+ console.error(`Error: ${(err as Error).message}`);
247
+ }
248
+ }
249
+ process.exitCode = 1;
250
+ }
251
+ },
252
+ );
253
+ }
@@ -0,0 +1,33 @@
1
+ import { existsSync } from "node:fs";
2
+ import chalk from "chalk";
3
+ import type { Command } from "commander";
4
+ import { getMulchDir, initMulchDir } from "../utils/config.ts";
5
+ import { outputJson } from "../utils/json-output.ts";
6
+
7
+ export function registerInitCommand(program: Command): void {
8
+ program
9
+ .command("init")
10
+ .description("Initialize .mulch/ in the current project")
11
+ .action(async () => {
12
+ const jsonMode = program.opts().json === true;
13
+ const mulchDir = getMulchDir();
14
+ const alreadyExists = existsSync(mulchDir);
15
+
16
+ await initMulchDir();
17
+
18
+ if (jsonMode) {
19
+ outputJson({
20
+ success: true,
21
+ command: "init",
22
+ created: !alreadyExists,
23
+ path: mulchDir,
24
+ });
25
+ } else if (alreadyExists) {
26
+ console.log(
27
+ chalk.green("Updated .mulch/ — filled in any missing artifacts."),
28
+ );
29
+ } else {
30
+ console.log(chalk.green(`Initialized .mulch/ in ${process.cwd()}`));
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,170 @@
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
4
+ import { readExpertiseFile } from "../utils/expertise.ts";
5
+ import { getChangedFiles, isGitRepo } from "../utils/git.ts";
6
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
7
+
8
+ interface DomainMatch {
9
+ domain: string;
10
+ matchedFiles: string[];
11
+ }
12
+
13
+ export async function matchFilesToDomains(
14
+ changedFiles: string[],
15
+ cwd?: string,
16
+ ): Promise<{ matches: DomainMatch[]; unmatched: string[] }> {
17
+ const config = await readConfig(cwd);
18
+ const matched = new Set<string>();
19
+ const matches: DomainMatch[] = [];
20
+
21
+ for (const domain of config.domains) {
22
+ const filePath = getExpertisePath(domain, cwd);
23
+ const records = await readExpertiseFile(filePath);
24
+
25
+ // Collect all file paths from pattern and reference records
26
+ const domainFiles = new Set<string>();
27
+ for (const record of records) {
28
+ if (
29
+ (record.type === "pattern" || record.type === "reference") &&
30
+ record.files
31
+ ) {
32
+ for (const f of record.files) {
33
+ domainFiles.add(f);
34
+ }
35
+ }
36
+ }
37
+
38
+ // Match changed files against domain file paths
39
+ const domainMatched: string[] = [];
40
+ for (const changedFile of changedFiles) {
41
+ for (const domainFile of domainFiles) {
42
+ if (
43
+ changedFile === domainFile ||
44
+ changedFile.endsWith(domainFile) ||
45
+ domainFile.endsWith(changedFile)
46
+ ) {
47
+ domainMatched.push(changedFile);
48
+ matched.add(changedFile);
49
+ break;
50
+ }
51
+ }
52
+ }
53
+
54
+ if (domainMatched.length > 0) {
55
+ matches.push({ domain, matchedFiles: domainMatched });
56
+ }
57
+ }
58
+
59
+ // Sort by match count descending
60
+ matches.sort((a, b) => b.matchedFiles.length - a.matchedFiles.length);
61
+
62
+ const unmatched = changedFiles.filter((f) => !matched.has(f));
63
+ return { matches, unmatched };
64
+ }
65
+
66
+ export function registerLearnCommand(program: Command): void {
67
+ program
68
+ .command("learn")
69
+ .description(
70
+ "Show changed files and suggest domains for recording learnings",
71
+ )
72
+ .option("--since <ref>", "git ref to diff against", "HEAD~1")
73
+ .action(async (options: { since: string }) => {
74
+ const jsonMode = program.opts().json === true;
75
+ const cwd = process.cwd();
76
+
77
+ if (!isGitRepo(cwd)) {
78
+ if (jsonMode) {
79
+ outputJsonError(
80
+ "learn",
81
+ "Not in a git repository. Run this command from within a git repository.",
82
+ );
83
+ } else {
84
+ console.error(chalk.red("Error: not in a git repository."));
85
+ }
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+
90
+ try {
91
+ const changedFiles = getChangedFiles(cwd, options.since);
92
+
93
+ if (changedFiles.length === 0) {
94
+ if (jsonMode) {
95
+ outputJson({
96
+ success: true,
97
+ command: "learn",
98
+ changedFiles: [],
99
+ suggestedDomains: [],
100
+ unmatchedFiles: [],
101
+ message: "No changed files found",
102
+ });
103
+ } else {
104
+ console.log("No changed files found. Nothing to learn from.");
105
+ }
106
+ return;
107
+ }
108
+
109
+ const { matches, unmatched } = await matchFilesToDomains(changedFiles);
110
+
111
+ if (jsonMode) {
112
+ outputJson({
113
+ success: true,
114
+ command: "learn",
115
+ changedFiles,
116
+ suggestedDomains: matches.map((m) => ({
117
+ domain: m.domain,
118
+ matchCount: m.matchedFiles.length,
119
+ files: m.matchedFiles,
120
+ })),
121
+ unmatchedFiles: unmatched,
122
+ });
123
+ return;
124
+ }
125
+
126
+ // Plain text output
127
+ console.log(chalk.bold("\nSession learnings check\n"));
128
+
129
+ console.log(chalk.cyan(`Changed files (${changedFiles.length}):`));
130
+ for (const f of changedFiles) {
131
+ console.log(` ${f}`);
132
+ }
133
+
134
+ if (matches.length > 0) {
135
+ console.log(chalk.cyan("\nSuggested domains:"));
136
+ for (const m of matches) {
137
+ const label =
138
+ m.matchedFiles.length === 1 ? "file matches" : "files match";
139
+ console.log(
140
+ ` ${chalk.bold(m.domain)} (${m.matchedFiles.length} ${label} existing records)`,
141
+ );
142
+ }
143
+ }
144
+
145
+ if (unmatched.length > 0) {
146
+ console.log(
147
+ chalk.yellow("\nUnmatched files (no domain association):"),
148
+ );
149
+ for (const f of unmatched) {
150
+ console.log(` ${f}`);
151
+ }
152
+ }
153
+
154
+ console.log(chalk.dim("\nRecord learnings with:"));
155
+ console.log(
156
+ chalk.dim(
157
+ ' mulch record <domain> --type <type> --description "..."',
158
+ ),
159
+ );
160
+ console.log();
161
+ } catch (err) {
162
+ if (jsonMode) {
163
+ outputJsonError("learn", (err as Error).message);
164
+ } else {
165
+ console.error(`Error: ${(err as Error).message}`);
166
+ }
167
+ process.exitCode = 1;
168
+ }
169
+ });
170
+ }