@hasna/todos 0.13.7 → 0.13.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/assignee-guard.d.ts +13 -0
- package/dist/cli/assignee-guard.d.ts.map +1 -0
- package/dist/cli/claim-guard.d.ts +3 -0
- package/dist/cli/claim-guard.d.ts.map +1 -0
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +3231 -3024
- package/dist/contracts.js +77 -29
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/index.js +110 -58
- package/dist/lib/agent-tasks.d.ts.map +1 -1
- package/dist/lib/assignee-context.d.ts +40 -0
- package/dist/lib/assignee-context.d.ts.map +1 -0
- package/dist/lib/assignee-validation.d.ts +72 -0
- package/dist/lib/assignee-validation.d.ts.map +1 -0
- package/dist/lib/claude-tasks.d.ts.map +1 -1
- package/dist/lib/sync-utils.d.ts +7 -0
- package/dist/lib/sync-utils.d.ts.map +1 -1
- package/dist/mcp/index.js +236 -117
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/registry.js +77 -29
- package/dist/release-provenance.json +5 -5
- package/dist/server/index.js +240 -121
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +62 -14
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -12080,6 +12080,50 @@ var init_checklists = __esm(() => {
|
|
|
12080
12080
|
init_database();
|
|
12081
12081
|
});
|
|
12082
12082
|
|
|
12083
|
+
// src/lib/creator-identity.ts
|
|
12084
|
+
import { existsSync as existsSync6, rmSync } from "fs";
|
|
12085
|
+
import { join as join5 } from "path";
|
|
12086
|
+
function identityFilePath() {
|
|
12087
|
+
return join5(getTodosGlobalDir(), "identity.json");
|
|
12088
|
+
}
|
|
12089
|
+
function readPersistedIdentity() {
|
|
12090
|
+
const path = identityFilePath();
|
|
12091
|
+
if (!existsSync6(path))
|
|
12092
|
+
return null;
|
|
12093
|
+
const parsed = readJsonFile(path);
|
|
12094
|
+
if (!parsed || typeof parsed.agent_id !== "string" || !parsed.agent_id.trim())
|
|
12095
|
+
return null;
|
|
12096
|
+
return parsed;
|
|
12097
|
+
}
|
|
12098
|
+
function canonicalAgentRef(value) {
|
|
12099
|
+
return value.trim().toLowerCase();
|
|
12100
|
+
}
|
|
12101
|
+
function isProcessBoundSource(source) {
|
|
12102
|
+
return source === "explicit" || source === "env";
|
|
12103
|
+
}
|
|
12104
|
+
function resolveWritableIdentity(explicit) {
|
|
12105
|
+
const resolved = resolveCreatorIdentity(explicit);
|
|
12106
|
+
if (!isProcessBoundSource(resolved.source))
|
|
12107
|
+
return { agent_id: null, source: "none" };
|
|
12108
|
+
return resolved;
|
|
12109
|
+
}
|
|
12110
|
+
function resolveCreatorIdentity(explicit) {
|
|
12111
|
+
const fromExplicit = explicit?.trim();
|
|
12112
|
+
if (fromExplicit)
|
|
12113
|
+
return { agent_id: canonicalAgentRef(fromExplicit), source: "explicit" };
|
|
12114
|
+
const fromEnv = (process.env["TODOS_AGENT_ID"] || process.env["HASNA_TODOS_AGENT_ID"] || "").trim();
|
|
12115
|
+
if (fromEnv)
|
|
12116
|
+
return { agent_id: canonicalAgentRef(fromEnv), source: "env" };
|
|
12117
|
+
const persisted = readPersistedIdentity();
|
|
12118
|
+
if (persisted) {
|
|
12119
|
+
return { agent_id: canonicalAgentRef(persisted.agent_name || persisted.agent_id), source: "persisted" };
|
|
12120
|
+
}
|
|
12121
|
+
return { agent_id: null, source: "none" };
|
|
12122
|
+
}
|
|
12123
|
+
var init_creator_identity = __esm(() => {
|
|
12124
|
+
init_sync_utils();
|
|
12125
|
+
});
|
|
12126
|
+
|
|
12083
12127
|
// src/lib/recurrence.ts
|
|
12084
12128
|
function parseRecurrenceRule(rule) {
|
|
12085
12129
|
const normalized = rule.trim().toLowerCase();
|
|
@@ -12806,6 +12850,11 @@ __export(exports_task_lifecycle, {
|
|
|
12806
12850
|
claimOrSteal: () => claimOrSteal,
|
|
12807
12851
|
claimNextTask: () => claimNextTask
|
|
12808
12852
|
});
|
|
12853
|
+
function sameHolder(stored, incoming) {
|
|
12854
|
+
if (!stored || !incoming)
|
|
12855
|
+
return false;
|
|
12856
|
+
return canonicalAgentRef(stored) === canonicalAgentRef(incoming);
|
|
12857
|
+
}
|
|
12809
12858
|
function lockExpiresAt(lockedAt) {
|
|
12810
12859
|
if (!lockedAt)
|
|
12811
12860
|
return null;
|
|
@@ -12856,13 +12905,13 @@ function startTask(id, agentId, db) {
|
|
|
12856
12905
|
const cutoff = lockExpiryCutoff();
|
|
12857
12906
|
const timestamp2 = now();
|
|
12858
12907
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
12859
|
-
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp2, timestamp2, timestamp2, id, agentId, cutoff]);
|
|
12908
|
+
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR LOWER(TRIM(locked_by)) = LOWER(TRIM(?)) OR locked_at < ?)`, [agentId, agentId, timestamp2, timestamp2, timestamp2, id, agentId, cutoff]);
|
|
12860
12909
|
if (result.changes === 0) {
|
|
12861
12910
|
const current = getTask(id, d);
|
|
12862
12911
|
if (!current)
|
|
12863
12912
|
throw new TaskNotFoundError(id);
|
|
12864
12913
|
assertStartable(current, agentId);
|
|
12865
|
-
if (current.locked_by && current.locked_by
|
|
12914
|
+
if (current.locked_by && !sameHolder(current.locked_by, agentId) && !isLockExpired(current.locked_at)) {
|
|
12866
12915
|
throw new LockError(id, current.locked_by);
|
|
12867
12916
|
}
|
|
12868
12917
|
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
@@ -12887,7 +12936,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
12887
12936
|
if (task.status === "cancelled") {
|
|
12888
12937
|
throw new Error(`Task ${id} is cancelled and cannot be completed`);
|
|
12889
12938
|
}
|
|
12890
|
-
if (agentId && task.locked_by && task.locked_by
|
|
12939
|
+
if (agentId && task.locked_by && !sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
|
|
12891
12940
|
throw new LockError(id, task.locked_by);
|
|
12892
12941
|
}
|
|
12893
12942
|
checkCompletionGuard(task, agentId || null, d);
|
|
@@ -13009,16 +13058,16 @@ function lockTask(id, agentId, db) {
|
|
|
13009
13058
|
error: `Task is ${task.status} and cannot be locked`
|
|
13010
13059
|
};
|
|
13011
13060
|
}
|
|
13012
|
-
if (task.locked_by
|
|
13061
|
+
if (sameHolder(task.locked_by, agentId) && !isLockExpired(task.locked_at)) {
|
|
13013
13062
|
const timestamp3 = now();
|
|
13014
|
-
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by =
|
|
13063
|
+
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND LOWER(TRIM(locked_by)) = LOWER(TRIM(?))`, [timestamp3, timestamp3, id, agentId]);
|
|
13015
13064
|
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
13016
13065
|
return { success: true, locked_by: agentId, locked_at: timestamp3, expires_at: lockExpiresAt(timestamp3) };
|
|
13017
13066
|
}
|
|
13018
13067
|
const cutoff = lockExpiryCutoff();
|
|
13019
13068
|
const timestamp2 = now();
|
|
13020
13069
|
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
13021
|
-
WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp2, timestamp2, id, agentId, cutoff]);
|
|
13070
|
+
WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR LOWER(TRIM(locked_by)) = LOWER(TRIM(?)) OR locked_at < ?)`, [agentId, timestamp2, timestamp2, id, agentId, cutoff]);
|
|
13022
13071
|
if (result.changes === 0) {
|
|
13023
13072
|
const current = getTask(id, d);
|
|
13024
13073
|
if (!current)
|
|
@@ -13050,7 +13099,7 @@ function unlockTask(id, agentId, db) {
|
|
|
13050
13099
|
const task = getTask(id, d);
|
|
13051
13100
|
if (!task)
|
|
13052
13101
|
throw new TaskNotFoundError(id);
|
|
13053
|
-
if (agentId && task.locked_by && task.locked_by
|
|
13102
|
+
if (agentId && task.locked_by && !sameHolder(task.locked_by, agentId)) {
|
|
13054
13103
|
throw new LockError(id, task.locked_by);
|
|
13055
13104
|
}
|
|
13056
13105
|
const timestamp2 = now();
|
|
@@ -13343,6 +13392,7 @@ var MAX_SPAWN_DEPTH = 10;
|
|
|
13343
13392
|
var init_task_lifecycle = __esm(() => {
|
|
13344
13393
|
init_types();
|
|
13345
13394
|
init_database();
|
|
13395
|
+
init_creator_identity();
|
|
13346
13396
|
init_completion_guard();
|
|
13347
13397
|
init_event_emission_safety();
|
|
13348
13398
|
init_event_hooks();
|
|
@@ -15254,8 +15304,8 @@ var init_boards = __esm(() => {
|
|
|
15254
15304
|
|
|
15255
15305
|
// src/lib/artifact-store.ts
|
|
15256
15306
|
import { createHash as createHash2 } from "crypto";
|
|
15257
|
-
import { existsSync as
|
|
15258
|
-
import { basename, dirname as dirname4, join as
|
|
15307
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
15308
|
+
import { basename, dirname as dirname4, join as join6, resolve as resolve7 } from "path";
|
|
15259
15309
|
import { tmpdir as tmpdir2 } from "os";
|
|
15260
15310
|
function isInMemoryDb2(path) {
|
|
15261
15311
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
@@ -15267,15 +15317,15 @@ function artifactStoreRoot() {
|
|
|
15267
15317
|
return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
15268
15318
|
const dbPath = getDatabasePath();
|
|
15269
15319
|
if (isInMemoryDb2(dbPath))
|
|
15270
|
-
return
|
|
15271
|
-
return
|
|
15320
|
+
return join6(tmpdir2(), "hasna-todos-artifacts");
|
|
15321
|
+
return join6(dirname4(resolve7(dbPath)), "artifacts");
|
|
15272
15322
|
}
|
|
15273
15323
|
function artifactStorePath(relativePath) {
|
|
15274
15324
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
15275
15325
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
15276
15326
|
throw new Error("Invalid artifact store path");
|
|
15277
15327
|
}
|
|
15278
|
-
return
|
|
15328
|
+
return join6(artifactStoreRoot(), normalized);
|
|
15279
15329
|
}
|
|
15280
15330
|
function sha256(buffer) {
|
|
15281
15331
|
return createHash2("sha256").update(buffer).digest("hex");
|
|
@@ -15316,7 +15366,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
15316
15366
|
}
|
|
15317
15367
|
function storeArtifactContent(input) {
|
|
15318
15368
|
const sourcePath = resolve7(input.path);
|
|
15319
|
-
if (!
|
|
15369
|
+
if (!existsSync7(sourcePath))
|
|
15320
15370
|
return null;
|
|
15321
15371
|
const sourceStat = statSync2(sourcePath);
|
|
15322
15372
|
if (!sourceStat.isFile())
|
|
@@ -15333,9 +15383,9 @@ function storeArtifactContent(input) {
|
|
|
15333
15383
|
redactionStatus = "redacted";
|
|
15334
15384
|
}
|
|
15335
15385
|
const storedSha = sha256(storedBuffer);
|
|
15336
|
-
const relativePath =
|
|
15386
|
+
const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
15337
15387
|
const destination = artifactStorePath(relativePath);
|
|
15338
|
-
if (!
|
|
15388
|
+
if (!existsSync7(destination)) {
|
|
15339
15389
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
15340
15390
|
writeFileSync2(destination, storedBuffer);
|
|
15341
15391
|
}
|
|
@@ -15395,7 +15445,7 @@ function verifyStoredArtifact(input) {
|
|
|
15395
15445
|
};
|
|
15396
15446
|
}
|
|
15397
15447
|
const storedPath = artifactStorePath(store.relative_path);
|
|
15398
|
-
if (!
|
|
15448
|
+
if (!existsSync7(storedPath)) {
|
|
15399
15449
|
return {
|
|
15400
15450
|
id: input.id,
|
|
15401
15451
|
path: input.path,
|
|
@@ -17869,48 +17919,98 @@ var init_token_utils = __esm(() => {
|
|
|
17869
17919
|
};
|
|
17870
17920
|
});
|
|
17871
17921
|
|
|
17872
|
-
// src/lib/
|
|
17873
|
-
import {
|
|
17874
|
-
import {
|
|
17875
|
-
|
|
17876
|
-
|
|
17922
|
+
// src/lib/assignee-validation.ts
|
|
17923
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
17924
|
+
import { homedir as homedir2 } from "os";
|
|
17925
|
+
import { join as join7 } from "path";
|
|
17926
|
+
function defaultSeatRosterPath() {
|
|
17927
|
+
return process.env["TODOS_SEAT_ROSTER_PATH"] || join7(homedir2(), ".hasna", "identities", "hasna-seats.roster.json");
|
|
17877
17928
|
}
|
|
17878
|
-
function
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
}
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
return source === "explicit" || source === "env";
|
|
17929
|
+
function loadSeatSlugs(path = defaultSeatRosterPath()) {
|
|
17930
|
+
try {
|
|
17931
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
17932
|
+
const slugs = new Set;
|
|
17933
|
+
for (const agent of parsed.agents ?? []) {
|
|
17934
|
+
if (typeof agent?.slug === "string" && agent.slug.trim()) {
|
|
17935
|
+
slugs.add(normalizeAgentNameInput(agent.slug));
|
|
17936
|
+
}
|
|
17937
|
+
}
|
|
17938
|
+
return slugs;
|
|
17939
|
+
} catch {
|
|
17940
|
+
return new Set;
|
|
17941
|
+
}
|
|
17892
17942
|
}
|
|
17893
|
-
function
|
|
17894
|
-
const
|
|
17895
|
-
|
|
17896
|
-
|
|
17897
|
-
|
|
17943
|
+
function validateAssignee(input, ctx) {
|
|
17944
|
+
const raw = input.trim();
|
|
17945
|
+
const normalized = normalizeAgentNameInput(raw);
|
|
17946
|
+
if (!normalized) {
|
|
17947
|
+
return {
|
|
17948
|
+
ok: true,
|
|
17949
|
+
assignee: raw,
|
|
17950
|
+
isSeat: false,
|
|
17951
|
+
warning: "Assignee is empty. Use --unassigned to file this deliberately with no owner."
|
|
17952
|
+
};
|
|
17953
|
+
}
|
|
17954
|
+
const byId = ctx.agents.find((a) => a.id.toLowerCase() === normalized);
|
|
17955
|
+
const byName = ctx.agents.filter((a) => normalizeAgentNameInput(a.name) === normalized);
|
|
17956
|
+
const effectiveName = byId ? normalizeAgentNameInput(byId.name) : normalized;
|
|
17957
|
+
if (ctx.seats.has(effectiveName)) {
|
|
17958
|
+
if (ctx.allowSeat) {
|
|
17959
|
+
return { ok: true, assignee: byId ? byId.name : raw, agentId: byId?.id, isSeat: true };
|
|
17960
|
+
}
|
|
17961
|
+
return {
|
|
17962
|
+
ok: false,
|
|
17963
|
+
reason: "seat",
|
|
17964
|
+
message: `'${raw}' is a durable SEAT, and a task assigned to a seat is assigned to nobody \u2014 no session is watching that queue. ` + `Assign a specific agent, use --unassigned to file it with no owner on purpose, or pass --assign-seat if filing at the seat is what you mean.`
|
|
17965
|
+
};
|
|
17966
|
+
}
|
|
17967
|
+
if (byId) {
|
|
17968
|
+
return { ok: true, assignee: byId.name, agentId: byId.id, isSeat: false };
|
|
17969
|
+
}
|
|
17970
|
+
if (byName.length === 0) {
|
|
17971
|
+
return {
|
|
17972
|
+
ok: true,
|
|
17973
|
+
assignee: raw,
|
|
17974
|
+
isSeat: false,
|
|
17975
|
+
warning: `No agent named '${raw}' is registered yet. If that is a typo the task is now routed to nobody \u2014 ` + `check with 'todos agents'.`
|
|
17976
|
+
};
|
|
17977
|
+
}
|
|
17978
|
+
if (byName.length > 1) {
|
|
17979
|
+
const ids = byName.map((a) => a.id).sort();
|
|
17980
|
+
return {
|
|
17981
|
+
ok: false,
|
|
17982
|
+
reason: "ambiguous",
|
|
17983
|
+
message: `'${raw}' names ${byName.length} registered agents (${ids.join(", ")}), so assigning by name would pick one at random \u2014 ` + `which is how a task lands on a stranger. Pass the agent ID instead.`,
|
|
17984
|
+
candidates: byName
|
|
17985
|
+
};
|
|
17986
|
+
}
|
|
17987
|
+
return { ok: true, assignee: byName[0].name, agentId: byName[0].id, isSeat: false };
|
|
17898
17988
|
}
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17904
|
-
|
|
17905
|
-
|
|
17906
|
-
|
|
17907
|
-
|
|
17908
|
-
|
|
17989
|
+
var init_assignee_validation = () => {};
|
|
17990
|
+
|
|
17991
|
+
// src/lib/assignee-context.ts
|
|
17992
|
+
async function loadAssigneeContext(listAgentsFn, allowSeat, nowMs = Date.now()) {
|
|
17993
|
+
if (!cache || nowMs - cache.at >= ROSTER_TTL_MS) {
|
|
17994
|
+
let agents;
|
|
17995
|
+
let degraded = false;
|
|
17996
|
+
try {
|
|
17997
|
+
agents = await listAgentsFn();
|
|
17998
|
+
} catch {
|
|
17999
|
+
agents = [];
|
|
18000
|
+
degraded = true;
|
|
18001
|
+
}
|
|
18002
|
+
cache = {
|
|
18003
|
+
at: nowMs,
|
|
18004
|
+
agents: agents.map((a) => ({ id: a.id, name: a.name })),
|
|
18005
|
+
seats: loadSeatSlugs(),
|
|
18006
|
+
degraded
|
|
18007
|
+
};
|
|
17909
18008
|
}
|
|
17910
|
-
return {
|
|
18009
|
+
return { agents: cache.agents, seats: cache.seats, allowSeat, degraded: cache.degraded };
|
|
17911
18010
|
}
|
|
17912
|
-
var
|
|
17913
|
-
|
|
18011
|
+
var ROSTER_TTL_MS = 15000, cache;
|
|
18012
|
+
var init_assignee_context = __esm(() => {
|
|
18013
|
+
init_assignee_validation();
|
|
17914
18014
|
});
|
|
17915
18015
|
|
|
17916
18016
|
// src/pr-groups/types.ts
|
|
@@ -20629,6 +20729,14 @@ var init_cloud_router = __esm(() => {
|
|
|
20629
20729
|
});
|
|
20630
20730
|
|
|
20631
20731
|
// src/mcp/tools/task-crud.ts
|
|
20732
|
+
async function validateMcpAssignee(value, allowSeat) {
|
|
20733
|
+
const cloud = getTodosCloudClient();
|
|
20734
|
+
const ctx = await loadAssigneeContext(() => cloud ? cloudListAgents(cloud) : listAgents(), allowSeat);
|
|
20735
|
+
const verdict = validateAssignee(value, ctx);
|
|
20736
|
+
if (!verdict.ok)
|
|
20737
|
+
throw new Error(`Cannot assign to '${value}'. ${verdict.message}`);
|
|
20738
|
+
return verdict.assignee;
|
|
20739
|
+
}
|
|
20632
20740
|
function registerTaskCrudTools(server, ctx) {
|
|
20633
20741
|
const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
|
|
20634
20742
|
function mutationTaskResponse(task) {
|
|
@@ -20665,6 +20773,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
20665
20773
|
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
20666
20774
|
created_by: exports_external.string().optional().describe("Agent who FILED this task. Defaults to the ambient agent identity (todos init / TODOS_AGENT_ID)."),
|
|
20667
20775
|
unassigned: exports_external.boolean().optional().describe("Deliberately file with no assignee. Without it, the task defaults to the filer."),
|
|
20776
|
+
allow_seat: exports_external.boolean().optional().describe("Allow assigned_to to name a durable seat. A seat queue has no session watching it, so this must be deliberate."),
|
|
20668
20777
|
depends_on: exports_external.array(exports_external.string()).optional().describe("Array of task IDs this task depends on"),
|
|
20669
20778
|
short_id: exports_external.string().nullable().optional().describe("Short ID (auto-generated if not provided, disabled if null)"),
|
|
20670
20779
|
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
@@ -20675,10 +20784,11 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
20675
20784
|
retry_count: exports_external.number().optional().describe("Max retry count for agent failures")
|
|
20676
20785
|
}, async (params) => {
|
|
20677
20786
|
try {
|
|
20678
|
-
const { depends_on, assigned_to, project_id, task_list_id, tags, estimate, confidence, retry_count, deadline, created_by, unassigned, ...rest } = params;
|
|
20787
|
+
const { depends_on, assigned_to, project_id, task_list_id, tags, estimate, confidence, retry_count, deadline, created_by, unassigned, allow_seat, ...rest } = params;
|
|
20788
|
+
const requestedAssignee = assigned_to ? await validateMcpAssignee(assigned_to, Boolean(allow_seat)) : undefined;
|
|
20679
20789
|
const creator = resolveCreatorIdentity(created_by);
|
|
20680
20790
|
const router = resolveWritableIdentity(created_by);
|
|
20681
|
-
const assignee =
|
|
20791
|
+
const assignee = requestedAssignee || (unassigned ? undefined : router.agent_id || undefined);
|
|
20682
20792
|
const cloud = getTodosCloudClient();
|
|
20683
20793
|
if (cloud) {
|
|
20684
20794
|
const payload = { ...rest };
|
|
@@ -20747,6 +20857,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
20747
20857
|
project_id: exports_external.string().optional().describe("Project ID"),
|
|
20748
20858
|
task_list_id: exports_external.string().optional().describe("Task list ID"),
|
|
20749
20859
|
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
20860
|
+
allow_seat: exports_external.boolean().optional().describe("Allow assigned_to to name a durable seat. A seat queue has no session watching it, so this must be deliberate."),
|
|
20750
20861
|
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
20751
20862
|
working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
|
|
20752
20863
|
metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
|
|
@@ -20760,7 +20871,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
20760
20871
|
acceptance: exports_external.unknown().optional()
|
|
20761
20872
|
}, async (params) => {
|
|
20762
20873
|
try {
|
|
20763
|
-
const { assigned_to, project_id, task_list_id, metadata, expectation_id, expectation_fingerprint, evidence_paths, origin_loop_id, origin_run_id, expected, observed, acceptance, ...rest } = params;
|
|
20874
|
+
const { assigned_to, allow_seat: _allowSeatUpsert, project_id, task_list_id, metadata, expectation_id, expectation_fingerprint, evidence_paths, origin_loop_id, origin_run_id, expected, observed, acceptance, ...rest } = params;
|
|
20764
20875
|
const mergedMetadata = { ...metadata ?? {} };
|
|
20765
20876
|
if (expectation_id !== undefined)
|
|
20766
20877
|
mergedMetadata["expectation_id"] = expectation_id;
|
|
@@ -20779,8 +20890,9 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
20779
20890
|
if (acceptance !== undefined)
|
|
20780
20891
|
mergedMetadata["acceptance"] = acceptance;
|
|
20781
20892
|
const resolved = { ...rest, metadata: mergedMetadata };
|
|
20782
|
-
if (assigned_to)
|
|
20783
|
-
resolved.assigned_to = resolveAssignee(assigned_to);
|
|
20893
|
+
if (assigned_to) {
|
|
20894
|
+
resolved.assigned_to = resolveAssignee(await validateMcpAssignee(assigned_to, Boolean(params.allow_seat)));
|
|
20895
|
+
}
|
|
20784
20896
|
if (project_id)
|
|
20785
20897
|
resolved.project_id = resolveId(project_id, "projects");
|
|
20786
20898
|
if (task_list_id)
|
|
@@ -20911,12 +21023,16 @@ ${task.description}` : null
|
|
|
20911
21023
|
completed_at: exports_external.string().optional().describe("ISO timestamp for backdating completion"),
|
|
20912
21024
|
deadline: exports_external.string().nullable().optional(),
|
|
20913
21025
|
retry_count: exports_external.number().optional(),
|
|
21026
|
+
allow_seat: exports_external.boolean().optional().describe("Allow assigned_to to name a durable seat. A seat queue has no session watching it, so this must be deliberate."),
|
|
20914
21027
|
version: exports_external.number().optional().describe("Expected version for optimistic locking")
|
|
20915
21028
|
}, async (params) => {
|
|
20916
21029
|
try {
|
|
21030
|
+
if (typeof params.assigned_to === "string" && params.assigned_to !== "") {
|
|
21031
|
+
params.assigned_to = await validateMcpAssignee(params.assigned_to, Boolean(params.allow_seat));
|
|
21032
|
+
}
|
|
20917
21033
|
const cloud = getTodosCloudClient();
|
|
20918
21034
|
if (cloud) {
|
|
20919
|
-
const { task_id: task_id2, version: version2, estimate, deadline, ...updates2 } = params;
|
|
21035
|
+
const { task_id: task_id2, version: version2, estimate, deadline, allow_seat: _allowSeatCloud, ...updates2 } = params;
|
|
20920
21036
|
const patch = { ...updates2 };
|
|
20921
21037
|
if (patch.assigned_to === "")
|
|
20922
21038
|
patch.assigned_to = null;
|
|
@@ -20941,7 +21057,7 @@ ${task.description}` : null
|
|
|
20941
21057
|
return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
|
|
20942
21058
|
}
|
|
20943
21059
|
const resolvedId = resolveId(params.task_id);
|
|
20944
|
-
const { task_id, version, ...updates } = params;
|
|
21060
|
+
const { task_id, version, allow_seat: _allowSeatLocal, ...updates } = params;
|
|
20945
21061
|
const resolved = { ...updates };
|
|
20946
21062
|
if (resolved.assigned_to === "")
|
|
20947
21063
|
resolved.assigned_to = null;
|
|
@@ -20994,11 +21110,14 @@ var init_task_crud2 = __esm(() => {
|
|
|
20994
21110
|
init_types();
|
|
20995
21111
|
init_token_utils();
|
|
20996
21112
|
init_creator_identity();
|
|
21113
|
+
init_assignee_validation();
|
|
21114
|
+
init_assignee_context();
|
|
21115
|
+
init_agents();
|
|
20997
21116
|
init_cloud_router();
|
|
20998
21117
|
});
|
|
20999
21118
|
|
|
21000
21119
|
// src/lib/project-bootstrap.ts
|
|
21001
|
-
import { existsSync as existsSync8, readFileSync as
|
|
21120
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, statSync as statSync3 } from "fs";
|
|
21002
21121
|
import { basename as basename2, dirname as dirname5, resolve as resolve8 } from "path";
|
|
21003
21122
|
function safeStat(path) {
|
|
21004
21123
|
try {
|
|
@@ -21032,7 +21151,7 @@ function readPackageJson(path) {
|
|
|
21032
21151
|
if (!existsSync8(file))
|
|
21033
21152
|
return null;
|
|
21034
21153
|
try {
|
|
21035
|
-
const parsed = JSON.parse(
|
|
21154
|
+
const parsed = JSON.parse(readFileSync4(file, "utf-8"));
|
|
21036
21155
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
21037
21156
|
} catch {
|
|
21038
21157
|
return null;
|
|
@@ -21594,8 +21713,8 @@ var init_retention_cleanup = __esm(() => {
|
|
|
21594
21713
|
});
|
|
21595
21714
|
|
|
21596
21715
|
// src/lib/mention-resolver.ts
|
|
21597
|
-
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as
|
|
21598
|
-
import { basename as basename3, isAbsolute, join as
|
|
21716
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
|
|
21717
|
+
import { basename as basename3, isAbsolute, join as join8, relative as relative3, resolve as resolve9, sep as sep2 } from "path";
|
|
21599
21718
|
function blankResolution(parsed) {
|
|
21600
21719
|
return {
|
|
21601
21720
|
input: parsed.input,
|
|
@@ -21703,7 +21822,7 @@ function resolveFile(parsed, workspace) {
|
|
|
21703
21822
|
return resolution;
|
|
21704
21823
|
}
|
|
21705
21824
|
if (parsed.line !== undefined) {
|
|
21706
|
-
const lineCount =
|
|
21825
|
+
const lineCount = readFileSync5(absolutePath, "utf-8").split(/\r?\n/).length;
|
|
21707
21826
|
if (parsed.line < 1 || parsed.line > lineCount) {
|
|
21708
21827
|
resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
|
|
21709
21828
|
return resolution;
|
|
@@ -21726,7 +21845,7 @@ function walkSourceFiles(root, current = root, files = []) {
|
|
|
21726
21845
|
if (SKIP_DIRS.has(entry.name))
|
|
21727
21846
|
continue;
|
|
21728
21847
|
}
|
|
21729
|
-
const absolutePath =
|
|
21848
|
+
const absolutePath = join8(current, entry.name);
|
|
21730
21849
|
if (entry.isDirectory()) {
|
|
21731
21850
|
if (!SKIP_DIRS.has(entry.name))
|
|
21732
21851
|
walkSourceFiles(root, absolutePath, files);
|
|
@@ -21756,7 +21875,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
|
|
|
21756
21875
|
const pattern = symbolPattern(name);
|
|
21757
21876
|
const matches = [];
|
|
21758
21877
|
for (const file of walkSourceFiles(workspace)) {
|
|
21759
|
-
const lines =
|
|
21878
|
+
const lines = readFileSync5(file, "utf-8").split(/\r?\n/);
|
|
21760
21879
|
for (let index = 0;index < lines.length; index += 1) {
|
|
21761
21880
|
const line = lines[index];
|
|
21762
21881
|
const found = pattern.exec(line);
|
|
@@ -25417,8 +25536,8 @@ var init_audit_ledger = __esm(() => {
|
|
|
25417
25536
|
});
|
|
25418
25537
|
|
|
25419
25538
|
// src/lib/release-compatibility.ts
|
|
25420
|
-
import { readFileSync as
|
|
25421
|
-
import { join as
|
|
25539
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
25540
|
+
import { join as join9, resolve as resolve11 } from "path";
|
|
25422
25541
|
import { Database as Database2 } from "bun:sqlite";
|
|
25423
25542
|
function pass(id, message, details) {
|
|
25424
25543
|
return { id, status: "passed", message, details };
|
|
@@ -25430,7 +25549,7 @@ function warn(id, message, details) {
|
|
|
25430
25549
|
return { id, status: "warning", message, details };
|
|
25431
25550
|
}
|
|
25432
25551
|
function readPackageJson2(root) {
|
|
25433
|
-
return JSON.parse(
|
|
25552
|
+
return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
|
|
25434
25553
|
}
|
|
25435
25554
|
function sortedKeys(value) {
|
|
25436
25555
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -29444,7 +29563,7 @@ __export(exports_doctor, {
|
|
|
29444
29563
|
runTodosDoctor: () => runTodosDoctor
|
|
29445
29564
|
});
|
|
29446
29565
|
import { chmodSync, copyFileSync, existsSync as existsSync11, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
|
|
29447
|
-
import { basename as basename4, dirname as dirname6, join as
|
|
29566
|
+
import { basename as basename4, dirname as dirname6, join as join10 } from "path";
|
|
29448
29567
|
function tableExists2(db, table) {
|
|
29449
29568
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
29450
29569
|
}
|
|
@@ -29601,13 +29720,13 @@ function createBackup(dbPath) {
|
|
|
29601
29720
|
if (!existsSync11(dbPath))
|
|
29602
29721
|
return;
|
|
29603
29722
|
const stamp = now().replace(/[:.]/g, "-");
|
|
29604
|
-
const backupDir =
|
|
29723
|
+
const backupDir = join10(dirname6(dbPath), `${basename4(dbPath)}.backup-${stamp}`);
|
|
29605
29724
|
const files = [];
|
|
29606
29725
|
mkdirSync5(backupDir, { recursive: true });
|
|
29607
29726
|
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
29608
29727
|
if (!existsSync11(source))
|
|
29609
29728
|
continue;
|
|
29610
|
-
const target =
|
|
29729
|
+
const target = join10(backupDir, basename4(source));
|
|
29611
29730
|
copyFileSync(source, target);
|
|
29612
29731
|
files.push(target);
|
|
29613
29732
|
}
|
|
@@ -32343,7 +32462,7 @@ var init_agent_run_dispatcher = __esm(() => {
|
|
|
32343
32462
|
});
|
|
32344
32463
|
|
|
32345
32464
|
// src/lib/verification-providers.ts
|
|
32346
|
-
import { existsSync as existsSync12, readFileSync as
|
|
32465
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
32347
32466
|
function normalizeName5(name) {
|
|
32348
32467
|
const normalized = name.trim().toLowerCase();
|
|
32349
32468
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
|
|
@@ -32495,7 +32614,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
32495
32614
|
};
|
|
32496
32615
|
}
|
|
32497
32616
|
function runCiLogProvider(input) {
|
|
32498
|
-
const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ?
|
|
32617
|
+
const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ? readFileSync7(input.log_path, "utf-8") : "");
|
|
32499
32618
|
return {
|
|
32500
32619
|
status: classifyLog(text),
|
|
32501
32620
|
attempts: 1,
|
|
@@ -34656,7 +34775,7 @@ var package_default;
|
|
|
34656
34775
|
var init_package = __esm(() => {
|
|
34657
34776
|
package_default = {
|
|
34658
34777
|
name: "@hasna/todos",
|
|
34659
|
-
version: "0.13.
|
|
34778
|
+
version: "0.13.9",
|
|
34660
34779
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
34661
34780
|
type: "module",
|
|
34662
34781
|
main: "dist/index.js",
|
|
@@ -35363,7 +35482,7 @@ var init_local_bridge = __esm(() => {
|
|
|
35363
35482
|
|
|
35364
35483
|
// src/lib/local-backups.ts
|
|
35365
35484
|
import { createHash as createHash6 } from "crypto";
|
|
35366
|
-
import { readFileSync as
|
|
35485
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
|
|
35367
35486
|
import { dirname as dirname7, resolve as resolve12 } from "path";
|
|
35368
35487
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
35369
35488
|
function stableJson2(value) {
|
|
@@ -35473,7 +35592,7 @@ function writeLocalBackupFile(backup, outputPath) {
|
|
|
35473
35592
|
return path;
|
|
35474
35593
|
}
|
|
35475
35594
|
function readLocalBackupFile(path) {
|
|
35476
|
-
return JSON.parse(
|
|
35595
|
+
return JSON.parse(readFileSync8(resolve12(path), "utf-8"));
|
|
35477
35596
|
}
|
|
35478
35597
|
function verifyLocalBackup(value, options = {}, db) {
|
|
35479
35598
|
const verifiedAt = options.verified_at ?? now();
|
|
@@ -36924,8 +37043,8 @@ __export(exports_local_extensions, {
|
|
|
36924
37043
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
36925
37044
|
});
|
|
36926
37045
|
import { createHash as createHash9, createVerify } from "crypto";
|
|
36927
|
-
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as
|
|
36928
|
-
import { basename as basename5, join as
|
|
37046
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
|
|
37047
|
+
import { basename as basename5, join as join11, resolve as resolve13 } from "path";
|
|
36929
37048
|
function isObject2(value) {
|
|
36930
37049
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
36931
37050
|
}
|
|
@@ -37006,7 +37125,7 @@ function normalizeManifest(input) {
|
|
|
37006
37125
|
};
|
|
37007
37126
|
}
|
|
37008
37127
|
function parseJson(path) {
|
|
37009
|
-
return JSON.parse(
|
|
37128
|
+
return JSON.parse(readFileSync9(path, "utf8"));
|
|
37010
37129
|
}
|
|
37011
37130
|
function sha2566(bytes) {
|
|
37012
37131
|
return `sha256:${createHash9("sha256").update(bytes).digest("hex")}`;
|
|
@@ -37187,10 +37306,10 @@ function inspectExtensionSource(source3) {
|
|
|
37187
37306
|
if (!existsSync13(resolved))
|
|
37188
37307
|
throw new Error(`extension source not found: ${source3}`);
|
|
37189
37308
|
const stat = statSync6(resolved);
|
|
37190
|
-
const manifestPath = stat.isDirectory() ? [
|
|
37309
|
+
const manifestPath = stat.isDirectory() ? [join11(resolved, "todos.extension.json"), join11(resolved, "extension.json")].find(existsSync13) : resolved;
|
|
37191
37310
|
if (!manifestPath)
|
|
37192
37311
|
throw new Error(`extension directory ${source3} is missing todos.extension.json`);
|
|
37193
|
-
const raw =
|
|
37312
|
+
const raw = readFileSync9(manifestPath);
|
|
37194
37313
|
const parsed = parseJson(manifestPath);
|
|
37195
37314
|
const bundle = isObject2(parsed) && isObject2(parsed["manifest"]);
|
|
37196
37315
|
const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
|
|
@@ -37283,15 +37402,15 @@ function projectExtensionSources(projectPath) {
|
|
|
37283
37402
|
return [];
|
|
37284
37403
|
const root = resolve13(projectPath);
|
|
37285
37404
|
const candidates = [
|
|
37286
|
-
|
|
37287
|
-
|
|
37405
|
+
join11(root, "todos.extension.json"),
|
|
37406
|
+
join11(root, ".todos", "todos.extension.json")
|
|
37288
37407
|
];
|
|
37289
|
-
const extensionDir =
|
|
37408
|
+
const extensionDir = join11(root, ".todos", "extensions");
|
|
37290
37409
|
if (existsSync13(extensionDir)) {
|
|
37291
37410
|
for (const entry of readdirSync3(extensionDir)) {
|
|
37292
37411
|
if (entry.startsWith("."))
|
|
37293
37412
|
continue;
|
|
37294
|
-
const full =
|
|
37413
|
+
const full = join11(extensionDir, entry);
|
|
37295
37414
|
if (statSync6(full).isDirectory() || entry.endsWith(".json"))
|
|
37296
37415
|
candidates.push(full);
|
|
37297
37416
|
}
|
|
@@ -41703,9 +41822,9 @@ __export(exports_extract, {
|
|
|
41703
41822
|
buildCodebaseIndex: () => buildCodebaseIndex,
|
|
41704
41823
|
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
41705
41824
|
});
|
|
41706
|
-
import { existsSync as existsSync14, readFileSync as
|
|
41825
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
|
|
41707
41826
|
import { createHash as createHash11 } from "crypto";
|
|
41708
|
-
import { relative as relative5, resolve as resolve14, join as
|
|
41827
|
+
import { relative as relative5, resolve as resolve14, join as join12 } from "path";
|
|
41709
41828
|
function stableHash(value) {
|
|
41710
41829
|
return createHash11("sha256").update(value).digest("hex");
|
|
41711
41830
|
}
|
|
@@ -41714,11 +41833,11 @@ function normalizePathForMatch(value) {
|
|
|
41714
41833
|
}
|
|
41715
41834
|
function readGitignorePatterns(basePath) {
|
|
41716
41835
|
const root = statSync7(basePath).isFile() ? resolve14(basePath, "..") : basePath;
|
|
41717
|
-
const gitignorePath =
|
|
41836
|
+
const gitignorePath = join12(root, ".gitignore");
|
|
41718
41837
|
if (!existsSync14(gitignorePath))
|
|
41719
41838
|
return [];
|
|
41720
41839
|
try {
|
|
41721
|
-
return
|
|
41840
|
+
return readFileSync10(gitignorePath, "utf-8").split(`
|
|
41722
41841
|
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!"));
|
|
41723
41842
|
} catch {
|
|
41724
41843
|
return [];
|
|
@@ -41857,9 +41976,9 @@ function buildCodebaseIndex(options) {
|
|
|
41857
41976
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
41858
41977
|
const indexed = [];
|
|
41859
41978
|
for (const file of files) {
|
|
41860
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
41979
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
41861
41980
|
try {
|
|
41862
|
-
const source3 =
|
|
41981
|
+
const source3 = readFileSync10(fullPath, "utf-8");
|
|
41863
41982
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
41864
41983
|
indexed.push({
|
|
41865
41984
|
file: relPath,
|
|
@@ -41888,9 +42007,9 @@ function extractTodos(options, db) {
|
|
|
41888
42007
|
const files = collectFiles(basePath, extensions, excludes, respectGitignore);
|
|
41889
42008
|
const allComments = [];
|
|
41890
42009
|
for (const file of files) {
|
|
41891
|
-
const fullPath = statSync7(basePath).isFile() ? basePath :
|
|
42010
|
+
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
41892
42011
|
try {
|
|
41893
|
-
const source3 =
|
|
42012
|
+
const source3 = readFileSync10(fullPath, "utf-8");
|
|
41894
42013
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
41895
42014
|
const comments = extractFromSource(source3, relPath, tags);
|
|
41896
42015
|
allComments.push(...comments);
|
|
@@ -42839,7 +42958,7 @@ __export(exports_builtin_templates, {
|
|
|
42839
42958
|
BUILTIN_TEMPLATES: () => BUILTIN_TEMPLATES
|
|
42840
42959
|
});
|
|
42841
42960
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
42842
|
-
import { join as
|
|
42961
|
+
import { join as join13 } from "path";
|
|
42843
42962
|
function templateMetadata(template) {
|
|
42844
42963
|
return {
|
|
42845
42964
|
source: BUILTIN_TEMPLATE_LIBRARY_SOURCE,
|
|
@@ -42898,7 +43017,7 @@ function writeBuiltinTemplateFiles(directory) {
|
|
|
42898
43017
|
mkdirSync7(directory, { recursive: true });
|
|
42899
43018
|
const files = [];
|
|
42900
43019
|
for (const entry of exportBuiltinTemplateFiles()) {
|
|
42901
|
-
const path =
|
|
43020
|
+
const path = join13(directory, entry.filename);
|
|
42902
43021
|
writeFileSync4(path, `${JSON.stringify(entry.template, null, 2)}
|
|
42903
43022
|
`, "utf-8");
|
|
42904
43023
|
files.push(path);
|
|
@@ -43424,28 +43543,28 @@ __export(exports_environment_snapshots, {
|
|
|
43424
43543
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
43425
43544
|
});
|
|
43426
43545
|
import { createHash as createHash12 } from "crypto";
|
|
43427
|
-
import { existsSync as existsSync15, readFileSync as
|
|
43546
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
|
|
43428
43547
|
import { hostname as hostname2, platform, arch } from "os";
|
|
43429
|
-
import { dirname as dirname8, join as
|
|
43548
|
+
import { dirname as dirname8, join as join14, resolve as resolve15 } from "path";
|
|
43430
43549
|
import { tmpdir as tmpdir3 } from "os";
|
|
43431
43550
|
function sha2567(value) {
|
|
43432
43551
|
return createHash12("sha256").update(value).digest("hex");
|
|
43433
43552
|
}
|
|
43434
43553
|
function fileRecord(root, relativePath) {
|
|
43435
|
-
const path =
|
|
43554
|
+
const path = join14(root, relativePath);
|
|
43436
43555
|
if (!existsSync15(path))
|
|
43437
43556
|
return null;
|
|
43438
43557
|
const stat = statSync8(path);
|
|
43439
43558
|
if (!stat.isFile())
|
|
43440
43559
|
return null;
|
|
43441
|
-
const content =
|
|
43560
|
+
const content = readFileSync11(path);
|
|
43442
43561
|
return { path: relativePath, sha256: sha2567(content), size_bytes: content.length };
|
|
43443
43562
|
}
|
|
43444
43563
|
function manifestRecord(root, relativePath) {
|
|
43445
43564
|
const base = fileRecord(root, relativePath);
|
|
43446
43565
|
if (!base)
|
|
43447
43566
|
return null;
|
|
43448
|
-
const parsed = readJsonFile(
|
|
43567
|
+
const parsed = readJsonFile(join14(root, relativePath));
|
|
43449
43568
|
if (!parsed)
|
|
43450
43569
|
return { ...base, redacted: {} };
|
|
43451
43570
|
const redacted = redactValue({
|
|
@@ -43540,8 +43659,8 @@ function commandEnv(env, includeValues) {
|
|
|
43540
43659
|
function defaultSnapshotDir() {
|
|
43541
43660
|
const dbPath = getDatabasePath();
|
|
43542
43661
|
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
43543
|
-
return
|
|
43544
|
-
return
|
|
43662
|
+
return join14(tmpdir3(), "hasna-todos", "environment-snapshots");
|
|
43663
|
+
return join14(dirname8(resolve15(dbPath)), "environment-snapshots");
|
|
43545
43664
|
}
|
|
43546
43665
|
function snapshotWithId(snapshot) {
|
|
43547
43666
|
const digest = sha2567(JSON.stringify(snapshot)).slice(0, 24);
|
|
@@ -43588,7 +43707,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
43588
43707
|
});
|
|
43589
43708
|
}
|
|
43590
43709
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
43591
|
-
const path = outputPath ? resolve15(outputPath) :
|
|
43710
|
+
const path = outputPath ? resolve15(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
43592
43711
|
ensureDir2(dirname8(path));
|
|
43593
43712
|
writeJsonFile(path, snapshot);
|
|
43594
43713
|
return path;
|
|
@@ -47894,7 +48013,7 @@ var init_headless_boundaries = __esm(() => {
|
|
|
47894
48013
|
});
|
|
47895
48014
|
|
|
47896
48015
|
// src/server/routes.ts
|
|
47897
|
-
import { join as
|
|
48016
|
+
import { join as join15, resolve as resolve16, sep as sep3 } from "path";
|
|
47898
48017
|
function parseFieldsParam(url) {
|
|
47899
48018
|
const fieldsParam = url.searchParams.get("fields");
|
|
47900
48019
|
return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
@@ -48689,7 +48808,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
48689
48808
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
48690
48809
|
return null;
|
|
48691
48810
|
if (path !== "/") {
|
|
48692
|
-
const filePath =
|
|
48811
|
+
const filePath = join15(ctx.dashboardDir, path);
|
|
48693
48812
|
const resolvedFile = resolve16(filePath);
|
|
48694
48813
|
const resolvedBase = resolve16(ctx.dashboardDir);
|
|
48695
48814
|
if (!resolvedFile.startsWith(resolvedBase + sep3) && resolvedFile !== resolvedBase) {
|
|
@@ -48699,7 +48818,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
48699
48818
|
if (res2)
|
|
48700
48819
|
return res2;
|
|
48701
48820
|
}
|
|
48702
|
-
const indexPath =
|
|
48821
|
+
const indexPath = join15(ctx.dashboardDir, "index.html");
|
|
48703
48822
|
const res = serveStaticFile2(indexPath);
|
|
48704
48823
|
if (res)
|
|
48705
48824
|
return res;
|
|
@@ -50992,10 +51111,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50992
51111
|
const released2 = await store.tasks.unlock(id);
|
|
50993
51112
|
return json3({ success: released2 });
|
|
50994
51113
|
}
|
|
50995
|
-
if (body2.agent_id &&
|
|
51114
|
+
if (body2.agent_id && body2.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
|
|
50996
51115
|
return error(403, "unlock agent_id must match the authenticated agent");
|
|
50997
51116
|
}
|
|
50998
|
-
const agentId2 =
|
|
51117
|
+
const agentId2 = body2.agent_id || principal.agent;
|
|
50999
51118
|
if (!agentId2)
|
|
51000
51119
|
return error(403, "unlock requires an agent-bound key or force=true");
|
|
51001
51120
|
const released = await store.tasks.unlock(id, agentId2);
|
|
@@ -51837,26 +51956,26 @@ __export(exports_serve, {
|
|
|
51837
51956
|
MIME_TYPES: () => MIME_TYPES
|
|
51838
51957
|
});
|
|
51839
51958
|
import { existsSync as existsSync16 } from "fs";
|
|
51840
|
-
import { join as
|
|
51959
|
+
import { join as join16, dirname as dirname9, extname } from "path";
|
|
51841
51960
|
import { fileURLToPath } from "url";
|
|
51842
51961
|
function resolveDashboardDir() {
|
|
51843
51962
|
const candidates = [];
|
|
51844
51963
|
try {
|
|
51845
51964
|
const scriptDir = dirname9(fileURLToPath(import.meta.url));
|
|
51846
|
-
candidates.push(
|
|
51847
|
-
candidates.push(
|
|
51965
|
+
candidates.push(join16(scriptDir, "..", "dashboard", "dist"));
|
|
51966
|
+
candidates.push(join16(scriptDir, "..", "..", "dashboard", "dist"));
|
|
51848
51967
|
} catch {}
|
|
51849
51968
|
if (process.argv[1]) {
|
|
51850
51969
|
const mainDir = dirname9(process.argv[1]);
|
|
51851
|
-
candidates.push(
|
|
51852
|
-
candidates.push(
|
|
51970
|
+
candidates.push(join16(mainDir, "..", "dashboard", "dist"));
|
|
51971
|
+
candidates.push(join16(mainDir, "..", "..", "dashboard", "dist"));
|
|
51853
51972
|
}
|
|
51854
|
-
candidates.push(
|
|
51973
|
+
candidates.push(join16(process.cwd(), "dashboard", "dist"));
|
|
51855
51974
|
for (const candidate of candidates) {
|
|
51856
51975
|
if (existsSync16(candidate))
|
|
51857
51976
|
return candidate;
|
|
51858
51977
|
}
|
|
51859
|
-
return
|
|
51978
|
+
return join16(process.cwd(), "dashboard", "dist");
|
|
51860
51979
|
}
|
|
51861
51980
|
function getProvidedApiKey(req) {
|
|
51862
51981
|
const headerKey = req.headers.get("x-api-key");
|