gitnexushub 0.4.5 → 0.7.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 +90 -1
- package/dist/api.js +34 -0
- package/dist/cli-helpers.js +2 -0
- package/dist/connect-command.js +6 -2
- package/dist/context.js +41 -2
- package/dist/editors/detect.js +5 -0
- package/dist/editors/kiro.d.ts +15 -0
- package/dist/editors/kiro.js +118 -0
- package/dist/editors/types.d.ts +1 -1
- package/dist/hooks-installer.d.ts +33 -1
- package/dist/hooks-installer.js +125 -18
- package/dist/index.js +29 -2
- package/dist/install-ci-command.d.ts +176 -0
- package/dist/install-ci-command.js +680 -0
- package/dist/wiki/claude.d.ts +11 -5
- package/dist/wiki/claude.js +8 -3
- package/dist/wiki/compose-overview.d.ts +29 -0
- package/dist/wiki/compose-overview.js +48 -0
- package/dist/wiki/concurrency.d.ts +20 -0
- package/dist/wiki/concurrency.js +91 -0
- package/dist/wiki/helpers.d.ts +102 -0
- package/dist/wiki/helpers.js +308 -0
- package/dist/wiki/incremental.d.ts +72 -0
- package/dist/wiki/incremental.js +214 -0
- package/dist/wiki/index.js +37 -0
- package/dist/wiki/session.d.ts +10 -0
- package/dist/wiki/session.js +89 -9
- package/dist/wiki/upload-command.d.ts +12 -0
- package/dist/wiki/upload-command.js +384 -53
- package/hooks/gitnexus-enterprise-hook.cjs +245 -13
- package/package.json +1 -1
- package/skills/gitnexus-debugging.md +89 -89
- package/skills/gitnexus-exploring.md +78 -78
- package/skills/gitnexus-impact-analysis.md +99 -99
- package/skills/gitnexus-pr-review.md +161 -161
package/dist/api.d.ts
CHANGED
|
@@ -33,6 +33,44 @@ export interface IndexResult {
|
|
|
33
33
|
fullName: string;
|
|
34
34
|
status: string;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Response from `POST /api/repos/:id/install-ci-bundle`.
|
|
38
|
+
*
|
|
39
|
+
* Hub mints a long-lived CI token + renders the single Claude-led
|
|
40
|
+
* workflow YAML. `token` and `ciToken` are the same value emitted
|
|
41
|
+
* twice for back-compat with the v1 client (which read `ciToken`)
|
|
42
|
+
* and any future caller that reads the more conventional `token`.
|
|
43
|
+
* Use `ciToken` — the field name documents intent.
|
|
44
|
+
*
|
|
45
|
+
* `workflowYaml` is the rendered `.github/workflows/gitnexus.yml`
|
|
46
|
+
* contents. The CLI base64-encodes and PUTs it via the GitHub
|
|
47
|
+
* Contents API; no template assembly happens client-side.
|
|
48
|
+
*/
|
|
49
|
+
export interface InstallCiBundle {
|
|
50
|
+
/** CI token. Same value as `ciToken` — emitted twice for back-compat. */
|
|
51
|
+
token: string;
|
|
52
|
+
/** CI token (preferred field name; documents intent). */
|
|
53
|
+
ciToken: string;
|
|
54
|
+
/** First 12 chars of the token for display in the CLI summary. */
|
|
55
|
+
ciTokenPrefix?: string;
|
|
56
|
+
/** Repo secret name the workflow YAML references for the gnx_ token. */
|
|
57
|
+
ciTokenSecretName?: string;
|
|
58
|
+
/** Repo secret name the workflow YAML references for Claude OAuth. */
|
|
59
|
+
claudeOauthSecretName?: string;
|
|
60
|
+
/** Rendered `.github/workflows/gitnexus.yml` contents. */
|
|
61
|
+
workflowYaml: string;
|
|
62
|
+
/** Hub MCP endpoint URL embedded in the workflow YAML. */
|
|
63
|
+
hubMcpUrl?: string;
|
|
64
|
+
/** owner/repo this token + workflow target. */
|
|
65
|
+
repoFullName: string;
|
|
66
|
+
/**
|
|
67
|
+
* Map of relative-path → file-content. Currently always
|
|
68
|
+
* `{ '.github/workflows/gitnexus.yml': workflowYaml }` — emitted as
|
|
69
|
+
* an object so future installs can ship sidecar config files (e.g.
|
|
70
|
+
* `.gitnexus.toml`) without bumping the response shape.
|
|
71
|
+
*/
|
|
72
|
+
files?: Record<string, string>;
|
|
73
|
+
}
|
|
36
74
|
export interface RepoDetail {
|
|
37
75
|
id: string;
|
|
38
76
|
name: string;
|
|
@@ -121,6 +159,24 @@ export interface WikiUploadStatus {
|
|
|
121
159
|
active: WikiUploadSessionRow | null;
|
|
122
160
|
last: WikiUploadSessionRow | null;
|
|
123
161
|
}
|
|
162
|
+
export interface WikiGroupingContext {
|
|
163
|
+
communityGroups: string;
|
|
164
|
+
interCommunityEdges: string;
|
|
165
|
+
crossCommunityProcesses: string;
|
|
166
|
+
filesWithExports: string;
|
|
167
|
+
directoryTree: string;
|
|
168
|
+
communityMapping?: Array<{
|
|
169
|
+
label: string;
|
|
170
|
+
files: string[];
|
|
171
|
+
}>;
|
|
172
|
+
filesWithExportsRaw?: Array<{
|
|
173
|
+
filePath: string;
|
|
174
|
+
symbols: Array<{
|
|
175
|
+
name: string;
|
|
176
|
+
type: string;
|
|
177
|
+
}>;
|
|
178
|
+
}>;
|
|
179
|
+
}
|
|
124
180
|
export declare class HubAPI {
|
|
125
181
|
private hubUrl;
|
|
126
182
|
private token;
|
|
@@ -135,6 +191,14 @@ export declare class HubAPI {
|
|
|
135
191
|
listRepos(): Promise<HubRepo[]>;
|
|
136
192
|
getConnectContext(repoFullName: string): Promise<ConnectContext>;
|
|
137
193
|
indexRepo(fullName: string): Promise<IndexResult>;
|
|
194
|
+
/**
|
|
195
|
+
* Mint a CI token for the given repo and fetch the rendered
|
|
196
|
+
* Claude-led workflow YAML in one call. Hub atomically revokes
|
|
197
|
+
* any prior `GitNexus CI` token for this repo and mints a fresh
|
|
198
|
+
* one — calling this repeatedly is safe and idempotent (the prior
|
|
199
|
+
* token stops working, the new one starts).
|
|
200
|
+
*/
|
|
201
|
+
installCiBundle(repoId: string): Promise<InstallCiBundle>;
|
|
138
202
|
getRepo(repoId: string): Promise<RepoDetail>;
|
|
139
203
|
meta(repoId: string): Promise<RepoMeta>;
|
|
140
204
|
sync(repoId: string, params: {
|
|
@@ -158,7 +222,32 @@ export declare class HubAPI {
|
|
|
158
222
|
}>;
|
|
159
223
|
wikiUploadStatus(repoId: string): Promise<WikiUploadStatus>;
|
|
160
224
|
getWikiConfig(): Promise<WikiConfig>;
|
|
161
|
-
wikiGroupingContext(repoId: string): Promise<
|
|
225
|
+
wikiGroupingContext(repoId: string): Promise<WikiGroupingContext>;
|
|
226
|
+
wikiStatus(repoId: string): Promise<{
|
|
227
|
+
status: string;
|
|
228
|
+
fromCommit?: string | null;
|
|
229
|
+
generatedAt?: string | null;
|
|
230
|
+
pageCount?: number;
|
|
231
|
+
generating?: boolean;
|
|
232
|
+
} | null>;
|
|
233
|
+
wikiTree(repoId: string): Promise<{
|
|
234
|
+
tree: Array<{
|
|
235
|
+
name: string;
|
|
236
|
+
slug: string;
|
|
237
|
+
files: string[];
|
|
238
|
+
children?: Array<{
|
|
239
|
+
name: string;
|
|
240
|
+
slug: string;
|
|
241
|
+
files: string[];
|
|
242
|
+
}>;
|
|
243
|
+
}>;
|
|
244
|
+
} | null>;
|
|
245
|
+
wikiPage(repoId: string, slug: string): Promise<{
|
|
246
|
+
slug: string;
|
|
247
|
+
title: string;
|
|
248
|
+
content: string;
|
|
249
|
+
updatedAt: string;
|
|
250
|
+
} | null>;
|
|
162
251
|
wikiLeafContext(repoId: string, moduleName: string, filePaths: string[]): Promise<any>;
|
|
163
252
|
wikiOverviewContext(repoId: string, moduleFiles: Record<string, string[]>): Promise<any>;
|
|
164
253
|
wikiPromptTemplates(repoId: string): Promise<{
|
package/dist/api.js
CHANGED
|
@@ -77,6 +77,16 @@ export class HubAPI {
|
|
|
77
77
|
async indexRepo(fullName) {
|
|
78
78
|
return this.post('/api/repos/public', { fullName });
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Mint a CI token for the given repo and fetch the rendered
|
|
82
|
+
* Claude-led workflow YAML in one call. Hub atomically revokes
|
|
83
|
+
* any prior `GitNexus CI` token for this repo and mints a fresh
|
|
84
|
+
* one — calling this repeatedly is safe and idempotent (the prior
|
|
85
|
+
* token stops working, the new one starts).
|
|
86
|
+
*/
|
|
87
|
+
async installCiBundle(repoId) {
|
|
88
|
+
return this.post(`/api/repos/${repoId}/install-ci-bundle`, {});
|
|
89
|
+
}
|
|
80
90
|
async getRepo(repoId) {
|
|
81
91
|
return this.request(`/api/repos/${repoId}`);
|
|
82
92
|
}
|
|
@@ -143,6 +153,30 @@ export class HubAPI {
|
|
|
143
153
|
async wikiGroupingContext(repoId) {
|
|
144
154
|
return this.request(`/api/repos/${repoId}/wiki/context/grouping`);
|
|
145
155
|
}
|
|
156
|
+
async wikiStatus(repoId) {
|
|
157
|
+
try {
|
|
158
|
+
return await this.request(`/api/repos/${repoId}/wiki/status`);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async wikiTree(repoId) {
|
|
165
|
+
try {
|
|
166
|
+
return await this.request(`/api/repos/${repoId}/wiki/tree`);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async wikiPage(repoId, slug) {
|
|
173
|
+
try {
|
|
174
|
+
return await this.request(`/api/repos/${repoId}/wiki/page/${encodeURIComponent(slug)}`);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
146
180
|
async wikiLeafContext(repoId, moduleName, filePaths) {
|
|
147
181
|
const params = new URLSearchParams({ name: moduleName, files: filePaths.join(',') });
|
|
148
182
|
return this.request(`/api/repos/${repoId}/wiki/context/leaf?${params}`);
|
package/dist/cli-helpers.js
CHANGED
|
@@ -12,11 +12,13 @@ import { claudeCodeEditor } from './editors/claude-code.js';
|
|
|
12
12
|
import { cursorEditor } from './editors/cursor.js';
|
|
13
13
|
import { windsurfEditor } from './editors/windsurf.js';
|
|
14
14
|
import { opencodeEditor } from './editors/opencode.js';
|
|
15
|
+
import { kiroEditor } from './editors/kiro.js';
|
|
15
16
|
export const EDITORS = {
|
|
16
17
|
'claude-code': claudeCodeEditor,
|
|
17
18
|
cursor: cursorEditor,
|
|
18
19
|
windsurf: windsurfEditor,
|
|
19
20
|
opencode: opencodeEditor,
|
|
21
|
+
kiro: kiroEditor,
|
|
20
22
|
};
|
|
21
23
|
export const ok = (msg) => console.log(` ${pc.green('✔')} ${msg}`);
|
|
22
24
|
export const info = (msg) => console.log(` ${pc.cyan('ℹ')} ${msg}`);
|
package/dist/connect-command.js
CHANGED
|
@@ -13,7 +13,7 @@ import { generateConnectContext } from './content.js';
|
|
|
13
13
|
import { isGitRepo, getGitRemoteUrl, parseGitRemote, matchRepo } from './project.js';
|
|
14
14
|
import { writeProjectContext } from './context.js';
|
|
15
15
|
import { detectInstalledEditors } from './editors/detect.js';
|
|
16
|
-
import { installClaudeCodeHook, installCursorHook, installOpenCodeHook, } from './hooks-installer.js';
|
|
16
|
+
import { installClaudeCodeHook, installCursorHook, installKiroHook, installOpenCodeHook, } from './hooks-installer.js';
|
|
17
17
|
import { ok, info, warn, fail, resolveAuth, EDITORS } from './cli-helpers.js';
|
|
18
18
|
/**
|
|
19
19
|
* Resolve the path to the shipped hook script (`gitnexus-enterprise-hook.cjs`).
|
|
@@ -65,7 +65,7 @@ export async function runConnect(tokenArg, opts) {
|
|
|
65
65
|
}
|
|
66
66
|
else {
|
|
67
67
|
fail('No editor detected. Please specify one:');
|
|
68
|
-
console.error(` ${pc.cyan('--editor claude-code | cursor | windsurf | opencode')}`);
|
|
68
|
+
console.error(` ${pc.cyan('--editor claude-code | cursor | windsurf | opencode | kiro')}`);
|
|
69
69
|
console.error('');
|
|
70
70
|
return process.exit(1);
|
|
71
71
|
}
|
|
@@ -116,6 +116,10 @@ export async function runConnect(tokenArg, opts) {
|
|
|
116
116
|
await installOpenCodeHook({ hookScriptPath });
|
|
117
117
|
ok('Hooks installed');
|
|
118
118
|
}
|
|
119
|
+
else if (editorId === 'kiro') {
|
|
120
|
+
await installKiroHook({ hookScriptPath });
|
|
121
|
+
ok('Hooks installed');
|
|
122
|
+
}
|
|
119
123
|
// windsurf: no hook system, skip silently
|
|
120
124
|
}
|
|
121
125
|
catch (err) {
|
package/dist/context.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
7
|
import path from 'path';
|
|
8
|
-
import { upsertMarkedSection, removeMarkedSection } from './utils.js';
|
|
8
|
+
import { upsertMarkedSection, removeMarkedSection, fileExists } from './utils.js';
|
|
9
9
|
export async function writeProjectContext(projectDir, ctx) {
|
|
10
10
|
const files = [];
|
|
11
11
|
// Write CLAUDE.md
|
|
@@ -16,6 +16,31 @@ export async function writeProjectContext(projectDir, ctx) {
|
|
|
16
16
|
const agentsPath = path.join(projectDir, 'AGENTS.md');
|
|
17
17
|
const agentsResult = await upsertMarkedSection(agentsPath, ctx.agentsMd);
|
|
18
18
|
files.push(`AGENTS.md (${agentsResult})`);
|
|
19
|
+
// Write Kiro steering doc — Kiro doesn't read AGENTS.md / CLAUDE.md,
|
|
20
|
+
// so we mirror the same content into .kiro/steering/gitnexus.md with
|
|
21
|
+
// `inclusion: always` frontmatter so it's loaded into every Kiro
|
|
22
|
+
// chat in this workspace.
|
|
23
|
+
//
|
|
24
|
+
// Unlike CLAUDE.md / AGENTS.md (which may carry hand-written user
|
|
25
|
+
// content alongside the gitnexus block), this file is dedicated to
|
|
26
|
+
// gitnexus — it's named gitnexus.md, lives in a gitnexus-managed
|
|
27
|
+
// path, and isn't expected to host anything else. So we rewrite the
|
|
28
|
+
// whole file every run instead of using upsertMarkedSection. The
|
|
29
|
+
// previous marker-based approach broke because the YAML frontmatter
|
|
30
|
+
// sat *outside* the gitnexus markers (Kiro requires frontmatter on
|
|
31
|
+
// line 1), so each re-run's "before" prefix re-included the existing
|
|
32
|
+
// frontmatter and stacked another copy on top — Bugbot caught the
|
|
33
|
+
// accumulation on commit de24e98c.
|
|
34
|
+
const kiroSteeringPath = path.join(projectDir, '.kiro', 'steering', 'gitnexus.md');
|
|
35
|
+
const kiroBody = `---\n` +
|
|
36
|
+
`inclusion: always\n` +
|
|
37
|
+
`description: GitNexus code intelligence — graph-backed query, context, and impact tools\n` +
|
|
38
|
+
`---\n\n` +
|
|
39
|
+
`${ctx.agentsMd}\n`;
|
|
40
|
+
const kiroExisted = await fileExists(kiroSteeringPath);
|
|
41
|
+
await fs.mkdir(path.dirname(kiroSteeringPath), { recursive: true });
|
|
42
|
+
await fs.writeFile(kiroSteeringPath, kiroBody, 'utf-8');
|
|
43
|
+
files.push(`.kiro/steering/gitnexus.md (${kiroExisted ? 'updated' : 'created'})`);
|
|
19
44
|
// Write skills to .claude/skills/gitnexus/
|
|
20
45
|
if (ctx.skills.length > 0) {
|
|
21
46
|
const skillsDir = path.join(projectDir, '.claude', 'skills', 'gitnexus');
|
|
@@ -30,13 +55,27 @@ export async function writeProjectContext(projectDir, ctx) {
|
|
|
30
55
|
}
|
|
31
56
|
export async function removeProjectContext(projectDir) {
|
|
32
57
|
const removed = [];
|
|
33
|
-
//
|
|
58
|
+
// CLAUDE.md / AGENTS.md may carry user content; only strip the
|
|
59
|
+
// gitnexus-marked block.
|
|
34
60
|
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
35
61
|
const filePath = path.join(projectDir, name);
|
|
36
62
|
if (await removeMarkedSection(filePath)) {
|
|
37
63
|
removed.push(name);
|
|
38
64
|
}
|
|
39
65
|
}
|
|
66
|
+
// The Kiro steering doc is dedicated to gitnexus (no user content
|
|
67
|
+
// ever lives here, no markers are written), so delete the file
|
|
68
|
+
// outright when it exists.
|
|
69
|
+
const kiroSteeringPath = path.join(projectDir, '.kiro', 'steering', 'gitnexus.md');
|
|
70
|
+
if (await fileExists(kiroSteeringPath)) {
|
|
71
|
+
try {
|
|
72
|
+
await fs.unlink(kiroSteeringPath);
|
|
73
|
+
removed.push('.kiro/steering/gitnexus.md');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Already gone
|
|
77
|
+
}
|
|
78
|
+
}
|
|
40
79
|
// Remove .claude/skills/gitnexus/ directory
|
|
41
80
|
const skillsDir = path.join(projectDir, '.claude', 'skills', 'gitnexus');
|
|
42
81
|
try {
|
package/dist/editors/detect.js
CHANGED
|
@@ -27,6 +27,11 @@ const PROBES = [
|
|
|
27
27
|
name: 'OpenCode',
|
|
28
28
|
dirs: [path.join(os.homedir(), '.config', 'opencode')],
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
id: 'kiro',
|
|
32
|
+
name: 'Kiro',
|
|
33
|
+
dirs: [path.join(os.homedir(), '.kiro')],
|
|
34
|
+
},
|
|
30
35
|
];
|
|
31
36
|
export async function detectInstalledEditors() {
|
|
32
37
|
const found = [];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kiro Editor Setup
|
|
3
|
+
*
|
|
4
|
+
* Writes MCP config to ~/.kiro/settings/mcp.json
|
|
5
|
+
* Installs skill content as steering files in ~/.kiro/steering/
|
|
6
|
+
*
|
|
7
|
+
* Kiro (kiro.dev) doesn't have a SKILL.md system like Claude Code or
|
|
8
|
+
* Cursor — its closest concept is "steering" (always-on guidance docs
|
|
9
|
+
* with optional frontmatter for fileMatch / manual inclusion modes).
|
|
10
|
+
* We emit each Hub skill as `~/.kiro/steering/gitnexus-<name>.md` with
|
|
11
|
+
* `inclusion: manual` so the user pulls them in via `#gitnexus-<name>`
|
|
12
|
+
* in chat — same mental model as `/gitnexus-<name>` in Claude Code.
|
|
13
|
+
*/
|
|
14
|
+
import type { EditorConfig } from './types.js';
|
|
15
|
+
export declare const kiroEditor: EditorConfig;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kiro Editor Setup
|
|
3
|
+
*
|
|
4
|
+
* Writes MCP config to ~/.kiro/settings/mcp.json
|
|
5
|
+
* Installs skill content as steering files in ~/.kiro/steering/
|
|
6
|
+
*
|
|
7
|
+
* Kiro (kiro.dev) doesn't have a SKILL.md system like Claude Code or
|
|
8
|
+
* Cursor — its closest concept is "steering" (always-on guidance docs
|
|
9
|
+
* with optional frontmatter for fileMatch / manual inclusion modes).
|
|
10
|
+
* We emit each Hub skill as `~/.kiro/steering/gitnexus-<name>.md` with
|
|
11
|
+
* `inclusion: manual` so the user pulls them in via `#gitnexus-<name>`
|
|
12
|
+
* in chat — same mental model as `/gitnexus-<name>` in Claude Code.
|
|
13
|
+
*/
|
|
14
|
+
import os from 'os';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import fs from 'fs/promises';
|
|
17
|
+
import { readJsonFile, writeJsonFile } from '../utils.js';
|
|
18
|
+
import { HUB_SKILLS } from '../content.js';
|
|
19
|
+
import { computeFingerprint, getDeviceName } from '../fingerprint.js';
|
|
20
|
+
function getMcpConfig(hubUrl, token) {
|
|
21
|
+
// Kiro's `~/.kiro/settings/mcp.json` is documented at
|
|
22
|
+
// kiro.dev/docs/mcp/configuration. Remote HTTP MCP servers are
|
|
23
|
+
// identified by the presence of `url`. `disabled: false` and the
|
|
24
|
+
// `autoApprove` array are optional — we leave them off so Kiro
|
|
25
|
+
// falls back to its sensible defaults (auto-approval prompts on
|
|
26
|
+
// first call).
|
|
27
|
+
return {
|
|
28
|
+
url: `${hubUrl}/mcp`,
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${token}`,
|
|
31
|
+
'X-Device-Fingerprint': computeFingerprint(),
|
|
32
|
+
'X-Device-Name': getDeviceName(),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function configure(hubUrl, token) {
|
|
37
|
+
const mcpPath = path.join(os.homedir(), '.kiro', 'settings', 'mcp.json');
|
|
38
|
+
try {
|
|
39
|
+
const existing = (await readJsonFile(mcpPath)) || {};
|
|
40
|
+
if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
|
|
41
|
+
existing.mcpServers = {};
|
|
42
|
+
}
|
|
43
|
+
const overrodeCli = !!(existing.mcpServers.gitnexus && 'command' in existing.mcpServers.gitnexus);
|
|
44
|
+
existing.mcpServers.gitnexus = getMcpConfig(hubUrl, token);
|
|
45
|
+
await writeJsonFile(mcpPath, existing);
|
|
46
|
+
return { success: true, message: 'MCP configured in ~/.kiro/settings/mcp.json', overrodeCli };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Wrap raw skill content in a steering frontmatter block so Kiro
|
|
54
|
+
* treats it as a manually-included guidance doc. Without the
|
|
55
|
+
* `inclusion: manual` line Kiro defaults to `always`, which would
|
|
56
|
+
* shove every GitNexus skill into every chat turn — far too noisy.
|
|
57
|
+
*/
|
|
58
|
+
function toSteeringDoc(name, body) {
|
|
59
|
+
return `---
|
|
60
|
+
inclusion: manual
|
|
61
|
+
description: ${name} (GitNexus skill — invoke via #${name})
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
${body.trim()}
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
async function installSkills(skills) {
|
|
68
|
+
const steeringDir = path.join(os.homedir(), '.kiro', 'steering');
|
|
69
|
+
let installed = 0;
|
|
70
|
+
await fs.mkdir(steeringDir, { recursive: true });
|
|
71
|
+
for (const skill of skills) {
|
|
72
|
+
try {
|
|
73
|
+
const filePath = path.join(steeringDir, `${skill.name}.md`);
|
|
74
|
+
await fs.writeFile(filePath, toSteeringDoc(skill.name, skill.content), 'utf-8');
|
|
75
|
+
installed++;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Skip on error
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return installed;
|
|
82
|
+
}
|
|
83
|
+
async function unconfigure() {
|
|
84
|
+
const mcpPath = path.join(os.homedir(), '.kiro', 'settings', 'mcp.json');
|
|
85
|
+
try {
|
|
86
|
+
const existing = await readJsonFile(mcpPath);
|
|
87
|
+
if (existing?.mcpServers?.gitnexus) {
|
|
88
|
+
delete existing.mcpServers.gitnexus;
|
|
89
|
+
await writeJsonFile(mcpPath, existing);
|
|
90
|
+
}
|
|
91
|
+
return { success: true, message: 'MCP removed from ~/.kiro/settings/mcp.json' };
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function removeSkills() {
|
|
98
|
+
const steeringDir = path.join(os.homedir(), '.kiro', 'steering');
|
|
99
|
+
let removed = 0;
|
|
100
|
+
for (const name of HUB_SKILLS) {
|
|
101
|
+
try {
|
|
102
|
+
await fs.rm(path.join(steeringDir, `${name}.md`), { force: true });
|
|
103
|
+
removed++;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* already gone */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return removed;
|
|
110
|
+
}
|
|
111
|
+
export const kiroEditor = {
|
|
112
|
+
id: 'kiro',
|
|
113
|
+
name: 'Kiro',
|
|
114
|
+
configure,
|
|
115
|
+
unconfigure,
|
|
116
|
+
installSkills,
|
|
117
|
+
removeSkills,
|
|
118
|
+
};
|
package/dist/editors/types.d.ts
CHANGED
|
@@ -17,10 +17,42 @@ export interface InstallOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function installClaudeCodeHook(opts: InstallOptions): Promise<string>;
|
|
19
19
|
/**
|
|
20
|
-
* Install the Cursor
|
|
20
|
+
* Install the Cursor preToolUse + postToolUse hooks.
|
|
21
21
|
* Returns the absolute path to the written hooks.json.
|
|
22
|
+
*
|
|
23
|
+
* Cursor 2.4+ hooks live at ~/.cursor/hooks.json with a
|
|
24
|
+
* { version: 1, hooks: { <eventName>: [{ command, ... }] } }
|
|
25
|
+
* shape (cursor.com/docs/hooks). We register `preToolUse` for
|
|
26
|
+
* graph-context augmentation on Shell/Read/Write, and `postToolUse`
|
|
27
|
+
* for staleness detection after `git commit/merge/...` and for
|
|
28
|
+
* edit-closure capture on Edit/Write/MultiEdit.
|
|
29
|
+
*
|
|
30
|
+
* The earlier 0.6.0 implementation used `beforeShellExecution`,
|
|
31
|
+
* which:
|
|
32
|
+
* 1. fires *before* shell execution — wrong direction for the
|
|
33
|
+
* git-staleness post-commit nudge,
|
|
34
|
+
* 2. sends a different stdin shape (no `tool_name`/`tool_input`),
|
|
35
|
+
* so the hook script silently bails on the event check,
|
|
36
|
+
* 3. has no documented context-injection envelope.
|
|
37
|
+
* Switching to pre/postToolUse fixes all three.
|
|
22
38
|
*/
|
|
23
39
|
export declare function installCursorHook(opts: InstallOptions): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Install the Kiro graph-context + staleness hooks.
|
|
42
|
+
* Returns the absolute paths to both written hook files.
|
|
43
|
+
*
|
|
44
|
+
* Kiro hooks (kiro.dev/docs/hooks/types) live as one JSON file per
|
|
45
|
+
* hook under `.kiro/hooks/<name>.kiro.hook`. Trigger names are kebab-
|
|
46
|
+
* case ("pre-tool-use" / "post-tool-use"), matching the Claude Code
|
|
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.
|
|
51
|
+
*
|
|
52
|
+
* We register two hook files because Kiro's hook spec is one trigger
|
|
53
|
+
* per file; bundling both into a single file would silently lose one.
|
|
54
|
+
*/
|
|
55
|
+
export declare function installKiroHook(opts: InstallOptions): Promise<string[]>;
|
|
24
56
|
/**
|
|
25
57
|
* Install the OpenCode plugin stub.
|
|
26
58
|
* Returns the absolute path to the written TypeScript file.
|
package/dist/hooks-installer.js
CHANGED
|
@@ -16,6 +16,35 @@ async function writeJsonIdempotent(filePath, obj) {
|
|
|
16
16
|
const body = JSON.stringify(obj, null, 2) + '\n';
|
|
17
17
|
await fs.writeFile(filePath, body);
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Quote a path for embedding in a shell-style command string. The
|
|
21
|
+
* hook spec across Claude Code / Cursor / Kiro is a literal shell
|
|
22
|
+
* command, not an argv array — so spaces (`John Doe`) and stray
|
|
23
|
+
* quotes both have to survive shell parsing. Wraps in `"..."` and
|
|
24
|
+
* escapes any `"` already inside the path so the shell sees a single
|
|
25
|
+
* argument.
|
|
26
|
+
*
|
|
27
|
+
* Realistic risk of `"` inside the path is near-zero (npm install
|
|
28
|
+
* dirs, Windows profile dirs), but matching the same defensive quoting
|
|
29
|
+
* the OSS gitnexus/src/cli/setup.ts uses keeps the two installers
|
|
30
|
+
* symmetrical.
|
|
31
|
+
*/
|
|
32
|
+
function shellQuote(p) {
|
|
33
|
+
return `"${p.replace(/"/g, '\\"')}"`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read an existing JSON file, returning null if the file is absent
|
|
37
|
+
* or unparseable. Used by installers that need to merge into a
|
|
38
|
+
* shared config rather than clobber it.
|
|
39
|
+
*/
|
|
40
|
+
async function readJsonOrNull(filePath) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(await fs.readFile(filePath, 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
19
48
|
/**
|
|
20
49
|
* Install the Claude Code PreToolUse + PostToolUse hooks.
|
|
21
50
|
* Returns the absolute path to the written hooks.json.
|
|
@@ -24,11 +53,15 @@ export async function installClaudeCodeHook(opts) {
|
|
|
24
53
|
const home = getHome(opts);
|
|
25
54
|
const hooksDir = path.join(home, '.claude', 'plugins', 'gitnexus-enterprise', 'hooks');
|
|
26
55
|
const hooksJsonPath = path.join(hooksDir, 'hooks.json');
|
|
27
|
-
// Double-quote
|
|
28
|
-
// when the install dir contains a space (e.g. Windows
|
|
29
|
-
// `C:\Users\John Doe\...`). The hook spec is a shell-
|
|
30
|
-
// string, not an argv array, so quoting is the only
|
|
31
|
-
|
|
56
|
+
// Double-quote (and escape any embedded quotes) so shell splitting
|
|
57
|
+
// doesn't break when the install dir contains a space (e.g. Windows
|
|
58
|
+
// profiles at `C:\Users\John Doe\...`). The hook spec is a shell-
|
|
59
|
+
// style command string, not an argv array, so quoting is the only
|
|
60
|
+
// knob available. The `--agent=claude-code` flag tells the hook
|
|
61
|
+
// script which editor it's running under so /api/activity captures
|
|
62
|
+
// get the right agentName (the stdin payload alone can't
|
|
63
|
+
// distinguish editors that share the PascalCase MCP event names).
|
|
64
|
+
const command = `node ${shellQuote(opts.hookScriptPath)} --agent=claude-code`;
|
|
32
65
|
const config = {
|
|
33
66
|
hooks: {
|
|
34
67
|
PreToolUse: [
|
|
@@ -37,7 +70,7 @@ export async function installClaudeCodeHook(opts) {
|
|
|
37
70
|
hooks: [
|
|
38
71
|
{
|
|
39
72
|
type: 'command',
|
|
40
|
-
command
|
|
73
|
+
command,
|
|
41
74
|
timeout: 5,
|
|
42
75
|
},
|
|
43
76
|
],
|
|
@@ -49,7 +82,7 @@ export async function installClaudeCodeHook(opts) {
|
|
|
49
82
|
hooks: [
|
|
50
83
|
{
|
|
51
84
|
type: 'command',
|
|
52
|
-
command
|
|
85
|
+
command,
|
|
53
86
|
timeout: 5,
|
|
54
87
|
},
|
|
55
88
|
],
|
|
@@ -61,24 +94,92 @@ export async function installClaudeCodeHook(opts) {
|
|
|
61
94
|
return hooksJsonPath;
|
|
62
95
|
}
|
|
63
96
|
/**
|
|
64
|
-
* Install the Cursor
|
|
97
|
+
* Install the Cursor preToolUse + postToolUse hooks.
|
|
65
98
|
* Returns the absolute path to the written hooks.json.
|
|
99
|
+
*
|
|
100
|
+
* Cursor 2.4+ hooks live at ~/.cursor/hooks.json with a
|
|
101
|
+
* { version: 1, hooks: { <eventName>: [{ command, ... }] } }
|
|
102
|
+
* shape (cursor.com/docs/hooks). We register `preToolUse` for
|
|
103
|
+
* graph-context augmentation on Shell/Read/Write, and `postToolUse`
|
|
104
|
+
* for staleness detection after `git commit/merge/...` and for
|
|
105
|
+
* edit-closure capture on Edit/Write/MultiEdit.
|
|
106
|
+
*
|
|
107
|
+
* The earlier 0.6.0 implementation used `beforeShellExecution`,
|
|
108
|
+
* which:
|
|
109
|
+
* 1. fires *before* shell execution — wrong direction for the
|
|
110
|
+
* git-staleness post-commit nudge,
|
|
111
|
+
* 2. sends a different stdin shape (no `tool_name`/`tool_input`),
|
|
112
|
+
* so the hook script silently bails on the event check,
|
|
113
|
+
* 3. has no documented context-injection envelope.
|
|
114
|
+
* Switching to pre/postToolUse fixes all three.
|
|
66
115
|
*/
|
|
67
116
|
export async function installCursorHook(opts) {
|
|
68
117
|
const home = getHome(opts);
|
|
69
118
|
const hooksJsonPath = path.join(home, '.cursor', 'hooks.json');
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
const command = `node ${shellQuote(opts.hookScriptPath)} --agent=cursor`;
|
|
120
|
+
// Merge into the user's existing ~/.cursor/hooks.json instead of
|
|
121
|
+
// clobbering it — that file is the GLOBAL Cursor hooks config and
|
|
122
|
+
// may already carry the user's own audit / lint hooks. Idempotent:
|
|
123
|
+
// re-running replaces only the gitnexus entries (matched by their
|
|
124
|
+
// command containing "gitnexus-enterprise-hook") and leaves
|
|
125
|
+
// unrelated event arrays + entries intact.
|
|
126
|
+
const existing = (await readJsonOrNull(hooksJsonPath)) || {};
|
|
127
|
+
if (typeof existing.version !== 'number')
|
|
128
|
+
existing.version = 1;
|
|
129
|
+
if (!existing.hooks || typeof existing.hooks !== 'object')
|
|
130
|
+
existing.hooks = {};
|
|
131
|
+
function upsertGitNexus(eventName, matcher, timeout) {
|
|
132
|
+
const arr = Array.isArray(existing.hooks[eventName])
|
|
133
|
+
? existing.hooks[eventName]
|
|
134
|
+
: [];
|
|
135
|
+
const filtered = arr.filter((e) => !e.command?.includes('--agent=cursor'));
|
|
136
|
+
filtered.push({ matcher, command, timeout });
|
|
137
|
+
existing.hooks[eventName] = filtered;
|
|
138
|
+
}
|
|
139
|
+
upsertGitNexus('preToolUse', 'Shell|Read|Write', 5);
|
|
140
|
+
upsertGitNexus('postToolUse', 'Shell|Edit|Write|MultiEdit', 5);
|
|
141
|
+
await fs.mkdir(path.dirname(hooksJsonPath), { recursive: true });
|
|
142
|
+
await fs.writeFile(hooksJsonPath, JSON.stringify(existing, null, 2) + '\n');
|
|
80
143
|
return hooksJsonPath;
|
|
81
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Install the Kiro graph-context + staleness hooks.
|
|
147
|
+
* Returns the absolute paths to both written hook files.
|
|
148
|
+
*
|
|
149
|
+
* Kiro hooks (kiro.dev/docs/hooks/types) live as one JSON file per
|
|
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.
|
|
156
|
+
*
|
|
157
|
+
* We register two hook files because Kiro's hook spec is one trigger
|
|
158
|
+
* per file; bundling both into a single file would silently lose one.
|
|
159
|
+
*/
|
|
160
|
+
export async function installKiroHook(opts) {
|
|
161
|
+
const home = getHome(opts);
|
|
162
|
+
const hooksDir = path.join(home, '.kiro', 'hooks');
|
|
163
|
+
// `--agent=kiro` tags /api/activity captures so distillation can
|
|
164
|
+
// split rows by editor — Kiro's kebab-case events would also let
|
|
165
|
+
// detectAgent infer "kiro", but explicit > inferred.
|
|
166
|
+
const cmd = `node ${shellQuote(opts.hookScriptPath)} --agent=kiro`;
|
|
167
|
+
const preHookPath = path.join(hooksDir, 'gitnexus-graph-context.kiro.hook');
|
|
168
|
+
const postHookPath = path.join(hooksDir, 'gitnexus-staleness-check.kiro.hook');
|
|
169
|
+
await writeJsonIdempotent(preHookPath, {
|
|
170
|
+
name: 'gitnexus-graph-context',
|
|
171
|
+
description: 'Inject GitNexus graph context (callers, callees, impact) before Grep/Glob/Bash tool calls',
|
|
172
|
+
trigger: { type: 'pre-tool-use' },
|
|
173
|
+
actions: [{ type: 'shell', command: cmd }],
|
|
174
|
+
});
|
|
175
|
+
await writeJsonIdempotent(postHookPath, {
|
|
176
|
+
name: 'gitnexus-staleness-check',
|
|
177
|
+
description: 'Capture file edits (edit-closure) and nudge to gnx sync after git commit/merge/rebase',
|
|
178
|
+
trigger: { type: 'post-tool-use' },
|
|
179
|
+
actions: [{ type: 'shell', command: cmd }],
|
|
180
|
+
});
|
|
181
|
+
return [preHookPath, postHookPath];
|
|
182
|
+
}
|
|
82
183
|
/**
|
|
83
184
|
* Install the OpenCode plugin stub.
|
|
84
185
|
* Returns the absolute path to the written TypeScript file.
|
|
@@ -95,6 +196,12 @@ export async function installOpenCodeHook(opts) {
|
|
|
95
196
|
const content = `// GitNexus Enterprise OpenCode plugin
|
|
96
197
|
// Stub — delegates to gitnexus-enterprise-hook.cjs via child process.
|
|
97
198
|
// Hook script: ${opts.hookScriptPath}
|
|
199
|
+
//
|
|
200
|
+
// When wiring up the real spawn() call, pass --agent=opencode so
|
|
201
|
+
// /api/activity captures land with agentName='opencode' (otherwise
|
|
202
|
+
// the script defaults to 'claude-code', which corrupts distillation).
|
|
203
|
+
// Example:
|
|
204
|
+
// spawn('node', [HOOK_PATH, '--agent=opencode'], { stdio: ... })
|
|
98
205
|
|
|
99
206
|
export default {
|
|
100
207
|
async toolBefore(ctx: { tool: string }) {
|