@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
package/dist/storage.js CHANGED
@@ -4731,7 +4731,7 @@ var init_database = __esm(() => {
4731
4731
  });
4732
4732
 
4733
4733
  // src/lib/config.ts
4734
- import { existsSync as existsSync4 } from "fs";
4734
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
4735
4735
  import { dirname as dirname2, join as join3 } from "path";
4736
4736
  function getConfigPath() {
4737
4737
  return join3(getTodosGlobalDir(), "config.json");
@@ -4763,6 +4763,22 @@ function saveConfig(config) {
4763
4763
  function updateConfig(patch) {
4764
4764
  return saveConfig({ ...loadConfig(), ...patch });
4765
4765
  }
4766
+ function getTodosAiConfig() {
4767
+ const configPath = getConfigPath();
4768
+ if (!existsSync4(configPath))
4769
+ return {};
4770
+ const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
4771
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4772
+ throw new Error("Todos config must be a JSON object");
4773
+ }
4774
+ const ai = parsed["ai"];
4775
+ if (ai === undefined)
4776
+ return {};
4777
+ if (ai === null || typeof ai !== "object" || Array.isArray(ai)) {
4778
+ throw new Error("Todos AI configuration must be an object");
4779
+ }
4780
+ return { ...ai };
4781
+ }
4766
4782
  function normalizeApiUrl(value) {
4767
4783
  const trimmed = value?.trim();
4768
4784
  if (!trimmed)
@@ -7093,7 +7109,7 @@ var init_shared_events = __esm(() => {
7093
7109
  });
7094
7110
 
7095
7111
  // src/lib/secret-redaction.ts
7096
- import { readFileSync as readFileSync2, existsSync as existsSync6 } from "fs";
7112
+ import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
7097
7113
  function registerCustomRedactor(fn) {
7098
7114
  customRedactors.push(fn);
7099
7115
  }
@@ -7160,7 +7176,7 @@ function scanAndRedactText(text, options = {}) {
7160
7176
  function scanFileForSecrets(path, options = {}) {
7161
7177
  if (!existsSync6(path))
7162
7178
  throw new Error(`File not found: ${path}`);
7163
- const content = readFileSync2(path, "utf8");
7179
+ const content = readFileSync3(path, "utf8");
7164
7180
  return scanAndRedactText(content, options);
7165
7181
  }
7166
7182
  function safeStringify(value, space) {
@@ -8606,7 +8622,7 @@ function requireCanonicalLockVersion(value) {
8606
8622
  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" });
8607
8623
  }
8608
8624
  const parsed = Date.parse(value);
8609
- if (Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
8625
+ if (value.startsWith("0000-") || Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
8610
8626
  throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
8611
8627
  }
8612
8628
  return value;
@@ -8631,9 +8647,6 @@ function prepareStaleLockHandoff(input, options = {}) {
8631
8647
  if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
8632
8648
  throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
8633
8649
  }
8634
- if (canonicalAgentRef(expectedHolder) === canonicalAgentRef(newHolder)) {
8635
- throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "new_holder must differ from expected_holder", { field: "new_holder" });
8636
- }
8637
8650
  const operationTimestamp = options.now ?? new Date().toISOString();
8638
8651
  if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
8639
8652
  throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
@@ -11211,7 +11224,7 @@ var init_boards = __esm(() => {
11211
11224
 
11212
11225
  // src/lib/artifact-store.ts
11213
11226
  import { createHash as createHash3 } from "crypto";
11214
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
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) {
@@ -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";
@@ -15040,6 +15053,32 @@ function assertSafeIdentifier(value) {
15040
15053
 
15041
15054
  // src/storage/postgres-adapter.ts
15042
15055
  init_redaction();
15056
+
15057
+ // src/task-manifest/canonical.ts
15058
+ import { createHash as createHash5 } from "crypto";
15059
+ function canonicalize2(value) {
15060
+ if (Array.isArray(value))
15061
+ return value.map(canonicalize2);
15062
+ if (value !== null && typeof value === "object") {
15063
+ return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry2]) => [key, canonicalize2(entry2)]));
15064
+ }
15065
+ return value;
15066
+ }
15067
+ function canonicalJson(value) {
15068
+ return JSON.stringify(canonicalize2(value));
15069
+ }
15070
+ function canonicalDigest(value) {
15071
+ return createHash5("sha256").update(canonicalJson(value)).digest("hex");
15072
+ }
15073
+ function deterministicUuid(namespace, ...parts) {
15074
+ const bytes = createHash5("sha256").update([namespace, ...parts].join("\x1F")).digest().subarray(0, 16);
15075
+ bytes[6] = bytes[6] & 15 | 80;
15076
+ bytes[8] = bytes[8] & 63 | 128;
15077
+ const hex = bytes.toString("hex");
15078
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
15079
+ }
15080
+
15081
+ // src/storage/postgres-adapter.ts
15043
15082
  function createPostgresTodosStorageAdapter(options) {
15044
15083
  const store = new PostgresJsonRecordStore(options);
15045
15084
  const adapter = {
@@ -15263,7 +15302,7 @@ class PostgresJsonRecordStore {
15263
15302
  AND task_record.deleted_at IS NULL
15264
15303
  AND target.payload->>'locked_by' = $3
15265
15304
  AND target.payload->>'locked_at' = $4
15266
- AND todos_try_timestamptz(target.payload->>'locked_at') < $5::timestamptz
15305
+ AND $4::timestamptz < $5::timestamptz
15267
15306
  AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
15268
15307
  RETURNING task_record.payload
15269
15308
  ),
@@ -16706,16 +16745,17 @@ async function findCommit(sha, store) {
16706
16745
  async function addGitRef(input, store, context) {
16707
16746
  if (!await store.get("tasks", input.task_id))
16708
16747
  throw new Error(`Task not found: ${input.task_id}`);
16748
+ 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];
16709
16749
  const timestamp2 = new Date().toISOString();
16710
16750
  const gitRef = {
16711
- id: randomUUID3(),
16751
+ id: existing?.id ?? deterministicUuid("todos:git-ref:v1", input.task_id, input.ref_type, input.name),
16712
16752
  task_id: input.task_id,
16713
16753
  ref_type: input.ref_type,
16714
16754
  name: input.name,
16715
- url: input.url ?? null,
16716
- provider: input.provider ?? null,
16755
+ url: input.url ?? existing?.url ?? null,
16756
+ provider: input.provider ?? existing?.provider ?? null,
16717
16757
  metadata: input.metadata ?? {},
16718
- created_at: timestamp2,
16758
+ created_at: existing?.created_at ?? timestamp2,
16719
16759
  updated_at: timestamp2
16720
16760
  };
16721
16761
  await store.upsert("refs", gitRef, context);
@@ -18226,7 +18266,7 @@ function isCommentRedactionBackfillComplete(result) {
18226
18266
  return !result.dry_run && result.conflicts === 0 && result.remaining_candidates === 0;
18227
18267
  }
18228
18268
  // src/storage/s3-artifacts.ts
18229
- import { createHash as createHash5, createHmac as createHmac2 } from "crypto";
18269
+ import { createHash as createHash6, createHmac as createHmac2 } from "crypto";
18230
18270
  function createTodosS3ArtifactStore(options) {
18231
18271
  const requestFetch = options.fetch ?? fetch;
18232
18272
  const now3 = options.now ?? (() => new Date);
@@ -18398,7 +18438,7 @@ function toAmzDate(date) {
18398
18438
  return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
18399
18439
  }
18400
18440
  function sha256Hex(value) {
18401
- return createHash5("sha256").update(value).digest("hex");
18441
+ return createHash6("sha256").update(value).digest("hex");
18402
18442
  }
18403
18443
  function hmac(key, value) {
18404
18444
  return createHmac2("sha256", key).update(value).digest();
@@ -421,7 +421,7 @@ var init_sync_utils = __esm(() => {
421
421
  });
422
422
 
423
423
  // src/lib/config.ts
424
- import { existsSync as existsSync2 } from "fs";
424
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
425
425
  import { dirname, join as join2 } from "path";
426
426
  function getConfigPath() {
427
427
  return join2(getTodosGlobalDir(), "config.json");
@@ -453,6 +453,22 @@ function saveConfig(config) {
453
453
  function updateConfig(patch) {
454
454
  return saveConfig({ ...loadConfig(), ...patch });
455
455
  }
456
+ function getTodosAiConfig() {
457
+ const configPath = getConfigPath();
458
+ if (!existsSync2(configPath))
459
+ return {};
460
+ const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
461
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
462
+ throw new Error("Todos config must be a JSON object");
463
+ }
464
+ const ai = parsed["ai"];
465
+ if (ai === undefined)
466
+ return {};
467
+ if (ai === null || typeof ai !== "object" || Array.isArray(ai)) {
468
+ throw new Error("Todos AI configuration must be an object");
469
+ }
470
+ return { ...ai };
471
+ }
456
472
  function normalizeApiUrl(value) {
457
473
  const trimmed = value?.trim();
458
474
  if (!trimmed)
@@ -627,7 +643,7 @@ var init_redaction = __esm(() => {
627
643
  });
628
644
 
629
645
  // src/lib/secret-redaction.ts
630
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
646
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
631
647
  function registerCustomRedactor(fn) {
632
648
  customRedactors.push(fn);
633
649
  }
@@ -694,7 +710,7 @@ function scanAndRedactText(text, options = {}) {
694
710
  function scanFileForSecrets(path, options = {}) {
695
711
  if (!existsSync3(path))
696
712
  throw new Error(`File not found: ${path}`);
697
- const content = readFileSync2(path, "utf8");
713
+ const content = readFileSync3(path, "utf8");
698
714
  return scanAndRedactText(content, options);
699
715
  }
700
716
  function safeStringify(value, space) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.15.20",
3
+ "version": "0.15.24",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",