@hasna/hooks 0.2.19 → 0.2.20
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 +5 -8
- package/bin/index.js +249 -9629
- package/dist/cli/components/App.d.ts +7 -0
- package/dist/cli/components/CategorySelect.d.ts +7 -0
- package/dist/cli/components/DataTable.d.ts +11 -0
- package/dist/cli/components/Header.d.ts +7 -0
- package/dist/cli/components/HookSelect.d.ts +11 -0
- package/dist/cli/components/InstallProgress.d.ts +9 -0
- package/dist/cli/components/SearchView.d.ts +9 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/db/index.d.ts +11 -0
- package/dist/db/legacy-import.d.ts +11 -0
- package/dist/db/migrations/001_initial.d.ts +6 -0
- package/dist/db/migrations/index.d.ts +7 -0
- package/dist/db/pg-migrations.d.ts +7 -0
- package/dist/db/retention.d.ts +8 -0
- package/dist/db/schema.d.ts +21 -0
- package/dist/index.d.ts +59 -0
- package/dist/lib/db-writer.d.ts +9 -0
- package/dist/lib/installer.d.ts +36 -0
- package/dist/lib/profiles.d.ts +33 -0
- package/dist/lib/registry.d.ts +19 -0
- package/dist/mcp/http.d.ts +16 -0
- package/dist/mcp/server.d.ts +17 -0
- package/package.json +2 -3
- package/hooks/hook-affected-tests/dist/hook.js +0 -106
- package/hooks/hook-announce-start/dist/hook.js +0 -98
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { HookMeta } from "../../lib/registry.js";
|
|
3
|
+
interface DataTableProps {
|
|
4
|
+
hooks: HookMeta[];
|
|
5
|
+
selected: Set<string>;
|
|
6
|
+
onToggle: (name: string) => void;
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
onBack: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function DataTable({ hooks, selected, onToggle, onConfirm, onBack, }: DataTableProps): React.JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { HookMeta } from "../../lib/registry.js";
|
|
3
|
+
interface HookSelectProps {
|
|
4
|
+
hooks: HookMeta[];
|
|
5
|
+
selected: Set<string>;
|
|
6
|
+
onToggle: (name: string) => void;
|
|
7
|
+
onConfirm: () => void;
|
|
8
|
+
onBack: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function HookSelect({ hooks, selected, onToggle, onConfirm, onBack, }: HookSelectProps): React.JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { InstallResult } from "../../lib/installer.js";
|
|
3
|
+
interface InstallProgressProps {
|
|
4
|
+
hooks: string[];
|
|
5
|
+
overwrite?: boolean;
|
|
6
|
+
onComplete: (results: InstallResult[]) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function InstallProgress({ hooks, overwrite, onComplete, }: InstallProgressProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface SearchViewProps {
|
|
3
|
+
selected: Set<string>;
|
|
4
|
+
onToggle: (name: string) => void;
|
|
5
|
+
onConfirm: () => void;
|
|
6
|
+
onBack: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function SearchView({ selected, onToggle, onConfirm, onBack, }: SearchViewProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite DB module for hooks — persistent storage at ~/.hasna/hooks/hooks.db
|
|
3
|
+
*
|
|
4
|
+
* Uses bun:sqlite with WAL mode for concurrent reads.
|
|
5
|
+
* Supports HASNA_HOOKS_DATA_DIR / HOOKS_DATA_DIR and HASNA_HOOKS_DB_PATH / HOOKS_DB_PATH env overrides.
|
|
6
|
+
*/
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
8
|
+
export declare function getDbPath(): string;
|
|
9
|
+
export declare function getDb(): Database;
|
|
10
|
+
export declare function closeDb(): void;
|
|
11
|
+
export declare function createTestDb(): Database;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy flat-file import — runs once on first DB creation.
|
|
3
|
+
*
|
|
4
|
+
* Scans for old .claude/session-log-*.jsonl and .claude/errors.log files
|
|
5
|
+
* in the user's home projects directory and imports their entries into hook_events.
|
|
6
|
+
*
|
|
7
|
+
* Non-blocking: any failure is logged to stderr and skipped.
|
|
8
|
+
* Tracks completion via a `_meta` table row keyed "legacy_import_done".
|
|
9
|
+
*/
|
|
10
|
+
import type { Database } from "bun:sqlite";
|
|
11
|
+
export declare function runLegacyImport(db: Database): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration runner — applies pending migrations in order.
|
|
3
|
+
* Tracks applied migrations in a `schema_migrations` table.
|
|
4
|
+
* Migrations are additive-only, never destructive.
|
|
5
|
+
*/
|
|
6
|
+
import type { Database } from "bun:sqlite";
|
|
7
|
+
export declare function runMigrations(db: Database): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention/cleanup — auto-prune hook_events older than N days.
|
|
3
|
+
*
|
|
4
|
+
* Configurable via HOOKS_RETENTION_DAYS env var (default: 30).
|
|
5
|
+
* Called on DB open after migrations.
|
|
6
|
+
*/
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
export declare function runRetention(db: Database, days?: number): number;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook_events table schema and DDL
|
|
3
|
+
*/
|
|
4
|
+
import type { Database } from "bun:sqlite";
|
|
5
|
+
export declare const CREATE_HOOK_EVENTS_TABLE = "\n CREATE TABLE IF NOT EXISTS hook_events (\n id TEXT PRIMARY KEY,\n timestamp TEXT NOT NULL,\n session_id TEXT NOT NULL,\n hook_name TEXT NOT NULL,\n event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification')),\n tool_name TEXT,\n tool_input TEXT,\n result TEXT CHECK (result IN ('continue', 'block', NULL)),\n error TEXT,\n duration_ms INTEGER,\n project_dir TEXT,\n metadata TEXT\n )\n";
|
|
6
|
+
export declare const CREATE_INDEXES: string[];
|
|
7
|
+
export interface HookEventRow {
|
|
8
|
+
id: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
session_id: string;
|
|
11
|
+
hook_name: string;
|
|
12
|
+
event_type: "PreToolUse" | "PostToolUse" | "Stop" | "Notification";
|
|
13
|
+
tool_name: string | null;
|
|
14
|
+
tool_input: string | null;
|
|
15
|
+
result: "continue" | "block" | null;
|
|
16
|
+
error: string | null;
|
|
17
|
+
duration_ms: number | null;
|
|
18
|
+
project_dir: string | null;
|
|
19
|
+
metadata: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function applySchema(db: Database): void;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hasna/hooks - Open source Claude Code hooks library
|
|
3
|
+
*
|
|
4
|
+
* Install hooks with a single command:
|
|
5
|
+
* npx @hasna/hooks install gitguard branchprotect
|
|
6
|
+
*
|
|
7
|
+
* Or use the interactive CLI:
|
|
8
|
+
* npx @hasna/hooks
|
|
9
|
+
*/
|
|
10
|
+
export { HOOKS, CATEGORIES, getHook, getHooksByCategory, searchHooks, type HookMeta, type Category, } from "./lib/registry.js";
|
|
11
|
+
export { installHook, installHooks, getInstalledHooks, getRegisteredHooks, getRegisteredHooksForTarget, removeHook, hookExists, getHookPath, getSettingsPath, type InstallResult, type InstallOptions, type Scope, type Target, } from "./lib/installer.js";
|
|
12
|
+
export interface HookAgentInfo {
|
|
13
|
+
agent_id: string;
|
|
14
|
+
agent_type: "claude" | "gemini" | "custom";
|
|
15
|
+
name?: string;
|
|
16
|
+
preferences?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
/** The JSON object passed to a hook via stdin */
|
|
19
|
+
export interface HookInput {
|
|
20
|
+
session_id?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
tool_name?: string;
|
|
23
|
+
tool_input?: Record<string, unknown>;
|
|
24
|
+
agent?: HookAgentInfo;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
/** The JSON object a PreToolUse hook returns via stdout */
|
|
28
|
+
export interface HookOutput {
|
|
29
|
+
decision?: "approve" | "block";
|
|
30
|
+
reason?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
import type { InstallOptions, InstallResult } from "./lib/installer.js";
|
|
34
|
+
/** Install a hook scoped to the current project (.claude/settings.json) */
|
|
35
|
+
export declare function installHookForProject(name: string, options?: Omit<InstallOptions, "scope">): InstallResult;
|
|
36
|
+
/** Install multiple hooks scoped to the current project */
|
|
37
|
+
export declare function installHooksForProject(names: string[], options?: Omit<InstallOptions, "scope">): InstallResult[];
|
|
38
|
+
/** List all hooks registered for the current project */
|
|
39
|
+
export declare function listProjectHooks(): string[];
|
|
40
|
+
/** Remove a hook from the current project */
|
|
41
|
+
export declare function removeProjectHook(name: string): boolean;
|
|
42
|
+
export interface RunHookOptions {
|
|
43
|
+
/** Agent profile ID to inject into hook input */
|
|
44
|
+
profile?: string;
|
|
45
|
+
/** Timeout in milliseconds (default: 10000) */
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface RunHookResult {
|
|
49
|
+
output: HookOutput;
|
|
50
|
+
stderr: string;
|
|
51
|
+
exitCode: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Programmatically execute a hook with the given input.
|
|
55
|
+
* Spawns the hook's src/hook.ts via bun, passes input as stdin JSON,
|
|
56
|
+
* and returns the parsed stdout JSON.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runHook(name: string, input: HookInput, options?: RunHookOptions): Promise<RunHookResult>;
|
|
59
|
+
export { createProfile, getProfile, listProfiles, updateProfile, deleteProfile, touchProfile, getProfilesDir, exportProfiles, importProfiles, type AgentProfile, type CreateProfileInput, } from "./lib/profiles.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared hook DB writer — single write path for all observability hooks.
|
|
3
|
+
* Never throws: errors are written to stderr only.
|
|
4
|
+
*/
|
|
5
|
+
import type { HookEventRow } from "../db/schema";
|
|
6
|
+
export type HookEventInput = Omit<HookEventRow, "id" | "timestamp"> & {
|
|
7
|
+
timestamp?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function writeHookEvent(event: HookEventInput): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook installer - registers hooks in AI coding agent settings
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Claude Code: ~/.claude/settings.json (PreToolUse, PostToolUse, Stop, Notification)
|
|
6
|
+
* - Gemini CLI: ~/.gemini/settings.json (BeforeTool, AfterTool, AfterAgent, Notification)
|
|
7
|
+
*
|
|
8
|
+
* Hooks run directly from the globally installed @hasna/hooks package.
|
|
9
|
+
* No files are copied. The settings entry points to `hooks run <name>`.
|
|
10
|
+
*/
|
|
11
|
+
export type Scope = "global" | "project";
|
|
12
|
+
export type Target = "claude" | "gemini" | "all";
|
|
13
|
+
export interface InstallResult {
|
|
14
|
+
hook: string;
|
|
15
|
+
success: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
scope?: Scope;
|
|
18
|
+
target?: Target;
|
|
19
|
+
conflict?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface InstallOptions {
|
|
22
|
+
scope?: Scope;
|
|
23
|
+
overwrite?: boolean;
|
|
24
|
+
target?: Target;
|
|
25
|
+
profile?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function getSettingsPath(scope?: Scope, target?: "claude" | "gemini"): string;
|
|
28
|
+
export declare function getHookPath(name: string): string;
|
|
29
|
+
export declare function hookExists(name: string): boolean;
|
|
30
|
+
export declare function installHook(name: string, options?: InstallOptions): InstallResult;
|
|
31
|
+
export declare function installHooks(names: string[], options?: InstallOptions): InstallResult[];
|
|
32
|
+
export declare function getRegisteredHooksForTarget(scope?: Scope, target?: "claude" | "gemini"): string[];
|
|
33
|
+
export declare function getRegisteredHooks(scope?: Scope): string[];
|
|
34
|
+
/** @deprecated Use getRegisteredHooks instead */
|
|
35
|
+
export declare const getInstalledHooks: typeof getRegisteredHooks;
|
|
36
|
+
export declare function removeHook(name: string, scope?: Scope, target?: Target): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent profile management — identity system for hooks
|
|
3
|
+
*
|
|
4
|
+
* Each agent instance gets a unique 8-char UUID stored at ~/.hasna/hooks/profiles/<id>.json.
|
|
5
|
+
* Profiles are injected into HookInput when hooks are run with --profile <id>,
|
|
6
|
+
* allowing hooks to identify which agent is calling them.
|
|
7
|
+
*/
|
|
8
|
+
export interface AgentProfile {
|
|
9
|
+
agent_id: string;
|
|
10
|
+
agent_type: "claude" | "gemini" | "custom";
|
|
11
|
+
name?: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
last_seen_at: string;
|
|
14
|
+
preferences: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface CreateProfileInput {
|
|
17
|
+
agent_type: "claude" | "gemini" | "custom";
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function getProfilesDir(): string;
|
|
21
|
+
export declare function createProfile(input: CreateProfileInput): AgentProfile;
|
|
22
|
+
export declare function getProfile(id: string): AgentProfile | null;
|
|
23
|
+
export declare function listProfiles(): AgentProfile[];
|
|
24
|
+
export declare function updateProfile(id: string, data: Partial<Pick<AgentProfile, "name" | "preferences">>): AgentProfile | null;
|
|
25
|
+
export declare function deleteProfile(id: string): boolean;
|
|
26
|
+
export declare function touchProfile(id: string): void;
|
|
27
|
+
/** Export all profiles as a JSON bundle for cross-machine backup */
|
|
28
|
+
export declare function exportProfiles(): AgentProfile[];
|
|
29
|
+
/** Import profiles from a JSON bundle, skipping duplicates by agent_id */
|
|
30
|
+
export declare function importProfiles(profiles: AgentProfile[]): {
|
|
31
|
+
imported: number;
|
|
32
|
+
skipped: number;
|
|
33
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook registry - metadata about all available hooks
|
|
3
|
+
*/
|
|
4
|
+
export interface HookMeta {
|
|
5
|
+
name: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
description: string;
|
|
8
|
+
version: string;
|
|
9
|
+
category: string;
|
|
10
|
+
event: "PreToolUse" | "PostToolUse" | "Stop" | "Notification";
|
|
11
|
+
matcher: string;
|
|
12
|
+
tags: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare const CATEGORIES: readonly ["Git Safety", "Code Quality", "Security", "Notifications", "Context Management", "Workflow Automation", "Environment", "Permissions", "Observability", "Agent Teams"];
|
|
15
|
+
export type Category = (typeof CATEGORIES)[number];
|
|
16
|
+
export declare const HOOKS: HookMeta[];
|
|
17
|
+
export declare function getHooksByCategory(category: Category): HookMeta[];
|
|
18
|
+
export declare function searchHooks(query: string): HookMeta[];
|
|
19
|
+
export declare function getHook(name: string): HookMeta | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
export declare const DEFAULT_MCP_HTTP_PORT = 8847;
|
|
3
|
+
export declare const MCP_HTTP_HOST = "127.0.0.1";
|
|
4
|
+
export declare const MCP_SERVICE_NAME = "hooks";
|
|
5
|
+
export declare function isHttpMode(args: string[]): boolean;
|
|
6
|
+
export declare function resolveMcpHttpPort(args: string[]): number;
|
|
7
|
+
export declare function healthPayload(name?: string): {
|
|
8
|
+
status: string;
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function handleMcpRequest(req: Request, buildServer: () => McpServer): Promise<Response>;
|
|
12
|
+
export declare function startMcpHttpServer(options: {
|
|
13
|
+
name: string;
|
|
14
|
+
port: number;
|
|
15
|
+
buildServer: () => McpServer;
|
|
16
|
+
}): ReturnType<typeof Bun.serve>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server for @hasna/hooks
|
|
3
|
+
*
|
|
4
|
+
* Exposes hook management as MCP tools for AI agents.
|
|
5
|
+
* Runs on port 39427 (SSE) or stdio transport.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
export declare const MCP_PORT = 39427;
|
|
9
|
+
export declare function createHooksServer(): McpServer;
|
|
10
|
+
/**
|
|
11
|
+
* Start the MCP server with SSE transport on the configured port
|
|
12
|
+
*/
|
|
13
|
+
export declare function startSSEServer(port?: number): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Start the MCP server with stdio transport
|
|
16
|
+
*/
|
|
17
|
+
export declare function startStdioServer(): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
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": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"main": "./dist/index.js",
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"scripts": {
|
|
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",
|
|
18
|
+
"build": "rm -rf ./bin ./dist && 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 && bunx tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
19
19
|
"dev": "bun run ./src/cli/index.tsx",
|
|
20
20
|
"test": "bun test",
|
|
21
21
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"typescript": "^5"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@hasna/cloud": "0.1.24",
|
|
47
46
|
"@hasna/events": "^0.1.6",
|
|
48
47
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
49
48
|
"chalk": "^5.3.0",
|
|
@@ -1,106 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
};
|