@kikedealba/recap 0.10.0 → 0.11.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/README.md CHANGED
@@ -37,9 +37,14 @@ Requisitos: Node 24 o superior, ffmpeg, whisper.cpp (`whisper-cli`) y
37
37
  `start`, `stop`, `cancel` y `amend` de las entradas `remote-meeting` e
38
38
  `in-person-meeting`. Reemplaza al viejo `bita hooks add` y quita ese hook si
39
39
  lo encuentra, para que no se dispare dos veces;
40
- - instala la skill y los comandos en los agentes que encuentre: Claude Code,
41
- opencode, Codex y Gemini CLI (`--agents codex,gemini`, `--agents all` o
42
- `--agents none`). Si el plugin de Claude Code ya está instalado, no lo duplica.
40
+ - en Claude Code agrega o actualiza el marketplace `KikeDeAlba/recap` e instala
41
+ o actualiza el plugin `recap@recap` (`claude plugin marketplace add|update` y
42
+ `claude plugin install|update --scope user`), así el plugin queda en la misma
43
+ versión que la CLI; también quita los archivos sueltos que versiones viejas
44
+ dejaron en `~/.claude`;
45
+ - en opencode, Codex y Gemini CLI instala la skill y las skills de usuario en el
46
+ formato de cada uno (`--agents codex,gemini`, `--agents all` o
47
+ `--agents none`).
43
48
 
44
49
  En Windows y Linux `recap start` falla con `CAPTURE_UNAVAILABLE`: graba con la
45
50
  app que quieras y procesa el archivo con
@@ -133,26 +138,47 @@ recap prompt <meeting> # print the summary prompt with transcrip
133
138
  recap save-summary <meeting> file.md # store minutes written elsewhere
134
139
  ```
135
140
 
136
- ## Claude Code plugin
141
+ ## Plugin de Claude Code
137
142
 
138
- The repository is also a Claude Code plugin marketplace:
143
+ El repositorio también es un marketplace de plugins de Claude Code. `recap setup`
144
+ lo instala y lo mantiene al día; a mano sería:
139
145
 
140
146
  ```
141
- /plugin marketplace add KikeDeAlba/recap
142
- /plugin install recap@recap
147
+ claude plugin marketplace add KikeDeAlba/recap
148
+ claude plugin install recap@recap --scope user
149
+ claude plugin marketplace update recap && claude plugin update recap@recap # al actualizar la CLI
143
150
  ```
144
151
 
145
- | Command | What it does |
146
- |---|---|
147
- | `/recap-start [remota\|presencial] [título]` | Starts a recording; infers the mode from the arguments or the conversation and asks when it cannot |
148
- | `/recap-stop [--wait]` | Stops the recording; with `--wait` it processes in the foreground and shows agreements and action items |
149
- | `/recap-status` | Shows whether a recording is running and how far processing got |
150
- | `/recap-list [meeting]` | Lists meetings or shows one meeting's minutes |
151
- | `/recap-summarize [meeting] [instructions]` | Rewrites the minutes inside the session, following extra instructions, and stores them with `recap save-summary` |
152
- | `/recap-ask [question]` | Answers the last question of the meeting being recorded, or the one given, from the inkwell pages and the project repositories |
153
- | `/recap-proposals [meeting]` | Reviews the documentation changes proposed by a meeting and accepts, edits or rejects them |
154
-
155
- The `recap` skill lets Claude answer questions such as "¿qué acordamos en la reunión de ayer?" from the stored minutes and transcripts. The commands call `recap`, so it must be on the `PATH` of the shell Claude Code runs.
152
+ Trae tres cosas:
153
+
154
+ - **La skill `recap`**, que Claude carga sola cuando la conversación trata de
155
+ reuniones ("¿qué acordamos en la reunión de ayer?"). Es la única fuente del
156
+ flujo de una reunión; bita y tally enlazan a ella. Lo largo (cierre con bita e
157
+ inkwell, en vivo, propuestas, espacio) vive en archivos aparte que se leen
158
+ solo cuando hacen falta.
159
+ - **Skills de usuario** con `disable-model-invocation: true`: no ocupan contexto
160
+ y solo corren cuando las escribes.
161
+
162
+ | Skill | Qué hace |
163
+ |---|---|
164
+ | `/recap:recap-start [remota\|presencial] [título]` | Empieza a grabar; saca el modo de los argumentos o de la conversación y pregunta si no puede |
165
+ | `/recap:recap-stop [--no-wait]` | Para la grabación (por bita si la sigue un cronómetro), espera el procesamiento y muestra el resultado |
166
+ | `/recap:recap-status` | Dice si se está grabando y en qué va el procesamiento |
167
+ | `/recap:recap-list [reunión]` | Lista las reuniones o muestra la minuta de una |
168
+ | `/recap:recap-summarize [reunión] [indicaciones]` | Rehace la minuta dentro de la sesión y la guarda con `recap save-summary` |
169
+ | `/recap:recap-ask [pregunta]` | Responde la última pregunta de la reunión en curso, o la que le pases, con las páginas de inkwell y los repos del proyecto |
170
+ | `/recap:recap-proposals [reunión]` | Revisa los cambios a la documentación que propuso una reunión |
171
+
172
+ - **Un monitor** (`monitors/monitors.json`) que corre `recap watch` en segundo
173
+ plano en cada sesión interactiva. Revisa las carpetas de reuniones cada 5 s,
174
+ sin llamar al modelo, y escribe una sola línea cuando una reunión termina de
175
+ procesarse o falla; esa línea le llega a Claude como notificación. No corre
176
+ con `claude -p`. Si `recap` no está en el `PATH`, no hace nada. Para apagarlo,
177
+ define `RECAP_MONITOR=off` en el entorno de Claude Code (por ejemplo en
178
+ `"env"` de `~/.claude/settings.json`).
179
+
180
+ Las skills llaman a `recap`, así que tiene que estar en el `PATH` del shell que
181
+ usa Claude Code.
156
182
 
157
183
  ## Live assistant
158
184
 
@@ -0,0 +1,109 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadConfig, rootDir } from "../../core/config.js";
4
+ import { MEETING_FILE, loadMeeting } from "../../core/meeting.js";
5
+ import { sleep } from "../../core/proc.js";
6
+ import { suffix, trimmed } from "../../core/text.js";
7
+ import { usageError } from "../../errors.js";
8
+ import { parseArgs } from "../args.js";
9
+ import { output } from "../output.js";
10
+ import { failedStage, isSettled } from "./query.js";
11
+ export const WATCH_OPT_OUT = 'RECAP_MONITOR';
12
+ const NOTICE = ' (FYI: act on it only if this session is handling that meeting)';
13
+ export function watchDisabled(env = process.env) {
14
+ const value = trimmed(env[WATCH_OPT_OUT] ?? '').toLowerCase();
15
+ return ['0', 'off', 'false', 'no'].includes(value);
16
+ }
17
+ export function describeSettled(meeting, dir) {
18
+ const failed = failedStage(meeting);
19
+ const base = { id: meeting.id, title: meeting.title, status: meeting.status, dir };
20
+ if (meeting.status !== 'processed') {
21
+ const where = failed ? ` at ${failed[0]}` : '';
22
+ const reason = suffix(trimmed((failed ? failed[1].error : meeting.error) ?? 'no reason given').replace(/\s+/g, ' '), 200);
23
+ return { ...base, failed: true, message: `recap: meeting "${meeting.title}" failed${where}: ${reason} (${meeting.id}); run \`recap process ${meeting.id}\` to resume${NOTICE}` };
24
+ }
25
+ const parts = [`recap: meeting "${meeting.title}" finished processing (${meeting.id})`, `minutes at ${path.join(dir, 'summary.md')}`];
26
+ const wrapup = meeting.wrapup;
27
+ if (wrapup?.pageId !== undefined)
28
+ parts.push(`inkwell page #${wrapup.pageId}`);
29
+ if (wrapup && !wrapup.projectResolved)
30
+ parts.push('project unresolved');
31
+ return { ...base, failed: false, message: `${parts.join('; ')}${NOTICE}` };
32
+ }
33
+ export class MeetingWatcher {
34
+ root;
35
+ seen = new Map();
36
+ primed = false;
37
+ constructor(root) {
38
+ this.root = root;
39
+ }
40
+ tick() {
41
+ let names;
42
+ try {
43
+ names = readdirSync(this.root);
44
+ }
45
+ catch {
46
+ names = [];
47
+ }
48
+ const events = [];
49
+ const present = new Set();
50
+ for (const name of names) {
51
+ const dir = path.join(this.root, name);
52
+ let mtimeMs;
53
+ try {
54
+ mtimeMs = statSync(path.join(dir, MEETING_FILE)).mtimeMs;
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ present.add(name);
60
+ const before = this.seen.get(name);
61
+ if (before && before.mtimeMs === mtimeMs)
62
+ continue;
63
+ let meeting;
64
+ try {
65
+ meeting = loadMeeting(dir);
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ const settled = isSettled(meeting);
71
+ this.seen.set(name, { mtimeMs, settled });
72
+ const wasSettled = before ? before.settled : !this.primed;
73
+ if (settled && !wasSettled)
74
+ events.push(describeSettled(meeting, dir));
75
+ }
76
+ for (const name of [...this.seen.keys()])
77
+ if (!present.has(name))
78
+ this.seen.delete(name);
79
+ this.primed = true;
80
+ return events;
81
+ }
82
+ }
83
+ export async function watchCommand(argv) {
84
+ const json = argv.includes('--json');
85
+ let interval;
86
+ let root;
87
+ try {
88
+ const args = parseArgs(argv, { values: ['interval'], maxPositionals: 0 });
89
+ interval = args.number('interval') ?? 5;
90
+ if (interval < 1 || interval > 300)
91
+ throw usageError('--interval takes seconds between 1 and 300');
92
+ root = rootDir(loadConfig());
93
+ }
94
+ catch (error) {
95
+ return output('watch', json, () => {
96
+ throw error;
97
+ });
98
+ }
99
+ if (watchDisabled())
100
+ return 0;
101
+ process.stdout.on('error', () => process.exit(0));
102
+ const watcher = new MeetingWatcher(root);
103
+ watcher.tick();
104
+ for (;;) {
105
+ await sleep(interval * 1000);
106
+ for (const event of watcher.tick())
107
+ process.stdout.write(`${json ? JSON.stringify(event) : event.message}\n`);
108
+ }
109
+ }
@@ -12,6 +12,7 @@ import { proposalsCommand } from "./commands/proposals.js";
12
12
  import { listCommand, processCommand, promptCommand, saveSummaryCommand, showCommand, waitCommand } from "./commands/query.js";
13
13
  import { discardCommand, importCommand, startCommand, statusCommand, stopCommand } from "./commands/recording.js";
14
14
  import { setupCommand } from "./commands/setup.js";
15
+ import { watchCommand } from "./commands/watch.js";
15
16
  import { output } from "./output.js";
16
17
  async function capabilitiesCommand(argv) {
17
18
  return output('capabilities', argv.includes('--json'), async () => {
@@ -42,6 +43,7 @@ const COMMANDS = {
42
43
  ask: askCommand,
43
44
  proposals: proposalsCommand,
44
45
  config: configCommand,
46
+ watch: watchCommand,
45
47
  'live-worker': liveWorkerCommand,
46
48
  capabilities: capabilitiesCommand,
47
49
  };
@@ -64,6 +66,7 @@ export function help() {
64
66
  ' recap proposals ls|show|accept|reject <meeting> [n] [--bita-entry id] [--md file]',
65
67
  ' recap compress-video <meeting> --preset light|medium|max | strip-video <meeting> | prune <meeting> --intermediates | delete <meeting>',
66
68
  ' recap config get [key] | config set <key> <value>',
69
+ ' recap watch [--interval s]: one line per meeting that finishes processing; RECAP_MONITOR=off turns it off',
67
70
  ' recap setup [--install-deps] [--skip-models] [--skip-permissions] [--skip-bita] [--skip-app] [--agents detected|all|none|claude,opencode,codex,gemini]',
68
71
  ' recap capabilities',
69
72
  '',
@@ -3,9 +3,6 @@ import path from 'node:path';
3
3
  import { agents } from '@kikedealba/kit';
4
4
  import { defineManifest, isInstalled, readManifest, registerTool } from '@kikedealba/kit/registry';
5
5
  import { bitaCommand } from "../bita/client.js";
6
- import { readText } from "../core/fsutil.js";
7
- import { isRecord } from "../core/json.js";
8
- import { home } from "../core/paths.js";
9
6
  import { stableCommand } from "../core/self.js";
10
7
  import { HOOK_EVENTS, MEETING_KINDS } from "../bita/hook.js";
11
8
  import { PACKAGE_ROOT, VERSION } from "../version.js";
@@ -47,24 +44,29 @@ export async function bitaLinked(config) {
47
44
  return false;
48
45
  return (await bitaCommand(config)) !== null;
49
46
  }
50
- export function commandSources() {
51
- const dir = path.join(PACKAGE_ROOT, 'commands');
47
+ export const MAIN_SKILL = 'recap';
48
+ export const CLAUDE_PLUGIN = { marketplace: 'KikeDeAlba/recap', marketplaceName: 'recap', plugin: 'recap' };
49
+ export function userSkillSources() {
50
+ const dir = path.join(PACKAGE_ROOT, 'skills');
52
51
  let names = [];
53
52
  try {
54
- names = readdirSync(dir).filter((name) => name.endsWith('.md')).sort();
53
+ names = readdirSync(dir, { withFileTypes: true })
54
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${MAIN_SKILL}-`))
55
+ .map((entry) => entry.name)
56
+ .sort();
55
57
  }
56
58
  catch {
57
59
  return [];
58
60
  }
59
- return names.map((name) => ({ name: name.slice(0, -3), file: path.join(dir, name) }));
61
+ return names.map((name) => ({ name, dir: path.join(dir, name) }));
60
62
  }
61
63
  export function integration() {
62
64
  return {
63
65
  tool: 'recap',
64
66
  version: VERSION,
65
67
  description: 'Record meetings and turn them into minutes with recap',
66
- skills: [{ name: 'recap', dir: path.join(PACKAGE_ROOT, 'skills', 'recap') }],
67
- commands: commandSources(),
68
+ skills: [{ name: MAIN_SKILL, dir: path.join(PACKAGE_ROOT, 'skills', MAIN_SKILL) }],
69
+ userSkills: userSkillSources(),
68
70
  claude: {
69
71
  permissions: {
70
72
  allow: ['Bash(recap status:*)', 'Bash(recap list:*)', 'Bash(recap show:*)', 'Bash(recap wait:*)', 'Bash(recap prompt:*)', 'Bash(recap --version)'],
@@ -72,19 +74,9 @@ export function integration() {
72
74
  },
73
75
  };
74
76
  }
75
- export function claudePluginInstalled() {
76
- const claudeHome = process.env['CLAUDE_CONFIG_DIR'] ?? path.join(home(), '.claude');
77
- const text = readText(path.join(claudeHome, 'plugins', 'installed_plugins.json'));
78
- if (text === null)
79
- return false;
80
- try {
81
- const value = JSON.parse(text);
82
- const plugins = isRecord(value) && isRecord(value['plugins']) ? value['plugins'] : isRecord(value) ? value : {};
83
- return Object.keys(plugins).some((key) => key.startsWith('recap@'));
84
- }
85
- catch {
86
- return text.includes('"recap@');
87
- }
77
+ export function legacyClaudeIntegration() {
78
+ const { claude: _permissions, ...base } = integration();
79
+ return { ...base, commands: (base.userSkills ?? []).map((skill) => ({ name: skill.name, file: path.join(skill.dir, 'SKILL.md') })) };
88
80
  }
89
81
  export function parseAgents(raw) {
90
82
  if (raw === undefined || raw === 'detected')
@@ -97,16 +89,19 @@ export function parseAgents(raw) {
97
89
  throw new Error(`Unknown agent ${invalid.join(', ')}; use ${agents.AGENTS.join(', ')}, detected, all or none`);
98
90
  return wanted;
99
91
  }
100
- export async function installAgents(choice, exec) {
92
+ export async function installAgents(choice, options = {}) {
101
93
  if (choice === 'none')
102
94
  return [];
103
- let list = choice === 'detected' ? await agents.detectAgents() : choice === 'all' ? [...agents.AGENTS] : choice;
95
+ const list = choice === 'detected' ? await agents.detectAgents(options.ctx, agents.agentHomes(options.ctx, options.homes), options.locate) : choice === 'all' ? [...agents.AGENTS] : choice;
104
96
  const steps = [];
105
- if (list.includes('claude') && claudePluginInstalled()) {
106
- list = list.filter((agent) => agent !== 'claude');
107
- steps.push({ agent: 'claude', item: 'integration', state: 'present', detail: 'the recap plugin for Claude Code is installed; it already carries the skill and commands' });
97
+ if (list.includes('claude')) {
98
+ const plugin = await agents.installClaudePlugin(CLAUDE_PLUGIN, options);
99
+ steps.push(...plugin);
100
+ if (plugin.every((step) => step.state === 'installed' || step.state === 'present'))
101
+ steps.push(...(await agents.removeLegacyClaude(legacyClaudeIntegration(), options)));
108
102
  }
109
- if (list.length === 0)
103
+ const others = list.filter((agent) => agent !== 'claude');
104
+ if (others.length === 0)
110
105
  return steps;
111
- return [...steps, ...(await agents.installIntegration(integration(), { agents: list, ...(exec ? { exec } : {}) }))];
106
+ return [...steps, ...(await agents.installIntegration(integration(), { ...options, agents: others }))];
112
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kikedealba/recap",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Meeting recorder CLI: local transcription with whisper.cpp, minutes written by Claude Code, live answers, bita timers and inkwell docs.",
5
5
  "license": "MIT",
6
6
  "author": "Jesus Enrique De Alba Gaytan",
@@ -35,7 +35,6 @@
35
35
  "dist",
36
36
  "Resources/*.md",
37
37
  "skills",
38
- "commands",
39
38
  "README.md",
40
39
  "LICENSE"
41
40
  ],
@@ -50,7 +49,7 @@
50
49
  "recap": "node src/bin/recap.ts"
51
50
  },
52
51
  "dependencies": {
53
- "@kikedealba/kit": "^0.2.0"
52
+ "@kikedealba/kit": "^0.3.0"
54
53
  },
55
54
  "devDependencies": {
56
55
  "@types/node": "^24.0.0",
@@ -1,26 +1,31 @@
1
1
  ---
2
2
  name: recap
3
- description: Graba reuniones con la CLI `recap` y las convierte en minuta (resumen, acuerdos, pendientes y preguntas abiertas). Úsala cuando el usuario quiera empezar o terminar de grabar una reunión, saber si se está grabando, regenerar la minuta, o consultar qué se habló, qué se acordó o qué quedó pendiente en reuniones pasadas. Frases que la disparan: "graba la reunión", "empieza a grabar la junta", "ya terminó la reunión", "para la grabación", "¿se está grabando?", "¿qué acordamos en la reunión de ayer?", "pendientes de la junta con X", "rehaz la minuta".
3
+ description: Graba reuniones con la CLI recap y las vuelve minuta con acuerdos y pendientes. Frases como "graba la reunión", "ya terminó la reunión", "¿se está grabando?", "¿qué acordamos en la reunión de ayer?".
4
4
  ---
5
5
 
6
6
  # recap
7
7
 
8
8
  `recap` graba reuniones (en macOS, con Recap.app) y las procesa localmente en
9
- macOS, Windows y Linux. Todo vive en una carpeta por reunión bajo `~/Recap` (o
10
- la raíz configurada): `meeting.json`, la grabación, `transcript.md`, `frames/` y
9
+ macOS, Windows y Linux. Cada reunión es una carpeta bajo `~/Recap` (o la raíz
10
+ configurada) con `meeting.json`, la grabación, `transcript.md`, `frames/` y
11
11
  `summary.md`.
12
12
 
13
- Fuera de macOS no se puede grabar (`start` falla con `CAPTURE_UNAVAILABLE`):
14
- la reunión se graba con cualquier otra app y se procesa con
15
- `recap import <archivo> [--title "…"] [--in-person|--remote] --json`.
13
+ Quién hace qué: bita mide el tiempo, recap graba y escribe la minuta, inkwell
14
+ guarda la documentación, tally vuelca el tiempo a Jira y atl habla con Jira y
15
+ Confluence. Este es el único lugar donde se describe el flujo de una reunión;
16
+ las demás herramientas enlazan aquí.
16
17
 
17
18
  Todos los comandos aceptan `--json` y devuelven un sobre
18
19
  `{schemaVersion, ok, command, data, error}`. Usa `--json` siempre que vayas a
19
20
  leer el resultado; el texto plano es para mostrarlo tal cual.
20
21
 
21
- ## Modos
22
+ Fuera de macOS no se puede grabar (`start` falla con `CAPTURE_UNAVAILABLE`):
23
+ la reunión se graba con otra app y se procesa con
24
+ `recap import <archivo> [--title "…"] [--in-person|--remote] --json`.
22
25
 
23
- Hay que elegir el modo siempre; no tiene valor por defecto.
26
+ ## Modo
27
+
28
+ Hay que elegirlo siempre; no tiene valor por defecto.
24
29
 
25
30
  | Modo | Flag | Cuándo |
26
31
  |---|---|---|
@@ -28,154 +33,49 @@ Hay que elegir el modo siempre; no tiene valor por defecto.
28
33
  | Presencial | `--in-person` | En sala, en oficina, en persona, junta física, comida, "aquí con" |
29
34
 
30
35
  En remota se graba pantalla, audio del sistema y micrófono; en presencial solo
31
- el micrófono, sin pantalla ni capturas. Si el contexto no deja claro cuál es,
32
- pregunta con una sola línea ("¿remota o presencial?") antes de arrancar.
33
-
34
- ## Ciclo
35
-
36
- ```sh
37
- recap start --remote "Planeación sprint 42"
38
- recap status
39
- recap stop
40
- ```
41
-
42
- - `start` falla con `ALREADY_RECORDING` si ya hay una grabación activa: no
43
- arranques otra, avísalo.
44
- - `stop` cierra el archivo y lanza el procesamiento en segundo plano
45
- (transcripción, capturas y minuta). Tarda de segundos a unos minutos según la
46
- duración. `recap status` y `recap show <id>` dicen en qué etapa va.
47
- - `recap discard` descarta la grabación activa y borra su carpeta. Solo si el
48
- usuario lo pide explícitamente.
49
-
50
- ## Liberar espacio
51
-
52
- Solo si el usuario lo pide; todos aceptan `<id>` o `--bita-entry <id>` y `--json`.
53
-
54
- ```sh
55
- recap compress-video <id> --preset light|medium|max # recomprime el video en HEVC (solo remotas)
56
- recap strip-video <id> # quita el video y deja recording.m4a con ambas pistas de audio
57
- recap prune <id> --intermediates # borra mic.wav, system.wav, transcript-mic/system.json y live/chunks
58
- recap delete <id> # borra la carpeta completa de la reunión
59
- ```
60
-
61
- `compress-video` y `strip-video` aceptan `--prune-intermediates`. Fallan con
62
- `NOT_REMOTE` en reuniones presenciales, `NO_VIDEO` si ya no hay video,
63
- `MEETING_ACTIVE` mientras se graba y `NOT_SMALLER` si la compresión no ahorra
64
- espacio (el original se conserva). `recap list --json` trae `storage` y
65
- `hasVideo` de cada reunión.
66
-
67
- Errores típicos y qué hacer:
68
-
69
- | Código | Causa | Acción |
70
- |---|---|---|
71
- | `SCREEN_DENIED` | Recap.app sin permiso de grabación de pantalla | Pide activarlo en Configuración > Privacidad y seguridad > Grabación de pantalla y audio del sistema |
72
- | `MICROPHONE_DENIED` | Sin permiso de micrófono | Igual, en Micrófono |
73
- | `RECORDER_TIMEOUT` | Un diálogo de permisos quedó esperando | `recap setup` |
74
- | `DEPENDENCY_MISSING`, `MODEL_MISSING` | Falta ffmpeg, whisper-cli, claude o el modelo | `recap setup --install-deps` |
75
- | `CAPTURE_UNAVAILABLE` | No hay grabador en este equipo (Windows, Linux, o falta Recap.app) | Graba con otra app y usa `recap import <archivo>`; en mac, `recap setup` |
76
-
77
- ## Con bita e inkwell
78
-
79
- Cada herramienta hace una sola cosa:
80
-
81
- - **bita** lleva el tiempo (cronómetros). recap solo le pide `projects`,
82
- `project repo ls`, `entries get` y `amend` (título y proyecto).
83
- - **inkwell** guarda la documentación: páginas, backlog, propuestas, historial
84
- y la nota de cada entrada de bita (`inkwell note …`).
85
- - **tally** vuelca a Jira el tiempo de bita (`tally summary --pending`); recap
86
- no toca Jira.
87
-
88
- Si `recap status --json` trae `data.bitaLinked: true` (recap está suscrito a
89
- los eventos de bita desde `recap setup`), **arranca y para las reuniones desde
90
- bita**, no desde recap: así el tiempo queda medido y la grabación sigue al
91
- cronómetro.
92
-
93
- ```sh
94
- bita start "<título>" --kind remote-meeting
95
- bita start "<título>" --kind in-person-meeting
96
- bita stop <id> --json # sin escribir la página: eso lo hace recap
97
- recap wait --bita-entry <id> --json # espera a que todo quede listo
98
- ```
99
-
100
- Cambiar el título o el proyecto del cronómetro mientras se graba (`bita amend`)
101
- no corta ni duplica la grabación: recap solo actualiza los datos de la reunión.
102
- Solo un cambio de tipo (`--kind`) que cruce la línea de reunión la arranca o la
103
- detiene.
104
-
105
- Al parar, recap hace todo lo demás sin que nadie lo pida:
106
-
107
- - transcribe y escribe la minuta;
108
- - le pone al cronómetro un título real, si el que tenía era genérico (`bita amend`);
109
- - le asigna el proyecto si no tenía y la conversación lo deja claro (`bita amend`);
110
- - crea la página en inkwell, ligada a la entrada (`inkwell page new --from-entry`),
111
- o agrega una sección a la que ya tenía (`inkwell page ls --entry`), y la escribe;
112
- - pasa pendientes y hallazgos al backlog de inkwell (`inkwell backlog add`);
113
- - deja la minuta en la sección «Reunión» de la nota de la entrada
114
- (`inkwell note save <entrada> --section Reunión --md …`).
115
-
116
- El resultado está en `data.wrapup` de `recap wait`. Si `wrapup.projectResolved`
117
- es false, pregunta el proyecto, aplícalo con `bita amend <id> --project …` y
118
- mueve la página y el backlog con `inkwell page move --project` e
119
- `inkwell backlog edit --project`.
120
-
121
- **Sin inkwell** la reunión igual queda procesada: las etapas `proposals` y
122
- `wrapup` quedan en `skipped` con el motivo en `stages.<etapa>.reason` y la pista
123
- `npm i -g @kikedealba/inkwell && inkwell setup`; la minuta se queda en
124
- `summary.md` de la carpeta de la reunión. Si inkwell es viejo y no tiene la
125
- capacidad `docs.entry-notes`, se escribe la página y el backlog pero la minuta
126
- no pasa a la nota (queda anotado en `process.log`). Después de instalar o
127
- actualizar inkwell, `recap process <id> --from proposals` lo completa.
128
-
129
- ## En vivo
130
-
131
- Mientras se graba, recap transcribe por tramos de 5 a 20 s en
132
- `live/transcript.jsonl` (una línea `{startMs, endMs, channel, text}`; `mic` es
133
- Sala y `system` Remotos). Es aproximada: la transcripción de después del stop
134
- (`transcript.md`) sigue siendo la fuente de verdad.
135
-
136
- Para responder una pregunta que hicieron en la reunión, con las páginas de
137
- inkwell y los repos del proyecto (`bita project repo ls`); sin inkwell responde
138
- solo con los repos:
139
-
140
- ```sh
141
- recap ask --active --json # la última pregunta de la transcripción
142
- recap ask --active --question "¿cómo se despliega X?" --json
143
- recap ask --meeting <id> --question "..." --json-stream # eventos question/progress/delta/source/done
144
- recap ask --sources --project <p> --json # qué consultaría, sin llamar a claude
145
- ```
146
-
147
- La respuesta lleva `found` y `sources` (página, `archivo:línea` o commit) y se
148
- guarda en `live/answers.jsonl`. Con `found: false` no está documentado: no lo
149
- completes por tu cuenta. Se configura con `recap config get|set`
150
- (`live.enabled`, `live.openWindow`, `live.proposals`, `live.maxChunkSeconds`,
151
- `live.assistModel`, `live.autoAsk`, `live.autoAskModel`,
152
- `live.autoAskMinSeconds`).
153
-
154
- Con `live.autoAsk` (encendido por omisión), el live-worker detecta solo las
155
- preguntas técnicas de la reunión y las responde sin que nadie las pida: esas
156
- respuestas llevan `auto: true` en `live/answers.jsonl`. Mientras se responde
157
- algo, `live/asking.json` dice qué pregunta va (`{question, startedAt, auto}`);
158
- solo corre una respuesta a la vez y una manual detiene a la automática.
159
-
160
- ## Cambios propuestos a la documentación
161
-
162
- Si la reunión está ligada a bita e inkwell está instalado, la etapa `proposals`
163
- (antes de `wrapup`) detecta los cambios explícitos y firmes a páginas de inkwell
164
- que ya existen y los deja en la rama `proposal/meeting-<entrada>` de sus docs
165
- (`inkwell git propose`), sin tocar `main`. Ideas,
166
- dudas y lo que se corrigió después no entran.
167
-
168
- ```sh
169
- recap proposals ls <id> --json # o --bita-entry <id>
170
- recap proposals show <id> <n> --json # markdown, citas y diff
171
- recap proposals accept <id> <n> [--md <archivo editado>] --json
172
- recap proposals reject <id> <n> --json
173
- ```
174
-
175
- Acepta o rechaza **solo cuando el usuario lo pida**. Si `accept` deja la
176
- propuesta en `stale`, la página cambió desde entonces: edítala sobre la versión
177
- actual y acéptala con `--md`. Cuando no queda ninguna pendiente, recap borra la
178
- rama. Usa `/recap-proposals` para revisarlas en la sesión.
36
+ el micrófono. Si el contexto no deja claro cuál es, pregunta con una sola línea
37
+ ("¿remota o presencial?") antes de arrancar.
38
+
39
+ ## Flujo de una reunión
40
+
41
+ 1. **Ver el estado**: `recap status --json`. Si ya se está grabando
42
+ (`ALREADY_RECORDING` al arrancar), no arranques otra: avísalo.
43
+ 2. **Arrancar.** Si `data.bitaLinked` es true, recap sigue a los cronómetros
44
+ de bita: arranca por bita para que el tiempo también quede medido.
45
+
46
+ ```sh
47
+ bita start "<título>" --kind remote-meeting # o --kind in-person-meeting
48
+ recap start --remote "<título>" # solo si bitaLinked es false
49
+ ```
50
+
51
+ 3. **Durante la reunión**: `recap ask --active --json` responde la última
52
+ pregunta con las páginas de inkwell y los repos del proyecto. Detalle en
53
+ [live.md](live.md).
54
+ 4. **Parar.** Con cronómetro, para el cronómetro y espera; sin él, para recap:
55
+
56
+ ```sh
57
+ bita stop <id> --json && recap wait --bita-entry <id> --json
58
+ recap stop --json && recap wait --json
59
+ ```
60
+
61
+ `stop` lanza el procesamiento en segundo plano (transcripción, capturas,
62
+ minuta y cierre). Tarda de uno a cinco minutos por hora de reunión: corre
63
+ `recap wait` con un timeout amplio. El monitor del plugin de Claude Code
64
+ avisa en la sesión cuando termina, así que no hace falta sondear.
65
+ 5. **Cierre automático** (con bita e inkwell): recap le pone título y proyecto
66
+ al cronómetro, crea o completa la página en inkwell, pasa pendientes y
67
+ hallazgos al backlog y deja la minuta en la nota de la entrada. El
68
+ resultado viene en `data.wrapup` de `recap wait`; si
69
+ `wrapup.projectResolved` es false, pregunta el proyecto. Detalle y
70
+ correcciones en [wrapup.md](wrapup.md).
71
+ 6. **Después**: los cambios que la reunión propuso a la documentación se
72
+ revisan con `/recap-proposals` ([proposals.md](proposals.md)), y el tiempo
73
+ de la reunión llega a Jira con tally, no desde recap.
74
+
75
+ Cambiar el título o el proyecto del cronómetro mientras se graba
76
+ (`bita amend`) no corta la grabación; solo un cambio de `--kind` que cruce la
77
+ línea de reunión la arranca o la detiene. `recap discard` borra la grabación
78
+ activa: solo si el usuario lo pide.
179
79
 
180
80
  ## Consultar reuniones
181
81
 
@@ -188,17 +88,35 @@ recap show <id> --path
188
88
  Para responder qué se habló o qué se acordó, lee `summary.md` de las reuniones
189
89
  que apliquen. Si hace falta precisión (una cifra, quién dijo qué), ve a
190
90
  `transcript.md`, que lleva el minuto de cada párrafo. Cita la reunión y el
191
- minuto en la respuesta. En reuniones remotas, **Sala** es el micrófono local y
192
- **Remotos** el audio de la llamada; no hay separación por persona.
91
+ minuto. En remotas, **Sala** es el micrófono local y **Remotos** el audio de la
92
+ llamada; no hay separación por persona.
193
93
 
194
94
  ## Regenerar la minuta
195
95
 
196
96
  - Sin intervención: `recap process <id> --from summarize`.
197
- - Dentro de la sesión, cuando el usuario quiere dirigir el enfoque o corregir
198
- algo: usa `/recap-summarize`.
97
+ - Dentro de la sesión, cuando el usuario quiere dirigir el enfoque: `/recap-summarize`.
98
+ - Si una etapa falló, `recap process <id>` retoma desde ahí.
99
+
100
+ ## Errores típicos
101
+
102
+ | Código | Causa | Acción |
103
+ |---|---|---|
104
+ | `SCREEN_DENIED` | Recap.app sin permiso de grabación de pantalla | Configuración > Privacidad y seguridad > Grabación de pantalla y audio del sistema |
105
+ | `MICROPHONE_DENIED` | Sin permiso de micrófono | Igual, en Micrófono |
106
+ | `RECORDER_TIMEOUT` | Un diálogo de permisos quedó esperando | `recap setup` |
107
+ | `DEPENDENCY_MISSING`, `MODEL_MISSING` | Falta ffmpeg, whisper-cli, claude o el modelo | `recap setup --install-deps` |
108
+ | `CAPTURE_UNAVAILABLE` | No hay grabador en este equipo | Graba con otra app y usa `recap import <archivo>`; en mac, `recap setup` |
109
+
110
+ ## Más detalle, bajo demanda
111
+
112
+ - [wrapup.md](wrapup.md): qué hace el cierre con bita e inkwell, sin inkwell, y cómo corregir el proyecto.
113
+ - [live.md](live.md): transcripción en vivo, `recap ask` y su configuración.
114
+ - [proposals.md](proposals.md): cambios propuestos a la documentación.
115
+ - [storage.md](storage.md): liberar espacio (comprimir, quitar video, borrar).
199
116
 
200
117
  ## Lo que no hace
201
118
 
202
119
  - No separa hablantes por persona: los nombres salen del contexto.
203
120
  - No sube nada a la nube salvo la transcripción que lee Claude al resumir, al
204
121
  responder con `recap ask` y al buscar cambios propuestos.
122
+ - No toca Jira.
@@ -0,0 +1,31 @@
1
+ # En vivo
2
+
3
+ Mientras se graba, recap transcribe por tramos de 5 a 20 s en
4
+ `live/transcript.jsonl` (una línea `{startMs, endMs, channel, text}`; `mic` es
5
+ Sala y `system` Remotos). Es aproximada: la transcripción de después del stop
6
+ (`transcript.md`) sigue siendo la fuente de verdad.
7
+
8
+ Para responder una pregunta que hicieron en la reunión, con las páginas de
9
+ inkwell y los repos del proyecto (`bita project repo ls`); sin inkwell responde
10
+ solo con los repos:
11
+
12
+ ```sh
13
+ recap ask --active --json # la última pregunta de la transcripción
14
+ recap ask --active --question "¿cómo se despliega X?" --json
15
+ recap ask --meeting <id> --question "..." --json-stream # eventos question/progress/delta/source/done
16
+ recap ask --sources --project <p> --json # qué consultaría, sin llamar a claude
17
+ ```
18
+
19
+ La respuesta lleva `found` y `sources` (página, `archivo:línea` o commit) y se
20
+ guarda en `live/answers.jsonl`. Con `found: false` no está documentado: no lo
21
+ completes por tu cuenta.
22
+
23
+ Con `live.autoAsk` (encendido por omisión), el live-worker detecta solo las
24
+ preguntas técnicas de la reunión y las responde sin que nadie las pida: esas
25
+ respuestas llevan `auto: true` en `live/answers.jsonl`. Mientras se responde
26
+ algo, `live/asking.json` dice qué pregunta va (`{question, startedAt, auto}`);
27
+ una respuesta manual detiene a la automática.
28
+
29
+ Se configura con `recap config get|set`: `live.enabled`, `live.openWindow`,
30
+ `live.proposals`, `live.maxChunkSeconds`, `live.assistModel`, `live.autoAsk`,
31
+ `live.autoAskModel`, `live.autoAskMinSeconds` y `live.autoAskConcurrency`.
@@ -0,0 +1,19 @@
1
+ # Cambios propuestos a la documentación
2
+
3
+ Si la reunión está ligada a bita e inkwell está instalado, la etapa `proposals`
4
+ (antes de `wrapup`) detecta los cambios explícitos y firmes a páginas de
5
+ inkwell que ya existen y los deja en la rama `proposal/meeting-<entrada>` de
6
+ sus docs (`inkwell git propose`), sin tocar `main`. Ideas, dudas y lo que se
7
+ corrigió después no entran.
8
+
9
+ ```sh
10
+ recap proposals ls <id> --json # o --bita-entry <id>
11
+ recap proposals show <id> <n> --json # markdown, citas y diff
12
+ recap proposals accept <id> <n> [--md <archivo editado>] --json
13
+ recap proposals reject <id> <n> --json
14
+ ```
15
+
16
+ Acepta o rechaza **solo cuando el usuario lo pida**. Si `accept` deja la
17
+ propuesta en `stale`, la página cambió desde entonces: edítala sobre la versión
18
+ actual y acéptala con `--md`. Cuando no queda ninguna pendiente, recap borra la
19
+ rama. `/recap-proposals` las revisa una por una en la sesión.
@@ -0,0 +1,16 @@
1
+ # Liberar espacio
2
+
3
+ Solo si el usuario lo pide. Todos aceptan `<id>` o `--bita-entry <id>` y `--json`.
4
+
5
+ ```sh
6
+ recap compress-video <id> --preset light|medium|max # recomprime el video en HEVC (solo remotas)
7
+ recap strip-video <id> # quita el video y deja recording.m4a con ambas pistas de audio
8
+ recap prune <id> --intermediates # borra mic.wav, system.wav, transcript-mic/system.json y live/chunks
9
+ recap delete <id> # borra la carpeta completa de la reunión
10
+ ```
11
+
12
+ `compress-video` y `strip-video` aceptan `--prune-intermediates`. Fallan con
13
+ `NOT_REMOTE` en reuniones presenciales, `NO_VIDEO` si ya no hay video,
14
+ `MEETING_ACTIVE` mientras se graba y `NOT_SMALLER` si la compresión no ahorra
15
+ espacio (el original se conserva). `recap list --json` trae `storage` y
16
+ `hasVideo` de cada reunión.
@@ -0,0 +1,49 @@
1
+ # Cierre de una reunión con bita e inkwell
2
+
3
+ Si `recap status --json` trae `data.bitaLinked: true` (recap está suscrito a
4
+ los eventos de bita desde `recap setup`), las reuniones se arrancan y se paran
5
+ desde bita. recap solo le pide a bita `projects`, `project repo ls`,
6
+ `entries get` y `amend` (título y proyecto).
7
+
8
+ Al parar el cronómetro, recap hace todo lo demás sin que nadie lo pida:
9
+
10
+ - transcribe y escribe la minuta;
11
+ - le pone al cronómetro un título real, si el que tenía era genérico (`bita amend`);
12
+ - le asigna el proyecto si no tenía y la conversación lo deja claro (`bita amend`);
13
+ - crea la página en inkwell, ligada a la entrada (`inkwell page new --from-entry`),
14
+ o agrega una sección a la que ya tenía (`inkwell page ls --entry`), y la escribe;
15
+ - pasa pendientes y hallazgos al backlog de inkwell (`inkwell backlog add`);
16
+ - deja la minuta en la sección «Reunión» de la nota de la entrada
17
+ (`inkwell note save <entrada> --section Reunión --md …`).
18
+
19
+ El resultado está en `data.wrapup` de `recap wait`: `title`, `project`,
20
+ `projectResolved`, `pageId`, `pageCreated` y `backlogKeys`.
21
+
22
+ ## Corregir el proyecto
23
+
24
+ Si `wrapup.projectResolved` es false, pregunta solo «¿De qué proyecto fue?» y,
25
+ con la respuesta:
26
+
27
+ ```sh
28
+ bita amend <entryId> --project <X>
29
+ inkwell page move <pageId> --project <X>
30
+ inkwell backlog edit <CLAVE> --project <X> # una por cada valor de wrapup.backlogKeys
31
+ ```
32
+
33
+ ## Sin inkwell, o con un inkwell viejo
34
+
35
+ La reunión igual queda procesada: las etapas `proposals` y `wrapup` quedan en
36
+ `skipped` con el motivo en `stages.<etapa>.reason` y la pista
37
+ `npm i -g @kikedealba/inkwell && inkwell setup`; la minuta se queda en
38
+ `summary.md` de la carpeta de la reunión. Si inkwell no tiene la capacidad
39
+ `docs.entry-notes`, se escriben la página y el backlog, pero la minuta no pasa
40
+ a la nota (queda anotado en `process.log`). Después de instalar o actualizar
41
+ inkwell, `recap process <id> --from proposals` lo completa.
42
+
43
+ ## Qué responder al usuario
44
+
45
+ - Título y duración.
46
+ - Con bita e inkwell: el proyecto, la página (`wrapup.pageId`) y cuántos
47
+ pendientes y hallazgos quedaron en el backlog.
48
+ - Sin bita o sin inkwell: los **Acuerdos** y **Pendientes** de la minuta, tal cual.
49
+ - Si una etapa falló, su error y la sugerencia `recap process <id>`.
@@ -1,7 +1,9 @@
1
1
  ---
2
- description: Responde la última pregunta de la reunión en curso con las páginas y repos del proyecto
2
+ name: recap-ask
3
+ description: Responde la última pregunta de la reunión en curso
3
4
  argument-hint: [pregunta, opcional]
4
5
  allowed-tools: Bash(recap ask:*)
6
+ disable-model-invocation: true
5
7
  ---
6
8
 
7
9
  Responde en vivo una pregunta de la reunión que se está grabando.
@@ -1,7 +1,9 @@
1
1
  ---
2
+ name: recap-list
2
3
  description: Lista las reuniones grabadas, o muestra la minuta de una
3
4
  argument-hint: [id, parte del título o "last"]
4
5
  allowed-tools: Bash(recap list:*), Bash(recap show:*)
6
+ disable-model-invocation: true
5
7
  ---
6
8
 
7
9
  **Sin `$ARGUMENTS`**, lista las reuniones recientes:
@@ -1,7 +1,9 @@
1
1
  ---
2
- description: Revisa los cambios a la documentación que propuso una reunión
2
+ name: recap-proposals
3
+ description: Revisa los cambios de documentación que propuso una reunión
3
4
  argument-hint: [id de la reunión o "last"]
4
5
  allowed-tools: Bash(recap proposals:*), Bash(recap show:*), Read, Write
6
+ disable-model-invocation: true
5
7
  ---
6
8
 
7
9
  Al cerrar una reunión ligada a bita, recap deja los cambios explícitos a
@@ -1,7 +1,9 @@
1
1
  ---
2
+ name: recap-start
2
3
  description: Empieza a grabar una reunión (remota o presencial)
3
4
  argument-hint: [remota|presencial] [título]
4
5
  allowed-tools: Bash(recap start:*), Bash(recap status:*), Bash(recap import:*), Bash(bita start:*)
6
+ disable-model-invocation: true
5
7
  ---
6
8
 
7
9
  Estado actual:
@@ -1,6 +1,8 @@
1
1
  ---
2
+ name: recap-status
2
3
  description: Muestra si se está grabando y en qué va la última reunión
3
4
  allowed-tools: Bash(recap status:*)
5
+ disable-model-invocation: true
4
6
  ---
5
7
 
6
8
  !`recap status --json`
@@ -1,7 +1,9 @@
1
1
  ---
2
+ name: recap-stop
2
3
  description: Termina la grabación y deja la reunión documentada
3
4
  argument-hint: [--no-wait para no esperar el resultado]
4
5
  allowed-tools: Bash(recap stop:*), Bash(recap status:*), Bash(recap wait:*), Bash(recap show:*), Bash(bita stop:*), Bash(bita ls:*), Bash(bita amend:*), Bash(inkwell page move:*), Bash(inkwell backlog edit:*)
6
+ disable-model-invocation: true
5
7
  ---
6
8
 
7
9
  Detén la grabación activa y deja la reunión documentada.
@@ -22,13 +24,10 @@ bita stop <id> --json
22
24
  recap wait --bita-entry <id> --json
23
25
  ```
24
26
 
25
- El evento de bita detiene la grabación y recap hace lo demás:
26
-
27
- - transcribe y escribe la minuta;
28
- - le pone al cronómetro un título real y, si no tenía, su proyecto (`bita amend`);
29
- - crea o completa la página en inkwell;
30
- - pasa los pendientes y hallazgos al backlog de inkwell;
31
- - deja la minuta en la sección «Reunión» de la nota de la entrada (`inkwell note save`).
27
+ El evento de bita detiene la grabación y recap hace lo demás: transcribe,
28
+ escribe la minuta, le pone título y proyecto al cronómetro (`bita amend`),
29
+ crea o completa la página en inkwell, pasa pendientes y hallazgos al backlog
30
+ de inkwell y deja la minuta en la nota de la entrada.
32
31
 
33
32
  Sin inkwell, las etapas `proposals` y `wrapup` quedan en `skipped` con su
34
33
  motivo (`stages.wrapup.reason`) y la minuta se queda en la carpeta de la
@@ -70,4 +69,5 @@ inkwell backlog edit <CLAVE> --project <X> # una por cada valor de wrapup.back
70
69
  El tiempo de la reunión se vuelca a Jira después con tally, no desde aquí.
71
70
 
72
71
  Con `--no-wait` en `$ARGUMENTS`, solo para y responde en una línea que el
73
- procesamiento sigue en segundo plano.
72
+ procesamiento sigue en segundo plano. El monitor del plugin avisa en la sesión
73
+ cuando la reunión termine de procesarse.
@@ -1,7 +1,9 @@
1
1
  ---
2
- description: Rehace la minuta de una reunión dentro de la sesión, con indicaciones
2
+ name: recap-summarize
3
+ description: Rehace la minuta de una reunión con indicaciones
3
4
  argument-hint: [id o "last"] [indicaciones, p. ej. "enfócate en los riesgos"]
4
5
  allowed-tools: Bash(recap prompt:*), Bash(recap save-summary:*), Bash(recap show:*), Read, Write
6
+ disable-model-invocation: true
5
7
  ---
6
8
 
7
9
  Rehaz la minuta de una reunión aquí mismo, en lugar del `claude -p` sin