wendkeep 0.9.0 → 0.10.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.
- package/CHANGELOG.md +28 -0
- package/hooks/session-stop.mjs +9 -0
- package/hooks/subagent-usage.mjs +176 -0
- package/package.json +1 -1
- package/src/companion-select.mjs +5 -4
- package/src/init.mjs +42 -6
- package/src/taxonomy.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to **wendkeep** are documented here. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.10.0] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Subagent & workflow telemetry — closing the biggest observability gap.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Subagent + workflow capture:** the Stop hook now scans the session's sibling subagent
|
|
13
|
+
transcripts (`<session>/subagents/**`) and workflow runs, and folds them into the session
|
|
14
|
+
note — a new `## Subagents & Workflows` section (aggregate + a collapsible per-subagent
|
|
15
|
+
table) plus frontmatter fields (`subagents_count`, `subagents_tokens_total`,
|
|
16
|
+
`subagents_custo_usd`, `tokens_total_incl_subagents`). Reuses the token-usage parser
|
|
17
|
+
(deduped per request). Previously a session that spawned a Workflow recorded ONLY the main
|
|
18
|
+
transcript — on a real audit session that hid **12 subagents / 4.6M tokens / $7.59** (2× the
|
|
19
|
+
main). The main `tokens_total` stays the main agent's (comparable to Claude Code's own
|
|
20
|
+
display); subagents are a separate axis.
|
|
21
|
+
- Provider-gated by structure (Claude Code's `subagents/` layout); fail-open — never blocks Stop.
|
|
22
|
+
|
|
23
|
+
## [0.9.1] — 2026-07-06
|
|
24
|
+
|
|
25
|
+
Interactive install UX: language first.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`wendkeep init` asks the vault language first** on an interactive TTY (when `--locale`
|
|
29
|
+
isn't passed): `[1] Português [2] English`. The answer drives the folders, scaffold and
|
|
30
|
+
skills — and the remaining prompts (vault path, companion selection) now render in the
|
|
31
|
+
chosen locale instead of always Portuguese. `--yes`, `--locale` and non-TTY are unchanged.
|
|
32
|
+
|
|
7
33
|
## [0.9.0] — 2026-07-06
|
|
8
34
|
|
|
9
35
|
Engineering debt: sensor editing + i18n coherence for auto-generated notes.
|
|
@@ -213,6 +239,8 @@ Initial release — the capture engine, extracted from a system in daily product
|
|
|
213
239
|
- `wendkeep init` (cross-platform installer) + optional `@bitbonsai/mcpvault` MCP server.
|
|
214
240
|
|
|
215
241
|
<!-- Only v0.4.0+ is tagged in git (history starts here); older versions link to npm. -->
|
|
242
|
+
[0.10.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.10.0
|
|
243
|
+
[0.9.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.1
|
|
216
244
|
[0.9.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.9.0
|
|
217
245
|
[0.8.1]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.1
|
|
218
246
|
[0.8.0]: https://github.com/rogersialves/wendkeep/releases/tag/v0.8.0
|
package/hooks/session-stop.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normali
|
|
|
8
8
|
import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
|
|
9
9
|
import { activeChangeLink } from './change-core.mjs';
|
|
10
10
|
import { getLocale } from './locale.mjs';
|
|
11
|
+
import { upsertSubagentUsage } from './subagent-usage.mjs';
|
|
11
12
|
import {
|
|
12
13
|
ensureDir,
|
|
13
14
|
findActiveSessionByTranscript,
|
|
@@ -1044,6 +1045,14 @@ function main() {
|
|
|
1044
1045
|
process.stderr.write(`[codex-obsidian] Token usage falhou: ${error.message}\n`);
|
|
1045
1046
|
}
|
|
1046
1047
|
|
|
1048
|
+
// Subagent/workflow telemetry (0.10.0): fold sibling subagent transcripts into the note.
|
|
1049
|
+
// Provider-gated by structure + fail-open — never derruba o Stop.
|
|
1050
|
+
try {
|
|
1051
|
+
upsertSubagentUsage(sessionPath, transcriptPath);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
process.stderr.write(`[codex-obsidian] Subagent usage falhou: ${error.message}\n`);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1047
1056
|
if (!shouldFinalizeSession()) {
|
|
1048
1057
|
writeControl(vaultBase, {
|
|
1049
1058
|
...control,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// hooks/subagent-usage.mjs — capture subagent + workflow telemetry (0.10.0). The Stop hook
|
|
2
|
+
// only reads the MAIN transcript; a session that spawns subagents/workflows (e.g. a Workflow
|
|
3
|
+
// run) burns tokens in sibling transcripts the note never recorded. This scans them.
|
|
4
|
+
// Reuses token-usage.mjs's parser. Provider-gated by structure (Claude Code layout).
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { basename, join } from 'node:path';
|
|
7
|
+
import { parseTokenUsageFromTranscript, summarizeTokenUsage } from './token-usage.mjs';
|
|
8
|
+
|
|
9
|
+
const fmt = (n) => Math.trunc(Number(n) || 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
10
|
+
const usd = (n) => `$${(Number(n) || 0).toFixed(4)}`;
|
|
11
|
+
|
|
12
|
+
// <slug>/<id>.jsonl (main transcript) -> <slug>/<id>/ (sibling dir with subagents/workflows).
|
|
13
|
+
export function sessionDirFromTranscript(transcriptPath) {
|
|
14
|
+
return String(transcriptPath || '').replace(/\.jsonl?$/i, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function walkAgentJsonl(dir) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let names;
|
|
20
|
+
try { names = readdirSync(dir); } catch { return out; }
|
|
21
|
+
for (const n of names) {
|
|
22
|
+
const p = join(dir, n);
|
|
23
|
+
let st;
|
|
24
|
+
try { st = statSync(p); } catch { continue; }
|
|
25
|
+
if (st.isDirectory()) out.push(...walkAgentJsonl(p));
|
|
26
|
+
else if (n.startsWith('agent-') && n.endsWith('.jsonl')) out.push(p);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// workflows/scripts/<name>-wf_<rid>.js -> { wf_<rid>: <name> }
|
|
32
|
+
function workflowNameMap(sessionDir) {
|
|
33
|
+
const map = {};
|
|
34
|
+
let names;
|
|
35
|
+
try { names = readdirSync(join(sessionDir, 'workflows', 'scripts')); } catch { return map; }
|
|
36
|
+
for (const n of names) {
|
|
37
|
+
const m = n.match(/^(.*)-(wf_[a-z0-9-]+)\.js$/i);
|
|
38
|
+
if (m) map[m[2]] = m[1];
|
|
39
|
+
}
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runIdOfPath(file) {
|
|
44
|
+
const m = file.replace(/\\/g, '/').match(/\/(wf_[a-z0-9-]+)\//i);
|
|
45
|
+
return m ? m[1] : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function tokensTotal(t = {}) {
|
|
49
|
+
return (t.input || 0) + (t.cached || 0) + (t.cacheWrite || 0) + (t.output || 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const round4 = (n) => Math.round((Number(n) || 0) * 10000) / 10000;
|
|
53
|
+
|
|
54
|
+
// Aggregate every subagent transcript under <sessionDir>/subagents. null when the dir is
|
|
55
|
+
// absent (Codex / a session with no subagents) or nothing parseable.
|
|
56
|
+
export function collectSubagentUsage(sessionDir) {
|
|
57
|
+
const subDir = join(sessionDir, 'subagents');
|
|
58
|
+
if (!existsSync(subDir)) return null;
|
|
59
|
+
const files = walkAgentJsonl(subDir);
|
|
60
|
+
if (!files.length) return null;
|
|
61
|
+
|
|
62
|
+
const nameMap = workflowNameMap(sessionDir);
|
|
63
|
+
const subagents = [];
|
|
64
|
+
const wf = {};
|
|
65
|
+
const usageAgg = { input: 0, cached: 0, cacheWrite: 0, output: 0 };
|
|
66
|
+
let count = 0;
|
|
67
|
+
let calls = 0;
|
|
68
|
+
let cost = 0;
|
|
69
|
+
|
|
70
|
+
for (const f of files) {
|
|
71
|
+
const summary = summarizeTokenUsage(parseTokenUsageFromTranscript(f));
|
|
72
|
+
if (!summary.calls) continue;
|
|
73
|
+
const runId = runIdOfPath(f);
|
|
74
|
+
const workflow = runId ? nameMap[runId] || runId : null;
|
|
75
|
+
let agentType = '';
|
|
76
|
+
try { agentType = JSON.parse(readFileSync(f.replace(/\.jsonl$/i, '.meta.json'), 'utf8')).agentType || ''; } catch { /* no meta */ }
|
|
77
|
+
const tokens = tokensTotal(summary.totals);
|
|
78
|
+
|
|
79
|
+
subagents.push({
|
|
80
|
+
id: basename(f).replace(/^agent-/, '').replace(/\.jsonl$/i, '').slice(0, 12),
|
|
81
|
+
agentType,
|
|
82
|
+
workflow,
|
|
83
|
+
model: summary.models[0] || '?',
|
|
84
|
+
tools: summary.tools.length,
|
|
85
|
+
calls: summary.calls,
|
|
86
|
+
tokens,
|
|
87
|
+
cost: round4(summary.costs.model),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
count += 1;
|
|
91
|
+
calls += summary.calls;
|
|
92
|
+
cost += summary.costs.model;
|
|
93
|
+
for (const k of Object.keys(usageAgg)) usageAgg[k] += summary.totals[k] || 0;
|
|
94
|
+
if (runId) {
|
|
95
|
+
wf[runId] = wf[runId] || { agents: 0, cost: 0 };
|
|
96
|
+
wf[runId].agents += 1;
|
|
97
|
+
wf[runId].cost += summary.costs.model;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!count) return null;
|
|
101
|
+
|
|
102
|
+
const workflows = Object.entries(wf).map(([runId, v]) => ({
|
|
103
|
+
runId, name: nameMap[runId] || runId, agents: v.agents, cost: round4(v.cost),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
subagents,
|
|
108
|
+
workflows,
|
|
109
|
+
aggregate: { count, calls, tokens: tokensTotal(usageAgg), cost: round4(cost), usage: usageAgg },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function renderSubagentSection(c) {
|
|
114
|
+
const a = c.aggregate;
|
|
115
|
+
const wf = c.workflows.length
|
|
116
|
+
? c.workflows.map((w) => `${w.name} (${w.runId} · ${w.agents} agentes · ${usd(w.cost)})`).join('; ')
|
|
117
|
+
: '(nenhum)';
|
|
118
|
+
const rows = c.subagents
|
|
119
|
+
.map((s) => `| ${s.id} | ${s.agentType || '-'} | ${s.workflow || '-'} | ${s.model} | ${s.tools} | ${fmt(s.tokens)} | ${usd(s.cost)} |`)
|
|
120
|
+
.join('\n');
|
|
121
|
+
return `## Subagents & Workflows
|
|
122
|
+
|
|
123
|
+
> Custo de subagents/workflows desta sessão — NÃO incluído no total principal acima.
|
|
124
|
+
|
|
125
|
+
- **Subagents:** ${a.count} · ${a.calls} chamadas · ${fmt(a.tokens)} tokens · ${usd(a.cost)}
|
|
126
|
+
- **Workflows:** ${wf}
|
|
127
|
+
|
|
128
|
+
<details><summary>Por subagent (${a.count})</summary>
|
|
129
|
+
|
|
130
|
+
| Agent | Tipo | Workflow | Modelo | Tools | Tokens | Custo |
|
|
131
|
+
|---|---|---|---|---:|---:|---:|
|
|
132
|
+
${rows}
|
|
133
|
+
|
|
134
|
+
</details>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function setFrontmatterField(content, key, value) {
|
|
138
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
139
|
+
if (!m) return content;
|
|
140
|
+
const re = new RegExp(`^${key}:.*$`, 'm');
|
|
141
|
+
const line = `${key}: ${value}`;
|
|
142
|
+
const fm = re.test(m[1]) ? m[1].replace(re, line) : `${m[1]}\n${line}`;
|
|
143
|
+
return content.replace(m[0], `---\n${fm}\n---`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function frontmatterNumber(content, key) {
|
|
147
|
+
const m = content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
148
|
+
return m ? Number(String(m[1]).trim().replace(/^["']|["']$/g, '')) || 0 : 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function upsertSection(content, heading, body) {
|
|
152
|
+
const esc = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
153
|
+
const re = new RegExp(`${esc}\\n[\\s\\S]*?(?=\\n## |$)`);
|
|
154
|
+
if (re.test(content)) return content.replace(re, `${body}\n`);
|
|
155
|
+
for (const anchor of ['\n## Encerramento', '\n## Issues Linear', '\n## Pendências']) {
|
|
156
|
+
const i = content.indexOf(anchor);
|
|
157
|
+
if (i >= 0) return `${content.slice(0, i)}\n\n${body}\n${content.slice(i)}`;
|
|
158
|
+
}
|
|
159
|
+
return `${content.trimEnd()}\n\n${body}\n`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Stop-hook entry: scan the session's subagents/workflows, fold into the note. Fail-open.
|
|
163
|
+
export function upsertSubagentUsage(sessionPath, transcriptPath) {
|
|
164
|
+
if (!sessionPath || !existsSync(sessionPath)) return false;
|
|
165
|
+
const collected = collectSubagentUsage(sessionDirFromTranscript(transcriptPath));
|
|
166
|
+
if (!collected) return false;
|
|
167
|
+
const a = collected.aggregate;
|
|
168
|
+
let content = readFileSync(sessionPath, 'utf8');
|
|
169
|
+
content = setFrontmatterField(content, 'subagents_count', a.count);
|
|
170
|
+
content = setFrontmatterField(content, 'subagents_tokens_total', a.tokens);
|
|
171
|
+
content = setFrontmatterField(content, 'subagents_custo_usd', a.cost);
|
|
172
|
+
content = setFrontmatterField(content, 'tokens_total_incl_subagents', frontmatterNumber(content, 'tokens_total') + a.tokens);
|
|
173
|
+
content = upsertSection(content, '## Subagents & Workflows', renderSubagentSection(collected));
|
|
174
|
+
writeFileSync(sessionPath, content, 'utf8');
|
|
175
|
+
return true;
|
|
176
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A persistent-memory harness for AI coding agents on your Obsidian vault: turn-by-turn session capture plus a native, zero-dependency spec→change→verify→archive loop (sensor-gated, independent verdict, mutation discrimination). Local-first, agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/companion-select.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import readline from 'node:readline';
|
|
5
5
|
|
|
6
6
|
const HINT = 'Espaço marca/desmarca · ↑/↓ move · a=todos · n=nenhum · Enter confirma';
|
|
7
|
+
const HEADER = 'Companions';
|
|
7
8
|
|
|
8
9
|
export function initialCompanionMenu(companions) {
|
|
9
10
|
return {
|
|
@@ -35,8 +36,8 @@ export function reduceCompanionMenu(state, action) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export function renderCompanionMenu(state) {
|
|
39
|
-
const lines = [
|
|
39
|
+
export function renderCompanionMenu(state, { hint = HINT, header = HEADER } = {}) {
|
|
40
|
+
const lines = [`${header} — ${hint}:`];
|
|
40
41
|
state.items.forEach((it, i) => {
|
|
41
42
|
const cursor = i === state.cursor ? '>' : ' ';
|
|
42
43
|
const box = it.checked ? '[x]' : '[ ]';
|
|
@@ -63,7 +64,7 @@ export function canInteractiveSelect(input = process.stdin, output = process.std
|
|
|
63
64
|
|
|
64
65
|
// Run the raw-TTY checkbox menu; resolves with the selected ids. Caller must check
|
|
65
66
|
// canInteractiveSelect() first (otherwise use the text fallback).
|
|
66
|
-
export function selectCompanionsInteractive(companions, { input = process.stdin, output = process.stdout } = {}) {
|
|
67
|
+
export function selectCompanionsInteractive(companions, { input = process.stdin, output = process.stdout, labels } = {}) {
|
|
67
68
|
return new Promise((resolve) => {
|
|
68
69
|
let state = initialCompanionMenu(companions);
|
|
69
70
|
let drawnLines = 0;
|
|
@@ -77,7 +78,7 @@ export function selectCompanionsInteractive(companions, { input = process.stdin,
|
|
|
77
78
|
readline.moveCursor(output, 0, -drawnLines);
|
|
78
79
|
readline.clearScreenDown(output);
|
|
79
80
|
}
|
|
80
|
-
const text = renderCompanionMenu(state);
|
|
81
|
+
const text = renderCompanionMenu(state, labels);
|
|
81
82
|
output.write(`${text}\n`);
|
|
82
83
|
drawnLines = text.split('\n').length;
|
|
83
84
|
};
|
package/src/init.mjs
CHANGED
|
@@ -192,17 +192,55 @@ function installVaultColors(vaultPath) {
|
|
|
192
192
|
|
|
193
193
|
// --- main -------------------------------------------------------------------
|
|
194
194
|
|
|
195
|
+
// Interactive prompt strings by locale. The language question itself is bilingual (asked
|
|
196
|
+
// before the locale is known); everything after follows the answer.
|
|
197
|
+
const PROMPTS = {
|
|
198
|
+
'pt-BR': {
|
|
199
|
+
vault: (f) => `Caminho do vault Obsidian (Enter aceita o padrão, ou digite outro)\n [${f}]\n> `,
|
|
200
|
+
companionsHeader: '\nCompanions (plugins/MCP opcionais — context-mode é a principal):',
|
|
201
|
+
companionsAsk: (def) => `Digite os ids separados por vírgula (Enter aceita [${def}], "none" p/ nenhum): `,
|
|
202
|
+
menu: { hint: 'Espaço marca/desmarca · ↑/↓ move · a=todos · n=nenhum · Enter confirma', header: 'Companions' },
|
|
203
|
+
},
|
|
204
|
+
en: {
|
|
205
|
+
vault: (f) => `Obsidian vault path (Enter for the default, or type another)\n [${f}]\n> `,
|
|
206
|
+
companionsHeader: '\nCompanions (optional plugins/MCP — context-mode is the main one):',
|
|
207
|
+
companionsAsk: (def) => `Enter ids comma-separated (Enter for [${def}], "none" for none): `,
|
|
208
|
+
menu: { hint: 'Space toggles · ↑/↓ move · a=all · n=none · Enter confirms', header: 'Companions' },
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export function promptStrings(localeId) {
|
|
213
|
+
return PROMPTS[localeId] || PROMPTS['pt-BR'];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Map the language answer to a locale id. 2/en → en; 1/pt/empty/unknown → pt-BR.
|
|
217
|
+
export function parseLocaleAnswer(ans) {
|
|
218
|
+
const a = String(ans || '').trim().toLowerCase();
|
|
219
|
+
if (a === '2' || a === 'en' || a === 'english') return 'en';
|
|
220
|
+
return 'pt-BR';
|
|
221
|
+
}
|
|
222
|
+
|
|
195
223
|
export async function runInit(argv) {
|
|
196
224
|
const args = parseArgs(argv);
|
|
197
225
|
const projectPath = resolve(args.project || process.cwd());
|
|
198
226
|
const log = (s) => process.stdout.write(`${s}\n`);
|
|
199
227
|
|
|
228
|
+
// Language first (i18n): an interactive TTY without --locale is asked the vault language;
|
|
229
|
+
// folders, prompts and scaffold all follow the answer.
|
|
230
|
+
if (!args.locale && process.stdin.isTTY && !args.yes) {
|
|
231
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
232
|
+
const ans = await rl.question('Idioma do vault / Vault language:\n [1] Português (pt-BR) [2] English (en)\n> ');
|
|
233
|
+
rl.close();
|
|
234
|
+
args.locale = parseLocaleAnswer(ans);
|
|
235
|
+
}
|
|
236
|
+
const P = promptStrings(args.locale && LOCALES[args.locale] ? args.locale : DEFAULT_LOCALE);
|
|
237
|
+
|
|
200
238
|
let vaultPath = args.vault;
|
|
201
239
|
if (!vaultPath) {
|
|
202
240
|
const fallback = join(projectPath, deriveVaultDirName(projectPath));
|
|
203
241
|
if (process.stdin.isTTY && !args.yes) {
|
|
204
242
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
205
|
-
const ans = (await rl.question(
|
|
243
|
+
const ans = (await rl.question(P.vault(fallback))).trim();
|
|
206
244
|
rl.close();
|
|
207
245
|
vaultPath = ans || fallback;
|
|
208
246
|
} else {
|
|
@@ -222,16 +260,14 @@ export async function runInit(argv) {
|
|
|
222
260
|
} else if (process.stdin.isTTY && !args.yes) {
|
|
223
261
|
if (canInteractiveSelect()) {
|
|
224
262
|
log(''); // the checkbox menu renders its own header
|
|
225
|
-
companions = await selectCompanionsInteractive(COMPANIONS);
|
|
263
|
+
companions = await selectCompanionsInteractive(COMPANIONS, { labels: P.menu });
|
|
226
264
|
} else {
|
|
227
265
|
// Text fallback (no raw-mode TTY): list + comma input with clear instructions.
|
|
228
|
-
log(
|
|
266
|
+
log(P.companionsHeader);
|
|
229
267
|
for (const c of COMPANIONS) log(` ${c.default ? '[x]' : '[ ]'} ${c.label}`);
|
|
230
268
|
const def = resolveCompanions({}).join(',');
|
|
231
269
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
232
|
-
const ans = (await rl.question(
|
|
233
|
-
`Digite os ids separados por vírgula (Enter aceita [${def}], "none" p/ nenhum): `,
|
|
234
|
-
)).trim();
|
|
270
|
+
const ans = (await rl.question(P.companionsAsk(def))).trim();
|
|
235
271
|
rl.close();
|
|
236
272
|
companions = ans.toLowerCase() === 'none' ? [] : resolveCompanions({ companionsFlag: ans || def });
|
|
237
273
|
}
|