@hasna/hooks 0.2.17 → 0.2.19

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/bin/index.js CHANGED
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
861
861
  this._exitCallback = (err) => {
862
862
  if (err.code !== "commander.executeSubCommandAsync") {
863
863
  throw err;
864
- }
864
+ } else {}
865
865
  };
866
866
  }
867
867
  return this;
@@ -13884,8 +13884,8 @@ function getDb() {
13884
13884
  instance = new SqliteAdapter(dbPath);
13885
13885
  instance.exec("PRAGMA journal_mode=WAL");
13886
13886
  instance.exec("PRAGMA foreign_keys=ON");
13887
- runMigrations(instance);
13888
- runRetention(instance);
13887
+ runMigrations(instance.raw);
13888
+ runRetention(instance.raw);
13889
13889
  instance.exec(`CREATE TABLE IF NOT EXISTS feedback (
13890
13890
  id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
13891
13891
  message TEXT NOT NULL,
@@ -13896,7 +13896,7 @@ function getDb() {
13896
13896
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
13897
13897
  )`);
13898
13898
  if (isNew) {
13899
- runLegacyImport(instance);
13899
+ runLegacyImport(instance.raw);
13900
13900
  }
13901
13901
  return instance;
13902
13902
  }
@@ -14745,7 +14745,6 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
14745
14745
  function getEventsDataDir(override) {
14746
14746
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
14747
14747
  }
14748
-
14749
14748
  class JsonEventsStore {
14750
14749
  dataDir;
14751
14750
  channelsPath;
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/hook.ts
5
+ import { readFileSync, existsSync } from "fs";
6
+ import { join, dirname, basename, extname } from "path";
7
+ import { execSync } from "child_process";
8
+ function readStdinJson() {
9
+ try {
10
+ const input = readFileSync(0, "utf-8").trim();
11
+ if (!input)
12
+ return null;
13
+ return JSON.parse(input);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+ function respond(output) {
19
+ console.log(JSON.stringify(output));
20
+ }
21
+ function isTestFile(filePath) {
22
+ return /\.(test|spec)\.[jt]sx?$/.test(filePath) || /_test\.(py|go|rs)$/.test(filePath) || /test_.*\.py$/.test(filePath);
23
+ }
24
+ function findTestFiles(filePath, cwd) {
25
+ if (isTestFile(filePath))
26
+ return [];
27
+ const ext = extname(filePath);
28
+ const base = basename(filePath, ext);
29
+ const dir = dirname(filePath);
30
+ const candidates = [];
31
+ const testExts = [`.test${ext}`, `.spec${ext}`, `.test.ts`, `.spec.ts`, `.test.tsx`, `.spec.tsx`];
32
+ for (const testExt of testExts) {
33
+ candidates.push(join(dir, `${base}${testExt}`));
34
+ }
35
+ candidates.push(join(dir, "__tests__", `${base}.test${ext}`));
36
+ candidates.push(join(dir, "__tests__", `${base}.spec${ext}`));
37
+ for (const testDir of ["test", "tests", "__tests__"]) {
38
+ const relPath = filePath.replace(/^.*?\/(src|lib|app)\//, "");
39
+ candidates.push(join(cwd, testDir, relPath.replace(ext, `.test${ext}`)));
40
+ candidates.push(join(cwd, testDir, relPath.replace(ext, `.spec${ext}`)));
41
+ }
42
+ return candidates.filter((p) => existsSync(p));
43
+ }
44
+ function detectTestCommand(cwd) {
45
+ const pkgPath = join(cwd, "package.json");
46
+ if (existsSync(pkgPath)) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
49
+ const scripts = pkg.scripts || {};
50
+ if (scripts.test)
51
+ return "bun test";
52
+ } catch {}
53
+ }
54
+ return "bun test";
55
+ }
56
+ function runTests(cwd, testFiles) {
57
+ const cmd = detectTestCommand(cwd);
58
+ const fileArgs = testFiles.map((f) => `"${f}"`).join(" ");
59
+ const fullCmd = `${cmd} ${fileArgs}`;
60
+ console.error(`[hook-affected-tests] Running: ${fullCmd}`);
61
+ try {
62
+ const output = execSync(fullCmd, {
63
+ cwd,
64
+ encoding: "utf-8",
65
+ stdio: ["pipe", "pipe", "pipe"],
66
+ timeout: 60000
67
+ });
68
+ const lines = output.trim().split(`
69
+ `);
70
+ const summary = lines[lines.length - 1] || "Tests passed";
71
+ console.error(`[hook-affected-tests] ${summary}`);
72
+ } catch (error) {
73
+ const execError = error;
74
+ const output = (execError.stdout || "") + (execError.stderr || "");
75
+ const lines = output.trim().split(`
76
+ `).filter((l) => l.trim());
77
+ const summary = lines.slice(-3).join(" | ");
78
+ console.error(`[hook-affected-tests] Tests failed: ${summary}`);
79
+ }
80
+ }
81
+ function run() {
82
+ const input = readStdinJson();
83
+ if (!input) {
84
+ respond({ continue: true });
85
+ return;
86
+ }
87
+ const filePath = input.tool_input.file_path || input.tool_input.notebook_path;
88
+ if (!filePath) {
89
+ respond({ continue: true });
90
+ return;
91
+ }
92
+ const testFiles = findTestFiles(filePath, input.cwd);
93
+ if (testFiles.length === 0) {
94
+ respond({ continue: true });
95
+ return;
96
+ }
97
+ console.error(`[hook-affected-tests] Found ${testFiles.length} test file(s) for ${basename(filePath)}`);
98
+ runTests(input.cwd, testFiles);
99
+ respond({ continue: true });
100
+ }
101
+ if (__require.main == __require.module) {
102
+ run();
103
+ }
104
+ export {
105
+ run
106
+ };
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/hook.ts
5
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
6
+ import { join } from "path";
7
+ import { tmpdir } from "os";
8
+ import { execSync } from "child_process";
9
+ var STATE_DIR = join(tmpdir(), "hook-announce-start");
10
+ function readStdinJson() {
11
+ try {
12
+ const input = readFileSync(0, "utf-8").trim();
13
+ if (!input)
14
+ return null;
15
+ return JSON.parse(input);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function respond(output) {
21
+ console.log(JSON.stringify(output));
22
+ }
23
+ function sanitizeId(id) {
24
+ return id.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 64);
25
+ }
26
+ function hasAlreadyAnnounced(sessionId) {
27
+ const safe = sanitizeId(sessionId);
28
+ return existsSync(join(STATE_DIR, `${safe}.announced`));
29
+ }
30
+ function markAnnounced(sessionId) {
31
+ mkdirSync(STATE_DIR, { recursive: true });
32
+ const safe = sanitizeId(sessionId);
33
+ writeFileSync(join(STATE_DIR, `${safe}.announced`), new Date().toISOString());
34
+ }
35
+ function registerAgentProfile() {
36
+ try {
37
+ execSync("hooks init", {
38
+ encoding: "utf-8",
39
+ timeout: 5000,
40
+ stdio: ["pipe", "pipe", "pipe"]
41
+ });
42
+ } catch {}
43
+ }
44
+ function fetchContext() {
45
+ try {
46
+ const output = execSync("conversations context", {
47
+ encoding: "utf-8",
48
+ timeout: 1e4,
49
+ stdio: ["pipe", "pipe", "pipe"]
50
+ });
51
+ return output.trim() || null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+ function announceToSpace(cwd, sessionId) {
57
+ const project = cwd.split("/").filter(Boolean).pop() || "project";
58
+ const agent = process.env.HOOKS_AGENT_NAME || `session:${sessionId.slice(0, 8)}`;
59
+ const space = process.env.HOOKS_SPACE || "general";
60
+ const message = `Agent **${agent}** started a session on **${project}**`;
61
+ try {
62
+ execSync(`conversations send "${message}" --space "${space}"`, {
63
+ encoding: "utf-8",
64
+ timeout: 1e4,
65
+ stdio: ["pipe", "pipe", "pipe"]
66
+ });
67
+ console.error(`[hook-announce-start] Announced to space '${space}'`);
68
+ } catch {
69
+ console.error(`[hook-announce-start] Could not post to space`);
70
+ }
71
+ }
72
+ function run() {
73
+ const input = readStdinJson();
74
+ if (!input) {
75
+ respond({ continue: true });
76
+ return;
77
+ }
78
+ if (hasAlreadyAnnounced(input.session_id)) {
79
+ respond({ continue: true });
80
+ return;
81
+ }
82
+ markAnnounced(input.session_id);
83
+ registerAgentProfile();
84
+ const context = fetchContext();
85
+ if (context) {
86
+ process.stderr.write(`[hook-announce-start] Session context:
87
+ ${context}
88
+ `);
89
+ }
90
+ announceToSpace(input.cwd, input.session_id);
91
+ respond({ continue: true });
92
+ }
93
+ if (__require.main == __require.module) {
94
+ run();
95
+ }
96
+ export {
97
+ run
98
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
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": {
@@ -18,7 +18,7 @@
18
18
  "build": "bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf --external @modelcontextprotocol/sdk --external zod && bun build ./src/index.ts --outdir ./dist --target bun",
19
19
  "dev": "bun run ./src/cli/index.tsx",
20
20
  "test": "bun test",
21
- "typecheck": "tsc --noEmit",
21
+ "typecheck": "bunx tsc --noEmit",
22
22
  "prepublishOnly": "bun run build",
23
23
  "dashboard:dev": "cd dashboard && bun run dev",
24
24
  "dashboard:build": "cd dashboard && bun run build",