@hasna/todos 0.11.41 → 0.11.42

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/index.js CHANGED
@@ -979,6 +979,19 @@ var init_migrations = __esm(() => {
979
979
  );
980
980
  CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at);
981
981
  INSERT OR IGNORE INTO _migrations (id) VALUES (54);
982
+ `,
983
+ `
984
+ CREATE TABLE IF NOT EXISTS saved_search_views (
985
+ id TEXT PRIMARY KEY,
986
+ name TEXT NOT NULL UNIQUE,
987
+ description TEXT,
988
+ scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
989
+ filters TEXT NOT NULL DEFAULT '{}',
990
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
991
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
992
+ );
993
+ CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope);
994
+ INSERT OR IGNORE INTO _migrations (id) VALUES (55);
982
995
  `
983
996
  ];
984
997
  });
@@ -1155,6 +1168,17 @@ function ensureSchema(db) {
1155
1168
  PRIMARY KEY (handoff_id, agent_id)
1156
1169
  )`);
1157
1170
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at)");
1171
+ ensureTable("saved_search_views", `
1172
+ CREATE TABLE saved_search_views (
1173
+ id TEXT PRIMARY KEY,
1174
+ name TEXT NOT NULL UNIQUE,
1175
+ description TEXT,
1176
+ scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
1177
+ filters TEXT NOT NULL DEFAULT '{}',
1178
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1179
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1180
+ )`);
1181
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope)");
1158
1182
  ensureTable("task_relationships", `
1159
1183
  CREATE TABLE task_relationships (
1160
1184
  id TEXT PRIMARY KEY,
@@ -2713,6 +2737,39 @@ var TODOS_JSON_CONTRACTS = [
2713
2737
  created: field("object", "Flags and source types created by this bootstrap run.")
2714
2738
  },
2715
2739
  optional: {}
2740
+ }),
2741
+ contract({
2742
+ id: "saved_search_view",
2743
+ name: "Saved Search View",
2744
+ description: "Local saved search view for repeatable task, project, plan, run, comment, or cross-entity searches.",
2745
+ surfaces: ["cli", "mcp", "sdk"],
2746
+ stability: "stable",
2747
+ required: {
2748
+ id: idField,
2749
+ name: field("string", "Human-readable unique view name."),
2750
+ description: field(["string", "null"], "Optional view description.", true),
2751
+ scope: field("string", "Search scope: all, tasks, projects, plans, runs, or comments."),
2752
+ filters: field("object", "Saved local filter object."),
2753
+ created_at: isoDateField,
2754
+ updated_at: isoDateField
2755
+ },
2756
+ optional: {}
2757
+ }),
2758
+ contract({
2759
+ id: "saved_search_run_result",
2760
+ name: "Saved Search Run Result",
2761
+ description: "Stable JSON envelope returned when running a saved search view or cross-entity search.",
2762
+ surfaces: ["cli", "mcp", "sdk"],
2763
+ stability: "stable",
2764
+ required: {
2765
+ scope: field("string", "Search scope used for the run."),
2766
+ filters: field("object", "Applied local filters."),
2767
+ count: field("integer", "Number of returned result records."),
2768
+ results: field("array", "Result records with entity_type and entity.")
2769
+ },
2770
+ optional: {
2771
+ view: field("object", "Saved view metadata when the run came from a named view.")
2772
+ }
2716
2773
  })
2717
2774
  ];
2718
2775
  function expectedTypes(contract2) {
@@ -6247,7 +6304,8 @@ var dataKeys = [
6247
6304
  "task_files",
6248
6305
  "task_commits",
6249
6306
  "task_git_refs",
6250
- "task_verifications"
6307
+ "task_verifications",
6308
+ "saved_views"
6251
6309
  ];
6252
6310
  var insertColumns = {
6253
6311
  projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
@@ -6319,7 +6377,8 @@ var insertColumns = {
6319
6377
  task_files: ["id", "task_id", "path", "status", "agent_id", "note", "created_at", "updated_at", "machine_id"],
6320
6378
  task_commits: ["id", "task_id", "sha", "message", "author", "files_changed", "committed_at", "created_at"],
6321
6379
  task_git_refs: ["id", "task_id", "ref_type", "name", "url", "provider", "metadata", "created_at", "updated_at"],
6322
- task_verifications: ["id", "task_id", "command", "status", "output_summary", "artifact_path", "agent_id", "run_at", "created_at"]
6380
+ task_verifications: ["id", "task_id", "command", "status", "output_summary", "artifact_path", "agent_id", "run_at", "created_at"],
6381
+ saved_views: ["id", "name", "description", "scope", "filters", "created_at", "updated_at"]
6323
6382
  };
6324
6383
  var tableByKey = {
6325
6384
  projects: "projects",
@@ -6335,9 +6394,10 @@ var tableByKey = {
6335
6394
  task_files: "task_files",
6336
6395
  task_commits: "task_commits",
6337
6396
  task_git_refs: "task_git_refs",
6338
- task_verifications: "task_verifications"
6397
+ task_verifications: "task_verifications",
6398
+ saved_views: "saved_search_views"
6339
6399
  };
6340
- var jsonColumns = new Set(["metadata", "tags", "data", "files_changed"]);
6400
+ var jsonColumns = new Set(["metadata", "tags", "data", "files_changed", "filters"]);
6341
6401
  function packageSource(version) {
6342
6402
  return {
6343
6403
  packageName: "@hasna/todos",
@@ -6410,6 +6470,9 @@ function rowToRunEvent(row) {
6410
6470
  function rowToCommit(row) {
6411
6471
  return { ...row, files_changed: row.files_changed ? parseJsonArray(row.files_changed) : null };
6412
6472
  }
6473
+ function rowToSavedView(row) {
6474
+ return { ...row, filters: parseJsonObject(row.filters) };
6475
+ }
6413
6476
  function bridgeStats(data) {
6414
6477
  return Object.fromEntries(dataKeys.map((key) => [key, data[key].length]));
6415
6478
  }
@@ -6433,7 +6496,8 @@ function createLocalBridgeBundle(options = {}, db) {
6433
6496
  task_files: queryByTaskIds(d, "SELECT * FROM task_files WHERE task_id IN (__TASK_IDS__) ORDER BY path, id", taskIds),
6434
6497
  task_commits: queryByTaskIds(d, "SELECT * FROM task_commits WHERE task_id IN (__TASK_IDS__) ORDER BY created_at, id", taskIds).map(rowToCommit),
6435
6498
  task_git_refs: queryByTaskIds(d, "SELECT * FROM task_git_refs WHERE task_id IN (__TASK_IDS__) ORDER BY created_at, id", taskIds).map(rowWithMetadata),
6436
- task_verifications: queryByTaskIds(d, "SELECT * FROM task_verifications WHERE task_id IN (__TASK_IDS__) ORDER BY run_at, id", taskIds)
6499
+ task_verifications: queryByTaskIds(d, "SELECT * FROM task_verifications WHERE task_id IN (__TASK_IDS__) ORDER BY run_at, id", taskIds),
6500
+ saved_views: (options.project_id ? d.query("SELECT * FROM saved_search_views WHERE json_extract(filters, '$.project_id') = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM saved_search_views ORDER BY name").all()).map(rowToSavedView)
6437
6501
  };
6438
6502
  const artifactContents = data.run_artifacts.map((artifact) => exportStoredArtifactContent({
6439
6503
  id: artifact.id,
@@ -6472,6 +6536,8 @@ function validateLocalBridgeBundle(value) {
6472
6536
  issues.push("data must be an object");
6473
6537
  } else {
6474
6538
  for (const key of dataKeys) {
6539
+ if (key === "saved_views" && data[key] === undefined)
6540
+ continue;
6475
6541
  if (!Array.isArray(data[key]))
6476
6542
  issues.push(`data.${key} must be an array`);
6477
6543
  }
@@ -6644,7 +6710,8 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
6644
6710
  const conflictStrategy = options.conflictStrategy ?? "skip";
6645
6711
  const data = {
6646
6712
  ...bundle.data,
6647
- tasks: sortedTasks(bundle.data.tasks)
6713
+ tasks: sortedTasks(bundle.data.tasks),
6714
+ saved_views: bundle.data.saved_views ?? []
6648
6715
  };
6649
6716
  for (const key of dataKeys) {
6650
6717
  for (const row of data[key]) {
@@ -7255,6 +7322,10 @@ var MCP_TOOL_GROUPS = {
7255
7322
  "request_task_review",
7256
7323
  "reschedule_task",
7257
7324
  "search_tasks",
7325
+ "save_search_view",
7326
+ "list_search_views",
7327
+ "run_search_view",
7328
+ "delete_search_view",
7258
7329
  "standup",
7259
7330
  "set_task_contract",
7260
7331
  "task_context",
@@ -8214,6 +8285,10 @@ var TODOS_CLI_MCP_PARITY = [
8214
8285
  domain: "search",
8215
8286
  cliCommands: [
8216
8287
  "todos search",
8288
+ "todos views save",
8289
+ "todos views list",
8290
+ "todos views run",
8291
+ "todos views delete",
8217
8292
  "todos status",
8218
8293
  "todos recap",
8219
8294
  "todos standup",
@@ -8225,6 +8300,10 @@ var TODOS_CLI_MCP_PARITY = [
8225
8300
  ],
8226
8301
  mcpTools: [
8227
8302
  "search_tasks",
8303
+ "save_search_view",
8304
+ "list_search_views",
8305
+ "run_search_view",
8306
+ "delete_search_view",
8228
8307
  "get_status",
8229
8308
  "standup",
8230
8309
  "get_task_stats",
@@ -8233,7 +8312,7 @@ var TODOS_CLI_MCP_PARITY = [
8233
8312
  "get_task_graph",
8234
8313
  "get_recent_activity"
8235
8314
  ],
8236
- jsonContracts: ["task", "status_summary", "audit_history", "structured_error", "api_error"],
8315
+ jsonContracts: ["task", "saved_search_view", "saved_search_run_result", "status_summary", "audit_history", "structured_error", "api_error"],
8237
8316
  errorContracts: ["structured_error", "api_error"],
8238
8317
  status: "matched",
8239
8318
  example: {
@@ -11359,6 +11438,426 @@ function searchTasks(options, projectId, taskListId, db) {
11359
11438
  const rows = d.query(sql).all(...params);
11360
11439
  return rows.map(rowToTask4);
11361
11440
  }
11441
+ // src/lib/saved-search-views.ts
11442
+ init_database();
11443
+
11444
+ // src/lib/local-fields.ts
11445
+ init_database();
11446
+ init_types();
11447
+ var LOCAL_FIELDS_KEY = "local_fields";
11448
+ function normalizeList(values) {
11449
+ return [...new Set((values || []).map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
11450
+ }
11451
+ function metadataFields(task) {
11452
+ const value = task.metadata[LOCAL_FIELDS_KEY];
11453
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11454
+ }
11455
+ function sameCustomValue(actual, expected) {
11456
+ return JSON.stringify(actual) === JSON.stringify(expected);
11457
+ }
11458
+ function hasOwnField(fields, key) {
11459
+ return Object.prototype.hasOwnProperty.call(fields, key);
11460
+ }
11461
+ function getTaskLocalFields(taskId, db) {
11462
+ const d = db || getDatabase();
11463
+ const task = getTask(taskId, d);
11464
+ if (!task)
11465
+ throw new TaskNotFoundError(taskId);
11466
+ const fields = metadataFields(task);
11467
+ return {
11468
+ labels: normalizeList(fields.labels),
11469
+ priority: task.priority,
11470
+ severity: typeof fields.severity === "string" ? fields.severity : null,
11471
+ owner: hasOwnField(fields, "owner") ? typeof fields.owner === "string" ? fields.owner : null : task.assigned_to,
11472
+ area: typeof fields.area === "string" ? fields.area : null,
11473
+ custom: fields.custom && typeof fields.custom === "object" && !Array.isArray(fields.custom) ? fields.custom : {}
11474
+ };
11475
+ }
11476
+ function setTaskLocalFields(taskId, input, db) {
11477
+ const d = db || getDatabase();
11478
+ const task = getTask(taskId, d);
11479
+ if (!task)
11480
+ throw new TaskNotFoundError(taskId);
11481
+ const currentFields = getTaskLocalFields(taskId, d);
11482
+ const labels = input.labels !== undefined ? normalizeList(input.labels) : currentFields.labels;
11483
+ const custom = input.custom !== undefined ? redactValue(input.merge_custom === false ? input.custom : { ...currentFields.custom, ...input.custom }) : currentFields.custom;
11484
+ const nextFields = {
11485
+ labels,
11486
+ priority: input.priority || task.priority,
11487
+ severity: input.severity !== undefined ? input.severity : currentFields.severity,
11488
+ owner: input.owner !== undefined ? input.owner : currentFields.owner,
11489
+ area: input.area !== undefined ? input.area : currentFields.area,
11490
+ custom
11491
+ };
11492
+ const nextMetadata = {
11493
+ ...task.metadata,
11494
+ [LOCAL_FIELDS_KEY]: nextFields
11495
+ };
11496
+ const previousLabels = new Set(currentFields.labels);
11497
+ const nextTags = normalizeList([...task.tags.filter((tag) => !previousLabels.has(tag)), ...labels]);
11498
+ const updates = {
11499
+ version: task.version,
11500
+ priority: input.priority,
11501
+ tags: nextTags,
11502
+ metadata: nextMetadata
11503
+ };
11504
+ if (input.owner !== undefined)
11505
+ updates.assigned_to = nextFields.owner;
11506
+ return updateTask(taskId, updates, d);
11507
+ }
11508
+ function queryTasksByLocalFields(query, db) {
11509
+ const d = db || getDatabase();
11510
+ const tasks = listTasks({
11511
+ priority: query.priority,
11512
+ tags: query.labels,
11513
+ limit: 1e4
11514
+ }, d);
11515
+ const matches = tasks.filter((task) => {
11516
+ const fields = getTaskLocalFields(task.id, d);
11517
+ if (query.labels && !query.labels.every((label) => fields.labels.includes(label)))
11518
+ return false;
11519
+ if (query.severity && fields.severity !== query.severity)
11520
+ return false;
11521
+ if (query.owner && fields.owner !== query.owner)
11522
+ return false;
11523
+ if (query.area && fields.area !== query.area)
11524
+ return false;
11525
+ if (query.custom) {
11526
+ for (const [key, expected] of Object.entries(query.custom)) {
11527
+ if (!sameCustomValue(fields.custom[key], expected))
11528
+ return false;
11529
+ }
11530
+ }
11531
+ return true;
11532
+ });
11533
+ return matches.slice(0, query.limit || 100);
11534
+ }
11535
+
11536
+ // src/lib/saved-search-views.ts
11537
+ function parseFilters(value) {
11538
+ if (!value)
11539
+ return {};
11540
+ try {
11541
+ const parsed = JSON.parse(value);
11542
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
11543
+ } catch {
11544
+ return {};
11545
+ }
11546
+ }
11547
+ function rowToSavedSearchView(row) {
11548
+ return {
11549
+ ...row,
11550
+ scope: normalizeScope(row.scope),
11551
+ filters: parseFilters(row.filters)
11552
+ };
11553
+ }
11554
+ function normalizeScope(scope) {
11555
+ if (scope === "all" || scope === "tasks" || scope === "projects" || scope === "plans" || scope === "runs" || scope === "comments") {
11556
+ return scope;
11557
+ }
11558
+ return "tasks";
11559
+ }
11560
+ function normalizeName(name) {
11561
+ const normalized = name.trim();
11562
+ if (!normalized)
11563
+ throw new Error("Saved view name is required");
11564
+ return normalized;
11565
+ }
11566
+ function normalizeLimit(limit) {
11567
+ if (!Number.isFinite(limit) || !limit || limit <= 0)
11568
+ return 100;
11569
+ return Math.min(Math.trunc(limit), 1000);
11570
+ }
11571
+ function valuesList(value) {
11572
+ if (value === undefined)
11573
+ return [];
11574
+ return (Array.isArray(value) ? value : [value]).map((item) => item.trim()).filter(Boolean);
11575
+ }
11576
+ function addStatusFilter(sql, params, column, value) {
11577
+ const values = valuesList(value);
11578
+ if (values.length === 0)
11579
+ return sql;
11580
+ params.push(...values);
11581
+ return `${sql} AND ${column} IN (${values.map(() => "?").join(",")})`;
11582
+ }
11583
+ function addDateFilter(sql, params, column, value) {
11584
+ if (!value)
11585
+ return sql;
11586
+ params.push(value);
11587
+ return `${sql} AND ${column} > ?`;
11588
+ }
11589
+ function likePattern(query) {
11590
+ const trimmed = query?.trim();
11591
+ if (!trimmed || trimmed === "*")
11592
+ return null;
11593
+ return `%${trimmed}%`;
11594
+ }
11595
+ function parseJsonObject2(value) {
11596
+ if (!value)
11597
+ return {};
11598
+ if (typeof value === "object" && !Array.isArray(value))
11599
+ return value;
11600
+ if (typeof value !== "string")
11601
+ return {};
11602
+ try {
11603
+ const parsed = JSON.parse(value);
11604
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
11605
+ } catch {
11606
+ return {};
11607
+ }
11608
+ }
11609
+ function rowToTaskRun(row) {
11610
+ return { ...row, metadata: parseJsonObject2(row.metadata) };
11611
+ }
11612
+ function taskMatchesSavedFilters(task, filters, db) {
11613
+ if (filters.plan_id && task.plan_id !== filters.plan_id)
11614
+ return false;
11615
+ if (filters.tags && !filters.tags.every((tag) => task.tags.includes(tag)))
11616
+ return false;
11617
+ if (filters.depends_on) {
11618
+ const row = db.query("SELECT 1 FROM task_dependencies WHERE task_id = ? AND depends_on = ?").get(task.id, filters.depends_on);
11619
+ if (!row)
11620
+ return false;
11621
+ }
11622
+ if (filters.blocks) {
11623
+ const row = db.query("SELECT 1 FROM task_dependencies WHERE task_id = ? AND depends_on = ?").get(filters.blocks, task.id);
11624
+ if (!row)
11625
+ return false;
11626
+ }
11627
+ return true;
11628
+ }
11629
+ function searchTaskEntities(filters, db) {
11630
+ let tasks;
11631
+ if (filters.local_fields) {
11632
+ const localMatches = queryTasksByLocalFields({ ...filters.local_fields, limit: 1e4 }, db);
11633
+ const allowedIds = new Set(localMatches.map((task) => task.id));
11634
+ tasks = searchTasks({
11635
+ query: filters.query,
11636
+ project_id: filters.project_id,
11637
+ task_list_id: filters.task_list_id,
11638
+ status: filters.status,
11639
+ priority: filters.priority,
11640
+ assigned_to: filters.assigned_to,
11641
+ agent_id: filters.agent_id,
11642
+ created_after: filters.created_after,
11643
+ updated_after: filters.updated_after,
11644
+ has_dependencies: filters.has_dependencies,
11645
+ is_blocked: filters.is_blocked
11646
+ }, undefined, undefined, db).filter((task) => allowedIds.has(task.id));
11647
+ } else {
11648
+ tasks = searchTasks({
11649
+ query: filters.query,
11650
+ project_id: filters.project_id,
11651
+ task_list_id: filters.task_list_id,
11652
+ status: filters.status,
11653
+ priority: filters.priority,
11654
+ assigned_to: filters.assigned_to,
11655
+ agent_id: filters.agent_id,
11656
+ created_after: filters.created_after,
11657
+ updated_after: filters.updated_after,
11658
+ has_dependencies: filters.has_dependencies,
11659
+ is_blocked: filters.is_blocked
11660
+ }, undefined, undefined, db);
11661
+ }
11662
+ return tasks.filter((task) => taskMatchesSavedFilters(task, filters, db)).slice(0, normalizeLimit(filters.limit));
11663
+ }
11664
+ function searchProjects(filters, db) {
11665
+ const params = [];
11666
+ let sql = "SELECT * FROM projects WHERE 1=1";
11667
+ if (filters.project_id) {
11668
+ sql += " AND id = ?";
11669
+ params.push(filters.project_id);
11670
+ }
11671
+ const pattern = likePattern(filters.query);
11672
+ if (pattern) {
11673
+ sql += " AND (name LIKE ? OR description LIKE ? OR path LIKE ?)";
11674
+ params.push(pattern, pattern, pattern);
11675
+ }
11676
+ sql = addDateFilter(sql, params, "created_at", filters.created_after);
11677
+ sql = addDateFilter(sql, params, "updated_at", filters.updated_after);
11678
+ sql += " ORDER BY name LIMIT ?";
11679
+ params.push(normalizeLimit(filters.limit));
11680
+ return db.query(sql).all(...params);
11681
+ }
11682
+ function searchPlans(filters, db) {
11683
+ const params = [];
11684
+ let sql = "SELECT * FROM plans WHERE 1=1";
11685
+ if (filters.project_id) {
11686
+ sql += " AND project_id = ?";
11687
+ params.push(filters.project_id);
11688
+ }
11689
+ if (filters.task_list_id) {
11690
+ sql += " AND task_list_id = ?";
11691
+ params.push(filters.task_list_id);
11692
+ }
11693
+ if (filters.agent_id) {
11694
+ sql += " AND agent_id = ?";
11695
+ params.push(filters.agent_id);
11696
+ }
11697
+ sql = addStatusFilter(sql, params, "status", filters.status);
11698
+ const pattern = likePattern(filters.query);
11699
+ if (pattern) {
11700
+ sql += " AND (name LIKE ? OR description LIKE ?)";
11701
+ params.push(pattern, pattern);
11702
+ }
11703
+ sql = addDateFilter(sql, params, "created_at", filters.created_after);
11704
+ sql = addDateFilter(sql, params, "updated_at", filters.updated_after);
11705
+ sql += " ORDER BY updated_at DESC, created_at DESC LIMIT ?";
11706
+ params.push(normalizeLimit(filters.limit));
11707
+ return db.query(sql).all(...params);
11708
+ }
11709
+ function searchRuns(filters, db) {
11710
+ const params = [];
11711
+ let sql = `SELECT r.* FROM task_runs r
11712
+ JOIN tasks t ON t.id = r.task_id
11713
+ WHERE 1=1`;
11714
+ if (filters.project_id) {
11715
+ sql += " AND t.project_id = ?";
11716
+ params.push(filters.project_id);
11717
+ }
11718
+ if (filters.task_list_id) {
11719
+ sql += " AND t.task_list_id = ?";
11720
+ params.push(filters.task_list_id);
11721
+ }
11722
+ if (filters.plan_id) {
11723
+ sql += " AND t.plan_id = ?";
11724
+ params.push(filters.plan_id);
11725
+ }
11726
+ if (filters.task_id) {
11727
+ sql += " AND r.task_id = ?";
11728
+ params.push(filters.task_id);
11729
+ }
11730
+ if (filters.agent_id) {
11731
+ sql += " AND r.agent_id = ?";
11732
+ params.push(filters.agent_id);
11733
+ }
11734
+ sql = addStatusFilter(sql, params, "r.status", filters.status);
11735
+ const pattern = likePattern(filters.query);
11736
+ if (pattern) {
11737
+ sql += " AND (r.title LIKE ? OR r.summary LIKE ? OR t.title LIKE ?)";
11738
+ params.push(pattern, pattern, pattern);
11739
+ }
11740
+ sql = addDateFilter(sql, params, "r.created_at", filters.created_after);
11741
+ sql = addDateFilter(sql, params, "r.updated_at", filters.updated_after);
11742
+ sql += " ORDER BY r.started_at DESC, r.created_at DESC LIMIT ?";
11743
+ params.push(normalizeLimit(filters.limit));
11744
+ return db.query(sql).all(...params).map(rowToTaskRun);
11745
+ }
11746
+ function searchComments(filters, db) {
11747
+ const params = [];
11748
+ let sql = `SELECT c.* FROM task_comments c
11749
+ JOIN tasks t ON t.id = c.task_id
11750
+ WHERE 1=1`;
11751
+ if (filters.project_id) {
11752
+ sql += " AND t.project_id = ?";
11753
+ params.push(filters.project_id);
11754
+ }
11755
+ if (filters.task_list_id) {
11756
+ sql += " AND t.task_list_id = ?";
11757
+ params.push(filters.task_list_id);
11758
+ }
11759
+ if (filters.plan_id) {
11760
+ sql += " AND t.plan_id = ?";
11761
+ params.push(filters.plan_id);
11762
+ }
11763
+ if (filters.task_id) {
11764
+ sql += " AND c.task_id = ?";
11765
+ params.push(filters.task_id);
11766
+ }
11767
+ if (filters.agent_id) {
11768
+ sql += " AND c.agent_id = ?";
11769
+ params.push(filters.agent_id);
11770
+ }
11771
+ const pattern = likePattern(filters.query);
11772
+ if (pattern) {
11773
+ sql += " AND (c.content LIKE ? OR t.title LIKE ?)";
11774
+ params.push(pattern, pattern);
11775
+ }
11776
+ sql = addDateFilter(sql, params, "c.created_at", filters.created_after);
11777
+ sql += " ORDER BY c.created_at DESC, c.id LIMIT ?";
11778
+ params.push(normalizeLimit(filters.limit));
11779
+ return db.query(sql).all(...params);
11780
+ }
11781
+ function toResults(entityType, rows) {
11782
+ return rows.map((entity) => ({ entity_type: entityType, entity }));
11783
+ }
11784
+ function runSavedSearch(filters = {}, scope = "tasks", db) {
11785
+ const d = db || getDatabase();
11786
+ const normalizedScope = normalizeScope(scope);
11787
+ const scopes = normalizedScope === "all" ? ["tasks", "projects", "plans", "runs", "comments"] : [normalizedScope];
11788
+ const results = [];
11789
+ for (const item of scopes) {
11790
+ if (item === "tasks")
11791
+ results.push(...toResults("tasks", searchTaskEntities(filters, d)));
11792
+ if (item === "projects")
11793
+ results.push(...toResults("projects", searchProjects(filters, d)));
11794
+ if (item === "plans")
11795
+ results.push(...toResults("plans", searchPlans(filters, d)));
11796
+ if (item === "runs")
11797
+ results.push(...toResults("runs", searchRuns(filters, d)));
11798
+ if (item === "comments")
11799
+ results.push(...toResults("comments", searchComments(filters, d)));
11800
+ }
11801
+ return {
11802
+ scope: normalizedScope,
11803
+ filters,
11804
+ count: results.length,
11805
+ results: results.slice(0, normalizeLimit(filters.limit))
11806
+ };
11807
+ }
11808
+ function saveSearchView(input, db) {
11809
+ const d = db || getDatabase();
11810
+ const name = normalizeName(input.name);
11811
+ const timestamp = now();
11812
+ const existing = getSearchView(name, d);
11813
+ if (existing) {
11814
+ d.run(`UPDATE saved_search_views
11815
+ SET description = ?, scope = ?, filters = ?, updated_at = ?
11816
+ WHERE id = ?`, [
11817
+ input.description ?? existing.description,
11818
+ normalizeScope(input.scope ?? existing.scope),
11819
+ JSON.stringify(input.filters ?? existing.filters),
11820
+ timestamp,
11821
+ existing.id
11822
+ ]);
11823
+ return getSearchView(existing.id, d);
11824
+ }
11825
+ const id = uuid();
11826
+ d.run(`INSERT INTO saved_search_views (id, name, description, scope, filters, created_at, updated_at)
11827
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [
11828
+ id,
11829
+ name,
11830
+ input.description ?? null,
11831
+ normalizeScope(input.scope),
11832
+ JSON.stringify(input.filters ?? {}),
11833
+ timestamp,
11834
+ timestamp
11835
+ ]);
11836
+ return getSearchView(id, d);
11837
+ }
11838
+ function getSearchView(idOrName, db) {
11839
+ const d = db || getDatabase();
11840
+ const row = d.query("SELECT * FROM saved_search_views WHERE id = ? OR name = ?").get(idOrName, idOrName);
11841
+ return row ? rowToSavedSearchView(row) : null;
11842
+ }
11843
+ function listSearchViews(scope, db) {
11844
+ const d = db || getDatabase();
11845
+ const normalizedScope = scope ? normalizeScope(scope) : null;
11846
+ const rows = normalizedScope ? d.query("SELECT * FROM saved_search_views WHERE scope = ? ORDER BY name").all(normalizedScope) : d.query("SELECT * FROM saved_search_views ORDER BY name").all();
11847
+ return rows.map(rowToSavedSearchView);
11848
+ }
11849
+ function deleteSearchView(idOrName, db) {
11850
+ const d = db || getDatabase();
11851
+ const result = d.run("DELETE FROM saved_search_views WHERE id = ? OR name = ?", [idOrName, idOrName]);
11852
+ return result.changes > 0;
11853
+ }
11854
+ function runSearchView(idOrName, db) {
11855
+ const d = db || getDatabase();
11856
+ const view = getSearchView(idOrName, d);
11857
+ if (!view)
11858
+ throw new Error(`Saved search view not found: ${idOrName}`);
11859
+ return { ...runSavedSearch(view.filters, view.scope, d), view };
11860
+ }
11362
11861
  // src/lib/claude-tasks.ts
11363
11862
  import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
11364
11863
  import { join as join7 } from "path";
@@ -13449,97 +13948,6 @@ function assertApprovalGate(taskId, gateName, db) {
13449
13948
  throw new Error(result.reasons.join("; "));
13450
13949
  return result.gate;
13451
13950
  }
13452
- // src/lib/local-fields.ts
13453
- init_database();
13454
- init_types();
13455
- var LOCAL_FIELDS_KEY = "local_fields";
13456
- function normalizeList(values) {
13457
- return [...new Set((values || []).map((value) => value.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b));
13458
- }
13459
- function metadataFields(task) {
13460
- const value = task.metadata[LOCAL_FIELDS_KEY];
13461
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
13462
- }
13463
- function sameCustomValue(actual, expected) {
13464
- return JSON.stringify(actual) === JSON.stringify(expected);
13465
- }
13466
- function hasOwnField(fields, key) {
13467
- return Object.prototype.hasOwnProperty.call(fields, key);
13468
- }
13469
- function getTaskLocalFields(taskId, db) {
13470
- const d = db || getDatabase();
13471
- const task = getTask(taskId, d);
13472
- if (!task)
13473
- throw new TaskNotFoundError(taskId);
13474
- const fields = metadataFields(task);
13475
- return {
13476
- labels: normalizeList(fields.labels),
13477
- priority: task.priority,
13478
- severity: typeof fields.severity === "string" ? fields.severity : null,
13479
- owner: hasOwnField(fields, "owner") ? typeof fields.owner === "string" ? fields.owner : null : task.assigned_to,
13480
- area: typeof fields.area === "string" ? fields.area : null,
13481
- custom: fields.custom && typeof fields.custom === "object" && !Array.isArray(fields.custom) ? fields.custom : {}
13482
- };
13483
- }
13484
- function setTaskLocalFields(taskId, input, db) {
13485
- const d = db || getDatabase();
13486
- const task = getTask(taskId, d);
13487
- if (!task)
13488
- throw new TaskNotFoundError(taskId);
13489
- const currentFields = getTaskLocalFields(taskId, d);
13490
- const labels = input.labels !== undefined ? normalizeList(input.labels) : currentFields.labels;
13491
- const custom = input.custom !== undefined ? redactValue(input.merge_custom === false ? input.custom : { ...currentFields.custom, ...input.custom }) : currentFields.custom;
13492
- const nextFields = {
13493
- labels,
13494
- priority: input.priority || task.priority,
13495
- severity: input.severity !== undefined ? input.severity : currentFields.severity,
13496
- owner: input.owner !== undefined ? input.owner : currentFields.owner,
13497
- area: input.area !== undefined ? input.area : currentFields.area,
13498
- custom
13499
- };
13500
- const nextMetadata = {
13501
- ...task.metadata,
13502
- [LOCAL_FIELDS_KEY]: nextFields
13503
- };
13504
- const previousLabels = new Set(currentFields.labels);
13505
- const nextTags = normalizeList([...task.tags.filter((tag) => !previousLabels.has(tag)), ...labels]);
13506
- const updates = {
13507
- version: task.version,
13508
- priority: input.priority,
13509
- tags: nextTags,
13510
- metadata: nextMetadata
13511
- };
13512
- if (input.owner !== undefined)
13513
- updates.assigned_to = nextFields.owner;
13514
- return updateTask(taskId, updates, d);
13515
- }
13516
- function queryTasksByLocalFields(query, db) {
13517
- const d = db || getDatabase();
13518
- const tasks = listTasks({
13519
- priority: query.priority,
13520
- tags: query.labels,
13521
- limit: 1e4
13522
- }, d);
13523
- const matches = tasks.filter((task) => {
13524
- const fields = getTaskLocalFields(task.id, d);
13525
- if (query.labels && !query.labels.every((label) => fields.labels.includes(label)))
13526
- return false;
13527
- if (query.severity && fields.severity !== query.severity)
13528
- return false;
13529
- if (query.owner && fields.owner !== query.owner)
13530
- return false;
13531
- if (query.area && fields.area !== query.area)
13532
- return false;
13533
- if (query.custom) {
13534
- for (const [key, expected] of Object.entries(query.custom)) {
13535
- if (!sameCustomValue(fields.custom[key], expected))
13536
- return false;
13537
- }
13538
- }
13539
- return true;
13540
- });
13541
- return matches.slice(0, query.limit || 100);
13542
- }
13543
13951
  // src/lib/task-dedupe.ts
13544
13952
  init_database();
13545
13953
  var DEFAULT_THRESHOLD = 0.74;
@@ -13978,7 +14386,7 @@ var DEFAULT_CAPABILITIES = {
13978
14386
  browser: ["browser", "screenshot", "artifact", "evidence"],
13979
14387
  script: ["script", "command", "retry", "evidence"]
13980
14388
  };
13981
- function normalizeName(name) {
14389
+ function normalizeName2(name) {
13982
14390
  const normalized = name.trim().toLowerCase();
13983
14391
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
13984
14392
  throw new Error("verification provider name must use lowercase letters, numbers, dashes, or underscores");
@@ -13997,10 +14405,10 @@ function timeoutMs(value) {
13997
14405
  return Math.max(1, Math.min(24 * 60 * 60000, Math.floor(value)));
13998
14406
  }
13999
14407
  function getProvider(name) {
14000
- return loadConfig().verification_providers?.[normalizeName(name)] || null;
14408
+ return loadConfig().verification_providers?.[normalizeName2(name)] || null;
14001
14409
  }
14002
14410
  function upsertVerificationProvider(input) {
14003
- const name = normalizeName(input.name);
14411
+ const name = normalizeName2(input.name);
14004
14412
  const config = loadConfig();
14005
14413
  const existing = config.verification_providers?.[name];
14006
14414
  const timestamp = new Date().toISOString();
@@ -14030,7 +14438,7 @@ function listVerificationProviders() {
14030
14438
  return Object.values(loadConfig().verification_providers || {}).sort((a, b) => a.name.localeCompare(b.name));
14031
14439
  }
14032
14440
  function removeVerificationProvider(name) {
14033
- const normalized = normalizeName(name);
14441
+ const normalized = normalizeName2(name);
14034
14442
  const config = loadConfig();
14035
14443
  if (!config.verification_providers?.[normalized])
14036
14444
  return false;
@@ -14879,7 +15287,8 @@ function emptyCounts2() {
14879
15287
  task_files: 0,
14880
15288
  task_commits: 0,
14881
15289
  task_git_refs: 0,
14882
- task_verifications: 0
15290
+ task_verifications: 0,
15291
+ saved_views: 0
14883
15292
  };
14884
15293
  }
14885
15294
  function bridgeToMarkdownPayload(bundle) {
@@ -15963,8 +16372,11 @@ export {
15963
16372
  searchTasks,
15964
16373
  scoreTask,
15965
16374
  saveSnapshot,
16375
+ saveSearchView,
15966
16376
  runVerificationProvider,
15967
16377
  runTodosDoctor,
16378
+ runSearchView,
16379
+ runSavedSearch,
15968
16380
  runNextAgentDispatch,
15969
16381
  runDueDispatches,
15970
16382
  revokeApiKey,
@@ -16009,6 +16421,7 @@ export {
16009
16421
  parseRecurrenceRule,
16010
16422
  parseGitHubUrl,
16011
16423
  now,
16424
+ normalizeScope,
16012
16425
  normalizeGeneratedAgentNames,
16013
16426
  normalizeApiUrl,
16014
16427
  nextTaskShortId,
@@ -16033,6 +16446,7 @@ export {
16033
16446
  listTaskFiles,
16034
16447
  listSnapshots,
16035
16448
  listSessions,
16449
+ listSearchViews,
16036
16450
  listRunnerSandboxProfiles,
16037
16451
  listProjects,
16038
16452
  listProjectSources,
@@ -16097,6 +16511,7 @@ export {
16097
16511
  getStatus,
16098
16512
  getStaleTasks,
16099
16513
  getSession,
16514
+ getSearchView,
16100
16515
  getRunnerSandboxProfile,
16101
16516
  getReviewQueue,
16102
16517
  getRelated,
@@ -16193,6 +16608,7 @@ export {
16193
16608
  deleteTaskList,
16194
16609
  deleteTask,
16195
16610
  deleteSession,
16611
+ deleteSearchView,
16196
16612
  deleteProject,
16197
16613
  deletePlan,
16198
16614
  deleteOrg,