@tasklumina/cli 1.0.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/index.js ADDED
@@ -0,0 +1,2865 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command13 } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/lib/config.ts
10
+ import { resolve } from "path";
11
+ import { homedir } from "os";
12
+ import { existsSync, readFileSync } from "fs";
13
+ function loadEnvFile(path) {
14
+ if (!existsSync(path)) return;
15
+ const content = readFileSync(path, "utf8");
16
+ for (const line of content.split("\n")) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed || trimmed.startsWith("#")) continue;
19
+ const eqIdx = trimmed.indexOf("=");
20
+ if (eqIdx === -1) continue;
21
+ const key = trimmed.slice(0, eqIdx).trim();
22
+ const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
23
+ if (!process.env[key]) process.env[key] = value;
24
+ }
25
+ }
26
+ loadEnvFile(resolve(homedir(), ".tasklumina", ".env"));
27
+ function requireEnv(key) {
28
+ const value = process.env[key];
29
+ if (!value) {
30
+ throw new Error(
31
+ `Missing required environment variable: ${key}
32
+ Set it in ~/.tasklumina/.env or as an environment variable.
33
+ See cli/.env.example for required variables.`
34
+ );
35
+ }
36
+ return value;
37
+ }
38
+ var _envConfig = null;
39
+ function getEnvConfig() {
40
+ if (!_envConfig) {
41
+ _envConfig = {
42
+ region: requireEnv("TASKLUMINA_AWS_REGION"),
43
+ appsyncEndpoint: requireEnv("TASKLUMINA_APPSYNC_ENDPOINT")
44
+ };
45
+ }
46
+ return _envConfig;
47
+ }
48
+ var CONFIG = {
49
+ get region() {
50
+ return getEnvConfig().region;
51
+ },
52
+ get appsyncEndpoint() {
53
+ return getEnvConfig().appsyncEndpoint;
54
+ },
55
+ tokenStorageKey: "tasklumina-cli",
56
+ autoRotateIntervalHours: 24
57
+ };
58
+
59
+ // src/lib/crypto.ts
60
+ import {
61
+ createCipheriv,
62
+ createDecipheriv,
63
+ scryptSync,
64
+ randomBytes
65
+ } from "crypto";
66
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
67
+ import { resolve as resolve2 } from "path";
68
+ import { homedir as homedir2 } from "os";
69
+ var ALGO = "aes-256-gcm";
70
+ var SCRYPT_KEYLEN = 32;
71
+ var SCRYPT_COST = { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
72
+ function getMachineSecret() {
73
+ const dir = resolve2(homedir2(), ".tasklumina");
74
+ const keyFile = resolve2(dir, "machine-key");
75
+ try {
76
+ const secret2 = readFileSync2(keyFile, "utf8").trim();
77
+ if (secret2.length >= 32) return secret2;
78
+ } catch {
79
+ }
80
+ mkdirSync(dir, { recursive: true, mode: 448 });
81
+ const secret = randomBytes(32).toString("hex");
82
+ try {
83
+ writeFileSync(keyFile, secret, { mode: 384, flag: "wx" });
84
+ return secret;
85
+ } catch (err) {
86
+ if (err.code === "EEXIST") {
87
+ const existing = readFileSync2(keyFile, "utf8").trim();
88
+ if (existing.length >= 32) return existing;
89
+ }
90
+ throw err;
91
+ }
92
+ }
93
+ var _cachedSecret = null;
94
+ function deriveKey(salt) {
95
+ if (!_cachedSecret) {
96
+ _cachedSecret = getMachineSecret();
97
+ }
98
+ return scryptSync(_cachedSecret, salt, SCRYPT_KEYLEN, SCRYPT_COST);
99
+ }
100
+ function encrypt(plaintext) {
101
+ const salt = randomBytes(16);
102
+ const key = deriveKey(salt);
103
+ const iv = randomBytes(16);
104
+ const cipher = createCipheriv(ALGO, key, iv);
105
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
106
+ encrypted += cipher.final("hex");
107
+ const tag = cipher.getAuthTag();
108
+ return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${tag.toString("hex")}:${encrypted}`;
109
+ }
110
+ function decrypt(ciphertext) {
111
+ if (ciphertext.startsWith("enc2:")) {
112
+ const parts = ciphertext.slice(5).split(":");
113
+ if (parts.length !== 4) {
114
+ throw new Error("Malformed encrypted value");
115
+ }
116
+ const [saltHex, ivHex, tagHex, encrypted] = parts;
117
+ const key = deriveKey(Buffer.from(saltHex, "hex"));
118
+ const decipher = createDecipheriv(ALGO, key, Buffer.from(ivHex, "hex"));
119
+ decipher.setAuthTag(Buffer.from(tagHex, "hex"));
120
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
121
+ decrypted += decipher.final("utf8");
122
+ return decrypted;
123
+ }
124
+ if (ciphertext.startsWith("enc:")) {
125
+ throw new Error(
126
+ "Token encrypted with legacy format (v1). Run `tasklumina login` to re-authenticate."
127
+ );
128
+ }
129
+ throw new Error("Not an encrypted value");
130
+ }
131
+ function isPlaintext(value) {
132
+ return !value.startsWith("enc2:") && !value.startsWith("enc:") && value.startsWith("{");
133
+ }
134
+
135
+ // src/lib/token-store.ts
136
+ var keytarModule = null;
137
+ var keytarFailed = false;
138
+ async function getKeytar() {
139
+ if (keytarFailed) {
140
+ throw new Error(
141
+ "System keychain (keytar) is unavailable. Install it with: npm install keytar\nTask Lumina CLI requires a secure keychain \u2014 plaintext token storage is not supported."
142
+ );
143
+ }
144
+ if (keytarModule) return keytarModule;
145
+ try {
146
+ const mod = await import("keytar");
147
+ keytarModule = mod.default ?? mod;
148
+ return keytarModule;
149
+ } catch {
150
+ keytarFailed = true;
151
+ throw new Error(
152
+ "System keychain (keytar) is unavailable. Install it with: npm install keytar\nTask Lumina CLI requires a secure keychain \u2014 plaintext token storage is not supported."
153
+ );
154
+ }
155
+ }
156
+ async function loadApiKeyV1() {
157
+ const keytar = await getKeytar();
158
+ const raw = await keytar.getPassword(CONFIG.tokenStorageKey, "api-key");
159
+ if (!raw) return null;
160
+ if (isPlaintext(raw)) return raw;
161
+ return decrypt(raw);
162
+ }
163
+ async function clearApiKeyV1() {
164
+ const keytar = await getKeytar();
165
+ await keytar.deletePassword(CONFIG.tokenStorageKey, "api-key");
166
+ }
167
+ async function saveApiKeyData(data) {
168
+ const json = JSON.stringify(data);
169
+ const encrypted = encrypt(json);
170
+ const keytar = await getKeytar();
171
+ await keytar.setPassword(CONFIG.tokenStorageKey, "api-key-v2", encrypted);
172
+ }
173
+ async function loadApiKeyData() {
174
+ const keytar = await getKeytar();
175
+ const raw = await keytar.getPassword(CONFIG.tokenStorageKey, "api-key-v2");
176
+ if (raw) {
177
+ const json = decrypt(raw);
178
+ return JSON.parse(json);
179
+ }
180
+ const legacyKey = await loadApiKeyV1();
181
+ if (legacyKey) {
182
+ const data = { key: legacyKey, lastRotatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
183
+ await saveApiKeyData(data);
184
+ await clearApiKeyV1();
185
+ return data;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ // src/lib/graphql-client.ts
191
+ async function getAuthToken() {
192
+ const envKey = process.env.TASKLUMINA_API_KEY;
193
+ if (envKey) return envKey;
194
+ const data = await loadApiKeyData();
195
+ if (data) return data.key;
196
+ throw new Error("No API key configured. Run `tasklumina configure` to set up authentication.");
197
+ }
198
+ var _verboseWarningShown = false;
199
+ function isVerbose() {
200
+ const verbose = process.argv.includes("--verbose");
201
+ if (verbose && !_verboseWarningShown) {
202
+ console.error("\u26A0 Verbose mode: debug output may expose operation names and timing in terminal/logs.");
203
+ _verboseWarningShown = true;
204
+ }
205
+ return verbose;
206
+ }
207
+ function extractOperationName(query) {
208
+ const match = query.match(/(?:query|mutation)\s+(\w+)/);
209
+ return match?.[1] || "unknown";
210
+ }
211
+ function maskEndpoint(url) {
212
+ return url.replace(/^(https:\/\/)[^.]+/, "$1***");
213
+ }
214
+ var SENSITIVE_KEYS = ["email", "password", "token", "accessToken", "idToken", "refreshToken", "sharedWithEmail", "ownerEmail", "key"];
215
+ function redactVariables(vars) {
216
+ const redacted = {};
217
+ for (const [key, value] of Object.entries(vars)) {
218
+ if (SENSITIVE_KEYS.includes(key)) {
219
+ redacted[key] = "[REDACTED]";
220
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
221
+ redacted[key] = redactVariables(value);
222
+ } else {
223
+ redacted[key] = value;
224
+ }
225
+ }
226
+ return redacted;
227
+ }
228
+ async function graphql(query, variables) {
229
+ const token = await getAuthToken();
230
+ const verbose = isVerbose();
231
+ const opName = extractOperationName(query);
232
+ const startTime = Date.now();
233
+ if (verbose) {
234
+ console.error(`\u2192 POST ${maskEndpoint(CONFIG.appsyncEndpoint)}`);
235
+ console.error(` Operation: ${opName}`);
236
+ if (variables) console.error(` Variables: ${JSON.stringify(redactVariables(variables))}`);
237
+ }
238
+ let res;
239
+ try {
240
+ res = await fetch(CONFIG.appsyncEndpoint, {
241
+ method: "POST",
242
+ headers: {
243
+ "Content-Type": "application/json",
244
+ Authorization: token
245
+ },
246
+ body: JSON.stringify({ query, variables }),
247
+ signal: AbortSignal.timeout(3e4)
248
+ });
249
+ } catch (err) {
250
+ if (err instanceof DOMException && err.name === "TimeoutError") {
251
+ throw new Error("Request timed out after 30s. Check your connection.");
252
+ }
253
+ throw new Error("Cannot reach Task Lumina API. Check your connection.");
254
+ }
255
+ if (verbose) {
256
+ console.error(`\u2190 ${res.status} ${res.statusText} (${Date.now() - startTime}ms)`);
257
+ }
258
+ if (!res.ok) {
259
+ throw new Error(`GraphQL request failed: ${res.status} ${res.statusText}`);
260
+ }
261
+ const json = await res.json();
262
+ if (json.errors?.length) {
263
+ const msg = json.errors[0].message;
264
+ if (msg.includes("Not Authorized") || msg.includes("Unauthorized")) {
265
+ throw new Error("Not authorized. Your API key may be revoked \u2014 run `tasklumina configure`.");
266
+ }
267
+ const MAX_ERROR_LEN = 200;
268
+ const sanitized = msg.length > MAX_ERROR_LEN ? msg.slice(0, MAX_ERROR_LEN) + "..." : msg;
269
+ if (verbose) {
270
+ console.error(` Server error: ${msg}`);
271
+ }
272
+ throw new Error(sanitized);
273
+ }
274
+ if (!json.data) {
275
+ throw new Error("No data returned from GraphQL");
276
+ }
277
+ return json.data;
278
+ }
279
+
280
+ // src/lib/output.ts
281
+ import chalk from "chalk";
282
+ import Table from "cli-table3";
283
+ function isJsonMode() {
284
+ return process.argv.includes("--json");
285
+ }
286
+ function isRedactMode() {
287
+ return process.argv.includes("--redact");
288
+ }
289
+ function printTable(headers, rows) {
290
+ if (isJsonMode()) return;
291
+ const table = new Table({ head: headers.map((h) => chalk.bold(h)) });
292
+ for (const row of rows) {
293
+ table.push(row);
294
+ }
295
+ console.log(table.toString());
296
+ }
297
+ function printJson(data) {
298
+ console.log(JSON.stringify(data, null, 2));
299
+ }
300
+ function printError(msg) {
301
+ console.error(chalk.red(`Error: ${msg}`));
302
+ }
303
+ function printSuccess(msg) {
304
+ console.log(chalk.green(msg));
305
+ }
306
+ function printWarning(msg) {
307
+ console.log(chalk.yellow(msg));
308
+ }
309
+ function formatDate(iso) {
310
+ if (!iso) return "\u2014";
311
+ const d = new Date(iso);
312
+ return d.toLocaleDateString("en-US", {
313
+ year: "numeric",
314
+ month: "short",
315
+ day: "numeric"
316
+ });
317
+ }
318
+ function formatPriority(p) {
319
+ switch (p) {
320
+ case "critical":
321
+ return chalk.red.bold("CRITICAL");
322
+ case "high":
323
+ return chalk.hex("#f97316").bold("HIGH");
324
+ case "medium":
325
+ return chalk.yellow("MEDIUM");
326
+ case "low":
327
+ return chalk.gray("LOW");
328
+ default:
329
+ return p;
330
+ }
331
+ }
332
+ function formatStatus(s) {
333
+ switch (s) {
334
+ case "todo":
335
+ return chalk.white("To Do");
336
+ case "in_progress":
337
+ return chalk.blue("In Progress");
338
+ case "review":
339
+ return chalk.magenta("Review");
340
+ case "done":
341
+ return chalk.green("Done");
342
+ default:
343
+ return s;
344
+ }
345
+ }
346
+ function truncateId(id) {
347
+ return id.substring(0, 8);
348
+ }
349
+ function maskEmail(email) {
350
+ if (!isRedactMode()) return email;
351
+ const [local, domain] = email.split("@");
352
+ if (!domain) return email;
353
+ const [domainName, ...tld] = domain.split(".");
354
+ const maskedLocal = local[0] + "***";
355
+ const maskedDomain = domainName[0] + "***";
356
+ return `${maskedLocal}@${maskedDomain}.${tld.join(".")}`;
357
+ }
358
+
359
+ // ../shared/graphql/queries.ts
360
+ var listTasks = (
361
+ /* GraphQL */
362
+ `
363
+ query ListTasks($status: TaskStatus, $archived: Boolean) {
364
+ listTasks(status: $status, archived: $archived) {
365
+ id
366
+ title
367
+ description
368
+ status
369
+ archived
370
+ deletedAt
371
+ priority
372
+ categoryId
373
+ color
374
+ tags
375
+ dueDate
376
+ dueTime
377
+ position
378
+ createdAt
379
+ updatedAt
380
+ }
381
+ }
382
+ `
383
+ );
384
+ var listCategories = (
385
+ /* GraphQL */
386
+ `
387
+ query ListCategories {
388
+ listCategories {
389
+ id
390
+ name
391
+ color
392
+ position
393
+ }
394
+ }
395
+ `
396
+ );
397
+ var listTags = (
398
+ /* GraphQL */
399
+ `
400
+ query ListTags {
401
+ listTags {
402
+ id
403
+ name
404
+ }
405
+ }
406
+ `
407
+ );
408
+ var listTaskShares = (
409
+ /* GraphQL */
410
+ `
411
+ query ListTaskShares($taskId: ID!) {
412
+ listTaskShares(taskId: $taskId) {
413
+ taskId
414
+ ownerId
415
+ ownerEmail
416
+ sharedWithId
417
+ sharedWithEmail
418
+ permission
419
+ status
420
+ sharedAt
421
+ respondedAt
422
+ }
423
+ }
424
+ `
425
+ );
426
+ var listSharedWithMe = (
427
+ /* GraphQL */
428
+ `
429
+ query ListSharedWithMe {
430
+ listSharedWithMe {
431
+ taskId
432
+ ownerId
433
+ ownerEmail
434
+ sharedWithId
435
+ sharedWithEmail
436
+ permission
437
+ status
438
+ sharedAt
439
+ respondedAt
440
+ task {
441
+ id
442
+ userId
443
+ title
444
+ description
445
+ status
446
+ archived
447
+ deletedAt
448
+ priority
449
+ categoryId
450
+ color
451
+ tags
452
+ dueDate
453
+ position
454
+ createdAt
455
+ updatedAt
456
+ }
457
+ }
458
+ }
459
+ `
460
+ );
461
+ var listMyOutgoingTaskShares = (
462
+ /* GraphQL */
463
+ `
464
+ query ListMyOutgoingTaskShares {
465
+ listMyOutgoingTaskShares {
466
+ taskId
467
+ ownerId
468
+ ownerEmail
469
+ sharedWithId
470
+ sharedWithEmail
471
+ permission
472
+ status
473
+ sharedAt
474
+ respondedAt
475
+ task {
476
+ id
477
+ userId
478
+ title
479
+ description
480
+ status
481
+ priority
482
+ dueDate
483
+ createdAt
484
+ updatedAt
485
+ }
486
+ }
487
+ }
488
+ `
489
+ );
490
+ var listLists = (
491
+ /* GraphQL */
492
+ `
493
+ query ListLists {
494
+ listLists {
495
+ id
496
+ name
497
+ description
498
+ icon
499
+ color
500
+ itemCount
501
+ createdAt
502
+ updatedAt
503
+ }
504
+ }
505
+ `
506
+ );
507
+ var listListItems = (
508
+ /* GraphQL */
509
+ `
510
+ query ListListItems($listId: ID!) {
511
+ listListItems(listId: $listId) {
512
+ id
513
+ listId
514
+ name
515
+ description
516
+ quantity
517
+ category
518
+ tags
519
+ link
520
+ customFields
521
+ position
522
+ createdAt
523
+ updatedAt
524
+ }
525
+ }
526
+ `
527
+ );
528
+ var listListShares = (
529
+ /* GraphQL */
530
+ `
531
+ query ListListShares($listId: ID!) {
532
+ listListShares(listId: $listId) {
533
+ listId
534
+ ownerId
535
+ ownerEmail
536
+ sharedWithId
537
+ sharedWithEmail
538
+ permission
539
+ status
540
+ sharedAt
541
+ respondedAt
542
+ }
543
+ }
544
+ `
545
+ );
546
+ var listListsSharedWithMe = (
547
+ /* GraphQL */
548
+ `
549
+ query ListListsSharedWithMe {
550
+ listListsSharedWithMe {
551
+ listId
552
+ ownerId
553
+ ownerEmail
554
+ sharedWithId
555
+ sharedWithEmail
556
+ permission
557
+ status
558
+ sharedAt
559
+ respondedAt
560
+ list {
561
+ id
562
+ name
563
+ description
564
+ icon
565
+ color
566
+ }
567
+ }
568
+ }
569
+ `
570
+ );
571
+ var listMyOutgoingListShares = (
572
+ /* GraphQL */
573
+ `
574
+ query ListMyOutgoingListShares {
575
+ listMyOutgoingListShares {
576
+ listId
577
+ ownerId
578
+ ownerEmail
579
+ sharedWithId
580
+ sharedWithEmail
581
+ permission
582
+ status
583
+ sharedAt
584
+ respondedAt
585
+ list {
586
+ id
587
+ name
588
+ description
589
+ icon
590
+ color
591
+ }
592
+ }
593
+ }
594
+ `
595
+ );
596
+ var listContacts = (
597
+ /* GraphQL */
598
+ `
599
+ query ListContacts {
600
+ listContacts {
601
+ id
602
+ name
603
+ email
604
+ createdAt
605
+ updatedAt
606
+ }
607
+ }
608
+ `
609
+ );
610
+ var listNotes = `
611
+ query ListNotes($taskId: ID!) {
612
+ listNotes(taskId: $taskId) {
613
+ id
614
+ taskId
615
+ content
616
+ authorId
617
+ authorName
618
+ createdAt
619
+ }
620
+ }
621
+ `;
622
+ var listApiKeys = (
623
+ /* GraphQL */
624
+ `
625
+ query ListApiKeys {
626
+ listApiKeys {
627
+ id
628
+ name
629
+ keyPrefix
630
+ status
631
+ createdAt
632
+ lastUsedAt
633
+ revokedAt
634
+ }
635
+ }
636
+ `
637
+ );
638
+ var getPreferences = `
639
+ query GetPreferences {
640
+ getPreferences {
641
+ theme
642
+ defaultView
643
+ archiveDelay
644
+ colorPalette
645
+ colorRules
646
+ notificationPrefs
647
+ filterPresets
648
+ defaultPriority
649
+ defaultCategoryId
650
+ defaultColor
651
+ updatedAt
652
+ }
653
+ }
654
+ `;
655
+
656
+ // src/commands/auth.ts
657
+ var whoamiCommand = new Command("whoami").description("Verify API key and show key info").action(async () => {
658
+ try {
659
+ const data = await loadApiKeyData();
660
+ if (!data) {
661
+ printError("No API key configured. Run `tasklumina configure` first.");
662
+ process.exitCode = 1;
663
+ return;
664
+ }
665
+ const result = await graphql(listApiKeys);
666
+ const activeKeys = result.listApiKeys.filter((k) => k.status === "active");
667
+ const prefix = data.keyPrefix ?? data.key.slice(0, 12);
668
+ if (isJsonMode()) {
669
+ printJson({
670
+ keyPrefix: prefix,
671
+ activeKeyCount: activeKeys.length,
672
+ lastRotatedAt: data.lastRotatedAt
673
+ });
674
+ return;
675
+ }
676
+ printSuccess("API key is valid.");
677
+ console.log(` Key prefix: ${prefix}...`);
678
+ console.log(` Last rotated: ${data.lastRotatedAt}`);
679
+ console.log(` Active keys: ${activeKeys.length}`);
680
+ } catch (err) {
681
+ printError(err.message);
682
+ process.exitCode = 1;
683
+ }
684
+ });
685
+
686
+ // src/commands/configure.ts
687
+ import { Command as Command2 } from "commander";
688
+ import { password } from "@inquirer/prompts";
689
+
690
+ // src/lib/auto-rotate.ts
691
+ import { resolve as resolve3 } from "path";
692
+ import { homedir as homedir3 } from "os";
693
+ import { mkdirSync as mkdirSync2, rmSync, statSync } from "fs";
694
+
695
+ // ../shared/graphql/mutations.ts
696
+ var createTask = (
697
+ /* GraphQL */
698
+ `
699
+ mutation CreateTask($input: CreateTaskInput!) {
700
+ createTask(input: $input) {
701
+ id
702
+ title
703
+ description
704
+ status
705
+ archived
706
+ deletedAt
707
+ priority
708
+ categoryId
709
+ color
710
+ tags
711
+ dueDate
712
+ dueTime
713
+ position
714
+ createdAt
715
+ updatedAt
716
+ }
717
+ }
718
+ `
719
+ );
720
+ var updateTask = (
721
+ /* GraphQL */
722
+ `
723
+ mutation UpdateTask($input: UpdateTaskInput!) {
724
+ updateTask(input: $input) {
725
+ id
726
+ title
727
+ description
728
+ status
729
+ archived
730
+ deletedAt
731
+ priority
732
+ categoryId
733
+ color
734
+ tags
735
+ dueDate
736
+ dueTime
737
+ position
738
+ createdAt
739
+ updatedAt
740
+ }
741
+ }
742
+ `
743
+ );
744
+ var deleteTask = (
745
+ /* GraphQL */
746
+ `
747
+ mutation DeleteTask($id: ID!) {
748
+ deleteTask(id: $id)
749
+ }
750
+ `
751
+ );
752
+ var createCategory = (
753
+ /* GraphQL */
754
+ `
755
+ mutation CreateCategory($input: CreateCategoryInput!) {
756
+ createCategory(input: $input) {
757
+ id
758
+ name
759
+ color
760
+ position
761
+ }
762
+ }
763
+ `
764
+ );
765
+ var updateCategory = (
766
+ /* GraphQL */
767
+ `
768
+ mutation UpdateCategory($input: UpdateCategoryInput!) {
769
+ updateCategory(input: $input) {
770
+ id
771
+ name
772
+ color
773
+ position
774
+ }
775
+ }
776
+ `
777
+ );
778
+ var deleteCategory = (
779
+ /* GraphQL */
780
+ `
781
+ mutation DeleteCategory($id: ID!) {
782
+ deleteCategory(id: $id)
783
+ }
784
+ `
785
+ );
786
+ var createTag = (
787
+ /* GraphQL */
788
+ `
789
+ mutation CreateTag($input: CreateTagInput!) {
790
+ createTag(input: $input) {
791
+ id
792
+ name
793
+ }
794
+ }
795
+ `
796
+ );
797
+ var updateTag = (
798
+ /* GraphQL */
799
+ `
800
+ mutation UpdateTag($input: UpdateTagInput!) {
801
+ updateTag(input: $input) {
802
+ id
803
+ name
804
+ }
805
+ }
806
+ `
807
+ );
808
+ var deleteTagMutation = (
809
+ /* GraphQL */
810
+ `
811
+ mutation DeleteTag($id: ID!) {
812
+ deleteTag(id: $id)
813
+ }
814
+ `
815
+ );
816
+ var shareTask = (
817
+ /* GraphQL */
818
+ `
819
+ mutation ShareTask($input: ShareTaskInput!) {
820
+ shareTask(input: $input) {
821
+ taskId
822
+ ownerId
823
+ ownerEmail
824
+ sharedWithId
825
+ sharedWithEmail
826
+ permission
827
+ status
828
+ sharedAt
829
+ respondedAt
830
+ }
831
+ }
832
+ `
833
+ );
834
+ var acceptShare = (
835
+ /* GraphQL */
836
+ `
837
+ mutation AcceptShare($taskId: ID!, $ownerId: ID!) {
838
+ acceptShare(taskId: $taskId, ownerId: $ownerId) {
839
+ taskId
840
+ ownerId
841
+ ownerEmail
842
+ sharedWithId
843
+ sharedWithEmail
844
+ permission
845
+ status
846
+ sharedAt
847
+ respondedAt
848
+ }
849
+ }
850
+ `
851
+ );
852
+ var declineShare = (
853
+ /* GraphQL */
854
+ `
855
+ mutation DeclineShare($taskId: ID!, $ownerId: ID!) {
856
+ declineShare(taskId: $taskId, ownerId: $ownerId) {
857
+ taskId
858
+ ownerId
859
+ ownerEmail
860
+ sharedWithId
861
+ sharedWithEmail
862
+ permission
863
+ status
864
+ sharedAt
865
+ respondedAt
866
+ }
867
+ }
868
+ `
869
+ );
870
+ var revokeShare = (
871
+ /* GraphQL */
872
+ `
873
+ mutation RevokeShare($taskId: ID!, $sharedWithId: ID!) {
874
+ revokeShare(taskId: $taskId, sharedWithId: $sharedWithId)
875
+ }
876
+ `
877
+ );
878
+ var updateSharePermission = (
879
+ /* GraphQL */
880
+ `
881
+ mutation UpdateSharePermission($input: UpdateSharePermissionInput!) {
882
+ updateSharePermission(input: $input) {
883
+ taskId
884
+ ownerId
885
+ ownerEmail
886
+ sharedWithId
887
+ sharedWithEmail
888
+ permission
889
+ status
890
+ sharedAt
891
+ respondedAt
892
+ }
893
+ }
894
+ `
895
+ );
896
+ var createList = (
897
+ /* GraphQL */
898
+ `
899
+ mutation CreateList($input: CreateListInput!) {
900
+ createList(input: $input) {
901
+ id
902
+ name
903
+ description
904
+ icon
905
+ color
906
+ itemCount
907
+ createdAt
908
+ updatedAt
909
+ }
910
+ }
911
+ `
912
+ );
913
+ var updateList = (
914
+ /* GraphQL */
915
+ `
916
+ mutation UpdateList($input: UpdateListInput!) {
917
+ updateList(input: $input) {
918
+ id
919
+ name
920
+ description
921
+ icon
922
+ color
923
+ itemCount
924
+ createdAt
925
+ updatedAt
926
+ }
927
+ }
928
+ `
929
+ );
930
+ var deleteListMutation = (
931
+ /* GraphQL */
932
+ `
933
+ mutation DeleteList($id: ID!) {
934
+ deleteList(id: $id)
935
+ }
936
+ `
937
+ );
938
+ var createListItem = (
939
+ /* GraphQL */
940
+ `
941
+ mutation CreateListItem($input: CreateListItemInput!) {
942
+ createListItem(input: $input) {
943
+ id
944
+ listId
945
+ name
946
+ description
947
+ quantity
948
+ category
949
+ tags
950
+ link
951
+ customFields
952
+ position
953
+ createdAt
954
+ updatedAt
955
+ }
956
+ }
957
+ `
958
+ );
959
+ var updateListItem = (
960
+ /* GraphQL */
961
+ `
962
+ mutation UpdateListItem($input: UpdateListItemInput!) {
963
+ updateListItem(input: $input) {
964
+ id
965
+ listId
966
+ name
967
+ description
968
+ quantity
969
+ category
970
+ tags
971
+ link
972
+ customFields
973
+ position
974
+ createdAt
975
+ updatedAt
976
+ }
977
+ }
978
+ `
979
+ );
980
+ var deleteListItemMutation = (
981
+ /* GraphQL */
982
+ `
983
+ mutation DeleteListItem($id: ID!, $listId: ID!) {
984
+ deleteListItem(id: $id, listId: $listId)
985
+ }
986
+ `
987
+ );
988
+ var bulkCreateListItems = (
989
+ /* GraphQL */
990
+ `
991
+ mutation BulkCreateListItems($input: BulkCreateListItemsInput!) {
992
+ bulkCreateListItems(input: $input) {
993
+ id
994
+ listId
995
+ name
996
+ description
997
+ quantity
998
+ category
999
+ tags
1000
+ link
1001
+ customFields
1002
+ position
1003
+ createdAt
1004
+ updatedAt
1005
+ }
1006
+ }
1007
+ `
1008
+ );
1009
+ var shareList = (
1010
+ /* GraphQL */
1011
+ `
1012
+ mutation ShareList($input: ShareListInput!) {
1013
+ shareList(input: $input) {
1014
+ listId
1015
+ ownerId
1016
+ ownerEmail
1017
+ sharedWithId
1018
+ sharedWithEmail
1019
+ permission
1020
+ status
1021
+ sharedAt
1022
+ respondedAt
1023
+ }
1024
+ }
1025
+ `
1026
+ );
1027
+ var acceptListShare = (
1028
+ /* GraphQL */
1029
+ `
1030
+ mutation AcceptListShare($listId: ID!, $ownerId: ID!) {
1031
+ acceptListShare(listId: $listId, ownerId: $ownerId) {
1032
+ listId
1033
+ ownerId
1034
+ ownerEmail
1035
+ sharedWithId
1036
+ sharedWithEmail
1037
+ permission
1038
+ status
1039
+ sharedAt
1040
+ respondedAt
1041
+ }
1042
+ }
1043
+ `
1044
+ );
1045
+ var declineListShare = (
1046
+ /* GraphQL */
1047
+ `
1048
+ mutation DeclineListShare($listId: ID!, $ownerId: ID!) {
1049
+ declineListShare(listId: $listId, ownerId: $ownerId) {
1050
+ listId
1051
+ ownerId
1052
+ ownerEmail
1053
+ sharedWithId
1054
+ sharedWithEmail
1055
+ permission
1056
+ status
1057
+ sharedAt
1058
+ respondedAt
1059
+ }
1060
+ }
1061
+ `
1062
+ );
1063
+ var revokeListShare = (
1064
+ /* GraphQL */
1065
+ `
1066
+ mutation RevokeListShare($listId: ID!, $sharedWithId: ID!) {
1067
+ revokeListShare(listId: $listId, sharedWithId: $sharedWithId)
1068
+ }
1069
+ `
1070
+ );
1071
+ var updateListSharePermission = (
1072
+ /* GraphQL */
1073
+ `
1074
+ mutation UpdateListSharePermission($input: UpdateListSharePermissionInput!) {
1075
+ updateListSharePermission(input: $input) {
1076
+ listId
1077
+ ownerId
1078
+ ownerEmail
1079
+ sharedWithId
1080
+ sharedWithEmail
1081
+ permission
1082
+ status
1083
+ sharedAt
1084
+ respondedAt
1085
+ }
1086
+ }
1087
+ `
1088
+ );
1089
+ var createNote = `
1090
+ mutation CreateNote($input: CreateNoteInput!) {
1091
+ createNote(input: $input) {
1092
+ id
1093
+ taskId
1094
+ content
1095
+ authorId
1096
+ authorName
1097
+ createdAt
1098
+ }
1099
+ }
1100
+ `;
1101
+ var deleteNote = `
1102
+ mutation DeleteNote($taskId: ID!, $noteId: ID!, $createdAt: AWSDateTime!) {
1103
+ deleteNote(taskId: $taskId, noteId: $noteId, createdAt: $createdAt)
1104
+ }
1105
+ `;
1106
+ var createContact = (
1107
+ /* GraphQL */
1108
+ `
1109
+ mutation CreateContact($input: CreateContactInput!) {
1110
+ createContact(input: $input) {
1111
+ id
1112
+ name
1113
+ email
1114
+ createdAt
1115
+ updatedAt
1116
+ }
1117
+ }
1118
+ `
1119
+ );
1120
+ var updateContact = (
1121
+ /* GraphQL */
1122
+ `
1123
+ mutation UpdateContact($input: UpdateContactInput!) {
1124
+ updateContact(input: $input) {
1125
+ id
1126
+ name
1127
+ email
1128
+ createdAt
1129
+ updatedAt
1130
+ }
1131
+ }
1132
+ `
1133
+ );
1134
+ var deleteContactMutation = (
1135
+ /* GraphQL */
1136
+ `
1137
+ mutation DeleteContact($id: ID!) {
1138
+ deleteContact(id: $id)
1139
+ }
1140
+ `
1141
+ );
1142
+ var revokeApiKey = (
1143
+ /* GraphQL */
1144
+ `
1145
+ mutation RevokeApiKey($id: ID!) {
1146
+ revokeApiKey(id: $id) {
1147
+ id
1148
+ name
1149
+ keyPrefix
1150
+ status
1151
+ createdAt
1152
+ lastUsedAt
1153
+ revokedAt
1154
+ }
1155
+ }
1156
+ `
1157
+ );
1158
+ var rotateSelfApiKey = (
1159
+ /* GraphQL */
1160
+ `
1161
+ mutation RotateSelfApiKey {
1162
+ rotateSelfApiKey {
1163
+ revokedKeyId
1164
+ newKey {
1165
+ id
1166
+ name
1167
+ keyPrefix
1168
+ key
1169
+ createdAt
1170
+ }
1171
+ }
1172
+ }
1173
+ `
1174
+ );
1175
+ var updatePreferences = `
1176
+ mutation UpdatePreferences($input: UpdatePreferencesInput!) {
1177
+ updatePreferences(input: $input) {
1178
+ theme
1179
+ defaultView
1180
+ archiveDelay
1181
+ colorPalette
1182
+ colorRules
1183
+ notificationPrefs
1184
+ filterPresets
1185
+ defaultPriority
1186
+ defaultCategoryId
1187
+ defaultColor
1188
+ updatedAt
1189
+ }
1190
+ }
1191
+ `;
1192
+
1193
+ // src/lib/auto-rotate.ts
1194
+ var LOCK_DIR = resolve3(homedir3(), ".tasklumina", ".rotate-lock");
1195
+ var LOCK_STALE_MS = 6e4;
1196
+ function acquireLock() {
1197
+ try {
1198
+ try {
1199
+ const stat = statSync(LOCK_DIR);
1200
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
1201
+ rmSync(LOCK_DIR, { recursive: true, force: true });
1202
+ }
1203
+ } catch {
1204
+ }
1205
+ mkdirSync2(LOCK_DIR, { recursive: false });
1206
+ return true;
1207
+ } catch {
1208
+ return false;
1209
+ }
1210
+ }
1211
+ function releaseLock() {
1212
+ try {
1213
+ rmSync(LOCK_DIR, { recursive: true, force: true });
1214
+ } catch {
1215
+ }
1216
+ }
1217
+ async function performSelfRotation() {
1218
+ const data = await graphql(rotateSelfApiKey);
1219
+ const result = data.rotateSelfApiKey.newKey;
1220
+ await saveApiKeyData({
1221
+ key: result.key,
1222
+ keyPrefix: result.keyPrefix,
1223
+ lastRotatedAt: (/* @__PURE__ */ new Date()).toISOString()
1224
+ });
1225
+ return result.key;
1226
+ }
1227
+ async function maybeAutoRotate() {
1228
+ try {
1229
+ if (process.env.TASKLUMINA_API_KEY) return;
1230
+ const data = await loadApiKeyData();
1231
+ if (!data) return;
1232
+ const lastRotated = new Date(data.lastRotatedAt).getTime();
1233
+ const hoursSince = (Date.now() - lastRotated) / (1e3 * 60 * 60);
1234
+ if (hoursSince < CONFIG.autoRotateIntervalHours) return;
1235
+ if (!acquireLock()) return;
1236
+ try {
1237
+ const freshData = await loadApiKeyData();
1238
+ if (freshData) {
1239
+ const freshHours = (Date.now() - new Date(freshData.lastRotatedAt).getTime()) / (1e3 * 60 * 60);
1240
+ if (freshHours < CONFIG.autoRotateIntervalHours) return;
1241
+ }
1242
+ await performSelfRotation();
1243
+ } finally {
1244
+ releaseLock();
1245
+ }
1246
+ } catch {
1247
+ releaseLock();
1248
+ }
1249
+ }
1250
+
1251
+ // src/commands/configure.ts
1252
+ var configureCommand = new Command2("configure").description("Configure CLI authentication with an API key").action(async () => {
1253
+ try {
1254
+ const key = await password({ message: "Enter your API key:" });
1255
+ if (!key.startsWith("tl_live_")) {
1256
+ printError("Invalid API key format. Keys must start with tl_live_");
1257
+ process.exitCode = 1;
1258
+ return;
1259
+ }
1260
+ await saveApiKeyData({
1261
+ key,
1262
+ keyPrefix: key.slice(0, 12),
1263
+ lastRotatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
1264
+ });
1265
+ printWarning("Rotating key for security (the key you entered will be revoked)...");
1266
+ try {
1267
+ const newKey = await performSelfRotation();
1268
+ printSuccess("API key configured and rotated successfully.");
1269
+ console.log(` Active key prefix: ${newKey.slice(0, 12)}...`);
1270
+ printWarning("The key you entered has been revoked. Your CLI now uses a new key stored securely in the OS keychain.");
1271
+ } catch (rotateErr) {
1272
+ const msg = rotateErr instanceof Error ? rotateErr.message : "Unknown error";
1273
+ printWarning(`Key saved but auto-rotation failed: ${msg}`);
1274
+ printWarning("The original key is still active and stored in your OS keychain.");
1275
+ printWarning("Rotation will be attempted automatically on your next command.");
1276
+ }
1277
+ } catch (err) {
1278
+ printError(err.message);
1279
+ process.exitCode = 1;
1280
+ }
1281
+ });
1282
+
1283
+ // src/commands/tasks.ts
1284
+ import { Command as Command3 } from "commander";
1285
+ import { confirm } from "@inquirer/prompts";
1286
+
1287
+ // src/lib/validators.ts
1288
+ var VALID_STATUSES = ["todo", "in_progress", "review", "done"];
1289
+ var VALID_PRIORITIES = ["critical", "high", "medium", "low"];
1290
+ var VALID_PERMISSIONS = ["view", "edit"];
1291
+ var VALID_THEMES = ["light", "dark", "system"];
1292
+ var VALID_VIEWS = ["list", "kanban", "priority", "category"];
1293
+ function validateEnum(value, allowed, label) {
1294
+ if (!allowed.includes(value)) {
1295
+ throw new Error(`Invalid ${label}: "${value}". Must be one of: ${allowed.join(", ")}`);
1296
+ }
1297
+ }
1298
+ function validateStatus(value) {
1299
+ validateEnum(value, VALID_STATUSES, "status");
1300
+ }
1301
+ function validatePriority(value) {
1302
+ validateEnum(value, VALID_PRIORITIES, "priority");
1303
+ }
1304
+ function validatePermission(value) {
1305
+ validateEnum(value, VALID_PERMISSIONS, "permission");
1306
+ }
1307
+ function validateTheme(value) {
1308
+ validateEnum(value, VALID_THEMES, "theme");
1309
+ }
1310
+ function validateView(value) {
1311
+ validateEnum(value, VALID_VIEWS, "default view");
1312
+ }
1313
+ function validateTime(value) {
1314
+ if (!/^\d{2}:\d{2}$/.test(value)) {
1315
+ throw new Error(`Invalid time: "${value}". Expected format: HH:MM (e.g. 09:30)`);
1316
+ }
1317
+ const [h, m] = value.split(":").map(Number);
1318
+ if (h > 23 || m > 59) {
1319
+ throw new Error(`Invalid time: "${value}". Hours must be 0-23 and minutes 0-59.`);
1320
+ }
1321
+ }
1322
+ function validateDate(value) {
1323
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1324
+ throw new Error(`Invalid date: "${value}". Expected format: YYYY-MM-DD`);
1325
+ }
1326
+ const [year, month, day] = value.split("-").map(Number);
1327
+ const d = new Date(value);
1328
+ if (isNaN(d.getTime()) || d.getUTCFullYear() !== year || d.getUTCMonth() + 1 !== month || d.getUTCDate() !== day) {
1329
+ throw new Error(`Invalid date: "${value}". Day does not exist in that month.`);
1330
+ }
1331
+ }
1332
+ function validateEmail(value) {
1333
+ if (value.length > 254) {
1334
+ throw new Error(`Email too long (max 254 characters): "${value}"`);
1335
+ }
1336
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
1337
+ throw new Error(`Invalid email address: "${value}"`);
1338
+ }
1339
+ }
1340
+ function validateUrl(value) {
1341
+ try {
1342
+ const url = new URL(value);
1343
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1344
+ throw new Error("Only http and https URLs are allowed");
1345
+ }
1346
+ } catch {
1347
+ throw new Error(`Invalid URL: "${value}". Must be a valid http or https URL.`);
1348
+ }
1349
+ }
1350
+ function validateUuid(value, label = "ID") {
1351
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
1352
+ if (!/^[0-9a-f]{6,}$/i.test(value)) {
1353
+ throw new Error(`Invalid ${label}: "${value}". Must be a UUID or at least a 6-character hex prefix.`);
1354
+ }
1355
+ }
1356
+ }
1357
+ function validateStringLength(value, field, max) {
1358
+ if (value.length > max) {
1359
+ throw new Error(`${field} too long (${value.length} chars, max ${max}).`);
1360
+ }
1361
+ }
1362
+ function validateHexColor(value) {
1363
+ if (!/^#[0-9A-Fa-f]{6}$/.test(value)) {
1364
+ throw new Error(`Invalid color: "${value}". Must be a hex color like #FF5733.`);
1365
+ }
1366
+ }
1367
+ function validateQuantity(value) {
1368
+ const n = parseInt(value, 10);
1369
+ if (isNaN(n) || n < 0 || n > 99999) {
1370
+ throw new Error(`Invalid quantity: "${value}". Must be a number between 0 and 99999.`);
1371
+ }
1372
+ return n;
1373
+ }
1374
+
1375
+ // src/commands/tasks.ts
1376
+ var PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
1377
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1378
+ async function resolveTaskId(idOrPrefix) {
1379
+ if (UUID_RE.test(idOrPrefix)) return idOrPrefix;
1380
+ const data = await graphql(listTasks, {});
1381
+ const archivedData = await graphql(listTasks, { archived: true });
1382
+ const allTasks = [...data.listTasks, ...archivedData.listTasks];
1383
+ const matches = allTasks.filter((t) => t.id.startsWith(idOrPrefix));
1384
+ if (matches.length === 0) throw new Error(`No task found matching ID prefix: ${idOrPrefix}`);
1385
+ if (matches.length > 1) {
1386
+ const list = matches.map((t) => ` ${truncateId(t.id)} \u2014 ${t.title}`).join("\n");
1387
+ throw new Error(`Ambiguous ID prefix "${idOrPrefix}" matches ${matches.length} tasks:
1388
+ ${list}`);
1389
+ }
1390
+ return matches[0].id;
1391
+ }
1392
+ function escapeCsv(value) {
1393
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1394
+ return `"${value.replace(/"/g, '""')}"`;
1395
+ }
1396
+ return value;
1397
+ }
1398
+ function tasksToCsv(tasks) {
1399
+ const headers = ["ID", "Title", "Status", "Priority", "Due Date", "Due Time", "Category", "Tags", "Created", "Updated"];
1400
+ const rows = tasks.map((t) => [
1401
+ t.id,
1402
+ t.title,
1403
+ t.status,
1404
+ t.priority,
1405
+ t.dueDate || "",
1406
+ t.dueTime || "",
1407
+ t.categoryId || "",
1408
+ t.tags.join(";"),
1409
+ t.createdAt,
1410
+ t.updatedAt
1411
+ ].map(escapeCsv).join(","));
1412
+ return [headers.join(","), ...rows].join("\n");
1413
+ }
1414
+ var tasksCommand = new Command3("tasks").description("Manage tasks").option("--status <status>", "Filter by status (todo, in_progress, review, done)").option("--priority <priority>", "Filter by priority (critical, high, medium, low)").option("--completed", "Show completed (archived) tasks instead of active tasks").option("--archived", "Show all archived tasks including trash").option("--category <id>", "Filter by category ID").option("--tag <tag>", "Filter by tag name").option("--due-on <date>", "Tasks due on exactly this date (YYYY-MM-DD)").option("--due-before <date>", "Tasks due before date (YYYY-MM-DD)").option("--due-after <date>", "Tasks due after date (YYYY-MM-DD)").option("--overdue", "Show only overdue tasks").option("--sort <field>", "Sort by: due, priority, created, updated, title").option("--desc", "Sort descending (default: ascending)").option("--csv", "Export as CSV").action(async (opts) => {
1415
+ try {
1416
+ if (opts.status) validateStatus(opts.status);
1417
+ if (opts.priority) validatePriority(opts.priority);
1418
+ if (opts.category) validateUuid(opts.category, "Category ID");
1419
+ if (opts.dueOn) validateDate(opts.dueOn);
1420
+ if (opts.dueBefore) validateDate(opts.dueBefore);
1421
+ if (opts.dueAfter) validateDate(opts.dueAfter);
1422
+ const variables = {};
1423
+ if (opts.status) variables.status = opts.status;
1424
+ if (opts.completed || opts.archived) {
1425
+ variables.archived = true;
1426
+ } else {
1427
+ variables.archived = false;
1428
+ }
1429
+ const data = await graphql(listTasks, variables);
1430
+ let tasks = data.listTasks;
1431
+ if (opts.completed) {
1432
+ tasks = tasks.filter((t) => !t.deletedAt);
1433
+ }
1434
+ if (opts.dueOn) {
1435
+ tasks = tasks.filter((t) => t.dueDate === opts.dueOn);
1436
+ }
1437
+ if (opts.priority) {
1438
+ tasks = tasks.filter((t) => t.priority === opts.priority);
1439
+ }
1440
+ if (opts.category) {
1441
+ tasks = tasks.filter((t) => t.categoryId === opts.category);
1442
+ }
1443
+ if (opts.tag) {
1444
+ const tag = opts.tag.toLowerCase();
1445
+ tasks = tasks.filter((t) => t.tags.some((tg) => tg.toLowerCase() === tag));
1446
+ }
1447
+ if (opts.dueBefore) {
1448
+ const before = new Date(opts.dueBefore);
1449
+ tasks = tasks.filter((t) => t.dueDate && new Date(t.dueDate) <= before);
1450
+ }
1451
+ if (opts.dueAfter) {
1452
+ const after = new Date(opts.dueAfter);
1453
+ tasks = tasks.filter((t) => t.dueDate && new Date(t.dueDate) >= after);
1454
+ }
1455
+ if (opts.overdue) {
1456
+ const now = /* @__PURE__ */ new Date();
1457
+ tasks = tasks.filter((t) => {
1458
+ if (t.status === "done" || !t.dueDate) return false;
1459
+ const dueMs = t.dueTime ? (/* @__PURE__ */ new Date(`${t.dueDate}T${t.dueTime}`)).getTime() : (/* @__PURE__ */ new Date(`${t.dueDate}T23:59:59`)).getTime();
1460
+ return dueMs < now.getTime();
1461
+ });
1462
+ }
1463
+ if (opts.sort) {
1464
+ const dir = opts.desc ? -1 : 1;
1465
+ tasks.sort((a, b) => {
1466
+ switch (opts.sort) {
1467
+ case "due": {
1468
+ if (!a.dueDate && !b.dueDate) return 0;
1469
+ if (!a.dueDate) return 1;
1470
+ if (!b.dueDate) return -1;
1471
+ return dir * (new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime());
1472
+ }
1473
+ case "priority":
1474
+ return dir * ((PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9));
1475
+ case "created":
1476
+ return dir * (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
1477
+ case "updated":
1478
+ return dir * (new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime());
1479
+ case "title":
1480
+ return dir * a.title.localeCompare(b.title);
1481
+ default:
1482
+ return 0;
1483
+ }
1484
+ });
1485
+ }
1486
+ if (opts.csv) {
1487
+ console.log(tasksToCsv(tasks));
1488
+ return;
1489
+ }
1490
+ if (isJsonMode()) {
1491
+ printJson(tasks);
1492
+ return;
1493
+ }
1494
+ if (tasks.length === 0) {
1495
+ console.log("No tasks found.");
1496
+ return;
1497
+ }
1498
+ printTable(
1499
+ ["ID", "Title", "Status", "Priority", "Due Date"],
1500
+ tasks.map((t) => [
1501
+ truncateId(t.id),
1502
+ t.title.length > 40 ? t.title.substring(0, 37) + "..." : t.title,
1503
+ formatStatus(t.status),
1504
+ formatPriority(t.priority),
1505
+ formatDate(t.dueDate)
1506
+ ])
1507
+ );
1508
+ } catch (err) {
1509
+ printError(err.message);
1510
+ process.exitCode = 1;
1511
+ }
1512
+ });
1513
+ tasksCommand.command("create").description("Create a new task").argument("<title>", "Task title").option("--desc <description>", "Task description").option("--status <status>", "Task status", "todo").option("--priority <priority>", "Task priority", "medium").option("--category <id>", "Category ID").option("--tags <tags>", "Comma-separated tags").option("--due <date>", "Due date (YYYY-MM-DD)").option("--due-time <time>", "Due time (HH:MM, 24-hour). Requires --due.").option("--color <color>", "Task color").action(async (title, opts) => {
1514
+ try {
1515
+ validateStringLength(title, "Title", 500);
1516
+ validateStatus(opts.status);
1517
+ validatePriority(opts.priority);
1518
+ if (opts.due) validateDate(opts.due);
1519
+ if (opts.dueTime) {
1520
+ if (!opts.due) {
1521
+ printError("--due-time requires --due to be set.");
1522
+ process.exitCode = 1;
1523
+ return;
1524
+ }
1525
+ validateTime(opts.dueTime);
1526
+ }
1527
+ if (opts.color) validateHexColor(opts.color);
1528
+ if (opts.desc) validateStringLength(opts.desc, "Description", 5e3);
1529
+ const taskInput = {
1530
+ title,
1531
+ status: opts.status,
1532
+ priority: opts.priority,
1533
+ tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : []
1534
+ };
1535
+ if (opts.desc) taskInput.description = opts.desc;
1536
+ if (opts.category) taskInput.categoryId = opts.category;
1537
+ if (opts.due) taskInput.dueDate = opts.due;
1538
+ if (opts.dueTime) taskInput.dueTime = opts.dueTime;
1539
+ if (opts.color) taskInput.color = opts.color;
1540
+ const data = await graphql(createTask, { input: taskInput });
1541
+ if (isJsonMode()) {
1542
+ printJson(data.createTask);
1543
+ return;
1544
+ }
1545
+ printSuccess(`Created task: ${data.createTask.title} (id: ${truncateId(data.createTask.id)})`);
1546
+ } catch (err) {
1547
+ printError(err.message);
1548
+ process.exitCode = 1;
1549
+ }
1550
+ });
1551
+ tasksCommand.command("update").description("Update a task").argument("<id>", "Task ID").option("--title <title>", "New title").option("--desc <description>", "New description").option("--status <status>", "New status").option("--priority <priority>", "New priority").option("--category <id>", "New category ID").option("--tags <tags>", "New tags (comma-separated)").option("--due <date>", "New due date (YYYY-MM-DD)").option("--due-time <time>", "Set due time (HH:MM, 24-hour)").option("--all-day", "Remove due time (make all-day)").option("--color <color>", "New color").action(async (id, opts) => {
1552
+ try {
1553
+ validateUuid(id, "Task ID");
1554
+ const resolvedId = await resolveTaskId(id);
1555
+ if (opts.status) validateStatus(opts.status);
1556
+ if (opts.priority) validatePriority(opts.priority);
1557
+ if (opts.due) validateDate(opts.due);
1558
+ if (opts.dueTime) validateTime(opts.dueTime);
1559
+ if (opts.dueTime && opts.allDay) {
1560
+ printError("--due-time and --all-day are mutually exclusive.");
1561
+ process.exitCode = 1;
1562
+ return;
1563
+ }
1564
+ if (opts.color) validateHexColor(opts.color);
1565
+ if (opts.title) validateStringLength(opts.title, "Title", 500);
1566
+ if (opts.desc) validateStringLength(opts.desc, "Description", 5e3);
1567
+ const taskInput = { id: resolvedId };
1568
+ if (opts.title) taskInput.title = opts.title;
1569
+ if (opts.desc) taskInput.description = opts.desc;
1570
+ if (opts.status) taskInput.status = opts.status;
1571
+ if (opts.priority) taskInput.priority = opts.priority;
1572
+ if (opts.category) taskInput.categoryId = opts.category;
1573
+ if (opts.tags) taskInput.tags = opts.tags.split(",").map((t) => t.trim());
1574
+ if (opts.due) taskInput.dueDate = opts.due;
1575
+ if (opts.dueTime) taskInput.dueTime = opts.dueTime;
1576
+ if (opts.allDay) taskInput.dueTime = null;
1577
+ if (opts.color) taskInput.color = opts.color;
1578
+ const data = await graphql(updateTask, { input: taskInput });
1579
+ if (isJsonMode()) {
1580
+ printJson(data.updateTask);
1581
+ return;
1582
+ }
1583
+ printSuccess(`Updated task: ${data.updateTask.title}`);
1584
+ } catch (err) {
1585
+ printError(err.message);
1586
+ process.exitCode = 1;
1587
+ }
1588
+ });
1589
+ function addStatusShortcut(name, targetStatus, pastTense) {
1590
+ tasksCommand.command(name).description(`Mark a task as ${targetStatus.replace("_", " ")}`).argument("<id>", "Task ID").action(async (id) => {
1591
+ try {
1592
+ validateUuid(id, "Task ID");
1593
+ const resolvedId = await resolveTaskId(id);
1594
+ const data = await graphql(updateTask, {
1595
+ input: { id: resolvedId, status: targetStatus }
1596
+ });
1597
+ if (isJsonMode()) {
1598
+ printJson(data.updateTask);
1599
+ return;
1600
+ }
1601
+ printSuccess(`${pastTense}: ${data.updateTask.title}`);
1602
+ } catch (err) {
1603
+ printError(err.message);
1604
+ process.exitCode = 1;
1605
+ }
1606
+ });
1607
+ }
1608
+ addStatusShortcut("start", "in_progress", "Started");
1609
+ addStatusShortcut("review", "review", "Moved to review");
1610
+ addStatusShortcut("done", "done", "Marked done");
1611
+ tasksCommand.command("delete").description("Delete a task (soft delete)").argument("<id>", "Task ID").option("--force", "Skip confirmation").action(async (id, opts) => {
1612
+ try {
1613
+ validateUuid(id, "Task ID");
1614
+ const resolvedId = await resolveTaskId(id);
1615
+ if (!opts.force) {
1616
+ const ok = await confirm({ message: `Delete task ${truncateId(resolvedId)}?`, default: false });
1617
+ if (!ok) {
1618
+ console.log("Cancelled.");
1619
+ return;
1620
+ }
1621
+ }
1622
+ await graphql(deleteTask, { id: resolvedId });
1623
+ if (isJsonMode()) {
1624
+ printJson({ deleted: resolvedId });
1625
+ return;
1626
+ }
1627
+ printSuccess(`Deleted task: ${truncateId(resolvedId)}`);
1628
+ } catch (err) {
1629
+ printError(err.message);
1630
+ process.exitCode = 1;
1631
+ }
1632
+ });
1633
+ tasksCommand.command("show").description("Show detailed task info").argument("<id>", "Task ID").action(async (id) => {
1634
+ try {
1635
+ validateUuid(id, "Task ID");
1636
+ const resolvedId = await resolveTaskId(id);
1637
+ const data = await graphql(listTasks, {});
1638
+ const archivedData = await graphql(listTasks, { archived: true });
1639
+ const allTasks = [...data.listTasks, ...archivedData.listTasks];
1640
+ const task = allTasks.find((t) => t.id === resolvedId);
1641
+ if (!task) {
1642
+ printError(`Task not found: ${id}`);
1643
+ process.exitCode = 1;
1644
+ return;
1645
+ }
1646
+ const [notesData, sharesData] = await Promise.all([
1647
+ graphql(
1648
+ listNotes,
1649
+ { taskId: task.id }
1650
+ ).catch(() => ({ listNotes: [] })),
1651
+ graphql(
1652
+ listTaskShares,
1653
+ { taskId: task.id }
1654
+ ).catch(() => ({ listTaskShares: [] }))
1655
+ ]);
1656
+ if (isJsonMode()) {
1657
+ printJson({ ...task, notes: notesData.listNotes, shares: sharesData.listTaskShares });
1658
+ return;
1659
+ }
1660
+ console.log(`ID: ${task.id}`);
1661
+ console.log(`Title: ${task.title}`);
1662
+ console.log(`Status: ${formatStatus(task.status)}`);
1663
+ console.log(`Priority: ${formatPriority(task.priority)}`);
1664
+ console.log(`Description: ${task.description || "\u2014"}`);
1665
+ console.log(`Category: ${task.categoryId || "\u2014"}`);
1666
+ console.log(`Tags: ${task.tags.length ? task.tags.join(", ") : "\u2014"}`);
1667
+ console.log(`Due Date: ${formatDate(task.dueDate)}${task.dueTime ? ` at ${task.dueTime}` : task.dueDate ? " (all day)" : ""}`);
1668
+ console.log(`Color: ${task.color || "\u2014"}`);
1669
+ console.log(`Archived: ${task.archived ? "Yes" : "No"}`);
1670
+ console.log(`Created: ${formatDate(task.createdAt)}`);
1671
+ console.log(`Updated: ${formatDate(task.updatedAt)}`);
1672
+ if (notesData.listNotes.length > 0) {
1673
+ console.log(`
1674
+ Notes (${notesData.listNotes.length}):`);
1675
+ for (const note of notesData.listNotes) {
1676
+ console.log(` [${formatDate(note.createdAt)}] ${note.authorName}: ${note.content}`);
1677
+ }
1678
+ }
1679
+ if (sharesData.listTaskShares.length > 0) {
1680
+ console.log(`
1681
+ Shares (${sharesData.listTaskShares.length}):`);
1682
+ printTable(
1683
+ ["Email", "Permission", "Status", "Shared At"],
1684
+ sharesData.listTaskShares.map((s) => [maskEmail(s.sharedWithEmail), s.permission, s.status, formatDate(s.sharedAt)])
1685
+ );
1686
+ }
1687
+ } catch (err) {
1688
+ printError(err.message);
1689
+ process.exitCode = 1;
1690
+ }
1691
+ });
1692
+
1693
+ // src/commands/categories.ts
1694
+ import { Command as Command4 } from "commander";
1695
+ import { randomUUID } from "crypto";
1696
+ import { confirm as confirm2 } from "@inquirer/prompts";
1697
+ var categoriesCommand = new Command4("categories").description("Manage categories").action(async () => {
1698
+ try {
1699
+ const data = await graphql(listCategories);
1700
+ if (isJsonMode()) {
1701
+ printJson(data.listCategories);
1702
+ return;
1703
+ }
1704
+ if (data.listCategories.length === 0) {
1705
+ console.log("No categories.");
1706
+ return;
1707
+ }
1708
+ printTable(
1709
+ ["ID", "Name", "Color"],
1710
+ data.listCategories.map((c) => [truncateId(c.id), c.name, c.color || "\u2014"])
1711
+ );
1712
+ } catch (err) {
1713
+ printError(err.message);
1714
+ process.exitCode = 1;
1715
+ }
1716
+ });
1717
+ categoriesCommand.command("create").description("Create a category").argument("<name>", "Category name").option("--color <color>", "Category color (hex)").action(async (name, opts) => {
1718
+ try {
1719
+ validateStringLength(name, "Name", 100);
1720
+ if (opts.color) validateHexColor(opts.color);
1721
+ const input2 = { id: randomUUID(), name, position: Date.now() };
1722
+ if (opts.color) input2.color = opts.color;
1723
+ const data = await graphql(createCategory, { input: input2 });
1724
+ if (isJsonMode()) {
1725
+ printJson(data.createCategory);
1726
+ return;
1727
+ }
1728
+ printSuccess(`Created category: ${data.createCategory.name}`);
1729
+ } catch (err) {
1730
+ printError(err.message);
1731
+ process.exitCode = 1;
1732
+ }
1733
+ });
1734
+ categoriesCommand.command("update").description("Update a category").argument("<id>", "Category ID").option("--name <name>", "New name").option("--color <color>", "New color").action(async (id, opts) => {
1735
+ try {
1736
+ validateUuid(id, "Category ID");
1737
+ if (opts.name) validateStringLength(opts.name, "Name", 100);
1738
+ if (opts.color) validateHexColor(opts.color);
1739
+ const input2 = { id };
1740
+ if (opts.name) input2.name = opts.name;
1741
+ if (opts.color) input2.color = opts.color;
1742
+ const data = await graphql(updateCategory, { input: input2 });
1743
+ if (isJsonMode()) {
1744
+ printJson(data.updateCategory);
1745
+ return;
1746
+ }
1747
+ printSuccess(`Updated category: ${data.updateCategory.name}`);
1748
+ } catch (err) {
1749
+ printError(err.message);
1750
+ process.exitCode = 1;
1751
+ }
1752
+ });
1753
+ categoriesCommand.command("delete").description("Delete a category").argument("<id>", "Category ID").option("--force", "Skip confirmation").action(async (id, opts) => {
1754
+ try {
1755
+ validateUuid(id, "Category ID");
1756
+ if (!opts.force) {
1757
+ const ok = await confirm2({ message: `Delete category ${truncateId(id)}?`, default: false });
1758
+ if (!ok) {
1759
+ console.log("Cancelled.");
1760
+ return;
1761
+ }
1762
+ }
1763
+ await graphql(deleteCategory, { id });
1764
+ if (isJsonMode()) {
1765
+ printJson({ deleted: id });
1766
+ return;
1767
+ }
1768
+ printSuccess("Deleted category");
1769
+ } catch (err) {
1770
+ printError(err.message);
1771
+ process.exitCode = 1;
1772
+ }
1773
+ });
1774
+
1775
+ // src/commands/tags.ts
1776
+ import { Command as Command5 } from "commander";
1777
+ import { randomUUID as randomUUID2 } from "crypto";
1778
+ import { confirm as confirm3 } from "@inquirer/prompts";
1779
+ var tagsCommand = new Command5("tags").description("Manage tags").action(async () => {
1780
+ try {
1781
+ const data = await graphql(listTags);
1782
+ if (isJsonMode()) {
1783
+ printJson(data.listTags);
1784
+ return;
1785
+ }
1786
+ if (data.listTags.length === 0) {
1787
+ console.log("No tags.");
1788
+ return;
1789
+ }
1790
+ printTable(["ID", "Name"], data.listTags.map((t) => [truncateId(t.id), t.name]));
1791
+ } catch (err) {
1792
+ printError(err.message);
1793
+ process.exitCode = 1;
1794
+ }
1795
+ });
1796
+ tagsCommand.command("create").description("Create a tag").argument("<name>", "Tag name").action(async (name) => {
1797
+ try {
1798
+ validateStringLength(name, "Tag name", 100);
1799
+ const data = await graphql(createTag, { input: { id: randomUUID2(), name } });
1800
+ if (isJsonMode()) {
1801
+ printJson(data.createTag);
1802
+ return;
1803
+ }
1804
+ printSuccess(`Created tag: ${data.createTag.name}`);
1805
+ } catch (err) {
1806
+ printError(err.message);
1807
+ process.exitCode = 1;
1808
+ }
1809
+ });
1810
+ tagsCommand.command("update").description("Update a tag").argument("<id>", "Tag ID").option("--name <name>", "New name").action(async (id, opts) => {
1811
+ try {
1812
+ validateUuid(id, "Tag ID");
1813
+ if (opts.name) validateStringLength(opts.name, "Tag name", 100);
1814
+ const input2 = { id };
1815
+ if (opts.name) input2.name = opts.name;
1816
+ const data = await graphql(updateTag, { input: input2 });
1817
+ if (isJsonMode()) {
1818
+ printJson(data.updateTag);
1819
+ return;
1820
+ }
1821
+ printSuccess(`Updated tag: ${data.updateTag.name}`);
1822
+ } catch (err) {
1823
+ printError(err.message);
1824
+ process.exitCode = 1;
1825
+ }
1826
+ });
1827
+ tagsCommand.command("delete").description("Delete a tag").argument("<id>", "Tag ID").option("--force", "Skip confirmation").action(async (id, opts) => {
1828
+ try {
1829
+ validateUuid(id, "Tag ID");
1830
+ if (!opts.force) {
1831
+ const ok = await confirm3({ message: `Delete tag ${truncateId(id)}?`, default: false });
1832
+ if (!ok) {
1833
+ console.log("Cancelled.");
1834
+ return;
1835
+ }
1836
+ }
1837
+ await graphql(deleteTagMutation, { id });
1838
+ if (isJsonMode()) {
1839
+ printJson({ deleted: id });
1840
+ return;
1841
+ }
1842
+ printSuccess("Deleted tag");
1843
+ } catch (err) {
1844
+ printError(err.message);
1845
+ process.exitCode = 1;
1846
+ }
1847
+ });
1848
+
1849
+ // src/commands/notes.ts
1850
+ import { Command as Command6 } from "commander";
1851
+ import { randomUUID as randomUUID3 } from "crypto";
1852
+ import { confirm as confirm4 } from "@inquirer/prompts";
1853
+ var notesCommand = new Command6("notes").description("Manage task notes").argument("<task-id>", "Task ID").action(async (taskId) => {
1854
+ try {
1855
+ validateUuid(taskId, "Task ID");
1856
+ const data = await graphql(listNotes, { taskId });
1857
+ if (isJsonMode()) {
1858
+ printJson(data.listNotes);
1859
+ return;
1860
+ }
1861
+ if (data.listNotes.length === 0) {
1862
+ console.log("No notes.");
1863
+ return;
1864
+ }
1865
+ printTable(
1866
+ ["ID", "Author", "Content", "Created"],
1867
+ data.listNotes.map((n) => [truncateId(n.id), n.authorName, n.content.length > 50 ? n.content.substring(0, 47) + "..." : n.content, formatDate(n.createdAt)])
1868
+ );
1869
+ } catch (err) {
1870
+ printError(err.message);
1871
+ process.exitCode = 1;
1872
+ }
1873
+ });
1874
+ notesCommand.command("add").description("Add a note to a task").argument("<task-id>", "Task ID").argument("<content>", "Note content").action(async (taskId, content) => {
1875
+ try {
1876
+ validateUuid(taskId, "Task ID");
1877
+ validateStringLength(content, "Note content", 5e3);
1878
+ const data = await graphql(createNote, {
1879
+ input: { id: randomUUID3(), taskId, content }
1880
+ });
1881
+ if (isJsonMode()) {
1882
+ printJson(data.createNote);
1883
+ return;
1884
+ }
1885
+ printSuccess("Added note to task");
1886
+ } catch (err) {
1887
+ printError(err.message);
1888
+ process.exitCode = 1;
1889
+ }
1890
+ });
1891
+ notesCommand.command("delete").description("Delete a note").argument("<task-id>", "Task ID").argument("<note-id>", "Note ID").requiredOption("--created-at <date>", "Note creation date (ISO 8601)").option("--force", "Skip confirmation").action(async (taskId, noteId, opts) => {
1892
+ try {
1893
+ validateUuid(taskId, "Task ID");
1894
+ validateUuid(noteId, "Note ID");
1895
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(opts.createdAt)) {
1896
+ throw new Error("Invalid --created-at: must be an ISO 8601 datetime (e.g. 2024-01-15T10:30:00Z).");
1897
+ }
1898
+ if (!opts.force) {
1899
+ const ok = await confirm4({ message: "Delete this note?", default: false });
1900
+ if (!ok) {
1901
+ console.log("Cancelled.");
1902
+ return;
1903
+ }
1904
+ }
1905
+ await graphql(deleteNote, { taskId, noteId, createdAt: opts.createdAt });
1906
+ if (isJsonMode()) {
1907
+ printJson({ deleted: noteId });
1908
+ return;
1909
+ }
1910
+ printSuccess("Deleted note");
1911
+ } catch (err) {
1912
+ printError(err.message);
1913
+ process.exitCode = 1;
1914
+ }
1915
+ });
1916
+
1917
+ // src/commands/contacts.ts
1918
+ import { Command as Command7 } from "commander";
1919
+ import { randomUUID as randomUUID4 } from "crypto";
1920
+ import { confirm as confirm5 } from "@inquirer/prompts";
1921
+ var contactsCommand = new Command7("contacts").description("Manage contacts").action(async () => {
1922
+ try {
1923
+ const data = await graphql(listContacts);
1924
+ if (isJsonMode()) {
1925
+ printJson(data.listContacts);
1926
+ return;
1927
+ }
1928
+ if (data.listContacts.length === 0) {
1929
+ console.log("No contacts.");
1930
+ return;
1931
+ }
1932
+ printTable(
1933
+ ["ID", "Name", "Email"],
1934
+ data.listContacts.map((c) => [truncateId(c.id), c.name, maskEmail(c.email)])
1935
+ );
1936
+ } catch (err) {
1937
+ printError(err.message);
1938
+ process.exitCode = 1;
1939
+ }
1940
+ });
1941
+ contactsCommand.command("create").description("Create a contact").argument("<name>", "Contact name").requiredOption("--email <email>", "Contact email").action(async (name, opts) => {
1942
+ try {
1943
+ validateEmail(opts.email);
1944
+ validateStringLength(name, "Name", 200);
1945
+ const data = await graphql(createContact, {
1946
+ input: { id: randomUUID4(), name, email: opts.email }
1947
+ });
1948
+ if (isJsonMode()) {
1949
+ printJson(data.createContact);
1950
+ return;
1951
+ }
1952
+ printSuccess(`Created contact: ${data.createContact.name}`);
1953
+ } catch (err) {
1954
+ printError(err.message);
1955
+ process.exitCode = 1;
1956
+ }
1957
+ });
1958
+ contactsCommand.command("update").description("Update a contact").argument("<id>", "Contact ID").option("--name <name>", "New name").option("--email <email>", "New email").action(async (id, opts) => {
1959
+ try {
1960
+ validateUuid(id, "Contact ID");
1961
+ const input2 = { id };
1962
+ if (opts.name) {
1963
+ validateStringLength(opts.name, "Name", 200);
1964
+ input2.name = opts.name;
1965
+ }
1966
+ if (opts.email) {
1967
+ validateEmail(opts.email);
1968
+ input2.email = opts.email;
1969
+ }
1970
+ const data = await graphql(updateContact, { input: input2 });
1971
+ if (isJsonMode()) {
1972
+ printJson(data.updateContact);
1973
+ return;
1974
+ }
1975
+ printSuccess(`Updated contact: ${data.updateContact.name}`);
1976
+ } catch (err) {
1977
+ printError(err.message);
1978
+ process.exitCode = 1;
1979
+ }
1980
+ });
1981
+ contactsCommand.command("delete").description("Delete a contact").argument("<id>", "Contact ID").option("--force", "Skip confirmation").action(async (id, opts) => {
1982
+ try {
1983
+ validateUuid(id, "Contact ID");
1984
+ if (!opts.force) {
1985
+ const ok = await confirm5({ message: `Delete contact ${truncateId(id)}?`, default: false });
1986
+ if (!ok) {
1987
+ console.log("Cancelled.");
1988
+ return;
1989
+ }
1990
+ }
1991
+ await graphql(deleteContactMutation, { id });
1992
+ if (isJsonMode()) {
1993
+ printJson({ deleted: id });
1994
+ return;
1995
+ }
1996
+ printSuccess("Deleted contact");
1997
+ } catch (err) {
1998
+ printError(err.message);
1999
+ process.exitCode = 1;
2000
+ }
2001
+ });
2002
+
2003
+ // src/commands/prefs.ts
2004
+ import { Command as Command8 } from "commander";
2005
+ import { randomUUID as randomUUID5 } from "crypto";
2006
+ import chalk2 from "chalk";
2007
+ var prefsCommand = new Command8("prefs").description("Manage preferences").action(async () => {
2008
+ try {
2009
+ const data = await graphql(getPreferences);
2010
+ const p = data.getPreferences;
2011
+ if (isJsonMode()) {
2012
+ printJson(p);
2013
+ return;
2014
+ }
2015
+ printTable(
2016
+ ["Setting", "Value"],
2017
+ [
2018
+ ["Theme", p.theme],
2019
+ ["Default View", p.defaultView],
2020
+ ["Archive Delay (hours)", String(p.archiveDelay)],
2021
+ ["Default Priority", p.defaultPriority || "\u2014"],
2022
+ ["Default Category ID", p.defaultCategoryId ? truncateId(p.defaultCategoryId) : "\u2014"],
2023
+ ["Default Color", p.defaultColor || "\u2014"]
2024
+ ]
2025
+ );
2026
+ } catch (err) {
2027
+ printError(err.message);
2028
+ process.exitCode = 1;
2029
+ }
2030
+ });
2031
+ prefsCommand.command("set").description("Update a preference").option("--theme <theme>", "Theme (light, dark, system)").option("--default-view <view>", "Default view (list, kanban, priority, category)").option("--archive-delay <hours>", "Archive delay in hours").option("--default-priority <priority>", 'Default task priority (critical, high, medium, low) or "none" to clear').option("--default-category <id>", 'Default category ID or "none" to clear').option("--default-color <color>", 'Default task color (hex, e.g. #FF5733) or "none" to clear').action(async (opts) => {
2032
+ try {
2033
+ if (opts.theme) validateTheme(opts.theme);
2034
+ if (opts.defaultView) validateView(opts.defaultView);
2035
+ if (opts.defaultPriority && opts.defaultPriority !== "none") validatePriority(opts.defaultPriority);
2036
+ if (opts.defaultColor && opts.defaultColor !== "none") validateHexColor(opts.defaultColor);
2037
+ const input2 = {};
2038
+ if (opts.theme) input2.theme = opts.theme;
2039
+ if (opts.defaultView) input2.defaultView = opts.defaultView;
2040
+ if (opts.archiveDelay) {
2041
+ const delay = parseInt(opts.archiveDelay, 10);
2042
+ if (isNaN(delay) || delay < 0 || delay > 8760) {
2043
+ printError("Archive delay must be between 0 and 8760 hours (1 year).");
2044
+ process.exitCode = 1;
2045
+ return;
2046
+ }
2047
+ input2.archiveDelay = delay;
2048
+ }
2049
+ if (opts.defaultPriority) input2.defaultPriority = opts.defaultPriority === "none" ? "" : opts.defaultPriority;
2050
+ if (opts.defaultCategory) input2.defaultCategoryId = opts.defaultCategory === "none" ? "" : opts.defaultCategory;
2051
+ if (opts.defaultColor) input2.defaultColor = opts.defaultColor === "none" ? "" : opts.defaultColor;
2052
+ if (Object.keys(input2).length === 0) {
2053
+ printError("Provide at least one option: --theme, --default-view, --archive-delay, --default-priority, --default-category, --default-color");
2054
+ process.exitCode = 1;
2055
+ return;
2056
+ }
2057
+ const data = await graphql(updatePreferences, { input: input2 });
2058
+ if (isJsonMode()) {
2059
+ printJson(data.updatePreferences);
2060
+ return;
2061
+ }
2062
+ for (const [key, value] of Object.entries(input2)) {
2063
+ printSuccess(`Updated preference: ${key} = ${value}`);
2064
+ }
2065
+ } catch (err) {
2066
+ printError(err.message);
2067
+ process.exitCode = 1;
2068
+ }
2069
+ });
2070
+ var VALID_CONDITION_TYPES = ["tag", "priority", "category"];
2071
+ function isValidColorRule(item) {
2072
+ if (typeof item !== "object" || item === null) return false;
2073
+ const obj = item;
2074
+ return typeof obj.id === "string" && typeof obj.conditionType === "string" && VALID_CONDITION_TYPES.includes(obj.conditionType) && typeof obj.conditionValue === "string" && typeof obj.color === "string" && /^#[0-9A-Fa-f]{6}$/.test(obj.color);
2075
+ }
2076
+ async function loadColorRules() {
2077
+ const data = await graphql(getPreferences);
2078
+ const raw = data.getPreferences.colorRules;
2079
+ if (!raw) return [];
2080
+ const parsed = JSON.parse(raw);
2081
+ if (!Array.isArray(parsed)) return [];
2082
+ return parsed.filter(isValidColorRule);
2083
+ }
2084
+ async function saveColorRules(rules) {
2085
+ await graphql(updatePreferences, {
2086
+ input: { colorRules: JSON.stringify(rules) }
2087
+ });
2088
+ }
2089
+ var colorRulesCommand = prefsCommand.command("color-rules").description("Manage automatic color rules").action(async () => {
2090
+ try {
2091
+ const rules = await loadColorRules();
2092
+ if (isJsonMode()) {
2093
+ printJson(rules);
2094
+ return;
2095
+ }
2096
+ if (rules.length === 0) {
2097
+ console.log("No color rules configured.");
2098
+ console.log(chalk2.dim("Add one: tasklumina prefs color-rules add --type priority --value critical --color #ef4444"));
2099
+ return;
2100
+ }
2101
+ printTable(
2102
+ ["ID", "Type", "Value", "Color"],
2103
+ rules.map((r) => [
2104
+ truncateId(r.id),
2105
+ r.conditionType,
2106
+ r.conditionValue,
2107
+ `${chalk2.hex(r.color)("\u2588\u2588")} ${r.color}`
2108
+ ])
2109
+ );
2110
+ } catch (err) {
2111
+ printError(err.message);
2112
+ process.exitCode = 1;
2113
+ }
2114
+ });
2115
+ colorRulesCommand.command("add").description("Add a color rule").requiredOption("--type <type>", "Condition type (tag, priority, category)").requiredOption("--value <value>", 'Condition value (e.g. "critical", "Work")').requiredOption("--color <hex>", "Color hex code (e.g. #ef4444)").action(async (opts) => {
2116
+ try {
2117
+ if (!VALID_CONDITION_TYPES.includes(opts.type)) {
2118
+ printError(`Invalid type: "${opts.type}". Must be one of: ${VALID_CONDITION_TYPES.join(", ")}`);
2119
+ process.exitCode = 1;
2120
+ return;
2121
+ }
2122
+ validateHexColor(opts.color);
2123
+ validateStringLength(opts.value, "Condition value", 200);
2124
+ const rules = await loadColorRules();
2125
+ const existing = rules.find(
2126
+ (r) => r.conditionType === opts.type && r.conditionValue === opts.value
2127
+ );
2128
+ if (existing) {
2129
+ printError(`A rule for ${opts.type} "${opts.value}" already exists (${truncateId(existing.id)}). Remove it first.`);
2130
+ process.exitCode = 1;
2131
+ return;
2132
+ }
2133
+ const rule = {
2134
+ id: randomUUID5(),
2135
+ conditionType: opts.type,
2136
+ conditionValue: opts.value,
2137
+ color: opts.color
2138
+ };
2139
+ rules.push(rule);
2140
+ await saveColorRules(rules);
2141
+ if (isJsonMode()) {
2142
+ printJson(rule);
2143
+ return;
2144
+ }
2145
+ printSuccess(`Added color rule: ${opts.type} "${opts.value}" \u2192 ${chalk2.hex(opts.color)("\u2588\u2588")} ${opts.color}`);
2146
+ } catch (err) {
2147
+ printError(err.message);
2148
+ process.exitCode = 1;
2149
+ }
2150
+ });
2151
+ colorRulesCommand.command("remove").description("Remove a color rule").argument("<id>", "Rule ID or prefix").action(async (id) => {
2152
+ try {
2153
+ const rules = await loadColorRules();
2154
+ const match = rules.filter((r) => r.id === id || r.id.startsWith(id));
2155
+ if (match.length === 0) {
2156
+ printError(`No color rule found matching "${id}".`);
2157
+ process.exitCode = 1;
2158
+ return;
2159
+ }
2160
+ if (match.length > 1) {
2161
+ printError(`Multiple rules match "${id}". Be more specific.`);
2162
+ process.exitCode = 1;
2163
+ return;
2164
+ }
2165
+ const removed = match[0];
2166
+ const updated = rules.filter((r) => r.id !== removed.id);
2167
+ await saveColorRules(updated);
2168
+ if (isJsonMode()) {
2169
+ printJson({ removed: removed.id });
2170
+ return;
2171
+ }
2172
+ printSuccess(`Removed color rule: ${removed.conditionType} "${removed.conditionValue}"`);
2173
+ } catch (err) {
2174
+ printError(err.message);
2175
+ process.exitCode = 1;
2176
+ }
2177
+ });
2178
+
2179
+ // src/commands/lists.ts
2180
+ import { Command as Command9 } from "commander";
2181
+ import { randomUUID as randomUUID6 } from "crypto";
2182
+ import { confirm as confirm6 } from "@inquirer/prompts";
2183
+ var listsCommand = new Command9("lists").description("Manage lists").action(async () => {
2184
+ try {
2185
+ const data = await graphql(listLists);
2186
+ if (isJsonMode()) {
2187
+ printJson(data.listLists);
2188
+ return;
2189
+ }
2190
+ if (data.listLists.length === 0) {
2191
+ console.log("No lists.");
2192
+ return;
2193
+ }
2194
+ printTable(
2195
+ ["ID", "Name", "Items", "Created"],
2196
+ data.listLists.map((l) => [truncateId(l.id), l.name, String(l.itemCount), formatDate(l.createdAt)])
2197
+ );
2198
+ } catch (err) {
2199
+ printError(err.message);
2200
+ process.exitCode = 1;
2201
+ }
2202
+ });
2203
+ listsCommand.command("show").description("Show a list with items").argument("<id>", "List ID").action(async (id) => {
2204
+ try {
2205
+ validateUuid(id, "List ID");
2206
+ const [listsData, itemsData] = await Promise.all([
2207
+ graphql(listLists),
2208
+ graphql(listListItems, { listId: id })
2209
+ ]);
2210
+ const list = listsData.listLists.find((l) => l.id === id || l.id.startsWith(id));
2211
+ if (!list) {
2212
+ printError(`List not found: ${id}`);
2213
+ process.exitCode = 1;
2214
+ return;
2215
+ }
2216
+ if (isJsonMode()) {
2217
+ printJson({ ...list, items: itemsData.listListItems });
2218
+ return;
2219
+ }
2220
+ console.log(`ID: ${list.id}`);
2221
+ console.log(`Name: ${list.name}`);
2222
+ console.log(`Description: ${list.description || "\u2014"}`);
2223
+ console.log(`Icon: ${list.icon || "\u2014"}`);
2224
+ console.log(`Color: ${list.color || "\u2014"}`);
2225
+ console.log(`Items: ${list.itemCount}`);
2226
+ console.log(`Created: ${formatDate(list.createdAt)}`);
2227
+ if (itemsData.listListItems.length > 0) {
2228
+ console.log("\nItems:");
2229
+ printTable(
2230
+ ["ID", "Name", "Qty", "Category"],
2231
+ itemsData.listListItems.map((i) => [truncateId(i.id), i.name, i.quantity != null ? String(i.quantity) : "\u2014", i.category || "\u2014"])
2232
+ );
2233
+ }
2234
+ } catch (err) {
2235
+ printError(err.message);
2236
+ process.exitCode = 1;
2237
+ }
2238
+ });
2239
+ listsCommand.command("create").description("Create a list").argument("<name>", "List name").option("--desc <description>", "Description").option("--icon <icon>", "Icon emoji").option("--color <color>", "Color hex").action(async (name, opts) => {
2240
+ try {
2241
+ validateStringLength(name, "Name", 200);
2242
+ if (opts.desc) validateStringLength(opts.desc, "Description", 2e3);
2243
+ if (opts.color) validateHexColor(opts.color);
2244
+ const input2 = { id: randomUUID6(), name };
2245
+ if (opts.desc) input2.description = opts.desc;
2246
+ if (opts.icon) input2.icon = opts.icon;
2247
+ if (opts.color) input2.color = opts.color;
2248
+ const data = await graphql(createList, { input: input2 });
2249
+ if (isJsonMode()) {
2250
+ printJson(data.createList);
2251
+ return;
2252
+ }
2253
+ printSuccess(`Created list: ${data.createList.name}`);
2254
+ } catch (err) {
2255
+ printError(err.message);
2256
+ process.exitCode = 1;
2257
+ }
2258
+ });
2259
+ listsCommand.command("update").description("Update a list").argument("<id>", "List ID").option("--name <name>", "New name").option("--desc <description>", "New description").option("--icon <icon>", "New icon").option("--color <color>", "New color").action(async (id, opts) => {
2260
+ try {
2261
+ validateUuid(id, "List ID");
2262
+ if (opts.name) validateStringLength(opts.name, "Name", 200);
2263
+ if (opts.desc) validateStringLength(opts.desc, "Description", 2e3);
2264
+ if (opts.color) validateHexColor(opts.color);
2265
+ const input2 = { id };
2266
+ if (opts.name) input2.name = opts.name;
2267
+ if (opts.desc) input2.description = opts.desc;
2268
+ if (opts.icon) input2.icon = opts.icon;
2269
+ if (opts.color) input2.color = opts.color;
2270
+ const data = await graphql(updateList, { input: input2 });
2271
+ if (isJsonMode()) {
2272
+ printJson(data.updateList);
2273
+ return;
2274
+ }
2275
+ printSuccess(`Updated list: ${data.updateList.name}`);
2276
+ } catch (err) {
2277
+ printError(err.message);
2278
+ process.exitCode = 1;
2279
+ }
2280
+ });
2281
+ listsCommand.command("delete").description("Delete a list").argument("<id>", "List ID").option("--force", "Skip confirmation").action(async (id, opts) => {
2282
+ try {
2283
+ validateUuid(id, "List ID");
2284
+ if (!opts.force) {
2285
+ const ok = await confirm6({ message: `Delete list ${truncateId(id)}?`, default: false });
2286
+ if (!ok) {
2287
+ console.log("Cancelled.");
2288
+ return;
2289
+ }
2290
+ }
2291
+ await graphql(deleteListMutation, { id });
2292
+ if (isJsonMode()) {
2293
+ printJson({ deleted: id });
2294
+ return;
2295
+ }
2296
+ printSuccess("Deleted list");
2297
+ } catch (err) {
2298
+ printError(err.message);
2299
+ process.exitCode = 1;
2300
+ }
2301
+ });
2302
+ listsCommand.command("add-item").description("Add an item to a list").argument("<list-id>", "List ID").argument("<name>", "Item name").option("--qty <quantity>", "Quantity").option("--category <category>", "Category").option("--link <url>", "Link URL").action(async (listId, name, opts) => {
2303
+ try {
2304
+ validateUuid(listId, "List ID");
2305
+ validateStringLength(name, "Name", 500);
2306
+ if (opts.qty) validateQuantity(opts.qty);
2307
+ if (opts.link) validateUrl(opts.link);
2308
+ const input2 = { id: randomUUID6(), listId, name, tags: [], customFields: "{}", position: Date.now() };
2309
+ if (opts.qty) input2.quantity = validateQuantity(opts.qty);
2310
+ if (opts.category) input2.category = opts.category;
2311
+ if (opts.link) input2.link = opts.link;
2312
+ const data = await graphql(createListItem, { input: input2 });
2313
+ if (isJsonMode()) {
2314
+ printJson(data.createListItem);
2315
+ return;
2316
+ }
2317
+ printSuccess(`Added item: ${data.createListItem.name}`);
2318
+ } catch (err) {
2319
+ printError(err.message);
2320
+ process.exitCode = 1;
2321
+ }
2322
+ });
2323
+ listsCommand.command("bulk-add").description("Add multiple items to a list").argument("<list-id>", "List ID").argument("<items...>", "Item names").action(async (listId, items) => {
2324
+ try {
2325
+ validateUuid(listId, "List ID");
2326
+ for (const name of items) {
2327
+ validateStringLength(name, "Item name", 500);
2328
+ }
2329
+ const itemInputs = items.map((name, i) => ({
2330
+ id: randomUUID6(),
2331
+ listId,
2332
+ name,
2333
+ tags: [],
2334
+ customFields: "{}",
2335
+ position: Date.now() + i
2336
+ }));
2337
+ const data = await graphql(bulkCreateListItems, {
2338
+ input: { listId, items: itemInputs }
2339
+ });
2340
+ if (isJsonMode()) {
2341
+ printJson(data.bulkCreateListItems);
2342
+ return;
2343
+ }
2344
+ printSuccess(`Added ${data.bulkCreateListItems.length} items to list`);
2345
+ } catch (err) {
2346
+ printError(err.message);
2347
+ process.exitCode = 1;
2348
+ }
2349
+ });
2350
+ listsCommand.command("update-item").description("Update a list item").argument("<item-id>", "Item ID").requiredOption("--list <list-id>", "List ID").option("--name <name>", "New name").option("--qty <quantity>", "New quantity").option("--category <category>", "New category").option("--link <url>", "New link").action(async (itemId, opts) => {
2351
+ try {
2352
+ validateUuid(itemId, "Item ID");
2353
+ validateUuid(opts.list, "List ID");
2354
+ if (opts.name) validateStringLength(opts.name, "Name", 500);
2355
+ if (opts.qty) validateQuantity(opts.qty);
2356
+ if (opts.link) validateUrl(opts.link);
2357
+ const input2 = { id: itemId, listId: opts.list };
2358
+ if (opts.name) input2.name = opts.name;
2359
+ if (opts.qty) input2.quantity = validateQuantity(opts.qty);
2360
+ if (opts.category) input2.category = opts.category;
2361
+ if (opts.link) input2.link = opts.link;
2362
+ const data = await graphql(updateListItem, { input: input2 });
2363
+ if (isJsonMode()) {
2364
+ printJson(data.updateListItem);
2365
+ return;
2366
+ }
2367
+ printSuccess("Updated item");
2368
+ } catch (err) {
2369
+ printError(err.message);
2370
+ process.exitCode = 1;
2371
+ }
2372
+ });
2373
+ listsCommand.command("remove-item").description("Remove a list item").argument("<item-id>", "Item ID").requiredOption("--list <list-id>", "List ID").option("--force", "Skip confirmation").action(async (itemId, opts) => {
2374
+ try {
2375
+ validateUuid(itemId, "Item ID");
2376
+ validateUuid(opts.list, "List ID");
2377
+ if (!opts.force) {
2378
+ const ok = await confirm6({ message: "Remove this item?", default: false });
2379
+ if (!ok) {
2380
+ console.log("Cancelled.");
2381
+ return;
2382
+ }
2383
+ }
2384
+ await graphql(deleteListItemMutation, { id: itemId, listId: opts.list });
2385
+ if (isJsonMode()) {
2386
+ printJson({ deleted: itemId });
2387
+ return;
2388
+ }
2389
+ printSuccess("Removed item");
2390
+ } catch (err) {
2391
+ printError(err.message);
2392
+ process.exitCode = 1;
2393
+ }
2394
+ });
2395
+
2396
+ // src/commands/shares.ts
2397
+ import { Command as Command10 } from "commander";
2398
+ async function resolveEmail(opts) {
2399
+ if (opts.email) {
2400
+ validateEmail(opts.email);
2401
+ return opts.email;
2402
+ }
2403
+ if (opts.contact) {
2404
+ const data = await graphql(listContacts);
2405
+ const name = opts.contact.toLowerCase();
2406
+ const match = data.listContacts.filter((c) => c.name.toLowerCase().includes(name));
2407
+ if (match.length === 0) {
2408
+ throw new Error(`No contact found matching "${opts.contact}". Run \`tasklumina contacts\` to see your contacts.`);
2409
+ }
2410
+ if (match.length > 1) {
2411
+ const names = match.map((c) => ` ${c.name} (${c.email})`).join("\n");
2412
+ throw new Error(`Multiple contacts match "${opts.contact}":
2413
+ ${names}
2414
+ Be more specific or use --email instead.`);
2415
+ }
2416
+ return match[0].email;
2417
+ }
2418
+ throw new Error("Provide --email or --contact");
2419
+ }
2420
+ var sharesCommand = new Command10("shares").description("Manage task and list sharing");
2421
+ sharesCommand.command("send").description("Share a task with someone").argument("<task-id>", "Task ID").option("--email <email>", "Recipient email").option("--contact <name>", "Recipient contact name").option("--permission <perm>", "Permission (view or edit)", "view").action(async (taskId, opts) => {
2422
+ try {
2423
+ validateUuid(taskId, "Task ID");
2424
+ const email = await resolveEmail(opts);
2425
+ validatePermission(opts.permission);
2426
+ const data = await graphql(shareTask, {
2427
+ input: { taskId, sharedWithEmail: email, permission: opts.permission }
2428
+ });
2429
+ if (isJsonMode()) {
2430
+ printJson(data.shareTask);
2431
+ return;
2432
+ }
2433
+ printSuccess(`Shared task with ${maskEmail(email)} (${opts.permission})`);
2434
+ } catch (err) {
2435
+ printError(err.message);
2436
+ process.exitCode = 1;
2437
+ }
2438
+ });
2439
+ sharesCommand.command("inbox").description("List tasks shared with me").action(async () => {
2440
+ try {
2441
+ const data = await graphql(listSharedWithMe);
2442
+ if (isJsonMode()) {
2443
+ printJson(data.listSharedWithMe);
2444
+ return;
2445
+ }
2446
+ if (data.listSharedWithMe.length === 0) {
2447
+ console.log("No shared tasks.");
2448
+ return;
2449
+ }
2450
+ printTable(
2451
+ ["Task", "Owner", "Permission", "Status"],
2452
+ data.listSharedWithMe.map((s) => [
2453
+ s.task?.title || truncateId(s.taskId),
2454
+ maskEmail(s.ownerEmail),
2455
+ s.permission,
2456
+ s.status
2457
+ ])
2458
+ );
2459
+ } catch (err) {
2460
+ printError(err.message);
2461
+ process.exitCode = 1;
2462
+ }
2463
+ });
2464
+ sharesCommand.command("accept").description("Accept a shared task").argument("<task-id>", "Task ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (taskId, opts) => {
2465
+ try {
2466
+ validateUuid(taskId, "Task ID");
2467
+ validateUuid(opts.owner, "Owner ID");
2468
+ const data = await graphql(acceptShare, { taskId, ownerId: opts.owner });
2469
+ if (isJsonMode()) {
2470
+ printJson(data.acceptShare);
2471
+ return;
2472
+ }
2473
+ printSuccess("Accepted share");
2474
+ } catch (err) {
2475
+ printError(err.message);
2476
+ process.exitCode = 1;
2477
+ }
2478
+ });
2479
+ sharesCommand.command("decline").description("Decline a shared task").argument("<task-id>", "Task ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (taskId, opts) => {
2480
+ try {
2481
+ validateUuid(taskId, "Task ID");
2482
+ validateUuid(opts.owner, "Owner ID");
2483
+ const data = await graphql(declineShare, { taskId, ownerId: opts.owner });
2484
+ if (isJsonMode()) {
2485
+ printJson(data.declineShare);
2486
+ return;
2487
+ }
2488
+ printSuccess("Declined share");
2489
+ } catch (err) {
2490
+ printError(err.message);
2491
+ process.exitCode = 1;
2492
+ }
2493
+ });
2494
+ sharesCommand.command("revoke").description("Revoke a task share").argument("<task-id>", "Task ID").requiredOption("--user <user-id>", "Shared-with user ID").action(async (taskId, opts) => {
2495
+ try {
2496
+ validateUuid(taskId, "Task ID");
2497
+ validateUuid(opts.user, "User ID");
2498
+ await graphql(revokeShare, { taskId, sharedWithId: opts.user });
2499
+ if (isJsonMode()) {
2500
+ printJson({ revoked: true, taskId });
2501
+ return;
2502
+ }
2503
+ printSuccess("Revoked share");
2504
+ } catch (err) {
2505
+ printError(err.message);
2506
+ process.exitCode = 1;
2507
+ }
2508
+ });
2509
+ sharesCommand.command("ls").description("List shares for a task").argument("<task-id>", "Task ID").action(async (taskId) => {
2510
+ try {
2511
+ validateUuid(taskId, "Task ID");
2512
+ const data = await graphql(listTaskShares, { taskId });
2513
+ if (isJsonMode()) {
2514
+ printJson(data.listTaskShares);
2515
+ return;
2516
+ }
2517
+ if (data.listTaskShares.length === 0) {
2518
+ console.log("Not shared.");
2519
+ return;
2520
+ }
2521
+ printTable(
2522
+ ["Email", "Permission", "Status", "Shared At"],
2523
+ data.listTaskShares.map((s) => [maskEmail(s.sharedWithEmail), s.permission, s.status, formatDate(s.sharedAt)])
2524
+ );
2525
+ } catch (err) {
2526
+ printError(err.message);
2527
+ process.exitCode = 1;
2528
+ }
2529
+ });
2530
+ sharesCommand.command("outbox").description("List all shares you have sent (tasks and lists)").action(async () => {
2531
+ try {
2532
+ const [taskData, listData] = await Promise.all([
2533
+ graphql(listMyOutgoingTaskShares),
2534
+ graphql(listMyOutgoingListShares)
2535
+ ]);
2536
+ const tasks = taskData.listMyOutgoingTaskShares;
2537
+ const lists = listData.listMyOutgoingListShares;
2538
+ if (isJsonMode()) {
2539
+ printJson({ tasks, lists });
2540
+ return;
2541
+ }
2542
+ if (tasks.length === 0 && lists.length === 0) {
2543
+ console.log("No outgoing shares.");
2544
+ return;
2545
+ }
2546
+ if (tasks.length > 0) {
2547
+ console.log("\nTask shares:");
2548
+ printTable(
2549
+ ["Task", "To", "Permission", "Status", "Shared At"],
2550
+ tasks.map((s) => [
2551
+ s.task?.title || truncateId(s.taskId),
2552
+ maskEmail(s.sharedWithEmail),
2553
+ s.permission,
2554
+ s.status,
2555
+ formatDate(s.sharedAt)
2556
+ ])
2557
+ );
2558
+ }
2559
+ if (lists.length > 0) {
2560
+ console.log("\nList shares:");
2561
+ printTable(
2562
+ ["List", "To", "Permission", "Status", "Shared At"],
2563
+ lists.map((s) => [
2564
+ s.list?.name || truncateId(s.listId),
2565
+ maskEmail(s.sharedWithEmail),
2566
+ s.permission,
2567
+ s.status,
2568
+ formatDate(s.sharedAt)
2569
+ ])
2570
+ );
2571
+ }
2572
+ } catch (err) {
2573
+ printError(err.message);
2574
+ process.exitCode = 1;
2575
+ }
2576
+ });
2577
+ sharesCommand.command("update-permission").description("Change permission on a task share").argument("<task-id>", "Task ID").requiredOption("--user <user-id>", "Shared-with user ID").requiredOption("--permission <perm>", "New permission (view or edit)").action(async (taskId, opts) => {
2578
+ try {
2579
+ validateUuid(taskId, "Task ID");
2580
+ validateUuid(opts.user, "User ID");
2581
+ validatePermission(opts.permission);
2582
+ const data = await graphql(updateSharePermission, {
2583
+ input: { taskId, sharedWithId: opts.user, permission: opts.permission }
2584
+ });
2585
+ if (isJsonMode()) {
2586
+ printJson(data.updateSharePermission);
2587
+ return;
2588
+ }
2589
+ printSuccess(`Updated permission to ${opts.permission}`);
2590
+ } catch (err) {
2591
+ printError(err.message);
2592
+ process.exitCode = 1;
2593
+ }
2594
+ });
2595
+ sharesCommand.command("list-send").description("Share a list with someone").argument("<list-id>", "List ID").option("--email <email>", "Recipient email").option("--contact <name>", "Recipient contact name").option("--permission <perm>", "Permission (view or edit)", "view").action(async (listId, opts) => {
2596
+ try {
2597
+ validateUuid(listId, "List ID");
2598
+ const email = await resolveEmail(opts);
2599
+ validatePermission(opts.permission);
2600
+ const data = await graphql(shareList, {
2601
+ input: { listId, sharedWithEmail: email, permission: opts.permission }
2602
+ });
2603
+ if (isJsonMode()) {
2604
+ printJson(data.shareList);
2605
+ return;
2606
+ }
2607
+ printSuccess(`Shared list with ${maskEmail(email)} (${opts.permission})`);
2608
+ } catch (err) {
2609
+ printError(err.message);
2610
+ process.exitCode = 1;
2611
+ }
2612
+ });
2613
+ sharesCommand.command("list-inbox").description("List lists shared with me").action(async () => {
2614
+ try {
2615
+ const data = await graphql(listListsSharedWithMe);
2616
+ if (isJsonMode()) {
2617
+ printJson(data.listListsSharedWithMe);
2618
+ return;
2619
+ }
2620
+ if (data.listListsSharedWithMe.length === 0) {
2621
+ console.log("No shared lists.");
2622
+ return;
2623
+ }
2624
+ printTable(
2625
+ ["List", "Owner", "Permission", "Status"],
2626
+ data.listListsSharedWithMe.map((s) => [
2627
+ s.list?.name || truncateId(s.listId),
2628
+ s.ownerEmail ? maskEmail(s.ownerEmail) : "\u2014",
2629
+ s.permission,
2630
+ s.status
2631
+ ])
2632
+ );
2633
+ } catch (err) {
2634
+ printError(err.message);
2635
+ process.exitCode = 1;
2636
+ }
2637
+ });
2638
+ sharesCommand.command("list-accept").description("Accept a shared list").argument("<list-id>", "List ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (listId, opts) => {
2639
+ try {
2640
+ validateUuid(listId, "List ID");
2641
+ validateUuid(opts.owner, "Owner ID");
2642
+ await graphql(acceptListShare, { listId, ownerId: opts.owner });
2643
+ if (isJsonMode()) {
2644
+ printJson({ accepted: true, listId });
2645
+ return;
2646
+ }
2647
+ printSuccess("Accepted list share");
2648
+ } catch (err) {
2649
+ printError(err.message);
2650
+ process.exitCode = 1;
2651
+ }
2652
+ });
2653
+ sharesCommand.command("list-decline").description("Decline a shared list").argument("<list-id>", "List ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (listId, opts) => {
2654
+ try {
2655
+ validateUuid(listId, "List ID");
2656
+ validateUuid(opts.owner, "Owner ID");
2657
+ await graphql(declineListShare, { listId, ownerId: opts.owner });
2658
+ if (isJsonMode()) {
2659
+ printJson({ declined: true, listId });
2660
+ return;
2661
+ }
2662
+ printSuccess("Declined list share");
2663
+ } catch (err) {
2664
+ printError(err.message);
2665
+ process.exitCode = 1;
2666
+ }
2667
+ });
2668
+ sharesCommand.command("list-revoke").description("Revoke a list share").argument("<list-id>", "List ID").requiredOption("--user <user-id>", "Shared-with user ID").action(async (listId, opts) => {
2669
+ try {
2670
+ validateUuid(listId, "List ID");
2671
+ validateUuid(opts.user, "User ID");
2672
+ await graphql(revokeListShare, { listId, sharedWithId: opts.user });
2673
+ if (isJsonMode()) {
2674
+ printJson({ revoked: true, listId });
2675
+ return;
2676
+ }
2677
+ printSuccess("Revoked list share");
2678
+ } catch (err) {
2679
+ printError(err.message);
2680
+ process.exitCode = 1;
2681
+ }
2682
+ });
2683
+ sharesCommand.command("list-ls").description("List shares for a list").argument("<list-id>", "List ID").action(async (listId) => {
2684
+ try {
2685
+ validateUuid(listId, "List ID");
2686
+ const data = await graphql(listListShares, { listId });
2687
+ if (isJsonMode()) {
2688
+ printJson(data.listListShares);
2689
+ return;
2690
+ }
2691
+ if (data.listListShares.length === 0) {
2692
+ console.log("Not shared.");
2693
+ return;
2694
+ }
2695
+ printTable(
2696
+ ["Email", "Permission", "Status", "Shared At"],
2697
+ data.listListShares.map((s) => [maskEmail(s.sharedWithEmail), s.permission, s.status, formatDate(s.sharedAt)])
2698
+ );
2699
+ } catch (err) {
2700
+ printError(err.message);
2701
+ process.exitCode = 1;
2702
+ }
2703
+ });
2704
+ sharesCommand.command("list-update-permission").description("Change permission on a list share").argument("<list-id>", "List ID").requiredOption("--user <user-id>", "Shared-with user ID").requiredOption("--permission <perm>", "New permission (view or edit)").action(async (listId, opts) => {
2705
+ try {
2706
+ validateUuid(listId, "List ID");
2707
+ validateUuid(opts.user, "User ID");
2708
+ validatePermission(opts.permission);
2709
+ const data = await graphql(updateListSharePermission, {
2710
+ input: { listId, sharedWithId: opts.user, permission: opts.permission }
2711
+ });
2712
+ if (isJsonMode()) {
2713
+ printJson(data.updateListSharePermission);
2714
+ return;
2715
+ }
2716
+ printSuccess(`Updated list share permission to ${opts.permission}`);
2717
+ } catch (err) {
2718
+ printError(err.message);
2719
+ process.exitCode = 1;
2720
+ }
2721
+ });
2722
+
2723
+ // src/commands/status.ts
2724
+ import { Command as Command11 } from "commander";
2725
+ import chalk3 from "chalk";
2726
+ var statusCommand = new Command11("status").description("Show task dashboard with counts by status").option("--all", "Include archived tasks").action(async (opts) => {
2727
+ try {
2728
+ const data = await graphql(listTasks, {});
2729
+ const tasks = opts.all ? data.listTasks : data.listTasks.filter((t) => !t.archived);
2730
+ if (isJsonMode()) {
2731
+ const counts = {};
2732
+ for (const t of tasks) {
2733
+ counts[t.status] = (counts[t.status] || 0) + 1;
2734
+ }
2735
+ printJson({ total: tasks.length, counts });
2736
+ return;
2737
+ }
2738
+ if (tasks.length === 0) {
2739
+ console.log("No tasks found.");
2740
+ return;
2741
+ }
2742
+ const buckets = {
2743
+ todo: [],
2744
+ in_progress: [],
2745
+ review: [],
2746
+ done: []
2747
+ };
2748
+ for (const t of tasks) {
2749
+ if (buckets[t.status]) buckets[t.status].push(t);
2750
+ }
2751
+ const total = tasks.length;
2752
+ const parts = Object.entries(buckets).filter(([, list]) => list.length > 0).map(([status, list]) => `${formatStatus(status)} ${chalk3.bold(String(list.length))}`);
2753
+ console.log(`
2754
+ ${chalk3.bold(String(total))} tasks: ${parts.join(" ")}
2755
+ `);
2756
+ for (const status of ["in_progress", "review", "todo"]) {
2757
+ const list = buckets[status];
2758
+ if (list.length === 0) continue;
2759
+ console.log(` ${formatStatus(status)}`);
2760
+ for (const t of list.slice(0, 10)) {
2761
+ const due = t.dueDate ? chalk3.dim(` due ${formatDate(t.dueDate)}`) : "";
2762
+ const pri = t.priority === "critical" || t.priority === "high" ? ` ${formatPriority(t.priority)}` : "";
2763
+ console.log(` ${chalk3.dim(truncateId(t.id))} ${t.title}${pri}${due}`);
2764
+ }
2765
+ if (list.length > 10) {
2766
+ console.log(chalk3.dim(` ... and ${list.length - 10} more`));
2767
+ }
2768
+ console.log();
2769
+ }
2770
+ const now = /* @__PURE__ */ new Date();
2771
+ const overdue = tasks.filter(
2772
+ (t) => t.status !== "done" && t.dueDate && new Date(t.dueDate) < now
2773
+ );
2774
+ if (overdue.length > 0) {
2775
+ console.log(chalk3.red.bold(` ! ${overdue.length} overdue task${overdue.length > 1 ? "s" : ""}`));
2776
+ for (const t of overdue.slice(0, 5)) {
2777
+ console.log(chalk3.red(` ${truncateId(t.id)} ${t.title} (due ${formatDate(t.dueDate)})`));
2778
+ }
2779
+ console.log();
2780
+ }
2781
+ } catch (err) {
2782
+ printError(err.message);
2783
+ process.exitCode = 1;
2784
+ }
2785
+ });
2786
+
2787
+ // src/commands/apikeys.ts
2788
+ import { Command as Command12 } from "commander";
2789
+ import { confirm as confirm7 } from "@inquirer/prompts";
2790
+ function printKeyList(keys) {
2791
+ if (keys.length === 0) {
2792
+ console.log("No API keys.");
2793
+ return;
2794
+ }
2795
+ printTable(
2796
+ ["Name", "Prefix", "Status", "Created", "Last Used"],
2797
+ keys.map((k) => [k.name, k.keyPrefix, k.status, formatDate(k.createdAt), k.lastUsedAt ? formatDate(k.lastUsedAt) : "never"])
2798
+ );
2799
+ }
2800
+ var apikeysCommand = new Command12("apikeys").description("List and manage API keys").action(async () => {
2801
+ try {
2802
+ const data = await graphql(listApiKeys);
2803
+ if (isJsonMode()) {
2804
+ printJson(data.listApiKeys);
2805
+ return;
2806
+ }
2807
+ printKeyList(data.listApiKeys);
2808
+ } catch (err) {
2809
+ printError(err.message);
2810
+ process.exitCode = 1;
2811
+ }
2812
+ });
2813
+ apikeysCommand.command("list").description("List all API keys").action(async () => {
2814
+ try {
2815
+ const data = await graphql(listApiKeys);
2816
+ if (isJsonMode()) {
2817
+ printJson(data.listApiKeys);
2818
+ return;
2819
+ }
2820
+ printKeyList(data.listApiKeys);
2821
+ } catch (err) {
2822
+ printError(err.message);
2823
+ process.exitCode = 1;
2824
+ }
2825
+ });
2826
+ apikeysCommand.command("revoke").description("Revoke an API key").argument("<id>", "API key ID").option("--force", "Skip confirmation").action(async (id, opts) => {
2827
+ try {
2828
+ if (!opts.force) {
2829
+ const ok = await confirm7({ message: `Revoke API key ${id}?`, default: false });
2830
+ if (!ok) {
2831
+ console.log("Cancelled.");
2832
+ return;
2833
+ }
2834
+ }
2835
+ const data = await graphql(revokeApiKey, { id });
2836
+ if (isJsonMode()) {
2837
+ printJson(data.revokeApiKey);
2838
+ return;
2839
+ }
2840
+ printSuccess(`Revoked API key: ${data.revokeApiKey.name}`);
2841
+ } catch (err) {
2842
+ printError(err.message);
2843
+ process.exitCode = 1;
2844
+ }
2845
+ });
2846
+
2847
+ // src/index.ts
2848
+ var program = new Command13();
2849
+ program.name("tasklumina").version("1.0.0").description("Task Lumina CLI \u2014 manage tasks from the terminal").option("--json", "Output as JSON").option("--no-color", "Disable colored output").option("--verbose", "Show detailed request info (endpoint masked)").option("--redact", "Mask emails and sensitive data in output");
2850
+ program.hook("postAction", async () => {
2851
+ await maybeAutoRotate();
2852
+ });
2853
+ program.addCommand(configureCommand);
2854
+ program.addCommand(whoamiCommand);
2855
+ program.addCommand(statusCommand);
2856
+ program.addCommand(tasksCommand);
2857
+ program.addCommand(categoriesCommand);
2858
+ program.addCommand(tagsCommand);
2859
+ program.addCommand(notesCommand);
2860
+ program.addCommand(contactsCommand);
2861
+ program.addCommand(prefsCommand);
2862
+ program.addCommand(listsCommand);
2863
+ program.addCommand(sharesCommand);
2864
+ program.addCommand(apikeysCommand);
2865
+ program.parse();