@hasna/todos 0.15.20 → 0.15.24

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 (40) hide show
  1. package/dist/ai-tools.d.ts +80 -0
  2. package/dist/ai-tools.d.ts.map +1 -0
  3. package/dist/ai.d.ts +313 -0
  4. package/dist/ai.d.ts.map +1 -0
  5. package/dist/cli/cloud-router.d.ts +10 -1
  6. package/dist/cli/cloud-router.d.ts.map +1 -1
  7. package/dist/cli/commands/ai-commands.d.ts +3 -0
  8. package/dist/cli/commands/ai-commands.d.ts.map +1 -0
  9. package/dist/cli/commands/help-commands.d.ts +2 -1
  10. package/dist/cli/commands/help-commands.d.ts.map +1 -1
  11. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  12. package/dist/cli/index.js +2856 -547
  13. package/dist/cli/stage-a.d.ts +27 -16
  14. package/dist/cli/stage-a.d.ts.map +1 -1
  15. package/dist/contracts.d.ts +1 -0
  16. package/dist/contracts.d.ts.map +1 -1
  17. package/dist/contracts.js +638 -18
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3142 -1089
  21. package/dist/lib/cli-help.d.ts +2 -2
  22. package/dist/lib/config.d.ts +4 -0
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/saved-search-views.d.ts.map +1 -1
  25. package/dist/lib/stale-lock-handoff.d.ts.map +1 -1
  26. package/dist/mcp/index.js +127 -72
  27. package/dist/mcp.js +1 -1
  28. package/dist/project-registration.js +67 -29
  29. package/dist/registry.js +603 -18
  30. package/dist/release-provenance.json +5 -5
  31. package/dist/sdk/index.js +1 -1
  32. package/dist/sdk/v1.generated.d.ts +2 -0
  33. package/dist/sdk/v1.generated.d.ts.map +1 -1
  34. package/dist/server/index.js +133 -78
  35. package/dist/server/openapi.d.ts +7 -0
  36. package/dist/server/openapi.d.ts.map +1 -1
  37. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  38. package/dist/storage.js +60 -20
  39. package/dist/task-manifest.js +19 -3
  40. package/package.json +1 -1
@@ -429,7 +429,7 @@ var init_creator_identity = __esm(() => {
429
429
  });
430
430
 
431
431
  // src/lib/config.ts
432
- import { existsSync as existsSync2 } from "fs";
432
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
433
433
  import { dirname, join as join2 } from "path";
434
434
  function getConfigPath() {
435
435
  return join2(getTodosGlobalDir(), "config.json");
@@ -461,6 +461,22 @@ function saveConfig(config) {
461
461
  function updateConfig(patch) {
462
462
  return saveConfig({ ...loadConfig(), ...patch });
463
463
  }
464
+ function getTodosAiConfig() {
465
+ const configPath = getConfigPath();
466
+ if (!existsSync2(configPath))
467
+ return {};
468
+ const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
469
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
470
+ throw new Error("Todos config must be a JSON object");
471
+ }
472
+ const ai = parsed["ai"];
473
+ if (ai === undefined)
474
+ return {};
475
+ if (ai === null || typeof ai !== "object" || Array.isArray(ai)) {
476
+ throw new Error("Todos AI configuration must be an object");
477
+ }
478
+ return { ...ai };
479
+ }
464
480
  function normalizeApiUrl(value) {
465
481
  const trimmed = value?.trim();
466
482
  if (!trimmed)
@@ -635,7 +651,7 @@ var init_redaction = __esm(() => {
635
651
  });
636
652
 
637
653
  // src/lib/secret-redaction.ts
638
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
654
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
639
655
  function registerCustomRedactor(fn) {
640
656
  customRedactors.push(fn);
641
657
  }
@@ -702,7 +718,7 @@ function scanAndRedactText(text, options = {}) {
702
718
  function scanFileForSecrets(path, options = {}) {
703
719
  if (!existsSync3(path))
704
720
  throw new Error(`File not found: ${path}`);
705
- const content = readFileSync2(path, "utf8");
721
+ const content = readFileSync3(path, "utf8");
706
722
  return scanAndRedactText(content, options);
707
723
  }
708
724
  function safeStringify(value, space) {
@@ -885,7 +901,7 @@ function requireCanonicalLockVersion(value) {
885
901
  throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must be the exact canonical locked_at timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)", { field: "expected_lock_version" });
886
902
  }
887
903
  const parsed = Date.parse(value);
888
- if (Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
904
+ if (value.startsWith("0000-") || Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
889
905
  throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
890
906
  }
891
907
  return value;
@@ -910,9 +926,6 @@ function prepareStaleLockHandoff(input, options = {}) {
910
926
  if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
911
927
  throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
912
928
  }
913
- if (canonicalAgentRef(expectedHolder) === canonicalAgentRef(newHolder)) {
914
- throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "new_holder must differ from expected_holder", { field: "new_holder" });
915
- }
916
929
  const operationTimestamp = options.now ?? new Date().toISOString();
917
930
  if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
918
931
  throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
@@ -6215,7 +6228,7 @@ var init_runner_sandbox = __esm(() => {
6215
6228
  });
6216
6229
 
6217
6230
  // src/lib/event-hooks.ts
6218
- import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
6231
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
6219
6232
  import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
6220
6233
  import { dirname as dirname3, resolve as resolve6 } from "path";
6221
6234
  import { createConnection } from "net";
@@ -6282,7 +6295,7 @@ function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
6282
6295
  payload: redactValue(payload ?? {}),
6283
6296
  source: { package: "@hasna/todos", local_only: true }
6284
6297
  };
6285
- const digest = createHash3("sha256").update(canonicalEvent(base)).digest("hex");
6298
+ const digest = createHash4("sha256").update(canonicalEvent(base)).digest("hex");
6286
6299
  return { ...base, integrity: { algorithm: "sha256", digest } };
6287
6300
  }
6288
6301
  function summarize(value) {
@@ -11210,8 +11223,8 @@ var init_boards = __esm(() => {
11210
11223
  });
11211
11224
 
11212
11225
  // src/lib/artifact-store.ts
11213
- import { createHash as createHash4 } from "crypto";
11214
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
11226
+ import { createHash as createHash5 } from "crypto";
11227
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
11215
11228
  import { basename, dirname as dirname4, join as join5, resolve as resolve7 } from "path";
11216
11229
  import { tmpdir as tmpdir2 } from "os";
11217
11230
  function isInMemoryDb2(path) {
@@ -11235,7 +11248,7 @@ function artifactStorePath(relativePath) {
11235
11248
  return join5(artifactStoreRoot(), normalized);
11236
11249
  }
11237
11250
  function sha256(buffer) {
11238
- return createHash4("sha256").update(buffer).digest("hex");
11251
+ return createHash5("sha256").update(buffer).digest("hex");
11239
11252
  }
11240
11253
  function isTextLike(buffer, path) {
11241
11254
  if (buffer.includes(0))
@@ -11278,7 +11291,7 @@ function storeArtifactContent(input) {
11278
11291
  const sourceStat = statSync2(sourcePath);
11279
11292
  if (!sourceStat.isFile())
11280
11293
  throw new Error(`Artifact path is not a file: ${sanitizePreWriteText(input.path, "artifact.path")}`);
11281
- const sourceBuffer = readFileSync3(sourcePath);
11294
+ const sourceBuffer = readFileSync4(sourcePath);
11282
11295
  const sourceSha = sha256(sourceBuffer);
11283
11296
  const textLike = isTextLike(sourceBuffer, input.path);
11284
11297
  let storedBuffer = sourceBuffer;
@@ -11365,7 +11378,7 @@ function verifyStoredArtifact(input) {
11365
11378
  message: "stored artifact content is missing"
11366
11379
  };
11367
11380
  }
11368
- const buffer = readFileSync3(storedPath);
11381
+ const buffer = readFileSync4(storedPath);
11369
11382
  const actualSha = sha256(buffer);
11370
11383
  const actualSize = buffer.length;
11371
11384
  const ok = actualSha === store.sha256 && actualSize === store.size_bytes;
@@ -11385,7 +11398,7 @@ function exportStoredArtifactContent(input) {
11385
11398
  const report = verifyStoredArtifact(input);
11386
11399
  if (report.status !== "ok" || !report.relative_path || !report.actual_sha256 || report.actual_size_bytes === null)
11387
11400
  return null;
11388
- const content = readFileSync3(artifactStorePath(report.relative_path));
11401
+ const content = readFileSync4(artifactStorePath(report.relative_path));
11389
11402
  return {
11390
11403
  artifact_id: input.id,
11391
11404
  sha256: report.actual_sha256,
@@ -11436,7 +11449,7 @@ function getArtifactStoreRoot(dbPath) {
11436
11449
  return join5(dirname4(resolve7(path)), "artifacts");
11437
11450
  }
11438
11451
  function computeContentHash(path) {
11439
- return sha256(readFileSync3(resolve7(path)));
11452
+ return sha256(readFileSync4(resolve7(path)));
11440
11453
  }
11441
11454
  function storeArtifactFile(input) {
11442
11455
  const sourcePath = resolve7(input.sourcePath);
@@ -11446,7 +11459,7 @@ function storeArtifactFile(input) {
11446
11459
  if (!statSync2(sourcePath).isFile()) {
11447
11460
  throw new Error(`Source path is not a file: ${input.sourcePath}`);
11448
11461
  }
11449
- const buffer = readFileSync3(sourcePath);
11462
+ const buffer = readFileSync4(sourcePath);
11450
11463
  const contentHash = sha256(buffer);
11451
11464
  const mimeType = mediaTypeFor(sourcePath, isTextLike(buffer, sourcePath));
11452
11465
  const storageMode = input.storageMode ?? "copy";
@@ -12644,11 +12657,11 @@ var init_tasks = __esm(() => {
12644
12657
  });
12645
12658
 
12646
12659
  // src/project-registration/authority.ts
12647
- import { createHash as createHash5 } from "crypto";
12660
+ import { createHash as createHash6 } from "crypto";
12648
12661
  // package.json
12649
12662
  var package_default = {
12650
12663
  name: "@hasna/todos",
12651
- version: "0.15.20",
12664
+ version: "0.15.24",
12652
12665
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12653
12666
  type: "module",
12654
12667
  main: "dist/index.js",
@@ -13523,6 +13536,30 @@ function forbiddenAuditHistoryTombstoneError(id) {
13523
13536
  return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
13524
13537
  }
13525
13538
 
13539
+ // src/task-manifest/canonical.ts
13540
+ import { createHash as createHash3 } from "crypto";
13541
+ function canonicalize2(value) {
13542
+ if (Array.isArray(value))
13543
+ return value.map(canonicalize2);
13544
+ if (value !== null && typeof value === "object") {
13545
+ return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry2]) => [key, canonicalize2(entry2)]));
13546
+ }
13547
+ return value;
13548
+ }
13549
+ function canonicalJson(value) {
13550
+ return JSON.stringify(canonicalize2(value));
13551
+ }
13552
+ function canonicalDigest(value) {
13553
+ return createHash3("sha256").update(canonicalJson(value)).digest("hex");
13554
+ }
13555
+ function deterministicUuid(namespace, ...parts) {
13556
+ const bytes = createHash3("sha256").update([namespace, ...parts].join("\x1F")).digest().subarray(0, 16);
13557
+ bytes[6] = bytes[6] & 15 | 80;
13558
+ bytes[8] = bytes[8] & 63 | 128;
13559
+ const hex = bytes.toString("hex");
13560
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
13561
+ }
13562
+
13526
13563
  // src/storage/postgres-adapter.ts
13527
13564
  function createPostgresTodosStorageAdapter(options) {
13528
13565
  const store = new PostgresJsonRecordStore(options);
@@ -13747,7 +13784,7 @@ class PostgresJsonRecordStore {
13747
13784
  AND task_record.deleted_at IS NULL
13748
13785
  AND target.payload->>'locked_by' = $3
13749
13786
  AND target.payload->>'locked_at' = $4
13750
- AND todos_try_timestamptz(target.payload->>'locked_at') < $5::timestamptz
13787
+ AND $4::timestamptz < $5::timestamptz
13751
13788
  AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
13752
13789
  RETURNING task_record.payload
13753
13790
  ),
@@ -15190,16 +15227,17 @@ async function findCommit(sha, store) {
15190
15227
  async function addGitRef(input, store, context) {
15191
15228
  if (!await store.get("tasks", input.task_id))
15192
15229
  throw new Error(`Task not found: ${input.task_id}`);
15230
+ const existing = (await store.list("refs")).filter((ref) => ref.task_id === input.task_id && ref.ref_type === input.ref_type && ref.name === input.name).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id))[0];
15193
15231
  const timestamp = new Date().toISOString();
15194
15232
  const gitRef = {
15195
- id: randomUUID(),
15233
+ id: existing?.id ?? deterministicUuid("todos:git-ref:v1", input.task_id, input.ref_type, input.name),
15196
15234
  task_id: input.task_id,
15197
15235
  ref_type: input.ref_type,
15198
15236
  name: input.name,
15199
- url: input.url ?? null,
15200
- provider: input.provider ?? null,
15237
+ url: input.url ?? existing?.url ?? null,
15238
+ provider: input.provider ?? existing?.provider ?? null,
15201
15239
  metadata: input.metadata ?? {},
15202
- created_at: timestamp,
15240
+ created_at: existing?.created_at ?? timestamp,
15203
15241
  updated_at: timestamp
15204
15242
  };
15205
15243
  await store.upsert("refs", gitRef, context);
@@ -18477,23 +18515,23 @@ class WriteBoundaryError extends Error {
18477
18515
  }
18478
18516
  }
18479
18517
  function canonicalProjectRegistrationJson(value) {
18480
- return JSON.stringify(canonicalize2(value));
18518
+ return JSON.stringify(canonicalize3(value));
18481
18519
  }
18482
- function canonicalize2(value) {
18520
+ function canonicalize3(value) {
18483
18521
  if (Array.isArray(value))
18484
- return value.map(canonicalize2);
18522
+ return value.map(canonicalize3);
18485
18523
  if (!value || typeof value !== "object")
18486
18524
  return value;
18487
18525
  const out = {};
18488
18526
  for (const key of Object.keys(value).sort()) {
18489
18527
  const entry2 = value[key];
18490
18528
  if (entry2 !== undefined)
18491
- out[key] = canonicalize2(entry2);
18529
+ out[key] = canonicalize3(entry2);
18492
18530
  }
18493
18531
  return out;
18494
18532
  }
18495
18533
  function digestProjectRegistrationValue(value) {
18496
- return createHash5("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
18534
+ return createHash6("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
18497
18535
  }
18498
18536
  function deriveTodosProjectRegistrationIdempotencyKey(input) {
18499
18537
  return `prk_${digestProjectRegistrationValue({