@hasna/hooks 0.4.1 → 0.6.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.
Files changed (35) hide show
  1. package/README.md +96 -12
  2. package/bin/index.js +2089 -470
  3. package/bin/serve.js +5248 -0
  4. package/dist/cf/provision.d.ts +24 -0
  5. package/dist/config.d.ts +18 -0
  6. package/dist/db/legacy-import.d.ts +1 -1
  7. package/dist/db/migrations/004_hooks_table.d.ts +9 -0
  8. package/dist/db/pg-migrations.d.ts +1 -1
  9. package/dist/db/storage-sync.d.ts +26 -6
  10. package/dist/index.d.ts +18 -2
  11. package/dist/index.js +5341 -286
  12. package/dist/lib/custom-install.d.ts +19 -0
  13. package/dist/lib/manifest.d.ts +70 -0
  14. package/dist/lib/resolve.d.ts +19 -0
  15. package/dist/lib/run.d.ts +37 -0
  16. package/dist/lib/store.d.ts +69 -0
  17. package/dist/lib/sync.d.ts +35 -0
  18. package/dist/serve.d.ts +36 -0
  19. package/dist/storage.d.ts +2 -2
  20. package/dist/storage.js +133 -42
  21. package/hooks/codewith-native-common.test.ts +1521 -3
  22. package/hooks/codewith-native-common.ts +1627 -37
  23. package/hooks/hook-scanoutput/README.md +151 -0
  24. package/hooks/hook-scanoutput/package.json +12 -0
  25. package/hooks/hook-scanoutput/src/hook.test.ts +217 -0
  26. package/hooks/hook-scanoutput/src/hook.ts +319 -0
  27. package/hooks/mention-context/README.md +109 -0
  28. package/hooks/mention-context/package.json +9 -0
  29. package/hooks/mention-context/src/hasna-mention-context.py +1218 -0
  30. package/hooks/mention-context/src/hasna-mention-warm.py +521 -0
  31. package/hooks/mention-context/src/hook.test.ts +68 -0
  32. package/hooks/mention-context/src/test_run_capture.py +345 -0
  33. package/hooks/pre-bash/README.md +72 -2
  34. package/hooks/worktree-guard/README.md +9 -2
  35. package/package.json +9 -5
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python3
2
+ """Regression suite for the shared-temp-file race in hasna-mention-context.py.
3
+
4
+ THE DEFECT. `run_capture` derived its output path from its `tag` argument alone,
5
+ and `probe_local_head` passed a constant `tag="gitlog"`. Repository probes run
6
+ concurrently against one shared temp directory, so every mentioned repository's
7
+ `git log` wrote to and read back the same `gitlog.out` and the reader got
8
+ whatever the last writer left. The emitted sha was always a real sha from a real
9
+ repository, just the wrong one.
10
+
11
+ WHY THESE TESTS ARE TIMED RATHER THAN LOOPED. A race reproduced by "run it many
12
+ times and hope" passes on broken code whenever the interleaving happens not to
13
+ occur, which makes it worthless as a gate. Both tests below instead FORCE the
14
+ losing interleaving with a controlled delay, so they fail on the unfixed code on
15
+ every run rather than on some runs.
16
+
17
+ The construction, which is the whole idea:
18
+
19
+ EARLY WRITER, LATE READER writes at t=0, exits and reads at t=SLOW
20
+ LATE WRITER, EARLY READER writes and exits and reads at t=FAST
21
+
22
+ t=0 both open(path,"wb") -> truncate
23
+ t=0 early's process writes its marker
24
+ t=FAST late's process overwrites the same path, then reads -> its own value
25
+ t=SLOW early reads -> LATE'S VALUE, not its own <- the defect
26
+
27
+ With one file per call the two never interact and both read their own output.
28
+ """
29
+
30
+ import importlib.util
31
+ import os
32
+ import shutil
33
+ import subprocess
34
+ import sys
35
+ import tempfile
36
+ import threading
37
+ import unittest
38
+
39
+ HERE = os.path.dirname(os.path.abspath(__file__))
40
+ HOOK_PATH = os.path.join(HERE, "hasna-mention-context.py")
41
+
42
+ # Timings. SLOW must stay comfortably under probe_local_head's hardcoded 2.0 s
43
+ # subprocess timeout, or a slow machine turns a real pass into a spurious
44
+ # failure. The gap between them is the margin that makes the interleaving
45
+ # deterministic; 0.55 s absorbs a great deal of scheduler jitter.
46
+ SLOW_S = 0.80
47
+ FAST_S = 0.25
48
+
49
+
50
+ def load_hook():
51
+ """Import the hook by path. Its filename is not a legal module name, and it
52
+ guards main() behind __name__ == "__main__", so importing runs no hook."""
53
+ spec = importlib.util.spec_from_file_location("hasna_mention_context", HOOK_PATH)
54
+ if spec is None or spec.loader is None:
55
+ raise RuntimeError("could not load a module spec for %s" % HOOK_PATH)
56
+ mod = importlib.util.module_from_spec(spec)
57
+ spec.loader.exec_module(mod)
58
+ return mod
59
+
60
+
61
+ mod = load_hook()
62
+
63
+
64
+ def run_pair(first, second):
65
+ """Run two callables on two threads released simultaneously by a barrier.
66
+
67
+ Returns {name: value}. A barrier rather than two bare start() calls, because
68
+ the interleaving depends on both calls entering the region together.
69
+ """
70
+ results = {}
71
+ errors = {}
72
+ barrier = threading.Barrier(2)
73
+
74
+ def wrap(name, fn):
75
+ def run():
76
+ try:
77
+ barrier.wait(timeout=10)
78
+ results[name] = fn()
79
+ except BaseException as exc: # noqa: BLE001 - surfaced below
80
+ errors[name] = exc
81
+ return run
82
+
83
+ threads = [threading.Thread(target=wrap(n, f), daemon=True)
84
+ for n, f in (first, second)]
85
+ for t in threads:
86
+ t.start()
87
+ for t in threads:
88
+ t.join(timeout=30)
89
+ for t in threads:
90
+ if t.is_alive():
91
+ raise AssertionError("a probe thread did not finish within 30 s")
92
+ if errors:
93
+ raise AssertionError("probe thread raised: %r" % (errors,))
94
+ return results
95
+
96
+
97
+ class RunCaptureIsolation(unittest.TestCase):
98
+ """Level 1 — run_capture itself, no git, fully hermetic."""
99
+
100
+ def test_concurrent_captures_sharing_a_tag_do_not_cross(self):
101
+ tmpdir = tempfile.mkdtemp(prefix="mention-context-test-")
102
+ self.addCleanup(shutil.rmtree, tmpdir, ignore_errors=True)
103
+
104
+ # Equal-length markers, so that on the unfixed code the overwrite is
105
+ # clean and the failure message shows one whole wrong value rather than
106
+ # two values spliced at a byte boundary.
107
+ early_mark = "AAAAAAAA"
108
+ late_mark = "BBBBBBBB"
109
+
110
+ def capture(mark, pre_sleep, post_sleep):
111
+ script = (
112
+ "import sys, time\n"
113
+ "time.sleep(%r)\n"
114
+ "sys.stdout.write(%r)\n"
115
+ "sys.stdout.flush()\n"
116
+ "time.sleep(%r)\n" % (pre_sleep, mark, post_sleep)
117
+ )
118
+ # A python child rather than a shell one-liner: the write and the
119
+ # flush are explicit, so "the bytes reached the file before the
120
+ # sleep" is a property of the test rather than of shell builtin
121
+ # buffering.
122
+ def call():
123
+ return mod.run_capture(
124
+ [sys.executable, "-c", script],
125
+ timeout=30.0, tmpdir=tmpdir, tag="gitlog",
126
+ )
127
+ return call
128
+
129
+ results = run_pair(
130
+ ("early", capture(early_mark, 0.0, SLOW_S)), # writes t=0, reads t=SLOW
131
+ ("late", capture(late_mark, FAST_S, 0.0)), # writes and reads t=FAST
132
+ )
133
+
134
+ early_rc, early_out = results["early"]
135
+ late_rc, late_out = results["late"]
136
+
137
+ self.assertEqual(early_rc, 0, "early capture did not run: rc=%r" % (early_rc,))
138
+ self.assertEqual(late_rc, 0, "late capture did not run: rc=%r" % (late_rc,))
139
+
140
+ # This is the assertion the defect breaks. On the unfixed code the early
141
+ # caller reads the LATE caller's marker, because both used gitlog.out.
142
+ self.assertEqual(
143
+ early_out, early_mark,
144
+ "the early caller read %r but produced %r — a concurrent call's "
145
+ "output was attributed to it (shared temp path)"
146
+ % (early_out, early_mark),
147
+ )
148
+ self.assertEqual(
149
+ late_out, late_mark,
150
+ "the late caller read %r but produced %r" % (late_out, late_mark),
151
+ )
152
+
153
+ def test_capture_paths_are_distinct_for_the_same_tag(self):
154
+ """The invariant directly: one tag, two calls, two files.
155
+
156
+ Kept separate from the timed test because it states the property in one
157
+ line and cannot be confused by machine load.
158
+ """
159
+ tmpdir = tempfile.mkdtemp(prefix="mention-context-test-")
160
+ self.addCleanup(shutil.rmtree, tmpdir, ignore_errors=True)
161
+
162
+ for _ in range(2):
163
+ rc, _out = mod.run_capture([sys.executable, "-c", "pass"],
164
+ timeout=30.0, tmpdir=tmpdir, tag="gitlog")
165
+ self.assertEqual(rc, 0)
166
+
167
+ outs = [n for n in os.listdir(tmpdir) if n.endswith(".out")]
168
+ self.assertEqual(
169
+ len(outs), 2,
170
+ "two calls with tag='gitlog' produced %d .out file(s) (%r); each "
171
+ "call must own its path or concurrent calls overwrite each other"
172
+ % (len(outs), sorted(outs)),
173
+ )
174
+ self.assertTrue(
175
+ all(n.startswith("gitlog") for n in outs),
176
+ "the tag must survive as a readable prefix, got %r" % (sorted(outs),),
177
+ )
178
+
179
+
180
+ class ProbeLocalHeadIsolation(unittest.TestCase):
181
+ """Level 2 — probe_local_head end to end, real git, real repositories.
182
+
183
+ The repositories are built here rather than borrowed from the machine, so
184
+ the test is hermetic and the expected shas are known exactly.
185
+ """
186
+
187
+ def setUp(self):
188
+ self.git = shutil.which("git")
189
+ if not self.git:
190
+ # Fail rather than skip. A skipped test is indistinguishable from a
191
+ # passing one in the summary line, and this suite exists to gate a
192
+ # defect that already shipped once.
193
+ self.fail("git is not on PATH; this regression test cannot run")
194
+ self.root = tempfile.mkdtemp(prefix="mention-context-repos-")
195
+ self.addCleanup(shutil.rmtree, self.root, ignore_errors=True)
196
+
197
+ def make_repo(self, name, content):
198
+ path = os.path.join(self.root, name)
199
+ os.makedirs(path)
200
+ # Identity is supplied through the environment of these throwaway
201
+ # fixture processes only. Nothing here writes git config, and no real
202
+ # repository is touched.
203
+ env = dict(os.environ)
204
+ env.update({
205
+ "GIT_AUTHOR_NAME": "mention-context test",
206
+ "GIT_AUTHOR_EMAIL": "test@example.invalid",
207
+ "GIT_COMMITTER_NAME": "mention-context test",
208
+ "GIT_COMMITTER_EMAIL": "test@example.invalid",
209
+ "GIT_CONFIG_GLOBAL": os.path.join(self.root, "no-such-gitconfig"),
210
+ "GIT_CONFIG_SYSTEM": os.path.join(self.root, "no-such-gitconfig"),
211
+ })
212
+ def git(*args):
213
+ cp = subprocess.run([self.git, "-C", path] + list(args), env=env,
214
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
215
+ if cp.returncode != 0:
216
+ self.fail("fixture git %r failed rc=%d: %s"
217
+ % (args, cp.returncode, cp.stderr.decode("utf-8", "replace")))
218
+ return cp.stdout.decode("utf-8", "replace")
219
+ git("init", "-q")
220
+ with open(os.path.join(path, "file.txt"), "w") as f:
221
+ f.write(content)
222
+ git("add", "file.txt")
223
+ git("commit", "-q", "-m", "fixture commit for " + name)
224
+ sha = git("log", "-1", "--format=%h").strip()
225
+ return path, sha
226
+
227
+ def write_timing_wrapper(self, slow_repo_path):
228
+ """A git wrapper that makes one repository the early-writer/late-reader.
229
+
230
+ For the slow repository: run git (its output lands in the capture file
231
+ immediately, flushed by git's own exit) and only then sleep before
232
+ exiting, so the read happens late. For every other repository: sleep
233
+ first, so its write lands in between.
234
+ """
235
+ wrapper = os.path.join(self.root, "git-timing-wrapper")
236
+ with open(wrapper, "w") as f:
237
+ f.write(
238
+ "#!/bin/sh\n"
239
+ 'case "$*" in\n'
240
+ " *%s*) %s \"$@\"; ec=$?; sleep %s; exit $ec ;;\n"
241
+ " *) sleep %s; exec %s \"$@\" ;;\n"
242
+ "esac\n" % (os.path.basename(slow_repo_path), self.git,
243
+ SLOW_S, FAST_S, self.git)
244
+ )
245
+ os.chmod(wrapper, 0o755)
246
+ return wrapper
247
+
248
+ def test_concurrent_probes_report_their_own_repositorys_head(self):
249
+ alpha_path, alpha_sha = self.make_repo("alpha", "alpha content\n")
250
+ beta_path, beta_sha = self.make_repo("beta", "beta content\n")
251
+ self.assertNotEqual(
252
+ alpha_sha, beta_sha,
253
+ "fixture repositories must have different heads for this test to "
254
+ "be able to detect wrong attribution at all",
255
+ )
256
+
257
+ tmpdir = tempfile.mkdtemp(prefix="mention-context-test-")
258
+ self.addCleanup(shutil.rmtree, tmpdir, ignore_errors=True)
259
+
260
+ wrapper = self.write_timing_wrapper(beta_path)
261
+ original_git_bin = mod.GIT_BIN
262
+ mod.GIT_BIN = wrapper
263
+ self.addCleanup(setattr, mod, "GIT_BIN", original_git_bin)
264
+
265
+ results = run_pair(
266
+ ("beta", lambda: mod.probe_local_head(beta_path, tmpdir)),
267
+ ("alpha", lambda: mod.probe_local_head(alpha_path, tmpdir)),
268
+ )
269
+
270
+ self.assertIsNotNone(
271
+ results["beta"],
272
+ "probe_local_head returned None for beta; the probe did not run, "
273
+ "so this test proved nothing about attribution",
274
+ )
275
+ self.assertIsNotNone(results["alpha"], "probe_local_head returned None for alpha")
276
+
277
+ # beta is the early writer and late reader: on the unfixed code it reads
278
+ # alpha's line out of the shared gitlog.out and reports alpha's sha.
279
+ self.assertEqual(
280
+ results["beta"]["sha"], beta_sha,
281
+ "beta reported %r; its own head is %r and alpha's is %r — one "
282
+ "repository's head was attributed to another"
283
+ % (results["beta"]["sha"], beta_sha, alpha_sha),
284
+ )
285
+ self.assertEqual(
286
+ results["alpha"]["sha"], alpha_sha,
287
+ "alpha reported %r, expected %r"
288
+ % (results["alpha"]["sha"], alpha_sha),
289
+ )
290
+
291
+
292
+ def colliding_heads(pairs):
293
+ """Field canary, NOT the gate — see the class below for why.
294
+
295
+ `pairs` is [(resolved_checkout, sha), ...]. Returns the shas claimed by more
296
+ than one DISTINCT checkout. Comparing resolved paths rather than mention
297
+ tokens is what stops it firing on `hasna/loops` and `hasna/open-loops`, which
298
+ are two tokens naming one checkout and legitimately share a head.
299
+ """
300
+ by_sha = {}
301
+ for checkout, sha in pairs:
302
+ if not checkout or not sha:
303
+ continue
304
+ by_sha.setdefault(sha, set()).add(os.path.realpath(checkout))
305
+ return sorted(sha for sha, paths in by_sha.items() if len(paths) > 1)
306
+
307
+
308
+ class DuplicateShaCanary(unittest.TestCase):
309
+ """The duplicate-sha check, with its own blind spot pinned down in a test.
310
+
311
+ Two unrelated repositories cannot share a commit sha, so a single emitted
312
+ block claiming one sha for two checkouts is self-evidently corrupt — no
313
+ fixtures needed, which makes this cheap to run against real output in the
314
+ field.
315
+
316
+ It is NOT the acceptance gate, and the test below is what stops anyone
317
+ promoting it to one: it detects COLLISION TO A COMMON VALUE while the defect
318
+ is WRONG ATTRIBUTION, and a clean swap between two concurrent writers is
319
+ pairwise distinct and entirely wrong.
320
+ """
321
+
322
+ def test_catches_the_duplicate_case(self):
323
+ pairs = [("/repos/open-loops", "82a3acf"), ("/repos/open-logs", "82a3acf")]
324
+ self.assertEqual(colliding_heads(pairs), ["82a3acf"])
325
+
326
+ def test_is_blind_to_the_swap_case(self):
327
+ # The 17:48 observation: each value real, each attributed to the wrong
328
+ # repository, pairwise distinct. This assertion records a limitation on
329
+ # purpose; if it ever starts failing the canary got stronger and this
330
+ # test should be revisited, not deleted quietly.
331
+ pairs = [("/repos/open-loops", "27cffd7"), ("/repos/open-logs", "82a3acf")]
332
+ self.assertEqual(
333
+ colliding_heads(pairs), [],
334
+ "the canary is expected to MISS a clean swap; it is a supplement to "
335
+ "the concurrency tests above, never a replacement",
336
+ )
337
+
338
+ def test_does_not_fire_when_two_tokens_name_one_checkout(self):
339
+ # `hasna/loops` and `hasna/open-loops` both resolve to open-loops.
340
+ pairs = [("/repos/open-loops", "82a3acf"), ("/repos/open-loops", "82a3acf")]
341
+ self.assertEqual(colliding_heads(pairs), [])
342
+
343
+
344
+ if __name__ == "__main__":
345
+ unittest.main(verbosity=2)
@@ -6,8 +6,78 @@ This hook is OSS-safe: optional Hasna CLIs are best-effort and missing CLIs fail
6
6
 
7
7
  It also blocks scoped destructive shell operations such as recursive `rm`,
8
8
  `rsync --delete`, destructive `find`, and destructive `git clean` / `git reset
9
- --hard` forms only when the resolved target threatens `~/.hasna`, configured
10
- workspace roots, Hasna division/scope roots, or active repo/worktree roots.
9
+ --hard` forms when the resolved target threatens a protected root.
10
+
11
+ ## Protected roots
12
+
13
+ - `/` and the system directories (`/usr`, `/etc`, `/bin`, `/lib`, `/var`, `/boot`,
14
+ `/home`, `/Users`, and the other FHS and macOS equivalents). Add machine-specific
15
+ entries with `HASNA_PROTECTED_SYSTEM_ROOTS` (colon-separated). `/tmp` is not
16
+ protected — scratch cleanup there is routine.
17
+ - `~/.hasna`, configured workspace roots, Hasna division/scope roots, and active
18
+ repo/worktree roots.
19
+
20
+ These match in *root* mode: wiping a root or its contents (`rm -rf /usr`,
21
+ `rm -rf /usr/*`) blocks, while a targeted delete beneath one
22
+ (`rm -rf /usr/local/lib/my-build`) is allowed.
23
+
24
+ ## Expansions that can collapse to empty
25
+
26
+ A destructive target containing a command substitution, backtick substitution or
27
+ variable expansion is checked twice: as written, and as the shell would render it
28
+ if the expansion returned empty. `rm -rf "$(anything)"/*`, `` rm -rf `cmd`/* ``,
29
+ `rm -rf "$VAR"/*` and `rm -rf "${VAR}"/*` are blocked by shape, whatever the
30
+ expansion is.
31
+
32
+ This exists because of a realized incident: `bun pm cache` exits non-zero with an
33
+ empty stdout when no `package.json` is found walking up from cwd, so
34
+ `rm -rf "$(bun pm cache)"/*` ran as `rm -rf /*`. Redirecting stderr does not help
35
+ — it discards the diagnostic, not the path.
36
+
37
+ Two forms are deliberately not blocked:
38
+
39
+ - `${VAR:?}` / `${VAR:?message}`, which POSIX guarantees non-empty. (`${VAR?}`
40
+ without the colon permits an empty value and is *not* exempt.)
41
+ - A bare `rm -rf "$(cmd)"` with no trailing separator, which degrades to
42
+ `rm -rf ""` — rejected by `rm` without deleting anything.
43
+
44
+ The recommended form is to resolve the path first and assert it:
45
+
46
+ ```bash
47
+ dir="$(bun pm cache)" || exit 1
48
+ case "$dir" in /|"") exit 1;; esac
49
+ rm -rf -- "$dir"
50
+ ```
51
+
52
+ ## Globs
53
+
54
+ A glob threatens a protected root when it can match that root or an ancestor of it, or when it
55
+ wipes the root's contents wholesale. Matching is per path component, so a trailing literal
56
+ bounds the delete: `rm -rf */node_modules` at a monorepo root is allowed, while `rm -rf /*/*`
57
+ is not.
58
+
59
+ A glob directly under a protected root is refused only when it is *unanchored* — when no
60
+ literal text survives once the wildcards are removed. `[a-z]*`, `?*`, `.??*` and `*.*` are
61
+ unanchored and blocked; `*.log`, `tmp-*`, `.turbo*` and `snapshot-[0-9]*` keep their literal
62
+ anchor and are allowed.
63
+
64
+ Bracket expressions that this matcher does not model exactly — POSIX `[:class:]`, `[=equiv=]`,
65
+ `[.collate.]`, backslash escapes, anything unterminated — are treated as **matching**, never as
66
+ not-matching. An under-match would leave a protected root unmatched and allow the delete, so
67
+ ambiguity resolves toward refusing.
68
+
69
+ ## Working directory
70
+
71
+ `cd`, `pushd`, `pushd -n`, `popd` and `cd -` are tracked, per subshell, with a directory stack.
72
+ A `cd` inside `( … )` or a pipeline stage applies within that shell and does not escape it.
73
+
74
+ ## Wrappers
75
+
76
+ Commands are unwrapped before scanning: `bash -c` / `sh -c` / `zsh -c`, `su -c`,
77
+ `runuser -c`, `eval`, and `ssh host '…'`, including nested combinations. `cd` is
78
+ tracked within a command, and a `for VAR in <glob>` binding is followed into
79
+ `rm -rf "$VAR"`. Remote (`ssh`) layers only consider absolute targets, because a
80
+ remote relative path cannot be resolved against the local working directory.
11
81
 
12
82
  ## Install for Codewith
13
83
 
@@ -5,8 +5,15 @@ Codewith-native hook installed as `hooks run worktree-guard`.
5
5
  This hook is OSS-safe: optional Hasna CLIs are best-effort and missing CLIs fail open with concise warnings. Security gates only fail closed when a guarded commit/push scan runs successfully and finds possible secrets.
6
6
 
7
7
  It blocks scoped destructive shell operations and file-tool-like payloads when
8
- the resolved target threatens `~/.hasna`, configured workspace roots, Hasna
9
- division/scope roots, or active repo/worktree roots.
8
+ the resolved target threatens `/` or a system root (`/usr`, `/etc`, `/var`,
9
+ `/home`, …), `~/.hasna`, configured workspace roots, Hasna division/scope roots,
10
+ or active repo/worktree roots.
11
+
12
+ It shares its classifier with `pre-bash`, so it also blocks destructive targets
13
+ whose command substitution or variable expansion could collapse to empty —
14
+ `rm -rf "$(cmd)"/*` and `rm -rf "$VAR"/*`. See
15
+ [`hooks/pre-bash/README.md`](../pre-bash/README.md) for the full rules and the
16
+ recommended safe form.
10
17
 
11
18
  ## Canonical worktree path
12
19
 
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {
7
- "hooks": "bin/index.js"
7
+ "hooks": "bin/index.js",
8
+ "hooks-serve": "bin/serve.js"
8
9
  },
9
10
  "exports": {
10
11
  ".": {
@@ -19,7 +20,7 @@
19
20
  "main": "./dist/index.js",
20
21
  "types": "./dist/index.d.ts",
21
22
  "scripts": {
22
- "build": "rm -rf dist && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external pg --external ink --external react --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/index.ts ./src/storage.ts --outdir ./dist --target bun --external pg && bun run build:types",
23
+ "build": "rm -rf dist bin && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external pg --external ink --external react --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/serve.ts --outdir ./bin --target bun --external pg && bun build ./src/index.ts ./src/storage.ts --outdir ./dist --target bun --external pg && bun run build:types",
23
24
  "build:types": "tsc -p tsconfig.build.json",
24
25
  "dev": "bun run ./src/cli/index.tsx",
25
26
  "test": "bun test",
@@ -47,10 +48,12 @@
47
48
  "@types/bun": "latest",
48
49
  "@types/pg": "^8.15.6",
49
50
  "@types/react": "^18.2.0",
50
- "typescript": "^5"
51
+ "typescript": "^5",
52
+ "bun-types": "1.3.14"
51
53
  },
52
54
  "dependencies": {
53
55
  "@hasna/events": "^0.1.6",
56
+ "@hasna/secrets": "^0.2.22",
54
57
  "@modelcontextprotocol/sdk": "^1.26.0",
55
58
  "chalk": "^5.3.0",
56
59
  "commander": "^12.1.0",
@@ -60,7 +63,8 @@
60
63
  "ink-spinner": "^5.0.0",
61
64
  "ink-text-input": "^6.0.0",
62
65
  "pg": "^8.16.3",
63
- "react": "^18.2.0"
66
+ "react": "^18.2.0",
67
+ "zod": "3.25.76"
64
68
  },
65
69
  "engines": {
66
70
  "bun": ">=1.0.0"