donebear 0.3.8 → 0.4.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/dist/cli.mjs CHANGED
@@ -1,25 +1,256 @@
1
1
  #!/usr/bin/env node
2
- import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-DuCNavgl.mjs";
2
+ import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-BBEVWudm.mjs";
3
3
  import { n as readStdin, t as promptForValue } from "./prompt-GfcWO85b.mjs";
4
4
  import { createRequire } from "node:module";
5
- import { Command, InvalidArgumentError } from "commander";
6
- import { homedir } from "node:os";
7
- import { basename, join, resolve } from "node:path";
8
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
- import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs";
10
- import { config } from "dotenv";
11
- import { createHash, randomBytes, randomUUID } from "node:crypto";
6
+ import { basename, join, resolve } from "node:path";
7
+ import { createInterface } from "node:readline";
8
+ import { homedir } from "node:os";
12
9
  import { execSync, spawn } from "node:child_process";
13
10
  import { styleText } from "node:util";
14
11
  import jmespath from "jmespath";
15
12
  import { stringify } from "yaml";
13
+ import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs";
14
+ import { config } from "dotenv";
15
+ import { Command, InvalidArgumentError } from "commander";
16
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
16
17
  import { createServer } from "node:http";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import Database from "better-sqlite3";
19
- import { createInterface } from "node:readline";
20
20
  //#region \0rolldown/runtime.js
21
21
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
22
22
  //#endregion
23
+ //#region src/output.ts
24
+ const writeLine = (stream, message) => {
25
+ stream.write(`${message}\n`);
26
+ };
27
+ const colorize = (context, format, message) => {
28
+ if (!context.color) return message;
29
+ return styleText(format, message);
30
+ };
31
+ const writeJson = (value) => {
32
+ writeLine(process.stdout, JSON.stringify(value, null, 2));
33
+ };
34
+ const writeYaml = (value) => {
35
+ process.stdout.write(stringify(value));
36
+ };
37
+ const writeInfo = (context, message) => {
38
+ writeLine(process.stdout, colorize(context, "cyan", message));
39
+ };
40
+ const writeSuccess = (context, message) => {
41
+ writeLine(process.stdout, colorize(context, "green", message));
42
+ };
43
+ const writeWarning = (context, message) => {
44
+ writeLine(process.stderr, colorize(context, "yellow", message));
45
+ };
46
+ const writeError = (context, message) => {
47
+ writeLine(process.stderr, colorize(context, "red", message));
48
+ };
49
+ const writeDebug = (context, message) => {
50
+ if (!context.debug) return;
51
+ writeLine(process.stderr, colorize(context, "gray", `[debug] ${message}`));
52
+ };
53
+ const writeTotal = (context, count) => {
54
+ writeLine(process.stdout, colorize(context, "cyan", String(count)));
55
+ };
56
+ const escapeCsvField = (value) => {
57
+ if (value.includes(",") || value.includes("\"") || value.includes("\n")) return `"${value.replaceAll("\"", "\"\"")}"`;
58
+ return value;
59
+ };
60
+ const writeCsv = (headers, rows) => {
61
+ writeLine(process.stdout, headers.map(escapeCsvField).join(","));
62
+ for (const row of rows) writeLine(process.stdout, row.map(escapeCsvField).join(","));
63
+ };
64
+ const writeTsv = (headers, rows) => {
65
+ writeLine(process.stdout, headers.join(" "));
66
+ for (const row of rows) writeLine(process.stdout, row.join(" "));
67
+ };
68
+ /**
69
+ * Write structured data to stdout, applying --jq filter and --limit if set.
70
+ * Use this for all JSON/YAML/structured output from commands.
71
+ */
72
+ const writeOutput = (context, data) => {
73
+ let result = data;
74
+ if (context.jq) try {
75
+ result = jmespath.search(result, context.jq);
76
+ } catch (error) {
77
+ throw new CliError(`Invalid --jq expression: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.VALIDATION);
78
+ }
79
+ if (context.limit !== null && Array.isArray(result)) result = result.slice(0, context.limit);
80
+ switch (context.format) {
81
+ case "yaml":
82
+ writeYaml(result);
83
+ break;
84
+ case "json":
85
+ writeJson(result);
86
+ break;
87
+ default:
88
+ writeJson(result);
89
+ break;
90
+ }
91
+ if (context.copy) copyToClipboard(JSON.stringify(result, null, 2));
92
+ };
93
+ /**
94
+ * Handles --total, --format csv, --format tsv, --format yaml output.
95
+ * Returns true if the format was handled; caller renders text mode if false.
96
+ */
97
+ const writeFormattedRows = (context, totalCount, headers, rows) => {
98
+ if (context.total) {
99
+ writeTotal(context, totalCount);
100
+ return true;
101
+ }
102
+ if (context.format === "yaml") {
103
+ const objects = rows.map((row) => Object.fromEntries(headers.map((h, i) => [h, row[i]])));
104
+ writeYaml(objects);
105
+ if (context.copy) copyToClipboard(stringify(objects));
106
+ return true;
107
+ }
108
+ if (context.format === "csv") {
109
+ writeCsv(headers, rows);
110
+ if (context.copy) copyToClipboard([headers.join(","), ...rows.map((r) => r.join(","))].join("\n"));
111
+ return true;
112
+ }
113
+ if (context.format === "tsv") {
114
+ writeTsv(headers, rows);
115
+ if (context.copy) copyToClipboard([headers.join(" "), ...rows.map((r) => r.join(" "))].join("\n"));
116
+ return true;
117
+ }
118
+ return false;
119
+ };
120
+ const copyToClipboard = (text) => {
121
+ try {
122
+ if (process.platform === "darwin") execSync("pbcopy", { input: text });
123
+ else if (process.platform === "win32") execSync("clip", { input: text });
124
+ else try {
125
+ execSync("xclip -selection clipboard", { input: text });
126
+ } catch {
127
+ execSync("xsel --clipboard --input", { input: text });
128
+ }
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ };
134
+ //#endregion
135
+ //#region src/commands/interactive.ts
136
+ const HISTORY_FILE = join(CONFIG_DIR, "history.json");
137
+ const MAX_HISTORY = 200;
138
+ const isHistoryFile = (value) => {
139
+ if (typeof value !== "object" || value === null) return false;
140
+ const record = value;
141
+ return record.version === 1 && Array.isArray(record.entries) && record.entries.every((e) => typeof e === "string");
142
+ };
143
+ const loadHistory = async (historyFile) => {
144
+ try {
145
+ const raw = await readFile(historyFile, "utf-8");
146
+ const parsed = JSON.parse(raw);
147
+ if (isHistoryFile(parsed)) return parsed.entries;
148
+ return [];
149
+ } catch {
150
+ return [];
151
+ }
152
+ };
153
+ const tokenize = (input) => {
154
+ const tokens = [];
155
+ let current = "";
156
+ let inQuote = false;
157
+ let quoteChar = "";
158
+ for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
159
+ else current += char;
160
+ else if (char === "\"" || char === "'") {
161
+ inQuote = true;
162
+ quoteChar = char;
163
+ } else if (char === " ") {
164
+ if (current.length > 0) {
165
+ tokens.push(current);
166
+ current = "";
167
+ }
168
+ } else current += char;
169
+ if (current.length > 0) tokens.push(current);
170
+ return tokens;
171
+ };
172
+ const buildCompleter = (program) => (line) => {
173
+ const allCommands = program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()]);
174
+ const hits = allCommands.filter((c) => c.startsWith(line));
175
+ return [hits.length > 0 ? hits : allCommands, line];
176
+ };
177
+ const runInteractiveMode = async (context, program) => {
178
+ const history = await loadHistory(HISTORY_FILE);
179
+ const rl = createInterface({
180
+ completer: buildCompleter(program),
181
+ historySize: MAX_HISTORY,
182
+ input: process.stdin,
183
+ output: process.stdout,
184
+ prompt: "donebear> "
185
+ });
186
+ for (const entry of [...history].toReversed()) rl.history.push(entry);
187
+ writeSuccess(context, "Done Bear interactive mode. Type \"exit\" or Ctrl+D to quit.");
188
+ writeInfo(context, "Commands: task, workspace, project, label, team, today, search, auth, whoami");
189
+ rl.prompt();
190
+ const sessionEntries = [];
191
+ for await (const line of rl) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed) {
194
+ rl.prompt();
195
+ continue;
196
+ }
197
+ if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") break;
198
+ const tokens = tokenize(trimmed);
199
+ try {
200
+ await program.parseAsync([
201
+ "node",
202
+ "donebear",
203
+ ...tokens
204
+ ]);
205
+ } catch {}
206
+ sessionEntries.push(trimmed);
207
+ rl.prompt();
208
+ }
209
+ rl.close();
210
+ if (sessionEntries.length > 0) try {
211
+ const payload = {
212
+ entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY),
213
+ version: 1
214
+ };
215
+ await mkdir(CONFIG_DIR, {
216
+ mode: 448,
217
+ recursive: true
218
+ });
219
+ await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
220
+ encoding: "utf-8",
221
+ mode: 384
222
+ });
223
+ } catch {}
224
+ };
225
+ //#endregion
226
+ //#region src/env.ts
227
+ const DEFAULT_ENV_FILES = [
228
+ ".env.local",
229
+ ".env",
230
+ "apps/manage-frontend/.env.local",
231
+ "apps/manage-api/.env"
232
+ ];
233
+ let loaded = false;
234
+ const loadCliEnvironmentFiles = () => {
235
+ if (loaded) return;
236
+ for (const relativePath of DEFAULT_ENV_FILES) {
237
+ const absolutePath = resolve(process.cwd(), relativePath);
238
+ if (!existsSync(absolutePath)) continue;
239
+ config({
240
+ override: false,
241
+ path: absolutePath,
242
+ quiet: true
243
+ });
244
+ }
245
+ loaded = true;
246
+ };
247
+ const readEnvValue = (key) => {
248
+ const value = process.env[key];
249
+ if (typeof value !== "string") return;
250
+ const trimmed = value.trim();
251
+ return trimmed.length > 0 ? trimmed : void 0;
252
+ };
253
+ //#endregion
23
254
  //#region src/jwt.ts
24
255
  const parseJwtBase64Url = (input) => {
25
256
  const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
@@ -140,34 +371,6 @@ const clearStoredAuthSession = async () => {
140
371
  await rm(AUTH_FILE_PATH, { force: true });
141
372
  };
142
373
  //#endregion
143
- //#region src/env.ts
144
- const DEFAULT_ENV_FILES = [
145
- ".env.local",
146
- ".env",
147
- "apps/manage-frontend/.env.local",
148
- "apps/manage-api/.env"
149
- ];
150
- let loaded = false;
151
- const loadCliEnvironmentFiles = () => {
152
- if (loaded) return;
153
- for (const relativePath of DEFAULT_ENV_FILES) {
154
- const absolutePath = resolve(process.cwd(), relativePath);
155
- if (!existsSync(absolutePath)) continue;
156
- config({
157
- override: false,
158
- path: absolutePath,
159
- quiet: true
160
- });
161
- }
162
- loaded = true;
163
- };
164
- const readEnvValue = (key) => {
165
- const value = process.env[key];
166
- if (typeof value !== "string") return;
167
- const trimmed = value.trim();
168
- return trimmed.length > 0 ? trimmed : void 0;
169
- };
170
- //#endregion
171
374
  //#region src/supabase.ts
172
375
  const resolveSupabaseConfig = () => {
173
376
  const url = readEnvValue("DONEBEAR_SUPABASE_URL") ?? "https://kdarvputxmlxqemisnbh.supabase.co";
@@ -368,153 +571,41 @@ const formatDateOnlyEpoch = (epochMs) => {
368
571
  const formatInstantEpoch = (epochMs) => {
369
572
  if (!isFiniteEpoch(epochMs)) return null;
370
573
  const date = new Date(epochMs);
371
- if (Number.isNaN(date.getTime())) return null;
372
- return date.toISOString();
373
- };
374
- const parseDateOnlyString = (value) => {
375
- if (typeof value !== "string") return null;
376
- const trimmed = value.trim();
377
- const match = ISO_DATE_ONLY_REGEX.exec(trimmed);
378
- if (!match) return null;
379
- const year = Number.parseInt(match[1] ?? "", 10);
380
- const month = Number.parseInt(match[2] ?? "", 10);
381
- const parts = {
382
- day: Number.parseInt(match[3] ?? "", 10),
383
- month,
384
- year
385
- };
386
- return isValidDateOnlyParts(parts) ? parts : null;
387
- };
388
- const parseApiDateOnlyToEpoch = (value) => {
389
- const parts = parseDateOnlyString(value);
390
- return parts ? dateOnlyPartsToEpoch(parts) : null;
391
- };
392
- const parseApiInstantToEpoch = (value) => {
393
- if (typeof value !== "string") return null;
394
- const trimmed = value.trim();
395
- if (trimmed.length === 0 || !ISO_INSTANT_REGEX.test(trimmed)) return null;
396
- const parsed = Date.parse(trimmed);
397
- return Number.isNaN(parsed) ? null : parsed;
398
- };
399
- const parseCliDateOnlyToEpoch = (value, fieldLabel) => {
400
- if (!value) return null;
401
- const parts = parseDateOnlyString(value);
402
- if (!parts) throw new Error(`Invalid ${fieldLabel}: ${value}. Expected YYYY-MM-DD.`);
403
- return dateOnlyPartsToEpoch(parts);
404
- };
405
- const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
406
- //#endregion
407
- //#region src/output.ts
408
- const writeLine = (stream, message) => {
409
- stream.write(`${message}\n`);
410
- };
411
- const colorize = (context, format, message) => {
412
- if (!context.color) return message;
413
- return styleText(format, message);
414
- };
415
- const writeJson = (value) => {
416
- writeLine(process.stdout, JSON.stringify(value, null, 2));
417
- };
418
- const writeYaml = (value) => {
419
- process.stdout.write(stringify(value));
420
- };
421
- const writeInfo = (context, message) => {
422
- writeLine(process.stdout, colorize(context, "cyan", message));
423
- };
424
- const writeSuccess = (context, message) => {
425
- writeLine(process.stdout, colorize(context, "green", message));
426
- };
427
- const writeWarning = (context, message) => {
428
- writeLine(process.stderr, colorize(context, "yellow", message));
429
- };
430
- const writeError = (context, message) => {
431
- writeLine(process.stderr, colorize(context, "red", message));
432
- };
433
- const writeDebug = (context, message) => {
434
- if (!context.debug) return;
435
- writeLine(process.stderr, colorize(context, "gray", `[debug] ${message}`));
436
- };
437
- const writeTotal = (context, count) => {
438
- writeLine(process.stdout, colorize(context, "cyan", String(count)));
439
- };
440
- const escapeCsvField = (value) => {
441
- if (value.includes(",") || value.includes("\"") || value.includes("\n")) return `"${value.replaceAll("\"", "\"\"")}"`;
442
- return value;
443
- };
444
- const writeCsv = (headers, rows) => {
445
- writeLine(process.stdout, headers.map(escapeCsvField).join(","));
446
- for (const row of rows) writeLine(process.stdout, row.map(escapeCsvField).join(","));
447
- };
448
- const writeTsv = (headers, rows) => {
449
- writeLine(process.stdout, headers.join(" "));
450
- for (const row of rows) writeLine(process.stdout, row.join(" "));
451
- };
452
- /**
453
- * Write structured data to stdout, applying --jq filter and --limit if set.
454
- * Use this for all JSON/YAML/structured output from commands.
455
- */
456
- const writeOutput = (context, data) => {
457
- let result = data;
458
- if (context.jq) try {
459
- result = jmespath.search(result, context.jq);
460
- } catch (error) {
461
- throw new CliError(`Invalid --jq expression: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.VALIDATION);
462
- }
463
- if (context.limit !== null && Array.isArray(result)) result = result.slice(0, context.limit);
464
- switch (context.format) {
465
- case "yaml":
466
- writeYaml(result);
467
- break;
468
- case "json":
469
- writeJson(result);
470
- break;
471
- default:
472
- writeJson(result);
473
- break;
474
- }
475
- if (context.copy) copyToClipboard(JSON.stringify(result, null, 2));
476
- };
477
- /**
478
- * Handles --total, --format csv, --format tsv, --format yaml output.
479
- * Returns true if the format was handled; caller renders text mode if false.
480
- */
481
- const writeFormattedRows = (context, totalCount, headers, rows) => {
482
- if (context.total) {
483
- writeTotal(context, totalCount);
484
- return true;
485
- }
486
- if (context.format === "yaml") {
487
- const objects = rows.map((row) => Object.fromEntries(headers.map((h, i) => [h, row[i]])));
488
- writeYaml(objects);
489
- if (context.copy) copyToClipboard(stringify(objects));
490
- return true;
491
- }
492
- if (context.format === "csv") {
493
- writeCsv(headers, rows);
494
- if (context.copy) copyToClipboard([headers.join(","), ...rows.map((r) => r.join(","))].join("\n"));
495
- return true;
496
- }
497
- if (context.format === "tsv") {
498
- writeTsv(headers, rows);
499
- if (context.copy) copyToClipboard([headers.join(" "), ...rows.map((r) => r.join(" "))].join("\n"));
500
- return true;
501
- }
502
- return false;
574
+ if (Number.isNaN(date.getTime())) return null;
575
+ return date.toISOString();
503
576
  };
504
- const copyToClipboard = (text) => {
505
- try {
506
- if (process.platform === "darwin") execSync("pbcopy", { input: text });
507
- else if (process.platform === "win32") execSync("clip", { input: text });
508
- else try {
509
- execSync("xclip -selection clipboard", { input: text });
510
- } catch {
511
- execSync("xsel --clipboard --input", { input: text });
512
- }
513
- return true;
514
- } catch {
515
- return false;
516
- }
577
+ const parseDateOnlyString = (value) => {
578
+ if (typeof value !== "string") return null;
579
+ const trimmed = value.trim();
580
+ const match = ISO_DATE_ONLY_REGEX.exec(trimmed);
581
+ if (!match) return null;
582
+ const year = Number.parseInt(match[1] ?? "", 10);
583
+ const month = Number.parseInt(match[2] ?? "", 10);
584
+ const parts = {
585
+ day: Number.parseInt(match[3] ?? "", 10),
586
+ month,
587
+ year
588
+ };
589
+ return isValidDateOnlyParts(parts) ? parts : null;
590
+ };
591
+ const parseApiDateOnlyToEpoch = (value) => {
592
+ const parts = parseDateOnlyString(value);
593
+ return parts ? dateOnlyPartsToEpoch(parts) : null;
517
594
  };
595
+ const parseApiInstantToEpoch = (value) => {
596
+ if (typeof value !== "string") return null;
597
+ const trimmed = value.trim();
598
+ if (trimmed.length === 0 || !ISO_INSTANT_REGEX.test(trimmed)) return null;
599
+ const parsed = Date.parse(trimmed);
600
+ return Number.isNaN(parsed) ? null : parsed;
601
+ };
602
+ const parseCliDateOnlyToEpoch = (value, fieldLabel) => {
603
+ if (!value) return null;
604
+ const parts = parseDateOnlyString(value);
605
+ if (!parts) throw new Error(`Invalid ${fieldLabel}: ${value}. Expected YYYY-MM-DD.`);
606
+ return dateOnlyPartsToEpoch(parts);
607
+ };
608
+ const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
518
609
  //#endregion
519
610
  //#region src/manage-api.ts
520
611
  const TASK_FIELDS = `
@@ -1999,11 +2090,11 @@ Examples:
1999
2090
  });
2000
2091
  });
2001
2092
  };
2093
+ const packageManifest = createRequire(import.meta.url)("../package.json");
2002
2094
  //#endregion
2003
2095
  //#region src/commands/doctor.ts
2004
- const packageManifest$1 = createRequire(import.meta.url)("../package.json");
2005
2096
  const checkCliVersion = async () => {
2006
- const current = packageManifest$1.version;
2097
+ const current = packageManifest.version;
2007
2098
  try {
2008
2099
  const response = await fetch("https://registry.npmjs.org/donebear/latest", { signal: AbortSignal.timeout(5e3) });
2009
2100
  if (!response.ok) return {
@@ -2068,7 +2159,7 @@ const checkAuth = async (context) => {
2068
2159
  };
2069
2160
  const checkWorkspace = async () => {
2070
2161
  try {
2071
- const { ensureLocalContext } = await import("./context-store-CK4yIl9e.mjs");
2162
+ const { ensureLocalContext } = await import("./context-store-ClAx1T2N.mjs");
2072
2163
  const localContext = await ensureLocalContext();
2073
2164
  if (!localContext.workspaceId) return {
2074
2165
  message: "No workspace selected (run `donebear workspace use`)",
@@ -2283,6 +2374,7 @@ const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase(
2283
2374
  //#region src/import/model-id.ts
2284
2375
  const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
2285
2376
  const SYNC_COMPOSITE_ID_NAMESPACE = "92a73695-d772-4b43-9fb4-d79f5fbef300";
2377
+ const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
2286
2378
  const uuidToBytes = (uuid) => {
2287
2379
  const normalized = uuid.replaceAll("-", "");
2288
2380
  if (normalized.length !== 32) throw new Error(`Invalid UUID namespace: ${uuid}`);
@@ -2311,6 +2403,7 @@ const createUuidV5 = (name, namespace) => {
2311
2403
  };
2312
2404
  const createCompositeSyncId = (model, parts) => createUuidV5(`${model}:${JSON.stringify(parts)}`, SYNC_COMPOSITE_ID_NAMESPACE);
2313
2405
  const createDeterministicImportId = (params) => createUuidV5(`things:${params.workspaceId}:${params.modelName}:${params.sourceId}`, IMPORT_ID_NAMESPACE);
2406
+ const createIdempotentMutationId = (params) => createUuidV5(`idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`, IDEMPOTENCY_ID_NAMESPACE);
2314
2407
  const createTaskLabelId = (params) => createCompositeSyncId("TaskLabel", [
2315
2408
  params.workspaceId,
2316
2409
  params.taskId,
@@ -6527,97 +6620,6 @@ Notes:
6527
6620
  });
6528
6621
  };
6529
6622
  //#endregion
6530
- //#region src/commands/interactive.ts
6531
- const HISTORY_FILE = join(CONFIG_DIR, "history.json");
6532
- const MAX_HISTORY = 200;
6533
- const isHistoryFile = (value) => {
6534
- if (typeof value !== "object" || value === null) return false;
6535
- const record = value;
6536
- return record.version === 1 && Array.isArray(record.entries) && record.entries.every((e) => typeof e === "string");
6537
- };
6538
- const loadHistory = async (historyFile) => {
6539
- try {
6540
- const raw = await readFile(historyFile, "utf-8");
6541
- const parsed = JSON.parse(raw);
6542
- if (isHistoryFile(parsed)) return parsed.entries;
6543
- return [];
6544
- } catch {
6545
- return [];
6546
- }
6547
- };
6548
- const tokenize = (input) => {
6549
- const tokens = [];
6550
- let current = "";
6551
- let inQuote = false;
6552
- let quoteChar = "";
6553
- for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
6554
- else current += char;
6555
- else if (char === "\"" || char === "'") {
6556
- inQuote = true;
6557
- quoteChar = char;
6558
- } else if (char === " ") {
6559
- if (current.length > 0) {
6560
- tokens.push(current);
6561
- current = "";
6562
- }
6563
- } else current += char;
6564
- if (current.length > 0) tokens.push(current);
6565
- return tokens;
6566
- };
6567
- const buildCompleter = (program) => (line) => {
6568
- const allCommands = program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()]);
6569
- const hits = allCommands.filter((c) => c.startsWith(line));
6570
- return [hits.length > 0 ? hits : allCommands, line];
6571
- };
6572
- const runInteractiveMode = async (context, program) => {
6573
- const history = await loadHistory(HISTORY_FILE);
6574
- const rl = createInterface({
6575
- completer: buildCompleter(program),
6576
- historySize: MAX_HISTORY,
6577
- input: process.stdin,
6578
- output: process.stdout,
6579
- prompt: "donebear> "
6580
- });
6581
- for (const entry of [...history].toReversed()) rl.history.push(entry);
6582
- writeSuccess(context, "Done Bear interactive mode. Type \"exit\" or Ctrl+D to quit.");
6583
- writeInfo(context, "Commands: task, workspace, project, label, team, today, search, auth, whoami");
6584
- rl.prompt();
6585
- const sessionEntries = [];
6586
- for await (const line of rl) {
6587
- const trimmed = line.trim();
6588
- if (!trimmed) {
6589
- rl.prompt();
6590
- continue;
6591
- }
6592
- if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") break;
6593
- const tokens = tokenize(trimmed);
6594
- try {
6595
- await program.parseAsync([
6596
- "node",
6597
- "donebear",
6598
- ...tokens
6599
- ]);
6600
- } catch {}
6601
- sessionEntries.push(trimmed);
6602
- rl.prompt();
6603
- }
6604
- rl.close();
6605
- if (sessionEntries.length > 0) try {
6606
- const payload = {
6607
- entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY),
6608
- version: 1
6609
- };
6610
- await mkdir(CONFIG_DIR, {
6611
- mode: 448,
6612
- recursive: true
6613
- });
6614
- await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
6615
- encoding: "utf-8",
6616
- mode: 384
6617
- });
6618
- } catch {}
6619
- };
6620
- //#endregion
6621
6623
  //#region src/commands/label.ts
6622
6624
  const sortLabels = (labels, sort) => {
6623
6625
  const sorted = [...labels];
@@ -7091,17 +7093,23 @@ const runProjectAdd = async (context, name, options) => {
7091
7093
  token: commandContext.token,
7092
7094
  workspaceRef: normalizeWorkspaceRef(options.workspace)
7093
7095
  });
7094
- const projectId = randomUUID();
7096
+ const idempotencyId = (part) => options.idempotencyKey ? createIdempotentMutationId({
7097
+ key: options.idempotencyKey,
7098
+ modelName: "Project",
7099
+ part,
7100
+ workspaceId: workspace.id
7101
+ }) : null;
7102
+ const projectId = idempotencyId("model") ?? randomUUID();
7095
7103
  const now = Date.now();
7096
7104
  const key = generateProjectSlug(normalizedName, projectId);
7097
7105
  await mutateModel(context, {
7098
7106
  baseUrl: commandContext.apiBaseUrl,
7099
7107
  request: {
7100
- batchId: randomUUID(),
7108
+ batchId: idempotencyId("batch") ?? randomUUID(),
7101
7109
  transactions: [{
7102
7110
  action: "INSERT",
7103
- clientId: commandContext.localContext.clientId,
7104
- clientTxId: randomUUID(),
7111
+ clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7112
+ clientTxId: idempotencyId("tx") ?? randomUUID(),
7105
7113
  modelId: projectId,
7106
7114
  modelName: "Project",
7107
7115
  payload: {
@@ -7268,7 +7276,7 @@ Agent tip: run "donebear spec project add" for full option reference
7268
7276
  await runProjectShow(context, projectRef, { workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0 });
7269
7277
  });
7270
7278
  });
7271
- project.command("add").alias("create").description("Create a project").argument("[name]", "Project name (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-d, --description <text>", "Project description").option("--target-date <date>", "Target date (YYYY-MM-DD)").action(async (name, options, command) => {
7279
+ project.command("add").alias("create").description("Create a project").argument("[name]", "Project name (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-d, --description <text>", "Project description").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one project)").action(async (name, options, command) => {
7272
7280
  const context = contextFromCommand(command);
7273
7281
  await runWithErrorHandling(context, async () => {
7274
7282
  await runProjectAdd(context, name, {
@@ -7387,8 +7395,8 @@ const filterTasks = (tasks, options) => {
7387
7395
  if (query.length === 0) return viewFiltered;
7388
7396
  return viewFiltered.filter((task) => {
7389
7397
  const title = task.title.toLowerCase();
7390
- const notes = task.description?.toLowerCase() ?? "";
7391
- return title.includes(query) || notes.includes(query);
7398
+ const description = task.description?.toLowerCase() ?? "";
7399
+ return title.includes(query) || description.includes(query);
7392
7400
  });
7393
7401
  };
7394
7402
  const loadTasksForScope = (context, scope, apiOptions) => {
@@ -7469,7 +7477,7 @@ const runTaskShow = async (context, taskRef, options, workspaceOverride) => {
7469
7477
  if (task.projectId) writeInfo(context, `projectId: ${task.projectId}`);
7470
7478
  if (task.teamId) writeInfo(context, `teamId: ${task.teamId}`);
7471
7479
  if (task.assigneeId) writeInfo(context, `assigneeId: ${task.assigneeId}`);
7472
- if (task.description) writeInfo(context, `notes: ${task.description}`);
7480
+ if (task.description) writeInfo(context, `description: ${task.description}`);
7473
7481
  };
7474
7482
  const resolveTitle = async (context, rawTitle) => {
7475
7483
  if (rawTitle) {
@@ -7480,20 +7488,28 @@ const resolveTitle = async (context, rawTitle) => {
7480
7488
  if (prompted) return prompted;
7481
7489
  throw new CliError("Task title is required.", EXIT_CODES.VALIDATION, "Provide a title as an argument: donebear task add \"My task\"");
7482
7490
  };
7483
- const resolveNotes = async (_context, flagNotes) => {
7484
- if (typeof flagNotes === "string") {
7485
- const trimmed = flagNotes.trim();
7486
- return trimmed.length > 0 ? trimmed : null;
7491
+ const resolveDescription = async (context, flagDescription) => {
7492
+ if (typeof flagDescription !== "string") return null;
7493
+ if (flagDescription === "-") {
7494
+ const piped = await readStdin();
7495
+ if (!piped) {
7496
+ writeWarning(context, "No description received on stdin (--description -); task created without a description.");
7497
+ return null;
7498
+ }
7499
+ return piped;
7487
7500
  }
7488
- const stdinContent = await readStdin();
7489
- if (stdinContent) return stdinContent;
7490
- return null;
7501
+ const trimmed = flagDescription.trim();
7502
+ if (trimmed.length === 0) {
7503
+ writeWarning(context, "Empty --description value; task created without a description.");
7504
+ return null;
7505
+ }
7506
+ return trimmed;
7491
7507
  };
7492
7508
  const runTaskAdd = async (context, title, options, workspaceOverride) => {
7493
7509
  const commandContext = await resolveCommandContext(context);
7494
7510
  const userId = await requireUserId(context, commandContext);
7495
7511
  const normalizedTitle = await resolveTitle(context, title);
7496
- const notes = await resolveNotes(context, options.notes);
7512
+ const description = await resolveDescription(context, options.description);
7497
7513
  const { workspace, autoSelected } = await resolveWorkspace(context, {
7498
7514
  apiBaseUrl: commandContext.apiBaseUrl,
7499
7515
  storedWorkspaceId: commandContext.localContext.workspaceId,
@@ -7509,7 +7525,13 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7509
7525
  token: commandContext.token,
7510
7526
  workspaceId: workspace.id
7511
7527
  }).then((teams) => resolveEntityId(teams, options.team, "Team")) : Promise.resolve(null)]);
7512
- const taskId = randomUUID();
7528
+ const idempotencyId = (part) => options.idempotencyKey ? createIdempotentMutationId({
7529
+ key: options.idempotencyKey,
7530
+ modelName: "Task",
7531
+ part,
7532
+ workspaceId: workspace.id
7533
+ }) : null;
7534
+ const taskId = idempotencyId("model") ?? randomUUID();
7513
7535
  const now = Date.now();
7514
7536
  const view = parseTaskView(options.when);
7515
7537
  const startFields = buildTaskStartFields(view);
@@ -7517,11 +7539,11 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7517
7539
  await mutateModel(context, {
7518
7540
  baseUrl: commandContext.apiBaseUrl,
7519
7541
  request: {
7520
- batchId: randomUUID(),
7542
+ batchId: idempotencyId("batch") ?? randomUUID(),
7521
7543
  transactions: [{
7522
7544
  action: "INSERT",
7523
- clientId: commandContext.localContext.clientId,
7524
- clientTxId: randomUUID(),
7545
+ clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7546
+ clientTxId: idempotencyId("tx") ?? randomUUID(),
7525
7547
  modelId: taskId,
7526
7548
  modelName: "Task",
7527
7549
  payload: {
@@ -7530,7 +7552,7 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7530
7552
  createdAt: now,
7531
7553
  creatorId: userId,
7532
7554
  deadlineAt: deadlineEpoch,
7533
- description: notes,
7555
+ description,
7534
7556
  key: taskId.replaceAll("-", "").slice(0, 8),
7535
7557
  projectId,
7536
7558
  sortOrder: 0,
@@ -7550,6 +7572,7 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7550
7572
  autoSelected,
7551
7573
  created: true,
7552
7574
  deadlineAt: deadlineEpoch,
7575
+ description,
7553
7576
  id: taskId,
7554
7577
  projectId,
7555
7578
  teamId,
@@ -7563,6 +7586,7 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7563
7586
  if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
7564
7587
  writeSuccess(context, `Added task: ${normalizedTitle}`);
7565
7588
  writeInfo(context, `id: ${taskId}`);
7589
+ if (description) writeInfo(context, `description: ${description}`);
7566
7590
  };
7567
7591
  const runTaskMutation = async (context, taskRef, payload) => {
7568
7592
  const commandContext = await resolveCommandContext(context);
@@ -7630,7 +7654,7 @@ const resolveTeamId = async (context, commandContext, teamRef, workspaceRef) =>
7630
7654
  }), teamRef, "Team");
7631
7655
  };
7632
7656
  const validateTaskEditOptions = (options) => {
7633
- if (options.clearNotes && typeof options.notes === "string") throw new Error("Use either --notes or --clear-notes, not both.");
7657
+ if (options.clearDescription && typeof options.description === "string") throw new Error("Use either --description or --clear-description, not both.");
7634
7658
  if (options.clearDeadline && typeof options.deadline === "string") throw new Error("Use either --deadline or --clear-deadline, not both.");
7635
7659
  if (options.clearProject && typeof options.project === "string") throw new Error("Use either --project or --clear-project, not both.");
7636
7660
  if (options.clearTeam && typeof options.team === "string") throw new Error("Use either --team or --clear-team, not both.");
@@ -7641,10 +7665,10 @@ const applyScalarEdits = (updates, options) => {
7641
7665
  if (title.length === 0) throw new Error("Task title cannot be empty.");
7642
7666
  updates.title = title;
7643
7667
  }
7644
- if (options.clearNotes) updates.description = null;
7645
- else if (typeof options.notes === "string") {
7646
- const notes = options.notes.trim();
7647
- updates.description = notes.length > 0 ? notes : null;
7668
+ if (options.clearDescription) updates.description = null;
7669
+ else if (typeof options.description === "string") {
7670
+ const description = options.description.trim();
7671
+ updates.description = description.length > 0 ? description : null;
7648
7672
  }
7649
7673
  if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskView(options.when)));
7650
7674
  if (options.clearDeadline) updates.deadlineAt = null;
@@ -7660,7 +7684,7 @@ const runTaskEdit = async (context, taskRef, options, workspaceOverride) => {
7660
7684
  else if (typeof options.project === "string") updates.projectId = await resolveProjectId(context, commandContext, options.project, wsRef);
7661
7685
  if (options.clearTeam) updates.teamId = null;
7662
7686
  else if (typeof options.team === "string") updates.teamId = await resolveTeamId(context, commandContext, options.team, wsRef);
7663
- if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --notes, --clear-notes, --when, --deadline, --clear-deadline, --project, --clear-project, --team, or --clear-team.");
7687
+ if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --description, --clear-description, --when, --deadline, --clear-deadline, --project, --clear-project, --team, or --clear-team.");
7664
7688
  updates.updatedAt = Date.now();
7665
7689
  await runTaskMutation(context, taskRef, {
7666
7690
  action: "UPDATE",
@@ -7767,7 +7791,7 @@ const runTaskRandom = async (context, options, workspaceOverride) => {
7767
7791
  writeInfo(context, `start: ${task.start}`);
7768
7792
  writeInfo(context, `startBucket: ${task.startBucket}`);
7769
7793
  writeInfo(context, `deadline: ${formatDateOnlyEpoch(task.deadlineAt) ?? "(none)"}`);
7770
- if (task.description) writeInfo(context, `notes: ${task.description}`);
7794
+ if (task.description) writeInfo(context, `description: ${task.description}`);
7771
7795
  };
7772
7796
  const resolveChecklistItemReference = async (context, commandContext, taskId, itemRef) => {
7773
7797
  const trimmed = itemRef.trim();
@@ -7838,7 +7862,7 @@ Agent tip: run "donebear spec task add" for full option reference
7838
7862
  }, workspaceOverride);
7839
7863
  });
7840
7864
  });
7841
- task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or notes").option("-v, --view <view>", "Filter by view (inbox|anytime|today|upcoming|someday|logbook|trash)").addHelpText("after", `
7865
+ task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or description").option("-v, --view <view>", "Filter by view (inbox|anytime|today|upcoming|someday|logbook|trash)").addHelpText("after", `
7842
7866
  Examples:
7843
7867
  donebear task list --json
7844
7868
  donebear task list --state done --json
@@ -7859,7 +7883,7 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
7859
7883
  await runTaskShow(context, taskId, options, workspaceOverride);
7860
7884
  });
7861
7885
  });
7862
- task.command("read").description("Print the description (notes) of a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
7886
+ task.command("read").description("Print the description of a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
7863
7887
  const context = contextFromCommand(command);
7864
7888
  await runWithErrorHandling(context, async () => {
7865
7889
  await runTaskRead(context, taskId, options, workspaceOverride);
@@ -7883,15 +7907,15 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
7883
7907
  await runTaskRandom(context, options, workspaceOverride);
7884
7908
  });
7885
7909
  });
7886
- task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --notes <text>", "Task notes (or pipe via stdin)").option("--when <view>", "inbox|anytime|today|upcoming|someday", "inbox").option("--deadline <date>", "Deadline (YYYY-MM-DD or ISO datetime)").option("--project <key-or-id>", "Project key or id").option("--team <key-or-id>", "Team key or id").addHelpText("after", `
7910
+ task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-d, --description <text>", "Task description (use --description - to read from piped stdin)").option("--when <view>", "inbox|anytime|today|upcoming|someday", "inbox").option("--deadline <date>", "Deadline (YYYY-MM-DD or ISO datetime)").option("--project <key-or-id>", "Project key or id").option("--team <key-or-id>", "Team key or id").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one task)").addHelpText("after", `
7887
7911
  Examples:
7888
7912
  donebear task add "Fix login bug" --when today --json
7889
- donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
7890
- donebear task add "Review PR" --when anytime --notes "Check for XSS" --json
7891
- echo "Multi-line notes" | donebear task add "My task"
7913
+ donebear task add "Write tests" --deadline 2030-03-15 --project PROJ --json
7914
+ donebear task add "Review PR" --when anytime --description "Check for XSS" --json
7915
+ echo "Multi-line description" | donebear task add "My task" --description -
7892
7916
  donebear task add # prompts for title interactively
7893
7917
 
7894
- Date format: YYYY-MM-DD (e.g. 2026-03-15)
7918
+ Date format: YYYY-MM-DD (e.g. 2030-03-15)
7895
7919
  View (--when): inbox | anytime | today | upcoming | someday
7896
7920
  `).action(async (title, options, command) => {
7897
7921
  const context = contextFromCommand(command);
@@ -7899,7 +7923,7 @@ View (--when): inbox | anytime | today | upcoming | someday
7899
7923
  await runTaskAdd(context, title, options, workspaceOverride);
7900
7924
  });
7901
7925
  });
7902
- task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--notes <text>", "Replace task notes (empty string clears)").option("--clear-notes", "Clear task notes").option("--when <view>", "Set view bucket (inbox|anytime|today|upcoming|someday)").option("--deadline <date>", "Set deadline (YYYY-MM-DD or ISO datetime)").option("--clear-deadline", "Clear deadline").option("--project <key-or-id>", "Set project (key or id)").option("--clear-project", "Remove project assignment").option("--team <key-or-id>", "Set team (key or id)").option("--clear-team", "Remove team assignment").action(async (taskId, options, command) => {
7926
+ task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--description <text>", "Replace task description (empty string clears)").option("--clear-description", "Clear task description").option("--when <view>", "Set view bucket (inbox|anytime|today|upcoming|someday)").option("--deadline <date>", "Set deadline (YYYY-MM-DD or ISO datetime)").option("--clear-deadline", "Clear deadline").option("--project <key-or-id>", "Set project (key or id)").option("--clear-project", "Remove project assignment").option("--team <key-or-id>", "Set team (key or id)").option("--clear-team", "Remove team assignment").action(async (taskId, options, command) => {
7903
7927
  const context = contextFromCommand(command);
7904
7928
  await runWithErrorHandling(context, async () => {
7905
7929
  await runTaskEdit(context, taskId, options, workspaceOverride);
@@ -8216,7 +8240,7 @@ const runSearch = async (context, query, options, workspaceOverride) => {
8216
8240
  if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
8217
8241
  };
8218
8242
  const registerSearchCommand = (program, workspaceOverride) => {
8219
- program.command("search").description("Search tasks by title or notes").argument("[query]", "Search query (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "all").option("-n, --limit <count>", "Maximum number of results", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_SEARCH_LIMIT).addHelpText("after", `
8243
+ program.command("search").description("Search tasks by title or description").argument("[query]", "Search query (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "all").option("-n, --limit <count>", "Maximum number of results", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_SEARCH_LIMIT).addHelpText("after", `
8220
8244
  Examples:
8221
8245
  donebear search "meeting"
8222
8246
  donebear search "launch" --state open
@@ -8271,9 +8295,15 @@ const OVERVIEW = {
8271
8295
  subcommands: ["things"]
8272
8296
  },
8273
8297
  label: {
8274
- description: "List workspace labels",
8298
+ description: "Manage workspace labels and task label assignments",
8275
8299
  quick_example: "donebear label list --json",
8276
- subcommands: ["list", "show"]
8300
+ subcommands: [
8301
+ "list",
8302
+ "show",
8303
+ "create",
8304
+ "add",
8305
+ "remove"
8306
+ ]
8277
8307
  },
8278
8308
  project: {
8279
8309
  description: "Manage projects",
@@ -8311,6 +8341,7 @@ const OVERVIEW = {
8311
8341
  "read",
8312
8342
  "append",
8313
8343
  "prepend",
8344
+ "random",
8314
8345
  "checklist"
8315
8346
  ]
8316
8347
  },
@@ -8436,10 +8467,21 @@ const COMMAND_SPECS = {
8436
8467
  user: "{ id, email }"
8437
8468
  },
8438
8469
  non_interactive: "For CI/scripts, set DONEBEAR_TOKEN=<token> instead of using auth login.",
8439
- options: { "--no-open": {
8440
- description: "Print auth URL instead of opening browser",
8441
- type: "boolean"
8442
- } },
8470
+ options: {
8471
+ "--no-open": {
8472
+ description: "Print auth URL instead of opening browser",
8473
+ type: "boolean"
8474
+ },
8475
+ "--port": {
8476
+ default: 8787,
8477
+ description: "Loopback callback port for the OAuth redirect",
8478
+ type: "number"
8479
+ },
8480
+ "--timeout": {
8481
+ description: "Seconds to wait for the OAuth callback",
8482
+ type: "number"
8483
+ }
8484
+ },
8443
8485
  usage: "donebear auth login [options]"
8444
8486
  },
8445
8487
  "auth status": {
@@ -8561,6 +8603,10 @@ const COMMAND_SPECS = {
8561
8603
  enum: ["note", "none"],
8562
8604
  type: "string"
8563
8605
  },
8606
+ "--skip-teams": {
8607
+ description: "Skip importing Things Areas as Teams and TeamProject links",
8608
+ type: "boolean"
8609
+ },
8564
8610
  "--workspace": {
8565
8611
  description: "Workspace id, slug, or name",
8566
8612
  type: "string"
@@ -8576,7 +8622,15 @@ const COMMAND_SPECS = {
8576
8622
  ok: "boolean",
8577
8623
  workspace: "WorkspaceSummary"
8578
8624
  },
8579
- options: { "--workspace": { type: "string" } },
8625
+ options: {
8626
+ "--sort": {
8627
+ default: "alpha",
8628
+ description: "Sort order: alpha|count",
8629
+ enum: ["alpha", "count"],
8630
+ type: "string"
8631
+ },
8632
+ "--workspace": { type: "string" }
8633
+ },
8580
8634
  usage: "donebear label list [options]"
8581
8635
  },
8582
8636
  "project add": {
@@ -8595,6 +8649,10 @@ const COMMAND_SPECS = {
8595
8649
  notes: "URL slug is auto-generated from name (e.g. 'Q2 Launch' → 'q2-launch-a1b2c3d4').",
8596
8650
  options: {
8597
8651
  "--description": { type: "string" },
8652
+ "--idempotency-key": {
8653
+ description: "Stable key making retries safe (repeated calls create only one project)",
8654
+ type: "string"
8655
+ },
8598
8656
  "--target-date": {
8599
8657
  description: "Target completion date",
8600
8658
  format: "YYYY-MM-DD",
@@ -8633,8 +8691,25 @@ const COMMAND_SPECS = {
8633
8691
  ok: "boolean"
8634
8692
  },
8635
8693
  options: {
8694
+ "--clear-description": {
8695
+ description: "Clear the project description",
8696
+ type: "boolean"
8697
+ },
8698
+ "--clear-target-date": {
8699
+ description: "Clear the project target date",
8700
+ type: "boolean"
8701
+ },
8636
8702
  "--description": { type: "string" },
8637
8703
  "--name": { type: "string" },
8704
+ "--status": {
8705
+ description: "Set status",
8706
+ enum: [
8707
+ "active",
8708
+ "done",
8709
+ "archived"
8710
+ ],
8711
+ type: "string"
8712
+ },
8638
8713
  "--target-date": {
8639
8714
  format: "YYYY-MM-DD",
8640
8715
  type: "string"
@@ -8712,13 +8787,14 @@ const COMMAND_SPECS = {
8712
8787
  examples: [
8713
8788
  "donebear task add \"Fix login bug\" --when today --json",
8714
8789
  "donebear task add \"Write tests\" --deadline 2026-03-15 --project PROJ --json",
8715
- "donebear task add \"Review PR\" --when anytime --notes \"Triage for bugs\" --json",
8716
- "echo \"Multi-line notes\" | donebear task add \"My task\"",
8790
+ "donebear task add \"Review PR\" --when anytime --description \"Triage for bugs\" --json",
8791
+ "echo \"Multi-line description\" | donebear task add \"My task\" --description -",
8717
8792
  "donebear task add # prompts for title interactively in TTY"
8718
8793
  ],
8719
8794
  json_output: {
8720
8795
  created: "boolean",
8721
8796
  deadlineAt: "number | null",
8797
+ description: "string | null",
8722
8798
  id: "string (UUID)",
8723
8799
  projectId: "string | null",
8724
8800
  teamId: "string | null",
@@ -8727,15 +8803,19 @@ const COMMAND_SPECS = {
8727
8803
  workspaceId: "string",
8728
8804
  workspaceName: "string"
8729
8805
  },
8730
- notes: "Title is prompted interactively when omitted in TTY. Notes can be piped via stdin. Task id accepts full UUID or prefix of >= 4 chars in all other task subcommands.",
8806
+ notes: "Title is prompted interactively when omitted in TTY. The description is NOT read from stdin unless you pass --description - explicitly; without --description the task is created with no description (and the saved description is echoed back in the output). Task id accepts full UUID or prefix of >= 4 chars in all other task subcommands.",
8731
8807
  options: {
8732
8808
  "--deadline": {
8733
8809
  description: "Due date",
8734
8810
  format: "YYYY-MM-DD",
8735
8811
  type: "string"
8736
8812
  },
8737
- "--notes": {
8738
- description: "Body / description text (or pipe via stdin)",
8813
+ "--description": {
8814
+ description: "Body / description text. Pass --description - to read from piped stdin.",
8815
+ type: "string"
8816
+ },
8817
+ "--idempotency-key": {
8818
+ description: "Stable key making retries safe (repeated calls create only one task)",
8739
8819
  type: "string"
8740
8820
  },
8741
8821
  "--project": {
@@ -8778,7 +8858,7 @@ const COMMAND_SPECS = {
8778
8858
  }
8779
8859
  },
8780
8860
  command: "task append",
8781
- description: "Append text to task notes",
8861
+ description: "Append text to a task's description",
8782
8862
  examples: ["donebear task append abc1 \"Additional context\" --json"],
8783
8863
  json_output: {
8784
8864
  id: "string",
@@ -8824,20 +8904,40 @@ const COMMAND_SPECS = {
8824
8904
  type: "string"
8825
8905
  } },
8826
8906
  command: "task edit",
8827
- examples: ["donebear task edit abc1 --title \"Updated title\" --json", "donebear task edit abc1 --when today --deadline 2026-03-20 --json"],
8907
+ examples: [
8908
+ "donebear task edit abc1 --title \"Updated title\" --json",
8909
+ "donebear task edit abc1 --when today --deadline 2030-01-15 --json",
8910
+ "donebear task edit abc1 --clear-deadline --json"
8911
+ ],
8828
8912
  json_output: {
8829
8913
  action: "string",
8830
8914
  id: "string",
8831
8915
  ok: "boolean"
8832
8916
  },
8833
8917
  options: {
8918
+ "--clear-deadline": {
8919
+ description: "Clear the due date",
8920
+ type: "boolean"
8921
+ },
8922
+ "--clear-description": {
8923
+ description: "Clear the description",
8924
+ type: "boolean"
8925
+ },
8926
+ "--clear-project": {
8927
+ description: "Remove the task from its project",
8928
+ type: "boolean"
8929
+ },
8930
+ "--clear-team": {
8931
+ description: "Remove the task from its team",
8932
+ type: "boolean"
8933
+ },
8834
8934
  "--deadline": {
8835
- description: "Set due date (pass empty string to clear)",
8935
+ description: "Set due date",
8836
8936
  format: "YYYY-MM-DD",
8837
8937
  type: "string"
8838
8938
  },
8839
- "--notes": {
8840
- description: "Replace notes/description",
8939
+ "--description": {
8940
+ description: "Replace the task description",
8841
8941
  type: "string"
8842
8942
  },
8843
8943
  "--project": {
@@ -8935,7 +9035,7 @@ const COMMAND_SPECS = {
8935
9035
  }
8936
9036
  },
8937
9037
  command: "task prepend",
8938
- description: "Prepend text to task notes",
9038
+ description: "Prepend text to a task's description",
8939
9039
  examples: ["donebear task prepend abc1 \"URGENT: \" --json"],
8940
9040
  json_output: {
8941
9041
  id: "string",
@@ -8951,7 +9051,7 @@ const COMMAND_SPECS = {
8951
9051
  type: "string"
8952
9052
  } },
8953
9053
  command: "task read",
8954
- description: "Print task description/notes as plain text",
9054
+ description: "Print task description as plain text",
8955
9055
  examples: ["donebear task read abc1"],
8956
9056
  options: { "--workspace": { type: "string" } },
8957
9057
  usage: "donebear task read <id> [options]"
@@ -9098,9 +9198,12 @@ const TOP_LEVEL_SPECS = {
9098
9198
  },
9099
9199
  label: {
9100
9200
  command: "label",
9101
- description: "List workspace labels",
9201
+ description: "Manage workspace labels and task label assignments",
9102
9202
  subcommands: {
9203
+ add: "Add a label to a task",
9204
+ create: "Create a new label",
9103
9205
  list: "List all labels",
9206
+ remove: "Remove a label from a task",
9104
9207
  show: "Get label details"
9105
9208
  }
9106
9209
  },
@@ -9124,14 +9227,15 @@ const TOP_LEVEL_SPECS = {
9124
9227
  notes: "Task id accepts full UUID or a prefix of >= 4 chars.",
9125
9228
  subcommands: {
9126
9229
  add: "Create a new task",
9127
- append: "Append text to task notes",
9230
+ append: "Append text to a task's description",
9128
9231
  archive: "Archive a task",
9129
9232
  checklist: "Manage task checklist items",
9130
9233
  done: "Mark a task as complete",
9131
9234
  edit: "Update task fields",
9132
9235
  list: "List tasks (--state, --view, --search, --limit)",
9133
- prepend: "Prepend text to task notes",
9134
- read: "Print task notes as plain text",
9236
+ prepend: "Prepend text to a task's description",
9237
+ random: "Pick a random task (--state to filter)",
9238
+ read: "Print task description as plain text",
9135
9239
  reopen: "Reopen a completed task",
9136
9240
  show: "Get task details by id",
9137
9241
  unarchive: "Restore an archived task"
@@ -9757,30 +9861,8 @@ Examples:
9757
9861
  });
9758
9862
  };
9759
9863
  //#endregion
9760
- //#region src/cli.ts
9761
- const packageManifest = createRequire(import.meta.url)("../package.json");
9762
- const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/iu;
9763
- const extractWorkspaceTarget = (argv) => {
9764
- const processedArgv = [...argv];
9765
- const firstArg = processedArgv[2];
9766
- if (firstArg) {
9767
- const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
9768
- if (match?.[1]) {
9769
- processedArgv.splice(2, 1);
9770
- return {
9771
- processedArgv,
9772
- workspaceOverride: match[1].trim()
9773
- };
9774
- }
9775
- }
9776
- return {
9777
- processedArgv,
9778
- workspaceOverride: null
9779
- };
9780
- };
9781
- const main = async () => {
9782
- loadCliEnvironmentFiles();
9783
- const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
9864
+ //#region src/program.ts
9865
+ const buildProgram = (workspaceOverride = null) => {
9784
9866
  const program = new Command();
9785
9867
  program.name(CLI_NAME).description("Done Bear CLI for auth, workspace, and task workflows").version(packageManifest.version, "-v, --version").showHelpAfterError().showSuggestionAfterError();
9786
9868
  addGlobalOptions(program);
@@ -9824,6 +9906,33 @@ Filtering & output:
9824
9906
  registerSearchCommand(program, workspaceOverride);
9825
9907
  registerHistoryCommand(program, workspaceOverride);
9826
9908
  registerWhoAmICommand(program);
9909
+ return program;
9910
+ };
9911
+ //#endregion
9912
+ //#region src/cli.ts
9913
+ const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/iu;
9914
+ const extractWorkspaceTarget = (argv) => {
9915
+ const processedArgv = [...argv];
9916
+ const firstArg = processedArgv[2];
9917
+ if (firstArg) {
9918
+ const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
9919
+ if (match?.[1]) {
9920
+ processedArgv.splice(2, 1);
9921
+ return {
9922
+ processedArgv,
9923
+ workspaceOverride: match[1].trim()
9924
+ };
9925
+ }
9926
+ }
9927
+ return {
9928
+ processedArgv,
9929
+ workspaceOverride: null
9930
+ };
9931
+ };
9932
+ const main = async () => {
9933
+ loadCliEnvironmentFiles();
9934
+ const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
9935
+ const program = buildProgram(workspaceOverride);
9827
9936
  const hasUserArgs = processedArgv.length <= 2;
9828
9937
  const isTTY = process.stdin.isTTY === true;
9829
9938
  if (hasUserArgs && isTTY) {