agny 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.js ADDED
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ clearCredentials,
4
+ cursorLogin,
5
+ cursorLoginWithToken,
6
+ cursorLogout,
7
+ cursorStatus,
8
+ cursorSync,
9
+ detectProjectContext,
10
+ finalizeReportAndGraphAsync,
11
+ getApiBaseUrl,
12
+ getCursorCachePath,
13
+ loadCredentials,
14
+ loadCursorCredentials,
15
+ parseCursorCsv,
16
+ parseLocalSourcesAsync,
17
+ saveCredentials,
18
+ syncCursorCache,
19
+ uploadBatch,
20
+ watch
21
+ } from "./chunk-COBQNFEL.js";
22
+ import {
23
+ install,
24
+ status,
25
+ uninstall
26
+ } from "./chunk-FXTJMSTV.js";
27
+
28
+ // src/index.ts
29
+ import { Command } from "commander";
30
+
31
+ // src/auth.ts
32
+ import pc from "picocolors";
33
+ import * as os from "os";
34
+ var MAX_POLL_ATTEMPTS = 180;
35
+ async function login() {
36
+ const credentials = loadCredentials();
37
+ if (credentials) {
38
+ console.log(pc.yellow(`
39
+ Already logged in as ${pc.bold(credentials.username)}`));
40
+ console.log(pc.gray(" Run 'agny logout' to sign out first.\n"));
41
+ return;
42
+ }
43
+ const baseUrl = getApiBaseUrl();
44
+ console.log(pc.cyan("\n AGNY - Login\n"));
45
+ console.log(pc.gray(" Requesting authorization code..."));
46
+ let deviceCodeData;
47
+ try {
48
+ const response = await fetch(`${baseUrl}/api/auth/device`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ deviceName: getDeviceName() })
52
+ });
53
+ if (!response.ok) {
54
+ throw new Error(`Server returned ${response.status}`);
55
+ }
56
+ deviceCodeData = await response.json();
57
+ } catch (error) {
58
+ console.error(pc.red(`
59
+ Error: Failed to connect to server.`));
60
+ console.error(pc.gray(` ${error.message}
61
+ `));
62
+ process.exit(1);
63
+ }
64
+ console.log();
65
+ console.log(pc.white(" Open this URL in your browser:"));
66
+ console.log(pc.cyan(` ${deviceCodeData.verificationUrl}
67
+ `));
68
+ console.log(pc.white(" Enter this code:"));
69
+ console.log(pc.bold(pc.green(` ${deviceCodeData.userCode}
70
+ `)));
71
+ await openBrowser(deviceCodeData.verificationUrl);
72
+ console.log(pc.gray(" Waiting for authorization..."));
73
+ let attempts = 0;
74
+ const pollInterval = (deviceCodeData.interval || 5) * 1e3;
75
+ while (attempts < MAX_POLL_ATTEMPTS) {
76
+ await sleep(pollInterval);
77
+ attempts++;
78
+ try {
79
+ const response = await fetch(`${baseUrl}/api/auth/device/poll`, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({ deviceCode: deviceCodeData.deviceCode })
83
+ });
84
+ const data = await response.json();
85
+ if (data.status === "complete" && data.token && data.user) {
86
+ saveCredentials({
87
+ token: data.token,
88
+ username: data.user.username,
89
+ avatarUrl: data.user.avatarUrl,
90
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
91
+ });
92
+ console.log(pc.green(`
93
+ Success! Logged in as ${pc.bold(data.user.username)}`));
94
+ console.log(pc.gray(" You can now use 'agny submit' to share your usage.\n"));
95
+ return;
96
+ }
97
+ if (data.status === "expired") {
98
+ console.error(pc.red("\n Authorization code expired. Please try again.\n"));
99
+ process.exit(1);
100
+ }
101
+ process.stdout.write(pc.gray("."));
102
+ } catch {
103
+ process.stdout.write(pc.red("!"));
104
+ }
105
+ }
106
+ console.error(pc.red("\n\n Timeout: Authorization took too long. Please try again.\n"));
107
+ process.exit(1);
108
+ }
109
+ async function logout() {
110
+ const credentials = loadCredentials();
111
+ if (!credentials) {
112
+ console.log(pc.yellow("\n Not logged in.\n"));
113
+ return;
114
+ }
115
+ const username = credentials.username;
116
+ const cleared = clearCredentials();
117
+ if (cleared) {
118
+ console.log(pc.green(`
119
+ Logged out from ${pc.bold(username)}
120
+ `));
121
+ } else {
122
+ console.error(pc.red("\n Failed to clear credentials.\n"));
123
+ process.exit(1);
124
+ }
125
+ }
126
+ async function whoami() {
127
+ const credentials = loadCredentials();
128
+ if (!credentials) {
129
+ console.log(pc.yellow("\n Not logged in."));
130
+ console.log(pc.gray(" Run 'agny login' to authenticate.\n"));
131
+ return;
132
+ }
133
+ console.log(pc.cyan("\n AGNY - Account Info\n"));
134
+ console.log(pc.white(` Username: ${pc.bold(credentials.username)}`));
135
+ console.log(pc.gray(` Logged in: ${new Date(credentials.createdAt).toLocaleDateString()}`));
136
+ console.log();
137
+ }
138
+ function getDeviceName() {
139
+ return `CLI on ${os.hostname()}`;
140
+ }
141
+ function sleep(ms) {
142
+ return new Promise((resolve) => setTimeout(resolve, ms));
143
+ }
144
+ async function openBrowser(url) {
145
+ try {
146
+ const { exec } = await import("child_process");
147
+ const platform = process.platform;
148
+ let command;
149
+ if (platform === "darwin") {
150
+ command = `open "${url}"`;
151
+ } else if (platform === "win32") {
152
+ command = `start "" "${url}"`;
153
+ } else {
154
+ command = `xdg-open "${url}"`;
155
+ }
156
+ exec(command, (error) => {
157
+ if (error) {
158
+ }
159
+ });
160
+ } catch {
161
+ }
162
+ }
163
+
164
+ // src/submit.ts
165
+ import * as fs from "fs";
166
+ import * as crypto from "crypto";
167
+ import pc2 from "picocolors";
168
+ var formatCurrency = (val) => `$${val.toFixed(2)}`;
169
+ async function submit(options = {}) {
170
+ const credentials = loadCredentials();
171
+ if (!credentials) {
172
+ console.log(pc2.yellow("\n Not logged in."));
173
+ console.log(pc2.gray(" Run 'agny login' first.\n"));
174
+ process.exit(1);
175
+ }
176
+ const ctx = detectProjectContext();
177
+ console.log(pc2.cyan("\n AGNY - Submit Usage Data\n"));
178
+ console.log(pc2.gray(` Project: ${ctx.projectName} (${ctx.gitBranch || "no-git"})`));
179
+ console.log(pc2.gray(" Scanning local session data..."));
180
+ let data;
181
+ let cursorRows = [];
182
+ let localMessagesObject = null;
183
+ try {
184
+ const [parsedLocals, cursorSync2] = await Promise.all([
185
+ parseLocalSourcesAsync({
186
+ since: options.since,
187
+ until: options.until,
188
+ year: options.year
189
+ }),
190
+ loadCursorCredentials() ? syncCursorCache() : Promise.resolve({ synced: false, rows: 0 })
191
+ ]);
192
+ localMessagesObject = parsedLocals;
193
+ if (cursorSync2.synced) {
194
+ try {
195
+ const cursorCachePath = getCursorCachePath();
196
+ if (fs.existsSync(cursorCachePath)) {
197
+ const csvContent = fs.readFileSync(cursorCachePath, "utf-8");
198
+ cursorRows = parseCursorCsv(csvContent);
199
+ }
200
+ } catch {
201
+ console.warn(pc2.yellow(" Failed to read Cursor cache for details."));
202
+ }
203
+ }
204
+ const { report, graph } = await finalizeReportAndGraphAsync({
205
+ localMessages: localMessagesObject,
206
+ includeCursor: cursorSync2.synced,
207
+ since: options.since,
208
+ until: options.until,
209
+ year: options.year
210
+ });
211
+ data = graph;
212
+ data.summary.totalCost = report.totalCost;
213
+ } catch (error) {
214
+ console.error(pc2.red(`
215
+ Error generating data: ${error.message}
216
+ `));
217
+ process.exit(1);
218
+ }
219
+ console.log(pc2.white(" Data to submit:"));
220
+ console.log(pc2.gray(` Date range: ${data.meta.dateRange.start} to ${data.meta.dateRange.end}`));
221
+ console.log(pc2.gray(` Active days: ${data.summary.activeDays}`));
222
+ console.log(pc2.gray(` Total tokens: ${data.summary.totalTokens.toLocaleString()}`));
223
+ console.log(pc2.gray(` Total cost: ${formatCurrency(data.summary.totalCost)}`));
224
+ console.log(pc2.gray(` Sources: ${data.summary.sources.join(", ")}`));
225
+ console.log();
226
+ if (data.summary.totalTokens === 0) {
227
+ console.log(pc2.yellow(" No usage data found to submit.\n"));
228
+ return;
229
+ }
230
+ if (options.dryRun) {
231
+ console.log(pc2.yellow(" Dry run - not submitting data.\n"));
232
+ return;
233
+ }
234
+ console.log(pc2.gray(" Submitting to server..."));
235
+ const events = [];
236
+ const now = Date.now();
237
+ const ATTRIBUTION_THRESHOLD_MS = 4 * 60 * 60 * 1e3;
238
+ for (const msg of localMessagesObject?.messages || []) {
239
+ const isRecent = now - msg.timestamp < ATTRIBUTION_THRESHOLD_MS;
240
+ const idSeed = `${msg.timestamp}-${msg.modelId}-${msg.input}-${msg.output}-${msg.providerId || msg.source}`;
241
+ const external_id = crypto.createHash("sha256").update(idSeed).digest("hex").slice(0, 36);
242
+ events.push({
243
+ external_id,
244
+ source: msg.source,
245
+ source_type: msg.sourceType || "local",
246
+ model: msg.modelId,
247
+ provider: msg.providerId || msg.source,
248
+ input_tokens: msg.input,
249
+ output_tokens: msg.output,
250
+ cache_read: msg.cacheRead,
251
+ cache_write: msg.cacheWrite,
252
+ reasoning: msg.reasoning,
253
+ total_tokens: msg.input + msg.output + msg.cacheRead + msg.cacheWrite + msg.reasoning,
254
+ cost: 0,
255
+ // Server will calculate cost based on historical_rates
256
+ // Only attribute to project if recent or we have stronger evidence (not yet available in core)
257
+ project: isRecent ? ctx.projectName : "General",
258
+ project_path: isRecent ? ctx.projectPath : void 0,
259
+ git_branch: isRecent ? ctx.gitBranch || void 0 : void 0,
260
+ git_commit_hash: isRecent ? ctx.gitCommitHash || void 0 : void 0,
261
+ language: ctx.primaryLanguage || void 0,
262
+ session_id: `loc_${msg.timestamp}`,
263
+ timestamp: new Date(msg.timestamp).toISOString(),
264
+ date: new Date(msg.timestamp).toISOString().split("T")[0],
265
+ agent: msg.agent || void 0
266
+ });
267
+ }
268
+ for (const row of cursorRows) {
269
+ const isRecent = now - row.timestamp < ATTRIBUTION_THRESHOLD_MS;
270
+ const idSeed = `cursor-${row.timestamp}-${row.model}-${row.inputWithCacheWrite + row.inputWithoutCacheWrite}-${row.outputTokens}`;
271
+ const external_id = crypto.createHash("sha256").update(idSeed).digest("hex").slice(0, 36);
272
+ events.push({
273
+ external_id,
274
+ source: "cursor",
275
+ source_type: row.kind || "chat",
276
+ model: row.model,
277
+ provider: "cursor",
278
+ input_tokens: row.inputWithoutCacheWrite + row.inputWithCacheWrite,
279
+ output_tokens: row.outputTokens,
280
+ cache_read: row.cacheRead,
281
+ cache_write: row.inputWithCacheWrite,
282
+ reasoning: 0,
283
+ total_tokens: row.totalTokens || row.inputWithoutCacheWrite + row.inputWithCacheWrite + row.outputTokens,
284
+ cost: row.costToYou,
285
+ project: isRecent ? ctx.projectName : "General",
286
+ session_id: `cur_${row.timestamp}`,
287
+ timestamp: new Date(row.timestamp).toISOString(),
288
+ date: row.date
289
+ });
290
+ }
291
+ const payload = {
292
+ tps_avg: 0,
293
+ // Not calculated here
294
+ tps_max: 0,
295
+ total_input_tokens: 0,
296
+ // Server aggregates
297
+ total_output_tokens: 0,
298
+ total_cache_read: 0,
299
+ total_cache_write: 0,
300
+ total_cost: data.summary.totalCost,
301
+ projects: [{
302
+ name: ctx.projectName,
303
+ root_path: ctx.projectPath,
304
+ // Send root_path for collision fix
305
+ tokens: data.summary.totalTokens,
306
+ // Approximate contribution to this batch
307
+ models: data.summary.models,
308
+ last_active: (/* @__PURE__ */ new Date()).toISOString()
309
+ }],
310
+ events
311
+ };
312
+ console.log(pc2.gray(" Submitting to server..."));
313
+ try {
314
+ await uploadBatch(payload);
315
+ console.log(pc2.green("\n Successfully submitted!"));
316
+ const baseUrl = getApiBaseUrl();
317
+ console.log(pc2.cyan(` View your profile: ${baseUrl}/u/${credentials.username}
318
+ `));
319
+ } catch (error) {
320
+ console.error(pc2.red(`
321
+ Error: Failed to submit data.`));
322
+ console.error(pc2.gray(` ${error.message}
323
+ `));
324
+ process.exit(1);
325
+ }
326
+ }
327
+
328
+ // src/utils/version-check.ts
329
+ import pc3 from "picocolors";
330
+
331
+ // src/utils/pkg.ts
332
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
333
+ import { fileURLToPath } from "url";
334
+ import { dirname, join } from "path";
335
+ var __filename = fileURLToPath(import.meta.url);
336
+ var __dirname = dirname(__filename);
337
+ function getPackageJson() {
338
+ const paths = [
339
+ join(__dirname, "../../package.json"),
340
+ join(__dirname, "../package.json"),
341
+ join(__dirname, "../../../package.json")
342
+ ];
343
+ for (const p of paths) {
344
+ if (existsSync2(p)) {
345
+ try {
346
+ const content = readFileSync2(p, "utf-8");
347
+ const pkg2 = JSON.parse(content);
348
+ if (pkg2.name === "agny") {
349
+ return pkg2;
350
+ }
351
+ } catch {
352
+ continue;
353
+ }
354
+ }
355
+ }
356
+ return { name: "agny", version: "0.0.0" };
357
+ }
358
+ var pkg = getPackageJson();
359
+
360
+ // src/utils/version-check.ts
361
+ function getCurrentVersion() {
362
+ return pkg.version || "0.0.0";
363
+ }
364
+ async function getLatestVersion() {
365
+ try {
366
+ const controller = new AbortController();
367
+ const timeout = setTimeout(() => controller.abort(), 3e3);
368
+ const res = await fetch("https://registry.npmjs.org/agny/latest", {
369
+ signal: controller.signal
370
+ });
371
+ clearTimeout(timeout);
372
+ if (!res.ok) return null;
373
+ const data = await res.json();
374
+ return data.version || null;
375
+ } catch {
376
+ return null;
377
+ }
378
+ }
379
+ function isNewer(latest, current) {
380
+ const l = latest.split(".").map(Number);
381
+ const c = current.split(".").map(Number);
382
+ for (let i = 0; i < 3; i++) {
383
+ if ((l[i] || 0) > (c[i] || 0)) return true;
384
+ if ((l[i] || 0) < (c[i] || 0)) return false;
385
+ }
386
+ return false;
387
+ }
388
+ async function checkForUpdates() {
389
+ const current = getCurrentVersion();
390
+ const latest = await getLatestVersion();
391
+ if (latest && isNewer(latest, current)) {
392
+ console.log();
393
+ console.log(
394
+ pc3.yellow(` \u26A0\uFE0F Update available: ${pc3.bold(latest)} (current: ${current})`)
395
+ );
396
+ console.log(pc3.dim(` Run: npm update -g agny`));
397
+ console.log();
398
+ }
399
+ }
400
+
401
+ // src/index.ts
402
+ import pc4 from "picocolors";
403
+ var program = new Command();
404
+ program.name("agny").description("\u{1F525} Track your Brain RPM - The passive hustle tracker for AI-native builders").version(pkg.version);
405
+ program.command("watch").description("Start watching AI tool logs (Cursor, Claude, etc.)").action(async () => {
406
+ try {
407
+ await watch();
408
+ } catch (e) {
409
+ console.error(pc4.red(`
410
+ Error: ${e.message}`));
411
+ process.exit(1);
412
+ }
413
+ });
414
+ program.command("login").description("Login to agny.log").action(async () => {
415
+ await login();
416
+ });
417
+ program.command("logout").description("Logout from agny.log").action(async () => {
418
+ await logout();
419
+ });
420
+ program.command("whoami").description("Check current account").action(async () => {
421
+ await whoami();
422
+ });
423
+ program.command("submit").description("Manually submit usage data").option("--dry-run", "Show what would be submitted without sending").action(async (options) => {
424
+ await submit(options);
425
+ });
426
+ program.command("install").description("Install background daemon (auto-start on login)").action(async () => {
427
+ await install();
428
+ });
429
+ program.command("uninstall").description("Uninstall background daemon").action(async () => {
430
+ await uninstall();
431
+ });
432
+ program.command("status").description("Check daemon status").action(async () => {
433
+ await status();
434
+ });
435
+ program.command("daemon-run", { hidden: true }).description("Internal: Run as daemon process").action(async () => {
436
+ const { runAsDaemon } = await import("./daemon-MNAFMFH4.js");
437
+ await runAsDaemon();
438
+ });
439
+ var cursorCmd = program.command("cursor").description("Cursor IDE integration");
440
+ cursorCmd.command("login [token]").description("Login to Cursor (get token from cursor.com/settings)").action(async (token) => {
441
+ if (token) {
442
+ await cursorLoginWithToken(token);
443
+ } else {
444
+ await cursorLogin();
445
+ }
446
+ });
447
+ cursorCmd.command("sync").description("Sync Cursor usage data from API").action(async () => {
448
+ await cursorSync();
449
+ });
450
+ cursorCmd.command("status").description("Check Cursor auth and cache status").action(() => {
451
+ cursorStatus();
452
+ });
453
+ cursorCmd.command("logout").description("Clear Cursor credentials").action(() => {
454
+ cursorLogout();
455
+ });
456
+ checkForUpdates();
457
+ program.parse();
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/native-runner.ts
4
+ import { createRequire } from "module";
5
+ import { readFileSync } from "fs";
6
+ var require2 = createRequire(import.meta.url);
7
+ var nativeCore = require2("../native-loader.cjs");
8
+ async function main() {
9
+ const inputFile = process.argv[2];
10
+ if (!inputFile) {
11
+ process.stderr.write(JSON.stringify({ error: "No input file provided" }));
12
+ process.exit(1);
13
+ }
14
+ const input = readFileSync(inputFile, "utf-8");
15
+ let request;
16
+ try {
17
+ request = JSON.parse(input);
18
+ } catch (e) {
19
+ throw new Error(`Malformed JSON input: ${e.message}`);
20
+ }
21
+ const { method, args } = request;
22
+ if (!Array.isArray(args) || args.length === 0) {
23
+ throw new Error(`Invalid args for method '${method}': expected at least 1 argument`);
24
+ }
25
+ let result;
26
+ switch (method) {
27
+ case "parseLocalSources":
28
+ result = nativeCore.parseLocalSources(args[0]);
29
+ break;
30
+ case "finalizeReport":
31
+ result = await nativeCore.finalizeReport(args[0]);
32
+ break;
33
+ case "finalizeMonthlyReport":
34
+ result = await nativeCore.finalizeMonthlyReport(args[0]);
35
+ break;
36
+ case "finalizeGraph":
37
+ result = await nativeCore.finalizeGraph(args[0]);
38
+ break;
39
+ case "finalizeReportAndGraph":
40
+ result = await nativeCore.finalizeReportAndGraph(args[0]);
41
+ break;
42
+ default:
43
+ throw new Error(`Unknown method: ${method}`);
44
+ }
45
+ process.stdout.write(JSON.stringify(result));
46
+ }
47
+ main().catch((e) => {
48
+ const error = e;
49
+ process.stderr.write(JSON.stringify({
50
+ error: error.message,
51
+ stack: error.stack
52
+ }));
53
+ process.exit(1);
54
+ });
@@ -0,0 +1,6 @@
1
+ import {
2
+ watch
3
+ } from "./chunk-COBQNFEL.js";
4
+ export {
5
+ watch
6
+ };