@hasna/todos 0.15.52 → 0.17.0
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/CHANGELOG.md +1993 -0
- package/README.md +356 -42
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/cli/cloud-router.d.ts +162 -72
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/delegate.d.ts.map +1 -1
- package/dist/cli/commands/environment-snapshots.d.ts.map +1 -1
- package/dist/cli/commands/machines-api.d.ts +4 -0
- package/dist/cli/commands/machines-api.d.ts.map +1 -0
- package/dist/cli/commands/machines.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-commands.d.ts +3 -0
- package/dist/cli/commands/plan-commands.d.ts.map +1 -0
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/pr-group-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/storage-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-list-commands.d.ts +3 -0
- package/dist/cli/commands/task-list-commands.d.ts.map +1 -0
- package/dist/cli/commands/template-commands.d.ts +3 -0
- package/dist/cli/commands/template-commands.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +22072 -15949
- package/dist/cli/machine-api.d.ts +7 -0
- package/dist/cli/machine-api.d.ts.map +1 -0
- package/dist/cli/plan-read-api.d.ts +6 -0
- package/dist/cli/plan-read-api.d.ts.map +1 -0
- package/dist/cli/project-api.d.ts +119 -0
- package/dist/cli/project-api.d.ts.map +1 -0
- package/dist/cli/stage-a.d.ts +25 -5
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/cli/task-query-api.d.ts +21 -0
- package/dist/cli/task-query-api.d.ts.map +1 -0
- package/dist/cli/template-api.d.ts +7 -0
- package/dist/cli/template-api.d.ts.map +1 -0
- package/dist/cli/template-remote.d.ts +90 -0
- package/dist/cli/template-remote.d.ts.map +1 -0
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +908 -291
- package/dist/db/agents.d.ts +8 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/db/builtin-templates.d.ts +2 -33
- package/dist/db/builtin-templates.d.ts.map +1 -1
- package/dist/db/database.d.ts +6 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/identity-mapping.d.ts +25 -0
- package/dist/db/identity-mapping.d.ts.map +1 -1
- package/dist/db/machines.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/retrospectives.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +2 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4341 -1756
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/builtin-template-library.d.ts +32 -0
- package/dist/lib/builtin-template-library.d.ts.map +1 -0
- package/dist/lib/cli-reference.d.ts.map +1 -1
- package/dist/lib/config.d.ts +10 -13
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/db-backup.d.ts +1 -1
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/env.d.ts +18 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/environment-snapshots.d.ts.map +1 -1
- package/dist/lib/feature-manifest.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/headless-boundaries.d.ts +4 -3
- package/dist/lib/headless-boundaries.d.ts.map +1 -1
- package/dist/lib/local-encryption.d.ts +12 -0
- package/dist/lib/local-encryption.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +104 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +20 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/plan-artifacts.d.ts +1 -0
- package/dist/lib/plan-artifacts.d.ts.map +1 -1
- package/dist/lib/plan-client-boundary.d.ts +2 -0
- package/dist/lib/plan-client-boundary.d.ts.map +1 -0
- package/dist/lib/plan-schedule.d.ts +6 -0
- package/dist/lib/plan-schedule.d.ts.map +1 -0
- package/dist/lib/project-panel.d.ts +6 -0
- package/dist/lib/project-panel.d.ts.map +1 -1
- package/dist/lib/project-receipt.d.ts +3 -0
- package/dist/lib/project-receipt.d.ts.map +1 -0
- package/dist/lib/sandbox-profiles.d.ts.map +1 -1
- package/dist/lib/shared-plan-artifacts.d.ts +14 -0
- package/dist/lib/shared-plan-artifacts.d.ts.map +1 -0
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/lib/task-list-client-boundary.d.ts +3 -0
- package/dist/lib/task-list-client-boundary.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/lib/template-client-boundary.d.ts +2 -0
- package/dist/lib/template-client-boundary.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +53 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +50124 -60838
- package/dist/mcp/plan-api.d.ts +6 -0
- package/dist/mcp/plan-api.d.ts.map +1 -0
- package/dist/mcp/remote-authority.d.ts +49 -0
- package/dist/mcp/remote-authority.d.ts.map +1 -0
- package/dist/mcp/task-coordination-api.d.ts +15 -0
- package/dist/mcp/task-coordination-api.d.ts.map +1 -0
- package/dist/mcp/task-list-api.d.ts +8 -0
- package/dist/mcp/task-list-api.d.ts.map +1 -0
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -1
- package/dist/mcp/tools/machines.d.ts.map +1 -1
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp/tools/templates.d.ts.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +45 -19
- package/dist/project-registration.js +3349 -645
- package/dist/registry.js +909 -292
- package/dist/release-provenance.json +6 -6
- package/dist/sdk/client.d.ts +53 -4
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +1253 -1088
- package/dist/sdk/resolve.d.ts +101 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/types.d.ts +17 -2
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +334 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/auth-posture.d.ts +66 -7
- package/dist/server/auth-posture.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +2 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +24758 -16687
- package/dist/server/openapi.d.ts +2514 -469
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/port.d.ts +2 -2
- package/dist/server/port.d.ts.map +1 -1
- package/dist/server/rate-limit-config.d.ts +6 -0
- package/dist/server/rate-limit-config.d.ts.map +1 -0
- package/dist/server/routes.d.ts +3 -6
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts +2 -6
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts +3 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/atomic-project-migration.d.ts +53 -0
- package/dist/storage/atomic-project-migration.d.ts.map +1 -0
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +135 -3
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/machine-registry.d.ts +36 -0
- package/dist/storage/machine-registry.d.ts.map +1 -0
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +2 -2
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/run-artifact-creation-upload.d.ts +50 -0
- package/dist/storage/run-artifact-creation-upload.d.ts.map +1 -0
- package/dist/storage/s3-artifact-sync.d.ts +18 -0
- package/dist/storage/s3-artifact-sync.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts +2 -0
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +3154 -350
- package/dist/task-manifest/postgres.d.ts +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -1
- package/dist/task-manifest.js +250 -73
- package/dist/task-subtree-transfer.js +198 -4
- package/dist/testing.d.ts +90 -29
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +34 -10
- package/dist/types/index.d.ts +43 -4
- package/dist/types/index.d.ts.map +1 -1
- package/docs/PLAN_API.md +93 -0
- package/docs/TASK_LIST_API.md +58 -0
- package/docs/TASK_QUERY_API.md +14 -0
- package/docs/TEMPLATE_API.md +18 -0
- package/docs/native-storage.md +405 -0
- package/package.json +18 -15
- package/postinstall.js +52 -1
- package/dashboard/dist/assets/index-BNQ4gJua.js +0 -342
- package/dashboard/dist/assets/index-DjzvHUWt.css +0 -1
- package/dashboard/dist/index.html +0 -13
- package/dashboard/dist/logo.jpg +0 -0
- package/dist/mcp/tools/code-tools.d.ts +0 -15
- package/dist/mcp/tools/code-tools.d.ts.map +0 -1
package/dist/registry.js
CHANGED
|
@@ -39,6 +39,84 @@ var __export = (target, all) => {
|
|
|
39
39
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
40
40
|
var __require = import.meta.require;
|
|
41
41
|
|
|
42
|
+
// src/lib/env.ts
|
|
43
|
+
var alias = (canonical, legacy) => process.env[canonical] ?? process.env[legacy], env;
|
|
44
|
+
var init_env = __esm(() => {
|
|
45
|
+
env = {
|
|
46
|
+
profile: () => alias("HASNA_TODOS_PROFILE", "TODOS_PROFILE"),
|
|
47
|
+
toolGroups: () => alias("HASNA_TODOS_TOOL_GROUPS", "TODOS_TOOL_GROUPS"),
|
|
48
|
+
machineId: () => alias("HASNA_TODOS_MACHINE_ID", "TODOS_MACHINE_ID"),
|
|
49
|
+
machineName: () => alias("HASNA_TODOS_MACHINE_NAME", "TODOS_MACHINE_NAME"),
|
|
50
|
+
rateLimitMax: () => alias("HASNA_TODOS_RATE_LIMIT_MAX", "TODOS_RATE_LIMIT_MAX"),
|
|
51
|
+
trustProxy: () => alias("HASNA_TODOS_TRUST_PROXY", "TODOS_TRUST_PROXY"),
|
|
52
|
+
autoProject: () => alias("HASNA_TODOS_AUTO_PROJECT", "TODOS_AUTO_PROJECT"),
|
|
53
|
+
aiFormat: () => alias("HASNA_TODOS_AI_FORMAT", "TODOS_AI_FORMAT"),
|
|
54
|
+
syncAgents: () => alias("HASNA_TODOS_SYNC_AGENTS", "TODOS_SYNC_AGENTS"),
|
|
55
|
+
taskListId: () => alias("HASNA_TODOS_TASK_LIST_ID", "TODOS_TASK_LIST_ID"),
|
|
56
|
+
claudeTaskList: () => alias("HASNA_TODOS_CLAUDE_TASK_LIST", "TODOS_CLAUDE_TASK_LIST"),
|
|
57
|
+
sandboxProfilesPath: () => alias("HASNA_TODOS_SANDBOX_PROFILES_PATH", "TODOS_SANDBOX_PROFILES_PATH"),
|
|
58
|
+
seatRosterPath: () => alias("HASNA_TODOS_SEAT_ROSTER_PATH", "TODOS_SEAT_ROSTER_PATH"),
|
|
59
|
+
delegateNoticeChannel: () => alias("HASNA_TODOS_DELEGATE_NOTICE_CHANNEL", "TODOS_DELEGATE_NOTICE_CHANNEL"),
|
|
60
|
+
delegateNotifyBin: () => alias("HASNA_TODOS_DELEGATE_NOTIFY_BIN", "TODOS_DELEGATE_NOTIFY_BIN")
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// src/lib/local-opt-in.ts
|
|
65
|
+
import {
|
|
66
|
+
clientTransportEnvKeys,
|
|
67
|
+
credentialOverrideEnvKey,
|
|
68
|
+
credentialPointerEnvKey,
|
|
69
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
70
|
+
} from "@hasna/contracts/client";
|
|
71
|
+
function isTodosLocalOptIn(env2 = process.env) {
|
|
72
|
+
return TODOS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env2[key] ?? "").trim() !== "");
|
|
73
|
+
}
|
|
74
|
+
function todosAuthorityEnvKeys() {
|
|
75
|
+
const keys = clientTransportEnvKeys("todos");
|
|
76
|
+
return [
|
|
77
|
+
...keys.apiUrlKeys,
|
|
78
|
+
...keys.apiKeyKeys,
|
|
79
|
+
credentialOverrideEnvKey("todos"),
|
|
80
|
+
credentialPointerEnvKey("todos"),
|
|
81
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
function hasTodosEnvAuthorityIntent(env2 = process.env) {
|
|
85
|
+
return todosAuthorityEnvKeys().some((key) => (env2[key] ?? "").trim() !== "");
|
|
86
|
+
}
|
|
87
|
+
function selectsTodosLocalStore(env2 = process.env) {
|
|
88
|
+
return !hasTodosEnvAuthorityIntent(env2) && isTodosLocalOptIn(env2);
|
|
89
|
+
}
|
|
90
|
+
function todosResolverEnv(env2) {
|
|
91
|
+
const blanks = todosAuthorityEnvKeys().filter((key) => (key in env2) && (env2[key] ?? "").trim() === "");
|
|
92
|
+
if (blanks.length === 0)
|
|
93
|
+
return env2;
|
|
94
|
+
const next = { ...env2 };
|
|
95
|
+
for (const key of blanks)
|
|
96
|
+
delete next[key];
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
function isAmbientTodosEnv(env2) {
|
|
100
|
+
if (typeof process !== "undefined" && env2 === process.env)
|
|
101
|
+
return true;
|
|
102
|
+
return env2[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
103
|
+
}
|
|
104
|
+
function todosResolverInputs(env2, credentials = {}) {
|
|
105
|
+
const normalised = todosResolverEnv(env2);
|
|
106
|
+
if (normalised === env2)
|
|
107
|
+
return { env: normalised, credentials };
|
|
108
|
+
const keychain = { ...credentials.keychain };
|
|
109
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
110
|
+
keychain.enabled = isAmbientTodosEnv(env2);
|
|
111
|
+
}
|
|
112
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
113
|
+
}
|
|
114
|
+
var TODOS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
|
|
115
|
+
var init_local_opt_in = __esm(() => {
|
|
116
|
+
TODOS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_TODOS_LOCAL", "TODOS_LOCAL"];
|
|
117
|
+
CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
118
|
+
});
|
|
119
|
+
|
|
42
120
|
// src/project-registration/schema.ts
|
|
43
121
|
function sqliteTodosProjectRegistrationSchemaSql() {
|
|
44
122
|
return `
|
|
@@ -1608,16 +1686,18 @@ var init_migrations = __esm(() => {
|
|
|
1608
1686
|
SET leaf_task_id = COALESCE(leaf_task_id, (
|
|
1609
1687
|
SELECT attempt.leaf_task_id
|
|
1610
1688
|
FROM pr_group_attempts AS attempt
|
|
1689
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
1611
1690
|
WHERE attempt.group_id = pr_groups.id
|
|
1612
|
-
ORDER BY CASE WHEN attempt.id =
|
|
1691
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
1613
1692
|
attempt.created_at ASC, attempt.id ASC
|
|
1614
1693
|
LIMIT 1
|
|
1615
1694
|
)),
|
|
1616
1695
|
branch = COALESCE(branch, (
|
|
1617
1696
|
SELECT attempt.branch
|
|
1618
1697
|
FROM pr_group_attempts AS attempt
|
|
1698
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
1619
1699
|
WHERE attempt.group_id = pr_groups.id
|
|
1620
|
-
ORDER BY CASE WHEN attempt.id =
|
|
1700
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
1621
1701
|
attempt.created_at ASC, attempt.id ASC
|
|
1622
1702
|
LIMIT 1
|
|
1623
1703
|
));
|
|
@@ -1640,16 +1720,18 @@ var init_migrations = __esm(() => {
|
|
|
1640
1720
|
SET pr_number = COALESCE(pr_number, (
|
|
1641
1721
|
SELECT attempt.pr_number
|
|
1642
1722
|
FROM pr_group_attempts AS attempt
|
|
1723
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
1643
1724
|
WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
|
|
1644
|
-
ORDER BY CASE WHEN attempt.id =
|
|
1725
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
1645
1726
|
attempt.created_at ASC, attempt.id ASC
|
|
1646
1727
|
LIMIT 1
|
|
1647
1728
|
)),
|
|
1648
1729
|
base_sha = COALESCE(base_sha, (
|
|
1649
1730
|
SELECT attempt.base_sha
|
|
1650
1731
|
FROM pr_group_attempts AS attempt
|
|
1732
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
1651
1733
|
WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
|
|
1652
|
-
ORDER BY CASE WHEN attempt.id =
|
|
1734
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
1653
1735
|
attempt.created_at ASC, attempt.id ASC
|
|
1654
1736
|
LIMIT 1
|
|
1655
1737
|
));
|
|
@@ -1688,7 +1770,7 @@ var init_migrations = __esm(() => {
|
|
|
1688
1770
|
));
|
|
1689
1771
|
|
|
1690
1772
|
INSERT OR IGNORE INTO _migrations (id)
|
|
1691
|
-
SELECT
|
|
1773
|
+
SELECT 67
|
|
1692
1774
|
WHERE NOT EXISTS (
|
|
1693
1775
|
SELECT 1 FROM pr_groups WHERE leaf_task_id IS NULL OR branch IS NULL
|
|
1694
1776
|
)
|
|
@@ -1893,32 +1975,52 @@ var init_migrations = __esm(() => {
|
|
|
1893
1975
|
});
|
|
1894
1976
|
|
|
1895
1977
|
// src/db/schema.ts
|
|
1978
|
+
function runPrGroupLineageMigration(db, migration, rebuild) {
|
|
1979
|
+
if (db.inTransaction)
|
|
1980
|
+
throw new Error("PR-group migration requires its own transaction");
|
|
1981
|
+
const priorForeignKeys = db.query("PRAGMA foreign_keys").get().foreign_keys;
|
|
1982
|
+
try {
|
|
1983
|
+
if (rebuild)
|
|
1984
|
+
db.exec("PRAGMA foreign_keys = OFF");
|
|
1985
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1986
|
+
for (const statement of migration.split(/;\s*(?:\n|$)/).map((value) => value.trim()).filter(Boolean)) {
|
|
1987
|
+
if (/^(?:PRAGMA foreign_keys\s*=|BEGIN IMMEDIATE$|COMMIT$)/i.test(statement))
|
|
1988
|
+
continue;
|
|
1989
|
+
db.run(statement);
|
|
1990
|
+
}
|
|
1991
|
+
const completed = db.query("SELECT id FROM _migrations WHERE id = ?").get(rebuild ? 68 : 67);
|
|
1992
|
+
if (!completed)
|
|
1993
|
+
throw new Error("PR-group lineage backfill is incomplete; migration rolled back");
|
|
1994
|
+
if (rebuild && db.query("PRAGMA foreign_key_check").all().length) {
|
|
1995
|
+
throw new Error("PR-group migration failed foreign-key integrity verification");
|
|
1996
|
+
}
|
|
1997
|
+
db.exec("COMMIT");
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
if (db.inTransaction)
|
|
2000
|
+
db.exec("ROLLBACK");
|
|
2001
|
+
throw error;
|
|
2002
|
+
} finally {
|
|
2003
|
+
db.exec(`PRAGMA foreign_keys = ${priorForeignKeys ? "ON" : "OFF"}`);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
1896
2006
|
function runMigrations(db) {
|
|
1897
|
-
let
|
|
1898
|
-
|
|
2007
|
+
let currentLevel = 0;
|
|
2008
|
+
try {
|
|
2009
|
+
currentLevel = db.query("SELECT MAX(id) as max_id FROM _migrations").get()?.max_id ?? 0;
|
|
2010
|
+
} catch {}
|
|
2011
|
+
for (let index = currentLevel;index < MIGRATIONS.length; index++) {
|
|
1899
2012
|
try {
|
|
1900
|
-
|
|
2013
|
+
if (index === 66 || index === 67)
|
|
2014
|
+
runPrGroupLineageMigration(db, MIGRATIONS[index], index === 67);
|
|
2015
|
+
else
|
|
2016
|
+
db.exec(MIGRATIONS[index]);
|
|
1901
2017
|
} catch (error) {
|
|
1902
|
-
|
|
2018
|
+
if (db.inTransaction)
|
|
1903
2019
|
db.exec("ROLLBACK");
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
strictMigrationFailure = error;
|
|
1907
|
-
}
|
|
1908
|
-
};
|
|
1909
|
-
try {
|
|
1910
|
-
const result = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
1911
|
-
const currentLevel = result?.max_id ?? 0;
|
|
1912
|
-
for (let i = currentLevel;i < MIGRATIONS.length; i++) {
|
|
1913
|
-
executeMigration(MIGRATIONS[i], i);
|
|
1914
|
-
}
|
|
1915
|
-
} catch {
|
|
1916
|
-
for (const [index, migration] of MIGRATIONS.entries()) {
|
|
1917
|
-
executeMigration(migration, index);
|
|
2020
|
+
if (index + 1 >= 67)
|
|
2021
|
+
throw error;
|
|
1918
2022
|
}
|
|
1919
2023
|
}
|
|
1920
|
-
if (strictMigrationFailure)
|
|
1921
|
-
throw strictMigrationFailure;
|
|
1922
2024
|
ensureSchema(db);
|
|
1923
2025
|
}
|
|
1924
2026
|
function planSlugBase(value) {
|
|
@@ -3230,16 +3332,18 @@ function ensureSchema(db) {
|
|
|
3230
3332
|
SET leaf_task_id = COALESCE(leaf_task_id, (
|
|
3231
3333
|
SELECT attempt.leaf_task_id
|
|
3232
3334
|
FROM pr_group_attempts AS attempt
|
|
3335
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
3233
3336
|
WHERE attempt.group_id = pr_groups.id
|
|
3234
|
-
ORDER BY CASE WHEN attempt.id =
|
|
3337
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
3235
3338
|
attempt.created_at ASC, attempt.id ASC
|
|
3236
3339
|
LIMIT 1
|
|
3237
3340
|
)),
|
|
3238
3341
|
branch = COALESCE(branch, (
|
|
3239
3342
|
SELECT attempt.branch
|
|
3240
3343
|
FROM pr_group_attempts AS attempt
|
|
3344
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
3241
3345
|
WHERE attempt.group_id = pr_groups.id
|
|
3242
|
-
ORDER BY CASE WHEN attempt.id =
|
|
3346
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
3243
3347
|
attempt.created_at ASC, attempt.id ASC
|
|
3244
3348
|
LIMIT 1
|
|
3245
3349
|
));
|
|
@@ -3260,16 +3364,18 @@ function ensureSchema(db) {
|
|
|
3260
3364
|
SET pr_number = COALESCE(pr_number, (
|
|
3261
3365
|
SELECT attempt.pr_number
|
|
3262
3366
|
FROM pr_group_attempts AS attempt
|
|
3367
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
3263
3368
|
WHERE attempt.group_id = pr_groups.id AND attempt.pr_number IS NOT NULL
|
|
3264
|
-
ORDER BY CASE WHEN attempt.id =
|
|
3369
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
3265
3370
|
attempt.created_at ASC, attempt.id ASC
|
|
3266
3371
|
LIMIT 1
|
|
3267
3372
|
)),
|
|
3268
3373
|
base_sha = COALESCE(base_sha, (
|
|
3269
3374
|
SELECT attempt.base_sha
|
|
3270
3375
|
FROM pr_group_attempts AS attempt
|
|
3376
|
+
JOIN pr_groups AS owner ON owner.id = attempt.group_id
|
|
3271
3377
|
WHERE attempt.group_id = pr_groups.id AND attempt.base_sha IS NOT NULL
|
|
3272
|
-
ORDER BY CASE WHEN attempt.id =
|
|
3378
|
+
ORDER BY CASE WHEN attempt.id = owner.active_attempt_id THEN 0 ELSE 1 END,
|
|
3273
3379
|
attempt.created_at ASC, attempt.id ASC
|
|
3274
3380
|
LIMIT 1
|
|
3275
3381
|
));
|
|
@@ -3443,7 +3549,7 @@ function extractTopology(machine) {
|
|
|
3443
3549
|
}
|
|
3444
3550
|
function getOrCreateLocalMachine(db) {
|
|
3445
3551
|
const d = db || getDatabase();
|
|
3446
|
-
const name =
|
|
3552
|
+
const name = env.machineName() || osHostname();
|
|
3447
3553
|
const host = osHostname();
|
|
3448
3554
|
const plat = osPlatform();
|
|
3449
3555
|
const existing = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
@@ -3512,7 +3618,7 @@ function registerMachine(name, opts, db) {
|
|
|
3512
3618
|
}
|
|
3513
3619
|
function updateMachineHeartbeat(idOrName, opts = {}, db) {
|
|
3514
3620
|
const d = db || getDatabase();
|
|
3515
|
-
const key = idOrName ||
|
|
3621
|
+
const key = idOrName || env.machineName() || osHostname();
|
|
3516
3622
|
const row = d.query("SELECT * FROM machines WHERE id = ? OR name = ?").get(key, key);
|
|
3517
3623
|
if (!row) {
|
|
3518
3624
|
return registerMachine(key, {
|
|
@@ -3664,6 +3770,7 @@ function backfillMachineId(db, force = false) {
|
|
|
3664
3770
|
var _machineId = null, TABLES_WITH_MACHINE_ID;
|
|
3665
3771
|
var init_machines = __esm(() => {
|
|
3666
3772
|
init_database();
|
|
3773
|
+
init_env();
|
|
3667
3774
|
TABLES_WITH_MACHINE_ID = [
|
|
3668
3775
|
"projects",
|
|
3669
3776
|
"tasks",
|
|
@@ -3695,7 +3802,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
3695
3802
|
function isTerminalStatus(status) {
|
|
3696
3803
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
3697
3804
|
}
|
|
3698
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3805
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError, InputValidationError;
|
|
3699
3806
|
var init_types = __esm(() => {
|
|
3700
3807
|
TASK_STATUSES = [
|
|
3701
3808
|
"pending",
|
|
@@ -3710,7 +3817,7 @@ var init_types = __esm(() => {
|
|
|
3710
3817
|
"high",
|
|
3711
3818
|
"critical"
|
|
3712
3819
|
];
|
|
3713
|
-
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
3820
|
+
PLAN_STATUSES = ["active", "completed", "archived", "planning", "cancelled"];
|
|
3714
3821
|
VersionConflictError = class VersionConflictError extends Error {
|
|
3715
3822
|
taskId;
|
|
3716
3823
|
expectedVersion;
|
|
@@ -3899,6 +4006,16 @@ var init_types = __esm(() => {
|
|
|
3899
4006
|
this.name = "DispatchNotFoundError";
|
|
3900
4007
|
}
|
|
3901
4008
|
};
|
|
4009
|
+
InputValidationError = class InputValidationError extends Error {
|
|
4010
|
+
static code = "INVALID_INPUT";
|
|
4011
|
+
code = InputValidationError.code;
|
|
4012
|
+
suggestion;
|
|
4013
|
+
constructor(message, suggestion) {
|
|
4014
|
+
super(message);
|
|
4015
|
+
this.name = "InputValidationError";
|
|
4016
|
+
this.suggestion = suggestion;
|
|
4017
|
+
}
|
|
4018
|
+
};
|
|
3902
4019
|
});
|
|
3903
4020
|
|
|
3904
4021
|
// src/db/identity-mapping.ts
|
|
@@ -4000,20 +4117,17 @@ function ensureAgentIdentitySchema(db) {
|
|
|
4000
4117
|
SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
|
|
4001
4118
|
END;
|
|
4002
4119
|
|
|
4120
|
+
-- Drop first so an older definition is replaced rather than silently
|
|
4121
|
+
-- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
|
|
4122
|
+
-- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
|
|
4123
|
+
-- do different jobs and both are kept: the DROP is what refreshes a
|
|
4124
|
+
-- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
|
|
4125
|
+
-- on its own \u2014 so a caller that reaches it without a preceding DROP, in
|
|
4126
|
+
-- its own connection or transaction, gets a no-op instead of
|
|
4127
|
+
-- "trigger ... already exists".
|
|
4003
4128
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
|
|
4004
|
-
CREATE TRIGGER trg_agent_identity_mapping_history_immutable
|
|
4005
|
-
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
4006
|
-
ON agent_identity_source_mappings
|
|
4007
|
-
BEGIN
|
|
4008
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4009
|
-
END;
|
|
4010
|
-
|
|
4011
4129
|
DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
|
|
4012
|
-
|
|
4013
|
-
BEFORE DELETE ON agent_identity_source_mappings
|
|
4014
|
-
BEGIN
|
|
4015
|
-
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4016
|
-
END;
|
|
4130
|
+
${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
|
|
4017
4131
|
`);
|
|
4018
4132
|
db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
|
|
4019
4133
|
});
|
|
@@ -4053,7 +4167,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
|
|
|
4053
4167
|
function recordAgentAlias(agentId, label, db) {
|
|
4054
4168
|
return storeAgentAlias(agentId, label, "historical", "active", db);
|
|
4055
4169
|
}
|
|
4056
|
-
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65
|
|
4170
|
+
var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
|
|
4171
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
|
|
4172
|
+
BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
|
|
4173
|
+
ON agent_identity_source_mappings
|
|
4174
|
+
BEGIN
|
|
4175
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4176
|
+
END;
|
|
4177
|
+
|
|
4178
|
+
CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
|
|
4179
|
+
BEFORE DELETE ON agent_identity_source_mappings
|
|
4180
|
+
BEGIN
|
|
4181
|
+
SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
|
|
4182
|
+
END;
|
|
4183
|
+
`;
|
|
4057
4184
|
var init_identity_mapping = __esm(() => {
|
|
4058
4185
|
init_types();
|
|
4059
4186
|
init_types();
|
|
@@ -4075,108 +4202,106 @@ var init_identity_mapping = __esm(() => {
|
|
|
4075
4202
|
});
|
|
4076
4203
|
});
|
|
4077
4204
|
|
|
4078
|
-
//
|
|
4205
|
+
// src/lib/paths.ts
|
|
4206
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4079
4207
|
import { homedir } from "os";
|
|
4080
|
-
import { join } from "path";
|
|
4081
|
-
|
|
4208
|
+
import { isAbsolute, join, resolve as resolve2 } from "path";
|
|
4209
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
4210
|
+
import { join as pathsResolverJoin } from "path";
|
|
4211
|
+
function pathsResolverAssertApp(app) {
|
|
4082
4212
|
if (typeof app !== "string" || app.length === 0) {
|
|
4083
4213
|
throw new TypeError("paths: app must be a non-empty string");
|
|
4084
4214
|
}
|
|
4085
|
-
if (!
|
|
4215
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
4086
4216
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
4087
4217
|
}
|
|
4088
4218
|
}
|
|
4089
|
-
function
|
|
4090
|
-
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
4094
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
4095
|
-
}
|
|
4096
|
-
function isMacOS(platform) {
|
|
4097
|
-
return platform === "darwin";
|
|
4219
|
+
function pathsResolverAssertKind(kind) {
|
|
4220
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
4221
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
4222
|
+
}
|
|
4098
4223
|
}
|
|
4099
|
-
function
|
|
4100
|
-
|
|
4101
|
-
|
|
4224
|
+
function pathsResolverBaseDir(kind, options) {
|
|
4225
|
+
pathsResolverAssertKind(kind);
|
|
4226
|
+
const env2 = options.env ?? process.env;
|
|
4227
|
+
const override = env2[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
4228
|
+
if (typeof override === "string" && override.length > 0)
|
|
4102
4229
|
return override;
|
|
4103
|
-
const home = options.home ??
|
|
4230
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
4104
4231
|
const platform = options.platform ?? process.platform;
|
|
4105
|
-
if (
|
|
4232
|
+
if (platform === "darwin") {
|
|
4106
4233
|
switch (kind) {
|
|
4107
4234
|
case "config":
|
|
4108
4235
|
case "data":
|
|
4109
|
-
return
|
|
4236
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
4110
4237
|
case "cache":
|
|
4111
|
-
return
|
|
4238
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
4112
4239
|
case "state":
|
|
4113
|
-
return
|
|
4240
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
4114
4241
|
}
|
|
4115
4242
|
}
|
|
4116
4243
|
switch (kind) {
|
|
4117
4244
|
case "config":
|
|
4118
|
-
return
|
|
4245
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
4119
4246
|
case "data":
|
|
4120
|
-
return
|
|
4247
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
4121
4248
|
case "state":
|
|
4122
|
-
return
|
|
4249
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
4123
4250
|
case "cache":
|
|
4124
|
-
return
|
|
4251
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
4125
4252
|
}
|
|
4126
4253
|
}
|
|
4127
|
-
function
|
|
4128
|
-
|
|
4129
|
-
const appSegment = options.internal === true ?
|
|
4130
|
-
return
|
|
4254
|
+
function pathsResolverResolve(kind, options) {
|
|
4255
|
+
pathsResolverAssertApp(options.app);
|
|
4256
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
4257
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
4131
4258
|
}
|
|
4132
4259
|
function dataDir(options) {
|
|
4133
|
-
return
|
|
4260
|
+
return pathsResolverResolve("data", options);
|
|
4134
4261
|
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4144
|
-
});
|
|
4145
|
-
|
|
4146
|
-
// src/lib/paths.ts
|
|
4147
|
-
import { existsSync as existsSync2 } from "fs";
|
|
4148
|
-
import { homedir as homedir2 } from "os";
|
|
4149
|
-
import { join as join2, resolve as resolve2 } from "path";
|
|
4150
|
-
function effectiveHome(env = process.env) {
|
|
4151
|
-
return env.HOME || env.USERPROFILE || homedir2();
|
|
4262
|
+
function effectiveHome(env2 = process.env) {
|
|
4263
|
+
return env2.HOME || env2.USERPROFILE || homedir();
|
|
4264
|
+
}
|
|
4265
|
+
function hasnaHomeRoot(env2 = process.env) {
|
|
4266
|
+
const override = env2["HASNA_HOME"]?.trim();
|
|
4267
|
+
if (override && isAbsolute(override))
|
|
4268
|
+
return override;
|
|
4269
|
+
return join(effectiveHome(env2), ".hasna");
|
|
4152
4270
|
}
|
|
4153
|
-
function legacyHomeDir(
|
|
4154
|
-
return
|
|
4271
|
+
function legacyHomeDir(env2 = process.env) {
|
|
4272
|
+
return join(hasnaHomeRoot(env2), "todos");
|
|
4155
4273
|
}
|
|
4156
|
-
function resolverHome(
|
|
4157
|
-
return dataDir({ app: "todos", home: effectiveHome(
|
|
4274
|
+
function resolverHome(env2 = process.env) {
|
|
4275
|
+
return dataDir({ app: "todos", home: effectiveHome(env2), env: env2 });
|
|
4158
4276
|
}
|
|
4159
|
-
function adoptResolverHome(resolved,
|
|
4160
|
-
const dataOverride =
|
|
4277
|
+
function adoptResolverHome(resolved, env2 = process.env) {
|
|
4278
|
+
const dataOverride = env2.HASNA_DATA_HOME;
|
|
4161
4279
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
4162
4280
|
return true;
|
|
4163
|
-
return existsSync2(
|
|
4281
|
+
return existsSync2(join(resolved, "todos.db")) || existsSync2(join(resolved, "config.json"));
|
|
4164
4282
|
}
|
|
4165
|
-
function getTodosDir(
|
|
4166
|
-
const resolved = resolverHome(
|
|
4167
|
-
return resolve2(adoptResolverHome(resolved,
|
|
4283
|
+
function getTodosDir(env2 = process.env) {
|
|
4284
|
+
const resolved = resolverHome(env2);
|
|
4285
|
+
return resolve2(adoptResolverHome(resolved, env2) ? resolved : legacyHomeDir(env2));
|
|
4168
4286
|
}
|
|
4287
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
4169
4288
|
var init_paths = __esm(() => {
|
|
4170
|
-
|
|
4289
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
4290
|
+
config: "HASNA_CONFIG_HOME",
|
|
4291
|
+
data: "HASNA_DATA_HOME",
|
|
4292
|
+
state: "HASNA_STATE_HOME",
|
|
4293
|
+
cache: "HASNA_CACHE_HOME"
|
|
4294
|
+
};
|
|
4295
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4171
4296
|
});
|
|
4172
4297
|
|
|
4173
4298
|
// src/lib/sync-utils.ts
|
|
4174
4299
|
import { existsSync as existsSync3, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
4175
4300
|
import { createHash } from "crypto";
|
|
4176
|
-
import { homedir as
|
|
4177
|
-
import { join as
|
|
4301
|
+
import { homedir as homedir2 } from "os";
|
|
4302
|
+
import { join as join2 } from "path";
|
|
4178
4303
|
function getHomeDir() {
|
|
4179
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
4304
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
4180
4305
|
}
|
|
4181
4306
|
function getTodosGlobalDir() {
|
|
4182
4307
|
return getTodosDir();
|
|
@@ -4202,14 +4327,14 @@ function writeJsonFile(path, data) {
|
|
|
4202
4327
|
`);
|
|
4203
4328
|
}
|
|
4204
4329
|
function readHighWaterMark(dir) {
|
|
4205
|
-
const path =
|
|
4330
|
+
const path = join2(dir, ".highwatermark");
|
|
4206
4331
|
if (!existsSync3(path))
|
|
4207
4332
|
return 1;
|
|
4208
4333
|
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
4209
4334
|
return isNaN(val) ? 1 : val;
|
|
4210
4335
|
}
|
|
4211
4336
|
function writeHighWaterMark(dir, value) {
|
|
4212
|
-
writeFileSync(
|
|
4337
|
+
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
4213
4338
|
}
|
|
4214
4339
|
function getFileMtimeMs(path) {
|
|
4215
4340
|
try {
|
|
@@ -4294,23 +4419,23 @@ __export(exports_config, {
|
|
|
4294
4419
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4295
4420
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
4296
4421
|
});
|
|
4297
|
-
function getCanonicalTodosRdsConfig(
|
|
4422
|
+
function getCanonicalTodosRdsConfig(env2 = process.env) {
|
|
4298
4423
|
return {
|
|
4299
|
-
cluster: clean(
|
|
4424
|
+
cluster: clean(env2[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
|
|
4300
4425
|
database: CANONICAL_TODOS_RDS_DATABASE,
|
|
4301
|
-
runtimeSecretPath: clean(
|
|
4426
|
+
runtimeSecretPath: clean(env2[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
|
|
4302
4427
|
primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
|
|
4303
4428
|
fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
|
|
4304
4429
|
};
|
|
4305
4430
|
}
|
|
4306
|
-
function loadTodosStorageConfig(
|
|
4307
|
-
const mode = getTodosStorageBackend(
|
|
4308
|
-
const databaseUrl = getTodosStorageDatabaseUrl(
|
|
4309
|
-
const bucket = readStorageEnv(
|
|
4310
|
-
const prefix = readStorageEnv(
|
|
4311
|
-
const region = readStorageEnv(
|
|
4312
|
-
const endpoint = readStorageEnv(
|
|
4313
|
-
const schema = readStorageEnv(
|
|
4431
|
+
function loadTodosStorageConfig(env2 = process.env) {
|
|
4432
|
+
const mode = getTodosStorageBackend(env2);
|
|
4433
|
+
const databaseUrl = getTodosStorageDatabaseUrl(env2);
|
|
4434
|
+
const bucket = readStorageEnv(env2, "s3Bucket").value;
|
|
4435
|
+
const prefix = readStorageEnv(env2, "s3Prefix").value ?? "todos/";
|
|
4436
|
+
const region = readStorageEnv(env2, "awsRegion").value;
|
|
4437
|
+
const endpoint = readStorageEnv(env2, "s3Endpoint").value;
|
|
4438
|
+
const schema = readStorageEnv(env2, "databaseSchema").value;
|
|
4314
4439
|
return {
|
|
4315
4440
|
service: "todos",
|
|
4316
4441
|
mode,
|
|
@@ -4318,7 +4443,7 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
4318
4443
|
database: {
|
|
4319
4444
|
provider: "postgres",
|
|
4320
4445
|
url: databaseUrl,
|
|
4321
|
-
ssl: parseBoolean(readStorageEnv(
|
|
4446
|
+
ssl: parseBoolean(readStorageEnv(env2, "databaseSsl").value, true),
|
|
4322
4447
|
...schema ? { schema } : {}
|
|
4323
4448
|
}
|
|
4324
4449
|
} : {},
|
|
@@ -4329,17 +4454,17 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
4329
4454
|
prefix,
|
|
4330
4455
|
...region ? { region } : {},
|
|
4331
4456
|
...endpoint ? { endpoint } : {},
|
|
4332
|
-
forcePathStyle: parseBoolean(readStorageEnv(
|
|
4457
|
+
forcePathStyle: parseBoolean(readStorageEnv(env2, "s3ForcePathStyle").value, false)
|
|
4333
4458
|
}
|
|
4334
4459
|
} : {},
|
|
4335
4460
|
sync: {
|
|
4336
|
-
batchSize: parsePositiveInteger(readStorageEnv(
|
|
4337
|
-
dryRun: parseBoolean(readStorageEnv(
|
|
4461
|
+
batchSize: parsePositiveInteger(readStorageEnv(env2, "syncBatchSize").value, 500),
|
|
4462
|
+
dryRun: parseBoolean(readStorageEnv(env2, "syncDryRun").value, false)
|
|
4338
4463
|
}
|
|
4339
4464
|
};
|
|
4340
4465
|
}
|
|
4341
|
-
function loadStorageConfig(
|
|
4342
|
-
return loadTodosStorageConfig(
|
|
4466
|
+
function loadStorageConfig(env2 = process.env) {
|
|
4467
|
+
return loadTodosStorageConfig(env2);
|
|
4343
4468
|
}
|
|
4344
4469
|
function isTodosPostgresBackend(config) {
|
|
4345
4470
|
return config.mode === "postgres";
|
|
@@ -4361,51 +4486,51 @@ function parseStorageBackend(value) {
|
|
|
4361
4486
|
return "postgres";
|
|
4362
4487
|
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4363
4488
|
}
|
|
4364
|
-
function getTodosStorageBackend(
|
|
4365
|
-
if (!getTodosStorageDatabaseUrl(
|
|
4489
|
+
function getTodosStorageBackend(env2 = process.env) {
|
|
4490
|
+
if (!getTodosStorageDatabaseUrl(env2))
|
|
4366
4491
|
return "sqlite";
|
|
4367
|
-
if (isTodosShadowEnabled(
|
|
4492
|
+
if (isTodosShadowEnabled(env2))
|
|
4368
4493
|
return "sqlite";
|
|
4369
4494
|
return "postgres";
|
|
4370
4495
|
}
|
|
4371
|
-
function isTodosShadowEnabled(
|
|
4372
|
-
return parseBoolean(readStorageEnv(
|
|
4496
|
+
function isTodosShadowEnabled(env2 = process.env) {
|
|
4497
|
+
return parseBoolean(readStorageEnv(env2, "shadow").value, false);
|
|
4373
4498
|
}
|
|
4374
|
-
function getTodosStorageShadowEnvName(
|
|
4375
|
-
return readStorageEnv(
|
|
4499
|
+
function getTodosStorageShadowEnvName(env2 = process.env) {
|
|
4500
|
+
return readStorageEnv(env2, "shadow").name;
|
|
4376
4501
|
}
|
|
4377
|
-
function assertTodosShadowConfig(config,
|
|
4378
|
-
if (!isTodosShadowEnabled(
|
|
4502
|
+
function assertTodosShadowConfig(config, env2 = process.env) {
|
|
4503
|
+
if (!isTodosShadowEnabled(env2))
|
|
4379
4504
|
return;
|
|
4380
4505
|
if (config.mode !== "sqlite") {
|
|
4381
|
-
throw new Error(`${readStorageEnv(
|
|
4506
|
+
throw new Error(`${readStorageEnv(env2, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
4382
4507
|
}
|
|
4383
4508
|
if (!config.database?.url) {
|
|
4384
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(
|
|
4509
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env2, "shadow").name} is enabled`);
|
|
4385
4510
|
}
|
|
4386
4511
|
}
|
|
4387
|
-
function getTodosStorageDatabaseEnv(
|
|
4388
|
-
return readStorageEnv(
|
|
4512
|
+
function getTodosStorageDatabaseEnv(env2 = process.env) {
|
|
4513
|
+
return readStorageEnv(env2, "databaseUrl").name;
|
|
4389
4514
|
}
|
|
4390
|
-
function getStorageDatabaseEnv(
|
|
4391
|
-
return getTodosStorageDatabaseEnv(
|
|
4515
|
+
function getStorageDatabaseEnv(env2 = process.env) {
|
|
4516
|
+
return getTodosStorageDatabaseEnv(env2);
|
|
4392
4517
|
}
|
|
4393
|
-
function getTodosStorageDatabaseUrl(
|
|
4394
|
-
return readStorageEnv(
|
|
4518
|
+
function getTodosStorageDatabaseUrl(env2 = process.env) {
|
|
4519
|
+
return readStorageEnv(env2, "databaseUrl").value;
|
|
4395
4520
|
}
|
|
4396
|
-
function getStorageDatabaseUrl(
|
|
4397
|
-
return getTodosStorageDatabaseUrl(
|
|
4521
|
+
function getStorageDatabaseUrl(env2 = process.env) {
|
|
4522
|
+
return getTodosStorageDatabaseUrl(env2);
|
|
4398
4523
|
}
|
|
4399
|
-
function getTodosStorageEnvName(
|
|
4400
|
-
return readStorageEnv(
|
|
4524
|
+
function getTodosStorageEnvName(env2, key) {
|
|
4525
|
+
return readStorageEnv(env2, key).name;
|
|
4401
4526
|
}
|
|
4402
|
-
function readStorageEnv(
|
|
4527
|
+
function readStorageEnv(env2, key) {
|
|
4403
4528
|
const primaryName = TODOS_STORAGE_ENV[key];
|
|
4404
|
-
const primaryValue = clean(
|
|
4529
|
+
const primaryValue = clean(env2[primaryName]);
|
|
4405
4530
|
if (primaryValue !== undefined)
|
|
4406
4531
|
return { name: primaryName, value: primaryValue };
|
|
4407
4532
|
const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
|
|
4408
|
-
const fallbackValue = clean(
|
|
4533
|
+
const fallbackValue = clean(env2[fallbackName]);
|
|
4409
4534
|
if (fallbackValue !== undefined)
|
|
4410
4535
|
return { name: fallbackName, value: fallbackValue };
|
|
4411
4536
|
return { name: primaryName };
|
|
@@ -4563,20 +4688,23 @@ __export(exports_database, {
|
|
|
4563
4688
|
resolvePartialId: () => resolvePartialId,
|
|
4564
4689
|
resolveAssignedToAliases: () => resolveAssignedToAliases,
|
|
4565
4690
|
resetDatabase: () => resetDatabase,
|
|
4691
|
+
refuseLocalStore: () => refuseLocalStore,
|
|
4566
4692
|
now: () => now,
|
|
4567
4693
|
lowerInClause: () => lowerInClause,
|
|
4568
4694
|
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
4569
4695
|
isLockExpired: () => isLockExpired,
|
|
4696
|
+
isLocalStoreRefused: () => isLocalStoreRefused,
|
|
4570
4697
|
getDatabasePath: () => getDatabasePath,
|
|
4571
4698
|
getDatabase: () => getDatabase,
|
|
4572
4699
|
closeDatabase: () => closeDatabase,
|
|
4573
4700
|
clearExpiredLocks: () => clearExpiredLocks,
|
|
4574
4701
|
assignedToAliasSet: () => assignedToAliasSet,
|
|
4702
|
+
allowLocalStore: () => allowLocalStore,
|
|
4575
4703
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
4576
4704
|
});
|
|
4577
4705
|
import { Database } from "bun:sqlite";
|
|
4578
4706
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
4579
|
-
import { dirname, join as
|
|
4707
|
+
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
4580
4708
|
function isInMemoryDb(path) {
|
|
4581
4709
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
4582
4710
|
}
|
|
@@ -4585,7 +4713,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4585
4713
|
const stopAt = gitRoot ? resolve3(gitRoot) : resolve3(startDir);
|
|
4586
4714
|
let dir = resolve3(startDir);
|
|
4587
4715
|
while (true) {
|
|
4588
|
-
const candidate =
|
|
4716
|
+
const candidate = join3(dir, ".hasna", "todos", "todos.db");
|
|
4589
4717
|
if (existsSync4(candidate))
|
|
4590
4718
|
return candidate;
|
|
4591
4719
|
if (dir === stopAt)
|
|
@@ -4600,7 +4728,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4600
4728
|
function findGitRoot(startDir) {
|
|
4601
4729
|
let dir = resolve3(startDir);
|
|
4602
4730
|
while (true) {
|
|
4603
|
-
if (existsSync4(
|
|
4731
|
+
if (existsSync4(join3(dir, ".git")))
|
|
4604
4732
|
return dir;
|
|
4605
4733
|
const parent = dirname(dir);
|
|
4606
4734
|
if (parent === dir)
|
|
@@ -4610,7 +4738,7 @@ function findGitRoot(startDir) {
|
|
|
4610
4738
|
return null;
|
|
4611
4739
|
}
|
|
4612
4740
|
function getGlobalDbPath() {
|
|
4613
|
-
return
|
|
4741
|
+
return join3(getTodosGlobalDir(), "todos.db");
|
|
4614
4742
|
}
|
|
4615
4743
|
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4616
4744
|
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
@@ -4648,7 +4776,7 @@ function getDbPath() {
|
|
|
4648
4776
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4649
4777
|
const gitRoot = findGitRoot(cwd);
|
|
4650
4778
|
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4651
|
-
return
|
|
4779
|
+
return join3(gitRoot, ".hasna", "todos", "todos.db");
|
|
4652
4780
|
}
|
|
4653
4781
|
}
|
|
4654
4782
|
return getGlobalDbPath();
|
|
@@ -4688,7 +4816,21 @@ function maybeInstallShadowCapture(db) {
|
|
|
4688
4816
|
console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4689
4817
|
}
|
|
4690
4818
|
}
|
|
4819
|
+
function refuseLocalStore(message) {
|
|
4820
|
+
localStoreRefusal = message;
|
|
4821
|
+
}
|
|
4822
|
+
function allowLocalStore() {
|
|
4823
|
+
localStoreRefusal = null;
|
|
4824
|
+
}
|
|
4825
|
+
function isLocalStoreRefused() {
|
|
4826
|
+
return localStoreRefusal !== null;
|
|
4827
|
+
}
|
|
4691
4828
|
function getDatabase(dbPath) {
|
|
4829
|
+
if (localStoreRefusal !== null)
|
|
4830
|
+
throw new Error(localStoreRefusal);
|
|
4831
|
+
if (dbPath === undefined && !selectsTodosLocalStore()) {
|
|
4832
|
+
throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
|
|
4833
|
+
}
|
|
4692
4834
|
const path = dbPath || getDbPath();
|
|
4693
4835
|
if (_db && _dbPath === path)
|
|
4694
4836
|
return _db;
|
|
@@ -4826,8 +4968,9 @@ function lowerInClause(column, values, params) {
|
|
|
4826
4968
|
params.push(...values.map((v) => v.toLowerCase()));
|
|
4827
4969
|
return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
|
|
4828
4970
|
}
|
|
4829
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
4971
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
|
|
4830
4972
|
var init_database = __esm(() => {
|
|
4973
|
+
init_local_opt_in();
|
|
4831
4974
|
init_schema();
|
|
4832
4975
|
init_machines();
|
|
4833
4976
|
init_identity_mapping();
|
|
@@ -4838,9 +4981,9 @@ var init_database = __esm(() => {
|
|
|
4838
4981
|
|
|
4839
4982
|
// src/lib/config.ts
|
|
4840
4983
|
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
4841
|
-
import { dirname as dirname2, join as
|
|
4984
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
4842
4985
|
function getConfigPath() {
|
|
4843
|
-
return
|
|
4986
|
+
return join4(getTodosGlobalDir(), "config.json");
|
|
4844
4987
|
}
|
|
4845
4988
|
function normalizeAgent(agent) {
|
|
4846
4989
|
return agent.trim().toLowerCase();
|
|
@@ -4891,21 +5034,6 @@ function normalizeApiUrl(value) {
|
|
|
4891
5034
|
return null;
|
|
4892
5035
|
return trimmed.replace(/\/+$/, "");
|
|
4893
5036
|
}
|
|
4894
|
-
function getLocalApiConfig(env = process.env) {
|
|
4895
|
-
const config = loadConfig();
|
|
4896
|
-
const envApiUrl = normalizeApiUrl(env["TODOS_URL"]);
|
|
4897
|
-
const configApiUrl = normalizeApiUrl(config.apiUrl);
|
|
4898
|
-
const apiUrl = envApiUrl ?? configApiUrl;
|
|
4899
|
-
const apiKey = env["TODOS_API_KEY"] || config.apiKey || null;
|
|
4900
|
-
return {
|
|
4901
|
-
apiUrl,
|
|
4902
|
-
apiKey,
|
|
4903
|
-
source: {
|
|
4904
|
-
apiUrl: envApiUrl ? "TODOS_URL" : configApiUrl ? "config" : "none",
|
|
4905
|
-
apiKey: env["TODOS_API_KEY"] ? "TODOS_API_KEY" : config.apiKey ? "config" : "none"
|
|
4906
|
-
}
|
|
4907
|
-
};
|
|
4908
|
-
}
|
|
4909
5037
|
function getSyncAgentsFromConfig() {
|
|
4910
5038
|
const config = loadConfig();
|
|
4911
5039
|
const agents = config.sync_agents;
|
|
@@ -5756,11 +5884,11 @@ function writeAllowed(profile, root, writePath) {
|
|
|
5756
5884
|
return isPathInside(scopeRoot, target);
|
|
5757
5885
|
});
|
|
5758
5886
|
}
|
|
5759
|
-
function redactedEnvKeys(profile,
|
|
5760
|
-
if (!
|
|
5887
|
+
function redactedEnvKeys(profile, env2) {
|
|
5888
|
+
if (!env2)
|
|
5761
5889
|
return [];
|
|
5762
5890
|
const patterns = unique2([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
5763
|
-
return Object.keys(
|
|
5891
|
+
return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
5764
5892
|
}
|
|
5765
5893
|
function checkWorkspacePermission(input = {}) {
|
|
5766
5894
|
const status = getWorkspaceTrustStatus(input.path || process.cwd());
|
|
@@ -5898,18 +6026,18 @@ function profileByName(name, path) {
|
|
|
5898
6026
|
const resolved = normalizePath2(path);
|
|
5899
6027
|
return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
|
|
5900
6028
|
}
|
|
5901
|
-
function redactedEnvKeys2(profile,
|
|
5902
|
-
if (!
|
|
6029
|
+
function redactedEnvKeys2(profile, env2) {
|
|
6030
|
+
if (!env2)
|
|
5903
6031
|
return [];
|
|
5904
6032
|
const patterns = unique3([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
5905
|
-
return Object.keys(
|
|
6033
|
+
return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
5906
6034
|
}
|
|
5907
|
-
function omittedEnvKeys(profile,
|
|
5908
|
-
if (!
|
|
6035
|
+
function omittedEnvKeys(profile, env2) {
|
|
6036
|
+
if (!env2)
|
|
5909
6037
|
return [];
|
|
5910
6038
|
if (profile.env_allowlist.includes("*"))
|
|
5911
6039
|
return [];
|
|
5912
|
-
return Object.keys(
|
|
6040
|
+
return Object.keys(env2).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
|
|
5913
6041
|
}
|
|
5914
6042
|
function resolveFromCwd(cwd, path) {
|
|
5915
6043
|
return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
|
|
@@ -6292,15 +6420,23 @@ var init_event_hooks = __esm(() => {
|
|
|
6292
6420
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
6293
6421
|
});
|
|
6294
6422
|
|
|
6295
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
6423
|
+
// node_modules/.bun/@hasna+events@0.1.17/node_modules/@hasna/events/dist/index.js
|
|
6296
6424
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6297
6425
|
import { Buffer as Buffer2 } from "buffer";
|
|
6426
|
+
import { existsSync as existsSync22 } from "fs";
|
|
6427
|
+
import { join as join32 } from "path";
|
|
6298
6428
|
import { existsSync as existsSync6 } from "fs";
|
|
6299
|
-
import { homedir as
|
|
6300
|
-
import { join as
|
|
6429
|
+
import { homedir as homedir22 } from "os";
|
|
6430
|
+
import { join as join22, resolve as resolve8 } from "path";
|
|
6431
|
+
import { homedir as homedir3 } from "os";
|
|
6432
|
+
import { join as join5 } from "path";
|
|
6301
6433
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
6434
|
+
import { lookup as dnsLookup } from "dns/promises";
|
|
6435
|
+
import { isIP } from "net";
|
|
6302
6436
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6303
6437
|
import { spawn } from "child_process";
|
|
6438
|
+
import { request as nodeHttpRequest } from "http";
|
|
6439
|
+
import { request as nodeHttpsRequest } from "https";
|
|
6304
6440
|
import { randomUUID as randomUUID22 } from "crypto";
|
|
6305
6441
|
function getPathValue(input, path) {
|
|
6306
6442
|
return path.split(".").reduce((value, part) => {
|
|
@@ -6397,8 +6533,93 @@ function channelMatchesEvent(channel, event) {
|
|
|
6397
6533
|
return true;
|
|
6398
6534
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
6399
6535
|
}
|
|
6536
|
+
function assertApp(app) {
|
|
6537
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
6538
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
6539
|
+
}
|
|
6540
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
6541
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
function envOf(options) {
|
|
6545
|
+
return options.env ?? process.env;
|
|
6546
|
+
}
|
|
6547
|
+
function envValue(options, kind) {
|
|
6548
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
6549
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
6550
|
+
}
|
|
6551
|
+
function isMacOS(platform) {
|
|
6552
|
+
return platform === "darwin";
|
|
6553
|
+
}
|
|
6554
|
+
function baseDir(kind, options) {
|
|
6555
|
+
const override = envValue(options, kind);
|
|
6556
|
+
if (override)
|
|
6557
|
+
return override;
|
|
6558
|
+
const home = options.home ?? homedir3();
|
|
6559
|
+
const platform = options.platform ?? process.platform;
|
|
6560
|
+
if (isMacOS(platform)) {
|
|
6561
|
+
switch (kind) {
|
|
6562
|
+
case "config":
|
|
6563
|
+
case "data":
|
|
6564
|
+
return join5(home, "Library", "Application Support", "Hasna");
|
|
6565
|
+
case "cache":
|
|
6566
|
+
return join5(home, "Library", "Caches", "Hasna");
|
|
6567
|
+
case "state":
|
|
6568
|
+
return join5(home, "Library", "Logs", "Hasna");
|
|
6569
|
+
}
|
|
6570
|
+
}
|
|
6571
|
+
switch (kind) {
|
|
6572
|
+
case "config":
|
|
6573
|
+
return join5(home, ".config", "hasna");
|
|
6574
|
+
case "data":
|
|
6575
|
+
return join5(home, ".local", "share", "hasna");
|
|
6576
|
+
case "state":
|
|
6577
|
+
return join5(home, ".local", "state", "hasna");
|
|
6578
|
+
case "cache":
|
|
6579
|
+
return join5(home, ".cache", "hasna");
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
6582
|
+
function resolvePath(kind, options) {
|
|
6583
|
+
assertApp(options.app);
|
|
6584
|
+
const appSegment = options.internal === true ? join5("internal", options.app) : options.app;
|
|
6585
|
+
return join5(baseDir(kind, options), appSegment);
|
|
6586
|
+
}
|
|
6587
|
+
function dataDir2(options) {
|
|
6588
|
+
return resolvePath("data", options);
|
|
6589
|
+
}
|
|
6590
|
+
function effectiveHome2() {
|
|
6591
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir22();
|
|
6592
|
+
}
|
|
6593
|
+
function legacyHomeDir2() {
|
|
6594
|
+
return join22(effectiveHome2(), ".hasna", "events");
|
|
6595
|
+
}
|
|
6596
|
+
function resolverHome2() {
|
|
6597
|
+
return dataDir2({ app: "events", home: effectiveHome2() || undefined });
|
|
6598
|
+
}
|
|
6599
|
+
function adoptResolverHome2(resolved, env2 = process.env) {
|
|
6600
|
+
const dataOverride = env2.HASNA_DATA_HOME;
|
|
6601
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
6602
|
+
return true;
|
|
6603
|
+
return existsSync6(join22(resolved, EVENTS_STORE_SENTINEL_FILE));
|
|
6604
|
+
}
|
|
6605
|
+
function exactEventsHome() {
|
|
6606
|
+
const dir = process.env[HASNA_EVENTS_DIR_ENV];
|
|
6607
|
+
if (dir && dir.trim())
|
|
6608
|
+
return dir.trim();
|
|
6609
|
+
const home = process.env[HASNA_EVENTS_HOME_ENV];
|
|
6610
|
+
if (home && home.trim())
|
|
6611
|
+
return home.trim();
|
|
6612
|
+
return;
|
|
6613
|
+
}
|
|
6614
|
+
function getEventsHome() {
|
|
6615
|
+
const exact = exactEventsHome();
|
|
6616
|
+
if (exact)
|
|
6617
|
+
return resolve8(exact);
|
|
6618
|
+
const resolved = resolverHome2();
|
|
6619
|
+
return adoptResolverHome2(resolved) ? resolve8(resolved) : resolve8(legacyHomeDir2());
|
|
6620
|
+
}
|
|
6400
6621
|
function getEventsDataDir(override) {
|
|
6401
|
-
return override ||
|
|
6622
|
+
return override || getEventsHome();
|
|
6402
6623
|
}
|
|
6403
6624
|
|
|
6404
6625
|
class JsonEventsStore {
|
|
@@ -6407,12 +6628,12 @@ class JsonEventsStore {
|
|
|
6407
6628
|
channelsPath;
|
|
6408
6629
|
eventsPath;
|
|
6409
6630
|
deliveriesPath;
|
|
6410
|
-
constructor(
|
|
6411
|
-
this.dataDir =
|
|
6412
|
-
this.runtime = localJsonRuntime(
|
|
6413
|
-
this.channelsPath =
|
|
6414
|
-
this.eventsPath =
|
|
6415
|
-
this.deliveriesPath =
|
|
6631
|
+
constructor(dataDir22 = getEventsDataDir()) {
|
|
6632
|
+
this.dataDir = dataDir22;
|
|
6633
|
+
this.runtime = localJsonRuntime(dataDir22);
|
|
6634
|
+
this.channelsPath = join32(dataDir22, "channels.json");
|
|
6635
|
+
this.eventsPath = join32(dataDir22, "events.json");
|
|
6636
|
+
this.deliveriesPath = join32(dataDir22, "deliveries.json");
|
|
6416
6637
|
}
|
|
6417
6638
|
async init() {
|
|
6418
6639
|
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
@@ -6529,7 +6750,7 @@ class JsonEventsStore {
|
|
|
6529
6750
|
};
|
|
6530
6751
|
}
|
|
6531
6752
|
async ensureArrayFile(path) {
|
|
6532
|
-
if (!
|
|
6753
|
+
if (!existsSync22(path)) {
|
|
6533
6754
|
await writeFile(path, `[]
|
|
6534
6755
|
`, { encoding: "utf-8", mode: 384 });
|
|
6535
6756
|
}
|
|
@@ -6559,7 +6780,7 @@ class JsonEventsStore {
|
|
|
6559
6780
|
});
|
|
6560
6781
|
}
|
|
6561
6782
|
}
|
|
6562
|
-
function localJsonRuntime(
|
|
6783
|
+
function localJsonRuntime(dataDir22 = getEventsDataDir()) {
|
|
6563
6784
|
return {
|
|
6564
6785
|
mode: "local-files",
|
|
6565
6786
|
name: "json-events-store",
|
|
@@ -6572,7 +6793,7 @@ function localJsonRuntime(dataDir2 = getEventsDataDir()) {
|
|
|
6572
6793
|
durable: true,
|
|
6573
6794
|
idempotency: "best-effort-local",
|
|
6574
6795
|
replayCursors: true,
|
|
6575
|
-
description: `Local JSON files in ${
|
|
6796
|
+
description: `Local JSON files in ${dataDir22}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
6576
6797
|
};
|
|
6577
6798
|
}
|
|
6578
6799
|
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
@@ -6643,6 +6864,177 @@ function signPayload(secret, timestamp2, body) {
|
|
|
6643
6864
|
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp2, body)).digest("hex");
|
|
6644
6865
|
return `sha256=${digest}`;
|
|
6645
6866
|
}
|
|
6867
|
+
function isPrivateAddress(address) {
|
|
6868
|
+
const normalized = stripZoneId(address);
|
|
6869
|
+
const version = isIP(normalized);
|
|
6870
|
+
if (version === 4) {
|
|
6871
|
+
const integer = ipv4ToInt(normalized);
|
|
6872
|
+
if (integer === undefined)
|
|
6873
|
+
return true;
|
|
6874
|
+
return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
|
|
6875
|
+
}
|
|
6876
|
+
if (version === 6) {
|
|
6877
|
+
const groups = ipv6Groups(normalized);
|
|
6878
|
+
if (!groups)
|
|
6879
|
+
return true;
|
|
6880
|
+
for (const prefix of IPV6_SPECIAL_PREFIXES) {
|
|
6881
|
+
if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
|
|
6882
|
+
continue;
|
|
6883
|
+
if (prefix.bits === 96 && groups[5] === 65535) {
|
|
6884
|
+
return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
|
|
6885
|
+
}
|
|
6886
|
+
if (prefix.bits === 16 && groups[0] === 8194) {
|
|
6887
|
+
return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
|
|
6888
|
+
}
|
|
6889
|
+
return true;
|
|
6890
|
+
}
|
|
6891
|
+
return false;
|
|
6892
|
+
}
|
|
6893
|
+
return true;
|
|
6894
|
+
}
|
|
6895
|
+
async function resolveWebhookTarget(url, policy = {}) {
|
|
6896
|
+
const hostname = normalizeHostname(url.hostname);
|
|
6897
|
+
const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
|
|
6898
|
+
if (allowlist.includes(hostname)) {
|
|
6899
|
+
const version2 = isIP(hostname);
|
|
6900
|
+
if (version2 === 4 || version2 === 6) {
|
|
6901
|
+
return { hostname, addresses: [hostname] };
|
|
6902
|
+
}
|
|
6903
|
+
const lookup2 = policy.lookup ?? defaultTargetLookup;
|
|
6904
|
+
let resolved2;
|
|
6905
|
+
try {
|
|
6906
|
+
resolved2 = await lookup2(hostname);
|
|
6907
|
+
} catch {
|
|
6908
|
+
throw new Error(`Webhook target ${hostname} could not be resolved`);
|
|
6909
|
+
}
|
|
6910
|
+
if (!Array.isArray(resolved2) || resolved2.length === 0) {
|
|
6911
|
+
throw new Error(`Webhook target ${hostname} resolved to no addresses`);
|
|
6912
|
+
}
|
|
6913
|
+
const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
|
|
6914
|
+
return { hostname, addresses };
|
|
6915
|
+
}
|
|
6916
|
+
const version = isIP(hostname);
|
|
6917
|
+
if (version === 4 || version === 6) {
|
|
6918
|
+
if (isPrivateAddress(hostname)) {
|
|
6919
|
+
throw new Error(`Webhook target ${hostname} is a private or special-use address`);
|
|
6920
|
+
}
|
|
6921
|
+
return { hostname, addresses: [hostname] };
|
|
6922
|
+
}
|
|
6923
|
+
const lookup = policy.lookup ?? defaultTargetLookup;
|
|
6924
|
+
let resolved;
|
|
6925
|
+
try {
|
|
6926
|
+
resolved = await lookup(hostname);
|
|
6927
|
+
} catch {
|
|
6928
|
+
throw new Error(`Webhook target ${hostname} could not be resolved`);
|
|
6929
|
+
}
|
|
6930
|
+
if (!Array.isArray(resolved) || resolved.length === 0) {
|
|
6931
|
+
throw new Error(`Webhook target ${hostname} resolved to no addresses`);
|
|
6932
|
+
}
|
|
6933
|
+
const allowed = [];
|
|
6934
|
+
for (const entry of resolved) {
|
|
6935
|
+
const address = normalizeHostname(entry.address);
|
|
6936
|
+
if (isPrivateAddress(address)) {
|
|
6937
|
+
if (allowlist.includes(address)) {
|
|
6938
|
+
allowed.push(address);
|
|
6939
|
+
continue;
|
|
6940
|
+
}
|
|
6941
|
+
throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
|
|
6942
|
+
}
|
|
6943
|
+
allowed.push(address);
|
|
6944
|
+
}
|
|
6945
|
+
if (allowed.length === 0) {
|
|
6946
|
+
throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
|
|
6947
|
+
}
|
|
6948
|
+
return { hostname, addresses: allowed };
|
|
6949
|
+
}
|
|
6950
|
+
function normalizeMaxRedirects(value) {
|
|
6951
|
+
if (value === undefined)
|
|
6952
|
+
return DEFAULT_MAX_REDIRECTS;
|
|
6953
|
+
if (!Number.isInteger(value) || value < 0)
|
|
6954
|
+
throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
|
|
6955
|
+
return value;
|
|
6956
|
+
}
|
|
6957
|
+
function normalizeHostname(hostname) {
|
|
6958
|
+
const lower = hostname.toLowerCase();
|
|
6959
|
+
if (lower.startsWith("[") && lower.endsWith("]"))
|
|
6960
|
+
return lower.slice(1, -1);
|
|
6961
|
+
return lower;
|
|
6962
|
+
}
|
|
6963
|
+
function stripZoneId(address) {
|
|
6964
|
+
const percent = address.indexOf("%");
|
|
6965
|
+
return percent === -1 ? address : address.slice(0, percent);
|
|
6966
|
+
}
|
|
6967
|
+
function ipv4ToInt(address) {
|
|
6968
|
+
const parts = address.split(".");
|
|
6969
|
+
if (parts.length !== 4)
|
|
6970
|
+
return;
|
|
6971
|
+
let value = 0;
|
|
6972
|
+
for (const part of parts) {
|
|
6973
|
+
if (!/^\d{1,3}$/.test(part))
|
|
6974
|
+
return;
|
|
6975
|
+
const octet = Number(part);
|
|
6976
|
+
if (octet > 255)
|
|
6977
|
+
return;
|
|
6978
|
+
value = value << 8 | octet;
|
|
6979
|
+
}
|
|
6980
|
+
return value >>> 0;
|
|
6981
|
+
}
|
|
6982
|
+
function ipv4IntToString(integer) {
|
|
6983
|
+
return [
|
|
6984
|
+
integer >>> 24 & 255,
|
|
6985
|
+
integer >>> 16 & 255,
|
|
6986
|
+
integer >>> 8 & 255,
|
|
6987
|
+
integer & 255
|
|
6988
|
+
].join(".");
|
|
6989
|
+
}
|
|
6990
|
+
function ipv6Groups(address) {
|
|
6991
|
+
const raw = stripZoneId(address);
|
|
6992
|
+
const doubleColon = raw.indexOf("::");
|
|
6993
|
+
const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
|
|
6994
|
+
const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
|
|
6995
|
+
const parseGroups = (text) => {
|
|
6996
|
+
if (text === "")
|
|
6997
|
+
return [];
|
|
6998
|
+
const out = [];
|
|
6999
|
+
for (const part of text.split(":")) {
|
|
7000
|
+
if (part.includes(".")) {
|
|
7001
|
+
const v4 = ipv4ToInt(part);
|
|
7002
|
+
if (v4 === undefined)
|
|
7003
|
+
return;
|
|
7004
|
+
out.push(v4 >>> 16 & 65535, v4 & 65535);
|
|
7005
|
+
} else {
|
|
7006
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(part))
|
|
7007
|
+
return;
|
|
7008
|
+
out.push(parseInt(part, 16));
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
return out;
|
|
7012
|
+
};
|
|
7013
|
+
const head = parseGroups(headText);
|
|
7014
|
+
if (!head)
|
|
7015
|
+
return;
|
|
7016
|
+
const tail = parseGroups(tailText);
|
|
7017
|
+
if (!tail)
|
|
7018
|
+
return;
|
|
7019
|
+
const total = head.length + tail.length;
|
|
7020
|
+
if (doubleColon === -1) {
|
|
7021
|
+
return total === 8 ? head : undefined;
|
|
7022
|
+
}
|
|
7023
|
+
if (total >= 8)
|
|
7024
|
+
return;
|
|
7025
|
+
return [...head, ...new Array(8 - total).fill(0), ...tail];
|
|
7026
|
+
}
|
|
7027
|
+
function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
|
|
7028
|
+
let remaining = prefixBits;
|
|
7029
|
+
for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
|
|
7030
|
+
const take = Math.min(16, remaining);
|
|
7031
|
+
const mask = 65535 << 16 - take & 65535;
|
|
7032
|
+
if ((groups[index] & mask) !== (prefixGroups[index] & mask))
|
|
7033
|
+
return false;
|
|
7034
|
+
remaining -= take;
|
|
7035
|
+
}
|
|
7036
|
+
return true;
|
|
7037
|
+
}
|
|
6646
7038
|
function now2() {
|
|
6647
7039
|
return new Date().toISOString();
|
|
6648
7040
|
}
|
|
@@ -6673,9 +7065,18 @@ function buildWebhookRequest(event, channel, options = {}) {
|
|
|
6673
7065
|
}
|
|
6674
7066
|
return { body, headers };
|
|
6675
7067
|
}
|
|
7068
|
+
function normalizeWebhookUrl(raw) {
|
|
7069
|
+
const url = new URL(raw);
|
|
7070
|
+
if (url.username !== "" || url.password !== "") {
|
|
7071
|
+
url.username = "";
|
|
7072
|
+
url.password = "";
|
|
7073
|
+
}
|
|
7074
|
+
return url.toString();
|
|
7075
|
+
}
|
|
6676
7076
|
async function dispatchWebhook(event, channel, options = {}) {
|
|
6677
7077
|
if (!channel.webhook)
|
|
6678
7078
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7079
|
+
const webhookUrl = normalizeWebhookUrl(channel.webhook.url);
|
|
6679
7080
|
const startedAt = now2();
|
|
6680
7081
|
let secret = channel.webhook.secret;
|
|
6681
7082
|
if (channel.webhook.secretRef) {
|
|
@@ -6692,10 +7093,14 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6692
7093
|
}
|
|
6693
7094
|
const timestamp2 = (options.now?.() ?? new Date).toISOString();
|
|
6694
7095
|
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
|
|
7096
|
+
const validateTargets = options.webhookTargetPolicy !== undefined || options.fetchImpl === undefined;
|
|
7097
|
+
if (validateTargets) {
|
|
7098
|
+
return dispatchValidatedWebhook(event, channel, { body, headers, startedAt, options });
|
|
7099
|
+
}
|
|
6695
7100
|
const controller = new AbortController;
|
|
6696
7101
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
6697
7102
|
try {
|
|
6698
|
-
const response = await (options.fetchImpl ?? fetch)(
|
|
7103
|
+
const response = await (options.fetchImpl ?? fetch)(webhookUrl, {
|
|
6699
7104
|
method: "POST",
|
|
6700
7105
|
headers,
|
|
6701
7106
|
body,
|
|
@@ -6723,6 +7128,130 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6723
7128
|
clearTimeout(timeout);
|
|
6724
7129
|
}
|
|
6725
7130
|
}
|
|
7131
|
+
function isRedirectStatus(status) {
|
|
7132
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
7133
|
+
}
|
|
7134
|
+
function redirectKeepsBody(status) {
|
|
7135
|
+
return status === 307 || status === 308;
|
|
7136
|
+
}
|
|
7137
|
+
async function pinnedNativeRequest(target, addresses, method, headers, body, signal, tls) {
|
|
7138
|
+
const isHttps = target.protocol === "https:";
|
|
7139
|
+
if (!isHttps && target.protocol !== "http:") {
|
|
7140
|
+
throw new Error(`Webhook target uses unsupported protocol ${target.protocol}`);
|
|
7141
|
+
}
|
|
7142
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
7143
|
+
const port = target.port ? Number(target.port) : defaultPort;
|
|
7144
|
+
const requestOptions = {
|
|
7145
|
+
hostname: target.hostname,
|
|
7146
|
+
port,
|
|
7147
|
+
path: `${target.pathname}${target.search}`,
|
|
7148
|
+
method,
|
|
7149
|
+
headers,
|
|
7150
|
+
...tls?.ca ? { ca: tls.ca } : {},
|
|
7151
|
+
lookup: (hostname, _options, callback) => {
|
|
7152
|
+
const entries = addresses.map((address) => ({
|
|
7153
|
+
address,
|
|
7154
|
+
family: address.includes(":") ? 6 : 4
|
|
7155
|
+
}));
|
|
7156
|
+
callback(null, entries);
|
|
7157
|
+
}
|
|
7158
|
+
};
|
|
7159
|
+
return new Promise((resolve22, reject) => {
|
|
7160
|
+
const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
|
|
7161
|
+
const onAbort = () => {
|
|
7162
|
+
const error = new Error("The operation was aborted.");
|
|
7163
|
+
error.name = "AbortError";
|
|
7164
|
+
request.destroy(error);
|
|
7165
|
+
};
|
|
7166
|
+
if (signal.aborted)
|
|
7167
|
+
onAbort();
|
|
7168
|
+
else
|
|
7169
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
7170
|
+
request.on("error", reject);
|
|
7171
|
+
if (body !== undefined)
|
|
7172
|
+
request.write(body);
|
|
7173
|
+
request.end();
|
|
7174
|
+
function onResponse(response) {
|
|
7175
|
+
const chunks = [];
|
|
7176
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
7177
|
+
response.on("error", reject);
|
|
7178
|
+
response.on("end", () => {
|
|
7179
|
+
const headersRecord = {};
|
|
7180
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
7181
|
+
if (typeof value === "string")
|
|
7182
|
+
headersRecord[name] = value;
|
|
7183
|
+
else if (Array.isArray(value))
|
|
7184
|
+
headersRecord[name] = value.join(", ");
|
|
7185
|
+
}
|
|
7186
|
+
resolve22(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
|
|
7187
|
+
});
|
|
7188
|
+
}
|
|
7189
|
+
});
|
|
7190
|
+
}
|
|
7191
|
+
async function dispatchValidatedWebhook(event, channel, input) {
|
|
7192
|
+
const { body, headers, startedAt, options } = input;
|
|
7193
|
+
const webhook = channel.webhook;
|
|
7194
|
+
if (!webhook)
|
|
7195
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7196
|
+
const policy = options.webhookTargetPolicy ?? {};
|
|
7197
|
+
const maxRedirects = normalizeMaxRedirects(policy.maxRedirects);
|
|
7198
|
+
const controller = new AbortController;
|
|
7199
|
+
const timeout = setTimeout(() => controller.abort(), webhook.timeoutMs ?? 15000);
|
|
7200
|
+
try {
|
|
7201
|
+
let target = new URL(normalizeWebhookUrl(webhook.url));
|
|
7202
|
+
let requestHeaders = headers;
|
|
7203
|
+
let method = "POST";
|
|
7204
|
+
let requestBody = body;
|
|
7205
|
+
let redirectsFollowed = 0;
|
|
7206
|
+
for (;; ) {
|
|
7207
|
+
const resolved = await resolveWebhookTarget(target, policy).catch((error) => {
|
|
7208
|
+
throw new Error(`Webhook target rejected by SSRF guard: ${error.message}`);
|
|
7209
|
+
});
|
|
7210
|
+
const response = options.fetchImpl ? await options.fetchImpl(target, {
|
|
7211
|
+
method,
|
|
7212
|
+
headers: requestHeaders,
|
|
7213
|
+
body: requestBody,
|
|
7214
|
+
signal: controller.signal,
|
|
7215
|
+
redirect: "manual"
|
|
7216
|
+
}) : await pinnedNativeRequest(target, resolved.addresses, method, requestHeaders, requestBody, controller.signal, options.tls);
|
|
7217
|
+
const location = response.headers.get("location");
|
|
7218
|
+
if (isRedirectStatus(response.status) && location) {
|
|
7219
|
+
if (redirectsFollowed >= maxRedirects) {
|
|
7220
|
+
return failedAttempt(startedAt, `Webhook target exceeded ${maxRedirects} redirects`);
|
|
7221
|
+
}
|
|
7222
|
+
redirectsFollowed += 1;
|
|
7223
|
+
const next = new URL(location, target);
|
|
7224
|
+
target = next;
|
|
7225
|
+
if (!redirectKeepsBody(response.status)) {
|
|
7226
|
+
method = "GET";
|
|
7227
|
+
requestBody = undefined;
|
|
7228
|
+
requestHeaders = Object.fromEntries(Object.entries(requestHeaders).filter(([name]) => name.toLowerCase() !== "content-type" && name.toLowerCase() !== "content-length"));
|
|
7229
|
+
}
|
|
7230
|
+
continue;
|
|
7231
|
+
}
|
|
7232
|
+
const responseBody = truncate(await response.text());
|
|
7233
|
+
return {
|
|
7234
|
+
attempt: 1,
|
|
7235
|
+
status: response.ok ? "success" : "failed",
|
|
7236
|
+
startedAt,
|
|
7237
|
+
completedAt: now2(),
|
|
7238
|
+
responseStatus: response.status,
|
|
7239
|
+
responseBody,
|
|
7240
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
7241
|
+
};
|
|
7242
|
+
}
|
|
7243
|
+
} catch (error) {
|
|
7244
|
+
return {
|
|
7245
|
+
attempt: 1,
|
|
7246
|
+
status: "failed",
|
|
7247
|
+
startedAt,
|
|
7248
|
+
completedAt: now2(),
|
|
7249
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7250
|
+
};
|
|
7251
|
+
} finally {
|
|
7252
|
+
clearTimeout(timeout);
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
6726
7255
|
function failedAttempt(startedAt, error) {
|
|
6727
7256
|
return {
|
|
6728
7257
|
attempt: 1,
|
|
@@ -6737,7 +7266,7 @@ async function dispatchCommand(event, channel) {
|
|
|
6737
7266
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
6738
7267
|
const startedAt = now2();
|
|
6739
7268
|
const eventJson = JSON.stringify(event);
|
|
6740
|
-
const
|
|
7269
|
+
const env2 = {
|
|
6741
7270
|
...process.env,
|
|
6742
7271
|
...channel.command.env,
|
|
6743
7272
|
HASNA_CHANNEL_ID: channel.id,
|
|
@@ -6751,10 +7280,10 @@ async function dispatchCommand(event, channel) {
|
|
|
6751
7280
|
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
6752
7281
|
HASNA_EVENT_JSON: eventJson
|
|
6753
7282
|
};
|
|
6754
|
-
return new Promise((
|
|
7283
|
+
return new Promise((resolve22) => {
|
|
6755
7284
|
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
6756
7285
|
cwd: channel.command.cwd,
|
|
6757
|
-
env,
|
|
7286
|
+
env: env2,
|
|
6758
7287
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6759
7288
|
});
|
|
6760
7289
|
let stdout = "";
|
|
@@ -6769,7 +7298,7 @@ async function dispatchCommand(event, channel) {
|
|
|
6769
7298
|
});
|
|
6770
7299
|
child.on("error", (error) => {
|
|
6771
7300
|
clearTimeout(timeout);
|
|
6772
|
-
|
|
7301
|
+
resolve22({
|
|
6773
7302
|
attempt: 1,
|
|
6774
7303
|
status: "failed",
|
|
6775
7304
|
startedAt,
|
|
@@ -6782,7 +7311,7 @@ async function dispatchCommand(event, channel) {
|
|
|
6782
7311
|
child.on("close", (code, signal) => {
|
|
6783
7312
|
clearTimeout(timeout);
|
|
6784
7313
|
const success = code === 0;
|
|
6785
|
-
|
|
7314
|
+
resolve22({
|
|
6786
7315
|
attempt: 1,
|
|
6787
7316
|
status: success ? "success" : "failed",
|
|
6788
7317
|
startedAt,
|
|
@@ -6918,7 +7447,9 @@ class EventsClient {
|
|
|
6918
7447
|
this.transportOptions = {
|
|
6919
7448
|
fetchImpl: options.fetchImpl,
|
|
6920
7449
|
secretResolver: options.secretResolver,
|
|
6921
|
-
now: options.now
|
|
7450
|
+
now: options.now,
|
|
7451
|
+
tls: options.tls,
|
|
7452
|
+
webhookTargetPolicy: options.webhookTargetPolicy
|
|
6922
7453
|
};
|
|
6923
7454
|
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
6924
7455
|
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
@@ -7123,9 +7654,51 @@ function normalizeRetryPolicy(policy) {
|
|
|
7123
7654
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
7124
7655
|
};
|
|
7125
7656
|
}
|
|
7126
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS,
|
|
7127
|
-
|
|
7657
|
+
var KIND_ENV, APP_SLUG_RE, HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR", HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME", EVENTS_STORE_SENTINEL_FILE = "events.json", LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:", DEFAULT_EVENT_PAGE_LIMIT = 100, MAX_EVENT_PAGE_LIMIT = 1000, DEFAULT_SIGNATURE_TOLERANCE_MS, DEFAULT_MAX_REDIRECTS = 5, IPV4_PRIVATE_RANGES, IPV6_SPECIAL_PREFIXES, defaultTargetLookup = async (hostname) => {
|
|
7658
|
+
return dnsLookup(hostname, { all: true, verbatim: false });
|
|
7659
|
+
}, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
|
|
7660
|
+
var init_dist = __esm(() => {
|
|
7661
|
+
KIND_ENV = {
|
|
7662
|
+
config: "HASNA_CONFIG_HOME",
|
|
7663
|
+
data: "HASNA_DATA_HOME",
|
|
7664
|
+
state: "HASNA_STATE_HOME",
|
|
7665
|
+
cache: "HASNA_CACHE_HOME"
|
|
7666
|
+
};
|
|
7667
|
+
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7128
7668
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
7669
|
+
IPV4_PRIVATE_RANGES = [
|
|
7670
|
+
[0, 16777215],
|
|
7671
|
+
[167772160, 184549375],
|
|
7672
|
+
[1681915904, 1686110207],
|
|
7673
|
+
[2130706432, 2147483647],
|
|
7674
|
+
[2851995648, 2852061183],
|
|
7675
|
+
[2886729728, 2887778303],
|
|
7676
|
+
[3221225472, 3221225727],
|
|
7677
|
+
[3221225984, 3221226239],
|
|
7678
|
+
[3227017984, 3227018239],
|
|
7679
|
+
[3232235520, 3232301055],
|
|
7680
|
+
[3323068416, 3323199487],
|
|
7681
|
+
[3325256704, 3325256959],
|
|
7682
|
+
[3405803776, 3405804031],
|
|
7683
|
+
[3758096384, 4294967295]
|
|
7684
|
+
];
|
|
7685
|
+
IPV6_SPECIAL_PREFIXES = [
|
|
7686
|
+
{ groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
|
|
7687
|
+
{ groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
|
|
7688
|
+
{ groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
|
|
7689
|
+
{ groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
|
|
7690
|
+
{ groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
|
|
7691
|
+
{ groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
|
|
7692
|
+
{ groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
|
|
7693
|
+
{ groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
|
|
7694
|
+
{ groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
|
|
7695
|
+
{ groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
|
|
7696
|
+
{ groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
|
|
7697
|
+
{ groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
|
|
7698
|
+
{ groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
|
|
7699
|
+
{ groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
|
|
7700
|
+
{ groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
|
|
7701
|
+
];
|
|
7129
7702
|
EventValidationError = class EventValidationError extends Error {
|
|
7130
7703
|
eventType;
|
|
7131
7704
|
issues;
|
|
@@ -7149,6 +7722,8 @@ function rowToTaskList(row) {
|
|
|
7149
7722
|
};
|
|
7150
7723
|
}
|
|
7151
7724
|
function createTaskList(input, db) {
|
|
7725
|
+
if (input.status !== undefined)
|
|
7726
|
+
throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
|
|
7152
7727
|
const d = db || getDatabase();
|
|
7153
7728
|
return d.transaction(() => {
|
|
7154
7729
|
const id = uuid();
|
|
@@ -7190,6 +7765,8 @@ function listTaskLists(projectId, db) {
|
|
|
7190
7765
|
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
7191
7766
|
}
|
|
7192
7767
|
function updateTaskList(id, input, db) {
|
|
7768
|
+
if (input.status !== undefined)
|
|
7769
|
+
throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
|
|
7193
7770
|
const d = db || getDatabase();
|
|
7194
7771
|
return d.transaction(() => {
|
|
7195
7772
|
const existing = getTaskList(id, d);
|
|
@@ -7547,7 +8124,7 @@ function emitSharedTaskEventQuiet(input) {
|
|
|
7547
8124
|
}
|
|
7548
8125
|
var SOURCE = "todos";
|
|
7549
8126
|
var init_shared_events = __esm(() => {
|
|
7550
|
-
|
|
8127
|
+
init_dist();
|
|
7551
8128
|
init_database();
|
|
7552
8129
|
init_projects();
|
|
7553
8130
|
init_task_lists();
|
|
@@ -10132,7 +10709,9 @@ function listTasks(filter = {}, db) {
|
|
|
10132
10709
|
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
10133
10710
|
} catch {}
|
|
10134
10711
|
}
|
|
10135
|
-
if (
|
|
10712
|
+
if (filter.archived_only) {
|
|
10713
|
+
conditions.push("archived_at IS NOT NULL");
|
|
10714
|
+
} else if (!filter.include_archived) {
|
|
10136
10715
|
conditions.push("archived_at IS NULL");
|
|
10137
10716
|
}
|
|
10138
10717
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -10145,7 +10724,8 @@ function listTasks(filter = {}, db) {
|
|
|
10145
10724
|
params.push(filter.offset);
|
|
10146
10725
|
}
|
|
10147
10726
|
}
|
|
10148
|
-
const
|
|
10727
|
+
const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK}, created_at DESC, id ASC`;
|
|
10728
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
|
|
10149
10729
|
return rows.map(rowToTask);
|
|
10150
10730
|
}
|
|
10151
10731
|
function getTaskByFingerprint(fingerprint, db) {
|
|
@@ -10288,7 +10868,9 @@ function countTasks(filter = {}, db) {
|
|
|
10288
10868
|
}
|
|
10289
10869
|
}
|
|
10290
10870
|
addMetadataConditions(filter.metadata, conditions, params);
|
|
10291
|
-
if (
|
|
10871
|
+
if (filter.archived_only) {
|
|
10872
|
+
conditions.push("archived_at IS NOT NULL");
|
|
10873
|
+
} else if (!filter.include_archived) {
|
|
10292
10874
|
conditions.push("archived_at IS NULL");
|
|
10293
10875
|
}
|
|
10294
10876
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
@@ -10425,6 +11007,10 @@ function updateTaskStored(id, input, db) {
|
|
|
10425
11007
|
sets.push("completed_at = ?");
|
|
10426
11008
|
params.push(input.completed_at);
|
|
10427
11009
|
}
|
|
11010
|
+
if (input.archived_at !== undefined) {
|
|
11011
|
+
sets.push("archived_at = ?");
|
|
11012
|
+
params.push(input.archived_at);
|
|
11013
|
+
}
|
|
10428
11014
|
if (input.confidence !== undefined) {
|
|
10429
11015
|
sets.push("confidence = ?");
|
|
10430
11016
|
params.push(input.confidence);
|
|
@@ -10488,6 +11074,8 @@ function updateTaskStored(id, input, db) {
|
|
|
10488
11074
|
logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
|
|
10489
11075
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
10490
11076
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
11077
|
+
if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
|
|
11078
|
+
logTaskChange(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
|
|
10491
11079
|
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
10492
11080
|
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
10493
11081
|
if (input.approved_by !== undefined)
|
|
@@ -11188,7 +11776,13 @@ function resolveCreateSlug(input, projectId, db) {
|
|
|
11188
11776
|
}
|
|
11189
11777
|
return nextPlanSlug(planSlugBase2(input.name), projectId, db);
|
|
11190
11778
|
}
|
|
11779
|
+
function assertLegacyPlanInput(input) {
|
|
11780
|
+
if (input.start_date !== undefined || input.end_date !== undefined || input.status === "planning" || input.status === "cancelled") {
|
|
11781
|
+
throw new Error("Plan calendar dates and planning/cancelled statuses require the shared Todos API; explicit SQLite storage cannot retain them");
|
|
11782
|
+
}
|
|
11783
|
+
}
|
|
11191
11784
|
function createPlan(input, db) {
|
|
11785
|
+
assertLegacyPlanInput(input);
|
|
11192
11786
|
const d = db || getDatabase();
|
|
11193
11787
|
const id = uuid();
|
|
11194
11788
|
const timestamp2 = now();
|
|
@@ -11269,6 +11863,7 @@ function updatePlanStored(id, input, db) {
|
|
|
11269
11863
|
return updated;
|
|
11270
11864
|
}
|
|
11271
11865
|
function updatePlan(id, input, db) {
|
|
11866
|
+
assertLegacyPlanInput(input);
|
|
11272
11867
|
const d = db || getDatabase();
|
|
11273
11868
|
return d.transaction(() => {
|
|
11274
11869
|
guardPlanRowsSqlite([id], d);
|
|
@@ -11743,27 +12338,27 @@ var init_boards = __esm(() => {
|
|
|
11743
12338
|
// src/lib/artifact-store.ts
|
|
11744
12339
|
import { createHash as createHash3 } from "crypto";
|
|
11745
12340
|
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
11746
|
-
import { basename, dirname as dirname4, join as
|
|
12341
|
+
import { basename, dirname as dirname4, join as join6, resolve as resolve9 } from "path";
|
|
11747
12342
|
import { tmpdir as tmpdir2 } from "os";
|
|
11748
12343
|
function isInMemoryDb2(path) {
|
|
11749
12344
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
11750
12345
|
}
|
|
11751
12346
|
function artifactStoreRoot() {
|
|
11752
12347
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
11753
|
-
return
|
|
12348
|
+
return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
11754
12349
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
11755
|
-
return
|
|
12350
|
+
return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
11756
12351
|
const dbPath = getDatabasePath();
|
|
11757
12352
|
if (isInMemoryDb2(dbPath))
|
|
11758
|
-
return
|
|
11759
|
-
return
|
|
12353
|
+
return join6(tmpdir2(), "hasna-todos-artifacts");
|
|
12354
|
+
return join6(dirname4(resolve9(dbPath)), "artifacts");
|
|
11760
12355
|
}
|
|
11761
12356
|
function artifactStorePath(relativePath) {
|
|
11762
12357
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
11763
12358
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
11764
12359
|
throw new Error("Invalid artifact store path");
|
|
11765
12360
|
}
|
|
11766
|
-
return
|
|
12361
|
+
return join6(artifactStoreRoot(), normalized);
|
|
11767
12362
|
}
|
|
11768
12363
|
function sha256(buffer) {
|
|
11769
12364
|
return createHash3("sha256").update(buffer).digest("hex");
|
|
@@ -11803,7 +12398,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
11803
12398
|
return "application/octet-stream";
|
|
11804
12399
|
}
|
|
11805
12400
|
function storeArtifactContent(input) {
|
|
11806
|
-
const sourcePath =
|
|
12401
|
+
const sourcePath = resolve9(input.path);
|
|
11807
12402
|
if (!existsSync8(sourcePath))
|
|
11808
12403
|
return null;
|
|
11809
12404
|
const sourceStat = statSync2(sourcePath);
|
|
@@ -11821,7 +12416,7 @@ function storeArtifactContent(input) {
|
|
|
11821
12416
|
redactionStatus = "redacted";
|
|
11822
12417
|
}
|
|
11823
12418
|
const storedSha = sha256(storedBuffer);
|
|
11824
|
-
const relativePath =
|
|
12419
|
+
const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
11825
12420
|
const destination = artifactStorePath(relativePath);
|
|
11826
12421
|
if (!existsSync8(destination)) {
|
|
11827
12422
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
@@ -11958,19 +12553,19 @@ function importStoredArtifactContent(content) {
|
|
|
11958
12553
|
}
|
|
11959
12554
|
function getArtifactStoreRoot(dbPath) {
|
|
11960
12555
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
11961
|
-
return
|
|
12556
|
+
return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
11962
12557
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
11963
|
-
return
|
|
12558
|
+
return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
11964
12559
|
const path = dbPath ?? getDatabasePath();
|
|
11965
12560
|
if (isInMemoryDb2(path))
|
|
11966
|
-
return
|
|
11967
|
-
return
|
|
12561
|
+
return join6(tmpdir2(), "hasna-todos-artifacts");
|
|
12562
|
+
return join6(dirname4(resolve9(path)), "artifacts");
|
|
11968
12563
|
}
|
|
11969
12564
|
function computeContentHash(path) {
|
|
11970
|
-
return sha256(readFileSync4(
|
|
12565
|
+
return sha256(readFileSync4(resolve9(path)));
|
|
11971
12566
|
}
|
|
11972
12567
|
function storeArtifactFile(input) {
|
|
11973
|
-
const sourcePath =
|
|
12568
|
+
const sourcePath = resolve9(input.sourcePath);
|
|
11974
12569
|
if (!existsSync8(sourcePath)) {
|
|
11975
12570
|
throw new Error(`Source file not found: ${input.sourcePath}`);
|
|
11976
12571
|
}
|
|
@@ -11984,7 +12579,7 @@ function storeArtifactFile(input) {
|
|
|
11984
12579
|
let localPath = sourcePath;
|
|
11985
12580
|
if (storageMode === "copy") {
|
|
11986
12581
|
const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
|
|
11987
|
-
const destination =
|
|
12582
|
+
const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
|
|
11988
12583
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
11989
12584
|
writeFileSync2(destination, buffer);
|
|
11990
12585
|
localPath = destination;
|
|
@@ -12019,7 +12614,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
|
|
|
12019
12614
|
};
|
|
12020
12615
|
}
|
|
12021
12616
|
function writeArtifactExportManifest(manifest, outputPath) {
|
|
12022
|
-
const destination =
|
|
12617
|
+
const destination = resolve9(outputPath);
|
|
12023
12618
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
12024
12619
|
writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
|
|
12025
12620
|
`);
|
|
@@ -12707,7 +13302,9 @@ function finishTaskRunTransaction(input, db) {
|
|
|
12707
13302
|
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
12708
13303
|
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
12709
13304
|
if (!run) {
|
|
12710
|
-
|
|
13305
|
+
if (input.run_id)
|
|
13306
|
+
throw new InputValidationError(`Run not found: ${input.run_id}`, "Pass an existing run id, or --key.");
|
|
13307
|
+
throw new InputValidationError("runs finish requires a run id or --key", "Pass run_id, or key with task_id.");
|
|
12711
13308
|
}
|
|
12712
13309
|
if (input.task_id && run.task_id !== input.task_id) {
|
|
12713
13310
|
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
@@ -13176,7 +13773,7 @@ var init_tasks = __esm(() => {
|
|
|
13176
13773
|
// package.json
|
|
13177
13774
|
var package_default = {
|
|
13178
13775
|
name: "@hasna/todos",
|
|
13179
|
-
version: "0.
|
|
13776
|
+
version: "0.17.0",
|
|
13180
13777
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
13181
13778
|
type: "module",
|
|
13182
13779
|
main: "dist/index.js",
|
|
@@ -13229,37 +13826,40 @@ var package_default = {
|
|
|
13229
13826
|
}
|
|
13230
13827
|
},
|
|
13231
13828
|
workspaces: [
|
|
13232
|
-
"dashboard",
|
|
13233
13829
|
"ai"
|
|
13234
13830
|
],
|
|
13235
13831
|
files: [
|
|
13236
13832
|
"dist",
|
|
13237
|
-
"dashboard/dist",
|
|
13238
13833
|
"postinstall.js",
|
|
13239
13834
|
"LICENSE",
|
|
13240
|
-
"README.md"
|
|
13835
|
+
"README.md",
|
|
13836
|
+
"CHANGELOG.md",
|
|
13837
|
+
"docs/PLAN_API.md",
|
|
13838
|
+
"docs/TASK_LIST_API.md",
|
|
13839
|
+
"docs/TEMPLATE_API.md",
|
|
13840
|
+
"docs/TASK_QUERY_API.md",
|
|
13841
|
+
"docs/native-storage.md"
|
|
13241
13842
|
],
|
|
13242
13843
|
scripts: {
|
|
13243
|
-
build: "
|
|
13844
|
+
build: "cd ai && bun install --frozen-lockfile && cd .. && rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
13244
13845
|
"build:js": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
13245
13846
|
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
13246
13847
|
migrate: "bun run src/server/index.ts migrate",
|
|
13247
13848
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
13248
13849
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
13249
|
-
"build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
|
|
13250
13850
|
typecheck: "tsc --noEmit -p tsconfig.typecheck.json",
|
|
13251
|
-
test: "bun test",
|
|
13252
|
-
"test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
|
|
13851
|
+
test: "bun test --timeout 120000",
|
|
13852
|
+
"test:no-cloud": "bun test --timeout 120000 src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
|
|
13253
13853
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
13254
13854
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
13255
13855
|
"dev:serve": "bun run src/server/index.ts",
|
|
13256
13856
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
13257
13857
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
13258
13858
|
"verify:attested-container-candidate": "bun run scripts/attested-container-candidate.ts verify",
|
|
13259
|
-
"test:attested-container-candidate": "bun test scripts/attested-container-candidate.test.ts",
|
|
13859
|
+
"test:attested-container-candidate": "bun test --timeout 120000 scripts/attested-container-candidate.test.ts",
|
|
13260
13860
|
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
13261
13861
|
"verify:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts verify",
|
|
13262
|
-
"test:attested-container-compatibility-vector": "bun test scripts/attested-container-compatibility-vector.test.ts",
|
|
13862
|
+
"test:attested-container-compatibility-vector": "bun test --timeout 120000 scripts/attested-container-compatibility-vector.test.ts",
|
|
13263
13863
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
13264
13864
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
13265
13865
|
postinstall: "node postinstall.js"
|
|
@@ -13283,11 +13883,12 @@ var package_default = {
|
|
|
13283
13883
|
},
|
|
13284
13884
|
repository: {
|
|
13285
13885
|
type: "git",
|
|
13286
|
-
url: "https://github.com/hasna/
|
|
13886
|
+
url: "https://github.com/hasna/apps.git",
|
|
13887
|
+
directory: "apps/todos"
|
|
13287
13888
|
},
|
|
13288
|
-
homepage: "https://github.com/hasna/todos",
|
|
13889
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
|
|
13289
13890
|
bugs: {
|
|
13290
|
-
url: "https://github.com/hasna/
|
|
13891
|
+
url: "https://github.com/hasna/apps/issues"
|
|
13291
13892
|
},
|
|
13292
13893
|
engines: {
|
|
13293
13894
|
bun: ">=1.0.0"
|
|
@@ -13296,9 +13897,8 @@ var package_default = {
|
|
|
13296
13897
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
13297
13898
|
license: "Apache-2.0",
|
|
13298
13899
|
dependencies: {
|
|
13299
|
-
"@hasna/contracts": "0.
|
|
13900
|
+
"@hasna/contracts": "1.0.2",
|
|
13300
13901
|
"@hasna/events": "^0.1.11",
|
|
13301
|
-
"@hasna/paths": "0.1.0",
|
|
13302
13902
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
13303
13903
|
chalk: "^5.4.1",
|
|
13304
13904
|
commander: "^13.1.0",
|
|
@@ -13327,6 +13927,7 @@ function getPackageVersion(_fromUrl = import.meta.url) {
|
|
|
13327
13927
|
}
|
|
13328
13928
|
|
|
13329
13929
|
// src/mcp/token-utils.ts
|
|
13930
|
+
init_env();
|
|
13330
13931
|
var CORE_MCP_TOOLS = new Set([
|
|
13331
13932
|
"add_comment",
|
|
13332
13933
|
"bootstrap",
|
|
@@ -13708,7 +14309,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
13708
14309
|
"machines_topology",
|
|
13709
14310
|
"machines_unarchive"
|
|
13710
14311
|
],
|
|
13711
|
-
maintenance: ["
|
|
14312
|
+
maintenance: ["get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task"]
|
|
13712
14313
|
};
|
|
13713
14314
|
var MCP_PROFILE_GROUPS = {
|
|
13714
14315
|
minimal: ["core", "loops"],
|
|
@@ -13728,7 +14329,7 @@ function addGroupTools(toolNames, groupName) {
|
|
|
13728
14329
|
toolNames.add(tool);
|
|
13729
14330
|
return true;
|
|
13730
14331
|
}
|
|
13731
|
-
function shouldRegisterToolForProfile(name, profileValue =
|
|
14332
|
+
function shouldRegisterToolForProfile(name, profileValue = env.profile(), groupValue = env.toolGroups()) {
|
|
13732
14333
|
const profileTokens = splitTokens(profileValue || "minimal");
|
|
13733
14334
|
const groupTokens = splitTokens(groupValue);
|
|
13734
14335
|
if (profileTokens.includes("full") || profileTokens.includes("all"))
|
|
@@ -13800,8 +14401,8 @@ function createMcpManifest(options = {}) {
|
|
|
13800
14401
|
name: "todos",
|
|
13801
14402
|
binary: "todos-mcp",
|
|
13802
14403
|
transport: "stdio",
|
|
13803
|
-
profileEnvironmentVariable: "
|
|
13804
|
-
groupEnvironmentVariable: "
|
|
14404
|
+
profileEnvironmentVariable: "HASNA_TODOS_PROFILE",
|
|
14405
|
+
groupEnvironmentVariable: "HASNA_TODOS_TOOL_GROUPS"
|
|
13805
14406
|
},
|
|
13806
14407
|
groups: MCP_TOOL_GROUPS,
|
|
13807
14408
|
profiles: MCP_PROFILE_GROUPS,
|
|
@@ -14781,7 +15382,7 @@ var TODOS_JSON_CONTRACTS = [
|
|
|
14781
15382
|
contract({
|
|
14782
15383
|
id: "tester_issue_report",
|
|
14783
15384
|
name: "Tester Issue Report",
|
|
14784
|
-
description: "Generic issue report payload emitted by
|
|
15385
|
+
description: "Generic issue report payload emitted by the testers QA CLI or compatible test runners for conversion into local tasks.",
|
|
14785
15386
|
surfaces: ["cli", "sdk"],
|
|
14786
15387
|
stability: "stable",
|
|
14787
15388
|
required: {
|
|
@@ -16206,14 +16807,15 @@ function normalizeApprovalRefs(values) {
|
|
|
16206
16807
|
function resolveTodosAiCommandOptions(input) {
|
|
16207
16808
|
const cli = input.cli ?? {};
|
|
16208
16809
|
const config = input.config ?? {};
|
|
16209
|
-
const
|
|
16210
|
-
const
|
|
16211
|
-
const
|
|
16810
|
+
const env2 = input.env ?? {};
|
|
16811
|
+
const envFormat = env2["HASNA_TODOS_AI_FORMAT"] ?? env2["TODOS_AI_FORMAT"];
|
|
16812
|
+
const format = parseEnum(selectedString(cli.format, envFormat, config.format), TODOS_AI_FORMATS, "format", TODOS_AI_DEFAULTS.format);
|
|
16813
|
+
const configuredWriteMode = parseEnum(selectedString(cli.writeMode, env2["TODOS_AI_WRITE_MODE"], config.write_mode), TODOS_AI_WRITE_MODES, "write mode", TODOS_AI_DEFAULTS.write_mode);
|
|
16212
16814
|
if (cli.dryRun && cli.writeMode === "execute") {
|
|
16213
16815
|
throw new TodosAiContractError("invalid_configuration", "--dry-run cannot be combined with --write-mode execute");
|
|
16214
16816
|
}
|
|
16215
16817
|
const writeMode = cli.dryRun ? "plan" : configuredWriteMode;
|
|
16216
|
-
const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode,
|
|
16818
|
+
const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode, env2["TODOS_AI_APPROVAL_MODE"], config.approval_mode);
|
|
16217
16819
|
const defaultApproval = writeMode === "execute" ? input.interactive ? "prompt" : "required" : "deny";
|
|
16218
16820
|
const approvalMode = cli.dryRun ? parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", "deny") : parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", defaultApproval);
|
|
16219
16821
|
const approvalRefs = normalizeApprovalRefs(cli.approvalRefs);
|
|
@@ -16236,12 +16838,12 @@ function resolveTodosAiCommandOptions(input) {
|
|
|
16236
16838
|
throw new TodosAiContractError("invalid_configuration", "--approval references require --approval-mode existing");
|
|
16237
16839
|
}
|
|
16238
16840
|
return {
|
|
16239
|
-
provider: selectedString(cli.provider,
|
|
16240
|
-
model: selectedString(cli.model,
|
|
16241
|
-
profile: selectedString(cli.profile,
|
|
16841
|
+
provider: selectedString(cli.provider, env2["TODOS_AI_PROVIDER"], config.provider),
|
|
16842
|
+
model: selectedString(cli.model, env2["TODOS_AI_MODEL"], config.model),
|
|
16843
|
+
profile: selectedString(cli.profile, env2["TODOS_AI_PROFILE"], config.profile),
|
|
16242
16844
|
format,
|
|
16243
|
-
max_steps: parseBoundedInteger(cli.maxSteps ??
|
|
16244
|
-
timeout_ms: parseBoundedInteger(cli.timeoutMs ??
|
|
16845
|
+
max_steps: parseBoundedInteger(cli.maxSteps ?? env2["TODOS_AI_MAX_STEPS"] ?? config.max_steps, "max steps", TODOS_AI_DEFAULTS.max_steps, TODOS_AI_LIMITS.min_steps, TODOS_AI_LIMITS.max_steps),
|
|
16846
|
+
timeout_ms: parseBoundedInteger(cli.timeoutMs ?? env2["TODOS_AI_TIMEOUT_MS"] ?? config.timeout_ms, "timeout", TODOS_AI_DEFAULTS.timeout_ms, TODOS_AI_LIMITS.min_timeout_ms, TODOS_AI_LIMITS.max_timeout_ms),
|
|
16245
16847
|
write_mode: writeMode,
|
|
16246
16848
|
approval_mode: approvalMode,
|
|
16247
16849
|
approval_refs: approvalRefs,
|
|
@@ -16539,7 +17141,7 @@ function todosAiExitCodeForResult(result) {
|
|
|
16539
17141
|
}
|
|
16540
17142
|
// src/lib/onboarding-fixtures.ts
|
|
16541
17143
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
16542
|
-
import { join as
|
|
17144
|
+
import { join as join7 } from "path";
|
|
16543
17145
|
|
|
16544
17146
|
// src/lib/local-bridge.ts
|
|
16545
17147
|
init_database();
|
|
@@ -17476,7 +18078,7 @@ function writeOnboardingFixtureFiles(directory) {
|
|
|
17476
18078
|
mkdirSync5(directory, { recursive: true });
|
|
17477
18079
|
const files = [];
|
|
17478
18080
|
for (const fixture of allFixtures()) {
|
|
17479
|
-
const path =
|
|
18081
|
+
const path = join7(directory, `${fixture.summary.name}.bridge.json`);
|
|
17480
18082
|
writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
|
|
17481
18083
|
`, "utf-8");
|
|
17482
18084
|
files.push(path);
|
|
@@ -17494,7 +18096,7 @@ function importOnboardingFixture(options = {}) {
|
|
|
17494
18096
|
init_database();
|
|
17495
18097
|
import { createHash as createHash4 } from "crypto";
|
|
17496
18098
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
17497
|
-
import { dirname as dirname5, resolve as
|
|
18099
|
+
import { dirname as dirname5, resolve as resolve10 } from "path";
|
|
17498
18100
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
17499
18101
|
var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
|
|
17500
18102
|
var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
|
|
@@ -17601,14 +18203,14 @@ function createLocalBackup(options = {}, db) {
|
|
|
17601
18203
|
return backup;
|
|
17602
18204
|
}
|
|
17603
18205
|
function writeLocalBackupFile(backup, outputPath) {
|
|
17604
|
-
const path =
|
|
18206
|
+
const path = resolve10(outputPath);
|
|
17605
18207
|
mkdirSync6(dirname5(path), { recursive: true });
|
|
17606
18208
|
writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
|
|
17607
18209
|
`);
|
|
17608
18210
|
return path;
|
|
17609
18211
|
}
|
|
17610
18212
|
function readLocalBackupFile(path) {
|
|
17611
|
-
return JSON.parse(readFileSync5(
|
|
18213
|
+
return JSON.parse(readFileSync5(resolve10(path), "utf-8"));
|
|
17612
18214
|
}
|
|
17613
18215
|
function verifyLocalBackup(value, options = {}, db) {
|
|
17614
18216
|
const verifiedAt = options.verified_at ?? now();
|
|
@@ -18302,7 +18904,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
|
|
|
18302
18904
|
}
|
|
18303
18905
|
// src/lib/sdk-integration-fixtures.ts
|
|
18304
18906
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
18305
|
-
import { join as
|
|
18907
|
+
import { join as join8 } from "path";
|
|
18306
18908
|
|
|
18307
18909
|
// src/cli-mcp-parity.ts
|
|
18308
18910
|
function source4(version) {
|
|
@@ -19132,17 +19734,23 @@ var TODOS_CLI_MCP_PARITY = [
|
|
|
19132
19734
|
"todos extract",
|
|
19133
19735
|
"todos extract-watch"
|
|
19134
19736
|
],
|
|
19135
|
-
mcpTools: [
|
|
19136
|
-
"extract_todos",
|
|
19137
|
-
"watch_source_todos"
|
|
19138
|
-
],
|
|
19737
|
+
mcpTools: [],
|
|
19139
19738
|
jsonContracts: ["source_code_index", "source_todo_comment", "task", "structured_error", "api_error"],
|
|
19140
19739
|
errorContracts: ["structured_error", "api_error"],
|
|
19141
|
-
status: "
|
|
19142
|
-
intentionalGaps: [
|
|
19740
|
+
status: "intentional-gap",
|
|
19741
|
+
intentionalGaps: [
|
|
19742
|
+
{
|
|
19743
|
+
cliCommand: "todos extract",
|
|
19744
|
+
reason: "The source TODO index scans the local checkout the CLI runs in; the former extract_todos MCP tool read the agent host's filesystem behind a task-store tool surface and had no hosted arm, so it was removed (fleet alignment 2026-09-11)."
|
|
19745
|
+
},
|
|
19746
|
+
{
|
|
19747
|
+
cliCommand: "todos extract-watch",
|
|
19748
|
+
reason: "A polling filesystem watcher is a long-running local process, not an MCP tool call; the former watch_source_todos tool was removed with extract_todos \u2014 run `todos extract-watch` from the checkout instead."
|
|
19749
|
+
}
|
|
19750
|
+
],
|
|
19751
|
+
gapReason: "Source scanning and watching are local-filesystem CLI operations; no MCP tool mapping.",
|
|
19143
19752
|
example: {
|
|
19144
|
-
cli: "todos extract . --dry-run --index --json"
|
|
19145
|
-
mcpTool: "extract_todos"
|
|
19753
|
+
cli: "todos extract . --dry-run --index --json"
|
|
19146
19754
|
}
|
|
19147
19755
|
},
|
|
19148
19756
|
{
|
|
@@ -20273,7 +20881,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
|
|
|
20273
20881
|
];
|
|
20274
20882
|
const written = [];
|
|
20275
20883
|
for (const [name, payload] of files) {
|
|
20276
|
-
const file =
|
|
20884
|
+
const file = join8(directory, name);
|
|
20277
20885
|
writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
|
|
20278
20886
|
`, "utf-8");
|
|
20279
20887
|
written.push(file);
|
|
@@ -21556,11 +22164,11 @@ function renderLocalAuditLedgerMarkdown(ledger) {
|
|
|
21556
22164
|
init_migrations();
|
|
21557
22165
|
init_schema();
|
|
21558
22166
|
import { readFileSync as readFileSync6 } from "fs";
|
|
21559
|
-
import { join as
|
|
22167
|
+
import { join as join9, resolve as resolve11 } from "path";
|
|
21560
22168
|
import { Database as Database2 } from "bun:sqlite";
|
|
21561
22169
|
var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
|
|
21562
22170
|
var EXPECTED_PACKAGE_NAME = "@hasna/todos";
|
|
21563
|
-
var EXPECTED_REPOSITORY = "https://github.com/hasna/
|
|
22171
|
+
var EXPECTED_REPOSITORY = "https://github.com/hasna/apps.git";
|
|
21564
22172
|
var EXPECTED_BINS = ["todos", "todos-mcp", "todos-serve"];
|
|
21565
22173
|
var EXPECTED_EXPORTS = [".", "./sdk", "./mcp", "./registry", "./contracts", "./storage"];
|
|
21566
22174
|
var REQUIRED_SCRIPTS = ["build", "test:no-cloud", "verify:release", "prepublishOnly"];
|
|
@@ -21602,7 +22210,7 @@ function warn(id, message, details) {
|
|
|
21602
22210
|
return { id, status: "warning", message, details };
|
|
21603
22211
|
}
|
|
21604
22212
|
function readPackageJson(root) {
|
|
21605
|
-
return JSON.parse(readFileSync6(
|
|
22213
|
+
return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
|
|
21606
22214
|
}
|
|
21607
22215
|
function sortedKeys(value) {
|
|
21608
22216
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -21664,7 +22272,7 @@ function checkPackage(packageJson) {
|
|
|
21664
22272
|
const checks = [];
|
|
21665
22273
|
checks.push(packageJson.name === EXPECTED_PACKAGE_NAME ? pass("package-name", "Package name is @hasna/todos.") : fail("package-name", "Package name must be @hasna/todos.", { actual: packageJson.name ?? null }));
|
|
21666
22274
|
checks.push(packageJson.publishConfig?.access === "public" ? pass("publish-access", "Package publish access is public.") : fail("publish-access", "Package publish access must be public.", { actual: packageJson.publishConfig?.access ?? null }));
|
|
21667
|
-
checks.push(packageJson.repository?.url === EXPECTED_REPOSITORY ? pass("repository", "Repository points at hasna/
|
|
22275
|
+
checks.push(packageJson.repository?.url === EXPECTED_REPOSITORY ? pass("repository", "Repository points at hasna/apps.") : fail("repository", "Repository must point at hasna/apps.", { actual: packageJson.repository?.url ?? null }));
|
|
21668
22276
|
const dependencyNames = Object.keys(packageJson.dependencies ?? {});
|
|
21669
22277
|
const privateStudio = `hasna${"studio"}`;
|
|
21670
22278
|
const privatePlatform = `platform${"-"}todos`;
|
|
@@ -21698,7 +22306,7 @@ function checkChangelog() {
|
|
|
21698
22306
|
];
|
|
21699
22307
|
}
|
|
21700
22308
|
function createReleaseCompatibilityReport(options = {}) {
|
|
21701
|
-
const root =
|
|
22309
|
+
const root = resolve11(options.root ?? process.cwd());
|
|
21702
22310
|
const packageJson = readPackageJson(root);
|
|
21703
22311
|
const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
|
|
21704
22312
|
const checks = [
|
|
@@ -24028,22 +24636,22 @@ function millisBetween(start, end) {
|
|
|
24028
24636
|
return 0;
|
|
24029
24637
|
return endMs - startMs;
|
|
24030
24638
|
}
|
|
24031
|
-
function addTaskScope(where, params, options, db,
|
|
24639
|
+
function addTaskScope(where, params, options, db, alias2 = "t") {
|
|
24032
24640
|
if (options.project_id) {
|
|
24033
|
-
where.push(`${
|
|
24641
|
+
where.push(`${alias2}.project_id = ?`);
|
|
24034
24642
|
params.push(options.project_id);
|
|
24035
24643
|
}
|
|
24036
24644
|
if (options.agent_id) {
|
|
24037
24645
|
params.push(options.agent_id);
|
|
24038
|
-
const assignedInClause = lowerInClause(`${
|
|
24039
|
-
where.push(`(${
|
|
24646
|
+
const assignedInClause = lowerInClause(`${alias2}.assigned_to`, resolveAssignedToAliases(db, options.agent_id), params);
|
|
24647
|
+
where.push(`(${alias2}.agent_id = ? OR ${assignedInClause})`);
|
|
24040
24648
|
}
|
|
24041
24649
|
if (options.since) {
|
|
24042
|
-
where.push(`${
|
|
24650
|
+
where.push(`${alias2}.created_at >= ?`);
|
|
24043
24651
|
params.push(options.since);
|
|
24044
24652
|
}
|
|
24045
24653
|
if (options.until) {
|
|
24046
|
-
where.push(`${
|
|
24654
|
+
where.push(`${alias2}.created_at <= ?`);
|
|
24047
24655
|
params.push(options.until);
|
|
24048
24656
|
}
|
|
24049
24657
|
}
|
|
@@ -24712,16 +25320,25 @@ var DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY";
|
|
|
24712
25320
|
class EncryptionKeyUnavailableError extends Error {
|
|
24713
25321
|
keyEnv;
|
|
24714
25322
|
profile;
|
|
25323
|
+
static code = "ENCRYPTION_KEY_UNAVAILABLE";
|
|
25324
|
+
code = EncryptionKeyUnavailableError.code;
|
|
25325
|
+
suggestion;
|
|
24715
25326
|
constructor(keyEnv, profile) {
|
|
24716
25327
|
super(`Encryption key is locked: set ${keyEnv} to use profile ${profile}`);
|
|
24717
25328
|
this.keyEnv = keyEnv;
|
|
24718
25329
|
this.profile = profile;
|
|
25330
|
+
this.name = "EncryptionKeyUnavailableError";
|
|
25331
|
+
this.suggestion = `Set ${keyEnv} in the environment of the process serving this store, then retry.`;
|
|
24719
25332
|
}
|
|
24720
25333
|
}
|
|
24721
25334
|
|
|
24722
25335
|
class EncryptedPayloadError extends Error {
|
|
25336
|
+
static code = "ENCRYPTED_PAYLOAD_INVALID";
|
|
25337
|
+
code = EncryptedPayloadError.code;
|
|
25338
|
+
static suggestion = "Pass a value produced by encrypt_local_value for this profile; a plain value is not an encrypted envelope.";
|
|
24723
25339
|
constructor(message) {
|
|
24724
25340
|
super(message);
|
|
25341
|
+
this.name = "EncryptedPayloadError";
|
|
24725
25342
|
}
|
|
24726
25343
|
}
|
|
24727
25344
|
function now3() {
|
|
@@ -24793,17 +25410,17 @@ function removeEncryptionProfile(name) {
|
|
|
24793
25410
|
saveConfig({ ...config, encryption_profiles: next });
|
|
24794
25411
|
return true;
|
|
24795
25412
|
}
|
|
24796
|
-
function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE,
|
|
25413
|
+
function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env2 = process.env) {
|
|
24797
25414
|
const profile = ensureEncryptionProfile(name);
|
|
24798
25415
|
return {
|
|
24799
25416
|
profile: redactValue(profile),
|
|
24800
|
-
locked: !
|
|
25417
|
+
locked: !env2[profile.key_env],
|
|
24801
25418
|
key_env: profile.key_env,
|
|
24802
|
-
key_present: Boolean(
|
|
25419
|
+
key_present: Boolean(env2[profile.key_env])
|
|
24803
25420
|
};
|
|
24804
25421
|
}
|
|
24805
|
-
function keyForProfile(profile,
|
|
24806
|
-
const secret =
|
|
25422
|
+
function keyForProfile(profile, env2) {
|
|
25423
|
+
const secret = env2[profile.key_env];
|
|
24807
25424
|
if (!secret)
|
|
24808
25425
|
throw new EncryptionKeyUnavailableError(profile.key_env, profile.name);
|
|
24809
25426
|
return deriveKey(secret, profile.salt);
|
|
@@ -24834,7 +25451,7 @@ function isEncryptedValue(value) {
|
|
|
24834
25451
|
const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
24835
25452
|
return Boolean(record && record.kind === TODOS_ENCRYPTED_VALUE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
|
|
24836
25453
|
}
|
|
24837
|
-
function decryptString(envelope,
|
|
25454
|
+
function decryptString(envelope, env2 = process.env) {
|
|
24838
25455
|
if (!isEncryptedValue(envelope))
|
|
24839
25456
|
throw new EncryptedPayloadError("value is not a hasna/todos encrypted envelope");
|
|
24840
25457
|
const profile = profileFromConfig(envelope.profile) ?? {
|
|
@@ -24844,7 +25461,7 @@ function decryptString(envelope, env = process.env) {
|
|
|
24844
25461
|
key_env: envelope.key_env,
|
|
24845
25462
|
salt: envelope.salt
|
|
24846
25463
|
};
|
|
24847
|
-
const key = keyForProfile(profile,
|
|
25464
|
+
const key = keyForProfile(profile, env2);
|
|
24848
25465
|
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"));
|
|
24849
25466
|
decipher.setAuthTag(Buffer.from(envelope.auth_tag, "base64"));
|
|
24850
25467
|
const plaintext = Buffer.concat([
|
|
@@ -24861,8 +25478,8 @@ function decryptString(envelope, env = process.env) {
|
|
|
24861
25478
|
function encryptValue(value, options = {}) {
|
|
24862
25479
|
return encryptString(JSON.stringify(value), options);
|
|
24863
25480
|
}
|
|
24864
|
-
function decryptValue(envelope,
|
|
24865
|
-
return JSON.parse(decryptString(envelope,
|
|
25481
|
+
function decryptValue(envelope, env2 = process.env) {
|
|
25482
|
+
return JSON.parse(decryptString(envelope, env2));
|
|
24866
25483
|
}
|
|
24867
25484
|
function looksSensitiveKey(key) {
|
|
24868
25485
|
return /api[_-]?key|token|secret|password|credential|evidence|artifact|output|summary|metadata/i.test(key);
|
|
@@ -24920,7 +25537,7 @@ function isEncryptedBridgeBundle(value) {
|
|
|
24920
25537
|
const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
24921
25538
|
return Boolean(record && record.kind === TODOS_ENCRYPTED_BRIDGE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
|
|
24922
25539
|
}
|
|
24923
|
-
function decryptBridgeBundle(bundle,
|
|
25540
|
+
function decryptBridgeBundle(bundle, env2 = process.env) {
|
|
24924
25541
|
if (!isEncryptedBridgeBundle(bundle))
|
|
24925
25542
|
throw new EncryptedPayloadError("bundle is not an encrypted hasna/todos bridge export");
|
|
24926
25543
|
const plaintext = decryptString({
|
|
@@ -24936,7 +25553,7 @@ function decryptBridgeBundle(bundle, env = process.env) {
|
|
|
24936
25553
|
auth_tag: bundle.encryption.auth_tag,
|
|
24937
25554
|
ciphertext: bundle.encryption.ciphertext,
|
|
24938
25555
|
plaintext_sha256: bundle.plaintext.sha256
|
|
24939
|
-
},
|
|
25556
|
+
}, env2);
|
|
24940
25557
|
return JSON.parse(plaintext);
|
|
24941
25558
|
}
|
|
24942
25559
|
function assertExportProfileAllowed(profile) {
|
|
@@ -25433,7 +26050,7 @@ function buildCapabilities(version) {
|
|
|
25433
26050
|
id: "server.local-api",
|
|
25434
26051
|
kind: "server",
|
|
25435
26052
|
name: "todos-serve",
|
|
25436
|
-
description: "Start the local HTTP server for task APIs
|
|
26053
|
+
description: "Start the local HTTP server for task APIs.",
|
|
25437
26054
|
tags: ["server", "api", "local"],
|
|
25438
26055
|
docsPath: "README.md#server",
|
|
25439
26056
|
stability: "experimental",
|