gitnexushub 0.3.0 → 0.4.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.
- package/dist/api.d.ts +28 -0
- package/dist/api.js +39 -0
- package/dist/cli-helpers.d.ts +23 -0
- package/dist/cli-helpers.js +57 -0
- package/dist/connect-command.d.ts +29 -0
- package/dist/connect-command.js +169 -0
- package/dist/editors/claude-code.js +8 -0
- package/dist/editors/cursor.js +5 -1
- package/dist/hooks-installer.d.ts +33 -0
- package/dist/hooks-installer.js +114 -0
- package/dist/index.js +23 -171
- package/dist/registry.d.ts +41 -0
- package/dist/registry.js +92 -0
- package/dist/sync-command.d.ts +16 -0
- package/dist/sync-command.js +169 -0
- package/dist/tarball.d.ts +17 -0
- package/dist/tarball.js +75 -0
- package/hooks/gitnexus-enterprise-hook.cjs +415 -0
- package/package.json +5 -2
- package/skills/gitnexus-guide.md +1 -1
- package/skills/gitnexus-refactoring.md +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitNexus Enterprise Hook
|
|
4
|
+
*
|
|
5
|
+
* PreToolUse - POST /api/repos/:id/augment, prepend text to tool result
|
|
6
|
+
* PostToolUse - after git mutations, compare local HEAD vs hub last_commit,
|
|
7
|
+
* emit staleness hint to nudge the agent toward `gnx sync`
|
|
8
|
+
*
|
|
9
|
+
* Resolves cwd -> hub_repo_id via ~/.gitnexus/connect-registry.json.
|
|
10
|
+
* Reads auth from ~/.gitnexus/config.json (written by `gnx connect`).
|
|
11
|
+
*
|
|
12
|
+
* Hard 1500ms timeout on all HTTP calls. Silent failure on error so the
|
|
13
|
+
* original tool runs unchanged. GITNEXUS_NO_AUGMENT=1 disables entirely.
|
|
14
|
+
*
|
|
15
|
+
* CJS, runs as `node gitnexus-enterprise-hook.cjs` without a build step.
|
|
16
|
+
* Dependencies: Node stdlib only (fs, path, os, http, https, child_process,
|
|
17
|
+
* url).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const http = require('http');
|
|
24
|
+
const https = require('https');
|
|
25
|
+
const { spawnSync } = require('child_process');
|
|
26
|
+
const { URL } = require('url');
|
|
27
|
+
|
|
28
|
+
const HOOK_TIMEOUT_MS = 1500;
|
|
29
|
+
const CONFIG_PATH = path.join(os.homedir(), '.gitnexus', 'config.json');
|
|
30
|
+
const REGISTRY_PATH = path.join(os.homedir(), '.gitnexus', 'connect-registry.json');
|
|
31
|
+
const META_CACHE_DIR = path.join(os.homedir(), '.gitnexus', 'meta-cache');
|
|
32
|
+
const META_CACHE_TTL_MS = 30_000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read the hook event JSON from stdin (file descriptor 0). Claude Code /
|
|
36
|
+
* Cursor / OpenCode all send the event payload as a single blob on stdin.
|
|
37
|
+
* Any parse failure returns an empty object so downstream code can fall
|
|
38
|
+
* through to the silent-failure path.
|
|
39
|
+
*/
|
|
40
|
+
function readInput() {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(0, 'utf-8'));
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readConfig() {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readRegistry() {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'));
|
|
59
|
+
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the editor's current working directory to the best-matching
|
|
67
|
+
* registry entry.
|
|
68
|
+
*
|
|
69
|
+
* Mirrors the semantics of gitnexus-connect/src/registry.ts
|
|
70
|
+
* resolveCwdToRepo (longest-path match + symlink normalization +
|
|
71
|
+
* path-separator boundary) but reimplemented here with sync fs APIs so
|
|
72
|
+
* the hook has no runtime dependency on the compiled connect bundle.
|
|
73
|
+
* Windows paths are lowercased before comparison so D:\foo and d:\foo
|
|
74
|
+
* match.
|
|
75
|
+
*/
|
|
76
|
+
function resolveCwdToRepo(cwd, entries) {
|
|
77
|
+
if (!entries.length) return null;
|
|
78
|
+
|
|
79
|
+
let resolved;
|
|
80
|
+
try {
|
|
81
|
+
resolved = fs.realpathSync(path.resolve(cwd));
|
|
82
|
+
} catch {
|
|
83
|
+
resolved = path.resolve(cwd);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isWin = process.platform === 'win32';
|
|
87
|
+
const norm = isWin ? resolved.toLowerCase() : resolved;
|
|
88
|
+
const sep = path.sep;
|
|
89
|
+
|
|
90
|
+
let best = null;
|
|
91
|
+
let bestLen = 0;
|
|
92
|
+
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (!entry || !entry.localPath || !entry.hubRepoId) continue;
|
|
95
|
+
let ep;
|
|
96
|
+
try {
|
|
97
|
+
ep = fs.realpathSync(path.resolve(entry.localPath));
|
|
98
|
+
} catch {
|
|
99
|
+
ep = path.resolve(entry.localPath);
|
|
100
|
+
}
|
|
101
|
+
const nep = isWin ? ep.toLowerCase() : ep;
|
|
102
|
+
const matched = norm === nep || norm.startsWith(nep + sep);
|
|
103
|
+
if (matched && nep.length > bestLen) {
|
|
104
|
+
best = entry;
|
|
105
|
+
bestLen = nep.length;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return best;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract a usable search pattern from the tool input, or null if the
|
|
114
|
+
* tool is not one we augment. Mirrors the OSS engine's best-effort
|
|
115
|
+
* extraction:
|
|
116
|
+
* - Grep: direct pattern field
|
|
117
|
+
* - Glob: first word-like token after a slash or star
|
|
118
|
+
* - Bash: rg/grep command — skip flags and flag values, first bare
|
|
119
|
+
* token wins
|
|
120
|
+
*/
|
|
121
|
+
function extractPattern(toolName, toolInput) {
|
|
122
|
+
if (toolName === 'Grep') {
|
|
123
|
+
return (toolInput && toolInput.pattern) || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (toolName === 'Glob') {
|
|
127
|
+
const raw = (toolInput && toolInput.pattern) || '';
|
|
128
|
+
const m = raw.match(/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/);
|
|
129
|
+
return m ? m[1] : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (toolName === 'Bash') {
|
|
133
|
+
const cmd = (toolInput && toolInput.command) || '';
|
|
134
|
+
if (!/\brg\b|\bgrep\b/.test(cmd)) return null;
|
|
135
|
+
|
|
136
|
+
const tokens = cmd.split(/\s+/);
|
|
137
|
+
let foundCmd = false;
|
|
138
|
+
let skipNext = false;
|
|
139
|
+
const flagsWithValues = new Set([
|
|
140
|
+
'-e',
|
|
141
|
+
'-f',
|
|
142
|
+
'-m',
|
|
143
|
+
'-A',
|
|
144
|
+
'-B',
|
|
145
|
+
'-C',
|
|
146
|
+
'-g',
|
|
147
|
+
'--glob',
|
|
148
|
+
'-t',
|
|
149
|
+
'--type',
|
|
150
|
+
'--include',
|
|
151
|
+
'--exclude',
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
for (const token of tokens) {
|
|
155
|
+
if (skipNext) {
|
|
156
|
+
skipNext = false;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!foundCmd) {
|
|
160
|
+
if (/\brg$|\bgrep$/.test(token)) foundCmd = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (token.startsWith('-')) {
|
|
164
|
+
if (flagsWithValues.has(token)) skipNext = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const cleaned = token.replace(/['"]/g, '');
|
|
168
|
+
return cleaned.length >= 3 ? cleaned : null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* POST JSON with a hard 1500ms timeout. Resolves to
|
|
177
|
+
* {status: number, body: any} on success
|
|
178
|
+
* null on any error / timeout / non-JSON
|
|
179
|
+
*
|
|
180
|
+
* Never throws so callers can use the short-circuit pattern
|
|
181
|
+
* const res = await httpPostJson(...); if (!res) return;
|
|
182
|
+
* and rely on silent-failure semantics.
|
|
183
|
+
*/
|
|
184
|
+
function httpPostJson(urlStr, headers, body) {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
try {
|
|
187
|
+
const u = new URL(urlStr);
|
|
188
|
+
const mod = u.protocol === 'https:' ? https : http;
|
|
189
|
+
const payload = JSON.stringify(body);
|
|
190
|
+
const req = mod.request(
|
|
191
|
+
{
|
|
192
|
+
method: 'POST',
|
|
193
|
+
hostname: u.hostname,
|
|
194
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
195
|
+
path: u.pathname + u.search,
|
|
196
|
+
headers: Object.assign(
|
|
197
|
+
{
|
|
198
|
+
'Content-Type': 'application/json',
|
|
199
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
200
|
+
},
|
|
201
|
+
headers || {},
|
|
202
|
+
),
|
|
203
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
204
|
+
},
|
|
205
|
+
(res) => {
|
|
206
|
+
let data = '';
|
|
207
|
+
res.on('data', (c) => (data += c));
|
|
208
|
+
res.on('end', () => {
|
|
209
|
+
try {
|
|
210
|
+
resolve({ status: res.statusCode, body: JSON.parse(data) });
|
|
211
|
+
} catch {
|
|
212
|
+
resolve({ status: res.statusCode, body: null });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
req.on('error', () => resolve(null));
|
|
218
|
+
req.on('timeout', () => {
|
|
219
|
+
try {
|
|
220
|
+
req.destroy();
|
|
221
|
+
} catch {
|
|
222
|
+
/* ignore */
|
|
223
|
+
}
|
|
224
|
+
resolve(null);
|
|
225
|
+
});
|
|
226
|
+
req.write(payload);
|
|
227
|
+
req.end();
|
|
228
|
+
} catch {
|
|
229
|
+
resolve(null);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* GET JSON with a hard 1500ms timeout. Same contract as httpPostJson.
|
|
236
|
+
*/
|
|
237
|
+
function httpGetJson(urlStr, headers) {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
try {
|
|
240
|
+
const u = new URL(urlStr);
|
|
241
|
+
const mod = u.protocol === 'https:' ? https : http;
|
|
242
|
+
const req = mod.request(
|
|
243
|
+
{
|
|
244
|
+
method: 'GET',
|
|
245
|
+
hostname: u.hostname,
|
|
246
|
+
port: u.port || (u.protocol === 'https:' ? 443 : 80),
|
|
247
|
+
path: u.pathname + u.search,
|
|
248
|
+
headers: headers || {},
|
|
249
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
250
|
+
},
|
|
251
|
+
(res) => {
|
|
252
|
+
let data = '';
|
|
253
|
+
res.on('data', (c) => (data += c));
|
|
254
|
+
res.on('end', () => {
|
|
255
|
+
try {
|
|
256
|
+
resolve({ status: res.statusCode, body: JSON.parse(data) });
|
|
257
|
+
} catch {
|
|
258
|
+
resolve(null);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
req.on('error', () => resolve(null));
|
|
264
|
+
req.on('timeout', () => {
|
|
265
|
+
try {
|
|
266
|
+
req.destroy();
|
|
267
|
+
} catch {
|
|
268
|
+
/* ignore */
|
|
269
|
+
}
|
|
270
|
+
resolve(null);
|
|
271
|
+
});
|
|
272
|
+
req.end();
|
|
273
|
+
} catch {
|
|
274
|
+
resolve(null);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Emit the Claude Code / Cursor / OpenCode hookSpecificOutput envelope.
|
|
281
|
+
* The editor will prepend `message` to the tool's output before handing
|
|
282
|
+
* control back to the model.
|
|
283
|
+
*/
|
|
284
|
+
function sendResponse(event, message) {
|
|
285
|
+
process.stdout.write(
|
|
286
|
+
JSON.stringify({
|
|
287
|
+
hookSpecificOutput: { hookEventName: event, additionalContext: message },
|
|
288
|
+
}) + '\n',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function handlePreToolUse(input, config, entry) {
|
|
293
|
+
const toolName = input.tool_name || '';
|
|
294
|
+
if (toolName !== 'Grep' && toolName !== 'Glob' && toolName !== 'Bash') return;
|
|
295
|
+
|
|
296
|
+
const pattern = extractPattern(toolName, input.tool_input || {});
|
|
297
|
+
if (!pattern || pattern.length < 3) return;
|
|
298
|
+
|
|
299
|
+
const res = await httpPostJson(
|
|
300
|
+
`${config.hubUrl}/api/repos/${entry.hubRepoId}/augment`,
|
|
301
|
+
{ Authorization: `Bearer ${config.hubToken}` },
|
|
302
|
+
{ pattern },
|
|
303
|
+
);
|
|
304
|
+
if (!res || res.status !== 200 || !res.body || !res.body.text) return;
|
|
305
|
+
|
|
306
|
+
const text = String(res.body.text).trim();
|
|
307
|
+
if (!text) return;
|
|
308
|
+
sendResponse('PreToolUse', text);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Read the meta cache for a given repo, returning null if absent or
|
|
313
|
+
* older than META_CACHE_TTL_MS. The cache keeps the hook from hammering
|
|
314
|
+
* /meta on every git commit in a batch — editors can fire multiple
|
|
315
|
+
* PostToolUse events in quick succession.
|
|
316
|
+
*/
|
|
317
|
+
function readMetaCache(repoId) {
|
|
318
|
+
try {
|
|
319
|
+
const p = path.join(META_CACHE_DIR, `${repoId}.json`);
|
|
320
|
+
const stat = fs.statSync(p);
|
|
321
|
+
if (Date.now() - stat.mtimeMs > META_CACHE_TTL_MS) return null;
|
|
322
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function writeMetaCache(repoId, meta) {
|
|
329
|
+
try {
|
|
330
|
+
fs.mkdirSync(META_CACHE_DIR, { recursive: true });
|
|
331
|
+
fs.writeFileSync(path.join(META_CACHE_DIR, `${repoId}.json`), JSON.stringify(meta));
|
|
332
|
+
} catch {
|
|
333
|
+
/* ignore — cache is best-effort */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function handlePostToolUse(input, config, entry) {
|
|
338
|
+
if (input.tool_name !== 'Bash') return;
|
|
339
|
+
const cmd = (input.tool_input && input.tool_input.command) || '';
|
|
340
|
+
if (!/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(cmd)) return;
|
|
341
|
+
|
|
342
|
+
// Only nudge when the git command actually succeeded. Some editors
|
|
343
|
+
// omit exit_code; treat missing as success so we don't silently
|
|
344
|
+
// skip legitimate mutations.
|
|
345
|
+
const exitCode = input.tool_output && input.tool_output.exit_code;
|
|
346
|
+
if (exitCode !== undefined && exitCode !== 0) return;
|
|
347
|
+
|
|
348
|
+
let localHead = '';
|
|
349
|
+
try {
|
|
350
|
+
const r = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
351
|
+
cwd: input.cwd || process.cwd(),
|
|
352
|
+
encoding: 'utf-8',
|
|
353
|
+
timeout: 2000,
|
|
354
|
+
});
|
|
355
|
+
localHead = (r.stdout || '').trim();
|
|
356
|
+
} catch {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!localHead) return;
|
|
360
|
+
|
|
361
|
+
let meta = readMetaCache(entry.hubRepoId);
|
|
362
|
+
if (!meta) {
|
|
363
|
+
const res = await httpGetJson(`${config.hubUrl}/api/repos/${entry.hubRepoId}/meta`, {
|
|
364
|
+
Authorization: `Bearer ${config.hubToken}`,
|
|
365
|
+
});
|
|
366
|
+
if (!res || res.status !== 200 || !res.body) return;
|
|
367
|
+
meta = res.body;
|
|
368
|
+
writeMetaCache(entry.hubRepoId, meta);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (meta.last_commit === localHead) return;
|
|
372
|
+
const shortOld = meta.last_commit ? String(meta.last_commit).slice(0, 7) : 'none';
|
|
373
|
+
sendResponse(
|
|
374
|
+
'PostToolUse',
|
|
375
|
+
`GitNexus index is stale (last indexed: ${shortOld}). Run \`gnx sync\` to update.`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function main() {
|
|
380
|
+
if (process.env.GITNEXUS_NO_AUGMENT === '1') return;
|
|
381
|
+
|
|
382
|
+
const input = readInput();
|
|
383
|
+
const event = input.hook_event_name;
|
|
384
|
+
if (event !== 'PreToolUse' && event !== 'PostToolUse') return;
|
|
385
|
+
|
|
386
|
+
const config = readConfig();
|
|
387
|
+
if (!config || !config.hubToken || !config.hubUrl) return;
|
|
388
|
+
|
|
389
|
+
const entries = readRegistry();
|
|
390
|
+
const cwd = input.cwd || process.cwd();
|
|
391
|
+
const entry = resolveCwdToRepo(cwd, entries);
|
|
392
|
+
if (!entry) return;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
if (event === 'PreToolUse') {
|
|
396
|
+
await handlePreToolUse(input, config, entry);
|
|
397
|
+
} else {
|
|
398
|
+
await handlePostToolUse(input, config, entry);
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (process.env.GITNEXUS_DEBUG) {
|
|
402
|
+
process.stderr.write(
|
|
403
|
+
`gitnexus hook error: ${(err && err.message ? err.message : String(err)).slice(0, 300)}\n`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
main().catch((err) => {
|
|
410
|
+
if (process.env.GITNEXUS_DEBUG) {
|
|
411
|
+
process.stderr.write(
|
|
412
|
+
`gitnexus hook fatal: ${(err && err.message ? err.message : String(err)).slice(0, 300)}\n`,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexushub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Connect your editor to GitNexus Hub — one command MCP setup + project context",
|
|
5
5
|
"author": "Abhigyan Patwari",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"commander": "^12.0.0",
|
|
39
|
-
"picocolors": "^1.1.1"
|
|
39
|
+
"picocolors": "^1.1.1",
|
|
40
|
+
"tar-stream": "^3.1.8"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/node": "^20.19.37",
|
|
44
|
+
"@types/tar-stream": "^3.1.4",
|
|
43
45
|
"tsx": "^4.0.0",
|
|
44
46
|
"typescript": "^5.4.5",
|
|
45
47
|
"vitest": "^4.1.4"
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
},
|
|
50
52
|
"files": [
|
|
51
53
|
"dist",
|
|
54
|
+
"hooks",
|
|
52
55
|
"skills"
|
|
53
56
|
],
|
|
54
57
|
"publishConfig": {}
|
package/skills/gitnexus-guide.md
CHANGED
|
@@ -15,7 +15,7 @@ For any task involving code understanding, debugging, impact analysis, or refact
|
|
|
15
15
|
2. **Match your task to a skill below** and **read that skill file**
|
|
16
16
|
3. **Follow the skill's workflow and checklist**
|
|
17
17
|
|
|
18
|
-
> If step 1 warns the index is stale, run `
|
|
18
|
+
> If step 1 warns the index is stale, run `gnx sync` in the terminal first.
|
|
19
19
|
|
|
20
20
|
## Skills
|
|
21
21
|
|
|
@@ -22,7 +22,7 @@ description: "Use when the user wants to rename, extract, split, move, or restru
|
|
|
22
22
|
4. Plan update order: interfaces → implementations → callers → tests
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
> If "Index is stale" → run `
|
|
25
|
+
> If "Index is stale" → run `gnx sync` in terminal.
|
|
26
26
|
|
|
27
27
|
## Checklists
|
|
28
28
|
|