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 +212 -294
- package/dist/lib/pr-comment-worker.js +86 -0
- package/package.json +1 -1
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
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/commands/
|
|
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 ||
|
|
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:
|
|
163
|
-
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/
|
|
258
|
-
|
|
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
|
|
273
|
-
const globalConfig = existing || {
|
|
306
|
+
const globalConfig = {
|
|
274
307
|
server_url: serverUrl,
|
|
275
308
|
machine_id: machineId,
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
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/
|
|
334
|
+
// src/commands/unlink.ts
|
|
309
335
|
import { Command as Command2 } from "commander";
|
|
310
|
-
import
|
|
311
|
-
import
|
|
312
|
-
var
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
350
|
+
fs3.unlinkSync(configFile);
|
|
332
351
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
|
373
|
-
import
|
|
374
|
-
import
|
|
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
|
|
381
|
+
fail("Authenticated", "run `glop login` first");
|
|
390
382
|
console.log();
|
|
391
383
|
process.exit(1);
|
|
392
384
|
}
|
|
393
|
-
|
|
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
|
|
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
|
|
430
|
-
|
|
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(
|
|
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", "
|
|
427
|
+
check("pass", "Global hooks installed", `${glopEvents.length} events in ${globalSettingsFile}`);
|
|
440
428
|
} else {
|
|
441
|
-
fail("
|
|
429
|
+
fail("Global hooks installed", "run `glop login`");
|
|
442
430
|
}
|
|
443
431
|
} catch {
|
|
444
|
-
fail("
|
|
432
|
+
fail("Global hooks installed", `${globalSettingsFile} is corrupted`);
|
|
445
433
|
}
|
|
446
434
|
} else {
|
|
447
|
-
fail("
|
|
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 =
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
708
|
-
var
|
|
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
|
|
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
|
|
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(`
|
|
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.
|
|
742
|
-
process.exit(
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
process.
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
|
822
|
-
import
|
|
823
|
-
var CONFIG_DIR2 =
|
|
824
|
-
var CACHE_FILE =
|
|
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.
|
|
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
|
|
914
|
-
program.addCommand(
|
|
915
|
-
program.addCommand(
|
|
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(
|
|
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));
|