glmcode 0.1.2 → 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/CHANGELOG.md +8 -0
- package/README.md +14 -27
- package/glmcode-0.1.4.tgz +0 -0
- package/glmcode-0.1.5.tgz +0 -0
- package/package.json +7 -23
- package/src/agent.js +41 -63
- package/src/auth.js +40 -0
- package/src/cli.js +70 -148
- package/src/config.js +13 -59
- package/src/mcp.js +12 -70
- package/src/platform/browser.js +30 -0
- package/src/platform/shell.js +64 -0
- package/src/skills.js +13 -33
- package/src/tools.js +13 -85
- package/LICENSE +0 -21
- package/glmcode.example.json +0 -19
- package/src/provider.js +0 -54
- package/src/utils.js +0 -15
- package/templates/.glmcode/plugins/example-plugin.js +0 -2
- package/templates/.glmcode/skills/code-review/SKILL.md +0 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.4
|
|
4
|
+
- Cross-platform OS and shell detection.
|
|
5
|
+
- Safe browser launching for Termux, Linux, macOS and Windows.
|
|
6
|
+
- Headless Linux/Alpine environments no longer crash when no browser launcher exists.
|
|
7
|
+
- Added `glmcode auth login --no-browser`.
|
|
8
|
+
- Added shell inspection commands.
|
package/README.md
CHANGED
|
@@ -1,42 +1,29 @@
|
|
|
1
|
-
# GLMCode
|
|
1
|
+
# GLMCode 0.1.4
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Install
|
|
3
|
+
Cross-platform GLM CLI for Termux, Linux/Alpine, macOS and Windows.
|
|
6
4
|
|
|
5
|
+
Install:
|
|
7
6
|
```bash
|
|
8
7
|
npm install -g glmcode
|
|
9
8
|
```
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
Run:
|
|
13
11
|
```bash
|
|
14
|
-
glmcode auth login
|
|
15
12
|
glmcode
|
|
16
13
|
```
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
Aliases: `glm`, `gc`
|
|
19
16
|
|
|
17
|
+
Authentication:
|
|
20
18
|
```bash
|
|
21
|
-
|
|
19
|
+
glmcode auth login
|
|
20
|
+
glmcode auth login --no-browser
|
|
22
21
|
```
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- API-key login/logout stored under `~/.glmcode`
|
|
30
|
-
- Project instructions from `AGENTS.md`, `CLAUDE.md`, `.glmcode/INSTRUCTIONS.md`
|
|
31
|
-
- Skills discovery from `.glmcode/skills/**/SKILL.md` and `~/.glmcode/skills/**/SKILL.md`
|
|
32
|
-
- File read/write/list/search tools
|
|
33
|
-
- Shell execution with `ask`, `auto`, or `deny` approval modes
|
|
34
|
-
- Session persistence
|
|
35
|
-
- Plugin discovery foundation
|
|
36
|
-
- MCP server configuration and stdio launcher foundation
|
|
37
|
-
|
|
38
|
-
## Project config
|
|
39
|
-
|
|
40
|
-
Create `glmcode.json` in a project directory. Start from `glmcode.example.json`.
|
|
23
|
+
Shell inspection:
|
|
24
|
+
```bash
|
|
25
|
+
glmcode shell
|
|
26
|
+
glmcode shell list
|
|
27
|
+
```
|
|
41
28
|
|
|
42
|
-
|
|
29
|
+
Headless environments print the login URL instead of crashing when no browser launcher exists.
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glmcode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "GLM
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Cross-platform GLM coding agent CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"glmcode": "src/cli.js"
|
|
7
|
+
"glmcode": "src/cli.js",
|
|
8
|
+
"glm": "src/cli.js",
|
|
9
|
+
"gc": "src/cli.js"
|
|
8
10
|
},
|
|
9
|
-
"engines": {
|
|
10
|
-
"node": ">=20"
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
"src",
|
|
14
|
-
"templates",
|
|
15
|
-
"README.md",
|
|
16
|
-
"LICENSE",
|
|
17
|
-
"glmcode.example.json"
|
|
18
|
-
],
|
|
19
11
|
"scripts": {
|
|
20
12
|
"start": "node src/cli.js",
|
|
21
|
-
"check": "node --check src/cli.js"
|
|
13
|
+
"check": "node --check src/cli.js && node --check src/auth.js && node --check src/platform/shell.js && node --check src/platform/browser.js"
|
|
22
14
|
},
|
|
23
|
-
"
|
|
24
|
-
"glm",
|
|
25
|
-
"zhipu",
|
|
26
|
-
"coding-agent",
|
|
27
|
-
"cli",
|
|
28
|
-
"mcp",
|
|
29
|
-
"skills",
|
|
30
|
-
"plugins"
|
|
31
|
-
],
|
|
15
|
+
"engines": { "node": ">=18" },
|
|
32
16
|
"license": "MIT"
|
|
33
17
|
}
|
package/src/agent.js
CHANGED
|
@@ -1,68 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { loadSkills, loadPlugins, skillsPrompt } from './skills.js';
|
|
4
|
-
import { discoverMcpTools, callMcpTool } from './mcp.js';
|
|
5
|
-
import { nowId } from './utils.js';
|
|
6
|
-
import fs from 'node:fs/promises';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { SESSIONS_FILE, readJson, writeJson } from './config.js';
|
|
1
|
+
import { getApiKey } from "./auth.js";
|
|
2
|
+
import { discoverSkills } from "./skills.js";
|
|
9
3
|
|
|
10
|
-
const
|
|
4
|
+
const endpoint = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
|
|
11
5
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
for (let turn = 0; turn < 16; turn++) {
|
|
30
|
-
if (!quiet) process.stdout.write('\n');
|
|
31
|
-
const answer = await streamCompletion({
|
|
32
|
-
baseUrl: config.baseUrl, apiKey, model: model || config.model, messages,
|
|
33
|
-
tools, maxTokens: config.maxTokens, temperature: config.temperature, thinking: config.thinking,
|
|
34
|
-
onText: t => process.stdout.write(t)
|
|
6
|
+
export function createAgent({ getConfig }) {
|
|
7
|
+
return { async run(input) {
|
|
8
|
+
const key = getApiKey();
|
|
9
|
+
if (!key) throw new Error("没有可用的 GLM API Key,请运行 glmcode auth login。");
|
|
10
|
+
const cfg = getConfig();
|
|
11
|
+
const skillText = discoverSkills().map(s => `## Skill: ${s.name}\n${s.content}`).join("\n\n");
|
|
12
|
+
const res = await fetch(endpoint, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
model: cfg.model,
|
|
17
|
+
messages: [
|
|
18
|
+
{ role: "system", content: `You are GLMCode, a coding assistant.\n${skillText}` },
|
|
19
|
+
{ role: "user", content: input }
|
|
20
|
+
],
|
|
21
|
+
stream: true
|
|
22
|
+
})
|
|
35
23
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
24
|
+
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
|
25
|
+
const reader = res.body.getReader();
|
|
26
|
+
const decoder = new TextDecoder();
|
|
27
|
+
let buffer = "";
|
|
28
|
+
while (true) {
|
|
29
|
+
const { value, done } = await reader.read();
|
|
30
|
+
if (done) break;
|
|
31
|
+
buffer += decoder.decode(value, { stream: true });
|
|
32
|
+
const lines = buffer.split("\n");
|
|
33
|
+
buffer = lines.pop() || "";
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (!line.startsWith("data:")) continue;
|
|
36
|
+
const data = line.slice(5).trim();
|
|
37
|
+
if (data === "[DONE]") continue;
|
|
38
|
+
try {
|
|
39
|
+
const delta = JSON.parse(data).choices?.[0]?.delta?.content;
|
|
40
|
+
if (delta) process.stdout.write(delta);
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
55
43
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
sessions[sessionKey] = session;
|
|
59
|
-
await writeJson(SESSIONS_FILE, sessions);
|
|
60
|
-
return sessionKey;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function readProjectInstruction(cwd) {
|
|
64
|
-
const files = ['AGENTS.md', 'CLAUDE.md', '.glmcode/INSTRUCTIONS.md'];
|
|
65
|
-
const chunks = [];
|
|
66
|
-
for (const f of files) { try { chunks.push(`# ${f}\n${await fs.readFile(path.join(cwd, f), 'utf8')}`); } catch {} }
|
|
67
|
-
return chunks.join('\n\n');
|
|
44
|
+
process.stdout.write("\n");
|
|
45
|
+
}};
|
|
68
46
|
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { openBrowser } from "./platform/browser.js";
|
|
5
|
+
|
|
6
|
+
const dir = join(homedir(), ".glmcode");
|
|
7
|
+
const authFile = join(dir, "auth.json");
|
|
8
|
+
const loginUrl = "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys";
|
|
9
|
+
|
|
10
|
+
export function getApiKey() {
|
|
11
|
+
for (const k of ["ZHIPU_API_KEY", "GLM_API_KEY", "BIGMODEL_API_KEY"]) {
|
|
12
|
+
if (process.env[k]) return process.env[k];
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const x = JSON.parse(readFileSync(authFile, "utf8"));
|
|
16
|
+
return x.apiKey || x.api_key || "";
|
|
17
|
+
} catch { return ""; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function login({ noBrowser = false } = {}) {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
console.log("正在打开智谱 GLM 登录/API Key 页面...");
|
|
23
|
+
console.log("GLM API 使用 API Key 进行 API 认证。");
|
|
24
|
+
|
|
25
|
+
if (getApiKey()) {
|
|
26
|
+
console.log("已检测到现有 API Key,无需再次输入。");
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (noBrowser) console.log(`请在浏览器中打开:\n${loginUrl}\n`);
|
|
31
|
+
else await openBrowser(loginUrl);
|
|
32
|
+
return Boolean(getApiKey());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function saveApiKey(apiKey) {
|
|
36
|
+
if (!apiKey?.trim()) throw new Error("API Key 不能为空");
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
writeFileSync(authFile, JSON.stringify({ apiKey: apiKey.trim() }, null, 2), { mode: 0o600 });
|
|
39
|
+
console.log(`Saved to ${authFile}`);
|
|
40
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -1,166 +1,88 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import readline from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { listMcp, saveMcpConfig } from './mcp.js';
|
|
9
|
-
import { loadSkills, loadPlugins } from './skills.js';
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import { getApiKey, login } from "./auth.js";
|
|
4
|
+
import { createAgent } from "./agent.js";
|
|
5
|
+
import { getConfig, saveConfig } from "./config.js";
|
|
6
|
+
import { discoverSkills } from "./skills.js";
|
|
7
|
+
import { detectPlatform, detectShell, listAvailableShells } from "./platform/shell.js";
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const LOGO = [
|
|
18
|
-
' ██████╗ ██╗ ███╗ ███╗',
|
|
19
|
-
'██╔════╝ ██║ ████╗ ████║',
|
|
20
|
-
'██║ ███╗ ██║ ██╔████╔██║',
|
|
21
|
-
'██║ ██║ ██║ ██║╚██╔╝██║',
|
|
22
|
-
'╚██████╔╝ ███████╗ ██║ ╚═╝ ██║',
|
|
23
|
-
' ╚═════╝ ╚══════╝ ╚═╝ ╚═╝'
|
|
9
|
+
const VERSION = "0.1.5";
|
|
10
|
+
const models = [
|
|
11
|
+
["glm-5.2", "GLM-5.2"],
|
|
12
|
+
["glm-4.5-air", "GLM-4.5-Air"],
|
|
13
|
+
["glm-4.5-flash", "GLM-4.5-Flash (free)"],
|
|
14
|
+
["glm-z1-flash", "GLM-Z1-Flash (free)"]
|
|
24
15
|
];
|
|
25
|
-
const BASE_COMMANDS = {
|
|
26
|
-
'/help':'显示全部命令','/clear':'清空对话','/thinking':'切换思考模式(开/关)','/model':'切换模型 /model <ID>',
|
|
27
|
-
'/pwd':'显示当前目录','/cd':'切换目录 /cd <路径>','/ls':'列出文件','/cat':'查看文件 /cat <路径>','/run':'执行命令 /run <shell>','/exit':'退出'
|
|
28
|
-
};
|
|
29
16
|
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log();
|
|
36
|
-
for (const [i, line] of lines.entries()) console.log(color(center(line), i >= 1 && i <= 8 ? C.cyb : C.gry));
|
|
17
|
+
function printShell() {
|
|
18
|
+
const p = detectPlatform(), s = detectShell();
|
|
19
|
+
console.log(`OS: ${p.platform}${p.isTermux ? " (Termux)" : ""}`);
|
|
20
|
+
console.log(`Architecture: ${p.arch}`);
|
|
21
|
+
console.log(`Shell: ${s.name}`);
|
|
22
|
+
console.log(`Executable: ${s.executable}`);
|
|
37
23
|
}
|
|
38
24
|
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
25
|
+
function help() {
|
|
26
|
+
console.log(`
|
|
27
|
+
/help 帮助
|
|
28
|
+
/models 模型列表
|
|
29
|
+
/model <id> 切换模型
|
|
30
|
+
/thinking 切换思考模式
|
|
31
|
+
/auth 登录
|
|
32
|
+
/skills Skills
|
|
33
|
+
/shell Shell 信息
|
|
34
|
+
/shell list 可用 Shell
|
|
35
|
+
/pwd 当前目录
|
|
36
|
+
/clear 清屏
|
|
37
|
+
/exit 退出
|
|
38
|
+
`);
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
}
|
|
41
|
+
async function main() {
|
|
42
|
+
const args = process.argv.slice(2);
|
|
43
|
+
if (args.includes("--version") || args.includes("-v")) return console.log(VERSION);
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await saveAuth('zhipu', key.trim());
|
|
62
|
-
console.log(color(`已保存到 ${AUTH_FILE}`, C.grn));
|
|
45
|
+
if (args[0] === "auth" && args[1] === "login") {
|
|
46
|
+
await login({ noBrowser: args.includes("--no-browser") });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (args[0] === "models") {
|
|
50
|
+
models.forEach(([id, label]) => console.log(`${id}\t${label}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (args[0] === "shell") {
|
|
54
|
+
if (args[1] === "list") return listAvailableShells().forEach(x => console.log(x));
|
|
55
|
+
printShell();
|
|
63
56
|
return;
|
|
64
57
|
}
|
|
65
|
-
if (sub === 'logout') { await deleteAuth('zhipu'); console.log(color('已退出智谱登录。', C.grn)); return; }
|
|
66
|
-
const key = await getAuth(); console.log(key ? color('zhipu: authenticated', C.grn) : color('zhipu: not authenticated', C.yel));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function hiddenInput(prompt) {
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
if (!process.stdin.isTTY) { const rl = readline.createInterface({input:process.stdin, output:process.stdout}); rl.question(prompt, a=>{rl.close();resolve(a)}); return; }
|
|
72
|
-
process.stdout.write(color(prompt, C.cyb));
|
|
73
|
-
readline.emitKeypressEvents(process.stdin); process.stdin.setRawMode(true);
|
|
74
|
-
let value = '', plain = false;
|
|
75
|
-
const onKey = (str, key={}) => {
|
|
76
|
-
if (key.name === 'return' || key.name === 'enter') { cleanup(); process.stdout.write('\n'); resolve(value); }
|
|
77
|
-
else if (key.ctrl && key.name === 'c') { cleanup(); reject(new Error('Cancelled')); }
|
|
78
|
-
else if (key.ctrl && key.name === 'r') { plain = !plain; redraw(); }
|
|
79
|
-
else if (key.name === 'backspace') { value = value.slice(0,-1); redraw(); }
|
|
80
|
-
else if (str && !key.ctrl && !key.meta) { value += str; process.stdout.write(plain ? str : '*'); }
|
|
81
|
-
};
|
|
82
|
-
const redraw=()=>{process.stdout.write(`\r\x1b[2K${color(prompt,C.cyb)}${plain?value:'*'.repeat(value.length)}`)};
|
|
83
|
-
const cleanup=()=>{process.stdin.off('keypress',onKey);process.stdin.setRawMode(false)};
|
|
84
|
-
process.stdin.on('keypress',onKey);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
58
|
|
|
88
|
-
|
|
89
|
-
console.log([
|
|
90
|
-
'zhipu/glm-5.2','zhipu/glm-5','zhipu/glm-4.7','zhipu/glm-4.5',
|
|
91
|
-
'zhipu/glm-4.5-flash (free preset)','zhipu/glm-z1-flash (free preset)'
|
|
92
|
-
].join('\n'));
|
|
93
|
-
}
|
|
59
|
+
if (!getApiKey()) await login();
|
|
94
60
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const file = path.join(cwd,'glmcode.json'); let local={};
|
|
99
|
-
try { local=JSON.parse(await fs.readFile(file,'utf8')); } catch {}
|
|
100
|
-
local.model=wanted; await fs.writeFile(file,JSON.stringify(local,null,2)+'\n');
|
|
101
|
-
console.log(color(`模型已切换: ${wanted}`, C.grn));
|
|
102
|
-
}
|
|
61
|
+
console.log(`GLMCode v${VERSION}`);
|
|
62
|
+
printShell();
|
|
63
|
+
console.log(`模型: ${getConfig().model} Skills: ${discoverSkills().length}`);
|
|
103
64
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
for(const s of Object.values(data).sort((a,b)=>String(b.createdAt).localeCompare(String(a.createdAt)))) console.log(`${s.id}\t${s.cwd}\t${s.createdAt}`);
|
|
108
|
-
}
|
|
109
|
-
async function skillCmd(args,cwd){const skills=await loadSkills(cwd);if(args[0]!=='list')return console.log('Usage: glmcode skill list');skills.forEach(s=>console.log(`${s.name}\t${s.path}`));}
|
|
110
|
-
async function pluginCmd(args,config,cwd){const plugins=await loadPlugins(cwd,config.plugins);if(args[0]!=='list')return console.log('Usage: glmcode plugin list');plugins.forEach(p=>console.log(`${p.id}${p.error?`\tERROR ${p.error}`:''}`));}
|
|
111
|
-
async function mcpCmd(args,config,cwd){const sub=args[0]||'list';const file=path.join(cwd,'glmcode.json');if(sub==='list')return console.log(JSON.stringify(await listMcp(config),null,2));if(sub==='add'){const name=args[1],command=args.slice(2);if(!name||!command.length)return console.log('Usage: glmcode mcp add <name> <command> [args...]');await saveMcpConfig(file,name,{type:'stdio',command});return console.log(color(`已添加 MCP: ${name}`,C.grn));}if(sub==='remove'){const name=args[1];let local={};try{local=JSON.parse(await fs.readFile(file,'utf8'))}catch{}if(local.mcp)delete local.mcp[name];await fs.writeFile(file,JSON.stringify(local,null,2)+'\n');return console.log(color(`已删除 MCP: ${name}`,C.grn));}console.log('Usage: glmcode mcp list | add <name> <command> [args...] | remove <name>');}
|
|
65
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "\n> " });
|
|
66
|
+
const agent = createAgent({ getConfig });
|
|
67
|
+
rl.prompt();
|
|
112
68
|
|
|
113
|
-
async
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
69
|
+
rl.on("line", async line => {
|
|
70
|
+
const text = line.trim();
|
|
71
|
+
if (!text) return rl.prompt();
|
|
72
|
+
if (text === "/exit" || text === "/quit") return rl.close();
|
|
73
|
+
if (text === "/help") { help(); return rl.prompt(); }
|
|
74
|
+
if (text === "/clear") { console.clear(); return rl.prompt(); }
|
|
75
|
+
if (text === "/auth") { await login(); return rl.prompt(); }
|
|
76
|
+
if (text === "/shell") { printShell(); return rl.prompt(); }
|
|
77
|
+
if (text === "/shell list") { listAvailableShells().forEach(x => console.log(x)); return rl.prompt(); }
|
|
78
|
+
if (text === "/skills") { discoverSkills().forEach(s => console.log(`${s.name}\t${s.path}`)); return rl.prompt(); }
|
|
79
|
+
if (text === "/models") { models.forEach(([id, label]) => console.log(`${id}\t${label}`)); return rl.prompt(); }
|
|
80
|
+
if (text.startsWith("/model ")) { saveConfig({ model: text.slice(7).trim() }); console.log(`模型已切换为 ${getConfig().model}`); return rl.prompt(); }
|
|
81
|
+
if (text === "/thinking") { saveConfig({ thinking: !getConfig().thinking }); console.log(`思考模式: ${getConfig().thinking ? "开" : "关"}`); return rl.prompt(); }
|
|
82
|
+
try { await agent.run(text); } catch (e) { console.error(`\n错误: ${e.message}`); }
|
|
83
|
+
rl.prompt();
|
|
117
84
|
});
|
|
85
|
+
rl.on("close", () => process.exit(0));
|
|
118
86
|
}
|
|
119
87
|
|
|
120
|
-
|
|
121
|
-
const config=await getProjectConfig(cwd0); let model=config.model, thinking=Boolean(config.thinking), cwd=cwd0;
|
|
122
|
-
let apiKey=await getAuth(); if(!apiKey){console.log(color('未配置 API Key。请先执行: glmcode auth login',C.red));return;}
|
|
123
|
-
let skills=await loadSkills(cwd), commands=commandMap(skills), sessionId;
|
|
124
|
-
startPage();
|
|
125
|
-
console.log(color(`提示: 思考模式默认${thinking?'开启':'关闭'},输入 /thinking ${thinking?'关闭':'开启'}`,C.gry));
|
|
126
|
-
if(skills.length) console.log(color(`已加载技能: ${skills.map(s=>s.name).join(', ')}`,C.mag));
|
|
127
|
-
console.log();
|
|
128
|
-
const completer=(line)=>{const hits=Object.entries(commands).filter(([c])=>c.startsWith(line)).map(([c,d])=>[c,d]);return [hits.length?hits:[],line]};
|
|
129
|
-
let interrupted=false;
|
|
130
|
-
const onSig=()=>{if(interrupted){process.exit(0)}interrupted=true;console.log(color('\n[已停止,再按一次 Ctrl+C 退出]',C.yel));setTimeout(()=>interrupted=false,1200)};
|
|
131
|
-
process.on('SIGINT',onSig);
|
|
132
|
-
while(true){
|
|
133
|
-
drawStatus(model,thinking,cwd,skills.length);
|
|
134
|
-
const user=(await readLineInput(color('┃ 输入消息... ┃ ',C.cyb),completer)).trim();
|
|
135
|
-
if(!user)continue;
|
|
136
|
-
if(user.startsWith('/')){
|
|
137
|
-
const [cmd,...rest]=user.split(/\s+/), arg=rest.join(' ');
|
|
138
|
-
if(cmd==='/exit'||cmd==='/quit'){console.log(color('再见',C.cya));break;}
|
|
139
|
-
if(cmd==='/help'){printHelp(commands);continue;}
|
|
140
|
-
if(cmd==='/clear'){console.clear();startPage();continue;}
|
|
141
|
-
if(cmd==='/thinking'){thinking=!thinking;config.thinking=thinking;console.log(color(`思考模式: ${thinking?'开启':'关闭'}`,C.yel));continue;}
|
|
142
|
-
if(cmd==='/model'){if(arg){model=arg;console.log(color(`模型已切换: ${model}`,C.grn));}else console.log(`当前模型: ${model}`);continue;}
|
|
143
|
-
if(cmd==='/pwd'){console.log(cwd);continue;}
|
|
144
|
-
if(cmd==='/cd'){try{process.chdir(path.resolve(cwd,arg||'.'));cwd=process.cwd();console.log(color(`-> ${cwd}`,C.grn));skills=await loadSkills(cwd);commands=commandMap(skills);}catch(e){console.log(color(`切换失败: ${e.message}`,C.red));}continue;}
|
|
145
|
-
if(cmd==='/ls'){try{for(const f of (await fs.readdir(cwd,{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name)))console.log(` ${f.name}${f.isDirectory()?'/':''}`)}catch(e){console.log(color(`失败: ${e.message}`,C.red));}continue;}
|
|
146
|
-
if(cmd==='/cat'){try{console.log(await fs.readFile(path.resolve(cwd,arg),'utf8').then(x=>x.slice(0,4000)))}catch(e){console.log(color(`失败: ${e.message}`,C.red));}continue;}
|
|
147
|
-
if(cmd==='/run'){if(!arg)console.log('用法: /run <命令>');else {try{const { execSync }=await import('node:child_process');console.log(execSync(arg,{cwd,encoding:'utf8',timeout:60000,stdio:['ignore','pipe','pipe']}).slice(0,4000))}catch(e){console.log(color(`执行失败: ${e.stdout||e.message}`,C.red));}}continue;}
|
|
148
|
-
if(skills.some(s=>'/'+s.name===cmd)){const s=skills.find(s=>s.name===cmd);console.log(color(`已加载技能:${s.name}`,C.grn));console.log(color('技能指令已注入,现在输入你的任务:',C.gry));config.__activeSkill=s.content;continue;}
|
|
149
|
-
console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));continue;
|
|
150
|
-
}
|
|
151
|
-
try{const merged={...config,model,thinking};if(config.__activeSkill)merged.__activeSkill=config.__activeSkill;sessionId=await runAgent({cwd,config:merged,apiKey,prompt:user,model,sessionId});console.log(color(`\n[session ${sessionId}]`,C.gry));}catch(e){console.log(color(`\n[错误] ${e.message}`,C.red));}
|
|
152
|
-
}
|
|
153
|
-
process.removeListener('SIGINT',onSig);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function main(){
|
|
157
|
-
const [command,...argv]=process.argv.slice(2); const cwd=process.cwd();
|
|
158
|
-
if(!command||command==='chat')return interactive();
|
|
159
|
-
if(command==='-h'||command==='--help'||command==='help'){console.log(`GLMCode ${VERSION}\n\n用法:\n glmcode 启动 GLM 风格交互界面\n glmcode run <prompt> 执行一次 coding task\n glmcode auth login|logout 管理智谱 API Key\n glmcode models 查看模型\n glmcode model <id>|free 切换模型\n glmcode session list|clear <id> 管理会话\n glmcode skill list 查看技能\n glmcode plugin list 查看插件\n glmcode mcp list|add|remove 管理 MCP\n glmcode config 查看配置`);return;}
|
|
160
|
-
if(command==='-v'||command==='--version')return console.log(VERSION);
|
|
161
|
-
const config=await getProjectConfig(cwd);
|
|
162
|
-
if(command==='run'){const key=await getAuth();return runAgent({cwd,config,apiKey:key,prompt:argv.join(' '),model:config.model});}
|
|
163
|
-
if(command==='auth')return authCmd(argv);if(command==='models')return modelsCmd();if(command==='model')return modelCmd(argv,config,cwd);if(command==='session')return sessionCmd(argv);if(command==='skill')return skillCmd(argv,cwd);if(command==='plugin')return pluginCmd(argv,config,cwd);if(command==='mcp')return mcpCmd(argv,config,cwd);if(command==='config')return console.log(JSON.stringify(config,null,2));
|
|
164
|
-
console.error(`Unknown command: ${command}`);process.exitCode=1;
|
|
165
|
-
}
|
|
166
|
-
main().catch(e=>{console.error(color(`Error: ${e.message}`,C.red));process.exit(1)});
|
|
88
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
package/src/config.js
CHANGED
|
@@ -1,63 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const AUTH_FILE = path.join(GLOBAL_DIR, 'auth.json');
|
|
8
|
-
export const SESSIONS_FILE = path.join(GLOBAL_DIR, 'sessions.json');
|
|
5
|
+
const dir = join(homedir(), ".glmcode");
|
|
6
|
+
const file = join(dir, "config.json");
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
model:
|
|
13
|
-
freeModel: 'glm-4.5-flash',
|
|
14
|
-
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
15
|
-
maxTokens: 8192,
|
|
16
|
-
temperature: 0.2,
|
|
17
|
-
thinking: 'enabled',
|
|
18
|
-
approval: 'ask',
|
|
19
|
-
autoCompact: true,
|
|
20
|
-
skillsDirs: ['.glmcode/skills', '~/.glmcode/skills'],
|
|
21
|
-
plugins: [],
|
|
22
|
-
mcp: {}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export async function ensureState() {
|
|
26
|
-
await fs.mkdir(GLOBAL_DIR, { recursive: true, mode: 0o700 });
|
|
27
|
-
for (const file of [CONFIG_FILE, AUTH_FILE, SESSIONS_FILE]) {
|
|
28
|
-
try { await fs.access(file); } catch { await fs.writeFile(file, file === CONFIG_FILE ? JSON.stringify(defaultConfig, null, 2) : '{}', { mode: 0o600 }); }
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function readJson(file, fallback = {}) {
|
|
33
|
-
try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch { return fallback; }
|
|
34
|
-
}
|
|
35
|
-
export async function writeJson(file, data) {
|
|
36
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
37
|
-
await fs.writeFile(file, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function getProjectConfig(cwd) {
|
|
41
|
-
const global = await readJson(CONFIG_FILE, defaultConfig);
|
|
42
|
-
const localFile = path.join(cwd, 'glmcode.json');
|
|
43
|
-
const local = await readJson(localFile, {});
|
|
44
|
-
return { ...defaultConfig, ...global, ...local, cwd };
|
|
8
|
+
export function getConfig() {
|
|
9
|
+
try { return { model: "glm-4.5-air", thinking: false, ...JSON.parse(readFileSync(file, "utf8")) }; }
|
|
10
|
+
catch { return { model: "glm-4.5-air", thinking: false }; }
|
|
45
11
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function saveAuth(provider, apiKey) {
|
|
54
|
-
const auth = await readJson(AUTH_FILE, {});
|
|
55
|
-
auth[provider] = { apiKey, savedAt: new Date().toISOString() };
|
|
56
|
-
await writeJson(AUTH_FILE, auth);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function deleteAuth(provider) {
|
|
60
|
-
const auth = await readJson(AUTH_FILE, {});
|
|
61
|
-
delete auth[provider];
|
|
62
|
-
await writeJson(AUTH_FILE, auth);
|
|
12
|
+
export function saveConfig(patch) {
|
|
13
|
+
mkdirSync(dir, { recursive: true });
|
|
14
|
+
const next = { ...getConfig(), ...patch };
|
|
15
|
+
writeFileSync(file, JSON.stringify(next, null, 2));
|
|
16
|
+
return next;
|
|
63
17
|
}
|
package/src/mcp.js
CHANGED
|
@@ -1,71 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const child = spawn(command, args, { env: { ...process.env, ...(server.env || {}) }, stdio: ['pipe', 'pipe', 'inherit'] });
|
|
14
|
-
let buffer = '';
|
|
15
|
-
const pending = new Map();
|
|
16
|
-
child.stdout.on('data', data => {
|
|
17
|
-
buffer += data.toString();
|
|
18
|
-
const lines = buffer.split('\n');
|
|
19
|
-
buffer = lines.pop() || '';
|
|
20
|
-
for (const line of lines) {
|
|
21
|
-
if (!line.trim()) continue;
|
|
22
|
-
try {
|
|
23
|
-
const msg = JSON.parse(line);
|
|
24
|
-
if (msg.id != null && pending.has(msg.id)) {
|
|
25
|
-
const p = pending.get(msg.id); pending.delete(msg.id); p(msg);
|
|
26
|
-
}
|
|
27
|
-
} catch {}
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
child.on('exit', () => { for (const p of pending.values()) p({ error: { message: 'MCP process exited' } }); pending.clear(); });
|
|
31
|
-
let nextId = 1;
|
|
32
|
-
function request(method, params = {}) {
|
|
33
|
-
const id = nextId++;
|
|
34
|
-
return new Promise((resolve, reject) => {
|
|
35
|
-
pending.set(id, msg => msg.error ? reject(new Error(msg.error.message || JSON.stringify(msg.error))) : resolve(msg.result));
|
|
36
|
-
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
37
|
-
setTimeout(() => { if (pending.has(id)) { pending.delete(id); reject(new Error(`MCP timeout: ${method}`)); } }, server.timeout ?? 15000).unref();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return { child, request };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function discoverMcpTools(servers) {
|
|
44
|
-
const outputs = [];
|
|
45
|
-
for (const serverInfo of servers) {
|
|
46
|
-
try {
|
|
47
|
-
const server = startServer(serverInfo);
|
|
48
|
-
await server.request('initialize', { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'glmcode', version: '0.1.1' } });
|
|
49
|
-
server.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '\n');
|
|
50
|
-
const result = await server.request('tools/list');
|
|
51
|
-
for (const tool of result?.tools || []) outputs.push({ ...tool, _server: serverInfo, _runtime: server, _mcpName: serverInfo.name });
|
|
52
|
-
} catch (e) {
|
|
53
|
-
outputs.push({ name: `__error__${serverInfo.name}`, description: `MCP connection error: ${e.message}`, inputSchema: { type: 'object', properties: {} }, _error: e.message, _server: serverInfo });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return outputs;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function callMcpTool(runtimeTool, args) {
|
|
60
|
-
if (!runtimeTool._runtime) throw new Error(runtimeTool._error || 'MCP server unavailable');
|
|
61
|
-
return runtimeTool._runtime.request('tools/call', { name: runtimeTool.name, arguments: args });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function saveMcpConfig(file, name, server) {
|
|
65
|
-
let cfg = {};
|
|
66
|
-
try { cfg = JSON.parse(await fs.readFile(file, 'utf8')); } catch {}
|
|
67
|
-
cfg.mcp ??= {};
|
|
68
|
-
cfg.mcp[name] = server;
|
|
69
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
70
|
-
await fs.writeFile(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
1
|
+
import { readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const file = join(homedir(), ".glmcode", "mcp.json");
|
|
6
|
+
export function loadMcp() {
|
|
7
|
+
try { return JSON.parse(readFileSync(file, "utf8")); }
|
|
8
|
+
catch { return { mcpServers: {} }; }
|
|
9
|
+
}
|
|
10
|
+
export function saveMcp(config) {
|
|
11
|
+
mkdirSync(join(homedir(), ".glmcode"), { recursive: true });
|
|
12
|
+
writeFileSync(file, JSON.stringify(config, null, 2));
|
|
71
13
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { detectPlatform } from "./shell.js";
|
|
3
|
+
|
|
4
|
+
function tryRun(command, args) {
|
|
5
|
+
return new Promise(resolve => {
|
|
6
|
+
const child = execFile(command, args, { windowsHide: true }, error => resolve(!error));
|
|
7
|
+
child.on("error", () => resolve(false));
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function openBrowser(url) {
|
|
12
|
+
const p = detectPlatform();
|
|
13
|
+
|
|
14
|
+
if (process.env.GLMCODE_NO_BROWSER === "1") {
|
|
15
|
+
console.log(`\n浏览器自动打开已禁用。\n请手动打开:\n${url}\n`);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (p.platform === "win32") {
|
|
20
|
+
if (await tryRun("cmd.exe", ["/c", "start", "", url])) return true;
|
|
21
|
+
} else if (p.platform === "darwin") {
|
|
22
|
+
if (await tryRun("open", [url])) return true;
|
|
23
|
+
} else {
|
|
24
|
+
if (p.isTermux && await tryRun("termux-open-url", [url])) return true;
|
|
25
|
+
if (await tryRun("xdg-open", [url])) return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(`\n当前环境没有可用的浏览器启动器。\n请在浏览器中打开:\n${url}\n`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
function exists(command) {
|
|
5
|
+
try {
|
|
6
|
+
if (process.platform === "win32") {
|
|
7
|
+
execFileSync("where", [command], { stdio: "ignore" });
|
|
8
|
+
} else {
|
|
9
|
+
execFileSync("sh", ["-lc", `command -v ${JSON.stringify(command)}`], { stdio: "ignore" });
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
} catch { return false; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function detectPlatform() {
|
|
16
|
+
return {
|
|
17
|
+
platform: process.platform,
|
|
18
|
+
arch: process.arch,
|
|
19
|
+
isTermux: Boolean(process.env.TERMUX_VERSION || process.env.PREFIX?.includes("com.termux")),
|
|
20
|
+
release: os.release()
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function detectShell() {
|
|
25
|
+
const p = detectPlatform();
|
|
26
|
+
if (p.platform === "win32") {
|
|
27
|
+
if (process.env.PSModulePath && exists("pwsh")) return { name: "PowerShell", executable: "pwsh" };
|
|
28
|
+
if (process.env.PSModulePath) return { name: "PowerShell", executable: "powershell.exe" };
|
|
29
|
+
return { name: "CMD", executable: process.env.ComSpec || "cmd.exe" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const envShell = process.env.SHELL || "";
|
|
33
|
+
const name = envShell.split("/").pop();
|
|
34
|
+
if (["bash", "zsh", "fish", "sh", "dash", "ksh", "nu"].includes(name) && exists(name)) {
|
|
35
|
+
return { name, executable: envShell };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const candidate of ["bash", "zsh", "fish", "sh"]) {
|
|
39
|
+
if (exists(candidate)) return { name: candidate, executable: candidate };
|
|
40
|
+
}
|
|
41
|
+
return { name: "sh", executable: "/bin/sh" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function listAvailableShells() {
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
return [
|
|
47
|
+
...(exists("pwsh") ? ["powershell"] : []),
|
|
48
|
+
...(exists("powershell.exe") ? ["powershell.exe"] : []),
|
|
49
|
+
"cmd.exe",
|
|
50
|
+
...(exists("bash.exe") ? ["bash.exe"] : [])
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
return ["sh", ...(exists("bash") ? ["bash"] : []), ...(exists("zsh") ? ["zsh"] : []), ...(exists("fish") ? ["fish"] : [])];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function shellCommand(command, shell = detectShell().executable) {
|
|
57
|
+
if (process.platform === "win32" && /powershell|pwsh/i.test(shell)) {
|
|
58
|
+
return { file: shell, args: ["-NoLogo", "-NoProfile", "-Command", command] };
|
|
59
|
+
}
|
|
60
|
+
if (process.platform === "win32" && /cmd/i.test(shell)) {
|
|
61
|
+
return { file: shell, args: ["/d", "/s", "/c", command] };
|
|
62
|
+
}
|
|
63
|
+
return { file: shell, args: ["-lc", command] };
|
|
64
|
+
}
|
package/src/skills.js
CHANGED
|
@@ -1,37 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
for (const root of dirs) {
|
|
9
|
-
if (!await exists(root)) continue;
|
|
10
|
-
for (const name of await fs.readdir(root)) {
|
|
11
|
-
const file = path.join(root, name, 'SKILL.md');
|
|
12
|
-
if (!await exists(file)) continue;
|
|
13
|
-
const content = await fs.readFile(file, 'utf8');
|
|
14
|
-
skills.push({ name, path: file, content });
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return skills;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function loadPlugins(cwd, configured = []) {
|
|
21
|
-
const roots = [path.join(cwd, '.glmcode', 'plugins'), path.join(expandHome('~/.glmcode'), 'plugins')];
|
|
22
|
-
const mods = [...configured];
|
|
5
|
+
export function discoverSkills() {
|
|
6
|
+
const roots = [join(process.cwd(), ".glmcode", "skills"), join(homedir(), ".glmcode", "skills")];
|
|
7
|
+
const out = [];
|
|
23
8
|
for (const root of roots) {
|
|
24
|
-
if (!
|
|
25
|
-
for (const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
if (!existsSync(root)) continue;
|
|
10
|
+
for (const e of readdirSync(root, { withFileTypes: true })) {
|
|
11
|
+
if (!e.isDirectory()) continue;
|
|
12
|
+
const p = join(root, e.name, "SKILL.md");
|
|
13
|
+
if (existsSync(p)) out.push({ name: e.name, path: p, content: readFileSync(p, "utf8") });
|
|
14
|
+
}
|
|
30
15
|
}
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function skillsPrompt(skills) {
|
|
35
|
-
if (!skills.length) return '';
|
|
36
|
-
return '\n\nAVAILABLE SKILLS:\n' + skills.map(s => `## ${s.name}\n${s.content}`).join('\n\n');
|
|
16
|
+
return out;
|
|
37
17
|
}
|
package/src/tools.js
CHANGED
|
@@ -1,86 +1,14 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
async function approve(action, details, mode) {
|
|
17
|
-
if (mode === 'auto') return true;
|
|
18
|
-
if (mode === 'deny') return false;
|
|
19
|
-
const rl = readline.createInterface({ input, output });
|
|
20
|
-
const answer = await rl.question(`\n⚠ ${action}\n${details}\nAllow? [y/N] `);
|
|
21
|
-
rl.close();
|
|
22
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function listTree(dir, root, depth, out = []) {
|
|
26
|
-
if (depth < 0) return out;
|
|
27
|
-
for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
|
|
28
|
-
if (['.git', 'node_modules', '.glmcode/cache'].includes(path.relative(root, path.join(dir, ent.name))) || ent.name === '.git') continue;
|
|
29
|
-
const full = path.join(dir, ent.name);
|
|
30
|
-
out.push({ path: path.relative(root, full) || '.', type: ent.isDirectory() ? 'dir' : 'file' });
|
|
31
|
-
if (ent.isDirectory() && depth > 0) await listTree(full, root, depth - 1, out);
|
|
32
|
-
if (out.length > 500) break;
|
|
33
|
-
}
|
|
34
|
-
return out;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function makeToolExecutor({ cwd, approval = 'ask' }) {
|
|
38
|
-
return async function execute(name, args) {
|
|
39
|
-
switch (name) {
|
|
40
|
-
case 'read_file': {
|
|
41
|
-
const file = relSafe(cwd, args.path);
|
|
42
|
-
return trimOutput(await fs.readFile(file, 'utf8'));
|
|
43
|
-
}
|
|
44
|
-
case 'write_file': {
|
|
45
|
-
const file = relSafe(cwd, args.path);
|
|
46
|
-
if (!await approve('File write', file, approval)) return 'DENIED by user.';
|
|
47
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
48
|
-
await fs.writeFile(file, args.content, 'utf8');
|
|
49
|
-
return `Wrote ${path.relative(cwd, file)} (${Buffer.byteLength(args.content, 'utf8')} bytes).`;
|
|
50
|
-
}
|
|
51
|
-
case 'list_files': {
|
|
52
|
-
const dir = relSafe(cwd, args.path || '.');
|
|
53
|
-
return jsonText(await listTree(dir, cwd, Math.min(args.depth ?? 1, 4)));
|
|
54
|
-
}
|
|
55
|
-
case 'search_text': {
|
|
56
|
-
const base = relSafe(cwd, args.path || '.');
|
|
57
|
-
const hits = [];
|
|
58
|
-
async function walk(d) {
|
|
59
|
-
for (const ent of await fs.readdir(d, { withFileTypes: true })) {
|
|
60
|
-
if (['.git', 'node_modules', 'dist', 'build'].includes(ent.name)) continue;
|
|
61
|
-
const f = path.join(d, ent.name);
|
|
62
|
-
if (ent.isDirectory()) await walk(f);
|
|
63
|
-
else if (hits.length < 100) {
|
|
64
|
-
try { const text = await fs.readFile(f, 'utf8'); const lines = text.split(/\r?\n/); lines.forEach((line, i) => { if (line.toLowerCase().includes(String(args.query).toLowerCase()) && hits.length < 100) hits.push({ path: path.relative(cwd, f), line: i + 1, text: line.slice(0, 500) }); }); } catch {}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
await walk(base);
|
|
69
|
-
return jsonText(hits);
|
|
70
|
-
}
|
|
71
|
-
case 'run_shell': {
|
|
72
|
-
if (!await approve('Shell execution', args.command, approval)) return 'DENIED by user.';
|
|
73
|
-
const timeout = Math.max(1000, Math.min(args.timeoutMs ?? 120000, 300000));
|
|
74
|
-
return await new Promise((resolve) => {
|
|
75
|
-
const child = spawn(args.command, { cwd, shell: true, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
76
|
-
let stdout = '', stderr = '';
|
|
77
|
-
const timer = setTimeout(() => child.kill('SIGTERM'), timeout);
|
|
78
|
-
child.stdout.on('data', d => stdout += d);
|
|
79
|
-
child.stderr.on('data', d => stderr += d);
|
|
80
|
-
child.on('close', code => { clearTimeout(timer); resolve(`exit=${code}\nSTDOUT:\n${trimOutput(stdout)}\nSTDERR:\n${trimOutput(stderr)}`); });
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
default: throw new Error(`Unknown tool: ${name}`);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { detectShell, shellCommand } from "./platform/shell.js";
|
|
4
|
+
|
|
5
|
+
export async function readFile(path) { return fs.readFile(path, "utf8"); }
|
|
6
|
+
export async function writeFile(path, content) { await fs.writeFile(path, content, "utf8"); return `wrote ${path}`; }
|
|
7
|
+
|
|
8
|
+
export async function runShell(command, cwd = process.cwd()) {
|
|
9
|
+
const s = detectShell(), spec = shellCommand(command, s.executable);
|
|
10
|
+
return await new Promise((resolve, reject) => {
|
|
11
|
+
execFile(spec.file, spec.args, { cwd, maxBuffer: 10 * 1024 * 1024 },
|
|
12
|
+
(error, stdout, stderr) => error ? reject(new Error(stderr || error.message)) : resolve({ stdout, stderr, code: 0, shell: s.name }));
|
|
13
|
+
});
|
|
86
14
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 GLMCode contributors
|
|
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/glmcode.example.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"provider": "zhipu",
|
|
3
|
-
"model": "glm-5.2",
|
|
4
|
-
"freeModel": "glm-4.5-flash",
|
|
5
|
-
"baseUrl": "https://open.bigmodel.cn/api/paas/v4",
|
|
6
|
-
"approval": "ask",
|
|
7
|
-
"thinking": "enabled",
|
|
8
|
-
"maxTokens": 8192,
|
|
9
|
-
"temperature": 0.2,
|
|
10
|
-
"plugins": [],
|
|
11
|
-
"mcp": {
|
|
12
|
-
"example": {
|
|
13
|
-
"type": "stdio",
|
|
14
|
-
"command": ["npx", "-y", "<your-mcp-server>"],
|
|
15
|
-
"env": {},
|
|
16
|
-
"timeout": 15000
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/provider.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
export async function chatCompletion({ baseUrl, apiKey, model, messages, tools, maxTokens, temperature, thinking = 'enabled', stream = false }) {
|
|
2
|
-
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
|
3
|
-
method: 'POST',
|
|
4
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
5
|
-
body: JSON.stringify({
|
|
6
|
-
model, messages, tools,
|
|
7
|
-
max_tokens: maxTokens,
|
|
8
|
-
temperature,
|
|
9
|
-
thinking: { type: thinking },
|
|
10
|
-
stream
|
|
11
|
-
})
|
|
12
|
-
});
|
|
13
|
-
const text = await res.text();
|
|
14
|
-
if (!res.ok) throw new Error(`GLM API ${res.status}: ${text.slice(0, 1000)}`);
|
|
15
|
-
return JSON.parse(text);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function streamCompletion({ baseUrl, apiKey, model, messages, tools, maxTokens, temperature, thinking = 'enabled', onText }) {
|
|
19
|
-
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
22
|
-
body: JSON.stringify({ model, messages, tools, max_tokens: maxTokens, temperature, thinking: { type: thinking }, stream: true })
|
|
23
|
-
});
|
|
24
|
-
if (!res.ok) throw new Error(`GLM API ${res.status}: ${await res.text()}`);
|
|
25
|
-
const reader = res.body.getReader();
|
|
26
|
-
const decoder = new TextDecoder();
|
|
27
|
-
let buf = '';
|
|
28
|
-
let final = { role: 'assistant', content: '', tool_calls: [] };
|
|
29
|
-
while (true) {
|
|
30
|
-
const { value, done } = await reader.read();
|
|
31
|
-
if (done) break;
|
|
32
|
-
buf += decoder.decode(value, { stream: true });
|
|
33
|
-
const parts = buf.split('\n'); buf = parts.pop() ?? '';
|
|
34
|
-
for (const line of parts) {
|
|
35
|
-
if (!line.startsWith('data:')) continue;
|
|
36
|
-
const data = line.slice(5).trim();
|
|
37
|
-
if (data === '[DONE]') continue;
|
|
38
|
-
let chunk; try { chunk = JSON.parse(data); } catch { continue; }
|
|
39
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
40
|
-
if (!delta) continue;
|
|
41
|
-
if (delta.content) { final.content += delta.content; onText?.(delta.content); }
|
|
42
|
-
if (delta.tool_calls) {
|
|
43
|
-
for (const tc of delta.tool_calls) {
|
|
44
|
-
const i = tc.index ?? 0;
|
|
45
|
-
final.tool_calls[i] ??= { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } };
|
|
46
|
-
if (tc.id) final.tool_calls[i].id = tc.id;
|
|
47
|
-
if (tc.function?.name) final.tool_calls[i].function.name += tc.function.name;
|
|
48
|
-
if (tc.function?.arguments) final.tool_calls[i].function.arguments += tc.function.arguments;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return final;
|
|
54
|
-
}
|
package/src/utils.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
|
|
5
|
-
export function expandHome(p) { return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p; }
|
|
6
|
-
export async function exists(p) { try { await fs.access(p); return true; } catch { return false; } }
|
|
7
|
-
export function trimOutput(s, max = 12000) { return s.length <= max ? s : s.slice(0, max) + `\n… [truncated to ${max} chars]`; }
|
|
8
|
-
export function jsonText(value) { return JSON.stringify(value, null, 2); }
|
|
9
|
-
export function relSafe(cwd, target) {
|
|
10
|
-
const abs = path.resolve(cwd, target);
|
|
11
|
-
const root = path.resolve(cwd);
|
|
12
|
-
if (abs !== root && !abs.startsWith(root + path.sep)) throw new Error(`Path escapes project root: ${target}`);
|
|
13
|
-
return abs;
|
|
14
|
-
}
|
|
15
|
-
export function nowId(prefix = 's') { return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; }
|