thebird 1.2.97 → 1.2.98
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/docs/agent-chat.js +0 -4
- package/docs/app.js +6 -6
- package/docs/chat-providers.js +1 -3
- package/package.json +1 -1
- package/docs/kilo-fs-mirror.js +0 -22
- package/docs/kilo-http-stream.js +0 -129
- package/start-kilo.js +0 -68
package/docs/agent-chat.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { streamGemini, streamOpenAI } from './vendor/thebird-browser.js';
|
|
2
|
-
import { streamKiloHTTP } from './kilo-http-stream.js';
|
|
3
2
|
|
|
4
3
|
function idbRead(path) {
|
|
5
4
|
const snap = window.__debug.idbSnapshot;
|
|
@@ -91,9 +90,6 @@ function buildStream(provider) {
|
|
|
91
90
|
if (provider.type === 'gemini') {
|
|
92
91
|
return streamGemini({ model: provider.model, messages: provider.messages, tools: TOOLS, apiKey: provider.apiKey, maxOutputTokens: 8192 }).fullStream;
|
|
93
92
|
}
|
|
94
|
-
if (provider.type === 'kilo' || provider.type === 'opencode') {
|
|
95
|
-
return streamKiloHTTP({ url: provider.baseUrl, model: provider.model, messages: provider.messages, providerType: provider.type });
|
|
96
|
-
}
|
|
97
93
|
const url = (provider.baseUrl || '').replace(/\/$/, '') + '/chat/completions';
|
|
98
94
|
return streamOpenAI({ url, apiKey: provider.apiKey, messages: provider.messages, model: provider.model, tools: TOOLS, maxOutputTokens: 8192 });
|
|
99
95
|
}
|
package/docs/app.js
CHANGED
|
@@ -8,7 +8,7 @@ class BirdChat extends HTMLElement {
|
|
|
8
8
|
constructor() {
|
|
9
9
|
super();
|
|
10
10
|
const rawSaved = localStorage.getItem('provider_type') || 'gemini';
|
|
11
|
-
const savedProvider = PROVIDERS[rawSaved] ? rawSaved : (
|
|
11
|
+
const savedProvider = PROVIDERS[rawSaved] ? rawSaved : (['acp','kilohttp','kilo','opencode','acp2openai'].includes(rawSaved) ? 'acptoapi' : 'gemini');
|
|
12
12
|
const savedBaseUrl = localStorage.getItem('provider_base_url') || PROVIDERS[savedProvider]?.baseUrl || '';
|
|
13
13
|
this.state = {
|
|
14
14
|
messages: [], streaming: false,
|
|
@@ -29,7 +29,7 @@ class BirdChat extends HTMLElement {
|
|
|
29
29
|
connectedCallback() {
|
|
30
30
|
this.render();
|
|
31
31
|
Object.assign(window.__debug, { acp: { baseUrl: this.state.baseUrl, provider: this.state.providerType } });
|
|
32
|
-
if (this.state.apiKey || ['
|
|
32
|
+
if (this.state.apiKey || ['acptoapi','ollama','lmstudio'].includes(this.state.providerType)) this.loadModels();
|
|
33
33
|
this.statsTimer = setInterval(() => this.updateStats(), 250);
|
|
34
34
|
}
|
|
35
35
|
disconnectedCallback() { if (this.statsTimer) clearInterval(this.statsTimer); }
|
|
@@ -62,20 +62,20 @@ class BirdChat extends HTMLElement {
|
|
|
62
62
|
localStorage.setItem('provider_base_url', baseUrl);
|
|
63
63
|
localStorage.setItem('provider_model', model);
|
|
64
64
|
this.setState({ providerType: type, baseUrl, model, models: [], apiKey: localStorage.getItem('provider_api_key') || '' });
|
|
65
|
-
if (['
|
|
65
|
+
if (['acptoapi','ollama','lmstudio'].includes(type)) this.loadModels();
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
renderBaseUrlInput() {
|
|
69
69
|
const { providerType, baseUrl } = this.state;
|
|
70
70
|
if (!['custom','kilo','opencode','acp2openai','ollama'].includes(providerType)) return null;
|
|
71
|
-
const phMap = {
|
|
71
|
+
const phMap = { acptoapi: 'http://localhost:4800/v1', ollama: 'http://localhost:11434/v1', lmstudio: 'http://localhost:1234/v1', custom: 'https://your-endpoint/v1' };
|
|
72
72
|
const ph = phMap[providerType] || phMap.custom;
|
|
73
73
|
return html`<input type="text" class="tui-input" style="flex:1;min-width:140px" placeholder=${ph} value=${baseUrl}
|
|
74
74
|
onchange=${e => { localStorage.setItem('provider_base_url', e.target.value); this.setState({ baseUrl: e.target.value }); }} />`;
|
|
75
75
|
}
|
|
76
76
|
renderApiKeyInput() {
|
|
77
77
|
const { providerType, apiKey } = this.state;
|
|
78
|
-
if (['
|
|
78
|
+
if (['acptoapi','ollama','lmstudio'].includes(providerType)) return null;
|
|
79
79
|
const provDef = PROVIDERS[providerType] || PROVIDERS.custom;
|
|
80
80
|
return html`<input id="api-key-input" type="password" class="tui-input" style="flex:1;min-width:120px" placeholder=${provDef.keyPlaceholder} value=${apiKey}
|
|
81
81
|
onchange=${e => { const v = e.target.value.trim(); localStorage.setItem('provider_api_key', v); this.setState({ apiKey: v }); if (v) this.loadModels(); }} />`;
|
|
@@ -130,7 +130,7 @@ class BirdChat extends HTMLElement {
|
|
|
130
130
|
const text = input?.value.trim();
|
|
131
131
|
if (!text || this.state.streaming) return;
|
|
132
132
|
const { apiKey, model, providerType, baseUrl } = this.state;
|
|
133
|
-
if (!apiKey && !['
|
|
133
|
+
if (!apiKey && !['acptoapi','ollama','lmstudio'].includes(providerType)) { this.setState({ status: 'Enter an API key above.' }); return; }
|
|
134
134
|
input.value = '';
|
|
135
135
|
input.style.height = 'auto';
|
|
136
136
|
const normalizedMessages = [...this.state.messages, { role: 'user', content: text }].map(m => ({
|
package/docs/chat-providers.js
CHANGED
|
@@ -23,9 +23,7 @@ export const PROVIDERS = {
|
|
|
23
23
|
github: { label: 'GitHub Models', baseUrl: 'https://models.github.ai/inference', keyPlaceholder: 'GITHUB_TOKEN', models: ['openai/gpt-4.1', 'openai/gpt-4o-mini', 'meta/llama-3.3-70b-instruct', 'deepseek/deepseek-v3'] },
|
|
24
24
|
vercel: { label: 'Vercel AI Gateway', baseUrl: 'https://ai-gateway.vercel.sh/v1', keyPlaceholder: 'AI_GATEWAY_API_KEY', models: ['anthropic/claude-sonnet-4.5', 'openai/gpt-4.1', 'google/gemini-2.5-flash', 'xai/grok-code-fast-1'] },
|
|
25
25
|
cohere: { label: 'Cohere', baseUrl: 'https://api.cohere.com/compatibility/v1', keyPlaceholder: 'COHERE_API_KEY', models: ['command-a-03-2025', 'command-r-plus-08-2024', 'command-r-08-2024'] },
|
|
26
|
-
|
|
27
|
-
opencode: { label: 'opencode (zen)', baseUrl: 'http://localhost:4790', keyPlaceholder: '(needs opencode auth login)', models: ['minimax-m2.5-free', 'nemotron-3-super-free'] },
|
|
28
|
-
acp2openai: { label: 'acp2openai (OpenAI-compat)', baseUrl: 'http://localhost:4800/v1', keyPlaceholder: '(no key needed)', models: ['kilo/x-ai/grok-code-fast-1:optimized:free', 'kilo/kilo-auto/free', 'kilo/openrouter/free', 'opencode/minimax-m2.5-free'] },
|
|
26
|
+
acptoapi: { label: 'acptoapi (Kilo/opencode via OpenAI-compat)', baseUrl: 'http://localhost:4800/v1', keyPlaceholder: '(run: npx acptoapi)', models: ['kilo/x-ai/grok-code-fast-1:optimized:free', 'kilo/kilo-auto/free', 'kilo/openrouter/free', 'opencode/minimax-m2.5-free'] },
|
|
29
27
|
custom: { label: 'Custom (OpenAI-compat)', baseUrl: '', keyPlaceholder: 'API_KEY', models: [] },
|
|
30
28
|
};
|
|
31
29
|
|
package/package.json
CHANGED
package/docs/kilo-fs-mirror.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export async function mirrorFromSandbox(fsBase) {
|
|
2
|
-
try {
|
|
3
|
-
const listRes = await fetch(fsBase + '/__list');
|
|
4
|
-
if (!listRes.ok) return [];
|
|
5
|
-
const relFiles = await listRes.json();
|
|
6
|
-
const snap = window.__debug.idbSnapshot || (window.__debug.idbSnapshot = {});
|
|
7
|
-
const mirrored = [];
|
|
8
|
-
for (const rel of relFiles) {
|
|
9
|
-
const r = await fetch(fsBase + '/' + rel);
|
|
10
|
-
if (!r.ok) continue;
|
|
11
|
-
const content = await r.text();
|
|
12
|
-
if (snap[rel] !== content) { snap[rel] = content; mirrored.push(rel); }
|
|
13
|
-
}
|
|
14
|
-
if (mirrored.length) {
|
|
15
|
-
window.__debug.idbPersist?.();
|
|
16
|
-
window.__debug.shell?.onPreviewWrite?.();
|
|
17
|
-
window.showPreview?.();
|
|
18
|
-
window.refreshPreview?.();
|
|
19
|
-
}
|
|
20
|
-
return mirrored;
|
|
21
|
-
} catch (e) { return []; }
|
|
22
|
-
}
|
package/docs/kilo-http-stream.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { mirrorFromSandbox } from './kilo-fs-mirror.js';
|
|
2
|
-
|
|
3
|
-
const PART_HANDLERS = {
|
|
4
|
-
text: (part, st, emit) => {
|
|
5
|
-
const prior = st.textByPart.get(part.id) || '';
|
|
6
|
-
const txt = part.text || '';
|
|
7
|
-
if (txt.length > prior.length) {
|
|
8
|
-
emit({ type: 'text-delta', textDelta: txt.slice(prior.length) });
|
|
9
|
-
st.textByPart.set(part.id, txt);
|
|
10
|
-
}
|
|
11
|
-
},
|
|
12
|
-
reasoning: (part, st, emit) => {
|
|
13
|
-
const prior = st.reasonByPart.get(part.id) || '';
|
|
14
|
-
const txt = part.text || '';
|
|
15
|
-
if (txt.length > prior.length) {
|
|
16
|
-
emit({ type: 'reasoning-delta', textDelta: txt.slice(prior.length) });
|
|
17
|
-
st.reasonByPart.set(part.id, txt);
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
tool: (part, st, emit) => {
|
|
21
|
-
const sig = part.id + ':' + (part.state?.status || '') + ':' + JSON.stringify(part.state?.input || '').length + ':' + (part.state?.output ? 1 : 0);
|
|
22
|
-
if (st.seenTool.has(sig)) return;
|
|
23
|
-
st.seenTool.add(sig);
|
|
24
|
-
emit({ type: 'tool-event', toolName: part.tool || part.state?.tool, status: part.state?.status, input: part.state?.input, output: part.state?.output, error: part.state?.error, id: part.id });
|
|
25
|
-
},
|
|
26
|
-
file: (part, st, emit) => {
|
|
27
|
-
if (st.seenFile.has(part.id)) return;
|
|
28
|
-
st.seenFile.add(part.id);
|
|
29
|
-
emit({ type: 'file-event', filename: part.filename || part.path, mime: part.mime, url: part.url, id: part.id });
|
|
30
|
-
},
|
|
31
|
-
'step-start': (part, st, emit) => { if (!st.seenStep.has(part.id+':start')) { st.seenStep.add(part.id+':start'); emit({ type: 'step-start', id: part.id }); } },
|
|
32
|
-
'step-finish': (part, st, emit) => { if (!st.seenStep.has(part.id+':finish')) { st.seenStep.add(part.id+':finish'); emit({ type: 'step-finish', id: part.id, tokens: part.tokens, cost: part.cost }); st.stepFinished = true; } },
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
function makeState() { return { textByPart: new Map(), reasonByPart: new Map(), seenTool: new Set(), seenFile: new Set(), seenStep: new Set(), stepFinished: false }; }
|
|
36
|
-
|
|
37
|
-
export async function* streamKiloHTTP({ url, model, messages, providerType, agent }) {
|
|
38
|
-
yield { type: 'start-step' };
|
|
39
|
-
const base = (url || 'http://localhost:4780').replace(/\/$/, '');
|
|
40
|
-
const fsBase = base.replace(/:\d+$/, ':4781');
|
|
41
|
-
const isOpencode = providerType === 'opencode';
|
|
42
|
-
const dbgKey = isOpencode ? 'opencode' : 'kilo';
|
|
43
|
-
yield { type: 'status', message: 'connecting ' + base };
|
|
44
|
-
let sessRes;
|
|
45
|
-
try { sessRes = await fetch(base + '/session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); }
|
|
46
|
-
catch (e) { throw new Error(dbgKey + ' serve not reachable at ' + base + ' — start it with: node start-kilo.js --origin ' + location.origin); }
|
|
47
|
-
if (!sessRes.ok) throw new Error('/session ' + sessRes.status + ': ' + await sessRes.text());
|
|
48
|
-
const { id: sessionId } = await sessRes.json();
|
|
49
|
-
Object.assign(window.__debug = window.__debug || {}, { [dbgKey]: { sessionId, url: base, fsBase, lastStatus: null, events: [] } });
|
|
50
|
-
yield { type: 'status', message: 'session ' + sessionId.slice(0, 8) };
|
|
51
|
-
|
|
52
|
-
const userText = messages.filter(m => m.role === 'user').map(m =>
|
|
53
|
-
typeof m.content === 'string' ? m.content : (m.content || []).filter(b => b.type === 'text').map(b => b.text).join('')
|
|
54
|
-
).join('\n');
|
|
55
|
-
|
|
56
|
-
const modelId = model || (isOpencode ? 'minimax-m2.5-free' : 'x-ai/grok-code-fast-1:optimized:free');
|
|
57
|
-
const body = { parts: [{ type: 'text', text: userText }] };
|
|
58
|
-
if (agent) body.agent = agent;
|
|
59
|
-
if (isOpencode) body.model = { providerID: 'opencode', modelID: modelId };
|
|
60
|
-
else { body.providerID = 'kilo'; body.modelID = modelId; }
|
|
61
|
-
|
|
62
|
-
const st = makeState();
|
|
63
|
-
const emit = ev => { (window.__debug[dbgKey].events ||= []).push({ ...ev, t: Date.now() }); };
|
|
64
|
-
|
|
65
|
-
if (isOpencode) {
|
|
66
|
-
const es = new EventSource(base + '/event');
|
|
67
|
-
const assistantMsgs = new Set();
|
|
68
|
-
const pending = [];
|
|
69
|
-
let resolveNext = null;
|
|
70
|
-
const wake = () => { if (resolveNext) { const r = resolveNext; resolveNext = null; r(); } };
|
|
71
|
-
const push = ev => { emit(ev); pending.push(ev); wake(); };
|
|
72
|
-
es.onmessage = e => {
|
|
73
|
-
try {
|
|
74
|
-
const m = JSON.parse(e.data);
|
|
75
|
-
if (m.type === 'message.updated') {
|
|
76
|
-
const info = m.properties?.info;
|
|
77
|
-
if (info?.sessionID === sessionId && info.role === 'assistant') { assistantMsgs.add(info.id); if (info.modelID) push({ type: 'model-info', modelID: info.modelID, providerID: info.providerID }); }
|
|
78
|
-
} else if (m.type === 'message.part.updated') {
|
|
79
|
-
const part = m.properties?.part;
|
|
80
|
-
if (part?.sessionID !== sessionId || !assistantMsgs.has(part.messageID)) return;
|
|
81
|
-
const h = PART_HANDLERS[part.type];
|
|
82
|
-
if (h) h(part, st, push);
|
|
83
|
-
else push({ type: 'unknown-part', partType: part.type, id: part.id, text: part.text, raw: part });
|
|
84
|
-
}
|
|
85
|
-
} catch (_) {}
|
|
86
|
-
};
|
|
87
|
-
yield { type: 'status', message: 'POST /message' };
|
|
88
|
-
const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
89
|
-
window.__debug[dbgKey].lastStatus = msgRes.status;
|
|
90
|
-
yield { type: 'status', message: 'msg ' + msgRes.status };
|
|
91
|
-
if (!msgRes.ok) { es.close(); throw new Error('message ' + msgRes.status + ': ' + await msgRes.text()); }
|
|
92
|
-
const deadline = Date.now() + 180000;
|
|
93
|
-
let graceUntil = 0;
|
|
94
|
-
while (true) {
|
|
95
|
-
if (pending.length) { yield pending.shift(); continue; }
|
|
96
|
-
if (st.stepFinished) { if (!graceUntil) graceUntil = Date.now() + 1500; if (Date.now() > graceUntil) break; }
|
|
97
|
-
if (Date.now() > deadline) break;
|
|
98
|
-
await new Promise(r => { resolveNext = r; setTimeout(r, 500); });
|
|
99
|
-
}
|
|
100
|
-
es.close();
|
|
101
|
-
} else {
|
|
102
|
-
yield { type: 'status', message: 'POST /message' };
|
|
103
|
-
const msgRes = await fetch(base + '/session/' + sessionId + '/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
104
|
-
window.__debug[dbgKey].lastStatus = msgRes.status;
|
|
105
|
-
yield { type: 'status', message: 'msg ' + msgRes.status };
|
|
106
|
-
if (!msgRes.ok) throw new Error('message ' + msgRes.status + ': ' + await msgRes.text());
|
|
107
|
-
const result = await msgRes.json();
|
|
108
|
-
window.__debug[dbgKey].lastResult = result;
|
|
109
|
-
const info = result.info || {};
|
|
110
|
-
if (info.modelID) { const ev = { type: 'model-info', modelID: info.modelID, providerID: info.providerID }; emit(ev); yield ev; }
|
|
111
|
-
for (const part of (result.parts || [])) {
|
|
112
|
-
const h = PART_HANDLERS[part.type];
|
|
113
|
-
const pending = [];
|
|
114
|
-
const pushLocal = ev => { emit(ev); pending.push(ev); };
|
|
115
|
-
if (h) h(part, st, pushLocal);
|
|
116
|
-
else pushLocal({ type: 'unknown-part', partType: part.type, id: part.id, text: part.text, raw: part });
|
|
117
|
-
for (const ev of pending) yield ev;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
yield { type: 'status', message: 'mirror sandbox' };
|
|
121
|
-
const mirrored = await mirrorFromSandbox(fsBase);
|
|
122
|
-
if (mirrored.length) {
|
|
123
|
-
window.__debug[dbgKey].writes = mirrored;
|
|
124
|
-
for (const path of mirrored) yield { type: 'file-mirrored', path };
|
|
125
|
-
window.showPreview?.();
|
|
126
|
-
window.refreshPreview?.();
|
|
127
|
-
}
|
|
128
|
-
yield { type: 'finish-step', finishReason: 'stop' };
|
|
129
|
-
}
|
package/start-kilo.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const { spawn } = require('child_process');
|
|
3
|
-
const http = require('http');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const args = process.argv.slice(2);
|
|
8
|
-
const get = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
|
|
9
|
-
const kiloPort = get('--kilo-port', '4780');
|
|
10
|
-
const ocPort = get('--opencode-port', '4790');
|
|
11
|
-
const fsPort = get('--fs-port', '4781');
|
|
12
|
-
const origin = get('--origin', 'http://localhost:8787');
|
|
13
|
-
const sandbox = path.resolve(get('--sandbox', '.sandbox'));
|
|
14
|
-
fs.mkdirSync(sandbox, { recursive: true });
|
|
15
|
-
try { fs.writeFileSync(path.join(sandbox, '.gitignore'), '*\n!.gitignore\n'); } catch (e) {}
|
|
16
|
-
|
|
17
|
-
const cfgRoot = path.resolve('.thebird-cfg');
|
|
18
|
-
for (const sub of ['kilo', 'opencode']) {
|
|
19
|
-
const dir = path.join(cfgRoot, sub);
|
|
20
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
-
const cfgFile = path.join(dir, sub + '.json');
|
|
22
|
-
if (!fs.existsSync(cfgFile)) fs.writeFileSync(cfgFile, JSON.stringify({ $schema: 'https://' + sub + '.ai/config.json' }, null, 2));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const isWin = os.platform() === 'win32';
|
|
26
|
-
const kiloBin = isWin ? process.env.USERPROFILE + '\\AppData\\Roaming\\npm\\node_modules\\@kilocode\\cli\\node_modules\\@kilocode\\cli-windows-x64\\bin\\kilo.exe' : 'kilo';
|
|
27
|
-
const ocBin = isWin ? process.env.USERPROFILE + '\\AppData\\Roaming\\npm\\node_modules\\opencode-windows-x64\\bin\\opencode.exe' : 'opencode';
|
|
28
|
-
|
|
29
|
-
const procs = [];
|
|
30
|
-
const launch = (name, bin, port) => {
|
|
31
|
-
if (args.includes('--no-' + name)) return;
|
|
32
|
-
if (!fs.existsSync(bin)) { console.log(`[${name}] skip (${bin} not found)`); return; }
|
|
33
|
-
const env = { ...process.env, XDG_CONFIG_HOME: cfgRoot };
|
|
34
|
-
const p = spawn(bin, ['serve', '--port', port, '--hostname', '127.0.0.1', '--cors', origin], { stdio: 'inherit', env, cwd: sandbox });
|
|
35
|
-
procs.push(p);
|
|
36
|
-
console.log(`[${name}] serve --port ${port} pid ${p.pid}`);
|
|
37
|
-
};
|
|
38
|
-
launch('kilo', kiloBin, kiloPort);
|
|
39
|
-
launch('opencode', ocBin, ocPort);
|
|
40
|
-
|
|
41
|
-
const cors = { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Methods': 'GET,OPTIONS', 'Access-Control-Allow-Headers': 'content-type' };
|
|
42
|
-
const srv = http.createServer((req, res) => {
|
|
43
|
-
if (req.method === 'OPTIONS') { res.writeHead(204, cors); res.end(); return; }
|
|
44
|
-
const rel = decodeURIComponent(req.url.replace(/^\/+/, '').split('?')[0]);
|
|
45
|
-
if (rel === '__list') {
|
|
46
|
-
const out = [];
|
|
47
|
-
const walk = d => { for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
48
|
-
if (e.name.startsWith('.')) continue;
|
|
49
|
-
const full = path.join(d, e.name);
|
|
50
|
-
if (e.isDirectory()) walk(full);
|
|
51
|
-
else out.push(path.relative(sandbox, full).replace(/\\/g, '/'));
|
|
52
|
-
}};
|
|
53
|
-
try { walk(sandbox); } catch (e) {}
|
|
54
|
-
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify(out)); return;
|
|
55
|
-
}
|
|
56
|
-
const full = path.resolve(path.join(sandbox, rel));
|
|
57
|
-
if (!full.startsWith(sandbox)) { res.writeHead(403, cors); res.end(); return; }
|
|
58
|
-
fs.readFile(full, (err, data) => {
|
|
59
|
-
if (err) { res.writeHead(404, cors); res.end(); return; }
|
|
60
|
-
const ct = { '.html':'text/html','.js':'application/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.md':'text/plain','.txt':'text/plain' }[path.extname(rel)] || 'application/octet-stream';
|
|
61
|
-
res.writeHead(200, { ...cors, 'Content-Type': ct }); res.end(data);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
srv.listen(fsPort, '127.0.0.1', () => console.log(`[fs-bridge] sandbox=${sandbox} serving http://127.0.0.1:${fsPort}`));
|
|
65
|
-
|
|
66
|
-
const stop = () => { try { srv.close(); } catch (e) {} for (const p of procs) { try { p.kill(); } catch (e) {} } process.exit(0); };
|
|
67
|
-
process.on('SIGINT', stop);
|
|
68
|
-
process.on('SIGTERM', stop);
|