@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/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/index.js +278 -49
- package/dist/contracts.js +135 -5
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts +8 -0
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index.js +223 -20
- package/dist/lib/json-schemas.d.ts.map +1 -1
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/onboarding-fixtures.d.ts.map +1 -1
- package/dist/lib/plan-artifacts.d.ts +3 -0
- package/dist/lib/plan-artifacts.d.ts.map +1 -1
- package/dist/mcp/index.js +138 -5
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/registry.js +135 -5
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +138 -5
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/storage.js +153 -7
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -1199,6 +1199,11 @@ var init_migrations = __esm(() => {
|
|
|
1199
1199
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
|
|
1200
1200
|
CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
|
|
1201
1201
|
INSERT OR IGNORE INTO _migrations (id) VALUES (63);
|
|
1202
|
+
`,
|
|
1203
|
+
`
|
|
1204
|
+
ALTER TABLE plans ADD COLUMN slug TEXT;
|
|
1205
|
+
CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug);
|
|
1206
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (64);
|
|
1202
1207
|
`
|
|
1203
1208
|
];
|
|
1204
1209
|
});
|
|
@@ -1222,6 +1227,29 @@ function runMigrations(db) {
|
|
|
1222
1227
|
}
|
|
1223
1228
|
ensureSchema(db);
|
|
1224
1229
|
}
|
|
1230
|
+
function planSlugBase(value) {
|
|
1231
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "plan";
|
|
1232
|
+
}
|
|
1233
|
+
function backfillPlanSlugs(db) {
|
|
1234
|
+
try {
|
|
1235
|
+
const rows = db.query("SELECT id, project_id, name, slug FROM plans ORDER BY created_at ASC, id ASC").all();
|
|
1236
|
+
const used = new Set;
|
|
1237
|
+
for (const row of rows) {
|
|
1238
|
+
const scope = row.project_id ?? "__global__";
|
|
1239
|
+
const base = planSlugBase(row.slug || row.name);
|
|
1240
|
+
let candidate = base;
|
|
1241
|
+
let suffix = 2;
|
|
1242
|
+
while (used.has(`${scope}:${candidate}`)) {
|
|
1243
|
+
candidate = `${base}-${suffix}`;
|
|
1244
|
+
suffix += 1;
|
|
1245
|
+
}
|
|
1246
|
+
used.add(`${scope}:${candidate}`);
|
|
1247
|
+
if (row.slug !== candidate) {
|
|
1248
|
+
db.run("UPDATE plans SET slug = ? WHERE id = ?", [candidate, row.id]);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
} catch {}
|
|
1252
|
+
}
|
|
1225
1253
|
function ensureSchema(db) {
|
|
1226
1254
|
const ensureColumn = (table, column, type) => {
|
|
1227
1255
|
try {
|
|
@@ -1271,7 +1299,8 @@ function ensureSchema(db) {
|
|
|
1271
1299
|
)`);
|
|
1272
1300
|
ensureTable("plans", `
|
|
1273
1301
|
CREATE TABLE plans (
|
|
1274
|
-
id TEXT PRIMARY KEY,
|
|
1302
|
+
id TEXT PRIMARY KEY, slug TEXT,
|
|
1303
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
1275
1304
|
task_list_id TEXT, agent_id TEXT,
|
|
1276
1305
|
name TEXT NOT NULL, description TEXT,
|
|
1277
1306
|
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
|
|
@@ -1786,8 +1815,10 @@ function ensureSchema(db) {
|
|
|
1786
1815
|
ensureColumn("agents", "org_id", "TEXT");
|
|
1787
1816
|
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
1788
1817
|
ensureColumn("projects", "org_id", "TEXT");
|
|
1818
|
+
ensureColumn("plans", "slug", "TEXT");
|
|
1789
1819
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
1790
1820
|
ensureColumn("plans", "agent_id", "TEXT");
|
|
1821
|
+
backfillPlanSlugs(db);
|
|
1791
1822
|
ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
|
|
1792
1823
|
ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
|
|
1793
1824
|
ensureColumn("template_tasks", "condition", "TEXT");
|
|
@@ -1902,6 +1933,8 @@ function ensureSchema(db) {
|
|
|
1902
1933
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
|
|
1903
1934
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
1904
1935
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
1936
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_slug ON plans(slug)");
|
|
1937
|
+
ensureIndex("CREATE UNIQUE INDEX IF NOT EXISTS idx_plans_scope_slug ON plans(COALESCE(project_id, ''), slug) WHERE slug IS NOT NULL");
|
|
1905
1938
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
1906
1939
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_agent ON plans(agent_id)");
|
|
1907
1940
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_history_task ON task_history(task_id)");
|
|
@@ -2750,6 +2783,9 @@ function clearExpiredLocks(db) {
|
|
|
2750
2783
|
const cutoff = lockExpiryCutoff();
|
|
2751
2784
|
db.run("UPDATE tasks SET locked_by = NULL, locked_at = NULL WHERE locked_at IS NOT NULL AND locked_at < ?", [cutoff]);
|
|
2752
2785
|
}
|
|
2786
|
+
function slugifyRef(value) {
|
|
2787
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
2788
|
+
}
|
|
2753
2789
|
function resolvePartialId(db, table, partialId) {
|
|
2754
2790
|
if (!ALLOWED_TABLES.has(table)) {
|
|
2755
2791
|
throw new Error(`Invalid table name: ${table}`);
|
|
@@ -2776,6 +2812,16 @@ function resolvePartialId(db, table, partialId) {
|
|
|
2776
2812
|
if (slugRow)
|
|
2777
2813
|
return slugRow.id;
|
|
2778
2814
|
}
|
|
2815
|
+
if (table === "plans") {
|
|
2816
|
+
const slug = slugifyRef(partialId);
|
|
2817
|
+
if (slug) {
|
|
2818
|
+
const slugRows = db.query("SELECT id FROM plans WHERE slug = ?").all(slug);
|
|
2819
|
+
if (slugRows.length === 1)
|
|
2820
|
+
return slugRows[0].id;
|
|
2821
|
+
if (slugRows.length > 1)
|
|
2822
|
+
return null;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2779
2825
|
if (table === "projects") {
|
|
2780
2826
|
const nameRow = db.query("SELECT id FROM projects WHERE lower(name) = ?").get(partialId.toLowerCase());
|
|
2781
2827
|
if (nameRow)
|
|
@@ -12788,15 +12834,59 @@ var init_task_relations = __esm(() => {
|
|
|
12788
12834
|
});
|
|
12789
12835
|
|
|
12790
12836
|
// src/db/plans.ts
|
|
12837
|
+
function planSlugBase2(value) {
|
|
12838
|
+
return slugify(value) || "plan";
|
|
12839
|
+
}
|
|
12840
|
+
function normalizePlanSlug(value) {
|
|
12841
|
+
const slug = slugify(value);
|
|
12842
|
+
if (!slug)
|
|
12843
|
+
throw new Error("Invalid plan slug");
|
|
12844
|
+
return slug;
|
|
12845
|
+
}
|
|
12846
|
+
function plansBySlug(slug, db, projectId) {
|
|
12847
|
+
if (projectId !== undefined) {
|
|
12848
|
+
if (projectId === null) {
|
|
12849
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id IS NULL ORDER BY created_at ASC, id ASC").all(slug);
|
|
12850
|
+
}
|
|
12851
|
+
return db.query("SELECT * FROM plans WHERE slug = ? AND project_id = ? ORDER BY created_at ASC, id ASC").all(slug, projectId);
|
|
12852
|
+
}
|
|
12853
|
+
return db.query("SELECT * FROM plans WHERE slug = ? ORDER BY created_at ASC, id ASC").all(slug);
|
|
12854
|
+
}
|
|
12855
|
+
function planSlugExists(slug, projectId, db, excludeId) {
|
|
12856
|
+
const rows = plansBySlug(slug, db, projectId);
|
|
12857
|
+
return rows.some((plan) => plan.id !== excludeId);
|
|
12858
|
+
}
|
|
12859
|
+
function nextPlanSlug(base, projectId, db, excludeId) {
|
|
12860
|
+
let candidate = base;
|
|
12861
|
+
let suffix = 2;
|
|
12862
|
+
while (planSlugExists(candidate, projectId, db, excludeId)) {
|
|
12863
|
+
candidate = `${base}-${suffix}`;
|
|
12864
|
+
suffix += 1;
|
|
12865
|
+
}
|
|
12866
|
+
return candidate;
|
|
12867
|
+
}
|
|
12868
|
+
function resolveCreateSlug(input, projectId, db) {
|
|
12869
|
+
if (input.slug !== undefined) {
|
|
12870
|
+
const slug = normalizePlanSlug(input.slug);
|
|
12871
|
+
if (planSlugExists(slug, projectId, db)) {
|
|
12872
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
12873
|
+
}
|
|
12874
|
+
return slug;
|
|
12875
|
+
}
|
|
12876
|
+
return nextPlanSlug(planSlugBase2(input.name), projectId, db);
|
|
12877
|
+
}
|
|
12791
12878
|
function createPlan(input, db) {
|
|
12792
12879
|
const d = db || getDatabase();
|
|
12793
12880
|
const id = uuid();
|
|
12794
12881
|
const timestamp = now();
|
|
12882
|
+
const projectId = input.project_id || null;
|
|
12883
|
+
const slug = resolveCreateSlug(input, projectId, d);
|
|
12795
12884
|
const machineId = currentStorageMachineId(d);
|
|
12796
|
-
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
12797
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
12885
|
+
d.run(`INSERT INTO plans (id, slug, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
|
|
12886
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
12798
12887
|
id,
|
|
12799
|
-
|
|
12888
|
+
slug,
|
|
12889
|
+
projectId,
|
|
12800
12890
|
input.task_list_id || null,
|
|
12801
12891
|
input.agent_id || null,
|
|
12802
12892
|
input.name,
|
|
@@ -12831,6 +12921,14 @@ function updatePlan(id, input, db) {
|
|
|
12831
12921
|
sets.push("name = ?");
|
|
12832
12922
|
params.push(input.name);
|
|
12833
12923
|
}
|
|
12924
|
+
if (input.slug !== undefined) {
|
|
12925
|
+
const slug = normalizePlanSlug(input.slug);
|
|
12926
|
+
if (planSlugExists(slug, plan.project_id, d, id)) {
|
|
12927
|
+
throw new Error(`Plan slug already exists in this scope: ${slug}`);
|
|
12928
|
+
}
|
|
12929
|
+
sets.push("slug = ?");
|
|
12930
|
+
params.push(slug);
|
|
12931
|
+
}
|
|
12834
12932
|
if (input.description !== undefined) {
|
|
12835
12933
|
sets.push("description = ?");
|
|
12836
12934
|
params.push(input.description);
|
|
@@ -12875,6 +12973,7 @@ var init_plans = __esm(() => {
|
|
|
12875
12973
|
init_event_emission_safety();
|
|
12876
12974
|
init_event_hooks();
|
|
12877
12975
|
init_database();
|
|
12976
|
+
init_projects();
|
|
12878
12977
|
init_storage_tombstones();
|
|
12879
12978
|
});
|
|
12880
12979
|
|
|
@@ -22445,6 +22544,7 @@ Tasks:` : null,
|
|
|
22445
22544
|
if (shouldRegisterTool("create_plan")) {
|
|
22446
22545
|
server.tool("create_plan", "Create a new plan (sprint/milestone).", {
|
|
22447
22546
|
name: exports_external.string().describe("Plan name"),
|
|
22547
|
+
slug: exports_external.string().optional().describe("Readable plan slug"),
|
|
22448
22548
|
project_id: exports_external.string().optional().describe("Project ID"),
|
|
22449
22549
|
description: exports_external.string().optional(),
|
|
22450
22550
|
start_date: exports_external.string().optional().describe("ISO date"),
|
|
@@ -29506,6 +29606,36 @@ function prepareValue(column, value) {
|
|
|
29506
29606
|
return JSON.stringify(value ?? (column === "tags" || column === "files_changed" ? [] : {}));
|
|
29507
29607
|
return value === undefined ? null : value;
|
|
29508
29608
|
}
|
|
29609
|
+
function slugifyPlanValue(value) {
|
|
29610
|
+
return typeof value === "string" ? value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : "";
|
|
29611
|
+
}
|
|
29612
|
+
function planSlugBase3(plan) {
|
|
29613
|
+
return slugifyPlanValue(plan.slug) || slugifyPlanValue(plan.name) || "plan";
|
|
29614
|
+
}
|
|
29615
|
+
function planSlugScope(projectId) {
|
|
29616
|
+
return typeof projectId === "string" && projectId ? projectId : "__global__";
|
|
29617
|
+
}
|
|
29618
|
+
function planSlugKey(projectId, slug) {
|
|
29619
|
+
return `${planSlugScope(projectId)}:${slug}`;
|
|
29620
|
+
}
|
|
29621
|
+
function normalizeBridgePlanSlugs(plans, db) {
|
|
29622
|
+
const existingRows = db.query("SELECT id, project_id, slug FROM plans WHERE slug IS NOT NULL").all();
|
|
29623
|
+
const existingIds = new Set(existingRows.map((row) => row.id));
|
|
29624
|
+
const used = new Set(existingRows.filter((row) => row.slug).map((row) => planSlugKey(row.project_id, row.slug)));
|
|
29625
|
+
return plans.map((plan) => {
|
|
29626
|
+
if (existingIds.has(plan.id))
|
|
29627
|
+
return plan;
|
|
29628
|
+
const base = planSlugBase3(plan);
|
|
29629
|
+
let candidate = base;
|
|
29630
|
+
let suffix = 2;
|
|
29631
|
+
while (used.has(planSlugKey(plan.project_id, candidate))) {
|
|
29632
|
+
candidate = `${base}-${suffix}`;
|
|
29633
|
+
suffix += 1;
|
|
29634
|
+
}
|
|
29635
|
+
used.add(planSlugKey(plan.project_id, candidate));
|
|
29636
|
+
return { ...plan, slug: candidate };
|
|
29637
|
+
});
|
|
29638
|
+
}
|
|
29509
29639
|
function insertRecord(db, tableKey, row) {
|
|
29510
29640
|
const table = tableByKey[tableKey];
|
|
29511
29641
|
const columns = insertColumns[tableKey];
|
|
@@ -29642,6 +29772,7 @@ function importLocalBridgeBundle(bundle, options = {}, db) {
|
|
|
29642
29772
|
const conflictStrategy = options.conflictStrategy ?? "skip";
|
|
29643
29773
|
const data = {
|
|
29644
29774
|
...bundle.data,
|
|
29775
|
+
plans: normalizeBridgePlanSlugs(bundle.data.plans, d),
|
|
29645
29776
|
tasks: sortedTasks(bundle.data.tasks),
|
|
29646
29777
|
saved_views: bundle.data.saved_views ?? [],
|
|
29647
29778
|
task_boards: bundle.data.task_boards ?? [],
|
|
@@ -29726,7 +29857,7 @@ var init_local_bridge = __esm(() => {
|
|
|
29726
29857
|
insertColumns = {
|
|
29727
29858
|
projects: ["id", "name", "path", "description", "task_list_id", "task_prefix", "task_counter", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
29728
29859
|
task_lists: ["id", "project_id", "slug", "name", "description", "metadata", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
29729
|
-
plans: ["id", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
29860
|
+
plans: ["id", "slug", "project_id", "task_list_id", "agent_id", "name", "description", "status", "created_at", "updated_at", "machine_id", "synced_at"],
|
|
29730
29861
|
tasks: [
|
|
29731
29862
|
"id",
|
|
29732
29863
|
"short_id",
|
|
@@ -30228,6 +30359,7 @@ function createAgentProjectDemoBundle() {
|
|
|
30228
30359
|
});
|
|
30229
30360
|
data.plans.push({
|
|
30230
30361
|
id: ids.plan,
|
|
30362
|
+
slug: "ship-local-demo-workflow",
|
|
30231
30363
|
project_id: ids.project,
|
|
30232
30364
|
task_list_id: ids.list,
|
|
30233
30365
|
agent_id: "demo-agent",
|
|
@@ -39238,6 +39370,7 @@ async function handleCreatePlan(req, _ctx, json2) {
|
|
|
39238
39370
|
return json2({ error: "Missing 'name'" }, 400);
|
|
39239
39371
|
const plan = createPlan({
|
|
39240
39372
|
name: body.name,
|
|
39373
|
+
slug: body.slug,
|
|
39241
39374
|
description: body.description,
|
|
39242
39375
|
project_id: body.project_id,
|
|
39243
39376
|
task_list_id: body.task_list_id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-project-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/task-project-tools.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAkJjD,UAAU,kBAAkB;IAC1B,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACxC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC,gBAAgB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACvE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CAC3F;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"task-project-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/task-project-tools.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAkJjD,UAAU,kBAAkB;IAC1B,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACxC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC,gBAAgB,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACvE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CAC3F;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,kBAAkB,QAg2FlF"}
|
package/dist/registry.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,
|
|
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
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageName": "@hasna/todos",
|
|
3
|
-
"packageVersion": "0.11.
|
|
3
|
+
"packageVersion": "0.11.71",
|
|
4
4
|
"repository": "https://github.com/hasna/todos.git",
|
|
5
|
-
"gitCommit": "
|
|
6
|
-
"generatedAt": "2026-
|
|
5
|
+
"gitCommit": "7cb2824a2e8d60063a31a4bdb465fce9a5d41880",
|
|
6
|
+
"generatedAt": "2026-07-01T10:22:06.425Z"
|
|
7
7
|
}
|