@fentech/gitcontext 0.3.3 → 0.5.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/README.md +392 -0
- package/dist/index.js +1141 -41
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14844,6 +14844,49 @@ var {
|
|
|
14844
14844
|
Option,
|
|
14845
14845
|
Help
|
|
14846
14846
|
} = import__.default;
|
|
14847
|
+
// package.json
|
|
14848
|
+
var package_default = {
|
|
14849
|
+
name: "@fentech/gitcontext",
|
|
14850
|
+
version: "0.4.0",
|
|
14851
|
+
description: "GitContext CLI \u2014 capture, sync, and query git-backed knowledge docs",
|
|
14852
|
+
type: "module",
|
|
14853
|
+
bin: {
|
|
14854
|
+
gitcontext: "./dist/index.js"
|
|
14855
|
+
},
|
|
14856
|
+
files: [
|
|
14857
|
+
"dist",
|
|
14858
|
+
"src/templates"
|
|
14859
|
+
],
|
|
14860
|
+
scripts: {
|
|
14861
|
+
build: "bun build src/index.ts --outfile dist/index.js --target bun",
|
|
14862
|
+
"build:dev": "bun build src/index.ts --outfile dist/index.dev.js --target bun",
|
|
14863
|
+
dev: "bun build src/index.ts --outfile dist/index.dev.js --target bun --watch",
|
|
14864
|
+
typecheck: "tsc --noEmit",
|
|
14865
|
+
start: "bun run src/index.ts",
|
|
14866
|
+
lint: "biome check .",
|
|
14867
|
+
"lint:fix": "biome check --write .",
|
|
14868
|
+
test: "bun test",
|
|
14869
|
+
"test:watch": "bun test --watch",
|
|
14870
|
+
"release:patch": `npm version patch --no-workspaces --no-git-tag-version && NEW_V=$(node -p 'require("./package.json").version') && git add package.json && SKIP_SIMPLE_GIT_HOOKS=1 git commit -m "chore(cli): bump to v$NEW_V" && git tag "v$NEW_V" && git push origin main --tags`,
|
|
14871
|
+
"release:minor": `npm version minor --no-workspaces --no-git-tag-version && NEW_V=$(node -p 'require("./package.json").version') && git add package.json && SKIP_SIMPLE_GIT_HOOKS=1 git commit -m "chore(cli): bump to v$NEW_V" && git tag "v$NEW_V" && git push origin main --tags`,
|
|
14872
|
+
"release:major": `npm version major --no-workspaces --no-git-tag-version && NEW_V=$(node -p 'require("./package.json").version') && git add package.json && SKIP_SIMPLE_GIT_HOOKS=1 git commit -m "chore(cli): bump to v$NEW_V" && git tag "v$NEW_V" && git push origin main --tags`
|
|
14873
|
+
},
|
|
14874
|
+
dependencies: {
|
|
14875
|
+
"@anthropic-ai/sdk": "^0.71.2",
|
|
14876
|
+
"@inquirer/prompts": "^8.5.0",
|
|
14877
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
14878
|
+
chalk: "^5.6.2",
|
|
14879
|
+
commander: "^14.0.3",
|
|
14880
|
+
"fast-glob": "^3.3.3",
|
|
14881
|
+
zod: "^4.4.3"
|
|
14882
|
+
},
|
|
14883
|
+
devDependencies: {
|
|
14884
|
+
"@types/bun": "latest",
|
|
14885
|
+
"@types/node": "^22.10.2",
|
|
14886
|
+
"bun-types": "latest",
|
|
14887
|
+
typescript: "^6.0.2"
|
|
14888
|
+
}
|
|
14889
|
+
};
|
|
14847
14890
|
|
|
14848
14891
|
// src/commands/analyze.ts
|
|
14849
14892
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
@@ -17094,7 +17137,11 @@ function readActiveProfile(profileName) {
|
|
|
17094
17137
|
if (!profile || typeof profile.apiUrl !== "string" || typeof profile.userToken !== "string") {
|
|
17095
17138
|
return null;
|
|
17096
17139
|
}
|
|
17097
|
-
return {
|
|
17140
|
+
return {
|
|
17141
|
+
apiUrl: profile.apiUrl,
|
|
17142
|
+
userToken: profile.userToken,
|
|
17143
|
+
orgId: typeof profile.orgId === "string" ? profile.orgId : undefined
|
|
17144
|
+
};
|
|
17098
17145
|
}
|
|
17099
17146
|
function getActiveProfileName() {
|
|
17100
17147
|
const config = readProfilesConfig();
|
|
@@ -17134,7 +17181,8 @@ function readRepoConfig(workingDir) {
|
|
|
17134
17181
|
repoName: parsed.repoName,
|
|
17135
17182
|
platforms: Array.isArray(parsed.platforms) ? parsed.platforms.filter((p) => typeof p === "string") : undefined,
|
|
17136
17183
|
docProfiles: parsed.docProfiles && typeof parsed.docProfiles === "object" ? parsed.docProfiles : undefined,
|
|
17137
|
-
storageMode
|
|
17184
|
+
storageMode,
|
|
17185
|
+
docStorageModes: parsed.docStorageModes && typeof parsed.docStorageModes === "object" ? parsed.docStorageModes : undefined
|
|
17138
17186
|
};
|
|
17139
17187
|
}
|
|
17140
17188
|
function writeRepoConfig(workingDir, config) {
|
|
@@ -17145,7 +17193,8 @@ function writeRepoConfig(workingDir, config) {
|
|
|
17145
17193
|
}
|
|
17146
17194
|
const payload = {
|
|
17147
17195
|
...config,
|
|
17148
|
-
storageMode: normalizeStorageMode(config.storageMode)
|
|
17196
|
+
storageMode: normalizeStorageMode(config.storageMode),
|
|
17197
|
+
...config.docStorageModes ? { docStorageModes: config.docStorageModes } : {}
|
|
17149
17198
|
};
|
|
17150
17199
|
writeFileSync(join2(root, REPO_CONFIG_RELATIVE), `${JSON.stringify(payload, null, 2)}
|
|
17151
17200
|
`, "utf-8");
|
|
@@ -17158,6 +17207,10 @@ function normalizeStorageMode(value) {
|
|
|
17158
17207
|
}
|
|
17159
17208
|
|
|
17160
17209
|
// src/lib/api-client.ts
|
|
17210
|
+
var meCache = null;
|
|
17211
|
+
function getCachedMe() {
|
|
17212
|
+
return meCache;
|
|
17213
|
+
}
|
|
17161
17214
|
class ApiClientError extends Error {
|
|
17162
17215
|
status;
|
|
17163
17216
|
constructor(message, status) {
|
|
@@ -17170,26 +17223,33 @@ class ApiClientError extends Error {
|
|
|
17170
17223
|
class ApiClient {
|
|
17171
17224
|
apiUrl;
|
|
17172
17225
|
userToken;
|
|
17173
|
-
|
|
17226
|
+
orgId;
|
|
17227
|
+
constructor(apiUrl, userToken, orgId) {
|
|
17174
17228
|
this.apiUrl = apiUrl.replace(/\/$/, "");
|
|
17175
17229
|
this.userToken = userToken;
|
|
17230
|
+
this.orgId = orgId;
|
|
17176
17231
|
}
|
|
17177
17232
|
static fromProfile(profileName) {
|
|
17178
17233
|
const config = readActiveProfile(profileName);
|
|
17179
17234
|
if (!config) {
|
|
17180
17235
|
throw new ApiClientError("Not authenticated. Run `gitcontext auth login` first.", 401);
|
|
17181
17236
|
}
|
|
17182
|
-
return new ApiClient(config.apiUrl, config.userToken);
|
|
17237
|
+
return new ApiClient(config.apiUrl, config.userToken, config.orgId);
|
|
17183
17238
|
}
|
|
17184
17239
|
static fromUserConfig() {
|
|
17185
17240
|
const config = readUserConfig();
|
|
17186
17241
|
if (!config) {
|
|
17187
17242
|
throw new ApiClientError("Not authenticated. Run `gitcontext auth login` first.", 401);
|
|
17188
17243
|
}
|
|
17189
|
-
return new ApiClient(config.apiUrl, config.userToken);
|
|
17244
|
+
return new ApiClient(config.apiUrl, config.userToken, config.orgId);
|
|
17245
|
+
}
|
|
17246
|
+
static forPublic(apiUrl) {
|
|
17247
|
+
return new ApiClient(apiUrl, "");
|
|
17190
17248
|
}
|
|
17191
17249
|
async getMe() {
|
|
17192
|
-
|
|
17250
|
+
const me = await this.request("GET", "/api/me");
|
|
17251
|
+
meCache = me;
|
|
17252
|
+
return me;
|
|
17193
17253
|
}
|
|
17194
17254
|
async processDoc(input) {
|
|
17195
17255
|
return this.request("POST", "/api/process-doc", input);
|
|
@@ -17235,6 +17295,57 @@ class ApiClient {
|
|
|
17235
17295
|
async getDocRevision(docId, revisionNumber) {
|
|
17236
17296
|
return this.request("GET", `/api/docs/${encodeURIComponent(docId)}/revisions/${revisionNumber}`);
|
|
17237
17297
|
}
|
|
17298
|
+
async getInstallLink(repoName) {
|
|
17299
|
+
return this.request("GET", "/api/github/install-link", undefined, {
|
|
17300
|
+
repoName
|
|
17301
|
+
});
|
|
17302
|
+
}
|
|
17303
|
+
async listProjects(repoName) {
|
|
17304
|
+
return this.request("GET", "/api/projects", undefined, repoName ? { repoName } : undefined);
|
|
17305
|
+
}
|
|
17306
|
+
async estimateBackfill(params) {
|
|
17307
|
+
return this.request("GET", "/api/backfill/estimate", undefined, params);
|
|
17308
|
+
}
|
|
17309
|
+
async startPRBackfill(params) {
|
|
17310
|
+
return this.request("POST", "/api/backfill/start", params);
|
|
17311
|
+
}
|
|
17312
|
+
async getOrgDocStorageModes() {
|
|
17313
|
+
const result = await this.request("GET", "/api/org-doc-type-settings");
|
|
17314
|
+
return result.docStorageModes;
|
|
17315
|
+
}
|
|
17316
|
+
async getLatestChangelog(packageKey) {
|
|
17317
|
+
return this.requestPublic("GET", "/api/changelog/latest", {
|
|
17318
|
+
package: packageKey
|
|
17319
|
+
});
|
|
17320
|
+
}
|
|
17321
|
+
async getChangelog(packageKey, limit) {
|
|
17322
|
+
const query = { package: packageKey };
|
|
17323
|
+
if (limit !== undefined)
|
|
17324
|
+
query.limit = limit;
|
|
17325
|
+
return this.requestPublic("GET", "/api/changelog", query);
|
|
17326
|
+
}
|
|
17327
|
+
async requestPublic(method, path2, query) {
|
|
17328
|
+
const url = new URL(`${this.apiUrl}${path2}`);
|
|
17329
|
+
if (query) {
|
|
17330
|
+
for (const [key, value] of Object.entries(query)) {
|
|
17331
|
+
if (value !== undefined) {
|
|
17332
|
+
url.searchParams.set(key, String(value));
|
|
17333
|
+
}
|
|
17334
|
+
}
|
|
17335
|
+
}
|
|
17336
|
+
const response = await fetch(url, {
|
|
17337
|
+
method,
|
|
17338
|
+
headers: {
|
|
17339
|
+
"Content-Type": "application/json",
|
|
17340
|
+
Accept: "application/json"
|
|
17341
|
+
}
|
|
17342
|
+
});
|
|
17343
|
+
if (!response.ok) {
|
|
17344
|
+
const message = await response.text();
|
|
17345
|
+
throw new ApiClientError(message || response.statusText, response.status);
|
|
17346
|
+
}
|
|
17347
|
+
return await response.json();
|
|
17348
|
+
}
|
|
17238
17349
|
async request(method, path2, body, query) {
|
|
17239
17350
|
const url = new URL(`${this.apiUrl}${path2}`);
|
|
17240
17351
|
if (query) {
|
|
@@ -17248,6 +17359,7 @@ class ApiClient {
|
|
|
17248
17359
|
method,
|
|
17249
17360
|
headers: {
|
|
17250
17361
|
Authorization: `Bearer ${this.userToken}`,
|
|
17362
|
+
...this.orgId ? { "X-Org-Id": this.orgId } : {},
|
|
17251
17363
|
"Content-Type": "application/json",
|
|
17252
17364
|
Accept: "application/json"
|
|
17253
17365
|
},
|
|
@@ -17270,7 +17382,8 @@ var TYPE_META = {
|
|
|
17270
17382
|
bug: { emoji: "\uD83D\uDC1B", label: "Bug Discovery" },
|
|
17271
17383
|
convention: { emoji: "\uD83D\uDCD0", label: "Convention Change" },
|
|
17272
17384
|
blocker: { emoji: "\uD83D\uDEA7", label: "Blocker" },
|
|
17273
|
-
conversation: { emoji: "\uD83D\uDCAC", label: "Conversation" }
|
|
17385
|
+
conversation: { emoji: "\uD83D\uDCAC", label: "Conversation" },
|
|
17386
|
+
report: { emoji: "\uD83D\uDCCA", label: "Report" }
|
|
17274
17387
|
};
|
|
17275
17388
|
function typeMeta(type) {
|
|
17276
17389
|
return TYPE_META[type] ?? { emoji: "\uD83D\uDCC4", label: type };
|
|
@@ -17329,7 +17442,14 @@ function wrapLine(text, width) {
|
|
|
17329
17442
|
|
|
17330
17443
|
// src/lib/file-writer.ts
|
|
17331
17444
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
17332
|
-
import {
|
|
17445
|
+
import {
|
|
17446
|
+
copyFileSync,
|
|
17447
|
+
existsSync as existsSync3,
|
|
17448
|
+
mkdirSync as mkdirSync2,
|
|
17449
|
+
readdirSync,
|
|
17450
|
+
readFileSync as readFileSync2,
|
|
17451
|
+
writeFileSync as writeFileSync2
|
|
17452
|
+
} from "fs";
|
|
17333
17453
|
import { basename, join as join3 } from "path";
|
|
17334
17454
|
|
|
17335
17455
|
// src/lib/profiles.ts
|
|
@@ -17383,6 +17503,91 @@ ${structuredDoc.content.trim()}
|
|
|
17383
17503
|
}
|
|
17384
17504
|
return { filePath: relativePath, committed };
|
|
17385
17505
|
}
|
|
17506
|
+
function findDocFileById(id, workingDir = process.cwd()) {
|
|
17507
|
+
let root;
|
|
17508
|
+
try {
|
|
17509
|
+
root = resolveGitRoot(workingDir);
|
|
17510
|
+
} catch {
|
|
17511
|
+
return null;
|
|
17512
|
+
}
|
|
17513
|
+
const docsDir = join3(root, ".gitcontext", "docs");
|
|
17514
|
+
if (!existsSync3(docsDir))
|
|
17515
|
+
return null;
|
|
17516
|
+
let entries;
|
|
17517
|
+
try {
|
|
17518
|
+
entries = readdirSync(docsDir);
|
|
17519
|
+
} catch {
|
|
17520
|
+
return null;
|
|
17521
|
+
}
|
|
17522
|
+
for (const entry of entries) {
|
|
17523
|
+
if (!entry.endsWith(".md") && !entry.endsWith(".mdx"))
|
|
17524
|
+
continue;
|
|
17525
|
+
const absolutePath = join3(docsDir, entry);
|
|
17526
|
+
let raw;
|
|
17527
|
+
try {
|
|
17528
|
+
raw = readFileSync2(absolutePath, "utf-8");
|
|
17529
|
+
} catch {
|
|
17530
|
+
continue;
|
|
17531
|
+
}
|
|
17532
|
+
if (!raw.startsWith(`---
|
|
17533
|
+
`))
|
|
17534
|
+
continue;
|
|
17535
|
+
const end = raw.indexOf(`
|
|
17536
|
+
---`, 4);
|
|
17537
|
+
if (end === -1)
|
|
17538
|
+
continue;
|
|
17539
|
+
const yamlBlock = raw.slice(4, end);
|
|
17540
|
+
for (const line of yamlBlock.split(`
|
|
17541
|
+
`)) {
|
|
17542
|
+
if (!line.startsWith("id:"))
|
|
17543
|
+
continue;
|
|
17544
|
+
const val = line.slice(3).trim().replace(/^"|"$/g, "");
|
|
17545
|
+
if (val === id) {
|
|
17546
|
+
return { absolutePath, relativePath: join3(".gitcontext", "docs", entry) };
|
|
17547
|
+
}
|
|
17548
|
+
}
|
|
17549
|
+
}
|
|
17550
|
+
return null;
|
|
17551
|
+
}
|
|
17552
|
+
function overwriteDocFile(structuredDoc, absolutePath, relativePath, options = {}, workingDir = process.cwd()) {
|
|
17553
|
+
let gitRoot;
|
|
17554
|
+
try {
|
|
17555
|
+
gitRoot = resolveGitRoot(workingDir);
|
|
17556
|
+
} catch {
|
|
17557
|
+
gitRoot = workingDir;
|
|
17558
|
+
}
|
|
17559
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
17560
|
+
const docProfile = resolveDocProfile(structuredDoc.type, gitRoot);
|
|
17561
|
+
const frontmatter = {
|
|
17562
|
+
id: structuredDoc.id,
|
|
17563
|
+
title: structuredDoc.title,
|
|
17564
|
+
branch: getCurrentBranch(gitRoot),
|
|
17565
|
+
commit: getCurrentCommit(gitRoot),
|
|
17566
|
+
repo: getRepoName(gitRoot),
|
|
17567
|
+
author: options.author ?? "unknown",
|
|
17568
|
+
date,
|
|
17569
|
+
docProfile,
|
|
17570
|
+
...structuredDoc.frontmatter,
|
|
17571
|
+
type: structuredDoc.type
|
|
17572
|
+
};
|
|
17573
|
+
const body = `---
|
|
17574
|
+
${toYamlFrontmatter(frontmatter)}
|
|
17575
|
+
---
|
|
17576
|
+
|
|
17577
|
+
${structuredDoc.content.trim()}
|
|
17578
|
+
`;
|
|
17579
|
+
writeFileSync2(absolutePath, body, "utf-8");
|
|
17580
|
+
let committed = false;
|
|
17581
|
+
if (!options.noCommit) {
|
|
17582
|
+
execFileSync2("git", ["add", relativePath], { cwd: gitRoot, stdio: "inherit" });
|
|
17583
|
+
execFileSync2("git", ["commit", "-m", `docs: revise \u2014 ${structuredDoc.title}`], {
|
|
17584
|
+
cwd: gitRoot,
|
|
17585
|
+
stdio: "inherit"
|
|
17586
|
+
});
|
|
17587
|
+
committed = true;
|
|
17588
|
+
}
|
|
17589
|
+
return { filePath: relativePath, committed };
|
|
17590
|
+
}
|
|
17386
17591
|
function adoptDoc(input, repoRoot) {
|
|
17387
17592
|
const docsDir = join3(repoRoot, ".gitcontext", "docs");
|
|
17388
17593
|
if (!existsSync3(docsDir)) {
|
|
@@ -17775,7 +17980,8 @@ async function handleMultiDoc(docs, ctx) {
|
|
|
17775
17980
|
}
|
|
17776
17981
|
}
|
|
17777
17982
|
async function persistDoc(doc, ctx) {
|
|
17778
|
-
const
|
|
17983
|
+
const repoConfig = readRepoConfig(ctx.workingDir);
|
|
17984
|
+
const storageMode = repoConfig?.docStorageModes?.[doc.type] ?? "repo";
|
|
17779
17985
|
let filePath;
|
|
17780
17986
|
if (storageMode === "cloud") {
|
|
17781
17987
|
filePath = buildCloudFilePath(doc);
|
|
@@ -17794,6 +18000,7 @@ async function persistDoc(doc, ctx) {
|
|
|
17794
18000
|
title: doc.title,
|
|
17795
18001
|
content: doc.content,
|
|
17796
18002
|
filePath,
|
|
18003
|
+
storageMode,
|
|
17797
18004
|
branch: ctx.branch,
|
|
17798
18005
|
commitHash: ctx.commitHash,
|
|
17799
18006
|
repoName: ctx.repoName,
|
|
@@ -18005,6 +18212,7 @@ async function runAnalyze(options) {
|
|
|
18005
18212
|
title: doc.title,
|
|
18006
18213
|
content: doc.content,
|
|
18007
18214
|
filePath,
|
|
18215
|
+
storageMode: "repo",
|
|
18008
18216
|
branch,
|
|
18009
18217
|
commitHash,
|
|
18010
18218
|
repoName,
|
|
@@ -18069,7 +18277,8 @@ async function startCallbackServer() {
|
|
|
18069
18277
|
}
|
|
18070
18278
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
18071
18279
|
res.end(renderHtml("\u2705 Authenticated. You can close this tab and return to your terminal."));
|
|
18072
|
-
|
|
18280
|
+
const orgId = url.searchParams.get("orgId") ?? undefined;
|
|
18281
|
+
resolveToken({ token, orgId });
|
|
18073
18282
|
});
|
|
18074
18283
|
await new Promise((resolve2) => server.listen(0, "127.0.0.1", resolve2));
|
|
18075
18284
|
const address = server.address();
|
|
@@ -18127,8 +18336,9 @@ async function login(options) {
|
|
|
18127
18336
|
`);
|
|
18128
18337
|
openBrowser(loginUrl.toString());
|
|
18129
18338
|
let token;
|
|
18339
|
+
let orgId;
|
|
18130
18340
|
try {
|
|
18131
|
-
token = await server.waitForToken();
|
|
18341
|
+
({ token, orgId } = await server.waitForToken());
|
|
18132
18342
|
} catch (error) {
|
|
18133
18343
|
server.close();
|
|
18134
18344
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -18137,7 +18347,7 @@ async function login(options) {
|
|
|
18137
18347
|
}
|
|
18138
18348
|
server.close();
|
|
18139
18349
|
const newConfig = profilesConfig ?? { active: profileName, profiles: {} };
|
|
18140
|
-
newConfig.profiles[profileName] = { apiUrl, userToken: token };
|
|
18350
|
+
newConfig.profiles[profileName] = { apiUrl, userToken: token, orgId };
|
|
18141
18351
|
newConfig.active = profileName;
|
|
18142
18352
|
writeProfilesConfig(newConfig);
|
|
18143
18353
|
try {
|
|
@@ -18203,11 +18413,14 @@ async function list() {
|
|
|
18203
18413
|
}
|
|
18204
18414
|
}
|
|
18205
18415
|
|
|
18416
|
+
// src/commands/backfill.ts
|
|
18417
|
+
import * as readline3 from "readline";
|
|
18418
|
+
|
|
18206
18419
|
// src/lib/backfill.ts
|
|
18207
18420
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
18208
18421
|
|
|
18209
18422
|
// src/lib/local-doc-scanner.ts
|
|
18210
|
-
import { readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
18423
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
18211
18424
|
import { join as join4, relative } from "path";
|
|
18212
18425
|
var EXCLUDED_DIRS = new Set([
|
|
18213
18426
|
"node_modules",
|
|
@@ -18235,7 +18448,7 @@ function walkDir(repoRoot, dir, docsDir, depth, maxDepth, results) {
|
|
|
18235
18448
|
return;
|
|
18236
18449
|
let entries;
|
|
18237
18450
|
try {
|
|
18238
|
-
entries =
|
|
18451
|
+
entries = readdirSync2(dir);
|
|
18239
18452
|
} catch {
|
|
18240
18453
|
return;
|
|
18241
18454
|
}
|
|
@@ -18361,6 +18574,7 @@ ${candidate.content.slice(0, 3000)}`,
|
|
|
18361
18574
|
title: c.title,
|
|
18362
18575
|
content: result.content,
|
|
18363
18576
|
filePath: result.relativePath,
|
|
18577
|
+
storageMode: "repo",
|
|
18364
18578
|
branch,
|
|
18365
18579
|
commitHash,
|
|
18366
18580
|
repoName,
|
|
@@ -18389,14 +18603,29 @@ ${candidate.content.slice(0, 3000)}`,
|
|
|
18389
18603
|
}
|
|
18390
18604
|
|
|
18391
18605
|
// src/commands/backfill.ts
|
|
18606
|
+
async function promptConfirm(question) {
|
|
18607
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
18608
|
+
return new Promise((resolve2) => {
|
|
18609
|
+
rl.question(question, (answer) => {
|
|
18610
|
+
rl.close();
|
|
18611
|
+
resolve2(answer.trim().toLowerCase() === "y");
|
|
18612
|
+
});
|
|
18613
|
+
});
|
|
18614
|
+
}
|
|
18392
18615
|
function registerBackfillCommand(program2) {
|
|
18393
|
-
program2.command("backfill").description("
|
|
18616
|
+
program2.command("backfill").description("Seed your knowledge base from existing docs or PR history").option("--local", "Adopt local .md/.mdx files found outside .gitcontext/docs/").option("--from-prs", "Generate draft docs from your last 6 months of merged PRs").option("--months <n>", "Number of months of PR history to analyze (default: 6)", "6").option("--max-prs <n>", "Maximum number of PRs to analyze (default: 20, max: 50)", "20").action(async (options) => {
|
|
18617
|
+
if (options.fromPrs) {
|
|
18618
|
+
await runBackfillFromPRs(options);
|
|
18619
|
+
return;
|
|
18620
|
+
}
|
|
18394
18621
|
if (!options.local) {
|
|
18395
|
-
console.error("
|
|
18622
|
+
console.error("Specify a backfill mode:");
|
|
18623
|
+
console.error(" gitcontext backfill --local Adopt existing markdown docs");
|
|
18624
|
+
console.error(" gitcontext backfill --from-prs Generate docs from PR history");
|
|
18396
18625
|
process.exitCode = 1;
|
|
18397
18626
|
return;
|
|
18398
18627
|
}
|
|
18399
|
-
const config =
|
|
18628
|
+
const config = readActiveProfile();
|
|
18400
18629
|
if (!config) {
|
|
18401
18630
|
console.error("Not authenticated. Run `gitcontext auth login` first.");
|
|
18402
18631
|
process.exitCode = 1;
|
|
@@ -18406,6 +18635,66 @@ function registerBackfillCommand(program2) {
|
|
|
18406
18635
|
await runBackfill(client, process.cwd());
|
|
18407
18636
|
});
|
|
18408
18637
|
}
|
|
18638
|
+
async function runBackfillFromPRs(options) {
|
|
18639
|
+
const config = readActiveProfile();
|
|
18640
|
+
if (!config) {
|
|
18641
|
+
console.error("Not authenticated. Run `gitcontext auth login` first.");
|
|
18642
|
+
process.exitCode = 1;
|
|
18643
|
+
return;
|
|
18644
|
+
}
|
|
18645
|
+
const monthsBack = Math.max(1, Math.min(12, Number(options.months ?? "6") || 6));
|
|
18646
|
+
const maxPRs = Math.max(1, Math.min(50, Number(options.maxPrs ?? "20") || 20));
|
|
18647
|
+
let repoName;
|
|
18648
|
+
let repoFullName;
|
|
18649
|
+
try {
|
|
18650
|
+
repoName = getRepoName(process.cwd());
|
|
18651
|
+
repoFullName = repoName;
|
|
18652
|
+
} catch {
|
|
18653
|
+
console.error("Could not detect repository. Run from inside a git repo.");
|
|
18654
|
+
process.exitCode = 1;
|
|
18655
|
+
return;
|
|
18656
|
+
}
|
|
18657
|
+
const client = new ApiClient(config.apiUrl, config.userToken);
|
|
18658
|
+
let estimate;
|
|
18659
|
+
try {
|
|
18660
|
+
estimate = await client.estimateBackfill({ monthsBack, maxPRs });
|
|
18661
|
+
} catch (err) {
|
|
18662
|
+
const msg = err instanceof ApiClientError ? err.message : String(err);
|
|
18663
|
+
console.error(`Failed to fetch backfill estimate: ${msg}`);
|
|
18664
|
+
process.exitCode = 1;
|
|
18665
|
+
return;
|
|
18666
|
+
}
|
|
18667
|
+
console.log(`
|
|
18668
|
+
PR history backfill for ${repoName}`);
|
|
18669
|
+
console.log(` Months of history : ${monthsBack}`);
|
|
18670
|
+
console.log(` PRs to analyze : up to ${estimate.estimatedPRs}`);
|
|
18671
|
+
console.log(` Estimated tokens : ~${estimate.estimatedTokens.toLocaleString()}`);
|
|
18672
|
+
console.log(` Estimated cost : ~$${(estimate.estimatedCostCents / 100).toFixed(2)}
|
|
18673
|
+
`);
|
|
18674
|
+
const confirmed = await promptConfirm("Proceed? [y/N]: ");
|
|
18675
|
+
if (!confirmed) {
|
|
18676
|
+
console.log("Backfill cancelled.");
|
|
18677
|
+
return;
|
|
18678
|
+
}
|
|
18679
|
+
console.log(`
|
|
18680
|
+
Queuing backfill job\u2026`);
|
|
18681
|
+
let result;
|
|
18682
|
+
try {
|
|
18683
|
+
result = await client.startPRBackfill({ repoName, repoFullName, monthsBack, maxPRs });
|
|
18684
|
+
} catch (err) {
|
|
18685
|
+
const msg = err instanceof ApiClientError ? err.message : String(err);
|
|
18686
|
+
console.error(`Backfill failed to start: ${msg}`);
|
|
18687
|
+
process.exitCode = 1;
|
|
18688
|
+
return;
|
|
18689
|
+
}
|
|
18690
|
+
console.log("Backfill queued successfully!");
|
|
18691
|
+
console.log(`Session ID: ${result.sessionId}`);
|
|
18692
|
+
console.log(`
|
|
18693
|
+
Once complete, review suggestions at:
|
|
18694
|
+
${result.reviewUrl}
|
|
18695
|
+
`);
|
|
18696
|
+
console.log("Tip: check status with `gitcontext status`.");
|
|
18697
|
+
}
|
|
18409
18698
|
|
|
18410
18699
|
// src/commands/blockers.ts
|
|
18411
18700
|
function handleError(error) {
|
|
@@ -18529,6 +18818,117 @@ Cancelled.`);
|
|
|
18529
18818
|
});
|
|
18530
18819
|
}
|
|
18531
18820
|
|
|
18821
|
+
// src/commands/changelog.ts
|
|
18822
|
+
import { spawnSync } from "child_process";
|
|
18823
|
+
var PRODUCTION_API_URL = "https://app.gitcontext.app";
|
|
18824
|
+
var WRAP_WIDTH = 72;
|
|
18825
|
+
function registerChangelogCommand(program2) {
|
|
18826
|
+
program2.command("changelog").description("Display CLI release notes").option("--json", "Output raw JSON array").option("--limit <n>", "Show only the N most recent entries", Number).action(async (opts) => {
|
|
18827
|
+
await runChangelog(opts);
|
|
18828
|
+
});
|
|
18829
|
+
}
|
|
18830
|
+
async function runChangelog(opts) {
|
|
18831
|
+
const profile = readActiveProfile();
|
|
18832
|
+
const apiUrl = profile?.apiUrl ?? PRODUCTION_API_URL;
|
|
18833
|
+
const client = ApiClient.forPublic(apiUrl);
|
|
18834
|
+
let entries;
|
|
18835
|
+
try {
|
|
18836
|
+
entries = await client.getChangelog("cli", opts.limit);
|
|
18837
|
+
} catch {
|
|
18838
|
+
process.stderr.write(`Could not fetch changelog. Visit https://gitcontext.dev/changelog for the latest updates.
|
|
18839
|
+
`);
|
|
18840
|
+
process.exitCode = 1;
|
|
18841
|
+
return;
|
|
18842
|
+
}
|
|
18843
|
+
if (opts.json) {
|
|
18844
|
+
process.stdout.write(`${JSON.stringify(entries, null, 2)}
|
|
18845
|
+
`);
|
|
18846
|
+
return;
|
|
18847
|
+
}
|
|
18848
|
+
const isTTY = process.stdout.isTTY === true;
|
|
18849
|
+
const output = isTTY ? buildColorOutput(entries) : buildPlainOutput(entries);
|
|
18850
|
+
if (isTTY) {
|
|
18851
|
+
const lessResult = spawnSync("less", ["-R"], {
|
|
18852
|
+
input: output,
|
|
18853
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
18854
|
+
});
|
|
18855
|
+
const lessNotFound = lessResult.error && lessResult.error.code === "ENOENT";
|
|
18856
|
+
if (lessNotFound) {
|
|
18857
|
+
const moreResult = spawnSync("more", [], {
|
|
18858
|
+
input: output,
|
|
18859
|
+
stdio: ["pipe", "inherit", "inherit"]
|
|
18860
|
+
});
|
|
18861
|
+
const moreNotFound = moreResult.error && moreResult.error.code === "ENOENT";
|
|
18862
|
+
if (moreNotFound) {
|
|
18863
|
+
process.stdout.write(output);
|
|
18864
|
+
}
|
|
18865
|
+
}
|
|
18866
|
+
} else {
|
|
18867
|
+
process.stdout.write(output);
|
|
18868
|
+
}
|
|
18869
|
+
}
|
|
18870
|
+
function wrapBullet(text, firstPrefix, hangPrefix) {
|
|
18871
|
+
const words = text.split(/\s+/).filter(Boolean);
|
|
18872
|
+
const lines = [];
|
|
18873
|
+
let line = firstPrefix;
|
|
18874
|
+
for (const word of words) {
|
|
18875
|
+
if (line === firstPrefix) {
|
|
18876
|
+
line += word;
|
|
18877
|
+
} else if (line.length + 1 + word.length <= WRAP_WIDTH) {
|
|
18878
|
+
line += ` ${word}`;
|
|
18879
|
+
} else {
|
|
18880
|
+
lines.push(line);
|
|
18881
|
+
line = hangPrefix + word;
|
|
18882
|
+
}
|
|
18883
|
+
}
|
|
18884
|
+
if (line !== firstPrefix)
|
|
18885
|
+
lines.push(line);
|
|
18886
|
+
return lines.join(`
|
|
18887
|
+
`);
|
|
18888
|
+
}
|
|
18889
|
+
function parseMarkdownBullets(notes) {
|
|
18890
|
+
return notes.split(`
|
|
18891
|
+
`).map((line) => line.trim()).filter((line) => /^[-*+]\s+/.test(line)).map((line) => line.replace(/^[-*+]\s+/, ""));
|
|
18892
|
+
}
|
|
18893
|
+
function buildPlainOutput(entries) {
|
|
18894
|
+
const divider = "\u2500".repeat(WRAP_WIDTH);
|
|
18895
|
+
const parts = [];
|
|
18896
|
+
for (const entry of entries) {
|
|
18897
|
+
parts.push(divider);
|
|
18898
|
+
parts.push(`${entry.date} \u2014 ${entry.headline}`);
|
|
18899
|
+
if (entry.notes) {
|
|
18900
|
+
const bullets = parseMarkdownBullets(entry.notes);
|
|
18901
|
+
for (const bullet of bullets) {
|
|
18902
|
+
parts.push(wrapBullet(bullet, " \u2022 ", " "));
|
|
18903
|
+
}
|
|
18904
|
+
}
|
|
18905
|
+
parts.push("");
|
|
18906
|
+
}
|
|
18907
|
+
if (parts.length > 0)
|
|
18908
|
+
parts.push(divider);
|
|
18909
|
+
return parts.join(`
|
|
18910
|
+
`);
|
|
18911
|
+
}
|
|
18912
|
+
function buildColorOutput(entries) {
|
|
18913
|
+
const divider = source_default.dim("\u2500".repeat(WRAP_WIDTH));
|
|
18914
|
+
const parts = [];
|
|
18915
|
+
for (const entry of entries) {
|
|
18916
|
+
parts.push(divider);
|
|
18917
|
+
parts.push(source_default.bold(`${entry.date} \u2014 ${entry.headline}`));
|
|
18918
|
+
if (entry.notes) {
|
|
18919
|
+
const bullets = parseMarkdownBullets(entry.notes);
|
|
18920
|
+
for (const bullet of bullets) {
|
|
18921
|
+
parts.push(wrapBullet(bullet, ` ${source_default.dim("\u2022")} `, " "));
|
|
18922
|
+
}
|
|
18923
|
+
}
|
|
18924
|
+
parts.push("");
|
|
18925
|
+
}
|
|
18926
|
+
if (parts.length > 0)
|
|
18927
|
+
parts.push(divider);
|
|
18928
|
+
return parts.join(`
|
|
18929
|
+
`);
|
|
18930
|
+
}
|
|
18931
|
+
|
|
18532
18932
|
// src/commands/docs.ts
|
|
18533
18933
|
function handleError2(error) {
|
|
18534
18934
|
if (error instanceof ApiClientError) {
|
|
@@ -18716,10 +19116,10 @@ ${meta.emoji} ${meta.label} \u2014 ${doc.title}`);
|
|
|
18716
19116
|
}
|
|
18717
19117
|
|
|
18718
19118
|
// src/commands/init.ts
|
|
18719
|
-
import { execFileSync as execFileSync5, spawnSync } from "child_process";
|
|
19119
|
+
import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "child_process";
|
|
18720
19120
|
|
|
18721
19121
|
// src/lib/platform-installer.ts
|
|
18722
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as
|
|
19122
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
18723
19123
|
import { homedir as homedir2 } from "os";
|
|
18724
19124
|
import { dirname, join as join5 } from "path";
|
|
18725
19125
|
var TEMPLATES_DIR = join5(import.meta.dir, "..", "templates");
|
|
@@ -18777,7 +19177,7 @@ function copyTemplateDir(srcDir, destDir, global3 = false) {
|
|
|
18777
19177
|
if (!existsSync5(srcDir))
|
|
18778
19178
|
return [];
|
|
18779
19179
|
const files = [];
|
|
18780
|
-
for (const entry of
|
|
19180
|
+
for (const entry of readdirSync3(srcDir, { withFileTypes: true })) {
|
|
18781
19181
|
if (entry.isFile()) {
|
|
18782
19182
|
const content = readFileSync5(join5(srcDir, entry.name), "utf-8");
|
|
18783
19183
|
files.push(writeFileSafe(join5(destDir, entry.name), content, global3));
|
|
@@ -18870,7 +19270,6 @@ var BANNER = `
|
|
|
18870
19270
|
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557 \u2588\u2588\u2551
|
|
18871
19271
|
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D
|
|
18872
19272
|
`;
|
|
18873
|
-
var GITHUB_APP_URL = process.env.GITCONTEXT_GITHUB_APP_URL ?? "https://github.com/apps/gitcontext/installations/new";
|
|
18874
19273
|
var PLATFORM_CHOICES = [
|
|
18875
19274
|
{ value: "claude-code", name: "Claude Code" },
|
|
18876
19275
|
{ value: "cursor", name: "Cursor" },
|
|
@@ -18961,19 +19360,80 @@ Setting up GitContext for ${repoName}...
|
|
|
18961
19360
|
`);
|
|
18962
19361
|
}
|
|
18963
19362
|
}
|
|
18964
|
-
const
|
|
18965
|
-
|
|
18966
|
-
|
|
18967
|
-
|
|
18968
|
-
|
|
18969
|
-
|
|
18970
|
-
|
|
19363
|
+
const apiClientForGH = new ApiClient(userConfig.apiUrl, userConfig.userToken, userConfig.orgId);
|
|
19364
|
+
let alreadyConnected = false;
|
|
19365
|
+
try {
|
|
19366
|
+
const { projects: existingProjects } = await apiClientForGH.listProjects(repoName);
|
|
19367
|
+
if (existingProjects.length > 0) {
|
|
19368
|
+
alreadyConnected = true;
|
|
19369
|
+
console.log(source_default.green(`
|
|
19370
|
+
\u2713 GitHub App already installed \u2014 ${repoName} is connected`));
|
|
19371
|
+
}
|
|
19372
|
+
} catch {}
|
|
19373
|
+
if (!alreadyConnected) {
|
|
19374
|
+
const installApp = await dist_default5({
|
|
19375
|
+
message: "Install the GitContext GitHub App to enable PR analysis and doc suggestions?",
|
|
19376
|
+
default: true
|
|
19377
|
+
});
|
|
19378
|
+
if (installApp) {
|
|
19379
|
+
let installUrl;
|
|
19380
|
+
let usePolling = true;
|
|
19381
|
+
try {
|
|
19382
|
+
const { url } = await apiClientForGH.getInstallLink(repoName);
|
|
19383
|
+
installUrl = url;
|
|
19384
|
+
} catch {
|
|
19385
|
+
const githubAppFallbackUrl = process.env.GITCONTEXT_GITHUB_APP_URL ?? null;
|
|
19386
|
+
if (!githubAppFallbackUrl) {
|
|
19387
|
+
console.log(source_default.yellow(`
|
|
19388
|
+
Warning: could not retrieve install URL and no fallback is configured.
|
|
19389
|
+
` + ` Set GITCONTEXT_GITHUB_APP_URL or connect your repo from the web dashboard.
|
|
19390
|
+
`));
|
|
19391
|
+
usePolling = false;
|
|
19392
|
+
installUrl = "";
|
|
19393
|
+
} else {
|
|
19394
|
+
usePolling = false;
|
|
19395
|
+
installUrl = `${githubAppFallbackUrl}?repo=${encodeURIComponent(repoName)}`;
|
|
19396
|
+
console.log(source_default.yellow(`
|
|
19397
|
+
Warning: could not get signed install URL \u2014 using fallback link.`));
|
|
19398
|
+
}
|
|
19399
|
+
}
|
|
19400
|
+
if (!installUrl) {} else {
|
|
19401
|
+
console.log(`
|
|
18971
19402
|
Opening GitHub App installation page...`);
|
|
18972
|
-
|
|
19403
|
+
console.log(`If it doesn't open automatically, visit:
|
|
18973
19404
|
${installUrl}
|
|
18974
19405
|
`);
|
|
18975
|
-
|
|
18976
|
-
|
|
19406
|
+
openBrowser(installUrl);
|
|
19407
|
+
await dist_default5({ message: "Press enter once the GitHub App is installed", default: true });
|
|
19408
|
+
}
|
|
19409
|
+
if (usePolling) {
|
|
19410
|
+
const POLL_INTERVAL_MS = 2000;
|
|
19411
|
+
const MAX_ATTEMPTS = 8;
|
|
19412
|
+
let connected = false;
|
|
19413
|
+
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
19414
|
+
try {
|
|
19415
|
+
const { projects: polledProjects } = await apiClientForGH.listProjects(repoName);
|
|
19416
|
+
if (polledProjects.length > 0) {
|
|
19417
|
+
connected = true;
|
|
19418
|
+
break;
|
|
19419
|
+
}
|
|
19420
|
+
} catch {}
|
|
19421
|
+
if (attempt < MAX_ATTEMPTS - 1) {
|
|
19422
|
+
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
19423
|
+
}
|
|
19424
|
+
}
|
|
19425
|
+
if (connected) {
|
|
19426
|
+
console.log(source_default.green(`
|
|
19427
|
+
\u2713 GitHub App installed \u2014 ${repoName} is now connected`));
|
|
19428
|
+
} else {
|
|
19429
|
+
const settingsUrl = `${userConfig.apiUrl}/settings/org/repos`;
|
|
19430
|
+
console.log(source_default.yellow(`
|
|
19431
|
+
Warning: could not confirm GitHub App installation for ${repoName}.
|
|
19432
|
+
` + ` Verify at: ${settingsUrl}
|
|
19433
|
+
`));
|
|
19434
|
+
}
|
|
19435
|
+
}
|
|
19436
|
+
}
|
|
18977
19437
|
}
|
|
18978
19438
|
const installedFiles = [];
|
|
18979
19439
|
for (const platform2 of selectedPlatforms) {
|
|
@@ -19008,7 +19468,7 @@ Opening GitHub App installation page...`);
|
|
|
19008
19468
|
process.exitCode = 1;
|
|
19009
19469
|
return;
|
|
19010
19470
|
}
|
|
19011
|
-
const hasStagedChanges =
|
|
19471
|
+
const hasStagedChanges = spawnSync2("git", ["diff", "--cached", "--quiet"], { cwd: repoRoot }).status !== 0;
|
|
19012
19472
|
if (!hasStagedChanges) {
|
|
19013
19473
|
console.log(`
|
|
19014
19474
|
\u2713 Files written (no new git changes to commit)`);
|
|
@@ -19028,7 +19488,7 @@ Opening GitHub App installation page...`);
|
|
|
19028
19488
|
}
|
|
19029
19489
|
}
|
|
19030
19490
|
}
|
|
19031
|
-
const apiClient = new ApiClient(userConfig.apiUrl, userConfig.userToken);
|
|
19491
|
+
const apiClient = new ApiClient(userConfig.apiUrl, userConfig.userToken, userConfig.orgId);
|
|
19032
19492
|
const candidateCount = scanLocalDocs(repoRoot).length;
|
|
19033
19493
|
if (candidateCount > 0) {
|
|
19034
19494
|
const wantAdopt = await dist_default5({
|
|
@@ -19040,7 +19500,7 @@ Opening GitHub App installation page...`);
|
|
|
19040
19500
|
}
|
|
19041
19501
|
}
|
|
19042
19502
|
console.log(source_default.bold.hex("#6366f1")(BANNER));
|
|
19043
|
-
console.log(source_default.dim(" docs that ship with your code")
|
|
19503
|
+
console.log(`${source_default.dim(" docs that ship with your code")}
|
|
19044
19504
|
`);
|
|
19045
19505
|
console.log(` Repo: ${repoName}`);
|
|
19046
19506
|
console.log(` Profile: ${docProfile}`);
|
|
@@ -41881,6 +42341,31 @@ After all docs are created, provide a summary table: doc type, title, file path.
|
|
|
41881
42341
|
}
|
|
41882
42342
|
]
|
|
41883
42343
|
}));
|
|
42344
|
+
server.registerPrompt("revise_doc", {
|
|
42345
|
+
title: "Revise Doc",
|
|
42346
|
+
description: "AI-assisted revision of an existing doc \u2014 find, revise, and persist"
|
|
42347
|
+
}, async () => ({
|
|
42348
|
+
messages: [
|
|
42349
|
+
{
|
|
42350
|
+
role: "user",
|
|
42351
|
+
content: {
|
|
42352
|
+
type: "text",
|
|
42353
|
+
text: `You are revising an existing knowledge doc in this repository.
|
|
42354
|
+
|
|
42355
|
+
## Steps
|
|
42356
|
+
|
|
42357
|
+
1. Call \`mcp__gitcontext__list_docs\` (optionally with a branch or type filter) to find the target doc, or ask the developer which doc they want to revise.
|
|
42358
|
+
2. Call \`mcp__gitcontext__revise_doc\` with the chosen \`docId\` and the developer's revision prompt.
|
|
42359
|
+
3. Present the suggested revision to the developer (show title and content).
|
|
42360
|
+
4. Ask: "Accept (write & save), Revise (refine with feedback), or Reject (discard)?"
|
|
42361
|
+
- **Accept**: call \`mcp__gitcontext__write_doc\` with \`overwrite: true\`. If the result contains \`notFoundLocally: true\`, the doc has no local file \u2014 skip the local write and call \`mcp__gitcontext__ingest_doc\` directly using the doc's existing \`filePath\` from the \`revise_doc\` response. If the file was written, use the \`filePath\` from \`write_doc\` result for \`ingest_doc\`.
|
|
42362
|
+
- **Revise**: collect feedback, call \`mcp__gitcontext__revise_doc\` again using the current suggestion as the new base (pass the current content as context in the prompt), then loop back to step 3.
|
|
42363
|
+
- **Reject**: discard \u2014 nothing is saved.
|
|
42364
|
+
5. On accept, confirm: "\u2705 Doc revised and live in dashboard."`
|
|
42365
|
+
}
|
|
42366
|
+
}
|
|
42367
|
+
]
|
|
42368
|
+
}));
|
|
41884
42369
|
server.registerPrompt("analyze_pr", {
|
|
41885
42370
|
title: "Analyze PR",
|
|
41886
42371
|
description: "Lightweight one-shot PR analysis \u2014 automatically fetches the diff and presents doc suggestions"
|
|
@@ -41999,7 +42484,7 @@ function registerDocsTools(server) {
|
|
|
41999
42484
|
}
|
|
42000
42485
|
});
|
|
42001
42486
|
server.registerTool("write_doc", {
|
|
42002
|
-
description: "Write a structured doc to .gitcontext/docs/ and optionally commit it",
|
|
42487
|
+
description: "Write a structured doc to .gitcontext/docs/ and optionally commit it. Set overwrite: true to overwrite an existing file matched by frontmatter id.",
|
|
42003
42488
|
inputSchema: {
|
|
42004
42489
|
structuredDoc: exports_external.object({
|
|
42005
42490
|
id: exports_external.string(),
|
|
@@ -42010,10 +42495,26 @@ function registerDocsTools(server) {
|
|
|
42010
42495
|
frontmatter: exports_external.record(exports_external.string(), exports_external.unknown())
|
|
42011
42496
|
}).describe("Structured doc object from process_doc"),
|
|
42012
42497
|
workingDir: exports_external.string().optional().describe("Git working directory"),
|
|
42013
|
-
noCommit: exports_external.boolean().optional().describe("Skip git commit after writing")
|
|
42498
|
+
noCommit: exports_external.boolean().optional().describe("Skip git commit after writing"),
|
|
42499
|
+
overwrite: exports_external.boolean().optional().describe("When true, overwrite the existing file matched by frontmatter id instead of creating a new one")
|
|
42014
42500
|
}
|
|
42015
|
-
}, async ({ structuredDoc, workingDir, noCommit }) => {
|
|
42501
|
+
}, async ({ structuredDoc, workingDir, noCommit, overwrite }) => {
|
|
42016
42502
|
try {
|
|
42503
|
+
if (overwrite) {
|
|
42504
|
+
const existing = findDocFileById(structuredDoc.id, workingDir);
|
|
42505
|
+
if (existing) {
|
|
42506
|
+
const result2 = overwriteDocFile(structuredDoc, existing.absolutePath, existing.relativePath, { noCommit }, workingDir);
|
|
42507
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
42508
|
+
}
|
|
42509
|
+
return {
|
|
42510
|
+
content: [
|
|
42511
|
+
{
|
|
42512
|
+
type: "text",
|
|
42513
|
+
text: JSON.stringify({ notFoundLocally: true, id: structuredDoc.id }, null, 2)
|
|
42514
|
+
}
|
|
42515
|
+
]
|
|
42516
|
+
};
|
|
42517
|
+
}
|
|
42017
42518
|
const result = writeDoc(structuredDoc, workingDir, { noCommit });
|
|
42018
42519
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
42019
42520
|
} catch (err) {
|
|
@@ -42042,6 +42543,7 @@ function registerDocsTools(server) {
|
|
|
42042
42543
|
title,
|
|
42043
42544
|
content,
|
|
42044
42545
|
filePath,
|
|
42546
|
+
storageMode: "repo",
|
|
42045
42547
|
branch,
|
|
42046
42548
|
commitHash,
|
|
42047
42549
|
repoName,
|
|
@@ -42053,6 +42555,49 @@ function registerDocsTools(server) {
|
|
|
42053
42555
|
return errorResult2(err);
|
|
42054
42556
|
}
|
|
42055
42557
|
});
|
|
42558
|
+
server.registerTool("revise_doc", {
|
|
42559
|
+
description: "Fetch an existing doc by ID, send it + a revision prompt to /api/process-doc, and return the suggested revised content. Stateless \u2014 does not write or ingest. The agent drives the accept/revise/reject loop through conversation.",
|
|
42560
|
+
inputSchema: {
|
|
42561
|
+
docId: exports_external.string().describe("ID of the doc to revise"),
|
|
42562
|
+
prompt: exports_external.string().describe("Revision instruction"),
|
|
42563
|
+
workingDir: exports_external.string().optional().describe("Git working directory (for git context)")
|
|
42564
|
+
}
|
|
42565
|
+
}, async ({ docId, prompt }) => {
|
|
42566
|
+
try {
|
|
42567
|
+
const client = ApiClient.fromUserConfig();
|
|
42568
|
+
const originalDoc = await client.getDoc(docId);
|
|
42569
|
+
const originalContent = originalDoc.content;
|
|
42570
|
+
let author;
|
|
42571
|
+
try {
|
|
42572
|
+
const me = await client.getMe();
|
|
42573
|
+
author = me.githubLogin ?? me.name ?? me.email ?? "unknown";
|
|
42574
|
+
} catch {
|
|
42575
|
+
author = "unknown";
|
|
42576
|
+
}
|
|
42577
|
+
const context = `Current doc (revise this):
|
|
42578
|
+
${originalContent}`;
|
|
42579
|
+
const result = await client.processDoc({
|
|
42580
|
+
prompt,
|
|
42581
|
+
branch: originalDoc.branch,
|
|
42582
|
+
commitHash: originalDoc.commitHash,
|
|
42583
|
+
repoName: originalDoc.repoName,
|
|
42584
|
+
author,
|
|
42585
|
+
context
|
|
42586
|
+
});
|
|
42587
|
+
const raw = result.docs[0] ?? null;
|
|
42588
|
+
const suggested = raw ? { ...raw, id: docId, frontmatter: { ...raw.frontmatter, id: docId } } : null;
|
|
42589
|
+
return {
|
|
42590
|
+
content: [
|
|
42591
|
+
{
|
|
42592
|
+
type: "text",
|
|
42593
|
+
text: JSON.stringify({ ...suggested, originalContent }, null, 2)
|
|
42594
|
+
}
|
|
42595
|
+
]
|
|
42596
|
+
};
|
|
42597
|
+
} catch (err) {
|
|
42598
|
+
return errorResult2(err);
|
|
42599
|
+
}
|
|
42600
|
+
});
|
|
42056
42601
|
server.registerTool("list_docs", {
|
|
42057
42602
|
description: "List docs with optional filters and cursor-based pagination",
|
|
42058
42603
|
inputSchema: {
|
|
@@ -42305,6 +42850,7 @@ function registerPushCommand(program2) {
|
|
|
42305
42850
|
title: doc2.title,
|
|
42306
42851
|
content: doc2.content,
|
|
42307
42852
|
filePath: doc2.filePath,
|
|
42853
|
+
storageMode: "repo",
|
|
42308
42854
|
branch: doc2.branch,
|
|
42309
42855
|
commitHash: doc2.commitHash,
|
|
42310
42856
|
repoName: doc2.repoName,
|
|
@@ -42344,6 +42890,257 @@ function registerPushCommand(program2) {
|
|
|
42344
42890
|
});
|
|
42345
42891
|
}
|
|
42346
42892
|
|
|
42893
|
+
// src/commands/revise.ts
|
|
42894
|
+
function registerReviseCommand(program2) {
|
|
42895
|
+
program2.command("revise").description("AI-assisted revision of an existing doc").option("--doc <id>", "Doc ID to revise (skips fuzzy search)").option("--prompt <text>", "Revision prompt (skips interactive prompt)").option("--no-commit", "Write the file without creating a git commit").option("--no-local", "Skip local file overwrite; only update the cloud DB").option("--json", "Output revised doc as JSON (non-interactive)").action(async (options) => {
|
|
42896
|
+
await runRevise(options);
|
|
42897
|
+
});
|
|
42898
|
+
}
|
|
42899
|
+
async function runRevise(options) {
|
|
42900
|
+
const config2 = readUserConfig();
|
|
42901
|
+
if (!config2) {
|
|
42902
|
+
console.error("Not authenticated. Run `gitcontext auth login` first.");
|
|
42903
|
+
process.exitCode = 1;
|
|
42904
|
+
return;
|
|
42905
|
+
}
|
|
42906
|
+
const client = new ApiClient(config2.apiUrl, config2.userToken);
|
|
42907
|
+
const workingDir = process.cwd();
|
|
42908
|
+
const branch = getCurrentBranch(workingDir);
|
|
42909
|
+
const commitHash = getCurrentCommit(workingDir);
|
|
42910
|
+
const repoName = getRepoName(workingDir);
|
|
42911
|
+
let me;
|
|
42912
|
+
try {
|
|
42913
|
+
me = await client.getMe();
|
|
42914
|
+
} catch (error51) {
|
|
42915
|
+
handleApiError(error51);
|
|
42916
|
+
return;
|
|
42917
|
+
}
|
|
42918
|
+
const author = me.githubLogin ?? me.name ?? me.email ?? "unknown";
|
|
42919
|
+
let targetDoc;
|
|
42920
|
+
if (options.doc) {
|
|
42921
|
+
try {
|
|
42922
|
+
targetDoc = await client.getDoc(options.doc);
|
|
42923
|
+
} catch (error51) {
|
|
42924
|
+
handleApiError(error51);
|
|
42925
|
+
return;
|
|
42926
|
+
}
|
|
42927
|
+
} else {
|
|
42928
|
+
if (options.json) {
|
|
42929
|
+
console.error("--doc <id> is required in --json mode.");
|
|
42930
|
+
process.exitCode = 1;
|
|
42931
|
+
return;
|
|
42932
|
+
}
|
|
42933
|
+
try {
|
|
42934
|
+
targetDoc = await pickDocInteractive(client);
|
|
42935
|
+
} catch (error51) {
|
|
42936
|
+
if (error51.code === "ERR_USE_AFTER_CLOSE") {
|
|
42937
|
+
return;
|
|
42938
|
+
}
|
|
42939
|
+
handleApiError(error51);
|
|
42940
|
+
return;
|
|
42941
|
+
}
|
|
42942
|
+
}
|
|
42943
|
+
let revisionPrompt = options.prompt?.trim() ?? "";
|
|
42944
|
+
if (!revisionPrompt) {
|
|
42945
|
+
if (options.json) {
|
|
42946
|
+
console.error("--prompt <text> is required in --json mode.");
|
|
42947
|
+
process.exitCode = 1;
|
|
42948
|
+
return;
|
|
42949
|
+
}
|
|
42950
|
+
try {
|
|
42951
|
+
revisionPrompt = await dist_default6({ message: "What should be updated?" });
|
|
42952
|
+
} catch {
|
|
42953
|
+
return;
|
|
42954
|
+
}
|
|
42955
|
+
}
|
|
42956
|
+
if (!revisionPrompt) {
|
|
42957
|
+
console.error("No revision prompt provided.");
|
|
42958
|
+
process.exitCode = 1;
|
|
42959
|
+
return;
|
|
42960
|
+
}
|
|
42961
|
+
const originalContent = targetDoc.content;
|
|
42962
|
+
const context = `Current doc (revise this):
|
|
42963
|
+
${originalContent}`;
|
|
42964
|
+
let result;
|
|
42965
|
+
try {
|
|
42966
|
+
result = await client.processDoc({
|
|
42967
|
+
prompt: revisionPrompt,
|
|
42968
|
+
branch,
|
|
42969
|
+
commitHash,
|
|
42970
|
+
repoName,
|
|
42971
|
+
author,
|
|
42972
|
+
context
|
|
42973
|
+
});
|
|
42974
|
+
} catch (error51) {
|
|
42975
|
+
handleApiError(error51);
|
|
42976
|
+
return;
|
|
42977
|
+
}
|
|
42978
|
+
if (result.docs.length === 0) {
|
|
42979
|
+
console.error("AI returned no revised content.");
|
|
42980
|
+
process.exitCode = 1;
|
|
42981
|
+
return;
|
|
42982
|
+
}
|
|
42983
|
+
let current = normalizeRevisionId(result.docs[0], targetDoc.id);
|
|
42984
|
+
if (options.json) {
|
|
42985
|
+
console.log(JSON.stringify({ ...current, originalContent }, null, 2));
|
|
42986
|
+
return;
|
|
42987
|
+
}
|
|
42988
|
+
while (true) {
|
|
42989
|
+
console.log(`
|
|
42990
|
+
${renderDocBox(current)}
|
|
42991
|
+
`);
|
|
42992
|
+
let action;
|
|
42993
|
+
try {
|
|
42994
|
+
action = await dist_default8({
|
|
42995
|
+
message: "What would you like to do?",
|
|
42996
|
+
choices: [
|
|
42997
|
+
{ name: "[A] Accept \u2014 save this revision", value: "accept" },
|
|
42998
|
+
{ name: "[R] Revise \u2014 refine with another prompt", value: "revise" },
|
|
42999
|
+
{ name: "[X] Reject \u2014 discard", value: "reject" }
|
|
43000
|
+
]
|
|
43001
|
+
});
|
|
43002
|
+
} catch {
|
|
43003
|
+
return;
|
|
43004
|
+
}
|
|
43005
|
+
if (action === "reject") {
|
|
43006
|
+
console.log("Discarded. Nothing was saved.");
|
|
43007
|
+
return;
|
|
43008
|
+
}
|
|
43009
|
+
if (action === "accept") {
|
|
43010
|
+
await persistRevision(current, targetDoc, {
|
|
43011
|
+
client,
|
|
43012
|
+
workingDir,
|
|
43013
|
+
branch,
|
|
43014
|
+
commitHash,
|
|
43015
|
+
repoName,
|
|
43016
|
+
options,
|
|
43017
|
+
author
|
|
43018
|
+
});
|
|
43019
|
+
return;
|
|
43020
|
+
}
|
|
43021
|
+
let feedback;
|
|
43022
|
+
try {
|
|
43023
|
+
feedback = await dist_default6({ message: "How should I revise it?" });
|
|
43024
|
+
} catch {
|
|
43025
|
+
return;
|
|
43026
|
+
}
|
|
43027
|
+
const reviseContext = `Current draft (revise this):
|
|
43028
|
+
${current.content}`;
|
|
43029
|
+
let revised;
|
|
43030
|
+
try {
|
|
43031
|
+
revised = await client.processDoc({
|
|
43032
|
+
prompt: feedback,
|
|
43033
|
+
branch,
|
|
43034
|
+
commitHash,
|
|
43035
|
+
repoName,
|
|
43036
|
+
author,
|
|
43037
|
+
context: reviseContext
|
|
43038
|
+
});
|
|
43039
|
+
} catch (error51) {
|
|
43040
|
+
handleApiError(error51);
|
|
43041
|
+
return;
|
|
43042
|
+
}
|
|
43043
|
+
if (revised.docs.length === 0) {
|
|
43044
|
+
console.error("Revision produced no doc; keeping the previous draft.");
|
|
43045
|
+
continue;
|
|
43046
|
+
}
|
|
43047
|
+
current = normalizeRevisionId(revised.docs[0], targetDoc.id);
|
|
43048
|
+
}
|
|
43049
|
+
}
|
|
43050
|
+
function normalizeRevisionId(doc2, canonicalId) {
|
|
43051
|
+
return {
|
|
43052
|
+
...doc2,
|
|
43053
|
+
id: canonicalId,
|
|
43054
|
+
frontmatter: { ...doc2.frontmatter, id: canonicalId }
|
|
43055
|
+
};
|
|
43056
|
+
}
|
|
43057
|
+
async function pickDocInteractive(client) {
|
|
43058
|
+
return dist_default7({
|
|
43059
|
+
message: "Search docs:",
|
|
43060
|
+
source: async (term) => {
|
|
43061
|
+
const query = (term ?? "").trim();
|
|
43062
|
+
if (query.length < 2)
|
|
43063
|
+
return [];
|
|
43064
|
+
try {
|
|
43065
|
+
const results = await client.searchDocs({ query, limit: 10 });
|
|
43066
|
+
return results.map((d) => {
|
|
43067
|
+
const meta3 = typeMeta(d.type);
|
|
43068
|
+
const age = formatAge(d.createdAt);
|
|
43069
|
+
return {
|
|
43070
|
+
name: `${meta3.emoji} ${d.title} (${d.branch}, ${age})`,
|
|
43071
|
+
value: d
|
|
43072
|
+
};
|
|
43073
|
+
});
|
|
43074
|
+
} catch {
|
|
43075
|
+
return [];
|
|
43076
|
+
}
|
|
43077
|
+
}
|
|
43078
|
+
});
|
|
43079
|
+
}
|
|
43080
|
+
async function persistRevision(doc2, original, ctx) {
|
|
43081
|
+
const noLocal = ctx.options.local === false;
|
|
43082
|
+
const noCommit = ctx.options.commit === false;
|
|
43083
|
+
let filePath;
|
|
43084
|
+
if (noLocal) {
|
|
43085
|
+
filePath = original.filePath ?? `.gitcontext/docs/${original.id}.md`;
|
|
43086
|
+
} else {
|
|
43087
|
+
const existing = findDocFileById(doc2.id, ctx.workingDir);
|
|
43088
|
+
if (existing) {
|
|
43089
|
+
try {
|
|
43090
|
+
const written = overwriteDocFile(doc2, existing.absolutePath, existing.relativePath, {
|
|
43091
|
+
noCommit,
|
|
43092
|
+
author: ctx.author
|
|
43093
|
+
});
|
|
43094
|
+
filePath = written.filePath;
|
|
43095
|
+
console.log(written.committed ? `\uD83D\uDCDD Revised & committed ${filePath}` : `\uD83D\uDCDD Revised ${filePath}`);
|
|
43096
|
+
} catch (error51) {
|
|
43097
|
+
console.error(`Failed to write file: ${error51 instanceof Error ? error51.message : String(error51)}`);
|
|
43098
|
+
process.exitCode = 1;
|
|
43099
|
+
return;
|
|
43100
|
+
}
|
|
43101
|
+
} else {
|
|
43102
|
+
filePath = original.filePath ?? `.gitcontext/docs/${original.id}.md`;
|
|
43103
|
+
console.log("No local file found \u2014 updating cloud DB only.");
|
|
43104
|
+
}
|
|
43105
|
+
}
|
|
43106
|
+
try {
|
|
43107
|
+
const res = await ctx.client.ingestDoc({
|
|
43108
|
+
id: doc2.id,
|
|
43109
|
+
type: doc2.type,
|
|
43110
|
+
title: doc2.title,
|
|
43111
|
+
content: doc2.content,
|
|
43112
|
+
filePath,
|
|
43113
|
+
storageMode: "repo",
|
|
43114
|
+
branch: ctx.branch,
|
|
43115
|
+
commitHash: ctx.commitHash,
|
|
43116
|
+
repoName: ctx.repoName,
|
|
43117
|
+
frontmatter: doc2.frontmatter,
|
|
43118
|
+
revisionSource: "cli_capture"
|
|
43119
|
+
});
|
|
43120
|
+
console.log(`
|
|
43121
|
+
\u2705 Doc revised (revision ${res.revisionNumber}) \u2014 committed and live in dashboard.`);
|
|
43122
|
+
} catch (error51) {
|
|
43123
|
+
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
43124
|
+
console.warn(`\u26A0\uFE0F Ingest failed for "${doc2.title}": ${message}`);
|
|
43125
|
+
console.warn(" The file is saved locally and will reconcile at PR merge.");
|
|
43126
|
+
}
|
|
43127
|
+
}
|
|
43128
|
+
function formatAge(isoDate) {
|
|
43129
|
+
const ms = Date.now() - new Date(isoDate).getTime();
|
|
43130
|
+
const days = Math.floor(ms / 86400000);
|
|
43131
|
+
if (days === 0)
|
|
43132
|
+
return "today";
|
|
43133
|
+
if (days === 1)
|
|
43134
|
+
return "1 day ago";
|
|
43135
|
+
if (days < 30)
|
|
43136
|
+
return `${days} days ago`;
|
|
43137
|
+
const weeks = Math.floor(days / 7);
|
|
43138
|
+
if (weeks < 8)
|
|
43139
|
+
return `${weeks} week${weeks === 1 ? "" : "s"} ago`;
|
|
43140
|
+
const months = Math.floor(days / 30);
|
|
43141
|
+
return `${months} month${months === 1 ? "" : "s"} ago`;
|
|
43142
|
+
}
|
|
43143
|
+
|
|
42347
43144
|
// src/commands/status.ts
|
|
42348
43145
|
function handleError4(error51) {
|
|
42349
43146
|
if (error51 instanceof ApiClientError) {
|
|
@@ -42424,8 +43221,80 @@ function registerStatusCommand(program2) {
|
|
|
42424
43221
|
});
|
|
42425
43222
|
}
|
|
42426
43223
|
|
|
43224
|
+
// src/commands/teach.ts
|
|
43225
|
+
var TEACH_PROMPT = "Pick one interesting, surprising, or practically useful insight that a developer on this team would benefit from knowing. It could be a hard-won lesson, a subtle gotcha, a convention rationale, or a pattern worth remembering. Make it specific and actionable \u2014 not generic advice. Return it as a single 'learning' doc.";
|
|
43226
|
+
var DOC_TYPES4 = ["learning", "bug", "blocker", "convention", "conversation"];
|
|
43227
|
+
function registerTeachCommand(program2) {
|
|
43228
|
+
program2.command("teach").description("Pull a surprising or useful insight from your team's captured docs via AI").option("--type <type>", "Bias toward a specific doc type").option("--repo <name>", "Scope to a specific repo").option("--branch <name>", "Scope to a specific branch").option("--json", "Output structured doc as JSON").action(async (options) => {
|
|
43229
|
+
await runTeach(options);
|
|
43230
|
+
});
|
|
43231
|
+
}
|
|
43232
|
+
async function runTeach(options) {
|
|
43233
|
+
const config2 = readUserConfig();
|
|
43234
|
+
if (!config2) {
|
|
43235
|
+
console.error("Not authenticated. Run `gitcontext auth login` first.");
|
|
43236
|
+
process.exitCode = 1;
|
|
43237
|
+
return;
|
|
43238
|
+
}
|
|
43239
|
+
if (options.type && !DOC_TYPES4.includes(options.type)) {
|
|
43240
|
+
console.error(`Invalid --type "${options.type}". Expected one of: ${DOC_TYPES4.join(", ")}.`);
|
|
43241
|
+
process.exitCode = 1;
|
|
43242
|
+
return;
|
|
43243
|
+
}
|
|
43244
|
+
const client = new ApiClient(config2.apiUrl, config2.userToken);
|
|
43245
|
+
let me;
|
|
43246
|
+
try {
|
|
43247
|
+
me = await client.getMe();
|
|
43248
|
+
} catch (error51) {
|
|
43249
|
+
handleApiError(error51);
|
|
43250
|
+
return;
|
|
43251
|
+
}
|
|
43252
|
+
if (me.subscription && me.subscription.monthlyAiDocLimit >= 0 && me.subscription.monthlyAiDocCount >= me.subscription.monthlyAiDocLimit) {
|
|
43253
|
+
console.error(`Monthly AI doc limit reached (${me.subscription.monthlyAiDocCount}/${me.subscription.monthlyAiDocLimit}).`);
|
|
43254
|
+
console.error(`Upgrade your plan to generate more docs: ${config2.apiUrl}/settings/org`);
|
|
43255
|
+
process.exitCode = 1;
|
|
43256
|
+
return;
|
|
43257
|
+
}
|
|
43258
|
+
if (!options.json) {
|
|
43259
|
+
console.log(source_default.dim(`
|
|
43260
|
+
\u2726 Teaching you something\u2026
|
|
43261
|
+
`));
|
|
43262
|
+
}
|
|
43263
|
+
let promptText = TEACH_PROMPT;
|
|
43264
|
+
if (options.type) {
|
|
43265
|
+
promptText += ` Focus on "${options.type}" type docs.`;
|
|
43266
|
+
}
|
|
43267
|
+
const author = me.githubLogin ?? me.name ?? me.email ?? "unknown";
|
|
43268
|
+
let result;
|
|
43269
|
+
try {
|
|
43270
|
+
result = await client.processDoc({
|
|
43271
|
+
prompt: promptText,
|
|
43272
|
+
...options.branch ? { branch: options.branch } : {},
|
|
43273
|
+
...options.repo ? { repoName: options.repo } : {},
|
|
43274
|
+
author
|
|
43275
|
+
});
|
|
43276
|
+
} catch (error51) {
|
|
43277
|
+
handleApiError(error51);
|
|
43278
|
+
return;
|
|
43279
|
+
}
|
|
43280
|
+
const { docs } = result;
|
|
43281
|
+
if (docs.length === 0) {
|
|
43282
|
+
console.error("No insight was generated. Try again.");
|
|
43283
|
+
process.exitCode = 1;
|
|
43284
|
+
return;
|
|
43285
|
+
}
|
|
43286
|
+
if (options.json) {
|
|
43287
|
+
console.log(JSON.stringify({ docs }, null, 2));
|
|
43288
|
+
return;
|
|
43289
|
+
}
|
|
43290
|
+
console.log(renderDocBox(docs[0]));
|
|
43291
|
+
console.log(`
|
|
43292
|
+
\uD83D\uDCA1 Run again for a different insight.
|
|
43293
|
+
`);
|
|
43294
|
+
}
|
|
43295
|
+
|
|
42427
43296
|
// src/commands/update.ts
|
|
42428
|
-
import { execFileSync as execFileSync6, spawnSync as
|
|
43297
|
+
import { execFileSync as execFileSync6, spawnSync as spawnSync3 } from "child_process";
|
|
42429
43298
|
function registerUpdateCommand(program2) {
|
|
42430
43299
|
program2.command("update").description("Re-install integration templates from the installed package").action(async () => {
|
|
42431
43300
|
await runUpdate();
|
|
@@ -42450,6 +43319,7 @@ async function runUpdate() {
|
|
|
42450
43319
|
const platforms = (config2.platforms ?? []).filter(isValidPlatform);
|
|
42451
43320
|
if (platforms.length === 0) {
|
|
42452
43321
|
console.log("No platforms configured. Run `gitcontext init` to set up platforms.");
|
|
43322
|
+
await syncDocStorageModes(workingDir);
|
|
42453
43323
|
return;
|
|
42454
43324
|
}
|
|
42455
43325
|
console.log(`Updating GitContext integration for ${platforms.join(", ")}...
|
|
@@ -42468,6 +43338,7 @@ async function runUpdate() {
|
|
|
42468
43338
|
}
|
|
42469
43339
|
if (installedFiles.length === 0) {
|
|
42470
43340
|
console.log("No files updated.");
|
|
43341
|
+
await syncDocStorageModes(workingDir);
|
|
42471
43342
|
return;
|
|
42472
43343
|
}
|
|
42473
43344
|
const repoLocalPaths = installedFiles.filter((f) => !f.global).map((f) => f.path.startsWith(repoRoot) ? f.path.slice(repoRoot.length + 1) : f.path).filter((p) => !p.startsWith("/"));
|
|
@@ -42480,7 +43351,7 @@ async function runUpdate() {
|
|
|
42480
43351
|
process.exitCode = 1;
|
|
42481
43352
|
return;
|
|
42482
43353
|
}
|
|
42483
|
-
const hasStagedChanges =
|
|
43354
|
+
const hasStagedChanges = spawnSync3("git", ["diff", "--cached", "--quiet"], { cwd: repoRoot }).status !== 0;
|
|
42484
43355
|
if (!hasStagedChanges) {
|
|
42485
43356
|
console.log(" \u2713 Files written (no new git changes to commit)");
|
|
42486
43357
|
} else {
|
|
@@ -42507,11 +43378,146 @@ async function runUpdate() {
|
|
|
42507
43378
|
const suffix = f.global ? " (global)" : "";
|
|
42508
43379
|
console.log(` ${prefix} ${displayPath}${suffix}`);
|
|
42509
43380
|
}
|
|
43381
|
+
await syncDocStorageModes(workingDir);
|
|
43382
|
+
}
|
|
43383
|
+
async function syncDocStorageModes(workingDir) {
|
|
43384
|
+
const userConfig = readUserConfig();
|
|
43385
|
+
if (!userConfig)
|
|
43386
|
+
return;
|
|
43387
|
+
try {
|
|
43388
|
+
const client = new ApiClient(userConfig.apiUrl, userConfig.userToken, userConfig.orgId);
|
|
43389
|
+
const docStorageModes = await client.getOrgDocStorageModes();
|
|
43390
|
+
const validDocTypes = [
|
|
43391
|
+
"learning",
|
|
43392
|
+
"bug",
|
|
43393
|
+
"blocker",
|
|
43394
|
+
"convention",
|
|
43395
|
+
"conversation",
|
|
43396
|
+
"report"
|
|
43397
|
+
];
|
|
43398
|
+
const typed = {};
|
|
43399
|
+
for (const dt of validDocTypes) {
|
|
43400
|
+
const val = docStorageModes[dt];
|
|
43401
|
+
if (val === "repo" || val === "cloud") {
|
|
43402
|
+
typed[dt] = val;
|
|
43403
|
+
}
|
|
43404
|
+
}
|
|
43405
|
+
const latestConfig = readRepoConfig(workingDir);
|
|
43406
|
+
if (latestConfig) {
|
|
43407
|
+
writeRepoConfig(workingDir, { ...latestConfig, docStorageModes: typed });
|
|
43408
|
+
console.log(`
|
|
43409
|
+
\u2713 Doc storage settings synced from org config`);
|
|
43410
|
+
}
|
|
43411
|
+
} catch (err) {
|
|
43412
|
+
if (err instanceof ApiClientError) {
|
|
43413
|
+
console.warn(`
|
|
43414
|
+
\u26A0\uFE0F Could not sync doc storage settings: ${err.message}`);
|
|
43415
|
+
}
|
|
43416
|
+
}
|
|
43417
|
+
}
|
|
43418
|
+
|
|
43419
|
+
// src/lib/session-alerts.ts
|
|
43420
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
43421
|
+
import { homedir as homedir3 } from "os";
|
|
43422
|
+
import { join as join7 } from "path";
|
|
43423
|
+
function getPath() {
|
|
43424
|
+
return join7(homedir3(), ".gitcontext", "session-alerts.json");
|
|
43425
|
+
}
|
|
43426
|
+
function read() {
|
|
43427
|
+
try {
|
|
43428
|
+
const raw = readFileSync7(getPath(), "utf-8");
|
|
43429
|
+
return JSON.parse(raw);
|
|
43430
|
+
} catch {
|
|
43431
|
+
return {};
|
|
43432
|
+
}
|
|
43433
|
+
}
|
|
43434
|
+
function write(data) {
|
|
43435
|
+
const dir = join7(homedir3(), ".gitcontext");
|
|
43436
|
+
if (!existsSync6(dir))
|
|
43437
|
+
mkdirSync4(dir, { recursive: true });
|
|
43438
|
+
writeFileSync4(getPath(), `${JSON.stringify(data, null, 2)}
|
|
43439
|
+
`, "utf-8");
|
|
43440
|
+
}
|
|
43441
|
+
function isToday(isoTimestamp) {
|
|
43442
|
+
const d = new Date(isoTimestamp);
|
|
43443
|
+
const now = new Date;
|
|
43444
|
+
return d.getUTCFullYear() === now.getUTCFullYear() && d.getUTCMonth() === now.getUTCMonth() && d.getUTCDate() === now.getUTCDate();
|
|
43445
|
+
}
|
|
43446
|
+
function shouldCheckToday(key) {
|
|
43447
|
+
const ts = read()[key];
|
|
43448
|
+
return !ts || !isToday(ts);
|
|
43449
|
+
}
|
|
43450
|
+
function markChecked(key) {
|
|
43451
|
+
const data = read();
|
|
43452
|
+
data[key] = new Date().toISOString();
|
|
43453
|
+
write(data);
|
|
43454
|
+
}
|
|
43455
|
+
function getLastSeenVersion() {
|
|
43456
|
+
return read().lastSeenVersion ?? null;
|
|
43457
|
+
}
|
|
43458
|
+
function setLastSeenVersion(version2) {
|
|
43459
|
+
const data = read();
|
|
43460
|
+
data.lastSeenVersion = version2;
|
|
43461
|
+
write(data);
|
|
43462
|
+
}
|
|
43463
|
+
|
|
43464
|
+
// src/lib/update-check.ts
|
|
43465
|
+
function parseSemver(v) {
|
|
43466
|
+
const parts = v.split(".").map(Number);
|
|
43467
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
43468
|
+
}
|
|
43469
|
+
function isNewerVersion(candidate, current) {
|
|
43470
|
+
const [caMaj, caMin, caPatch] = parseSemver(candidate);
|
|
43471
|
+
const [cuMaj, cuMin, cuPatch] = parseSemver(current);
|
|
43472
|
+
if (caMaj !== cuMaj)
|
|
43473
|
+
return caMaj > cuMaj;
|
|
43474
|
+
if (caMin !== cuMin)
|
|
43475
|
+
return caMin > cuMin;
|
|
43476
|
+
return caPatch > cuPatch;
|
|
43477
|
+
}
|
|
43478
|
+
async function fetchLatestNpmVersion(packageName) {
|
|
43479
|
+
try {
|
|
43480
|
+
const controller = new AbortController;
|
|
43481
|
+
const id = setTimeout(() => controller.abort(), 3000);
|
|
43482
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: controller.signal });
|
|
43483
|
+
clearTimeout(id);
|
|
43484
|
+
if (!res.ok)
|
|
43485
|
+
return null;
|
|
43486
|
+
const data = await res.json();
|
|
43487
|
+
return typeof data.version === "string" ? data.version : null;
|
|
43488
|
+
} catch {
|
|
43489
|
+
return null;
|
|
43490
|
+
}
|
|
43491
|
+
}
|
|
43492
|
+
function parseMarkdownBullets2(notes) {
|
|
43493
|
+
return notes.split(`
|
|
43494
|
+
`).map((line) => line.trim()).filter((line) => /^[-*+]\s+/.test(line)).map((line) => line.replace(/^[-*+]\s+/, ""));
|
|
43495
|
+
}
|
|
43496
|
+
async function fetchChangelogNotes(apiUrl) {
|
|
43497
|
+
try {
|
|
43498
|
+
const controller = new AbortController;
|
|
43499
|
+
const id = setTimeout(() => controller.abort(), 3000);
|
|
43500
|
+
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/changelog/latest?package=cli`, {
|
|
43501
|
+
signal: controller.signal
|
|
43502
|
+
});
|
|
43503
|
+
clearTimeout(id);
|
|
43504
|
+
if (!res.ok)
|
|
43505
|
+
return null;
|
|
43506
|
+
const data = await res.json();
|
|
43507
|
+
if (typeof data.notes !== "string" || !data.notes)
|
|
43508
|
+
return null;
|
|
43509
|
+
const lines = parseMarkdownBullets2(data.notes);
|
|
43510
|
+
return lines.length > 0 ? lines : null;
|
|
43511
|
+
} catch {
|
|
43512
|
+
return null;
|
|
43513
|
+
}
|
|
42510
43514
|
}
|
|
42511
43515
|
|
|
42512
43516
|
// src/index.ts
|
|
43517
|
+
var PACKAGE_NAME = "@fentech/gitcontext";
|
|
43518
|
+
var VERSION = package_default.version;
|
|
42513
43519
|
var program2 = new Command;
|
|
42514
|
-
program2.name("gitcontext").description("GitContext CLI \u2014 capture, sync, and query git-backed knowledge docs").version(
|
|
43520
|
+
program2.name("gitcontext").description("GitContext CLI \u2014 capture, sync, and query git-backed knowledge docs").version(VERSION).option("--profile <name>", "Auth profile to use for this invocation (overrides active profile)");
|
|
42515
43521
|
program2.hook("preAction", () => {
|
|
42516
43522
|
const opts = program2.opts();
|
|
42517
43523
|
setProfileOverride(opts.profile);
|
|
@@ -42519,8 +43525,100 @@ program2.hook("preAction", () => {
|
|
|
42519
43525
|
console.log(source_default.dim(`Using ${getActiveProfileName() ?? "prod"} profile`));
|
|
42520
43526
|
}
|
|
42521
43527
|
});
|
|
43528
|
+
function printUpdateAvailableBanner(current, latest) {
|
|
43529
|
+
const lines = [
|
|
43530
|
+
source_default.bold.yellow(` gitcontext update available: ${current} \u2192 ${latest}`),
|
|
43531
|
+
` Run: ${source_default.cyan(`npm install -g ${PACKAGE_NAME}`)}`
|
|
43532
|
+
];
|
|
43533
|
+
renderBox(lines);
|
|
43534
|
+
}
|
|
43535
|
+
var MAX_WHATS_NEW_NOTES = 8;
|
|
43536
|
+
function printWhatsNewBanner(version2, notes) {
|
|
43537
|
+
const lines = [];
|
|
43538
|
+
lines.push(source_default.bold.cyan(` \u2728 What's new in gitcontext ${version2}`));
|
|
43539
|
+
if (notes && notes.length > 0) {
|
|
43540
|
+
const displayed = notes.slice(0, MAX_WHATS_NEW_NOTES);
|
|
43541
|
+
const overflow = notes.length - displayed.length;
|
|
43542
|
+
for (const note of displayed) {
|
|
43543
|
+
lines.push(` ${source_default.dim("\u2022")} ${note}`);
|
|
43544
|
+
}
|
|
43545
|
+
if (overflow > 0) {
|
|
43546
|
+
lines.push(` ${source_default.dim(`\u2026and ${overflow} more`)}`);
|
|
43547
|
+
}
|
|
43548
|
+
}
|
|
43549
|
+
lines.push(` ${source_default.dim(`Full changelog: ${source_default.cyan("gitcontext changelog")}`)}`);
|
|
43550
|
+
renderBox(lines);
|
|
43551
|
+
}
|
|
43552
|
+
function renderBox(lines) {
|
|
43553
|
+
const width = Math.max(...lines.map((l) => stripAnsi(l).length)) + 2;
|
|
43554
|
+
const top = `\u256D${"\u2500".repeat(width)}\u256E`;
|
|
43555
|
+
const bottom = `\u2570${"\u2500".repeat(width)}\u256F`;
|
|
43556
|
+
const pad = (s) => {
|
|
43557
|
+
const visible = stripAnsi(s).length;
|
|
43558
|
+
return `\u2502${s}${" ".repeat(width - visible)}\u2502`;
|
|
43559
|
+
};
|
|
43560
|
+
process.stderr.write(`
|
|
43561
|
+
${top}
|
|
43562
|
+
${lines.map(pad).join(`
|
|
43563
|
+
`)}
|
|
43564
|
+
${bottom}
|
|
43565
|
+
`);
|
|
43566
|
+
}
|
|
43567
|
+
function stripAnsi(s) {
|
|
43568
|
+
return s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
43569
|
+
}
|
|
43570
|
+
program2.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
43571
|
+
const cmdName = actionCommand.name();
|
|
43572
|
+
const parentName = actionCommand.parent?.name();
|
|
43573
|
+
const isAuthLoginOrLogout = parentName === "auth" && (cmdName === "login" || cmdName === "logout");
|
|
43574
|
+
const tasks = [];
|
|
43575
|
+
if (shouldCheckToday("updateCheckedAt")) {
|
|
43576
|
+
markChecked("updateCheckedAt");
|
|
43577
|
+
tasks.push((async () => {
|
|
43578
|
+
const lastSeen = getLastSeenVersion();
|
|
43579
|
+
if (lastSeen !== null && isNewerVersion(VERSION, lastSeen)) {
|
|
43580
|
+
const profile = readActiveProfile();
|
|
43581
|
+
const apiUrl = profile?.apiUrl ?? "https://app.gitcontext.app";
|
|
43582
|
+
const notes = await fetchChangelogNotes(apiUrl);
|
|
43583
|
+
printWhatsNewBanner(VERSION, notes);
|
|
43584
|
+
setLastSeenVersion(VERSION);
|
|
43585
|
+
return;
|
|
43586
|
+
}
|
|
43587
|
+
if (lastSeen === null) {
|
|
43588
|
+
setLastSeenVersion(VERSION);
|
|
43589
|
+
return;
|
|
43590
|
+
}
|
|
43591
|
+
const latest = await fetchLatestNpmVersion(PACKAGE_NAME);
|
|
43592
|
+
if (latest && isNewerVersion(latest, VERSION)) {
|
|
43593
|
+
printUpdateAvailableBanner(VERSION, latest);
|
|
43594
|
+
}
|
|
43595
|
+
})());
|
|
43596
|
+
}
|
|
43597
|
+
if (!isAuthLoginOrLogout && readUserConfig() && shouldCheckToday("usageCheckedAt")) {
|
|
43598
|
+
markChecked("usageCheckedAt");
|
|
43599
|
+
tasks.push((async () => {
|
|
43600
|
+
try {
|
|
43601
|
+
const me = getCachedMe() ?? await ApiClient.fromUserConfig().getMe();
|
|
43602
|
+
const sub = me.subscription;
|
|
43603
|
+
if (sub && sub.monthlyAiDocLimit > 0) {
|
|
43604
|
+
const ratio = sub.monthlyAiDocCount / sub.monthlyAiDocLimit;
|
|
43605
|
+
if (ratio >= 0.9) {
|
|
43606
|
+
const pct = Math.round(ratio * 100);
|
|
43607
|
+
const profile = readActiveProfile();
|
|
43608
|
+
const appUrl = profile?.apiUrl ?? "https://app.gitcontext.app";
|
|
43609
|
+
process.stderr.write(source_default.yellow(`
|
|
43610
|
+
\u26A0 AI doc usage: ${sub.monthlyAiDocCount}/${sub.monthlyAiDocLimit} (${pct}%) \u2014 upgrade at ${appUrl}/settings/org
|
|
43611
|
+
`));
|
|
43612
|
+
}
|
|
43613
|
+
}
|
|
43614
|
+
} catch {}
|
|
43615
|
+
})());
|
|
43616
|
+
}
|
|
43617
|
+
await Promise.all(tasks);
|
|
43618
|
+
});
|
|
42522
43619
|
registerAnalyzeCommand(program2);
|
|
42523
43620
|
registerAuthCommand(program2);
|
|
43621
|
+
registerChangelogCommand(program2);
|
|
42524
43622
|
registerBackfillCommand(program2);
|
|
42525
43623
|
registerCaptureCommand(program2);
|
|
42526
43624
|
registerDocsCommand(program2);
|
|
@@ -42529,7 +43627,9 @@ registerStatusCommand(program2);
|
|
|
42529
43627
|
registerInitCommand(program2);
|
|
42530
43628
|
registerUpdateCommand(program2);
|
|
42531
43629
|
registerPushCommand(program2);
|
|
43630
|
+
registerReviseCommand(program2);
|
|
42532
43631
|
registerMcpCommand(program2);
|
|
43632
|
+
registerTeachCommand(program2);
|
|
42533
43633
|
program2.parseAsync(process.argv).catch((error51) => {
|
|
42534
43634
|
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
42535
43635
|
console.error(message);
|