@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/dist/bita/wrapup.js
CHANGED
|
@@ -3,11 +3,12 @@ import { dayString, parseDate } from "../core/dates.js";
|
|
|
3
3
|
import { readText, writeAtomic } from "../core/fsutil.js";
|
|
4
4
|
import { extractBetween, isRecord, swiftDefault } from "../core/json.js";
|
|
5
5
|
import { durationSeconds, emptyWrapup, updateMeeting } from "../core/meeting.js";
|
|
6
|
-
import { fold, formatDuration, prefix, replaceAll,
|
|
6
|
+
import { fold, formatDuration, prefix, replaceAll, trimmed } from "../core/text.js";
|
|
7
7
|
import { RecapError } from "../errors.js";
|
|
8
8
|
import { resultText, runSummaryClaude } from "../pipeline/claude.js";
|
|
9
9
|
import { loadResource } from "../pipeline/resources.js";
|
|
10
|
-
import { callBita,
|
|
10
|
+
import { INKWELL_CAPABILITIES, INKWELL_HINT, StageSkipped, callBita, callInkwell, lookupInkwell, meetingTarget, requireBita } from "./client.js";
|
|
11
|
+
import { entryPages } from "../live/context.js";
|
|
11
12
|
export const FORBIDDEN_SECTIONS = ['pendiente', 'proximos pasos', 'hallazgo', 'preguntas abiertas', 'lo que falta'];
|
|
12
13
|
export function stripForbiddenSections(markdown) {
|
|
13
14
|
const kept = [];
|
|
@@ -127,20 +128,30 @@ export function noteBody(summary, meeting, dir) {
|
|
|
127
128
|
lines.push('', `Archivos de la reunión: \`${dir}\` (\`summary.md\`, \`transcript.md\`${meeting.mode === 'remote' ? ', `frames/`' : ''}).`);
|
|
128
129
|
return `${lines.join('\n')}\n`;
|
|
129
130
|
}
|
|
130
|
-
export async function saveMinutes(meeting, dir,
|
|
131
|
+
export async function saveMinutes(meeting, dir, notes, log) {
|
|
131
132
|
const entryId = meeting.bitaEntryId;
|
|
132
133
|
if (entryId === undefined)
|
|
133
|
-
return;
|
|
134
|
-
const bita = client ?? (await requireBita(config, meetingTarget(meeting)));
|
|
134
|
+
return { saved: false, reason: 'the meeting has no bita entry' };
|
|
135
135
|
const summary = readText(path.join(dir, 'summary.md'));
|
|
136
136
|
if (summary === null)
|
|
137
|
-
throw new RecapError('NO_SUMMARY', `"${meeting.title}" has no summary to
|
|
138
|
-
const note = path.join(dir, '
|
|
137
|
+
throw new RecapError('NO_SUMMARY', `"${meeting.title}" has no summary to save in the entry note`);
|
|
138
|
+
const note = path.join(dir, 'entry-note.md');
|
|
139
139
|
writeAtomic(note, noteBody(summary, meeting, dir));
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
let client = notes;
|
|
141
|
+
let reason = 'no inkwell with docs.entry-notes was given';
|
|
142
|
+
if (client === undefined) {
|
|
143
|
+
const found = await lookupInkwell(INKWELL_CAPABILITIES.notes);
|
|
144
|
+
client = found.client;
|
|
145
|
+
if (found.reason !== undefined)
|
|
146
|
+
reason = found.reason;
|
|
147
|
+
}
|
|
148
|
+
if (!client) {
|
|
149
|
+
log?.(`minutes: not saved in the note of entry #${entryId}, ${reason}; they stay in ${dir} (${INKWELL_HINT})`);
|
|
150
|
+
return { saved: false, reason };
|
|
143
151
|
}
|
|
152
|
+
await callInkwell(client, ['note', 'save', String(entryId), '--section', NOTE_SECTION, '--md', note]);
|
|
153
|
+
log?.(`minutes: saved in the note of entry #${entryId}`);
|
|
154
|
+
return { saved: true };
|
|
144
155
|
}
|
|
145
156
|
export function meetingDay(meeting) {
|
|
146
157
|
return dayString(parseDate(meeting.startedAt) ?? parseDate(meeting.createdAt) ?? new Date());
|
|
@@ -155,7 +166,7 @@ export function encodeWrapupPlan(plan) {
|
|
|
155
166
|
});
|
|
156
167
|
}
|
|
157
168
|
async function pageText(docs, pageId) {
|
|
158
|
-
const page = await
|
|
169
|
+
const page = await callInkwell(docs, ['page', 'show', String(pageId)]);
|
|
159
170
|
const doc = isRecord(page) && isRecord(page['doc']) ? page['doc'] : undefined;
|
|
160
171
|
const file = typeof doc?.['path'] === 'string' ? doc['path'] : undefined;
|
|
161
172
|
return file ? readText(file) : null;
|
|
@@ -182,8 +193,16 @@ export async function runWrapup(meeting, dir, config, dependencies) {
|
|
|
182
193
|
const entryId = meeting.bitaEntryId;
|
|
183
194
|
if (entryId === undefined)
|
|
184
195
|
return;
|
|
196
|
+
let docs = dependencies?.docs;
|
|
197
|
+
if (!docs) {
|
|
198
|
+
const found = await lookupInkwell(INKWELL_CAPABILITIES.wrapup);
|
|
199
|
+
if (!found.client) {
|
|
200
|
+
await saveMinutes(meeting, dir, undefined, dependencies?.log).catch((error) => dependencies?.log?.(`minutes: ${String(error)}`));
|
|
201
|
+
throw new StageSkipped(found.reason, INKWELL_HINT);
|
|
202
|
+
}
|
|
203
|
+
docs = found.client;
|
|
204
|
+
}
|
|
185
205
|
const bita = dependencies?.bita ?? (await requireBita(config, meetingTarget(meeting)));
|
|
186
|
-
const docs = dependencies?.docs ?? (await docsClient(config, meetingTarget(meeting))) ?? bita;
|
|
187
206
|
const claude = dependencies?.claude ?? (async (prompt) => resultText(await runSummaryClaude(prompt, dir, config)));
|
|
188
207
|
const snapshot = meeting.bitaEntry ?? { title: meeting.title, pageIds: [] };
|
|
189
208
|
const wrapup = { ...emptyWrapup(), ...(meeting.wrapup ?? {}) };
|
|
@@ -192,7 +211,7 @@ export async function runWrapup(meeting, dir, config, dependencies) {
|
|
|
192
211
|
.filter(isRecord)
|
|
193
212
|
.filter((project) => project['active'] !== false)
|
|
194
213
|
.flatMap((project) => (typeof project['name'] === 'string' ? [project['name']] : []));
|
|
195
|
-
const existingPageId = wrapup.pageId ??
|
|
214
|
+
const existingPageId = wrapup.pageId ?? (await entryPages(docs, entryId, true))[0]?.pageId;
|
|
196
215
|
const existingPage = existingPageId === undefined ? null : await pageText(docs, existingPageId);
|
|
197
216
|
const plan = await planWrapup(meeting, dir, snapshot, projects, existingPage, claude);
|
|
198
217
|
writeAtomic(path.join(dir, 'wrapup.json'), encodeWrapupPlan(plan));
|
|
@@ -209,23 +228,23 @@ export async function runWrapup(meeting, dir, config, dependencies) {
|
|
|
209
228
|
await callBita(bita, ['amend', String(entryId), '--project', choice.apply]);
|
|
210
229
|
wrapup.project = choice.apply ?? snapshot.projectName;
|
|
211
230
|
wrapup.projectResolved = choice.resolved;
|
|
212
|
-
const pageFile = path.join(dir, '
|
|
231
|
+
const pageFile = path.join(dir, 'wrapup-page.md');
|
|
213
232
|
if (existingPageId !== undefined) {
|
|
214
233
|
writeAtomic(pageFile, demoteHeadings(plan.pageMarkdown));
|
|
215
|
-
await
|
|
234
|
+
await callInkwell(docs, ['page', 'write', String(existingPageId), '--md', pageFile, '--section', `Reunión ${meetingDay(meeting)}`]);
|
|
216
235
|
wrapup.pageId = existingPageId;
|
|
217
236
|
}
|
|
218
237
|
else {
|
|
219
238
|
writeAtomic(pageFile, plan.pageMarkdown);
|
|
220
|
-
const args = ['
|
|
239
|
+
const args = ['page', 'new', plan.pageTitle, '--from-entry', String(entryId)];
|
|
221
240
|
if (wrapup.project)
|
|
222
241
|
args.push('--project', wrapup.project);
|
|
223
|
-
const created = await
|
|
242
|
+
const created = await callInkwell(docs, args);
|
|
224
243
|
const page = isRecord(created) && isRecord(created['page']) ? created['page'] : undefined;
|
|
225
244
|
const pageId = typeof page?.['pageId'] === 'number' ? page['pageId'] : undefined;
|
|
226
245
|
if (pageId === undefined)
|
|
227
|
-
throw new RecapError('
|
|
228
|
-
await
|
|
246
|
+
throw new RecapError('INKWELL_FAILED', 'inkwell page new did not return the page id');
|
|
247
|
+
await callInkwell(docs, ['page', 'write', String(pageId), '--md', pageFile]);
|
|
229
248
|
wrapup.pageId = pageId;
|
|
230
249
|
wrapup.pageCreated = true;
|
|
231
250
|
}
|
|
@@ -237,17 +256,17 @@ export async function runWrapup(meeting, dir, config, dependencies) {
|
|
|
237
256
|
continue;
|
|
238
257
|
const args = ['backlog', 'add', '--kind', item.kind, '--title', item.title, '--page', String(wrapup.pageId ?? 0)];
|
|
239
258
|
if (item.body) {
|
|
240
|
-
const bodyFile = path.join(dir, '
|
|
259
|
+
const bodyFile = path.join(dir, 'wrapup-backlog-item.md');
|
|
241
260
|
writeAtomic(bodyFile, item.body);
|
|
242
261
|
args.push('--md', bodyFile);
|
|
243
262
|
}
|
|
244
|
-
const added = await
|
|
263
|
+
const added = await callInkwell(docs, args);
|
|
245
264
|
wrapup.backlogKeys[item.title] = isRecord(added) && typeof added['key'] === 'string' ? added['key'] : '?';
|
|
246
265
|
updateMeeting(dir, (current) => {
|
|
247
266
|
current.wrapup = { ...wrapup, backlogKeys: { ...wrapup.backlogKeys } };
|
|
248
267
|
});
|
|
249
268
|
}
|
|
250
|
-
await saveMinutes(meeting, dir,
|
|
269
|
+
await saveMinutes(meeting, dir, dependencies?.notes, dependencies?.log);
|
|
251
270
|
updateMeeting(dir, (current) => {
|
|
252
271
|
current.wrapup = { ...wrapup, backlogKeys: { ...wrapup.backlogKeys } };
|
|
253
272
|
});
|
package/dist/cli/commands/ask.js
CHANGED
|
@@ -5,10 +5,9 @@ import { MeetingStore, loadMeeting, resolveTarget } from "../../core/meeting.js"
|
|
|
5
5
|
import { signalNumber, signalTree } from "../../core/proc.js";
|
|
6
6
|
import { trimmed } from "../../core/text.js";
|
|
7
7
|
import { RecapError, asRecapError, usageError } from "../../errors.js";
|
|
8
|
-
import { BitaClient, docsClient, meetingTarget } from "../../bita/client.js";
|
|
9
8
|
import { runAsk } from "../../live/ask.js";
|
|
10
9
|
import { isValidAskId, makeAskId, performAsk, removeBoard } from "../../live/asking.js";
|
|
11
|
-
import { loadProjectContext,
|
|
10
|
+
import { contextSources, loadProjectContext, meetingContext, meetingProjectName } from "../../live/context.js";
|
|
12
11
|
import { jsonLine } from "../../live/files.js";
|
|
13
12
|
import { encodeAnswer, encodeAskEvent } from "../../live/stream.js";
|
|
14
13
|
import { parseArgs } from "../args.js";
|
|
@@ -77,8 +76,7 @@ async function answer(args, emit) {
|
|
|
77
76
|
let childPid = null;
|
|
78
77
|
installCleanup(found.dir, id, () => childPid);
|
|
79
78
|
return performAsk({ dir: found.dir, id, question, auto: args.has('auto'), now, questionMs, channel }, async () => {
|
|
80
|
-
const
|
|
81
|
-
const context = await loadProjectContext(await meetingProject(found.meeting, docs), found.meeting.bitaDocsRoot, docs);
|
|
79
|
+
const context = await meetingContext(found.meeting, config);
|
|
82
80
|
return runAsk(config, { meeting: found.meeting, dir: found.dir, question, windowSeconds: args.int('window') ?? 180, now, auto: args.has('auto'), questionMs, channel, askId: id }, context, emit, (pid) => {
|
|
83
81
|
childPid = pid;
|
|
84
82
|
});
|
|
@@ -105,9 +103,9 @@ export async function askCommand(argv) {
|
|
|
105
103
|
if (args.has('sources')) {
|
|
106
104
|
const config = loadConfig();
|
|
107
105
|
const found = args.has('active') || args.value('meeting') !== undefined || args.value('bita-entry') !== undefined ? target(args, config) : null;
|
|
108
|
-
const
|
|
106
|
+
const sources = await contextSources(config, found?.meeting ?? null);
|
|
109
107
|
const name = args.value('project') ?? (found ? meetingProjectName(found.meeting) : null);
|
|
110
|
-
const context = await loadProjectContext(name,
|
|
108
|
+
const context = await loadProjectContext(name, sources);
|
|
111
109
|
const lines = [`Project: ${context.project ?? '(none)'}`, `Docs: ${context.docsRoot ?? '(unknown)'}`, ...context.repos.map((repo) => `Repo: ${repo.slug} ${repo.path}${repo.exists ? '' : ' (missing)'}`)];
|
|
112
110
|
return { data: { project: context.project ?? undefined, docsRoot: context.docsRoot ?? undefined, repos: context.repos }, text: lines.join('\n') };
|
|
113
111
|
}
|
|
@@ -62,6 +62,21 @@ export async function handleHookEvent(event) {
|
|
|
62
62
|
log(`discarded ${record.dir}`);
|
|
63
63
|
break;
|
|
64
64
|
}
|
|
65
|
+
case 'refresh': {
|
|
66
|
+
const found = new MeetingStore(loadConfig()).find(event.entry.id);
|
|
67
|
+
if (!found) {
|
|
68
|
+
log(`nothing to do: no meeting for entry #${event.entry.id}`);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
updateMeeting(found.dir, (meeting) => {
|
|
72
|
+
meeting.bitaEntry = { ...eventSnapshot(event), pageIds: meeting.bitaEntry?.pageIds ?? [] };
|
|
73
|
+
const renamed = event.previousTitle !== undefined && event.previousTitle !== event.entry.description;
|
|
74
|
+
if (renamed && event.entry.description.trim().length > 0)
|
|
75
|
+
meeting.title = event.entry.description;
|
|
76
|
+
});
|
|
77
|
+
log(`updated ${found.dir}`);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
65
80
|
case 'ignore':
|
|
66
81
|
log(`nothing to do: ${action.reason}`);
|
|
67
82
|
break;
|
|
@@ -3,7 +3,7 @@ import { expandTilde } from "../../core/paths.js";
|
|
|
3
3
|
import { readText } from "../../core/fsutil.js";
|
|
4
4
|
import { MeetingStore, resolveTarget } from "../../core/meeting.js";
|
|
5
5
|
import { RecapError, usageError } from "../../errors.js";
|
|
6
|
-
import {
|
|
6
|
+
import { INKWELL_CAPABILITIES, INKWELL_HINT, findInkwell, requireInkwell } from "../../bita/client.js";
|
|
7
7
|
import { ProposalReview, loadProposals, proposalJson } from "../../bita/proposals.js";
|
|
8
8
|
import { parseArgs } from "../args.js";
|
|
9
9
|
import { output } from "../output.js";
|
|
@@ -21,13 +21,11 @@ function number(args) {
|
|
|
21
21
|
throw new RecapError('PROPOSAL_REQUIRED', 'Pass the proposal number');
|
|
22
22
|
return Number(raw);
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const noInkwell = { invoke: async () => ({ ok: false, errorCode: 'DEPENDENCY_MISSING', errorMessage: `inkwell not found (${INKWELL_HINT})` }) };
|
|
25
25
|
async function review(args, config, required) {
|
|
26
26
|
const found = meeting(args, config);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
throw new RecapError('DEPENDENCY_MISSING', 'bita not found. Install it with `npm install -g @kikedealba/bita`');
|
|
30
|
-
return new ProposalReview(found.dir, docs ?? noBita);
|
|
27
|
+
const inkwell = required ? await requireInkwell(INKWELL_CAPABILITIES.proposals, 'Reviewing proposals') : await findInkwell(INKWELL_CAPABILITIES.proposals);
|
|
28
|
+
return new ProposalReview(found.dir, inkwell ?? noInkwell);
|
|
31
29
|
}
|
|
32
30
|
export async function proposalsCommand(argv) {
|
|
33
31
|
const [sub, ...rest] = argv;
|
|
@@ -10,6 +10,7 @@ import { processInBackground } from "../../core/self.js";
|
|
|
10
10
|
import { formatDuration, padEnd } from "../../core/text.js";
|
|
11
11
|
import { RecapError, usageError } from "../../errors.js";
|
|
12
12
|
import { saveMinutes } from "../../bita/wrapup.js";
|
|
13
|
+
import { INKWELL_HINT } from "../../bita/client.js";
|
|
13
14
|
import { Pipeline } from "../../pipeline/pipeline.js";
|
|
14
15
|
import { STAGES, allStagesSatisfied, isStage } from "../../pipeline/stages.js";
|
|
15
16
|
import { saveSummary, summaryPromptFor } from "../../pipeline/summary.js";
|
|
@@ -106,14 +107,17 @@ export async function saveSummaryCommand(argv) {
|
|
|
106
107
|
throw new RecapError('FILE_UNREADABLE', `Cannot read ${file}: ${error.message}`);
|
|
107
108
|
}
|
|
108
109
|
saveSummary(body, meeting, dir);
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
let minutes = null;
|
|
111
|
+
if (meeting.bitaEntryId !== undefined) {
|
|
112
|
+
const result = await saveMinutes(meeting, dir);
|
|
113
|
+
minutes = result.saved ? `Minutes saved in the note of entry #${meeting.bitaEntryId}` : `Minutes not saved in the entry note: ${result.reason ?? 'no reason given'} (${INKWELL_HINT})`;
|
|
114
|
+
}
|
|
111
115
|
const updated = updateMeeting(dir, (current) => {
|
|
112
116
|
current.stages['summarize'] = { status: 'done', updatedAt: isoNow() };
|
|
113
117
|
if (allStagesSatisfied({ ...meeting, stages: current.stages }))
|
|
114
118
|
current.status = 'processed';
|
|
115
119
|
});
|
|
116
|
-
return { data: meetingRecord(updated, dir), text: path.join(dir, 'summary.md') };
|
|
120
|
+
return { data: meetingRecord(updated, dir), text: minutes === null ? path.join(dir, 'summary.md') : `${path.join(dir, 'summary.md')}\n${minutes}` };
|
|
117
121
|
});
|
|
118
122
|
}
|
|
119
123
|
export function failedStage(meeting) {
|
|
@@ -131,6 +135,10 @@ export function describeWait(meeting, dir) {
|
|
|
131
135
|
const failed = failedStage(meeting);
|
|
132
136
|
if (failed)
|
|
133
137
|
lines.push(`Failed at ${failed[0]}: ${failed[1].error ?? 'no reason given'}`);
|
|
138
|
+
for (const [stage, state] of Object.entries(meeting.stages)) {
|
|
139
|
+
if (state.status === 'skipped' && state.reason !== undefined)
|
|
140
|
+
lines.push(`Skipped ${stage}: ${state.reason}${state.hint !== undefined ? ` (${state.hint})` : ''}`);
|
|
141
|
+
}
|
|
134
142
|
const wrapup = meeting.wrapup;
|
|
135
143
|
if (wrapup) {
|
|
136
144
|
lines.push(`Title : ${wrapup.title ?? meeting.title}`);
|
|
@@ -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
|
+
}
|
package/dist/cli/router.js
CHANGED
|
@@ -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
|
'',
|
package/dist/live/context.js
CHANGED
|
@@ -2,6 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import { isDirectory } from "../core/fsutil.js";
|
|
3
3
|
import { isRecord } from "../core/json.js";
|
|
4
4
|
import { trimmed } from "../core/text.js";
|
|
5
|
+
import { BitaClient, INKWELL_CAPABILITIES, findInkwell, meetingTarget } from "../bita/client.js";
|
|
6
|
+
import { RecapError } from "../errors.js";
|
|
5
7
|
export function emptyContext(overrides = {}) {
|
|
6
8
|
return { project: null, docsRoot: null, pages: [], repos: [], ...overrides };
|
|
7
9
|
}
|
|
@@ -24,6 +26,7 @@ export function parsePages(data) {
|
|
|
24
26
|
title,
|
|
25
27
|
relPath: typeof item['relPath'] === 'string' ? item['relPath'] : '',
|
|
26
28
|
depth: typeof item['depth'] === 'number' && Number.isInteger(item['depth']) ? item['depth'] : depth,
|
|
29
|
+
...(typeof item['projectName'] === 'string' ? { projectName: item['projectName'] } : {}),
|
|
27
30
|
});
|
|
28
31
|
if (Array.isArray(item['children']))
|
|
29
32
|
walk(item['children'], depth + 1);
|
|
@@ -52,39 +55,63 @@ export function parseRepos(data) {
|
|
|
52
55
|
function metaRoot(meta) {
|
|
53
56
|
return typeof meta?.['root'] === 'string' ? meta['root'] : null;
|
|
54
57
|
}
|
|
55
|
-
export async function loadProjectContext(project,
|
|
56
|
-
const context = emptyContext({ project: project && trimmed(project).length > 0 ? project : null
|
|
57
|
-
|
|
58
|
-
return context;
|
|
58
|
+
export async function loadProjectContext(project, sources) {
|
|
59
|
+
const context = emptyContext({ project: project && trimmed(project).length > 0 ? project : null });
|
|
60
|
+
const { inkwell, bita } = sources;
|
|
59
61
|
if (context.project) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (inkwell) {
|
|
63
|
+
const pages = await inkwell.invoke(['page', 'ls', '--project', context.project]).catch(() => null);
|
|
64
|
+
if (pages?.ok) {
|
|
65
|
+
context.pages = parsePages(pages.data);
|
|
66
|
+
context.docsRoot = metaRoot(pages.meta);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (bita) {
|
|
70
|
+
const repos = await bita.invoke(['project', 'repo', 'ls', '--project', context.project]).catch(() => null);
|
|
71
|
+
if (repos?.ok)
|
|
72
|
+
context.repos = parseRepos(repos.data);
|
|
64
73
|
}
|
|
65
|
-
const repos = await bita.invoke(['project', 'repo', 'ls', '--project', context.project]).catch(() => null);
|
|
66
|
-
if (repos?.ok)
|
|
67
|
-
context.repos = parseRepos(repos.data);
|
|
68
74
|
}
|
|
69
|
-
if (context.docsRoot === null) {
|
|
70
|
-
const tree = await
|
|
75
|
+
if (context.docsRoot === null && inkwell) {
|
|
76
|
+
const tree = await inkwell.invoke(['tree']).catch(() => null);
|
|
71
77
|
if (tree?.ok)
|
|
72
78
|
context.docsRoot = metaRoot(tree.meta);
|
|
73
79
|
}
|
|
74
80
|
return context;
|
|
75
81
|
}
|
|
82
|
+
export async function entryPages(inkwell, entryId, strict = false) {
|
|
83
|
+
if (!inkwell || entryId === undefined)
|
|
84
|
+
return [];
|
|
85
|
+
const response = await inkwell.invoke(['page', 'ls', '--entry', String(entryId)]).catch(() => null);
|
|
86
|
+
if (response?.ok)
|
|
87
|
+
return parsePages(response.data);
|
|
88
|
+
if (strict)
|
|
89
|
+
throw new RecapError('INKWELL_FAILED', `inkwell page ls --entry ${entryId} failed: ${response?.errorMessage ?? 'no reason given'}`);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
76
92
|
export function meetingProjectName(meeting) {
|
|
77
93
|
return meeting.bitaEntry?.projectName ?? meeting.wrapup?.project ?? null;
|
|
78
94
|
}
|
|
79
|
-
export async function meetingProject(meeting,
|
|
95
|
+
export async function meetingProject(meeting, inkwell, linked) {
|
|
80
96
|
const name = meetingProjectName(meeting);
|
|
81
97
|
if (name && trimmed(name).length > 0)
|
|
82
98
|
return name;
|
|
83
|
-
|
|
84
|
-
if (!bita || pageId === undefined)
|
|
85
|
-
return null;
|
|
86
|
-
const response = await bita.invoke(['docs', 'page', 'show', String(pageId), '--no-markdown']).catch(() => null);
|
|
87
|
-
if (!response?.ok || !isRecord(response.data))
|
|
99
|
+
if (!inkwell)
|
|
88
100
|
return null;
|
|
89
|
-
|
|
101
|
+
const pageId = meeting.wrapup?.pageId;
|
|
102
|
+
if (pageId !== undefined) {
|
|
103
|
+
const response = await inkwell.invoke(['page', 'show', String(pageId), '--no-markdown']).catch(() => null);
|
|
104
|
+
if (response?.ok && isRecord(response.data) && typeof response.data['projectName'] === 'string')
|
|
105
|
+
return response.data['projectName'];
|
|
106
|
+
}
|
|
107
|
+
const pages = linked ?? (await entryPages(inkwell, meeting.bitaEntryId));
|
|
108
|
+
return pages.find((page) => page.projectName !== undefined)?.projectName ?? null;
|
|
109
|
+
}
|
|
110
|
+
export async function contextSources(config, meeting) {
|
|
111
|
+
const [inkwell, bita] = await Promise.all([findInkwell(INKWELL_CAPABILITIES.pages), BitaClient.create(config, meeting ? meetingTarget(meeting) : {})]);
|
|
112
|
+
return { inkwell, bita };
|
|
113
|
+
}
|
|
114
|
+
export async function meetingContext(meeting, config) {
|
|
115
|
+
const sources = await contextSources(config, meeting);
|
|
116
|
+
return loadProjectContext(await meetingProject(meeting, sources.inkwell), sources);
|
|
90
117
|
}
|
package/dist/live/worker.js
CHANGED
|
@@ -10,12 +10,11 @@ import { selfCommand } from "../core/self.js";
|
|
|
10
10
|
import { suffix, trimmed } from "../core/text.js";
|
|
11
11
|
import { requireTool } from "../core/tools.js";
|
|
12
12
|
import { RecapError, errorMessage } from "../errors.js";
|
|
13
|
-
import { docsClient, meetingTarget } from "../bita/client.js";
|
|
14
13
|
import { resultText, runClaude } from "../pipeline/claude.js";
|
|
15
14
|
import { whisperArguments } from "../pipeline/pipeline.js";
|
|
16
15
|
import { parseWhisper } from "../pipeline/transcript.js";
|
|
17
16
|
import { pruneBoard } from "./asking.js";
|
|
18
|
-
import {
|
|
17
|
+
import { meetingContext } from "./context.js";
|
|
19
18
|
import { AutoAskProcess, AutoAskQueue, QuestionDetector } from "./detector.js";
|
|
20
19
|
import { appendLines, liveFiles, readLines } from "./files.js";
|
|
21
20
|
import { LiveMerger } from "./merger.js";
|
|
@@ -177,10 +176,7 @@ export function makeLiveDetector(dir, meeting, config, log) {
|
|
|
177
176
|
const runner = new AutoAskProcess(dir, selfCommand());
|
|
178
177
|
const asks = new AutoAskQueue({ dir, concurrency: settings.autoAskConcurrency, run: (job) => runner.run(job), cancelRunning: () => runner.cancel(), log });
|
|
179
178
|
return new QuestionDetector(dir, meeting, settings.autoAskMinSeconds, {
|
|
180
|
-
context:
|
|
181
|
-
const docs = await docsClient(config, meetingTarget(meeting));
|
|
182
|
-
return loadProjectContext(await meetingProject(meeting, docs), meeting.bitaDocsRoot, docs);
|
|
183
|
-
},
|
|
179
|
+
context: () => meetingContext(meeting, config),
|
|
184
180
|
complete: async (prompt) => resultText(await runClaude({ prompt, cwd: dir, config, tools: [], addDirs: [], model: settings.autoAskModel, restrictTools: [] })),
|
|
185
181
|
enqueue: (question, origin) => {
|
|
186
182
|
asks.enqueue(question, origin);
|
|
@@ -11,7 +11,7 @@ import { exec } from "../core/proc.js";
|
|
|
11
11
|
import { suffix, trimmed } from "../core/text.js";
|
|
12
12
|
import { requireTool } from "../core/tools.js";
|
|
13
13
|
import { RecapError, errorMessage } from "../errors.js";
|
|
14
|
-
import {
|
|
14
|
+
import { INKWELL_CAPABILITIES, StageSkipped, inkwellForStage } from "../bita/client.js";
|
|
15
15
|
import { ProposalGenerator } from "../bita/proposals.js";
|
|
16
16
|
import { runWrapup } from "../bita/wrapup.js";
|
|
17
17
|
import { runSummaryClaude } from "./claude.js";
|
|
@@ -78,9 +78,9 @@ export class Pipeline {
|
|
|
78
78
|
recording(meeting) {
|
|
79
79
|
return recordingPath(this.dir, meeting.mode);
|
|
80
80
|
}
|
|
81
|
-
setStage(stage, status, error, extra) {
|
|
81
|
+
setStage(stage, status, error, extra, details = {}) {
|
|
82
82
|
return updateMeeting(this.dir, (current) => {
|
|
83
|
-
current.stages[stage] = { status, updatedAt: isoNow(), ...(error !== undefined ? { error } : {}) };
|
|
83
|
+
current.stages[stage] = { status, updatedAt: isoNow(), ...(error !== undefined ? { error } : {}), ...details };
|
|
84
84
|
extra?.(current);
|
|
85
85
|
});
|
|
86
86
|
}
|
|
@@ -131,6 +131,11 @@ export class Pipeline {
|
|
|
131
131
|
}
|
|
132
132
|
catch (error) {
|
|
133
133
|
const message = error instanceof RecapError ? error.message : errorMessage(error);
|
|
134
|
+
if (error instanceof StageSkipped) {
|
|
135
|
+
meeting = this.setStage(stage, 'skipped', undefined, undefined, { reason: message, ...(error.hint !== undefined ? { hint: error.hint } : {}) });
|
|
136
|
+
this.log(`${stage}: skipped, ${message}${error.hint !== undefined ? ` (${error.hint})` : ''}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
134
139
|
if (stageIsOptional(stage)) {
|
|
135
140
|
meeting = this.setStage(stage, 'failed', message);
|
|
136
141
|
this.log(`${stage}: failed, continuing: ${message}`);
|
|
@@ -165,7 +170,7 @@ export class Pipeline {
|
|
|
165
170
|
case 'proposals':
|
|
166
171
|
return this.proposals(meeting);
|
|
167
172
|
case 'wrapup':
|
|
168
|
-
return runWrapup(meeting, this.dir, this.config);
|
|
173
|
+
return runWrapup(meeting, this.dir, this.config, { log: this.log });
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
async extractAudio(meeting) {
|
|
@@ -268,8 +273,7 @@ export class Pipeline {
|
|
|
268
273
|
saveSummary(extractSummary(output), meeting, this.dir);
|
|
269
274
|
}
|
|
270
275
|
async proposals(meeting) {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
await new ProposalGenerator(this.config, docs, this.log).run(meeting, this.dir);
|
|
276
|
+
const inkwell = await inkwellForStage(INKWELL_CAPABILITIES.proposals);
|
|
277
|
+
await new ProposalGenerator(this.config, inkwell, this.log).run(meeting, this.dir);
|
|
274
278
|
}
|
|
275
279
|
}
|