@noobdemon/noob-cli 1.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/LICENSE +21 -0
- package/README.md +137 -0
- package/bin/noob.js +84 -0
- package/package.json +44 -0
- package/src/agent.js +116 -0
- package/src/api.js +106 -0
- package/src/config.js +61 -0
- package/src/i18n.js +82 -0
- package/src/models.js +58 -0
- package/src/repl.js +420 -0
- package/src/tools.js +183 -0
- package/src/ui.js +115 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yukkis3nse1
|
|
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,137 @@
|
|
|
1
|
+
# noob — agentic coding CLI
|
|
2
|
+
|
|
3
|
+
A Claude-Code-style coding agent that lives in your terminal, powered by
|
|
4
|
+
**Noob Demon** (34 models across 8 providers — GPT, Claude, Gemini, DeepSeek,
|
|
5
|
+
Grok, Qwen, Kimi, Llama).
|
|
6
|
+
|
|
7
|
+
It reads and edits files, runs shell commands, and iterates on a task — asking
|
|
8
|
+
permission before anything destructive — all inside a polished terminal UI.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
███╗ ██╗ ██████╗ ██████╗ ██████╗
|
|
12
|
+
████╗ ██║██╔═══██╗██╔═══██╗██╔══██╗
|
|
13
|
+
██╔██╗ ██║██║ ██║██║ ██║██████╔╝
|
|
14
|
+
██║╚██╗██║██║ ██║██║ ██║██╔══██╗
|
|
15
|
+
██║ ╚████║╚██████╔╝╚██████╔╝██████╔╝
|
|
16
|
+
╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd "noob cli"
|
|
23
|
+
npm install
|
|
24
|
+
npm link # optional: makes `noob` available globally
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Node.js ≥ 18 (tested on 22).
|
|
28
|
+
|
|
29
|
+
## Xác thực & gói cước (Authentication)
|
|
30
|
+
|
|
31
|
+
noob đi qua một **gateway** (Cloudflare Worker `claude-code-proxy`) để ẩn backend
|
|
32
|
+
thật và quản lý API key qua Supabase. Bạn cần một API key để dùng:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
noob login nk_xxx_xxxxxxxx # đăng nhập, key lưu ở ~/.noob/config.json
|
|
36
|
+
noob usage # xem hạn mức còn lại
|
|
37
|
+
noob logout # đăng xuất
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Trong phiên: `/login <key>`, `/usage`, `/logout`.
|
|
41
|
+
|
|
42
|
+
**Các gói:**
|
|
43
|
+
|
|
44
|
+
| Gói | Hạn mức |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `pro` | 5 000 request / 5 giờ (cửa sổ trượt) |
|
|
47
|
+
| `proplus` | 10 000 request / 5 giờ |
|
|
48
|
+
| `admin` | không giới hạn |
|
|
49
|
+
| `trial` | 200 request dùng thử, hết là key **dead** |
|
|
50
|
+
|
|
51
|
+
> Mỗi lệnh gọi mô hình (kể cả từng bước tool trong một tác vụ) tính là 1 request.
|
|
52
|
+
|
|
53
|
+
Cấp/huỷ key (admin) — trong repo gateway `D:\Dev\free claude code`:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
node scripts/admin.mjs create pro "khách A"
|
|
57
|
+
node scripts/admin.mjs list
|
|
58
|
+
node scripts/admin.mjs revoke nk_pro_xxx
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Use
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
noob # interactive session
|
|
65
|
+
noob "add input validation to api.js" # start with a request
|
|
66
|
+
noob -m gateway-claude-opus-4-7 # pick a model
|
|
67
|
+
noob --yolo # auto-approve edits & commands
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Without `npm link`, run `node bin/noob.js …`.
|
|
71
|
+
|
|
72
|
+
### In-session commands
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `/model [name]` | switch model (fuzzy match), or list all |
|
|
77
|
+
| `/models` | list every model grouped by provider |
|
|
78
|
+
| `/merge` | toggle **Merge AI** — synthesizes GPT-5.5 + Claude + Gemini via o3 |
|
|
79
|
+
| `/search` | toggle **web search** mode |
|
|
80
|
+
| `/chat` | back to normal chat mode |
|
|
81
|
+
| `/yolo` | toggle auto-approve for file/command tools (or **Shift+Tab**) |
|
|
82
|
+
| `/clear` `/new` | wipe conversation context |
|
|
83
|
+
| `/status` | show current model + working directory |
|
|
84
|
+
| `/exit` | quit (Ctrl+C once = stop turn, twice = quit) |
|
|
85
|
+
|
|
86
|
+
## How it works
|
|
87
|
+
|
|
88
|
+
The Noob Demon gateway is a **stateless single-message** endpoint with no native
|
|
89
|
+
function-calling. noob layers an agent on top:
|
|
90
|
+
|
|
91
|
+
1. The full transcript (system prompt + history + tool results) is serialized into
|
|
92
|
+
one `message` and streamed to the gateway's `/api/chat`.
|
|
93
|
+
2. The model replies with either a final answer or a single \`\`\`tool JSON block.
|
|
94
|
+
3. noob parses the tool call, asks permission if it's destructive, executes it, and
|
|
95
|
+
feeds the result back — looping until the model answers without a tool block.
|
|
96
|
+
|
|
97
|
+
### Tools the agent can call
|
|
98
|
+
|
|
99
|
+
`read_file` · `write_file` · `edit_file` · `list_dir` · `glob` · `grep` · `run_command`
|
|
100
|
+
|
|
101
|
+
Destructive tools (`write_file`, `edit_file`, `run_command`) prompt for approval
|
|
102
|
+
(`y` / `n` / `a`lways) unless `/yolo` is on.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
| Env var | Effect |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `NOOB_API_BASE` | override the gateway URL |
|
|
109
|
+
| `NOOB_API_KEY` | API key (overrides `~/.noob/config.json`) |
|
|
110
|
+
| `NOOB_INSECURE_TLS=1` | disable TLS verification — **last resort** for machines behind a TLS-intercepting proxy. Prefer adding your proxy CA to the trust store. |
|
|
111
|
+
|
|
112
|
+
## Model compatibility
|
|
113
|
+
|
|
114
|
+
The agent drives tools through a text protocol (the proxy has no native
|
|
115
|
+
function-calling). Models differ in how willingly they follow it:
|
|
116
|
+
|
|
117
|
+
| Provider | Agentic tools | Notes |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| **Anthropic** (Claude) | ✅ best | default — `gateway-claude-opus-4-7` |
|
|
120
|
+
| **DeepSeek** | ✅ works | good free alternative |
|
|
121
|
+
| OpenAI (GPT/o-series) | ⚠️ often refuses | replies "I can't access your filesystem" |
|
|
122
|
+
| Google (Gemini) | ⚠️ often refuses | same |
|
|
123
|
+
|
|
124
|
+
Stick with Claude or DeepSeek for file edits & commands. Any model is fine for
|
|
125
|
+
plain chat, `/merge`, and `/search`.
|
|
126
|
+
|
|
127
|
+
## Notes & limits
|
|
128
|
+
|
|
129
|
+
- Free shared proxy: rate limits and occasional hiccups are expected.
|
|
130
|
+
- Context is sent in full each turn, so very long sessions cost more tokens — use
|
|
131
|
+
`/clear` to reset.
|
|
132
|
+
- This is an unofficial tool built against a public community endpoint; it is not
|
|
133
|
+
affiliated with Anthropic or the model providers.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/bin/noob.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startRepl } from "../src/repl.js";
|
|
3
|
+
import { config } from "../src/config.js";
|
|
4
|
+
import { usage, ApiError } from "../src/api.js";
|
|
5
|
+
import { c } from "../src/ui.js";
|
|
6
|
+
import { t } from "../src/i18n.js";
|
|
7
|
+
|
|
8
|
+
const argv = process.argv.slice(2);
|
|
9
|
+
const opts = { yolo: false, model: undefined, prompt: undefined };
|
|
10
|
+
const positional = [];
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (a === "--yolo" || a === "-y") opts.yolo = true;
|
|
15
|
+
else if (a === "--insecure-tls") process.env.NOOB_INSECURE_TLS = "1";
|
|
16
|
+
else if (a === "--model" || a === "-m") opts.model = argv[++i];
|
|
17
|
+
else if (a === "--help" || a === "-h") {
|
|
18
|
+
printHelp();
|
|
19
|
+
process.exit(0);
|
|
20
|
+
} else positional.push(a);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sub = positional[0];
|
|
24
|
+
|
|
25
|
+
// ── subcommands ──────────────────────────────────────────────────────────
|
|
26
|
+
if (sub === "login") {
|
|
27
|
+
const key = positional[1];
|
|
28
|
+
if (!key) {
|
|
29
|
+
console.log(c.err(t.needKeyArg));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
config.setKey(key);
|
|
33
|
+
console.log(c.ok("✓ ") + t.loginSaved(config.path));
|
|
34
|
+
try {
|
|
35
|
+
const u = await usage();
|
|
36
|
+
if (u.ok) console.log(c.ok(t.loginOk({ pro: "Pro", proplus: "Pro+", admin: "Admin", trial: "Trial" }[u.plan] || u.plan)));
|
|
37
|
+
else console.log(c.err("✗ " + t.errInvalidKey));
|
|
38
|
+
} catch {
|
|
39
|
+
/* network — key still saved */
|
|
40
|
+
}
|
|
41
|
+
process.exit(0);
|
|
42
|
+
} else if (sub === "logout") {
|
|
43
|
+
config.clearKey();
|
|
44
|
+
console.log(c.ok(t.loggedOut));
|
|
45
|
+
process.exit(0);
|
|
46
|
+
} else if (sub === "usage") {
|
|
47
|
+
if (!config.apiKey) {
|
|
48
|
+
console.log(c.tool(t.notLoggedIn));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const u = await usage();
|
|
53
|
+
console.log(JSON.stringify(u, null, 2));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.log(c.err("✗ " + (err.message || t.errConn)));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (positional.length) opts.prompt = positional.join(" ");
|
|
62
|
+
|
|
63
|
+
startRepl(opts).catch((err) => {
|
|
64
|
+
console.error(c.err("lỗi nghiêm trọng: " + (err?.stack || err?.message || err)));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function printHelp() {
|
|
69
|
+
console.log(`noob — trợ lý lập trình CLI · Noob Demon
|
|
70
|
+
|
|
71
|
+
Cách dùng:
|
|
72
|
+
noob [tuỳ chọn] ["yêu cầu ban đầu"]
|
|
73
|
+
noob login <api-key> đăng nhập bằng API key
|
|
74
|
+
noob logout đăng xuất
|
|
75
|
+
noob usage xem hạn mức key
|
|
76
|
+
|
|
77
|
+
Tuỳ chọn:
|
|
78
|
+
-m, --model <id> chọn mô hình (vd: gateway-claude-opus-4-7)
|
|
79
|
+
-y, --yolo tự động duyệt sửa file & chạy lệnh (cẩn thận)
|
|
80
|
+
--insecure-tls tắt kiểm tra TLS (chỉ cho mạng có proxy chặn TLS)
|
|
81
|
+
-h, --help hiện trợ giúp
|
|
82
|
+
|
|
83
|
+
Trong phiên: gõ /help để xem mọi lệnh.`);
|
|
84
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noobdemon/noob-cli",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Trợ lý lập trình agentic trong terminal (kiểu Claude Code), tiếng Việt, dùng sức mạnh Noob Demon — 34 mô hình AI.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"noob": "bin/noob.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node bin/noob.js"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"cli",
|
|
26
|
+
"ai",
|
|
27
|
+
"coding-agent",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"gemini-cli",
|
|
30
|
+
"agent",
|
|
31
|
+
"tui",
|
|
32
|
+
"llm",
|
|
33
|
+
"vietnamese"
|
|
34
|
+
],
|
|
35
|
+
"author": "yukkis3nse1",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"boxen": "^8.0.1",
|
|
39
|
+
"chalk": "^5.4.1",
|
|
40
|
+
"cli-highlight": "^2.1.11",
|
|
41
|
+
"gradient-string": "^3.0.0",
|
|
42
|
+
"ora": "^8.2.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { stream } from "./api.js";
|
|
2
|
+
import { t } from "./i18n.js";
|
|
3
|
+
|
|
4
|
+
export const SYSTEM = `You are noob, an agentic coding assistant in the spirit of Claude Code. You help with software engineering tasks by reading and editing files and running commands in the user's current working directory.
|
|
5
|
+
|
|
6
|
+
You do NOT access anything yourself. Instead, a local runtime executes tools on your behalf: you emit a tool-call JSON block, the runtime runs it on the user's machine and replies with the result. This is fully supported — never claim you "can't access the terminal/filesystem". Just emit the tool call.
|
|
7
|
+
|
|
8
|
+
# Tools
|
|
9
|
+
To call a tool, emit EXACTLY ONE fenced code block tagged \`tool\` containing a single JSON object, and nothing after it:
|
|
10
|
+
|
|
11
|
+
\`\`\`tool
|
|
12
|
+
{"name": "<tool>", "input": { ... }}
|
|
13
|
+
\`\`\`
|
|
14
|
+
|
|
15
|
+
Then STOP and wait — the runtime executes the tool and replies with a TOOL RESULT. Use one tool per step. When the task is complete (or you are only answering a question), reply normally in Markdown with NO tool block.
|
|
16
|
+
|
|
17
|
+
Available tools:
|
|
18
|
+
- read_file {"path": str, "offset"?: int, "limit"?: int} — read a file (1-indexed lines)
|
|
19
|
+
- write_file {"path": str, "content": str} — create/overwrite a file
|
|
20
|
+
- edit_file {"path": str, "old_string": str, "new_string": str, "replace_all"?: bool} — exact string replace; old_string must be unique unless replace_all
|
|
21
|
+
- list_dir {"path"?: str} — list a directory
|
|
22
|
+
- glob {"pattern": str} — find files by glob (supports ** and *)
|
|
23
|
+
- grep {"pattern": str, "path"?: str, "glob"?: str} — regex search file contents
|
|
24
|
+
- run_command {"command": str} — run a shell command in the cwd
|
|
25
|
+
|
|
26
|
+
# Rules
|
|
27
|
+
- Investigate before editing: read the relevant files first; never invent file contents.
|
|
28
|
+
- Make the smallest change that fully solves the task. Match the surrounding code style.
|
|
29
|
+
- Prefer edit_file over write_file for existing files.
|
|
30
|
+
- After changes, verify when sensible (run a build/test/lint command).
|
|
31
|
+
- Keep prose tight. Explain what you did and why, not how to use a tool.
|
|
32
|
+
- JSON in the tool block must be valid: escape newlines as \\n inside string values.
|
|
33
|
+
- LANGUAGE: Always write your prose answers to the user in Vietnamese (tiếng Việt), unless the user explicitly writes in another language. Keep code, file paths, commands, and tool JSON unchanged.
|
|
34
|
+
|
|
35
|
+
# Example interaction
|
|
36
|
+
## USER
|
|
37
|
+
how many lines are in app.js?
|
|
38
|
+
## ASSISTANT
|
|
39
|
+
\`\`\`tool
|
|
40
|
+
{"name": "run_command", "input": {"command": "wc -l app.js"}}
|
|
41
|
+
\`\`\`
|
|
42
|
+
## TOOL RESULT (run_command)
|
|
43
|
+
42 app.js
|
|
44
|
+
[exit code 0]
|
|
45
|
+
## ASSISTANT
|
|
46
|
+
app.js has 42 lines.
|
|
47
|
+
|
|
48
|
+
Follow this pattern exactly. Your very first response to a task that needs the filesystem MUST be a tool block — do not refuse or explain limitations.`;
|
|
49
|
+
|
|
50
|
+
const MAX_STEPS = 30;
|
|
51
|
+
|
|
52
|
+
// The proxy is stateless, so we serialize the whole transcript into one prompt.
|
|
53
|
+
function buildPrompt(history) {
|
|
54
|
+
const parts = [SYSTEM, "", "=".repeat(60), "# CONVERSATION", ""];
|
|
55
|
+
for (const m of history) {
|
|
56
|
+
if (m.role === "user") parts.push(`## USER\n${m.content}`);
|
|
57
|
+
else if (m.role === "assistant") parts.push(`## ASSISTANT\n${m.content}`);
|
|
58
|
+
else if (m.role === "tool") parts.push(`## TOOL RESULT (${m.name})\n${m.content}`);
|
|
59
|
+
parts.push("");
|
|
60
|
+
}
|
|
61
|
+
parts.push("=".repeat(60));
|
|
62
|
+
parts.push("Continue. Emit a tool block to act, or reply in Markdown if done.");
|
|
63
|
+
return parts.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Extract a single tool call from an assistant message, if present.
|
|
67
|
+
export function parseToolCall(text) {
|
|
68
|
+
// Preferred: ```tool { ... } ```
|
|
69
|
+
let m = text.match(/```tool\s*\n([\s\S]*?)```/);
|
|
70
|
+
// Fallback: a ```json block that contains a "name" field.
|
|
71
|
+
if (!m) {
|
|
72
|
+
const j = text.match(/```json\s*\n([\s\S]*?)```/);
|
|
73
|
+
if (j && /"name"\s*:/.test(j[1])) m = j;
|
|
74
|
+
}
|
|
75
|
+
if (!m) return null;
|
|
76
|
+
try {
|
|
77
|
+
const obj = JSON.parse(m[1].trim());
|
|
78
|
+
if (obj && typeof obj.name === "string") return { name: obj.name, input: obj.input || {} };
|
|
79
|
+
} catch {
|
|
80
|
+
/* malformed — treat as prose */
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run one agent turn (which may span several tool steps).
|
|
87
|
+
*
|
|
88
|
+
* @param {object} opts
|
|
89
|
+
* @param {Array} opts.history mutated in place with assistant/tool msgs
|
|
90
|
+
* @param {string} opts.model
|
|
91
|
+
* @param {AbortSignal} opts.signal
|
|
92
|
+
* @param {(name:string,input:object)=>Promise<{allow:boolean,result?:string}>} opts.onTool
|
|
93
|
+
* Called for every tool call. Should handle permission + execution and
|
|
94
|
+
* return the textual result (or {allow:false} to feed a denial back).
|
|
95
|
+
* @param {(msg:string)=>void} opts.onStatus thinking/streaming status
|
|
96
|
+
* @returns {Promise<string>} the final assistant answer (no tool block)
|
|
97
|
+
*/
|
|
98
|
+
export async function runAgent({ history, model, signal, onTool, onStatus }) {
|
|
99
|
+
for (let step = 0; step < MAX_STEPS; step++) {
|
|
100
|
+
const prompt = buildPrompt(history);
|
|
101
|
+
onStatus?.("thinking");
|
|
102
|
+
const { text } = await stream({ mode: "chat", model, message: prompt, signal });
|
|
103
|
+
history.push({ role: "assistant", content: text });
|
|
104
|
+
|
|
105
|
+
const call = parseToolCall(text);
|
|
106
|
+
if (!call) return text; // final answer
|
|
107
|
+
|
|
108
|
+
const { allow, result } = await onTool(call.name, call.input);
|
|
109
|
+
history.push({
|
|
110
|
+
role: "tool",
|
|
111
|
+
name: call.name,
|
|
112
|
+
content: allow ? result : t.toolDenied,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return t.maxSteps;
|
|
116
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Client for the noob gateway (claude-code-proxy worker). The gateway validates
|
|
2
|
+
// the API key against Supabase, enforces plan limits, and hides the real
|
|
3
|
+
// upstream. The CLI only ever sees the gateway URL + the user's key.
|
|
4
|
+
import { config } from "./config.js";
|
|
5
|
+
|
|
6
|
+
// Opt-in TLS escape hatch for machines behind a TLS-intercepting / broken-
|
|
7
|
+
// revocation proxy. Off by default. Prefer fixing the trust store.
|
|
8
|
+
if (process.env.NOOB_INSECURE_TLS === "1") {
|
|
9
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function authHeaders() {
|
|
13
|
+
const h = { "Content-Type": "application/json" };
|
|
14
|
+
if (config.apiKey) h["Authorization"] = "Bearer " + config.apiKey;
|
|
15
|
+
return h;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class ApiError extends Error {
|
|
19
|
+
constructor(message, { status, code, reset_at, plan } = {}) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "ApiError";
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.reset_at = reset_at;
|
|
25
|
+
this.plan = plan;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function parseError(resp) {
|
|
30
|
+
let j = null;
|
|
31
|
+
try {
|
|
32
|
+
j = await resp.json();
|
|
33
|
+
} catch {}
|
|
34
|
+
return new ApiError(j?.error || `HTTP ${resp.status}`, {
|
|
35
|
+
status: resp.status,
|
|
36
|
+
code: j?.code,
|
|
37
|
+
reset_at: j?.reset_at,
|
|
38
|
+
plan: j?.plan,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stream a chat/merge/search request from the gateway.
|
|
44
|
+
* @returns {Promise<{text:string, reasoning:string}>}
|
|
45
|
+
*/
|
|
46
|
+
export async function stream({ mode = "chat", message, model, signal, onDelta, onReasoning, onStatus }) {
|
|
47
|
+
const endpoint = mode === "search" ? "/api/search" : mode === "merge" ? "/api/merge" : "/api/chat";
|
|
48
|
+
const body = mode === "search" ? { query: message } : mode === "merge" ? { message } : { message, model };
|
|
49
|
+
|
|
50
|
+
const resp = await fetch(config.gatewayUrl + endpoint, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: authHeaders(),
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
signal,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!resp.ok || !resp.body) throw await parseError(resp);
|
|
58
|
+
|
|
59
|
+
const reader = resp.body.getReader();
|
|
60
|
+
const decoder = new TextDecoder();
|
|
61
|
+
let buf = "";
|
|
62
|
+
let text = "";
|
|
63
|
+
let reasoning = "";
|
|
64
|
+
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) break;
|
|
68
|
+
buf += decoder.decode(value, { stream: true });
|
|
69
|
+
let nl;
|
|
70
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
71
|
+
const line = buf.slice(0, nl).trim();
|
|
72
|
+
buf = buf.slice(nl + 1);
|
|
73
|
+
if (!line.startsWith("data:")) continue;
|
|
74
|
+
const data = line.slice(5).trim();
|
|
75
|
+
if (!data) continue;
|
|
76
|
+
let p;
|
|
77
|
+
try {
|
|
78
|
+
p = JSON.parse(data);
|
|
79
|
+
} catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (p.status && onStatus) onStatus(p.status);
|
|
83
|
+
if (p.delta) {
|
|
84
|
+
text += p.delta;
|
|
85
|
+
onDelta?.(p.delta);
|
|
86
|
+
}
|
|
87
|
+
if (p.reasoning) {
|
|
88
|
+
reasoning = p.reasoning;
|
|
89
|
+
onReasoning?.(p.reasoning);
|
|
90
|
+
if (p.answer) text = p.answer;
|
|
91
|
+
}
|
|
92
|
+
if (p.error) throw new ApiError(p.error, { status: resp.status });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { text: text.trim(), reasoning: reasoning.trim() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Fetch the current key's quota/usage from the gateway (no request consumed). */
|
|
100
|
+
export async function usage() {
|
|
101
|
+
const resp = await fetch(config.gatewayUrl + "/api/usage", { headers: authHeaders() });
|
|
102
|
+
if (!resp.ok) throw await parseError(resp);
|
|
103
|
+
return await resp.json();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { ApiError };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
// Default gateway — the Noob Demon endpoint. The real upstream is hidden behind
|
|
6
|
+
// it; the CLI only ever talks to this gateway.
|
|
7
|
+
const DEFAULT_GATEWAY = "https://claude-code-proxy.lohieuky678.workers.dev";
|
|
8
|
+
|
|
9
|
+
const DIR = path.join(os.homedir(), ".noob");
|
|
10
|
+
const FILE = path.join(DIR, "config.json");
|
|
11
|
+
|
|
12
|
+
function read() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(FILE, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function write(cfg) {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(DIR, { recursive: true });
|
|
23
|
+
fs.writeFileSync(FILE, JSON.stringify(cfg, null, 2), "utf8");
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let cache = read();
|
|
31
|
+
|
|
32
|
+
export const config = {
|
|
33
|
+
get gatewayUrl() {
|
|
34
|
+
return process.env.NOOB_API_BASE || cache.gatewayUrl || DEFAULT_GATEWAY;
|
|
35
|
+
},
|
|
36
|
+
get apiKey() {
|
|
37
|
+
return process.env.NOOB_API_KEY || cache.apiKey || "";
|
|
38
|
+
},
|
|
39
|
+
get model() {
|
|
40
|
+
return cache.model || "";
|
|
41
|
+
},
|
|
42
|
+
setKey(key) {
|
|
43
|
+
cache.apiKey = key;
|
|
44
|
+
write(cache);
|
|
45
|
+
},
|
|
46
|
+
clearKey() {
|
|
47
|
+
delete cache.apiKey;
|
|
48
|
+
write(cache);
|
|
49
|
+
},
|
|
50
|
+
setGateway(url) {
|
|
51
|
+
cache.gatewayUrl = url;
|
|
52
|
+
write(cache);
|
|
53
|
+
},
|
|
54
|
+
setModel(id) {
|
|
55
|
+
cache.model = id;
|
|
56
|
+
write(cache);
|
|
57
|
+
},
|
|
58
|
+
get path() {
|
|
59
|
+
return FILE;
|
|
60
|
+
},
|
|
61
|
+
};
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Vietnamese UI strings for noob CLI. Single language (tiếng Việt).
|
|
2
|
+
export const t = {
|
|
3
|
+
tagline: "trợ lý lập trình trong terminal · sức mạnh từ Noob Demon",
|
|
4
|
+
ready: "Nhập yêu cầu của bạn, hoặc gõ /help để xem lệnh.",
|
|
5
|
+
promptYou: "bạn ",
|
|
6
|
+
thinking: "đang suy nghĩ",
|
|
7
|
+
searching: "đang tìm trên web",
|
|
8
|
+
merging: "đang tổng hợp đa mô hình",
|
|
9
|
+
bye: "tạm biệt 👋",
|
|
10
|
+
interrupted: "đã ngắt",
|
|
11
|
+
pressAgainToExit: "nhấn Ctrl+C lần nữa để thoát",
|
|
12
|
+
running: "đang chạy…",
|
|
13
|
+
denied: "đã từ chối",
|
|
14
|
+
|
|
15
|
+
// auth
|
|
16
|
+
notLoggedIn:
|
|
17
|
+
"Bạn chưa đăng nhập. Chạy: noob login <api-key>\nChưa có key? Liên hệ admin để lấy key (Pro / Pro+ / Trial).",
|
|
18
|
+
loginOk: (plan) => `Đăng nhập thành công. Gói: ${plan}.`,
|
|
19
|
+
loginSaved: (p) => `Đã lưu API key vào ${p}`,
|
|
20
|
+
loggedOut: "Đã đăng xuất, xoá API key khỏi máy.",
|
|
21
|
+
needKeyArg: "Thiếu key. Dùng: noob login <api-key>",
|
|
22
|
+
|
|
23
|
+
// usage
|
|
24
|
+
usageTitle: "Hạn mức API key",
|
|
25
|
+
plan: "Gói",
|
|
26
|
+
status: "Trạng thái",
|
|
27
|
+
remaining: "Còn lại",
|
|
28
|
+
used: "Đã dùng",
|
|
29
|
+
unlimited: "không giới hạn",
|
|
30
|
+
resetAt: "Đặt lại lúc",
|
|
31
|
+
trialLeft: (n) => `${n} lượt dùng thử còn lại`,
|
|
32
|
+
windowInfo: (used, limit) => `${used}/${limit} trong cửa sổ 5 giờ`,
|
|
33
|
+
|
|
34
|
+
// errors (gateway codes → VN)
|
|
35
|
+
errMissingKey: "Thiếu API key. Chạy: noob login <key>",
|
|
36
|
+
errInvalidKey: "API key không hợp lệ.",
|
|
37
|
+
errKeyDead: "API key đã bị vô hiệu hoá (dead).",
|
|
38
|
+
errTrialExhausted: "Key dùng thử đã hết 200 lượt — key đã dead. Liên hệ admin để nâng cấp.",
|
|
39
|
+
errDisabled: "API key đã bị khoá.",
|
|
40
|
+
errRateLimited: (reset) =>
|
|
41
|
+
`Đã hết hạn mức trong cửa sổ 5 giờ.${reset ? " Đặt lại lúc " + reset + "." : " Thử lại sau."}`,
|
|
42
|
+
errConn: "Lỗi kết nối tới máy chủ.",
|
|
43
|
+
|
|
44
|
+
// help
|
|
45
|
+
helpTitle: "noob · trợ giúp",
|
|
46
|
+
helpCommands: "Lệnh",
|
|
47
|
+
helpTips: "Mẹo",
|
|
48
|
+
cmdModel: "/model [tên] đổi mô hình, hoặc liệt kê tất cả",
|
|
49
|
+
cmdModels: "/models liệt kê mọi mô hình",
|
|
50
|
+
cmdMerge: "/merge bật/tắt Merge AI (tổng hợp đa mô hình)",
|
|
51
|
+
cmdSearch: "/search bật/tắt chế độ tìm web",
|
|
52
|
+
cmdChat: "/chat quay lại chế độ chat thường",
|
|
53
|
+
cmdYolo: "/yolo bật/tắt tự duyệt (hoặc nhấn Shift+Tab)",
|
|
54
|
+
cmdClear: "/clear /new xoá ngữ cảnh hội thoại",
|
|
55
|
+
cmdLogin: "/login <key> đăng nhập bằng API key",
|
|
56
|
+
cmdLogout: "/logout đăng xuất",
|
|
57
|
+
cmdUsage: "/usage xem hạn mức key còn lại",
|
|
58
|
+
cmdStatus: "/status xem mô hình + thư mục hiện tại",
|
|
59
|
+
cmdExit: "/exit /quit thoát",
|
|
60
|
+
tip1: "• Mô tả việc cần làm; noob sẽ đọc/sửa file & chạy lệnh giúp bạn.",
|
|
61
|
+
tip2: "• Thao tác nguy hiểm sẽ hỏi phép, trừ khi bật yolo (Shift+Tab).",
|
|
62
|
+
tip3: "• Ctrl+C 1 lần = dừng lượt hiện tại, 2 lần = thoát. CLI không tự tắt sau khi xong.",
|
|
63
|
+
|
|
64
|
+
// misc
|
|
65
|
+
yoloOn: "⚠ yolo BẬT — tự động duyệt mọi thao tác sửa file & chạy lệnh",
|
|
66
|
+
yoloOff: "✓ yolo TẮT — sẽ hỏi trước khi sửa file & chạy lệnh",
|
|
67
|
+
mergeOn: "Merge AI: BẬT",
|
|
68
|
+
mergeOff: "Merge AI: TẮT",
|
|
69
|
+
searchOn: "Tìm web: BẬT",
|
|
70
|
+
searchOff: "Tìm web: TẮT",
|
|
71
|
+
backToChat: "quay lại chế độ chat",
|
|
72
|
+
ctxCleared: "đã xoá ngữ cảnh",
|
|
73
|
+
unknownCmd: (c) => `không rõ lệnh: /${c}`,
|
|
74
|
+
tryHelp: "(gõ /help)",
|
|
75
|
+
noModelMatch: (q) => `không có mô hình khớp "${q}"`,
|
|
76
|
+
modelListHint: "/model <tên> để chuyển",
|
|
77
|
+
switchTo: "→ đã chuyển",
|
|
78
|
+
providerRefuses: (p) =>
|
|
79
|
+
`lưu ý: mô hình ${p} trên gateway này hay từ chối giao thức tool; nên dùng Anthropic/DeepSeek cho tác vụ sửa code.`,
|
|
80
|
+
maxSteps: "_(đã dừng: chạm giới hạn số bước tool)_",
|
|
81
|
+
toolDenied: "Người dùng từ chối thao tác này. Hãy đổi cách làm hoặc hỏi lại.",
|
|
82
|
+
};
|