@hasna/hooks 0.5.0 → 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.
- package/README.md +81 -8
- package/bin/index.js +1746 -382
- package/bin/serve.js +5248 -0
- package/dist/cf/provision.d.ts +24 -0
- package/dist/config.d.ts +18 -0
- package/dist/db/legacy-import.d.ts +1 -1
- package/dist/db/migrations/004_hooks_table.d.ts +9 -0
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/db/storage-sync.d.ts +26 -6
- package/dist/index.d.ts +18 -2
- package/dist/index.js +5341 -286
- package/dist/lib/custom-install.d.ts +19 -0
- package/dist/lib/manifest.d.ts +70 -0
- package/dist/lib/resolve.d.ts +19 -0
- package/dist/lib/run.d.ts +37 -0
- package/dist/lib/store.d.ts +69 -0
- package/dist/lib/sync.d.ts +35 -0
- package/dist/serve.d.ts +36 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +133 -42
- package/hooks/codewith-native-common.test.ts +15 -2
- package/hooks/hook-scanoutput/README.md +151 -0
- package/hooks/hook-scanoutput/package.json +12 -0
- package/hooks/hook-scanoutput/src/hook.test.ts +217 -0
- package/hooks/hook-scanoutput/src/hook.ts +319 -0
- package/hooks/mention-context/README.md +109 -0
- package/hooks/mention-context/package.json +9 -0
- package/hooks/mention-context/src/hasna-mention-context.py +1218 -0
- package/hooks/mention-context/src/hasna-mention-warm.py +521 -0
- package/hooks/mention-context/src/hook.test.ts +68 -0
- package/hooks/mention-context/src/test_run_capture.py +345 -0
- 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)
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.
|
|
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"
|