@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/README.md +58 -46
- package/README_zh.md +58 -46
- package/app/app/api/file/import/route.ts +0 -2
- package/app/app/api/setup/route.ts +2 -0
- package/app/components/Breadcrumb.tsx +1 -1
- package/app/components/FileTree.tsx +14 -1
- package/app/components/HomeContent.tsx +1 -1
- package/app/components/RightAskPanel.tsx +17 -10
- package/app/components/SidebarLayout.tsx +4 -2
- package/app/components/ask/AskContent.tsx +5 -5
- package/app/components/ask/FileChip.tsx +1 -1
- package/app/components/ask/MessageList.tsx +1 -1
- package/app/hooks/useAskPanel.ts +7 -3
- package/app/hooks/useFileImport.ts +1 -1
- package/app/lib/agent/tools.ts +1 -1
- package/app/lib/core/fs-ops.ts +3 -2
- package/app/lib/fs.ts +28 -11
- package/app/lib/i18n-en.ts +2 -0
- package/app/lib/i18n-zh.ts +2 -0
- package/app/lib/settings.ts +1 -1
- package/bin/cli.js +38 -20
- package/bin/commands/ask.js +101 -0
- package/bin/commands/file.js +286 -0
- package/bin/commands/space.js +167 -0
- package/bin/commands/status.js +69 -0
- package/bin/lib/command.js +156 -0
- package/mcp/dist/index.cjs +1 -1
- package/mcp/src/index.ts +1 -1
- package/package.json +1 -1
- package/skills/mindos/SKILL.md +2 -2
- package/skills/mindos-zh/SKILL.md +2 -2
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
|
-
|
|
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
|
|
package/app/lib/i18n-en.ts
CHANGED
package/app/lib/i18n-zh.ts
CHANGED
package/app/lib/settings.ts
CHANGED
|
@@ -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 ===
|
|
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('
|
|
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
|
|
1316
|
-
${row('mindos start',
|
|
1317
|
-
${row('mindos
|
|
1318
|
-
${row('mindos
|
|
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
|
|
1324
|
-
${row('mindos
|
|
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
|
|
1329
|
-
${row('mindos token', 'Show
|
|
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 <
|
|
1351
|
+
${row('mindos gateway <sub>', 'Manage service (install/start/stop/status/logs)')}
|
|
1336
1352
|
|
|
1337
1353
|
${bold('Config & Diagnostics:')}
|
|
1338
|
-
${row('mindos config <
|
|
1339
|
-
${row('mindos doctor', 'Health check
|
|
1340
|
-
${row('mindos
|
|
1341
|
-
${row('mindos
|
|
1342
|
-
|
|
1343
|
-
${
|
|
1344
|
-
${row('
|
|
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
|
+
}
|