@bonnard/agentops 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin/agentops.mjs +245 -70
  2. package/package.json +13 -12
@@ -1,26 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import pc from "picocolors";
4
+ import crypto from "node:crypto";
4
5
  import http from "node:http";
5
6
  import open from "open";
6
7
  import fs from "node:fs";
7
8
  import path from "node:path";
8
9
  import os from "node:os";
10
+ import { execFileSync } from "node:child_process";
9
11
  //#region src/lib/credentials.ts
10
- const AGENTOPS_DIR = path.join(os.homedir(), ".agentops");
11
- const CREDENTIALS_PATH = path.join(AGENTOPS_DIR, "credentials.json");
12
- const CONFIG_PATH = path.join(AGENTOPS_DIR, "config.json");
12
+ const AGENTOPS_DIR$1 = path.join(os.homedir(), ".agentops");
13
+ const CREDENTIALS_PATH = path.join(AGENTOPS_DIR$1, "credentials.json");
14
+ const CONFIG_PATH = path.join(AGENTOPS_DIR$1, "config.json");
13
15
  function ensureDir() {
14
- if (!fs.existsSync(AGENTOPS_DIR)) fs.mkdirSync(AGENTOPS_DIR, { recursive: true });
16
+ if (!fs.existsSync(AGENTOPS_DIR$1)) fs.mkdirSync(AGENTOPS_DIR$1, {
17
+ recursive: true,
18
+ mode: 448
19
+ });
15
20
  }
16
21
  function saveCredentials(creds) {
17
22
  ensureDir();
18
23
  fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
19
24
  }
25
+ /** Validate that an unknown value has the shape of Credentials */
26
+ function validateCredentials(data) {
27
+ if (!data || typeof data !== "object") return null;
28
+ const d = data;
29
+ if (typeof d.accessToken !== "string" || !d.accessToken) return null;
30
+ const user = d.user;
31
+ if (!user || typeof user.email !== "string" || typeof user.name !== "string") return null;
32
+ const org = d.org;
33
+ if (!org || typeof org.name !== "string" || typeof org.slug !== "string") return null;
34
+ return data;
35
+ }
20
36
  function loadCredentials() {
21
37
  try {
22
38
  const raw = fs.readFileSync(CREDENTIALS_PATH, "utf-8");
23
- return JSON.parse(raw);
39
+ return validateCredentials(JSON.parse(raw));
24
40
  } catch {
25
41
  return null;
26
42
  }
@@ -32,7 +48,7 @@ function clearCredentials() {
32
48
  }
33
49
  function saveConfig(config) {
34
50
  ensureDir();
35
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
51
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
36
52
  }
37
53
  function loadConfig() {
38
54
  try {
@@ -68,48 +84,87 @@ function getHeaders() {
68
84
  }
69
85
  //#endregion
70
86
  //#region src/commands/login.ts
71
- const CALLBACK_PORT = 9876;
87
+ const CALLBACK_HOST = "127.0.0.1";
72
88
  async function loginCommand(options) {
73
89
  const baseUrl = getBaseUrl(options.url);
74
90
  console.log(pc.dim(`Server: ${baseUrl}`));
91
+ const expectedState = crypto.randomBytes(20).toString("hex");
75
92
  let authRes;
93
+ let callbackPort;
76
94
  try {
77
- authRes = await fetch(`${baseUrl}/auth/url?redirect_uri=${encodeURIComponent(`http://localhost:${CALLBACK_PORT}/callback`)}`);
78
- } catch {
79
- console.error(pc.red(`Cannot reach server at ${baseUrl}`));
80
- console.error(pc.dim("Check the URL and ensure the server is running."));
81
- process.exit(1);
82
- return;
83
- }
84
- if (!authRes.ok) {
85
- console.error(pc.red(`Failed to get auth URL: ${authRes.status} ${await authRes.text()}`));
86
- process.exit(1);
87
- }
88
- const { url: authUrl } = await authRes.json();
89
- const { code, state } = await captureCallback(authUrl);
90
- console.log(pc.dim("Exchanging code for tokens..."));
91
- const callbackRes = await post("/auth/callback", {
92
- code,
93
- state
94
- }, baseUrl);
95
- if (!callbackRes.ok) {
96
- const body = await callbackRes.text();
97
- console.error(pc.red(`Authentication failed: ${body}`));
95
+ const { server, port } = await startCallbackServer();
96
+ callbackPort = port;
97
+ const redirectUri = `http://${CALLBACK_HOST}:${callbackPort}/callback`;
98
+ authRes = await fetch(`${baseUrl}/auth/url?redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent(expectedState)}`);
99
+ if (!authRes.ok) {
100
+ server.close();
101
+ console.error(pc.red(`Failed to get auth URL: ${authRes.status} ${await authRes.text()}`));
102
+ process.exit(1);
103
+ }
104
+ const { url: authUrl } = await authRes.json();
105
+ console.log(pc.dim(`Listening on http://${CALLBACK_HOST}:${callbackPort}`));
106
+ console.log("Opening browser for authentication...");
107
+ console.log(pc.dim(`If the browser doesn't open, visit:\n${authUrl}`));
108
+ console.log();
109
+ open(authUrl).catch(() => {});
110
+ const { code, state } = await waitForCallback(server, expectedState);
111
+ console.log(pc.dim("Exchanging code for tokens..."));
112
+ const callbackRes = await post("/auth/callback", {
113
+ code,
114
+ state
115
+ }, baseUrl);
116
+ if (!callbackRes.ok) {
117
+ const body = await callbackRes.text();
118
+ console.error(pc.red(`Authentication failed: ${body}`));
119
+ process.exit(1);
120
+ }
121
+ const creds = validateCredentials(await callbackRes.json());
122
+ if (!creds) {
123
+ console.error(pc.red("Server returned an unexpected response format."));
124
+ process.exit(1);
125
+ }
126
+ saveCredentials(creds);
127
+ saveConfig({ url: baseUrl });
128
+ console.log();
129
+ console.log(pc.green("✓ Logged in successfully"));
130
+ console.log(` ${pc.bold(creds.user.email)} (${creds.org.name})`);
131
+ console.log(` Role: ${creds.user.role}`);
132
+ console.log();
133
+ } catch (err) {
134
+ const message = err instanceof Error ? err.message : String(err);
135
+ if (message.includes("Cannot reach server") || message.includes("ECONNREFUSED")) {
136
+ console.error(pc.red(`Cannot reach server at ${baseUrl}`));
137
+ console.error(pc.dim("Check the URL and ensure the server is running."));
138
+ } else console.error(pc.red(`Login failed: ${message}`));
98
139
  process.exit(1);
99
140
  }
100
- const data = await callbackRes.json();
101
- saveCredentials(data);
102
- saveConfig({ url: baseUrl });
103
- console.log();
104
- console.log(pc.green("✓ Logged in successfully"));
105
- console.log(` ${pc.bold(data.user.email)} (${data.org.name})`);
106
- console.log(` Role: ${data.user.role}`);
107
- console.log();
108
141
  }
109
- function captureCallback(authUrl) {
142
+ function startCallbackServer() {
143
+ return new Promise((resolve, reject) => {
144
+ const server = http.createServer();
145
+ server.listen(0, CALLBACK_HOST, () => {
146
+ const addr = server.address();
147
+ if (!addr || typeof addr === "string") {
148
+ server.close();
149
+ reject(/* @__PURE__ */ new Error("Failed to get server address"));
150
+ return;
151
+ }
152
+ resolve({
153
+ server,
154
+ port: addr.port
155
+ });
156
+ });
157
+ server.on("error", reject);
158
+ });
159
+ }
160
+ function waitForCallback(server, expectedState) {
110
161
  return new Promise((resolve, reject) => {
111
- const server = http.createServer((req, res) => {
112
- const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
162
+ const timeout = setTimeout(() => {
163
+ server.close();
164
+ reject(/* @__PURE__ */ new Error("Login timed out after 5 minutes"));
165
+ }, 300 * 1e3);
166
+ server.on("request", (req, res) => {
167
+ const url = new URL(req.url ?? "/", `http://${CALLBACK_HOST}`);
113
168
  if (url.pathname !== "/callback") {
114
169
  res.writeHead(404);
115
170
  res.end("Not found");
@@ -118,62 +173,182 @@ function captureCallback(authUrl) {
118
173
  const code = url.searchParams.get("code");
119
174
  const state = url.searchParams.get("state");
120
175
  if (!code) {
121
- const error = url.searchParams.get("error") ?? "No code received";
122
176
  res.writeHead(400, { "Content-Type": "text/html" });
123
- res.end(errorPage(error));
177
+ res.end(resultPage("Authentication Failed", "Check the terminal for details."));
178
+ clearTimeout(timeout);
179
+ server.close();
180
+ const errorMsg = url.searchParams.get("error") ?? "No authorization code received";
181
+ reject(new Error(errorMsg));
182
+ return;
183
+ }
184
+ if (state !== expectedState) {
185
+ res.writeHead(400, { "Content-Type": "text/html" });
186
+ res.end(resultPage("Authentication Failed", "Check the terminal for details."));
187
+ clearTimeout(timeout);
124
188
  server.close();
125
- reject(new Error(error));
189
+ reject(/* @__PURE__ */ new Error("State mismatch — possible CSRF attack. Try logging in again."));
126
190
  return;
127
191
  }
128
192
  res.writeHead(200, { "Content-Type": "text/html" });
129
- res.end(successPage());
193
+ res.end(resultPage("Authenticated", "You can close this tab and return to the terminal."));
194
+ clearTimeout(timeout);
130
195
  server.close();
131
196
  resolve({
132
197
  code,
133
- state: state ?? ""
198
+ state
134
199
  });
135
200
  });
136
- server.listen(CALLBACK_PORT, () => {
137
- console.log(pc.dim(`Listening on http://localhost:${CALLBACK_PORT}`));
138
- console.log("Opening browser for authentication...");
139
- console.log();
140
- open(authUrl);
141
- });
142
- server.on("error", (err) => {
143
- if (err.code === "EADDRINUSE") reject(/* @__PURE__ */ new Error(`Port ${CALLBACK_PORT} is in use. Close the process using it and try again.`));
144
- else reject(err);
145
- });
146
- setTimeout(() => {
147
- server.close();
148
- reject(/* @__PURE__ */ new Error("Login timed out after 5 minutes"));
149
- }, 300 * 1e3);
150
201
  });
151
202
  }
152
- function successPage() {
203
+ /** Static HTML page — never interpolate user/query data into this */
204
+ function resultPage(title, message) {
153
205
  return `<!DOCTYPE html>
154
206
  <html><head><title>AgentOps</title></head>
155
207
  <body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#fafafa">
156
208
  <div style="text-align:center">
157
- <h1>Authenticated</h1>
158
- <p>You can close this tab and return to the terminal.</p>
209
+ <h1>${title}</h1>
210
+ <p>${message}</p>
159
211
  </div>
160
212
  </body></html>`;
161
213
  }
162
- function errorPage(error) {
163
- return `<!DOCTYPE html>
164
- <html><head><title>AgentOps</title></head>
165
- <body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#0a0a0a;color:#fafafa">
166
- <div style="text-align:center">
167
- <h1>Authentication Failed</h1>
168
- <p>${error}</p>
169
- </div>
170
- </body></html>`;
214
+ //#endregion
215
+ //#region src/commands/setup.ts
216
+ const SUPPORTED_EDITORS = [
217
+ "cursor",
218
+ "claude",
219
+ "codex"
220
+ ];
221
+ const AGENTOPS_DIR = path.join(os.homedir(), ".agentops");
222
+ const SCRIPTS_DIR = path.join(AGENTOPS_DIR, "scripts");
223
+ async function setupCommand(options) {
224
+ const creds = loadCredentials();
225
+ if (!creds) {
226
+ console.error(pc.red("Not logged in. Run: agentops login"));
227
+ process.exit(1);
228
+ }
229
+ const editor = options.editor?.toLowerCase();
230
+ if (!editor || !SUPPORTED_EDITORS.includes(editor)) {
231
+ console.error(pc.red(`Please specify an editor: --editor ${SUPPORTED_EDITORS.join(" | ")}`));
232
+ process.exit(1);
233
+ }
234
+ const baseUrl = getBaseUrl(options.url);
235
+ console.log(pc.dim(`Server: ${baseUrl}`));
236
+ console.log(`Logged in as ${pc.bold(creds.user.email)} (${creds.org.name})`);
237
+ console.log(`Setting up for ${pc.bold(editor)}...`);
238
+ console.log();
239
+ copyScripts();
240
+ fs.writeFileSync(path.join(AGENTOPS_DIR, "editor.json"), JSON.stringify({ editor }, null, 2), { mode: 384 });
241
+ switch (editor) {
242
+ case "cursor":
243
+ setupCursor();
244
+ break;
245
+ case "claude":
246
+ setupClaude();
247
+ break;
248
+ case "codex":
249
+ setupCodex();
250
+ break;
251
+ }
252
+ console.log(pc.dim("Running first sync..."));
253
+ try {
254
+ execFileSync("node", [path.join(SCRIPTS_DIR, "sync.mjs")], {
255
+ stdio: [
256
+ "pipe",
257
+ "pipe",
258
+ "inherit"
259
+ ],
260
+ env: {
261
+ ...process.env,
262
+ AGENTOPS_API_URL: baseUrl,
263
+ AGENTOPS_EDITOR: editor
264
+ }
265
+ });
266
+ console.log(pc.green(" Sync complete"));
267
+ } catch {
268
+ console.log(pc.yellow(" First sync failed — will retry on next session start"));
269
+ }
270
+ console.log();
271
+ console.log(pc.green("Setup complete."));
272
+ console.log(pc.dim("Skills will sync automatically on every session start."));
273
+ }
274
+ function copyScripts() {
275
+ fs.mkdirSync(SCRIPTS_DIR, {
276
+ recursive: true,
277
+ mode: 448
278
+ });
279
+ const source = [path.resolve(import.meta.dirname, "..", "..", "scripts", "sync.mjs"), path.resolve(import.meta.dirname, "..", "scripts", "sync.mjs")].find((p) => fs.existsSync(p));
280
+ if (!source) {
281
+ console.error(pc.red("Could not find sync.mjs script"));
282
+ process.exit(1);
283
+ }
284
+ fs.copyFileSync(source, path.join(SCRIPTS_DIR, "sync.mjs"));
285
+ console.log(pc.green(" Sync scripts installed to ~/.agentops/scripts/"));
286
+ }
287
+ function setupCursor() {
288
+ const cursorDir = path.join(os.homedir(), ".cursor");
289
+ const hooksPath = path.join(cursorDir, "hooks.json");
290
+ let hooks = {
291
+ version: 1,
292
+ hooks: {}
293
+ };
294
+ if (fs.existsSync(hooksPath)) try {
295
+ hooks = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
296
+ } catch {}
297
+ const hooksObj = hooks.hooks ?? {};
298
+ const syncCommand = `node "${path.join(SCRIPTS_DIR, "sync.mjs")}"`;
299
+ const filtered = (hooksObj.sessionStart ?? []).filter((h) => !h.command?.includes("agentops"));
300
+ filtered.push({
301
+ command: syncCommand,
302
+ timeout: 30
303
+ });
304
+ hooksObj.sessionStart = filtered;
305
+ hooks.hooks = hooksObj;
306
+ if (!hooks.version) hooks.version = 1;
307
+ fs.writeFileSync(hooksPath, JSON.stringify(hooks, null, 2));
308
+ fs.mkdirSync(path.join(cursorDir, "commands"), { recursive: true });
309
+ fs.mkdirSync(path.join(cursorDir, "rules"), { recursive: true });
310
+ console.log(pc.green(" Cursor hooks.json configured"));
311
+ }
312
+ function setupClaude() {
313
+ try {
314
+ execFileSync("claude", [
315
+ "plugin",
316
+ "add",
317
+ "https://github.com/bonnard-data/agentops-plugin"
318
+ ], { stdio: "inherit" });
319
+ console.log(pc.green(" Claude Code plugin installed"));
320
+ } catch {
321
+ console.log(pc.yellow(" Claude Code plugin install failed (may already be installed)"));
322
+ console.log(pc.dim(" Install manually: claude plugin add https://github.com/bonnard-data/agentops-plugin"));
323
+ }
324
+ }
325
+ function setupCodex() {
326
+ const codexDir = path.join(os.homedir(), ".codex");
327
+ const hooksPath = path.join(codexDir, "hooks.json");
328
+ let hooks = {};
329
+ if (fs.existsSync(hooksPath)) try {
330
+ hooks = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
331
+ } catch {}
332
+ const hooksObj = hooks.hooks ?? {};
333
+ const syncCommand = `node "${path.join(SCRIPTS_DIR, "sync.mjs")}"`;
334
+ const filtered = (hooksObj.SessionStart ?? []).filter((h) => !h.command?.includes("agentops"));
335
+ filtered.push({
336
+ command: syncCommand,
337
+ timeout: 30
338
+ });
339
+ hooksObj.SessionStart = filtered;
340
+ hooks.hooks = hooksObj;
341
+ fs.mkdirSync(codexDir, { recursive: true });
342
+ fs.writeFileSync(hooksPath, JSON.stringify(hooks, null, 2));
343
+ fs.mkdirSync(path.join(os.homedir(), ".agents", "skills"), { recursive: true });
344
+ console.log(pc.green(" Codex hooks.json configured"));
171
345
  }
172
346
  //#endregion
173
347
  //#region src/bin/agentops.ts
174
348
  const program = new Command();
175
349
  program.name("agentops").description("AgentOps CLI — setup and manage your AI agent skills").version("0.1.0");
176
350
  program.command("login").description("Authenticate with AgentOps via your browser").option("--url <url>", "AgentOps server URL").action(loginCommand);
351
+ program.command("setup").description("Configure an editor for AgentOps skill sync").requiredOption("--editor <editor>", "Editor to configure (cursor, claude, codex)").option("--url <url>", "AgentOps server URL").action(setupCommand);
177
352
  program.command("logout").description("Clear saved credentials").action(() => {
178
353
  clearCredentials();
179
354
  console.log(pc.green("✓ Logged out"));
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@bonnard/agentops",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "agentops": "./dist/bin/agentops.mjs"
7
7
  },
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "scripts"
10
11
  ],
11
12
  "repository": {
12
13
  "type": "git",
@@ -22,17 +23,17 @@
22
23
  "lint": "eslint ."
23
24
  },
24
25
  "dependencies": {
25
- "commander": "^14.0.3",
26
- "open": "^11.0.0",
27
- "picocolors": "^1.1.1"
26
+ "commander": "14.0.3",
27
+ "open": "11.0.0",
28
+ "picocolors": "1.1.1"
28
29
  },
29
30
  "devDependencies": {
30
- "@eslint/js": "^10.0.1",
31
- "@types/node": "^22.15.0",
32
- "eslint": "^10.0.1",
33
- "tsdown": "^0.21.4",
34
- "tsx": "^4.21.0",
35
- "typescript": "^6.0.2",
36
- "typescript-eslint": "^8.57.2"
31
+ "@eslint/js": "10.0.1",
32
+ "@types/node": "22.19.17",
33
+ "eslint": "10.2.0",
34
+ "tsdown": "0.21.7",
35
+ "tsx": "4.21.0",
36
+ "typescript": "6.0.2",
37
+ "typescript-eslint": "8.58.0"
37
38
  }
38
39
  }