donebear 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-BBEVWudm.mjs";
3
- import { n as readStdin, t as promptForValue } from "./prompt-GfcWO85b.mjs";
2
+ import { S as DONEBEAR_TOKEN_ENV, _ as DONEBEAR_API_URL_ENV, a as SyncMutationRejectedError, c as CLI_NAME, f as DEFAULT_APP_URL, i as EXIT_CODES, m as DEFAULT_OAUTH_CALLBACK_PORT, n as setCurrentWorkspace, o as toCliError, p as DEFAULT_OAUTH_CALLBACK_PATH, r as CliError, s as AUTH_FILE_PATH, t as ensureLocalContext, u as CONFIG_DIR } from "./context-store-ByUXl7Cn.mjs";
3
+ import { n as readStdin, t as promptForValue } from "./prompt-Bv9qkjNw.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { basename, join, resolve } from "node:path";
@@ -14,11 +14,31 @@ import { accessSync, constants, existsSync, readdirSync, statSync } from "node:f
14
14
  import { config } from "dotenv";
15
15
  import { Command, InvalidArgumentError } from "commander";
16
16
  import { createHash, randomBytes, randomUUID } from "node:crypto";
17
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
18
  import { createServer } from "node:http";
18
- import { fileURLToPath } from "node:url";
19
19
  import Database from "better-sqlite3";
20
20
  //#region \0rolldown/runtime.js
21
+ var __create = Object.create;
22
+ var __defProp = Object.defineProperty;
23
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
24
+ var __getOwnPropNames = Object.getOwnPropertyNames;
25
+ var __getProtoOf = Object.getPrototypeOf;
26
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
21
27
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
28
+ var __copyProps = (to, from, except, desc) => {
29
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
30
+ key = keys[i];
31
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
32
+ get: ((k) => from[k]).bind(null, key),
33
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
34
+ });
35
+ }
36
+ return to;
37
+ };
38
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
39
+ value: mod,
40
+ enumerable: true
41
+ }) : target, mod));
22
42
  //#endregion
23
43
  //#region src/output.ts
24
44
  const writeLine = (stream, message) => {
@@ -84,9 +104,7 @@ const writeOutput = (context, data) => {
84
104
  case "json":
85
105
  writeJson(result);
86
106
  break;
87
- default:
88
- writeJson(result);
89
- break;
107
+ default: writeJson(result);
90
108
  }
91
109
  if (context.copy) copyToClipboard(JSON.stringify(result, null, 2));
92
110
  };
@@ -155,9 +173,10 @@ const tokenize = (input) => {
155
173
  let current = "";
156
174
  let inQuote = false;
157
175
  let quoteChar = "";
158
- for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
159
- else current += char;
160
- else if (char === "\"" || char === "'") {
176
+ for (const char of input) if (inQuote) {
177
+ if (char === quoteChar) inQuote = false;
178
+ else current += char;
179
+ } else if (char === "\"" || char === "'") {
161
180
  inQuote = true;
162
181
  quoteChar = char;
163
182
  } else if (char === " ") {
@@ -209,7 +228,7 @@ const runInteractiveMode = async (context, program) => {
209
228
  rl.close();
210
229
  if (sessionEntries.length > 0) try {
211
230
  const payload = {
212
- entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY),
231
+ entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-200),
213
232
  version: 1
214
233
  };
215
234
  await mkdir(CONFIG_DIR, {
@@ -346,7 +365,8 @@ const parseAuthFile = (payload) => {
346
365
  };
347
366
  const readStoredAuthSession = async () => {
348
367
  try {
349
- return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf-8")).session;
368
+ const raw = await readFile(AUTH_FILE_PATH, "utf-8");
369
+ return parseAuthFile(raw).session;
350
370
  } catch (error) {
351
371
  if (isRecord$1(error) && error.code === "ENOENT") return null;
352
372
  if (error instanceof CliError) throw error;
@@ -501,6 +521,8 @@ const resolveTokenStatus = (token) => {
501
521
  */
502
522
  const TASK_FIELDS = {
503
523
  bootstrapFields: [
524
+ "agentAssigneeId",
525
+ "revision",
504
526
  "id",
505
527
  "number",
506
528
  "title",
@@ -534,6 +556,7 @@ const TASK_FIELDS = {
534
556
  "repeatNextAt"
535
557
  ],
536
558
  insertFields: [
559
+ "agentAssigneeId",
537
560
  "archivedAt",
538
561
  "assigneeId",
539
562
  "completedAt",
@@ -554,6 +577,7 @@ const TASK_FIELDS = {
554
577
  "repeatStatus",
555
578
  "repeatTemplateId",
556
579
  "repeatType",
580
+ "revision",
557
581
  "sortOrder",
558
582
  "start",
559
583
  "startBucket",
@@ -566,6 +590,8 @@ const TASK_FIELDS = {
566
590
  "workspaceId"
567
591
  ],
568
592
  iosKeys: [
593
+ "agentAssigneeId",
594
+ "revision",
569
595
  "id",
570
596
  "number",
571
597
  "title",
@@ -618,11 +644,14 @@ const TASK_FIELDS = {
618
644
  "projectId",
619
645
  "teamId",
620
646
  "assigneeId",
647
+ "agentAssigneeId",
648
+ "revision",
621
649
  "headingId",
622
650
  "repeatRule",
623
651
  "repeatTemplateId"
624
652
  ],
625
653
  updateFields: [
654
+ "agentAssigneeId",
626
655
  "title",
627
656
  "description",
628
657
  "sortOrder",
@@ -654,7 +683,7 @@ const TASK_FIELDS = {
654
683
  }.readProjection.join("\n");
655
684
  //#endregion
656
685
  //#region ../../packages/task-logic/src/date-only.ts
657
- const DATE_ONLY_DAY_MS = 1440 * 60 * 1e3;
686
+ const DATE_ONLY_DAY_MS = 864e5;
658
687
  const DATE_ONLY_INPUT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/u;
659
688
  const isFiniteEpoch$1 = (value) => typeof value === "number" && Number.isFinite(value);
660
689
  const isCanonicalDateOnlyEpoch = (value) => value % DATE_ONLY_DAY_MS === 0;
@@ -918,6 +947,71 @@ const createSyncMutateClient = ({ baseUrl, errorMapper = defaultErrorMapper$1, f
918
947
  return { mutate };
919
948
  };
920
949
  //#endregion
950
+ //#region ../../packages/manage-client/src/bound-task-update.ts
951
+ /** Task fields a bound agent may change through a conditional command. */
952
+ const BOUND_TASK_PATCH_FIELDS = [
953
+ "archivedAt",
954
+ "completedAt",
955
+ "deadlineAt",
956
+ "description",
957
+ "headingId",
958
+ "projectId",
959
+ "reminderAt",
960
+ "start",
961
+ "startBucket",
962
+ "startDate",
963
+ "teamId",
964
+ "title",
965
+ "todayIndexReferenceDate"
966
+ ];
967
+ /**
968
+ * Bound keys write through the conditional Task command; unbound callers
969
+ * retain native sync. Returns false when the caller is not a bound agent.
970
+ */
971
+ const updateBoundTasks = async (input) => {
972
+ const noun = input.noun ?? "change";
973
+ if (!input.token.startsWith("db_")) return false;
974
+ const workspaceId = input.updates[0]?.task.workspaceId;
975
+ if (!workspaceId || input.updates.some(({ task }) => task.workspaceId !== workspaceId)) throw new Error("Change tasks in one explicit workspace.");
976
+ const binding = await input.send(`/api/task-agents/self?workspaceId=${encodeURIComponent(workspaceId)}`);
977
+ if (binding.workspaceId !== workspaceId || binding.agent === void 0) throw new Error("Task agent binding is unavailable.");
978
+ if (binding.agent === null) return false;
979
+ if (binding.agent.disabledAt !== null) throw new Error("This task agent is disabled.");
980
+ if (input.updates.some(({ task }) => task.agentAssigneeId !== binding.agent?.id)) throw new Error("A task agent can change only tasks assigned to it.");
981
+ if (input.updates.some(({ task }) => !Number.isInteger(task.revision) || task.revision < 1)) throw new Error("The change requires the original selected task revision.");
982
+ const allowed = new Set(BOUND_TASK_PATCH_FIELDS);
983
+ for (const { patch } of input.updates) {
984
+ const refused = Object.keys(patch).filter((key) => !allowed.has(key));
985
+ if (refused.length > 0) throw new Error(`A task agent cannot change ${refused.join(", ")}. Hand the task off instead.`);
986
+ }
987
+ let changed = 0;
988
+ for (const { task, patch } of input.updates.filter(({ task: selected }, index, updates) => updates.findIndex((candidate) => candidate.task.id === selected.id) === index)) try {
989
+ const result = await input.send("/api/task-commands", {
990
+ expectedRevision: task.revision,
991
+ kind: "update",
992
+ operationId: crypto.randomUUID(),
993
+ patch,
994
+ taskId: task.id,
995
+ workspaceId
996
+ });
997
+ if (result.kind !== "update" || result.task?.id !== task.id || Object.entries(patch).some(([key, value]) => result.task?.[key] !== value) || result.task.revision !== task.revision + 1) throw new Error("The change has no matching committed receipt; do not retry automatically.");
998
+ changed += 1;
999
+ } catch (error) {
1000
+ throw new Error(`${noun[0]?.toUpperCase()}${noun.slice(1)} failed with ${changed} confirmed ${noun}(s). The current request may have an unknown outcome; do not retry automatically. ${error instanceof Error ? error.message : "No response."}`, { cause: error });
1001
+ }
1002
+ return true;
1003
+ };
1004
+ /** Archive is the one bound write the CLI also drives; it keeps its own wording. */
1005
+ const archiveBoundTasks = (input) => updateBoundTasks({
1006
+ noun: "archive",
1007
+ send: input.send,
1008
+ token: input.token,
1009
+ updates: input.tasks.map((task) => ({
1010
+ patch: { archivedAt: input.archivedAt },
1011
+ task
1012
+ }))
1013
+ });
1014
+ //#endregion
921
1015
  //#region ../../packages/manage-client/src/index.ts
922
1016
  const TASKS_QUERY = `
923
1017
  query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
@@ -956,6 +1050,7 @@ const toInstantEpoch = (value) => {
956
1050
  return Number.isFinite(epoch) ? epoch : null;
957
1051
  };
958
1052
  const toTaskRecord$1 = (node) => ({
1053
+ agentAssigneeId: node.agentAssigneeId,
959
1054
  archivedAt: toInstantEpoch(node.archivedAt),
960
1055
  assigneeId: node.assigneeId,
961
1056
  completedAt: toInstantEpoch(node.completedAt),
@@ -969,6 +1064,7 @@ const toTaskRecord$1 = (node) => ({
969
1064
  projectId: node.projectId,
970
1065
  repeatRule: node.repeatRule,
971
1066
  repeatTemplateId: node.repeatTemplateId,
1067
+ revision: node.revision,
972
1068
  start: node.start,
973
1069
  startBucket: node.startBucket,
974
1070
  startDate: parseDateOnlyInputToEpoch(node.startDate),
@@ -1291,7 +1387,7 @@ const WORKSPACE_INVITATION_CREATE_MUTATION = `
1291
1387
  mutation WorkspaceInvitationCreate($input: WorkspaceInvitationCreateInput!) {
1292
1388
  workspaceInvitationCreate(input: $input) {
1293
1389
  success lastSyncId
1294
- invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId }
1390
+ invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId kind projectId }
1295
1391
  }
1296
1392
  }
1297
1393
  `;
@@ -1369,6 +1465,7 @@ const toTaskRecord = (value) => {
1369
1465
  if (typeof row.id !== "string" || typeof row.number !== "number" || typeof row.title !== "string") return null;
1370
1466
  if (typeof row.creatorId !== "string" || typeof row.workspaceId !== "string" || typeof row.start !== "string" || typeof row.startBucket !== "string") return null;
1371
1467
  return {
1468
+ agentAssigneeId: typeof row.agentAssigneeId === "string" ? row.agentAssigneeId : null,
1372
1469
  archivedAt: toNullableNumber(row.archivedAt),
1373
1470
  assigneeId: typeof row.assigneeId === "string" ? row.assigneeId : null,
1374
1471
  completedAt: toNullableNumber(row.completedAt),
@@ -1382,6 +1479,7 @@ const toTaskRecord = (value) => {
1382
1479
  projectId: typeof row.projectId === "string" ? row.projectId : null,
1383
1480
  repeatRule: typeof row.repeatRule === "string" ? row.repeatRule : null,
1384
1481
  repeatTemplateId: typeof row.repeatTemplateId === "string" ? row.repeatTemplateId : null,
1482
+ revision: typeof row.revision === "number" ? row.revision : 0,
1385
1483
  start: row.start,
1386
1484
  startBucket: row.startBucket,
1387
1485
  startDate: toNullableNumber(row.startDate),
@@ -1530,12 +1628,14 @@ const toTaskAttachmentRecord = (value) => {
1530
1628
  };
1531
1629
  /** A task's comment thread, oldest first. */
1532
1630
  const loadCommentsByTask = async (context, options) => {
1533
- const rows = await requestNdjson(context, `${options.baseUrl}/sync/batch`, {
1534
- body: { requests: [{
1535
- indexedKey: "taskId",
1536
- keyValue: options.taskId,
1537
- modelName: "Comment"
1538
- }] },
1631
+ const url = `${options.baseUrl}/sync/batch`;
1632
+ const body = { requests: [{
1633
+ indexedKey: "taskId",
1634
+ keyValue: options.taskId,
1635
+ modelName: "Comment"
1636
+ }] };
1637
+ const rows = await requestNdjson(context, url, {
1638
+ body,
1539
1639
  token: options.token
1540
1640
  });
1541
1641
  const results = [];
@@ -1546,12 +1646,14 @@ const loadCommentsByTask = async (context, options) => {
1546
1646
  return results.toSorted((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
1547
1647
  };
1548
1648
  const loadReadyTaskAttachmentsByTask = async (context, options) => {
1549
- const rows = await requestNdjson(context, `${options.baseUrl}/sync/batch`, {
1550
- body: { requests: [{
1551
- indexedKey: "taskId",
1552
- keyValue: options.taskId,
1553
- modelName: "TaskAttachment"
1554
- }] },
1649
+ const url = `${options.baseUrl}/sync/batch`;
1650
+ const body = { requests: [{
1651
+ indexedKey: "taskId",
1652
+ keyValue: options.taskId,
1653
+ modelName: "TaskAttachment"
1654
+ }] };
1655
+ const rows = await requestNdjson(context, url, {
1656
+ body,
1555
1657
  token: options.token
1556
1658
  });
1557
1659
  const results = [];
@@ -1564,7 +1666,8 @@ const loadReadyTaskAttachmentsByTask = async (context, options) => {
1564
1666
  const resolveApiBaseUrl = (context) => {
1565
1667
  const fromContext = context.apiUrlOverride?.trim();
1566
1668
  if (fromContext) return normalizeApiUrl(fromContext);
1567
- return normalizeApiUrl(readEnvValue("DONEBEAR_API_URL") ?? "https://api.donebear.com");
1669
+ const fromEnv = readEnvValue(DONEBEAR_API_URL_ENV);
1670
+ return normalizeApiUrl(fromEnv ?? "https://api.donebear.com");
1568
1671
  };
1569
1672
  const mapGraphqlFailure = (failure) => {
1570
1673
  if (failure.type === "http") {
@@ -1600,11 +1703,12 @@ const listWorkspaces = async (_context, options) => await createGraphqlClient({
1600
1703
  getToken: () => options.token
1601
1704
  }).listWorkspaces();
1602
1705
  const createWorkspace = async (context, options) => {
1603
- return toWorkspaceRecordFromGraphqlNode((await requestGraphql(context, options.baseUrl, {
1706
+ const data = await requestGraphql(context, options.baseUrl, {
1604
1707
  query: WORKSPACE_CREATE_MUTATION,
1605
1708
  token: options.token,
1606
1709
  variables: { input: options.body }
1607
- })).workspaceCreate.workspace);
1710
+ });
1711
+ return toWorkspaceRecordFromGraphqlNode(data.workspaceCreate.workspace);
1608
1712
  };
1609
1713
  const joinWorkspace = async (context, options) => {
1610
1714
  const data = await requestGraphql(context, options.baseUrl, {
@@ -1634,12 +1738,14 @@ const loadTasksByWorkspaceIds = (context, options) => paginateGraphql(context, {
1634
1738
  variables: { workspaceIds: options.workspaceIds }
1635
1739
  });
1636
1740
  const loadTaskById = async (context, options) => {
1637
- const rows = await requestNdjson(context, `${options.baseUrl}/sync/batch`, {
1638
- body: { requests: [{
1639
- indexedKey: "id",
1640
- keyValue: options.taskId,
1641
- modelName: "Task"
1642
- }] },
1741
+ const url = `${options.baseUrl}/sync/batch`;
1742
+ const body = { requests: [{
1743
+ indexedKey: "id",
1744
+ keyValue: options.taskId,
1745
+ modelName: "Task"
1746
+ }] };
1747
+ const rows = await requestNdjson(context, url, {
1748
+ body,
1643
1749
  token: options.token
1644
1750
  });
1645
1751
  for (const row of rows) {
@@ -1697,12 +1803,14 @@ const loadTeamsByWorkspaceIds = (context, options) => paginateGraphql(context, {
1697
1803
  variables: { workspaceIds: options.workspaceIds }
1698
1804
  });
1699
1805
  const loadChecklistItemsByTask = async (context, options) => {
1700
- const rows = await requestNdjson(context, `${options.baseUrl}/sync/batch`, {
1701
- body: { requests: [{
1702
- indexedKey: "taskId",
1703
- keyValue: options.taskId,
1704
- modelName: "TaskChecklistItem"
1705
- }] },
1806
+ const url = `${options.baseUrl}/sync/batch`;
1807
+ const body = { requests: [{
1808
+ indexedKey: "taskId",
1809
+ keyValue: options.taskId,
1810
+ modelName: "TaskChecklistItem"
1811
+ }] };
1812
+ const rows = await requestNdjson(context, url, {
1813
+ body,
1706
1814
  token: options.token
1707
1815
  });
1708
1816
  const results = [];
@@ -1722,7 +1830,10 @@ const mapSyncMutateFailure = (failure) => {
1722
1830
  const exitCode = status === 401 || status === 403 ? EXIT_CODES.AUTH_REQUIRED : EXIT_CODES.ERROR;
1723
1831
  return new CliError(extractErrorMessage(body), exitCode);
1724
1832
  }
1725
- if (failure.type === "mutation") return new CliError(failure.response.results.find((item) => !item.success)?.error ?? "Sync mutation failed", EXIT_CODES.ERROR);
1833
+ if (failure.type === "mutation") {
1834
+ const result = failure.response.results.find((item) => !item.success);
1835
+ return new SyncMutationRejectedError(result?.error ?? "Sync mutation failed", failure.response.results);
1836
+ }
1726
1837
  if (failure.type === "timeout") return new CliError(`Sync mutation timed out after ${failure.timeoutMs}ms`, EXIT_CODES.ERROR);
1727
1838
  if (failure.cause instanceof Error) return failure.cause;
1728
1839
  return new CliError("Sync mutation request failed", EXIT_CODES.ERROR);
@@ -1766,14 +1877,17 @@ const listWorkspaceInvitations = async (context, options) => await paginateGraph
1766
1877
  const createWorkspaceInvitation = async (context, options) => {
1767
1878
  const input = omitUndefined({
1768
1879
  email: options.email,
1880
+ kind: options.kind,
1881
+ projectId: options.projectId,
1769
1882
  role: options.role,
1770
1883
  workspaceId: options.workspaceId
1771
1884
  });
1772
- return toWorkspaceInvitationRecordFromGraphqlNode((await requestGraphql(context, options.baseUrl, {
1885
+ const data = await requestGraphql(context, options.baseUrl, {
1773
1886
  query: WORKSPACE_INVITATION_CREATE_MUTATION,
1774
1887
  token: options.token,
1775
1888
  variables: { input }
1776
- })).workspaceInvitationCreate.invitation);
1889
+ });
1890
+ return toWorkspaceInvitationRecordFromGraphqlNode(data.workspaceInvitationCreate.invitation);
1777
1891
  };
1778
1892
  const listWorkspaceHistory = async (context, options) => {
1779
1893
  const variables = omitUndefined({
@@ -1853,6 +1967,56 @@ const requireUserId = async (context, commandContext) => {
1853
1967
  token: commandContext.token
1854
1968
  })).id;
1855
1969
  };
1970
+ const packageManifest = createRequire(import.meta.url)("../package.json");
1971
+ //#endregion
1972
+ //#region src/crash-report.ts
1973
+ const POSTHOG_HOST = "https://us.i.posthog.com";
1974
+ const SHUTDOWN_TIMEOUT_MS = 2e3;
1975
+ const buildKey = () => "";
1976
+ const hasNodeErrorCode = (error) => [error, error.cause].some((candidate) => typeof candidate === "object" && candidate !== null && "code" in candidate && typeof candidate.code === "string");
1977
+ const isCliBug = (error) => (error instanceof TypeError || error instanceof ReferenceError || error instanceof RangeError) && !hasNodeErrorCode(error);
1978
+ const crashReportingEnabled = (key, env) => key.length > 0 && ![
1979
+ "1",
1980
+ "true",
1981
+ "yes"
1982
+ ].includes(env.DO_NOT_TRACK?.trim().toLowerCase() ?? "");
1983
+ const REDACTED_MESSAGE = "[message not sent]";
1984
+ const WINDOWS_PATH = /^[A-Za-z]:\\/u;
1985
+ /** The report as sent: home paths cut to `~`, every exception message gone. */
1986
+ const redactCrashReport = (event, home) => {
1987
+ if (!event) return event;
1988
+ const forms = home ? [.../* @__PURE__ */ new Set([
1989
+ home,
1990
+ home.replaceAll("\\", "/"),
1991
+ pathToFileURL(home, { windows: WINDOWS_PATH.test(home) }).pathname
1992
+ ])] : [];
1993
+ let json = JSON.stringify(event);
1994
+ for (const form of forms) json = json.replaceAll(JSON.stringify(form).slice(1, -1), "~");
1995
+ const redacted = JSON.parse(json);
1996
+ const exceptions = redacted.properties?.$exception_list;
1997
+ for (const exception of exceptions ?? []) exception.value = REDACTED_MESSAGE;
1998
+ return redacted;
1999
+ };
2000
+ const reportCrash = async (error) => {
2001
+ const key = buildKey();
2002
+ if (!(isCliBug(error) && crashReportingEnabled(key, process.env))) return;
2003
+ try {
2004
+ const { PostHog } = await import("posthog-node");
2005
+ const home = homedir();
2006
+ const client = new PostHog(key, {
2007
+ before_send: (event) => redactCrashReport(event, home),
2008
+ disableGeoip: true,
2009
+ flushAt: 1,
2010
+ host: POSTHOG_HOST
2011
+ });
2012
+ client.captureException(error, void 0, {
2013
+ cli_version: packageManifest.version,
2014
+ node_version: process.version,
2015
+ os_platform: process.platform
2016
+ });
2017
+ await client.shutdown(SHUTDOWN_TIMEOUT_MS);
2018
+ } catch {}
2019
+ };
1856
2020
  //#endregion
1857
2021
  //#region src/options.ts
1858
2022
  const parseOutputFormat = (value) => {
@@ -1942,6 +2106,7 @@ const runWithErrorHandling = async (context, action) => {
1942
2106
  }
1943
2107
  if (context.debug && error instanceof Error && typeof error.stack === "string") writeDebug(context, error.stack);
1944
2108
  process.exitCode = cliError.exitCode;
2109
+ if (cliError.exitCode === EXIT_CODES.ERROR) await reportCrash(error);
1945
2110
  }
1946
2111
  };
1947
2112
  //#endregion
@@ -2023,8 +2188,9 @@ const runCreate = async (context, label, options) => {
2023
2188
  };
2024
2189
  const runList = async (context) => {
2025
2190
  const { token } = await requireAuthToken(context);
2191
+ const baseUrl = resolveApiBaseUrl(context);
2026
2192
  const keys = await listApiKeys(context, {
2027
- baseUrl: resolveApiBaseUrl(context),
2193
+ baseUrl,
2028
2194
  token
2029
2195
  });
2030
2196
  if (context.json) {
@@ -2046,8 +2212,9 @@ const runList = async (context) => {
2046
2212
  };
2047
2213
  const runRevoke = async (context, keyId) => {
2048
2214
  const { token } = await requireAuthToken(context);
2215
+ const baseUrl = resolveApiBaseUrl(context);
2049
2216
  await revokeApiKey(context, {
2050
- baseUrl: resolveApiBaseUrl(context),
2217
+ baseUrl,
2051
2218
  keyId,
2052
2219
  token
2053
2220
  });
@@ -2231,9 +2398,187 @@ const createOAuthState = () => randomBytes(24).toString("hex");
2231
2398
  const createCodeVerifier = () => randomBytes(64).toString("base64url");
2232
2399
  const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
2233
2400
  //#endregion
2401
+ //#region src/api/agent-signup.ts
2402
+ const parseJson = async (response) => {
2403
+ const raw = await response.text();
2404
+ if (raw.length === 0) return null;
2405
+ try {
2406
+ return JSON.parse(raw);
2407
+ } catch {
2408
+ return raw;
2409
+ }
2410
+ };
2411
+ const throwForFailure = (status, payload) => {
2412
+ const message = typeof payload === "string" && payload.length > 0 ? payload : extractErrorMessage(payload);
2413
+ const record = typeof payload === "object" && payload !== null ? payload : {};
2414
+ const code = typeof record.code === "string" ? record.code : null;
2415
+ if (status === 409 || code === "DUPLICATE_EMAIL") throw new CliError(message, EXIT_CODES.AUTH_REQUIRED, "Run `donebear auth login` with this email. Do not start another signup.");
2416
+ if (status === 429 || code === "RATE_LIMIT_EXCEEDED") throw new CliError(message, EXIT_CODES.ERROR, "Wait, then retry.");
2417
+ if (status === 400 || code === "INVALID_REQUEST") throw new CliError(message, EXIT_CODES.VALIDATION);
2418
+ throw new CliError(message, EXIT_CODES.ERROR);
2419
+ };
2420
+ const request = async (endpoint, options) => {
2421
+ const headers = new Headers({ Accept: "application/json" });
2422
+ if (options.token) headers.set("Authorization", `Bearer ${options.token}`);
2423
+ if (options.body !== void 0) headers.set("Content-Type", "application/json");
2424
+ let response;
2425
+ try {
2426
+ response = await fetch(endpoint, {
2427
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
2428
+ headers,
2429
+ method: options.method ?? "GET"
2430
+ });
2431
+ } catch (error) {
2432
+ throw new CliError("Cannot connect to Done Bear API.", EXIT_CODES.NETWORK, error instanceof Error ? error.message : void 0);
2433
+ }
2434
+ const payload = await parseJson(response);
2435
+ if (!response.ok) throwForFailure(response.status, payload);
2436
+ return payload;
2437
+ };
2438
+ const startAgentSignup = async (_context, apiBaseUrl, input) => {
2439
+ return await request(`${apiBaseUrl}/api/agent-signup`, {
2440
+ body: {
2441
+ email: input.email,
2442
+ ...input.name ? { name: input.name } : {}
2443
+ },
2444
+ method: "POST"
2445
+ });
2446
+ };
2447
+ const pollAgentSignup = async (_context, apiBaseUrl, pollToken) => {
2448
+ return await request(`${apiBaseUrl}/api/agent-signup/poll`, {
2449
+ method: "GET",
2450
+ token: pollToken
2451
+ });
2452
+ };
2453
+ const verifyAgentSignupCode = async (_context, apiBaseUrl, input) => {
2454
+ return await request(`${apiBaseUrl}/api/agent-signup/verify`, {
2455
+ body: input,
2456
+ method: "POST"
2457
+ });
2458
+ };
2459
+ //#endregion
2460
+ //#region src/commands/auth-signup.ts
2461
+ const MISSING_EMAIL_MESSAGE = "Pass --email with the human's address. Do not invent one.";
2462
+ const POLL_INTERVAL_MS = 2e3;
2463
+ const defaultClock = {
2464
+ now: () => Date.now(),
2465
+ sleep: (ms, signal) => new Promise((resolve, reject) => {
2466
+ if (signal.aborted) {
2467
+ resolve();
2468
+ return;
2469
+ }
2470
+ const timer = setTimeout(resolve, ms);
2471
+ signal.addEventListener("abort", () => {
2472
+ clearTimeout(timer);
2473
+ reject(/* @__PURE__ */ new Error("aborted"));
2474
+ }, { once: true });
2475
+ })
2476
+ };
2477
+ const isComplete = (value) => value.status === "complete";
2478
+ const printWaiting = (context, verifyUrl, instructions) => {
2479
+ writeWarning(context, instructions.agent);
2480
+ writeWarning(context, instructions.human);
2481
+ writeWarning(context, `Verify link: ${verifyUrl}`);
2482
+ writeWarning(context, "Waiting for the human to verify. The CLI will continue once that happens.");
2483
+ };
2484
+ const sessionFromComplete = (complete) => ({
2485
+ accessToken: complete.apiKey.key,
2486
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2487
+ expiresAt: null,
2488
+ provider: "oauth2",
2489
+ refreshToken: null,
2490
+ tokenType: "bearer",
2491
+ user: {
2492
+ email: complete.user.email,
2493
+ id: complete.user.id
2494
+ }
2495
+ });
2496
+ const printComplete = (context, complete, verifyUrl) => {
2497
+ if (context.json) {
2498
+ writeJson({
2499
+ apiKey: complete.apiKey,
2500
+ email: complete.email,
2501
+ instructions: {
2502
+ agent: "Store the API key as DONEBEAR_TOKEN. Paste mcp.cursor into the MCP config. Do not log the full key.",
2503
+ human: "Verification is done."
2504
+ },
2505
+ mcp: complete.mcp,
2506
+ ok: true,
2507
+ sessionStored: true,
2508
+ status: "complete",
2509
+ user: complete.user,
2510
+ verifyUrl,
2511
+ workspace: complete.workspace
2512
+ });
2513
+ return;
2514
+ }
2515
+ writeSuccess(context, "Signup complete. API key stored in the CLI session.");
2516
+ writeInfo(context, `Signed in as ${complete.user.email}`);
2517
+ writeInfo(context, `Workspace: ${complete.workspace.name} (${complete.workspace.urlKey ?? complete.workspace.id})`);
2518
+ writeInfo(context, "API key (shown once):");
2519
+ writeInfo(context, complete.apiKey.key);
2520
+ writeInfo(context, "Paste-ready MCP config for Cursor:");
2521
+ writeInfo(context, JSON.stringify(complete.mcp.cursor, null, 2));
2522
+ writeInfo(context, complete.mcp.claude);
2523
+ };
2524
+ const runAgentSignup = async (context, options, clock = defaultClock) => {
2525
+ const email = options.email?.trim() ?? "";
2526
+ if (email.length === 0) throw new CliError(MISSING_EMAIL_MESSAGE, EXIT_CODES.VALIDATION, "Example: donebear auth signup --email person@example.com");
2527
+ const apiBaseUrl = resolveApiBaseUrl(context);
2528
+ const started = await startAgentSignup(context, apiBaseUrl, {
2529
+ email,
2530
+ name: options.name?.trim() || void 0
2531
+ });
2532
+ printWaiting(context, started.verifyUrl, started.instructions);
2533
+ const abort = new AbortController();
2534
+ const canPrompt = options.interactive && process.stdin.isTTY && !context.json && !context.quiet;
2535
+ let submittedCode = null;
2536
+ const promptTask = canPrompt ? promptForValue(context, "6-digit email code", abort.signal).then((value) => {
2537
+ submittedCode = value;
2538
+ }) : Promise.resolve();
2539
+ const deadline = clock.now() + options.timeout * 1e3;
2540
+ try {
2541
+ while (clock.now() < deadline) {
2542
+ if (submittedCode) {
2543
+ const verified = await verifyAgentSignupCode(context, apiBaseUrl, {
2544
+ code: submittedCode,
2545
+ pollToken: started.pollToken
2546
+ });
2547
+ if (isComplete(verified)) {
2548
+ abort.abort();
2549
+ await writeStoredAuthSession(sessionFromComplete(verified));
2550
+ printComplete(context, verified, started.verifyUrl);
2551
+ return;
2552
+ }
2553
+ if (verified.status === "consumed") throw new CliError(verified.message, EXIT_CODES.ERROR);
2554
+ submittedCode = null;
2555
+ }
2556
+ const polled = await pollAgentSignup(context, apiBaseUrl, started.pollToken);
2557
+ if (isComplete(polled)) {
2558
+ abort.abort();
2559
+ await writeStoredAuthSession(sessionFromComplete(polled));
2560
+ printComplete(context, polled, started.verifyUrl);
2561
+ return;
2562
+ }
2563
+ if (polled.status === "consumed") throw new CliError(polled.message, EXIT_CODES.ERROR);
2564
+ try {
2565
+ await clock.sleep(POLL_INTERVAL_MS, abort.signal);
2566
+ } catch {
2567
+ break;
2568
+ }
2569
+ }
2570
+ } finally {
2571
+ abort.abort();
2572
+ await promptTask.catch(() => void 0);
2573
+ }
2574
+ throw new CliError("Signup timed out before the email was verified.", EXIT_CODES.ERROR, "Ask the human to open the verify link, then run this command again if the code expired.");
2575
+ };
2576
+ const parseSignupTimeout = (value) => parsePositiveInteger(value, "Timeout");
2577
+ //#endregion
2234
2578
  //#region src/commands/auth.ts
2235
2579
  const runLogin = async (context, options) => {
2236
- const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
2580
+ const config = resolveSupabaseConfig();
2581
+ const { authorizeUrl, tokenUrl } = buildOAuthUrls(config);
2237
2582
  const clientId = resolveOAuthClientId();
2238
2583
  const redirectUrl = new URL(`http://127.0.0.1:${options.port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
2239
2584
  const state = createOAuthState();
@@ -2262,10 +2607,11 @@ const runLogin = async (context, options) => {
2262
2607
  writeInfo(context, authUrlString);
2263
2608
  }
2264
2609
  const authorizationCode = await callbackPromise;
2265
- const storedSession = tokenResponseToStoredSession(await exchangeAuthorizationCode({
2610
+ const tokenResponse = await exchangeAuthorizationCode({
2266
2611
  clientId,
2267
2612
  tokenUrl
2268
- }, authorizationCode, codeVerifier, redirectUrl.toString()));
2613
+ }, authorizationCode, codeVerifier, redirectUrl.toString());
2614
+ const storedSession = tokenResponseToStoredSession(tokenResponse);
2269
2615
  await writeStoredAuthSession(storedSession);
2270
2616
  if (context.json) {
2271
2617
  writeJson({
@@ -2280,7 +2626,8 @@ const runLogin = async (context, options) => {
2280
2626
  if (storedSession.user?.email) writeInfo(context, `Signed in as ${storedSession.user.email}`);
2281
2627
  };
2282
2628
  const runStatus = async (context) => {
2283
- const token = await resolveAuthToken(context, getOptionalSupabaseConfig());
2629
+ const config = getOptionalSupabaseConfig();
2630
+ const token = await resolveAuthToken(context, config);
2284
2631
  if (!token) {
2285
2632
  if (context.json) writeJson({
2286
2633
  authenticated: false,
@@ -2333,6 +2680,7 @@ const getOptionalSupabaseConfig = () => {
2333
2680
  const registerAuthCommands = (program) => {
2334
2681
  const auth = program.command("auth").description("Authenticate donebear CLI").addHelpText("after", `
2335
2682
  Examples:
2683
+ donebear auth signup --email person@example.com --json
2336
2684
  donebear auth login
2337
2685
  donebear auth login --no-open (print URL, don't open browser)
2338
2686
  donebear auth status --json
@@ -2361,6 +2709,25 @@ Non-interactive / CI use:
2361
2709
  await runLogin(context, options);
2362
2710
  });
2363
2711
  });
2712
+ auth.command("signup").description("Create a Done Bear account from a human email (agent signup)").option("--email <email>", "Human email to verify. Do not invent one.").option("--name <name>", "Optional display name").option("--timeout <seconds>", "Seconds to wait for email verification", parseSignupTimeout, 600).option("--no-interactive", "Never prompt for the email code; poll the verify link instead").addHelpText("after", `
2713
+ Creates a real human account. One person must verify the email (code or link).
2714
+ Do not invent an email address. If the address is already registered, run
2715
+ \`donebear auth login\` instead.
2716
+
2717
+ Examples:
2718
+ donebear auth signup --email person@example.com
2719
+ donebear auth signup --email person@example.com --json --no-interactive
2720
+ `).action(async (options, command) => {
2721
+ const context = contextFromCommand(command);
2722
+ await runWithErrorHandling(context, async () => {
2723
+ await runAgentSignup(context, {
2724
+ email: options.email,
2725
+ interactive: options.interactive !== false,
2726
+ name: options.name,
2727
+ timeout: options.timeout
2728
+ });
2729
+ });
2730
+ });
2364
2731
  auth.command("status").description("Check authentication status").action(async (_options, command) => {
2365
2732
  const context = contextFromCommand(command);
2366
2733
  await runWithErrorHandling(context, async () => {
@@ -2418,7 +2785,7 @@ const parseTaskNumberRef = (value) => {
2418
2785
  const normalized = value.trim();
2419
2786
  if (!/^[1-9]\d*$/u.test(normalized)) throw new Error(`Invalid task number "${normalized}". Use the positive number shown on the task.`);
2420
2787
  const number = Number(normalized);
2421
- if (!Number.isSafeInteger(number) || number > MAX_TASK_NUMBER) throw new TypeError(`Invalid task number "${normalized}". Number is too large.`);
2788
+ if (!Number.isSafeInteger(number) || number > MAX_TASK_NUMBER) throw new Error(`Invalid task number "${normalized}". Number is too large.`);
2422
2789
  return number;
2423
2790
  };
2424
2791
  const parseTaskState = (value) => {
@@ -2487,29 +2854,29 @@ const confirmAction = (context, message) => {
2487
2854
  });
2488
2855
  };
2489
2856
  //#endregion
2490
- //#region ../../node_modules/uuid/dist-node/regex.js
2857
+ //#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/regex.js
2491
2858
  var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
2492
2859
  //#endregion
2493
- //#region ../../node_modules/uuid/dist-node/validate.js
2860
+ //#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/validate.js
2494
2861
  function validate(uuid) {
2495
2862
  return typeof uuid === "string" && regex_default.test(uuid);
2496
2863
  }
2497
2864
  //#endregion
2498
- //#region ../../node_modules/uuid/dist-node/parse.js
2865
+ //#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/parse.js
2499
2866
  function parse(uuid) {
2500
2867
  if (!validate(uuid)) throw TypeError("Invalid UUID");
2501
2868
  let v;
2502
2869
  return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, v >>> 16 & 255, v >>> 8 & 255, v & 255, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(24, 36), 16)) / 1099511627776 & 255, v / 4294967296 & 255, v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255);
2503
2870
  }
2504
2871
  //#endregion
2505
- //#region ../../node_modules/uuid/dist-node/stringify.js
2872
+ //#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/stringify.js
2506
2873
  const byteToHex = [];
2507
2874
  for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
2508
2875
  function unsafeStringify(arr, offset = 0) {
2509
2876
  return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
2510
2877
  }
2511
2878
  //#endregion
2512
- //#region ../../node_modules/uuid/dist-node/v35.js
2879
+ //#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/v35.js
2513
2880
  function stringToBytes(str) {
2514
2881
  str = unescape(encodeURIComponent(str));
2515
2882
  const bytes = new Uint8Array(str.length);
@@ -2538,14 +2905,14 @@ function v35(version, hash, value, namespace, buf, offset) {
2538
2905
  return unsafeStringify(bytes);
2539
2906
  }
2540
2907
  //#endregion
2541
- //#region ../../node_modules/uuid/dist-node/sha1.js
2908
+ //#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/sha1.js
2542
2909
  function sha1(bytes) {
2543
2910
  if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
2544
2911
  else if (typeof bytes === "string") bytes = Buffer.from(bytes, "utf8");
2545
2912
  return createHash("sha1").update(bytes).digest();
2546
2913
  }
2547
2914
  //#endregion
2548
- //#region ../../node_modules/uuid/dist-node/v5.js
2915
+ //#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/v5.js
2549
2916
  function v5(value, namespace, buf, offset) {
2550
2917
  return v35(80, sha1, value, namespace, buf, offset);
2551
2918
  }
@@ -2566,6 +2933,11 @@ const createTeamProjectId = (params) => createCompositeSyncId("TeamProject", [
2566
2933
  params.teamId,
2567
2934
  params.projectId
2568
2935
  ]);
2936
+ const createTeamMembershipId = (params) => createCompositeSyncId("TeamMembership", [
2937
+ params.workspaceId,
2938
+ params.teamId,
2939
+ params.userId
2940
+ ]);
2569
2941
  const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMembership", [
2570
2942
  params.workspaceId,
2571
2943
  params.projectId,
@@ -2651,7 +3023,7 @@ const resolveWorkspace = async (context, options) => {
2651
3023
  //#region src/commands/task.ts
2652
3024
  const DEFAULT_TASK_LIST_LIMIT = 20;
2653
3025
  /** The views `--when` can place a task in: the ones with start fields. */
2654
- const ACTIVE_TASK_VIEWS = new Set([
3026
+ const ACTIVE_TASK_VIEWS = /* @__PURE__ */ new Set([
2655
3027
  "inbox",
2656
3028
  "anytime",
2657
3029
  "today",
@@ -2659,7 +3031,7 @@ const ACTIVE_TASK_VIEWS = new Set([
2659
3031
  "someday"
2660
3032
  ]);
2661
3033
  /** The views `--view` can filter a list by, which adds the two read-only ones. */
2662
- const VALID_TASK_VIEWS = new Set([
3034
+ const VALID_TASK_VIEWS = /* @__PURE__ */ new Set([
2663
3035
  ...ACTIVE_TASK_VIEWS,
2664
3036
  "logbook",
2665
3037
  "trash"
@@ -2949,7 +3321,18 @@ const runTaskMutation = async (context, taskRef, payload) => {
2949
3321
  taskRef,
2950
3322
  workspaceRef: payload.workspaceRef
2951
3323
  });
2952
- await mutateModel(context, {
3324
+ if (!(payload.action === "ARCHIVE" && !context.dryRun && await archiveBoundTasks({
3325
+ archivedAt: Number(payload.data.archivedAt),
3326
+ send: (path, body) => requestJson(context, `${commandContext.apiBaseUrl}${path}`, {
3327
+ token: commandContext.token,
3328
+ ...body === void 0 ? {} : {
3329
+ body,
3330
+ method: "POST"
3331
+ }
3332
+ }),
3333
+ tasks: [task],
3334
+ token: commandContext.token
3335
+ }))) await mutateModel(context, {
2953
3336
  baseUrl: commandContext.apiBaseUrl,
2954
3337
  request: {
2955
3338
  batchId: randomUUID(),
@@ -2959,7 +3342,10 @@ const runTaskMutation = async (context, taskRef, payload) => {
2959
3342
  clientTxId: randomUUID(),
2960
3343
  modelId: task.id,
2961
3344
  modelName: "Task",
2962
- payload: payload.data
3345
+ payload: {
3346
+ ...payload.data,
3347
+ ...task.agentAssigneeId ? { expectedRevision: task.revision } : {}
3348
+ }
2963
3349
  }]
2964
3350
  },
2965
3351
  token: commandContext.token
@@ -2989,11 +3375,12 @@ const resolveProjectId = async (context, commandContext, projectRef, workspaceRe
2989
3375
  token: commandContext.token,
2990
3376
  workspaceRef
2991
3377
  });
2992
- return resolveEntityId(await loadProjectsByWorkspace(context, {
3378
+ const projects = await loadProjectsByWorkspace(context, {
2993
3379
  baseUrl: commandContext.apiBaseUrl,
2994
3380
  token: commandContext.token,
2995
3381
  workspaceId: workspace.id
2996
- }), projectRef, "Project");
3382
+ });
3383
+ return resolveEntityId(projects, projectRef, "Project");
2997
3384
  };
2998
3385
  const resolveTeamId = async (context, commandContext, teamRef, workspaceRef) => {
2999
3386
  const { workspace } = await resolveWorkspace(context, {
@@ -3002,11 +3389,12 @@ const resolveTeamId = async (context, commandContext, teamRef, workspaceRef) =>
3002
3389
  token: commandContext.token,
3003
3390
  workspaceRef
3004
3391
  });
3005
- return resolveEntityId(await loadTeamsByWorkspace(context, {
3392
+ const teams = await loadTeamsByWorkspace(context, {
3006
3393
  baseUrl: commandContext.apiBaseUrl,
3007
3394
  token: commandContext.token,
3008
3395
  workspaceId: workspace.id
3009
- }), teamRef, "Team");
3396
+ });
3397
+ return resolveEntityId(teams, teamRef, "Team");
3010
3398
  };
3011
3399
  const validateTaskEditOptions = (options) => {
3012
3400
  if (options.clearDescription && typeof options.description === "string") throw new Error("Use either --description or --clear-description, not both.");
@@ -3049,8 +3437,9 @@ const runTaskEdit = async (context, taskRef, options, workspaceOverride) => {
3049
3437
  });
3050
3438
  };
3051
3439
  const runTaskRead = async (context, taskRef, options, workspaceOverride) => {
3440
+ const commandContext = await resolveCommandContext(context);
3052
3441
  const task = await resolveTaskReference(context, {
3053
- commandContext: await resolveCommandContext(context),
3442
+ commandContext,
3054
3443
  taskRef,
3055
3444
  workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
3056
3445
  });
@@ -3065,14 +3454,16 @@ const runTaskRead = async (context, taskRef, options, workspaceOverride) => {
3065
3454
  writeInfo(context, task.description ?? "(empty)");
3066
3455
  };
3067
3456
  const runTaskAppend = async (context, taskRef, text, options, workspaceOverride) => {
3457
+ const commandContext = await resolveCommandContext(context);
3458
+ const newDesc = `${(await resolveTaskReference(context, {
3459
+ commandContext,
3460
+ taskRef,
3461
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
3462
+ })).description ?? ""}\n${text.trim()}`;
3068
3463
  await runTaskMutation(context, taskRef, {
3069
3464
  action: "UPDATE",
3070
3465
  data: {
3071
- description: `${(await resolveTaskReference(context, {
3072
- commandContext: await resolveCommandContext(context),
3073
- taskRef,
3074
- workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
3075
- })).description ?? ""}\n${text.trim()}`,
3466
+ description: newDesc,
3076
3467
  updatedAt: Date.now()
3077
3468
  },
3078
3469
  successMessage: "Appended",
@@ -3080,15 +3471,17 @@ const runTaskAppend = async (context, taskRef, text, options, workspaceOverride)
3080
3471
  });
3081
3472
  };
3082
3473
  const runTaskPrepend = async (context, taskRef, text, options, workspaceOverride) => {
3474
+ const commandContext = await resolveCommandContext(context);
3083
3475
  const existing = (await resolveTaskReference(context, {
3084
- commandContext: await resolveCommandContext(context),
3476
+ commandContext,
3085
3477
  taskRef,
3086
3478
  workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
3087
3479
  })).description ?? "";
3480
+ const newDesc = existing.length > 0 ? `${text.trim()}\n${existing}` : text.trim();
3088
3481
  await runTaskMutation(context, taskRef, {
3089
3482
  action: "UPDATE",
3090
3483
  data: {
3091
- description: existing.length > 0 ? `${text.trim()}\n${existing}` : text.trim(),
3484
+ description: newDesc,
3092
3485
  updatedAt: Date.now()
3093
3486
  },
3094
3487
  successMessage: "Prepended",
@@ -3104,11 +3497,12 @@ const runTaskRandom = async (context, options, workspaceOverride) => {
3104
3497
  token: commandContext.token,
3105
3498
  workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
3106
3499
  });
3107
- const filtered = filterTasks(await loadTasksByWorkspace(context, {
3500
+ const tasks = await loadTasksByWorkspace(context, {
3108
3501
  baseUrl: commandContext.apiBaseUrl,
3109
3502
  token: commandContext.token,
3110
3503
  workspaceId: workspace.id
3111
- }), {
3504
+ });
3505
+ const filtered = filterTasks(tasks, {
3112
3506
  search: null,
3113
3507
  state: requestedState,
3114
3508
  view: null
@@ -3188,7 +3582,10 @@ const runChecklistList = async (context, taskId, workspaceOption) => {
3188
3582
  return;
3189
3583
  }
3190
3584
  writeInfo(context, `Checklist for: ${task.title}`);
3191
- for (const item of sorted) writeInfo(context, `${item.completedAt === null ? "[ ]" : "[x]"} ${item.id.slice(0, 8)} ${item.title}`);
3585
+ for (const item of sorted) {
3586
+ const marker = item.completedAt === null ? "[ ]" : "[x]";
3587
+ writeInfo(context, `${marker} ${item.id.slice(0, 8)} ${item.title}`);
3588
+ }
3192
3589
  };
3193
3590
  const registerTaskSubcommands = (task, workspaceOverride) => {
3194
3591
  task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or description").option("-v, --view <view>", "Filter by view (inbox|anytime|today|upcoming|someday|logbook|trash)").addHelpText("after", `
@@ -3374,11 +3771,12 @@ const registerChecklistSubcommands = (checklist) => {
3374
3771
  const context = contextFromCommand(command);
3375
3772
  await runWithErrorHandling(context, async () => {
3376
3773
  const commandContext = await resolveCommandContext(context);
3377
- const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
3774
+ const resolvedTask = await resolveTaskReference(context, {
3378
3775
  commandContext,
3379
3776
  taskRef: taskId,
3380
3777
  workspaceRef: options.workspace?.trim() ?? null
3381
- })).id, itemId);
3778
+ });
3779
+ const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
3382
3780
  const now = Date.now();
3383
3781
  await mutateModel(context, {
3384
3782
  baseUrl: commandContext.apiBaseUrl,
@@ -3412,11 +3810,12 @@ const registerChecklistSubcommands = (checklist) => {
3412
3810
  const context = contextFromCommand(command);
3413
3811
  await runWithErrorHandling(context, async () => {
3414
3812
  const commandContext = await resolveCommandContext(context);
3415
- const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
3813
+ const resolvedTask = await resolveTaskReference(context, {
3416
3814
  commandContext,
3417
3815
  taskRef: taskId,
3418
3816
  workspaceRef: options.workspace?.trim() ?? null
3419
- })).id, itemId);
3817
+ });
3818
+ const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
3420
3819
  const now = Date.now();
3421
3820
  await mutateModel(context, {
3422
3821
  baseUrl: commandContext.apiBaseUrl,
@@ -3452,11 +3851,12 @@ const registerChecklistSubcommands = (checklist) => {
3452
3851
  const newTitle = options.title.trim();
3453
3852
  if (newTitle.length === 0) throw new Error("Checklist item title cannot be empty.");
3454
3853
  const commandContext = await resolveCommandContext(context);
3455
- const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
3854
+ const resolvedTask = await resolveTaskReference(context, {
3456
3855
  commandContext,
3457
3856
  taskRef: taskId,
3458
3857
  workspaceRef: options.workspace?.trim() ?? null
3459
- })).id, itemId);
3858
+ });
3859
+ const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
3460
3860
  const now = Date.now();
3461
3861
  await mutateModel(context, {
3462
3862
  baseUrl: commandContext.apiBaseUrl,
@@ -3490,11 +3890,12 @@ const registerChecklistSubcommands = (checklist) => {
3490
3890
  const context = contextFromCommand(command);
3491
3891
  await runWithErrorHandling(context, async () => {
3492
3892
  const commandContext = await resolveCommandContext(context);
3493
- const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
3893
+ const resolvedTask = await resolveTaskReference(context, {
3494
3894
  commandContext,
3495
3895
  taskRef: taskId,
3496
3896
  workspaceRef: options.workspace?.trim() ?? null
3497
- })).id, itemId);
3897
+ });
3898
+ const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
3498
3899
  await mutateModel(context, {
3499
3900
  baseUrl: commandContext.apiBaseUrl,
3500
3901
  request: {
@@ -3549,12 +3950,13 @@ Agent tip: run "donebear spec task add" for full option reference
3549
3950
  });
3550
3951
  });
3551
3952
  registerTaskSubcommands(task, workspaceOverride);
3552
- registerChecklistSubcommands(task.command("checklist").alias("cl").description("Manage task checklist items").argument("<task-number>", "Workspace-scoped task number").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
3953
+ const checklist = task.command("checklist").alias("cl").description("Manage task checklist items").argument("<task-number>", "Workspace-scoped task number").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
3553
3954
  const context = contextFromCommand(command);
3554
3955
  await runWithErrorHandling(context, async () => {
3555
3956
  await runChecklistList(context, taskId, options.workspace);
3556
3957
  });
3557
- }));
3958
+ });
3959
+ registerChecklistSubcommands(checklist);
3558
3960
  };
3559
3961
  //#endregion
3560
3962
  //#region src/commands/comment.ts
@@ -3716,11 +4118,12 @@ respond to a teammate's comment, add your own instead.
3716
4118
  await runWithErrorHandling(context, async () => {
3717
4119
  const body = await resolveCommentBody(options.body);
3718
4120
  const commandContext = await resolveCommandContext(context);
3719
- const existing = await resolveCommentReference(context, commandContext, (await resolveTaskReference(context, {
4121
+ const task = await resolveTaskReference(context, {
3720
4122
  commandContext,
3721
4123
  taskRef,
3722
4124
  workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
3723
- })).id, commentRef);
4125
+ });
4126
+ const existing = await resolveCommentReference(context, commandContext, task.id, commentRef);
3724
4127
  await mutateModel(context, {
3725
4128
  baseUrl: commandContext.apiBaseUrl,
3726
4129
  request: {
@@ -3756,11 +4159,12 @@ workspace owner or admin can remove anyone's.
3756
4159
  const context = contextFromCommand(command);
3757
4160
  await runWithErrorHandling(context, async () => {
3758
4161
  const commandContext = await resolveCommandContext(context);
3759
- const existing = await resolveCommentReference(context, commandContext, (await resolveTaskReference(context, {
4162
+ const task = await resolveTaskReference(context, {
3760
4163
  commandContext,
3761
4164
  taskRef,
3762
4165
  workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
3763
- })).id, commentRef);
4166
+ });
4167
+ const existing = await resolveCommentReference(context, commandContext, task.id, commentRef);
3764
4168
  await mutateModel(context, {
3765
4169
  baseUrl: commandContext.apiBaseUrl,
3766
4170
  request: {
@@ -3918,7 +4322,6 @@ Examples:
3918
4322
  });
3919
4323
  });
3920
4324
  };
3921
- const packageManifest = createRequire(import.meta.url)("../package.json");
3922
4325
  //#endregion
3923
4326
  //#region src/commands/doctor.ts
3924
4327
  const checkCliVersion = async () => {
@@ -3960,13 +4363,14 @@ const maskToken = (token) => {
3960
4363
  };
3961
4364
  const checkAuth = async (context) => {
3962
4365
  try {
3963
- const token = await resolveAuthToken(context, (() => {
4366
+ const config = (() => {
3964
4367
  try {
3965
4368
  return resolveSupabaseConfig();
3966
4369
  } catch {
3967
4370
  return null;
3968
4371
  }
3969
- })());
4372
+ })();
4373
+ const token = await resolveAuthToken(context, config);
3970
4374
  if (!token) return {
3971
4375
  message: "No API key or token found",
3972
4376
  name: "Auth",
@@ -3987,7 +4391,7 @@ const checkAuth = async (context) => {
3987
4391
  };
3988
4392
  const checkWorkspace = async () => {
3989
4393
  try {
3990
- const { ensureLocalContext } = await import("./context-store-ClAx1T2N.mjs");
4394
+ const { ensureLocalContext } = await import("./context-store-BuhPB0I9.mjs");
3991
4395
  const localContext = await ensureLocalContext();
3992
4396
  if (!localContext.workspaceId) return {
3993
4397
  message: "No workspace selected (run `donebear workspace use`)",
@@ -4009,7 +4413,10 @@ const checkWorkspace = async () => {
4009
4413
  };
4010
4414
  const detectClaudeDesktop = (home) => {
4011
4415
  if (process.platform === "darwin") return existsSync(join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"));
4012
- if (process.platform === "win32") return existsSync(join(process.env.APPDATA ?? join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"));
4416
+ if (process.platform === "win32") {
4417
+ const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
4418
+ return existsSync(join(appData, "Claude", "claude_desktop_config.json"));
4419
+ }
4013
4420
  return existsSync(join(home, ".config", "claude", "config.json"));
4014
4421
  };
4015
4422
  const detectAgents = () => {
@@ -4309,6 +4716,10 @@ state and start date rather than stored as a column:
4309
4716
 
4310
4717
  - inbox captured, not yet placed
4311
4718
  - today start date today or earlier
4719
+
4720
+ A private email address (Settings on the web) can also drop a message into
4721
+ today as a task: subject is the title, body is the notes. Slack capture is
4722
+ unchanged.
4312
4723
  - upcoming start date in the future
4313
4724
  - anytime actionable now, no date pressure
4314
4725
  - someday parked, off every working list
@@ -4324,7 +4735,9 @@ completed task's view is logbook, so \`--state done --view today\` matches
4324
4735
  nothing. What was finished today or this week comes off the completion date,
4325
4736
  not the ordering: the list prints it in the \`completed\` column, \`task show
4326
4737
  --json\` reports it as \`completedAtIso\`, and each row from \`task list --json\`
4327
- carries \`completedAt\`.
4738
+ carries \`completedAt\`. The iOS and web apps can keep completed to-dos on Today
4739
+ until a day roll or Log Completed (Settings → Preferences → Logging). The CLI
4740
+ still treats every completed task as logbook.
4328
4741
 
4329
4742
  donebear task add "Renew passport" --when upcoming --deadline 2030-03-01 --json
4330
4743
  donebear task list --view logbook --json
@@ -4382,8 +4795,16 @@ Clear a date with the explicit flag rather than passing an empty value:
4382
4795
  donebear task edit 12 --clear-deadline --json
4383
4796
  donebear project edit launch --clear-target-date --json
4384
4797
 
4385
- Repeating tasks exist in Done Bear but are not editable from the CLI. If a user
4386
- asks to make something repeat, say so rather than creating a run of copies.`,
4798
+ Repeat rules are configured in the app and cannot be edited from the CLI. If a user
4799
+ asks to make something repeat, say so rather than creating a run of copies.
4800
+
4801
+ Upcoming copies can be edited or completed early. Fixed repeats keep their
4802
+ calendar schedule; after-completion repeats count from actual completion.
4803
+ CLI date edits affect the copy only. Use When in the app to choose Make
4804
+ Exception or Update Rule for a fixed series. Custom and count-limited
4805
+ patterns need the Repeat editor.
4806
+ In the app, search for Repeating to see rules and upcoming copies. Select multiple
4807
+ repeating tasks and use More to pause, resume, or stop their series.`,
4387
4808
  title: "Dates and deadlines",
4388
4809
  whenToUse: "Setting or clearing a due date, or choosing between a deadline and a start date."
4389
4810
  },
@@ -4400,12 +4821,18 @@ never have to read a task first just to preserve its other fields.
4400
4821
  - cleared use the paired flag: \`--clear-deadline\`, \`--clear-project\`,
4401
4822
  \`--clear-team\`, \`--clear-description\`.
4402
4823
 
4824
+ The description is markdown. Write a link as a bare URL (\`https://…\`), which
4825
+ every client shows as a link; \`<https://…>\` shows its angle brackets on iOS
4826
+ and here.
4827
+
4403
4828
  Not on \`task edit\`:
4404
4829
 
4405
4830
  - labels \`donebear label add|remove\`
4406
4831
  - completion \`donebear task done|reopen\`
4407
4832
  - archiving \`donebear task archive\` (moves to Trash, not a delete)
4408
4833
 
4834
+ An API key bound to a task agent archives only its own assigned tasks, using the selected revision. A changed task returns a conflict; reload and review it before retrying. Pass \`-w <workspace>\` to select the intended workspace.
4835
+
4409
4836
  Task numbers are positive integers scoped to the workspace. Projects, teams, and
4410
4837
  labels also resolve by key or name.
4411
4838
 
@@ -4425,7 +4852,10 @@ thing that needs care.
4425
4852
  2. Check for truncation. Results carry a count; if you passed \`--limit\`, you
4426
4853
  are looking at a page, not the set.
4427
4854
  3. Show the user the titles, not the ids, and get agreement before writing.
4428
- 4. Then write, one call per task.
4855
+ 4. Then write, one call per task. Archive writes are not atomic: a later
4856
+ conflict leaves earlier tasks in Trash. Inspect Trash and re-read the failed
4857
+ task before retrying. After a timeout, reconcile current task state first;
4858
+ never retry the whole set blindly.
4429
4859
 
4430
4860
  donebear task list --state open --view today --json --jq "[].{number: number, title: title}"
4431
4861
 
@@ -4593,9 +5023,21 @@ off unless the user named a second one:
4593
5023
  Ids and names never cross workspaces. A missing task, project, or label is
4594
5024
  missing from *this* workspace before it is deleted.
4595
5025
 
5026
+ A workspace owner can turn on domain join from Settings → Members. People who
5027
+ later sign in with a verified address on that work domain join as members,
5028
+ without an invite code. Personal email hosts cannot be used. The CLI has no
5029
+ domain-join command; joining by code remains \`donebear workspace join\`.
5030
+
4596
5031
  Auth precedence: \`DONEBEAR_TOKEN\` in the environment, then the stored session
4597
- from \`donebear auth login\`. Set the env var for anything non-interactive; the
4598
- browser login flow will hang in CI.
5032
+ from \`donebear auth login\` or \`donebear auth signup\`. Set the env var for
5033
+ anything non-interactive; the browser login flow will hang in CI.
5034
+
5035
+ New accounts from a coding agent: \`donebear auth signup --email <human-email>
5036
+ --json --no-interactive\`. Do not invent an email. One human step verifies it
5037
+ (link or 6-digit code). The CLI then stores a \`db_\` key with read+write and
5038
+ prints paste-ready MCP config for \`https://mcp.donebear.com/mcp\`. If that
5039
+ email is already registered, run \`donebear auth login\` instead. WorkOS
5040
+ \`agent_auth\` and RFC 8628 device code are not offered.
4599
5041
 
4600
5042
  export DONEBEAR_TOKEN=<token>
4601
5043
  donebear whoami --json
@@ -4605,6 +5047,15 @@ write fails with an explicit message rather than silently doing nothing — tell
4605
5047
  the user they need a write-scoped key rather than trying to work around it.
4606
5048
  \`donebear doctor --json\` reports which of these is actually in effect.
4607
5049
 
5050
+ ## Invites and share links
5051
+
5052
+ \`donebear workspace invite --email user@example.com\` sends a single-use
5053
+ email invite. \`donebear workspace invite --share --copy\` mints a reusable
5054
+ join link at \`https://donebear.com/join?code=\`. \`--project <id>\` on
5055
+ \`--share\` scopes the link: whoever opens it joins the whole workspace as a
5056
+ member, gets that project, and uses a seat. Join with
5057
+ \`donebear workspace join <code>\` or by opening the link while signed in.
5058
+
4608
5059
  ## Task attachments (read-only)
4609
5060
 
4610
5061
  Tasks can carry file/image attachments, capped at 10 MB per file and roughly
@@ -4625,6 +5076,9 @@ Three commands orient you, in order of cost:
4625
5076
  donebear guide <topic> concepts and conventions, no auth
4626
5077
  donebear context --json the live workspace: counts, recent tasks, projects
4627
5078
 
5079
+ No account yet: \`donebear auth signup --email <human-email> --json --no-interactive\`.
5080
+ Do not invent the email. A human verifies it, then the CLI stores a \`db_\` key.
5081
+
4628
5082
  \`donebear spec <command> <subcommand>\` narrows to one command's flags, output
4629
5083
  shape, and examples. Read that before guessing at a flag.
4630
5084
 
@@ -4653,22 +5107,25 @@ workspace from the token.`,
4653
5107
 
4654
5108
  Two importers exist, and they are not the same thing.
4655
5109
 
5110
+ **In the apps**, Settings → Integrations & Import (web) and Settings → Import
5111
+ (iOS: Reminders, Todoist, Things) cover Todoist, TickTick, Microsoft To Do, Any.do, Apple
5112
+ Reminders, Things 3, Trello, Asana, Notion, Google Tasks, and plain CSV, JSON or
5113
+ iCal files. Things 3 is no longer CLI-only: on iPhone, Things → Settings →
5114
+ General → Diagnostics → Enter Code → \`491348\` → Send Things Database, then
5115
+ upload the \`.aar\` under Settings → Import → Things, or \`main.sqlite\` on the web.
5116
+
4656
5117
  **In the terminal**, \`donebear import things\` reads a copy of a Things 3
4657
5118
  SQLite database and writes it into a workspace. It is the only import this CLI
4658
- performs. \`donebear spec import things\` lists its flags.
5119
+ performs. \`donebear spec import things\` lists its flags. It is the power-user
5120
+ path; send people to the apps first.
4659
5121
 
4660
5122
  On macOS it finds that database itself — \`donebear import things --dry-run\`
4661
- with no other flags is the whole command. This is worth volunteering, because
4662
- Things keeps its database inside a folder Finder hides, under a dated directory
4663
- whose name nobody can guess, and people hunting for it in Finder is the single
4664
- most common way this import stalls. \`--database-url\` is for a database
4665
- somewhere unusual: a backup, an external disk, another machine's copy.
5123
+ with no other flags is the whole command. \`--database-url\` is for a database
5124
+ somewhere unusual: an iPhone export, a backup, an external disk, another
5125
+ machine's copy.
4666
5126
 
4667
- **In the web app**, Settings → Integrations & Import covers Todoist, TickTick,
4668
- Microsoft To Do, Any.do, Apple Reminders, Things 3, Trello, Asana, Notion,
4669
- Google Tasks, and plain CSV, JSON or iCal files. If a user asks how to move a
4670
- backlog in from anything other than Things, that is where to send them; there
4671
- is no CLI equivalent to offer.
5127
+ If a user asks how to move a backlog in from anything other than Things, that is
5128
+ the web import screen; there is no CLI equivalent to offer.
4672
5129
 
4673
5130
  Each of those is a file the user exports and uploads — Todoist from Settings →
4674
5131
  Backups, Google Tasks from Google Takeout, Microsoft To Do from a read-only
@@ -4677,6 +5134,13 @@ that may change: a read-only connect flow is being built so Done Bear can make
4677
5134
  that call instead, so treat the Graph export as the current route and check the
4678
5135
  import screen before promising a connection.
4679
5136
 
5137
+ Every importer, here and in the apps, only reads the source; none of them
5138
+ writes, edits or deletes anything in the app being left. The one path that
5139
+ does is Reminders Inbox on iPhone (Settings → Import → Reminders Inbox), which
5140
+ is a capture bridge rather than an import: a chosen Reminders list shows at the
5141
+ top of Inbox, and importing a to-do from there creates the task and removes it
5142
+ from Reminders after a confirmation. It has no CLI equivalent.
5143
+
4680
5144
  The other direction is here though: \`donebear export\` writes a portable JSON
4681
5145
  archive of a workspace, and the web importer restores one.
4682
5146
 
@@ -4709,9 +5173,8 @@ only what is new.
4709
5173
 
4710
5174
  There is no undo for \`donebear import things\`; a run has to be unpicked by
4711
5175
  hand. The web importer does offer one on its completion screen, but the receipt
4712
- lives in the browser that ran the import, so it cannot be reached from the CLI,
4713
- another device or the iOS app. Say that plainly rather than sending someone
4714
- looking for a flag.`,
5176
+ lives in the browser that ran the import. An iOS import is undone with the
5177
+ usual undo for that change, not from the web receipt.`,
4715
5178
  title: "Importing from another app",
4716
5179
  whenToUse: "Moving a backlog in from Things 3 or another task manager, or restoring an export."
4717
5180
  }
@@ -4866,11 +5329,14 @@ const summarizeThingsImportResult = (result) => {
4866
5329
  `source: ${result.counts.source.tasks} tasks, ${result.counts.source.projects} projects, ${result.counts.source.headings} headings, ${result.counts.source.areas} areas, ${result.counts.source.tags} tags, ${result.counts.source.checklistItems} checklist items`,
4867
5330
  `planned: ${result.counts.planned.teams ?? 0} teams, ${result.counts.planned.labels ?? 0} labels, ${result.counts.planned.projects ?? 0} projects, ${result.counts.planned.teamProjects ?? 0} team-project links, ${result.counts.planned.headings ?? 0} headings, ${result.counts.planned.tasks ?? 0} tasks, ${result.counts.planned.taskLabels ?? 0} task-label links, ${result.counts.planned.checklistItems ?? 0} checklist items`
4868
5331
  ];
5332
+ const { leftBehind } = result.counts;
5333
+ if (leftBehind.tasks > 0 || leftBehind.projects > 0) infoLines.push(`left behind (--open-only): ${leftBehind.tasks} to-dos, ${leftBehind.projects} projects`);
4869
5334
  if (result.dryRun) infoLines.push("dry run: no mutations sent");
4870
5335
  else infoLines.push(`mutations: ${result.execution.inserted} inserted, ${result.execution.updated} updated, ${result.execution.skipped} skipped across ${result.execution.batches} batch(es)`);
4871
5336
  return {
4872
5337
  infoLines,
4873
- successMessage: result.dryRun ? "Things 3 import dry run complete" : "Things 3 import complete"
5338
+ successMessage: result.dryRun ? "Things 3 import dry run complete" : "Things 3 import complete",
5339
+ warningLines: result.issues.map((issue) => issue.message)
4874
5340
  };
4875
5341
  };
4876
5342
  //#endregion
@@ -4946,15 +5412,50 @@ const getUtcCalendarDayEndEpoch = (date) => {
4946
5412
  return Date.UTC(parts.year, parts.month - 1, parts.day, 23, 59, 59, 999);
4947
5413
  };
4948
5414
  //#endregion
5415
+ //#region ../../packages/import-core/src/issues.ts
5416
+ /**
5417
+ * Codes are part of the contract between a provider and the completion report:
5418
+ * the UI groups by code, so adding a message variant must not mean adding a
5419
+ * code.
5420
+ */
5421
+ const ImportIssueCode = {
5422
+ /** The user chose to import without turning areas into teams. */
5423
+ AREAS_NOT_IMPORTED: "areas_not_imported",
5424
+ ASSIGNEE_UNRESOLVED: "assignee_unresolved",
5425
+ ATTACHMENT_NOT_TRANSFERRED: "attachment_not_transferred",
5426
+ DUPLICATE_SOURCE_ID: "duplicate_source_id",
5427
+ FILE_TOO_LARGE: "file_too_large",
5428
+ INVALID_DATE: "invalid_date",
5429
+ INVALID_RECORD: "invalid_record",
5430
+ /** Kept out of the plan because the user asked for open tasks only. */
5431
+ LEFT_BEHIND: "left_behind",
5432
+ MISSING_TITLE: "missing_title",
5433
+ PARENT_NOT_FOUND: "parent_not_found",
5434
+ RECURRENCE_APPROXIMATED: "recurrence_approximated",
5435
+ RECURRENCE_UNSUPPORTED: "recurrence_unsupported",
5436
+ TIMEZONE_UNKNOWN: "timezone_unknown",
5437
+ UNKNOWN_FILE_SHAPE: "unknown_file_shape",
5438
+ UNSUPPORTED_FIELD: "unsupported_field"
5439
+ };
5440
+ ImportIssueCode.AREAS_NOT_IMPORTED, ImportIssueCode.LEFT_BEHIND;
5441
+ const createIssue = (code, message, options = {}) => ({
5442
+ code,
5443
+ field: options.field ?? null,
5444
+ message,
5445
+ severity: options.severity ?? "warning",
5446
+ sourceRecordId: options.sourceRecordId ?? null
5447
+ });
5448
+ //#endregion
4949
5449
  //#region ../../packages/import-core/src/things/ids.ts
4950
5450
  const THINGS_IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
4951
5451
  const createDeterministicImportId = (params) => createDeterministicId(THINGS_IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
4952
5452
  //#endregion
4953
5453
  //#region ../../packages/import-core/src/things/labels.ts
4954
5454
  const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
5455
+ const normalizeThingsTeamLookupKey = (value) => value.trim().toLocaleLowerCase();
4955
5456
  //#endregion
4956
5457
  //#region ../../packages/import-core/src/things/values.ts
4957
- var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5458
+ var import_rrule = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
4958
5459
  (function webpackUniversalModuleDefinition(root, factory) {
4959
5460
  if (typeof exports === "object" && typeof module === "object") module.exports = factory();
4960
5461
  else if (typeof define === "function" && define.amd) define([], factory);
@@ -4964,17 +5465,23 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
4964
5465
  return (() => {
4965
5466
  "use strict";
4966
5467
  var __webpack_require__ = {};
4967
- __webpack_require__.d = (exports$1, definition) => {
4968
- for (var key in definition) if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports$1, key)) Object.defineProperty(exports$1, key, {
4969
- enumerable: true,
4970
- get: definition[key]
4971
- });
4972
- };
4973
- __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
4974
- __webpack_require__.r = (exports$2) => {
4975
- if (typeof Symbol !== "undefined" && Symbol.toStringTag) Object.defineProperty(exports$2, Symbol.toStringTag, { value: "Module" });
4976
- Object.defineProperty(exports$2, "__esModule", { value: true });
4977
- };
5468
+ (() => {
5469
+ __webpack_require__.d = (exports$1, definition) => {
5470
+ for (var key in definition) if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports$1, key)) Object.defineProperty(exports$1, key, {
5471
+ enumerable: true,
5472
+ get: definition[key]
5473
+ });
5474
+ };
5475
+ })();
5476
+ (() => {
5477
+ __webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
5478
+ })();
5479
+ (() => {
5480
+ __webpack_require__.r = (exports$2) => {
5481
+ if (typeof Symbol !== "undefined" && Symbol.toStringTag) Object.defineProperty(exports$2, Symbol.toStringTag, { value: "Module" });
5482
+ Object.defineProperty(exports$2, "__esModule", { value: true });
5483
+ };
5484
+ })();
4978
5485
  var __webpack_exports__ = {};
4979
5486
  __webpack_require__.r(__webpack_exports__);
4980
5487
  __webpack_require__.d(__webpack_exports__, {
@@ -5145,7 +5652,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5145
5652
  /**
5146
5653
  * Number of milliseconds of one day
5147
5654
  */
5148
- var ONE_DAY = 1e3 * 60 * 60 * 24;
5655
+ var ONE_DAY = 864e5;
5149
5656
  /**
5150
5657
  * @see: <http://docs.python.org/library/datetime.html#datetime.MAXYEAR>
5151
5658
  */
@@ -5380,9 +5887,6 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5380
5887
  }
5381
5888
  return to.concat(ar || Array.prototype.slice.call(from));
5382
5889
  }
5383
- function __await(v) {
5384
- return this instanceof __await ? (this.v = v, this) : new __await(v);
5385
- }
5386
5890
  const callbackiterresult = function(_super) {
5387
5891
  __extends(CallbackIterResult, _super);
5388
5892
  function CallbackIterResult(method, args, iterator) {
@@ -5596,9 +6100,10 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5596
6100
  ToText.prototype.WEEKLY = function() {
5597
6101
  var gettext = this.gettext;
5598
6102
  if (this.options.interval !== 1) this.add(this.options.interval.toString()).add(this.plural(this.options.interval) ? gettext("weeks") : gettext("week"));
5599
- if (this.byweekday && this.byweekday.isWeekdays) if (this.options.interval === 1) this.add(this.plural(this.options.interval) ? gettext("weekdays") : gettext("weekday"));
5600
- else this.add(gettext("on")).add(gettext("weekdays"));
5601
- else if (this.byweekday && this.byweekday.isEveryDay) this.add(this.plural(this.options.interval) ? gettext("days") : gettext("day"));
6103
+ if (this.byweekday && this.byweekday.isWeekdays) {
6104
+ if (this.options.interval === 1) this.add(this.plural(this.options.interval) ? gettext("weekdays") : gettext("weekday"));
6105
+ else this.add(gettext("on")).add(gettext("weekdays"));
6106
+ } else if (this.byweekday && this.byweekday.isEveryDay) this.add(this.plural(this.options.interval) ? gettext("days") : gettext("day"));
5602
6107
  else {
5603
6108
  if (this.options.interval === 1) this.add(gettext("week"));
5604
6109
  if (this.origOptions.bymonth) {
@@ -5709,8 +6214,10 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5709
6214
  var delimJoin = function(array, delimiter, finalDelimiter) {
5710
6215
  var list = "";
5711
6216
  for (var i = 0; i < array.length; i++) {
5712
- if (i !== 0) if (i === array.length - 1) list += " " + finalDelimiter + " ";
5713
- else list += delimiter + " ";
6217
+ if (i !== 0) {
6218
+ if (i === array.length - 1) list += " " + finalDelimiter + " ";
6219
+ else list += delimiter + " ";
6220
+ }
5714
6221
  list += array[i];
5715
6222
  }
5716
6223
  return list;
@@ -5920,16 +6427,17 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5920
6427
  var nth = decodeNTH();
5921
6428
  var wkd = decodeWKD();
5922
6429
  var m = decodeM();
5923
- if (nth) if (wkd) {
5924
- ttr.nextSymbol();
5925
- if (!options.byweekday) options.byweekday = [];
5926
- options.byweekday.push(RRule[wkd].nth(nth));
5927
- } else {
5928
- if (!options.bymonthday) options.bymonthday = [];
5929
- options.bymonthday.push(nth);
5930
- ttr.accept("day(s)");
5931
- }
5932
- else if (wkd) {
6430
+ if (nth) {
6431
+ if (wkd) {
6432
+ ttr.nextSymbol();
6433
+ if (!options.byweekday) options.byweekday = [];
6434
+ options.byweekday.push(RRule[wkd].nth(nth));
6435
+ } else {
6436
+ if (!options.bymonthday) options.bymonthday = [];
6437
+ options.bymonthday.push(nth);
6438
+ ttr.accept("day(s)");
6439
+ }
6440
+ } else if (wkd) {
5933
6441
  ttr.nextSymbol();
5934
6442
  if (!options.byweekday) options.byweekday = [];
5935
6443
  options.byweekday.push(RRule[wkd]);
@@ -6335,9 +6843,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
6335
6843
  case RRule.MONTHLY:
6336
6844
  opts.bymonthday = opts.dtstart.getUTCDate();
6337
6845
  break;
6338
- case RRule.WEEKLY:
6339
- opts.byweekday = [getWeekday(opts.dtstart)];
6340
- break;
6846
+ case RRule.WEEKLY: opts.byweekday = [getWeekday(opts.dtstart)];
6341
6847
  }
6342
6848
  if (isPresent(opts.bymonth) && !isArray(opts.bymonth)) opts.bymonth = [opts.bymonth];
6343
6849
  if (isPresent(opts.byyearday) && !isArray(opts.byyearday) && isNumber(opts.byyearday)) opts.byyearday = [opts.byyearday];
@@ -6369,14 +6875,15 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
6369
6875
  } else if (isWeekdayStr(opts.byweekday)) {
6370
6876
  opts.byweekday = [Weekday.fromStr(opts.byweekday).weekday];
6371
6877
  opts.bynweekday = null;
6372
- } else if (opts.byweekday instanceof Weekday) if (!opts.byweekday.n || opts.freq > RRule.MONTHLY) {
6373
- opts.byweekday = [opts.byweekday.weekday];
6374
- opts.bynweekday = null;
6878
+ } else if (opts.byweekday instanceof Weekday) {
6879
+ if (!opts.byweekday.n || opts.freq > RRule.MONTHLY) {
6880
+ opts.byweekday = [opts.byweekday.weekday];
6881
+ opts.bynweekday = null;
6882
+ } else {
6883
+ opts.bynweekday = [[opts.byweekday.weekday, opts.byweekday.n]];
6884
+ opts.byweekday = null;
6885
+ }
6375
6886
  } else {
6376
- opts.bynweekday = [[opts.byweekday.weekday, opts.byweekday.n]];
6377
- opts.byweekday = null;
6378
- }
6379
- else {
6380
6887
  var byweekday = [];
6381
6888
  var bynweekday = [];
6382
6889
  for (var i = 0; i < opts.byweekday.length; i++) {
@@ -6795,12 +7302,13 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
6795
7302
  nwdaymask: []
6796
7303
  };
6797
7304
  var ranges = [];
6798
- if (options.freq === RRule.YEARLY) if (empty(options.bymonth)) ranges = [[0, yearlen]];
6799
- else for (var j = 0; j < options.bymonth.length; j++) {
6800
- month = options.bymonth[j];
6801
- ranges.push(mrange.slice(month - 1, month + 1));
6802
- }
6803
- else if (options.freq === RRule.MONTHLY) ranges = [mrange.slice(month - 1, month + 1)];
7305
+ if (options.freq === RRule.YEARLY) {
7306
+ if (empty(options.bymonth)) ranges = [[0, yearlen]];
7307
+ else for (var j = 0; j < options.bymonth.length; j++) {
7308
+ month = options.bymonth[j];
7309
+ ranges.push(mrange.slice(month - 1, month + 1));
7310
+ }
7311
+ } else if (options.freq === RRule.MONTHLY) ranges = [mrange.slice(month - 1, month + 1)];
6804
7312
  if (empty(ranges)) return result;
6805
7313
  result.nwdaymask = repeat(0, yearlen);
6806
7314
  for (var j = 0; j < ranges.length; j++) {
@@ -6839,7 +7347,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
6839
7347
  var month = Math.floor((h + l - 7 * m + 114) / 31);
6840
7348
  var day = (h + l - 7 * m + 114) % 31 + 1;
6841
7349
  var date = Date.UTC(y, month - 1, day + offset);
6842
- return [Math.ceil((date - Date.UTC(y, 0, 1)) / (1e3 * 60 * 60 * 24))];
7350
+ return [Math.ceil((date - Date.UTC(y, 0, 1)) / 864e5)];
6843
7351
  }
6844
7352
  const iterinfo = function() {
6845
7353
  function Iterinfo(options) {
@@ -7708,10 +8216,8 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
7708
8216
  return __webpack_exports__;
7709
8217
  })();
7710
8218
  });
7711
- })))();
8219
+ })))(), 1);
7712
8220
  const PACKED_DATE_MULTIPLIER = 128;
7713
- const PACKED_DATE_DAY_MASK = 31;
7714
- const PACKED_DATE_MONTH_MASK = 15;
7715
8221
  const PACKED_DATE_MONTH_SHIFT = 5;
7716
8222
  const PACKED_DATE_YEAR_SHIFT = 9;
7717
8223
  const THINGS_REMINDER_HOUR_SCALE = 67108864;
@@ -7732,8 +8238,8 @@ const decodeThingsPackedDateParts = (value) => {
7732
8238
  if (integer <= 0 || integer % PACKED_DATE_MULTIPLIER !== 0) return null;
7733
8239
  const packed = integer / PACKED_DATE_MULTIPLIER;
7734
8240
  const parts = {
7735
- day: packed % (PACKED_DATE_DAY_MASK + 1),
7736
- month: Math.trunc(packed / 2 ** PACKED_DATE_MONTH_SHIFT) % (PACKED_DATE_MONTH_MASK + 1),
8241
+ day: packed % 32,
8242
+ month: Math.trunc(packed / 2 ** PACKED_DATE_MONTH_SHIFT) % 16,
7737
8243
  year: Math.trunc(packed / 2 ** PACKED_DATE_YEAR_SHIFT)
7738
8244
  };
7739
8245
  return isValidPackedDate(parts) ? parts : null;
@@ -7769,7 +8275,8 @@ const combineThingsReminderAt = (options) => {
7769
8275
  };
7770
8276
  //#endregion
7771
8277
  //#region ../../packages/import-core/src/things/recurrence.ts
7772
- const THINGS_FREQUENCY_TO_RRULE = new Map([
8278
+ const { RRule, rrulestr } = typeof import_rrule.RRule === "function" ? import_rrule : import_rrule.default;
8279
+ const THINGS_FREQUENCY_TO_RRULE = /* @__PURE__ */ new Map([
7773
8280
  [4, "YEARLY"],
7774
8281
  [8, "MONTHLY"],
7775
8282
  [16, "DAILY"],
@@ -7784,6 +8291,11 @@ const THINGS_WEEKDAY_TO_RRULE = [
7784
8291
  "FR",
7785
8292
  "SA"
7786
8293
  ];
8294
+ /**
8295
+ * Things writes `ed` as Unix seconds at UTC midnight, and uses 4001-01-01 as
8296
+ * "never ends". Anything at or past it is treated as no end date.
8297
+ */
8298
+ const THINGS_NEVER_ENDS_SECONDS = 64092211200;
7787
8299
  const parseNumericPlistField = (xml, key) => {
7788
8300
  const match = new RegExp(`<key>${key}</key>\\s*<(integer|real)>([^<]+)</\\1>`, "u").exec(xml);
7789
8301
  if (!match?.[2]) return null;
@@ -7809,8 +8321,8 @@ const parseOffsets = (xml) => {
7809
8321
  };
7810
8322
  const parseRRule = (rule) => {
7811
8323
  try {
7812
- const parsed = (0, import_rrule.rrulestr)(rule);
7813
- return parsed instanceof import_rrule.RRule ? parsed : null;
8324
+ const parsed = rrulestr(rule);
8325
+ return parsed instanceof RRule ? parsed : null;
7814
8326
  } catch {
7815
8327
  return null;
7816
8328
  }
@@ -7819,7 +8331,8 @@ const buildByDayTokens = (definition) => {
7819
8331
  const tokens = definition.offsets.map((offset) => typeof offset.wd === "number" ? THINGS_WEEKDAY_TO_RRULE[offset.wd] ?? null : null).filter((value) => value !== null);
7820
8332
  if (tokens.length > 0) return tokens;
7821
8333
  if (definition.ia === null) return [];
7822
- return [THINGS_WEEKDAY_TO_RRULE[(/* @__PURE__ */ new Date(definition.ia * 1e3)).getUTCDay()] ?? "MO"];
8334
+ const weekday = (/* @__PURE__ */ new Date(definition.ia * 1e3)).getUTCDay();
8335
+ return [THINGS_WEEKDAY_TO_RRULE[weekday] ?? "MO"];
7823
8336
  };
7824
8337
  const buildMonthDays = (definition) => {
7825
8338
  const monthDays = definition.offsets.map((offset) => typeof offset.dy === "number" ? Math.trunc(offset.dy) + 1 : null).filter((value) => value !== null && value >= 1);
@@ -7833,6 +8346,10 @@ const buildAnchor = (definition, task, timeZone) => {
7833
8346
  if (!timeZone) return `DTSTART:${anchor}`;
7834
8347
  return `DTSTART;TZID=${timeZone}:${anchor}`;
7835
8348
  };
8349
+ /**
8350
+ * Builds the rule without any end. The end is appended separately because a
8351
+ * count has to be resolved against this exact rule first.
8352
+ */
7836
8353
  const buildRepeatRule = (definition, task, timeZone) => {
7837
8354
  const frequency = definition.fu === null ? null : THINGS_FREQUENCY_TO_RRULE.get(definition.fu) ?? null;
7838
8355
  if (!frequency) return null;
@@ -7884,21 +8401,77 @@ const parseThingsRecurrenceDefinition = (xml) => {
7884
8401
  if (definition.ia === null && definition.sr === null && definition.ed === null && definition.rc === null && definition.fu === null && definition.fa === null && definition.tp === null && definition.ts === null && definition.offsets.length === 0) return null;
7885
8402
  return definition;
7886
8403
  };
8404
+ const resolveThingsRepeatEnd = (definition) => {
8405
+ const untilDate = definition.ed !== null && definition.ed < THINGS_NEVER_ENDS_SECONDS ? decodeThingsUnixCalendarDate(definition.ed, null) : null;
8406
+ const count = definition.rc !== null && definition.rc > 0 ? Math.trunc(definition.rc) : null;
8407
+ return untilDate === null && count === null ? null : {
8408
+ count,
8409
+ untilDate
8410
+ };
8411
+ };
8412
+ /**
8413
+ * Date-only epoch of the last occurrence a count allows, counted from the
8414
+ * rule's DTSTART, or null when the rule has no DTSTART to count from.
8415
+ *
8416
+ * COUNT itself is never written: iOS only honours COUNT <= 0, and rrule counts
8417
+ * from DTSTART while the rule anchor is Things' `ia`. Resolving the count here
8418
+ * against that same anchor and storing the result as UNTIL gives every
8419
+ * platform one date to agree on. `instanceCreationCount` is not used: it
8420
+ * counts instances Things has already made from that same anchor, so it does
8421
+ * not change which occurrence is last. It would matter only if `ia` had moved
8422
+ * since the count began, and nothing in the database records that.
8423
+ */
8424
+ const resolveLastOccurrenceDate = (repeatRule, count) => {
8425
+ const parsed = parseRRule(`${repeatRule};COUNT=${count}`);
8426
+ if (!parsed?.origOptions.dtstart) return null;
8427
+ const last = parsed.all().at(-1);
8428
+ return last ? normalizeOccurrenceEpoch(last, getRuleTimeZone(parsed)) : null;
8429
+ };
8430
+ /**
8431
+ * The last day a fixed repeat may land on, from Things' end date and count
8432
+ * together (whichever comes first), or null when neither can be resolved.
8433
+ */
8434
+ const resolveFixedUntilDate = (repeatRule, end) => {
8435
+ const lastByCount = end.count === null ? null : resolveLastOccurrenceDate(repeatRule, end.count);
8436
+ if (end.count !== null && lastByCount === null) return null;
8437
+ const candidates = [end.untilDate, lastByCount].filter((value) => value !== null);
8438
+ return candidates.length > 0 ? Math.min(...candidates) : null;
8439
+ };
8440
+ /**
8441
+ * UNTIL as the end of that calendar day. rrule compares it against the rule's
8442
+ * wall-clock occurrences, so this admits the last day in the rule's own zone,
8443
+ * and iOS reads only the date digits, so both stop on the same day.
8444
+ *
8445
+ * With a TZID the time is local, written without `Z`, because that is how
8446
+ * rrule serializes it and so how manage-api's `canonicalizeRepeatRule` stores
8447
+ * it. Writing `Z` there would have the server rewrite the rule on insert.
8448
+ */
8449
+ const formatUntil = (untilDate, timeZone) => {
8450
+ const day = formatDateOnlyEpoch(untilDate);
8451
+ return day ? `UNTIL=${day.replaceAll("-", "")}T235959${timeZone ? "" : "Z"}` : null;
8452
+ };
7887
8453
  const isThingsRepeatTemplateTask = (task) => task.recurrenceRuleRaw !== null && task.repeatTemplateSourceId === null;
7888
8454
  const isThingsRepeatInstanceTask = (task) => task.repeatTemplateSourceId !== null;
7889
- const resolveThingsRepeatTemplateFields = (task, timeZone) => {
8455
+ const resolveThingsRepeatTemplateFields = (task, timeZone, today) => {
7890
8456
  if (!isThingsRepeatTemplateTask(task)) return null;
7891
8457
  const recurrence = parseThingsRecurrenceDefinition(task.recurrenceRuleRaw);
7892
8458
  if (!recurrence) return null;
7893
8459
  const repeatTimeZone = resolveTimeZoneOrNull(timeZone);
7894
- const repeatRule = buildRepeatRule(recurrence, task, repeatTimeZone);
7895
- if (!repeatRule) return null;
7896
- const currentInstanceDate = decodeThingsPackedCalendarDate(task.nextInstanceStartDate, repeatTimeZone) ?? decodeThingsPackedCalendarDate(task.startDate, repeatTimeZone) ?? decodeThingsUnixCalendarDate(recurrence.ia, repeatTimeZone) ?? decodeThingsUnixSeconds(recurrence.ia);
8460
+ const openRule = buildRepeatRule(recurrence, task, repeatTimeZone);
8461
+ if (!openRule) return null;
7897
8462
  const repeatType = recurrence.tp === 1 ? "after_completion" : "fixed";
8463
+ const end = resolveThingsRepeatEnd(recurrence);
8464
+ const untilDate = end && repeatType === "fixed" ? resolveFixedUntilDate(openRule, end) : null;
8465
+ const until = untilDate === null ? null : formatUntil(untilDate, repeatTimeZone);
8466
+ const repeatRule = until ? `${openRule};${until}` : openRule;
8467
+ const currentInstanceDate = decodeThingsPackedCalendarDate(task.nextInstanceStartDate, repeatTimeZone) ?? decodeThingsPackedCalendarDate(task.startDate, repeatTimeZone) ?? decodeThingsUnixCalendarDate(recurrence.ia, repeatTimeZone) ?? decodeThingsUnixSeconds(recurrence.ia);
8468
+ const ended = untilDate !== null && (untilDate < today || currentInstanceDate !== null && currentInstanceDate > untilDate);
7898
8469
  return {
7899
8470
  currentInstanceDate,
8471
+ droppedEnd: until ? null : end,
8472
+ ended,
7900
8473
  recurrence,
7901
- repeatNextAt: repeatType === "fixed" ? calculateNextOccurrence(repeatRule, currentInstanceDate) : null,
8474
+ repeatNextAt: repeatType === "fixed" && !ended ? calculateNextOccurrence(repeatRule, currentInstanceDate) : null,
7902
8475
  repeatRule,
7903
8476
  repeatStatus: task.instanceCreationPaused || recurrence.ts === 1 ? "paused" : "active",
7904
8477
  repeatType
@@ -8004,11 +8577,15 @@ const buildReminderAt = (task, startDate) => combineThingsReminderAt({
8004
8577
  reminderTime: task.reminderTime,
8005
8578
  startDate
8006
8579
  });
8007
- const pickArchivedAt = (task) => {
8580
+ /**
8581
+ * `fallback` is the import time. A trashed task with no dates of its own must
8582
+ * still get an `archivedAt`, or it lands outside Trash.
8583
+ */
8584
+ const pickArchivedAt = (task, fallback) => {
8008
8585
  const updatedAt = decodeThingsUnixSeconds(task.userModificationDate);
8009
8586
  const completedAt = decodeThingsUnixSeconds(task.stopDate);
8010
8587
  const createdAt = decodeThingsUnixSeconds(task.creationDate);
8011
- if (task.trashed || task.status === 2) return updatedAt ?? completedAt ?? createdAt;
8588
+ if (task.trashed || task.status === 2) return updatedAt ?? completedAt ?? createdAt ?? fallback;
8012
8589
  return null;
8013
8590
  };
8014
8591
  const pickCompletedAt = (task) => {
@@ -8016,14 +8593,14 @@ const pickCompletedAt = (task) => {
8016
8593
  return decodeThingsUnixSeconds(task.stopDate) ?? decodeThingsUnixSeconds(task.userModificationDate) ?? decodeThingsUnixSeconds(task.creationDate);
8017
8594
  };
8018
8595
  const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
8019
- const textDecoder = new TextDecoder();
8596
+ const textDecoder$1 = new TextDecoder();
8020
8597
  const encodeBase64 = (value) => {
8021
8598
  let binary = "";
8022
8599
  for (const byte of value) binary += String.fromCodePoint(byte);
8023
8600
  return btoa(binary);
8024
8601
  };
8025
8602
  const sanitizeBinary = (value) => {
8026
- const asText = textDecoder.decode(value);
8603
+ const asText = textDecoder$1.decode(value);
8027
8604
  if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
8028
8605
  const trimmed = asText.trim();
8029
8606
  if (trimmed.length > 0) return trimmed;
@@ -8161,7 +8738,7 @@ const buildTagTitleMap = (tags) => {
8161
8738
  const buildChecklistMutation = (options) => {
8162
8739
  const sourceId = options.sourceId ?? options.item.uuid;
8163
8740
  const title = normalizeTitle(options.item.title, `Checklist item ${options.item.uuid.slice(0, 8)}`);
8164
- const safeTitle = title.length > MAX_CHECKLIST_TITLE_LENGTH ? `${title.slice(0, MAX_CHECKLIST_TITLE_LENGTH - 3)}...` : title;
8741
+ const safeTitle = title.length > MAX_CHECKLIST_TITLE_LENGTH ? `${title.slice(0, 497)}...` : title;
8165
8742
  return {
8166
8743
  modelId: createDeterministicImportId({
8167
8744
  modelName: "TaskChecklistItem",
@@ -8183,19 +8760,25 @@ const buildChecklistMutation = (options) => {
8183
8760
  };
8184
8761
  const buildTeamActions = (ctx, areas) => {
8185
8762
  const teams = [];
8763
+ const teamMemberships = [];
8764
+ const membershipIds = /* @__PURE__ */ new Set();
8186
8765
  for (const area of [...areas].toSorted((a, b) => {
8187
8766
  const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
8188
8767
  if (indexComparison !== 0) return indexComparison;
8189
8768
  return a.uuid.localeCompare(b.uuid);
8190
8769
  })) {
8191
- const teamId = createDeterministicImportId({
8770
+ const teamName = normalizeTitle(area.title, `Area ${area.uuid.slice(0, 8)}`);
8771
+ const teamLookupKey = normalizeThingsTeamLookupKey(teamName);
8772
+ const existingTeamId = ctx.existingTeamIdByName.get(teamLookupKey);
8773
+ const teamId = existingTeamId ?? createDeterministicImportId({
8192
8774
  modelName: "Team",
8193
8775
  sourceId: area.uuid,
8194
8776
  workspaceId: ctx.workspaceId
8195
8777
  });
8778
+ ctx.existingTeamIdByName.set(teamLookupKey, teamId);
8196
8779
  ctx.areaTeamIdBySourceId.set(area.uuid, teamId);
8197
8780
  const areaTagTitles = (ctx.areaTagMap.get(area.uuid) ?? []).map((link) => ctx.tagTitleById.get(link.tagId) ?? null).filter((title) => typeof title === "string");
8198
- teams.push({
8781
+ if (!existingTeamId) teams.push({
8199
8782
  modelId: teamId,
8200
8783
  modelName: "Team",
8201
8784
  payload: {
@@ -8204,14 +8787,36 @@ const buildTeamActions = (ctx, areas) => {
8204
8787
  description: appendMetadataBlock(null, ctx.metadataMode, buildAreaMetadata(area, areaTagTitles)),
8205
8788
  icon: null,
8206
8789
  key: ctx.teamKeys.get(area.uuid) ?? "AREA",
8207
- name: normalizeTitle(area.title, `Area ${area.uuid.slice(0, 8)}`),
8790
+ name: teamName,
8208
8791
  updatedAt: ctx.createdAtFallback,
8209
8792
  workspaceId: ctx.workspaceId
8210
8793
  },
8211
8794
  sourceId: area.uuid
8212
8795
  });
8796
+ const membershipId = createTeamMembershipId({
8797
+ teamId,
8798
+ userId: ctx.creatorId,
8799
+ workspaceId: ctx.workspaceId
8800
+ });
8801
+ if (!membershipIds.has(membershipId)) {
8802
+ membershipIds.add(membershipId);
8803
+ teamMemberships.push({
8804
+ modelId: membershipId,
8805
+ modelName: "TeamMembership",
8806
+ payload: {
8807
+ createdAt: ctx.createdAtFallback,
8808
+ teamId,
8809
+ userId: ctx.creatorId,
8810
+ workspaceId: ctx.workspaceId
8811
+ },
8812
+ sourceId: `${area.uuid}:${ctx.creatorId}`
8813
+ });
8814
+ }
8213
8815
  }
8214
- return teams;
8816
+ return {
8817
+ teamMemberships,
8818
+ teams
8819
+ };
8215
8820
  };
8216
8821
  const buildLabelActions = (ctx, tags) => {
8217
8822
  const labels = [];
@@ -8244,6 +8849,7 @@ const buildLabelActions = (ctx, tags) => {
8244
8849
  };
8245
8850
  const buildProjectActions = (ctx, sourceProjects) => {
8246
8851
  const projects = [];
8852
+ const projectMemberships = [];
8247
8853
  const teamProjects = [];
8248
8854
  for (const project of [...sourceProjects].toSorted((a, b) => {
8249
8855
  const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
@@ -8262,7 +8868,7 @@ const buildProjectActions = (ctx, sourceProjects) => {
8262
8868
  modelId: projectId,
8263
8869
  modelName: "Project",
8264
8870
  payload: {
8265
- archivedAt: pickArchivedAt(project),
8871
+ archivedAt: pickArchivedAt(project, ctx.createdAtFallback),
8266
8872
  color: null,
8267
8873
  completedAt: pickCompletedAt(project),
8268
8874
  createdAt: decodeThingsUnixSeconds(project.creationDate) ?? ctx.createdAtFallback,
@@ -8279,6 +8885,21 @@ const buildProjectActions = (ctx, sourceProjects) => {
8279
8885
  },
8280
8886
  sourceId: project.uuid
8281
8887
  });
8888
+ projectMemberships.push({
8889
+ modelId: createProjectMembershipId({
8890
+ projectId,
8891
+ userId: ctx.creatorId,
8892
+ workspaceId: ctx.workspaceId
8893
+ }),
8894
+ modelName: "ProjectMembership",
8895
+ payload: {
8896
+ createdAt: decodeThingsUnixSeconds(project.creationDate) ?? ctx.createdAtFallback,
8897
+ projectId,
8898
+ userId: ctx.creatorId,
8899
+ workspaceId: ctx.workspaceId
8900
+ },
8901
+ sourceId: `${project.uuid}:${ctx.creatorId}`
8902
+ });
8282
8903
  if (project.areaId !== null) {
8283
8904
  const teamId = ctx.areaTeamIdBySourceId.get(project.areaId);
8284
8905
  if (teamId) teamProjects.push({
@@ -8299,6 +8920,7 @@ const buildProjectActions = (ctx, sourceProjects) => {
8299
8920
  }
8300
8921
  }
8301
8922
  return {
8923
+ projectMemberships,
8302
8924
  projects,
8303
8925
  teamProjects
8304
8926
  };
@@ -8324,7 +8946,7 @@ const buildHeadingActions = (ctx, sourceHeadings) => {
8324
8946
  modelId: headingId,
8325
8947
  modelName: "ProjectHeading",
8326
8948
  payload: {
8327
- archivedAt: pickArchivedAt(heading),
8949
+ archivedAt: pickArchivedAt(heading, ctx.createdAtFallback),
8328
8950
  createdAt: decodeThingsUnixSeconds(heading.creationDate) ?? ctx.createdAtFallback,
8329
8951
  projectId: parentProjectId,
8330
8952
  sortOrder: sortValue(heading.sortIndex, headings.length),
@@ -8395,7 +9017,7 @@ const pushTaskMutation = (ctx, tasks, taskLabels, checklistItems, mutationOption
8395
9017
  modelId: mutationOptions.taskId,
8396
9018
  modelName: "Task",
8397
9019
  payload: {
8398
- archivedAt: pickArchivedAt(mutationOptions.task),
9020
+ archivedAt: pickArchivedAt(mutationOptions.task, ctx.createdAtFallback),
8399
9021
  assigneeId: null,
8400
9022
  completedAt: pickCompletedAt(mutationOptions.task),
8401
9023
  createdAt,
@@ -8435,8 +9057,22 @@ const pushTaskMutation = (ctx, tasks, taskLabels, checklistItems, mutationOption
8435
9057
  });
8436
9058
  return createdAt;
8437
9059
  };
9060
+ /**
9061
+ * The repeat is imported without its end, so it keeps going. Name the task in
9062
+ * the preview rather than let the user find out when it comes back.
9063
+ */
9064
+ const buildRepeatEndIssue = (task, end, repeatType) => {
9065
+ const title = normalizeTitle(task.title, `Task ${task.uuid.slice(0, 8)}`);
9066
+ const until = formatDateOnlyEpoch(end.untilDate);
9067
+ const thingsEnd = until ? `stops repeating on ${until} in Things` : `repeats ${end.count} ${end.count === 1 ? "time" : "times"} in Things`;
9068
+ const outcome = repeatType === "after_completion" ? "In Done Bear it repeats after each completion with no end, so stop it by hand when you're done." : "That end won't carry over, so it keeps repeating in Done Bear.";
9069
+ return createIssue(ImportIssueCode.RECURRENCE_APPROXIMATED, `"${title}" ${thingsEnd}. ${outcome}`, {
9070
+ field: "repeatRule",
9071
+ sourceRecordId: task.uuid
9072
+ });
9073
+ };
8438
9074
  const planRepeatTemplateTask = (ctx, task, taskId, openRepeatInstanceTemplateIds, tasks, taskLabels, checklistItems) => {
8439
- const repeatTemplate = resolveThingsRepeatTemplateFields(task, ctx.timeZone);
9075
+ const repeatTemplate = resolveThingsRepeatTemplateFields(task, ctx.timeZone, ctx.todayDateOnly);
8440
9076
  if (!repeatTemplate) {
8441
9077
  pushTaskMutation(ctx, tasks, taskLabels, checklistItems, {
8442
9078
  sourceId: task.uuid,
@@ -8445,6 +9081,8 @@ const planRepeatTemplateTask = (ctx, task, taskId, openRepeatInstanceTemplateIds
8445
9081
  });
8446
9082
  return;
8447
9083
  }
9084
+ const archived = pickArchivedAt(task, ctx.createdAtFallback) !== null;
9085
+ if (repeatTemplate.droppedEnd && !archived) ctx.issues.push(buildRepeatEndIssue(task, repeatTemplate.droppedEnd, repeatTemplate.repeatType));
8448
9086
  pushTaskMutation(ctx, tasks, taskLabels, checklistItems, {
8449
9087
  repeat: {
8450
9088
  repeatInstanceDate: null,
@@ -8458,7 +9096,7 @@ const planRepeatTemplateTask = (ctx, task, taskId, openRepeatInstanceTemplateIds
8458
9096
  task,
8459
9097
  taskId
8460
9098
  });
8461
- if (pickCompletedAt(task) !== null || pickArchivedAt(task) !== null || openRepeatInstanceTemplateIds.has(task.uuid)) return;
9099
+ if (pickCompletedAt(task) !== null || archived || repeatTemplate.ended || openRepeatInstanceTemplateIds.has(task.uuid)) return;
8462
9100
  const currentInstanceTaskId = createDeterministicImportId({
8463
9101
  modelName: "Task",
8464
9102
  sourceId: `${task.uuid}:current-instance`,
@@ -8497,7 +9135,7 @@ const buildTaskActions = (ctx, sourceTasks) => {
8497
9135
  if (todayComparison !== 0) return todayComparison;
8498
9136
  return a.uuid.localeCompare(b.uuid);
8499
9137
  });
8500
- const openRepeatInstanceTemplateIds = new Set(sourceTasks.filter(isThingsRepeatInstanceTask).filter((task) => pickCompletedAt(task) === null && pickArchivedAt(task) === null).map((task) => task.repeatTemplateSourceId).filter((value) => typeof value === "string"));
9138
+ const openRepeatInstanceTemplateIds = new Set(sourceTasks.filter(isThingsRepeatInstanceTask).filter((task) => pickCompletedAt(task) === null && pickArchivedAt(task, ctx.createdAtFallback) === null).map((task) => task.repeatTemplateSourceId).filter((value) => typeof value === "string"));
8501
9139
  for (const task of sortedSourceTasks) ctx.taskIdBySourceId.set(task.uuid, createDeterministicImportId({
8502
9140
  modelName: "Task",
8503
9141
  sourceId: task.uuid,
@@ -8540,6 +9178,45 @@ const buildTaskActions = (ctx, sourceTasks) => {
8540
9178
  tasks
8541
9179
  };
8542
9180
  };
9181
+ /**
9182
+ * Still to do in Things: not completed (status 3), not canceled (status 2),
9183
+ * not in the Trash. Canceled and trashed items would arrive in Done Bear's
9184
+ * Trash anyway, and someone asking for open items does not want them.
9185
+ */
9186
+ const isThingsItemOpen = (task) => !task.trashed && task.status !== 2 && task.status !== 3;
9187
+ /**
9188
+ * Decides, once and here, what `openOnly` keeps:
9189
+ *
9190
+ * - To-dos: only open ones. That includes repeat templates (the rule lives on
9191
+ * them) and open repeat instances; completed instances are the Logbook
9192
+ * history of a repeat and are left behind. An open instance whose template
9193
+ * is left behind keeps its date but loses the link, since the template is
9194
+ * not written.
9195
+ * - Projects: an open project always comes; a finished one comes only when an
9196
+ * open to-do still lives in it (directly or under one of its headings), and
9197
+ * keeps its finished state. Its headings follow it.
9198
+ * - Checklist items follow their to-do, ticked ones included: they are part of
9199
+ * an open to-do's state and do not count as tasks.
9200
+ * - Areas and tags are not filtered.
9201
+ */
9202
+ const selectOpenOnly = (sourceProjects, sourceHeadings, sourceTasks) => {
9203
+ const tasks = sourceTasks.filter(isThingsItemOpen);
9204
+ const projectIdByHeadingId = new Map(sourceHeadings.map((heading) => [heading.uuid, heading.projectId]));
9205
+ const projectIdsWithOpenTasks = /* @__PURE__ */ new Set();
9206
+ for (const task of tasks) {
9207
+ const projectId = task.projectId ?? (task.headingId === null ? null : projectIdByHeadingId.get(task.headingId) ?? null);
9208
+ if (projectId !== null) projectIdsWithOpenTasks.add(projectId);
9209
+ }
9210
+ const projects = sourceProjects.filter((project) => isThingsItemOpen(project) || projectIdsWithOpenTasks.has(project.uuid));
9211
+ return {
9212
+ leftBehind: {
9213
+ projects: sourceProjects.length - projects.length,
9214
+ tasks: sourceTasks.length - tasks.length
9215
+ },
9216
+ projects,
9217
+ tasks
9218
+ };
9219
+ };
8543
9220
  const buildThingsImportPlan = (options) => {
8544
9221
  const { snapshot, workspaceId, existingLabelsByTitle } = options;
8545
9222
  const ctx = {
@@ -8551,7 +9228,9 @@ const buildThingsImportPlan = (options) => {
8551
9228
  createdAtFallback: Date.now(),
8552
9229
  creatorId: options.creatorId,
8553
9230
  existingLabelIdByTitle: existingLabelsByTitle ?? /* @__PURE__ */ new Map(),
9231
+ existingTeamIdByName: options.existingTeamsByName ?? /* @__PURE__ */ new Map(),
8554
9232
  headingIdBySourceId: /* @__PURE__ */ new Map(),
9233
+ issues: [],
8555
9234
  metadataMode: options.metadataMode,
8556
9235
  projectIdBySourceId: /* @__PURE__ */ new Map(),
8557
9236
  projectKeyCandidates: buildUniqueKeys(snapshot.tasks.filter((task) => classifyTaskKind(task) === "project"), {
@@ -8570,19 +9249,34 @@ const buildThingsImportPlan = (options) => {
8570
9249
  todayDateOnly: localDateToDateOnlyEpoch(/* @__PURE__ */ new Date()),
8571
9250
  workspaceId
8572
9251
  };
8573
- const teams = options.skipTeams ? [] : buildTeamActions(ctx, snapshot.areas);
9252
+ const { teamMemberships, teams } = options.skipTeams ? {
9253
+ teamMemberships: [],
9254
+ teams: []
9255
+ } : buildTeamActions(ctx, snapshot.areas);
8574
9256
  const labels = buildLabelActions(ctx, snapshot.tags);
8575
9257
  const sourceProjects = snapshot.tasks.filter((task) => classifyTaskKind(task) === "project");
8576
9258
  const sourceHeadings = snapshot.tasks.filter((task) => classifyTaskKind(task) === "heading");
8577
9259
  const sourceTasks = snapshot.tasks.filter((task) => classifyTaskKind(task) === "task");
8578
- const { projects, teamProjects } = buildProjectActions(ctx, sourceProjects);
9260
+ const selected = options.openOnly ? selectOpenOnly(sourceProjects, sourceHeadings, sourceTasks) : {
9261
+ leftBehind: {
9262
+ projects: 0,
9263
+ tasks: 0
9264
+ },
9265
+ projects: sourceProjects,
9266
+ tasks: sourceTasks
9267
+ };
9268
+ const { projectMemberships, projects, teamProjects } = buildProjectActions(ctx, selected.projects);
8579
9269
  const headings = buildHeadingActions(ctx, sourceHeadings);
8580
- const { tasks, taskLabels, checklistItems } = buildTaskActions(ctx, sourceTasks);
9270
+ const { tasks, taskLabels, checklistItems } = buildTaskActions(ctx, selected.tasks);
8581
9271
  const phases = [
8582
9272
  {
8583
9273
  items: teams,
8584
9274
  name: "teams"
8585
9275
  },
9276
+ {
9277
+ items: teamMemberships,
9278
+ name: "teamMemberships"
9279
+ },
8586
9280
  {
8587
9281
  items: labels,
8588
9282
  name: "labels"
@@ -8591,6 +9285,10 @@ const buildThingsImportPlan = (options) => {
8591
9285
  items: projects,
8592
9286
  name: "projects"
8593
9287
  },
9288
+ {
9289
+ items: projectMemberships,
9290
+ name: "projectMemberships"
9291
+ },
8594
9292
  {
8595
9293
  items: teamProjects,
8596
9294
  name: "teamProjects"
@@ -8614,6 +9312,7 @@ const buildThingsImportPlan = (options) => {
8614
9312
  ];
8615
9313
  return {
8616
9314
  counts: {
9315
+ leftBehind: selected.leftBehind,
8617
9316
  planned: Object.fromEntries(phases.map((phase) => [phase.name, phase.items.length])),
8618
9317
  source: {
8619
9318
  areaTagLinks: snapshot.areaTags.length,
@@ -8627,11 +9326,161 @@ const buildThingsImportPlan = (options) => {
8627
9326
  }
8628
9327
  },
8629
9328
  databasePath: snapshot.databasePath,
9329
+ issues: ctx.issues,
8630
9330
  phases,
8631
9331
  taskColumns: snapshot.taskColumns
8632
9332
  };
8633
9333
  };
8634
9334
  //#endregion
9335
+ //#region ../../packages/import-core/src/things/snapshot.ts
9336
+ const textDecoder = new TextDecoder();
9337
+ const THINGS_REQUIRED_TABLE = "TMTask";
9338
+ const THINGS_SQLITE_FILE = "main.sqlite";
9339
+ const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
9340
+ const toThingsStringValue = (value) => {
9341
+ if (typeof value === "string") {
9342
+ const trimmed = value.trim();
9343
+ return trimmed.length > 0 ? trimmed : null;
9344
+ }
9345
+ if (value instanceof Uint8Array) {
9346
+ const text = textDecoder.decode(value).trim();
9347
+ return text.length > 0 ? text : null;
9348
+ }
9349
+ return null;
9350
+ };
9351
+ const toThingsNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
9352
+ const toThingsBooleanValue = (value) => {
9353
+ const numeric = toThingsNumberValue(value);
9354
+ return numeric === null ? null : numeric !== 0;
9355
+ };
9356
+ const normalizeThingsTaskRecord = (row) => {
9357
+ const uuid = toThingsStringValue(row.uuid);
9358
+ if (!uuid) return null;
9359
+ return {
9360
+ afterCompletionReferenceDate: toThingsNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
9361
+ areaId: toThingsStringValue(row.area),
9362
+ contactId: toThingsStringValue(row.contact ?? row.delegate),
9363
+ creationDate: toThingsNumberValue(row.creationDate),
9364
+ deadline: toThingsNumberValue(row.deadline ?? row.dueDate),
9365
+ deadlineOffset: toThingsNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
9366
+ deadlineSuppressionDate: toThingsNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
9367
+ headingId: toThingsStringValue(row.heading ?? row.actionGroup),
9368
+ instanceCreationCount: toThingsNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
9369
+ instanceCreationPaused: toThingsBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
9370
+ instanceCreationStartDate: toThingsNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
9371
+ lastReminderInteractionDate: toThingsNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
9372
+ nextInstanceStartDate: toThingsNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
9373
+ notes: toThingsStringValue(row.notes),
9374
+ projectId: toThingsStringValue(row.project),
9375
+ raw: row,
9376
+ recurrenceRuleRaw: toThingsStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
9377
+ reminderTime: toThingsNumberValue(row.reminderTime),
9378
+ repeatTemplateSourceId: toThingsStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
9379
+ repeaterMigrationDate: toThingsNumberValue(row.repeaterMigrationDate),
9380
+ repeaterRaw: toThingsStringValue(row.repeater),
9381
+ sortIndex: toThingsNumberValue(row.index),
9382
+ start: toThingsNumberValue(row.start),
9383
+ startBucket: toThingsNumberValue(row.startBucket),
9384
+ startDate: toThingsNumberValue(row.startDate),
9385
+ status: toThingsNumberValue(row.status),
9386
+ stopDate: toThingsNumberValue(row.stopDate),
9387
+ title: toThingsStringValue(row.title),
9388
+ todayIndex: toThingsNumberValue(row.todayIndex),
9389
+ todayIndexReferenceDate: toThingsNumberValue(row.todayIndexReferenceDate),
9390
+ trashed: toThingsBooleanValue(row.trashed) ?? false,
9391
+ type: toThingsNumberValue(row.type),
9392
+ userModificationDate: toThingsNumberValue(row.userModificationDate),
9393
+ uuid
9394
+ };
9395
+ };
9396
+ const normalizeThingsAreaRecord = (row) => {
9397
+ const uuid = toThingsStringValue(row.uuid);
9398
+ if (!uuid) return null;
9399
+ return {
9400
+ raw: row,
9401
+ sortIndex: toThingsNumberValue(row.index),
9402
+ title: toThingsStringValue(row.title),
9403
+ uuid,
9404
+ visible: toThingsBooleanValue(row.visible)
9405
+ };
9406
+ };
9407
+ const normalizeThingsTagRecord = (row) => {
9408
+ const uuid = toThingsStringValue(row.uuid);
9409
+ if (!uuid) return null;
9410
+ return {
9411
+ parentId: toThingsStringValue(row.parent),
9412
+ raw: row,
9413
+ shortcut: toThingsStringValue(row.shortcut),
9414
+ sortIndex: toThingsNumberValue(row.index),
9415
+ title: toThingsStringValue(row.title),
9416
+ usedDate: toThingsNumberValue(row.usedDate),
9417
+ uuid
9418
+ };
9419
+ };
9420
+ const normalizeThingsChecklistItemRecord = (row) => {
9421
+ const uuid = toThingsStringValue(row.uuid);
9422
+ if (!uuid) return null;
9423
+ return {
9424
+ creationDate: toThingsNumberValue(row.creationDate),
9425
+ raw: row,
9426
+ sortIndex: toThingsNumberValue(row.index),
9427
+ status: toThingsNumberValue(row.status),
9428
+ stopDate: toThingsNumberValue(row.stopDate),
9429
+ taskId: toThingsStringValue(row.task),
9430
+ title: toThingsStringValue(row.title),
9431
+ userModificationDate: toThingsNumberValue(row.userModificationDate),
9432
+ uuid
9433
+ };
9434
+ };
9435
+ const normalizeThingsTaskTagRecord = (row) => {
9436
+ const taskId = toThingsStringValue(row.tasks);
9437
+ const tagId = toThingsStringValue(row.tags);
9438
+ return taskId && tagId ? {
9439
+ tagId,
9440
+ taskId
9441
+ } : null;
9442
+ };
9443
+ const normalizeThingsAreaTagRecord = (row) => {
9444
+ const areaId = toThingsStringValue(row.areas);
9445
+ const tagId = toThingsStringValue(row.tags);
9446
+ return areaId && tagId ? {
9447
+ areaId,
9448
+ tagId
9449
+ } : null;
9450
+ };
9451
+ const normalizeThingsContactRecord = (row) => {
9452
+ const uuid = toThingsStringValue(row.uuid);
9453
+ if (!uuid) return null;
9454
+ return {
9455
+ appleAddressBookId: toThingsStringValue(row.appleAddressBookId),
9456
+ displayName: toThingsStringValue(row.displayName),
9457
+ emails: toThingsStringValue(row.emails),
9458
+ firstName: toThingsStringValue(row.firstName),
9459
+ lastName: toThingsStringValue(row.lastName),
9460
+ raw: row,
9461
+ uuid
9462
+ };
9463
+ };
9464
+ const mapPresent = (rows, normalize) => rows.map(normalize).filter((record) => record !== null);
9465
+ /**
9466
+ * Turns raw Things tables into the snapshot `buildThingsImportPlan` consumes.
9467
+ * Callers supply the SQL; this is the one place row → record mapping lives.
9468
+ */
9469
+ const buildThingsDatabaseSnapshot = (reader, databasePath) => {
9470
+ if (!reader.hasTable("TMTask")) throw new Error("This does not look like a Things 3 database. Select the main.sqlite file that contains the TMTask table.");
9471
+ return {
9472
+ areaTags: reader.hasTable("TMAreaTag") ? mapPresent(reader.readRows("TMAreaTag"), normalizeThingsAreaTagRecord) : [],
9473
+ areas: reader.hasTable("TMArea") ? mapPresent(reader.readRows("TMArea"), normalizeThingsAreaRecord) : [],
9474
+ checklistItems: reader.hasTable("TMChecklistItem") ? mapPresent(reader.readRows("TMChecklistItem"), normalizeThingsChecklistItemRecord) : [],
9475
+ contacts: reader.hasTable("TMContact") ? mapPresent(reader.readRows("TMContact"), normalizeThingsContactRecord) : [],
9476
+ databasePath,
9477
+ tags: reader.hasTable("TMTag") ? mapPresent(reader.readRows("TMTag"), normalizeThingsTagRecord) : [],
9478
+ taskColumns: reader.getTableColumns(THINGS_REQUIRED_TABLE),
9479
+ taskTags: reader.hasTable("TMTaskTag") ? mapPresent(reader.readRows("TMTaskTag"), normalizeThingsTaskTagRecord) : [],
9480
+ tasks: mapPresent(reader.readRows(THINGS_REQUIRED_TABLE), normalizeThingsTaskRecord)
9481
+ };
9482
+ };
9483
+ //#endregion
8635
9484
  //#region ../../packages/sync-ids/src/meet-done-bear.ts
8636
9485
  /**
8637
9486
  * Ids for the "Meet Done Bear" sample project every new workspace is seeded
@@ -8685,8 +9534,6 @@ const listMeetDoneBearArchiveTargets = (options) => {
8685
9534
  //#region src/import/things-sqlite.ts
8686
9535
  const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac");
8687
9536
  const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
8688
- const THINGS_SQLITE_FILE = "main.sqlite";
8689
- const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
8690
9537
  const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
8691
9538
  const isUrlLike = (value) => URL_SCHEME_PATTERN.test(value);
8692
9539
  const resolveInputPath = (databaseUrl) => {
@@ -8699,7 +9546,7 @@ const resolveInputPath = (databaseUrl) => {
8699
9546
  const expandThingsDatabasePath = (inputPath) => {
8700
9547
  const resolvedPath = resolve(inputPath);
8701
9548
  const lastSegment = basename(resolvedPath);
8702
- if (lastSegment === THINGS_SQLITE_FILE || lastSegment === LEGACY_THINGS_SQLITE_FILE) return resolvedPath;
9549
+ if (lastSegment === "main.sqlite" || lastSegment === "Things.sqlite3") return resolvedPath;
8703
9550
  if (lastSegment === THINGS_BUNDLE_NAME) return join(resolvedPath, THINGS_SQLITE_FILE);
8704
9551
  if (lastSegment.startsWith("ThingsData-")) return join(resolvedPath, THINGS_BUNDLE_NAME, THINGS_SQLITE_FILE);
8705
9552
  return resolvedPath;
@@ -8744,130 +9591,6 @@ const getTableColumns = (database, tableName) => {
8744
9591
  if (!hasTable(database, tableName)) return [];
8745
9592
  return database.prepare(`PRAGMA table_info(${tableName})`).all().map((row) => typeof row.name === "string" ? row.name : null).filter((name) => name !== null);
8746
9593
  };
8747
- const toStringValue = (value) => {
8748
- if (typeof value === "string") {
8749
- const trimmed = value.trim();
8750
- return trimmed.length > 0 ? trimmed : null;
8751
- }
8752
- if (Buffer.isBuffer(value)) {
8753
- const text = value.toString("utf-8").trim();
8754
- return text.length > 0 ? text : null;
8755
- }
8756
- return null;
8757
- };
8758
- const toNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
8759
- const toBooleanValue = (value) => {
8760
- const numeric = toNumberValue(value);
8761
- return numeric === null ? null : numeric !== 0;
8762
- };
8763
- const normalizeTaskRecord = (row) => {
8764
- const uuid = toStringValue(row.uuid);
8765
- if (!uuid) return null;
8766
- return {
8767
- afterCompletionReferenceDate: toNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
8768
- areaId: toStringValue(row.area),
8769
- contactId: toStringValue(row.contact ?? row.delegate),
8770
- creationDate: toNumberValue(row.creationDate),
8771
- deadline: toNumberValue(row.deadline ?? row.dueDate),
8772
- deadlineOffset: toNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
8773
- deadlineSuppressionDate: toNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
8774
- headingId: toStringValue(row.heading ?? row.actionGroup),
8775
- instanceCreationCount: toNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
8776
- instanceCreationPaused: toBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
8777
- instanceCreationStartDate: toNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
8778
- lastReminderInteractionDate: toNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
8779
- nextInstanceStartDate: toNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
8780
- notes: toStringValue(row.notes),
8781
- projectId: toStringValue(row.project),
8782
- raw: row,
8783
- recurrenceRuleRaw: toStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
8784
- reminderTime: toNumberValue(row.reminderTime),
8785
- repeatTemplateSourceId: toStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
8786
- repeaterMigrationDate: toNumberValue(row.repeaterMigrationDate),
8787
- repeaterRaw: toStringValue(row.repeater),
8788
- sortIndex: toNumberValue(row.index),
8789
- start: toNumberValue(row.start),
8790
- startBucket: toNumberValue(row.startBucket),
8791
- startDate: toNumberValue(row.startDate),
8792
- status: toNumberValue(row.status),
8793
- stopDate: toNumberValue(row.stopDate),
8794
- title: toStringValue(row.title),
8795
- todayIndex: toNumberValue(row.todayIndex),
8796
- todayIndexReferenceDate: toNumberValue(row.todayIndexReferenceDate),
8797
- trashed: toBooleanValue(row.trashed) ?? false,
8798
- type: toNumberValue(row.type),
8799
- userModificationDate: toNumberValue(row.userModificationDate),
8800
- uuid
8801
- };
8802
- };
8803
- const normalizeAreaRecord = (row) => {
8804
- const uuid = toStringValue(row.uuid);
8805
- if (!uuid) return null;
8806
- return {
8807
- raw: row,
8808
- sortIndex: toNumberValue(row.index),
8809
- title: toStringValue(row.title),
8810
- uuid,
8811
- visible: toBooleanValue(row.visible)
8812
- };
8813
- };
8814
- const normalizeTagRecord = (row) => {
8815
- const uuid = toStringValue(row.uuid);
8816
- if (!uuid) return null;
8817
- return {
8818
- parentId: toStringValue(row.parent),
8819
- raw: row,
8820
- shortcut: toStringValue(row.shortcut),
8821
- sortIndex: toNumberValue(row.index),
8822
- title: toStringValue(row.title),
8823
- usedDate: toNumberValue(row.usedDate),
8824
- uuid
8825
- };
8826
- };
8827
- const normalizeChecklistItemRecord = (row) => {
8828
- const uuid = toStringValue(row.uuid);
8829
- if (!uuid) return null;
8830
- return {
8831
- creationDate: toNumberValue(row.creationDate),
8832
- raw: row,
8833
- sortIndex: toNumberValue(row.index),
8834
- status: toNumberValue(row.status),
8835
- stopDate: toNumberValue(row.stopDate),
8836
- taskId: toStringValue(row.task),
8837
- title: toStringValue(row.title),
8838
- userModificationDate: toNumberValue(row.userModificationDate),
8839
- uuid
8840
- };
8841
- };
8842
- const normalizeTaskTagRecord = (row) => {
8843
- const taskId = toStringValue(row.tasks);
8844
- const tagId = toStringValue(row.tags);
8845
- return taskId && tagId ? {
8846
- tagId,
8847
- taskId
8848
- } : null;
8849
- };
8850
- const normalizeAreaTagRecord = (row) => {
8851
- const areaId = toStringValue(row.areas);
8852
- const tagId = toStringValue(row.tags);
8853
- return areaId && tagId ? {
8854
- areaId,
8855
- tagId
8856
- } : null;
8857
- };
8858
- const normalizeContactRecord = (row) => {
8859
- const uuid = toStringValue(row.uuid);
8860
- if (!uuid) return null;
8861
- return {
8862
- appleAddressBookId: toStringValue(row.appleAddressBookId),
8863
- displayName: toStringValue(row.displayName),
8864
- emails: toStringValue(row.emails),
8865
- firstName: toStringValue(row.firstName),
8866
- lastName: toStringValue(row.lastName),
8867
- raw: row,
8868
- uuid
8869
- };
8870
- };
8871
9594
  const resolveThingsDatabasePath = (databaseUrl) => expandThingsDatabasePath(resolveInputPath(databaseUrl));
8872
9595
  const resolveDefaultThingsDatabasePath = () => {
8873
9596
  const resolved = findLatestThingsDatabasePath();
@@ -8882,17 +9605,11 @@ const loadThingsDatabase = (databaseUrl) => {
8882
9605
  readonly: true
8883
9606
  });
8884
9607
  try {
8885
- return {
8886
- areaTags: hasTable(database, "TMAreaTag") ? readRows(database, "TMAreaTag").map(normalizeAreaTagRecord).filter((record) => record !== null) : [],
8887
- areas: hasTable(database, "TMArea") ? readRows(database, "TMArea").map(normalizeAreaRecord).filter((record) => record !== null) : [],
8888
- checklistItems: hasTable(database, "TMChecklistItem") ? readRows(database, "TMChecklistItem").map(normalizeChecklistItemRecord).filter((record) => record !== null) : [],
8889
- contacts: hasTable(database, "TMContact") ? readRows(database, "TMContact").map(normalizeContactRecord).filter((record) => record !== null) : [],
8890
- databasePath,
8891
- tags: hasTable(database, "TMTag") ? readRows(database, "TMTag").map(normalizeTagRecord).filter((record) => record !== null) : [],
8892
- taskColumns: getTableColumns(database, "TMTask"),
8893
- taskTags: hasTable(database, "TMTaskTag") ? readRows(database, "TMTaskTag").map(normalizeTaskTagRecord).filter((record) => record !== null) : [],
8894
- tasks: readRows(database, "TMTask").map(normalizeTaskRecord).filter((record) => record !== null)
8895
- };
9608
+ return buildThingsDatabaseSnapshot({
9609
+ getTableColumns: (tableName) => getTableColumns(database, tableName),
9610
+ hasTable: (tableName) => hasTable(database, tableName),
9611
+ readRows: (tableName) => readRows(database, tableName)
9612
+ }, databasePath);
8896
9613
  } finally {
8897
9614
  database.close();
8898
9615
  }
@@ -8900,7 +9617,15 @@ const loadThingsDatabase = (databaseUrl) => {
8900
9617
  //#endregion
8901
9618
  //#region src/import/things-runner.ts
8902
9619
  const WORKSPACE_SCOPED_MODELS = SYNC_CONTENT_MODELS;
8903
- const INSERT_ONLY_MODELS = new Set(["TaskLabel", "TeamProject"]);
9620
+ const INSERT_ONLY_MODELS = /* @__PURE__ */ new Set([
9621
+ "TaskLabel",
9622
+ "TeamProject",
9623
+ "TeamMembership",
9624
+ "ProjectMembership"
9625
+ ]);
9626
+ /** Membership joins are not in SYNC_CONTENT_MODELS (identity surface), but
9627
+ * Things import writes the importer's own rows and needs them for skip-on-rerun. */
9628
+ const THINGS_MEMBERSHIP_MODELS = ["TeamMembership", "ProjectMembership"];
8904
9629
  const chunk = (items, size) => {
8905
9630
  const result = [];
8906
9631
  for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
@@ -8944,9 +9669,9 @@ const loadWorkspaceScopedRowsByModel = async (context, options) => {
8944
9669
  };
8945
9670
  const loadExistingImportState = async (options) => {
8946
9671
  let modelNames;
8947
- if (options.dryRun) modelNames = ["Label"];
8948
- else if (options.skipTeams) modelNames = WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject");
8949
- else modelNames = WORKSPACE_SCOPED_MODELS;
9672
+ if (options.dryRun) modelNames = options.skipTeams ? ["Label"] : ["Label", "Team"];
9673
+ else if (options.skipTeams) modelNames = [...WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject"), "ProjectMembership"];
9674
+ else modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
8950
9675
  const rowsByModel = await loadWorkspaceScopedRowsByModel(options.context, {
8951
9676
  apiBaseUrl: options.commandContext.apiBaseUrl,
8952
9677
  modelNames,
@@ -8955,6 +9680,7 @@ const loadExistingImportState = async (options) => {
8955
9680
  });
8956
9681
  return {
8957
9682
  existingLabelIdsByTitle: buildExistingLabelIdsByTitle(rowsByModel),
9683
+ existingTeamIdsByName: buildExistingTeamIdsByName(rowsByModel),
8958
9684
  rowsByModel
8959
9685
  };
8960
9686
  };
@@ -8972,9 +9698,23 @@ const buildExistingLabelIdsByTitle = (rowsByModel) => {
8972
9698
  }
8973
9699
  return labelsByTitle;
8974
9700
  };
9701
+ const buildExistingTeamIdsByName = (rowsByModel) => {
9702
+ const rows = rowsByModel.get("Team") ?? [];
9703
+ const teamsByName = /* @__PURE__ */ new Map();
9704
+ for (const row of rows) {
9705
+ if (typeof row !== "object" || row === null) continue;
9706
+ const record = row;
9707
+ if (record.__class !== "Team") continue;
9708
+ const id = typeof record.id === "string" ? record.id : null;
9709
+ const name = typeof record.name === "string" ? record.name.trim() : "";
9710
+ if (!id || name.length === 0) continue;
9711
+ teamsByName.set(normalizeThingsTeamLookupKey(name), id);
9712
+ }
9713
+ return teamsByName;
9714
+ };
8975
9715
  const buildExistingIds = (rowsByModel) => {
8976
- const entries = WORKSPACE_SCOPED_MODELS.map((modelName) => [modelName, collectRowIds(rowsByModel.get(modelName) ?? [], modelName)]);
8977
- return new Map(entries);
9716
+ const modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
9717
+ return new Map(modelNames.map((modelName) => [modelName, collectRowIds(rowsByModel.get(modelName) ?? [], modelName)]));
8978
9718
  };
8979
9719
  const buildTransactions = (phaseItems, options) => {
8980
9720
  const transactions = [];
@@ -8992,6 +9732,7 @@ const buildTransactions = (phaseItems, options) => {
8992
9732
  if (action === "INSERT") {
8993
9733
  inserted += 1;
8994
9734
  existingIds.add(item.modelId);
9735
+ options.existingIds.set(item.modelName, existingIds);
8995
9736
  } else updated += 1;
8996
9737
  transactions.push({
8997
9738
  action,
@@ -9009,6 +9750,24 @@ const buildTransactions = (phaseItems, options) => {
9009
9750
  updated
9010
9751
  };
9011
9752
  };
9753
+ const PLAN_LIMIT_PATTERN = /plan includes up to|upgrade to add more/iu;
9754
+ const LIMITED_MODELS = ["Task", "Team"];
9755
+ /**
9756
+ * Turns a batch the server partly refused for the plan limit into one clear
9757
+ * stop: how many tasks and teams landed before it, and what to do next. Any
9758
+ * other refusal is passed through unchanged.
9759
+ */
9760
+ const explainPlanLimitStop = (error, context) => {
9761
+ if (!(error instanceof SyncMutationRejectedError) || !error.results.some((result) => !result.success && PLAN_LIMIT_PATTERN.test(result.error ?? ""))) return error;
9762
+ const landed = new Map(context.landed);
9763
+ const accepted = new Set(error.results.filter((result) => result.success).map((r) => r.clientTxId));
9764
+ for (const tx of context.batch) if (tx.action === "INSERT" && accepted.has(tx.clientTxId)) landed.set(tx.modelName, (landed.get(tx.modelName) ?? 0) + 1);
9765
+ const parts = LIMITED_MODELS.filter((model) => (context.planned.get(model) ?? 0) > 0).map((model) => {
9766
+ const noun = model === "Task" ? "tasks" : "teams";
9767
+ return `${landed.get(model) ?? 0} of ${context.planned.get(model) ?? 0} ${noun}`;
9768
+ });
9769
+ return new CliError(`Import stopped: the workspace reached its plan limit. Imported ${parts.join(" and ")} before it stopped.`, EXIT_CODES.ERROR, "Upgrade the workspace, or run again with --open-only (and --skip-teams if the team limit was hit). A re-run skips what already landed.");
9770
+ };
9012
9771
  const executeImportPlan = async (options, plan, rowsByModel) => {
9013
9772
  const execution = {
9014
9773
  batches: 0,
@@ -9017,25 +9776,37 @@ const executeImportPlan = async (options, plan, rowsByModel) => {
9017
9776
  updated: 0
9018
9777
  };
9019
9778
  const existingIds = buildExistingIds(rowsByModel);
9020
- for (const phase of plan.phases) {
9021
- const prepared = buildTransactions(phase.items, {
9022
- clientId: options.commandContext.localContext.clientId,
9023
- existingIds
9024
- });
9025
- execution.inserted += prepared.inserted;
9026
- execution.updated += prepared.updated;
9027
- execution.skipped += prepared.skipped;
9028
- const transactionBatches = chunk(prepared.transactions, options.batchSize);
9779
+ const prepared = plan.phases.map((phase) => buildTransactions(phase.items, {
9780
+ clientId: options.commandContext.localContext.clientId,
9781
+ existingIds
9782
+ }));
9783
+ const planned = /* @__PURE__ */ new Map();
9784
+ for (const tx of prepared.flatMap((phase) => phase.transactions)) if (tx.action === "INSERT") planned.set(tx.modelName, (planned.get(tx.modelName) ?? 0) + 1);
9785
+ const landed = /* @__PURE__ */ new Map();
9786
+ for (const phasePrepared of prepared) {
9787
+ execution.inserted += phasePrepared.inserted;
9788
+ execution.updated += phasePrepared.updated;
9789
+ execution.skipped += phasePrepared.skipped;
9790
+ const transactionBatches = chunk(phasePrepared.transactions, options.batchSize);
9029
9791
  for (const transactionBatch of transactionBatches) {
9030
9792
  if (transactionBatch.length === 0) continue;
9031
- await mutateModel(options.context, {
9032
- baseUrl: options.commandContext.apiBaseUrl,
9033
- request: {
9034
- batchId: randomUUID(),
9035
- transactions: transactionBatch
9036
- },
9037
- token: options.commandContext.token
9038
- });
9793
+ try {
9794
+ await mutateModel(options.context, {
9795
+ baseUrl: options.commandContext.apiBaseUrl,
9796
+ request: {
9797
+ batchId: randomUUID(),
9798
+ transactions: transactionBatch
9799
+ },
9800
+ token: options.commandContext.token
9801
+ });
9802
+ } catch (error) {
9803
+ throw explainPlanLimitStop(error, {
9804
+ batch: transactionBatch,
9805
+ landed,
9806
+ planned
9807
+ });
9808
+ }
9809
+ for (const tx of transactionBatch) if (tx.action === "INSERT") landed.set(tx.modelName, (landed.get(tx.modelName) ?? 0) + 1);
9039
9810
  execution.batches += 1;
9040
9811
  }
9041
9812
  }
@@ -9077,7 +9848,9 @@ const runThingsImport = async (options) => {
9077
9848
  const plan = buildThingsImportPlan({
9078
9849
  creatorId: options.creatorId,
9079
9850
  existingLabelsByTitle: existingState.existingLabelIdsByTitle,
9851
+ existingTeamsByName: existingState.existingTeamIdsByName,
9080
9852
  metadataMode: options.metadataMode,
9853
+ openOnly: options.openOnly,
9081
9854
  skipTeams: options.skipTeams,
9082
9855
  snapshot,
9083
9856
  timeZone: resolveLocalTimeZone(),
@@ -9093,6 +9866,7 @@ const runThingsImport = async (options) => {
9093
9866
  skipped: 0,
9094
9867
  updated: 0
9095
9868
  },
9869
+ issues: plan.issues,
9096
9870
  taskColumns: plan.taskColumns,
9097
9871
  workspace: options.workspace
9098
9872
  };
@@ -9103,6 +9877,7 @@ const runThingsImport = async (options) => {
9103
9877
  databasePath: plan.databasePath,
9104
9878
  dryRun: false,
9105
9879
  execution,
9880
+ issues: plan.issues,
9106
9881
  taskColumns: plan.taskColumns,
9107
9882
  workspace: options.workspace
9108
9883
  };
@@ -9132,6 +9907,7 @@ const runThingsImportCommand = async (context, options, workspaceOverride) => {
9132
9907
  databaseUrl: options.databaseUrl,
9133
9908
  dryRun: context.dryRun,
9134
9909
  metadataMode,
9910
+ openOnly: options.openOnly,
9135
9911
  skipTeams: options.skipTeams,
9136
9912
  workspace
9137
9913
  });
@@ -9143,9 +9919,10 @@ const runThingsImportCommand = async (context, options, workspaceOverride) => {
9143
9919
  const summary = summarizeThingsImportResult(result);
9144
9920
  writeSuccess(context, summary.successMessage);
9145
9921
  for (const line of summary.infoLines) writeInfo(context, line);
9922
+ for (const line of summary.warningLines) writeWarning(context, line);
9146
9923
  };
9147
9924
  const registerImportCommands = (program, workspaceOverride) => {
9148
- program.command("import").description("Import external data into Done Bear").command("things").description("Import a Things 3 SQLite export").option("--database-url <database-url>", "Path, file:// URL, sqlite:// URL, .thingsdatabase bundle, or ThingsData-* directory").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--skip-teams", "Skip importing Things Areas as Teams and TeamProject links", false).option("--metadata-mode <mode>", "How to preserve Things-only metadata: note|none", "none").option("--batch-size <count>", "Maximum sync mutations per batch", (value) => parsePositiveInteger(value, "Batch size"), 100).addHelpText("after", `
9925
+ program.command("import").description("Import external data into Done Bear").command("things").description("Import a Things 3 SQLite export").option("--database-url <database-url>", "Path, file:// URL, sqlite:// URL, .thingsdatabase bundle, or ThingsData-* directory").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--skip-teams", "Skip importing Things Areas as Teams and TeamProject links", false).option("--open-only", "Import only open to-dos; leave completed, canceled and trashed ones behind", false).option("--metadata-mode <mode>", "How to preserve Things-only metadata: note|none", "none").option("--batch-size <count>", "Maximum sync mutations per batch", (value) => parsePositiveInteger(value, "Batch size"), 100).addHelpText("after", `
9149
9926
  Examples:
9150
9927
  donebear import things --dry-run --json
9151
9928
  donebear import things --database-url ~/Backups/Things\\ Database.thingsdatabase --json
@@ -9153,6 +9930,7 @@ Examples:
9153
9930
 
9154
9931
  Notes:
9155
9932
  --database-url accepts a copied Things 3 export bundle, a direct main.sqlite path, or a ThingsData-* directory.
9933
+ --open-only leaves the Logbook behind. Completed to-dos count toward the Free plan's task limit.
9156
9934
  --metadata-mode note preserves Things-only fields in task/project/team descriptions using a structured appendix.
9157
9935
  `).action(async (options, command) => {
9158
9936
  const context = contextFromCommand(command);
@@ -9225,11 +10003,15 @@ const runLabelList = async (context, options) => {
9225
10003
  }
9226
10004
  writeInfo(context, `${scopeLabel(scope)} (${sorted.length} label(s))`);
9227
10005
  for (const labelRecord of sorted) writeInfo(context, `${labelRecord.id.slice(0, 8)} ${labelRecord.title}`);
9228
- if (context.copy) copyToClipboard(sorted.map((l) => `${l.id.slice(0, 8)} ${l.title}`).join("\n"));
10006
+ if (context.copy) {
10007
+ const text = sorted.map((l) => `${l.id.slice(0, 8)} ${l.title}`).join("\n");
10008
+ copyToClipboard(text);
10009
+ }
9229
10010
  };
9230
10011
  const runLabelShow = async (context, labelRef, options) => {
10012
+ const commandContext = await resolveCommandContext(context);
9231
10013
  const label = await resolveLabel(context, {
9232
- commandContext: await resolveCommandContext(context),
10014
+ commandContext,
9233
10015
  labelRef,
9234
10016
  workspaceRef: normalizeWorkspaceRef(options.workspace)
9235
10017
  });
@@ -9305,6 +10087,30 @@ const runLabelCreate = async (context, title, options) => {
9305
10087
  }
9306
10088
  writeSuccess(context, `Created label: ${title.trim()}`);
9307
10089
  };
10090
+ /**
10091
+ * A TaskLabel row is keyed on task, label and workspace together, so a DELETE
10092
+ * needs `workspaceId` as much as an INSERT does: without it the server cannot
10093
+ * address the row and rejects the mutation.
10094
+ */
10095
+ const buildTaskLabelTransaction = (options) => {
10096
+ const { action, clientId, labelId, taskId, workspaceId } = options;
10097
+ const key = {
10098
+ labelId,
10099
+ taskId,
10100
+ workspaceId
10101
+ };
10102
+ return {
10103
+ action,
10104
+ clientId,
10105
+ clientTxId: randomUUID(),
10106
+ modelId: `${taskId}:${labelId}`,
10107
+ modelName: "TaskLabel",
10108
+ payload: action === "INSERT" ? {
10109
+ ...key,
10110
+ createdAt: Date.now()
10111
+ } : key
10112
+ };
10113
+ };
9308
10114
  const runLabelAdd = async (context, taskRef, labelRef, options) => {
9309
10115
  const commandContext = await resolveCommandContext(context);
9310
10116
  const workspaceRef = normalizeWorkspaceRef(options.workspace);
@@ -9322,19 +10128,13 @@ const runLabelAdd = async (context, taskRef, labelRef, options) => {
9322
10128
  baseUrl: commandContext.apiBaseUrl,
9323
10129
  request: {
9324
10130
  batchId: randomUUID(),
9325
- transactions: [{
10131
+ transactions: [buildTaskLabelTransaction({
9326
10132
  action: "INSERT",
9327
10133
  clientId: commandContext.localContext.clientId,
9328
- clientTxId: randomUUID(),
9329
- modelId: `${task.id}:${label.id}`,
9330
- modelName: "TaskLabel",
9331
- payload: {
9332
- createdAt: Date.now(),
9333
- labelId: label.id,
9334
- taskId: task.id,
9335
- workspaceId: task.workspaceId
9336
- }
9337
- }]
10134
+ labelId: label.id,
10135
+ taskId: task.id,
10136
+ workspaceId: task.workspaceId
10137
+ })]
9338
10138
  },
9339
10139
  token: commandContext.token
9340
10140
  });
@@ -9366,17 +10166,13 @@ const runLabelRemove = async (context, taskRef, labelRef, options) => {
9366
10166
  baseUrl: commandContext.apiBaseUrl,
9367
10167
  request: {
9368
10168
  batchId: randomUUID(),
9369
- transactions: [{
10169
+ transactions: [buildTaskLabelTransaction({
9370
10170
  action: "DELETE",
9371
10171
  clientId: commandContext.localContext.clientId,
9372
- clientTxId: randomUUID(),
9373
- modelId: `${task.id}:${label.id}`,
9374
- modelName: "TaskLabel",
9375
- payload: {
9376
- labelId: label.id,
9377
- taskId: task.id
9378
- }
9379
- }]
10172
+ labelId: label.id,
10173
+ taskId: task.id,
10174
+ workspaceId: task.workspaceId
10175
+ })]
9380
10176
  },
9381
10177
  token: commandContext.token
9382
10178
  });
@@ -9498,7 +10294,7 @@ const runProjectList = async (context, options) => {
9498
10294
  token: commandContext.token,
9499
10295
  workspaceRef: normalizeWorkspaceRef(options.workspace)
9500
10296
  });
9501
- const filtered = filterProjects(sortProjectsByUpdatedDesc(scope.mode === "all" ? await loadProjectsByWorkspaceIds(context, {
10297
+ const allProjects = scope.mode === "all" ? await loadProjectsByWorkspaceIds(context, {
9502
10298
  baseUrl: commandContext.apiBaseUrl,
9503
10299
  token: commandContext.token,
9504
10300
  workspaceIds: scope.workspaces.map((w) => w.id)
@@ -9506,7 +10302,8 @@ const runProjectList = async (context, options) => {
9506
10302
  baseUrl: commandContext.apiBaseUrl,
9507
10303
  token: commandContext.token,
9508
10304
  workspaceId: scope.workspace.id
9509
- })), requestedStatus);
10305
+ });
10306
+ const filtered = filterProjects(sortProjectsByUpdatedDesc(allProjects), requestedStatus);
9510
10307
  if (context.json) {
9511
10308
  writeOutput(context, {
9512
10309
  ...scopeJsonFields(scope),
@@ -9545,11 +10342,15 @@ const runProjectList = async (context, options) => {
9545
10342
  const suffix = target ? ` due ${target}` : "";
9546
10343
  writeInfo(context, `${project.id.slice(0, 8)} [${project.key}] ${project.name} (${status})${suffix}`);
9547
10344
  }
9548
- if (context.copy) copyToClipboard(filtered.map((p) => `${p.id.slice(0, 8)} [${p.key}] ${p.name} (${getProjectStatus(p)})`).join("\n"));
10345
+ if (context.copy) {
10346
+ const text = filtered.map((p) => `${p.id.slice(0, 8)} [${p.key}] ${p.name} (${getProjectStatus(p)})`).join("\n");
10347
+ copyToClipboard(text);
10348
+ }
9549
10349
  };
9550
10350
  const runProjectShow = async (context, projectRef, options) => {
10351
+ const commandContext = await resolveCommandContext(context);
9551
10352
  const { project, workspace } = await resolveProjectReference(context, {
9552
- commandContext: await resolveCommandContext(context),
10353
+ commandContext,
9553
10354
  projectRef,
9554
10355
  workspaceRef: normalizeWorkspaceRef(options.workspace)
9555
10356
  });
@@ -9581,7 +10382,7 @@ const resolveProjectName = async (context, rawName) => {
9581
10382
  const trimmed = rawName.trim();
9582
10383
  if (trimmed.length > 0) return trimmed;
9583
10384
  }
9584
- const { promptForValue } = await import("./prompt-B7mDo4K7.mjs");
10385
+ const { promptForValue } = await import("./prompt-ujp1V6r9.mjs");
9585
10386
  const prompted = await promptForValue(context, "Project name");
9586
10387
  if (prompted) return prompted;
9587
10388
  throw new CliError("Project name is required.", EXIT_CODES.VALIDATION, "Provide a name: donebear project add \"My project\"");
@@ -9826,10 +10627,7 @@ const runProjectMembershipChange = async (context, projectRef, options) => {
9826
10627
  case "already_member":
9827
10628
  writeInfo(context, `Already in project: ${project.name}`);
9828
10629
  break;
9829
- case "not_a_member":
9830
- writeInfo(context, `Not in project: ${project.name}`);
9831
- break;
9832
- default: break;
10630
+ case "not_a_member": writeInfo(context, `Not in project: ${project.name}`);
9833
10631
  }
9834
10632
  };
9835
10633
  const registerProjectCommands = (program, workspaceOverride) => {
@@ -9964,7 +10762,7 @@ const runSearch = async (context, query, options, workspaceOverride) => {
9964
10762
  token: commandContext.token,
9965
10763
  workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
9966
10764
  });
9967
- const filtered = filterTasks(scope.mode === "all" ? await loadTasksByWorkspaceIds(context, {
10765
+ const tasks = scope.mode === "all" ? await loadTasksByWorkspaceIds(context, {
9968
10766
  baseUrl: commandContext.apiBaseUrl,
9969
10767
  token: commandContext.token,
9970
10768
  workspaceIds: scope.workspaces.map((w) => w.id)
@@ -9972,7 +10770,8 @@ const runSearch = async (context, query, options, workspaceOverride) => {
9972
10770
  baseUrl: commandContext.apiBaseUrl,
9973
10771
  token: commandContext.token,
9974
10772
  workspaceId: scope.workspace.id
9975
- }), {
10773
+ });
10774
+ const filtered = filterTasks(tasks, {
9976
10775
  search: trimmedQuery,
9977
10776
  state: requestedState,
9978
10777
  view: null
@@ -10017,7 +10816,7 @@ Examples:
10017
10816
  //#endregion
10018
10817
  //#region src/commands/spec.ts
10019
10818
  const OVERVIEW = {
10020
- authentication: "Set DONEBEAR_TOKEN env var, or run: donebear auth login",
10819
+ authentication: "Set DONEBEAR_TOKEN env var, run: donebear auth login, or create an account with donebear auth signup --email <human-email>",
10021
10820
  commands: {
10022
10821
  "api-key": {
10023
10822
  description: "Manage personal API keys",
@@ -10033,8 +10832,9 @@ const OVERVIEW = {
10033
10832
  quick_example: "donebear auth status --json",
10034
10833
  subcommands: [
10035
10834
  "login",
10036
- "status",
10037
- "logout"
10835
+ "logout",
10836
+ "signup",
10837
+ "status"
10038
10838
  ]
10039
10839
  },
10040
10840
  comment: {
@@ -10264,6 +11064,44 @@ const COMMAND_SPECS = {
10264
11064
  },
10265
11065
  usage: "donebear auth login [options]"
10266
11066
  },
11067
+ "auth signup": {
11068
+ command: "auth signup",
11069
+ examples: ["donebear auth signup --email person@example.com", "donebear auth signup --email person@example.com --json --no-interactive"],
11070
+ json_output: {
11071
+ apiKey: "{ id, key, prefix, scopes }",
11072
+ email: "string",
11073
+ instructions: "{ agent, human }",
11074
+ mcp: "{ url, headers, cursor, claude }",
11075
+ ok: "boolean",
11076
+ sessionStored: "boolean",
11077
+ status: "complete",
11078
+ user: "{ id, email }",
11079
+ verifyUrl: "string",
11080
+ workspace: "{ id, name, urlKey }"
11081
+ },
11082
+ non_interactive: "Requires --email. Do not invent an address. --json and --no-interactive print the verify link on stderr, poll until the human verifies, then emit one complete JSON object on stdout. If the email is already registered, run donebear auth login instead.",
11083
+ notes: "Creates a real human workspace after email verification, then mints a db_ API key with read+write (not admin). WorkOS agent_auth and RFC 8628 device code are not offered.",
11084
+ options: {
11085
+ "--email": {
11086
+ description: "Human email to verify. Required. Do not invent an address.",
11087
+ type: "string"
11088
+ },
11089
+ "--name": {
11090
+ description: "Optional display name",
11091
+ type: "string"
11092
+ },
11093
+ "--no-interactive": {
11094
+ description: "Never prompt for the email code; poll the verify link instead",
11095
+ type: "boolean"
11096
+ },
11097
+ "--timeout": {
11098
+ default: 600,
11099
+ description: "Seconds to wait for email verification",
11100
+ type: "number"
11101
+ }
11102
+ },
11103
+ usage: "donebear auth signup [options]"
11104
+ },
10267
11105
  "auth status": {
10268
11106
  command: "auth status",
10269
11107
  examples: ["donebear auth status --json"],
@@ -10536,6 +11374,10 @@ const COMMAND_SPECS = {
10536
11374
  enum: ["note", "none"],
10537
11375
  type: "string"
10538
11376
  },
11377
+ "--open-only": {
11378
+ description: "Import only open to-dos; completed, canceled and trashed ones are left behind",
11379
+ type: "boolean"
11380
+ },
10539
11381
  "--skip-teams": {
10540
11382
  description: "Skip importing Things Areas as Teams and TeamProject links",
10541
11383
  type: "boolean"
@@ -11120,6 +11962,44 @@ const COMMAND_SPECS = {
11120
11962
  options: {},
11121
11963
  usage: "donebear whoami"
11122
11964
  },
11965
+ "workspace invite": {
11966
+ arguments: { workspace: {
11967
+ description: "Workspace id, urlKey, or name. Defaults to the current workspace.",
11968
+ required: false,
11969
+ type: "string"
11970
+ } },
11971
+ command: "workspace invite",
11972
+ examples: [
11973
+ "donebear workspace invite --email user@example.com --role member",
11974
+ "donebear workspace invite --share --copy",
11975
+ "donebear workspace invite --share --project <project-id>"
11976
+ ],
11977
+ json_output: {
11978
+ invitation: "WorkspaceInvitationRecord",
11979
+ workspace: "WorkspaceRecord"
11980
+ },
11981
+ notes: "Email invites are single-use. --share mints a reusable /join?code= link. --project requires --share.",
11982
+ options: {
11983
+ "--email": {
11984
+ description: "Email address to invite (single-use)",
11985
+ type: "string"
11986
+ },
11987
+ "--project": {
11988
+ description: "Scope a share link so joiners also become members of this project",
11989
+ type: "string"
11990
+ },
11991
+ "--role": {
11992
+ default: "member",
11993
+ description: "Role for an email invite: member|admin",
11994
+ type: "string"
11995
+ },
11996
+ "--share": {
11997
+ description: "Create a reusable join link instead of an email invite",
11998
+ type: "boolean"
11999
+ }
12000
+ },
12001
+ usage: "donebear workspace invite [workspace] [options]"
12002
+ },
11123
12003
  "workspace list": {
11124
12004
  command: "workspace list",
11125
12005
  examples: ["donebear workspace list --json"],
@@ -11162,10 +12042,11 @@ const TOP_LEVEL_SPECS = {
11162
12042
  auth: {
11163
12043
  command: "auth",
11164
12044
  description: "Authentication",
11165
- non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login.",
12045
+ non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login. New accounts: donebear auth signup --email <human-email> --json --no-interactive.",
11166
12046
  subcommands: {
11167
12047
  login: "OAuth login (Google or GitHub)",
11168
12048
  logout: "Sign out and remove stored session",
12049
+ signup: "Create an account from a human email (agent signup)",
11169
12050
  status: "Show auth status"
11170
12051
  },
11171
12052
  tip: "donebear spec auth login: get full spec for auth login"
@@ -11252,7 +12133,7 @@ const TOP_LEVEL_SPECS = {
11252
12133
  create: "Create a new workspace",
11253
12134
  current: "Show the active workspace",
11254
12135
  invitations: "List pending invitations",
11255
- invite: "Invite a user by email",
12136
+ invite: "Invite by email, or mint a reusable --share join link",
11256
12137
  join: "Join a workspace by invite",
11257
12138
  list: "List all workspaces",
11258
12139
  members: "List workspace members",
@@ -11300,6 +12181,7 @@ Examples:
11300
12181
  donebear spec project list Detailed spec for project list
11301
12182
  donebear spec import things Detailed spec for the Things importer
11302
12183
  donebear spec search Detailed spec for search
12184
+ donebear spec auth signup Agent signup from a human email
11303
12185
  donebear spec auth login Auth login options and non-interactive notes
11304
12186
  `).action(async (command, subcommand, _options, cmd) => {
11305
12187
  const context = contextFromCommand(cmd);
@@ -11424,7 +12306,7 @@ const runToday = async (context, options, workspaceOverride) => {
11424
12306
  token: commandContext.token,
11425
12307
  workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
11426
12308
  });
11427
- const todayTasks = filterToday(scope.mode === "all" ? await loadTasksByWorkspaceIds(context, {
12309
+ const tasks = scope.mode === "all" ? await loadTasksByWorkspaceIds(context, {
11428
12310
  baseUrl: commandContext.apiBaseUrl,
11429
12311
  token: commandContext.token,
11430
12312
  workspaceIds: scope.workspaces.map((w) => w.id)
@@ -11432,7 +12314,8 @@ const runToday = async (context, options, workspaceOverride) => {
11432
12314
  baseUrl: commandContext.apiBaseUrl,
11433
12315
  token: commandContext.token,
11434
12316
  workspaceId: scope.workspace.id
11435
- }));
12317
+ });
12318
+ const todayTasks = filterToday(tasks);
11436
12319
  const limited = todayTasks.slice(0, options.limit);
11437
12320
  if (context.json) {
11438
12321
  writeOutput(context, {
@@ -11557,7 +12440,10 @@ const runWorkspaceList = async (context) => {
11557
12440
  return;
11558
12441
  }
11559
12442
  writeInfo(context, "Workspaces:");
11560
- for (const workspace of workspaces) writeInfo(context, `${workspace.id === commandContext.localContext.workspaceId ? "*" : " "} ${workspace.id} ${workspace.name}${workspace.urlKey ? ` (${workspace.urlKey})` : ""} role=${workspace.role}`);
12443
+ for (const workspace of workspaces) {
12444
+ const marker = workspace.id === commandContext.localContext.workspaceId ? "*" : " ";
12445
+ writeInfo(context, `${marker} ${workspace.id} ${workspace.name}${workspace.urlKey ? ` (${workspace.urlKey})` : ""} role=${workspace.role}`);
12446
+ }
11561
12447
  };
11562
12448
  const runWorkspaceCurrent = async (context) => {
11563
12449
  const commandContext = await resolveCommandContext(context);
@@ -11584,10 +12470,11 @@ const runWorkspaceCurrent = async (context) => {
11584
12470
  };
11585
12471
  const runWorkspaceUse = async (context, reference) => {
11586
12472
  const commandContext = await resolveCommandContext(context);
11587
- const selected = findWorkspace(reference, await listWorkspaces(context, {
12473
+ const workspaces = await listWorkspaces(context, {
11588
12474
  baseUrl: commandContext.apiBaseUrl,
11589
12475
  token: commandContext.token
11590
- }));
12476
+ });
12477
+ const selected = findWorkspace(reference, workspaces);
11591
12478
  if (!selected) throw new Error(`Workspace "${reference}" not found. Run \`donebear workspace list\`.`);
11592
12479
  await setCurrentWorkspace(selected.id);
11593
12480
  if (context.json) {
@@ -11758,10 +12645,14 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
11758
12645
  token: commandContext.token,
11759
12646
  workspaceRef: workspaceOverride ?? workspaceRefArg ?? null
11760
12647
  });
12648
+ if (options.share && options.email) throw new CliError("Use --share or --email, not both.", EXIT_CODES.VALIDATION);
12649
+ if (options.project && !options.share) throw new CliError("--project requires --share.", EXIT_CODES.VALIDATION);
11761
12650
  const invitation = await createWorkspaceInvitation(context, {
11762
12651
  baseUrl: commandContext.apiBaseUrl,
11763
12652
  email: options.email,
11764
- role: options.role ?? "member",
12653
+ kind: options.share ? "share" : void 0,
12654
+ projectId: options.project,
12655
+ role: options.share ? "member" : options.role ?? "member",
11765
12656
  token: commandContext.token,
11766
12657
  workspaceId: workspace.id
11767
12658
  });
@@ -11777,7 +12668,7 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
11777
12668
  writeInfo(context, `role: ${invitation.role}`);
11778
12669
  writeInfo(context, `expires: ${invitation.expiresAt}`);
11779
12670
  if (invitation.email) writeInfo(context, `email: ${invitation.email}`);
11780
- const link = `${commandContext.apiBaseUrl}/invite/${invitation.code}`;
12671
+ const link = `${DEFAULT_APP_URL}/join?code=${encodeURIComponent(invitation.code)}`;
11781
12672
  writeInfo(context, `link: ${link}`);
11782
12673
  if (context.copy) {
11783
12674
  if (copyToClipboard(link)) writeInfo(context, "Link copied to clipboard.");
@@ -11793,6 +12684,8 @@ Examples:
11793
12684
  donebear workspace members
11794
12685
  donebear workspace invitations
11795
12686
  donebear workspace invite --email user@example.com --role member
12687
+ donebear workspace invite --share --copy
12688
+ donebear workspace invite --share --project <project-id>
11796
12689
  `).action(async (_options, command) => {
11797
12690
  const context = contextFromCommand(command);
11798
12691
  await runWithErrorHandling(context, async () => {
@@ -11847,7 +12740,7 @@ Examples:
11847
12740
  await runWorkspaceInvitations(context, workspaceRef, workspaceOverride);
11848
12741
  });
11849
12742
  });
11850
- workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
12743
+ workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--share", "Create a reusable join link").option("--project <id>", "Scope a share link: joiners join the whole workspace as a member, get this project, and use a seat").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
11851
12744
  const context = contextFromCommand(command);
11852
12745
  await runWithErrorHandling(context, async () => {
11853
12746
  await runWorkspaceInvite(context, workspaceRef, workspaceOverride, options);
@@ -11875,8 +12768,9 @@ With auth:
11875
12768
  donebear import things --dry-run --json
11876
12769
 
11877
12770
  Auth:
11878
- donebear auth login OAuth browser login
11879
- DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
12771
+ donebear auth signup --email you@… Create an account (human verifies email)
12772
+ donebear auth login OAuth browser login
12773
+ DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
11880
12774
 
11881
12775
  Workspace targeting:
11882
12776
  donebear workspace=myorg task list
@@ -11935,7 +12829,8 @@ const main = async () => {
11935
12829
  const hasUserArgs = processedArgv.length <= 2;
11936
12830
  const isTTY = process.stdin.isTTY === true;
11937
12831
  if (hasUserArgs && isTTY) {
11938
- await runInteractiveMode(contextFromArgv(processedArgv), program);
12832
+ const ttyContext = contextFromArgv(processedArgv);
12833
+ await runInteractiveMode(ttyContext, program);
11939
12834
  return;
11940
12835
  }
11941
12836
  await program.parseAsync(processedArgv);