glop.dev 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -73,6 +73,26 @@ function getGitUserEmail() {
73
73
  return null;
74
74
  }
75
75
  }
76
+ function getCommitDiffStats() {
77
+ try {
78
+ const output = execSync("git diff --shortstat HEAD~1", {
79
+ encoding: "utf-8",
80
+ stdio: ["pipe", "pipe", "pipe"],
81
+ timeout: 3e3
82
+ }).trim();
83
+ if (!output) return null;
84
+ const files = output.match(/(\d+)\s+file/);
85
+ const insertions = output.match(/(\d+)\s+insertion/);
86
+ const deletions = output.match(/(\d+)\s+deletion/);
87
+ return {
88
+ files_changed: files ? parseInt(files[1], 10) : 0,
89
+ lines_added: insertions ? parseInt(insertions[1], 10) : 0,
90
+ lines_removed: deletions ? parseInt(deletions[1], 10) : 0
91
+ };
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
76
96
 
77
97
  // src/lib/config.ts
78
98
  var CONFIG_DIR = path.join(os.homedir(), ".glop");
@@ -435,6 +455,15 @@ var doctorCommand = new Command3("doctor").description("Check that glop is set u
435
455
  } catch {
436
456
  fail("CLI in PATH", "hooks won't fire \u2014 ensure `glop` is in your PATH");
437
457
  }
458
+ try {
459
+ const ghWhich = execSync2("which gh", {
460
+ encoding: "utf-8",
461
+ stdio: ["pipe", "pipe", "pipe"]
462
+ }).trim();
463
+ check("pass", "GitHub CLI (gh)", ghWhich);
464
+ } catch {
465
+ check("warn", "GitHub CLI (gh)", "PR comment features won't work \u2014 install from https://cli.github.com");
466
+ }
438
467
  console.log();
439
468
  if (hasFailure) {
440
469
  process.exit(1);
@@ -446,6 +475,10 @@ var doctorCommand = new Command3("doctor").description("Check that glop is set u
446
475
  // src/commands/hook.ts
447
476
  import { Command as Command4 } from "commander";
448
477
  import { openSync, readSync, closeSync, readFileSync } from "fs";
478
+ import { spawn } from "child_process";
479
+ import { fileURLToPath } from "url";
480
+ import path4 from "path";
481
+ var PR_URL_RE = /(https:\/\/github\.com\/[^\s]+\/pull\/\d+)/;
449
482
  function extractSlugFromTranscript(transcriptPath) {
450
483
  try {
451
484
  const fd = openSync(transcriptPath, "r");
@@ -480,11 +513,26 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
480
513
  payload.machine_id = config.machine_id;
481
514
  payload.git_user_name = getGitUserName();
482
515
  payload.git_user_email = getGitUserEmail();
516
+ if (payload.hook_event_name === "PostToolUse" && payload.tool_name === "Bash" && typeof payload.tool_response === "string" && /\bgit\s+commit\b/.test(
517
+ typeof payload.tool_input?.command === "string" ? payload.tool_input.command : ""
518
+ ) && /\[\w[^\]]*\s+[a-f0-9]{7,}\]/.test(payload.tool_response)) {
519
+ const diffStats = getCommitDiffStats();
520
+ if (diffStats) {
521
+ payload.commit_diff_stats = diffStats;
522
+ }
523
+ }
483
524
  const skipSlugEvents = /* @__PURE__ */ new Set(["PostToolUse"]);
484
525
  if (!skipSlugEvents.has(payload.hook_event_name) && typeof payload.transcript_path === "string") {
485
526
  const slug = extractSlugFromTranscript(payload.transcript_path);
486
527
  if (slug) payload.slug = slug;
487
528
  }
529
+ let prUrl = null;
530
+ if (payload.hook_event_name === "PostToolUse" && payload.tool_name === "Bash" && typeof payload.tool_response === "string" && /\bgh\s+pr\s+create\b/.test(
531
+ typeof payload.tool_input?.command === "string" ? payload.tool_input.command : ""
532
+ )) {
533
+ const prMatch = payload.tool_response.match(PR_URL_RE);
534
+ if (prMatch) prUrl = prMatch[1];
535
+ }
488
536
  try {
489
537
  const res = await fetch(`${config.server_url}/api/v1/ingest/hook`, {
490
538
  method: "POST",
@@ -495,10 +543,10 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
495
543
  body: JSON.stringify(payload),
496
544
  signal: AbortSignal.timeout(5e3)
497
545
  });
546
+ const resBody = res.ok ? await res.json().catch(() => ({})) : null;
498
547
  if (payload.hook_event_name === "SessionStart") {
499
- if (res.ok) {
500
- const body = await res.json();
501
- const runUrl = body.run_id ? `${config.server_url}/runs/${body.run_id}` : null;
548
+ if (res.ok && resBody) {
549
+ const runUrl = resBody.run_id ? `${config.server_url}/runs/${resBody.run_id}` : null;
502
550
  console.log(`glop: connected to ${config.server_url}`);
503
551
  if (runUrl) {
504
552
  console.log(`glop: Add "Glop-Run: ${runUrl}" as a git trailer in all commit messages and PR descriptions.`);
@@ -509,6 +557,22 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
509
557
  console.log(`glop: server returned HTTP ${res.status}`);
510
558
  }
511
559
  }
560
+ if (prUrl && resBody?.run_id) {
561
+ try {
562
+ const workerPath = path4.join(
563
+ path4.dirname(fileURLToPath(import.meta.url)),
564
+ "lib",
565
+ "pr-comment-worker.js"
566
+ );
567
+ const child = spawn(
568
+ process.execPath,
569
+ [workerPath, config.server_url, resBody.run_id, prUrl],
570
+ { detached: true, stdio: "ignore", env: { ...process.env, GLOP_API_KEY: config.api_key } }
571
+ );
572
+ child.unref();
573
+ } catch {
574
+ }
575
+ }
512
576
  } catch {
513
577
  if (payload.hook_event_name === "SessionStart") {
514
578
  console.log(`glop: server unreachable at ${config.server_url}`);
@@ -520,7 +584,7 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
520
584
  import { Command as Command5 } from "commander";
521
585
  import { execSync as execSync3 } from "child_process";
522
586
  import fs4 from "fs";
523
- import path4 from "path";
587
+ import path5 from "path";
524
588
  function hasGlopHooks(settings) {
525
589
  const hooks = settings.hooks;
526
590
  if (!hooks) return false;
@@ -558,13 +622,18 @@ var initCommand = new Command5("init").description("Install Claude Code hooks in
558
622
  } catch {
559
623
  console.warn("Warning: `glop` not found in PATH. Hooks won't fire until it's accessible.");
560
624
  }
625
+ try {
626
+ execSync3("which gh", { stdio: ["pipe", "pipe", "pipe"] });
627
+ } catch {
628
+ console.warn("Warning: `gh` (GitHub CLI) not found. PR comment features won't work. Install from https://cli.github.com");
629
+ }
561
630
  const repoRoot = getRepoRoot();
562
631
  if (!repoRoot) {
563
632
  console.warn("Warning: not in a git repository. Repo and branch tracking will be limited.");
564
633
  }
565
634
  const baseDir = repoRoot || process.cwd();
566
- const claudeDir = path4.join(baseDir, ".claude");
567
- const settingsFile = path4.join(claudeDir, "settings.json");
635
+ const claudeDir = path5.join(baseDir, ".claude");
636
+ const settingsFile = path5.join(claudeDir, "settings.json");
568
637
  if (!fs4.existsSync(claudeDir)) {
569
638
  fs4.mkdirSync(claudeDir, { recursive: true });
570
639
  }
@@ -604,7 +673,9 @@ var updateCommand = new Command6("update").description("Update glop to the lates
604
673
  console.log("Updating glop\u2026");
605
674
  try {
606
675
  execSync4("npm install -g glop.dev@latest", { stdio: "inherit" });
607
- console.log("\nglop has been updated successfully.");
676
+ const version = execSync4("glop --version", { encoding: "utf-8" }).trim();
677
+ console.log(`
678
+ glop has been updated successfully to v${version}.`);
608
679
  } catch {
609
680
  process.exitCode = 1;
610
681
  }
@@ -788,10 +859,10 @@ var workspaceCommand = new Command7("workspace").description("View or switch wor
788
859
 
789
860
  // src/lib/update-check.ts
790
861
  import fs5 from "fs";
791
- import path5 from "path";
862
+ import path6 from "path";
792
863
  import os2 from "os";
793
- var CONFIG_DIR2 = path5.join(os2.homedir(), ".glop");
794
- var CACHE_FILE = path5.join(CONFIG_DIR2, "update-check.json");
864
+ var CONFIG_DIR2 = path6.join(os2.homedir(), ".glop");
865
+ var CACHE_FILE = path6.join(CONFIG_DIR2, "update-check.json");
795
866
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
796
867
  function ensureConfigDir2() {
797
868
  if (!fs5.existsSync(CONFIG_DIR2)) {
@@ -852,7 +923,7 @@ async function checkForUpdate(currentVersion) {
852
923
  // package.json
853
924
  var package_default = {
854
925
  name: "glop.dev",
855
- version: "0.8.0",
926
+ version: "0.10.0",
856
927
  type: "module",
857
928
  bin: {
858
929
  glop: "./dist/index.js"
@@ -871,6 +942,7 @@ var package_default = {
871
942
  commander: "^13.0.0"
872
943
  },
873
944
  devDependencies: {
945
+ "@types/node": "^25.5.0",
874
946
  tsup: "^8.3.0",
875
947
  tsx: "^4.19.0",
876
948
  typescript: "^5.7.0",
@@ -0,0 +1,86 @@
1
+ // src/lib/pr-comment-worker.ts
2
+ import { execFileSync } from "child_process";
3
+ var [serverUrl, runId, prUrl] = process.argv.slice(2);
4
+ var apiKey = process.env.GLOP_API_KEY;
5
+ if (!serverUrl || !apiKey || !runId || !prUrl) {
6
+ process.exit(1);
7
+ }
8
+ async function main() {
9
+ const contextRes = await fetch(`${serverUrl}/api/v1/runs/${runId}/context`, {
10
+ headers: { Authorization: `Bearer ${apiKey}` },
11
+ signal: AbortSignal.timeout(1e4)
12
+ });
13
+ if (!contextRes.ok) {
14
+ process.exit(1);
15
+ }
16
+ const context = await contextRes.json();
17
+ const prompt = [
18
+ "Generate a concise GitHub PR comment summarizing this AI coding session.",
19
+ "Output ONLY the markdown body \u2014 no wrapping, no ```markdown fences, no preamble.",
20
+ "",
21
+ `Session title: ${context.title || "Untitled"}`,
22
+ `Session summary: ${context.summary || "No summary"}`,
23
+ "",
24
+ "Developer prompts:",
25
+ ...context.prompts.map((p, i) => `${i + 1}. ${p}`),
26
+ "",
27
+ "Actions taken:",
28
+ ...context.tool_use_labels.map((l, i) => `${i + 1}. ${l}`),
29
+ "",
30
+ "Files touched:",
31
+ ...context.files_touched.map((f) => `- ${f}`),
32
+ "",
33
+ "Format the comment with:",
34
+ "- A blockquote with the developer's core request",
35
+ "- 2-3 sentences on how the AI approached the task",
36
+ "- A bullet list of key decisions (if any)",
37
+ "- A collapsible <details> section listing files touched",
38
+ `- Stats line: ${context.event_count} events \xB7 ${context.file_count} files`
39
+ ].join("\n");
40
+ let commentBody;
41
+ try {
42
+ commentBody = execFileSync("claude", ["-p", prompt], {
43
+ encoding: "utf-8",
44
+ timeout: 6e4,
45
+ maxBuffer: 1024 * 1024
46
+ }).trim();
47
+ } catch {
48
+ commentBody = buildTemplate(context);
49
+ }
50
+ if (!commentBody) {
51
+ commentBody = buildTemplate(context);
52
+ }
53
+ const runUrl = `${serverUrl}/runs/${runId}`;
54
+ const fullComment = [
55
+ commentBody,
56
+ "",
57
+ `<sub>[View in Glop](${runUrl}) \xB7 Posted by [Glop](${serverUrl})</sub>`
58
+ ].join("\n");
59
+ execFileSync("gh", ["pr", "comment", prUrl, "--body", fullComment], {
60
+ encoding: "utf-8",
61
+ timeout: 15e3
62
+ });
63
+ }
64
+ function buildTemplate(context) {
65
+ const parts = [];
66
+ parts.push(`> ${context.prompts[0] || "No prompt recorded"}
67
+ `);
68
+ parts.push(`${context.summary || context.title || "No summary available"}
69
+ `);
70
+ if (context.files_touched.length > 0) {
71
+ parts.push(
72
+ "<details>",
73
+ `<summary>Files touched (${context.files_touched.length})</summary>
74
+ `
75
+ );
76
+ for (const file of context.files_touched) {
77
+ parts.push(`- \`${file}\``);
78
+ }
79
+ parts.push("\n</details>\n");
80
+ }
81
+ parts.push(
82
+ `<sub>${context.event_count} events \xB7 ${context.file_count} files</sub>`
83
+ );
84
+ return parts.join("\n");
85
+ }
86
+ main().catch(() => process.exit(1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glop.dev",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "glop": "./dist/index.js"
@@ -19,6 +19,7 @@
19
19
  "commander": "^13.0.0"
20
20
  },
21
21
  "devDependencies": {
22
+ "@types/node": "^25.5.0",
22
23
  "tsup": "^8.3.0",
23
24
  "tsx": "^4.19.0",
24
25
  "typescript": "^5.7.0",