@hasna/todos 0.15.47 → 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/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/index.js +296 -81
- package/dist/contracts.js +56 -5
- 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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +187 -70
- 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 +246 -80
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +3 -2
- package/dist/project-registration.js +128 -68
- package/dist/registry.js +56 -5
- package/dist/release-provenance.json +5 -5
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +248 -82
- 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 +139 -66
- 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 +3 -2
package/dist/index.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]" },
|
|
@@ -9933,6 +9935,12 @@ function listTasks(filter = {}, db) {
|
|
|
9933
9935
|
const d = db || getDatabase();
|
|
9934
9936
|
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
9935
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
|
+
}
|
|
9936
9944
|
const conditions = [];
|
|
9937
9945
|
const params = [];
|
|
9938
9946
|
if (filter.project_id) {
|
|
@@ -10466,6 +10474,7 @@ function deleteTask(id, db) {
|
|
|
10466
10474
|
}
|
|
10467
10475
|
return result.changes > 0;
|
|
10468
10476
|
}
|
|
10477
|
+
var MAX_TASK_LIST_LIMIT = 1e5;
|
|
10469
10478
|
var init_task_crud = __esm(() => {
|
|
10470
10479
|
init_types();
|
|
10471
10480
|
init_database();
|
|
@@ -13184,7 +13193,7 @@ var init_dispatches = __esm(() => {
|
|
|
13184
13193
|
// package.json
|
|
13185
13194
|
var package_default = {
|
|
13186
13195
|
name: "@hasna/todos",
|
|
13187
|
-
version: "0.15.
|
|
13196
|
+
version: "0.15.49",
|
|
13188
13197
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
13189
13198
|
type: "module",
|
|
13190
13199
|
main: "dist/index.js",
|
|
@@ -13248,6 +13257,7 @@ var package_default = {
|
|
|
13248
13257
|
],
|
|
13249
13258
|
scripts: {
|
|
13250
13259
|
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",
|
|
13260
|
+
"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",
|
|
13251
13261
|
"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/*'",
|
|
13252
13262
|
migrate: "bun run src/server/index.ts migrate",
|
|
13253
13263
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
@@ -13302,7 +13312,7 @@ var package_default = {
|
|
|
13302
13312
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
13303
13313
|
license: "Apache-2.0",
|
|
13304
13314
|
dependencies: {
|
|
13305
|
-
"@hasna/contracts": "0.
|
|
13315
|
+
"@hasna/contracts": "0.14.0",
|
|
13306
13316
|
"@hasna/events": "^0.1.11",
|
|
13307
13317
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
13308
13318
|
chalk: "^5.4.1",
|
|
@@ -22129,6 +22139,47 @@ function autoDetectFileRelationships(taskId, db) {
|
|
|
22129
22139
|
|
|
22130
22140
|
// src/lib/task-dedupe.ts
|
|
22131
22141
|
init_tasks();
|
|
22142
|
+
|
|
22143
|
+
// src/lib/dedupe-projection.ts
|
|
22144
|
+
init_redaction();
|
|
22145
|
+
var DEDUPE_SOURCE_KEY_ALLOWLIST = [
|
|
22146
|
+
"github_url",
|
|
22147
|
+
"github_issue_url",
|
|
22148
|
+
"github_pr_url",
|
|
22149
|
+
"source_url",
|
|
22150
|
+
"url",
|
|
22151
|
+
"external_url",
|
|
22152
|
+
"issue_url",
|
|
22153
|
+
"github_owner",
|
|
22154
|
+
"github_repo",
|
|
22155
|
+
"github_number"
|
|
22156
|
+
];
|
|
22157
|
+
function projectTasksForDedupe(tasks) {
|
|
22158
|
+
return tasks.map((task2) => {
|
|
22159
|
+
const metadata = {};
|
|
22160
|
+
for (const key of DEDUPE_SOURCE_KEY_ALLOWLIST) {
|
|
22161
|
+
if (key in task2.metadata) {
|
|
22162
|
+
metadata[key] = redactValue(task2.metadata[key]);
|
|
22163
|
+
}
|
|
22164
|
+
}
|
|
22165
|
+
return {
|
|
22166
|
+
id: task2.id,
|
|
22167
|
+
short_id: task2.short_id,
|
|
22168
|
+
title: redactEvidenceText(task2.title),
|
|
22169
|
+
description: task2.description === null || task2.description === undefined ? null : redactEvidenceText(task2.description),
|
|
22170
|
+
status: task2.status,
|
|
22171
|
+
created_at: task2.created_at,
|
|
22172
|
+
updated_at: task2.updated_at,
|
|
22173
|
+
project_id: task2.project_id,
|
|
22174
|
+
task_list_id: task2.task_list_id,
|
|
22175
|
+
assigned_to: task2.assigned_to,
|
|
22176
|
+
priority: task2.priority,
|
|
22177
|
+
metadata
|
|
22178
|
+
};
|
|
22179
|
+
});
|
|
22180
|
+
}
|
|
22181
|
+
|
|
22182
|
+
// src/lib/task-dedupe.ts
|
|
22132
22183
|
var DEFAULT_THRESHOLD = 0.74;
|
|
22133
22184
|
var STOP_WORDS = new Set([
|
|
22134
22185
|
"a",
|
|
@@ -22321,10 +22372,10 @@ function olderFirst(left, right) {
|
|
|
22321
22372
|
function findDuplicateTasks(options = {}, db) {
|
|
22322
22373
|
const d = db || getDatabase();
|
|
22323
22374
|
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
22324
|
-
const fingerprints = listTasks({
|
|
22375
|
+
const fingerprints = projectTasksForDedupe(listTasks({
|
|
22325
22376
|
include_archived: Boolean(options.include_archived),
|
|
22326
22377
|
limit: options.limit ?? 1000
|
|
22327
|
-
}, d).map(fingerprint);
|
|
22378
|
+
}, d)).map(fingerprint);
|
|
22328
22379
|
const candidates = [];
|
|
22329
22380
|
for (let i = 0;i < fingerprints.length; i++) {
|
|
22330
22381
|
for (let j = i + 1;j < fingerprints.length; j++) {
|
|
@@ -27632,6 +27683,30 @@ function changedSinceStampNewer(stamp, since) {
|
|
|
27632
27683
|
// src/storage/postgres-adapter.ts
|
|
27633
27684
|
init_stale_lock_handoff();
|
|
27634
27685
|
|
|
27686
|
+
// src/storage/postgres-sync.ts
|
|
27687
|
+
init_types();
|
|
27688
|
+
|
|
27689
|
+
// src/storage/postgres-errors.ts
|
|
27690
|
+
function isPostgresUniqueViolation(error) {
|
|
27691
|
+
if (typeof error !== "object" || error === null)
|
|
27692
|
+
return false;
|
|
27693
|
+
const candidate = error;
|
|
27694
|
+
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
27695
|
+
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
27696
|
+
const cause = candidate.cause;
|
|
27697
|
+
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
27698
|
+
}
|
|
27699
|
+
return states.some((state) => String(state) === "23505");
|
|
27700
|
+
}
|
|
27701
|
+
function postgresConstraintName(error) {
|
|
27702
|
+
if (typeof error !== "object" || error === null)
|
|
27703
|
+
return "";
|
|
27704
|
+
const candidate = error;
|
|
27705
|
+
const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
|
|
27706
|
+
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
27707
|
+
return typeof constraint === "string" ? constraint : "";
|
|
27708
|
+
}
|
|
27709
|
+
|
|
27635
27710
|
// src/storage/postgres-sync.ts
|
|
27636
27711
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
27637
27712
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -27834,53 +27909,98 @@ class PostgresTodosSyncStore {
|
|
|
27834
27909
|
if (routingErrors.length > 0) {
|
|
27835
27910
|
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
27836
27911
|
}
|
|
27837
|
-
const
|
|
27838
|
-
|
|
27839
|
-
|
|
27840
|
-
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
const
|
|
27844
|
-
|
|
27845
|
-
|
|
27846
|
-
|
|
27847
|
-
|
|
27848
|
-
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
27852
|
-
|
|
27853
|
-
|
|
27854
|
-
|
|
27855
|
-
|
|
27856
|
-
|
|
27857
|
-
|
|
27858
|
-
|
|
27859
|
-
|
|
27860
|
-
|
|
27861
|
-
|
|
27862
|
-
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
27866
|
-
|
|
27867
|
-
|
|
27868
|
-
|
|
27869
|
-
|
|
27870
|
-
|
|
27871
|
-
|
|
27872
|
-
|
|
27873
|
-
|
|
27874
|
-
|
|
27875
|
-
|
|
27876
|
-
|
|
27877
|
-
|
|
27878
|
-
|
|
27879
|
-
|
|
27880
|
-
|
|
27881
|
-
|
|
27912
|
+
const push = async (client) => {
|
|
27913
|
+
const existing = await client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
27914
|
+
FROM ${this.tableName}
|
|
27915
|
+
WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
|
|
27916
|
+
const existingProjects = [];
|
|
27917
|
+
const existingTaskLists = [];
|
|
27918
|
+
for (const row of existing.rows) {
|
|
27919
|
+
const payload = payloadRecord(row.payload);
|
|
27920
|
+
if (row.object_type === "projects")
|
|
27921
|
+
existingProjects.push(payload);
|
|
27922
|
+
if (row.object_type === "task_lists")
|
|
27923
|
+
existingTaskLists.push(payload);
|
|
27924
|
+
}
|
|
27925
|
+
const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
|
|
27926
|
+
if (destinationErrors.length > 0) {
|
|
27927
|
+
throw new ResourceConflictError("SNAPSHOT_DESTINATION_CONFLICT", `Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
27928
|
+
}
|
|
27929
|
+
const result = { records: 0, objectTypes: {} };
|
|
27930
|
+
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
27931
|
+
for (const entry of snapshotEntries(snapshot)) {
|
|
27932
|
+
if (entry.deletedAt === null)
|
|
27933
|
+
assertCanonicalScopedSlugEntry(entry);
|
|
27934
|
+
try {
|
|
27935
|
+
await client.query(`INSERT INTO ${this.tableName} (
|
|
27936
|
+
service, object_type, object_id, payload, updated_at,
|
|
27937
|
+
deleted_at, source_machine_id, version
|
|
27938
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
|
|
27939
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
27940
|
+
payload = EXCLUDED.payload,
|
|
27941
|
+
updated_at = EXCLUDED.updated_at,
|
|
27942
|
+
deleted_at = EXCLUDED.deleted_at,
|
|
27943
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
27944
|
+
version = EXCLUDED.version
|
|
27945
|
+
WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
27946
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
27947
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
|
|
27948
|
+
this.service,
|
|
27949
|
+
entry.type,
|
|
27950
|
+
entry.id,
|
|
27951
|
+
entry.payload,
|
|
27952
|
+
entry.updatedAt,
|
|
27953
|
+
entry.deletedAt,
|
|
27954
|
+
sourceMachineId,
|
|
27955
|
+
entry.version
|
|
27956
|
+
]);
|
|
27957
|
+
} catch (error) {
|
|
27958
|
+
await this.classifySyncInsertConflict(error, entry);
|
|
27959
|
+
}
|
|
27960
|
+
result.records += 1;
|
|
27961
|
+
result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
|
|
27962
|
+
}
|
|
27963
|
+
return result;
|
|
27964
|
+
};
|
|
27965
|
+
if (typeof this.client.transaction === "function") {
|
|
27966
|
+
return this.client.transaction((client) => push(client));
|
|
27882
27967
|
}
|
|
27883
|
-
return
|
|
27968
|
+
return push(this.client);
|
|
27969
|
+
}
|
|
27970
|
+
async classifySyncInsertConflict(error, entry) {
|
|
27971
|
+
if (!isPostgresUniqueViolation(error))
|
|
27972
|
+
throw error;
|
|
27973
|
+
const payload = entry.payload;
|
|
27974
|
+
const taskListSlug = String(payload["slug"] ?? "");
|
|
27975
|
+
const projectSlug = String(payload["task_list_id"] ?? "");
|
|
27976
|
+
const constraintName = postgresConstraintName(error);
|
|
27977
|
+
if (entry.type === "projects" && constraintName.includes("project_task_list_slug_uidx")) {
|
|
27978
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
27979
|
+
}
|
|
27980
|
+
if (entry.type === "task_lists" && constraintName.includes("task_list_scope_slug_uidx")) {
|
|
27981
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
27982
|
+
}
|
|
27983
|
+
if (entry.type === "task_lists") {
|
|
27984
|
+
const scope = typeof payload["project_id"] === "string" ? payload["project_id"] : "";
|
|
27985
|
+
const conflict = await this.client.query(`/* todos:classify-sync-task-list-conflict */ SELECT EXISTS (
|
|
27986
|
+
SELECT 1 FROM ${this.tableName}
|
|
27987
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id <> $2
|
|
27988
|
+
AND deleted_at IS NULL AND COALESCE(payload->>'project_id','') = $3 AND payload->>'slug' = $4
|
|
27989
|
+
) AS conflict`, [this.service, entry.id, scope, taskListSlug]);
|
|
27990
|
+
if (conflict.rows[0]?.conflict) {
|
|
27991
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${taskListSlug}" already exists in this scope`);
|
|
27992
|
+
}
|
|
27993
|
+
} else if (entry.type === "projects") {
|
|
27994
|
+
const conflict = await this.client.query(`/* todos:classify-sync-project-conflict */ SELECT EXISTS (
|
|
27995
|
+
SELECT 1 FROM ${this.tableName}
|
|
27996
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
|
|
27997
|
+
AND deleted_at IS NULL AND payload->>'task_list_id' = $3
|
|
27998
|
+
) AS conflict`, [this.service, entry.id, projectSlug]);
|
|
27999
|
+
if (conflict.rows[0]?.conflict) {
|
|
28000
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${projectSlug}" already exists`);
|
|
28001
|
+
}
|
|
28002
|
+
}
|
|
28003
|
+
throw error;
|
|
27884
28004
|
}
|
|
27885
28005
|
async pullSnapshot(options = {}) {
|
|
27886
28006
|
const params = [this.service];
|
|
@@ -30592,27 +30712,9 @@ async function retryOnTransientPostgresError(fn, attempts = 2, delayMs = 150) {
|
|
|
30592
30712
|
}
|
|
30593
30713
|
throw lastError;
|
|
30594
30714
|
}
|
|
30595
|
-
function isPostgresUniqueViolation(error) {
|
|
30596
|
-
if (typeof error !== "object" || error === null)
|
|
30597
|
-
return false;
|
|
30598
|
-
const candidate = error;
|
|
30599
|
-
const states = [candidate.code, candidate.errno, candidate.sqlState, candidate.sqlstate];
|
|
30600
|
-
if (typeof candidate.cause === "object" && candidate.cause !== null) {
|
|
30601
|
-
const cause = candidate.cause;
|
|
30602
|
-
states.push(cause.code, cause.errno, cause.sqlState, cause.sqlstate);
|
|
30603
|
-
}
|
|
30604
|
-
return states.some((state) => String(state) === "23505");
|
|
30605
|
-
}
|
|
30606
|
-
function postgresConstraintName(error) {
|
|
30607
|
-
if (typeof error !== "object" || error === null)
|
|
30608
|
-
return "";
|
|
30609
|
-
const candidate = error;
|
|
30610
|
-
const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : undefined;
|
|
30611
|
-
const constraint = candidate.constraint ?? candidate.constraint_name ?? cause?.constraint ?? cause?.constraint_name;
|
|
30612
|
-
return typeof constraint === "string" ? constraint : "";
|
|
30613
|
-
}
|
|
30614
30715
|
|
|
30615
30716
|
// src/storage/shadow.ts
|
|
30717
|
+
init_types();
|
|
30616
30718
|
var SNAPSHOT_TO_OBJECT_TYPE = {
|
|
30617
30719
|
tasks: "tasks",
|
|
30618
30720
|
projects: "projects",
|
|
@@ -30754,6 +30856,12 @@ class TodosShadowMirror {
|
|
|
30754
30856
|
const message = error instanceof Error ? error.message : String(error);
|
|
30755
30857
|
const objectType = op.kind === "upsert" ? SNAPSHOT_TO_OBJECT_TYPE[op.key] : op.objectType;
|
|
30756
30858
|
const id = op.kind === "upsert" ? String(op.record["id"]) : op.id;
|
|
30859
|
+
if (error instanceof ResourceConflictError) {
|
|
30860
|
+
this.metrics.failed += 1;
|
|
30861
|
+
this.metrics.lastError = message;
|
|
30862
|
+
this.onEvent?.({ type: "dropped", objectType, id, error: message });
|
|
30863
|
+
return;
|
|
30864
|
+
}
|
|
30757
30865
|
this.metrics.retries += 1;
|
|
30758
30866
|
this.metrics.lastError = message;
|
|
30759
30867
|
op.attempts += 1;
|
|
@@ -31104,6 +31212,7 @@ function assertPostgresAdapterCapabilities(adapter) {
|
|
|
31104
31212
|
}
|
|
31105
31213
|
}
|
|
31106
31214
|
// src/storage/shadow-outbox.ts
|
|
31215
|
+
init_types();
|
|
31107
31216
|
init_shadow_outbox_schema();
|
|
31108
31217
|
init_shadow_outbox_schema();
|
|
31109
31218
|
var MAX_BACKOFF_MS = 5 * 60000;
|
|
@@ -31233,6 +31342,12 @@ class TodosShadowOutbox {
|
|
|
31233
31342
|
this.onEvent?.({ type: "mirrored", objectType: row.object_type, id: row.object_id, lagMs });
|
|
31234
31343
|
} catch (error) {
|
|
31235
31344
|
const message = error instanceof Error ? error.message : String(error);
|
|
31345
|
+
if (error instanceof ResourceConflictError) {
|
|
31346
|
+
this.metrics.lastError = message;
|
|
31347
|
+
this.db.run(`UPDATE shadow_outbox SET attempts=?, last_error=?, status='failed' WHERE seq=? AND revision=?`, [row.attempts + 1, message, row.seq, row.revision]);
|
|
31348
|
+
this.onEvent?.({ type: "parked", objectType: row.object_type, id: row.object_id, error: message });
|
|
31349
|
+
return;
|
|
31350
|
+
}
|
|
31236
31351
|
this.metrics.retries += 1;
|
|
31237
31352
|
this.metrics.lastError = message;
|
|
31238
31353
|
const attempts = row.attempts + 1;
|
|
@@ -68238,6 +68353,7 @@ export {
|
|
|
68238
68353
|
queryTasksByWorkflowState,
|
|
68239
68354
|
queryTasksByLocalFields,
|
|
68240
68355
|
purgeArtifact,
|
|
68356
|
+
projectTasksForDedupe,
|
|
68241
68357
|
processDueReminders,
|
|
68242
68358
|
previewUserScaffold,
|
|
68243
68359
|
previewTemplate,
|
|
@@ -69206,6 +69322,7 @@ export {
|
|
|
69206
69322
|
DEFAULT_LEASE_MINUTES,
|
|
69207
69323
|
DEFAULT_ENCRYPTION_PROFILE,
|
|
69208
69324
|
DEFAULT_ENCRYPTION_KEY_ENV,
|
|
69325
|
+
DEDUPE_SOURCE_KEY_ALLOWLIST,
|
|
69209
69326
|
DECISION_STATUSES,
|
|
69210
69327
|
DECISION_RECORD_SCHEMA,
|
|
69211
69328
|
DB_BACKUP_SCHEMA,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Task, TaskPriority, TaskStatus } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The ONLY metadata keys the dedup projection carries. This is exactly the
|
|
4
|
+
* source-key set the fingerprint in `task-dedupe.ts` consumes
|
|
5
|
+
* (`sourceKeysFor`). Any other metadata — free-form `token`, `api_key`,
|
|
6
|
+
* `secret`, arbitrary composites — is never projected, so a credential value
|
|
7
|
+
* sitting in free-form metadata can never reach a dedup workflow's capture.
|
|
8
|
+
*
|
|
9
|
+
* Regression O15-00170 (incidents 713001/713022/713043-46/713119): workflows
|
|
10
|
+
* doing deduplication captured credential-bearing whole-task composites from
|
|
11
|
+
* `todos list --json` / `--format compact` / `--format csv` because no
|
|
12
|
+
* package-owned bounded projection existed. This allowlist is the bounded
|
|
13
|
+
* surface that abstraction provides.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEDUPE_SOURCE_KEY_ALLOWLIST: readonly ["github_url", "github_issue_url", "github_pr_url", "source_url", "url", "external_url", "issue_url", "github_owner", "github_repo", "github_number"];
|
|
16
|
+
/**
|
|
17
|
+
* A task reduced to exactly the fields the dedup fingerprint consumes, plus a
|
|
18
|
+
* bounded source-key metadata object. It deliberately excludes free-form
|
|
19
|
+
* `metadata`, `tags`, comments, run/plan fields, and every other composite
|
|
20
|
+
* field of `Task` — the surfaces where credential values leaked. The carried
|
|
21
|
+
* free-form text (`title`, `description`) is redacted through the same
|
|
22
|
+
* pattern redactor every other output surface uses, so a credential embedded
|
|
23
|
+
* in prose never survives into the projection.
|
|
24
|
+
*/
|
|
25
|
+
export interface DedupeTaskProjection {
|
|
26
|
+
id: string;
|
|
27
|
+
short_id: string | null;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string | null;
|
|
30
|
+
status: TaskStatus;
|
|
31
|
+
created_at: string;
|
|
32
|
+
updated_at: string;
|
|
33
|
+
project_id: string | null;
|
|
34
|
+
task_list_id: string | null;
|
|
35
|
+
assigned_to: string | null;
|
|
36
|
+
priority: TaskPriority;
|
|
37
|
+
metadata: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Project whole tasks into the bounded dedup shape.
|
|
41
|
+
*
|
|
42
|
+
* This is the package-owned "canonical-machine" abstraction: the only way a
|
|
43
|
+
* dedup workflow may capture task state for deduplication. It never emits a
|
|
44
|
+
* whole-task composite, so a credential that lives in free-form metadata,
|
|
45
|
+
* tags, or a nested composite cannot be captured with it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function projectTasksForDedupe(tasks: Task[]): DedupeTaskProjection[];
|
|
48
|
+
//# sourceMappingURL=dedupe-projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe-projection.d.ts","sourceRoot":"","sources":["../../src/lib/dedupe-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGxE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,gKAW9B,CAAC;AAEX;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,oBAAoB,EAAE,CAyB3E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../src/lib/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;
|
|
1
|
+
{"version":3,"file":"redaction.d.ts","sourceRoot":"","sources":["../../src/lib/redaction.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE9E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAqGD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUxD;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAe1C;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,EAAE,CAOjE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAK1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,kBAAkB,GAAG,kBAAkB,CAQtF"}
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
import type { Task } from "../types/index.js";
|
|
3
|
+
import { type DedupeTaskProjection } from "./dedupe-projection.js";
|
|
3
4
|
export interface FindDuplicateTasksOptions {
|
|
4
5
|
threshold?: number;
|
|
5
6
|
limit?: number;
|
|
6
7
|
include_archived?: boolean;
|
|
7
8
|
}
|
|
8
|
-
export interface TaskDuplicateProbe {
|
|
9
|
-
title: string;
|
|
10
|
-
description?: string | null;
|
|
11
|
-
metadata?: Record<string, unknown>;
|
|
12
|
-
}
|
|
13
9
|
export interface TaskDuplicateMatch {
|
|
14
|
-
task:
|
|
10
|
+
task: DedupeTaskProjection;
|
|
15
11
|
score: number;
|
|
16
12
|
reasons: string[];
|
|
17
13
|
}
|
|
18
14
|
export interface DuplicateTaskCandidate {
|
|
19
|
-
primary_task:
|
|
20
|
-
duplicate_task:
|
|
15
|
+
primary_task: DedupeTaskProjection;
|
|
16
|
+
duplicate_task: DedupeTaskProjection;
|
|
21
17
|
score: number;
|
|
22
18
|
reasons: string[];
|
|
23
19
|
}
|
|
@@ -55,7 +51,7 @@ export declare function findDuplicateTasks(options?: FindDuplicateTasksOptions,
|
|
|
55
51
|
* Score a prospective task against local tasks without inserting a temporary
|
|
56
52
|
* row. Producer previews use this to report likely duplicates before apply.
|
|
57
53
|
*/
|
|
58
|
-
export declare function findDuplicateTasksForInput(input:
|
|
54
|
+
export declare function findDuplicateTasksForInput(input: DedupeTaskProjection, options?: FindDuplicateTasksOptions, db?: Database): TaskDuplicateMatch[];
|
|
59
55
|
export declare function mergeDuplicateTask(input: MergeDuplicateTaskInput, db?: Database): TaskMergeResult;
|
|
60
56
|
export declare const findDuplicateCandidates: typeof findDuplicateTasks;
|
|
61
57
|
//# sourceMappingURL=task-dedupe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-dedupe.d.ts","sourceRoot":"","sources":["../../src/lib/task-dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"task-dedupe.d.ts","sourceRoot":"","sources":["../../src/lib/task-dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAK9C,OAAO,EAAyB,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE1F,MAAM,WAAW,yBAAyB;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,oBAAoB,CAAC;IACnC,cAAc,EAAE,oBAAoB,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,IAAI,CAAC;IACnB,kBAAkB,EAAE,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,oBAAoB,CAAC;CAC7B;AA2ND,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,sBAAsB,EAAE,CAwBnH;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,GAAE,yBAA8B,EACvC,EAAE,CAAC,EAAE,QAAQ,GACZ,kBAAkB,EAAE,CActB;AAqJD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,eAAe,CAiGjG;AAGD,eAAO,MAAM,uBAAuB,2BAAqB,CAAC"}
|