glmcode 0.1.5 → 0.1.8
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 +12 -6
- package/LICENSE +1 -0
- package/README.md +45 -16
- package/package.json +13 -6
- package/src/cli.js +214 -72
- package/glmcode-0.1.4.tgz +0 -0
- package/glmcode-0.1.5.tgz +0 -0
- package/src/agent.js +0 -46
- package/src/auth.js +0 -40
- package/src/config.js +0 -17
- package/src/mcp.js +0 -13
- package/src/platform/browser.js +0 -30
- package/src/platform/shell.js +0 -64
- package/src/skills.js +0 -17
- package/src/tools.js +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
### Skills 路径修正
|
|
6
|
+
|
|
7
|
+
- Skills 目录改为相对于主 CLI 文件位置解析,并在启动时自动创建
|
|
8
|
+
- 按原版 glmcode.py 重构交互式 coding agent
|
|
9
|
+
- 增加流式 GLM Chat Completions
|
|
10
|
+
- 增加 run_shell / read_file / write_file / use_skill
|
|
11
|
+
- 增加 Skills 自动扫描和斜杠调用
|
|
12
|
+
- 输入 `/` 后提供命令补全
|
|
13
|
+
- 增加底部状态栏:思考、模型、技能数量、当前目录
|
|
14
|
+
- 保留跨平台 Shell 和 API Key 自动检测
|
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT License
|
package/README.md
CHANGED
|
@@ -1,29 +1,58 @@
|
|
|
1
|
-
# GLMCode 0.1.
|
|
1
|
+
# GLMCode 0.1.7
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
基于原版 `glmcode.py` 的 Node.js CLI 实现,重点保留其交互界面、斜杠命令、技能系统、流式输出和 coding-agent 工具。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
```bash
|
|
7
|
-
npm install -g glmcode
|
|
8
|
-
```
|
|
5
|
+
## 启动
|
|
9
6
|
|
|
10
|
-
Run:
|
|
11
7
|
```bash
|
|
8
|
+
npm install -g glmcode
|
|
12
9
|
glmcode
|
|
13
10
|
```
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
## API Key
|
|
13
|
+
|
|
14
|
+
检测顺序:
|
|
15
|
+
|
|
16
|
+
1. `ZHIPU_API_KEY`
|
|
17
|
+
2. `GLM_API_KEY`
|
|
18
|
+
3. `~/.glmcode/auth.json`
|
|
19
|
+
|
|
20
|
+
已有 Key:
|
|
16
21
|
|
|
17
|
-
Authentication:
|
|
18
22
|
```bash
|
|
19
|
-
glmcode auth
|
|
20
|
-
glmcode auth login --no-browser
|
|
23
|
+
glmcode auth set YOUR_API_KEY
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
## 交互
|
|
27
|
+
|
|
28
|
+
输入 `/` 后按 Tab 可补全命令;命令包括:
|
|
29
|
+
|
|
30
|
+
`/help` `/clear` `/thinking` `/model` `/pwd` `/cd` `/ls` `/cat` `/run` `/exit`
|
|
31
|
+
|
|
32
|
+
Skills 目录由程序自动创建,位置固定为**主 CLI 文件所在目录的上一级目录中的 `skills` 子目录**。
|
|
33
|
+
|
|
34
|
+
npm 安装后通常是:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
.../glmcode/
|
|
38
|
+
├── src/
|
|
39
|
+
│ └── cli.js
|
|
40
|
+
└── skills/
|
|
41
|
+
├── coding/
|
|
42
|
+
│ └── SKILL.md
|
|
43
|
+
└── git/
|
|
44
|
+
└── SKILL.md
|
|
27
45
|
```
|
|
28
46
|
|
|
29
|
-
|
|
47
|
+
程序启动时自动创建 `skills/`,然后只检测这个目录的**直接子目录**;每个子目录中的 `SKILL.md` 会注册成一个斜杠命令,例如 `/coding`。
|
|
48
|
+
|
|
49
|
+
AI 可调用:
|
|
50
|
+
|
|
51
|
+
- `run_shell`
|
|
52
|
+
- `read_file`
|
|
53
|
+
- `write_file`
|
|
54
|
+
- `use_skill`
|
|
55
|
+
|
|
56
|
+
## 跨平台
|
|
57
|
+
|
|
58
|
+
根据实际系统选择 Windows CMD/PowerShell、macOS/Linux/Alpine/Android 可用 Shell;打开网页登录时不会强制依赖 `termux-open-url`。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glmcode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "GLM coding agent CLI with shell tools, skills, streaming chat and slash-command completion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"glmcode": "src/cli.js",
|
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
"gc": "src/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"
|
|
13
|
-
"check": "node --check src/cli.js && node --check src/auth.js && node --check src/platform/shell.js && node --check src/platform/browser.js"
|
|
12
|
+
"check": "node --check src/cli.js && npm pack --dry-run"
|
|
14
13
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
14
|
+
"files": [
|
|
15
|
+
"src/",
|
|
16
|
+
"skills/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
}
|
|
17
24
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,88 +1,230 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import readline from "node:readline";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { getConfig, saveConfig } from "./config.js";
|
|
6
|
-
import { discoverSkills } from "./skills.js";
|
|
7
|
-
import { detectPlatform, detectShell, listAvailableShells } from "./platform/shell.js";
|
|
6
|
+
import {spawnSync, spawn} from "node:child_process";
|
|
7
|
+
import {fileURLToPath} from "node:url";
|
|
8
8
|
|
|
9
|
-
const VERSION
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const VERSION="0.1.8";
|
|
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
|
+
};
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
23
59
|
}
|
|
24
60
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/exit 退出
|
|
38
|
-
`);
|
|
61
|
+
function loadSkills(){
|
|
62
|
+
skills=new Map(); fs.mkdirSync(SKILLS_DIR,{recursive:true});
|
|
63
|
+
for(const entry of fs.readdirSync(SKILLS_DIR,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))){
|
|
64
|
+
if(!entry.isDirectory())continue;
|
|
65
|
+
const file=path.join(SKILLS_DIR,entry.name,"SKILL.md"); if(!fs.existsSync(file))continue;
|
|
66
|
+
const content=fs.readFileSync(file,"utf8");
|
|
67
|
+
let name=entry.name, description="";
|
|
68
|
+
const fm=content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
69
|
+
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();}
|
|
70
|
+
if(!description)description=(content.trim().split("\n")[0]||name).replace(/^#\s*/,"").slice(0,60);
|
|
71
|
+
skills.set(name,{name,description,content});
|
|
72
|
+
}
|
|
39
73
|
}
|
|
74
|
+
const BASE_COMMANDS={
|
|
75
|
+
"/help":"显示全部命令","/clear":"清空对话","/thinking":"切换思考模式(开/关)",
|
|
76
|
+
"/model":"切换模型 /model <ID>","/pwd":"显示当前目录","/cd":"切换目录 /cd <路径>",
|
|
77
|
+
"/ls":"列出文件","/cat":"查看文件 /cat <路径>","/run":"执行命令 /run <shell>","/install":"安装程序/依赖 /install <程序>","/exit":"退出"
|
|
78
|
+
};
|
|
79
|
+
function commands(){const d={...BASE_COMMANDS};for(const s of skills.values())d["/"+s.name]="技能: "+s.description;return d;}
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if
|
|
81
|
+
function systemPrompt(){
|
|
82
|
+
let s="你是 GLMCode,一个运行在用户本机终端中的 coding agent。你可以使用 run_shell、write_file、read_file 和 use_skill 工具。回答简洁,执行任务时先理解当前工作目录。";
|
|
83
|
+
if(skills.size){s+="\n可用技能:\n";for(const x of skills.values())s+=`- ${x.name}: ${x.description}\n`;}
|
|
84
|
+
return s;
|
|
85
|
+
}
|
|
86
|
+
const TOOLS=[
|
|
87
|
+
{type:"function",function:{name:"run_shell",description:"在当前操作系统的用户 Shell 中执行命令并返回输出,限时60秒。",
|
|
88
|
+
parameters:{type:"object",properties:{command:{type:"string"}},required:["command"]}}},
|
|
89
|
+
{type:"function",function:{name:"write_file",description:"写入文本文件并自动创建父目录。",
|
|
90
|
+
parameters:{type:"object",properties:{path:{type:"string"},content:{type:"string"}},required:["path","content"]}}},
|
|
91
|
+
{type:"function",function:{name:"read_file",description:"读取文本文件内容。",
|
|
92
|
+
parameters:{type:"object",properties:{path:{type:"string"}},required:["path"]}}},
|
|
93
|
+
{type:"function",function:{name:"use_skill",description:"加载指定技能的 SKILL.md 并按其说明执行。",
|
|
94
|
+
parameters:{type:"object",properties:{skill_name:{type:"string"}},required:["skill_name"]}}}
|
|
95
|
+
];
|
|
44
96
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
97
|
+
function execTool(name,args){
|
|
98
|
+
if(name==="run_shell"){
|
|
99
|
+
const s=shellInfo(), isWin=process.platform==="win32";
|
|
100
|
+
const file=isWin?s.exe:s.exe, a=isWin?(s.name==="powershell"?["-NoProfile","-Command",args.command]:["/d","/s","/c",args.command]):["-lc",args.command];
|
|
101
|
+
try{const r=spawnSync(file,a,{cwd,encoding:"utf8",timeout:60000});return ((r.stdout||"")+(r.stderr?"\n[stderr]\n"+r.stderr:"")).slice(0,8000)||"(无输出)";}
|
|
102
|
+
catch(e){return `执行失败: ${e.message}`;}
|
|
48
103
|
}
|
|
49
|
-
if (args
|
|
50
|
-
|
|
51
|
-
|
|
104
|
+
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}`;}}
|
|
105
|
+
if(name==="read_file"){try{return fs.readFileSync(path.resolve(cwd,args.path),"utf8").slice(0,8000);}catch(e){return `读取失败: ${e.message}`;}}
|
|
106
|
+
if(name==="use_skill"){const s=skills.get(args.skill_name);return s?`技能已加载:${s.name}\n\n${s.content}`:`技能不存在:${args.skill_name}`;}
|
|
107
|
+
return "未知工具";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function streamChat(){
|
|
111
|
+
const body={model:config.model,messages,stream:true,tools:TOOLS,
|
|
112
|
+
max_tokens:Number(config.maxTokens)||4096,temperature:Number(config.temperature)||0.7,
|
|
113
|
+
thinking:{type:thinkingEnabled?"enabled":"disabled"}};
|
|
114
|
+
let res;
|
|
115
|
+
try{res=await fetch(`${config.baseUrl}/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json","Authorization":`Bearer ${apiKey}`},body:JSON.stringify(body)});}
|
|
116
|
+
catch(e){console.log(color(`\n[网络错误] ${e.message}`,C.red));return null;}
|
|
117
|
+
if(!res.ok){console.log(color(`\n[API错误 ${res.status}] ${(await res.text()).slice(0,500)}`,C.red));return null;}
|
|
118
|
+
let content="", calls=new Map();
|
|
119
|
+
console.log(color("\n◆ ",C.cyanB),{end:""});
|
|
120
|
+
const reader=res.body.getReader();let buf="";
|
|
121
|
+
while(true){
|
|
122
|
+
const {done,value}=await reader.read();if(done)break;
|
|
123
|
+
buf+=new TextDecoder().decode(value,{stream:true});
|
|
124
|
+
const lines=buf.split("\n");buf=lines.pop()||"";
|
|
125
|
+
for(let line of lines){
|
|
126
|
+
line=line.trim();if(!line.startsWith("data:"))continue;
|
|
127
|
+
const data=line.slice(5).trim();if(data==="[DONE]")continue;
|
|
128
|
+
try{
|
|
129
|
+
const d=JSON.parse(data).choices?.[0]?.delta||{};
|
|
130
|
+
if(d.reasoning_content&&thinkingEnabled)process.stdout.write(color(d.reasoning_content,C.gray));
|
|
131
|
+
if(d.content){content+=d.content;process.stdout.write(d.content);}
|
|
132
|
+
for(const tc of d.tool_calls||[]){
|
|
133
|
+
const i=tc.index??0;if(!calls.has(i))calls.set(i,{id:tc.id||"",name:"",arguments:""});
|
|
134
|
+
const x=calls.get(i);if(tc.function){x.name=tc.function.name||x.name;x.arguments+=tc.function.arguments||"";}
|
|
135
|
+
}
|
|
136
|
+
}catch{}
|
|
137
|
+
}
|
|
52
138
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
139
|
+
process.stdout.write("\n");
|
|
140
|
+
return {content,calls:[...calls.values()]};
|
|
141
|
+
}
|
|
142
|
+
async function chat(text){
|
|
143
|
+
if(!messages.length||messages[0].role!=="system")messages.unshift({role:"system",content:systemPrompt()});
|
|
144
|
+
messages.push({role:"user",content:text});
|
|
145
|
+
for(let n=0;n<8;n++){
|
|
146
|
+
const out=await streamChat();if(!out){messages.pop();return;}
|
|
147
|
+
if(!out.calls.length){if(out.content)messages.push({role:"assistant",content:out.content});return;}
|
|
148
|
+
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}}))});
|
|
149
|
+
for(const t of out.calls){
|
|
150
|
+
let a={};try{a=JSON.parse(t.arguments||"{}");}catch{}
|
|
151
|
+
console.log(color(`[工具] ${t.name}`,C.gray));
|
|
152
|
+
const result=execTool(t.name,a);
|
|
153
|
+
messages.push({role:"tool",tool_call_id:t.id,content:result});
|
|
154
|
+
}
|
|
57
155
|
}
|
|
156
|
+
}
|
|
157
|
+
function handleCommand(line){
|
|
158
|
+
const [cmd,...rest]=line.trim().split(/\s+/), arg=rest.join(" ");
|
|
159
|
+
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;}
|
|
160
|
+
if(cmd==="/help"){console.log("");for(const [c,d] of Object.entries(commands()))console.log(` ${color(c,C.cyanB).padEnd(24)} ${color(d,C.gray)}`);console.log("");}
|
|
161
|
+
else if(cmd==="/clear"){messages=[];console.log(color("已清空对话",C.green));}
|
|
162
|
+
else if(cmd==="/thinking"){thinkingEnabled=!thinkingEnabled;config.thinking=thinkingEnabled;saveConfig();console.log(color(`思考模式: ${thinkingEnabled?"开启":"关闭"}`,C.yellow));}
|
|
163
|
+
else if(cmd==="/model"){if(arg){config.model=arg;saveConfig();console.log(color(`模型已切换: ${arg}`,C.green));}else console.log(`当前模型: ${config.model}`);}
|
|
164
|
+
else if(cmd==="/pwd")console.log(cwd);
|
|
165
|
+
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));}}
|
|
166
|
+
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));}}
|
|
167
|
+
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));}}
|
|
168
|
+
else if(cmd==="/run"){if(!arg)console.log("用法: /run <命令>");else console.log(execTool("run_shell",{command:arg}));}
|
|
169
|
+
else if(cmd==="/install"){installProgram(arg)}
|
|
170
|
+
else if(cmd==="/exit"){console.log(color("再见",C.cyan));process.exit(0);}
|
|
171
|
+
else console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));
|
|
172
|
+
}
|
|
58
173
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.log(`
|
|
62
|
-
|
|
63
|
-
|
|
174
|
+
function logo(){
|
|
175
|
+
const rows=[" ██████╗ ██╗ ███╗ ███╗","██╔════╝ ██║ ████╗ ████║","██║ ███╗ ██║ ██╔████╔██║","██║ ██║ ██║ ██║╚██╔╝██║","╚██████╔╝ ███████╗ ██║ ╚═╝ ██║"," ╚═════╝ ╚══════╝ ╚═╝ ╚═╝"];
|
|
176
|
+
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("");
|
|
177
|
+
}
|
|
178
|
+
function toolbar(){return `思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} ${cwd}`;}
|
|
179
|
+
function completer(line){
|
|
180
|
+
if(!line.startsWith("/"))return [];
|
|
181
|
+
return Object.entries(commands()).filter(([c])=>c.startsWith(line)).map(([c,d])=>`${c} — ${d}`);
|
|
182
|
+
}
|
|
183
|
+
function promptLoop(){
|
|
184
|
+
const rl=readline.createInterface({input:process.stdin,output:process.stdout,completer:(line)=>{const x=completer(line);return [x,x.map(v=>v.split(" — ")[0])];}});
|
|
185
|
+
rl.setPrompt(color("┃ 输入消息... ┃ ",C.cyanB));rl.prompt();
|
|
186
|
+
rl.on("line",async line=>{
|
|
187
|
+
const text=line.trim();if(!text){rl.prompt();return;}
|
|
188
|
+
if(text.startsWith("/"))handleCommand(text);else{rl.pause();await chat(text);rl.resume();}
|
|
189
|
+
console.log(color(toolbar(),C.gray));rl.prompt();
|
|
190
|
+
});
|
|
191
|
+
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();});
|
|
192
|
+
rl.on("close",()=>process.exit(0));
|
|
193
|
+
}
|
|
64
194
|
|
|
65
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "\n> " });
|
|
66
|
-
const agent = createAgent({ getConfig });
|
|
67
|
-
rl.prompt();
|
|
68
195
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
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();
|
|
84
|
-
});
|
|
85
|
-
rl.on("close", () => process.exit(0));
|
|
196
|
+
function detectInstaller(){
|
|
197
|
+
if(process.platform==="win32")return "winget";
|
|
198
|
+
if(process.platform==="darwin")return "brew";
|
|
199
|
+
for(const pm of ["apk","apt-get","apt","dnf","pacman","pkg"]){
|
|
200
|
+
if(spawnSync("sh",["-lc",`command -v ${pm}`],{encoding:"utf8"}).status===0)return pm;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
86
203
|
}
|
|
87
|
-
|
|
88
|
-
|
|
204
|
+
function installProgram(name){
|
|
205
|
+
if(!name){console.log(color("用法: /install <程序>",C.yellow));return;}
|
|
206
|
+
const pm=detectInstaller();
|
|
207
|
+
if(!pm){console.log(color("未检测到可用的软件包管理器",C.red));return;}
|
|
208
|
+
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];
|
|
209
|
+
console.log(color(`[安装] ${pm}: ${q}`,C.gray));
|
|
210
|
+
const r=spawnSync(q,{shell:true,stdio:"inherit",cwd:process.cwd()});
|
|
211
|
+
console.log(r.status===0?color("安装完成",C.green):color(`安装失败: ${r.status}`,C.red));
|
|
212
|
+
}
|
|
213
|
+
async function main(){
|
|
214
|
+
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);}
|
|
215
|
+
apiKey=a.key;loadSkills();logo();console.log(color(`API: ${mask(apiKey)} (${a.source})`,C.green));console.log(color(`模型: ${config.model} 技能: ${skills.size}`,C.gray));console.log("");
|
|
216
|
+
promptLoop();
|
|
217
|
+
}
|
|
218
|
+
const argv=process.argv.slice(2);
|
|
219
|
+
if(argv[0]==="--version"||argv[0]==="-v"){console.log(VERSION);process.exit(0);}
|
|
220
|
+
if(argv[0]==="--help"||argv[0]==="-h"){console.log(`GLMCode ${VERSION}
|
|
221
|
+
启动: glmcode
|
|
222
|
+
斜杠命令: /help /clear /thinking /model /pwd /cd /ls /cat /run /exit
|
|
223
|
+
认证: glmcode auth set <API_KEY> | auth status | auth logout
|
|
224
|
+
Shell: glmcode shell | shell list`);process.exit(0);}
|
|
225
|
+
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}`);}
|
|
226
|
+
else if(s==="status"){const a=loadAuth();console.log(a?`API Key: ${mask(a.key)}\n来源: ${a.source}`:"未检测到 API Key。");}
|
|
227
|
+
else if(s==="logout"){saveJson(AUTH_FILE,{});console.log("本地 API Key 已删除。");}
|
|
228
|
+
else if(s==="login"){console.log(`API Key 页面: ${API_KEYS_URL}`);if(openUrl(API_KEYS_URL))console.log("已尝试打开浏览器。");else console.log("当前环境无法自动打开浏览器。");}process.exit(0);}
|
|
229
|
+
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);}
|
|
230
|
+
await main();
|
package/glmcode-0.1.4.tgz
DELETED
|
Binary file
|
package/glmcode-0.1.5.tgz
DELETED
|
Binary file
|
package/src/agent.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { getApiKey } from "./auth.js";
|
|
2
|
-
import { discoverSkills } from "./skills.js";
|
|
3
|
-
|
|
4
|
-
const endpoint = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
|
|
5
|
-
|
|
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
|
-
})
|
|
23
|
-
});
|
|
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
|
-
}
|
|
43
|
-
}
|
|
44
|
-
process.stdout.write("\n");
|
|
45
|
-
}};
|
|
46
|
-
}
|
package/src/auth.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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/config.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
const dir = join(homedir(), ".glmcode");
|
|
6
|
-
const file = join(dir, "config.json");
|
|
7
|
-
|
|
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 }; }
|
|
11
|
-
}
|
|
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;
|
|
17
|
-
}
|
package/src/mcp.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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));
|
|
13
|
-
}
|
package/src/platform/browser.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
}
|
package/src/platform/shell.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
|
|
5
|
-
export function discoverSkills() {
|
|
6
|
-
const roots = [join(process.cwd(), ".glmcode", "skills"), join(homedir(), ".glmcode", "skills")];
|
|
7
|
-
const out = [];
|
|
8
|
-
for (const root of roots) {
|
|
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
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return out;
|
|
17
|
-
}
|
package/src/tools.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
});
|
|
14
|
-
}
|