donebear 0.3.7 → 0.3.9

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,30 +1,261 @@
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-DH561Z1u.mjs";
3
- import { n as readStdin, t as promptForValue } from "./prompt-BrfcK2Ry.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
+ 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
- var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
21
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
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
+ };
22
253
  //#endregion
23
254
  //#region src/jwt.ts
24
255
  const parseJwtBase64Url = (input) => {
25
256
  const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
26
257
  const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
27
- return Buffer.from(padded, "base64").toString("utf8");
258
+ return Buffer.from(padded, "base64").toString("utf-8");
28
259
  };
29
260
  const parseJwtClaims = (token) => {
30
261
  const payloadPart = token.split(".").at(1);
@@ -115,7 +346,7 @@ const parseAuthFile = (payload) => {
115
346
  };
116
347
  const readStoredAuthSession = async () => {
117
348
  try {
118
- return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf8")).session;
349
+ return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf-8")).session;
119
350
  } catch (error) {
120
351
  if (isRecord(error) && error.code === "ENOENT") return null;
121
352
  if (error instanceof CliError) throw error;
@@ -132,7 +363,7 @@ const writeStoredAuthSession = async (session) => {
132
363
  recursive: true
133
364
  });
134
365
  await writeFile(AUTH_FILE_PATH, `${JSON.stringify(payload, null, 2)}\n`, {
135
- encoding: "utf8",
366
+ encoding: "utf-8",
136
367
  mode: 384
137
368
  });
138
369
  };
@@ -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";
@@ -199,19 +402,6 @@ const tokenResponseToStoredSession = (response) => {
199
402
  } : null
200
403
  };
201
404
  };
202
- const getSupabaseUser = async (supabaseUrl, publishableKey, accessToken) => {
203
- const response = await fetch(`${supabaseUrl}/auth/v1/user`, { headers: {
204
- apikey: publishableKey,
205
- authorization: `Bearer ${accessToken}`
206
- } });
207
- if (!response.ok) throw new CliError(`Failed to load user (${response.status})`, EXIT_CODES.ERROR);
208
- const data = await response.json();
209
- if (typeof data.id !== "string") throw new CliError("No user returned for current token", EXIT_CODES.ERROR);
210
- return {
211
- email: typeof data.email === "string" ? data.email : null,
212
- id: data.id
213
- };
214
- };
215
405
  //#endregion
216
406
  //#region src/auth-session.ts
217
407
  const expiryFromClaims = (claims) => {
@@ -346,8 +536,8 @@ const requestNdjson = async (context, endpoint, options) => {
346
536
  };
347
537
  //#endregion
348
538
  //#region src/date-codecs.ts
349
- const ISO_DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
350
- const ISO_INSTANT_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
539
+ const ISO_DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/u;
540
+ const ISO_INSTANT_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/u;
351
541
  const pad = (value, length = 2) => String(value).padStart(length, "0");
352
542
  const isFiniteEpoch = (value) => typeof value === "number" && Number.isFinite(value);
353
543
  const isValidDateOnlyParts = (parts) => {
@@ -417,118 +607,6 @@ const parseCliDateOnlyToEpoch = (value, fieldLabel) => {
417
607
  };
418
608
  const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
419
609
  //#endregion
420
- //#region src/output.ts
421
- const writeLine = (stream, message) => {
422
- stream.write(`${message}\n`);
423
- };
424
- const colorize = (context, format, message) => {
425
- if (!context.color) return message;
426
- return styleText(format, message);
427
- };
428
- const writeJson = (value) => {
429
- writeLine(process.stdout, JSON.stringify(value, null, 2));
430
- };
431
- const writeYaml = (value) => {
432
- process.stdout.write(stringify(value));
433
- };
434
- const writeInfo = (context, message) => {
435
- writeLine(process.stdout, colorize(context, "cyan", message));
436
- };
437
- const writeSuccess = (context, message) => {
438
- writeLine(process.stdout, colorize(context, "green", message));
439
- };
440
- const writeWarning = (context, message) => {
441
- writeLine(process.stderr, colorize(context, "yellow", message));
442
- };
443
- const writeError = (context, message) => {
444
- writeLine(process.stderr, colorize(context, "red", message));
445
- };
446
- const writeDebug = (context, message) => {
447
- if (!context.debug) return;
448
- writeLine(process.stderr, colorize(context, "gray", `[debug] ${message}`));
449
- };
450
- const writeTotal = (context, count) => {
451
- writeLine(process.stdout, colorize(context, "cyan", String(count)));
452
- };
453
- const escapeCsvField = (value) => {
454
- if (value.includes(",") || value.includes("\"") || value.includes("\n")) return `"${value.replaceAll("\"", "\"\"")}"`;
455
- return value;
456
- };
457
- const writeCsv = (headers, rows) => {
458
- writeLine(process.stdout, headers.map(escapeCsvField).join(","));
459
- for (const row of rows) writeLine(process.stdout, row.map(escapeCsvField).join(","));
460
- };
461
- const writeTsv = (headers, rows) => {
462
- writeLine(process.stdout, headers.join(" "));
463
- for (const row of rows) writeLine(process.stdout, row.join(" "));
464
- };
465
- /**
466
- * Write structured data to stdout, applying --jq filter and --limit if set.
467
- * Use this for all JSON/YAML/structured output from commands.
468
- */
469
- const writeOutput = (context, data) => {
470
- let result = data;
471
- if (context.jq) try {
472
- result = jmespath.search(result, context.jq);
473
- } catch (error) {
474
- throw new CliError(`Invalid --jq expression: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.VALIDATION);
475
- }
476
- if (context.limit !== null && Array.isArray(result)) result = result.slice(0, context.limit);
477
- switch (context.format) {
478
- case "yaml":
479
- writeYaml(result);
480
- break;
481
- case "json":
482
- writeJson(result);
483
- break;
484
- default:
485
- writeJson(result);
486
- break;
487
- }
488
- if (context.copy) copyToClipboard(JSON.stringify(result, null, 2));
489
- };
490
- /**
491
- * Handles --total, --format csv, --format tsv, --format yaml output.
492
- * Returns true if the format was handled; caller renders text mode if false.
493
- */
494
- const writeFormattedRows = (context, totalCount, headers, rows) => {
495
- if (context.total) {
496
- writeTotal(context, totalCount);
497
- return true;
498
- }
499
- if (context.format === "yaml") {
500
- const objects = rows.map((row) => Object.fromEntries(headers.map((h, i) => [h, row[i]])));
501
- writeYaml(objects);
502
- if (context.copy) copyToClipboard(stringify(objects));
503
- return true;
504
- }
505
- if (context.format === "csv") {
506
- writeCsv(headers, rows);
507
- if (context.copy) copyToClipboard([headers.join(","), ...rows.map((r) => r.join(","))].join("\n"));
508
- return true;
509
- }
510
- if (context.format === "tsv") {
511
- writeTsv(headers, rows);
512
- if (context.copy) copyToClipboard([headers.join(" "), ...rows.map((r) => r.join(" "))].join("\n"));
513
- return true;
514
- }
515
- return false;
516
- };
517
- const copyToClipboard = (text) => {
518
- try {
519
- if (process.platform === "darwin") execSync("pbcopy", { input: text });
520
- else if (process.platform === "win32") execSync("clip", { input: text });
521
- else try {
522
- execSync("xclip -selection clipboard", { input: text });
523
- } catch {
524
- execSync("xsel --clipboard --input", { input: text });
525
- }
526
- return true;
527
- } catch {
528
- return false;
529
- }
530
- };
531
- //#endregion
532
610
  //#region src/manage-api.ts
533
611
  const TASK_FIELDS = `
534
612
  id
@@ -681,17 +759,17 @@ const WORKSPACE_HISTORY_QUERY = `
681
759
  }
682
760
  `;
683
761
  const API_KEY_CREATE_MUTATION = `
684
- mutation ApiKeyCreate($label: String!) {
685
- apiKeyCreate(label: $label) {
762
+ mutation ApiKeyCreate($label: String!, $scopes: [String!], $workspaceIds: [ID!], $expiresAt: DateTime) {
763
+ apiKeyCreate(label: $label, scopes: $scopes, workspaceIds: $workspaceIds, expiresAt: $expiresAt) {
686
764
  success lastSyncId
687
- apiKey { id label keyPrefix createdAt }
765
+ apiKey { id label keyPrefix scopes workspaceIds expiresAt createdAt }
688
766
  token
689
767
  }
690
768
  }
691
769
  `;
692
770
  const API_KEYS_QUERY = `
693
771
  query ApiKeys {
694
- apiKeys { id label keyPrefix lastUsedAt revokedAt createdAt }
772
+ apiKeys { id label keyPrefix scopes workspaceIds expiresAt lastUsedAt revokedAt createdAt }
695
773
  }
696
774
  `;
697
775
  const API_KEY_REVOKE_MUTATION = `
@@ -875,11 +953,14 @@ const toWorkspaceHistoryEntryFromGraphqlNode = (node) => ({
875
953
  });
876
954
  const toApiKeyRecordFromGraphqlNode = (node) => ({
877
955
  createdAt: node.createdAt,
956
+ expiresAt: node.expiresAt,
878
957
  id: node.id,
879
958
  keyPrefix: node.keyPrefix,
880
959
  label: node.label,
881
960
  lastUsedAt: node.lastUsedAt,
882
- revokedAt: node.revokedAt
961
+ revokedAt: node.revokedAt,
962
+ scopes: node.scopes,
963
+ workspaceIds: node.workspaceIds
883
964
  });
884
965
  const toChecklistItemRecord = (value) => {
885
966
  if (typeof value !== "object" || value === null) return null;
@@ -898,11 +979,31 @@ const toChecklistItemRecord = (value) => {
898
979
  workspaceId: row.workspaceId
899
980
  };
900
981
  };
901
- const resolveApiBaseUrl = (context) => {
902
- const fromContext = context.apiUrlOverride?.trim();
903
- if (fromContext) return normalizeApiUrl(fromContext);
904
- return normalizeApiUrl(readEnvValue("DONEBEAR_API_URL") ?? "https://api.donebear.com");
905
- };
982
+ const resolveApiBaseUrl = (context) => {
983
+ const fromContext = context.apiUrlOverride?.trim();
984
+ if (fromContext) return normalizeApiUrl(fromContext);
985
+ return normalizeApiUrl(readEnvValue("DONEBEAR_API_URL") ?? "https://api.donebear.com");
986
+ };
987
+ const VIEWER_QUERY = `
988
+ query Viewer {
989
+ viewer {
990
+ id
991
+ email
992
+ }
993
+ }
994
+ `;
995
+ const loadViewer = async (context, options) => {
996
+ const data = await requestGraphql(context, `${options.baseUrl}/graphql`, {
997
+ query: VIEWER_QUERY,
998
+ token: options.token,
999
+ variables: {}
1000
+ });
1001
+ if (!data.viewer) throw new CliError("Could not resolve current user. The token may be invalid or revoked.", EXIT_CODES.AUTH_REQUIRED);
1002
+ return {
1003
+ email: data.viewer.email ?? null,
1004
+ id: data.viewer.id
1005
+ };
1006
+ };
906
1007
  const listWorkspaces = async (context, options) => {
907
1008
  return (await requestGraphql(context, `${options.baseUrl}/graphql`, {
908
1009
  query: MY_WORKSPACES_QUERY,
@@ -1089,10 +1190,15 @@ const listWorkspaceHistory = async (context, options) => {
1089
1190
  })).workspaceHistory.map(toWorkspaceHistoryEntryFromGraphqlNode);
1090
1191
  };
1091
1192
  const createApiKey = async (context, options) => {
1092
- const data = await requestGraphql(context, `${options.baseUrl}/graphql`, {
1193
+ const endpoint = `${options.baseUrl}/graphql`;
1194
+ const variables = { label: options.label };
1195
+ if (options.scopes !== void 0) variables.scopes = options.scopes;
1196
+ if (options.workspaceIds !== void 0) variables.workspaceIds = options.workspaceIds;
1197
+ if (options.expiresAt !== void 0) variables.expiresAt = options.expiresAt;
1198
+ const data = await requestGraphql(context, endpoint, {
1093
1199
  query: API_KEY_CREATE_MUTATION,
1094
1200
  token: options.token,
1095
- variables: { label: options.label }
1201
+ variables
1096
1202
  });
1097
1203
  return {
1098
1204
  ...toApiKeyRecordFromGraphqlNode(data.apiKeyCreate.apiKey),
@@ -1140,10 +1246,13 @@ const resolveCommandContext = async (context) => {
1140
1246
  token
1141
1247
  };
1142
1248
  };
1143
- const requireUserId = (resolvedToken) => {
1144
- const userId = resolvedToken.user?.id ?? null;
1145
- if (!(typeof userId === "string" && userId.length > 0)) throw new CliError("Could not determine user id from auth token", EXIT_CODES.AUTH_REQUIRED);
1146
- return userId;
1249
+ const requireUserId = async (context, commandContext) => {
1250
+ const claimUserId = commandContext.resolvedToken.user?.id ?? null;
1251
+ if (typeof claimUserId === "string" && claimUserId.length > 0) return claimUserId;
1252
+ return (await loadViewer(context, {
1253
+ baseUrl: commandContext.apiBaseUrl,
1254
+ token: commandContext.token
1255
+ })).id;
1147
1256
  };
1148
1257
  //#endregion
1149
1258
  //#region src/options.ts
@@ -1238,20 +1347,68 @@ const runWithErrorHandling = async (context, action) => {
1238
1347
  };
1239
1348
  //#endregion
1240
1349
  //#region src/commands/api-key.ts
1241
- const runCreate = async (context, label) => {
1350
+ const PERMISSION_SCOPES = {
1351
+ "full-access": [
1352
+ "read",
1353
+ "write",
1354
+ "admin"
1355
+ ],
1356
+ "read-only": ["read"]
1357
+ };
1358
+ const parseDuration = (duration) => {
1359
+ const match = duration.match(/^(\d+)(d|y)$/u);
1360
+ if (!match) throw new CliError(`Invalid duration "${duration}". Use a format like "30d", "90d", or "1y".`, EXIT_CODES.VALIDATION);
1361
+ const [, rawAmount, unit] = match;
1362
+ const amount = Number(rawAmount);
1363
+ const now = /* @__PURE__ */ new Date();
1364
+ if (unit === "d") now.setDate(now.getDate() + amount);
1365
+ else now.setDate(now.getDate() + amount * 365);
1366
+ return now;
1367
+ };
1368
+ const runCreate = async (context, label, options) => {
1242
1369
  const { token } = await requireAuthToken(context);
1370
+ const baseUrl = resolveApiBaseUrl(context);
1371
+ let scopes;
1372
+ if (options.permissions !== void 0) {
1373
+ const level = options.permissions;
1374
+ if (!PERMISSION_SCOPES[level]) throw new CliError(`Invalid permissions "${options.permissions}". Use "full-access" or "read-only".`, EXIT_CODES.VALIDATION);
1375
+ scopes = PERMISSION_SCOPES[level];
1376
+ }
1377
+ let workspaceIds;
1378
+ if (options.workspaces !== void 0) {
1379
+ const slugs = options.workspaces.split(",").map((s) => s.trim()).filter(Boolean);
1380
+ if (slugs.length === 0) throw new CliError("No workspace slugs provided for --workspaces.", EXIT_CODES.VALIDATION);
1381
+ const allWorkspaces = await listWorkspaces(context, {
1382
+ baseUrl,
1383
+ token
1384
+ });
1385
+ workspaceIds = [];
1386
+ for (const slug of slugs) {
1387
+ const match = allWorkspaces.find((ws) => ws.urlKey === slug || ws.id === slug || ws.name.toLowerCase() === slug.toLowerCase());
1388
+ if (!match) throw new CliError(`Workspace "${slug}" not found. Run \`donebear workspace list\` to see available workspaces.`, EXIT_CODES.VALIDATION);
1389
+ workspaceIds.push(match.id);
1390
+ }
1391
+ }
1392
+ let expiresAt;
1393
+ if (options.expires !== void 0) expiresAt = parseDuration(options.expires).toISOString();
1243
1394
  const key = await createApiKey(context, {
1244
- baseUrl: resolveApiBaseUrl(context),
1395
+ baseUrl,
1396
+ expiresAt,
1245
1397
  label,
1246
- token
1398
+ scopes,
1399
+ token,
1400
+ workspaceIds
1247
1401
  });
1248
1402
  if (context.json) {
1249
1403
  writeOutput(context, {
1250
1404
  createdAt: key.createdAt,
1405
+ expiresAt: key.expiresAt,
1251
1406
  id: key.id,
1252
1407
  keyPrefix: key.keyPrefix,
1253
1408
  label: key.label,
1254
- token: key.token
1409
+ scopes: key.scopes,
1410
+ token: key.token,
1411
+ workspaceIds: key.workspaceIds
1255
1412
  });
1256
1413
  return;
1257
1414
  }
@@ -1259,6 +1416,10 @@ const runCreate = async (context, label) => {
1259
1416
  writeInfo(context, "");
1260
1417
  writeInfo(context, ` ${key.token}`);
1261
1418
  writeInfo(context, "");
1419
+ writeInfo(context, ` Scopes: ${key.scopes.join(", ")}`);
1420
+ if (key.workspaceIds && key.workspaceIds.length > 0) writeInfo(context, ` Restricted to workspaces: ${key.workspaceIds.join(", ")}`);
1421
+ if (key.expiresAt) writeInfo(context, ` Expires: ${new Date(key.expiresAt).toLocaleDateString()}`);
1422
+ writeInfo(context, "");
1262
1423
  writeWarning(context, "Save this key now — it will not be shown again.");
1263
1424
  };
1264
1425
  const runList = async (context) => {
@@ -1278,7 +1439,10 @@ const runList = async (context) => {
1278
1439
  for (const key of keys) {
1279
1440
  const status = key.revokedAt ? " (revoked)" : "";
1280
1441
  const lastUsed = key.lastUsedAt ? `Last used: ${new Date(key.lastUsedAt).toLocaleDateString()}` : "Never used";
1281
- writeInfo(context, `${key.keyPrefix}... ${key.label} ${lastUsed}${status} [${key.id}]`);
1442
+ const scopeStr = `[${key.scopes.join(",")}]`;
1443
+ const wsStr = key.workspaceIds && key.workspaceIds.length > 0 ? ` ws:${key.workspaceIds.length}` : "";
1444
+ const expStr = key.expiresAt ? ` exp:${new Date(key.expiresAt).toLocaleDateString()}` : "";
1445
+ writeInfo(context, `${key.keyPrefix}... ${key.label} ${scopeStr}${wsStr}${expStr} ${lastUsed}${status} [${key.id}]`);
1282
1446
  }
1283
1447
  };
1284
1448
  const runRevoke = async (context, keyId) => {
@@ -1301,6 +1465,8 @@ const registerApiKeyCommands = (program) => {
1301
1465
  const apiKey = program.command("api-key").alias("key").description("Manage personal API keys").addHelpText("after", `
1302
1466
  Examples:
1303
1467
  donebear api-key create "Raycast"
1468
+ donebear api-key create "CI" --permissions read-only --expires 90d
1469
+ donebear api-key create "Scoped" --workspaces my-org,side-project --expires 1y
1304
1470
  donebear api-key list
1305
1471
  donebear api-key revoke <key-id>
1306
1472
  `).action(async (_options, command) => {
@@ -1309,10 +1475,10 @@ Examples:
1309
1475
  await runList(context);
1310
1476
  });
1311
1477
  });
1312
- apiKey.command("create <label>").description("Create a new personal API key").action(async (label, _options, command) => {
1478
+ apiKey.command("create <label>").description("Create a new personal API key").option("--permissions <value>", "Permission level: \"full-access\" or \"read-only\"", "full-access").option("--workspaces <slugs>", "Comma-separated workspace slugs to restrict the key to").option("--expires <duration>", "Expiration duration, e.g. \"30d\", \"90d\", \"1y\"").action(async (label, options, command) => {
1313
1479
  const context = contextFromCommand(command);
1314
1480
  await runWithErrorHandling(context, async () => {
1315
- await runCreate(context, label);
1481
+ await runCreate(context, label, options);
1316
1482
  });
1317
1483
  });
1318
1484
  apiKey.command("list").description("List all personal API keys").action(async (_options, command) => {
@@ -1669,7 +1835,7 @@ const resolveTaskView = (task) => {
1669
1835
  const formatEpochDate = (epochMs) => formatInstantEpoch(epochMs);
1670
1836
  //#endregion
1671
1837
  //#region src/command-helpers.ts
1672
- const SHORT_ID_SUFFIX_PATTERN = /-([a-f0-9]{8})$/;
1838
+ const SHORT_ID_SUFFIX_PATTERN = /-([a-f0-9]{8})$/u;
1673
1839
  /**
1674
1840
  * Find a record by reference: exact ID, key, ID prefix, slug shortId, or name.
1675
1841
  * Works for any entity with id, key, and name fields.
@@ -1723,7 +1889,7 @@ const taskListLines = (tasks) => tasks.map((task) => {
1723
1889
  });
1724
1890
  //#endregion
1725
1891
  //#region src/workspace-context.ts
1726
- const WORKSPACE_URL_KEY_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1892
+ const WORKSPACE_URL_KEY_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1727
1893
  const fetchWorkspaces = async (context, options) => {
1728
1894
  const workspaces = await listWorkspaces(context, {
1729
1895
  baseUrl: options.apiBaseUrl,
@@ -1782,7 +1948,7 @@ const byReference = (workspaces, reference) => {
1782
1948
  return null;
1783
1949
  };
1784
1950
  const toWorkspaceUrlKey = (name) => {
1785
- return name.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").replaceAll(/-{2,}/g, "-").slice(0, 50);
1951
+ return name.trim().toLowerCase().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-+|-+$/gu, "").replaceAll(/-{2,}/gu, "-").slice(0, 50);
1786
1952
  };
1787
1953
  const isValidWorkspaceUrlKey = (value) => WORKSPACE_URL_KEY_REGEX.test(value) && value.length >= 3;
1788
1954
  const resolveWorkspace = async (context, options) => {
@@ -1924,11 +2090,11 @@ Examples:
1924
2090
  });
1925
2091
  });
1926
2092
  };
2093
+ const packageManifest = createRequire(import.meta.url)("../package.json");
1927
2094
  //#endregion
1928
2095
  //#region src/commands/doctor.ts
1929
- const packageManifest$1 = createRequire(import.meta.url)("../package.json");
1930
2096
  const checkCliVersion = async () => {
1931
- const current = packageManifest$1.version;
2097
+ const current = packageManifest.version;
1932
2098
  try {
1933
2099
  const response = await fetch("https://registry.npmjs.org/donebear/latest", { signal: AbortSignal.timeout(5e3) });
1934
2100
  if (!response.ok) return {
@@ -1993,7 +2159,7 @@ const checkAuth = async (context) => {
1993
2159
  };
1994
2160
  const checkWorkspace = async () => {
1995
2161
  try {
1996
- const { ensureLocalContext } = await import("./context-store-D0Z9vbaU.mjs");
2162
+ const { ensureLocalContext } = await import("./context-store-ClAx1T2N.mjs");
1997
2163
  const localContext = await ensureLocalContext();
1998
2164
  if (!localContext.workspaceId) return {
1999
2165
  message: "No workspace selected (run `donebear workspace use`)",
@@ -2208,6 +2374,7 @@ const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase(
2208
2374
  //#region src/import/model-id.ts
2209
2375
  const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
2210
2376
  const SYNC_COMPOSITE_ID_NAMESPACE = "92a73695-d772-4b43-9fb4-d79f5fbef300";
2377
+ const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
2211
2378
  const uuidToBytes = (uuid) => {
2212
2379
  const normalized = uuid.replaceAll("-", "");
2213
2380
  if (normalized.length !== 32) throw new Error(`Invalid UUID namespace: ${uuid}`);
@@ -2225,7 +2392,7 @@ const bytesToUuid = (bytes) => {
2225
2392
  };
2226
2393
  const createUuidV5 = (name, namespace) => {
2227
2394
  const namespaceBytes = uuidToBytes(namespace);
2228
- const hash = createHash("sha1").update(namespaceBytes).update(Buffer.from(name, "utf8")).digest();
2395
+ const hash = createHash("sha1").update(namespaceBytes).update(Buffer.from(name, "utf-8")).digest();
2229
2396
  const bytes = Uint8Array.from(hash.subarray(0, 16));
2230
2397
  const versionByte = bytes.at(6);
2231
2398
  const variantByte = bytes.at(8);
@@ -2236,6 +2403,7 @@ const createUuidV5 = (name, namespace) => {
2236
2403
  };
2237
2404
  const createCompositeSyncId = (model, parts) => createUuidV5(`${model}:${JSON.stringify(parts)}`, SYNC_COMPOSITE_ID_NAMESPACE);
2238
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);
2239
2407
  const createTaskLabelId = (params) => createCompositeSyncId("TaskLabel", [
2240
2408
  params.workspaceId,
2241
2409
  params.taskId,
@@ -2665,7 +2833,6 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
2665
2833
  };
2666
2834
  return __assign.apply(this, arguments);
2667
2835
  };
2668
- Object.create;
2669
2836
  function __spreadArray(to, from, pack) {
2670
2837
  if (pack || arguments.length === 2) {
2671
2838
  for (var i = 0, l = from.length, ar; i < l; i++) if (ar || !(i in from)) {
@@ -2678,7 +2845,6 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
2678
2845
  function __await(v) {
2679
2846
  return this instanceof __await ? (this.v = v, this) : new __await(v);
2680
2847
  }
2681
- Object.create;
2682
2848
  const callbackiterresult = function(_super) {
2683
2849
  __extends(CallbackIterResult, _super);
2684
2850
  function CallbackIterResult(method, args, iterator) {
@@ -3515,7 +3681,8 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
3515
3681
  this.month += months;
3516
3682
  if (this.month > 12) {
3517
3683
  var yearDiv = Math.floor(this.month / 12);
3518
- this.month = pymod(this.month, 12);
3684
+ var monthMod = pymod(this.month, 12);
3685
+ this.month = monthMod;
3519
3686
  this.year += yearDiv;
3520
3687
  if (this.month === 0) {
3521
3688
  this.month = 12;
@@ -4134,8 +4301,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
4134
4301
  var month = Math.floor((h + l - 7 * m + 114) / 31);
4135
4302
  var day = (h + l - 7 * m + 114) % 31 + 1;
4136
4303
  var date = Date.UTC(y, month - 1, day + offset);
4137
- var yearStart = Date.UTC(y, 0, 1);
4138
- return [Math.ceil((date - yearStart) / (1e3 * 60 * 60 * 24))];
4304
+ return [Math.ceil((date - Date.UTC(y, 0, 1)) / (1e3 * 60 * 60 * 24))];
4139
4305
  }
4140
4306
  const iterinfo = function() {
4141
4307
  function Iterinfo(options) {
@@ -4461,7 +4627,8 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
4461
4627
  if (noCache === void 0) noCache = false;
4462
4628
  this._cache = noCache ? null : new Cache();
4463
4629
  this.origOptions = initializeOptions(options);
4464
- this.options = parseOptions(options).parsedOptions;
4630
+ var parsedOptions = parseOptions(options).parsedOptions;
4631
+ this.options = parsedOptions;
4465
4632
  }
4466
4633
  RRule.parseText = function(text, language) {
4467
4634
  return parseText(text, language);
@@ -5152,17 +5319,17 @@ const THINGS_WEEKDAY_TO_RRULE = [
5152
5319
  "SA"
5153
5320
  ];
5154
5321
  const parseNumericPlistField = (xml, key) => {
5155
- const match = new RegExp(`<key>${key}</key>\\s*<(integer|real)>([^<]+)</\\1>`).exec(xml);
5322
+ const match = new RegExp(`<key>${key}</key>\\s*<(integer|real)>([^<]+)</\\1>`, "u").exec(xml);
5156
5323
  if (!match?.[2]) return null;
5157
5324
  const parsed = Number(match[2]);
5158
5325
  return Number.isFinite(parsed) ? parsed : null;
5159
5326
  };
5160
- const PLIST_OFFSETS_ARRAY_PATTERN = /<key>of<\/key>\s*<array>([\s\S]*?)<\/array>/;
5327
+ const PLIST_OFFSETS_ARRAY_PATTERN = /<key>of<\/key>\s*<array>([\s\S]*?)<\/array>/u;
5161
5328
  const parseOffsets = (xml) => {
5162
5329
  const arrayMatch = PLIST_OFFSETS_ARRAY_PATTERN.exec(xml);
5163
5330
  if (!arrayMatch?.[1]) return [];
5164
5331
  const offsets = [];
5165
- for (const dictMatch of arrayMatch[1].matchAll(/<dict>([\s\S]*?)<\/dict>/g)) {
5332
+ for (const dictMatch of arrayMatch[1].matchAll(/<dict>([\s\S]*?)<\/dict>/gu)) {
5166
5333
  const [, fragment] = dictMatch;
5167
5334
  if (!fragment) continue;
5168
5335
  const weekday = parseNumericPlistField(fragment, "wd");
@@ -5290,11 +5457,11 @@ const normalizeTitle = (value, fallback) => {
5290
5457
  const trimmed = value?.trim();
5291
5458
  return trimmed && trimmed.length > 0 ? trimmed : fallback;
5292
5459
  };
5293
- const NON_ALPHANUMERIC_PATTERN = /[^A-Z0-9]+/g;
5294
- const WHITESPACE_SPLIT_PATTERN = /\s+/;
5295
- const NON_ALPHANUMERIC_STRICT_PATTERN = /[^A-Z0-9]/g;
5460
+ const NON_ALPHANUMERIC_PATTERN = /[^A-Z0-9]+/gu;
5461
+ const WHITESPACE_SPLIT_PATTERN = /\s+/u;
5462
+ const NON_ALPHANUMERIC_STRICT_PATTERN = /[^A-Z0-9]/gu;
5296
5463
  const READABLE_TEXT_PATTERN = /^[\t\n\r\u0020-\u007E\u00A0-\uFFFF]*$/u;
5297
- const EMAIL_SPLIT_PATTERN = /[,\n;]/;
5464
+ const EMAIL_SPLIT_PATTERN = /[,\n;]/u;
5298
5465
  const normalizeKeyBase = (value, fallback) => {
5299
5466
  const initials = value.toUpperCase().replace(NON_ALPHANUMERIC_PATTERN, " ").trim().split(WHITESPACE_SPLIT_PATTERN).filter((word) => word.length > 0).map((word) => word[0] ?? "").join("");
5300
5467
  const compact = value.toUpperCase().replace(NON_ALPHANUMERIC_STRICT_PATTERN, "");
@@ -5312,7 +5479,7 @@ const buildUniqueKeys = (items, options) => {
5312
5479
  const base = normalizeKeyBase(normalizeTitle(item.title, options.fallback), options.fallback).slice(0, options.maxLength);
5313
5480
  let candidate = base || options.fallback;
5314
5481
  if (used.has(candidate)) {
5315
- const suffixBase = item.uuid.replaceAll(/[^a-zA-Z0-9]/g, "").slice(-4);
5482
+ const suffixBase = item.uuid.replaceAll(/[^a-zA-Z0-9]/gu, "").slice(-4);
5316
5483
  const suffix = suffixBase.length > 0 ? suffixBase.toUpperCase() : "X";
5317
5484
  const headLength = Math.max(1, options.maxLength - suffix.length - 1);
5318
5485
  candidate = `${candidate.slice(0, headLength)}-${suffix}`;
@@ -5384,7 +5551,7 @@ const pickCompletedAt = (task) => {
5384
5551
  };
5385
5552
  const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
5386
5553
  const sanitizeBuffer = (buffer) => {
5387
- const asText = buffer.toString("utf8");
5554
+ const asText = buffer.toString("utf-8");
5388
5555
  if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
5389
5556
  const trimmed = asText.trim();
5390
5557
  if (trimmed.length > 0) return trimmed;
@@ -5998,7 +6165,7 @@ const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK8
5998
6165
  const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
5999
6166
  const THINGS_SQLITE_FILE = "main.sqlite";
6000
6167
  const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
6001
- const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
6168
+ const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
6002
6169
  const isUrlLike = (value) => URL_SCHEME_PATTERN.test(value);
6003
6170
  const resolveInputPath = (databaseUrl) => {
6004
6171
  if (!isUrlLike(databaseUrl)) return resolve(databaseUrl);
@@ -6061,7 +6228,7 @@ const toStringValue = (value) => {
6061
6228
  return trimmed.length > 0 ? trimmed : null;
6062
6229
  }
6063
6230
  if (Buffer.isBuffer(value)) {
6064
- const text = value.toString("utf8").trim();
6231
+ const text = value.toString("utf-8").trim();
6065
6232
  return text.length > 0 ? text : null;
6066
6233
  }
6067
6234
  return null;
@@ -6407,7 +6574,7 @@ const parseMetadataMode = (value) => {
6407
6574
  };
6408
6575
  const runThingsImportCommand = async (context, options, workspaceOverride) => {
6409
6576
  const commandContext = await resolveCommandContext(context);
6410
- const creatorId = requireUserId(commandContext.resolvedToken);
6577
+ const creatorId = await requireUserId(context, commandContext);
6411
6578
  const metadataMode = parseMetadataMode(options.metadataMode);
6412
6579
  const { workspace, autoSelected } = await resolveWorkspace(context, {
6413
6580
  apiBaseUrl: commandContext.apiBaseUrl,
@@ -6453,97 +6620,6 @@ Notes:
6453
6620
  });
6454
6621
  };
6455
6622
  //#endregion
6456
- //#region src/commands/interactive.ts
6457
- const HISTORY_FILE = join(CONFIG_DIR, "history.json");
6458
- const MAX_HISTORY = 200;
6459
- const isHistoryFile = (value) => {
6460
- if (typeof value !== "object" || value === null) return false;
6461
- const record = value;
6462
- return record.version === 1 && Array.isArray(record.entries) && record.entries.every((e) => typeof e === "string");
6463
- };
6464
- const loadHistory = async (historyFile) => {
6465
- try {
6466
- const raw = await readFile(historyFile, "utf8");
6467
- const parsed = JSON.parse(raw);
6468
- if (isHistoryFile(parsed)) return parsed.entries;
6469
- return [];
6470
- } catch {
6471
- return [];
6472
- }
6473
- };
6474
- const tokenize = (input) => {
6475
- const tokens = [];
6476
- let current = "";
6477
- let inQuote = false;
6478
- let quoteChar = "";
6479
- for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
6480
- else current += char;
6481
- else if (char === "\"" || char === "'") {
6482
- inQuote = true;
6483
- quoteChar = char;
6484
- } else if (char === " ") {
6485
- if (current.length > 0) {
6486
- tokens.push(current);
6487
- current = "";
6488
- }
6489
- } else current += char;
6490
- if (current.length > 0) tokens.push(current);
6491
- return tokens;
6492
- };
6493
- const buildCompleter = (program) => (line) => {
6494
- const allCommands = program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()]);
6495
- const hits = allCommands.filter((c) => c.startsWith(line));
6496
- return [hits.length > 0 ? hits : allCommands, line];
6497
- };
6498
- const runInteractiveMode = async (context, program) => {
6499
- const history = await loadHistory(HISTORY_FILE);
6500
- const rl = createInterface({
6501
- completer: buildCompleter(program),
6502
- historySize: MAX_HISTORY,
6503
- input: process.stdin,
6504
- output: process.stdout,
6505
- prompt: "donebear> "
6506
- });
6507
- for (const entry of [...history].toReversed()) rl.history.push(entry);
6508
- writeSuccess(context, "Done Bear interactive mode. Type \"exit\" or Ctrl+D to quit.");
6509
- writeInfo(context, "Commands: task, workspace, project, label, team, today, search, auth, whoami");
6510
- rl.prompt();
6511
- const sessionEntries = [];
6512
- for await (const line of rl) {
6513
- const trimmed = line.trim();
6514
- if (!trimmed) {
6515
- rl.prompt();
6516
- continue;
6517
- }
6518
- if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") break;
6519
- const tokens = tokenize(trimmed);
6520
- try {
6521
- await program.parseAsync([
6522
- "node",
6523
- "donebear",
6524
- ...tokens
6525
- ]);
6526
- } catch {}
6527
- sessionEntries.push(trimmed);
6528
- rl.prompt();
6529
- }
6530
- rl.close();
6531
- if (sessionEntries.length > 0) try {
6532
- const payload = {
6533
- entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY),
6534
- version: 1
6535
- };
6536
- await mkdir(CONFIG_DIR, {
6537
- mode: 448,
6538
- recursive: true
6539
- });
6540
- await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
6541
- encoding: "utf8",
6542
- mode: 384
6543
- });
6544
- } catch {}
6545
- };
6546
- //#endregion
6547
6623
  //#region src/commands/label.ts
6548
6624
  const sortLabels = (labels, sort) => {
6549
6625
  const sorted = [...labels];
@@ -6635,7 +6711,7 @@ const runLabelShow = async (context, labelRef, options) => {
6635
6711
  writeInfo(context, `createdAt: ${formatEpochDate(label.createdAt) ?? "(none)"}`);
6636
6712
  writeInfo(context, `updatedAt: ${formatEpochDate(label.updatedAt) ?? "(none)"}`);
6637
6713
  };
6638
- const UUID_REGEX$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6714
+ const UUID_REGEX$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
6639
6715
  const resolveTaskForLabel = async (context, options) => {
6640
6716
  const taskRef = options.taskRef.trim();
6641
6717
  if (!taskRef) throw new Error("Task id is required.");
@@ -6882,7 +6958,7 @@ const getProjectStatus = (project) => {
6882
6958
  };
6883
6959
  const parseTargetDate = (value) => parseCliDateOnlyToEpoch(value, "target date");
6884
6960
  const generateProjectSlug = (name, id) => {
6885
- const slug = name.toLowerCase().trim().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "").slice(0, 39);
6961
+ const slug = name.toLowerCase().trim().replaceAll(/[^a-z0-9]+/gu, "-").replaceAll(/^-|-$/gu, "").slice(0, 39);
6886
6962
  const shortId = id.replaceAll("-", "").slice(0, 8);
6887
6963
  return slug ? `${slug}-${shortId}` : shortId;
6888
6964
  };
@@ -7002,14 +7078,14 @@ const resolveProjectName = async (context, rawName) => {
7002
7078
  const trimmed = rawName.trim();
7003
7079
  if (trimmed.length > 0) return trimmed;
7004
7080
  }
7005
- const { promptForValue } = await import("./prompt-FRvfbinv.mjs");
7081
+ const { promptForValue } = await import("./prompt-B7mDo4K7.mjs");
7006
7082
  const prompted = await promptForValue(context, "Project name");
7007
7083
  if (prompted) return prompted;
7008
7084
  throw new CliError("Project name is required.", EXIT_CODES.VALIDATION, "Provide a name: donebear project add \"My project\"");
7009
7085
  };
7010
7086
  const runProjectAdd = async (context, name, options) => {
7011
7087
  const commandContext = await resolveCommandContext(context);
7012
- const userId = requireUserId(commandContext.resolvedToken);
7088
+ const userId = await requireUserId(context, commandContext);
7013
7089
  const normalizedName = await resolveProjectName(context, name);
7014
7090
  const { workspace, autoSelected } = await resolveWorkspace(context, {
7015
7091
  apiBaseUrl: commandContext.apiBaseUrl,
@@ -7017,17 +7093,23 @@ const runProjectAdd = async (context, name, options) => {
7017
7093
  token: commandContext.token,
7018
7094
  workspaceRef: normalizeWorkspaceRef(options.workspace)
7019
7095
  });
7020
- 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();
7021
7103
  const now = Date.now();
7022
7104
  const key = generateProjectSlug(normalizedName, projectId);
7023
7105
  await mutateModel(context, {
7024
7106
  baseUrl: commandContext.apiBaseUrl,
7025
7107
  request: {
7026
- batchId: randomUUID(),
7108
+ batchId: idempotencyId("batch") ?? randomUUID(),
7027
7109
  transactions: [{
7028
7110
  action: "INSERT",
7029
- clientId: commandContext.localContext.clientId,
7030
- clientTxId: randomUUID(),
7111
+ clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7112
+ clientTxId: idempotencyId("tx") ?? randomUUID(),
7031
7113
  modelId: projectId,
7032
7114
  modelName: "Project",
7033
7115
  payload: {
@@ -7194,7 +7276,7 @@ Agent tip: run "donebear spec project add" for full option reference
7194
7276
  await runProjectShow(context, projectRef, { workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0 });
7195
7277
  });
7196
7278
  });
7197
- 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) => {
7198
7280
  const context = contextFromCommand(command);
7199
7281
  await runWithErrorHandling(context, async () => {
7200
7282
  await runProjectAdd(context, name, {
@@ -7253,7 +7335,7 @@ Agent tip: run "donebear spec project add" for full option reference
7253
7335
  };
7254
7336
  //#endregion
7255
7337
  //#region src/commands/task.ts
7256
- const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7338
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
7257
7339
  const DEFAULT_TASK_LIST_LIMIT = 20;
7258
7340
  const VALID_TASK_VIEWS = new Set([
7259
7341
  "inbox",
@@ -7417,7 +7499,7 @@ const resolveNotes = async (_context, flagNotes) => {
7417
7499
  };
7418
7500
  const runTaskAdd = async (context, title, options, workspaceOverride) => {
7419
7501
  const commandContext = await resolveCommandContext(context);
7420
- const userId = requireUserId(commandContext.resolvedToken);
7502
+ const userId = await requireUserId(context, commandContext);
7421
7503
  const normalizedTitle = await resolveTitle(context, title);
7422
7504
  const notes = await resolveNotes(context, options.notes);
7423
7505
  const { workspace, autoSelected } = await resolveWorkspace(context, {
@@ -7435,7 +7517,13 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7435
7517
  token: commandContext.token,
7436
7518
  workspaceId: workspace.id
7437
7519
  }).then((teams) => resolveEntityId(teams, options.team, "Team")) : Promise.resolve(null)]);
7438
- const taskId = randomUUID();
7520
+ const idempotencyId = (part) => options.idempotencyKey ? createIdempotentMutationId({
7521
+ key: options.idempotencyKey,
7522
+ modelName: "Task",
7523
+ part,
7524
+ workspaceId: workspace.id
7525
+ }) : null;
7526
+ const taskId = idempotencyId("model") ?? randomUUID();
7439
7527
  const now = Date.now();
7440
7528
  const view = parseTaskView(options.when);
7441
7529
  const startFields = buildTaskStartFields(view);
@@ -7443,11 +7531,11 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7443
7531
  await mutateModel(context, {
7444
7532
  baseUrl: commandContext.apiBaseUrl,
7445
7533
  request: {
7446
- batchId: randomUUID(),
7534
+ batchId: idempotencyId("batch") ?? randomUUID(),
7447
7535
  transactions: [{
7448
7536
  action: "INSERT",
7449
- clientId: commandContext.localContext.clientId,
7450
- clientTxId: randomUUID(),
7537
+ clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7538
+ clientTxId: idempotencyId("tx") ?? randomUUID(),
7451
7539
  modelId: taskId,
7452
7540
  modelName: "Task",
7453
7541
  payload: {
@@ -7809,15 +7897,15 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
7809
7897
  await runTaskRandom(context, options, workspaceOverride);
7810
7898
  });
7811
7899
  });
7812
- 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", `
7900
+ 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").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one task)").addHelpText("after", `
7813
7901
  Examples:
7814
7902
  donebear task add "Fix login bug" --when today --json
7815
- donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
7903
+ donebear task add "Write tests" --deadline 2030-03-15 --project PROJ --json
7816
7904
  donebear task add "Review PR" --when anytime --notes "Check for XSS" --json
7817
7905
  echo "Multi-line notes" | donebear task add "My task"
7818
7906
  donebear task add # prompts for title interactively
7819
7907
 
7820
- Date format: YYYY-MM-DD (e.g. 2026-03-15)
7908
+ Date format: YYYY-MM-DD (e.g. 2030-03-15)
7821
7909
  View (--when): inbox | anytime | today | upcoming | someday
7822
7910
  `).action(async (title, options, command) => {
7823
7911
  const context = contextFromCommand(command);
@@ -8161,6 +8249,15 @@ Examples:
8161
8249
  const OVERVIEW = {
8162
8250
  authentication: "Set DONEBEAR_TOKEN env var, or run: donebear auth login",
8163
8251
  commands: {
8252
+ "api-key": {
8253
+ description: "Manage personal API keys",
8254
+ quick_example: "donebear api-key list --json",
8255
+ subcommands: [
8256
+ "create",
8257
+ "list",
8258
+ "revoke"
8259
+ ]
8260
+ },
8164
8261
  auth: {
8165
8262
  description: "Authentication",
8166
8263
  quick_example: "donebear auth status --json",
@@ -8188,9 +8285,15 @@ const OVERVIEW = {
8188
8285
  subcommands: ["things"]
8189
8286
  },
8190
8287
  label: {
8191
- description: "List workspace labels",
8288
+ description: "Manage workspace labels and task label assignments",
8192
8289
  quick_example: "donebear label list --json",
8193
- subcommands: ["list", "show"]
8290
+ subcommands: [
8291
+ "list",
8292
+ "show",
8293
+ "create",
8294
+ "add",
8295
+ "remove"
8296
+ ]
8194
8297
  },
8195
8298
  project: {
8196
8299
  description: "Manage projects",
@@ -8228,6 +8331,7 @@ const OVERVIEW = {
8228
8331
  "read",
8229
8332
  "append",
8230
8333
  "prepend",
8334
+ "random",
8231
8335
  "checklist"
8232
8336
  ]
8233
8337
  },
@@ -8276,6 +8380,70 @@ const OVERVIEW = {
8276
8380
  workspace_targeting: "Prefix any command: donebear workspace=<slug> task list"
8277
8381
  };
8278
8382
  const COMMAND_SPECS = {
8383
+ "api-key create": {
8384
+ arguments: { label: {
8385
+ description: "Human-readable label for the key",
8386
+ required: true,
8387
+ type: "string"
8388
+ } },
8389
+ command: "api-key create",
8390
+ examples: [
8391
+ "donebear api-key create \"Raycast\" --json",
8392
+ "donebear api-key create \"CI\" --permissions read-only --expires 90d --json",
8393
+ "donebear api-key create \"Scoped\" --workspaces my-org,side-project --expires 1y --json"
8394
+ ],
8395
+ json_output: {
8396
+ createdAt: "string",
8397
+ expiresAt: "string | null",
8398
+ id: "string",
8399
+ keyPrefix: "string",
8400
+ label: "string",
8401
+ scopes: "string[]",
8402
+ token: "string",
8403
+ workspaceIds: "string[] | null"
8404
+ },
8405
+ notes: "The full token is only shown once at creation time. Scopes default to full-access (read, write, admin).",
8406
+ options: {
8407
+ "--expires": {
8408
+ description: "Expiration duration, e.g. \"30d\", \"90d\", \"1y\"",
8409
+ type: "string"
8410
+ },
8411
+ "--permissions": {
8412
+ default: "full-access",
8413
+ description: "\"full-access\" (read, write, admin) or \"read-only\" (read only)",
8414
+ enum: ["full-access", "read-only"],
8415
+ type: "string"
8416
+ },
8417
+ "--workspaces": {
8418
+ description: "Comma-separated workspace slugs to restrict the key to",
8419
+ type: "string"
8420
+ }
8421
+ },
8422
+ usage: "donebear api-key create <label> [options]"
8423
+ },
8424
+ "api-key list": {
8425
+ command: "api-key list",
8426
+ examples: ["donebear api-key list --json"],
8427
+ json_output: { keys: "ApiKeyRecord[]" },
8428
+ notes: "Each key includes scopes, workspaceIds, and expiresAt fields.",
8429
+ options: {},
8430
+ usage: "donebear api-key list"
8431
+ },
8432
+ "api-key revoke": {
8433
+ arguments: { "key-id": {
8434
+ description: "The UUID of the API key to revoke",
8435
+ required: true,
8436
+ type: "string"
8437
+ } },
8438
+ command: "api-key revoke",
8439
+ examples: ["donebear api-key revoke abc12345 --json"],
8440
+ json_output: {
8441
+ id: "string",
8442
+ ok: "boolean"
8443
+ },
8444
+ options: {},
8445
+ usage: "donebear api-key revoke <key-id>"
8446
+ },
8279
8447
  "auth login": {
8280
8448
  command: "auth login",
8281
8449
  examples: [
@@ -8289,10 +8457,21 @@ const COMMAND_SPECS = {
8289
8457
  user: "{ id, email }"
8290
8458
  },
8291
8459
  non_interactive: "For CI/scripts, set DONEBEAR_TOKEN=<token> instead of using auth login.",
8292
- options: { "--no-open": {
8293
- description: "Print auth URL instead of opening browser",
8294
- type: "boolean"
8295
- } },
8460
+ options: {
8461
+ "--no-open": {
8462
+ description: "Print auth URL instead of opening browser",
8463
+ type: "boolean"
8464
+ },
8465
+ "--port": {
8466
+ default: 8787,
8467
+ description: "Loopback callback port for the OAuth redirect",
8468
+ type: "number"
8469
+ },
8470
+ "--timeout": {
8471
+ description: "Seconds to wait for the OAuth callback",
8472
+ type: "number"
8473
+ }
8474
+ },
8296
8475
  usage: "donebear auth login [options]"
8297
8476
  },
8298
8477
  "auth status": {
@@ -8414,6 +8593,10 @@ const COMMAND_SPECS = {
8414
8593
  enum: ["note", "none"],
8415
8594
  type: "string"
8416
8595
  },
8596
+ "--skip-teams": {
8597
+ description: "Skip importing Things Areas as Teams and TeamProject links",
8598
+ type: "boolean"
8599
+ },
8417
8600
  "--workspace": {
8418
8601
  description: "Workspace id, slug, or name",
8419
8602
  type: "string"
@@ -8429,7 +8612,15 @@ const COMMAND_SPECS = {
8429
8612
  ok: "boolean",
8430
8613
  workspace: "WorkspaceSummary"
8431
8614
  },
8432
- options: { "--workspace": { type: "string" } },
8615
+ options: {
8616
+ "--sort": {
8617
+ default: "alpha",
8618
+ description: "Sort order: alpha|count",
8619
+ enum: ["alpha", "count"],
8620
+ type: "string"
8621
+ },
8622
+ "--workspace": { type: "string" }
8623
+ },
8433
8624
  usage: "donebear label list [options]"
8434
8625
  },
8435
8626
  "project add": {
@@ -8448,6 +8639,10 @@ const COMMAND_SPECS = {
8448
8639
  notes: "URL slug is auto-generated from name (e.g. 'Q2 Launch' → 'q2-launch-a1b2c3d4').",
8449
8640
  options: {
8450
8641
  "--description": { type: "string" },
8642
+ "--idempotency-key": {
8643
+ description: "Stable key making retries safe (repeated calls create only one project)",
8644
+ type: "string"
8645
+ },
8451
8646
  "--target-date": {
8452
8647
  description: "Target completion date",
8453
8648
  format: "YYYY-MM-DD",
@@ -8486,8 +8681,25 @@ const COMMAND_SPECS = {
8486
8681
  ok: "boolean"
8487
8682
  },
8488
8683
  options: {
8684
+ "--clear-description": {
8685
+ description: "Clear the project description",
8686
+ type: "boolean"
8687
+ },
8688
+ "--clear-target-date": {
8689
+ description: "Clear the project target date",
8690
+ type: "boolean"
8691
+ },
8489
8692
  "--description": { type: "string" },
8490
8693
  "--name": { type: "string" },
8694
+ "--status": {
8695
+ description: "Set status",
8696
+ enum: [
8697
+ "active",
8698
+ "done",
8699
+ "archived"
8700
+ ],
8701
+ type: "string"
8702
+ },
8491
8703
  "--target-date": {
8492
8704
  format: "YYYY-MM-DD",
8493
8705
  type: "string"
@@ -8587,6 +8799,10 @@ const COMMAND_SPECS = {
8587
8799
  format: "YYYY-MM-DD",
8588
8800
  type: "string"
8589
8801
  },
8802
+ "--idempotency-key": {
8803
+ description: "Stable key making retries safe (repeated calls create only one task)",
8804
+ type: "string"
8805
+ },
8590
8806
  "--notes": {
8591
8807
  description: "Body / description text (or pipe via stdin)",
8592
8808
  type: "string"
@@ -8677,15 +8893,35 @@ const COMMAND_SPECS = {
8677
8893
  type: "string"
8678
8894
  } },
8679
8895
  command: "task edit",
8680
- examples: ["donebear task edit abc1 --title \"Updated title\" --json", "donebear task edit abc1 --when today --deadline 2026-03-20 --json"],
8896
+ examples: [
8897
+ "donebear task edit abc1 --title \"Updated title\" --json",
8898
+ "donebear task edit abc1 --when today --deadline 2030-01-15 --json",
8899
+ "donebear task edit abc1 --clear-deadline --json"
8900
+ ],
8681
8901
  json_output: {
8682
8902
  action: "string",
8683
8903
  id: "string",
8684
8904
  ok: "boolean"
8685
8905
  },
8686
8906
  options: {
8907
+ "--clear-deadline": {
8908
+ description: "Clear the due date",
8909
+ type: "boolean"
8910
+ },
8911
+ "--clear-notes": {
8912
+ description: "Clear notes/description",
8913
+ type: "boolean"
8914
+ },
8915
+ "--clear-project": {
8916
+ description: "Remove the task from its project",
8917
+ type: "boolean"
8918
+ },
8919
+ "--clear-team": {
8920
+ description: "Remove the task from its team",
8921
+ type: "boolean"
8922
+ },
8687
8923
  "--deadline": {
8688
- description: "Set due date (pass empty string to clear)",
8924
+ description: "Set due date",
8689
8925
  format: "YYYY-MM-DD",
8690
8926
  type: "string"
8691
8927
  },
@@ -8922,6 +9158,16 @@ const COMMAND_SPECS = {
8922
9158
  }
8923
9159
  };
8924
9160
  const TOP_LEVEL_SPECS = {
9161
+ "api-key": {
9162
+ command: "api-key",
9163
+ description: "Manage personal API keys",
9164
+ subcommands: {
9165
+ create: "Create a new API key (--permissions, --workspaces, --expires)",
9166
+ list: "List all API keys with scopes and expiration",
9167
+ revoke: "Revoke an API key by id"
9168
+ },
9169
+ tip: "donebear spec api-key create — get full spec for api-key create"
9170
+ },
8925
9171
  auth: {
8926
9172
  command: "auth",
8927
9173
  description: "Authentication",
@@ -8941,9 +9187,12 @@ const TOP_LEVEL_SPECS = {
8941
9187
  },
8942
9188
  label: {
8943
9189
  command: "label",
8944
- description: "List workspace labels",
9190
+ description: "Manage workspace labels and task label assignments",
8945
9191
  subcommands: {
9192
+ add: "Add a label to a task",
9193
+ create: "Create a new label",
8946
9194
  list: "List all labels",
9195
+ remove: "Remove a label from a task",
8947
9196
  show: "Get label details"
8948
9197
  }
8949
9198
  },
@@ -8974,6 +9223,7 @@ const TOP_LEVEL_SPECS = {
8974
9223
  edit: "Update task fields",
8975
9224
  list: "List tasks (--state, --view, --search, --limit)",
8976
9225
  prepend: "Prepend text to task notes",
9226
+ random: "Pick a random task (--state to filter)",
8977
9227
  read: "Print task notes as plain text",
8978
9228
  reopen: "Reopen a completed task",
8979
9229
  show: "Get task details by id",
@@ -9215,8 +9465,13 @@ Examples:
9215
9465
  //#endregion
9216
9466
  //#region src/commands/whoami.ts
9217
9467
  const runWhoAmI = async (context) => {
9218
- const config = resolveSupabaseConfig();
9219
- const resolvedToken = await resolveAuthToken(context, config);
9468
+ let supabaseConfig = null;
9469
+ try {
9470
+ supabaseConfig = resolveSupabaseConfig();
9471
+ } catch {
9472
+ supabaseConfig = null;
9473
+ }
9474
+ const resolvedToken = await resolveAuthToken(context, supabaseConfig);
9220
9475
  if (!resolvedToken) {
9221
9476
  process.exitCode = EXIT_CODES.AUTH_REQUIRED;
9222
9477
  if (context.json) {
@@ -9233,7 +9488,10 @@ const runWhoAmI = async (context) => {
9233
9488
  writeInfo(context, "Run `donebear auth login`.");
9234
9489
  return;
9235
9490
  }
9236
- const user = await getSupabaseUser(config.url, config.publishableKey, resolvedToken.token);
9491
+ const user = await loadViewer(context, {
9492
+ baseUrl: resolveApiBaseUrl(context),
9493
+ token: resolvedToken.token
9494
+ });
9237
9495
  if (context.json) {
9238
9496
  writeOutput(context, {
9239
9497
  ok: true,
@@ -9592,30 +9850,8 @@ Examples:
9592
9850
  });
9593
9851
  };
9594
9852
  //#endregion
9595
- //#region src/cli.ts
9596
- const packageManifest = createRequire(import.meta.url)("../package.json");
9597
- const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/i;
9598
- const extractWorkspaceTarget = (argv) => {
9599
- const processedArgv = [...argv];
9600
- const [, , firstArg] = processedArgv;
9601
- if (firstArg) {
9602
- const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
9603
- if (match?.[1]) {
9604
- processedArgv.splice(2, 1);
9605
- return {
9606
- processedArgv,
9607
- workspaceOverride: match[1].trim()
9608
- };
9609
- }
9610
- }
9611
- return {
9612
- processedArgv,
9613
- workspaceOverride: null
9614
- };
9615
- };
9616
- const main = async () => {
9617
- loadCliEnvironmentFiles();
9618
- const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
9853
+ //#region src/program.ts
9854
+ const buildProgram = (workspaceOverride = null) => {
9619
9855
  const program = new Command();
9620
9856
  program.name(CLI_NAME).description("Done Bear CLI for auth, workspace, and task workflows").version(packageManifest.version, "-v, --version").showHelpAfterError().showSuggestionAfterError();
9621
9857
  addGlobalOptions(program);
@@ -9659,6 +9895,33 @@ Filtering & output:
9659
9895
  registerSearchCommand(program, workspaceOverride);
9660
9896
  registerHistoryCommand(program, workspaceOverride);
9661
9897
  registerWhoAmICommand(program);
9898
+ return program;
9899
+ };
9900
+ //#endregion
9901
+ //#region src/cli.ts
9902
+ const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/iu;
9903
+ const extractWorkspaceTarget = (argv) => {
9904
+ const processedArgv = [...argv];
9905
+ const firstArg = processedArgv[2];
9906
+ if (firstArg) {
9907
+ const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
9908
+ if (match?.[1]) {
9909
+ processedArgv.splice(2, 1);
9910
+ return {
9911
+ processedArgv,
9912
+ workspaceOverride: match[1].trim()
9913
+ };
9914
+ }
9915
+ }
9916
+ return {
9917
+ processedArgv,
9918
+ workspaceOverride: null
9919
+ };
9920
+ };
9921
+ const main = async () => {
9922
+ loadCliEnvironmentFiles();
9923
+ const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
9924
+ const program = buildProgram(workspaceOverride);
9662
9925
  const hasUserArgs = processedArgv.length <= 2;
9663
9926
  const isTTY = process.stdin.isTTY === true;
9664
9927
  if (hasUserArgs && isTTY) {