@hasna/todos 0.14.0 → 0.15.1
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/README.md +2 -0
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +258 -43
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +113 -112
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +120 -119
- package/dist/lib/bulk-tags.d.ts +57 -0
- package/dist/lib/bulk-tags.d.ts.map +1 -0
- package/dist/lib/enum-vocabulary.d.ts.map +1 -1
- package/dist/lib/run-records.d.ts.map +1 -1
- package/dist/lib/sandbox-profiles.d.ts.map +1 -1
- package/dist/lib/sync-utils.d.ts +1 -1
- package/dist/lib/sync-utils.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +75 -58
- package/dist/mcp/tools/task-crud.d.ts +1 -0
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/registry.js +113 -112
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +3 -2
- package/dist/server/index.js +31 -14
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +112 -111
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -70,7 +70,7 @@ var package_default;
|
|
|
70
70
|
var init_package = __esm(() => {
|
|
71
71
|
package_default = {
|
|
72
72
|
name: "@hasna/todos",
|
|
73
|
-
version: "0.
|
|
73
|
+
version: "0.15.1",
|
|
74
74
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
75
75
|
type: "module",
|
|
76
76
|
main: "dist/index.js",
|
|
@@ -1041,9 +1041,10 @@ function normalizeAgentNameInput(name) {
|
|
|
1041
1041
|
|
|
1042
1042
|
// src/lib/sync-utils.ts
|
|
1043
1043
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
1044
|
+
import { homedir } from "os";
|
|
1044
1045
|
import { join } from "path";
|
|
1045
1046
|
function getHomeDir() {
|
|
1046
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
1047
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
1047
1048
|
}
|
|
1048
1049
|
function getTodosGlobalDir() {
|
|
1049
1050
|
return join(getHomeDir(), ".hasna", "todos");
|
|
@@ -1070,7 +1071,7 @@ function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
|
1070
1071
|
}
|
|
1071
1072
|
var HOME;
|
|
1072
1073
|
var init_sync_utils = __esm(() => {
|
|
1073
|
-
HOME =
|
|
1074
|
+
HOME = getHomeDir();
|
|
1074
1075
|
});
|
|
1075
1076
|
|
|
1076
1077
|
// src/lib/creator-identity.ts
|
|
@@ -9372,8 +9373,7 @@ function findGitRoot(startDir) {
|
|
|
9372
9373
|
return null;
|
|
9373
9374
|
}
|
|
9374
9375
|
function getGlobalDbPath() {
|
|
9375
|
-
|
|
9376
|
-
return join4(home, ".hasna", "todos", "todos.db");
|
|
9376
|
+
return join4(getHomeDir(), ".hasna", "todos", "todos.db");
|
|
9377
9377
|
}
|
|
9378
9378
|
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
9379
9379
|
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
@@ -9595,6 +9595,7 @@ var init_database = __esm(() => {
|
|
|
9595
9595
|
init_machines();
|
|
9596
9596
|
init_identity_mapping();
|
|
9597
9597
|
init_types();
|
|
9598
|
+
init_sync_utils();
|
|
9598
9599
|
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
|
|
9599
9600
|
});
|
|
9600
9601
|
|
|
@@ -10874,7 +10875,7 @@ var init_event_hooks = __esm(() => {
|
|
|
10874
10875
|
// node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
|
|
10875
10876
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
10876
10877
|
import { existsSync as existsSync6 } from "fs";
|
|
10877
|
-
import { homedir } from "os";
|
|
10878
|
+
import { homedir as homedir2 } from "os";
|
|
10878
10879
|
import { join as join5 } from "path";
|
|
10879
10880
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
10880
10881
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
@@ -10976,7 +10977,7 @@ function channelMatchesEvent(channel, event) {
|
|
|
10976
10977
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
10977
10978
|
}
|
|
10978
10979
|
function getEventsDataDir(override) {
|
|
10979
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(
|
|
10980
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir2(), ".hasna", "events");
|
|
10980
10981
|
}
|
|
10981
10982
|
|
|
10982
10983
|
class JsonEventsStore {
|
|
@@ -17301,14 +17302,21 @@ function suggestVocabularyMatches(value, vocabulary, limit = 3) {
|
|
|
17301
17302
|
}
|
|
17302
17303
|
function resolveEnumVocabulary(raw, spec) {
|
|
17303
17304
|
const allowList = spec.allowList !== false;
|
|
17304
|
-
const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim())
|
|
17305
|
-
if (rawElements.length === 0) {
|
|
17305
|
+
const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
|
|
17306
|
+
if (rawElements.every((element) => element.length === 0)) {
|
|
17306
17307
|
return {
|
|
17307
17308
|
ok: false,
|
|
17308
17309
|
message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
|
|
17309
17310
|
invalid: []
|
|
17310
17311
|
};
|
|
17311
17312
|
}
|
|
17313
|
+
if (rawElements.some((element) => element.length === 0)) {
|
|
17314
|
+
return {
|
|
17315
|
+
ok: false,
|
|
17316
|
+
message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
|
|
17317
|
+
invalid: []
|
|
17318
|
+
};
|
|
17319
|
+
}
|
|
17312
17320
|
const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
|
|
17313
17321
|
const allowed = new Set(spec.vocabulary);
|
|
17314
17322
|
const invalid = [];
|
|
@@ -23377,7 +23385,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
23377
23385
|
return error(405, `method ${method} not allowed on /v1/refs/:ref`);
|
|
23378
23386
|
if (!store.gitRefs)
|
|
23379
23387
|
return error(501, "git ref links are not supported by this storage backend");
|
|
23380
|
-
|
|
23388
|
+
let decodedRef;
|
|
23389
|
+
try {
|
|
23390
|
+
decodedRef = decodeURIComponent(id);
|
|
23391
|
+
} catch {
|
|
23392
|
+
return error(400, "ref path segment has invalid percent encoding");
|
|
23393
|
+
}
|
|
23394
|
+
const refs = await store.gitRefs.find(decodedRef);
|
|
23381
23395
|
return json3({ refs, count: refs.length });
|
|
23382
23396
|
}
|
|
23383
23397
|
if (resource === "next" && !id) {
|
|
@@ -44944,10 +44958,10 @@ var init_token_utils = __esm(() => {
|
|
|
44944
44958
|
|
|
44945
44959
|
// src/lib/assignee-validation.ts
|
|
44946
44960
|
import { readFileSync as readFileSync3 } from "fs";
|
|
44947
|
-
import { homedir as
|
|
44961
|
+
import { homedir as homedir3 } from "os";
|
|
44948
44962
|
import { join as join9 } from "path";
|
|
44949
44963
|
function defaultSeatRosterPath() {
|
|
44950
|
-
return process.env["TODOS_SEAT_ROSTER_PATH"] || join9(
|
|
44964
|
+
return process.env["TODOS_SEAT_ROSTER_PATH"] || join9(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
|
|
44951
44965
|
}
|
|
44952
44966
|
function loadSeatSlugs(path = defaultSeatRosterPath()) {
|
|
44953
44967
|
try {
|
|
@@ -58680,7 +58694,7 @@ async function validateMcpAssignee(value, allowSeat) {
|
|
|
58680
58694
|
return verdict.assignee;
|
|
58681
58695
|
}
|
|
58682
58696
|
function registerTaskCrudTools(server, ctx) {
|
|
58683
|
-
const { shouldRegisterTool, resolveId, formatError: formatError2, formatTask } = ctx;
|
|
58697
|
+
const { shouldRegisterTool, resolveId, formatError: formatError2, formatTask, applyFocus } = ctx;
|
|
58684
58698
|
function mutationTaskResponse(task) {
|
|
58685
58699
|
const compact = compactTask(task, 240);
|
|
58686
58700
|
compact["version"] = task.version;
|
|
@@ -58756,6 +58770,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
58756
58770
|
payload.max_retries = retry_count;
|
|
58757
58771
|
if (deadline)
|
|
58758
58772
|
payload.due_at = deadline;
|
|
58773
|
+
applyFocus(payload, router.agent_id || undefined);
|
|
58759
58774
|
const created = await cloudCreateTask(cloud, payload);
|
|
58760
58775
|
return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
|
|
58761
58776
|
}
|
|
@@ -58782,6 +58797,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
58782
58797
|
resolved.max_retries = retry_count;
|
|
58783
58798
|
if (deadline)
|
|
58784
58799
|
resolved.due_at = deadline;
|
|
58800
|
+
applyFocus(resolved, router.agent_id || undefined);
|
|
58785
58801
|
const task = createTask2(resolved);
|
|
58786
58802
|
return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
|
|
58787
58803
|
} catch (e) {
|
|
@@ -105060,7 +105076,8 @@ function buildServer() {
|
|
|
105060
105076
|
formatError: formatError2,
|
|
105061
105077
|
formatTask,
|
|
105062
105078
|
formatTaskDetail,
|
|
105063
|
-
getAgentFocus
|
|
105079
|
+
getAgentFocus,
|
|
105080
|
+
applyFocus
|
|
105064
105081
|
};
|
|
105065
105082
|
registerTaskCrudTools(server, toolContext);
|
|
105066
105083
|
registerTaskProjectTools(server, toolContext);
|
package/dist/server/v1.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v1.d.ts","sourceRoot":"","sources":["../../src/server/v1.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAuB,oBAAoB,EAAmD,MAAM,0BAA0B,CAAC;AAC3I,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKhH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACxC,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAClD,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;CACjD;AA0TD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAiB1E;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAapE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,YAAY,GAAE,qBAA0B,GACvC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"v1.d.ts","sourceRoot":"","sources":["../../src/server/v1.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAuB,oBAAoB,EAAmD,MAAM,0BAA0B,CAAC;AAC3I,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKhH,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,OAAO,gBAAgB,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,iBAAiB,CAAC;IACxC,iBAAiB,CAAC,EAAE,OAAO,sBAAsB,CAAC;IAClD,gBAAgB,CAAC,EAAE,OAAO,qBAAqB,CAAC;CACjD;AA0TD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,oBAAoB,CAiB1E;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAapE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,YAAY,GAAE,qBAA0B,GACvC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAu6B1B"}
|
package/dist/storage.js
CHANGED
|
@@ -4020,6 +4020,103 @@ var init_identity_mapping = __esm(() => {
|
|
|
4020
4020
|
});
|
|
4021
4021
|
});
|
|
4022
4022
|
|
|
4023
|
+
// src/lib/sync-utils.ts
|
|
4024
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
4025
|
+
import { createHash } from "crypto";
|
|
4026
|
+
import { homedir } from "os";
|
|
4027
|
+
import { join } from "path";
|
|
4028
|
+
function getHomeDir() {
|
|
4029
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
4030
|
+
}
|
|
4031
|
+
function getTodosGlobalDir() {
|
|
4032
|
+
return join(getHomeDir(), ".hasna", "todos");
|
|
4033
|
+
}
|
|
4034
|
+
function ensureDir(dir) {
|
|
4035
|
+
if (!existsSync2(dir))
|
|
4036
|
+
mkdirSync(dir, { recursive: true });
|
|
4037
|
+
}
|
|
4038
|
+
function listJsonFiles(dir) {
|
|
4039
|
+
if (!existsSync2(dir))
|
|
4040
|
+
return [];
|
|
4041
|
+
return readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
4042
|
+
}
|
|
4043
|
+
function readJsonFile(path) {
|
|
4044
|
+
try {
|
|
4045
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
4046
|
+
} catch {
|
|
4047
|
+
return null;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
function writeJsonFile(path, data) {
|
|
4051
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
4052
|
+
`);
|
|
4053
|
+
}
|
|
4054
|
+
function readHighWaterMark(dir) {
|
|
4055
|
+
const path = join(dir, ".highwatermark");
|
|
4056
|
+
if (!existsSync2(path))
|
|
4057
|
+
return 1;
|
|
4058
|
+
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
4059
|
+
return isNaN(val) ? 1 : val;
|
|
4060
|
+
}
|
|
4061
|
+
function writeHighWaterMark(dir, value) {
|
|
4062
|
+
writeFileSync(join(dir, ".highwatermark"), String(value));
|
|
4063
|
+
}
|
|
4064
|
+
function getFileMtimeMs(path) {
|
|
4065
|
+
try {
|
|
4066
|
+
return statSync(path).mtimeMs;
|
|
4067
|
+
} catch {
|
|
4068
|
+
return null;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
function parseTimestamp(value) {
|
|
4072
|
+
if (typeof value !== "string")
|
|
4073
|
+
return null;
|
|
4074
|
+
const parsed = Date.parse(value);
|
|
4075
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
4076
|
+
}
|
|
4077
|
+
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
4078
|
+
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
4079
|
+
const next = [conflict, ...current].slice(0, limit);
|
|
4080
|
+
return { ...metadata, sync_conflicts: next };
|
|
4081
|
+
}
|
|
4082
|
+
function canonicalize(value) {
|
|
4083
|
+
if (Array.isArray(value))
|
|
4084
|
+
return value.map(canonicalize);
|
|
4085
|
+
if (!value || typeof value !== "object")
|
|
4086
|
+
return value;
|
|
4087
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).sort(([a], [b]) => a.localeCompare(b));
|
|
4088
|
+
return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
|
|
4089
|
+
}
|
|
4090
|
+
function withoutSyncFingerprintMetadata(metadata) {
|
|
4091
|
+
const { [TODO_SYNC_FINGERPRINT_KEY]: _fingerprint, ...rest } = metadata;
|
|
4092
|
+
return rest;
|
|
4093
|
+
}
|
|
4094
|
+
function syncFingerprint(record) {
|
|
4095
|
+
const metadata = withoutSyncFingerprintMetadata(record.metadata || {});
|
|
4096
|
+
const canonical = canonicalize({ ...record, metadata });
|
|
4097
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(canonical)).digest("hex")}`;
|
|
4098
|
+
}
|
|
4099
|
+
function withSyncFingerprint(record) {
|
|
4100
|
+
const metadata = withoutSyncFingerprintMetadata(record.metadata);
|
|
4101
|
+
return {
|
|
4102
|
+
...record,
|
|
4103
|
+
metadata: {
|
|
4104
|
+
...metadata,
|
|
4105
|
+
[TODO_SYNC_FINGERPRINT_KEY]: syncFingerprint({ ...record, metadata })
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
4109
|
+
function hasSyncFingerprintChanged(record) {
|
|
4110
|
+
const stored = record.metadata?.[TODO_SYNC_FINGERPRINT_KEY];
|
|
4111
|
+
if (typeof stored !== "string" || stored.length === 0)
|
|
4112
|
+
return null;
|
|
4113
|
+
return stored !== syncFingerprint(record);
|
|
4114
|
+
}
|
|
4115
|
+
var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
|
|
4116
|
+
var init_sync_utils = __esm(() => {
|
|
4117
|
+
HOME = getHomeDir();
|
|
4118
|
+
});
|
|
4119
|
+
|
|
4023
4120
|
// src/storage/shadow-outbox-schema.ts
|
|
4024
4121
|
var exports_shadow_outbox_schema = {};
|
|
4025
4122
|
__export(exports_shadow_outbox_schema, {
|
|
@@ -4120,8 +4217,8 @@ __export(exports_database, {
|
|
|
4120
4217
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
4121
4218
|
});
|
|
4122
4219
|
import { Database } from "bun:sqlite";
|
|
4123
|
-
import { existsSync as
|
|
4124
|
-
import { dirname, join, resolve as resolve2 } from "path";
|
|
4220
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
4221
|
+
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
4125
4222
|
function isInMemoryDb(path) {
|
|
4126
4223
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
4127
4224
|
}
|
|
@@ -4130,8 +4227,8 @@ function findNearestProjectDb(startDir) {
|
|
|
4130
4227
|
const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
|
|
4131
4228
|
let dir = resolve2(startDir);
|
|
4132
4229
|
while (true) {
|
|
4133
|
-
const candidate =
|
|
4134
|
-
if (
|
|
4230
|
+
const candidate = join2(dir, ".hasna", "todos", "todos.db");
|
|
4231
|
+
if (existsSync3(candidate))
|
|
4135
4232
|
return candidate;
|
|
4136
4233
|
if (dir === stopAt)
|
|
4137
4234
|
break;
|
|
@@ -4145,7 +4242,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4145
4242
|
function findGitRoot(startDir) {
|
|
4146
4243
|
let dir = resolve2(startDir);
|
|
4147
4244
|
while (true) {
|
|
4148
|
-
if (
|
|
4245
|
+
if (existsSync3(join2(dir, ".git")))
|
|
4149
4246
|
return dir;
|
|
4150
4247
|
const parent = dirname(dir);
|
|
4151
4248
|
if (parent === dir)
|
|
@@ -4155,8 +4252,7 @@ function findGitRoot(startDir) {
|
|
|
4155
4252
|
return null;
|
|
4156
4253
|
}
|
|
4157
4254
|
function getGlobalDbPath() {
|
|
4158
|
-
|
|
4159
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4255
|
+
return join2(getHomeDir(), ".hasna", "todos", "todos.db");
|
|
4160
4256
|
}
|
|
4161
4257
|
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4162
4258
|
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
@@ -4194,7 +4290,7 @@ function getDbPath() {
|
|
|
4194
4290
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4195
4291
|
const gitRoot = findGitRoot(cwd);
|
|
4196
4292
|
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4197
|
-
return
|
|
4293
|
+
return join2(gitRoot, ".hasna", "todos", "todos.db");
|
|
4198
4294
|
}
|
|
4199
4295
|
}
|
|
4200
4296
|
return getGlobalDbPath();
|
|
@@ -4202,16 +4298,16 @@ function getDbPath() {
|
|
|
4202
4298
|
function getDatabasePath() {
|
|
4203
4299
|
return getDbPath();
|
|
4204
4300
|
}
|
|
4205
|
-
function
|
|
4301
|
+
function ensureDir2(filePath) {
|
|
4206
4302
|
if (isInMemoryDb(filePath))
|
|
4207
4303
|
return;
|
|
4208
4304
|
const dir = dirname(resolve2(filePath));
|
|
4209
|
-
if (!
|
|
4210
|
-
|
|
4305
|
+
if (!existsSync3(dir)) {
|
|
4306
|
+
mkdirSync2(dir, { recursive: true });
|
|
4211
4307
|
}
|
|
4212
4308
|
}
|
|
4213
4309
|
function openDatabase(path) {
|
|
4214
|
-
|
|
4310
|
+
ensureDir2(path);
|
|
4215
4311
|
const db = new Database(path);
|
|
4216
4312
|
db.run("PRAGMA busy_timeout = 5000");
|
|
4217
4313
|
db.run("PRAGMA journal_mode = WAL");
|
|
@@ -4378,105 +4474,10 @@ var init_database = __esm(() => {
|
|
|
4378
4474
|
init_machines();
|
|
4379
4475
|
init_identity_mapping();
|
|
4380
4476
|
init_types();
|
|
4477
|
+
init_sync_utils();
|
|
4381
4478
|
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
|
|
4382
4479
|
});
|
|
4383
4480
|
|
|
4384
|
-
// src/lib/sync-utils.ts
|
|
4385
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
4386
|
-
import { createHash } from "crypto";
|
|
4387
|
-
import { join as join2 } from "path";
|
|
4388
|
-
function getHomeDir() {
|
|
4389
|
-
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4390
|
-
}
|
|
4391
|
-
function getTodosGlobalDir() {
|
|
4392
|
-
return join2(getHomeDir(), ".hasna", "todos");
|
|
4393
|
-
}
|
|
4394
|
-
function ensureDir2(dir) {
|
|
4395
|
-
if (!existsSync3(dir))
|
|
4396
|
-
mkdirSync2(dir, { recursive: true });
|
|
4397
|
-
}
|
|
4398
|
-
function listJsonFiles(dir) {
|
|
4399
|
-
if (!existsSync3(dir))
|
|
4400
|
-
return [];
|
|
4401
|
-
return readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
4402
|
-
}
|
|
4403
|
-
function readJsonFile(path) {
|
|
4404
|
-
try {
|
|
4405
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
4406
|
-
} catch {
|
|
4407
|
-
return null;
|
|
4408
|
-
}
|
|
4409
|
-
}
|
|
4410
|
-
function writeJsonFile(path, data) {
|
|
4411
|
-
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
4412
|
-
`);
|
|
4413
|
-
}
|
|
4414
|
-
function readHighWaterMark(dir) {
|
|
4415
|
-
const path = join2(dir, ".highwatermark");
|
|
4416
|
-
if (!existsSync3(path))
|
|
4417
|
-
return 1;
|
|
4418
|
-
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
4419
|
-
return isNaN(val) ? 1 : val;
|
|
4420
|
-
}
|
|
4421
|
-
function writeHighWaterMark(dir, value) {
|
|
4422
|
-
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
4423
|
-
}
|
|
4424
|
-
function getFileMtimeMs(path) {
|
|
4425
|
-
try {
|
|
4426
|
-
return statSync(path).mtimeMs;
|
|
4427
|
-
} catch {
|
|
4428
|
-
return null;
|
|
4429
|
-
}
|
|
4430
|
-
}
|
|
4431
|
-
function parseTimestamp(value) {
|
|
4432
|
-
if (typeof value !== "string")
|
|
4433
|
-
return null;
|
|
4434
|
-
const parsed = Date.parse(value);
|
|
4435
|
-
return Number.isNaN(parsed) ? null : parsed;
|
|
4436
|
-
}
|
|
4437
|
-
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
4438
|
-
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
4439
|
-
const next = [conflict, ...current].slice(0, limit);
|
|
4440
|
-
return { ...metadata, sync_conflicts: next };
|
|
4441
|
-
}
|
|
4442
|
-
function canonicalize(value) {
|
|
4443
|
-
if (Array.isArray(value))
|
|
4444
|
-
return value.map(canonicalize);
|
|
4445
|
-
if (!value || typeof value !== "object")
|
|
4446
|
-
return value;
|
|
4447
|
-
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).sort(([a], [b]) => a.localeCompare(b));
|
|
4448
|
-
return Object.fromEntries(entries.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
|
|
4449
|
-
}
|
|
4450
|
-
function withoutSyncFingerprintMetadata(metadata) {
|
|
4451
|
-
const { [TODO_SYNC_FINGERPRINT_KEY]: _fingerprint, ...rest } = metadata;
|
|
4452
|
-
return rest;
|
|
4453
|
-
}
|
|
4454
|
-
function syncFingerprint(record) {
|
|
4455
|
-
const metadata = withoutSyncFingerprintMetadata(record.metadata || {});
|
|
4456
|
-
const canonical = canonicalize({ ...record, metadata });
|
|
4457
|
-
return `sha256:${createHash("sha256").update(JSON.stringify(canonical)).digest("hex")}`;
|
|
4458
|
-
}
|
|
4459
|
-
function withSyncFingerprint(record) {
|
|
4460
|
-
const metadata = withoutSyncFingerprintMetadata(record.metadata);
|
|
4461
|
-
return {
|
|
4462
|
-
...record,
|
|
4463
|
-
metadata: {
|
|
4464
|
-
...metadata,
|
|
4465
|
-
[TODO_SYNC_FINGERPRINT_KEY]: syncFingerprint({ ...record, metadata })
|
|
4466
|
-
}
|
|
4467
|
-
};
|
|
4468
|
-
}
|
|
4469
|
-
function hasSyncFingerprintChanged(record) {
|
|
4470
|
-
const stored = record.metadata?.[TODO_SYNC_FINGERPRINT_KEY];
|
|
4471
|
-
if (typeof stored !== "string" || stored.length === 0)
|
|
4472
|
-
return null;
|
|
4473
|
-
return stored !== syncFingerprint(record);
|
|
4474
|
-
}
|
|
4475
|
-
var TODO_SYNC_FINGERPRINT_KEY = "todos_sync_fingerprint", HOME;
|
|
4476
|
-
var init_sync_utils = __esm(() => {
|
|
4477
|
-
HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4478
|
-
});
|
|
4479
|
-
|
|
4480
4481
|
// src/lib/config.ts
|
|
4481
4482
|
import { existsSync as existsSync4 } from "fs";
|
|
4482
4483
|
import { dirname as dirname2, join as join3 } from "path";
|
|
@@ -4502,7 +4503,7 @@ function loadConfig() {
|
|
|
4502
4503
|
}
|
|
4503
4504
|
function saveConfig(config) {
|
|
4504
4505
|
const configPath = getConfigPath();
|
|
4505
|
-
|
|
4506
|
+
ensureDir(dirname2(configPath));
|
|
4506
4507
|
writeJsonFile(configPath, config);
|
|
4507
4508
|
cached = config;
|
|
4508
4509
|
return config;
|
|
@@ -5847,7 +5848,7 @@ var init_event_hooks = __esm(() => {
|
|
|
5847
5848
|
// node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
|
|
5848
5849
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
5849
5850
|
import { existsSync as existsSync5 } from "fs";
|
|
5850
|
-
import { homedir } from "os";
|
|
5851
|
+
import { homedir as homedir2 } from "os";
|
|
5851
5852
|
import { join as join4 } from "path";
|
|
5852
5853
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
5853
5854
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -5949,7 +5950,7 @@ function channelMatchesEvent(channel, event) {
|
|
|
5949
5950
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
5950
5951
|
}
|
|
5951
5952
|
function getEventsDataDir(override) {
|
|
5952
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(
|
|
5953
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir2(), ".hasna", "events");
|
|
5953
5954
|
}
|
|
5954
5955
|
|
|
5955
5956
|
class JsonEventsStore {
|