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,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
+ }