@nectarcrm/ccx 0.3.0 → 0.4.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/bin/ccx.js +59 -22
- package/package.json +1 -1
package/bin/ccx.js
CHANGED
|
@@ -101,6 +101,14 @@ function writeSettings(s) {
|
|
|
101
101
|
writeJson(SETTINGS, s)
|
|
102
102
|
if (!WIN) chmodSync(SETTINGS, 0o600) // agora guarda um token
|
|
103
103
|
}
|
|
104
|
+
// Quando cada conta passou a valer neste computador (o sync usa pra saber a conta de cada sessão)
|
|
105
|
+
const HISTORY_DAYS = 45
|
|
106
|
+
function withHistory(app, id) {
|
|
107
|
+
const since = new Date(Date.now() - HISTORY_DAYS * 86_400_000).toISOString()
|
|
108
|
+
const history = (app?.history ?? []).filter(h => h.at >= since)
|
|
109
|
+
if (history.at(-1)?.id !== id) history.push({ at: new Date().toISOString(), id })
|
|
110
|
+
return history
|
|
111
|
+
}
|
|
104
112
|
function applyAccount(app, key, account, token) {
|
|
105
113
|
const s = readSettings()
|
|
106
114
|
const env = { ...s.env }
|
|
@@ -117,7 +125,7 @@ function applyAccount(app, key, account, token) {
|
|
|
117
125
|
// Métricas não são usadas; desliga só se a pessoa não configurou as dela
|
|
118
126
|
if (!('OTEL_METRICS_EXPORTER' in env) || app.owned?.includes('OTEL_METRICS_EXPORTER')) vars.OTEL_METRICS_EXPORTER = 'none'
|
|
119
127
|
writeSettings({ ...s, env: { ...env, ...vars } })
|
|
120
|
-
return { ...app, account: { id: account.id, slug: account.slug, label: account.label }, token_fp: fingerprint(token), owned: Object.keys(vars) }
|
|
128
|
+
return { ...app, account: { id: account.id, slug: account.slug, label: account.label }, token_fp: fingerprint(token), owned: Object.keys(vars), history: withHistory(app, account.id) }
|
|
121
129
|
}
|
|
122
130
|
function removeAccount(app) {
|
|
123
131
|
if (app?.owned?.length && existsSync(SETTINGS)) {
|
|
@@ -128,7 +136,7 @@ function removeAccount(app) {
|
|
|
128
136
|
else delete s.env
|
|
129
137
|
writeSettings(s)
|
|
130
138
|
}
|
|
131
|
-
return { ...app, account: null, token_fp: null, owned: [] }
|
|
139
|
+
return { ...app, account: null, token_fp: null, owned: [], history: withHistory(app, null) }
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
// ---------- vigia em segundo plano ----------
|
|
@@ -248,8 +256,8 @@ async function agentOnce() {
|
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
// ---------- sync: resumo das sessões do Claude Code ----------
|
|
251
|
-
// Lê ~/.claude/projects/**/*.jsonl, soma por sessão, projeto e dia (de Brasília)
|
|
252
|
-
// totais: pedidos, respostas, ferramentas e tokens. Nunca o texto da conversa.
|
|
259
|
+
// Lê ~/.claude/projects/**/*.jsonl, soma por sessão, projeto e dia (de Brasília), separando branch e modelo,
|
|
260
|
+
// e manda só os totais: pedidos, respostas, ferramentas e tokens. Nunca o texto da conversa.
|
|
253
261
|
// Os arquivos só crescem: guardamos até onde lemos cada um e os totais, e mandamos só o que mudou.
|
|
254
262
|
const PROJECTS = join(dirname(SETTINGS), 'projects')
|
|
255
263
|
const SYNC_STATE = join(DIR, 'sync.json')
|
|
@@ -257,6 +265,7 @@ const SYNC_LOCK = join(DIR, 'sync.lock')
|
|
|
257
265
|
const SYNC_BATCH = 2000
|
|
258
266
|
const SYNC_CHUNK = 1 << 20
|
|
259
267
|
const SYNC_KEEP_DAYS = 40 // o Claude apaga as sessões com 30 dias
|
|
268
|
+
const SYNC_VERSION = 2 // mudou o formato do sync.json: lê tudo de novo
|
|
260
269
|
const brtDay = ts => new Date(Date.parse(ts) - 3 * 3600_000).toISOString().slice(0, 10)
|
|
261
270
|
|
|
262
271
|
function jsonlFiles(dir) {
|
|
@@ -326,9 +335,10 @@ function addLine(events, text) {
|
|
|
326
335
|
const u = o.message.usage
|
|
327
336
|
const tools = (events.get(o.requestId)?.tools ?? 0) + (o.message.content ?? []).filter(b => b?.type === 'tool_use').length
|
|
328
337
|
events.set(o.requestId, {
|
|
329
|
-
...base, kind: 'resposta', tools,
|
|
338
|
+
...base, kind: 'resposta', tools, model: typeof o.message.model === 'string' ? o.message.model : '',
|
|
330
339
|
input_tokens: u.input_tokens ?? 0, output_tokens: u.output_tokens ?? 0,
|
|
331
340
|
cache_read_tokens: u.cache_read_input_tokens ?? 0, cache_creation_tokens: u.cache_creation_input_tokens ?? 0,
|
|
341
|
+
cache_1h_tokens: u.cache_creation?.ephemeral_1h_input_tokens ?? 0,
|
|
332
342
|
})
|
|
333
343
|
return
|
|
334
344
|
}
|
|
@@ -340,17 +350,19 @@ function addLine(events, text) {
|
|
|
340
350
|
}
|
|
341
351
|
}
|
|
342
352
|
|
|
343
|
-
// Soma um evento no
|
|
353
|
+
// Soma um evento no dia da sessão e projeto, na parte da branch e do modelo dele
|
|
354
|
+
const SUMS = ['tools', 'input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_creation_tokens', 'cache_1h_tokens']
|
|
344
355
|
function addToDay(days, e) {
|
|
345
356
|
const day = brtDay(e.at)
|
|
346
357
|
const key = `${e.session_id}\t${e.project}\t${day}`
|
|
347
|
-
const d = days[key] ??= { session_id: e.session_id, project: e.project, day,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (e.
|
|
358
|
+
const d = days[key] ??= { session_id: e.session_id, project: e.project, day, parts: {} }
|
|
359
|
+
const branch = e.branch ?? '', model = e.model ?? ''
|
|
360
|
+
const p = d.parts[`${branch}\t${model}`] ??= { branch, model, last_at: e.at, prompts: 0, responses: 0, ...Object.fromEntries(SUMS.map(k => [k, 0])) }
|
|
361
|
+
if (e.at > p.last_at) p.last_at = e.at
|
|
362
|
+
if (e.kind === 'pedido') p.prompts++
|
|
351
363
|
else {
|
|
352
|
-
|
|
353
|
-
for (const k of
|
|
364
|
+
p.responses++
|
|
365
|
+
for (const k of SUMS) p[k] += e[k]
|
|
354
366
|
}
|
|
355
367
|
return key
|
|
356
368
|
}
|
|
@@ -370,9 +382,16 @@ async function sync(app, key, quiet = false) {
|
|
|
370
382
|
const say = msg => { if (!quiet) process.stderr.write(msg) }
|
|
371
383
|
if (!takeLock()) return say('Outro sync já está rodando. Tente de novo daqui a pouco.\n')
|
|
372
384
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
385
|
+
let state = readJson(SYNC_STATE, {})
|
|
386
|
+
if (state.v !== SYNC_VERSION) state = {}
|
|
387
|
+
const offsets = state.files ?? {}, days = state.days ?? {}, sessions = state.sessions ?? {}
|
|
375
388
|
const changed = new Set()
|
|
389
|
+
// Modo oculto: da conta marcada pelo superadmin, só os projetos da lista saem daqui. App antigo: nenhuma.
|
|
390
|
+
const h = await api(app, key, 'GET', '/api/cli/hidden', null, 20_000)
|
|
391
|
+
if (h.status !== 200 && h.status !== 404) throw new Error(h.data.message ?? `HTTP ${h.status}`) // na dúvida, não manda nada
|
|
392
|
+
const hidden = h.status === 200 ? h.data.accounts ?? {} : {}
|
|
393
|
+
const accountAt = at => (app.history ?? []).filter(x => x.at <= at).at(-1)?.id ?? null
|
|
394
|
+
const excluded = new Set()
|
|
376
395
|
const files = jsonlFiles(PROJECTS)
|
|
377
396
|
say(`Lendo as sessões do Claude Code em ${PROJECTS}`)
|
|
378
397
|
for (const file of files) {
|
|
@@ -380,7 +399,13 @@ async function sync(app, key, quiet = false) {
|
|
|
380
399
|
const from = offsets[file] ?? 0
|
|
381
400
|
if (from >= size) continue // nada novo (ou arquivo trocado: não soma de novo)
|
|
382
401
|
const { events, offset } = readNewLines(file, from)
|
|
383
|
-
for (const e of events)
|
|
402
|
+
for (const e of events) {
|
|
403
|
+
// A conta da sessão é a que valia quando ela começou (claude aberto não troca de conta)
|
|
404
|
+
const s = sessions[e.session_id] ??= { id: accountAt(e.at), at: e.at }
|
|
405
|
+
const only = s.id === null ? null : hidden[s.id]
|
|
406
|
+
if (only && !only.includes(e.project)) { excluded.add(e.session_id); continue }
|
|
407
|
+
changed.add(addToDay(days, e))
|
|
408
|
+
}
|
|
384
409
|
offsets[file] = offset
|
|
385
410
|
}
|
|
386
411
|
// Esquece arquivo apagado e dia antigo
|
|
@@ -388,17 +413,29 @@ async function sync(app, key, quiet = false) {
|
|
|
388
413
|
for (const f of Object.keys(offsets)) if (!alive.has(f)) delete offsets[f]
|
|
389
414
|
const oldest = brtDay(new Date(Date.now() - SYNC_KEEP_DAYS * 86_400_000).toISOString())
|
|
390
415
|
for (const [k, d] of Object.entries(days)) if (d.day < oldest) delete days[k]
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
416
|
+
for (const [k, s] of Object.entries(sessions)) if (brtDay(s.at) < oldest) delete sessions[k]
|
|
417
|
+
|
|
418
|
+
// O app troca o dia inteiro da sessão: as partes de um dia vão sempre no mesmo lote
|
|
419
|
+
const sent = [...changed].map(k => days[k]).filter(Boolean)
|
|
420
|
+
const items = sent.map(({ parts, ...d }) => Object.values(parts).map(p => ({ ...d, ...p })))
|
|
421
|
+
let batch = [], exclude = [...excluded]
|
|
422
|
+
const send = async () => {
|
|
423
|
+
if (!batch.length && !exclude.length) return
|
|
394
424
|
say('.')
|
|
395
|
-
const r = await api(app, key, 'POST', '/api/cli/sync', { items:
|
|
425
|
+
const r = await api(app, key, 'POST', '/api/cli/sync', { items: batch, ...(exclude.length && { exclude_sessions: exclude }) })
|
|
396
426
|
if (r.status !== 200) throw new Error(r.data.message ?? `HTTP ${r.status}`)
|
|
427
|
+
batch = []
|
|
428
|
+
exclude = []
|
|
429
|
+
}
|
|
430
|
+
for (const day of items) {
|
|
431
|
+
if (batch.length + day.length > SYNC_BATCH) await send()
|
|
432
|
+
batch.push(...day.slice(0, SYNC_BATCH))
|
|
397
433
|
}
|
|
434
|
+
await send()
|
|
398
435
|
// Só grava depois de enviar: se falhar, a próxima vez lê e manda de novo
|
|
399
|
-
writeJson(SYNC_STATE, { files: offsets, days })
|
|
400
|
-
say(`\n${c(32, '✓')} Enviado: ${
|
|
401
|
-
return
|
|
436
|
+
writeJson(SYNC_STATE, { v: SYNC_VERSION, files: offsets, days, sessions })
|
|
437
|
+
say(`\n${c(32, '✓')} Enviado: ${sent.length} ${sent.length === 1 ? 'resumo' : 'resumos'} (sessão, projeto e dia).\n`)
|
|
438
|
+
return sent.length
|
|
402
439
|
}
|
|
403
440
|
finally { rmSync(SYNC_LOCK, { force: true }) }
|
|
404
441
|
}
|