gitnexushub 0.4.2 → 0.4.4

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 CHANGED
@@ -72,6 +72,55 @@ export interface SyncJobStatus {
72
72
  phase: string | null;
73
73
  error: string | null;
74
74
  }
75
+ export interface ResolveRepoResult {
76
+ repoId: string;
77
+ fullName: string;
78
+ status: string;
79
+ indexedAt: string | null;
80
+ lastCommit: string | null;
81
+ permissions: {
82
+ read: boolean;
83
+ write: boolean;
84
+ };
85
+ }
86
+ export interface WikiUploadStartParams {
87
+ mode: 'full' | 'incremental';
88
+ fromCommit?: string;
89
+ clientVersion?: string;
90
+ clientModel?: string;
91
+ }
92
+ export interface WikiUploadStartResult {
93
+ sessionId: string;
94
+ status: 'active';
95
+ startedAt: string;
96
+ }
97
+ export interface WikiUploadPagePayload {
98
+ slug: string;
99
+ title: string;
100
+ contentMd: string;
101
+ }
102
+ export interface WikiUploadFinishParams {
103
+ moduleTree: unknown;
104
+ receivedSlugs: string[];
105
+ }
106
+ export interface WikiConfig {
107
+ serverGenerationEnabled: boolean;
108
+ clientUploadAvailable: boolean;
109
+ clientCommand: string;
110
+ }
111
+ export interface WikiUploadSessionRow {
112
+ id: string;
113
+ userId: string;
114
+ status: 'active' | 'finished' | 'aborted' | 'stale';
115
+ mode: 'full' | 'incremental';
116
+ pagesReceived: number;
117
+ startedAt: string;
118
+ finishedAt: string | null;
119
+ }
120
+ export interface WikiUploadStatus {
121
+ active: WikiUploadSessionRow | null;
122
+ last: WikiUploadSessionRow | null;
123
+ }
75
124
  export declare class HubAPI {
76
125
  private hubUrl;
77
126
  private token;
@@ -81,6 +130,7 @@ export declare class HubAPI {
81
130
  private get authHeaders();
82
131
  private request;
83
132
  private post;
133
+ private postJson;
84
134
  getMe(): Promise<UserProfile>;
85
135
  listRepos(): Promise<HubRepo[]>;
86
136
  getConnectContext(repoFullName: string): Promise<ConnectContext>;
@@ -92,4 +142,41 @@ export declare class HubAPI {
92
142
  tarball: NodeJS.ReadableStream;
93
143
  }): Promise<SyncResult>;
94
144
  syncStatus(repoId: string, jobId: string): Promise<SyncJobStatus>;
145
+ resolveRepoByRemote(remote: string): Promise<ResolveRepoResult>;
146
+ wikiUploadStart(repoId: string, params: WikiUploadStartParams): Promise<WikiUploadStartResult>;
147
+ wikiUploadPage(repoId: string, sessionId: string, page: WikiUploadPagePayload): Promise<{
148
+ slug: string;
149
+ pagesReceived: number;
150
+ }>;
151
+ wikiUploadFinish(repoId: string, sessionId: string, params: WikiUploadFinishParams): Promise<{
152
+ sessionId: string;
153
+ pagesPersisted: number;
154
+ }>;
155
+ wikiUploadAbort(repoId: string, sessionId: string): Promise<{
156
+ sessionId: string;
157
+ status: string;
158
+ }>;
159
+ wikiUploadStatus(repoId: string): Promise<WikiUploadStatus>;
160
+ getWikiConfig(): Promise<WikiConfig>;
161
+ wikiGroupingContext(repoId: string): Promise<any>;
162
+ wikiLeafContext(repoId: string, moduleName: string, filePaths: string[]): Promise<any>;
163
+ wikiOverviewContext(repoId: string, moduleFiles: Record<string, string[]>): Promise<any>;
164
+ wikiPromptTemplates(repoId: string): Promise<{
165
+ grouping: {
166
+ system: string;
167
+ user: string;
168
+ };
169
+ module: {
170
+ system: string;
171
+ user: string;
172
+ };
173
+ parent: {
174
+ system: string;
175
+ user: string;
176
+ };
177
+ overview: {
178
+ system: string;
179
+ user: string;
180
+ };
181
+ }>;
95
182
  }
package/dist/api.js CHANGED
@@ -49,6 +49,22 @@ export class HubAPI {
49
49
  }
50
50
  return res.json();
51
51
  }
52
+ async postJson(path, body, extraHeaders = {}) {
53
+ const res = await fetch(`${this.hubUrl}${path}`, {
54
+ method: 'POST',
55
+ headers: {
56
+ ...this.authHeaders,
57
+ 'Content-Type': 'application/json',
58
+ ...extraHeaders,
59
+ },
60
+ body: JSON.stringify(body),
61
+ });
62
+ if (!res.ok) {
63
+ const err = await res.json().catch(() => ({ error: res.statusText }));
64
+ throw new Error(err.error || `HTTP ${res.status}`);
65
+ }
66
+ return res.json();
67
+ }
52
68
  async getMe() {
53
69
  return this.request('/auth/me');
54
70
  }
@@ -103,4 +119,39 @@ export class HubAPI {
103
119
  async syncStatus(repoId, jobId) {
104
120
  return this.request(`/api/repos/${repoId}/sync/${jobId}`);
105
121
  }
122
+ async resolveRepoByRemote(remote) {
123
+ return this.postJson('/api/repos/resolve', { remote });
124
+ }
125
+ async wikiUploadStart(repoId, params) {
126
+ return this.postJson(`/api/repos/${repoId}/wiki/upload/start`, params);
127
+ }
128
+ async wikiUploadPage(repoId, sessionId, page) {
129
+ return this.postJson(`/api/repos/${repoId}/wiki/upload/page`, { slug: page.slug, title: page.title, content_md: page.contentMd }, { 'X-Wiki-Upload-Session': sessionId });
130
+ }
131
+ async wikiUploadFinish(repoId, sessionId, params) {
132
+ return this.postJson(`/api/repos/${repoId}/wiki/upload/finish`, params, { 'X-Wiki-Upload-Session': sessionId });
133
+ }
134
+ async wikiUploadAbort(repoId, sessionId) {
135
+ return this.postJson(`/api/repos/${repoId}/wiki/upload/abort`, {}, { 'X-Wiki-Upload-Session': sessionId });
136
+ }
137
+ async wikiUploadStatus(repoId) {
138
+ return this.request(`/api/repos/${repoId}/wiki/upload/status`);
139
+ }
140
+ async getWikiConfig() {
141
+ return this.request(`/api/wiki-config`);
142
+ }
143
+ async wikiGroupingContext(repoId) {
144
+ return this.request(`/api/repos/${repoId}/wiki/context/grouping`);
145
+ }
146
+ async wikiLeafContext(repoId, moduleName, filePaths) {
147
+ const params = new URLSearchParams({ name: moduleName, files: filePaths.join(',') });
148
+ return this.request(`/api/repos/${repoId}/wiki/context/leaf?${params}`);
149
+ }
150
+ async wikiOverviewContext(repoId, moduleFiles) {
151
+ const params = new URLSearchParams({ moduleFiles: JSON.stringify(moduleFiles) });
152
+ return this.request(`/api/repos/${repoId}/wiki/context/overview?${params}`);
153
+ }
154
+ async wikiPromptTemplates(repoId) {
155
+ return this.request(`/api/repos/${repoId}/wiki/context/prompts`);
156
+ }
106
157
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Canonicalizes a git remote URL into "host/path" form so that two variants
3
+ * of the same upstream (https vs ssh, with/without `.git`, with/without creds)
4
+ * compare equal. Returns null if the input isn't a recognizable git URL.
5
+ */
6
+ export declare function canonicalizeGitRemote(raw: string): string | null;
@@ -0,0 +1,57 @@
1
+ const CASE_INSENSITIVE_HOSTS = new Set([
2
+ 'github.com',
3
+ 'gitlab.com',
4
+ 'bitbucket.org',
5
+ 'codeberg.org',
6
+ 'gitea.com',
7
+ ]);
8
+ /**
9
+ * Canonicalizes a git remote URL into "host/path" form so that two variants
10
+ * of the same upstream (https vs ssh, with/without `.git`, with/without creds)
11
+ * compare equal. Returns null if the input isn't a recognizable git URL.
12
+ */
13
+ export function canonicalizeGitRemote(raw) {
14
+ if (!raw || typeof raw !== 'string')
15
+ return null;
16
+ const trimmed = raw.trim();
17
+ if (!trimmed)
18
+ return null;
19
+ // Reject local paths before URL parsing
20
+ if (trimmed.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(trimmed))
21
+ return null;
22
+ if (trimmed.startsWith('file://'))
23
+ return null;
24
+ let host;
25
+ let pathPart;
26
+ // SCP-style: user@host:path
27
+ const scpMatch = trimmed.match(/^(?:([^@]+)@)?([^:/@]+):([^:].*)$/);
28
+ if (scpMatch && !trimmed.includes('://')) {
29
+ host = scpMatch[2];
30
+ pathPart = scpMatch[3];
31
+ }
32
+ else {
33
+ try {
34
+ const url = new URL(trimmed);
35
+ if (!['http:', 'https:', 'ssh:', 'git:'].includes(url.protocol))
36
+ return null;
37
+ host = url.hostname;
38
+ pathPart = url.pathname;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ if (!host)
45
+ return null;
46
+ // Strip leading/trailing slashes and .git suffix
47
+ pathPart = pathPart
48
+ .replace(/^\/+/, '')
49
+ .replace(/\/+$/, '')
50
+ .replace(/\.git$/i, '');
51
+ if (!pathPart)
52
+ return null;
53
+ // Lowercase host always; lowercase path only for known case-insensitive hosts
54
+ const lowerHost = host.toLowerCase();
55
+ const normalizedPath = CASE_INSENSITIVE_HOSTS.has(lowerHost) ? pathPart.toLowerCase() : pathPart;
56
+ return `${lowerHost}/${normalizedPath}`;
57
+ }
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { removeProjectContext } from './context.js';
18
18
  import { runSync } from './sync-command.js';
19
19
  import { ok, info, warn, fail, resolveAuth, DEFAULT_HUB_URL, EDITORS } from './cli-helpers.js';
20
20
  import { runConnect } from './connect-command.js';
21
+ import { registerWikiCommand } from './wiki/index.js';
21
22
  const BANNER = [
22
23
  ' ██████╗ ██╗████████╗███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗',
23
24
  '██╔════╝ ██║╚══██╔══╝████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝',
@@ -110,6 +111,8 @@ program
110
111
  process.exit(1);
111
112
  }
112
113
  });
114
+ // ─── wiki commands ────────────────────────────────────────────────
115
+ registerWikiCommand(program);
113
116
  program.parse();
114
117
  // ─── Disconnect Flow ──────────────────────────────────────────────
115
118
  async function runDisconnect(opts) {
@@ -0,0 +1,7 @@
1
+ import type { ResolveContextDeps } from './resolve-context.js';
2
+ export declare function runWikiAbort(deps: ResolveContextDeps): Promise<{
3
+ sessionId: string;
4
+ status: string;
5
+ } | {
6
+ noActive: true;
7
+ }>;
@@ -0,0 +1,15 @@
1
+ import { resolveWikiContext } from './resolve-context.js';
2
+ import { GnxError, ErrorCode } from './errors.js';
3
+ export async function runWikiAbort(deps) {
4
+ const ctx = await resolveWikiContext(deps, { skipClaudeChecks: true });
5
+ const status = await ctx.api.wikiUploadStatus(ctx.hubRepoId);
6
+ if (!status.active) {
7
+ return { noActive: true };
8
+ }
9
+ try {
10
+ return await ctx.api.wikiUploadAbort(ctx.hubRepoId, status.active.id);
11
+ }
12
+ catch (err) {
13
+ throw new GnxError(ErrorCode.NETWORK, 'failed to abort session', { cause: err });
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ export interface ClaudeGenerationResult {
2
+ text: string;
3
+ durationMs: number;
4
+ }
5
+ export interface ClaudeRunner {
6
+ run(prompt: string, opts: {
7
+ cwd: string;
8
+ model?: string;
9
+ allowedTools?: string[];
10
+ }): Promise<ClaudeGenerationResult>;
11
+ }
12
+ export declare function createClaudeRunner(): ClaudeRunner;
@@ -0,0 +1,48 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import { GnxError, ErrorCode } from './errors.js';
3
+ const DEFAULT_ALLOWED_TOOLS = [
4
+ 'Read',
5
+ 'Grep',
6
+ 'mcp__gitnexus__context',
7
+ 'mcp__gitnexus__query',
8
+ 'mcp__gitnexus__group_list',
9
+ 'mcp__gitnexus__group_contracts',
10
+ 'mcp__gitnexus__cypher',
11
+ ];
12
+ export function createClaudeRunner() {
13
+ return {
14
+ async run(prompt, opts) {
15
+ const start = Date.now();
16
+ let finalText = '';
17
+ try {
18
+ for await (const msg of query({
19
+ prompt,
20
+ options: {
21
+ cwd: opts.cwd,
22
+ model: opts.model,
23
+ allowedTools: opts.allowedTools ?? DEFAULT_ALLOWED_TOOLS,
24
+ },
25
+ })) {
26
+ const m = msg;
27
+ if (m.type === 'result' && typeof m.result === 'string') {
28
+ finalText = m.result;
29
+ }
30
+ }
31
+ }
32
+ catch (err) {
33
+ const msg = err?.message ?? String(err);
34
+ if (/auth|login|credentials/i.test(msg)) {
35
+ throw new GnxError(ErrorCode.CLAUDE_AUTH_FAILED, 'Claude Code authentication failed', {
36
+ hint: 're-authenticate: run `claude /login`',
37
+ cause: err,
38
+ });
39
+ }
40
+ throw new GnxError(ErrorCode.GENERATION_FAILED, `Claude Code error: ${msg}`, {
41
+ cause: err,
42
+ });
43
+ }
44
+ const durationMs = Date.now() - start;
45
+ return { text: finalText, durationMs };
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Typed errors with stable exit codes for `gnx wiki *` commands.
3
+ *
4
+ * Exit code ranges (keep stable — used by shell scripts / CI):
5
+ * 10-19 local git problems
6
+ * 20-29 repo resolution / Hub repo state
7
+ * 30-39 auth / config problems
8
+ * 40-49 Claude Code problems
9
+ * 50-59 network / Hub reachability
10
+ * 60-69 permissions (403, read-only)
11
+ * 70-79 session conflict / rate limit
12
+ * 80-89 upload validation / in-flight failures
13
+ */
14
+ export declare enum ErrorCode {
15
+ NOT_GIT_REPO = "NOT_GIT_REPO",
16
+ NO_COMMITS = "NO_COMMITS",
17
+ NO_REMOTE = "NO_REMOTE",
18
+ UNPARSEABLE_REMOTE = "UNPARSEABLE_REMOTE",
19
+ LOCAL_PATH_REMOTE = "LOCAL_PATH_REMOTE",
20
+ NOT_CONNECTED = "NOT_CONNECTED",
21
+ CONFIG_CORRUPT = "CONFIG_CORRUPT",
22
+ AUTH_INVALID = "AUTH_INVALID",
23
+ CLAUDE_NOT_INSTALLED = "CLAUDE_NOT_INSTALLED",
24
+ CLAUDE_MCP_MISSING = "CLAUDE_MCP_MISSING",
25
+ CLAUDE_AUTH_FAILED = "CLAUDE_AUTH_FAILED",
26
+ NETWORK = "NETWORK",
27
+ HUB_UNREACHABLE = "HUB_UNREACHABLE",
28
+ REPO_NOT_ON_HUB = "REPO_NOT_ON_HUB",
29
+ REPO_NOT_INDEXED = "REPO_NOT_INDEXED",
30
+ REPO_READ_ONLY = "REPO_READ_ONLY",
31
+ SESSION_CONFLICT = "SESSION_CONFLICT",
32
+ RATE_LIMITED = "RATE_LIMITED",
33
+ PAGE_VALIDATION = "PAGE_VALIDATION",
34
+ PAGE_TOO_LARGE = "PAGE_TOO_LARGE",
35
+ GENERATION_FAILED = "GENERATION_FAILED",
36
+ USER_ABORTED = "USER_ABORTED",
37
+ UNKNOWN = "UNKNOWN"
38
+ }
39
+ export interface GnxErrorOptions {
40
+ hint?: string;
41
+ cause?: unknown;
42
+ }
43
+ export declare class GnxError extends Error {
44
+ readonly code: ErrorCode;
45
+ readonly exitCode: number;
46
+ readonly hint?: string;
47
+ readonly cause?: unknown;
48
+ constructor(code: ErrorCode, message: string, opts?: GnxErrorOptions);
49
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Typed errors with stable exit codes for `gnx wiki *` commands.
3
+ *
4
+ * Exit code ranges (keep stable — used by shell scripts / CI):
5
+ * 10-19 local git problems
6
+ * 20-29 repo resolution / Hub repo state
7
+ * 30-39 auth / config problems
8
+ * 40-49 Claude Code problems
9
+ * 50-59 network / Hub reachability
10
+ * 60-69 permissions (403, read-only)
11
+ * 70-79 session conflict / rate limit
12
+ * 80-89 upload validation / in-flight failures
13
+ */
14
+ export var ErrorCode;
15
+ (function (ErrorCode) {
16
+ ErrorCode["NOT_GIT_REPO"] = "NOT_GIT_REPO";
17
+ ErrorCode["NO_COMMITS"] = "NO_COMMITS";
18
+ ErrorCode["NO_REMOTE"] = "NO_REMOTE";
19
+ ErrorCode["UNPARSEABLE_REMOTE"] = "UNPARSEABLE_REMOTE";
20
+ ErrorCode["LOCAL_PATH_REMOTE"] = "LOCAL_PATH_REMOTE";
21
+ ErrorCode["NOT_CONNECTED"] = "NOT_CONNECTED";
22
+ ErrorCode["CONFIG_CORRUPT"] = "CONFIG_CORRUPT";
23
+ ErrorCode["AUTH_INVALID"] = "AUTH_INVALID";
24
+ ErrorCode["CLAUDE_NOT_INSTALLED"] = "CLAUDE_NOT_INSTALLED";
25
+ ErrorCode["CLAUDE_MCP_MISSING"] = "CLAUDE_MCP_MISSING";
26
+ ErrorCode["CLAUDE_AUTH_FAILED"] = "CLAUDE_AUTH_FAILED";
27
+ ErrorCode["NETWORK"] = "NETWORK";
28
+ ErrorCode["HUB_UNREACHABLE"] = "HUB_UNREACHABLE";
29
+ ErrorCode["REPO_NOT_ON_HUB"] = "REPO_NOT_ON_HUB";
30
+ ErrorCode["REPO_NOT_INDEXED"] = "REPO_NOT_INDEXED";
31
+ ErrorCode["REPO_READ_ONLY"] = "REPO_READ_ONLY";
32
+ ErrorCode["SESSION_CONFLICT"] = "SESSION_CONFLICT";
33
+ ErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
34
+ ErrorCode["PAGE_VALIDATION"] = "PAGE_VALIDATION";
35
+ ErrorCode["PAGE_TOO_LARGE"] = "PAGE_TOO_LARGE";
36
+ ErrorCode["GENERATION_FAILED"] = "GENERATION_FAILED";
37
+ ErrorCode["USER_ABORTED"] = "USER_ABORTED";
38
+ ErrorCode["UNKNOWN"] = "UNKNOWN";
39
+ })(ErrorCode || (ErrorCode = {}));
40
+ const EXIT_CODES = {
41
+ [ErrorCode.NOT_GIT_REPO]: 10,
42
+ [ErrorCode.NO_COMMITS]: 11,
43
+ [ErrorCode.NO_REMOTE]: 12,
44
+ [ErrorCode.UNPARSEABLE_REMOTE]: 13,
45
+ [ErrorCode.LOCAL_PATH_REMOTE]: 14,
46
+ [ErrorCode.REPO_NOT_INDEXED]: 20,
47
+ [ErrorCode.REPO_NOT_ON_HUB]: 21,
48
+ [ErrorCode.NOT_CONNECTED]: 30,
49
+ [ErrorCode.CONFIG_CORRUPT]: 31,
50
+ [ErrorCode.AUTH_INVALID]: 32,
51
+ [ErrorCode.CLAUDE_NOT_INSTALLED]: 40,
52
+ [ErrorCode.CLAUDE_MCP_MISSING]: 41,
53
+ [ErrorCode.CLAUDE_AUTH_FAILED]: 42,
54
+ [ErrorCode.NETWORK]: 50,
55
+ [ErrorCode.HUB_UNREACHABLE]: 51,
56
+ [ErrorCode.REPO_READ_ONLY]: 62,
57
+ [ErrorCode.SESSION_CONFLICT]: 70,
58
+ [ErrorCode.RATE_LIMITED]: 72,
59
+ [ErrorCode.PAGE_VALIDATION]: 80,
60
+ [ErrorCode.PAGE_TOO_LARGE]: 81,
61
+ [ErrorCode.GENERATION_FAILED]: 82,
62
+ [ErrorCode.USER_ABORTED]: 130,
63
+ [ErrorCode.UNKNOWN]: 1,
64
+ };
65
+ export class GnxError extends Error {
66
+ code;
67
+ exitCode;
68
+ hint;
69
+ cause;
70
+ constructor(code, message, opts = {}) {
71
+ super(message);
72
+ this.name = 'GnxError';
73
+ this.code = code;
74
+ this.exitCode = EXIT_CODES[code];
75
+ this.hint = opts.hint;
76
+ this.cause = opts.cause;
77
+ }
78
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerWikiCommand(program: Command): void;
@@ -0,0 +1,118 @@
1
+ import pc from 'picocolors';
2
+ import { ok, info, warn, fail } from '../cli-helpers.js';
3
+ import { buildRealDeps, REAL_DEFAULT_MODEL } from './real-deps.js';
4
+ import { runWikiUpload } from './upload-command.js';
5
+ import { runWikiStatus } from './status-command.js';
6
+ import { runWikiAbort } from './abort-command.js';
7
+ import { GnxError } from './errors.js';
8
+ function reportError(err) {
9
+ if (err instanceof GnxError) {
10
+ fail(err.message);
11
+ if (err.hint)
12
+ console.error(` ${pc.dim(err.hint)}`);
13
+ process.exit(err.exitCode);
14
+ }
15
+ fail(err instanceof Error ? err.message : String(err));
16
+ process.exit(1);
17
+ }
18
+ export function registerWikiCommand(program) {
19
+ const wiki = program
20
+ .command('wiki')
21
+ .description('Generate the wiki locally with Claude Code and upload it to the Hub')
22
+ .option('--mode <mode>', 'full | incremental', 'full')
23
+ .option('--model <model>', 'Claude model to use', REAL_DEFAULT_MODEL)
24
+ .action(async (opts) => {
25
+ const cwd = process.cwd();
26
+ const deps = buildRealDeps(cwd);
27
+ if (opts.model)
28
+ deps.model = opts.model;
29
+ const ac = new AbortController();
30
+ let sigCount = 0;
31
+ let forceExitTimer = null;
32
+ const onSig = () => {
33
+ sigCount++;
34
+ if (sigCount === 1) {
35
+ warn('interrupt received — cleaning up (Ctrl+C again to force quit)...');
36
+ ac.abort();
37
+ forceExitTimer = setTimeout(() => {
38
+ warn('cleanup timed out, forcing exit');
39
+ process.exit(130);
40
+ }, 3000);
41
+ forceExitTimer.unref();
42
+ }
43
+ else {
44
+ if (forceExitTimer)
45
+ clearTimeout(forceExitTimer);
46
+ process.exit(130);
47
+ }
48
+ };
49
+ process.on('SIGINT', onSig);
50
+ process.on('SIGTERM', onSig);
51
+ try {
52
+ info('Resolving repo context...');
53
+ const result = await runWikiUpload({ cwd, mode: opts.mode, model: opts.model, abortSignal: ac.signal }, deps);
54
+ ok(`Uploaded ${result.pagesPersisted} pages.`);
55
+ if (result.failedSlugs.length > 0) {
56
+ warn(`${result.failedSlugs.length} page(s) failed: ${result.failedSlugs.join(', ')}`);
57
+ }
58
+ }
59
+ catch (err) {
60
+ if (ac.signal.aborted) {
61
+ warn('aborted');
62
+ process.exit(130);
63
+ }
64
+ reportError(err);
65
+ }
66
+ finally {
67
+ if (forceExitTimer)
68
+ clearTimeout(forceExitTimer);
69
+ process.off('SIGINT', onSig);
70
+ process.off('SIGTERM', onSig);
71
+ }
72
+ });
73
+ wiki
74
+ .command('status')
75
+ .description('Show the wiki upload status for the current repo')
76
+ .action(async () => {
77
+ try {
78
+ const deps = buildRealDeps(process.cwd());
79
+ const status = await runWikiStatus(deps);
80
+ info(`Repo: ${pc.bold(status.fullName)} (${status.canonicalRemote})`);
81
+ info(`Hub indexed commit: ${status.hubIndexedCommit ?? '(none)'}`);
82
+ if (status.active) {
83
+ info(`Active session: ${pc.yellow(status.active.id)} ` +
84
+ `started ${status.active.startedAt}, ` +
85
+ `${status.active.pagesReceived} pages received`);
86
+ }
87
+ else {
88
+ info('No active upload session.');
89
+ }
90
+ if (status.last) {
91
+ info(`Last session: ${status.last.status}, ` +
92
+ `${status.last.pagesReceived} pages, ` +
93
+ `started ${status.last.startedAt}`);
94
+ }
95
+ }
96
+ catch (err) {
97
+ reportError(err);
98
+ }
99
+ });
100
+ wiki
101
+ .command('abort')
102
+ .description('Abort the active wiki upload session for the current repo')
103
+ .action(async () => {
104
+ try {
105
+ const deps = buildRealDeps(process.cwd());
106
+ const result = await runWikiAbort(deps);
107
+ if ('noActive' in result) {
108
+ info('No active session to abort.');
109
+ }
110
+ else {
111
+ ok(`Aborted session ${result.sessionId}`);
112
+ }
113
+ }
114
+ catch (err) {
115
+ reportError(err);
116
+ }
117
+ });
118
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Prompt templates for client-side wiki generation via Claude Code headless.
3
+ *
4
+ * The runner passes these prompts to Claude Code with the user's repo as cwd
5
+ * and read-only tools (Read, Grep) plus the gitnexus MCP tools available.
6
+ * Graph context comes from the Hub via MCP — no local LadybugDB needed.
7
+ */
8
+ export declare function renderModuleTreePrompt(fullName: string): string;
9
+ export declare function renderPagePrompt(fullName: string, moduleSlug: string, moduleTitle: string): string;
10
+ export declare const MODULE_TREE_PROMPT_TEMPLATE: string;
11
+ export declare const PAGE_PROMPT_TEMPLATE: string;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Prompt templates for client-side wiki generation via Claude Code headless.
3
+ *
4
+ * The runner passes these prompts to Claude Code with the user's repo as cwd
5
+ * and read-only tools (Read, Grep) plus the gitnexus MCP tools available.
6
+ * Graph context comes from the Hub via MCP — no local LadybugDB needed.
7
+ */
8
+ export function renderModuleTreePrompt(fullName) {
9
+ return `You are generating a repository wiki for the repo "${fullName}".
10
+
11
+ Your task: produce a JSON module hierarchy for this repo.
12
+
13
+ Use these MCP tools (they query the live Hub-indexed graph):
14
+ - mcp__gitnexus__context — overview of the repo
15
+ - mcp__gitnexus__group_list — existing cluster/module groupings
16
+ - mcp__gitnexus__query — concept/code search
17
+
18
+ Output ONLY valid JSON with this exact shape (no prose, no markdown fences):
19
+
20
+ {
21
+ "modules": [
22
+ {
23
+ "slug": "kebab-case-slug",
24
+ "title": "Human-Readable Title",
25
+ "summary": "One-sentence description",
26
+ "files": ["optional/representative/file.ts"]
27
+ }
28
+ ]
29
+ }
30
+
31
+ Rules:
32
+ - 6–15 top-level modules (adjust based on repo size).
33
+ - "slug" MUST match ^[a-z0-9][a-z0-9/_-]*$ and be unique within the array.
34
+ - Include an "overview" module as the first entry.
35
+ - Group by responsibility, not file type. "parser" > "typescript-files".
36
+ - No duplicates, no empty strings, no trailing punctuation in titles.`;
37
+ }
38
+ export function renderPagePrompt(fullName, moduleSlug, moduleTitle) {
39
+ return `You are writing a single wiki page for the repo "${fullName}".
40
+
41
+ Module: "${moduleSlug}" (${moduleTitle})
42
+
43
+ Use these tools freely:
44
+ - mcp__gitnexus__context — caller/callee graph for symbols
45
+ - mcp__gitnexus__query — concept search across the codebase
46
+ - mcp__gitnexus__group_contracts — structural facts about a group
47
+ - Read, Grep — inspect local files in the repo for concrete snippets
48
+
49
+ Output: a single Markdown document starting with "# ${moduleTitle}" as the H1.
50
+
51
+ Structure:
52
+ # ${moduleTitle}
53
+
54
+ One-paragraph summary.
55
+
56
+ ## Responsibilities
57
+ - Bulleted list of what this module owns.
58
+
59
+ ## Key Components
60
+ Short sections per major symbol/class/function with a brief explanation.
61
+ Include short code blocks (<= 20 lines) from the actual repo when illustrative.
62
+
63
+ ## How It Fits In
64
+ How this module connects to the rest of the system (callers, callees, flows).
65
+
66
+ Rules:
67
+ - Return ONLY the markdown — no preamble, no conclusion, no "Here's the wiki page".
68
+ - Do not invent symbols or file paths. If you're unsure, query the graph.
69
+ - Keep the total length under 50 KB.`;
70
+ }
71
+ export const MODULE_TREE_PROMPT_TEMPLATE = renderModuleTreePrompt('__FULL_NAME__');
72
+ export const PAGE_PROMPT_TEMPLATE = renderPagePrompt('__FULL_NAME__', '__SLUG__', '__TITLE__');