@hasna/todos 0.15.52 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +1884 -0
  2. package/README.md +314 -37
  3. package/dist/ai-tools.d.ts.map +1 -1
  4. package/dist/ai.d.ts.map +1 -1
  5. package/dist/cli/cloud-router.d.ts +99 -51
  6. package/dist/cli/cloud-router.d.ts.map +1 -1
  7. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  8. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  9. package/dist/cli/commands/delegate.d.ts.map +1 -1
  10. package/dist/cli/commands/environment-snapshots.d.ts.map +1 -1
  11. package/dist/cli/commands/machines-api.d.ts +4 -0
  12. package/dist/cli/commands/machines-api.d.ts.map +1 -0
  13. package/dist/cli/commands/machines.d.ts.map +1 -1
  14. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  15. package/dist/cli/commands/plan-commands.d.ts +3 -0
  16. package/dist/cli/commands/plan-commands.d.ts.map +1 -0
  17. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  18. package/dist/cli/commands/pr-group-commands.d.ts.map +1 -1
  19. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  20. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  21. package/dist/cli/commands/storage-commands.d.ts.map +1 -1
  22. package/dist/cli/commands/task-list-commands.d.ts +3 -0
  23. package/dist/cli/commands/task-list-commands.d.ts.map +1 -0
  24. package/dist/cli/commands/template-commands.d.ts +3 -0
  25. package/dist/cli/commands/template-commands.d.ts.map +1 -0
  26. package/dist/cli/helpers.d.ts.map +1 -1
  27. package/dist/cli/index.js +15128 -11093
  28. package/dist/cli/machine-api.d.ts +7 -0
  29. package/dist/cli/machine-api.d.ts.map +1 -0
  30. package/dist/cli/plan-read-api.d.ts +6 -0
  31. package/dist/cli/plan-read-api.d.ts.map +1 -0
  32. package/dist/cli/project-api.d.ts +119 -0
  33. package/dist/cli/project-api.d.ts.map +1 -0
  34. package/dist/cli/stage-a.d.ts +34 -4
  35. package/dist/cli/stage-a.d.ts.map +1 -1
  36. package/dist/cli/task-query-api.d.ts +21 -0
  37. package/dist/cli/task-query-api.d.ts.map +1 -0
  38. package/dist/cli/template-api.d.ts +7 -0
  39. package/dist/cli/template-api.d.ts.map +1 -0
  40. package/dist/contracts.js +835 -265
  41. package/dist/db/builtin-templates.d.ts +2 -33
  42. package/dist/db/builtin-templates.d.ts.map +1 -1
  43. package/dist/db/database.d.ts.map +1 -1
  44. package/dist/db/handoffs.d.ts.map +1 -1
  45. package/dist/db/machines.d.ts.map +1 -1
  46. package/dist/db/migrations.d.ts.map +1 -1
  47. package/dist/db/plans.d.ts.map +1 -1
  48. package/dist/db/retrospectives.d.ts.map +1 -1
  49. package/dist/db/schema.d.ts.map +1 -1
  50. package/dist/db/task-lists.d.ts.map +1 -1
  51. package/dist/db/task-runs.d.ts +2 -0
  52. package/dist/db/task-runs.d.ts.map +1 -1
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3862 -1715
  56. package/dist/lib/assignee-validation.d.ts.map +1 -1
  57. package/dist/lib/builtin-template-library.d.ts +32 -0
  58. package/dist/lib/builtin-template-library.d.ts.map +1 -0
  59. package/dist/lib/cli-reference.d.ts.map +1 -1
  60. package/dist/lib/config.d.ts +10 -13
  61. package/dist/lib/config.d.ts.map +1 -1
  62. package/dist/lib/db-backup.d.ts +1 -1
  63. package/dist/lib/db-backup.d.ts.map +1 -1
  64. package/dist/lib/env.d.ts +18 -0
  65. package/dist/lib/env.d.ts.map +1 -0
  66. package/dist/lib/environment-snapshots.d.ts.map +1 -1
  67. package/dist/lib/gatherer.d.ts.map +1 -1
  68. package/dist/lib/headless-boundaries.d.ts +4 -3
  69. package/dist/lib/headless-boundaries.d.ts.map +1 -1
  70. package/dist/lib/local-encryption.d.ts +12 -0
  71. package/dist/lib/local-encryption.d.ts.map +1 -1
  72. package/dist/lib/local-opt-in.d.ts +90 -0
  73. package/dist/lib/local-opt-in.d.ts.map +1 -0
  74. package/dist/lib/paths.d.ts +9 -0
  75. package/dist/lib/paths.d.ts.map +1 -1
  76. package/dist/lib/plan-artifacts.d.ts +1 -0
  77. package/dist/lib/plan-artifacts.d.ts.map +1 -1
  78. package/dist/lib/plan-client-boundary.d.ts +2 -0
  79. package/dist/lib/plan-client-boundary.d.ts.map +1 -0
  80. package/dist/lib/plan-schedule.d.ts +6 -0
  81. package/dist/lib/plan-schedule.d.ts.map +1 -0
  82. package/dist/lib/project-panel.d.ts +6 -0
  83. package/dist/lib/project-panel.d.ts.map +1 -1
  84. package/dist/lib/project-receipt.d.ts +3 -0
  85. package/dist/lib/project-receipt.d.ts.map +1 -0
  86. package/dist/lib/sandbox-profiles.d.ts.map +1 -1
  87. package/dist/lib/shared-plan-artifacts.d.ts +14 -0
  88. package/dist/lib/shared-plan-artifacts.d.ts.map +1 -0
  89. package/dist/lib/sync.d.ts.map +1 -1
  90. package/dist/lib/task-list-client-boundary.d.ts +3 -0
  91. package/dist/lib/task-list-client-boundary.d.ts.map +1 -0
  92. package/dist/lib/task-routing.d.ts.map +1 -1
  93. package/dist/lib/template-client-boundary.d.ts +2 -0
  94. package/dist/lib/template-client-boundary.d.ts.map +1 -0
  95. package/dist/mcp/index.d.ts +1 -0
  96. package/dist/mcp/index.d.ts.map +1 -1
  97. package/dist/mcp/index.js +45271 -40452
  98. package/dist/mcp/plan-api.d.ts +6 -0
  99. package/dist/mcp/plan-api.d.ts.map +1 -0
  100. package/dist/mcp/remote-authority.d.ts +49 -0
  101. package/dist/mcp/remote-authority.d.ts.map +1 -0
  102. package/dist/mcp/task-coordination-api.d.ts +15 -0
  103. package/dist/mcp/task-coordination-api.d.ts.map +1 -0
  104. package/dist/mcp/task-list-api.d.ts +8 -0
  105. package/dist/mcp/task-list-api.d.ts.map +1 -0
  106. package/dist/mcp/token-utils.d.ts.map +1 -1
  107. package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -1
  108. package/dist/mcp/tools/machines.d.ts.map +1 -1
  109. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  110. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  111. package/dist/mcp.d.ts +2 -2
  112. package/dist/mcp.d.ts.map +1 -1
  113. package/dist/mcp.js +44 -18
  114. package/dist/project-registration.js +2829 -556
  115. package/dist/registry.js +836 -266
  116. package/dist/release-provenance.json +6 -6
  117. package/dist/sdk/client.d.ts +51 -4
  118. package/dist/sdk/client.d.ts.map +1 -1
  119. package/dist/sdk/index.d.ts +2 -0
  120. package/dist/sdk/index.d.ts.map +1 -1
  121. package/dist/sdk/index.js +1209 -1080
  122. package/dist/sdk/resolve.d.ts +91 -0
  123. package/dist/sdk/resolve.d.ts.map +1 -0
  124. package/dist/sdk/v1.generated.d.ts +219 -3
  125. package/dist/sdk/v1.generated.d.ts.map +1 -1
  126. package/dist/server/auth-posture.d.ts +66 -7
  127. package/dist/server/auth-posture.d.ts.map +1 -1
  128. package/dist/server/client-ip.d.ts +39 -0
  129. package/dist/server/client-ip.d.ts.map +1 -0
  130. package/dist/server/cloud.d.ts +2 -1
  131. package/dist/server/cloud.d.ts.map +1 -1
  132. package/dist/server/index.d.ts +1 -1
  133. package/dist/server/index.js +20556 -15541
  134. package/dist/server/openapi.d.ts +1341 -83
  135. package/dist/server/openapi.d.ts.map +1 -1
  136. package/dist/server/port.d.ts +2 -2
  137. package/dist/server/port.d.ts.map +1 -1
  138. package/dist/server/routes.d.ts +3 -6
  139. package/dist/server/routes.d.ts.map +1 -1
  140. package/dist/server/serve.d.ts +2 -6
  141. package/dist/server/serve.d.ts.map +1 -1
  142. package/dist/server/v1.d.ts +3 -1
  143. package/dist/server/v1.d.ts.map +1 -1
  144. package/dist/storage/atomic-project-migration.d.ts +53 -0
  145. package/dist/storage/atomic-project-migration.d.ts.map +1 -0
  146. package/dist/storage/index.d.ts +3 -1
  147. package/dist/storage/index.d.ts.map +1 -1
  148. package/dist/storage/interfaces.d.ts +61 -0
  149. package/dist/storage/interfaces.d.ts.map +1 -1
  150. package/dist/storage/machine-registry.d.ts +36 -0
  151. package/dist/storage/machine-registry.d.ts.map +1 -0
  152. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  153. package/dist/storage/postgres-sync.d.ts +2 -2
  154. package/dist/storage/postgres-sync.d.ts.map +1 -1
  155. package/dist/storage/run-artifact-creation-upload.d.ts +50 -0
  156. package/dist/storage/run-artifact-creation-upload.d.ts.map +1 -0
  157. package/dist/storage/s3-artifact-sync.d.ts +18 -0
  158. package/dist/storage/s3-artifact-sync.d.ts.map +1 -1
  159. package/dist/storage/sqlite-snapshot.d.ts +2 -0
  160. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  161. package/dist/storage.d.ts +2 -2
  162. package/dist/storage.d.ts.map +1 -1
  163. package/dist/storage.js +2695 -322
  164. package/dist/task-manifest/postgres.d.ts +1 -1
  165. package/dist/task-manifest/sqlite.d.ts +1 -1
  166. package/dist/task-manifest.js +244 -73
  167. package/dist/task-subtree-transfer.js +198 -4
  168. package/dist/testing.d.ts +90 -29
  169. package/dist/testing.d.ts.map +1 -1
  170. package/dist/testing.js +34 -10
  171. package/dist/types/index.d.ts +40 -4
  172. package/dist/types/index.d.ts.map +1 -1
  173. package/docs/PLAN_API.md +93 -0
  174. package/docs/TASK_LIST_API.md +58 -0
  175. package/docs/TASK_QUERY_API.md +14 -0
  176. package/docs/TEMPLATE_API.md +18 -0
  177. package/docs/native-storage.md +405 -0
  178. package/package.json +18 -15
  179. package/postinstall.js +52 -1
  180. package/dashboard/dist/assets/index-BNQ4gJua.js +0 -342
  181. package/dashboard/dist/assets/index-DjzvHUWt.css +0 -1
  182. package/dashboard/dist/index.html +0 -13
  183. package/dashboard/dist/logo.jpg +0 -0
@@ -13,7 +13,7 @@ export declare class PostgresTodosTaskManifestBackend implements TodosTaskManife
13
13
  private persistTerminal;
14
14
  apply(input: NormalizedTaskManifest, faults: PreparedTaskManifestFaults): Promise<TodosTaskManifestApplyResult>;
15
15
  readExact(receiptId: string): Promise<TodosTaskManifestApplyResult>;
16
- lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "schema_version" | "authority" | "route" | "tenant_id">>;
16
+ lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "authority" | "route" | "tenant_id" | "schema_version">>;
17
17
  markOutboxDelivered(outboxId: string, deliveredAt: string): Promise<void>;
18
18
  compensate(input: TodosTaskManifestCompensateRequest, receipt: TodosTaskManifestReceipt, compensationReceiptId: string, requestDigest: string, now: string): Promise<TodosTaskManifestCompensationResult>;
19
19
  private readback;
@@ -10,7 +10,7 @@ export declare class SqliteTodosTaskManifestBackend implements TodosTaskManifest
10
10
  apply(input: NormalizedTaskManifest, faults: PreparedTaskManifestFaults): Promise<TodosTaskManifestApplyResult>;
11
11
  private persistTerminal;
12
12
  readExact(receiptId: string): Promise<TodosTaskManifestApplyResult>;
13
- lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "schema_version" | "authority" | "route" | "tenant_id">>;
13
+ lookupBindingByPlanId(planId: string): Promise<Omit<import("./types.js").TodosTaskManifestBindingLookupResult, "authority" | "route" | "tenant_id" | "schema_version">>;
14
14
  markOutboxDelivered(outboxId: string, deliveredAt: string): Promise<void>;
15
15
  compensate(input: TodosTaskManifestCompensateRequest, receipt: TodosTaskManifestReceipt, compensationReceiptId: string, requestDigest: string, now: string): Promise<TodosTaskManifestCompensationResult>;
16
16
  private readback;
@@ -46,7 +46,7 @@ function isBlockingDependencyStatus(status) {
46
46
  function isTerminalStatus(status) {
47
47
  return status === "completed" || status === "failed" || status === "cancelled";
48
48
  }
49
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
49
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError, InputValidationError;
50
50
  var init_types = __esm(() => {
51
51
  TASK_STATUSES = [
52
52
  "pending",
@@ -61,7 +61,7 @@ var init_types = __esm(() => {
61
61
  "high",
62
62
  "critical"
63
63
  ];
64
- PLAN_STATUSES = ["active", "completed", "archived"];
64
+ PLAN_STATUSES = ["active", "completed", "archived", "planning", "cancelled"];
65
65
  VersionConflictError = class VersionConflictError extends Error {
66
66
  taskId;
67
67
  expectedVersion;
@@ -250,6 +250,16 @@ var init_types = __esm(() => {
250
250
  this.name = "DispatchNotFoundError";
251
251
  }
252
252
  };
253
+ InputValidationError = class InputValidationError extends Error {
254
+ static code = "INVALID_INPUT";
255
+ code = InputValidationError.code;
256
+ suggestion;
257
+ constructor(message, suggestion) {
258
+ super(message);
259
+ this.name = "InputValidationError";
260
+ this.suggestion = suggestion;
261
+ }
262
+ };
253
263
  });
254
264
 
255
265
  // src/lib/slugs.ts
@@ -323,83 +333,68 @@ function validateSnapshotRoutingDestinationConflicts(projects, taskLists, existi
323
333
  return errors2;
324
334
  }
325
335
 
326
- // node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
336
+ // src/lib/paths.ts
337
+ import { existsSync } from "fs";
327
338
  import { homedir } from "os";
328
- import { join } from "path";
329
- function assertApp(app) {
339
+ import { join, resolve } from "path";
340
+ import { homedir as pathsResolverHomedir } from "os";
341
+ import { join as pathsResolverJoin } from "path";
342
+ function pathsResolverAssertApp(app) {
330
343
  if (typeof app !== "string" || app.length === 0) {
331
344
  throw new TypeError("paths: app must be a non-empty string");
332
345
  }
333
- if (!APP_SLUG_RE.test(app)) {
346
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
334
347
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
335
348
  }
336
349
  }
337
- function envOf(options) {
338
- return options.env ?? process.env;
339
- }
340
- function envValue(options, kind) {
341
- const value = envOf(options)[KIND_ENV[kind]];
342
- return typeof value === "string" && value.length > 0 ? value : undefined;
343
- }
344
- function isMacOS(platform) {
345
- return platform === "darwin";
350
+ function pathsResolverAssertKind(kind) {
351
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
352
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
353
+ }
346
354
  }
347
- function baseDir(kind, options) {
348
- const override = envValue(options, kind);
349
- if (override)
355
+ function pathsResolverBaseDir(kind, options) {
356
+ pathsResolverAssertKind(kind);
357
+ const env = options.env ?? process.env;
358
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
359
+ if (typeof override === "string" && override.length > 0)
350
360
  return override;
351
- const home = options.home ?? homedir();
361
+ const home = options.home ?? pathsResolverHomedir();
352
362
  const platform = options.platform ?? process.platform;
353
- if (isMacOS(platform)) {
363
+ if (platform === "darwin") {
354
364
  switch (kind) {
355
365
  case "config":
356
366
  case "data":
357
- return join(home, "Library", "Application Support", "Hasna");
367
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
358
368
  case "cache":
359
- return join(home, "Library", "Caches", "Hasna");
369
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
360
370
  case "state":
361
- return join(home, "Library", "Logs", "Hasna");
371
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
362
372
  }
363
373
  }
364
374
  switch (kind) {
365
375
  case "config":
366
- return join(home, ".config", "hasna");
376
+ return pathsResolverJoin(home, ".config", "hasna");
367
377
  case "data":
368
- return join(home, ".local", "share", "hasna");
378
+ return pathsResolverJoin(home, ".local", "share", "hasna");
369
379
  case "state":
370
- return join(home, ".local", "state", "hasna");
380
+ return pathsResolverJoin(home, ".local", "state", "hasna");
371
381
  case "cache":
372
- return join(home, ".cache", "hasna");
382
+ return pathsResolverJoin(home, ".cache", "hasna");
373
383
  }
374
384
  }
375
- function resolvePath(kind, options) {
376
- assertApp(options.app);
377
- const appSegment = options.internal === true ? join("internal", options.app) : options.app;
378
- return join(baseDir(kind, options), appSegment);
385
+ function pathsResolverResolve(kind, options) {
386
+ pathsResolverAssertApp(options.app);
387
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
388
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
379
389
  }
380
390
  function dataDir(options) {
381
- return resolvePath("data", options);
391
+ return pathsResolverResolve("data", options);
382
392
  }
383
- var KIND_ENV, APP_SLUG_RE;
384
- var init_dist = __esm(() => {
385
- KIND_ENV = {
386
- config: "HASNA_CONFIG_HOME",
387
- data: "HASNA_DATA_HOME",
388
- state: "HASNA_STATE_HOME",
389
- cache: "HASNA_CACHE_HOME"
390
- };
391
- APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
392
- });
393
-
394
- // src/lib/paths.ts
395
- import { existsSync } from "fs";
396
- import { homedir as homedir2 } from "os";
397
- import { join as join2, resolve } from "path";
398
393
  function effectiveHome(env = process.env) {
399
- return env.HOME || env.USERPROFILE || homedir2();
394
+ return env.HOME || env.USERPROFILE || homedir();
400
395
  }
401
396
  function legacyHomeDir(env = process.env) {
402
- return join2(effectiveHome(env), ".hasna", "todos");
397
+ return join(effectiveHome(env), ".hasna", "todos");
403
398
  }
404
399
  function resolverHome(env = process.env) {
405
400
  return dataDir({ app: "todos", home: effectiveHome(env), env });
@@ -408,23 +403,30 @@ function adoptResolverHome(resolved, env = process.env) {
408
403
  const dataOverride = env.HASNA_DATA_HOME;
409
404
  if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
410
405
  return true;
411
- return existsSync(join2(resolved, "todos.db")) || existsSync(join2(resolved, "config.json"));
406
+ return existsSync(join(resolved, "todos.db")) || existsSync(join(resolved, "config.json"));
412
407
  }
413
408
  function getTodosDir(env = process.env) {
414
409
  const resolved = resolverHome(env);
415
410
  return resolve(adoptResolverHome(resolved, env) ? resolved : legacyHomeDir(env));
416
411
  }
412
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
417
413
  var init_paths = __esm(() => {
418
- init_dist();
414
+ PATHS_RESOLVER_KIND_ENV = {
415
+ config: "HASNA_CONFIG_HOME",
416
+ data: "HASNA_DATA_HOME",
417
+ state: "HASNA_STATE_HOME",
418
+ cache: "HASNA_CACHE_HOME"
419
+ };
420
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
419
421
  });
420
422
 
421
423
  // src/lib/sync-utils.ts
422
424
  import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
423
425
  import { createHash as createHash2 } from "crypto";
424
- import { homedir as homedir3 } from "os";
425
- import { join as join3 } from "path";
426
+ import { homedir as homedir2 } from "os";
427
+ import { join as join2 } from "path";
426
428
  function getHomeDir() {
427
- return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
429
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
428
430
  }
429
431
  function getTodosGlobalDir() {
430
432
  return getTodosDir();
@@ -450,14 +452,14 @@ function writeJsonFile(path, data) {
450
452
  `);
451
453
  }
452
454
  function readHighWaterMark(dir) {
453
- const path = join3(dir, ".highwatermark");
455
+ const path = join2(dir, ".highwatermark");
454
456
  if (!existsSync2(path))
455
457
  return 1;
456
458
  const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
457
459
  return isNaN(val) ? 1 : val;
458
460
  }
459
461
  function writeHighWaterMark(dir, value) {
460
- writeFileSync(join3(dir, ".highwatermark"), String(value));
462
+ writeFileSync(join2(dir, ".highwatermark"), String(value));
461
463
  }
462
464
  function getFileMtimeMs(path) {
463
465
  try {
@@ -518,9 +520,9 @@ var init_sync_utils = __esm(() => {
518
520
 
519
521
  // src/lib/config.ts
520
522
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
521
- import { dirname, join as join4 } from "path";
523
+ import { dirname, join as join3 } from "path";
522
524
  function getConfigPath() {
523
- return join4(getTodosGlobalDir(), "config.json");
525
+ return join3(getTodosGlobalDir(), "config.json");
524
526
  }
525
527
  function normalizeAgent(agent) {
526
528
  return agent.trim().toLowerCase();
@@ -571,21 +573,6 @@ function normalizeApiUrl(value) {
571
573
  return null;
572
574
  return trimmed.replace(/\/+$/, "");
573
575
  }
574
- function getLocalApiConfig(env = process.env) {
575
- const config = loadConfig();
576
- const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
577
- const configApiUrl = normalizeApiUrl(config.apiUrl);
578
- const apiUrl = envApiUrl ?? configApiUrl;
579
- const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
580
- return {
581
- apiUrl,
582
- apiKey,
583
- source: {
584
- apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
585
- apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
586
- }
587
- };
588
- }
589
576
  function getSyncAgentsFromConfig() {
590
577
  const config = loadConfig();
591
578
  const agents = config.sync_agents;
@@ -6108,6 +6095,171 @@ class SqliteTodosTaskManifestBackend {
6108
6095
  }
6109
6096
  }
6110
6097
 
6098
+ // src/storage/machine-registry.ts
6099
+ import { randomUUID } from "crypto";
6100
+ import { isDeepStrictEqual } from "util";
6101
+ class MachineRegistryError extends Error {
6102
+ status;
6103
+ constructor(message, status = 409) {
6104
+ super(message);
6105
+ this.status = status;
6106
+ }
6107
+ }
6108
+ var fields = new Set(["id", "name", "hostname", "platform", "last_seen_at", "metadata", "created_at", "ssh_address", "is_primary", "archived_at"]);
6109
+ function validateMachines(value) {
6110
+ if (!Array.isArray(value) || value.length > 1e4)
6111
+ throw new MachineRegistryError("machines must be an array of at most 10000 complete records", 400);
6112
+ for (const row of value) {
6113
+ if (!row || typeof row !== "object" || Array.isArray(row) || Object.keys(row).some((key2) => !fields.has(key2)))
6114
+ throw new MachineRegistryError("Invalid machine record fields", 400);
6115
+ for (const key2 of ["id", "name", "last_seen_at", "created_at"])
6116
+ if (typeof row[key2] !== "string" || !row[key2].trim() || row[key2].length > 1024)
6117
+ throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
6118
+ for (const key2 of ["hostname", "platform", "ssh_address", "archived_at"])
6119
+ if (row[key2] !== null && typeof row[key2] !== "string")
6120
+ throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
6121
+ for (const key2 of ["last_seen_at", "created_at", "archived_at"])
6122
+ if (row[key2] !== null && !Number.isFinite(Date.parse(row[key2])))
6123
+ throw new MachineRegistryError(`Invalid machine ${key2}`, 400);
6124
+ if (typeof row.is_primary !== "boolean" || !row.metadata || typeof row.metadata !== "object" || Array.isArray(row.metadata))
6125
+ throw new MachineRegistryError("Invalid machine flags or metadata", 400);
6126
+ if (!isDeepStrictEqual(JSON.parse(JSON.stringify(row.metadata)), row.metadata))
6127
+ throw new MachineRegistryError("Machine metadata must preserve JSON values exactly", 400);
6128
+ if (JSON.stringify(row.metadata).length > 262144)
6129
+ throw new MachineRegistryError("Machine metadata is too large", 400);
6130
+ if (row.is_primary && row.archived_at !== null)
6131
+ throw new MachineRegistryError("An archived machine cannot be primary", 400);
6132
+ }
6133
+ return value;
6134
+ }
6135
+ function planMachineImport(existing, incoming) {
6136
+ const rows = validateMachines(incoming);
6137
+ const byId = new Map(existing.map((row) => [row.id, row]));
6138
+ const byName = new Map(existing.map((row) => [row.name, row.id]));
6139
+ const insert = [];
6140
+ let skipped = 0;
6141
+ for (const row of rows) {
6142
+ const prior = byId.get(row.id);
6143
+ if (prior && !isDeepStrictEqual(prior, row))
6144
+ throw new MachineRegistryError("Machine migration conflicts with an existing identity; reconcile before retrying");
6145
+ if (byName.has(row.name) && byName.get(row.name) !== row.id)
6146
+ throw new MachineRegistryError("Machine name belongs to a different identity; migration was not applied");
6147
+ if (prior)
6148
+ skipped++;
6149
+ else {
6150
+ insert.push(row);
6151
+ byId.set(row.id, row);
6152
+ byName.set(row.name, row.id);
6153
+ }
6154
+ }
6155
+ if ([...byId.values()].filter((row) => row.is_primary).length > 1)
6156
+ throw new MachineRegistryError("Machine migration would create multiple primary machines");
6157
+ return { rows: insert, skipped };
6158
+ }
6159
+ function createPostgresMachineRegistry(client, service, table, ensure) {
6160
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table))
6161
+ throw new Error("Invalid machine registry table");
6162
+ async function list(connection = client) {
6163
+ await ensure();
6164
+ const result = await connection.query(`SELECT payload FROM ${table} WHERE service=$1 AND object_type='machines' AND deleted_at IS NULL ORDER BY object_id`, [service]);
6165
+ return validateMachines(result.rows.map((row) => typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload));
6166
+ }
6167
+ return { list, async execute(input) {
6168
+ if (!input || typeof input !== "object" || Array.isArray(input) || !["register", "heartbeat", "set-primary", "archive", "unarchive", "delete", "import"].includes(input.action))
6169
+ throw new MachineRegistryError("Invalid machine operation", 400);
6170
+ if (Object.keys(input).some((key2) => !["action", "name", "id", "options", "machines"].includes(key2)))
6171
+ throw new MachineRegistryError("Unknown machine operation fields", 400);
6172
+ if (input.options !== undefined && (!input.options || typeof input.options !== "object" || Array.isArray(input.options)))
6173
+ throw new MachineRegistryError("Invalid machine options", 400);
6174
+ await ensure();
6175
+ if (!client.transaction)
6176
+ throw new MachineRegistryError("Machine registry requires a transactional server backend", 501);
6177
+ return client.transaction(async (tx) => {
6178
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${service}:machine-registry`]);
6179
+ const current = await list(tx);
6180
+ let changed = [];
6181
+ let skipped = 0;
6182
+ let machine;
6183
+ let deleted;
6184
+ const stamp = new Date().toISOString();
6185
+ if (input.action === "import") {
6186
+ const plan = planMachineImport(current, input.machines);
6187
+ const ids = plan.rows.map((row) => row.id);
6188
+ if (ids.length) {
6189
+ const retired = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type='machines' AND object_id IN (SELECT jsonb_array_elements_text($2::text::jsonb)) AND deleted_at IS NOT NULL`, [service, JSON.stringify(ids)]);
6190
+ if (retired.rows.length)
6191
+ throw new MachineRegistryError("Machine migration contains a retired identity");
6192
+ }
6193
+ changed = plan.rows;
6194
+ skipped = plan.skipped;
6195
+ } else {
6196
+ if (typeof input.name !== "string" || !input.name.trim() || input.name.length > 1024)
6197
+ throw new MachineRegistryError("Machine name or ID is required", 400);
6198
+ const matches = current.filter((row) => row.name === input.name || row.id === input.name);
6199
+ if (matches.length > 1)
6200
+ throw new MachineRegistryError("Machine selector is ambiguous between a name and another identity; use an unambiguous selector");
6201
+ const prior = matches[0];
6202
+ if (input.action === "register" || input.action === "heartbeat") {
6203
+ const opts = input.options ?? {};
6204
+ const allowed = new Set(["hostname", "platform", "ssh_address", "primary", "tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"]);
6205
+ for (const [key2, value] of Object.entries(opts)) {
6206
+ if (!allowed.has(key2) || (key2 === "primary" ? typeof value !== "boolean" : typeof value !== "string" || value.length > 4096))
6207
+ throw new MachineRegistryError("Invalid machine registration options", 400);
6208
+ }
6209
+ if (input.id !== undefined && (typeof input.id !== "string" || !input.id.trim() || input.id.length > 1024))
6210
+ throw new MachineRegistryError("Invalid stable machine ID", 400);
6211
+ if (prior && input.id && input.id !== prior.id)
6212
+ throw new MachineRegistryError("Machine registration ID does not match existing identity");
6213
+ if (!prior && input.id && current.some((row) => row.id === input.id))
6214
+ throw new MachineRegistryError("Machine ID already belongs to another name");
6215
+ const metadata = { ...prior?.metadata };
6216
+ for (const key2 of ["tailscale_name", "tailscale_ip", "lan_address", "workspace_path", "git_root", "arch"])
6217
+ if (opts[key2] !== undefined)
6218
+ metadata[key2] = opts[key2];
6219
+ if (!prior && input.id) {
6220
+ const retired = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type='machines' AND object_id=$2 AND deleted_at IS NOT NULL`, [service, input.id]);
6221
+ if (retired.rows.length)
6222
+ throw new MachineRegistryError("Machine identity has been retired");
6223
+ }
6224
+ machine = { id: prior?.id ?? input.id ?? randomUUID(), name: prior?.name ?? input.name, hostname: opts.hostname ?? prior?.hostname ?? null, platform: opts.platform ?? prior?.platform ?? null, ssh_address: opts.ssh_address ?? prior?.ssh_address ?? null, created_at: prior?.created_at ?? stamp, last_seen_at: stamp, archived_at: prior?.archived_at ?? null, is_primary: opts.primary === true || (prior?.is_primary ?? false), metadata };
6225
+ if (machine.archived_at && machine.is_primary)
6226
+ throw new MachineRegistryError("Cannot set archived machine as primary");
6227
+ changed = [machine];
6228
+ if (opts.primary === true)
6229
+ changed.push(...current.filter((row) => row.is_primary && row.id !== machine.id).map((row) => ({ ...row, is_primary: false })));
6230
+ } else {
6231
+ if (!prior)
6232
+ throw new MachineRegistryError("Machine not found", 404);
6233
+ machine = { ...prior };
6234
+ if (input.action === "set-primary") {
6235
+ if (prior.archived_at)
6236
+ throw new MachineRegistryError("Cannot set archived machine as primary");
6237
+ machine.is_primary = true;
6238
+ changed = current.filter((row) => row.is_primary && row.id !== prior.id).map((row) => ({ ...row, is_primary: false }));
6239
+ } else if (input.action === "archive" || input.action === "delete") {
6240
+ if (prior.is_primary)
6241
+ throw new MachineRegistryError("Cannot archive or delete the primary machine");
6242
+ const refs = await tx.query(`SELECT object_id FROM ${table} WHERE service=$1 AND object_type<>'machines' AND deleted_at IS NULL AND payload->>'machine_id'=$2 AND ($3='delete' OR (object_type='tasks' AND payload->>'status' IN ('pending','in_progress'))) LIMIT 1`, [service, prior.id, input.action]);
6243
+ if (refs.rows.length)
6244
+ throw new MachineRegistryError("Machine still has referenced records or active tasks");
6245
+ machine.archived_at = stamp;
6246
+ deleted = input.action === "delete" ? true : undefined;
6247
+ } else if (input.action === "unarchive")
6248
+ machine.archived_at = null;
6249
+ else
6250
+ throw new MachineRegistryError("Unknown machine action", 400);
6251
+ changed.push(machine);
6252
+ }
6253
+ }
6254
+ for (const row of changed) {
6255
+ await tx.query(`INSERT INTO ${table} (service,object_type,object_id,payload,updated_at,deleted_at,version) VALUES ($1,'machines',$2,$3::text::jsonb,$4,$5,1) ON CONFLICT (service,object_type,object_id) DO UPDATE SET payload=EXCLUDED.payload,updated_at=EXCLUDED.updated_at,deleted_at=EXCLUDED.deleted_at,version=COALESCE(${table}.version,0)+1`, [service, row.id, JSON.stringify(row), stamp, deleted && row.id === machine?.id ? stamp : null]);
6256
+ }
6257
+ const machines = await list(tx);
6258
+ return { schema_version: 2, machines, ...machine ? { machine } : {}, inserted: changed.filter((row) => !current.some((old) => old.id === row.id)).length, skipped, ...deleted ? { deleted } : {} };
6259
+ });
6260
+ } };
6261
+ }
6262
+
6111
6263
  // src/storage/postgres-sync.ts
6112
6264
  init_types();
6113
6265
 
@@ -6330,6 +6482,10 @@ class PostgresTodosSyncStore {
6330
6482
  }
6331
6483
  }
6332
6484
  async pushSnapshot(snapshot, context = {}) {
6485
+ if ((snapshot.tombstones ?? []).some((row) => row.object_type === "machines"))
6486
+ throw new Error("Machine tombstones require explicit registry lifecycle operations");
6487
+ if (snapshot.machines !== undefined)
6488
+ validateMachines(snapshot.machines);
6333
6489
  const routingErrors = validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists);
6334
6490
  if (routingErrors.length > 0) {
6335
6491
  throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
@@ -6352,6 +6508,14 @@ class PostgresTodosSyncStore {
6352
6508
  throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
6353
6509
  }
6354
6510
  const result = { records: 0, objectTypes: {} };
6511
+ if (snapshot.machines?.length) {
6512
+ if (!this.client.transaction)
6513
+ throw new Error("Machine snapshot migration requires transaction(callback)");
6514
+ const registry = createPostgresMachineRegistry({ query: client.query.bind(client), transaction: (fn) => fn(client) }, this.service, this.tableName, async () => {});
6515
+ const receipt = await registry.execute({ action: "import", machines: snapshot.machines });
6516
+ result.records += receipt.inserted + receipt.skipped;
6517
+ result.objectTypes.machines = receipt.inserted + receipt.skipped;
6518
+ }
6355
6519
  const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
6356
6520
  for (const entry of snapshotEntries(snapshot)) {
6357
6521
  if (entry.deletedAt === null)
@@ -6515,6 +6679,7 @@ function rowsToSnapshot(rows) {
6515
6679
  tasks: [],
6516
6680
  projects: [],
6517
6681
  projectMachinePaths: [],
6682
+ machines: [],
6518
6683
  plans: [],
6519
6684
  agents: [],
6520
6685
  taskLists: [],
@@ -6526,6 +6691,12 @@ function rowsToSnapshot(rows) {
6526
6691
  for (const row of rows) {
6527
6692
  const payload = payloadRecord(row.payload);
6528
6693
  const deletedAt = stringValue(row.deleted_at);
6694
+ if (row.object_type === "machines") {
6695
+ if (deletedAt)
6696
+ throw new Error("Snapshot contains retired machine identities; use the authoritative registry instead of silently discarding tombstones");
6697
+ snapshot.machines.push(payload);
6698
+ continue;
6699
+ }
6529
6700
  if (deletedAt) {
6530
6701
  snapshot.tombstones ??= [];
6531
6702
  snapshot.tombstones.push({