@hasna/todos 0.10.1 → 0.10.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IAC1D,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC1C;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,EAItC,CAAC;AAWF,wBAAgB,UAAU,IAAI,WAAW,CAYxC;AAED,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,GAAG,IAAI,CAKzD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM7D;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAG7D;AAUD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBpE;AAED,wBAAgB,wBAAwB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC,qBAAqB,CAAC,CASrG"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IAC1D,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC1C;AAWD,wBAAgB,UAAU,IAAI,WAAW,CAYxC;AAED,wBAAgB,uBAAuB,IAAI,MAAM,EAAE,GAAG,IAAI,CAKzD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM7D;AAED,wBAAgB,mBAAmB,IAAI,gBAAgB,GAAG,IAAI,CAG7D;AAUD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAmB3E;AAED,wBAAgB,wBAAwB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC,qBAAqB,CAAC,CASrG"}
package/dist/mcp/index.js CHANGED
@@ -150,6 +150,7 @@ function ensureSchema(db) {
150
150
  CREATE TABLE agents (
151
151
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
152
152
  role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
153
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
153
154
  metadata TEXT DEFAULT '{}',
154
155
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
155
156
  last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
@@ -799,6 +800,11 @@ var init_database = __esm(() => {
799
800
  `
800
801
  ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]';
801
802
  INSERT OR IGNORE INTO _migrations (id) VALUES (29);
803
+ `,
804
+ `
805
+ ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
806
+ CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
807
+ INSERT OR IGNORE INTO _migrations (id) VALUES (30);
802
808
  `
803
809
  ];
804
810
  });
@@ -953,6 +959,7 @@ var exports_agents = {};
953
959
  __export(exports_agents, {
954
960
  updateAgentActivity: () => updateAgentActivity,
955
961
  updateAgent: () => updateAgent,
962
+ unarchiveAgent: () => unarchiveAgent,
956
963
  registerAgent: () => registerAgent,
957
964
  matchCapabilities: () => matchCapabilities,
958
965
  listAgents: () => listAgents,
@@ -963,11 +970,12 @@ __export(exports_agents, {
963
970
  getAvailableNamesFromPool: () => getAvailableNamesFromPool,
964
971
  getAgentByName: () => getAgentByName,
965
972
  getAgent: () => getAgent,
966
- deleteAgent: () => deleteAgent
973
+ deleteAgent: () => deleteAgent,
974
+ archiveAgent: () => archiveAgent
967
975
  });
968
976
  function getAvailableNamesFromPool(pool, db) {
969
977
  const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS).toISOString();
970
- const activeNames = new Set(db.query("SELECT name FROM agents WHERE last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
978
+ const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
971
979
  return pool.filter((name) => !activeNames.has(name.toLowerCase()));
972
980
  }
973
981
  function shortUuid() {
@@ -978,30 +986,13 @@ function rowToAgent(row) {
978
986
  ...row,
979
987
  permissions: JSON.parse(row.permissions || '["*"]'),
980
988
  capabilities: JSON.parse(row.capabilities || "[]"),
989
+ status: row.status || "active",
981
990
  metadata: JSON.parse(row.metadata || "{}")
982
991
  };
983
992
  }
984
993
  function registerAgent(input, db) {
985
994
  const d = db || getDatabase();
986
995
  const normalizedName = input.name.trim().toLowerCase();
987
- if (input.pool && input.pool.length > 0) {
988
- const poolLower = input.pool.map((n) => n.toLowerCase());
989
- if (!poolLower.includes(normalizedName)) {
990
- const available = getAvailableNamesFromPool(input.pool, d);
991
- const suggestion = available.length > 0 ? available[0] : null;
992
- return {
993
- conflict: true,
994
- pool_violation: true,
995
- existing_id: "",
996
- existing_name: normalizedName,
997
- last_seen_at: "",
998
- session_hint: null,
999
- working_dir: input.working_dir || null,
1000
- suggestions: available.slice(0, 5),
1001
- message: `"${normalizedName}" is not in this project's agent pool [${input.pool.join(", ")}]. ${available.length > 0 ? `Try: ${available.slice(0, 3).join(", ")}` : "No names are currently available \u2014 wait for an active agent to go stale."}${suggestion ? ` Suggested: ${suggestion}` : ""}`
1002
- };
1003
- }
1004
- }
1005
996
  const existing = getAgentByName(normalizedName, d);
1006
997
  if (existing) {
1007
998
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
@@ -1022,7 +1013,7 @@ function registerAgent(input, db) {
1022
1013
  message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
1023
1014
  };
1024
1015
  }
1025
- const updates = ["last_seen_at = ?"];
1016
+ const updates = ["last_seen_at = ?", "status = 'active'"];
1026
1017
  const params = [now()];
1027
1018
  if (input.session_id && !sameSession) {
1028
1019
  updates.push("session_id = ?");
@@ -1076,9 +1067,19 @@ function getAgentByName(name, db) {
1076
1067
  const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
1077
1068
  return row ? rowToAgent(row) : null;
1078
1069
  }
1079
- function listAgents(db) {
1080
- const d = db || getDatabase();
1081
- return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
1070
+ function listAgents(opts, db) {
1071
+ let d;
1072
+ let includeArchived = false;
1073
+ if (opts && typeof opts === "object" && "query" in opts) {
1074
+ d = opts;
1075
+ } else {
1076
+ includeArchived = opts?.include_archived ?? false;
1077
+ d = db || getDatabase();
1078
+ }
1079
+ if (includeArchived) {
1080
+ return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
1081
+ }
1082
+ return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
1082
1083
  }
1083
1084
  function updateAgentActivity(id, db) {
1084
1085
  const d = db || getDatabase();
@@ -1137,7 +1138,17 @@ function updateAgent(id, input, db) {
1137
1138
  }
1138
1139
  function deleteAgent(id, db) {
1139
1140
  const d = db || getDatabase();
1140
- return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
1141
+ return d.run("UPDATE agents SET status = 'archived', last_seen_at = ? WHERE id = ?", [now(), id]).changes > 0;
1142
+ }
1143
+ function archiveAgent(id, db) {
1144
+ const d = db || getDatabase();
1145
+ d.run("UPDATE agents SET status = 'archived', last_seen_at = ? WHERE id = ?", [now(), id]);
1146
+ return getAgent(id, d);
1147
+ }
1148
+ function unarchiveAgent(id, db) {
1149
+ const d = db || getDatabase();
1150
+ d.run("UPDATE agents SET status = 'active', last_seen_at = ? WHERE id = ?", [now(), id]);
1151
+ return getAgent(id, d);
1141
1152
  }
1142
1153
  function getDirectReports(agentId, db) {
1143
1154
  const d = db || getDatabase();
@@ -6135,23 +6146,6 @@ function appendSyncConflict(metadata, conflict, limit = 5) {
6135
6146
  }
6136
6147
 
6137
6148
  // src/lib/config.ts
6138
- var DEFAULT_AGENT_POOL = [
6139
- "maximus",
6140
- "cassius",
6141
- "aurelius",
6142
- "brutus",
6143
- "titus",
6144
- "nero",
6145
- "cicero",
6146
- "seneca",
6147
- "cato",
6148
- "julius",
6149
- "marcus",
6150
- "lucius",
6151
- "quintus",
6152
- "gaius",
6153
- "publius"
6154
- ];
6155
6149
  function getConfigPath() {
6156
6150
  return join3(process.env["HOME"] || HOME, ".todos", "config.json");
6157
6151
  }
@@ -6216,7 +6210,7 @@ function getAgentPoolForProject(workingDir) {
6216
6210
  return config.project_pools[bestKey];
6217
6211
  }
6218
6212
  }
6219
- return config.agent_pool || DEFAULT_AGENT_POOL;
6213
+ return config.agent_pool || null;
6220
6214
  }
6221
6215
  function getCompletionGuardConfig(projectPath) {
6222
6216
  const config = loadConfig();
@@ -8253,13 +8247,15 @@ var MINIMAL_TOOLS = new Set([
8253
8247
  "add_comment",
8254
8248
  "get_next_task",
8255
8249
  "bootstrap",
8256
- "get_tasks_changed_since"
8250
+ "get_tasks_changed_since",
8251
+ "heartbeat"
8257
8252
  ]);
8258
8253
  var STANDARD_EXCLUDED = new Set([
8259
8254
  "get_org_chart",
8260
8255
  "set_reports_to",
8261
8256
  "rename_agent",
8262
8257
  "delete_agent",
8258
+ "unarchive_agent",
8263
8259
  "create_webhook",
8264
8260
  "list_webhooks",
8265
8261
  "delete_webhook",
@@ -9228,8 +9224,8 @@ if (shouldRegisterTool("unfocus")) {
9228
9224
  });
9229
9225
  }
9230
9226
  if (shouldRegisterTool("register_agent")) {
9231
- server.tool("register_agent", "Register an agent. Name must be from the project's configured pool. Returns a conflict error with available name suggestions if the name is taken or not allowed.", {
9232
- name: exports_external.string().describe("Agent name \u2014 must be from the project's allowed pool (Roman names by default). Use suggest_agent_name first if unsure."),
9227
+ server.tool("register_agent", "Register an agent. Any name is allowed \u2014 the configured pool is advisory, not enforced. Returns a conflict error if the name is held by a recently-active agent.", {
9228
+ name: exports_external.string().describe("Agent name \u2014 any name is allowed. Use suggest_agent_name to see pool suggestions and avoid conflicts."),
9233
9229
  description: exports_external.string().optional(),
9234
9230
  capabilities: exports_external.array(exports_external.string()).optional().describe("Agent capabilities/skills for task routing (e.g. ['typescript', 'testing', 'devops'])"),
9235
9231
  session_id: exports_external.string().optional().describe("Unique ID for this coding session (e.g. process PID + timestamp, or env var). Used to detect name collisions across sessions. Store it and pass on every register_agent call."),
@@ -9237,17 +9233,19 @@ if (shouldRegisterTool("register_agent")) {
9237
9233
  }, async ({ name, description, capabilities, session_id, working_dir }) => {
9238
9234
  try {
9239
9235
  const pool = getAgentPoolForProject(working_dir);
9240
- const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool });
9236
+ const result = registerAgent({ name, description, capabilities, session_id, working_dir, pool: pool || undefined });
9241
9237
  if (isAgentConflict(result)) {
9242
9238
  const suggestLine = result.suggestions && result.suggestions.length > 0 ? `
9243
9239
  Available names: ${result.suggestions.join(", ")}` : "";
9244
- const hint = result.pool_violation ? `POOL_VIOLATION: ${result.message}${suggestLine}` : `CONFLICT: ${result.message}${suggestLine}`;
9240
+ const hint = `CONFLICT: ${result.message}${suggestLine}`;
9245
9241
  return {
9246
9242
  content: [{ type: "text", text: hint }],
9247
9243
  isError: true
9248
9244
  };
9249
9245
  }
9250
9246
  const agent = result;
9247
+ const poolLine = pool ? `
9248
+ Pool: [${pool.join(", ")}]` : "";
9251
9249
  return {
9252
9250
  content: [{
9253
9251
  type: "text",
@@ -9255,8 +9253,7 @@ Available names: ${result.suggestions.join(", ")}` : "";
9255
9253
  ID: ${agent.id}
9256
9254
  Name: ${agent.name}${agent.description ? `
9257
9255
  Description: ${agent.description}` : ""}
9258
- Session: ${agent.session_id ?? "unbound"}
9259
- Pool: [${pool.join(", ")}]
9256
+ Session: ${agent.session_id ?? "unbound"}${poolLine}
9260
9257
  Created: ${agent.created_at}
9261
9258
  Last seen: ${agent.last_seen_at}`
9262
9259
  }]
@@ -9267,21 +9264,32 @@ Last seen: ${agent.last_seen_at}`
9267
9264
  });
9268
9265
  }
9269
9266
  if (shouldRegisterTool("suggest_agent_name")) {
9270
- server.tool("suggest_agent_name", "Get available agent names for a project. Call this before register_agent to avoid conflicts.", {
9271
- working_dir: exports_external.string().optional().describe("Your working directory \u2014 used to look up the project's allowed name pool")
9267
+ server.tool("suggest_agent_name", "Get available agent names for a project. Shows configured pool, active agents, and suggestions. If no pool is configured, any name is allowed.", {
9268
+ working_dir: exports_external.string().optional().describe("Your working directory \u2014 used to look up the project's allowed name pool from config")
9272
9269
  }, async ({ working_dir }) => {
9273
9270
  try {
9274
9271
  const pool = getAgentPoolForProject(working_dir);
9275
- const available = getAvailableNamesFromPool(pool, getDatabase());
9276
9272
  const cutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
9277
- const active = listAgents().filter((a) => a.last_seen_at > cutoff && pool.map((n) => n.toLowerCase()).includes(a.name));
9273
+ const allActive = listAgents().filter((a) => a.last_seen_at > cutoff);
9274
+ if (!pool) {
9275
+ const lines2 = [
9276
+ "No agent pool configured \u2014 any name is allowed.",
9277
+ allActive.length > 0 ? `Active agents (avoid these names): ${allActive.map((a) => `${a.name} (seen ${Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000)}m ago)`).join(", ")}` : "No active agents.",
9278
+ `
9279
+ To restrict names, configure agent_pool or project_pools in ~/.todos/config.json`
9280
+ ];
9281
+ return { content: [{ type: "text", text: lines2.join(`
9282
+ `) }] };
9283
+ }
9284
+ const available = getAvailableNamesFromPool(pool, getDatabase());
9285
+ const activeInPool = allActive.filter((a) => pool.map((n) => n.toLowerCase()).includes(a.name));
9278
9286
  const lines = [
9279
9287
  `Project pool: ${pool.join(", ")}`,
9280
9288
  `Available now (${available.length}): ${available.length > 0 ? available.join(", ") : "none \u2014 all names in use"}`,
9281
- active.length > 0 ? `Active agents: ${active.map((a) => `${a.name} (seen ${Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000)}m ago)`).join(", ")}` : "Active agents: none",
9289
+ activeInPool.length > 0 ? `Active agents: ${activeInPool.map((a) => `${a.name} (seen ${Math.round((Date.now() - new Date(a.last_seen_at).getTime()) / 60000)}m ago)`).join(", ")}` : "Active agents: none",
9282
9290
  available.length > 0 ? `
9283
9291
  Suggested: ${available[0]}` : `
9284
- No names available yet. Wait for an active agent to go stale (30min timeout).`
9292
+ No names available. Wait for an active agent to go stale (30min timeout).`
9285
9293
  ];
9286
9294
  return { content: [{ type: "text", text: lines.join(`
9287
9295
  `) }] };
@@ -9291,14 +9299,17 @@ No names available yet. Wait for an active agent to go stale (30min timeout).`
9291
9299
  });
9292
9300
  }
9293
9301
  if (shouldRegisterTool("list_agents")) {
9294
- server.tool("list_agents", "List all registered agents", {}, async () => {
9302
+ server.tool("list_agents", "List all registered agents. By default shows only active agents \u2014 set include_archived to see archived ones too.", {
9303
+ include_archived: exports_external.boolean().optional().describe("Include archived agents in the list (default: false)")
9304
+ }, async ({ include_archived }) => {
9295
9305
  try {
9296
- const agents = listAgents();
9306
+ const agents = listAgents({ include_archived: include_archived ?? false });
9297
9307
  if (agents.length === 0) {
9298
9308
  return { content: [{ type: "text", text: "No agents registered." }] };
9299
9309
  }
9300
9310
  const text = agents.map((a) => {
9301
- return `${a.id} | ${a.name}${a.description ? ` - ${a.description}` : ""} (last seen: ${a.last_seen_at})`;
9311
+ const statusTag = a.status === "archived" ? " [archived]" : "";
9312
+ return `${a.id} | ${a.name}${statusTag}${a.description ? ` - ${a.description}` : ""} (last seen: ${a.last_seen_at})`;
9302
9313
  }).join(`
9303
9314
  `);
9304
9315
  return { content: [{ type: "text", text: `${agents.length} agent(s):
@@ -9366,7 +9377,7 @@ ID: ${updated.id}`
9366
9377
  });
9367
9378
  }
9368
9379
  if (shouldRegisterTool("delete_agent")) {
9369
- server.tool("delete_agent", "Delete an agent permanently. Resolve by id or name.", {
9380
+ server.tool("delete_agent", "Archive an agent (soft delete). The agent is hidden from list_agents but preserved for task history. Use unarchive_agent to restore. Resolve by id or name.", {
9370
9381
  id: exports_external.string().optional(),
9371
9382
  name: exports_external.string().optional()
9372
9383
  }, async ({ id, name }) => {
@@ -9378,13 +9389,63 @@ if (shouldRegisterTool("delete_agent")) {
9378
9389
  if (!agent) {
9379
9390
  return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
9380
9391
  }
9381
- const deleted = deleteAgent(agent.id);
9392
+ const archived = archiveAgent(agent.id);
9382
9393
  return {
9383
9394
  content: [{
9384
9395
  type: "text",
9385
- text: deleted ? `Agent deleted: ${agent.name} (${agent.id})` : `Failed to delete agent: ${agent.name}`
9396
+ text: archived ? `Agent archived: ${agent.name} (${agent.id}). Use unarchive_agent to restore.` : `Failed to archive agent: ${agent.name}`
9386
9397
  }],
9387
- isError: !deleted
9398
+ isError: !archived
9399
+ };
9400
+ } catch (e) {
9401
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
9402
+ }
9403
+ });
9404
+ }
9405
+ if (shouldRegisterTool("unarchive_agent")) {
9406
+ server.tool("unarchive_agent", "Restore an archived agent back to active status. Resolve by id or name.", {
9407
+ id: exports_external.string().optional(),
9408
+ name: exports_external.string().optional()
9409
+ }, async ({ id, name }) => {
9410
+ try {
9411
+ if (!id && !name) {
9412
+ return { content: [{ type: "text", text: "Provide either id or name." }], isError: true };
9413
+ }
9414
+ const agent = id ? getAgent(id) : getAgentByName(name);
9415
+ if (!agent) {
9416
+ return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
9417
+ }
9418
+ if (agent.status === "active") {
9419
+ return { content: [{ type: "text", text: `Agent ${agent.name} is already active.` }] };
9420
+ }
9421
+ const restored = unarchiveAgent(agent.id);
9422
+ return {
9423
+ content: [{
9424
+ type: "text",
9425
+ text: restored ? `Agent restored: ${agent.name} (${agent.id}) is now active.` : `Failed to restore agent: ${agent.name}`
9426
+ }],
9427
+ isError: !restored
9428
+ };
9429
+ } catch (e) {
9430
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
9431
+ }
9432
+ });
9433
+ }
9434
+ if (shouldRegisterTool("heartbeat")) {
9435
+ server.tool("heartbeat", "Update your last_seen_at timestamp to signal you're still active. Call periodically during long tasks to prevent being marked stale.", {
9436
+ agent_id: exports_external.string().describe("Your agent ID or name.")
9437
+ }, async ({ agent_id }) => {
9438
+ try {
9439
+ const agent = getAgent(agent_id) || getAgentByName(agent_id);
9440
+ if (!agent) {
9441
+ return { content: [{ type: "text", text: `Agent not found: ${agent_id}` }], isError: true };
9442
+ }
9443
+ updateAgentActivity(agent.id);
9444
+ return {
9445
+ content: [{
9446
+ type: "text",
9447
+ text: `Heartbeat: ${agent.name} (${agent.id}) \u2014 last_seen_at updated to ${new Date().toISOString()}`
9448
+ }]
9388
9449
  };
9389
9450
  } catch (e) {
9390
9451
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -10487,6 +10548,8 @@ if (shouldRegisterTool("search_tools")) {
10487
10548
  "get_agent",
10488
10549
  "rename_agent",
10489
10550
  "delete_agent",
10551
+ "unarchive_agent",
10552
+ "heartbeat",
10490
10553
  "get_my_tasks",
10491
10554
  "get_org_chart",
10492
10555
  "set_reports_to",
@@ -10624,22 +10687,30 @@ if (shouldRegisterTool("describe_tools")) {
10624
10687
  delete_plan: `Delete a plan. Tasks in the plan are orphaned, not deleted.
10625
10688
  Params: id(string, req)
10626
10689
  Example: {id: 'a1b2c3d4'}`,
10627
- suggest_agent_name: `Get available agent names for your project before registering. Shows the pool, which names are active, and the best suggestion.
10628
- Params: working_dir(string \u2014 your working directory, used to look up project pool)
10690
+ suggest_agent_name: `Check available agent names before registering. Shows active agents and, if a pool is configured, which pool names are free.
10691
+ Params: working_dir(string \u2014 your working directory, used to look up project pool from config)
10629
10692
  Example: {working_dir: '/workspace/platform'}`,
10630
- register_agent: `Register an agent. Name must be from the project's pool (call suggest_agent_name first). Returns CONFLICT or POOL_VIOLATION with suggestions if name is taken or not allowed.
10631
- Params: name(string, req), description(string), session_id(string \u2014 unique per session, e.g. PID+timestamp), working_dir(string, req \u2014 used to determine project pool)
10632
- Example: {name: 'cassius', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
10633
- list_agents: "List all registered agents with IDs, names, and last seen timestamps. No params.",
10693
+ register_agent: `Register an agent. Any name is allowed \u2014 pool is advisory. Returns CONFLICT if name is held by a recently-active agent.
10694
+ Params: name(string, req), description(string), capabilities(string[]), session_id(string \u2014 unique per session), working_dir(string \u2014 used to determine project pool)
10695
+ Example: {name: 'my-agent', session_id: 'abc123-1741952000', working_dir: '/workspace/platform'}`,
10696
+ list_agents: `List all registered agents (active by default). Set include_archived: true to see archived agents.
10697
+ Params: include_archived(boolean, optional)
10698
+ Example: {include_archived: true}`,
10634
10699
  get_agent: `Get agent details by ID or name. Provide one of id or name.
10635
10700
  Params: id(string), name(string)
10636
10701
  Example: {name: 'maximus'}`,
10637
10702
  rename_agent: `Rename an agent. Resolve by id or current name.
10638
10703
  Params: id(string), name(string \u2014 current name), new_name(string, req)
10639
10704
  Example: {name: 'old-name', new_name: 'new-name'}`,
10640
- delete_agent: `Delete an agent permanently. Resolve by id or name.
10705
+ delete_agent: `Archive an agent (soft delete). Agent is preserved for task history but hidden from list_agents. Use unarchive_agent to restore.
10706
+ Params: id(string), name(string)
10707
+ Example: {name: 'maximus'}`,
10708
+ unarchive_agent: `Restore an archived agent back to active status.
10641
10709
  Params: id(string), name(string)
10642
10710
  Example: {name: 'maximus'}`,
10711
+ heartbeat: `Update last_seen_at timestamp to signal you're still active. Call periodically during long tasks.
10712
+ Params: agent_id(string, req \u2014 your agent ID or name)
10713
+ Example: {agent_id: 'maximus'}`,
10643
10714
  get_my_tasks: `Get all tasks assigned to/created by an agent, with stats (pending/active/done/rate).
10644
10715
  Params: agent_name(string, req)
10645
10716
  Example: {agent_name: 'maximus'}`,
@@ -1,21 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var __create = Object.create;
4
- var __getProtoOf = Object.getPrototypeOf;
5
3
  var __defProp = Object.defineProperty;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __toESM = (mod, isNodeMode, target) => {
9
- target = mod != null ? __create(__getProtoOf(mod)) : {};
10
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
- for (let key of __getOwnPropNames(mod))
12
- if (!__hasOwnProp.call(to, key))
13
- __defProp(to, key, {
14
- get: () => mod[key],
15
- enumerable: true
16
- });
17
- return to;
18
- };
19
4
  var __export = (target, all) => {
20
5
  for (var name in all)
21
6
  __defProp(target, name, {
@@ -234,6 +219,7 @@ function ensureSchema(db) {
234
219
  CREATE TABLE agents (
235
220
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT,
236
221
  role TEXT DEFAULT 'agent', permissions TEXT DEFAULT '["*"]',
222
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived')),
237
223
  metadata TEXT DEFAULT '{}',
238
224
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
239
225
  last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
@@ -863,6 +849,11 @@ var init_database = __esm(() => {
863
849
  `
864
850
  ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]';
865
851
  INSERT OR IGNORE INTO _migrations (id) VALUES (29);
852
+ `,
853
+ `
854
+ ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'archived'));
855
+ CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
856
+ INSERT OR IGNORE INTO _migrations (id) VALUES (30);
866
857
  `
867
858
  ];
868
859
  });
@@ -2477,6 +2468,7 @@ var exports_agents = {};
2477
2468
  __export(exports_agents, {
2478
2469
  updateAgentActivity: () => updateAgentActivity,
2479
2470
  updateAgent: () => updateAgent,
2471
+ unarchiveAgent: () => unarchiveAgent,
2480
2472
  registerAgent: () => registerAgent,
2481
2473
  matchCapabilities: () => matchCapabilities,
2482
2474
  listAgents: () => listAgents,
@@ -2487,11 +2479,12 @@ __export(exports_agents, {
2487
2479
  getAvailableNamesFromPool: () => getAvailableNamesFromPool,
2488
2480
  getAgentByName: () => getAgentByName,
2489
2481
  getAgent: () => getAgent,
2490
- deleteAgent: () => deleteAgent
2482
+ deleteAgent: () => deleteAgent,
2483
+ archiveAgent: () => archiveAgent
2491
2484
  });
2492
2485
  function getAvailableNamesFromPool(pool, db) {
2493
2486
  const cutoff = new Date(Date.now() - AGENT_ACTIVE_WINDOW_MS).toISOString();
2494
- const activeNames = new Set(db.query("SELECT name FROM agents WHERE last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
2487
+ const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
2495
2488
  return pool.filter((name) => !activeNames.has(name.toLowerCase()));
2496
2489
  }
2497
2490
  function shortUuid() {
@@ -2502,30 +2495,13 @@ function rowToAgent(row) {
2502
2495
  ...row,
2503
2496
  permissions: JSON.parse(row.permissions || '["*"]'),
2504
2497
  capabilities: JSON.parse(row.capabilities || "[]"),
2498
+ status: row.status || "active",
2505
2499
  metadata: JSON.parse(row.metadata || "{}")
2506
2500
  };
2507
2501
  }
2508
2502
  function registerAgent(input, db) {
2509
2503
  const d = db || getDatabase();
2510
2504
  const normalizedName = input.name.trim().toLowerCase();
2511
- if (input.pool && input.pool.length > 0) {
2512
- const poolLower = input.pool.map((n) => n.toLowerCase());
2513
- if (!poolLower.includes(normalizedName)) {
2514
- const available = getAvailableNamesFromPool(input.pool, d);
2515
- const suggestion = available.length > 0 ? available[0] : null;
2516
- return {
2517
- conflict: true,
2518
- pool_violation: true,
2519
- existing_id: "",
2520
- existing_name: normalizedName,
2521
- last_seen_at: "",
2522
- session_hint: null,
2523
- working_dir: input.working_dir || null,
2524
- suggestions: available.slice(0, 5),
2525
- message: `"${normalizedName}" is not in this project's agent pool [${input.pool.join(", ")}]. ${available.length > 0 ? `Try: ${available.slice(0, 3).join(", ")}` : "No names are currently available \u2014 wait for an active agent to go stale."}${suggestion ? ` Suggested: ${suggestion}` : ""}`
2526
- };
2527
- }
2528
- }
2529
2505
  const existing = getAgentByName(normalizedName, d);
2530
2506
  if (existing) {
2531
2507
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
@@ -2546,7 +2522,7 @@ function registerAgent(input, db) {
2546
2522
  message: `Agent "${normalizedName}" is already active (last seen ${minutesAgo}m ago, session ${existing.session_id?.slice(0, 8)}\u2026, dir: ${existing.working_dir ?? "unknown"}). Are you that agent? If so, pass session_id="${existing.session_id}" to reclaim it. Otherwise choose a different name.${suggestions.length > 0 ? ` Available: ${suggestions.slice(0, 3).join(", ")}` : ""}`
2547
2523
  };
2548
2524
  }
2549
- const updates = ["last_seen_at = ?"];
2525
+ const updates = ["last_seen_at = ?", "status = 'active'"];
2550
2526
  const params = [now()];
2551
2527
  if (input.session_id && !sameSession) {
2552
2528
  updates.push("session_id = ?");
@@ -2600,9 +2576,19 @@ function getAgentByName(name, db) {
2600
2576
  const row = d.query("SELECT * FROM agents WHERE LOWER(name) = ?").get(normalizedName);
2601
2577
  return row ? rowToAgent(row) : null;
2602
2578
  }
2603
- function listAgents(db) {
2604
- const d = db || getDatabase();
2605
- return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
2579
+ function listAgents(opts, db) {
2580
+ let d;
2581
+ let includeArchived = false;
2582
+ if (opts && typeof opts === "object" && "query" in opts) {
2583
+ d = opts;
2584
+ } else {
2585
+ includeArchived = opts?.include_archived ?? false;
2586
+ d = db || getDatabase();
2587
+ }
2588
+ if (includeArchived) {
2589
+ return d.query("SELECT * FROM agents ORDER BY name").all().map(rowToAgent);
2590
+ }
2591
+ return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
2606
2592
  }
2607
2593
  function updateAgentActivity(id, db) {
2608
2594
  const d = db || getDatabase();
@@ -2661,7 +2647,17 @@ function updateAgent(id, input, db) {
2661
2647
  }
2662
2648
  function deleteAgent(id, db) {
2663
2649
  const d = db || getDatabase();
2664
- return d.run("DELETE FROM agents WHERE id = ?", [id]).changes > 0;
2650
+ return d.run("UPDATE agents SET status = 'archived', last_seen_at = ? WHERE id = ?", [now(), id]).changes > 0;
2651
+ }
2652
+ function archiveAgent(id, db) {
2653
+ const d = db || getDatabase();
2654
+ d.run("UPDATE agents SET status = 'archived', last_seen_at = ? WHERE id = ?", [now(), id]);
2655
+ return getAgent(id, d);
2656
+ }
2657
+ function unarchiveAgent(id, db) {
2658
+ const d = db || getDatabase();
2659
+ d.run("UPDATE agents SET status = 'active', last_seen_at = ? WHERE id = ?", [now(), id]);
2660
+ return getAgent(id, d);
2665
2661
  }
2666
2662
  function getDirectReports(agentId, db) {
2667
2663
  const d = db || getDatabase();
@@ -3807,6 +3803,7 @@ async function main() {
3807
3803
  if (port !== requestedPort) {
3808
3804
  console.log(`Port ${requestedPort} in use, using ${port}`);
3809
3805
  }
3810
- startServer(port);
3806
+ const noOpen = process.argv.includes("--no-open") || process.env["TODOS_NO_OPEN"] === "true";
3807
+ startServer(port, { open: !noOpen });
3811
3808
  }
3812
3809
  main();
@@ -92,6 +92,7 @@ export interface UpdatePlanInput {
92
92
  task_list_id?: string;
93
93
  agent_id?: string;
94
94
  }
95
+ export type AgentStatus = "active" | "archived";
95
96
  export interface Agent {
96
97
  id: string;
97
98
  name: string;
@@ -103,6 +104,7 @@ export interface Agent {
103
104
  reports_to: string | null;
104
105
  org_id: string | null;
105
106
  capabilities: string[];
107
+ status: AgentStatus;
106
108
  metadata: Record<string, unknown>;
107
109
  created_at: string;
108
110
  last_seen_at: string;
@@ -120,6 +122,7 @@ export interface AgentRow {
120
122
  capabilities: string | null;
121
123
  reports_to: string | null;
122
124
  org_id: string | null;
125
+ status: string;
123
126
  metadata: string | null;
124
127
  created_at: string;
125
128
  last_seen_at: string;
@@ -151,7 +154,6 @@ export interface AgentConflictError {
151
154
  working_dir: string | null;
152
155
  message: string;
153
156
  suggestions?: string[];
154
- pool_violation?: true;
155
157
  }
156
158
  export interface TaskList {
157
159
  id: string;