@nothumanwork/nn 0.1.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/bin/nn.js +106 -0
  4. package/package.json +74 -0
  5. package/src/config/env.ts +31 -0
  6. package/src/config/paths.ts +50 -0
  7. package/src/config/runtime.ts +37 -0
  8. package/src/config/sync.ts +48 -0
  9. package/src/db/client.ts +333 -0
  10. package/src/db/libsql-native.ts +66 -0
  11. package/src/db/lock.ts +72 -0
  12. package/src/db/migrate.ts +246 -0
  13. package/src/db/replica-migrate.ts +162 -0
  14. package/src/db/schema.sql +99 -0
  15. package/src/export/claude.ts +92 -0
  16. package/src/export/codex.ts +86 -0
  17. package/src/export/cursor.ts +68 -0
  18. package/src/export/generic.ts +19 -0
  19. package/src/export/registry.ts +118 -0
  20. package/src/export/types.ts +44 -0
  21. package/src/hooks/ingest.ts +107 -0
  22. package/src/hooks/resolvers/antigravity.ts +44 -0
  23. package/src/hooks/resolvers/claude.ts +27 -0
  24. package/src/hooks/resolvers/codex.ts +65 -0
  25. package/src/hooks/resolvers/common.ts +21 -0
  26. package/src/hooks/resolvers/cursor.ts +31 -0
  27. package/src/hooks/resolvers/grok.ts +59 -0
  28. package/src/hooks/resolvers/index.ts +35 -0
  29. package/src/hooks/resolvers/pi.ts +72 -0
  30. package/src/hooks/types.ts +20 -0
  31. package/src/index.ts +247 -0
  32. package/src/ingest/jsonl.ts +38 -0
  33. package/src/ingest/pipeline.ts +101 -0
  34. package/src/install/index.ts +227 -0
  35. package/src/install/types.ts +85 -0
  36. package/src/ir/event-id.ts +26 -0
  37. package/src/ir/types.ts +84 -0
  38. package/src/providers/antigravity/index.ts +175 -0
  39. package/src/providers/claude/index.ts +228 -0
  40. package/src/providers/codex/index.ts +264 -0
  41. package/src/providers/copilot/index.ts +24 -0
  42. package/src/providers/cursor/index.ts +340 -0
  43. package/src/providers/grok/index.ts +146 -0
  44. package/src/providers/pi/index.ts +197 -0
  45. package/src/providers/registry.ts +31 -0
  46. package/src/providers/types.ts +53 -0
  47. package/src/sync/coordinator.ts +186 -0
  48. package/src/sync/turso.ts +64 -0
  49. package/src/types/assets.d.ts +4 -0
package/src/index.ts ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import "./db/libsql-native.ts";
4
+ import { resolve } from "node:path";
5
+
6
+ import { doctorDb } from "./db/migrate.ts";
7
+ import { listSessions, searchEvents } from "./db/client.ts";
8
+ import { exportSession, resolveSessionId } from "./export/registry.ts";
9
+ import { ingestIncremental, ingestProvider } from "./ingest/pipeline.ts";
10
+ import { installHooks } from "./install/index.ts";
11
+ import { listProviders } from "./providers/registry.ts";
12
+ import type { ProviderId } from "./ir/types.ts";
13
+ import type { ExportTarget } from "./export/types.ts";
14
+ import { syncPull, syncPush, syncStatus } from "./sync/turso.ts";
15
+ import { runCoordinatorOnce } from "./sync/coordinator.ts";
16
+ import { handleHookInvocation } from "./hooks/ingest.ts";
17
+ import type { HookEventKind, HookPayload } from "./hooks/types.ts";
18
+ import { HOOK_EVENTS } from "./hooks/types.ts";
19
+ import { cursorSessionIdFromPath } from "./providers/cursor/index.ts";
20
+ import { stdin } from "bun";
21
+
22
+ const USAGE = `nn (Neural Net) — multi-provider transcript fabric
23
+
24
+ Usage:
25
+ nn install hooks [--global] [--provider <id>]
26
+ nn hook --provider <id> --event <kind>
27
+ nn ingest --provider <provider> [--session <id>] [--all]
28
+ nn export --session <id> [--to <target>] [--output <file.jsonl>]
29
+ nn search <query> [--limit <n>]
30
+ nn sessions list [--provider <provider>]
31
+ nn sync status|push|pull
32
+ nn doctor
33
+
34
+ Providers: ${listProviders().join(", ")}
35
+ Export targets: generic, cursor, claude, codex, grok, pi, antigravity, copilot
36
+ `;
37
+
38
+ function printHelp(): void {
39
+ console.log(USAGE.trimEnd());
40
+ }
41
+
42
+ function parseFlag(args: string[], flag: string): string | undefined {
43
+ const index = args.indexOf(flag);
44
+ if (index === -1) {
45
+ return undefined;
46
+ }
47
+ return args[index + 1];
48
+ }
49
+
50
+ function hasFlag(args: string[], flag: string): boolean {
51
+ return args.includes(flag);
52
+ }
53
+
54
+ function parsePositiveInt(value: string, flag: string): number {
55
+ const parsed = Number(value);
56
+ if (!Number.isInteger(parsed) || parsed < 1) {
57
+ throw new Error(`${flag} must be a positive integer`);
58
+ }
59
+ return parsed;
60
+ }
61
+
62
+ function commandArgs(subcommand: string | undefined, rest: string[]): string[] {
63
+ return subcommand ? [subcommand, ...rest] : rest;
64
+ }
65
+
66
+ function searchArgs(args: string[]): { query: string; limit?: number } {
67
+ const limitRaw = parseFlag(args, "--limit");
68
+ const limit = limitRaw ? parsePositiveInt(limitRaw, "--limit") : undefined;
69
+ const query = args
70
+ .filter((arg, index, all) => {
71
+ if (arg === "--limit") {
72
+ return false;
73
+ }
74
+ if (index > 0 && all[index - 1] === "--limit") {
75
+ return false;
76
+ }
77
+ return true;
78
+ })
79
+ .join(" ")
80
+ .trim();
81
+ return { query, limit };
82
+ }
83
+
84
+ async function runHook(args: string[]): Promise<number> {
85
+ try {
86
+ const provider = parseFlag(args, "--provider") as ProviderId | undefined;
87
+ const hookEvent = parseFlag(args, "--event") as HookEventKind | undefined;
88
+ if (!provider || !hookEvent || !HOOK_EVENTS.includes(hookEvent)) {
89
+ console.error("usage: nn hook --provider <id> --event <kind>");
90
+ return 0;
91
+ }
92
+ const raw = await stdin.text();
93
+ const payload = raw.trim() ? (JSON.parse(raw) as HookPayload) : {};
94
+ await handleHookInvocation(provider, hookEvent, payload);
95
+ console.log(JSON.stringify({}));
96
+ return 0;
97
+ } catch (error) {
98
+ console.error("[nn] hook failed", error);
99
+ console.log(JSON.stringify({}));
100
+ return 0;
101
+ }
102
+ }
103
+
104
+ async function main(): Promise<number> {
105
+ const argv = process.argv.slice(2);
106
+ if (argv.length === 0 || hasFlag(argv, "--help") || hasFlag(argv, "-h")) {
107
+ printHelp();
108
+ return 0;
109
+ }
110
+
111
+ const [command, subcommand, ...rest] = argv;
112
+
113
+ if (command === "__coordinator") {
114
+ await runCoordinatorOnce();
115
+ return 0;
116
+ }
117
+
118
+ if (command === "hook") {
119
+ return runHook(argv.slice(1));
120
+ }
121
+
122
+ if (command === "install" && subcommand === "hooks") {
123
+ const provider = parseFlag(rest, "--provider") as ProviderId | undefined;
124
+ const result = installHooks({
125
+ global: hasFlag(rest, "--global"),
126
+ providers: provider ? [provider] : undefined,
127
+ });
128
+ console.log(JSON.stringify(result, null, 2));
129
+ return 0;
130
+ }
131
+
132
+ if (command === "ingest") {
133
+ const args = commandArgs(subcommand, rest);
134
+ const provider = parseFlag(args, "--provider") as ProviderId | undefined;
135
+ if (!provider) {
136
+ console.error("--provider is required");
137
+ return 1;
138
+ }
139
+ const sessionId = parseFlag(args, "--session");
140
+ const report = await ingestProvider({
141
+ provider,
142
+ sessionId,
143
+ all: hasFlag(args, "--all"),
144
+ });
145
+ console.log(JSON.stringify(report, null, 2));
146
+ return 0;
147
+ }
148
+
149
+ if (command === "export") {
150
+ const args = commandArgs(subcommand, rest);
151
+ const target = parseFlag(args, "--to") as ExportTarget | undefined;
152
+ const sessionRef = parseFlag(args, "--session");
153
+ const outputPath = parseFlag(args, "--output");
154
+ if (!sessionRef) {
155
+ console.error("--session is required");
156
+ return 1;
157
+ }
158
+ let sessionId = sessionRef;
159
+ if (!sessionRef.startsWith("sess:")) {
160
+ sessionId = await resolveSessionId("cursor", sessionRef).catch(
161
+ async () => {
162
+ for (const provider of listProviders()) {
163
+ const found = await resolveSessionId(provider, sessionRef).catch(
164
+ () => null,
165
+ );
166
+ if (found) {
167
+ return found;
168
+ }
169
+ }
170
+ throw new Error(`Session not found: ${sessionRef}`);
171
+ },
172
+ );
173
+ }
174
+ const result = await exportSession(
175
+ sessionId,
176
+ target,
177
+ outputPath ? resolve(outputPath) : undefined,
178
+ );
179
+ if (outputPath) {
180
+ console.error(`Wrote ${result.outputPath}`);
181
+ if (result.manifestPath) {
182
+ console.error(`Wrote ${result.manifestPath}`);
183
+ }
184
+ return 0;
185
+ }
186
+ process.stdout.write(result.content);
187
+ return 0;
188
+ }
189
+
190
+ if (command === "search") {
191
+ const args = commandArgs(subcommand, rest);
192
+ let parsed: ReturnType<typeof searchArgs>;
193
+ try {
194
+ parsed = searchArgs(args);
195
+ } catch (error) {
196
+ console.error(error instanceof Error ? error.message : String(error));
197
+ return 1;
198
+ }
199
+ if (!parsed.query) {
200
+ console.error("search query required");
201
+ return 1;
202
+ }
203
+ const results = await searchEvents(parsed.query, parsed.limit);
204
+ console.log(JSON.stringify(results, null, 2));
205
+ return 0;
206
+ }
207
+
208
+ if (command === "sessions" && subcommand === "list") {
209
+ const provider = parseFlag(rest, "--provider") as ProviderId | undefined;
210
+ const sessions = await listSessions(provider);
211
+ console.log(JSON.stringify(sessions, null, 2));
212
+ return 0;
213
+ }
214
+
215
+ if (command === "sync") {
216
+ if (subcommand === "status") {
217
+ console.log(JSON.stringify(await syncStatus(), null, 2));
218
+ return 0;
219
+ }
220
+ if (subcommand === "push") {
221
+ console.log(await syncPush());
222
+ return 0;
223
+ }
224
+ if (subcommand === "pull") {
225
+ console.log(await syncPull());
226
+ return 0;
227
+ }
228
+ console.error("Unknown sync subcommand");
229
+ return 1;
230
+ }
231
+
232
+ if (command === "doctor") {
233
+ const db = await doctorDb();
234
+ const sync = await syncStatus();
235
+ console.log(JSON.stringify({ db, sync }, null, 2));
236
+ return db.ok ? 0 : 1;
237
+ }
238
+
239
+ console.error(`Unknown command: ${argv.join(" ")}`);
240
+ printHelp();
241
+ return 1;
242
+ }
243
+
244
+ export { ingestIncremental, cursorSessionIdFromPath };
245
+
246
+ const exitCode = await main();
247
+ process.exit(exitCode);
@@ -0,0 +1,38 @@
1
+ export interface ParsedJsonlLine {
2
+ lineIndex: number;
3
+ lineStart: number;
4
+ lineEnd: number;
5
+ raw: string;
6
+ }
7
+
8
+ export function parseCompleteJsonlLines(
9
+ buffer: Buffer,
10
+ startOffset = 0,
11
+ ): { lines: ParsedJsonlLine[]; consumedBytes: number; partialTail: boolean } {
12
+ const lines: ParsedJsonlLine[] = [];
13
+ let lineStart = Math.max(0, startOffset);
14
+ let consumedBytes = startOffset;
15
+ let lineIndex = 0;
16
+
17
+ while (lineStart < buffer.length) {
18
+ const newlineIndex = buffer.indexOf(0x0a, lineStart);
19
+ if (newlineIndex === -1) {
20
+ return { lines, consumedBytes, partialTail: lineStart < buffer.length };
21
+ }
22
+
23
+ const lineEnd = newlineIndex + 1;
24
+ const raw = buffer.subarray(lineStart, newlineIndex).toString("utf-8");
25
+ if (raw.trim().length > 0) {
26
+ lines.push({ lineIndex: lineIndex + 1, lineStart, lineEnd, raw });
27
+ lineIndex += 1;
28
+ }
29
+ consumedBytes = lineEnd;
30
+ lineStart = lineEnd;
31
+ }
32
+
33
+ return { lines, consumedBytes, partialTail: false };
34
+ }
35
+
36
+ export function lineKeyFor(sourcePath: string, lineStart: number): string {
37
+ return `${sourcePath}:${lineStart}`;
38
+ }
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import type { ProviderId } from "../ir/types.ts";
4
+ import { getSourceCursor } from "../db/client.ts";
5
+ import { withDb } from "../db/migrate.ts";
6
+ import { ingestBatch } from "../db/client.ts";
7
+ import { getProvider } from "../providers/registry.ts";
8
+
9
+ export interface IngestOptions {
10
+ provider: ProviderId;
11
+ sessionId?: string;
12
+ all?: boolean;
13
+ sourcePath?: string;
14
+ }
15
+
16
+ export interface IngestReport {
17
+ provider: ProviderId;
18
+ sessionsProcessed: number;
19
+ eventsInserted: number;
20
+ warnings: string[];
21
+ }
22
+
23
+ export async function ingestIncremental(
24
+ provider: ProviderId,
25
+ sourcePath: string,
26
+ sessionId: string,
27
+ ): Promise<IngestReport> {
28
+ return withDb(async (client) => {
29
+ const adapter = getProvider(provider);
30
+ const offset = await getSourceCursor(client, provider, sourcePath);
31
+ const batch = adapter.parseIncremental(sourcePath, sessionId, offset);
32
+ const { inserted } = await ingestBatch(batch);
33
+ return {
34
+ provider,
35
+ sessionsProcessed: 1,
36
+ eventsInserted: inserted,
37
+ warnings: batch.warnings,
38
+ };
39
+ });
40
+ }
41
+
42
+ export async function ingestProvider(
43
+ options: IngestOptions,
44
+ ): Promise<IngestReport> {
45
+ const adapter = getProvider(options.provider);
46
+
47
+ if (options.sourcePath && options.sessionId) {
48
+ return ingestIncremental(
49
+ options.provider,
50
+ options.sourcePath,
51
+ options.sessionId,
52
+ );
53
+ }
54
+
55
+ const sessions = adapter.discover();
56
+ const filtered =
57
+ options.sessionId ?
58
+ sessions.filter((session) => session.sessionId === options.sessionId)
59
+ : sessions;
60
+
61
+ if (filtered.length === 0) {
62
+ return {
63
+ provider: options.provider,
64
+ sessionsProcessed: 0,
65
+ eventsInserted: 0,
66
+ warnings: [],
67
+ };
68
+ }
69
+
70
+ return withDb(async (client) => {
71
+ let eventsInserted = 0;
72
+ const warnings: string[] = [];
73
+
74
+ for (const session of filtered) {
75
+ const offset =
76
+ options.all ? 0 : (
77
+ await getSourceCursor(client, options.provider, session.path)
78
+ );
79
+ const batch = adapter.parseIncremental(
80
+ session.path,
81
+ session.sessionId,
82
+ offset,
83
+ );
84
+ const result = await ingestBatch(batch);
85
+ eventsInserted += result.inserted;
86
+ warnings.push(...batch.warnings);
87
+ }
88
+
89
+ return {
90
+ provider: options.provider,
91
+ sessionsProcessed: filtered.length,
92
+ eventsInserted,
93
+ warnings,
94
+ };
95
+ });
96
+ }
97
+
98
+ export function readFileFromOffset(path: string, offset: number): Buffer {
99
+ const buffer = readFileSync(path);
100
+ return buffer.subarray(offset);
101
+ }
@@ -0,0 +1,227 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+
10
+ import { nnCommand, packageRoot } from "../config/runtime.ts";
11
+ import type { HookEventKind } from "../hooks/types.ts";
12
+ import { HOOK_EVENTS } from "../hooks/types.ts";
13
+ import type {
14
+ InstallHooksOptions,
15
+ InstallHooksResult,
16
+ InstallScope,
17
+ InstallableProvider,
18
+ ProviderInstallResult,
19
+ } from "./types.ts";
20
+ import {
21
+ HOOK_EVENT_BINDINGS,
22
+ INSTALLABLE_PROVIDERS,
23
+ } from "./types.ts";
24
+
25
+ const NN_HOOK_MARKER = "hook --provider";
26
+
27
+ function hookCommand(
28
+ nn: string,
29
+ provider: InstallableProvider,
30
+ hookEvent: HookEventKind,
31
+ ): string {
32
+ return `${nn} hook --provider ${provider} --event ${hookEvent}`;
33
+ }
34
+
35
+ function nnHookEvent(command: string): string | null {
36
+ const match = command.match(/--event (\w+)/);
37
+ return match?.[1] ?? null;
38
+ }
39
+
40
+ function isNnHook(command: string): boolean {
41
+ return (
42
+ command.includes(" hook --provider ") ||
43
+ command.includes("hooks/ingest.ts") ||
44
+ command.includes("hooks/memory/ingest.ts")
45
+ );
46
+ }
47
+
48
+ function loadJson(path: string): Record<string, unknown> {
49
+ if (!existsSync(path)) {
50
+ return {};
51
+ }
52
+ return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
53
+ }
54
+
55
+ function saveJson(path: string, value: Record<string, unknown>): void {
56
+ mkdirSync(dirname(path), { recursive: true });
57
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf-8");
58
+ }
59
+
60
+ function mergeCursorHooks(
61
+ config: Record<string, unknown>,
62
+ registrations: ProviderInstallResult["registrations"],
63
+ ): Record<string, unknown> {
64
+ const hooks =
65
+ typeof config.hooks === "object" && config.hooks !== null ?
66
+ { ...(config.hooks as Record<string, unknown>) }
67
+ : {};
68
+
69
+ for (const registration of registrations) {
70
+ const existing = Array.isArray(hooks[registration.nativeEvent]) ?
71
+ [...(hooks[registration.nativeEvent] as unknown[])]
72
+ : [];
73
+ const filtered = existing.filter((entry) => {
74
+ if (typeof entry !== "object" || entry === null) {
75
+ return true;
76
+ }
77
+ const command = String((entry as { command?: string }).command ?? "");
78
+ if (!isNnHook(command)) {
79
+ return true;
80
+ }
81
+ return nnHookEvent(command) !== registration.hookEvent;
82
+ });
83
+ hooks[registration.nativeEvent] = [
84
+ ...filtered,
85
+ { command: registration.command },
86
+ ];
87
+ }
88
+
89
+ return { version: 1, ...config, hooks };
90
+ }
91
+
92
+ function mergeNestedHooks(
93
+ config: Record<string, unknown>,
94
+ registrations: ProviderInstallResult["registrations"],
95
+ ): Record<string, unknown> {
96
+ const hooks =
97
+ typeof config.hooks === "object" && config.hooks !== null ?
98
+ { ...(config.hooks as Record<string, unknown>) }
99
+ : {};
100
+
101
+ for (const registration of registrations) {
102
+ const existingGroups = Array.isArray(hooks[registration.nativeEvent]) ?
103
+ [...(hooks[registration.nativeEvent] as unknown[])]
104
+ : [];
105
+
106
+ let group =
107
+ registration.matcher ?
108
+ (existingGroups.find(
109
+ (entry) =>
110
+ typeof entry === "object" &&
111
+ entry !== null &&
112
+ (entry as { matcher?: string }).matcher === registration.matcher,
113
+ ) as { matcher?: string; hooks?: unknown[] } | undefined)
114
+ : (existingGroups[0] as { matcher?: string; hooks?: unknown[] } | undefined);
115
+
116
+ if (!group) {
117
+ group = registration.matcher ? { matcher: registration.matcher, hooks: [] } : { hooks: [] };
118
+ existingGroups.push(group);
119
+ }
120
+
121
+ const commandHooks = Array.isArray(group.hooks) ? [...group.hooks] : [];
122
+ group.hooks = [
123
+ ...commandHooks.filter((entry) => {
124
+ if (typeof entry !== "object" || entry === null) {
125
+ return true;
126
+ }
127
+ const command = String((entry as { command?: string }).command ?? "");
128
+ if (!isNnHook(command)) {
129
+ return true;
130
+ }
131
+ return nnHookEvent(command) !== registration.hookEvent;
132
+ }),
133
+ { type: "command", command: registration.command },
134
+ ];
135
+ hooks[registration.nativeEvent] = existingGroups;
136
+ }
137
+
138
+ return { ...config, hooks };
139
+ }
140
+
141
+ function providerConfigPath(
142
+ provider: InstallableProvider,
143
+ scope: InstallScope,
144
+ projectDir: string,
145
+ ): string {
146
+ if (provider === "cursor") {
147
+ return scope === "global" ?
148
+ join(homedir(), ".cursor/hooks.json")
149
+ : join(projectDir, ".cursor/hooks.json");
150
+ }
151
+ if (provider === "claude") {
152
+ return scope === "global" ?
153
+ join(homedir(), ".claude/settings.json")
154
+ : join(projectDir, ".claude/settings.json");
155
+ }
156
+ if (provider === "codex") {
157
+ return scope === "global" ?
158
+ join(homedir(), ".codex/hooks.json")
159
+ : join(projectDir, ".codex/hooks.json");
160
+ }
161
+ if (provider === "pi") {
162
+ return scope === "global" ?
163
+ join(homedir(), ".pi/agent/settings.json")
164
+ : join(projectDir, ".pi/settings.json");
165
+ }
166
+ if (provider === "grok") {
167
+ return scope === "global" ?
168
+ join(homedir(), ".grok/user-settings.json")
169
+ : join(projectDir, ".grok/settings.json");
170
+ }
171
+ return scope === "global" ?
172
+ join(homedir(), ".gemini/settings.json")
173
+ : join(projectDir, ".gemini/settings.json");
174
+ }
175
+
176
+ function installProvider(
177
+ provider: InstallableProvider,
178
+ scope: InstallScope,
179
+ projectDir: string,
180
+ nn: string,
181
+ ): ProviderInstallResult {
182
+ const configPath = providerConfigPath(provider, scope, projectDir);
183
+ const registrations = HOOK_EVENTS.map((hookEvent) => {
184
+ const binding = HOOK_EVENT_BINDINGS[provider][hookEvent];
185
+ return {
186
+ provider,
187
+ hookEvent,
188
+ nativeEvent: binding.nativeEvent,
189
+ matcher: binding.matcher,
190
+ command: hookCommand(nn, provider, hookEvent),
191
+ };
192
+ });
193
+
194
+ const existing = loadJson(configPath);
195
+ const merged =
196
+ provider === "cursor" ?
197
+ mergeCursorHooks(existing, registrations)
198
+ : mergeNestedHooks(existing, registrations);
199
+ saveJson(configPath, merged);
200
+
201
+ return { provider, configPath, registrations };
202
+ }
203
+
204
+ export function installHooks(
205
+ options: InstallHooksOptions = {},
206
+ ): InstallHooksResult {
207
+ const sourceRoot = options.sourceRoot ?? packageRoot();
208
+ const scope: InstallScope = options.global ? "global" : "project";
209
+ const projectDir = options.projectDir ?? process.cwd();
210
+ const nn = nnCommand(sourceRoot);
211
+
212
+ const providers = (options.providers ?? INSTALLABLE_PROVIDERS).filter(
213
+ (provider): provider is InstallableProvider =>
214
+ INSTALLABLE_PROVIDERS.includes(provider as InstallableProvider),
215
+ );
216
+
217
+ const results = providers.map((provider) =>
218
+ installProvider(provider, scope, projectDir, nn),
219
+ );
220
+
221
+ return {
222
+ scope,
223
+ sourceRoot,
224
+ ingestScriptPath: nn,
225
+ providers: results,
226
+ };
227
+ }
@@ -0,0 +1,85 @@
1
+ import type { HookEventKind } from "../hooks/types.ts";
2
+ import type { ProviderId } from "../ir/types.ts";
3
+
4
+ export type InstallScope = "project" | "global";
5
+
6
+ export interface HookRegistration {
7
+ provider: ProviderId;
8
+ hookEvent: HookEventKind;
9
+ nativeEvent: string;
10
+ matcher?: string;
11
+ command: string;
12
+ }
13
+
14
+ export interface ProviderInstallResult {
15
+ provider: ProviderId;
16
+ configPath: string;
17
+ registrations: HookRegistration[];
18
+ }
19
+
20
+ export interface InstallHooksOptions {
21
+ global?: boolean;
22
+ projectDir?: string;
23
+ sourceRoot?: string;
24
+ providers?: ProviderId[];
25
+ }
26
+
27
+ export interface InstallHooksResult {
28
+ scope: InstallScope;
29
+ sourceRoot: string;
30
+ ingestScriptPath: string;
31
+ providers: ProviderInstallResult[];
32
+ }
33
+
34
+ export const INSTALLABLE_PROVIDERS = [
35
+ "cursor",
36
+ "claude",
37
+ "codex",
38
+ "pi",
39
+ "grok",
40
+ "antigravity",
41
+ ] as const satisfies readonly ProviderId[];
42
+
43
+ export type InstallableProvider = (typeof INSTALLABLE_PROVIDERS)[number];
44
+
45
+ export const HOOK_EVENT_BINDINGS: Record<
46
+ InstallableProvider,
47
+ Record<HookEventKind, { nativeEvent: string; matcher?: string }>
48
+ > = {
49
+ cursor: {
50
+ sessionStart: { nativeEvent: "sessionStart" },
51
+ postToolUse: { nativeEvent: "postToolUse" },
52
+ turnEnd: { nativeEvent: "afterAgentResponse" },
53
+ stop: { nativeEvent: "stop" },
54
+ },
55
+ claude: {
56
+ sessionStart: { nativeEvent: "SessionStart", matcher: "startup" },
57
+ postToolUse: { nativeEvent: "PostToolUse" },
58
+ turnEnd: { nativeEvent: "Stop" },
59
+ stop: { nativeEvent: "Stop" },
60
+ },
61
+ codex: {
62
+ sessionStart: { nativeEvent: "SessionStart", matcher: "startup" },
63
+ postToolUse: { nativeEvent: "PostToolUse" },
64
+ turnEnd: { nativeEvent: "Stop" },
65
+ stop: { nativeEvent: "Stop" },
66
+ },
67
+ pi: {
68
+ sessionStart: { nativeEvent: "SessionStart", matcher: "startup" },
69
+ postToolUse: { nativeEvent: "PostToolUse" },
70
+ turnEnd: { nativeEvent: "Stop" },
71
+ stop: { nativeEvent: "Stop" },
72
+ },
73
+ grok: {
74
+ sessionStart: { nativeEvent: "SessionStart", matcher: "startup" },
75
+ postToolUse: { nativeEvent: "PostToolUse" },
76
+ turnEnd: { nativeEvent: "Stop" },
77
+ stop: { nativeEvent: "Stop" },
78
+ },
79
+ antigravity: {
80
+ sessionStart: { nativeEvent: "SessionStart", matcher: "startup" },
81
+ postToolUse: { nativeEvent: "AfterTool", matcher: "*" },
82
+ turnEnd: { nativeEvent: "AfterAgent", matcher: "*" },
83
+ stop: { nativeEvent: "SessionEnd", matcher: "exit" },
84
+ },
85
+ };