@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.
Files changed (3) hide show
  1. package/README.md +392 -0
  2. package/dist/index.js +1141 -41
  3. 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 { apiUrl: profile.apiUrl, userToken: profile.userToken };
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
- constructor(apiUrl, userToken) {
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
- return this.request("GET", "/api/me");
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 { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
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 storageMode = readRepoConfig(ctx.workingDir)?.storageMode ?? "repo";
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
- resolveToken(token);
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 = readdirSync(dir);
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("Scan, classify, and adopt existing markdown docs into .gitcontext/docs/").option("--local", "Adopt local .md/.mdx files found outside .gitcontext/docs/").action(async (options) => {
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("Unknown backfill mode. Use: gitcontext backfill --local");
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 = readUserConfig();
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 readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
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 readdirSync2(srcDir, { withFileTypes: true })) {
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 installApp = await dist_default5({
18965
- message: "Install the GitContext GitHub App to enable PR analysis and doc suggestions?",
18966
- default: true
18967
- });
18968
- if (installApp) {
18969
- const installUrl = `${GITHUB_APP_URL}?repo=${encodeURIComponent(repoName)}`;
18970
- console.log(`
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
- console.log(`If it doesn't open automatically, visit:
19403
+ console.log(`If it doesn't open automatically, visit:
18973
19404
  ${installUrl}
18974
19405
  `);
18975
- openBrowser(installUrl);
18976
- await dist_default5({ message: "Press enter once the GitHub App is installed", default: true });
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 = spawnSync("git", ["diff", "--cached", "--quiet"], { cwd: repoRoot }).status !== 0;
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 spawnSync2 } from "child_process";
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 = spawnSync2("git", ["diff", "--cached", "--quiet"], { cwd: repoRoot }).status !== 0;
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("0.1.0").option("--profile <name>", "Auth profile to use for this invocation (overrides active profile)");
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);