@qlucent/fishi 0.5.0 → 0.7.0

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.
Files changed (2) hide show
  1. package/dist/index.js +328 -4
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import chalk6 from "chalk";
5
+ import chalk9 from "chalk";
6
6
 
7
7
  // src/commands/init.ts
8
8
  import chalk from "chalk";
@@ -10,7 +10,7 @@ import ora from "ora";
10
10
  import inquirer from "inquirer";
11
11
  import path3 from "path";
12
12
  import fs3 from "fs";
13
- import { detectConflicts, createBackup } from "@qlucent/fishi-core";
13
+ import { detectConflicts, createBackup, detectDocker } from "@qlucent/fishi-core";
14
14
 
15
15
  // src/analyzers/detector.ts
16
16
  import fs from "fs";
@@ -1005,6 +1005,23 @@ async function initCommand(description, options) {
1005
1005
  } else {
1006
1006
  initOptions = await runWizard(options);
1007
1007
  }
1008
+ const dockerAvailable = detectDocker();
1009
+ let sandboxMode = "process";
1010
+ if (options.interactive !== false) {
1011
+ if (dockerAvailable) {
1012
+ const { useSandbox } = await inquirer.prompt([{
1013
+ type: "confirm",
1014
+ name: "useSandbox",
1015
+ message: "Docker detected. Use Docker sandbox for agent isolation? (Recommended)",
1016
+ default: true
1017
+ }]);
1018
+ sandboxMode = useSandbox ? "docker" : "process";
1019
+ } else {
1020
+ console.log(chalk.yellow(" Docker not found. Using process-level sandbox (limited isolation)."));
1021
+ console.log(chalk.gray(" Install Docker for full agent isolation: https://docs.docker.com/get-docker/"));
1022
+ console.log("");
1023
+ }
1024
+ }
1008
1025
  let brownfieldAnalysis = null;
1009
1026
  if (detection.type === "brownfield" || detection.type === "hybrid") {
1010
1027
  console.log("");
@@ -1148,6 +1165,21 @@ async function initCommand(description, options) {
1148
1165
  const report = generateBrownfieldReport(brownfieldAnalysis);
1149
1166
  fs3.writeFileSync(reportPath, report, "utf-8");
1150
1167
  }
1168
+ const sandboxYaml = `
1169
+ sandbox:
1170
+ mode: ${sandboxMode}
1171
+ docker_available: ${dockerAvailable}
1172
+ `;
1173
+ const fishiYamlPath = path3.join(targetDir, ".fishi", "fishi.yaml");
1174
+ if (fs3.existsSync(fishiYamlPath)) {
1175
+ fs3.appendFileSync(fishiYamlPath, sandboxYaml, "utf-8");
1176
+ }
1177
+ const { getSandboxPolicyTemplate, getDockerfileTemplate } = await import("@qlucent/fishi-core");
1178
+ fs3.writeFileSync(path3.join(targetDir, ".fishi", "sandbox-policy.yaml"), getSandboxPolicyTemplate(), "utf-8");
1179
+ if (sandboxMode === "docker") {
1180
+ fs3.mkdirSync(path3.join(targetDir, ".fishi", "docker"), { recursive: true });
1181
+ fs3.writeFileSync(path3.join(targetDir, ".fishi", "docker", "Dockerfile"), getDockerfileTemplate(), "utf-8");
1182
+ }
1151
1183
  scaffoldSpinner.succeed("FISHI framework scaffolded successfully!");
1152
1184
  console.log("");
1153
1185
  console.log(chalk.bold(" Created:"));
@@ -1158,6 +1190,7 @@ async function initCommand(description, options) {
1158
1190
  console.log(chalk.gray(` \u{1F4C4} .claude/CLAUDE.md \u2014 Project instructions`));
1159
1191
  console.log(chalk.gray(` \u{1F4C4} .claude/settings.json \u2014 Hooks & permissions`));
1160
1192
  console.log(chalk.gray(` \u{1F4C4} .mcp.json \u2014 MCP server config`));
1193
+ console.log(chalk.gray(` \u{1F512} Sandbox: ${sandboxMode} mode${sandboxMode === "docker" ? " (full isolation)" : " (limited isolation)"}`));
1161
1194
  if (brownfieldAnalysis) {
1162
1195
  console.log(chalk.gray(` \u{1F4C4} .fishi/memory/brownfield-analysis.md \u2014 Codebase analysis report`));
1163
1196
  console.log("");
@@ -1649,11 +1682,299 @@ async function validateCommand() {
1649
1682
  }
1650
1683
  }
1651
1684
 
1685
+ // src/commands/monitor.ts
1686
+ import chalk6 from "chalk";
1687
+ import fs8 from "fs";
1688
+ import path8 from "path";
1689
+ import { parse as parseYaml4 } from "yaml";
1690
+ import { readMonitorState, getAgentSummary } from "@qlucent/fishi-core";
1691
+ var PHASES = ["init", "discovery", "planning", "development", "testing", "review", "deployed"];
1692
+ function formatNumber(n) {
1693
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1694
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1695
+ return String(n);
1696
+ }
1697
+ function renderPhaseBar(currentPhase) {
1698
+ const idx = PHASES.indexOf(currentPhase);
1699
+ const parts = PHASES.map((p, i) => {
1700
+ if (i < idx) return chalk6.green(`[${p}]`);
1701
+ if (i === idx) return chalk6.cyan.bold(`[${p}]`);
1702
+ return chalk6.gray(`[${p}]`);
1703
+ });
1704
+ return " " + parts.join(chalk6.gray(" \u2192 "));
1705
+ }
1706
+ function renderDivider(label) {
1707
+ const line = "\u2500".repeat(60);
1708
+ return chalk6.gray(`
1709
+ ${line}
1710
+ `) + chalk6.bold(label) + "\n";
1711
+ }
1712
+ function renderMonitor(projectDir) {
1713
+ const state = readMonitorState(projectDir);
1714
+ const agentSummary = getAgentSummary(projectDir);
1715
+ let currentPhase = "init";
1716
+ const projectYamlPath = path8.join(projectDir, ".fishi", "state", "project.yaml");
1717
+ if (fs8.existsSync(projectYamlPath)) {
1718
+ try {
1719
+ const projectState = parseYaml4(fs8.readFileSync(projectYamlPath, "utf-8"));
1720
+ currentPhase = projectState?.current_phase || "init";
1721
+ } catch {
1722
+ }
1723
+ }
1724
+ let gates = [];
1725
+ const gatesYamlPath = path8.join(projectDir, ".fishi", "state", "gates.yaml");
1726
+ if (fs8.existsSync(gatesYamlPath)) {
1727
+ try {
1728
+ const gatesData = parseYaml4(fs8.readFileSync(gatesYamlPath, "utf-8"));
1729
+ gates = gatesData?.gates || [];
1730
+ } catch {
1731
+ }
1732
+ }
1733
+ const { summary, dynamicAgents, events, lastUpdated } = state;
1734
+ console.log("");
1735
+ console.log(chalk6.cyan.bold(" FISHI Agent Monitor"));
1736
+ console.log(chalk6.gray(` Last updated: ${new Date(lastUpdated).toLocaleTimeString()}`));
1737
+ console.log(renderDivider("Phase Progress"));
1738
+ console.log(renderPhaseBar(currentPhase));
1739
+ console.log("");
1740
+ console.log(renderDivider("Summary"));
1741
+ console.log(
1742
+ ` ${chalk6.bold("Completions:")} ${chalk6.cyan(summary.totalAgentCompletions)} ${chalk6.bold("Files Changed:")} ${chalk6.cyan(summary.totalFilesChanged)} ${chalk6.bold("Tokens:")} ${chalk6.cyan(formatNumber(summary.totalTokens))} ${chalk6.bold("Dynamic Agents:")} ${chalk6.cyan(summary.dynamicAgentsCreated)}`
1743
+ );
1744
+ console.log(renderDivider("Tokens by Model"));
1745
+ const modelEntries = Object.entries(summary.tokensByModel);
1746
+ if (modelEntries.length === 0) {
1747
+ console.log(chalk6.gray(" (none yet)"));
1748
+ } else {
1749
+ for (const [model, tokens] of modelEntries.sort((a, b) => b[1] - a[1])) {
1750
+ console.log(` ${chalk6.yellow(model.padEnd(30))} ${chalk6.cyan(formatNumber(tokens))}`);
1751
+ }
1752
+ }
1753
+ console.log(renderDivider("Tools Used"));
1754
+ const toolEntries = Object.entries(summary.toolsUsed);
1755
+ if (toolEntries.length === 0) {
1756
+ console.log(chalk6.gray(" (none yet)"));
1757
+ } else {
1758
+ for (const [tool, count] of toolEntries.sort((a, b) => b[1] - a[1]).slice(0, 10)) {
1759
+ console.log(` ${chalk6.yellow(tool.padEnd(30))} ${chalk6.cyan(count)}`);
1760
+ }
1761
+ }
1762
+ console.log(renderDivider("Agent Activity"));
1763
+ const agentEntries = Object.entries(agentSummary);
1764
+ if (agentEntries.length === 0) {
1765
+ console.log(chalk6.gray(" (no agent events yet)"));
1766
+ } else {
1767
+ for (const [agent, stats] of agentEntries.sort((a, b) => b[1].completions - a[1].completions)) {
1768
+ const failColor = stats.failures > 0 ? chalk6.red : chalk6.gray;
1769
+ console.log(
1770
+ ` ${chalk6.magenta(agent.padEnd(28))} completions: ${chalk6.cyan(stats.completions)} failures: ${failColor(stats.failures)} files: ${chalk6.cyan(stats.filesChanged)}`
1771
+ );
1772
+ }
1773
+ }
1774
+ if (dynamicAgents.length > 0) {
1775
+ console.log(renderDivider("Dynamic Agents"));
1776
+ for (const da of dynamicAgents) {
1777
+ console.log(` ${chalk6.magenta(da.name)} ${chalk6.gray("\u2190")} ${chalk6.gray(da.coordinator)}`);
1778
+ }
1779
+ }
1780
+ if (gates.length > 0) {
1781
+ console.log(renderDivider("Gates"));
1782
+ for (const gate of gates) {
1783
+ const statusColor = gate.status === "passed" ? chalk6.green : gate.status === "failed" ? chalk6.red : chalk6.yellow;
1784
+ console.log(` ${statusColor("\u25CF")} ${gate.name.padEnd(30)} ${statusColor(gate.status)}`);
1785
+ }
1786
+ }
1787
+ console.log(renderDivider("Recent Events (last 10)"));
1788
+ const recentEvents = events.slice(-10).reverse();
1789
+ if (recentEvents.length === 0) {
1790
+ console.log(chalk6.gray(" (no events yet)"));
1791
+ } else {
1792
+ for (const ev of recentEvents) {
1793
+ const ts = new Date(ev.timestamp).toLocaleTimeString();
1794
+ console.log(
1795
+ ` ${chalk6.gray(ts)} ${chalk6.cyan(ev.type.padEnd(20))} ${chalk6.magenta(ev.agent)}`
1796
+ );
1797
+ }
1798
+ }
1799
+ console.log("");
1800
+ }
1801
+ async function monitorCommand(options) {
1802
+ const projectDir = process.cwd();
1803
+ const fishiDir = path8.join(projectDir, ".fishi");
1804
+ if (!fs8.existsSync(fishiDir)) {
1805
+ console.log(chalk6.yellow("\n FISHI is not initialized in this directory."));
1806
+ console.log(chalk6.gray(" Run `fishi init` to get started.\n"));
1807
+ return;
1808
+ }
1809
+ if (!options.watch) {
1810
+ renderMonitor(projectDir);
1811
+ return;
1812
+ }
1813
+ const monitorJsonPath = path8.join(fishiDir, "state", "monitor.json");
1814
+ let lastMtime = 0;
1815
+ const render = () => {
1816
+ process.stdout.write("\x1B[2J\x1B[0;0H");
1817
+ renderMonitor(projectDir);
1818
+ console.log(chalk6.gray(" [watch mode \u2014 polling every 1s, Ctrl+C to exit]"));
1819
+ };
1820
+ render();
1821
+ const interval = setInterval(() => {
1822
+ try {
1823
+ if (fs8.existsSync(monitorJsonPath)) {
1824
+ const mtime = fs8.statSync(monitorJsonPath).mtimeMs;
1825
+ if (mtime !== lastMtime) {
1826
+ lastMtime = mtime;
1827
+ render();
1828
+ }
1829
+ }
1830
+ } catch {
1831
+ }
1832
+ }, 1e3);
1833
+ process.on("SIGINT", () => {
1834
+ clearInterval(interval);
1835
+ console.log(chalk6.gray("\n Monitor exited.\n"));
1836
+ process.exit(0);
1837
+ });
1838
+ }
1839
+
1840
+ // src/commands/dashboard.ts
1841
+ import http from "http";
1842
+ import fs9 from "fs";
1843
+ import path9 from "path";
1844
+ import { parse as parseYaml5 } from "yaml";
1845
+ import chalk7 from "chalk";
1846
+ import { readMonitorState as readMonitorState2, getAgentSummary as getAgentSummary2, getDashboardHtml } from "@qlucent/fishi-core";
1847
+ var DEFAULT_PORT = 4269;
1848
+ async function dashboardCommand(options) {
1849
+ const projectDir = process.cwd();
1850
+ const fishiDir = path9.join(projectDir, ".fishi");
1851
+ if (!fs9.existsSync(fishiDir)) {
1852
+ console.log(chalk7.yellow("\n FISHI is not initialized in this directory."));
1853
+ console.log(chalk7.gray(" Run `fishi init` to get started.\n"));
1854
+ return;
1855
+ }
1856
+ const port = Number(options.port || DEFAULT_PORT);
1857
+ if (isNaN(port) || port < 1 || port > 65535) {
1858
+ console.error(chalk7.red(` Invalid port: ${options.port}`));
1859
+ process.exit(1);
1860
+ }
1861
+ const html = getDashboardHtml();
1862
+ const server = http.createServer((req, res) => {
1863
+ const url = req.url?.split("?")[0] ?? "/";
1864
+ if (url === "/") {
1865
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1866
+ res.end(html);
1867
+ return;
1868
+ }
1869
+ if (url === "/api/state") {
1870
+ try {
1871
+ const monitorState = readMonitorState2(projectDir);
1872
+ const agentSummary = getAgentSummary2(projectDir);
1873
+ let phase = "init";
1874
+ const projectYamlPath = path9.join(fishiDir, "state", "project.yaml");
1875
+ if (fs9.existsSync(projectYamlPath)) {
1876
+ try {
1877
+ const projectState = parseYaml5(fs9.readFileSync(projectYamlPath, "utf-8"));
1878
+ phase = projectState?.current_phase || "init";
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ let gates = [];
1883
+ const gatesYamlPath = path9.join(fishiDir, "state", "gates.yaml");
1884
+ if (fs9.existsSync(gatesYamlPath)) {
1885
+ try {
1886
+ const gatesData = parseYaml5(fs9.readFileSync(gatesYamlPath, "utf-8"));
1887
+ gates = gatesData?.gates || [];
1888
+ } catch {
1889
+ }
1890
+ }
1891
+ const payload = {
1892
+ summary: monitorState.summary,
1893
+ events: monitorState.events,
1894
+ dynamicAgents: monitorState.dynamicAgents,
1895
+ lastUpdated: monitorState.lastUpdated,
1896
+ agentSummary,
1897
+ phase,
1898
+ gates
1899
+ };
1900
+ res.writeHead(200, {
1901
+ "Content-Type": "application/json",
1902
+ "Access-Control-Allow-Origin": "*"
1903
+ });
1904
+ res.end(JSON.stringify(payload));
1905
+ } catch (err) {
1906
+ res.writeHead(500, { "Content-Type": "application/json" });
1907
+ res.end(JSON.stringify({ error: String(err) }));
1908
+ }
1909
+ return;
1910
+ }
1911
+ res.writeHead(404, { "Content-Type": "text/plain" });
1912
+ res.end("Not found");
1913
+ });
1914
+ server.listen(port, () => {
1915
+ console.log("");
1916
+ console.log(chalk7.cyan.bold(" FISHI Agent Dashboard"));
1917
+ console.log("");
1918
+ console.log(` ${chalk7.bold("URL:")} ${chalk7.cyan(`http://localhost:${port}`)}`);
1919
+ console.log(` ${chalk7.bold("API:")} ${chalk7.gray(`http://localhost:${port}/api/state`)}`);
1920
+ console.log("");
1921
+ console.log(chalk7.gray(" Press Ctrl+C to stop.\n"));
1922
+ });
1923
+ process.on("SIGINT", () => {
1924
+ server.close(() => {
1925
+ console.log(chalk7.gray("\n Dashboard stopped.\n"));
1926
+ process.exit(0);
1927
+ });
1928
+ });
1929
+ await new Promise((resolve) => {
1930
+ server.on("close", resolve);
1931
+ });
1932
+ }
1933
+
1934
+ // src/commands/sandbox.ts
1935
+ import chalk8 from "chalk";
1936
+ import fs10 from "fs";
1937
+ import path10 from "path";
1938
+ import { detectDocker as detectDocker2, readSandboxConfig, readSandboxPolicy } from "@qlucent/fishi-core";
1939
+ async function sandboxCommand(action) {
1940
+ const targetDir = process.cwd();
1941
+ if (!fs10.existsSync(path10.join(targetDir, ".fishi"))) {
1942
+ console.log(chalk8.yellow(" No FISHI project found. Run `fishi init` first."));
1943
+ return;
1944
+ }
1945
+ if (action === "status") {
1946
+ const config = readSandboxConfig(targetDir);
1947
+ const dockerNow = detectDocker2();
1948
+ console.log("");
1949
+ console.log(chalk8.cyan.bold(" FISHI Sandbox Status"));
1950
+ console.log("");
1951
+ console.log(chalk8.white(" Mode: ") + (config.mode === "docker" ? chalk8.green("Docker") : chalk8.yellow("Process")));
1952
+ console.log(chalk8.white(" Docker available: ") + (dockerNow ? chalk8.green("yes") : chalk8.red("no")));
1953
+ if (config.mode === "docker" && !dockerNow) {
1954
+ console.log(chalk8.red(" Warning: Docker mode configured but Docker not available!"));
1955
+ }
1956
+ console.log("");
1957
+ const policy = readSandboxPolicy(targetDir);
1958
+ console.log(chalk8.white.bold(" Policy"));
1959
+ console.log(chalk8.gray(` Timeout: ${policy.timeout}s`));
1960
+ console.log(chalk8.gray(` Memory limit: ${policy.memory}`));
1961
+ console.log(chalk8.gray(` CPU limit: ${policy.cpus}`));
1962
+ console.log(chalk8.gray(` Network allow: ${policy.networkAllow.join(", ")}`));
1963
+ console.log(chalk8.gray(` Env passthrough: ${policy.envPassthrough.length > 0 ? policy.envPassthrough.join(", ") : "(none)"}`));
1964
+ console.log("");
1965
+ } else if (action === "policy") {
1966
+ const policy = readSandboxPolicy(targetDir);
1967
+ console.log(JSON.stringify(policy, null, 2));
1968
+ } else {
1969
+ console.log(chalk8.yellow(` Unknown action: ${action}. Use: status, policy`));
1970
+ }
1971
+ }
1972
+
1652
1973
  // src/index.ts
1653
1974
  var program = new Command();
1654
1975
  program.name("fishi").description(
1655
- chalk6.cyan("\u{1F41F} FISHI") + " \u2014 Your AI Dev Team That Actually Ships\n Autonomous agent framework for Claude Code"
1656
- ).version("0.5.0");
1976
+ chalk9.cyan("\u{1F41F} FISHI") + " \u2014 Your AI Dev Team That Actually Ships\n Autonomous agent framework for Claude Code"
1977
+ ).version("0.7.0");
1657
1978
  program.command("init").description("Initialize FISHI in the current directory").argument("[description]", "Project description (skip wizard with zero-config)").option("-l, --language <lang>", "Primary language (e.g., typescript, python)").option("-f, --framework <framework>", "Framework (e.g., nextjs, express, django)").option(
1658
1979
  "-c, --cost-mode <mode>",
1659
1980
  "Cost mode: performance | balanced | economy",
@@ -1663,4 +1984,7 @@ program.command("status").description("Show project status, active agents, and T
1663
1984
  program.command("mcp").description("Manage MCP server integrations").argument("<action>", "Action: add | list | remove").argument("[name]", "MCP server name").action(mcpCommand);
1664
1985
  program.command("reset").description("Rollback to a previous checkpoint").argument("[checkpoint]", "Checkpoint ID (defaults to latest)").action(resetCommand);
1665
1986
  program.command("validate").description("Validate scaffold integrity \u2014 checks files, frontmatter, cross-references, pipeline, and permissions").action(validateCommand);
1987
+ program.command("monitor").description("Agent observability \u2014 TUI dashboard showing agent activity, tokens, gates").option("-w, --watch", "Watch mode \u2014 auto-refresh on changes").action(monitorCommand);
1988
+ program.command("dashboard").description("Agent observability \u2014 web dashboard at http://localhost:4269").option("-p, --port <port>", "Port number", "4269").action(dashboardCommand);
1989
+ program.command("sandbox").description("Sandbox status and policy management").argument("<action>", "Action: status | policy").action(sandboxCommand);
1666
1990
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlucent/fishi",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "FISHI — Your AI Dev Team That Actually Ships. Autonomous agent framework for Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",