@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 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
@@ -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. Cuando [inkwell](https://github.com/KikeDeAlba/inkwell) está
55
- instalado y ya migró los documentos de bita, las páginas, las propuestas y el
56
- backlog se escriben con inkwell; si no, con `bita docs` y `bita backlog`.
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 plugin
141
+ ## Plugin de Claude Code
135
142
 
136
- 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:
137
145
 
138
146
  ```
139
- /plugin marketplace add KikeDeAlba/recap
140
- /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
141
150
  ```
142
151
 
143
- | Command | What it does |
144
- |---|---|
145
- | `/recap-start [remota\|presencial] [título]` | Starts a recording; infers the mode from the arguments or the conversation and asks when it cannot |
146
- | `/recap-stop [--wait]` | Stops the recording; with `--wait` it processes in the foreground and shows agreements and action items |
147
- | `/recap-status` | Shows whether a recording is running and how far processing got |
148
- | `/recap-list [meeting]` | Lists meetings or shows one meeting's minutes |
149
- | `/recap-summarize [meeting] [instructions]` | Rewrites the minutes inside the session, following extra instructions, and stores them with `recap save-summary` |
150
- | `/recap-ask [question]` | Answers the last question of the meeting being recorded, or the one given, from the bita pages and the project repositories |
151
- | `/recap-proposals [meeting]` | Reviews the documentation changes proposed by a meeting and accepts, edits or rejects them |
152
-
153
- 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.
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 bita pages and the repositories registered for the entry's project (`bita project repo ls`, bita 0.16 or later):
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 bita's writing rule: nothing that reveals the conversation.
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 `bita docs propose`, and is listed in `proposals.json` (its markdown in `proposals/<n>.md`). Nothing reaches `main`, or Confluence, until it is accepted:
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 `bita docs branch apply`; 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. Turn it off with `recap config set live.proposals false`.
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) 0.12 or later, a meeting timer records the meeting while it runs:
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` registers the hook in bita (`bita hooks add --on start,stop,cancel,amend --kind in-person-meeting,remote-meeting -- …/recap bita-hook`). It works the same whether the timer is started from the terminal, from Claude Code or from bita-desktop.
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 in bita (see below) |
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 bita CLI:
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 `/bita-stop` asks;
233
- - a page is created with `bita docs page new --from-entry` in that project and written as formal documentation (no pending sections); if the timer already had a page, a `## Reunión <date>` section is added to it instead;
234
- - action items and open questions become `bita backlog` items on that page;
235
- - the minutes still land in the `## Reunión` section of the entry document.
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 sent to bita again.
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, a bita page, backlog items and the `## Reunión` section of the entry document |
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.
@@ -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 de bita: {{docsRoot}}
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 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.
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}}
@@ -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 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() {
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', { capability: 'docs.page.write' }).catch(() => null);
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
- 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;
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 findInkwell() {
100
- inkwellCache ??= discoverInkwell();
101
- return inkwellCache;
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)
@@ -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 { callBita } from "./client.js";
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(bita, branch, pageId, section, file, reason, entryId) {
194
- const args = ['docs', 'propose', '--branch', branch, String(pageId), '--md', file];
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 callBita(bita, args);
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('BITA_FAILED', 'bita docs propose did not return the commit sha');
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 ?? meeting.bitaEntry?.pageIds[0];
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
- bita;
209
+ inkwell;
210
210
  log;
211
211
  claude;
212
- constructor(config, bita, log, claude) {
212
+ constructor(config, inkwell, log, claude) {
213
213
  this.config = config;
214
- this.bita = bita;
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
- async candidates(meeting, context) {
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 pageId of meeting.bitaEntry?.pageIds ?? []) {
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
- const response = await this.bita.invoke(['docs', 'page', 'show', String(pageId), '--no-markdown']).catch(() => null);
228
- if (!response?.ok || !isRecord(response.data) || typeof response.data['title'] !== 'string')
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.bita.invoke(['docs', 'branch', 'drop', branch]).catch(() => null);
244
+ await this.inkwell.invoke(['branch', 'drop', branch]).catch(() => null);
248
245
  }
249
- const context = await loadProjectContext(await meetingProject(meeting, this.bita), meeting.bitaDocsRoot, this.bita);
250
- const pages = await this.candidates(meeting, context);
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.bita, branch, draft.pageId, draft.section, markdownFile, draft.title, entryId);
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
- bita;
327
- constructor(dir, bita) {
324
+ inkwell;
325
+ constructor(dir, inkwell) {
328
326
  this.dir = dir;
329
- this.bita = bita;
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.bita.invoke(['docs', 'branch', 'diff', proposal.branch, '--commit', proposal.sha]).catch(() => null);
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.bita.invoke(['docs', 'branch', 'drop', file.branch]).catch(() => null);
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.bita, proposal.branch, proposal.pageId, proposal.section, target, proposal.title, file.entryId);
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.bita.invoke(['docs', 'branch', 'apply', proposal.branch, '--commit', proposal.sha]);
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('BITA_FAILED', `bita docs branch apply failed: ${response.errorMessage ?? 'no reason given'}`);
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);