glmcode 0.1.9 → 0.1.11
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 +14 -0
- package/README.md +3 -3
- package/package.json +4 -4
- package/src/cli.js +135 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.1.11 (2026-08-28)
|
|
4
|
+
|
|
5
|
+
- **真 TUI 交互层**:引入 `terminal-kit`,彻底替换 readline
|
|
6
|
+
- 输入 `/` 立即弹出**悬浮命令菜单**(不回车也显示),输入字母**实时过滤**命令
|
|
7
|
+
- **↑/↓ 方向键**直接选择菜单中的命令,Enter 执行,Tab 补全
|
|
8
|
+
- 底部**常驻状态栏**(反色条):思考开关 / 模型 / 技能数 / 当前目录
|
|
9
|
+
- 支持命令历史(↑/↓ 无菜单时浏览)、Home/End/DEL、Ctrl+L 清屏
|
|
10
|
+
- 修复 `term.height` 未就绪时菜单无法绘制的问题
|
|
11
|
+
|
|
12
|
+
## 0.1.10 (2026-08-28)
|
|
13
|
+
|
|
14
|
+
- 移除 `postinstall` 脚本,消除 npm `install-scripts / allowScripts` 警告
|
|
15
|
+
- 依赖 `adm-zip` 仍由 `dependencies` 自动安装,运行时 `ensureDeps` 兜底不变
|
|
16
|
+
|
|
3
17
|
## 0.1.9 (2026-08-28)
|
|
4
18
|
|
|
5
19
|
- 输入 `/` 或前缀自动弹出斜杠命令菜单(含技能命令)
|
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ npm install -g glmcode
|
|
|
11
11
|
- 流式对话(支持思考模式)
|
|
12
12
|
- 四个内置工具:`run_shell` / `write_file` / `read_file` / `use_skill`
|
|
13
13
|
- 技能系统:把 `SKILL.md` 目录或 `.zip` 技能包放进 `skills/` 自动加载
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- 自动安装依赖:缺失 `adm-zip` 等依赖时启动自动 `npm install`
|
|
14
|
+
- **真 TUI 交互**:输入 `/` 立即弹出悬浮命令菜单(不回车也显示),按字母实时过滤,↑/↓ 方向键选择,Tab 补全,Enter 执行
|
|
15
|
+
- **底部常驻状态栏**(反色条):实时显示思考开关 / 模型 / 技能数 / 当前目录
|
|
16
|
+
- 自动安装依赖:缺失 `adm-zip`、`terminal-kit` 等依赖时启动自动 `npm install`
|
|
17
17
|
- `/install` 命令:调用系统包管理器(apt/apk/pkg/brew/winget 等)自动安装程序
|
|
18
18
|
- 认证:`glmcode auth set <API_KEY>`
|
|
19
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glmcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
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": {
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
"gc": "src/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
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')}\""
|
|
12
|
+
"check": "node --check src/cli.js && npm pack --dry-run"
|
|
14
13
|
},
|
|
15
14
|
"files": [
|
|
16
15
|
"src/",
|
|
@@ -20,7 +19,8 @@
|
|
|
20
19
|
"LICENSE"
|
|
21
20
|
],
|
|
22
21
|
"dependencies": {
|
|
23
|
-
"adm-zip": "^0.5.16"
|
|
22
|
+
"adm-zip": "^0.5.16",
|
|
23
|
+
"terminal-kit": "^3.1.4"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
package/src/cli.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import readline from "node:readline";
|
|
6
5
|
import {spawnSync, spawn} from "node:child_process";
|
|
7
6
|
import {fileURLToPath} from "node:url";
|
|
7
|
+
import termkit from "terminal-kit";
|
|
8
|
+
const term = termkit.terminal;
|
|
8
9
|
|
|
9
|
-
const VERSION="0.1.
|
|
10
|
+
const VERSION="0.1.11";
|
|
10
11
|
const BASE_URL="https://open.bigmodel.cn/api/paas/v4";
|
|
11
12
|
const API_KEYS_URL="https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys";
|
|
12
13
|
const HOME=os.homedir();
|
|
@@ -28,7 +29,6 @@ const DEFAULT_CONFIG={
|
|
|
28
29
|
|
|
29
30
|
let config={...DEFAULT_CONFIG};
|
|
30
31
|
let apiKey=null, messages=[], cwd=process.cwd(), thinkingEnabled=false, skills=new Map();
|
|
31
|
-
let stopping=false;
|
|
32
32
|
|
|
33
33
|
function readJson(file,fallback={}){try{return JSON.parse(fs.readFileSync(file,"utf8"));}catch{return fallback;}}
|
|
34
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{}}
|
|
@@ -62,6 +62,7 @@ function openUrl(url){
|
|
|
62
62
|
async function ensureDeps(){
|
|
63
63
|
const missing=[];
|
|
64
64
|
try{await import("adm-zip");}catch{missing.push("adm-zip");}
|
|
65
|
+
try{await import("terminal-kit");}catch{missing.push("terminal-kit");}
|
|
65
66
|
if(!missing.length)return true;
|
|
66
67
|
console.log(color(`[glmcode] 缺少依赖: ${missing.join(", ")},正在自动安装...`,C.yellow));
|
|
67
68
|
let ok=true;
|
|
@@ -84,7 +85,6 @@ async function extractZipSkills(){
|
|
|
84
85
|
if(fs.existsSync(target))continue;
|
|
85
86
|
try{
|
|
86
87
|
new AdmZip(zp).extractAllTo(target,true);
|
|
87
|
-
// 扁平化:zip 内带同名根目录时(如 my-skill/SKILL.md)解压会出现 target/my-skill/... 嵌套,上移一层
|
|
88
88
|
const sub=fs.readdirSync(target,{withFileTypes:true}).find(d=>d.isDirectory());
|
|
89
89
|
if(sub && !fs.existsSync(path.join(target,"SKILL.md")) && fs.existsSync(path.join(target,sub.name,"SKILL.md"))){
|
|
90
90
|
for(const e of fs.readdirSync(path.join(target,sub.name))){
|
|
@@ -194,7 +194,7 @@ async function chat(text){
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
/* ----
|
|
197
|
+
/* ---- 斜杠命令处理(/help 文本模式) ---- */
|
|
198
198
|
function showMenu(prefix){
|
|
199
199
|
const p=(prefix||"/").trim();
|
|
200
200
|
const list=Object.entries(commands()).filter(([c])=>c.startsWith(p));
|
|
@@ -216,44 +216,145 @@ function handleCommand(line){
|
|
|
216
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
217
|
else if(cmd==="/run"){if(!arg)console.log("用法: /run <命令>");else console.log(execTool("run_shell",{command:arg}));}
|
|
218
218
|
else if(cmd==="/install"){installProgram(arg)}
|
|
219
|
-
else if(cmd==="/exit"){console.log(color("再见",C.cyan));process.exit(0);}
|
|
219
|
+
else if(cmd==="/exit"){term.grabInput(false);term("\n");console.log(color("再见",C.cyan));process.exit(0);}
|
|
220
220
|
else console.log(color(`未知命令: ${cmd}(输入 / 查看全部)`,C.red));
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
function logo(){
|
|
224
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("/
|
|
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("输入 / 弹菜单 · ↑↓ 选择 · 回车执行 · Ctrl+C 退出",C.gray));console.log("");
|
|
226
226
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
|
|
228
|
+
/* =============== 真 TUI:悬浮菜单 + 上下键 + 实时过滤 + 底部状态栏 =============== */
|
|
229
|
+
const PROMPT="┃ 输入( / 菜单 · ↑↓选择 · Enter执行) ┃ ";
|
|
230
|
+
const TUI={input:"",cursor:0,items:[],sel:0,scroll:0,history:[],histIdx:-1};
|
|
231
|
+
function statusText(){return ` 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${cwd} `;}
|
|
232
|
+
function filterItems(input){
|
|
233
|
+
const s=(input||"").trim();
|
|
234
|
+
if(!s.startsWith("/"))return [];
|
|
235
|
+
const name=s.slice(1).toLowerCase();
|
|
236
|
+
const list=Object.entries(commands());
|
|
237
|
+
if(!name)return list.map(([c,d])=>`${c} ${d}`);
|
|
238
|
+
return list.filter(([c,d])=>{
|
|
239
|
+
const n=c.slice(1).toLowerCase();
|
|
240
|
+
return n.startsWith(name)||n.includes(name)||d.toLowerCase().includes(name);
|
|
241
|
+
}).map(([c,d])=>`${c} ${d}`);
|
|
233
242
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
243
|
+
function menuMax(){const h=term.height>0?term.height:24;return Math.max(1,Math.min(10,h-3));}
|
|
244
|
+
function keepSelVisible(){
|
|
245
|
+
const MAX=menuMax();
|
|
246
|
+
if(TUI.sel<TUI.scroll)TUI.scroll=TUI.sel;
|
|
247
|
+
else if(TUI.sel>=TUI.scroll+MAX)TUI.scroll=TUI.sel-MAX+1;
|
|
248
|
+
}
|
|
249
|
+
function render(){
|
|
250
|
+
const h=term.height>0?term.height:24, w=term.width>0?term.width:80;
|
|
251
|
+
const inputRow=h-1, MAX=menuMax();
|
|
252
|
+
term.saveCursor();
|
|
253
|
+
// 底部状态栏(常驻)
|
|
254
|
+
term.moveTo(1,h);term.eraseLine();
|
|
255
|
+
const st=statusText().padEnd(Math.max(1,w-1));
|
|
256
|
+
term.inverse(st.slice(0,w));term.styleReset;
|
|
257
|
+
// 擦除菜单区
|
|
258
|
+
for(let j=0;j<MAX;j++){term.moveTo(1,inputRow-1-j);term.eraseLine();}
|
|
259
|
+
// 悬浮菜单
|
|
260
|
+
if(TUI.items.length){
|
|
261
|
+
const count=Math.min(TUI.items.length,MAX);
|
|
262
|
+
for(let j=0;j<count;j++){
|
|
263
|
+
const idx=TUI.scroll+j;
|
|
264
|
+
const row=inputRow-1-j;
|
|
265
|
+
term.moveTo(1,row);
|
|
266
|
+
const line=" "+TUI.items[idx]+" ";
|
|
267
|
+
if(idx===TUI.sel)term.bgBrightBlue.black(line.slice(0,w));
|
|
268
|
+
else term.gray(line.slice(0,w));
|
|
269
|
+
term.styleReset;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// 输入行
|
|
273
|
+
term.moveTo(1,inputRow);term.eraseLine();
|
|
274
|
+
term.cyan.bold(PROMPT);
|
|
275
|
+
const before=TUI.input.slice(0,TUI.cursor), after=TUI.input.slice(TUI.cursor);
|
|
276
|
+
term(before);term.cyan(after);term.styleReset;
|
|
277
|
+
// 光标
|
|
278
|
+
term.restoreCursor();
|
|
279
|
+
term.moveTo(Math.min(1+PROMPT.length+TUI.cursor,w),inputRow);
|
|
280
|
+
term.hideCursor(false);
|
|
281
|
+
}
|
|
282
|
+
function refreshFilter(){
|
|
283
|
+
TUI.items=filterItems(TUI.input);
|
|
284
|
+
if(TUI.sel>=TUI.items.length)TUI.sel=TUI.items.length?TUI.items.length-1:0;
|
|
285
|
+
keepSelVisible();
|
|
286
|
+
}
|
|
287
|
+
function tuiInput(){
|
|
288
|
+
return new Promise(resolve=>{
|
|
289
|
+
TUI.input="";TUI.cursor=0;TUI.items=[];TUI.sel=0;TUI.scroll=0;TUI.histIdx=-1;
|
|
290
|
+
term.grabInput(true);
|
|
291
|
+
render();
|
|
292
|
+
const onKey=(name,matches,data)=>{
|
|
293
|
+
if(data&&data.isCharacter&&name.length===1&&name.charCodeAt(0)>=32){
|
|
294
|
+
TUI.input=TUI.input.slice(0,TUI.cursor)+name+TUI.input.slice(TUI.cursor);
|
|
295
|
+
TUI.cursor++;refreshFilter();render();
|
|
296
|
+
}
|
|
297
|
+
else if(name==="UP"){
|
|
298
|
+
if(TUI.items.length){TUI.sel=TUI.sel>0?TUI.sel-1:TUI.items.length-1;keepSelVisible();}
|
|
299
|
+
else if(TUI.histIdx<TUI.history.length-1){TUI.histIdx++;TUI.input=TUI.history[TUI.history.length-1-TUI.histIdx];TUI.cursor=TUI.input.length;refreshFilter();}
|
|
300
|
+
render();
|
|
301
|
+
}
|
|
302
|
+
else if(name==="DOWN"){
|
|
303
|
+
if(TUI.items.length){TUI.sel=(TUI.sel+1)%TUI.items.length;keepSelVisible();}
|
|
304
|
+
else if(TUI.histIdx>0){TUI.histIdx--;TUI.input=TUI.history[TUI.history.length-1-TUI.histIdx];TUI.cursor=TUI.input.length;refreshFilter();}
|
|
305
|
+
else if(TUI.histIdx===0){TUI.histIdx=-1;TUI.input="";TUI.cursor=0;refreshFilter();}
|
|
306
|
+
render();
|
|
307
|
+
}
|
|
308
|
+
else if(name==="TAB"){
|
|
309
|
+
if(TUI.items.length){const chosen=TUI.items[TUI.sel].split(" ")[0];TUI.input=chosen;TUI.cursor=chosen.length;TUI.items=[];}
|
|
310
|
+
render();
|
|
311
|
+
}
|
|
312
|
+
else if(name==="ENTER"){
|
|
313
|
+
const v=TUI.input;
|
|
314
|
+
term.removeListener("key",onKey);
|
|
315
|
+
term.grabInput(false);
|
|
316
|
+
term.hideCursor();
|
|
317
|
+
term("\n");
|
|
318
|
+
if(v.trim())TUI.history.push(v.trim());
|
|
319
|
+
resolve(v);
|
|
320
|
+
}
|
|
321
|
+
else if(name==="BACKSPACE"){
|
|
322
|
+
if(TUI.cursor>0){TUI.input=TUI.input.slice(0,TUI.cursor-1)+TUI.input.slice(TUI.cursor);TUI.cursor--;refreshFilter();}
|
|
323
|
+
render();
|
|
324
|
+
}
|
|
325
|
+
else if(name==="DELETE"){
|
|
326
|
+
if(TUI.cursor<TUI.input.length){TUI.input=TUI.input.slice(0,TUI.cursor)+TUI.input.slice(TUI.cursor+1);refreshFilter();}
|
|
327
|
+
render();
|
|
328
|
+
}
|
|
329
|
+
else if(name==="LEFT"){if(TUI.cursor>0){TUI.cursor--;}render();}
|
|
330
|
+
else if(name==="RIGHT"){if(TUI.cursor<TUI.input.length){TUI.cursor++;}render();}
|
|
331
|
+
else if(name==="HOME"){TUI.cursor=0;render();}
|
|
332
|
+
else if(name==="END"){TUI.cursor=TUI.input.length;render();}
|
|
333
|
+
else if(name==="CTRL_C"){term.removeListener("key",onKey);term.grabInput(false);term.hideCursor(true);term("\n");resolve(null);}
|
|
334
|
+
else if(name==="CTRL_L"){term.clear();term.moveTo(1,1);render();}
|
|
335
|
+
};
|
|
336
|
+
term.on("key",onKey);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
async function runTui(){
|
|
340
|
+
term.hideCursor();
|
|
341
|
+
while(true){
|
|
342
|
+
const input=await tuiInput();
|
|
343
|
+
if(input===null)break;
|
|
344
|
+
const text=input.trim();
|
|
345
|
+
if(!text)continue;
|
|
346
|
+
if(text.startsWith("/"))handleCommand(text);
|
|
347
|
+
else await chat(text);
|
|
348
|
+
}
|
|
349
|
+
term.grabInput(false);
|
|
350
|
+
term("\n"+color("再见",C.cyan)+"\n");
|
|
351
|
+
process.exit(0);
|
|
250
352
|
}
|
|
251
|
-
|
|
252
353
|
|
|
253
354
|
function detectInstaller(){
|
|
254
355
|
if(process.platform==="win32")return "winget";
|
|
255
356
|
if(process.platform==="darwin")return "brew";
|
|
256
|
-
for(const pm of ["apk","apt-get","apt","dnf","pacman","pkg"
|
|
357
|
+
for(const pm of ["apk","apt-get","apt","dnf","pacman","pkg"]){
|
|
257
358
|
if(spawnSync("sh",["-lc",`command -v ${pm}`],{encoding:"utf8"}).status===0)return pm;
|
|
258
359
|
}
|
|
259
360
|
return null;
|
|
@@ -270,14 +371,15 @@ function installProgram(name){
|
|
|
270
371
|
async function main(){
|
|
271
372
|
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
373
|
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
|
-
|
|
374
|
+
runTui();
|
|
274
375
|
}
|
|
275
376
|
const argv=process.argv.slice(2);
|
|
276
377
|
if(argv[0]==="--version"||argv[0]==="-v"){console.log(VERSION);process.exit(0);}
|
|
277
378
|
if(argv[0]==="--help"||argv[0]==="-h"){console.log(`GLMCode ${VERSION}
|
|
278
379
|
启动: glmcode
|
|
380
|
+
TUI: 输入 / 或字母实时弹出悬浮命令菜单,↑↓ 键选择,Tab 补全,Enter 执行
|
|
381
|
+
底部常驻状态栏:思考开关 / 模型 / 技能数 / 当前目录
|
|
279
382
|
斜杠命令: /help /clear /thinking /model /pwd /cd /ls /cat /run /install /exit
|
|
280
|
-
输入 / 或前缀自动弹出命令菜单,Tab 补全,底部常驻状态栏。
|
|
281
383
|
技能: 将 SKILL.md 目录或 .zip 包放入 skills/ 自动加载。
|
|
282
384
|
认证: glmcode auth set <API_KEY> | auth status | auth logout | auth login
|
|
283
385
|
Shell: glmcode shell | shell list`);process.exit(0);}
|