gitnexushub 0.7.0 → 0.7.1
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/dist/hooks-installer.d.ts +42 -11
- package/dist/hooks-installer.js +61 -30
- package/hooks/gitnexus-enterprise-hook.cjs +208 -30
- package/package.json +1 -1
|
@@ -10,6 +10,22 @@ export interface InstallOptions {
|
|
|
10
10
|
homeDir?: string;
|
|
11
11
|
/** Absolute path to the shipped hook script. */
|
|
12
12
|
hookScriptPath: string;
|
|
13
|
+
/**
|
|
14
|
+
* Absolute path to the `node` binary that should run the hook
|
|
15
|
+
* script. Defaults to `process.execPath` — the node binary
|
|
16
|
+
* currently running the installer, which is the safest cross-
|
|
17
|
+
* platform guarantee that the hook spawn will find an interpreter.
|
|
18
|
+
*
|
|
19
|
+
* Why this matters: GUI editor processes (Cursor.app on macOS, Kiro
|
|
20
|
+
* launched from Dock) inherit a minimal `PATH` that often excludes
|
|
21
|
+
* `~/.local/node/bin`, `/opt/homebrew/bin`, etc. — wherever the
|
|
22
|
+
* user installed node. A hook command of `node "..."` then fails
|
|
23
|
+
* to spawn (`/bin/sh: node: command not found`) and the editor
|
|
24
|
+
* silently marks the hook as failed (Cursor renders this as a
|
|
25
|
+
* cosmetic red X in the Hooks tab; the hook never runs). Embedding
|
|
26
|
+
* the absolute path sidesteps PATH entirely. Tests can override.
|
|
27
|
+
*/
|
|
28
|
+
nodePath?: string;
|
|
13
29
|
}
|
|
14
30
|
/**
|
|
15
31
|
* Install the Claude Code PreToolUse + PostToolUse hooks.
|
|
@@ -38,19 +54,34 @@ export declare function installClaudeCodeHook(opts: InstallOptions): Promise<str
|
|
|
38
54
|
*/
|
|
39
55
|
export declare function installCursorHook(opts: InstallOptions): Promise<string>;
|
|
40
56
|
/**
|
|
41
|
-
* Install
|
|
42
|
-
* Returns the absolute paths to both
|
|
57
|
+
* Install Kiro hooks — graph-tools nudge (askAgent) + edit-closure
|
|
58
|
+
* capture (runCommand). Returns the absolute paths to both files.
|
|
43
59
|
*
|
|
44
|
-
* Kiro
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* PreToolUse/PostToolUse semantics. The gitnexus hook script handles
|
|
48
|
-
* both — it normalises the kebab-case event name back to PascalCase
|
|
49
|
-
* internally so a single script serves Kiro + Claude Code + Cursor +
|
|
50
|
-
* OpenCode.
|
|
60
|
+
* Kiro's actual hook schema (verified against a Kiro-UI-generated
|
|
61
|
+
* file, kiro.dev/docs/hooks notwithstanding) differs in three places
|
|
62
|
+
* from what 0.7.0 shipped:
|
|
51
63
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
64
|
+
* 1. Top-level shape is `{when, then, version, name, enabled}` —
|
|
65
|
+
* not the `{trigger, actions[]}` shape we were writing.
|
|
66
|
+
* 2. Trigger type values are camelCase (`postToolUse`) — not the
|
|
67
|
+
* kebab-case (`post-tool-use`) we emitted.
|
|
68
|
+
* 3. Action type for shell commands is `runCommand` — not `shell`.
|
|
69
|
+
*
|
|
70
|
+
* Plus three runtime constraints Kiro disclosed when asked directly:
|
|
71
|
+
*
|
|
72
|
+
* - `runCommand` actions get NO stdin payload, NO env vars for
|
|
73
|
+
* tool metadata, and NO `${variable}` substitution. So unlike
|
|
74
|
+
* Claude Code / Cursor, the script can't read the just-edited
|
|
75
|
+
* file path off stdin — we have to scan the working tree
|
|
76
|
+
* ourselves (see runKiroEditCapture in the hook script).
|
|
77
|
+
* - `runCommand` stdout is NOT fed back to the agent. Staleness
|
|
78
|
+
* nudges and graph-context augmentation are invisible via this
|
|
79
|
+
* channel — only `askAgent` actions can inject prompt text.
|
|
80
|
+
* - The graph-tools nudge therefore uses `askAgent`; capture uses
|
|
81
|
+
* `runCommand` plus the `--kiro-edit-capture` flag.
|
|
82
|
+
*
|
|
83
|
+
* `~/.kiro/hooks/` (user-level) IS loaded by Kiro despite some docs
|
|
84
|
+
* suggesting workspace-only — verified empirically in this session.
|
|
54
85
|
*/
|
|
55
86
|
export declare function installKiroHook(opts: InstallOptions): Promise<string[]>;
|
|
56
87
|
/**
|
package/dist/hooks-installer.js
CHANGED
|
@@ -11,6 +11,9 @@ import os from 'os';
|
|
|
11
11
|
function getHome(opts) {
|
|
12
12
|
return opts.homeDir ?? os.homedir();
|
|
13
13
|
}
|
|
14
|
+
function getNode(opts) {
|
|
15
|
+
return opts.nodePath ?? process.execPath;
|
|
16
|
+
}
|
|
14
17
|
async function writeJsonIdempotent(filePath, obj) {
|
|
15
18
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
16
19
|
const body = JSON.stringify(obj, null, 2) + '\n';
|
|
@@ -61,7 +64,7 @@ export async function installClaudeCodeHook(opts) {
|
|
|
61
64
|
// script which editor it's running under so /api/activity captures
|
|
62
65
|
// get the right agentName (the stdin payload alone can't
|
|
63
66
|
// distinguish editors that share the PascalCase MCP event names).
|
|
64
|
-
const command =
|
|
67
|
+
const command = `${shellQuote(getNode(opts))} ${shellQuote(opts.hookScriptPath)} --agent=claude-code`;
|
|
65
68
|
const config = {
|
|
66
69
|
hooks: {
|
|
67
70
|
PreToolUse: [
|
|
@@ -116,7 +119,7 @@ export async function installClaudeCodeHook(opts) {
|
|
|
116
119
|
export async function installCursorHook(opts) {
|
|
117
120
|
const home = getHome(opts);
|
|
118
121
|
const hooksJsonPath = path.join(home, '.cursor', 'hooks.json');
|
|
119
|
-
const command =
|
|
122
|
+
const command = `${shellQuote(getNode(opts))} ${shellQuote(opts.hookScriptPath)} --agent=cursor`;
|
|
120
123
|
// Merge into the user's existing ~/.cursor/hooks.json instead of
|
|
121
124
|
// clobbering it — that file is the GLOBAL Cursor hooks config and
|
|
122
125
|
// may already carry the user's own audit / lint hooks. Idempotent:
|
|
@@ -143,42 +146,70 @@ export async function installCursorHook(opts) {
|
|
|
143
146
|
return hooksJsonPath;
|
|
144
147
|
}
|
|
145
148
|
/**
|
|
146
|
-
* Install
|
|
147
|
-
* Returns the absolute paths to both
|
|
149
|
+
* Install Kiro hooks — graph-tools nudge (askAgent) + edit-closure
|
|
150
|
+
* capture (runCommand). Returns the absolute paths to both files.
|
|
151
|
+
*
|
|
152
|
+
* Kiro's actual hook schema (verified against a Kiro-UI-generated
|
|
153
|
+
* file, kiro.dev/docs/hooks notwithstanding) differs in three places
|
|
154
|
+
* from what 0.7.0 shipped:
|
|
155
|
+
*
|
|
156
|
+
* 1. Top-level shape is `{when, then, version, name, enabled}` —
|
|
157
|
+
* not the `{trigger, actions[]}` shape we were writing.
|
|
158
|
+
* 2. Trigger type values are camelCase (`postToolUse`) — not the
|
|
159
|
+
* kebab-case (`post-tool-use`) we emitted.
|
|
160
|
+
* 3. Action type for shell commands is `runCommand` — not `shell`.
|
|
148
161
|
*
|
|
149
|
-
*
|
|
150
|
-
* hook under `.kiro/hooks/<name>.kiro.hook`. Trigger names are kebab-
|
|
151
|
-
* case ("pre-tool-use" / "post-tool-use"), matching the Claude Code
|
|
152
|
-
* PreToolUse/PostToolUse semantics. The gitnexus hook script handles
|
|
153
|
-
* both — it normalises the kebab-case event name back to PascalCase
|
|
154
|
-
* internally so a single script serves Kiro + Claude Code + Cursor +
|
|
155
|
-
* OpenCode.
|
|
162
|
+
* Plus three runtime constraints Kiro disclosed when asked directly:
|
|
156
163
|
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
164
|
+
* - `runCommand` actions get NO stdin payload, NO env vars for
|
|
165
|
+
* tool metadata, and NO `${variable}` substitution. So unlike
|
|
166
|
+
* Claude Code / Cursor, the script can't read the just-edited
|
|
167
|
+
* file path off stdin — we have to scan the working tree
|
|
168
|
+
* ourselves (see runKiroEditCapture in the hook script).
|
|
169
|
+
* - `runCommand` stdout is NOT fed back to the agent. Staleness
|
|
170
|
+
* nudges and graph-context augmentation are invisible via this
|
|
171
|
+
* channel — only `askAgent` actions can inject prompt text.
|
|
172
|
+
* - The graph-tools nudge therefore uses `askAgent`; capture uses
|
|
173
|
+
* `runCommand` plus the `--kiro-edit-capture` flag.
|
|
174
|
+
*
|
|
175
|
+
* `~/.kiro/hooks/` (user-level) IS loaded by Kiro despite some docs
|
|
176
|
+
* suggesting workspace-only — verified empirically in this session.
|
|
159
177
|
*/
|
|
160
178
|
export async function installKiroHook(opts) {
|
|
161
179
|
const home = getHome(opts);
|
|
162
180
|
const hooksDir = path.join(home, '.kiro', 'hooks');
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
await writeJsonIdempotent(
|
|
170
|
-
name: 'gitnexus-graph-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
const editCaptureCmd = `${shellQuote(getNode(opts))} ${shellQuote(opts.hookScriptPath)} --agent=kiro --kiro-edit-capture`;
|
|
182
|
+
const preferGraphHookPath = path.join(hooksDir, 'gitnexus-prefer-graph-tools.kiro.hook');
|
|
183
|
+
const editCaptureHookPath = path.join(hooksDir, 'gitnexus-edit-capture.kiro.hook');
|
|
184
|
+
// 1. Steer the agent toward graph tools before shell searches.
|
|
185
|
+
// askAgent is the only Kiro action whose prompt actually reaches
|
|
186
|
+
// the agent context; runCommand stdout is discarded.
|
|
187
|
+
await writeJsonIdempotent(preferGraphHookPath, {
|
|
188
|
+
name: 'gitnexus-prefer-graph-tools',
|
|
189
|
+
version: '1',
|
|
190
|
+
enabled: true,
|
|
191
|
+
description: 'Before running shell searches (grep/find/rg), prefer GitNexus graph tools for richer code context',
|
|
192
|
+
when: { type: 'preToolUse', toolTypes: ['shell'] },
|
|
193
|
+
then: {
|
|
194
|
+
type: 'askAgent',
|
|
195
|
+
prompt: "Before running this shell command: if it's a search (grep, find, glob, rg), prefer the GitNexus MCP tools instead — `gitnexus_query` for concept search, `gitnexus_context` for symbol lookup, `gitnexus_cypher` for structural queries. Only fall back to shell search if GitNexus can't answer it.",
|
|
196
|
+
},
|
|
174
197
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
// 2. Edit-closure capture. Fires after every write tool. Hook
|
|
199
|
+
// script runs `git status --porcelain` in the workspace root,
|
|
200
|
+
// posts each dirty file to /api/activity/edit-observed. Coarser
|
|
201
|
+
// than Claude Code / Cursor (catches all dirty files, not just
|
|
202
|
+
// the one the agent just touched) — but it's the only signal
|
|
203
|
+
// Kiro's hook plumbing exposes.
|
|
204
|
+
await writeJsonIdempotent(editCaptureHookPath, {
|
|
205
|
+
name: 'gitnexus-edit-capture',
|
|
206
|
+
version: '1',
|
|
207
|
+
enabled: true,
|
|
208
|
+
description: 'Capture file edits for distillation by scanning the working tree after write tool calls',
|
|
209
|
+
when: { type: 'postToolUse', toolTypes: ['write'] },
|
|
210
|
+
then: { type: 'runCommand', command: editCaptureCmd, timeout: 5 },
|
|
180
211
|
});
|
|
181
|
-
return [
|
|
212
|
+
return [preferGraphHookPath, editCaptureHookPath];
|
|
182
213
|
}
|
|
183
214
|
/**
|
|
184
215
|
* Install the OpenCode plugin stub.
|
|
@@ -389,23 +389,67 @@ function normaliseToolName(raw) {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
*
|
|
392
|
+
* Parse a `tool_output` field that may be either an object (Claude
|
|
393
|
+
* Code / Kiro / OpenCode native shape) or a JSON-stringified blob
|
|
394
|
+
* (Cursor wraps the result as a string per cursor.com/docs/hooks).
|
|
395
|
+
* Returns the parsed object, or the original value if it isn't a
|
|
396
|
+
* string we can decode. Caller still has to check for null.
|
|
397
|
+
*/
|
|
398
|
+
function parseToolOutput(out) {
|
|
399
|
+
if (typeof out === 'string') {
|
|
400
|
+
try {
|
|
401
|
+
return JSON.parse(out);
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get the exit code from a normalised tool_output, accepting either
|
|
411
|
+
* `exit_code` (Claude Code / snake_case) or `exitCode` (Cursor /
|
|
412
|
+
* camelCase). Returns null when neither is present so callers can
|
|
413
|
+
* treat that as "succeeded" (some editors omit exit info entirely).
|
|
414
|
+
*/
|
|
415
|
+
function readExitCode(out) {
|
|
416
|
+
if (!out || typeof out !== 'object') return null;
|
|
417
|
+
if (typeof out.exit_code === 'number') return out.exit_code;
|
|
418
|
+
if (typeof out.exitCode === 'number') return out.exitCode;
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Emit the response in the format the source editor expects. Four
|
|
393
424
|
* envelopes today, one script:
|
|
394
425
|
*
|
|
395
426
|
* Claude Code / OpenCode → `hookSpecificOutput` envelope
|
|
396
427
|
* ({ hookSpecificOutput: { hookEventName, additionalContext } })
|
|
397
|
-
* Cursor 2.4+
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
428
|
+
* Cursor 2.4+ preToolUse → `permission` field is REQUIRED (cursor.com/docs/hooks)
|
|
429
|
+
* plus optional `additional_context` for prompt injection
|
|
430
|
+
* Cursor 2.4+ postToolUse → `additional_context` only
|
|
431
|
+
* Kiro IDE → plain stdout (kiro.dev/docs/hooks/actions —
|
|
432
|
+
* except `runCommand` actions are fire-and-forget; stdout is NOT
|
|
433
|
+
* fed back into agent context per Kiro's own clarification)
|
|
401
434
|
*/
|
|
402
435
|
function sendResponse(event, agentName, message) {
|
|
403
436
|
if (agentName === 'kiro') {
|
|
437
|
+
// Kiro `runCommand` action discards stdout. Writing it is harmless
|
|
438
|
+
// — it just doesn't reach the agent. We still emit so manual
|
|
439
|
+
// testing / log inspection has something to read.
|
|
404
440
|
process.stdout.write(message + '\n');
|
|
405
441
|
return;
|
|
406
442
|
}
|
|
407
443
|
if (agentName === 'cursor') {
|
|
408
|
-
|
|
444
|
+
// Cursor preToolUse rejects entries that don't include `permission`
|
|
445
|
+
// — without it the Hooks tab logs the call as failed (cosmetic red
|
|
446
|
+
// X) even when our capture POST succeeded behind the scenes. We
|
|
447
|
+
// always allow; gating tool calls isn't gitnexus's job.
|
|
448
|
+
const envelope =
|
|
449
|
+
event === 'PreToolUse'
|
|
450
|
+
? { permission: 'allow', additional_context: message }
|
|
451
|
+
: { additional_context: message };
|
|
452
|
+
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
409
453
|
return;
|
|
410
454
|
}
|
|
411
455
|
process.stdout.write(
|
|
@@ -415,23 +459,48 @@ function sendResponse(event, agentName, message) {
|
|
|
415
459
|
);
|
|
416
460
|
}
|
|
417
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Cursor preToolUse with no augmentation message still has to return
|
|
464
|
+
* `permission: "allow"` or the IDE logs the call as failed. Use this
|
|
465
|
+
* when the script's other handlers decide not to inject context but
|
|
466
|
+
* we still need to clear the permission gate.
|
|
467
|
+
*/
|
|
468
|
+
function sendCursorAllow() {
|
|
469
|
+
process.stdout.write(JSON.stringify({ permission: 'allow' }) + '\n');
|
|
470
|
+
}
|
|
471
|
+
|
|
418
472
|
async function handlePreToolUse(input, config, entry, agentName) {
|
|
419
473
|
const toolName = normaliseToolName(input.tool_name);
|
|
420
|
-
if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return;
|
|
421
474
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
475
|
+
// Cursor's preToolUse matcher includes Read + Write so the hook
|
|
476
|
+
// also fires for those, but our augment flow only handles search
|
|
477
|
+
// tools. Track whether we actually injected context so we can
|
|
478
|
+
// satisfy Cursor's permission contract on the no-augment path.
|
|
479
|
+
let injected = false;
|
|
480
|
+
|
|
481
|
+
if (toolName === 'Grep' || toolName === 'Glob' || toolName === 'Bash') {
|
|
482
|
+
const pattern = extractPattern(toolName, input.tool_input || {});
|
|
483
|
+
if (pattern && pattern.length >= 3) {
|
|
484
|
+
const res = await httpPostJson(
|
|
485
|
+
`${config.hubUrl}/api/repos/${entry.hubRepoId}/augment`,
|
|
486
|
+
authHeaders(config),
|
|
487
|
+
{ pattern },
|
|
488
|
+
);
|
|
489
|
+
if (res && res.status === 200 && res.body && res.body.text) {
|
|
490
|
+
const text = String(res.body.text).trim();
|
|
491
|
+
if (text) {
|
|
492
|
+
sendResponse('PreToolUse', agentName, text);
|
|
493
|
+
injected = true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
431
498
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
499
|
+
// Cursor preToolUse REQUIRES `permission` in the response; without
|
|
500
|
+
// it the Hooks tab marks every fire as failed (red X). Allow the
|
|
501
|
+
// tool unconditionally when we have nothing else to say — gating
|
|
502
|
+
// tool execution isn't gitnexus's responsibility.
|
|
503
|
+
if (!injected && agentName === 'cursor') sendCursorAllow();
|
|
435
504
|
}
|
|
436
505
|
|
|
437
506
|
/**
|
|
@@ -473,10 +542,15 @@ async function handleEditObservation(input, config, entry, agentName) {
|
|
|
473
542
|
if (tool !== 'Edit' && tool !== 'Write' && tool !== 'MultiEdit') return;
|
|
474
543
|
|
|
475
544
|
// Only fire when the edit succeeded. Some editors omit exit info; treat
|
|
476
|
-
// missing as success so we don't drop legitimate edits.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
545
|
+
// missing as success so we don't drop legitimate edits. Cursor wraps
|
|
546
|
+
// tool_output as a JSON-stringified blob; everyone else passes an
|
|
547
|
+
// object — parseToolOutput normalises both. Cursor uses `exitCode`
|
|
548
|
+
// (camel) while Claude Code uses `exit_code` (snake) — readExitCode
|
|
549
|
+
// accepts either.
|
|
550
|
+
const out = parseToolOutput(input.tool_output);
|
|
551
|
+
const exitCode = readExitCode(out);
|
|
552
|
+
if (exitCode !== null && exitCode !== 0) return;
|
|
553
|
+
if (out && typeof out === 'object' && out.error) return;
|
|
480
554
|
|
|
481
555
|
const filePath = (input.tool_input && input.tool_input.file_path) || '';
|
|
482
556
|
if (!filePath || typeof filePath !== 'string') return;
|
|
@@ -511,11 +585,12 @@ async function handlePostToolUse(input, config, entry, agentName) {
|
|
|
511
585
|
const cmd = (input.tool_input && input.tool_input.command) || '';
|
|
512
586
|
if (!/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(cmd)) return;
|
|
513
587
|
|
|
514
|
-
// Only nudge when the git command actually succeeded.
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
const
|
|
518
|
-
|
|
588
|
+
// Only nudge when the git command actually succeeded. Same parse
|
|
589
|
+
// shenanigans as handleEditObservation — Cursor stringifies and uses
|
|
590
|
+
// camelCase. Treat missing exit info as success.
|
|
591
|
+
const out = parseToolOutput(input.tool_output);
|
|
592
|
+
const exitCode = readExitCode(out);
|
|
593
|
+
if (exitCode !== null && exitCode !== 0) return;
|
|
519
594
|
|
|
520
595
|
let localHead = '';
|
|
521
596
|
try {
|
|
@@ -633,9 +708,95 @@ async function maybeNudgeUpgrade(config) {
|
|
|
633
708
|
}
|
|
634
709
|
}
|
|
635
710
|
|
|
711
|
+
/**
|
|
712
|
+
* Kiro edit-capture mode. Kiro's `runCommand` action provides no stdin
|
|
713
|
+
* payload, no env vars for tool metadata, and no argument substitution
|
|
714
|
+
* — so we can't know which file the agent just edited. Instead, fire
|
|
715
|
+
* this on Kiro's `postToolUse` toolTypes:["write"] hook and scan the
|
|
716
|
+
* git working tree for changed files via `git status --porcelain`.
|
|
717
|
+
* Each dirty path POSTs to /api/activity/edit-observed so distillation
|
|
718
|
+
* gets per-file edit-closure data, even though the precision is
|
|
719
|
+
* coarser than Claude Code / Cursor (catches all dirty files in the
|
|
720
|
+
* tree, not just the one the agent just touched).
|
|
721
|
+
*
|
|
722
|
+
* cwd defaults to process.cwd() which Kiro sets to the workspace root.
|
|
723
|
+
* Resolving to a registry repo + reading the auth token mirrors the
|
|
724
|
+
* standard PostToolUse path.
|
|
725
|
+
*/
|
|
726
|
+
async function runKiroEditCapture(agentName) {
|
|
727
|
+
const config = readConfig();
|
|
728
|
+
if (!config || !config.hubToken || !config.hubUrl) return;
|
|
729
|
+
|
|
730
|
+
const entries = readRegistry();
|
|
731
|
+
const cwd = process.cwd();
|
|
732
|
+
const entry = resolveCwdToRepo(cwd, entries);
|
|
733
|
+
if (!entry) return;
|
|
734
|
+
|
|
735
|
+
// Two parallel git invocations: tracked-file modifications via
|
|
736
|
+
// `git diff --name-only HEAD` (catches modified + staged + deleted-
|
|
737
|
+
// tracked), and untracked files via `git ls-files --others
|
|
738
|
+
// --exclude-standard`. Both produce one absolute-relative path per
|
|
739
|
+
// line, no status-code prefix — far more robust than parsing
|
|
740
|
+
// `git status --porcelain` (whose XY field varies across git
|
|
741
|
+
// versions and edge cases).
|
|
742
|
+
const collected = new Set();
|
|
743
|
+
for (const args of [
|
|
744
|
+
['diff', '--name-only', 'HEAD'],
|
|
745
|
+
['ls-files', '--others', '--exclude-standard'],
|
|
746
|
+
]) {
|
|
747
|
+
try {
|
|
748
|
+
const r = spawnSync('git', args, { cwd, encoding: 'utf-8', timeout: 2000 });
|
|
749
|
+
const out = (r.stdout || '').trim();
|
|
750
|
+
if (!out) continue;
|
|
751
|
+
for (const line of out.split('\n')) {
|
|
752
|
+
const trimmed = line.trim();
|
|
753
|
+
if (trimmed) collected.add(trimmed);
|
|
754
|
+
}
|
|
755
|
+
} catch {
|
|
756
|
+
// Best effort — proceed with whatever we got from prior args.
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (collected.size === 0) return;
|
|
760
|
+
|
|
761
|
+
// Cap at 20 files per fire so a big rewrite or a fresh branch
|
|
762
|
+
// pull doesn't hammer the hub with hundreds of edit-observed
|
|
763
|
+
// posts. The 20 cap is arbitrary but matches the per-call
|
|
764
|
+
// ladybugdb worker pool's typical session length.
|
|
765
|
+
let posted = 0;
|
|
766
|
+
for (const filePath of collected) {
|
|
767
|
+
if (posted >= 20) break;
|
|
768
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
769
|
+
await httpPostJson(`${config.hubUrl}/api/activity/edit-observed`, authHeaders(config), {
|
|
770
|
+
sessionId: null,
|
|
771
|
+
repoId: entry.hubRepoId,
|
|
772
|
+
filePath: absPath,
|
|
773
|
+
line: null,
|
|
774
|
+
tool: 'Write',
|
|
775
|
+
agentName,
|
|
776
|
+
});
|
|
777
|
+
posted++;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
636
781
|
async function main() {
|
|
637
782
|
if (process.env.GITNEXUS_NO_AUGMENT === '1') return;
|
|
638
783
|
|
|
784
|
+
// Kiro short-circuit: when invoked with --kiro-edit-capture, skip
|
|
785
|
+
// the standard stdin-driven flow entirely (Kiro's runCommand
|
|
786
|
+
// doesn't pipe stdin) and run the porcelain scan instead. The
|
|
787
|
+
// installer wires this flag onto Kiro's postToolUse:write hook.
|
|
788
|
+
if (process.argv.slice(2).includes('--kiro-edit-capture')) {
|
|
789
|
+
const agentName = parseAgentArg() || 'kiro';
|
|
790
|
+
try {
|
|
791
|
+
await runKiroEditCapture(agentName);
|
|
792
|
+
} catch (err) {
|
|
793
|
+
if (process.env.GITNEXUS_DEBUG) {
|
|
794
|
+
process.stderr.write(`kiro edit-capture: ${(err && err.message) || String(err)}\n`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
639
800
|
const input = readInput();
|
|
640
801
|
const rawEvent = input.hook_event_name;
|
|
641
802
|
const event = normaliseEvent(rawEvent);
|
|
@@ -654,9 +815,26 @@ async function main() {
|
|
|
654
815
|
}
|
|
655
816
|
|
|
656
817
|
const entries = readRegistry();
|
|
657
|
-
|
|
818
|
+
// Cursor 3.x spawns hooks from ~/.cursor (NOT the workspace) and
|
|
819
|
+
// sets `cwd: ""` in the stdin payload. The actual project root is
|
|
820
|
+
// in `workspace_roots[0]`. Without this fallback, resolveCwdToRepo
|
|
821
|
+
// looks for a registry entry at ~/.cursor → no match → script
|
|
822
|
+
// bails before posting /api/activity/edit-observed. Other editors
|
|
823
|
+
// (Claude Code, Kiro) set `cwd` themselves, so the fallback chain
|
|
824
|
+
// hits their value first.
|
|
825
|
+
const cwd =
|
|
826
|
+
(Array.isArray(input.workspace_roots) && input.workspace_roots[0]) ||
|
|
827
|
+
input.cwd ||
|
|
828
|
+
process.cwd();
|
|
658
829
|
const entry = resolveCwdToRepo(cwd, entries);
|
|
659
|
-
if (!entry)
|
|
830
|
+
if (!entry) {
|
|
831
|
+
// Cursor preToolUse must still emit `permission: "allow"` even
|
|
832
|
+
// when we have nothing else to say — without it the IDE flags the
|
|
833
|
+
// hook entry as failed (cosmetic red X). All other paths exit
|
|
834
|
+
// silently as before.
|
|
835
|
+
if (event === 'PreToolUse' && agentName === 'cursor') sendCursorAllow();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
660
838
|
|
|
661
839
|
try {
|
|
662
840
|
if (event === 'PreToolUse') {
|
package/package.json
CHANGED