@hasna/hooks 0.6.8 → 0.7.2
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 +1 -1
- package/bin/index.js +579 -113
- package/bin/serve.js +503 -38
- package/dist/config.d.ts +7 -5
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/index.js +533 -71
- package/dist/openapi.d.ts +216 -0
- package/hooks/codewith-native-common.test.ts +25 -25
- package/hooks/codewith-native-common.ts +1 -1
- package/hooks/hook-autoformat/package.json +1 -1
- package/hooks/hook-desktopnotify/package.json +1 -1
- package/hooks/hook-envsetup/package.json +1 -1
- package/hooks/hook-knowledge-context/README.md +1 -1
- package/hooks/hook-knowledge-context/src/hook.test.ts +1 -1
- package/hooks/hook-permissionguard/package.json +1 -1
- package/hooks/hook-protectfiles/package.json +1 -1
- package/hooks/hook-spiral-detector/LICENSE +191 -0
- package/hooks/hook-spiral-detector/README.md +23 -0
- package/hooks/hook-spiral-detector/package.json +12 -0
- package/hooks/hook-spiral-detector/src/hook.test.ts +72 -0
- package/hooks/hook-spiral-detector/src/hook.ts +93 -0
- package/hooks/hook-workspace-repos-guard/README.md +4 -3
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +7 -7
- package/hooks/hook-workspace-repos-guard/src/hook.ts +5 -3
- package/hooks/mention-context/src/hasna-mention-context.py +2 -2
- package/hooks/mention-context/src/hasna-mention-warm.py +1 -1
- package/hooks/pre-bash/README.md +1 -1
- package/hooks/prompt-guard/README.md +1 -1
- package/hooks/session-start/README.md +1 -1
- package/hooks/stop-sync/README.md +1 -1
- package/hooks/worktree-guard/README.md +1 -1
- package/package.json +10 -6
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# hook-spiral-detector
|
|
2
|
+
|
|
3
|
+
A PostToolUse hook that interrupts Claude Code after five consecutive Bash calls produce the same command, non-zero exit status, and first line of stderr.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
hooks install spiral-detector
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Behavior
|
|
12
|
+
|
|
13
|
+
- Hashes the command and error signature; command text and stderr are not persisted
|
|
14
|
+
- Keeps streaks separate by session
|
|
15
|
+
- Resets the streak after a success, a different failure, or a non-Bash tool call
|
|
16
|
+
- Returns `{ "continue": false }` on the fifth identical failure, stopping the agent loop
|
|
17
|
+
- Fails open if input or state cannot be read or written
|
|
18
|
+
|
|
19
|
+
The threshold is deliberately five: it permits a small number of legitimate retries while interrupting a repeated repair loop early.
|
|
20
|
+
|
|
21
|
+
## Event
|
|
22
|
+
|
|
23
|
+
- **PostToolUse** (all tools, so non-Bash calls can reset the streak)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hook-spiral-detector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Interrupts Claude Code sessions after repeated identical command failures",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/hook.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"author": "Hasna",
|
|
11
|
+
"license": "Apache-2.0"
|
|
12
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const homes: string[] = [];
|
|
7
|
+
const hookPath = join(import.meta.dir, "hook.ts");
|
|
8
|
+
|
|
9
|
+
async function invoke(home: string, input: object): Promise<Record<string, unknown>> {
|
|
10
|
+
const proc = Bun.spawn(["bun", "run", hookPath], {
|
|
11
|
+
stdin: new Response(JSON.stringify(input)),
|
|
12
|
+
stdout: "pipe",
|
|
13
|
+
stderr: "pipe",
|
|
14
|
+
env: { ...process.env, HOME: home },
|
|
15
|
+
});
|
|
16
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
17
|
+
expect(exitCode).toBe(0);
|
|
18
|
+
return JSON.parse(stdout);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function input(session: string, command = "bun test", exitCode = 1): object {
|
|
22
|
+
return {
|
|
23
|
+
session_id: session,
|
|
24
|
+
tool_name: "Bash",
|
|
25
|
+
tool_input: { command },
|
|
26
|
+
tool_response: { exit_code: exitCode, stderr: "tests failed\nmore details" },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("spiral detector", () => {
|
|
35
|
+
test("interrupts on the fifth identical red signature", async () => {
|
|
36
|
+
const home = mkdtempSync(join(tmpdir(), "spiral-detector-"));
|
|
37
|
+
homes.push(home);
|
|
38
|
+
for (let attempt = 1; attempt < 5; attempt++) {
|
|
39
|
+
expect(await invoke(home, input("session-1"))).toEqual({ continue: true });
|
|
40
|
+
}
|
|
41
|
+
expect(await invoke(home, input("session-1"))).toMatchObject({
|
|
42
|
+
continue: false,
|
|
43
|
+
stopReason: expect.stringContaining("after 5 identical command failures"),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("successes and changed failures reset the streak", async () => {
|
|
48
|
+
const home = mkdtempSync(join(tmpdir(), "spiral-detector-"));
|
|
49
|
+
homes.push(home);
|
|
50
|
+
for (let attempt = 0; attempt < 4; attempt++) await invoke(home, input("session-2"));
|
|
51
|
+
expect(await invoke(home, input("session-2", "bun test", 0))).toEqual({ continue: true });
|
|
52
|
+
for (let attempt = 0; attempt < 4; attempt++) await invoke(home, input("session-2"));
|
|
53
|
+
expect(await invoke(home, input("session-2", "bun run typecheck"))).toEqual({ continue: true });
|
|
54
|
+
for (let attempt = 1; attempt < 5; attempt++) {
|
|
55
|
+
expect(await invoke(home, input("session-2"))).toEqual({ continue: true });
|
|
56
|
+
}
|
|
57
|
+
expect(await invoke(home, input("session-2"))).toMatchObject({ continue: false });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("supports the repository's legacy tool_output field", async () => {
|
|
61
|
+
const home = mkdtempSync(join(tmpdir(), "spiral-detector-"));
|
|
62
|
+
homes.push(home);
|
|
63
|
+
const legacy = {
|
|
64
|
+
session_id: "session-3",
|
|
65
|
+
tool_name: "Bash",
|
|
66
|
+
tool_input: { command: "npm test" },
|
|
67
|
+
tool_output: { exitCode: "2", stderr: "same error" },
|
|
68
|
+
};
|
|
69
|
+
for (let attempt = 1; attempt < 5; attempt++) await invoke(home, legacy);
|
|
70
|
+
expect(await invoke(home, legacy)).toMatchObject({ continue: false });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/** Interrupt a session after five consecutive identical Bash failures. */
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
interface HookInput {
|
|
11
|
+
session_id?: string;
|
|
12
|
+
tool_name?: string;
|
|
13
|
+
tool_input?: Record<string, unknown>;
|
|
14
|
+
tool_response?: unknown;
|
|
15
|
+
tool_output?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface State {
|
|
19
|
+
signature: string;
|
|
20
|
+
count: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const THRESHOLD = 5;
|
|
24
|
+
const STATE_DIR = join(homedir(), ".hasna", "hooks", "state", "spiral-detector");
|
|
25
|
+
|
|
26
|
+
function hash(value: string): string {
|
|
27
|
+
return createHash("sha256").update(value).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function statePath(sessionId: string): string {
|
|
31
|
+
return join(STATE_DIR, `${hash(sessionId)}.json`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readState(path: string): State {
|
|
35
|
+
try {
|
|
36
|
+
const state = JSON.parse(readFileSync(path, "utf8")) as State;
|
|
37
|
+
if (typeof state.signature === "string" && Number.isInteger(state.count) && state.count > 0) return state;
|
|
38
|
+
} catch {}
|
|
39
|
+
return { signature: "", count: 0 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearState(path: string): void {
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(path)) unlinkSync(path);
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function redSignature(input: HookInput): string | null {
|
|
49
|
+
if (input.tool_name !== "Bash" || typeof input.tool_input?.command !== "string") return null;
|
|
50
|
+
const output = input.tool_response ?? input.tool_output;
|
|
51
|
+
if (!output || typeof output !== "object") return null;
|
|
52
|
+
const record = output as Record<string, unknown>;
|
|
53
|
+
const rawCode = record.exit_code ?? record.exitCode ?? record.code;
|
|
54
|
+
const exitCode = typeof rawCode === "string" ? Number(rawCode) : rawCode;
|
|
55
|
+
if (typeof exitCode !== "number" || !Number.isFinite(exitCode) || exitCode === 0) return null;
|
|
56
|
+
const stderr = typeof record.stderr === "string" ? record.stderr : "";
|
|
57
|
+
const firstLine = stderr.split(/\r?\n/, 1)[0] ?? "";
|
|
58
|
+
return hash(`${hash(input.tool_input.command)}\0${exitCode}\0${firstLine}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function processInput(input: HookInput): { continue: boolean; stopReason?: string } {
|
|
62
|
+
if (!input.session_id) return { continue: true };
|
|
63
|
+
const path = statePath(input.session_id);
|
|
64
|
+
const signature = redSignature(input);
|
|
65
|
+
if (!signature) {
|
|
66
|
+
clearState(path);
|
|
67
|
+
return { continue: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const previous = readState(path);
|
|
71
|
+
const count = previous.signature === signature ? previous.count + 1 : 1;
|
|
72
|
+
try {
|
|
73
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
74
|
+
writeFileSync(path, JSON.stringify({ signature, count }));
|
|
75
|
+
} catch {
|
|
76
|
+
return { continue: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return count >= THRESHOLD
|
|
80
|
+
? { continue: false, stopReason: `Spiral detector interrupted the session after ${count} identical command failures. Change the command or underlying state before resuming.` }
|
|
81
|
+
: { continue: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function run(): void {
|
|
85
|
+
try {
|
|
86
|
+
const input = JSON.parse(readFileSync(0, "utf8")) as HookInput;
|
|
87
|
+
console.log(JSON.stringify(processInput(input)));
|
|
88
|
+
} catch {
|
|
89
|
+
console.log(JSON.stringify({ continue: true }));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (import.meta.main) run();
|
|
@@ -35,9 +35,10 @@ it sits under `repos/`; parenthesized command groups
|
|
|
35
35
|
|
|
36
36
|
## Configuration
|
|
37
37
|
|
|
38
|
-
Allowed orgs default to `hasna,hasnaxyz,hasna-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Allowed orgs default to `hasna,hasnaxyz,hasna-products` and are overridable
|
|
39
|
+
with the `WORKSPACE_REPOS_GUARD_ORGS` env var (comma-separated). Private
|
|
40
|
+
workspace orgs must be added per-install via that var; they are never part
|
|
41
|
+
of the public default. The home directory is resolved with `os.homedir()` —
|
|
41
42
|
never hardcoded.
|
|
42
43
|
|
|
43
44
|
## Failure mode
|
|
@@ -15,8 +15,8 @@ function isBlocked(result: { output: { decision?: string; reason?: string } }):
|
|
|
15
15
|
return null;
|
|
16
16
|
}describe("hook-workspace-repos-guard", () => {
|
|
17
17
|
describe("resolveAllowedOrgs", () => {
|
|
18
|
-
test("defaults to the
|
|
19
|
-
expect([...resolveAllowedOrgs({})].sort()).toEqual(["hasna", "hasna-
|
|
18
|
+
test("defaults to the three public canonical GitHub orgs", () => {
|
|
19
|
+
expect([...resolveAllowedOrgs({})].sort()).toEqual(["hasna", "hasna-products", "hasnaxyz"]);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test("parses comma-separated env override, trimming whitespace and empties", () => {
|
|
@@ -31,9 +31,9 @@ function isBlocked(result: { output: { decision?: string; reason?: string } }):
|
|
|
31
31
|
expect(isBlocked(result)).toBeNull();
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test("Edit inside
|
|
35
|
-
const result = evaluate(preToolUse("Edit", { file_path: join(REPOS, "
|
|
36
|
-
expect(result.
|
|
34
|
+
test("Edit inside a non-default org is blocked", () => {
|
|
35
|
+
const result = evaluate(preToolUse("Edit", { file_path: join(REPOS, "notanorg", "platform", "docs", "readme.md") }));
|
|
36
|
+
expect(isBlocked(result)).not.toBeNull();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test("MultiEdit with relative file_path resolves against cwd and continues inside allowed org", () => {
|
|
@@ -65,7 +65,7 @@ function isBlocked(result: { output: { decision?: string; reason?: string } }):
|
|
|
65
65
|
`echo note > ${join(REPOS, "hasna", "apps", "scratch.md")}`,
|
|
66
66
|
`touch ${join(REPOS, "hasna", "apps", "apps", "foo", "x.txt")}`,
|
|
67
67
|
`git clone https://github.com/hasna/foo.git ${join(REPOS, "hasna", "foo")}`,
|
|
68
|
-
`mkdir -p ${join(REPOS, "
|
|
68
|
+
`mkdir -p ${join(REPOS, "hasnaxyz", "iapp-probe", "docs")}`,
|
|
69
69
|
];
|
|
70
70
|
for (const command of commands) {
|
|
71
71
|
const result = evaluate(preToolUse("Bash", { command }));
|
|
@@ -77,7 +77,7 @@ function isBlocked(result: { output: { decision?: string; reason?: string } }):
|
|
|
77
77
|
const commands = [
|
|
78
78
|
`touch ~/workspace/repos/hasna/apps/foo.md`,
|
|
79
79
|
`echo x > ~/workspace/repos/hasnaxyz/iapp-probe/notes.md`,
|
|
80
|
-
`mkdir -p "${HOME}/workspace/repos/
|
|
80
|
+
`mkdir -p "${HOME}/workspace/repos/hasnaxyz/iapp-probe/notes"`,
|
|
81
81
|
`touch "$HOME"/workspace/repos/hasna/apps/x.txt`,
|
|
82
82
|
`echo x > "\${HOME}"/workspace/repos/hasnaxyz/iapp-probe/notes.md`,
|
|
83
83
|
];
|
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
* tools are inspected through their `*** Add File:` / `*** Update File:` /
|
|
31
31
|
* `*** Delete File:` markers. Parenthesized command groups are unwrapped.
|
|
32
32
|
*
|
|
33
|
-
* Allowed orgs default to hasna,hasnaxyz,hasna-
|
|
34
|
-
*
|
|
33
|
+
* Allowed orgs default to hasna,hasnaxyz,hasna-products and are overridable
|
|
34
|
+
* with the WORKSPACE_REPOS_GUARD_ORGS env var (comma-separated). Private
|
|
35
|
+
* workspace orgs must be added per-install via that var; they are never part
|
|
36
|
+
* of the public default.
|
|
35
37
|
* Home is resolved with os.homedir(); never hardcoded. Fail-open on any parse
|
|
36
38
|
* or evaluation error so a guard defect cannot wedge the agent.
|
|
37
39
|
*/
|
|
@@ -51,7 +53,7 @@ const FILE_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"])
|
|
|
51
53
|
|
|
52
54
|
const PATCH_TOOLS = new Set(["apply_patch", "ApplyPatch", "functions.apply_patch"]);
|
|
53
55
|
|
|
54
|
-
const DEFAULT_ORGS = ["hasna", "hasnaxyz", "hasna-
|
|
56
|
+
const DEFAULT_ORGS = ["hasna", "hasnaxyz", "hasna-products"];
|
|
55
57
|
|
|
56
58
|
const RULE = "workspace-repos-guard";
|
|
57
59
|
|
|
@@ -721,7 +721,7 @@ def render_section(t):
|
|
|
721
721
|
if proj.get("path"):
|
|
722
722
|
L.append(f" {'':<11} path {sanitize(proj.get('path'), 120)}")
|
|
723
723
|
if isinstance(dups, int) and dups > 1:
|
|
724
|
-
# Real hazard, not trivia: `
|
|
724
|
+
# Real hazard, not trivia: `todos` exists as FIVE project rows
|
|
725
725
|
# with task counters 3/50/58/101/150 across three machine_ids, and
|
|
726
726
|
# the BUG rows are spread over all of them. An agent that files
|
|
727
727
|
# against the wrong row files into a fossil nobody reads.
|
|
@@ -732,7 +732,7 @@ def render_section(t):
|
|
|
732
732
|
|
|
733
733
|
# --- last 3 bugs -------------------------------------------------------
|
|
734
734
|
# Merged across every matching project row, because BUG rows are genuinely
|
|
735
|
-
# spread across the duplicates:
|
|
735
|
+
# spread across the duplicates: todos carries 58 across 5 rows. Scoping
|
|
736
736
|
# to the primary alone would hide real bugs. The scope is stated on the line
|
|
737
737
|
# so the count is never read as belonging to the single project above.
|
|
738
738
|
bugs = t.get("bugs") or []
|
|
@@ -340,7 +340,7 @@ def candidate_names(org, word):
|
|
|
340
340
|
def warm_slow(org, word, tmpdir, projects_cache):
|
|
341
341
|
"""Resolve the todos project for a repo and collect its last 3 BUG rows.
|
|
342
342
|
|
|
343
|
-
Duplicate project rows are the norm, not the exception: `
|
|
343
|
+
Duplicate project rows are the norm, not the exception: `todos` exists
|
|
344
344
|
FOUR times, with 3 / 101 / 58 / 150 task counters on three different
|
|
345
345
|
machine_ids, and the BUG rows are spread across all four (2+7+14+34 = 57).
|
|
346
346
|
Picking by name alone would silently return a 3-task fossil. So the primary
|
package/hooks/pre-bash/README.md
CHANGED
|
@@ -81,7 +81,7 @@ remote relative path cannot be resolved against the local working directory.
|
|
|
81
81
|
|
|
82
82
|
## Install for Codewith
|
|
83
83
|
|
|
84
|
-
Prefer renderer-managed configuration through
|
|
84
|
+
Prefer renderer-managed configuration through configs. @hasna/hooks can emit the TOML fragment:
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
hooks install pre-bash --target codewith
|
|
@@ -6,7 +6,7 @@ This hook is OSS-safe: optional Hasna CLIs are best-effort and missing CLIs fail
|
|
|
6
6
|
|
|
7
7
|
## Install for Codewith
|
|
8
8
|
|
|
9
|
-
Prefer renderer-managed configuration through
|
|
9
|
+
Prefer renderer-managed configuration through configs. @hasna/hooks can emit the TOML fragment:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
hooks install prompt-guard --target codewith
|
|
@@ -6,7 +6,7 @@ This hook is OSS-safe: optional Hasna CLIs are best-effort and missing CLIs fail
|
|
|
6
6
|
|
|
7
7
|
## Install for Codewith
|
|
8
8
|
|
|
9
|
-
Prefer renderer-managed configuration through
|
|
9
|
+
Prefer renderer-managed configuration through configs. @hasna/hooks can emit the TOML fragment:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
hooks install session-start --target codewith
|
|
@@ -6,7 +6,7 @@ This hook is OSS-safe: optional Hasna CLIs are best-effort and missing CLIs fail
|
|
|
6
6
|
|
|
7
7
|
## Install for Codewith
|
|
8
8
|
|
|
9
|
-
Prefer renderer-managed configuration through
|
|
9
|
+
Prefer renderer-managed configuration through configs. @hasna/hooks can emit the TOML fragment:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
hooks install stop-sync --target codewith
|
|
@@ -61,7 +61,7 @@ from a guardrail by a cooperating agent, not a way to reach a shared checkout.
|
|
|
61
61
|
|
|
62
62
|
## Install for Codewith
|
|
63
63
|
|
|
64
|
-
Prefer renderer-managed configuration through
|
|
64
|
+
Prefer renderer-managed configuration through configs. @hasna/hooks can emit the TOML fragment:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
hooks install worktree-guard --target codewith
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.2",
|
|
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": {
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"dashboard:dev": "cd dashboard && bun run dev",
|
|
31
31
|
"dashboard:build": "cd dashboard && bun run build",
|
|
32
32
|
"dashboard:preview": "cd dashboard && bun run preview",
|
|
33
|
-
"postinstall": "mkdir -p $HOME/.hasna/hooks/profiles 2>/dev/null || true"
|
|
33
|
+
"postinstall": "mkdir -p $HOME/.hasna/hooks/profiles 2>/dev/null || true",
|
|
34
|
+
"scan:artifact": "bun scripts/artifact-scan.ts",
|
|
35
|
+
"prepack": "bun run build && bun run scan:artifact"
|
|
34
36
|
},
|
|
35
37
|
"keywords": [
|
|
36
38
|
"claude-code",
|
|
@@ -49,11 +51,12 @@
|
|
|
49
51
|
"@types/pg": "^8.15.6",
|
|
50
52
|
"@types/react": "^18.2.0",
|
|
51
53
|
"typescript": "^5",
|
|
52
|
-
"bun-types": "1.3.14"
|
|
54
|
+
"bun-types": "1.3.14",
|
|
55
|
+
"@hasna/contracts": "0.13.3"
|
|
53
56
|
},
|
|
54
57
|
"dependencies": {
|
|
55
|
-
"@hasna/events": "^0.1.
|
|
56
|
-
"@hasna/secrets": "0.3.
|
|
58
|
+
"@hasna/events": "^0.1.16",
|
|
59
|
+
"@hasna/secrets": "0.3.4",
|
|
57
60
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
58
61
|
"chalk": "^5.3.0",
|
|
59
62
|
"commander": "^12.1.0",
|
|
@@ -64,7 +67,8 @@
|
|
|
64
67
|
"ink-text-input": "^6.0.0",
|
|
65
68
|
"pg": "^8.16.3",
|
|
66
69
|
"react": "^18.2.0",
|
|
67
|
-
"zod": "3.25.76"
|
|
70
|
+
"zod": "3.25.76",
|
|
71
|
+
"@hasna/contracts": "0.13.3"
|
|
68
72
|
},
|
|
69
73
|
"engines": {
|
|
70
74
|
"bun": ">=1.0.0"
|