@jellyos/agent 0.1.6 → 0.1.9
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.
- package/dist/cli.js +11 -1
- package/dist/loader.js +21 -1
- package/dist/tui/App.js +53 -2
- package/dist/tui/REPL.js +1 -1
- package/dist/tui/StatusBar.js +17 -1
- package/dist/util/safeLog.d.ts +3 -0
- package/dist/util/safeLog.js +21 -0
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Registry } from "./api/Registry.js";
|
|
|
12
12
|
import { loadExtension } from "./loader.js";
|
|
13
13
|
import { App } from "./tui/App.js";
|
|
14
14
|
import { T } from "./tui/theme.js";
|
|
15
|
+
import { wireNotify, safeLog } from "./util/safeLog.js";
|
|
15
16
|
import { modelRegistry } from "./models/ModelRegistry.js";
|
|
16
17
|
import { CostTracker } from "./models/CostTracker.js";
|
|
17
18
|
import { AgentRunner } from "./runner/AgentRunner.js";
|
|
@@ -212,10 +213,19 @@ else {
|
|
|
212
213
|
chain,
|
|
213
214
|
modelReg: modelRegistry,
|
|
214
215
|
costTracker,
|
|
215
|
-
onNotifyReady: (fn) => { _notifyFn = fn; },
|
|
216
|
+
onNotifyReady: (fn) => { _notifyFn = fn; wireNotify(fn); },
|
|
216
217
|
onStatusReady: (fn) => { _setStatusFn = fn; },
|
|
217
218
|
onModelSelectorReady: (fn) => { _showModelSelectorFn = fn; },
|
|
218
219
|
}), { exitOnCtrlC: false });
|
|
220
|
+
// Ink owns the terminal from this point on. Any console.log/error/warn
|
|
221
|
+
// writes raw bytes to stdout, bypassing Ink's rendering buffer. Ink's
|
|
222
|
+
// cursor-up calculation becomes wrong → stacked border lines.
|
|
223
|
+
// NOTE: process.stdout.write is intentionally NOT patched — Ink uses it
|
|
224
|
+
// for every render frame; intercepting it globally would break Ink output.
|
|
225
|
+
process.prependListener("SIGWINCH", () => { process.stdout.write("\x1B[2J\x1B[H"); });
|
|
226
|
+
console.log = safeLog;
|
|
227
|
+
console.error = safeLog;
|
|
228
|
+
console.warn = safeLog;
|
|
219
229
|
})();
|
|
220
230
|
} // end headless else
|
|
221
231
|
//# sourceMappingURL=cli.js.map
|
package/dist/loader.js
CHANGED
|
@@ -80,6 +80,26 @@ export async function loadExtension(extensionPath, registry, opts = {}) {
|
|
|
80
80
|
if (typeof fn !== "function") {
|
|
81
81
|
throw new Error(`Extension must export a default function. Got: ${typeof fn} from ${abs}`);
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
// Intercept console.log/error/warn during extension load so that any
|
|
84
|
+
// stray prints in the extension (e.g. loadSkills logging) are routed
|
|
85
|
+
// through ui.notify instead of raw stdout writes that corrupt the TUI.
|
|
86
|
+
const _origLog = console.log;
|
|
87
|
+
const _origError = console.error;
|
|
88
|
+
const _origWarn = console.warn;
|
|
89
|
+
const _extLog = (...args) => {
|
|
90
|
+
const msg = args.map(a => (typeof a === "string" ? a : String(a))).join(" ");
|
|
91
|
+
ui.notify(msg);
|
|
92
|
+
};
|
|
93
|
+
console.log = _extLog;
|
|
94
|
+
console.error = _extLog;
|
|
95
|
+
console.warn = _extLog;
|
|
96
|
+
try {
|
|
97
|
+
await fn(api);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
console.log = _origLog;
|
|
101
|
+
console.error = _origError;
|
|
102
|
+
console.warn = _origWarn;
|
|
103
|
+
}
|
|
84
104
|
}
|
|
85
105
|
//# sourceMappingURL=loader.js.map
|
package/dist/tui/App.js
CHANGED
|
@@ -96,6 +96,10 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
|
|
|
96
96
|
const [ticker, setTicker] = useState("");
|
|
97
97
|
const [costBadge, setCostBadge] = useState("");
|
|
98
98
|
const [newsBadge, setNewsBadge] = useState("");
|
|
99
|
+
// ── Sidebar live data state ─────────────────────────────────────────────
|
|
100
|
+
const [sidebarTickers, setSidebarTickers] = useState([]);
|
|
101
|
+
const [sidebarNews, setSidebarNews] = useState([]);
|
|
102
|
+
const [rotationSlots, setRotationSlots] = useState([null, null, null, null, null]);
|
|
99
103
|
// ── Model selector overlay state ──────────────────────────────────────
|
|
100
104
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
|
101
105
|
const [modelSelectorInitialQuery, setModelSelectorInitialQuery] = useState("");
|
|
@@ -210,7 +214,28 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
|
|
|
210
214
|
if (costTracker)
|
|
211
215
|
setCostBadge(costTracker.statusLine());
|
|
212
216
|
setNewsBadge(newsFeed.statusBadge());
|
|
213
|
-
|
|
217
|
+
// Update sidebar live data
|
|
218
|
+
const allTicks = priceFeed.getAll();
|
|
219
|
+
setSidebarTickers(allTicks.slice(0, 8).map(t => ({ symbol: t.symbol, price: t.price, changePct: t.change24h })));
|
|
220
|
+
const latestNews = newsFeed.getLatest();
|
|
221
|
+
if (latestNews?.items) {
|
|
222
|
+
setSidebarNews(latestNews.items.slice(0, 6).map(item => ({ title: item.title ?? item.source ?? "News", sentiment: item.sentiment ?? 0, source: item.source ?? "" })));
|
|
223
|
+
}
|
|
224
|
+
// Read rotation slots from context.json
|
|
225
|
+
try {
|
|
226
|
+
const { existsSync: exists, readFileSync: read } = require("node:fs");
|
|
227
|
+
const { join } = require("node:path");
|
|
228
|
+
const { homedir } = require("node:os");
|
|
229
|
+
const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
|
|
230
|
+
const ctxPath = join(JELLY_HOME, "context.json");
|
|
231
|
+
if (exists(ctxPath)) {
|
|
232
|
+
const store = JSON.parse(read(ctxPath, "utf-8"));
|
|
233
|
+
if (store.rotationSlots)
|
|
234
|
+
setRotationSlots(store.rotationSlots);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch { /* non-fatal */ }
|
|
238
|
+
}, 3_000);
|
|
214
239
|
const saveInterval = setInterval(() => { costTracker?.saveLifetime(); }, 60_000);
|
|
215
240
|
const session = new SessionManager();
|
|
216
241
|
session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt || "You are JellyOS, an autonomous AI trading agent.");
|
|
@@ -284,6 +309,13 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
|
|
|
284
309
|
if (costTracker)
|
|
285
310
|
setCostBadge(costTracker.statusLine());
|
|
286
311
|
setNewsBadge(newsFeed.statusBadge());
|
|
312
|
+
// Initial sidebar data fetch (before interval fires)
|
|
313
|
+
const initialTicks = priceFeed.getAll();
|
|
314
|
+
setSidebarTickers(initialTicks.slice(0, 8).map(t => ({ symbol: t.symbol, price: t.price, changePct: t.change24h })));
|
|
315
|
+
const initialNews = newsFeed.getLatest();
|
|
316
|
+
if (initialNews?.items) {
|
|
317
|
+
setSidebarNews(initialNews.items.slice(0, 6).map(item => ({ title: item.title ?? item.source ?? "News", sentiment: item.sentiment ?? 0, source: item.source ?? "" })));
|
|
318
|
+
}
|
|
287
319
|
return () => {
|
|
288
320
|
if (sessionCtxRef.current)
|
|
289
321
|
registry.fireHook("session_end", sessionCtxRef.current).catch(() => { });
|
|
@@ -492,6 +524,11 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
|
|
|
492
524
|
const ctxPath = join(JELLY_HOME, "context.json");
|
|
493
525
|
const store = existsSync(ctxPath) ? JSON.parse(readFileSync(ctxPath, "utf-8")) : {};
|
|
494
526
|
store.model = modelId;
|
|
527
|
+
// Also save as rotation slot 1 (primary)
|
|
528
|
+
const tier = modelReg?.getTier?.(modelId) ?? "worker";
|
|
529
|
+
const slots = store.rotationSlots ?? [null, null, null, null, null];
|
|
530
|
+
slots[0] = { id: modelId, tier };
|
|
531
|
+
store.rotationSlots = slots;
|
|
495
532
|
writeFileSync(ctxPath, JSON.stringify(store, null, 2), "utf-8");
|
|
496
533
|
}
|
|
497
534
|
catch { /* non-fatal */ }
|
|
@@ -517,12 +554,26 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
|
|
|
517
554
|
}, [modelReg]);
|
|
518
555
|
const ctx = getContextBar(sessionRef.current);
|
|
519
556
|
const statusLine = [ticker, costBadge, newsBadge, ...Object.values(statusBadges)].filter(Boolean).join(" ") || null;
|
|
557
|
+
// ── Sidebar helper functions ───────────────────────────────────────────────
|
|
558
|
+
const changeColor = (pct) => pct > 0 ? JELLY_COLORS.success : pct < 0 ? JELLY_COLORS.error : JELLY_COLORS.muted;
|
|
559
|
+
const changeArrow = (pct) => pct > 0 ? "▲" : pct < 0 ? "▼" : "─";
|
|
560
|
+
const SIDEBAR_W = 42;
|
|
520
561
|
// ── Overlay: model selector ────────────────────────────────────────────
|
|
521
562
|
if (showModelSelector) {
|
|
522
563
|
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(StatusBar, { model: modelName, chain: chain, vaultLocked: vaultLocked, effectLevel: effectLevel, toolRunning: toolRunning, connected: true, statusLine: statusLine }), _jsx(ModelSelector, { models: modelList, currentModelId: process.env.DEFAULT_MODEL ?? "", onSelect: handleModelSelect, onCancel: handleModelCancel, initialQuery: modelSelectorInitialQuery })] }));
|
|
523
564
|
}
|
|
524
565
|
// ── Multi-pane layout ────────────────────────────────────────────────────
|
|
525
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(StatusBar, { model: modelName, chain: chain, vaultLocked: vaultLocked, effectLevel: effectLevel, toolRunning: toolRunning, connected: true, statusLine: statusLine }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width:
|
|
566
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(StatusBar, { model: modelName, chain: chain, vaultLocked: vaultLocked, effectLevel: effectLevel, toolRunning: toolRunning, connected: true, statusLine: statusLine }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: SIDEBAR_W, flexShrink: 0, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["\uD83D\uDCE1 Ticker", rotationSlots.filter(s => s && s.id).length > 0 && (_jsxs(Text, { color: "#6b7280", children: [" \u21BB", rotationSlots.filter(s => s && s.id).length] }))] }), sidebarTickers.length > 0 ? sidebarTickers.map((t, i) => {
|
|
567
|
+
const arrow = changeArrow(t.changePct);
|
|
568
|
+
const col = changeColor(t.changePct);
|
|
569
|
+
const pctStr = (t.changePct > 0 ? "+" : "") + t.changePct.toFixed(1) + "%";
|
|
570
|
+
return _jsxs(Text, { color: col, children: [t.symbol.padEnd(8), "$", String(t.price).padStart(9), " ", arrow, pctStr] }, i);
|
|
571
|
+
}) : _jsx(Text, { color: JELLY_COLORS.muted, children: "Loading prices\u2026" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Context" }), _jsxs(Text, { color: ctx.color, children: [ctx.bar, " ", ctx.pct, "%"] }), ctx.turboReady ? null : _jsx(Text, { color: JELLY_COLORS.warn, children: "\u26A0 no turbo" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Effect" }), _jsx(Text, { color: effectLevel === "eco" ? "#22c55e" : effectLevel === "turbo" ? "#f59e0b" : effectLevel === "max" ? "#ef4444" : JELLY_COLORS.accent, children: effectLevel.toUpperCase() })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "News" }), sidebarNews.length > 0 ? sidebarNews.map((n, i) => {
|
|
572
|
+
const sentColor = n.sentiment > 0.3 ? JELLY_COLORS.success : n.sentiment < -0.3 ? JELLY_COLORS.error : JELLY_COLORS.muted;
|
|
573
|
+
const sentPct = Math.round((n.sentiment + 1) * 50);
|
|
574
|
+
const title = n.title.length > 28 ? n.title.slice(0, 26) + "…" : n.title;
|
|
575
|
+
return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: JELLY_COLORS.muted, children: title }), _jsxs(Text, { color: sentColor, children: [sentPct, "%"] })] }, i));
|
|
576
|
+
}) : _jsx(Text, { color: JELLY_COLORS.muted, children: "Loading news\u2026" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Feeds" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[coingecko] SOLANA\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[coingecko] ETHEREUM\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[etherscan] BTC Price\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[etherscan] ETF Gas\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[binance] XRPUSDT 24h\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[binance] AVAXUSDT 24h\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[binance] SOLUSDT 24h\u2026" })] })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(REPL, { messages: messages, streamingText: streaming, toolRunning: toolRunning, onSubmit: handleSubmit, disabled: disabled, onAbort: () => {
|
|
526
577
|
runnerRef.current?.abort();
|
|
527
578
|
setDisabled(false);
|
|
528
579
|
setToolRunning(null);
|
package/dist/tui/REPL.js
CHANGED
|
@@ -46,6 +46,6 @@ export function REPL({ messages, streamingText, toolRunning, onSubmit, onAbort,
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
const visible = messages.slice(-MAX_VISIBLE);
|
|
49
|
-
return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [visible.map(m => _jsx(MessageLine, { msg: m }, m.id)), streamingText && (_jsxs(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: " " }), _jsx(Text, { color: JELLY_COLORS.header, bold: true, children: "\uD83E\uDEBC " }), _jsx(Text, { wrap: "wrap", children: streamingText })] })), toolRunning && (_jsx(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: _jsxs(Text, { color: JELLY_COLORS.warn, children: ["\u2699 running ", toolRunning, "\u2026"] }) }))] }), _jsxs(Box, { borderStyle: "round", borderColor: disabled ? JELLY_COLORS.dim : JELLY_COLORS.accent, paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.accent, children: "\u203A " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: disabled ? "thinking…" : "message or /command" })] })
|
|
49
|
+
return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [visible.map(m => _jsx(MessageLine, { msg: m }, m.id)), streamingText && (_jsxs(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: " " }), _jsx(Text, { color: JELLY_COLORS.header, bold: true, children: "\uD83E\uDEBC " }), _jsx(Text, { wrap: "wrap", children: streamingText })] })), toolRunning && (_jsx(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: _jsxs(Text, { color: JELLY_COLORS.warn, children: ["\u2699 running ", toolRunning, "\u2026"] }) }))] }), _jsxs(Box, { borderStyle: "round", borderColor: disabled ? JELLY_COLORS.dim : JELLY_COLORS.accent, paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.accent, children: "\u203A " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: disabled ? "thinking…" : "message or /command" })] })] }));
|
|
50
50
|
}
|
|
51
51
|
//# sourceMappingURL=REPL.js.map
|
package/dist/tui/StatusBar.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { JELLY_COLORS } from "./theme.js";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
4
7
|
export function StatusBar({ model, chain, vaultLocked, effectLevel, toolRunning, connected, statusLine, }) {
|
|
5
8
|
const vaultIcon = vaultLocked ? "🔒" : "🔓";
|
|
6
9
|
const chainShort = chain.slice(0, 8);
|
|
7
10
|
const modelShort = model.split("/").pop()?.slice(0, 18) ?? model.slice(0, 18);
|
|
8
11
|
const effectIcon = { eco: "🌿", normal: "⚡", turbo: "🚀", max: "🌊" }[effectLevel] ?? "⚡";
|
|
9
|
-
|
|
12
|
+
// Read rotation slot count for display
|
|
13
|
+
let rotationCount = 0;
|
|
14
|
+
try {
|
|
15
|
+
const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
|
|
16
|
+
const ctxPath = join(JELLY_HOME, "context.json");
|
|
17
|
+
if (existsSync(ctxPath)) {
|
|
18
|
+
const store = JSON.parse(readFileSync(ctxPath, "utf-8"));
|
|
19
|
+
const slots = store.rotationSlots ?? [];
|
|
20
|
+
rotationCount = slots.filter((s) => s && s.id).length;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch { /* non-fatal */ }
|
|
24
|
+
const rotationBadge = rotationCount > 0 ? `↻${rotationCount}` : null;
|
|
25
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: JELLY_COLORS.dim, paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: JELLY_COLORS.accent, bold: true, children: "\uD83E\uDEBC JellyOS" }), _jsx(Text, { color: JELLY_COLORS.muted, children: modelShort }), rotationBadge ? _jsx(Text, { color: "#6b7280", children: rotationBadge }) : null] }), _jsx(Box, { children: toolRunning
|
|
10
26
|
? _jsxs(Text, { color: JELLY_COLORS.warn, children: ["\u2699 ", toolRunning] })
|
|
11
27
|
: statusLine
|
|
12
28
|
? _jsx(Text, { color: JELLY_COLORS.muted, children: statusLine.slice(0, 80) })
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
|
|
5
|
+
const DEBUG_LOG = join(JELLY_HOME, "debug.log");
|
|
6
|
+
let _notifyFn = null;
|
|
7
|
+
export function wireNotify(fn) { _notifyFn = fn; }
|
|
8
|
+
export function safeLog(...args) {
|
|
9
|
+
const msg = args.map(a => (typeof a === "string" ? a : String(a))).join(" ");
|
|
10
|
+
if (_notifyFn) {
|
|
11
|
+
_notifyFn(msg);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
try {
|
|
15
|
+
mkdirSync(JELLY_HOME, { recursive: true });
|
|
16
|
+
appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] ${msg}\n`);
|
|
17
|
+
}
|
|
18
|
+
catch { /* non-fatal */ }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=safeLog.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jellyos/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "JellyOS — standalone AI trading agent. Runs locally, no server required.",
|
|
5
5
|
"author": "JellyChain",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"jellyagent": "./bin/jellyagent",
|
|
19
19
|
"jellyos-mcp": "./bin/jellyos-mcp"
|
|
20
20
|
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
21
24
|
"scripts": {
|
|
22
25
|
"build": "npx tsc",
|
|
23
26
|
"build:watch": "tsc --watch",
|