@hasna/todos 0.11.56 → 0.11.57
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/README.md +34 -0
- package/dist/cli/commands/dispatch.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +0 -2
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +2689 -2030
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1222 -93
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1622 -291
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/dispatch.d.ts +3 -0
- package/dist/lib/dispatch.d.ts.map +1 -1
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/json-schemas.d.ts +1 -1
- package/dist/lib/json-schemas.d.ts.map +1 -1
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts +14 -0
- package/dist/lib/shared-events.d.ts.map +1 -0
- package/dist/lib/tester-issue-reports.d.ts +105 -0
- package/dist/lib/tester-issue-reports.d.ts.map +1 -0
- package/dist/lib/tmux.d.ts +19 -1
- package/dist/lib/tmux.d.ts.map +1 -1
- package/dist/mcp/index.js +865 -172
- package/dist/mcp/tools/dispatch.d.ts.map +1 -1
- package/dist/registry.js +1214 -93
- package/dist/release-provenance.json +7 -0
- package/dist/server/index.js +861 -168
- package/dist/storage.js +661 -65
- package/package.json +1 -1
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
|
|
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 (!
|
|
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,568 @@ async function testLocalEventHook(name, input) {
|
|
|
4417
4418
|
return emitLocalEventHooks({ ...input, hooks: [hook] });
|
|
4418
4419
|
}
|
|
4419
4420
|
|
|
4421
|
+
// node_modules/.bun/@hasna+events@0.1.7/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) {
|
|
4439
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
4440
|
+
return new RegExp(`^${escaped}$`);
|
|
4441
|
+
}
|
|
4442
|
+
function matchString(value, matcher) {
|
|
4443
|
+
if (matcher === undefined)
|
|
4444
|
+
return true;
|
|
4445
|
+
if (value === undefined)
|
|
4446
|
+
return false;
|
|
4447
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
4448
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
4449
|
+
}
|
|
4450
|
+
function matchRecord(input, matcher) {
|
|
4451
|
+
if (!matcher)
|
|
4452
|
+
return true;
|
|
4453
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
4454
|
+
const actual = getPathValue(input, path);
|
|
4455
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
4456
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
4457
|
+
}
|
|
4458
|
+
return actual === expected;
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4461
|
+
function eventMatchesFilter(event, filter) {
|
|
4462
|
+
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);
|
|
4463
|
+
}
|
|
4464
|
+
function channelMatchesEvent(channel, event) {
|
|
4465
|
+
if (!channel.enabled)
|
|
4466
|
+
return false;
|
|
4467
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
4468
|
+
return true;
|
|
4469
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
4470
|
+
}
|
|
4471
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
4472
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
4473
|
+
function getEventsDataDir(override) {
|
|
4474
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
class JsonEventsStore {
|
|
4478
|
+
dataDir;
|
|
4479
|
+
channelsPath;
|
|
4480
|
+
eventsPath;
|
|
4481
|
+
deliveriesPath;
|
|
4482
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
4483
|
+
this.dataDir = dataDir;
|
|
4484
|
+
this.channelsPath = join4(dataDir, "channels.json");
|
|
4485
|
+
this.eventsPath = join4(dataDir, "events.json");
|
|
4486
|
+
this.deliveriesPath = join4(dataDir, "deliveries.json");
|
|
4487
|
+
}
|
|
4488
|
+
async init() {
|
|
4489
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
4490
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
4491
|
+
return;
|
|
4492
|
+
});
|
|
4493
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
4494
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
4495
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
4496
|
+
}
|
|
4497
|
+
async addChannel(channel) {
|
|
4498
|
+
await this.init();
|
|
4499
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
4500
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
4501
|
+
if (index >= 0) {
|
|
4502
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
4503
|
+
} else {
|
|
4504
|
+
channels.push(channel);
|
|
4505
|
+
}
|
|
4506
|
+
await this.writeJson(this.channelsPath, channels);
|
|
4507
|
+
return index >= 0 ? channels[index] : channel;
|
|
4508
|
+
}
|
|
4509
|
+
async listChannels() {
|
|
4510
|
+
await this.init();
|
|
4511
|
+
return this.readJson(this.channelsPath, []);
|
|
4512
|
+
}
|
|
4513
|
+
async getChannel(id) {
|
|
4514
|
+
const channels = await this.listChannels();
|
|
4515
|
+
return channels.find((channel) => channel.id === id);
|
|
4516
|
+
}
|
|
4517
|
+
async removeChannel(id) {
|
|
4518
|
+
await this.init();
|
|
4519
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
4520
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
4521
|
+
await this.writeJson(this.channelsPath, next);
|
|
4522
|
+
return next.length !== channels.length;
|
|
4523
|
+
}
|
|
4524
|
+
async appendEvent(event) {
|
|
4525
|
+
await this.init();
|
|
4526
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
4527
|
+
events.push(event);
|
|
4528
|
+
await this.writeJson(this.eventsPath, events);
|
|
4529
|
+
return event;
|
|
4530
|
+
}
|
|
4531
|
+
async listEvents() {
|
|
4532
|
+
await this.init();
|
|
4533
|
+
return this.readJson(this.eventsPath, []);
|
|
4534
|
+
}
|
|
4535
|
+
async findEventByIdentity(identity) {
|
|
4536
|
+
const events = await this.listEvents();
|
|
4537
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
4538
|
+
}
|
|
4539
|
+
async appendDelivery(result) {
|
|
4540
|
+
await this.init();
|
|
4541
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
4542
|
+
deliveries.push(result);
|
|
4543
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
4544
|
+
return result;
|
|
4545
|
+
}
|
|
4546
|
+
async listDeliveries() {
|
|
4547
|
+
await this.init();
|
|
4548
|
+
return this.readJson(this.deliveriesPath, []);
|
|
4549
|
+
}
|
|
4550
|
+
async exportData() {
|
|
4551
|
+
return {
|
|
4552
|
+
channels: await this.listChannels(),
|
|
4553
|
+
events: await this.listEvents(),
|
|
4554
|
+
deliveries: await this.listDeliveries()
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
async ensureArrayFile(path) {
|
|
4558
|
+
if (!existsSync5(path)) {
|
|
4559
|
+
await writeFile(path, `[]
|
|
4560
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
4561
|
+
}
|
|
4562
|
+
await chmod(path, 384).catch(() => {
|
|
4563
|
+
return;
|
|
4564
|
+
});
|
|
4565
|
+
}
|
|
4566
|
+
async readJson(path, fallback) {
|
|
4567
|
+
try {
|
|
4568
|
+
const raw = await readFile(path, "utf-8");
|
|
4569
|
+
if (!raw.trim())
|
|
4570
|
+
return fallback;
|
|
4571
|
+
return JSON.parse(raw);
|
|
4572
|
+
} catch (error) {
|
|
4573
|
+
if (error.code === "ENOENT")
|
|
4574
|
+
return fallback;
|
|
4575
|
+
throw error;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
async writeJson(path, value) {
|
|
4579
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
4580
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
4581
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
4582
|
+
await rename(tempPath, path);
|
|
4583
|
+
await chmod(path, 384).catch(() => {
|
|
4584
|
+
return;
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
4589
|
+
function buildSignatureBase(timestamp, body) {
|
|
4590
|
+
return `${timestamp}.${body}`;
|
|
4591
|
+
}
|
|
4592
|
+
function signPayload(secret, timestamp, body) {
|
|
4593
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
4594
|
+
return `sha256=${digest}`;
|
|
4595
|
+
}
|
|
4596
|
+
function now2() {
|
|
4597
|
+
return new Date().toISOString();
|
|
4598
|
+
}
|
|
4599
|
+
function truncate(value, max = 4096) {
|
|
4600
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
4601
|
+
}
|
|
4602
|
+
function buildWebhookRequest(event, channel) {
|
|
4603
|
+
if (!channel.webhook)
|
|
4604
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
4605
|
+
const body = JSON.stringify(event);
|
|
4606
|
+
const timestamp = event.time;
|
|
4607
|
+
const headers = {
|
|
4608
|
+
"Content-Type": "application/json",
|
|
4609
|
+
"User-Agent": "@hasna/events",
|
|
4610
|
+
"X-Hasna-Event-Id": event.id,
|
|
4611
|
+
"X-Hasna-Event-Type": event.type,
|
|
4612
|
+
"X-Hasna-Timestamp": timestamp,
|
|
4613
|
+
...channel.webhook.headers
|
|
4614
|
+
};
|
|
4615
|
+
if (channel.webhook.secret) {
|
|
4616
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
4617
|
+
}
|
|
4618
|
+
return { body, headers };
|
|
4619
|
+
}
|
|
4620
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
4621
|
+
if (!channel.webhook)
|
|
4622
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
4623
|
+
const startedAt = now2();
|
|
4624
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
4625
|
+
const controller = new AbortController;
|
|
4626
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
4627
|
+
try {
|
|
4628
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
4629
|
+
method: "POST",
|
|
4630
|
+
headers,
|
|
4631
|
+
body,
|
|
4632
|
+
signal: controller.signal
|
|
4633
|
+
});
|
|
4634
|
+
const responseBody = truncate(await response.text());
|
|
4635
|
+
return {
|
|
4636
|
+
attempt: 1,
|
|
4637
|
+
status: response.ok ? "success" : "failed",
|
|
4638
|
+
startedAt,
|
|
4639
|
+
completedAt: now2(),
|
|
4640
|
+
responseStatus: response.status,
|
|
4641
|
+
responseBody,
|
|
4642
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
4643
|
+
};
|
|
4644
|
+
} catch (error) {
|
|
4645
|
+
return {
|
|
4646
|
+
attempt: 1,
|
|
4647
|
+
status: "failed",
|
|
4648
|
+
startedAt,
|
|
4649
|
+
completedAt: now2(),
|
|
4650
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4651
|
+
};
|
|
4652
|
+
} finally {
|
|
4653
|
+
clearTimeout(timeout);
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
async function dispatchCommand(event, channel) {
|
|
4657
|
+
if (!channel.command)
|
|
4658
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
4659
|
+
const startedAt = now2();
|
|
4660
|
+
const eventJson = JSON.stringify(event);
|
|
4661
|
+
const env = {
|
|
4662
|
+
...process.env,
|
|
4663
|
+
...channel.command.env,
|
|
4664
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
4665
|
+
HASNA_EVENT_ID: event.id,
|
|
4666
|
+
HASNA_EVENT_TYPE: event.type,
|
|
4667
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
4668
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
4669
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
4670
|
+
HASNA_EVENT_TIME: event.time,
|
|
4671
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
4672
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
4673
|
+
HASNA_EVENT_JSON: eventJson
|
|
4674
|
+
};
|
|
4675
|
+
return new Promise((resolve6) => {
|
|
4676
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
4677
|
+
cwd: channel.command.cwd,
|
|
4678
|
+
env,
|
|
4679
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4680
|
+
});
|
|
4681
|
+
let stdout = "";
|
|
4682
|
+
let stderr = "";
|
|
4683
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
4684
|
+
child.stdin.end(eventJson);
|
|
4685
|
+
child.stdout.on("data", (chunk) => {
|
|
4686
|
+
stdout += chunk.toString();
|
|
4687
|
+
});
|
|
4688
|
+
child.stderr.on("data", (chunk) => {
|
|
4689
|
+
stderr += chunk.toString();
|
|
4690
|
+
});
|
|
4691
|
+
child.on("error", (error) => {
|
|
4692
|
+
clearTimeout(timeout);
|
|
4693
|
+
resolve6({
|
|
4694
|
+
attempt: 1,
|
|
4695
|
+
status: "failed",
|
|
4696
|
+
startedAt,
|
|
4697
|
+
completedAt: now2(),
|
|
4698
|
+
stdout: truncate(stdout),
|
|
4699
|
+
stderr: truncate(stderr),
|
|
4700
|
+
error: error.message
|
|
4701
|
+
});
|
|
4702
|
+
});
|
|
4703
|
+
child.on("close", (code, signal) => {
|
|
4704
|
+
clearTimeout(timeout);
|
|
4705
|
+
const success = code === 0;
|
|
4706
|
+
resolve6({
|
|
4707
|
+
attempt: 1,
|
|
4708
|
+
status: success ? "success" : "failed",
|
|
4709
|
+
startedAt,
|
|
4710
|
+
completedAt: now2(),
|
|
4711
|
+
stdout: truncate(stdout),
|
|
4712
|
+
stderr: truncate(stderr),
|
|
4713
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
4714
|
+
});
|
|
4715
|
+
});
|
|
4716
|
+
});
|
|
4717
|
+
}
|
|
4718
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
4719
|
+
if (channel.transport === "webhook")
|
|
4720
|
+
return dispatchWebhook(event, channel, options);
|
|
4721
|
+
if (channel.transport === "command")
|
|
4722
|
+
return dispatchCommand(event, channel);
|
|
4723
|
+
return {
|
|
4724
|
+
attempt: 1,
|
|
4725
|
+
status: "skipped",
|
|
4726
|
+
startedAt: now2(),
|
|
4727
|
+
completedAt: now2(),
|
|
4728
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
4732
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
4733
|
+
return {
|
|
4734
|
+
id: randomUUID2(),
|
|
4735
|
+
eventId: event.id,
|
|
4736
|
+
channelId: channel.id,
|
|
4737
|
+
transport: channel.transport,
|
|
4738
|
+
status,
|
|
4739
|
+
attempts,
|
|
4740
|
+
createdAt: attempts[0]?.startedAt ?? now2(),
|
|
4741
|
+
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4744
|
+
function createEvent(input) {
|
|
4745
|
+
return {
|
|
4746
|
+
id: input.id ?? randomUUID22(),
|
|
4747
|
+
source: input.source,
|
|
4748
|
+
type: input.type,
|
|
4749
|
+
time: normalizeTime(input.time),
|
|
4750
|
+
subject: input.subject,
|
|
4751
|
+
severity: input.severity ?? "info",
|
|
4752
|
+
data: input.data ?? {},
|
|
4753
|
+
message: input.message,
|
|
4754
|
+
dedupeKey: input.dedupeKey,
|
|
4755
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
4756
|
+
metadata: input.metadata ?? {}
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
class EventsClient {
|
|
4761
|
+
store;
|
|
4762
|
+
redactors;
|
|
4763
|
+
transportOptions;
|
|
4764
|
+
constructor(options = {}) {
|
|
4765
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
4766
|
+
this.redactors = options.redactors ?? [];
|
|
4767
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
4768
|
+
}
|
|
4769
|
+
async addChannel(input) {
|
|
4770
|
+
const timestamp = new Date().toISOString();
|
|
4771
|
+
return this.store.addChannel({
|
|
4772
|
+
...input,
|
|
4773
|
+
createdAt: input.createdAt ?? timestamp,
|
|
4774
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
4775
|
+
});
|
|
4776
|
+
}
|
|
4777
|
+
async listChannels() {
|
|
4778
|
+
return this.store.listChannels();
|
|
4779
|
+
}
|
|
4780
|
+
async removeChannel(id) {
|
|
4781
|
+
return this.store.removeChannel(id);
|
|
4782
|
+
}
|
|
4783
|
+
async emit(input, options = {}) {
|
|
4784
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
4785
|
+
if (options.dedupe !== false) {
|
|
4786
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
4787
|
+
if (existing) {
|
|
4788
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
await this.store.appendEvent(event);
|
|
4792
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
4793
|
+
return { event, deliveries, deduped: false };
|
|
4794
|
+
}
|
|
4795
|
+
async listEvents() {
|
|
4796
|
+
return this.store.listEvents();
|
|
4797
|
+
}
|
|
4798
|
+
async listDeliveries() {
|
|
4799
|
+
return this.store.listDeliveries();
|
|
4800
|
+
}
|
|
4801
|
+
async deliver(event) {
|
|
4802
|
+
const channels = await this.store.listChannels();
|
|
4803
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
4804
|
+
const deliveries = [];
|
|
4805
|
+
for (const channel of selected) {
|
|
4806
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4807
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4808
|
+
await this.store.appendDelivery(result);
|
|
4809
|
+
deliveries.push(result);
|
|
4810
|
+
}
|
|
4811
|
+
return deliveries;
|
|
4812
|
+
}
|
|
4813
|
+
async testChannel(id, input = {}) {
|
|
4814
|
+
const channel = await this.store.getChannel(id);
|
|
4815
|
+
if (!channel)
|
|
4816
|
+
throw new Error(`Channel not found: ${id}`);
|
|
4817
|
+
const event = createEvent({
|
|
4818
|
+
source: input.source ?? "hasna.events",
|
|
4819
|
+
type: input.type ?? "events.test",
|
|
4820
|
+
subject: input.subject ?? id,
|
|
4821
|
+
severity: input.severity ?? "info",
|
|
4822
|
+
data: input.data ?? { test: true },
|
|
4823
|
+
message: input.message ?? "Hasna events test delivery",
|
|
4824
|
+
dedupeKey: input.dedupeKey,
|
|
4825
|
+
schemaVersion: input.schemaVersion,
|
|
4826
|
+
metadata: input.metadata,
|
|
4827
|
+
time: input.time,
|
|
4828
|
+
id: input.id
|
|
4829
|
+
});
|
|
4830
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4831
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4832
|
+
await this.store.appendDelivery(result);
|
|
4833
|
+
return result;
|
|
4834
|
+
}
|
|
4835
|
+
async replay(options = {}) {
|
|
4836
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
4837
|
+
if (options.eventId && event.id !== options.eventId)
|
|
4838
|
+
return false;
|
|
4839
|
+
if (options.source && event.source !== options.source)
|
|
4840
|
+
return false;
|
|
4841
|
+
if (options.type && event.type !== options.type)
|
|
4842
|
+
return false;
|
|
4843
|
+
return true;
|
|
4844
|
+
});
|
|
4845
|
+
if (options.dryRun)
|
|
4846
|
+
return { events, deliveries: [] };
|
|
4847
|
+
const deliveries = [];
|
|
4848
|
+
for (const event of events) {
|
|
4849
|
+
deliveries.push(...await this.deliver(event));
|
|
4850
|
+
}
|
|
4851
|
+
return { events, deliveries };
|
|
4852
|
+
}
|
|
4853
|
+
async applyRedaction(event, channel) {
|
|
4854
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
4855
|
+
for (const redactor of this.redactors) {
|
|
4856
|
+
next = await redactor(next, channel);
|
|
4857
|
+
}
|
|
4858
|
+
return next;
|
|
4859
|
+
}
|
|
4860
|
+
async deliverWithRetry(event, channel) {
|
|
4861
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
4862
|
+
const attempts = [];
|
|
4863
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
4864
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
4865
|
+
attempt.attempt = index + 1;
|
|
4866
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
4867
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
4868
|
+
}
|
|
4869
|
+
attempts.push(attempt);
|
|
4870
|
+
if (attempt.status !== "failed")
|
|
4871
|
+
break;
|
|
4872
|
+
if (attempt.nextBackoffMs)
|
|
4873
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
4874
|
+
}
|
|
4875
|
+
return createDeliveryResult(event, channel, attempts);
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
4879
|
+
if (paths.length === 0)
|
|
4880
|
+
return event;
|
|
4881
|
+
const copy = structuredClone(event);
|
|
4882
|
+
for (const path of paths) {
|
|
4883
|
+
setPath(copy, path, replacement);
|
|
4884
|
+
}
|
|
4885
|
+
return copy;
|
|
4886
|
+
}
|
|
4887
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
4888
|
+
return redactValue2(event, replacement);
|
|
4889
|
+
}
|
|
4890
|
+
function shouldRedactKey(key) {
|
|
4891
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
4892
|
+
}
|
|
4893
|
+
function redactValue2(value, replacement) {
|
|
4894
|
+
if (Array.isArray(value))
|
|
4895
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
4896
|
+
if (!value || typeof value !== "object")
|
|
4897
|
+
return value;
|
|
4898
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
4899
|
+
key,
|
|
4900
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
4901
|
+
]));
|
|
4902
|
+
}
|
|
4903
|
+
function setPath(input, path, replacement) {
|
|
4904
|
+
const parts = path.split(".");
|
|
4905
|
+
let cursor = input;
|
|
4906
|
+
for (const part of parts.slice(0, -1)) {
|
|
4907
|
+
const next = cursor[part];
|
|
4908
|
+
if (!next || typeof next !== "object")
|
|
4909
|
+
return;
|
|
4910
|
+
cursor = next;
|
|
4911
|
+
}
|
|
4912
|
+
const last = parts.at(-1);
|
|
4913
|
+
if (last && last in cursor)
|
|
4914
|
+
cursor[last] = replacement;
|
|
4915
|
+
}
|
|
4916
|
+
function normalizeTime(value) {
|
|
4917
|
+
if (!value)
|
|
4918
|
+
return new Date().toISOString();
|
|
4919
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
4920
|
+
}
|
|
4921
|
+
function normalizeRetryPolicy(policy) {
|
|
4922
|
+
return {
|
|
4923
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
4924
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
4925
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
4926
|
+
};
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
// src/lib/shared-events.ts
|
|
4930
|
+
var SOURCE = "todos";
|
|
4931
|
+
function taskEventData(task, extra = {}) {
|
|
4932
|
+
return {
|
|
4933
|
+
id: task.id,
|
|
4934
|
+
task_id: task.id,
|
|
4935
|
+
short_id: task.short_id,
|
|
4936
|
+
title: task.title,
|
|
4937
|
+
description: task.description,
|
|
4938
|
+
status: task.status,
|
|
4939
|
+
priority: task.priority,
|
|
4940
|
+
project_id: task.project_id,
|
|
4941
|
+
parent_id: task.parent_id,
|
|
4942
|
+
plan_id: task.plan_id,
|
|
4943
|
+
task_list_id: task.task_list_id,
|
|
4944
|
+
agent_id: task.agent_id,
|
|
4945
|
+
assigned_to: task.assigned_to,
|
|
4946
|
+
session_id: task.session_id,
|
|
4947
|
+
working_dir: task.working_dir,
|
|
4948
|
+
tags: task.tags,
|
|
4949
|
+
metadata: task.metadata,
|
|
4950
|
+
version: task.version,
|
|
4951
|
+
created_at: task.created_at,
|
|
4952
|
+
updated_at: task.updated_at,
|
|
4953
|
+
started_at: task.started_at,
|
|
4954
|
+
completed_at: task.completed_at,
|
|
4955
|
+
due_at: task.due_at,
|
|
4956
|
+
...extra
|
|
4957
|
+
};
|
|
4958
|
+
}
|
|
4959
|
+
async function emitSharedTaskEvent(input) {
|
|
4960
|
+
const data = taskEventData(input.task, input.data);
|
|
4961
|
+
await new EventsClient().emit({
|
|
4962
|
+
source: SOURCE,
|
|
4963
|
+
type: input.type,
|
|
4964
|
+
subject: input.task.id,
|
|
4965
|
+
severity: input.severity ?? "info",
|
|
4966
|
+
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
4967
|
+
data,
|
|
4968
|
+
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
4969
|
+
metadata: {
|
|
4970
|
+
package: "@hasna/todos",
|
|
4971
|
+
task_id: input.task.id,
|
|
4972
|
+
project_id: input.task.project_id,
|
|
4973
|
+
task_list_id: input.task.task_list_id
|
|
4974
|
+
}
|
|
4975
|
+
}, { deliver: true, dedupe: true });
|
|
4976
|
+
}
|
|
4977
|
+
function emitSharedTaskEventQuiet(input) {
|
|
4978
|
+
emitSharedTaskEvent(input).catch(() => {
|
|
4979
|
+
return;
|
|
4980
|
+
});
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4420
4983
|
// src/db/audit.ts
|
|
4421
4984
|
init_database();
|
|
4422
4985
|
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
@@ -4679,7 +5242,7 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
4679
5242
|
activeDeliveries--;
|
|
4680
5243
|
}
|
|
4681
5244
|
}
|
|
4682
|
-
async function
|
|
5245
|
+
async function dispatchWebhook2(event, payload, db) {
|
|
4683
5246
|
const d = db || getDatabase();
|
|
4684
5247
|
const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
|
|
4685
5248
|
const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
|
|
@@ -4833,7 +5396,10 @@ function createTask(input, db) {
|
|
|
4833
5396
|
insertTaskTags(id, tags, d);
|
|
4834
5397
|
}
|
|
4835
5398
|
const task = getTask(id, d);
|
|
4836
|
-
|
|
5399
|
+
const payload = taskEventData(task);
|
|
5400
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
5401
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload });
|
|
5402
|
+
emitSharedTaskEventQuiet({ type: "task.created", task });
|
|
4837
5403
|
return task;
|
|
4838
5404
|
}
|
|
4839
5405
|
function getTask(id, db) {
|
|
@@ -5177,18 +5743,7 @@ function updateTask(id, input, db) {
|
|
|
5177
5743
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
5178
5744
|
if (input.approved_by !== undefined)
|
|
5179
5745
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
5180
|
-
|
|
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 {
|
|
5746
|
+
const updatedTask = {
|
|
5192
5747
|
...task,
|
|
5193
5748
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
5194
5749
|
tags: input.tags ?? task.tags,
|
|
@@ -5206,6 +5761,22 @@ function updateTask(id, input, db) {
|
|
|
5206
5761
|
approved_by: input.approved_by ?? task.approved_by,
|
|
5207
5762
|
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
5208
5763
|
};
|
|
5764
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
5765
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
5766
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
5767
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
5768
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
|
|
5769
|
+
}
|
|
5770
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
5771
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
5772
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
5773
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
|
|
5774
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
|
|
5775
|
+
}
|
|
5776
|
+
if (input.approved_by !== undefined) {
|
|
5777
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5778
|
+
}
|
|
5779
|
+
return updatedTask;
|
|
5209
5780
|
}
|
|
5210
5781
|
function deleteTask(id, db) {
|
|
5211
5782
|
const d = db || getDatabase();
|
|
@@ -5938,9 +6509,12 @@ function startTask(id, agentId, db) {
|
|
|
5938
6509
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
5939
6510
|
}
|
|
5940
6511
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
6512
|
+
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 };
|
|
6513
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
6514
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
6515
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload });
|
|
6516
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
|
|
6517
|
+
return startedTask;
|
|
5944
6518
|
}
|
|
5945
6519
|
function completeTask(id, agentId, db, options) {
|
|
5946
6520
|
const d = db || getDatabase();
|
|
@@ -5976,8 +6550,21 @@ function completeTask(id, agentId, db, options) {
|
|
|
5976
6550
|
});
|
|
5977
6551
|
tx();
|
|
5978
6552
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
5979
|
-
|
|
5980
|
-
|
|
6553
|
+
const completedTaskForEvent = {
|
|
6554
|
+
...task,
|
|
6555
|
+
status: "completed",
|
|
6556
|
+
locked_by: null,
|
|
6557
|
+
locked_at: null,
|
|
6558
|
+
completed_at: timestamp,
|
|
6559
|
+
confidence,
|
|
6560
|
+
version: task.version + 1,
|
|
6561
|
+
updated_at: timestamp,
|
|
6562
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
6563
|
+
};
|
|
6564
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
6565
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
6566
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
|
|
6567
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
|
|
5981
6568
|
let spawnedTask = null;
|
|
5982
6569
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
5983
6570
|
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
@@ -6018,8 +6605,12 @@ function completeTask(id, agentId, db, options) {
|
|
|
6018
6605
|
if (unblockedDeps.length > 0) {
|
|
6019
6606
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
6020
6607
|
for (const dep of unblockedDeps) {
|
|
6021
|
-
|
|
6022
|
-
|
|
6608
|
+
const depTask = getTask(dep.id, d);
|
|
6609
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
6610
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
6611
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
|
|
6612
|
+
if (depTask)
|
|
6613
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
|
|
6023
6614
|
}
|
|
6024
6615
|
}
|
|
6025
6616
|
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 +6796,6 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
6205
6796
|
const timestamp = now();
|
|
6206
6797
|
d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
6207
6798
|
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
6799
|
const failedTask = {
|
|
6212
6800
|
...task,
|
|
6213
6801
|
status: "failed",
|
|
@@ -6217,6 +6805,11 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
6217
6805
|
version: task.version + 1,
|
|
6218
6806
|
updated_at: timestamp
|
|
6219
6807
|
};
|
|
6808
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
6809
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
6810
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
6811
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
|
|
6812
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
|
|
6220
6813
|
let retryTask;
|
|
6221
6814
|
if (options?.retry) {
|
|
6222
6815
|
const retryCount = (task.retry_count || 0) + 1;
|
|
@@ -6291,9 +6884,12 @@ function stealTask(agentId, opts, db) {
|
|
|
6291
6884
|
return null;
|
|
6292
6885
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
6293
6886
|
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6887
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
6888
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
6889
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
6890
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload });
|
|
6891
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
|
|
6892
|
+
return stolenTask;
|
|
6297
6893
|
}
|
|
6298
6894
|
function claimOrSteal(agentId, filters, db) {
|
|
6299
6895
|
const d = db || getDatabase();
|
|
@@ -7352,8 +7948,8 @@ init_database();
|
|
|
7352
7948
|
init_database();
|
|
7353
7949
|
init_redaction();
|
|
7354
7950
|
import { createHash as createHash2 } from "crypto";
|
|
7355
|
-
import { existsSync as
|
|
7356
|
-
import { basename, dirname as dirname4, join as
|
|
7951
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
7952
|
+
import { basename, dirname as dirname4, join as join5, resolve as resolve6 } from "path";
|
|
7357
7953
|
import { tmpdir } from "os";
|
|
7358
7954
|
function isInMemoryDb2(path) {
|
|
7359
7955
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -7365,15 +7961,15 @@ function artifactStoreRoot() {
|
|
|
7365
7961
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
7366
7962
|
const dbPath = getDatabasePath();
|
|
7367
7963
|
if (isInMemoryDb2(dbPath))
|
|
7368
|
-
return
|
|
7369
|
-
return
|
|
7964
|
+
return join5(tmpdir(), "hasna-todos-artifacts");
|
|
7965
|
+
return join5(dirname4(resolve6(dbPath)), "artifacts");
|
|
7370
7966
|
}
|
|
7371
7967
|
function artifactStorePath(relativePath) {
|
|
7372
7968
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
7373
7969
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
7374
7970
|
throw new Error("Invalid artifact store path");
|
|
7375
7971
|
}
|
|
7376
|
-
return
|
|
7972
|
+
return join5(artifactStoreRoot(), normalized);
|
|
7377
7973
|
}
|
|
7378
7974
|
function sha256(buffer) {
|
|
7379
7975
|
return createHash2("sha256").update(buffer).digest("hex");
|
|
@@ -7414,7 +8010,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
7414
8010
|
}
|
|
7415
8011
|
function storeArtifactContent(input) {
|
|
7416
8012
|
const sourcePath = resolve6(input.path);
|
|
7417
|
-
if (!
|
|
8013
|
+
if (!existsSync7(sourcePath))
|
|
7418
8014
|
return null;
|
|
7419
8015
|
const sourceStat = statSync2(sourcePath);
|
|
7420
8016
|
if (!sourceStat.isFile())
|
|
@@ -7431,9 +8027,9 @@ function storeArtifactContent(input) {
|
|
|
7431
8027
|
redactionStatus = "redacted";
|
|
7432
8028
|
}
|
|
7433
8029
|
const storedSha = sha256(storedBuffer);
|
|
7434
|
-
const relativePath =
|
|
8030
|
+
const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
7435
8031
|
const destination = artifactStorePath(relativePath);
|
|
7436
|
-
if (!
|
|
8032
|
+
if (!existsSync7(destination)) {
|
|
7437
8033
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
7438
8034
|
writeFileSync2(destination, storedBuffer);
|
|
7439
8035
|
}
|
|
@@ -7493,7 +8089,7 @@ function verifyStoredArtifact(input) {
|
|
|
7493
8089
|
};
|
|
7494
8090
|
}
|
|
7495
8091
|
const storedPath = artifactStorePath(store.relative_path);
|
|
7496
|
-
if (!
|
|
8092
|
+
if (!existsSync7(storedPath)) {
|
|
7497
8093
|
return {
|
|
7498
8094
|
id: input.id,
|
|
7499
8095
|
path: input.path,
|
|
@@ -7574,15 +8170,15 @@ function getArtifactStoreRoot(dbPath) {
|
|
|
7574
8170
|
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
7575
8171
|
const path = dbPath ?? getDatabasePath();
|
|
7576
8172
|
if (isInMemoryDb2(path))
|
|
7577
|
-
return
|
|
7578
|
-
return
|
|
8173
|
+
return join5(tmpdir(), "hasna-todos-artifacts");
|
|
8174
|
+
return join5(dirname4(resolve6(path)), "artifacts");
|
|
7579
8175
|
}
|
|
7580
8176
|
function computeContentHash(path) {
|
|
7581
8177
|
return sha256(readFileSync3(resolve6(path)));
|
|
7582
8178
|
}
|
|
7583
8179
|
function storeArtifactFile(input) {
|
|
7584
8180
|
const sourcePath = resolve6(input.sourcePath);
|
|
7585
|
-
if (!
|
|
8181
|
+
if (!existsSync7(sourcePath)) {
|
|
7586
8182
|
throw new Error(`Source file not found: ${input.sourcePath}`);
|
|
7587
8183
|
}
|
|
7588
8184
|
if (!statSync2(sourcePath).isFile()) {
|
|
@@ -7595,7 +8191,7 @@ function storeArtifactFile(input) {
|
|
|
7595
8191
|
let localPath = sourcePath;
|
|
7596
8192
|
if (storageMode === "copy") {
|
|
7597
8193
|
const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
|
|
7598
|
-
const destination =
|
|
8194
|
+
const destination = join5(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
|
|
7599
8195
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
7600
8196
|
writeFileSync2(destination, buffer);
|
|
7601
8197
|
localPath = destination;
|
|
@@ -7605,7 +8201,7 @@ function storeArtifactFile(input) {
|
|
|
7605
8201
|
function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
|
|
7606
8202
|
if (storageMode === "reference")
|
|
7607
8203
|
return false;
|
|
7608
|
-
if (!localPath || !
|
|
8204
|
+
if (!localPath || !existsSync7(localPath))
|
|
7609
8205
|
return false;
|
|
7610
8206
|
rmSync(localPath, { force: true });
|
|
7611
8207
|
try {
|
|
@@ -7617,8 +8213,8 @@ function isArtifactExpired(deletedAt, policy = {}) {
|
|
|
7617
8213
|
if (!deletedAt)
|
|
7618
8214
|
return false;
|
|
7619
8215
|
const retentionDays = policy.deleted_retention_days ?? DEFAULT_DELETED_RETENTION_DAYS;
|
|
7620
|
-
const
|
|
7621
|
-
const ageMs =
|
|
8216
|
+
const now3 = policy.now ?? new Date;
|
|
8217
|
+
const ageMs = now3.getTime() - new Date(deletedAt).getTime();
|
|
7622
8218
|
return ageMs > retentionDays * 24 * 60 * 60 * 1000;
|
|
7623
8219
|
}
|
|
7624
8220
|
function buildArtifactExportManifest(artifacts, dbPath) {
|
|
@@ -9794,7 +10390,7 @@ function createHybridTodosStorageAdapter(options) {
|
|
|
9794
10390
|
}
|
|
9795
10391
|
|
|
9796
10392
|
// src/storage/postgres-adapter.ts
|
|
9797
|
-
import { randomUUID as
|
|
10393
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
9798
10394
|
function createPostgresTodosStorageAdapter(options) {
|
|
9799
10395
|
const store = new PostgresJsonRecordStore(options);
|
|
9800
10396
|
const adapter = {
|
|
@@ -9977,7 +10573,7 @@ async function createTask2(input, store, context) {
|
|
|
9977
10573
|
const timestamp = new Date().toISOString();
|
|
9978
10574
|
const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
|
|
9979
10575
|
const task = {
|
|
9980
|
-
id:
|
|
10576
|
+
id: randomUUID3(),
|
|
9981
10577
|
short_id: shortId,
|
|
9982
10578
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
9983
10579
|
parent_id: input.parent_id ?? null,
|
|
@@ -10144,7 +10740,7 @@ async function getChangedSince(since, filters, store) {
|
|
|
10144
10740
|
async function createProject2(input, store, context) {
|
|
10145
10741
|
const timestamp = new Date().toISOString();
|
|
10146
10742
|
const project = {
|
|
10147
|
-
id:
|
|
10743
|
+
id: randomUUID3(),
|
|
10148
10744
|
name: input.name,
|
|
10149
10745
|
path: input.path,
|
|
10150
10746
|
description: input.description ?? null,
|
|
@@ -10164,7 +10760,7 @@ async function updateProject2(id, input, store) {
|
|
|
10164
10760
|
async function createPlan2(input, store, context) {
|
|
10165
10761
|
const timestamp = new Date().toISOString();
|
|
10166
10762
|
return store.upsert("plans", {
|
|
10167
|
-
id:
|
|
10763
|
+
id: randomUUID3(),
|
|
10168
10764
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
10169
10765
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
10170
10766
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
@@ -10186,7 +10782,7 @@ async function registerAgent2(input, store, context) {
|
|
|
10186
10782
|
}
|
|
10187
10783
|
const timestamp = new Date().toISOString();
|
|
10188
10784
|
const agent = {
|
|
10189
|
-
id: existing?.id ??
|
|
10785
|
+
id: existing?.id ?? randomUUID3().slice(0, 8),
|
|
10190
10786
|
name: input.name,
|
|
10191
10787
|
description: input.description ?? existing?.description ?? null,
|
|
10192
10788
|
role: input.role ?? existing?.role ?? null,
|
|
@@ -10222,7 +10818,7 @@ async function updateAgent2(id, input, store) {
|
|
|
10222
10818
|
async function createTaskList2(input, store, context) {
|
|
10223
10819
|
const timestamp = new Date().toISOString();
|
|
10224
10820
|
return store.upsert("task_lists", {
|
|
10225
|
-
id:
|
|
10821
|
+
id: randomUUID3(),
|
|
10226
10822
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
10227
10823
|
slug: input.slug ?? slugify2(input.name),
|
|
10228
10824
|
name: input.name,
|
|
@@ -10244,7 +10840,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
10244
10840
|
async function createTemplate2(input, store, context) {
|
|
10245
10841
|
const timestamp = new Date().toISOString();
|
|
10246
10842
|
return store.upsert("templates", {
|
|
10247
|
-
id:
|
|
10843
|
+
id: randomUUID3(),
|
|
10248
10844
|
name: input.name,
|
|
10249
10845
|
title_pattern: input.title_pattern,
|
|
10250
10846
|
description: input.description ?? null,
|
|
@@ -10273,7 +10869,7 @@ async function updateTemplate2(id, input, store) {
|
|
|
10273
10869
|
}
|
|
10274
10870
|
async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
|
|
10275
10871
|
const entry2 = {
|
|
10276
|
-
id:
|
|
10872
|
+
id: randomUUID3(),
|
|
10277
10873
|
task_id: taskId,
|
|
10278
10874
|
action,
|
|
10279
10875
|
field: field ?? null,
|
|
@@ -10286,7 +10882,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
10286
10882
|
}
|
|
10287
10883
|
async function addComment2(input, store, context) {
|
|
10288
10884
|
const comment = {
|
|
10289
|
-
id:
|
|
10885
|
+
id: randomUUID3(),
|
|
10290
10886
|
task_id: input.task_id,
|
|
10291
10887
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
10292
10888
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
@@ -10461,10 +11057,10 @@ function assertRemoteAdapterCapabilities(adapter, mode) {
|
|
|
10461
11057
|
}
|
|
10462
11058
|
}
|
|
10463
11059
|
// src/storage/s3-artifacts.ts
|
|
10464
|
-
import { createHash as createHash3, createHmac } from "crypto";
|
|
11060
|
+
import { createHash as createHash3, createHmac as createHmac2 } from "crypto";
|
|
10465
11061
|
function createTodosS3ArtifactStore(options) {
|
|
10466
11062
|
const requestFetch = options.fetch ?? fetch;
|
|
10467
|
-
const
|
|
11063
|
+
const now3 = options.now ?? (() => new Date);
|
|
10468
11064
|
return {
|
|
10469
11065
|
objectKey: (relativePath) => buildS3ObjectKey(options.config, relativePath),
|
|
10470
11066
|
objectUrl: (relativePath) => buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath)),
|
|
@@ -10486,7 +11082,7 @@ function createTodosS3ArtifactStore(options) {
|
|
|
10486
11082
|
headers,
|
|
10487
11083
|
body,
|
|
10488
11084
|
credentials: options.credentials,
|
|
10489
|
-
now:
|
|
11085
|
+
now: now3()
|
|
10490
11086
|
});
|
|
10491
11087
|
const response = await requestFetch(url, { method: "PUT", headers: signed.headers, body });
|
|
10492
11088
|
if (!response.ok)
|
|
@@ -10507,7 +11103,7 @@ function createTodosS3ArtifactStore(options) {
|
|
|
10507
11103
|
service: "s3",
|
|
10508
11104
|
headers: {},
|
|
10509
11105
|
credentials: options.credentials,
|
|
10510
|
-
now:
|
|
11106
|
+
now: now3()
|
|
10511
11107
|
});
|
|
10512
11108
|
const response = await requestFetch(url, { method: "GET", headers: signed.headers });
|
|
10513
11109
|
if (!response.ok)
|
|
@@ -10523,7 +11119,7 @@ function createTodosS3ArtifactStore(options) {
|
|
|
10523
11119
|
service: "s3",
|
|
10524
11120
|
headers: {},
|
|
10525
11121
|
credentials: options.credentials,
|
|
10526
|
-
now:
|
|
11122
|
+
now: now3()
|
|
10527
11123
|
});
|
|
10528
11124
|
const response = await requestFetch(url, { method: "DELETE", headers: signed.headers });
|
|
10529
11125
|
if (!response.ok && response.status !== 404)
|
|
@@ -10636,10 +11232,10 @@ function sha256Hex(value) {
|
|
|
10636
11232
|
return createHash3("sha256").update(value).digest("hex");
|
|
10637
11233
|
}
|
|
10638
11234
|
function hmac(key, value) {
|
|
10639
|
-
return
|
|
11235
|
+
return createHmac2("sha256", key).update(value).digest();
|
|
10640
11236
|
}
|
|
10641
11237
|
function hmacHex(key, value) {
|
|
10642
|
-
return
|
|
11238
|
+
return createHmac2("sha256", key).update(value).digest("hex");
|
|
10643
11239
|
}
|
|
10644
11240
|
function getSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
10645
11241
|
const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp);
|
|
@@ -10651,7 +11247,7 @@ function getSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
|
10651
11247
|
init_database();
|
|
10652
11248
|
async function uploadRunArtifactsToS3(options) {
|
|
10653
11249
|
const db = options.db ?? getDatabase();
|
|
10654
|
-
const
|
|
11250
|
+
const now3 = options.now ?? (() => new Date);
|
|
10655
11251
|
const result = emptyResult();
|
|
10656
11252
|
for (const artifact of listRunArtifacts(db, options.filter)) {
|
|
10657
11253
|
try {
|
|
@@ -10690,7 +11286,7 @@ async function uploadRunArtifactsToS3(options) {
|
|
|
10690
11286
|
url: ref.url,
|
|
10691
11287
|
sha256: content.sha256,
|
|
10692
11288
|
size_bytes: content.size_bytes,
|
|
10693
|
-
uploaded_at:
|
|
11289
|
+
uploaded_at: now3().toISOString()
|
|
10694
11290
|
};
|
|
10695
11291
|
updateArtifactMetadata(db, artifact.id, {
|
|
10696
11292
|
...metadata,
|
|
@@ -10765,7 +11361,7 @@ function planRunArtifactsS3Sync(options) {
|
|
|
10765
11361
|
}
|
|
10766
11362
|
async function downloadRunArtifactsFromS3(options) {
|
|
10767
11363
|
const db = options.db ?? getDatabase();
|
|
10768
|
-
const
|
|
11364
|
+
const now3 = options.now ?? (() => new Date);
|
|
10769
11365
|
const result = emptyResult();
|
|
10770
11366
|
for (const artifact of listRunArtifacts(db, options.filter)) {
|
|
10771
11367
|
try {
|
|
@@ -10801,7 +11397,7 @@ async function downloadRunArtifactsFromS3(options) {
|
|
|
10801
11397
|
...metadata,
|
|
10802
11398
|
remote_artifact_store: {
|
|
10803
11399
|
...remote,
|
|
10804
|
-
downloaded_at:
|
|
11400
|
+
downloaded_at: now3().toISOString()
|
|
10805
11401
|
}
|
|
10806
11402
|
});
|
|
10807
11403
|
result.downloaded += 1;
|