nou-cli 0.1.0 → 0.2.1

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 (45) hide show
  1. package/README.md +142 -31
  2. package/dist/grammars/tree-sitter-bash.wasm +0 -0
  3. package/dist/grammars/tree-sitter-c.wasm +0 -0
  4. package/dist/grammars/tree-sitter-css.wasm +0 -0
  5. package/dist/grammars/tree-sitter-go.wasm +0 -0
  6. package/dist/grammars/tree-sitter-html.wasm +0 -0
  7. package/dist/grammars/tree-sitter-java.wasm +0 -0
  8. package/dist/grammars/tree-sitter-javascript.wasm +0 -0
  9. package/dist/grammars/tree-sitter-json.wasm +0 -0
  10. package/dist/grammars/tree-sitter-python.wasm +0 -0
  11. package/dist/grammars/tree-sitter-rust.wasm +0 -0
  12. package/dist/grammars/tree-sitter-toml.wasm +0 -0
  13. package/dist/grammars/tree-sitter-tsx.wasm +0 -0
  14. package/dist/grammars/tree-sitter-typescript.wasm +0 -0
  15. package/dist/grammars/tree-sitter-yaml.wasm +0 -0
  16. package/dist/index.js +55 -6
  17. package/dist/src/agent/index.js +37 -5
  18. package/dist/src/commands/index.js +16 -6
  19. package/dist/src/config/index.js +6 -2
  20. package/dist/src/diagnostics/index.js +235 -0
  21. package/dist/src/diagnostics/process.js +64 -0
  22. package/dist/src/diagnostics/providers.js +103 -0
  23. package/dist/src/diagnostics/types.js +1 -0
  24. package/dist/src/env.js +12 -0
  25. package/dist/src/history/index.js +113 -0
  26. package/dist/src/lsp/client.js +169 -0
  27. package/dist/src/lsp/index.js +169 -0
  28. package/dist/src/plugins/index.js +94 -3
  29. package/dist/src/providers/ids.js +3 -0
  30. package/dist/src/providers/ollama.js +2 -1
  31. package/dist/src/providers/openai.js +3 -2
  32. package/dist/src/syntax/index.js +240 -0
  33. package/dist/src/ui/App.js +299 -30
  34. package/dist/src/ui/Chat.js +19 -6
  35. package/dist/src/ui/ChatInput.js +128 -0
  36. package/dist/src/ui/CodeView.js +25 -5
  37. package/dist/src/ui/Editor.js +94 -21
  38. package/dist/src/ui/FileTree.js +23 -4
  39. package/dist/src/ui/Header.js +4 -2
  40. package/dist/src/ui/ProblemsView.js +75 -0
  41. package/dist/src/ui/Settings.js +31 -15
  42. package/dist/src/ui/mouse.js +52 -0
  43. package/dist/src/ui/text.js +6 -1
  44. package/dist/src/update/index.js +102 -0
  45. package/package.json +10 -5
@@ -2,13 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useState } from "react";
3
3
  import { Box, Text, useAnimation, useInput } from "ink";
4
4
  import TextInput from "ink-text-input";
5
- import { EFFORTS } from "../config/index.js";
6
- import { PLUGIN_DIRS } from "../plugins/index.js";
5
+ import { AUTO_SAVE, EFFORTS } from "../config/index.js";
6
+ import { PLUGIN_DIRS, isEnabled, trustState } from "../plugins/index.js";
7
+ import { currentVersion } from "../update/index.js";
7
8
  import { describe, providers } from "../providers/index.js";
8
9
  import { C, SPINNER } from "./theme.js";
9
10
  const TABS = ["Model", "Preferences", "Plugins"];
10
11
  const PREFS = [
11
12
  { key: "effort", label: "Reasoning effort", help: "How hard Claude thinks (low → max). Higher is smarter but slower and pricier." },
13
+ { key: "autoSave", label: "Auto-save", help: "delay: a second after you stop typing · focus: when you leave the editor · off: only Ctrl+S." },
14
+ { key: "mouse", label: "Mouse", help: "Wheel scrolls the pane under the pointer; click to focus or open files. Hold Shift to select text." },
12
15
  { key: "followMode", label: "Follow mode on start", help: "The code view jumps to files Nou reads and edits." },
13
16
  { key: "gitContext", label: "Git context", help: "Give the model your branch, changes and commits, plus git tools." },
14
17
  { key: "autoApproveEdits", label: "Auto-approve edits", help: "Apply file edits without asking (diffs are still shown).", warn: true },
@@ -86,10 +89,13 @@ export function Settings({ width, height, settings, choice, plugins, onModel, on
86
89
  }
87
90
  else if (tab === "Preferences" && (key.return || input === " " || key.leftArrow || key.rightArrow)) {
88
91
  const pref = PREFS[row];
92
+ const step = key.leftArrow ? -1 : 1;
89
93
  if (pref.key === "effort") {
90
- const step = key.leftArrow ? -1 : 1;
91
94
  onChange({ effort: EFFORTS[(EFFORTS.indexOf(settings.effort) + step + EFFORTS.length) % EFFORTS.length] });
92
95
  }
96
+ else if (pref.key === "autoSave") {
97
+ onChange({ autoSave: AUTO_SAVE[(AUTO_SAVE.indexOf(settings.autoSave) + step + AUTO_SAVE.length) % AUTO_SAVE.length] });
98
+ }
93
99
  else {
94
100
  onChange({ [pref.key]: !settings[pref.key] });
95
101
  }
@@ -98,7 +104,7 @@ export function Settings({ width, height, settings, choice, plugins, onModel, on
98
104
  if (input === "r")
99
105
  onRescanPlugins();
100
106
  else if ((key.return || input === " ") && plugins[row])
101
- onTogglePlugin(plugins[row].id, !settings.plugins[plugins[row].id]);
107
+ onTogglePlugin(plugins[row].id, !isEnabled(plugins[row], settings));
102
108
  }
103
109
  });
104
110
  const boxHeight = height - 6;
@@ -130,21 +136,31 @@ export function Settings({ width, height, settings, choice, plugins, onModel, on
130
136
  return (_jsx(Box, { flexDirection: "column", height: boxHeight, borderStyle: "round", borderColor: C.accent, paddingX: 1, children: PREFS.map((pref, i) => {
131
137
  const selected = i === row;
132
138
  const value = settings[pref.key];
133
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { ...(selected ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { color: selected ? C.accent2 : C.dim, children: selected ? "▸ " : " " }), _jsx(Text, { color: C.text, bold: selected, children: pref.label.padEnd(26) }), pref.key === "effort"
134
- ? _jsx(Text, { children: EFFORTS.map((e) => _jsx(Text, { color: e === value ? C.accent2 : C.dim, bold: e === value, children: e === value ? `[${e}]` : ` ${e} ` }, e)) })
139
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { ...(selected ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { color: selected ? C.accent2 : C.dim, children: selected ? "▸ " : " " }), _jsx(Text, { color: C.text, bold: selected, children: pref.label.padEnd(26) }), pref.key === "effort" || pref.key === "autoSave"
140
+ ? _jsx(Text, { children: (pref.key === "effort" ? EFFORTS : AUTO_SAVE).map((e) => _jsx(Text, { color: e === value ? C.accent2 : C.dim, bold: e === value, children: e === value ? `[${e}]` : ` ${e} ` }, e)) })
135
141
  : toggle(value, pref.warn)] }), _jsxs(Text, { color: C.dim, children: [" ", pref.help] })] }, pref.key));
136
142
  }) }));
137
143
  }
138
- return (_jsx(Box, { flexDirection: "column", height: boxHeight, borderStyle: "round", borderColor: C.accent, paddingX: 1, children: plugins.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: C.text, children: "No plugins found." }), _jsx(Text, { children: " " }), _jsx(Text, { color: C.dim, children: "Drop a .js/.mjs/.ts file (or a folder with index.*) into one of:" }), PLUGIN_DIRS.map((d) => _jsxs(Text, { color: C.cyan, children: [" ", d.dir, " ", _jsxs(Text, { color: C.dim, children: ["(", d.scope, ")"] })] }, d.dir)), _jsx(Text, { children: " " }), _jsx(Text, { color: C.dim, children: "See examples/plugins/word-count.mjs for the shape. Press r to rescan." })] })) : plugins.map((p, i) => {
139
- const selected = i === row;
140
- const enabled = !!settings.plugins[p.id];
141
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { ...(selected ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { color: selected ? C.accent2 : C.dim, children: selected ? "▸ " : " " }), toggle(enabled), _jsxs(Text, { color: C.text, bold: true, children: [" ", p.plugin?.name ?? p.id] }), _jsxs(Text, { color: C.dim, children: [" ", p.plugin?.version ? `v${p.plugin.version} · ` : "", p.scope] })] }), p.error
142
- ? _jsxs(Text, { color: C.red, children: [" \u2716 ", p.error] })
143
- : _jsxs(Text, { color: C.dim, children: [" ", p.plugin?.description ?? (enabled ? "" : "Enable to load it."), p.plugin ? ` [${(p.plugin.tools ?? []).length} tools, ${(p.plugin.commands ?? []).length} commands]` : ""] })] }, p.id));
144
- }) }));
144
+ return (_jsxs(Box, { flexDirection: "column", height: boxHeight, borderStyle: "round", borderColor: C.accent, paddingX: 1, children: [plugins.map((p, i) => {
145
+ const selected = i === row;
146
+ const state = trustState(p, settings);
147
+ const enabled = state === "on";
148
+ const parts = p.plugin
149
+ ? [
150
+ (p.plugin.tools ?? []).length && `${p.plugin.tools.length} tools`,
151
+ (p.plugin.commands ?? []).length && `${p.plugin.commands.length} commands`,
152
+ (p.plugin.diagnostics ?? []).length && "checker",
153
+ ].filter(Boolean)
154
+ : [];
155
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...(selected ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { color: selected ? C.accent2 : C.dim, children: selected ? "▸ " : " " }), toggle(enabled), _jsxs(Text, { color: C.text, bold: true, children: [" ", p.plugin?.name ?? p.id] }), _jsxs(Text, { color: C.dim, children: [" ", p.plugin?.version ? `v${p.plugin.version} · ` : "", p.scope, parts.length ? ` · ${parts.join(", ")}` : ""] })] }), state === "changed"
156
+ ? _jsx(Text, { color: C.yellow, wrap: "truncate-end", children: " \u26A0 Changed since you enabled it. Review the code, then press Enter to enable again." })
157
+ : p.error
158
+ ? _jsxs(Text, { color: C.red, wrap: "truncate-end", children: [" \u2716 ", p.error] })
159
+ : _jsxs(Text, { color: C.dim, wrap: "truncate-end", children: [" ", p.plugin?.description ?? (enabled ? "" : "Enable to load it.")] })] }, p.id));
160
+ }), _jsx(Text, { children: " " }), _jsxs(Text, { color: C.dim, wrap: "truncate-end", children: ["Add your own: drop a .js/.mjs file into ", PLUGIN_DIRS.map((d) => d.dir).join(" or "), ", then press r."] }), _jsx(Text, { color: C.dim, wrap: "truncate-end", children: "Example: examples/plugins/word-count.mjs in the Nou repo." })] }));
145
161
  };
146
162
  const hints = tab === "Model"
147
163
  ? column === "models" ? "↑↓ choose · Enter use model · ← providers" : "↑↓ provider · Enter/→ pick a model"
148
- : tab === "Preferences" ? "↑↓ move · Enter/Space toggle · ←→ change effort" : "↑↓ move · Enter toggle · r rescan";
149
- return (_jsxs(Box, { flexDirection: "column", width: width, height: height, paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: C.accent2, bold: true, children: "\u2699 Settings " }), TABS.map((t) => (_jsx(Text, { ...(t === tab ? { backgroundColor: C.accent2, color: "black" } : { color: C.dim }), bold: t === tab, children: ` ${t} ` }, t))), _jsx(Text, { color: C.dim, children: " current: " }), _jsx(Text, { color: C.green, children: describe(choice) })] }), _jsx(Text, { children: " " }), body(), _jsxs(Text, { color: C.dim, children: [" Tab next section \u00B7 ", hints, " \u00B7 Esc close ", _jsx(Text, { color: C.dim, children: "saved to ~/.nou/config.json" })] })] }));
164
+ : tab === "Preferences" ? "↑↓ move · Enter/Space toggle · ←→ change option" : "↑↓ move · Enter toggle · r rescan";
165
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, paddingX: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: C.accent2, bold: true, children: "\u2699 Settings " }), TABS.map((t) => (_jsx(Text, { ...(t === tab ? { backgroundColor: C.accent2, color: "black" } : { color: C.dim }), bold: t === tab, children: ` ${t} ` }, t))), _jsx(Text, { color: C.dim, children: " current: " }), _jsx(Text, { color: C.green, children: describe(choice) }), _jsxs(Text, { color: C.dim, children: [" nou v", currentVersion()] })] }), _jsx(Text, { children: " " }), body(), _jsxs(Text, { color: C.dim, children: [" Tab next section \u00B7 ", hints, " \u00B7 Esc close ", _jsx(Text, { color: C.dim, children: "saved to ~/.nou/config.json" })] })] }));
150
166
  }
@@ -0,0 +1,52 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useStdin } from "ink";
3
+ // Report clicks and wheel (1000) using SGR coordinates (1006), which work past column 223.
4
+ const ENABLE = "\x1b[?1000h\x1b[?1006h";
5
+ const DISABLE = "\x1b[?1000l\x1b[?1006l";
6
+ /** Mouse events in a chunk of terminal input (SGR format: ESC [ < button ; x ; y M|m). */
7
+ export function parseMouse(chunk) {
8
+ const events = [];
9
+ // eslint-disable-next-line no-control-regex -- mouse reports start with ESC
10
+ for (const m of chunk.matchAll(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g)) {
11
+ const code = Number(m[1]);
12
+ const x = Number(m[2]) - 1;
13
+ const y = Number(m[3]) - 1;
14
+ const button = code & 0b11000011; // drop the shift/alt/ctrl bits
15
+ if (button === 64 || button === 65)
16
+ events.push({ kind: "wheel", dir: button === 64 ? -1 : 1, x, y });
17
+ else if (button === 0 && m[4] === "M")
18
+ events.push({ kind: "click", x, y });
19
+ }
20
+ return events;
21
+ }
22
+ let exitHookInstalled = false;
23
+ /**
24
+ * Turns on mouse reporting while mounted and calls `handler` for wheel and click events.
25
+ * Ink keeps reading the keyboard as usual: a 'data' listener sees the same chunks Ink reads, and Ink
26
+ * already ignores mouse sequences, so they never show up as typed text.
27
+ */
28
+ export function useMouse(handler, enabled) {
29
+ const { stdin } = useStdin();
30
+ const latest = useRef(handler);
31
+ latest.current = handler;
32
+ useEffect(() => {
33
+ if (!enabled || !stdin)
34
+ return;
35
+ const terminal = process.stdout.isTTY ? process.stdout : null;
36
+ terminal?.write(ENABLE);
37
+ if (terminal && !exitHookInstalled) {
38
+ // Leave the terminal clean even if Nou crashes; otherwise the shell gets mouse garbage.
39
+ process.on("exit", () => process.stdout.write(DISABLE));
40
+ exitHookInstalled = true;
41
+ }
42
+ const onData = (data) => {
43
+ for (const event of parseMouse(String(data)))
44
+ latest.current(event);
45
+ };
46
+ stdin.on("data", onData);
47
+ return () => {
48
+ stdin.off("data", onData);
49
+ terminal?.write(DISABLE);
50
+ };
51
+ }, [enabled, stdin]);
52
+ }
@@ -1,6 +1,7 @@
1
1
  import path from "path";
2
2
  import { highlight, supportsLanguage } from "cli-highlight";
3
3
  import { diffLines } from "diff";
4
+ import { highlightTree } from "../syntax/index.js";
4
5
  const LANGUAGES = {
5
6
  ".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
6
7
  ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
@@ -16,8 +17,12 @@ export function splitLines(text) {
16
17
  return lines;
17
18
  }
18
19
  export function highlightLines(filePath, text) {
19
- const language = LANGUAGES[path.extname(filePath).toLowerCase()];
20
20
  const plain = splitLines(text);
21
+ // tree-sitter when the grammar is loaded (accurate); highlight.js otherwise.
22
+ const tree = highlightTree(filePath, plain.join("\n"));
23
+ if (tree)
24
+ return tree;
25
+ const language = LANGUAGES[path.extname(filePath).toLowerCase()];
21
26
  if (!language || !supportsLanguage(language) || text.length > 200_000)
22
27
  return plain;
23
28
  try {
@@ -0,0 +1,102 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { NOU_HOME } from "../config/index.js";
6
+ export const PACKAGE_NAME = "nou-cli";
7
+ const CACHE = path.join(NOU_HOME, "update.json");
8
+ const DAY = 24 * 60 * 60 * 1000;
9
+ /** Nou's own package.json (works from source and from the installed dist build). */
10
+ function ownPackage() {
11
+ let dir = path.dirname(fileURLToPath(import.meta.url));
12
+ for (let i = 0; i < 5; i++) {
13
+ const file = path.join(dir, "package.json");
14
+ if (existsSync(file)) {
15
+ const pkg = JSON.parse(readFileSync(file, "utf-8"));
16
+ if (pkg.name === PACKAGE_NAME)
17
+ return { version: pkg.version, dir };
18
+ }
19
+ dir = path.dirname(dir);
20
+ }
21
+ return { version: "0.0.0", dir };
22
+ }
23
+ export const currentVersion = () => ownPackage().version;
24
+ /** Running from a git checkout (npm run dev) rather than an npm install: never nag about updates. */
25
+ export const isDevCheckout = () => existsSync(path.join(ownPackage().dir, ".git"));
26
+ /** -1, 0 or 1. Plain x.y.z comparison; pre-release tags sort before the release. */
27
+ export function compareVersions(a, b) {
28
+ const parse = (v) => {
29
+ const [main, pre] = v.replace(/^v/, "").split("-", 2);
30
+ return { nums: main.split(".").map((n) => Number(n) || 0), pre };
31
+ };
32
+ const x = parse(a);
33
+ const y = parse(b);
34
+ for (let i = 0; i < 3; i++) {
35
+ const d = (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
36
+ if (d)
37
+ return Math.sign(d);
38
+ }
39
+ if (x.pre && !y.pre)
40
+ return -1;
41
+ if (!x.pre && y.pre)
42
+ return 1;
43
+ return 0;
44
+ }
45
+ async function fetchLatest(timeout) {
46
+ try {
47
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal: AbortSignal.timeout(timeout) });
48
+ if (!res.ok)
49
+ return null;
50
+ return (await res.json()).version ?? null;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /**
57
+ * The newer version available on npm, or null. Asks npm at most once a day (cached in ~/.nou/update.json)
58
+ * and never waits more than `timeout` ms, so startup stays fast offline.
59
+ */
60
+ export async function checkForUpdate({ force = false, timeout = 1500 } = {}) {
61
+ if (!force && (process.env.NOU_NO_UPDATE_CHECK || isDevCheckout()))
62
+ return null;
63
+ let latest = null;
64
+ try {
65
+ const cached = JSON.parse(readFileSync(CACHE, "utf-8"));
66
+ if (!force && Date.now() - cached.checkedAt < DAY)
67
+ latest = cached.latest;
68
+ }
69
+ catch {
70
+ // no cache yet
71
+ }
72
+ if (!latest) {
73
+ latest = await fetchLatest(timeout);
74
+ if (latest) {
75
+ try {
76
+ mkdirSync(NOU_HOME, { recursive: true });
77
+ writeFileSync(CACHE, JSON.stringify({ latest, checkedAt: Date.now() }));
78
+ }
79
+ catch {
80
+ // caching is optional
81
+ }
82
+ }
83
+ }
84
+ return latest && compareVersions(latest, currentVersion()) > 0 ? latest : null;
85
+ }
86
+ /** Runs `npm install -g nou-cli@<version>`, showing npm's output. Resolves to success. */
87
+ export function installUpdate(version = "latest") {
88
+ return new Promise((resolve) => {
89
+ // npm is npm.cmd on Windows, which needs a shell; the arguments are fixed, not user input.
90
+ const child = spawn("npm", ["install", "-g", `${PACKAGE_NAME}@${version}`], {
91
+ stdio: "inherit",
92
+ shell: process.platform === "win32",
93
+ });
94
+ child.on("error", () => resolve(false));
95
+ child.on("close", (code) => resolve(code === 0));
96
+ });
97
+ }
98
+ /** Starts the freshly installed Nou in place of this process. */
99
+ export function relaunch() {
100
+ const child = spawn(process.execPath, process.argv.slice(1).filter((a) => a !== "update"), { stdio: "inherit" });
101
+ child.on("close", (code) => process.exit(code ?? 0));
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nou-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Nou: an IDE-style AI coding assistant for your terminal. Local (Ollama, LM Studio) and cloud (Claude, OpenAI, Gemini...) models, live diffs, git-aware, built-in editor.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,15 +12,17 @@
12
12
  "README.md"
13
13
  ],
14
14
  "engines": {
15
- "node": ">=20.12"
15
+ "node": ">=22.12"
16
16
  },
17
17
  "scripts": {
18
18
  "dev": "npx tsx index.ts",
19
- "build": "tsc -p tsconfig.build.json",
19
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-grammars.mjs",
20
20
  "start": "node dist/index.js",
21
21
  "typecheck": "tsc --noEmit -p .",
22
22
  "lint": "eslint index.ts src",
23
- "prepublishOnly": "npm run build"
23
+ "prepublishOnly": "npm run build",
24
+ "screenshot": "node -e \"process.env.FORCE_COLOR='3';require('child_process').execSync('npx tsx scripts/screenshot.tsx',{stdio:'inherit'})\"",
25
+ "test": "tsx tests/unit.ts && tsx tests/e2e.tsx"
24
26
  },
25
27
  "repository": {
26
28
  "type": "git",
@@ -50,15 +52,18 @@
50
52
  "ollama": "^0.6.3",
51
53
  "openai": "^7.32.0",
52
54
  "react": "^19.3.0",
55
+ "web-tree-sitter": "0.25.10",
53
56
  "zod": "^4.3.6"
54
57
  },
55
58
  "devDependencies": {
56
59
  "@eslint/js": "^10.0.1",
57
- "@types/node": "^25.5.0",
60
+ "@types/node": "^22.20.5",
58
61
  "@types/react": "^19.3.0",
59
62
  "eslint": "^10.2.0",
60
63
  "globals": "^17.4.0",
64
+ "ink-testing-library": "^4.0.0",
61
65
  "jiti": "^2.6.1",
66
+ "tree-sitter-wasms": "0.1.13",
62
67
  "tsx": "^4.23.15",
63
68
  "typescript": "^6.0.2",
64
69
  "typescript-eslint": "^8.58.1"