glop.dev 0.9.0 → 0.11.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
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command8 } from "commander";
4
+ import { Command as Command7 } from "commander";
5
5
 
6
- // src/commands/auth.ts
6
+ // src/commands/login.ts
7
7
  import { Command } from "commander";
8
8
 
9
9
  // src/lib/config.ts
@@ -151,21 +151,13 @@ function saveRepoConfig(config) {
151
151
  }
152
152
  function loadConfig() {
153
153
  const global = loadGlobalConfig();
154
- if (!global || Object.keys(global.workspaces).length === 0) return null;
155
- const repoConfig = loadRepoConfig();
156
- const workspaceId = repoConfig?.workspace_id || global.default_workspace || Object.keys(global.workspaces)[0];
157
- if (!workspaceId) return null;
158
- const ws = global.workspaces[workspaceId];
159
- if (!ws) return null;
154
+ if (!global || !global.api_key) return null;
160
155
  return {
161
156
  server_url: global.server_url,
162
- api_key: ws.api_key,
163
- developer_id: ws.developer_id,
157
+ api_key: global.api_key,
158
+ developer_id: global.developer_id,
164
159
  developer_name: global.developer_name,
165
- machine_id: global.machine_id,
166
- workspace_id: workspaceId,
167
- workspace_name: ws.workspace_name,
168
- workspace_slug: ws.workspace_slug
160
+ machine_id: global.machine_id
169
161
  };
170
162
  }
171
163
  function getDefaultServerUrl() {
@@ -223,10 +215,7 @@ function waitForCallback(port) {
223
215
  resolve({
224
216
  api_key: apiKey,
225
217
  developer_id: developerId,
226
- developer_name: developerName,
227
- workspace_id: url.searchParams.get("workspace_id") || void 0,
228
- workspace_name: url.searchParams.get("workspace_name") || void 0,
229
- workspace_slug: url.searchParams.get("workspace_slug") || void 0
218
+ developer_name: developerName
230
219
  });
231
220
  return;
232
221
  }
@@ -254,8 +243,53 @@ h1{margin:0 0 1rem;font-size:1.25rem}</style></head>
254
243
  </html>`;
255
244
  }
256
245
 
257
- // src/commands/auth.ts
258
- var authCommand = new Command("auth").description("Authenticate with a glop server").option("-s, --server <url>", "Server URL").action(async (opts) => {
246
+ // src/commands/login.ts
247
+ import { execSync as execSync2 } from "child_process";
248
+ import fs2 from "fs";
249
+ import path2 from "path";
250
+ import os2 from "os";
251
+ function installGlobalHooks() {
252
+ const claudeDir = path2.join(os2.homedir(), ".claude");
253
+ const settingsFile = path2.join(claudeDir, "settings.json");
254
+ if (!fs2.existsSync(claudeDir)) {
255
+ fs2.mkdirSync(claudeDir, { recursive: true });
256
+ }
257
+ let settings = {};
258
+ if (fs2.existsSync(settingsFile)) {
259
+ try {
260
+ settings = JSON.parse(fs2.readFileSync(settingsFile, "utf-8"));
261
+ } catch {
262
+ }
263
+ }
264
+ const hookHandler = {
265
+ type: "command",
266
+ command: "glop __hook"
267
+ };
268
+ const hookGroup = {
269
+ hooks: [hookHandler]
270
+ };
271
+ const hooks = settings.hooks || {};
272
+ const hookEvents = [
273
+ "PostToolUse",
274
+ "PermissionRequest",
275
+ "Stop",
276
+ "UserPromptSubmit",
277
+ "SessionStart",
278
+ "SessionEnd"
279
+ ];
280
+ for (const event of hookEvents) {
281
+ const existing = (hooks[event] || []).filter((group) => {
282
+ const groupHooks = group?.hooks || [];
283
+ return !groupHooks.some(
284
+ (h) => h?.command?.includes("glop __hook")
285
+ );
286
+ });
287
+ hooks[event] = [...existing, hookGroup];
288
+ }
289
+ settings.hooks = hooks;
290
+ fs2.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
291
+ }
292
+ var loginCommand = new Command("login").description("Authenticate with a glop server").option("-s, --server <url>", "Server URL").action(async (opts) => {
259
293
  const serverUrl = (opts.server || getDefaultServerUrl()).replace(/\/+$/, "");
260
294
  const port = await findOpenPort();
261
295
  const machineId = getMachineId();
@@ -269,109 +303,67 @@ var authCommand = new Command("auth").description("Authenticate with a glop serv
269
303
  console.log("Waiting for authorization...");
270
304
  openBrowser(authUrl);
271
305
  const result = await waitForCallback(port);
272
- const existing = loadGlobalConfig();
273
- const globalConfig = existing || {
306
+ const globalConfig = {
274
307
  server_url: serverUrl,
275
308
  machine_id: machineId,
276
- developer_name: result.developer_name,
277
- workspaces: {}
309
+ api_key: result.api_key,
310
+ developer_id: result.developer_id,
311
+ developer_name: result.developer_name
278
312
  };
279
- globalConfig.server_url = serverUrl;
280
- globalConfig.machine_id = machineId;
281
- globalConfig.developer_name = result.developer_name;
282
- if (result.workspace_id) {
283
- globalConfig.workspaces[result.workspace_id] = {
284
- api_key: result.api_key,
285
- developer_id: result.developer_id,
286
- workspace_name: result.workspace_name,
287
- workspace_slug: result.workspace_slug
288
- };
289
- globalConfig.default_workspace = result.workspace_id;
290
- }
291
313
  saveGlobalConfig(globalConfig);
314
+ installGlobalHooks();
315
+ try {
316
+ execSync2("which glop", { stdio: ["pipe", "pipe", "pipe"] });
317
+ } catch {
318
+ console.warn("\nWarning: `glop` not found in PATH. Hooks won't fire until it's accessible.");
319
+ }
292
320
  console.log("\nAuthenticated successfully!");
293
321
  console.log(` Developer: ${result.developer_name}`);
294
- if (result.workspace_name) {
295
- console.log(` Workspace: ${result.workspace_name}`);
296
- }
297
322
  console.log(` Server: ${serverUrl}`);
298
323
  console.log(` Machine: ${machineId.slice(0, 8)}...`);
299
324
  console.log(`
300
325
  API key saved to ~/.glop/config.json`);
326
+ console.log(`Hooks installed in ~/.claude/settings.json`);
301
327
  console.log(
302
328
  `
303
- \u2192 Run \`glop init\` in a repo to start streaming sessions.`
329
+ \u2192 Run \`glop link\` in a repo to start streaming sessions.`
304
330
  );
305
331
  process.exit(0);
306
332
  });
307
333
 
308
- // src/commands/deactivate.ts
334
+ // src/commands/unlink.ts
309
335
  import { Command as Command2 } from "commander";
310
- import fs2 from "fs";
311
- import path2 from "path";
312
- var HOOK_EVENTS = [
313
- "PostToolUse",
314
- "PermissionRequest",
315
- "Stop",
316
- "UserPromptSubmit",
317
- "SessionStart",
318
- "SessionEnd"
319
- ];
320
- var deactivateCommand = new Command2("deactivate").description("Remove glop hooks from the current repo").action(async () => {
336
+ import fs3 from "fs";
337
+ import path3 from "path";
338
+ var unlinkCommand = new Command2("unlink").description("Unbind this repo from its glop workspace").action(async () => {
321
339
  const repoRoot = getRepoRoot();
322
340
  if (!repoRoot) {
323
341
  console.error("Not in a git repository.");
324
342
  process.exit(1);
325
343
  }
326
- const settingsFile = path2.join(repoRoot, ".claude", "settings.json");
327
- if (!fs2.existsSync(settingsFile)) {
328
- console.log("No .claude/settings.json found. Nothing to remove.");
344
+ const glopDir = path3.join(repoRoot, ".glop");
345
+ const configFile = path3.join(glopDir, "config.json");
346
+ if (!fs3.existsSync(configFile)) {
347
+ console.log("This repo is not bound to a workspace. Nothing to do.");
329
348
  return;
330
349
  }
331
- let settings;
350
+ fs3.unlinkSync(configFile);
332
351
  try {
333
- settings = JSON.parse(fs2.readFileSync(settingsFile, "utf-8"));
334
- } catch {
335
- console.error("Could not parse .claude/settings.json");
336
- process.exit(1);
337
- }
338
- const hooks = settings.hooks;
339
- if (!hooks) {
340
- console.log("No hooks found in settings. Nothing to remove.");
341
- return;
342
- }
343
- let removed = 0;
344
- for (const event of HOOK_EVENTS) {
345
- if (!hooks[event]) continue;
346
- const before = hooks[event].length;
347
- hooks[event] = hooks[event].filter((group) => {
348
- const groupHooks = group?.hooks || [];
349
- return !groupHooks.some(
350
- (h) => h.command && (h.command.includes("glop __hook") || h.command.includes("/api/v1/ingest/hook"))
351
- );
352
- });
353
- removed += before - hooks[event].length;
354
- if (hooks[event].length === 0) {
355
- delete hooks[event];
352
+ const remaining = fs3.readdirSync(glopDir);
353
+ if (remaining.length === 0) {
354
+ fs3.rmdirSync(glopDir);
356
355
  }
356
+ } catch {
357
357
  }
358
- if (Object.keys(hooks).length === 0) {
359
- delete settings.hooks;
360
- }
361
- fs2.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
362
- if (removed > 0) {
363
- console.log(`Removed glop hooks from ${removed} event(s).`);
364
- console.log(` Settings: ${settingsFile}`);
365
- } else {
366
- console.log("No glop hooks found. Nothing to remove.");
367
- }
358
+ console.log("\u2713 Workspace binding removed. Hooks will no-op for this repo.");
368
359
  });
369
360
 
370
361
  // src/commands/doctor.ts
371
362
  import { Command as Command3 } from "commander";
372
- import { execSync as execSync2 } from "child_process";
373
- import fs3 from "fs";
374
- import path3 from "path";
363
+ import { execSync as execSync3 } from "child_process";
364
+ import fs4 from "fs";
365
+ import path4 from "path";
366
+ import os3 from "os";
375
367
  function check(status, label, detail) {
376
368
  const icon = status === "pass" ? "\u2713" : status === "fail" ? "\u2717" : "!";
377
369
  const line = ` ${icon} ${label}`;
@@ -386,14 +378,11 @@ var doctorCommand = new Command3("doctor").description("Check that glop is set u
386
378
  };
387
379
  const config = loadConfig();
388
380
  if (!config) {
389
- fail("Authenticated", "run `glop auth` first");
381
+ fail("Authenticated", "run `glop login` first");
390
382
  console.log();
391
383
  process.exit(1);
392
384
  }
393
- const repoBinding = loadRepoConfig();
394
- const wsSource = repoBinding?.workspace_id ? "repo binding" : "default";
395
- const authDetail = config.workspace_name ? `${config.developer_name} on ${config.server_url} (${config.workspace_name}, ${wsSource})` : `${config.developer_name} on ${config.server_url}`;
396
- check("pass", "Authenticated", authDetail);
385
+ check("pass", "Authenticated", `${config.developer_name} on ${config.server_url}`);
397
386
  try {
398
387
  const res = await fetch(`${config.server_url}/api/v1/health`, {
399
388
  headers: {
@@ -405,7 +394,7 @@ var doctorCommand = new Command3("doctor").description("Check that glop is set u
405
394
  if (res.ok) {
406
395
  check("pass", "Server reachable");
407
396
  } else if (res.status === 401) {
408
- fail("API key valid", "re-run `glop auth`");
397
+ fail("API key valid", "re-run `glop login`");
409
398
  } else {
410
399
  fail("Server reachable", `HTTP ${res.status}`);
411
400
  }
@@ -426,28 +415,35 @@ var doctorCommand = new Command3("doctor").description("Check that glop is set u
426
415
  check("warn", "Git remote", "no origin remote found");
427
416
  }
428
417
  }
429
- const baseDir = repoRoot || process.cwd();
430
- const settingsFile = path3.join(baseDir, ".claude", "settings.json");
431
- if (fs3.existsSync(settingsFile)) {
418
+ const globalSettingsFile = path4.join(os3.homedir(), ".claude", "settings.json");
419
+ if (fs4.existsSync(globalSettingsFile)) {
432
420
  try {
433
- const settings = JSON.parse(fs3.readFileSync(settingsFile, "utf-8"));
421
+ const settings = JSON.parse(fs4.readFileSync(globalSettingsFile, "utf-8"));
434
422
  const hooks = settings.hooks || {};
435
423
  const glopEvents = Object.entries(hooks).filter(
436
424
  ([, handlers]) => JSON.stringify(handlers).includes("glop __hook")
437
425
  );
438
426
  if (glopEvents.length > 0) {
439
- check("pass", "Hooks installed", `${glopEvents.length} events in ${settingsFile}`);
427
+ check("pass", "Global hooks installed", `${glopEvents.length} events in ${globalSettingsFile}`);
440
428
  } else {
441
- fail("Hooks installed", "run `glop init`");
429
+ fail("Global hooks installed", "run `glop login`");
442
430
  }
443
431
  } catch {
444
- fail("Hooks installed", `${settingsFile} is corrupted`);
432
+ fail("Global hooks installed", `${globalSettingsFile} is corrupted`);
445
433
  }
446
434
  } else {
447
- fail("Hooks installed", "run `glop init`");
435
+ fail("Global hooks installed", "run `glop login`");
436
+ }
437
+ const repoBinding = loadRepoConfig();
438
+ if (repoBinding) {
439
+ check("pass", "Repo bound to workspace", repoBinding.workspace_id);
440
+ } else if (repoRoot) {
441
+ check("warn", "Repo bound to workspace", "run `glop link` to bind this repo");
442
+ } else {
443
+ check("warn", "Repo bound to workspace", "not in a git repo");
448
444
  }
449
445
  try {
450
- const which = execSync2("which glop", {
446
+ const which = execSync3("which glop", {
451
447
  encoding: "utf-8",
452
448
  stdio: ["pipe", "pipe", "pipe"]
453
449
  }).trim();
@@ -455,6 +451,15 @@ var doctorCommand = new Command3("doctor").description("Check that glop is set u
455
451
  } catch {
456
452
  fail("CLI in PATH", "hooks won't fire \u2014 ensure `glop` is in your PATH");
457
453
  }
454
+ try {
455
+ const ghWhich = execSync3("which gh", {
456
+ encoding: "utf-8",
457
+ stdio: ["pipe", "pipe", "pipe"]
458
+ }).trim();
459
+ check("pass", "GitHub CLI (gh)", ghWhich);
460
+ } catch {
461
+ check("warn", "GitHub CLI (gh)", "PR comment features won't work \u2014 install from https://cli.github.com");
462
+ }
458
463
  console.log();
459
464
  if (hasFailure) {
460
465
  process.exit(1);
@@ -466,6 +471,10 @@ var doctorCommand = new Command3("doctor").description("Check that glop is set u
466
471
  // src/commands/hook.ts
467
472
  import { Command as Command4 } from "commander";
468
473
  import { openSync, readSync, closeSync, readFileSync } from "fs";
474
+ import { spawn } from "child_process";
475
+ import { fileURLToPath } from "url";
476
+ import path5 from "path";
477
+ var PR_URL_RE = /(https:\/\/github\.com\/[^\s]+\/pull\/\d+)/;
469
478
  function extractSlugFromTranscript(transcriptPath) {
470
479
  try {
471
480
  const fd = openSync(transcriptPath, "r");
@@ -485,6 +494,8 @@ function extractSlugFromTranscript(transcriptPath) {
485
494
  var hookCommand = new Command4("__hook").description("internal").action(async () => {
486
495
  const config = loadConfig();
487
496
  if (!config) return;
497
+ const repoConfig = loadRepoConfig();
498
+ if (!repoConfig) return;
488
499
  let input = "";
489
500
  for await (const chunk of process.stdin) {
490
501
  input += chunk;
@@ -498,6 +509,7 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
498
509
  payload.repo_key = getRepoKey() || payload.cwd || "unknown";
499
510
  payload.branch = getBranch();
500
511
  payload.machine_id = config.machine_id;
512
+ payload.workspace_id = repoConfig.workspace_id;
501
513
  payload.git_user_name = getGitUserName();
502
514
  payload.git_user_email = getGitUserEmail();
503
515
  if (payload.hook_event_name === "PostToolUse" && payload.tool_name === "Bash" && typeof payload.tool_response === "string" && /\bgit\s+commit\b/.test(
@@ -513,6 +525,13 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
513
525
  const slug = extractSlugFromTranscript(payload.transcript_path);
514
526
  if (slug) payload.slug = slug;
515
527
  }
528
+ let prUrl = null;
529
+ if (payload.hook_event_name === "PostToolUse" && payload.tool_name === "Bash" && typeof payload.tool_response === "string" && /\bgh\s+pr\s+create\b/.test(
530
+ typeof payload.tool_input?.command === "string" ? payload.tool_input.command : ""
531
+ )) {
532
+ const prMatch = payload.tool_response.match(PR_URL_RE);
533
+ if (prMatch) prUrl = prMatch[1];
534
+ }
516
535
  try {
517
536
  const res = await fetch(`${config.server_url}/api/v1/ingest/hook`, {
518
537
  method: "POST",
@@ -523,20 +542,36 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
523
542
  body: JSON.stringify(payload),
524
543
  signal: AbortSignal.timeout(5e3)
525
544
  });
545
+ const resBody = res.ok ? await res.json().catch(() => ({})) : null;
526
546
  if (payload.hook_event_name === "SessionStart") {
527
- if (res.ok) {
528
- const body = await res.json();
529
- const runUrl = body.run_id ? `${config.server_url}/runs/${body.run_id}` : null;
547
+ if (res.ok && resBody) {
548
+ const runUrl = resBody.run_id ? `${config.server_url}/runs/${resBody.run_id}` : null;
530
549
  console.log(`glop: connected to ${config.server_url}`);
531
550
  if (runUrl) {
532
551
  console.log(`glop: Add "Glop-Run: ${runUrl}" as a git trailer in all commit messages and PR descriptions.`);
533
552
  }
534
553
  } else if (res.status === 401) {
535
- console.log("glop: API key expired or invalid \u2014 run `glop auth` to re-authenticate");
554
+ console.log("glop: API key expired or invalid \u2014 run `glop login` to re-authenticate");
536
555
  } else {
537
556
  console.log(`glop: server returned HTTP ${res.status}`);
538
557
  }
539
558
  }
559
+ if (prUrl && resBody?.run_id) {
560
+ try {
561
+ const workerPath = path5.join(
562
+ path5.dirname(fileURLToPath(import.meta.url)),
563
+ "lib",
564
+ "pr-comment-worker.js"
565
+ );
566
+ const child = spawn(
567
+ process.execPath,
568
+ [workerPath, config.server_url, resBody.run_id, prUrl],
569
+ { detached: true, stdio: "ignore", env: { ...process.env, GLOP_API_KEY: config.api_key } }
570
+ );
571
+ child.unref();
572
+ } catch {
573
+ }
574
+ }
540
575
  } catch {
541
576
  if (payload.hook_event_name === "SessionStart") {
542
577
  console.log(`glop: server unreachable at ${config.server_url}`);
@@ -544,104 +579,8 @@ var hookCommand = new Command4("__hook").description("internal").action(async ()
544
579
  }
545
580
  });
546
581
 
547
- // src/commands/init.ts
582
+ // src/commands/link.ts
548
583
  import { Command as Command5 } from "commander";
549
- import { execSync as execSync3 } from "child_process";
550
- import fs4 from "fs";
551
- import path4 from "path";
552
- function hasGlopHooks(settings) {
553
- const hooks = settings.hooks;
554
- if (!hooks) return false;
555
- return Object.values(hooks).some(
556
- (handlers) => JSON.stringify(handlers).includes("glop __hook")
557
- );
558
- }
559
- var initCommand = new Command5("init").description("Install Claude Code hooks in the current repo").action(async () => {
560
- const config = loadConfig();
561
- if (!config) {
562
- console.error("Not authenticated. Run `glop auth` first.");
563
- process.exit(1);
564
- }
565
- try {
566
- const res = await fetch(`${config.server_url}/api/v1/health`, {
567
- headers: {
568
- Authorization: `Bearer ${config.api_key}`,
569
- "X-Machine-Id": config.machine_id
570
- },
571
- signal: AbortSignal.timeout(5e3)
572
- });
573
- if (res.status === 401) {
574
- console.error("API key is invalid or expired. Run `glop auth` again.");
575
- process.exit(1);
576
- }
577
- if (!res.ok) {
578
- console.error(`Server error: HTTP ${res.status}. Try again later.`);
579
- process.exit(1);
580
- }
581
- } catch {
582
- console.warn(`Warning: Cannot reach ${config.server_url}. Key validation skipped.`);
583
- }
584
- try {
585
- execSync3("which glop", { stdio: ["pipe", "pipe", "pipe"] });
586
- } catch {
587
- console.warn("Warning: `glop` not found in PATH. Hooks won't fire until it's accessible.");
588
- }
589
- const repoRoot = getRepoRoot();
590
- if (!repoRoot) {
591
- console.warn("Warning: not in a git repository. Repo and branch tracking will be limited.");
592
- }
593
- const baseDir = repoRoot || process.cwd();
594
- const claudeDir = path4.join(baseDir, ".claude");
595
- const settingsFile = path4.join(claudeDir, "settings.json");
596
- if (!fs4.existsSync(claudeDir)) {
597
- fs4.mkdirSync(claudeDir, { recursive: true });
598
- }
599
- let settings = {};
600
- const isUpdate = fs4.existsSync(settingsFile);
601
- if (isUpdate) {
602
- try {
603
- settings = JSON.parse(fs4.readFileSync(settingsFile, "utf-8"));
604
- } catch {
605
- }
606
- }
607
- const hadHooks = hasGlopHooks(settings);
608
- const hookHandler = {
609
- type: "command",
610
- command: "glop __hook"
611
- };
612
- const hookGroup = {
613
- hooks: [hookHandler]
614
- };
615
- const hooks = settings.hooks || {};
616
- hooks.PostToolUse = [hookGroup];
617
- hooks.PermissionRequest = [hookGroup];
618
- hooks.Stop = [hookGroup];
619
- hooks.UserPromptSubmit = [hookGroup];
620
- hooks.SessionStart = [hookGroup];
621
- hooks.SessionEnd = [hookGroup];
622
- settings.hooks = hooks;
623
- fs4.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
624
- const workspace = config.workspace_name || config.workspace_slug || config.workspace_id || "default";
625
- console.log(`${hadHooks ? "\u2713 glop updated" : "\u2713 glop connected"} to workspace "${workspace}" \u2014 sessions will appear at ${config.server_url}/live`);
626
- });
627
-
628
- // src/commands/update.ts
629
- import { Command as Command6 } from "commander";
630
- import { execSync as execSync4 } from "child_process";
631
- var updateCommand = new Command6("update").description("Update glop to the latest version").action(() => {
632
- console.log("Updating glop\u2026");
633
- try {
634
- execSync4("npm install -g glop.dev@latest", { stdio: "inherit" });
635
- const version = execSync4("glop --version", { encoding: "utf-8" }).trim();
636
- console.log(`
637
- glop has been updated successfully to v${version}.`);
638
- } catch {
639
- process.exitCode = 1;
640
- }
641
- });
642
-
643
- // src/commands/workspace.ts
644
- import { Command as Command7 } from "commander";
645
584
 
646
585
  // src/lib/select.ts
647
586
  function interactiveSelect(items, initialIndex = 0) {
@@ -704,13 +643,26 @@ function interactiveSelect(items, initialIndex = 0) {
704
643
  });
705
644
  }
706
645
 
707
- // src/commands/workspace.ts
708
- var workspaceCommand = new Command7("workspace").description("View or switch workspaces").action(async () => {
646
+ // src/commands/link.ts
647
+ var linkCommand = new Command5("link").description("Bind this repo to a glop workspace").action(async () => {
709
648
  const config = loadConfig();
710
649
  if (!config) {
711
- console.error("Not authenticated. Run `glop auth` first.");
650
+ console.error("Not authenticated. Run `glop login` first.");
651
+ process.exit(1);
652
+ }
653
+ const repoRoot = getRepoRoot();
654
+ if (!repoRoot) {
655
+ console.error("Not in a git repository. Run this from a git repo.");
712
656
  process.exit(1);
713
657
  }
658
+ const existingRepo = loadRepoConfig();
659
+ if (existingRepo) {
660
+ console.log(`This repo is already bound to workspace ${existingRepo.workspace_id}.`);
661
+ if (!process.stdin.isTTY) {
662
+ process.exit(0);
663
+ }
664
+ console.log("Re-running to switch workspace...\n");
665
+ }
714
666
  let data;
715
667
  try {
716
668
  const res = await fetch(`${config.server_url}/api/v1/cli/workspaces`, {
@@ -721,11 +673,11 @@ var workspaceCommand = new Command7("workspace").description("View or switch wor
721
673
  signal: AbortSignal.timeout(1e4)
722
674
  });
723
675
  if (res.status === 401) {
724
- console.error("API key is invalid. Run `glop auth` to re-authenticate.");
676
+ console.error("API key is invalid or expired. Run `glop login` again.");
725
677
  process.exit(1);
726
678
  }
727
679
  if (!res.ok) {
728
- console.error(`Failed to fetch workspaces (HTTP ${res.status}).`);
680
+ console.error(`Server error: HTTP ${res.status}. Try again later.`);
729
681
  process.exit(1);
730
682
  }
731
683
  data = await res.json();
@@ -738,90 +690,57 @@ var workspaceCommand = new Command7("workspace").description("View or switch wor
738
690
  process.exit(1);
739
691
  }
740
692
  if (data.workspaces.length === 0) {
741
- console.log("No workspaces found.");
742
- process.exit(0);
743
- }
744
- const repoConfig = loadRepoConfig();
745
- const currentId = repoConfig?.workspace_id || config.workspace_id || data.current_workspace_id;
746
- if (!process.stdin.isTTY) {
747
- const current = data.workspaces.find((w) => w.id === currentId);
748
- console.log(current ? current.name : currentId);
749
- process.exit(0);
693
+ console.error("No workspaces found. Create one at " + config.server_url);
694
+ process.exit(1);
750
695
  }
696
+ let selectedWorkspace;
751
697
  if (data.workspaces.length === 1) {
752
- console.log(` Workspace: ${data.workspaces[0].name}`);
753
- console.log(" (only workspace)");
754
- process.exit(0);
755
- }
756
- const items = data.workspaces.map((w) => {
757
- const marker = w.id === currentId ? "\u25CF" : "\u25CB";
758
- return `${marker} ${w.name}`;
759
- });
760
- const currentIndex = data.workspaces.findIndex((w) => w.id === currentId);
761
- console.log("\n Workspaces:\n");
762
- console.log(" \x1B[2m\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel\x1B[0m\n");
763
- const selected = await interactiveSelect(items, Math.max(currentIndex, 0));
764
- if (selected === null) {
765
- console.log("\n Cancelled.");
766
- process.exit(0);
767
- }
768
- const selectedWorkspace = data.workspaces[selected];
769
- if (selectedWorkspace.id === currentId) {
770
- console.log(`
771
- Already on ${selectedWorkspace.name}.`);
772
- process.exit(0);
773
- }
774
- const globalConfig = loadGlobalConfig();
775
- const existingCreds = globalConfig.workspaces[selectedWorkspace.id];
776
- const repoRoot = getRepoRoot();
777
- if (existingCreds) {
778
- if (repoRoot) {
779
- saveRepoConfig({ workspace_id: selectedWorkspace.id });
780
- } else {
781
- globalConfig.default_workspace = selectedWorkspace.id;
782
- saveGlobalConfig(globalConfig);
698
+ selectedWorkspace = data.workspaces[0];
699
+ } else {
700
+ if (!process.stdin.isTTY) {
701
+ console.error("Multiple workspaces available. Run interactively to choose.");
702
+ process.exit(1);
783
703
  }
784
- console.log(`
785
- Switched to ${selectedWorkspace.name}!`);
786
- process.exit(0);
704
+ const currentId = existingRepo?.workspace_id || data.current_workspace_id;
705
+ const items = data.workspaces.map((w) => {
706
+ const marker = w.id === currentId ? "\u25CF" : "\u25CB";
707
+ return `${marker} ${w.name}`;
708
+ });
709
+ const currentIndex = data.workspaces.findIndex((w) => w.id === currentId);
710
+ console.log(" Select a workspace:\n");
711
+ console.log(" \x1B[2m\u2191/\u2193 navigate \xB7 Enter select \xB7 Esc cancel\x1B[0m\n");
712
+ const selected = await interactiveSelect(items, Math.max(currentIndex, 0));
713
+ if (selected === null) {
714
+ console.log("\n Cancelled.");
715
+ process.exit(0);
716
+ }
717
+ selectedWorkspace = data.workspaces[selected];
787
718
  }
788
- console.log(`
789
- Switching to ${selectedWorkspace.name}...`);
790
- console.log(" Opening browser for authorization...\n");
791
- const port = await findOpenPort();
792
- const machineId = getMachineId();
793
- const authUrl = `${config.server_url}/cli-auth?port=${port}&workspace_id=${selectedWorkspace.id}`;
794
- console.log(" If the browser doesn't open, visit this URL manually:");
795
- console.log(` ${authUrl}
796
- `);
797
- console.log(" Waiting for authorization...");
798
- openBrowser(authUrl);
799
- const result = await waitForCallback(port);
800
- const wsId = result.workspace_id || selectedWorkspace.id;
801
- globalConfig.workspaces[wsId] = {
802
- api_key: result.api_key,
803
- developer_id: result.developer_id,
804
- workspace_name: result.workspace_name,
805
- workspace_slug: result.workspace_slug
806
- };
807
- globalConfig.developer_name = result.developer_name;
808
- if (repoRoot) {
809
- saveRepoConfig({ workspace_id: wsId });
810
- } else {
811
- globalConfig.default_workspace = wsId;
719
+ saveRepoConfig({ workspace_id: selectedWorkspace.id });
720
+ console.log(`\u2713 Bound to workspace "${selectedWorkspace.name}" \u2014 sessions will appear at ${config.server_url}/live`);
721
+ });
722
+
723
+ // src/commands/update.ts
724
+ import { Command as Command6 } from "commander";
725
+ import { execSync as execSync4 } from "child_process";
726
+ var updateCommand = new Command6("update").description("Update glop to the latest version").action(() => {
727
+ console.log("Updating glop\u2026");
728
+ try {
729
+ execSync4("npm install -g glop.dev@latest", { stdio: "inherit" });
730
+ const version = execSync4("glop --version", { encoding: "utf-8" }).trim();
731
+ console.log(`
732
+ glop has been updated successfully to v${version}.`);
733
+ } catch {
734
+ process.exitCode = 1;
812
735
  }
813
- saveGlobalConfig(globalConfig);
814
- console.log(`
815
- Switched to ${result.workspace_name || selectedWorkspace.name}!`);
816
- process.exit(0);
817
736
  });
818
737
 
819
738
  // src/lib/update-check.ts
820
739
  import fs5 from "fs";
821
- import path5 from "path";
822
- import os2 from "os";
823
- var CONFIG_DIR2 = path5.join(os2.homedir(), ".glop");
824
- var CACHE_FILE = path5.join(CONFIG_DIR2, "update-check.json");
740
+ import path6 from "path";
741
+ import os4 from "os";
742
+ var CONFIG_DIR2 = path6.join(os4.homedir(), ".glop");
743
+ var CACHE_FILE = path6.join(CONFIG_DIR2, "update-check.json");
825
744
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
826
745
  function ensureConfigDir2() {
827
746
  if (!fs5.existsSync(CONFIG_DIR2)) {
@@ -882,7 +801,7 @@ async function checkForUpdate(currentVersion) {
882
801
  // package.json
883
802
  var package_default = {
884
803
  name: "glop.dev",
885
- version: "0.9.0",
804
+ version: "0.11.0",
886
805
  type: "module",
887
806
  bin: {
888
807
  glop: "./dist/index.js"
@@ -910,14 +829,13 @@ var package_default = {
910
829
  };
911
830
 
912
831
  // src/index.ts
913
- var program = new Command8().name("glop").description("Passive control plane for local Claude-driven development").version(package_default.version);
914
- program.addCommand(authCommand);
915
- program.addCommand(deactivateCommand);
832
+ var program = new Command7().name("glop").description("Passive control plane for local Claude-driven development").version(package_default.version);
833
+ program.addCommand(loginCommand);
834
+ program.addCommand(unlinkCommand);
916
835
  program.addCommand(doctorCommand);
917
836
  program.addCommand(hookCommand, { hidden: true });
918
- program.addCommand(initCommand);
837
+ program.addCommand(linkCommand);
919
838
  program.addCommand(updateCommand);
920
- program.addCommand(workspaceCommand);
921
839
  program.hook("postAction", async (_thisCommand, actionCommand) => {
922
840
  if (actionCommand.name() === "__hook") return;
923
841
  await checkForUpdate(package_default.version);
@@ -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.9.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "glop": "./dist/index.js"