@hasna/todos 0.11.56 → 0.11.58

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/storage.js CHANGED
@@ -3050,7 +3050,7 @@ var init_redaction = __esm(() => {
3050
3050
  });
3051
3051
 
3052
3052
  // src/lib/secret-redaction.ts
3053
- import { readFileSync as readFileSync2, existsSync as existsSync5 } from "fs";
3053
+ import { readFileSync as readFileSync2, existsSync as existsSync6 } from "fs";
3054
3054
  function registerCustomRedactor(fn) {
3055
3055
  customRedactors.push(fn);
3056
3056
  }
@@ -3115,7 +3115,7 @@ function scanAndRedactText(text, options = {}) {
3115
3115
  };
3116
3116
  }
3117
3117
  function scanFileForSecrets(path, options = {}) {
3118
- if (!existsSync5(path))
3118
+ if (!existsSync6(path))
3119
3119
  throw new Error(`File not found: ${path}`);
3120
3120
  const content = readFileSync2(path, "utf8");
3121
3121
  return scanAndRedactText(content, options);
@@ -4175,6 +4175,7 @@ function explainRunnerSandbox(input = {}) {
4175
4175
  // src/lib/event-hooks.ts
4176
4176
  init_config();
4177
4177
  var LOCAL_EVENT_TYPES = [
4178
+ "task.created",
4178
4179
  "task.assigned",
4179
4180
  "task.blocked",
4180
4181
  "task.started",
@@ -4417,6 +4418,752 @@ async function testLocalEventHook(name, input) {
4417
4418
  return emitLocalEventHooks({ ...input, hooks: [hook] });
4418
4419
  }
4419
4420
 
4421
+ // node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
4422
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4423
+ import { existsSync as existsSync5 } from "fs";
4424
+ import { homedir } from "os";
4425
+ import { join as join4 } from "path";
4426
+ import { createHmac, timingSafeEqual } from "crypto";
4427
+ import { randomUUID as randomUUID2 } from "crypto";
4428
+ import { spawn } from "child_process";
4429
+ import { randomUUID as randomUUID22 } from "crypto";
4430
+ function getPathValue(input, path) {
4431
+ return path.split(".").reduce((value, part) => {
4432
+ if (value && typeof value === "object" && part in value) {
4433
+ return value[part];
4434
+ }
4435
+ return;
4436
+ }, input);
4437
+ }
4438
+ function wildcardToRegExp(pattern, options = {}) {
4439
+ let body = "";
4440
+ for (let index = 0;index < pattern.length; index += 1) {
4441
+ const char = pattern[index];
4442
+ if (char === "*") {
4443
+ if (pattern[index + 1] === "*") {
4444
+ body += ".*";
4445
+ index += 1;
4446
+ } else {
4447
+ body += options.segmentSafe ? "[^/]*" : ".*";
4448
+ }
4449
+ } else {
4450
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
4451
+ }
4452
+ }
4453
+ return new RegExp(`^${body}$`);
4454
+ }
4455
+ function matchString(value, matcher, options = {}) {
4456
+ if (matcher === undefined)
4457
+ return true;
4458
+ if (value === undefined)
4459
+ return false;
4460
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
4461
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
4462
+ }
4463
+ function matchRecord(input, matcher) {
4464
+ if (!matcher)
4465
+ return true;
4466
+ return Object.entries(matcher).every(([path, expected]) => {
4467
+ const actual = getPathValue(input, path);
4468
+ if (typeof expected === "string" || Array.isArray(expected)) {
4469
+ return matchString(actual === undefined ? undefined : String(actual), expected, {
4470
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
4471
+ });
4472
+ }
4473
+ return actual === expected;
4474
+ });
4475
+ }
4476
+ function eventMatchesFilter(event, filter) {
4477
+ return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
4478
+ }
4479
+ function channelMatchesEvent(channel, event) {
4480
+ if (!channel.enabled)
4481
+ return false;
4482
+ if (!channel.filters || channel.filters.length === 0)
4483
+ return true;
4484
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
4485
+ }
4486
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
4487
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
4488
+ function getEventsDataDir(override) {
4489
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
4490
+ }
4491
+ class JsonEventsStore {
4492
+ dataDir;
4493
+ channelsPath;
4494
+ eventsPath;
4495
+ deliveriesPath;
4496
+ constructor(dataDir = getEventsDataDir()) {
4497
+ this.dataDir = dataDir;
4498
+ this.channelsPath = join4(dataDir, "channels.json");
4499
+ this.eventsPath = join4(dataDir, "events.json");
4500
+ this.deliveriesPath = join4(dataDir, "deliveries.json");
4501
+ }
4502
+ async init() {
4503
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
4504
+ await chmod(this.dataDir, 448).catch(() => {
4505
+ return;
4506
+ });
4507
+ await this.ensureArrayFile(this.channelsPath);
4508
+ await this.ensureArrayFile(this.eventsPath);
4509
+ await this.ensureArrayFile(this.deliveriesPath);
4510
+ }
4511
+ async addChannel(channel) {
4512
+ await this.init();
4513
+ const channels = await this.readJson(this.channelsPath, []);
4514
+ const index = channels.findIndex((item) => item.id === channel.id);
4515
+ if (index >= 0) {
4516
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
4517
+ } else {
4518
+ channels.push(channel);
4519
+ }
4520
+ await this.writeJson(this.channelsPath, channels);
4521
+ return index >= 0 ? channels[index] : channel;
4522
+ }
4523
+ async listChannels() {
4524
+ await this.init();
4525
+ return this.readJson(this.channelsPath, []);
4526
+ }
4527
+ async getChannel(id) {
4528
+ const channels = await this.listChannels();
4529
+ return channels.find((channel) => channel.id === id);
4530
+ }
4531
+ async removeChannel(id) {
4532
+ await this.init();
4533
+ const channels = await this.readJson(this.channelsPath, []);
4534
+ const next = channels.filter((channel) => channel.id !== id);
4535
+ await this.writeJson(this.channelsPath, next);
4536
+ return next.length !== channels.length;
4537
+ }
4538
+ async appendEvent(event) {
4539
+ await this.init();
4540
+ const events = await this.readJson(this.eventsPath, []);
4541
+ events.push(event);
4542
+ await this.writeJson(this.eventsPath, events);
4543
+ return event;
4544
+ }
4545
+ async listEvents() {
4546
+ await this.init();
4547
+ return this.readJson(this.eventsPath, []);
4548
+ }
4549
+ async findEventByIdentity(identity) {
4550
+ const events = await this.listEvents();
4551
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
4552
+ }
4553
+ async appendDelivery(result) {
4554
+ await this.init();
4555
+ const deliveries = await this.readJson(this.deliveriesPath, []);
4556
+ deliveries.push(result);
4557
+ await this.writeJson(this.deliveriesPath, deliveries);
4558
+ return result;
4559
+ }
4560
+ async listDeliveries() {
4561
+ await this.init();
4562
+ return this.readJson(this.deliveriesPath, []);
4563
+ }
4564
+ async exportData() {
4565
+ return {
4566
+ channels: await this.listChannels(),
4567
+ events: await this.listEvents(),
4568
+ deliveries: await this.listDeliveries()
4569
+ };
4570
+ }
4571
+ async ensureArrayFile(path) {
4572
+ if (!existsSync5(path)) {
4573
+ await writeFile(path, `[]
4574
+ `, { encoding: "utf-8", mode: 384 });
4575
+ }
4576
+ await chmod(path, 384).catch(() => {
4577
+ return;
4578
+ });
4579
+ }
4580
+ async readJson(path, fallback) {
4581
+ try {
4582
+ const raw = await readFile(path, "utf-8");
4583
+ if (!raw.trim())
4584
+ return fallback;
4585
+ return JSON.parse(raw);
4586
+ } catch (error) {
4587
+ if (error.code === "ENOENT")
4588
+ return fallback;
4589
+ throw error;
4590
+ }
4591
+ }
4592
+ async writeJson(path, value) {
4593
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
4594
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
4595
+ `, { encoding: "utf-8", mode: 384 });
4596
+ await rename(tempPath, path);
4597
+ await chmod(path, 384).catch(() => {
4598
+ return;
4599
+ });
4600
+ }
4601
+ }
4602
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
4603
+ function buildSignatureBase(timestamp, body) {
4604
+ return `${timestamp}.${body}`;
4605
+ }
4606
+ function signPayload(secret, timestamp, body) {
4607
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
4608
+ return `sha256=${digest}`;
4609
+ }
4610
+ function now2() {
4611
+ return new Date().toISOString();
4612
+ }
4613
+ function truncate(value, max = 4096) {
4614
+ return value.length > max ? `${value.slice(0, max)}...` : value;
4615
+ }
4616
+ function buildWebhookRequest(event, channel) {
4617
+ if (!channel.webhook)
4618
+ throw new Error(`Channel ${channel.id} has no webhook config`);
4619
+ const body = JSON.stringify(event);
4620
+ const timestamp = event.time;
4621
+ const headers = {
4622
+ "Content-Type": "application/json",
4623
+ "User-Agent": "@hasna/events",
4624
+ "X-Hasna-Event-Id": event.id,
4625
+ "X-Hasna-Event-Type": event.type,
4626
+ "X-Hasna-Timestamp": timestamp,
4627
+ ...channel.webhook.headers
4628
+ };
4629
+ if (channel.webhook.secret) {
4630
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
4631
+ }
4632
+ return { body, headers };
4633
+ }
4634
+ async function dispatchWebhook(event, channel, options = {}) {
4635
+ if (!channel.webhook)
4636
+ throw new Error(`Channel ${channel.id} has no webhook config`);
4637
+ const startedAt = now2();
4638
+ const { body, headers } = buildWebhookRequest(event, channel);
4639
+ const controller = new AbortController;
4640
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
4641
+ try {
4642
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
4643
+ method: "POST",
4644
+ headers,
4645
+ body,
4646
+ signal: controller.signal
4647
+ });
4648
+ const responseBody = truncate(await response.text());
4649
+ return {
4650
+ attempt: 1,
4651
+ status: response.ok ? "success" : "failed",
4652
+ startedAt,
4653
+ completedAt: now2(),
4654
+ responseStatus: response.status,
4655
+ responseBody,
4656
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
4657
+ };
4658
+ } catch (error) {
4659
+ return {
4660
+ attempt: 1,
4661
+ status: "failed",
4662
+ startedAt,
4663
+ completedAt: now2(),
4664
+ error: error instanceof Error ? error.message : String(error)
4665
+ };
4666
+ } finally {
4667
+ clearTimeout(timeout);
4668
+ }
4669
+ }
4670
+ async function dispatchCommand(event, channel) {
4671
+ if (!channel.command)
4672
+ throw new Error(`Channel ${channel.id} has no command config`);
4673
+ const startedAt = now2();
4674
+ const eventJson = JSON.stringify(event);
4675
+ const env = {
4676
+ ...process.env,
4677
+ ...channel.command.env,
4678
+ HASNA_CHANNEL_ID: channel.id,
4679
+ HASNA_EVENT_ID: event.id,
4680
+ HASNA_EVENT_TYPE: event.type,
4681
+ HASNA_EVENT_SOURCE: event.source,
4682
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
4683
+ HASNA_EVENT_SEVERITY: event.severity,
4684
+ HASNA_EVENT_TIME: event.time,
4685
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
4686
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
4687
+ HASNA_EVENT_JSON: eventJson
4688
+ };
4689
+ return new Promise((resolve6) => {
4690
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
4691
+ cwd: channel.command.cwd,
4692
+ env,
4693
+ stdio: ["pipe", "pipe", "pipe"]
4694
+ });
4695
+ let stdout = "";
4696
+ let stderr = "";
4697
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
4698
+ child.stdin.end(eventJson);
4699
+ child.stdout.on("data", (chunk) => {
4700
+ stdout += chunk.toString();
4701
+ });
4702
+ child.stderr.on("data", (chunk) => {
4703
+ stderr += chunk.toString();
4704
+ });
4705
+ child.on("error", (error) => {
4706
+ clearTimeout(timeout);
4707
+ resolve6({
4708
+ attempt: 1,
4709
+ status: "failed",
4710
+ startedAt,
4711
+ completedAt: now2(),
4712
+ stdout: truncate(stdout),
4713
+ stderr: truncate(stderr),
4714
+ error: error.message
4715
+ });
4716
+ });
4717
+ child.on("close", (code, signal) => {
4718
+ clearTimeout(timeout);
4719
+ const success = code === 0;
4720
+ resolve6({
4721
+ attempt: 1,
4722
+ status: success ? "success" : "failed",
4723
+ startedAt,
4724
+ completedAt: now2(),
4725
+ stdout: truncate(stdout),
4726
+ stderr: truncate(stderr),
4727
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
4728
+ });
4729
+ });
4730
+ });
4731
+ }
4732
+ async function dispatchChannel(event, channel, options = {}) {
4733
+ if (channel.transport === "webhook")
4734
+ return dispatchWebhook(event, channel, options);
4735
+ if (channel.transport === "command")
4736
+ return dispatchCommand(event, channel);
4737
+ return {
4738
+ attempt: 1,
4739
+ status: "skipped",
4740
+ startedAt: now2(),
4741
+ completedAt: now2(),
4742
+ error: `Unsupported transport: ${channel.transport}`
4743
+ };
4744
+ }
4745
+ function createDeliveryResult(event, channel, attempts) {
4746
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
4747
+ return {
4748
+ id: randomUUID2(),
4749
+ eventId: event.id,
4750
+ channelId: channel.id,
4751
+ transport: channel.transport,
4752
+ status,
4753
+ attempts,
4754
+ createdAt: attempts[0]?.startedAt ?? now2(),
4755
+ completedAt: attempts.at(-1)?.completedAt ?? now2()
4756
+ };
4757
+ }
4758
+ function createEvent(input) {
4759
+ return {
4760
+ id: input.id ?? randomUUID22(),
4761
+ source: input.source,
4762
+ type: input.type,
4763
+ time: normalizeTime(input.time),
4764
+ subject: input.subject,
4765
+ severity: input.severity ?? "info",
4766
+ data: input.data ?? {},
4767
+ message: input.message,
4768
+ dedupeKey: input.dedupeKey,
4769
+ schemaVersion: input.schemaVersion ?? "1.0",
4770
+ metadata: input.metadata ?? {}
4771
+ };
4772
+ }
4773
+
4774
+ class EventsClient {
4775
+ store;
4776
+ redactors;
4777
+ transportOptions;
4778
+ constructor(options = {}) {
4779
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
4780
+ this.redactors = options.redactors ?? [];
4781
+ this.transportOptions = { fetchImpl: options.fetchImpl };
4782
+ }
4783
+ async addChannel(input) {
4784
+ const timestamp = new Date().toISOString();
4785
+ return this.store.addChannel({
4786
+ ...input,
4787
+ createdAt: input.createdAt ?? timestamp,
4788
+ updatedAt: input.updatedAt ?? timestamp
4789
+ });
4790
+ }
4791
+ async listChannels() {
4792
+ return this.store.listChannels();
4793
+ }
4794
+ async removeChannel(id) {
4795
+ return this.store.removeChannel(id);
4796
+ }
4797
+ async emit(input, options = {}) {
4798
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
4799
+ if (options.dedupe !== false) {
4800
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
4801
+ if (existing) {
4802
+ return { event: existing, deliveries: [], deduped: true };
4803
+ }
4804
+ }
4805
+ await this.store.appendEvent(event);
4806
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
4807
+ return { event, deliveries, deduped: false };
4808
+ }
4809
+ async listEvents() {
4810
+ return this.store.listEvents();
4811
+ }
4812
+ async listDeliveries() {
4813
+ return this.store.listDeliveries();
4814
+ }
4815
+ async deliver(event) {
4816
+ const channels = await this.store.listChannels();
4817
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
4818
+ const deliveries = [];
4819
+ for (const channel of selected) {
4820
+ const eventForChannel = await this.applyRedaction(event, channel);
4821
+ const result = await this.deliverWithRetry(eventForChannel, channel);
4822
+ await this.store.appendDelivery(result);
4823
+ deliveries.push(result);
4824
+ }
4825
+ return deliveries;
4826
+ }
4827
+ async matchChannel(id, input = {}) {
4828
+ const channel = await this.store.getChannel(id);
4829
+ if (!channel)
4830
+ throw new Error(`Channel not found: ${id}`);
4831
+ const event = createEvent({
4832
+ source: input.source ?? "hasna.events",
4833
+ type: input.type ?? "events.test",
4834
+ subject: input.subject ?? id,
4835
+ severity: input.severity ?? "info",
4836
+ data: input.data ?? { test: true },
4837
+ message: input.message ?? "Hasna events test delivery",
4838
+ dedupeKey: input.dedupeKey,
4839
+ schemaVersion: input.schemaVersion,
4840
+ metadata: input.metadata,
4841
+ time: input.time,
4842
+ id: input.id
4843
+ });
4844
+ const matched = channelMatchesEvent(channel, event);
4845
+ return {
4846
+ channelId: channel.id,
4847
+ matched,
4848
+ event,
4849
+ filters: channel.filters,
4850
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
4851
+ };
4852
+ }
4853
+ async testChannel(id, input = {}, options = {}) {
4854
+ const channel = await this.store.getChannel(id);
4855
+ if (!channel)
4856
+ throw new Error(`Channel not found: ${id}`);
4857
+ const match = await this.matchChannel(id, input);
4858
+ const event = match.event;
4859
+ if (options.honorFilters && !match.matched) {
4860
+ const timestamp = new Date().toISOString();
4861
+ const result2 = createDeliveryResult(event, channel, [{
4862
+ attempt: 1,
4863
+ status: "skipped",
4864
+ startedAt: timestamp,
4865
+ completedAt: timestamp,
4866
+ error: match.reason
4867
+ }]);
4868
+ result2.metadata = { reason: "filter_mismatch" };
4869
+ await this.store.appendDelivery(result2);
4870
+ return result2;
4871
+ }
4872
+ const eventForChannel = await this.applyRedaction(event, channel);
4873
+ const result = await this.deliverWithRetry(eventForChannel, channel);
4874
+ await this.store.appendDelivery(result);
4875
+ return result;
4876
+ }
4877
+ async replay(options = {}) {
4878
+ const events = (await this.store.listEvents()).filter((event) => {
4879
+ if (options.eventId && event.id !== options.eventId)
4880
+ return false;
4881
+ if (options.source && event.source !== options.source)
4882
+ return false;
4883
+ if (options.type && event.type !== options.type)
4884
+ return false;
4885
+ return true;
4886
+ });
4887
+ if (options.dryRun)
4888
+ return { events, deliveries: [] };
4889
+ const deliveries = [];
4890
+ for (const event of events) {
4891
+ deliveries.push(...await this.deliver(event));
4892
+ }
4893
+ return { events, deliveries };
4894
+ }
4895
+ async applyRedaction(event, channel) {
4896
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
4897
+ for (const redactor of this.redactors) {
4898
+ next = await redactor(next, channel);
4899
+ }
4900
+ return next;
4901
+ }
4902
+ async deliverWithRetry(event, channel) {
4903
+ const policy = normalizeRetryPolicy(channel.retry);
4904
+ const attempts = [];
4905
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
4906
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
4907
+ attempt.attempt = index + 1;
4908
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
4909
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
4910
+ }
4911
+ attempts.push(attempt);
4912
+ if (attempt.status !== "failed")
4913
+ break;
4914
+ if (attempt.nextBackoffMs)
4915
+ await Bun.sleep(attempt.nextBackoffMs);
4916
+ }
4917
+ return createDeliveryResult(event, channel, attempts);
4918
+ }
4919
+ }
4920
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
4921
+ if (paths.length === 0)
4922
+ return event;
4923
+ const copy = structuredClone(event);
4924
+ for (const path of paths) {
4925
+ setPath(copy, path, replacement);
4926
+ }
4927
+ return copy;
4928
+ }
4929
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
4930
+ return redactValue2(event, replacement);
4931
+ }
4932
+ function shouldRedactKey(key) {
4933
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
4934
+ }
4935
+ function redactValue2(value, replacement) {
4936
+ if (Array.isArray(value))
4937
+ return value.map((item) => redactValue2(item, replacement));
4938
+ if (!value || typeof value !== "object")
4939
+ return value;
4940
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
4941
+ key,
4942
+ shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
4943
+ ]));
4944
+ }
4945
+ function setPath(input, path, replacement) {
4946
+ const parts = path.split(".");
4947
+ let cursor = input;
4948
+ for (const part of parts.slice(0, -1)) {
4949
+ const next = cursor[part];
4950
+ if (!next || typeof next !== "object")
4951
+ return;
4952
+ cursor = next;
4953
+ }
4954
+ const last = parts.at(-1);
4955
+ if (last && last in cursor)
4956
+ cursor[last] = replacement;
4957
+ }
4958
+ function normalizeTime(value) {
4959
+ if (!value)
4960
+ return new Date().toISOString();
4961
+ return value instanceof Date ? value.toISOString() : value;
4962
+ }
4963
+ function normalizeRetryPolicy(policy) {
4964
+ return {
4965
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
4966
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
4967
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
4968
+ };
4969
+ }
4970
+
4971
+ // src/lib/shared-events.ts
4972
+ init_database();
4973
+
4974
+ // src/db/task-lists.ts
4975
+ init_types();
4976
+ init_database();
4977
+ function rowToTaskList(row) {
4978
+ return {
4979
+ ...row,
4980
+ metadata: JSON.parse(row.metadata || "{}")
4981
+ };
4982
+ }
4983
+ function createTaskList(input, db) {
4984
+ const d = db || getDatabase();
4985
+ const id = uuid();
4986
+ const timestamp = now();
4987
+ const slug = input.slug || slugify(input.name);
4988
+ if (!input.project_id) {
4989
+ const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
4990
+ if (existing) {
4991
+ throw new Error(`Standalone task list with slug "${slug}" already exists`);
4992
+ }
4993
+ }
4994
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
4995
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
4996
+ return getTaskList(id, d);
4997
+ }
4998
+ function getTaskList(id, db) {
4999
+ const d = db || getDatabase();
5000
+ const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
5001
+ return row ? rowToTaskList(row) : null;
5002
+ }
5003
+ function getTaskListBySlug(slug, projectId, db) {
5004
+ const d = db || getDatabase();
5005
+ let row;
5006
+ if (projectId) {
5007
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
5008
+ } else {
5009
+ row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
5010
+ }
5011
+ return row ? rowToTaskList(row) : null;
5012
+ }
5013
+ function listTaskLists(projectId, db) {
5014
+ const d = db || getDatabase();
5015
+ if (projectId) {
5016
+ return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
5017
+ }
5018
+ return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
5019
+ }
5020
+ function updateTaskList(id, input, db) {
5021
+ const d = db || getDatabase();
5022
+ const existing = getTaskList(id, d);
5023
+ if (!existing)
5024
+ throw new TaskListNotFoundError(id);
5025
+ const sets = ["updated_at = ?"];
5026
+ const params = [now()];
5027
+ if (input.name !== undefined) {
5028
+ sets.push("name = ?");
5029
+ params.push(input.name);
5030
+ }
5031
+ if (input.description !== undefined) {
5032
+ sets.push("description = ?");
5033
+ params.push(input.description);
5034
+ }
5035
+ if (input.metadata !== undefined) {
5036
+ sets.push("metadata = ?");
5037
+ params.push(JSON.stringify(input.metadata));
5038
+ }
5039
+ params.push(id);
5040
+ d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
5041
+ return getTaskList(id, d);
5042
+ }
5043
+ function deleteTaskList(id, db) {
5044
+ const d = db || getDatabase();
5045
+ return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
5046
+ }
5047
+ function ensureTaskList(name, slug, projectId, db) {
5048
+ const d = db || getDatabase();
5049
+ const existing = getTaskListBySlug(slug, projectId, d);
5050
+ if (existing)
5051
+ return existing;
5052
+ return createTaskList({ name, slug, project_id: projectId }, d);
5053
+ }
5054
+
5055
+ // src/lib/shared-events.ts
5056
+ var SOURCE = "todos";
5057
+ function taskEventData(task, extra = {}) {
5058
+ return {
5059
+ id: task.id,
5060
+ task_id: task.id,
5061
+ short_id: task.short_id,
5062
+ title: task.title,
5063
+ description: task.description,
5064
+ status: task.status,
5065
+ priority: task.priority,
5066
+ project_id: task.project_id,
5067
+ parent_id: task.parent_id,
5068
+ plan_id: task.plan_id,
5069
+ task_list_id: task.task_list_id,
5070
+ agent_id: task.agent_id,
5071
+ assigned_to: task.assigned_to,
5072
+ session_id: task.session_id,
5073
+ working_dir: task.working_dir,
5074
+ tags: task.tags,
5075
+ metadata: task.metadata,
5076
+ version: task.version,
5077
+ created_at: task.created_at,
5078
+ updated_at: task.updated_at,
5079
+ started_at: task.started_at,
5080
+ completed_at: task.completed_at,
5081
+ due_at: task.due_at,
5082
+ ...extra
5083
+ };
5084
+ }
5085
+ function taskEventMetadata(task) {
5086
+ const metadata = {
5087
+ package: "@hasna/todos",
5088
+ todos_event_schema_version: 1,
5089
+ task_id: task.id,
5090
+ task_short_id: task.short_id,
5091
+ project_id: task.project_id,
5092
+ task_list_id: task.task_list_id,
5093
+ working_dir: task.working_dir
5094
+ };
5095
+ try {
5096
+ const project = task.project_id ? getProject(task.project_id) : null;
5097
+ const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
5098
+ if (project) {
5099
+ metadata.project_id = project.id;
5100
+ metadata.project_name = project.name;
5101
+ metadata.project_path = projectPath;
5102
+ metadata.project_canonical_path = project.path;
5103
+ metadata.project_default_task_list_slug = project.task_list_id;
5104
+ metadata.root_project_id = inferRootProjectId(project);
5105
+ } else if (projectPath) {
5106
+ metadata.project_path = projectPath;
5107
+ metadata.project_canonical_path = projectPath;
5108
+ }
5109
+ if (projectPath) {
5110
+ metadata.project_kind = classifyProjectKind(projectPath);
5111
+ metadata.project_is_worktree = isWorktreePath(projectPath);
5112
+ if (typeof task.metadata.route_enabled === "boolean") {
5113
+ metadata.route_enabled = task.metadata.route_enabled;
5114
+ }
5115
+ metadata.working_dir = task.working_dir ?? projectPath;
5116
+ }
5117
+ const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
5118
+ if (taskList) {
5119
+ metadata.task_list_id = taskList.id;
5120
+ metadata.task_list_slug = taskList.slug;
5121
+ metadata.task_list_name = taskList.name;
5122
+ metadata.task_list_project_id = taskList.project_id;
5123
+ metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
5124
+ }
5125
+ } catch {}
5126
+ return metadata;
5127
+ }
5128
+ function classifyProjectKind(path) {
5129
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
5130
+ }
5131
+ function isWorktreePath(path) {
5132
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
5133
+ }
5134
+ function inferRootProjectId(project) {
5135
+ return isWorktreePath(project.path) ? null : project.id;
5136
+ }
5137
+ function readMachineLocalPath(project) {
5138
+ const machineId = process.env["TODOS_MACHINE_ID"];
5139
+ if (!machineId)
5140
+ return null;
5141
+ try {
5142
+ const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
5143
+ return row?.path ?? null;
5144
+ } catch {
5145
+ return null;
5146
+ }
5147
+ }
5148
+ async function emitSharedTaskEvent(input) {
5149
+ const data = taskEventData(input.task, input.data);
5150
+ await new EventsClient().emit({
5151
+ source: SOURCE,
5152
+ type: input.type,
5153
+ subject: input.task.id,
5154
+ severity: input.severity ?? "info",
5155
+ message: input.message ?? `${input.type}: ${input.task.title}`,
5156
+ data,
5157
+ dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
5158
+ metadata: taskEventMetadata(input.task)
5159
+ }, { deliver: true, dedupe: true });
5160
+ }
5161
+ function emitSharedTaskEventQuiet(input) {
5162
+ emitSharedTaskEvent(input).catch(() => {
5163
+ return;
5164
+ });
5165
+ }
5166
+
4420
5167
  // src/db/audit.ts
4421
5168
  init_database();
4422
5169
  function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
@@ -4679,7 +5426,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
4679
5426
  activeDeliveries--;
4680
5427
  }
4681
5428
  }
4682
- async function dispatchWebhook(event, payload, db) {
5429
+ async function dispatchWebhook2(event, payload, db) {
4683
5430
  const d = db || getDatabase();
4684
5431
  const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
4685
5432
  const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
@@ -4833,7 +5580,10 @@ function createTask(input, db) {
4833
5580
  insertTaskTags(id, tags, d);
4834
5581
  }
4835
5582
  const task = getTask(id, d);
4836
- dispatchWebhook("task.created", { id: task.id, short_id: task.short_id, title: task.title, status: task.status, priority: task.priority, project_id: task.project_id, assigned_to: task.assigned_to }, d).catch(() => {});
5583
+ const payload = taskEventData(task);
5584
+ dispatchWebhook2("task.created", payload, d).catch(() => {});
5585
+ emitLocalEventHooksQuiet({ type: "task.created", payload });
5586
+ emitSharedTaskEventQuiet({ type: "task.created", task });
4837
5587
  return task;
4838
5588
  }
4839
5589
  function getTask(id, db) {
@@ -5177,18 +5927,7 @@ function updateTask(id, input, db) {
5177
5927
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
5178
5928
  if (input.approved_by !== undefined)
5179
5929
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
5180
- if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
5181
- dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
5182
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
5183
- }
5184
- if (input.status !== undefined && input.status !== task.status) {
5185
- dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
5186
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
5187
- }
5188
- if (input.approved_by !== undefined) {
5189
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
5190
- }
5191
- return {
5930
+ const updatedTask = {
5192
5931
  ...task,
5193
5932
  ...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
5194
5933
  tags: input.tags ?? task.tags,
@@ -5206,6 +5945,22 @@ function updateTask(id, input, db) {
5206
5945
  approved_by: input.approved_by ?? task.approved_by,
5207
5946
  approved_at: input.approved_by ? timestamp : task.approved_at
5208
5947
  };
5948
+ if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
5949
+ const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
5950
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
5951
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
5952
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
5953
+ }
5954
+ if (input.status !== undefined && input.status !== task.status) {
5955
+ const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
5956
+ dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
5957
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
5958
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
5959
+ }
5960
+ if (input.approved_by !== undefined) {
5961
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
5962
+ }
5963
+ return updatedTask;
5209
5964
  }
5210
5965
  function deleteTask(id, db) {
5211
5966
  const d = db || getDatabase();
@@ -5938,9 +6693,12 @@ function startTask(id, agentId, db) {
5938
6693
  throw new Error(`Task ${id} could not be started because it changed during claim`);
5939
6694
  }
5940
6695
  logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
5941
- dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
5942
- emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
5943
- return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
6696
+ const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
6697
+ const payload = taskEventData(startedTask, { agent_id: agentId });
6698
+ dispatchWebhook2("task.started", payload, d).catch(() => {});
6699
+ emitLocalEventHooksQuiet({ type: "task.started", payload });
6700
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
6701
+ return startedTask;
5944
6702
  }
5945
6703
  function completeTask(id, agentId, db, options) {
5946
6704
  const d = db || getDatabase();
@@ -5976,8 +6734,21 @@ function completeTask(id, agentId, db, options) {
5976
6734
  });
5977
6735
  tx();
5978
6736
  logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
5979
- dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
5980
- emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
6737
+ const completedTaskForEvent = {
6738
+ ...task,
6739
+ status: "completed",
6740
+ locked_by: null,
6741
+ locked_at: null,
6742
+ completed_at: timestamp,
6743
+ confidence,
6744
+ version: task.version + 1,
6745
+ updated_at: timestamp,
6746
+ metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
6747
+ };
6748
+ const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
6749
+ dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
6750
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
6751
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
5981
6752
  let spawnedTask = null;
5982
6753
  if (task.recurrence_rule && !options?.skip_recurrence) {
5983
6754
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -6018,8 +6789,12 @@ function completeTask(id, agentId, db, options) {
6018
6789
  if (unblockedDeps.length > 0) {
6019
6790
  meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
6020
6791
  for (const dep of unblockedDeps) {
6021
- dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
6022
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
6792
+ const depTask = getTask(dep.id, d);
6793
+ const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
6794
+ dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
6795
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
6796
+ if (depTask)
6797
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
6023
6798
  }
6024
6799
  }
6025
6800
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -6205,9 +6980,6 @@ function failTask(id, agentId, reason, options, db) {
6205
6980
  const timestamp = now();
6206
6981
  d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
6207
6982
  WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
6208
- logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
6209
- dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
6210
- emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
6211
6983
  const failedTask = {
6212
6984
  ...task,
6213
6985
  status: "failed",
@@ -6217,6 +6989,11 @@ function failTask(id, agentId, reason, options, db) {
6217
6989
  version: task.version + 1,
6218
6990
  updated_at: timestamp
6219
6991
  };
6992
+ logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
6993
+ const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
6994
+ dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
6995
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
6996
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
6220
6997
  let retryTask;
6221
6998
  if (options?.retry) {
6222
6999
  const retryCount = (task.retry_count || 0) + 1;
@@ -6291,9 +7068,12 @@ function stealTask(agentId, opts, db) {
6291
7068
  return null;
6292
7069
  logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
6293
7070
  logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
6294
- dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
6295
- emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to } });
6296
- return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
7071
+ const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
7072
+ const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
7073
+ dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7074
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload });
7075
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
7076
+ return stolenTask;
6297
7077
  }
6298
7078
  function claimOrSteal(agentId, filters, db) {
6299
7079
  const d = db || getDatabase();
@@ -7352,8 +8132,8 @@ init_database();
7352
8132
  init_database();
7353
8133
  init_redaction();
7354
8134
  import { createHash as createHash2 } from "crypto";
7355
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
7356
- import { basename, dirname as dirname4, join as join4, resolve as resolve6 } from "path";
8135
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
8136
+ import { basename, dirname as dirname4, join as join5, resolve as resolve6 } from "path";
7357
8137
  import { tmpdir } from "os";
7358
8138
  function isInMemoryDb2(path) {
7359
8139
  return path === ":memory:" || path.startsWith("file::memory:");
@@ -7365,15 +8145,15 @@ function artifactStoreRoot() {
7365
8145
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
7366
8146
  const dbPath = getDatabasePath();
7367
8147
  if (isInMemoryDb2(dbPath))
7368
- return join4(tmpdir(), "hasna-todos-artifacts");
7369
- return join4(dirname4(resolve6(dbPath)), "artifacts");
8148
+ return join5(tmpdir(), "hasna-todos-artifacts");
8149
+ return join5(dirname4(resolve6(dbPath)), "artifacts");
7370
8150
  }
7371
8151
  function artifactStorePath(relativePath) {
7372
8152
  const normalized = relativePath.replace(/\\/g, "/");
7373
8153
  if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
7374
8154
  throw new Error("Invalid artifact store path");
7375
8155
  }
7376
- return join4(artifactStoreRoot(), normalized);
8156
+ return join5(artifactStoreRoot(), normalized);
7377
8157
  }
7378
8158
  function sha256(buffer) {
7379
8159
  return createHash2("sha256").update(buffer).digest("hex");
@@ -7414,7 +8194,7 @@ function mediaTypeFor(path, textLike) {
7414
8194
  }
7415
8195
  function storeArtifactContent(input) {
7416
8196
  const sourcePath = resolve6(input.path);
7417
- if (!existsSync6(sourcePath))
8197
+ if (!existsSync7(sourcePath))
7418
8198
  return null;
7419
8199
  const sourceStat = statSync2(sourcePath);
7420
8200
  if (!sourceStat.isFile())
@@ -7431,9 +8211,9 @@ function storeArtifactContent(input) {
7431
8211
  redactionStatus = "redacted";
7432
8212
  }
7433
8213
  const storedSha = sha256(storedBuffer);
7434
- const relativePath = join4("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
8214
+ const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
7435
8215
  const destination = artifactStorePath(relativePath);
7436
- if (!existsSync6(destination)) {
8216
+ if (!existsSync7(destination)) {
7437
8217
  mkdirSync4(dirname4(destination), { recursive: true });
7438
8218
  writeFileSync2(destination, storedBuffer);
7439
8219
  }
@@ -7493,7 +8273,7 @@ function verifyStoredArtifact(input) {
7493
8273
  };
7494
8274
  }
7495
8275
  const storedPath = artifactStorePath(store.relative_path);
7496
- if (!existsSync6(storedPath)) {
8276
+ if (!existsSync7(storedPath)) {
7497
8277
  return {
7498
8278
  id: input.id,
7499
8279
  path: input.path,
@@ -7574,15 +8354,15 @@ function getArtifactStoreRoot(dbPath) {
7574
8354
  return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
7575
8355
  const path = dbPath ?? getDatabasePath();
7576
8356
  if (isInMemoryDb2(path))
7577
- return join4(tmpdir(), "hasna-todos-artifacts");
7578
- return join4(dirname4(resolve6(path)), "artifacts");
8357
+ return join5(tmpdir(), "hasna-todos-artifacts");
8358
+ return join5(dirname4(resolve6(path)), "artifacts");
7579
8359
  }
7580
8360
  function computeContentHash(path) {
7581
8361
  return sha256(readFileSync3(resolve6(path)));
7582
8362
  }
7583
8363
  function storeArtifactFile(input) {
7584
8364
  const sourcePath = resolve6(input.sourcePath);
7585
- if (!existsSync6(sourcePath)) {
8365
+ if (!existsSync7(sourcePath)) {
7586
8366
  throw new Error(`Source file not found: ${input.sourcePath}`);
7587
8367
  }
7588
8368
  if (!statSync2(sourcePath).isFile()) {
@@ -7595,7 +8375,7 @@ function storeArtifactFile(input) {
7595
8375
  let localPath = sourcePath;
7596
8376
  if (storageMode === "copy") {
7597
8377
  const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
7598
- const destination = join4(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
8378
+ const destination = join5(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
7599
8379
  mkdirSync4(dirname4(destination), { recursive: true });
7600
8380
  writeFileSync2(destination, buffer);
7601
8381
  localPath = destination;
@@ -7605,7 +8385,7 @@ function storeArtifactFile(input) {
7605
8385
  function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
7606
8386
  if (storageMode === "reference")
7607
8387
  return false;
7608
- if (!localPath || !existsSync6(localPath))
8388
+ if (!localPath || !existsSync7(localPath))
7609
8389
  return false;
7610
8390
  rmSync(localPath, { force: true });
7611
8391
  try {
@@ -7617,8 +8397,8 @@ function isArtifactExpired(deletedAt, policy = {}) {
7617
8397
  if (!deletedAt)
7618
8398
  return false;
7619
8399
  const retentionDays = policy.deleted_retention_days ?? DEFAULT_DELETED_RETENTION_DAYS;
7620
- const now2 = policy.now ?? new Date;
7621
- const ageMs = now2.getTime() - new Date(deletedAt).getTime();
8400
+ const now3 = policy.now ?? new Date;
8401
+ const ageMs = now3.getTime() - new Date(deletedAt).getTime();
7622
8402
  return ageMs > retentionDays * 24 * 60 * 60 * 1000;
7623
8403
  }
7624
8404
  function buildArtifactExportManifest(artifacts, dbPath) {
@@ -9145,87 +9925,6 @@ function getCapableAgents(capabilities, opts, db) {
9145
9925
  return opts?.limit ? scored.slice(0, opts.limit) : scored;
9146
9926
  }
9147
9927
 
9148
- // src/db/task-lists.ts
9149
- init_types();
9150
- init_database();
9151
- function rowToTaskList(row) {
9152
- return {
9153
- ...row,
9154
- metadata: JSON.parse(row.metadata || "{}")
9155
- };
9156
- }
9157
- function createTaskList(input, db) {
9158
- const d = db || getDatabase();
9159
- const id = uuid();
9160
- const timestamp = now();
9161
- const slug = input.slug || slugify(input.name);
9162
- if (!input.project_id) {
9163
- const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
9164
- if (existing) {
9165
- throw new Error(`Standalone task list with slug "${slug}" already exists`);
9166
- }
9167
- }
9168
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
9169
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
9170
- return getTaskList(id, d);
9171
- }
9172
- function getTaskList(id, db) {
9173
- const d = db || getDatabase();
9174
- const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
9175
- return row ? rowToTaskList(row) : null;
9176
- }
9177
- function getTaskListBySlug(slug, projectId, db) {
9178
- const d = db || getDatabase();
9179
- let row;
9180
- if (projectId) {
9181
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
9182
- } else {
9183
- row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
9184
- }
9185
- return row ? rowToTaskList(row) : null;
9186
- }
9187
- function listTaskLists(projectId, db) {
9188
- const d = db || getDatabase();
9189
- if (projectId) {
9190
- return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
9191
- }
9192
- return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
9193
- }
9194
- function updateTaskList(id, input, db) {
9195
- const d = db || getDatabase();
9196
- const existing = getTaskList(id, d);
9197
- if (!existing)
9198
- throw new TaskListNotFoundError(id);
9199
- const sets = ["updated_at = ?"];
9200
- const params = [now()];
9201
- if (input.name !== undefined) {
9202
- sets.push("name = ?");
9203
- params.push(input.name);
9204
- }
9205
- if (input.description !== undefined) {
9206
- sets.push("description = ?");
9207
- params.push(input.description);
9208
- }
9209
- if (input.metadata !== undefined) {
9210
- sets.push("metadata = ?");
9211
- params.push(JSON.stringify(input.metadata));
9212
- }
9213
- params.push(id);
9214
- d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
9215
- return getTaskList(id, d);
9216
- }
9217
- function deleteTaskList(id, db) {
9218
- const d = db || getDatabase();
9219
- return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
9220
- }
9221
- function ensureTaskList(name, slug, projectId, db) {
9222
- const d = db || getDatabase();
9223
- const existing = getTaskListBySlug(slug, projectId, d);
9224
- if (existing)
9225
- return existing;
9226
- return createTaskList({ name, slug, project_id: projectId }, d);
9227
- }
9228
-
9229
9928
  // src/storage/local-sqlite.ts
9230
9929
  init_database();
9231
9930
 
@@ -9794,7 +10493,7 @@ function createHybridTodosStorageAdapter(options) {
9794
10493
  }
9795
10494
 
9796
10495
  // src/storage/postgres-adapter.ts
9797
- import { randomUUID as randomUUID2 } from "crypto";
10496
+ import { randomUUID as randomUUID3 } from "crypto";
9798
10497
  function createPostgresTodosStorageAdapter(options) {
9799
10498
  const store = new PostgresJsonRecordStore(options);
9800
10499
  const adapter = {
@@ -9977,7 +10676,7 @@ async function createTask2(input, store, context) {
9977
10676
  const timestamp = new Date().toISOString();
9978
10677
  const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
9979
10678
  const task = {
9980
- id: randomUUID2(),
10679
+ id: randomUUID3(),
9981
10680
  short_id: shortId,
9982
10681
  project_id: input.project_id ?? context?.projectId ?? null,
9983
10682
  parent_id: input.parent_id ?? null,
@@ -10144,7 +10843,7 @@ async function getChangedSince(since, filters, store) {
10144
10843
  async function createProject2(input, store, context) {
10145
10844
  const timestamp = new Date().toISOString();
10146
10845
  const project = {
10147
- id: randomUUID2(),
10846
+ id: randomUUID3(),
10148
10847
  name: input.name,
10149
10848
  path: input.path,
10150
10849
  description: input.description ?? null,
@@ -10164,7 +10863,7 @@ async function updateProject2(id, input, store) {
10164
10863
  async function createPlan2(input, store, context) {
10165
10864
  const timestamp = new Date().toISOString();
10166
10865
  return store.upsert("plans", {
10167
- id: randomUUID2(),
10866
+ id: randomUUID3(),
10168
10867
  project_id: input.project_id ?? context?.projectId ?? null,
10169
10868
  task_list_id: input.task_list_id ?? context?.taskListId ?? null,
10170
10869
  agent_id: input.agent_id ?? context?.agentId ?? null,
@@ -10186,7 +10885,7 @@ async function registerAgent2(input, store, context) {
10186
10885
  }
10187
10886
  const timestamp = new Date().toISOString();
10188
10887
  const agent = {
10189
- id: existing?.id ?? randomUUID2().slice(0, 8),
10888
+ id: existing?.id ?? randomUUID3().slice(0, 8),
10190
10889
  name: input.name,
10191
10890
  description: input.description ?? existing?.description ?? null,
10192
10891
  role: input.role ?? existing?.role ?? null,
@@ -10222,7 +10921,7 @@ async function updateAgent2(id, input, store) {
10222
10921
  async function createTaskList2(input, store, context) {
10223
10922
  const timestamp = new Date().toISOString();
10224
10923
  return store.upsert("task_lists", {
10225
- id: randomUUID2(),
10924
+ id: randomUUID3(),
10226
10925
  project_id: input.project_id ?? context?.projectId ?? null,
10227
10926
  slug: input.slug ?? slugify2(input.name),
10228
10927
  name: input.name,
@@ -10244,7 +10943,7 @@ async function updateTaskList2(id, input, store) {
10244
10943
  async function createTemplate2(input, store, context) {
10245
10944
  const timestamp = new Date().toISOString();
10246
10945
  return store.upsert("templates", {
10247
- id: randomUUID2(),
10946
+ id: randomUUID3(),
10248
10947
  name: input.name,
10249
10948
  title_pattern: input.title_pattern,
10250
10949
  description: input.description ?? null,
@@ -10273,7 +10972,7 @@ async function updateTemplate2(id, input, store) {
10273
10972
  }
10274
10973
  async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
10275
10974
  const entry2 = {
10276
- id: randomUUID2(),
10975
+ id: randomUUID3(),
10277
10976
  task_id: taskId,
10278
10977
  action,
10279
10978
  field: field ?? null,
@@ -10286,7 +10985,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
10286
10985
  }
10287
10986
  async function addComment2(input, store, context) {
10288
10987
  const comment = {
10289
- id: randomUUID2(),
10988
+ id: randomUUID3(),
10290
10989
  task_id: input.task_id,
10291
10990
  agent_id: input.agent_id ?? context?.agentId ?? null,
10292
10991
  session_id: input.session_id ?? context?.sessionId ?? null,
@@ -10461,10 +11160,10 @@ function assertRemoteAdapterCapabilities(adapter, mode) {
10461
11160
  }
10462
11161
  }
10463
11162
  // src/storage/s3-artifacts.ts
10464
- import { createHash as createHash3, createHmac } from "crypto";
11163
+ import { createHash as createHash3, createHmac as createHmac2 } from "crypto";
10465
11164
  function createTodosS3ArtifactStore(options) {
10466
11165
  const requestFetch = options.fetch ?? fetch;
10467
- const now2 = options.now ?? (() => new Date);
11166
+ const now3 = options.now ?? (() => new Date);
10468
11167
  return {
10469
11168
  objectKey: (relativePath) => buildS3ObjectKey(options.config, relativePath),
10470
11169
  objectUrl: (relativePath) => buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath)),
@@ -10486,7 +11185,7 @@ function createTodosS3ArtifactStore(options) {
10486
11185
  headers,
10487
11186
  body,
10488
11187
  credentials: options.credentials,
10489
- now: now2()
11188
+ now: now3()
10490
11189
  });
10491
11190
  const response = await requestFetch(url, { method: "PUT", headers: signed.headers, body });
10492
11191
  if (!response.ok)
@@ -10507,7 +11206,7 @@ function createTodosS3ArtifactStore(options) {
10507
11206
  service: "s3",
10508
11207
  headers: {},
10509
11208
  credentials: options.credentials,
10510
- now: now2()
11209
+ now: now3()
10511
11210
  });
10512
11211
  const response = await requestFetch(url, { method: "GET", headers: signed.headers });
10513
11212
  if (!response.ok)
@@ -10523,7 +11222,7 @@ function createTodosS3ArtifactStore(options) {
10523
11222
  service: "s3",
10524
11223
  headers: {},
10525
11224
  credentials: options.credentials,
10526
- now: now2()
11225
+ now: now3()
10527
11226
  });
10528
11227
  const response = await requestFetch(url, { method: "DELETE", headers: signed.headers });
10529
11228
  if (!response.ok && response.status !== 404)
@@ -10636,10 +11335,10 @@ function sha256Hex(value) {
10636
11335
  return createHash3("sha256").update(value).digest("hex");
10637
11336
  }
10638
11337
  function hmac(key, value) {
10639
- return createHmac("sha256", key).update(value).digest();
11338
+ return createHmac2("sha256", key).update(value).digest();
10640
11339
  }
10641
11340
  function hmacHex(key, value) {
10642
- return createHmac("sha256", key).update(value).digest("hex");
11341
+ return createHmac2("sha256", key).update(value).digest("hex");
10643
11342
  }
10644
11343
  function getSigningKey(secretAccessKey, dateStamp, region, service) {
10645
11344
  const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp);
@@ -10651,7 +11350,7 @@ function getSigningKey(secretAccessKey, dateStamp, region, service) {
10651
11350
  init_database();
10652
11351
  async function uploadRunArtifactsToS3(options) {
10653
11352
  const db = options.db ?? getDatabase();
10654
- const now2 = options.now ?? (() => new Date);
11353
+ const now3 = options.now ?? (() => new Date);
10655
11354
  const result = emptyResult();
10656
11355
  for (const artifact of listRunArtifacts(db, options.filter)) {
10657
11356
  try {
@@ -10690,7 +11389,7 @@ async function uploadRunArtifactsToS3(options) {
10690
11389
  url: ref.url,
10691
11390
  sha256: content.sha256,
10692
11391
  size_bytes: content.size_bytes,
10693
- uploaded_at: now2().toISOString()
11392
+ uploaded_at: now3().toISOString()
10694
11393
  };
10695
11394
  updateArtifactMetadata(db, artifact.id, {
10696
11395
  ...metadata,
@@ -10765,7 +11464,7 @@ function planRunArtifactsS3Sync(options) {
10765
11464
  }
10766
11465
  async function downloadRunArtifactsFromS3(options) {
10767
11466
  const db = options.db ?? getDatabase();
10768
- const now2 = options.now ?? (() => new Date);
11467
+ const now3 = options.now ?? (() => new Date);
10769
11468
  const result = emptyResult();
10770
11469
  for (const artifact of listRunArtifacts(db, options.filter)) {
10771
11470
  try {
@@ -10801,7 +11500,7 @@ async function downloadRunArtifactsFromS3(options) {
10801
11500
  ...metadata,
10802
11501
  remote_artifact_store: {
10803
11502
  ...remote,
10804
- downloaded_at: now2().toISOString()
11503
+ downloaded_at: now3().toISOString()
10805
11504
  }
10806
11505
  });
10807
11506
  result.downloaded += 1;