mulch-cli 0.4.3 → 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 (193) hide show
  1. package/README.md +24 -4
  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/src/utils/scoring.ts +101 -0
  41. package/src/utils/version.ts +46 -0
  42. package/dist/cli.d.ts +0 -3
  43. package/dist/cli.d.ts.map +0 -1
  44. package/dist/cli.js +0 -50
  45. package/dist/cli.js.map +0 -1
  46. package/dist/commands/add.d.ts +0 -3
  47. package/dist/commands/add.d.ts.map +0 -1
  48. package/dist/commands/add.js +0 -47
  49. package/dist/commands/add.js.map +0 -1
  50. package/dist/commands/compact.d.ts +0 -5
  51. package/dist/commands/compact.d.ts.map +0 -1
  52. package/dist/commands/compact.js +0 -709
  53. package/dist/commands/compact.js.map +0 -1
  54. package/dist/commands/delete.d.ts +0 -3
  55. package/dist/commands/delete.d.ts.map +0 -1
  56. package/dist/commands/delete.js +0 -82
  57. package/dist/commands/delete.js.map +0 -1
  58. package/dist/commands/diff.d.ts +0 -11
  59. package/dist/commands/diff.d.ts.map +0 -1
  60. package/dist/commands/diff.js +0 -170
  61. package/dist/commands/diff.js.map +0 -1
  62. package/dist/commands/doctor.d.ts +0 -3
  63. package/dist/commands/doctor.d.ts.map +0 -1
  64. package/dist/commands/doctor.js +0 -391
  65. package/dist/commands/doctor.js.map +0 -1
  66. package/dist/commands/edit.d.ts +0 -3
  67. package/dist/commands/edit.d.ts.map +0 -1
  68. package/dist/commands/edit.js +0 -210
  69. package/dist/commands/edit.js.map +0 -1
  70. package/dist/commands/init.d.ts +0 -3
  71. package/dist/commands/init.d.ts.map +0 -1
  72. package/dist/commands/init.js +0 -30
  73. package/dist/commands/init.js.map +0 -1
  74. package/dist/commands/learn.d.ts +0 -12
  75. package/dist/commands/learn.d.ts.map +0 -1
  76. package/dist/commands/learn.js +0 -130
  77. package/dist/commands/learn.js.map +0 -1
  78. package/dist/commands/onboard.d.ts +0 -10
  79. package/dist/commands/onboard.d.ts.map +0 -1
  80. package/dist/commands/onboard.js +0 -286
  81. package/dist/commands/onboard.js.map +0 -1
  82. package/dist/commands/prime.d.ts +0 -3
  83. package/dist/commands/prime.d.ts.map +0 -1
  84. package/dist/commands/prime.js +0 -242
  85. package/dist/commands/prime.js.map +0 -1
  86. package/dist/commands/prune.d.ts +0 -8
  87. package/dist/commands/prune.d.ts.map +0 -1
  88. package/dist/commands/prune.js +0 -90
  89. package/dist/commands/prune.js.map +0 -1
  90. package/dist/commands/query.d.ts +0 -3
  91. package/dist/commands/query.d.ts.map +0 -1
  92. package/dist/commands/query.js +0 -118
  93. package/dist/commands/query.js.map +0 -1
  94. package/dist/commands/ready.d.ts +0 -3
  95. package/dist/commands/ready.d.ts.map +0 -1
  96. package/dist/commands/ready.js +0 -160
  97. package/dist/commands/ready.js.map +0 -1
  98. package/dist/commands/record.d.ts +0 -13
  99. package/dist/commands/record.d.ts.map +0 -1
  100. package/dist/commands/record.js +0 -688
  101. package/dist/commands/record.js.map +0 -1
  102. package/dist/commands/search.d.ts +0 -3
  103. package/dist/commands/search.d.ts.map +0 -1
  104. package/dist/commands/search.js +0 -163
  105. package/dist/commands/search.js.map +0 -1
  106. package/dist/commands/setup.d.ts +0 -29
  107. package/dist/commands/setup.d.ts.map +0 -1
  108. package/dist/commands/setup.js +0 -548
  109. package/dist/commands/setup.js.map +0 -1
  110. package/dist/commands/status.d.ts +0 -3
  111. package/dist/commands/status.d.ts.map +0 -1
  112. package/dist/commands/status.js +0 -61
  113. package/dist/commands/status.js.map +0 -1
  114. package/dist/commands/sync.d.ts +0 -3
  115. package/dist/commands/sync.d.ts.map +0 -1
  116. package/dist/commands/sync.js +0 -176
  117. package/dist/commands/sync.js.map +0 -1
  118. package/dist/commands/update.d.ts +0 -3
  119. package/dist/commands/update.d.ts.map +0 -1
  120. package/dist/commands/update.js +0 -72
  121. package/dist/commands/update.js.map +0 -1
  122. package/dist/commands/validate.d.ts +0 -3
  123. package/dist/commands/validate.d.ts.map +0 -1
  124. package/dist/commands/validate.js +0 -86
  125. package/dist/commands/validate.js.map +0 -1
  126. package/dist/index.d.ts +0 -7
  127. package/dist/index.d.ts.map +0 -1
  128. package/dist/index.js +0 -8
  129. package/dist/index.js.map +0 -1
  130. package/dist/schemas/config.d.ts +0 -17
  131. package/dist/schemas/config.d.ts.map +0 -1
  132. package/dist/schemas/config.js +0 -16
  133. package/dist/schemas/config.js.map +0 -1
  134. package/dist/schemas/index.d.ts +0 -5
  135. package/dist/schemas/index.d.ts.map +0 -1
  136. package/dist/schemas/index.js +0 -3
  137. package/dist/schemas/index.js.map +0 -1
  138. package/dist/schemas/record-schema.d.ts +0 -379
  139. package/dist/schemas/record-schema.d.ts.map +0 -1
  140. package/dist/schemas/record-schema.js +0 -148
  141. package/dist/schemas/record-schema.js.map +0 -1
  142. package/dist/schemas/record.d.ts +0 -60
  143. package/dist/schemas/record.d.ts.map +0 -1
  144. package/dist/schemas/record.js +0 -2
  145. package/dist/schemas/record.js.map +0 -1
  146. package/dist/utils/bm25.d.ts +0 -39
  147. package/dist/utils/bm25.d.ts.map +0 -1
  148. package/dist/utils/bm25.js +0 -171
  149. package/dist/utils/bm25.js.map +0 -1
  150. package/dist/utils/budget.d.ts +0 -35
  151. package/dist/utils/budget.d.ts.map +0 -1
  152. package/dist/utils/budget.js +0 -114
  153. package/dist/utils/budget.js.map +0 -1
  154. package/dist/utils/config.d.ts +0 -12
  155. package/dist/utils/config.d.ts.map +0 -1
  156. package/dist/utils/config.js +0 -89
  157. package/dist/utils/config.js.map +0 -1
  158. package/dist/utils/expertise.d.ts +0 -57
  159. package/dist/utils/expertise.d.ts.map +0 -1
  160. package/dist/utils/expertise.js +0 -264
  161. package/dist/utils/expertise.js.map +0 -1
  162. package/dist/utils/format.d.ts +0 -31
  163. package/dist/utils/format.d.ts.map +0 -1
  164. package/dist/utils/format.js +0 -556
  165. package/dist/utils/format.js.map +0 -1
  166. package/dist/utils/git.d.ts +0 -6
  167. package/dist/utils/git.d.ts.map +0 -1
  168. package/dist/utils/git.js +0 -81
  169. package/dist/utils/git.js.map +0 -1
  170. package/dist/utils/index.d.ts +0 -8
  171. package/dist/utils/index.d.ts.map +0 -1
  172. package/dist/utils/index.js +0 -8
  173. package/dist/utils/index.js.map +0 -1
  174. package/dist/utils/json-output.d.ts +0 -8
  175. package/dist/utils/json-output.d.ts.map +0 -1
  176. package/dist/utils/json-output.js +0 -7
  177. package/dist/utils/json-output.js.map +0 -1
  178. package/dist/utils/lock.d.ts +0 -6
  179. package/dist/utils/lock.d.ts.map +0 -1
  180. package/dist/utils/lock.js +0 -70
  181. package/dist/utils/lock.js.map +0 -1
  182. package/dist/utils/markers.d.ts +0 -22
  183. package/dist/utils/markers.d.ts.map +0 -1
  184. package/dist/utils/markers.js +0 -42
  185. package/dist/utils/markers.js.map +0 -1
  186. package/dist/utils/scoring.d.ts +0 -73
  187. package/dist/utils/scoring.d.ts.map +0 -1
  188. package/dist/utils/scoring.js +0 -80
  189. package/dist/utils/scoring.js.map +0 -1
  190. package/dist/utils/version.d.ts +0 -15
  191. package/dist/utils/version.d.ts.map +0 -1
  192. package/dist/utils/version.js +0 -48
  193. package/dist/utils/version.js.map +0 -1
@@ -0,0 +1,103 @@
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
4
+ import {
5
+ readExpertiseFile,
6
+ resolveRecordId,
7
+ writeExpertiseFile,
8
+ } from "../utils/expertise.ts";
9
+ import { getRecordSummary } from "../utils/format.ts";
10
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
11
+ import { withFileLock } from "../utils/lock.ts";
12
+
13
+ export function registerDeleteCommand(program: Command): void {
14
+ program
15
+ .command("delete")
16
+ .argument("<domain>", "expertise domain")
17
+ .argument("<id>", "record ID (e.g. mx-abc123, abc123, or abc)")
18
+ .description("Delete an expertise record")
19
+ .action(async (domain: string, id: string) => {
20
+ const jsonMode = program.opts().json === true;
21
+ try {
22
+ const config = await readConfig();
23
+
24
+ if (!config.domains.includes(domain)) {
25
+ if (jsonMode) {
26
+ outputJsonError(
27
+ "delete",
28
+ `Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`,
29
+ );
30
+ } else {
31
+ console.error(
32
+ chalk.red(`Error: domain "${domain}" not found in config.`),
33
+ );
34
+ console.error(
35
+ chalk.red(
36
+ `Available domains: ${config.domains.join(", ") || "(none)"}`,
37
+ ),
38
+ );
39
+ }
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+
44
+ const filePath = getExpertisePath(domain);
45
+ await withFileLock(filePath, async () => {
46
+ const records = await readExpertiseFile(filePath);
47
+
48
+ const resolved = resolveRecordId(records, id);
49
+ if (!resolved.ok) {
50
+ if (jsonMode) {
51
+ outputJsonError("delete", resolved.error);
52
+ } else {
53
+ console.error(chalk.red(`Error: ${resolved.error}`));
54
+ }
55
+ process.exitCode = 1;
56
+ return;
57
+ }
58
+ const targetIndex = resolved.index;
59
+
60
+ const deleted = records[targetIndex];
61
+ records.splice(targetIndex, 1);
62
+ await writeExpertiseFile(filePath, records);
63
+
64
+ if (jsonMode) {
65
+ outputJson({
66
+ success: true,
67
+ command: "delete",
68
+ domain,
69
+ id: deleted.id ?? null,
70
+ type: deleted.type,
71
+ summary: getRecordSummary(deleted),
72
+ });
73
+ } else {
74
+ console.log(
75
+ chalk.green(
76
+ `✔ Deleted ${deleted.type} ${deleted.id ?? ""} from ${domain}: ${getRecordSummary(deleted)}`,
77
+ ),
78
+ );
79
+ }
80
+ });
81
+ } catch (err) {
82
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
83
+ if (jsonMode) {
84
+ outputJsonError(
85
+ "delete",
86
+ "No .mulch/ directory found. Run `mulch init` first.",
87
+ );
88
+ } else {
89
+ console.error(
90
+ "Error: No .mulch/ directory found. Run `mulch init` first.",
91
+ );
92
+ }
93
+ } else {
94
+ if (jsonMode) {
95
+ outputJsonError("delete", (err as Error).message);
96
+ } else {
97
+ console.error(`Error: ${(err as Error).message}`);
98
+ }
99
+ }
100
+ process.exitCode = 1;
101
+ }
102
+ });
103
+ }
@@ -0,0 +1,209 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import chalk from "chalk";
3
+ import type { Command } from "commander";
4
+ import type { ExpertiseRecord } from "../schemas/record.ts";
5
+ import { getRecordSummary } from "../utils/format.ts";
6
+ import { isGitRepo } from "../utils/git.ts";
7
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
8
+
9
+ export interface DiffEntry {
10
+ domain: string;
11
+ added: ExpertiseRecord[];
12
+ removed: ExpertiseRecord[];
13
+ }
14
+
15
+ export function parseExpertiseDiff(diffOutput: string): DiffEntry[] {
16
+ const lines = diffOutput.split("\n");
17
+ const entriesMap = new Map<string, DiffEntry>();
18
+ let currentDomain: string | null = null;
19
+
20
+ for (const line of lines) {
21
+ // Extract domain from 'diff --git' headers
22
+ if (line.startsWith("diff --git")) {
23
+ const match = line.match(/\.mulch\/expertise\/([^/]+)\.jsonl/);
24
+ if (match) {
25
+ currentDomain = match[1];
26
+ if (!entriesMap.has(currentDomain)) {
27
+ entriesMap.set(currentDomain, {
28
+ domain: currentDomain,
29
+ added: [],
30
+ removed: [],
31
+ });
32
+ }
33
+ }
34
+ continue;
35
+ }
36
+
37
+ // Skip file metadata lines
38
+ if (
39
+ line.startsWith("+++") ||
40
+ line.startsWith("---") ||
41
+ line.startsWith("@@")
42
+ ) {
43
+ continue;
44
+ }
45
+
46
+ // Parse added records (lines starting with '+')
47
+ if (line.startsWith("+") && currentDomain) {
48
+ const jsonStr = line.slice(1);
49
+ try {
50
+ const record = JSON.parse(jsonStr) as ExpertiseRecord;
51
+ const entry = entriesMap.get(currentDomain);
52
+ if (entry) {
53
+ entry.added.push(record);
54
+ }
55
+ } catch {
56
+ // Skip lines that fail JSON.parse (context lines, hunk headers, etc.)
57
+ }
58
+ continue;
59
+ }
60
+
61
+ // Parse removed records (lines starting with '-')
62
+ if (line.startsWith("-") && currentDomain) {
63
+ const jsonStr = line.slice(1);
64
+ try {
65
+ const record = JSON.parse(jsonStr) as ExpertiseRecord;
66
+ const entry = entriesMap.get(currentDomain);
67
+ if (entry) {
68
+ entry.removed.push(record);
69
+ }
70
+ } catch {
71
+ // Skip lines that fail JSON.parse (context lines, hunk headers, etc.)
72
+ }
73
+ }
74
+ }
75
+
76
+ // Return entries sorted by domain, filtered to non-empty
77
+ return [...entriesMap.values()]
78
+ .filter((entry) => entry.added.length > 0 || entry.removed.length > 0)
79
+ .sort((a, b) => a.domain.localeCompare(b.domain));
80
+ }
81
+
82
+ export function formatDiffOutput(entries: DiffEntry[], since: string): string {
83
+ const lines: string[] = [];
84
+ lines.push(`Expertise changes since ${since}`);
85
+ lines.push("");
86
+
87
+ for (const entry of entries) {
88
+ const changeCount = entry.added.length + entry.removed.length;
89
+ const plural = changeCount === 1 ? "change" : "changes";
90
+ lines.push(`${entry.domain} (${changeCount} ${plural}):`);
91
+
92
+ for (const record of entry.added) {
93
+ const summary = getRecordSummary(record);
94
+ lines.push(` + [${record.type}] ${record.id} ${summary}`);
95
+ }
96
+
97
+ for (const record of entry.removed) {
98
+ const summary = getRecordSummary(record);
99
+ lines.push(` - [${record.type}] ${record.id} ${summary}`);
100
+ }
101
+
102
+ lines.push("");
103
+ }
104
+
105
+ return lines.join("\n").trimEnd();
106
+ }
107
+
108
+ export function registerDiffCommand(program: Command): void {
109
+ program
110
+ .command("diff")
111
+ .description("Show expertise record changes since a git ref")
112
+ .option("--since <ref>", "git ref to diff against", "HEAD~1")
113
+ .action(async (options: { since: string }) => {
114
+ const jsonMode = program.opts().json === true;
115
+ const cwd = process.cwd();
116
+
117
+ if (!isGitRepo(cwd)) {
118
+ if (jsonMode) {
119
+ outputJsonError(
120
+ "diff",
121
+ "Not in a git repository. Run this command from within a git repository.",
122
+ );
123
+ } else {
124
+ console.error(chalk.red("Error: not in a git repository."));
125
+ }
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+
130
+ try {
131
+ let diffOutput: string;
132
+ try {
133
+ diffOutput = execFileSync(
134
+ "git",
135
+ ["diff", options.since, "--", ".mulch/expertise/"],
136
+ {
137
+ cwd,
138
+ encoding: "utf-8",
139
+ stdio: ["pipe", "pipe", "pipe"],
140
+ },
141
+ );
142
+ } catch {
143
+ // ref doesn't exist or no changes, treat as empty diff
144
+ diffOutput = "";
145
+ }
146
+
147
+ const entries = parseExpertiseDiff(diffOutput);
148
+
149
+ if (entries.length === 0) {
150
+ if (jsonMode) {
151
+ outputJson({
152
+ success: true,
153
+ command: "diff",
154
+ since: options.since,
155
+ domains: [],
156
+ message: "No expertise changes found.",
157
+ });
158
+ } else {
159
+ console.log("No expertise changes found.");
160
+ }
161
+ return;
162
+ }
163
+
164
+ if (jsonMode) {
165
+ outputJson({
166
+ success: true,
167
+ command: "diff",
168
+ since: options.since,
169
+ domains: entries,
170
+ });
171
+ return;
172
+ }
173
+
174
+ // Plain text output with colors
175
+ console.log(chalk.bold(`\nExpertise changes since ${options.since}\n`));
176
+
177
+ for (const entry of entries) {
178
+ const changeCount = entry.added.length + entry.removed.length;
179
+ const plural = changeCount === 1 ? "change" : "changes";
180
+ console.log(
181
+ chalk.cyan(`${entry.domain} (${changeCount} ${plural}):`),
182
+ );
183
+
184
+ for (const record of entry.added) {
185
+ const summary = getRecordSummary(record);
186
+ console.log(
187
+ chalk.green(` + [${record.type}] ${record.id} ${summary}`),
188
+ );
189
+ }
190
+
191
+ for (const record of entry.removed) {
192
+ const summary = getRecordSummary(record);
193
+ console.log(
194
+ chalk.red(` - [${record.type}] ${record.id} ${summary}`),
195
+ );
196
+ }
197
+
198
+ console.log("");
199
+ }
200
+ } catch (err) {
201
+ if (jsonMode) {
202
+ outputJsonError("diff", (err as Error).message);
203
+ } else {
204
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
205
+ }
206
+ process.exitCode = 1;
207
+ }
208
+ });
209
+ }