@quillmeetings/cli 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.
- package/LICENSE +21 -0
- package/README.md +367 -0
- package/bin/quill.js +20 -0
- package/package.json +46 -0
- package/src/browser.js +695 -0
- package/src/cli.js +841 -0
- package/src/config.js +132 -0
- package/src/doctor.js +190 -0
- package/src/format.js +275 -0
- package/src/mcp-client.js +326 -0
- package/src/tool-router.js +104 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { runMeetingBrowser } from "./browser.js";
|
|
3
|
+
import { configPath, defaultBridgePath, ensureConfigFile, getConfigValue, loadConfig, readUserConfig, setConfigValue, supportedPlatform, writeUserConfig } from "./config.js";
|
|
4
|
+
import { CLI_VERSION, runDoctorChecks } from "./doctor.js";
|
|
5
|
+
import { extractToolResult, McpClient } from "./mcp-client.js";
|
|
6
|
+
import { printData, structuredError, truncateText, withHelp } from "./format.js";
|
|
7
|
+
import { buildArgs, findTool } from "./tool-router.js";
|
|
8
|
+
|
|
9
|
+
const ACTIONS_PROMPT = "Extract action items from this meeting. Return only concrete tasks. For each item include owner if mentioned, due date if mentioned, status or uncertainty, and brief source context. If there are no clear action items, say so explicitly.";
|
|
10
|
+
const FOLLOWUP_PROMPT = "Draft a concise follow-up note for this meeting. Include a short recap, decisions, open questions, action items, and a friendly next-step section. Keep it practical and ready to send.";
|
|
11
|
+
|
|
12
|
+
export async function runCli(argv) {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const options = parseGlobalOptions(argv, config);
|
|
15
|
+
const args = normalizeCommandArgs(options.args);
|
|
16
|
+
const command = args[0];
|
|
17
|
+
|
|
18
|
+
if (options.help || command === "help") {
|
|
19
|
+
printHelp(command === "help" ? args[1] : args[0]);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (options.version) {
|
|
24
|
+
printData({ version: CLI_VERSION }, options);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (command === "completion") {
|
|
29
|
+
printCompletion(args[1] || "zsh");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (command === "init") {
|
|
34
|
+
await runInit(options, config);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (command === "doctor") {
|
|
39
|
+
await runDoctor(options, config);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!command) {
|
|
44
|
+
await runHome(options, config);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (command === "config") {
|
|
49
|
+
await runConfig(args.slice(1), options, config);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
validateBeforeMcp(args, options);
|
|
54
|
+
|
|
55
|
+
const client = new McpClient(config.mcp);
|
|
56
|
+
await client.connect();
|
|
57
|
+
try {
|
|
58
|
+
if (command === "mcp") {
|
|
59
|
+
await runMcp(client, args.slice(1), options);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await runCurated(client, args, options);
|
|
64
|
+
} finally {
|
|
65
|
+
await client.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runHome(options, config) {
|
|
70
|
+
const bridge = config.mcp.args[0];
|
|
71
|
+
printData(withHelp({
|
|
72
|
+
bin: "quill",
|
|
73
|
+
description: "Browse and search Quill Meetings through the local Quill MCP server",
|
|
74
|
+
mcp_bridge: bridge,
|
|
75
|
+
mcp_bridge_found: existsSync(bridge),
|
|
76
|
+
}, [
|
|
77
|
+
"Run `quill mcp tools` to inspect available Quill MCP tools",
|
|
78
|
+
"Run `quill meetings list --limit 10` to list recent meetings",
|
|
79
|
+
"Run `quill search \"<query>\"` to search meeting content",
|
|
80
|
+
]), options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function runConfig(args, options, config) {
|
|
84
|
+
const command = args[0] || "show";
|
|
85
|
+
if (command === "path") {
|
|
86
|
+
printData({ config_path: configPath() }, options);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (command === "init") {
|
|
90
|
+
const result = ensureConfigFile();
|
|
91
|
+
printData({
|
|
92
|
+
config_path: result.path,
|
|
93
|
+
created: result.created,
|
|
94
|
+
}, options);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (command === "show") {
|
|
98
|
+
printData({
|
|
99
|
+
config_path: configPath(),
|
|
100
|
+
config,
|
|
101
|
+
}, options);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (command === "get") {
|
|
105
|
+
const key = args[1];
|
|
106
|
+
if (!key) throw cliError("missing_config_key", "Usage: quill config get <key>");
|
|
107
|
+
printData({
|
|
108
|
+
key,
|
|
109
|
+
value: getConfigValue(config, key),
|
|
110
|
+
}, options);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (command === "set") {
|
|
114
|
+
const key = args[1];
|
|
115
|
+
const value = args.slice(2).join(" ");
|
|
116
|
+
if (!key || value === "") throw cliError("missing_config_set_args", "Usage: quill config set <key> <value>");
|
|
117
|
+
const userConfig = readUserConfig();
|
|
118
|
+
setConfigValue(userConfig, key, value);
|
|
119
|
+
writeUserConfig(userConfig);
|
|
120
|
+
printData({
|
|
121
|
+
config_path: configPath(),
|
|
122
|
+
key,
|
|
123
|
+
value: getConfigValue(loadConfig(), key),
|
|
124
|
+
}, options);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
throw cliError("unknown_config_command", `Unknown config command: ${command}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function runInit(options, config) {
|
|
131
|
+
const result = ensureConfigFile();
|
|
132
|
+
const doctor = await runDoctorChecks(loadConfig());
|
|
133
|
+
const setup = withHelp({
|
|
134
|
+
config_path: result.path,
|
|
135
|
+
config_created: result.created,
|
|
136
|
+
platform: doctor.platform,
|
|
137
|
+
mcp_bridge: doctor.configured_bridge || defaultBridgePath(),
|
|
138
|
+
mcp_bridge_found: existsSync(doctor.configured_bridge || defaultBridgePath()),
|
|
139
|
+
doctor: doctor.all_good ? "pass" : `${doctor.issue_count} issue${doctor.issue_count === 1 ? "" : "s"}`,
|
|
140
|
+
next: doctor.all_good
|
|
141
|
+
? "Run `quill mcp tools` to verify MCP, then `quill browse`."
|
|
142
|
+
: `Run \`quill doctor\`. Start with: ${doctor.start_with}`,
|
|
143
|
+
}, [
|
|
144
|
+
"Run `quill doctor` to diagnose Quill desktop and MCP setup",
|
|
145
|
+
"Run `quill browse` to open the meeting picker",
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
printData(setup, options);
|
|
149
|
+
if (!doctor.all_good) {
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function runDoctor(options, config) {
|
|
155
|
+
const doctor = await runDoctorChecks(config);
|
|
156
|
+
printData({ result: doctor }, options);
|
|
157
|
+
if (!doctor.all_good) process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function runMcp(client, args, options) {
|
|
161
|
+
const command = args[0];
|
|
162
|
+
if (command === "tools" || !command) {
|
|
163
|
+
const tools = await client.listTools();
|
|
164
|
+
printData(withHelp({
|
|
165
|
+
count: tools.length,
|
|
166
|
+
tools: tools.map((tool) => ({
|
|
167
|
+
name: tool.name,
|
|
168
|
+
description: truncateText(tool.description || "", 180),
|
|
169
|
+
})),
|
|
170
|
+
}, [
|
|
171
|
+
"Run `quill mcp schema <tool>` to inspect an input schema",
|
|
172
|
+
"Run `quill mcp call <tool> --input '{...}'` to call a raw MCP tool",
|
|
173
|
+
]), options);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (command === "schema") {
|
|
178
|
+
const name = args[1];
|
|
179
|
+
if (!name) throw cliError("missing_tool", "Usage: quill mcp schema <tool>");
|
|
180
|
+
const tools = await client.listTools();
|
|
181
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
182
|
+
if (!tool) throw cliError("tool_not_found", `No MCP tool named ${name}`);
|
|
183
|
+
printData(tool, options);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (command === "call") {
|
|
188
|
+
const name = args[1];
|
|
189
|
+
if (!name) throw cliError("missing_tool", "Usage: quill mcp call <tool> --input '{...}'");
|
|
190
|
+
const input = readOption(args.slice(2), "--input") || "{}";
|
|
191
|
+
const parsed = JSON.parse(input);
|
|
192
|
+
printData(extractToolResult(await client.callTool(name, parsed)), options);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
throw cliError("unknown_command", `Unknown mcp command: ${command}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function runCurated(client, args, options) {
|
|
200
|
+
const [domain, actionOrValue, maybeValue] = args;
|
|
201
|
+
const parsed = parseFlags(args.slice(1));
|
|
202
|
+
const flags = parsed.flags;
|
|
203
|
+
const tools = await client.listTools();
|
|
204
|
+
|
|
205
|
+
if (domain === "browse" || ((domain === "meetings" || domain === "meeting") && actionOrValue === "browse")) {
|
|
206
|
+
await browseMeetings(client, tools, options, {
|
|
207
|
+
limit: flags.limit || browseDefaultLimit(options),
|
|
208
|
+
since: flags.since,
|
|
209
|
+
until: flags.until,
|
|
210
|
+
today: flags.today,
|
|
211
|
+
yesterday: flags.yesterday,
|
|
212
|
+
query: flags.search,
|
|
213
|
+
});
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (domain === "meetings" || domain === "meeting") {
|
|
218
|
+
if (!actionOrValue || actionOrValue === "list") {
|
|
219
|
+
await callRoute(client, tools, "listMeetings", {
|
|
220
|
+
limit: flags.limit || options.limit,
|
|
221
|
+
since: flags.since,
|
|
222
|
+
until: flags.until,
|
|
223
|
+
today: flags.today,
|
|
224
|
+
yesterday: flags.yesterday,
|
|
225
|
+
query: flags.search,
|
|
226
|
+
}, options, [
|
|
227
|
+
"Run `quill meetings view <id>`",
|
|
228
|
+
"Run `quill transcript <id> --full`",
|
|
229
|
+
"Run `quill search \"<query>\"`",
|
|
230
|
+
]);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (actionOrValue === "view") {
|
|
235
|
+
const id = await resolveMeetingId(client, tools, maybeValue, options);
|
|
236
|
+
await callRoute(client, tools, "getMeeting", { id }, options, [
|
|
237
|
+
"Run `quill notes <id>`",
|
|
238
|
+
"Run `quill transcript <id>`",
|
|
239
|
+
]);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (domain === "transcript") {
|
|
245
|
+
const id = await resolveMeetingId(client, tools, actionOrValue, options);
|
|
246
|
+
await callRoute(client, tools, "getTranscript", { id }, options, [
|
|
247
|
+
"Run `quill notes <id>`",
|
|
248
|
+
"Run `quill export <id> --format json`",
|
|
249
|
+
]);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (domain === "notes" || domain === "summary" || domain === "summarize") {
|
|
254
|
+
const id = await resolveMeetingId(client, tools, actionOrValue, options);
|
|
255
|
+
await callRoute(client, tools, "getNotes", { id }, options, [
|
|
256
|
+
"Run `quill transcript <id> --full`",
|
|
257
|
+
"Run `quill search \"<query>\"`",
|
|
258
|
+
]);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (domain === "note") {
|
|
263
|
+
if (actionOrValue === "create") {
|
|
264
|
+
const id = await resolveMeetingId(client, tools, maybeValue, options);
|
|
265
|
+
await createGeneratedNote(client, tools, {
|
|
266
|
+
meetingId: id,
|
|
267
|
+
prompt: flags.prompt || parseFlags(args.slice(3)).positionals.join(" "),
|
|
268
|
+
instruction: flags.instruction,
|
|
269
|
+
templateId: flags.template,
|
|
270
|
+
includePrivateNotes: parseOptionalBoolean(flags.includePrivateNotes),
|
|
271
|
+
data: flags.data,
|
|
272
|
+
}, options);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (domain === "actions" || domain === "action-items") {
|
|
278
|
+
const id = await resolveMeetingId(client, tools, actionOrValue, options);
|
|
279
|
+
await createGeneratedNote(client, tools, {
|
|
280
|
+
meetingId: id,
|
|
281
|
+
prompt: ACTIONS_PROMPT,
|
|
282
|
+
instruction: flags.instruction,
|
|
283
|
+
}, options);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (domain === "followup" || domain === "follow-up") {
|
|
288
|
+
const id = await resolveMeetingId(client, tools, actionOrValue, options);
|
|
289
|
+
await createGeneratedNote(client, tools, {
|
|
290
|
+
meetingId: id,
|
|
291
|
+
prompt: FOLLOWUP_PROMPT,
|
|
292
|
+
instruction: flags.instruction,
|
|
293
|
+
}, options);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (domain === "search") {
|
|
298
|
+
await callRoute(client, tools, "search", {
|
|
299
|
+
query: parsed.positionals.join(" "),
|
|
300
|
+
limit: flags.limit || options.limit,
|
|
301
|
+
since: flags.since,
|
|
302
|
+
until: flags.until,
|
|
303
|
+
today: flags.today,
|
|
304
|
+
yesterday: flags.yesterday,
|
|
305
|
+
}, options, [
|
|
306
|
+
"Run `quill meetings view <id>`",
|
|
307
|
+
"Run `quill transcript <id> --full`",
|
|
308
|
+
]);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (domain === "contacts") {
|
|
313
|
+
if (!actionOrValue || actionOrValue === "list" || actionOrValue === "search") {
|
|
314
|
+
await callRoute(client, tools, "listContacts", {
|
|
315
|
+
query: actionOrValue === "search" ? parseFlags(args.slice(2)).positionals.join(" ") : flags.search,
|
|
316
|
+
limit: flags.limit || options.limit,
|
|
317
|
+
offset: flags.offset,
|
|
318
|
+
}, options, [
|
|
319
|
+
"Run `quill contacts get <id>`",
|
|
320
|
+
"Run `quill search \"<person or topic>\"`",
|
|
321
|
+
]);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (actionOrValue === "get") {
|
|
325
|
+
await callRoute(client, tools, "getContact", { id: maybeValue }, options, [
|
|
326
|
+
"Run `quill contacts search \"<name>\"`",
|
|
327
|
+
]);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (domain === "threads") {
|
|
333
|
+
if (!actionOrValue || actionOrValue === "list") {
|
|
334
|
+
await callRoute(client, tools, "listThreads", {
|
|
335
|
+
includeMeetings: Boolean(flags.includeMeetings),
|
|
336
|
+
meetingsLimit: flags.meetingsLimit,
|
|
337
|
+
}, options, [
|
|
338
|
+
"Run `quill threads get <id>`",
|
|
339
|
+
"Run `quill meetings list --search \"<topic>\"`",
|
|
340
|
+
]);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (actionOrValue === "get") {
|
|
344
|
+
await callRoute(client, tools, "getThread", { id: maybeValue }, options, [
|
|
345
|
+
"Run `quill threads list --include-meetings`",
|
|
346
|
+
]);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (domain === "events") {
|
|
352
|
+
if (!actionOrValue || actionOrValue === "list") {
|
|
353
|
+
await callRoute(client, tools, "listEvents", {
|
|
354
|
+
limit: flags.limit || options.limit,
|
|
355
|
+
offset: flags.offset,
|
|
356
|
+
since: flags.after || flags.since,
|
|
357
|
+
until: flags.before || flags.until,
|
|
358
|
+
}, options, [
|
|
359
|
+
"Run `quill events get <id>`",
|
|
360
|
+
"Run `quill meetings list --since 7d`",
|
|
361
|
+
]);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (actionOrValue === "get") {
|
|
365
|
+
await callRoute(client, tools, "getEvent", { id: maybeValue }, options, [
|
|
366
|
+
"Run `quill events list`",
|
|
367
|
+
]);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (domain === "templates") {
|
|
373
|
+
if (!actionOrValue || actionOrValue === "list") {
|
|
374
|
+
await callRoute(client, tools, "listTemplates", {
|
|
375
|
+
limit: flags.limit || options.limit,
|
|
376
|
+
offset: flags.offset,
|
|
377
|
+
kind: flags.kind,
|
|
378
|
+
includeDisabled: Boolean(flags.includeDisabled),
|
|
379
|
+
}, options, [
|
|
380
|
+
"Run `quill templates get <id>`",
|
|
381
|
+
"Run `quill mcp tools` for template mutation tools",
|
|
382
|
+
]);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (actionOrValue === "get") {
|
|
386
|
+
await callRoute(client, tools, "getTemplate", { id: maybeValue }, options, [
|
|
387
|
+
"Run `quill templates list`",
|
|
388
|
+
]);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
throw cliError("unknown_command", `Unknown command: ${domain}. Run quill --help.`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function callRoute(client, tools, route, values, options, help) {
|
|
397
|
+
const tool = findTool(tools, route);
|
|
398
|
+
if (!tool) {
|
|
399
|
+
printData(structuredError("tool_route_unavailable", `Could not find a Quill MCP tool for ${route}`, {
|
|
400
|
+
available_tools: tools.map((candidate) => candidate.name),
|
|
401
|
+
next: "Run `quill mcp tools` and `quill mcp schema <tool>` to inspect the server.",
|
|
402
|
+
}), options);
|
|
403
|
+
process.exitCode = 1;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const args = tool.name === "search_meetings" ? buildSearchMeetingsArgs(values) : buildArgs(tool, normalizeValues(values));
|
|
408
|
+
const result = extractToolResult(await client.callTool(tool.name, args));
|
|
409
|
+
addPagination(result, values);
|
|
410
|
+
printData(withHelp({ tool: tool.name, result }, help), options);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function createGeneratedNote(client, tools, values, options) {
|
|
414
|
+
if (!values.prompt && !values.templateId) {
|
|
415
|
+
throw cliError("missing_prompt", "Provide --prompt, a prompt argument, or --template.");
|
|
416
|
+
}
|
|
417
|
+
const tool = findTool(tools, "createNote");
|
|
418
|
+
if (!tool) throw cliError("tool_route_unavailable", "Could not find Quill MCP create_note tool.");
|
|
419
|
+
const result = extractToolResult(await client.callTool(tool.name, buildArgs(tool, values)));
|
|
420
|
+
printData(withHelp({
|
|
421
|
+
tool: tool.name,
|
|
422
|
+
result,
|
|
423
|
+
}, [
|
|
424
|
+
"Run `quill notes <id>` to read notes for this meeting",
|
|
425
|
+
"Run `quill browse` to continue from the meeting picker",
|
|
426
|
+
]), options);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function addPagination(result, values) {
|
|
430
|
+
if (!result || typeof result !== "object") return;
|
|
431
|
+
const limit = Number.parseInt(values.limit, 10);
|
|
432
|
+
if (!Number.isInteger(limit) || limit <= 0) return;
|
|
433
|
+
const offset = Number.parseInt(values.offset || result.offset || 0, 10);
|
|
434
|
+
const collection = ["meetings", "events", "contacts", "templates", "threads", "notes"]
|
|
435
|
+
.map((key) => result[key])
|
|
436
|
+
.find(Array.isArray);
|
|
437
|
+
if (collection && collection.length >= limit) result.next_offset = offset + limit;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function normalizeValues(values) {
|
|
441
|
+
const normalized = applyTimeFlags({ ...values });
|
|
442
|
+
if (normalized.since) normalized.since = parseDateCutoff(normalized.since);
|
|
443
|
+
if (normalized.until) normalized.until = parseDateCutoff(normalized.until);
|
|
444
|
+
return normalized;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function buildSearchMeetingsArgs(values) {
|
|
448
|
+
values = applyTimeFlags(values);
|
|
449
|
+
const args = {};
|
|
450
|
+
if (values.query) args.query = values.query;
|
|
451
|
+
if (values.limit) args.limit = Math.min(Number.parseInt(values.limit, 10) || 10, 30);
|
|
452
|
+
|
|
453
|
+
const filter = {};
|
|
454
|
+
if (values.since) filter.after = parseDateCutoff(values.since);
|
|
455
|
+
if (values.until) filter.before = parseDateCutoff(values.until);
|
|
456
|
+
if (Object.keys(filter).length > 0) args.filter = filter;
|
|
457
|
+
|
|
458
|
+
if (values.query) {
|
|
459
|
+
args.ranking = {
|
|
460
|
+
freshness: values.since ? "strong" : "default",
|
|
461
|
+
scope: "default",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return args;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function resolveMeetingId(client, tools, id, options) {
|
|
469
|
+
if (id) return id;
|
|
470
|
+
if (options.agent || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
471
|
+
throw cliError("missing_meeting_id", "Missing meeting id. Usage: quill meetings view <id>, quill notes <id>, or quill transcript <id>.");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const selection = await pickRecentMeeting(client, tools, options, { limit: 10 });
|
|
475
|
+
if (!selection) throw cliError("no_selection", "No meeting selected.");
|
|
476
|
+
return selection.meeting.id;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function pickRecentMeeting(client, tools, options, values) {
|
|
480
|
+
const selection = await browseMeetings(client, tools, options, values, { selectOnly: true });
|
|
481
|
+
return selection;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function browseMeetings(client, tools, options, values, pickerOptions = {}) {
|
|
485
|
+
if (options.agent || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
486
|
+
throw cliError("interactive_unavailable", "Interactive browsing requires a terminal. Use `quill meetings list --json` in agent mode.");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const tool = findTool(tools, "listMeetings");
|
|
490
|
+
if (!tool) throw cliError("tool_route_unavailable", "Cannot list recent meetings to choose an id.");
|
|
491
|
+
|
|
492
|
+
const searchMeetings = async (query) => {
|
|
493
|
+
const result = extractToolResult(await client.callTool(tool.name, buildSearchMeetingsArgs({
|
|
494
|
+
...values,
|
|
495
|
+
query,
|
|
496
|
+
})));
|
|
497
|
+
return result.meetings || [];
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const meetings = await searchMeetings(values.query);
|
|
501
|
+
if (meetings.length === 0) throw cliError("no_meetings", "No recent meetings found.");
|
|
502
|
+
|
|
503
|
+
return runMeetingBrowser(client, tools, meetings, options, pickerOptions, { searchMeetings });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function applyTimeFlags(values) {
|
|
507
|
+
const normalized = { ...values };
|
|
508
|
+
if (normalized.today) {
|
|
509
|
+
normalized.since = startOfDay(0);
|
|
510
|
+
normalized.until = startOfDay(1);
|
|
511
|
+
} else if (normalized.yesterday) {
|
|
512
|
+
normalized.since = startOfDay(-1);
|
|
513
|
+
normalized.until = startOfDay(0);
|
|
514
|
+
}
|
|
515
|
+
return normalized;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function parseDateCutoff(value) {
|
|
519
|
+
const phrase = String(value).trim().toLowerCase();
|
|
520
|
+
if (phrase === "today") return startOfDay(0);
|
|
521
|
+
if (phrase === "yesterday") return startOfDay(-1);
|
|
522
|
+
if (phrase === "last week") return new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
523
|
+
if (phrase === "last month") return new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
524
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(value)) return value;
|
|
525
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return `${value}T00:00:00Z`;
|
|
526
|
+
|
|
527
|
+
const match = String(value).match(/^(\d+)([dwmy])$/);
|
|
528
|
+
if (!match) return value;
|
|
529
|
+
|
|
530
|
+
const amount = Number.parseInt(match[1], 10);
|
|
531
|
+
const unit = match[2];
|
|
532
|
+
const days = unit === "d" ? amount : unit === "w" ? amount * 7 : unit === "m" ? amount * 30 : amount * 365;
|
|
533
|
+
return new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function startOfDay(offsetDays) {
|
|
537
|
+
const date = new Date();
|
|
538
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
539
|
+
date.setUTCDate(date.getUTCDate() + offsetDays);
|
|
540
|
+
return date.toISOString();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function parseGlobalOptions(argv, config) {
|
|
544
|
+
const args = [];
|
|
545
|
+
const options = {
|
|
546
|
+
format: config.output.format || "human",
|
|
547
|
+
limit: config.output.limit || 20,
|
|
548
|
+
truncate: config.output.truncate || 1200,
|
|
549
|
+
browseLimit: config.browse?.limit || config.output.limit || 20,
|
|
550
|
+
browsePanelTruncate: config.browse?.panel_truncate || 5000,
|
|
551
|
+
limitExplicit: false,
|
|
552
|
+
help: false,
|
|
553
|
+
version: false,
|
|
554
|
+
agent: Boolean(config.agent?.enabled) || process.env.QUILL_AGENT_MODE === "1",
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
for (let index = 0; index < argv.length; index++) {
|
|
558
|
+
const arg = argv[index];
|
|
559
|
+
if (arg === "--help" || arg === "-h") options.help = true;
|
|
560
|
+
else if (arg === "--version" || arg === "-v") options.version = true;
|
|
561
|
+
else if (arg === "--json") {
|
|
562
|
+
options.format = "json";
|
|
563
|
+
options.agent = true;
|
|
564
|
+
}
|
|
565
|
+
else if (arg === "--agent") options.agent = true;
|
|
566
|
+
else if (arg === "--no-agent") options.agent = false;
|
|
567
|
+
else if (arg === "--human" || arg === "--table") options.forceHuman = true;
|
|
568
|
+
else if (arg === "--full") options.full = true;
|
|
569
|
+
else if (arg === "--fields") options.fields = splitCsv(argv[++index] || "");
|
|
570
|
+
else if (arg === "--truncate") options.truncate = Number.parseInt(argv[++index] || "1200", 10);
|
|
571
|
+
else if (arg === "--format" || arg === "-o") {
|
|
572
|
+
options.format = argv[++index] || "human";
|
|
573
|
+
if (options.format === "json") options.agent = true;
|
|
574
|
+
}
|
|
575
|
+
else if (arg === "--limit" || arg === "-l") {
|
|
576
|
+
options.limit = Number.parseInt(argv[++index] || "20", 10);
|
|
577
|
+
options.limitExplicit = true;
|
|
578
|
+
}
|
|
579
|
+
else args.push(arg);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (options.format === "json") options.agent = true;
|
|
583
|
+
if (options.forceHuman) options.format = "human";
|
|
584
|
+
if (options.agent && options.format === "human" && !options.forceHuman) options.format = "toon";
|
|
585
|
+
options.args = args;
|
|
586
|
+
options.human = !options.agent && options.format === "human";
|
|
587
|
+
return options;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function parseFlags(args) {
|
|
591
|
+
const flags = {};
|
|
592
|
+
const positionals = [];
|
|
593
|
+
for (let index = 0; index < args.length; index++) {
|
|
594
|
+
const arg = args[index];
|
|
595
|
+
if (!arg.startsWith("--")) {
|
|
596
|
+
positionals.push(arg);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
600
|
+
if (!args[index + 1] || args[index + 1].startsWith("--")) {
|
|
601
|
+
flags[key] = true;
|
|
602
|
+
} else {
|
|
603
|
+
const value = args[++index];
|
|
604
|
+
const phrase = `${value} ${args[index + 1] || ""}`.trim().toLowerCase();
|
|
605
|
+
if (["last week", "last month"].includes(phrase)) {
|
|
606
|
+
flags[key] = phrase;
|
|
607
|
+
index++;
|
|
608
|
+
} else {
|
|
609
|
+
flags[key] = value;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return { flags, positionals };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function browseDefaultLimit(options) {
|
|
617
|
+
return options.limitExplicit ? options.limit : options.browseLimit || options.limit;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function readOption(args, name) {
|
|
621
|
+
const index = args.indexOf(name);
|
|
622
|
+
return index === -1 ? undefined : args[index + 1];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function printHelp(topic) {
|
|
626
|
+
if (topic && topic !== "help") {
|
|
627
|
+
const text = subcommandHelp(topic);
|
|
628
|
+
if (text) {
|
|
629
|
+
process.stdout.write(text);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
process.stdout.write(`Quill CLI - agent-friendly Quill Meetings from the terminal
|
|
635
|
+
|
|
636
|
+
Usage:
|
|
637
|
+
quill
|
|
638
|
+
quill init
|
|
639
|
+
quill doctor
|
|
640
|
+
quill meetings list [--limit 20] [--since 7d] [--search text]
|
|
641
|
+
quill browse [--limit 20] [--search text]
|
|
642
|
+
quill meetings browse [--limit 20] [--search text]
|
|
643
|
+
quill meetings view <id>
|
|
644
|
+
quill ls
|
|
645
|
+
quill v <id>
|
|
646
|
+
quill <id>
|
|
647
|
+
quill notes <id>
|
|
648
|
+
quill summarize <id>
|
|
649
|
+
quill note create <id> --prompt "..."
|
|
650
|
+
quill actions <id>
|
|
651
|
+
quill followup <id>
|
|
652
|
+
quill transcript <id> [--full]
|
|
653
|
+
quill search "<query>" [--limit 20] [--since "last week"] [--today]
|
|
654
|
+
quill contacts list [--search text]
|
|
655
|
+
quill threads list [--include-meetings]
|
|
656
|
+
quill events list [--limit 20]
|
|
657
|
+
quill templates list [--kind minutes]
|
|
658
|
+
quill config show|get|set|init|path
|
|
659
|
+
quill mcp tools
|
|
660
|
+
quill mcp schema <tool>
|
|
661
|
+
quill mcp call <tool> --input '{"key":"value"}'
|
|
662
|
+
quill completion zsh|bash|fish
|
|
663
|
+
|
|
664
|
+
Global options:
|
|
665
|
+
--json Print JSON instead of human output
|
|
666
|
+
--agent Disable interactive prompts and human formatting
|
|
667
|
+
--no-agent Override agent.enabled from config for one command
|
|
668
|
+
--human, --table Force human table output when not using --json
|
|
669
|
+
--fields <a,b,c> Select list fields
|
|
670
|
+
--full Disable large text truncation
|
|
671
|
+
--truncate <chars> Large text truncation limit
|
|
672
|
+
-o, --format <format> Output format: human, toon, or json
|
|
673
|
+
-l, --limit <n> Default result limit
|
|
674
|
+
-h, --help Show help
|
|
675
|
+
-v, --version Show version
|
|
676
|
+
|
|
677
|
+
Environment:
|
|
678
|
+
QUILL_CONFIG Config file path, defaults to ~/.config/quill-cli/config.json
|
|
679
|
+
QUILL_AGENT_MODE=1 Agent mode by default
|
|
680
|
+
QUILL_DEBUG=1 Forward MCP stderr
|
|
681
|
+
`);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function subcommandHelp(topic) {
|
|
685
|
+
const help = {
|
|
686
|
+
meetings: `Usage:
|
|
687
|
+
quill meetings list [--limit 20] [--since 7d] [--today] [--fields id,title,date,duration]
|
|
688
|
+
quill meetings browse [--limit 20] [--search text]
|
|
689
|
+
quill meetings view <id>
|
|
690
|
+
|
|
691
|
+
Hints:
|
|
692
|
+
Use \`quill browse\` to avoid copying meeting UUIDs.
|
|
693
|
+
Use \`--json\` for agent/script output.
|
|
694
|
+
`,
|
|
695
|
+
init: `Usage:
|
|
696
|
+
quill init
|
|
697
|
+
|
|
698
|
+
Notes:
|
|
699
|
+
Writes the default JSON config if it does not exist, then runs the same setup checks as \`quill doctor\`.
|
|
700
|
+
`,
|
|
701
|
+
doctor: `Usage:
|
|
702
|
+
quill doctor
|
|
703
|
+
|
|
704
|
+
Notes:
|
|
705
|
+
Checks Quill desktop install state, MCP bridge path, CLI config, app/CLI version info, and a short MCP handshake.
|
|
706
|
+
`,
|
|
707
|
+
browse: `Usage:
|
|
708
|
+
quill browse [--limit 20] [--search text] [--since 7d]
|
|
709
|
+
|
|
710
|
+
Keys:
|
|
711
|
+
Enter=view, n=notes, t=transcript, a=actions, f=follow-up
|
|
712
|
+
c=copy current panel, y=confirm note generation, b=back
|
|
713
|
+
/=search, ?=help, q=quit
|
|
714
|
+
|
|
715
|
+
Agent mode:
|
|
716
|
+
Browse is interactive and disabled with \`--json\`, \`--agent\`, or QUILL_AGENT_MODE=1.
|
|
717
|
+
`,
|
|
718
|
+
notes: `Usage:
|
|
719
|
+
quill notes <id> [--full]
|
|
720
|
+
quill notes
|
|
721
|
+
|
|
722
|
+
Notes:
|
|
723
|
+
Without an id in a TTY, opens the meeting picker.
|
|
724
|
+
`,
|
|
725
|
+
note: `Usage:
|
|
726
|
+
quill note create <id> --prompt "Generate a customer-ready recap"
|
|
727
|
+
quill note create <id> "Summarize risks and blockers"
|
|
728
|
+
quill note create <id> --template <template-id> [--instruction "..."]
|
|
729
|
+
|
|
730
|
+
Notes:
|
|
731
|
+
Creates a generated Quill note attached to the meeting.
|
|
732
|
+
`,
|
|
733
|
+
actions: `Usage:
|
|
734
|
+
quill actions <id> [--instruction "..."]
|
|
735
|
+
quill action-items <id>
|
|
736
|
+
|
|
737
|
+
Notes:
|
|
738
|
+
Creates a generated action-item note attached to the meeting.
|
|
739
|
+
`,
|
|
740
|
+
followup: `Usage:
|
|
741
|
+
quill followup <id> [--instruction "..."]
|
|
742
|
+
|
|
743
|
+
Notes:
|
|
744
|
+
Creates a generated follow-up note attached to the meeting.
|
|
745
|
+
`,
|
|
746
|
+
transcript: `Usage:
|
|
747
|
+
quill transcript <id> [--full]
|
|
748
|
+
quill transcript
|
|
749
|
+
|
|
750
|
+
Notes:
|
|
751
|
+
Transcripts can be long. Default output is truncated; use \`--full\` when needed.
|
|
752
|
+
`,
|
|
753
|
+
search: `Usage:
|
|
754
|
+
quill search "<query>" [--limit 20] [--since 7d] [--today] [--fields id,title,date,duration]
|
|
755
|
+
`,
|
|
756
|
+
mcp: `Usage:
|
|
757
|
+
quill mcp tools
|
|
758
|
+
quill mcp schema <tool>
|
|
759
|
+
quill mcp call <tool> --input '{"key":"value"}'
|
|
760
|
+
`,
|
|
761
|
+
config: `Usage:
|
|
762
|
+
quill config path
|
|
763
|
+
quill config init
|
|
764
|
+
quill config show
|
|
765
|
+
quill config get <key>
|
|
766
|
+
quill config set <key> <value>
|
|
767
|
+
|
|
768
|
+
Examples:
|
|
769
|
+
quill config set mcp.mutation_timeout_ms 180000
|
|
770
|
+
quill config set mcp.args '["/path/to/mcp-stdio-bridge.js"]'
|
|
771
|
+
`,
|
|
772
|
+
};
|
|
773
|
+
return help[topic];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function normalizeCommandArgs(args) {
|
|
777
|
+
if (args[0] === "ls") return ["meetings", "list", ...args.slice(1)];
|
|
778
|
+
if (args[0] === "v" || args[0] === "view") return ["meetings", "view", ...args.slice(1)];
|
|
779
|
+
if (args[0] === "n") return ["notes", ...args.slice(1)];
|
|
780
|
+
if (args[0] === "t") return ["transcript", ...args.slice(1)];
|
|
781
|
+
if (args[0] && looksLikeId(args[0])) return ["meetings", "view", args[0], ...args.slice(1)];
|
|
782
|
+
return args;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function validateBeforeMcp(args, options) {
|
|
786
|
+
if (!supportedPlatform()) {
|
|
787
|
+
throw cliError("unsupported_platform", "Quill CLI currently supports macOS and Windows. Linux support is not available yet.");
|
|
788
|
+
}
|
|
789
|
+
if (!options.agent) return;
|
|
790
|
+
const [domain, actionOrValue, maybeValue] = args;
|
|
791
|
+
if (domain === "browse" || ((domain === "meetings" || domain === "meeting") && actionOrValue === "browse")) {
|
|
792
|
+
throw cliError("interactive_unavailable", "Interactive browsing is disabled in agent mode. Use `quill meetings list --json`.");
|
|
793
|
+
}
|
|
794
|
+
if (domain === "notes" || domain === "summary" || domain === "summarize" || domain === "transcript") {
|
|
795
|
+
if (!actionOrValue) throw cliError("missing_meeting_id", `Missing meeting id. Usage: quill ${domain} <id>.`);
|
|
796
|
+
}
|
|
797
|
+
if (domain === "actions" || domain === "action-items" || domain === "followup" || domain === "follow-up") {
|
|
798
|
+
if (!actionOrValue) throw cliError("missing_meeting_id", `Missing meeting id. Usage: quill ${domain} <id>.`);
|
|
799
|
+
}
|
|
800
|
+
if (domain === "note" && actionOrValue === "create" && !maybeValue) {
|
|
801
|
+
throw cliError("missing_meeting_id", "Missing meeting id. Usage: quill note create <id> --prompt \"...\".");
|
|
802
|
+
}
|
|
803
|
+
if ((domain === "meetings" || domain === "meeting") && actionOrValue === "view" && !maybeValue) {
|
|
804
|
+
throw cliError("missing_meeting_id", "Missing meeting id. Usage: quill meetings view <id>.");
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function looksLikeId(value) {
|
|
809
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function printCompletion(shell) {
|
|
813
|
+
const commands = "doctor init browse meetings meeting ls v view notes note n actions action-items followup follow-up transcript t search contacts threads events templates mcp config completion help";
|
|
814
|
+
if (shell === "bash") {
|
|
815
|
+
process.stdout.write(`_quill_complete(){ COMPREPLY=( $(compgen -W "${commands}" -- "\${COMP_WORDS[COMP_CWORD]}") ); }\ncomplete -F _quill_complete quill\n`);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (shell === "fish") {
|
|
819
|
+
process.stdout.write(commands.split(" ").map((command) => `complete -c quill -f -a ${command}`).join("\n") + "\n");
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
process.stdout.write(`#compdef quill\n_arguments '1:command:(${commands})'\n`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function cliError(code, message) {
|
|
826
|
+
const error = new Error(message);
|
|
827
|
+
error.code = code;
|
|
828
|
+
error.exitCode = 1;
|
|
829
|
+
return error;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function splitCsv(value) {
|
|
833
|
+
return value.split(",").map((field) => field.trim()).filter(Boolean);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function parseOptionalBoolean(value) {
|
|
837
|
+
if (value === undefined) return undefined;
|
|
838
|
+
if (value === true || value === "true") return true;
|
|
839
|
+
if (value === false || value === "false") return false;
|
|
840
|
+
return Boolean(value);
|
|
841
|
+
}
|