swagmanager-mcp 1.2.0 → 3.0.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.
- package/bin/swagmanager-mcp.js +107 -13
- package/dist/cli/app.d.ts +11 -0
- package/dist/cli/app.js +39 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +7 -0
- package/dist/cli/chat/ChatApp.js +288 -0
- package/dist/cli/chat/ChatInput.d.ts +19 -0
- package/dist/cli/chat/ChatInput.js +71 -0
- package/dist/cli/chat/MarkdownText.d.ts +9 -0
- package/dist/cli/chat/MarkdownText.js +32 -0
- package/dist/cli/chat/MessageList.d.ts +32 -0
- package/dist/cli/chat/MessageList.js +43 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +24 -0
- package/dist/cli/chat/ToolIndicator.d.ts +18 -0
- package/dist/cli/chat/ToolIndicator.js +200 -0
- package/dist/cli/login/LoginApp.d.ts +4 -0
- package/dist/cli/login/LoginApp.js +103 -0
- package/dist/cli/services/agent-loop.d.ts +34 -0
- package/dist/cli/services/agent-loop.js +360 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +159 -0
- package/dist/cli/services/config-store.d.ts +36 -0
- package/dist/cli/services/config-store.js +65 -0
- package/dist/cli/services/local-tools.d.ts +23 -0
- package/dist/cli/services/local-tools.js +244 -0
- package/dist/cli/services/server-tools.d.ts +29 -0
- package/dist/cli/services/server-tools.js +394 -0
- package/dist/cli/services/telemetry.d.ts +22 -0
- package/dist/cli/services/telemetry.js +129 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +65 -0
- package/dist/cli/shared/Theme.js +94 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +11 -0
- package/dist/cli/shared/markdown.js +271 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +108 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/tools/executor.d.ts +2 -1
- package/dist/tools/executor.js +131 -18
- package/package.json +17 -4
package/bin/swagmanager-mcp.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* whale code — local-first AI agent CLI
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* STORE_ID - Default store ID (optional)
|
|
7
|
+
* whale — Start chat (default, interactive terminal)
|
|
8
|
+
* whale login — Log in with SwagManager credentials
|
|
9
|
+
* whale logout — Clear saved session
|
|
10
|
+
* whale status — Show connection status
|
|
11
|
+
* whale help — Show this help
|
|
12
|
+
* (non-TTY stdin) — MCP stdio server for Claude Code / Cursor
|
|
14
13
|
*/
|
|
15
14
|
|
|
16
15
|
import { fileURLToPath } from "url";
|
|
@@ -21,9 +20,104 @@ const __dirname = dirname(__filename);
|
|
|
21
20
|
|
|
22
21
|
const command = process.argv[2];
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// ── Help ──
|
|
24
|
+
function showHelp() {
|
|
25
|
+
const d = "\x1b[2m";
|
|
26
|
+
const B = "\x1b[1m";
|
|
27
|
+
const r = "\x1b[0m";
|
|
28
|
+
const c = "\x1b[38;2;99;102;241m";
|
|
29
|
+
const g = "\x1b[38;2;100;116;139m";
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(` ${g}╭──────────────────────────────────────────╮${r}`);
|
|
33
|
+
console.log(` ${g}│${r} ${g}│${r}`);
|
|
34
|
+
console.log(` ${g}│${r} ${c}${B}◆ whale code${r} ${d}v3.0.0${r} ${g}│${r}`);
|
|
35
|
+
console.log(` ${g}│${r} ${d}local-first AI agent CLI${r} ${g}│${r}`);
|
|
36
|
+
console.log(` ${g}│${r} ${g}│${r}`);
|
|
37
|
+
console.log(` ${g}╰──────────────────────────────────────────╯${r}`);
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(` ${B}Commands:${r}`);
|
|
40
|
+
console.log(` whale${d} Start chatting (default)${r}`);
|
|
41
|
+
console.log(` whale login${d} Log in to SwagManager${r}`);
|
|
42
|
+
console.log(` whale logout${d} Clear session${r}`);
|
|
43
|
+
console.log(` whale stores${d} Switch active store${r}`);
|
|
44
|
+
console.log(` whale status${d} Connection & tools${r}`);
|
|
45
|
+
console.log(` whale setup${d} Install MCP to IDEs${r}`);
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(` ${B}In chat:${r}`);
|
|
48
|
+
console.log(` ${d}Type ${r}/${d} to open command menu${r}`);
|
|
49
|
+
console.log(` ${d}^C to exit, esc to cancel${r}`);
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Route ──
|
|
54
|
+
switch (command) {
|
|
55
|
+
case "help":
|
|
56
|
+
case "--help":
|
|
57
|
+
case "-h":
|
|
58
|
+
showHelp();
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case "login": {
|
|
62
|
+
if (!process.stdin.isTTY) {
|
|
63
|
+
console.error("Error: whale login requires an interactive terminal.");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const { renderLogin } = await import(join(__dirname, "..", "dist", "cli", "app.js"));
|
|
67
|
+
await renderLogin();
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "logout": {
|
|
72
|
+
const { renderLogout } = await import(join(__dirname, "..", "dist", "cli", "app.js"));
|
|
73
|
+
await renderLogout();
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "chat":
|
|
78
|
+
case undefined: {
|
|
79
|
+
if (process.stdin.isTTY) {
|
|
80
|
+
const { renderChat } = await import(join(__dirname, "..", "dist", "cli", "app.js"));
|
|
81
|
+
await renderChat();
|
|
82
|
+
} else if (command === "chat") {
|
|
83
|
+
console.error("Error: whale chat requires an interactive terminal.");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
} else {
|
|
86
|
+
// Non-TTY, no command → MCP stdio server
|
|
87
|
+
await import(join(__dirname, "..", "dist", "index.js"));
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case "status": {
|
|
93
|
+
const { renderStatus } = await import(join(__dirname, "..", "dist", "cli", "app.js"));
|
|
94
|
+
await renderStatus();
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "setup": {
|
|
99
|
+
if (!process.stdin.isTTY) {
|
|
100
|
+
console.error("Error: whale setup requires an interactive terminal.");
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const { renderSetup } = await import(join(__dirname, "..", "dist", "cli", "app.js"));
|
|
104
|
+
await renderSetup();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case "stores":
|
|
109
|
+
case "store": {
|
|
110
|
+
if (!process.stdin.isTTY) {
|
|
111
|
+
console.error("Error: whale stores requires an interactive terminal.");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const { renderStores } = await import(join(__dirname, "..", "dist", "cli", "app.js"));
|
|
115
|
+
await renderStores();
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
console.error(`Unknown command: ${command}`);
|
|
121
|
+
console.error(`Run 'whale help' for usage.`);
|
|
122
|
+
process.exit(1);
|
|
29
123
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI App Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Dynamic imports for each mode — keeps MCP server path clean.
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderLogin(): Promise<void>;
|
|
7
|
+
export declare function renderLogout(): Promise<void>;
|
|
8
|
+
export declare function renderChat(): Promise<void>;
|
|
9
|
+
export declare function renderSetup(): Promise<void>;
|
|
10
|
+
export declare function renderStatus(): Promise<void>;
|
|
11
|
+
export declare function renderStores(): Promise<void>;
|
package/dist/cli/app.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
export async function renderLogin() {
|
|
4
|
+
const { LoginApp } = await import("./login/LoginApp.js");
|
|
5
|
+
const { waitUntilExit } = render(_jsx(LoginApp, {}));
|
|
6
|
+
await waitUntilExit();
|
|
7
|
+
}
|
|
8
|
+
export async function renderLogout() {
|
|
9
|
+
const { signOut, isLoggedIn } = await import("./services/auth-service.js");
|
|
10
|
+
if (!isLoggedIn()) {
|
|
11
|
+
console.log("Not logged in.");
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
signOut();
|
|
15
|
+
console.log("Logged out. Tokens cleared.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function renderChat() {
|
|
19
|
+
const { matrixIntro } = await import("./shared/MatrixIntro.js");
|
|
20
|
+
await matrixIntro();
|
|
21
|
+
const { ChatApp } = await import("./chat/ChatApp.js");
|
|
22
|
+
const { waitUntilExit } = render(_jsx(ChatApp, {}));
|
|
23
|
+
await waitUntilExit();
|
|
24
|
+
}
|
|
25
|
+
export async function renderSetup() {
|
|
26
|
+
const { SetupApp } = await import("./setup/SetupApp.js");
|
|
27
|
+
const { waitUntilExit } = render(_jsx(SetupApp, {}));
|
|
28
|
+
await waitUntilExit();
|
|
29
|
+
}
|
|
30
|
+
export async function renderStatus() {
|
|
31
|
+
const { StatusApp } = await import("./status/StatusApp.js");
|
|
32
|
+
const { waitUntilExit } = render(_jsx(StatusApp, {}));
|
|
33
|
+
await waitUntilExit();
|
|
34
|
+
}
|
|
35
|
+
export async function renderStores() {
|
|
36
|
+
const { StoreApp } = await import("./stores/StoreApp.js");
|
|
37
|
+
const { waitUntilExit } = render(_jsx(StoreApp, {}));
|
|
38
|
+
await waitUntilExit();
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentSelector — polished agent picker with descriptions
|
|
3
|
+
*/
|
|
4
|
+
interface Agent {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
model: string;
|
|
8
|
+
}
|
|
9
|
+
interface AgentSelectorProps {
|
|
10
|
+
agents: Agent[];
|
|
11
|
+
onSelect: (agentId: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function AgentSelector({ agents, onSelect }: AgentSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import SelectInput from "ink-select-input";
|
|
4
|
+
import { colors, symbols } from "../shared/Theme.js";
|
|
5
|
+
export function AgentSelector({ agents, onSelect }) {
|
|
6
|
+
const items = agents.map((a) => ({
|
|
7
|
+
label: `${a.name}`,
|
|
8
|
+
value: a.id,
|
|
9
|
+
}));
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.muted, children: "Select an agent:" }), _jsx(Box, { height: 1 }), _jsx(SelectInput, { items: items, onSelect: (item) => onSelect(item.value), indicatorComponent: ({ isSelected }) => (_jsxs(Text, { color: isSelected ? colors.brand : colors.dim, children: [isSelected ? symbols.arrowRight : " ", " "] })), itemComponent: ({ isSelected, label }) => {
|
|
11
|
+
const agent = agents.find((a) => a.name === label);
|
|
12
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? colors.brand : colors.text, bold: isSelected, children: label }), agent && (_jsxs(Text, { color: colors.dim, children: [" (", agent.model, ")"] }))] }));
|
|
13
|
+
} })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ChatApp — whale code CLI
|
|
4
|
+
*
|
|
5
|
+
* Clean, Apple-polished chat interface.
|
|
6
|
+
* Minimal header, generous spacing, subtle status.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
9
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import { runAgentLoop, canUseAgent, getServerToolCount, getServerStatus } from "../services/agent-loop.js";
|
|
12
|
+
import { getAllServerToolDefinitions, resetServerToolClient } from "../services/server-tools.js";
|
|
13
|
+
import { LOCAL_TOOL_DEFINITIONS } from "../services/local-tools.js";
|
|
14
|
+
import { MessageList } from "./MessageList.js";
|
|
15
|
+
import { ChatInput, SLASH_COMMANDS } from "./ChatInput.js";
|
|
16
|
+
import { StoreSelector } from "./StoreSelector.js";
|
|
17
|
+
import { colors, symbols, boxLine } from "../shared/Theme.js";
|
|
18
|
+
import { loadConfig } from "../services/config-store.js";
|
|
19
|
+
import { getStoresForUser, getValidToken, selectStore } from "../services/auth-service.js";
|
|
20
|
+
import { createRequire } from "module";
|
|
21
|
+
const PKG_NAME = "swagmanager-mcp";
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const PKG_VERSION = require("../../package.json").version;
|
|
24
|
+
export function ChatApp() {
|
|
25
|
+
const { exit } = useApp();
|
|
26
|
+
const [ready, setReady] = useState(false);
|
|
27
|
+
const [error, setError] = useState("");
|
|
28
|
+
const [messages, setMessages] = useState([]);
|
|
29
|
+
const [streamingText, setStreamingText] = useState("");
|
|
30
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
31
|
+
const [activeTools, setActiveTools] = useState([]);
|
|
32
|
+
const [userLabel, setUserLabel] = useState("");
|
|
33
|
+
const [toolsExpanded, setToolsExpanded] = useState(false);
|
|
34
|
+
const [serverToolsAvailable, setServerToolsAvailable] = useState(0);
|
|
35
|
+
const [storeSelectMode, setStoreSelectMode] = useState(false);
|
|
36
|
+
const [storeList, setStoreList] = useState([]);
|
|
37
|
+
const conversationRef = useRef([]);
|
|
38
|
+
const abortRef = useRef(null);
|
|
39
|
+
// ── Init ──
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const check = canUseAgent();
|
|
42
|
+
if (!check.ready) {
|
|
43
|
+
setError(check.reason || "Run 'whale login' to authenticate.");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
if (config.email)
|
|
48
|
+
setUserLabel(config.store_name || config.email);
|
|
49
|
+
setReady(true);
|
|
50
|
+
getServerToolCount().then((count) => setServerToolsAvailable(count));
|
|
51
|
+
}
|
|
52
|
+
}, []);
|
|
53
|
+
// ── Keys ──
|
|
54
|
+
useInput((input, key) => {
|
|
55
|
+
if (input === "c" && key.ctrl) {
|
|
56
|
+
if (abortRef.current)
|
|
57
|
+
abortRef.current.abort();
|
|
58
|
+
exit();
|
|
59
|
+
}
|
|
60
|
+
if (key.escape && isStreaming && abortRef.current) {
|
|
61
|
+
abortRef.current.abort();
|
|
62
|
+
}
|
|
63
|
+
if (input === "e" && key.ctrl) {
|
|
64
|
+
setToolsExpanded((prev) => !prev);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// ── Commands ──
|
|
68
|
+
const handleCommand = useCallback((command) => {
|
|
69
|
+
switch (command) {
|
|
70
|
+
case "/help":
|
|
71
|
+
setMessages((prev) => [...prev, {
|
|
72
|
+
role: "assistant",
|
|
73
|
+
text: SLASH_COMMANDS
|
|
74
|
+
.map((c) => ` ${c.name.padEnd(12)} ${c.description}`)
|
|
75
|
+
.join("\n") + "\n\n ^C exit esc cancel ^E expand tools",
|
|
76
|
+
}]);
|
|
77
|
+
break;
|
|
78
|
+
case "/clear":
|
|
79
|
+
setMessages([]);
|
|
80
|
+
setStreamingText("");
|
|
81
|
+
setActiveTools([]);
|
|
82
|
+
conversationRef.current = [];
|
|
83
|
+
break;
|
|
84
|
+
case "/exit":
|
|
85
|
+
if (abortRef.current)
|
|
86
|
+
abortRef.current.abort();
|
|
87
|
+
exit();
|
|
88
|
+
break;
|
|
89
|
+
case "/status": {
|
|
90
|
+
const config = loadConfig();
|
|
91
|
+
const toolsLine = serverToolsAvailable > 0
|
|
92
|
+
? ` tools 7 local + ${serverToolsAvailable} server`
|
|
93
|
+
: ` tools 7 local`;
|
|
94
|
+
setMessages((prev) => [...prev, {
|
|
95
|
+
role: "assistant",
|
|
96
|
+
text: [
|
|
97
|
+
` version v${PKG_VERSION}`,
|
|
98
|
+
` user ${config.email || "—"}`,
|
|
99
|
+
` store ${config.store_name || "—"}`,
|
|
100
|
+
` model claude-sonnet-4`,
|
|
101
|
+
toolsLine,
|
|
102
|
+
` expand ${toolsExpanded ? "on" : "off"} (^E)`,
|
|
103
|
+
].join("\n"),
|
|
104
|
+
}]);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "/mcp": {
|
|
108
|
+
getServerStatus().then((status) => {
|
|
109
|
+
const lines = [];
|
|
110
|
+
if (status.connected) {
|
|
111
|
+
lines.push(` ● Connected`);
|
|
112
|
+
lines.push(` auth ${status.authMethod === "service_role" ? "service role" : "user JWT"}`);
|
|
113
|
+
lines.push(` store ${status.storeName || status.storeId || "—"}`);
|
|
114
|
+
lines.push(` tools ${status.toolCount} active`);
|
|
115
|
+
lines.push("");
|
|
116
|
+
const serverDefs = getAllServerToolDefinitions();
|
|
117
|
+
for (const t of serverDefs) {
|
|
118
|
+
lines.push(` ${t.name.padEnd(20)} ${(t.description || "").slice(0, 45)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
lines.push(` ○ Disconnected`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push(" Run: whale login");
|
|
125
|
+
}
|
|
126
|
+
setMessages((prev) => [...prev, { role: "assistant", text: lines.join("\n") }]);
|
|
127
|
+
});
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case "/store": {
|
|
131
|
+
getValidToken().then(async (token) => {
|
|
132
|
+
if (!token) {
|
|
133
|
+
setMessages((prev) => [...prev, { role: "assistant", text: " Not logged in. Run: whale login" }]);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const config = loadConfig();
|
|
137
|
+
const stores = await getStoresForUser(token, config.user_id || "");
|
|
138
|
+
if (stores.length === 0) {
|
|
139
|
+
setMessages((prev) => [...prev, { role: "assistant", text: " No stores found for this account." }]);
|
|
140
|
+
}
|
|
141
|
+
else if (stores.length === 1) {
|
|
142
|
+
setMessages((prev) => [...prev, { role: "assistant", text: ` Only one store: ${stores[0].name}` }]);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
setStoreList(stores);
|
|
146
|
+
setStoreSelectMode(true);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "/update": {
|
|
152
|
+
setMessages((prev) => [...prev, { role: "assistant", text: ` Checking for updates...` }]);
|
|
153
|
+
try {
|
|
154
|
+
const latest = execSync(`npm view ${PKG_NAME} version 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
155
|
+
if (latest === PKG_VERSION) {
|
|
156
|
+
setMessages((prev) => [...prev, { role: "assistant", text: ` ${symbols.check} Already on latest v${PKG_VERSION}` }]);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
setMessages((prev) => [...prev, { role: "assistant", text: ` v${PKG_VERSION} → v${latest} Installing...` }]);
|
|
160
|
+
try {
|
|
161
|
+
execSync(`npm install -g ${PKG_NAME}@latest 2>&1`, { encoding: "utf-8", timeout: 30000 });
|
|
162
|
+
setMessages((prev) => [...prev, { role: "assistant", text: ` ${symbols.check} Updated to v${latest}\n Restart whale to use the new version.` }]);
|
|
163
|
+
}
|
|
164
|
+
catch (installErr) {
|
|
165
|
+
setMessages((prev) => [...prev, { role: "assistant", text: ` ${symbols.cross} Install failed. Try manually:\n npm install -g ${PKG_NAME}@latest` }]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
setMessages((prev) => [...prev, { role: "assistant", text: ` ${symbols.cross} Could not check npm. Are you online?` }]);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "/tools": {
|
|
175
|
+
const lines = [];
|
|
176
|
+
lines.push(" Local (7)");
|
|
177
|
+
for (const t of LOCAL_TOOL_DEFINITIONS) {
|
|
178
|
+
lines.push(` ${t.name.padEnd(20)} ${t.description.slice(0, 48)}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push("");
|
|
181
|
+
if (serverToolsAvailable > 0) {
|
|
182
|
+
lines.push(` Server (${serverToolsAvailable})`);
|
|
183
|
+
const serverDefs = getAllServerToolDefinitions();
|
|
184
|
+
for (const t of serverDefs) {
|
|
185
|
+
lines.push(` ${t.name.padEnd(20)} ${(t.description || "").slice(0, 48)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
lines.push(" Server (unavailable — /mcp for details)");
|
|
190
|
+
}
|
|
191
|
+
setMessages((prev) => [...prev, { role: "assistant", text: lines.join("\n") }]);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, [exit, toolsExpanded, serverToolsAvailable]);
|
|
196
|
+
// ── Store Select ──
|
|
197
|
+
const handleStoreSelect = useCallback((store) => {
|
|
198
|
+
selectStore(store.id, store.name);
|
|
199
|
+
resetServerToolClient();
|
|
200
|
+
setStoreSelectMode(false);
|
|
201
|
+
setStoreList([]);
|
|
202
|
+
setUserLabel(store.name);
|
|
203
|
+
setMessages((prev) => [...prev, {
|
|
204
|
+
role: "assistant",
|
|
205
|
+
text: ` ${symbols.check} Switched to ${store.name}`,
|
|
206
|
+
}]);
|
|
207
|
+
// Re-check server tools with new store
|
|
208
|
+
getServerToolCount().then((count) => setServerToolsAvailable(count));
|
|
209
|
+
}, []);
|
|
210
|
+
const handleStoreCancel = useCallback(() => {
|
|
211
|
+
setStoreSelectMode(false);
|
|
212
|
+
setStoreList([]);
|
|
213
|
+
}, []);
|
|
214
|
+
// ── Send ──
|
|
215
|
+
const handleSend = useCallback(async (userMessage) => {
|
|
216
|
+
if (isStreaming)
|
|
217
|
+
return;
|
|
218
|
+
setMessages((prev) => [...prev, { role: "user", text: userMessage }]);
|
|
219
|
+
setStreamingText("");
|
|
220
|
+
setActiveTools([]);
|
|
221
|
+
setIsStreaming(true);
|
|
222
|
+
const abort = new AbortController();
|
|
223
|
+
abortRef.current = abort;
|
|
224
|
+
let accumulatedText = "";
|
|
225
|
+
const toolCalls = [];
|
|
226
|
+
let usage;
|
|
227
|
+
await runAgentLoop({
|
|
228
|
+
message: userMessage,
|
|
229
|
+
conversationHistory: conversationRef.current,
|
|
230
|
+
abortSignal: abort.signal,
|
|
231
|
+
callbacks: {
|
|
232
|
+
onText: (text) => {
|
|
233
|
+
accumulatedText += text;
|
|
234
|
+
setStreamingText(accumulatedText);
|
|
235
|
+
},
|
|
236
|
+
onToolStart: (name) => {
|
|
237
|
+
toolCalls.push({ name, status: "running" });
|
|
238
|
+
setActiveTools([...toolCalls]);
|
|
239
|
+
},
|
|
240
|
+
onToolResult: (name, success, result, input, durationMs) => {
|
|
241
|
+
const tc = toolCalls.find((t) => t.name === name && t.status === "running");
|
|
242
|
+
if (tc) {
|
|
243
|
+
tc.status = success ? "success" : "error";
|
|
244
|
+
tc.result = typeof result === "string" ? result : JSON.stringify(result);
|
|
245
|
+
tc.input = input;
|
|
246
|
+
tc.durationMs = durationMs;
|
|
247
|
+
setActiveTools([...toolCalls]);
|
|
248
|
+
}
|
|
249
|
+
accumulatedText = "";
|
|
250
|
+
setStreamingText("");
|
|
251
|
+
},
|
|
252
|
+
onUsage: (input_tokens, output_tokens) => {
|
|
253
|
+
usage = { input_tokens, output_tokens };
|
|
254
|
+
},
|
|
255
|
+
onDone: (finalMessages) => {
|
|
256
|
+
setMessages((prev) => [...prev, {
|
|
257
|
+
role: "assistant",
|
|
258
|
+
text: accumulatedText,
|
|
259
|
+
toolCalls: toolCalls.length > 0 ? [...toolCalls] : undefined,
|
|
260
|
+
usage,
|
|
261
|
+
}]);
|
|
262
|
+
setStreamingText("");
|
|
263
|
+
setActiveTools([]);
|
|
264
|
+
setIsStreaming(false);
|
|
265
|
+
abortRef.current = null;
|
|
266
|
+
conversationRef.current = finalMessages;
|
|
267
|
+
},
|
|
268
|
+
onError: (err) => {
|
|
269
|
+
if (err !== "Cancelled") {
|
|
270
|
+
setMessages((prev) => [...prev, { role: "assistant", text: `Error: ${err}` }]);
|
|
271
|
+
}
|
|
272
|
+
setStreamingText("");
|
|
273
|
+
setActiveTools([]);
|
|
274
|
+
setIsStreaming(false);
|
|
275
|
+
abortRef.current = null;
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}, [isStreaming]);
|
|
280
|
+
// ── Render ──
|
|
281
|
+
if (error) {
|
|
282
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: colors.brand, bold: true, children: "\u25C6 whale code" }), _jsx(Box, { height: 1 }), _jsx(Text, { color: colors.error, children: error })] }));
|
|
283
|
+
}
|
|
284
|
+
if (!ready) {
|
|
285
|
+
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { color: colors.tertiary, children: "loading..." }) }));
|
|
286
|
+
}
|
|
287
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: "\u25C6 whale code" }), userLabel && _jsxs(Text, { color: colors.dim, children: [" ", userLabel] }), serverToolsAvailable > 0 && (_jsxs(Text, { color: colors.tertiary, children: [" ", symbols.dot, " ", serverToolsAvailable, " server tools"] }))] }), _jsx(Text, { color: colors.separator, children: boxLine(56) }), _jsx(MessageList, { messages: messages, streamingText: streamingText, isStreaming: isStreaming, activeTools: activeTools, toolsExpanded: toolsExpanded }), storeSelectMode ? (_jsx(StoreSelector, { stores: storeList, currentStoreId: loadConfig().store_id || "", onSelect: handleStoreSelect, onCancel: handleStoreCancel })) : (_jsx(ChatInput, { onSubmit: handleSend, onCommand: handleCommand, disabled: isStreaming, agentName: "whale code" }))] }));
|
|
288
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatInput — clean input with slash command menu
|
|
3
|
+
*
|
|
4
|
+
* "/" triggers a select menu. Esc/backspace dismisses.
|
|
5
|
+
* Minimal chrome, Apple-style focus states.
|
|
6
|
+
*/
|
|
7
|
+
export interface SlashCommand {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const SLASH_COMMANDS: SlashCommand[];
|
|
12
|
+
interface ChatInputProps {
|
|
13
|
+
onSubmit: (message: string) => void;
|
|
14
|
+
onCommand: (command: string) => void;
|
|
15
|
+
disabled: boolean;
|
|
16
|
+
agentName?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function ChatInput({ onSubmit, onCommand, disabled, agentName }: ChatInputProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ChatInput — clean input with slash command menu
|
|
4
|
+
*
|
|
5
|
+
* "/" triggers a select menu. Esc/backspace dismisses.
|
|
6
|
+
* Minimal chrome, Apple-style focus states.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { Box, Text, useInput } from "ink";
|
|
10
|
+
import TextInput from "ink-text-input";
|
|
11
|
+
import SelectInput from "ink-select-input";
|
|
12
|
+
import Spinner from "ink-spinner";
|
|
13
|
+
import { colors, symbols } from "../shared/Theme.js";
|
|
14
|
+
export const SLASH_COMMANDS = [
|
|
15
|
+
{ name: "/help", description: "Show available commands" },
|
|
16
|
+
{ name: "/tools", description: "List all tools" },
|
|
17
|
+
{ name: "/mcp", description: "Server connection status" },
|
|
18
|
+
{ name: "/store", description: "Switch active store" },
|
|
19
|
+
{ name: "/status", description: "Show session info" },
|
|
20
|
+
{ name: "/update", description: "Check for updates & install" },
|
|
21
|
+
{ name: "/clear", description: "Clear conversation" },
|
|
22
|
+
{ name: "/exit", description: "Exit" },
|
|
23
|
+
];
|
|
24
|
+
export function ChatInput({ onSubmit, onCommand, disabled, agentName }) {
|
|
25
|
+
const [value, setValue] = useState("");
|
|
26
|
+
const [menuMode, setMenuMode] = useState(false);
|
|
27
|
+
const handleChange = (newValue) => {
|
|
28
|
+
if (newValue === "/") {
|
|
29
|
+
setMenuMode(true);
|
|
30
|
+
setValue("");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setValue(newValue);
|
|
34
|
+
};
|
|
35
|
+
useInput((input, key) => {
|
|
36
|
+
if (!menuMode || disabled)
|
|
37
|
+
return;
|
|
38
|
+
if (key.escape || key.delete || key.backspace) {
|
|
39
|
+
setMenuMode(false);
|
|
40
|
+
}
|
|
41
|
+
}, { isActive: menuMode });
|
|
42
|
+
const handleMenuSelect = (item) => {
|
|
43
|
+
setMenuMode(false);
|
|
44
|
+
setValue("");
|
|
45
|
+
onCommand(item.value);
|
|
46
|
+
};
|
|
47
|
+
const handleSubmit = (text) => {
|
|
48
|
+
const trimmed = text.trim();
|
|
49
|
+
if (!trimmed || disabled)
|
|
50
|
+
return;
|
|
51
|
+
onSubmit(trimmed);
|
|
52
|
+
setValue("");
|
|
53
|
+
};
|
|
54
|
+
// Thinking state — smooth spinner
|
|
55
|
+
if (disabled) {
|
|
56
|
+
return (_jsx(Box, { children: _jsx(Text, { color: colors.brand, children: _jsx(Spinner, { type: "dots" }) }) }));
|
|
57
|
+
}
|
|
58
|
+
// Slash command menu
|
|
59
|
+
if (menuMode) {
|
|
60
|
+
const items = SLASH_COMMANDS.map((c) => ({
|
|
61
|
+
label: c.name,
|
|
62
|
+
value: c.name,
|
|
63
|
+
}));
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SelectInput, { items: items, onSelect: handleMenuSelect, indicatorComponent: ({ isSelected }) => (_jsxs(Text, { color: isSelected ? colors.brand : colors.quaternary, children: [isSelected ? "›" : " ", " "] })), itemComponent: ({ isSelected, label }) => {
|
|
65
|
+
const cmd = SLASH_COMMANDS.find((c) => c.name === label);
|
|
66
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? colors.brand : colors.secondary, bold: isSelected, children: label }), _jsxs(Text, { color: colors.tertiary, children: [" ", cmd?.description] })] }));
|
|
67
|
+
} }), _jsx(Text, { color: colors.quaternary, children: " esc to dismiss" })] }));
|
|
68
|
+
}
|
|
69
|
+
// Normal input
|
|
70
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: colors.brand, bold: true, children: [symbols.user, " "] }), _jsx(TextInput, { value: value, onChange: handleChange, onSubmit: handleSubmit, placeholder: `Message ${agentName || "whale"}, or /` })] }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownText — renders markdown as ANSI-styled terminal output
|
|
3
|
+
*/
|
|
4
|
+
interface MarkdownTextProps {
|
|
5
|
+
text: string;
|
|
6
|
+
streaming?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function MarkdownText({ text, streaming }: MarkdownTextProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* MarkdownText — renders markdown as ANSI-styled terminal output
|
|
4
|
+
*/
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
import { Text } from "ink";
|
|
7
|
+
import { renderMarkdown } from "../shared/markdown.js";
|
|
8
|
+
/**
|
|
9
|
+
* During streaming, an odd number of ``` fences means a code block is unclosed.
|
|
10
|
+
* Append a closing fence so marked renders it properly mid-stream.
|
|
11
|
+
*/
|
|
12
|
+
function closeIncompleteFences(text) {
|
|
13
|
+
const fenceCount = (text.match(/^```/gm) || []).length;
|
|
14
|
+
if (fenceCount % 2 !== 0) {
|
|
15
|
+
return text + "\n```";
|
|
16
|
+
}
|
|
17
|
+
return text;
|
|
18
|
+
}
|
|
19
|
+
export function MarkdownText({ text, streaming = false }) {
|
|
20
|
+
const rendered = useMemo(() => {
|
|
21
|
+
if (!text)
|
|
22
|
+
return "";
|
|
23
|
+
try {
|
|
24
|
+
const safeText = streaming ? closeIncompleteFences(text) : text;
|
|
25
|
+
return renderMarkdown(safeText);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
}, [text, streaming]);
|
|
31
|
+
return _jsx(Text, { children: rendered });
|
|
32
|
+
}
|