@manix-cli/manix 0.1.2
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/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/manix.js +9 -0
- package/dist/agent.js +194 -0
- package/dist/cli.js +123 -0
- package/dist/config.js +38 -0
- package/dist/manixmd.js +18 -0
- package/dist/markdown.js +35 -0
- package/dist/mcp.js +100 -0
- package/dist/openrouter.js +108 -0
- package/dist/permissions.js +19 -0
- package/dist/print.js +51 -0
- package/dist/prompts.js +47 -0
- package/dist/sessions.js +80 -0
- package/dist/skills.js +70 -0
- package/dist/slash.js +31 -0
- package/dist/theme.js +84 -0
- package/dist/tools/bash.js +44 -0
- package/dist/tools/fs-tools.js +119 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/search.js +106 -0
- package/dist/ui/App.js +345 -0
- package/dist/ui/Footer.js +38 -0
- package/dist/ui/InputBox.js +123 -0
- package/dist/ui/LogItem.js +82 -0
- package/dist/ui/Mascot.js +13 -0
- package/dist/ui/Onboarding.js +60 -0
- package/dist/ui/PermissionPrompt.js +32 -0
- package/dist/ui/Picker.js +69 -0
- package/dist/ui/SpinnerLine.js +65 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Manindhra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# ▲ Manix
|
|
2
|
+
|
|
3
|
+
A fast, beautiful terminal coding agent powered by [OpenRouter](https://openrouter.ai) —
|
|
4
|
+
any model (Claude, GPT, Gemini, DeepSeek, free models), one key. Hand-written agent loop,
|
|
5
|
+
no frameworks.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g manix # or: npx manix
|
|
9
|
+
cd your-project
|
|
10
|
+
manix
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
First run asks for your OpenRouter API key (get one at [openrouter.ai/keys](https://openrouter.ai/keys)) —
|
|
14
|
+
or `export OPENROUTER_API_KEY=...`.
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
❯ fix the failing test in src/auth.test.js
|
|
20
|
+
● Bash(npm test)
|
|
21
|
+
● Read(src/auth.js)
|
|
22
|
+
● Edit(src/auth.js) ← asks permission, shows the diff
|
|
23
|
+
⏺ Fixed — the token expiry check was inverted. Tests pass.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Agentic loop** — streams responses, reads/writes/edits files, runs shell commands,
|
|
27
|
+
greps the repo, keeps going until the task is done.
|
|
28
|
+
- **Safe by default** — mutating actions ask permission (yes / always this session / no).
|
|
29
|
+
`--yolo` to live dangerously.
|
|
30
|
+
- **Any model** — `/model` opens a live picker with context windows and $/M pricing.
|
|
31
|
+
- **Sessions** — every conversation is saved; `manix --continue` or `/resume`.
|
|
32
|
+
- **MANIX.md** — project memory auto-loaded each session; generate with `/init`.
|
|
33
|
+
- **Skills** — drop a `SKILL.md` playbook in `~/.manix/skills/<name>/` or `.manix/skills/<name>/`;
|
|
34
|
+
the agent loads it when relevant, or invoke directly with `/<name>`.
|
|
35
|
+
- **MCP** — add servers to `~/.manix/mcp.json` or `.manix/mcp.json`:
|
|
36
|
+
```json
|
|
37
|
+
{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] } } }
|
|
38
|
+
```
|
|
39
|
+
- **Cost tracking** — `/cost` shows session spend and remaining OpenRouter credits.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Command | | Flags | |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `/help` | commands & shortcuts | `manix "task"` | start with a prompt |
|
|
46
|
+
| `/model [id]` | switch model | `-p, --print` | non-interactive mode |
|
|
47
|
+
| `/resume` | resume a session | `-m, --model <id>` | model for this run |
|
|
48
|
+
| `/compact` | summarize history | `-c, --continue` | resume last session |
|
|
49
|
+
| `/cost` | usage + credits | `-r, --resume [id]` | pick a session |
|
|
50
|
+
| `/mcp` `/skills` | status / list | `--yolo` | auto-approve tools |
|
|
51
|
+
| `/init` | generate MANIX.md | `-v` `-h` | version / help |
|
|
52
|
+
| `/clear` `/yolo` `/exit` | | `echo task \| manix` | pipe a prompt |
|
|
53
|
+
|
|
54
|
+
Shortcuts: **Esc** interrupt · **↑/↓** history · **Tab** complete · **Ctrl+C** twice to quit.
|
|
55
|
+
|
|
56
|
+
## Skills format
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
---
|
|
60
|
+
name: commit
|
|
61
|
+
description: Write conventional commits from the staged diff
|
|
62
|
+
---
|
|
63
|
+
Steps: run `git diff --staged`, group changes, write a conventional commit message…
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install && npm test # build + offline smoke tests
|
|
70
|
+
npm run dev # build & run locally
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Plain JavaScript (ESM) + JSX via esbuild. Ink 5 TUI. Raw-fetch OpenRouter client with a
|
|
74
|
+
hand-rolled SSE parser — see [CLAUDE.md](CLAUDE.md) for the full architecture.
|
package/bin/manix.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import('../dist/cli.js').catch((err) => {
|
|
3
|
+
if (err?.code === 'ERR_MODULE_NOT_FOUND' && String(err.message).includes('dist')) {
|
|
4
|
+
console.error('manix: dist/ missing — run `npm run build` first.')
|
|
5
|
+
} else {
|
|
6
|
+
console.error(err)
|
|
7
|
+
}
|
|
8
|
+
process.exit(1)
|
|
9
|
+
})
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { streamChat, fetchModels, cachedModels } from "./openrouter.js";
|
|
2
|
+
import { systemPrompt, COMPACT_PROMPT } from "./prompts.js";
|
|
3
|
+
import { builtinTools, toOpenAI } from "./tools/index.js";
|
|
4
|
+
import { skillTool } from "./skills.js";
|
|
5
|
+
import { appendMessage, createSession } from "./sessions.js";
|
|
6
|
+
class Agent {
|
|
7
|
+
constructor({ config, cwd, permissions, mcp, skills, contextFiles, handlers }) {
|
|
8
|
+
this.apiKey = config.apiKey;
|
|
9
|
+
this.model = config.model;
|
|
10
|
+
this.cwd = cwd;
|
|
11
|
+
this.permissions = permissions;
|
|
12
|
+
this.mcp = mcp;
|
|
13
|
+
this.skills = skills || [];
|
|
14
|
+
this.contextFiles = contextFiles || [];
|
|
15
|
+
this.h = handlers || {};
|
|
16
|
+
this.messages = [];
|
|
17
|
+
this.session = null;
|
|
18
|
+
this.usage = { prompt: 0, completion: 0, cost: 0, requests: 0 };
|
|
19
|
+
this.contextLength = 128e3;
|
|
20
|
+
this.ac = null;
|
|
21
|
+
fetchModels(this.apiKey).then(() => this.refreshModelInfo()).catch(() => {
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
refreshModelInfo() {
|
|
25
|
+
const m = (cachedModels() || []).find((x) => x.id === this.model);
|
|
26
|
+
if (m?.context_length) this.contextLength = m.context_length;
|
|
27
|
+
this.h.onStats?.(this.stats());
|
|
28
|
+
}
|
|
29
|
+
setModel(id) {
|
|
30
|
+
this.model = id;
|
|
31
|
+
this.refreshModelInfo();
|
|
32
|
+
}
|
|
33
|
+
get busy() {
|
|
34
|
+
return this.ac !== null;
|
|
35
|
+
}
|
|
36
|
+
pushMessage(msg) {
|
|
37
|
+
if (!this.session) this.session = createSession(this.cwd, this.model);
|
|
38
|
+
this.messages.push(msg);
|
|
39
|
+
appendMessage(this.session, msg);
|
|
40
|
+
}
|
|
41
|
+
resumeFrom(loaded) {
|
|
42
|
+
this.messages = loaded.messages;
|
|
43
|
+
this.session = { id: loaded.meta.id, file: loaded.file };
|
|
44
|
+
}
|
|
45
|
+
reset() {
|
|
46
|
+
this.messages = [];
|
|
47
|
+
this.session = null;
|
|
48
|
+
}
|
|
49
|
+
buildTools() {
|
|
50
|
+
return [...builtinTools(), skillTool(this.skills), ...this.mcp?.getTools() || []];
|
|
51
|
+
}
|
|
52
|
+
estimateTokens() {
|
|
53
|
+
return Math.round((JSON.stringify(this.messages).length + 4e3) / 4);
|
|
54
|
+
}
|
|
55
|
+
stats() {
|
|
56
|
+
return { ...this.usage, tokens: this.estimateTokens(), contextLength: this.contextLength };
|
|
57
|
+
}
|
|
58
|
+
trackUsage(usage) {
|
|
59
|
+
if (!usage) return;
|
|
60
|
+
this.usage.prompt += usage.prompt_tokens || 0;
|
|
61
|
+
this.usage.completion += usage.completion_tokens || 0;
|
|
62
|
+
this.usage.requests += 1;
|
|
63
|
+
if (typeof usage.cost === "number") {
|
|
64
|
+
this.usage.cost += usage.cost;
|
|
65
|
+
} else {
|
|
66
|
+
const m = (cachedModels() || []).find((x) => x.id === this.model);
|
|
67
|
+
if (m?.pricing) {
|
|
68
|
+
this.usage.cost += (usage.prompt_tokens || 0) * parseFloat(m.pricing.prompt || 0) + (usage.completion_tokens || 0) * parseFloat(m.pricing.completion || 0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.h.onStats?.(this.stats());
|
|
72
|
+
}
|
|
73
|
+
abort() {
|
|
74
|
+
this.ac?.abort();
|
|
75
|
+
}
|
|
76
|
+
async send(text) {
|
|
77
|
+
this.pushMessage({ role: "user", content: text });
|
|
78
|
+
await this.run();
|
|
79
|
+
}
|
|
80
|
+
async run() {
|
|
81
|
+
this.ac = new AbortController();
|
|
82
|
+
const tools = this.buildTools();
|
|
83
|
+
const toolSchemas = tools.map(toOpenAI);
|
|
84
|
+
try {
|
|
85
|
+
for (let turn = 0; turn < 40; turn++) {
|
|
86
|
+
const sys = {
|
|
87
|
+
role: "system",
|
|
88
|
+
content: systemPrompt({
|
|
89
|
+
cwd: this.cwd,
|
|
90
|
+
model: this.model,
|
|
91
|
+
contextFiles: this.contextFiles,
|
|
92
|
+
skills: this.skills
|
|
93
|
+
})
|
|
94
|
+
};
|
|
95
|
+
const res = await streamChat({
|
|
96
|
+
apiKey: this.apiKey,
|
|
97
|
+
model: this.model,
|
|
98
|
+
messages: [sys, ...this.messages],
|
|
99
|
+
tools: toolSchemas,
|
|
100
|
+
signal: this.ac.signal,
|
|
101
|
+
onText: this.h.onTextDelta
|
|
102
|
+
});
|
|
103
|
+
this.trackUsage(res.usage);
|
|
104
|
+
const assistant = { role: "assistant", content: res.content || null };
|
|
105
|
+
if (res.toolCalls.length) assistant.tool_calls = res.toolCalls;
|
|
106
|
+
this.pushMessage(assistant);
|
|
107
|
+
this.h.onAssistantDone?.(res.content);
|
|
108
|
+
if (!res.toolCalls.length) return;
|
|
109
|
+
for (const tc of res.toolCalls) {
|
|
110
|
+
const result = await this.execTool(tools, tc);
|
|
111
|
+
this.pushMessage({
|
|
112
|
+
role: "tool",
|
|
113
|
+
tool_call_id: tc.id,
|
|
114
|
+
content: result.length > 6e4 ? result.slice(0, 6e4) + "\n\u2026[truncated]" : result
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (this.estimateTokens() > this.contextLength * 0.8) {
|
|
118
|
+
this.h.onInfo?.("Context above 80% \u2014 run /compact to summarize and free space.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this.h.onInfo?.("Stopped after 40 tool turns \u2014 send a message to continue.");
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err?.name === "AbortError" || this.ac?.signal.aborted) {
|
|
124
|
+
this.h.onInfo?.("Interrupted.");
|
|
125
|
+
} else {
|
|
126
|
+
this.h.onError?.(err?.message || String(err));
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
this.ac = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async execTool(tools, tc) {
|
|
133
|
+
const name = tc.function?.name || "";
|
|
134
|
+
const tool = tools.find((t) => t.name === name);
|
|
135
|
+
if (!tool) {
|
|
136
|
+
this.h.onToolEnd?.({ display: name, ok: false, summary: "unknown tool" });
|
|
137
|
+
return `Error: unknown tool "${name}"`;
|
|
138
|
+
}
|
|
139
|
+
let args;
|
|
140
|
+
try {
|
|
141
|
+
args = JSON.parse(tc.function.arguments || "{}");
|
|
142
|
+
} catch {
|
|
143
|
+
this.h.onToolEnd?.({ display: name, ok: false, summary: "bad arguments" });
|
|
144
|
+
return "Error: tool arguments were not valid JSON";
|
|
145
|
+
}
|
|
146
|
+
const display = tool.describe ? tool.describe(args) : name;
|
|
147
|
+
if (this.ac.signal.aborted) return "Interrupted by user before execution.";
|
|
148
|
+
const ask = this.h.requestPermission ? () => this.h.requestPermission({ display, preview: tool.preview?.(args) || null }) : null;
|
|
149
|
+
const allowed = await this.permissions.check(tool, ask);
|
|
150
|
+
if (!allowed) {
|
|
151
|
+
this.h.onToolEnd?.({ display, ok: false, summary: "permission denied" });
|
|
152
|
+
return "The user denied permission for this action. Do not retry it; ask or take another approach.";
|
|
153
|
+
}
|
|
154
|
+
this.h.onToolStart?.({ display });
|
|
155
|
+
try {
|
|
156
|
+
const out = String(await tool.run(args, { cwd: this.cwd }));
|
|
157
|
+
this.h.onToolEnd?.({ display, ok: true, summary: summarize(out) });
|
|
158
|
+
return out;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
this.h.onToolEnd?.({ display, ok: false, summary: err.message });
|
|
161
|
+
return `Error: ${err.message}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Replace history with a model-written summary; starts a new session file. */
|
|
165
|
+
async compact() {
|
|
166
|
+
this.ac = new AbortController();
|
|
167
|
+
try {
|
|
168
|
+
const res = await streamChat({
|
|
169
|
+
apiKey: this.apiKey,
|
|
170
|
+
model: this.model,
|
|
171
|
+
messages: [...this.messages, { role: "user", content: COMPACT_PROMPT }],
|
|
172
|
+
signal: this.ac.signal
|
|
173
|
+
});
|
|
174
|
+
this.messages = [
|
|
175
|
+
{ role: "user", content: "[Earlier conversation was compacted. Summary:]\n\n" + res.content },
|
|
176
|
+
{ role: "assistant", content: "Got it \u2014 continuing from that summary." }
|
|
177
|
+
];
|
|
178
|
+
this.session = createSession(this.cwd, this.model);
|
|
179
|
+
for (const m of this.messages) appendMessage(this.session, m);
|
|
180
|
+
this.h.onStats?.(this.stats());
|
|
181
|
+
} finally {
|
|
182
|
+
this.ac = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function summarize(out) {
|
|
187
|
+
const lines = String(out).split("\n").filter((l) => l.trim());
|
|
188
|
+
const shown = lines.slice(0, 3).map((l) => l.length > 100 ? l.slice(0, 99) + "\u2026" : l);
|
|
189
|
+
if (lines.length > 3) shown.push(`\u2026 +${lines.length - 3} lines`);
|
|
190
|
+
return shown.join("\n");
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
Agent
|
|
194
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { loadSkills } from "./skills.js";
|
|
5
|
+
import { loadContextFiles } from "./manixmd.js";
|
|
6
|
+
import { loadMcpConfigs, McpManager } from "./mcp.js";
|
|
7
|
+
import { gradient, t, LOGO } from "./theme.js";
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const args2 = { _: [] };
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const a = argv[i];
|
|
13
|
+
if (a === "-h" || a === "--help") args2.help = true;
|
|
14
|
+
else if (a === "-v" || a === "--version") args2.version = true;
|
|
15
|
+
else if (a === "-p" || a === "--print") args2.print = true;
|
|
16
|
+
else if (a === "-m" || a === "--model") args2.model = argv[++i];
|
|
17
|
+
else if (a === "-c" || a === "--continue") args2.resume = "last";
|
|
18
|
+
else if (a === "-r" || a === "--resume") {
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
if (next && !next.startsWith("-")) args2.resume = argv[++i];
|
|
21
|
+
else args2.resume = "pick";
|
|
22
|
+
} else if (a === "--yolo") args2.yolo = true;
|
|
23
|
+
else args2._.push(a);
|
|
24
|
+
}
|
|
25
|
+
return args2;
|
|
26
|
+
}
|
|
27
|
+
function usage() {
|
|
28
|
+
return `${gradient(`${LOGO} Manix`)} ${t.dim(`v${pkg.version} \u2014 terminal coding agent powered by OpenRouter`)}
|
|
29
|
+
|
|
30
|
+
${t.bold("Usage")}
|
|
31
|
+
manix [prompt] interactive TUI (optional starting prompt)
|
|
32
|
+
manix -p "task" print mode (no TUI); assistant text \u2192 stdout
|
|
33
|
+
echo "task" | manix pipe a prompt
|
|
34
|
+
|
|
35
|
+
${t.bold("Flags")}
|
|
36
|
+
-m, --model <id> OpenRouter model for this run
|
|
37
|
+
-c, --continue resume the most recent session
|
|
38
|
+
-r, --resume [id] pick (or load) a previous session
|
|
39
|
+
--yolo auto-approve all tools (careful!)
|
|
40
|
+
-p, --print non-interactive print mode
|
|
41
|
+
-v, --version print version
|
|
42
|
+
-h, --help this help
|
|
43
|
+
|
|
44
|
+
${t.bold("Files")}
|
|
45
|
+
config ~/.manix/config.json sessions ~/.manix/sessions/
|
|
46
|
+
MCP ~/.manix/mcp.json \xB7 .manix/mcp.json
|
|
47
|
+
skills ~/.manix/skills/<name>/SKILL.md \xB7 .manix/skills/<name>/SKILL.md
|
|
48
|
+
memory ./MANIX.md (generate with /init)`;
|
|
49
|
+
}
|
|
50
|
+
const args = parseArgs(process.argv.slice(2));
|
|
51
|
+
if (args.version) {
|
|
52
|
+
console.log(pkg.version);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
if (args.help) {
|
|
56
|
+
console.log(usage());
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
const cwd = process.cwd();
|
|
60
|
+
const config = loadConfig();
|
|
61
|
+
if (args.model) config.model = args.model;
|
|
62
|
+
let prompt = args._.join(" ").trim();
|
|
63
|
+
if (!process.stdin.isTTY && !args.print) {
|
|
64
|
+
const stdin = fs.readFileSync(0, "utf8").trim();
|
|
65
|
+
if (stdin) {
|
|
66
|
+
prompt = [prompt, stdin].filter(Boolean).join("\n\n");
|
|
67
|
+
args.print = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const skills = loadSkills(cwd);
|
|
71
|
+
const contextFiles = loadContextFiles(cwd);
|
|
72
|
+
const mcp = new McpManager();
|
|
73
|
+
const mcpConfigs = loadMcpConfigs(cwd);
|
|
74
|
+
if (args.print) {
|
|
75
|
+
if (!config.apiKey) {
|
|
76
|
+
console.error("No API key. Run `manix` once interactively, or set OPENROUTER_API_KEY.");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
if (!prompt) {
|
|
80
|
+
console.error('No prompt. Usage: manix -p "task"');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
mcp.start(mcpConfigs);
|
|
84
|
+
if (Object.keys(mcpConfigs).length) await waitForMcp(mcp, 5e3);
|
|
85
|
+
const { runPrint } = await import("./print.js");
|
|
86
|
+
await runPrint({ config, cwd, prompt, yolo: !!args.yolo, mcp, skills, contextFiles });
|
|
87
|
+
await mcp.closeAll();
|
|
88
|
+
process.exit(process.exitCode || 0);
|
|
89
|
+
}
|
|
90
|
+
if (!process.stdout.isTTY) {
|
|
91
|
+
console.error('Interactive mode needs a TTY. Use -p "task" for print mode.');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
mcp.start(mcpConfigs);
|
|
95
|
+
const { render } = await import("ink");
|
|
96
|
+
const { default: App } = await import("./ui/App.js");
|
|
97
|
+
const instance = render(
|
|
98
|
+
/* @__PURE__ */ jsx(
|
|
99
|
+
App,
|
|
100
|
+
{
|
|
101
|
+
config,
|
|
102
|
+
cwd,
|
|
103
|
+
version: pkg.version,
|
|
104
|
+
mcp,
|
|
105
|
+
skills,
|
|
106
|
+
contextFiles,
|
|
107
|
+
initialPrompt: prompt || null,
|
|
108
|
+
resumeTarget: args.resume || null,
|
|
109
|
+
yolo: !!args.yolo
|
|
110
|
+
}
|
|
111
|
+
),
|
|
112
|
+
{ exitOnCtrlC: false }
|
|
113
|
+
);
|
|
114
|
+
await instance.waitUntilExit();
|
|
115
|
+
await mcp.closeAll();
|
|
116
|
+
process.exit(0);
|
|
117
|
+
async function waitForMcp(manager, timeoutMs) {
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
while (Date.now() - start < timeoutMs) {
|
|
120
|
+
if (manager.status().every((s) => s.status !== "connecting")) return;
|
|
121
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MANIX_DIR = path.join(os.homedir(), ".manix");
|
|
5
|
+
const CONFIG_FILE = path.join(MANIX_DIR, "config.json");
|
|
6
|
+
const DEFAULT_MODEL = "anthropic/claude-sonnet-4.5";
|
|
7
|
+
function ensureDir(dir = MANIX_DIR) {
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
function readJson(file) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function loadConfig() {
|
|
18
|
+
const cfg = readJson(CONFIG_FILE);
|
|
19
|
+
return {
|
|
20
|
+
model: DEFAULT_MODEL,
|
|
21
|
+
...cfg,
|
|
22
|
+
apiKey: process.env.OPENROUTER_API_KEY || cfg.apiKey || null
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function saveConfig(patch) {
|
|
26
|
+
ensureDir();
|
|
27
|
+
const next = { ...readJson(CONFIG_FILE), ...patch };
|
|
28
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
CONFIG_FILE,
|
|
33
|
+
DEFAULT_MODEL,
|
|
34
|
+
MANIX_DIR,
|
|
35
|
+
ensureDir,
|
|
36
|
+
loadConfig,
|
|
37
|
+
saveConfig
|
|
38
|
+
};
|
package/dist/manixmd.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MANIX_DIR } from "./config.js";
|
|
4
|
+
function loadContextFiles(cwd) {
|
|
5
|
+
const candidates = [path.join(MANIX_DIR, "MANIX.md"), path.join(cwd, "MANIX.md")];
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const p of candidates) {
|
|
8
|
+
try {
|
|
9
|
+
const content = fs.readFileSync(p, "utf8").trim();
|
|
10
|
+
if (content) out.push({ path: p, content: content.slice(0, 2e4) });
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
loadContextFiles
|
|
18
|
+
};
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { t } from "./theme.js";
|
|
2
|
+
function renderMarkdown(md) {
|
|
3
|
+
const out = [];
|
|
4
|
+
let inCode = false;
|
|
5
|
+
for (const line of String(md).split("\n")) {
|
|
6
|
+
const fence = line.match(/^\s*```(\S*)/);
|
|
7
|
+
if (fence) {
|
|
8
|
+
inCode = !inCode;
|
|
9
|
+
out.push(inCode ? t.faint("\u250C\u2574" + (fence[1] || "code")) : t.faint("\u2514\u2574"));
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (inCode) {
|
|
13
|
+
out.push(t.faint("\u2502 ") + t.code(line));
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
out.push(renderLine(line));
|
|
17
|
+
}
|
|
18
|
+
if (inCode) out.push(t.faint("\u2514\u2574"));
|
|
19
|
+
return out.join("\n");
|
|
20
|
+
}
|
|
21
|
+
function renderLine(line) {
|
|
22
|
+
let m;
|
|
23
|
+
if (m = line.match(/^#{1,6}\s+(.*)$/)) return t.accent.bold(inline(m[1]));
|
|
24
|
+
if (m = line.match(/^(\s*)[-*]\s+(.*)$/)) return m[1] + t.accent("\u2022") + " " + inline(m[2]);
|
|
25
|
+
if (m = line.match(/^(\s*)(\d+)\.\s+(.*)$/)) return m[1] + t.accent(m[2] + ".") + " " + inline(m[3]);
|
|
26
|
+
if (/^\s*>\s?/.test(line)) return t.dim("\u2502 " + line.replace(/^\s*>\s?/, ""));
|
|
27
|
+
if (/^\s*([-*_])\s?(\1\s?){2,}$/.test(line)) return t.faint("\u2500".repeat(40));
|
|
28
|
+
return inline(line);
|
|
29
|
+
}
|
|
30
|
+
function inline(s) {
|
|
31
|
+
return s.replace(/`([^`]+)`/g, (_, c) => t.code(c)).replace(/\*\*([^*]+)\*\*/g, (_, b) => t.bold(b)).replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, txt, url) => txt + " " + t.faint(`(${url})`));
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
renderMarkdown
|
|
35
|
+
};
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import { MANIX_DIR } from "./config.js";
|
|
6
|
+
function loadMcpConfigs(cwd) {
|
|
7
|
+
const files = [path.join(MANIX_DIR, "mcp.json"), path.join(cwd, ".manix", "mcp.json")];
|
|
8
|
+
const servers = {};
|
|
9
|
+
for (const f of files) {
|
|
10
|
+
try {
|
|
11
|
+
Object.assign(servers, JSON.parse(fs.readFileSync(f, "utf8")).mcpServers || {});
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return servers;
|
|
16
|
+
}
|
|
17
|
+
function withTimeout(promise, ms, msg) {
|
|
18
|
+
let timer;
|
|
19
|
+
const timeout = new Promise((_, reject) => {
|
|
20
|
+
timer = setTimeout(() => reject(new Error(msg)), ms);
|
|
21
|
+
timer.unref?.();
|
|
22
|
+
});
|
|
23
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
24
|
+
}
|
|
25
|
+
class McpManager {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.servers = /* @__PURE__ */ new Map();
|
|
28
|
+
}
|
|
29
|
+
/** Connect all configured servers in the background; onUpdate fires per status change. */
|
|
30
|
+
start(configs, onUpdate) {
|
|
31
|
+
for (const [name, cfg] of Object.entries(configs)) {
|
|
32
|
+
const entry = { name, status: "connecting", tools: [], client: null, error: null };
|
|
33
|
+
this.servers.set(name, entry);
|
|
34
|
+
this.connect(entry, cfg).then(() => onUpdate?.(entry)).catch((err) => {
|
|
35
|
+
entry.status = "error";
|
|
36
|
+
entry.error = err.message;
|
|
37
|
+
onUpdate?.(entry);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async connect(entry, cfg) {
|
|
42
|
+
const client = new Client({ name: "manix", version: "0.1.0" });
|
|
43
|
+
const transport = new StdioClientTransport({
|
|
44
|
+
command: cfg.command,
|
|
45
|
+
args: cfg.args || [],
|
|
46
|
+
env: { ...process.env, ...cfg.env || {} },
|
|
47
|
+
stderr: "ignore"
|
|
48
|
+
});
|
|
49
|
+
await withTimeout(client.connect(transport), 15e3, `MCP "${entry.name}": connect timed out`);
|
|
50
|
+
const { tools } = await withTimeout(client.listTools(), 15e3, `MCP "${entry.name}": listTools timed out`);
|
|
51
|
+
entry.client = client;
|
|
52
|
+
entry.tools = tools || [];
|
|
53
|
+
entry.status = "ready";
|
|
54
|
+
}
|
|
55
|
+
/** All ready servers' tools in Manix tool shape, namespaced mcp__server__tool. */
|
|
56
|
+
getTools() {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const s of this.servers.values()) {
|
|
59
|
+
if (s.status !== "ready") continue;
|
|
60
|
+
for (const tl of s.tools) {
|
|
61
|
+
out.push({
|
|
62
|
+
name: `mcp__${s.name}__${tl.name}`,
|
|
63
|
+
safe: false,
|
|
64
|
+
// external code — always gate behind permissions
|
|
65
|
+
description: (tl.description || `${tl.name} from MCP server ${s.name}`).slice(0, 1024),
|
|
66
|
+
parameters: tl.inputSchema || { type: "object", properties: {} },
|
|
67
|
+
describe: (a) => `${s.name}:${tl.name}(${JSON.stringify(a).slice(0, 48)})`,
|
|
68
|
+
preview: (a) => JSON.stringify(a, null, 2).split("\n").slice(0, 10),
|
|
69
|
+
run: async (a) => {
|
|
70
|
+
const res = await s.client.callTool({ name: tl.name, arguments: a });
|
|
71
|
+
const text = (res.content || []).map((c) => c.type === "text" ? c.text : `[${c.type} content]`).join("\n");
|
|
72
|
+
if (res.isError) throw new Error(text || "MCP tool returned an error");
|
|
73
|
+
return text || "(no output)";
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
status() {
|
|
81
|
+
return [...this.servers.values()].map((s) => ({
|
|
82
|
+
name: s.name,
|
|
83
|
+
status: s.status,
|
|
84
|
+
tools: s.tools.length,
|
|
85
|
+
error: s.error
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
async closeAll() {
|
|
89
|
+
for (const s of this.servers.values()) {
|
|
90
|
+
try {
|
|
91
|
+
await s.client?.close();
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
McpManager,
|
|
99
|
+
loadMcpConfigs
|
|
100
|
+
};
|