@geminilight/mindos 0.6.22 → 0.6.23

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/app/lib/fs.ts CHANGED
@@ -59,9 +59,36 @@ const CACHE_TTL_MS = 5_000; // 5 seconds
59
59
 
60
60
  let _treeVersion = 0;
61
61
 
62
+ function buildCache(root: string): FileTreeCache {
63
+ const tree = buildFileTree(root);
64
+ const allFiles: string[] = [];
65
+ function collect(nodes: FileNode[]) {
66
+ for (const n of nodes) {
67
+ if (n.type === 'file') allFiles.push(n.path);
68
+ else if (n.children) collect(n.children);
69
+ }
70
+ }
71
+ collect(tree);
72
+ return { tree, allFiles, timestamp: Date.now() };
73
+ }
74
+
75
+ function sameFileList(a: string[], b: string[]): boolean {
76
+ if (a.length !== b.length) return false;
77
+ const sa = [...a].sort();
78
+ const sb = [...b].sort();
79
+ return sa.every((p, i) => p === sb[i]);
80
+ }
81
+
62
82
  /** Monotonically increasing counter — bumped on every file mutation so the
63
83
  * client can cheaply detect changes without rebuilding the full tree. */
64
84
  export function getTreeVersion(): number {
85
+ if (_cache && !isCacheValid()) {
86
+ const next = buildCache(getMindRoot());
87
+ const changed = !sameFileList(_cache.allFiles, next.allFiles);
88
+ _cache = next;
89
+ _searchIndex = null;
90
+ if (changed) _treeVersion++;
91
+ }
65
92
  return _treeVersion;
66
93
  }
67
94
 
@@ -80,17 +107,7 @@ export function invalidateCache(): void {
80
107
  function ensureCache(): FileTreeCache {
81
108
  if (isCacheValid()) return _cache!;
82
109
  const root = getMindRoot();
83
- const tree = buildFileTree(root);
84
- // Extract all file paths from the tree to avoid a second full traversal.
85
- const allFiles: string[] = [];
86
- function collect(nodes: FileNode[]) {
87
- for (const n of nodes) {
88
- if (n.type === 'file') allFiles.push(n.path);
89
- else if (n.children) collect(n.children);
90
- }
91
- }
92
- collect(tree);
93
- _cache = { tree, allFiles, timestamp: Date.now() };
110
+ _cache = buildCache(root);
94
111
  return _cache;
95
112
  }
96
113
 
@@ -699,6 +699,8 @@ export const en = {
699
699
  newFile: 'New File',
700
700
  importFile: 'Import File',
701
701
  importToSpace: 'Import file to this space',
702
+ copyPath: 'Copy Path',
703
+ pathCopied: 'Path copied',
702
704
  },
703
705
  fileImport: {
704
706
  title: 'Import Files',
@@ -723,6 +723,8 @@ export const zh = {
723
723
  newFile: '新建文件',
724
724
  importFile: '导入文件',
725
725
  importToSpace: '导入文件到此空间',
726
+ copyPath: '复制路径',
727
+ pathCopied: '路径已复制',
726
728
  },
727
729
  fileImport: {
728
730
  title: '导入文件',
@@ -148,7 +148,7 @@ function parseGuideState(raw: unknown): GuideState | undefined {
148
148
  askedAI: obj.askedAI === true,
149
149
  nextStepIndex: typeof obj.nextStepIndex === 'number' ? obj.nextStepIndex : 0,
150
150
  walkthroughStep: typeof obj.walkthroughStep === 'number' ? obj.walkthroughStep : undefined,
151
- walkthroughDismissed: obj.walkthroughDismissed === true ? true : undefined,
151
+ walkthroughDismissed: typeof obj.walkthroughDismissed === 'boolean' ? obj.walkthroughDismissed : undefined,
152
152
  };
153
153
  }
154
154
 
package/bin/cli.js CHANGED
@@ -60,6 +60,13 @@ import { spawnMcp } from './lib/mcp-spawn.js';
60
60
  import { ensureMcpBundle } from './lib/mcp-build.js';
61
61
  import { mcpInstall } from './lib/mcp-install.js';
62
62
  import { initSync, startSyncDaemon, stopSyncDaemon, getSyncStatus, manualSync, listConflicts, setSyncEnabled } from './lib/sync.js';
63
+ import { parseArgs } from './lib/command.js';
64
+
65
+ // ── New modular commands ──────────────────────────────────────────────────────
66
+ import * as fileCmd from './commands/file.js';
67
+ import * as spaceCmd from './commands/space.js';
68
+ import * as askCmd from './commands/ask.js';
69
+ import * as statusCmd from './commands/status.js';
63
70
 
64
71
  // ── Helpers ───────────────────────────────────────────────────────────────────
65
72
 
@@ -1298,6 +1305,12 @@ ${bold('Examples:')}
1298
1305
  }
1299
1306
  console.log();
1300
1307
  },
1308
+
1309
+ // ── New modular commands (knowledge operations) ──────────────────────────
1310
+ file: async () => { const p = parseArgs(process.argv.slice(3)); await fileCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1311
+ space: async () => { const p = parseArgs(process.argv.slice(3)); await spaceCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1312
+ ask: async () => { const p = parseArgs(process.argv.slice(3)); await askCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1313
+ status: async () => { const p = parseArgs(process.argv.slice(3)); await statusCmd.run([p.command, ...p.args].filter(Boolean), p.flags); },
1301
1314
  };
1302
1315
 
1303
1316
  // ── Entry ─────────────────────────────────────────────────────────────────────
@@ -1308,40 +1321,45 @@ if (!resolvedCmd || !commands[resolvedCmd]) {
1308
1321
  const pkgVersion = (() => { try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; } })();
1309
1322
  const row = (c, d) => ` ${cyan(c.padEnd(36))}${dim(d)}`;
1310
1323
  console.log(`
1311
- ${bold('🧠 MindOS CLI')} ${dim(`v${pkgVersion}`)}
1324
+ ${bold('MindOS CLI')} ${dim(`v${pkgVersion}`)}
1312
1325
 
1313
1326
  ${bold('Core:')}
1314
1327
  ${row('mindos onboard', 'Interactive setup (aliases: init, setup)')}
1315
- ${row('mindos onboard --install-daemon', 'Setup + install & start as background OS service')}
1316
- ${row('mindos start', 'Start app + MCP server (production, auto-rebuilds if needed)')}
1317
- ${row('mindos start --daemon', 'Install + start as background OS service (survives terminal close)')}
1318
- ${row('mindos start --verbose', 'Start with verbose MCP logging')}
1319
- ${row('mindos dev', 'Start app + MCP server (dev mode)')}
1320
- ${row('mindos dev --turbopack', 'Start with Turbopack (faster HMR)')}
1321
- ${row('mindos stop', 'Stop running MindOS processes')}
1328
+ ${row('mindos start', 'Start app + MCP server (production)')}
1329
+ ${row('mindos start --daemon', 'Start as background OS service')}
1330
+ ${row('mindos dev', 'Start in dev mode')}
1331
+ ${row('mindos stop', 'Stop running processes')}
1322
1332
  ${row('mindos restart', 'Stop then start again')}
1323
- ${row('mindos build', 'Build the app for production')}
1324
- ${row('mindos open', 'Open Web UI in the default browser')}
1333
+ ${row('mindos build', 'Build for production')}
1334
+ ${row('mindos status', 'Show service status overview')}
1335
+ ${row('mindos open', 'Open Web UI in browser')}
1336
+
1337
+ ${bold('Knowledge:')}
1338
+ ${row('mindos file <sub>', 'File operations (list/read/create/delete/search)')}
1339
+ ${row('mindos space <sub>', 'Space management (list/create/info)')}
1340
+ ${row('mindos ask "<question>"', 'Ask AI using your knowledge base')}
1325
1341
 
1326
1342
  ${bold('MCP:')}
1327
1343
  ${row('mindos mcp', 'Start MCP server only')}
1328
- ${row('mindos mcp install [agent]', 'Install MindOS MCP config into Agent (claude-code/cursor/windsurf/…) [-g]')}
1329
- ${row('mindos token', 'Show current auth token and MCP config snippet')}
1344
+ ${row('mindos mcp install [agent]', 'Install MCP config into Agent')}
1345
+ ${row('mindos token', 'Show auth token and MCP config')}
1330
1346
 
1331
1347
  ${bold('Sync:')}
1332
1348
  ${row('mindos sync', 'Show sync status (init/now/conflicts/on/off)')}
1333
1349
 
1334
1350
  ${bold('Gateway (Background Service):')}
1335
- ${row('mindos gateway <subcommand>', 'Manage background service (install/uninstall/start/stop/status/logs)')}
1351
+ ${row('mindos gateway <sub>', 'Manage service (install/start/stop/status/logs)')}
1336
1352
 
1337
1353
  ${bold('Config & Diagnostics:')}
1338
- ${row('mindos config <subcommand>', 'View/update config (show/validate/set/unset)')}
1339
- ${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
1340
- ${row('mindos init-skills', 'Create user-skill-rules.md for personalization')}
1341
- ${row('mindos update', 'Update MindOS to the latest version')}
1342
- ${row('mindos uninstall', 'Fully uninstall MindOS (stop, remove daemon, npm uninstall)')}
1343
- ${row('mindos logs', 'Tail service logs (~/.mindos/mindos.log)')}
1344
- ${row('mindos', 'Start using mode saved in ~/.mindos/config.json')}
1354
+ ${row('mindos config <sub>', 'View/update config (show/set/unset/validate)')}
1355
+ ${row('mindos doctor', 'Health check')}
1356
+ ${row('mindos update', 'Update to latest version')}
1357
+ ${row('mindos logs', 'Tail service logs')}
1358
+
1359
+ ${bold('Global Flags:')}
1360
+ ${row('--json', 'Output in JSON (for AI agents)')}
1361
+ ${row('--help, -h', 'Show help')}
1362
+ ${row('--version, -v', 'Show version')}
1345
1363
  `);
1346
1364
  const isHelp = (cmd === '--help' || cmd === '-h');
1347
1365
  process.exit((cmd && !isHelp) ? 1 : 0);
@@ -0,0 +1,101 @@
1
+ /**
2
+ * mindos ask — AI question answering via local MindOS API
3
+ */
4
+
5
+ import { bold, dim, cyan, green, red } from '../lib/colors.js';
6
+ import { loadConfig } from '../lib/config.js';
7
+ import { output, isJsonMode } from '../lib/command.js';
8
+
9
+ export const meta = {
10
+ name: 'ask',
11
+ group: 'Knowledge',
12
+ summary: 'Ask AI a question using your knowledge base',
13
+ usage: 'mindos ask "<question>"',
14
+ flags: {
15
+ '--json': 'Output as JSON',
16
+ '--port <port>': 'MindOS web port (default: 3456)',
17
+ },
18
+ examples: [
19
+ 'mindos ask "Summarize my meeting notes from today"',
20
+ 'mindos ask "What are the key points in my RAG research?"',
21
+ 'mindos ask "List all TODOs across my notes" --json',
22
+ ],
23
+ };
24
+
25
+ export async function run(args, flags) {
26
+ const question = args.join(' ');
27
+
28
+ if (!question || flags.help || flags.h) {
29
+ console.log(`
30
+ ${bold('mindos ask')} — Ask AI using your knowledge base
31
+
32
+ ${bold('Usage:')}
33
+ ${cyan('mindos ask "<question>"')}
34
+
35
+ ${bold('Examples:')}
36
+ ${dim('mindos ask "Summarize my meeting notes"')}
37
+ ${dim('mindos ask "What are the key insights from my research?" --json')}
38
+
39
+ ${bold('Note:')} MindOS must be running (mindos start).
40
+ `);
41
+ return;
42
+ }
43
+
44
+ loadConfig();
45
+ const port = flags.port || process.env.MINDOS_WEB_PORT || '3456';
46
+ const token = process.env.MINDOS_AUTH_TOKEN || '';
47
+ const baseUrl = `http://localhost:${port}`;
48
+
49
+ // Check if MindOS is running
50
+ try {
51
+ const healthRes = await fetch(`${baseUrl}/api/health`);
52
+ if (!healthRes.ok) throw new Error();
53
+ } catch {
54
+ console.error(red('MindOS is not running. Start it with: mindos start'));
55
+ process.exit(1);
56
+ }
57
+
58
+ if (!isJsonMode(flags)) {
59
+ process.stdout.write(dim('Thinking...'));
60
+ }
61
+
62
+ try {
63
+ const headers = { 'Content-Type': 'application/json' };
64
+ if (token) headers['Authorization'] = `Bearer ${token}`;
65
+
66
+ const res = await fetch(`${baseUrl}/api/ask`, {
67
+ method: 'POST',
68
+ headers,
69
+ body: JSON.stringify({ question }),
70
+ });
71
+
72
+ if (!res.ok) {
73
+ const errText = await res.text();
74
+ throw new Error(`API error (${res.status}): ${errText}`);
75
+ }
76
+
77
+ const data = await res.json();
78
+
79
+ if (isJsonMode(flags)) {
80
+ output(data, flags);
81
+ return;
82
+ }
83
+
84
+ // Clear "Thinking..." line
85
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
86
+
87
+ if (data.answer) {
88
+ console.log(data.answer);
89
+ } else if (data.text) {
90
+ console.log(data.text);
91
+ } else {
92
+ console.log(JSON.stringify(data, null, 2));
93
+ }
94
+ } catch (err) {
95
+ if (!isJsonMode(flags)) {
96
+ process.stdout.write('\r' + ' '.repeat(40) + '\r');
97
+ }
98
+ console.error(red(err.message));
99
+ process.exit(1);
100
+ }
101
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * mindos file — Knowledge base file operations
3
+ *
4
+ * Subcommands: list, read, create, delete, rename, move, search
5
+ * Supports --json for agent consumption
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, renameSync, mkdirSync, readdirSync, statSync } from 'node:fs';
9
+ import { resolve, basename, dirname, relative } from 'node:path';
10
+ import { bold, dim, cyan, green, red, yellow } from '../lib/colors.js';
11
+ import { loadConfig } from '../lib/config.js';
12
+ import { output, isJsonMode } from '../lib/command.js';
13
+
14
+ function getMindRoot() {
15
+ loadConfig();
16
+ const root = process.env.MIND_ROOT;
17
+ if (!root || !existsSync(root)) {
18
+ console.error(red('Mind root not configured. Run `mindos onboard` first.'));
19
+ process.exit(1);
20
+ }
21
+ return root;
22
+ }
23
+
24
+ function resolvePath(root, filePath) {
25
+ // Accept both relative (to mind root) and absolute paths
26
+ if (filePath.startsWith('/')) return filePath;
27
+ return resolve(root, filePath);
28
+ }
29
+
30
+ export const meta = {
31
+ name: 'file',
32
+ group: 'Knowledge',
33
+ summary: 'Knowledge base file operations (list, read, create, delete, search)',
34
+ usage: 'mindos file <subcommand>',
35
+ flags: {
36
+ '--space <name>': 'Filter by space name',
37
+ '--json': 'Output as JSON',
38
+ '--recursive, -r': 'Recursive listing',
39
+ },
40
+ examples: [
41
+ 'mindos file list',
42
+ 'mindos file list --space "Work"',
43
+ 'mindos file read "notes/meeting.md"',
44
+ 'mindos file create "notes/idea.md" --content "# My Idea"',
45
+ 'mindos file search "RAG implementation"',
46
+ 'mindos file delete "notes/old.md"',
47
+ ],
48
+ };
49
+
50
+ export async function run(args, flags) {
51
+ const sub = args[0];
52
+ const root = getMindRoot();
53
+
54
+ if (!sub || flags.help || flags.h) {
55
+ printFileHelp();
56
+ return;
57
+ }
58
+
59
+ switch (sub) {
60
+ case 'list': return fileList(root, args.slice(1), flags);
61
+ case 'ls': return fileList(root, args.slice(1), flags);
62
+ case 'read': return fileRead(root, args[1], flags);
63
+ case 'cat': return fileRead(root, args[1], flags);
64
+ case 'create': return fileCreate(root, args[1], flags);
65
+ case 'delete': return fileDelete(root, args[1], flags);
66
+ case 'rm': return fileDelete(root, args[1], flags);
67
+ case 'rename': return fileRename(root, args[1], args[2], flags);
68
+ case 'mv': return fileRename(root, args[1], args[2], flags);
69
+ case 'move': return fileRename(root, args[1], args[2], flags);
70
+ case 'search': return fileSearch(root, args.slice(1).join(' '), flags);
71
+ default:
72
+ console.error(red(`Unknown subcommand: ${sub}`));
73
+ console.error(dim('Available: list, read, create, delete, rename, move, search'));
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ function printFileHelp() {
79
+ console.log(`
80
+ ${bold('mindos file')} — Knowledge base file operations
81
+
82
+ ${bold('Subcommands:')}
83
+ ${cyan('list'.padEnd(20))}${dim('List files in knowledge base')}
84
+ ${cyan('read <path>'.padEnd(20))}${dim('Read file content')}
85
+ ${cyan('create <path>'.padEnd(20))}${dim('Create a new file (--content "...")')}
86
+ ${cyan('delete <path>'.padEnd(20))}${dim('Delete a file')}
87
+ ${cyan('rename <old> <new>'.padEnd(20))}${dim('Rename or move a file')}
88
+ ${cyan('search <query>'.padEnd(20))}${dim('Search files by content')}
89
+
90
+ ${bold('Aliases:')} ls=list, cat=read, rm=delete, mv=rename
91
+
92
+ ${bold('Examples:')}
93
+ ${dim('mindos file list')}
94
+ ${dim('mindos file list --json')}
95
+ ${dim('mindos file read "notes/meeting.md"')}
96
+ ${dim('mindos file create "ideas/new.md" --content "# New Idea"')}
97
+ ${dim('mindos file search "machine learning"')}
98
+ `);
99
+ }
100
+
101
+ function walkFiles(dir, root, opts = {}) {
102
+ const { recursive = true, space = null } = opts;
103
+ const results = [];
104
+ let entries;
105
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return results; }
106
+
107
+ for (const entry of entries) {
108
+ if (entry.name.startsWith('.')) continue;
109
+ const full = resolve(dir, entry.name);
110
+ const rel = relative(root, full);
111
+
112
+ if (entry.isDirectory()) {
113
+ if (space && dirname(rel) === '.' && entry.name !== space) continue;
114
+ if (recursive) results.push(...walkFiles(full, root, { ...opts, space: null }));
115
+ } else if (entry.isFile()) {
116
+ results.push({
117
+ path: rel,
118
+ name: entry.name,
119
+ size: statSync(full).size,
120
+ });
121
+ }
122
+ }
123
+ return results;
124
+ }
125
+
126
+ function fileList(root, _args, flags) {
127
+ const files = walkFiles(root, root, {
128
+ recursive: flags.recursive || flags.r || true,
129
+ space: flags.space || null,
130
+ });
131
+
132
+ if (isJsonMode(flags)) {
133
+ output({ count: files.length, files }, flags);
134
+ return;
135
+ }
136
+
137
+ if (files.length === 0) {
138
+ console.log(dim('No files found.'));
139
+ return;
140
+ }
141
+
142
+ console.log(`\n${bold(`Files (${files.length}):`)}\n`);
143
+ for (const f of files) {
144
+ const sizeStr = f.size < 1024 ? `${f.size}B` : `${(f.size / 1024).toFixed(1)}K`;
145
+ console.log(` ${f.path} ${dim(sizeStr)}`);
146
+ }
147
+ console.log();
148
+ }
149
+
150
+ function fileRead(root, filePath, flags) {
151
+ if (!filePath) {
152
+ console.error(red('Usage: mindos file read <path>'));
153
+ process.exit(1);
154
+ }
155
+ const full = resolvePath(root, filePath);
156
+ if (!existsSync(full)) {
157
+ console.error(red(`File not found: ${filePath}`));
158
+ process.exit(1);
159
+ }
160
+ const content = readFileSync(full, 'utf-8');
161
+
162
+ if (isJsonMode(flags)) {
163
+ output({ path: filePath, size: content.length, content }, flags);
164
+ return;
165
+ }
166
+ console.log(content);
167
+ }
168
+
169
+ function fileCreate(root, filePath, flags) {
170
+ if (!filePath) {
171
+ console.error(red('Usage: mindos file create <path> --content "..."'));
172
+ process.exit(1);
173
+ }
174
+ const full = resolvePath(root, filePath);
175
+ if (existsSync(full) && !flags.force) {
176
+ console.error(red(`File already exists: ${filePath}`));
177
+ console.error(dim('Use --force to overwrite.'));
178
+ process.exit(1);
179
+ }
180
+
181
+ const content = flags.content || `# ${basename(filePath, '.md')}\n`;
182
+ mkdirSync(dirname(full), { recursive: true });
183
+ writeFileSync(full, content, 'utf-8');
184
+
185
+ if (isJsonMode(flags)) {
186
+ output({ ok: true, path: filePath, size: content.length }, flags);
187
+ return;
188
+ }
189
+ console.log(`${green('✔')} Created: ${cyan(filePath)}`);
190
+ }
191
+
192
+ function fileDelete(root, filePath, flags) {
193
+ if (!filePath) {
194
+ console.error(red('Usage: mindos file delete <path>'));
195
+ process.exit(1);
196
+ }
197
+ const full = resolvePath(root, filePath);
198
+ if (!existsSync(full)) {
199
+ console.error(red(`File not found: ${filePath}`));
200
+ process.exit(1);
201
+ }
202
+
203
+ unlinkSync(full);
204
+
205
+ if (isJsonMode(flags)) {
206
+ output({ ok: true, deleted: filePath }, flags);
207
+ return;
208
+ }
209
+ console.log(`${green('✔')} Deleted: ${filePath}`);
210
+ }
211
+
212
+ function fileRename(root, oldPath, newPath, flags) {
213
+ if (!oldPath || !newPath) {
214
+ console.error(red('Usage: mindos file rename <old-path> <new-path>'));
215
+ process.exit(1);
216
+ }
217
+ const fullOld = resolvePath(root, oldPath);
218
+ const fullNew = resolvePath(root, newPath);
219
+
220
+ if (!existsSync(fullOld)) {
221
+ console.error(red(`File not found: ${oldPath}`));
222
+ process.exit(1);
223
+ }
224
+ if (existsSync(fullNew) && !flags.force) {
225
+ console.error(red(`Target already exists: ${newPath}`));
226
+ process.exit(1);
227
+ }
228
+
229
+ mkdirSync(dirname(fullNew), { recursive: true });
230
+ renameSync(fullOld, fullNew);
231
+
232
+ if (isJsonMode(flags)) {
233
+ output({ ok: true, from: oldPath, to: newPath }, flags);
234
+ return;
235
+ }
236
+ console.log(`${green('✔')} Renamed: ${oldPath} → ${cyan(newPath)}`);
237
+ }
238
+
239
+ function fileSearch(root, query, flags) {
240
+ if (!query) {
241
+ console.error(red('Usage: mindos file search <query>'));
242
+ process.exit(1);
243
+ }
244
+
245
+ const files = walkFiles(root, root);
246
+ const results = [];
247
+ const queryLower = query.toLowerCase();
248
+
249
+ for (const f of files) {
250
+ try {
251
+ const content = readFileSync(resolve(root, f.path), 'utf-8');
252
+ const lines = content.split('\n');
253
+ const matches = [];
254
+ for (let i = 0; i < lines.length; i++) {
255
+ if (lines[i].toLowerCase().includes(queryLower)) {
256
+ matches.push({ line: i + 1, text: lines[i].trim().slice(0, 120) });
257
+ }
258
+ }
259
+ if (matches.length > 0 || f.name.toLowerCase().includes(queryLower)) {
260
+ results.push({ path: f.path, matches });
261
+ }
262
+ } catch { /* skip unreadable files */ }
263
+ }
264
+
265
+ if (isJsonMode(flags)) {
266
+ output({ query, count: results.length, results }, flags);
267
+ return;
268
+ }
269
+
270
+ if (results.length === 0) {
271
+ console.log(dim(`No results for "${query}"`));
272
+ return;
273
+ }
274
+
275
+ console.log(`\n${bold(`Search: "${query}" (${results.length} files)`)}\n`);
276
+ for (const r of results) {
277
+ console.log(` ${cyan(r.path)}`);
278
+ for (const m of r.matches.slice(0, 3)) {
279
+ console.log(` ${dim(`L${m.line}:`)} ${m.text}`);
280
+ }
281
+ if (r.matches.length > 3) {
282
+ console.log(` ${dim(`...and ${r.matches.length - 3} more`)}`);
283
+ }
284
+ }
285
+ console.log();
286
+ }