@kikedealba/recap 0.9.1 → 0.11.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/README.md +68 -37
- package/Resources/ask-prompt.md +1 -1
- package/Resources/wrapup-prompt.md +1 -1
- package/dist/bita/client.js +39 -51
- package/dist/bita/hook.js +6 -0
- package/dist/bita/proposals.js +30 -32
- package/dist/bita/wrapup.js +41 -22
- package/dist/cli/commands/ask.js +4 -6
- package/dist/cli/commands/hook.js +15 -0
- package/dist/cli/commands/proposals.js +4 -6
- package/dist/cli/commands/query.js +11 -3
- package/dist/cli/commands/watch.js +109 -0
- package/dist/cli/router.js +3 -0
- package/dist/live/context.js +47 -20
- package/dist/live/worker.js +2 -6
- package/dist/pipeline/pipeline.js +11 -7
- package/dist/setup/integration.js +24 -29
- package/package.json +3 -4
- package/skills/recap/SKILL.md +77 -137
- package/skills/recap/live.md +31 -0
- package/skills/recap/proposals.md +19 -0
- package/skills/recap/storage.md +16 -0
- package/skills/recap/wrapup.md +49 -0
- package/{commands/recap-ask.md → skills/recap-ask/SKILL.md} +2 -0
- package/{commands/recap-list.md → skills/recap-list/SKILL.md} +2 -0
- package/{commands/recap-proposals.md → skills/recap-proposals/SKILL.md} +6 -2
- package/{commands/recap-start.md → skills/recap-start/SKILL.md} +2 -0
- package/{commands/recap-status.md → skills/recap-status/SKILL.md} +2 -0
- package/skills/recap-stop/SKILL.md +73 -0
- package/{commands/recap-summarize.md → skills/recap-summarize/SKILL.md} +2 -0
- package/commands/recap-stop.md +0 -69
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
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
@@ -51,9 +56,11 @@ recap import llamada.mov --remote # video con dos pistas
|
|
|
51
56
|
|
|
52
57
|
Los archivos en disco, los comandos, las opciones y el sobre `--json` son los
|
|
53
58
|
mismos que los del `recap` en Swift, así que las dos versiones leen las mismas
|
|
54
|
-
reuniones.
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
reuniones. bita solo lleva el tiempo: las páginas, las propuestas, el backlog
|
|
60
|
+
y la nota de cada entrada viven en [inkwell](https://github.com/KikeDeAlba/inkwell),
|
|
61
|
+
y el volcado del tiempo a Jira lo hace [tally](https://github.com/KikeDeAlba/tally).
|
|
62
|
+
Sin inkwell la reunión se procesa igual: las etapas `proposals` y `wrapup` se
|
|
63
|
+
saltan con un aviso y la minuta se queda en la carpeta de la reunión.
|
|
57
64
|
|
|
58
65
|
Para trabajar en el paquete: `pnpm install`, `pnpm test`, `pnpm typecheck` y
|
|
59
66
|
`node src/bin/recap.ts <comando>`.
|
|
@@ -131,26 +138,47 @@ recap prompt <meeting> # print the summary prompt with transcrip
|
|
|
131
138
|
recap save-summary <meeting> file.md # store minutes written elsewhere
|
|
132
139
|
```
|
|
133
140
|
|
|
134
|
-
## Claude Code
|
|
141
|
+
## Plugin de Claude Code
|
|
135
142
|
|
|
136
|
-
|
|
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:
|
|
137
145
|
|
|
138
146
|
```
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
141
150
|
```
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
154
182
|
|
|
155
183
|
## Live assistant
|
|
156
184
|
|
|
@@ -160,7 +188,7 @@ While a meeting is being recorded, recap also transcribes it live:
|
|
|
160
188
|
- A detached `recap live-worker` transcribes each chunk with `whisper-cli` (same model and VAD as the pipeline, 4 threads, the previous text as prompt), drops hallucinations and microphone echo, and appends `{startMs, endMs, channel, text}` lines to `live/transcript.jsonl`, with offsets from the start of the recording. It exits once the recording stops and the queue is empty.
|
|
161
189
|
- The transcript after `stop` is still the source of truth; the live one is for answering during the meeting.
|
|
162
190
|
|
|
163
|
-
`recap ask` answers a question with Claude Code (headless, streaming), reading only the
|
|
191
|
+
`recap ask` answers a question with Claude Code (headless, streaming), reading only the inkwell pages of the project (`inkwell page ls --project`) and the repositories registered for it in bita (`bita project repo ls`). Without inkwell it answers from the repositories alone:
|
|
164
192
|
|
|
165
193
|
```sh
|
|
166
194
|
recap ask --active # the last question in the live transcript
|
|
@@ -191,9 +219,9 @@ With `live.autoAsk` on (the default), the live worker also looks for questions o
|
|
|
191
219
|
|
|
192
220
|
### Proposed documentation changes
|
|
193
221
|
|
|
194
|
-
For meetings linked to a bita entry, the `proposals` stage (before `wrapup`) asks Claude Code (`Resources/proposals-prompt.md`) for the explicit, firm changes said about existing pages: the project pages and the pages linked to the entry, never the meeting's own page. Ideas, doubts and statements corrected later are left out, and the text follows
|
|
222
|
+
For meetings linked to a bita entry, when inkwell is installed, the `proposals` stage (before `wrapup`) asks Claude Code (`Resources/proposals-prompt.md`) for the explicit, firm changes said about existing pages: the project pages and the pages linked to the entry in inkwell (`inkwell page ls --entry`), never the meeting's own page. Ideas, doubts and statements corrected later are left out, and the text follows the documentation writing rule: nothing that reveals the conversation.
|
|
195
223
|
|
|
196
|
-
Each change becomes a commit on the docs branch `proposal/meeting-<entry>` through `
|
|
224
|
+
Each change becomes a commit on the docs branch `proposal/meeting-<entry>` through `inkwell git propose`, and is listed in `proposals.json` (its markdown in `proposals/<n>.md`). Nothing reaches `main`, or Confluence, until it is accepted:
|
|
197
225
|
|
|
198
226
|
```sh
|
|
199
227
|
recap proposals ls <meeting> --json # or --bita-entry <id>
|
|
@@ -202,41 +230,44 @@ recap proposals accept <meeting> <n> [--md edited.md]
|
|
|
202
230
|
recap proposals reject <meeting> <n>
|
|
203
231
|
```
|
|
204
232
|
|
|
205
|
-
`accept` runs `
|
|
233
|
+
`accept` runs `inkwell branch apply` (and needs inkwell); with `--md` it first proposes the edited text again. If the page changed since the proposal, the merge conflicts and the proposal turns `stale` (the command still succeeds). When no proposal is pending, the branch is dropped. A failure in this stage is recorded in `stages.proposals` and never stops the wrap-up; without inkwell the stage is `skipped` with the reason in `stages.proposals.reason`. Turn it off with `recap config set live.proposals false`.
|
|
206
234
|
|
|
207
235
|
## bita integration
|
|
208
236
|
|
|
209
|
-
With [bita](https://github.com/KikeDeAlba/bita-cli)
|
|
237
|
+
With [bita](https://github.com/KikeDeAlba/bita-cli), a meeting timer records the meeting while it runs:
|
|
210
238
|
|
|
211
239
|
```sh
|
|
212
240
|
bita start "Planeación sprint 42" --kind remote-meeting # recap starts recording the screen, system audio and mic
|
|
213
241
|
bita start "1:1 con Ana" --kind in-person-meeting # recap records the mic only
|
|
214
|
-
bita stop # recap stops, processes, and writes the minutes into the entry
|
|
242
|
+
bita stop # recap stops, processes, and writes the minutes into the entry note in inkwell
|
|
215
243
|
```
|
|
216
244
|
|
|
217
|
-
`recap setup`
|
|
245
|
+
`recap setup` subscribes `recap bita-hook` to bita's `start`, `stop`, `cancel` and `amend` events through the kit registry. It works the same whether the timer is started from the terminal, from Claude Code or from bita-desktop.
|
|
218
246
|
|
|
219
247
|
| bita event | recap |
|
|
220
248
|
|---|---|
|
|
221
249
|
| `start` of a meeting kind | starts recording in the matching mode, linked to the entry |
|
|
222
|
-
| `stop` | stops, processes in the background and wraps the meeting up
|
|
250
|
+
| `stop` | stops, processes in the background and wraps the meeting up (see below) |
|
|
223
251
|
| `cancel` | discards the recording |
|
|
224
252
|
| `amend --kind <meeting kind>` on a running entry | starts recording |
|
|
225
253
|
| `amend --kind none` while recording | stops without processing; the recording is kept |
|
|
254
|
+
| `amend` of the title or project only | updates the meeting's title and entry data; the recording is not touched |
|
|
226
255
|
|
|
227
256
|
### Wrap-up
|
|
228
257
|
|
|
229
|
-
After the summary, the `wrapup` stage asks Claude Code (headless, `Resources/wrapup-prompt.md`) for a title, a project, a documentation page and backlog items, and applies them through the
|
|
258
|
+
After the summary, the `wrapup` stage asks Claude Code (headless, `Resources/wrapup-prompt.md`) for a title, a project, a documentation page and backlog items, and applies them: title and project through `bita amend`, everything else through inkwell (found in the kit registry by its capabilities):
|
|
230
259
|
|
|
231
260
|
- the timer gets the meeting's real topic as its title when the current one is generic ("Reunión presencial", "Junta", …);
|
|
232
|
-
- if the timer has no project, it gets one only when the conversation makes it clear and the name exists in `bita projects`; otherwise `wrapup.projectResolved` is false and `/
|
|
233
|
-
- a page is created with `
|
|
234
|
-
- action items and
|
|
235
|
-
- the minutes
|
|
261
|
+
- if the timer has no project, it gets one only when the conversation makes it clear and the name exists in `bita projects`; otherwise `wrapup.projectResolved` is false and `/recap-stop` asks;
|
|
262
|
+
- a page is created with `inkwell page new --from-entry` in that project and written as formal documentation (no pending sections); if the entry already has a page in inkwell (`inkwell page ls --entry`), a `## Reunión <date>` section is added to it instead;
|
|
263
|
+
- action items and findings become `inkwell backlog add` items on that page;
|
|
264
|
+
- the minutes land in the `Reunión` section of the entry note (`inkwell note save <entry> --section Reunión --md …`, capability `docs.entry-notes`).
|
|
265
|
+
|
|
266
|
+
Without inkwell the stage is `skipped` (reason and hint `npm i -g @kikedealba/inkwell && inkwell setup` in `stages.wrapup`) and the meeting is still `processed`; the minutes stay in `summary.md`. With an inkwell lacking `docs.entry-notes` the page and backlog are written and `process.log` notes that the minutes were not saved. Run `recap process <id> --from proposals` after installing it. Time goes to Jira later through tally, never from recap.
|
|
236
267
|
|
|
237
268
|
`recap wait --bita-entry <id>` blocks until all of that is done and prints the result (`data.wrapup` with `--json`).
|
|
238
269
|
|
|
239
|
-
The hook output goes to `hooks.log` beside the bita database. Minutes regenerated with `/recap-summarize` or `recap save-summary` are
|
|
270
|
+
The hook output goes to `hooks.log` beside the bita database. Minutes regenerated with `/recap-summarize` or `recap save-summary` are saved in the entry note again when inkwell is installed.
|
|
240
271
|
|
|
241
272
|
## Pipeline
|
|
242
273
|
|
|
@@ -246,8 +277,8 @@ The hook output goes to `hooks.log` beside the bita database. Minutes regenerate
|
|
|
246
277
|
| `transcribe` | per channel, then merged | mic | `transcript.json`, `transcript.md` |
|
|
247
278
|
| `frames` | scene changes, at least 20 s apart, at most 40 | skipped | `frames/hh-mm-ss.jpg`, `frames.json` |
|
|
248
279
|
| `summarize` | `claude -p` with transcript and frames | `claude -p` with transcript | `summary.md` |
|
|
249
|
-
| `proposals` | only when linked to a bita entry and `live.proposals` is on | same | `proposals.json`, `proposals/<n>.md` and the docs branch `proposal/meeting-<entry>` |
|
|
250
|
-
| `wrapup` | only when linked to a bita entry | same | entry title and project,
|
|
280
|
+
| `proposals` | only when linked to a bita entry and `live.proposals` is on; skipped without inkwell | same | `proposals.json`, `proposals/<n>.md` and the docs branch `proposal/meeting-<entry>` |
|
|
281
|
+
| `wrapup` | only when linked to a bita entry; skipped without inkwell | same | entry title and project (bita), an inkwell page, backlog items and the `Reunión` section of the entry note |
|
|
251
282
|
|
|
252
283
|
- Transcription runs locally with `whisper-cli`, `large-v3-turbo` and Silero VAD. Known whisper hallucinations on silence are dropped.
|
|
253
284
|
- In remote meetings the microphone is labelled **Sala** and the call audio **Remotos**. When the microphone picks up the speakers, segments that repeat the call audio within a few seconds are removed as echo; headphones avoid the problem entirely.
|
package/Resources/ask-prompt.md
CHANGED
|
@@ -10,7 +10,7 @@ Estás acompañando en vivo una reunión. Quien te consulta necesita una respues
|
|
|
10
10
|
|
|
11
11
|
Responde solo con lo que encuentres en estas fuentes. Puedes leerlas con Read, Grep y Glob. Para el historial de un repositorio usa solo `git -C <ruta del repo> log`, `git -C <ruta del repo> show` o `git -C <ruta del repo> diff`, con la ruta exacta de la lista: un comando por llamada, sin `cd`, sin `&&` y sin tuberías; cualquier otra forma se rechaza.
|
|
12
12
|
|
|
13
|
-
- Raíz de la documentación
|
|
13
|
+
- Raíz de la documentación (inkwell): {{docsRoot}}
|
|
14
14
|
- Páginas del proyecto (ruta relativa a la raíz):
|
|
15
15
|
{{pages}}
|
|
16
16
|
- Repositorios del proyecto:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Vas a cerrar
|
|
1
|
+
Vas a cerrar 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
2
|
|
|
3
3
|
- Título actual del contador: «{{currentTitle}}»
|
|
4
4
|
- Proyecto actual del contador: {{currentProject}}
|
package/dist/bita/client.js
CHANGED
|
@@ -53,8 +53,6 @@ export class BitaClient {
|
|
|
53
53
|
const args = [];
|
|
54
54
|
if (this.target.databasePath)
|
|
55
55
|
args.push('--db-path', this.target.databasePath);
|
|
56
|
-
if (this.target.docsRoot)
|
|
57
|
-
args.push('--docs-dir', this.target.docsRoot);
|
|
58
56
|
return args;
|
|
59
57
|
}
|
|
60
58
|
async run(args) {
|
|
@@ -69,36 +67,49 @@ export class BitaClient {
|
|
|
69
67
|
return parseResponse(result.stdout, result.stderr, result.status);
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
|
-
export
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
let inkwellCache = null;
|
|
82
|
-
export function resetInkwellCache() {
|
|
83
|
-
inkwellCache = null;
|
|
84
|
-
}
|
|
85
|
-
async function discoverInkwell() {
|
|
70
|
+
export const INKWELL_HINT = 'npm i -g @kikedealba/inkwell && inkwell setup';
|
|
71
|
+
export const INKWELL_CAPABILITIES = {
|
|
72
|
+
pages: ['docs.page.read'],
|
|
73
|
+
wrapup: ['docs.page.read', 'docs.page.write', 'docs.backlog'],
|
|
74
|
+
proposals: ['docs.page.read', 'docs.propose'],
|
|
75
|
+
notes: ['docs.entry-notes'],
|
|
76
|
+
};
|
|
77
|
+
export async function lookupInkwell(capabilities = []) {
|
|
86
78
|
if (process.env['RECAP_NO_INKWELL'] === '1')
|
|
87
|
-
return null;
|
|
88
|
-
const tool = await findTool('inkwell'
|
|
79
|
+
return { client: null, reason: 'inkwell is turned off (RECAP_NO_INKWELL=1)' };
|
|
80
|
+
const tool = await findTool('inkwell').catch(() => null);
|
|
89
81
|
if (!tool)
|
|
90
|
-
return null;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
return { client: null, reason: 'inkwell is not installed' };
|
|
83
|
+
const missing = capabilities.filter((capability) => !tool.manifest.capabilities.includes(capability));
|
|
84
|
+
if (missing.length > 0)
|
|
85
|
+
return { client: null, reason: `inkwell ${tool.manifest.version} lacks ${missing.join(', ')}` };
|
|
86
|
+
return { client: new InkwellClient(tool) };
|
|
87
|
+
}
|
|
88
|
+
export async function findInkwell(capabilities = []) {
|
|
89
|
+
return (await lookupInkwell(capabilities)).client;
|
|
90
|
+
}
|
|
91
|
+
export async function requireInkwell(capabilities, purpose) {
|
|
92
|
+
const found = await lookupInkwell(capabilities);
|
|
93
|
+
if (found.client)
|
|
94
|
+
return found.client;
|
|
95
|
+
throw new RecapError('DEPENDENCY_MISSING', `${purpose} needs inkwell: ${found.reason}`, { hint: INKWELL_HINT });
|
|
96
|
+
}
|
|
97
|
+
export class StageSkipped extends RecapError {
|
|
98
|
+
constructor(message, hint) {
|
|
99
|
+
super('STAGE_SKIPPED', message, hint === undefined ? {} : { hint });
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
|
-
export function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
export async function inkwellForStage(capabilities) {
|
|
103
|
+
const found = await lookupInkwell(capabilities);
|
|
104
|
+
if (found.client)
|
|
105
|
+
return found.client;
|
|
106
|
+
throw new StageSkipped(found.reason, INKWELL_HINT);
|
|
107
|
+
}
|
|
108
|
+
export async function callInkwell(inkwell, args) {
|
|
109
|
+
const response = await inkwell.invoke(args);
|
|
110
|
+
if (!response.ok)
|
|
111
|
+
throw new RecapError('INKWELL_FAILED', `inkwell ${args.slice(0, 2).join(' ')} failed: ${response.errorMessage ?? 'no reason given'}`);
|
|
112
|
+
return response.data;
|
|
102
113
|
}
|
|
103
114
|
export class InkwellClient {
|
|
104
115
|
tool;
|
|
@@ -121,32 +132,9 @@ export class InkwellClient {
|
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
134
|
}
|
|
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
135
|
export function meetingTarget(meeting) {
|
|
141
136
|
return { databasePath: meeting.bitaDatabasePath, docsRoot: meeting.bitaDocsRoot };
|
|
142
137
|
}
|
|
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
138
|
export async function requireBita(config, target) {
|
|
151
139
|
const client = await BitaClient.create(config, target);
|
|
152
140
|
if (!client)
|
package/dist/bita/hook.js
CHANGED
|
@@ -14,6 +14,8 @@ export function decodeHookEvent(text) {
|
|
|
14
14
|
const entry = value['entry'];
|
|
15
15
|
if (typeof entry['id'] !== 'number' || !Number.isInteger(entry['id']) || typeof entry['description'] !== 'string')
|
|
16
16
|
throw new Error('the entry has no id or description');
|
|
17
|
+
const reportsEveryChange = 'previousTitle' in value || 'previousProjectId' in value;
|
|
18
|
+
const kindChanged = !reportsEveryChange || (optionalString(value['previousKind']) ?? null) !== (optionalString(entry['kind']) ?? null);
|
|
17
19
|
const pageIds = Array.isArray(value['pageIds']) ? value['pageIds'].filter((id) => typeof id === 'number' && Number.isInteger(id)) : undefined;
|
|
18
20
|
return {
|
|
19
21
|
event: value['event'],
|
|
@@ -25,9 +27,11 @@ export function decodeHookEvent(text) {
|
|
|
25
27
|
projectName: optionalString(entry['projectName']),
|
|
26
28
|
},
|
|
27
29
|
previousKind: optionalString(value['previousKind']),
|
|
30
|
+
previousTitle: optionalString(value['previousTitle']),
|
|
28
31
|
databasePath: optionalString(value['databasePath']),
|
|
29
32
|
docsRoot: optionalString(value['docsRoot']),
|
|
30
33
|
pageIds,
|
|
34
|
+
kindChanged,
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
export function eventSnapshot(event) {
|
|
@@ -65,6 +69,8 @@ export function planHook(event, activeEntryId) {
|
|
|
65
69
|
case 'cancel':
|
|
66
70
|
return { type: 'discard' };
|
|
67
71
|
case 'amend': {
|
|
72
|
+
if (!event.kindChanged)
|
|
73
|
+
return { type: 'refresh' };
|
|
68
74
|
const previous = previousMode(event);
|
|
69
75
|
if (mode && previous === undefined) {
|
|
70
76
|
if (event.entry.running === false)
|
package/dist/bita/proposals.js
CHANGED
|
@@ -7,10 +7,10 @@ import { withDirectoryLockAsync } from "../core/lock.js";
|
|
|
7
7
|
import { home } from "../core/paths.js";
|
|
8
8
|
import { fold, prefix, replaceAll, trimmed } from "../core/text.js";
|
|
9
9
|
import { RecapError, errorMessage } from "../errors.js";
|
|
10
|
-
import { loadProjectContext, meetingProject } from "../live/context.js";
|
|
10
|
+
import { entryPages, loadProjectContext, meetingProject } from "../live/context.js";
|
|
11
11
|
import { resultText, runClaude } from "../pipeline/claude.js";
|
|
12
12
|
import { loadResource } from "../pipeline/resources.js";
|
|
13
|
-
import {
|
|
13
|
+
import { callInkwell } from "./client.js";
|
|
14
14
|
import { meetingDay } from "./wrapup.js";
|
|
15
15
|
export const REVEALING_PHRASES = [
|
|
16
16
|
'se acordo',
|
|
@@ -190,45 +190,42 @@ export function saveProposals(file, dir) {
|
|
|
190
190
|
discarded: file.discarded.map((item) => ({ index: item.index, pageId: item.pageId, reason: item.reason })),
|
|
191
191
|
}));
|
|
192
192
|
}
|
|
193
|
-
export async function propose(
|
|
194
|
-
const args = ['
|
|
193
|
+
export async function propose(inkwell, branch, pageId, section, file, reason, entryId) {
|
|
194
|
+
const args = ['git', 'propose', '--branch', branch, String(pageId), '--md', file];
|
|
195
195
|
if (section !== null)
|
|
196
196
|
args.push('--section', section);
|
|
197
197
|
args.push('--reason', reason, '--source', `meeting:${entryId}`);
|
|
198
|
-
const data = await
|
|
198
|
+
const data = await callInkwell(inkwell, args);
|
|
199
199
|
const sha = isRecord(data) && typeof data['sha'] === 'string' ? data['sha'] : '';
|
|
200
200
|
if (sha.length === 0)
|
|
201
|
-
throw new RecapError('
|
|
201
|
+
throw new RecapError('INKWELL_FAILED', 'inkwell git propose did not return the commit sha');
|
|
202
202
|
return sha;
|
|
203
203
|
}
|
|
204
|
-
export function ownPageId(meeting) {
|
|
205
|
-
return meeting.wrapup?.pageId ??
|
|
204
|
+
export function ownPageId(meeting, linked) {
|
|
205
|
+
return meeting.wrapup?.pageId ?? linked[0]?.pageId;
|
|
206
206
|
}
|
|
207
207
|
export class ProposalGenerator {
|
|
208
208
|
config;
|
|
209
|
-
|
|
209
|
+
inkwell;
|
|
210
210
|
log;
|
|
211
211
|
claude;
|
|
212
|
-
constructor(config,
|
|
212
|
+
constructor(config, inkwell, log, claude) {
|
|
213
213
|
this.config = config;
|
|
214
|
-
this.
|
|
214
|
+
this.inkwell = inkwell;
|
|
215
215
|
this.log = log;
|
|
216
216
|
this.claude =
|
|
217
217
|
claude ??
|
|
218
218
|
(async (prompt, dirs) => resultText(await runClaude({ prompt, cwd: dirs[dirs.length - 1] ?? home(), config, tools: ['Read Grep Glob'], addDirs: dirs, model: config.summaryModel })));
|
|
219
219
|
}
|
|
220
|
-
|
|
221
|
-
const own = ownPageId(meeting);
|
|
220
|
+
candidates(meeting, context, linked) {
|
|
221
|
+
const own = ownPageId(meeting, linked);
|
|
222
222
|
const pages = [...context.pages];
|
|
223
223
|
const known = new Set(pages.map((page) => page.pageId));
|
|
224
|
-
for (const
|
|
225
|
-
if (known.has(pageId) || pageId === own)
|
|
224
|
+
for (const page of linked) {
|
|
225
|
+
if (known.has(page.pageId) || page.pageId === own)
|
|
226
226
|
continue;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
continue;
|
|
230
|
-
pages.push({ pageId, title: response.data['title'], relPath: typeof response.data['relPath'] === 'string' ? response.data['relPath'] : '', depth: 0 });
|
|
231
|
-
known.add(pageId);
|
|
227
|
+
pages.push({ ...page, depth: 0 });
|
|
228
|
+
known.add(page.pageId);
|
|
232
229
|
}
|
|
233
230
|
return pages.filter((page) => page.pageId !== own);
|
|
234
231
|
}
|
|
@@ -244,10 +241,11 @@ export class ProposalGenerator {
|
|
|
244
241
|
return existing;
|
|
245
242
|
}
|
|
246
243
|
if (existing.proposals.length > 0)
|
|
247
|
-
await this.
|
|
244
|
+
await this.inkwell.invoke(['branch', 'drop', branch]).catch(() => null);
|
|
248
245
|
}
|
|
249
|
-
const
|
|
250
|
-
const
|
|
246
|
+
const linked = await entryPages(this.inkwell, entryId);
|
|
247
|
+
const context = await loadProjectContext(await meetingProject(meeting, this.inkwell, linked), { inkwell: this.inkwell, bita: null });
|
|
248
|
+
const pages = this.candidates(meeting, context, linked);
|
|
251
249
|
const file = { entryId, branch, generatedAt: isoNow(), branchDropped: false, proposals: [], discarded: [] };
|
|
252
250
|
if (pages.length === 0 || context.docsRoot === null) {
|
|
253
251
|
this.log('proposals: no candidate pages');
|
|
@@ -276,7 +274,7 @@ export class ProposalGenerator {
|
|
|
276
274
|
const markdownFile = proposalStore.markdownFile(dir, n);
|
|
277
275
|
writeAtomic(markdownFile, `${draft.markdown}\n`);
|
|
278
276
|
try {
|
|
279
|
-
const sha = await propose(this.
|
|
277
|
+
const sha = await propose(this.inkwell, branch, draft.pageId, draft.section, markdownFile, draft.title, entryId);
|
|
280
278
|
file.proposals.push({
|
|
281
279
|
n,
|
|
282
280
|
pageId: draft.pageId,
|
|
@@ -323,10 +321,10 @@ export class ProposalGenerator {
|
|
|
323
321
|
}
|
|
324
322
|
export class ProposalReview {
|
|
325
323
|
dir;
|
|
326
|
-
|
|
327
|
-
constructor(dir,
|
|
324
|
+
inkwell;
|
|
325
|
+
constructor(dir, inkwell) {
|
|
328
326
|
this.dir = dir;
|
|
329
|
-
this.
|
|
327
|
+
this.inkwell = inkwell;
|
|
330
328
|
}
|
|
331
329
|
list() {
|
|
332
330
|
return loadProposals(this.dir)?.proposals ?? [];
|
|
@@ -341,7 +339,7 @@ export class ProposalReview {
|
|
|
341
339
|
return found;
|
|
342
340
|
}
|
|
343
341
|
async diff(proposal) {
|
|
344
|
-
const response = await this.
|
|
342
|
+
const response = await this.inkwell.invoke(['branch', 'diff', proposal.branch, '--commit', proposal.sha]).catch(() => null);
|
|
345
343
|
return response?.ok ? response.data : null;
|
|
346
344
|
}
|
|
347
345
|
load() {
|
|
@@ -361,7 +359,7 @@ export class ProposalReview {
|
|
|
361
359
|
if (index !== -1)
|
|
362
360
|
file.proposals[index] = proposal;
|
|
363
361
|
if (!file.branchDropped && !file.proposals.some((item) => item.status === 'pending')) {
|
|
364
|
-
const response = await this.
|
|
362
|
+
const response = await this.inkwell.invoke(['branch', 'drop', file.branch]).catch(() => null);
|
|
365
363
|
file.branchDropped = response?.ok === true;
|
|
366
364
|
}
|
|
367
365
|
saveProposals(file, this.dir);
|
|
@@ -384,10 +382,10 @@ export class ProposalReview {
|
|
|
384
382
|
writeAtomic(target, text);
|
|
385
383
|
}
|
|
386
384
|
if (editedMarkdown !== undefined || file.branchDropped) {
|
|
387
|
-
proposal.sha = await propose(this.
|
|
385
|
+
proposal.sha = await propose(this.inkwell, proposal.branch, proposal.pageId, proposal.section, target, proposal.title, file.entryId);
|
|
388
386
|
file.branchDropped = false;
|
|
389
387
|
}
|
|
390
|
-
const response = await this.
|
|
388
|
+
const response = await this.inkwell.invoke(['branch', 'apply', proposal.branch, '--commit', proposal.sha]);
|
|
391
389
|
if (response.ok) {
|
|
392
390
|
proposal.status = 'accepted';
|
|
393
391
|
const data = isRecord(response.data) ? response.data : {};
|
|
@@ -397,7 +395,7 @@ export class ProposalReview {
|
|
|
397
395
|
proposal.status = 'stale';
|
|
398
396
|
}
|
|
399
397
|
else {
|
|
400
|
-
throw new RecapError('
|
|
398
|
+
throw new RecapError('INKWELL_FAILED', `inkwell branch apply failed: ${response.errorMessage ?? 'no reason given'}`);
|
|
401
399
|
}
|
|
402
400
|
proposal.updatedAt = isoNow();
|
|
403
401
|
await this.store(proposal, file);
|