@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/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 +1887 -1124
- package/dist/contracts.d.ts +2 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1408 -95
- 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 +1808 -374
- 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 +1053 -256
- package/dist/mcp/tools/dispatch.d.ts.map +1 -1
- package/dist/registry.js +1400 -95
- package/dist/release-provenance.json +7 -0
- package/dist/server/index.js +1049 -252
- package/dist/storage.js +845 -146
- package/package.json +2 -2
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,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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
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
|
-
|
|
5980
|
-
|
|
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
|
-
|
|
6022
|
-
|
|
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
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
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
|
|
7356
|
-
import { basename, dirname as dirname4, join as
|
|
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
|
|
7369
|
-
return
|
|
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
|
|
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 (!
|
|
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 =
|
|
8214
|
+
const relativePath = join5("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
7435
8215
|
const destination = artifactStorePath(relativePath);
|
|
7436
|
-
if (!
|
|
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 (!
|
|
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
|
|
7578
|
-
return
|
|
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 (!
|
|
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 =
|
|
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 || !
|
|
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
|
|
7621
|
-
const ageMs =
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
11338
|
+
return createHmac2("sha256", key).update(value).digest();
|
|
10640
11339
|
}
|
|
10641
11340
|
function hmacHex(key, value) {
|
|
10642
|
-
return
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
11503
|
+
downloaded_at: now3().toISOString()
|
|
10805
11504
|
}
|
|
10806
11505
|
});
|
|
10807
11506
|
result.downloaded += 1;
|