donebear 0.4.2 → 0.4.4

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 { a as toCliError, d as DEFAULT_APP_URL, f as DEFAULT_OAUTH_CALLBACK_PATH, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, p as DEFAULT_OAUTH_CALLBACK_PORT, r as CliError, s as CLI_NAME, t as ensureLocalContext, x as DONEBEAR_TOKEN_ENV } from "./context-store-BbZMKY0F.mjs";
3
+ import { n as readStdin, t as promptForValue } from "./prompt-B66eG-Zm.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";
@@ -18,7 +18,27 @@ import { createServer } from "node:http";
18
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 ? __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) => {
@@ -1291,7 +1311,7 @@ const WORKSPACE_INVITATION_CREATE_MUTATION = `
1291
1311
  mutation WorkspaceInvitationCreate($input: WorkspaceInvitationCreateInput!) {
1292
1312
  workspaceInvitationCreate(input: $input) {
1293
1313
  success lastSyncId
1294
- invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId }
1314
+ invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId kind projectId }
1295
1315
  }
1296
1316
  }
1297
1317
  `;
@@ -1766,6 +1786,8 @@ const listWorkspaceInvitations = async (context, options) => await paginateGraph
1766
1786
  const createWorkspaceInvitation = async (context, options) => {
1767
1787
  const input = omitUndefined({
1768
1788
  email: options.email,
1789
+ kind: options.kind,
1790
+ projectId: options.projectId,
1769
1791
  role: options.role,
1770
1792
  workspaceId: options.workspaceId
1771
1793
  });
@@ -2143,9 +2165,21 @@ const parsePositiveInteger = (value, label) => {
2143
2165
  };
2144
2166
  //#endregion
2145
2167
  //#region src/oauth-callback.ts
2168
+ const CALLBACK_HTML_HEADERS = {
2169
+ connection: "close",
2170
+ "content-type": "text/html; charset=utf-8"
2171
+ };
2146
2172
  const successHtml = () => `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication complete</h1><p>You can return to your terminal.</p></body></html>`;
2147
2173
  const escapeHtml = (text) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
2148
2174
  const errorHtml = (message) => `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication failed</h1><p>${escapeHtml(message)}</p></body></html>`;
2175
+ const writeCallbackHtml = (request, response, statusCode, html, onFlushed) => {
2176
+ response.shouldKeepAlive = false;
2177
+ response.writeHead(statusCode, CALLBACK_HTML_HEADERS);
2178
+ response.end(html, () => {
2179
+ request.socket.destroy();
2180
+ onFlushed?.();
2181
+ });
2182
+ };
2149
2183
  const waitForOAuthCode = async (options) => await new Promise((resolve, reject) => {
2150
2184
  const host = options.redirectUrl.hostname;
2151
2185
  const port = Number(options.redirectUrl.port);
@@ -2159,6 +2193,7 @@ const waitForOAuthCode = async (options) => await new Promise((resolve, reject)
2159
2193
  if (settled) return;
2160
2194
  settled = true;
2161
2195
  clearTimeout(timeout);
2196
+ server.closeAllConnections();
2162
2197
  server.close(() => {
2163
2198
  if (result.error) {
2164
2199
  reject(result.error);
@@ -2169,41 +2204,40 @@ const waitForOAuthCode = async (options) => await new Promise((resolve, reject)
2169
2204
  };
2170
2205
  const server = createServer((request, response) => {
2171
2206
  if (!request.url) {
2172
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2173
- response.end(errorHtml("Missing request URL"));
2174
- finish({ error: new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR) });
2207
+ writeCallbackHtml(request, response, 400, errorHtml("Missing request URL"), () => {
2208
+ finish({ error: new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR) });
2209
+ });
2175
2210
  return;
2176
2211
  }
2177
2212
  const url = new URL(request.url, options.redirectUrl.origin);
2178
2213
  if (url.pathname !== pathname) {
2179
- response.writeHead(404, { "content-type": "text/html; charset=utf-8" });
2180
- response.end(errorHtml("Invalid callback path"));
2214
+ writeCallbackHtml(request, response, 404, errorHtml("Invalid callback path"));
2181
2215
  return;
2182
2216
  }
2183
2217
  const providerError = url.searchParams.get("error");
2184
2218
  if (providerError) {
2185
2219
  const description = url.searchParams.get("error_description") ?? providerError;
2186
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2187
- response.end(errorHtml(description));
2188
- finish({ error: new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR) });
2220
+ writeCallbackHtml(request, response, 400, errorHtml(description), () => {
2221
+ finish({ error: new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR) });
2222
+ });
2189
2223
  return;
2190
2224
  }
2191
2225
  if (url.searchParams.get("state") !== options.expectedState) {
2192
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2193
- response.end(errorHtml("State verification failed"));
2194
- finish({ error: new CliError("OAuth state verification failed", EXIT_CODES.ERROR) });
2226
+ writeCallbackHtml(request, response, 400, errorHtml("State verification failed"), () => {
2227
+ finish({ error: new CliError("OAuth state verification failed", EXIT_CODES.ERROR) });
2228
+ });
2195
2229
  return;
2196
2230
  }
2197
2231
  const code = url.searchParams.get("code");
2198
2232
  if (!code) {
2199
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2200
- response.end(errorHtml("Authorization code was missing"));
2201
- finish({ error: new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR) });
2233
+ writeCallbackHtml(request, response, 400, errorHtml("Authorization code was missing"), () => {
2234
+ finish({ error: new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR) });
2235
+ });
2202
2236
  return;
2203
2237
  }
2204
- response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2205
- response.end(successHtml());
2206
- finish({ code });
2238
+ writeCallbackHtml(request, response, 200, successHtml(), () => {
2239
+ finish({ code });
2240
+ });
2207
2241
  });
2208
2242
  server.on("error", (error) => {
2209
2243
  finish({ error: new CliError(`Failed to start callback server on ${host}:${port}: ${error.message}`, EXIT_CODES.ERROR) });
@@ -2219,6 +2253,183 @@ const createOAuthState = () => randomBytes(24).toString("hex");
2219
2253
  const createCodeVerifier = () => randomBytes(64).toString("base64url");
2220
2254
  const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
2221
2255
  //#endregion
2256
+ //#region src/api/agent-signup.ts
2257
+ const parseJson = async (response) => {
2258
+ const raw = await response.text();
2259
+ if (raw.length === 0) return null;
2260
+ try {
2261
+ return JSON.parse(raw);
2262
+ } catch {
2263
+ return raw;
2264
+ }
2265
+ };
2266
+ const throwForFailure = (status, payload) => {
2267
+ const message = typeof payload === "string" && payload.length > 0 ? payload : extractErrorMessage(payload);
2268
+ const record = typeof payload === "object" && payload !== null ? payload : {};
2269
+ const code = typeof record.code === "string" ? record.code : null;
2270
+ 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.");
2271
+ if (status === 429 || code === "RATE_LIMIT_EXCEEDED") throw new CliError(message, EXIT_CODES.ERROR, "Wait, then retry.");
2272
+ if (status === 400 || code === "INVALID_REQUEST") throw new CliError(message, EXIT_CODES.VALIDATION);
2273
+ throw new CliError(message, EXIT_CODES.ERROR);
2274
+ };
2275
+ const request = async (endpoint, options) => {
2276
+ const headers = new Headers({ Accept: "application/json" });
2277
+ if (options.token) headers.set("Authorization", `Bearer ${options.token}`);
2278
+ if (options.body !== void 0) headers.set("Content-Type", "application/json");
2279
+ let response;
2280
+ try {
2281
+ response = await fetch(endpoint, {
2282
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
2283
+ headers,
2284
+ method: options.method ?? "GET"
2285
+ });
2286
+ } catch (error) {
2287
+ throw new CliError("Cannot connect to Done Bear API.", EXIT_CODES.NETWORK, error instanceof Error ? error.message : void 0);
2288
+ }
2289
+ const payload = await parseJson(response);
2290
+ if (!response.ok) throwForFailure(response.status, payload);
2291
+ return payload;
2292
+ };
2293
+ const startAgentSignup = async (_context, apiBaseUrl, input) => {
2294
+ return await request(`${apiBaseUrl}/api/agent-signup`, {
2295
+ body: {
2296
+ email: input.email,
2297
+ ...input.name ? { name: input.name } : {}
2298
+ },
2299
+ method: "POST"
2300
+ });
2301
+ };
2302
+ const pollAgentSignup = async (_context, apiBaseUrl, pollToken) => {
2303
+ return await request(`${apiBaseUrl}/api/agent-signup/poll`, {
2304
+ method: "GET",
2305
+ token: pollToken
2306
+ });
2307
+ };
2308
+ const verifyAgentSignupCode = async (_context, apiBaseUrl, input) => {
2309
+ return await request(`${apiBaseUrl}/api/agent-signup/verify`, {
2310
+ body: input,
2311
+ method: "POST"
2312
+ });
2313
+ };
2314
+ //#endregion
2315
+ //#region src/commands/auth-signup.ts
2316
+ const MISSING_EMAIL_MESSAGE = "Pass --email with the human's address. Do not invent one.";
2317
+ const POLL_INTERVAL_MS = 2e3;
2318
+ const defaultClock = {
2319
+ now: () => Date.now(),
2320
+ sleep: (ms, signal) => new Promise((resolve, reject) => {
2321
+ if (signal.aborted) {
2322
+ resolve();
2323
+ return;
2324
+ }
2325
+ const timer = setTimeout(resolve, ms);
2326
+ signal.addEventListener("abort", () => {
2327
+ clearTimeout(timer);
2328
+ reject(/* @__PURE__ */ new Error("aborted"));
2329
+ }, { once: true });
2330
+ })
2331
+ };
2332
+ const isComplete = (value) => value.status === "complete";
2333
+ const printWaiting = (context, verifyUrl, instructions) => {
2334
+ writeWarning(context, instructions.agent);
2335
+ writeWarning(context, instructions.human);
2336
+ writeWarning(context, `Verify link: ${verifyUrl}`);
2337
+ writeWarning(context, "Waiting for the human to verify. The CLI will continue once that happens.");
2338
+ };
2339
+ const sessionFromComplete = (complete) => ({
2340
+ accessToken: complete.apiKey.key,
2341
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2342
+ expiresAt: null,
2343
+ provider: "oauth2",
2344
+ refreshToken: null,
2345
+ tokenType: "bearer",
2346
+ user: {
2347
+ email: complete.user.email,
2348
+ id: complete.user.id
2349
+ }
2350
+ });
2351
+ const printComplete = (context, complete, verifyUrl) => {
2352
+ if (context.json) {
2353
+ writeJson({
2354
+ apiKey: complete.apiKey,
2355
+ email: complete.email,
2356
+ instructions: {
2357
+ agent: "Store the API key as DONEBEAR_TOKEN. Paste mcp.cursor into the MCP config. Do not log the full key.",
2358
+ human: "Verification is done."
2359
+ },
2360
+ mcp: complete.mcp,
2361
+ ok: true,
2362
+ sessionStored: true,
2363
+ status: "complete",
2364
+ user: complete.user,
2365
+ verifyUrl,
2366
+ workspace: complete.workspace
2367
+ });
2368
+ return;
2369
+ }
2370
+ writeSuccess(context, "Signup complete. API key stored in the CLI session.");
2371
+ writeInfo(context, `Signed in as ${complete.user.email}`);
2372
+ writeInfo(context, `Workspace: ${complete.workspace.name} (${complete.workspace.urlKey ?? complete.workspace.id})`);
2373
+ writeInfo(context, "API key (shown once):");
2374
+ writeInfo(context, complete.apiKey.key);
2375
+ writeInfo(context, "Paste-ready MCP config for Cursor:");
2376
+ writeInfo(context, JSON.stringify(complete.mcp.cursor, null, 2));
2377
+ writeInfo(context, complete.mcp.claude);
2378
+ };
2379
+ const runAgentSignup = async (context, options, clock = defaultClock) => {
2380
+ const email = options.email?.trim() ?? "";
2381
+ if (email.length === 0) throw new CliError(MISSING_EMAIL_MESSAGE, EXIT_CODES.VALIDATION, "Example: donebear auth signup --email person@example.com");
2382
+ const apiBaseUrl = resolveApiBaseUrl(context);
2383
+ const started = await startAgentSignup(context, apiBaseUrl, {
2384
+ email,
2385
+ name: options.name?.trim() || void 0
2386
+ });
2387
+ printWaiting(context, started.verifyUrl, started.instructions);
2388
+ const abort = new AbortController();
2389
+ const canPrompt = options.interactive && process.stdin.isTTY && !context.json && !context.quiet;
2390
+ let submittedCode = null;
2391
+ const promptTask = canPrompt ? promptForValue(context, "6-digit email code", abort.signal).then((value) => {
2392
+ submittedCode = value;
2393
+ }) : Promise.resolve();
2394
+ const deadline = clock.now() + options.timeout * 1e3;
2395
+ try {
2396
+ while (clock.now() < deadline) {
2397
+ if (submittedCode) {
2398
+ const verified = await verifyAgentSignupCode(context, apiBaseUrl, {
2399
+ code: submittedCode,
2400
+ pollToken: started.pollToken
2401
+ });
2402
+ if (isComplete(verified)) {
2403
+ abort.abort();
2404
+ await writeStoredAuthSession(sessionFromComplete(verified));
2405
+ printComplete(context, verified, started.verifyUrl);
2406
+ return;
2407
+ }
2408
+ if (verified.status === "consumed") throw new CliError(verified.message, EXIT_CODES.ERROR);
2409
+ submittedCode = null;
2410
+ }
2411
+ const polled = await pollAgentSignup(context, apiBaseUrl, started.pollToken);
2412
+ if (isComplete(polled)) {
2413
+ abort.abort();
2414
+ await writeStoredAuthSession(sessionFromComplete(polled));
2415
+ printComplete(context, polled, started.verifyUrl);
2416
+ return;
2417
+ }
2418
+ if (polled.status === "consumed") throw new CliError(polled.message, EXIT_CODES.ERROR);
2419
+ try {
2420
+ await clock.sleep(POLL_INTERVAL_MS, abort.signal);
2421
+ } catch {
2422
+ break;
2423
+ }
2424
+ }
2425
+ } finally {
2426
+ abort.abort();
2427
+ await promptTask.catch(() => void 0);
2428
+ }
2429
+ 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.");
2430
+ };
2431
+ const parseSignupTimeout = (value) => parsePositiveInteger(value, "Timeout");
2432
+ //#endregion
2222
2433
  //#region src/commands/auth.ts
2223
2434
  const runLogin = async (context, options) => {
2224
2435
  const { authorizeUrl, tokenUrl } = buildOAuthUrls(resolveSupabaseConfig());
@@ -2321,6 +2532,7 @@ const getOptionalSupabaseConfig = () => {
2321
2532
  const registerAuthCommands = (program) => {
2322
2533
  const auth = program.command("auth").description("Authenticate donebear CLI").addHelpText("after", `
2323
2534
  Examples:
2535
+ donebear auth signup --email person@example.com --json
2324
2536
  donebear auth login
2325
2537
  donebear auth login --no-open (print URL, don't open browser)
2326
2538
  donebear auth status --json
@@ -2349,6 +2561,25 @@ Non-interactive / CI use:
2349
2561
  await runLogin(context, options);
2350
2562
  });
2351
2563
  });
2564
+ 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", `
2565
+ Creates a real human account. One person must verify the email (code or link).
2566
+ Do not invent an email address. If the address is already registered, run
2567
+ \`donebear auth login\` instead.
2568
+
2569
+ Examples:
2570
+ donebear auth signup --email person@example.com
2571
+ donebear auth signup --email person@example.com --json --no-interactive
2572
+ `).action(async (options, command) => {
2573
+ const context = contextFromCommand(command);
2574
+ await runWithErrorHandling(context, async () => {
2575
+ await runAgentSignup(context, {
2576
+ email: options.email,
2577
+ interactive: options.interactive !== false,
2578
+ name: options.name,
2579
+ timeout: options.timeout
2580
+ });
2581
+ });
2582
+ });
2352
2583
  auth.command("status").description("Check authentication status").action(async (_options, command) => {
2353
2584
  const context = contextFromCommand(command);
2354
2585
  await runWithErrorHandling(context, async () => {
@@ -2428,23 +2659,32 @@ const toTaskMarker = (task) => {
2428
2659
  if (state === "archived") return "[-]";
2429
2660
  return "[ ]";
2430
2661
  };
2662
+ const completedOn = (task) => {
2663
+ if (task.completedAt === null) return null;
2664
+ const date = new Date(task.completedAt);
2665
+ return Number.isNaN(date.getTime()) ? null : formatDateOnlyEpoch(dateOnlyEpochFromLocalDate(date));
2666
+ };
2667
+ const deadlineDetail = (deadline) => deadline ? `due ${deadline}` : "";
2431
2668
  const taskListHeaders = () => [
2432
2669
  "number",
2433
2670
  "title",
2434
2671
  "state",
2435
- "deadline"
2672
+ "deadline",
2673
+ "completed"
2436
2674
  ];
2437
2675
  const taskListRows = (tasks) => tasks.map((task) => [
2438
2676
  String(task.number),
2439
2677
  task.title,
2440
2678
  getTaskState(task),
2441
- formatDateOnlyEpoch(task.deadlineAt) ?? ""
2679
+ formatDateOnlyEpoch(task.deadlineAt) ?? "",
2680
+ completedOn(task) ?? ""
2442
2681
  ]);
2443
2682
  const taskListLines = (tasks) => tasks.map((task) => {
2444
2683
  const marker = toTaskMarker(task);
2445
2684
  const deadline = formatDateOnlyEpoch(task.deadlineAt);
2446
- const suffix = deadline ? ` due ${deadline}` : "";
2447
- return `${marker} ${task.number} ${task.title}${suffix}`;
2685
+ const completed = completedOn(task);
2686
+ const detail = completed ? `done ${completed}` : deadlineDetail(deadline);
2687
+ return `${marker} ${task.number} ${task.title}${detail ? ` ${detail}` : ""}`;
2448
2688
  });
2449
2689
  //#endregion
2450
2690
  //#region src/confirm.ts
@@ -2466,29 +2706,29 @@ const confirmAction = (context, message) => {
2466
2706
  });
2467
2707
  };
2468
2708
  //#endregion
2469
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/regex.js
2709
+ //#region ../../node_modules/uuid/dist-node/regex.js
2470
2710
  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;
2471
2711
  //#endregion
2472
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/validate.js
2712
+ //#region ../../node_modules/uuid/dist-node/validate.js
2473
2713
  function validate(uuid) {
2474
2714
  return typeof uuid === "string" && regex_default.test(uuid);
2475
2715
  }
2476
2716
  //#endregion
2477
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/parse.js
2717
+ //#region ../../node_modules/uuid/dist-node/parse.js
2478
2718
  function parse(uuid) {
2479
2719
  if (!validate(uuid)) throw TypeError("Invalid UUID");
2480
2720
  let v;
2481
2721
  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);
2482
2722
  }
2483
2723
  //#endregion
2484
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/stringify.js
2724
+ //#region ../../node_modules/uuid/dist-node/stringify.js
2485
2725
  const byteToHex = [];
2486
2726
  for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
2487
2727
  function unsafeStringify(arr, offset = 0) {
2488
2728
  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();
2489
2729
  }
2490
2730
  //#endregion
2491
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/v35.js
2731
+ //#region ../../node_modules/uuid/dist-node/v35.js
2492
2732
  function stringToBytes(str) {
2493
2733
  str = unescape(encodeURIComponent(str));
2494
2734
  const bytes = new Uint8Array(str.length);
@@ -2517,14 +2757,14 @@ function v35(version, hash, value, namespace, buf, offset) {
2517
2757
  return unsafeStringify(bytes);
2518
2758
  }
2519
2759
  //#endregion
2520
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/sha1.js
2760
+ //#region ../../node_modules/uuid/dist-node/sha1.js
2521
2761
  function sha1(bytes) {
2522
2762
  if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
2523
2763
  else if (typeof bytes === "string") bytes = Buffer.from(bytes, "utf8");
2524
2764
  return createHash("sha1").update(bytes).digest();
2525
2765
  }
2526
2766
  //#endregion
2527
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/v5.js
2767
+ //#region ../../node_modules/uuid/dist-node/v5.js
2528
2768
  function v5(value, namespace, buf, offset) {
2529
2769
  return v35(80, sha1, value, namespace, buf, offset);
2530
2770
  }
@@ -2545,6 +2785,11 @@ const createTeamProjectId = (params) => createCompositeSyncId("TeamProject", [
2545
2785
  params.teamId,
2546
2786
  params.projectId
2547
2787
  ]);
2788
+ const createTeamMembershipId = (params) => createCompositeSyncId("TeamMembership", [
2789
+ params.workspaceId,
2790
+ params.teamId,
2791
+ params.userId
2792
+ ]);
2548
2793
  const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMembership", [
2549
2794
  params.workspaceId,
2550
2795
  params.projectId,
@@ -2552,9 +2797,7 @@ const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMemb
2552
2797
  ]);
2553
2798
  //#endregion
2554
2799
  //#region src/import/model-id.ts
2555
- const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
2556
2800
  const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
2557
- const createDeterministicImportId = (params) => createDeterministicId(IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
2558
2801
  const createIdempotentMutationId = (params) => createDeterministicId(IDEMPOTENCY_ID_NAMESPACE, `idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`);
2559
2802
  //#endregion
2560
2803
  //#region src/workspace-context.ts
@@ -2631,19 +2874,35 @@ const resolveWorkspace = async (context, options) => {
2631
2874
  //#endregion
2632
2875
  //#region src/commands/task.ts
2633
2876
  const DEFAULT_TASK_LIST_LIMIT = 20;
2634
- const VALID_TASK_VIEWS = new Set([
2877
+ /** The views `--when` can place a task in: the ones with start fields. */
2878
+ const ACTIVE_TASK_VIEWS = new Set([
2635
2879
  "inbox",
2636
2880
  "anytime",
2637
2881
  "today",
2638
2882
  "upcoming",
2639
- "someday",
2883
+ "someday"
2884
+ ]);
2885
+ /** The views `--view` can filter a list by, which adds the two read-only ones. */
2886
+ const VALID_TASK_VIEWS = new Set([
2887
+ ...ACTIVE_TASK_VIEWS,
2640
2888
  "logbook",
2641
2889
  "trash"
2642
2890
  ]);
2643
2891
  const parseTaskView = (value) => {
2644
2892
  const normalized = value.trim().toLowerCase();
2645
2893
  if (VALID_TASK_VIEWS.has(normalized)) return normalized;
2646
- throw new Error(`Invalid view "${value}". Expected one of: inbox, anytime, today, upcoming, someday.`);
2894
+ throw new Error(`Invalid view "${value}". Expected one of: ${[...VALID_TASK_VIEWS].join(", ")}.`);
2895
+ };
2896
+ /**
2897
+ * The view a `--when` may name. Logbook and Trash are derived from state, not
2898
+ * from a start field, so there are no start fields to build for them: passing
2899
+ * either used to fall through `buildTaskStartFields` and quietly land the task
2900
+ * in Inbox. Completing and archiving have their own commands.
2901
+ */
2902
+ const parseTaskWhenView = (value) => {
2903
+ const view = parseTaskView(value);
2904
+ if (view === "logbook" || view === "trash") throw new Error(`Invalid --when "${value}". Expected one of: ${[...ACTIVE_TASK_VIEWS].join(", ")}. Use "task done" to complete a task and "task archive" to archive one.`);
2905
+ return view;
2647
2906
  };
2648
2907
  const parseDeadline = (value) => parseCliDateOnlyToEpoch(value, "deadline date");
2649
2908
  /**
@@ -2667,7 +2926,7 @@ const parseDeadline = (value) => parseCliDateOnlyToEpoch(value, "deadline date")
2667
2926
  * there would make "typed nothing" indistinguishable from "typed --when today".
2668
2927
  */
2669
2928
  const resolveAddView = (when, deadlineEpoch, hasContainer = false) => {
2670
- if (when) return parseTaskView(when);
2929
+ if (when) return parseTaskWhenView(when);
2671
2930
  if (deadlineEpoch !== null) return "anytime";
2672
2931
  return hasContainer ? "inbox" : "today";
2673
2932
  };
@@ -2990,7 +3249,7 @@ const applyScalarEdits = (updates, options) => {
2990
3249
  const description = options.description.trim();
2991
3250
  updates.description = description.length > 0 ? description : null;
2992
3251
  }
2993
- if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskView(options.when)));
3252
+ if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskWhenView(options.when)));
2994
3253
  if (options.clearDeadline) updates.deadlineAt = null;
2995
3254
  else if (typeof options.deadline === "string") updates.deadlineAt = parseDeadline(options.deadline);
2996
3255
  };
@@ -3155,33 +3414,7 @@ const runChecklistList = async (context, taskId, workspaceOption) => {
3155
3414
  writeInfo(context, `Checklist for: ${task.title}`);
3156
3415
  for (const item of sorted) writeInfo(context, `${item.completedAt === null ? "[ ]" : "[x]"} ${item.id.slice(0, 8)} ${item.title}`);
3157
3416
  };
3158
- const registerTaskCommands = (program, workspaceOverride) => {
3159
- const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
3160
- Examples:
3161
- donebear task list --json
3162
- donebear task add "Fix login bug" --when today --json
3163
- donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
3164
- donebear task edit 12 --deadline 2026-03-05 --json
3165
- donebear task done 12 --json
3166
- donebear task list --state done --limit 50 --json
3167
-
3168
- Date format: YYYY-MM-DD (e.g. 2026-03-15)
3169
- Task number: workspace-scoped positive integer (e.g. 12)
3170
- View buckets: inbox | anytime | today | upcoming | someday
3171
- New tasks default to today; to anytime with --deadline and no --when; to
3172
- inbox with --project or --team and no --when. Pass --when inbox to capture
3173
- without scheduling.
3174
-
3175
- Agent tip: run "donebear spec task add" for full option reference
3176
- `).action(async (_options, command) => {
3177
- const context = contextFromCommand(command);
3178
- await runWithErrorHandling(context, async () => {
3179
- await runTaskList(context, {
3180
- limit: DEFAULT_TASK_LIST_LIMIT,
3181
- state: "open"
3182
- }, workspaceOverride);
3183
- });
3184
- });
3417
+ const registerTaskSubcommands = (task, workspaceOverride) => {
3185
3418
  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", `
3186
3419
  Examples:
3187
3420
  donebear task list --json
@@ -3308,12 +3541,8 @@ without scheduling.
3308
3541
  });
3309
3542
  });
3310
3543
  });
3311
- 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) => {
3312
- const context = contextFromCommand(command);
3313
- await runWithErrorHandling(context, async () => {
3314
- await runChecklistList(context, taskId, options.workspace);
3315
- });
3316
- });
3544
+ };
3545
+ const registerChecklistSubcommands = (checklist) => {
3317
3546
  checklist.command("add").description("Add a checklist item").argument("<task-number>", "Workspace-scoped task number").argument("<title>", "Item title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, title, options, command) => {
3318
3547
  const context = contextFromCommand(command);
3319
3548
  await runWithErrorHandling(context, async () => {
@@ -3516,6 +3745,41 @@ without scheduling.
3516
3745
  });
3517
3746
  });
3518
3747
  };
3748
+ const registerTaskCommands = (program, workspaceOverride) => {
3749
+ const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
3750
+ Examples:
3751
+ donebear task list --json
3752
+ donebear task add "Fix login bug" --when today --json
3753
+ donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
3754
+ donebear task edit 12 --deadline 2026-03-05 --json
3755
+ donebear task done 12 --json
3756
+ donebear task list --state done --limit 50 --json
3757
+
3758
+ Date format: YYYY-MM-DD (e.g. 2026-03-15)
3759
+ Task number: workspace-scoped positive integer (e.g. 12)
3760
+ View buckets: inbox | anytime | today | upcoming | someday
3761
+ New tasks default to today; to anytime with --deadline and no --when; to
3762
+ inbox with --project or --team and no --when. Pass --when inbox to capture
3763
+ without scheduling.
3764
+
3765
+ Agent tip: run "donebear spec task add" for full option reference
3766
+ `).action(async (_options, command) => {
3767
+ const context = contextFromCommand(command);
3768
+ await runWithErrorHandling(context, async () => {
3769
+ await runTaskList(context, {
3770
+ limit: DEFAULT_TASK_LIST_LIMIT,
3771
+ state: "open"
3772
+ }, workspaceOverride);
3773
+ });
3774
+ });
3775
+ registerTaskSubcommands(task, workspaceOverride);
3776
+ 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) => {
3777
+ const context = contextFromCommand(command);
3778
+ await runWithErrorHandling(context, async () => {
3779
+ await runChecklistList(context, taskId, options.workspace);
3780
+ });
3781
+ }));
3782
+ };
3519
3783
  //#endregion
3520
3784
  //#region src/commands/comment.ts
3521
3785
  /**
@@ -3947,7 +4211,7 @@ const checkAuth = async (context) => {
3947
4211
  };
3948
4212
  const checkWorkspace = async () => {
3949
4213
  try {
3950
- const { ensureLocalContext } = await import("./context-store-ClAx1T2N.mjs");
4214
+ const { ensureLocalContext } = await import("./context-store-D8zfa5LS.mjs");
3951
4215
  const localContext = await ensureLocalContext();
3952
4216
  if (!localContext.workspaceId) return {
3953
4217
  message: "No workspace selected (run `donebear workspace use`)",
@@ -4269,17 +4533,31 @@ state and start date rather than stored as a column:
4269
4533
 
4270
4534
  - inbox captured, not yet placed
4271
4535
  - today start date today or earlier
4536
+
4537
+ A private email address (Settings on the web) can also drop a message into
4538
+ today as a task: subject is the title, body is the notes. Slack capture is
4539
+ unchanged.
4272
4540
  - upcoming start date in the future
4273
4541
  - anytime actionable now, no date pressure
4274
4542
  - someday parked, off every working list
4275
4543
  - logbook completed
4276
4544
  - trash archived
4277
4545
 
4278
- \`--when\` sets the bucket on create and edit; \`--state open|done|archived|all\`
4279
- reaches the Logbook and Trash on list.
4546
+ \`--when\` sets the bucket on create and edit, and takes the first five only:
4547
+ logbook and trash come from a task's state, so \`task done\` and \`task archive\`
4548
+ put a task there, not \`--when\`. On list, \`--state open|done|archived|all\`
4549
+ reaches the Logbook and Trash, and so does \`--view logbook\` or \`--view trash\`,
4550
+ which widen the state for you. Do not pair \`--state done\` with a bucket view: a
4551
+ completed task's view is logbook, so \`--state done --view today\` matches
4552
+ nothing. What was finished today or this week comes off the completion date,
4553
+ not the ordering: the list prints it in the \`completed\` column, \`task show
4554
+ --json\` reports it as \`completedAtIso\`, and each row from \`task list --json\`
4555
+ carries \`completedAt\`. The iOS and web apps can keep completed to-dos on Today
4556
+ until a day roll or Log Completed (Settings → Preferences → Logging). The CLI
4557
+ still treats every completed task as logbook.
4280
4558
 
4281
4559
  donebear task add "Renew passport" --when upcoming --deadline 2030-03-01 --json
4282
- donebear task list --state done --json
4560
+ donebear task list --view logbook --json
4283
4561
 
4284
4562
  \`donebear task add\` defaults to today, so a task created without \`--when\`
4285
4563
  starts today and shows up on the user's working list straight away. That is the
@@ -4334,8 +4612,16 @@ Clear a date with the explicit flag rather than passing an empty value:
4334
4612
  donebear task edit 12 --clear-deadline --json
4335
4613
  donebear project edit launch --clear-target-date --json
4336
4614
 
4337
- Repeating tasks exist in Done Bear but are not editable from the CLI. If a user
4338
- asks to make something repeat, say so rather than creating a run of copies.`,
4615
+ Repeat rules are configured in the app and cannot be edited from the CLI. If a user
4616
+ asks to make something repeat, say so rather than creating a run of copies.
4617
+
4618
+ Upcoming copies can be edited or completed early. Fixed repeats keep their
4619
+ calendar schedule; after-completion repeats count from actual completion.
4620
+ CLI date edits affect the copy only. Use When in the app to choose Make
4621
+ Exception or Update Rule for a fixed series. Custom and count-limited
4622
+ patterns need the Repeat editor.
4623
+ In the app, search for Repeating to see rules and upcoming copies. Select multiple
4624
+ repeating tasks and use More to pause, resume, or stop their series.`,
4339
4625
  title: "Dates and deadlines",
4340
4626
  whenToUse: "Setting or clearing a due date, or choosing between a deadline and a start date."
4341
4627
  },
@@ -4352,6 +4638,10 @@ never have to read a task first just to preserve its other fields.
4352
4638
  - cleared use the paired flag: \`--clear-deadline\`, \`--clear-project\`,
4353
4639
  \`--clear-team\`, \`--clear-description\`.
4354
4640
 
4641
+ The description is markdown. Write a link as a bare URL (\`https://…\`), which
4642
+ every client shows as a link; \`<https://…>\` shows its angle brackets on iOS
4643
+ and here.
4644
+
4355
4645
  Not on \`task edit\`:
4356
4646
 
4357
4647
  - labels \`donebear label add|remove\`
@@ -4496,10 +4786,16 @@ a slightly wrong fit. Check \`donebear label list --json\` first and prefer an
4496
4786
  existing label when it is a reasonable match. Teams are set up in the app;
4497
4787
  there is no team create command.
4498
4788
 
4499
- Every workspace member sees every project; \`donebear project join\` and
4500
- \`donebear project leave\` only change which ones sit in your sidebar's Your
4501
- projects list. Neither grants nor removes access, both are idempotent, and
4502
- creating a project already joins you to it.`,
4789
+ A project is either visible to the whole workspace (the default) or private
4790
+ to its members. A private project and its tasks are invisible to everyone
4791
+ else — absent from \`project list\`, \`task list\` and \`search\`, not merely
4792
+ hidden. Visibility is set in the app.
4793
+
4794
+ On a workspace-visible project, \`donebear project join\` and \`donebear
4795
+ project leave\` only change which ones sit in your sidebar's Your projects
4796
+ list: neither grants nor removes access. On a private project those same rows
4797
+ are the access, so leaving one gives up your ability to see it. Both are
4798
+ idempotent, and creating a project already joins you to it.`,
4503
4799
  title: "Projects, teams, and labels",
4504
4800
  whenToUse: "Deciding where a task belongs, creating a project or label, or joining/leaving a project."
4505
4801
  },
@@ -4539,9 +4835,21 @@ off unless the user named a second one:
4539
4835
  Ids and names never cross workspaces. A missing task, project, or label is
4540
4836
  missing from *this* workspace before it is deleted.
4541
4837
 
4838
+ A workspace owner can turn on domain join from Settings → Members. People who
4839
+ later sign in with a verified address on that work domain join as members,
4840
+ without an invite code. Personal email hosts cannot be used. The CLI has no
4841
+ domain-join command; joining by code remains \`donebear workspace join\`.
4842
+
4542
4843
  Auth precedence: \`DONEBEAR_TOKEN\` in the environment, then the stored session
4543
- from \`donebear auth login\`. Set the env var for anything non-interactive; the
4544
- browser login flow will hang in CI.
4844
+ from \`donebear auth login\` or \`donebear auth signup\`. Set the env var for
4845
+ anything non-interactive; the browser login flow will hang in CI.
4846
+
4847
+ New accounts from a coding agent: \`donebear auth signup --email <human-email>
4848
+ --json --no-interactive\`. Do not invent an email. One human step verifies it
4849
+ (link or 6-digit code). The CLI then stores a \`db_\` key with read+write and
4850
+ prints paste-ready MCP config for \`https://mcp.donebear.com/mcp\`. If that
4851
+ email is already registered, run \`donebear auth login\` instead. WorkOS
4852
+ \`agent_auth\` and RFC 8628 device code are not offered.
4545
4853
 
4546
4854
  export DONEBEAR_TOKEN=<token>
4547
4855
  donebear whoami --json
@@ -4551,6 +4859,15 @@ write fails with an explicit message rather than silently doing nothing — tell
4551
4859
  the user they need a write-scoped key rather than trying to work around it.
4552
4860
  \`donebear doctor --json\` reports which of these is actually in effect.
4553
4861
 
4862
+ ## Invites and share links
4863
+
4864
+ \`donebear workspace invite --email user@example.com\` sends a single-use
4865
+ email invite. \`donebear workspace invite --share --copy\` mints a reusable
4866
+ join link at \`https://donebear.com/join?code=\`. \`--project <id>\` on
4867
+ \`--share\` scopes the link: whoever opens it joins the whole workspace as a
4868
+ member, gets that project, and uses a seat. Join with
4869
+ \`donebear workspace join <code>\` or by opening the link while signed in.
4870
+
4554
4871
  ## Task attachments (read-only)
4555
4872
 
4556
4873
  Tasks can carry file/image attachments, capped at 10 MB per file and roughly
@@ -4571,6 +4888,9 @@ Three commands orient you, in order of cost:
4571
4888
  donebear guide <topic> concepts and conventions, no auth
4572
4889
  donebear context --json the live workspace: counts, recent tasks, projects
4573
4890
 
4891
+ No account yet: \`donebear auth signup --email <human-email> --json --no-interactive\`.
4892
+ Do not invent the email. A human verifies it, then the CLI stores a \`db_\` key.
4893
+
4574
4894
  \`donebear spec <command> <subcommand>\` narrows to one command's flags, output
4575
4895
  shape, and examples. Read that before guessing at a flag.
4576
4896
 
@@ -4599,22 +4919,25 @@ workspace from the token.`,
4599
4919
 
4600
4920
  Two importers exist, and they are not the same thing.
4601
4921
 
4922
+ **In the apps**, Settings → Integrations & Import (web) and Settings → Import
4923
+ (iOS: Reminders, Todoist, Things) cover Todoist, TickTick, Microsoft To Do, Any.do, Apple
4924
+ Reminders, Things 3, Trello, Asana, Notion, Google Tasks, and plain CSV, JSON or
4925
+ iCal files. Things 3 is no longer CLI-only: on iPhone, Things → Settings →
4926
+ General → Diagnostics → Enter Code → \`491348\` → Send Things Database, then
4927
+ upload the \`.aar\` under Settings → Import → Things, or \`main.sqlite\` on the web.
4928
+
4602
4929
  **In the terminal**, \`donebear import things\` reads a copy of a Things 3
4603
4930
  SQLite database and writes it into a workspace. It is the only import this CLI
4604
- performs. \`donebear spec import things\` lists its flags.
4931
+ performs. \`donebear spec import things\` lists its flags. It is the power-user
4932
+ path; send people to the apps first.
4605
4933
 
4606
4934
  On macOS it finds that database itself — \`donebear import things --dry-run\`
4607
- with no other flags is the whole command. This is worth volunteering, because
4608
- Things keeps its database inside a folder Finder hides, under a dated directory
4609
- whose name nobody can guess, and people hunting for it in Finder is the single
4610
- most common way this import stalls. \`--database-url\` is for a database
4611
- somewhere unusual: a backup, an external disk, another machine's copy.
4935
+ with no other flags is the whole command. \`--database-url\` is for a database
4936
+ somewhere unusual: an iPhone export, a backup, an external disk, another
4937
+ machine's copy.
4612
4938
 
4613
- **In the web app**, Settings → Integrations & Import covers Todoist, TickTick,
4614
- Microsoft To Do, Any.do, Apple Reminders, Things 3, Trello, Asana, Notion,
4615
- Google Tasks, and plain CSV, JSON or iCal files. If a user asks how to move a
4616
- backlog in from anything other than Things, that is where to send them; there
4617
- is no CLI equivalent to offer.
4939
+ If a user asks how to move a backlog in from anything other than Things, that is
4940
+ the web import screen; there is no CLI equivalent to offer.
4618
4941
 
4619
4942
  Each of those is a file the user exports and uploads — Todoist from Settings →
4620
4943
  Backups, Google Tasks from Google Takeout, Microsoft To Do from a read-only
@@ -4623,6 +4946,13 @@ that may change: a read-only connect flow is being built so Done Bear can make
4623
4946
  that call instead, so treat the Graph export as the current route and check the
4624
4947
  import screen before promising a connection.
4625
4948
 
4949
+ Every importer, here and in the apps, only reads the source; none of them
4950
+ writes, edits or deletes anything in the app being left. The one path that
4951
+ does is Reminders Inbox on iPhone (Settings → Import → Reminders Inbox), which
4952
+ is a capture bridge rather than an import: a chosen Reminders list shows at the
4953
+ top of Inbox, and importing a to-do from there creates the task and removes it
4954
+ from Reminders after a confirmation. It has no CLI equivalent.
4955
+
4626
4956
  The other direction is here though: \`donebear export\` writes a portable JSON
4627
4957
  archive of a workspace, and the web importer restores one.
4628
4958
 
@@ -4655,9 +4985,8 @@ only what is new.
4655
4985
 
4656
4986
  There is no undo for \`donebear import things\`; a run has to be unpicked by
4657
4987
  hand. The web importer does offer one on its completion screen, but the receipt
4658
- lives in the browser that ran the import, so it cannot be reached from the CLI,
4659
- another device or the iOS app. Say that plainly rather than sending someone
4660
- looking for a flag.`,
4988
+ lives in the browser that ran the import. An iOS import is undone with the
4989
+ usual undo for that change, not from the web receipt.`,
4661
4990
  title: "Importing from another app",
4662
4991
  whenToUse: "Moving a backlog in from Things 3 or another task manager, or restoring an export."
4663
4992
  }
@@ -4892,60 +5221,16 @@ const getUtcCalendarDayEndEpoch = (date) => {
4892
5221
  return Date.UTC(parts.year, parts.month - 1, parts.day, 23, 59, 59, 999);
4893
5222
  };
4894
5223
  //#endregion
4895
- //#region ../../packages/sync-ids/src/meet-done-bear.ts
4896
- /**
4897
- * Ids for the "Meet Done Bear" sample project every new workspace is seeded
4898
- * with.
4899
- *
4900
- * `manage-api` seeds the rows; `manage-frontend` and the CLI have to recognise
4901
- * and trash the same rows when an import lands, and neither can read the ids
4902
- * off the server. All three therefore re-derive them from the workspace id,
4903
- * which only works while the namespace and the row counts agree — so they live
4904
- * here rather than in each app.
4905
- *
4906
- * Unlike the composite ids in `./index`, these have no Swift port and no entry
4907
- * in `test-vectors.json`: iOS never seeds or dismisses the sample project.
4908
- */
4909
- const MEET_DONE_BEAR_ID_NAMESPACE = "b7e1d4a0-6c2f-4a8e-9d13-5f0c8a2b7e91";
4910
- const createMeetDoneBearProjectId = (workspaceId) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Project:${workspaceId}`);
4911
- const createMeetDoneBearHeadingId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Heading:${workspaceId}:${index}`);
4912
- const createMeetDoneBearTaskId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Task:${workspaceId}:${index}`);
4913
- //#endregion
4914
- //#region src/import/meet-done-bear.ts
4915
- /**
4916
- * Sample rows that still exist in the pre-import bootstrap. Missing or
4917
- * already-trashed rows are omitted so a later import does not fail.
4918
- */
4919
- const listMeetDoneBearArchiveTargets = (options) => {
4920
- const projectId = createMeetDoneBearProjectId(options.workspaceId);
4921
- if (!options.existingProjectIds.has(projectId)) return [];
4922
- const targets = [{
4923
- modelId: projectId,
4924
- modelName: "Project"
4925
- }];
4926
- const headingIds = options.existingHeadingIds;
4927
- if (headingIds) for (let index = 0; index < 3; index += 1) {
4928
- const modelId = createMeetDoneBearHeadingId(options.workspaceId, index);
4929
- if (headingIds.has(modelId)) targets.push({
4930
- modelId,
4931
- modelName: "ProjectHeading"
4932
- });
4933
- }
4934
- for (let index = 0; index < 9; index += 1) {
4935
- const modelId = createMeetDoneBearTaskId(options.workspaceId, index);
4936
- if (options.existingTaskIds.has(modelId)) targets.push({
4937
- modelId,
4938
- modelName: "Task"
4939
- });
4940
- }
4941
- return targets;
4942
- };
5224
+ //#region ../../packages/import-core/src/things/ids.ts
5225
+ const THINGS_IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
5226
+ const createDeterministicImportId = (params) => createDeterministicId(THINGS_IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
4943
5227
  //#endregion
4944
- //#region src/import/things-labels.ts
5228
+ //#region ../../packages/import-core/src/things/labels.ts
4945
5229
  const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
5230
+ const normalizeThingsTeamLookupKey = (value) => value.trim().toLocaleLowerCase();
4946
5231
  //#endregion
4947
- //#region src/import/things-values.ts
4948
- var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
5232
+ //#region ../../packages/import-core/src/things/values.ts
5233
+ var import_rrule = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
4949
5234
  (function webpackUniversalModuleDefinition(root, factory) {
4950
5235
  if (typeof exports === "object" && typeof module === "object") module.exports = factory();
4951
5236
  else if (typeof define === "function" && define.amd) define([], factory);
@@ -7699,7 +7984,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
7699
7984
  return __webpack_exports__;
7700
7985
  })();
7701
7986
  });
7702
- })))();
7987
+ })))(), 1);
7703
7988
  const PACKED_DATE_MULTIPLIER = 128;
7704
7989
  const PACKED_DATE_DAY_MASK = 31;
7705
7990
  const PACKED_DATE_MONTH_MASK = 15;
@@ -7759,7 +8044,8 @@ const combineThingsReminderAt = (options) => {
7759
8044
  return baseInstant === null ? null : baseInstant + offset;
7760
8045
  };
7761
8046
  //#endregion
7762
- //#region src/import/things-recurrence.ts
8047
+ //#region ../../packages/import-core/src/things/recurrence.ts
8048
+ const { RRule, rrulestr } = typeof import_rrule.RRule === "function" ? import_rrule : import_rrule.default;
7763
8049
  const THINGS_FREQUENCY_TO_RRULE = new Map([
7764
8050
  [4, "YEARLY"],
7765
8051
  [8, "MONTHLY"],
@@ -7800,8 +8086,8 @@ const parseOffsets = (xml) => {
7800
8086
  };
7801
8087
  const parseRRule = (rule) => {
7802
8088
  try {
7803
- const parsed = (0, import_rrule.rrulestr)(rule);
7804
- return parsed instanceof import_rrule.RRule ? parsed : null;
8089
+ const parsed = rrulestr(rule);
8090
+ return parsed instanceof RRule ? parsed : null;
7805
8091
  } catch {
7806
8092
  return null;
7807
8093
  }
@@ -7900,7 +8186,7 @@ const resolveThingsRepeatInstanceDate = (task, timeZone) => {
7900
8186
  return decodeThingsPackedCalendarDate(task.startDate, repeatTimeZone) ?? decodeThingsPackedCalendarDate(task.nextInstanceStartDate, repeatTimeZone) ?? null;
7901
8187
  };
7902
8188
  //#endregion
7903
- //#region src/import/things-mapper.ts
8189
+ //#region ../../packages/import-core/src/things/mapper.ts
7904
8190
  const MAX_CHECKLIST_TITLE_LENGTH = 500;
7905
8191
  const classifyTaskKind = (task) => {
7906
8192
  if (task.type === 2) return "heading";
@@ -8007,14 +8293,14 @@ const pickCompletedAt = (task) => {
8007
8293
  return decodeThingsUnixSeconds(task.stopDate) ?? decodeThingsUnixSeconds(task.userModificationDate) ?? decodeThingsUnixSeconds(task.creationDate);
8008
8294
  };
8009
8295
  const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
8010
- const textDecoder = new TextDecoder();
8296
+ const textDecoder$1 = new TextDecoder();
8011
8297
  const encodeBase64 = (value) => {
8012
8298
  let binary = "";
8013
8299
  for (const byte of value) binary += String.fromCodePoint(byte);
8014
8300
  return btoa(binary);
8015
8301
  };
8016
8302
  const sanitizeBinary = (value) => {
8017
- const asText = textDecoder.decode(value);
8303
+ const asText = textDecoder$1.decode(value);
8018
8304
  if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
8019
8305
  const trimmed = asText.trim();
8020
8306
  if (trimmed.length > 0) return trimmed;
@@ -8174,19 +8460,25 @@ const buildChecklistMutation = (options) => {
8174
8460
  };
8175
8461
  const buildTeamActions = (ctx, areas) => {
8176
8462
  const teams = [];
8463
+ const teamMemberships = [];
8464
+ const membershipIds = /* @__PURE__ */ new Set();
8177
8465
  for (const area of [...areas].toSorted((a, b) => {
8178
8466
  const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
8179
8467
  if (indexComparison !== 0) return indexComparison;
8180
8468
  return a.uuid.localeCompare(b.uuid);
8181
8469
  })) {
8182
- const teamId = createDeterministicImportId({
8470
+ const teamName = normalizeTitle(area.title, `Area ${area.uuid.slice(0, 8)}`);
8471
+ const teamLookupKey = normalizeThingsTeamLookupKey(teamName);
8472
+ const existingTeamId = ctx.existingTeamIdByName.get(teamLookupKey);
8473
+ const teamId = existingTeamId ?? createDeterministicImportId({
8183
8474
  modelName: "Team",
8184
8475
  sourceId: area.uuid,
8185
8476
  workspaceId: ctx.workspaceId
8186
8477
  });
8478
+ ctx.existingTeamIdByName.set(teamLookupKey, teamId);
8187
8479
  ctx.areaTeamIdBySourceId.set(area.uuid, teamId);
8188
8480
  const areaTagTitles = (ctx.areaTagMap.get(area.uuid) ?? []).map((link) => ctx.tagTitleById.get(link.tagId) ?? null).filter((title) => typeof title === "string");
8189
- teams.push({
8481
+ if (!existingTeamId) teams.push({
8190
8482
  modelId: teamId,
8191
8483
  modelName: "Team",
8192
8484
  payload: {
@@ -8195,14 +8487,36 @@ const buildTeamActions = (ctx, areas) => {
8195
8487
  description: appendMetadataBlock(null, ctx.metadataMode, buildAreaMetadata(area, areaTagTitles)),
8196
8488
  icon: null,
8197
8489
  key: ctx.teamKeys.get(area.uuid) ?? "AREA",
8198
- name: normalizeTitle(area.title, `Area ${area.uuid.slice(0, 8)}`),
8490
+ name: teamName,
8199
8491
  updatedAt: ctx.createdAtFallback,
8200
8492
  workspaceId: ctx.workspaceId
8201
8493
  },
8202
8494
  sourceId: area.uuid
8203
8495
  });
8496
+ const membershipId = createTeamMembershipId({
8497
+ teamId,
8498
+ userId: ctx.creatorId,
8499
+ workspaceId: ctx.workspaceId
8500
+ });
8501
+ if (!membershipIds.has(membershipId)) {
8502
+ membershipIds.add(membershipId);
8503
+ teamMemberships.push({
8504
+ modelId: membershipId,
8505
+ modelName: "TeamMembership",
8506
+ payload: {
8507
+ createdAt: ctx.createdAtFallback,
8508
+ teamId,
8509
+ userId: ctx.creatorId,
8510
+ workspaceId: ctx.workspaceId
8511
+ },
8512
+ sourceId: `${area.uuid}:${ctx.creatorId}`
8513
+ });
8514
+ }
8204
8515
  }
8205
- return teams;
8516
+ return {
8517
+ teamMemberships,
8518
+ teams
8519
+ };
8206
8520
  };
8207
8521
  const buildLabelActions = (ctx, tags) => {
8208
8522
  const labels = [];
@@ -8235,6 +8549,7 @@ const buildLabelActions = (ctx, tags) => {
8235
8549
  };
8236
8550
  const buildProjectActions = (ctx, sourceProjects) => {
8237
8551
  const projects = [];
8552
+ const projectMemberships = [];
8238
8553
  const teamProjects = [];
8239
8554
  for (const project of [...sourceProjects].toSorted((a, b) => {
8240
8555
  const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
@@ -8270,6 +8585,21 @@ const buildProjectActions = (ctx, sourceProjects) => {
8270
8585
  },
8271
8586
  sourceId: project.uuid
8272
8587
  });
8588
+ projectMemberships.push({
8589
+ modelId: createProjectMembershipId({
8590
+ projectId,
8591
+ userId: ctx.creatorId,
8592
+ workspaceId: ctx.workspaceId
8593
+ }),
8594
+ modelName: "ProjectMembership",
8595
+ payload: {
8596
+ createdAt: decodeThingsUnixSeconds(project.creationDate) ?? ctx.createdAtFallback,
8597
+ projectId,
8598
+ userId: ctx.creatorId,
8599
+ workspaceId: ctx.workspaceId
8600
+ },
8601
+ sourceId: `${project.uuid}:${ctx.creatorId}`
8602
+ });
8273
8603
  if (project.areaId !== null) {
8274
8604
  const teamId = ctx.areaTeamIdBySourceId.get(project.areaId);
8275
8605
  if (teamId) teamProjects.push({
@@ -8290,6 +8620,7 @@ const buildProjectActions = (ctx, sourceProjects) => {
8290
8620
  }
8291
8621
  }
8292
8622
  return {
8623
+ projectMemberships,
8293
8624
  projects,
8294
8625
  teamProjects
8295
8626
  };
@@ -8542,6 +8873,7 @@ const buildThingsImportPlan = (options) => {
8542
8873
  createdAtFallback: Date.now(),
8543
8874
  creatorId: options.creatorId,
8544
8875
  existingLabelIdByTitle: existingLabelsByTitle ?? /* @__PURE__ */ new Map(),
8876
+ existingTeamIdByName: options.existingTeamsByName ?? /* @__PURE__ */ new Map(),
8545
8877
  headingIdBySourceId: /* @__PURE__ */ new Map(),
8546
8878
  metadataMode: options.metadataMode,
8547
8879
  projectIdBySourceId: /* @__PURE__ */ new Map(),
@@ -8558,15 +8890,18 @@ const buildThingsImportPlan = (options) => {
8558
8890
  maxLength: 24
8559
8891
  }),
8560
8892
  timeZone: options.timeZone,
8561
- todayDateOnly: dateOnlyEpochFromLocalDate(),
8893
+ todayDateOnly: localDateToDateOnlyEpoch(/* @__PURE__ */ new Date()),
8562
8894
  workspaceId
8563
8895
  };
8564
- const teams = options.skipTeams ? [] : buildTeamActions(ctx, snapshot.areas);
8896
+ const { teamMemberships, teams } = options.skipTeams ? {
8897
+ teamMemberships: [],
8898
+ teams: []
8899
+ } : buildTeamActions(ctx, snapshot.areas);
8565
8900
  const labels = buildLabelActions(ctx, snapshot.tags);
8566
8901
  const sourceProjects = snapshot.tasks.filter((task) => classifyTaskKind(task) === "project");
8567
8902
  const sourceHeadings = snapshot.tasks.filter((task) => classifyTaskKind(task) === "heading");
8568
8903
  const sourceTasks = snapshot.tasks.filter((task) => classifyTaskKind(task) === "task");
8569
- const { projects, teamProjects } = buildProjectActions(ctx, sourceProjects);
8904
+ const { projectMemberships, projects, teamProjects } = buildProjectActions(ctx, sourceProjects);
8570
8905
  const headings = buildHeadingActions(ctx, sourceHeadings);
8571
8906
  const { tasks, taskLabels, checklistItems } = buildTaskActions(ctx, sourceTasks);
8572
8907
  const phases = [
@@ -8574,6 +8909,10 @@ const buildThingsImportPlan = (options) => {
8574
8909
  items: teams,
8575
8910
  name: "teams"
8576
8911
  },
8912
+ {
8913
+ items: teamMemberships,
8914
+ name: "teamMemberships"
8915
+ },
8577
8916
  {
8578
8917
  items: labels,
8579
8918
  name: "labels"
@@ -8582,6 +8921,10 @@ const buildThingsImportPlan = (options) => {
8582
8921
  items: projects,
8583
8922
  name: "projects"
8584
8923
  },
8924
+ {
8925
+ items: projectMemberships,
8926
+ name: "projectMemberships"
8927
+ },
8585
8928
  {
8586
8929
  items: teamProjects,
8587
8930
  name: "teamProjects"
@@ -8623,11 +8966,208 @@ const buildThingsImportPlan = (options) => {
8623
8966
  };
8624
8967
  };
8625
8968
  //#endregion
8969
+ //#region ../../packages/import-core/src/things/snapshot.ts
8970
+ const textDecoder = new TextDecoder();
8971
+ const THINGS_REQUIRED_TABLE = "TMTask";
8972
+ const THINGS_SQLITE_FILE = "main.sqlite";
8973
+ const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
8974
+ const toThingsStringValue = (value) => {
8975
+ if (typeof value === "string") {
8976
+ const trimmed = value.trim();
8977
+ return trimmed.length > 0 ? trimmed : null;
8978
+ }
8979
+ if (value instanceof Uint8Array) {
8980
+ const text = textDecoder.decode(value).trim();
8981
+ return text.length > 0 ? text : null;
8982
+ }
8983
+ return null;
8984
+ };
8985
+ const toThingsNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
8986
+ const toThingsBooleanValue = (value) => {
8987
+ const numeric = toThingsNumberValue(value);
8988
+ return numeric === null ? null : numeric !== 0;
8989
+ };
8990
+ const normalizeThingsTaskRecord = (row) => {
8991
+ const uuid = toThingsStringValue(row.uuid);
8992
+ if (!uuid) return null;
8993
+ return {
8994
+ afterCompletionReferenceDate: toThingsNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
8995
+ areaId: toThingsStringValue(row.area),
8996
+ contactId: toThingsStringValue(row.contact ?? row.delegate),
8997
+ creationDate: toThingsNumberValue(row.creationDate),
8998
+ deadline: toThingsNumberValue(row.deadline ?? row.dueDate),
8999
+ deadlineOffset: toThingsNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
9000
+ deadlineSuppressionDate: toThingsNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
9001
+ headingId: toThingsStringValue(row.heading ?? row.actionGroup),
9002
+ instanceCreationCount: toThingsNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
9003
+ instanceCreationPaused: toThingsBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
9004
+ instanceCreationStartDate: toThingsNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
9005
+ lastReminderInteractionDate: toThingsNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
9006
+ nextInstanceStartDate: toThingsNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
9007
+ notes: toThingsStringValue(row.notes),
9008
+ projectId: toThingsStringValue(row.project),
9009
+ raw: row,
9010
+ recurrenceRuleRaw: toThingsStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
9011
+ reminderTime: toThingsNumberValue(row.reminderTime),
9012
+ repeatTemplateSourceId: toThingsStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
9013
+ repeaterMigrationDate: toThingsNumberValue(row.repeaterMigrationDate),
9014
+ repeaterRaw: toThingsStringValue(row.repeater),
9015
+ sortIndex: toThingsNumberValue(row.index),
9016
+ start: toThingsNumberValue(row.start),
9017
+ startBucket: toThingsNumberValue(row.startBucket),
9018
+ startDate: toThingsNumberValue(row.startDate),
9019
+ status: toThingsNumberValue(row.status),
9020
+ stopDate: toThingsNumberValue(row.stopDate),
9021
+ title: toThingsStringValue(row.title),
9022
+ todayIndex: toThingsNumberValue(row.todayIndex),
9023
+ todayIndexReferenceDate: toThingsNumberValue(row.todayIndexReferenceDate),
9024
+ trashed: toThingsBooleanValue(row.trashed) ?? false,
9025
+ type: toThingsNumberValue(row.type),
9026
+ userModificationDate: toThingsNumberValue(row.userModificationDate),
9027
+ uuid
9028
+ };
9029
+ };
9030
+ const normalizeThingsAreaRecord = (row) => {
9031
+ const uuid = toThingsStringValue(row.uuid);
9032
+ if (!uuid) return null;
9033
+ return {
9034
+ raw: row,
9035
+ sortIndex: toThingsNumberValue(row.index),
9036
+ title: toThingsStringValue(row.title),
9037
+ uuid,
9038
+ visible: toThingsBooleanValue(row.visible)
9039
+ };
9040
+ };
9041
+ const normalizeThingsTagRecord = (row) => {
9042
+ const uuid = toThingsStringValue(row.uuid);
9043
+ if (!uuid) return null;
9044
+ return {
9045
+ parentId: toThingsStringValue(row.parent),
9046
+ raw: row,
9047
+ shortcut: toThingsStringValue(row.shortcut),
9048
+ sortIndex: toThingsNumberValue(row.index),
9049
+ title: toThingsStringValue(row.title),
9050
+ usedDate: toThingsNumberValue(row.usedDate),
9051
+ uuid
9052
+ };
9053
+ };
9054
+ const normalizeThingsChecklistItemRecord = (row) => {
9055
+ const uuid = toThingsStringValue(row.uuid);
9056
+ if (!uuid) return null;
9057
+ return {
9058
+ creationDate: toThingsNumberValue(row.creationDate),
9059
+ raw: row,
9060
+ sortIndex: toThingsNumberValue(row.index),
9061
+ status: toThingsNumberValue(row.status),
9062
+ stopDate: toThingsNumberValue(row.stopDate),
9063
+ taskId: toThingsStringValue(row.task),
9064
+ title: toThingsStringValue(row.title),
9065
+ userModificationDate: toThingsNumberValue(row.userModificationDate),
9066
+ uuid
9067
+ };
9068
+ };
9069
+ const normalizeThingsTaskTagRecord = (row) => {
9070
+ const taskId = toThingsStringValue(row.tasks);
9071
+ const tagId = toThingsStringValue(row.tags);
9072
+ return taskId && tagId ? {
9073
+ tagId,
9074
+ taskId
9075
+ } : null;
9076
+ };
9077
+ const normalizeThingsAreaTagRecord = (row) => {
9078
+ const areaId = toThingsStringValue(row.areas);
9079
+ const tagId = toThingsStringValue(row.tags);
9080
+ return areaId && tagId ? {
9081
+ areaId,
9082
+ tagId
9083
+ } : null;
9084
+ };
9085
+ const normalizeThingsContactRecord = (row) => {
9086
+ const uuid = toThingsStringValue(row.uuid);
9087
+ if (!uuid) return null;
9088
+ return {
9089
+ appleAddressBookId: toThingsStringValue(row.appleAddressBookId),
9090
+ displayName: toThingsStringValue(row.displayName),
9091
+ emails: toThingsStringValue(row.emails),
9092
+ firstName: toThingsStringValue(row.firstName),
9093
+ lastName: toThingsStringValue(row.lastName),
9094
+ raw: row,
9095
+ uuid
9096
+ };
9097
+ };
9098
+ const mapPresent = (rows, normalize) => rows.map(normalize).filter((record) => record !== null);
9099
+ /**
9100
+ * Turns raw Things tables into the snapshot `buildThingsImportPlan` consumes.
9101
+ * Callers supply the SQL; this is the one place row → record mapping lives.
9102
+ */
9103
+ const buildThingsDatabaseSnapshot = (reader, databasePath) => {
9104
+ 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.");
9105
+ return {
9106
+ areaTags: reader.hasTable("TMAreaTag") ? mapPresent(reader.readRows("TMAreaTag"), normalizeThingsAreaTagRecord) : [],
9107
+ areas: reader.hasTable("TMArea") ? mapPresent(reader.readRows("TMArea"), normalizeThingsAreaRecord) : [],
9108
+ checklistItems: reader.hasTable("TMChecklistItem") ? mapPresent(reader.readRows("TMChecklistItem"), normalizeThingsChecklistItemRecord) : [],
9109
+ contacts: reader.hasTable("TMContact") ? mapPresent(reader.readRows("TMContact"), normalizeThingsContactRecord) : [],
9110
+ databasePath,
9111
+ tags: reader.hasTable("TMTag") ? mapPresent(reader.readRows("TMTag"), normalizeThingsTagRecord) : [],
9112
+ taskColumns: reader.getTableColumns(THINGS_REQUIRED_TABLE),
9113
+ taskTags: reader.hasTable("TMTaskTag") ? mapPresent(reader.readRows("TMTaskTag"), normalizeThingsTaskTagRecord) : [],
9114
+ tasks: mapPresent(reader.readRows(THINGS_REQUIRED_TABLE), normalizeThingsTaskRecord)
9115
+ };
9116
+ };
9117
+ //#endregion
9118
+ //#region ../../packages/sync-ids/src/meet-done-bear.ts
9119
+ /**
9120
+ * Ids for the "Meet Done Bear" sample project every new workspace is seeded
9121
+ * with.
9122
+ *
9123
+ * `manage-api` seeds the rows; `manage-frontend` and the CLI have to recognise
9124
+ * and trash the same rows when an import lands, and neither can read the ids
9125
+ * off the server. All three therefore re-derive them from the workspace id,
9126
+ * which only works while the namespace and the row counts agree — so they live
9127
+ * here rather than in each app.
9128
+ *
9129
+ * iOS ports `createMeetDoneBearProjectId` / `isMeetDoneBearProjectId` in
9130
+ * `DeterministicIds.swift` so Quiet Invite can ignore seed tasks. There is
9131
+ * still no vector section — dedicated Swift tests pin the same namespace.
9132
+ */
9133
+ const MEET_DONE_BEAR_ID_NAMESPACE = "b7e1d4a0-6c2f-4a8e-9d13-5f0c8a2b7e91";
9134
+ const createMeetDoneBearProjectId = (workspaceId) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Project:${workspaceId}`);
9135
+ const createMeetDoneBearHeadingId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Heading:${workspaceId}:${index}`);
9136
+ const createMeetDoneBearTaskId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Task:${workspaceId}:${index}`);
9137
+ //#endregion
9138
+ //#region src/import/meet-done-bear.ts
9139
+ /**
9140
+ * Sample rows that still exist in the pre-import bootstrap. Missing or
9141
+ * already-trashed rows are omitted so a later import does not fail.
9142
+ */
9143
+ const listMeetDoneBearArchiveTargets = (options) => {
9144
+ const projectId = createMeetDoneBearProjectId(options.workspaceId);
9145
+ if (!options.existingProjectIds.has(projectId)) return [];
9146
+ const targets = [{
9147
+ modelId: projectId,
9148
+ modelName: "Project"
9149
+ }];
9150
+ const headingIds = options.existingHeadingIds;
9151
+ if (headingIds) for (let index = 0; index < 3; index += 1) {
9152
+ const modelId = createMeetDoneBearHeadingId(options.workspaceId, index);
9153
+ if (headingIds.has(modelId)) targets.push({
9154
+ modelId,
9155
+ modelName: "ProjectHeading"
9156
+ });
9157
+ }
9158
+ for (let index = 0; index < 9; index += 1) {
9159
+ const modelId = createMeetDoneBearTaskId(options.workspaceId, index);
9160
+ if (options.existingTaskIds.has(modelId)) targets.push({
9161
+ modelId,
9162
+ modelName: "Task"
9163
+ });
9164
+ }
9165
+ return targets;
9166
+ };
9167
+ //#endregion
8626
9168
  //#region src/import/things-sqlite.ts
8627
9169
  const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac");
8628
9170
  const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
8629
- const THINGS_SQLITE_FILE = "main.sqlite";
8630
- const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
8631
9171
  const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
8632
9172
  const isUrlLike = (value) => URL_SCHEME_PATTERN.test(value);
8633
9173
  const resolveInputPath = (databaseUrl) => {
@@ -8640,7 +9180,7 @@ const resolveInputPath = (databaseUrl) => {
8640
9180
  const expandThingsDatabasePath = (inputPath) => {
8641
9181
  const resolvedPath = resolve(inputPath);
8642
9182
  const lastSegment = basename(resolvedPath);
8643
- if (lastSegment === THINGS_SQLITE_FILE || lastSegment === LEGACY_THINGS_SQLITE_FILE) return resolvedPath;
9183
+ if (lastSegment === "main.sqlite" || lastSegment === "Things.sqlite3") return resolvedPath;
8644
9184
  if (lastSegment === THINGS_BUNDLE_NAME) return join(resolvedPath, THINGS_SQLITE_FILE);
8645
9185
  if (lastSegment.startsWith("ThingsData-")) return join(resolvedPath, THINGS_BUNDLE_NAME, THINGS_SQLITE_FILE);
8646
9186
  return resolvedPath;
@@ -8685,130 +9225,6 @@ const getTableColumns = (database, tableName) => {
8685
9225
  if (!hasTable(database, tableName)) return [];
8686
9226
  return database.prepare(`PRAGMA table_info(${tableName})`).all().map((row) => typeof row.name === "string" ? row.name : null).filter((name) => name !== null);
8687
9227
  };
8688
- const toStringValue = (value) => {
8689
- if (typeof value === "string") {
8690
- const trimmed = value.trim();
8691
- return trimmed.length > 0 ? trimmed : null;
8692
- }
8693
- if (Buffer.isBuffer(value)) {
8694
- const text = value.toString("utf-8").trim();
8695
- return text.length > 0 ? text : null;
8696
- }
8697
- return null;
8698
- };
8699
- const toNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
8700
- const toBooleanValue = (value) => {
8701
- const numeric = toNumberValue(value);
8702
- return numeric === null ? null : numeric !== 0;
8703
- };
8704
- const normalizeTaskRecord = (row) => {
8705
- const uuid = toStringValue(row.uuid);
8706
- if (!uuid) return null;
8707
- return {
8708
- afterCompletionReferenceDate: toNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
8709
- areaId: toStringValue(row.area),
8710
- contactId: toStringValue(row.contact ?? row.delegate),
8711
- creationDate: toNumberValue(row.creationDate),
8712
- deadline: toNumberValue(row.deadline ?? row.dueDate),
8713
- deadlineOffset: toNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
8714
- deadlineSuppressionDate: toNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
8715
- headingId: toStringValue(row.heading ?? row.actionGroup),
8716
- instanceCreationCount: toNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
8717
- instanceCreationPaused: toBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
8718
- instanceCreationStartDate: toNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
8719
- lastReminderInteractionDate: toNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
8720
- nextInstanceStartDate: toNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
8721
- notes: toStringValue(row.notes),
8722
- projectId: toStringValue(row.project),
8723
- raw: row,
8724
- recurrenceRuleRaw: toStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
8725
- reminderTime: toNumberValue(row.reminderTime),
8726
- repeatTemplateSourceId: toStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
8727
- repeaterMigrationDate: toNumberValue(row.repeaterMigrationDate),
8728
- repeaterRaw: toStringValue(row.repeater),
8729
- sortIndex: toNumberValue(row.index),
8730
- start: toNumberValue(row.start),
8731
- startBucket: toNumberValue(row.startBucket),
8732
- startDate: toNumberValue(row.startDate),
8733
- status: toNumberValue(row.status),
8734
- stopDate: toNumberValue(row.stopDate),
8735
- title: toStringValue(row.title),
8736
- todayIndex: toNumberValue(row.todayIndex),
8737
- todayIndexReferenceDate: toNumberValue(row.todayIndexReferenceDate),
8738
- trashed: toBooleanValue(row.trashed) ?? false,
8739
- type: toNumberValue(row.type),
8740
- userModificationDate: toNumberValue(row.userModificationDate),
8741
- uuid
8742
- };
8743
- };
8744
- const normalizeAreaRecord = (row) => {
8745
- const uuid = toStringValue(row.uuid);
8746
- if (!uuid) return null;
8747
- return {
8748
- raw: row,
8749
- sortIndex: toNumberValue(row.index),
8750
- title: toStringValue(row.title),
8751
- uuid,
8752
- visible: toBooleanValue(row.visible)
8753
- };
8754
- };
8755
- const normalizeTagRecord = (row) => {
8756
- const uuid = toStringValue(row.uuid);
8757
- if (!uuid) return null;
8758
- return {
8759
- parentId: toStringValue(row.parent),
8760
- raw: row,
8761
- shortcut: toStringValue(row.shortcut),
8762
- sortIndex: toNumberValue(row.index),
8763
- title: toStringValue(row.title),
8764
- usedDate: toNumberValue(row.usedDate),
8765
- uuid
8766
- };
8767
- };
8768
- const normalizeChecklistItemRecord = (row) => {
8769
- const uuid = toStringValue(row.uuid);
8770
- if (!uuid) return null;
8771
- return {
8772
- creationDate: toNumberValue(row.creationDate),
8773
- raw: row,
8774
- sortIndex: toNumberValue(row.index),
8775
- status: toNumberValue(row.status),
8776
- stopDate: toNumberValue(row.stopDate),
8777
- taskId: toStringValue(row.task),
8778
- title: toStringValue(row.title),
8779
- userModificationDate: toNumberValue(row.userModificationDate),
8780
- uuid
8781
- };
8782
- };
8783
- const normalizeTaskTagRecord = (row) => {
8784
- const taskId = toStringValue(row.tasks);
8785
- const tagId = toStringValue(row.tags);
8786
- return taskId && tagId ? {
8787
- tagId,
8788
- taskId
8789
- } : null;
8790
- };
8791
- const normalizeAreaTagRecord = (row) => {
8792
- const areaId = toStringValue(row.areas);
8793
- const tagId = toStringValue(row.tags);
8794
- return areaId && tagId ? {
8795
- areaId,
8796
- tagId
8797
- } : null;
8798
- };
8799
- const normalizeContactRecord = (row) => {
8800
- const uuid = toStringValue(row.uuid);
8801
- if (!uuid) return null;
8802
- return {
8803
- appleAddressBookId: toStringValue(row.appleAddressBookId),
8804
- displayName: toStringValue(row.displayName),
8805
- emails: toStringValue(row.emails),
8806
- firstName: toStringValue(row.firstName),
8807
- lastName: toStringValue(row.lastName),
8808
- raw: row,
8809
- uuid
8810
- };
8811
- };
8812
9228
  const resolveThingsDatabasePath = (databaseUrl) => expandThingsDatabasePath(resolveInputPath(databaseUrl));
8813
9229
  const resolveDefaultThingsDatabasePath = () => {
8814
9230
  const resolved = findLatestThingsDatabasePath();
@@ -8823,17 +9239,11 @@ const loadThingsDatabase = (databaseUrl) => {
8823
9239
  readonly: true
8824
9240
  });
8825
9241
  try {
8826
- return {
8827
- areaTags: hasTable(database, "TMAreaTag") ? readRows(database, "TMAreaTag").map(normalizeAreaTagRecord).filter((record) => record !== null) : [],
8828
- areas: hasTable(database, "TMArea") ? readRows(database, "TMArea").map(normalizeAreaRecord).filter((record) => record !== null) : [],
8829
- checklistItems: hasTable(database, "TMChecklistItem") ? readRows(database, "TMChecklistItem").map(normalizeChecklistItemRecord).filter((record) => record !== null) : [],
8830
- contacts: hasTable(database, "TMContact") ? readRows(database, "TMContact").map(normalizeContactRecord).filter((record) => record !== null) : [],
8831
- databasePath,
8832
- tags: hasTable(database, "TMTag") ? readRows(database, "TMTag").map(normalizeTagRecord).filter((record) => record !== null) : [],
8833
- taskColumns: getTableColumns(database, "TMTask"),
8834
- taskTags: hasTable(database, "TMTaskTag") ? readRows(database, "TMTaskTag").map(normalizeTaskTagRecord).filter((record) => record !== null) : [],
8835
- tasks: readRows(database, "TMTask").map(normalizeTaskRecord).filter((record) => record !== null)
8836
- };
9242
+ return buildThingsDatabaseSnapshot({
9243
+ getTableColumns: (tableName) => getTableColumns(database, tableName),
9244
+ hasTable: (tableName) => hasTable(database, tableName),
9245
+ readRows: (tableName) => readRows(database, tableName)
9246
+ }, databasePath);
8837
9247
  } finally {
8838
9248
  database.close();
8839
9249
  }
@@ -8841,7 +9251,15 @@ const loadThingsDatabase = (databaseUrl) => {
8841
9251
  //#endregion
8842
9252
  //#region src/import/things-runner.ts
8843
9253
  const WORKSPACE_SCOPED_MODELS = SYNC_CONTENT_MODELS;
8844
- const INSERT_ONLY_MODELS = new Set(["TaskLabel", "TeamProject"]);
9254
+ const INSERT_ONLY_MODELS = new Set([
9255
+ "TaskLabel",
9256
+ "TeamProject",
9257
+ "TeamMembership",
9258
+ "ProjectMembership"
9259
+ ]);
9260
+ /** Membership joins are not in SYNC_CONTENT_MODELS (identity surface), but
9261
+ * Things import writes the importer's own rows and needs them for skip-on-rerun. */
9262
+ const THINGS_MEMBERSHIP_MODELS = ["TeamMembership", "ProjectMembership"];
8845
9263
  const chunk = (items, size) => {
8846
9264
  const result = [];
8847
9265
  for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
@@ -8885,9 +9303,9 @@ const loadWorkspaceScopedRowsByModel = async (context, options) => {
8885
9303
  };
8886
9304
  const loadExistingImportState = async (options) => {
8887
9305
  let modelNames;
8888
- if (options.dryRun) modelNames = ["Label"];
8889
- else if (options.skipTeams) modelNames = WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject");
8890
- else modelNames = WORKSPACE_SCOPED_MODELS;
9306
+ if (options.dryRun) modelNames = options.skipTeams ? ["Label"] : ["Label", "Team"];
9307
+ else if (options.skipTeams) modelNames = [...WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject"), "ProjectMembership"];
9308
+ else modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
8891
9309
  const rowsByModel = await loadWorkspaceScopedRowsByModel(options.context, {
8892
9310
  apiBaseUrl: options.commandContext.apiBaseUrl,
8893
9311
  modelNames,
@@ -8896,6 +9314,7 @@ const loadExistingImportState = async (options) => {
8896
9314
  });
8897
9315
  return {
8898
9316
  existingLabelIdsByTitle: buildExistingLabelIdsByTitle(rowsByModel),
9317
+ existingTeamIdsByName: buildExistingTeamIdsByName(rowsByModel),
8899
9318
  rowsByModel
8900
9319
  };
8901
9320
  };
@@ -8913,9 +9332,23 @@ const buildExistingLabelIdsByTitle = (rowsByModel) => {
8913
9332
  }
8914
9333
  return labelsByTitle;
8915
9334
  };
9335
+ const buildExistingTeamIdsByName = (rowsByModel) => {
9336
+ const rows = rowsByModel.get("Team") ?? [];
9337
+ const teamsByName = /* @__PURE__ */ new Map();
9338
+ for (const row of rows) {
9339
+ if (typeof row !== "object" || row === null) continue;
9340
+ const record = row;
9341
+ if (record.__class !== "Team") continue;
9342
+ const id = typeof record.id === "string" ? record.id : null;
9343
+ const name = typeof record.name === "string" ? record.name.trim() : "";
9344
+ if (!id || name.length === 0) continue;
9345
+ teamsByName.set(normalizeThingsTeamLookupKey(name), id);
9346
+ }
9347
+ return teamsByName;
9348
+ };
8916
9349
  const buildExistingIds = (rowsByModel) => {
8917
- const entries = WORKSPACE_SCOPED_MODELS.map((modelName) => [modelName, collectRowIds(rowsByModel.get(modelName) ?? [], modelName)]);
8918
- return new Map(entries);
9350
+ const modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
9351
+ return new Map(modelNames.map((modelName) => [modelName, collectRowIds(rowsByModel.get(modelName) ?? [], modelName)]));
8919
9352
  };
8920
9353
  const buildTransactions = (phaseItems, options) => {
8921
9354
  const transactions = [];
@@ -8933,6 +9366,7 @@ const buildTransactions = (phaseItems, options) => {
8933
9366
  if (action === "INSERT") {
8934
9367
  inserted += 1;
8935
9368
  existingIds.add(item.modelId);
9369
+ options.existingIds.set(item.modelName, existingIds);
8936
9370
  } else updated += 1;
8937
9371
  transactions.push({
8938
9372
  action,
@@ -9018,6 +9452,7 @@ const runThingsImport = async (options) => {
9018
9452
  const plan = buildThingsImportPlan({
9019
9453
  creatorId: options.creatorId,
9020
9454
  existingLabelsByTitle: existingState.existingLabelIdsByTitle,
9455
+ existingTeamsByName: existingState.existingTeamIdsByName,
9021
9456
  metadataMode: options.metadataMode,
9022
9457
  skipTeams: options.skipTeams,
9023
9458
  snapshot,
@@ -9246,6 +9681,30 @@ const runLabelCreate = async (context, title, options) => {
9246
9681
  }
9247
9682
  writeSuccess(context, `Created label: ${title.trim()}`);
9248
9683
  };
9684
+ /**
9685
+ * A TaskLabel row is keyed on task, label and workspace together, so a DELETE
9686
+ * needs `workspaceId` as much as an INSERT does: without it the server cannot
9687
+ * address the row and rejects the mutation.
9688
+ */
9689
+ const buildTaskLabelTransaction = (options) => {
9690
+ const { action, clientId, labelId, taskId, workspaceId } = options;
9691
+ const key = {
9692
+ labelId,
9693
+ taskId,
9694
+ workspaceId
9695
+ };
9696
+ return {
9697
+ action,
9698
+ clientId,
9699
+ clientTxId: randomUUID(),
9700
+ modelId: `${taskId}:${labelId}`,
9701
+ modelName: "TaskLabel",
9702
+ payload: action === "INSERT" ? {
9703
+ ...key,
9704
+ createdAt: Date.now()
9705
+ } : key
9706
+ };
9707
+ };
9249
9708
  const runLabelAdd = async (context, taskRef, labelRef, options) => {
9250
9709
  const commandContext = await resolveCommandContext(context);
9251
9710
  const workspaceRef = normalizeWorkspaceRef(options.workspace);
@@ -9263,19 +9722,13 @@ const runLabelAdd = async (context, taskRef, labelRef, options) => {
9263
9722
  baseUrl: commandContext.apiBaseUrl,
9264
9723
  request: {
9265
9724
  batchId: randomUUID(),
9266
- transactions: [{
9725
+ transactions: [buildTaskLabelTransaction({
9267
9726
  action: "INSERT",
9268
9727
  clientId: commandContext.localContext.clientId,
9269
- clientTxId: randomUUID(),
9270
- modelId: `${task.id}:${label.id}`,
9271
- modelName: "TaskLabel",
9272
- payload: {
9273
- createdAt: Date.now(),
9274
- labelId: label.id,
9275
- taskId: task.id,
9276
- workspaceId: task.workspaceId
9277
- }
9278
- }]
9728
+ labelId: label.id,
9729
+ taskId: task.id,
9730
+ workspaceId: task.workspaceId
9731
+ })]
9279
9732
  },
9280
9733
  token: commandContext.token
9281
9734
  });
@@ -9307,17 +9760,13 @@ const runLabelRemove = async (context, taskRef, labelRef, options) => {
9307
9760
  baseUrl: commandContext.apiBaseUrl,
9308
9761
  request: {
9309
9762
  batchId: randomUUID(),
9310
- transactions: [{
9763
+ transactions: [buildTaskLabelTransaction({
9311
9764
  action: "DELETE",
9312
9765
  clientId: commandContext.localContext.clientId,
9313
- clientTxId: randomUUID(),
9314
- modelId: `${task.id}:${label.id}`,
9315
- modelName: "TaskLabel",
9316
- payload: {
9317
- labelId: label.id,
9318
- taskId: task.id
9319
- }
9320
- }]
9766
+ labelId: label.id,
9767
+ taskId: task.id,
9768
+ workspaceId: task.workspaceId
9769
+ })]
9321
9770
  },
9322
9771
  token: commandContext.token
9323
9772
  });
@@ -9522,7 +9971,7 @@ const resolveProjectName = async (context, rawName) => {
9522
9971
  const trimmed = rawName.trim();
9523
9972
  if (trimmed.length > 0) return trimmed;
9524
9973
  }
9525
- const { promptForValue } = await import("./prompt-B7mDo4K7.mjs");
9974
+ const { promptForValue } = await import("./prompt--P7Y1FBL.mjs");
9526
9975
  const prompted = await promptForValue(context, "Project name");
9527
9976
  if (prompted) return prompted;
9528
9977
  throw new CliError("Project name is required.", EXIT_CODES.VALIDATION, "Provide a name: donebear project add \"My project\"");
@@ -9958,7 +10407,7 @@ Examples:
9958
10407
  //#endregion
9959
10408
  //#region src/commands/spec.ts
9960
10409
  const OVERVIEW = {
9961
- authentication: "Set DONEBEAR_TOKEN env var, or run: donebear auth login",
10410
+ authentication: "Set DONEBEAR_TOKEN env var, run: donebear auth login, or create an account with donebear auth signup --email <human-email>",
9962
10411
  commands: {
9963
10412
  "api-key": {
9964
10413
  description: "Manage personal API keys",
@@ -9974,8 +10423,9 @@ const OVERVIEW = {
9974
10423
  quick_example: "donebear auth status --json",
9975
10424
  subcommands: [
9976
10425
  "login",
9977
- "status",
9978
- "logout"
10426
+ "logout",
10427
+ "signup",
10428
+ "status"
9979
10429
  ]
9980
10430
  },
9981
10431
  comment: {
@@ -10205,6 +10655,44 @@ const COMMAND_SPECS = {
10205
10655
  },
10206
10656
  usage: "donebear auth login [options]"
10207
10657
  },
10658
+ "auth signup": {
10659
+ command: "auth signup",
10660
+ examples: ["donebear auth signup --email person@example.com", "donebear auth signup --email person@example.com --json --no-interactive"],
10661
+ json_output: {
10662
+ apiKey: "{ id, key, prefix, scopes }",
10663
+ email: "string",
10664
+ instructions: "{ agent, human }",
10665
+ mcp: "{ url, headers, cursor, claude }",
10666
+ ok: "boolean",
10667
+ sessionStored: "boolean",
10668
+ status: "complete",
10669
+ user: "{ id, email }",
10670
+ verifyUrl: "string",
10671
+ workspace: "{ id, name, urlKey }"
10672
+ },
10673
+ 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.",
10674
+ 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.",
10675
+ options: {
10676
+ "--email": {
10677
+ description: "Human email to verify. Required. Do not invent an address.",
10678
+ type: "string"
10679
+ },
10680
+ "--name": {
10681
+ description: "Optional display name",
10682
+ type: "string"
10683
+ },
10684
+ "--no-interactive": {
10685
+ description: "Never prompt for the email code; poll the verify link instead",
10686
+ type: "boolean"
10687
+ },
10688
+ "--timeout": {
10689
+ default: 600,
10690
+ description: "Seconds to wait for email verification",
10691
+ type: "number"
10692
+ }
10693
+ },
10694
+ usage: "donebear auth signup [options]"
10695
+ },
10208
10696
  "auth status": {
10209
10697
  command: "auth status",
10210
10698
  examples: ["donebear auth status --json"],
@@ -10878,17 +11366,16 @@ const COMMAND_SPECS = {
10878
11366
  command: "task list",
10879
11367
  examples: [
10880
11368
  "donebear task list --json",
10881
- "donebear task list --state done --json",
11369
+ "donebear task list --view logbook --json",
10882
11370
  "donebear task list --view today --json",
10883
11371
  "donebear task list --search \"login\" --json"
10884
11372
  ],
10885
11373
  json_output: {
10886
11374
  autoSelected: "boolean",
10887
11375
  count: "number",
10888
- limited: "TaskRecord[]",
10889
- ok: "boolean",
10890
- search: "string | null",
10891
11376
  state: "string",
11377
+ tasks: "TaskRecord[]",
11378
+ total: "number",
10892
11379
  view: "string | null",
10893
11380
  workspace: "WorkspaceSummary"
10894
11381
  },
@@ -10913,13 +11400,15 @@ const COMMAND_SPECS = {
10913
11400
  type: "string"
10914
11401
  },
10915
11402
  "--view": {
10916
- description: "Filter by view bucket",
11403
+ description: "Filter by view bucket. logbook and trash widen the state to done and archived, so they read the completed and archived tasks whatever --state says; every other view reads --state, and pairing --state done with one of them matches nothing",
10917
11404
  enum: [
10918
11405
  "inbox",
10919
11406
  "anytime",
10920
11407
  "today",
10921
11408
  "upcoming",
10922
- "someday"
11409
+ "someday",
11410
+ "logbook",
11411
+ "trash"
10923
11412
  ],
10924
11413
  type: "string"
10925
11414
  },
@@ -11060,6 +11549,44 @@ const COMMAND_SPECS = {
11060
11549
  options: {},
11061
11550
  usage: "donebear whoami"
11062
11551
  },
11552
+ "workspace invite": {
11553
+ arguments: { workspace: {
11554
+ description: "Workspace id, urlKey, or name. Defaults to the current workspace.",
11555
+ required: false,
11556
+ type: "string"
11557
+ } },
11558
+ command: "workspace invite",
11559
+ examples: [
11560
+ "donebear workspace invite --email user@example.com --role member",
11561
+ "donebear workspace invite --share --copy",
11562
+ "donebear workspace invite --share --project <project-id>"
11563
+ ],
11564
+ json_output: {
11565
+ invitation: "WorkspaceInvitationRecord",
11566
+ workspace: "WorkspaceRecord"
11567
+ },
11568
+ notes: "Email invites are single-use. --share mints a reusable /join?code= link. --project requires --share.",
11569
+ options: {
11570
+ "--email": {
11571
+ description: "Email address to invite (single-use)",
11572
+ type: "string"
11573
+ },
11574
+ "--project": {
11575
+ description: "Scope a share link so joiners also become members of this project",
11576
+ type: "string"
11577
+ },
11578
+ "--role": {
11579
+ default: "member",
11580
+ description: "Role for an email invite: member|admin",
11581
+ type: "string"
11582
+ },
11583
+ "--share": {
11584
+ description: "Create a reusable join link instead of an email invite",
11585
+ type: "boolean"
11586
+ }
11587
+ },
11588
+ usage: "donebear workspace invite [workspace] [options]"
11589
+ },
11063
11590
  "workspace list": {
11064
11591
  command: "workspace list",
11065
11592
  examples: ["donebear workspace list --json"],
@@ -11102,10 +11629,11 @@ const TOP_LEVEL_SPECS = {
11102
11629
  auth: {
11103
11630
  command: "auth",
11104
11631
  description: "Authentication",
11105
- non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login.",
11632
+ non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login. New accounts: donebear auth signup --email <human-email> --json --no-interactive.",
11106
11633
  subcommands: {
11107
11634
  login: "OAuth login (Google or GitHub)",
11108
11635
  logout: "Sign out and remove stored session",
11636
+ signup: "Create an account from a human email (agent signup)",
11109
11637
  status: "Show auth status"
11110
11638
  },
11111
11639
  tip: "donebear spec auth login: get full spec for auth login"
@@ -11192,7 +11720,7 @@ const TOP_LEVEL_SPECS = {
11192
11720
  create: "Create a new workspace",
11193
11721
  current: "Show the active workspace",
11194
11722
  invitations: "List pending invitations",
11195
- invite: "Invite a user by email",
11723
+ invite: "Invite by email, or mint a reusable --share join link",
11196
11724
  join: "Join a workspace by invite",
11197
11725
  list: "List all workspaces",
11198
11726
  members: "List workspace members",
@@ -11240,6 +11768,7 @@ Examples:
11240
11768
  donebear spec project list Detailed spec for project list
11241
11769
  donebear spec import things Detailed spec for the Things importer
11242
11770
  donebear spec search Detailed spec for search
11771
+ donebear spec auth signup Agent signup from a human email
11243
11772
  donebear spec auth login Auth login options and non-interactive notes
11244
11773
  `).action(async (command, subcommand, _options, cmd) => {
11245
11774
  const context = contextFromCommand(cmd);
@@ -11698,10 +12227,14 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
11698
12227
  token: commandContext.token,
11699
12228
  workspaceRef: workspaceOverride ?? workspaceRefArg ?? null
11700
12229
  });
12230
+ if (options.share && options.email) throw new CliError("Use --share or --email, not both.", EXIT_CODES.VALIDATION);
12231
+ if (options.project && !options.share) throw new CliError("--project requires --share.", EXIT_CODES.VALIDATION);
11701
12232
  const invitation = await createWorkspaceInvitation(context, {
11702
12233
  baseUrl: commandContext.apiBaseUrl,
11703
12234
  email: options.email,
11704
- role: options.role ?? "member",
12235
+ kind: options.share ? "share" : void 0,
12236
+ projectId: options.project,
12237
+ role: options.share ? "member" : options.role ?? "member",
11705
12238
  token: commandContext.token,
11706
12239
  workspaceId: workspace.id
11707
12240
  });
@@ -11717,7 +12250,7 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
11717
12250
  writeInfo(context, `role: ${invitation.role}`);
11718
12251
  writeInfo(context, `expires: ${invitation.expiresAt}`);
11719
12252
  if (invitation.email) writeInfo(context, `email: ${invitation.email}`);
11720
- const link = `${commandContext.apiBaseUrl}/invite/${invitation.code}`;
12253
+ const link = `${DEFAULT_APP_URL}/join?code=${encodeURIComponent(invitation.code)}`;
11721
12254
  writeInfo(context, `link: ${link}`);
11722
12255
  if (context.copy) {
11723
12256
  if (copyToClipboard(link)) writeInfo(context, "Link copied to clipboard.");
@@ -11733,6 +12266,8 @@ Examples:
11733
12266
  donebear workspace members
11734
12267
  donebear workspace invitations
11735
12268
  donebear workspace invite --email user@example.com --role member
12269
+ donebear workspace invite --share --copy
12270
+ donebear workspace invite --share --project <project-id>
11736
12271
  `).action(async (_options, command) => {
11737
12272
  const context = contextFromCommand(command);
11738
12273
  await runWithErrorHandling(context, async () => {
@@ -11787,7 +12322,7 @@ Examples:
11787
12322
  await runWorkspaceInvitations(context, workspaceRef, workspaceOverride);
11788
12323
  });
11789
12324
  });
11790
- 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) => {
12325
+ 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) => {
11791
12326
  const context = contextFromCommand(command);
11792
12327
  await runWithErrorHandling(context, async () => {
11793
12328
  await runWorkspaceInvite(context, workspaceRef, workspaceOverride, options);
@@ -11815,8 +12350,9 @@ With auth:
11815
12350
  donebear import things --dry-run --json
11816
12351
 
11817
12352
  Auth:
11818
- donebear auth login OAuth browser login
11819
- DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
12353
+ donebear auth signup --email you@… Create an account (human verifies email)
12354
+ donebear auth login OAuth browser login
12355
+ DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
11820
12356
 
11821
12357
  Workspace targeting:
11822
12358
  donebear workspace=myorg task list