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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bom-pi",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "description": "bom × pi — hệ sinh thái durable cho pi trên nền pi-durable: core daemon + web UI pocket + CLI",
6
6
  "engines": {
@@ -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);