wendkeep 0.2.0 → 0.2.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wendkeep",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Automatically capture AI coding agent sessions (Claude Code, Codex) as local Markdown in your Obsidian vault — turn-by-turn history, cost/token tracking, auto-extracted decisions/bugs/learnings, rendered in the graph. Local-first.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,111 @@
1
+ // Interactive companion checkbox selector for `wendkeep init`.
2
+ // The state machine (initial/reduce/render/mapKey) is pure and tested; the raw-TTY
3
+ // event loop is a thin shell over it, with a text fallback when raw mode is absent.
4
+ import readline from 'node:readline';
5
+
6
+ const HINT = 'Espaço marca/desmarca · ↑/↓ move · a=todos · n=nenhum · Enter confirma';
7
+
8
+ export function initialCompanionMenu(companions) {
9
+ return {
10
+ items: companions.map((c) => ({ id: c.id, label: c.label, checked: !!c.default })),
11
+ cursor: 0,
12
+ };
13
+ }
14
+
15
+ export function reduceCompanionMenu(state, action) {
16
+ const n = state.items.length;
17
+ switch (action) {
18
+ case 'up':
19
+ return { ...state, cursor: (state.cursor - 1 + n) % n };
20
+ case 'down':
21
+ return { ...state, cursor: (state.cursor + 1) % n };
22
+ case 'space':
23
+ return {
24
+ ...state,
25
+ items: state.items.map((it, i) => (i === state.cursor ? { ...it, checked: !it.checked } : it)),
26
+ };
27
+ case 'all':
28
+ return { ...state, items: state.items.map((it) => ({ ...it, checked: true })) };
29
+ case 'none':
30
+ return { ...state, items: state.items.map((it) => ({ ...it, checked: false })) };
31
+ case 'enter':
32
+ return { ...state, done: true, selected: state.items.filter((it) => it.checked).map((it) => it.id) };
33
+ default:
34
+ return state;
35
+ }
36
+ }
37
+
38
+ export function renderCompanionMenu(state) {
39
+ const lines = [`Companions — ${HINT}:`];
40
+ state.items.forEach((it, i) => {
41
+ const cursor = i === state.cursor ? '>' : ' ';
42
+ const box = it.checked ? '[x]' : '[ ]';
43
+ lines.push(`${cursor} ${box} ${it.label}`);
44
+ });
45
+ return lines.join('\n');
46
+ }
47
+
48
+ export function mapKey(str, key = {}) {
49
+ const name = key.name;
50
+ if (name === 'up' || name === 'k') return 'up';
51
+ if (name === 'down' || name === 'j') return 'down';
52
+ if (name === 'space' || str === ' ') return 'space';
53
+ if (name === 'return' || name === 'enter') return 'enter';
54
+ if (str === 'a') return 'all';
55
+ if (str === 'n') return 'none';
56
+ return null;
57
+ }
58
+
59
+ // True when stdin/stdout can drive an interactive raw-mode menu.
60
+ export function canInteractiveSelect(input = process.stdin, output = process.stdout) {
61
+ return !!(input && output && input.isTTY && output.isTTY && typeof input.setRawMode === 'function');
62
+ }
63
+
64
+ // Run the raw-TTY checkbox menu; resolves with the selected ids. Caller must check
65
+ // canInteractiveSelect() first (otherwise use the text fallback).
66
+ export function selectCompanionsInteractive(companions, { input = process.stdin, output = process.stdout } = {}) {
67
+ return new Promise((resolve) => {
68
+ let state = initialCompanionMenu(companions);
69
+ let drawnLines = 0;
70
+
71
+ readline.emitKeypressEvents(input);
72
+ input.setRawMode(true);
73
+ input.resume();
74
+
75
+ const draw = () => {
76
+ if (drawnLines) {
77
+ readline.moveCursor(output, 0, -drawnLines);
78
+ readline.clearScreenDown(output);
79
+ }
80
+ const text = renderCompanionMenu(state);
81
+ output.write(`${text}\n`);
82
+ drawnLines = text.split('\n').length;
83
+ };
84
+
85
+ const cleanup = () => {
86
+ input.removeListener('keypress', onKey);
87
+ if (typeof input.setRawMode === 'function') input.setRawMode(false);
88
+ input.pause();
89
+ };
90
+
91
+ const onKey = (str, key = {}) => {
92
+ if (key.ctrl && key.name === 'c') {
93
+ cleanup();
94
+ output.write('\n');
95
+ process.exit(130);
96
+ }
97
+ const action = mapKey(str, key);
98
+ if (!action) return;
99
+ state = reduceCompanionMenu(state, action);
100
+ if (state.done) {
101
+ cleanup();
102
+ resolve(state.selected);
103
+ return;
104
+ }
105
+ draw();
106
+ };
107
+
108
+ input.on('keypress', onKey);
109
+ draw();
110
+ });
111
+ }
package/src/init.mjs CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  cavemanInstallerCommand,
22
22
  } from './taxonomy.mjs';
23
23
  import { renderVaultReadme } from './vault-readme.mjs';
24
+ import { canInteractiveSelect, selectCompanionsInteractive } from './companion-select.mjs';
24
25
 
25
26
  function parseArgs(argv) {
26
27
  const args = { mcp: true, yes: false, force: false };
@@ -129,7 +130,7 @@ export async function runInit(argv) {
129
130
  const fallback = join(projectPath, deriveVaultDirName(projectPath));
130
131
  if (process.stdin.isTTY && !args.yes) {
131
132
  const rl = createInterface({ input: process.stdin, output: process.stdout });
132
- const ans = (await rl.question(`Obsidian vault path [${fallback}]: `)).trim();
133
+ const ans = (await rl.question(`Obsidian vault path (Enter aceita o padrão, ou digite outro caminho)\n [${fallback}]\n> `)).trim();
133
134
  rl.close();
134
135
  vaultPath = ans || fallback;
135
136
  } else {
@@ -148,12 +149,19 @@ export async function runInit(argv) {
148
149
  companions = resolveCompanions({ companionsFlag: args.companions });
149
150
  } else if (process.stdin.isTTY && !args.yes) {
150
151
  log('\nCompanions (plugins/MCP opcionais — context-mode é a principal):');
151
- for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
152
- const def = resolveCompanions({}).join(',');
153
- const rl = createInterface({ input: process.stdin, output: process.stdout });
154
- const ans = (await rl.question(`Instalar quais? (vírgula) [${def}], "none" p/ nenhum: `)).trim();
155
- rl.close();
156
- companions = ans.toLowerCase() === 'none' ? [] : resolveCompanions({ companionsFlag: ans || def });
152
+ if (canInteractiveSelect()) {
153
+ companions = await selectCompanionsInteractive(COMPANIONS);
154
+ } else {
155
+ // Text fallback (no raw-mode TTY): list + comma input with clear instructions.
156
+ for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
157
+ const def = resolveCompanions({}).join(',');
158
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
159
+ const ans = (await rl.question(
160
+ `Digite os ids separados por vírgula (Enter aceita [${def}], "none" p/ nenhum): `,
161
+ )).trim();
162
+ rl.close();
163
+ companions = ans.toLowerCase() === 'none' ? [] : resolveCompanions({ companionsFlag: ans || def });
164
+ }
157
165
  } else {
158
166
  companions = resolveCompanions({});
159
167
  }