@danforthh/aimprint-sync 1.0.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 +417 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// index.ts
|
|
26
|
+
var import_node_fs2 = __toESM(require("node:fs"));
|
|
27
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
28
|
+
var import_node_os2 = __toESM(require("node:os"));
|
|
29
|
+
var import_node_readline = __toESM(require("node:readline"));
|
|
30
|
+
|
|
31
|
+
// cursor.ts
|
|
32
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
33
|
+
var import_node_path = __toESM(require("node:path"));
|
|
34
|
+
var import_node_os = __toESM(require("node:os"));
|
|
35
|
+
var CURSOR_DIR = import_node_path.default.join(import_node_os.default.homedir(), ".claude-tracker");
|
|
36
|
+
var CURSOR_FILE = import_node_path.default.join(CURSOR_DIR, "cursor.json");
|
|
37
|
+
function loadCursors() {
|
|
38
|
+
try {
|
|
39
|
+
if (!import_node_fs.default.existsSync(CURSOR_FILE)) return {};
|
|
40
|
+
return JSON.parse(import_node_fs.default.readFileSync(CURSOR_FILE, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function saveCursors(cursors) {
|
|
46
|
+
import_node_fs.default.mkdirSync(CURSOR_DIR, { recursive: true });
|
|
47
|
+
import_node_fs.default.writeFileSync(CURSOR_FILE, JSON.stringify(cursors, null, 2));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// classifier.ts
|
|
51
|
+
var RE_REFINEMENT = /refactor|simplif|improve|clean|optim|restructur|reorganis|reorganiz/i;
|
|
52
|
+
var RE_QUALITY = /test|coverage|jest|vitest|eslint|lint|review|security|audit|sonar/i;
|
|
53
|
+
var RE_OPS = /deploy|docker|ci\b|cd\b|pipeline|github.?action|kubernetes|k8s|helm|terraform|wrangler deploy|npm run build|npm run deploy/i;
|
|
54
|
+
var RE_PLANNING = /plan|prd|ticket|story|roadmap|strateg|epic|sprint|backlog|clickup|jira|milestone/i;
|
|
55
|
+
var RE_ANALYSIS = /explai|analys|analyz|understand|how does|what is|why |architecture|investigate|research|explore/i;
|
|
56
|
+
var RE_WRITING = /implement|write|create|add|build|generat|develop|code/i;
|
|
57
|
+
var RE_DEBUG = /fix|bug|error|crash|broken|debug|issue|problem|fail/i;
|
|
58
|
+
var RE_DOCUMENT = /powerpoint|presentation|slides|slide.?deck|\.pptx|word.?doc|\.docx|spreadsheet|\.xlsx|generate.*(report|document|pdf)|pptx|docx|xlsx/i;
|
|
59
|
+
var RE_BASH_OPS = /docker|kubectl|helm|wrangler deploy|terraform|npm run (deploy|prod)|yarn deploy|gh pr create|gh release/i;
|
|
60
|
+
var RE_BASH_QUAL = /jest|vitest|npm test|yarn test|eslint|prettier|lint|coverage/i;
|
|
61
|
+
var RE_BASH_DOC = /\.pptx\b|\.docx\b|\.xlsx\b|python-pptx/i;
|
|
62
|
+
function classifyRequest(input) {
|
|
63
|
+
const { toolNames, bashCommands, userMessage = "" } = input;
|
|
64
|
+
if (bashCommands.some((c) => RE_BASH_OPS.test(c))) return "code_process";
|
|
65
|
+
if (bashCommands.some((c) => RE_BASH_QUAL.test(c))) return "quality";
|
|
66
|
+
if (bashCommands.some((c) => RE_BASH_DOC.test(c))) return "document_writing";
|
|
67
|
+
if (toolNames.includes("TodoWrite")) return "planning";
|
|
68
|
+
if (toolNames.some((t) => ["Edit", "Write", "NotebookEdit"].includes(t))) return "code_writing";
|
|
69
|
+
if (toolNames.length > 0 && toolNames.every((t) => ["Read", "Grep", "Glob"].includes(t))) return "";
|
|
70
|
+
if (toolNames.length === 0 && userMessage) {
|
|
71
|
+
if (RE_OPS.test(userMessage)) return "code_process";
|
|
72
|
+
if (RE_QUALITY.test(userMessage)) return "quality";
|
|
73
|
+
if (RE_DOCUMENT.test(userMessage)) return "document_writing";
|
|
74
|
+
if (RE_PLANNING.test(userMessage)) return "planning";
|
|
75
|
+
if (RE_REFINEMENT.test(userMessage)) return "refinement";
|
|
76
|
+
if (RE_ANALYSIS.test(userMessage)) return "deep_analysis";
|
|
77
|
+
if (RE_WRITING.test(userMessage)) return "code_writing";
|
|
78
|
+
if (RE_DEBUG.test(userMessage)) return "code_writing";
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
function classify(input) {
|
|
83
|
+
const { firstMessage = "", toolCounts, bashCommands, durationMinutes, requestCount } = input;
|
|
84
|
+
if (requestCount <= 3 && toolCounts.total <= 2 && durationMinutes < 3) return "random";
|
|
85
|
+
const opsCount = bashCommands.filter((c) => RE_BASH_OPS.test(c)).length;
|
|
86
|
+
const qualCount = bashCommands.filter((c) => RE_BASH_QUAL.test(c)).length;
|
|
87
|
+
const totalBash = bashCommands.length || 1;
|
|
88
|
+
const opsRatio = opsCount / totalBash;
|
|
89
|
+
const bashHasOps = opsCount >= 2 || opsCount >= 1 && opsRatio >= 0.3;
|
|
90
|
+
const bashHasQual = qualCount >= 1;
|
|
91
|
+
const docBashCount = bashCommands.filter((c) => RE_BASH_DOC.test(c)).length;
|
|
92
|
+
const signals = {
|
|
93
|
+
planning: (toolCounts.todo > 0 ? 3 : 0) + (RE_PLANNING.test(firstMessage) ? 2 : 0),
|
|
94
|
+
code_process: (bashHasOps ? 4 : 0) + (RE_OPS.test(firstMessage) ? 2 : 0),
|
|
95
|
+
quality: (bashHasQual ? 4 : 0) + (RE_QUALITY.test(firstMessage) ? 2 : 0),
|
|
96
|
+
document_writing: (docBashCount >= 1 ? 4 : 0) + (RE_DOCUMENT.test(firstMessage) ? 3 : 0),
|
|
97
|
+
refinement: (RE_REFINEMENT.test(firstMessage) ? 3 : 0) + (toolCounts.edit > 2 && toolCounts.bash === 0 ? 1 : 0),
|
|
98
|
+
deep_analysis: (toolCounts.read > 8 && toolCounts.edit < 2 ? 4 : 0) + (RE_ANALYSIS.test(firstMessage) ? 2 : 0),
|
|
99
|
+
code_writing: (toolCounts.edit > 3 ? 3 : 0) + (RE_WRITING.test(firstMessage) ? 2 : 0) + (RE_DEBUG.test(firstMessage) ? 1 : 0)
|
|
100
|
+
};
|
|
101
|
+
const top = Object.entries(signals).sort((a, b) => b[1] - a[1])[0];
|
|
102
|
+
if (top[1] === 0) {
|
|
103
|
+
if (toolCounts.edit > 2) return "code_writing";
|
|
104
|
+
if (toolCounts.read > 5) return "deep_analysis";
|
|
105
|
+
if (requestCount <= 4) return "random";
|
|
106
|
+
return "other";
|
|
107
|
+
}
|
|
108
|
+
return top[0];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// index.ts
|
|
112
|
+
var ALLOWED_ENV_KEYS = /* @__PURE__ */ new Set(["WORKER_URL", "SYNC_TOKEN"]);
|
|
113
|
+
var envCandidates = [
|
|
114
|
+
import_node_path2.default.join(import_node_os2.default.homedir(), ".aimprint"),
|
|
115
|
+
import_node_path2.default.join(process.cwd(), "sync", ".env")
|
|
116
|
+
];
|
|
117
|
+
for (const f of envCandidates) {
|
|
118
|
+
if (import_node_fs2.default.existsSync(f)) {
|
|
119
|
+
for (const line of import_node_fs2.default.readFileSync(f, "utf8").split("\n")) {
|
|
120
|
+
const m = line.match(/^([A-Z_]+)=(.+)$/);
|
|
121
|
+
if (m && ALLOWED_ENV_KEYS.has(m[1]) && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var WORKER_URL = process.env["WORKER_URL"];
|
|
127
|
+
var SYNC_TOKEN = process.env["SYNC_TOKEN"];
|
|
128
|
+
if (!WORKER_URL || !SYNC_TOKEN) {
|
|
129
|
+
console.error("Error: WORKER_URL and SYNC_TOKEN are required.");
|
|
130
|
+
console.error("Create ~/.aimprint with:");
|
|
131
|
+
console.error(" WORKER_URL=https://your-worker.workers.dev");
|
|
132
|
+
console.error(" SYNC_TOKEN=your-sync-token");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
var CLAUDE_PROJECTS_DIR = import_node_path2.default.join(import_node_os2.default.homedir(), ".claude", "projects");
|
|
136
|
+
var REAL_PROJECTS_DIR = (() => {
|
|
137
|
+
try {
|
|
138
|
+
return import_node_fs2.default.realpathSync(CLAUDE_PROJECTS_DIR);
|
|
139
|
+
} catch {
|
|
140
|
+
return CLAUDE_PROJECTS_DIR;
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
var MACHINE = import_node_os2.default.hostname();
|
|
144
|
+
var BATCH_SIZE = 100;
|
|
145
|
+
function estimateCost(model, input, output, cacheRead, cacheCreation) {
|
|
146
|
+
const p = {
|
|
147
|
+
"claude-fable-5": [10, 50, 1, 12.5],
|
|
148
|
+
"claude-opus-4-8": [5, 25, 0.5, 6.25],
|
|
149
|
+
"claude-opus-4-7": [5, 25, 0.5, 6.25],
|
|
150
|
+
"claude-opus-4-6": [5, 25, 0.5, 6.25],
|
|
151
|
+
"claude-opus-4-5": [5, 25, 0.5, 6.25],
|
|
152
|
+
"claude-sonnet-4-6": [3, 15, 0.3, 3.75],
|
|
153
|
+
"claude-sonnet-4-5": [3, 15, 0.3, 3.75],
|
|
154
|
+
"claude-haiku-4-5": [1, 5, 0.1, 1.25]
|
|
155
|
+
};
|
|
156
|
+
const [pi, po, pr, pc] = p[model] ?? [3, 15, 0.3, 3.75];
|
|
157
|
+
return (input * pi + output * po + cacheRead * pr + cacheCreation * pc) / 1e6;
|
|
158
|
+
}
|
|
159
|
+
var TICKET_RE = /\b(PROTOP-\d+|PORTV4-\d+|NODE20-\d+)\b/i;
|
|
160
|
+
function extractTicket(branch) {
|
|
161
|
+
if (!branch) return void 0;
|
|
162
|
+
const m = branch.match(TICKET_RE);
|
|
163
|
+
return m ? m[1].toUpperCase() : void 0;
|
|
164
|
+
}
|
|
165
|
+
async function parseFile(filePath, offset) {
|
|
166
|
+
const stat = import_node_fs2.default.statSync(filePath);
|
|
167
|
+
if (stat.size <= offset) return { records: /* @__PURE__ */ new Map(), sessions: /* @__PURE__ */ new Map(), newOffset: offset };
|
|
168
|
+
const fd = import_node_fs2.default.openSync(filePath, "r");
|
|
169
|
+
const stream = import_node_fs2.default.createReadStream(filePath, { start: offset, fd, autoClose: true });
|
|
170
|
+
const rl = import_node_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
|
|
171
|
+
const records = /* @__PURE__ */ new Map();
|
|
172
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
173
|
+
let newOffset = offset;
|
|
174
|
+
const lastUserMessage = /* @__PURE__ */ new Map();
|
|
175
|
+
for await (const line of rl) {
|
|
176
|
+
newOffset += Buffer.byteLength(line + "\n");
|
|
177
|
+
if (!line.trim()) continue;
|
|
178
|
+
let d;
|
|
179
|
+
try {
|
|
180
|
+
d = JSON.parse(line);
|
|
181
|
+
} catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const sid = d.sessionId ?? "";
|
|
185
|
+
if (!sid) continue;
|
|
186
|
+
if (!sessions.has(sid)) {
|
|
187
|
+
sessions.set(sid, {
|
|
188
|
+
sessionId: sid,
|
|
189
|
+
cwd: d.cwd ?? "",
|
|
190
|
+
model: "",
|
|
191
|
+
entrypoint: d.entrypoint ?? "",
|
|
192
|
+
gitBranch: d.gitBranch ?? "",
|
|
193
|
+
toolCounts: { edit: 0, bash: 0, read: 0, todo: 0, agent: 0, total: 0 },
|
|
194
|
+
bashCommands: [],
|
|
195
|
+
requestCount: 0
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const sess = sessions.get(sid);
|
|
199
|
+
if (d.cwd) sess.cwd = d.cwd;
|
|
200
|
+
if (d.entrypoint) sess.entrypoint = d.entrypoint;
|
|
201
|
+
if (d.gitBranch) sess.gitBranch = d.gitBranch;
|
|
202
|
+
if (d.type === "user" && d.message?.role === "user") {
|
|
203
|
+
const content = d.message.content;
|
|
204
|
+
let msgText;
|
|
205
|
+
if (Array.isArray(content)) {
|
|
206
|
+
const textBlock = content.find((b) => b["type"] === "text");
|
|
207
|
+
if (textBlock && "text" in textBlock) {
|
|
208
|
+
msgText = String(textBlock["text"]).slice(0, 500);
|
|
209
|
+
}
|
|
210
|
+
} else if (typeof content === "string") {
|
|
211
|
+
msgText = content.slice(0, 500);
|
|
212
|
+
}
|
|
213
|
+
if (msgText) {
|
|
214
|
+
if (!sess.firstMessage) sess.firstMessage = msgText;
|
|
215
|
+
lastUserMessage.set(sid, msgText);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const turnToolNames = [];
|
|
219
|
+
const turnBashCmds = [];
|
|
220
|
+
if (d.type === "assistant" && d.message?.content) {
|
|
221
|
+
for (const block of d.message.content) {
|
|
222
|
+
if (block.type !== "tool_use") continue;
|
|
223
|
+
const name = block.name ?? "";
|
|
224
|
+
sess.toolCounts.total++;
|
|
225
|
+
if (/^(Edit|Write|NotebookEdit)$/.test(name)) sess.toolCounts.edit++;
|
|
226
|
+
else if (name === "Bash") {
|
|
227
|
+
sess.toolCounts.bash++;
|
|
228
|
+
const cmd = String(block.input?.["command"] ?? "");
|
|
229
|
+
if (cmd) {
|
|
230
|
+
sess.bashCommands.push(cmd.slice(0, 200));
|
|
231
|
+
turnBashCmds.push(cmd.slice(0, 200));
|
|
232
|
+
}
|
|
233
|
+
} else if (/^(Read|Grep|Glob)$/.test(name)) sess.toolCounts.read++;
|
|
234
|
+
else if (name === "TodoWrite") sess.toolCounts.todo++;
|
|
235
|
+
else if (name === "Agent") sess.toolCounts.agent++;
|
|
236
|
+
turnToolNames.push(name);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (d.type === "assistant" && d.requestId && d.message?.usage) {
|
|
240
|
+
const usage = d.message.usage;
|
|
241
|
+
const input = usage.input_tokens ?? 0;
|
|
242
|
+
const output = usage.output_tokens ?? 0;
|
|
243
|
+
const cacheR = usage.cache_read_input_tokens ?? 0;
|
|
244
|
+
const cacheC = usage.cache_creation_input_tokens ?? 0;
|
|
245
|
+
if (output > 0) {
|
|
246
|
+
sess.requestCount++;
|
|
247
|
+
sess.model = d.message.model ?? sess.model;
|
|
248
|
+
if (!sess.firstMessageAt || d.timestamp < sess.firstMessageAt) sess.firstMessageAt = d.timestamp;
|
|
249
|
+
if (!sess.lastMessageAt || d.timestamp > sess.lastMessageAt) sess.lastMessageAt = d.timestamp;
|
|
250
|
+
const project = import_node_path2.default.basename(d.cwd ?? sess.cwd ?? "unknown");
|
|
251
|
+
const ticket = extractTicket(d.gitBranch ?? sess.gitBranch);
|
|
252
|
+
const ts = d.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
253
|
+
if (!records.has(d.requestId)) {
|
|
254
|
+
const reqCategory = classifyRequest({
|
|
255
|
+
toolNames: turnToolNames,
|
|
256
|
+
bashCommands: turnBashCmds,
|
|
257
|
+
userMessage: lastUserMessage.get(sid) ?? ""
|
|
258
|
+
});
|
|
259
|
+
records.set(d.requestId, {
|
|
260
|
+
request_id: d.requestId,
|
|
261
|
+
session_id: sid,
|
|
262
|
+
timestamp: ts,
|
|
263
|
+
date: ts.slice(0, 10),
|
|
264
|
+
machine: MACHINE,
|
|
265
|
+
project,
|
|
266
|
+
cwd: d.cwd ?? sess.cwd,
|
|
267
|
+
model: sess.model,
|
|
268
|
+
entrypoint: d.entrypoint ?? sess.entrypoint,
|
|
269
|
+
git_branch: d.gitBranch ?? sess.gitBranch ?? void 0,
|
|
270
|
+
ticket,
|
|
271
|
+
input_tokens: input,
|
|
272
|
+
output_tokens: output,
|
|
273
|
+
cache_read: cacheR,
|
|
274
|
+
cache_creation: cacheC,
|
|
275
|
+
is_sidechain: d.isSidechain ? 1 : 0,
|
|
276
|
+
cost_usd: estimateCost(sess.model, input, output, cacheR, cacheC),
|
|
277
|
+
request_category: reqCategory
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { records, sessions, newOffset };
|
|
284
|
+
}
|
|
285
|
+
async function postBatch(records, sessions, attempt = 1) {
|
|
286
|
+
try {
|
|
287
|
+
const res = await fetch(`${WORKER_URL}/ingest`, {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { "Content-Type": "application/json", "X-Sync-Token": SYNC_TOKEN },
|
|
290
|
+
body: JSON.stringify({ records, sessions })
|
|
291
|
+
});
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
const text = await res.text();
|
|
294
|
+
if (res.status >= 500 && attempt < 3) {
|
|
295
|
+
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
296
|
+
return postBatch(records, sessions, attempt + 1);
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`Ingest failed ${res.status}: ${text}`);
|
|
299
|
+
}
|
|
300
|
+
const result = await res.json();
|
|
301
|
+
console.log(` \u2192 inserted: ${result.inserted}, skipped: ${result.skipped}`);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
if (attempt < 3 && e instanceof TypeError) {
|
|
304
|
+
await new Promise((r) => setTimeout(r, 1e3 * attempt));
|
|
305
|
+
return postBatch(records, sessions, attempt + 1);
|
|
306
|
+
}
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function main() {
|
|
311
|
+
console.log(`Aimprint sync agent`);
|
|
312
|
+
console.log(`Machine: ${MACHINE}`);
|
|
313
|
+
console.log(`Scanning: ${CLAUDE_PROJECTS_DIR}
|
|
314
|
+
`);
|
|
315
|
+
if (!import_node_fs2.default.existsSync(CLAUDE_PROJECTS_DIR)) {
|
|
316
|
+
console.error(`Directory not found: ${CLAUDE_PROJECTS_DIR}`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
const cursors = loadCursors();
|
|
320
|
+
const allRecords = /* @__PURE__ */ new Map();
|
|
321
|
+
const allSessions = /* @__PURE__ */ new Map();
|
|
322
|
+
for (const projectDir of import_node_fs2.default.readdirSync(CLAUDE_PROJECTS_DIR)) {
|
|
323
|
+
const fullDir = import_node_path2.default.join(CLAUDE_PROJECTS_DIR, projectDir);
|
|
324
|
+
if (!import_node_fs2.default.statSync(fullDir).isDirectory()) continue;
|
|
325
|
+
for (const file of import_node_fs2.default.readdirSync(fullDir)) {
|
|
326
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
327
|
+
const filePath = import_node_path2.default.join(fullDir, file);
|
|
328
|
+
try {
|
|
329
|
+
const realFilePath = import_node_fs2.default.realpathSync(filePath);
|
|
330
|
+
if (!realFilePath.startsWith(REAL_PROJECTS_DIR + import_node_path2.default.sep)) continue;
|
|
331
|
+
} catch {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const offset = cursors[filePath] ?? 0;
|
|
335
|
+
try {
|
|
336
|
+
const { records: records2, sessions, newOffset } = await parseFile(filePath, offset);
|
|
337
|
+
for (const [k, v] of records2) allRecords.set(k, v);
|
|
338
|
+
for (const [k, v] of sessions) {
|
|
339
|
+
const existing = allSessions.get(k);
|
|
340
|
+
if (!existing) {
|
|
341
|
+
allSessions.set(k, v);
|
|
342
|
+
} else {
|
|
343
|
+
existing.requestCount += v.requestCount;
|
|
344
|
+
existing.toolCounts.edit += v.toolCounts.edit;
|
|
345
|
+
existing.toolCounts.bash += v.toolCounts.bash;
|
|
346
|
+
existing.toolCounts.read += v.toolCounts.read;
|
|
347
|
+
existing.toolCounts.todo += v.toolCounts.todo;
|
|
348
|
+
existing.toolCounts.agent += v.toolCounts.agent;
|
|
349
|
+
existing.toolCounts.total += v.toolCounts.total;
|
|
350
|
+
existing.bashCommands.push(...v.bashCommands);
|
|
351
|
+
if (!existing.firstMessage && v.firstMessage) existing.firstMessage = v.firstMessage;
|
|
352
|
+
if (!existing.model && v.model) existing.model = v.model;
|
|
353
|
+
if (!existing.cwd && v.cwd) existing.cwd = v.cwd;
|
|
354
|
+
if (!existing.gitBranch && v.gitBranch) existing.gitBranch = v.gitBranch;
|
|
355
|
+
if (!existing.entrypoint && v.entrypoint) existing.entrypoint = v.entrypoint;
|
|
356
|
+
if (v.firstMessageAt && (!existing.firstMessageAt || v.firstMessageAt < existing.firstMessageAt))
|
|
357
|
+
existing.firstMessageAt = v.firstMessageAt;
|
|
358
|
+
if (v.lastMessageAt && (!existing.lastMessageAt || v.lastMessageAt > existing.lastMessageAt))
|
|
359
|
+
existing.lastMessageAt = v.lastMessageAt;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
cursors[filePath] = newOffset;
|
|
363
|
+
} catch (e) {
|
|
364
|
+
console.warn(` Warning: could not parse ${filePath}: ${e}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
console.log(`Found ${allRecords.size} new request records across ${allSessions.size} sessions`);
|
|
369
|
+
if (allRecords.size === 0) {
|
|
370
|
+
console.log("Nothing new to sync.");
|
|
371
|
+
saveCursors(cursors);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const sessionMetas = [];
|
|
375
|
+
for (const [, sess] of allSessions) {
|
|
376
|
+
const duration = (() => {
|
|
377
|
+
if (!sess.firstMessageAt || !sess.lastMessageAt) return 0;
|
|
378
|
+
const start = new Date(sess.firstMessageAt).getTime();
|
|
379
|
+
const end = new Date(sess.lastMessageAt).getTime();
|
|
380
|
+
if (isNaN(start) || isNaN(end) || end < start) return 0;
|
|
381
|
+
return (end - start) / 6e4;
|
|
382
|
+
})();
|
|
383
|
+
const input = {
|
|
384
|
+
firstMessage: sess.firstMessage,
|
|
385
|
+
toolCounts: sess.toolCounts,
|
|
386
|
+
bashCommands: sess.bashCommands,
|
|
387
|
+
durationMinutes: duration,
|
|
388
|
+
requestCount: sess.requestCount
|
|
389
|
+
};
|
|
390
|
+
sessionMetas.push({
|
|
391
|
+
session_id: sess.sessionId,
|
|
392
|
+
category: classify(input),
|
|
393
|
+
category_source: "auto",
|
|
394
|
+
first_message: sess.firstMessage,
|
|
395
|
+
tool_summary: JSON.stringify({
|
|
396
|
+
edit: sess.toolCounts.edit,
|
|
397
|
+
bash: sess.toolCounts.bash,
|
|
398
|
+
read: sess.toolCounts.read,
|
|
399
|
+
todo: sess.toolCounts.todo,
|
|
400
|
+
agent: sess.toolCounts.agent
|
|
401
|
+
})
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const records = Array.from(allRecords.values());
|
|
405
|
+
const totalBatches = Math.ceil(records.length / BATCH_SIZE);
|
|
406
|
+
for (let i = 0; i < records.length; i += BATCH_SIZE) {
|
|
407
|
+
const batch = records.slice(i, i + BATCH_SIZE);
|
|
408
|
+
console.log(`Posting batch ${Math.floor(i / BATCH_SIZE) + 1}/${totalBatches}...`);
|
|
409
|
+
await postBatch(batch, sessionMetas);
|
|
410
|
+
}
|
|
411
|
+
saveCursors(cursors);
|
|
412
|
+
console.log("\nSync complete.");
|
|
413
|
+
}
|
|
414
|
+
main().catch((e) => {
|
|
415
|
+
console.error(e);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@danforthh/aimprint-sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sync Claude Code token logs to your Aimprint dashboard",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aimprint-sync": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "node build.mjs",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.0.0",
|
|
18
|
+
"esbuild": "^0.25.0",
|
|
19
|
+
"typescript": "^5.7.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Danforthhh/Aimprint.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"claude",
|
|
30
|
+
"claude-code",
|
|
31
|
+
"token",
|
|
32
|
+
"usage",
|
|
33
|
+
"tracker"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|