tina4-nodejs 3.11.14 → 3.11.16

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.
@@ -519,6 +519,52 @@ export class DevAdmin {
519
519
  }},
520
520
  // Version check (proxy to avoid CORS)
521
521
  { method: "GET", pattern: "/__dev/api/version-check", handler: handleVersionCheck },
522
+ // ── Parity surface area (ported from Python tina4_python.dev_admin) ──
523
+ // Thoughts / activity feed (live tail of MessageLog for the AI chat pane)
524
+ { method: "GET", pattern: "/__dev/api/thoughts", handler: handleThoughts },
525
+ // Supervise — currently stubbed; full implementation requires a Rust
526
+ // agent + worktree manager that doesn't exist in tina4-nodejs yet.
527
+ { method: "POST", pattern: "/__dev/api/supervise/create", handler: handleSuperviseStub },
528
+ { method: "GET", pattern: "/__dev/api/supervise/sessions", handler: handleSuperviseStub },
529
+ { method: "GET", pattern: "/__dev/api/supervise/diff", handler: handleSuperviseStub },
530
+ { method: "POST", pattern: "/__dev/api/supervise/commit", handler: handleSuperviseStub },
531
+ { method: "POST", pattern: "/__dev/api/supervise/cancel", handler: handleSuperviseStub },
532
+ // Execute — proxies to the framework_port+2000 Rust agent (SSE passthrough)
533
+ { method: "POST", pattern: "/__dev/api/execute", handler: handleExecute },
534
+ // File browser / editor
535
+ { method: "GET", pattern: "/__dev/api/files", handler: handleFiles },
536
+ { method: "GET", pattern: "/__dev/api/file", handler: handleFileRead },
537
+ { method: "POST", pattern: "/__dev/api/file/save", handler: handleFileSave },
538
+ { method: "GET", pattern: "/__dev/api/file/raw", handler: handleFileRaw },
539
+ { method: "POST", pattern: "/__dev/api/file/rename", handler: handleFileRename },
540
+ { method: "POST", pattern: "/__dev/api/file/delete", handler: handleFileDelete },
541
+ // Dependency search (npm registry) + install
542
+ { method: "GET", pattern: "/__dev/api/deps/search", handler: handleDepsSearch },
543
+ { method: "POST", pattern: "/__dev/api/deps/install", handler: handleDepsInstall },
544
+ // Git status
545
+ { method: "GET", pattern: "/__dev/api/git/status", handler: handleGitStatus },
546
+ // MCP tool introspection over the built-in MCP server
547
+ { method: "GET", pattern: "/__dev/api/mcp/tools", handler: handleMcpTools },
548
+ { method: "POST", pattern: "/__dev/api/mcp/call", handler: handleMcpCall },
549
+ // Scaffolding
550
+ { method: "GET", pattern: "/__dev/api/scaffold", handler: handleScaffoldList },
551
+ { method: "POST", pattern: "/__dev/api/scaffold/run", handler: handleScaffoldRun },
552
+ // Plan API (ported from Python)
553
+ { method: "GET", pattern: "/__dev/api/plan/current", handler: handlePlanCurrent },
554
+ { method: "GET", pattern: "/__dev/api/plan/list", handler: handlePlanList },
555
+ { method: "POST", pattern: "/__dev/api/plan/create", handler: handlePlanCreate },
556
+ { method: "POST", pattern: "/__dev/api/plan/switch", handler: handlePlanSwitch },
557
+ { method: "POST", pattern: "/__dev/api/plan/complete-step", handler: handlePlanCompleteStep },
558
+ { method: "POST", pattern: "/__dev/api/plan/add-step", handler: handlePlanAddStep },
559
+ { method: "POST", pattern: "/__dev/api/plan/note", handler: handlePlanNote },
560
+ { method: "POST", pattern: "/__dev/api/plan/archive", handler: handlePlanArchive },
561
+ { method: "GET", pattern: "/__dev/api/plan/read", handler: handlePlanRead },
562
+ { method: "POST", pattern: "/__dev/api/plan/flesh", handler: handlePlanFlesh },
563
+ // Project index API
564
+ { method: "POST", pattern: "/__dev/api/index/rebuild", handler: handleIndexRebuild },
565
+ { method: "GET", pattern: "/__dev/api/index/search", handler: handleIndexSearch },
566
+ { method: "GET", pattern: "/__dev/api/index/file", handler: handleIndexFile },
567
+ { method: "GET", pattern: "/__dev/api/index/overview", handler: handleIndexOverview },
522
568
  // JS asset
523
569
  { method: "GET", pattern: "/__dev/js/tina4-dev-admin.min.js", handler: handleDevAdminJs },
524
570
  ];
@@ -1380,6 +1426,447 @@ const handleVersionCheck: RouteHandler = async (_req, res) => {
1380
1426
  res.json({ current, latest });
1381
1427
  };
1382
1428
 
1429
+ // ---------------------------------------------------------------------------
1430
+ // Parity handlers — ported from Python tina4_python.dev_admin
1431
+ // ---------------------------------------------------------------------------
1432
+
1433
+ function safeJoin(projectRoot: string, rel: string): string | null {
1434
+ const resolved = resolve(projectRoot, rel);
1435
+ if (!resolved.startsWith(projectRoot)) return null;
1436
+ return resolved;
1437
+ }
1438
+
1439
+ const handleThoughts: RouteHandler = (req, res) => {
1440
+ const url = new URL(req.url ?? "/", "http://localhost");
1441
+ const limit = parseInt(url.searchParams.get("limit") ?? "100", 10);
1442
+ const entries = MessageLog.get(undefined, limit).map((e) => ({
1443
+ id: e.id,
1444
+ timestamp: e.timestamp,
1445
+ level: e.level,
1446
+ category: e.category,
1447
+ message: e.message,
1448
+ data: e.data,
1449
+ }));
1450
+ res.json({ thoughts: entries, count: entries.length });
1451
+ };
1452
+
1453
+ const handleSuperviseStub: RouteHandler = (_req, res) => {
1454
+ res.json(
1455
+ {
1456
+ error: "supervise API not implemented in tina4-nodejs yet",
1457
+ note: "Requires Rust agent + worktree manager for parity with Python/PHP. Stubbed intentionally.",
1458
+ },
1459
+ 501,
1460
+ );
1461
+ };
1462
+
1463
+ const handleExecute: RouteHandler = async (req, res) => {
1464
+ // Proxy to framework_port+2000 Rust agent (SSE passthrough).
1465
+ const port = parseInt(process.env.TINA4_PORT ?? process.env.PORT ?? "7148", 10);
1466
+ const agentUrl = `http://127.0.0.1:${port + 2000}/execute`;
1467
+ try {
1468
+ const upstream = await fetch(agentUrl, {
1469
+ method: "POST",
1470
+ headers: { "Content-Type": "application/json" },
1471
+ body: JSON.stringify(req.body ?? {}),
1472
+ });
1473
+ if (!upstream.body) {
1474
+ res.json({ error: "agent returned no body" }, 502);
1475
+ return;
1476
+ }
1477
+ res.raw.writeHead(upstream.status || 200, {
1478
+ "Content-Type": upstream.headers.get("content-type") || "text/event-stream",
1479
+ "Cache-Control": "no-cache",
1480
+ Connection: "keep-alive",
1481
+ });
1482
+ const reader = upstream.body.getReader();
1483
+ while (true) {
1484
+ const { done, value } = await reader.read();
1485
+ if (done) break;
1486
+ if (value) res.raw.write(Buffer.from(value));
1487
+ }
1488
+ res.raw.end();
1489
+ } catch (e) {
1490
+ res.json({ error: `agent unreachable at ${agentUrl}: ${(e as Error).message}` }, 502);
1491
+ }
1492
+ };
1493
+
1494
+ const handleFiles: RouteHandler = (req, res) => {
1495
+ const url = new URL(req.url ?? "/", "http://localhost");
1496
+ const rel = url.searchParams.get("path") ?? ".";
1497
+ const root = resolve(process.cwd());
1498
+ const target = safeJoin(root, rel);
1499
+ if (!target || !existsSync(target)) {
1500
+ res.json({ error: `Path not found: ${rel}` }, 404);
1501
+ return;
1502
+ }
1503
+ const stat = statSync(target);
1504
+ if (!stat.isDirectory()) {
1505
+ res.json({ error: `Not a directory: ${rel}` }, 400);
1506
+ return;
1507
+ }
1508
+ const entries = readdirSync(target, { withFileTypes: true })
1509
+ .filter((e) => !e.name.startsWith(".") || e.name === ".env")
1510
+ .map((e) => {
1511
+ const full = join(target, e.name);
1512
+ let size = 0;
1513
+ try { size = e.isFile() ? statSync(full).size : 0; } catch { /* ignore */ }
1514
+ return {
1515
+ name: e.name,
1516
+ type: e.isDirectory() ? "dir" : "file",
1517
+ size,
1518
+ path: relative(root, full),
1519
+ };
1520
+ })
1521
+ .sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
1522
+ res.json({ path: relative(root, target) || ".", entries });
1523
+ };
1524
+
1525
+ const handleFileRead: RouteHandler = (req, res) => {
1526
+ const url = new URL(req.url ?? "/", "http://localhost");
1527
+ const rel = url.searchParams.get("path") ?? "";
1528
+ const root = resolve(process.cwd());
1529
+ const target = safeJoin(root, rel);
1530
+ if (!target || !existsSync(target) || !statSync(target).isFile()) {
1531
+ res.json({ error: `File not found: ${rel}` }, 404);
1532
+ return;
1533
+ }
1534
+ try {
1535
+ const content = readFileSync(target, "utf-8");
1536
+ res.json({ path: relative(root, target), content, bytes: Buffer.byteLength(content, "utf-8") });
1537
+ } catch (e) {
1538
+ res.json({ error: (e as Error).message }, 500);
1539
+ }
1540
+ };
1541
+
1542
+ const handleFileSave: RouteHandler = async (req, res) => {
1543
+ const body = (req.body as Record<string, unknown>) || {};
1544
+ const rel = (body.path as string) || "";
1545
+ const content = (body.content as string) ?? "";
1546
+ const root = resolve(process.cwd());
1547
+ const target = safeJoin(root, rel);
1548
+ if (!target) {
1549
+ res.json({ error: `Path escapes project directory: ${rel}` }, 400);
1550
+ return;
1551
+ }
1552
+ try {
1553
+ mkdirSync(dirname(target), { recursive: true });
1554
+ const existed = existsSync(target);
1555
+ writeFileSync(target, content, "utf-8");
1556
+ try {
1557
+ const { Plan } = await import("./plan.js");
1558
+ Plan.recordAction(existed ? "patched" : "created", relative(root, target));
1559
+ } catch { /* ignore */ }
1560
+ res.json({ ok: true, path: relative(root, target), bytes: Buffer.byteLength(content, "utf-8") });
1561
+ } catch (e) {
1562
+ res.json({ error: (e as Error).message }, 500);
1563
+ }
1564
+ };
1565
+
1566
+ const handleFileRaw: RouteHandler = (req, res) => {
1567
+ const url = new URL(req.url ?? "/", "http://localhost");
1568
+ const rel = url.searchParams.get("path") ?? "";
1569
+ const root = resolve(process.cwd());
1570
+ const target = safeJoin(root, rel);
1571
+ if (!target || !existsSync(target) || !statSync(target).isFile()) {
1572
+ res.raw.writeHead(404);
1573
+ res.raw.end("Not found");
1574
+ return;
1575
+ }
1576
+ try {
1577
+ const buf = readFileSync(target);
1578
+ const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
1579
+ const mime: Record<string, string> = {
1580
+ js: "application/javascript", ts: "text/plain", json: "application/json",
1581
+ html: "text/html", css: "text/css", svg: "image/svg+xml",
1582
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif",
1583
+ md: "text/markdown", txt: "text/plain",
1584
+ };
1585
+ res.raw.writeHead(200, { "Content-Type": mime[ext] || "application/octet-stream" });
1586
+ res.raw.end(buf);
1587
+ } catch (e) {
1588
+ res.raw.writeHead(500);
1589
+ res.raw.end((e as Error).message);
1590
+ }
1591
+ };
1592
+
1593
+ const handleFileRename: RouteHandler = async (req, res) => {
1594
+ const body = (req.body as Record<string, unknown>) || {};
1595
+ const from = (body.from as string) || "";
1596
+ const to = (body.to as string) || "";
1597
+ const root = resolve(process.cwd());
1598
+ const src = safeJoin(root, from);
1599
+ const dst = safeJoin(root, to);
1600
+ if (!src || !dst) {
1601
+ res.json({ error: "Invalid path" }, 400);
1602
+ return;
1603
+ }
1604
+ if (!existsSync(src)) {
1605
+ res.json({ error: `Source not found: ${from}` }, 404);
1606
+ return;
1607
+ }
1608
+ try {
1609
+ const { renameSync } = await import("node:fs");
1610
+ mkdirSync(dirname(dst), { recursive: true });
1611
+ renameSync(src, dst);
1612
+ res.json({ ok: true, from: relative(root, src), to: relative(root, dst) });
1613
+ } catch (e) {
1614
+ res.json({ error: (e as Error).message }, 500);
1615
+ }
1616
+ };
1617
+
1618
+ const handleFileDelete: RouteHandler = async (req, res) => {
1619
+ const body = (req.body as Record<string, unknown>) || {};
1620
+ const rel = (body.path as string) || "";
1621
+ const root = resolve(process.cwd());
1622
+ const target = safeJoin(root, rel);
1623
+ if (!target) {
1624
+ res.json({ error: "Invalid path" }, 400);
1625
+ return;
1626
+ }
1627
+ if (!existsSync(target)) {
1628
+ res.json({ error: `Not found: ${rel}` }, 404);
1629
+ return;
1630
+ }
1631
+ try {
1632
+ const { rmSync } = await import("node:fs");
1633
+ rmSync(target, { recursive: true, force: true });
1634
+ res.json({ ok: true, deleted: relative(root, target) });
1635
+ } catch (e) {
1636
+ res.json({ error: (e as Error).message }, 500);
1637
+ }
1638
+ };
1639
+
1640
+ const handleDepsSearch: RouteHandler = async (req, res) => {
1641
+ const url = new URL(req.url ?? "/", "http://localhost");
1642
+ const q = url.searchParams.get("q") ?? "";
1643
+ if (!q) {
1644
+ res.json({ error: "q required" }, 400);
1645
+ return;
1646
+ }
1647
+ try {
1648
+ const r = await fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=20`);
1649
+ const data = (await r.json()) as { objects?: Array<Record<string, any>> };
1650
+ const results = (data.objects || []).map((o) => ({
1651
+ name: o.package?.name,
1652
+ version: o.package?.version,
1653
+ description: o.package?.description,
1654
+ links: o.package?.links,
1655
+ }));
1656
+ res.json({ results });
1657
+ } catch (e) {
1658
+ res.json({ error: (e as Error).message }, 502);
1659
+ }
1660
+ };
1661
+
1662
+ const handleDepsInstall: RouteHandler = async (req, res) => {
1663
+ const body = (req.body as Record<string, unknown>) || {};
1664
+ const pkg = (body.package as string) || "";
1665
+ const dev = Boolean(body.dev);
1666
+ if (!pkg || !/^[@A-Za-z0-9][\w@/.\-]*$/.test(pkg)) {
1667
+ res.json({ error: "invalid package name" }, 400);
1668
+ return;
1669
+ }
1670
+ try {
1671
+ const { execFileSync } = await import("node:child_process");
1672
+ const args = ["install", dev ? "--save-dev" : "--save", pkg];
1673
+ const output = execFileSync("npm", args, {
1674
+ cwd: resolve(process.cwd()),
1675
+ timeout: 120_000,
1676
+ encoding: "utf-8",
1677
+ }).toString();
1678
+ res.json({ ok: true, package: pkg, output });
1679
+ } catch (e) {
1680
+ res.json({ error: (e as Error).message }, 500);
1681
+ }
1682
+ };
1683
+
1684
+ const handleGitStatus: RouteHandler = async (_req, res) => {
1685
+ try {
1686
+ const { execFileSync } = await import("node:child_process");
1687
+ const cwd = resolve(process.cwd());
1688
+ try {
1689
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 3000 });
1690
+ } catch {
1691
+ res.json({ error: "Not a git repository" }, 400);
1692
+ return;
1693
+ }
1694
+ const run = (args: string[]): string =>
1695
+ execFileSync("git", args, { cwd, timeout: 3000, encoding: "utf-8" }).toString().trim();
1696
+ res.json({
1697
+ branch: run(["branch", "--show-current"]),
1698
+ status: run(["status", "--porcelain"]).split(/\r?\n/).filter((l) => l),
1699
+ recent_commits: run(["log", "--oneline", "-5"]).split(/\r?\n/).filter((l) => l),
1700
+ });
1701
+ } catch (e) {
1702
+ res.json({ error: `git unavailable: ${(e as Error).message}` }, 500);
1703
+ }
1704
+ };
1705
+
1706
+ const handleMcpTools: RouteHandler = async (_req, res) => {
1707
+ try {
1708
+ const { McpServer } = await import("./mcp.js");
1709
+ const instances = (McpServer as unknown as { _instances: Array<any> })._instances || [];
1710
+ const tools: Array<{ server: string; name: string; description: string; inputSchema: unknown }> = [];
1711
+ for (const s of instances) {
1712
+ for (const t of ((s as any)._tools as Map<string, any>).values()) {
1713
+ tools.push({ server: s.name, name: t.name, description: t.description, inputSchema: t.inputSchema });
1714
+ }
1715
+ }
1716
+ res.json({ tools, count: tools.length });
1717
+ } catch (e) {
1718
+ res.json({ error: (e as Error).message }, 500);
1719
+ }
1720
+ };
1721
+
1722
+ const handleMcpCall: RouteHandler = async (req, res) => {
1723
+ const body = (req.body as Record<string, unknown>) || {};
1724
+ const name = (body.name as string) || "";
1725
+ const args = (body.arguments as Record<string, unknown>) || {};
1726
+ try {
1727
+ const { McpServer } = await import("./mcp.js");
1728
+ const instances = (McpServer as unknown as { _instances: Array<any> })._instances || [];
1729
+ for (const s of instances) {
1730
+ const tool = ((s as any)._tools as Map<string, any>).get(name);
1731
+ if (tool) {
1732
+ const result = await tool.handler(args);
1733
+ res.json({ ok: true, result });
1734
+ return;
1735
+ }
1736
+ }
1737
+ res.json({ error: `Unknown tool: ${name}` }, 404);
1738
+ } catch (e) {
1739
+ res.json({ error: (e as Error).message }, 500);
1740
+ }
1741
+ };
1742
+
1743
+ const handleScaffoldList: RouteHandler = (_req, res) => {
1744
+ res.json({
1745
+ scaffolds: [
1746
+ { name: "route", description: "Create a new route file in src/routes/" },
1747
+ { name: "model", description: "Create a new ORM model in src/models/" },
1748
+ { name: "migration", description: "Create a new SQL migration file" },
1749
+ { name: "middleware", description: "Create a new middleware class" },
1750
+ ],
1751
+ });
1752
+ };
1753
+
1754
+ const handleScaffoldRun: RouteHandler = async (req, res) => {
1755
+ const body = (req.body as Record<string, unknown>) || {};
1756
+ const kind = (body.kind as string) || "";
1757
+ const name = (body.name as string) || "";
1758
+ if (!kind || !name || !/^[A-Za-z_][\w]*$/.test(name)) {
1759
+ res.json({ error: "kind and valid name required" }, 400);
1760
+ return;
1761
+ }
1762
+ try {
1763
+ const { execFileSync } = await import("node:child_process");
1764
+ const output = execFileSync("npx", ["tina4nodejs", "generate", kind, name], {
1765
+ cwd: resolve(process.cwd()),
1766
+ timeout: 30_000,
1767
+ encoding: "utf-8",
1768
+ }).toString();
1769
+ res.json({ ok: true, kind, name, output });
1770
+ } catch (e) {
1771
+ res.json({ error: (e as Error).message }, 500);
1772
+ }
1773
+ };
1774
+
1775
+ // ── Plan routes ─────────────────────────────────────────────
1776
+
1777
+ const handlePlanCurrent: RouteHandler = async (_req, res) => {
1778
+ const { Plan } = await import("./plan.js");
1779
+ res.json(Plan.current());
1780
+ };
1781
+
1782
+ const handlePlanList: RouteHandler = async (_req, res) => {
1783
+ const { Plan } = await import("./plan.js");
1784
+ res.json({ plans: Plan.listPlans() });
1785
+ };
1786
+
1787
+ const handlePlanCreate: RouteHandler = async (req, res) => {
1788
+ const { Plan } = await import("./plan.js");
1789
+ const body = (req.body as Record<string, unknown>) || {};
1790
+ res.json(
1791
+ Plan.create(
1792
+ (body.title as string) || "",
1793
+ (body.goal as string) || "",
1794
+ (body.steps as string[]) || [],
1795
+ body.make_current !== false,
1796
+ ),
1797
+ );
1798
+ };
1799
+
1800
+ const handlePlanSwitch: RouteHandler = async (req, res) => {
1801
+ const { Plan } = await import("./plan.js");
1802
+ const body = (req.body as Record<string, unknown>) || {};
1803
+ res.json(Plan.setCurrent((body.name as string) || ""));
1804
+ };
1805
+
1806
+ const handlePlanCompleteStep: RouteHandler = async (req, res) => {
1807
+ const { Plan } = await import("./plan.js");
1808
+ const body = (req.body as Record<string, unknown>) || {};
1809
+ res.json(Plan.completeStep((body.index as number) ?? -1, (body.name as string) || ""));
1810
+ };
1811
+
1812
+ const handlePlanAddStep: RouteHandler = async (req, res) => {
1813
+ const { Plan } = await import("./plan.js");
1814
+ const body = (req.body as Record<string, unknown>) || {};
1815
+ res.json(Plan.addStep((body.text as string) || "", (body.name as string) || ""));
1816
+ };
1817
+
1818
+ const handlePlanNote: RouteHandler = async (req, res) => {
1819
+ const { Plan } = await import("./plan.js");
1820
+ const body = (req.body as Record<string, unknown>) || {};
1821
+ res.json(Plan.appendNote((body.text as string) || "", (body.name as string) || ""));
1822
+ };
1823
+
1824
+ const handlePlanArchive: RouteHandler = async (req, res) => {
1825
+ const { Plan } = await import("./plan.js");
1826
+ const body = (req.body as Record<string, unknown>) || {};
1827
+ res.json(Plan.archive((body.name as string) || ""));
1828
+ };
1829
+
1830
+ const handlePlanRead: RouteHandler = async (req, res) => {
1831
+ const { Plan } = await import("./plan.js");
1832
+ const url = new URL(req.url ?? "/", "http://localhost");
1833
+ const name = url.searchParams.get("name") ?? "";
1834
+ res.json(Plan.read(name));
1835
+ };
1836
+
1837
+ const handlePlanFlesh: RouteHandler = async (req, res) => {
1838
+ const { Plan } = await import("./plan.js");
1839
+ const body = (req.body as Record<string, unknown>) || {};
1840
+ res.json(await Plan.flesh((body.name as string) || "", (body.prompt as string) || ""));
1841
+ };
1842
+
1843
+ // ── Project index routes ────────────────────────────────────
1844
+
1845
+ const handleIndexRebuild: RouteHandler = async (_req, res) => {
1846
+ const { ProjectIndex } = await import("./projectIndex.js");
1847
+ res.json(ProjectIndex.refresh());
1848
+ };
1849
+
1850
+ const handleIndexSearch: RouteHandler = async (req, res) => {
1851
+ const { ProjectIndex } = await import("./projectIndex.js");
1852
+ const url = new URL(req.url ?? "/", "http://localhost");
1853
+ const q = url.searchParams.get("q") ?? "";
1854
+ const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
1855
+ res.json({ results: ProjectIndex.search(q, limit) });
1856
+ };
1857
+
1858
+ const handleIndexFile: RouteHandler = async (req, res) => {
1859
+ const { ProjectIndex } = await import("./projectIndex.js");
1860
+ const url = new URL(req.url ?? "/", "http://localhost");
1861
+ const p = url.searchParams.get("path") ?? "";
1862
+ res.json(ProjectIndex.fileEntry(p));
1863
+ };
1864
+
1865
+ const handleIndexOverview: RouteHandler = async (_req, res) => {
1866
+ const { ProjectIndex } = await import("./projectIndex.js");
1867
+ res.json(ProjectIndex.overview());
1868
+ };
1869
+
1383
1870
  // ---------------------------------------------------------------------------
1384
1871
  // Dev Admin JS handler — serves the shared JS file
1385
1872
  // ---------------------------------------------------------------------------
@@ -111,3 +111,7 @@ export {
111
111
  PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR,
112
112
  } from "./mcp.js";
113
113
  export type { JsonRpcMessage, McpToolDefinition, McpResourceDefinition, JsonSchema, McpToolParam } from "./mcp.js";
114
+ export { Plan } from "./plan.js";
115
+ export type { PlanStep, ParsedPlan, PlanSummary, ExecutionSummary, CurrentPlan } from "./plan.js";
116
+ export { ProjectIndex } from "./projectIndex.js";
117
+ export type { FileEntry, FileRoute } from "./projectIndex.js";