@hasna/todos 0.16.0 → 0.17.0
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/CHANGELOG.md +109 -0
- package/README.md +59 -22
- package/dist/cli/cloud-router.d.ts +63 -21
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/template-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2591 -503
- package/dist/cli/stage-a.d.ts +2 -12
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/cli/template-remote.d.ts +90 -0
- package/dist/cli/template-remote.d.ts.map +1 -0
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +77 -30
- package/dist/db/agents.d.ts +8 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/database.d.ts +6 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/identity-mapping.d.ts +25 -0
- package/dist/db/identity-mapping.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.js +496 -58
- package/dist/lib/feature-manifest.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +14 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -1
- package/dist/lib/paths.d.ts +11 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +52 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +15162 -30695
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp/tools/templates.d.ts.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/project-registration.js +466 -35
- package/dist/registry.js +77 -30
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/client.d.ts +3 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.js +48 -12
- package/dist/sdk/resolve.d.ts +23 -13
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +17 -2
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +115 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +3972 -916
- package/dist/server/openapi.d.ts +837 -50
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/rate-limit-config.d.ts +6 -0
- package/dist/server/rate-limit-config.d.ts.map +1 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +74 -3
- 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.js +464 -33
- package/dist/task-manifest.js +8 -2
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/mcp/tools/code-tools.d.ts +0 -15
- package/dist/mcp/tools/code-tools.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -2123,7 +2123,7 @@ var package_default;
|
|
|
2123
2123
|
var init_package = __esm(() => {
|
|
2124
2124
|
package_default = {
|
|
2125
2125
|
name: "@hasna/todos",
|
|
2126
|
-
version: "0.
|
|
2126
|
+
version: "0.17.0",
|
|
2127
2127
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
2128
2128
|
type: "module",
|
|
2129
2129
|
main: "dist/index.js",
|
|
@@ -2236,7 +2236,7 @@ var init_package = __esm(() => {
|
|
|
2236
2236
|
url: "https://github.com/hasna/apps.git",
|
|
2237
2237
|
directory: "apps/todos"
|
|
2238
2238
|
},
|
|
2239
|
-
homepage: "https://github.com/hasna/apps",
|
|
2239
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
|
|
2240
2240
|
bugs: {
|
|
2241
2241
|
url: "https://github.com/hasna/apps/issues"
|
|
2242
2242
|
},
|
|
@@ -2566,7 +2566,7 @@ var init_types = __esm(() => {
|
|
|
2566
2566
|
// src/lib/paths.ts
|
|
2567
2567
|
import { existsSync } from "fs";
|
|
2568
2568
|
import { homedir } from "os";
|
|
2569
|
-
import { join, resolve } from "path";
|
|
2569
|
+
import { isAbsolute, join, resolve } from "path";
|
|
2570
2570
|
import { homedir as pathsResolverHomedir } from "os";
|
|
2571
2571
|
import { join as pathsResolverJoin } from "path";
|
|
2572
2572
|
function pathsResolverAssertApp(app) {
|
|
@@ -2623,8 +2623,14 @@ function dataDir(options) {
|
|
|
2623
2623
|
function effectiveHome(env = process.env) {
|
|
2624
2624
|
return env.HOME || env.USERPROFILE || homedir();
|
|
2625
2625
|
}
|
|
2626
|
+
function hasnaHomeRoot(env = process.env) {
|
|
2627
|
+
const override = env["HASNA_HOME"]?.trim();
|
|
2628
|
+
if (override && isAbsolute(override))
|
|
2629
|
+
return override;
|
|
2630
|
+
return join(effectiveHome(env), ".hasna");
|
|
2631
|
+
}
|
|
2626
2632
|
function legacyHomeDir(env = process.env) {
|
|
2627
|
-
return join(
|
|
2633
|
+
return join(hasnaHomeRoot(env), "todos");
|
|
2628
2634
|
}
|
|
2629
2635
|
function resolverHome(env = process.env) {
|
|
2630
2636
|
return dataDir({ app: "todos", home: effectiveHome(env), env });
|
|
@@ -5881,6 +5887,12 @@ import {
|
|
|
5881
5887
|
function isTodosLocalOptIn(env = process.env) {
|
|
5882
5888
|
return TODOS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
5883
5889
|
}
|
|
5890
|
+
function todosLocalModeNotice(reason = "local-opt-in") {
|
|
5891
|
+
if (reason === "local-only-command") {
|
|
5892
|
+
return "todos: LOCAL mode \u2014 this command only ever runs against the on-box SQLite store, so this run " + "does not reach the hosted fleet even though a Todos authority is configured.";
|
|
5893
|
+
}
|
|
5894
|
+
return "todos: LOCAL mode \u2014 using the on-box SQLite store, not the hosted fleet " + "(HASNA_TODOS_LOCAL is set). Unset it, and provide a credential via the Keychain item " + "hasna.credentials.todos.api-key, ~/.hasna/todos/config/credentials, or HASNA_TODOS_API_KEY, " + "to work against https://api.hasna.com/todos.";
|
|
5895
|
+
}
|
|
5884
5896
|
function todosAuthorityEnvKeys() {
|
|
5885
5897
|
const keys = clientTransportEnvKeys("todos");
|
|
5886
5898
|
return [
|
|
@@ -6036,6 +6048,10 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env, options = {}) {
|
|
|
6036
6048
|
};
|
|
6037
6049
|
}
|
|
6038
6050
|
function classifyRemoteRequestError(baseUrl, route, error) {
|
|
6051
|
+
const errorName = error instanceof Error ? error.name : "";
|
|
6052
|
+
if (errorName === "CredentialResolutionError" || errorName === "CredentialFileUnsafeError") {
|
|
6053
|
+
rethrowAuthorityFailure(error);
|
|
6054
|
+
}
|
|
6039
6055
|
const status = error && typeof error === "object" ? error.status : undefined;
|
|
6040
6056
|
if (status === 401) {
|
|
6041
6057
|
throw new Error(`REMOTE_API_UNAUTHORIZED: configured Todos authority ${baseUrl} rejected HASNA_TODOS_API_KEY for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
@@ -6359,6 +6375,10 @@ function toListQuery(filter = {}) {
|
|
|
6359
6375
|
query["parent_id"] = filter.parent_id ?? "";
|
|
6360
6376
|
if (filter.include_subtasks !== undefined)
|
|
6361
6377
|
query["include_subtasks"] = filter.include_subtasks ? "true" : "false";
|
|
6378
|
+
if (filter.include_archived !== undefined)
|
|
6379
|
+
query["include_archived"] = filter.include_archived ? "true" : "false";
|
|
6380
|
+
if (filter.archived_only !== undefined)
|
|
6381
|
+
query["archived_only"] = filter.archived_only ? "true" : "false";
|
|
6362
6382
|
if (filter.plan_id)
|
|
6363
6383
|
query["plan_id"] = filter.plan_id;
|
|
6364
6384
|
if (filter.task_list_id)
|
|
@@ -6559,6 +6579,143 @@ async function cloudListTasks(client, filter = {}) {
|
|
|
6559
6579
|
union.sort(compareCloudTaskOrder);
|
|
6560
6580
|
return union.slice(start, windowEnd);
|
|
6561
6581
|
}
|
|
6582
|
+
async function cloudListArchivedTasksPage(client, options = {}) {
|
|
6583
|
+
const limit = options.limit ?? 50;
|
|
6584
|
+
const offset = options.offset ?? 0;
|
|
6585
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
|
|
6586
|
+
throw new Error("archived task limit must be an integer from 1 to 200");
|
|
6587
|
+
}
|
|
6588
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > 1e4) {
|
|
6589
|
+
throw new Error("archived task offset must be an integer from 0 to 10000");
|
|
6590
|
+
}
|
|
6591
|
+
const page = await requestRawCloudTaskPage(client, {
|
|
6592
|
+
include_archived: true,
|
|
6593
|
+
archived_only: true,
|
|
6594
|
+
include_subtasks: true,
|
|
6595
|
+
...options.project_id ? { project_id: options.project_id } : {},
|
|
6596
|
+
limit,
|
|
6597
|
+
offset
|
|
6598
|
+
});
|
|
6599
|
+
if (page.total === undefined || page.tasks.length > limit || offset + page.tasks.length > page.total) {
|
|
6600
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: archived-only task page lacks a consistent authoritative total");
|
|
6601
|
+
}
|
|
6602
|
+
const seen = new Set;
|
|
6603
|
+
for (const task of page.tasks) {
|
|
6604
|
+
if (!task || typeof task.id !== "string" || !task.id || !task.archived_at || seen.has(task.id) || options.project_id !== undefined && task.project_id !== options.project_id) {
|
|
6605
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: archived-only task page returned a malformed, repeated, or out-of-scope row");
|
|
6606
|
+
}
|
|
6607
|
+
seen.add(task.id);
|
|
6608
|
+
}
|
|
6609
|
+
const hasMore = offset + page.tasks.length < page.total;
|
|
6610
|
+
if (hasMore && page.tasks.length === 0) {
|
|
6611
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: archived-only task pagination stalled before its authoritative total");
|
|
6612
|
+
}
|
|
6613
|
+
return {
|
|
6614
|
+
tasks: page.tasks,
|
|
6615
|
+
total: page.total,
|
|
6616
|
+
limit,
|
|
6617
|
+
offset,
|
|
6618
|
+
has_more: hasMore,
|
|
6619
|
+
next_offset: hasMore ? offset + page.tasks.length : null
|
|
6620
|
+
};
|
|
6621
|
+
}
|
|
6622
|
+
async function cloudListTasksCompleteBounded(client, filter, maxTasks = 1e4) {
|
|
6623
|
+
if (!Number.isSafeInteger(maxTasks) || maxTasks < 1 || maxTasks > 1e4) {
|
|
6624
|
+
throw new Error("maxTasks must be an integer from 1 to 10000");
|
|
6625
|
+
}
|
|
6626
|
+
if ((filter.offset ?? 0) !== 0)
|
|
6627
|
+
throw new Error("complete bounded task reads require offset zero");
|
|
6628
|
+
if (Array.isArray(filter.status)) {
|
|
6629
|
+
if (filter.status.length === 0) {
|
|
6630
|
+
const { status: _status2, ...unfiltered } = filter;
|
|
6631
|
+
return cloudListTasksCompleteBounded(client, unfiltered, maxTasks);
|
|
6632
|
+
}
|
|
6633
|
+
const combined = [];
|
|
6634
|
+
const ids2 = new Set;
|
|
6635
|
+
const { status: _status, ...baseFilter } = filter;
|
|
6636
|
+
for (const status of filter.status) {
|
|
6637
|
+
const remaining = maxTasks - combined.length;
|
|
6638
|
+
if (remaining < 1)
|
|
6639
|
+
throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/tasks exceeds the bounded limit ${maxTasks}`);
|
|
6640
|
+
const rows = await cloudListTasksCompleteBounded(client, { ...baseFilter, status }, remaining);
|
|
6641
|
+
for (const task of rows) {
|
|
6642
|
+
if (ids2.has(task.id)) {
|
|
6643
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: scalar status pages repeated a task across disjoint statuses");
|
|
6644
|
+
}
|
|
6645
|
+
ids2.add(task.id);
|
|
6646
|
+
combined.push(task);
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
combined.sort(compareCloudTaskOrder);
|
|
6650
|
+
return combined;
|
|
6651
|
+
}
|
|
6652
|
+
const pageSize = Math.min(500, maxTasks);
|
|
6653
|
+
const base = { ...filter, offset: undefined, limit: undefined };
|
|
6654
|
+
const first = await requestRawCloudTaskPage(client, { ...base, limit: pageSize, offset: 0 });
|
|
6655
|
+
if (first.total === undefined) {
|
|
6656
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: complete bounded task reads require an authoritative total");
|
|
6657
|
+
}
|
|
6658
|
+
const total = first.total;
|
|
6659
|
+
if (!Number.isSafeInteger(total) || total < 0 || total > maxTasks) {
|
|
6660
|
+
throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/tasks reports ${String(total)} rows above the bounded limit ${maxTasks}`);
|
|
6661
|
+
}
|
|
6662
|
+
const tasks = [];
|
|
6663
|
+
const ids = new Set;
|
|
6664
|
+
const statuses = filter.status === undefined ? null : new Set(Array.isArray(filter.status) ? filter.status : [filter.status]);
|
|
6665
|
+
const priorities = filter.priority === undefined ? null : new Set(Array.isArray(filter.priority) ? filter.priority : [filter.priority]);
|
|
6666
|
+
const matchesSelection = (task) => {
|
|
6667
|
+
if (statuses && !statuses.has(task.status))
|
|
6668
|
+
return false;
|
|
6669
|
+
if (priorities && !priorities.has(task.priority))
|
|
6670
|
+
return false;
|
|
6671
|
+
if (filter.project_id !== undefined && task.project_id !== filter.project_id)
|
|
6672
|
+
return false;
|
|
6673
|
+
if (filter.parent_id !== undefined && (task.parent_id ?? null) !== filter.parent_id)
|
|
6674
|
+
return false;
|
|
6675
|
+
if (filter.plan_id !== undefined && task.plan_id !== filter.plan_id)
|
|
6676
|
+
return false;
|
|
6677
|
+
if (filter.task_list_id !== undefined && task.task_list_id !== filter.task_list_id)
|
|
6678
|
+
return false;
|
|
6679
|
+
if (filter.assigned_to !== undefined && task.assigned_to !== filter.assigned_to)
|
|
6680
|
+
return false;
|
|
6681
|
+
if (filter.agent_id !== undefined && task.agent_id !== filter.agent_id)
|
|
6682
|
+
return false;
|
|
6683
|
+
if (filter.archived_only === true && !task.archived_at)
|
|
6684
|
+
return false;
|
|
6685
|
+
if (filter.include_archived === false && task.archived_at)
|
|
6686
|
+
return false;
|
|
6687
|
+
if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
|
|
6688
|
+
return false;
|
|
6689
|
+
return true;
|
|
6690
|
+
};
|
|
6691
|
+
const append = (page) => {
|
|
6692
|
+
for (const task of page) {
|
|
6693
|
+
if (!task || typeof task.id !== "string" || !task.id || ids.has(task.id)) {
|
|
6694
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a malformed or repeated task page");
|
|
6695
|
+
}
|
|
6696
|
+
if (!matchesSelection(task)) {
|
|
6697
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned a row outside the requested bounded selection");
|
|
6698
|
+
}
|
|
6699
|
+
ids.add(task.id);
|
|
6700
|
+
tasks.push(task);
|
|
6701
|
+
}
|
|
6702
|
+
};
|
|
6703
|
+
append(first.tasks);
|
|
6704
|
+
if (tasks.length > total)
|
|
6705
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks returned more rows than its total");
|
|
6706
|
+
while (tasks.length < total) {
|
|
6707
|
+
const page = await requestRawCloudTaskPage(client, {
|
|
6708
|
+
...base,
|
|
6709
|
+
limit: Math.min(pageSize, total - tasks.length),
|
|
6710
|
+
offset: tasks.length
|
|
6711
|
+
});
|
|
6712
|
+
if (page.total !== total || page.tasks.length === 0 || tasks.length + page.tasks.length > total) {
|
|
6713
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks pagination changed or stopped before the authoritative total");
|
|
6714
|
+
}
|
|
6715
|
+
append(page.tasks);
|
|
6716
|
+
}
|
|
6717
|
+
return tasks;
|
|
6718
|
+
}
|
|
6562
6719
|
async function cloudResolveTaskRef(client, ref) {
|
|
6563
6720
|
const input = ref.trim().toLowerCase();
|
|
6564
6721
|
if (!input)
|
|
@@ -6592,6 +6749,10 @@ async function cloudGetTask(client, id) {
|
|
|
6592
6749
|
async function cloudCreateTask(client, input, verification = {}) {
|
|
6593
6750
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
6594
6751
|
const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
|
|
6752
|
+
const expectedTaskListId = typeof input["task_list_id"] === "string" ? input["task_list_id"] : null;
|
|
6753
|
+
const expectedAssignedTo = typeof input["assigned_to"] === "string" ? input["assigned_to"] : null;
|
|
6754
|
+
const verifyTaskListId = expectedTaskListId !== null;
|
|
6755
|
+
const verifyAssignedTo = expectedAssignedTo !== null;
|
|
6595
6756
|
const expectedCreatedBy = typeof verification.expectedCreatedBy === "string" && verification.expectedCreatedBy.trim() ? verification.expectedCreatedBy : null;
|
|
6596
6757
|
if (expectedCreatedBy !== null) {
|
|
6597
6758
|
await requireTaskCreatorCapability(client);
|
|
@@ -6604,15 +6765,35 @@ async function cloudCreateTask(client, input, verification = {}) {
|
|
|
6604
6765
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
|
|
6605
6766
|
}
|
|
6606
6767
|
const persisted = await cloudGetTask(client, created.id);
|
|
6607
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
|
|
6768
|
+
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || verifyTaskListId && (persisted.task_list_id ?? null) !== expectedTaskListId || verifyAssignedTo && (persisted.assigned_to ?? null) !== expectedAssignedTo || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
|
|
6608
6769
|
const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
|
|
6609
|
-
throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id, parent_id, and
|
|
6770
|
+
throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id, parent_id, plan_id, and every explicitly requested routing field ` + `(requested plan_id=${JSON.stringify(expectedPlanId)}, readback=${JSON.stringify(persisted?.plan_id ?? null)}; ` + `${verifyTaskListId ? `requested task_list_id=${JSON.stringify(expectedTaskListId)}, readback=${JSON.stringify(persisted?.task_list_id ?? null)}; ` : ""}` + `${verifyAssignedTo ? `requested assigned_to=${JSON.stringify(expectedAssignedTo)}, readback=${JSON.stringify(persisted?.assigned_to ?? null)}` : ""})` + `${creatorDetail}; no success row or local SQLite fallback is permitted`);
|
|
6610
6771
|
}
|
|
6611
6772
|
return persisted;
|
|
6612
6773
|
}
|
|
6613
6774
|
async function cloudUpdateTask(client, id, patch) {
|
|
6614
6775
|
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
6615
6776
|
}
|
|
6777
|
+
async function cloudUpdateTaskVerified(client, id, patch) {
|
|
6778
|
+
const task = await cloudUpdateTask(client, id, patch);
|
|
6779
|
+
if (!task || typeof task !== "object" || typeof task.id !== "string" || task.id !== id) {
|
|
6780
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: PATCH /v1/tasks/${encodeURIComponent(id)} returned a mismatched task`);
|
|
6781
|
+
}
|
|
6782
|
+
const row = task;
|
|
6783
|
+
if (typeof row["title"] !== "string" || !TASK_STATUSES.includes(row["status"]) || !TASK_PRIORITIES.includes(row["priority"]) || !Number.isSafeInteger(row["version"]) || typeof row["created_at"] !== "string" || typeof row["updated_at"] !== "string" || !Array.isArray(row["tags"]) || !row["metadata"] || typeof row["metadata"] !== "object" || Array.isArray(row["metadata"])) {
|
|
6784
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: PATCH /v1/tasks/${encodeURIComponent(id)} returned an incomplete task record`);
|
|
6785
|
+
}
|
|
6786
|
+
for (const [field, expected] of Object.entries(patch)) {
|
|
6787
|
+
if (field === "version")
|
|
6788
|
+
continue;
|
|
6789
|
+
const actual = row[field] ?? null;
|
|
6790
|
+
const normalizedExpected = expected ?? null;
|
|
6791
|
+
if (JSON.stringify(actual) !== JSON.stringify(normalizedExpected)) {
|
|
6792
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: PATCH /v1/tasks/${encodeURIComponent(id)} did not preserve requested field ${field}`);
|
|
6793
|
+
}
|
|
6794
|
+
}
|
|
6795
|
+
return task;
|
|
6796
|
+
}
|
|
6616
6797
|
async function cloudDeleteTask(client, id) {
|
|
6617
6798
|
try {
|
|
6618
6799
|
await client.transport.del(`/tasks/${encodeURIComponent(id)}`);
|
|
@@ -6623,6 +6804,129 @@ async function cloudDeleteTask(client, id) {
|
|
|
6623
6804
|
throw error;
|
|
6624
6805
|
}
|
|
6625
6806
|
}
|
|
6807
|
+
function bulkReceiptError(route, detail) {
|
|
6808
|
+
return new Error(`REMOTE_API_INCOMPATIBLE: ${route} ${detail}; refusing to report a partial or ambiguous bulk mutation`);
|
|
6809
|
+
}
|
|
6810
|
+
async function cloudBulkCreateTasks(client, tasks) {
|
|
6811
|
+
const tempIds = new Set(tasks.flatMap((task) => task.temp_id ? [task.temp_id] : []));
|
|
6812
|
+
const normalizedTasks = [];
|
|
6813
|
+
for (const task of tasks) {
|
|
6814
|
+
const dependsOn = [];
|
|
6815
|
+
for (const reference of task.depends_on ?? []) {
|
|
6816
|
+
dependsOn.push(tempIds.has(reference) ? reference : await cloudResolveTaskRef(client, reference));
|
|
6817
|
+
}
|
|
6818
|
+
if (new Set(dependsOn).size !== dependsOn.length) {
|
|
6819
|
+
throw new Error("BULK_CREATE_DUPLICATE_DEPENDENCY: multiple references resolve to the same dependency; no request was sent");
|
|
6820
|
+
}
|
|
6821
|
+
normalizedTasks.push({ ...task, ...dependsOn.length ? { depends_on: dependsOn } : { depends_on: undefined } });
|
|
6822
|
+
}
|
|
6823
|
+
const raw = await requiredRemoteRoute(client, "/v1/tasks/bulk-create", () => client.transport.post("/tasks/bulk-create", { schema_version: 1, tasks: normalizedTasks }, { retry: false }));
|
|
6824
|
+
const receipt = raw && typeof raw === "object" ? raw["receipt"] : null;
|
|
6825
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
6826
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", "did not return a receipt object");
|
|
6827
|
+
}
|
|
6828
|
+
const value = receipt;
|
|
6829
|
+
if (value["schema_version"] !== 1 || value["atomic"] !== true) {
|
|
6830
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", "did not attest schema_version=1 and atomic=true");
|
|
6831
|
+
}
|
|
6832
|
+
const created = value["created"];
|
|
6833
|
+
const dependencies = value["dependencies"];
|
|
6834
|
+
if (!Array.isArray(created) || created.length !== normalizedTasks.length || !Array.isArray(dependencies)) {
|
|
6835
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", "returned incomplete created/dependency collections");
|
|
6836
|
+
}
|
|
6837
|
+
const createdIds = new Set;
|
|
6838
|
+
const parsedCreated = created.map((item, index) => {
|
|
6839
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6840
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", `created[${index}] is malformed`);
|
|
6841
|
+
}
|
|
6842
|
+
const row = item;
|
|
6843
|
+
const expectedTemp = normalizedTasks[index].temp_id ?? null;
|
|
6844
|
+
if (row["temp_id"] !== expectedTemp || typeof row["id"] !== "string" || !UUID_RE.test(row["id"]) || typeof row["title"] !== "string" || row["title"] !== normalizedTasks[index].title || row["short_id"] !== null && typeof row["short_id"] !== "string") {
|
|
6845
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", `created[${index}] does not match the requested task`);
|
|
6846
|
+
}
|
|
6847
|
+
if (createdIds.has(row["id"]))
|
|
6848
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", "returned duplicate task ids");
|
|
6849
|
+
createdIds.add(row["id"]);
|
|
6850
|
+
return {
|
|
6851
|
+
temp_id: expectedTemp,
|
|
6852
|
+
id: row["id"],
|
|
6853
|
+
short_id: row["short_id"],
|
|
6854
|
+
title: row["title"]
|
|
6855
|
+
};
|
|
6856
|
+
});
|
|
6857
|
+
const idByTemp = new Map;
|
|
6858
|
+
normalizedTasks.forEach((task, index) => {
|
|
6859
|
+
if (task.temp_id)
|
|
6860
|
+
idByTemp.set(task.temp_id, parsedCreated[index].id);
|
|
6861
|
+
});
|
|
6862
|
+
const expectedEdges = normalizedTasks.flatMap((task, index) => (task.depends_on ?? []).map((reference) => ({
|
|
6863
|
+
task_id: parsedCreated[index].id,
|
|
6864
|
+
depends_on: idByTemp.get(reference) ?? reference
|
|
6865
|
+
})));
|
|
6866
|
+
if (dependencies.length !== expectedEdges.length) {
|
|
6867
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", "returned the wrong dependency-edge count");
|
|
6868
|
+
}
|
|
6869
|
+
const seenEdges = new Set;
|
|
6870
|
+
const parsedDependencies = dependencies.map((item, index) => {
|
|
6871
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6872
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", `dependencies[${index}] is malformed`);
|
|
6873
|
+
}
|
|
6874
|
+
const row = item;
|
|
6875
|
+
if (typeof row["task_id"] !== "string" || !createdIds.has(row["task_id"]) || typeof row["depends_on"] !== "string" || !row["depends_on"]) {
|
|
6876
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", `dependencies[${index}] is not bound to this batch`);
|
|
6877
|
+
}
|
|
6878
|
+
const key = `${row["task_id"]}\x00${row["depends_on"]}`;
|
|
6879
|
+
if (seenEdges.has(key))
|
|
6880
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", "returned duplicate dependency edges");
|
|
6881
|
+
seenEdges.add(key);
|
|
6882
|
+
return { task_id: row["task_id"], depends_on: row["depends_on"] };
|
|
6883
|
+
});
|
|
6884
|
+
const expectedKeys = expectedEdges.map((edge) => `${edge.task_id}\x00${edge.depends_on}`).sort();
|
|
6885
|
+
const receivedKeys = parsedDependencies.map((edge) => `${edge.task_id}\x00${edge.depends_on}`).sort();
|
|
6886
|
+
if (JSON.stringify(receivedKeys) !== JSON.stringify(expectedKeys)) {
|
|
6887
|
+
throw bulkReceiptError("/v1/tasks/bulk-create", "returned dependency edges that do not match the request");
|
|
6888
|
+
}
|
|
6889
|
+
return { schema_version: 1, atomic: true, created: parsedCreated, dependencies: parsedDependencies };
|
|
6890
|
+
}
|
|
6891
|
+
async function cloudBulkDeleteTasks(client, taskIds, force) {
|
|
6892
|
+
const raw = await requiredRemoteRoute(client, "/v1/tasks/bulk-delete", () => client.transport.post("/tasks/bulk-delete", { schema_version: 1, task_ids: taskIds, force }, { retry: false }));
|
|
6893
|
+
const receipt = raw && typeof raw === "object" ? raw["receipt"] : null;
|
|
6894
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
6895
|
+
throw bulkReceiptError("/v1/tasks/bulk-delete", "did not return a receipt object");
|
|
6896
|
+
}
|
|
6897
|
+
const value = receipt;
|
|
6898
|
+
if (value["schema_version"] !== 1 || value["atomic"] !== true || value["force"] !== force || !Array.isArray(value["results"])) {
|
|
6899
|
+
throw bulkReceiptError("/v1/tasks/bulk-delete", "did not return the requested atomic force receipt");
|
|
6900
|
+
}
|
|
6901
|
+
if (value["results"].length !== taskIds.length) {
|
|
6902
|
+
throw bulkReceiptError("/v1/tasks/bulk-delete", "returned an incomplete result set");
|
|
6903
|
+
}
|
|
6904
|
+
const seenTaskIds = new Set;
|
|
6905
|
+
const results = value["results"].map((item, index) => {
|
|
6906
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
6907
|
+
throw bulkReceiptError("/v1/tasks/bulk-delete", `results[${index}] is malformed`);
|
|
6908
|
+
}
|
|
6909
|
+
const row = item;
|
|
6910
|
+
const outcome = row["outcome"];
|
|
6911
|
+
const taskId = row["task_id"];
|
|
6912
|
+
const reason = row["reason"];
|
|
6913
|
+
if (row["requested_id"] !== taskIds[index] || !["deleted", "skipped", "missing"].includes(String(outcome)) || taskId !== null && (typeof taskId !== "string" || !UUID_RE.test(taskId)) || UUID_RE.test(taskIds[index]) && taskId !== null && taskId.toLowerCase() !== taskIds[index].toLowerCase() || outcome === "deleted" && (typeof taskId !== "string" || reason !== null) || outcome === "skipped" && (typeof taskId !== "string" || reason !== "has_children") || outcome === "missing" && (taskId !== null || reason !== "not_found")) {
|
|
6914
|
+
throw bulkReceiptError("/v1/tasks/bulk-delete", `results[${index}] contradicts the request`);
|
|
6915
|
+
}
|
|
6916
|
+
if (typeof taskId === "string") {
|
|
6917
|
+
if (seenTaskIds.has(taskId))
|
|
6918
|
+
throw bulkReceiptError("/v1/tasks/bulk-delete", "returned duplicate resolved task ids");
|
|
6919
|
+
seenTaskIds.add(taskId);
|
|
6920
|
+
}
|
|
6921
|
+
return {
|
|
6922
|
+
requested_id: taskIds[index],
|
|
6923
|
+
task_id: taskId,
|
|
6924
|
+
outcome,
|
|
6925
|
+
reason
|
|
6926
|
+
};
|
|
6927
|
+
});
|
|
6928
|
+
return { schema_version: 1, atomic: true, force, results };
|
|
6929
|
+
}
|
|
6626
6930
|
function unwrapTemplate(raw) {
|
|
6627
6931
|
if (raw && typeof raw === "object" && "template" in raw) {
|
|
6628
6932
|
return raw.template;
|
|
@@ -6864,10 +7168,66 @@ async function cloudScanTaskRows(client, options = {}) {
|
|
|
6864
7168
|
}
|
|
6865
7169
|
return { complete: reason === undefined, ...reason ? { reason } : {}, scanned: rows.length, total, pages, rows };
|
|
6866
7170
|
}
|
|
6867
|
-
async function cloudListAgents(client) {
|
|
6868
|
-
const
|
|
6869
|
-
|
|
6870
|
-
|
|
7171
|
+
async function cloudListAgents(client, options = {}) {
|
|
7172
|
+
const maxAgents = options.max_agents ?? 1e4;
|
|
7173
|
+
if (!Number.isSafeInteger(maxAgents) || maxAgents < 1 || maxAgents > 1e4) {
|
|
7174
|
+
throw new Error("max_agents must be an integer from 1 to 10000");
|
|
7175
|
+
}
|
|
7176
|
+
const pageSize = Math.min(500, maxAgents);
|
|
7177
|
+
const agents = [];
|
|
7178
|
+
const seen = new Set;
|
|
7179
|
+
let total = null;
|
|
7180
|
+
while (total === null || agents.length < total) {
|
|
7181
|
+
const offset = agents.length;
|
|
7182
|
+
const requestedLimit = Math.min(pageSize, maxAgents - offset);
|
|
7183
|
+
if (requestedLimit < 1) {
|
|
7184
|
+
throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/agents exceeds the bounded limit ${maxAgents}`);
|
|
7185
|
+
}
|
|
7186
|
+
const raw = await requiredRemoteRoute(client, "/v1/agents", () => client.transport.get("/agents", {
|
|
7187
|
+
query: { limit: requestedLimit, offset, include_archived: options.include_archived === true }
|
|
7188
|
+
}));
|
|
7189
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
7190
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents did not return a bounded roster envelope");
|
|
7191
|
+
}
|
|
7192
|
+
const envelope = raw;
|
|
7193
|
+
const page = envelope["agents"];
|
|
7194
|
+
const pageTotal = envelope["total"];
|
|
7195
|
+
if (!Array.isArray(page) || !Number.isSafeInteger(pageTotal) || pageTotal < 0 || envelope["count"] !== page.length || envelope["limit"] !== requestedLimit || envelope["offset"] !== offset || page.length > requestedLimit || offset + page.length > pageTotal) {
|
|
7196
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents returned a contradictory bounded page");
|
|
7197
|
+
}
|
|
7198
|
+
if (pageTotal > maxAgents) {
|
|
7199
|
+
throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/agents reports ${String(pageTotal)} rows above the bounded limit ${maxAgents}`);
|
|
7200
|
+
}
|
|
7201
|
+
if (total !== null && pageTotal !== total) {
|
|
7202
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents total changed during pagination");
|
|
7203
|
+
}
|
|
7204
|
+
total = pageTotal;
|
|
7205
|
+
const hasMore = offset + page.length < total;
|
|
7206
|
+
if (envelope["has_more"] !== hasMore || envelope["next_offset"] !== (hasMore ? offset + page.length : null) || hasMore && page.length === 0) {
|
|
7207
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents returned a stalled or contradictory page receipt");
|
|
7208
|
+
}
|
|
7209
|
+
for (const item of page) {
|
|
7210
|
+
const agent = item;
|
|
7211
|
+
if (!agent || typeof agent !== "object" || typeof agent.id !== "string" || !agent.id || typeof agent.name !== "string" || !agent.name || seen.has(agent.id)) {
|
|
7212
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/agents returned a malformed or repeated agent");
|
|
7213
|
+
}
|
|
7214
|
+
seen.add(agent.id);
|
|
7215
|
+
agents.push(agent);
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
return agents;
|
|
7219
|
+
}
|
|
7220
|
+
async function cloudResolveAgentRef(client, ref) {
|
|
7221
|
+
const normalized = ref.trim().toLowerCase();
|
|
7222
|
+
if (!normalized)
|
|
7223
|
+
throw new Error("Agent reference must not be empty");
|
|
7224
|
+
const matches = (await cloudListAgents(client)).filter((agent) => agent.id.toLowerCase() === normalized || agent.name.toLowerCase() === normalized);
|
|
7225
|
+
if (matches.length === 0)
|
|
7226
|
+
throw new Error(`Agent not found: ${ref}`);
|
|
7227
|
+
if (matches.length > 1 && new Set(matches.map((agent) => agent.id)).size > 1) {
|
|
7228
|
+
throw new Error(`Agent reference is ambiguous: ${ref}`);
|
|
7229
|
+
}
|
|
7230
|
+
return matches[0].id;
|
|
6871
7231
|
}
|
|
6872
7232
|
async function cloudListProjects(client) {
|
|
6873
7233
|
const res = await requiredRemoteRoute(client, "/v1/projects", () => client.list("projects"));
|
|
@@ -7191,12 +7551,66 @@ function normalizePlanComment(value) {
|
|
|
7191
7551
|
function redactComment(comment) {
|
|
7192
7552
|
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
7193
7553
|
}
|
|
7554
|
+
function assertTaskHistoryRows(raw, route) {
|
|
7555
|
+
if (!Array.isArray(raw)) {
|
|
7556
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} did not return a task-history array`);
|
|
7557
|
+
}
|
|
7558
|
+
for (let index = 0;index < raw.length; index++) {
|
|
7559
|
+
const entry = raw[index];
|
|
7560
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
7561
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} entry ${index} is malformed`);
|
|
7562
|
+
}
|
|
7563
|
+
const row = entry;
|
|
7564
|
+
if (typeof row["id"] !== "string" || !row["id"] || typeof row["task_id"] !== "string" || !row["task_id"] || typeof row["action"] !== "string" || !row["action"] || typeof row["created_at"] !== "string" || !row["created_at"]) {
|
|
7565
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} entry ${index} is incomplete`);
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7568
|
+
return raw;
|
|
7569
|
+
}
|
|
7194
7570
|
async function cloudTaskHistory(client, taskId) {
|
|
7195
7571
|
const raw = await client.transport.get(`/tasks/${encodeURIComponent(taskId)}/history`);
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7572
|
+
if (Array.isArray(raw))
|
|
7573
|
+
return assertTaskHistoryRows(raw, "/v1/tasks/:id/history");
|
|
7574
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
7575
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks/:id/history did not return an object");
|
|
7576
|
+
}
|
|
7577
|
+
const envelope = raw;
|
|
7578
|
+
const history = assertTaskHistoryRows(envelope["history"], "/v1/tasks/:id/history");
|
|
7579
|
+
if (envelope["count"] !== undefined && envelope["count"] !== history.length) {
|
|
7580
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks/:id/history count contradicts its rows");
|
|
7581
|
+
}
|
|
7582
|
+
if (history.some((entry) => entry.task_id !== taskId)) {
|
|
7583
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/tasks/:id/history returned an entry for another task");
|
|
7584
|
+
}
|
|
7585
|
+
return history;
|
|
7586
|
+
}
|
|
7587
|
+
async function cloudTaskHistoryPage(client, taskId, options) {
|
|
7588
|
+
const raw = await requiredRemoteRoute(client, "/v1/tasks/:id/history pagination", () => client.transport.get(`/tasks/${encodeURIComponent(taskId)}/history`, { query: options }));
|
|
7589
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
7590
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: paginated task history did not return an object");
|
|
7591
|
+
}
|
|
7592
|
+
const envelope = raw;
|
|
7593
|
+
const history = assertTaskHistoryRows(envelope["history"], "/v1/tasks/:id/history");
|
|
7594
|
+
const count = envelope["count"];
|
|
7595
|
+
const total = envelope["total"];
|
|
7596
|
+
const limit = envelope["limit"];
|
|
7597
|
+
const offset = envelope["offset"];
|
|
7598
|
+
const order = envelope["order"];
|
|
7599
|
+
const hasMore = envelope["has_more"];
|
|
7600
|
+
const nextOffset = envelope["next_offset"];
|
|
7601
|
+
if (count !== history.length || total === undefined || !Number.isSafeInteger(total) || total < 0 || limit !== options.limit || offset !== options.offset || order !== options.order || typeof hasMore !== "boolean" || nextOffset !== null && (!Number.isSafeInteger(nextOffset) || nextOffset < 0) || history.length > options.limit || history.length > 0 && (options.offset >= total || history.length > total - options.offset) || (hasMore ? nextOffset !== options.offset + history.length : nextOffset !== null) || hasMore !== options.offset + history.length < total || history.some((entry) => entry.task_id !== taskId)) {
|
|
7602
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: paginated task history returned contradictory metadata");
|
|
7603
|
+
}
|
|
7604
|
+
return {
|
|
7605
|
+
history,
|
|
7606
|
+
count: history.length,
|
|
7607
|
+
total,
|
|
7608
|
+
limit: options.limit,
|
|
7609
|
+
offset: options.offset,
|
|
7610
|
+
order: options.order,
|
|
7611
|
+
has_more: hasMore,
|
|
7612
|
+
next_offset: nextOffset
|
|
7613
|
+
};
|
|
7200
7614
|
}
|
|
7201
7615
|
async function cloudUpsertTaskByFingerprint(client, input) {
|
|
7202
7616
|
const raw = await requiredRemoteRoute(client, "/v1/tasks/upsert", () => client.transport.post("/tasks/upsert", input));
|
|
@@ -7217,6 +7631,43 @@ async function cloudCountTasks(client, filter = {}) {
|
|
|
7217
7631
|
const tasks = await cloudListTasks(client, rest);
|
|
7218
7632
|
return tasks.length;
|
|
7219
7633
|
}
|
|
7634
|
+
function isCloudAgentRecord(value) {
|
|
7635
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
7636
|
+
}
|
|
7637
|
+
function isNullableString(value) {
|
|
7638
|
+
return value === null || typeof value === "string";
|
|
7639
|
+
}
|
|
7640
|
+
function assertCloudAgentEnvelope(raw, expectedId) {
|
|
7641
|
+
if (!isCloudAgentRecord(raw) || !Object.prototype.hasOwnProperty.call(raw, "agent")) {
|
|
7642
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id did not return the required { agent } envelope");
|
|
7643
|
+
}
|
|
7644
|
+
const agent = raw["agent"];
|
|
7645
|
+
if (!isCloudAgentRecord(agent)) {
|
|
7646
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned a malformed agent object");
|
|
7647
|
+
}
|
|
7648
|
+
if (agent["id"] !== expectedId) {
|
|
7649
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned an agent with a different immutable ID");
|
|
7650
|
+
}
|
|
7651
|
+
if (typeof agent["name"] !== "string" || !agent["name"] || !isNullableString(agent["description"]) || !isNullableString(agent["role"]) || !isNullableString(agent["title"]) || !isNullableString(agent["level"]) || !Array.isArray(agent["permissions"]) || agent["permissions"].some((value) => typeof value !== "string") || !isNullableString(agent["reports_to"]) || !isNullableString(agent["org_id"]) || !Array.isArray(agent["capabilities"]) || agent["capabilities"].some((value) => typeof value !== "string") || !["active", "archived"].includes(String(agent["status"])) || !isCloudAgentRecord(agent["metadata"]) || typeof agent["created_at"] !== "string" || !agent["created_at"] || typeof agent["last_seen_at"] !== "string" || !agent["last_seen_at"] || !isNullableString(agent["session_id"]) || !isNullableString(agent["working_dir"]) || !isNullableString(agent["active_project_id"])) {
|
|
7652
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned an incomplete agent record");
|
|
7653
|
+
}
|
|
7654
|
+
for (const optionalString of ["identity_id", "project_id", "runtime_instance_id", "machine_id", "synced_at"]) {
|
|
7655
|
+
if (agent[optionalString] !== undefined && !isNullableString(agent[optionalString])) {
|
|
7656
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: GET /v1/agents/:id returned invalid optional agent fields");
|
|
7657
|
+
}
|
|
7658
|
+
}
|
|
7659
|
+
return agent;
|
|
7660
|
+
}
|
|
7661
|
+
async function cloudGetAgent(client, id) {
|
|
7662
|
+
try {
|
|
7663
|
+
const raw = await client.transport.get(`/agents/${encodeURIComponent(id)}`);
|
|
7664
|
+
return assertCloudAgentEnvelope(raw, id);
|
|
7665
|
+
} catch (error) {
|
|
7666
|
+
if (error && typeof error === "object" && error.status === 404)
|
|
7667
|
+
return null;
|
|
7668
|
+
throw error;
|
|
7669
|
+
}
|
|
7670
|
+
}
|
|
7220
7671
|
async function cloudRegisterAgent(client, input) {
|
|
7221
7672
|
const raw = await client.transport.post("/agents", input);
|
|
7222
7673
|
if (raw && typeof raw === "object" && "agent" in raw) {
|
|
@@ -7243,6 +7694,20 @@ async function cloudLinkCommit(client, taskId, input) {
|
|
|
7243
7694
|
}
|
|
7244
7695
|
return raw;
|
|
7245
7696
|
}
|
|
7697
|
+
async function cloudListTaskCommits(client, taskId) {
|
|
7698
|
+
const route = `/v1/tasks/${encodeURIComponent(taskId)}/commits`;
|
|
7699
|
+
const raw = await requiredRemoteRoute(client, route, () => client.transport.get(`/tasks/${encodeURIComponent(taskId)}/commits`));
|
|
7700
|
+
const envelope = raw ?? {};
|
|
7701
|
+
if (!Array.isArray(envelope.commits)) {
|
|
7702
|
+
if (Array.isArray(raw))
|
|
7703
|
+
return raw;
|
|
7704
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} did not return a commits array; refusing an incomplete commit trail`);
|
|
7705
|
+
}
|
|
7706
|
+
if (typeof envelope.count === "number" && envelope.count !== envelope.commits.length) {
|
|
7707
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} reported ${envelope.count} commits but returned ${envelope.commits.length}; refusing a partial commit trail`);
|
|
7708
|
+
}
|
|
7709
|
+
return envelope.commits;
|
|
7710
|
+
}
|
|
7246
7711
|
async function cloudFindCommit(client, sha) {
|
|
7247
7712
|
const raw = await client.transport.get(`/commits/${encodeURIComponent(sha)}`);
|
|
7248
7713
|
const env = raw ?? {};
|
|
@@ -7631,13 +8096,26 @@ async function cloudTaskStats(client, filter = {}) {
|
|
|
7631
8096
|
};
|
|
7632
8097
|
}
|
|
7633
8098
|
async function cloudRecentActivity(client, limit = 50) {
|
|
8099
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
8100
|
+
throw new Error("activity limit must be an integer from 1 to 10000");
|
|
8101
|
+
}
|
|
7634
8102
|
const raw = await client.transport.get("/activity", { query: { limit } });
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
return
|
|
7640
|
-
|
|
8103
|
+
if (Array.isArray(raw)) {
|
|
8104
|
+
const legacy = assertTaskHistoryRows(raw, "/v1/activity");
|
|
8105
|
+
if (legacy.length > limit)
|
|
8106
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/activity exceeded its requested bound");
|
|
8107
|
+
return legacy;
|
|
8108
|
+
}
|
|
8109
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
8110
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/activity did not return an object");
|
|
8111
|
+
}
|
|
8112
|
+
const envelope = raw;
|
|
8113
|
+
const rows = envelope["activity"] ?? envelope["entries"];
|
|
8114
|
+
const activity = assertTaskHistoryRows(rows, "/v1/activity");
|
|
8115
|
+
if (activity.length > limit || envelope["count"] !== activity.length || envelope["limit"] !== undefined && envelope["limit"] !== limit) {
|
|
8116
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: /v1/activity count or limit contradicts its rows");
|
|
8117
|
+
}
|
|
8118
|
+
return activity;
|
|
7641
8119
|
}
|
|
7642
8120
|
async function cloudListTaskLists(client, projectId) {
|
|
7643
8121
|
const query = projectId ? { project_id: projectId } : {};
|
|
@@ -7768,12 +8246,78 @@ async function cloudClaimNext(client, agentId) {
|
|
|
7768
8246
|
const task = unwrapTask(raw);
|
|
7769
8247
|
return task && task.id ? task : null;
|
|
7770
8248
|
}
|
|
8249
|
+
function dependencyEnvelopeError(detail) {
|
|
8250
|
+
return new Error(`REMOTE_API_INCOMPATIBLE: /v1/dependencies ${detail}; refusing incomplete dependency analytics`);
|
|
8251
|
+
}
|
|
7771
8252
|
async function cloudAllDependencies(client) {
|
|
7772
|
-
const
|
|
7773
|
-
const
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
8253
|
+
const edges = [];
|
|
8254
|
+
const seen = new Set;
|
|
8255
|
+
let offset = 0;
|
|
8256
|
+
let expectedTotal = null;
|
|
8257
|
+
while (true) {
|
|
8258
|
+
const raw = await requiredRemoteRoute(client, "/v1/dependencies", () => client.transport.get("/dependencies", {
|
|
8259
|
+
query: { limit: CLOUD_DEPENDENCY_PAGE_SIZE, offset }
|
|
8260
|
+
}));
|
|
8261
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
8262
|
+
throw dependencyEnvelopeError("did not return an object envelope");
|
|
8263
|
+
}
|
|
8264
|
+
const envelope = raw;
|
|
8265
|
+
const page = envelope["dependencies"];
|
|
8266
|
+
const count = envelope["count"];
|
|
8267
|
+
const total = envelope["total"];
|
|
8268
|
+
const returnedLimit = envelope["limit"];
|
|
8269
|
+
const returnedOffset = envelope["offset"];
|
|
8270
|
+
const hasMore = envelope["has_more"];
|
|
8271
|
+
const nextOffset = envelope["next_offset"];
|
|
8272
|
+
if (!Array.isArray(page))
|
|
8273
|
+
throw dependencyEnvelopeError("did not return a dependencies array");
|
|
8274
|
+
if (!Number.isSafeInteger(count) || count !== page.length) {
|
|
8275
|
+
throw dependencyEnvelopeError("returned a count that does not match the page");
|
|
8276
|
+
}
|
|
8277
|
+
if (!Number.isSafeInteger(total) || total < 0) {
|
|
8278
|
+
throw dependencyEnvelopeError("did not return a non-negative total");
|
|
8279
|
+
}
|
|
8280
|
+
if (total > CLOUD_DEPENDENCY_LIMIT) {
|
|
8281
|
+
throw new Error(`REMOTE_RESULT_TOO_LARGE: /v1/dependencies reports ${total} edges, above the ` + `${CLOUD_DEPENDENCY_LIMIT}-edge analytics limit; narrow the operation or use a server aggregate`);
|
|
8282
|
+
}
|
|
8283
|
+
if (returnedLimit !== CLOUD_DEPENDENCY_PAGE_SIZE || returnedOffset !== offset) {
|
|
8284
|
+
throw dependencyEnvelopeError("did not honor the requested bounded page");
|
|
8285
|
+
}
|
|
8286
|
+
const computedHasMore = offset + page.length < total;
|
|
8287
|
+
if (typeof hasMore !== "boolean" || hasMore !== computedHasMore) {
|
|
8288
|
+
throw dependencyEnvelopeError("returned inconsistent has_more evidence");
|
|
8289
|
+
}
|
|
8290
|
+
if (hasMore ? nextOffset !== offset + page.length : nextOffset !== null) {
|
|
8291
|
+
throw dependencyEnvelopeError("returned an invalid next_offset");
|
|
8292
|
+
}
|
|
8293
|
+
if (expectedTotal === null)
|
|
8294
|
+
expectedTotal = total;
|
|
8295
|
+
else if (expectedTotal !== total)
|
|
8296
|
+
throw dependencyEnvelopeError("changed total during pagination");
|
|
8297
|
+
if (hasMore && page.length === 0)
|
|
8298
|
+
throw dependencyEnvelopeError("made no pagination progress");
|
|
8299
|
+
for (const value of page) {
|
|
8300
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8301
|
+
throw dependencyEnvelopeError("returned a non-object edge");
|
|
8302
|
+
}
|
|
8303
|
+
const edge = value;
|
|
8304
|
+
if (typeof edge["task_id"] !== "string" || !edge["task_id"] || typeof edge["depends_on"] !== "string" || !edge["depends_on"]) {
|
|
8305
|
+
throw dependencyEnvelopeError("returned an edge without task_id and depends_on");
|
|
8306
|
+
}
|
|
8307
|
+
const key = `${edge["task_id"]}\x00${edge["depends_on"]}`;
|
|
8308
|
+
if (seen.has(key))
|
|
8309
|
+
throw dependencyEnvelopeError("repeated an edge across pages");
|
|
8310
|
+
seen.add(key);
|
|
8311
|
+
edges.push(value);
|
|
8312
|
+
}
|
|
8313
|
+
if (!hasMore) {
|
|
8314
|
+
if (edges.length !== expectedTotal) {
|
|
8315
|
+
throw dependencyEnvelopeError(`returned ${edges.length} edges for total ${expectedTotal}`);
|
|
8316
|
+
}
|
|
8317
|
+
return edges;
|
|
8318
|
+
}
|
|
8319
|
+
offset = nextOffset;
|
|
8320
|
+
}
|
|
7777
8321
|
}
|
|
7778
8322
|
async function cloudGetTasksByIds(client, ids) {
|
|
7779
8323
|
const unique2 = Array.from(new Set(ids));
|
|
@@ -7879,7 +8423,7 @@ async function cloudTimeline(client, options = {}) {
|
|
|
7879
8423
|
const limit = options.limit ?? 50;
|
|
7880
8424
|
return { entries: entries.slice(offset, offset + limit), total, limit, offset };
|
|
7881
8425
|
}
|
|
7882
|
-
var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6;
|
|
8426
|
+
var UUID_RE, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6, CLOUD_DEPENDENCY_LIMIT = 1e4, CLOUD_DEPENDENCY_PAGE_SIZE = 500;
|
|
7883
8427
|
var init_cloud_router = __esm(() => {
|
|
7884
8428
|
init_types();
|
|
7885
8429
|
init_redaction();
|
|
@@ -7920,12 +8464,6 @@ var init_cloud_router = __esm(() => {
|
|
|
7920
8464
|
});
|
|
7921
8465
|
|
|
7922
8466
|
// src/cli/stage-a.ts
|
|
7923
|
-
function todosLocalModeNotice(reason = "local-opt-in") {
|
|
7924
|
-
if (reason === "local-only-command") {
|
|
7925
|
-
return "todos: LOCAL mode \u2014 this command only ever runs against the on-box SQLite store, so this run " + "does not reach the hosted fleet even though a Todos authority is configured.";
|
|
7926
|
-
}
|
|
7927
|
-
return "todos: LOCAL mode \u2014 using the on-box SQLite store, not the hosted fleet " + "(HASNA_TODOS_LOCAL is set). Unset it, and provide a credential via the Keychain item " + "hasna.credentials.todos.api-key, ~/.hasna/todos/config/credentials, or HASNA_TODOS_API_KEY, " + "to work against https://api.hasna.com/todos.";
|
|
7928
|
-
}
|
|
7929
8467
|
function announceTodosCliLocalMode(authority, write = (line) => process.stderr.write(`${line}
|
|
7930
8468
|
`)) {
|
|
7931
8469
|
if (authority.route !== "local")
|
|
@@ -8266,6 +8804,7 @@ var REGISTERED_CANONICAL_COMMANDS, TODOS_CLI_COMMAND_ALIASES, BUNDLED_STATIC_COM
|
|
|
8266
8804
|
var init_stage_a = __esm(() => {
|
|
8267
8805
|
init_esm();
|
|
8268
8806
|
init_cloud_router();
|
|
8807
|
+
init_local_opt_in();
|
|
8269
8808
|
REGISTERED_CANONICAL_COMMANDS = [
|
|
8270
8809
|
"active",
|
|
8271
8810
|
"add",
|
|
@@ -12403,20 +12942,17 @@ function ensureAgentIdentitySchema(db) {
|
|
|
12403
12942
|
SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
|
|
12404
12943
|
END;
|
|
12405
12944
|
|
|
12945
|
+
-- Drop first so an older definition is replaced rather than silently
|
|
12946
|
+
-- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
|
|
12947
|
+
-- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
|
|
12948
|
+
-- do different jobs and both are kept: the DROP is what refreshes a
|
|
12949
|
+
-- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
|
|
12950
|
+
-- on its own \u2014 so a caller that reaches it without a preceding DROP, in
|
|
12951
|
+
-- its own connection or transaction, gets a no-op instead of
|
|
12952
|
+
-- "trigger ... already exists".
|
|
12406
12953
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
|
|
12407
|
-
CREATE TRIGGER trg_agent_identity_mapping_history_immutable
|
|
12408
|
-
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
12409
|
-
ON agent_identity_source_mappings
|
|
12410
|
-
BEGIN
|
|
12411
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
12412
|
-
END;
|
|
12413
|
-
|
|
12414
12954
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
|
|
12415
|
-
|
|
12416
|
-
BEFORE DELETE ON agent_identity_source_mappings
|
|
12417
|
-
BEGIN
|
|
12418
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
12419
|
-
END;
|
|
12955
|
+
${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
|
|
12420
12956
|
`);
|
|
12421
12957
|
db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
|
|
12422
12958
|
});
|
|
@@ -12456,7 +12992,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
|
|
|
12456
12992
|
function recordAgentAlias(agentId, label, db) {
|
|
12457
12993
|
return storeAgentAlias(agentId, label, "historical", "active", db);
|
|
12458
12994
|
}
|
|
12459
|
-
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65
|
|
12995
|
+
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
|
|
12996
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
|
|
12997
|
+
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
12998
|
+
ON agent_identity_source_mappings
|
|
12999
|
+
BEGIN
|
|
13000
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
13001
|
+
END;
|
|
13002
|
+
|
|
13003
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
|
|
13004
|
+
BEFORE DELETE ON agent_identity_source_mappings
|
|
13005
|
+
BEGIN
|
|
13006
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
13007
|
+
END;
|
|
13008
|
+
`;
|
|
12460
13009
|
var init_identity_mapping = __esm(() => {
|
|
12461
13010
|
init_types();
|
|
12462
13011
|
init_types();
|
|
@@ -12773,15 +13322,18 @@ __export(exports_database, {
|
|
|
12773
13322
|
resolvePartialId: () => resolvePartialId,
|
|
12774
13323
|
resolveAssignedToAliases: () => resolveAssignedToAliases,
|
|
12775
13324
|
resetDatabase: () => resetDatabase,
|
|
13325
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
12776
13326
|
now: () => now,
|
|
12777
13327
|
lowerInClause: () => lowerInClause,
|
|
12778
13328
|
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
12779
13329
|
isLockExpired: () => isLockExpired,
|
|
13330
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
12780
13331
|
getDatabasePath: () => getDatabasePath,
|
|
12781
13332
|
getDatabase: () => getDatabase,
|
|
12782
13333
|
closeDatabase: () => closeDatabase,
|
|
12783
13334
|
clearExpiredLocks: () => clearExpiredLocks,
|
|
12784
13335
|
assignedToAliasSet: () => assignedToAliasSet,
|
|
13336
|
+
allowLocalStore: () => allowLocalStore,
|
|
12785
13337
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
12786
13338
|
});
|
|
12787
13339
|
import { Database } from "bun:sqlite";
|
|
@@ -12898,7 +13450,18 @@ function maybeInstallShadowCapture(db) {
|
|
|
12898
13450
|
console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
12899
13451
|
}
|
|
12900
13452
|
}
|
|
13453
|
+
function refuseLocalStore(message) {
|
|
13454
|
+
localStoreRefusal = message;
|
|
13455
|
+
}
|
|
13456
|
+
function allowLocalStore() {
|
|
13457
|
+
localStoreRefusal = null;
|
|
13458
|
+
}
|
|
13459
|
+
function isLocalStoreRefused() {
|
|
13460
|
+
return localStoreRefusal !== null;
|
|
13461
|
+
}
|
|
12901
13462
|
function getDatabase(dbPath) {
|
|
13463
|
+
if (localStoreRefusal !== null)
|
|
13464
|
+
throw new Error(localStoreRefusal);
|
|
12902
13465
|
if (dbPath === undefined && !selectsTodosLocalStore()) {
|
|
12903
13466
|
throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
|
|
12904
13467
|
}
|
|
@@ -13039,7 +13602,7 @@ function lowerInClause(column, values, params) {
|
|
|
13039
13602
|
params.push(...values.map((v) => v.toLowerCase()));
|
|
13040
13603
|
return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
|
|
13041
13604
|
}
|
|
13042
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
13605
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
|
|
13043
13606
|
var init_database = __esm(() => {
|
|
13044
13607
|
init_local_opt_in();
|
|
13045
13608
|
init_schema();
|
|
@@ -19142,7 +19705,9 @@ function listTasks(filter = {}, db) {
|
|
|
19142
19705
|
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
19143
19706
|
} catch {}
|
|
19144
19707
|
}
|
|
19145
|
-
if (
|
|
19708
|
+
if (filter.archived_only) {
|
|
19709
|
+
conditions.push("archived_at IS NOT NULL");
|
|
19710
|
+
} else if (!filter.include_archived) {
|
|
19146
19711
|
conditions.push("archived_at IS NULL");
|
|
19147
19712
|
}
|
|
19148
19713
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -19155,7 +19720,8 @@ function listTasks(filter = {}, db) {
|
|
|
19155
19720
|
params.push(filter.offset);
|
|
19156
19721
|
}
|
|
19157
19722
|
}
|
|
19158
|
-
const
|
|
19723
|
+
const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK2}, created_at DESC, id ASC`;
|
|
19724
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
|
|
19159
19725
|
return rows.map(rowToTask);
|
|
19160
19726
|
}
|
|
19161
19727
|
function getTaskByFingerprint(fingerprint, db) {
|
|
@@ -19298,7 +19864,9 @@ function countTasks(filter = {}, db) {
|
|
|
19298
19864
|
}
|
|
19299
19865
|
}
|
|
19300
19866
|
addMetadataConditions(filter.metadata, conditions, params);
|
|
19301
|
-
if (
|
|
19867
|
+
if (filter.archived_only) {
|
|
19868
|
+
conditions.push("archived_at IS NOT NULL");
|
|
19869
|
+
} else if (!filter.include_archived) {
|
|
19302
19870
|
conditions.push("archived_at IS NULL");
|
|
19303
19871
|
}
|
|
19304
19872
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -19435,6 +20003,10 @@ function updateTaskStored(id, input, db) {
|
|
|
19435
20003
|
sets.push("completed_at = ?");
|
|
19436
20004
|
params.push(input.completed_at);
|
|
19437
20005
|
}
|
|
20006
|
+
if (input.archived_at !== undefined) {
|
|
20007
|
+
sets.push("archived_at = ?");
|
|
20008
|
+
params.push(input.archived_at);
|
|
20009
|
+
}
|
|
19438
20010
|
if (input.confidence !== undefined) {
|
|
19439
20011
|
sets.push("confidence = ?");
|
|
19440
20012
|
params.push(input.confidence);
|
|
@@ -19498,6 +20070,8 @@ function updateTaskStored(id, input, db) {
|
|
|
19498
20070
|
logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
|
|
19499
20071
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
19500
20072
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
20073
|
+
if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
|
|
20074
|
+
logTaskChange(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
|
|
19501
20075
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
19502
20076
|
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
19503
20077
|
if (input.approved_by !== undefined)
|
|
@@ -23102,6 +23676,7 @@ __export(exports_agents, {
|
|
|
23102
23676
|
registerAgent: () => registerAgent,
|
|
23103
23677
|
normalizeGeneratedAgentNames: () => normalizeGeneratedAgentNames,
|
|
23104
23678
|
matchCapabilities: () => matchCapabilities,
|
|
23679
|
+
listAgentsPage: () => listAgentsPage,
|
|
23105
23680
|
listAgents: () => listAgents,
|
|
23106
23681
|
isAgentConflict: () => isAgentConflict,
|
|
23107
23682
|
getOrgChart: () => getOrgChart,
|
|
@@ -23322,6 +23897,19 @@ function listAgents(opts, db) {
|
|
|
23322
23897
|
}
|
|
23323
23898
|
return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
|
|
23324
23899
|
}
|
|
23900
|
+
function listAgentsPage(options, db) {
|
|
23901
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 500) {
|
|
23902
|
+
throw new Error("Agent page limit must be an integer from 1 to 500");
|
|
23903
|
+
}
|
|
23904
|
+
if (!Number.isSafeInteger(options.offset) || options.offset < 0) {
|
|
23905
|
+
throw new Error("Agent page offset must be a non-negative integer");
|
|
23906
|
+
}
|
|
23907
|
+
const d = db || getDatabase();
|
|
23908
|
+
const where = options.include_archived ? "" : "WHERE status = 'active'";
|
|
23909
|
+
const total = d.query(`SELECT COUNT(*) AS total FROM agents ${where}`).get().total;
|
|
23910
|
+
const agents = d.query(`SELECT * FROM agents ${where} ORDER BY LOWER(name), id LIMIT ? OFFSET ?`).all(options.limit, options.offset).map(rowToAgent);
|
|
23911
|
+
return { agents, total };
|
|
23912
|
+
}
|
|
23325
23913
|
function updateAgentActivity(id, db) {
|
|
23326
23914
|
const d = db || getDatabase();
|
|
23327
23915
|
d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
|
|
@@ -26271,6 +26859,234 @@ async function updateSharedTemplate(client, id, patch) {
|
|
|
26271
26859
|
return raw.template;
|
|
26272
26860
|
}
|
|
26273
26861
|
|
|
26862
|
+
// src/cli/template-remote.ts
|
|
26863
|
+
function requireTemplateClient() {
|
|
26864
|
+
assertTemplateApiEnvironment(process.env);
|
|
26865
|
+
const client = getTodosCloudClient();
|
|
26866
|
+
if (!client)
|
|
26867
|
+
throw new Error("Templates require HASNA_TODOS_API_URL and HASNA_TODOS_API_KEY, or saved account credentials");
|
|
26868
|
+
return client;
|
|
26869
|
+
}
|
|
26870
|
+
async function resolveRemoteTemplate(client, ref) {
|
|
26871
|
+
if (!ref.trim())
|
|
26872
|
+
throw new Error("Blank template ID prefix");
|
|
26873
|
+
const direct = await cloudGetTemplate(client, ref);
|
|
26874
|
+
if (direct?.id === ref)
|
|
26875
|
+
return direct;
|
|
26876
|
+
const rows = await listSharedTemplates(client);
|
|
26877
|
+
const exact = rows.find((row) => row.id === ref);
|
|
26878
|
+
const matches = exact ? [exact] : rows.filter((row) => row.id.startsWith(ref));
|
|
26879
|
+
if (!ref.trim() || matches.length > 1)
|
|
26880
|
+
throw new Error("Ambiguous or blank template ID prefix");
|
|
26881
|
+
return matches[0] ? cloudGetTemplate(client, matches[0].id) : null;
|
|
26882
|
+
}
|
|
26883
|
+
async function preflightTemplate(client, template, variables, seen = new Set) {
|
|
26884
|
+
if (seen.has(template.id) || seen.size >= 32)
|
|
26885
|
+
throw new Error("Circular or excessively deep template include graph");
|
|
26886
|
+
seen.add(template.id);
|
|
26887
|
+
try {
|
|
26888
|
+
const resolved = resolveTemplateVariables(template.variables ?? [], variables);
|
|
26889
|
+
for (const step of template.tasks) {
|
|
26890
|
+
if (step.include_template_id) {
|
|
26891
|
+
const included = await resolveRemoteTemplate(client, step.include_template_id);
|
|
26892
|
+
if (!included)
|
|
26893
|
+
throw new Error("Included template not found");
|
|
26894
|
+
await preflightTemplate(client, included, resolved, seen);
|
|
26895
|
+
} else if (step.condition)
|
|
26896
|
+
evaluateTemplateCondition(step.condition, resolved);
|
|
26897
|
+
}
|
|
26898
|
+
} finally {
|
|
26899
|
+
seen.delete(template.id);
|
|
26900
|
+
}
|
|
26901
|
+
}
|
|
26902
|
+
function normalizeTemplateDescription(value) {
|
|
26903
|
+
return value || null;
|
|
26904
|
+
}
|
|
26905
|
+
function previewRemoteTemplate(template, variables) {
|
|
26906
|
+
const resolved = resolveTemplateVariables(template.variables ?? [], variables);
|
|
26907
|
+
const renderDescription = (value) => {
|
|
26908
|
+
if (!value)
|
|
26909
|
+
return null;
|
|
26910
|
+
return substituteTemplateVariables(value, resolved) || null;
|
|
26911
|
+
};
|
|
26912
|
+
if (template.tasks.length === 0) {
|
|
26913
|
+
return {
|
|
26914
|
+
template_id: template.id,
|
|
26915
|
+
template_name: template.name,
|
|
26916
|
+
description: normalizeTemplateDescription(template.description),
|
|
26917
|
+
variables: template.variables,
|
|
26918
|
+
resolved_variables: resolved,
|
|
26919
|
+
tasks: [
|
|
26920
|
+
{
|
|
26921
|
+
position: 0,
|
|
26922
|
+
title: substituteTemplateVariables(template.title_pattern, resolved),
|
|
26923
|
+
description: renderDescription(template.description),
|
|
26924
|
+
priority: template.priority,
|
|
26925
|
+
tags: template.tags,
|
|
26926
|
+
task_type: null,
|
|
26927
|
+
depends_on_positions: []
|
|
26928
|
+
}
|
|
26929
|
+
]
|
|
26930
|
+
};
|
|
26931
|
+
}
|
|
26932
|
+
return {
|
|
26933
|
+
template_id: template.id,
|
|
26934
|
+
template_name: template.name,
|
|
26935
|
+
description: normalizeTemplateDescription(template.description),
|
|
26936
|
+
variables: template.variables,
|
|
26937
|
+
resolved_variables: resolved,
|
|
26938
|
+
tasks: template.tasks.filter((step) => !step.condition || evaluateTemplateCondition(step.condition, resolved)).map((step) => ({
|
|
26939
|
+
position: step.position,
|
|
26940
|
+
title: substituteTemplateVariables(step.title_pattern, resolved),
|
|
26941
|
+
description: renderDescription(step.description),
|
|
26942
|
+
priority: step.priority,
|
|
26943
|
+
tags: step.tags,
|
|
26944
|
+
task_type: step.task_type,
|
|
26945
|
+
depends_on_positions: step.depends_on_positions
|
|
26946
|
+
}))
|
|
26947
|
+
};
|
|
26948
|
+
}
|
|
26949
|
+
function exportRemoteTemplate(template) {
|
|
26950
|
+
return {
|
|
26951
|
+
name: template.name,
|
|
26952
|
+
title_pattern: template.title_pattern,
|
|
26953
|
+
description: normalizeTemplateDescription(template.description),
|
|
26954
|
+
priority: template.priority,
|
|
26955
|
+
tags: template.tags,
|
|
26956
|
+
variables: template.variables,
|
|
26957
|
+
project_id: template.project_id,
|
|
26958
|
+
plan_id: template.plan_id,
|
|
26959
|
+
metadata: template.metadata,
|
|
26960
|
+
tasks: template.tasks.map((step) => ({
|
|
26961
|
+
position: step.position,
|
|
26962
|
+
title_pattern: step.title_pattern,
|
|
26963
|
+
description: normalizeTemplateDescription(step.description),
|
|
26964
|
+
priority: step.priority,
|
|
26965
|
+
tags: step.tags,
|
|
26966
|
+
task_type: step.task_type,
|
|
26967
|
+
condition: step.condition,
|
|
26968
|
+
include_template_id: step.include_template_id,
|
|
26969
|
+
depends_on_positions: step.depends_on_positions,
|
|
26970
|
+
metadata: step.metadata
|
|
26971
|
+
}))
|
|
26972
|
+
};
|
|
26973
|
+
}
|
|
26974
|
+
async function createRemoteTemplateTasks(cloud, template, projectId, variables, options = {}, visited = new Set, progress = {
|
|
26975
|
+
tasks: [],
|
|
26976
|
+
dependencies: [],
|
|
26977
|
+
pending: null
|
|
26978
|
+
}) {
|
|
26979
|
+
if (visited.size === 0)
|
|
26980
|
+
await preflightTemplate(cloud, template, variables);
|
|
26981
|
+
const { actorAgentId, assignedTo, taskListId, overrides } = options;
|
|
26982
|
+
if (visited.has(template.id)) {
|
|
26983
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
26984
|
+
}
|
|
26985
|
+
visited.add(template.id);
|
|
26986
|
+
try {
|
|
26987
|
+
const resolved = resolveTemplateVariables(template.variables ?? [], variables);
|
|
26988
|
+
const render = (value) => value === null || value === undefined ? value : substituteTemplateVariables(value, resolved);
|
|
26989
|
+
if (template.tasks.length === 0) {
|
|
26990
|
+
progress.pending = "task-create";
|
|
26991
|
+
const task = await cloudCreateTask(cloud, {
|
|
26992
|
+
title: render(overrides?.title || template.title_pattern),
|
|
26993
|
+
...render(overrides?.description ?? template.description) ? {
|
|
26994
|
+
description: render(overrides?.description ?? template.description)
|
|
26995
|
+
} : {},
|
|
26996
|
+
priority: overrides?.priority ?? template.priority,
|
|
26997
|
+
tags: template.tags,
|
|
26998
|
+
...projectId ? { project_id: projectId } : {},
|
|
26999
|
+
...taskListId ? { task_list_id: taskListId } : {},
|
|
27000
|
+
...template.plan_id ? { plan_id: template.plan_id } : {},
|
|
27001
|
+
...actorAgentId ? { agent_id: actorAgentId } : {},
|
|
27002
|
+
...assignedTo ? { assigned_to: assignedTo } : {},
|
|
27003
|
+
...Object.keys(template.metadata ?? {}).length > 0 ? { metadata: template.metadata } : {}
|
|
27004
|
+
});
|
|
27005
|
+
progress.tasks.push(task);
|
|
27006
|
+
progress.pending = null;
|
|
27007
|
+
return { tasks: [task] };
|
|
27008
|
+
}
|
|
27009
|
+
const created = [];
|
|
27010
|
+
const positionToTaskId = new Map;
|
|
27011
|
+
const skippedPositions = new Set;
|
|
27012
|
+
for (const step of template.tasks) {
|
|
27013
|
+
if (step.include_template_id) {
|
|
27014
|
+
const included = await resolveRemoteTemplate(cloud, step.include_template_id);
|
|
27015
|
+
if (!included)
|
|
27016
|
+
throw new Error(`Included template not found: ${step.include_template_id}`);
|
|
27017
|
+
const result = await createRemoteTemplateTasks(cloud, included, projectId, resolved, { ...options, overrides: undefined }, visited, progress);
|
|
27018
|
+
created.push(...result.tasks);
|
|
27019
|
+
if (result.tasks.length > 0)
|
|
27020
|
+
positionToTaskId.set(step.position, result.tasks[0].id);
|
|
27021
|
+
else
|
|
27022
|
+
skippedPositions.add(step.position);
|
|
27023
|
+
continue;
|
|
27024
|
+
}
|
|
27025
|
+
if (step.condition && !evaluateTemplateCondition(step.condition, resolved)) {
|
|
27026
|
+
skippedPositions.add(step.position);
|
|
27027
|
+
continue;
|
|
27028
|
+
}
|
|
27029
|
+
progress.pending = "task-create";
|
|
27030
|
+
const task = await cloudCreateTask(cloud, {
|
|
27031
|
+
title: render(step.title_pattern),
|
|
27032
|
+
...render(step.description) ? { description: render(step.description) } : {},
|
|
27033
|
+
priority: step.priority,
|
|
27034
|
+
tags: step.tags,
|
|
27035
|
+
...step.task_type ? { task_type: step.task_type } : {},
|
|
27036
|
+
...projectId ? { project_id: projectId } : {},
|
|
27037
|
+
...taskListId ? { task_list_id: taskListId } : {},
|
|
27038
|
+
...template.plan_id ? { plan_id: template.plan_id } : {},
|
|
27039
|
+
...actorAgentId ? { agent_id: actorAgentId } : {},
|
|
27040
|
+
...assignedTo ? { assigned_to: assignedTo } : {},
|
|
27041
|
+
...Object.keys(step.metadata ?? {}).length > 0 ? { metadata: step.metadata } : {}
|
|
27042
|
+
});
|
|
27043
|
+
progress.tasks.push(task);
|
|
27044
|
+
progress.pending = null;
|
|
27045
|
+
created.push(task);
|
|
27046
|
+
positionToTaskId.set(step.position, task.id);
|
|
27047
|
+
}
|
|
27048
|
+
for (const step of template.tasks) {
|
|
27049
|
+
if (skippedPositions.has(step.position) || step.include_template_id)
|
|
27050
|
+
continue;
|
|
27051
|
+
const taskId = positionToTaskId.get(step.position);
|
|
27052
|
+
if (!taskId)
|
|
27053
|
+
continue;
|
|
27054
|
+
for (const dependencyPosition of step.depends_on_positions) {
|
|
27055
|
+
if (skippedPositions.has(dependencyPosition))
|
|
27056
|
+
continue;
|
|
27057
|
+
const dependencyId = positionToTaskId.get(dependencyPosition);
|
|
27058
|
+
if (dependencyId) {
|
|
27059
|
+
progress.pending = "dependency-add";
|
|
27060
|
+
await cloudAddDependency(cloud, taskId, dependencyId);
|
|
27061
|
+
progress.dependencies.push({
|
|
27062
|
+
task_id: taskId,
|
|
27063
|
+
depends_on: dependencyId
|
|
27064
|
+
});
|
|
27065
|
+
progress.pending = null;
|
|
27066
|
+
}
|
|
27067
|
+
}
|
|
27068
|
+
}
|
|
27069
|
+
return { tasks: created };
|
|
27070
|
+
} catch (error) {
|
|
27071
|
+
if (error instanceof TemplateApplyError)
|
|
27072
|
+
throw error;
|
|
27073
|
+
throw new TemplateApplyError(progress);
|
|
27074
|
+
} finally {
|
|
27075
|
+
visited.delete(template.id);
|
|
27076
|
+
}
|
|
27077
|
+
}
|
|
27078
|
+
var TemplateApplyError;
|
|
27079
|
+
var init_template_remote = __esm(() => {
|
|
27080
|
+
init_cloud_router();
|
|
27081
|
+
TemplateApplyError = class TemplateApplyError extends Error {
|
|
27082
|
+
progress;
|
|
27083
|
+
constructor(progress) {
|
|
27084
|
+
super("Template application did not finish; inspect confirmed tasks before retrying");
|
|
27085
|
+
this.progress = progress;
|
|
27086
|
+
}
|
|
27087
|
+
};
|
|
27088
|
+
});
|
|
27089
|
+
|
|
26274
27090
|
// src/lib/builtin-template-library.ts
|
|
26275
27091
|
var exports_builtin_template_library = {};
|
|
26276
27092
|
__export(exports_builtin_template_library, {
|
|
@@ -27098,26 +27914,6 @@ var init_builtin_template_library = __esm(() => {
|
|
|
27098
27914
|
|
|
27099
27915
|
// src/cli/commands/template-commands.ts
|
|
27100
27916
|
import chalk4 from "chalk";
|
|
27101
|
-
function requireTemplateClient() {
|
|
27102
|
-
assertTemplateApiEnvironment(process.env);
|
|
27103
|
-
const client = getTodosCloudClient();
|
|
27104
|
-
if (!client)
|
|
27105
|
-
throw new Error("Templates require HASNA_TODOS_API_URL and HASNA_TODOS_API_KEY, or saved account credentials");
|
|
27106
|
-
return client;
|
|
27107
|
-
}
|
|
27108
|
-
async function cloudGetTemplate2(client, ref) {
|
|
27109
|
-
if (!ref.trim())
|
|
27110
|
-
throw new Error("Blank template ID prefix");
|
|
27111
|
-
const direct = await cloudGetTemplate(client, ref);
|
|
27112
|
-
if (direct?.id === ref)
|
|
27113
|
-
return direct;
|
|
27114
|
-
const rows = await listSharedTemplates(client);
|
|
27115
|
-
const exact = rows.find((row) => row.id === ref);
|
|
27116
|
-
const matches = exact ? [exact] : rows.filter((row) => row.id.startsWith(ref));
|
|
27117
|
-
if (!ref.trim() || matches.length > 1)
|
|
27118
|
-
throw new Error("Ambiguous or blank template ID prefix");
|
|
27119
|
-
return matches[0] ? cloudGetTemplate(client, matches[0].id) : null;
|
|
27120
|
-
}
|
|
27121
27917
|
function reportTemplateError(error, jsonMode) {
|
|
27122
27918
|
if (!(error instanceof TemplateApplyError)) {
|
|
27123
27919
|
handleError(error);
|
|
@@ -27133,196 +27929,6 @@ function reportTemplateError(error, jsonMode) {
|
|
|
27133
27929
|
}, jsonMode);
|
|
27134
27930
|
process.exitCode = 1;
|
|
27135
27931
|
}
|
|
27136
|
-
async function preflightTemplate(client, template, variables, seen = new Set) {
|
|
27137
|
-
if (seen.has(template.id) || seen.size >= 32)
|
|
27138
|
-
throw new Error("Circular or excessively deep template include graph");
|
|
27139
|
-
seen.add(template.id);
|
|
27140
|
-
try {
|
|
27141
|
-
const resolved = resolveTemplateVariables(template.variables ?? [], variables);
|
|
27142
|
-
for (const step of template.tasks) {
|
|
27143
|
-
if (step.include_template_id) {
|
|
27144
|
-
const included = await cloudGetTemplate2(client, step.include_template_id);
|
|
27145
|
-
if (!included)
|
|
27146
|
-
throw new Error("Included template not found");
|
|
27147
|
-
await preflightTemplate(client, included, resolved, seen);
|
|
27148
|
-
} else if (step.condition)
|
|
27149
|
-
evaluateTemplateCondition(step.condition, resolved);
|
|
27150
|
-
}
|
|
27151
|
-
} finally {
|
|
27152
|
-
seen.delete(template.id);
|
|
27153
|
-
}
|
|
27154
|
-
}
|
|
27155
|
-
function normalizeTemplateDescription(value) {
|
|
27156
|
-
return value || null;
|
|
27157
|
-
}
|
|
27158
|
-
function previewRemoteTemplate(template, variables) {
|
|
27159
|
-
const resolved = resolveTemplateVariables(template.variables ?? [], variables);
|
|
27160
|
-
const renderDescription = (value) => {
|
|
27161
|
-
if (!value)
|
|
27162
|
-
return null;
|
|
27163
|
-
return substituteTemplateVariables(value, resolved) || null;
|
|
27164
|
-
};
|
|
27165
|
-
if (template.tasks.length === 0) {
|
|
27166
|
-
return {
|
|
27167
|
-
template_id: template.id,
|
|
27168
|
-
template_name: template.name,
|
|
27169
|
-
description: normalizeTemplateDescription(template.description),
|
|
27170
|
-
variables: template.variables,
|
|
27171
|
-
resolved_variables: resolved,
|
|
27172
|
-
tasks: [
|
|
27173
|
-
{
|
|
27174
|
-
position: 0,
|
|
27175
|
-
title: substituteTemplateVariables(template.title_pattern, resolved),
|
|
27176
|
-
description: renderDescription(template.description),
|
|
27177
|
-
priority: template.priority,
|
|
27178
|
-
tags: template.tags,
|
|
27179
|
-
task_type: null,
|
|
27180
|
-
depends_on_positions: []
|
|
27181
|
-
}
|
|
27182
|
-
]
|
|
27183
|
-
};
|
|
27184
|
-
}
|
|
27185
|
-
return {
|
|
27186
|
-
template_id: template.id,
|
|
27187
|
-
template_name: template.name,
|
|
27188
|
-
description: normalizeTemplateDescription(template.description),
|
|
27189
|
-
variables: template.variables,
|
|
27190
|
-
resolved_variables: resolved,
|
|
27191
|
-
tasks: template.tasks.filter((step) => !step.condition || evaluateTemplateCondition(step.condition, resolved)).map((step) => ({
|
|
27192
|
-
position: step.position,
|
|
27193
|
-
title: substituteTemplateVariables(step.title_pattern, resolved),
|
|
27194
|
-
description: renderDescription(step.description),
|
|
27195
|
-
priority: step.priority,
|
|
27196
|
-
tags: step.tags,
|
|
27197
|
-
task_type: step.task_type,
|
|
27198
|
-
depends_on_positions: step.depends_on_positions
|
|
27199
|
-
}))
|
|
27200
|
-
};
|
|
27201
|
-
}
|
|
27202
|
-
function exportRemoteTemplate(template) {
|
|
27203
|
-
return {
|
|
27204
|
-
name: template.name,
|
|
27205
|
-
title_pattern: template.title_pattern,
|
|
27206
|
-
description: normalizeTemplateDescription(template.description),
|
|
27207
|
-
priority: template.priority,
|
|
27208
|
-
tags: template.tags,
|
|
27209
|
-
variables: template.variables,
|
|
27210
|
-
project_id: template.project_id,
|
|
27211
|
-
plan_id: template.plan_id,
|
|
27212
|
-
metadata: template.metadata,
|
|
27213
|
-
tasks: template.tasks.map((step) => ({
|
|
27214
|
-
position: step.position,
|
|
27215
|
-
title_pattern: step.title_pattern,
|
|
27216
|
-
description: normalizeTemplateDescription(step.description),
|
|
27217
|
-
priority: step.priority,
|
|
27218
|
-
tags: step.tags,
|
|
27219
|
-
task_type: step.task_type,
|
|
27220
|
-
condition: step.condition,
|
|
27221
|
-
include_template_id: step.include_template_id,
|
|
27222
|
-
depends_on_positions: step.depends_on_positions,
|
|
27223
|
-
metadata: step.metadata
|
|
27224
|
-
}))
|
|
27225
|
-
};
|
|
27226
|
-
}
|
|
27227
|
-
async function createRemoteTemplateTasks(cloud, template, projectId, variables, agentId, overrides, visited = new Set, progress = {
|
|
27228
|
-
tasks: [],
|
|
27229
|
-
dependencies: [],
|
|
27230
|
-
pending: null
|
|
27231
|
-
}) {
|
|
27232
|
-
if (visited.size === 0)
|
|
27233
|
-
await preflightTemplate(cloud, template, variables);
|
|
27234
|
-
if (visited.has(template.id)) {
|
|
27235
|
-
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
27236
|
-
}
|
|
27237
|
-
visited.add(template.id);
|
|
27238
|
-
try {
|
|
27239
|
-
const resolved = resolveTemplateVariables(template.variables ?? [], variables);
|
|
27240
|
-
const render = (value) => value === null || value === undefined ? value : substituteTemplateVariables(value, resolved);
|
|
27241
|
-
if (template.tasks.length === 0) {
|
|
27242
|
-
progress.pending = "task-create";
|
|
27243
|
-
const task = await cloudCreateTask(cloud, {
|
|
27244
|
-
title: render(overrides?.title || template.title_pattern),
|
|
27245
|
-
...render(overrides?.description ?? template.description) ? {
|
|
27246
|
-
description: render(overrides?.description ?? template.description)
|
|
27247
|
-
} : {},
|
|
27248
|
-
priority: overrides?.priority ?? template.priority,
|
|
27249
|
-
tags: template.tags,
|
|
27250
|
-
...projectId ? { project_id: projectId } : {},
|
|
27251
|
-
...template.plan_id ? { plan_id: template.plan_id } : {},
|
|
27252
|
-
...agentId ? { agent_id: agentId } : {},
|
|
27253
|
-
...Object.keys(template.metadata ?? {}).length > 0 ? { metadata: template.metadata } : {}
|
|
27254
|
-
});
|
|
27255
|
-
progress.tasks.push(task);
|
|
27256
|
-
progress.pending = null;
|
|
27257
|
-
return { tasks: [task] };
|
|
27258
|
-
}
|
|
27259
|
-
const created = [];
|
|
27260
|
-
const positionToTaskId = new Map;
|
|
27261
|
-
const skippedPositions = new Set;
|
|
27262
|
-
for (const step of template.tasks) {
|
|
27263
|
-
if (step.include_template_id) {
|
|
27264
|
-
const included = await cloudGetTemplate2(cloud, step.include_template_id);
|
|
27265
|
-
if (!included)
|
|
27266
|
-
throw new Error(`Included template not found: ${step.include_template_id}`);
|
|
27267
|
-
const result = await createRemoteTemplateTasks(cloud, included, projectId, resolved, agentId, undefined, visited, progress);
|
|
27268
|
-
created.push(...result.tasks);
|
|
27269
|
-
if (result.tasks.length > 0)
|
|
27270
|
-
positionToTaskId.set(step.position, result.tasks[0].id);
|
|
27271
|
-
else
|
|
27272
|
-
skippedPositions.add(step.position);
|
|
27273
|
-
continue;
|
|
27274
|
-
}
|
|
27275
|
-
if (step.condition && !evaluateTemplateCondition(step.condition, resolved)) {
|
|
27276
|
-
skippedPositions.add(step.position);
|
|
27277
|
-
continue;
|
|
27278
|
-
}
|
|
27279
|
-
progress.pending = "task-create";
|
|
27280
|
-
const task = await cloudCreateTask(cloud, {
|
|
27281
|
-
title: render(step.title_pattern),
|
|
27282
|
-
...render(step.description) ? { description: render(step.description) } : {},
|
|
27283
|
-
priority: step.priority,
|
|
27284
|
-
tags: step.tags,
|
|
27285
|
-
...step.task_type ? { task_type: step.task_type } : {},
|
|
27286
|
-
...projectId ? { project_id: projectId } : {},
|
|
27287
|
-
...template.plan_id ? { plan_id: template.plan_id } : {},
|
|
27288
|
-
...agentId ? { agent_id: agentId } : {},
|
|
27289
|
-
...Object.keys(step.metadata ?? {}).length > 0 ? { metadata: step.metadata } : {}
|
|
27290
|
-
});
|
|
27291
|
-
progress.tasks.push(task);
|
|
27292
|
-
progress.pending = null;
|
|
27293
|
-
created.push(task);
|
|
27294
|
-
positionToTaskId.set(step.position, task.id);
|
|
27295
|
-
}
|
|
27296
|
-
for (const step of template.tasks) {
|
|
27297
|
-
if (skippedPositions.has(step.position) || step.include_template_id)
|
|
27298
|
-
continue;
|
|
27299
|
-
const taskId = positionToTaskId.get(step.position);
|
|
27300
|
-
if (!taskId)
|
|
27301
|
-
continue;
|
|
27302
|
-
for (const dependencyPosition of step.depends_on_positions) {
|
|
27303
|
-
if (skippedPositions.has(dependencyPosition))
|
|
27304
|
-
continue;
|
|
27305
|
-
const dependencyId = positionToTaskId.get(dependencyPosition);
|
|
27306
|
-
if (dependencyId) {
|
|
27307
|
-
progress.pending = "dependency-add";
|
|
27308
|
-
await cloudAddDependency(cloud, taskId, dependencyId);
|
|
27309
|
-
progress.dependencies.push({
|
|
27310
|
-
task_id: taskId,
|
|
27311
|
-
depends_on: dependencyId
|
|
27312
|
-
});
|
|
27313
|
-
progress.pending = null;
|
|
27314
|
-
}
|
|
27315
|
-
}
|
|
27316
|
-
}
|
|
27317
|
-
return { tasks: created };
|
|
27318
|
-
} catch (error) {
|
|
27319
|
-
if (error instanceof TemplateApplyError)
|
|
27320
|
-
throw error;
|
|
27321
|
-
throw new TemplateApplyError(progress);
|
|
27322
|
-
} finally {
|
|
27323
|
-
visited.delete(template.id);
|
|
27324
|
-
}
|
|
27325
|
-
}
|
|
27326
27932
|
function registerTemplateCommands(program2) {
|
|
27327
27933
|
program2.command("templates").description("List and manage task templates").option("--add <name>", "Create a template").option("--title <pattern>", "Title pattern (with --add)").option("-d, --description <text>", "Default description").option("-p, --priority <level>", "Default priority").option("-t, --tags <tags>", "Default tags (comma-separated)").option("--delete <id>", "Delete a template").option("--update <id>", "Update a template").option("--use <id>", "Create a task from a template").option("--var <vars...>", "Variable substitutions: key=value (e.g. --var feature=login)").action(async (opts) => {
|
|
27328
27934
|
const globalOpts = program2.opts();
|
|
@@ -27350,7 +27956,7 @@ function registerTemplateCommands(program2) {
|
|
|
27350
27956
|
return;
|
|
27351
27957
|
}
|
|
27352
27958
|
if (opts.delete) {
|
|
27353
|
-
const prior = await
|
|
27959
|
+
const prior = await resolveRemoteTemplate(cloud, opts.delete);
|
|
27354
27960
|
const deleted = prior ? await cloudDeleteTemplate(cloud, prior.id) : false;
|
|
27355
27961
|
if (globalOpts.json) {
|
|
27356
27962
|
output({ deleted }, true);
|
|
@@ -27374,7 +27980,7 @@ function registerTemplateCommands(program2) {
|
|
|
27374
27980
|
if (Object.keys(updates).length === 0) {
|
|
27375
27981
|
handleError(new Error("Provide --title, --description, --priority, or --tags with --update"));
|
|
27376
27982
|
}
|
|
27377
|
-
const prior = await
|
|
27983
|
+
const prior = await resolveRemoteTemplate(cloud, opts.update);
|
|
27378
27984
|
if (!prior)
|
|
27379
27985
|
handleError(new Error("Template not found."));
|
|
27380
27986
|
const updated = await updateSharedTemplate(cloud, prior.id, {
|
|
@@ -27400,15 +28006,18 @@ function registerTemplateCommands(program2) {
|
|
|
27400
28006
|
}
|
|
27401
28007
|
variables[value.slice(0, separator)] = value.slice(separator + 1);
|
|
27402
28008
|
}
|
|
27403
|
-
const template = await
|
|
28009
|
+
const template = await resolveRemoteTemplate(cloud, opts.use);
|
|
27404
28010
|
if (!template) {
|
|
27405
28011
|
handleError(new Error("Template not found."));
|
|
27406
28012
|
}
|
|
27407
28013
|
const targetProjectId = template.project_id ?? projectId;
|
|
27408
|
-
const { tasks: created } = await createRemoteTemplateTasks(cloud, template, targetProjectId, variables,
|
|
27409
|
-
|
|
27410
|
-
|
|
27411
|
-
|
|
28014
|
+
const { tasks: created } = await createRemoteTemplateTasks(cloud, template, targetProjectId, variables, {
|
|
28015
|
+
actorAgentId: globalOpts.agent,
|
|
28016
|
+
overrides: {
|
|
28017
|
+
title: opts.title,
|
|
28018
|
+
description: opts.description,
|
|
28019
|
+
priority: opts.priority
|
|
28020
|
+
}
|
|
27412
28021
|
});
|
|
27413
28022
|
if (globalOpts.json) {
|
|
27414
28023
|
output(created, true);
|
|
@@ -27515,7 +28124,7 @@ function registerTemplateCommands(program2) {
|
|
|
27515
28124
|
const cloud = requireTemplateClient();
|
|
27516
28125
|
let result;
|
|
27517
28126
|
if (cloud) {
|
|
27518
|
-
const template = await
|
|
28127
|
+
const template = await resolveRemoteTemplate(cloud, id);
|
|
27519
28128
|
if (!template) {
|
|
27520
28129
|
handleError(new Error("Template not found."));
|
|
27521
28130
|
}
|
|
@@ -27548,7 +28157,7 @@ function registerTemplateCommands(program2) {
|
|
|
27548
28157
|
program2.command("template-export <id>").alias("templates-export").description("Export a template as JSON to stdout").action(async (id) => {
|
|
27549
28158
|
try {
|
|
27550
28159
|
const cloud = requireTemplateClient();
|
|
27551
|
-
const template = cloud ? await
|
|
28160
|
+
const template = cloud ? await resolveRemoteTemplate(cloud, id) : null;
|
|
27552
28161
|
if (cloud && !template) {
|
|
27553
28162
|
handleError(new Error("Template not found."));
|
|
27554
28163
|
}
|
|
@@ -27583,7 +28192,7 @@ function registerTemplateCommands(program2) {
|
|
|
27583
28192
|
const globalOpts = program2.opts();
|
|
27584
28193
|
const cloud = requireTemplateClient();
|
|
27585
28194
|
try {
|
|
27586
|
-
const template = await
|
|
28195
|
+
const template = await resolveRemoteTemplate(cloud, id);
|
|
27587
28196
|
if (!template) {
|
|
27588
28197
|
handleError(new Error("Template not found."));
|
|
27589
28198
|
}
|
|
@@ -27614,17 +28223,10 @@ function registerTemplateCommands(program2) {
|
|
|
27614
28223
|
}
|
|
27615
28224
|
});
|
|
27616
28225
|
}
|
|
27617
|
-
var TemplateApplyError;
|
|
27618
28226
|
var init_template_commands = __esm(() => {
|
|
27619
28227
|
init_helpers();
|
|
27620
28228
|
init_cloud_router();
|
|
27621
|
-
|
|
27622
|
-
progress;
|
|
27623
|
-
constructor(progress) {
|
|
27624
|
-
super("Template application did not finish; inspect confirmed tasks before retrying");
|
|
27625
|
-
this.progress = progress;
|
|
27626
|
-
}
|
|
27627
|
-
};
|
|
28229
|
+
init_template_remote();
|
|
27628
28230
|
});
|
|
27629
28231
|
|
|
27630
28232
|
// src/cli/commands/plan-template-commands.ts
|
|
@@ -30075,6 +30677,102 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
30075
30677
|
});
|
|
30076
30678
|
|
|
30077
30679
|
// src/storage/local-sqlite.ts
|
|
30680
|
+
var exports_local_sqlite = {};
|
|
30681
|
+
__export(exports_local_sqlite, {
|
|
30682
|
+
createLocalSqliteTodosStorageAdapter: () => createLocalSqliteTodosStorageAdapter
|
|
30683
|
+
});
|
|
30684
|
+
function bulkCreateAtomicSqlite(db, inputs) {
|
|
30685
|
+
return db.transaction(() => {
|
|
30686
|
+
const tempIds = new Map;
|
|
30687
|
+
const created = [];
|
|
30688
|
+
const dependencies = [];
|
|
30689
|
+
for (const input of inputs) {
|
|
30690
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
30691
|
+
const task = createTask(taskInput, db);
|
|
30692
|
+
if (temp_id)
|
|
30693
|
+
tempIds.set(temp_id, task.id);
|
|
30694
|
+
created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
|
|
30695
|
+
}
|
|
30696
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
30697
|
+
const input = inputs[index];
|
|
30698
|
+
const taskId = created[index].id;
|
|
30699
|
+
const seenDependencies = new Set;
|
|
30700
|
+
for (const reference of input.depends_on ?? []) {
|
|
30701
|
+
const dependencyId = tempIds.get(reference) ?? resolveTaskRefLocal(db, reference)?.id;
|
|
30702
|
+
if (!dependencyId)
|
|
30703
|
+
throw new TaskNotFoundError(reference);
|
|
30704
|
+
if (seenDependencies.has(dependencyId)) {
|
|
30705
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
|
|
30706
|
+
}
|
|
30707
|
+
seenDependencies.add(dependencyId);
|
|
30708
|
+
addDependency(taskId, dependencyId, db);
|
|
30709
|
+
dependencies.push({ task_id: taskId, depends_on: dependencyId });
|
|
30710
|
+
}
|
|
30711
|
+
}
|
|
30712
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
30713
|
+
})();
|
|
30714
|
+
}
|
|
30715
|
+
function deleteTaskHierarchySqlite(db, rootId) {
|
|
30716
|
+
const rows = db.query(`WITH RECURSIVE tree(id, depth) AS (
|
|
30717
|
+
SELECT id, 0 FROM tasks WHERE id = ?
|
|
30718
|
+
UNION ALL
|
|
30719
|
+
SELECT child.id, tree.depth + 1 FROM tasks child JOIN tree ON child.parent_id = tree.id
|
|
30720
|
+
) SELECT id FROM tree ORDER BY depth DESC, id`).all(rootId);
|
|
30721
|
+
if (rows.length === 0)
|
|
30722
|
+
throw new TaskNotFoundError(rootId);
|
|
30723
|
+
for (const row of rows) {
|
|
30724
|
+
if (!deleteTask(row.id, db))
|
|
30725
|
+
throw new Error(`Atomic hierarchy delete lost task ${row.id}`);
|
|
30726
|
+
}
|
|
30727
|
+
}
|
|
30728
|
+
function bulkDeleteAtomicSqlite(db, ids, force) {
|
|
30729
|
+
return db.transaction(() => {
|
|
30730
|
+
const resolved = ids.map((reference) => ({ reference, task: resolveTaskRefLocal(db, reference) }));
|
|
30731
|
+
const seen = new Set;
|
|
30732
|
+
for (const item of resolved) {
|
|
30733
|
+
if (!item.task)
|
|
30734
|
+
continue;
|
|
30735
|
+
if (seen.has(item.task.id)) {
|
|
30736
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
30737
|
+
}
|
|
30738
|
+
seen.add(item.task.id);
|
|
30739
|
+
}
|
|
30740
|
+
const childState = new Map;
|
|
30741
|
+
for (const item of resolved) {
|
|
30742
|
+
if (!item.task)
|
|
30743
|
+
continue;
|
|
30744
|
+
childState.set(item.task.id, Boolean(db.query("SELECT id FROM tasks WHERE parent_id = ? LIMIT 1").get(item.task.id)));
|
|
30745
|
+
}
|
|
30746
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
30747
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
30748
|
+
const roots = planned.filter((item) => {
|
|
30749
|
+
if (!force)
|
|
30750
|
+
return true;
|
|
30751
|
+
let parentId = item.task.parent_id;
|
|
30752
|
+
while (parentId) {
|
|
30753
|
+
if (plannedIds.has(parentId))
|
|
30754
|
+
return false;
|
|
30755
|
+
parentId = getTask(parentId, db)?.parent_id ?? null;
|
|
30756
|
+
}
|
|
30757
|
+
return true;
|
|
30758
|
+
});
|
|
30759
|
+
for (const item of roots)
|
|
30760
|
+
deleteTaskHierarchySqlite(db, item.task.id);
|
|
30761
|
+
return {
|
|
30762
|
+
schema_version: 1,
|
|
30763
|
+
atomic: true,
|
|
30764
|
+
force,
|
|
30765
|
+
results: resolved.map((item) => {
|
|
30766
|
+
if (!item.task)
|
|
30767
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
30768
|
+
if (!force && childState.get(item.task.id)) {
|
|
30769
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
30770
|
+
}
|
|
30771
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
30772
|
+
})
|
|
30773
|
+
};
|
|
30774
|
+
})();
|
|
30775
|
+
}
|
|
30078
30776
|
function resolveTaskRefLocal(db, ref) {
|
|
30079
30777
|
const raw = ref.trim().toLowerCase();
|
|
30080
30778
|
if (!raw)
|
|
@@ -30170,6 +30868,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
30170
30868
|
},
|
|
30171
30869
|
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
30172
30870
|
delete: (id) => deleteTask(id, database()),
|
|
30871
|
+
bulkCreateAtomic: (inputs, _context) => bulkCreateAtomicSqlite(database(), inputs),
|
|
30872
|
+
bulkDeleteAtomic: (ids, force, _context) => bulkDeleteAtomicSqlite(database(), ids, force),
|
|
30173
30873
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
30174
30874
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
30175
30875
|
fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, database()),
|
|
@@ -30178,6 +30878,30 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
30178
30878
|
getActiveWork: (filters) => getActiveWork(filters, database()),
|
|
30179
30879
|
getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
|
|
30180
30880
|
},
|
|
30881
|
+
dependencies: {
|
|
30882
|
+
add: (taskId, dependsOn) => {
|
|
30883
|
+
addDependency(taskId, dependsOn, database());
|
|
30884
|
+
return { task_id: taskId, depends_on: dependsOn };
|
|
30885
|
+
},
|
|
30886
|
+
remove: (taskId, dependsOn) => removeDependency(taskId, dependsOn, database()),
|
|
30887
|
+
list: (taskId) => {
|
|
30888
|
+
const dependencies = getTaskDependencies(taskId, database());
|
|
30889
|
+
const blocks = getTaskDependents(taskId, database());
|
|
30890
|
+
return { dependencies, blocks, blocked_by: blocks };
|
|
30891
|
+
},
|
|
30892
|
+
listPage: ({ limit, offset }) => {
|
|
30893
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
30894
|
+
throw new Error("SQLite dependency page limit must be an integer from 1 to 500");
|
|
30895
|
+
}
|
|
30896
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
30897
|
+
throw new Error("SQLite dependency page offset must be a non-negative integer");
|
|
30898
|
+
}
|
|
30899
|
+
const totalRow = database().query("SELECT COUNT(*) AS total FROM task_dependencies").get();
|
|
30900
|
+
const dependencies = database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on LIMIT ? OFFSET ?").all(limit, offset);
|
|
30901
|
+
return { dependencies, total: totalRow.total };
|
|
30902
|
+
},
|
|
30903
|
+
listAll: () => database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on").all()
|
|
30904
|
+
},
|
|
30181
30905
|
projects: {
|
|
30182
30906
|
create: (input) => createProject(input, database()),
|
|
30183
30907
|
get: (id) => getProject(id, database()),
|
|
@@ -30221,6 +30945,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
30221
30945
|
get: (id) => getAgent(id, database()),
|
|
30222
30946
|
getByName: (name) => getAgentByName(name, database()),
|
|
30223
30947
|
list: (options2) => listAgents(options2, database()),
|
|
30948
|
+
listPage: (options2) => listAgentsPage(options2, database()),
|
|
30224
30949
|
update: (id, input) => updateAgent(id, input, database())
|
|
30225
30950
|
},
|
|
30226
30951
|
taskLists: {
|
|
@@ -30259,6 +30984,29 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
30259
30984
|
return comments;
|
|
30260
30985
|
},
|
|
30261
30986
|
getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
|
|
30987
|
+
getTaskHistoryPage: (taskId, options2) => {
|
|
30988
|
+
if (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 500) {
|
|
30989
|
+
throw new Error("Task history limit must be an integer from 1 to 500");
|
|
30990
|
+
}
|
|
30991
|
+
if (!Number.isSafeInteger(options2.offset) || options2.offset < 0) {
|
|
30992
|
+
throw new Error("Task history offset must be a non-negative integer");
|
|
30993
|
+
}
|
|
30994
|
+
const conditions = ["task_id = ?"];
|
|
30995
|
+
const values = [taskId];
|
|
30996
|
+
if (options2.since) {
|
|
30997
|
+
conditions.push("created_at >= ?");
|
|
30998
|
+
values.push(options2.since);
|
|
30999
|
+
}
|
|
31000
|
+
if (options2.until) {
|
|
31001
|
+
conditions.push("created_at <= ?");
|
|
31002
|
+
values.push(options2.until);
|
|
31003
|
+
}
|
|
31004
|
+
const where = conditions.join(" AND ");
|
|
31005
|
+
const total = database().query(`SELECT COUNT(*) AS total FROM task_history WHERE ${where}`).get(...values).total;
|
|
31006
|
+
const direction = options2.order === "asc" ? "ASC" : "DESC";
|
|
31007
|
+
const history = database().query(`SELECT * FROM task_history WHERE ${where} ORDER BY created_at ${direction}, id ${direction} LIMIT ? OFFSET ?`).all(...values, options2.limit, options2.offset);
|
|
31008
|
+
return { history, total };
|
|
31009
|
+
},
|
|
30262
31010
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
30263
31011
|
},
|
|
30264
31012
|
sync: {
|
|
@@ -30281,6 +31029,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
30281
31029
|
init_types();
|
|
30282
31030
|
init_search();
|
|
30283
31031
|
init_tasks();
|
|
31032
|
+
init_task_graph();
|
|
30284
31033
|
init_projects();
|
|
30285
31034
|
init_plans();
|
|
30286
31035
|
init_plan_comments();
|
|
@@ -34755,6 +35504,8 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
34755
35504
|
count: (filter = {}) => store.countTasks(filter),
|
|
34756
35505
|
update: (id, input, context) => updateTask2(id, input, store, context),
|
|
34757
35506
|
delete: (id, context) => store.deleteTaskHierarchy(id, context),
|
|
35507
|
+
bulkCreateAtomic: (inputs, context) => bulkCreateTasksAtomic(inputs, store, context),
|
|
35508
|
+
bulkDeleteAtomic: (ids, force, context) => bulkDeleteTasksAtomic(ids, force, store, context),
|
|
34758
35509
|
start: (id, agentId) => startTask2(id, agentId, store),
|
|
34759
35510
|
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
34760
35511
|
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
@@ -34771,6 +35522,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
34771
35522
|
add: (taskId, dependsOn, context) => addDependency2(taskId, dependsOn, store, context),
|
|
34772
35523
|
remove: (taskId, dependsOn) => removeDependency2(taskId, dependsOn, store),
|
|
34773
35524
|
list: (taskId) => listDependencies(taskId, store),
|
|
35525
|
+
listPage: (page) => store.listDependencyPage(page),
|
|
34774
35526
|
listAll: () => store.list("dependencies")
|
|
34775
35527
|
},
|
|
34776
35528
|
verifications: {
|
|
@@ -34836,6 +35588,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
34836
35588
|
get: (id) => store.get("agents", id),
|
|
34837
35589
|
getByName: async (name) => matchAgentByName(await store.list("agents"), name),
|
|
34838
35590
|
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
35591
|
+
listPage: (options2) => store.listAgentPage(options2),
|
|
34839
35592
|
update: (id, input) => updateAgent2(id, input, store),
|
|
34840
35593
|
heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
|
|
34841
35594
|
release: (idOrName, sessionId, context) => releaseAgent2(idOrName, sessionId, store, context)
|
|
@@ -34893,8 +35646,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
34893
35646
|
getCommentsPage: async (taskId, options2) => {
|
|
34894
35647
|
return (await store.listComments(taskId, options2)).map(redactComment2).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
34895
35648
|
},
|
|
34896
|
-
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) =>
|
|
34897
|
-
|
|
35649
|
+
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id)),
|
|
35650
|
+
getTaskHistoryPage: (taskId, options2) => store.listTaskHistoryPage(taskId, options2),
|
|
35651
|
+
getRecentActivity: (limit = 20) => store.listRecentActivity(limit)
|
|
34898
35652
|
},
|
|
34899
35653
|
machines: createPostgresMachineRegistry(options.client, options.service ?? "todos", options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, () => store.ensureSchema()),
|
|
34900
35654
|
atomicProjectMigration: createAtomicProjectMigration({ client: options.client, table: options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, service: options.service ?? "todos", ensureSchema: () => store.ensureSchema() }),
|
|
@@ -34931,6 +35685,8 @@ class PostgresJsonRecordStore {
|
|
|
34931
35685
|
}
|
|
34932
35686
|
async withDependencyGraphTransaction(fn) {
|
|
34933
35687
|
await this.ensureSchema();
|
|
35688
|
+
if (this.projectIntegrityLocked)
|
|
35689
|
+
return fn(this);
|
|
34934
35690
|
return this.withTaskParentIntegrityTransaction(async (client) => {
|
|
34935
35691
|
const scoped = new PostgresJsonRecordStore({ ...this.options, client });
|
|
34936
35692
|
scoped.schemaReady = Promise.resolve();
|
|
@@ -35077,6 +35833,128 @@ class PostgresJsonRecordStore {
|
|
|
35077
35833
|
async list(type) {
|
|
35078
35834
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
35079
35835
|
}
|
|
35836
|
+
async listAgentPage(options) {
|
|
35837
|
+
const { limit, offset } = options;
|
|
35838
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
35839
|
+
throw new Error("Postgres agent page limit must be an integer from 1 to 500");
|
|
35840
|
+
}
|
|
35841
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
35842
|
+
throw new Error("Postgres agent page offset must be a non-negative integer");
|
|
35843
|
+
}
|
|
35844
|
+
await this.ensureSchema();
|
|
35845
|
+
const result = await this.options.client.query(`/* todos:list-agent-page */ WITH filtered AS (
|
|
35846
|
+
SELECT object_id, payload
|
|
35847
|
+
FROM ${this.tableName}
|
|
35848
|
+
WHERE service = $1 AND object_type = 'agents' AND deleted_at IS NULL
|
|
35849
|
+
AND ($2::boolean OR COALESCE(payload->>'status', 'active') <> 'archived')
|
|
35850
|
+
), page AS (
|
|
35851
|
+
SELECT object_id, payload FROM filtered
|
|
35852
|
+
ORDER BY LOWER(payload->>'name'), object_id
|
|
35853
|
+
LIMIT $3 OFFSET $4
|
|
35854
|
+
)
|
|
35855
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
35856
|
+
COALESCE((SELECT jsonb_agg(payload ORDER BY LOWER(payload->>'name'), object_id) FROM page), '[]'::jsonb) AS agents`, [this.service, options.include_archived === true, limit, offset]);
|
|
35857
|
+
const row = result.rows[0];
|
|
35858
|
+
const total = Number(row?.total);
|
|
35859
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.agents)) {
|
|
35860
|
+
throw new Error("Postgres agent page returned invalid count or agents");
|
|
35861
|
+
}
|
|
35862
|
+
if (row.agents.length > limit || row.agents.length > 0 && offset + row.agents.length > total) {
|
|
35863
|
+
throw new Error("Postgres agent page exceeded its requested bounds");
|
|
35864
|
+
}
|
|
35865
|
+
return { agents: row.agents.map((value) => payloadRecord2(value)), total };
|
|
35866
|
+
}
|
|
35867
|
+
async listRecentActivity(limit) {
|
|
35868
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
35869
|
+
throw new Error("Postgres recent activity limit must be an integer from 1 to 10000");
|
|
35870
|
+
}
|
|
35871
|
+
await this.ensureSchema();
|
|
35872
|
+
const result = await this.options.client.query(`/* todos:list-recent-activity */ SELECT payload
|
|
35873
|
+
FROM ${this.tableName}
|
|
35874
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
35875
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
35876
|
+
LIMIT $2`, [this.service, limit]);
|
|
35877
|
+
return result.rows.map((row) => payloadRecord2(row.payload));
|
|
35878
|
+
}
|
|
35879
|
+
async listTaskHistoryPage(taskId, options) {
|
|
35880
|
+
const { limit, offset } = options;
|
|
35881
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
35882
|
+
throw new Error("Postgres task history limit must be an integer from 1 to 500");
|
|
35883
|
+
}
|
|
35884
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
35885
|
+
throw new Error("Postgres task history offset must be a non-negative integer");
|
|
35886
|
+
}
|
|
35887
|
+
await this.ensureSchema();
|
|
35888
|
+
const direction = options.order === "asc" ? "ASC" : "DESC";
|
|
35889
|
+
const result = await this.options.client.query(`/* todos:list-task-history-page */ WITH filtered AS (
|
|
35890
|
+
SELECT object_id, payload, payload->>'created_at' AS created_at
|
|
35891
|
+
FROM ${this.tableName}
|
|
35892
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
35893
|
+
AND payload->>'task_id' = $2
|
|
35894
|
+
AND ($3::text IS NULL OR payload->>'created_at' >= $3)
|
|
35895
|
+
AND ($4::text IS NULL OR payload->>'created_at' <= $4)
|
|
35896
|
+
), page AS (
|
|
35897
|
+
SELECT object_id, payload, created_at FROM filtered
|
|
35898
|
+
ORDER BY created_at ${direction}, object_id ${direction}
|
|
35899
|
+
LIMIT $5 OFFSET $6
|
|
35900
|
+
)
|
|
35901
|
+
SELECT (SELECT count(*) FROM filtered) AS total,
|
|
35902
|
+
COALESCE((SELECT jsonb_agg(payload ORDER BY created_at ${direction}, object_id ${direction}) FROM page), '[]'::jsonb) AS history`, [this.service, taskId, options.since ?? null, options.until ?? null, limit, offset]);
|
|
35903
|
+
const row = result.rows[0];
|
|
35904
|
+
const total = Number(row?.total);
|
|
35905
|
+
if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.history)) {
|
|
35906
|
+
throw new Error("Postgres task history page returned invalid count or history");
|
|
35907
|
+
}
|
|
35908
|
+
if (row.history.length > limit || row.history.length > 0 && offset + row.history.length > total) {
|
|
35909
|
+
throw new Error("Postgres task history page exceeded its requested bounds");
|
|
35910
|
+
}
|
|
35911
|
+
return { history: row.history.map((value) => payloadRecord2(value)), total };
|
|
35912
|
+
}
|
|
35913
|
+
async listDependencyPage(options) {
|
|
35914
|
+
const { limit, offset } = options;
|
|
35915
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
35916
|
+
throw new Error("Postgres dependency page limit must be an integer from 1 to 500");
|
|
35917
|
+
}
|
|
35918
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
35919
|
+
throw new Error("Postgres dependency page offset must be a non-negative integer");
|
|
35920
|
+
}
|
|
35921
|
+
await this.ensureSchema();
|
|
35922
|
+
const result = await this.options.client.query(`/* todos:list-dependencies-page */ WITH page AS (
|
|
35923
|
+
SELECT payload, updated_at, object_id
|
|
35924
|
+
FROM ${this.tableName}
|
|
35925
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL
|
|
35926
|
+
ORDER BY updated_at ASC, object_id ASC
|
|
35927
|
+
LIMIT $2 OFFSET $3
|
|
35928
|
+
)
|
|
35929
|
+
SELECT
|
|
35930
|
+
(SELECT COUNT(*)::text FROM ${this.tableName}
|
|
35931
|
+
WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL) AS total,
|
|
35932
|
+
COALESCE(
|
|
35933
|
+
jsonb_agg(page.payload ORDER BY page.updated_at ASC, page.object_id ASC)
|
|
35934
|
+
FILTER (WHERE page.payload IS NOT NULL),
|
|
35935
|
+
'[]'::jsonb
|
|
35936
|
+
) AS dependencies
|
|
35937
|
+
FROM page`, [this.service, limit, offset]);
|
|
35938
|
+
const row = result.rows[0];
|
|
35939
|
+
const totalText = typeof row?.total === "string" ? row.total : typeof row?.total === "number" && Number.isSafeInteger(row.total) ? String(row.total) : null;
|
|
35940
|
+
if (totalText === null || !/^\d+$/.test(totalText)) {
|
|
35941
|
+
throw new Error("Postgres dependency page returned an invalid total");
|
|
35942
|
+
}
|
|
35943
|
+
const total = Number(totalText);
|
|
35944
|
+
if (!Number.isSafeInteger(total)) {
|
|
35945
|
+
throw new Error("Postgres dependency page total exceeds the safe integer range");
|
|
35946
|
+
}
|
|
35947
|
+
if (!Array.isArray(row?.dependencies)) {
|
|
35948
|
+
throw new Error("Postgres dependency page returned an invalid dependencies array");
|
|
35949
|
+
}
|
|
35950
|
+
if (row.dependencies.length > limit || offset + row.dependencies.length > total) {
|
|
35951
|
+
throw new Error("Postgres dependency page returned rows outside the requested bound");
|
|
35952
|
+
}
|
|
35953
|
+
return {
|
|
35954
|
+
dependencies: row.dependencies.map((value) => payloadRecord2(value)),
|
|
35955
|
+
total
|
|
35956
|
+
};
|
|
35957
|
+
}
|
|
35080
35958
|
async listComments(taskId, options = {}) {
|
|
35081
35959
|
await this.ensureSchema();
|
|
35082
35960
|
const limit = options.limit ?? 100;
|
|
@@ -35152,7 +36030,9 @@ class PostgresJsonRecordStore {
|
|
|
35152
36030
|
return "1=0";
|
|
35153
36031
|
return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
|
|
35154
36032
|
};
|
|
35155
|
-
if (filter.
|
|
36033
|
+
if (filter.archived_only === true)
|
|
36034
|
+
conds.push(`(payload->>'archived_at' IS NOT NULL)`);
|
|
36035
|
+
else if (filter.include_archived === false)
|
|
35156
36036
|
conds.push(`(payload->>'archived_at' IS NULL)`);
|
|
35157
36037
|
if (filter.ids)
|
|
35158
36038
|
conds.push(inClause("payload->>'id'", filter.ids));
|
|
@@ -35209,7 +36089,7 @@ class PostgresJsonRecordStore {
|
|
|
35209
36089
|
async listTasks(filter) {
|
|
35210
36090
|
await this.ensureSchema();
|
|
35211
36091
|
const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
|
|
35212
|
-
const orderBy = queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
|
|
36092
|
+
const orderBy = filter.archived_only ? `ORDER BY payload->>'archived_at' DESC, object_id DESC` : queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
|
|
35213
36093
|
let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
|
|
35214
36094
|
if (filter.limit !== undefined) {
|
|
35215
36095
|
params.push(filter.limit);
|
|
@@ -35429,6 +36309,8 @@ class PostgresJsonRecordStore {
|
|
|
35429
36309
|
if (planIds.length === 0 && !parentGuard)
|
|
35430
36310
|
return this.upsert("tasks", value, context);
|
|
35431
36311
|
await this.ensureSchema();
|
|
36312
|
+
if (!queryClient && this.projectIntegrityLocked)
|
|
36313
|
+
queryClient = this.options.client;
|
|
35432
36314
|
if (!queryClient) {
|
|
35433
36315
|
return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
|
|
35434
36316
|
}
|
|
@@ -35966,9 +36848,11 @@ class PostgresJsonRecordStore {
|
|
|
35966
36848
|
}
|
|
35967
36849
|
async deleteTaskHierarchy(id, context = {}) {
|
|
35968
36850
|
await this.ensureSchema();
|
|
35969
|
-
|
|
35970
|
-
|
|
35971
|
-
|
|
36851
|
+
if (!this.projectIntegrityLocked) {
|
|
36852
|
+
return this.withDependencyGraphTransaction((scoped) => scoped.deleteTaskHierarchy(id, context));
|
|
36853
|
+
}
|
|
36854
|
+
const timestamp2 = new Date().toISOString();
|
|
36855
|
+
const result = await this.options.client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
|
|
35972
36856
|
task_tree(object_id, path, cycle) AS (
|
|
35973
36857
|
SELECT task.object_id, ARRAY[task.object_id], false
|
|
35974
36858
|
FROM ${this.tableName} AS task
|
|
@@ -36030,13 +36914,12 @@ class PostgresJsonRecordStore {
|
|
|
36030
36914
|
SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
|
|
36031
36915
|
(SELECT count(*) FROM tombstoned) AS deleted_count,
|
|
36032
36916
|
(SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
|
|
36033
|
-
|
|
36034
|
-
|
|
36035
|
-
|
|
36036
|
-
|
|
36037
|
-
|
|
36038
|
-
|
|
36039
|
-
});
|
|
36917
|
+
this.service,
|
|
36918
|
+
id,
|
|
36919
|
+
timestamp2,
|
|
36920
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
36921
|
+
]);
|
|
36922
|
+
return Boolean(result.rows[0]?.found);
|
|
36040
36923
|
}
|
|
36041
36924
|
async getPlanProjectLinkReceipt(receiptId2) {
|
|
36042
36925
|
const value = await this.get("plan_project_link_receipts", receiptId2);
|
|
@@ -36492,6 +37375,102 @@ class PostgresJsonRecordStore {
|
|
|
36492
37375
|
updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
|
|
36493
37376
|
}
|
|
36494
37377
|
}
|
|
37378
|
+
async function bulkCreateTasksAtomic(inputs, store, context) {
|
|
37379
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
37380
|
+
const tempIds = new Map;
|
|
37381
|
+
const created = [];
|
|
37382
|
+
const dependencies = [];
|
|
37383
|
+
for (const input of inputs) {
|
|
37384
|
+
const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
|
|
37385
|
+
const task = await createTask2(taskInput, scoped, context);
|
|
37386
|
+
if (temp_id)
|
|
37387
|
+
tempIds.set(temp_id, task.id);
|
|
37388
|
+
created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
|
|
37389
|
+
}
|
|
37390
|
+
for (let index = 0;index < inputs.length; index++) {
|
|
37391
|
+
const input = inputs[index];
|
|
37392
|
+
const taskId = created[index].id;
|
|
37393
|
+
const seenDependencies = new Set;
|
|
37394
|
+
for (const reference of input.depends_on ?? []) {
|
|
37395
|
+
const exact = tempIds.get(reference) ?? (await scoped.get("tasks", reference))?.id;
|
|
37396
|
+
const dependencyId = exact ?? (await scoped.resolveTaskRef(reference))?.id;
|
|
37397
|
+
if (!dependencyId)
|
|
37398
|
+
throw new TaskNotFoundError(reference);
|
|
37399
|
+
if (seenDependencies.has(dependencyId)) {
|
|
37400
|
+
throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
|
|
37401
|
+
}
|
|
37402
|
+
seenDependencies.add(dependencyId);
|
|
37403
|
+
dependencies.push(await addDependency2(taskId, dependencyId, scoped, context));
|
|
37404
|
+
}
|
|
37405
|
+
}
|
|
37406
|
+
return { schema_version: 1, atomic: true, created, dependencies };
|
|
37407
|
+
});
|
|
37408
|
+
}
|
|
37409
|
+
async function bulkDeleteTasksAtomic(ids, force, store, context) {
|
|
37410
|
+
return store.withDependencyGraphTransaction(async (scoped) => {
|
|
37411
|
+
const resolved = [];
|
|
37412
|
+
for (const reference of ids) {
|
|
37413
|
+
resolved.push({
|
|
37414
|
+
reference,
|
|
37415
|
+
task: await scoped.get("tasks", reference) ?? await scoped.resolveTaskRef(reference)
|
|
37416
|
+
});
|
|
37417
|
+
}
|
|
37418
|
+
const seen = new Set;
|
|
37419
|
+
for (const item of resolved) {
|
|
37420
|
+
if (!item.task)
|
|
37421
|
+
continue;
|
|
37422
|
+
if (seen.has(item.task.id)) {
|
|
37423
|
+
throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
|
|
37424
|
+
}
|
|
37425
|
+
seen.add(item.task.id);
|
|
37426
|
+
}
|
|
37427
|
+
const childState = new Map;
|
|
37428
|
+
for (const item of resolved) {
|
|
37429
|
+
if (!item.task)
|
|
37430
|
+
continue;
|
|
37431
|
+
const children = await scoped.listTasks({ parent_id: item.task.id, include_subtasks: true, limit: 1 });
|
|
37432
|
+
childState.set(item.task.id, children.length > 0);
|
|
37433
|
+
}
|
|
37434
|
+
const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
|
|
37435
|
+
const plannedIds = new Set(planned.map((item) => item.task.id));
|
|
37436
|
+
const roots = [];
|
|
37437
|
+
for (const item of planned) {
|
|
37438
|
+
if (!force) {
|
|
37439
|
+
roots.push(item);
|
|
37440
|
+
continue;
|
|
37441
|
+
}
|
|
37442
|
+
let parentId = item.task.parent_id;
|
|
37443
|
+
let covered = false;
|
|
37444
|
+
while (parentId) {
|
|
37445
|
+
if (plannedIds.has(parentId)) {
|
|
37446
|
+
covered = true;
|
|
37447
|
+
break;
|
|
37448
|
+
}
|
|
37449
|
+
parentId = (await scoped.get("tasks", parentId))?.parent_id ?? null;
|
|
37450
|
+
}
|
|
37451
|
+
if (!covered)
|
|
37452
|
+
roots.push(item);
|
|
37453
|
+
}
|
|
37454
|
+
for (const item of roots) {
|
|
37455
|
+
if (!await scoped.deleteTaskHierarchy(item.task.id, context)) {
|
|
37456
|
+
throw new Error(`Atomic bulk delete lost task ${item.task.id}`);
|
|
37457
|
+
}
|
|
37458
|
+
}
|
|
37459
|
+
return {
|
|
37460
|
+
schema_version: 1,
|
|
37461
|
+
atomic: true,
|
|
37462
|
+
force,
|
|
37463
|
+
results: resolved.map((item) => {
|
|
37464
|
+
if (!item.task)
|
|
37465
|
+
return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
|
|
37466
|
+
if (!force && childState.get(item.task.id)) {
|
|
37467
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
|
|
37468
|
+
}
|
|
37469
|
+
return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
|
|
37470
|
+
})
|
|
37471
|
+
};
|
|
37472
|
+
});
|
|
37473
|
+
}
|
|
36495
37474
|
async function createTask2(input, store, context) {
|
|
36496
37475
|
const timestamp2 = new Date().toISOString();
|
|
36497
37476
|
const taskId = randomUUID6();
|
|
@@ -36582,12 +37561,16 @@ async function updateTask2(id, input, store, context) {
|
|
|
36582
37561
|
}
|
|
36583
37562
|
}
|
|
36584
37563
|
}
|
|
37564
|
+
const patchTouchesPlacement = input.project_id !== undefined || input.task_list_id !== undefined;
|
|
37565
|
+
const effectiveProjectIdForShortId = input.project_id !== undefined ? input.project_id : existing.project_id;
|
|
37566
|
+
const backfilledShortId = existing.short_id === null && patchTouchesPlacement && effectiveProjectIdForShortId ? await nextTaskShortId2(effectiveProjectIdForShortId, store, context) : null;
|
|
36585
37567
|
const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
|
|
36586
37568
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
36587
37569
|
const task = {
|
|
36588
37570
|
...existing,
|
|
36589
37571
|
...definedPatch(input),
|
|
36590
37572
|
...terminalNow ? { locked_by: null, locked_at: null } : {},
|
|
37573
|
+
short_id: backfilledShortId ?? existing.short_id,
|
|
36591
37574
|
version: existing.version + 1,
|
|
36592
37575
|
updated_at: new Date().toISOString(),
|
|
36593
37576
|
tags: input.tags ?? existing.tags,
|
|
@@ -44847,7 +45830,7 @@ var init_token_utils = __esm(() => {
|
|
|
44847
45830
|
"machines_topology",
|
|
44848
45831
|
"machines_unarchive"
|
|
44849
45832
|
],
|
|
44850
|
-
maintenance: ["
|
|
45833
|
+
maintenance: ["get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task"]
|
|
44851
45834
|
};
|
|
44852
45835
|
MCP_PROFILE_GROUPS = {
|
|
44853
45836
|
minimal: ["core", "loops"],
|
|
@@ -56620,6 +57603,19 @@ var init_client_ip = __esm(() => {
|
|
|
56620
57603
|
TRUST_TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
56621
57604
|
});
|
|
56622
57605
|
|
|
57606
|
+
// src/server/rate-limit-config.ts
|
|
57607
|
+
function resolveRateLimitMax(raw) {
|
|
57608
|
+
const value = raw?.trim();
|
|
57609
|
+
if (!value)
|
|
57610
|
+
return DEFAULT_RATE_LIMIT_MAX;
|
|
57611
|
+
const maximum = Number(value);
|
|
57612
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(maximum) || maximum < 1 || maximum > MAX_RATE_LIMIT_MAX) {
|
|
57613
|
+
throw new Error(`HASNA_TODOS_RATE_LIMIT_MAX (legacy TODOS_RATE_LIMIT_MAX) must be an integer from 1 to ${MAX_RATE_LIMIT_MAX}.`);
|
|
57614
|
+
}
|
|
57615
|
+
return maximum;
|
|
57616
|
+
}
|
|
57617
|
+
var DEFAULT_RATE_LIMIT_MAX = 12000, MAX_RATE_LIMIT_MAX = 1e6;
|
|
57618
|
+
|
|
56623
57619
|
// src/storage/shadow-outbox.ts
|
|
56624
57620
|
class TodosShadowOutbox {
|
|
56625
57621
|
db;
|
|
@@ -57008,6 +58004,11 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
|
|
|
57008
58004
|
archived_at: { type: ["string", "null"] }
|
|
57009
58005
|
} },
|
|
57010
58006
|
Task: taskSchema,
|
|
58007
|
+
TaskDependency: taskDependencySchema,
|
|
58008
|
+
DependencyPage: dependencyPageSchema,
|
|
58009
|
+
BulkCreateTaskInput: bulkCreateTaskInputSchema,
|
|
58010
|
+
BulkCreateReceipt: bulkCreateReceiptSchema,
|
|
58011
|
+
BulkDeleteReceipt: bulkDeleteReceiptSchema,
|
|
57011
58012
|
Project: projectSchema,
|
|
57012
58013
|
TaskManifestBounds: taskManifestBoundsSchema,
|
|
57013
58014
|
TaskManifestCapability: taskManifestCapabilitySchema,
|
|
@@ -57088,6 +58089,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
|
|
|
57088
58089
|
parent_id: { type: "string", nullable: true },
|
|
57089
58090
|
plan_id: { type: "string", nullable: true },
|
|
57090
58091
|
task_list_id: { type: "string", nullable: true },
|
|
58092
|
+
archived_at: { type: "string", format: "date-time", nullable: true },
|
|
57091
58093
|
version: { type: "number" }
|
|
57092
58094
|
}
|
|
57093
58095
|
},
|
|
@@ -58732,6 +59734,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
|
|
|
58732
59734
|
{ name: "parent_id", in: "query", schema: { type: "string", nullable: true } },
|
|
58733
59735
|
{ name: "include_subtasks", in: "query", schema: { type: "boolean" } },
|
|
58734
59736
|
{ name: "include_archived", in: "query", schema: { type: "boolean" }, description: "Explicit archive selection; false excludes archived rows" },
|
|
59737
|
+
{ name: "archived_only", in: "query", schema: { type: "boolean" }, description: "Select only rows carrying archived_at; applied before bounded LIMIT/OFFSET and implies include_archived" },
|
|
58735
59738
|
{ name: "plan_read_contract", in: "query", schema: { type: "string", enum: ["1"] }, description: "Requires plan_id and explicit include_subtasks=true/include_archived; returns a versioned selection receipt" },
|
|
58736
59739
|
{ name: "plan_id", in: "query", schema: { type: "string" } },
|
|
58737
59740
|
{ name: "task_list_id", in: "query", schema: { type: "string" } },
|
|
@@ -58784,6 +59787,66 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
|
|
|
58784
59787
|
}
|
|
58785
59788
|
}
|
|
58786
59789
|
},
|
|
59790
|
+
"/v1/tasks/bulk-create": {
|
|
59791
|
+
post: {
|
|
59792
|
+
operationId: "bulkCreateTasks",
|
|
59793
|
+
summary: "Atomically create a bounded task batch and its dependency edges",
|
|
59794
|
+
requestBody: {
|
|
59795
|
+
required: true,
|
|
59796
|
+
content: {
|
|
59797
|
+
"application/json": {
|
|
59798
|
+
schema: {
|
|
59799
|
+
type: "object",
|
|
59800
|
+
additionalProperties: false,
|
|
59801
|
+
required: ["schema_version", "tasks"],
|
|
59802
|
+
properties: {
|
|
59803
|
+
schema_version: { type: "integer", enum: [1] },
|
|
59804
|
+
tasks: { type: "array", minItems: 1, maxItems: 50, items: { $ref: "#/components/schemas/BulkCreateTaskInput" } }
|
|
59805
|
+
}
|
|
59806
|
+
}
|
|
59807
|
+
}
|
|
59808
|
+
}
|
|
59809
|
+
},
|
|
59810
|
+
responses: {
|
|
59811
|
+
"201": { content: { "application/json": { schema: { type: "object", required: ["receipt"], properties: { receipt: { $ref: "#/components/schemas/BulkCreateReceipt" } } } } } },
|
|
59812
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59813
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59814
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59815
|
+
"413": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59816
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
59817
|
+
}
|
|
59818
|
+
}
|
|
59819
|
+
},
|
|
59820
|
+
"/v1/tasks/bulk-delete": {
|
|
59821
|
+
post: {
|
|
59822
|
+
operationId: "bulkDeleteTasks",
|
|
59823
|
+
summary: "Atomically delete a bounded task set using authoritative child checks",
|
|
59824
|
+
requestBody: {
|
|
59825
|
+
required: true,
|
|
59826
|
+
content: {
|
|
59827
|
+
"application/json": {
|
|
59828
|
+
schema: {
|
|
59829
|
+
type: "object",
|
|
59830
|
+
additionalProperties: false,
|
|
59831
|
+
required: ["schema_version", "task_ids"],
|
|
59832
|
+
properties: {
|
|
59833
|
+
schema_version: { type: "integer", enum: [1] },
|
|
59834
|
+
task_ids: { type: "array", minItems: 1, maxItems: 100, uniqueItems: true, items: { type: "string", minLength: 1, maxLength: 200 } },
|
|
59835
|
+
force: { type: "boolean", default: false, description: "Requires todos:* scope when true" }
|
|
59836
|
+
}
|
|
59837
|
+
}
|
|
59838
|
+
}
|
|
59839
|
+
}
|
|
59840
|
+
},
|
|
59841
|
+
responses: {
|
|
59842
|
+
"200": { content: { "application/json": { schema: { type: "object", required: ["receipt"], properties: { receipt: { $ref: "#/components/schemas/BulkDeleteReceipt" } } } } } },
|
|
59843
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59844
|
+
"403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59845
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59846
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
59847
|
+
}
|
|
59848
|
+
}
|
|
59849
|
+
},
|
|
58787
59850
|
"/v1/tasks/{id}": {
|
|
58788
59851
|
get: {
|
|
58789
59852
|
operationId: "getTask",
|
|
@@ -58832,6 +59895,42 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
|
|
|
58832
59895
|
}
|
|
58833
59896
|
}
|
|
58834
59897
|
},
|
|
59898
|
+
"/v1/tasks/{id}/history": {
|
|
59899
|
+
get: {
|
|
59900
|
+
operationId: "listTaskHistory",
|
|
59901
|
+
summary: "List a storage-bounded page of one task's audit history",
|
|
59902
|
+
description: "The storage adapter applies task scope, time filters, deterministic order, LIMIT, and OFFSET before materializing rows. Omission uses a bounded 500-row compatibility window and returns 426 when incomplete.",
|
|
59903
|
+
parameters: [
|
|
59904
|
+
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
|
59905
|
+
{ name: "limit", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 500, default: 500 } },
|
|
59906
|
+
{ name: "offset", in: "query", required: false, schema: { type: "integer", minimum: 0, default: 0 } },
|
|
59907
|
+
{ name: "since", in: "query", schema: { type: "string", format: "date-time" } },
|
|
59908
|
+
{ name: "until", in: "query", schema: { type: "string", format: "date-time" } },
|
|
59909
|
+
{ name: "order", in: "query", schema: { type: "string", enum: ["asc", "desc"], default: "desc" } }
|
|
59910
|
+
],
|
|
59911
|
+
responses: {
|
|
59912
|
+
"200": { content: { "application/json": { schema: {
|
|
59913
|
+
type: "object",
|
|
59914
|
+
additionalProperties: false,
|
|
59915
|
+
required: ["history", "count", "total", "limit", "offset", "order", "has_more", "next_offset"],
|
|
59916
|
+
properties: {
|
|
59917
|
+
history: { type: "array", maxItems: 500, items: { type: "object", additionalProperties: true } },
|
|
59918
|
+
count: { type: "integer", minimum: 0, maximum: 500 },
|
|
59919
|
+
total: { type: "integer", minimum: 0 },
|
|
59920
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
59921
|
+
offset: { type: "integer", minimum: 0 },
|
|
59922
|
+
order: { type: "string", enum: ["asc", "desc"] },
|
|
59923
|
+
has_more: { type: "boolean" },
|
|
59924
|
+
next_offset: { type: "integer", minimum: 0, nullable: true }
|
|
59925
|
+
}
|
|
59926
|
+
} } } },
|
|
59927
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59928
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59929
|
+
"426": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
59930
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
59931
|
+
}
|
|
59932
|
+
}
|
|
59933
|
+
},
|
|
58835
59934
|
"/v1/tasks/{id}/comments": {
|
|
58836
59935
|
get: {
|
|
58837
59936
|
operationId: "listTaskComments",
|
|
@@ -59890,6 +60989,95 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
|
|
|
59890
60989
|
}
|
|
59891
60990
|
}
|
|
59892
60991
|
},
|
|
60992
|
+
"/v1/agents": {
|
|
60993
|
+
get: {
|
|
60994
|
+
operationId: "listAgents",
|
|
60995
|
+
summary: "List the shared agent roster with storage-bounded pagination",
|
|
60996
|
+
description: "The authority applies a maximum 500-row LIMIT/OFFSET before materializing agents. Omitting pagination uses a 500-row legacy window and returns 426 when the roster is larger.",
|
|
60997
|
+
parameters: [
|
|
60998
|
+
{ name: "limit", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 500, default: 500 } },
|
|
60999
|
+
{ name: "offset", in: "query", required: false, schema: { type: "integer", minimum: 0, default: 0 } },
|
|
61000
|
+
{ name: "include_archived", in: "query", required: false, schema: { type: "boolean", default: false } }
|
|
61001
|
+
],
|
|
61002
|
+
responses: {
|
|
61003
|
+
"200": {
|
|
61004
|
+
content: {
|
|
61005
|
+
"application/json": {
|
|
61006
|
+
schema: {
|
|
61007
|
+
type: "object",
|
|
61008
|
+
required: ["agents", "count", "total", "limit", "offset", "has_more", "next_offset"],
|
|
61009
|
+
properties: {
|
|
61010
|
+
agents: { type: "array", items: { type: "object" } },
|
|
61011
|
+
count: { type: "integer", minimum: 0 },
|
|
61012
|
+
total: { type: "integer", minimum: 0 },
|
|
61013
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
61014
|
+
offset: { type: "integer", minimum: 0 },
|
|
61015
|
+
has_more: { type: "boolean" },
|
|
61016
|
+
next_offset: { anyOf: [{ type: "integer", minimum: 0 }, { type: "null" }] }
|
|
61017
|
+
}
|
|
61018
|
+
}
|
|
61019
|
+
}
|
|
61020
|
+
}
|
|
61021
|
+
},
|
|
61022
|
+
"426": { description: "Legacy unpaged request exceeded the bounded roster window" }
|
|
61023
|
+
}
|
|
61024
|
+
}
|
|
61025
|
+
},
|
|
61026
|
+
"/v1/activity": {
|
|
61027
|
+
get: {
|
|
61028
|
+
operationId: "listRecentActivity",
|
|
61029
|
+
summary: "List a bounded recent task-history window",
|
|
61030
|
+
parameters: [
|
|
61031
|
+
{ name: "limit", in: "query", required: false, schema: { type: "integer", minimum: 1, maximum: 1e4, default: 50 } }
|
|
61032
|
+
],
|
|
61033
|
+
responses: {
|
|
61034
|
+
"200": {
|
|
61035
|
+
content: {
|
|
61036
|
+
"application/json": {
|
|
61037
|
+
schema: {
|
|
61038
|
+
type: "object",
|
|
61039
|
+
required: ["activity", "count", "limit"],
|
|
61040
|
+
properties: {
|
|
61041
|
+
activity: { type: "array", items: { type: "object" } },
|
|
61042
|
+
count: { type: "integer", minimum: 0 },
|
|
61043
|
+
limit: { type: "integer", minimum: 1, maximum: 1e4 }
|
|
61044
|
+
}
|
|
61045
|
+
}
|
|
61046
|
+
}
|
|
61047
|
+
}
|
|
61048
|
+
}
|
|
61049
|
+
}
|
|
61050
|
+
}
|
|
61051
|
+
},
|
|
61052
|
+
"/v1/dependencies": {
|
|
61053
|
+
get: {
|
|
61054
|
+
operationId: "listDependencies",
|
|
61055
|
+
summary: "List dependency edges with storage-bounded pagination",
|
|
61056
|
+
description: "The authority always applies a maximum 500-row LIMIT/OFFSET before materializing edges. Omitting both parameters uses limit=500 and offset=0 only when that window is complete; a larger graph returns 426 so an old client cannot mistake a partial page for the whole graph.",
|
|
61057
|
+
parameters: [
|
|
61058
|
+
{
|
|
61059
|
+
name: "limit",
|
|
61060
|
+
in: "query",
|
|
61061
|
+
required: false,
|
|
61062
|
+
description: "Maximum edges to materialize. Omission defaults to the bounded 500-row legacy window.",
|
|
61063
|
+
schema: { type: "integer", minimum: 1, maximum: 500, default: 500 }
|
|
61064
|
+
},
|
|
61065
|
+
{
|
|
61066
|
+
name: "offset",
|
|
61067
|
+
in: "query",
|
|
61068
|
+
required: false,
|
|
61069
|
+
description: "Zero-based dependency-edge offset in stable storage order.",
|
|
61070
|
+
schema: { type: "integer", minimum: 0, default: 0 }
|
|
61071
|
+
}
|
|
61072
|
+
],
|
|
61073
|
+
responses: {
|
|
61074
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DependencyPage" } } } },
|
|
61075
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
61076
|
+
"426": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
61077
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
61078
|
+
}
|
|
61079
|
+
}
|
|
61080
|
+
},
|
|
59893
61081
|
"/v1/task-lists": {
|
|
59894
61082
|
get: {
|
|
59895
61083
|
operationId: "listTaskLists",
|
|
@@ -60137,7 +61325,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
|
|
|
60137
61325
|
}
|
|
60138
61326
|
});
|
|
60139
61327
|
}
|
|
60140
|
-
var atomicMigrationAuthoritySchema, atomicMigrationReceiptSchema, taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskSubtreeTransferExpectedTaskSchema, taskSubtreeTransferSharedPlanSplitSchema, taskSubtreeTransferInspectRequestSchema, taskSubtreeTransferInspectionSchema, taskSubtreeTransferApplyRequestSchema, taskSubtreeTransferTaskImageSchema, taskSubtreeTransferPlanImageSchema, taskSubtreeTransferImageSchema, taskSubtreeTransferReceiptSchema, taskSubtreeTransferResultSchema, taskSubtreeTransferCapabilitySchema, taskSubtreeTransferRollbackRequestSchema, taskSubtreeTransferReadExactRequestSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
|
|
61328
|
+
var atomicMigrationAuthoritySchema, atomicMigrationReceiptSchema, taskSchema, taskDependencySchema, bulkCreateTaskInputSchema, bulkCreateReceiptSchema, bulkDeleteReceiptSchema, dependencyPageSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskSubtreeTransferExpectedTaskSchema, taskSubtreeTransferSharedPlanSplitSchema, taskSubtreeTransferInspectRequestSchema, taskSubtreeTransferInspectionSchema, taskSubtreeTransferApplyRequestSchema, taskSubtreeTransferTaskImageSchema, taskSubtreeTransferPlanImageSchema, taskSubtreeTransferImageSchema, taskSubtreeTransferReceiptSchema, taskSubtreeTransferResultSchema, taskSubtreeTransferCapabilitySchema, taskSubtreeTransferRollbackRequestSchema, taskSubtreeTransferReadExactRequestSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
|
|
60141
61329
|
var init_openapi = __esm(() => {
|
|
60142
61330
|
init_package_version();
|
|
60143
61331
|
init_types();
|
|
@@ -60165,6 +61353,103 @@ var init_openapi = __esm(() => {
|
|
|
60165
61353
|
updated_at: { type: "string" }
|
|
60166
61354
|
}
|
|
60167
61355
|
};
|
|
61356
|
+
taskDependencySchema = {
|
|
61357
|
+
type: "object",
|
|
61358
|
+
additionalProperties: false,
|
|
61359
|
+
required: ["task_id", "depends_on"],
|
|
61360
|
+
properties: {
|
|
61361
|
+
task_id: { type: "string", minLength: 1 },
|
|
61362
|
+
depends_on: { type: "string", minLength: 1 },
|
|
61363
|
+
external_project_id: { type: "string", nullable: true },
|
|
61364
|
+
external_task_id: { type: "string", nullable: true }
|
|
61365
|
+
}
|
|
61366
|
+
};
|
|
61367
|
+
bulkCreateTaskInputSchema = {
|
|
61368
|
+
type: "object",
|
|
61369
|
+
additionalProperties: false,
|
|
61370
|
+
required: ["title"],
|
|
61371
|
+
properties: {
|
|
61372
|
+
temp_id: { type: "string", minLength: 1, maxLength: 100 },
|
|
61373
|
+
depends_on: { type: "array", maxItems: 100, items: { type: "string", minLength: 1, maxLength: 200 } },
|
|
61374
|
+
title: { type: "string", minLength: 1, maxLength: 20000 },
|
|
61375
|
+
description: { type: "string", nullable: true, maxLength: 1e5 },
|
|
61376
|
+
status: { type: "string", enum: [...TASK_STATUSES] },
|
|
61377
|
+
priority: { type: "string", enum: [...TASK_PRIORITIES] },
|
|
61378
|
+
project_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
61379
|
+
parent_id: { type: "string" },
|
|
61380
|
+
plan_id: { type: "string" },
|
|
61381
|
+
task_list_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
61382
|
+
assigned_to: { type: "string", minLength: 1, maxLength: 200 },
|
|
61383
|
+
agent_id: { type: "string" },
|
|
61384
|
+
created_by: { type: "string" },
|
|
61385
|
+
tags: { type: "array", maxItems: 100, items: { type: "string", minLength: 1, maxLength: 100 } },
|
|
61386
|
+
estimated_minutes: { type: "number", minimum: 0 }
|
|
61387
|
+
}
|
|
61388
|
+
};
|
|
61389
|
+
bulkCreateReceiptSchema = {
|
|
61390
|
+
type: "object",
|
|
61391
|
+
additionalProperties: false,
|
|
61392
|
+
required: ["schema_version", "atomic", "created", "dependencies"],
|
|
61393
|
+
properties: {
|
|
61394
|
+
schema_version: { type: "integer", enum: [1] },
|
|
61395
|
+
atomic: { type: "boolean", enum: [true] },
|
|
61396
|
+
created: {
|
|
61397
|
+
type: "array",
|
|
61398
|
+
maxItems: 100,
|
|
61399
|
+
items: {
|
|
61400
|
+
type: "object",
|
|
61401
|
+
additionalProperties: false,
|
|
61402
|
+
required: ["temp_id", "id", "short_id", "title"],
|
|
61403
|
+
properties: {
|
|
61404
|
+
temp_id: { type: "string", nullable: true },
|
|
61405
|
+
id: { type: "string" },
|
|
61406
|
+
short_id: { type: "string", nullable: true },
|
|
61407
|
+
title: { type: "string" }
|
|
61408
|
+
}
|
|
61409
|
+
}
|
|
61410
|
+
},
|
|
61411
|
+
dependencies: { type: "array", maxItems: 100, items: { $ref: "#/components/schemas/TaskDependency" } }
|
|
61412
|
+
}
|
|
61413
|
+
};
|
|
61414
|
+
bulkDeleteReceiptSchema = {
|
|
61415
|
+
type: "object",
|
|
61416
|
+
additionalProperties: false,
|
|
61417
|
+
required: ["schema_version", "atomic", "force", "results"],
|
|
61418
|
+
properties: {
|
|
61419
|
+
schema_version: { type: "integer", enum: [1] },
|
|
61420
|
+
atomic: { type: "boolean", enum: [true] },
|
|
61421
|
+
force: { type: "boolean" },
|
|
61422
|
+
results: {
|
|
61423
|
+
type: "array",
|
|
61424
|
+
maxItems: 100,
|
|
61425
|
+
items: {
|
|
61426
|
+
type: "object",
|
|
61427
|
+
additionalProperties: false,
|
|
61428
|
+
required: ["requested_id", "task_id", "outcome", "reason"],
|
|
61429
|
+
properties: {
|
|
61430
|
+
requested_id: { type: "string" },
|
|
61431
|
+
task_id: { type: "string", nullable: true },
|
|
61432
|
+
outcome: { type: "string", enum: ["deleted", "skipped", "missing"] },
|
|
61433
|
+
reason: { oneOf: [{ type: "string", enum: ["has_children", "not_found"] }, { type: "null" }] }
|
|
61434
|
+
}
|
|
61435
|
+
}
|
|
61436
|
+
}
|
|
61437
|
+
}
|
|
61438
|
+
};
|
|
61439
|
+
dependencyPageSchema = {
|
|
61440
|
+
type: "object",
|
|
61441
|
+
additionalProperties: false,
|
|
61442
|
+
required: ["dependencies", "count", "total", "limit", "offset", "has_more", "next_offset"],
|
|
61443
|
+
properties: {
|
|
61444
|
+
dependencies: { type: "array", items: { $ref: "#/components/schemas/TaskDependency" } },
|
|
61445
|
+
count: { type: "integer", minimum: 0 },
|
|
61446
|
+
total: { type: "integer", minimum: 0 },
|
|
61447
|
+
limit: { type: "integer", minimum: 0 },
|
|
61448
|
+
offset: { type: "integer", minimum: 0 },
|
|
61449
|
+
has_more: { type: "boolean" },
|
|
61450
|
+
next_offset: { type: "integer", minimum: 0, nullable: true }
|
|
61451
|
+
}
|
|
61452
|
+
};
|
|
60168
61453
|
taskManifestBoundsSchema = {
|
|
60169
61454
|
type: "object",
|
|
60170
61455
|
additionalProperties: false,
|
|
@@ -61690,6 +62975,9 @@ function validateTaskPatchVocabulary(value) {
|
|
|
61690
62975
|
if (!parsed.ok)
|
|
61691
62976
|
return { ok: false, message: parsed.message };
|
|
61692
62977
|
}
|
|
62978
|
+
if (body2.archived_at !== undefined && body2.archived_at !== null && (typeof body2.archived_at !== "string" || !body2.archived_at.trim())) {
|
|
62979
|
+
return { ok: false, message: "archived_at must be a non-empty timestamp string or null" };
|
|
62980
|
+
}
|
|
61693
62981
|
if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
|
|
61694
62982
|
return { ok: false, message: "parent_id must be a non-empty task id or null" };
|
|
61695
62983
|
}
|
|
@@ -61939,6 +63227,147 @@ function validateTemplatePatch(value) {
|
|
|
61939
63227
|
...body2.plan_id === null ? { plan_id: null } : {}
|
|
61940
63228
|
} };
|
|
61941
63229
|
}
|
|
63230
|
+
function validateBulkCreateRequest(value) {
|
|
63231
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
63232
|
+
return { ok: false, message: "bulk create body must be an object" };
|
|
63233
|
+
}
|
|
63234
|
+
const body2 = value;
|
|
63235
|
+
const allowedTop = new Set(["schema_version", "tasks"]);
|
|
63236
|
+
const unknownTop = Object.keys(body2).find((key3) => !allowedTop.has(key3));
|
|
63237
|
+
if (unknownTop)
|
|
63238
|
+
return { ok: false, message: `unknown bulk create field: ${unknownTop}` };
|
|
63239
|
+
if (body2.schema_version !== 1)
|
|
63240
|
+
return { ok: false, message: "schema_version must be 1" };
|
|
63241
|
+
if (!Array.isArray(body2.tasks) || body2.tasks.length < 1 || body2.tasks.length > MAX_BULK_CREATE_TASKS) {
|
|
63242
|
+
return { ok: false, message: `tasks must contain between 1 and ${MAX_BULK_CREATE_TASKS} items` };
|
|
63243
|
+
}
|
|
63244
|
+
const allowedTask = new Set([
|
|
63245
|
+
"temp_id",
|
|
63246
|
+
"depends_on",
|
|
63247
|
+
"title",
|
|
63248
|
+
"description",
|
|
63249
|
+
"status",
|
|
63250
|
+
"priority",
|
|
63251
|
+
"project_id",
|
|
63252
|
+
"parent_id",
|
|
63253
|
+
"plan_id",
|
|
63254
|
+
"task_list_id",
|
|
63255
|
+
"agent_id",
|
|
63256
|
+
"created_by",
|
|
63257
|
+
"assigned_to",
|
|
63258
|
+
"tags",
|
|
63259
|
+
"estimated_minutes"
|
|
63260
|
+
]);
|
|
63261
|
+
const tempIds = new Set;
|
|
63262
|
+
const tasks = [];
|
|
63263
|
+
let dependencyCount = 0;
|
|
63264
|
+
for (let index = 0;index < body2.tasks.length; index++) {
|
|
63265
|
+
const raw = body2.tasks[index];
|
|
63266
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
63267
|
+
return { ok: false, message: `tasks[${index}] must be an object` };
|
|
63268
|
+
}
|
|
63269
|
+
const task2 = raw;
|
|
63270
|
+
const unknown = Object.keys(task2).find((key3) => !allowedTask.has(key3));
|
|
63271
|
+
if (unknown)
|
|
63272
|
+
return { ok: false, message: `unknown tasks[${index}] field: ${unknown}` };
|
|
63273
|
+
if (typeof task2.title !== "string" || !task2.title.trim() || task2.title.length > 20000) {
|
|
63274
|
+
return { ok: false, message: `tasks[${index}].title must be a non-empty string of at most 20000 characters` };
|
|
63275
|
+
}
|
|
63276
|
+
if (task2.description !== undefined && task2.description !== null && (typeof task2.description !== "string" || task2.description.length > 1e5)) {
|
|
63277
|
+
return { ok: false, message: `tasks[${index}].description must be a string or null of at most 100000 characters` };
|
|
63278
|
+
}
|
|
63279
|
+
if (task2.status !== undefined && (typeof task2.status !== "string" || !TASK_STATUSES.includes(task2.status))) {
|
|
63280
|
+
return { ok: false, message: `tasks[${index}].status is invalid` };
|
|
63281
|
+
}
|
|
63282
|
+
if (task2.priority !== undefined && (typeof task2.priority !== "string" || !TASK_PRIORITIES.includes(task2.priority))) {
|
|
63283
|
+
return { ok: false, message: `tasks[${index}].priority is invalid` };
|
|
63284
|
+
}
|
|
63285
|
+
for (const field of ["project_id", "parent_id", "plan_id", "task_list_id", "agent_id", "created_by", "assigned_to"]) {
|
|
63286
|
+
const candidate = task2[field];
|
|
63287
|
+
if (candidate !== undefined && (typeof candidate !== "string" || !candidate.trim() || candidate.length > 200)) {
|
|
63288
|
+
return { ok: false, message: `tasks[${index}].${field} must be a non-empty string of at most 200 characters` };
|
|
63289
|
+
}
|
|
63290
|
+
}
|
|
63291
|
+
if (task2.tags !== undefined && (!Array.isArray(task2.tags) || task2.tags.length > 100 || task2.tags.some((tag) => typeof tag !== "string" || !tag.trim() || tag.length > 100))) {
|
|
63292
|
+
return { ok: false, message: `tasks[${index}].tags must be an array of at most 100 non-empty strings` };
|
|
63293
|
+
}
|
|
63294
|
+
if (task2.estimated_minutes !== undefined && (typeof task2.estimated_minutes !== "number" || !Number.isFinite(task2.estimated_minutes) || task2.estimated_minutes < 0)) {
|
|
63295
|
+
return { ok: false, message: `tasks[${index}].estimated_minutes must be a finite non-negative number` };
|
|
63296
|
+
}
|
|
63297
|
+
if (task2.temp_id !== undefined) {
|
|
63298
|
+
if (typeof task2.temp_id !== "string" || !task2.temp_id.trim() || task2.temp_id.length > 100) {
|
|
63299
|
+
return { ok: false, message: `tasks[${index}].temp_id must be a non-empty string of at most 100 characters` };
|
|
63300
|
+
}
|
|
63301
|
+
if (tempIds.has(task2.temp_id))
|
|
63302
|
+
return { ok: false, message: `duplicate temp_id: ${task2.temp_id}` };
|
|
63303
|
+
tempIds.add(task2.temp_id);
|
|
63304
|
+
}
|
|
63305
|
+
if (task2.depends_on !== undefined && (!Array.isArray(task2.depends_on) || task2.depends_on.length > MAX_BULK_DEPENDENCIES || task2.depends_on.some((ref) => typeof ref !== "string" || !ref.trim() || ref.length > 200))) {
|
|
63306
|
+
return { ok: false, message: `tasks[${index}].depends_on must be an array of at most ${MAX_BULK_DEPENDENCIES} non-empty task references` };
|
|
63307
|
+
}
|
|
63308
|
+
if (Array.isArray(task2.depends_on) && new Set(task2.depends_on).size !== task2.depends_on.length) {
|
|
63309
|
+
return { ok: false, message: `tasks[${index}].depends_on must not contain duplicates` };
|
|
63310
|
+
}
|
|
63311
|
+
dependencyCount += Array.isArray(task2.depends_on) ? task2.depends_on.length : 0;
|
|
63312
|
+
if (dependencyCount > MAX_BULK_DEPENDENCIES) {
|
|
63313
|
+
return { ok: false, message: `bulk create supports at most ${MAX_BULK_DEPENDENCIES} dependency edges` };
|
|
63314
|
+
}
|
|
63315
|
+
tasks.push(task2);
|
|
63316
|
+
}
|
|
63317
|
+
return { ok: true, tasks };
|
|
63318
|
+
}
|
|
63319
|
+
function validateBulkDeleteRequest(value) {
|
|
63320
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
63321
|
+
return { ok: false, message: "bulk delete body must be an object" };
|
|
63322
|
+
}
|
|
63323
|
+
const body2 = value;
|
|
63324
|
+
const allowed = new Set(["schema_version", "task_ids", "force"]);
|
|
63325
|
+
const unknown = Object.keys(body2).find((key3) => !allowed.has(key3));
|
|
63326
|
+
if (unknown)
|
|
63327
|
+
return { ok: false, message: `unknown bulk delete field: ${unknown}` };
|
|
63328
|
+
if (body2.schema_version !== 1)
|
|
63329
|
+
return { ok: false, message: "schema_version must be 1" };
|
|
63330
|
+
if (!Array.isArray(body2.task_ids) || body2.task_ids.length < 1 || body2.task_ids.length > MAX_BULK_DELETE_TASKS) {
|
|
63331
|
+
return { ok: false, message: `task_ids must contain between 1 and ${MAX_BULK_DELETE_TASKS} items` };
|
|
63332
|
+
}
|
|
63333
|
+
if (body2.task_ids.some((id) => typeof id !== "string" || !id.trim() || id.length > 200)) {
|
|
63334
|
+
return { ok: false, message: "task_ids must contain non-empty task references of at most 200 characters" };
|
|
63335
|
+
}
|
|
63336
|
+
if (new Set(body2.task_ids).size !== body2.task_ids.length) {
|
|
63337
|
+
return { ok: false, message: "task_ids must not contain duplicates" };
|
|
63338
|
+
}
|
|
63339
|
+
if (body2.force !== undefined && typeof body2.force !== "boolean") {
|
|
63340
|
+
return { ok: false, message: "force must be a boolean" };
|
|
63341
|
+
}
|
|
63342
|
+
return { ok: true, taskIds: body2.task_ids, force: body2.force === true };
|
|
63343
|
+
}
|
|
63344
|
+
async function readBoundedJson(req, maxBytes) {
|
|
63345
|
+
const reader = req.body?.getReader();
|
|
63346
|
+
if (!reader)
|
|
63347
|
+
return { ok: false, status: 400, message: "missing JSON body" };
|
|
63348
|
+
let size = 0;
|
|
63349
|
+
const chunks = [];
|
|
63350
|
+
try {
|
|
63351
|
+
while (true) {
|
|
63352
|
+
const part = await reader.read();
|
|
63353
|
+
if (part.done)
|
|
63354
|
+
break;
|
|
63355
|
+
size += part.value.byteLength;
|
|
63356
|
+
if (size > maxBytes) {
|
|
63357
|
+
await reader.cancel();
|
|
63358
|
+
return { ok: false, status: 413, message: `request exceeds the ${maxBytes}-byte limit` };
|
|
63359
|
+
}
|
|
63360
|
+
chunks.push(part.value);
|
|
63361
|
+
}
|
|
63362
|
+
} finally {
|
|
63363
|
+
reader.releaseLock();
|
|
63364
|
+
}
|
|
63365
|
+
try {
|
|
63366
|
+
return { ok: true, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
|
|
63367
|
+
} catch {
|
|
63368
|
+
return { ok: false, status: 400, message: "invalid JSON body" };
|
|
63369
|
+
}
|
|
63370
|
+
}
|
|
61942
63371
|
async function readJson3(req) {
|
|
61943
63372
|
try {
|
|
61944
63373
|
const text = await req.text();
|
|
@@ -62174,6 +63603,50 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
62174
63603
|
return error(405, "Use GET or POST /v1/machines");
|
|
62175
63604
|
}
|
|
62176
63605
|
if (resource === "tasks") {
|
|
63606
|
+
if (id === "bulk-create" && !action) {
|
|
63607
|
+
if (method !== "POST")
|
|
63608
|
+
return error(405, "bulk task creation requires POST");
|
|
63609
|
+
if (typeof store.tasks.bulkCreateAtomic !== "function") {
|
|
63610
|
+
return error(501, "Upgrade the Todos backend: atomic bulk task creation is unavailable");
|
|
63611
|
+
}
|
|
63612
|
+
const body2 = await readBoundedJson(req, 2 * 1024 * 1024);
|
|
63613
|
+
if (!body2.ok)
|
|
63614
|
+
return error(body2.status, body2.message);
|
|
63615
|
+
const parsed = validateBulkCreateRequest(body2.value);
|
|
63616
|
+
if (!parsed.ok)
|
|
63617
|
+
return error(400, parsed.message);
|
|
63618
|
+
const normalizedTasks = [];
|
|
63619
|
+
for (const task2 of parsed.tasks) {
|
|
63620
|
+
if (!task2.assigned_to) {
|
|
63621
|
+
normalizedTasks.push(task2);
|
|
63622
|
+
continue;
|
|
63623
|
+
}
|
|
63624
|
+
const assigned = await store.agents.get(task2.assigned_to) ?? await store.agents.getByName(task2.assigned_to);
|
|
63625
|
+
if (!assigned)
|
|
63626
|
+
return error(404, `assigned agent not found: ${task2.assigned_to}`);
|
|
63627
|
+
normalizedTasks.push({ ...task2, assigned_to: assigned.id });
|
|
63628
|
+
}
|
|
63629
|
+
const receipt = await store.tasks.bulkCreateAtomic(normalizedTasks, contextFromPrincipal(principal));
|
|
63630
|
+
return json6({ receipt }, 201);
|
|
63631
|
+
}
|
|
63632
|
+
if (id === "bulk-delete" && !action) {
|
|
63633
|
+
if (method !== "POST")
|
|
63634
|
+
return error(405, "bulk task deletion requires POST");
|
|
63635
|
+
if (typeof store.tasks.bulkDeleteAtomic !== "function") {
|
|
63636
|
+
return error(501, "Upgrade the Todos backend: atomic bulk task deletion is unavailable");
|
|
63637
|
+
}
|
|
63638
|
+
const body2 = await readBoundedJson(req, 64 * 1024);
|
|
63639
|
+
if (!body2.ok)
|
|
63640
|
+
return error(body2.status, body2.message);
|
|
63641
|
+
const parsed = validateBulkDeleteRequest(body2.value);
|
|
63642
|
+
if (!parsed.ok)
|
|
63643
|
+
return error(400, parsed.message);
|
|
63644
|
+
if (parsed.force && !principal.scopes.includes("todos:*")) {
|
|
63645
|
+
return error(403, "force bulk deletion requires todos:* scope");
|
|
63646
|
+
}
|
|
63647
|
+
const receipt = await store.tasks.bulkDeleteAtomic(parsed.taskIds, parsed.force, contextFromPrincipal(principal));
|
|
63648
|
+
return json6({ receipt });
|
|
63649
|
+
}
|
|
62177
63650
|
if (id === "exists" && !action) {
|
|
62178
63651
|
if (method !== "POST")
|
|
62179
63652
|
return error(405, `method ${method} not allowed on /v1/tasks/exists`);
|
|
@@ -62259,6 +63732,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
62259
63732
|
if (includeArchived !== null && includeArchived !== "true" && includeArchived !== "false") {
|
|
62260
63733
|
return error(400, "include_archived must be true or false");
|
|
62261
63734
|
}
|
|
63735
|
+
const archivedOnly = url.searchParams.get("archived_only");
|
|
63736
|
+
if (archivedOnly !== null && archivedOnly !== "true" && archivedOnly !== "false") {
|
|
63737
|
+
return error(400, "archived_only must be true or false");
|
|
63738
|
+
}
|
|
63739
|
+
if (archivedOnly === "true" && includeArchived === "false") {
|
|
63740
|
+
return error(400, "archived_only=true is incompatible with include_archived=false");
|
|
63741
|
+
}
|
|
62262
63742
|
const planRead = url.searchParams.get("plan_read_contract");
|
|
62263
63743
|
if (planRead !== null) {
|
|
62264
63744
|
const keys = [...url.searchParams.keys()];
|
|
@@ -62297,6 +63777,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
62297
63777
|
return offsetParam.response;
|
|
62298
63778
|
const filter = {
|
|
62299
63779
|
...includeArchived !== null ? { include_archived: includeArchived === "true" } : {},
|
|
63780
|
+
...archivedOnly === "true" ? { archived_only: true, include_archived: true } : {},
|
|
62300
63781
|
...updatedAfter !== null && updatedAfter.ok ? { updated_after: updatedAfter.value } : {},
|
|
62301
63782
|
...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
|
|
62302
63783
|
...statusParam.value !== undefined ? { status: statusParam.value } : {},
|
|
@@ -62468,8 +63949,48 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
62468
63949
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/history`);
|
|
62469
63950
|
if (!await store.tasks.get(id))
|
|
62470
63951
|
return error(404, "task not found");
|
|
62471
|
-
|
|
62472
|
-
|
|
63952
|
+
if (typeof store.audit.getTaskHistoryPage !== "function") {
|
|
63953
|
+
return error(501, "bounded task history is not supported by this storage backend");
|
|
63954
|
+
}
|
|
63955
|
+
const rawLimit = url.searchParams.get("limit");
|
|
63956
|
+
const rawOffset = url.searchParams.get("offset");
|
|
63957
|
+
const requestedPage = rawLimit !== null || rawOffset !== null || url.searchParams.has("order") || url.searchParams.has("since") || url.searchParams.has("until");
|
|
63958
|
+
const limitParam = paginationQueryParam(url, "limit");
|
|
63959
|
+
if (!limitParam.ok)
|
|
63960
|
+
return limitParam.response;
|
|
63961
|
+
const offsetParam = paginationQueryParam(url, "offset");
|
|
63962
|
+
if (!offsetParam.ok)
|
|
63963
|
+
return offsetParam.response;
|
|
63964
|
+
const limit = limitParam.value ?? 500;
|
|
63965
|
+
const offset = offsetParam.value ?? 0;
|
|
63966
|
+
if (limit > 500)
|
|
63967
|
+
return error(400, "history limit must be at most 500");
|
|
63968
|
+
const order = url.searchParams.get("order") ?? "desc";
|
|
63969
|
+
if (order !== "asc" && order !== "desc")
|
|
63970
|
+
return error(400, "history order must be asc or desc");
|
|
63971
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
63972
|
+
const until = url.searchParams.get("until") ?? undefined;
|
|
63973
|
+
const result = await store.audit.getTaskHistoryPage(id, { limit, offset, order, ...since ? { since } : {}, ...until ? { until } : {} }, contextFromPrincipal(principal));
|
|
63974
|
+
if (!result || !Array.isArray(result.history) || !Number.isSafeInteger(result.total) || result.total < 0 || result.history.length > limit || result.history.length > 0 && offset + result.history.length > result.total) {
|
|
63975
|
+
return error(500, "TASK_HISTORY_PAGE_INVALID: storage returned contradictory pagination");
|
|
63976
|
+
}
|
|
63977
|
+
const hasMore = offset + result.history.length < result.total;
|
|
63978
|
+
if (!requestedPage && hasMore) {
|
|
63979
|
+
return error(426, "task history exceeds the bounded legacy window; send limit, offset, and order with an upgraded client");
|
|
63980
|
+
}
|
|
63981
|
+
if (hasMore && result.history.length === 0) {
|
|
63982
|
+
return error(500, "TASK_HISTORY_PAGE_STALLED: storage returned no progress before total");
|
|
63983
|
+
}
|
|
63984
|
+
return json6({
|
|
63985
|
+
history: result.history,
|
|
63986
|
+
count: result.history.length,
|
|
63987
|
+
total: result.total,
|
|
63988
|
+
limit,
|
|
63989
|
+
offset,
|
|
63990
|
+
order,
|
|
63991
|
+
has_more: hasMore,
|
|
63992
|
+
next_offset: hasMore ? offset + result.history.length : null
|
|
63993
|
+
});
|
|
62473
63994
|
}
|
|
62474
63995
|
if (action === "lock" || action === "unlock") {
|
|
62475
63996
|
if (method !== "POST")
|
|
@@ -63111,8 +64632,48 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
63111
64632
|
}
|
|
63112
64633
|
if (resource === "agents") {
|
|
63113
64634
|
if (!id && method === "GET") {
|
|
63114
|
-
|
|
63115
|
-
|
|
64635
|
+
if (typeof store.agents.listPage !== "function") {
|
|
64636
|
+
return error(501, "bounded agent pagination is not supported by this storage backend");
|
|
64637
|
+
}
|
|
64638
|
+
const rawLimit = url.searchParams.get("limit");
|
|
64639
|
+
const rawOffset = url.searchParams.get("offset");
|
|
64640
|
+
const requestedPage = rawLimit !== null || rawOffset !== null || url.searchParams.has("include_archived");
|
|
64641
|
+
const limit = rawLimit === null ? 500 : Number(rawLimit);
|
|
64642
|
+
const offset = rawOffset === null ? 0 : Number(rawOffset);
|
|
64643
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
64644
|
+
return error(400, "agent limit must be an integer from 1 to 500");
|
|
64645
|
+
}
|
|
64646
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
64647
|
+
return error(400, "agent offset must be a non-negative integer");
|
|
64648
|
+
}
|
|
64649
|
+
const includeArchivedParam = url.searchParams.get("include_archived");
|
|
64650
|
+
if (includeArchivedParam !== null && includeArchivedParam !== "true" && includeArchivedParam !== "false") {
|
|
64651
|
+
return error(400, "include_archived must be true or false");
|
|
64652
|
+
}
|
|
64653
|
+
const page = await store.agents.listPage({
|
|
64654
|
+
limit,
|
|
64655
|
+
offset,
|
|
64656
|
+
include_archived: includeArchivedParam === "true"
|
|
64657
|
+
}, contextFromPrincipal(principal));
|
|
64658
|
+
if (!page || !Array.isArray(page.agents) || !Number.isSafeInteger(page.total) || page.total < 0 || page.agents.length > limit || page.agents.length > 0 && offset + page.agents.length > page.total) {
|
|
64659
|
+
return error(500, "agent storage returned an invalid bounded page");
|
|
64660
|
+
}
|
|
64661
|
+
const hasMore = offset + page.agents.length < page.total;
|
|
64662
|
+
if (!requestedPage && hasMore) {
|
|
64663
|
+
return error(426, "agent roster exceeds the bounded legacy window; send limit and offset with an upgraded client");
|
|
64664
|
+
}
|
|
64665
|
+
if (hasMore && page.agents.length === 0) {
|
|
64666
|
+
return error(500, "agent storage returned a stalled bounded page");
|
|
64667
|
+
}
|
|
64668
|
+
return json6({
|
|
64669
|
+
agents: page.agents,
|
|
64670
|
+
count: page.agents.length,
|
|
64671
|
+
total: page.total,
|
|
64672
|
+
limit,
|
|
64673
|
+
offset,
|
|
64674
|
+
has_more: hasMore,
|
|
64675
|
+
next_offset: hasMore ? offset + page.agents.length : null
|
|
64676
|
+
});
|
|
63116
64677
|
}
|
|
63117
64678
|
if (!id && method === "POST") {
|
|
63118
64679
|
const body2 = await readJson3(req);
|
|
@@ -63157,9 +64718,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
63157
64718
|
if (method !== "GET")
|
|
63158
64719
|
return error(405, `method ${method} not allowed on /v1/activity`);
|
|
63159
64720
|
const limitParam = url.searchParams.get("limit");
|
|
63160
|
-
const limit = limitParam ?
|
|
63161
|
-
|
|
63162
|
-
|
|
64721
|
+
const limit = limitParam === null ? 50 : Number(limitParam);
|
|
64722
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
|
|
64723
|
+
return error(400, "activity limit must be an integer from 1 to 10000");
|
|
64724
|
+
}
|
|
64725
|
+
const activity = await store.audit.getRecentActivity(limit, contextFromPrincipal(principal));
|
|
64726
|
+
if (!Array.isArray(activity) || activity.length > limit) {
|
|
64727
|
+
return error(500, "activity storage returned an invalid bounded result");
|
|
64728
|
+
}
|
|
64729
|
+
return json6({ activity, count: activity.length, limit });
|
|
63163
64730
|
}
|
|
63164
64731
|
if (resource === "task-lists") {
|
|
63165
64732
|
if (!id && method === "GET") {
|
|
@@ -63253,11 +64820,40 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
63253
64820
|
if (resource === "dependencies" && !id) {
|
|
63254
64821
|
if (method !== "GET")
|
|
63255
64822
|
return error(405, `method ${method} not allowed on /v1/dependencies`);
|
|
63256
|
-
|
|
63257
|
-
|
|
63258
|
-
|
|
63259
|
-
const
|
|
63260
|
-
|
|
64823
|
+
const limitParam = url.searchParams.get("limit");
|
|
64824
|
+
const offsetParam = url.searchParams.get("offset");
|
|
64825
|
+
const requestedPage = limitParam !== null || offsetParam !== null;
|
|
64826
|
+
const limit = limitParam === null ? 500 : Number(limitParam);
|
|
64827
|
+
const offset = offsetParam === null ? 0 : Number(offsetParam);
|
|
64828
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
64829
|
+
return error(400, "dependency limit must be an integer from 1 to 500");
|
|
64830
|
+
}
|
|
64831
|
+
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
64832
|
+
return error(400, "dependency offset must be a non-negative integer");
|
|
64833
|
+
}
|
|
64834
|
+
if (typeof store.dependencies?.listPage !== "function") {
|
|
64835
|
+
return error(501, "bounded dependency pagination is not supported by this storage backend");
|
|
64836
|
+
}
|
|
64837
|
+
const page = await store.dependencies.listPage({ limit, offset }, contextFromPrincipal(principal));
|
|
64838
|
+
if (!page || !Array.isArray(page.dependencies) || !Number.isSafeInteger(page.total) || page.total < 0 || page.dependencies.length > limit || offset + page.dependencies.length > page.total) {
|
|
64839
|
+
return error(500, "dependency storage returned an invalid bounded page");
|
|
64840
|
+
}
|
|
64841
|
+
const hasMore = offset + page.dependencies.length < page.total;
|
|
64842
|
+
if (!requestedPage && hasMore) {
|
|
64843
|
+
return error(426, "dependency graph exceeds the bounded legacy window; send limit and offset with an upgraded client");
|
|
64844
|
+
}
|
|
64845
|
+
if (hasMore && page.dependencies.length === 0) {
|
|
64846
|
+
return error(500, "dependency storage returned a stalled bounded page");
|
|
64847
|
+
}
|
|
64848
|
+
return json6({
|
|
64849
|
+
dependencies: page.dependencies,
|
|
64850
|
+
count: page.dependencies.length,
|
|
64851
|
+
total: page.total,
|
|
64852
|
+
limit,
|
|
64853
|
+
offset,
|
|
64854
|
+
has_more: hasMore,
|
|
64855
|
+
next_offset: hasMore ? offset + page.dependencies.length : null
|
|
64856
|
+
});
|
|
63261
64857
|
}
|
|
63262
64858
|
if (resource === "commits" && id) {
|
|
63263
64859
|
if (method !== "GET")
|
|
@@ -63447,7 +65043,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
63447
65043
|
return error(500, e.message || "internal error");
|
|
63448
65044
|
}
|
|
63449
65045
|
}
|
|
63450
|
-
var JSON_HEADERS5, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500, RFC3339_DATE_TIME;
|
|
65046
|
+
var JSON_HEADERS5, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500, MAX_BULK_CREATE_TASKS = 50, MAX_BULK_DELETE_TASKS = 100, MAX_BULK_DEPENDENCIES = 100, RFC3339_DATE_TIME;
|
|
63451
65047
|
var init_v1 = __esm(() => {
|
|
63452
65048
|
init_atomic_project_migration();
|
|
63453
65049
|
init_machine_registry();
|
|
@@ -64176,6 +65772,15 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
64176
65772
|
if (assigned_to) {
|
|
64177
65773
|
resolved.assigned_to = resolveAssignee(await validateMcpAssignee(assigned_to, Boolean(params.allow_seat)));
|
|
64178
65774
|
}
|
|
65775
|
+
const cloud = getTodosCloudClient();
|
|
65776
|
+
if (cloud) {
|
|
65777
|
+
if (project_id)
|
|
65778
|
+
resolved.project_id = await cloudResolveProjectRef(cloud, project_id);
|
|
65779
|
+
if (task_list_id)
|
|
65780
|
+
resolved.task_list_id = await cloudResolveTaskListRef(cloud, task_list_id);
|
|
65781
|
+
const remote = await cloudUpsertTaskByFingerprint(cloud, resolved);
|
|
65782
|
+
return { content: [{ type: "text", text: compactJson({ created: remote.created, task: JSON.parse(mutationTaskResponse(remote.task)) }) }] };
|
|
65783
|
+
}
|
|
64179
65784
|
if (project_id)
|
|
64180
65785
|
resolved.project_id = resolveId(project_id, "projects");
|
|
64181
65786
|
if (task_list_id)
|
|
@@ -64744,7 +66349,7 @@ __export(exports_mention_resolver, {
|
|
|
64744
66349
|
resolveMentions: () => resolveMentions
|
|
64745
66350
|
});
|
|
64746
66351
|
import { existsSync as existsSync20, readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync10 } from "fs";
|
|
64747
|
-
import { basename as basename8, isAbsolute, join as join19, relative as relative5, resolve as resolve19, sep as sep4 } from "path";
|
|
66352
|
+
import { basename as basename8, isAbsolute as isAbsolute2, join as join19, relative as relative5, resolve as resolve19, sep as sep4 } from "path";
|
|
64748
66353
|
function blankResolution(parsed) {
|
|
64749
66354
|
return {
|
|
64750
66355
|
input: parsed.input,
|
|
@@ -64771,7 +66376,7 @@ function normalizeWorkspace(workspace) {
|
|
|
64771
66376
|
}
|
|
64772
66377
|
function isInside(root, absolutePath) {
|
|
64773
66378
|
const rel = relative5(root, absolutePath);
|
|
64774
|
-
return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep4}`) && !
|
|
66379
|
+
return rel === "" || !rel.startsWith("..") && !rel.includes(`..${sep4}`) && !isAbsolute2(rel);
|
|
64775
66380
|
}
|
|
64776
66381
|
function normalizeRelativePath(value) {
|
|
64777
66382
|
const normalized2 = value.trim().replace(/^\.?\//, "").replace(/\\/g, "/");
|
|
@@ -68834,6 +70439,12 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
68834
70439
|
version: exports_external.number().optional().describe("Expected version for optimistic locking")
|
|
68835
70440
|
}, async ({ task_id, deadline, version: version2 }) => {
|
|
68836
70441
|
try {
|
|
70442
|
+
const cloud = getTodosCloudClient();
|
|
70443
|
+
if (cloud) {
|
|
70444
|
+
const remoteTaskId = await cloudResolveTaskRef(cloud, task_id);
|
|
70445
|
+
const rescheduled = await cloudUpdateTaskVerified(cloud, remoteTaskId, version2 !== undefined ? { due_at: deadline, version: version2 } : { due_at: deadline });
|
|
70446
|
+
return { content: [{ type: "text", text: formatTask(rescheduled) }] };
|
|
70447
|
+
}
|
|
68837
70448
|
const resolvedId = resolveId(task_id);
|
|
68838
70449
|
const task2 = updateWithOptionalVersion(resolvedId, { due_at: deadline }, version2);
|
|
68839
70450
|
return { content: [{ type: "text", text: formatTask(task2) }] };
|
|
@@ -68921,12 +70532,36 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
68921
70532
|
}
|
|
68922
70533
|
if (shouldRegisterTool("bulk_update_tasks")) {
|
|
68923
70534
|
server.tool("bulk_update_tasks", "Update multiple tasks at once. All tasks must pass the dependency check.", {
|
|
68924
|
-
task_ids: exports_external.array(exports_external.string()).describe("Array of task IDs to update"),
|
|
70535
|
+
task_ids: exports_external.array(exports_external.string().min(1).max(200)).min(1).max(100).describe("Array of task IDs to update"),
|
|
68925
70536
|
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
68926
70537
|
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
68927
70538
|
assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign")
|
|
68928
70539
|
}, async ({ task_ids, status: status3, priority, assigned_to }) => {
|
|
68929
70540
|
try {
|
|
70541
|
+
const cloud = getTodosCloudClient();
|
|
70542
|
+
if (cloud) {
|
|
70543
|
+
const resolvedIds = [];
|
|
70544
|
+
for (const reference of task_ids)
|
|
70545
|
+
resolvedIds.push(await cloudResolveTaskRef(cloud, reference));
|
|
70546
|
+
const patch = {};
|
|
70547
|
+
if (status3 !== undefined)
|
|
70548
|
+
patch.status = status3;
|
|
70549
|
+
if (priority !== undefined)
|
|
70550
|
+
patch.priority = priority;
|
|
70551
|
+
if (assigned_to !== undefined)
|
|
70552
|
+
patch.assigned_to = assigned_to === null ? null : await cloudResolveAgentRef(cloud, assigned_to);
|
|
70553
|
+
let updated = 0;
|
|
70554
|
+
const failed = [];
|
|
70555
|
+
for (let index = 0;index < resolvedIds.length; index++) {
|
|
70556
|
+
try {
|
|
70557
|
+
await cloudUpdateTaskVerified(cloud, resolvedIds[index], patch);
|
|
70558
|
+
updated++;
|
|
70559
|
+
} catch {
|
|
70560
|
+
failed.push(task_ids[index]);
|
|
70561
|
+
}
|
|
70562
|
+
}
|
|
70563
|
+
return { content: [{ type: "text", text: `${updated} task(s) updated, ${failed.length} failed.${failed.length ? ` Failed: ${failed.map((f) => f.slice(0, 8)).join(", ")}` : ""}` }] };
|
|
70564
|
+
}
|
|
68930
70565
|
const { bulkUpdateTasks: bulkUpdateTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
68931
70566
|
const resolved = task_ids.map(resolveId);
|
|
68932
70567
|
let resolvedAssignee = assigned_to;
|
|
@@ -68942,6 +70577,7 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
68942
70577
|
if (shouldRegisterTool("bulk_create_tasks")) {
|
|
68943
70578
|
server.tool("bulk_create_tasks", "Create multiple tasks at once from an array of task objects.", {
|
|
68944
70579
|
tasks: exports_external.array(exports_external.object({
|
|
70580
|
+
temp_id: exports_external.string().min(1).max(100).optional().describe("Caller-local handle for sibling dependencies in this batch"),
|
|
68945
70581
|
title: exports_external.string(),
|
|
68946
70582
|
description: exports_external.string().optional(),
|
|
68947
70583
|
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
@@ -68949,28 +70585,44 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
68949
70585
|
project_id: exports_external.string().optional(),
|
|
68950
70586
|
task_list_id: exports_external.string().optional(),
|
|
68951
70587
|
assigned_to: exports_external.string().optional(),
|
|
68952
|
-
depends_on: exports_external.array(exports_external.string()).optional(),
|
|
68953
|
-
short_id: exports_external.string().nullable().optional(),
|
|
70588
|
+
depends_on: exports_external.array(exports_external.string().min(1).max(200)).max(100).optional(),
|
|
68954
70589
|
tags: exports_external.array(exports_external.string()).optional(),
|
|
68955
70590
|
estimate: exports_external.number().optional()
|
|
68956
|
-
})).describe("Array of task objects")
|
|
70591
|
+
})).min(1).max(50).describe("Array of task objects")
|
|
68957
70592
|
}, async ({ tasks }) => {
|
|
68958
70593
|
try {
|
|
68959
|
-
const
|
|
68960
|
-
const
|
|
68961
|
-
|
|
68962
|
-
|
|
68963
|
-
|
|
68964
|
-
|
|
68965
|
-
|
|
68966
|
-
|
|
68967
|
-
|
|
68968
|
-
|
|
68969
|
-
|
|
68970
|
-
|
|
68971
|
-
|
|
68972
|
-
|
|
68973
|
-
|
|
70594
|
+
const router = resolveWritableIdentity();
|
|
70595
|
+
const cloud = getTodosCloudClient();
|
|
70596
|
+
if (cloud) {
|
|
70597
|
+
const remoteAgentId = router.agent_id ? await cloudResolveAgentRef(cloud, router.agent_id) : undefined;
|
|
70598
|
+
const remote = [];
|
|
70599
|
+
for (const t of tasks) {
|
|
70600
|
+
const { estimate, project_id, task_list_id, ...rest } = t;
|
|
70601
|
+
const assignedTo = t.assigned_to ? await cloudResolveAgentRef(cloud, t.assigned_to) : undefined;
|
|
70602
|
+
remote.push({
|
|
70603
|
+
...rest,
|
|
70604
|
+
...remoteAgentId ? { agent_id: remoteAgentId, created_by: remoteAgentId } : {},
|
|
70605
|
+
...assignedTo ? { assigned_to: assignedTo } : {},
|
|
70606
|
+
...estimate !== undefined ? { estimated_minutes: estimate } : {},
|
|
70607
|
+
...project_id ? { project_id: await cloudResolveProjectRef(cloud, project_id) } : {},
|
|
70608
|
+
...task_list_id ? { task_list_id: await cloudResolveTaskListRef(cloud, task_list_id) } : {}
|
|
70609
|
+
});
|
|
70610
|
+
}
|
|
70611
|
+
const receipt2 = await cloudBulkCreateTasks(cloud, remote);
|
|
70612
|
+
return { content: [{ type: "text", text: JSON.stringify(receipt2) }] };
|
|
70613
|
+
}
|
|
70614
|
+
const { createLocalSqliteTodosStorageAdapter: createLocalSqliteTodosStorageAdapter2 } = (init_local_sqlite(), __toCommonJS(exports_local_sqlite));
|
|
70615
|
+
const resolved = tasks.map((task2) => ({
|
|
70616
|
+
...task2,
|
|
70617
|
+
...router.agent_id ? { agent_id: router.agent_id, created_by: router.agent_id } : {},
|
|
70618
|
+
...task2.project_id ? { project_id: resolveId(task2.project_id, "projects") } : {},
|
|
70619
|
+
...task2.task_list_id ? { task_list_id: resolveId(task2.task_list_id, "task_lists") } : {},
|
|
70620
|
+
...task2.assigned_to ? { assigned_to: resolveId(task2.assigned_to, "agents") } : {},
|
|
70621
|
+
...task2.estimate !== undefined ? { estimated_minutes: task2.estimate } : {}
|
|
70622
|
+
}));
|
|
70623
|
+
const local = createLocalSqliteTodosStorageAdapter2({ db: getDatabase() });
|
|
70624
|
+
const receipt = await local.tasks.bulkCreateAtomic(resolved);
|
|
70625
|
+
return { content: [{ type: "text", text: `${receipt.created.length} task(s) created.` }] };
|
|
68974
70626
|
} catch (e) {
|
|
68975
70627
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
68976
70628
|
}
|
|
@@ -68978,14 +70630,21 @@ function registerTaskProjectTools(server, ctx) {
|
|
|
68978
70630
|
}
|
|
68979
70631
|
if (shouldRegisterTool("bulk_delete_tasks")) {
|
|
68980
70632
|
server.tool("bulk_delete_tasks", "Delete multiple tasks at once. Tasks with active children are skipped.", {
|
|
68981
|
-
task_ids: exports_external.array(exports_external.string()).describe("Array of task IDs"),
|
|
70633
|
+
task_ids: exports_external.array(exports_external.string().min(1).max(200)).min(1).max(100).describe("Array of unique task IDs or refs"),
|
|
68982
70634
|
force: exports_external.boolean().optional().describe("Skip child check for all tasks (dangerous)")
|
|
68983
70635
|
}, async ({ task_ids, force }) => {
|
|
68984
70636
|
try {
|
|
68985
|
-
const
|
|
68986
|
-
|
|
68987
|
-
|
|
68988
|
-
|
|
70637
|
+
const cloud = getTodosCloudClient();
|
|
70638
|
+
if (cloud) {
|
|
70639
|
+
const receipt2 = await cloudBulkDeleteTasks(cloud, task_ids, force === true);
|
|
70640
|
+
return { content: [{ type: "text", text: JSON.stringify(receipt2) }] };
|
|
70641
|
+
}
|
|
70642
|
+
const { createLocalSqliteTodosStorageAdapter: createLocalSqliteTodosStorageAdapter2 } = (init_local_sqlite(), __toCommonJS(exports_local_sqlite));
|
|
70643
|
+
const local = createLocalSqliteTodosStorageAdapter2({ db: getDatabase() });
|
|
70644
|
+
const receipt = await local.tasks.bulkDeleteAtomic(task_ids, force === true);
|
|
70645
|
+
const deleted = receipt.results.filter((result) => result.outcome === "deleted").length;
|
|
70646
|
+
const skipped = receipt.results.filter((result) => result.outcome === "skipped").length;
|
|
70647
|
+
return { content: [{ type: "text", text: `${deleted} task(s) deleted, ${skipped} skipped (has children).` }] };
|
|
68989
70648
|
} catch (e) {
|
|
68990
70649
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
68991
70650
|
}
|
|
@@ -69921,6 +71580,20 @@ Tasks:` : null,
|
|
|
69921
71580
|
task_id: exports_external.string().describe("Task ID")
|
|
69922
71581
|
}, async ({ task_id }) => {
|
|
69923
71582
|
try {
|
|
71583
|
+
const cloud = getTodosCloudClient();
|
|
71584
|
+
if (cloud) {
|
|
71585
|
+
const remoteTaskId = await cloudResolveTaskRef(cloud, task_id);
|
|
71586
|
+
const page = await cloudListComments(cloud, remoteTaskId, { limit: 100 });
|
|
71587
|
+
if (page.comments.length === 0)
|
|
71588
|
+
return { content: [{ type: "text", text: "No comments." }] };
|
|
71589
|
+
const remoteLines = page.comments.map((c) => `[${c.agent_id || "unknown"}] ${c.created_at?.slice(0, 16)}:
|
|
71590
|
+
${c.content}`);
|
|
71591
|
+
if (page.has_more)
|
|
71592
|
+
remoteLines.push(`(older comments available; showing the ${page.comments.length} most recent)`);
|
|
71593
|
+
return { content: [{ type: "text", text: remoteLines.join(`
|
|
71594
|
+
|
|
71595
|
+
`) }] };
|
|
71596
|
+
}
|
|
69924
71597
|
const resolvedId = resolveId(task_id);
|
|
69925
71598
|
const comments = listComments(resolvedId);
|
|
69926
71599
|
if (comments.length === 0)
|
|
@@ -69939,13 +71612,50 @@ Tasks:` : null,
|
|
|
69939
71612
|
server.tool("get_activity_timeline", "Get a unified local activity timeline across comments, task history, and run evidence.", {
|
|
69940
71613
|
entity_type: exports_external.enum(["all", "task", "project", "plan", "run"]).optional().describe("Scope type. Defaults to all."),
|
|
69941
71614
|
entity_id: exports_external.string().optional().describe("ID for task/project/plan/run scope."),
|
|
69942
|
-
limit: exports_external.number().optional().describe("Max entries, default 50."),
|
|
69943
|
-
offset: exports_external.number().optional().describe("Entries to skip for pagination."),
|
|
71615
|
+
limit: exports_external.number().int().min(1).max(200).optional().describe("Max entries, default 50, maximum 200."),
|
|
71616
|
+
offset: exports_external.number().int().min(0).max(1e4).optional().describe("Entries to skip for pagination, maximum 10000."),
|
|
69944
71617
|
order: exports_external.enum(["asc", "desc"]).optional().describe("Sort order, default desc."),
|
|
69945
71618
|
since: exports_external.string().optional().describe("Only entries at or after this ISO timestamp."),
|
|
69946
71619
|
until: exports_external.string().optional().describe("Only entries at or before this ISO timestamp.")
|
|
69947
71620
|
}, async (input) => {
|
|
69948
71621
|
try {
|
|
71622
|
+
const cloud = getTodosCloudClient();
|
|
71623
|
+
if (cloud) {
|
|
71624
|
+
const limit = input.limit ?? 50;
|
|
71625
|
+
const offset = input.offset ?? 0;
|
|
71626
|
+
const scopeType = input.entity_type ?? "all";
|
|
71627
|
+
if (scopeType !== "all" && scopeType !== "task") {
|
|
71628
|
+
throw new Error(`REMOTE_API_INCOMPATIBLE: hosted activity does not yet support ${scopeType}-scoped timelines`);
|
|
71629
|
+
}
|
|
71630
|
+
const entityId2 = scopeType === "task" ? input.entity_id ? await cloudResolveTaskRef(cloud, input.entity_id) : null : null;
|
|
71631
|
+
const historyPage = entityId2 ? await cloudTaskHistoryPage(cloud, entityId2, {
|
|
71632
|
+
limit,
|
|
71633
|
+
offset,
|
|
71634
|
+
order: input.order ?? "desc",
|
|
71635
|
+
...input.since ? { since: input.since } : {},
|
|
71636
|
+
...input.until ? { until: input.until } : {}
|
|
71637
|
+
}) : null;
|
|
71638
|
+
let entries = historyPage ? historyPage.history : await cloudRecentActivity(cloud, Math.min(1e4, limit + offset));
|
|
71639
|
+
if (!historyPage) {
|
|
71640
|
+
if (input.since)
|
|
71641
|
+
entries = entries.filter((entry2) => (entry2.created_at ?? "") >= input.since);
|
|
71642
|
+
if (input.until)
|
|
71643
|
+
entries = entries.filter((entry2) => (entry2.created_at ?? "") <= input.until);
|
|
71644
|
+
}
|
|
71645
|
+
if (!historyPage && input.order === "asc")
|
|
71646
|
+
entries = [...entries].reverse();
|
|
71647
|
+
const page = historyPage ? entries : entries.slice(offset, offset + limit);
|
|
71648
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
71649
|
+
source: "cloud",
|
|
71650
|
+
scope: { entity_type: scopeType, entity_id: entityId2 },
|
|
71651
|
+
entries: page,
|
|
71652
|
+
count: page.length,
|
|
71653
|
+
complete: historyPage ? offset === 0 && !historyPage.has_more : false,
|
|
71654
|
+
...historyPage ? { total: historyPage.total, has_more: historyPage.has_more, next_offset: historyPage.next_offset } : {},
|
|
71655
|
+
omitted_sources: entityId2 ? ["comments", "run_evidence"] : ["comments", "run_evidence", "older_task_history"],
|
|
71656
|
+
omitted_reason: entityId2 ? "the hosted task-history route does not include comments or run evidence" : "the hosted recent-activity route is bounded and does not include comments or run evidence"
|
|
71657
|
+
}) }] };
|
|
71658
|
+
}
|
|
69949
71659
|
let entityId = input.entity_id;
|
|
69950
71660
|
if (input.entity_type === "task" && entityId)
|
|
69951
71661
|
entityId = resolveId(entityId);
|
|
@@ -70163,9 +71873,25 @@ Tasks:` : null,
|
|
|
70163
71873
|
query: exports_external.string().describe("Search query"),
|
|
70164
71874
|
project_id: exports_external.string().optional().describe("Filter by project"),
|
|
70165
71875
|
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
70166
|
-
limit: exports_external.number().optional().describe("Max results (default: 20)")
|
|
71876
|
+
limit: exports_external.number().int().min(1).max(100).optional().describe("Max results (default: 20, maximum: 100)")
|
|
70167
71877
|
}, async ({ query, project_id, status: status3, limit }) => {
|
|
70168
71878
|
try {
|
|
71879
|
+
const cloud = getTodosCloudClient();
|
|
71880
|
+
if (cloud) {
|
|
71881
|
+
const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
|
|
71882
|
+
const remote = await cloudListTasks(cloud, {
|
|
71883
|
+
query,
|
|
71884
|
+
...remoteProjectId ? { project_id: remoteProjectId } : {},
|
|
71885
|
+
...status3 ? { status: status3 } : {},
|
|
71886
|
+
limit: limit || 20
|
|
71887
|
+
});
|
|
71888
|
+
if (remote.length === 0)
|
|
71889
|
+
return { content: [{ type: "text", text: `No results for: ${query}` }] };
|
|
71890
|
+
const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title}`);
|
|
71891
|
+
return { content: [{ type: "text", text: `${remote.length} result(s) for "${query}":
|
|
71892
|
+
${remoteLines.join(`
|
|
71893
|
+
`)}` }] };
|
|
71894
|
+
}
|
|
70169
71895
|
const { searchTasks: searchTasks2 } = (init_search(), __toCommonJS(exports_search));
|
|
70170
71896
|
const resolved = { query, limit };
|
|
70171
71897
|
if (project_id)
|
|
@@ -70266,6 +71992,8 @@ var init_task_project_tools = __esm(() => {
|
|
|
70266
71992
|
init_comments();
|
|
70267
71993
|
init_task_runs();
|
|
70268
71994
|
init_project_bootstrap();
|
|
71995
|
+
init_database();
|
|
71996
|
+
init_creator_identity();
|
|
70269
71997
|
init_labels();
|
|
70270
71998
|
init_tags();
|
|
70271
71999
|
init_redaction();
|
|
@@ -71068,6 +72796,29 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
71068
72796
|
project_id: exports_external.string().optional().describe("Scope to a project")
|
|
71069
72797
|
}, async ({ days = 7, project_id }) => {
|
|
71070
72798
|
try {
|
|
72799
|
+
const cloud = getTodosCloudClient();
|
|
72800
|
+
if (cloud) {
|
|
72801
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
72802
|
+
const stampedAt = new Date().toISOString();
|
|
72803
|
+
const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
|
|
72804
|
+
const selection = await cloudListTasksCompleteBounded(cloud, {
|
|
72805
|
+
status: "completed",
|
|
72806
|
+
include_archived: false,
|
|
72807
|
+
...remoteProjectId ? { project_id: remoteProjectId } : {}
|
|
72808
|
+
});
|
|
72809
|
+
for (const task2 of selection) {
|
|
72810
|
+
if (typeof task2.updated_at !== "string" || !task2.updated_at || !Number.isSafeInteger(task2.version)) {
|
|
72811
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: archive selection contains a task without updated_at and version");
|
|
72812
|
+
}
|
|
72813
|
+
}
|
|
72814
|
+
const candidates = selection.filter((task2) => task2.updated_at < cutoff);
|
|
72815
|
+
let archived = 0;
|
|
72816
|
+
for (const t of candidates) {
|
|
72817
|
+
await cloudUpdateTaskVerified(cloud, t.id, { archived_at: stampedAt, version: t.version });
|
|
72818
|
+
archived++;
|
|
72819
|
+
}
|
|
72820
|
+
return { content: [{ type: "text", text: `Archived ${archived} completed task(s) older than ${days} days.` }] };
|
|
72821
|
+
}
|
|
71071
72822
|
const { archiveCompletedTasks: archiveCompletedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71072
72823
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
71073
72824
|
const count = archiveCompletedTasks2(days, resolvedProjectId);
|
|
@@ -71082,6 +72833,14 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
71082
72833
|
task_id: exports_external.string().describe("Task ID")
|
|
71083
72834
|
}, async ({ task_id }) => {
|
|
71084
72835
|
try {
|
|
72836
|
+
const cloud = getTodosCloudClient();
|
|
72837
|
+
if (cloud) {
|
|
72838
|
+
const current = await cloudGetTask(cloud, task_id);
|
|
72839
|
+
if (!current)
|
|
72840
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
72841
|
+
await cloudUpdateTaskVerified(cloud, current.id, { archived_at: null, version: current.version });
|
|
72842
|
+
return { content: [{ type: "text", text: `Task ${task_id.slice(0, 8)} restored from archive.` }] };
|
|
72843
|
+
}
|
|
71085
72844
|
const { unarchiveTask: unarchiveTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71086
72845
|
unarchiveTask2(resolveId(task_id));
|
|
71087
72846
|
return { content: [{ type: "text", text: `Task ${task_id.slice(0, 8)} restored from archive.` }] };
|
|
@@ -71093,12 +72852,36 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
71093
72852
|
if (shouldRegisterTool("get_archived_tasks")) {
|
|
71094
72853
|
server.tool("get_archived_tasks", "List archived tasks.", {
|
|
71095
72854
|
project_id: exports_external.string().optional().describe("Filter by project"),
|
|
71096
|
-
limit: exports_external.number().optional().describe("Max results (default: 50)")
|
|
71097
|
-
|
|
72855
|
+
limit: exports_external.number().int().min(1).max(200).optional().describe("Max results (default: 50, maximum: 200)"),
|
|
72856
|
+
offset: exports_external.number().int().min(0).max(1e4).optional().describe("Archived rows to skip (default: 0, maximum: 10000)")
|
|
72857
|
+
}, async ({ project_id, limit, offset }) => {
|
|
71098
72858
|
try {
|
|
71099
|
-
const
|
|
72859
|
+
const cloud = getTodosCloudClient();
|
|
72860
|
+
if (cloud) {
|
|
72861
|
+
const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
|
|
72862
|
+
const page = await cloudListArchivedTasksPage(cloud, {
|
|
72863
|
+
...remoteProjectId ? { project_id: remoteProjectId } : {},
|
|
72864
|
+
limit: limit ?? 50,
|
|
72865
|
+
offset: offset ?? 0
|
|
72866
|
+
});
|
|
72867
|
+
if (page.tasks.length === 0)
|
|
72868
|
+
return { content: [{ type: "text", text: "No archived tasks." }] };
|
|
72869
|
+
const remoteLines = page.tasks.map((t) => `${t.short_id || t.id.slice(0, 8)} ${t.title} archived ${t.archived_at}`);
|
|
72870
|
+
if (page.has_more)
|
|
72871
|
+
remoteLines.push(`(${page.total - (page.offset + page.tasks.length)} more archived task(s); next offset ${page.next_offset})`);
|
|
72872
|
+
return { content: [{ type: "text", text: remoteLines.join(`
|
|
72873
|
+
`) }] };
|
|
72874
|
+
}
|
|
72875
|
+
const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71100
72876
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
71101
|
-
const tasks =
|
|
72877
|
+
const tasks = listTasks3({
|
|
72878
|
+
...resolvedProjectId ? { project_id: resolvedProjectId } : {},
|
|
72879
|
+
include_archived: true,
|
|
72880
|
+
archived_only: true,
|
|
72881
|
+
include_subtasks: true,
|
|
72882
|
+
limit: limit ?? 50,
|
|
72883
|
+
offset: offset ?? 0
|
|
72884
|
+
});
|
|
71102
72885
|
if (tasks.length === 0)
|
|
71103
72886
|
return { content: [{ type: "text", text: "No archived tasks." }] };
|
|
71104
72887
|
const lines = tasks.map((t) => `${t.short_id || t.id.slice(0, 8)} ${t.title} archived ${t.archived_at}`);
|
|
@@ -71131,12 +72914,29 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
71131
72914
|
agent_id: exports_external.string().optional().describe("Agent ID (defaults to context agent)")
|
|
71132
72915
|
}, async ({ agent_id }) => {
|
|
71133
72916
|
try {
|
|
71134
|
-
const { listTasks: listTasks3, getBlockedTasks: getBlockedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71135
72917
|
const focus = ctx.getAgentFocus(agent_id || "");
|
|
71136
72918
|
const effectiveAgentId = focus ? focus.agent_id : agent_id || "";
|
|
71137
72919
|
if (!effectiveAgentId) {
|
|
71138
72920
|
return { content: [{ type: "text", text: "No agent_id provided and no agent focus is active." }], isError: true };
|
|
71139
72921
|
}
|
|
72922
|
+
const cloud = getTodosCloudClient();
|
|
72923
|
+
if (cloud) {
|
|
72924
|
+
const remoteAssigned = await cloudListTasks(cloud, { assigned_to: effectiveAgentId, limit: 500 });
|
|
72925
|
+
const remoteNow = Date.now();
|
|
72926
|
+
const remoteDueSoon = remoteNow + 24 * 60 * 60 * 1000;
|
|
72927
|
+
const remoteBlocked = await cloudBlockingDepsMap(cloud, remoteAssigned.filter((t) => !["completed", "cancelled", "failed"].includes(t.status)));
|
|
72928
|
+
const remoteLines = [
|
|
72929
|
+
`Agent: ${effectiveAgentId}`,
|
|
72930
|
+
`In Progress: ${remoteAssigned.filter((t) => t.status === "in_progress").length}`,
|
|
72931
|
+
`Pending: ${remoteAssigned.filter((t) => t.status === "pending").length}`,
|
|
72932
|
+
`Completed (recent): ${remoteAssigned.filter((t) => t.status === "completed" && t.completed_at && remoteNow - new Date(t.completed_at).getTime() <= 7 * 24 * 60 * 60 * 1000).length}`,
|
|
72933
|
+
`Due Soon: ${remoteAssigned.filter((t) => t.due_at && new Date(t.due_at).getTime() <= remoteDueSoon && new Date(t.due_at).getTime() >= remoteNow && !["completed", "cancelled", "failed"].includes(t.status)).length}`,
|
|
72934
|
+
`Blocked: ${remoteBlocked.size}`
|
|
72935
|
+
];
|
|
72936
|
+
return { content: [{ type: "text", text: remoteLines.join(`
|
|
72937
|
+
`) }] };
|
|
72938
|
+
}
|
|
72939
|
+
const { listTasks: listTasks3, getBlockedTasks: getBlockedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71140
72940
|
const assigned = listTasks3({ assigned_to: effectiveAgentId, limit: 500 }, undefined);
|
|
71141
72941
|
const now4 = Date.now();
|
|
71142
72942
|
const dueSoonCutoff = now4 + 24 * 60 * 60 * 1000;
|
|
@@ -71170,6 +72970,60 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
71170
72970
|
max_per_agent: exports_external.number().optional().describe("Max tasks per agent (default: 5)")
|
|
71171
72971
|
}, async ({ project_id, max_per_agent }) => {
|
|
71172
72972
|
try {
|
|
72973
|
+
const cloud = getTodosCloudClient();
|
|
72974
|
+
if (cloud) {
|
|
72975
|
+
const cap = max_per_agent || 5;
|
|
72976
|
+
const remoteProjectId = project_id ? await cloudResolveProjectRef(cloud, project_id) : undefined;
|
|
72977
|
+
const roster = (await cloudListAgents(cloud)).filter((a) => a.status !== "archived");
|
|
72978
|
+
if (roster.length === 0)
|
|
72979
|
+
return { content: [{ type: "text", text: "No active agents available for rebalancing." }] };
|
|
72980
|
+
const active = await cloudListTasksCompleteBounded(cloud, {
|
|
72981
|
+
status: ["pending", "in_progress"],
|
|
72982
|
+
...remoteProjectId ? { project_id: remoteProjectId } : {}
|
|
72983
|
+
});
|
|
72984
|
+
for (const task2 of active) {
|
|
72985
|
+
if (!Number.isSafeInteger(task2.version) || typeof task2.status !== "string") {
|
|
72986
|
+
throw new Error("REMOTE_API_INCOMPATIBLE: rebalance selection contains an incomplete task");
|
|
72987
|
+
}
|
|
72988
|
+
}
|
|
72989
|
+
const keyByAlias = new Map;
|
|
72990
|
+
const ambiguous = new Set;
|
|
72991
|
+
const index = (alias2, agentId) => {
|
|
72992
|
+
if (!alias2)
|
|
72993
|
+
return;
|
|
72994
|
+
const key3 = alias2.toLowerCase();
|
|
72995
|
+
if (ambiguous.has(key3))
|
|
72996
|
+
return;
|
|
72997
|
+
const seen = keyByAlias.get(key3);
|
|
72998
|
+
if (seen !== undefined && seen !== agentId) {
|
|
72999
|
+
keyByAlias.delete(key3);
|
|
73000
|
+
ambiguous.add(key3);
|
|
73001
|
+
return;
|
|
73002
|
+
}
|
|
73003
|
+
keyByAlias.set(key3, agentId);
|
|
73004
|
+
};
|
|
73005
|
+
for (const agent of roster) {
|
|
73006
|
+
index(agent.id, agent.id);
|
|
73007
|
+
index(agent.name, agent.id);
|
|
73008
|
+
}
|
|
73009
|
+
const keyOf = (assignedTo) => assignedTo ? keyByAlias.get(assignedTo.toLowerCase()) : undefined;
|
|
73010
|
+
const load2 = new Map(roster.map((a) => [a.id, active.filter((t) => keyOf(t.assigned_to) === a.id).length]));
|
|
73011
|
+
let moved2 = 0;
|
|
73012
|
+
let skipped2 = 0;
|
|
73013
|
+
for (const t of active.filter((t2) => t2.status === "pending" && keyOf(t2.assigned_to) && (load2.get(keyOf(t2.assigned_to)) ?? 0) > cap)) {
|
|
73014
|
+
const from = keyOf(t.assigned_to);
|
|
73015
|
+
const target = roster.filter((a) => a.id !== from).sort((a, b) => (load2.get(a.id) ?? 0) - (load2.get(b.id) ?? 0))[0];
|
|
73016
|
+
if (!target || (load2.get(target.id) ?? 0) >= cap) {
|
|
73017
|
+
skipped2++;
|
|
73018
|
+
continue;
|
|
73019
|
+
}
|
|
73020
|
+
await cloudUpdateTaskVerified(cloud, t.id, { assigned_to: target.id, version: t.version });
|
|
73021
|
+
load2.set(from, (load2.get(from) ?? 1) - 1);
|
|
73022
|
+
load2.set(target.id, (load2.get(target.id) ?? 0) + 1);
|
|
73023
|
+
moved2++;
|
|
73024
|
+
}
|
|
73025
|
+
return { content: [{ type: "text", text: `Rebalanced: moved ${moved2} task(s), ${skipped2} skipped.` }] };
|
|
73026
|
+
}
|
|
71173
73027
|
const { listAgents: listAgents2 } = (init_agents(), __toCommonJS(exports_agents));
|
|
71174
73028
|
const { listTasks: listTasks3, updateTask: updateTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71175
73029
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
@@ -71226,6 +73080,20 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
71226
73080
|
agent_id: exports_external.string().optional().describe("Filter by assignee")
|
|
71227
73081
|
}, async ({ hours = 24, project_id, agent_id }) => {
|
|
71228
73082
|
try {
|
|
73083
|
+
const cloud = getTodosCloudClient();
|
|
73084
|
+
if (cloud) {
|
|
73085
|
+
const horizon = Date.now() + hours * 60 * 60 * 1000;
|
|
73086
|
+
const remote = (await cloudListTasks(cloud, {
|
|
73087
|
+
...project_id ? { project_id } : {},
|
|
73088
|
+
...agent_id ? { assigned_to: agent_id } : {}
|
|
73089
|
+
})).filter((t) => t.due_at && !["completed", "cancelled", "failed"].includes(t.status) && new Date(t.due_at).getTime() <= horizon && new Date(t.due_at).getTime() >= Date.now()).sort((a, b) => (a.due_at ?? "").localeCompare(b.due_at ?? ""));
|
|
73090
|
+
if (remote.length === 0)
|
|
73091
|
+
return { content: [{ type: "text", text: "No deadlines approaching." }] };
|
|
73092
|
+
const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} ${t.title} due ${t.due_at}`);
|
|
73093
|
+
return { content: [{ type: "text", text: `${remote.length} task(s) due within ${hours}h:
|
|
73094
|
+
${remoteLines.join(`
|
|
73095
|
+
`)}` }] };
|
|
73096
|
+
}
|
|
71229
73097
|
const { notifyUpcomingDeadlines: notifyUpcomingDeadlines2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71230
73098
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
71231
73099
|
const resolvedAgentId = agent_id ? resolveId(agent_id, "agents") : undefined;
|
|
@@ -71248,6 +73116,16 @@ ${lines.join(`
|
|
|
71248
73116
|
limit: exports_external.number().optional().describe("Max results (default: 50)")
|
|
71249
73117
|
}, async ({ project_id, agent_id, limit }) => {
|
|
71250
73118
|
try {
|
|
73119
|
+
const cloud = getTodosCloudClient();
|
|
73120
|
+
if (cloud) {
|
|
73121
|
+
const remote = (await cloudEscalatedTasks(cloud, { project_id, agent_id })).slice(0, limit || 50);
|
|
73122
|
+
if (remote.length === 0)
|
|
73123
|
+
return { content: [{ type: "text", text: "No SLA breaches or overdue tasks." }] };
|
|
73124
|
+
const remoteLines = remote.map((item) => `${item.task.short_id || item.task.id.slice(0, 8)} ${item.task.title} ${item.reasons.join(",")} breached ${item.breached_at}`);
|
|
73125
|
+
return { content: [{ type: "text", text: `${remote.length} escalation(s):
|
|
73126
|
+
${remoteLines.join(`
|
|
73127
|
+
`)}` }] };
|
|
73128
|
+
}
|
|
71251
73129
|
const { getEscalatedTasks: getEscalatedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71252
73130
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
71253
73131
|
const resolvedAgentId = agent_id ? resolveId(agent_id, "agents") : undefined;
|
|
@@ -71273,6 +73151,16 @@ ${lines.join(`
|
|
|
71273
73151
|
project_id: exports_external.string().optional().describe("Filter by project")
|
|
71274
73152
|
}, async ({ hours = 48, minutes: minutes2, project_id }) => {
|
|
71275
73153
|
try {
|
|
73154
|
+
const cloud = getTodosCloudClient();
|
|
73155
|
+
if (cloud) {
|
|
73156
|
+
const remote = await cloudStaleTasks(cloud, minutes2 ?? hours * 60, project_id ? { project_id } : {});
|
|
73157
|
+
if (remote.length === 0)
|
|
73158
|
+
return { content: [{ type: "text", text: "No stale tasks." }] };
|
|
73159
|
+
const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title} \u2014 last updated ${t.updated_at}`);
|
|
73160
|
+
return { content: [{ type: "text", text: `${remote.length} stale task(s):
|
|
73161
|
+
${remoteLines.join(`
|
|
73162
|
+
`)}` }] };
|
|
73163
|
+
}
|
|
71276
73164
|
const { getStaleTasks: getStaleTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71277
73165
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
71278
73166
|
const tasks = getStaleTasks2({ hours, minutes: minutes2, project_id: resolvedProjectId });
|
|
@@ -71292,6 +73180,18 @@ ${lines.join(`
|
|
|
71292
73180
|
project_id: exports_external.string().optional().describe("Filter by project")
|
|
71293
73181
|
}, async ({ project_id }) => {
|
|
71294
73182
|
try {
|
|
73183
|
+
const cloud = getTodosCloudClient();
|
|
73184
|
+
if (cloud) {
|
|
73185
|
+
const candidates = (await cloudListTasks(cloud, project_id ? { project_id } : {})).filter((t) => !["completed", "cancelled"].includes(t.status));
|
|
73186
|
+
const blocking = await cloudBlockingDepsMap(cloud, candidates);
|
|
73187
|
+
const remote = candidates.filter((t) => blocking.has(t.id));
|
|
73188
|
+
if (remote.length === 0)
|
|
73189
|
+
return { content: [{ type: "text", text: "No blocked tasks." }] };
|
|
73190
|
+
const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title} \u2014 blocked by ${(blocking.get(t.id) ?? []).map((b) => b.id.slice(0, 8)).join(", ")}`);
|
|
73191
|
+
return { content: [{ type: "text", text: `${remote.length} blocked task(s):
|
|
73192
|
+
${remoteLines.join(`
|
|
73193
|
+
`)}` }] };
|
|
73194
|
+
}
|
|
71295
73195
|
const { getBlockedTasks: getBlockedTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71296
73196
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
71297
73197
|
const tasks = getBlockedTasks2(resolvedProjectId);
|
|
@@ -71311,6 +73211,24 @@ ${lines.join(`
|
|
|
71311
73211
|
project_id: exports_external.string().optional().describe("Filter by project")
|
|
71312
73212
|
}, async ({ project_id }) => {
|
|
71313
73213
|
try {
|
|
73214
|
+
const cloud = getTodosCloudClient();
|
|
73215
|
+
if (cloud) {
|
|
73216
|
+
const edges = await cloudAllDependencies(cloud);
|
|
73217
|
+
const dependentCount = new Map;
|
|
73218
|
+
for (const edge of edges) {
|
|
73219
|
+
if (!edge.task_id || !edge.depends_on)
|
|
73220
|
+
continue;
|
|
73221
|
+
dependentCount.set(edge.depends_on, (dependentCount.get(edge.depends_on) ?? 0) + 1);
|
|
73222
|
+
}
|
|
73223
|
+
const blockers = await cloudGetTasksByIds(cloud, [...dependentCount.keys()]);
|
|
73224
|
+
const remote = [...blockers.values()].filter((t) => !["completed", "cancelled"].includes(t.status)).filter((t) => !project_id || t.project_id === project_id).sort((a, b) => (dependentCount.get(b.id) ?? 0) - (dependentCount.get(a.id) ?? 0));
|
|
73225
|
+
if (remote.length === 0)
|
|
73226
|
+
return { content: [{ type: "text", text: "No tasks blocking others." }] };
|
|
73227
|
+
const remoteLines = remote.map((t) => `${t.short_id || t.id.slice(0, 8)} [${t.status}] ${t.title} \u2014 blocking ${dependentCount.get(t.id) ?? 0} task(s)`);
|
|
73228
|
+
return { content: [{ type: "text", text: `${remote.length} blocking task(s):
|
|
73229
|
+
${remoteLines.join(`
|
|
73230
|
+
`)}` }] };
|
|
73231
|
+
}
|
|
71314
73232
|
const { getBlockingTasks: getBlockingTasks2 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
71315
73233
|
const resolvedProjectId = project_id ? resolveId(project_id, "projects") : undefined;
|
|
71316
73234
|
const tasks = getBlockingTasks2(resolvedProjectId);
|
|
@@ -71376,6 +73294,16 @@ ${lines.join(`
|
|
|
71376
73294
|
apply: exports_external.boolean().optional().describe("Apply safe repairs. Defaults to false/dry-run.")
|
|
71377
73295
|
}, async ({ apply }) => {
|
|
71378
73296
|
try {
|
|
73297
|
+
const cloud = getTodosCloudClient();
|
|
73298
|
+
if (cloud) {
|
|
73299
|
+
if (apply) {
|
|
73300
|
+
return { content: [{ type: "text", text: "run_doctor --apply is not available on the hosted authority: repairs are server-side. Re-run without apply for the hosted integrity report." }], isError: true };
|
|
73301
|
+
}
|
|
73302
|
+
const report = await cloudGetIntegrityReport(cloud);
|
|
73303
|
+
if (!report)
|
|
73304
|
+
throw new Error("The configured Todos authority returned no integrity report");
|
|
73305
|
+
return { content: [{ type: "text", text: JSON.stringify({ source: "cloud", integrity: report }, null, 2) }] };
|
|
73306
|
+
}
|
|
71379
73307
|
const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
|
|
71380
73308
|
const result = runTodosDoctor2({ apply: Boolean(apply) });
|
|
71381
73309
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -72548,10 +74476,35 @@ ${JSON.stringify(task2.metadata, null, 2)}` : null
|
|
|
72548
74476
|
project_id: exports_external.string().optional().describe("Filter by project")
|
|
72549
74477
|
}, async ({ agent_id, project_id }) => {
|
|
72550
74478
|
try {
|
|
72551
|
-
const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
72552
74479
|
const focus = ctx.getAgentFocus(agent_id || "");
|
|
72553
74480
|
const effectiveAgentId = focus ? focus.agent_id : agent_id || "";
|
|
72554
74481
|
const effectiveProjectId = focus?.project_id || project_id;
|
|
74482
|
+
const cloud = getTodosCloudClient();
|
|
74483
|
+
if (cloud) {
|
|
74484
|
+
const recap = await cloudRecap(cloud, 24, effectiveProjectId);
|
|
74485
|
+
const mine = (rows) => effectiveAgentId ? rows.filter((t) => (t.assigned_to ?? "").toLowerCase() === effectiveAgentId.toLowerCase()) : rows;
|
|
74486
|
+
const remoteInProgress = mine(recap.in_progress);
|
|
74487
|
+
const remoteCompleted = mine(recap.completed);
|
|
74488
|
+
const remoteBlocked = mine(recap.blocked);
|
|
74489
|
+
const remoteLines = [
|
|
74490
|
+
`Standup for ${effectiveAgentId} (${effectiveProjectId ? `project: ${effectiveProjectId.slice(0, 8)}` : "all projects"})`,
|
|
74491
|
+
remoteInProgress.length > 0 ? `
|
|
74492
|
+
In Progress (${remoteInProgress.length}):` : `
|
|
74493
|
+
No tasks in progress.`,
|
|
74494
|
+
...remoteInProgress.map((t) => ` - ${t.title} (${t.id.slice(0, 8)})`),
|
|
74495
|
+
remoteCompleted.length > 0 ? `
|
|
74496
|
+
Completed in the last 24h (${remoteCompleted.length}):` : `
|
|
74497
|
+
No tasks completed in the last 24h.`,
|
|
74498
|
+
...remoteCompleted.map((t) => ` - ${t.title} (${t.id.slice(0, 8)})`),
|
|
74499
|
+
remoteBlocked.length > 0 ? `
|
|
74500
|
+
Blocked (${remoteBlocked.length}):` : `
|
|
74501
|
+
No blocked tasks.`,
|
|
74502
|
+
...remoteBlocked.map((t) => ` - ${t.title} (${t.id.slice(0, 8)})`)
|
|
74503
|
+
].filter(Boolean);
|
|
74504
|
+
return { content: [{ type: "text", text: remoteLines.join(`
|
|
74505
|
+
`) }] };
|
|
74506
|
+
}
|
|
74507
|
+
const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
72555
74508
|
const inProgress = listTasks3({
|
|
72556
74509
|
assigned_to: effectiveAgentId,
|
|
72557
74510
|
status: "in_progress",
|
|
@@ -72595,13 +74548,25 @@ No blocked tasks.`,
|
|
|
72595
74548
|
if (shouldRegisterTool("claim_task")) {
|
|
72596
74549
|
server.tool("claim_task", "Alias for start_task \u2014 mark a task as in_progress and assign it to the calling agent.", {
|
|
72597
74550
|
task_id: exports_external.string().describe("Task ID"),
|
|
72598
|
-
agent_id: exports_external.string().
|
|
74551
|
+
agent_id: exports_external.string().describe("Exact agent ID or configured focus identity claiming the task")
|
|
72599
74552
|
}, async ({ task_id, agent_id }) => {
|
|
72600
74553
|
try {
|
|
74554
|
+
const requestedAgent = agent_id?.trim();
|
|
74555
|
+
if (!requestedAgent) {
|
|
74556
|
+
return {
|
|
74557
|
+
content: [{ type: "text", text: "claim_task requires agent_id; the placeholder identity 'mcp' is never substituted." }],
|
|
74558
|
+
isError: true
|
|
74559
|
+
};
|
|
74560
|
+
}
|
|
74561
|
+
const focus = ctx.getAgentFocus(requestedAgent);
|
|
74562
|
+
const effectiveAgent = focus?.agent_id || requestedAgent;
|
|
74563
|
+
const cloud = getTodosCloudClient();
|
|
74564
|
+
if (cloud) {
|
|
74565
|
+
const claimed = await cloudTaskAction(cloud, task_id, "start", { agent_id: effectiveAgent });
|
|
74566
|
+
return { content: [{ type: "text", text: formatTask(claimed) }] };
|
|
74567
|
+
}
|
|
72601
74568
|
const { startTask: startTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
72602
74569
|
const resolvedId = resolveId(task_id);
|
|
72603
|
-
const focus = ctx.getAgentFocus(agent_id || "");
|
|
72604
|
-
const effectiveAgent = focus ? focus.agent_id : agent_id || "mcp";
|
|
72605
74570
|
const task2 = startTask3(resolvedId, effectiveAgent);
|
|
72606
74571
|
return { content: [{ type: "text", text: formatTask(task2) }] };
|
|
72607
74572
|
} catch (e) {
|
|
@@ -72614,6 +74579,14 @@ No blocked tasks.`,
|
|
|
72614
74579
|
task_id: exports_external.string().describe("Task ID")
|
|
72615
74580
|
}, async ({ task_id }) => {
|
|
72616
74581
|
try {
|
|
74582
|
+
const cloud = getTodosCloudClient();
|
|
74583
|
+
if (cloud) {
|
|
74584
|
+
const current2 = await cloudGetTask(cloud, task_id);
|
|
74585
|
+
if (!current2)
|
|
74586
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
74587
|
+
const released = await cloudUpdateTask(cloud, current2.id, { status: "pending", assigned_to: null, version: current2.version });
|
|
74588
|
+
return { content: [{ type: "text", text: formatTask(released) }] };
|
|
74589
|
+
}
|
|
72617
74590
|
const { getTask: getTask2, updateTask: updateTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
72618
74591
|
const resolvedId = resolveId(task_id);
|
|
72619
74592
|
const current = getTask2(resolvedId);
|
|
@@ -72706,6 +74679,15 @@ No blocked tasks.`,
|
|
|
72706
74679
|
minutes: exports_external.number().describe("Additional minutes to add to estimate")
|
|
72707
74680
|
}, async ({ task_id, minutes: minutes2 }) => {
|
|
72708
74681
|
try {
|
|
74682
|
+
const cloud = getTodosCloudClient();
|
|
74683
|
+
if (cloud) {
|
|
74684
|
+
const current = await cloudGetTask(cloud, task_id);
|
|
74685
|
+
if (!current)
|
|
74686
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
74687
|
+
const before = current.estimated_minutes || 0;
|
|
74688
|
+
const after = await cloudUpdateTask(cloud, current.id, { estimated_minutes: before + minutes2, version: current.version });
|
|
74689
|
+
return { content: [{ type: "text", text: `Estimate updated: ${before} \u2192 ${after.estimated_minutes} min (+${minutes2})` }] };
|
|
74690
|
+
}
|
|
72709
74691
|
const { getTask: getTask2, updateTask: updateTask3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
72710
74692
|
const resolvedId = resolveId(task_id);
|
|
72711
74693
|
const task2 = getTask2(resolvedId);
|
|
@@ -72748,6 +74730,18 @@ No blocked tasks.`,
|
|
|
72748
74730
|
limit: exports_external.number().optional().describe("Max recent comments in compact mode (default: 20)")
|
|
72749
74731
|
}, async ({ task_id, detail, limit }) => {
|
|
72750
74732
|
try {
|
|
74733
|
+
const cloud = getTodosCloudClient();
|
|
74734
|
+
if (cloud) {
|
|
74735
|
+
const page = await cloudListComments(cloud, task_id, { limit: detail === "full" ? 500 : limit || 20 });
|
|
74736
|
+
if (page.comments.length === 0)
|
|
74737
|
+
return { content: [{ type: "text", text: "No comments." }] };
|
|
74738
|
+
const remoteLines = page.comments.map((c) => `[${c.agent_id || "?"}] ${c.created_at?.slice(0, 16)}: ${detail === "full" ? c.content : truncateText(c.content, 180)}`);
|
|
74739
|
+
if (page.has_more)
|
|
74740
|
+
remoteLines.unshift(`Showing ${page.comments.length} comment(s); older pages available.`);
|
|
74741
|
+
return { content: [{ type: "text", text: remoteLines.join(`
|
|
74742
|
+
|
|
74743
|
+
`) }] };
|
|
74744
|
+
}
|
|
72751
74745
|
const { listComments: listComments2 } = (init_comments(), __toCommonJS(exports_comments));
|
|
72752
74746
|
const comments = listComments2(resolveId(task_id));
|
|
72753
74747
|
if (comments.length === 0)
|
|
@@ -72772,10 +74766,23 @@ No blocked tasks.`,
|
|
|
72772
74766
|
limit: exports_external.number().optional()
|
|
72773
74767
|
}, async ({ agent_id, status: status3, project_id, limit }) => {
|
|
72774
74768
|
try {
|
|
72775
|
-
const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
72776
74769
|
const focus = ctx.getAgentFocus(agent_id || "");
|
|
72777
74770
|
const effectiveAgentId = focus ? focus.agent_id : agent_id || "";
|
|
72778
74771
|
const effectiveProjectId = focus?.project_id || project_id;
|
|
74772
|
+
const cloud = getTodosCloudClient();
|
|
74773
|
+
if (cloud) {
|
|
74774
|
+
const remote = await cloudListTasks(cloud, {
|
|
74775
|
+
assigned_to: effectiveAgentId,
|
|
74776
|
+
status: status3,
|
|
74777
|
+
project_id: effectiveProjectId,
|
|
74778
|
+
limit: limit || 50
|
|
74779
|
+
});
|
|
74780
|
+
if (remote.length === 0)
|
|
74781
|
+
return { content: [{ type: "text", text: "No tasks found." }] };
|
|
74782
|
+
return { content: [{ type: "text", text: remote.map(formatTask).join(`
|
|
74783
|
+
`) }] };
|
|
74784
|
+
}
|
|
74785
|
+
const { listTasks: listTasks3 } = (init_tasks(), __toCommonJS(exports_tasks));
|
|
72779
74786
|
const tasks = listTasks3({
|
|
72780
74787
|
assigned_to: effectiveAgentId,
|
|
72781
74788
|
status: status3,
|
|
@@ -72962,7 +74969,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
72962
74969
|
bulk_create_tasks: "bulk_create_tasks \u2014 Create multiple tasks. Params: tasks[]",
|
|
72963
74970
|
bulk_delete_tasks: "bulk_delete_tasks \u2014 Delete multiple tasks. Params: task_ids[], force",
|
|
72964
74971
|
archive_completed: "archive_completed \u2014 Auto-archive old completed tasks. Params: days, project_id",
|
|
72965
|
-
get_archived_tasks: "get_archived_tasks \u2014 List archived tasks. Params: project_id, limit",
|
|
74972
|
+
get_archived_tasks: "get_archived_tasks \u2014 List archived tasks. Params: project_id, limit, offset",
|
|
72966
74973
|
unarchive_task: "unarchive_task \u2014 Restore archived task. Params: task_id",
|
|
72967
74974
|
auto_assign_task: "auto_assign_task \u2014 Auto-assign based on capabilities. Params: task_id",
|
|
72968
74975
|
get_my_workload: "get_my_workload \u2014 Get agent workload stats. Params: agent_id",
|
|
@@ -72987,7 +74994,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
72987
74994
|
fail_task: "fail_task \u2014 Mark task failed. Params: task_id, reason, agent_id, version",
|
|
72988
74995
|
register_agent: "register_agent \u2014 Register an agent. Params: name, description, role, title, capabilities, session_id, working_dir, force",
|
|
72989
74996
|
list_agents: "list_agents \u2014 List registered agents. Params: include_archived",
|
|
72990
|
-
get_agent: "get_agent \u2014 Get agent details. Params: agent_id
|
|
74997
|
+
get_agent: "get_agent \u2014 Get agent details by exact ID. Params: agent_id (or id alias)",
|
|
72991
74998
|
update_agent: "update_agent \u2014 Update an agent. Params: agent_id, id, name, description, role, title, level, capabilities, permissions, metadata",
|
|
72992
74999
|
delete_agent: "delete_agent \u2014 Archive an agent. Params: agent_id, id, name",
|
|
72993
75000
|
unarchive_agent: "unarchive_agent \u2014 Restore an archived agent. Params: agent_id, id, name",
|
|
@@ -72998,9 +75005,7 @@ function registerTaskMetaTools(server, ctx) {
|
|
|
72998
75005
|
unfocus: "unfocus \u2014 Clear agent focus. Params: agent_id",
|
|
72999
75006
|
suggest_agent_name: "suggest_agent_name \u2014 Suggest available agent names. Params: working_dir",
|
|
73000
75007
|
get_org_chart: "get_org_chart \u2014 Get global org chart. Params: format",
|
|
73001
|
-
set_reports_to: "set_reports_to \u2014 Set org hierarchy. Params: agent_id, reports_to"
|
|
73002
|
-
extract_todos: "extract_todos \u2014 Scan code for TODO comments. Params: path, project_id, task_list_id, patterns, tags, assigned_to, agent_id, dry_run, extensions, exclude, respect_gitignore, include_index",
|
|
73003
|
-
watch_source_todos: "watch_source_todos \u2014 Run finite local source TODO watcher scans. Params: path, project_id, task_list_id, patterns, tags, assigned_to, agent_id, dry_run, extensions, exclude, respect_gitignore, interval_ms, max_runs"
|
|
75008
|
+
set_reports_to: "set_reports_to \u2014 Set org hierarchy. Params: agent_id, reports_to"
|
|
73004
75009
|
};
|
|
73005
75010
|
if (toolDocs[tool_name]) {
|
|
73006
75011
|
return { content: [{ type: "text", text: toolDocs[tool_name] }] };
|
|
@@ -80356,6 +82361,14 @@ ${lines.join(`
|
|
|
80356
82361
|
committed_at: exports_external.string().optional().describe("ISO timestamp of commit")
|
|
80357
82362
|
}, async ({ task_id, sha, message, author, files_changed, committed_at }) => {
|
|
80358
82363
|
try {
|
|
82364
|
+
const cloud = getTodosCloudClient();
|
|
82365
|
+
if (cloud) {
|
|
82366
|
+
if (committed_at !== undefined) {
|
|
82367
|
+
throw new Error("HOSTED_FIELD_UNSUPPORTED: committed_at is not accepted by the current Todos /v1 commit contract; omit it rather than recording the wrong timestamp");
|
|
82368
|
+
}
|
|
82369
|
+
const linked = await cloudLinkCommit(cloud, task_id, { sha, message, author, files_changed });
|
|
82370
|
+
return { content: [{ type: "text", text: JSON.stringify(linked, null, 2) }] };
|
|
82371
|
+
}
|
|
80359
82372
|
const resolvedId = resolveId(task_id);
|
|
80360
82373
|
const commit = linkTaskToCommit({ task_id: resolvedId, sha, message, author, files_changed, committed_at });
|
|
80361
82374
|
return { content: [{ type: "text", text: JSON.stringify(commit, null, 2) }] };
|
|
@@ -80367,6 +82380,14 @@ ${lines.join(`
|
|
|
80367
82380
|
if (shouldRegisterTool("get_task_commits")) {
|
|
80368
82381
|
server.tool("get_task_commits", "Get all git commits linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
|
|
80369
82382
|
try {
|
|
82383
|
+
const cloud = getTodosCloudClient();
|
|
82384
|
+
if (cloud) {
|
|
82385
|
+
const task3 = await cloudGetTask(cloud, task_id);
|
|
82386
|
+
if (!task3)
|
|
82387
|
+
throw new Error(`Task not found: ${task_id}`);
|
|
82388
|
+
const remote = await cloudListTaskCommits(cloud, task3.id);
|
|
82389
|
+
return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
|
|
82390
|
+
}
|
|
80370
82391
|
const commits = getTaskCommits(resolveId(task_id));
|
|
80371
82392
|
return { content: [{ type: "text", text: JSON.stringify(commits, null, 2) }] };
|
|
80372
82393
|
} catch (e) {
|
|
@@ -80377,6 +82398,13 @@ ${lines.join(`
|
|
|
80377
82398
|
if (shouldRegisterTool("find_task_by_commit")) {
|
|
80378
82399
|
server.tool("find_task_by_commit", "Find which task a git commit SHA is linked to. Supports prefix matching.", { sha: exports_external.string().describe("Git commit SHA (full or short prefix)") }, async ({ sha }) => {
|
|
80379
82400
|
try {
|
|
82401
|
+
const cloud = getTodosCloudClient();
|
|
82402
|
+
if (cloud) {
|
|
82403
|
+
const remote = await cloudFindCommit(cloud, sha);
|
|
82404
|
+
if (!remote)
|
|
82405
|
+
return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
|
|
82406
|
+
return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
|
|
82407
|
+
}
|
|
80380
82408
|
const result = findTaskByCommit(sha);
|
|
80381
82409
|
if (!result)
|
|
80382
82410
|
return { content: [{ type: "text", text: `No task linked to commit ${sha}` }] };
|
|
@@ -80396,6 +82424,11 @@ ${lines.join(`
|
|
|
80396
82424
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata")
|
|
80397
82425
|
}, async ({ task_id, ref_type, name, url, provider, metadata }) => {
|
|
80398
82426
|
try {
|
|
82427
|
+
const cloud = getTodosCloudClient();
|
|
82428
|
+
if (cloud) {
|
|
82429
|
+
const linked = await cloudLinkRef(cloud, task_id, { ref_type, name, url, provider, metadata });
|
|
82430
|
+
return { content: [{ type: "text", text: JSON.stringify(linked, null, 2) }] };
|
|
82431
|
+
}
|
|
80399
82432
|
const resolvedId = resolveId(task_id);
|
|
80400
82433
|
const ref = linkTaskGitRef({ task_id: resolvedId, ref_type, name, url, provider, metadata });
|
|
80401
82434
|
return { content: [{ type: "text", text: JSON.stringify(ref, null, 2) }] };
|
|
@@ -80407,6 +82440,11 @@ ${lines.join(`
|
|
|
80407
82440
|
if (shouldRegisterTool("get_task_git_refs")) {
|
|
80408
82441
|
server.tool("get_task_git_refs", "Get branches and pull requests linked to a task.", { task_id: exports_external.string().describe("Task ID") }, async ({ task_id }) => {
|
|
80409
82442
|
try {
|
|
82443
|
+
const cloud = getTodosCloudClient();
|
|
82444
|
+
if (cloud) {
|
|
82445
|
+
const remote = await cloudListTaskRefs(cloud, task_id);
|
|
82446
|
+
return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
|
|
82447
|
+
}
|
|
80410
82448
|
const refs = getTaskGitRefs(resolveId(task_id));
|
|
80411
82449
|
return { content: [{ type: "text", text: JSON.stringify(refs, null, 2) }] };
|
|
80412
82450
|
} catch (e) {
|
|
@@ -80417,6 +82455,11 @@ ${lines.join(`
|
|
|
80417
82455
|
if (shouldRegisterTool("find_tasks_by_git_ref")) {
|
|
80418
82456
|
server.tool("find_tasks_by_git_ref", "Find tasks linked to a branch name, PR number, or PR URL.", { ref: exports_external.string().describe("Branch name, PR number, or PR URL substring") }, async ({ ref }) => {
|
|
80419
82457
|
try {
|
|
82458
|
+
const cloud = getTodosCloudClient();
|
|
82459
|
+
if (cloud) {
|
|
82460
|
+
const remote = await cloudFindRefs(cloud, ref);
|
|
82461
|
+
return { content: [{ type: "text", text: JSON.stringify(remote, null, 2) }] };
|
|
82462
|
+
}
|
|
80420
82463
|
const refs = findTasksByGitRef(ref);
|
|
80421
82464
|
return { content: [{ type: "text", text: JSON.stringify(refs, null, 2) }] };
|
|
80422
82465
|
} catch (e) {
|
|
@@ -80435,6 +82478,14 @@ ${lines.join(`
|
|
|
80435
82478
|
run_at: exports_external.string().optional().describe("ISO timestamp when the command was run")
|
|
80436
82479
|
}, async ({ task_id, command, status: status3, output_summary, artifact_path, agent_id, run_at }) => {
|
|
80437
82480
|
try {
|
|
82481
|
+
const cloud = getTodosCloudClient();
|
|
82482
|
+
if (cloud) {
|
|
82483
|
+
if (run_at !== undefined) {
|
|
82484
|
+
throw new Error("HOSTED_FIELD_UNSUPPORTED: run_at is not accepted by the current Todos /v1 verification contract; omit it rather than recording the wrong timestamp");
|
|
82485
|
+
}
|
|
82486
|
+
const recorded = await cloudRecordVerification(cloud, task_id, { command, status: status3, output_summary, artifact_path, agent_id });
|
|
82487
|
+
return { content: [{ type: "text", text: JSON.stringify(recorded, null, 2) }] };
|
|
82488
|
+
}
|
|
80438
82489
|
const verification2 = addTaskVerification({
|
|
80439
82490
|
task_id: resolveId(task_id),
|
|
80440
82491
|
command,
|
|
@@ -81148,6 +83199,7 @@ ${lines.join(`
|
|
|
81148
83199
|
var init_task_resources = __esm(() => {
|
|
81149
83200
|
init_zod();
|
|
81150
83201
|
init_types();
|
|
83202
|
+
init_cloud_router();
|
|
81151
83203
|
init_tasks();
|
|
81152
83204
|
init_projects();
|
|
81153
83205
|
init_agents();
|
|
@@ -83154,92 +85206,6 @@ var init_task_rel_tools = __esm(() => {
|
|
|
83154
85206
|
init_zod();
|
|
83155
85207
|
});
|
|
83156
85208
|
|
|
83157
|
-
// src/mcp/tools/code-tools.ts
|
|
83158
|
-
function registerCodeTools(server, ctx) {
|
|
83159
|
-
const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
|
|
83160
|
-
if (shouldRegisterTool("extract_todos")) {
|
|
83161
|
-
server.tool("extract_todos", "Scan source files for TODO/FIXME/HACK/BUG/XXX/NOTE comments and create tasks from them. Deduplicates on re-runs.", {
|
|
83162
|
-
path: exports_external.string().describe("Directory or file path to scan"),
|
|
83163
|
-
project_id: exports_external.string().optional().describe("Project to assign tasks to"),
|
|
83164
|
-
task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
|
|
83165
|
-
patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
|
|
83166
|
-
tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
|
|
83167
|
-
assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
|
|
83168
|
-
agent_id: exports_external.string().optional().describe("Agent performing the extraction"),
|
|
83169
|
-
dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
|
|
83170
|
-
extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan (e.g. ['ts', 'py'])"),
|
|
83171
|
-
exclude: exports_external.array(exports_external.string()).optional().describe("Gitignore-style path patterns to skip"),
|
|
83172
|
-
respect_gitignore: exports_external.boolean().optional().describe("Respect .gitignore from the scanned root (default true)"),
|
|
83173
|
-
include_index: exports_external.boolean().optional().describe("Include local codebase index metadata in the response")
|
|
83174
|
-
}, async (params) => {
|
|
83175
|
-
try {
|
|
83176
|
-
const { extractTodos: extractTodos2 } = (init_extract(), __toCommonJS(exports_extract));
|
|
83177
|
-
const resolved = { ...params };
|
|
83178
|
-
if (resolved["project_id"])
|
|
83179
|
-
resolved["project_id"] = resolveId(resolved["project_id"], "projects");
|
|
83180
|
-
if (resolved["task_list_id"])
|
|
83181
|
-
resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
|
|
83182
|
-
const result = extractTodos2(resolved);
|
|
83183
|
-
if (params.dry_run) {
|
|
83184
|
-
if (params.include_index) {
|
|
83185
|
-
return { content: [{ type: "text", text: JSON.stringify({ comments: result.comments, index: result.index }, null, 2) }] };
|
|
83186
|
-
}
|
|
83187
|
-
const lines = result.comments.map((c) => `[${c.tag}] ${c.message} \u2014 ${c.file}:${c.line}${c.symbol ? ` (${c.symbol})` : ""}`);
|
|
83188
|
-
return { content: [{ type: "text", text: `Found ${result.comments.length} comment(s):
|
|
83189
|
-
${lines.join(`
|
|
83190
|
-
`)}` }] };
|
|
83191
|
-
}
|
|
83192
|
-
const summary = [
|
|
83193
|
-
`Created ${result.tasks.length} task(s)`,
|
|
83194
|
-
result.skipped > 0 ? `Skipped ${result.skipped} duplicate(s)` : null,
|
|
83195
|
-
`Total comments found: ${result.comments.length}`
|
|
83196
|
-
].filter(Boolean).join(`
|
|
83197
|
-
`);
|
|
83198
|
-
const taskLines = result.tasks.map(formatTask);
|
|
83199
|
-
return { content: [{ type: "text", text: `${summary}
|
|
83200
|
-
|
|
83201
|
-
${taskLines.join(`
|
|
83202
|
-
`)}` }] };
|
|
83203
|
-
} catch (e) {
|
|
83204
|
-
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
83205
|
-
}
|
|
83206
|
-
});
|
|
83207
|
-
}
|
|
83208
|
-
if (shouldRegisterTool("watch_source_todos")) {
|
|
83209
|
-
server.tool("watch_source_todos", "Run finite local source TODO watcher scans. Uses polling, respects .gitignore/excludes, and can dry-run or apply extracted tasks.", {
|
|
83210
|
-
path: exports_external.string().describe("Directory or file path to watch"),
|
|
83211
|
-
project_id: exports_external.string().optional().describe("Project to assign tasks to"),
|
|
83212
|
-
task_list_id: exports_external.string().optional().describe("Task list to add tasks to"),
|
|
83213
|
-
patterns: exports_external.array(exports_external.enum(["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"])).optional().describe("Tags to search for (default: all)"),
|
|
83214
|
-
tags: exports_external.array(exports_external.string()).optional().describe("Extra tags to add to created tasks"),
|
|
83215
|
-
assigned_to: exports_external.string().optional().describe("Agent to assign tasks to"),
|
|
83216
|
-
agent_id: exports_external.string().optional().describe("Agent performing the watcher scan"),
|
|
83217
|
-
dry_run: exports_external.boolean().optional().describe("If true, return found comments without creating tasks"),
|
|
83218
|
-
extensions: exports_external.array(exports_external.string()).optional().describe("File extensions to scan"),
|
|
83219
|
-
exclude: exports_external.array(exports_external.string()).optional().describe("Gitignore-style path patterns to skip"),
|
|
83220
|
-
respect_gitignore: exports_external.boolean().optional().describe("Respect .gitignore from the watched root"),
|
|
83221
|
-
interval_ms: exports_external.number().optional().describe("Polling interval for repeated scans"),
|
|
83222
|
-
max_runs: exports_external.number().optional().describe("Maximum scans before returning; default 1")
|
|
83223
|
-
}, async (params) => {
|
|
83224
|
-
try {
|
|
83225
|
-
const { watchSourceTodos: watchSourceTodos2 } = (init_extract(), __toCommonJS(exports_extract));
|
|
83226
|
-
const resolved = { ...params, include_index: true, once: (params.max_runs || 1) <= 1 };
|
|
83227
|
-
if (resolved["project_id"])
|
|
83228
|
-
resolved["project_id"] = resolveId(resolved["project_id"], "projects");
|
|
83229
|
-
if (resolved["task_list_id"])
|
|
83230
|
-
resolved["task_list_id"] = resolveId(resolved["task_list_id"], "task_lists");
|
|
83231
|
-
const result = await watchSourceTodos2(resolved);
|
|
83232
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
83233
|
-
} catch (e) {
|
|
83234
|
-
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
83235
|
-
}
|
|
83236
|
-
});
|
|
83237
|
-
}
|
|
83238
|
-
}
|
|
83239
|
-
var init_code_tools = __esm(() => {
|
|
83240
|
-
init_zod();
|
|
83241
|
-
});
|
|
83242
|
-
|
|
83243
85209
|
// src/cli/machine-api.ts
|
|
83244
85210
|
import { resolve as resolve22 } from "path";
|
|
83245
85211
|
function receipt(raw) {
|
|
@@ -83702,9 +85668,11 @@ Last seen: ${agent.last_seen_at}`
|
|
|
83702
85668
|
try {
|
|
83703
85669
|
const pool = getAgentPoolForProject(working_dir);
|
|
83704
85670
|
const cutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
83705
|
-
const
|
|
85671
|
+
const cloud = getTodosCloudClient();
|
|
85672
|
+
const roster = cloud ? await cloudListAgents(cloud) : listAgents();
|
|
85673
|
+
const allActive = roster.filter((a) => (a.last_seen_at ?? "") > cutoff);
|
|
83706
85674
|
if (!pool) {
|
|
83707
|
-
const
|
|
85675
|
+
const defaultPool = [
|
|
83708
85676
|
"caesar",
|
|
83709
85677
|
"augustus",
|
|
83710
85678
|
"marcus",
|
|
@@ -83724,7 +85692,8 @@ Last seen: ${agent.last_seen_at}`
|
|
|
83724
85692
|
"thalia",
|
|
83725
85693
|
"phoebe",
|
|
83726
85694
|
"daphne"
|
|
83727
|
-
]
|
|
85695
|
+
];
|
|
85696
|
+
const suggestions = cloud ? defaultPool.filter((name) => !roster.some((a) => a.name?.toLowerCase() === name)) : getAvailableNamesFromPool(defaultPool, getDatabase());
|
|
83728
85697
|
const lines2 = [
|
|
83729
85698
|
"No project pool configured. Use a distinctive one-word name; generic generated names are blocked.",
|
|
83730
85699
|
`Suggested names: ${suggestions.slice(0, 8).join(", ")}`,
|
|
@@ -83735,8 +85704,8 @@ To restrict names, configure agent_pool or project_pools in the todos data home
|
|
|
83735
85704
|
return { content: [{ type: "text", text: lines2.join(`
|
|
83736
85705
|
`) }] };
|
|
83737
85706
|
}
|
|
83738
|
-
const available = getAvailableNamesFromPool(pool, getDatabase());
|
|
83739
|
-
const activeInPool = allActive.filter((
|
|
85707
|
+
const available = cloud ? pool.filter((name) => !roster.some((a) => a.name?.toLowerCase() === name.toLowerCase())) : getAvailableNamesFromPool(pool, getDatabase());
|
|
85708
|
+
const activeInPool = allActive.filter((agent) => pool.map((name) => name.toLowerCase()).includes(agent.name.toLowerCase()));
|
|
83740
85709
|
const lines = [
|
|
83741
85710
|
`Project pool: ${pool.join(", ")}`,
|
|
83742
85711
|
`Available now (${available.length}): ${available.length > 0 ? available.join(", ") : "none \u2014 all names in use"}`,
|
|
@@ -83758,7 +85727,7 @@ No names available. Wait for an active agent to go stale (30min timeout).`
|
|
|
83758
85727
|
}, async ({ include_archived }) => {
|
|
83759
85728
|
try {
|
|
83760
85729
|
const cloud = getTodosCloudClient();
|
|
83761
|
-
const agents = cloud ? await cloudListAgents(cloud) : listAgents({ include_archived: include_archived ?? false });
|
|
85730
|
+
const agents = cloud ? await cloudListAgents(cloud, { include_archived: include_archived ?? false }) : listAgents({ include_archived: include_archived ?? false });
|
|
83762
85731
|
if (agents.length === 0) {
|
|
83763
85732
|
return { content: [{ type: "text", text: "No agents registered." }] };
|
|
83764
85733
|
}
|
|
@@ -83775,19 +85744,19 @@ ${text2}` }] };
|
|
|
83775
85744
|
});
|
|
83776
85745
|
}
|
|
83777
85746
|
if (shouldRegisterTool("get_agent")) {
|
|
83778
|
-
server.tool("get_agent", "Get agent details by
|
|
83779
|
-
agent_id: exports_external.string().optional(),
|
|
83780
|
-
id: exports_external.string().optional()
|
|
83781
|
-
|
|
83782
|
-
}, async ({ agent_id, id, name }) => {
|
|
85747
|
+
server.tool("get_agent", "Get agent details by exact immutable ID. Use list_agents to resolve a name first.", {
|
|
85748
|
+
agent_id: exports_external.string().optional().describe("Exact immutable agent ID"),
|
|
85749
|
+
id: exports_external.string().optional().describe("Compatibility alias for agent_id; exact immutable agent ID")
|
|
85750
|
+
}, async ({ agent_id, id }) => {
|
|
83783
85751
|
try {
|
|
83784
|
-
const identifier2 = agent_id || id ||
|
|
85752
|
+
const identifier2 = (agent_id || id || "").trim();
|
|
83785
85753
|
if (!identifier2) {
|
|
83786
|
-
return { content: [{ type: "text", text: "Provide agent_id
|
|
85754
|
+
return { content: [{ type: "text", text: "Provide the exact agent_id. Use list_agents to resolve a name first." }], isError: true };
|
|
83787
85755
|
}
|
|
83788
|
-
const
|
|
85756
|
+
const cloud = getTodosCloudClient();
|
|
85757
|
+
const agent = cloud ? await cloudGetAgent(cloud, identifier2) : getAgent(identifier2);
|
|
83789
85758
|
if (!agent) {
|
|
83790
|
-
return { content: [{ type: "text", text: `Agent not found: ${identifier2}` }], isError: true };
|
|
85759
|
+
return { content: [{ type: "text", text: `Agent not found by exact ID: ${identifier2}` }], isError: true };
|
|
83791
85760
|
}
|
|
83792
85761
|
const parts = [
|
|
83793
85762
|
`ID: ${agent.id}`,
|
|
@@ -83795,7 +85764,7 @@ ${text2}` }] };
|
|
|
83795
85764
|
];
|
|
83796
85765
|
if (agent.description)
|
|
83797
85766
|
parts.push(`Description: ${agent.description}`);
|
|
83798
|
-
if (Object.keys(agent.metadata).length > 0)
|
|
85767
|
+
if (agent.metadata && Object.keys(agent.metadata).length > 0)
|
|
83799
85768
|
parts.push(`Metadata: ${JSON.stringify(agent.metadata)}`);
|
|
83800
85769
|
parts.push(`Created: ${agent.created_at}`);
|
|
83801
85770
|
parts.push(`Last seen: ${agent.last_seen_at}`);
|
|
@@ -84083,6 +86052,13 @@ function registerTemplateTools(server, { shouldRegisterTool, resolveId, formatEr
|
|
|
84083
86052
|
})).optional().describe("Multi-task template: ordered list of tasks to create together with dependencies")
|
|
84084
86053
|
}, async (params) => {
|
|
84085
86054
|
try {
|
|
86055
|
+
const cloud = getTodosCloudClient();
|
|
86056
|
+
if (cloud) {
|
|
86057
|
+
const created = await cloudCreateTemplate(cloud, params);
|
|
86058
|
+
const remoteCount = created.tasks?.length ?? 0;
|
|
86059
|
+
const remoteInfo = remoteCount > 0 ? ` | ${remoteCount} task(s)` : "";
|
|
86060
|
+
return { content: [{ type: "text", text: `Template created: ${created.id.slice(0, 8)} | ${created.name} | "${created.title_pattern}"${remoteInfo}` }] };
|
|
86061
|
+
}
|
|
84086
86062
|
const { createTemplate: createTemplate3, getTemplateWithTasks: getTemplateWithTasks2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84087
86063
|
const t = createTemplate3(params);
|
|
84088
86064
|
const withTasks = getTemplateWithTasks2(t.id);
|
|
@@ -84097,8 +86073,8 @@ function registerTemplateTools(server, { shouldRegisterTool, resolveId, formatEr
|
|
|
84097
86073
|
if (shouldRegisterTool("list_templates")) {
|
|
84098
86074
|
server.tool("list_templates", "List all task templates", {}, async () => {
|
|
84099
86075
|
try {
|
|
84100
|
-
const
|
|
84101
|
-
const templates =
|
|
86076
|
+
const cloud = getTodosCloudClient();
|
|
86077
|
+
const templates = cloud ? await listSharedTemplates(cloud) : (await Promise.resolve().then(() => (init_templates(), exports_templates))).listTemplates();
|
|
84102
86078
|
if (templates.length === 0)
|
|
84103
86079
|
return { content: [{ type: "text", text: "No templates." }] };
|
|
84104
86080
|
const text2 = templates.map((t) => {
|
|
@@ -84125,6 +86101,28 @@ ${text2}` }] };
|
|
|
84125
86101
|
variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders in multi-task templates")
|
|
84126
86102
|
}, async (params) => {
|
|
84127
86103
|
try {
|
|
86104
|
+
const cloud = getTodosCloudClient();
|
|
86105
|
+
if (cloud) {
|
|
86106
|
+
const template = await resolveRemoteTemplate(cloud, params.template_id);
|
|
86107
|
+
if (!template)
|
|
86108
|
+
return { content: [{ type: "text", text: `Template not found: ${params.template_id}` }], isError: true };
|
|
86109
|
+
const projectRef = params.project_id ? await cloudResolveProjectRef(cloud, params.project_id) : undefined;
|
|
86110
|
+
const effectiveProject = projectRef ?? template.project_id ?? undefined;
|
|
86111
|
+
const taskListId = params.task_list_id ? await cloudResolveTaskListRef(cloud, params.task_list_id, effectiveProject) : undefined;
|
|
86112
|
+
const { tasks } = await createRemoteTemplateTasks(cloud, template, effectiveProject, params.variables ?? {}, {
|
|
86113
|
+
assignedTo: params.assigned_to,
|
|
86114
|
+
taskListId,
|
|
86115
|
+
overrides: {
|
|
86116
|
+
title: params.title,
|
|
86117
|
+
description: params.description,
|
|
86118
|
+
priority: params.priority
|
|
86119
|
+
}
|
|
86120
|
+
});
|
|
86121
|
+
const remoteText = tasks.map((t) => `${t.id.slice(0, 8)} | ${t.priority} | ${t.title}`).join(`
|
|
86122
|
+
`);
|
|
86123
|
+
return { content: [{ type: "text", text: `${tasks.length} task(s) created from template:
|
|
86124
|
+
${remoteText}` }] };
|
|
86125
|
+
}
|
|
84128
86126
|
const { taskFromTemplate: taskFromTemplate2, getTemplateWithTasks: getTemplateWithTasks2, tasksFromTemplate: tasksFromTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84129
86127
|
const resolvedTemplateId = resolveId(params.template_id, "task_templates");
|
|
84130
86128
|
const resolvedProjectId = params.project_id ? resolveId(params.project_id, "projects") : undefined;
|
|
@@ -84157,6 +86155,12 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
|
|
|
84157
86155
|
if (shouldRegisterTool("delete_template")) {
|
|
84158
86156
|
server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
|
|
84159
86157
|
try {
|
|
86158
|
+
const cloud = getTodosCloudClient();
|
|
86159
|
+
if (cloud) {
|
|
86160
|
+
const template = await resolveRemoteTemplate(cloud, id);
|
|
86161
|
+
const removed = template ? await cloudDeleteTemplate(cloud, template.id) : false;
|
|
86162
|
+
return { content: [{ type: "text", text: removed ? "Template deleted." : "Template not found." }] };
|
|
86163
|
+
}
|
|
84160
86164
|
const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84161
86165
|
const resolvedId = resolveId(id, "task_templates");
|
|
84162
86166
|
const deleted = deleteTemplate3(resolvedId);
|
|
@@ -84178,6 +86182,14 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
|
|
|
84178
86182
|
plan_id: exports_external.string().optional()
|
|
84179
86183
|
}, async ({ id, ...updates }) => {
|
|
84180
86184
|
try {
|
|
86185
|
+
const cloud = getTodosCloudClient();
|
|
86186
|
+
if (cloud) {
|
|
86187
|
+
const prior = await resolveRemoteTemplate(cloud, id);
|
|
86188
|
+
if (!prior)
|
|
86189
|
+
return { content: [{ type: "text", text: `Template not found: ${id}` }], isError: true };
|
|
86190
|
+
const patched = await updateSharedTemplate(cloud, prior.id, { ...updates, expected_version: prior.version });
|
|
86191
|
+
return { content: [{ type: "text", text: `Template updated: ${patched.id.slice(0, 8)} | ${patched.name} | "${patched.title_pattern}" | ${patched.priority}` }] };
|
|
86192
|
+
}
|
|
84181
86193
|
const { updateTemplate: updateTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84182
86194
|
const resolvedId = resolveId(id, "task_templates");
|
|
84183
86195
|
const t = updateTemplate3(resolvedId, updates);
|
|
@@ -84192,8 +86204,8 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
|
|
|
84192
86204
|
if (shouldRegisterTool("init_templates")) {
|
|
84193
86205
|
server.tool("init_templates", "Initialize the bundled local template library. Skips templates that already exist by name.", {}, async () => {
|
|
84194
86206
|
try {
|
|
84195
|
-
const
|
|
84196
|
-
const result =
|
|
86207
|
+
const cloud = getTodosCloudClient();
|
|
86208
|
+
const result = cloud ? await initializeSharedTemplates(cloud) : (await Promise.resolve().then(() => (init_builtin_templates(), exports_builtin_templates))).initBuiltinTemplates();
|
|
84197
86209
|
if (result.created === 0)
|
|
84198
86210
|
return { content: [{ type: "text", text: `All ${result.skipped} built-in template(s) already exist.` }] };
|
|
84199
86211
|
return { content: [{ type: "text", text: `Created ${result.created} template(s): ${result.names.join(", ")}. Skipped ${result.skipped} existing.` }] };
|
|
@@ -84205,8 +86217,7 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
|
|
|
84205
86217
|
if (shouldRegisterTool("list_template_library")) {
|
|
84206
86218
|
server.tool("list_template_library", "List the bundled marketplace-free local template library without mutating the database.", {}, async () => {
|
|
84207
86219
|
try {
|
|
84208
|
-
const
|
|
84209
|
-
const templates = listBuiltinTemplates2().map((template) => ({
|
|
86220
|
+
const templates = listBuiltinTemplates().map((template) => ({
|
|
84210
86221
|
name: template.name,
|
|
84211
86222
|
description: template.description,
|
|
84212
86223
|
category: template.category,
|
|
@@ -84223,8 +86234,7 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
|
|
|
84223
86234
|
if (shouldRegisterTool("write_template_library")) {
|
|
84224
86235
|
server.tool("write_template_library", "Write the bundled local template library to editable JSON files for review or import.", { directory: exports_external.string() }, async ({ directory }) => {
|
|
84225
86236
|
try {
|
|
84226
|
-
|
|
84227
|
-
return { content: [{ type: "text", text: JSON.stringify(writeBuiltinTemplateFiles2(directory), null, 2) }] };
|
|
86237
|
+
return { content: [{ type: "text", text: JSON.stringify(writeBuiltinTemplateFiles(directory), null, 2) }] };
|
|
84228
86238
|
} catch (e) {
|
|
84229
86239
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
84230
86240
|
}
|
|
@@ -84236,9 +86246,17 @@ ${task3.id.slice(0, 8)} | ${task3.priority} | ${task3.title}` }] };
|
|
|
84236
86246
|
variables: exports_external.record(exports_external.string()).optional().describe("Variable substitution map for {name} placeholders")
|
|
84237
86247
|
}, async (params) => {
|
|
84238
86248
|
try {
|
|
84239
|
-
const
|
|
84240
|
-
|
|
84241
|
-
|
|
86249
|
+
const cloud = getTodosCloudClient();
|
|
86250
|
+
let preview;
|
|
86251
|
+
if (cloud) {
|
|
86252
|
+
const template = await resolveRemoteTemplate(cloud, params.template_id);
|
|
86253
|
+
if (!template)
|
|
86254
|
+
return { content: [{ type: "text", text: `Template not found: ${params.template_id}` }], isError: true };
|
|
86255
|
+
preview = previewRemoteTemplate(template, params.variables);
|
|
86256
|
+
} else {
|
|
86257
|
+
const { previewTemplate: previewTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
86258
|
+
preview = previewTemplate2(resolveId(params.template_id, "task_templates"), params.variables);
|
|
86259
|
+
}
|
|
84242
86260
|
const lines = preview.tasks.map((t) => {
|
|
84243
86261
|
const deps = t.depends_on_positions.length > 0 ? ` (after: ${t.depends_on_positions.join(", ")})` : "";
|
|
84244
86262
|
return ` [${t.position}] ${t.priority} | ${t.title}${deps}`;
|
|
@@ -84258,6 +86276,13 @@ ${lines.join(`
|
|
|
84258
86276
|
if (shouldRegisterTool("export_template")) {
|
|
84259
86277
|
server.tool("export_template", "Export a template as a full JSON object (template + tasks + variables). Useful for sharing or backup.", { template_id: exports_external.string() }, async ({ template_id }) => {
|
|
84260
86278
|
try {
|
|
86279
|
+
const cloud = getTodosCloudClient();
|
|
86280
|
+
if (cloud) {
|
|
86281
|
+
const template = await resolveRemoteTemplate(cloud, template_id);
|
|
86282
|
+
if (!template)
|
|
86283
|
+
return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
|
|
86284
|
+
return { content: [{ type: "text", text: JSON.stringify(exportRemoteTemplate(template), null, 2) }] };
|
|
86285
|
+
}
|
|
84261
86286
|
const { exportTemplate: exportTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84262
86287
|
const resolvedId = resolveId(template_id, "task_templates");
|
|
84263
86288
|
const json7 = exportTemplate2(resolvedId);
|
|
@@ -84270,8 +86295,13 @@ ${lines.join(`
|
|
|
84270
86295
|
if (shouldRegisterTool("import_template")) {
|
|
84271
86296
|
server.tool("import_template", "Import a template from a JSON string (as returned by export_template). Creates new template with new IDs.", { json: exports_external.string().describe("JSON string of the template export") }, async ({ json: json7 }) => {
|
|
84272
86297
|
try {
|
|
84273
|
-
const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84274
86298
|
const parsed = JSON.parse(json7);
|
|
86299
|
+
const cloud = getTodosCloudClient();
|
|
86300
|
+
if (cloud) {
|
|
86301
|
+
const imported = await cloudCreateTemplate(cloud, parsed);
|
|
86302
|
+
return { content: [{ type: "text", text: `Template imported: ${imported.id.slice(0, 8)} | ${imported.name} | "${imported.title_pattern}"` }] };
|
|
86303
|
+
}
|
|
86304
|
+
const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84275
86305
|
const t = importTemplate2(parsed);
|
|
84276
86306
|
return { content: [{ type: "text", text: `Template imported: ${t.id.slice(0, 8)} | ${t.name} | "${t.title_pattern}"` }] };
|
|
84277
86307
|
} catch (e) {
|
|
@@ -84282,6 +86312,22 @@ ${lines.join(`
|
|
|
84282
86312
|
if (shouldRegisterTool("template_history")) {
|
|
84283
86313
|
server.tool("template_history", "Show version history of a template. Each update creates a snapshot of the previous state.", { template_id: exports_external.string() }, async ({ template_id }) => {
|
|
84284
86314
|
try {
|
|
86315
|
+
const cloud = getTodosCloudClient();
|
|
86316
|
+
if (cloud) {
|
|
86317
|
+
const template2 = await resolveRemoteTemplate(cloud, template_id);
|
|
86318
|
+
if (!template2)
|
|
86319
|
+
return { content: [{ type: "text", text: `Template not found: ${template_id}` }], isError: true };
|
|
86320
|
+
const history = await readSharedTemplateHistory(cloud, template2.id);
|
|
86321
|
+
if (history.versions.length === 0)
|
|
86322
|
+
return { content: [{ type: "text", text: `${template2.name} v${template2.version} \u2014 no previous versions.` }] };
|
|
86323
|
+
const remoteLines = history.versions.map((v) => {
|
|
86324
|
+
const snap = typeof v.snapshot === "string" ? JSON.parse(v.snapshot) : v.snapshot;
|
|
86325
|
+
return `v${v.version} | ${v.created_at} | ${snap.name} | "${snap.title_pattern}"`;
|
|
86326
|
+
});
|
|
86327
|
+
return { content: [{ type: "text", text: `${template2.name} \u2014 current: v${template2.version}
|
|
86328
|
+
${remoteLines.join(`
|
|
86329
|
+
`)}` }] };
|
|
86330
|
+
}
|
|
84285
86331
|
const { listTemplateVersions: listTemplateVersions2, getTemplate: getTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
|
|
84286
86332
|
const resolvedId = resolveId(template_id, "task_templates");
|
|
84287
86333
|
const template = getTemplate2(resolvedId);
|
|
@@ -84306,6 +86352,9 @@ ${lines.join(`
|
|
|
84306
86352
|
var init_templates2 = __esm(() => {
|
|
84307
86353
|
init_zod();
|
|
84308
86354
|
init_tasks();
|
|
86355
|
+
init_cloud_router();
|
|
86356
|
+
init_template_remote();
|
|
86357
|
+
init_builtin_template_library();
|
|
84309
86358
|
});
|
|
84310
86359
|
|
|
84311
86360
|
// src/lib/environment-snapshots.ts
|
|
@@ -84670,11 +86719,16 @@ var init_workflow_prompts2 = __esm(() => {
|
|
|
84670
86719
|
// src/mcp/index.ts
|
|
84671
86720
|
var exports_mcp = {};
|
|
84672
86721
|
__export(exports_mcp, {
|
|
86722
|
+
resolveTodosMcpAuthority: () => resolveTodosMcpAuthority,
|
|
86723
|
+
mcpHttpRefusal: () => mcpHttpRefusal,
|
|
86724
|
+
hostedRouteLocalStoreRefusal: () => hostedRouteLocalStoreRefusal,
|
|
84673
86725
|
formatTaskDetail: () => formatTaskDetail,
|
|
84674
86726
|
formatTask: () => formatTask,
|
|
84675
86727
|
formatError: () => formatError,
|
|
84676
86728
|
buildServer: () => buildServer,
|
|
84677
|
-
applyFocus: () => applyFocus
|
|
86729
|
+
applyFocus: () => applyFocus,
|
|
86730
|
+
agentFocusMap: () => agentFocusMap,
|
|
86731
|
+
MCP_HTTP_REFUSED_EXIT_CODE: () => MCP_HTTP_REFUSED_EXIT_CODE
|
|
84678
86732
|
});
|
|
84679
86733
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
84680
86734
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -84693,18 +86747,18 @@ function printHelp() {
|
|
|
84693
86747
|
Start the @hasna/todos MCP server.
|
|
84694
86748
|
|
|
84695
86749
|
Options:
|
|
84696
|
-
--stdio Use stdio transport (
|
|
84697
|
-
--http Use Streamable HTTP transport
|
|
84698
|
-
--port <port> Use Streamable HTTP on the given port (implies --http)
|
|
86750
|
+
--stdio Use stdio transport (the only transport this binary serves)
|
|
84699
86751
|
-V, --version output the version number
|
|
84700
86752
|
-h, --help display help for command
|
|
84701
86753
|
|
|
84702
86754
|
Environment:
|
|
84703
86755
|
MCP_STDIO=1 Force stdio transport
|
|
84704
|
-
MCP_HTTP=1 Use Streamable HTTP transport
|
|
84705
|
-
MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
|
|
84706
86756
|
TODOS_PROFILE=<profile> Tool profile filter
|
|
84707
|
-
TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter
|
|
86757
|
+
TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter
|
|
86758
|
+
|
|
86759
|
+
The Streamable HTTP transport (POST /mcp) is served by \`todos-serve\`, which owns
|
|
86760
|
+
the HTTP listener and its auth posture; \`--http\`, \`--port\` and MCP_HTTP=1 are
|
|
86761
|
+
refused here (exit 2).`);
|
|
84708
86762
|
}
|
|
84709
86763
|
function shouldRegisterTool(name) {
|
|
84710
86764
|
return shouldRegisterToolForProfile2(name);
|
|
@@ -84713,7 +86767,7 @@ function getAgentFocus(agentId) {
|
|
|
84713
86767
|
const sessionFocus = agentFocusMap.get(agentId);
|
|
84714
86768
|
if (sessionFocus)
|
|
84715
86769
|
return sessionFocus;
|
|
84716
|
-
if (getTodosCloudClient())
|
|
86770
|
+
if (isLocalStoreRefused() || hasTodosEnvAuthorityIntent(process.env) || getTodosCloudClient())
|
|
84717
86771
|
return;
|
|
84718
86772
|
try {
|
|
84719
86773
|
const agent = getAgentByName(agentId) || getAgent(agentId);
|
|
@@ -84734,11 +86788,14 @@ function applyFocus(params, agentId) {
|
|
|
84734
86788
|
}
|
|
84735
86789
|
}
|
|
84736
86790
|
function guardRefusalPayload(message) {
|
|
84737
|
-
const match = /^(REMOTE_API_[A-Z_]+|API_DATABASE_FALLBACK_FORBIDDEN):\s*([\s\S]*)$/.exec(message);
|
|
86791
|
+
const match = /^(REMOTE_API_[A-Z_]+|REMOTE_COMMAND_[A-Z_]+|API_DATABASE_FALLBACK_FORBIDDEN):\s*([\s\S]*)$/.exec(message);
|
|
84738
86792
|
if (!match)
|
|
84739
86793
|
return;
|
|
84740
86794
|
const code = match[1];
|
|
84741
86795
|
const detail = match[2].trim();
|
|
86796
|
+
if (code.startsWith("REMOTE_COMMAND_")) {
|
|
86797
|
+
return JSON.stringify({ code, message: detail || message });
|
|
86798
|
+
}
|
|
84742
86799
|
return JSON.stringify({
|
|
84743
86800
|
code,
|
|
84744
86801
|
message: detail || message,
|
|
@@ -84934,7 +86991,6 @@ function buildServer() {
|
|
|
84934
86991
|
registerTaskMetaTools(server, toolContext);
|
|
84935
86992
|
registerTaskResources(server, toolContext);
|
|
84936
86993
|
registerTaskRelTools(server, toolContext);
|
|
84937
|
-
registerCodeTools(server, toolContext);
|
|
84938
86994
|
registerAgentTools(server, { ...toolContext, agentFocusMap });
|
|
84939
86995
|
registerTemplateTools(server, toolContext);
|
|
84940
86996
|
registerEnvironmentSnapshotTools(server, toolContext);
|
|
@@ -84943,30 +86999,57 @@ function buildServer() {
|
|
|
84943
86999
|
registerDispatchTools(server, { shouldRegisterTool: shouldRegisterToolOnce, resolveId, formatError });
|
|
84944
87000
|
return server;
|
|
84945
87001
|
}
|
|
87002
|
+
function resolveTodosMcpAuthority(env2 = process.env) {
|
|
87003
|
+
const resolution = resolveTodosCliTransport(env2);
|
|
87004
|
+
if (resolution.transport === "sqlite") {
|
|
87005
|
+
return { route: "local", notice: todosLocalModeNotice("local-opt-in") };
|
|
87006
|
+
}
|
|
87007
|
+
return { route: "hosted", v1_base_url: resolution.authority.baseUrl, source: resolution.source };
|
|
87008
|
+
}
|
|
87009
|
+
function hostedRouteLocalStoreRefusal(v1BaseUrl) {
|
|
87010
|
+
return "REMOTE_COMMAND_UNSUPPORTED: this tool or resource is local-only \u2014 it reads the on-box SQLite " + `store, which the hosted Todos /v1 authority (${v1BaseUrl}) does not serve; local SQLite is ` + "opt-in only (HASNA_TODOS_LOCAL=1, alias TODOS_LOCAL=1) and is disabled by default. Use the hosted " + "tools on this route (list_tasks, list_projects, list_agents, get_status, ...) instead.";
|
|
87011
|
+
}
|
|
87012
|
+
function mcpHttpRefusal() {
|
|
87013
|
+
return "todos-mcp: the HTTP transport is not served by this binary (stdio only). " + "Use `todos-serve` \u2014 it mounts the MCP Streamable HTTP endpoint at POST /mcp with its own auth " + 'posture (see README "REST API"). `--http`, `--port` and MCP_HTTP=1 are refused.';
|
|
87014
|
+
}
|
|
84946
87015
|
async function main() {
|
|
84947
|
-
const { isHttpMode
|
|
87016
|
+
const { isHttpMode } = await Promise.resolve().then(() => (init_http4(), exports_http));
|
|
84948
87017
|
const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
|
|
84949
|
-
if (
|
|
84950
|
-
|
|
87018
|
+
if (isHttpMode() || portRequested) {
|
|
87019
|
+
process.stderr.write(`${mcpHttpRefusal()}
|
|
87020
|
+
`);
|
|
87021
|
+
process.exit(MCP_HTTP_REFUSED_EXIT_CODE);
|
|
87022
|
+
}
|
|
87023
|
+
let authority;
|
|
87024
|
+
try {
|
|
87025
|
+
authority = resolveTodosMcpAuthority();
|
|
87026
|
+
} catch (error2) {
|
|
87027
|
+
process.stderr.write(`${error2 instanceof Error ? error2.message : String(error2)}
|
|
87028
|
+
`);
|
|
87029
|
+
process.exit(1);
|
|
87030
|
+
}
|
|
87031
|
+
if (authority.route === "hosted") {
|
|
87032
|
+
refuseLocalStore(hostedRouteLocalStoreRefusal(authority.v1_base_url));
|
|
87033
|
+
}
|
|
87034
|
+
const server = buildServer();
|
|
87035
|
+
if (authority.route === "local") {
|
|
87036
|
+
process.stderr.write(`${authority.notice}
|
|
87037
|
+
`);
|
|
84951
87038
|
try {
|
|
84952
87039
|
const { startRuntimeShadowDrain: startRuntimeShadowDrain2 } = await Promise.resolve().then(() => (init_shadow_runtime(), exports_shadow_runtime));
|
|
84953
|
-
|
|
84954
|
-
startRuntimeShadowDrain2(getDatabase());
|
|
87040
|
+
startRuntimeShadowDrain2(getDatabase());
|
|
84955
87041
|
} catch {}
|
|
84956
|
-
const transport = new StdioServerTransport;
|
|
84957
|
-
await server.connect(transport);
|
|
84958
|
-
return;
|
|
84959
87042
|
}
|
|
84960
|
-
const
|
|
84961
|
-
|
|
84962
|
-
await startServer(port, { host: "127.0.0.1", allowAnonymous: true });
|
|
84963
|
-
console.error(`todos MCP HTTP mounted at http://127.0.0.1:${port}/mcp`);
|
|
87043
|
+
const transport = new StdioServerTransport;
|
|
87044
|
+
await server.connect(transport);
|
|
84964
87045
|
}
|
|
84965
|
-
var agentFocusMap, API_DATABASE_FALLBACK_FORBIDDEN_SUGGESTION, isDirectRun;
|
|
87046
|
+
var agentFocusMap, API_DATABASE_FALLBACK_FORBIDDEN_SUGGESTION, MCP_HTTP_REFUSED_EXIT_CODE = 2, isDirectRun;
|
|
84966
87047
|
var init_mcp2 = __esm(() => {
|
|
84967
87048
|
init_cloud_router();
|
|
84968
87049
|
init_agents();
|
|
84969
87050
|
init_database();
|
|
87051
|
+
init_cloud_router();
|
|
87052
|
+
init_local_opt_in();
|
|
84970
87053
|
init_lock_display();
|
|
84971
87054
|
init_types();
|
|
84972
87055
|
init_local_encryption();
|
|
@@ -84979,7 +87062,6 @@ var init_mcp2 = __esm(() => {
|
|
|
84979
87062
|
init_task_meta_tools();
|
|
84980
87063
|
init_task_resources();
|
|
84981
87064
|
init_task_rel_tools();
|
|
84982
|
-
init_code_tools();
|
|
84983
87065
|
init_machines2();
|
|
84984
87066
|
init_agents2();
|
|
84985
87067
|
init_templates2();
|
|
@@ -85387,7 +87469,7 @@ function resolveClientIp(req, server) {
|
|
|
85387
87469
|
trustedProxies: trustedProxiesFromEnv()
|
|
85388
87470
|
}) ?? "unknown";
|
|
85389
87471
|
}
|
|
85390
|
-
function checkRateLimit(ip) {
|
|
87472
|
+
function checkRateLimit(ip, maximum) {
|
|
85391
87473
|
const now4 = Date.now();
|
|
85392
87474
|
const entry2 = rateLimitMap.get(ip);
|
|
85393
87475
|
if (!entry2 || now4 > entry2.resetAt) {
|
|
@@ -85395,7 +87477,7 @@ function checkRateLimit(ip) {
|
|
|
85395
87477
|
return { allowed: true };
|
|
85396
87478
|
}
|
|
85397
87479
|
entry2.count++;
|
|
85398
|
-
if (entry2.count >
|
|
87480
|
+
if (entry2.count > maximum) {
|
|
85399
87481
|
return { allowed: false, retryAfter: Math.ceil((entry2.resetAt - now4) / 1000) };
|
|
85400
87482
|
}
|
|
85401
87483
|
return { allowed: true };
|
|
@@ -85449,6 +87531,7 @@ function hasGeneratedApiKeysSafely() {
|
|
|
85449
87531
|
}
|
|
85450
87532
|
}
|
|
85451
87533
|
async function startServer(port, options) {
|
|
87534
|
+
const rateLimitMax = resolveRateLimitMax(env.rateLimitMax());
|
|
85452
87535
|
const envServerKey = resolveServerKeyEnv();
|
|
85453
87536
|
const cliApiKey = options?.apiKey || null;
|
|
85454
87537
|
const apiKey = cliApiKey || envServerKey?.value || null;
|
|
@@ -85543,7 +87626,7 @@ data: ${data}
|
|
|
85543
87626
|
}
|
|
85544
87627
|
const ip = resolveClientIp(req, server2);
|
|
85545
87628
|
const peerIp = server2.requestIP(req)?.address;
|
|
85546
|
-
const rl = checkRateLimit(ip);
|
|
87629
|
+
const rl = checkRateLimit(ip, rateLimitMax);
|
|
85547
87630
|
if (!rl.allowed) {
|
|
85548
87631
|
return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
|
|
85549
87632
|
status: 429,
|
|
@@ -85818,7 +87901,7 @@ data: ${data}
|
|
|
85818
87901
|
const serverUrl = `http://localhost:${boundPort}`;
|
|
85819
87902
|
console.log(`Todos HTTP server running at ${serverUrl}`);
|
|
85820
87903
|
}
|
|
85821
|
-
var SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000
|
|
87904
|
+
var SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000;
|
|
85822
87905
|
var init_serve = __esm(() => {
|
|
85823
87906
|
init_database();
|
|
85824
87907
|
init_api_keys();
|
|
@@ -85834,7 +87917,6 @@ var init_serve = __esm(() => {
|
|
|
85834
87917
|
"Permissions-Policy": "camera=, microphone=, geolocation="
|
|
85835
87918
|
};
|
|
85836
87919
|
rateLimitMap = new Map;
|
|
85837
|
-
RATE_LIMIT_MAX = Number.parseInt(env.rateLimitMax() || "120", 10);
|
|
85838
87920
|
});
|
|
85839
87921
|
|
|
85840
87922
|
// src/cli/components/Header.tsx
|
|
@@ -88367,7 +90449,7 @@ ${tasks.length} task(s) shown`));
|
|
|
88367
90449
|
});
|
|
88368
90450
|
await new Promise(() => {});
|
|
88369
90451
|
});
|
|
88370
|
-
program2.command("stream").description("Subscribe to real-time task events via SSE (requires todos serve)").option("--agent <id>", "Filter to events for a specific agent").option("--events <list>", "Comma-separated event types (default: all)", "task.created,task.started,task.completed,task.failed,task.assigned,task.status_changed").option("--port <n>", "
|
|
90452
|
+
program2.command("stream").description("Subscribe to real-time task events via SSE (requires todos serve)").option("--agent <id>", "Filter to events for a specific agent").option("--events <list>", "Comma-separated event types (default: all)", "task.created,task.started,task.completed,task.failed,task.assigned,task.status_changed").option("--port <n>", "Port of the local `todos serve` to subscribe to", String(DEFAULT_PORT)).option("--json", "Output raw JSON events").action(async (opts) => {
|
|
88371
90453
|
const baseUrl = `http://localhost:${opts.port}`;
|
|
88372
90454
|
const params = new URLSearchParams;
|
|
88373
90455
|
if (opts.agent)
|
|
@@ -97185,17 +99267,23 @@ var init_cli_mcp_parity = __esm(() => {
|
|
|
97185
99267
|
"todos extract",
|
|
97186
99268
|
"todos extract-watch"
|
|
97187
99269
|
],
|
|
97188
|
-
mcpTools: [
|
|
97189
|
-
"extract_todos",
|
|
97190
|
-
"watch_source_todos"
|
|
97191
|
-
],
|
|
99270
|
+
mcpTools: [],
|
|
97192
99271
|
jsonContracts: ["source_code_index", "source_todo_comment", "task", "structured_error", "api_error"],
|
|
97193
99272
|
errorContracts: ["structured_error", "api_error"],
|
|
97194
|
-
status: "
|
|
97195
|
-
intentionalGaps: [
|
|
99273
|
+
status: "intentional-gap",
|
|
99274
|
+
intentionalGaps: [
|
|
99275
|
+
{
|
|
99276
|
+
cliCommand: "todos extract",
|
|
99277
|
+
reason: "The source TODO index scans the local checkout the CLI runs in; the former extract_todos MCP tool read the agent host's filesystem behind a task-store tool surface and had no hosted arm, so it was removed (fleet alignment 2026-09-11)."
|
|
99278
|
+
},
|
|
99279
|
+
{
|
|
99280
|
+
cliCommand: "todos extract-watch",
|
|
99281
|
+
reason: "A polling filesystem watcher is a long-running local process, not an MCP tool call; the former watch_source_todos tool was removed with extract_todos \u2014 run `todos extract-watch` from the checkout instead."
|
|
99282
|
+
}
|
|
99283
|
+
],
|
|
99284
|
+
gapReason: "Source scanning and watching are local-filesystem CLI operations; no MCP tool mapping.",
|
|
97196
99285
|
example: {
|
|
97197
|
-
cli: "todos extract . --dry-run --index --json"
|
|
97198
|
-
mcpTool: "extract_todos"
|
|
99286
|
+
cli: "todos extract . --dry-run --index --json"
|
|
97199
99287
|
}
|
|
97200
99288
|
},
|
|
97201
99289
|
{
|
|
@@ -102288,7 +104376,7 @@ function registerHelpCommands(program2, route = "local", remoteCapabilities = ne
|
|
|
102288
104376
|
});
|
|
102289
104377
|
const format = opts.json || globalOpts.json ? "json" : opts.format || "markdown";
|
|
102290
104378
|
if (format === "json") {
|
|
102291
|
-
|
|
104379
|
+
printJson(manual);
|
|
102292
104380
|
return;
|
|
102293
104381
|
}
|
|
102294
104382
|
if (format !== "markdown")
|