@hasna/todos 0.10.5 → 0.10.8
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/index.js +141 -1
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/file-locks.d.ts +43 -0
- package/dist/db/file-locks.d.ts.map +1 -0
- package/dist/index.js +15 -1
- package/dist/mcp/index.js +255 -111
- package/dist/server/index.js +15 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2897,6 +2897,20 @@ var init_database = __esm(() => {
|
|
|
2897
2897
|
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
|
|
2898
2898
|
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
2899
2899
|
INSERT OR IGNORE INTO _migrations (id) VALUES (30);
|
|
2900
|
+
`,
|
|
2901
|
+
`
|
|
2902
|
+
CREATE TABLE IF NOT EXISTS file_locks (
|
|
2903
|
+
id TEXT PRIMARY KEY,
|
|
2904
|
+
path TEXT NOT NULL UNIQUE,
|
|
2905
|
+
agent_id TEXT NOT NULL,
|
|
2906
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
2907
|
+
expires_at TEXT NOT NULL,
|
|
2908
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2909
|
+
);
|
|
2910
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
|
|
2911
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
2912
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
2913
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (31);
|
|
2900
2914
|
`
|
|
2901
2915
|
];
|
|
2902
2916
|
});
|
|
@@ -4807,7 +4821,7 @@ function registerAgent(input, db) {
|
|
|
4807
4821
|
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
4808
4822
|
working_dir: existing.working_dir,
|
|
4809
4823
|
suggestions: suggestions.slice(0, 5),
|
|
4810
|
-
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(
|
|
4824
|
+
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
4811
4825
|
};
|
|
4812
4826
|
}
|
|
4813
4827
|
const updates = ["last_seen_at = ?", "status = 'active'"];
|
|
@@ -9878,6 +9892,74 @@ var init_task_files = __esm(() => {
|
|
|
9878
9892
|
init_database();
|
|
9879
9893
|
});
|
|
9880
9894
|
|
|
9895
|
+
// src/db/file-locks.ts
|
|
9896
|
+
var exports_file_locks = {};
|
|
9897
|
+
__export(exports_file_locks, {
|
|
9898
|
+
unlockFile: () => unlockFile,
|
|
9899
|
+
lockFile: () => lockFile,
|
|
9900
|
+
listFileLocks: () => listFileLocks,
|
|
9901
|
+
forceUnlockFile: () => forceUnlockFile,
|
|
9902
|
+
cleanExpiredFileLocks: () => cleanExpiredFileLocks,
|
|
9903
|
+
checkFileLock: () => checkFileLock,
|
|
9904
|
+
FILE_LOCK_DEFAULT_TTL_SECONDS: () => FILE_LOCK_DEFAULT_TTL_SECONDS
|
|
9905
|
+
});
|
|
9906
|
+
function expiresAt(ttlSeconds) {
|
|
9907
|
+
return new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
9908
|
+
}
|
|
9909
|
+
function cleanExpiredFileLocks(db) {
|
|
9910
|
+
const d = db || getDatabase();
|
|
9911
|
+
const result = d.run("DELETE FROM file_locks WHERE expires_at <= ?", [now()]);
|
|
9912
|
+
return result.changes;
|
|
9913
|
+
}
|
|
9914
|
+
function lockFile(input, db) {
|
|
9915
|
+
const d = db || getDatabase();
|
|
9916
|
+
const ttl = input.ttl_seconds ?? FILE_LOCK_DEFAULT_TTL_SECONDS;
|
|
9917
|
+
const expiry = expiresAt(ttl);
|
|
9918
|
+
const timestamp = now();
|
|
9919
|
+
cleanExpiredFileLocks(d);
|
|
9920
|
+
const existing = d.query("SELECT * FROM file_locks WHERE path = ?").get(input.path);
|
|
9921
|
+
if (existing) {
|
|
9922
|
+
if (existing.agent_id === input.agent_id) {
|
|
9923
|
+
d.run("UPDATE file_locks SET expires_at = ?, task_id = COALESCE(?, task_id) WHERE id = ?", [expiry, input.task_id ?? null, existing.id]);
|
|
9924
|
+
return d.query("SELECT * FROM file_locks WHERE id = ?").get(existing.id);
|
|
9925
|
+
}
|
|
9926
|
+
throw new LockError(input.path, existing.agent_id);
|
|
9927
|
+
}
|
|
9928
|
+
const id = uuid();
|
|
9929
|
+
d.run("INSERT INTO file_locks (id, path, agent_id, task_id, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?)", [id, input.path, input.agent_id, input.task_id ?? null, expiry, timestamp]);
|
|
9930
|
+
return d.query("SELECT * FROM file_locks WHERE id = ?").get(id);
|
|
9931
|
+
}
|
|
9932
|
+
function unlockFile(path, agentId, db) {
|
|
9933
|
+
const d = db || getDatabase();
|
|
9934
|
+
cleanExpiredFileLocks(d);
|
|
9935
|
+
const result = d.run("DELETE FROM file_locks WHERE path = ? AND agent_id = ?", [path, agentId]);
|
|
9936
|
+
return result.changes > 0;
|
|
9937
|
+
}
|
|
9938
|
+
function checkFileLock(path, db) {
|
|
9939
|
+
const d = db || getDatabase();
|
|
9940
|
+
cleanExpiredFileLocks(d);
|
|
9941
|
+
return d.query("SELECT * FROM file_locks WHERE path = ?").get(path);
|
|
9942
|
+
}
|
|
9943
|
+
function listFileLocks(agentId, db) {
|
|
9944
|
+
const d = db || getDatabase();
|
|
9945
|
+
cleanExpiredFileLocks(d);
|
|
9946
|
+
if (agentId) {
|
|
9947
|
+
return d.query("SELECT * FROM file_locks WHERE agent_id = ? ORDER BY created_at DESC").all(agentId);
|
|
9948
|
+
}
|
|
9949
|
+
return d.query("SELECT * FROM file_locks ORDER BY created_at DESC").all();
|
|
9950
|
+
}
|
|
9951
|
+
function forceUnlockFile(path, db) {
|
|
9952
|
+
const d = db || getDatabase();
|
|
9953
|
+
const result = d.run("DELETE FROM file_locks WHERE path = ?", [path]);
|
|
9954
|
+
return result.changes > 0;
|
|
9955
|
+
}
|
|
9956
|
+
var FILE_LOCK_DEFAULT_TTL_SECONDS;
|
|
9957
|
+
var init_file_locks = __esm(() => {
|
|
9958
|
+
init_database();
|
|
9959
|
+
init_types();
|
|
9960
|
+
FILE_LOCK_DEFAULT_TTL_SECONDS = 30 * 60;
|
|
9961
|
+
});
|
|
9962
|
+
|
|
9881
9963
|
// src/db/handoffs.ts
|
|
9882
9964
|
var exports_handoffs = {};
|
|
9883
9965
|
__export(exports_handoffs, {
|
|
@@ -13279,6 +13361,64 @@ ${lines.join(`
|
|
|
13279
13361
|
}
|
|
13280
13362
|
});
|
|
13281
13363
|
}
|
|
13364
|
+
if (shouldRegisterTool("lock_file")) {
|
|
13365
|
+
server.tool("lock_file", "Acquire an exclusive lock on a file path. Throws if another agent holds an active lock. Same agent re-locks refreshes the TTL.", {
|
|
13366
|
+
path: exports_external.string().describe("File path to lock"),
|
|
13367
|
+
agent_id: exports_external.string().describe("Agent acquiring the lock"),
|
|
13368
|
+
task_id: exports_external.string().optional().describe("Task this lock is associated with"),
|
|
13369
|
+
ttl_seconds: exports_external.number().optional().describe("Lock TTL in seconds (default: 1800 = 30 min)")
|
|
13370
|
+
}, async ({ path, agent_id, task_id, ttl_seconds }) => {
|
|
13371
|
+
try {
|
|
13372
|
+
const { lockFile: lockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
13373
|
+
const lock = lockFile2({ path, agent_id, task_id, ttl_seconds });
|
|
13374
|
+
return { content: [{ type: "text", text: JSON.stringify(lock, null, 2) }] };
|
|
13375
|
+
} catch (e) {
|
|
13376
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13377
|
+
}
|
|
13378
|
+
});
|
|
13379
|
+
}
|
|
13380
|
+
if (shouldRegisterTool("unlock_file")) {
|
|
13381
|
+
server.tool("unlock_file", "Release a file lock. Only the lock holder can release it. Returns true if released.", {
|
|
13382
|
+
path: exports_external.string().describe("File path to unlock"),
|
|
13383
|
+
agent_id: exports_external.string().describe("Agent releasing the lock (must be the lock holder)")
|
|
13384
|
+
}, async ({ path, agent_id }) => {
|
|
13385
|
+
try {
|
|
13386
|
+
const { unlockFile: unlockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
13387
|
+
const released = unlockFile2(path, agent_id);
|
|
13388
|
+
return { content: [{ type: "text", text: JSON.stringify({ released, path }) }] };
|
|
13389
|
+
} catch (e) {
|
|
13390
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13391
|
+
}
|
|
13392
|
+
});
|
|
13393
|
+
}
|
|
13394
|
+
if (shouldRegisterTool("check_file_lock")) {
|
|
13395
|
+
server.tool("check_file_lock", "Check who holds a lock on a file path. Returns null if unlocked or expired.", {
|
|
13396
|
+
path: exports_external.string().describe("File path to check")
|
|
13397
|
+
}, async ({ path }) => {
|
|
13398
|
+
try {
|
|
13399
|
+
const { checkFileLock: checkFileLock2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
13400
|
+
const lock = checkFileLock2(path);
|
|
13401
|
+
if (!lock)
|
|
13402
|
+
return { content: [{ type: "text", text: JSON.stringify({ path, locked: false }) }] };
|
|
13403
|
+
return { content: [{ type: "text", text: JSON.stringify({ path, locked: true, ...lock }) }] };
|
|
13404
|
+
} catch (e) {
|
|
13405
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13406
|
+
}
|
|
13407
|
+
});
|
|
13408
|
+
}
|
|
13409
|
+
if (shouldRegisterTool("list_file_locks")) {
|
|
13410
|
+
server.tool("list_file_locks", "List all active file locks. Optionally filter by agent_id.", {
|
|
13411
|
+
agent_id: exports_external.string().optional().describe("Filter locks by agent")
|
|
13412
|
+
}, async ({ agent_id }) => {
|
|
13413
|
+
try {
|
|
13414
|
+
const { listFileLocks: listFileLocks2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
13415
|
+
const locks = listFileLocks2(agent_id);
|
|
13416
|
+
return { content: [{ type: "text", text: JSON.stringify(locks, null, 2) }] };
|
|
13417
|
+
} catch (e) {
|
|
13418
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
13419
|
+
}
|
|
13420
|
+
});
|
|
13421
|
+
}
|
|
13282
13422
|
if (shouldRegisterTool("create_handoff")) {
|
|
13283
13423
|
server.tool("create_handoff", "Create a session handoff note for agent coordination.", {
|
|
13284
13424
|
agent_id: exports_external.string().optional().describe("Agent creating the handoff"),
|
package/dist/db/agents.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAyB,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAM9G;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,GAAG,MAAM,EAAE,CAOhF;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,eAAO,MAAM,mBAAmB,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAItC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA6hBtC,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAkBrD;AAyRD,wBAAgB,aAAa,IAAI,IAAI,CAKpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAK9D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAa,GAAG,MAAM,CAG3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0B9F"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
export declare const FILE_LOCK_DEFAULT_TTL_SECONDS: number;
|
|
3
|
+
export interface FileLock {
|
|
4
|
+
id: string;
|
|
5
|
+
path: string;
|
|
6
|
+
agent_id: string;
|
|
7
|
+
task_id: string | null;
|
|
8
|
+
expires_at: string;
|
|
9
|
+
created_at: string;
|
|
10
|
+
}
|
|
11
|
+
export interface LockFileInput {
|
|
12
|
+
path: string;
|
|
13
|
+
agent_id: string;
|
|
14
|
+
task_id?: string;
|
|
15
|
+
/** TTL in seconds (default: 1800 = 30 min) */
|
|
16
|
+
ttl_seconds?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Clean up expired locks. Called automatically on read operations. */
|
|
19
|
+
export declare function cleanExpiredFileLocks(db?: Database): number;
|
|
20
|
+
/**
|
|
21
|
+
* Acquire an exclusive lock on a file path.
|
|
22
|
+
* - If no lock exists (or existing lock is expired), lock is granted.
|
|
23
|
+
* - If same agent already holds the lock, the TTL is refreshed.
|
|
24
|
+
* - If another agent holds an active lock, throws LockError.
|
|
25
|
+
*/
|
|
26
|
+
export declare function lockFile(input: LockFileInput, db?: Database): FileLock;
|
|
27
|
+
/**
|
|
28
|
+
* Release a file lock. Only the lock holder can release it.
|
|
29
|
+
* Returns true if released, false if not found or wrong agent.
|
|
30
|
+
*/
|
|
31
|
+
export declare function unlockFile(path: string, agentId: string, db?: Database): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Check who holds a lock on a file path.
|
|
34
|
+
* Returns null if unlocked or expired.
|
|
35
|
+
*/
|
|
36
|
+
export declare function checkFileLock(path: string, db?: Database): FileLock | null;
|
|
37
|
+
/**
|
|
38
|
+
* List all active (non-expired) file locks, optionally filtered by agent.
|
|
39
|
+
*/
|
|
40
|
+
export declare function listFileLocks(agentId?: string, db?: Database): FileLock[];
|
|
41
|
+
/** Force-release a lock regardless of which agent holds it (admin operation). */
|
|
42
|
+
export declare function forceUnlockFile(path: string, db?: Database): boolean;
|
|
43
|
+
//# sourceMappingURL=file-locks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-locks.d.ts","sourceRoot":"","sources":["../../src/db/file-locks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C,eAAO,MAAM,6BAA6B,QAAU,CAAC;AAErD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD,uEAAuE;AACvE,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI3D;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CA8BtE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAQhF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAI1E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAOzE;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAIpE"}
|
package/dist/index.js
CHANGED
|
@@ -702,6 +702,20 @@ var MIGRATIONS = [
|
|
|
702
702
|
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
|
|
703
703
|
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
704
704
|
INSERT OR IGNORE INTO _migrations (id) VALUES (30);
|
|
705
|
+
`,
|
|
706
|
+
`
|
|
707
|
+
CREATE TABLE IF NOT EXISTS file_locks (
|
|
708
|
+
id TEXT PRIMARY KEY,
|
|
709
|
+
path TEXT NOT NULL UNIQUE,
|
|
710
|
+
agent_id TEXT NOT NULL,
|
|
711
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
712
|
+
expires_at TEXT NOT NULL,
|
|
713
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
714
|
+
);
|
|
715
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
|
|
716
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
717
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
718
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (31);
|
|
705
719
|
`
|
|
706
720
|
];
|
|
707
721
|
var _db = null;
|
|
@@ -2871,7 +2885,7 @@ function registerAgent(input, db) {
|
|
|
2871
2885
|
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
2872
2886
|
working_dir: existing.working_dir,
|
|
2873
2887
|
suggestions: suggestions.slice(0, 5),
|
|
2874
|
-
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(
|
|
2888
|
+
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
2875
2889
|
};
|
|
2876
2890
|
}
|
|
2877
2891
|
const updates = ["last_seen_at = ?", "status = 'active'"];
|
package/dist/mcp/index.js
CHANGED
|
@@ -39,6 +39,111 @@ var __export = (target, all) => {
|
|
|
39
39
|
};
|
|
40
40
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
41
41
|
|
|
42
|
+
// src/types/index.ts
|
|
43
|
+
var VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError;
|
|
44
|
+
var init_types = __esm(() => {
|
|
45
|
+
VersionConflictError = class VersionConflictError extends Error {
|
|
46
|
+
taskId;
|
|
47
|
+
expectedVersion;
|
|
48
|
+
actualVersion;
|
|
49
|
+
static code = "VERSION_CONFLICT";
|
|
50
|
+
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
51
|
+
constructor(taskId, expectedVersion, actualVersion) {
|
|
52
|
+
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
53
|
+
this.taskId = taskId;
|
|
54
|
+
this.expectedVersion = expectedVersion;
|
|
55
|
+
this.actualVersion = actualVersion;
|
|
56
|
+
this.name = "VersionConflictError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
60
|
+
taskId;
|
|
61
|
+
static code = "TASK_NOT_FOUND";
|
|
62
|
+
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
63
|
+
constructor(taskId) {
|
|
64
|
+
super(`Task not found: ${taskId}`);
|
|
65
|
+
this.taskId = taskId;
|
|
66
|
+
this.name = "TaskNotFoundError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
70
|
+
projectId;
|
|
71
|
+
static code = "PROJECT_NOT_FOUND";
|
|
72
|
+
static suggestion = "Use list_projects to see available projects.";
|
|
73
|
+
constructor(projectId) {
|
|
74
|
+
super(`Project not found: ${projectId}`);
|
|
75
|
+
this.projectId = projectId;
|
|
76
|
+
this.name = "ProjectNotFoundError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
80
|
+
planId;
|
|
81
|
+
static code = "PLAN_NOT_FOUND";
|
|
82
|
+
static suggestion = "Use list_plans to see available plans.";
|
|
83
|
+
constructor(planId) {
|
|
84
|
+
super(`Plan not found: ${planId}`);
|
|
85
|
+
this.planId = planId;
|
|
86
|
+
this.name = "PlanNotFoundError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
LockError = class LockError extends Error {
|
|
90
|
+
taskId;
|
|
91
|
+
lockedBy;
|
|
92
|
+
static code = "LOCK_ERROR";
|
|
93
|
+
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
94
|
+
constructor(taskId, lockedBy) {
|
|
95
|
+
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
96
|
+
this.taskId = taskId;
|
|
97
|
+
this.lockedBy = lockedBy;
|
|
98
|
+
this.name = "LockError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
102
|
+
agentId;
|
|
103
|
+
static code = "AGENT_NOT_FOUND";
|
|
104
|
+
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
105
|
+
constructor(agentId) {
|
|
106
|
+
super(`Agent not found: ${agentId}`);
|
|
107
|
+
this.agentId = agentId;
|
|
108
|
+
this.name = "AgentNotFoundError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
112
|
+
taskListId;
|
|
113
|
+
static code = "TASK_LIST_NOT_FOUND";
|
|
114
|
+
static suggestion = "Use list_task_lists to see available lists.";
|
|
115
|
+
constructor(taskListId) {
|
|
116
|
+
super(`Task list not found: ${taskListId}`);
|
|
117
|
+
this.taskListId = taskListId;
|
|
118
|
+
this.name = "TaskListNotFoundError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
DependencyCycleError = class DependencyCycleError extends Error {
|
|
122
|
+
taskId;
|
|
123
|
+
dependsOn;
|
|
124
|
+
static code = "DEPENDENCY_CYCLE";
|
|
125
|
+
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
126
|
+
constructor(taskId, dependsOn) {
|
|
127
|
+
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
128
|
+
this.taskId = taskId;
|
|
129
|
+
this.dependsOn = dependsOn;
|
|
130
|
+
this.name = "DependencyCycleError";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
CompletionGuardError = class CompletionGuardError extends Error {
|
|
134
|
+
reason;
|
|
135
|
+
retryAfterSeconds;
|
|
136
|
+
static code = "COMPLETION_BLOCKED";
|
|
137
|
+
static suggestion = "Wait for the cooldown period, then retry.";
|
|
138
|
+
constructor(reason, retryAfterSeconds) {
|
|
139
|
+
super(reason);
|
|
140
|
+
this.reason = reason;
|
|
141
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
142
|
+
this.name = "CompletionGuardError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
|
|
42
147
|
// src/db/database.ts
|
|
43
148
|
var exports_database = {};
|
|
44
149
|
__export(exports_database, {
|
|
@@ -837,6 +942,20 @@ var init_database = __esm(() => {
|
|
|
837
942
|
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
|
|
838
943
|
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
839
944
|
INSERT OR IGNORE INTO _migrations (id) VALUES (30);
|
|
945
|
+
`,
|
|
946
|
+
`
|
|
947
|
+
CREATE TABLE IF NOT EXISTS file_locks (
|
|
948
|
+
id TEXT PRIMARY KEY,
|
|
949
|
+
path TEXT NOT NULL UNIQUE,
|
|
950
|
+
agent_id TEXT NOT NULL,
|
|
951
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
952
|
+
expires_at TEXT NOT NULL,
|
|
953
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
954
|
+
);
|
|
955
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
|
|
956
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
957
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
958
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (31);
|
|
840
959
|
`
|
|
841
960
|
];
|
|
842
961
|
});
|
|
@@ -1042,7 +1161,7 @@ function registerAgent(input, db) {
|
|
|
1042
1161
|
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
1043
1162
|
working_dir: existing.working_dir,
|
|
1044
1163
|
suggestions: suggestions.slice(0, 5),
|
|
1045
|
-
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(
|
|
1164
|
+
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
1046
1165
|
};
|
|
1047
1166
|
}
|
|
1048
1167
|
const updates = ["last_seen_at = ?", "status = 'active'"];
|
|
@@ -1318,6 +1437,74 @@ var init_task_files = __esm(() => {
|
|
|
1318
1437
|
init_database();
|
|
1319
1438
|
});
|
|
1320
1439
|
|
|
1440
|
+
// src/db/file-locks.ts
|
|
1441
|
+
var exports_file_locks = {};
|
|
1442
|
+
__export(exports_file_locks, {
|
|
1443
|
+
unlockFile: () => unlockFile,
|
|
1444
|
+
lockFile: () => lockFile,
|
|
1445
|
+
listFileLocks: () => listFileLocks,
|
|
1446
|
+
forceUnlockFile: () => forceUnlockFile,
|
|
1447
|
+
cleanExpiredFileLocks: () => cleanExpiredFileLocks,
|
|
1448
|
+
checkFileLock: () => checkFileLock,
|
|
1449
|
+
FILE_LOCK_DEFAULT_TTL_SECONDS: () => FILE_LOCK_DEFAULT_TTL_SECONDS
|
|
1450
|
+
});
|
|
1451
|
+
function expiresAt(ttlSeconds) {
|
|
1452
|
+
return new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
1453
|
+
}
|
|
1454
|
+
function cleanExpiredFileLocks(db) {
|
|
1455
|
+
const d = db || getDatabase();
|
|
1456
|
+
const result = d.run("DELETE FROM file_locks WHERE expires_at <= ?", [now()]);
|
|
1457
|
+
return result.changes;
|
|
1458
|
+
}
|
|
1459
|
+
function lockFile(input, db) {
|
|
1460
|
+
const d = db || getDatabase();
|
|
1461
|
+
const ttl = input.ttl_seconds ?? FILE_LOCK_DEFAULT_TTL_SECONDS;
|
|
1462
|
+
const expiry = expiresAt(ttl);
|
|
1463
|
+
const timestamp = now();
|
|
1464
|
+
cleanExpiredFileLocks(d);
|
|
1465
|
+
const existing = d.query("SELECT * FROM file_locks WHERE path = ?").get(input.path);
|
|
1466
|
+
if (existing) {
|
|
1467
|
+
if (existing.agent_id === input.agent_id) {
|
|
1468
|
+
d.run("UPDATE file_locks SET expires_at = ?, task_id = COALESCE(?, task_id) WHERE id = ?", [expiry, input.task_id ?? null, existing.id]);
|
|
1469
|
+
return d.query("SELECT * FROM file_locks WHERE id = ?").get(existing.id);
|
|
1470
|
+
}
|
|
1471
|
+
throw new LockError(input.path, existing.agent_id);
|
|
1472
|
+
}
|
|
1473
|
+
const id = uuid();
|
|
1474
|
+
d.run("INSERT INTO file_locks (id, path, agent_id, task_id, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?)", [id, input.path, input.agent_id, input.task_id ?? null, expiry, timestamp]);
|
|
1475
|
+
return d.query("SELECT * FROM file_locks WHERE id = ?").get(id);
|
|
1476
|
+
}
|
|
1477
|
+
function unlockFile(path, agentId, db) {
|
|
1478
|
+
const d = db || getDatabase();
|
|
1479
|
+
cleanExpiredFileLocks(d);
|
|
1480
|
+
const result = d.run("DELETE FROM file_locks WHERE path = ? AND agent_id = ?", [path, agentId]);
|
|
1481
|
+
return result.changes > 0;
|
|
1482
|
+
}
|
|
1483
|
+
function checkFileLock(path, db) {
|
|
1484
|
+
const d = db || getDatabase();
|
|
1485
|
+
cleanExpiredFileLocks(d);
|
|
1486
|
+
return d.query("SELECT * FROM file_locks WHERE path = ?").get(path);
|
|
1487
|
+
}
|
|
1488
|
+
function listFileLocks(agentId, db) {
|
|
1489
|
+
const d = db || getDatabase();
|
|
1490
|
+
cleanExpiredFileLocks(d);
|
|
1491
|
+
if (agentId) {
|
|
1492
|
+
return d.query("SELECT * FROM file_locks WHERE agent_id = ? ORDER BY created_at DESC").all(agentId);
|
|
1493
|
+
}
|
|
1494
|
+
return d.query("SELECT * FROM file_locks ORDER BY created_at DESC").all();
|
|
1495
|
+
}
|
|
1496
|
+
function forceUnlockFile(path, db) {
|
|
1497
|
+
const d = db || getDatabase();
|
|
1498
|
+
const result = d.run("DELETE FROM file_locks WHERE path = ?", [path]);
|
|
1499
|
+
return result.changes > 0;
|
|
1500
|
+
}
|
|
1501
|
+
var FILE_LOCK_DEFAULT_TTL_SECONDS;
|
|
1502
|
+
var init_file_locks = __esm(() => {
|
|
1503
|
+
init_database();
|
|
1504
|
+
init_types();
|
|
1505
|
+
FILE_LOCK_DEFAULT_TTL_SECONDS = 30 * 60;
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1321
1508
|
// src/db/handoffs.ts
|
|
1322
1509
|
var exports_handoffs = {};
|
|
1323
1510
|
__export(exports_handoffs, {
|
|
@@ -5944,120 +6131,12 @@ var coerce = {
|
|
|
5944
6131
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
5945
6132
|
};
|
|
5946
6133
|
var NEVER = INVALID;
|
|
5947
|
-
// src/types/index.ts
|
|
5948
|
-
class VersionConflictError extends Error {
|
|
5949
|
-
taskId;
|
|
5950
|
-
expectedVersion;
|
|
5951
|
-
actualVersion;
|
|
5952
|
-
static code = "VERSION_CONFLICT";
|
|
5953
|
-
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
5954
|
-
constructor(taskId, expectedVersion, actualVersion) {
|
|
5955
|
-
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
5956
|
-
this.taskId = taskId;
|
|
5957
|
-
this.expectedVersion = expectedVersion;
|
|
5958
|
-
this.actualVersion = actualVersion;
|
|
5959
|
-
this.name = "VersionConflictError";
|
|
5960
|
-
}
|
|
5961
|
-
}
|
|
5962
|
-
|
|
5963
|
-
class TaskNotFoundError extends Error {
|
|
5964
|
-
taskId;
|
|
5965
|
-
static code = "TASK_NOT_FOUND";
|
|
5966
|
-
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
5967
|
-
constructor(taskId) {
|
|
5968
|
-
super(`Task not found: ${taskId}`);
|
|
5969
|
-
this.taskId = taskId;
|
|
5970
|
-
this.name = "TaskNotFoundError";
|
|
5971
|
-
}
|
|
5972
|
-
}
|
|
5973
|
-
|
|
5974
|
-
class ProjectNotFoundError extends Error {
|
|
5975
|
-
projectId;
|
|
5976
|
-
static code = "PROJECT_NOT_FOUND";
|
|
5977
|
-
static suggestion = "Use list_projects to see available projects.";
|
|
5978
|
-
constructor(projectId) {
|
|
5979
|
-
super(`Project not found: ${projectId}`);
|
|
5980
|
-
this.projectId = projectId;
|
|
5981
|
-
this.name = "ProjectNotFoundError";
|
|
5982
|
-
}
|
|
5983
|
-
}
|
|
5984
|
-
|
|
5985
|
-
class PlanNotFoundError extends Error {
|
|
5986
|
-
planId;
|
|
5987
|
-
static code = "PLAN_NOT_FOUND";
|
|
5988
|
-
static suggestion = "Use list_plans to see available plans.";
|
|
5989
|
-
constructor(planId) {
|
|
5990
|
-
super(`Plan not found: ${planId}`);
|
|
5991
|
-
this.planId = planId;
|
|
5992
|
-
this.name = "PlanNotFoundError";
|
|
5993
|
-
}
|
|
5994
|
-
}
|
|
5995
|
-
|
|
5996
|
-
class LockError extends Error {
|
|
5997
|
-
taskId;
|
|
5998
|
-
lockedBy;
|
|
5999
|
-
static code = "LOCK_ERROR";
|
|
6000
|
-
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
6001
|
-
constructor(taskId, lockedBy) {
|
|
6002
|
-
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
6003
|
-
this.taskId = taskId;
|
|
6004
|
-
this.lockedBy = lockedBy;
|
|
6005
|
-
this.name = "LockError";
|
|
6006
|
-
}
|
|
6007
|
-
}
|
|
6008
|
-
|
|
6009
|
-
class AgentNotFoundError extends Error {
|
|
6010
|
-
agentId;
|
|
6011
|
-
static code = "AGENT_NOT_FOUND";
|
|
6012
|
-
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
6013
|
-
constructor(agentId) {
|
|
6014
|
-
super(`Agent not found: ${agentId}`);
|
|
6015
|
-
this.agentId = agentId;
|
|
6016
|
-
this.name = "AgentNotFoundError";
|
|
6017
|
-
}
|
|
6018
|
-
}
|
|
6019
|
-
|
|
6020
|
-
class TaskListNotFoundError extends Error {
|
|
6021
|
-
taskListId;
|
|
6022
|
-
static code = "TASK_LIST_NOT_FOUND";
|
|
6023
|
-
static suggestion = "Use list_task_lists to see available lists.";
|
|
6024
|
-
constructor(taskListId) {
|
|
6025
|
-
super(`Task list not found: ${taskListId}`);
|
|
6026
|
-
this.taskListId = taskListId;
|
|
6027
|
-
this.name = "TaskListNotFoundError";
|
|
6028
|
-
}
|
|
6029
|
-
}
|
|
6030
|
-
|
|
6031
|
-
class DependencyCycleError extends Error {
|
|
6032
|
-
taskId;
|
|
6033
|
-
dependsOn;
|
|
6034
|
-
static code = "DEPENDENCY_CYCLE";
|
|
6035
|
-
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
6036
|
-
constructor(taskId, dependsOn) {
|
|
6037
|
-
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
6038
|
-
this.taskId = taskId;
|
|
6039
|
-
this.dependsOn = dependsOn;
|
|
6040
|
-
this.name = "DependencyCycleError";
|
|
6041
|
-
}
|
|
6042
|
-
}
|
|
6043
|
-
|
|
6044
|
-
class CompletionGuardError extends Error {
|
|
6045
|
-
reason;
|
|
6046
|
-
retryAfterSeconds;
|
|
6047
|
-
static code = "COMPLETION_BLOCKED";
|
|
6048
|
-
static suggestion = "Wait for the cooldown period, then retry.";
|
|
6049
|
-
constructor(reason, retryAfterSeconds) {
|
|
6050
|
-
super(reason);
|
|
6051
|
-
this.reason = reason;
|
|
6052
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
6053
|
-
this.name = "CompletionGuardError";
|
|
6054
|
-
}
|
|
6055
|
-
}
|
|
6056
|
-
|
|
6057
6134
|
// src/db/tasks.ts
|
|
6135
|
+
init_types();
|
|
6058
6136
|
init_database();
|
|
6059
6137
|
|
|
6060
6138
|
// src/db/projects.ts
|
|
6139
|
+
init_types();
|
|
6061
6140
|
init_database();
|
|
6062
6141
|
function slugify(name) {
|
|
6063
6142
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -6146,6 +6225,9 @@ function nextTaskShortId(projectId, db) {
|
|
|
6146
6225
|
return `${updated.task_prefix}-${padded}`;
|
|
6147
6226
|
}
|
|
6148
6227
|
|
|
6228
|
+
// src/lib/completion-guard.ts
|
|
6229
|
+
init_types();
|
|
6230
|
+
|
|
6149
6231
|
// src/lib/config.ts
|
|
6150
6232
|
import { existsSync as existsSync3 } from "fs";
|
|
6151
6233
|
import { join as join3 } from "path";
|
|
@@ -7489,6 +7571,7 @@ function bulkUpdateTasks(taskIds, updates, db) {
|
|
|
7489
7571
|
}
|
|
7490
7572
|
|
|
7491
7573
|
// src/db/comments.ts
|
|
7574
|
+
init_types();
|
|
7492
7575
|
init_database();
|
|
7493
7576
|
function addComment(input, db) {
|
|
7494
7577
|
const d = db || getDatabase();
|
|
@@ -7519,6 +7602,7 @@ function getComment(id, db) {
|
|
|
7519
7602
|
}
|
|
7520
7603
|
|
|
7521
7604
|
// src/db/plans.ts
|
|
7605
|
+
init_types();
|
|
7522
7606
|
init_database();
|
|
7523
7607
|
function createPlan(input, db) {
|
|
7524
7608
|
const d = db || getDatabase();
|
|
@@ -7591,6 +7675,7 @@ function deletePlan(id, db) {
|
|
|
7591
7675
|
init_agents();
|
|
7592
7676
|
|
|
7593
7677
|
// src/db/task-lists.ts
|
|
7678
|
+
init_types();
|
|
7594
7679
|
init_database();
|
|
7595
7680
|
function rowToTaskList(row) {
|
|
7596
7681
|
return {
|
|
@@ -8277,6 +8362,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
8277
8362
|
|
|
8278
8363
|
// src/mcp/index.ts
|
|
8279
8364
|
init_database();
|
|
8365
|
+
init_types();
|
|
8280
8366
|
import { readFileSync as readFileSync3 } from "fs";
|
|
8281
8367
|
import { join as join6, dirname as dirname2 } from "path";
|
|
8282
8368
|
import { fileURLToPath } from "url";
|
|
@@ -11003,6 +11089,64 @@ if (shouldRegisterTool("list_active_files")) {
|
|
|
11003
11089
|
}
|
|
11004
11090
|
});
|
|
11005
11091
|
}
|
|
11092
|
+
if (shouldRegisterTool("lock_file")) {
|
|
11093
|
+
server.tool("lock_file", "Acquire an exclusive lock on a file path. Throws if another agent holds an active lock. Same agent re-locks refreshes the TTL.", {
|
|
11094
|
+
path: exports_external.string().describe("File path to lock"),
|
|
11095
|
+
agent_id: exports_external.string().describe("Agent acquiring the lock"),
|
|
11096
|
+
task_id: exports_external.string().optional().describe("Task this lock is associated with"),
|
|
11097
|
+
ttl_seconds: exports_external.number().optional().describe("Lock TTL in seconds (default: 1800 = 30 min)")
|
|
11098
|
+
}, async ({ path, agent_id, task_id, ttl_seconds }) => {
|
|
11099
|
+
try {
|
|
11100
|
+
const { lockFile: lockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
11101
|
+
const lock = lockFile2({ path, agent_id, task_id, ttl_seconds });
|
|
11102
|
+
return { content: [{ type: "text", text: JSON.stringify(lock, null, 2) }] };
|
|
11103
|
+
} catch (e) {
|
|
11104
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11105
|
+
}
|
|
11106
|
+
});
|
|
11107
|
+
}
|
|
11108
|
+
if (shouldRegisterTool("unlock_file")) {
|
|
11109
|
+
server.tool("unlock_file", "Release a file lock. Only the lock holder can release it. Returns true if released.", {
|
|
11110
|
+
path: exports_external.string().describe("File path to unlock"),
|
|
11111
|
+
agent_id: exports_external.string().describe("Agent releasing the lock (must be the lock holder)")
|
|
11112
|
+
}, async ({ path, agent_id }) => {
|
|
11113
|
+
try {
|
|
11114
|
+
const { unlockFile: unlockFile2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
11115
|
+
const released = unlockFile2(path, agent_id);
|
|
11116
|
+
return { content: [{ type: "text", text: JSON.stringify({ released, path }) }] };
|
|
11117
|
+
} catch (e) {
|
|
11118
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11119
|
+
}
|
|
11120
|
+
});
|
|
11121
|
+
}
|
|
11122
|
+
if (shouldRegisterTool("check_file_lock")) {
|
|
11123
|
+
server.tool("check_file_lock", "Check who holds a lock on a file path. Returns null if unlocked or expired.", {
|
|
11124
|
+
path: exports_external.string().describe("File path to check")
|
|
11125
|
+
}, async ({ path }) => {
|
|
11126
|
+
try {
|
|
11127
|
+
const { checkFileLock: checkFileLock2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
11128
|
+
const lock = checkFileLock2(path);
|
|
11129
|
+
if (!lock)
|
|
11130
|
+
return { content: [{ type: "text", text: JSON.stringify({ path, locked: false }) }] };
|
|
11131
|
+
return { content: [{ type: "text", text: JSON.stringify({ path, locked: true, ...lock }) }] };
|
|
11132
|
+
} catch (e) {
|
|
11133
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11134
|
+
}
|
|
11135
|
+
});
|
|
11136
|
+
}
|
|
11137
|
+
if (shouldRegisterTool("list_file_locks")) {
|
|
11138
|
+
server.tool("list_file_locks", "List all active file locks. Optionally filter by agent_id.", {
|
|
11139
|
+
agent_id: exports_external.string().optional().describe("Filter locks by agent")
|
|
11140
|
+
}, async ({ agent_id }) => {
|
|
11141
|
+
try {
|
|
11142
|
+
const { listFileLocks: listFileLocks2 } = (init_file_locks(), __toCommonJS(exports_file_locks));
|
|
11143
|
+
const locks = listFileLocks2(agent_id);
|
|
11144
|
+
return { content: [{ type: "text", text: JSON.stringify(locks, null, 2) }] };
|
|
11145
|
+
} catch (e) {
|
|
11146
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11147
|
+
}
|
|
11148
|
+
});
|
|
11149
|
+
}
|
|
11006
11150
|
if (shouldRegisterTool("create_handoff")) {
|
|
11007
11151
|
server.tool("create_handoff", "Create a session handoff note for agent coordination.", {
|
|
11008
11152
|
agent_id: exports_external.string().optional().describe("Agent creating the handoff"),
|
package/dist/server/index.js
CHANGED
|
@@ -858,6 +858,20 @@ var init_database = __esm(() => {
|
|
|
858
858
|
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
|
|
859
859
|
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
|
|
860
860
|
INSERT OR IGNORE INTO _migrations (id) VALUES (30);
|
|
861
|
+
`,
|
|
862
|
+
`
|
|
863
|
+
CREATE TABLE IF NOT EXISTS file_locks (
|
|
864
|
+
id TEXT PRIMARY KEY,
|
|
865
|
+
path TEXT NOT NULL UNIQUE,
|
|
866
|
+
agent_id TEXT NOT NULL,
|
|
867
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
868
|
+
expires_at TEXT NOT NULL,
|
|
869
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
870
|
+
);
|
|
871
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_path ON file_locks(path);
|
|
872
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_agent ON file_locks(agent_id);
|
|
873
|
+
CREATE INDEX IF NOT EXISTS idx_file_locks_expires ON file_locks(expires_at);
|
|
874
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (31);
|
|
861
875
|
`
|
|
862
876
|
];
|
|
863
877
|
});
|
|
@@ -2523,7 +2537,7 @@ function registerAgent(input, db) {
|
|
|
2523
2537
|
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
2524
2538
|
working_dir: existing.working_dir,
|
|
2525
2539
|
suggestions: suggestions.slice(0, 5),
|
|
2526
|
-
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(
|
|
2540
|
+
message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session \u2026${existing.session_id?.slice(-4)}, dir: ${existing.working_dir ?? "unknown"}). Cannot reclaim an active agent \u2014 choose a different name, or wait for the session to go stale.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
|
|
2527
2541
|
};
|
|
2528
2542
|
}
|
|
2529
2543
|
const updates = ["last_seen_at = ?", "status = 'active'"];
|