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 +1 -1
- package/src/companion-select.mjs +111 -0
- package/src/init.mjs +15 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.2.
|
|
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}]
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
}
|