@hasna/todos 0.11.86 → 0.11.87
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/cloud-router.d.ts +33 -2
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts +2 -0
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +934 -412
- package/dist/contracts.js +1 -1
- package/dist/db/comments.d.ts.map +1 -1
- package/dist/index.js +167 -4
- package/dist/mcp/index.js +407 -38
- package/dist/registry.js +1 -1
- package/dist/release-provenance.json +3 -3
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +21 -0
- package/dist/sdk/v1.generated.d.ts +54 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +13 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +824 -383
- package/dist/server/openapi.d.ts +171 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +7 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/comment-redaction-backfill.d.ts +32 -0
- package/dist/storage/comment-redaction-backfill.d.ts.map +1 -0
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +16 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +6 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.d.ts +3 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +171 -4
- package/package.json +2 -1
- package/vendor/hasna-contracts-0.5.1.tgz +0 -0
package/dist/mcp/index.js
CHANGED
|
@@ -14085,7 +14085,7 @@ function getComment(id, db) {
|
|
|
14085
14085
|
}
|
|
14086
14086
|
function listComments(taskId, db) {
|
|
14087
14087
|
const d = db || getDatabase();
|
|
14088
|
-
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
|
|
14088
|
+
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at, rowid").all(taskId);
|
|
14089
14089
|
}
|
|
14090
14090
|
function updateComment(id, input, db) {
|
|
14091
14091
|
const d = db || getDatabase();
|
|
@@ -22792,14 +22792,18 @@ function unwrapTask(raw) {
|
|
|
22792
22792
|
}
|
|
22793
22793
|
function toListQuery(filter = {}) {
|
|
22794
22794
|
const query = {};
|
|
22795
|
-
if (
|
|
22796
|
-
query["status"] = filter.status;
|
|
22797
|
-
if (
|
|
22798
|
-
query["priority"] = filter.priority;
|
|
22795
|
+
if (filter.status)
|
|
22796
|
+
query["status"] = Array.isArray(filter.status) ? filter.status.join(",") : filter.status;
|
|
22797
|
+
if (filter.priority)
|
|
22798
|
+
query["priority"] = Array.isArray(filter.priority) ? filter.priority.join(",") : filter.priority;
|
|
22799
22799
|
if (filter.project_id)
|
|
22800
22800
|
query["project_id"] = filter.project_id;
|
|
22801
|
+
if (filter.parent_id !== undefined)
|
|
22802
|
+
query["parent_id"] = filter.parent_id ?? "";
|
|
22801
22803
|
if (filter.plan_id)
|
|
22802
22804
|
query["plan_id"] = filter.plan_id;
|
|
22805
|
+
if (filter.task_list_id)
|
|
22806
|
+
query["task_list_id"] = filter.task_list_id;
|
|
22803
22807
|
if (filter.assigned_to)
|
|
22804
22808
|
query["assigned_to"] = filter.assigned_to;
|
|
22805
22809
|
if (filter.agent_id)
|
|
@@ -22849,10 +22853,19 @@ async function cloudListProjects(client) {
|
|
|
22849
22853
|
}
|
|
22850
22854
|
async function cloudAddComment(client, taskId, input) {
|
|
22851
22855
|
const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
|
|
22852
|
-
|
|
22853
|
-
|
|
22854
|
-
|
|
22855
|
-
return
|
|
22856
|
+
const comment = raw && typeof raw === "object" && "comment" in raw ? raw.comment : raw;
|
|
22857
|
+
if (!isTaskComment(comment))
|
|
22858
|
+
throw new Error("Invalid cloud comment response");
|
|
22859
|
+
return redactComment(comment);
|
|
22860
|
+
}
|
|
22861
|
+
function isTaskComment(value) {
|
|
22862
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
22863
|
+
return false;
|
|
22864
|
+
const comment = value;
|
|
22865
|
+
return typeof comment["id"] === "string" && typeof comment["task_id"] === "string" && (comment["agent_id"] === null || typeof comment["agent_id"] === "string") && (comment["session_id"] === null || typeof comment["session_id"] === "string") && typeof comment["content"] === "string" && (comment["type"] === "comment" || comment["type"] === "progress" || comment["type"] === "note") && (comment["progress_pct"] === null || typeof comment["progress_pct"] === "number") && typeof comment["created_at"] === "string";
|
|
22866
|
+
}
|
|
22867
|
+
function redactComment(comment) {
|
|
22868
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
22856
22869
|
}
|
|
22857
22870
|
async function cloudCountTasks(client, filter = {}) {
|
|
22858
22871
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
@@ -22885,6 +22898,7 @@ async function cloudReleaseAgent(client, idOrName, sessionId) {
|
|
|
22885
22898
|
var _cache;
|
|
22886
22899
|
var init_cloud_router = __esm(() => {
|
|
22887
22900
|
init_storage();
|
|
22901
|
+
init_redaction();
|
|
22888
22902
|
});
|
|
22889
22903
|
|
|
22890
22904
|
// src/mcp/tools/task-crud.ts
|
|
@@ -46148,6 +46162,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
46148
46162
|
list: (filter = {}) => listTasks(filter, database()),
|
|
46149
46163
|
count: (filter = {}) => countTasks(filter, database()),
|
|
46150
46164
|
update: (id, input) => updateTask(id, input, database()),
|
|
46165
|
+
unlock: (id, agentId) => {
|
|
46166
|
+
unlockTask(id, agentId, database());
|
|
46167
|
+
return true;
|
|
46168
|
+
},
|
|
46151
46169
|
delete: (id) => deleteTask(id, database()),
|
|
46152
46170
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
46153
46171
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
@@ -46199,6 +46217,20 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
46199
46217
|
logTaskChange: (taskId, action, field, oldValue, newValue, agentId) => logTaskChange(taskId, action, field, oldValue, newValue, agentId, database()),
|
|
46200
46218
|
addComment: (input) => addComment(input, database()),
|
|
46201
46219
|
getComments: (taskId) => listComments(taskId, database()),
|
|
46220
|
+
getCommentsPage: (taskId, options2) => {
|
|
46221
|
+
if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
|
|
46222
|
+
throw new Error("Comment limit must be an integer between 1 and 1001");
|
|
46223
|
+
}
|
|
46224
|
+
let comments = listComments(taskId, database());
|
|
46225
|
+
comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
|
|
46226
|
+
if (options2?.before) {
|
|
46227
|
+
const before = options2.before;
|
|
46228
|
+
comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
|
|
46229
|
+
}
|
|
46230
|
+
if (options2?.limit !== undefined)
|
|
46231
|
+
comments = comments.slice(-options2.limit);
|
|
46232
|
+
return comments;
|
|
46233
|
+
},
|
|
46202
46234
|
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
46203
46235
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
46204
46236
|
},
|
|
@@ -46256,6 +46288,12 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
|
|
|
46256
46288
|
)`
|
|
46257
46289
|
];
|
|
46258
46290
|
}
|
|
46291
|
+
function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
|
|
46292
|
+
assertSafeIdentifier(tableName);
|
|
46293
|
+
return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_comment_task_created_idx
|
|
46294
|
+
ON ${tableName} (service, (payload->>'task_id'), (payload->>'created_at'), object_id)
|
|
46295
|
+
WHERE object_type = 'comments' AND deleted_at IS NULL`;
|
|
46296
|
+
}
|
|
46259
46297
|
|
|
46260
46298
|
class PostgresTodosSyncStore {
|
|
46261
46299
|
client;
|
|
@@ -48240,7 +48278,24 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
48240
48278
|
audit: {
|
|
48241
48279
|
logTaskChange: (taskId, action, field, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context),
|
|
48242
48280
|
addComment: (input, context) => addComment2(input, store, context),
|
|
48243
|
-
getComments: async (taskId) =>
|
|
48281
|
+
getComments: async (taskId) => {
|
|
48282
|
+
const pages = [];
|
|
48283
|
+
let before;
|
|
48284
|
+
while (true) {
|
|
48285
|
+
const page = await store.listComments(taskId, { limit: 1000, ...before ? { before } : {} });
|
|
48286
|
+
if (page.length === 0)
|
|
48287
|
+
break;
|
|
48288
|
+
pages.unshift(page);
|
|
48289
|
+
if (page.length < 1000)
|
|
48290
|
+
break;
|
|
48291
|
+
const oldest = page[0];
|
|
48292
|
+
before = { created_at: oldest.created_at, id: oldest.id };
|
|
48293
|
+
}
|
|
48294
|
+
return pages.flat().map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
48295
|
+
},
|
|
48296
|
+
getCommentsPage: async (taskId, options2) => {
|
|
48297
|
+
return (await store.listComments(taskId, options2)).map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
48298
|
+
},
|
|
48244
48299
|
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
|
|
48245
48300
|
getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
|
|
48246
48301
|
},
|
|
@@ -48290,6 +48345,27 @@ class PostgresJsonRecordStore {
|
|
|
48290
48345
|
async list(type) {
|
|
48291
48346
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
48292
48347
|
}
|
|
48348
|
+
async listComments(taskId, options = {}) {
|
|
48349
|
+
await this.ensureSchema();
|
|
48350
|
+
const limit = options.limit ?? 100;
|
|
48351
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
|
|
48352
|
+
throw new Error("Postgres comment limit must be an integer between 1 and 1001");
|
|
48353
|
+
}
|
|
48354
|
+
const params = [this.service, taskId];
|
|
48355
|
+
let cursorPredicate = "";
|
|
48356
|
+
if (options.before) {
|
|
48357
|
+
params.push(options.before.created_at, options.before.id);
|
|
48358
|
+
cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
|
|
48359
|
+
}
|
|
48360
|
+
params.push(limit);
|
|
48361
|
+
const result = await this.options.client.query(`/* todos:list-comments */ SELECT payload FROM ${this.tableName}
|
|
48362
|
+
WHERE service = $1 AND object_type = 'comments' AND deleted_at IS NULL
|
|
48363
|
+
AND payload->>'task_id' = $2
|
|
48364
|
+
${cursorPredicate}
|
|
48365
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
48366
|
+
LIMIT $${params.length}`, params);
|
|
48367
|
+
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
48368
|
+
}
|
|
48293
48369
|
async listRecords(type) {
|
|
48294
48370
|
await this.ensureSchema();
|
|
48295
48371
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
@@ -48683,7 +48759,7 @@ async function lockTask2(id, agentId, store) {
|
|
|
48683
48759
|
async function unlockTask2(id, agentId, store) {
|
|
48684
48760
|
const task2 = await requireRecord("tasks", id, store);
|
|
48685
48761
|
if (agentId && task2.locked_by && task2.locked_by !== agentId) {
|
|
48686
|
-
throw new
|
|
48762
|
+
throw new LockError(id, task2.locked_by);
|
|
48687
48763
|
}
|
|
48688
48764
|
await patchTask(task2, { locked_by: null, locked_at: null }, store);
|
|
48689
48765
|
return true;
|
|
@@ -49048,13 +49124,16 @@ async function addComment2(input, store, context) {
|
|
|
49048
49124
|
task_id: input.task_id,
|
|
49049
49125
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
49050
49126
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
49051
|
-
content: input.content,
|
|
49127
|
+
content: redactEvidenceText(input.content),
|
|
49052
49128
|
type: input.type ?? "comment",
|
|
49053
49129
|
progress_pct: input.progress_pct ?? null,
|
|
49054
49130
|
created_at: new Date().toISOString()
|
|
49055
49131
|
};
|
|
49056
49132
|
return store.upsert("comments", comment, context);
|
|
49057
49133
|
}
|
|
49134
|
+
function redactComment2(comment) {
|
|
49135
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
49136
|
+
}
|
|
49058
49137
|
async function exportSnapshot(store) {
|
|
49059
49138
|
return {
|
|
49060
49139
|
exportedAt: new Date().toISOString(),
|
|
@@ -49200,7 +49279,106 @@ function numberValue3(value) {
|
|
|
49200
49279
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
49201
49280
|
}
|
|
49202
49281
|
var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_BY = "ORDER BY 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";
|
|
49203
|
-
var init_postgres_adapter = () => {
|
|
49282
|
+
var init_postgres_adapter = __esm(() => {
|
|
49283
|
+
init_types();
|
|
49284
|
+
init_redaction();
|
|
49285
|
+
});
|
|
49286
|
+
|
|
49287
|
+
// src/storage/comment-redaction-backfill.ts
|
|
49288
|
+
function assertSafeIdentifier2(value) {
|
|
49289
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
49290
|
+
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
49291
|
+
}
|
|
49292
|
+
}
|
|
49293
|
+
function payloadObject(value) {
|
|
49294
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
49295
|
+
return value;
|
|
49296
|
+
if (typeof value !== "string")
|
|
49297
|
+
return null;
|
|
49298
|
+
try {
|
|
49299
|
+
const parsed = JSON.parse(value);
|
|
49300
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
49301
|
+
} catch {
|
|
49302
|
+
return null;
|
|
49303
|
+
}
|
|
49304
|
+
}
|
|
49305
|
+
async function backfillPostgresCommentRedaction(client, options = {}) {
|
|
49306
|
+
const apply = options.apply === true;
|
|
49307
|
+
if (apply && options.confirmation !== COMMENT_REDACTION_BACKFILL_CONFIRMATION) {
|
|
49308
|
+
throw new Error(`Applying the comment redaction backfill requires confirmation ${COMMENT_REDACTION_BACKFILL_CONFIRMATION}`);
|
|
49309
|
+
}
|
|
49310
|
+
const tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
|
|
49311
|
+
assertSafeIdentifier2(tableName);
|
|
49312
|
+
const service = options.service ?? "todos";
|
|
49313
|
+
const batchSize = options.batchSize ?? 100;
|
|
49314
|
+
if (!Number.isSafeInteger(batchSize) || batchSize < 1 || batchSize > 500) {
|
|
49315
|
+
throw new Error("Comment redaction backfill batchSize must be an integer between 1 and 500");
|
|
49316
|
+
}
|
|
49317
|
+
const result = {
|
|
49318
|
+
dry_run: !apply,
|
|
49319
|
+
scanned: 0,
|
|
49320
|
+
candidates: 0,
|
|
49321
|
+
updated: 0,
|
|
49322
|
+
conflicts: 0,
|
|
49323
|
+
batches: 0,
|
|
49324
|
+
remaining_candidates: 0
|
|
49325
|
+
};
|
|
49326
|
+
let afterId = "";
|
|
49327
|
+
while (true) {
|
|
49328
|
+
const page = await client.query(`/* todos:comment-redaction-backfill-scan */
|
|
49329
|
+
SELECT object_id, payload
|
|
49330
|
+
FROM ${tableName}
|
|
49331
|
+
WHERE service = $1 AND object_type = 'comments'
|
|
49332
|
+
AND object_id > $2
|
|
49333
|
+
ORDER BY object_id ASC
|
|
49334
|
+
LIMIT $3`, [service, afterId, batchSize]);
|
|
49335
|
+
if (page.rows.length === 0)
|
|
49336
|
+
break;
|
|
49337
|
+
result.batches += 1;
|
|
49338
|
+
for (const row of page.rows) {
|
|
49339
|
+
afterId = row.object_id;
|
|
49340
|
+
result.scanned += 1;
|
|
49341
|
+
const payload = payloadObject(row.payload);
|
|
49342
|
+
const original = payload?.["content"];
|
|
49343
|
+
if (typeof original !== "string")
|
|
49344
|
+
continue;
|
|
49345
|
+
const redacted = redactEvidenceText(original);
|
|
49346
|
+
if (redacted === original)
|
|
49347
|
+
continue;
|
|
49348
|
+
result.candidates += 1;
|
|
49349
|
+
if (!apply)
|
|
49350
|
+
continue;
|
|
49351
|
+
const nextPayload = { ...payload, content: redacted };
|
|
49352
|
+
const update = await client.query(`/* todos:comment-redaction-backfill-apply */
|
|
49353
|
+
UPDATE ${tableName}
|
|
49354
|
+
SET payload = $3::jsonb
|
|
49355
|
+
WHERE service = $1 AND object_type = 'comments' AND object_id = $2
|
|
49356
|
+
AND payload = $4::jsonb
|
|
49357
|
+
RETURNING object_id`, [service, row.object_id, nextPayload, row.payload]);
|
|
49358
|
+
if (update.rows.length === 1)
|
|
49359
|
+
result.updated += 1;
|
|
49360
|
+
else
|
|
49361
|
+
result.conflicts += 1;
|
|
49362
|
+
}
|
|
49363
|
+
if (page.rows.length < batchSize)
|
|
49364
|
+
break;
|
|
49365
|
+
}
|
|
49366
|
+
if (!apply) {
|
|
49367
|
+
result.remaining_candidates = result.candidates;
|
|
49368
|
+
return result;
|
|
49369
|
+
}
|
|
49370
|
+
const verification = await backfillPostgresCommentRedaction(client, {
|
|
49371
|
+
...options,
|
|
49372
|
+
apply: false,
|
|
49373
|
+
confirmation: undefined
|
|
49374
|
+
});
|
|
49375
|
+
result.remaining_candidates = verification.candidates;
|
|
49376
|
+
return result;
|
|
49377
|
+
}
|
|
49378
|
+
var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
|
|
49379
|
+
var init_comment_redaction_backfill = __esm(() => {
|
|
49380
|
+
init_redaction();
|
|
49381
|
+
});
|
|
49204
49382
|
|
|
49205
49383
|
// src/server/cloud.ts
|
|
49206
49384
|
var exports_cloud = {};
|
|
@@ -49214,7 +49392,9 @@ __export(exports_cloud, {
|
|
|
49214
49392
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
49215
49393
|
getApiKeyStore: () => getApiKeyStore,
|
|
49216
49394
|
ensureCloudSchema: () => ensureCloudSchema,
|
|
49395
|
+
ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
|
|
49217
49396
|
closeCloud: () => closeCloud,
|
|
49397
|
+
backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
|
|
49218
49398
|
TODOS_APP_SLUG: () => TODOS_APP_SLUG
|
|
49219
49399
|
});
|
|
49220
49400
|
function resolveCloudDatabaseUrl(env = process.env) {
|
|
@@ -49292,6 +49472,9 @@ async function ensureCloudSchema() {
|
|
|
49292
49472
|
})();
|
|
49293
49473
|
return schemaEnsured;
|
|
49294
49474
|
}
|
|
49475
|
+
async function ensureCloudCommentCursorIndex() {
|
|
49476
|
+
await getClient().query(postgresTodosCommentCursorIndexSql());
|
|
49477
|
+
}
|
|
49295
49478
|
async function normalizeCloudPayloads() {
|
|
49296
49479
|
const client = getClient();
|
|
49297
49480
|
const res = await client.query(`UPDATE todos_sync_records
|
|
@@ -49300,6 +49483,9 @@ async function normalizeCloudPayloads() {
|
|
|
49300
49483
|
RETURNING object_id AS id`);
|
|
49301
49484
|
return res.rows.length;
|
|
49302
49485
|
}
|
|
49486
|
+
function backfillCloudCommentRedaction(options = {}) {
|
|
49487
|
+
return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
|
|
49488
|
+
}
|
|
49303
49489
|
async function pingCloud() {
|
|
49304
49490
|
const client = getClient();
|
|
49305
49491
|
const res = await client.query("select 1 as ok");
|
|
@@ -49320,6 +49506,7 @@ var init_cloud = __esm(() => {
|
|
|
49320
49506
|
init_auth();
|
|
49321
49507
|
init_cloud_client();
|
|
49322
49508
|
init_postgres_adapter();
|
|
49509
|
+
init_comment_redaction_backfill();
|
|
49323
49510
|
});
|
|
49324
49511
|
|
|
49325
49512
|
// src/server/openapi.ts
|
|
@@ -49343,6 +49530,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49343
49530
|
schemas: {
|
|
49344
49531
|
Task: taskSchema,
|
|
49345
49532
|
Project: projectSchema,
|
|
49533
|
+
TaskComment: taskCommentSchema,
|
|
49346
49534
|
CreateTaskInput: {
|
|
49347
49535
|
type: "object",
|
|
49348
49536
|
required: ["title"],
|
|
@@ -49377,6 +49565,17 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49377
49565
|
description: { type: "string" },
|
|
49378
49566
|
task_prefix: { type: "string" }
|
|
49379
49567
|
}
|
|
49568
|
+
},
|
|
49569
|
+
CreateTaskCommentInput: {
|
|
49570
|
+
type: "object",
|
|
49571
|
+
required: ["content"],
|
|
49572
|
+
properties: {
|
|
49573
|
+
content: { type: "string", minLength: 1 },
|
|
49574
|
+
agent_id: { type: "string" },
|
|
49575
|
+
session_id: { type: "string" },
|
|
49576
|
+
type: { type: "string", enum: ["comment", "progress", "note"] },
|
|
49577
|
+
progress_pct: { type: "number" }
|
|
49578
|
+
}
|
|
49380
49579
|
}
|
|
49381
49580
|
}
|
|
49382
49581
|
},
|
|
@@ -49475,6 +49674,61 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49475
49674
|
}
|
|
49476
49675
|
}
|
|
49477
49676
|
},
|
|
49677
|
+
"/v1/tasks/{id}/comments": {
|
|
49678
|
+
get: {
|
|
49679
|
+
operationId: "listTaskComments",
|
|
49680
|
+
summary: "List a bounded page of task comments",
|
|
49681
|
+
description: "Returns the newest page in oldest-to-newest display order. Use next_cursor to request older pages; count is the page size, not a total. Pagination-aware clients must send limit during the mixed-version rollout.",
|
|
49682
|
+
parameters: [
|
|
49683
|
+
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
|
49684
|
+
{ name: "limit", in: "query", required: true, schema: { type: "integer", minimum: 1, maximum: 500, default: 100 } },
|
|
49685
|
+
{ name: "cursor", in: "query", schema: { type: "string" } }
|
|
49686
|
+
],
|
|
49687
|
+
responses: {
|
|
49688
|
+
"200": {
|
|
49689
|
+
content: {
|
|
49690
|
+
"application/json": {
|
|
49691
|
+
schema: {
|
|
49692
|
+
type: "object",
|
|
49693
|
+
required: ["comments", "count", "has_more", "next_cursor"],
|
|
49694
|
+
properties: {
|
|
49695
|
+
comments: { type: "array", maxItems: 500, items: { $ref: "#/components/schemas/TaskComment" } },
|
|
49696
|
+
count: { type: "integer", minimum: 0, maximum: 500 },
|
|
49697
|
+
has_more: { type: "boolean" },
|
|
49698
|
+
next_cursor: { type: "string", nullable: true }
|
|
49699
|
+
}
|
|
49700
|
+
}
|
|
49701
|
+
}
|
|
49702
|
+
}
|
|
49703
|
+
},
|
|
49704
|
+
"426": {
|
|
49705
|
+
description: "Upgrade required: a predecessor client omitted limit and the complete legacy history exceeds 500 comments, or the configured storage adapter lacks cursor pagination support."
|
|
49706
|
+
}
|
|
49707
|
+
}
|
|
49708
|
+
},
|
|
49709
|
+
post: {
|
|
49710
|
+
operationId: "createTaskComment",
|
|
49711
|
+
summary: "Create a task comment",
|
|
49712
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
49713
|
+
requestBody: {
|
|
49714
|
+
required: true,
|
|
49715
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTaskCommentInput" } } }
|
|
49716
|
+
},
|
|
49717
|
+
responses: {
|
|
49718
|
+
"201": {
|
|
49719
|
+
content: {
|
|
49720
|
+
"application/json": {
|
|
49721
|
+
schema: {
|
|
49722
|
+
type: "object",
|
|
49723
|
+
required: ["comment"],
|
|
49724
|
+
properties: { comment: { $ref: "#/components/schemas/TaskComment" } }
|
|
49725
|
+
}
|
|
49726
|
+
}
|
|
49727
|
+
}
|
|
49728
|
+
}
|
|
49729
|
+
}
|
|
49730
|
+
}
|
|
49731
|
+
},
|
|
49478
49732
|
"/v1/tasks/{id}/start": {
|
|
49479
49733
|
post: {
|
|
49480
49734
|
operationId: "startTask",
|
|
@@ -49593,7 +49847,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49593
49847
|
}
|
|
49594
49848
|
};
|
|
49595
49849
|
}
|
|
49596
|
-
var taskSchema, projectSchema;
|
|
49850
|
+
var taskSchema, projectSchema, taskCommentSchema;
|
|
49597
49851
|
var init_openapi = __esm(() => {
|
|
49598
49852
|
init_package_version();
|
|
49599
49853
|
taskSchema = {
|
|
@@ -49624,6 +49878,20 @@ var init_openapi = __esm(() => {
|
|
|
49624
49878
|
updated_at: { type: "string" }
|
|
49625
49879
|
}
|
|
49626
49880
|
};
|
|
49881
|
+
taskCommentSchema = {
|
|
49882
|
+
type: "object",
|
|
49883
|
+
required: ["id", "task_id", "agent_id", "session_id", "content", "type", "progress_pct", "created_at"],
|
|
49884
|
+
properties: {
|
|
49885
|
+
id: { type: "string" },
|
|
49886
|
+
task_id: { type: "string" },
|
|
49887
|
+
agent_id: { type: "string", nullable: true },
|
|
49888
|
+
session_id: { type: "string", nullable: true },
|
|
49889
|
+
content: { type: "string" },
|
|
49890
|
+
type: { type: "string", enum: ["comment", "progress", "note"] },
|
|
49891
|
+
progress_pct: { type: "number", nullable: true },
|
|
49892
|
+
created_at: { type: "string", format: "date-time" }
|
|
49893
|
+
}
|
|
49894
|
+
};
|
|
49627
49895
|
});
|
|
49628
49896
|
|
|
49629
49897
|
// src/server/v1.ts
|
|
@@ -49653,6 +49921,29 @@ function contextFromPrincipal(principal, body) {
|
|
|
49653
49921
|
const agentId = body?.agent_id || principal.agent || undefined;
|
|
49654
49922
|
return agentId ? { agentId } : {};
|
|
49655
49923
|
}
|
|
49924
|
+
function redactComment3(comment) {
|
|
49925
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
49926
|
+
}
|
|
49927
|
+
function encodeCommentCursor(comment) {
|
|
49928
|
+
return Buffer.from(JSON.stringify({ created_at: comment.created_at, id: comment.id }), "utf8").toString("base64url");
|
|
49929
|
+
}
|
|
49930
|
+
function decodeCommentCursor(value) {
|
|
49931
|
+
if (value.length > 1024)
|
|
49932
|
+
throw new Error("invalid comment cursor");
|
|
49933
|
+
let parsed;
|
|
49934
|
+
try {
|
|
49935
|
+
parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
49936
|
+
} catch {
|
|
49937
|
+
throw new Error("invalid comment cursor");
|
|
49938
|
+
}
|
|
49939
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
49940
|
+
throw new Error("invalid comment cursor");
|
|
49941
|
+
const cursor = parsed;
|
|
49942
|
+
if (typeof cursor["created_at"] !== "string" || cursor["created_at"].length > 64 || !Number.isFinite(Date.parse(cursor["created_at"])) || typeof cursor["id"] !== "string" || !cursor["id"] || cursor["id"].length > 256) {
|
|
49943
|
+
throw new Error("invalid comment cursor");
|
|
49944
|
+
}
|
|
49945
|
+
return { created_at: cursor["created_at"], id: cursor["id"] };
|
|
49946
|
+
}
|
|
49656
49947
|
function normalizeImportSnapshot(raw) {
|
|
49657
49948
|
const body = raw && typeof raw === "object" ? raw : {};
|
|
49658
49949
|
const arr = (v) => Array.isArray(v) ? v : [];
|
|
@@ -49673,7 +49964,7 @@ function normalizeImportSnapshot(raw) {
|
|
|
49673
49964
|
function countSnapshotRecords(s) {
|
|
49674
49965
|
return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
|
|
49675
49966
|
}
|
|
49676
|
-
async function handleV1Request(req, url) {
|
|
49967
|
+
async function handleV1Request(req, url, dependencies = {}) {
|
|
49677
49968
|
const path = url.pathname;
|
|
49678
49969
|
if (path !== "/v1" && !path.startsWith("/v1/"))
|
|
49679
49970
|
return null;
|
|
@@ -49682,7 +49973,7 @@ async function handleV1Request(req, url) {
|
|
|
49682
49973
|
const requiredScopes = [isWrite ? "todos:write" : "todos:read"];
|
|
49683
49974
|
let verifier;
|
|
49684
49975
|
try {
|
|
49685
|
-
verifier = getCloudVerifier();
|
|
49976
|
+
verifier = (dependencies.getVerifier ?? getCloudVerifier)();
|
|
49686
49977
|
} catch (e) {
|
|
49687
49978
|
return error(503, e.message);
|
|
49688
49979
|
}
|
|
@@ -49691,8 +49982,8 @@ async function handleV1Request(req, url) {
|
|
|
49691
49982
|
return error(decision.status, decision.message, { reason: decision.reason });
|
|
49692
49983
|
}
|
|
49693
49984
|
const principal = decision.principal;
|
|
49694
|
-
await ensureCloudSchema();
|
|
49695
|
-
const store = getCloudStorageAdapter();
|
|
49985
|
+
await (dependencies.ensureSchema ?? ensureCloudSchema)();
|
|
49986
|
+
const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
|
|
49696
49987
|
const segments = path.split("/").filter(Boolean);
|
|
49697
49988
|
const resource = segments[1];
|
|
49698
49989
|
const id = segments[2];
|
|
@@ -49778,10 +50069,16 @@ async function handleV1Request(req, url) {
|
|
|
49778
50069
|
if (!id) {
|
|
49779
50070
|
if (method === "GET") {
|
|
49780
50071
|
const filter = {
|
|
49781
|
-
...url.searchParams.get("status") ? {
|
|
49782
|
-
|
|
50072
|
+
...url.searchParams.get("status") ? {
|
|
50073
|
+
status: url.searchParams.get("status").includes(",") ? url.searchParams.get("status").split(",") : url.searchParams.get("status")
|
|
50074
|
+
} : {},
|
|
50075
|
+
...url.searchParams.get("priority") ? {
|
|
50076
|
+
priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
|
|
50077
|
+
} : {},
|
|
49783
50078
|
...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
|
|
50079
|
+
...url.searchParams.has("parent_id") ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : {},
|
|
49784
50080
|
...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
|
|
50081
|
+
...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
|
|
49785
50082
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
49786
50083
|
...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
|
|
49787
50084
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
@@ -49805,8 +50102,47 @@ async function handleV1Request(req, url) {
|
|
|
49805
50102
|
if (action) {
|
|
49806
50103
|
if (action === "comments") {
|
|
49807
50104
|
if (method === "GET") {
|
|
49808
|
-
|
|
49809
|
-
|
|
50105
|
+
if (!await store.tasks.get(id))
|
|
50106
|
+
return error(404, "task not found");
|
|
50107
|
+
const rawLimit = url.searchParams.get("limit");
|
|
50108
|
+
const cursor = url.searchParams.get("cursor");
|
|
50109
|
+
if (rawLimit === null && cursor === null) {
|
|
50110
|
+
const storageContext = contextFromPrincipal(principal);
|
|
50111
|
+
const legacyPage = (await (store.audit.getCommentsPage ? store.audit.getCommentsPage(id, { limit: LEGACY_COMMENT_RESPONSE_LIMIT + 1 }, storageContext) : store.audit.getComments(id, storageContext))).map(redactComment3);
|
|
50112
|
+
if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
|
|
50113
|
+
return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
|
|
50114
|
+
}
|
|
50115
|
+
return json2({
|
|
50116
|
+
comments: legacyPage,
|
|
50117
|
+
count: legacyPage.length,
|
|
50118
|
+
has_more: false,
|
|
50119
|
+
next_cursor: null
|
|
50120
|
+
});
|
|
50121
|
+
}
|
|
50122
|
+
const limit = rawLimit === null ? DEFAULT_COMMENT_PAGE_SIZE : Number(rawLimit);
|
|
50123
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_COMMENT_PAGE_SIZE) {
|
|
50124
|
+
return error(400, `limit must be an integer between 1 and ${MAX_COMMENT_PAGE_SIZE}`);
|
|
50125
|
+
}
|
|
50126
|
+
let before;
|
|
50127
|
+
if (cursor) {
|
|
50128
|
+
try {
|
|
50129
|
+
before = decodeCommentCursor(cursor);
|
|
50130
|
+
} catch {
|
|
50131
|
+
return error(400, "invalid comment cursor");
|
|
50132
|
+
}
|
|
50133
|
+
}
|
|
50134
|
+
if (!store.audit.getCommentsPage) {
|
|
50135
|
+
return error(426, "storage adapter must be upgraded to support cursor-paginated comments");
|
|
50136
|
+
}
|
|
50137
|
+
const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment3);
|
|
50138
|
+
const hasMore = page.length > limit;
|
|
50139
|
+
const comments = hasMore ? page.slice(1) : page;
|
|
50140
|
+
return json2({
|
|
50141
|
+
comments,
|
|
50142
|
+
count: comments.length,
|
|
50143
|
+
has_more: hasMore,
|
|
50144
|
+
next_cursor: hasMore && comments[0] ? encodeCommentCursor(comments[0]) : null
|
|
50145
|
+
});
|
|
49810
50146
|
}
|
|
49811
50147
|
if (method === "POST") {
|
|
49812
50148
|
const body2 = await readJson(req) ?? {};
|
|
@@ -49824,7 +50160,7 @@ async function handleV1Request(req, url) {
|
|
|
49824
50160
|
type: body2.type,
|
|
49825
50161
|
progress_pct: body2.progress_pct
|
|
49826
50162
|
}, contextFromPrincipal(principal, body2));
|
|
49827
|
-
return json2({ comment }, 201);
|
|
50163
|
+
return json2({ comment: redactComment3(comment) }, 201);
|
|
49828
50164
|
}
|
|
49829
50165
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
|
|
49830
50166
|
}
|
|
@@ -49839,17 +50175,30 @@ async function handleV1Request(req, url) {
|
|
|
49839
50175
|
if (action === "lock" || action === "unlock") {
|
|
49840
50176
|
if (method !== "POST")
|
|
49841
50177
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/${action}`);
|
|
49842
|
-
if (typeof store.tasks.lock !== "function" || typeof store.tasks.unlock !== "function") {
|
|
49843
|
-
return error(501, "task locking is not supported by this storage backend");
|
|
49844
|
-
}
|
|
49845
50178
|
const body2 = await readJson(req) ?? {};
|
|
49846
50179
|
if (!await store.tasks.get(id))
|
|
49847
50180
|
return error(404, "task not found");
|
|
49848
50181
|
if (action === "lock") {
|
|
49849
|
-
|
|
49850
|
-
|
|
50182
|
+
if (typeof store.tasks.lock !== "function")
|
|
50183
|
+
return error(501, "task locking is not supported by this storage backend");
|
|
50184
|
+
const agentId3 = body2.agent_id || principal.agent || "todos-serve";
|
|
50185
|
+
return json2({ result: await store.tasks.lock(id, agentId3) });
|
|
50186
|
+
}
|
|
50187
|
+
if (typeof store.tasks.unlock !== "function")
|
|
50188
|
+
return error(501, "task unlocking is not supported by this storage backend");
|
|
50189
|
+
if (body2.force === true) {
|
|
50190
|
+
if (!principal.scopes.includes("todos:*"))
|
|
50191
|
+
return error(403, "force unlock requires todos:* scope");
|
|
50192
|
+
const released2 = await store.tasks.unlock(id);
|
|
50193
|
+
return json2({ success: released2 });
|
|
50194
|
+
}
|
|
50195
|
+
if (body2.agent_id && principal.agent && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
|
|
50196
|
+
return error(403, "unlock agent_id must match the authenticated agent");
|
|
49851
50197
|
}
|
|
49852
|
-
const
|
|
50198
|
+
const agentId2 = principal.agent || body2.agent_id;
|
|
50199
|
+
if (!agentId2)
|
|
50200
|
+
return error(403, "unlock requires an agent-bound key or force=true");
|
|
50201
|
+
const released = await store.tasks.unlock(id, agentId2);
|
|
49853
50202
|
return json2({ success: released });
|
|
49854
50203
|
}
|
|
49855
50204
|
if (action === "dependencies") {
|
|
@@ -50132,12 +50481,28 @@ async function handleV1Request(req, url) {
|
|
|
50132
50481
|
const activity = await store.audit.getRecentActivity(limit);
|
|
50133
50482
|
return json2({ activity, count: activity.length });
|
|
50134
50483
|
}
|
|
50135
|
-
if (resource === "task-lists"
|
|
50136
|
-
if (method
|
|
50137
|
-
|
|
50138
|
-
|
|
50139
|
-
|
|
50140
|
-
|
|
50484
|
+
if (resource === "task-lists") {
|
|
50485
|
+
if (!id && method === "GET") {
|
|
50486
|
+
const projectId = url.searchParams.get("project_id") ?? undefined;
|
|
50487
|
+
const taskLists = await store.taskLists.list(projectId);
|
|
50488
|
+
return json2({ task_lists: taskLists, count: taskLists.length });
|
|
50489
|
+
}
|
|
50490
|
+
if (!id && method === "POST") {
|
|
50491
|
+
const body = await readJson(req);
|
|
50492
|
+
if (!body || typeof body.name !== "string" || !body.name.trim())
|
|
50493
|
+
return error(400, "name is required");
|
|
50494
|
+
const taskList = await store.taskLists.create(body, contextFromPrincipal(principal));
|
|
50495
|
+
return json2({ task_list: taskList }, 201);
|
|
50496
|
+
}
|
|
50497
|
+
if (id && method === "GET") {
|
|
50498
|
+
const taskList = await store.taskLists.get(id);
|
|
50499
|
+
return taskList ? json2({ task_list: taskList }) : error(404, "task list not found");
|
|
50500
|
+
}
|
|
50501
|
+
if (id && method === "DELETE") {
|
|
50502
|
+
const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
|
|
50503
|
+
return deleted ? json2({ deleted: true, id }) : error(404, "task list not found");
|
|
50504
|
+
}
|
|
50505
|
+
return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
|
|
50141
50506
|
}
|
|
50142
50507
|
if (resource === "dependencies" && !id) {
|
|
50143
50508
|
if (method !== "GET")
|
|
@@ -50145,8 +50510,8 @@ async function handleV1Request(req, url) {
|
|
|
50145
50510
|
if (typeof store.dependencies?.listAll !== "function") {
|
|
50146
50511
|
return error(501, "dependency edge listing is not supported by this storage backend");
|
|
50147
50512
|
}
|
|
50148
|
-
const
|
|
50149
|
-
return json2({ dependencies, count:
|
|
50513
|
+
const dependencies2 = await store.dependencies.listAll();
|
|
50514
|
+
return json2({ dependencies: dependencies2, count: dependencies2.length });
|
|
50150
50515
|
}
|
|
50151
50516
|
if (resource === "commits" && id) {
|
|
50152
50517
|
if (method !== "GET")
|
|
@@ -50203,12 +50568,16 @@ async function handleV1Request(req, url) {
|
|
|
50203
50568
|
}
|
|
50204
50569
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
50205
50570
|
} catch (e) {
|
|
50571
|
+
if (e instanceof LockError)
|
|
50572
|
+
return error(409, e.message, { code: LockError.code });
|
|
50206
50573
|
return error(500, e.message || "internal error");
|
|
50207
50574
|
}
|
|
50208
50575
|
}
|
|
50209
|
-
var JSON_HEADERS;
|
|
50576
|
+
var JSON_HEADERS, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500;
|
|
50210
50577
|
var init_v1 = __esm(() => {
|
|
50578
|
+
init_types();
|
|
50211
50579
|
init_cloud();
|
|
50580
|
+
init_redaction();
|
|
50212
50581
|
JSON_HEADERS = { "Content-Type": "application/json" };
|
|
50213
50582
|
});
|
|
50214
50583
|
|