@ancleto/spec 0.6.26 → 0.6.28
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/README.md +8 -0
- package/agents/documenter.md +7 -6
- package/agents/spec-writer.md +3 -2
- package/package.json +1 -1
- package/skills/ancleto-archive/SKILL.md +2 -0
- package/skills/ancleto-verify/SKILL.md +6 -0
- package/src/cli/index.js +319 -1
package/README.md
CHANGED
|
@@ -120,6 +120,8 @@ Cómo bajarlo:
|
|
|
120
120
|
- Evitá `--with-engram` salvo que uses esa memoria: es el que más pesa (9× la memoria propia).
|
|
121
121
|
- Podés deshabilitar cualquier servidor en tu `opencode.json`: `"mcp": { "engram": { "enabled": false } }`.
|
|
122
122
|
|
|
123
|
+
Para medir el consumo **real** por sesión (no estimado): `ancleto stats` lee la base de sesiones de opencode y reporta tokens de entrada/salida/razonamiento/cache, costo y subagentes por sesión. Con `--session <id>` desglosa por agente y con `--since YYYY-MM-DD` permite comparar antes/después de una optimización. Solo funciona con opencode.
|
|
124
|
+
|
|
123
125
|
---
|
|
124
126
|
|
|
125
127
|
## Guía de .gitignore
|
|
@@ -201,6 +203,12 @@ ancleto upgrade # Re-aplica templates y skills respetando tus personal
|
|
|
201
203
|
|
|
202
204
|
ancleto check # Verifica la integridad de la instalación (faltantes / huérfanos)
|
|
203
205
|
|
|
206
|
+
ancleto specs check [--change <nombre>] [--json]
|
|
207
|
+
# Valida keywords canónicos en aspec/specs y en los deltas de un change
|
|
208
|
+
|
|
209
|
+
ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
|
|
210
|
+
# Tokens por sesión de opencode (rollup de subagentes; --session desglosa por agente)
|
|
211
|
+
|
|
204
212
|
ancleto doctor # Diagnostica el entorno (Node, node:sqlite, opencode.json)
|
|
205
213
|
|
|
206
214
|
ancleto memory context [--scope <project|feature|task>] [--out <archivo>]
|
package/agents/documenter.md
CHANGED
|
@@ -105,12 +105,13 @@ Do not treat archive as a blind file move.
|
|
|
105
105
|
- `tasks.md`
|
|
106
106
|
- `specs/` when the change includes delta specs
|
|
107
107
|
3. Determine whether the change includes spec deltas that must be reflected in `aspec/specs/`
|
|
108
|
-
4.
|
|
109
|
-
5. If
|
|
110
|
-
6. If the change is ready
|
|
111
|
-
7.
|
|
112
|
-
8.
|
|
113
|
-
9.
|
|
108
|
+
4. When deltas exist, run `ancleto specs check --change {change-name} --json` before merging. If a delta spec or its target source-of-truth spec is non-canonical (translated keywords), stop and report it instead of merging into it
|
|
109
|
+
5. If needed, update the relevant source-of-truth specs so they reflect the completed change
|
|
110
|
+
6. If the change is not ready for archive, stop and report the inconsistency clearly
|
|
111
|
+
7. If the change is ready, archive it to `aspec/changes/archive/{YYYY-MM-DD}-{change-name}/`
|
|
112
|
+
8. Remove the original active change directory from `aspec/changes/{change-name}/` after a successful archive
|
|
113
|
+
9. If any files or folders remain in the original active change directory, report them as cleanup issues instead of leaving silent residue
|
|
114
|
+
10. Return a short structured summary of the finalization and archive result
|
|
114
115
|
|
|
115
116
|
## Source-of-Truth Rules
|
|
116
117
|
|
package/agents/spec-writer.md
CHANGED
|
@@ -44,8 +44,9 @@ using the `ancleto-workflow` skill and the conventions of:
|
|
|
44
44
|
- `design.md`
|
|
45
45
|
- `tasks.md`
|
|
46
46
|
- `specs/{capability}/spec.md` as needed
|
|
47
|
-
6.
|
|
48
|
-
7.
|
|
47
|
+
6. Before returning, verify every generated spec uses the canonical keywords (`### Requirement:`, `#### Scenario:`, `**WHEN**`/`**THEN**`, `SHALL`); fix any translated heading or keyword in place — never leave a spec with translated keywords.
|
|
48
|
+
7. When the delegation says the context was produced without a technical seed, declare that in the generated artifacts.
|
|
49
|
+
8. Return a brief summary with generated paths, scope, and open questions or risks
|
|
49
50
|
|
|
50
51
|
## Change Naming
|
|
51
52
|
|
package/package.json
CHANGED
|
@@ -31,6 +31,8 @@ Read `aspec/changes/<name>/tasks.md` (when present); count `- [ ]` (incomplete)
|
|
|
31
31
|
|
|
32
32
|
Check for delta specs at `aspec/changes/<name>/specs/`; if none, proceed without a sync prompt.
|
|
33
33
|
|
|
34
|
+
**Canonical keywords first:** when delta specs exist and the CLI is available, run `ancleto specs check --change <name> --json`. If a delta spec or its target source-of-truth spec is non-canonical (translated keywords), warn that requirement blocks cannot be matched reliably and ask whether to normalize the affected spec first or archive without syncing that capability. Never merge into a non-canonical spec silently.
|
|
35
|
+
|
|
34
36
|
**If delta specs exist:**
|
|
35
37
|
|
|
36
38
|
- Diff each delta against its main spec at `aspec/specs/<capability>/spec.md`; show a combined summary of adds/modifications/removals/renames.
|
|
@@ -26,6 +26,12 @@ Read under `aspec/changes/<name>/`: `tasks.md` (required), plus `proposal.md`, `
|
|
|
26
26
|
|
|
27
27
|
**Tasks:** parse checkboxes (`- [ ]` incomplete vs `- [x]` complete) and count. Each incomplete → CRITICAL: "Complete task: `<description>`" or "Mark as done if already implemented".
|
|
28
28
|
|
|
29
|
+
**Canonical spec keywords:** run `ancleto specs check --change <name> --json` when the CLI is available. For every reported file:
|
|
30
|
+
- Delta spec (`aspec/changes/<name>/specs/...`) → CRITICAL: "Non-canonical spec: `<file>` (missing: `<keywords>`)" + "Normalize `<file>` to the canonical English keywords before archiving".
|
|
31
|
+
- Source-of-truth spec (`aspec/specs/...`) → WARNING with the same recommendation.
|
|
32
|
+
|
|
33
|
+
A non-canonical spec cannot be matched for coverage or merged reliably, so report it before the checks below.
|
|
34
|
+
|
|
29
35
|
**Spec coverage:** for each `### Requirement:` in `specs/`, search the codebase for related keywords. Unimplemented → CRITICAL: "Requirement not found: `<requirement name>`" + "Implement requirement: `<description>`".
|
|
30
36
|
|
|
31
37
|
### 3. Correctness
|
package/src/cli/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import { cp, mkdir, access, writeFile, readFile, readdir, rename } from 'node:fs
|
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
6
|
import { createInterface } from 'node:readline'
|
|
7
|
-
import {
|
|
7
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
8
|
+
import { join, dirname, resolve, basename, relative } from 'node:path'
|
|
8
9
|
import { fileURLToPath } from 'node:url'
|
|
9
10
|
import { homedir, tmpdir } from 'node:os'
|
|
10
11
|
import { createMemoryEngine, createReadonlyMemoryEngine, defaultMemoryDbPath } from '../core/memory/engine.js'
|
|
@@ -40,6 +41,12 @@ Uso:
|
|
|
40
41
|
ancleto discovery --check Estado del seed (READY/STALE/PARTIAL/MISSING)
|
|
41
42
|
ancleto discovery [--compress] [--include G] [--ignore G] [--token-budget N]
|
|
42
43
|
Empaca el repo con Repomix y guarda estado
|
|
44
|
+
ancleto specs check [--change <name>] [--json]
|
|
45
|
+
Valida keywords canonicos (### Requirement:, WHEN, THEN)
|
|
46
|
+
en aspec/specs y, con --change, en los deltas del change
|
|
47
|
+
ancleto stats [--all] [--limit N] [--since YYYY-MM-DD] [--session <id>] [--json]
|
|
48
|
+
Tokens por sesion de opencode (entrada/salida/razonamiento/cache),
|
|
49
|
+
con rollup de subagentes y desglose por agente con --session
|
|
43
50
|
ancleto memory context [--scope X] [--out file]
|
|
44
51
|
Imprime/escribe el bloque <ProjectMemoryRules> (reglas activas)
|
|
45
52
|
ancleto memory list [--type X] [--scope X] [--all] [--json]
|
|
@@ -980,6 +987,311 @@ async function checkTierOrphan(cwd) {
|
|
|
980
987
|
return warnings
|
|
981
988
|
}
|
|
982
989
|
|
|
990
|
+
// Un spec es canonico cuando usa los keywords literales en ingles. Detecta specs
|
|
991
|
+
// traducidos (p. ej. "#### RF1:" + "**Dado**/**Cuando**/**Entonces**"), que rompen
|
|
992
|
+
// la cobertura por keywords y el merge de deltas.
|
|
993
|
+
function specIssues(content) {
|
|
994
|
+
const missing = []
|
|
995
|
+
if (!/^###\s+Requirement:/m.test(content)) missing.push('### Requirement:')
|
|
996
|
+
if (/^####\s+Scenario:/m.test(content)) {
|
|
997
|
+
if (!/\bWHEN\b/.test(content)) missing.push('WHEN')
|
|
998
|
+
if (!/\bTHEN\b/.test(content)) missing.push('THEN')
|
|
999
|
+
}
|
|
1000
|
+
const unexpected = []
|
|
1001
|
+
const re = /^####\s+(.+)$/gm
|
|
1002
|
+
let m
|
|
1003
|
+
while ((m = re.exec(content)) !== null) {
|
|
1004
|
+
if (!/^Scenario\b/.test(m[1])) unexpected.push(`#### ${m[1]}`)
|
|
1005
|
+
}
|
|
1006
|
+
return { missing, unexpected }
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function collectSpecFiles(dir) {
|
|
1010
|
+
const out = []
|
|
1011
|
+
if (!(await exists(dir))) return out
|
|
1012
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
1013
|
+
const p = join(dir, e.name)
|
|
1014
|
+
if (e.isDirectory()) out.push(...(await collectSpecFiles(p)))
|
|
1015
|
+
else if (e.name === 'spec.md') out.push(p)
|
|
1016
|
+
}
|
|
1017
|
+
return out
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
async function specsCheckCommand(args) {
|
|
1021
|
+
const cwd = process.cwd()
|
|
1022
|
+
const asJson = args.includes('--json')
|
|
1023
|
+
const ci = args.indexOf('--change')
|
|
1024
|
+
const change = ci >= 0 ? args[ci + 1] : null
|
|
1025
|
+
const files = await collectSpecFiles(join(cwd, CHANGES_ROOT, 'specs'))
|
|
1026
|
+
if (change) {
|
|
1027
|
+
const changeDir = join(cwd, CHANGES_ROOT, 'changes', change)
|
|
1028
|
+
if (!(await exists(changeDir))) {
|
|
1029
|
+
console.error(`ancleto: no existe el change "${change}" en ${CHANGES_ROOT}/changes/`)
|
|
1030
|
+
process.exit(1)
|
|
1031
|
+
}
|
|
1032
|
+
files.push(...(await collectSpecFiles(join(changeDir, 'specs'))))
|
|
1033
|
+
}
|
|
1034
|
+
const findings = []
|
|
1035
|
+
for (const f of files) {
|
|
1036
|
+
const { missing, unexpected } = specIssues(await readFile(f, 'utf8'))
|
|
1037
|
+
if (missing.length || unexpected.length) {
|
|
1038
|
+
findings.push({ file: relative(cwd, f).replace(/\\/g, '/'), missing, unexpected })
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const ok = findings.length === 0
|
|
1042
|
+
if (asJson) {
|
|
1043
|
+
console.log(JSON.stringify({ ok, scanned: files.length, nonCanonical: findings }, null, 2))
|
|
1044
|
+
} else if (files.length === 0) {
|
|
1045
|
+
console.log('ancleto: no hay specs para revisar')
|
|
1046
|
+
} else {
|
|
1047
|
+
for (const f of files) {
|
|
1048
|
+
const rel = relative(cwd, f).replace(/\\/g, '/')
|
|
1049
|
+
const found = findings.find((x) => x.file === rel)
|
|
1050
|
+
if (found) {
|
|
1051
|
+
console.log(` ✖ ${rel} (revisar: ${[...found.missing, ...found.unexpected].join(', ')})`)
|
|
1052
|
+
} else {
|
|
1053
|
+
console.log(` ✔ ${rel}`)
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
console.log(ok ? `ancleto: ${files.length} spec(s) canonicos` : `ancleto: ${findings.length} de ${files.length} spec(s) sin keywords canonicos`)
|
|
1057
|
+
}
|
|
1058
|
+
process.exit(ok ? 0 : 1)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function opencodeDbPath() {
|
|
1062
|
+
return process.env.ANCLETO_OPENCODE_DB || join(homedir(), '.local', 'share', 'opencode', 'opencode.db')
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function fmtTokens(n) {
|
|
1066
|
+
if (!n) return '0'
|
|
1067
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`
|
|
1068
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`
|
|
1069
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`
|
|
1070
|
+
return String(n)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function fmtCost(c) {
|
|
1074
|
+
return c && c < 0.01 ? `$${c.toFixed(4)}` : `$${(c || 0).toFixed(2)}`
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function fmtDate(ms) {
|
|
1078
|
+
const d = new Date(ms || 0)
|
|
1079
|
+
const p = (x) => String(x).padStart(2, '0')
|
|
1080
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const normPath = (p) => String(p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
|
|
1084
|
+
|
|
1085
|
+
function sessionModel(model) {
|
|
1086
|
+
try {
|
|
1087
|
+
const m = JSON.parse(model || '{}')
|
|
1088
|
+
return m.id ? `${m.id}${m.providerID ? ` (${m.providerID})` : ''}` : ''
|
|
1089
|
+
} catch {
|
|
1090
|
+
return ''
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function buildSessionTree(sessions) {
|
|
1095
|
+
const byId = new Map(sessions.map((s) => [s.id, s]))
|
|
1096
|
+
const children = new Map()
|
|
1097
|
+
for (const s of sessions) {
|
|
1098
|
+
if (!s.parent_id) continue
|
|
1099
|
+
if (!children.has(s.parent_id)) children.set(s.parent_id, [])
|
|
1100
|
+
children.get(s.parent_id).push(s)
|
|
1101
|
+
}
|
|
1102
|
+
const cache = new Map()
|
|
1103
|
+
const totalsOf = (id) => {
|
|
1104
|
+
if (cache.has(id)) return cache.get(id)
|
|
1105
|
+
const own = byId.get(id)
|
|
1106
|
+
const t = {
|
|
1107
|
+
input: own?.tokens_input || 0,
|
|
1108
|
+
output: own?.tokens_output || 0,
|
|
1109
|
+
reasoning: own?.tokens_reasoning || 0,
|
|
1110
|
+
cacheRead: own?.tokens_cache_read || 0,
|
|
1111
|
+
cacheWrite: own?.tokens_cache_write || 0,
|
|
1112
|
+
cost: own?.cost || 0,
|
|
1113
|
+
sessions: own ? 1 : 0
|
|
1114
|
+
}
|
|
1115
|
+
for (const c of children.get(id) || []) {
|
|
1116
|
+
const ct = totalsOf(c.id)
|
|
1117
|
+
t.input += ct.input
|
|
1118
|
+
t.output += ct.output
|
|
1119
|
+
t.reasoning += ct.reasoning
|
|
1120
|
+
t.cacheRead += ct.cacheRead
|
|
1121
|
+
t.cacheWrite += ct.cacheWrite
|
|
1122
|
+
t.cost += ct.cost
|
|
1123
|
+
t.sessions += ct.sessions
|
|
1124
|
+
}
|
|
1125
|
+
cache.set(id, t)
|
|
1126
|
+
return t
|
|
1127
|
+
}
|
|
1128
|
+
const subtreeIds = (id) => [id, ...(children.get(id) || []).flatMap((c) => subtreeIds(c.id))]
|
|
1129
|
+
return { byId, totalsOf, subtreeIds }
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function sessionInfo(s, totals) {
|
|
1133
|
+
return {
|
|
1134
|
+
id: s.id,
|
|
1135
|
+
title: s.title || null,
|
|
1136
|
+
agent: s.agent || null,
|
|
1137
|
+
model: sessionModel(s.model) || null,
|
|
1138
|
+
subagentSessions: Math.max(0, totals.sessions - 1),
|
|
1139
|
+
timeCreated: s.time_created || null,
|
|
1140
|
+
timeUpdated: s.time_updated || null,
|
|
1141
|
+
cost: totals.cost,
|
|
1142
|
+
tokens: {
|
|
1143
|
+
input: totals.input,
|
|
1144
|
+
output: totals.output,
|
|
1145
|
+
reasoning: totals.reasoning,
|
|
1146
|
+
cacheRead: totals.cacheRead,
|
|
1147
|
+
cacheWrite: totals.cacheWrite
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async function statsCommand(args) {
|
|
1153
|
+
const asJson = args.includes('--json')
|
|
1154
|
+
const all = args.includes('--all')
|
|
1155
|
+
const si = args.indexOf('--session')
|
|
1156
|
+
const sessionId = si >= 0 ? args[si + 1] : null
|
|
1157
|
+
const li = args.indexOf('--limit')
|
|
1158
|
+
const limit = li >= 0 ? Number.parseInt(args[li + 1], 10) : 10
|
|
1159
|
+
if (li >= 0 && (!Number.isFinite(limit) || limit < 1)) {
|
|
1160
|
+
console.error('ancleto: --limit requiere un entero >= 1')
|
|
1161
|
+
process.exit(1)
|
|
1162
|
+
}
|
|
1163
|
+
const sinceIdx = args.indexOf('--since')
|
|
1164
|
+
const sinceRaw = sinceIdx >= 0 ? args[sinceIdx + 1] : null
|
|
1165
|
+
let since = null
|
|
1166
|
+
if (sinceRaw) {
|
|
1167
|
+
const t = Date.parse(`${sinceRaw}T00:00:00`)
|
|
1168
|
+
if (Number.isNaN(t)) {
|
|
1169
|
+
console.error('ancleto: --since requiere formato YYYY-MM-DD')
|
|
1170
|
+
process.exit(1)
|
|
1171
|
+
}
|
|
1172
|
+
since = t
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const dbPath = opencodeDbPath()
|
|
1176
|
+
if (!(await exists(dbPath))) {
|
|
1177
|
+
console.error(`ancleto: no se encontro la base de sesiones de opencode en ${dbPath}`)
|
|
1178
|
+
console.error('ancleto: ancleto stats solo funciona con opencode (podes apuntar ANCLETO_OPENCODE_DB a la base)')
|
|
1179
|
+
process.exit(1)
|
|
1180
|
+
}
|
|
1181
|
+
const db = new DatabaseSync(dbPath, { readOnly: true })
|
|
1182
|
+
let allSessions
|
|
1183
|
+
try {
|
|
1184
|
+
allSessions = db.prepare('SELECT * FROM session').all()
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
console.error(`ancleto: no se pudo leer la base de sesiones: ${err.message}`)
|
|
1187
|
+
process.exit(1)
|
|
1188
|
+
}
|
|
1189
|
+
const tree = buildSessionTree(allSessions)
|
|
1190
|
+
const cwd = process.cwd()
|
|
1191
|
+
|
|
1192
|
+
if (sessionId) {
|
|
1193
|
+
const root = tree.byId.get(sessionId)
|
|
1194
|
+
if (!root) {
|
|
1195
|
+
console.error(`ancleto: no existe la sesion "${sessionId}"`)
|
|
1196
|
+
process.exit(1)
|
|
1197
|
+
}
|
|
1198
|
+
const ids = tree.subtreeIds(sessionId)
|
|
1199
|
+
let messages = []
|
|
1200
|
+
try {
|
|
1201
|
+
messages = db.prepare(`SELECT data FROM message WHERE session_id IN (${ids.map(() => '?').join(',')})`).all(...ids)
|
|
1202
|
+
} catch {
|
|
1203
|
+
messages = []
|
|
1204
|
+
}
|
|
1205
|
+
const byAgent = new Map()
|
|
1206
|
+
for (const row of messages) {
|
|
1207
|
+
let d
|
|
1208
|
+
try {
|
|
1209
|
+
d = JSON.parse(row.data)
|
|
1210
|
+
} catch {
|
|
1211
|
+
continue
|
|
1212
|
+
}
|
|
1213
|
+
if (d.role !== 'assistant' || !d.tokens) continue
|
|
1214
|
+
const key = d.agent || '(desconocido)'
|
|
1215
|
+
const agg = byAgent.get(key) || { agent: key, messages: 0, input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 }
|
|
1216
|
+
agg.messages++
|
|
1217
|
+
agg.input += d.tokens.input || 0
|
|
1218
|
+
agg.output += d.tokens.output || 0
|
|
1219
|
+
agg.reasoning += d.tokens.reasoning || 0
|
|
1220
|
+
agg.cacheRead += d.tokens.cache?.read || 0
|
|
1221
|
+
agg.cacheWrite += d.tokens.cache?.write || 0
|
|
1222
|
+
byAgent.set(key, agg)
|
|
1223
|
+
}
|
|
1224
|
+
const agents = [...byAgent.values()].sort((a, b) => b.input - a.input)
|
|
1225
|
+
const totals = tree.totalsOf(sessionId)
|
|
1226
|
+
db.close()
|
|
1227
|
+
if (asJson) {
|
|
1228
|
+
console.log(JSON.stringify({ ok: true, scope: 'session', session: sessionInfo(root, totals), byAgent: agents, totals }, null, 2))
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1231
|
+
console.log(`ancleto: sesion ${root.id}`)
|
|
1232
|
+
console.log(` titulo: ${root.title || '(sin titulo)'}`)
|
|
1233
|
+
console.log(` agente: ${root.agent || '-'} · modelo: ${sessionModel(root.model) || '-'}`)
|
|
1234
|
+
console.log(` creada: ${fmtDate(root.time_created)} · ultima actividad: ${fmtDate(root.time_updated)}`)
|
|
1235
|
+
console.log(` sesiones incluidas: ${totals.sessions} (raiz + subagentes)`)
|
|
1236
|
+
console.log('')
|
|
1237
|
+
console.log(' agente mensajes entrada salida razon cache-read')
|
|
1238
|
+
for (const a of agents) {
|
|
1239
|
+
console.log(` ${String(a.agent).padEnd(13)} ${String(a.messages).padEnd(10)} ${fmtTokens(a.input).padEnd(9)} ${fmtTokens(a.output).padEnd(9)} ${fmtTokens(a.reasoning).padEnd(9)} ${fmtTokens(a.cacheRead)}`)
|
|
1240
|
+
}
|
|
1241
|
+
console.log('')
|
|
1242
|
+
console.log(` totales: entrada ${fmtTokens(totals.input)} · salida ${fmtTokens(totals.output)} · razon ${fmtTokens(totals.reasoning)} · cache ${fmtTokens(totals.cacheRead)} · costo ${fmtCost(totals.cost)}`)
|
|
1243
|
+
return
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
let roots = allSessions.filter((s) => !s.parent_id)
|
|
1247
|
+
let scope = 'all'
|
|
1248
|
+
if (!all) {
|
|
1249
|
+
scope = 'directory'
|
|
1250
|
+
const cwdN = normPath(cwd)
|
|
1251
|
+
const inDir = (d) => {
|
|
1252
|
+
const dn = normPath(d)
|
|
1253
|
+
return dn === cwdN || dn.startsWith(`${cwdN}/`) || cwdN.startsWith(`${dn}/`)
|
|
1254
|
+
}
|
|
1255
|
+
roots = roots.filter((s) => inDir(s.directory))
|
|
1256
|
+
}
|
|
1257
|
+
if (since !== null) roots = roots.filter((s) => (s.time_created || 0) >= since)
|
|
1258
|
+
roots.sort((a, b) => (b.time_updated || 0) - (a.time_updated || 0))
|
|
1259
|
+
const shown = roots.slice(0, limit)
|
|
1260
|
+
const rows = shown.map((s) => ({ ...sessionInfo(s, tree.totalsOf(s.id)), directory: s.directory }))
|
|
1261
|
+
const totals = rows.reduce(
|
|
1262
|
+
(acc, r) => {
|
|
1263
|
+
acc.input += r.tokens.input
|
|
1264
|
+
acc.output += r.tokens.output
|
|
1265
|
+
acc.reasoning += r.tokens.reasoning
|
|
1266
|
+
acc.cacheRead += r.tokens.cacheRead
|
|
1267
|
+
acc.cacheWrite += r.tokens.cacheWrite
|
|
1268
|
+
acc.cost += r.cost
|
|
1269
|
+
return acc
|
|
1270
|
+
},
|
|
1271
|
+
{ input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }
|
|
1272
|
+
)
|
|
1273
|
+
db.close()
|
|
1274
|
+
|
|
1275
|
+
if (asJson) {
|
|
1276
|
+
console.log(JSON.stringify({ ok: true, scope, directory: scope === 'directory' ? cwd.replace(/\\/g, '/') : null, since: sinceRaw || null, sessions: rows, totals }, null, 2))
|
|
1277
|
+
return
|
|
1278
|
+
}
|
|
1279
|
+
if (rows.length === 0) {
|
|
1280
|
+
console.log(`ancleto: no hay sesiones${scope === 'directory' ? ' para este directorio' : ''}${sinceRaw ? ` desde ${sinceRaw}` : ''}`)
|
|
1281
|
+
if (scope === 'directory') console.log('ancleto: proba --all para ver todas las sesiones de la maquina')
|
|
1282
|
+
return
|
|
1283
|
+
}
|
|
1284
|
+
console.log(`ancleto: stats (opencode)${scope === 'directory' ? ` — ${cwd.replace(/\\/g, '/')}` : ' — todas las sesiones'}${sinceRaw ? ` — desde ${sinceRaw}` : ''}`)
|
|
1285
|
+
console.log('')
|
|
1286
|
+
console.log(' fecha agente entrada salida razon cache costo titulo')
|
|
1287
|
+
for (const r of rows) {
|
|
1288
|
+
console.log(` ${fmtDate(r.timeUpdated).padEnd(18)} ${String(r.agent || '-').padEnd(13)} ${fmtTokens(r.tokens.input).padEnd(9)} ${fmtTokens(r.tokens.output).padEnd(9)} ${fmtTokens(r.tokens.reasoning).padEnd(9)} ${fmtTokens(r.tokens.cacheRead).padEnd(9)} ${fmtCost(r.cost).padEnd(8)} ${String(r.title || '').slice(0, 42)}`)
|
|
1289
|
+
}
|
|
1290
|
+
console.log('')
|
|
1291
|
+
const shownLabel = roots.length > rows.length ? `${rows.length} de ${roots.length} sesiones` : rows.length === 1 ? '1 sesion' : `${rows.length} sesiones`
|
|
1292
|
+
console.log(` totales (${shownLabel}): entrada ${fmtTokens(totals.input)} · salida ${fmtTokens(totals.output)} · razon ${fmtTokens(totals.reasoning)} · cache ${fmtTokens(totals.cacheRead)} · costo ${fmtCost(totals.cost)}`)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
983
1295
|
async function doctorCommand() {
|
|
984
1296
|
let fatal = false
|
|
985
1297
|
|
|
@@ -1045,6 +1357,12 @@ switch (cmd) {
|
|
|
1045
1357
|
case 'check':
|
|
1046
1358
|
await checkCommand()
|
|
1047
1359
|
break
|
|
1360
|
+
case 'specs':
|
|
1361
|
+
await specsCheckCommand(rest)
|
|
1362
|
+
break
|
|
1363
|
+
case 'stats':
|
|
1364
|
+
await statsCommand(rest)
|
|
1365
|
+
break
|
|
1048
1366
|
case 'doctor':
|
|
1049
1367
|
await doctorCommand()
|
|
1050
1368
|
break
|