@hasna/todos 0.11.33 → 0.11.35

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.
Files changed (41) hide show
  1. package/README.md +12 -0
  2. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/api-key-commands.d.ts +3 -0
  4. package/dist/cli/commands/api-key-commands.d.ts.map +1 -0
  5. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  6. package/dist/cli/index.js +2343 -607
  7. package/dist/db/agent-names.d.ts +23 -0
  8. package/dist/db/agent-names.d.ts.map +1 -0
  9. package/dist/db/agents.d.ts +2 -0
  10. package/dist/db/agents.d.ts.map +1 -1
  11. package/dist/db/api-keys.d.ts +28 -0
  12. package/dist/db/api-keys.d.ts.map +1 -0
  13. package/dist/db/comments.d.ts +3 -0
  14. package/dist/db/comments.d.ts.map +1 -1
  15. package/dist/db/migrations.d.ts.map +1 -1
  16. package/dist/db/schema.d.ts.map +1 -1
  17. package/dist/db/task-crud.d.ts.map +1 -1
  18. package/dist/db/task-lifecycle.d.ts +1 -0
  19. package/dist/db/task-lifecycle.d.ts.map +1 -1
  20. package/dist/db/task-relations.d.ts +24 -0
  21. package/dist/db/task-relations.d.ts.map +1 -1
  22. package/dist/db/tasks.d.ts +1 -1
  23. package/dist/db/tasks.d.ts.map +1 -1
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +400 -13
  27. package/dist/mcp/index.d.ts.map +1 -1
  28. package/dist/mcp/index.js +2585 -536
  29. package/dist/mcp/tools/agents.d.ts.map +1 -1
  30. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  31. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  32. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  33. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  34. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  35. package/dist/mcp/tools/task-workflow-tools.d.ts.map +1 -1
  36. package/dist/server/index.js +352 -42
  37. package/dist/server/routes.d.ts.map +1 -1
  38. package/dist/server/serve.d.ts.map +1 -1
  39. package/dist/types/index.d.ts +21 -0
  40. package/dist/types/index.d.ts.map +1 -1
  41. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -808,6 +808,42 @@ var init_migrations = __esm(() => {
808
808
  ALTER TABLE tasks ADD COLUMN current_step TEXT;
809
809
  ALTER TABLE tasks ADD COLUMN total_steps INTEGER;
810
810
  INSERT OR IGNORE INTO _migrations (id) VALUES (48);
811
+ `,
812
+ `
813
+ CREATE TABLE IF NOT EXISTS cycles (
814
+ id TEXT PRIMARY KEY,
815
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
816
+ number INTEGER NOT NULL,
817
+ start_date TEXT NOT NULL,
818
+ end_date TEXT NOT NULL,
819
+ duration_weeks INTEGER NOT NULL DEFAULT 1,
820
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
821
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
822
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
823
+ );
824
+ CREATE INDEX IF NOT EXISTS idx_cycles_project ON cycles(project_id);
825
+ CREATE INDEX IF NOT EXISTS idx_cycles_number ON cycles(number);
826
+ CREATE INDEX IF NOT EXISTS idx_cycles_status ON cycles(status);
827
+ CREATE INDEX IF NOT EXISTS idx_cycles_dates ON cycles(start_date, end_date);
828
+ ALTER TABLE tasks ADD COLUMN cycle_id TEXT REFERENCES cycles(id) ON DELETE SET NULL;
829
+ CREATE INDEX IF NOT EXISTS idx_tasks_cycle ON tasks(cycle_id) WHERE cycle_id IS NOT NULL;
830
+ INSERT OR IGNORE INTO _migrations (id) VALUES (49);
831
+ `,
832
+ `
833
+ CREATE TABLE IF NOT EXISTS api_keys (
834
+ id TEXT PRIMARY KEY,
835
+ name TEXT NOT NULL,
836
+ key_hash TEXT NOT NULL UNIQUE,
837
+ prefix TEXT NOT NULL UNIQUE,
838
+ permissions TEXT NOT NULL DEFAULT '["*"]',
839
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
840
+ last_used_at TEXT,
841
+ expires_at TEXT,
842
+ revoked_at TEXT
843
+ );
844
+ CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix);
845
+ CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at);
846
+ INSERT OR IGNORE INTO _migrations (id) VALUES (50);
811
847
  `
812
848
  ];
813
849
  });
@@ -1206,6 +1242,20 @@ function ensureSchema(db) {
1206
1242
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_cycles_dates ON cycles(start_date, end_date)");
1207
1243
  ensureColumn("tasks", "cycle_id", "TEXT REFERENCES cycles(id) ON DELETE SET NULL");
1208
1244
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_cycle ON tasks(cycle_id) WHERE cycle_id IS NOT NULL");
1245
+ ensureTable("api_keys", `
1246
+ CREATE TABLE api_keys (
1247
+ id TEXT PRIMARY KEY,
1248
+ name TEXT NOT NULL,
1249
+ key_hash TEXT NOT NULL UNIQUE,
1250
+ prefix TEXT NOT NULL UNIQUE,
1251
+ permissions TEXT NOT NULL DEFAULT '["*"]',
1252
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1253
+ last_used_at TEXT,
1254
+ expires_at TEXT,
1255
+ revoked_at TEXT
1256
+ )`);
1257
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix)");
1258
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at)");
1209
1259
  }
1210
1260
  function backfillTaskTags(db) {
1211
1261
  try {
@@ -3043,8 +3093,8 @@ function createTask(input, db) {
3043
3093
  let id = uuid();
3044
3094
  for (let attempt = 0;attempt < 3; attempt++) {
3045
3095
  try {
3046
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
3047
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3096
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
3097
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3048
3098
  id,
3049
3099
  null,
3050
3100
  input.project_id || null,
@@ -3066,6 +3116,10 @@ function createTask(input, db) {
3066
3116
  timestamp,
3067
3117
  input.due_at || null,
3068
3118
  input.estimated_minutes || null,
3119
+ input.confidence ?? null,
3120
+ input.retry_count ?? 0,
3121
+ input.max_retries ?? 3,
3122
+ input.retry_after ?? null,
3069
3123
  input.requires_approval ? 1 : 0,
3070
3124
  null,
3071
3125
  null,
@@ -3310,8 +3364,10 @@ function updateTask(id, input, db) {
3310
3364
  if (task.version !== input.version) {
3311
3365
  throw new VersionConflictError(id, input.version, task.version);
3312
3366
  }
3367
+ const timestamp = now();
3368
+ const completionTimestamp = input.completed_at ?? timestamp;
3313
3369
  const sets = ["version = version + 1", "updated_at = ?"];
3314
- const params = [now()];
3370
+ const params = [timestamp];
3315
3371
  if (input.title !== undefined) {
3316
3372
  sets.push("title = ?");
3317
3373
  params.push(input.title);
@@ -3328,13 +3384,17 @@ function updateTask(id, input, db) {
3328
3384
  params.push(input.status);
3329
3385
  if (input.status === "completed") {
3330
3386
  sets.push("completed_at = ?");
3331
- params.push(now());
3387
+ params.push(completionTimestamp);
3332
3388
  }
3333
3389
  }
3334
3390
  if (input.priority !== undefined) {
3335
3391
  sets.push("priority = ?");
3336
3392
  params.push(input.priority);
3337
3393
  }
3394
+ if (input.project_id !== undefined) {
3395
+ sets.push("project_id = ?");
3396
+ params.push(input.project_id);
3397
+ }
3338
3398
  if (input.assigned_to !== undefined) {
3339
3399
  sets.push("assigned_to = ?");
3340
3400
  params.push(input.assigned_to);
@@ -3363,6 +3423,30 @@ function updateTask(id, input, db) {
3363
3423
  sets.push("estimated_minutes = ?");
3364
3424
  params.push(input.estimated_minutes);
3365
3425
  }
3426
+ if (input.actual_minutes !== undefined) {
3427
+ sets.push("actual_minutes = ?");
3428
+ params.push(input.actual_minutes);
3429
+ }
3430
+ if (input.completed_at !== undefined && input.status !== "completed") {
3431
+ sets.push("completed_at = ?");
3432
+ params.push(input.completed_at);
3433
+ }
3434
+ if (input.confidence !== undefined) {
3435
+ sets.push("confidence = ?");
3436
+ params.push(input.confidence);
3437
+ }
3438
+ if (input.retry_count !== undefined) {
3439
+ sets.push("retry_count = ?");
3440
+ params.push(input.retry_count);
3441
+ }
3442
+ if (input.max_retries !== undefined) {
3443
+ sets.push("max_retries = ?");
3444
+ params.push(input.max_retries);
3445
+ }
3446
+ if (input.retry_after !== undefined) {
3447
+ sets.push("retry_after = ?");
3448
+ params.push(input.retry_after);
3449
+ }
3366
3450
  if (input.requires_approval !== undefined) {
3367
3451
  sets.push("requires_approval = ?");
3368
3452
  params.push(input.requires_approval ? 1 : 0);
@@ -3413,11 +3497,16 @@ function updateTask(id, input, db) {
3413
3497
  tags: input.tags ?? task.tags,
3414
3498
  metadata: input.metadata ?? task.metadata,
3415
3499
  version: task.version + 1,
3416
- updated_at: now(),
3417
- completed_at: input.status === "completed" ? now() : task.completed_at,
3500
+ updated_at: timestamp,
3501
+ completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
3502
+ actual_minutes: input.actual_minutes ?? task.actual_minutes,
3503
+ confidence: input.confidence !== undefined ? input.confidence : task.confidence,
3504
+ retry_count: input.retry_count ?? task.retry_count,
3505
+ max_retries: input.max_retries ?? task.max_retries,
3506
+ retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
3418
3507
  requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
3419
3508
  approved_by: input.approved_by ?? task.approved_by,
3420
- approved_at: input.approved_by ? now() : task.approved_at
3509
+ approved_at: input.approved_by ? timestamp : task.approved_at
3421
3510
  };
3422
3511
  }
3423
3512
  function deleteTask(id, db) {
@@ -4145,7 +4234,7 @@ function completeTask(id, agentId, db, options) {
4145
4234
  completionMeta._completion = { confidence: options.confidence };
4146
4235
  }
4147
4236
  const hasMeta = Object.keys(completionMeta).length > 0;
4148
- const timestamp = now();
4237
+ const timestamp = options?.completed_at || now();
4149
4238
  const confidence = options?.confidence !== undefined ? options.confidence : null;
4150
4239
  const tx = d.transaction(() => {
4151
4240
  if (hasMeta) {
@@ -4977,6 +5066,214 @@ function deleteComment(id, db) {
4977
5066
  }
4978
5067
  // src/db/agents.ts
4979
5068
  init_database();
5069
+
5070
+ // src/db/agent-names.ts
5071
+ init_database();
5072
+
5073
+ class InvalidAgentNameError extends Error {
5074
+ suggestions;
5075
+ constructor(name, reason, suggestions = []) {
5076
+ super(`Invalid agent name "${name}": ${reason}${suggestions.length > 0 ? `. Try: ${suggestions.join(", ")}` : ""}`);
5077
+ this.name = "InvalidAgentNameError";
5078
+ this.suggestions = suggestions;
5079
+ }
5080
+ }
5081
+ var ROMAN_AGENT_NAMES = [
5082
+ "caesar",
5083
+ "augustus",
5084
+ "marcus",
5085
+ "brutus",
5086
+ "cicero",
5087
+ "cato",
5088
+ "nero",
5089
+ "claudius",
5090
+ "tiberius",
5091
+ "hadrian",
5092
+ "trajan",
5093
+ "vespasian",
5094
+ "domitian",
5095
+ "caligula",
5096
+ "commodus",
5097
+ "livia",
5098
+ "julia",
5099
+ "octavia",
5100
+ "claudia",
5101
+ "agrippina",
5102
+ "cornelia",
5103
+ "valeria",
5104
+ "fulvia",
5105
+ "hortensia",
5106
+ "fabia"
5107
+ ];
5108
+ var GREEK_AGENT_NAMES = [
5109
+ "athena",
5110
+ "apollo",
5111
+ "artemis",
5112
+ "hera",
5113
+ "iris",
5114
+ "hector",
5115
+ "achilles",
5116
+ "odysseus",
5117
+ "theseus",
5118
+ "pericles",
5119
+ "solon",
5120
+ "sophia",
5121
+ "thalia",
5122
+ "calliope",
5123
+ "clio",
5124
+ "phoebe",
5125
+ "daphne",
5126
+ "leonidas",
5127
+ "andromeda",
5128
+ "cassander"
5129
+ ];
5130
+ var NICE_AGENT_NAMES = [
5131
+ "atlas",
5132
+ "aurora",
5133
+ "ember",
5134
+ "nova",
5135
+ "orion",
5136
+ "rhea",
5137
+ "selene",
5138
+ "sirius",
5139
+ "vesper",
5140
+ "zephyr"
5141
+ ];
5142
+ var PREFERRED_AGENT_NAMES = [
5143
+ ...ROMAN_AGENT_NAMES,
5144
+ ...GREEK_AGENT_NAMES,
5145
+ ...NICE_AGENT_NAMES
5146
+ ];
5147
+ var RESERVED_GENERIC_NAMES = new Set([
5148
+ "agent",
5149
+ "agents",
5150
+ "ai",
5151
+ "assistant",
5152
+ "bot",
5153
+ "coder",
5154
+ "default",
5155
+ "helper",
5156
+ "model",
5157
+ "system",
5158
+ "user",
5159
+ "worker"
5160
+ ]);
5161
+ var NUMERIC_SUFFIX_RE = /[-_]\d+$/;
5162
+ var ONE_WORD_NAME_RE = /^[a-z]+$/;
5163
+ function normalizeAgentNameInput(name) {
5164
+ return name.trim().toLowerCase();
5165
+ }
5166
+ function hasGeneratedNumericSuffix(name) {
5167
+ return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
5168
+ }
5169
+ function isGenericAgentName(name) {
5170
+ const normalized = normalizeAgentNameInput(name);
5171
+ if (RESERVED_GENERIC_NAMES.has(normalized))
5172
+ return true;
5173
+ for (const generic of RESERVED_GENERIC_NAMES) {
5174
+ if (normalized === `${generic}s`)
5175
+ return true;
5176
+ if (normalized.match(new RegExp(`^${generic}\\d+$`)))
5177
+ return true;
5178
+ if (normalized.match(new RegExp(`^${generic}[-_]\\d+$`)))
5179
+ return true;
5180
+ }
5181
+ return false;
5182
+ }
5183
+ function isBlockedAgentName(name) {
5184
+ const normalized = normalizeAgentNameInput(name);
5185
+ return isGenericAgentName(normalized) || hasGeneratedNumericSuffix(normalized) || !ONE_WORD_NAME_RE.test(normalized);
5186
+ }
5187
+ function suggestAgentNames(existingNames = []) {
5188
+ const existing = new Set([...existingNames].map(normalizeAgentNameInput));
5189
+ return PREFERRED_AGENT_NAMES.filter((name) => !existing.has(name));
5190
+ }
5191
+ function validateAgentName(name, existingNames = []) {
5192
+ const normalized = normalizeAgentNameInput(name);
5193
+ const suggestions = suggestAgentNames(existingNames).slice(0, 5);
5194
+ if (!normalized) {
5195
+ throw new InvalidAgentNameError(name, "choose a real one-word name instead of an empty value", suggestions);
5196
+ }
5197
+ if (/\s/.test(normalized)) {
5198
+ throw new InvalidAgentNameError(name, "use a single word, preferably a Roman or Greek name", suggestions);
5199
+ }
5200
+ if (normalized.length < 3) {
5201
+ throw new InvalidAgentNameError(name, "use a more distinctive name with at least three characters", suggestions);
5202
+ }
5203
+ if (isGenericAgentName(normalized)) {
5204
+ throw new InvalidAgentNameError(name, "generic names like agent, agent-1, assistant, or worker-2 are reserved", suggestions);
5205
+ }
5206
+ if (hasGeneratedNumericSuffix(normalized)) {
5207
+ throw new InvalidAgentNameError(name, "numbered suffix names are not allowed; pick a distinct human-readable name", suggestions);
5208
+ }
5209
+ if (!ONE_WORD_NAME_RE.test(normalized)) {
5210
+ throw new InvalidAgentNameError(name, "use one word made of letters only, preferably a Roman or Greek name", suggestions);
5211
+ }
5212
+ return normalized;
5213
+ }
5214
+ function tableHasColumn(db, table, column) {
5215
+ try {
5216
+ return db.query(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
5217
+ } catch {
5218
+ return false;
5219
+ }
5220
+ }
5221
+ function updateReferences(db, oldName, newName) {
5222
+ const refs = [
5223
+ ["tasks", "assigned_to"],
5224
+ ["tasks", "agent_id"],
5225
+ ["tasks", "locked_by"],
5226
+ ["tasks", "assigned_by"],
5227
+ ["plans", "agent_id"],
5228
+ ["sessions", "agent_id"],
5229
+ ["task_comments", "agent_id"],
5230
+ ["task_history", "agent_id"],
5231
+ ["webhooks", "agent_id"],
5232
+ ["task_files", "agent_id"],
5233
+ ["task_time_logs", "agent_id"],
5234
+ ["task_watchers", "agent_id"],
5235
+ ["task_checkpoints", "agent_id"],
5236
+ ["task_heartbeats", "agent_id"],
5237
+ ["project_agent_roles", "agent_id"]
5238
+ ];
5239
+ let changed = 0;
5240
+ for (const [table, column] of refs) {
5241
+ if (!tableHasColumn(db, table, column))
5242
+ continue;
5243
+ try {
5244
+ changed += db.run(`UPDATE ${table} SET ${column} = ? WHERE LOWER(${column}) = ?`, [newName, oldName]).changes;
5245
+ } catch {}
5246
+ }
5247
+ return changed;
5248
+ }
5249
+ function normalizeGeneratedAgentNames(db) {
5250
+ const rows = db.query("SELECT * FROM agents ORDER BY created_at, id").all();
5251
+ const existing = new Set(rows.map((agent) => normalizeAgentNameInput(agent.name)));
5252
+ const renamed = [];
5253
+ for (const agent of rows) {
5254
+ const oldName = normalizeAgentNameInput(agent.name);
5255
+ if (!isBlockedAgentName(oldName))
5256
+ continue;
5257
+ const candidates = suggestAgentNames(existing);
5258
+ const replacement = candidates[0];
5259
+ if (!replacement) {
5260
+ throw new Error("No safe agent names are available for normalization");
5261
+ }
5262
+ existing.delete(oldName);
5263
+ existing.add(replacement);
5264
+ db.run("UPDATE agents SET name = ?, last_seen_at = ? WHERE id = ?", [replacement, now(), agent.id]);
5265
+ const referenceUpdates = updateReferences(db, oldName, replacement);
5266
+ renamed.push({
5267
+ id: agent.id,
5268
+ old_name: oldName,
5269
+ new_name: replacement,
5270
+ reference_updates: referenceUpdates
5271
+ });
5272
+ }
5273
+ return renamed;
5274
+ }
5275
+
5276
+ // src/db/agents.ts
4980
5277
  function getActiveWindowMs() {
4981
5278
  const env = process.env["TODOS_AGENT_TIMEOUT_MS"];
4982
5279
  if (env) {
@@ -4998,7 +5295,7 @@ function getAvailableNamesFromPool(pool, db) {
4998
5295
  autoReleaseStaleAgents(db);
4999
5296
  const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
5000
5297
  const activeNames = new Set(db.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
5001
- return pool.filter((name) => !activeNames.has(name.toLowerCase()));
5298
+ return pool.map(normalizeAgentNameInput).filter((name) => !activeNames.has(name));
5002
5299
  }
5003
5300
  function shortUuid() {
5004
5301
  return crypto.randomUUID().slice(0, 8);
@@ -5014,7 +5311,8 @@ function rowToAgent(row) {
5014
5311
  }
5015
5312
  function registerAgent(input, db) {
5016
5313
  const d = db || getDatabase();
5017
- const normalizedName = input.name.trim().toLowerCase();
5314
+ const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
5315
+ const normalizedName = validateAgentName(input.name, existingNames);
5018
5316
  const existing = getAgentByName(normalizedName, d);
5019
5317
  if (existing) {
5020
5318
  const lastSeenMs = new Date(existing.last_seen_at).getTime();
@@ -5147,7 +5445,8 @@ function updateAgent(id, input, db) {
5147
5445
  const sets = ["last_seen_at = ?"];
5148
5446
  const params = [now()];
5149
5447
  if (input.name !== undefined) {
5150
- const newName = input.name.trim().toLowerCase();
5448
+ const existingNames = d.query("SELECT name FROM agents WHERE id != ?").all(id).map((row) => row.name);
5449
+ const newName = validateAgentName(input.name, existingNames);
5151
5450
  const holder = getAgentByName(newName, d);
5152
5451
  if (holder && holder.id !== id) {
5153
5452
  const lastSeenMs = new Date(holder.last_seen_at).getTime();
@@ -5256,6 +5555,87 @@ function getCapableAgents(capabilities, opts, db) {
5256
5555
  })).filter((entry) => entry.score >= minScore).sort((a, b) => b.score - a.score);
5257
5556
  return opts?.limit ? scored.slice(0, opts.limit) : scored;
5258
5557
  }
5558
+ // src/db/api-keys.ts
5559
+ init_database();
5560
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
5561
+ function rowToRecord(row) {
5562
+ return {
5563
+ id: row.id,
5564
+ name: row.name,
5565
+ prefix: row.prefix,
5566
+ permissions: JSON.parse(row.permissions || '["*"]'),
5567
+ created_at: row.created_at,
5568
+ last_used_at: row.last_used_at,
5569
+ expires_at: row.expires_at,
5570
+ revoked_at: row.revoked_at
5571
+ };
5572
+ }
5573
+ function hashApiKey(key) {
5574
+ return createHash("sha256").update(key).digest("hex");
5575
+ }
5576
+ function safeEqualHex(a, b) {
5577
+ if (a.length !== b.length)
5578
+ return false;
5579
+ return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
5580
+ }
5581
+ function generatePlaintextKey() {
5582
+ return `tdos_${randomBytes(32).toString("base64url")}`;
5583
+ }
5584
+ function createApiKey(input, db) {
5585
+ const d = db || getDatabase();
5586
+ const name = input.name.trim();
5587
+ if (!name)
5588
+ throw new Error("API key name is required");
5589
+ const key = generatePlaintextKey();
5590
+ const timestamp = now();
5591
+ const id = uuid();
5592
+ const prefix = key.slice(0, 12);
5593
+ d.run(`INSERT INTO api_keys (id, name, key_hash, prefix, permissions, created_at, expires_at)
5594
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
5595
+ id,
5596
+ name,
5597
+ hashApiKey(key),
5598
+ prefix,
5599
+ JSON.stringify(input.permissions?.length ? input.permissions : ["*"]),
5600
+ timestamp,
5601
+ input.expires_at || null
5602
+ ]);
5603
+ const row = d.query("SELECT * FROM api_keys WHERE id = ?").get(id);
5604
+ return { key, record: rowToRecord(row) };
5605
+ }
5606
+ function listApiKeys(opts, db) {
5607
+ const d = db || getDatabase();
5608
+ const includeRevoked = opts?.include_revoked ?? false;
5609
+ const sql = includeRevoked ? "SELECT * FROM api_keys ORDER BY created_at DESC" : "SELECT * FROM api_keys WHERE revoked_at IS NULL ORDER BY created_at DESC";
5610
+ return d.query(sql).all().map(rowToRecord);
5611
+ }
5612
+ function hasActiveApiKeys(db) {
5613
+ const d = db || getDatabase();
5614
+ const row = d.query("SELECT COUNT(*) AS count FROM api_keys WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)").get(now());
5615
+ return (row?.count ?? 0) > 0;
5616
+ }
5617
+ function verifyApiKey(key, db) {
5618
+ const d = db || getDatabase();
5619
+ const candidateHash = hashApiKey(key);
5620
+ const rows = d.query("SELECT * FROM api_keys WHERE revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)").all(now());
5621
+ for (const row of rows) {
5622
+ if (!safeEqualHex(candidateHash, row.key_hash))
5623
+ continue;
5624
+ d.run("UPDATE api_keys SET last_used_at = ? WHERE id = ?", [now(), row.id]);
5625
+ return rowToRecord({ ...row, last_used_at: now() });
5626
+ }
5627
+ return null;
5628
+ }
5629
+ function revokeApiKey(idOrPrefix, db) {
5630
+ const d = db || getDatabase();
5631
+ const identifier = idOrPrefix.trim();
5632
+ const row = d.query("SELECT * FROM api_keys WHERE id = ? OR prefix = ?").get(identifier, identifier);
5633
+ if (!row)
5634
+ return null;
5635
+ d.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [now(), row.id]);
5636
+ const updated = d.query("SELECT * FROM api_keys WHERE id = ?").get(row.id);
5637
+ return rowToRecord(updated);
5638
+ }
5259
5639
  // src/db/task-lists.ts
5260
5640
  init_types();
5261
5641
  init_database();
@@ -8176,7 +8556,7 @@ var require_utils_webcrypto = __commonJS((exports, module) => {
8176
8556
  var nodeCrypto = __require("crypto");
8177
8557
  module.exports = {
8178
8558
  postgresMd5PasswordHash,
8179
- randomBytes,
8559
+ randomBytes: randomBytes2,
8180
8560
  deriveKey,
8181
8561
  sha256,
8182
8562
  hashByName,
@@ -8186,7 +8566,7 @@ var require_utils_webcrypto = __commonJS((exports, module) => {
8186
8566
  var webCrypto = nodeCrypto.webcrypto || globalThis.crypto;
8187
8567
  var subtleCrypto = webCrypto.subtle;
8188
8568
  var textEncoder = new TextEncoder;
8189
- function randomBytes(length) {
8569
+ function randomBytes2(length) {
8190
8570
  return webCrypto.getRandomValues(Buffer.alloc(length));
8191
8571
  }
8192
8572
  async function md5(string) {
@@ -17985,6 +18365,7 @@ async function dispatchToMultiple(input, opts = {}, db) {
17985
18365
  return dispatches;
17986
18366
  }
17987
18367
  export {
18368
+ verifyApiKey,
17988
18369
  validateTmuxTarget,
17989
18370
  uuid,
17990
18371
  updateTemplate,
@@ -18009,6 +18390,7 @@ export {
18009
18390
  syncWithAgents,
18010
18391
  syncWithAgent,
18011
18392
  syncKgEdges,
18393
+ suggestAgentNames,
18012
18394
  stealTask,
18013
18395
  startTask,
18014
18396
  slugify,
@@ -18022,6 +18404,7 @@ export {
18022
18404
  scoreTask,
18023
18405
  saveSnapshot,
18024
18406
  runDueDispatches,
18407
+ revokeApiKey,
18025
18408
  resolveVariables,
18026
18409
  resolvePartialId,
18027
18410
  resetMachineId,
@@ -18045,6 +18428,7 @@ export {
18045
18428
  parseRecurrenceRule,
18046
18429
  parseGitHubUrl,
18047
18430
  now,
18431
+ normalizeGeneratedAgentNames,
18048
18432
  nextTaskShortId,
18049
18433
  nextOccurrence,
18050
18434
  moveTask,
@@ -18076,12 +18460,14 @@ export {
18076
18460
  listCyclesWithStats,
18077
18461
  listCycles,
18078
18462
  listComments,
18463
+ listApiKeys,
18079
18464
  listAgents,
18080
18465
  issueToTask,
18081
18466
  isValidRecurrenceRule,
18082
18467
  isAgentConflict,
18083
18468
  initBuiltinTemplates,
18084
18469
  importTemplate,
18470
+ hasActiveApiKeys,
18085
18471
  getWebhook,
18086
18472
  getTraceStats,
18087
18473
  getTemplateWithTasks,
@@ -18196,6 +18582,7 @@ export {
18196
18582
  createDispatch,
18197
18583
  createCycle,
18198
18584
  createClient,
18585
+ createApiKey,
18199
18586
  countTasks,
18200
18587
  completeTask,
18201
18588
  closeDatabase,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";AAgGA,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAO9E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":";AAuGA,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAO9E"}