@nectarcrm/ccx 0.1.1 → 0.3.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 (3) hide show
  1. package/README.md +2 -0
  2. package/bin/ccx.js +309 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,6 +17,8 @@ ccx login # autoriza este computador (abre o navegador
17
17
  ccx # mostra suas contas e troca a conta deste computador
18
18
  ccx claude-infra # troca direto
19
19
  ccx status # conta atual e uso de cada conta
20
+ ccx doctor # confere se o claude está mesmo usando a conta escolhida (e o que pode atrapalhar)
21
+ ccx sync # manda pro app o resumo das sessões (roda sozinho após o login e a cada 5 min)
20
22
  ccx sair # o claude volta ao seu login normal
21
23
  ccx logout # sai do app neste computador
22
24
  ```
package/bin/ccx.js CHANGED
@@ -6,6 +6,8 @@
6
6
  // ccx mostra suas contas e troca a conta deste computador
7
7
  // ccx <conta> troca direto (nome curto, ex.: claude-infra)
8
8
  // ccx status conta atual e uso das contas liberadas pra você
9
+ // ccx doctor confere se o claude está mesmo usando a conta escolhida
10
+ // ccx sync manda pro app o resumo das sessões do Claude (totais por projeto e dia; nunca o texto)
9
11
  // ccx sair tira a conta deste computador (o claude volta ao login normal)
10
12
  // ccx logout sai do app: tira a conta, a autorização e o vigia
11
13
  // ccx --version versão instalada (atualizar: npm install -g @nectarcrm/ccx@latest)
@@ -14,12 +16,13 @@
14
16
  // (terminal, VS Code, JetBrains) usa essa conta. Claudes já abertos continuam na anterior.
15
17
  // O vigia roda sozinho a cada 5 min: avisa o app, atualiza o token se o admin trocou
16
18
  // e tira a conta se o acesso for removido. Os tokens gastos vão pro app pela telemetria.
19
+ // O vigia também roda o sync, então o resumo dos projetos chega sozinho.
17
20
 
18
21
  import { execFileSync } from 'node:child_process'
19
- import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
22
+ import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from 'node:fs'
20
23
  import { createHash } from 'node:crypto'
21
24
  import { homedir, hostname } from 'node:os'
22
- import { dirname, join } from 'node:path'
25
+ import { basename, dirname, join } from 'node:path'
23
26
  import { fileURLToPath } from 'node:url'
24
27
 
25
28
  const DIR = join(homedir(), '.config', 'ccx')
@@ -221,6 +224,8 @@ async function agentOnce() {
221
224
  writeJson(APP, removeAccount(app))
222
225
  return log('login vencido: conta tirada deste computador')
223
226
  }
227
+ try { await sync(app, key, true) }
228
+ catch (e) { log(`sync falhou: ${e.message}`) }
224
229
  const r = await api(app, key, 'POST', '/api/cli/heartbeat', { account_id: app.account?.id ?? null }, 20_000)
225
230
  if (r.status === 401) {
226
231
  writeJson(APP, removeAccount(app))
@@ -242,6 +247,292 @@ async function agentOnce() {
242
247
  }
243
248
  }
244
249
 
250
+ // ---------- sync: resumo das sessões do Claude Code ----------
251
+ // Lê ~/.claude/projects/**/*.jsonl, soma por sessão, projeto e dia (de Brasília) e manda só os
252
+ // totais: pedidos, respostas, ferramentas e tokens. Nunca o texto da conversa.
253
+ // Os arquivos só crescem: guardamos até onde lemos cada um e os totais, e mandamos só o que mudou.
254
+ const PROJECTS = join(dirname(SETTINGS), 'projects')
255
+ const SYNC_STATE = join(DIR, 'sync.json')
256
+ const SYNC_LOCK = join(DIR, 'sync.lock')
257
+ const SYNC_BATCH = 2000
258
+ const SYNC_CHUNK = 1 << 20
259
+ const SYNC_KEEP_DAYS = 40 // o Claude apaga as sessões com 30 dias
260
+ const brtDay = ts => new Date(Date.parse(ts) - 3 * 3600_000).toISOString().slice(0, 10)
261
+
262
+ function jsonlFiles(dir) {
263
+ const out = []
264
+ let list = []
265
+ try { list = readdirSync(dir, { withFileTypes: true }) }
266
+ catch { return out }
267
+ for (const e of list) {
268
+ const p = join(dir, e.name)
269
+ if (e.isDirectory()) out.push(...jsonlFiles(p))
270
+ else if (e.name.endsWith('.jsonl')) out.push(p)
271
+ }
272
+ return out
273
+ }
274
+
275
+ // Nome do projeto: o repositório do git (vale pra worktree também); sem git, o nome da pasta
276
+ const projectNames = new Map()
277
+ function projectOf(cwd) {
278
+ if (!projectNames.has(cwd)) {
279
+ let name = basename(cwd) || cwd
280
+ try {
281
+ const url = execFileSync('git', ['-C', cwd, 'remote', 'get-url', 'origin'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
282
+ name = basename(url.replace(/[/:]+$/, '')).replace(/\.git$/, '') || name
283
+ }
284
+ catch {}
285
+ projectNames.set(cwd, name)
286
+ }
287
+ return projectNames.get(cwd)
288
+ }
289
+
290
+ // Lê só as linhas completas a partir de `from`. Devolve as respostas e pedidos novos e até onde leu.
291
+ function readNewLines(file, from) {
292
+ const events = new Map()
293
+ const fd = openSync(file, 'r')
294
+ const buf = Buffer.alloc(SYNC_CHUNK)
295
+ let pos = from, rest = Buffer.alloc(0)
296
+ try {
297
+ for (;;) {
298
+ const n = readSync(fd, buf, 0, SYNC_CHUNK, pos + rest.length)
299
+ if (!n) break
300
+ let data = Buffer.concat([rest, buf.subarray(0, n)])
301
+ let nl
302
+ while ((nl = data.indexOf(10)) !== -1) {
303
+ const lineBuf = data.subarray(0, nl)
304
+ data = data.subarray(nl + 1)
305
+ pos += nl + 1
306
+ // Filtro barato antes de ler o JSON (as linhas com resultado de ferramenta são enormes)
307
+ const assistant = lineBuf.includes('"type":"assistant"') && lineBuf.includes('"usage"')
308
+ const prompt = !assistant && lineBuf.includes('"type":"user"') && !lineBuf.includes('"tool_result"')
309
+ if (assistant || prompt) addLine(events, lineBuf.toString())
310
+ }
311
+ rest = Buffer.from(data)
312
+ }
313
+ }
314
+ finally { closeSync(fd) }
315
+ return { events: [...events.values()], offset: pos }
316
+ }
317
+
318
+ function addLine(events, text) {
319
+ let o
320
+ try { o = JSON.parse(text) }
321
+ catch { return }
322
+ if (!o.cwd || !o.sessionId || !o.timestamp || Number.isNaN(Date.parse(o.timestamp))) return
323
+ const base = { session_id: o.sessionId, project: projectOf(o.cwd), branch: o.gitBranch || null, at: o.timestamp }
324
+ if (o.type === 'assistant' && o.requestId && o.message?.usage) {
325
+ // Uma resposta vem em várias linhas (uma por bloco), com o mesmo requestId
326
+ const u = o.message.usage
327
+ const tools = (events.get(o.requestId)?.tools ?? 0) + (o.message.content ?? []).filter(b => b?.type === 'tool_use').length
328
+ events.set(o.requestId, {
329
+ ...base, kind: 'resposta', tools,
330
+ input_tokens: u.input_tokens ?? 0, output_tokens: u.output_tokens ?? 0,
331
+ cache_read_tokens: u.cache_read_input_tokens ?? 0, cache_creation_tokens: u.cache_creation_input_tokens ?? 0,
332
+ })
333
+ return
334
+ }
335
+ // Pedido digitado pela pessoa (não conta subagente, comando interno nem mensagem do sistema)
336
+ const content = o.message?.content
337
+ const typed = typeof content === 'string' ? content : Array.isArray(content) ? content.find(b => b?.type === 'text')?.text : null
338
+ if (o.type === 'user' && o.uuid && !o.isSidechain && !o.isMeta && typed && !typed.trimStart().startsWith('<')) {
339
+ events.set(`p:${o.uuid}`, { ...base, kind: 'pedido' })
340
+ }
341
+ }
342
+
343
+ // Soma um evento no total da sessão, projeto e dia
344
+ function addToDay(days, e) {
345
+ const day = brtDay(e.at)
346
+ const key = `${e.session_id}\t${e.project}\t${day}`
347
+ const d = days[key] ??= { session_id: e.session_id, project: e.project, day, branch: null, last_at: e.at,
348
+ prompts: 0, responses: 0, tools: 0, input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0 }
349
+ if (e.at >= d.last_at) { d.last_at = e.at; d.branch = e.branch ?? d.branch }
350
+ if (e.kind === 'pedido') d.prompts++
351
+ else {
352
+ d.responses++
353
+ for (const k of ['tools', 'input_tokens', 'output_tokens', 'cache_read_tokens', 'cache_creation_tokens']) d[k] += e[k]
354
+ }
355
+ return key
356
+ }
357
+
358
+ // Só um sync por vez (o vigia pode rodar junto com o login). Trava velha (sync que caiu) é ignorada.
359
+ function takeLock() {
360
+ mkdirSync(DIR, { recursive: true })
361
+ try { closeSync(openSync(SYNC_LOCK, 'wx')); return true }
362
+ catch {
363
+ try { if (Date.now() - statSync(SYNC_LOCK).mtimeMs < 30 * 60_000) return false } catch {}
364
+ rmSync(SYNC_LOCK, { force: true })
365
+ try { closeSync(openSync(SYNC_LOCK, 'wx')); return true } catch { return false }
366
+ }
367
+ }
368
+
369
+ async function sync(app, key, quiet = false) {
370
+ const say = msg => { if (!quiet) process.stderr.write(msg) }
371
+ if (!takeLock()) return say('Outro sync já está rodando. Tente de novo daqui a pouco.\n')
372
+ try {
373
+ const state = readJson(SYNC_STATE, {})
374
+ const offsets = state.files ?? {}, days = state.days ?? {}
375
+ const changed = new Set()
376
+ const files = jsonlFiles(PROJECTS)
377
+ say(`Lendo as sessões do Claude Code em ${PROJECTS}`)
378
+ for (const file of files) {
379
+ const size = statSync(file).size
380
+ const from = offsets[file] ?? 0
381
+ if (from >= size) continue // nada novo (ou arquivo trocado: não soma de novo)
382
+ const { events, offset } = readNewLines(file, from)
383
+ for (const e of events) changed.add(addToDay(days, e))
384
+ offsets[file] = offset
385
+ }
386
+ // Esquece arquivo apagado e dia antigo
387
+ const alive = new Set(files)
388
+ for (const f of Object.keys(offsets)) if (!alive.has(f)) delete offsets[f]
389
+ const oldest = brtDay(new Date(Date.now() - SYNC_KEEP_DAYS * 86_400_000).toISOString())
390
+ for (const [k, d] of Object.entries(days)) if (d.day < oldest) delete days[k]
391
+
392
+ const items = [...changed].map(k => days[k]).filter(Boolean)
393
+ for (let i = 0; i < items.length; i += SYNC_BATCH) {
394
+ say('.')
395
+ const r = await api(app, key, 'POST', '/api/cli/sync', { items: items.slice(i, i + SYNC_BATCH) })
396
+ if (r.status !== 200) throw new Error(r.data.message ?? `HTTP ${r.status}`)
397
+ }
398
+ // 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: ${items.length} ${items.length === 1 ? 'resumo' : 'resumos'} (sessão, projeto e dia).\n`)
401
+ return items.length
402
+ }
403
+ finally { rmSync(SYNC_LOCK, { force: true }) }
404
+ }
405
+
406
+ // ---------- doctor: confere se o claude está mesmo usando a conta escolhida ----------
407
+ // Nunca mostra token: compara só a impressão digital (fingerprint).
408
+ const AUTH_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX']
409
+ const MANAGED = WIN ? ['C:\\Program Files\\ClaudeCode\\managed-settings.json', 'C:\\ProgramData\\ClaudeCode\\managed-settings.json']
410
+ : MAC ? ['/Library/Application Support/ClaudeCode/managed-settings.json'] : ['/etc/claude-code/managed-settings.json']
411
+
412
+ function winUserEnv(name) {
413
+ try { return ps(`[Environment]::GetEnvironmentVariable('${name}','User'); [Environment]::GetEnvironmentVariable('${name}','Machine')`).split(/\r?\n/).find(Boolean) || null }
414
+ catch { return null }
415
+ }
416
+ function agentInstalled() {
417
+ try {
418
+ if (MAC) return existsSync(PLIST)
419
+ if (WIN) { run('schtasks', ['/Query', '/TN', AGENT_ID]); return true }
420
+ if (existsSync(join(UNIT_DIR, 'ccx-vigia.timer'))) return true
421
+ return run('crontab', ['-l']).includes(CRON_TAG)
422
+ }
423
+ catch { return false }
424
+ }
425
+
426
+ async function doctor() {
427
+ let bad = 0, warn = 0
428
+ const ok = msg => console.log(`${c(32, '✓')} ${msg}`)
429
+ const aviso = (msg, fix) => { warn++; console.log(`${c(33, '!')} ${msg}${fix ? `\n ${c(90, '→ ' + fix)}` : ''}`) }
430
+ const erro = (msg, fix) => { bad++; console.log(`${c(31, '✗')} ${msg}${fix ? `\n ${c(90, '→ ' + fix)}` : ''}`) }
431
+ const mine = readJson(join(dirname(SELF), '..', 'package.json'), {}).version
432
+
433
+ console.log(c(1, 'ccx doctor\n'))
434
+ // Versões
435
+ const [major, minor] = process.versions.node.split('.').map(Number)
436
+ if (major > 18 || (major === 18 && minor >= 17)) ok(`Node ${process.versions.node}`)
437
+ else erro(`Node ${process.versions.node} é velho demais`, 'instale o Node 18.17 ou mais novo')
438
+ try {
439
+ const r = await fetch('https://registry.npmjs.org/@nectarcrm%2fccx/latest', { signal: AbortSignal.timeout(8000) })
440
+ const latest = (await r.json()).version
441
+ if (latest && latest !== mine) aviso(`ccx ${mine} (a mais nova é ${latest})`, 'npm install -g @nectarcrm/ccx@latest')
442
+ else ok(`ccx ${mine} (a mais nova)`)
443
+ }
444
+ catch { ok(`ccx ${mine} (não deu pra ver se há versão nova)`) }
445
+ try { ok(`Claude Code ${execFileSync('claude', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], shell: WIN }).toString().trim()}`) }
446
+ catch { aviso('Não achei o comando claude neste terminal', 'instale o Claude Code ou abra um terminal novo') }
447
+
448
+ // Login no app
449
+ const app = readJson(APP, null)
450
+ const key = app && secretGet()
451
+ if (!app || !key) {
452
+ erro('Este computador não está conectado ao app', 'ccx login')
453
+ return finish()
454
+ }
455
+ if (new Date(app.expires_at) < new Date()) erro(`O login venceu em ${new Date(app.expires_at).toLocaleDateString('pt-BR')}`, 'ccx login')
456
+ else ok(`Conectado como ${app.email} em ${app.url} (vale até ${new Date(app.expires_at).toLocaleDateString('pt-BR')})`)
457
+
458
+ const st = await api(app, key, 'GET', '/api/cli/status', null, 20_000)
459
+ if (st.status !== 200) {
460
+ erro(`O app não respondeu: ${st.data.message ?? `HTTP ${st.status}`}`, st.status === 401 ? 'ccx login' : 'confira a internet ou fale com um admin')
461
+ return finish()
462
+ }
463
+ ok('O app respondeu e a chave deste computador vale')
464
+
465
+ // Conta escolhida
466
+ if (!app.account) {
467
+ erro('Nenhuma conta escolhida neste computador', 'ccx')
468
+ return finish()
469
+ }
470
+ const hb = await api(app, key, 'POST', '/api/cli/heartbeat', { account_id: app.account.id }, 20_000)
471
+ if (hb.status !== 200 || !hb.data.allowed) erro(`A conta ${app.account.label} não está mais liberada pra você`, 'ccx (e escolha outra) ou peça a um admin')
472
+ else ok(`Conta escolhida: ${app.account.label}, liberada`)
473
+
474
+ // Token no settings.json do Claude Code
475
+ let settings = null
476
+ try { settings = readJson(SETTINGS, {}) }
477
+ catch { erro(`${SETTINGS} não é um JSON válido`, 'corrija o arquivo e rode ccx de novo') }
478
+ const token = settings?.env?.CLAUDE_CODE_OAUTH_TOKEN
479
+ if (process.env.CLAUDE_CONFIG_DIR) aviso(`CLAUDE_CONFIG_DIR=${process.env.CLAUDE_CONFIG_DIR}: o claude só usa a conta se também tiver essa variável`)
480
+ if (settings && !token) erro(`O token da conta não está em ${SETTINGS}`, 'ccx (escolha a conta de novo)')
481
+ else if (token && fingerprint(token) !== app.token_fp) erro('O token no settings.json não é o que o ccx gravou (alguém mexeu no arquivo)', 'ccx (escolha a conta de novo)')
482
+ else if (token && hb.data.token_fp && fingerprint(token) !== hb.data.token_fp) aviso('O admin trocou o token desta conta; o vigia atualiza em até 5 min', 'ou rode: ccx (escolha a conta de novo)')
483
+ else if (token) ok(`Token da conta ${app.account.label} gravado em ${SETTINGS}`)
484
+
485
+ // O que passa por cima do token
486
+ const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN
487
+ if (envToken && envToken !== token) erro('Este terminal tem outro CLAUDE_CODE_OAUTH_TOKEN, que pode passar por cima da conta do ccx', WIN ? 'apague a variável (comando abaixo) e abra um terminal novo' : 'tire o export do seu ~/.zshrc ou ~/.bashrc e abra um terminal novo')
488
+ if (WIN) {
489
+ const winToken = winUserEnv('CLAUDE_CODE_OAUTH_TOKEN')
490
+ if (winToken && winToken !== token) erro('O Windows tem um CLAUDE_CODE_OAUTH_TOKEN antigo guardado', `PowerShell: [Environment]::SetEnvironmentVariable('CLAUDE_CODE_OAUTH_TOKEN', $null, 'User') e abra um terminal novo`)
491
+ }
492
+ for (const v of AUTH_VARS) {
493
+ const val = process.env[v] || (WIN ? winUserEnv(v) : null) || settings?.env?.[v]
494
+ if (val) erro(`${v} está definida: ela passa por cima da conta do ccx`, `tire ${v} do terminal${WIN ? ', das variáveis do Windows' : ''} e do settings.json`)
495
+ }
496
+ if (settings?.apiKeyHelper) erro('O settings.json tem apiKeyHelper, que passa por cima da conta do ccx', `tire "apiKeyHelper" de ${SETTINGS}`)
497
+
498
+ // Settings de projeto (nesta pasta) e da empresa também podem trocar a conta
499
+ const overrides = [join(process.cwd(), '.claude', 'settings.json'), join(process.cwd(), '.claude', 'settings.local.json'), ...MANAGED]
500
+ for (const f of overrides) {
501
+ if (!existsSync(f) || f === SETTINGS) continue
502
+ let s
503
+ try { s = JSON.parse(readFileSync(f, 'utf8')) }
504
+ catch { continue }
505
+ const keys = ['CLAUDE_CODE_OAUTH_TOKEN', ...AUTH_VARS].filter(k => s?.env?.[k])
506
+ if (s?.apiKeyHelper) keys.push('apiKeyHelper')
507
+ if (keys.length) erro(`${f} define ${keys.join(', ')}: passa por cima da conta do ccx nesta pasta`, 'tire essas chaves do arquivo')
508
+ }
509
+ if (!bad) ok('Nada passando por cima da conta do ccx')
510
+
511
+ // Telemetria e vigia
512
+ const env = settings?.env ?? {}
513
+ if (env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT !== `${app.url}/api/cli/otel/v1/logs` || env.OTEL_EXPORTER_OTLP_LOGS_HEADERS !== `Authorization=Bearer ${key}`) {
514
+ aviso('A telemetria não está apontando pro app com a chave atual (os tokens não aparecem no painel)', 'ccx (escolha a conta de novo)')
515
+ }
516
+ else ok('Telemetria ligada (tokens vão pro painel)')
517
+ if (agentInstalled()) ok(`Vigia instalado (roda a cada ${AGENT_MINUTES} min)`)
518
+ else aviso('O vigia não está instalado: token novo e sync não chegam sozinhos', 'ccx (escolha a conta de novo)')
519
+ // Só o que deu errado nas últimas 24 h (o log tem a data no começo de cada linha)
520
+ const dayAgo = new Date(Date.now() - 86_400_000).toISOString()
521
+ const lastErr = existsSync(LOG) && readFileSync(LOG, 'utf8').trim().split('\n').filter(l => l >= dayAgo && /falhou|removid|vencid/.test(l)).pop()
522
+ if (lastErr) aviso(`Problema recente do vigia: ${lastErr}`)
523
+ const sync = existsSync(SYNC_STATE) && statSync(SYNC_STATE).mtime
524
+ if (sync) ok(`Último sync: ${sync.toLocaleString('pt-BR')}`)
525
+ else aviso('O sync nunca rodou neste computador', 'ccx sync')
526
+
527
+ return finish()
528
+
529
+ function finish() {
530
+ console.log(`\n${bad ? c(31, `${bad} problema(s)`) : c(32, 'Tudo certo')}${warn ? c(33, ` · ${warn} aviso(s)`) : ''}.`)
531
+ if (!bad && app?.account) console.log(c(90, `Todo claude aberto a partir de agora usa a conta ${app.account.label}. Os que já estavam abertos continuam na anterior até serem fechados.`))
532
+ process.exit(bad ? 1 : 0)
533
+ }
534
+ }
535
+
245
536
  // ---------- saída ----------
246
537
  const isTTY = process.stderr.isTTY
247
538
  const c = (code, s) => (isTTY ? `\x1b[${code}m${s}\x1b[0m` : s)
@@ -346,7 +637,7 @@ async function useAccount(app, key, wanted) {
346
637
  const [cmd, ...rest] = process.argv.slice(2)
347
638
 
348
639
  if (cmd === 'help' || cmd === '-h' || cmd === '--help') {
349
- console.log(readFileSync(SELF, 'utf8').split('\n').filter(l => l.startsWith('//')).slice(0, 15).map(l => l.slice(3)).join('\n'))
640
+ console.log(readFileSync(SELF, 'utf8').split('\n').filter(l => l.startsWith('//')).slice(0, 18).map(l => l.slice(3)).join('\n'))
350
641
  process.exit(0)
351
642
  }
352
643
 
@@ -360,6 +651,8 @@ if (cmd === 'vigia') {
360
651
  process.exit(0)
361
652
  }
362
653
 
654
+ if (cmd === 'doctor') await doctor()
655
+
363
656
  if (cmd === 'login') await login(rest[0] ?? readJson(APP, null)?.url ?? DEFAULT_URL)
364
657
 
365
658
  const app = readJson(APP, null)
@@ -381,6 +674,12 @@ if (cmd === 'sair') {
381
674
  process.exit(0)
382
675
  }
383
676
 
677
+ if (cmd === 'sync') {
678
+ try { await sync(app, key) }
679
+ catch (e) { die(`\nO sync falhou: ${e.message}`) }
680
+ process.exit(0)
681
+ }
682
+
384
683
  if (cmd === 'status') {
385
684
  const r = await api(app, key, 'GET', '/api/cli/status')
386
685
  if (r.status !== 200) die(r.data.message ?? `Falhou (HTTP ${r.status})`)
@@ -393,3 +692,10 @@ if (cmd === 'status') {
393
692
 
394
693
  // `ccx`, `ccx login ...` (depois de autorizar) ou `ccx <conta>`
395
694
  await useAccount(app, key, cmd && cmd !== 'login' ? cmd : await chooseAccount(app, key))
695
+
696
+ // Logo depois do login, manda o resumo das sessões (as próximas vão pelo vigia)
697
+ if (cmd === 'login') {
698
+ console.error('')
699
+ try { await sync(app, key) }
700
+ catch (e) { console.error(c(33, `\nO sync falhou (${e.message}). Tente: ccx sync`)) }
701
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectarcrm/ccx",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Escolhe qual conta Claude este computador usa, conectado ao app Contas Claude da Nectar.",
5
5
  "type": "module",
6
6
  "bin": { "ccx": "bin/ccx.js" },