@teammates/cli 0.1.0 → 0.2.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 (76) hide show
  1. package/README.md +31 -22
  2. package/dist/adapter.d.ts +1 -1
  3. package/dist/adapter.js +68 -56
  4. package/dist/adapter.test.js +34 -21
  5. package/dist/adapters/cli-proxy.d.ts +11 -4
  6. package/dist/adapters/cli-proxy.js +176 -162
  7. package/dist/adapters/copilot.d.ts +50 -0
  8. package/dist/adapters/copilot.js +210 -0
  9. package/dist/adapters/echo.d.ts +2 -2
  10. package/dist/adapters/echo.js +2 -1
  11. package/dist/adapters/echo.test.js +4 -2
  12. package/dist/cli-utils.d.ts +21 -0
  13. package/dist/cli-utils.js +74 -0
  14. package/dist/cli-utils.test.d.ts +1 -0
  15. package/dist/cli-utils.test.js +179 -0
  16. package/dist/cli.js +3160 -961
  17. package/dist/compact.d.ts +39 -0
  18. package/dist/compact.js +269 -0
  19. package/dist/compact.test.d.ts +1 -0
  20. package/dist/compact.test.js +198 -0
  21. package/dist/console/ansi.d.ts +18 -0
  22. package/dist/console/ansi.js +20 -0
  23. package/dist/console/ansi.test.d.ts +1 -0
  24. package/dist/console/ansi.test.js +50 -0
  25. package/dist/console/dropdown.d.ts +23 -0
  26. package/dist/console/dropdown.js +63 -0
  27. package/dist/console/file-drop.d.ts +59 -0
  28. package/dist/console/file-drop.js +186 -0
  29. package/dist/console/file-drop.test.d.ts +1 -0
  30. package/dist/console/file-drop.test.js +145 -0
  31. package/dist/console/index.d.ts +22 -0
  32. package/dist/console/index.js +23 -0
  33. package/dist/console/interactive-readline.d.ts +65 -0
  34. package/dist/console/interactive-readline.js +132 -0
  35. package/dist/console/markdown-table.d.ts +17 -0
  36. package/dist/console/markdown-table.js +270 -0
  37. package/dist/console/markdown-table.test.d.ts +1 -0
  38. package/dist/console/markdown-table.test.js +130 -0
  39. package/dist/console/mutable-output.d.ts +21 -0
  40. package/dist/console/mutable-output.js +51 -0
  41. package/dist/console/paste-handler.d.ts +63 -0
  42. package/dist/console/paste-handler.js +177 -0
  43. package/dist/console/prompt-box.d.ts +55 -0
  44. package/dist/console/prompt-box.js +120 -0
  45. package/dist/console/prompt-input.d.ts +136 -0
  46. package/dist/console/prompt-input.js +618 -0
  47. package/dist/console/startup.d.ts +20 -0
  48. package/dist/console/startup.js +138 -0
  49. package/dist/console/startup.test.d.ts +1 -0
  50. package/dist/console/startup.test.js +41 -0
  51. package/dist/console/wordwheel.d.ts +75 -0
  52. package/dist/console/wordwheel.js +123 -0
  53. package/dist/dropdown.js +4 -21
  54. package/dist/index.d.ts +5 -5
  55. package/dist/index.js +3 -3
  56. package/dist/onboard.d.ts +24 -0
  57. package/dist/onboard.js +174 -11
  58. package/dist/orchestrator.d.ts +8 -11
  59. package/dist/orchestrator.js +33 -81
  60. package/dist/orchestrator.test.js +59 -79
  61. package/dist/registry.d.ts +1 -1
  62. package/dist/registry.js +56 -12
  63. package/dist/registry.test.js +57 -13
  64. package/dist/theme.d.ts +56 -0
  65. package/dist/theme.js +54 -0
  66. package/dist/types.d.ts +18 -13
  67. package/package.json +8 -3
  68. package/template/CROSS-TEAM.md +2 -2
  69. package/template/PROTOCOL.md +72 -15
  70. package/template/README.md +2 -2
  71. package/template/TEMPLATE.md +118 -15
  72. package/template/example/SOUL.md +2 -1
  73. package/template/example/WISDOM.md +9 -0
  74. package/dist/adapters/codex.d.ts +0 -50
  75. package/dist/adapters/codex.js +0 -213
  76. package/template/example/MEMORIES.md +0 -26
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Episodic memory compaction.
3
+ *
4
+ * Compresses daily logs into weekly summaries, and old weekly summaries
5
+ * into monthly summaries. During weekly compaction, durable knowledge
6
+ * is extracted into typed memory files.
7
+ *
8
+ * Compaction pipeline:
9
+ * daily logs (7 days) → weekly summary (kept 52 weeks)
10
+ * weekly summaries (>52 weeks) → monthly summary (kept permanently)
11
+ */
12
+ export interface CompactionResult {
13
+ teammate: string;
14
+ weekliesCreated: string[];
15
+ monthliesCreated: string[];
16
+ dailiesRemoved: string[];
17
+ weekliesRemoved: string[];
18
+ }
19
+ /**
20
+ * Compact daily logs into weekly summaries for a single teammate.
21
+ * Only compacts complete weeks (not the current week).
22
+ */
23
+ export declare function compactDailies(teammateDir: string): Promise<{
24
+ created: string[];
25
+ removed: string[];
26
+ }>;
27
+ /**
28
+ * Compact weekly summaries older than 52 weeks into monthly summaries.
29
+ */
30
+ export declare function compactWeeklies(teammateDir: string): Promise<{
31
+ created: string[];
32
+ removed: string[];
33
+ }>;
34
+ /**
35
+ * Run full episodic compaction for a teammate:
36
+ * 1. Compact completed weeks' dailies → weekly summaries
37
+ * 2. Compact weeklies older than 52 weeks → monthly summaries
38
+ */
39
+ export declare function compactEpisodic(teammateDir: string, teammateName: string): Promise<CompactionResult>;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Episodic memory compaction.
3
+ *
4
+ * Compresses daily logs into weekly summaries, and old weekly summaries
5
+ * into monthly summaries. During weekly compaction, durable knowledge
6
+ * is extracted into typed memory files.
7
+ *
8
+ * Compaction pipeline:
9
+ * daily logs (7 days) → weekly summary (kept 52 weeks)
10
+ * weekly summaries (>52 weeks) → monthly summary (kept permanently)
11
+ */
12
+ import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
13
+ import { basename, join } from "node:path";
14
+ /**
15
+ * Get ISO week number and year for a date.
16
+ * Returns { year, week } where week is 1-53.
17
+ */
18
+ function getISOWeek(date) {
19
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
20
+ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
21
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
22
+ const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
23
+ return { year: d.getUTCFullYear(), week };
24
+ }
25
+ /**
26
+ * Format a week number as YYYY-Wnn.
27
+ */
28
+ function formatWeek(year, week) {
29
+ return `${year}-W${week.toString().padStart(2, "0")}`;
30
+ }
31
+ /**
32
+ * Group daily logs by ISO week.
33
+ */
34
+ function groupDailiesByWeek(dailies) {
35
+ const groups = new Map();
36
+ for (const daily of dailies) {
37
+ const d = new Date(`${daily.date}T00:00:00Z`);
38
+ const { year, week } = getISOWeek(d);
39
+ const key = formatWeek(year, week);
40
+ const group = groups.get(key) ?? [];
41
+ group.push(daily);
42
+ groups.set(key, group);
43
+ }
44
+ return groups;
45
+ }
46
+ /**
47
+ * Group weekly summaries by month (YYYY-MM).
48
+ */
49
+ function groupWeekliesByMonth(weeklies) {
50
+ const groups = new Map();
51
+ for (const weekly of weeklies) {
52
+ // Parse YYYY-Wnn to get approximate month from the Thursday of that week
53
+ const match = weekly.week.match(/^(\d{4})-W(\d{2})$/);
54
+ if (!match)
55
+ continue;
56
+ const year = parseInt(match[1], 10);
57
+ const weekNum = parseInt(match[2], 10);
58
+ // ISO week date: Jan 4 is always in week 1
59
+ const jan4 = new Date(Date.UTC(year, 0, 4));
60
+ const dayOfWeek = jan4.getUTCDay() || 7;
61
+ const monday = new Date(jan4);
62
+ monday.setUTCDate(jan4.getUTCDate() - dayOfWeek + 1 + (weekNum - 1) * 7);
63
+ const thursday = new Date(monday);
64
+ thursday.setUTCDate(monday.getUTCDate() + 3);
65
+ const month = `${thursday.getUTCFullYear()}-${(thursday.getUTCMonth() + 1).toString().padStart(2, "0")}`;
66
+ const group = groups.get(month) ?? [];
67
+ group.push(weekly);
68
+ groups.set(month, group);
69
+ }
70
+ return groups;
71
+ }
72
+ /**
73
+ * Build a weekly summary from daily logs.
74
+ * This is a structural concatenation — the agent can refine it afterward.
75
+ */
76
+ function buildWeeklySummary(weekKey, dailies) {
77
+ // Sort chronologically
78
+ const sorted = [...dailies].sort((a, b) => a.date.localeCompare(b.date));
79
+ const firstDate = sorted[0].date;
80
+ const lastDate = sorted[sorted.length - 1].date;
81
+ const lines = [];
82
+ lines.push("---");
83
+ lines.push(`type: weekly`);
84
+ lines.push(`week: ${weekKey}`);
85
+ lines.push(`period: ${firstDate} to ${lastDate}`);
86
+ lines.push("---");
87
+ lines.push("");
88
+ lines.push(`# Week ${weekKey}`);
89
+ lines.push("");
90
+ for (const daily of sorted) {
91
+ lines.push(`## ${daily.date}`);
92
+ lines.push("");
93
+ lines.push(daily.content.trim());
94
+ lines.push("");
95
+ }
96
+ return lines.join("\n");
97
+ }
98
+ /**
99
+ * Build a monthly summary from weekly summaries.
100
+ */
101
+ function buildMonthlySummary(monthKey, weeklies) {
102
+ const sorted = [...weeklies].sort((a, b) => a.week.localeCompare(b.week));
103
+ const firstWeek = sorted[0].week;
104
+ const lastWeek = sorted[sorted.length - 1].week;
105
+ const lines = [];
106
+ lines.push("---");
107
+ lines.push(`type: monthly`);
108
+ lines.push(`month: ${monthKey}`);
109
+ lines.push(`period: ${firstWeek} to ${lastWeek}`);
110
+ lines.push("---");
111
+ lines.push("");
112
+ lines.push(`# Month ${monthKey}`);
113
+ lines.push("");
114
+ for (const weekly of sorted) {
115
+ // Strip frontmatter from weekly content before including
116
+ const content = weekly.content.replace(/^---[\s\S]*?---\s*\n/, "").trim();
117
+ lines.push(content);
118
+ lines.push("");
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+ /**
123
+ * Compact daily logs into weekly summaries for a single teammate.
124
+ * Only compacts complete weeks (not the current week).
125
+ */
126
+ export async function compactDailies(teammateDir) {
127
+ const memoryDir = join(teammateDir, "memory");
128
+ const weeklyDir = join(memoryDir, "weekly");
129
+ // Read all daily logs
130
+ const entries = await readdir(memoryDir).catch(() => []);
131
+ const dailies = [];
132
+ for (const entry of entries) {
133
+ if (!entry.endsWith(".md"))
134
+ continue;
135
+ const stem = basename(entry, ".md");
136
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(stem))
137
+ continue;
138
+ const content = await readFile(join(memoryDir, entry), "utf-8");
139
+ dailies.push({ date: stem, content, file: entry });
140
+ }
141
+ if (dailies.length === 0)
142
+ return { created: [], removed: [] };
143
+ // Group by ISO week
144
+ const groups = groupDailiesByWeek(dailies);
145
+ // Determine current week — don't compact it
146
+ const now = new Date();
147
+ const { year: curYear, week: curWeek } = getISOWeek(now);
148
+ const currentWeek = formatWeek(curYear, curWeek);
149
+ // Check which weekly summaries already exist
150
+ const existingWeeklies = new Set();
151
+ try {
152
+ const wEntries = await readdir(weeklyDir);
153
+ for (const e of wEntries) {
154
+ if (e.endsWith(".md"))
155
+ existingWeeklies.add(basename(e, ".md"));
156
+ }
157
+ }
158
+ catch {
159
+ // No weekly dir yet
160
+ }
161
+ const created = [];
162
+ const removed = [];
163
+ for (const [weekKey, weekDailies] of groups) {
164
+ // Skip current week
165
+ if (weekKey === currentWeek)
166
+ continue;
167
+ // Skip if weekly summary already exists
168
+ if (existingWeeklies.has(weekKey))
169
+ continue;
170
+ // Create weekly dir if needed
171
+ await mkdir(weeklyDir, { recursive: true });
172
+ // Build and write weekly summary
173
+ const summary = buildWeeklySummary(weekKey, weekDailies);
174
+ const weeklyFile = join(weeklyDir, `${weekKey}.md`);
175
+ await writeFile(weeklyFile, summary, "utf-8");
176
+ created.push(`${weekKey}.md`);
177
+ // Delete the daily logs that were compacted
178
+ for (const daily of weekDailies) {
179
+ await unlink(join(memoryDir, daily.file)).catch(() => { });
180
+ removed.push(daily.file);
181
+ }
182
+ }
183
+ return { created, removed };
184
+ }
185
+ /**
186
+ * Compact weekly summaries older than 52 weeks into monthly summaries.
187
+ */
188
+ export async function compactWeeklies(teammateDir) {
189
+ const memoryDir = join(teammateDir, "memory");
190
+ const weeklyDir = join(memoryDir, "weekly");
191
+ const monthlyDir = join(memoryDir, "monthly");
192
+ // Read all weekly summaries
193
+ let entries;
194
+ try {
195
+ entries = await readdir(weeklyDir);
196
+ }
197
+ catch {
198
+ return { created: [], removed: [] };
199
+ }
200
+ const weeklies = [];
201
+ for (const entry of entries) {
202
+ if (!entry.endsWith(".md"))
203
+ continue;
204
+ const stem = basename(entry, ".md");
205
+ if (!/^\d{4}-W\d{2}$/.test(stem))
206
+ continue;
207
+ const content = await readFile(join(weeklyDir, entry), "utf-8");
208
+ weeklies.push({ week: stem, content, file: entry });
209
+ }
210
+ if (weeklies.length === 0)
211
+ return { created: [], removed: [] };
212
+ // Determine cutoff: 52 weeks ago
213
+ const now = new Date();
214
+ const cutoff = new Date(now);
215
+ cutoff.setDate(cutoff.getDate() - 52 * 7);
216
+ const { year: cutYear, week: cutWeek } = getISOWeek(cutoff);
217
+ const cutoffWeek = formatWeek(cutYear, cutWeek);
218
+ // Filter to old weeklies only
219
+ const oldWeeklies = weeklies.filter((w) => w.week < cutoffWeek);
220
+ if (oldWeeklies.length === 0)
221
+ return { created: [], removed: [] };
222
+ // Group by month
223
+ const groups = groupWeekliesByMonth(oldWeeklies);
224
+ // Check existing monthlies
225
+ const existingMonthlies = new Set();
226
+ try {
227
+ const mEntries = await readdir(monthlyDir);
228
+ for (const e of mEntries) {
229
+ if (e.endsWith(".md"))
230
+ existingMonthlies.add(basename(e, ".md"));
231
+ }
232
+ }
233
+ catch {
234
+ // No monthly dir yet
235
+ }
236
+ const created = [];
237
+ const removed = [];
238
+ for (const [monthKey, monthWeeklies] of groups) {
239
+ if (existingMonthlies.has(monthKey))
240
+ continue;
241
+ await mkdir(monthlyDir, { recursive: true });
242
+ const summary = buildMonthlySummary(monthKey, monthWeeklies);
243
+ const monthlyFile = join(monthlyDir, `${monthKey}.md`);
244
+ await writeFile(monthlyFile, summary, "utf-8");
245
+ created.push(`${monthKey}.md`);
246
+ // Delete the old weekly files
247
+ for (const weekly of monthWeeklies) {
248
+ await unlink(join(weeklyDir, weekly.file)).catch(() => { });
249
+ removed.push(weekly.file);
250
+ }
251
+ }
252
+ return { created, removed };
253
+ }
254
+ /**
255
+ * Run full episodic compaction for a teammate:
256
+ * 1. Compact completed weeks' dailies → weekly summaries
257
+ * 2. Compact weeklies older than 52 weeks → monthly summaries
258
+ */
259
+ export async function compactEpisodic(teammateDir, teammateName) {
260
+ const dailyResult = await compactDailies(teammateDir);
261
+ const weeklyResult = await compactWeeklies(teammateDir);
262
+ return {
263
+ teammate: teammateName,
264
+ weekliesCreated: dailyResult.created,
265
+ monthliesCreated: weeklyResult.created,
266
+ dailiesRemoved: dailyResult.removed,
267
+ weekliesRemoved: weeklyResult.removed,
268
+ };
269
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,198 @@
1
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { compactDailies, compactEpisodic, compactWeeklies } from "./compact.js";
6
+ let testDir;
7
+ beforeEach(async () => {
8
+ testDir = join(tmpdir(), `compact-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
9
+ await mkdir(testDir, { recursive: true });
10
+ });
11
+ afterEach(async () => {
12
+ await rm(testDir, { recursive: true, force: true });
13
+ });
14
+ // Verified ISO weeks: 2024-03-05 (Tue)=W10, 2024-03-06 (Wed)=W10, 2024-03-07 (Thu)=W10
15
+ // 2024-03-12 (Tue)=W11, 2024-03-13 (Wed)=W11
16
+ describe("compactDailies", () => {
17
+ it("creates weekly summary from completed week's dailies", async () => {
18
+ const memDir = join(testDir, "memory");
19
+ await mkdir(memDir, { recursive: true });
20
+ // All three in ISO 2024-W10
21
+ await writeFile(join(memDir, "2024-03-05.md"), "# Tuesday\nDid stuff");
22
+ await writeFile(join(memDir, "2024-03-06.md"), "# Wednesday\nDid more stuff");
23
+ await writeFile(join(memDir, "2024-03-07.md"), "# Thursday\nDid even more");
24
+ const result = await compactDailies(testDir);
25
+ expect(result.created).toHaveLength(1);
26
+ expect(result.created[0]).toBe("2024-W10.md");
27
+ expect(result.removed).toHaveLength(3);
28
+ expect(result.removed).toContain("2024-03-05.md");
29
+ expect(result.removed).toContain("2024-03-06.md");
30
+ expect(result.removed).toContain("2024-03-07.md");
31
+ // Verify the weekly summary was actually written
32
+ const weeklyDir = join(memDir, "weekly");
33
+ const weeklyFiles = await readdir(weeklyDir);
34
+ expect(weeklyFiles).toHaveLength(1);
35
+ const content = await readFile(join(weeklyDir, weeklyFiles[0]), "utf-8");
36
+ expect(content).toContain("type: weekly");
37
+ expect(content).toContain("2024-03-05");
38
+ expect(content).toContain("Did stuff");
39
+ expect(content).toContain("Did more stuff");
40
+ });
41
+ it("does not compact current week's dailies", async () => {
42
+ const memDir = join(testDir, "memory");
43
+ await mkdir(memDir, { recursive: true });
44
+ const today = new Date().toISOString().slice(0, 10);
45
+ await writeFile(join(memDir, `${today}.md`), "# Today\nDoing stuff");
46
+ const result = await compactDailies(testDir);
47
+ expect(result.created).toHaveLength(0);
48
+ expect(result.removed).toHaveLength(0);
49
+ });
50
+ it("groups dailies into separate weeks", async () => {
51
+ const memDir = join(testDir, "memory");
52
+ await mkdir(memDir, { recursive: true });
53
+ // W10: Mar 5-6, W11: Mar 12-13
54
+ await writeFile(join(memDir, "2024-03-05.md"), "# W10 day1");
55
+ await writeFile(join(memDir, "2024-03-06.md"), "# W10 day2");
56
+ await writeFile(join(memDir, "2024-03-12.md"), "# W11 day1");
57
+ await writeFile(join(memDir, "2024-03-13.md"), "# W11 day2");
58
+ const result = await compactDailies(testDir);
59
+ expect(result.created).toHaveLength(2);
60
+ expect(result.removed).toHaveLength(4);
61
+ });
62
+ it("skips weeks that already have a weekly summary", async () => {
63
+ const memDir = join(testDir, "memory");
64
+ const weeklyDir = join(memDir, "weekly");
65
+ await mkdir(weeklyDir, { recursive: true });
66
+ await writeFile(join(memDir, "2024-03-05.md"), "# Day 1");
67
+ // Pre-existing weekly summary for W10
68
+ await writeFile(join(weeklyDir, "2024-W10.md"), "# Already compacted");
69
+ const result = await compactDailies(testDir);
70
+ expect(result.created).toHaveLength(0);
71
+ expect(result.removed).toHaveLength(0);
72
+ });
73
+ it("returns empty when no daily logs exist", async () => {
74
+ const memDir = join(testDir, "memory");
75
+ await mkdir(memDir, { recursive: true });
76
+ await writeFile(join(memDir, "feedback_test.md"), "# Not a daily");
77
+ const result = await compactDailies(testDir);
78
+ expect(result.created).toHaveLength(0);
79
+ expect(result.removed).toHaveLength(0);
80
+ });
81
+ it("returns empty when memory dir doesn't exist", async () => {
82
+ const result = await compactDailies(testDir);
83
+ expect(result.created).toEqual([]);
84
+ expect(result.removed).toEqual([]);
85
+ });
86
+ it("builds weekly summary with correct frontmatter and chronological order", async () => {
87
+ const memDir = join(testDir, "memory");
88
+ await mkdir(memDir, { recursive: true });
89
+ // Write out of order — compaction should sort chronologically
90
+ await writeFile(join(memDir, "2024-03-07.md"), "# Thursday entry");
91
+ await writeFile(join(memDir, "2024-03-05.md"), "# Tuesday entry");
92
+ await writeFile(join(memDir, "2024-03-06.md"), "# Wednesday entry");
93
+ await compactDailies(testDir);
94
+ const weeklyDir = join(memDir, "weekly");
95
+ const files = await readdir(weeklyDir);
96
+ const content = await readFile(join(weeklyDir, files[0]), "utf-8");
97
+ // Check frontmatter
98
+ expect(content).toMatch(/^---\n/);
99
+ expect(content).toContain("type: weekly");
100
+ expect(content).toContain("week: 2024-W10");
101
+ expect(content).toContain("period: 2024-03-05 to 2024-03-07");
102
+ // Check chronological order
103
+ const tuesdayIdx = content.indexOf("Tuesday entry");
104
+ const wednesdayIdx = content.indexOf("Wednesday entry");
105
+ const thursdayIdx = content.indexOf("Thursday entry");
106
+ expect(tuesdayIdx).toBeLessThan(wednesdayIdx);
107
+ expect(wednesdayIdx).toBeLessThan(thursdayIdx);
108
+ });
109
+ });
110
+ describe("compactWeeklies", () => {
111
+ it("compacts weeklies older than 52 weeks into monthly summary", async () => {
112
+ const weeklyDir = join(testDir, "memory", "weekly");
113
+ await mkdir(weeklyDir, { recursive: true });
114
+ // ~2 years ago — definitely older than 52 weeks
115
+ await writeFile(join(weeklyDir, "2023-W10.md"), "---\ntype: weekly\nweek: 2023-W10\n---\n# Week 10\nStuff");
116
+ await writeFile(join(weeklyDir, "2023-W11.md"), "---\ntype: weekly\nweek: 2023-W11\n---\n# Week 11\nMore stuff");
117
+ const result = await compactWeeklies(testDir);
118
+ expect(result.created.length).toBeGreaterThanOrEqual(1);
119
+ expect(result.removed).toHaveLength(2);
120
+ expect(result.removed).toContain("2023-W10.md");
121
+ expect(result.removed).toContain("2023-W11.md");
122
+ // Verify monthly summary was written
123
+ const monthlyDir = join(testDir, "memory", "monthly");
124
+ const monthlyFiles = await readdir(monthlyDir);
125
+ expect(monthlyFiles.length).toBeGreaterThanOrEqual(1);
126
+ const content = await readFile(join(monthlyDir, monthlyFiles[0]), "utf-8");
127
+ expect(content).toContain("type: monthly");
128
+ });
129
+ it("does not compact recent weeklies", async () => {
130
+ const weeklyDir = join(testDir, "memory", "weekly");
131
+ await mkdir(weeklyDir, { recursive: true });
132
+ // Use current year — should be within 52 weeks
133
+ const now = new Date();
134
+ const year = now.getFullYear();
135
+ await writeFile(join(weeklyDir, `${year}-W10.md`), `---\ntype: weekly\nweek: ${year}-W10\n---\n# Recent`);
136
+ const result = await compactWeeklies(testDir);
137
+ expect(result.created).toHaveLength(0);
138
+ expect(result.removed).toHaveLength(0);
139
+ });
140
+ it("returns empty when no weekly dir exists", async () => {
141
+ const result = await compactWeeklies(testDir);
142
+ expect(result.created).toEqual([]);
143
+ expect(result.removed).toEqual([]);
144
+ });
145
+ it("skips months that already have a monthly summary", async () => {
146
+ const weeklyDir = join(testDir, "memory", "weekly");
147
+ const monthlyDir = join(testDir, "memory", "monthly");
148
+ await mkdir(weeklyDir, { recursive: true });
149
+ await mkdir(monthlyDir, { recursive: true });
150
+ await writeFile(join(weeklyDir, "2023-W10.md"), "---\ntype: weekly\nweek: 2023-W10\n---\n# W10");
151
+ await writeFile(join(monthlyDir, "2023-03.md"), "# Already compacted");
152
+ const result = await compactWeeklies(testDir);
153
+ // The monthly for 2023-03 already exists
154
+ const created = result.created.filter((f) => f === "2023-03.md");
155
+ expect(created).toHaveLength(0);
156
+ });
157
+ it("strips frontmatter from weekly content in monthly summary", async () => {
158
+ const weeklyDir = join(testDir, "memory", "weekly");
159
+ await mkdir(weeklyDir, { recursive: true });
160
+ const weeklyContent = "---\ntype: weekly\nweek: 2023-W10\nperiod: 2023-03-06 to 2023-03-10\n---\n\n# Week 2023-W10\n\nSome work was done.";
161
+ await writeFile(join(weeklyDir, "2023-W10.md"), weeklyContent);
162
+ const result = await compactWeeklies(testDir);
163
+ if (result.created.length > 0) {
164
+ const monthlyDir = join(testDir, "memory", "monthly");
165
+ const monthlyFiles = await readdir(monthlyDir);
166
+ const content = await readFile(join(monthlyDir, monthlyFiles[0]), "utf-8");
167
+ // Monthly should have its own frontmatter but NOT the weekly's
168
+ expect(content).toContain("type: monthly");
169
+ expect(content).not.toContain("type: weekly");
170
+ expect(content).toContain("Some work was done.");
171
+ }
172
+ });
173
+ });
174
+ describe("compactEpisodic", () => {
175
+ it("runs both daily and weekly compaction", async () => {
176
+ const memDir = join(testDir, "memory");
177
+ await mkdir(memDir, { recursive: true });
178
+ // Both in ISO 2024-W10
179
+ await writeFile(join(memDir, "2024-03-05.md"), "# Day 1");
180
+ await writeFile(join(memDir, "2024-03-06.md"), "# Day 2");
181
+ const result = await compactEpisodic(testDir, "test");
182
+ expect(result.teammate).toBe("test");
183
+ expect(result.weekliesCreated).toHaveLength(1);
184
+ expect(result.dailiesRemoved).toHaveLength(2);
185
+ // The newly created weekly (2024-W10) is >52 weeks old, so compactWeeklies
186
+ // immediately compacts it into a monthly summary
187
+ expect(result.monthliesCreated).toHaveLength(1);
188
+ expect(result.weekliesRemoved).toHaveLength(1);
189
+ });
190
+ it("returns empty results for teammate with no memory", async () => {
191
+ const result = await compactEpisodic(testDir, "empty");
192
+ expect(result.teammate).toBe("empty");
193
+ expect(result.weekliesCreated).toEqual([]);
194
+ expect(result.monthliesCreated).toEqual([]);
195
+ expect(result.dailiesRemoved).toEqual([]);
196
+ expect(result.weekliesRemoved).toEqual([]);
197
+ });
198
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ANSI helpers — re-exports from consolonia plus CLI-specific extras.
3
+ */
4
+ import { stripAnsi, truncateAnsi, visibleLength } from "@teammates/consolonia";
5
+ export { stripAnsi, truncateAnsi, visibleLength };
6
+ export declare const cursorUp: (n?: number) => string;
7
+ export declare const cursorDown: (n?: number) => string;
8
+ export declare const eraseLine = "\u001B[2K";
9
+ export declare const eraseDown = "\u001B[0J";
10
+ export declare const eraseScreen = "\u001B[2J";
11
+ /** Move cursor to absolute column (1-based). */
12
+ export declare const cursorToCol: (col: number) => string;
13
+ /** Erase from cursor to end of line. */
14
+ export declare const eraseToEnd = "\u001B[0K";
15
+ /** Move cursor to home (0,0). */
16
+ export declare const cursorHome = "\u001B[H";
17
+ /** Carriage return. */
18
+ export declare const cr = "\r";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ANSI helpers — re-exports from consolonia plus CLI-specific extras.
3
+ */
4
+ import { esc, stripAnsi, truncateAnsi, visibleLength, } from "@teammates/consolonia";
5
+ // ── Re-exports from consolonia ──────────────────────────────────
6
+ export { stripAnsi, truncateAnsi, visibleLength };
7
+ export const cursorUp = (n = 1) => esc.moveUp(n);
8
+ export const cursorDown = (n = 1) => esc.moveDown(n);
9
+ export const eraseLine = esc.eraseLine;
10
+ export const eraseDown = esc.eraseDown;
11
+ export const eraseScreen = esc.clearScreen;
12
+ // ── CLI-specific (not in consolonia) ────────────────────────────
13
+ /** Move cursor to absolute column (1-based). */
14
+ export const cursorToCol = (col) => `\x1b[${col}G`;
15
+ /** Erase from cursor to end of line. */
16
+ export const eraseToEnd = "\x1b[0K";
17
+ /** Move cursor to home (0,0). */
18
+ export const cursorHome = "\x1b[H";
19
+ /** Carriage return. */
20
+ export const cr = "\r";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { cursorDown, cursorToCol, cursorUp } from "./ansi.js";
3
+ describe("cursorUp", () => {
4
+ it("produces correct escape for n=1", () => {
5
+ expect(cursorUp(1)).toBe("\x1b[1A");
6
+ });
7
+ it("produces correct escape for n=5", () => {
8
+ expect(cursorUp(5)).toBe("\x1b[5A");
9
+ });
10
+ it("uses default n=1 when called with no arguments", () => {
11
+ expect(cursorUp()).toBe("\x1b[1A");
12
+ });
13
+ it("handles n=0", () => {
14
+ expect(cursorUp(0)).toBe("\x1b[0A");
15
+ });
16
+ it("handles large n", () => {
17
+ expect(cursorUp(100)).toBe("\x1b[100A");
18
+ });
19
+ });
20
+ describe("cursorDown", () => {
21
+ it("produces correct escape for n=1", () => {
22
+ expect(cursorDown(1)).toBe("\x1b[1B");
23
+ });
24
+ it("produces correct escape for n=3", () => {
25
+ expect(cursorDown(3)).toBe("\x1b[3B");
26
+ });
27
+ it("uses default n=1 when called with no arguments", () => {
28
+ expect(cursorDown()).toBe("\x1b[1B");
29
+ });
30
+ it("handles n=0", () => {
31
+ expect(cursorDown(0)).toBe("\x1b[0B");
32
+ });
33
+ it("handles large n", () => {
34
+ expect(cursorDown(500)).toBe("\x1b[500B");
35
+ });
36
+ });
37
+ describe("cursorToCol", () => {
38
+ it("produces correct escape for col=1", () => {
39
+ expect(cursorToCol(1)).toBe("\x1b[1G");
40
+ });
41
+ it("produces correct escape for col=10", () => {
42
+ expect(cursorToCol(10)).toBe("\x1b[10G");
43
+ });
44
+ it("handles col=0", () => {
45
+ expect(cursorToCol(0)).toBe("\x1b[0G");
46
+ });
47
+ it("handles large col", () => {
48
+ expect(cursorToCol(999)).toBe("\x1b[999G");
49
+ });
50
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Dropdown — renders lines below the readline prompt without disrupting input.
3
+ *
4
+ * Hooks readline's internal _refreshLine to append dropdown content below
5
+ * the prompt, then repositions the cursor back to the input line.
6
+ *
7
+ * Works on both Windows and macOS terminals.
8
+ */
9
+ import type { Interface as ReadlineInterface } from "node:readline";
10
+ export declare class Dropdown {
11
+ private rl;
12
+ private lines;
13
+ private out;
14
+ private refreshing;
15
+ constructor(rl: ReadlineInterface);
16
+ /** Number of lines currently rendered below the prompt. */
17
+ get rendered(): number;
18
+ /** Set dropdown content and trigger a re-render. */
19
+ render(newLines: string[]): void;
20
+ /** Clear dropdown content. Next _refreshLine won't append anything. */
21
+ clear(): void;
22
+ private installHook;
23
+ }