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,128 @@
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import type { Classification, ExpertiseRecord } from "../schemas/record.ts";
4
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
5
+ import { readExpertiseFile, writeExpertiseFile } from "../utils/expertise.ts";
6
+ import { outputJson } from "../utils/json-output.ts";
7
+ import { withFileLock } from "../utils/lock.ts";
8
+
9
+ interface PruneResult {
10
+ domain: string;
11
+ before: number;
12
+ pruned: number;
13
+ after: number;
14
+ }
15
+
16
+ export function isStale(
17
+ record: ExpertiseRecord,
18
+ now: Date,
19
+ shelfLife: { tactical: number; observational: number },
20
+ ): boolean {
21
+ const classification: Classification = record.classification;
22
+
23
+ if (classification === "foundational") {
24
+ return false;
25
+ }
26
+
27
+ const recordedAt = new Date(record.recorded_at);
28
+ const ageInDays = Math.floor(
29
+ (now.getTime() - recordedAt.getTime()) / (1000 * 60 * 60 * 24),
30
+ );
31
+
32
+ if (classification === "tactical") {
33
+ return ageInDays > shelfLife.tactical;
34
+ }
35
+
36
+ if (classification === "observational") {
37
+ return ageInDays > shelfLife.observational;
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ export function registerPruneCommand(program: Command): void {
44
+ program
45
+ .command("prune")
46
+ .description("Remove outdated or low-value expertise records")
47
+ .option("--dry-run", "Show what would be pruned without removing", false)
48
+ .action(async (options: { dryRun: boolean }) => {
49
+ const jsonMode = program.opts().json === true;
50
+ const config = await readConfig();
51
+ const now = new Date();
52
+ const shelfLife = config.classification_defaults.shelf_life;
53
+ const results: PruneResult[] = [];
54
+ let totalPruned = 0;
55
+
56
+ for (const domain of config.domains) {
57
+ const filePath = getExpertisePath(domain);
58
+
59
+ const domainResult = await withFileLock(filePath, async () => {
60
+ const records = await readExpertiseFile(filePath);
61
+
62
+ if (records.length === 0) {
63
+ return null;
64
+ }
65
+
66
+ const kept: ExpertiseRecord[] = [];
67
+ let pruned = 0;
68
+
69
+ for (const record of records) {
70
+ if (isStale(record, now, shelfLife)) {
71
+ pruned++;
72
+ } else {
73
+ kept.push(record);
74
+ }
75
+ }
76
+
77
+ if (pruned > 0) {
78
+ if (!options.dryRun) {
79
+ await writeExpertiseFile(filePath, kept);
80
+ }
81
+ return {
82
+ domain,
83
+ before: records.length,
84
+ pruned,
85
+ after: kept.length,
86
+ };
87
+ }
88
+ return null;
89
+ });
90
+
91
+ if (domainResult) {
92
+ results.push(domainResult);
93
+ totalPruned += domainResult.pruned;
94
+ }
95
+ }
96
+
97
+ if (jsonMode) {
98
+ outputJson({
99
+ success: true,
100
+ command: "prune",
101
+ dryRun: options.dryRun,
102
+ totalPruned,
103
+ results,
104
+ });
105
+ return;
106
+ }
107
+
108
+ if (totalPruned === 0) {
109
+ console.log(
110
+ chalk.green("No stale records found. All records are current."),
111
+ );
112
+ return;
113
+ }
114
+
115
+ const label = options.dryRun ? "Would prune" : "Pruned";
116
+ const prefix = options.dryRun ? chalk.yellow("[DRY RUN] ") : "";
117
+
118
+ for (const result of results) {
119
+ console.log(
120
+ `${prefix}${chalk.cyan(result.domain)}: ${label} ${chalk.red(String(result.pruned))} of ${result.before} records (${result.after} remaining)`,
121
+ );
122
+ }
123
+
124
+ console.log(
125
+ `\n${prefix}${chalk.bold(`Total: ${label.toLowerCase()} ${totalPruned} stale ${totalPruned === 1 ? "record" : "records"}.`)}`,
126
+ );
127
+ });
128
+ }
@@ -0,0 +1,177 @@
1
+ import { type Command, Option } from "commander";
2
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
3
+ import {
4
+ filterByClassification,
5
+ filterByFile,
6
+ filterByType,
7
+ getFileModTime,
8
+ readExpertiseFile,
9
+ } from "../utils/expertise.ts";
10
+ import { formatDomainExpertise } from "../utils/format.ts";
11
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
12
+ import {
13
+ type ScoredRecord,
14
+ sortByConfirmationScore,
15
+ } from "../utils/scoring.ts";
16
+
17
+ export function registerQueryCommand(program: Command): void {
18
+ program
19
+ .command("query")
20
+ .argument("[domain]", "expertise domain to query")
21
+ .description("Query expertise records")
22
+ .option("--type <type>", "filter by record type")
23
+ .addOption(
24
+ new Option(
25
+ "--classification <classification>",
26
+ "filter by classification",
27
+ ).choices(["foundational", "tactical", "observational"]),
28
+ )
29
+ .option("--file <file>", "filter by associated file path (substring match)")
30
+ .addOption(
31
+ new Option(
32
+ "--outcome-status <status>",
33
+ "filter by outcome status",
34
+ ).choices(["success", "failure"]),
35
+ )
36
+ .option(
37
+ "--sort-by-score",
38
+ "sort results by confirmation-frequency score (highest first)",
39
+ )
40
+ .option("--all", "show all domains")
41
+ .action(
42
+ async (domain: string | undefined, options: Record<string, unknown>) => {
43
+ const jsonMode = program.opts().json === true;
44
+ try {
45
+ const config = await readConfig();
46
+
47
+ const domainsToQuery: string[] = [];
48
+
49
+ if (options.all) {
50
+ domainsToQuery.push(...config.domains);
51
+ if (domainsToQuery.length === 0) {
52
+ if (jsonMode) {
53
+ outputJson({ success: true, command: "query", domains: [] });
54
+ } else {
55
+ console.log(
56
+ "No domains configured. Run `mulch add <domain>` to get started.",
57
+ );
58
+ }
59
+ return;
60
+ }
61
+ } else if (domain) {
62
+ if (!config.domains.includes(domain)) {
63
+ if (jsonMode) {
64
+ outputJsonError(
65
+ "query",
66
+ `Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`,
67
+ );
68
+ } else {
69
+ console.error(
70
+ `Error: Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`,
71
+ );
72
+ }
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+ domainsToQuery.push(domain);
77
+ } else {
78
+ if (jsonMode) {
79
+ outputJsonError(
80
+ "query",
81
+ "Please specify a domain or use --all to query all domains.",
82
+ );
83
+ } else {
84
+ console.error(
85
+ "Error: Please specify a domain or use --all to query all domains.",
86
+ );
87
+ }
88
+ process.exitCode = 1;
89
+ return;
90
+ }
91
+
92
+ if (jsonMode) {
93
+ const result: Array<{ domain: string; records: unknown[] }> = [];
94
+ for (const d of domainsToQuery) {
95
+ const filePath = getExpertisePath(d);
96
+ let records = await readExpertiseFile(filePath);
97
+ if (options.type) {
98
+ records = filterByType(records, options.type as string);
99
+ }
100
+ if (options.classification) {
101
+ records = filterByClassification(
102
+ records,
103
+ options.classification as string,
104
+ );
105
+ }
106
+ if (options.file) {
107
+ records = filterByFile(records, options.file as string);
108
+ }
109
+ if (options.outcomeStatus) {
110
+ records = records.filter((r) =>
111
+ r.outcomes?.some(
112
+ (o) => o.status === (options.outcomeStatus as string),
113
+ ),
114
+ );
115
+ }
116
+ if (options.sortByScore) {
117
+ records = sortByConfirmationScore(records as ScoredRecord[]);
118
+ }
119
+ result.push({ domain: d, records });
120
+ }
121
+ outputJson({ success: true, command: "query", domains: result });
122
+ } else {
123
+ const sections: string[] = [];
124
+ for (const d of domainsToQuery) {
125
+ const filePath = getExpertisePath(d);
126
+ let records = await readExpertiseFile(filePath);
127
+ const lastUpdated = await getFileModTime(filePath);
128
+ if (options.type) {
129
+ records = filterByType(records, options.type as string);
130
+ }
131
+ if (options.classification) {
132
+ records = filterByClassification(
133
+ records,
134
+ options.classification as string,
135
+ );
136
+ }
137
+ if (options.file) {
138
+ records = filterByFile(records, options.file as string);
139
+ }
140
+ if (options.outcomeStatus) {
141
+ records = records.filter((r) =>
142
+ r.outcomes?.some(
143
+ (o) => o.status === (options.outcomeStatus as string),
144
+ ),
145
+ );
146
+ }
147
+ if (options.sortByScore) {
148
+ records = sortByConfirmationScore(records as ScoredRecord[]);
149
+ }
150
+ sections.push(formatDomainExpertise(d, records, lastUpdated));
151
+ }
152
+ console.log(sections.join("\n\n"));
153
+ }
154
+ } catch (err) {
155
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
156
+ if (jsonMode) {
157
+ outputJsonError(
158
+ "query",
159
+ "No .mulch/ directory found. Run `mulch init` first.",
160
+ );
161
+ } else {
162
+ console.error(
163
+ "Error: No .mulch/ directory found. Run `mulch init` first.",
164
+ );
165
+ }
166
+ } else {
167
+ if (jsonMode) {
168
+ outputJsonError("query", (err as Error).message);
169
+ } else {
170
+ console.error(`Error: ${(err as Error).message}`);
171
+ }
172
+ }
173
+ process.exitCode = 1;
174
+ }
175
+ },
176
+ );
177
+ }
@@ -0,0 +1,194 @@
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import type { ExpertiseRecord } from "../schemas/record.ts";
4
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
5
+ import { readExpertiseFile } from "../utils/expertise.ts";
6
+ import { formatTimeAgo, getRecordSummary } from "../utils/format.ts";
7
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
8
+
9
+ interface AnnotatedRecord {
10
+ domain: string;
11
+ record: ExpertiseRecord;
12
+ }
13
+
14
+ function parseDuration(input: string): number {
15
+ const match = input.match(/^(\d+)(h|d|w)$/);
16
+ if (!match) {
17
+ throw new Error(
18
+ `Invalid duration: "${input}". Use format like "24h", "7d", "2w".`,
19
+ );
20
+ }
21
+ const value = Number.parseInt(match[1], 10);
22
+ const unit = match[2];
23
+ switch (unit) {
24
+ case "h":
25
+ return value * 3600000;
26
+ case "d":
27
+ return value * 86400000;
28
+ case "w":
29
+ return value * 7 * 86400000;
30
+ default:
31
+ throw new Error(`Unknown unit: ${unit}`);
32
+ }
33
+ }
34
+
35
+ export function registerReadyCommand(program: Command): void {
36
+ program
37
+ .command("ready")
38
+ .description("Show recently added or updated expertise records")
39
+ .option("--limit <n>", "maximum number of records to show", "10")
40
+ .option("--domain <domain>", "limit to a specific domain")
41
+ .option(
42
+ "--since <duration>",
43
+ "show records from the last duration (e.g. 24h, 7d, 2w)",
44
+ )
45
+ .action(
46
+ async (options: { limit: string; domain?: string; since?: string }) => {
47
+ const jsonMode = program.opts().json === true;
48
+
49
+ try {
50
+ const config = await readConfig();
51
+ const limit = Number.parseInt(options.limit, 10);
52
+
53
+ if (Number.isNaN(limit) || limit < 1) {
54
+ if (jsonMode) {
55
+ outputJsonError("ready", "Limit must be a positive integer.");
56
+ } else {
57
+ console.error(
58
+ chalk.red("Error: --limit must be a positive integer."),
59
+ );
60
+ }
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+
65
+ let domainsToCheck: string[];
66
+ if (options.domain) {
67
+ if (!config.domains.includes(options.domain)) {
68
+ if (jsonMode) {
69
+ outputJsonError(
70
+ "ready",
71
+ `Domain "${options.domain}" not found in config. Available domains: ${config.domains.join(", ")}`,
72
+ );
73
+ } else {
74
+ console.error(
75
+ chalk.red(
76
+ `Error: domain "${options.domain}" not found. Available: ${config.domains.join(", ")}`,
77
+ ),
78
+ );
79
+ }
80
+ process.exitCode = 1;
81
+ return;
82
+ }
83
+ domainsToCheck = [options.domain];
84
+ } else {
85
+ domainsToCheck = config.domains;
86
+ }
87
+
88
+ let sinceMs: number | undefined;
89
+ if (options.since) {
90
+ try {
91
+ sinceMs = parseDuration(options.since);
92
+ } catch (err) {
93
+ if (jsonMode) {
94
+ outputJsonError("ready", (err as Error).message);
95
+ } else {
96
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
97
+ }
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ }
102
+
103
+ // Collect all records with domain annotation
104
+ const all: AnnotatedRecord[] = [];
105
+ for (const domain of domainsToCheck) {
106
+ const filePath = getExpertisePath(domain);
107
+ const records = await readExpertiseFile(filePath);
108
+ for (const record of records) {
109
+ all.push({ domain, record });
110
+ }
111
+ }
112
+
113
+ // Sort by recorded_at descending
114
+ all.sort((a, b) => {
115
+ const aTime = new Date(a.record.recorded_at).getTime();
116
+ const bTime = new Date(b.record.recorded_at).getTime();
117
+ return bTime - aTime;
118
+ });
119
+
120
+ // Apply --since filter
121
+ let filtered = all;
122
+ if (sinceMs !== undefined) {
123
+ const cutoff = Date.now() - sinceMs;
124
+ filtered = all.filter((entry) => {
125
+ return new Date(entry.record.recorded_at).getTime() >= cutoff;
126
+ });
127
+ }
128
+
129
+ // Apply limit
130
+ const entries = filtered.slice(0, limit);
131
+
132
+ if (jsonMode) {
133
+ outputJson({
134
+ success: true,
135
+ command: "ready",
136
+ count: entries.length,
137
+ entries: entries.map((e) => ({
138
+ domain: e.domain,
139
+ id: e.record.id ?? null,
140
+ type: e.record.type,
141
+ recorded_at: e.record.recorded_at,
142
+ summary: getRecordSummary(e.record),
143
+ record: e.record,
144
+ })),
145
+ });
146
+ } else {
147
+ if (entries.length === 0) {
148
+ console.log("No recent expertise records found.");
149
+ return;
150
+ }
151
+
152
+ const header = options.since
153
+ ? `Recent Expertise (last ${options.since})`
154
+ : `Recent Expertise (last ${entries.length} records)`;
155
+ console.log(header);
156
+ console.log("");
157
+
158
+ for (const entry of entries) {
159
+ const age = formatTimeAgo(new Date(entry.record.recorded_at));
160
+ const id = entry.record.id
161
+ ? chalk.dim(`${entry.record.id} `)
162
+ : "";
163
+ const domain = chalk.cyan(entry.domain.padEnd(14));
164
+ const type = chalk.yellow(`[${entry.record.type}]`.padEnd(14));
165
+ const summary = getRecordSummary(entry.record);
166
+ console.log(
167
+ ` ${id}${chalk.dim(age.padEnd(10))}${domain}${type}${summary}`,
168
+ );
169
+ }
170
+ }
171
+ } catch (err) {
172
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
173
+ if (jsonMode) {
174
+ outputJsonError(
175
+ "ready",
176
+ "No .mulch/ directory found. Run `mulch init` first.",
177
+ );
178
+ } else {
179
+ console.error(
180
+ "Error: No .mulch/ directory found. Run `mulch init` first.",
181
+ );
182
+ }
183
+ } else {
184
+ if (jsonMode) {
185
+ outputJsonError("ready", (err as Error).message);
186
+ } else {
187
+ console.error(`Error: ${(err as Error).message}`);
188
+ }
189
+ }
190
+ process.exitCode = 1;
191
+ }
192
+ },
193
+ );
194
+ }