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.
Files changed (84) hide show
  1. package/.env.production +4 -6
  2. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
  3. package/dist/agent/infra/agent/cipher-agent.d.ts +1 -0
  4. package/dist/agent/infra/agent/cipher-agent.js +1 -0
  5. package/dist/oclif/commands/curate/view.js +5 -25
  6. package/dist/oclif/commands/dream.d.ts +18 -0
  7. package/dist/oclif/commands/dream.js +230 -0
  8. package/dist/oclif/commands/query-log/summary.d.ts +18 -0
  9. package/dist/oclif/commands/query-log/summary.js +75 -0
  10. package/dist/oclif/commands/query-log/view.d.ts +23 -0
  11. package/dist/oclif/commands/query-log/view.js +95 -0
  12. package/dist/oclif/lib/time-filter.d.ts +10 -0
  13. package/dist/oclif/lib/time-filter.js +21 -0
  14. package/dist/server/config/environment.d.ts +10 -3
  15. package/dist/server/config/environment.js +34 -15
  16. package/dist/server/constants.d.ts +5 -0
  17. package/dist/server/constants.js +7 -0
  18. package/dist/server/core/domain/entities/query-log-entry.d.ts +61 -0
  19. package/dist/server/core/domain/entities/query-log-entry.js +40 -0
  20. package/dist/server/core/domain/transport/schemas.d.ts +108 -7
  21. package/dist/server/core/domain/transport/schemas.js +34 -2
  22. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +23 -2
  23. package/dist/server/core/interfaces/i-terminal.d.ts +3 -0
  24. package/dist/server/core/interfaces/i-terminal.js +1 -0
  25. package/dist/server/core/interfaces/storage/i-query-log-store.d.ts +23 -0
  26. package/dist/server/core/interfaces/storage/i-query-log-store.js +2 -0
  27. package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.d.ts +44 -0
  28. package/dist/server/core/interfaces/usecase/i-query-log-summary-use-case.js +1 -0
  29. package/dist/server/core/interfaces/usecase/i-query-log-use-case.d.ts +13 -0
  30. package/dist/server/core/interfaces/usecase/i-query-log-use-case.js +3 -0
  31. package/dist/server/infra/daemon/agent-process.js +79 -9
  32. package/dist/server/infra/daemon/brv-server.js +74 -5
  33. package/dist/server/infra/dream/dream-lock-service.d.ts +37 -0
  34. package/dist/server/infra/dream/dream-lock-service.js +88 -0
  35. package/dist/server/infra/dream/dream-log-schema.d.ts +966 -0
  36. package/dist/server/infra/dream/dream-log-schema.js +57 -0
  37. package/dist/server/infra/dream/dream-log-store.d.ts +55 -0
  38. package/dist/server/infra/dream/dream-log-store.js +141 -0
  39. package/dist/server/infra/dream/dream-response-schemas.d.ts +219 -0
  40. package/dist/server/infra/dream/dream-response-schemas.js +38 -0
  41. package/dist/server/infra/dream/dream-state-schema.d.ts +67 -0
  42. package/dist/server/infra/dream/dream-state-schema.js +23 -0
  43. package/dist/server/infra/dream/dream-state-service.d.ts +38 -0
  44. package/dist/server/infra/dream/dream-state-service.js +91 -0
  45. package/dist/server/infra/dream/dream-trigger.d.ts +46 -0
  46. package/dist/server/infra/dream/dream-trigger.js +65 -0
  47. package/dist/server/infra/dream/dream-undo.d.ts +38 -0
  48. package/dist/server/infra/dream/dream-undo.js +293 -0
  49. package/dist/server/infra/dream/operations/consolidate.d.ts +52 -0
  50. package/dist/server/infra/dream/operations/consolidate.js +514 -0
  51. package/dist/server/infra/dream/operations/prune.d.ts +45 -0
  52. package/dist/server/infra/dream/operations/prune.js +362 -0
  53. package/dist/server/infra/dream/operations/synthesize.d.ts +37 -0
  54. package/dist/server/infra/dream/operations/synthesize.js +278 -0
  55. package/dist/server/infra/dream/parse-dream-response.d.ts +11 -0
  56. package/dist/server/infra/dream/parse-dream-response.js +35 -0
  57. package/dist/server/infra/executor/curate-executor.js +10 -0
  58. package/dist/server/infra/executor/dream-executor.d.ts +97 -0
  59. package/dist/server/infra/executor/dream-executor.js +431 -0
  60. package/dist/server/infra/executor/query-executor.d.ts +2 -2
  61. package/dist/server/infra/executor/query-executor.js +92 -22
  62. package/dist/server/infra/process/feature-handlers.js +10 -6
  63. package/dist/server/infra/process/query-log-handler.d.ts +42 -0
  64. package/dist/server/infra/process/query-log-handler.js +150 -0
  65. package/dist/server/infra/process/task-router.d.ts +40 -0
  66. package/dist/server/infra/process/task-router.js +67 -9
  67. package/dist/server/infra/process/transport-handlers.d.ts +4 -0
  68. package/dist/server/infra/process/transport-handlers.js +1 -0
  69. package/dist/server/infra/storage/file-curate-log-store.js +1 -1
  70. package/dist/server/infra/storage/file-query-log-store.d.ts +81 -0
  71. package/dist/server/infra/storage/file-query-log-store.js +249 -0
  72. package/dist/server/infra/transport/handlers/config-handler.js +1 -1
  73. package/dist/server/infra/usecase/curate-log-use-case.js +7 -3
  74. package/dist/server/infra/usecase/query-log-summary-narrative-formatter.d.ts +15 -0
  75. package/dist/server/infra/usecase/query-log-summary-narrative-formatter.js +79 -0
  76. package/dist/server/infra/usecase/query-log-summary-use-case.d.ts +13 -0
  77. package/dist/server/infra/usecase/query-log-summary-use-case.js +217 -0
  78. package/dist/server/infra/usecase/query-log-use-case.d.ts +31 -0
  79. package/dist/server/infra/usecase/query-log-use-case.js +128 -0
  80. package/dist/server/utils/log-format-utils.d.ts +5 -0
  81. package/dist/server/utils/log-format-utils.js +23 -0
  82. package/dist/shared/transport/events/config-events.d.ts +1 -1
  83. package/oclif.manifest.json +510 -255
  84. package/package.json +1 -1
@@ -32,12 +32,16 @@ import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-li
32
32
  import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
33
33
  import type { IProjectRouter } from '../../core/interfaces/routing/i-project-router.js';
34
34
  import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
35
+ import type { PreDispatchCheck } from './task-router.js';
36
+ export type { PreDispatchCheck, PreDispatchCheckResult } from './task-router.js';
35
37
  export type { TaskInfo } from './types.js';
36
38
  type TransportHandlersOptions = {
37
39
  agentPool?: IAgentPool;
38
40
  clientManager?: IClientManager;
39
41
  /** Lifecycle hooks for task events (e.g. CurateLogHandler). */
40
42
  lifecycleHooks?: ITaskLifecycleHook[];
43
+ /** Optional daemon-side gate run before dispatching a task to the agent pool. */
44
+ preDispatchCheck?: PreDispatchCheck;
41
45
  projectRegistry?: IProjectRegistry;
42
46
  projectRouter?: IProjectRouter;
43
47
  transport: ITransportServer;
@@ -42,6 +42,7 @@ export class TransportHandlers {
42
42
  agentPool: options.agentPool,
43
43
  getAgentForProject: (projectPath) => this.connectionCoordinator.getAgentForProject(projectPath),
44
44
  lifecycleHooks: options.lifecycleHooks,
45
+ preDispatchCheck: options.preDispatchCheck,
45
46
  projectRegistry: options.projectRegistry,
46
47
  projectRouter: options.projectRouter,
47
48
  resolveClientProjectPath: (clientId) => options.clientManager?.getClient(clientId)?.projectPath,
@@ -57,7 +57,7 @@ const CurateLogEntryFileSchema = z.discriminatedUnion('status', [
57
57
  ]);
58
58
  // ── FileCurateLogStore ────────────────────────────────────────────────────────
59
59
  const ID_PATTERN = new RegExp(`^${CURATE_LOG_ID_PREFIX}-\\d+$`);
60
- const DEFAULT_MAX_ENTRIES = 100;
60
+ const DEFAULT_MAX_ENTRIES = 1000;
61
61
  /** Entries stuck in "processing" longer than this are considered interrupted (daemon was killed). */
62
62
  const STALE_PROCESSING_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
63
63
  /**
@@ -0,0 +1,81 @@
1
+ import type { QueryLogEntry } from '../../core/domain/entities/query-log-entry.js';
2
+ import type { IQueryLogStore, QueryLogStatus, QueryLogTier } from '../../core/interfaces/storage/i-query-log-store.js';
3
+ type FileQueryLogStoreOptions = {
4
+ baseDir: string;
5
+ maxAgeDays?: number;
6
+ maxEntries?: number;
7
+ };
8
+ /**
9
+ * File-based implementation of IQueryLogStore.
10
+ *
11
+ * Each log entry is stored as a JSON file:
12
+ * {baseDir}/query-log/qry-{timestamp_ms}.json
13
+ *
14
+ * Writes are atomic (tmp → rename). Reads validate with Zod and return undefined
15
+ * for corrupt/missing files. Prunes by age (default 30 days) then by count (default 1000).
16
+ */
17
+ export declare class FileQueryLogStore implements IQueryLogStore {
18
+ private lastTimestamp;
19
+ private readonly logDir;
20
+ private readonly maxAgeDays;
21
+ private readonly maxEntries;
22
+ private pruneInFlight;
23
+ constructor(opts: FileQueryLogStoreOptions);
24
+ /**
25
+ * Retrieve an entry by ID. Returns undefined if:
26
+ * - ID format is invalid (security: prevents path traversal)
27
+ * - File does not exist
28
+ * - File content fails Zod validation (corrupted)
29
+ */
30
+ getById(id: string): Promise<QueryLogEntry | undefined>;
31
+ /**
32
+ * Generate the next monotonic log entry ID in the format `qry-{timestamp_ms}`.
33
+ * Guaranteed to increase even if called multiple times in the same millisecond.
34
+ *
35
+ * Note: monotonicity is instance-local. A new instance resets `lastTimestamp` to 0
36
+ * and relies on wall-clock time. Two instances pointing at the same baseDir could
37
+ * theoretically collide in the same millisecond, but this is practically impossible
38
+ * given the sequential task queue (max concurrency = 1 per project).
39
+ */
40
+ getNextId(): Promise<string>;
41
+ /**
42
+ * List entries sorted newest-first (by timestamp embedded in filename).
43
+ * Filters (status, tier, after, before) are applied before limit.
44
+ * Reads stop early once `limit` matches are found, so filtered queries with small limits
45
+ * are O(matches) rather than O(total entries). Skips corrupt entries silently.
46
+ */
47
+ list({ after, before, limit, status, tier, }?: {
48
+ after?: number;
49
+ before?: number;
50
+ limit?: number;
51
+ status?: QueryLogStatus[];
52
+ tier?: QueryLogTier[];
53
+ }): Promise<QueryLogEntry[]>;
54
+ /**
55
+ * Persist a log entry atomically (write to tmp, then rename).
56
+ * On rename failure, cleans up the tmp file. After saving, prunes by age then by count (best-effort).
57
+ */
58
+ save(entry: QueryLogEntry): Promise<void>;
59
+ private entryPath;
60
+ /**
61
+ * Schedule a prune pass without blocking the caller.
62
+ * Deduplicates concurrent calls — only one prune runs at a time.
63
+ */
64
+ private firePrune;
65
+ private pruneOldest;
66
+ /**
67
+ * If a "processing" entry is older than STALE_PROCESSING_THRESHOLD_MS, the daemon
68
+ * was killed before it could finalize it. Rewrite it as "error" on disk (best-effort)
69
+ * and return the corrected entry so the display shows "interrupted" instead of processing.
70
+ *
71
+ * Uses writeAtomic directly (not save) to skip the prune cascade — list() with N stale
72
+ * entries would otherwise trigger N concurrent prune passes.
73
+ */
74
+ private resolveStale;
75
+ /**
76
+ * Atomic write: write to a tmp file with random UUID suffix, then rename.
77
+ * On failure, cleans up the tmp file and re-throws the original error.
78
+ */
79
+ private writeAtomic;
80
+ }
81
+ export {};
@@ -0,0 +1,249 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { QUERY_LOG_DIR, QUERY_LOG_ID_PREFIX } from '../../constants.js';
6
+ import { QUERY_LOG_TIERS } from '../../core/domain/entities/query-log-entry.js';
7
+ const QueryLogMatchedDocFileSchema = z.object({
8
+ path: z.string(),
9
+ score: z.number(),
10
+ title: z.string(),
11
+ });
12
+ const QueryLogSearchMetadataFileSchema = z.object({
13
+ cacheFingerprint: z.string().optional(),
14
+ resultCount: z.number(),
15
+ topScore: z.number(),
16
+ totalFound: z.number(),
17
+ });
18
+ const QueryLogTimingFileSchema = z.object({
19
+ durationMs: z.number(),
20
+ });
21
+ // Single source of truth: tier validation is derived from QUERY_LOG_TIERS at runtime.
22
+ // Adding/removing a tier in the entity automatically updates schema validation.
23
+ const QUERY_LOG_TIER_SET = new Set(QUERY_LOG_TIERS);
24
+ const QueryLogTierSchema = z.custom((val) => QUERY_LOG_TIER_SET.has(val), { message: 'Invalid query log tier' });
25
+ const QueryLogEntryBaseSchema = z.object({
26
+ id: z.string(),
27
+ matchedDocs: z.array(QueryLogMatchedDocFileSchema),
28
+ query: z.string(),
29
+ searchMetadata: QueryLogSearchMetadataFileSchema.optional(),
30
+ startedAt: z.number(),
31
+ taskId: z.string(),
32
+ tier: QueryLogTierSchema.optional(),
33
+ timing: QueryLogTimingFileSchema.optional(),
34
+ });
35
+ const QueryLogEntryFileSchema = z.discriminatedUnion('status', [
36
+ QueryLogEntryBaseSchema.extend({ status: z.literal('processing') }),
37
+ QueryLogEntryBaseSchema.extend({
38
+ completedAt: z.number(),
39
+ response: z.string().optional(),
40
+ status: z.literal('completed'),
41
+ }),
42
+ QueryLogEntryBaseSchema.extend({
43
+ completedAt: z.number(),
44
+ error: z.string(),
45
+ status: z.literal('error'),
46
+ }),
47
+ QueryLogEntryBaseSchema.extend({
48
+ completedAt: z.number(),
49
+ status: z.literal('cancelled'),
50
+ }),
51
+ ]);
52
+ const ID_PATTERN = new RegExp(`^${QUERY_LOG_ID_PREFIX}-\\d+$`);
53
+ const DEFAULT_MAX_ENTRIES = 1000;
54
+ const DEFAULT_MAX_AGE_DAYS = 30;
55
+ /** Entries stuck in "processing" longer than this are considered interrupted (daemon was killed). */
56
+ const STALE_PROCESSING_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes
57
+ /**
58
+ * File-based implementation of IQueryLogStore.
59
+ *
60
+ * Each log entry is stored as a JSON file:
61
+ * {baseDir}/query-log/qry-{timestamp_ms}.json
62
+ *
63
+ * Writes are atomic (tmp → rename). Reads validate with Zod and return undefined
64
+ * for corrupt/missing files. Prunes by age (default 30 days) then by count (default 1000).
65
+ */
66
+ export class FileQueryLogStore {
67
+ lastTimestamp = 0;
68
+ logDir;
69
+ maxAgeDays;
70
+ maxEntries;
71
+ pruneInFlight = false;
72
+ constructor(opts) {
73
+ this.logDir = join(opts.baseDir, QUERY_LOG_DIR);
74
+ this.maxAgeDays = opts.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS;
75
+ this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
76
+ }
77
+ /**
78
+ * Retrieve an entry by ID. Returns undefined if:
79
+ * - ID format is invalid (security: prevents path traversal)
80
+ * - File does not exist
81
+ * - File content fails Zod validation (corrupted)
82
+ */
83
+ async getById(id) {
84
+ if (!ID_PATTERN.test(id))
85
+ return undefined;
86
+ try {
87
+ const raw = await readFile(this.entryPath(id), 'utf8');
88
+ const parsed = QueryLogEntryFileSchema.safeParse(JSON.parse(raw));
89
+ if (!parsed.success)
90
+ return undefined;
91
+ return await this.resolveStale(parsed.data);
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ }
97
+ /**
98
+ * Generate the next monotonic log entry ID in the format `qry-{timestamp_ms}`.
99
+ * Guaranteed to increase even if called multiple times in the same millisecond.
100
+ *
101
+ * Note: monotonicity is instance-local. A new instance resets `lastTimestamp` to 0
102
+ * and relies on wall-clock time. Two instances pointing at the same baseDir could
103
+ * theoretically collide in the same millisecond, but this is practically impossible
104
+ * given the sequential task queue (max concurrency = 1 per project).
105
+ */
106
+ async getNextId() {
107
+ const now = Date.now();
108
+ this.lastTimestamp = now <= this.lastTimestamp ? this.lastTimestamp + 1 : now;
109
+ return `${QUERY_LOG_ID_PREFIX}-${this.lastTimestamp}`;
110
+ }
111
+ /**
112
+ * List entries sorted newest-first (by timestamp embedded in filename).
113
+ * Filters (status, tier, after, before) are applied before limit.
114
+ * Reads stop early once `limit` matches are found, so filtered queries with small limits
115
+ * are O(matches) rather than O(total entries). Skips corrupt entries silently.
116
+ */
117
+ async list({ after, before, limit, status, tier, } = {}) {
118
+ let files;
119
+ try {
120
+ const entries = await readdir(this.logDir, { withFileTypes: true });
121
+ files = entries
122
+ .filter((e) => e.isFile() && e.name.endsWith('.json') && ID_PATTERN.test(e.name.slice(0, -5)))
123
+ .map((e) => e.name)
124
+ .sort()
125
+ .reverse(); // newest-first (lexicographic descending)
126
+ }
127
+ catch {
128
+ return [];
129
+ }
130
+ // Sequential read with early termination once limit matches are found.
131
+ // Avoids scanning all files for queries like list({status: ['error'], limit: 1}).
132
+ const results = [];
133
+ const targetCount = limit ?? Number.POSITIVE_INFINITY;
134
+ for (const filename of files) {
135
+ if (results.length >= targetCount)
136
+ break;
137
+ const id = filename.slice(0, -5);
138
+ // eslint-disable-next-line no-await-in-loop -- early termination requires sequential reads
139
+ const entry = await this.getById(id);
140
+ if (!entry)
141
+ continue;
142
+ if (status?.length && !status.includes(entry.status))
143
+ continue;
144
+ if (tier?.length && (entry.tier === undefined || !tier.includes(entry.tier)))
145
+ continue;
146
+ if (after !== undefined && entry.startedAt < after)
147
+ continue;
148
+ if (before !== undefined && entry.startedAt > before)
149
+ continue;
150
+ results.push(entry);
151
+ }
152
+ return results;
153
+ }
154
+ /**
155
+ * Persist a log entry atomically (write to tmp, then rename).
156
+ * On rename failure, cleans up the tmp file. After saving, prunes by age then by count (best-effort).
157
+ */
158
+ async save(entry) {
159
+ await mkdir(this.logDir, { recursive: true });
160
+ await this.writeAtomic(this.entryPath(entry.id), JSON.stringify(entry, null, 2));
161
+ this.firePrune();
162
+ }
163
+ entryPath(id) {
164
+ return join(this.logDir, `${id}.json`);
165
+ }
166
+ /**
167
+ * Schedule a prune pass without blocking the caller.
168
+ * Deduplicates concurrent calls — only one prune runs at a time.
169
+ */
170
+ firePrune() {
171
+ if (this.pruneInFlight)
172
+ return;
173
+ this.pruneInFlight = true;
174
+ this.pruneOldest()
175
+ .catch(() => { })
176
+ .finally(() => {
177
+ this.pruneInFlight = false;
178
+ });
179
+ }
180
+ async pruneOldest() {
181
+ const dirEntries = await readdir(this.logDir, { withFileTypes: true });
182
+ const files = dirEntries
183
+ .filter((e) => e.isFile() && e.name.endsWith('.json') && ID_PATTERN.test(e.name.slice(0, -5)))
184
+ .map((e) => e.name)
185
+ .sort(); // oldest-first
186
+ // Phase 1: Age-based pruning (skip if maxAgeDays === 0)
187
+ let remaining = files;
188
+ if (this.maxAgeDays > 0) {
189
+ const cutoff = Date.now() - this.maxAgeDays * 86_400_000;
190
+ const expired = [];
191
+ const kept = [];
192
+ for (const f of files) {
193
+ const ts = Number(f.slice(QUERY_LOG_ID_PREFIX.length + 1, -5)); // extract timestamp from "qry-{ts}.json"
194
+ if (ts < cutoff) {
195
+ expired.push(f);
196
+ }
197
+ else {
198
+ kept.push(f);
199
+ }
200
+ }
201
+ if (expired.length > 0) {
202
+ await Promise.all(expired.map((f) => rm(join(this.logDir, f), { force: true }).catch(() => { })));
203
+ }
204
+ remaining = kept;
205
+ }
206
+ // Phase 2: Count-based pruning
207
+ if (remaining.length <= this.maxEntries)
208
+ return;
209
+ const toDelete = remaining.slice(0, remaining.length - this.maxEntries);
210
+ await Promise.all(toDelete.map((f) => rm(join(this.logDir, f), { force: true }).catch(() => { })));
211
+ }
212
+ /**
213
+ * If a "processing" entry is older than STALE_PROCESSING_THRESHOLD_MS, the daemon
214
+ * was killed before it could finalize it. Rewrite it as "error" on disk (best-effort)
215
+ * and return the corrected entry so the display shows "interrupted" instead of processing.
216
+ *
217
+ * Uses writeAtomic directly (not save) to skip the prune cascade — list() with N stale
218
+ * entries would otherwise trigger N concurrent prune passes.
219
+ */
220
+ async resolveStale(entry) {
221
+ if (entry.status !== 'processing')
222
+ return entry;
223
+ if (Date.now() - entry.startedAt <= STALE_PROCESSING_THRESHOLD_MS)
224
+ return entry;
225
+ const recovered = {
226
+ ...entry,
227
+ completedAt: Date.now(),
228
+ error: 'Interrupted (daemon terminated)',
229
+ status: 'error',
230
+ };
231
+ this.writeAtomic(this.entryPath(recovered.id), JSON.stringify(recovered, null, 2)).catch(() => { });
232
+ return recovered;
233
+ }
234
+ /**
235
+ * Atomic write: write to a tmp file with random UUID suffix, then rename.
236
+ * On failure, cleans up the tmp file and re-throws the original error.
237
+ */
238
+ async writeAtomic(filePath, content) {
239
+ const tmpPath = `${filePath}.${randomUUID()}.tmp`;
240
+ try {
241
+ await writeFile(tmpPath, content, 'utf8');
242
+ await rename(tmpPath, filePath);
243
+ }
244
+ catch (error) {
245
+ await rm(tmpPath, { force: true }).catch(() => { });
246
+ throw error;
247
+ }
248
+ }
249
+ }
@@ -13,7 +13,7 @@ export class ConfigHandler {
13
13
  this.transport.onRequest(ConfigEvents.GET_ENVIRONMENT, () => {
14
14
  const config = getCurrentConfig();
15
15
  return {
16
- apiBaseUrl: config.apiBaseUrl,
16
+ iamBaseUrl: config.iamBaseUrl,
17
17
  isDevelopment: isDevelopment(),
18
18
  webAppUrl: config.webAppUrl,
19
19
  };
@@ -67,8 +67,12 @@ export class CurateLogUseCase {
67
67
  }
68
68
  this.log();
69
69
  this.log('Input:');
70
- if (entry.input.context)
71
- this.log(` Context: ${entry.input.context.slice(0, 200)}${entry.input.context.length > 200 ? '...' : ''}`);
70
+ if (entry.input.context) {
71
+ const [firstLine, ...rest] = entry.input.context.split('\n');
72
+ this.log(` Context: ${firstLine}`);
73
+ for (const line of rest)
74
+ this.log(` ${line}`);
75
+ }
72
76
  if (entry.input.files?.length)
73
77
  this.log(` Files: ${entry.input.files.join(', ')}`);
74
78
  if (entry.input.folders?.length)
@@ -89,7 +93,7 @@ export class CurateLogUseCase {
89
93
  if (entry.status === 'completed' && entry.response) {
90
94
  this.log();
91
95
  this.log('Response:');
92
- this.log(` ${entry.response.slice(0, 500)}${entry.response.length > 500 ? '...' : ''}`);
96
+ this.log(entry.response.split('\n').map((line) => ` ${line}`).join('\n'));
93
97
  }
94
98
  }
95
99
  async showList({ after, before, detail, format, limit, status }) {
@@ -0,0 +1,15 @@
1
+ import type { QueryLogSummary } from '../../core/interfaces/usecase/i-query-log-summary-use-case.js';
2
+ export declare function formatQueryLogSummaryNarrative(summary: QueryLogSummary): string;
3
+ /**
4
+ * Describe the time period as a human-readable label.
5
+ *
6
+ * Two formats:
7
+ * - 'short': "last 1h", "last 24h", "last 7d" (for text summary header)
8
+ * - 'long': "in the last hour", "in the last 24 hours" (for narrative prose)
9
+ *
10
+ * Only produces a period label when we have a clear relative window
11
+ * (--since/--last without --before). Bounded or ambiguous ranges
12
+ * return empty string (short) or "in the selected period" (long).
13
+ */
14
+ export declare function describePeriod(period: QueryLogSummary['period'], format?: 'long' | 'short'): string;
15
+ export declare function formatDurationMs(ms: number): string;
@@ -0,0 +1,79 @@
1
+ const NARRATIVE_TOP_DOCS = 2;
2
+ const NARRATIVE_TOP_GAPS = 2;
3
+ const MS_PER_HOUR = 3_600_000;
4
+ const MS_PER_DAY = 86_400_000;
5
+ export function formatQueryLogSummaryNarrative(summary) {
6
+ const periodLabel = describePeriod(summary.period);
7
+ if (summary.totalQueries === 0) {
8
+ return `No queries recorded ${periodLabel}. Your knowledge base is ready — try asking a question!`;
9
+ }
10
+ const paragraphs = [];
11
+ paragraphs.push(buildOverviewParagraph(summary, periodLabel));
12
+ if (summary.totalMatchedDocs > 0 && summary.topRecalledDocs.length > 0) {
13
+ paragraphs.push(buildTopDocsParagraph(summary));
14
+ }
15
+ paragraphs.push(buildGapsParagraph(summary));
16
+ return paragraphs.join('\n\n');
17
+ }
18
+ /**
19
+ * Describe the time period as a human-readable label.
20
+ *
21
+ * Two formats:
22
+ * - 'short': "last 1h", "last 24h", "last 7d" (for text summary header)
23
+ * - 'long': "in the last hour", "in the last 24 hours" (for narrative prose)
24
+ *
25
+ * Only produces a period label when we have a clear relative window
26
+ * (--since/--last without --before). Bounded or ambiguous ranges
27
+ * return empty string (short) or "in the selected period" (long).
28
+ */
29
+ export function describePeriod(period, format = 'long') {
30
+ if (period.from > 0 && period.to === 0) {
31
+ const spanMs = Date.now() - period.from;
32
+ const hours = Math.round(spanMs / MS_PER_HOUR);
33
+ const days = Math.round(spanMs / MS_PER_DAY);
34
+ if (format === 'short') {
35
+ if (hours <= 1)
36
+ return 'last 1h';
37
+ if (hours <= 24)
38
+ return 'last 24h';
39
+ return `last ${days}d`;
40
+ }
41
+ if (hours <= 1)
42
+ return 'in the last hour';
43
+ if (hours <= 24)
44
+ return 'in the last 24 hours';
45
+ return `in the last ${days} days`;
46
+ }
47
+ return format === 'short' ? 'selected period' : 'in the selected period';
48
+ }
49
+ function buildOverviewParagraph(summary, periodLabel) {
50
+ const { byStatus, cacheHitRate, coverageRate, queriesWithoutMatches, responseTime, totalQueries } = summary;
51
+ const answered = byStatus.completed - queriesWithoutMatches;
52
+ const coveragePct = Math.round(coverageRate * 100);
53
+ const cachePct = Math.round(cacheHitRate * 100);
54
+ return (`Your team asked ${totalQueries} questions ${periodLabel}. ` +
55
+ `ByteRover answered ${answered} from curated knowledge ` +
56
+ `(${coveragePct}% coverage), with ${cachePct}% served from cache. ` +
57
+ `Average response time was ${formatDurationMs(responseTime.avgMs)}.`);
58
+ }
59
+ function buildTopDocsParagraph(summary) {
60
+ const topDocs = summary.topRecalledDocs.slice(0, NARRATIVE_TOP_DOCS);
61
+ const docsList = topDocs.map((doc) => `${doc.path} (${doc.count} queries)`).join(', ');
62
+ return `Most useful knowledge: ${docsList}.`;
63
+ }
64
+ function buildGapsParagraph(summary) {
65
+ if (summary.knowledgeGaps.length === 0) {
66
+ return 'Every question was answered from curated knowledge.';
67
+ }
68
+ const unansweredCount = summary.queriesWithoutMatches;
69
+ const topGaps = summary.knowledgeGaps.slice(0, NARRATIVE_TOP_GAPS);
70
+ const gapsList = topGaps.map((gap) => `"${gap.topic}"`).join(' and ');
71
+ return (`${unansweredCount} question${unansweredCount === 1 ? '' : 's'} couldn't be answered — ` +
72
+ `consider curating more about ${gapsList}.`);
73
+ }
74
+ export function formatDurationMs(ms) {
75
+ if (ms >= 1000) {
76
+ return `${(ms / 1000).toFixed(1)}s`;
77
+ }
78
+ return `${Math.round(ms)}ms`;
79
+ }
@@ -0,0 +1,13 @@
1
+ import type { ITerminal } from '../../core/interfaces/i-terminal.js';
2
+ import type { IQueryLogStore } from '../../core/interfaces/storage/i-query-log-store.js';
3
+ import type { IQueryLogSummaryUseCase } from '../../core/interfaces/usecase/i-query-log-summary-use-case.js';
4
+ type QueryLogSummaryUseCaseDeps = {
5
+ queryLogStore: IQueryLogStore;
6
+ terminal: ITerminal;
7
+ };
8
+ export declare class QueryLogSummaryUseCase implements IQueryLogSummaryUseCase {
9
+ private readonly deps;
10
+ constructor(deps: QueryLogSummaryUseCaseDeps);
11
+ run(options: Parameters<IQueryLogSummaryUseCase['run']>[0]): Promise<void>;
12
+ }
13
+ export {};