@hasna/todos 0.15.46 → 0.15.49
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/dashboard/dist/assets/index-BNQ4gJua.js +342 -0
- package/dashboard/dist/assets/index-DjzvHUWt.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/components/Dashboard.d.ts +1 -1
- package/dist/cli/components/Dashboard.d.ts.map +1 -1
- package/dist/cli/components/Header.d.ts +1 -1
- package/dist/cli/components/Header.d.ts.map +1 -1
- package/dist/cli/components/ProjectList.d.ts +1 -1
- package/dist/cli/components/ProjectList.d.ts.map +1 -1
- package/dist/cli/components/SearchView.d.ts +1 -1
- package/dist/cli/components/SearchView.d.ts.map +1 -1
- package/dist/cli/components/TaskDetail.d.ts +1 -1
- package/dist/cli/components/TaskDetail.d.ts.map +1 -1
- package/dist/cli/components/TaskForm.d.ts +1 -1
- package/dist/cli/components/TaskForm.d.ts.map +1 -1
- package/dist/cli/components/TaskList.d.ts +1 -1
- package/dist/cli/components/TaskList.d.ts.map +1 -1
- package/dist/cli/index.js +671 -155
- package/dist/contracts.js +396 -82
- package/dist/db/task-crud.d.ts +6 -0
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/db/webhooks.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +561 -143
- package/dist/lib/dedupe-projection.d.ts +48 -0
- package/dist/lib/dedupe-projection.d.ts.map +1 -0
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/task-dedupe.d.ts +5 -9
- package/dist/lib/task-dedupe.d.ts.map +1 -1
- package/dist/mcp/index.js +621 -154
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +6 -4
- package/dist/project-registration.js +502 -141
- package/dist/registry.js +396 -82
- package/dist/release-provenance.json +5 -5
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1278 -342
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-errors.d.ts +15 -0
- package/dist/storage/postgres-errors.d.ts.map +1 -0
- package/dist/storage/postgres-sync.d.ts +7 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow-outbox.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage.js +510 -137
- package/dist/task-manifest.js +118 -47
- package/dist/task-subtree-transfer.js +326 -44
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +6 -4
- package/dashboard/dist/assets/index-DJm6m6Yy.css +0 -1
- package/dashboard/dist/assets/index-DVotjwab.js +0 -346
package/dist/registry.js
CHANGED
|
@@ -5531,13 +5531,15 @@ function upsertSecretSafetyConfig(input) {
|
|
|
5531
5531
|
saveConfig({ ...config, secret_safety: next });
|
|
5532
5532
|
return next;
|
|
5533
5533
|
}
|
|
5534
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
5534
|
+
var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
5535
5535
|
var init_redaction = __esm(() => {
|
|
5536
5536
|
init_config2();
|
|
5537
|
+
XAI_PREFIX = ["x", "ai", "-"].join("");
|
|
5537
5538
|
DEFAULT_SECRET_PATTERNS = [
|
|
5538
5539
|
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
5539
5540
|
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
5540
5541
|
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
5542
|
+
{ name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
5541
5543
|
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
|
|
5542
5544
|
{ name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
5543
5545
|
{ name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
@@ -6194,8 +6196,9 @@ var init_event_hooks = __esm(() => {
|
|
|
6194
6196
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
6195
6197
|
});
|
|
6196
6198
|
|
|
6197
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
6199
|
+
// node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
|
|
6198
6200
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6201
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
6199
6202
|
import { existsSync as existsSync5 } from "fs";
|
|
6200
6203
|
import { homedir as homedir2 } from "os";
|
|
6201
6204
|
import { join as join4 } from "path";
|
|
@@ -6304,11 +6307,13 @@ function getEventsDataDir(override) {
|
|
|
6304
6307
|
|
|
6305
6308
|
class JsonEventsStore {
|
|
6306
6309
|
dataDir;
|
|
6310
|
+
runtime;
|
|
6307
6311
|
channelsPath;
|
|
6308
6312
|
eventsPath;
|
|
6309
6313
|
deliveriesPath;
|
|
6310
6314
|
constructor(dataDir = getEventsDataDir()) {
|
|
6311
6315
|
this.dataDir = dataDir;
|
|
6316
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
6312
6317
|
this.channelsPath = join4(dataDir, "channels.json");
|
|
6313
6318
|
this.eventsPath = join4(dataDir, "events.json");
|
|
6314
6319
|
this.deliveriesPath = join4(dataDir, "deliveries.json");
|
|
@@ -6356,13 +6361,58 @@ class JsonEventsStore {
|
|
|
6356
6361
|
await this.writeJson(this.eventsPath, events);
|
|
6357
6362
|
return event;
|
|
6358
6363
|
}
|
|
6359
|
-
async
|
|
6364
|
+
async appendEventOnce(event, options = {}) {
|
|
6360
6365
|
await this.init();
|
|
6361
|
-
|
|
6366
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6367
|
+
const dedupe = options.dedupe !== false;
|
|
6368
|
+
if (dedupe) {
|
|
6369
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
6370
|
+
if (existing) {
|
|
6371
|
+
return {
|
|
6372
|
+
event: existing,
|
|
6373
|
+
stored: false,
|
|
6374
|
+
deduped: true,
|
|
6375
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
6376
|
+
};
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
events.push(event);
|
|
6380
|
+
await this.writeJson(this.eventsPath, events);
|
|
6381
|
+
return {
|
|
6382
|
+
event,
|
|
6383
|
+
stored: true,
|
|
6384
|
+
deduped: false,
|
|
6385
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
6386
|
+
};
|
|
6387
|
+
}
|
|
6388
|
+
async listEvents(options = {}) {
|
|
6389
|
+
await this.init();
|
|
6390
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6391
|
+
return queryEvents(events, options);
|
|
6392
|
+
}
|
|
6393
|
+
async listEventsPage(options = {}) {
|
|
6394
|
+
await this.init();
|
|
6395
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
6396
|
+
const queried = queryEvents(events, {
|
|
6397
|
+
eventId: options.eventId,
|
|
6398
|
+
source: options.source,
|
|
6399
|
+
type: options.type
|
|
6400
|
+
});
|
|
6401
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6402
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
6403
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
6404
|
+
const nextOffset = offset + pageEvents.length;
|
|
6405
|
+
const hasMore = nextOffset < queried.length;
|
|
6406
|
+
return {
|
|
6407
|
+
events: pageEvents,
|
|
6408
|
+
cursor: options.cursor,
|
|
6409
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
6410
|
+
hasMore
|
|
6411
|
+
};
|
|
6362
6412
|
}
|
|
6363
6413
|
async findEventByIdentity(identity) {
|
|
6364
6414
|
const events = await this.listEvents();
|
|
6365
|
-
return events
|
|
6415
|
+
return findEventByIdentity(events, identity);
|
|
6366
6416
|
}
|
|
6367
6417
|
async appendDelivery(result) {
|
|
6368
6418
|
await this.init();
|
|
@@ -6413,6 +6463,83 @@ class JsonEventsStore {
|
|
|
6413
6463
|
});
|
|
6414
6464
|
}
|
|
6415
6465
|
}
|
|
6466
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
6467
|
+
return {
|
|
6468
|
+
mode: "local-files",
|
|
6469
|
+
name: "json-events-store",
|
|
6470
|
+
remote: false,
|
|
6471
|
+
localFiles: true,
|
|
6472
|
+
localSqlite: false,
|
|
6473
|
+
postgres: false,
|
|
6474
|
+
s3: false,
|
|
6475
|
+
aws: false,
|
|
6476
|
+
durable: true,
|
|
6477
|
+
idempotency: "best-effort-local",
|
|
6478
|
+
replayCursors: true,
|
|
6479
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
6480
|
+
};
|
|
6481
|
+
}
|
|
6482
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
6483
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
6484
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
6485
|
+
const payload = {
|
|
6486
|
+
offset,
|
|
6487
|
+
eventId: options.eventId,
|
|
6488
|
+
source: options.source,
|
|
6489
|
+
type: options.type
|
|
6490
|
+
};
|
|
6491
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
6492
|
+
}
|
|
6493
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
6494
|
+
if (!cursor)
|
|
6495
|
+
return 0;
|
|
6496
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
6497
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6498
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
6499
|
+
let payload;
|
|
6500
|
+
try {
|
|
6501
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
6502
|
+
} catch {
|
|
6503
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6504
|
+
}
|
|
6505
|
+
const offset = payload.offset;
|
|
6506
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
6507
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
6508
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
6509
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
6510
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
6511
|
+
return offset;
|
|
6512
|
+
}
|
|
6513
|
+
function normalizeEventPageLimit(limit) {
|
|
6514
|
+
if (limit === undefined)
|
|
6515
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
6516
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
6517
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
6518
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
6519
|
+
}
|
|
6520
|
+
function queryEvents(events, options) {
|
|
6521
|
+
let rows = events;
|
|
6522
|
+
if (options.eventId)
|
|
6523
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
6524
|
+
if (options.source)
|
|
6525
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
6526
|
+
if (options.type)
|
|
6527
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
6528
|
+
if (options.cursor) {
|
|
6529
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6530
|
+
rows = rows.slice(offset);
|
|
6531
|
+
}
|
|
6532
|
+
if (options.limit !== undefined)
|
|
6533
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
6534
|
+
return rows;
|
|
6535
|
+
}
|
|
6536
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
6537
|
+
if (cursorValue !== optionValue)
|
|
6538
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
6539
|
+
}
|
|
6540
|
+
function findEventByIdentity(events, identity) {
|
|
6541
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
6542
|
+
}
|
|
6416
6543
|
function buildSignatureBase(timestamp2, body) {
|
|
6417
6544
|
return `${timestamp2}.${body}`;
|
|
6418
6545
|
}
|
|
@@ -6426,21 +6553,27 @@ function now2() {
|
|
|
6426
6553
|
function truncate(value, max = 4096) {
|
|
6427
6554
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
6428
6555
|
}
|
|
6429
|
-
function buildWebhookRequest(event, channel) {
|
|
6556
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
6430
6557
|
if (!channel.webhook)
|
|
6431
6558
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
6559
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
6560
|
+
if (/^x-hasna-/i.test(name)) {
|
|
6561
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6432
6564
|
const body = JSON.stringify(event);
|
|
6433
|
-
const timestamp2 =
|
|
6565
|
+
const timestamp2 = options.timestamp ?? new Date().toISOString();
|
|
6434
6566
|
const headers = {
|
|
6435
6567
|
"Content-Type": "application/json",
|
|
6436
6568
|
"User-Agent": "@hasna/events",
|
|
6437
6569
|
"X-Hasna-Event-Id": event.id,
|
|
6438
6570
|
"X-Hasna-Event-Type": event.type,
|
|
6439
|
-
|
|
6440
|
-
|
|
6571
|
+
...channel.webhook.headers,
|
|
6572
|
+
"X-Hasna-Timestamp": timestamp2
|
|
6441
6573
|
};
|
|
6442
|
-
|
|
6443
|
-
|
|
6574
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
6575
|
+
if (secret) {
|
|
6576
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
|
|
6444
6577
|
}
|
|
6445
6578
|
return { body, headers };
|
|
6446
6579
|
}
|
|
@@ -6448,7 +6581,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6448
6581
|
if (!channel.webhook)
|
|
6449
6582
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
6450
6583
|
const startedAt = now2();
|
|
6451
|
-
|
|
6584
|
+
let secret = channel.webhook.secret;
|
|
6585
|
+
if (channel.webhook.secretRef) {
|
|
6586
|
+
if (!options.secretResolver) {
|
|
6587
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
6588
|
+
}
|
|
6589
|
+
try {
|
|
6590
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
6591
|
+
} catch {
|
|
6592
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
6593
|
+
}
|
|
6594
|
+
if (!secret)
|
|
6595
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
6596
|
+
}
|
|
6597
|
+
const timestamp2 = (options.now?.() ?? new Date).toISOString();
|
|
6598
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
|
|
6452
6599
|
const controller = new AbortController;
|
|
6453
6600
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
6454
6601
|
try {
|
|
@@ -6480,6 +6627,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6480
6627
|
clearTimeout(timeout);
|
|
6481
6628
|
}
|
|
6482
6629
|
}
|
|
6630
|
+
function failedAttempt(startedAt, error) {
|
|
6631
|
+
return {
|
|
6632
|
+
attempt: 1,
|
|
6633
|
+
status: "failed",
|
|
6634
|
+
startedAt,
|
|
6635
|
+
completedAt: now2(),
|
|
6636
|
+
error
|
|
6637
|
+
};
|
|
6638
|
+
}
|
|
6483
6639
|
async function dispatchCommand(event, channel) {
|
|
6484
6640
|
if (!channel.command)
|
|
6485
6641
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -6568,6 +6724,76 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
6568
6724
|
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
6569
6725
|
};
|
|
6570
6726
|
}
|
|
6727
|
+
|
|
6728
|
+
class EventTypeCatalog {
|
|
6729
|
+
definitions = new Map;
|
|
6730
|
+
register(definition) {
|
|
6731
|
+
this.definitions.set(definition.type, definition);
|
|
6732
|
+
return this;
|
|
6733
|
+
}
|
|
6734
|
+
unregister(type) {
|
|
6735
|
+
return this.definitions.delete(type);
|
|
6736
|
+
}
|
|
6737
|
+
has(type) {
|
|
6738
|
+
return this.definitions.has(type);
|
|
6739
|
+
}
|
|
6740
|
+
get(type) {
|
|
6741
|
+
return this.definitions.get(type);
|
|
6742
|
+
}
|
|
6743
|
+
list() {
|
|
6744
|
+
return [...this.definitions.values()];
|
|
6745
|
+
}
|
|
6746
|
+
validateEvent(event) {
|
|
6747
|
+
const definition = this.definitions.get(event.type);
|
|
6748
|
+
if (!definition)
|
|
6749
|
+
return { ok: true };
|
|
6750
|
+
return definition.validate(event.data, event);
|
|
6751
|
+
}
|
|
6752
|
+
assertEventValid(event) {
|
|
6753
|
+
const result = this.validateEvent(event);
|
|
6754
|
+
if (!result.ok) {
|
|
6755
|
+
throw new EventValidationError(event.type, result.issues);
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
6760
|
+
if (paths.length === 0)
|
|
6761
|
+
return event;
|
|
6762
|
+
const copy = structuredClone(event);
|
|
6763
|
+
for (const path of paths) {
|
|
6764
|
+
setPath(copy, path, replacement);
|
|
6765
|
+
}
|
|
6766
|
+
return copy;
|
|
6767
|
+
}
|
|
6768
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
6769
|
+
return redactValue2(event, replacement);
|
|
6770
|
+
}
|
|
6771
|
+
function shouldRedactKey(key) {
|
|
6772
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
6773
|
+
}
|
|
6774
|
+
function redactValue2(value, replacement) {
|
|
6775
|
+
if (Array.isArray(value))
|
|
6776
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
6777
|
+
if (!value || typeof value !== "object")
|
|
6778
|
+
return value;
|
|
6779
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
6780
|
+
key,
|
|
6781
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
6782
|
+
]));
|
|
6783
|
+
}
|
|
6784
|
+
function setPath(input, path, replacement) {
|
|
6785
|
+
const parts = path.split(".");
|
|
6786
|
+
let cursor = input;
|
|
6787
|
+
for (const part of parts.slice(0, -1)) {
|
|
6788
|
+
const next = cursor[part];
|
|
6789
|
+
if (!next || typeof next !== "object")
|
|
6790
|
+
return;
|
|
6791
|
+
cursor = next;
|
|
6792
|
+
}
|
|
6793
|
+
const last = parts.at(-1);
|
|
6794
|
+
if (last && last in cursor)
|
|
6795
|
+
cursor[last] = replacement;
|
|
6796
|
+
}
|
|
6571
6797
|
function createEvent(input) {
|
|
6572
6798
|
return {
|
|
6573
6799
|
id: input.id ?? randomUUID22(),
|
|
@@ -6588,10 +6814,18 @@ class EventsClient {
|
|
|
6588
6814
|
store;
|
|
6589
6815
|
redactors;
|
|
6590
6816
|
transportOptions;
|
|
6817
|
+
catalog;
|
|
6818
|
+
validateCatalogTypes;
|
|
6591
6819
|
constructor(options = {}) {
|
|
6592
6820
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
6593
6821
|
this.redactors = options.redactors ?? [];
|
|
6594
|
-
this.transportOptions = {
|
|
6822
|
+
this.transportOptions = {
|
|
6823
|
+
fetchImpl: options.fetchImpl,
|
|
6824
|
+
secretResolver: options.secretResolver,
|
|
6825
|
+
now: options.now
|
|
6826
|
+
};
|
|
6827
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
6828
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
6595
6829
|
}
|
|
6596
6830
|
async addChannel(input) {
|
|
6597
6831
|
const timestamp2 = new Date().toISOString();
|
|
@@ -6609,18 +6843,40 @@ class EventsClient {
|
|
|
6609
6843
|
}
|
|
6610
6844
|
async emit(input, options = {}) {
|
|
6611
6845
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
6612
|
-
if (options.
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
6620
|
-
return { event, deliveries, deduped: false };
|
|
6621
|
-
}
|
|
6622
|
-
async listEvents() {
|
|
6623
|
-
|
|
6846
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
6847
|
+
this.catalog.assertEventValid(event);
|
|
6848
|
+
}
|
|
6849
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
6850
|
+
if (append.deduped) {
|
|
6851
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
6852
|
+
}
|
|
6853
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
6854
|
+
return { event: append.event, deliveries, deduped: false };
|
|
6855
|
+
}
|
|
6856
|
+
async listEvents(options = {}) {
|
|
6857
|
+
if (Object.keys(options).length === 0)
|
|
6858
|
+
return this.store.listEvents();
|
|
6859
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
6860
|
+
}
|
|
6861
|
+
async listEventsPage(options = {}) {
|
|
6862
|
+
if (this.store.listEventsPage)
|
|
6863
|
+
return this.store.listEventsPage(options);
|
|
6864
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
6865
|
+
eventId: options.eventId,
|
|
6866
|
+
source: options.source,
|
|
6867
|
+
type: options.type
|
|
6868
|
+
});
|
|
6869
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
6870
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
6871
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
6872
|
+
const nextOffset = offset + pageEvents.length;
|
|
6873
|
+
const hasMore = nextOffset < events.length;
|
|
6874
|
+
return {
|
|
6875
|
+
events: pageEvents,
|
|
6876
|
+
cursor: options.cursor,
|
|
6877
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
6878
|
+
hasMore
|
|
6879
|
+
};
|
|
6624
6880
|
}
|
|
6625
6881
|
async listDeliveries() {
|
|
6626
6882
|
return this.store.listDeliveries();
|
|
@@ -6688,22 +6944,37 @@ class EventsClient {
|
|
|
6688
6944
|
return result;
|
|
6689
6945
|
}
|
|
6690
6946
|
async replay(options = {}) {
|
|
6691
|
-
const
|
|
6692
|
-
if (options.eventId && event.id !== options.eventId)
|
|
6693
|
-
return false;
|
|
6694
|
-
if (options.source && event.source !== options.source)
|
|
6695
|
-
return false;
|
|
6696
|
-
if (options.type && event.type !== options.type)
|
|
6697
|
-
return false;
|
|
6698
|
-
return true;
|
|
6699
|
-
});
|
|
6947
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
6700
6948
|
if (options.dryRun)
|
|
6701
|
-
return { events, deliveries: [] };
|
|
6949
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
6702
6950
|
const deliveries = [];
|
|
6703
|
-
for (const event of events) {
|
|
6951
|
+
for (const event of page.events) {
|
|
6704
6952
|
deliveries.push(...await this.deliver(event));
|
|
6705
6953
|
}
|
|
6706
|
-
return { events, deliveries };
|
|
6954
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
6955
|
+
}
|
|
6956
|
+
async appendEvent(event, options) {
|
|
6957
|
+
if (this.store.appendEventOnce) {
|
|
6958
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
6959
|
+
}
|
|
6960
|
+
if (options.dedupe) {
|
|
6961
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
6962
|
+
if (existing) {
|
|
6963
|
+
return {
|
|
6964
|
+
event: existing,
|
|
6965
|
+
stored: false,
|
|
6966
|
+
deduped: true,
|
|
6967
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
6968
|
+
};
|
|
6969
|
+
}
|
|
6970
|
+
}
|
|
6971
|
+
const stored = await this.store.appendEvent(event);
|
|
6972
|
+
return {
|
|
6973
|
+
event: stored,
|
|
6974
|
+
stored: true,
|
|
6975
|
+
deduped: false,
|
|
6976
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
6977
|
+
};
|
|
6707
6978
|
}
|
|
6708
6979
|
async applyRedaction(event, channel) {
|
|
6709
6980
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -6730,43 +7001,19 @@ class EventsClient {
|
|
|
6730
7001
|
return createDeliveryResult(event, channel, attempts);
|
|
6731
7002
|
}
|
|
6732
7003
|
}
|
|
6733
|
-
function
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
6747
|
-
}
|
|
6748
|
-
function redactValue2(value, replacement) {
|
|
6749
|
-
if (Array.isArray(value))
|
|
6750
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
6751
|
-
if (!value || typeof value !== "object")
|
|
6752
|
-
return value;
|
|
6753
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
6754
|
-
key,
|
|
6755
|
-
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
6756
|
-
]));
|
|
6757
|
-
}
|
|
6758
|
-
function setPath(input, path, replacement) {
|
|
6759
|
-
const parts = path.split(".");
|
|
6760
|
-
let cursor = input;
|
|
6761
|
-
for (const part of parts.slice(0, -1)) {
|
|
6762
|
-
const next = cursor[part];
|
|
6763
|
-
if (!next || typeof next !== "object")
|
|
6764
|
-
return;
|
|
6765
|
-
cursor = next;
|
|
6766
|
-
}
|
|
6767
|
-
const last = parts.at(-1);
|
|
6768
|
-
if (last && last in cursor)
|
|
6769
|
-
cursor[last] = replacement;
|
|
7004
|
+
function queryClientEvents(events, options) {
|
|
7005
|
+
let rows = events;
|
|
7006
|
+
if (options.eventId)
|
|
7007
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
7008
|
+
if (options.source)
|
|
7009
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
7010
|
+
if (options.type)
|
|
7011
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
7012
|
+
if (options.cursor)
|
|
7013
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
7014
|
+
if (options.limit !== undefined)
|
|
7015
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
7016
|
+
return rows;
|
|
6770
7017
|
}
|
|
6771
7018
|
function normalizeTime(value) {
|
|
6772
7019
|
if (!value)
|
|
@@ -6780,9 +7027,22 @@ function normalizeRetryPolicy(policy) {
|
|
|
6780
7027
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
6781
7028
|
};
|
|
6782
7029
|
}
|
|
6783
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
7030
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
|
|
6784
7031
|
var init_dist = __esm(() => {
|
|
6785
7032
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
7033
|
+
EventValidationError = class EventValidationError extends Error {
|
|
7034
|
+
eventType;
|
|
7035
|
+
issues;
|
|
7036
|
+
constructor(eventType, issues) {
|
|
7037
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
7038
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
7039
|
+
this.name = "EventValidationError";
|
|
7040
|
+
this.eventType = eventType;
|
|
7041
|
+
this.issues = issues;
|
|
7042
|
+
}
|
|
7043
|
+
};
|
|
7044
|
+
defaultEventTypeCatalog = new EventTypeCatalog;
|
|
7045
|
+
APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
6786
7046
|
});
|
|
6787
7047
|
|
|
6788
7048
|
// src/db/task-lists.ts
|
|
@@ -7886,8 +8146,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
7886
8146
|
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
7887
8147
|
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7888
8148
|
}
|
|
7889
|
-
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
8149
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
|
|
7890
8150
|
const respText = await resp.text().catch(() => "");
|
|
8151
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
8152
|
+
logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
|
|
8153
|
+
return;
|
|
8154
|
+
}
|
|
7891
8155
|
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
7892
8156
|
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
7893
8157
|
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
@@ -9671,6 +9935,12 @@ function listTasks(filter = {}, db) {
|
|
|
9671
9935
|
const d = db || getDatabase();
|
|
9672
9936
|
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
9673
9937
|
clearExpiredLocks2(d);
|
|
9938
|
+
if (filter.limit !== undefined) {
|
|
9939
|
+
const limit = filter.limit;
|
|
9940
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
|
|
9941
|
+
throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
|
|
9942
|
+
}
|
|
9943
|
+
}
|
|
9674
9944
|
const conditions = [];
|
|
9675
9945
|
const params = [];
|
|
9676
9946
|
if (filter.project_id) {
|
|
@@ -10204,6 +10474,7 @@ function deleteTask(id, db) {
|
|
|
10204
10474
|
}
|
|
10205
10475
|
return result.changes > 0;
|
|
10206
10476
|
}
|
|
10477
|
+
var MAX_TASK_LIST_LIMIT = 1e5;
|
|
10207
10478
|
var init_task_crud = __esm(() => {
|
|
10208
10479
|
init_types();
|
|
10209
10480
|
init_database();
|
|
@@ -12809,7 +13080,7 @@ var init_tasks = __esm(() => {
|
|
|
12809
13080
|
// package.json
|
|
12810
13081
|
var package_default = {
|
|
12811
13082
|
name: "@hasna/todos",
|
|
12812
|
-
version: "0.15.
|
|
13083
|
+
version: "0.15.49",
|
|
12813
13084
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12814
13085
|
type: "module",
|
|
12815
13086
|
main: "dist/index.js",
|
|
@@ -12873,6 +13144,7 @@ var package_default = {
|
|
|
12873
13144
|
],
|
|
12874
13145
|
scripts: {
|
|
12875
13146
|
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
13147
|
+
"build:js": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
12876
13148
|
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
12877
13149
|
migrate: "bun run src/server/index.ts migrate",
|
|
12878
13150
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
@@ -12927,18 +13199,19 @@ var package_default = {
|
|
|
12927
13199
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
12928
13200
|
license: "Apache-2.0",
|
|
12929
13201
|
dependencies: {
|
|
12930
|
-
"@hasna/contracts": "0.
|
|
13202
|
+
"@hasna/contracts": "0.14.0",
|
|
12931
13203
|
"@hasna/events": "^0.1.11",
|
|
12932
13204
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
12933
13205
|
chalk: "^5.4.1",
|
|
12934
13206
|
commander: "^13.1.0",
|
|
12935
13207
|
ink: "^5.2.0",
|
|
12936
13208
|
react: "^18.3.1",
|
|
12937
|
-
zod: "
|
|
13209
|
+
zod: "3.25.76"
|
|
12938
13210
|
},
|
|
12939
13211
|
overrides: {
|
|
12940
13212
|
ajv: "8.20.0",
|
|
12941
|
-
"fast-uri": "3.1.2"
|
|
13213
|
+
"fast-uri": "3.1.2",
|
|
13214
|
+
zod: "3.25.76"
|
|
12942
13215
|
},
|
|
12943
13216
|
devDependencies: {
|
|
12944
13217
|
"@types/bun": "^1.2.4",
|
|
@@ -21753,6 +22026,47 @@ function autoDetectFileRelationships(taskId, db) {
|
|
|
21753
22026
|
|
|
21754
22027
|
// src/lib/task-dedupe.ts
|
|
21755
22028
|
init_tasks();
|
|
22029
|
+
|
|
22030
|
+
// src/lib/dedupe-projection.ts
|
|
22031
|
+
init_redaction();
|
|
22032
|
+
var DEDUPE_SOURCE_KEY_ALLOWLIST = [
|
|
22033
|
+
"github_url",
|
|
22034
|
+
"github_issue_url",
|
|
22035
|
+
"github_pr_url",
|
|
22036
|
+
"source_url",
|
|
22037
|
+
"url",
|
|
22038
|
+
"external_url",
|
|
22039
|
+
"issue_url",
|
|
22040
|
+
"github_owner",
|
|
22041
|
+
"github_repo",
|
|
22042
|
+
"github_number"
|
|
22043
|
+
];
|
|
22044
|
+
function projectTasksForDedupe(tasks) {
|
|
22045
|
+
return tasks.map((task2) => {
|
|
22046
|
+
const metadata = {};
|
|
22047
|
+
for (const key of DEDUPE_SOURCE_KEY_ALLOWLIST) {
|
|
22048
|
+
if (key in task2.metadata) {
|
|
22049
|
+
metadata[key] = redactValue(task2.metadata[key]);
|
|
22050
|
+
}
|
|
22051
|
+
}
|
|
22052
|
+
return {
|
|
22053
|
+
id: task2.id,
|
|
22054
|
+
short_id: task2.short_id,
|
|
22055
|
+
title: redactEvidenceText(task2.title),
|
|
22056
|
+
description: task2.description === null || task2.description === undefined ? null : redactEvidenceText(task2.description),
|
|
22057
|
+
status: task2.status,
|
|
22058
|
+
created_at: task2.created_at,
|
|
22059
|
+
updated_at: task2.updated_at,
|
|
22060
|
+
project_id: task2.project_id,
|
|
22061
|
+
task_list_id: task2.task_list_id,
|
|
22062
|
+
assigned_to: task2.assigned_to,
|
|
22063
|
+
priority: task2.priority,
|
|
22064
|
+
metadata
|
|
22065
|
+
};
|
|
22066
|
+
});
|
|
22067
|
+
}
|
|
22068
|
+
|
|
22069
|
+
// src/lib/task-dedupe.ts
|
|
21756
22070
|
var DEFAULT_THRESHOLD = 0.74;
|
|
21757
22071
|
var STOP_WORDS = new Set([
|
|
21758
22072
|
"a",
|
|
@@ -21945,10 +22259,10 @@ function olderFirst(left, right) {
|
|
|
21945
22259
|
function findDuplicateTasks(options = {}, db) {
|
|
21946
22260
|
const d = db || getDatabase();
|
|
21947
22261
|
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
21948
|
-
const fingerprints = listTasks({
|
|
22262
|
+
const fingerprints = projectTasksForDedupe(listTasks({
|
|
21949
22263
|
include_archived: Boolean(options.include_archived),
|
|
21950
22264
|
limit: options.limit ?? 1000
|
|
21951
|
-
}, d).map(fingerprint);
|
|
22265
|
+
}, d)).map(fingerprint);
|
|
21952
22266
|
const candidates = [];
|
|
21953
22267
|
for (let i = 0;i < fingerprints.length; i++) {
|
|
21954
22268
|
for (let j = i + 1;j < fingerprints.length; j++) {
|