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 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
- - 斜杠命令菜单:输入 `/` 或前缀即弹出菜单,Tab 补全
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.9",
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.9";
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("/help 查看命令 · /thinking 思考开关",C.gray));console.log("");
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
- 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])];
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 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));
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","termux-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
- promptLoop();
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);}