glop.dev 0.1.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.d.ts +1 -0
- package/dist/index.js +611 -0
- package/package.json +27 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command7 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/auth.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/lib/config.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
var CONFIG_DIR = path.join(os.homedir(), ".glop");
|
|
15
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
16
|
+
function ensureConfigDir() {
|
|
17
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
18
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getMachineId() {
|
|
22
|
+
const machineIdFile = path.join(CONFIG_DIR, "machine_id");
|
|
23
|
+
ensureConfigDir();
|
|
24
|
+
if (fs.existsSync(machineIdFile)) {
|
|
25
|
+
return fs.readFileSync(machineIdFile, "utf-8").trim();
|
|
26
|
+
}
|
|
27
|
+
const machineId = crypto.randomUUID();
|
|
28
|
+
fs.writeFileSync(machineIdFile, machineId);
|
|
29
|
+
return machineId;
|
|
30
|
+
}
|
|
31
|
+
function loadConfig() {
|
|
32
|
+
if (!fs.existsSync(CONFIG_FILE)) return null;
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function saveConfig(config) {
|
|
41
|
+
ensureConfigDir();
|
|
42
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
43
|
+
}
|
|
44
|
+
function getServerUrl() {
|
|
45
|
+
return process.env.GLOP_SERVER_URL || loadConfig()?.server_url || "https://www.glop.dev";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/commands/auth.ts
|
|
49
|
+
import http from "http";
|
|
50
|
+
import { exec } from "child_process";
|
|
51
|
+
function openBrowser(url) {
|
|
52
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
53
|
+
const child = exec(`${cmd} "${url}"`);
|
|
54
|
+
child.unref();
|
|
55
|
+
}
|
|
56
|
+
function findOpenPort() {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const server = http.createServer();
|
|
59
|
+
server.listen(0, () => {
|
|
60
|
+
const addr = server.address();
|
|
61
|
+
if (addr && typeof addr === "object") {
|
|
62
|
+
const port = addr.port;
|
|
63
|
+
server.close(() => resolve(port));
|
|
64
|
+
} else {
|
|
65
|
+
reject(new Error("Could not find open port"));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
var authCommand = new Command("auth").description("Authenticate with a glop server").option("-s, --server <url>", "Server URL").action(async (opts) => {
|
|
71
|
+
const serverUrl = (opts.server || getServerUrl()).replace(/\/+$/, "");
|
|
72
|
+
const port = await findOpenPort();
|
|
73
|
+
const machineId = getMachineId();
|
|
74
|
+
console.log("Opening browser for authentication...");
|
|
75
|
+
console.log(
|
|
76
|
+
"If the browser doesn't open, visit this URL manually:"
|
|
77
|
+
);
|
|
78
|
+
const authUrl = `${serverUrl}/cli-auth?port=${port}`;
|
|
79
|
+
console.log(` ${authUrl}
|
|
80
|
+
`);
|
|
81
|
+
console.log("Waiting for authorization...");
|
|
82
|
+
openBrowser(authUrl);
|
|
83
|
+
const result = await waitForCallback(port);
|
|
84
|
+
saveConfig({
|
|
85
|
+
server_url: serverUrl,
|
|
86
|
+
api_key: result.api_key,
|
|
87
|
+
developer_id: result.developer_id,
|
|
88
|
+
developer_name: result.developer_name,
|
|
89
|
+
machine_id: machineId
|
|
90
|
+
});
|
|
91
|
+
console.log("\nAuthenticated successfully!");
|
|
92
|
+
console.log(` Developer: ${result.developer_name}`);
|
|
93
|
+
console.log(` Server: ${serverUrl}`);
|
|
94
|
+
console.log(` Machine: ${machineId.slice(0, 8)}...`);
|
|
95
|
+
console.log(`
|
|
96
|
+
API key saved to ~/.glop/config.json`);
|
|
97
|
+
console.log(
|
|
98
|
+
`
|
|
99
|
+
\u2192 Run \`glop init\` in a repo to start streaming sessions.`
|
|
100
|
+
);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
});
|
|
103
|
+
function waitForCallback(port) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
server.close();
|
|
107
|
+
reject(new Error("Authentication timed out after 5 minutes"));
|
|
108
|
+
}, 5 * 60 * 1e3);
|
|
109
|
+
const server = http.createServer((req, res) => {
|
|
110
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
111
|
+
if (url.pathname === "/callback") {
|
|
112
|
+
const apiKey = url.searchParams.get("api_key");
|
|
113
|
+
const developerId = url.searchParams.get("developer_id");
|
|
114
|
+
const developerName = url.searchParams.get("developer_name");
|
|
115
|
+
const error = url.searchParams.get("error");
|
|
116
|
+
if (error) {
|
|
117
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
118
|
+
res.end(htmlPage("Authentication Failed", `<p>Error: ${escapeHtml(error)}</p><p>You can close this tab.</p>`));
|
|
119
|
+
clearTimeout(timeout);
|
|
120
|
+
server.close();
|
|
121
|
+
reject(new Error(error));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (apiKey && developerId && developerName) {
|
|
125
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
126
|
+
res.end(htmlPage("Authenticated!", `<p>You can close this tab and return to the terminal.</p>`));
|
|
127
|
+
clearTimeout(timeout);
|
|
128
|
+
server.close();
|
|
129
|
+
resolve({
|
|
130
|
+
api_key: apiKey,
|
|
131
|
+
developer_id: developerId,
|
|
132
|
+
developer_name: developerName
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
137
|
+
res.end(htmlPage("Error", "<p>Missing parameters. Please try again.</p>"));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
res.writeHead(404);
|
|
141
|
+
res.end("Not found");
|
|
142
|
+
});
|
|
143
|
+
server.listen(port, "127.0.0.1");
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function escapeHtml(str) {
|
|
147
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
148
|
+
}
|
|
149
|
+
function htmlPage(title, body) {
|
|
150
|
+
return `<!DOCTYPE html>
|
|
151
|
+
<html>
|
|
152
|
+
<head><title>glop - ${escapeHtml(title)}</title>
|
|
153
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa}
|
|
154
|
+
.card{background:white;border:1px solid #e5e5e5;border-radius:8px;padding:2rem;text-align:center;max-width:400px}
|
|
155
|
+
h1{margin:0 0 1rem;font-size:1.25rem}</style></head>
|
|
156
|
+
<body><div class="card"><h1>${escapeHtml(title)}</h1>${body}</div></body>
|
|
157
|
+
</html>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/commands/deactivate.ts
|
|
161
|
+
import { Command as Command2 } from "commander";
|
|
162
|
+
|
|
163
|
+
// src/lib/git.ts
|
|
164
|
+
import { execSync } from "child_process";
|
|
165
|
+
function getRepoRoot() {
|
|
166
|
+
try {
|
|
167
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
170
|
+
}).trim();
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function getRepoKey() {
|
|
176
|
+
try {
|
|
177
|
+
const remote = execSync("git remote get-url origin", {
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
180
|
+
}).trim();
|
|
181
|
+
const match = remote.match(
|
|
182
|
+
/(?:github\.com|gitlab\.com|bitbucket\.org)[/:](.+?)(?:\.git)?$/
|
|
183
|
+
);
|
|
184
|
+
if (match) return match[1];
|
|
185
|
+
const parts = remote.split("/").filter(Boolean);
|
|
186
|
+
if (parts.length >= 2) {
|
|
187
|
+
return `${parts[parts.length - 2]}/${parts[parts.length - 1].replace(".git", "")}`;
|
|
188
|
+
}
|
|
189
|
+
return remote;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function getBranch() {
|
|
195
|
+
try {
|
|
196
|
+
return execSync("git rev-parse --abbrev-ref HEAD", {
|
|
197
|
+
encoding: "utf-8",
|
|
198
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
199
|
+
}).trim();
|
|
200
|
+
} catch {
|
|
201
|
+
return "noname";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function getGitUserName() {
|
|
205
|
+
try {
|
|
206
|
+
return execSync("git config user.name", {
|
|
207
|
+
encoding: "utf-8",
|
|
208
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
209
|
+
}).trim() || null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function getGitUserEmail() {
|
|
215
|
+
try {
|
|
216
|
+
return execSync("git config user.email", {
|
|
217
|
+
encoding: "utf-8",
|
|
218
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
219
|
+
}).trim() || null;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/commands/deactivate.ts
|
|
226
|
+
import fs2 from "fs";
|
|
227
|
+
import path2 from "path";
|
|
228
|
+
var HOOK_EVENTS = [
|
|
229
|
+
"PostToolUse",
|
|
230
|
+
"PermissionRequest",
|
|
231
|
+
"Stop",
|
|
232
|
+
"UserPromptSubmit",
|
|
233
|
+
"SessionStart",
|
|
234
|
+
"SessionEnd"
|
|
235
|
+
];
|
|
236
|
+
var deactivateCommand = new Command2("deactivate").description("Remove glop hooks from the current repo").action(async () => {
|
|
237
|
+
const repoRoot = getRepoRoot();
|
|
238
|
+
if (!repoRoot) {
|
|
239
|
+
console.error("Not in a git repository.");
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
const settingsFile = path2.join(repoRoot, ".claude", "settings.json");
|
|
243
|
+
if (!fs2.existsSync(settingsFile)) {
|
|
244
|
+
console.log("No .claude/settings.json found. Nothing to remove.");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let settings;
|
|
248
|
+
try {
|
|
249
|
+
settings = JSON.parse(fs2.readFileSync(settingsFile, "utf-8"));
|
|
250
|
+
} catch {
|
|
251
|
+
console.error("Could not parse .claude/settings.json");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
const hooks = settings.hooks;
|
|
255
|
+
if (!hooks) {
|
|
256
|
+
console.log("No hooks found in settings. Nothing to remove.");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
let removed = 0;
|
|
260
|
+
for (const event of HOOK_EVENTS) {
|
|
261
|
+
if (!hooks[event]) continue;
|
|
262
|
+
const before = hooks[event].length;
|
|
263
|
+
hooks[event] = hooks[event].filter((group) => {
|
|
264
|
+
const groupHooks = group?.hooks || [];
|
|
265
|
+
return !groupHooks.some(
|
|
266
|
+
(h) => h.command && (h.command.includes("glop __hook") || h.command.includes("/api/v1/ingest/hook"))
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
removed += before - hooks[event].length;
|
|
270
|
+
if (hooks[event].length === 0) {
|
|
271
|
+
delete hooks[event];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (Object.keys(hooks).length === 0) {
|
|
275
|
+
delete settings.hooks;
|
|
276
|
+
}
|
|
277
|
+
fs2.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
278
|
+
if (removed > 0) {
|
|
279
|
+
console.log(`Removed glop hooks from ${removed} event(s).`);
|
|
280
|
+
console.log(` Settings: ${settingsFile}`);
|
|
281
|
+
} else {
|
|
282
|
+
console.log("No glop hooks found. Nothing to remove.");
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// src/commands/doctor.ts
|
|
287
|
+
import { Command as Command3 } from "commander";
|
|
288
|
+
import { execSync as execSync2 } from "child_process";
|
|
289
|
+
import fs3 from "fs";
|
|
290
|
+
import path3 from "path";
|
|
291
|
+
function check(status, label, detail) {
|
|
292
|
+
const icon = status === "pass" ? "\u2713" : status === "fail" ? "\u2717" : "!";
|
|
293
|
+
const line = ` ${icon} ${label}`;
|
|
294
|
+
console.log(detail ? `${line} \u2014 ${detail}` : line);
|
|
295
|
+
return status;
|
|
296
|
+
}
|
|
297
|
+
var doctorCommand = new Command3("doctor").description("Check that glop is set up correctly").action(async () => {
|
|
298
|
+
let hasFailure = false;
|
|
299
|
+
const fail = (label, detail) => {
|
|
300
|
+
hasFailure = true;
|
|
301
|
+
return check("fail", label, detail);
|
|
302
|
+
};
|
|
303
|
+
const config = loadConfig();
|
|
304
|
+
if (!config) {
|
|
305
|
+
fail("Authenticated", "run `glop auth` first");
|
|
306
|
+
console.log();
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
check("pass", "Authenticated", `${config.developer_name} on ${config.server_url}`);
|
|
310
|
+
try {
|
|
311
|
+
const res = await fetch(`${config.server_url}/api/v1/health`, {
|
|
312
|
+
headers: {
|
|
313
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
314
|
+
"X-Machine-Id": config.machine_id
|
|
315
|
+
},
|
|
316
|
+
signal: AbortSignal.timeout(5e3)
|
|
317
|
+
});
|
|
318
|
+
if (res.ok) {
|
|
319
|
+
check("pass", "Server reachable");
|
|
320
|
+
} else if (res.status === 401) {
|
|
321
|
+
fail("API key valid", "re-run `glop auth`");
|
|
322
|
+
} else {
|
|
323
|
+
fail("Server reachable", `HTTP ${res.status}`);
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
fail("Server reachable", `cannot connect to ${config.server_url}`);
|
|
327
|
+
}
|
|
328
|
+
const repoRoot = getRepoRoot();
|
|
329
|
+
if (repoRoot) {
|
|
330
|
+
check("pass", "Git repository", repoRoot);
|
|
331
|
+
} else {
|
|
332
|
+
check("warn", "Git repository", "not in a git repo");
|
|
333
|
+
}
|
|
334
|
+
if (repoRoot) {
|
|
335
|
+
const repoKey = getRepoKey();
|
|
336
|
+
if (repoKey) {
|
|
337
|
+
check("pass", "Git remote", repoKey);
|
|
338
|
+
} else {
|
|
339
|
+
check("warn", "Git remote", "no origin remote found");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const baseDir = repoRoot || process.cwd();
|
|
343
|
+
const settingsFile = path3.join(baseDir, ".claude", "settings.json");
|
|
344
|
+
if (fs3.existsSync(settingsFile)) {
|
|
345
|
+
try {
|
|
346
|
+
const settings = JSON.parse(fs3.readFileSync(settingsFile, "utf-8"));
|
|
347
|
+
const hooks = settings.hooks || {};
|
|
348
|
+
const glopEvents = Object.entries(hooks).filter(
|
|
349
|
+
([, handlers]) => JSON.stringify(handlers).includes("glop __hook")
|
|
350
|
+
);
|
|
351
|
+
if (glopEvents.length > 0) {
|
|
352
|
+
check("pass", "Hooks installed", `${glopEvents.length} events in ${settingsFile}`);
|
|
353
|
+
} else {
|
|
354
|
+
fail("Hooks installed", "run `glop init`");
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
fail("Hooks installed", `${settingsFile} is corrupted`);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
fail("Hooks installed", "run `glop init`");
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const which = execSync2("which glop", {
|
|
364
|
+
encoding: "utf-8",
|
|
365
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
366
|
+
}).trim();
|
|
367
|
+
check("pass", "CLI in PATH", which);
|
|
368
|
+
} catch {
|
|
369
|
+
fail("CLI in PATH", "hooks won't fire \u2014 ensure `glop` is in your PATH");
|
|
370
|
+
}
|
|
371
|
+
console.log();
|
|
372
|
+
if (hasFailure) {
|
|
373
|
+
process.exit(1);
|
|
374
|
+
} else {
|
|
375
|
+
console.log("Everything looks good!");
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// src/commands/hook.ts
|
|
380
|
+
import { Command as Command4 } from "commander";
|
|
381
|
+
import { openSync, readSync, closeSync } from "fs";
|
|
382
|
+
function extractSlugFromTranscript(transcriptPath) {
|
|
383
|
+
try {
|
|
384
|
+
const fd = openSync(transcriptPath, "r");
|
|
385
|
+
const buf = Buffer.alloc(65536);
|
|
386
|
+
const bytesRead = readSync(fd, buf, 0, 65536, 0);
|
|
387
|
+
closeSync(fd);
|
|
388
|
+
const head = buf.toString("utf-8", 0, bytesRead);
|
|
389
|
+
const match = head.match(/"slug":"([^"]+)"/);
|
|
390
|
+
return match ? match[1] : null;
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
var hookCommand = new Command4("__hook").description("internal").action(async () => {
|
|
396
|
+
const config = loadConfig();
|
|
397
|
+
if (!config) return;
|
|
398
|
+
let input = "";
|
|
399
|
+
for await (const chunk of process.stdin) {
|
|
400
|
+
input += chunk;
|
|
401
|
+
}
|
|
402
|
+
let payload;
|
|
403
|
+
try {
|
|
404
|
+
payload = JSON.parse(input);
|
|
405
|
+
} catch {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
payload.repo_key = getRepoKey() || payload.cwd || "unknown";
|
|
409
|
+
payload.branch = getBranch();
|
|
410
|
+
payload.machine_id = config.machine_id;
|
|
411
|
+
payload.git_user_name = getGitUserName();
|
|
412
|
+
payload.git_user_email = getGitUserEmail();
|
|
413
|
+
const skipSlugEvents = /* @__PURE__ */ new Set(["PostToolUse"]);
|
|
414
|
+
if (!skipSlugEvents.has(payload.hook_event_name) && typeof payload.transcript_path === "string") {
|
|
415
|
+
const slug = extractSlugFromTranscript(payload.transcript_path);
|
|
416
|
+
if (slug) payload.slug = slug;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const res = await fetch(`${config.server_url}/api/v1/ingest/hook`, {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: {
|
|
422
|
+
"Content-Type": "application/json",
|
|
423
|
+
Authorization: `Bearer ${config.api_key}`
|
|
424
|
+
},
|
|
425
|
+
body: JSON.stringify(payload),
|
|
426
|
+
signal: AbortSignal.timeout(5e3)
|
|
427
|
+
});
|
|
428
|
+
if (payload.hook_event_name === "SessionStart") {
|
|
429
|
+
if (res.ok) {
|
|
430
|
+
console.log(`glop: connected to ${config.server_url}`);
|
|
431
|
+
} else if (res.status === 401) {
|
|
432
|
+
console.log("glop: API key expired or invalid \u2014 run `glop auth` to re-authenticate");
|
|
433
|
+
} else {
|
|
434
|
+
console.log(`glop: server returned HTTP ${res.status}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
if (payload.hook_event_name === "SessionStart") {
|
|
439
|
+
console.log(`glop: server unreachable at ${config.server_url}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// src/commands/init.ts
|
|
445
|
+
import { Command as Command5 } from "commander";
|
|
446
|
+
import { execSync as execSync3 } from "child_process";
|
|
447
|
+
import fs4 from "fs";
|
|
448
|
+
import path4 from "path";
|
|
449
|
+
function hasGlopHooks(settings) {
|
|
450
|
+
const hooks = settings.hooks;
|
|
451
|
+
if (!hooks) return false;
|
|
452
|
+
return Object.values(hooks).some(
|
|
453
|
+
(handlers) => JSON.stringify(handlers).includes("glop __hook")
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
var initCommand = new Command5("init").description("Install Claude Code hooks in the current repo").action(async () => {
|
|
457
|
+
const config = loadConfig();
|
|
458
|
+
if (!config) {
|
|
459
|
+
console.error("Not authenticated. Run `glop auth` first.");
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const res = await fetch(`${config.server_url}/api/v1/health`, {
|
|
464
|
+
headers: {
|
|
465
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
466
|
+
"X-Machine-Id": config.machine_id
|
|
467
|
+
},
|
|
468
|
+
signal: AbortSignal.timeout(5e3)
|
|
469
|
+
});
|
|
470
|
+
if (res.status === 401) {
|
|
471
|
+
console.error("API key is invalid or expired. Run `glop auth` again.");
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
if (!res.ok) {
|
|
475
|
+
console.error(`Server error: HTTP ${res.status}. Try again later.`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
console.warn(`Warning: Cannot reach ${config.server_url}. Key validation skipped.`);
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
execSync3("which glop", { stdio: ["pipe", "pipe", "pipe"] });
|
|
483
|
+
} catch {
|
|
484
|
+
console.warn("Warning: `glop` not found in PATH. Hooks won't fire until it's accessible.");
|
|
485
|
+
}
|
|
486
|
+
const repoRoot = getRepoRoot();
|
|
487
|
+
if (!repoRoot) {
|
|
488
|
+
console.warn("Warning: not in a git repository. Repo and branch tracking will be limited.");
|
|
489
|
+
}
|
|
490
|
+
const baseDir = repoRoot || process.cwd();
|
|
491
|
+
const claudeDir = path4.join(baseDir, ".claude");
|
|
492
|
+
const settingsFile = path4.join(claudeDir, "settings.json");
|
|
493
|
+
if (!fs4.existsSync(claudeDir)) {
|
|
494
|
+
fs4.mkdirSync(claudeDir, { recursive: true });
|
|
495
|
+
}
|
|
496
|
+
let settings = {};
|
|
497
|
+
const isUpdate = fs4.existsSync(settingsFile);
|
|
498
|
+
if (isUpdate) {
|
|
499
|
+
try {
|
|
500
|
+
settings = JSON.parse(fs4.readFileSync(settingsFile, "utf-8"));
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const hadHooks = hasGlopHooks(settings);
|
|
505
|
+
const hookHandler = {
|
|
506
|
+
type: "command",
|
|
507
|
+
command: "glop __hook"
|
|
508
|
+
};
|
|
509
|
+
const hookGroup = {
|
|
510
|
+
hooks: [hookHandler]
|
|
511
|
+
};
|
|
512
|
+
const hooks = settings.hooks || {};
|
|
513
|
+
hooks.PostToolUse = [hookGroup];
|
|
514
|
+
hooks.PermissionRequest = [hookGroup];
|
|
515
|
+
hooks.Stop = [hookGroup];
|
|
516
|
+
hooks.UserPromptSubmit = [hookGroup];
|
|
517
|
+
hooks.SessionStart = [hookGroup];
|
|
518
|
+
hooks.SessionEnd = [hookGroup];
|
|
519
|
+
settings.hooks = hooks;
|
|
520
|
+
fs4.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
521
|
+
console.log(`${hadHooks ? "\u2713 glop updated" : "\u2713 glop connected"} \u2014 sessions will appear at ${config.server_url}/live`);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// src/commands/status.ts
|
|
525
|
+
import { Command as Command6 } from "commander";
|
|
526
|
+
|
|
527
|
+
// src/lib/api-client.ts
|
|
528
|
+
function getConfig() {
|
|
529
|
+
const config = loadConfig();
|
|
530
|
+
if (!config) {
|
|
531
|
+
console.error(
|
|
532
|
+
"Not authenticated. Run `glop auth` first."
|
|
533
|
+
);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
return config;
|
|
537
|
+
}
|
|
538
|
+
async function apiRequest(path5, options = {}) {
|
|
539
|
+
const config = getConfig();
|
|
540
|
+
const url = `${config.server_url}${path5}`;
|
|
541
|
+
const headers = {
|
|
542
|
+
"Content-Type": "application/json",
|
|
543
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
544
|
+
"X-Machine-Id": config.machine_id,
|
|
545
|
+
...options.headers
|
|
546
|
+
};
|
|
547
|
+
return fetch(url, {
|
|
548
|
+
...options,
|
|
549
|
+
headers
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/commands/status.ts
|
|
554
|
+
function timeAgo(iso) {
|
|
555
|
+
const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1e3);
|
|
556
|
+
if (seconds < 5) return "just now";
|
|
557
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
558
|
+
const minutes = Math.floor(seconds / 60);
|
|
559
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
560
|
+
const hours = Math.floor(minutes / 60);
|
|
561
|
+
if (hours < 24) return `${hours}h ago`;
|
|
562
|
+
const days = Math.floor(hours / 24);
|
|
563
|
+
return `${days}d ago`;
|
|
564
|
+
}
|
|
565
|
+
var statusCommand = new Command6("status").description("Show current Run status for this repo").action(async () => {
|
|
566
|
+
const repoKey = getRepoKey();
|
|
567
|
+
const branch = getBranch();
|
|
568
|
+
if (!repoKey) {
|
|
569
|
+
console.error("Not in a git repository with a remote.");
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
const res = await apiRequest("/api/v1/live");
|
|
574
|
+
if (!res.ok) {
|
|
575
|
+
console.error("Failed to fetch status:", res.statusText);
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
const data = await res.json();
|
|
579
|
+
const matchingRuns = data.runs.filter(
|
|
580
|
+
(r) => r.repo_key.includes(repoKey.split("/").pop() || "") && r.branch_name === branch
|
|
581
|
+
);
|
|
582
|
+
if (matchingRuns.length === 0) {
|
|
583
|
+
console.log(`No active runs for ${repoKey} (${branch})`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
for (const run of matchingRuns) {
|
|
587
|
+
console.log(`Run: ${run.id.slice(0, 8)}`);
|
|
588
|
+
console.log(` Status: ${run.status}`);
|
|
589
|
+
console.log(` Phase: ${run.phase}`);
|
|
590
|
+
console.log(` Title: ${run.title || "-"}`);
|
|
591
|
+
console.log(` Last: ${run.last_action_label || "-"}`);
|
|
592
|
+
console.log(` Updated: ${timeAgo(run.last_event_at)}`);
|
|
593
|
+
}
|
|
594
|
+
} catch (err) {
|
|
595
|
+
console.error(
|
|
596
|
+
"Failed to connect:",
|
|
597
|
+
err instanceof Error ? err.message : err
|
|
598
|
+
);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// src/index.ts
|
|
604
|
+
var program = new Command7().name("glop").description("Passive control plane for local Claude-driven development").version("0.1.0");
|
|
605
|
+
program.addCommand(authCommand);
|
|
606
|
+
program.addCommand(deactivateCommand);
|
|
607
|
+
program.addCommand(doctorCommand);
|
|
608
|
+
program.addCommand(hookCommand, { hidden: true });
|
|
609
|
+
program.addCommand(initCommand);
|
|
610
|
+
program.addCommand(statusCommand);
|
|
611
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "glop.dev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"glop": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"prepublishOnly": "node -e \"if (!process.env.GLOP_DEFAULT_SERVER_URL) { console.error('Error: GLOP_DEFAULT_SERVER_URL must be set before publishing'); process.exit(1); }\" && tsup"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"commander": "^13.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tsup": "^8.3.0",
|
|
23
|
+
"tsx": "^4.19.0",
|
|
24
|
+
"typescript": "^5.7.0",
|
|
25
|
+
"vitest": "^4.0.18"
|
|
26
|
+
}
|
|
27
|
+
}
|