glmcode 0.1.6 → 0.1.9
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 +20 -6
- package/LICENSE +20 -0
- package/README.md +38 -42
- package/package.json +20 -4
- package/src/cli.js +288 -30
- package/src/auth.js +0 -62
- package/src/config.js +0 -8
- package/src/shell.js +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 0.1.9 (2026-08-28)
|
|
4
|
+
|
|
5
|
+
- 输入 `/` 或前缀自动弹出斜杠命令菜单(含技能命令)
|
|
6
|
+
- 新增底部状态栏,实时显示思考开关 / 模型 / 技能数 / 当前目录
|
|
7
|
+
- 技能包支持 `.zip` 自动解压加载(依赖 `adm-zip`)
|
|
8
|
+
- 启动时自动检测并安装缺失依赖(`ensureDeps`)
|
|
9
|
+
- `/install` 增加 termux `pkg` 支持
|
|
10
|
+
- Tab 补全菜单增强
|
|
11
|
+
|
|
12
|
+
## 0.1.8 (2026-08-27)
|
|
13
|
+
|
|
14
|
+
- 重构为单文件 `src/cli.js`,零第三方运行时依赖
|
|
15
|
+
- 内置工具:`run_shell` / `write_file` / `read_file` / `use_skill`
|
|
16
|
+
- 技能系统:`skills/` 目录 `SKILL.md` 自动加载
|
|
17
|
+
- 认证:`glmcode auth set / status / logout / login`
|
|
18
|
+
- `glmcode shell` 系统信息查看
|
|
19
|
+
- 流式对话 + 思考模式切换
|
|
2
20
|
|
|
3
21
|
## 0.1.6
|
|
4
22
|
|
|
5
|
-
-
|
|
6
|
-
- 增加 `auth set/status/logout`
|
|
7
|
-
- 修复没有 `termux-open-url` 时的 ENOENT 崩溃
|
|
8
|
-
- 更新智谱 API Key 管理页面
|
|
9
|
-
- 保留跨平台 Shell 检测与配置入口
|
|
23
|
+
- 首个公开版本
|
package/LICENSE
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
1
|
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jimioffice
|
|
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,60 +1,56 @@
|
|
|
1
|
-
# GLMCode
|
|
1
|
+
# GLMCode
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
GLM 移动端 coding agent CLI。在终端里直接和 GLM 对话,自动执行写文件、读文件、Shell 命令、加载技能等任务。
|
|
4
|
+
|
|
5
|
+
```
|
|
5
6
|
npm install -g glmcode
|
|
6
|
-
glmcode --version
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
##
|
|
10
|
-
检测顺序:
|
|
11
|
-
1. `ZHIPU_API_KEY`
|
|
12
|
-
2. `GLM_API_KEY`
|
|
13
|
-
3. `~/.glmcode/auth.json`
|
|
9
|
+
## 特性
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
- 流式对话(支持思考模式)
|
|
12
|
+
- 四个内置工具:`run_shell` / `write_file` / `read_file` / `use_skill`
|
|
13
|
+
- 技能系统:把 `SKILL.md` 目录或 `.zip` 技能包放进 `skills/` 自动加载
|
|
14
|
+
- 斜杠命令菜单:输入 `/` 或前缀即弹出菜单,Tab 补全
|
|
15
|
+
- 底部状态栏:实时显示思考开关 / 模型 / 技能数 / 当前目录
|
|
16
|
+
- 自动安装依赖:缺失 `adm-zip` 等依赖时启动自动 `npm install`
|
|
17
|
+
- `/install` 命令:调用系统包管理器(apt/apk/pkg/brew/winget 等)自动安装程序
|
|
18
|
+
- 认证:`glmcode auth set <API_KEY>`
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
glmcode auth status
|
|
23
|
-
```
|
|
20
|
+
## 用法
|
|
24
21
|
|
|
25
|
-
没有 Key:
|
|
26
22
|
```bash
|
|
27
|
-
glmcode
|
|
23
|
+
glmcode # 启动对话
|
|
24
|
+
glmcode --help # 帮助
|
|
25
|
+
glmcode auth set sk-xxx # 设置 API Key
|
|
26
|
+
glmcode shell # 查看系统信息
|
|
27
|
+
glmcode shell list # 列出可用 Shell
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
glmcode auth login --no-browser
|
|
33
|
-
```
|
|
30
|
+
## 斜杠命令
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
| 命令 | 说明 |
|
|
33
|
+
|------|------|
|
|
34
|
+
| `/help` | 显示全部命令 |
|
|
35
|
+
| `/clear` | 清空对话 |
|
|
36
|
+
| `/thinking` | 切换思考模式 |
|
|
37
|
+
| `/model <ID>` | 切换模型 |
|
|
38
|
+
| `/pwd` `/cd` `/ls` `/cat` | 目录与文件操作 |
|
|
39
|
+
| `/run <shell>` | 执行命令 |
|
|
40
|
+
| `/install <程序>` | 自动安装程序/依赖 |
|
|
41
|
+
| `/exit` | 退出 |
|
|
37
42
|
|
|
38
|
-
##
|
|
39
|
-
```bash
|
|
40
|
-
glmcode shell
|
|
41
|
-
glmcode shell list
|
|
42
|
-
glmcode run "echo hello"
|
|
43
|
-
```
|
|
43
|
+
## 环境变量
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
- `ZHIPU_API_KEY` / `GLM_API_KEY`:直接注入 API Key,无需写盘
|
|
46
|
+
- `GLM_SKILLS_DIR`:自定义技能目录
|
|
46
47
|
|
|
47
|
-
##
|
|
48
|
-
配置:`~/.glmcode/config.json`
|
|
49
|
-
认证:`~/.glmcode/auth.json`
|
|
48
|
+
## 技能
|
|
50
49
|
|
|
51
50
|
```bash
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
mkdir -p skills/my-skill
|
|
52
|
+
# 写入 skills/my-skill/SKILL.md(含 name / description frontmatter)
|
|
53
|
+
# 或将技能打成 zip 放入 skills/,启动时自动解压加载
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
npm run check
|
|
59
|
-
npm pack
|
|
60
|
-
```
|
|
56
|
+
启动后输入 `/技能名` 即可注入技能指令。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glmcode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "GLM coding agent CLI with shell tools, skills (SKILL.md/zip auto-extract), streaming chat, slash-command menu and bottom status bar. Auto-installs its dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"glmcode": "src/cli.js",
|
|
@@ -9,15 +9,31 @@
|
|
|
9
9
|
"gc": "src/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"check": "node --check src/cli.js &&
|
|
12
|
+
"check": "node --check src/cli.js && npm pack --dry-run",
|
|
13
|
+
"postinstall": "node -e \"try{require.resolve('adm-zip')}catch{console.log('[glmcode] installing deps...');require('child_process').execSync('npm install --no-save adm-zip')}\""
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"src/",
|
|
17
|
+
"skills/",
|
|
16
18
|
"README.md",
|
|
17
19
|
"CHANGELOG.md",
|
|
18
20
|
"LICENSE"
|
|
19
21
|
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"adm-zip": "^0.5.16"
|
|
24
|
+
},
|
|
20
25
|
"engines": {
|
|
21
26
|
"node": ">=18"
|
|
22
|
-
}
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"glm",
|
|
30
|
+
"zhipu",
|
|
31
|
+
"coding-agent",
|
|
32
|
+
"cli",
|
|
33
|
+
"mcp",
|
|
34
|
+
"skills",
|
|
35
|
+
"plugins",
|
|
36
|
+
"termux"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT"
|
|
23
39
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,31 +1,289 @@
|
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import {spawnSync, spawn} from "node:child_process";
|
|
7
|
+
import {fileURLToPath} from "node:url";
|
|
8
|
+
|
|
9
|
+
const VERSION="0.1.9";
|
|
10
|
+
const BASE_URL="https://open.bigmodel.cn/api/paas/v4";
|
|
11
|
+
const API_KEYS_URL="https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys";
|
|
12
|
+
const HOME=os.homedir();
|
|
13
|
+
const DATA_DIR=path.join(HOME,".glmcode");
|
|
14
|
+
const AUTH_FILE=path.join(DATA_DIR,"auth.json");
|
|
15
|
+
const CONFIG_FILE=path.join(DATA_DIR,"config.json");
|
|
16
|
+
const PACKAGE_ROOT=path.resolve(path.dirname(new URL(import.meta.url).pathname),"..");
|
|
17
|
+
const MAIN_FILE_DIR=path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const SKILLS_DIR=process.env.GLM_SKILLS_DIR || path.join(PACKAGE_ROOT,"skills");
|
|
19
|
+
|
|
20
|
+
const C={reset:"\x1b[0m",dim:"\x1b[2m",bold:"\x1b[1m",cyan:"\x1b[36m",cyanB:"\x1b[1;36m",green:"\x1b[32m",yellow:"\x1b[33m",red:"\x1b[31m",mag:"\x1b[35m",gray:"\x1b[90m"};
|
|
21
|
+
const color=(s,c)=>c+s+C.reset;
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CONFIG={
|
|
24
|
+
provider:"zhipu", model:"glm-5.2", freeModel:"glm-5.2", baseUrl:BASE_URL,
|
|
25
|
+
maxTokens:4096, temperature:0.7, thinking:false, approval:"ask",
|
|
26
|
+
autoCompact:true, skillsDirs:[], plugins:[], mcp:{}, startupCommand:"", autoOpenLogin:true
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let config={...DEFAULT_CONFIG};
|
|
30
|
+
let apiKey=null, messages=[], cwd=process.cwd(), thinkingEnabled=false, skills=new Map();
|
|
31
|
+
let stopping=false;
|
|
32
|
+
|
|
33
|
+
function readJson(file,fallback={}){try{return JSON.parse(fs.readFileSync(file,"utf8"));}catch{return fallback;}}
|
|
34
|
+
function saveJson(file,v){fs.mkdirSync(DATA_DIR,{recursive:true,mode:0o700});fs.writeFileSync(file,JSON.stringify(v,null,2)+"\n",{mode:0o600});try{fs.chmodSync(file,0o600);}catch{}}
|
|
35
|
+
function loadAuth(){
|
|
36
|
+
const env=(process.env.ZHIPU_API_KEY||process.env.GLM_API_KEY||"").trim();
|
|
37
|
+
if(env)return {key:env,source:"environment"};
|
|
38
|
+
const a=readJson(AUTH_FILE,{});
|
|
39
|
+
const k=(a.apiKey||a.api_key||a.key||a.token||"").trim();
|
|
40
|
+
return k?{key:k,source:AUTH_FILE}:null;
|
|
41
|
+
}
|
|
42
|
+
function mask(k){return k&&k.length>8?`${k.slice(0,4)}...${k.slice(-4)}`:"********";}
|
|
43
|
+
function loadConfig(){config={...DEFAULT_CONFIG,...readJson(CONFIG_FILE,{})};thinkingEnabled=!!config.thinking;}
|
|
44
|
+
function saveConfig(){saveJson(CONFIG_FILE,config);}
|
|
45
|
+
function shellInfo(){
|
|
46
|
+
if(process.platform==="win32"){
|
|
47
|
+
const p=process.env.PWSH||process.env.POWERSHELL;
|
|
48
|
+
return p?{name:"powershell",exe:p}:{name:"cmd",exe:process.env.ComSpec||"cmd.exe"};
|
|
49
|
+
}
|
|
50
|
+
const exe=process.env.SHELL||"/bin/sh"; return {name:path.basename(exe),exe};
|
|
51
|
+
}
|
|
52
|
+
function commandExists(c){try{spawnSync(process.platform==="win32"?"where":"which",[c],{stdio:"ignore"});return true;}catch{return false;}}
|
|
53
|
+
function openUrl(url){
|
|
54
|
+
const c=process.platform==="win32"?[["cmd",["/c","start","",url]]]:
|
|
55
|
+
process.platform==="darwin"?[["open",[url]]]:
|
|
56
|
+
[["xdg-open",[url]],["termux-open-url",[url]],["gio",["open",url]]];
|
|
57
|
+
for(const [cmd,args] of c){if(!commandExists(cmd))continue;try{const p=spawn(cmd,args,{detached:true,stdio:"ignore"});p.unref();return true;}catch{}}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---- 自动依赖安装(确保所有需要的程序/依赖就绪) ---- */
|
|
62
|
+
async function ensureDeps(){
|
|
63
|
+
const missing=[];
|
|
64
|
+
try{await import("adm-zip");}catch{missing.push("adm-zip");}
|
|
65
|
+
if(!missing.length)return true;
|
|
66
|
+
console.log(color(`[glmcode] 缺少依赖: ${missing.join(", ")},正在自动安装...`,C.yellow));
|
|
67
|
+
let ok=true;
|
|
68
|
+
for(const m of missing){
|
|
69
|
+
const r=spawnSync(process.platform==="win32"?"npm.cmd":"npm",["install","--no-save",m],{cwd:PACKAGE_ROOT,encoding:"utf8",stdio:"inherit"});
|
|
70
|
+
if(r.status!==0){ok=false;console.log(color(`[glmcode] ${m} 自动安装失败,请手动运行: npm install -g glmcode`,C.red));}
|
|
71
|
+
}
|
|
72
|
+
return ok;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ---- 技能系统:SKILL.md + zip 自动解压 ---- */
|
|
76
|
+
async function extractZipSkills(){
|
|
77
|
+
if(!fs.existsSync(SKILLS_DIR))return;
|
|
78
|
+
const zips=fs.readdirSync(SKILLS_DIR).filter(x=>x.toLowerCase().endsWith(".zip")).sort((a,b)=>a.localeCompare(b));
|
|
79
|
+
if(!zips.length)return;
|
|
80
|
+
let AdmZip=null;
|
|
81
|
+
try{AdmZip=(await import("adm-zip")).default;}catch{return;}
|
|
82
|
+
for(const f of zips){
|
|
83
|
+
const zp=path.join(SKILLS_DIR,f), target=path.join(SKILLS_DIR,f.replace(/\.zip$/i,""));
|
|
84
|
+
if(fs.existsSync(target))continue;
|
|
85
|
+
try{
|
|
86
|
+
new AdmZip(zp).extractAllTo(target,true);
|
|
87
|
+
// 扁平化:zip 内带同名根目录时(如 my-skill/SKILL.md)解压会出现 target/my-skill/... 嵌套,上移一层
|
|
88
|
+
const sub=fs.readdirSync(target,{withFileTypes:true}).find(d=>d.isDirectory());
|
|
89
|
+
if(sub && !fs.existsSync(path.join(target,"SKILL.md")) && fs.existsSync(path.join(target,sub.name,"SKILL.md"))){
|
|
90
|
+
for(const e of fs.readdirSync(path.join(target,sub.name))){
|
|
91
|
+
fs.renameSync(path.join(target,sub.name,e),path.join(target,e));
|
|
92
|
+
}
|
|
93
|
+
fs.rmdirSync(path.join(target,sub.name));
|
|
94
|
+
}
|
|
95
|
+
console.log(color(`[glmcode] 技能包已解压: ${f} -> ${path.basename(target)}`,C.green));
|
|
96
|
+
}catch(e){console.log(color(`[glmcode] 解压失败 ${f}: ${e.message}`,C.red));}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function loadSkills(){
|
|
100
|
+
await extractZipSkills();
|
|
101
|
+
skills=new Map(); fs.mkdirSync(SKILLS_DIR,{recursive:true});
|
|
102
|
+
const entries=fs.readdirSync(SKILLS_DIR,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name));
|
|
103
|
+
for(const entry of entries){
|
|
104
|
+
if(!entry.isDirectory())continue;
|
|
105
|
+
const file=path.join(SKILLS_DIR,entry.name,"SKILL.md"); if(!fs.existsSync(file))continue;
|
|
106
|
+
const content=fs.readFileSync(file,"utf8");
|
|
107
|
+
let name=entry.name, description="";
|
|
108
|
+
const fm=content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
109
|
+
if(fm){const n=fm[1].match(/^name:\s*(.+)$/m),d=fm[1].match(/^description:\s*(.+)$/m);if(n)name=n[1].trim();if(d)description=d[1].trim();}
|
|
110
|
+
if(!description)description=(content.trim().split("\n")[0]||name).replace(/^#\s*/,"").slice(0,60);
|
|
111
|
+
skills.set(name,{name,description,content});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const BASE_COMMANDS={
|
|
115
|
+
"/help":"显示全部命令","/clear":"清空对话","/thinking":"切换思考模式(开/关)",
|
|
116
|
+
"/model":"切换模型 /model <ID>","/pwd":"显示当前目录","/cd":"切换目录 /cd <路径>",
|
|
117
|
+
"/ls":"列出文件","/cat":"查看文件 /cat <路径>","/run":"执行命令 /run <shell>","/install":"安装程序/依赖 /install <程序>","/exit":"退出"
|
|
118
|
+
};
|
|
119
|
+
function commands(){const d={...BASE_COMMANDS};for(const s of skills.values())d["/"+s.name]="技能: "+s.description;return d;}
|
|
120
|
+
|
|
121
|
+
function systemPrompt(){
|
|
122
|
+
let s="你是 GLMCode,一个运行在用户本机终端中的 coding agent。你可以使用 run_shell、write_file、read_file 和 use_skill 工具。回答简洁,执行任务时先理解当前工作目录。";
|
|
123
|
+
if(skills.size){s+="\n可用技能:\n";for(const x of skills.values())s+=`- ${x.name}: ${x.description}\n`;}
|
|
124
|
+
return s;
|
|
125
|
+
}
|
|
126
|
+
const TOOLS=[
|
|
127
|
+
{type:"function",function:{name:"run_shell",description:"在当前操作系统的用户 Shell 中执行命令并返回输出,限时60秒。",
|
|
128
|
+
parameters:{type:"object",properties:{command:{type:"string"}},required:["command"]}}},
|
|
129
|
+
{type:"function",function:{name:"write_file",description:"写入文本文件并自动创建父目录。",
|
|
130
|
+
parameters:{type:"object",properties:{path:{type:"string"},content:{type:"string"}},required:["path","content"]}}},
|
|
131
|
+
{type:"function",function:{name:"read_file",description:"读取文本文件内容。",
|
|
132
|
+
parameters:{type:"object",properties:{path:{type:"string"}},required:["path"]}}},
|
|
133
|
+
{type:"function",function:{name:"use_skill",description:"加载指定技能的 SKILL.md 并按其说明执行。",
|
|
134
|
+
parameters:{type:"object",properties:{skill_name:{type:"string"}},required:["skill_name"]}}}
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
function execTool(name,args){
|
|
138
|
+
if(name==="run_shell"){
|
|
139
|
+
const s=shellInfo(), isWin=process.platform==="win32";
|
|
140
|
+
const file=s.exe, a=isWin?(s.name==="powershell"?["-NoProfile","-Command",args.command]:["/d","/s","/c",args.command]):["-lc",args.command];
|
|
141
|
+
try{const r=spawnSync(file,a,{cwd,encoding:"utf8",timeout:60000});return ((r.stdout||"")+(r.stderr?"\n[stderr]\n"+r.stderr:"")).slice(0,8000)||"(无输出)";}
|
|
142
|
+
catch(e){return `执行失败: ${e.message}`;}
|
|
143
|
+
}
|
|
144
|
+
if(name==="write_file"){try{const p=path.resolve(cwd,args.path);fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,args.content);return `已写入 ${p}`;}catch(e){return `写入失败: ${e.message}`;}}
|
|
145
|
+
if(name==="read_file"){try{return fs.readFileSync(path.resolve(cwd,args.path),"utf8").slice(0,8000);}catch(e){return `读取失败: ${e.message}`;}}
|
|
146
|
+
if(name==="use_skill"){const s=skills.get(args.skill_name);return s?`技能已加载:${s.name}\n\n${s.content}`:`技能不存在:${args.skill_name}`;}
|
|
147
|
+
return "未知工具";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function streamChat(){
|
|
151
|
+
const body={model:config.model,messages,stream:true,tools:TOOLS,
|
|
152
|
+
max_tokens:Number(config.maxTokens)||4096,temperature:Number(config.temperature)||0.7,
|
|
153
|
+
thinking:{type:thinkingEnabled?"enabled":"disabled"}};
|
|
154
|
+
let res;
|
|
155
|
+
try{res=await fetch(`${config.baseUrl}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${apiKey}`},body:JSON.stringify(body)});}
|
|
156
|
+
catch(e){console.log(color(`\n[网络错误] ${e.message}`,C.red));return null;}
|
|
157
|
+
if(!res.ok){console.log(color(`\n[API错误 ${res.status}] ${(await res.text()).slice(0,500)}`,C.red));return null;}
|
|
158
|
+
let content="", calls=new Map();
|
|
159
|
+
console.log(color("\n◆ ",C.cyanB),{end:""});
|
|
160
|
+
const reader=res.body.getReader();let buf="";
|
|
161
|
+
while(true){
|
|
162
|
+
const {done,value}=await reader.read();if(done)break;
|
|
163
|
+
buf+=new TextDecoder().decode(value,{stream:true});
|
|
164
|
+
const lines=buf.split("\n");buf=lines.pop()||"";
|
|
165
|
+
for(let line of lines){
|
|
166
|
+
line=line.trim();if(!line.startsWith("data:"))continue;
|
|
167
|
+
const data=line.slice(5).trim();if(data==="[DONE]")continue;
|
|
168
|
+
try{
|
|
169
|
+
const d=JSON.parse(data).choices?.[0]?.delta||{};
|
|
170
|
+
if(d.reasoning_content&&thinkingEnabled)process.stdout.write(color(d.reasoning_content,C.gray));
|
|
171
|
+
if(d.content){content+=d.content;process.stdout.write(d.content);}
|
|
172
|
+
for(const tc of d.tool_calls||[]){
|
|
173
|
+
const i=tc.index??0;if(!calls.has(i))calls.set(i,{id:tc.id||"",name:"",arguments:""});
|
|
174
|
+
const x=calls.get(i);if(tc.function){x.name=tc.function.name||x.name;x.arguments+=tc.function.arguments||"";}
|
|
175
|
+
}
|
|
176
|
+
}catch{}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write("\n");
|
|
180
|
+
return {content,calls:[...calls.values()]};
|
|
181
|
+
}
|
|
182
|
+
async function chat(text){
|
|
183
|
+
if(!messages.length||messages[0].role!=="system")messages.unshift({role:"system",content:systemPrompt()});
|
|
184
|
+
messages.push({role:"user",content:text});
|
|
185
|
+
for(let n=0;n<8;n++){
|
|
186
|
+
const out=await streamChat();if(!out){messages.pop();return;}
|
|
187
|
+
if(!out.calls.length){if(out.content)messages.push({role:"assistant",content:out.content});return;}
|
|
188
|
+
messages.push({role:"assistant",content:out.content||null,tool_calls:out.calls.map(x=>({id:x.id,type:"function",function:{name:x.name,arguments:x.arguments}}))});
|
|
189
|
+
for(const t of out.calls){
|
|
190
|
+
let a={};try{a=JSON.parse(t.arguments||"{}");}catch{}
|
|
191
|
+
console.log(color(`[工具] ${t.name}`,C.gray));
|
|
192
|
+
const result=execTool(t.name,a);
|
|
193
|
+
messages.push({role:"tool",tool_call_id:t.id,content:result});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/* ---- 斜杠命令菜单:输入 / 即弹出 ---- */
|
|
198
|
+
function showMenu(prefix){
|
|
199
|
+
const p=(prefix||"/").trim();
|
|
200
|
+
const list=Object.entries(commands()).filter(([c])=>c.startsWith(p));
|
|
201
|
+
if(!list.length){console.log(color(`没有以 "${p}" 开头的命令`,C.red));return;}
|
|
202
|
+
console.log(color(`— 命令菜单 (${list.length}) —`,C.cyanB));
|
|
203
|
+
for(const [c,d] of list)console.log(` ${color(c,C.cyanB).padEnd(20)} ${color(d,C.gray)}`);
|
|
204
|
+
console.log("");
|
|
205
|
+
}
|
|
206
|
+
function handleCommand(line){
|
|
207
|
+
const [cmd,...rest]=line.trim().split(/\s+/), arg=rest.join(" ");
|
|
208
|
+
if(cmd.slice(1) && skills.has(cmd.slice(1))){const s=skills.get(cmd.slice(1));messages.push({role:"system",content:s.content});console.log(color(`已加载技能:${s.name}`,C.green));console.log(color("技能指令已注入,现在输入你的任务:",C.gray));return;}
|
|
209
|
+
if(cmd==="/help"){showMenu("/");}
|
|
210
|
+
else if(cmd==="/clear"){messages=[];console.log(color("已清空对话",C.green));}
|
|
211
|
+
else if(cmd==="/thinking"){thinkingEnabled=!thinkingEnabled;config.thinking=thinkingEnabled;saveConfig();console.log(color(`思考模式: ${thinkingEnabled?"开启":"关闭"}`,C.yellow));}
|
|
212
|
+
else if(cmd==="/model"){if(arg){config.model=arg;saveConfig();console.log(color(`模型已切换: ${arg}`,C.green));}else console.log(`当前模型: ${config.model}`);}
|
|
213
|
+
else if(cmd==="/pwd")console.log(cwd);
|
|
214
|
+
else if(cmd==="/cd"){try{cwd=path.resolve(cwd,arg||HOME);process.chdir(cwd);console.log(color(`-> ${cwd}`,C.green));}catch(e){console.log(color(`切换失败: ${e.message}`,C.red));}}
|
|
215
|
+
else if(cmd==="/ls"){try{for(const f of fs.readdirSync(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));}}
|
|
216
|
+
else if(cmd==="/cat"){try{console.log(fs.readFileSync(path.resolve(cwd,arg),"utf8").slice(0,8000));}catch(e){console.log(color(`失败: ${e.message}`,C.red));}}
|
|
217
|
+
else if(cmd==="/run"){if(!arg)console.log("用法: /run <命令>");else console.log(execTool("run_shell",{command:arg}));}
|
|
218
|
+
else if(cmd==="/install"){installProgram(arg)}
|
|
219
|
+
else if(cmd==="/exit"){console.log(color("再见",C.cyan));process.exit(0);}
|
|
220
|
+
else console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function logo(){
|
|
224
|
+
const rows=[" ██████╗ ██╗ ███╗ ███╗","██╔════╝ ██║ ████╗ ████║","██║ ███╗ ██║ ██╔████╔██║","██║ ██║ ██║ ██║╚██╔╝██║","╚██████╔╝ ███████╗ ██║ ╚═╝ ██║"," ╚═════╝ ╚══════╝ ╚═╝ ╚═╝"];
|
|
225
|
+
const W=44;console.log(color("".padStart(W),C.cyan));console.log(color("✧ GLM ✧".padStart((W+"✧ GLM ✧".length)/2),C.cyanB));for(const r of rows)console.log(color(r.padStart((W+r.length)/2),C.cyanB));console.log(color("GLM Code · Coding Agent",C.cyanB));console.log(color(`v${VERSION}`,C.gray));console.log(color("/help 查看命令 · /thinking 思考开关",C.gray));console.log("");
|
|
226
|
+
}
|
|
227
|
+
/* ---- 底部状态栏 ---- */
|
|
228
|
+
function toolbar(){return `┌ 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${cwd} ┐`;}
|
|
229
|
+
function completer(line){
|
|
230
|
+
if(!line.startsWith("/"))return [[],[]];
|
|
231
|
+
const list=Object.entries(commands()).filter(([c])=>c.startsWith(line)).map(([c,d])=>`${c} — ${d}`);
|
|
232
|
+
return [list.length?list:[line], list.map(v=>v.split(" — ")[0])];
|
|
233
|
+
}
|
|
234
|
+
function promptLoop(){
|
|
235
|
+
const rl=readline.createInterface({input:process.stdin,output:process.stdout,completer});
|
|
236
|
+
rl.setPrompt(color("┃ 输入消息( / 弹菜单, Tab 补全) ┃ ",C.cyanB));
|
|
237
|
+
function next(){console.log(color(toolbar(),C.gray));rl.prompt();}
|
|
238
|
+
next();
|
|
239
|
+
rl.on("line",async line=>{
|
|
240
|
+
const text=line.trim();if(!text){next();return;}
|
|
241
|
+
if(text.startsWith("/")){
|
|
242
|
+
const cmds=commands();
|
|
243
|
+
if(text!=="/"&&cmds[text])handleCommand(text);
|
|
244
|
+
else showMenu(text==="/"?"/":text);
|
|
245
|
+
}else{rl.pause();await chat(text);rl.resume();}
|
|
246
|
+
next();
|
|
247
|
+
});
|
|
248
|
+
rl.on("SIGINT",()=>{if(stopping){rl.close();process.exit(0);}stopping=true;console.log(color("\n[再次 Ctrl+C 退出]",C.yellow));setTimeout(()=>stopping=false,1200);rl.prompt();});
|
|
249
|
+
rl.on("close",()=>process.exit(0));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
function detectInstaller(){
|
|
254
|
+
if(process.platform==="win32")return "winget";
|
|
255
|
+
if(process.platform==="darwin")return "brew";
|
|
256
|
+
for(const pm of ["apk","apt-get","apt","dnf","pacman","pkg","termux-pkg"]){
|
|
257
|
+
if(spawnSync("sh",["-lc",`command -v ${pm}`],{encoding:"utf8"}).status===0)return pm;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
function installProgram(name){
|
|
262
|
+
if(!name){console.log(color("用法: /install <程序>",C.yellow));return;}
|
|
263
|
+
const pm=detectInstaller();
|
|
264
|
+
if(!pm){console.log(color("未检测到可用的软件包管理器",C.red));return;}
|
|
265
|
+
const q={apk:`apk add ${name}`,"apt-get":`apt-get install -y ${name}`,apt:`apt install -y ${name}`,dnf:`dnf install -y ${name}`,pacman:`pacman -S --noconfirm ${name}`,pkg:`pkg install -y ${name}`,brew:`brew install ${name}`,winget:`winget install ${name}`}[pm];
|
|
266
|
+
console.log(color(`[安装] ${pm}: ${q}`,C.gray));
|
|
267
|
+
const r=spawnSync(q,{shell:true,stdio:"inherit",cwd:process.cwd()});
|
|
268
|
+
console.log(r.status===0?color("安装完成",C.green):color(`安装失败: ${r.status}`,C.red));
|
|
269
|
+
}
|
|
270
|
+
async function main(){
|
|
271
|
+
loadConfig();const a=loadAuth();if(!a){console.log(color("未检测到 GLM API Key。",C.red));console.log(`API Key 页面: ${API_KEYS_URL}`);console.log("获得 Key 后运行: glmcode auth set <API_KEY>");process.exit(1);}
|
|
272
|
+
apiKey=a.key;await ensureDeps();await loadSkills();logo();console.log(color(`API: ${mask(apiKey)} (${a.source})`,C.green));console.log(color(`模型: ${config.model} 技能: ${skills.size}`,C.gray));console.log("");
|
|
273
|
+
promptLoop();
|
|
274
|
+
}
|
|
275
|
+
const argv=process.argv.slice(2);
|
|
276
|
+
if(argv[0]==="--version"||argv[0]==="-v"){console.log(VERSION);process.exit(0);}
|
|
277
|
+
if(argv[0]==="--help"||argv[0]==="-h"){console.log(`GLMCode ${VERSION}
|
|
278
|
+
启动: glmcode
|
|
279
|
+
斜杠命令: /help /clear /thinking /model /pwd /cd /ls /cat /run /install /exit
|
|
280
|
+
输入 / 或前缀自动弹出命令菜单,Tab 补全,底部常驻状态栏。
|
|
281
|
+
技能: 将 SKILL.md 目录或 .zip 包放入 skills/ 自动加载。
|
|
282
|
+
认证: glmcode auth set <API_KEY> | auth status | auth logout | auth login
|
|
283
|
+
Shell: glmcode shell | shell list`);process.exit(0);}
|
|
284
|
+
if(argv[0]==="auth"){const s=argv[1]||"status";if(s==="set"){if(!argv[2]){console.error("用法: glmcode auth set <API_KEY>");process.exit(2);}saveJson(AUTH_FILE,{provider:"zhipu",apiKey:argv[2],updatedAt:new Date().toISOString()});console.log(`API Key 已保存到 ${AUTH_FILE}`);}
|
|
285
|
+
else if(s==="status"){const a=loadAuth();console.log(a?`API Key: ${mask(a.key)}\n来源: ${a.source}`:"未检测到 API Key。");}
|
|
286
|
+
else if(s==="logout"){saveJson(AUTH_FILE,{});console.log("本地 API Key 已删除。");}
|
|
287
|
+
else if(s==="login"){console.log(`API Key 页面: ${API_KEYS_URL}`);if(openUrl(API_KEYS_URL))console.log("已尝试打开浏览器。");else console.log("当前环境无法自动打开浏览器。");}process.exit(0);}
|
|
288
|
+
if(argv[0]==="shell"){const s=shellInfo();if(argv[1]==="list"){const c=process.platform==="win32"?["cmd","powershell","pwsh"]:["sh","bash","zsh","fish","ash"];console.log(c.filter(commandExists).join("\n"));}else console.log(`OS: ${process.platform}\nArchitecture: ${process.arch}\nShell: ${s.name}\nExecutable: ${s.exe}`);process.exit(0);}
|
|
289
|
+
await main();
|
package/src/auth.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
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";
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
function readJson(file, fallback = {}) {
|
|
11
|
-
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return fallback; }
|
|
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; }
|
|
19
|
-
|
|
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;
|
|
32
|
-
}
|
|
33
|
-
export function clearAuth() { writeJson(AUTH_FILE,{}); }
|
|
34
|
-
|
|
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 {}
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
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;
|
|
61
|
-
}
|
|
62
|
-
export function maskKey(k) { return k && k.length>8 ? `${k.slice(0,4)}...${k.slice(-4)}` : "********"; }
|
package/src/config.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
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;}});}
|