scribeclock 0.0.0-stage → 0.1.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.
@@ -0,0 +1,254 @@
1
+ import { usageError } from "../cli/errors.js";
2
+ import { confirm } from "../cli/confirm.js";
3
+ import { fmtDate, parseWhen } from "../util/format.js";
4
+ import { truncate } from "../cli/output.js";
5
+ import { PROJECT_FLAG, cmd, emit, num, projectArg, requirePositional, resolveProjectId, str } from "./_shared.js";
6
+ export const meetingsList = cmd({
7
+ path: "meetings list",
8
+ group: "Meetings & docs",
9
+ summary: "Recorded meetings with status, project and whether minutes / a transcript exist.",
10
+ tool: "list_meetings",
11
+ positionals: [{ name: "query", description: "Text over title, summary and minutes." }],
12
+ flags: [
13
+ PROJECT_FLAG,
14
+ { name: "status", alias: "s", type: "string", choices: ["scheduled", "live", "processing", "completed", "failed"], description: "Only this status." },
15
+ { name: "since", type: "string", placeholder: "<when>", description: "Started at or after (ISO, YYYY-MM-DD, yesterday, 7d…)." },
16
+ { name: "until", type: "string", placeholder: "<when>", description: "Started before." },
17
+ { name: "limit", alias: "n", type: "number", description: "Max meetings (default 25)." },
18
+ ],
19
+ examples: ["scribeclock meetings list --since 7d", "scribeclock meetings list 'retirement' --status completed"],
20
+ async run(ctx, inv) {
21
+ const when = (v) => (typeof v === "string" ? parseWhen(v, ctx.now())?.toISOString() : undefined);
22
+ const data = await ctx.call("list_meetings", { query: inv.positionals.join(" ") || undefined, project_id: projectArg(ctx, inv), status: str(inv.flags.status), since: when(inv.flags.since), until: when(inv.flags.until), limit: num(inv.flags.limit) });
23
+ return emit(ctx, data, () => {
24
+ const meetings = (data.meetings ?? []);
25
+ if (!meetings.length)
26
+ ctx.out.log(ctx.out.dim("No meetings."));
27
+ else
28
+ ctx.out.table(["id", "title", "status", "started", "minutes", "transcript", "project"], meetings.map((m) => [m.id, truncate(String(m.title ?? ""), 48), m.status, fmtDate(String(m.started_at ?? ""), { time: true }), m.has_minutes ? "yes" : "", Number(m.segment_count ?? 0) ? `${String(m.segment_count)} seg` : "", m.project?.name ?? m.project_name ?? ""]), { maxWidth: 48 });
29
+ });
30
+ },
31
+ });
32
+ export const meetingsShow = cmd({
33
+ path: "meetings show",
34
+ group: "Meetings & docs",
35
+ summary: "Summary, AI notes and (with --minutes) the full minutes of a meeting.",
36
+ tool: "get_meeting",
37
+ positionals: [{ name: "id", required: true, description: "Meeting id from meetings list." }],
38
+ flags: [{ name: "minutes", type: "boolean", description: "Include the complete formatted minutes." }],
39
+ async run(ctx, inv) {
40
+ const meeting_id = requirePositional(inv, 0, "A meeting id", "scribeclock meetings show <id>");
41
+ const data = await ctx.call("get_meeting", { meeting_id, include_minutes: Boolean(inv.flags.minutes) });
42
+ return emit(ctx, data, () => renderMeeting(ctx, data));
43
+ },
44
+ });
45
+ function renderMeeting(ctx, data) {
46
+ const m = (data.meeting ?? data);
47
+ ctx.out.log(ctx.out.bold(String(m.title ?? "Meeting")));
48
+ ctx.out.kv([
49
+ ["status", m.status],
50
+ ["started", m.started_at ? fmtDate(String(m.started_at), { time: true }) : ""],
51
+ ["ended", m.ended_at ? fmtDate(String(m.ended_at), { time: true }) : ""],
52
+ ["project", m.project?.name ?? m.project_name],
53
+ ["recorded by", m.recorded_by?.name ?? m.recorded_by],
54
+ ["segments", m.segment_count],
55
+ ["url", m.url ?? `https://scribeclock.com/dashboard/meetings/${String(m.id ?? "")}`],
56
+ ]);
57
+ const summary = m.summary ?? data.summary;
58
+ if (summary) {
59
+ ctx.out.log("");
60
+ ctx.out.log(ctx.out.bold("Summary"));
61
+ ctx.out.log(` ${String(summary)}`);
62
+ }
63
+ const notes = (m.ai_notes ?? data.ai_notes);
64
+ if (notes && typeof notes === "object") {
65
+ for (const [k, v] of Object.entries(notes)) {
66
+ if (!v || (Array.isArray(v) && !v.length))
67
+ continue;
68
+ ctx.out.log("");
69
+ ctx.out.log(ctx.out.bold(k.replace(/_/g, " ")));
70
+ if (Array.isArray(v))
71
+ for (const item of v)
72
+ ctx.out.log(` - ${typeof item === "string" ? item : JSON.stringify(item)}`);
73
+ else
74
+ ctx.out.log(` ${String(v)}`);
75
+ }
76
+ }
77
+ else if (typeof notes === "string" && notes) {
78
+ ctx.out.log("");
79
+ ctx.out.log(ctx.out.bold("AI notes"));
80
+ ctx.out.log(notes);
81
+ }
82
+ const minutes = m.minutes ?? data.minutes ?? m.minutes_markdown ?? data.minutes_markdown;
83
+ if (typeof minutes === "string" && minutes) {
84
+ ctx.out.log("");
85
+ ctx.out.log(ctx.out.bold("Minutes"));
86
+ ctx.out.log(minutes);
87
+ }
88
+ }
89
+ export const meetingsTranscript = cmd({
90
+ path: "meetings transcript",
91
+ group: "Meetings & docs",
92
+ summary: "The spoken transcript ([m:ss] Speaker: text), all pages; --summarize prints the AI summary instead.",
93
+ tool: ["get_meeting_transcript", "get_meeting"],
94
+ positionals: [{ name: "id", required: true, description: "Meeting id." }],
95
+ flags: [
96
+ { name: "summarize", type: "boolean", description: "Print the meeting's AI summary and notes instead of the raw words." },
97
+ { name: "from", type: "number", description: "Start at this segment (default 0)." },
98
+ { name: "limit", alias: "n", type: "number", description: "Segments per page (default 400); all pages are fetched unless --one-page." },
99
+ { name: "one-page", type: "boolean", description: "Fetch a single page only." },
100
+ { name: "segments", type: "boolean", description: "Raw segments (JSON objects) instead of text lines." },
101
+ ],
102
+ examples: ["scribeclock meetings transcript <id> > transcript.txt", "scribeclock meetings transcript <id> --summarize"],
103
+ async run(ctx, inv) {
104
+ const meeting_id = requirePositional(inv, 0, "A meeting id", "scribeclock meetings transcript <id>");
105
+ if (inv.flags.summarize) {
106
+ const data = await ctx.call("get_meeting", { meeting_id, include_minutes: false });
107
+ return emit(ctx, data, () => renderMeeting(ctx, data));
108
+ }
109
+ const format = inv.flags.segments ? "segments" : "text";
110
+ let from_seq = num(inv.flags.from) ?? 0;
111
+ const pages = [];
112
+ const texts = [];
113
+ const segments = [];
114
+ for (let guard = 0; guard < 200; guard++) {
115
+ const page = await ctx.call("get_meeting_transcript", { meeting_id, from_seq, limit: num(inv.flags.limit), format });
116
+ pages.push(page);
117
+ if (typeof page.transcript === "string")
118
+ texts.push(page.transcript);
119
+ if (Array.isArray(page.segments))
120
+ segments.push(...page.segments);
121
+ const next = page.next_from_seq;
122
+ if (inv.flags["one-page"] || next === null || next === undefined || Number(next) <= from_seq)
123
+ break;
124
+ from_seq = Number(next);
125
+ }
126
+ if (ctx.flags.json) {
127
+ ctx.out.jsonOut({ ok: true, meeting_id, pages: pages.length, ...(format === "segments" ? { segments } : { transcript: texts.join("\n") }) });
128
+ return 0;
129
+ }
130
+ if (format === "segments")
131
+ for (const s of segments)
132
+ ctx.out.ndjson(s);
133
+ else
134
+ ctx.out.log(texts.join("\n"));
135
+ return 0;
136
+ },
137
+ });
138
+ /** Action-item extraction from AI notes / minutes / summary: structured first, then line heuristics. */
139
+ export function extractActionItems(meeting) {
140
+ const items = [];
141
+ const push = (raw, source) => {
142
+ if (!raw)
143
+ return;
144
+ if (typeof raw === "object" && !Array.isArray(raw)) {
145
+ const o = raw;
146
+ const title = String(o.title ?? o.text ?? o.action ?? o.item ?? "").trim();
147
+ if (title)
148
+ items.push({ title: title.slice(0, 140), owner: typeof o.owner === "string" ? o.owner : typeof o.assignee === "string" ? o.assignee : undefined, source });
149
+ return;
150
+ }
151
+ const text = String(raw).replace(/^[-*•]\s*(\[[ xX]?\]\s*)?/, "").trim();
152
+ if (text.length < 6)
153
+ return;
154
+ const owner = /^\*{0,2}([A-Z][\w.'-]+(?: [A-Z][\w.'-]+)?)\*{0,2}\s*(?::|will|to)\s/.exec(text)?.[1];
155
+ items.push({ title: text.replace(/\*\*/g, "").slice(0, 140), owner, source });
156
+ };
157
+ const notes = (meeting.ai_notes ?? {});
158
+ for (const key of ["action_items", "actions", "next_steps", "tasks", "follow_ups"]) {
159
+ const v = notes[key] ?? meeting[key];
160
+ if (Array.isArray(v))
161
+ for (const item of v)
162
+ push(item, `ai_notes.${key}`);
163
+ }
164
+ if (!items.length) {
165
+ const text = [meeting.minutes, meeting.minutes_markdown, meeting.summary].filter((x) => typeof x === "string").join("\n");
166
+ let inActions = false;
167
+ for (const line of text.split("\n")) {
168
+ if (/^#{1,6}\s*(action items?|next steps|follow[- ]?ups?|to[- ]?dos?)\b/i.test(line) || /^\*\*(action items?|next steps)\*\*/i.test(line)) {
169
+ inActions = true;
170
+ continue;
171
+ }
172
+ if (/^#{1,6}\s/.test(line))
173
+ inActions = false;
174
+ if ((inActions && /^\s*([-*•]|\d+\.)\s+\S/.test(line)) || /^\s*[-*]\s+\[ \]\s+/.test(line) || /\b(action item|TODO)\b/i.test(line))
175
+ push(line.replace(/^\s*\d+\.\s*/, ""), "minutes");
176
+ }
177
+ }
178
+ const seen = new Set();
179
+ return items.filter((i) => {
180
+ const k = i.title.toLowerCase();
181
+ if (seen.has(k))
182
+ return false;
183
+ seen.add(k);
184
+ return true;
185
+ });
186
+ }
187
+ export const meetingsActions = cmd({
188
+ path: "meetings actions",
189
+ group: "Meetings & docs",
190
+ summary: "Action items from a meeting's notes; --create-tasks files them as tasks after you confirm.",
191
+ tool: ["get_meeting", "create_task"],
192
+ positionals: [{ name: "id", required: true, description: "Meeting id." }],
193
+ flags: [
194
+ { name: "create-tasks", type: "boolean", description: "Create a task per action item (asks first; --yes to skip)." },
195
+ PROJECT_FLAG,
196
+ { name: "assignee", alias: "a", type: "string", description: "Assignee for the created tasks (default: the owner named in the item, when it matches a member)." },
197
+ { name: "priority", type: "string", choices: ["low", "medium", "high"], description: "Priority for created tasks." },
198
+ ],
199
+ examples: ["scribeclock meetings actions <id>", "scribeclock meetings actions <id> --create-tasks --project 'Kisa Print'"],
200
+ async run(ctx, inv) {
201
+ const meeting_id = requirePositional(inv, 0, "A meeting id", "scribeclock meetings actions <id>");
202
+ const data = await ctx.call("get_meeting", { meeting_id, include_minutes: true });
203
+ const meeting = (data.meeting ?? data);
204
+ const items = extractActionItems({ ...meeting, ...data });
205
+ if (!inv.flags["create-tasks"]) {
206
+ return emit(ctx, { ok: true, meeting_id, title: meeting.title, count: items.length, action_items: items }, () => {
207
+ ctx.out.log(ctx.out.bold(`Action items from "${String(meeting.title ?? meeting_id)}" (${items.length})`));
208
+ if (!items.length)
209
+ ctx.out.log(ctx.out.dim("None found in the AI notes or minutes."));
210
+ else
211
+ ctx.out.table(["#", "action", "owner", "source"], items.map((it, i) => [i + 1, it.title, it.owner ?? "", it.source]));
212
+ if (items.length)
213
+ ctx.out.info(ctx.out.dim("Add --create-tasks to file them."));
214
+ });
215
+ }
216
+ if (!items.length)
217
+ throw usageError("No action items to create.");
218
+ const project_id = (await resolveProjectId(ctx, projectArg(ctx, inv))) ?? meeting.project?.id;
219
+ if (!ctx.flags.dryRun) {
220
+ ctx.out.info(items.map((it, i) => ` ${i + 1}. ${it.title}${it.owner ? ctx.out.dim(` (${it.owner})`) : ""}`).join("\n"));
221
+ const ok = await confirm(ctx, `Create these ${items.length} tasks${project_id ? ` in project ${project_id}` : ""}?`);
222
+ if (!ok)
223
+ return 1;
224
+ }
225
+ const link = `https://scribeclock.com/dashboard/meetings/${meeting_id}`;
226
+ const created = [];
227
+ for (const it of items) {
228
+ const args = { title: it.title, description: `From the meeting "${String(meeting.title ?? "")}" (${fmtDate(String(meeting.started_at ?? ""))}).\n\nMeeting notes: ${link}`, project_id, priority: str(inv.flags.priority), assignee: str(inv.flags.assignee) ?? it.owner };
229
+ if (ctx.flags.dryRun) {
230
+ ctx.out.log(`${ctx.out.dim("dry run →")} create_task ${JSON.stringify({ title: it.title, project_id, assignee: args.assignee })}`);
231
+ continue;
232
+ }
233
+ let r;
234
+ try {
235
+ r = await ctx.call("create_task", args);
236
+ }
237
+ catch (err) {
238
+ // An owner name that is not a member: retry unassigned rather than losing the item.
239
+ if (args.assignee && !str(inv.flags.assignee))
240
+ r = await ctx.call("create_task", { ...args, assignee: undefined });
241
+ else
242
+ throw err;
243
+ }
244
+ created.push((r.task ?? r));
245
+ ctx.out.info(`${ctx.out.green("✓")} ${String((r.task ?? r).key ?? "")} ${it.title}`);
246
+ }
247
+ if (ctx.flags.json)
248
+ ctx.out.jsonOut({ ok: true, created: created.length, tasks: created });
249
+ else if (!ctx.flags.dryRun)
250
+ ctx.out.log(`Created ${created.length} tasks from the meeting.`);
251
+ return 0;
252
+ },
253
+ });
254
+ export const MEETING_COMMANDS = [meetingsList, meetingsShow, meetingsTranscript, meetingsActions];
@@ -0,0 +1,129 @@
1
+ import { renderCommandHelp, renderGlobalHelp } from "../cli/registry.js";
2
+ import { usageError } from "../cli/errors.js";
3
+ import { EXIT } from "../types.js";
4
+ export const versionCommand = {
5
+ path: "version",
6
+ group: "Getting started",
7
+ summary: "Print the CLI version.",
8
+ hidden: true,
9
+ async run(ctx) {
10
+ if (ctx.flags.json)
11
+ ctx.out.jsonOut({ ok: true, version: ctx.version, endpoint: ctx.endpoint, node: process.version });
12
+ else
13
+ ctx.out.log(`scribeclock ${ctx.version}`);
14
+ return EXIT.OK;
15
+ },
16
+ };
17
+ export const helpCommand = {
18
+ path: "help",
19
+ group: "Getting started",
20
+ summary: "Show help for a command: help tasks list.",
21
+ positionals: [{ name: "command", variadic: true }],
22
+ async run(ctx, inv) {
23
+ if (!inv.positionals.length) {
24
+ renderGlobalHelp(ctx.out, ctx.registry, ctx.version, null);
25
+ return EXIT.OK;
26
+ }
27
+ const resolved = ctx.registry.resolve(inv.positionals);
28
+ if (resolved) {
29
+ renderCommandHelp(ctx.out, resolved.command);
30
+ return EXIT.OK;
31
+ }
32
+ const children = ctx.registry.children(inv.positionals[0]);
33
+ if (children.length) {
34
+ ctx.out.log(ctx.out.bold(`scribeclock ${inv.positionals[0]} <subcommand>`));
35
+ const w = Math.max(...children.map((c) => c.path.length));
36
+ for (const c of children)
37
+ ctx.out.log(` ${ctx.out.cyan(c.path.padEnd(w))} ${c.summary}`);
38
+ return EXIT.OK;
39
+ }
40
+ const { findTool, renderToolHelp } = await import("./tools.js");
41
+ const tool = await findTool(ctx, inv.positionals[0]).catch(() => undefined);
42
+ if (tool) {
43
+ renderToolHelp(ctx, tool);
44
+ return EXIT.OK;
45
+ }
46
+ throw usageError(`No command or tool named "${inv.positionals.join(" ")}".`, "`scribeclock --help` lists commands; `scribeclock tools` lists server tools.");
47
+ },
48
+ };
49
+ const SHELLS = ["bash", "zsh", "fish"];
50
+ export const completionsCommand = {
51
+ path: "completions",
52
+ group: "Getting started",
53
+ summary: "Shell completion script (bash, zsh, fish), including the live tool names.",
54
+ description: "Prints a completion script. Commands come from the CLI, tool names from the cached catalog (log in and run `scribeclock refresh` first so they are included).\n\n bash: scribeclock completions bash > ~/.local/share/bash-completion/completions/scribeclock\n zsh: scribeclock completions zsh > ~/.zfunc/_scribeclock (add ~/.zfunc to fpath)\n fish: scribeclock completions fish > ~/.config/fish/completions/scribeclock.fish",
55
+ positionals: [{ name: "shell", required: true, description: "bash, zsh or fish" }],
56
+ async run(ctx, inv) {
57
+ const shell = inv.positionals[0];
58
+ if (!SHELLS.includes(shell))
59
+ throw usageError(`Unknown shell "${shell}".`, `One of: ${SHELLS.join(", ")}`);
60
+ const commands = ctx.registry.list().filter((c) => !c.hidden);
61
+ const top = Array.from(new Set(commands.map((c) => c.path.split(" ")[0])));
62
+ const subs = new Map();
63
+ for (const c of commands) {
64
+ const [a, b] = c.path.split(" ");
65
+ if (b)
66
+ subs.set(a, [...(subs.get(a) ?? []), b]);
67
+ }
68
+ let toolNames = [];
69
+ try {
70
+ const cached = ctx.catalog.readCache();
71
+ if (cached)
72
+ toolNames = cached.tools.map((t) => t.name);
73
+ else
74
+ toolNames = (await ctx.tools()).map((t) => t.name);
75
+ }
76
+ catch {
77
+ toolNames = [];
78
+ }
79
+ const globals = ["--json", "--org", "--yes", "--dry-run", "--url", "--quiet", "--no-color", "--debug", "--reveal", "--help", "--version"];
80
+ const words = [...top, ...toolNames];
81
+ if (shell === "bash") {
82
+ const cases = [...subs.entries()].map(([k, v]) => ` ${k}) COMPREPLY=( $(compgen -W "${v.join(" ")} ${globals.join(" ")}" -- "$cur") ); return;;`).join("\n");
83
+ ctx.out.log(`# bash completion for scribeclock
84
+ _scribeclock() {
85
+ local cur prev
86
+ COMPREPLY=()
87
+ cur="\${COMP_WORDS[COMP_CWORD]}"
88
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
89
+ if [[ $COMP_CWORD -eq 1 ]]; then
90
+ COMPREPLY=( $(compgen -W "${words.join(" ")} ${globals.join(" ")}" -- "$cur") ); return
91
+ fi
92
+ case "\${COMP_WORDS[1]}" in
93
+ ${cases}
94
+ esac
95
+ COMPREPLY=( $(compgen -W "${globals.join(" ")}" -- "$cur") )
96
+ }
97
+ complete -F _scribeclock scribeclock`);
98
+ }
99
+ else if (shell === "zsh") {
100
+ const subBlocks = [...subs.entries()].map(([k, v]) => ` ${k}) _values 'subcommand' ${v.map((s) => `'${s}'`).join(" ")} ;;`).join("\n");
101
+ ctx.out.log(`#compdef scribeclock
102
+ _scribeclock() {
103
+ local -a cmds
104
+ cmds=(${words.map((w) => `'${w}'`).join(" ")})
105
+ if (( CURRENT == 2 )); then
106
+ _describe 'command' cmds
107
+ return
108
+ fi
109
+ case $words[2] in
110
+ ${subBlocks}
111
+ *) _arguments ${globals.map((g) => `'${g}'`).join(" ")} ;;
112
+ esac
113
+ }
114
+ _scribeclock "$@"`);
115
+ }
116
+ else {
117
+ const lines = [`# fish completion for scribeclock`, `complete -c scribeclock -f`];
118
+ for (const w of words)
119
+ lines.push(`complete -c scribeclock -n "__fish_use_subcommand" -a ${w}`);
120
+ for (const [k, v] of subs)
121
+ for (const s of v)
122
+ lines.push(`complete -c scribeclock -n "__fish_seen_subcommand_from ${k}" -a ${s}`);
123
+ for (const g of globals)
124
+ lines.push(`complete -c scribeclock -l ${g.replace(/^--/, "")}`);
125
+ ctx.out.log(lines.join("\n"));
126
+ }
127
+ return EXIT.OK;
128
+ },
129
+ };