glmcode 0.1.3 → 0.1.6
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 +9 -0
- package/LICENSE +0 -20
- package/README.md +39 -29
- package/package.json +9 -24
- package/src/auth.js +53 -49
- package/src/cli.js +30 -173
- package/src/config.js +8 -65
- package/src/shell.js +5 -0
- package/glmcode.example.json +0 -19
- package/src/agent.js +0 -68
- package/src/mcp.js +0 -71
- package/src/provider.js +0 -54
- package/src/skills.js +0 -51
- package/src/tools.js +0 -86
- 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
package/LICENSE
CHANGED
|
@@ -1,21 +1 @@
|
|
|
1
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/README.md
CHANGED
|
@@ -1,50 +1,60 @@
|
|
|
1
|
-
# GLMCode
|
|
2
|
-
|
|
3
|
-
GLMCode 是一个 Node.js / npm 的 GLM Coding Agent,界面和交互以移动端 Python 原版 `glmcode.py` 为基础,同时提供文件工具、Shell、Skills、Plugins、MCP、Session 和模型切换。
|
|
1
|
+
# GLMCode 0.1.6
|
|
4
2
|
|
|
5
3
|
## 安装
|
|
6
|
-
|
|
7
4
|
```bash
|
|
8
5
|
npm install -g glmcode
|
|
6
|
+
glmcode --version
|
|
9
7
|
```
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
## API Key 自动检测
|
|
10
|
+
检测顺序:
|
|
11
|
+
1. `ZHIPU_API_KEY`
|
|
12
|
+
2. `GLM_API_KEY`
|
|
13
|
+
3. `~/.glmcode/auth.json`
|
|
12
14
|
|
|
15
|
+
已有 Key:
|
|
13
16
|
```bash
|
|
14
|
-
glmcode
|
|
15
|
-
glm
|
|
16
|
-
gc
|
|
17
|
+
glmcode auth set YOUR_API_KEY
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
状态:
|
|
21
|
+
```bash
|
|
22
|
+
glmcode auth status
|
|
23
|
+
```
|
|
22
24
|
|
|
25
|
+
没有 Key:
|
|
23
26
|
```bash
|
|
24
27
|
glmcode auth login
|
|
25
28
|
```
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
## 依赖
|
|
32
|
-
|
|
33
|
-
Python 原版需要 `requests` 与 `prompt_toolkit`,并使用 Python 标准库的 `zipfile`。Node 版对应为:
|
|
34
|
-
|
|
35
|
-
- Node.js >= 18:内置 `fetch`、readline、child_process、fs 等能力
|
|
36
|
-
- `adm-zip`:自动解压 `skills/*.zip` 技能包
|
|
30
|
+
无 GUI 环境:
|
|
31
|
+
```bash
|
|
32
|
+
glmcode auth login --no-browser
|
|
33
|
+
```
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
官方 API Key 页面:
|
|
36
|
+
https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys
|
|
39
37
|
|
|
40
|
-
##
|
|
38
|
+
## Shell
|
|
39
|
+
```bash
|
|
40
|
+
glmcode shell
|
|
41
|
+
glmcode shell list
|
|
42
|
+
glmcode run "echo hello"
|
|
43
|
+
```
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
0.1.6 不再假定 `termux-open-url` 一定存在;Windows、macOS、Linux/Alpine/Android 环境分别检测可用启动器和 Shell。
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
## 配置
|
|
48
|
+
配置:`~/.glmcode/config.json`
|
|
49
|
+
认证:`~/.glmcode/auth.json`
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
```bash
|
|
52
|
+
glmcode config
|
|
53
|
+
glmcode config set model glm-4.5
|
|
54
|
+
```
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
## 开发
|
|
57
|
+
```bash
|
|
58
|
+
npm run check
|
|
59
|
+
npm pack
|
|
60
|
+
```
|
package/package.json
CHANGED
|
@@ -1,38 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glmcode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "GLM
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Cross-platform GLM CLI with local authentication and shell detection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"glmcode": "src/cli.js",
|
|
8
8
|
"glm": "src/cli.js",
|
|
9
9
|
"gc": "src/cli.js"
|
|
10
10
|
},
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"scripts": {
|
|
12
|
+
"check": "node --check src/cli.js && node --check src/auth.js && node --check src/config.js && node --check src/shell.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"src",
|
|
16
|
-
"templates",
|
|
15
|
+
"src/",
|
|
17
16
|
"README.md",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"start": "node src/cli.js",
|
|
23
|
-
"check": "node --check src/cli.js"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"glm",
|
|
27
|
-
"zhipu",
|
|
28
|
-
"coding-agent",
|
|
29
|
-
"cli",
|
|
30
|
-
"mcp",
|
|
31
|
-
"skills",
|
|
32
|
-
"plugins"
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"LICENSE"
|
|
33
19
|
],
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"adm-zip": "^0.5.16"
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
37
22
|
}
|
|
38
23
|
}
|
package/src/auth.js
CHANGED
|
@@ -1,58 +1,62 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import { spawn } from
|
|
5
|
-
import { AUTH_FILE, readJson, writeJson, getAuth } from './config.js';
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
5
|
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
6
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".glmcode");
|
|
7
|
+
export const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
8
|
+
export const API_KEYS_URL = "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
? [['cmd', ['/c', 'start', '', url]]]
|
|
13
|
-
: process.platform === 'darwin'
|
|
14
|
-
? [['open', [url]]]
|
|
15
|
-
: [['termux-open-url', [url]], ['xdg-open', [url]]];
|
|
16
|
-
for (const [cmd, args] of attempts) {
|
|
17
|
-
try {
|
|
18
|
-
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
19
|
-
child.unref();
|
|
20
|
-
return true;
|
|
21
|
-
} catch {}
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
10
|
+
function readJson(file, fallback = {}) {
|
|
11
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return fallback; }
|
|
24
12
|
}
|
|
13
|
+
function writeJson(file, value) {
|
|
14
|
+
fs.mkdirSync(CONFIG_DIR, {recursive:true, mode:0o700});
|
|
15
|
+
fs.writeFileSync(file, JSON.stringify(value,null,2)+"\n", {mode:0o600});
|
|
16
|
+
try { fs.chmodSync(file,0o600); } catch {}
|
|
17
|
+
}
|
|
18
|
+
function key(v) { const s = v == null ? "" : String(v).trim(); return s || null; }
|
|
25
19
|
|
|
26
|
-
export
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
export function getApiKey() {
|
|
21
|
+
const env = key(process.env.ZHIPU_API_KEY) || key(process.env.GLM_API_KEY);
|
|
22
|
+
if (env) return {apiKey:env, source:"environment"};
|
|
23
|
+
const a = readJson(AUTH_FILE,{});
|
|
24
|
+
const saved = key(a.apiKey) || key(a.api_key) || key(a.key) || key(a.token);
|
|
25
|
+
return saved ? {apiKey:saved, source:AUTH_FILE} : null;
|
|
26
|
+
}
|
|
27
|
+
export function saveApiKey(apiKey) {
|
|
28
|
+
const k = key(apiKey);
|
|
29
|
+
if (!k) throw new Error("API Key 不能为空");
|
|
30
|
+
writeJson(AUTH_FILE,{provider:"zhipu",apiKey:k,updatedAt:new Date().toISOString()});
|
|
31
|
+
return AUTH_FILE;
|
|
34
32
|
}
|
|
33
|
+
export function clearAuth() { writeJson(AUTH_FILE,{}); }
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.log('检测到已有 API Key,正在使用已保存凭据。');
|
|
50
|
-
return key;
|
|
35
|
+
function exists(cmd) {
|
|
36
|
+
try { execFileSync(process.platform==="win32" ? "where" : "which",[cmd],{stdio:"ignore"}); return true; }
|
|
37
|
+
catch { return false; }
|
|
38
|
+
}
|
|
39
|
+
export function openUrl(url) {
|
|
40
|
+
const candidates = process.platform==="win32"
|
|
41
|
+
? [["cmd",["/c","start","",url]]]
|
|
42
|
+
: process.platform==="darwin"
|
|
43
|
+
? [["open",[url]]]
|
|
44
|
+
: [["xdg-open",[url]],["termux-open-url",[url]],["gio",["open",url]]];
|
|
45
|
+
for (const [cmd,args] of candidates) {
|
|
46
|
+
if (!exists(cmd)) continue;
|
|
47
|
+
try { const p=spawn(cmd,args,{detached:true,stdio:"ignore"}); p.unref(); return true; } catch {}
|
|
51
48
|
}
|
|
52
|
-
return
|
|
49
|
+
return false;
|
|
53
50
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
export function login({noBrowser=false}={}) {
|
|
52
|
+
const found=getApiKey();
|
|
53
|
+
if (found) { console.log(`已检测到 API Key(来源:${found.source}),无需重新配置。`); return found; }
|
|
54
|
+
console.log("未检测到 GLM API Key。");
|
|
55
|
+
console.log(`API Key 管理页面:${API_KEYS_URL}`);
|
|
56
|
+
if (!noBrowser && openUrl(API_KEYS_URL)) console.log("已尝试打开默认浏览器。");
|
|
57
|
+
else console.log("当前环境无法自动打开浏览器,请手动复制上面的地址。");
|
|
58
|
+
console.log("浏览器登录不会自动把网页中的 API Key 暴露给 CLI。");
|
|
59
|
+
console.log("获得 Key 后运行:glmcode auth set <API_KEY>");
|
|
60
|
+
return null;
|
|
58
61
|
}
|
|
62
|
+
export function maskKey(k) { return k && k.length>8 ? `${k.slice(0,4)}...${k.slice(-4)}` : "********"; }
|
package/src/cli.js
CHANGED
|
@@ -1,174 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
];
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const W = 44, center = s => s.padStart(Math.floor((W+s.length)/2)).padEnd(W);
|
|
33
|
-
const lines = [
|
|
34
|
-
'', '✧ GLM ✧', '', ...LOGO, '', 'GLM Code · 移动端', `v${VERSION}`, '', '/help 查看命令 · /thinking 思考开关', ''
|
|
35
|
-
];
|
|
36
|
-
console.log();
|
|
37
|
-
for (const [i, line] of lines.entries()) console.log(color(center(line), i >= 1 && i <= 8 ? C.cyb : C.gry));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function commandMap(skills) {
|
|
41
|
-
const d = { ...BASE_COMMANDS };
|
|
42
|
-
for (const s of skills) d['/' + s.name] = '技能: ' + s.description;
|
|
43
|
-
return d;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function drawStatus(model, thinking, cwd, skillCount) {
|
|
47
|
-
const text = `思考:${thinking ? '开' : '关'} 模型:${model}${skillCount ? ` 技能:${skillCount}` : ''} ${cwd}`;
|
|
48
|
-
process.stdout.write(`\x1b[2K\r${color(text, C.gry)}\n`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function printHelp(commands) {
|
|
52
|
-
console.log();
|
|
53
|
-
for (const [c,d] of Object.entries(commands)) console.log(` ${color(c.padEnd(18), C.cyb)} ${color(d, C.gry)}`);
|
|
54
|
-
console.log();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function authCmd(args) {
|
|
58
|
-
const sub = args[0] || 'status';
|
|
59
|
-
if (sub === 'login') {
|
|
60
|
-
const detected = await browserLogin();
|
|
61
|
-
if (detected) {
|
|
62
|
-
await saveAuth('zhipu', detected);
|
|
63
|
-
console.log(color(`已检测并保存现有 API 凭据到 ${AUTH_FILE}`, C.grn));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
console.log(color('未检测到本机已有 API Key。', C.yel));
|
|
67
|
-
console.log(color('请先在浏览器登录智谱开放平台并创建 API Key,然后设置 ZHIPU_API_KEY。', C.gry));
|
|
68
|
-
console.log(color(`登录入口: ${LOGIN_URL}`, C.gry));
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (sub === 'logout') { await deleteAuth('zhipu'); console.log(color('已退出智谱登录。', C.grn)); return; }
|
|
72
|
-
const status = await authStatus();
|
|
73
|
-
console.log(status.authenticated ? color(`zhipu: authenticated (${status.source})`, C.grn) : color('zhipu: not authenticated', C.yel));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function hiddenInput(prompt) {
|
|
77
|
-
return new Promise((resolve, reject) => {
|
|
78
|
-
if (!process.stdin.isTTY) { const rl = readline.createInterface({input:process.stdin, output:process.stdout}); rl.question(prompt, a=>{rl.close();resolve(a)}); return; }
|
|
79
|
-
process.stdout.write(color(prompt, C.cyb));
|
|
80
|
-
readline.emitKeypressEvents(process.stdin); process.stdin.setRawMode(true);
|
|
81
|
-
let value = '', plain = false;
|
|
82
|
-
const onKey = (str, key={}) => {
|
|
83
|
-
if (key.name === 'return' || key.name === 'enter') { cleanup(); process.stdout.write('\n'); resolve(value); }
|
|
84
|
-
else if (key.ctrl && key.name === 'c') { cleanup(); reject(new Error('Cancelled')); }
|
|
85
|
-
else if (key.ctrl && key.name === 'r') { plain = !plain; redraw(); }
|
|
86
|
-
else if (key.name === 'backspace') { value = value.slice(0,-1); redraw(); }
|
|
87
|
-
else if (str && !key.ctrl && !key.meta) { value += str; process.stdout.write(plain ? str : '*'); }
|
|
88
|
-
};
|
|
89
|
-
const redraw=()=>{process.stdout.write(`\r\x1b[2K${color(prompt,C.cyb)}${plain?value:'*'.repeat(value.length)}`)};
|
|
90
|
-
const cleanup=()=>{process.stdin.off('keypress',onKey);process.stdin.setRawMode(false)};
|
|
91
|
-
process.stdin.on('keypress',onKey);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function modelsCmd() {
|
|
96
|
-
console.log([
|
|
97
|
-
'zhipu/glm-5.2','zhipu/glm-5','zhipu/glm-4.7','zhipu/glm-4.5',
|
|
98
|
-
'zhipu/glm-4.5-flash (free preset)','zhipu/glm-z1-flash (free preset)'
|
|
99
|
-
].join('\n'));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function modelCmd(args, config, cwd) {
|
|
103
|
-
const wanted = args[0] === 'free' ? config.freeModel : args[0];
|
|
104
|
-
if (!wanted) return console.log(`当前模型: ${config.model}`);
|
|
105
|
-
const file = path.join(cwd,'glmcode.json'); let local={};
|
|
106
|
-
try { local=JSON.parse(await fs.readFile(file,'utf8')); } catch {}
|
|
107
|
-
local.model=wanted; await fs.writeFile(file,JSON.stringify(local,null,2)+'\n');
|
|
108
|
-
console.log(color(`模型已切换: ${wanted}`, C.grn));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function sessionCmd(args) {
|
|
112
|
-
const file=path.join(GLOBAL_DIR,'sessions.json'); const data=await readJson(file,{});
|
|
113
|
-
if(args[0]==='clear'){delete data[args[1]];await writeJson(file,data);console.log('Deleted.');return;}
|
|
114
|
-
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}`);
|
|
115
|
-
}
|
|
116
|
-
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}`));}
|
|
117
|
-
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}`:''}`));}
|
|
118
|
-
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>');}
|
|
119
|
-
|
|
120
|
-
async function readLineInput(prompt, completer) {
|
|
121
|
-
return new Promise(resolve => {
|
|
122
|
-
const rl=readline.createInterface({input:process.stdin,output:process.stdout,completer});
|
|
123
|
-
rl.question(prompt, a=>{rl.close();resolve(a)});
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function interactive() {
|
|
128
|
-
const config=await getProjectConfig(cwd0); let model=config.model, thinking=Boolean(config.thinking), cwd=cwd0;
|
|
129
|
-
let apiKey=await getAuth(); if(!apiKey){ apiKey=await browserLogin(); if(apiKey) await saveAuth('zhipu', apiKey); } if(!apiKey){console.log(color('未检测到 API Key。请在浏览器登录智谱开放平台后配置 API Key,或设置 ZHIPU_API_KEY。',C.red));return;}
|
|
130
|
-
let skills=await loadSkills(cwd), commands=commandMap(skills), sessionId;
|
|
131
|
-
startPage();
|
|
132
|
-
console.log(color(`提示: 思考模式默认${thinking?'开启':'关闭'},输入 /thinking ${thinking?'关闭':'开启'}`,C.gry));
|
|
133
|
-
if(skills.length) console.log(color(`已加载技能: ${skills.map(s=>s.name).join(', ')}`,C.mag));
|
|
134
|
-
console.log();
|
|
135
|
-
const completer=(line)=>{const hits=Object.entries(commands).filter(([c])=>c.startsWith(line)).map(([c,d])=>[c,d]);return [hits.length?hits:[],line]};
|
|
136
|
-
let interrupted=false;
|
|
137
|
-
const onSig=()=>{if(interrupted){process.exit(0)}interrupted=true;console.log(color('\n[已停止,再按一次 Ctrl+C 退出]',C.yel));setTimeout(()=>interrupted=false,1200)};
|
|
138
|
-
process.on('SIGINT',onSig);
|
|
139
|
-
while(true){
|
|
140
|
-
drawStatus(model,thinking,cwd,skills.length);
|
|
141
|
-
const user=(await readLineInput(color('┃ 输入消息... ┃ ',C.cyb),completer)).trim();
|
|
142
|
-
if(!user)continue;
|
|
143
|
-
if(user.startsWith('/')){
|
|
144
|
-
const [cmd,...rest]=user.split(/\s+/), arg=rest.join(' ');
|
|
145
|
-
if(cmd==='/exit'||cmd==='/quit'){console.log(color('再见',C.cya));break;}
|
|
146
|
-
if(cmd==='/help'){printHelp(commands);continue;}
|
|
147
|
-
if(cmd==='/clear'){console.clear();startPage();continue;}
|
|
148
|
-
if(cmd==='/thinking'){thinking=!thinking;config.thinking=thinking;console.log(color(`思考模式: ${thinking?'开启':'关闭'}`,C.yel));continue;}
|
|
149
|
-
if(cmd==='/model'){if(arg){model=arg;console.log(color(`模型已切换: ${model}`,C.grn));}else console.log(`当前模型: ${model}`);continue;}
|
|
150
|
-
if(cmd==='/pwd'){console.log(cwd);continue;}
|
|
151
|
-
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;}
|
|
152
|
-
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;}
|
|
153
|
-
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;}
|
|
154
|
-
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;}
|
|
155
|
-
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;}
|
|
156
|
-
console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));continue;
|
|
157
|
-
}
|
|
158
|
-
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));}
|
|
159
|
-
}
|
|
160
|
-
process.removeListener('SIGINT',onSig);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function main(){
|
|
164
|
-
const [command,...argv]=process.argv.slice(2); const cwd=process.cwd();
|
|
165
|
-
if(!command||command==='chat')return interactive();
|
|
166
|
-
if(command==='-h'||command==='--help'||command==='help'){console.log(`GLMCode ${VERSION}\n\n用法:\n glmcode 启动 GLM 风格交互界面
|
|
167
|
-
glm / gc 快捷启动 GLMCode\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;}
|
|
168
|
-
if(command==='-v'||command==='--version')return console.log(VERSION);
|
|
169
|
-
const config=await getProjectConfig(cwd);
|
|
170
|
-
if(command==='run'){let key=await getAuth();if(!key) key=await browserLogin();if(!key) throw new Error('No Zhipu API key detected. Run: glmcode auth login');return runAgent({cwd,config,apiKey:key,prompt:argv.join(' '),model:config.model});}
|
|
171
|
-
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));
|
|
172
|
-
console.error(`Unknown command: ${command}`);process.exitCode=1;
|
|
173
|
-
}
|
|
174
|
-
main().catch(e=>{console.error(color(`Error: ${e.message}`,C.red));process.exit(1)});
|
|
2
|
+
import {spawnSync} from "node:child_process";
|
|
3
|
+
import {getApiKey,saveApiKey,clearAuth,login,maskKey} from "./auth.js";
|
|
4
|
+
import {loadConfig,saveConfig} from "./config.js";
|
|
5
|
+
import {shellInfo,availableShells} from "./shell.js";
|
|
6
|
+
const VERSION="0.1.6";
|
|
7
|
+
function help(){console.log(`GLMCode ${VERSION}
|
|
8
|
+
Usage: glmcode [command]
|
|
9
|
+
auth login [--no-browser] 检测 Key;没有则打开官方页面
|
|
10
|
+
auth set <API_KEY> 保存 Key 到 ~/.glmcode/auth.json
|
|
11
|
+
auth status 查看认证状态(Key 脱敏)
|
|
12
|
+
auth logout 删除本地 Key
|
|
13
|
+
shell 查看 OS/CPU/Shell
|
|
14
|
+
shell list 列出可用 Shell
|
|
15
|
+
run <command> 使用当前 Shell 执行命令
|
|
16
|
+
config 查看配置
|
|
17
|
+
config set <key> <value> 修改配置
|
|
18
|
+
skills / plugins / mcp 查看对应配置
|
|
19
|
+
--version 显示版本`);}
|
|
20
|
+
function runShell(command){const s=shellInfo(),win=process.platform==="win32";const f=win?(s.shell==="powershell"||s.shell==="pwsh"?s.executable:process.env.ComSpec||"cmd.exe"):s.executable;const a=win?(s.shell==="powershell"||s.shell==="pwsh"?["-NoProfile","-Command",command]:["/d","/s","/c",command]):["-lc",command];const r=spawnSync(f,a,{stdio:"inherit"});process.exitCode=r.status??1;}
|
|
21
|
+
function value(v){if(v==="true")return true;if(v==="false")return false;if(/^-?\d+(?:\.\d+)?$/.test(v))return Number(v);try{return JSON.parse(v);}catch{return v;}}
|
|
22
|
+
const a=process.argv.slice(2);
|
|
23
|
+
if(!a.length){const f=getApiKey();if(!f){console.log(`GLMCode ${VERSION}\n未检测到 API Key。运行 glmcode auth login。`);process.exitCode=1;}else{const c=loadConfig();console.log(`GLMCode ${VERSION}\nProvider: ${c.provider}\nModel: ${c.model}\nAPI Key: ${maskKey(f.apiKey)} (${f.source})\n认证检测成功。`);}}
|
|
24
|
+
else if(a[0]==="--version"||a[0]==="-v")console.log(VERSION);
|
|
25
|
+
else if(a[0]==="--help"||a[0]==="-h"||a[0]==="help")help();
|
|
26
|
+
else if(a[0]==="auth"){const s=a[1]||"status";if(s==="login")login({noBrowser:a.includes("--no-browser")});else if(s==="set"){if(!a[2]){console.error("用法:glmcode auth set <API_KEY>");process.exitCode=2;}else console.log(`API Key 已保存到 ${saveApiKey(a[2])}`);}else if(s==="status"){const f=getApiKey();console.log(f?`API Key: ${maskKey(f.apiKey)}\n来源: ${f.source}`:"未检测到 API Key。");}else if(s==="logout"){clearAuth();console.log("本地 API Key 已删除。");}else help();}
|
|
27
|
+
else if(a[0]==="shell"){if(a[1]==="list")console.log(availableShells().join("\n")||"未检测到额外 Shell");else{const s=shellInfo();console.log(`OS: ${s.os}\nArchitecture: ${s.architecture}\nShell: ${s.shell}\nExecutable: ${s.executable}`);}}
|
|
28
|
+
else if(a[0]==="run"){if(!a[1]){console.error("用法:glmcode run <command>");process.exitCode=2;}else runShell(a.slice(1).join(" "));}
|
|
29
|
+
else if(a[0]==="config"){const c=loadConfig();if(a[1]==="set"){if(!a[2]||a[3]===undefined){console.error("用法:glmcode config set <key> <value>");process.exitCode=2;}else{c[a[2]]=value(a.slice(3).join(" "));saveConfig(c);console.log(`已设置 ${a[2]}`);}}else console.log(JSON.stringify(c,null,2));}
|
|
30
|
+
else if(["skills","plugins","mcp"].includes(a[0]))console.log(JSON.stringify(loadConfig()[a[0]],null,2));
|
|
31
|
+
else{console.error(`未知命令:${a[0]}`);help();process.exitCode=2;}
|
package/src/config.js
CHANGED
|
@@ -1,65 +1,8 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
const defaultConfig = {
|
|
11
|
-
provider: 'zhipu',
|
|
12
|
-
model: 'glm-5.2',
|
|
13
|
-
freeModel: 'glm-4.5-flash',
|
|
14
|
-
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
15
|
-
maxTokens: 8192,
|
|
16
|
-
temperature: 0.2,
|
|
17
|
-
thinking: 'disabled',
|
|
18
|
-
approval: 'ask',
|
|
19
|
-
autoCompact: true,
|
|
20
|
-
skillsDirs: ['.glmcode/skills', '~/.glmcode/skills'],
|
|
21
|
-
plugins: [],
|
|
22
|
-
mcp: {},
|
|
23
|
-
startupCommand: 'glmcode',
|
|
24
|
-
autoOpenLogin: true
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export async function ensureState() {
|
|
28
|
-
await fs.mkdir(GLOBAL_DIR, { recursive: true, mode: 0o700 });
|
|
29
|
-
for (const file of [CONFIG_FILE, AUTH_FILE, SESSIONS_FILE]) {
|
|
30
|
-
try { await fs.access(file); } catch { await fs.writeFile(file, file === CONFIG_FILE ? JSON.stringify(defaultConfig, null, 2) : '{}', { mode: 0o600 }); }
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function readJson(file, fallback = {}) {
|
|
35
|
-
try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch { return fallback; }
|
|
36
|
-
}
|
|
37
|
-
export async function writeJson(file, data) {
|
|
38
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
39
|
-
await fs.writeFile(file, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function getProjectConfig(cwd) {
|
|
43
|
-
const global = await readJson(CONFIG_FILE, defaultConfig);
|
|
44
|
-
const localFile = path.join(cwd, 'glmcode.json');
|
|
45
|
-
const local = await readJson(localFile, {});
|
|
46
|
-
return { ...defaultConfig, ...global, ...local, cwd };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function getAuth(provider = 'zhipu') {
|
|
50
|
-
const auth = await readJson(AUTH_FILE, {});
|
|
51
|
-
const envKey = provider === 'zhipu' ? process.env.ZHIPU_API_KEY : undefined;
|
|
52
|
-
return auth[provider]?.apiKey || envKey || '';
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function saveAuth(provider, apiKey) {
|
|
56
|
-
const auth = await readJson(AUTH_FILE, {});
|
|
57
|
-
auth[provider] = { apiKey, savedAt: new Date().toISOString() };
|
|
58
|
-
await writeJson(AUTH_FILE, auth);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function deleteAuth(provider) {
|
|
62
|
-
const auth = await readJson(AUTH_FILE, {});
|
|
63
|
-
delete auth[provider];
|
|
64
|
-
await writeJson(AUTH_FILE, auth);
|
|
65
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const CONFIG_DIR=path.join(os.homedir(),".glmcode");
|
|
5
|
+
export const CONFIG_FILE=path.join(CONFIG_DIR,"config.json");
|
|
6
|
+
const defaults={provider:"zhipu",model:"glm-4.5",freeModel:"glm-4.5-air",baseUrl:"https://open.bigmodel.cn/api/paas/v4",maxTokens:4096,temperature:0.7,thinking:true,approval:"ask",autoCompact:true,skillsDirs:[],plugins:[],mcp:{},startupCommand:"",autoOpenLogin:true};
|
|
7
|
+
export function loadConfig(){try{return {...defaults,...JSON.parse(fs.readFileSync(CONFIG_FILE,"utf8"))};}catch{return {...defaults};}}
|
|
8
|
+
export function saveConfig(c){fs.mkdirSync(CONFIG_DIR,{recursive:true,mode:0o700});fs.writeFileSync(CONFIG_FILE,JSON.stringify(c,null,2)+"\n",{mode:0o600});}
|
package/src/shell.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import {execFileSync} from "node:child_process";
|
|
3
|
+
function shell(){if(process.platform==="win32"){const p=process.env.PWSH||process.env.POWERSHELL;if(p)return{name:"powershell",executable:p};return{name:"cmd",executable:process.env.ComSpec||"cmd.exe"};}const e=process.env.SHELL||"/bin/sh";return{name:e.split("/").pop()||"sh",executable:e};}
|
|
4
|
+
export function shellInfo(){const s=shell();return{os:process.platform,architecture:process.arch,shell:s.name,executable:s.executable};}
|
|
5
|
+
export function availableShells(){const c=process.platform==="win32"?["cmd","powershell","pwsh"]:["sh","bash","zsh","fish","ash"];return c.filter(n=>{try{execFileSync(process.platform==="win32"?"where":"which",[n],{stdio:"ignore"});return true;}catch{return false;}});}
|
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/agent.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { chatCompletion, streamCompletion } from './provider.js';
|
|
2
|
-
import { TOOL_DEFS, makeToolExecutor } from './tools.js';
|
|
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';
|
|
9
|
-
|
|
10
|
-
const BASE_SYSTEM = `You are GLMCode, a terminal coding agent.\nYou work directly in the user's project.\nRules:\n- Inspect before modifying.\n- Prefer small, reversible edits.\n- Use tools for files and commands instead of pretending.\n- Never claim a command/file change happened unless the tool result says so.\n- Keep the user informed about important actions.\n- Respect project instructions and loaded skills.`;
|
|
11
|
-
|
|
12
|
-
export async function runAgent({ cwd, config, apiKey, prompt, model, sessionId, quiet = false }) {
|
|
13
|
-
if (!apiKey) throw new Error('No Zhipu API key. Run: glmcode auth login');
|
|
14
|
-
const skills = await loadSkills(cwd);
|
|
15
|
-
const plugins = await loadPlugins(cwd, config.plugins);
|
|
16
|
-
const pluginTools = plugins.flatMap(p => p.module?.tools || []);
|
|
17
|
-
const mcpTools = await discoverMcpTools(Object.entries(config.mcp ?? {}).map(([name, value]) => ({ name, ...value })));
|
|
18
|
-
const mcpDefs = mcpTools.filter(t => !t._error).map(t => ({ type: 'function', function: { name: `mcp__${t._mcpName}__${t.name}`, description: `[MCP ${t._mcpName}] ${t.description || t.name}`, parameters: t.inputSchema || { type: 'object', properties: {} } } }));
|
|
19
|
-
const tools = [...TOOL_DEFS, ...pluginTools, ...mcpDefs];
|
|
20
|
-
const sessionKey = sessionId || nowId('sess');
|
|
21
|
-
const sessions = await readJson(SESSIONS_FILE, {});
|
|
22
|
-
const session = sessions[sessionKey] || { id: sessionKey, cwd, createdAt: new Date().toISOString(), messages: [] };
|
|
23
|
-
const projectInstruction = await readProjectInstruction(cwd);
|
|
24
|
-
const activeSkill = config.__activeSkill ? `\n\nACTIVE SKILL INSTRUCTIONS:\n${config.__activeSkill}` : '';
|
|
25
|
-
const system = BASE_SYSTEM + (projectInstruction ? `\n\nPROJECT INSTRUCTIONS:\n${projectInstruction}` : '') + skillsPrompt(skills) + activeSkill;
|
|
26
|
-
const messages = [{ role: 'system', content: system }, ...session.messages, { role: 'user', content: prompt }];
|
|
27
|
-
const execute = makeToolExecutor({ cwd, approval: config.approval });
|
|
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)
|
|
35
|
-
});
|
|
36
|
-
const assistant = { role: 'assistant', content: answer.content || null };
|
|
37
|
-
if (answer.tool_calls?.length) assistant.tool_calls = answer.tool_calls;
|
|
38
|
-
messages.push(assistant);
|
|
39
|
-
if (!answer.tool_calls?.length) break;
|
|
40
|
-
for (const call of answer.tool_calls) {
|
|
41
|
-
const name = call.function.name;
|
|
42
|
-
let args; try { args = JSON.parse(call.function.arguments || '{}'); } catch { args = {}; }
|
|
43
|
-
let result;
|
|
44
|
-
try {
|
|
45
|
-
if (name.startsWith('mcp__')) {
|
|
46
|
-
const parts = name.split('__');
|
|
47
|
-
const serverName = parts[1]; const toolName = parts.slice(2).join('__');
|
|
48
|
-
const match = mcpTools.find(t => t._mcpName === serverName && t.name === toolName);
|
|
49
|
-
if (!match) throw new Error(`MCP tool not found: ${name}`);
|
|
50
|
-
result = JSON.stringify(await callMcpTool(match, args));
|
|
51
|
-
} else { result = await execute(name, args); }
|
|
52
|
-
} catch (e) { result = `ERROR: ${e.message}`; }
|
|
53
|
-
messages.push({ role: 'tool', tool_call_id: call.id, name, content: String(result) });
|
|
54
|
-
if (!quiet) process.stdout.write(`\n[tool:${name}] ${String(result).slice(0, 1200)}\n`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
session.messages = messages.filter(m => m.role !== 'system').slice(-80);
|
|
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');
|
|
68
|
-
}
|
package/src/mcp.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
|
|
5
|
-
export async function listMcp(config) {
|
|
6
|
-
return Object.entries(config?.mcp ?? {}).map(([name, value]) => ({ name, ...value }));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function startServer(server) {
|
|
10
|
-
if (server.type !== 'stdio') throw new Error('MCP server type must be "stdio" in GLMCode 0.1.x.');
|
|
11
|
-
const [command, ...args] = server.command;
|
|
12
|
-
if (!command) throw new Error('MCP stdio server requires command[]');
|
|
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');
|
|
71
|
-
}
|
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/skills.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { expandHome, exists } from './utils.js';
|
|
4
|
-
import AdmZip from 'adm-zip';
|
|
5
|
-
|
|
6
|
-
async function extractZipSkills(root) {
|
|
7
|
-
if (!await exists(root)) return;
|
|
8
|
-
for (const name of await fs.readdir(root)) {
|
|
9
|
-
if (!name.toLowerCase().endsWith('.zip')) continue;
|
|
10
|
-
const zipPath = path.join(root, name);
|
|
11
|
-
const target = path.join(root, name.slice(0, -4));
|
|
12
|
-
try {
|
|
13
|
-
if (!await exists(target)) new AdmZip(zipPath).extractAllTo(target, true);
|
|
14
|
-
} catch {}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function loadSkills(cwd) {
|
|
19
|
-
const dirs = [path.join(cwd, '.glmcode', 'skills'), path.join(expandHome('~/.glmcode'), 'skills')];
|
|
20
|
-
const skills = [];
|
|
21
|
-
for (const root of dirs) {
|
|
22
|
-
await extractZipSkills(root);
|
|
23
|
-
if (!await exists(root)) continue;
|
|
24
|
-
for (const name of await fs.readdir(root)) {
|
|
25
|
-
const file = path.join(root, name, 'SKILL.md');
|
|
26
|
-
if (!await exists(file)) continue;
|
|
27
|
-
const content = await fs.readFile(file, 'utf8');
|
|
28
|
-
skills.push({ name, path: file, content });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return skills;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function loadPlugins(cwd, configured = []) {
|
|
35
|
-
const roots = [path.join(cwd, '.glmcode', 'plugins'), path.join(expandHome('~/.glmcode'), 'plugins')];
|
|
36
|
-
const mods = [...configured];
|
|
37
|
-
for (const root of roots) {
|
|
38
|
-
if (!await exists(root)) continue;
|
|
39
|
-
for (const name of await fs.readdir(root)) mods.push(path.join(root, name));
|
|
40
|
-
}
|
|
41
|
-
const plugins = [];
|
|
42
|
-
for (const item of mods) {
|
|
43
|
-
try { const mod = await import(item.startsWith('.') ? path.resolve(cwd, item) : item); plugins.push({ id: item, module: mod }); } catch (e) { plugins.push({ id: item, error: e.message }); }
|
|
44
|
-
}
|
|
45
|
-
return plugins;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function skillsPrompt(skills) {
|
|
49
|
-
if (!skills.length) return '';
|
|
50
|
-
return '\n\nAVAILABLE SKILLS:\n' + skills.map(s => `## ${s.name}\n${s.content}`).join('\n\n');
|
|
51
|
-
}
|
package/src/tools.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import readline from 'node:readline/promises';
|
|
4
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { relSafe, trimOutput, jsonText } from './utils.js';
|
|
7
|
-
|
|
8
|
-
export const TOOL_DEFS = [
|
|
9
|
-
{ type: 'function', function: { name: 'read_file', description: 'Read a UTF-8 text file inside the project.', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } },
|
|
10
|
-
{ type: 'function', function: { name: 'write_file', description: 'Create or replace a UTF-8 text file inside the project.', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } } },
|
|
11
|
-
{ type: 'function', function: { name: 'list_files', description: 'List files/directories recursively or at one level inside the project.', parameters: { type: 'object', properties: { path: { type: 'string' }, depth: { type: 'integer', minimum: 1, maximum: 4 } } } } },
|
|
12
|
-
{ type: 'function', function: { name: 'search_text', description: 'Search text in files inside the project.', parameters: { type: 'object', properties: { query: { type: 'string' }, path: { type: 'string' } }, required: ['query'] } } },
|
|
13
|
-
{ type: 'function', function: { name: 'run_shell', description: 'Run a shell command in the project directory. Use for builds, tests, git and development commands.', parameters: { type: 'object', properties: { command: { type: 'string' }, timeoutMs: { type: 'integer', minimum: 1000, maximum: 300000 } }, required: ['command'] } } }
|
|
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
|
-
};
|
|
86
|
-
}
|
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)}`; }
|