@mangomagic/cli 0.1.4 → 0.1.5
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/README.md +2 -7
- package/package.json +1 -1
- package/src/ai/kimi.mjs +4 -63
- package/src/chat/natural-language.mjs +5 -12
- package/src/index.mjs +0 -28
- package/src/secrets/moonshot-key-store.mjs +0 -110
- package/src/ui/prompt-hidden.mjs +0 -45
package/README.md
CHANGED
|
@@ -20,7 +20,6 @@ at `~/.mangomagic/credentials.json` (mode 0600) so the next runs skip auth.
|
|
|
20
20
|
| `mangomagic home` | Show fast wins after sign-in. |
|
|
21
21
|
| `mangomagic chat` | Start an interactive natural-language session. |
|
|
22
22
|
| `mangomagic ask "..."` | Run one natural-language request. |
|
|
23
|
-
| `mangomagic key set` | Store a Moonshot/Kimi API key in macOS Keychain, or a mode-0600 fallback file on other platforms. |
|
|
24
23
|
| `mangomagic tools` | Show available MCP tools and near-next workflows. Add `--json` for machine-readable output. |
|
|
25
24
|
| `mangomagic cards` | Open the Talking Cards workspace. Add `--no-open` to print the URL only. |
|
|
26
25
|
| `mangomagic splash` | Show the splash once. `--anim`, `--loop` supported. |
|
|
@@ -52,12 +51,8 @@ npx -y @mangomagic/cli chat
|
|
|
52
51
|
npx -y @mangomagic/cli create talking cards about AI adoption
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
Obvious requests are handled locally. Fuzzy routing uses
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
npx -y @mangomagic/cli key set
|
|
60
|
-
```
|
|
54
|
+
Obvious requests are handled locally. Fuzzy routing uses MangoMagic's backend AI
|
|
55
|
+
router, so end users never need to provide a Moonshot/Kimi key.
|
|
61
56
|
|
|
62
57
|
## Requirements
|
|
63
58
|
|
package/package.json
CHANGED
package/src/ai/kimi.mjs
CHANGED
|
@@ -1,66 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export class MissingMoonshotKeyError extends Error {
|
|
4
|
-
constructor() {
|
|
5
|
-
super("Missing Moonshot API key.");
|
|
6
|
-
this.name = "MissingMoonshotKeyError";
|
|
7
|
-
}
|
|
8
|
-
}
|
|
1
|
+
import { apiCall } from "../api.mjs";
|
|
9
2
|
|
|
10
3
|
export async function planWithKimi(userText, context = {}) {
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
13
|
-
|
|
14
|
-
const response = await fetch("https://api.moonshot.ai/v1/chat/completions", {
|
|
15
|
-
method: "POST",
|
|
16
|
-
headers: {
|
|
17
|
-
"Content-Type": "application/json",
|
|
18
|
-
"Authorization": `Bearer ${secret.key}`,
|
|
19
|
-
},
|
|
20
|
-
body: JSON.stringify({
|
|
21
|
-
model: "kimi-k2.6",
|
|
22
|
-
messages: [
|
|
23
|
-
{
|
|
24
|
-
role: "system",
|
|
25
|
-
content: `You route natural-language terminal requests for MangoMagic.
|
|
26
|
-
|
|
27
|
-
Return only compact JSON. No markdown.
|
|
28
|
-
|
|
29
|
-
Allowed actions:
|
|
30
|
-
- open_cards: open the Talking Cards generator. Args: {"focus": optional string}
|
|
31
|
-
- open_brand_doc: open Brand Doc setup/review.
|
|
32
|
-
- show_tools: show available MCP tools.
|
|
33
|
-
- mcp_config: print MCP config.
|
|
34
|
-
- list_episodes: list recent episodes. Args: {"limit": integer 1-10}
|
|
35
|
-
- search_episodes: search episodes. Args: {"query": string}
|
|
36
|
-
- get_episode: get one episode. Args: {"episode": id or slug}
|
|
37
|
-
- home: show fast wins.
|
|
38
|
-
- answer: answer briefly without external action. Args: {"text": string}
|
|
39
|
-
|
|
40
|
-
Prefer a direct tool action over a chatty answer. If the user asks to create talking cards, choose open_cards and preserve their idea as focus.`,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
role: "user",
|
|
44
|
-
content: JSON.stringify({ userText, context }),
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
thinking: { type: "disabled" },
|
|
48
|
-
max_tokens: 700,
|
|
49
|
-
}),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const bodyText = await response.text();
|
|
53
|
-
let body;
|
|
54
|
-
try { body = JSON.parse(bodyText); } catch { body = null; }
|
|
55
|
-
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
const message = body?.error?.message || body?.error || bodyText || `HTTP ${response.status}`;
|
|
58
|
-
throw new Error(`Kimi request failed: ${message}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const content = body?.choices?.[0]?.message?.content;
|
|
62
|
-
if (!content) throw new Error("Kimi returned no routing decision.");
|
|
63
|
-
|
|
64
|
-
const jsonText = String(content).trim().replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/```$/i, "").trim();
|
|
65
|
-
return JSON.parse(jsonText);
|
|
4
|
+
const result = await apiCall("cli-ai-route", { body: { text: userText, context } });
|
|
5
|
+
if (!result?.plan?.action) throw new Error("MangoMagic AI returned no routing decision.");
|
|
6
|
+
return result.plan;
|
|
66
7
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
2
|
import { stdin as input, stdout as output } from "node:process";
|
|
3
3
|
import { apiCall } from "../api.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { moonshotKeyStorageHint } from "../secrets/moonshot-key-store.mjs";
|
|
4
|
+
import { planWithKimi } from "../ai/kimi.mjs";
|
|
6
5
|
|
|
7
6
|
const GOLD = "\x1b[38;2;241;171;28m";
|
|
8
7
|
const DIM = "\x1b[2m";
|
|
@@ -85,22 +84,16 @@ export async function handleNaturalLanguage(text, actions, { allowModel = true }
|
|
|
85
84
|
availableTools: ["open_cards", "open_brand_doc", "show_tools", "mcp_config", "list_episodes", "search_episodes", "get_episode", "home"],
|
|
86
85
|
});
|
|
87
86
|
} catch (err) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
${BOLD}Natural-language routing needs a Kimi API key for fuzzy requests.${RESET}
|
|
87
|
+
process.stdout.write(`
|
|
88
|
+
${BOLD}MangoMagic AI routing is unavailable right now.${RESET}
|
|
91
89
|
|
|
92
90
|
Obvious commands still work, for example:
|
|
93
91
|
${GOLD}npx -y @mangomagic/cli create talking cards${RESET}
|
|
94
92
|
${GOLD}npx -y @mangomagic/cli search episodes for AI adoption${RESET}
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
${GOLD}npx -y @mangomagic/cli key set${RESET}
|
|
98
|
-
|
|
99
|
-
Storage: ${moonshotKeyStorageHint()}
|
|
94
|
+
${DIM}${err?.message ?? err}${RESET}
|
|
100
95
|
`);
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
throw err;
|
|
96
|
+
return;
|
|
104
97
|
}
|
|
105
98
|
}
|
|
106
99
|
|
package/src/index.mjs
CHANGED
|
@@ -2,10 +2,8 @@ import { deviceLogin } from "./auth/device-flow.mjs";
|
|
|
2
2
|
import { chat, handleNaturalLanguage } from "./chat/natural-language.mjs";
|
|
3
3
|
import { loadToken, saveToken, clearToken, credentialsPath } from "./auth/token-store.mjs";
|
|
4
4
|
import { APP_ORIGIN } from "./config.mjs";
|
|
5
|
-
import { clearMoonshotKey, loadMoonshotKey, moonshotKeyStorageHint, saveMoonshotKey } from "./secrets/moonshot-key-store.mjs";
|
|
6
5
|
import { openUrl } from "./system/open-url.mjs";
|
|
7
6
|
import { COMMAND_PREFIX, MCP_TOOL_CATALOG, NEXT_WORKFLOWS, QUICK_WINS } from "./tools/catalog.mjs";
|
|
8
|
-
import { promptHidden } from "./ui/prompt-hidden.mjs";
|
|
9
7
|
import { playSplash } from "./ui/splash.mjs";
|
|
10
8
|
|
|
11
9
|
const GOLD = "\x1b[38;2;241;171;28m";
|
|
@@ -22,7 +20,6 @@ ${BOLD}mangomagic${RESET} ${DIM}— sign into MangoMagic and bring your account
|
|
|
22
20
|
${GOLD}mangomagic whoami${RESET} Show the cached identity.
|
|
23
21
|
${GOLD}mangomagic home${RESET} Show fast wins for creating value right now.
|
|
24
22
|
${GOLD}mangomagic chat${RESET} Talk to MangoMagic in natural language.
|
|
25
|
-
${GOLD}mangomagic key set${RESET} Store a Moonshot/Kimi API key securely.
|
|
26
23
|
${GOLD}mangomagic tools${RESET} Show available MCP tools and next workflows.
|
|
27
24
|
${GOLD}mangomagic cards${RESET} Open the Talking Cards workspace.
|
|
28
25
|
${GOLD}mangomagic splash${RESET} Show the MangoMagic splash once. Add --anim or --loop for motion.
|
|
@@ -169,29 +166,6 @@ function mcpConfig() {
|
|
|
169
166
|
process.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
|
|
170
167
|
}
|
|
171
168
|
|
|
172
|
-
async function keyCommand(argv) {
|
|
173
|
-
const sub = argv[0] || "status";
|
|
174
|
-
if (sub === "set") {
|
|
175
|
-
const key = await promptHidden("Moonshot API key: ");
|
|
176
|
-
const where = saveMoonshotKey(key);
|
|
177
|
-
process.stdout.write(`Saved Moonshot API key in ${where}.\n`);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (sub === "clear" || sub === "delete" || sub === "remove") {
|
|
181
|
-
const cleared = clearMoonshotKey();
|
|
182
|
-
process.stdout.write(cleared ? "Moonshot API key removed.\n" : "No stored Moonshot API key found.\n");
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (sub === "status") {
|
|
186
|
-
const key = loadMoonshotKey();
|
|
187
|
-
if (key) process.stdout.write(`Moonshot API key found via ${key.source}.\n`);
|
|
188
|
-
else process.stdout.write(`No Moonshot API key found. Store one with \`${COMMAND_PREFIX} key set\`.\nStorage: ${moonshotKeyStorageHint()}\n`);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
process.stderr.write(`Unknown key command: ${sub}\n`);
|
|
192
|
-
process.exit(2);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
169
|
function actions() {
|
|
196
170
|
return { cards, openPath, tools, mcpConfig, home };
|
|
197
171
|
}
|
|
@@ -205,8 +179,6 @@ export async function run(argv) {
|
|
|
205
179
|
case "home": return home();
|
|
206
180
|
case "chat": return chat(actions());
|
|
207
181
|
case "ask": return handleNaturalLanguage(argv.slice(1).join(" "), actions());
|
|
208
|
-
case "key":
|
|
209
|
-
case "kimi-key": return keyCommand(argv.slice(1));
|
|
210
182
|
case "tools": return tools({ json: argv.includes("--json") });
|
|
211
183
|
case "cards": return cards({ openInBrowser: !argv.includes("--no-open") && process.env.MANGOMAGIC_CLI_NO_OPEN !== "1" });
|
|
212
184
|
case "splash": return playSplash({ mode: argv.includes("--loop") ? "loop" : argv.includes("--anim") ? "anim" : "static" });
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, chmodSync } from "node:fs";
|
|
3
|
-
import { homedir, userInfo } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const SERVICE = "mangomagic.moonshot.api-key";
|
|
7
|
-
const dir = join(homedir(), ".mangomagic");
|
|
8
|
-
const fallbackFile = join(dir, "moonshot.json");
|
|
9
|
-
|
|
10
|
-
function account() {
|
|
11
|
-
return process.env.USER || userInfo().username || "default";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function canUseKeychain() {
|
|
15
|
-
return process.platform === "darwin" && existsSync("/usr/bin/security");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function readKeychain() {
|
|
19
|
-
if (!canUseKeychain()) return null;
|
|
20
|
-
try {
|
|
21
|
-
const key = execFileSync("/usr/bin/security", [
|
|
22
|
-
"find-generic-password",
|
|
23
|
-
"-a", account(),
|
|
24
|
-
"-s", SERVICE,
|
|
25
|
-
"-w",
|
|
26
|
-
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
27
|
-
return key || null;
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeKeychain(key) {
|
|
34
|
-
execFileSync("/usr/bin/security", [
|
|
35
|
-
"add-generic-password",
|
|
36
|
-
"-a", account(),
|
|
37
|
-
"-s", SERVICE,
|
|
38
|
-
"-w", key,
|
|
39
|
-
"-U",
|
|
40
|
-
], { stdio: "ignore" });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function clearKeychain() {
|
|
44
|
-
if (!canUseKeychain()) return false;
|
|
45
|
-
try {
|
|
46
|
-
execFileSync("/usr/bin/security", [
|
|
47
|
-
"delete-generic-password",
|
|
48
|
-
"-a", account(),
|
|
49
|
-
"-s", SERVICE,
|
|
50
|
-
], { stdio: "ignore" });
|
|
51
|
-
return true;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function readFallback() {
|
|
58
|
-
if (!existsSync(fallbackFile)) return null;
|
|
59
|
-
try {
|
|
60
|
-
const raw = JSON.parse(readFileSync(fallbackFile, "utf8"));
|
|
61
|
-
return typeof raw?.apiKey === "string" ? raw.apiKey : null;
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function writeFallback(key) {
|
|
68
|
-
mkdirSync(dir, { recursive: true });
|
|
69
|
-
writeFileSync(fallbackFile, JSON.stringify({ apiKey: key }, null, 2));
|
|
70
|
-
try { chmodSync(fallbackFile, 0o600); } catch { /* windows */ }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function loadMoonshotKey() {
|
|
74
|
-
if (process.env.MOONSHOT_API_KEY) return { key: process.env.MOONSHOT_API_KEY, source: "MOONSHOT_API_KEY" };
|
|
75
|
-
const keychainKey = readKeychain();
|
|
76
|
-
if (keychainKey) return { key: keychainKey, source: "macOS Keychain" };
|
|
77
|
-
const fileKey = readFallback();
|
|
78
|
-
if (fileKey) return { key: fileKey, source: fallbackFile };
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function saveMoonshotKey(key) {
|
|
83
|
-
if (!key || typeof key !== "string") throw new Error("Missing API key.");
|
|
84
|
-
const trimmed = key.trim();
|
|
85
|
-
if (!trimmed) throw new Error("Missing API key.");
|
|
86
|
-
|
|
87
|
-
if (canUseKeychain()) {
|
|
88
|
-
writeKeychain(trimmed);
|
|
89
|
-
return "macOS Keychain";
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
writeFallback(trimmed);
|
|
93
|
-
return fallbackFile;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function clearMoonshotKey() {
|
|
97
|
-
const keychainCleared = clearKeychain();
|
|
98
|
-
let fileCleared = false;
|
|
99
|
-
if (existsSync(fallbackFile)) {
|
|
100
|
-
unlinkSync(fallbackFile);
|
|
101
|
-
fileCleared = true;
|
|
102
|
-
}
|
|
103
|
-
return keychainCleared || fileCleared;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function moonshotKeyStorageHint() {
|
|
107
|
-
return canUseKeychain()
|
|
108
|
-
? "macOS Keychain service mangomagic.moonshot.api-key"
|
|
109
|
-
: `${fallbackFile} (mode 0600) or MOONSHOT_API_KEY`;
|
|
110
|
-
}
|
package/src/ui/prompt-hidden.mjs
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export function promptHidden(prompt) {
|
|
2
|
-
return new Promise((resolve, reject) => {
|
|
3
|
-
const stdin = process.stdin;
|
|
4
|
-
const stdout = process.stdout;
|
|
5
|
-
let value = "";
|
|
6
|
-
let rawWasEnabled = false;
|
|
7
|
-
|
|
8
|
-
function cleanup() {
|
|
9
|
-
stdin.off("data", onData);
|
|
10
|
-
if (rawWasEnabled && stdin.isTTY) stdin.setRawMode(false);
|
|
11
|
-
stdin.pause();
|
|
12
|
-
stdout.write("\n");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function onData(chunk) {
|
|
16
|
-
const s = String(chunk);
|
|
17
|
-
for (const ch of s) {
|
|
18
|
-
if (ch === "\u0003") {
|
|
19
|
-
cleanup();
|
|
20
|
-
reject(new Error("Cancelled."));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (ch === "\r" || ch === "\n") {
|
|
24
|
-
cleanup();
|
|
25
|
-
resolve(value);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (ch === "\u007f" || ch === "\b") {
|
|
29
|
-
value = value.slice(0, -1);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
value += ch;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
stdout.write(prompt);
|
|
37
|
-
stdin.resume();
|
|
38
|
-
stdin.setEncoding("utf8");
|
|
39
|
-
if (stdin.isTTY) {
|
|
40
|
-
stdin.setRawMode(true);
|
|
41
|
-
rawWasEnabled = true;
|
|
42
|
-
}
|
|
43
|
-
stdin.on("data", onData);
|
|
44
|
-
});
|
|
45
|
-
}
|