bom-pi 0.0.1 → 0.0.2
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.
|
@@ -179,7 +179,7 @@ export function makePocketAdapter({ rpc, checkToken }) {
|
|
|
179
179
|
return async function pocketRoute(req, res, url, authed) {
|
|
180
180
|
const path = url.pathname.replace(/^\/api\//, '');
|
|
181
181
|
const parts = path.split('/').filter(Boolean);
|
|
182
|
-
const [first, second, third] = parts;
|
|
182
|
+
const [first, second, third, fourth] = parts;
|
|
183
183
|
const method = req.method;
|
|
184
184
|
const isPost = method !== 'GET' && method !== 'HEAD';
|
|
185
185
|
|
|
@@ -243,6 +243,27 @@ export function makePocketAdapter({ rpc, checkToken }) {
|
|
|
243
243
|
}
|
|
244
244
|
if (third === 'turns' && isPost) return json(res, 200, { ok: true }), true; // PORT(bom-pi): multiplayer turns — chưa có
|
|
245
245
|
if (third === 'typing' && isPost) return json(res, 200, { ok: true }), true; // PORT(bom-pi): presence
|
|
246
|
+
// ---- Files tile + Changes review (repo thật của xưởng) ----
|
|
247
|
+
if (third === 'files' && method === 'GET') {
|
|
248
|
+
const r = await rpc('fs.list', { conversationId: id });
|
|
249
|
+
const since = url.searchParams.get('since');
|
|
250
|
+
const version = String(r.files.length) + ':' + (r.files[0] ?? '') + (r.files.at(-1) ?? '');
|
|
251
|
+
if (since === version) return json(res, 200, { version, same: true }), true;
|
|
252
|
+
return json(res, 200, { files: r.files, truncated: r.truncated, version }), true;
|
|
253
|
+
}
|
|
254
|
+
if (third === 'view' && method === 'GET') {
|
|
255
|
+
const r = await rpc('fs.view', { conversationId: id, path: url.searchParams.get('path') ?? '' });
|
|
256
|
+
return json(res, 200, { path: r.path, text: r.text }), true;
|
|
257
|
+
}
|
|
258
|
+
if (third === 'changes' && fourth === undefined && method === 'GET') {
|
|
259
|
+
return json(res, 200, await rpc('changes.get', { conversationId: id })), true;
|
|
260
|
+
}
|
|
261
|
+
if (third === 'changes' && fourth === 'diff' && method === 'GET') {
|
|
262
|
+
const r = await rpc('changes.diff', { conversationId: id, path: url.searchParams.get('path') ?? '' });
|
|
263
|
+
res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
|
|
264
|
+
res.end(r.diff ?? '');
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
246
267
|
if (isPost) { await readBody(req); return json(res, 200, { ok: true }), true; } // PORT(bom-pi): plan/chat/pin/notes/fork/... — noop
|
|
247
268
|
return json(res, 200, {}), true;
|
|
248
269
|
}
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { connect, createServer } from 'node:net';
|
|
27
27
|
import { mkdir, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
28
28
|
import { chmodSync } from 'node:fs';
|
|
29
|
-
import { dirname, join, resolve } from 'node:path';
|
|
29
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
30
30
|
import { homedir, userInfo } from 'node:os';
|
|
31
31
|
import process from 'node:process';
|
|
32
32
|
import { execFile as execFileCb } from 'node:child_process';
|
|
@@ -1614,6 +1614,91 @@ const methods = {
|
|
|
1614
1614
|
const entries = (view.entries ?? []).map((e) => ({ id: e.id, kind: e.kind, model: e.model ?? null }));
|
|
1615
1615
|
return { conversationId: conv.id, count: entries.length, entries };
|
|
1616
1616
|
},
|
|
1617
|
+
// ---- Files & Changes (pocket Files tile / Changes review) ----
|
|
1618
|
+
// cwd của conversation từ agent config; path client bị nhốt trong cwd (traversal chặn).
|
|
1619
|
+
async convCwd(params) {
|
|
1620
|
+
const conv = await convFor(params);
|
|
1621
|
+
const agent = await conv.agent(BACKGROUND_CONTEXT);
|
|
1622
|
+
return { conversationId: conv.id, cwd: agent?.cwd ?? process.env.HOME ?? process.cwd() };
|
|
1623
|
+
},
|
|
1624
|
+
async fsList(params) {
|
|
1625
|
+
const { cwd } = await methods.convCwd(params);
|
|
1626
|
+
const files = [];
|
|
1627
|
+
let truncated = false;
|
|
1628
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.cache']);
|
|
1629
|
+
const walk = async (rel) => {
|
|
1630
|
+
if (files.length >= 2000) { truncated = true; return; }
|
|
1631
|
+
let entries;
|
|
1632
|
+
try { entries = await readdir(join(cwd, rel), { withFileTypes: true }); } catch { return; }
|
|
1633
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1634
|
+
for (const e of entries) {
|
|
1635
|
+
if (e.name.startsWith('.') || SKIP.has(e.name)) continue;
|
|
1636
|
+
const p = rel ? `${rel}/${e.name}` : e.name;
|
|
1637
|
+
if (e.isDirectory()) await walk(p);
|
|
1638
|
+
else { files.push(p); if (files.length >= 2000) { truncated = true; break; } }
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
await walk('');
|
|
1642
|
+
return { files, truncated };
|
|
1643
|
+
},
|
|
1644
|
+
async fsView(params) {
|
|
1645
|
+
const { cwd } = await methods.convCwd(params);
|
|
1646
|
+
const p = resolve(cwd, String(params?.path ?? ''));
|
|
1647
|
+
if (p !== cwd && !p.startsWith(cwd + sep)) throw new Error('path ngoài thư mục làm việc');
|
|
1648
|
+
const st = await stat(p);
|
|
1649
|
+
if (st.size > 1_000_000) throw new Error('file quá lớn (>1MB)');
|
|
1650
|
+
return { path: String(params.path), text: await readFile(p, 'utf8') };
|
|
1651
|
+
},
|
|
1652
|
+
async changesGet(params) {
|
|
1653
|
+
const { cwd } = await methods.convCwd(params);
|
|
1654
|
+
let root = '';
|
|
1655
|
+
try { root = (await execFileAsync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { timeout: 10_000 })).stdout.trim(); } catch { return { repo: null, files: [], more: 0, piOnly: [] }; }
|
|
1656
|
+
const branch = (await execFileAsync('git', ['-C', cwd, 'rev-parse', '--abbrev-ref', 'HEAD'], { timeout: 10_000 })).stdout.trim();
|
|
1657
|
+
const head = (await execFileAsync('git', ['-C', cwd, 'rev-parse', 'HEAD'], { timeout: 10_000 })).stdout.trim();
|
|
1658
|
+
const st = (await execFileAsync('git', ['-C', cwd, 'status', '--porcelain=v1', '-uall'], { timeout: 10_000 })).stdout;
|
|
1659
|
+
let numstat = '';
|
|
1660
|
+
try { numstat = (await execFileAsync('git', ['-C', cwd, 'diff', 'HEAD', '--numstat'], { timeout: 10_000 })).stdout; } catch { numstat = ''; }
|
|
1661
|
+
const statMap = new Map();
|
|
1662
|
+
for (const line of numstat.split('\n').filter(Boolean)) {
|
|
1663
|
+
const [a, r, p] = line.split('\t');
|
|
1664
|
+
statMap.set(p, { added: a === '-' ? 0 : Number(a), removed: r === '-' ? 0 : Number(r) });
|
|
1665
|
+
}
|
|
1666
|
+
const KIND = { M: 'modified', A: 'new', '?': 'new', D: 'deleted', R: 'renamed' };
|
|
1667
|
+
const files = [];
|
|
1668
|
+
for (const line of st.split('\n').filter(Boolean)) {
|
|
1669
|
+
const x = line[0] === ' ' ? '?' : line[0]; // staged letter; '?' = untracked
|
|
1670
|
+
const path = line.slice(3).trim();
|
|
1671
|
+
let s = statMap.get(path) ?? { added: 0, removed: 0 };
|
|
1672
|
+
if (s.added === 0 && s.removed === 0 && (KIND[x] === 'new')) {
|
|
1673
|
+
// untracked: numstat không có — đếm dòng file (diff full là +all)
|
|
1674
|
+
try { s = { added: (await readFile(join(cwd, path), 'utf8')).split('\n').filter(Boolean).length, removed: 0 }; } catch { /* binary/đã đổi */ }
|
|
1675
|
+
}
|
|
1676
|
+
files.push({ path, kind: KIND[x] ?? 'modified', added: s.added, removed: s.removed, byPi: false, version: `1:${Date.now()}` });
|
|
1677
|
+
}
|
|
1678
|
+
return { repo: { root, branch, head }, files, more: 0, piOnly: [] };
|
|
1679
|
+
},
|
|
1680
|
+
async changesDiff(params) {
|
|
1681
|
+
const { cwd } = await methods.convCwd(params);
|
|
1682
|
+
const p = resolve(cwd, String(params?.path ?? ''));
|
|
1683
|
+
if (p !== cwd && !p.startsWith(cwd + sep)) throw new Error('path ngoài thư mục làm việc');
|
|
1684
|
+
const rel = relative(cwd, p);
|
|
1685
|
+
try {
|
|
1686
|
+
const tracked = (await execFileAsync('git', ['-C', cwd, 'ls-files', '--error-unmatch', rel], { timeout: 10_000 })).stdout.trim();
|
|
1687
|
+
if (tracked) {
|
|
1688
|
+
const { stdout } = await execFileAsync('git', ['-C', cwd, 'diff', 'HEAD', '--', rel], { timeout: 20_000, maxBuffer: 4 * 1024 * 1024 });
|
|
1689
|
+
return { diff: stdout };
|
|
1690
|
+
}
|
|
1691
|
+
} catch { /* untracked hoặc lỗi — rơi về --no-index */ }
|
|
1692
|
+
try {
|
|
1693
|
+
// -C + path relative để header diff đúng b/<rel> (absolute path làm header a/<cwd>/...)
|
|
1694
|
+
const { stdout } = await execFileAsync('git', ['-C', cwd, 'diff', '--no-index', '--', '/dev/null', rel], { timeout: 20_000, maxBuffer: 4 * 1024 * 1024 });
|
|
1695
|
+
return { diff: stdout };
|
|
1696
|
+
} catch (e) {
|
|
1697
|
+
// git diff --no-index exit 1 khi CÓ khác biệt — stdout vẫn chứa diff
|
|
1698
|
+
if (e?.stdout) return { diff: String(e.stdout) };
|
|
1699
|
+
throw e;
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1617
1702
|
async eventsSubscribe(params, ctx) {
|
|
1618
1703
|
const conv = await convFor(params);
|
|
1619
1704
|
const w = await ensureWatch(conv.id, conv);
|