@hasna/todos 0.15.52 → 0.16.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 +1884 -0
- package/README.md +314 -37
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/cli/cloud-router.d.ts +99 -51
- 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 +15128 -11093
- 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 +34 -4
- 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/contracts.js +835 -265
- 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.map +1 -1
- package/dist/db/handoffs.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-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 +3862 -1715
- 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/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 +90 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/paths.d.ts +9 -0
- 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 +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +45271 -40452
- 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/environment-snapshots.d.ts.map +1 -1
- package/dist/mcp/tools/machines.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.d.ts +2 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +44 -18
- package/dist/project-registration.js +2829 -556
- package/dist/registry.js +836 -266
- package/dist/release-provenance.json +6 -6
- package/dist/sdk/client.d.ts +51 -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 +1209 -1080
- package/dist/sdk/resolve.d.ts +91 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +219 -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 +20556 -15541
- package/dist/server/openapi.d.ts +1341 -83
- 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/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 +3 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +61 -0
- package/dist/storage/interfaces.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 +2695 -322
- package/dist/task-manifest/postgres.d.ts +1 -1
- package/dist/task-manifest/sqlite.d.ts +1 -1
- package/dist/task-manifest.js +244 -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 +40 -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/contracts.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
|
|
@@ -4075,108 +4192,100 @@ var init_identity_mapping = __esm(() => {
|
|
|
4075
4192
|
});
|
|
4076
4193
|
});
|
|
4077
4194
|
|
|
4078
|
-
//
|
|
4195
|
+
// src/lib/paths.ts
|
|
4196
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4079
4197
|
import { homedir } from "os";
|
|
4080
|
-
import { join } from "path";
|
|
4081
|
-
|
|
4198
|
+
import { join, resolve as resolve2 } from "path";
|
|
4199
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
4200
|
+
import { join as pathsResolverJoin } from "path";
|
|
4201
|
+
function pathsResolverAssertApp(app) {
|
|
4082
4202
|
if (typeof app !== "string" || app.length === 0) {
|
|
4083
4203
|
throw new TypeError("paths: app must be a non-empty string");
|
|
4084
4204
|
}
|
|
4085
|
-
if (!
|
|
4205
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
4086
4206
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
4087
4207
|
}
|
|
4088
4208
|
}
|
|
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";
|
|
4209
|
+
function pathsResolverAssertKind(kind) {
|
|
4210
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
4211
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
4212
|
+
}
|
|
4098
4213
|
}
|
|
4099
|
-
function
|
|
4100
|
-
|
|
4101
|
-
|
|
4214
|
+
function pathsResolverBaseDir(kind, options) {
|
|
4215
|
+
pathsResolverAssertKind(kind);
|
|
4216
|
+
const env2 = options.env ?? process.env;
|
|
4217
|
+
const override = env2[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
4218
|
+
if (typeof override === "string" && override.length > 0)
|
|
4102
4219
|
return override;
|
|
4103
|
-
const home = options.home ??
|
|
4220
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
4104
4221
|
const platform = options.platform ?? process.platform;
|
|
4105
|
-
if (
|
|
4222
|
+
if (platform === "darwin") {
|
|
4106
4223
|
switch (kind) {
|
|
4107
4224
|
case "config":
|
|
4108
4225
|
case "data":
|
|
4109
|
-
return
|
|
4226
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
4110
4227
|
case "cache":
|
|
4111
|
-
return
|
|
4228
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
4112
4229
|
case "state":
|
|
4113
|
-
return
|
|
4230
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
4114
4231
|
}
|
|
4115
4232
|
}
|
|
4116
4233
|
switch (kind) {
|
|
4117
4234
|
case "config":
|
|
4118
|
-
return
|
|
4235
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
4119
4236
|
case "data":
|
|
4120
|
-
return
|
|
4237
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
4121
4238
|
case "state":
|
|
4122
|
-
return
|
|
4239
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
4123
4240
|
case "cache":
|
|
4124
|
-
return
|
|
4241
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
4125
4242
|
}
|
|
4126
4243
|
}
|
|
4127
|
-
function
|
|
4128
|
-
|
|
4129
|
-
const appSegment = options.internal === true ?
|
|
4130
|
-
return
|
|
4244
|
+
function pathsResolverResolve(kind, options) {
|
|
4245
|
+
pathsResolverAssertApp(options.app);
|
|
4246
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
4247
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
4131
4248
|
}
|
|
4132
4249
|
function dataDir(options) {
|
|
4133
|
-
return
|
|
4250
|
+
return pathsResolverResolve("data", options);
|
|
4134
4251
|
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
KIND_ENV = {
|
|
4138
|
-
config: "HASNA_CONFIG_HOME",
|
|
4139
|
-
data: "HASNA_DATA_HOME",
|
|
4140
|
-
state: "HASNA_STATE_HOME",
|
|
4141
|
-
cache: "HASNA_CACHE_HOME"
|
|
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();
|
|
4252
|
+
function effectiveHome(env2 = process.env) {
|
|
4253
|
+
return env2.HOME || env2.USERPROFILE || homedir();
|
|
4152
4254
|
}
|
|
4153
|
-
function legacyHomeDir(
|
|
4154
|
-
return
|
|
4255
|
+
function legacyHomeDir(env2 = process.env) {
|
|
4256
|
+
return join(effectiveHome(env2), ".hasna", "todos");
|
|
4155
4257
|
}
|
|
4156
|
-
function resolverHome(
|
|
4157
|
-
return dataDir({ app: "todos", home: effectiveHome(
|
|
4258
|
+
function resolverHome(env2 = process.env) {
|
|
4259
|
+
return dataDir({ app: "todos", home: effectiveHome(env2), env: env2 });
|
|
4158
4260
|
}
|
|
4159
|
-
function adoptResolverHome(resolved,
|
|
4160
|
-
const dataOverride =
|
|
4261
|
+
function adoptResolverHome(resolved, env2 = process.env) {
|
|
4262
|
+
const dataOverride = env2.HASNA_DATA_HOME;
|
|
4161
4263
|
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
4162
4264
|
return true;
|
|
4163
|
-
return existsSync2(
|
|
4265
|
+
return existsSync2(join(resolved, "todos.db")) || existsSync2(join(resolved, "config.json"));
|
|
4164
4266
|
}
|
|
4165
|
-
function getTodosDir(
|
|
4166
|
-
const resolved = resolverHome(
|
|
4167
|
-
return resolve2(adoptResolverHome(resolved,
|
|
4267
|
+
function getTodosDir(env2 = process.env) {
|
|
4268
|
+
const resolved = resolverHome(env2);
|
|
4269
|
+
return resolve2(adoptResolverHome(resolved, env2) ? resolved : legacyHomeDir(env2));
|
|
4168
4270
|
}
|
|
4271
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
4169
4272
|
var init_paths = __esm(() => {
|
|
4170
|
-
|
|
4273
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
4274
|
+
config: "HASNA_CONFIG_HOME",
|
|
4275
|
+
data: "HASNA_DATA_HOME",
|
|
4276
|
+
state: "HASNA_STATE_HOME",
|
|
4277
|
+
cache: "HASNA_CACHE_HOME"
|
|
4278
|
+
};
|
|
4279
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4171
4280
|
});
|
|
4172
4281
|
|
|
4173
4282
|
// src/lib/sync-utils.ts
|
|
4174
4283
|
import { existsSync as existsSync3, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
4175
4284
|
import { createHash } from "crypto";
|
|
4176
|
-
import { homedir as
|
|
4177
|
-
import { join as
|
|
4285
|
+
import { homedir as homedir2 } from "os";
|
|
4286
|
+
import { join as join2 } from "path";
|
|
4178
4287
|
function getHomeDir() {
|
|
4179
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
4288
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
4180
4289
|
}
|
|
4181
4290
|
function getTodosGlobalDir() {
|
|
4182
4291
|
return getTodosDir();
|
|
@@ -4202,14 +4311,14 @@ function writeJsonFile(path, data) {
|
|
|
4202
4311
|
`);
|
|
4203
4312
|
}
|
|
4204
4313
|
function readHighWaterMark(dir) {
|
|
4205
|
-
const path =
|
|
4314
|
+
const path = join2(dir, ".highwatermark");
|
|
4206
4315
|
if (!existsSync3(path))
|
|
4207
4316
|
return 1;
|
|
4208
4317
|
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
4209
4318
|
return isNaN(val) ? 1 : val;
|
|
4210
4319
|
}
|
|
4211
4320
|
function writeHighWaterMark(dir, value) {
|
|
4212
|
-
writeFileSync(
|
|
4321
|
+
writeFileSync(join2(dir, ".highwatermark"), String(value));
|
|
4213
4322
|
}
|
|
4214
4323
|
function getFileMtimeMs(path) {
|
|
4215
4324
|
try {
|
|
@@ -4294,23 +4403,23 @@ __export(exports_config, {
|
|
|
4294
4403
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4295
4404
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
4296
4405
|
});
|
|
4297
|
-
function getCanonicalTodosRdsConfig(
|
|
4406
|
+
function getCanonicalTodosRdsConfig(env2 = process.env) {
|
|
4298
4407
|
return {
|
|
4299
|
-
cluster: clean(
|
|
4408
|
+
cluster: clean(env2[CANONICAL_TODOS_RDS_CLUSTER_ENV]) ?? null,
|
|
4300
4409
|
database: CANONICAL_TODOS_RDS_DATABASE,
|
|
4301
|
-
runtimeSecretPath: clean(
|
|
4410
|
+
runtimeSecretPath: clean(env2[CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV]) ?? null,
|
|
4302
4411
|
primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
|
|
4303
4412
|
fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
|
|
4304
4413
|
};
|
|
4305
4414
|
}
|
|
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(
|
|
4415
|
+
function loadTodosStorageConfig(env2 = process.env) {
|
|
4416
|
+
const mode = getTodosStorageBackend(env2);
|
|
4417
|
+
const databaseUrl = getTodosStorageDatabaseUrl(env2);
|
|
4418
|
+
const bucket = readStorageEnv(env2, "s3Bucket").value;
|
|
4419
|
+
const prefix = readStorageEnv(env2, "s3Prefix").value ?? "todos/";
|
|
4420
|
+
const region = readStorageEnv(env2, "awsRegion").value;
|
|
4421
|
+
const endpoint = readStorageEnv(env2, "s3Endpoint").value;
|
|
4422
|
+
const schema = readStorageEnv(env2, "databaseSchema").value;
|
|
4314
4423
|
return {
|
|
4315
4424
|
service: "todos",
|
|
4316
4425
|
mode,
|
|
@@ -4318,7 +4427,7 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
4318
4427
|
database: {
|
|
4319
4428
|
provider: "postgres",
|
|
4320
4429
|
url: databaseUrl,
|
|
4321
|
-
ssl: parseBoolean(readStorageEnv(
|
|
4430
|
+
ssl: parseBoolean(readStorageEnv(env2, "databaseSsl").value, true),
|
|
4322
4431
|
...schema ? { schema } : {}
|
|
4323
4432
|
}
|
|
4324
4433
|
} : {},
|
|
@@ -4329,17 +4438,17 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
4329
4438
|
prefix,
|
|
4330
4439
|
...region ? { region } : {},
|
|
4331
4440
|
...endpoint ? { endpoint } : {},
|
|
4332
|
-
forcePathStyle: parseBoolean(readStorageEnv(
|
|
4441
|
+
forcePathStyle: parseBoolean(readStorageEnv(env2, "s3ForcePathStyle").value, false)
|
|
4333
4442
|
}
|
|
4334
4443
|
} : {},
|
|
4335
4444
|
sync: {
|
|
4336
|
-
batchSize: parsePositiveInteger(readStorageEnv(
|
|
4337
|
-
dryRun: parseBoolean(readStorageEnv(
|
|
4445
|
+
batchSize: parsePositiveInteger(readStorageEnv(env2, "syncBatchSize").value, 500),
|
|
4446
|
+
dryRun: parseBoolean(readStorageEnv(env2, "syncDryRun").value, false)
|
|
4338
4447
|
}
|
|
4339
4448
|
};
|
|
4340
4449
|
}
|
|
4341
|
-
function loadStorageConfig(
|
|
4342
|
-
return loadTodosStorageConfig(
|
|
4450
|
+
function loadStorageConfig(env2 = process.env) {
|
|
4451
|
+
return loadTodosStorageConfig(env2);
|
|
4343
4452
|
}
|
|
4344
4453
|
function isTodosPostgresBackend(config) {
|
|
4345
4454
|
return config.mode === "postgres";
|
|
@@ -4361,51 +4470,51 @@ function parseStorageBackend(value) {
|
|
|
4361
4470
|
return "postgres";
|
|
4362
4471
|
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4363
4472
|
}
|
|
4364
|
-
function getTodosStorageBackend(
|
|
4365
|
-
if (!getTodosStorageDatabaseUrl(
|
|
4473
|
+
function getTodosStorageBackend(env2 = process.env) {
|
|
4474
|
+
if (!getTodosStorageDatabaseUrl(env2))
|
|
4366
4475
|
return "sqlite";
|
|
4367
|
-
if (isTodosShadowEnabled(
|
|
4476
|
+
if (isTodosShadowEnabled(env2))
|
|
4368
4477
|
return "sqlite";
|
|
4369
4478
|
return "postgres";
|
|
4370
4479
|
}
|
|
4371
|
-
function isTodosShadowEnabled(
|
|
4372
|
-
return parseBoolean(readStorageEnv(
|
|
4480
|
+
function isTodosShadowEnabled(env2 = process.env) {
|
|
4481
|
+
return parseBoolean(readStorageEnv(env2, "shadow").value, false);
|
|
4373
4482
|
}
|
|
4374
|
-
function getTodosStorageShadowEnvName(
|
|
4375
|
-
return readStorageEnv(
|
|
4483
|
+
function getTodosStorageShadowEnvName(env2 = process.env) {
|
|
4484
|
+
return readStorageEnv(env2, "shadow").name;
|
|
4376
4485
|
}
|
|
4377
|
-
function assertTodosShadowConfig(config,
|
|
4378
|
-
if (!isTodosShadowEnabled(
|
|
4486
|
+
function assertTodosShadowConfig(config, env2 = process.env) {
|
|
4487
|
+
if (!isTodosShadowEnabled(env2))
|
|
4379
4488
|
return;
|
|
4380
4489
|
if (config.mode !== "sqlite") {
|
|
4381
|
-
throw new Error(`${readStorageEnv(
|
|
4490
|
+
throw new Error(`${readStorageEnv(env2, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
4382
4491
|
}
|
|
4383
4492
|
if (!config.database?.url) {
|
|
4384
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(
|
|
4493
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env2, "shadow").name} is enabled`);
|
|
4385
4494
|
}
|
|
4386
4495
|
}
|
|
4387
|
-
function getTodosStorageDatabaseEnv(
|
|
4388
|
-
return readStorageEnv(
|
|
4496
|
+
function getTodosStorageDatabaseEnv(env2 = process.env) {
|
|
4497
|
+
return readStorageEnv(env2, "databaseUrl").name;
|
|
4389
4498
|
}
|
|
4390
|
-
function getStorageDatabaseEnv(
|
|
4391
|
-
return getTodosStorageDatabaseEnv(
|
|
4499
|
+
function getStorageDatabaseEnv(env2 = process.env) {
|
|
4500
|
+
return getTodosStorageDatabaseEnv(env2);
|
|
4392
4501
|
}
|
|
4393
|
-
function getTodosStorageDatabaseUrl(
|
|
4394
|
-
return readStorageEnv(
|
|
4502
|
+
function getTodosStorageDatabaseUrl(env2 = process.env) {
|
|
4503
|
+
return readStorageEnv(env2, "databaseUrl").value;
|
|
4395
4504
|
}
|
|
4396
|
-
function getStorageDatabaseUrl(
|
|
4397
|
-
return getTodosStorageDatabaseUrl(
|
|
4505
|
+
function getStorageDatabaseUrl(env2 = process.env) {
|
|
4506
|
+
return getTodosStorageDatabaseUrl(env2);
|
|
4398
4507
|
}
|
|
4399
|
-
function getTodosStorageEnvName(
|
|
4400
|
-
return readStorageEnv(
|
|
4508
|
+
function getTodosStorageEnvName(env2, key) {
|
|
4509
|
+
return readStorageEnv(env2, key).name;
|
|
4401
4510
|
}
|
|
4402
|
-
function readStorageEnv(
|
|
4511
|
+
function readStorageEnv(env2, key) {
|
|
4403
4512
|
const primaryName = TODOS_STORAGE_ENV[key];
|
|
4404
|
-
const primaryValue = clean(
|
|
4513
|
+
const primaryValue = clean(env2[primaryName]);
|
|
4405
4514
|
if (primaryValue !== undefined)
|
|
4406
4515
|
return { name: primaryName, value: primaryValue };
|
|
4407
4516
|
const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
|
|
4408
|
-
const fallbackValue = clean(
|
|
4517
|
+
const fallbackValue = clean(env2[fallbackName]);
|
|
4409
4518
|
if (fallbackValue !== undefined)
|
|
4410
4519
|
return { name: fallbackName, value: fallbackValue };
|
|
4411
4520
|
return { name: primaryName };
|
|
@@ -4576,7 +4685,7 @@ __export(exports_database, {
|
|
|
4576
4685
|
});
|
|
4577
4686
|
import { Database } from "bun:sqlite";
|
|
4578
4687
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
4579
|
-
import { dirname, join as
|
|
4688
|
+
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
4580
4689
|
function isInMemoryDb(path) {
|
|
4581
4690
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
4582
4691
|
}
|
|
@@ -4585,7 +4694,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4585
4694
|
const stopAt = gitRoot ? resolve3(gitRoot) : resolve3(startDir);
|
|
4586
4695
|
let dir = resolve3(startDir);
|
|
4587
4696
|
while (true) {
|
|
4588
|
-
const candidate =
|
|
4697
|
+
const candidate = join3(dir, ".hasna", "todos", "todos.db");
|
|
4589
4698
|
if (existsSync4(candidate))
|
|
4590
4699
|
return candidate;
|
|
4591
4700
|
if (dir === stopAt)
|
|
@@ -4600,7 +4709,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4600
4709
|
function findGitRoot(startDir) {
|
|
4601
4710
|
let dir = resolve3(startDir);
|
|
4602
4711
|
while (true) {
|
|
4603
|
-
if (existsSync4(
|
|
4712
|
+
if (existsSync4(join3(dir, ".git")))
|
|
4604
4713
|
return dir;
|
|
4605
4714
|
const parent = dirname(dir);
|
|
4606
4715
|
if (parent === dir)
|
|
@@ -4610,7 +4719,7 @@ function findGitRoot(startDir) {
|
|
|
4610
4719
|
return null;
|
|
4611
4720
|
}
|
|
4612
4721
|
function getGlobalDbPath() {
|
|
4613
|
-
return
|
|
4722
|
+
return join3(getTodosGlobalDir(), "todos.db");
|
|
4614
4723
|
}
|
|
4615
4724
|
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4616
4725
|
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
@@ -4648,7 +4757,7 @@ function getDbPath() {
|
|
|
4648
4757
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4649
4758
|
const gitRoot = findGitRoot(cwd);
|
|
4650
4759
|
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4651
|
-
return
|
|
4760
|
+
return join3(gitRoot, ".hasna", "todos", "todos.db");
|
|
4652
4761
|
}
|
|
4653
4762
|
}
|
|
4654
4763
|
return getGlobalDbPath();
|
|
@@ -4689,6 +4798,9 @@ function maybeInstallShadowCapture(db) {
|
|
|
4689
4798
|
}
|
|
4690
4799
|
}
|
|
4691
4800
|
function getDatabase(dbPath) {
|
|
4801
|
+
if (dbPath === undefined && !selectsTodosLocalStore()) {
|
|
4802
|
+
throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
|
|
4803
|
+
}
|
|
4692
4804
|
const path = dbPath || getDbPath();
|
|
4693
4805
|
if (_db && _dbPath === path)
|
|
4694
4806
|
return _db;
|
|
@@ -4828,6 +4940,7 @@ function lowerInClause(column, values, params) {
|
|
|
4828
4940
|
}
|
|
4829
4941
|
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
4830
4942
|
var init_database = __esm(() => {
|
|
4943
|
+
init_local_opt_in();
|
|
4831
4944
|
init_schema();
|
|
4832
4945
|
init_machines();
|
|
4833
4946
|
init_identity_mapping();
|
|
@@ -4838,9 +4951,9 @@ var init_database = __esm(() => {
|
|
|
4838
4951
|
|
|
4839
4952
|
// src/lib/config.ts
|
|
4840
4953
|
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
4841
|
-
import { dirname as dirname2, join as
|
|
4954
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
4842
4955
|
function getConfigPath() {
|
|
4843
|
-
return
|
|
4956
|
+
return join4(getTodosGlobalDir(), "config.json");
|
|
4844
4957
|
}
|
|
4845
4958
|
function normalizeAgent(agent) {
|
|
4846
4959
|
return agent.trim().toLowerCase();
|
|
@@ -4891,21 +5004,6 @@ function normalizeApiUrl(value) {
|
|
|
4891
5004
|
return null;
|
|
4892
5005
|
return trimmed.replace(/\/+$/, "");
|
|
4893
5006
|
}
|
|
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
5007
|
function getSyncAgentsFromConfig() {
|
|
4910
5008
|
const config = loadConfig();
|
|
4911
5009
|
const agents = config.sync_agents;
|
|
@@ -5756,11 +5854,11 @@ function writeAllowed(profile, root, writePath) {
|
|
|
5756
5854
|
return isPathInside(scopeRoot, target);
|
|
5757
5855
|
});
|
|
5758
5856
|
}
|
|
5759
|
-
function redactedEnvKeys(profile,
|
|
5760
|
-
if (!
|
|
5857
|
+
function redactedEnvKeys(profile, env2) {
|
|
5858
|
+
if (!env2)
|
|
5761
5859
|
return [];
|
|
5762
5860
|
const patterns = unique2([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
5763
|
-
return Object.keys(
|
|
5861
|
+
return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
5764
5862
|
}
|
|
5765
5863
|
function checkWorkspacePermission(input = {}) {
|
|
5766
5864
|
const status = getWorkspaceTrustStatus(input.path || process.cwd());
|
|
@@ -5898,18 +5996,18 @@ function profileByName(name, path) {
|
|
|
5898
5996
|
const resolved = normalizePath2(path);
|
|
5899
5997
|
return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
|
|
5900
5998
|
}
|
|
5901
|
-
function redactedEnvKeys2(profile,
|
|
5902
|
-
if (!
|
|
5999
|
+
function redactedEnvKeys2(profile, env2) {
|
|
6000
|
+
if (!env2)
|
|
5903
6001
|
return [];
|
|
5904
6002
|
const patterns = unique3([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
5905
|
-
return Object.keys(
|
|
6003
|
+
return Object.keys(env2).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
5906
6004
|
}
|
|
5907
|
-
function omittedEnvKeys(profile,
|
|
5908
|
-
if (!
|
|
6005
|
+
function omittedEnvKeys(profile, env2) {
|
|
6006
|
+
if (!env2)
|
|
5909
6007
|
return [];
|
|
5910
6008
|
if (profile.env_allowlist.includes("*"))
|
|
5911
6009
|
return [];
|
|
5912
|
-
return Object.keys(
|
|
6010
|
+
return Object.keys(env2).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
|
|
5913
6011
|
}
|
|
5914
6012
|
function resolveFromCwd(cwd, path) {
|
|
5915
6013
|
return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
|
|
@@ -6292,15 +6390,23 @@ var init_event_hooks = __esm(() => {
|
|
|
6292
6390
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
6293
6391
|
});
|
|
6294
6392
|
|
|
6295
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
6393
|
+
// node_modules/.bun/@hasna+events@0.1.17/node_modules/@hasna/events/dist/index.js
|
|
6296
6394
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6297
6395
|
import { Buffer as Buffer2 } from "buffer";
|
|
6396
|
+
import { existsSync as existsSync22 } from "fs";
|
|
6397
|
+
import { join as join32 } from "path";
|
|
6298
6398
|
import { existsSync as existsSync6 } from "fs";
|
|
6299
|
-
import { homedir as
|
|
6300
|
-
import { join as
|
|
6399
|
+
import { homedir as homedir22 } from "os";
|
|
6400
|
+
import { join as join22, resolve as resolve8 } from "path";
|
|
6401
|
+
import { homedir as homedir3 } from "os";
|
|
6402
|
+
import { join as join5 } from "path";
|
|
6301
6403
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
6404
|
+
import { lookup as dnsLookup } from "dns/promises";
|
|
6405
|
+
import { isIP } from "net";
|
|
6302
6406
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6303
6407
|
import { spawn } from "child_process";
|
|
6408
|
+
import { request as nodeHttpRequest } from "http";
|
|
6409
|
+
import { request as nodeHttpsRequest } from "https";
|
|
6304
6410
|
import { randomUUID as randomUUID22 } from "crypto";
|
|
6305
6411
|
function getPathValue(input, path) {
|
|
6306
6412
|
return path.split(".").reduce((value, part) => {
|
|
@@ -6397,8 +6503,93 @@ function channelMatchesEvent(channel, event) {
|
|
|
6397
6503
|
return true;
|
|
6398
6504
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
6399
6505
|
}
|
|
6506
|
+
function assertApp(app) {
|
|
6507
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
6508
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
6509
|
+
}
|
|
6510
|
+
if (!APP_SLUG_RE.test(app)) {
|
|
6511
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
function envOf(options) {
|
|
6515
|
+
return options.env ?? process.env;
|
|
6516
|
+
}
|
|
6517
|
+
function envValue(options, kind) {
|
|
6518
|
+
const value = envOf(options)[KIND_ENV[kind]];
|
|
6519
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
6520
|
+
}
|
|
6521
|
+
function isMacOS(platform) {
|
|
6522
|
+
return platform === "darwin";
|
|
6523
|
+
}
|
|
6524
|
+
function baseDir(kind, options) {
|
|
6525
|
+
const override = envValue(options, kind);
|
|
6526
|
+
if (override)
|
|
6527
|
+
return override;
|
|
6528
|
+
const home = options.home ?? homedir3();
|
|
6529
|
+
const platform = options.platform ?? process.platform;
|
|
6530
|
+
if (isMacOS(platform)) {
|
|
6531
|
+
switch (kind) {
|
|
6532
|
+
case "config":
|
|
6533
|
+
case "data":
|
|
6534
|
+
return join5(home, "Library", "Application Support", "Hasna");
|
|
6535
|
+
case "cache":
|
|
6536
|
+
return join5(home, "Library", "Caches", "Hasna");
|
|
6537
|
+
case "state":
|
|
6538
|
+
return join5(home, "Library", "Logs", "Hasna");
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
switch (kind) {
|
|
6542
|
+
case "config":
|
|
6543
|
+
return join5(home, ".config", "hasna");
|
|
6544
|
+
case "data":
|
|
6545
|
+
return join5(home, ".local", "share", "hasna");
|
|
6546
|
+
case "state":
|
|
6547
|
+
return join5(home, ".local", "state", "hasna");
|
|
6548
|
+
case "cache":
|
|
6549
|
+
return join5(home, ".cache", "hasna");
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
function resolvePath(kind, options) {
|
|
6553
|
+
assertApp(options.app);
|
|
6554
|
+
const appSegment = options.internal === true ? join5("internal", options.app) : options.app;
|
|
6555
|
+
return join5(baseDir(kind, options), appSegment);
|
|
6556
|
+
}
|
|
6557
|
+
function dataDir2(options) {
|
|
6558
|
+
return resolvePath("data", options);
|
|
6559
|
+
}
|
|
6560
|
+
function effectiveHome2() {
|
|
6561
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir22();
|
|
6562
|
+
}
|
|
6563
|
+
function legacyHomeDir2() {
|
|
6564
|
+
return join22(effectiveHome2(), ".hasna", "events");
|
|
6565
|
+
}
|
|
6566
|
+
function resolverHome2() {
|
|
6567
|
+
return dataDir2({ app: "events", home: effectiveHome2() || undefined });
|
|
6568
|
+
}
|
|
6569
|
+
function adoptResolverHome2(resolved, env2 = process.env) {
|
|
6570
|
+
const dataOverride = env2.HASNA_DATA_HOME;
|
|
6571
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
6572
|
+
return true;
|
|
6573
|
+
return existsSync6(join22(resolved, EVENTS_STORE_SENTINEL_FILE));
|
|
6574
|
+
}
|
|
6575
|
+
function exactEventsHome() {
|
|
6576
|
+
const dir = process.env[HASNA_EVENTS_DIR_ENV];
|
|
6577
|
+
if (dir && dir.trim())
|
|
6578
|
+
return dir.trim();
|
|
6579
|
+
const home = process.env[HASNA_EVENTS_HOME_ENV];
|
|
6580
|
+
if (home && home.trim())
|
|
6581
|
+
return home.trim();
|
|
6582
|
+
return;
|
|
6583
|
+
}
|
|
6584
|
+
function getEventsHome() {
|
|
6585
|
+
const exact = exactEventsHome();
|
|
6586
|
+
if (exact)
|
|
6587
|
+
return resolve8(exact);
|
|
6588
|
+
const resolved = resolverHome2();
|
|
6589
|
+
return adoptResolverHome2(resolved) ? resolve8(resolved) : resolve8(legacyHomeDir2());
|
|
6590
|
+
}
|
|
6400
6591
|
function getEventsDataDir(override) {
|
|
6401
|
-
return override ||
|
|
6592
|
+
return override || getEventsHome();
|
|
6402
6593
|
}
|
|
6403
6594
|
|
|
6404
6595
|
class JsonEventsStore {
|
|
@@ -6407,12 +6598,12 @@ class JsonEventsStore {
|
|
|
6407
6598
|
channelsPath;
|
|
6408
6599
|
eventsPath;
|
|
6409
6600
|
deliveriesPath;
|
|
6410
|
-
constructor(
|
|
6411
|
-
this.dataDir =
|
|
6412
|
-
this.runtime = localJsonRuntime(
|
|
6413
|
-
this.channelsPath =
|
|
6414
|
-
this.eventsPath =
|
|
6415
|
-
this.deliveriesPath =
|
|
6601
|
+
constructor(dataDir22 = getEventsDataDir()) {
|
|
6602
|
+
this.dataDir = dataDir22;
|
|
6603
|
+
this.runtime = localJsonRuntime(dataDir22);
|
|
6604
|
+
this.channelsPath = join32(dataDir22, "channels.json");
|
|
6605
|
+
this.eventsPath = join32(dataDir22, "events.json");
|
|
6606
|
+
this.deliveriesPath = join32(dataDir22, "deliveries.json");
|
|
6416
6607
|
}
|
|
6417
6608
|
async init() {
|
|
6418
6609
|
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
@@ -6529,7 +6720,7 @@ class JsonEventsStore {
|
|
|
6529
6720
|
};
|
|
6530
6721
|
}
|
|
6531
6722
|
async ensureArrayFile(path) {
|
|
6532
|
-
if (!
|
|
6723
|
+
if (!existsSync22(path)) {
|
|
6533
6724
|
await writeFile(path, `[]
|
|
6534
6725
|
`, { encoding: "utf-8", mode: 384 });
|
|
6535
6726
|
}
|
|
@@ -6559,7 +6750,7 @@ class JsonEventsStore {
|
|
|
6559
6750
|
});
|
|
6560
6751
|
}
|
|
6561
6752
|
}
|
|
6562
|
-
function localJsonRuntime(
|
|
6753
|
+
function localJsonRuntime(dataDir22 = getEventsDataDir()) {
|
|
6563
6754
|
return {
|
|
6564
6755
|
mode: "local-files",
|
|
6565
6756
|
name: "json-events-store",
|
|
@@ -6572,7 +6763,7 @@ function localJsonRuntime(dataDir2 = getEventsDataDir()) {
|
|
|
6572
6763
|
durable: true,
|
|
6573
6764
|
idempotency: "best-effort-local",
|
|
6574
6765
|
replayCursors: true,
|
|
6575
|
-
description: `Local JSON files in ${
|
|
6766
|
+
description: `Local JSON files in ${dataDir22}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
6576
6767
|
};
|
|
6577
6768
|
}
|
|
6578
6769
|
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
@@ -6643,6 +6834,177 @@ function signPayload(secret, timestamp2, body) {
|
|
|
6643
6834
|
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp2, body)).digest("hex");
|
|
6644
6835
|
return `sha256=${digest}`;
|
|
6645
6836
|
}
|
|
6837
|
+
function isPrivateAddress(address) {
|
|
6838
|
+
const normalized = stripZoneId(address);
|
|
6839
|
+
const version = isIP(normalized);
|
|
6840
|
+
if (version === 4) {
|
|
6841
|
+
const integer = ipv4ToInt(normalized);
|
|
6842
|
+
if (integer === undefined)
|
|
6843
|
+
return true;
|
|
6844
|
+
return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
|
|
6845
|
+
}
|
|
6846
|
+
if (version === 6) {
|
|
6847
|
+
const groups = ipv6Groups(normalized);
|
|
6848
|
+
if (!groups)
|
|
6849
|
+
return true;
|
|
6850
|
+
for (const prefix of IPV6_SPECIAL_PREFIXES) {
|
|
6851
|
+
if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
|
|
6852
|
+
continue;
|
|
6853
|
+
if (prefix.bits === 96 && groups[5] === 65535) {
|
|
6854
|
+
return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
|
|
6855
|
+
}
|
|
6856
|
+
if (prefix.bits === 16 && groups[0] === 8194) {
|
|
6857
|
+
return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
|
|
6858
|
+
}
|
|
6859
|
+
return true;
|
|
6860
|
+
}
|
|
6861
|
+
return false;
|
|
6862
|
+
}
|
|
6863
|
+
return true;
|
|
6864
|
+
}
|
|
6865
|
+
async function resolveWebhookTarget(url, policy = {}) {
|
|
6866
|
+
const hostname = normalizeHostname(url.hostname);
|
|
6867
|
+
const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
|
|
6868
|
+
if (allowlist.includes(hostname)) {
|
|
6869
|
+
const version2 = isIP(hostname);
|
|
6870
|
+
if (version2 === 4 || version2 === 6) {
|
|
6871
|
+
return { hostname, addresses: [hostname] };
|
|
6872
|
+
}
|
|
6873
|
+
const lookup2 = policy.lookup ?? defaultTargetLookup;
|
|
6874
|
+
let resolved2;
|
|
6875
|
+
try {
|
|
6876
|
+
resolved2 = await lookup2(hostname);
|
|
6877
|
+
} catch {
|
|
6878
|
+
throw new Error(`Webhook target ${hostname} could not be resolved`);
|
|
6879
|
+
}
|
|
6880
|
+
if (!Array.isArray(resolved2) || resolved2.length === 0) {
|
|
6881
|
+
throw new Error(`Webhook target ${hostname} resolved to no addresses`);
|
|
6882
|
+
}
|
|
6883
|
+
const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
|
|
6884
|
+
return { hostname, addresses };
|
|
6885
|
+
}
|
|
6886
|
+
const version = isIP(hostname);
|
|
6887
|
+
if (version === 4 || version === 6) {
|
|
6888
|
+
if (isPrivateAddress(hostname)) {
|
|
6889
|
+
throw new Error(`Webhook target ${hostname} is a private or special-use address`);
|
|
6890
|
+
}
|
|
6891
|
+
return { hostname, addresses: [hostname] };
|
|
6892
|
+
}
|
|
6893
|
+
const lookup = policy.lookup ?? defaultTargetLookup;
|
|
6894
|
+
let resolved;
|
|
6895
|
+
try {
|
|
6896
|
+
resolved = await lookup(hostname);
|
|
6897
|
+
} catch {
|
|
6898
|
+
throw new Error(`Webhook target ${hostname} could not be resolved`);
|
|
6899
|
+
}
|
|
6900
|
+
if (!Array.isArray(resolved) || resolved.length === 0) {
|
|
6901
|
+
throw new Error(`Webhook target ${hostname} resolved to no addresses`);
|
|
6902
|
+
}
|
|
6903
|
+
const allowed = [];
|
|
6904
|
+
for (const entry of resolved) {
|
|
6905
|
+
const address = normalizeHostname(entry.address);
|
|
6906
|
+
if (isPrivateAddress(address)) {
|
|
6907
|
+
if (allowlist.includes(address)) {
|
|
6908
|
+
allowed.push(address);
|
|
6909
|
+
continue;
|
|
6910
|
+
}
|
|
6911
|
+
throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
|
|
6912
|
+
}
|
|
6913
|
+
allowed.push(address);
|
|
6914
|
+
}
|
|
6915
|
+
if (allowed.length === 0) {
|
|
6916
|
+
throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
|
|
6917
|
+
}
|
|
6918
|
+
return { hostname, addresses: allowed };
|
|
6919
|
+
}
|
|
6920
|
+
function normalizeMaxRedirects(value) {
|
|
6921
|
+
if (value === undefined)
|
|
6922
|
+
return DEFAULT_MAX_REDIRECTS;
|
|
6923
|
+
if (!Number.isInteger(value) || value < 0)
|
|
6924
|
+
throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
|
|
6925
|
+
return value;
|
|
6926
|
+
}
|
|
6927
|
+
function normalizeHostname(hostname) {
|
|
6928
|
+
const lower = hostname.toLowerCase();
|
|
6929
|
+
if (lower.startsWith("[") && lower.endsWith("]"))
|
|
6930
|
+
return lower.slice(1, -1);
|
|
6931
|
+
return lower;
|
|
6932
|
+
}
|
|
6933
|
+
function stripZoneId(address) {
|
|
6934
|
+
const percent = address.indexOf("%");
|
|
6935
|
+
return percent === -1 ? address : address.slice(0, percent);
|
|
6936
|
+
}
|
|
6937
|
+
function ipv4ToInt(address) {
|
|
6938
|
+
const parts = address.split(".");
|
|
6939
|
+
if (parts.length !== 4)
|
|
6940
|
+
return;
|
|
6941
|
+
let value = 0;
|
|
6942
|
+
for (const part of parts) {
|
|
6943
|
+
if (!/^\d{1,3}$/.test(part))
|
|
6944
|
+
return;
|
|
6945
|
+
const octet = Number(part);
|
|
6946
|
+
if (octet > 255)
|
|
6947
|
+
return;
|
|
6948
|
+
value = value << 8 | octet;
|
|
6949
|
+
}
|
|
6950
|
+
return value >>> 0;
|
|
6951
|
+
}
|
|
6952
|
+
function ipv4IntToString(integer) {
|
|
6953
|
+
return [
|
|
6954
|
+
integer >>> 24 & 255,
|
|
6955
|
+
integer >>> 16 & 255,
|
|
6956
|
+
integer >>> 8 & 255,
|
|
6957
|
+
integer & 255
|
|
6958
|
+
].join(".");
|
|
6959
|
+
}
|
|
6960
|
+
function ipv6Groups(address) {
|
|
6961
|
+
const raw = stripZoneId(address);
|
|
6962
|
+
const doubleColon = raw.indexOf("::");
|
|
6963
|
+
const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
|
|
6964
|
+
const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
|
|
6965
|
+
const parseGroups = (text) => {
|
|
6966
|
+
if (text === "")
|
|
6967
|
+
return [];
|
|
6968
|
+
const out = [];
|
|
6969
|
+
for (const part of text.split(":")) {
|
|
6970
|
+
if (part.includes(".")) {
|
|
6971
|
+
const v4 = ipv4ToInt(part);
|
|
6972
|
+
if (v4 === undefined)
|
|
6973
|
+
return;
|
|
6974
|
+
out.push(v4 >>> 16 & 65535, v4 & 65535);
|
|
6975
|
+
} else {
|
|
6976
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(part))
|
|
6977
|
+
return;
|
|
6978
|
+
out.push(parseInt(part, 16));
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
return out;
|
|
6982
|
+
};
|
|
6983
|
+
const head = parseGroups(headText);
|
|
6984
|
+
if (!head)
|
|
6985
|
+
return;
|
|
6986
|
+
const tail = parseGroups(tailText);
|
|
6987
|
+
if (!tail)
|
|
6988
|
+
return;
|
|
6989
|
+
const total = head.length + tail.length;
|
|
6990
|
+
if (doubleColon === -1) {
|
|
6991
|
+
return total === 8 ? head : undefined;
|
|
6992
|
+
}
|
|
6993
|
+
if (total >= 8)
|
|
6994
|
+
return;
|
|
6995
|
+
return [...head, ...new Array(8 - total).fill(0), ...tail];
|
|
6996
|
+
}
|
|
6997
|
+
function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
|
|
6998
|
+
let remaining = prefixBits;
|
|
6999
|
+
for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
|
|
7000
|
+
const take = Math.min(16, remaining);
|
|
7001
|
+
const mask = 65535 << 16 - take & 65535;
|
|
7002
|
+
if ((groups[index] & mask) !== (prefixGroups[index] & mask))
|
|
7003
|
+
return false;
|
|
7004
|
+
remaining -= take;
|
|
7005
|
+
}
|
|
7006
|
+
return true;
|
|
7007
|
+
}
|
|
6646
7008
|
function now2() {
|
|
6647
7009
|
return new Date().toISOString();
|
|
6648
7010
|
}
|
|
@@ -6673,9 +7035,18 @@ function buildWebhookRequest(event, channel, options = {}) {
|
|
|
6673
7035
|
}
|
|
6674
7036
|
return { body, headers };
|
|
6675
7037
|
}
|
|
7038
|
+
function normalizeWebhookUrl(raw) {
|
|
7039
|
+
const url = new URL(raw);
|
|
7040
|
+
if (url.username !== "" || url.password !== "") {
|
|
7041
|
+
url.username = "";
|
|
7042
|
+
url.password = "";
|
|
7043
|
+
}
|
|
7044
|
+
return url.toString();
|
|
7045
|
+
}
|
|
6676
7046
|
async function dispatchWebhook(event, channel, options = {}) {
|
|
6677
7047
|
if (!channel.webhook)
|
|
6678
7048
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7049
|
+
const webhookUrl = normalizeWebhookUrl(channel.webhook.url);
|
|
6679
7050
|
const startedAt = now2();
|
|
6680
7051
|
let secret = channel.webhook.secret;
|
|
6681
7052
|
if (channel.webhook.secretRef) {
|
|
@@ -6692,10 +7063,14 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6692
7063
|
}
|
|
6693
7064
|
const timestamp2 = (options.now?.() ?? new Date).toISOString();
|
|
6694
7065
|
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp: timestamp2 });
|
|
7066
|
+
const validateTargets = options.webhookTargetPolicy !== undefined || options.fetchImpl === undefined;
|
|
7067
|
+
if (validateTargets) {
|
|
7068
|
+
return dispatchValidatedWebhook(event, channel, { body, headers, startedAt, options });
|
|
7069
|
+
}
|
|
6695
7070
|
const controller = new AbortController;
|
|
6696
7071
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
6697
7072
|
try {
|
|
6698
|
-
const response = await (options.fetchImpl ?? fetch)(
|
|
7073
|
+
const response = await (options.fetchImpl ?? fetch)(webhookUrl, {
|
|
6699
7074
|
method: "POST",
|
|
6700
7075
|
headers,
|
|
6701
7076
|
body,
|
|
@@ -6723,6 +7098,130 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
6723
7098
|
clearTimeout(timeout);
|
|
6724
7099
|
}
|
|
6725
7100
|
}
|
|
7101
|
+
function isRedirectStatus(status) {
|
|
7102
|
+
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
|
|
7103
|
+
}
|
|
7104
|
+
function redirectKeepsBody(status) {
|
|
7105
|
+
return status === 307 || status === 308;
|
|
7106
|
+
}
|
|
7107
|
+
async function pinnedNativeRequest(target, addresses, method, headers, body, signal, tls) {
|
|
7108
|
+
const isHttps = target.protocol === "https:";
|
|
7109
|
+
if (!isHttps && target.protocol !== "http:") {
|
|
7110
|
+
throw new Error(`Webhook target uses unsupported protocol ${target.protocol}`);
|
|
7111
|
+
}
|
|
7112
|
+
const defaultPort = isHttps ? 443 : 80;
|
|
7113
|
+
const port = target.port ? Number(target.port) : defaultPort;
|
|
7114
|
+
const requestOptions = {
|
|
7115
|
+
hostname: target.hostname,
|
|
7116
|
+
port,
|
|
7117
|
+
path: `${target.pathname}${target.search}`,
|
|
7118
|
+
method,
|
|
7119
|
+
headers,
|
|
7120
|
+
...tls?.ca ? { ca: tls.ca } : {},
|
|
7121
|
+
lookup: (hostname, _options, callback) => {
|
|
7122
|
+
const entries = addresses.map((address) => ({
|
|
7123
|
+
address,
|
|
7124
|
+
family: address.includes(":") ? 6 : 4
|
|
7125
|
+
}));
|
|
7126
|
+
callback(null, entries);
|
|
7127
|
+
}
|
|
7128
|
+
};
|
|
7129
|
+
return new Promise((resolve22, reject) => {
|
|
7130
|
+
const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
|
|
7131
|
+
const onAbort = () => {
|
|
7132
|
+
const error = new Error("The operation was aborted.");
|
|
7133
|
+
error.name = "AbortError";
|
|
7134
|
+
request.destroy(error);
|
|
7135
|
+
};
|
|
7136
|
+
if (signal.aborted)
|
|
7137
|
+
onAbort();
|
|
7138
|
+
else
|
|
7139
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
7140
|
+
request.on("error", reject);
|
|
7141
|
+
if (body !== undefined)
|
|
7142
|
+
request.write(body);
|
|
7143
|
+
request.end();
|
|
7144
|
+
function onResponse(response) {
|
|
7145
|
+
const chunks = [];
|
|
7146
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
7147
|
+
response.on("error", reject);
|
|
7148
|
+
response.on("end", () => {
|
|
7149
|
+
const headersRecord = {};
|
|
7150
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
7151
|
+
if (typeof value === "string")
|
|
7152
|
+
headersRecord[name] = value;
|
|
7153
|
+
else if (Array.isArray(value))
|
|
7154
|
+
headersRecord[name] = value.join(", ");
|
|
7155
|
+
}
|
|
7156
|
+
resolve22(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
|
|
7157
|
+
});
|
|
7158
|
+
}
|
|
7159
|
+
});
|
|
7160
|
+
}
|
|
7161
|
+
async function dispatchValidatedWebhook(event, channel, input) {
|
|
7162
|
+
const { body, headers, startedAt, options } = input;
|
|
7163
|
+
const webhook = channel.webhook;
|
|
7164
|
+
if (!webhook)
|
|
7165
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
7166
|
+
const policy = options.webhookTargetPolicy ?? {};
|
|
7167
|
+
const maxRedirects = normalizeMaxRedirects(policy.maxRedirects);
|
|
7168
|
+
const controller = new AbortController;
|
|
7169
|
+
const timeout = setTimeout(() => controller.abort(), webhook.timeoutMs ?? 15000);
|
|
7170
|
+
try {
|
|
7171
|
+
let target = new URL(normalizeWebhookUrl(webhook.url));
|
|
7172
|
+
let requestHeaders = headers;
|
|
7173
|
+
let method = "POST";
|
|
7174
|
+
let requestBody = body;
|
|
7175
|
+
let redirectsFollowed = 0;
|
|
7176
|
+
for (;; ) {
|
|
7177
|
+
const resolved = await resolveWebhookTarget(target, policy).catch((error) => {
|
|
7178
|
+
throw new Error(`Webhook target rejected by SSRF guard: ${error.message}`);
|
|
7179
|
+
});
|
|
7180
|
+
const response = options.fetchImpl ? await options.fetchImpl(target, {
|
|
7181
|
+
method,
|
|
7182
|
+
headers: requestHeaders,
|
|
7183
|
+
body: requestBody,
|
|
7184
|
+
signal: controller.signal,
|
|
7185
|
+
redirect: "manual"
|
|
7186
|
+
}) : await pinnedNativeRequest(target, resolved.addresses, method, requestHeaders, requestBody, controller.signal, options.tls);
|
|
7187
|
+
const location = response.headers.get("location");
|
|
7188
|
+
if (isRedirectStatus(response.status) && location) {
|
|
7189
|
+
if (redirectsFollowed >= maxRedirects) {
|
|
7190
|
+
return failedAttempt(startedAt, `Webhook target exceeded ${maxRedirects} redirects`);
|
|
7191
|
+
}
|
|
7192
|
+
redirectsFollowed += 1;
|
|
7193
|
+
const next = new URL(location, target);
|
|
7194
|
+
target = next;
|
|
7195
|
+
if (!redirectKeepsBody(response.status)) {
|
|
7196
|
+
method = "GET";
|
|
7197
|
+
requestBody = undefined;
|
|
7198
|
+
requestHeaders = Object.fromEntries(Object.entries(requestHeaders).filter(([name]) => name.toLowerCase() !== "content-type" && name.toLowerCase() !== "content-length"));
|
|
7199
|
+
}
|
|
7200
|
+
continue;
|
|
7201
|
+
}
|
|
7202
|
+
const responseBody = truncate(await response.text());
|
|
7203
|
+
return {
|
|
7204
|
+
attempt: 1,
|
|
7205
|
+
status: response.ok ? "success" : "failed",
|
|
7206
|
+
startedAt,
|
|
7207
|
+
completedAt: now2(),
|
|
7208
|
+
responseStatus: response.status,
|
|
7209
|
+
responseBody,
|
|
7210
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
7213
|
+
} catch (error) {
|
|
7214
|
+
return {
|
|
7215
|
+
attempt: 1,
|
|
7216
|
+
status: "failed",
|
|
7217
|
+
startedAt,
|
|
7218
|
+
completedAt: now2(),
|
|
7219
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7220
|
+
};
|
|
7221
|
+
} finally {
|
|
7222
|
+
clearTimeout(timeout);
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
6726
7225
|
function failedAttempt(startedAt, error) {
|
|
6727
7226
|
return {
|
|
6728
7227
|
attempt: 1,
|
|
@@ -6737,7 +7236,7 @@ async function dispatchCommand(event, channel) {
|
|
|
6737
7236
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
6738
7237
|
const startedAt = now2();
|
|
6739
7238
|
const eventJson = JSON.stringify(event);
|
|
6740
|
-
const
|
|
7239
|
+
const env2 = {
|
|
6741
7240
|
...process.env,
|
|
6742
7241
|
...channel.command.env,
|
|
6743
7242
|
HASNA_CHANNEL_ID: channel.id,
|
|
@@ -6751,10 +7250,10 @@ async function dispatchCommand(event, channel) {
|
|
|
6751
7250
|
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
6752
7251
|
HASNA_EVENT_JSON: eventJson
|
|
6753
7252
|
};
|
|
6754
|
-
return new Promise((
|
|
7253
|
+
return new Promise((resolve22) => {
|
|
6755
7254
|
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
6756
7255
|
cwd: channel.command.cwd,
|
|
6757
|
-
env,
|
|
7256
|
+
env: env2,
|
|
6758
7257
|
stdio: ["pipe", "pipe", "pipe"]
|
|
6759
7258
|
});
|
|
6760
7259
|
let stdout = "";
|
|
@@ -6769,7 +7268,7 @@ async function dispatchCommand(event, channel) {
|
|
|
6769
7268
|
});
|
|
6770
7269
|
child.on("error", (error) => {
|
|
6771
7270
|
clearTimeout(timeout);
|
|
6772
|
-
|
|
7271
|
+
resolve22({
|
|
6773
7272
|
attempt: 1,
|
|
6774
7273
|
status: "failed",
|
|
6775
7274
|
startedAt,
|
|
@@ -6782,7 +7281,7 @@ async function dispatchCommand(event, channel) {
|
|
|
6782
7281
|
child.on("close", (code, signal) => {
|
|
6783
7282
|
clearTimeout(timeout);
|
|
6784
7283
|
const success = code === 0;
|
|
6785
|
-
|
|
7284
|
+
resolve22({
|
|
6786
7285
|
attempt: 1,
|
|
6787
7286
|
status: success ? "success" : "failed",
|
|
6788
7287
|
startedAt,
|
|
@@ -6918,7 +7417,9 @@ class EventsClient {
|
|
|
6918
7417
|
this.transportOptions = {
|
|
6919
7418
|
fetchImpl: options.fetchImpl,
|
|
6920
7419
|
secretResolver: options.secretResolver,
|
|
6921
|
-
now: options.now
|
|
7420
|
+
now: options.now,
|
|
7421
|
+
tls: options.tls,
|
|
7422
|
+
webhookTargetPolicy: options.webhookTargetPolicy
|
|
6922
7423
|
};
|
|
6923
7424
|
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
6924
7425
|
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
@@ -7123,9 +7624,51 @@ function normalizeRetryPolicy(policy) {
|
|
|
7123
7624
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
7124
7625
|
};
|
|
7125
7626
|
}
|
|
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
|
-
|
|
7627
|
+
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) => {
|
|
7628
|
+
return dnsLookup(hostname, { all: true, verbatim: false });
|
|
7629
|
+
}, EventValidationError, defaultEventTypeCatalog, APP_EVENT_V1_MAX_DATA_BYTES;
|
|
7630
|
+
var init_dist = __esm(() => {
|
|
7631
|
+
KIND_ENV = {
|
|
7632
|
+
config: "HASNA_CONFIG_HOME",
|
|
7633
|
+
data: "HASNA_DATA_HOME",
|
|
7634
|
+
state: "HASNA_STATE_HOME",
|
|
7635
|
+
cache: "HASNA_CACHE_HOME"
|
|
7636
|
+
};
|
|
7637
|
+
APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
7128
7638
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
7639
|
+
IPV4_PRIVATE_RANGES = [
|
|
7640
|
+
[0, 16777215],
|
|
7641
|
+
[167772160, 184549375],
|
|
7642
|
+
[1681915904, 1686110207],
|
|
7643
|
+
[2130706432, 2147483647],
|
|
7644
|
+
[2851995648, 2852061183],
|
|
7645
|
+
[2886729728, 2887778303],
|
|
7646
|
+
[3221225472, 3221225727],
|
|
7647
|
+
[3221225984, 3221226239],
|
|
7648
|
+
[3227017984, 3227018239],
|
|
7649
|
+
[3232235520, 3232301055],
|
|
7650
|
+
[3323068416, 3323199487],
|
|
7651
|
+
[3325256704, 3325256959],
|
|
7652
|
+
[3405803776, 3405804031],
|
|
7653
|
+
[3758096384, 4294967295]
|
|
7654
|
+
];
|
|
7655
|
+
IPV6_SPECIAL_PREFIXES = [
|
|
7656
|
+
{ groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
|
|
7657
|
+
{ groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
|
|
7658
|
+
{ groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
|
|
7659
|
+
{ groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
|
|
7660
|
+
{ groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
|
|
7661
|
+
{ groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
|
|
7662
|
+
{ groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
|
|
7663
|
+
{ groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
|
|
7664
|
+
{ groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
|
|
7665
|
+
{ groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
|
|
7666
|
+
{ groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
|
|
7667
|
+
{ groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
|
|
7668
|
+
{ groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
|
|
7669
|
+
{ groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
|
|
7670
|
+
{ groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
|
|
7671
|
+
];
|
|
7129
7672
|
EventValidationError = class EventValidationError extends Error {
|
|
7130
7673
|
eventType;
|
|
7131
7674
|
issues;
|
|
@@ -7149,6 +7692,8 @@ function rowToTaskList(row) {
|
|
|
7149
7692
|
};
|
|
7150
7693
|
}
|
|
7151
7694
|
function createTaskList(input, db) {
|
|
7695
|
+
if (input.status !== undefined)
|
|
7696
|
+
throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
|
|
7152
7697
|
const d = db || getDatabase();
|
|
7153
7698
|
return d.transaction(() => {
|
|
7154
7699
|
const id = uuid();
|
|
@@ -7190,6 +7735,8 @@ function listTaskLists(projectId, db) {
|
|
|
7190
7735
|
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
7191
7736
|
}
|
|
7192
7737
|
function updateTaskList(id, input, db) {
|
|
7738
|
+
if (input.status !== undefined)
|
|
7739
|
+
throw new Error("The explicit SQLite storage cannot retain task-list status; use the shared API");
|
|
7193
7740
|
const d = db || getDatabase();
|
|
7194
7741
|
return d.transaction(() => {
|
|
7195
7742
|
const existing = getTaskList(id, d);
|
|
@@ -7547,7 +8094,7 @@ function emitSharedTaskEventQuiet(input) {
|
|
|
7547
8094
|
}
|
|
7548
8095
|
var SOURCE = "todos";
|
|
7549
8096
|
var init_shared_events = __esm(() => {
|
|
7550
|
-
|
|
8097
|
+
init_dist();
|
|
7551
8098
|
init_database();
|
|
7552
8099
|
init_projects();
|
|
7553
8100
|
init_task_lists();
|
|
@@ -11188,7 +11735,13 @@ function resolveCreateSlug(input, projectId, db) {
|
|
|
11188
11735
|
}
|
|
11189
11736
|
return nextPlanSlug(planSlugBase2(input.name), projectId, db);
|
|
11190
11737
|
}
|
|
11738
|
+
function assertLegacyPlanInput(input) {
|
|
11739
|
+
if (input.start_date !== undefined || input.end_date !== undefined || input.status === "planning" || input.status === "cancelled") {
|
|
11740
|
+
throw new Error("Plan calendar dates and planning/cancelled statuses require the shared Todos API; explicit SQLite storage cannot retain them");
|
|
11741
|
+
}
|
|
11742
|
+
}
|
|
11191
11743
|
function createPlan(input, db) {
|
|
11744
|
+
assertLegacyPlanInput(input);
|
|
11192
11745
|
const d = db || getDatabase();
|
|
11193
11746
|
const id = uuid();
|
|
11194
11747
|
const timestamp2 = now();
|
|
@@ -11269,6 +11822,7 @@ function updatePlanStored(id, input, db) {
|
|
|
11269
11822
|
return updated;
|
|
11270
11823
|
}
|
|
11271
11824
|
function updatePlan(id, input, db) {
|
|
11825
|
+
assertLegacyPlanInput(input);
|
|
11272
11826
|
const d = db || getDatabase();
|
|
11273
11827
|
return d.transaction(() => {
|
|
11274
11828
|
guardPlanRowsSqlite([id], d);
|
|
@@ -11743,27 +12297,27 @@ var init_boards = __esm(() => {
|
|
|
11743
12297
|
// src/lib/artifact-store.ts
|
|
11744
12298
|
import { createHash as createHash3 } from "crypto";
|
|
11745
12299
|
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
|
|
12300
|
+
import { basename, dirname as dirname4, join as join6, resolve as resolve9 } from "path";
|
|
11747
12301
|
import { tmpdir as tmpdir2 } from "os";
|
|
11748
12302
|
function isInMemoryDb2(path) {
|
|
11749
12303
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
11750
12304
|
}
|
|
11751
12305
|
function artifactStoreRoot() {
|
|
11752
12306
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
11753
|
-
return
|
|
12307
|
+
return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
11754
12308
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
11755
|
-
return
|
|
12309
|
+
return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
11756
12310
|
const dbPath = getDatabasePath();
|
|
11757
12311
|
if (isInMemoryDb2(dbPath))
|
|
11758
|
-
return
|
|
11759
|
-
return
|
|
12312
|
+
return join6(tmpdir2(), "hasna-todos-artifacts");
|
|
12313
|
+
return join6(dirname4(resolve9(dbPath)), "artifacts");
|
|
11760
12314
|
}
|
|
11761
12315
|
function artifactStorePath(relativePath) {
|
|
11762
12316
|
const normalized = relativePath.replace(/\\/g, "/");
|
|
11763
12317
|
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
11764
12318
|
throw new Error("Invalid artifact store path");
|
|
11765
12319
|
}
|
|
11766
|
-
return
|
|
12320
|
+
return join6(artifactStoreRoot(), normalized);
|
|
11767
12321
|
}
|
|
11768
12322
|
function sha256(buffer) {
|
|
11769
12323
|
return createHash3("sha256").update(buffer).digest("hex");
|
|
@@ -11803,7 +12357,7 @@ function mediaTypeFor(path, textLike) {
|
|
|
11803
12357
|
return "application/octet-stream";
|
|
11804
12358
|
}
|
|
11805
12359
|
function storeArtifactContent(input) {
|
|
11806
|
-
const sourcePath =
|
|
12360
|
+
const sourcePath = resolve9(input.path);
|
|
11807
12361
|
if (!existsSync8(sourcePath))
|
|
11808
12362
|
return null;
|
|
11809
12363
|
const sourceStat = statSync2(sourcePath);
|
|
@@ -11821,7 +12375,7 @@ function storeArtifactContent(input) {
|
|
|
11821
12375
|
redactionStatus = "redacted";
|
|
11822
12376
|
}
|
|
11823
12377
|
const storedSha = sha256(storedBuffer);
|
|
11824
|
-
const relativePath =
|
|
12378
|
+
const relativePath = join6("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
11825
12379
|
const destination = artifactStorePath(relativePath);
|
|
11826
12380
|
if (!existsSync8(destination)) {
|
|
11827
12381
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
@@ -11958,19 +12512,19 @@ function importStoredArtifactContent(content) {
|
|
|
11958
12512
|
}
|
|
11959
12513
|
function getArtifactStoreRoot(dbPath) {
|
|
11960
12514
|
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
11961
|
-
return
|
|
12515
|
+
return resolve9(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
11962
12516
|
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
11963
|
-
return
|
|
12517
|
+
return resolve9(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
11964
12518
|
const path = dbPath ?? getDatabasePath();
|
|
11965
12519
|
if (isInMemoryDb2(path))
|
|
11966
|
-
return
|
|
11967
|
-
return
|
|
12520
|
+
return join6(tmpdir2(), "hasna-todos-artifacts");
|
|
12521
|
+
return join6(dirname4(resolve9(path)), "artifacts");
|
|
11968
12522
|
}
|
|
11969
12523
|
function computeContentHash(path) {
|
|
11970
|
-
return sha256(readFileSync4(
|
|
12524
|
+
return sha256(readFileSync4(resolve9(path)));
|
|
11971
12525
|
}
|
|
11972
12526
|
function storeArtifactFile(input) {
|
|
11973
|
-
const sourcePath =
|
|
12527
|
+
const sourcePath = resolve9(input.sourcePath);
|
|
11974
12528
|
if (!existsSync8(sourcePath)) {
|
|
11975
12529
|
throw new Error(`Source file not found: ${input.sourcePath}`);
|
|
11976
12530
|
}
|
|
@@ -11984,7 +12538,7 @@ function storeArtifactFile(input) {
|
|
|
11984
12538
|
let localPath = sourcePath;
|
|
11985
12539
|
if (storageMode === "copy") {
|
|
11986
12540
|
const fileName = input.name && input.name.trim().length > 0 ? basename(input.name) : basename(sourcePath);
|
|
11987
|
-
const destination =
|
|
12541
|
+
const destination = join6(getArtifactStoreRoot(input.dbPath), input.artifactId, fileName);
|
|
11988
12542
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
11989
12543
|
writeFileSync2(destination, buffer);
|
|
11990
12544
|
localPath = destination;
|
|
@@ -12019,7 +12573,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
|
|
|
12019
12573
|
};
|
|
12020
12574
|
}
|
|
12021
12575
|
function writeArtifactExportManifest(manifest, outputPath) {
|
|
12022
|
-
const destination =
|
|
12576
|
+
const destination = resolve9(outputPath);
|
|
12023
12577
|
mkdirSync4(dirname4(destination), { recursive: true });
|
|
12024
12578
|
writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
|
|
12025
12579
|
`);
|
|
@@ -12707,7 +13261,9 @@ function finishTaskRunTransaction(input, db) {
|
|
|
12707
13261
|
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
12708
13262
|
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
12709
13263
|
if (!run) {
|
|
12710
|
-
|
|
13264
|
+
if (input.run_id)
|
|
13265
|
+
throw new InputValidationError(`Run not found: ${input.run_id}`, "Pass an existing run id, or --key.");
|
|
13266
|
+
throw new InputValidationError("runs finish requires a run id or --key", "Pass run_id, or key with task_id.");
|
|
12711
13267
|
}
|
|
12712
13268
|
if (input.task_id && run.task_id !== input.task_id) {
|
|
12713
13269
|
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
@@ -13176,7 +13732,7 @@ var init_tasks = __esm(() => {
|
|
|
13176
13732
|
// package.json
|
|
13177
13733
|
var package_default = {
|
|
13178
13734
|
name: "@hasna/todos",
|
|
13179
|
-
version: "0.
|
|
13735
|
+
version: "0.16.0",
|
|
13180
13736
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
13181
13737
|
type: "module",
|
|
13182
13738
|
main: "dist/index.js",
|
|
@@ -13229,37 +13785,40 @@ var package_default = {
|
|
|
13229
13785
|
}
|
|
13230
13786
|
},
|
|
13231
13787
|
workspaces: [
|
|
13232
|
-
"dashboard",
|
|
13233
13788
|
"ai"
|
|
13234
13789
|
],
|
|
13235
13790
|
files: [
|
|
13236
13791
|
"dist",
|
|
13237
|
-
"dashboard/dist",
|
|
13238
13792
|
"postinstall.js",
|
|
13239
13793
|
"LICENSE",
|
|
13240
|
-
"README.md"
|
|
13794
|
+
"README.md",
|
|
13795
|
+
"CHANGELOG.md",
|
|
13796
|
+
"docs/PLAN_API.md",
|
|
13797
|
+
"docs/TASK_LIST_API.md",
|
|
13798
|
+
"docs/TEMPLATE_API.md",
|
|
13799
|
+
"docs/TASK_QUERY_API.md",
|
|
13800
|
+
"docs/native-storage.md"
|
|
13241
13801
|
],
|
|
13242
13802
|
scripts: {
|
|
13243
|
-
build: "
|
|
13803
|
+
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
13804
|
"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
13805
|
"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
13806
|
migrate: "bun run src/server/index.ts migrate",
|
|
13247
13807
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
13248
13808
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
13249
|
-
"build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
|
|
13250
13809
|
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",
|
|
13810
|
+
test: "bun test --timeout 120000",
|
|
13811
|
+
"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
13812
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
13254
13813
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
13255
13814
|
"dev:serve": "bun run src/server/index.ts",
|
|
13256
13815
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
13257
13816
|
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
13258
13817
|
"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",
|
|
13818
|
+
"test:attested-container-candidate": "bun test --timeout 120000 scripts/attested-container-candidate.test.ts",
|
|
13260
13819
|
"emit:iapp-deployment-compatibility-vector": "bun run scripts/attested-container-compatibility-vector.ts emit",
|
|
13261
13820
|
"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",
|
|
13821
|
+
"test:attested-container-compatibility-vector": "bun test --timeout 120000 scripts/attested-container-compatibility-vector.test.ts",
|
|
13263
13822
|
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
13264
13823
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
13265
13824
|
postinstall: "node postinstall.js"
|
|
@@ -13283,11 +13842,12 @@ var package_default = {
|
|
|
13283
13842
|
},
|
|
13284
13843
|
repository: {
|
|
13285
13844
|
type: "git",
|
|
13286
|
-
url: "https://github.com/hasna/
|
|
13845
|
+
url: "https://github.com/hasna/apps.git",
|
|
13846
|
+
directory: "apps/todos"
|
|
13287
13847
|
},
|
|
13288
|
-
homepage: "https://github.com/hasna/
|
|
13848
|
+
homepage: "https://github.com/hasna/apps",
|
|
13289
13849
|
bugs: {
|
|
13290
|
-
url: "https://github.com/hasna/
|
|
13850
|
+
url: "https://github.com/hasna/apps/issues"
|
|
13291
13851
|
},
|
|
13292
13852
|
engines: {
|
|
13293
13853
|
bun: ">=1.0.0"
|
|
@@ -13296,9 +13856,8 @@ var package_default = {
|
|
|
13296
13856
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
13297
13857
|
license: "Apache-2.0",
|
|
13298
13858
|
dependencies: {
|
|
13299
|
-
"@hasna/contracts": "0.
|
|
13859
|
+
"@hasna/contracts": "1.0.2",
|
|
13300
13860
|
"@hasna/events": "^0.1.11",
|
|
13301
|
-
"@hasna/paths": "0.1.0",
|
|
13302
13861
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
13303
13862
|
chalk: "^5.4.1",
|
|
13304
13863
|
commander: "^13.1.0",
|
|
@@ -13327,6 +13886,7 @@ function getPackageVersion(_fromUrl = import.meta.url) {
|
|
|
13327
13886
|
}
|
|
13328
13887
|
|
|
13329
13888
|
// src/mcp/token-utils.ts
|
|
13889
|
+
init_env();
|
|
13330
13890
|
var CORE_MCP_TOOLS = new Set([
|
|
13331
13891
|
"add_comment",
|
|
13332
13892
|
"bootstrap",
|
|
@@ -13728,7 +14288,7 @@ function addGroupTools(toolNames, groupName) {
|
|
|
13728
14288
|
toolNames.add(tool);
|
|
13729
14289
|
return true;
|
|
13730
14290
|
}
|
|
13731
|
-
function shouldRegisterToolForProfile(name, profileValue =
|
|
14291
|
+
function shouldRegisterToolForProfile(name, profileValue = env.profile(), groupValue = env.toolGroups()) {
|
|
13732
14292
|
const profileTokens = splitTokens(profileValue || "minimal");
|
|
13733
14293
|
const groupTokens = splitTokens(groupValue);
|
|
13734
14294
|
if (profileTokens.includes("full") || profileTokens.includes("all"))
|
|
@@ -13800,8 +14360,8 @@ function createMcpManifest(options = {}) {
|
|
|
13800
14360
|
name: "todos",
|
|
13801
14361
|
binary: "todos-mcp",
|
|
13802
14362
|
transport: "stdio",
|
|
13803
|
-
profileEnvironmentVariable: "
|
|
13804
|
-
groupEnvironmentVariable: "
|
|
14363
|
+
profileEnvironmentVariable: "HASNA_TODOS_PROFILE",
|
|
14364
|
+
groupEnvironmentVariable: "HASNA_TODOS_TOOL_GROUPS"
|
|
13805
14365
|
},
|
|
13806
14366
|
groups: MCP_TOOL_GROUPS,
|
|
13807
14367
|
profiles: MCP_PROFILE_GROUPS,
|
|
@@ -14781,7 +15341,7 @@ var TODOS_JSON_CONTRACTS = [
|
|
|
14781
15341
|
contract({
|
|
14782
15342
|
id: "tester_issue_report",
|
|
14783
15343
|
name: "Tester Issue Report",
|
|
14784
|
-
description: "Generic issue report payload emitted by
|
|
15344
|
+
description: "Generic issue report payload emitted by the testers QA CLI or compatible test runners for conversion into local tasks.",
|
|
14785
15345
|
surfaces: ["cli", "sdk"],
|
|
14786
15346
|
stability: "stable",
|
|
14787
15347
|
required: {
|
|
@@ -16206,14 +16766,15 @@ function normalizeApprovalRefs(values) {
|
|
|
16206
16766
|
function resolveTodosAiCommandOptions(input) {
|
|
16207
16767
|
const cli = input.cli ?? {};
|
|
16208
16768
|
const config = input.config ?? {};
|
|
16209
|
-
const
|
|
16210
|
-
const
|
|
16211
|
-
const
|
|
16769
|
+
const env2 = input.env ?? {};
|
|
16770
|
+
const envFormat = env2["HASNA_TODOS_AI_FORMAT"] ?? env2["TODOS_AI_FORMAT"];
|
|
16771
|
+
const format = parseEnum(selectedString(cli.format, envFormat, config.format), TODOS_AI_FORMATS, "format", TODOS_AI_DEFAULTS.format);
|
|
16772
|
+
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
16773
|
if (cli.dryRun && cli.writeMode === "execute") {
|
|
16213
16774
|
throw new TodosAiContractError("invalid_configuration", "--dry-run cannot be combined with --write-mode execute");
|
|
16214
16775
|
}
|
|
16215
16776
|
const writeMode = cli.dryRun ? "plan" : configuredWriteMode;
|
|
16216
|
-
const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode,
|
|
16777
|
+
const explicitApproval = cli.dryRun ? selectedString(cli.approvalMode) : selectedString(cli.approvalMode, env2["TODOS_AI_APPROVAL_MODE"], config.approval_mode);
|
|
16217
16778
|
const defaultApproval = writeMode === "execute" ? input.interactive ? "prompt" : "required" : "deny";
|
|
16218
16779
|
const approvalMode = cli.dryRun ? parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", "deny") : parseEnum(explicitApproval, TODOS_AI_APPROVAL_MODES, "approval mode", defaultApproval);
|
|
16219
16780
|
const approvalRefs = normalizeApprovalRefs(cli.approvalRefs);
|
|
@@ -16236,12 +16797,12 @@ function resolveTodosAiCommandOptions(input) {
|
|
|
16236
16797
|
throw new TodosAiContractError("invalid_configuration", "--approval references require --approval-mode existing");
|
|
16237
16798
|
}
|
|
16238
16799
|
return {
|
|
16239
|
-
provider: selectedString(cli.provider,
|
|
16240
|
-
model: selectedString(cli.model,
|
|
16241
|
-
profile: selectedString(cli.profile,
|
|
16800
|
+
provider: selectedString(cli.provider, env2["TODOS_AI_PROVIDER"], config.provider),
|
|
16801
|
+
model: selectedString(cli.model, env2["TODOS_AI_MODEL"], config.model),
|
|
16802
|
+
profile: selectedString(cli.profile, env2["TODOS_AI_PROFILE"], config.profile),
|
|
16242
16803
|
format,
|
|
16243
|
-
max_steps: parseBoundedInteger(cli.maxSteps ??
|
|
16244
|
-
timeout_ms: parseBoundedInteger(cli.timeoutMs ??
|
|
16804
|
+
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),
|
|
16805
|
+
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
16806
|
write_mode: writeMode,
|
|
16246
16807
|
approval_mode: approvalMode,
|
|
16247
16808
|
approval_refs: approvalRefs,
|
|
@@ -16539,7 +17100,7 @@ function todosAiExitCodeForResult(result) {
|
|
|
16539
17100
|
}
|
|
16540
17101
|
// src/lib/onboarding-fixtures.ts
|
|
16541
17102
|
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
16542
|
-
import { join as
|
|
17103
|
+
import { join as join7 } from "path";
|
|
16543
17104
|
|
|
16544
17105
|
// src/lib/local-bridge.ts
|
|
16545
17106
|
init_database();
|
|
@@ -17476,7 +18037,7 @@ function writeOnboardingFixtureFiles(directory) {
|
|
|
17476
18037
|
mkdirSync5(directory, { recursive: true });
|
|
17477
18038
|
const files = [];
|
|
17478
18039
|
for (const fixture of allFixtures()) {
|
|
17479
|
-
const path =
|
|
18040
|
+
const path = join7(directory, `${fixture.summary.name}.bridge.json`);
|
|
17480
18041
|
writeFileSync3(path, `${JSON.stringify(fixture.bundle, null, 2)}
|
|
17481
18042
|
`, "utf-8");
|
|
17482
18043
|
files.push(path);
|
|
@@ -17494,7 +18055,7 @@ function importOnboardingFixture(options = {}) {
|
|
|
17494
18055
|
init_database();
|
|
17495
18056
|
import { createHash as createHash4 } from "crypto";
|
|
17496
18057
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
17497
|
-
import { dirname as dirname5, resolve as
|
|
18058
|
+
import { dirname as dirname5, resolve as resolve10 } from "path";
|
|
17498
18059
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
17499
18060
|
var TODOS_LOCAL_BACKUP_KIND = "hasna.todos.local-backup";
|
|
17500
18061
|
var TODOS_LOCAL_BACKUP_SCHEMA_VERSION = 1;
|
|
@@ -17601,14 +18162,14 @@ function createLocalBackup(options = {}, db) {
|
|
|
17601
18162
|
return backup;
|
|
17602
18163
|
}
|
|
17603
18164
|
function writeLocalBackupFile(backup, outputPath) {
|
|
17604
|
-
const path =
|
|
18165
|
+
const path = resolve10(outputPath);
|
|
17605
18166
|
mkdirSync6(dirname5(path), { recursive: true });
|
|
17606
18167
|
writeFileSync4(path, `${JSON.stringify(backup, null, 2)}
|
|
17607
18168
|
`);
|
|
17608
18169
|
return path;
|
|
17609
18170
|
}
|
|
17610
18171
|
function readLocalBackupFile(path) {
|
|
17611
|
-
return JSON.parse(readFileSync5(
|
|
18172
|
+
return JSON.parse(readFileSync5(resolve10(path), "utf-8"));
|
|
17612
18173
|
}
|
|
17613
18174
|
function verifyLocalBackup(value, options = {}, db) {
|
|
17614
18175
|
const verifiedAt = options.verified_at ?? now();
|
|
@@ -18302,7 +18863,7 @@ function renderLocalSnapshotMarkdown(snapshot) {
|
|
|
18302
18863
|
}
|
|
18303
18864
|
// src/lib/sdk-integration-fixtures.ts
|
|
18304
18865
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
18305
|
-
import { join as
|
|
18866
|
+
import { join as join8 } from "path";
|
|
18306
18867
|
|
|
18307
18868
|
// src/cli-mcp-parity.ts
|
|
18308
18869
|
function source4(version) {
|
|
@@ -20273,7 +20834,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
|
|
|
20273
20834
|
];
|
|
20274
20835
|
const written = [];
|
|
20275
20836
|
for (const [name, payload] of files) {
|
|
20276
|
-
const file =
|
|
20837
|
+
const file = join8(directory, name);
|
|
20277
20838
|
writeFileSync5(file, `${JSON.stringify(payload, null, 2)}
|
|
20278
20839
|
`, "utf-8");
|
|
20279
20840
|
written.push(file);
|
|
@@ -21556,11 +22117,11 @@ function renderLocalAuditLedgerMarkdown(ledger) {
|
|
|
21556
22117
|
init_migrations();
|
|
21557
22118
|
init_schema();
|
|
21558
22119
|
import { readFileSync as readFileSync6 } from "fs";
|
|
21559
|
-
import { join as
|
|
22120
|
+
import { join as join9, resolve as resolve11 } from "path";
|
|
21560
22121
|
import { Database as Database2 } from "bun:sqlite";
|
|
21561
22122
|
var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1;
|
|
21562
22123
|
var EXPECTED_PACKAGE_NAME = "@hasna/todos";
|
|
21563
|
-
var EXPECTED_REPOSITORY = "https://github.com/hasna/
|
|
22124
|
+
var EXPECTED_REPOSITORY = "https://github.com/hasna/apps.git";
|
|
21564
22125
|
var EXPECTED_BINS = ["todos", "todos-mcp", "todos-serve"];
|
|
21565
22126
|
var EXPECTED_EXPORTS = [".", "./sdk", "./mcp", "./registry", "./contracts", "./storage"];
|
|
21566
22127
|
var REQUIRED_SCRIPTS = ["build", "test:no-cloud", "verify:release", "prepublishOnly"];
|
|
@@ -21602,7 +22163,7 @@ function warn(id, message, details) {
|
|
|
21602
22163
|
return { id, status: "warning", message, details };
|
|
21603
22164
|
}
|
|
21604
22165
|
function readPackageJson(root) {
|
|
21605
|
-
return JSON.parse(readFileSync6(
|
|
22166
|
+
return JSON.parse(readFileSync6(join9(root, "package.json"), "utf8"));
|
|
21606
22167
|
}
|
|
21607
22168
|
function sortedKeys(value) {
|
|
21608
22169
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -21664,7 +22225,7 @@ function checkPackage(packageJson) {
|
|
|
21664
22225
|
const checks = [];
|
|
21665
22226
|
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
22227
|
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/
|
|
22228
|
+
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
22229
|
const dependencyNames = Object.keys(packageJson.dependencies ?? {});
|
|
21669
22230
|
const privateStudio = `hasna${"studio"}`;
|
|
21670
22231
|
const privatePlatform = `platform${"-"}todos`;
|
|
@@ -21698,7 +22259,7 @@ function checkChangelog() {
|
|
|
21698
22259
|
];
|
|
21699
22260
|
}
|
|
21700
22261
|
function createReleaseCompatibilityReport(options = {}) {
|
|
21701
|
-
const root =
|
|
22262
|
+
const root = resolve11(options.root ?? process.cwd());
|
|
21702
22263
|
const packageJson = readPackageJson(root);
|
|
21703
22264
|
const simulatedLevels = options.simulated_levels ?? defaultSimulationLevels();
|
|
21704
22265
|
const checks = [
|
|
@@ -24028,22 +24589,22 @@ function millisBetween(start, end) {
|
|
|
24028
24589
|
return 0;
|
|
24029
24590
|
return endMs - startMs;
|
|
24030
24591
|
}
|
|
24031
|
-
function addTaskScope(where, params, options, db,
|
|
24592
|
+
function addTaskScope(where, params, options, db, alias2 = "t") {
|
|
24032
24593
|
if (options.project_id) {
|
|
24033
|
-
where.push(`${
|
|
24594
|
+
where.push(`${alias2}.project_id = ?`);
|
|
24034
24595
|
params.push(options.project_id);
|
|
24035
24596
|
}
|
|
24036
24597
|
if (options.agent_id) {
|
|
24037
24598
|
params.push(options.agent_id);
|
|
24038
|
-
const assignedInClause = lowerInClause(`${
|
|
24039
|
-
where.push(`(${
|
|
24599
|
+
const assignedInClause = lowerInClause(`${alias2}.assigned_to`, resolveAssignedToAliases(db, options.agent_id), params);
|
|
24600
|
+
where.push(`(${alias2}.agent_id = ? OR ${assignedInClause})`);
|
|
24040
24601
|
}
|
|
24041
24602
|
if (options.since) {
|
|
24042
|
-
where.push(`${
|
|
24603
|
+
where.push(`${alias2}.created_at >= ?`);
|
|
24043
24604
|
params.push(options.since);
|
|
24044
24605
|
}
|
|
24045
24606
|
if (options.until) {
|
|
24046
|
-
where.push(`${
|
|
24607
|
+
where.push(`${alias2}.created_at <= ?`);
|
|
24047
24608
|
params.push(options.until);
|
|
24048
24609
|
}
|
|
24049
24610
|
}
|
|
@@ -24712,16 +25273,25 @@ var DEFAULT_ENCRYPTION_KEY_ENV = "TODOS_ENCRYPTION_KEY";
|
|
|
24712
25273
|
class EncryptionKeyUnavailableError extends Error {
|
|
24713
25274
|
keyEnv;
|
|
24714
25275
|
profile;
|
|
25276
|
+
static code = "ENCRYPTION_KEY_UNAVAILABLE";
|
|
25277
|
+
code = EncryptionKeyUnavailableError.code;
|
|
25278
|
+
suggestion;
|
|
24715
25279
|
constructor(keyEnv, profile) {
|
|
24716
25280
|
super(`Encryption key is locked: set ${keyEnv} to use profile ${profile}`);
|
|
24717
25281
|
this.keyEnv = keyEnv;
|
|
24718
25282
|
this.profile = profile;
|
|
25283
|
+
this.name = "EncryptionKeyUnavailableError";
|
|
25284
|
+
this.suggestion = `Set ${keyEnv} in the environment of the process serving this store, then retry.`;
|
|
24719
25285
|
}
|
|
24720
25286
|
}
|
|
24721
25287
|
|
|
24722
25288
|
class EncryptedPayloadError extends Error {
|
|
25289
|
+
static code = "ENCRYPTED_PAYLOAD_INVALID";
|
|
25290
|
+
code = EncryptedPayloadError.code;
|
|
25291
|
+
static suggestion = "Pass a value produced by encrypt_local_value for this profile; a plain value is not an encrypted envelope.";
|
|
24723
25292
|
constructor(message) {
|
|
24724
25293
|
super(message);
|
|
25294
|
+
this.name = "EncryptedPayloadError";
|
|
24725
25295
|
}
|
|
24726
25296
|
}
|
|
24727
25297
|
function now3() {
|
|
@@ -24793,17 +25363,17 @@ function removeEncryptionProfile(name) {
|
|
|
24793
25363
|
saveConfig({ ...config, encryption_profiles: next });
|
|
24794
25364
|
return true;
|
|
24795
25365
|
}
|
|
24796
|
-
function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE,
|
|
25366
|
+
function encryptionProfileStatus(name = DEFAULT_ENCRYPTION_PROFILE, env2 = process.env) {
|
|
24797
25367
|
const profile = ensureEncryptionProfile(name);
|
|
24798
25368
|
return {
|
|
24799
25369
|
profile: redactValue(profile),
|
|
24800
|
-
locked: !
|
|
25370
|
+
locked: !env2[profile.key_env],
|
|
24801
25371
|
key_env: profile.key_env,
|
|
24802
|
-
key_present: Boolean(
|
|
25372
|
+
key_present: Boolean(env2[profile.key_env])
|
|
24803
25373
|
};
|
|
24804
25374
|
}
|
|
24805
|
-
function keyForProfile(profile,
|
|
24806
|
-
const secret =
|
|
25375
|
+
function keyForProfile(profile, env2) {
|
|
25376
|
+
const secret = env2[profile.key_env];
|
|
24807
25377
|
if (!secret)
|
|
24808
25378
|
throw new EncryptionKeyUnavailableError(profile.key_env, profile.name);
|
|
24809
25379
|
return deriveKey(secret, profile.salt);
|
|
@@ -24834,7 +25404,7 @@ function isEncryptedValue(value) {
|
|
|
24834
25404
|
const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
24835
25405
|
return Boolean(record && record.kind === TODOS_ENCRYPTED_VALUE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
|
|
24836
25406
|
}
|
|
24837
|
-
function decryptString(envelope,
|
|
25407
|
+
function decryptString(envelope, env2 = process.env) {
|
|
24838
25408
|
if (!isEncryptedValue(envelope))
|
|
24839
25409
|
throw new EncryptedPayloadError("value is not a hasna/todos encrypted envelope");
|
|
24840
25410
|
const profile = profileFromConfig(envelope.profile) ?? {
|
|
@@ -24844,7 +25414,7 @@ function decryptString(envelope, env = process.env) {
|
|
|
24844
25414
|
key_env: envelope.key_env,
|
|
24845
25415
|
salt: envelope.salt
|
|
24846
25416
|
};
|
|
24847
|
-
const key = keyForProfile(profile,
|
|
25417
|
+
const key = keyForProfile(profile, env2);
|
|
24848
25418
|
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"));
|
|
24849
25419
|
decipher.setAuthTag(Buffer.from(envelope.auth_tag, "base64"));
|
|
24850
25420
|
const plaintext = Buffer.concat([
|
|
@@ -24861,8 +25431,8 @@ function decryptString(envelope, env = process.env) {
|
|
|
24861
25431
|
function encryptValue(value, options = {}) {
|
|
24862
25432
|
return encryptString(JSON.stringify(value), options);
|
|
24863
25433
|
}
|
|
24864
|
-
function decryptValue(envelope,
|
|
24865
|
-
return JSON.parse(decryptString(envelope,
|
|
25434
|
+
function decryptValue(envelope, env2 = process.env) {
|
|
25435
|
+
return JSON.parse(decryptString(envelope, env2));
|
|
24866
25436
|
}
|
|
24867
25437
|
function looksSensitiveKey(key) {
|
|
24868
25438
|
return /api[_-]?key|token|secret|password|credential|evidence|artifact|output|summary|metadata/i.test(key);
|
|
@@ -24920,7 +25490,7 @@ function isEncryptedBridgeBundle(value) {
|
|
|
24920
25490
|
const record = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
24921
25491
|
return Boolean(record && record.kind === TODOS_ENCRYPTED_BRIDGE_KIND && record.schemaVersion === TODOS_ENCRYPTION_SCHEMA_VERSION);
|
|
24922
25492
|
}
|
|
24923
|
-
function decryptBridgeBundle(bundle,
|
|
25493
|
+
function decryptBridgeBundle(bundle, env2 = process.env) {
|
|
24924
25494
|
if (!isEncryptedBridgeBundle(bundle))
|
|
24925
25495
|
throw new EncryptedPayloadError("bundle is not an encrypted hasna/todos bridge export");
|
|
24926
25496
|
const plaintext = decryptString({
|
|
@@ -24936,7 +25506,7 @@ function decryptBridgeBundle(bundle, env = process.env) {
|
|
|
24936
25506
|
auth_tag: bundle.encryption.auth_tag,
|
|
24937
25507
|
ciphertext: bundle.encryption.ciphertext,
|
|
24938
25508
|
plaintext_sha256: bundle.plaintext.sha256
|
|
24939
|
-
},
|
|
25509
|
+
}, env2);
|
|
24940
25510
|
return JSON.parse(plaintext);
|
|
24941
25511
|
}
|
|
24942
25512
|
function assertExportProfileAllowed(profile) {
|