@kikedealba/recap 0.0.0-stage → 0.9.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +388 -2
  3. package/Resources/ask-prompt.md +49 -0
  4. package/Resources/detect-prompt.md +71 -0
  5. package/Resources/proposals-prompt.md +44 -0
  6. package/Resources/summary-prompt.md +37 -0
  7. package/Resources/wrapup-prompt.md +32 -0
  8. package/commands/recap-ask.md +28 -0
  9. package/commands/recap-list.md +21 -0
  10. package/commands/recap-proposals.md +32 -0
  11. package/commands/recap-start.md +47 -0
  12. package/commands/recap-status.md +14 -0
  13. package/commands/recap-stop.md +69 -0
  14. package/commands/recap-summarize.md +31 -0
  15. package/dist/bin/recap.js +4 -0
  16. package/dist/bita/client.js +155 -0
  17. package/dist/bita/hook.js +81 -0
  18. package/dist/bita/proposals.js +419 -0
  19. package/dist/bita/wrapup.js +254 -0
  20. package/dist/capture/capture.js +109 -0
  21. package/dist/capture/import.js +80 -0
  22. package/dist/capture/recording.js +125 -0
  23. package/dist/cli/args.js +83 -0
  24. package/dist/cli/commands/ask.js +127 -0
  25. package/dist/cli/commands/config.js +40 -0
  26. package/dist/cli/commands/hook.js +94 -0
  27. package/dist/cli/commands/live.js +23 -0
  28. package/dist/cli/commands/media.js +53 -0
  29. package/dist/cli/commands/proposals.js +76 -0
  30. package/dist/cli/commands/query.js +172 -0
  31. package/dist/cli/commands/recording.js +102 -0
  32. package/dist/cli/commands/setup.js +132 -0
  33. package/dist/cli/output.js +24 -0
  34. package/dist/cli/router.js +94 -0
  35. package/dist/core/active.js +46 -0
  36. package/dist/core/config.js +77 -0
  37. package/dist/core/dates.js +30 -0
  38. package/dist/core/fsutil.js +44 -0
  39. package/dist/core/json.js +70 -0
  40. package/dist/core/live-settings.js +140 -0
  41. package/dist/core/lock.js +114 -0
  42. package/dist/core/meeting.js +201 -0
  43. package/dist/core/paths.js +55 -0
  44. package/dist/core/proc.js +129 -0
  45. package/dist/core/record.js +46 -0
  46. package/dist/core/self.js +22 -0
  47. package/dist/core/text.js +89 -0
  48. package/dist/core/tools.js +91 -0
  49. package/dist/errors.js +36 -0
  50. package/dist/live/ask.js +77 -0
  51. package/dist/live/asking.js +185 -0
  52. package/dist/live/context.js +90 -0
  53. package/dist/live/dedupe.js +21 -0
  54. package/dist/live/detector.js +498 -0
  55. package/dist/live/files.js +48 -0
  56. package/dist/live/merger.js +100 -0
  57. package/dist/live/origin.js +91 -0
  58. package/dist/live/stream.js +390 -0
  59. package/dist/live/worker.js +191 -0
  60. package/dist/media/media.js +83 -0
  61. package/dist/media/operations.js +131 -0
  62. package/dist/media/probe.js +50 -0
  63. package/dist/pipeline/claude.js +73 -0
  64. package/dist/pipeline/pipeline.js +275 -0
  65. package/dist/pipeline/resources.js +16 -0
  66. package/dist/pipeline/stages.js +27 -0
  67. package/dist/pipeline/summary.js +56 -0
  68. package/dist/pipeline/transcript.js +116 -0
  69. package/dist/setup/deps.js +119 -0
  70. package/dist/setup/integration.js +112 -0
  71. package/dist/setup/legacy.js +28 -0
  72. package/dist/version.js +6 -0
  73. package/package.json +57 -4
  74. package/skills/recap/SKILL.md +182 -0
@@ -0,0 +1,32 @@
1
+ Vas a cerrar en bita el registro de una reunión ya transcrita y resumida. Con la minuta y la transcripción de abajo, decide cómo queda documentada.
2
+
3
+ - Título actual del contador: «{{currentTitle}}»
4
+ - Proyecto actual del contador: {{currentProject}}
5
+ - Fecha: {{date}} · Duración: {{duration}} · Modo: {{mode}}
6
+ - Proyectos que existen en bita (elige solo de esta lista, con el nombre exacto):
7
+ {{projects}}
8
+ {{existingPage}}
9
+
10
+ Responde únicamente con un objeto JSON, sin texto antes ni después y sin bloque de código, con esta forma:
11
+
12
+ {"title": "...", "project": "..." | null, "pageTitle": "...", "pageMarkdown": "...", "backlog": [{"kind": "pending" | "finding", "title": "...", "body": "..."}]}
13
+
14
+ Reglas por campo:
15
+
16
+ - **title**: el tema real de la reunión, corto (máximo 80 caracteres), reconocible en Jira. Por ejemplo, «Reunión presencial: iniciativa de recompra con cupón post-entrega». Sin fecha.
17
+ - **project**: el nombre exacto de uno de los proyectos de la lista, solo si la conversación deja claro a cuál pertenece (se menciona el cliente, el producto o el sistema). Si hay duda, o si ninguno aplica, `null`. Nunca inventes un nombre.
18
+ - **pageTitle**: el título de la página de documentación, el tema sin la palabra «reunión». Por ejemplo, «Iniciativa de recompra con cupón post-entrega».
19
+ - **pageMarkdown**: el cuerpo de la página, en Markdown, sin título H1. Debe ser un documento técnico formal que explica **cómo es** lo que se trató, en presente:
20
+ - Secciones `##` por tema: contexto y objetivo, cómo funciona o cómo se propone, decisiones tomadas y, si aplica, arquitectura, datos, costos o plan de pruebas, con las cifras y nombres que se dijeron.
21
+ - Si hay un flujo o una arquitectura, puedes incluir un diagrama en un bloque `mermaid`. Solo con componentes que se mencionaron: si algo no se nombró, no lo supongas.
22
+ - **Prohibido**: secciones de «Pendientes», «Próximos pasos», «Hallazgos», «Preguntas abiertas» o «Lo que falta»; eso va en `backlog`. Tampoco «se acordó con el usuario», «según lo solicitado», «decidimos», «creo que», primera ni segunda persona, ni comentarios sobre la grabación o la transcripción.
23
+ - Entre 3 y 7 secciones. Nada que no esté en la minuta o en la transcripción.
24
+ - **backlog**: un ítem por cada pendiente (`kind: "pending"`, redactado como acción que empieza con verbo; en `body` el responsable, la fecha y el contexto si se mencionaron) y por cada pregunta abierta o riesgo (`kind: "finding"`). Títulos de una línea, sin numeración. Lista vacía si no hay ninguno.
25
+
26
+ <minuta>
27
+ {{summary}}
28
+ </minuta>
29
+
30
+ <transcripcion>
31
+ {{transcript}}
32
+ </transcripcion>
@@ -0,0 +1,28 @@
1
+ ---
2
+ description: Responde la última pregunta de la reunión en curso con las páginas y repos del proyecto
3
+ argument-hint: [pregunta, opcional]
4
+ allowed-tools: Bash(recap ask:*)
5
+ ---
6
+
7
+ Responde en vivo una pregunta de la reunión que se está grabando.
8
+
9
+ - **Con `$ARGUMENTS`**, es la pregunta:
10
+
11
+ ```
12
+ recap ask --active --question "$ARGUMENTS" --json
13
+ ```
14
+
15
+ - **Sin `$ARGUMENTS`**, recap toma la última pregunta de la transcripción en
16
+ vivo:
17
+
18
+ ```
19
+ recap ask --active --json
20
+ ```
21
+
22
+ Muestra `data.answer` tal cual y, debajo, las fuentes de `data.sources` (su
23
+ `label`). Si `data.found` es false, dilo en una línea: no está documentado.
24
+ Si falla con `NOT_RECORDING`, no hay grabación activa; con `NO_QUESTION`, la
25
+ transcripción en vivo todavía está vacía y hace falta escribir la pregunta.
26
+ Puede haber otras respuestas en curso al mismo tiempo; no se estorban. Las
27
+ respuestas que el live-worker encontró solo llevan `auto: true` en
28
+ `live/answers.jsonl`.
@@ -0,0 +1,21 @@
1
+ ---
2
+ description: Lista las reuniones grabadas, o muestra la minuta de una
3
+ argument-hint: [id, parte del título o "last"]
4
+ allowed-tools: Bash(recap list:*), Bash(recap show:*)
5
+ ---
6
+
7
+ **Sin `$ARGUMENTS`**, lista las reuniones recientes:
8
+
9
+ !`recap list`
10
+
11
+ y muéstralas en una tabla corta (fecha, modo, estado, duración, título).
12
+
13
+ **Con `$ARGUMENTS`**, muestra esa reunión:
14
+
15
+ ```
16
+ recap show "$ARGUMENTS"
17
+ ```
18
+
19
+ Si tiene minuta, muéstrala tal cual. Si no, di en qué estado está y si alguna
20
+ etapa falló. Si el texto coincide con varias reuniones (`MEETING_AMBIGUOUS`),
21
+ lista las candidatas y pregunta cuál.
@@ -0,0 +1,32 @@
1
+ ---
2
+ description: Revisa los cambios a la documentación que propuso una reunión
3
+ argument-hint: [id de la reunión o "last"]
4
+ allowed-tools: Bash(recap proposals:*), Bash(recap show:*), Read, Write
5
+ ---
6
+
7
+ Al cerrar una reunión ligada a bita, recap deja los cambios explícitos a
8
+ páginas existentes como propuestas en la rama `proposal/meeting-<entrada>` de
9
+ los docs. Nada llega a `main` sin aceptarse.
10
+
11
+ 1. La reunión es `$ARGUMENTS`, o `last` si viene vacío. Lista sus propuestas:
12
+
13
+ ```
14
+ recap proposals ls <reunión> --json
15
+ ```
16
+
17
+ 2. Por cada una en `pending` o `stale`, muestra el detalle con
18
+ `recap proposals show <reunión> <n> --json`: título, página y sección,
19
+ justificación, citas (con su minuto) y el diff.
20
+ 3. Pregunta qué hacer con cada una y aplícalo:
21
+
22
+ ```
23
+ recap proposals accept <reunión> <n> --json
24
+ recap proposals accept <reunión> <n> --md <archivo-editado> --json
25
+ recap proposals reject <reunión> <n> --json
26
+ ```
27
+
28
+ Para editar antes de aceptar, copia `proposal.file`, haz el cambio que pida
29
+ el usuario y pásalo con `--md`.
30
+
31
+ Si `accept` deja la propuesta en `stale`, la página cambió desde la propuesta:
32
+ muéstralo, y ofrece editarla sobre la versión actual y aceptarla con `--md`.
@@ -0,0 +1,47 @@
1
+ ---
2
+ description: Empieza a grabar una reunión (remota o presencial)
3
+ argument-hint: [remota|presencial] [título]
4
+ allowed-tools: Bash(recap start:*), Bash(recap status:*), Bash(recap import:*), Bash(bita start:*)
5
+ ---
6
+
7
+ Estado actual:
8
+
9
+ !`recap status --json`
10
+
11
+ Empieza a grabar una reunión con `$ARGUMENTS`.
12
+
13
+ 1. **Si arriba dice que ya se está grabando**, no arranques otra: dilo en una
14
+ línea y termina.
15
+ 2. **Modo.** Sácalo de `$ARGUMENTS` o del contexto de la conversación:
16
+ - remota: Meet, Zoom, Teams, llamada, videollamada, en línea.
17
+ - presencial: en sala, en oficina, en persona.
18
+
19
+ Si no se puede saber, pregunta solo "¿remota o presencial?" y espera.
20
+ 3. **Título.** Lo que quede de `$ARGUMENTS` sin la palabra del modo, literal. Si
21
+ no hay, uno corto sacado del contexto; si tampoco hay contexto, arranca sin
22
+ título.
23
+ 4. **Por dónde arrancar.** Si arriba `data.bitaLinked` es true, recap escucha
24
+ los eventos de bita: arranca por bita, para que el tiempo también quede
25
+ medido y la minuta llegue al documento de la entrada:
26
+
27
+ ```
28
+ bita start "<título>" --kind remote-meeting
29
+ bita start "<título>" --kind in-person-meeting
30
+ ```
31
+
32
+ Si `data.bitaLinked` es false, arranca recap directamente:
33
+
34
+ ```
35
+ recap start --remote "<título>"
36
+ recap start --in-person "<título>"
37
+ ```
38
+
39
+ Si falla por permisos (`SCREEN_DENIED`, `MICROPHONE_DENIED`), di exactamente qué
40
+ activar en Configuración > Privacidad y seguridad. Si es `RECORDER_TIMEOUT` o
41
+ falta una dependencia, sugiere `recap setup`. Si es `CAPTURE_UNAVAILABLE`
42
+ (Windows o Linux), explica que ahí no se graba: que grabe con otra app y la
43
+ procese con `recap import <archivo>`. Si arrancaste por bita, confirma
44
+ con `recap status` que la grabación empezó.
45
+
46
+ Responde en una línea: modo, título, que ya está grabando y el id de bita si
47
+ arrancó por ahí. Nada más.
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: Muestra si se está grabando y en qué va la última reunión
3
+ allowed-tools: Bash(recap status:*)
4
+ ---
5
+
6
+ !`recap status --json`
7
+
8
+ Resume en una o dos líneas:
9
+
10
+ - Si `data.recording` es true: modo, título y tiempo transcurrido.
11
+ - Si no: que no se está grabando, y el estado de la última reunión
12
+ (`data.latest.status`). Si está en `processing`, di qué etapas ya están en
13
+ `done` dentro de `data.latest.stages`. Si alguna está en `failed`, muestra
14
+ su error.
@@ -0,0 +1,69 @@
1
+ ---
2
+ description: Termina la grabación y deja la reunión documentada
3
+ argument-hint: [--no-wait para no esperar el resultado]
4
+ allowed-tools: Bash(recap stop:*), Bash(recap status:*), Bash(recap wait:*), Bash(recap show:*), Bash(bita stop:*), Bash(bita ls:*), Bash(bita amend:*), Bash(bita docs page move:*), Bash(bita backlog edit:*), Bash(inkwell page move:*), Bash(inkwell backlog edit:*)
5
+ ---
6
+
7
+ Detén la grabación activa y deja la reunión documentada.
8
+
9
+ Grabación actual:
10
+
11
+ !`recap status --json`
12
+
13
+ Si no hay grabación activa (`NOT_RECORDING`), dilo en una línea y termina.
14
+
15
+ ## Si la grabación sigue a un contador de bita
16
+
17
+ Cuando `data.active.bitaEntryId` trae un id, para **el contador**, no recap. Sin
18
+ `--did` y sin escribir la página:
19
+
20
+ ```
21
+ bita stop <id> --json
22
+ recap wait --bita-entry <id> --json
23
+ ```
24
+
25
+ El hook de bita detiene la grabación y recap hace lo demás:
26
+
27
+ - transcribe y escribe la minuta;
28
+ - le pone al contador un título real y, si no tenía, su proyecto;
29
+ - crea o completa la página;
30
+ - pasa los pendientes al backlog;
31
+ - deja la minuta en la sección «Reunión» de la entrada.
32
+
33
+ `recap wait` tarda de uno a cinco minutos en una reunión de una hora: córrelo con
34
+ un timeout amplio (15 min). Si después de un minuto `recap status` sigue
35
+ grabando, el evento no llegó: párala con `recap stop`.
36
+
37
+ ## Si es una grabación suelta
38
+
39
+ ```
40
+ recap stop --json
41
+ recap wait --json
42
+ ```
43
+
44
+ ## Qué responder
45
+
46
+ Con el resultado de `recap wait` responde:
47
+
48
+ - título y duración;
49
+ - con bita: el proyecto, la página (`wrapup.pageId`) y cuántos pendientes y
50
+ hallazgos quedaron en el backlog;
51
+ - sin bita: los **Acuerdos** y **Pendientes** de la minuta, tal cual.
52
+
53
+ Si una etapa falló, muestra su error y sugiere `recap process <id>`: retoma
54
+ desde la etapa que falló.
55
+
56
+ **Si `wrapup.projectResolved` es false**, pregunta solo «¿De qué proyecto fue?»
57
+ y, con la respuesta:
58
+
59
+ ```
60
+ bita amend <entryId> --project <X>
61
+ bita docs page move <pageId> --project <X>
62
+ bita backlog edit <CLAVE> --project <X> # una por cada valor de wrapup.backlogKeys
63
+ ```
64
+
65
+ Si inkwell está instalado, usa `inkwell page move <pageId> --project <X>` e
66
+ `inkwell backlog edit <CLAVE> --project <X>` en lugar de los dos últimos.
67
+
68
+ Con `--no-wait` en `$ARGUMENTS`, solo para y responde en una línea que el
69
+ procesamiento sigue en segundo plano.
@@ -0,0 +1,31 @@
1
+ ---
2
+ description: Rehace la minuta de una reunión dentro de la sesión, con indicaciones
3
+ argument-hint: [id o "last"] [indicaciones, p. ej. "enfócate en los riesgos"]
4
+ allowed-tools: Bash(recap prompt:*), Bash(recap save-summary:*), Bash(recap show:*), Read, Write
5
+ ---
6
+
7
+ Rehaz la minuta de una reunión aquí mismo, en lugar del `claude -p` sin
8
+ supervisión del pipeline.
9
+
10
+ 1. El primer token de `$ARGUMENTS` es la reunión (id, parte del id o `last`);
11
+ si no parece un id, usa `last`. El resto son indicaciones del usuario.
12
+ 2. Obtén las instrucciones completas, con la transcripción y la lista de
13
+ capturas:
14
+
15
+ ```
16
+ recap prompt <reunión>
17
+ ```
18
+
19
+ 3. Síguelas al pie de la letra. Las rutas de las capturas son relativas al
20
+ directorio que aparece en la primera línea; ábrelas con Read cuando el tema
21
+ lo amerite. Aplica las indicaciones del usuario encima de esas
22
+ instrucciones, sin romper la estructura de secciones.
23
+ 4. Escribe la minuta, empezando por `## Resumen`, en un archivo del directorio
24
+ temporal de la sesión, y guárdala:
25
+
26
+ ```
27
+ recap save-summary <reunión> <archivo>
28
+ ```
29
+
30
+ Responde con los **Acuerdos** y **Pendientes** de la nueva minuta y la ruta de
31
+ `summary.md`.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../cli/router.js";
3
+ const code = await main(process.argv.slice(2));
4
+ process.exitCode = code;
@@ -0,0 +1,155 @@
1
+ import path from 'node:path';
2
+ import { findTool, invokeTool } from '@kikedealba/kit/registry';
3
+ import { parseEnvelope } from '@kikedealba/kit/envelope';
4
+ import { isRecord } from "../core/json.js";
5
+ import { home } from "../core/paths.js";
6
+ import { exec } from "../core/proc.js";
7
+ import { suffix, trimmed } from "../core/text.js";
8
+ import { locateTool, toolEnvironment } from "../core/tools.js";
9
+ import { RecapError, errorMessage } from "../errors.js";
10
+ export function parseResponse(stdout, stderr, status) {
11
+ const envelope = parseEnvelope(stdout);
12
+ const error = isRecord(envelope?.['error']) ? envelope['error'] : undefined;
13
+ const ok = status === 0 && envelope?.['ok'] === true;
14
+ return {
15
+ ok,
16
+ data: envelope?.['data'],
17
+ meta: isRecord(envelope?.['meta']) ? envelope['meta'] : undefined,
18
+ errorCode: typeof error?.['code'] === 'string' ? error['code'] : undefined,
19
+ errorMessage: typeof error?.['message'] === 'string' ? error['message'] : ok ? undefined : suffix(trimmed(stdout + stderr), 300),
20
+ };
21
+ }
22
+ export async function callBita(bita, args) {
23
+ const response = await bita.invoke(args);
24
+ if (!response.ok)
25
+ throw new RecapError('BITA_FAILED', `bita ${args.slice(0, 3).join(' ')} failed: ${response.errorMessage ?? 'no reason given'}`);
26
+ return response.data;
27
+ }
28
+ export const QUIET_ENV = { BITA_NO_HOOKS: '1', KIT_NO_EVENTS: '1' };
29
+ export function neutralCwd() {
30
+ return path.parse(home()).root || '/';
31
+ }
32
+ export async function bitaCommand(config) {
33
+ const registered = await findTool('bita').catch(() => null);
34
+ if (registered)
35
+ return registered.bin;
36
+ const located = await locateTool('bita', config);
37
+ return located ? [located] : null;
38
+ }
39
+ export class BitaClient {
40
+ command;
41
+ config;
42
+ target;
43
+ constructor(command, config, target) {
44
+ this.command = command;
45
+ this.config = config;
46
+ this.target = target;
47
+ }
48
+ static async create(config, target) {
49
+ const command = await bitaCommand(config);
50
+ return command ? new BitaClient(command, config, target) : null;
51
+ }
52
+ targetArgs() {
53
+ const args = [];
54
+ if (this.target.databasePath)
55
+ args.push('--db-path', this.target.databasePath);
56
+ if (this.target.docsRoot)
57
+ args.push('--docs-dir', this.target.docsRoot);
58
+ return args;
59
+ }
60
+ async run(args) {
61
+ const [executable, ...prefix] = this.command;
62
+ if (!executable)
63
+ throw new RecapError('DEPENDENCY_MISSING', 'bita not found');
64
+ const env = { ...toolEnvironment(executable, this.config), ...QUIET_ENV };
65
+ return exec(executable, [...prefix, ...args], { env, cwd: neutralCwd() });
66
+ }
67
+ async invoke(args) {
68
+ const result = await this.run([...args, '--json', ...this.targetArgs()]);
69
+ return parseResponse(result.stdout, result.stderr, result.status);
70
+ }
71
+ }
72
+ export function inkwellArguments(args) {
73
+ if (args[0] === 'backlog')
74
+ return [...args];
75
+ if (args[0] !== 'docs')
76
+ return null;
77
+ if (args[1] === 'propose')
78
+ return ['git', 'propose', ...args.slice(2)];
79
+ return args.slice(1);
80
+ }
81
+ let inkwellCache = null;
82
+ export function resetInkwellCache() {
83
+ inkwellCache = null;
84
+ }
85
+ async function discoverInkwell() {
86
+ if (process.env['RECAP_NO_INKWELL'] === '1')
87
+ return null;
88
+ const tool = await findTool('inkwell', { capability: 'docs.page.write' }).catch(() => null);
89
+ if (!tool)
90
+ return null;
91
+ try {
92
+ const { envelope } = await invokeTool(tool, ['migrate', 'status'], { env: { ...process.env, ...QUIET_ENV }, timeoutMs: 20_000 });
93
+ return envelope.ok && envelope.data?.migrated === true ? tool : null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ export function findInkwell() {
100
+ inkwellCache ??= discoverInkwell();
101
+ return inkwellCache;
102
+ }
103
+ export class InkwellClient {
104
+ tool;
105
+ constructor(tool) {
106
+ this.tool = tool;
107
+ }
108
+ async invoke(args) {
109
+ try {
110
+ const { envelope } = await invokeTool(this.tool, args, { env: { ...process.env, ...QUIET_ENV }, cwd: neutralCwd(), timeoutMs: 300_000 });
111
+ return {
112
+ ok: envelope.ok,
113
+ data: envelope.data,
114
+ meta: envelope.meta,
115
+ errorCode: envelope.error?.code,
116
+ errorMessage: envelope.error?.message,
117
+ };
118
+ }
119
+ catch (error) {
120
+ return { ok: false, errorCode: 'INKWELL_FAILED', errorMessage: errorMessage(error) };
121
+ }
122
+ }
123
+ }
124
+ export class DocsRouter {
125
+ bita;
126
+ inkwell;
127
+ constructor(bita, inkwell) {
128
+ this.bita = bita;
129
+ this.inkwell = inkwell;
130
+ }
131
+ async invoke(args) {
132
+ const mapped = this.inkwell ? inkwellArguments(args) : null;
133
+ if (mapped && this.inkwell)
134
+ return this.inkwell.invoke(mapped);
135
+ if (this.bita)
136
+ return this.bita.invoke(args);
137
+ return { ok: false, errorCode: 'DEPENDENCY_MISSING', errorMessage: 'bita not found' };
138
+ }
139
+ }
140
+ export function meetingTarget(meeting) {
141
+ return { databasePath: meeting.bitaDatabasePath, docsRoot: meeting.bitaDocsRoot };
142
+ }
143
+ export async function docsClient(config, target) {
144
+ const bita = await BitaClient.create(config, target);
145
+ const inkwell = await findInkwell();
146
+ if (!bita && !inkwell)
147
+ return null;
148
+ return new DocsRouter(bita, inkwell ? new InkwellClient(inkwell) : null);
149
+ }
150
+ export async function requireBita(config, target) {
151
+ const client = await BitaClient.create(config, target);
152
+ if (!client)
153
+ throw new RecapError('DEPENDENCY_MISSING', 'bita not found. Install it with `npm install -g @kikedealba/bita`', { hint: 'npm install -g @kikedealba/bita' });
154
+ return client;
155
+ }
@@ -0,0 +1,81 @@
1
+ import { isRecord } from "../core/json.js";
2
+ export const MEETING_KINDS = {
3
+ 'remote-meeting': 'remote',
4
+ 'in-person-meeting': 'in-person',
5
+ };
6
+ export const HOOK_EVENTS = ['start', 'stop', 'cancel', 'amend'];
7
+ function optionalString(value) {
8
+ return typeof value === 'string' ? value : undefined;
9
+ }
10
+ export function decodeHookEvent(text) {
11
+ const value = JSON.parse(text);
12
+ if (!isRecord(value) || typeof value['event'] !== 'string' || !isRecord(value['entry']))
13
+ throw new Error('the event has no event name or entry');
14
+ const entry = value['entry'];
15
+ if (typeof entry['id'] !== 'number' || !Number.isInteger(entry['id']) || typeof entry['description'] !== 'string')
16
+ throw new Error('the entry has no id or description');
17
+ const pageIds = Array.isArray(value['pageIds']) ? value['pageIds'].filter((id) => typeof id === 'number' && Number.isInteger(id)) : undefined;
18
+ return {
19
+ event: value['event'],
20
+ entry: {
21
+ id: entry['id'],
22
+ description: entry['description'],
23
+ kind: optionalString(entry['kind']),
24
+ running: typeof entry['running'] === 'boolean' ? entry['running'] : undefined,
25
+ projectName: optionalString(entry['projectName']),
26
+ },
27
+ previousKind: optionalString(value['previousKind']),
28
+ databasePath: optionalString(value['databasePath']),
29
+ docsRoot: optionalString(value['docsRoot']),
30
+ pageIds,
31
+ };
32
+ }
33
+ export function eventSnapshot(event) {
34
+ return { title: event.entry.description, projectName: event.entry.projectName, kind: event.entry.kind, pageIds: event.pageIds ?? [] };
35
+ }
36
+ export function eventMode(event) {
37
+ return event.entry.kind === undefined ? undefined : MEETING_KINDS[event.entry.kind];
38
+ }
39
+ export function previousMode(event) {
40
+ return event.previousKind === undefined ? undefined : MEETING_KINDS[event.previousKind];
41
+ }
42
+ export function eventTarget(event) {
43
+ return { databasePath: event.databasePath, docsRoot: event.docsRoot };
44
+ }
45
+ export function describeAction(action) {
46
+ switch (action.type) {
47
+ case 'start':
48
+ return `start(${action.mode})`;
49
+ case 'ignore':
50
+ return `ignore(${action.reason})`;
51
+ default:
52
+ return action.type;
53
+ }
54
+ }
55
+ export function planHook(event, activeEntryId) {
56
+ const isActive = activeEntryId === event.entry.id;
57
+ const mode = eventMode(event);
58
+ switch (event.event) {
59
+ case 'start':
60
+ if (!mode)
61
+ return { type: 'ignore', reason: `entry #${event.entry.id} is not a meeting` };
62
+ return activeEntryId === undefined ? { type: 'start', mode } : { type: 'ignore', reason: 'another meeting is already being recorded' };
63
+ case 'stop':
64
+ return isActive ? { type: 'stopAndProcess' } : { type: 'processIfRecorded' };
65
+ case 'cancel':
66
+ return { type: 'discard' };
67
+ case 'amend': {
68
+ const previous = previousMode(event);
69
+ if (mode && previous === undefined) {
70
+ if (event.entry.running === false)
71
+ return { type: 'ignore', reason: `entry #${event.entry.id} already stopped` };
72
+ return activeEntryId === undefined ? { type: 'start', mode } : { type: 'ignore', reason: 'another meeting is already being recorded' };
73
+ }
74
+ if (mode === undefined && previous !== undefined && isActive)
75
+ return { type: 'stopWithoutProcessing' };
76
+ return { type: 'ignore', reason: 'kind change does not affect the recording' };
77
+ }
78
+ default:
79
+ return { type: 'ignore', reason: `unknown event ${event.event}` };
80
+ }
81
+ }