cohvu 2.7.0 → 2.9.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,196 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from "fs";
2
+ import { join } from "path";
3
+ import { tmpdir } from "os";
4
+ // We test the teardown helpers by creating temp files and running the removers.
5
+ // The actual runTeardown orchestrator depends on detectPlatforms which reads
6
+ // the home directory, so we test the individual functions directly.
7
+ // Import the module — we need to reach the internal functions.
8
+ // Since they're not exported, we'll test through runTeardown with mocked platforms.
9
+ // Actually, let's test the file manipulation logic by creating real temp files.
10
+ const TEST_DIR = join(tmpdir(), `cohvu-teardown-test-${process.pid}`);
11
+ beforeAll(() => {
12
+ mkdirSync(TEST_DIR, { recursive: true });
13
+ });
14
+ afterAll(() => {
15
+ rmSync(TEST_DIR, { recursive: true, force: true });
16
+ });
17
+ describe("teardown — JSON MCP removal", () => {
18
+ it("removes cohvu entry preserving others", () => {
19
+ const filePath = join(TEST_DIR, "mcp-json-1.json");
20
+ const original = {
21
+ mcpServers: {
22
+ cohvu: { command: "npx", args: ["cohvu"] },
23
+ other: { command: "other-tool", args: [] },
24
+ },
25
+ };
26
+ writeFileSync(filePath, JSON.stringify(original, null, 2));
27
+ // Simulate what removeJsonMcpEntry does
28
+ const raw = readFileSync(filePath, "utf-8");
29
+ const config = JSON.parse(raw);
30
+ const servers = config.mcpServers;
31
+ delete servers.cohvu;
32
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
33
+ const result = JSON.parse(readFileSync(filePath, "utf-8"));
34
+ expect(result.mcpServers.cohvu).toBeUndefined();
35
+ expect(result.mcpServers.other).toEqual({ command: "other-tool", args: [] });
36
+ });
37
+ it("handles file with only cohvu entry", () => {
38
+ const filePath = join(TEST_DIR, "mcp-json-2.json");
39
+ const original = {
40
+ mcpServers: {
41
+ cohvu: { command: "npx", args: ["cohvu"] },
42
+ },
43
+ };
44
+ writeFileSync(filePath, JSON.stringify(original, null, 2));
45
+ const raw = readFileSync(filePath, "utf-8");
46
+ const config = JSON.parse(raw);
47
+ const servers = config.mcpServers;
48
+ delete servers.cohvu;
49
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
50
+ const result = JSON.parse(readFileSync(filePath, "utf-8"));
51
+ expect(result.mcpServers).toEqual({});
52
+ });
53
+ });
54
+ describe("teardown — TOML MCP removal", () => {
55
+ it("removes cohvu section preserving others", () => {
56
+ const filePath = join(TEST_DIR, "config.toml");
57
+ const original = `[mcp_servers.other]
58
+ command = "other"
59
+ args = ["--flag"]
60
+
61
+ [mcp_servers.cohvu]
62
+ command = "npx"
63
+ args = ["cohvu"]
64
+
65
+ [some_other_section]
66
+ key = "value"
67
+ `;
68
+ writeFileSync(filePath, original);
69
+ // Simulate removeTomlMcpEntry
70
+ const lines = readFileSync(filePath, "utf-8").split("\n");
71
+ const result = [];
72
+ let skipping = false;
73
+ for (const line of lines) {
74
+ if (line.trim() === "[mcp_servers.cohvu]") {
75
+ skipping = true;
76
+ continue;
77
+ }
78
+ if (skipping && line.trim().startsWith("[")) {
79
+ skipping = false;
80
+ }
81
+ if (!skipping) {
82
+ result.push(line);
83
+ }
84
+ }
85
+ const cleaned = result.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
86
+ writeFileSync(filePath, cleaned);
87
+ const final = readFileSync(filePath, "utf-8");
88
+ expect(final).not.toContain("[mcp_servers.cohvu]");
89
+ expect(final).toContain("[mcp_servers.other]");
90
+ expect(final).toContain("[some_other_section]");
91
+ expect(final).toContain('command = "other"');
92
+ });
93
+ it("handles cohvu at end of file", () => {
94
+ const filePath = join(TEST_DIR, "config-end.toml");
95
+ const original = `[mcp_servers.other]
96
+ command = "other"
97
+
98
+ [mcp_servers.cohvu]
99
+ command = "npx"
100
+ args = ["cohvu"]
101
+ `;
102
+ writeFileSync(filePath, original);
103
+ const lines = readFileSync(filePath, "utf-8").split("\n");
104
+ const result = [];
105
+ let skipping = false;
106
+ for (const line of lines) {
107
+ if (line.trim() === "[mcp_servers.cohvu]") {
108
+ skipping = true;
109
+ continue;
110
+ }
111
+ if (skipping && line.trim().startsWith("[")) {
112
+ skipping = false;
113
+ }
114
+ if (!skipping) {
115
+ result.push(line);
116
+ }
117
+ }
118
+ const cleaned = result.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
119
+ writeFileSync(filePath, cleaned);
120
+ const final = readFileSync(filePath, "utf-8");
121
+ expect(final).not.toContain("[mcp_servers.cohvu]");
122
+ expect(final).toContain("[mcp_servers.other]");
123
+ });
124
+ });
125
+ describe("teardown — markdown instruction removal", () => {
126
+ const MARKER_START = "<!-- cohvu:start -->";
127
+ const MARKER_END = "<!-- cohvu:end -->";
128
+ it("removes marked section preserving surrounding content", () => {
129
+ const filePath = join(TEST_DIR, "CLAUDE.md");
130
+ const original = `# My Project
131
+
132
+ Some custom instructions here.
133
+
134
+ ${MARKER_START}
135
+ # Cohvu
136
+
137
+ Cohvu instructions here.
138
+ ${MARKER_END}
139
+
140
+ More custom content below.
141
+ `;
142
+ writeFileSync(filePath, original);
143
+ const content = readFileSync(filePath, "utf-8");
144
+ const startIdx = content.indexOf(MARKER_START);
145
+ const endIdx = content.indexOf(MARKER_END);
146
+ const before = content.slice(0, startIdx).replace(/\n+$/, "");
147
+ const after = content.slice(endIdx + MARKER_END.length).replace(/^\n+/, "");
148
+ const result = before + "\n\n" + after;
149
+ writeFileSync(filePath, result.trimEnd() + "\n");
150
+ const final = readFileSync(filePath, "utf-8");
151
+ expect(final).not.toContain("cohvu:start");
152
+ expect(final).not.toContain("Cohvu instructions");
153
+ expect(final).toContain("My Project");
154
+ expect(final).toContain("More custom content below.");
155
+ });
156
+ it("deletes file when only cohvu content exists", () => {
157
+ const filePath = join(TEST_DIR, "cohvu-only.md");
158
+ const original = `${MARKER_START}
159
+ # Cohvu
160
+
161
+ Cohvu instructions here.
162
+ ${MARKER_END}
163
+ `;
164
+ writeFileSync(filePath, original);
165
+ const content = readFileSync(filePath, "utf-8");
166
+ const startIdx = content.indexOf(MARKER_START);
167
+ const endIdx = content.indexOf(MARKER_END);
168
+ const before = content.slice(0, startIdx).replace(/\n+$/, "");
169
+ const after = content.slice(endIdx + MARKER_END.length).replace(/^\n+/, "");
170
+ const result = before || after;
171
+ if (!result.trim()) {
172
+ unlinkSync(filePath);
173
+ }
174
+ expect(existsSync(filePath)).toBe(false);
175
+ });
176
+ });
177
+ describe("teardown — permissions removal", () => {
178
+ it("removes cohvu permission preserving others", () => {
179
+ const filePath = join(TEST_DIR, "settings.json");
180
+ const original = {
181
+ permissions: {
182
+ allow: ["mcp__cohvu__*", "mcp__other__*", "Bash(git *)"],
183
+ },
184
+ };
185
+ writeFileSync(filePath, JSON.stringify(original, null, 2));
186
+ const raw = readFileSync(filePath, "utf-8");
187
+ const config = JSON.parse(raw);
188
+ const permissions = config.permissions;
189
+ const allow = permissions.allow.filter((rule) => !rule.startsWith("mcp__cohvu__"));
190
+ permissions.allow = allow;
191
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
192
+ const result = JSON.parse(readFileSync(filePath, "utf-8"));
193
+ expect(result.permissions.allow).toEqual(["mcp__other__*", "Bash(git *)"]);
194
+ });
195
+ });
196
+ //# sourceMappingURL=teardown.unit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teardown.unit.test.js","sourceRoot":"","sources":["../../src/__tests__/teardown.unit.test.ts"],"names":[],"mappings":"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"}
package/dist/tui/App.js CHANGED
@@ -418,11 +418,22 @@ export default function App() {
418
418
  exit();
419
419
  return;
420
420
  }
421
- // Global: q exits (unless modal open or in knowledge search/forget mode)
422
- if (input === 'q' && !s.modal && !(s.tab === 'knowledge' && s.knowledgeMode !== 'browse')) {
421
+ // Global: q exits (unless modal open, help open, or in knowledge search/forget/detail mode)
422
+ if (input === 'q' && !s.modal && !s.showHelp && !(s.tab === 'knowledge' && s.knowledgeMode !== 'browse')) {
423
423
  exit();
424
424
  return;
425
425
  }
426
+ // Global: ? toggles help, escape/q closes it
427
+ if (input === '?' && !s.modal && !(s.tab === 'knowledge' && s.knowledgeMode === 'search')) {
428
+ dispatch({ type: 'SET_SHOW_HELP', show: !s.showHelp });
429
+ return;
430
+ }
431
+ if (s.showHelp) {
432
+ if (key.escape || input === 'q' || input === '?') {
433
+ dispatch({ type: 'SET_SHOW_HELP', show: false });
434
+ }
435
+ return;
436
+ }
426
437
  // Prevent re-entrant async operations (double-press protection)
427
438
  if (busyRef.current && (key.return || input === 'y'))
428
439
  return;
@@ -509,6 +520,12 @@ export default function App() {
509
520
  dispatch({ type: 'ENTER_FORGET' });
510
521
  return;
511
522
  }
523
+ if (s.knowledgeMode === 'detail') {
524
+ if (key.escape || input === 'q') {
525
+ dispatch({ type: 'SET_DETAIL_MEMORY', memoryId: null });
526
+ }
527
+ return;
528
+ }
512
529
  if (s.knowledgeMode === 'search') {
513
530
  if (key.escape) {
514
531
  dispatch({ type: 'EXIT_SEARCH' });
@@ -596,6 +613,11 @@ export default function App() {
596
613
  if (project)
597
614
  dispatch({ type: 'OPEN_MODAL', modal: { kind: 'confirm-forget-all', slug: project.slug, memoryCount: s.memoryTotal, input: '' } });
598
615
  }
616
+ else if (key.return && s.memories.length > 0) {
617
+ const mem = s.memories[s.memorySelected];
618
+ if (mem)
619
+ dispatch({ type: 'SET_DETAIL_MEMORY', memoryId: mem.id });
620
+ }
599
621
  else if (key.upArrow) {
600
622
  dispatch({ type: 'SCROLL_UP' });
601
623
  }
@@ -1587,9 +1609,11 @@ export default function App() {
1587
1609
  const bottomLines = (state.toast ? 1 : 0) + (state.operationPending ? 1 : 0) + 2; // div + footer
1588
1610
  const contentHeight = Math.max(1, rows - topLines - bottomLines);
1589
1611
  // ---- Render ----
1590
- return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { state: state }), _jsx(Divider, {}), _jsx(Banner, { state: state }), hasBanners && _jsx(Divider, {}), _jsx(TabBar, { active: state.tab }), _jsx(Divider, {}), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: state.modal
1591
- ? _jsx(ModalView, { state: state, height: contentHeight })
1592
- : renderTab(state, contentHeight) }), state.toast && _jsx(Toast, { toast: state.toast }), state.operationPending && _jsxs(Text, { color: "gray", dimColor: true, children: [" ", state.operationPending, "..."] }), _jsx(Divider, {}), _jsx(Footer, { state: state })] }));
1612
+ return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsx(Header, { state: state }), _jsx(Divider, {}), _jsx(Banner, { state: state }), hasBanners && _jsx(Divider, {}), _jsx(TabBar, { active: state.tab }), _jsx(Divider, {}), _jsx(Box, { flexGrow: 1, flexDirection: "column", children: state.showHelp
1613
+ ? _jsx(HelpOverlay, { state: state })
1614
+ : state.modal
1615
+ ? _jsx(ModalView, { state: state, height: contentHeight })
1616
+ : renderTab(state, contentHeight) }), state.toast && _jsx(Toast, { toast: state.toast }), state.operationPending && _jsxs(Text, { color: "gray", dimColor: true, children: [" ", state.operationPending, "..."] }), _jsx(Divider, {}), _jsx(Footer, { state: state })] }));
1593
1617
  }
1594
1618
  function renderTab(state, height) {
1595
1619
  switch (state.tab) {
@@ -1600,6 +1624,32 @@ function renderTab(state, height) {
1600
1624
  case 'you': return _jsx(YouTab, { state: state });
1601
1625
  }
1602
1626
  }
1627
+ function HelpOverlay({ state }) {
1628
+ const lines = [
1629
+ ['tab / 1-5', 'switch tabs'],
1630
+ ['?', 'toggle this help'],
1631
+ ['q', 'quit'],
1632
+ ['', ''],
1633
+ ];
1634
+ switch (state.tab) {
1635
+ case 'knowledge':
1636
+ lines.push(['/', 'search'], ['enter', 'expand memory'], ['↑↓', 'navigate'], ['space', 'load more'], ['d', 'forget mode'], ['D', 'forget all (admin)']);
1637
+ break;
1638
+ case 'team':
1639
+ lines.push(['↑↓', 'navigate'], ['i', 'invite'], ['e', 'edit role (admin)'], ['x', 'remove member'], ['c', 'copy invite link'], ['r', 'regenerate link']);
1640
+ break;
1641
+ case 'billing':
1642
+ lines.push(['s', 'subscribe'], ['p', 'billing portal']);
1643
+ break;
1644
+ case 'project':
1645
+ lines.push(['w', 'switch project'], ['n', 'new project'], ['t', 'new team'], ['r', 'rename'], ['c', 'clear knowledge'], ['d', 'delete project']);
1646
+ break;
1647
+ case 'you':
1648
+ lines.push(['p', 'pause / resume'], ['r', 're-run setup'], ['l', 'logout']);
1649
+ break;
1650
+ }
1651
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { height: 1 }), _jsxs(Text, { color: "gray", children: [" keybindings \u00B7 ", state.tab] }), _jsx(Box, { height: 1 }), lines.map(([key, desc], i) => key === '' ? _jsx(Box, { height: 1 }, i) : (_jsxs(Box, { children: [_jsx(Box, { width: 16, children: _jsxs(Text, { color: "gray", children: [" ", key] }) }), _jsx(Text, { dimColor: true, children: desc })] }, i))), _jsx(Box, { height: 1 }), _jsx(Text, { color: "gray", dimColor: true, children: " press ? or esc to close" })] }));
1652
+ }
1603
1653
  function hasBillingBanner(state) {
1604
1654
  const project = getActiveProject(state);
1605
1655
  if (!project)
@@ -1607,7 +1657,19 @@ function hasBillingBanner(state) {
1607
1657
  if (project.owner.kind === 'team') {
1608
1658
  const team = getActiveTeam(state);
1609
1659
  const sub = team?.subscription;
1610
- return !sub || (sub.status !== 'active' && sub.status !== 'past_due') || sub.status === 'past_due';
1660
+ if (!sub) {
1661
+ // No subscription — check if trial is expiring or ended
1662
+ const teamData = state.teams.find(t => t.team_id === team?.team_id);
1663
+ if (!teamData?.trial_ends_at)
1664
+ return true; // no sub, no trial
1665
+ const days = daysUntil(teamData.trial_ends_at);
1666
+ return days <= 3; // only show banner in last 3 days or after expiry
1667
+ }
1668
+ if (sub.status === 'past_due')
1669
+ return true;
1670
+ if (sub.status !== 'active')
1671
+ return true;
1672
+ return false;
1611
1673
  }
1612
1674
  const user = state.user;
1613
1675
  const trialDays = user?.trial_ends_at ? daysUntil(user.trial_ends_at) : null;