@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/mcp/index.js
CHANGED
|
@@ -9582,13 +9582,15 @@ function upsertSecretSafetyConfig(input) {
|
|
|
9582
9582
|
saveConfig({ ...config, secret_safety: next });
|
|
9583
9583
|
return next;
|
|
9584
9584
|
}
|
|
9585
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
9585
|
+
var XAI_PREFIX, DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
9586
9586
|
var init_redaction = __esm(() => {
|
|
9587
9587
|
init_config2();
|
|
9588
|
+
XAI_PREFIX = ["x", "ai", "-"].join("");
|
|
9588
9589
|
DEFAULT_SECRET_PATTERNS = [
|
|
9589
9590
|
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
9590
9591
|
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
9591
9592
|
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
9593
|
+
{ name: `${XAI_PREFIX}token`, regex: /\bxai[-][A-Za-z0-9]{20,80}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
9592
9594
|
{ name: "npm-token", regex: /\bnpm_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_NPM_TOKEN]" },
|
|
9593
9595
|
{ name: "github-fine-grained-token", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
9594
9596
|
{ name: "github-token", regex: /\bgh[opsu]_[A-Za-z0-9]{20,}\b/g, replacement: "[REDACTED_GITHUB_TOKEN]" },
|
|
@@ -11084,8 +11086,9 @@ var init_event_hooks = __esm(() => {
|
|
|
11084
11086
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
11085
11087
|
});
|
|
11086
11088
|
|
|
11087
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
11089
|
+
// node_modules/.bun/@hasna+events@0.1.16/node_modules/@hasna/events/dist/index.js
|
|
11088
11090
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
11091
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
11089
11092
|
import { existsSync as existsSync5 } from "fs";
|
|
11090
11093
|
import { homedir as homedir2 } from "os";
|
|
11091
11094
|
import { join as join4 } from "path";
|
|
@@ -11194,11 +11197,13 @@ function getEventsDataDir(override) {
|
|
|
11194
11197
|
|
|
11195
11198
|
class JsonEventsStore {
|
|
11196
11199
|
dataDir;
|
|
11200
|
+
runtime;
|
|
11197
11201
|
channelsPath;
|
|
11198
11202
|
eventsPath;
|
|
11199
11203
|
deliveriesPath;
|
|
11200
11204
|
constructor(dataDir = getEventsDataDir()) {
|
|
11201
11205
|
this.dataDir = dataDir;
|
|
11206
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
11202
11207
|
this.channelsPath = join4(dataDir, "channels.json");
|
|
11203
11208
|
this.eventsPath = join4(dataDir, "events.json");
|
|
11204
11209
|
this.deliveriesPath = join4(dataDir, "deliveries.json");
|
|
@@ -11246,13 +11251,58 @@ class JsonEventsStore {
|
|
|
11246
11251
|
await this.writeJson(this.eventsPath, events);
|
|
11247
11252
|
return event;
|
|
11248
11253
|
}
|
|
11249
|
-
async
|
|
11254
|
+
async appendEventOnce(event, options = {}) {
|
|
11250
11255
|
await this.init();
|
|
11251
|
-
|
|
11256
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
11257
|
+
const dedupe = options.dedupe !== false;
|
|
11258
|
+
if (dedupe) {
|
|
11259
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
11260
|
+
if (existing) {
|
|
11261
|
+
return {
|
|
11262
|
+
event: existing,
|
|
11263
|
+
stored: false,
|
|
11264
|
+
deduped: true,
|
|
11265
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
11266
|
+
};
|
|
11267
|
+
}
|
|
11268
|
+
}
|
|
11269
|
+
events.push(event);
|
|
11270
|
+
await this.writeJson(this.eventsPath, events);
|
|
11271
|
+
return {
|
|
11272
|
+
event,
|
|
11273
|
+
stored: true,
|
|
11274
|
+
deduped: false,
|
|
11275
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
11276
|
+
};
|
|
11277
|
+
}
|
|
11278
|
+
async listEvents(options = {}) {
|
|
11279
|
+
await this.init();
|
|
11280
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
11281
|
+
return queryEvents(events, options);
|
|
11282
|
+
}
|
|
11283
|
+
async listEventsPage(options = {}) {
|
|
11284
|
+
await this.init();
|
|
11285
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
11286
|
+
const queried = queryEvents(events, {
|
|
11287
|
+
eventId: options.eventId,
|
|
11288
|
+
source: options.source,
|
|
11289
|
+
type: options.type
|
|
11290
|
+
});
|
|
11291
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
11292
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
11293
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
11294
|
+
const nextOffset = offset + pageEvents.length;
|
|
11295
|
+
const hasMore = nextOffset < queried.length;
|
|
11296
|
+
return {
|
|
11297
|
+
events: pageEvents,
|
|
11298
|
+
cursor: options.cursor,
|
|
11299
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
11300
|
+
hasMore
|
|
11301
|
+
};
|
|
11252
11302
|
}
|
|
11253
11303
|
async findEventByIdentity(identity) {
|
|
11254
11304
|
const events = await this.listEvents();
|
|
11255
|
-
return events
|
|
11305
|
+
return findEventByIdentity(events, identity);
|
|
11256
11306
|
}
|
|
11257
11307
|
async appendDelivery(result) {
|
|
11258
11308
|
await this.init();
|
|
@@ -11303,6 +11353,83 @@ class JsonEventsStore {
|
|
|
11303
11353
|
});
|
|
11304
11354
|
}
|
|
11305
11355
|
}
|
|
11356
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
11357
|
+
return {
|
|
11358
|
+
mode: "local-files",
|
|
11359
|
+
name: "json-events-store",
|
|
11360
|
+
remote: false,
|
|
11361
|
+
localFiles: true,
|
|
11362
|
+
localSqlite: false,
|
|
11363
|
+
postgres: false,
|
|
11364
|
+
s3: false,
|
|
11365
|
+
aws: false,
|
|
11366
|
+
durable: true,
|
|
11367
|
+
idempotency: "best-effort-local",
|
|
11368
|
+
replayCursors: true,
|
|
11369
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
11370
|
+
};
|
|
11371
|
+
}
|
|
11372
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
11373
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
11374
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
11375
|
+
const payload = {
|
|
11376
|
+
offset,
|
|
11377
|
+
eventId: options.eventId,
|
|
11378
|
+
source: options.source,
|
|
11379
|
+
type: options.type
|
|
11380
|
+
};
|
|
11381
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
11382
|
+
}
|
|
11383
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
11384
|
+
if (!cursor)
|
|
11385
|
+
return 0;
|
|
11386
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
11387
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
11388
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
11389
|
+
let payload;
|
|
11390
|
+
try {
|
|
11391
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
11392
|
+
} catch {
|
|
11393
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
11394
|
+
}
|
|
11395
|
+
const offset = payload.offset;
|
|
11396
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
11397
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
11398
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
11399
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
11400
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
11401
|
+
return offset;
|
|
11402
|
+
}
|
|
11403
|
+
function normalizeEventPageLimit(limit) {
|
|
11404
|
+
if (limit === undefined)
|
|
11405
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
11406
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
11407
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
11408
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
11409
|
+
}
|
|
11410
|
+
function queryEvents(events, options) {
|
|
11411
|
+
let rows = events;
|
|
11412
|
+
if (options.eventId)
|
|
11413
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
11414
|
+
if (options.source)
|
|
11415
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
11416
|
+
if (options.type)
|
|
11417
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
11418
|
+
if (options.cursor) {
|
|
11419
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
11420
|
+
rows = rows.slice(offset);
|
|
11421
|
+
}
|
|
11422
|
+
if (options.limit !== undefined)
|
|
11423
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
11424
|
+
return rows;
|
|
11425
|
+
}
|
|
11426
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
11427
|
+
if (cursorValue !== optionValue)
|
|
11428
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
11429
|
+
}
|
|
11430
|
+
function findEventByIdentity(events, identity) {
|
|
11431
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
11432
|
+
}
|
|
11306
11433
|
function buildSignatureBase(timestamp2, body) {
|
|
11307
11434
|
return `${timestamp2}.${body}`;
|
|
11308
11435
|
}
|
|
@@ -11316,21 +11443,27 @@ function now2() {
|
|
|
11316
11443
|
function truncate(value, max = 4096) {
|
|
11317
11444
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
11318
11445
|
}
|
|
11319
|
-
function buildWebhookRequest(event, channel) {
|
|
11446
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
11320
11447
|
if (!channel.webhook)
|
|
11321
11448
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
11449
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
11450
|
+
if (/^x-hasna-/i.test(name)) {
|
|
11451
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
11452
|
+
}
|
|
11453
|
+
}
|
|
11322
11454
|
const body = JSON.stringify(event);
|
|
11323
|
-
const timestamp2 =
|
|
11455
|
+
const timestamp2 = options.timestamp ?? new Date().toISOString();
|
|
11324
11456
|
const headers = {
|
|
11325
11457
|
"Content-Type": "application/json",
|
|
11326
11458
|
"User-Agent": "@hasna/events",
|
|
11327
11459
|
"X-Hasna-Event-Id": event.id,
|
|
11328
11460
|
"X-Hasna-Event-Type": event.type,
|
|
11329
|
-
|
|
11330
|
-
|
|
11461
|
+
...channel.webhook.headers,
|
|
11462
|
+
"X-Hasna-Timestamp": timestamp2
|
|
11331
11463
|
};
|
|
11332
|
-
|
|
11333
|
-
|
|
11464
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
11465
|
+
if (secret) {
|
|
11466
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp2, body);
|
|
11334
11467
|
}
|
|
11335
11468
|
return { body, headers };
|
|
11336
11469
|
}
|
|
@@ -11338,7 +11471,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
11338
11471
|
if (!channel.webhook)
|
|
11339
11472
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
11340
11473
|
const startedAt = now2();
|
|
11341
|
-
|
|
11474
|
+
let secret = channel.webhook.secret;
|
|
11475
|
+
if (channel.webhook.secretRef) {
|
|
11476
|
+
if (!options.secretResolver) {
|
|
11477
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
11478
|
+
}
|
|
11479
|
+
try {
|
|
11480
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
11481
|
+
} catch {
|
|
11482
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
11483
|
+
}
|
|
11484
|
+
if (!secret)
|
|
11485
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
11486
|
+
}
|
|
11487
|
+
const timestamp2 = (options.now?.() ?? new Date).toISOString();
|
|
11488
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
|
|
11342
11489
|
const controller = new AbortController;
|
|
11343
11490
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
11344
11491
|
try {
|
|
@@ -11370,6 +11517,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
11370
11517
|
clearTimeout(timeout);
|
|
11371
11518
|
}
|
|
11372
11519
|
}
|
|
11520
|
+
function failedAttempt(startedAt, error) {
|
|
11521
|
+
return {
|
|
11522
|
+
attempt: 1,
|
|
11523
|
+
status: "failed",
|
|
11524
|
+
startedAt,
|
|
11525
|
+
completedAt: now2(),
|
|
11526
|
+
error
|
|
11527
|
+
};
|
|
11528
|
+
}
|
|
11373
11529
|
async function dispatchCommand(event, channel) {
|
|
11374
11530
|
if (!channel.command)
|
|
11375
11531
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -11458,6 +11614,76 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
11458
11614
|
completedAt: attempts.at(-1)?.completedAt ?? now2()
|
|
11459
11615
|
};
|
|
11460
11616
|
}
|
|
11617
|
+
|
|
11618
|
+
class EventTypeCatalog {
|
|
11619
|
+
definitions = new Map;
|
|
11620
|
+
register(definition) {
|
|
11621
|
+
this.definitions.set(definition.type, definition);
|
|
11622
|
+
return this;
|
|
11623
|
+
}
|
|
11624
|
+
unregister(type) {
|
|
11625
|
+
return this.definitions.delete(type);
|
|
11626
|
+
}
|
|
11627
|
+
has(type) {
|
|
11628
|
+
return this.definitions.has(type);
|
|
11629
|
+
}
|
|
11630
|
+
get(type) {
|
|
11631
|
+
return this.definitions.get(type);
|
|
11632
|
+
}
|
|
11633
|
+
list() {
|
|
11634
|
+
return [...this.definitions.values()];
|
|
11635
|
+
}
|
|
11636
|
+
validateEvent(event) {
|
|
11637
|
+
const definition = this.definitions.get(event.type);
|
|
11638
|
+
if (!definition)
|
|
11639
|
+
return { ok: true };
|
|
11640
|
+
return definition.validate(event.data, event);
|
|
11641
|
+
}
|
|
11642
|
+
assertEventValid(event) {
|
|
11643
|
+
const result = this.validateEvent(event);
|
|
11644
|
+
if (!result.ok) {
|
|
11645
|
+
throw new EventValidationError(event.type, result.issues);
|
|
11646
|
+
}
|
|
11647
|
+
}
|
|
11648
|
+
}
|
|
11649
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
11650
|
+
if (paths.length === 0)
|
|
11651
|
+
return event;
|
|
11652
|
+
const copy = structuredClone(event);
|
|
11653
|
+
for (const path of paths) {
|
|
11654
|
+
setPath(copy, path, replacement);
|
|
11655
|
+
}
|
|
11656
|
+
return copy;
|
|
11657
|
+
}
|
|
11658
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
11659
|
+
return redactValue2(event, replacement);
|
|
11660
|
+
}
|
|
11661
|
+
function shouldRedactKey(key) {
|
|
11662
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
11663
|
+
}
|
|
11664
|
+
function redactValue2(value, replacement) {
|
|
11665
|
+
if (Array.isArray(value))
|
|
11666
|
+
return value.map((item) => redactValue2(item, replacement));
|
|
11667
|
+
if (!value || typeof value !== "object")
|
|
11668
|
+
return value;
|
|
11669
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
11670
|
+
key,
|
|
11671
|
+
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
11672
|
+
]));
|
|
11673
|
+
}
|
|
11674
|
+
function setPath(input, path, replacement) {
|
|
11675
|
+
const parts = path.split(".");
|
|
11676
|
+
let cursor = input;
|
|
11677
|
+
for (const part of parts.slice(0, -1)) {
|
|
11678
|
+
const next = cursor[part];
|
|
11679
|
+
if (!next || typeof next !== "object")
|
|
11680
|
+
return;
|
|
11681
|
+
cursor = next;
|
|
11682
|
+
}
|
|
11683
|
+
const last = parts.at(-1);
|
|
11684
|
+
if (last && last in cursor)
|
|
11685
|
+
cursor[last] = replacement;
|
|
11686
|
+
}
|
|
11461
11687
|
function createEvent(input) {
|
|
11462
11688
|
return {
|
|
11463
11689
|
id: input.id ?? randomUUID22(),
|
|
@@ -11478,10 +11704,18 @@ class EventsClient {
|
|
|
11478
11704
|
store;
|
|
11479
11705
|
redactors;
|
|
11480
11706
|
transportOptions;
|
|
11707
|
+
catalog;
|
|
11708
|
+
validateCatalogTypes;
|
|
11481
11709
|
constructor(options = {}) {
|
|
11482
11710
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
11483
11711
|
this.redactors = options.redactors ?? [];
|
|
11484
|
-
this.transportOptions = {
|
|
11712
|
+
this.transportOptions = {
|
|
11713
|
+
fetchImpl: options.fetchImpl,
|
|
11714
|
+
secretResolver: options.secretResolver,
|
|
11715
|
+
now: options.now
|
|
11716
|
+
};
|
|
11717
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
11718
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
11485
11719
|
}
|
|
11486
11720
|
async addChannel(input) {
|
|
11487
11721
|
const timestamp2 = new Date().toISOString();
|
|
@@ -11499,18 +11733,40 @@ class EventsClient {
|
|
|
11499
11733
|
}
|
|
11500
11734
|
async emit(input, options = {}) {
|
|
11501
11735
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
11502
|
-
if (options.
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
11510
|
-
return { event, deliveries, deduped: false };
|
|
11511
|
-
}
|
|
11512
|
-
async listEvents() {
|
|
11513
|
-
|
|
11736
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
11737
|
+
this.catalog.assertEventValid(event);
|
|
11738
|
+
}
|
|
11739
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
11740
|
+
if (append.deduped) {
|
|
11741
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
11742
|
+
}
|
|
11743
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
11744
|
+
return { event: append.event, deliveries, deduped: false };
|
|
11745
|
+
}
|
|
11746
|
+
async listEvents(options = {}) {
|
|
11747
|
+
if (Object.keys(options).length === 0)
|
|
11748
|
+
return this.store.listEvents();
|
|
11749
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
11750
|
+
}
|
|
11751
|
+
async listEventsPage(options = {}) {
|
|
11752
|
+
if (this.store.listEventsPage)
|
|
11753
|
+
return this.store.listEventsPage(options);
|
|
11754
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
11755
|
+
eventId: options.eventId,
|
|
11756
|
+
source: options.source,
|
|
11757
|
+
type: options.type
|
|
11758
|
+
});
|
|
11759
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
11760
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
11761
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
11762
|
+
const nextOffset = offset + pageEvents.length;
|
|
11763
|
+
const hasMore = nextOffset < events.length;
|
|
11764
|
+
return {
|
|
11765
|
+
events: pageEvents,
|
|
11766
|
+
cursor: options.cursor,
|
|
11767
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
11768
|
+
hasMore
|
|
11769
|
+
};
|
|
11514
11770
|
}
|
|
11515
11771
|
async listDeliveries() {
|
|
11516
11772
|
return this.store.listDeliveries();
|
|
@@ -11578,22 +11834,37 @@ class EventsClient {
|
|
|
11578
11834
|
return result;
|
|
11579
11835
|
}
|
|
11580
11836
|
async replay(options = {}) {
|
|
11581
|
-
const
|
|
11582
|
-
if (options.eventId && event.id !== options.eventId)
|
|
11583
|
-
return false;
|
|
11584
|
-
if (options.source && event.source !== options.source)
|
|
11585
|
-
return false;
|
|
11586
|
-
if (options.type && event.type !== options.type)
|
|
11587
|
-
return false;
|
|
11588
|
-
return true;
|
|
11589
|
-
});
|
|
11837
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
11590
11838
|
if (options.dryRun)
|
|
11591
|
-
return { events, deliveries: [] };
|
|
11839
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
11592
11840
|
const deliveries = [];
|
|
11593
|
-
for (const event of events) {
|
|
11841
|
+
for (const event of page.events) {
|
|
11594
11842
|
deliveries.push(...await this.deliver(event));
|
|
11595
11843
|
}
|
|
11596
|
-
return { events, deliveries };
|
|
11844
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
11845
|
+
}
|
|
11846
|
+
async appendEvent(event, options) {
|
|
11847
|
+
if (this.store.appendEventOnce) {
|
|
11848
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
11849
|
+
}
|
|
11850
|
+
if (options.dedupe) {
|
|
11851
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
11852
|
+
if (existing) {
|
|
11853
|
+
return {
|
|
11854
|
+
event: existing,
|
|
11855
|
+
stored: false,
|
|
11856
|
+
deduped: true,
|
|
11857
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
11858
|
+
};
|
|
11859
|
+
}
|
|
11860
|
+
}
|
|
11861
|
+
const stored = await this.store.appendEvent(event);
|
|
11862
|
+
return {
|
|
11863
|
+
event: stored,
|
|
11864
|
+
stored: true,
|
|
11865
|
+
deduped: false,
|
|
11866
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
11867
|
+
};
|
|
11597
11868
|
}
|
|
11598
11869
|
async applyRedaction(event, channel) {
|
|
11599
11870
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -11620,43 +11891,19 @@ class EventsClient {
|
|
|
11620
11891
|
return createDeliveryResult(event, channel, attempts);
|
|
11621
11892
|
}
|
|
11622
11893
|
}
|
|
11623
|
-
function
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
11637
|
-
}
|
|
11638
|
-
function redactValue2(value, replacement) {
|
|
11639
|
-
if (Array.isArray(value))
|
|
11640
|
-
return value.map((item) => redactValue2(item, replacement));
|
|
11641
|
-
if (!value || typeof value !== "object")
|
|
11642
|
-
return value;
|
|
11643
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
11644
|
-
key,
|
|
11645
|
-
shouldRedactKey(key) ? replacement : redactValue2(item, replacement)
|
|
11646
|
-
]));
|
|
11647
|
-
}
|
|
11648
|
-
function setPath(input, path, replacement) {
|
|
11649
|
-
const parts = path.split(".");
|
|
11650
|
-
let cursor = input;
|
|
11651
|
-
for (const part of parts.slice(0, -1)) {
|
|
11652
|
-
const next = cursor[part];
|
|
11653
|
-
if (!next || typeof next !== "object")
|
|
11654
|
-
return;
|
|
11655
|
-
cursor = next;
|
|
11656
|
-
}
|
|
11657
|
-
const last = parts.at(-1);
|
|
11658
|
-
if (last && last in cursor)
|
|
11659
|
-
cursor[last] = replacement;
|
|
11894
|
+
function queryClientEvents(events, options) {
|
|
11895
|
+
let rows = events;
|
|
11896
|
+
if (options.eventId)
|
|
11897
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
11898
|
+
if (options.source)
|
|
11899
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
11900
|
+
if (options.type)
|
|
11901
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
11902
|
+
if (options.cursor)
|
|
11903
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
11904
|
+
if (options.limit !== undefined)
|
|
11905
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
11906
|
+
return rows;
|
|
11660
11907
|
}
|
|
11661
11908
|
function normalizeTime(value) {
|
|
11662
11909
|
if (!value)
|
|
@@ -11670,9 +11917,22 @@ function normalizeRetryPolicy(policy) {
|
|
|
11670
11917
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
11671
11918
|
};
|
|
11672
11919
|
}
|
|
11673
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", DEFAULT_SIGNATURE_TOLERANCE_MS;
|
|
11920
|
+
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;
|
|
11674
11921
|
var init_dist = __esm(() => {
|
|
11675
11922
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
11923
|
+
EventValidationError = class EventValidationError extends Error {
|
|
11924
|
+
eventType;
|
|
11925
|
+
issues;
|
|
11926
|
+
constructor(eventType, issues) {
|
|
11927
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
11928
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
11929
|
+
this.name = "EventValidationError";
|
|
11930
|
+
this.eventType = eventType;
|
|
11931
|
+
this.issues = issues;
|
|
11932
|
+
}
|
|
11933
|
+
};
|
|
11934
|
+
defaultEventTypeCatalog = new EventTypeCatalog;
|
|
11935
|
+
APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
11676
11936
|
});
|
|
11677
11937
|
|
|
11678
11938
|
// src/db/task-lists.ts
|
|
@@ -12511,8 +12771,12 @@ async function deliverWebhook(wh, event, body, attempt, db) {
|
|
|
12511
12771
|
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
12512
12772
|
headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
12513
12773
|
}
|
|
12514
|
-
const resp = await fetch(wh.url, { method: "POST", headers, body });
|
|
12774
|
+
const resp = await fetch(wh.url, { method: "POST", headers, body, redirect: "manual" });
|
|
12515
12775
|
const respText = await resp.text().catch(() => "");
|
|
12776
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
12777
|
+
logDelivery(db, wh.id, event, body, resp.status, "Blocked: webhook URL returned a 3xx redirect; redirects are never followed (SSRF prevention)", attempt);
|
|
12778
|
+
return;
|
|
12779
|
+
}
|
|
12516
12780
|
logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
|
|
12517
12781
|
if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
|
|
12518
12782
|
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
@@ -14309,6 +14573,12 @@ function listTasks(filter = {}, db) {
|
|
|
14309
14573
|
const d = db || getDatabase();
|
|
14310
14574
|
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
14311
14575
|
clearExpiredLocks2(d);
|
|
14576
|
+
if (filter.limit !== undefined) {
|
|
14577
|
+
const limit = filter.limit;
|
|
14578
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > MAX_TASK_LIST_LIMIT) {
|
|
14579
|
+
throw new TypeError(`listTasks limit must be a positive integer between 1 and ${MAX_TASK_LIST_LIMIT}; got ${String(limit)}`);
|
|
14580
|
+
}
|
|
14581
|
+
}
|
|
14312
14582
|
const conditions = [];
|
|
14313
14583
|
const params = [];
|
|
14314
14584
|
if (filter.project_id) {
|
|
@@ -14842,6 +15112,7 @@ function deleteTask(id, db) {
|
|
|
14842
15112
|
}
|
|
14843
15113
|
return result.changes > 0;
|
|
14844
15114
|
}
|
|
15115
|
+
var MAX_TASK_LIST_LIMIT = 1e5;
|
|
14845
15116
|
var init_task_crud = __esm(() => {
|
|
14846
15117
|
init_types();
|
|
14847
15118
|
init_database();
|
|
@@ -17756,7 +18027,8 @@ __export(exports_tasks, {
|
|
|
17756
18027
|
buildTaskBoardSnapshot: () => buildTaskBoardSnapshot,
|
|
17757
18028
|
archiveTasks: () => archiveTasks,
|
|
17758
18029
|
archiveCompletedTasks: () => archiveCompletedTasks,
|
|
17759
|
-
addDependency: () => addDependency
|
|
18030
|
+
addDependency: () => addDependency,
|
|
18031
|
+
MAX_TASK_LIST_LIMIT: () => MAX_TASK_LIST_LIMIT
|
|
17760
18032
|
});
|
|
17761
18033
|
var init_tasks = __esm(() => {
|
|
17762
18034
|
init_task_crud();
|
|
@@ -22179,8 +22451,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
22179
22451
|
tags: exports_external.array(exports_external.string()).optional().describe("Filter by tags (AND logic)"),
|
|
22180
22452
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
22181
22453
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
22182
|
-
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
22183
|
-
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
22454
|
+
limit: exports_external.number().int().min(1).optional().describe("Max results (default: 50, max 500)"),
|
|
22455
|
+
offset: exports_external.number().int().min(0).optional().describe("Pagination offset"),
|
|
22184
22456
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
22185
22457
|
}, async (params) => {
|
|
22186
22458
|
try {
|
|
@@ -25530,6 +25802,48 @@ var init_task_relationships = __esm(() => {
|
|
|
25530
25802
|
];
|
|
25531
25803
|
});
|
|
25532
25804
|
|
|
25805
|
+
// src/lib/dedupe-projection.ts
|
|
25806
|
+
function projectTasksForDedupe(tasks) {
|
|
25807
|
+
return tasks.map((task) => {
|
|
25808
|
+
const metadata = {};
|
|
25809
|
+
for (const key of DEDUPE_SOURCE_KEY_ALLOWLIST) {
|
|
25810
|
+
if (key in task.metadata) {
|
|
25811
|
+
metadata[key] = redactValue(task.metadata[key]);
|
|
25812
|
+
}
|
|
25813
|
+
}
|
|
25814
|
+
return {
|
|
25815
|
+
id: task.id,
|
|
25816
|
+
short_id: task.short_id,
|
|
25817
|
+
title: redactEvidenceText(task.title),
|
|
25818
|
+
description: task.description === null || task.description === undefined ? null : redactEvidenceText(task.description),
|
|
25819
|
+
status: task.status,
|
|
25820
|
+
created_at: task.created_at,
|
|
25821
|
+
updated_at: task.updated_at,
|
|
25822
|
+
project_id: task.project_id,
|
|
25823
|
+
task_list_id: task.task_list_id,
|
|
25824
|
+
assigned_to: task.assigned_to,
|
|
25825
|
+
priority: task.priority,
|
|
25826
|
+
metadata
|
|
25827
|
+
};
|
|
25828
|
+
});
|
|
25829
|
+
}
|
|
25830
|
+
var DEDUPE_SOURCE_KEY_ALLOWLIST;
|
|
25831
|
+
var init_dedupe_projection = __esm(() => {
|
|
25832
|
+
init_redaction();
|
|
25833
|
+
DEDUPE_SOURCE_KEY_ALLOWLIST = [
|
|
25834
|
+
"github_url",
|
|
25835
|
+
"github_issue_url",
|
|
25836
|
+
"github_pr_url",
|
|
25837
|
+
"source_url",
|
|
25838
|
+
"url",
|
|
25839
|
+
"external_url",
|
|
25840
|
+
"issue_url",
|
|
25841
|
+
"github_owner",
|
|
25842
|
+
"github_repo",
|
|
25843
|
+
"github_number"
|
|
25844
|
+
];
|
|
25845
|
+
});
|
|
25846
|
+
|
|
25533
25847
|
// src/lib/task-dedupe.ts
|
|
25534
25848
|
function asObject(value) {
|
|
25535
25849
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -25694,10 +26008,10 @@ function olderFirst(left, right) {
|
|
|
25694
26008
|
function findDuplicateTasks(options = {}, db) {
|
|
25695
26009
|
const d = db || getDatabase();
|
|
25696
26010
|
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
25697
|
-
const fingerprints = listTasks({
|
|
26011
|
+
const fingerprints = projectTasksForDedupe(listTasks({
|
|
25698
26012
|
include_archived: Boolean(options.include_archived),
|
|
25699
26013
|
limit: options.limit ?? 1000
|
|
25700
|
-
}, d).map(fingerprint);
|
|
26014
|
+
}, d)).map(fingerprint);
|
|
25701
26015
|
const candidates = [];
|
|
25702
26016
|
for (let i = 0;i < fingerprints.length; i++) {
|
|
25703
26017
|
for (let j = i + 1;j < fingerprints.length; j++) {
|
|
@@ -25932,6 +26246,7 @@ var init_task_dedupe = __esm(() => {
|
|
|
25932
26246
|
init_database();
|
|
25933
26247
|
init_task_relationships();
|
|
25934
26248
|
init_tasks();
|
|
26249
|
+
init_dedupe_projection();
|
|
25935
26250
|
STOP_WORDS = new Set([
|
|
25936
26251
|
"a",
|
|
25937
26252
|
"an",
|
|
@@ -36102,7 +36417,7 @@ var package_default;
|
|
|
36102
36417
|
var init_package = __esm(() => {
|
|
36103
36418
|
package_default = {
|
|
36104
36419
|
name: "@hasna/todos",
|
|
36105
|
-
version: "0.15.
|
|
36420
|
+
version: "0.15.49",
|
|
36106
36421
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
36107
36422
|
type: "module",
|
|
36108
36423
|
main: "dist/index.js",
|
|
@@ -36166,6 +36481,7 @@ var init_package = __esm(() => {
|
|
|
36166
36481
|
],
|
|
36167
36482
|
scripts: {
|
|
36168
36483
|
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",
|
|
36484
|
+
"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",
|
|
36169
36485
|
"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/*'",
|
|
36170
36486
|
migrate: "bun run src/server/index.ts migrate",
|
|
36171
36487
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
@@ -36220,18 +36536,19 @@ var init_package = __esm(() => {
|
|
|
36220
36536
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
36221
36537
|
license: "Apache-2.0",
|
|
36222
36538
|
dependencies: {
|
|
36223
|
-
"@hasna/contracts": "0.
|
|
36539
|
+
"@hasna/contracts": "0.14.0",
|
|
36224
36540
|
"@hasna/events": "^0.1.11",
|
|
36225
36541
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
36226
36542
|
chalk: "^5.4.1",
|
|
36227
36543
|
commander: "^13.1.0",
|
|
36228
36544
|
ink: "^5.2.0",
|
|
36229
36545
|
react: "^18.3.1",
|
|
36230
|
-
zod: "
|
|
36546
|
+
zod: "3.25.76"
|
|
36231
36547
|
},
|
|
36232
36548
|
overrides: {
|
|
36233
36549
|
ajv: "8.20.0",
|
|
36234
|
-
"fast-uri": "3.1.2"
|
|
36550
|
+
"fast-uri": "3.1.2",
|
|
36551
|
+
zod: "3.25.76"
|
|
36235
36552
|
},
|
|
36236
36553
|
devDependencies: {
|
|
36237
36554
|
"@types/bun": "^1.2.4",
|
|
@@ -46475,6 +46792,27 @@ var init_local_sqlite = __esm(() => {
|
|
|
46475
46792
|
TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
46476
46793
|
});
|
|
46477
46794
|
|
|
46795
|
+
// src/storage/postgres-errors.ts
|
|
46796
|
+
function isPostgresUniqueViolation(error) {
|
|
46797
|
+
if (typeof error !== "object" || error === null)
|
|
46798
|
+
return false;
|
|
46799
|
+
const candidate = error;
|
|
46800
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
46801
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
46802
|
+
const cause = candidate.cause;
|
|
46803
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
46804
|
+
}
|
|
46805
|
+
return states.some((state) => String(state) === "23505");
|
|
46806
|
+
}
|
|
46807
|
+
function postgresConstraintName(error) {
|
|
46808
|
+
if (typeof error !== "object" || error === null)
|
|
46809
|
+
return "";
|
|
46810
|
+
const candidate = error;
|
|
46811
|
+
const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
|
|
46812
|
+
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
46813
|
+
return typeof constraint === "string" ? constraint : "";
|
|
46814
|
+
}
|
|
46815
|
+
|
|
46478
46816
|
// src/storage/postgres-sync.ts
|
|
46479
46817
|
function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
|
|
46480
46818
|
assertSafeIdentifier(tableName);
|
|
@@ -46669,53 +47007,98 @@ class PostgresTodosSyncStore {
|
|
|
46669
47007
|
if (routingErrors.length > 0) {
|
|
46670
47008
|
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
46671
47009
|
}
|
|
46672
|
-
const
|
|
46673
|
-
|
|
46674
|
-
|
|
46675
|
-
|
|
46676
|
-
|
|
46677
|
-
|
|
46678
|
-
const
|
|
46679
|
-
|
|
46680
|
-
|
|
46681
|
-
|
|
46682
|
-
|
|
46683
|
-
|
|
46684
|
-
|
|
46685
|
-
|
|
46686
|
-
|
|
46687
|
-
|
|
46688
|
-
|
|
46689
|
-
|
|
46690
|
-
|
|
46691
|
-
|
|
46692
|
-
|
|
46693
|
-
|
|
46694
|
-
|
|
46695
|
-
|
|
46696
|
-
|
|
46697
|
-
|
|
46698
|
-
|
|
46699
|
-
|
|
46700
|
-
|
|
46701
|
-
|
|
46702
|
-
|
|
46703
|
-
|
|
46704
|
-
|
|
46705
|
-
|
|
46706
|
-
|
|
46707
|
-
|
|
46708
|
-
|
|
46709
|
-
|
|
46710
|
-
|
|
46711
|
-
|
|
46712
|
-
|
|
46713
|
-
|
|
46714
|
-
|
|
46715
|
-
|
|
46716
|
-
|
|
47010
|
+
const push = async (client) => {
|
|
47011
|
+
const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
47012
|
+
FROM ${this.tableName}
|
|
47013
|
+
WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
|
|
47014
|
+
const existingProjects = [];
|
|
47015
|
+
const existingTaskLists = [];
|
|
47016
|
+
for (const row of existing.rows) {
|
|
47017
|
+
const payload = payloadRecord(row.payload);
|
|
47018
|
+
if (row.object_type === "projects")
|
|
47019
|
+
existingProjects.push(payload);
|
|
47020
|
+
if (row.object_type === "task_lists")
|
|
47021
|
+
existingTaskLists.push(payload);
|
|
47022
|
+
}
|
|
47023
|
+
const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
|
|
47024
|
+
if (destinationErrors.length > 0) {
|
|
47025
|
+
throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
47026
|
+
}
|
|
47027
|
+
const result = { records: 0, objectTypes: {} };
|
|
47028
|
+
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
47029
|
+
for (const entry of snapshotEntries(snapshot)) {
|
|
47030
|
+
if (entry.deletedAt === null)
|
|
47031
|
+
assertCanonicalScopedSlugEntry(entry);
|
|
47032
|
+
try {
|
|
47033
|
+
await client.query(`INSERT INTO ${this.tableName} (
|
|
47034
|
+
service, object_type, object_id, payload, updated_at,
|
|
47035
|
+
deleted_at, source_machine_id, version
|
|
47036
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
|
|
47037
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
47038
|
+
payload = EXCLUDED.payload,
|
|
47039
|
+
updated_at = EXCLUDED.updated_at,
|
|
47040
|
+
deleted_at = EXCLUDED.deleted_at,
|
|
47041
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
47042
|
+
version = EXCLUDED.version
|
|
47043
|
+
WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
47044
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
47045
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
|
|
47046
|
+
this.service,
|
|
47047
|
+
entry.type,
|
|
47048
|
+
entry.id,
|
|
47049
|
+
entry.payload,
|
|
47050
|
+
entry.updatedAt,
|
|
47051
|
+
entry.deletedAt,
|
|
47052
|
+
sourceMachineId,
|
|
47053
|
+
entry.version
|
|
47054
|
+
]);
|
|
47055
|
+
} catch (error) {
|
|
47056
|
+
await this.classifySyncInsertConflict(error, entry);
|
|
47057
|
+
}
|
|
47058
|
+
result.records += 1;
|
|
47059
|
+
result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
|
|
47060
|
+
}
|
|
47061
|
+
return result;
|
|
47062
|
+
};
|
|
47063
|
+
if (typeof this.client.transaction === "function") {
|
|
47064
|
+
return this.client.transaction((client) => push(client));
|
|
46717
47065
|
}
|
|
46718
|
-
return
|
|
47066
|
+
return push(this.client);
|
|
47067
|
+
}
|
|
47068
|
+
async classifySyncInsertConflict(error, entry) {
|
|
47069
|
+
if (!isPostgresUniqueViolation(error))
|
|
47070
|
+
throw error;
|
|
47071
|
+
const payload = entry.payload;
|
|
47072
|
+
const taskListSlug = String(payload["slug"] ?? "");
|
|
47073
|
+
const projectSlug = String(payload["task_list_id"] ?? "");
|
|
47074
|
+
const constraintName = postgresConstraintName(error);
|
|
47075
|
+
if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
|
|
47076
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
47077
|
+
}
|
|
47078
|
+
if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
|
|
47079
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
47080
|
+
}
|
|
47081
|
+
if (entry.type === "task_lists") {
|
|
47082
|
+
const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
|
|
47083
|
+
const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
|
|
47084
|
+
SELECT 1 FROM ${this.tableName}
|
|
47085
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
|
|
47086
|
+
AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
|
|
47087
|
+
) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
|
|
47088
|
+
if (conflict.rows[0]?.conflict) {
|
|
47089
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
47090
|
+
}
|
|
47091
|
+
} else if (entry.type === "projects") {
|
|
47092
|
+
const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
|
|
47093
|
+
SELECT 1 FROM ${this.tableName}
|
|
47094
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
|
|
47095
|
+
AND deleted_at IS NULL AND payload->>'task_list_id' = $3
|
|
47096
|
+
) AS conflict`, [this.service, entry.id, projectSlug]);
|
|
47097
|
+
if (conflict.rows[0]?.conflict) {
|
|
47098
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
47099
|
+
}
|
|
47100
|
+
}
|
|
47101
|
+
throw error;
|
|
46719
47102
|
}
|
|
46720
47103
|
async pullSnapshot(options = {}) {
|
|
46721
47104
|
const params = [this.service];
|
|
@@ -46872,6 +47255,7 @@ function assertSafeIdentifier(value) {
|
|
|
46872
47255
|
}
|
|
46873
47256
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records", DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors", PostgresScopedSlugMigrationConflictError, PostgresScopedSlugIndexBuildError;
|
|
46874
47257
|
var init_postgres_sync = __esm(() => {
|
|
47258
|
+
init_types();
|
|
46875
47259
|
PostgresScopedSlugMigrationConflictError = class PostgresScopedSlugMigrationConflictError extends Error {
|
|
46876
47260
|
conflicts;
|
|
46877
47261
|
constructor(conflicts) {
|
|
@@ -47017,6 +47401,12 @@ class TodosShadowOutbox {
|
|
|
47017
47401
|
this.onEvent?.({ type: "mirrored", objectType: row.object_type, id: row.object_id, lagMs });
|
|
47018
47402
|
} catch (error) {
|
|
47019
47403
|
const message = error instanceof Error ? error.message : String(error);
|
|
47404
|
+
if (error instanceof ResourceConflictError) {
|
|
47405
|
+
this.metrics.lastError = message;
|
|
47406
|
+
this.db.run(`UPDATE shadow_outbox SET attempts=?, last_error=?, status='failed' WHERE seq=? AND revision=?`, [row.attempts + 1, message, row.seq, row.revision]);
|
|
47407
|
+
this.onEvent?.({ type: "parked", objectType: row.object_type, id: row.object_id, error: message });
|
|
47408
|
+
return;
|
|
47409
|
+
}
|
|
47020
47410
|
this.metrics.retries += 1;
|
|
47021
47411
|
this.metrics.lastError = message;
|
|
47022
47412
|
const attempts = row.attempts + 1;
|
|
@@ -47139,6 +47529,7 @@ function emptySnapshot() {
|
|
|
47139
47529
|
}
|
|
47140
47530
|
var MAX_BACKOFF_MS;
|
|
47141
47531
|
var init_shadow_outbox = __esm(() => {
|
|
47532
|
+
init_types();
|
|
47142
47533
|
init_local_sqlite();
|
|
47143
47534
|
init_postgres_sync();
|
|
47144
47535
|
init_shadow_outbox_schema();
|
|
@@ -47490,9 +47881,11 @@ class PostgresJsonRecordStore {
|
|
|
47490
47881
|
async ensureSchema() {
|
|
47491
47882
|
if (!this.schemaReady) {
|
|
47492
47883
|
this.schemaReady = (async () => {
|
|
47493
|
-
|
|
47494
|
-
|
|
47495
|
-
|
|
47884
|
+
await retryOnTransientPostgresError(async () => {
|
|
47885
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
47886
|
+
await this.options.client.query(sql);
|
|
47887
|
+
}
|
|
47888
|
+
});
|
|
47496
47889
|
})().catch((error) => {
|
|
47497
47890
|
this.schemaReady = null;
|
|
47498
47891
|
throw error;
|
|
@@ -47906,13 +48299,14 @@ class PostgresJsonRecordStore {
|
|
|
47906
48299
|
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
47907
48300
|
}
|
|
47908
48301
|
async withTaskParentIntegrityTransaction(fn) {
|
|
47909
|
-
|
|
48302
|
+
const transaction = this.options.client.transaction;
|
|
48303
|
+
if (typeof transaction !== "function") {
|
|
47910
48304
|
throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
|
|
47911
48305
|
}
|
|
47912
|
-
return
|
|
48306
|
+
return retryOnTransientPostgresError(() => transaction(async (client) => {
|
|
47913
48307
|
await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
|
|
47914
48308
|
return fn(client);
|
|
47915
|
-
});
|
|
48309
|
+
}));
|
|
47916
48310
|
}
|
|
47917
48311
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
|
|
47918
48312
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
@@ -49795,7 +50189,7 @@ function compareClock(left, right) {
|
|
|
49795
50189
|
function numberValue3(value) {
|
|
49796
50190
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
49797
50191
|
}
|
|
49798
|
-
function
|
|
50192
|
+
function isTransientPostgresError(error) {
|
|
49799
50193
|
if (typeof error !== "object" || error === null)
|
|
49800
50194
|
return false;
|
|
49801
50195
|
const candidate = error;
|
|
@@ -49804,17 +50198,27 @@ function isPostgresUniqueViolation(error) {
|
|
|
49804
50198
|
const cause = candidate.cause;
|
|
49805
50199
|
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
49806
50200
|
}
|
|
49807
|
-
|
|
50201
|
+
if (states.some((state) => typeof state === "string" && TRANSIENT_POSTGRES_SQLSTATES.has(state))) {
|
|
50202
|
+
return true;
|
|
50203
|
+
}
|
|
50204
|
+
const message = typeof candidate.message === "string" ? candidate.message : candidate.cause?.message;
|
|
50205
|
+
return typeof message === "string" && TRANSIENT_POSTGRES_MESSAGE_MARKERS.some((marker) => message.includes(marker));
|
|
49808
50206
|
}
|
|
49809
|
-
function
|
|
49810
|
-
|
|
49811
|
-
|
|
49812
|
-
|
|
49813
|
-
|
|
49814
|
-
|
|
49815
|
-
|
|
50207
|
+
async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
|
|
50208
|
+
let lastError;
|
|
50209
|
+
for (let attempt = 1;attempt <= attempts; attempt += 1) {
|
|
50210
|
+
try {
|
|
50211
|
+
return await fn();
|
|
50212
|
+
} catch (error) {
|
|
50213
|
+
lastError = error;
|
|
50214
|
+
if (!isTransientPostgresError(error) || attempt === attempts)
|
|
50215
|
+
throw error;
|
|
50216
|
+
await new Promise((resolve16) => setTimeout(resolve16, delayMs * attempt));
|
|
50217
|
+
}
|
|
50218
|
+
}
|
|
50219
|
+
throw lastError;
|
|
49816
50220
|
}
|
|
49817
|
-
var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY;
|
|
50221
|
+
var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC", TASK_ORDER_BY, TRANSIENT_POSTGRES_SQLSTATES, TRANSIENT_POSTGRES_MESSAGE_MARKERS;
|
|
49818
50222
|
var init_postgres_adapter = __esm(() => {
|
|
49819
50223
|
init_types();
|
|
49820
50224
|
init_creator_identity();
|
|
@@ -49828,6 +50232,12 @@ var init_postgres_adapter = __esm(() => {
|
|
|
49828
50232
|
init_audit_history_import();
|
|
49829
50233
|
init_canonical();
|
|
49830
50234
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
50235
|
+
TRANSIENT_POSTGRES_SQLSTATES = new Set(["55P03", "40P01", "40001"]);
|
|
50236
|
+
TRANSIENT_POSTGRES_MESSAGE_MARKERS = [
|
|
50237
|
+
"canceling statement due to lock timeout",
|
|
50238
|
+
"deadlock detected",
|
|
50239
|
+
"could not serialize access"
|
|
50240
|
+
];
|
|
49831
50241
|
});
|
|
49832
50242
|
|
|
49833
50243
|
// src/pr-groups/postgres.ts
|
|
@@ -55707,6 +56117,13 @@ function resolveSigningSecret(env = process.env) {
|
|
|
55707
56117
|
function isPostgresBackendConfigured(env = process.env) {
|
|
55708
56118
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
55709
56119
|
}
|
|
56120
|
+
function schemaRetryMinIntervalMs(env = process.env) {
|
|
56121
|
+
const raw = env.HASNA_TODOS_SCHEMA_RETRY_MIN_MS;
|
|
56122
|
+
if (!raw)
|
|
56123
|
+
return DEFAULT_SCHEMA_RETRY_MIN_MS;
|
|
56124
|
+
const parsed = Number(raw);
|
|
56125
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SCHEMA_RETRY_MIN_MS;
|
|
56126
|
+
}
|
|
55710
56127
|
function getCloudTenantId() {
|
|
55711
56128
|
return process.env.HASNA_TODOS_TENANT_ID ?? "default";
|
|
55712
56129
|
}
|
|
@@ -55802,6 +56219,10 @@ function getCloudVerifier() {
|
|
|
55802
56219
|
async function ensureCloudSchema() {
|
|
55803
56220
|
if (schemaEnsured)
|
|
55804
56221
|
return schemaEnsured;
|
|
56222
|
+
if (lastSchemaFailure !== null && Date.now() - lastSchemaAttemptAtMs < schemaRetryMinIntervalMs()) {
|
|
56223
|
+
throw lastSchemaFailure;
|
|
56224
|
+
}
|
|
56225
|
+
lastSchemaAttemptAtMs = Date.now();
|
|
55805
56226
|
schemaEnsured = (async () => {
|
|
55806
56227
|
const client = getClient();
|
|
55807
56228
|
for (const sql of postgresTodosSyncSchemaSql()) {
|
|
@@ -55822,6 +56243,7 @@ async function ensureCloudSchema() {
|
|
|
55822
56243
|
await getApiKeyStore().ensureSchema();
|
|
55823
56244
|
})().catch((error) => {
|
|
55824
56245
|
schemaEnsured = null;
|
|
56246
|
+
lastSchemaFailure = error;
|
|
55825
56247
|
throw error;
|
|
55826
56248
|
});
|
|
55827
56249
|
return schemaEnsured;
|
|
@@ -55869,8 +56291,10 @@ async function closeCloud() {
|
|
|
55869
56291
|
cachedTaskManifestAuthority = null;
|
|
55870
56292
|
cachedTaskSubtreeTransferAuthority = null;
|
|
55871
56293
|
schemaEnsured = null;
|
|
56294
|
+
lastSchemaAttemptAtMs = 0;
|
|
56295
|
+
lastSchemaFailure = null;
|
|
55872
56296
|
}
|
|
55873
|
-
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null;
|
|
56297
|
+
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null, lastSchemaAttemptAtMs = 0, lastSchemaFailure = null, DEFAULT_SCHEMA_RETRY_MIN_MS = 1e4;
|
|
55874
56298
|
var init_cloud = __esm(() => {
|
|
55875
56299
|
init_cloud_client();
|
|
55876
56300
|
init_postgres_adapter();
|
|
@@ -56394,6 +56818,19 @@ function taskStatusQueryParam(url) {
|
|
|
56394
56818
|
return { ok: false, message: result.message };
|
|
56395
56819
|
return { ok: true, value: collapseEnumValues(result.values) };
|
|
56396
56820
|
}
|
|
56821
|
+
function parsePaginationQueryParam(url, name) {
|
|
56822
|
+
const raw = url.searchParams.get(name);
|
|
56823
|
+
if (raw === null)
|
|
56824
|
+
return { ok: true, value: undefined };
|
|
56825
|
+
const min = name === "limit" ? 1 : 0;
|
|
56826
|
+
const message = name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer";
|
|
56827
|
+
if (!/^\d+$/.test(raw))
|
|
56828
|
+
return { ok: false, message };
|
|
56829
|
+
const value = Number(raw);
|
|
56830
|
+
if (!Number.isSafeInteger(value) || value < min)
|
|
56831
|
+
return { ok: false, message };
|
|
56832
|
+
return { ok: true, value };
|
|
56833
|
+
}
|
|
56397
56834
|
async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
|
|
56398
56835
|
const statusParam = taskStatusQueryParam(url);
|
|
56399
56836
|
if (!statusParam.ok)
|
|
@@ -56401,16 +56838,20 @@ async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
|
|
|
56401
56838
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
56402
56839
|
const sessionId = url.searchParams.get("session_id") || undefined;
|
|
56403
56840
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
56404
|
-
const limitParam = url
|
|
56405
|
-
|
|
56841
|
+
const limitParam = parsePaginationQueryParam(url, "limit");
|
|
56842
|
+
if (!limitParam.ok)
|
|
56843
|
+
return json5({ error: limitParam.message }, 400);
|
|
56844
|
+
const offsetParam = parsePaginationQueryParam(url, "offset");
|
|
56845
|
+
if (!offsetParam.ok)
|
|
56846
|
+
return json5({ error: offsetParam.message }, 400);
|
|
56406
56847
|
const fields = parseFieldsParam(url);
|
|
56407
56848
|
const tasks = listTasks({
|
|
56408
56849
|
status: statusParam.value,
|
|
56409
56850
|
project_id: projectId,
|
|
56410
56851
|
session_id: sessionId,
|
|
56411
56852
|
agent_id: agentId,
|
|
56412
|
-
limit: limitParam
|
|
56413
|
-
offset: offsetParam
|
|
56853
|
+
limit: limitParam.value,
|
|
56854
|
+
offset: offsetParam.value
|
|
56414
56855
|
});
|
|
56415
56856
|
return json5(tasks.map((t) => taskToSummary2(t, fields)));
|
|
56416
56857
|
}
|
|
@@ -61279,6 +61720,26 @@ function parseSinceCursor(raw) {
|
|
|
61279
61720
|
}
|
|
61280
61721
|
return { ok: true, value: new Date(parsed).toISOString() };
|
|
61281
61722
|
}
|
|
61723
|
+
function paginationQueryParam(url, name) {
|
|
61724
|
+
const raw = url.searchParams.get(name);
|
|
61725
|
+
if (raw === null)
|
|
61726
|
+
return { ok: true, value: undefined };
|
|
61727
|
+
const min = name === "limit" ? 1 : 0;
|
|
61728
|
+
if (!/^\d+$/.test(raw)) {
|
|
61729
|
+
return {
|
|
61730
|
+
ok: false,
|
|
61731
|
+
response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
|
|
61732
|
+
};
|
|
61733
|
+
}
|
|
61734
|
+
const value = Number(raw);
|
|
61735
|
+
if (!Number.isSafeInteger(value) || value < min) {
|
|
61736
|
+
return {
|
|
61737
|
+
ok: false,
|
|
61738
|
+
response: error(400, name === "limit" ? "limit must be a positive integer" : "offset must be a non-negative integer")
|
|
61739
|
+
};
|
|
61740
|
+
}
|
|
61741
|
+
return { ok: true, value };
|
|
61742
|
+
}
|
|
61282
61743
|
function validateTaskCompletion(value) {
|
|
61283
61744
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
61284
61745
|
return { ok: false, message: "completion body must be an object" };
|
|
@@ -61807,6 +62268,12 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
61807
62268
|
if (updatedAfter !== null && !updatedAfter.ok) {
|
|
61808
62269
|
return error(400, updatedAfter.message);
|
|
61809
62270
|
}
|
|
62271
|
+
const limitParam = paginationQueryParam(url, "limit");
|
|
62272
|
+
if (!limitParam.ok)
|
|
62273
|
+
return limitParam.response;
|
|
62274
|
+
const offsetParam = paginationQueryParam(url, "offset");
|
|
62275
|
+
if (!offsetParam.ok)
|
|
62276
|
+
return offsetParam.response;
|
|
61810
62277
|
const filter = {
|
|
61811
62278
|
...updatedAfter !== null && updatedAfter.ok ? { updated_after: updatedAfter.value } : {},
|
|
61812
62279
|
...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
|
|
@@ -61823,8 +62290,8 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
61823
62290
|
...url.searchParams.get("tags") ? {
|
|
61824
62291
|
tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
|
|
61825
62292
|
} : {},
|
|
61826
|
-
...
|
|
61827
|
-
...
|
|
62293
|
+
...limitParam.value !== undefined ? { limit: limitParam.value } : {},
|
|
62294
|
+
...offsetParam.value !== undefined ? { offset: offsetParam.value } : {}
|
|
61828
62295
|
};
|
|
61829
62296
|
const tasks = await store.tasks.list(filter);
|
|
61830
62297
|
const { limit: _l, offset: _o, ...countFilter } = filter;
|