@bahulam/code 0.1.12 → 0.1.14
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/package.json +1 -1
- package/src/commands/install.mjs +382 -0
- package/src/commands/plugin-manage.mjs +467 -89
- package/src/config/model-catalog-default.json +0 -4
- package/src/config/settings-loader.mjs +15 -0
- package/src/core/resume-mode.mjs +0 -1
- package/src/core/stream-client.mjs +18 -0
- package/src/core/tool-executor.mjs +47 -0
- package/src/daemon/session-core.mjs +3 -0
- package/src/local-service/agent-relay.mjs +6 -1
- package/src/local-service/file-access.mjs +116 -2
- package/src/local-service/server.mjs +209 -20
- package/src/plugins/manifest.mjs +3 -0
- package/src/plugins/npm-install.mjs +138 -0
- package/src/plugins/pi-compat/loader-hook.mjs +45 -0
- package/src/plugins/pi-compat/probe.mjs +294 -0
- package/src/plugins/pi-compat/requirements.mjs +465 -0
- package/src/plugins/pi-compat/scaffold.mjs +563 -0
- package/src/plugins/pi-compat/shim.mjs +162 -0
- package/src/plugins/pi-compose.mjs +147 -0
- package/src/plugins/preflight.mjs +29 -2
- package/src/plugins/registry.mjs +6 -0
- package/src/terminal/main.mjs +33 -6
- package/src/terminal/paste-input.mjs +23 -0
- package/src/terminal/repl.mjs +104 -30
- package/src/tools/registry.mjs +19 -0
- package/src/ui/input-dock.mjs +53 -12
- package/src/ui/text-layout.mjs +4 -3
|
@@ -33,9 +33,6 @@
|
|
|
33
33
|
{"value": "qwen/qwq-32b", "label": "QwQ 32B", "provider": "qwen", "inputCost": 0.2, "outputCost": 0.2, "context": 131072, "maxOutput": 32768, "supportsTools": true, "supportsReasoning": true, "harnessValidated": false},
|
|
34
34
|
{"value": "minimax/minimax-m3", "label": "MiniMax M3", "provider": "minimax", "inputCost": 0.5, "outputCost": 2.0, "context": 1048576, "maxOutput": 512000, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
35
35
|
{"value": "minimax/minimax-m2.1", "label": "MiniMax M2.1", "provider": "minimax", "inputCost": 0.3, "outputCost": 1.1, "context": 204800, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
36
|
-
{"value": "xiaomi/mimo-v2.5-pro", "label": "MiMo V2.5 Pro", "provider": "xiaomi", "inputCost": 0.5, "outputCost": 2.0, "context": 1050000, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
37
|
-
{"value": "xiaomi/mimo-v2.5", "label": "MiMo V2.5", "provider": "xiaomi", "inputCost": 0.3, "outputCost": 1.0, "context": 1050000, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"Xiaomi\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
38
|
-
{"value": "xiaomi/mimo-v2-flash", "label": "MiMo V2 Flash", "provider": "xiaomi", "inputCost": 0.1, "outputCost": 0.3, "context": 131072, "maxOutput": 16384, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
39
36
|
{"value": "moonshotai/kimi-k2.5", "label": "Kimi K2.5", "provider": "kimi", "inputCost": 0.44, "outputCost": 2.0, "context": 262144, "maxOutput": 262144, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
40
37
|
{"value": "moonshotai/kimi-k2", "label": "Kimi K2", "provider": "kimi", "inputCost": 0.44, "outputCost": 2.0, "context": 131072, "maxOutput": 100352, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
41
38
|
{"value": "moonshotai/kimi-k2-instruct", "label": "Kimi K2 Instruct", "provider": "kimi", "inputCost": 0.44, "outputCost": 2.0, "context": 131072, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
@@ -60,7 +57,6 @@
|
|
|
60
57
|
{"value": "stealth/ox-alpha", "label": "Stealth OX Alpha", "provider": "stealth", "inputCost": 0.3, "outputCost": 1.0, "context": 1048576, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"Stealth\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
61
58
|
{"value": "upstage/solar-pro4", "label": "Solar Pro 4", "provider": "upstage", "inputCost": 0.3, "outputCost": 1.0, "context": 524288, "maxOutput": 131072, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"Upstage\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
62
59
|
{"value": "qwen/qwen3-coder:free", "label": "Qwen3 Coder (Free)", "provider": "qwen", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
63
|
-
{"value": "xiaomi/mimo-v2-flash:free", "label": "MiMo V2 Flash (Free)", "provider": "xiaomi", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 16384, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
64
60
|
{"value": "meta-llama/llama-3.3-70b-instruct:free", "label": "Llama 3.3 70B (Free)", "provider": "meta", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 8192, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
65
61
|
{"value": "qwen/qwen3-8b:free", "label": "Qwen3 8B (Free)", "provider": "qwen", "inputCost": 0, "outputCost": 0, "context": 131072, "maxOutput": 8192, "supportsTools": true, "supportsReasoning": false, "harnessValidated": false},
|
|
66
62
|
{"value": "nvidia/nemotron-3-ultra-550b-a55b:free", "label": "Nemotron 3 Ultra 550B (Free)", "provider": "nvidia", "inputCost": 0, "outputCost": 0, "context": 1000000, "maxOutput": 65536, "supportsTools": true, "supportsReasoning": false, "harnessValidated": true, "cacheProfile": "{\"type\": \"prefix_hash\", \"provider_hint\": \"NVIDIA\"}", "platformAccessTier": ["pro", "tier_49", "tier_99"]},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { deepMerge } from '../core/policy-resolver.mjs';
|
|
4
5
|
|
|
@@ -26,12 +27,26 @@ function readJson(filePath) {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// Global settings live alongside global plugins (~/.bahulam/), so an
|
|
31
|
+
// allowlist that applies to a globally-installed plugin belongs here.
|
|
32
|
+
// $BAHULAM_HOME overrides the default so tests and one-off installs
|
|
33
|
+
// don't touch the real home directory.
|
|
34
|
+
function globalSettingsPath() {
|
|
35
|
+
const home = process.env.BAHULAM_HOME || path.join(os.homedir(), '.bahulam');
|
|
36
|
+
return path.join(home, 'settings.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export function loadBahulamSettings({ cwd = process.cwd() } = {}) {
|
|
30
40
|
const base = path.join(cwd, '.bahulam');
|
|
31
41
|
const layers = [
|
|
32
42
|
{ name: 'default', path: null, data: DEFAULT_BAHULAM_SETTINGS },
|
|
33
43
|
];
|
|
44
|
+
// Merge order (later overrides earlier): default → global → project → local.
|
|
45
|
+
// Global keeps values that stay constant across projects (plugins live
|
|
46
|
+
// in ~/.bahulam, so plugin allowlists sit here). Project & local remain
|
|
47
|
+
// the last word so a repo can tighten or loosen without touching global.
|
|
34
48
|
for (const [name, file] of [
|
|
49
|
+
['global', globalSettingsPath()],
|
|
35
50
|
['project', path.join(base, 'settings.json')],
|
|
36
51
|
['local', path.join(base, 'settings.local.json')],
|
|
37
52
|
]) {
|
package/src/core/resume-mode.mjs
CHANGED
|
@@ -171,6 +171,7 @@ export class BahulamStreamClient {
|
|
|
171
171
|
this._pauseWaiters = new Set();
|
|
172
172
|
this._abort = null;
|
|
173
173
|
this._toolAbort = null;
|
|
174
|
+
this._sawComplete = false;
|
|
174
175
|
|
|
175
176
|
// Transport mode:
|
|
176
177
|
// 'remote' → cloud backend runs the agent loop server-side.
|
|
@@ -352,6 +353,7 @@ export class BahulamStreamClient {
|
|
|
352
353
|
this._cancelled = false;
|
|
353
354
|
this.currentTaskId = null;
|
|
354
355
|
this._firstEventTimedOut = false;
|
|
356
|
+
this._sawComplete = false;
|
|
355
357
|
|
|
356
358
|
// Bundled mode: spawn the local Python runtime on first turn.
|
|
357
359
|
// After this, this.baseUrl points at http://127.0.0.1:<random-port>
|
|
@@ -500,6 +502,21 @@ export class BahulamStreamClient {
|
|
|
500
502
|
if (this._cancelled) {
|
|
501
503
|
return;
|
|
502
504
|
}
|
|
505
|
+
if (this._sawComplete) {
|
|
506
|
+
telemetry.track('stream.post_complete_error_ignored', {
|
|
507
|
+
task_id: this.currentTaskId || null,
|
|
508
|
+
last_event_id: this.lastEventId || null,
|
|
509
|
+
message: err?.message || 'stream closed after complete',
|
|
510
|
+
error_code: err?.cause?.code || err?.code || '',
|
|
511
|
+
});
|
|
512
|
+
transportDebug('execute.post_complete_error_ignored', {
|
|
513
|
+
task_id: this.currentTaskId || null,
|
|
514
|
+
last_event_id: this.lastEventId || null,
|
|
515
|
+
error: err?.message || 'stream closed after complete',
|
|
516
|
+
code: err?.cause?.code || err?.code || '',
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
503
520
|
if (this._firstEventTimedOut) {
|
|
504
521
|
yield {
|
|
505
522
|
type: EVENT_TYPES.ERROR,
|
|
@@ -579,6 +596,7 @@ export class BahulamStreamClient {
|
|
|
579
596
|
}
|
|
580
597
|
|
|
581
598
|
if (event === EVENT_TYPES.COMPLETE) {
|
|
599
|
+
this._sawComplete = true;
|
|
582
600
|
this._persistMemoryFactsFromComplete(data);
|
|
583
601
|
}
|
|
584
602
|
|
|
@@ -850,6 +850,53 @@ export function createToolExecutor({
|
|
|
850
850
|
const name = String(toolDef.name || '').trim();
|
|
851
851
|
if (!name || toolMap[name]) continue;
|
|
852
852
|
const pluginName = toolDef._plugin_name || toolDef.plugin_name || null;
|
|
853
|
+
if (toolDef._composed?.kind === 'pi') {
|
|
854
|
+
// Composed pi tools resolve at invocation time: look up the
|
|
855
|
+
// installed pi package's directory, load the specific handler
|
|
856
|
+
// via the shim-backed probe, invoke, return the result. Handler
|
|
857
|
+
// cache is per-session (per tool name) to amortize the ~50ms
|
|
858
|
+
// child-process overhead on repeat calls.
|
|
859
|
+
let _piInvokeP = null;
|
|
860
|
+
registerPluginTool(name, async (args, options = {}) => {
|
|
861
|
+
try {
|
|
862
|
+
if (!_piInvokeP) {
|
|
863
|
+
const { loadPiToolHandler } = await import('../plugins/pi-compat/probe.mjs');
|
|
864
|
+
const packageName = toolDef._composed.package_name;
|
|
865
|
+
const originalName = toolDef._composed.original_name;
|
|
866
|
+
const { bahulamHome } = await import('./paths.mjs');
|
|
867
|
+
const piBaseDir = path.join(bahulamHome(), 'plugins-pi');
|
|
868
|
+
const piDir = path.join(piBaseDir, packageName.replace(/[/@]/g, '_'));
|
|
869
|
+
if (!fs.existsSync(piDir)) {
|
|
870
|
+
return {
|
|
871
|
+
success: false,
|
|
872
|
+
output: `Composed pi tool '${name}' unavailable: pi package ${packageName} is not installed. Run \`bahulam plugin install pi:${packageName}\`.`,
|
|
873
|
+
_tool: name,
|
|
874
|
+
_plugin: pluginName,
|
|
875
|
+
_composed: toolDef._composed,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
_piInvokeP = loadPiToolHandler(piDir, originalName, { pluginName: packageName });
|
|
879
|
+
}
|
|
880
|
+
const invoke = await _piInvokeP;
|
|
881
|
+
const result = await invoke(args || {});
|
|
882
|
+
return {
|
|
883
|
+
...(result && typeof result === 'object' ? result : { success: true, output: String(result) }),
|
|
884
|
+
_tool: name,
|
|
885
|
+
_plugin: pluginName,
|
|
886
|
+
_composed: toolDef._composed,
|
|
887
|
+
};
|
|
888
|
+
} catch (err) {
|
|
889
|
+
return {
|
|
890
|
+
success: false,
|
|
891
|
+
output: `Composed pi tool '${name}' failed: ${err.message}`,
|
|
892
|
+
_tool: name,
|
|
893
|
+
_plugin: pluginName,
|
|
894
|
+
_composed: toolDef._composed,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}, { pluginName, source: 'pi', composed: toolDef._composed });
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
853
900
|
registerPluginTool(name, async (args, options = {}) => {
|
|
854
901
|
const handler = await loadPluginTool(toolDef._plugin_dir, toolDef.tool);
|
|
855
902
|
if (!handler) {
|
|
@@ -99,10 +99,13 @@ export const DAEMON_OWNED_FIELDS = Object.freeze([
|
|
|
99
99
|
* • It's input state tied to a keyboard (`inputHistory`).
|
|
100
100
|
* • It's a rendering flag whose value depends on the current visible
|
|
101
101
|
* transcript, not the session's actual state (`inSubAgent`).
|
|
102
|
+
* • It's an active renderer collection that does not JSON round-trip
|
|
103
|
+
* safely (`activeSubAgentRuns` is a Map).
|
|
102
104
|
*/
|
|
103
105
|
export const CLIENT_OWNED_SESSION_FIELDS = Object.freeze([
|
|
104
106
|
'inputHistory',
|
|
105
107
|
'inSubAgent',
|
|
108
|
+
'activeSubAgentRuns',
|
|
106
109
|
'creditsLowWarned',
|
|
107
110
|
'msgsLowWarned',
|
|
108
111
|
'_lastEmittedThinking',
|
|
@@ -748,6 +748,8 @@ export class LocalAgentRelay {
|
|
|
748
748
|
lines.push(
|
|
749
749
|
'Use analyze_image(path=..., question=...) for images.',
|
|
750
750
|
'Use read_table(path=...) for CSV/TSV/Excel-style tables.',
|
|
751
|
+
'Use browser/web inspection tools for HTML and browser-rendered web assets when available.',
|
|
752
|
+
'For video and audio, inspect file metadata and ask for a transcript or frame extraction when content analysis is needed.',
|
|
751
753
|
'Use read_attachment(path=...) for text, PDFs, Markdown, JSON/YAML, and Jupyter notebooks.',
|
|
752
754
|
'For unsupported binary files, inspect metadata or ask before attempting lossy conversion.',
|
|
753
755
|
);
|
|
@@ -902,7 +904,10 @@ function attachmentToolHint(file) {
|
|
|
902
904
|
const ext = String(file?.path || file?.name || '').split('.').pop().toLowerCase();
|
|
903
905
|
if (kind === 'image' || mime.startsWith('image/')) return 'analyze_image';
|
|
904
906
|
if (kind === 'spreadsheet' || kind === 'table' || ['csv', 'tsv', 'xlsx', 'xls', 'ods'].includes(ext)) return 'read_table';
|
|
905
|
-
if (
|
|
907
|
+
if (kind === 'web' || ['html', 'htm'].includes(ext)) return 'web_preview';
|
|
908
|
+
if (kind === 'video' || mime.startsWith('video/')) return 'media_metadata';
|
|
909
|
+
if (kind === 'audio' || mime.startsWith('audio/')) return 'media_metadata';
|
|
910
|
+
if (['pdf', 'markdown', 'text', 'code', 'config', 'notebook'].includes(kind) || ['txt', 'md', 'mdx', 'pdf', 'json', 'yaml', 'yml', 'toml', 'xml', 'ipynb', 'log', 'rst', 'sql', 'sh'].includes(ext)) return 'read_attachment';
|
|
906
911
|
return '';
|
|
907
912
|
}
|
|
908
913
|
|
|
@@ -6,6 +6,7 @@ import * as fs from 'node:fs';
|
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
|
|
8
8
|
const DEFAULT_MAX_ENTRIES = 300;
|
|
9
|
+
const DEFAULT_MAX_SEARCH_RESULTS = 200;
|
|
9
10
|
const DEFAULT_MAX_TEXT_BYTES = 256 * 1024;
|
|
10
11
|
export const DEFAULT_MAX_RAW_BYTES = 100 * 1024 * 1024;
|
|
11
12
|
|
|
@@ -42,8 +43,8 @@ const EXTENSION_FILE_TYPES = new Map([
|
|
|
42
43
|
['env', { kind: 'config', language: 'plaintext', label: 'Environment', textLike: true }],
|
|
43
44
|
['properties', { kind: 'config', language: 'plaintext', label: 'Properties', textLike: true }],
|
|
44
45
|
['xml', { kind: 'code', language: 'xml', label: 'XML', textLike: true }],
|
|
45
|
-
['html', { kind: '
|
|
46
|
-
['htm', { kind: '
|
|
46
|
+
['html', { kind: 'web', language: 'html', label: 'HTML', textLike: true }],
|
|
47
|
+
['htm', { kind: 'web', language: 'html', label: 'HTML', textLike: true }],
|
|
47
48
|
['css', { kind: 'code', language: 'css', label: 'CSS', textLike: true }],
|
|
48
49
|
['scss', { kind: 'code', language: 'scss', label: 'SCSS', textLike: true }],
|
|
49
50
|
['sass', { kind: 'code', language: 'scss', label: 'Sass', textLike: true }],
|
|
@@ -110,6 +111,18 @@ const EXTENSION_FILE_TYPES = new Map([
|
|
|
110
111
|
['svg', { kind: 'image', language: 'xml', label: 'SVG Image', textLike: false }],
|
|
111
112
|
['bmp', { kind: 'image', language: null, label: 'Bitmap Image', textLike: false }],
|
|
112
113
|
['ico', { kind: 'image', language: null, label: 'Icon', textLike: false }],
|
|
114
|
+
['avif', { kind: 'image', language: null, label: 'AVIF Image', textLike: false }],
|
|
115
|
+
['mp4', { kind: 'video', language: null, label: 'MP4 Video', textLike: false }],
|
|
116
|
+
['webm', { kind: 'video', language: null, label: 'WebM Video', textLike: false }],
|
|
117
|
+
['mov', { kind: 'video', language: null, label: 'QuickTime Video', textLike: false }],
|
|
118
|
+
['m4v', { kind: 'video', language: null, label: 'M4V Video', textLike: false }],
|
|
119
|
+
['ogv', { kind: 'video', language: null, label: 'Ogg Video', textLike: false }],
|
|
120
|
+
['mp3', { kind: 'audio', language: null, label: 'MP3 Audio', textLike: false }],
|
|
121
|
+
['m4a', { kind: 'audio', language: null, label: 'M4A Audio', textLike: false }],
|
|
122
|
+
['wav', { kind: 'audio', language: null, label: 'WAV Audio', textLike: false }],
|
|
123
|
+
['ogg', { kind: 'audio', language: null, label: 'Ogg Audio', textLike: false }],
|
|
124
|
+
['flac', { kind: 'audio', language: null, label: 'FLAC Audio', textLike: false }],
|
|
125
|
+
['aac', { kind: 'audio', language: null, label: 'AAC Audio', textLike: false }],
|
|
113
126
|
['pdf', { kind: 'pdf', language: null, label: 'PDF', textLike: false }],
|
|
114
127
|
['xlsx', { kind: 'spreadsheet', language: null, label: 'Excel Workbook', textLike: false }],
|
|
115
128
|
['xls', { kind: 'spreadsheet', language: null, label: 'Excel Workbook', textLike: false }],
|
|
@@ -165,12 +178,16 @@ export function listWorkspacePath(session, requestedPath = '.', { maxEntries = D
|
|
|
165
178
|
try {
|
|
166
179
|
childStat = fs.statSync(fullPath);
|
|
167
180
|
} catch {}
|
|
181
|
+
const file = childStat?.isFile() ? describeFile(root, fullPath, childStat) : null;
|
|
168
182
|
return {
|
|
169
183
|
name: entry.name,
|
|
170
184
|
path: normalizeRelative(root, fullPath),
|
|
171
185
|
type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other',
|
|
172
186
|
size: childStat?.size ?? null,
|
|
173
187
|
updated_at: childStat?.mtime ? childStat.mtime.toISOString() : null,
|
|
188
|
+
kind: file?.kind ?? (entry.isDirectory() ? 'directory' : null),
|
|
189
|
+
viewer: file?.viewer ?? (entry.isDirectory() ? 'directory' : null),
|
|
190
|
+
label: file?.label ?? null,
|
|
174
191
|
};
|
|
175
192
|
});
|
|
176
193
|
|
|
@@ -201,6 +218,88 @@ export function listWorkspacePath(session, requestedPath = '.', { maxEntries = D
|
|
|
201
218
|
};
|
|
202
219
|
}
|
|
203
220
|
|
|
221
|
+
export function searchWorkspaceFiles(session, {
|
|
222
|
+
query = '',
|
|
223
|
+
kinds = [],
|
|
224
|
+
maxResults = DEFAULT_MAX_SEARCH_RESULTS,
|
|
225
|
+
} = {}) {
|
|
226
|
+
const root = fs.realpathSync(session.root_path);
|
|
227
|
+
const wanted = new Set((Array.isArray(kinds) ? kinds : String(kinds || '').split(','))
|
|
228
|
+
.map((kind) => String(kind || '').trim().toLowerCase())
|
|
229
|
+
.filter(Boolean)
|
|
230
|
+
.filter((kind) => kind !== 'all'));
|
|
231
|
+
const term = String(query || '').trim().toLowerCase();
|
|
232
|
+
const limit = Math.max(1, Math.min(1000, Number(maxResults) || DEFAULT_MAX_SEARCH_RESULTS));
|
|
233
|
+
const results = [];
|
|
234
|
+
|
|
235
|
+
function visit(dir) {
|
|
236
|
+
if (results.length >= limit) return;
|
|
237
|
+
let entries = [];
|
|
238
|
+
try {
|
|
239
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
240
|
+
} catch {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
entries.sort((a, b) => {
|
|
244
|
+
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
|
|
245
|
+
return a.name.localeCompare(b.name);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (results.length >= limit) break;
|
|
250
|
+
if (shouldHideEntry(entry.name)) continue;
|
|
251
|
+
const fullPath = path.join(dir, entry.name);
|
|
252
|
+
let stat = null;
|
|
253
|
+
try {
|
|
254
|
+
stat = fs.statSync(fullPath);
|
|
255
|
+
} catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const rel = normalizeRelative(root, fullPath);
|
|
259
|
+
if (entry.isDirectory()) {
|
|
260
|
+
if ((!term || entry.name.toLowerCase().includes(term) || rel.toLowerCase().includes(term))
|
|
261
|
+
&& (!wanted.size || wanted.has('directory'))) {
|
|
262
|
+
results.push({
|
|
263
|
+
name: entry.name,
|
|
264
|
+
path: rel,
|
|
265
|
+
type: 'directory',
|
|
266
|
+
size: null,
|
|
267
|
+
updated_at: stat.mtime.toISOString(),
|
|
268
|
+
kind: 'directory',
|
|
269
|
+
viewer: 'directory',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
visit(fullPath);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!entry.isFile()) continue;
|
|
276
|
+
const file = describeFile(root, fullPath, stat);
|
|
277
|
+
if (wanted.size && !wanted.has(file.kind) && !wanted.has(file.viewer)) continue;
|
|
278
|
+
if (term && !entry.name.toLowerCase().includes(term) && !rel.toLowerCase().includes(term)) continue;
|
|
279
|
+
results.push({
|
|
280
|
+
name: entry.name,
|
|
281
|
+
path: rel,
|
|
282
|
+
type: 'file',
|
|
283
|
+
size: stat.size,
|
|
284
|
+
updated_at: stat.mtime.toISOString(),
|
|
285
|
+
kind: file.kind,
|
|
286
|
+
viewer: file.viewer,
|
|
287
|
+
label: file.label,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
visit(root);
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
type: 'search',
|
|
296
|
+
query: term,
|
|
297
|
+
kinds: [...wanted],
|
|
298
|
+
results,
|
|
299
|
+
truncated: results.length >= limit,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
204
303
|
export function readWorkspaceFile(session, requestedPath, { maxBytes = DEFAULT_MAX_TEXT_BYTES } = {}) {
|
|
205
304
|
const target = resolveWorkspacePath(session, requestedPath);
|
|
206
305
|
const stat = fs.statSync(target);
|
|
@@ -252,6 +351,18 @@ export function contentTypeForPath(filePath) {
|
|
|
252
351
|
case 'svg': return 'image/svg+xml';
|
|
253
352
|
case 'bmp': return 'image/bmp';
|
|
254
353
|
case 'ico': return 'image/x-icon';
|
|
354
|
+
case 'avif': return 'image/avif';
|
|
355
|
+
case 'mp4': return 'video/mp4';
|
|
356
|
+
case 'webm': return 'video/webm';
|
|
357
|
+
case 'mov': return 'video/quicktime';
|
|
358
|
+
case 'm4v': return 'video/x-m4v';
|
|
359
|
+
case 'ogv': return 'video/ogg';
|
|
360
|
+
case 'mp3': return 'audio/mpeg';
|
|
361
|
+
case 'm4a': return 'audio/mp4';
|
|
362
|
+
case 'wav': return 'audio/wav';
|
|
363
|
+
case 'ogg': return 'audio/ogg';
|
|
364
|
+
case 'flac': return 'audio/flac';
|
|
365
|
+
case 'aac': return 'audio/aac';
|
|
255
366
|
case 'pdf': return 'application/pdf';
|
|
256
367
|
case 'txt':
|
|
257
368
|
case 'log':
|
|
@@ -351,7 +462,10 @@ function viewerForKind(kind) {
|
|
|
351
462
|
case 'mermaid': return 'mermaid';
|
|
352
463
|
case 'drawio': return 'drawio';
|
|
353
464
|
case 'image': return 'image';
|
|
465
|
+
case 'video': return 'video';
|
|
466
|
+
case 'audio': return 'audio';
|
|
354
467
|
case 'pdf': return 'pdf';
|
|
468
|
+
case 'web': return 'web';
|
|
355
469
|
case 'spreadsheet': return 'spreadsheet';
|
|
356
470
|
case 'document': return 'document';
|
|
357
471
|
case 'presentation': return 'presentation';
|