@hasna/todos 0.11.70 → 0.11.71

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/contracts.js CHANGED
@@ -1198,6 +1198,11 @@ var init_migrations = __esm(() => {
1198
1198
  CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
1199
1199
  CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
1200
1200
  INSERT OR IGNORE INTO _migrations (id) VALUES (63);
1201
+ `,
1202
+ `
1203
+ ALTER TABLE plans ADD COLUMN slug TEXT;
1204
+ CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
1205
+ INSERT OR IGNORE INTO _migrations (id) VALUES (64);
1201
1206
  `
1202
1207
  ];
1203
1208
  });
@@ -1221,6 +1226,29 @@ function runMigrations(db) {
1221
1226
  }
1222
1227
  ensureSchema(db);
1223
1228
  }
1229
+ function planSlugBase(value) {
1230
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "plan";
1231
+ }
1232
+ function backfillPlanSlugs(db) {
1233
+ try {
1234
+ const rows = db.query("SELECT id, project_id, name, slug FROM plans ORDER BY created_at ASC, id ASC").all();
1235
+ const used = new Set;
1236
+ for (const row of rows) {
1237
+ const scope = row.project_id ?? "__global__";
1238
+ const base = planSlugBase(row.slug || row.name);
1239
+ let candidate = base;
1240
+ let suffix = 2;
1241
+ while (used.has(`${scope}:${candidate}`)) {
1242
+ candidate = `${base}-${suffix}`;
1243
+ suffix += 1;
1244
+ }
1245
+ used.add(`${scope}:${candidate}`);
1246
+ if (row.slug !== candidate) {
1247
+ db.run("UPDATE plans SET slug = ? WHERE id = ?", [candidate, row.id]);
1248
+ }
1249
+ }
1250
+ } catch {}
1251
+ }
1224
1252
  function ensureSchema(db) {
1225
1253
  const ensureColumn = (table, column, type) => {
1226
1254
  try {
@@ -1270,7 +1298,8 @@ function ensureSchema(db) {
1270
1298
  )`);
1271
1299
  ensureTable("plans", `
1272
1300
  CREATE TABLE plans (
1273
- id TEXT PRIMARY KEY, project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
1301
+ id TEXT PRIMARY KEY, slug TEXT,
1302
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
1274
1303
  task_list_id TEXT, agent_id TEXT,
1275
1304
  name TEXT NOT NULL, description TEXT,
1276
1305
  status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
@@ -1785,8 +1814,10 @@ function ensureSchema(db) {
1785
1814
  ensureColumn("agents", "org_id", "TEXT");
1786
1815
  ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
1787
1816
  ensureColumn("projects", "org_id", "TEXT");
1817
+ ensureColumn("plans", "slug", "TEXT");
1788
1818
  ensureColumn("plans", "task_list_id", "TEXT");
1789
1819
  ensureColumn("plans", "agent_id", "TEXT");
1820
+ backfillPlanSlugs(db);
1790
1821
  ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
1791
1822
  ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
1792
1823
  ensureColumn("template_tasks", "condition", "TEXT");
@@ -1901,6 +1932,8 @@ function ensureSchema(db) {
1901
1932
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
1902
1933
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
1903
1934
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
1935
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug)");
1936
+ ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_scope_slug ON plans(COALESCE(project_id, ''), slug) WHERE slug IS NOT NULL");
1904
1937
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
1905
1938
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
1906
1939
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
@@ -2734,6 +2767,9 @@ function clearExpiredLocks(db) {
2734
2767
  const cutoff = lockExpiryCutoff();
2735
2768
  db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
2736
2769
  }
2770
+ function slugifyRef(value) {
2771
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2772
+ }
2737
2773
  function resolvePartialId(db, table, partialId) {
2738
2774
  if (!ALLOWED_TABLES.has(table)) {
2739
2775
  throw new Error(`Invalid table name: ${table}`);
@@ -2760,6 +2796,16 @@ function resolvePartialId(db, table, partialId) {
2760
2796
  if (slugRow)
2761
2797
  return slugRow.id;
2762
2798
  }
2799
+ if (table === "plans") {
2800
+ const slug = slugifyRef(partialId);
2801
+ if (slug) {
2802
+ const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
2803
+ if (slugRows.length === 1)
2804
+ return slugRows[0].id;
2805
+ if (slugRows.length > 1)
2806
+ return null;
2807
+ }
2808
+ }
2763
2809
  if (table === "projects") {
2764
2810
  const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
2765
2811
  if (nameRow)
@@ -10631,15 +10677,59 @@ init_database();
10631
10677
  // src/db/plans.ts
10632
10678
  init_types();
10633
10679
  init_database();
10680
+ function planSlugBase2(value) {
10681
+ return slugify(value) || "plan";
10682
+ }
10683
+ function normalizePlanSlug(value) {
10684
+ const slug = slugify(value);
10685
+ if (!slug)
10686
+ throw new Error("Invalid plan slug");
10687
+ return slug;
10688
+ }
10689
+ function plansBySlug(slug, db, projectId) {
10690
+ if (projectId !== undefined) {
10691
+ if (projectId === null) {
10692
+ return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
10693
+ }
10694
+ return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
10695
+ }
10696
+ return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
10697
+ }
10698
+ function planSlugExists(slug, projectId, db, excludeId) {
10699
+ const rows = plansBySlug(slug, db, projectId);
10700
+ return rows.some((plan) => plan.id !== excludeId);
10701
+ }
10702
+ function nextPlanSlug(base, projectId, db, excludeId) {
10703
+ let candidate = base;
10704
+ let suffix = 2;
10705
+ while (planSlugExists(candidate, projectId, db, excludeId)) {
10706
+ candidate = `${base}-${suffix}`;
10707
+ suffix += 1;
10708
+ }
10709
+ return candidate;
10710
+ }
10711
+ function resolveCreateSlug(input, projectId, db) {
10712
+ if (input.slug !== undefined) {
10713
+ const slug = normalizePlanSlug(input.slug);
10714
+ if (planSlugExists(slug, projectId, db)) {
10715
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
10716
+ }
10717
+ return slug;
10718
+ }
10719
+ return nextPlanSlug(planSlugBase2(input.name), projectId, db);
10720
+ }
10634
10721
  function createPlan(input, db) {
10635
10722
  const d = db || getDatabase();
10636
10723
  const id = uuid();
10637
10724
  const timestamp = now();
10725
+ const projectId = input.project_id || null;
10726
+ const slug = resolveCreateSlug(input, projectId, d);
10638
10727
  const machineId = currentStorageMachineId(d);
10639
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10640
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10728
+ d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
10729
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10641
10730
  id,
10642
- input.project_id || null,
10731
+ slug,
10732
+ projectId,
10643
10733
  input.task_list_id || null,
10644
10734
  input.agent_id || null,
10645
10735
  input.name,
@@ -10674,6 +10764,14 @@ function updatePlan(id, input, db) {
10674
10764
  sets.push("name = ?");
10675
10765
  params.push(input.name);
10676
10766
  }
10767
+ if (input.slug !== undefined) {
10768
+ const slug = normalizePlanSlug(input.slug);
10769
+ if (planSlugExists(slug, plan.project_id, d, id)) {
10770
+ throw new Error(`Plan slug already exists in this scope: ${slug}`);
10771
+ }
10772
+ sets.push("slug = ?");
10773
+ params.push(slug);
10774
+ }
10677
10775
  if (input.description !== undefined) {
10678
10776
  sets.push("description = ?");
10679
10777
  params.push(input.description);
@@ -12527,7 +12625,7 @@ var dataKeys = [
12527
12625
  var insertColumns = {
12528
12626
  projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
12529
12627
  task_lists: ["id", "project_id", "slug", "name", "description", "metadata", "created_at", "updated_at", "machine_id", "synced_at"],
12530
- plans: ["id", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
12628
+ plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
12531
12629
  tasks: [
12532
12630
  "id",
12533
12631
  "short_id",
@@ -12819,6 +12917,36 @@ function prepareValue(column, value) {
12819
12917
  return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
12820
12918
  return value === undefined ? null : value;
12821
12919
  }
12920
+ function slugifyPlanValue(value) {
12921
+ return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
12922
+ }
12923
+ function planSlugBase3(plan) {
12924
+ return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
12925
+ }
12926
+ function planSlugScope(projectId) {
12927
+ return typeof projectId === "string" && projectId ? projectId : "__global__";
12928
+ }
12929
+ function planSlugKey(projectId, slug) {
12930
+ return `${planSlugScope(projectId)}:${slug}`;
12931
+ }
12932
+ function normalizeBridgePlanSlugs(plans, db) {
12933
+ const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
12934
+ const existingIds = new Set(existingRows.map((row) => row.id));
12935
+ const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
12936
+ return plans.map((plan) => {
12937
+ if (existingIds.has(plan.id))
12938
+ return plan;
12939
+ const base = planSlugBase3(plan);
12940
+ let candidate = base;
12941
+ let suffix = 2;
12942
+ while (used.has(planSlugKey(plan.project_id, candidate))) {
12943
+ candidate = `${base}-${suffix}`;
12944
+ suffix += 1;
12945
+ }
12946
+ used.add(planSlugKey(plan.project_id, candidate));
12947
+ return { ...plan, slug: candidate };
12948
+ });
12949
+ }
12822
12950
  function insertRecord(db, tableKey, row) {
12823
12951
  const table = tableByKey[tableKey];
12824
12952
  const columns = insertColumns[tableKey];
@@ -12955,6 +13083,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
12955
13083
  const conflictStrategy = options.conflictStrategy ?? "skip";
12956
13084
  const data = {
12957
13085
  ...bundle.data,
13086
+ plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
12958
13087
  tasks: sortedTasks(bundle.data.tasks),
12959
13088
  saved_views: bundle.data.saved_views ?? [],
12960
13089
  task_boards: bundle.data.task_boards ?? [],
@@ -13148,6 +13277,7 @@ function createAgentProjectDemoBundle() {
13148
13277
  });
13149
13278
  data.plans.push({
13150
13279
  id: ids.plan,
13280
+ slug: "ship-local-demo-workflow",
13151
13281
  project_id: ids.project,
13152
13282
  task_list_id: ids.list,
13153
13283
  agent_id: "demo-agent",
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAMtC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA2DtC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAaD,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAyBrD;AAED,wBAAgB,aAAa,IAAI,IAAI,CAMpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,SAAa,GAAG,OAAO,CAKlF;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAa,GAAG,MAAM,CAG3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAID,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiD9F"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAMtC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA2DtC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAaD,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAyBrD;AAED,wBAAgB,aAAa,IAAI,IAAI,CAMpC;AAED,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED,wBAAgB,GAAG,IAAI,MAAM,CAE5B;AAED,wBAAgB,IAAI,IAAI,MAAM,CAE7B;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,SAAa,GAAG,OAAO,CAKlF;AAED,wBAAgB,gBAAgB,CAAC,KAAK,SAAa,GAAG,MAAM,CAG3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAGpD;AAQD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA4D9F"}
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,UAusCtB,CAAC"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,UA6sCtB,CAAC"}
@@ -1,5 +1,13 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import type { CreatePlanInput, Plan, UpdatePlanInput } from "../types/index.js";
3
+ export interface ResolvePlanRefResult {
4
+ id: string | null;
5
+ reason: "id" | "slug" | "not_found" | "ambiguous";
6
+ matches: Plan[];
7
+ }
8
+ export declare function normalizePlanSlug(value: string): string;
9
+ export declare function resolvePlanRefDetailed(ref: string, db?: Database, projectId?: string | null): ResolvePlanRefResult;
10
+ export declare function resolvePlanRef(ref: string, db?: Database, projectId?: string | null): string | null;
3
11
  export declare function createPlan(input: CreatePlanInput, db?: Database): Plan;
4
12
  export declare function getPlan(id: string, db?: Database): Plan | null;
5
13
  export declare function listPlans(projectId?: string, db?: Database): Plan[];
@@ -1 +1 @@
1
- {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../src/db/plans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAOhF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAwBtE;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAI9D;AAED,wBAAgB,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE,CAUnE;AAED,wBAAgB,UAAU,CACxB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,EAAE,CAAC,EAAE,QAAQ,GACZ,IAAI,CAuCN;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW7D"}
1
+ {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../src/db/plans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAQhF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IAClD,OAAO,EAAE,IAAI,EAAE,CAAC;CACjB;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD;AAsCD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,CAalH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAEnG;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CA2BtE;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAI9D;AAED,wBAAgB,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE,CAUnE;AAED,wBAAgB,UAAU,CACxB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,EAAE,CAAC,EAAE,QAAQ,GACZ,IAAI,CA+CN;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA8BhD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA2nC/C;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA4BnD"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA8BhD;AA6BD,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAgoC/C;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CA4BnD"}