byterover-cli 3.5.1 → 3.6.1
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/.env.production +4 -6
- package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/cipher-agent.d.ts +1 -0
- package/dist/agent/infra/agent/cipher-agent.js +1 -0
- package/dist/oclif/commands/curate/view.js +5 -25
- package/dist/oclif/commands/dream.d.ts +18 -0
- package/dist/oclif/commands/dream.js +230 -0
- package/dist/oclif/commands/query-log/summary.d.ts +18 -0
- package/dist/oclif/commands/query-log/summary.js +75 -0
- package/dist/oclif/commands/query-log/view.d.ts +23 -0
- package/dist/oclif/commands/query-log/view.js +95 -0
- package/dist/oclif/lib/time-filter.d.ts +10 -0
- package/dist/oclif/lib/time-filter.js +21 -0
- package/dist/server/config/environment.d.ts +10 -3
- package/dist/server/config/environment.js +34 -15
- package/dist/server/constants.d.ts +5 -0
- package/dist/server/constants.js +7 -0
- package/dist/server/core/domain/entities/query-log-entry.d.ts +61 -0
- package/dist/server/core/domain/entities/query-log-entry.js +40 -0
- package/dist/server/core/domain/transport/schemas.d.ts +108 -7
- package/dist/server/core/domain/transport/schemas.js +34 -2
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +23 -2
- package/dist/server/core/interfaces/i-terminal.d.ts +3 -0
- package/dist/server/core/interfaces/i-terminal.js +1 -0
- package/dist/server/core/interfaces/storage/i-query-log-store.d.ts +23 -0
- package/dist/server/core/interfaces/storage/i-query-log-store.js +2 -0
- package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.d.ts +44 -0
- package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.js +1 -0
- package/dist/server/core/interfaces/usecase/i-query-log-use-case.d.ts +13 -0
- package/dist/server/core/interfaces/usecase/i-query-log-use-case.js +3 -0
- package/dist/server/infra/daemon/agent-process.js +79 -9
- package/dist/server/infra/daemon/brv-server.js +74 -5
- package/dist/server/infra/dream/dream-lock-service.d.ts +37 -0
- package/dist/server/infra/dream/dream-lock-service.js +88 -0
- package/dist/server/infra/dream/dream-log-schema.d.ts +966 -0
- package/dist/server/infra/dream/dream-log-schema.js +57 -0
- package/dist/server/infra/dream/dream-log-store.d.ts +55 -0
- package/dist/server/infra/dream/dream-log-store.js +141 -0
- package/dist/server/infra/dream/dream-response-schemas.d.ts +219 -0
- package/dist/server/infra/dream/dream-response-schemas.js +38 -0
- package/dist/server/infra/dream/dream-state-schema.d.ts +67 -0
- package/dist/server/infra/dream/dream-state-schema.js +23 -0
- package/dist/server/infra/dream/dream-state-service.d.ts +38 -0
- package/dist/server/infra/dream/dream-state-service.js +91 -0
- package/dist/server/infra/dream/dream-trigger.d.ts +46 -0
- package/dist/server/infra/dream/dream-trigger.js +65 -0
- package/dist/server/infra/dream/dream-undo.d.ts +38 -0
- package/dist/server/infra/dream/dream-undo.js +293 -0
- package/dist/server/infra/dream/operations/consolidate.d.ts +52 -0
- package/dist/server/infra/dream/operations/consolidate.js +514 -0
- package/dist/server/infra/dream/operations/prune.d.ts +45 -0
- package/dist/server/infra/dream/operations/prune.js +362 -0
- package/dist/server/infra/dream/operations/synthesize.d.ts +37 -0
- package/dist/server/infra/dream/operations/synthesize.js +278 -0
- package/dist/server/infra/dream/parse-dream-response.d.ts +11 -0
- package/dist/server/infra/dream/parse-dream-response.js +35 -0
- package/dist/server/infra/executor/curate-executor.js +10 -0
- package/dist/server/infra/executor/dream-executor.d.ts +97 -0
- package/dist/server/infra/executor/dream-executor.js +431 -0
- package/dist/server/infra/executor/query-executor.d.ts +2 -2
- package/dist/server/infra/executor/query-executor.js +92 -22
- package/dist/server/infra/process/feature-handlers.js +10 -6
- package/dist/server/infra/process/query-log-handler.d.ts +42 -0
- package/dist/server/infra/process/query-log-handler.js +150 -0
- package/dist/server/infra/process/task-router.d.ts +40 -0
- package/dist/server/infra/process/task-router.js +67 -9
- package/dist/server/infra/process/transport-handlers.d.ts +4 -0
- package/dist/server/infra/process/transport-handlers.js +1 -0
- package/dist/server/infra/storage/file-curate-log-store.js +1 -1
- package/dist/server/infra/storage/file-query-log-store.d.ts +81 -0
- package/dist/server/infra/storage/file-query-log-store.js +249 -0
- package/dist/server/infra/transport/handlers/config-handler.js +1 -1
- package/dist/server/infra/usecase/curate-log-use-case.js +7 -3
- package/dist/server/infra/usecase/query-log-summary-narrative-formatter.d.ts +15 -0
- package/dist/server/infra/usecase/query-log-summary-narrative-formatter.js +79 -0
- package/dist/server/infra/usecase/query-log-summary-use-case.d.ts +13 -0
- package/dist/server/infra/usecase/query-log-summary-use-case.js +217 -0
- package/dist/server/infra/usecase/query-log-use-case.d.ts +31 -0
- package/dist/server/infra/usecase/query-log-use-case.js +128 -0
- package/dist/server/utils/log-format-utils.d.ts +5 -0
- package/dist/server/utils/log-format-utils.js +23 -0
- package/dist/shared/transport/events/config-events.d.ts +1 -1
- package/oclif.manifest.json +510 -255
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { CACHE_TIERS, emptyByTier, QUERY_LOG_TIER_SHORT_LABELS, QUERY_LOG_TIERS, } from '../../core/domain/entities/query-log-entry.js';
|
|
2
|
+
import { describePeriod, formatDurationMs, formatQueryLogSummaryNarrative } from './query-log-summary-narrative-formatter.js';
|
|
3
|
+
const TOP_LIMIT = 10;
|
|
4
|
+
const MAX_EXAMPLE_QUERIES = 3;
|
|
5
|
+
const MIN_KEYWORD_LENGTH = 3;
|
|
6
|
+
const STOPWORDS = new Set([
|
|
7
|
+
'about',
|
|
8
|
+
'all',
|
|
9
|
+
'and',
|
|
10
|
+
'any',
|
|
11
|
+
'are',
|
|
12
|
+
'but',
|
|
13
|
+
'can',
|
|
14
|
+
'did',
|
|
15
|
+
'does',
|
|
16
|
+
'for',
|
|
17
|
+
'from',
|
|
18
|
+
'get',
|
|
19
|
+
'had',
|
|
20
|
+
'has',
|
|
21
|
+
'have',
|
|
22
|
+
'how',
|
|
23
|
+
'into',
|
|
24
|
+
'just',
|
|
25
|
+
'like',
|
|
26
|
+
'not',
|
|
27
|
+
'our',
|
|
28
|
+
'set',
|
|
29
|
+
'that',
|
|
30
|
+
'the',
|
|
31
|
+
'this',
|
|
32
|
+
'use',
|
|
33
|
+
'using',
|
|
34
|
+
'was',
|
|
35
|
+
'were',
|
|
36
|
+
'what',
|
|
37
|
+
'when',
|
|
38
|
+
'where',
|
|
39
|
+
'which',
|
|
40
|
+
'who',
|
|
41
|
+
'why',
|
|
42
|
+
'with',
|
|
43
|
+
'you',
|
|
44
|
+
'your',
|
|
45
|
+
]);
|
|
46
|
+
export class QueryLogSummaryUseCase {
|
|
47
|
+
deps;
|
|
48
|
+
constructor(deps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
}
|
|
51
|
+
async run(options) {
|
|
52
|
+
const entries = await this.deps.queryLogStore.list({ after: options.after, before: options.before });
|
|
53
|
+
const summary = computeSummary(entries, { after: options.after, before: options.before });
|
|
54
|
+
const format = options.format ?? 'text';
|
|
55
|
+
if (format === 'narrative') {
|
|
56
|
+
this.deps.terminal.log(formatQueryLogSummaryNarrative(summary));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (format === 'json') {
|
|
60
|
+
this.deps.terminal.log(JSON.stringify(summary, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.deps.terminal.log(formatSummaryText(summary));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ── Aggregation ─────────────────────────────────────────────────────────────
|
|
67
|
+
function computeSummary(entries, range) {
|
|
68
|
+
const summary = {
|
|
69
|
+
byStatus: { cancelled: 0, completed: 0, error: 0 },
|
|
70
|
+
byTier: emptyByTier(),
|
|
71
|
+
cacheHitRate: 0,
|
|
72
|
+
coverageRate: 0,
|
|
73
|
+
knowledgeGaps: [],
|
|
74
|
+
period: { from: range.after ?? 0, to: range.before ?? 0 },
|
|
75
|
+
queriesWithoutMatches: 0,
|
|
76
|
+
responseTime: { avgMs: 0, p50Ms: 0, p95Ms: 0 },
|
|
77
|
+
topRecalledDocs: [],
|
|
78
|
+
topTopics: [],
|
|
79
|
+
totalMatchedDocs: 0,
|
|
80
|
+
totalQueries: 0,
|
|
81
|
+
};
|
|
82
|
+
if (entries.length === 0) {
|
|
83
|
+
return summary;
|
|
84
|
+
}
|
|
85
|
+
const durations = [];
|
|
86
|
+
const topicCounts = new Map();
|
|
87
|
+
const docCounts = new Map();
|
|
88
|
+
const gapBuckets = new Map();
|
|
89
|
+
let completedWithMatches = 0;
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.status === 'processing')
|
|
92
|
+
continue;
|
|
93
|
+
summary.totalQueries += 1;
|
|
94
|
+
summary.byStatus[entry.status] += 1;
|
|
95
|
+
if (entry.status !== 'completed')
|
|
96
|
+
continue;
|
|
97
|
+
// ── completed-only aggregations ──
|
|
98
|
+
if (entry.timing) {
|
|
99
|
+
durations.push(entry.timing.durationMs);
|
|
100
|
+
}
|
|
101
|
+
if (entry.tier === undefined) {
|
|
102
|
+
summary.byTier.unknown += 1;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
summary.byTier[`tier${entry.tier}`] += 1;
|
|
106
|
+
}
|
|
107
|
+
summary.totalMatchedDocs += entry.matchedDocs.length;
|
|
108
|
+
if (entry.matchedDocs.length > 0) {
|
|
109
|
+
completedWithMatches += 1;
|
|
110
|
+
for (const doc of entry.matchedDocs) {
|
|
111
|
+
const topic = doc.path.split('/')[0];
|
|
112
|
+
topicCounts.set(topic, (topicCounts.get(topic) ?? 0) + 1);
|
|
113
|
+
docCounts.set(doc.path, (docCounts.get(doc.path) ?? 0) + 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
collectGapKeywords(entry.query, gapBuckets);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (summary.byStatus.completed > 0) {
|
|
121
|
+
const cacheHits = CACHE_TIERS.reduce((sum, t) => sum + summary.byTier[`tier${t}`], 0);
|
|
122
|
+
summary.cacheHitRate = cacheHits / summary.byStatus.completed;
|
|
123
|
+
summary.coverageRate = completedWithMatches / summary.byStatus.completed;
|
|
124
|
+
summary.queriesWithoutMatches = summary.byStatus.completed - completedWithMatches;
|
|
125
|
+
}
|
|
126
|
+
summary.responseTime = computeResponseTime(durations);
|
|
127
|
+
summary.topTopics = sortAndLimitCounts(topicCounts, 'topic');
|
|
128
|
+
summary.topRecalledDocs = sortAndLimitCounts(docCounts, 'path');
|
|
129
|
+
summary.knowledgeGaps = sortAndLimitGaps(gapBuckets);
|
|
130
|
+
return summary;
|
|
131
|
+
}
|
|
132
|
+
function computeResponseTime(durations) {
|
|
133
|
+
if (durations.length === 0) {
|
|
134
|
+
return { avgMs: 0, p50Ms: 0, p95Ms: 0 };
|
|
135
|
+
}
|
|
136
|
+
const sorted = [...durations].sort((a, b) => a - b);
|
|
137
|
+
const sum = sorted.reduce((acc, v) => acc + v, 0);
|
|
138
|
+
return {
|
|
139
|
+
avgMs: Math.round(sum / sorted.length),
|
|
140
|
+
p50Ms: sorted[Math.ceil(sorted.length * 0.5) - 1],
|
|
141
|
+
p95Ms: sorted[Math.ceil(sorted.length * 0.95) - 1],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function sortAndLimitCounts(counts, key) {
|
|
145
|
+
return [...counts.entries()]
|
|
146
|
+
.map(([value, count]) => ({ count, [key]: value }))
|
|
147
|
+
.sort((a, b) => b.count - a.count || a[key].localeCompare(b[key]))
|
|
148
|
+
.slice(0, TOP_LIMIT);
|
|
149
|
+
}
|
|
150
|
+
function sortAndLimitGaps(buckets) {
|
|
151
|
+
return [...buckets.entries()]
|
|
152
|
+
.map(([topic, { count, exampleQueries }]) => ({ count, exampleQueries, topic }))
|
|
153
|
+
.sort((a, b) => b.count - a.count || a.topic.localeCompare(b.topic))
|
|
154
|
+
.slice(0, TOP_LIMIT);
|
|
155
|
+
}
|
|
156
|
+
function collectGapKeywords(query, buckets) {
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
for (const token of query.toLowerCase().split(/[^a-z0-9]+/)) {
|
|
159
|
+
if (token.length < MIN_KEYWORD_LENGTH || STOPWORDS.has(token) || seen.has(token))
|
|
160
|
+
continue;
|
|
161
|
+
seen.add(token);
|
|
162
|
+
const bucket = buckets.get(token) ?? { count: 0, exampleQueries: [] };
|
|
163
|
+
bucket.count += 1;
|
|
164
|
+
if (bucket.exampleQueries.length < MAX_EXAMPLE_QUERIES) {
|
|
165
|
+
bucket.exampleQueries.push(query);
|
|
166
|
+
}
|
|
167
|
+
buckets.set(token, bucket);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ── Text formatting ─────────────────────────────────────────────────────────
|
|
171
|
+
function formatSummaryText(summary) {
|
|
172
|
+
const periodLabel = describePeriod(summary.period, 'short');
|
|
173
|
+
const header = periodLabel ? `Query Recall Summary (${periodLabel})` : 'Query Recall Summary';
|
|
174
|
+
if (summary.totalQueries === 0) {
|
|
175
|
+
return `${header}\n(no entries yet)`;
|
|
176
|
+
}
|
|
177
|
+
const cacheHits = CACHE_TIERS.reduce((sum, t) => sum + summary.byTier[`tier${t}`], 0);
|
|
178
|
+
const cachePct = Math.round(summary.cacheHitRate * 100);
|
|
179
|
+
const coveragePct = Math.round(summary.coverageRate * 100);
|
|
180
|
+
const matchedCount = summary.byStatus.completed - summary.queriesWithoutMatches;
|
|
181
|
+
const lines = [
|
|
182
|
+
header,
|
|
183
|
+
'='.repeat(header.length),
|
|
184
|
+
`Total queries: ${summary.totalQueries}`,
|
|
185
|
+
` Completed: ${summary.byStatus.completed}`,
|
|
186
|
+
` Failed: ${summary.byStatus.error}`,
|
|
187
|
+
` Cancelled: ${summary.byStatus.cancelled}`,
|
|
188
|
+
'',
|
|
189
|
+
`Cache hit rate: ${cachePct}% (${cacheHits}/${summary.byStatus.completed})`,
|
|
190
|
+
];
|
|
191
|
+
const maxTierLen = Math.max(...QUERY_LOG_TIERS.map((t) => `Tier ${t} (${QUERY_LOG_TIER_SHORT_LABELS[t]}):`.length));
|
|
192
|
+
for (const t of QUERY_LOG_TIERS) {
|
|
193
|
+
const label = `Tier ${t} (${QUERY_LOG_TIER_SHORT_LABELS[t]}):`;
|
|
194
|
+
lines.push(` ${label.padEnd(maxTierLen)} ${summary.byTier[`tier${t}`]}`);
|
|
195
|
+
}
|
|
196
|
+
lines.push('', 'Response time:', ` Average: ${formatDurationMs(summary.responseTime.avgMs)}`, ` p50: ${formatDurationMs(summary.responseTime.p50Ms)}`, ` p95: ${formatDurationMs(summary.responseTime.p95Ms)}`, '', `Knowledge coverage: ${coveragePct}% (${matchedCount}/${summary.byStatus.completed} queries had relevant results)`);
|
|
197
|
+
if (summary.topTopics.length > 0) {
|
|
198
|
+
lines.push('', 'Top queried topics:');
|
|
199
|
+
for (const [i, t] of summary.topTopics.entries()) {
|
|
200
|
+
lines.push(` ${i + 1}. ${t.topic} — ${t.count} queries`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (summary.topRecalledDocs.length > 0) {
|
|
204
|
+
lines.push('', 'Top recalled documents:');
|
|
205
|
+
for (const [i, d] of summary.topRecalledDocs.entries()) {
|
|
206
|
+
lines.push(` ${i + 1}. ${d.path} — ${d.count} queries`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (summary.knowledgeGaps.length > 0) {
|
|
210
|
+
lines.push('', 'Knowledge gaps (asked but unanswered):');
|
|
211
|
+
for (const [i, g] of summary.knowledgeGaps.entries()) {
|
|
212
|
+
lines.push(` ${i + 1}. "${g.topic}" — ${g.count} unanswered queries`);
|
|
213
|
+
}
|
|
214
|
+
lines.push(" → Run 'brv curate' on these topics to close the gap");
|
|
215
|
+
}
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ITerminal } from '../../core/interfaces/i-terminal.js';
|
|
2
|
+
import type { IQueryLogStore, QueryLogStatus, QueryLogTier } from '../../core/interfaces/storage/i-query-log-store.js';
|
|
3
|
+
import type { IQueryLogUseCase } from '../../core/interfaces/usecase/i-query-log-use-case.js';
|
|
4
|
+
type QueryLogUseCaseDeps = {
|
|
5
|
+
queryLogStore: IQueryLogStore;
|
|
6
|
+
terminal: ITerminal;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Use case for displaying query log entries.
|
|
10
|
+
*
|
|
11
|
+
* Reads directly from FileQueryLogStore — no daemon connection required.
|
|
12
|
+
*/
|
|
13
|
+
export declare class QueryLogUseCase implements IQueryLogUseCase {
|
|
14
|
+
private readonly deps;
|
|
15
|
+
constructor(deps: QueryLogUseCaseDeps);
|
|
16
|
+
run({ after, before, detail, format, id, limit, status, tier, }: {
|
|
17
|
+
after?: number;
|
|
18
|
+
before?: number;
|
|
19
|
+
detail?: boolean;
|
|
20
|
+
format?: 'json' | 'text';
|
|
21
|
+
id?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
status?: QueryLogStatus[];
|
|
24
|
+
tier?: QueryLogTier[];
|
|
25
|
+
}): Promise<void>;
|
|
26
|
+
private log;
|
|
27
|
+
private logJson;
|
|
28
|
+
private showDetail;
|
|
29
|
+
private showList;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { QUERY_LOG_TIER_LABELS } from '../../core/domain/entities/query-log-entry.js';
|
|
2
|
+
import { formatEntryDuration, formatTimestamp, truncate } from '../../utils/log-format-utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Use case for displaying query log entries.
|
|
5
|
+
*
|
|
6
|
+
* Reads directly from FileQueryLogStore — no daemon connection required.
|
|
7
|
+
*/
|
|
8
|
+
export class QueryLogUseCase {
|
|
9
|
+
deps;
|
|
10
|
+
constructor(deps) {
|
|
11
|
+
this.deps = deps;
|
|
12
|
+
}
|
|
13
|
+
async run({ after, before, detail = false, format = 'text', id, limit = 10, status, tier, }) {
|
|
14
|
+
await (id ? this.showDetail(id, format) : this.showList({ after, before, detail, format, limit, status, tier }));
|
|
15
|
+
}
|
|
16
|
+
log(msg) {
|
|
17
|
+
this.deps.terminal.log(msg);
|
|
18
|
+
}
|
|
19
|
+
logJson(payload) {
|
|
20
|
+
this.log(JSON.stringify({ command: 'query view', ...payload, retrievedAt: new Date().toISOString() }, null, 2));
|
|
21
|
+
}
|
|
22
|
+
async showDetail(id, format) {
|
|
23
|
+
const entry = await this.deps.queryLogStore.getById(id);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
if (format === 'json') {
|
|
26
|
+
this.logJson({ data: { error: `Log entry not found: ${id}` }, success: false });
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.log(`No query log entry found with ID: ${id}`);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (format === 'json') {
|
|
34
|
+
this.logJson({ data: entry, success: true });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.log(`ID: ${entry.id}`);
|
|
38
|
+
this.log(`Status: ${entry.status}`);
|
|
39
|
+
this.log(`Tier: ${entry.tier === undefined ? '—' : `${entry.tier} (${QUERY_LOG_TIER_LABELS[entry.tier]})`}`);
|
|
40
|
+
this.log(`Started: ${formatTimestamp(entry.startedAt)}`);
|
|
41
|
+
if (entry.status !== 'processing') {
|
|
42
|
+
this.log(`Finished: ${formatTimestamp(entry.completedAt)}`);
|
|
43
|
+
this.log(`Duration: ${formatEntryDuration(entry)}`);
|
|
44
|
+
}
|
|
45
|
+
this.log();
|
|
46
|
+
this.log(`Query: ${entry.query}`);
|
|
47
|
+
if (entry.matchedDocs.length > 0) {
|
|
48
|
+
this.log();
|
|
49
|
+
this.log('Matched Documents:');
|
|
50
|
+
for (const doc of entry.matchedDocs) {
|
|
51
|
+
this.log(` [${doc.score.toFixed(2)}] ${doc.path}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (entry.searchMetadata) {
|
|
55
|
+
this.log();
|
|
56
|
+
this.log('Search Metadata:');
|
|
57
|
+
this.log(` Results: ${entry.searchMetadata.resultCount} of ${entry.searchMetadata.totalFound} found`);
|
|
58
|
+
this.log(` Top Score: ${entry.searchMetadata.topScore}`);
|
|
59
|
+
if (entry.searchMetadata.cacheFingerprint) {
|
|
60
|
+
this.log(` Cache Fingerprint: ${entry.searchMetadata.cacheFingerprint}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (entry.status === 'error') {
|
|
64
|
+
this.log();
|
|
65
|
+
this.log(`Error: ${entry.error}`);
|
|
66
|
+
}
|
|
67
|
+
if (entry.status === 'completed' && entry.response) {
|
|
68
|
+
this.log();
|
|
69
|
+
this.log('Response:');
|
|
70
|
+
this.log(entry.response.split('\n').map((line) => ` ${line}`).join('\n'));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async showList({ after, before, detail, format, limit, status, tier }) {
|
|
74
|
+
const hasFilters = Boolean(after !== undefined || before !== undefined || status?.length || tier?.length);
|
|
75
|
+
const entries = await this.deps.queryLogStore.list({
|
|
76
|
+
...(after === undefined ? {} : { after }),
|
|
77
|
+
...(before === undefined ? {} : { before }),
|
|
78
|
+
limit,
|
|
79
|
+
...(status?.length ? { status } : {}),
|
|
80
|
+
...(tier?.length ? { tier } : {}),
|
|
81
|
+
});
|
|
82
|
+
if (format === 'json') {
|
|
83
|
+
this.logJson({ data: entries, success: true });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (entries.length === 0) {
|
|
87
|
+
if (hasFilters) {
|
|
88
|
+
this.log('No query log entries found matching your filters.');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.log('No query log entries found.');
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const idWidth = 22;
|
|
96
|
+
const tierWidth = 6;
|
|
97
|
+
const statusWidth = 12;
|
|
98
|
+
const timeWidth = 8;
|
|
99
|
+
const queryWidth = 40;
|
|
100
|
+
const header = [
|
|
101
|
+
'ID'.padEnd(idWidth),
|
|
102
|
+
'Tier'.padEnd(tierWidth),
|
|
103
|
+
'Status'.padEnd(statusWidth),
|
|
104
|
+
'Time'.padEnd(timeWidth),
|
|
105
|
+
'Query',
|
|
106
|
+
].join(' ');
|
|
107
|
+
this.log(header);
|
|
108
|
+
this.log('-'.repeat(idWidth + tierWidth + statusWidth + timeWidth + queryWidth + 8));
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const duration = formatEntryDuration(entry);
|
|
111
|
+
const tierBadge = entry.tier === undefined ? '—' : `T${entry.tier}`;
|
|
112
|
+
const query = truncate(entry.query, queryWidth);
|
|
113
|
+
const row = [
|
|
114
|
+
entry.id.padEnd(idWidth),
|
|
115
|
+
tierBadge.padEnd(tierWidth),
|
|
116
|
+
entry.status.padEnd(statusWidth),
|
|
117
|
+
duration.padEnd(timeWidth),
|
|
118
|
+
query,
|
|
119
|
+
].join(' ');
|
|
120
|
+
this.log(row);
|
|
121
|
+
if (detail && entry.matchedDocs.length > 0) {
|
|
122
|
+
for (const doc of entry.matchedDocs) {
|
|
123
|
+
this.log(` [${doc.score.toFixed(2)}] ${doc.path}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { QueryLogEntry } from '../core/domain/entities/query-log-entry.js';
|
|
2
|
+
export declare function formatDuration(startedAt: number, completedAt?: number): string;
|
|
3
|
+
export declare function formatEntryDuration(entry: QueryLogEntry): string;
|
|
4
|
+
export declare function formatTimestamp(ms: number): string;
|
|
5
|
+
export declare function truncate(text: string, max: number): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function formatDuration(startedAt, completedAt) {
|
|
2
|
+
if (completedAt === undefined)
|
|
3
|
+
return '—';
|
|
4
|
+
const delta = completedAt - startedAt;
|
|
5
|
+
if (delta < 1000)
|
|
6
|
+
return `${delta}ms`;
|
|
7
|
+
return `${(delta / 1000).toFixed(1)}s`;
|
|
8
|
+
}
|
|
9
|
+
export function formatEntryDuration(entry) {
|
|
10
|
+
if (entry.status === 'processing')
|
|
11
|
+
return '—';
|
|
12
|
+
if (entry.timing)
|
|
13
|
+
return formatDuration(0, entry.timing.durationMs);
|
|
14
|
+
return formatDuration(entry.startedAt, entry.completedAt);
|
|
15
|
+
}
|
|
16
|
+
export function formatTimestamp(ms) {
|
|
17
|
+
return new Date(ms).toISOString().replace('T', ' ').slice(0, 19);
|
|
18
|
+
}
|
|
19
|
+
export function truncate(text, max) {
|
|
20
|
+
if (text.length <= max)
|
|
21
|
+
return text;
|
|
22
|
+
return text.slice(0, max) + '...';
|
|
23
|
+
}
|