@hasna/todos 0.11.34 → 0.11.36

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/mcp/index.js CHANGED
@@ -830,6 +830,22 @@ var init_migrations = __esm(() => {
830
830
  ALTER TABLE tasks ADD COLUMN cycle_id TEXT REFERENCES cycles(id) ON DELETE SET NULL;
831
831
  CREATE INDEX IF NOT EXISTS idx_tasks_cycle ON tasks(cycle_id) WHERE cycle_id IS NOT NULL;
832
832
  INSERT OR IGNORE INTO _migrations (id) VALUES (49);
833
+ `,
834
+ `
835
+ CREATE TABLE IF NOT EXISTS api_keys (
836
+ id TEXT PRIMARY KEY,
837
+ name TEXT NOT NULL,
838
+ key_hash TEXT NOT NULL UNIQUE,
839
+ prefix TEXT NOT NULL UNIQUE,
840
+ permissions TEXT NOT NULL DEFAULT '["*"]',
841
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
842
+ last_used_at TEXT,
843
+ expires_at TEXT,
844
+ revoked_at TEXT
845
+ );
846
+ CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix);
847
+ CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at);
848
+ INSERT OR IGNORE INTO _migrations (id) VALUES (50);
833
849
  `
834
850
  ];
835
851
  });
@@ -1228,6 +1244,20 @@ function ensureSchema(db) {
1228
1244
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_cycles_dates ON cycles(start_date, end_date)");
1229
1245
  ensureColumn("tasks", "cycle_id", "TEXT REFERENCES cycles(id) ON DELETE SET NULL");
1230
1246
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_cycle ON tasks(cycle_id) WHERE cycle_id IS NOT NULL");
1247
+ ensureTable("api_keys", `
1248
+ CREATE TABLE api_keys (
1249
+ id TEXT PRIMARY KEY,
1250
+ name TEXT NOT NULL,
1251
+ key_hash TEXT NOT NULL UNIQUE,
1252
+ prefix TEXT NOT NULL UNIQUE,
1253
+ permissions TEXT NOT NULL DEFAULT '["*"]',
1254
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1255
+ last_used_at TEXT,
1256
+ expires_at TEXT,
1257
+ revoked_at TEXT
1258
+ )`);
1259
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)");
1260
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at)");
1231
1261
  }
1232
1262
  function backfillTaskTags(db) {
1233
1263
  try {
@@ -12655,14 +12685,246 @@ See https://www.postgresql.org/docs/current/libpq-ssl.html for libpq SSL mode de
12655
12685
  init_adapter();
12656
12686
  });
12657
12687
 
12688
+ // src/db/agent-names.ts
12689
+ function normalizeAgentNameInput(name) {
12690
+ return name.trim().toLowerCase();
12691
+ }
12692
+ function hasGeneratedNumericSuffix(name) {
12693
+ return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
12694
+ }
12695
+ function isGenericAgentName(name) {
12696
+ const normalized = normalizeAgentNameInput(name);
12697
+ if (RESERVED_GENERIC_NAMES.has(normalized))
12698
+ return true;
12699
+ for (const generic of RESERVED_GENERIC_NAMES) {
12700
+ if (normalized === `${generic}s`)
12701
+ return true;
12702
+ if (normalized.match(new RegExp(`^${generic}\\d+$`)))
12703
+ return true;
12704
+ if (normalized.match(new RegExp(`^${generic}[-_]\\d+$`)))
12705
+ return true;
12706
+ }
12707
+ return false;
12708
+ }
12709
+ function isBlockedAgentName(name) {
12710
+ const normalized = normalizeAgentNameInput(name);
12711
+ return isGenericAgentName(normalized) || hasGeneratedNumericSuffix(normalized) || !ONE_WORD_NAME_RE.test(normalized);
12712
+ }
12713
+ function alphabeticSuffix(index) {
12714
+ const letters = "abcdefghijklmnopqrstuvwxyz";
12715
+ let value = index;
12716
+ let suffix = "";
12717
+ do {
12718
+ suffix = letters[value % letters.length] + suffix;
12719
+ value = Math.floor(value / letters.length) - 1;
12720
+ } while (value >= 0);
12721
+ return suffix;
12722
+ }
12723
+ function suggestAgentNames(existingNames = []) {
12724
+ const existing = new Set([...existingNames].map(normalizeAgentNameInput));
12725
+ const suggestions = PREFERRED_AGENT_NAMES.filter((name) => !existing.has(name));
12726
+ for (let suffixIndex = 0;suggestions.length < 20 && suffixIndex < 1000; suffixIndex++) {
12727
+ const suffix = alphabeticSuffix(suffixIndex);
12728
+ for (const base of PREFERRED_AGENT_NAMES) {
12729
+ const candidate = `${base}${suffix}`;
12730
+ if (existing.has(candidate) || suggestions.includes(candidate))
12731
+ continue;
12732
+ suggestions.push(candidate);
12733
+ if (suggestions.length >= 20)
12734
+ break;
12735
+ }
12736
+ }
12737
+ return suggestions;
12738
+ }
12739
+ function validateAgentName(name, existingNames = []) {
12740
+ const normalized = normalizeAgentNameInput(name);
12741
+ const suggestions = suggestAgentNames(existingNames).slice(0, 5);
12742
+ if (!normalized) {
12743
+ throw new InvalidAgentNameError(name, "choose a real one-word name instead of an empty value", suggestions);
12744
+ }
12745
+ if (/\s/.test(normalized)) {
12746
+ throw new InvalidAgentNameError(name, "use a single word, preferably a Roman or Greek name", suggestions);
12747
+ }
12748
+ if (normalized.length < 3) {
12749
+ throw new InvalidAgentNameError(name, "use a more distinctive name with at least three characters", suggestions);
12750
+ }
12751
+ if (isGenericAgentName(normalized)) {
12752
+ throw new InvalidAgentNameError(name, "generic names like agent, agent-1, assistant, or worker-2 are reserved", suggestions);
12753
+ }
12754
+ if (hasGeneratedNumericSuffix(normalized)) {
12755
+ throw new InvalidAgentNameError(name, "numbered suffix names are not allowed; pick a distinct human-readable name", suggestions);
12756
+ }
12757
+ if (!ONE_WORD_NAME_RE.test(normalized)) {
12758
+ throw new InvalidAgentNameError(name, "use one word made of letters only, preferably a Roman or Greek name", suggestions);
12759
+ }
12760
+ return normalized;
12761
+ }
12762
+ function tableHasColumn(db, table, column) {
12763
+ try {
12764
+ return db.query(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
12765
+ } catch {
12766
+ return false;
12767
+ }
12768
+ }
12769
+ function updateReferences(db, oldName, newName) {
12770
+ const refs = [
12771
+ ["tasks", "assigned_to"],
12772
+ ["tasks", "agent_id"],
12773
+ ["tasks", "locked_by"],
12774
+ ["tasks", "assigned_by"],
12775
+ ["plans", "agent_id"],
12776
+ ["sessions", "agent_id"],
12777
+ ["task_comments", "agent_id"],
12778
+ ["task_history", "agent_id"],
12779
+ ["webhooks", "agent_id"],
12780
+ ["task_files", "agent_id"],
12781
+ ["task_time_logs", "agent_id"],
12782
+ ["task_watchers", "agent_id"],
12783
+ ["task_checkpoints", "agent_id"],
12784
+ ["task_heartbeats", "agent_id"],
12785
+ ["project_agent_roles", "agent_id"]
12786
+ ];
12787
+ let changed = 0;
12788
+ for (const [table, column] of refs) {
12789
+ if (!tableHasColumn(db, table, column))
12790
+ continue;
12791
+ try {
12792
+ changed += db.run(`UPDATE ${table} SET ${column} = ? WHERE LOWER(${column}) = ?`, [newName, oldName]).changes;
12793
+ } catch {}
12794
+ }
12795
+ return changed;
12796
+ }
12797
+ function normalizeGeneratedAgentNames(db) {
12798
+ const rows = db.query("SELECT * FROM agents ORDER BY created_at, id").all();
12799
+ const existing = new Set(rows.map((agent) => normalizeAgentNameInput(agent.name)));
12800
+ const renamed = [];
12801
+ for (const agent of rows) {
12802
+ const oldName = normalizeAgentNameInput(agent.name);
12803
+ if (!isBlockedAgentName(oldName))
12804
+ continue;
12805
+ const candidates = suggestAgentNames(existing);
12806
+ const replacement = candidates[0];
12807
+ if (!replacement) {
12808
+ throw new Error("No safe agent names are available for normalization");
12809
+ }
12810
+ existing.delete(oldName);
12811
+ existing.add(replacement);
12812
+ db.run("UPDATE agents SET name = ?, last_seen_at = ? WHERE id = ?", [replacement, now(), agent.id]);
12813
+ const referenceUpdates = updateReferences(db, oldName, replacement);
12814
+ renamed.push({
12815
+ id: agent.id,
12816
+ old_name: oldName,
12817
+ new_name: replacement,
12818
+ reference_updates: referenceUpdates
12819
+ });
12820
+ }
12821
+ return renamed;
12822
+ }
12823
+ var InvalidAgentNameError, ROMAN_AGENT_NAMES, GREEK_AGENT_NAMES, NICE_AGENT_NAMES, PREFERRED_AGENT_NAMES, RESERVED_GENERIC_NAMES, NUMERIC_SUFFIX_RE, ONE_WORD_NAME_RE;
12824
+ var init_agent_names = __esm(() => {
12825
+ init_database();
12826
+ InvalidAgentNameError = class InvalidAgentNameError extends Error {
12827
+ suggestions;
12828
+ constructor(name, reason, suggestions = []) {
12829
+ super(`Invalid agent name "${name}": ${reason}${suggestions.length > 0 ? `. Try: ${suggestions.join(", ")}` : ""}`);
12830
+ this.name = "InvalidAgentNameError";
12831
+ this.suggestions = suggestions;
12832
+ }
12833
+ };
12834
+ ROMAN_AGENT_NAMES = [
12835
+ "caesar",
12836
+ "augustus",
12837
+ "marcus",
12838
+ "brutus",
12839
+ "cicero",
12840
+ "cato",
12841
+ "nero",
12842
+ "claudius",
12843
+ "tiberius",
12844
+ "hadrian",
12845
+ "trajan",
12846
+ "vespasian",
12847
+ "domitian",
12848
+ "caligula",
12849
+ "commodus",
12850
+ "livia",
12851
+ "julia",
12852
+ "octavia",
12853
+ "claudia",
12854
+ "agrippina",
12855
+ "cornelia",
12856
+ "valeria",
12857
+ "fulvia",
12858
+ "hortensia",
12859
+ "fabia"
12860
+ ];
12861
+ GREEK_AGENT_NAMES = [
12862
+ "athena",
12863
+ "apollo",
12864
+ "artemis",
12865
+ "hera",
12866
+ "iris",
12867
+ "hector",
12868
+ "achilles",
12869
+ "odysseus",
12870
+ "theseus",
12871
+ "pericles",
12872
+ "solon",
12873
+ "sophia",
12874
+ "thalia",
12875
+ "calliope",
12876
+ "clio",
12877
+ "phoebe",
12878
+ "daphne",
12879
+ "leonidas",
12880
+ "andromeda",
12881
+ "cassander"
12882
+ ];
12883
+ NICE_AGENT_NAMES = [
12884
+ "atlas",
12885
+ "aurora",
12886
+ "ember",
12887
+ "nova",
12888
+ "orion",
12889
+ "rhea",
12890
+ "selene",
12891
+ "sirius",
12892
+ "vesper",
12893
+ "zephyr"
12894
+ ];
12895
+ PREFERRED_AGENT_NAMES = [
12896
+ ...ROMAN_AGENT_NAMES,
12897
+ ...GREEK_AGENT_NAMES,
12898
+ ...NICE_AGENT_NAMES
12899
+ ];
12900
+ RESERVED_GENERIC_NAMES = new Set([
12901
+ "agent",
12902
+ "agents",
12903
+ "ai",
12904
+ "assistant",
12905
+ "bot",
12906
+ "coder",
12907
+ "default",
12908
+ "helper",
12909
+ "model",
12910
+ "system",
12911
+ "user",
12912
+ "worker"
12913
+ ]);
12914
+ NUMERIC_SUFFIX_RE = /[-_]\d+$/;
12915
+ ONE_WORD_NAME_RE = /^[a-z]+$/;
12916
+ });
12917
+
12658
12918
  // src/db/agents.ts
12659
12919
  var exports_agents = {};
12660
12920
  __export(exports_agents, {
12661
12921
  updateAgentActivity: () => updateAgentActivity,
12662
12922
  updateAgent: () => updateAgent,
12663
12923
  unarchiveAgent: () => unarchiveAgent,
12924
+ suggestAgentNames: () => suggestAgentNames,
12664
12925
  releaseAgent: () => releaseAgent,
12665
12926
  registerAgent: () => registerAgent,
12927
+ normalizeGeneratedAgentNames: () => normalizeGeneratedAgentNames,
12666
12928
  matchCapabilities: () => matchCapabilities,
12667
12929
  listAgents: () => listAgents,
12668
12930
  isAgentConflict: () => isAgentConflict,
@@ -12674,7 +12936,8 @@ __export(exports_agents, {
12674
12936
  getAgent: () => getAgent,
12675
12937
  deleteAgent: () => deleteAgent,
12676
12938
  autoReleaseStaleAgents: () => autoReleaseStaleAgents,
12677
- archiveAgent: () => archiveAgent
12939
+ archiveAgent: () => archiveAgent,
12940
+ InvalidAgentNameError: () => InvalidAgentNameError
12678
12941
  });
12679
12942
  function getActiveWindowMs() {
12680
12943
  const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
@@ -12697,7 +12960,7 @@ function getAvailableNamesFromPool(pool, db) {
12697
12960
  autoReleaseStaleAgents(db);
12698
12961
  const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
12699
12962
  const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
12700
- return pool.filter((name) => !activeNames.has(name.toLowerCase()));
12963
+ return pool.map(normalizeAgentNameInput).filter((name) => !activeNames.has(name));
12701
12964
  }
12702
12965
  function shortUuid() {
12703
12966
  return crypto.randomUUID().slice(0, 8);
@@ -12713,7 +12976,8 @@ function rowToAgent(row) {
12713
12976
  }
12714
12977
  function registerAgent(input, db) {
12715
12978
  const d = db || getDatabase();
12716
- const normalizedName = input.name.trim().toLowerCase();
12979
+ const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
12980
+ const normalizedName = validateAgentName(input.name, existingNames);
12717
12981
  const existing = getAgentByName(normalizedName, d);
12718
12982
  if (existing) {
12719
12983
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
@@ -12846,7 +13110,8 @@ function updateAgent(id, input, db) {
12846
13110
  const sets = ["last_seen_at = ?"];
12847
13111
  const params = [now()];
12848
13112
  if (input.name !== undefined) {
12849
- const newName = input.name.trim().toLowerCase();
13113
+ const existingNames = d.query("SELECT name FROM agents WHERE id != ?").all(id).map((row) => row.name);
13114
+ const newName = validateAgentName(input.name, existingNames);
12850
13115
  const holder = getAgentByName(newName, d);
12851
13116
  if (holder && holder.id !== id) {
12852
13117
  const lastSeenMs = new Date(holder.last_seen_at).getTime();
@@ -12957,6 +13222,7 @@ function getCapableAgents(capabilities, opts, db) {
12957
13222
  }
12958
13223
  var init_agents = __esm(() => {
12959
13224
  init_database();
13225
+ init_agent_names();
12960
13226
  });
12961
13227
 
12962
13228
  // src/types/index.ts
@@ -25474,8 +25740,8 @@ function registerAgentTools(server, { shouldRegisterTool, resolveId, formatError
25474
25740
  });
25475
25741
  }
25476
25742
  if (shouldRegisterTool("register_agent")) {
25477
- 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.", {
25478
- name: exports_external.string().describe("Agent name \u2014 any name is allowed. Use suggest_agent_name to see pool suggestions and avoid conflicts."),
25743
+ server.tool("register_agent", "Register an agent with a distinctive one-word name. Generic/generated names like agent, agent-1, assistant, or worker-2 are rejected. Prefer Roman or Greek names from suggest_agent_name.", {
25744
+ name: exports_external.string().describe("Distinctive one-word agent name. Use suggest_agent_name first; avoid agent, agent-1, and other numbered generated names."),
25479
25745
  description: exports_external.string().optional(),
25480
25746
  role: exports_external.string().optional(),
25481
25747
  title: exports_external.string().optional(),
@@ -25519,7 +25785,7 @@ Last seen: ${agent.last_seen_at}`
25519
25785
  });
25520
25786
  }
25521
25787
  if (shouldRegisterTool("suggest_agent_name")) {
25522
- 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.", {
25788
+ server.tool("suggest_agent_name", "Get available Roman/Greek-style agent names for a project. Use these instead of generic generated names.", {
25523
25789
  working_dir: exports_external.string().optional().describe("Your working directory \u2014 used to look up the project's allowed name pool from config")
25524
25790
  }, async ({ working_dir }) => {
25525
25791
  try {
@@ -25527,8 +25793,30 @@ Last seen: ${agent.last_seen_at}`
25527
25793
  const cutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
25528
25794
  const allActive = listAgents().filter((a) => a.last_seen_at > cutoff);
25529
25795
  if (!pool) {
25796
+ const suggestions = getAvailableNamesFromPool([
25797
+ "caesar",
25798
+ "augustus",
25799
+ "marcus",
25800
+ "brutus",
25801
+ "cicero",
25802
+ "cato",
25803
+ "nero",
25804
+ "claudius",
25805
+ "tiberius",
25806
+ "hadrian",
25807
+ "athena",
25808
+ "apollo",
25809
+ "artemis",
25810
+ "iris",
25811
+ "hector",
25812
+ "sophia",
25813
+ "thalia",
25814
+ "phoebe",
25815
+ "daphne"
25816
+ ], getDatabase());
25530
25817
  const lines2 = [
25531
- "No agent pool configured \u2014 any name is allowed.",
25818
+ "No project pool configured. Use a distinctive one-word name; generic generated names are blocked.",
25819
+ `Suggested names: ${suggestions.slice(0, 8).join(", ")}`,
25532
25820
  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.",
25533
25821
  `
25534
25822
  To restrict names, configure agent_pool or project_pools in ~/.hasna/todos/config.json`
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMzE,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;CAC5D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,OAAO,GAAG,IAAI,CAmbjJ"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMzE,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;CAC5D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,OAAO,GAAG,IAAI,CAwbjJ"}