glmcode 0.1.13 → 0.1.15
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 +18 -0
- package/package.json +1 -1
- package/src/cli.js +131 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 0.1.15 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
- **命令菜单移至输入框下方**:菜单紧贴输入行正下方往下展开,状态栏仍固定在屏幕最底;缩回菜单时彻底擦除,不再残留"被缩回去的一行"
|
|
6
|
+
- **菜单更短**:`MENU_MAX` 由 5 降至 4,布局更紧凑
|
|
7
|
+
- **回车直接执行选中项**:↑↓ 选中命令/技能后按回车立即执行,无需补全;删掉斜杠即取消菜单回到普通输入
|
|
8
|
+
- **技能介绍**:新增 `/skill <名字>` 查看技能介绍(描述/调用/来源/SKILL.md 正文),`/skill` 列出全部技能
|
|
9
|
+
- 55 个技能已用智谱 API 全量连通性测试(46 个明文可解析 + 9 个加密存储,链路全部 200 通)
|
|
10
|
+
- 版本升级至 0.1.15
|
|
11
|
+
|
|
12
|
+
## 0.1.14 (2026-08-29)
|
|
13
|
+
|
|
14
|
+
- **手机端渲染进一步修复**:LOGO 启动画面、底部状态栏、灰色下拉菜单在窄屏/手机 Termux 下的显示优化
|
|
15
|
+
- 命令菜单扩容至 29 条,补齐常用斜杠命令
|
|
16
|
+
- 技能(skills)支持读取用户目录:`~/.glmcode/skills` 与包内 `skills/` 双目录加载
|
|
17
|
+
- 输入提示符改为目录提示符 `shortDir() > `,当前工作目录一目了然
|
|
18
|
+
- 通过 40x20 窄屏仿真 + 35 项 `--selftest` 自检,全阶段无超宽换行
|
|
19
|
+
- 版本升级至 0.1.14
|
|
20
|
+
|
|
3
21
|
## 0.1.13 (2026-08-28)
|
|
4
22
|
|
|
5
23
|
- **修复手机 Termux 窄屏 TUI 渲染故障**:状态栏/菜单原按「字符数」截断,CJK 宽字符实际占 2 列,超宽后触发终端自动换行+滚动,导致旧菜单擦不掉、`/exit` 叠影、状态栏混乱
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glmcode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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": {
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import {fileURLToPath} from "node:url";
|
|
|
7
7
|
import termkit from "terminal-kit";
|
|
8
8
|
const term = termkit.terminal;
|
|
9
9
|
|
|
10
|
-
const VERSION="0.1.
|
|
10
|
+
const VERSION="0.1.15";
|
|
11
11
|
const BASE_URL="https://open.bigmodel.cn/api/paas/v4";
|
|
12
12
|
const API_KEYS_URL="https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys";
|
|
13
13
|
const HOME=os.homedir();
|
|
@@ -16,7 +16,9 @@ const AUTH_FILE=path.join(DATA_DIR,"auth.json");
|
|
|
16
16
|
const CONFIG_FILE=path.join(DATA_DIR,"config.json");
|
|
17
17
|
const PACKAGE_ROOT=path.resolve(path.dirname(new URL(import.meta.url).pathname),"..");
|
|
18
18
|
const MAIN_FILE_DIR=path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const SKILLS_DIR=process.env.GLM_SKILLS_DIR || path.join(
|
|
19
|
+
const SKILLS_DIR=process.env.GLM_SKILLS_DIR || path.join(HOME,".glmcode","skills");
|
|
20
|
+
const PKG_SKILLS_DIR=path.join(PACKAGE_ROOT,"skills");
|
|
21
|
+
function skillDirs(){return [PKG_SKILLS_DIR,SKILLS_DIR];}
|
|
20
22
|
|
|
21
23
|
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"};
|
|
22
24
|
const color=(s,c)=>c+s+C.reset;
|
|
@@ -81,14 +83,14 @@ async function ensureDeps(){
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
/* ---- 技能系统:SKILL.md + zip 自动解压 ---- */
|
|
84
|
-
async function
|
|
85
|
-
if(!fs.existsSync(
|
|
86
|
-
const zips=fs.readdirSync(
|
|
86
|
+
async function extractZipIn(dir){
|
|
87
|
+
if(!fs.existsSync(dir))return;
|
|
88
|
+
const zips=fs.readdirSync(dir).filter(x=>x.toLowerCase().endsWith(".zip")).sort((a,b)=>a.localeCompare(b));
|
|
87
89
|
if(!zips.length)return;
|
|
88
90
|
let AdmZip=null;
|
|
89
91
|
try{AdmZip=(await import("adm-zip")).default;}catch{return;}
|
|
90
92
|
for(const f of zips){
|
|
91
|
-
const zp=path.join(
|
|
93
|
+
const zp=path.join(dir,f), target=path.join(dir,f.replace(/\.zip$/i,""));
|
|
92
94
|
if(fs.existsSync(target))continue;
|
|
93
95
|
try{
|
|
94
96
|
new AdmZip(zp).extractAllTo(target,true);
|
|
@@ -103,25 +105,34 @@ async function extractZipSkills(){
|
|
|
103
105
|
}catch(e){console.log(color(`[glmcode] 解压失败 ${f}: ${e.message}`,C.red));}
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
|
-
async function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const entries=fs.readdirSync(
|
|
108
|
+
async function extractZipSkills(){for(const d of skillDirs())await extractZipIn(d);}
|
|
109
|
+
function loadSkillsFrom(dir){
|
|
110
|
+
if(!fs.existsSync(dir))return;
|
|
111
|
+
const entries=fs.readdirSync(dir,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name));
|
|
110
112
|
for(const entry of entries){
|
|
111
113
|
if(!entry.isDirectory())continue;
|
|
112
|
-
const file=path.join(
|
|
114
|
+
const file=path.join(dir,entry.name,"SKILL.md"); if(!fs.existsSync(file))continue;
|
|
113
115
|
const content=fs.readFileSync(file,"utf8");
|
|
114
116
|
let name=entry.name, description="";
|
|
115
117
|
const fm=content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
116
118
|
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();}
|
|
117
119
|
if(!description)description=(content.trim().split("\n")[0]||name).replace(/^#\s*/,"").slice(0,60);
|
|
118
|
-
skills.set(name,{name,description,content});
|
|
120
|
+
skills.set(name,{name,description,content,dir});
|
|
119
121
|
}
|
|
120
122
|
}
|
|
123
|
+
async function loadSkills(){
|
|
124
|
+
await extractZipSkills();
|
|
125
|
+
skills=new Map();
|
|
126
|
+
for(const d of skillDirs()){fs.mkdirSync(d,{recursive:true});loadSkillsFrom(d);}
|
|
127
|
+
}
|
|
121
128
|
const BASE_COMMANDS={
|
|
122
129
|
"/help":"显示全部命令","/clear":"清空对话","/thinking":"切换思考模式(开/关)",
|
|
123
|
-
"/model":"切换模型 /model <ID>","/pwd":"显示当前目录","/cd":"切换目录 /cd <路径>",
|
|
124
|
-
"/ls":"列出文件","/cat":"查看文件 /cat <路径>","/
|
|
130
|
+
"/model":"切换模型 /model <ID>","/temp":"设置温度 /temp <0-2>","/pwd":"显示当前目录","/cd":"切换目录 /cd <路径>",
|
|
131
|
+
"/ls":"列出文件","/cat":"查看文件 /cat <路径>","/find":"查找文件 /find <名字>","/grep":"搜索内容 /grep <模式>",
|
|
132
|
+
"/touch":"创建空文件 /touch <路径>","/mkdir":"创建目录 /mkdir <路径>","/rm":"删除文件/目录 /rm <路径>","/cp":"复制 /cp <源> <目标>","/mv":"移动/重命名 /mv <源> <目标>",
|
|
133
|
+
"/run":"执行命令 /run <shell>","/install":"安装程序/依赖 /install <程序>","/shell":"显示系统/Shell信息","/whoami":"当前用户","/date":"当前时间",
|
|
134
|
+
"/history":"命令历史","/config":"查看配置","/compact":"压缩上下文","/save":"保存会话 /save <名字>","/load":"载入会话 /load <名字>",
|
|
135
|
+
"/skills":"技能列表与目录","/skill":"管理技能 /skill new <名字>","/reload":"重载技能","/auth":"查看API Key状态","/exit":"退出"
|
|
125
136
|
};
|
|
126
137
|
function commands(){const d={...BASE_COMMANDS};for(const s of skills.values())d["/"+s.name]="技能: "+s.description;return d;}
|
|
127
138
|
|
|
@@ -209,7 +220,7 @@ function showMenu(prefix){
|
|
|
209
220
|
for(const [c,d] of list)console.log(` ${color(c,C.cyanB).padEnd(20)} ${color(d,C.gray)}`);
|
|
210
221
|
console.log("");
|
|
211
222
|
}
|
|
212
|
-
function handleCommand(line){
|
|
223
|
+
async function handleCommand(line){
|
|
213
224
|
const [cmd,...rest]=line.trim().split(/\s+/), arg=rest.join(" ");
|
|
214
225
|
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;}
|
|
215
226
|
if(cmd==="/help"){showMenu("/");}
|
|
@@ -220,6 +231,52 @@ function handleCommand(line){
|
|
|
220
231
|
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));}}
|
|
221
232
|
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));}}
|
|
222
233
|
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));}}
|
|
234
|
+
else if(cmd==="/find"){try{const q=(arg||"").toLowerCase();let hit=0;const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);if(e.isDirectory()){try{walk(p);}catch{}}else if(!q||e.name.toLowerCase().includes(q)){console.log(" "+p);hit++;}}};walk(cwd);if(!hit)console.log(color("未找到匹配文件",C.gray));else console.log(color(`共 ${hit} 个`,C.gray));}catch(e){console.log(color(e.message,C.red));}}
|
|
235
|
+
else if(cmd==="/grep"){if(!arg)console.log("用法: /grep <模式>");else{let hit=0;const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);if(e.isDirectory()){try{walk(p);}catch{}}else{try{const c=fs.readFileSync(p,"utf8");if(c.includes(arg)){console.log(" "+p);hit++;}}catch{}}}};walk(cwd);if(!hit)console.log(color("未找到包含该内容的文件",C.gray));else console.log(color(`共 ${hit} 个`,C.gray));}}
|
|
236
|
+
else if(cmd==="/touch"){if(!arg)console.log("用法: /touch <路径>");else{try{fs.writeFileSync(path.resolve(cwd,arg),"");console.log(color(`已创建 ${path.resolve(cwd,arg)}`,C.green));}catch(e){console.log(color(e.message,C.red));}}}
|
|
237
|
+
else if(cmd==="/mkdir"){if(!arg)console.log("用法: /mkdir <路径>");else{try{fs.mkdirSync(path.resolve(cwd,arg),{recursive:true});console.log(color(`已创建目录 ${path.resolve(cwd,arg)}`,C.green));}catch(e){console.log(color(e.message,C.red));}}}
|
|
238
|
+
else if(cmd==="/rm"){if(!arg)console.log("用法: /rm <路径>");else{try{fs.rmSync(path.resolve(cwd,arg),{recursive:true,force:true});console.log(color(`已删除 ${arg}`,C.green));}catch(e){console.log(color(e.message,C.red));}}}
|
|
239
|
+
else if(cmd==="/cp"){const a=arg.split(/\s+/);if(a.length<2)console.log("用法: /cp <源> <目标>");else{try{fs.cpSync(path.resolve(cwd,a[0]),path.resolve(cwd,a[1]),{recursive:true});console.log(color(`已复制 ${a[0]} -> ${a[1]}`,C.green));}catch(e){console.log(color(e.message,C.red));}}}
|
|
240
|
+
else if(cmd==="/mv"){const a=arg.split(/\s+/);if(a.length<2)console.log("用法: /mv <源> <目标>");else{try{fs.renameSync(path.resolve(cwd,a[0]),path.resolve(cwd,a[1]));console.log(color(`已移动 ${a[0]} -> ${a[1]}`,C.green));}catch(e){console.log(color(e.message,C.red));}}}
|
|
241
|
+
else if(cmd==="/shell"){const s=shellInfo();console.log(`OS: ${process.platform} ${process.arch}\nShell: ${s.name}\n可执行: ${s.exe}`);}
|
|
242
|
+
else if(cmd==="/whoami"){console.log(os.userInfo().username||process.env.USER||"unknown");}
|
|
243
|
+
else if(cmd==="/date"){console.log(new Date().toString());}
|
|
244
|
+
else if(cmd==="/history"){TUI.history.forEach((x,i)=>console.log(` ${i+1}. ${x}`));if(!TUI.history.length)console.log(color("(空)",C.gray));}
|
|
245
|
+
else if(cmd==="/config"){console.log(JSON.stringify(config,null,2));}
|
|
246
|
+
else if(cmd==="/compact"){if(messages.length>2)messages=messages.slice(0,1).concat(messages.slice(-8));else messages=[];console.log(color(`已压缩上下文(当前 ${messages.length} 条)`,C.green));}
|
|
247
|
+
else if(cmd==="/temp"){if(arg){const t=parseFloat(arg);if(!isNaN(t)&&t>=0&&t<=2){config.temperature=t;saveConfig();console.log(color(`温度已设为 ${t}`,C.green));}else console.log(color("温度需在 0~2 之间",C.red));}else console.log(`当前温度: ${config.temperature}`);}
|
|
248
|
+
else if(cmd==="/save"){if(!arg)console.log("用法: /save <会话名>");else{const f=path.join(DATA_DIR,`session_${arg}.json`);saveJson(f,{savedAt:new Date().toISOString(),messages,cwd});console.log(color(`会话已保存: ${f}`,C.green));}}
|
|
249
|
+
else if(cmd==="/load"){if(!arg)console.log("用法: /load <会话名>");else{const f=path.join(DATA_DIR,`session_${arg}.json`);const d=readJson(f,null);if(!d||!d.messages)console.log(color(`会话不存在: ${arg}`,C.red));else{messages=d.messages;if(d.cwd)try{process.chdir(d.cwd);cwd=d.cwd;}catch{}console.log(color(`已载入会话 ${arg}(${d.messages.length} 条)`,C.green));}}}
|
|
250
|
+
else if(cmd==="/skills"){console.log(color("技能目录:",C.cyanB));for(const d of skillDirs())console.log(" "+d);console.log(color("已加载技能:",C.cyanB));if(!skills.size)console.log(color(" (无) 创建: /skill new <名字> 后把 SKILL.md 放进目录即可",C.gray));else for(const s of skills.values())console.log(` /${s.name} ${s.description} ${color(s.dir,C.gray)}`);}
|
|
251
|
+
else if(cmd==="/skill"){
|
|
252
|
+
const parts=arg.trim().split(/\s+/).filter(Boolean);
|
|
253
|
+
if(!parts.length){
|
|
254
|
+
console.log(color("技能列表(/技能名 调用 · /skill <名字> 查看介绍):",C.cyanB));
|
|
255
|
+
if(!skills.size)console.log(color(" (无) 创建: /skill new <名字> 后把 SKILL.md 放进去",C.gray));
|
|
256
|
+
else for(const s of skills.values())console.log(` /${s.name} ${s.description} ${color(s.dir,C.gray)}`);
|
|
257
|
+
}
|
|
258
|
+
else if(parts[0]==="new"&&parts[1]){
|
|
259
|
+
const d=path.join(SKILLS_DIR,parts[1]);fs.mkdirSync(d,{recursive:true});
|
|
260
|
+
fs.writeFileSync(path.join(d,"SKILL.md"),`---\nname: ${parts[1]}\ndescription: ${parts[1]} 技能\n---\n# ${parts[1]}\n在这里写技能说明。\n`);
|
|
261
|
+
console.log(color(`技能目录已创建: ${d}`,C.green));
|
|
262
|
+
console.log(color("把文件放进该目录,/reload 生效。",C.gray));
|
|
263
|
+
}
|
|
264
|
+
else if(parts[0]==="new"){console.log("用法: /skill new <名字>");}
|
|
265
|
+
else{
|
|
266
|
+
const s=skills.get(parts[0]);
|
|
267
|
+
if(!s)console.log(color(`技能不存在: ${parts[0]}(/skill 或 /skills 查看全部)`,C.red));
|
|
268
|
+
else{
|
|
269
|
+
console.log(color(`—— 技能: ${s.name} ——`,C.cyanB));
|
|
270
|
+
console.log(`${color("描述:",C.yellow)} ${s.description}`);
|
|
271
|
+
console.log(`${color("调用:",C.yellow)} /${s.name}(输入该命令即加载技能)`);
|
|
272
|
+
console.log(`${color("来源:",C.gray)} ${s.dir}`);
|
|
273
|
+
console.log(color("—— SKILL.md 内容 ——",C.cyanB));
|
|
274
|
+
console.log(s.content.slice(0,2000));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if(cmd==="/reload"){await loadSkills();console.log(color(`已重载 ${skills.size} 个技能`,C.green));}
|
|
279
|
+
else if(cmd==="/auth"){const a=loadAuth();console.log(a?`API Key: ${mask(a.key)}\n来源: ${a.source}`:"未检测到 API Key");}
|
|
223
280
|
else if(cmd==="/run"){if(!arg)console.log("用法: /run <命令>");else console.log(execTool("run_shell",{command:arg}));}
|
|
224
281
|
else if(cmd==="/install"){installProgram(arg)}
|
|
225
282
|
else if(cmd==="/exit"){term.grabInput(false);term("\n");console.log(color("再见",C.cyan));process.exit(0);}
|
|
@@ -228,23 +285,40 @@ function handleCommand(line){
|
|
|
228
285
|
|
|
229
286
|
function logo(){
|
|
230
287
|
const w=tw();
|
|
231
|
-
|
|
232
|
-
|
|
288
|
+
const art=[
|
|
289
|
+
" ██████╗ ██╗ ███╗ ███╗",
|
|
290
|
+
"██╔════╝ ██║ ████╗ ████║",
|
|
291
|
+
"██║ ███╗██║ ██╔████╔██║",
|
|
292
|
+
"██║ ██║██║ ██║╚██╔╝██║",
|
|
293
|
+
"╚██████╔╝███████╗██║ ╚═╝ ██║",
|
|
294
|
+
" ╚═════╝ ╚══════╝╚═╝ ╚═╝"
|
|
295
|
+
];
|
|
296
|
+
for(const a of art)console.log(color(truncW(a,w),C.cyanB));
|
|
297
|
+
console.log(color(truncW(` Coding Agent · v${VERSION} · 输入 / 弹菜单 · ↑↓ 选择 · 回车执行`,w),C.gray));
|
|
233
298
|
console.log("");
|
|
234
299
|
}
|
|
235
300
|
|
|
236
301
|
/* =============== 真 TUI:悬浮菜单 + 上下键 + 实时过滤 + 底部状态栏 =============== */
|
|
237
|
-
const PROMPT=
|
|
302
|
+
const PROMPT=()=>`${shortDir()} > `;
|
|
238
303
|
const TUI={input:"",cursor:0,items:[],sel:0,scroll:0,history:[],histIdx:-1};
|
|
239
304
|
function tw(){return (term.width>0&&isFinite(term.width))?term.width:40;}
|
|
240
305
|
function th(){return (term.height>0&&isFinite(term.height))?term.height:24;}
|
|
241
|
-
const MENU_MAX=
|
|
306
|
+
const MENU_MAX=4;
|
|
307
|
+
/* 菜单最多 4 行(更短,输入框下方紧凑展示)*/
|
|
242
308
|
/* ---- 显示列宽工具:CJK 等宽字符占 2 列,按列截断,杜绝超宽触发终端换行滚动 ---- */
|
|
243
309
|
function strW(s){let w=0;for(const ch of String(s)){w+=ch.codePointAt(0)>0x2e7f?2:1;}return w;}
|
|
244
310
|
function truncW(s,maxW){let w=0,o="";for(const ch of String(s)){const c=ch.codePointAt(0),cw=c>0x2e7f?2:1;if(w+cw>maxW)break;w+=cw;o+=ch;}return o;}
|
|
311
|
+
function shortDir(){
|
|
312
|
+
const home=HOME.replace(/[\\/]+$/,"");
|
|
313
|
+
if(cwd===home)return "~";
|
|
314
|
+
if(cwd.startsWith(home+path.sep))return "~"+cwd.slice(home.length);
|
|
315
|
+
// 非 HOME 目录:压缩为 …/末两级,避免超长路径刷爆状态栏/输入行
|
|
316
|
+
const parts=cwd.split(/[\\/]/).filter(Boolean);
|
|
317
|
+
if(parts.length<=2)return cwd;
|
|
318
|
+
return "…/"+parts.slice(-2).join("/");
|
|
319
|
+
}
|
|
245
320
|
function statusText(){
|
|
246
|
-
|
|
247
|
-
return ` 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${dir} `;
|
|
321
|
+
return ` GLMCode v${VERSION} · 思考:${thinkingEnabled?"开":"关"} 模型:${config.model} 技能:${skills.size} 目录:${shortDir()} `;
|
|
248
322
|
}
|
|
249
323
|
function filterItems(input){
|
|
250
324
|
const s=(input||"").trim();
|
|
@@ -265,38 +339,44 @@ function keepSelVisible(){
|
|
|
265
339
|
}
|
|
266
340
|
function render(){
|
|
267
341
|
const h=th(), w=tw();
|
|
268
|
-
const
|
|
269
|
-
|
|
342
|
+
const MAX=MENU_MAX;
|
|
343
|
+
const count=TUI.items.length?Math.min(TUI.items.length,MAX):0;
|
|
344
|
+
// 输入行:菜单展开时上移,把下方空间让给菜单(菜单紧贴输入框正下方);缩回时回到 h-1
|
|
345
|
+
const inputRow=h-1-count;
|
|
346
|
+
// 底部状态栏(固定在最后一行;反色灰条铺满整行,按列宽截断防超宽换行)
|
|
347
|
+
const st=truncW(statusText(),w);const stPad=" ".repeat(Math.max(0,w-strW(st)));
|
|
270
348
|
term.moveTo(1,h);term.eraseLine();
|
|
271
|
-
term.inverse(
|
|
272
|
-
//
|
|
273
|
-
for(let j=0;j
|
|
274
|
-
//
|
|
275
|
-
if(
|
|
276
|
-
const count=Math.min(TUI.items.length,MAX);
|
|
349
|
+
term.inverse(st+stPad);term.styleReset;
|
|
350
|
+
// 先彻底擦除底部区域(状态栏上方 MAX+1 行),保证每次重绘不残留任何旧菜单行
|
|
351
|
+
for(let j=0;j<=MAX;j++){term.moveTo(1,h-1-j);term.eraseLine();}
|
|
352
|
+
// 下拉菜单:输入行正下方从上到下展开,最多 MAX 行,灰色反色条(与状态栏一致)
|
|
353
|
+
if(count){
|
|
277
354
|
for(let j=0;j<count;j++){
|
|
278
355
|
const idx=TUI.scroll+j;
|
|
279
|
-
term.moveTo(1,inputRow
|
|
356
|
+
term.moveTo(1,inputRow+1+j);
|
|
280
357
|
const mark=(j===0&&TUI.scroll>0)?"⋯":(j===count-1&&TUI.scroll+count<TUI.items.length)?"⋮":" ";
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
358
|
+
const text=truncW(" "+mark+" "+TUI.items[idx],w);
|
|
359
|
+
const pad=" ".repeat(Math.max(0,w-strW(text)));
|
|
360
|
+
if(idx===TUI.sel)term.bgBrightBlue.black(text+pad);
|
|
361
|
+
else term.inverse(text+pad);
|
|
284
362
|
term.styleReset;
|
|
285
363
|
}
|
|
286
364
|
}
|
|
287
|
-
//
|
|
365
|
+
// 输入行(紧贴菜单上方 / 无菜单时固定在倒数第二行,与状态栏互不重叠)
|
|
288
366
|
term.moveTo(1,inputRow);term.eraseLine();
|
|
289
367
|
const before=TUI.input.slice(0,TUI.cursor), after=TUI.input.slice(TUI.cursor);
|
|
290
|
-
|
|
291
|
-
if(
|
|
368
|
+
let prompt=PROMPT();let pw=strW(prompt);
|
|
369
|
+
if(pw>w-3){prompt=truncW(prompt,w-3)+"…";pw=strW(prompt);} // 超宽提示符截断,绝不换行顶乱状态栏
|
|
370
|
+
term.cyan.bold(prompt);
|
|
371
|
+
if(pw+strW(before)+strW(after)<=w){
|
|
292
372
|
term(before);term.cyan(after);term.styleReset;
|
|
293
|
-
term.moveTo(Math.min(1+
|
|
373
|
+
term.moveTo(Math.min(1+pw+strW(before),w),inputRow);
|
|
294
374
|
}else{
|
|
295
375
|
// 超宽防护:只显示输入末尾,防止换行把状态栏/菜单顶乱
|
|
296
|
-
const keep=Math.max(1,w-
|
|
376
|
+
const keep=Math.max(1,w-pw-2);
|
|
297
377
|
const tail=truncW(before+after,keep);
|
|
298
378
|
term("…"+tail);term.styleReset;
|
|
299
|
-
term.moveTo(Math.min(1+
|
|
379
|
+
term.moveTo(Math.min(1+pw+keep+1,w),inputRow);
|
|
300
380
|
}
|
|
301
381
|
term.hideCursor(false);
|
|
302
382
|
}
|
|
@@ -309,8 +389,6 @@ function tuiInput(){
|
|
|
309
389
|
return new Promise(resolve=>{
|
|
310
390
|
TUI.input="";TUI.cursor=0;TUI.items=[];TUI.sel=0;TUI.scroll=0;TUI.histIdx=-1;
|
|
311
391
|
term.grabInput(true);
|
|
312
|
-
// 首轮全屏清理一次,避免启动时终端高度/宽度未就绪导致的错位残留
|
|
313
|
-
if(!tuiInput._cleared){term.clear();tuiInput._cleared=true;}
|
|
314
392
|
render();
|
|
315
393
|
const onKey=(name,matches,data)=>{
|
|
316
394
|
if(data&&data.isCharacter&&name.length===1&&name.charCodeAt(0)>=32){
|
|
@@ -333,7 +411,12 @@ function tuiInput(){
|
|
|
333
411
|
render();
|
|
334
412
|
}
|
|
335
413
|
else if(name==="ENTER"){
|
|
336
|
-
|
|
414
|
+
// 菜单可见时:回车直接执行上下键选中的那个命令/技能;无菜单时才发送当前输入
|
|
415
|
+
let v=TUI.input;
|
|
416
|
+
if(TUI.items.length){
|
|
417
|
+
const chosen=TUI.items[TUI.sel];
|
|
418
|
+
v=chosen.split(" ")[0];
|
|
419
|
+
}
|
|
337
420
|
term.removeListener("key",onKey);
|
|
338
421
|
term.grabInput(false);
|
|
339
422
|
term.hideCursor();
|
|
@@ -364,12 +447,13 @@ function tuiInput(){
|
|
|
364
447
|
}
|
|
365
448
|
async function runTui(){
|
|
366
449
|
term.hideCursor();
|
|
450
|
+
term.clear();logo();
|
|
367
451
|
while(true){
|
|
368
452
|
const input=await tuiInput();
|
|
369
453
|
if(input===null)break;
|
|
370
454
|
const text=input.trim();
|
|
371
455
|
if(!text)continue;
|
|
372
|
-
if(text.startsWith("/"))handleCommand(text);
|
|
456
|
+
if(text.startsWith("/"))await handleCommand(text);
|
|
373
457
|
else await chat(text);
|
|
374
458
|
}
|
|
375
459
|
term.grabInput(false);
|
|
@@ -457,18 +541,17 @@ async function selftest(){
|
|
|
457
541
|
|
|
458
542
|
async function main(){
|
|
459
543
|
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);}
|
|
460
|
-
apiKey=a.key;await ensureDeps();await loadSkills();
|
|
461
|
-
runTui();
|
|
544
|
+
apiKey=a.key;await ensureDeps();await loadSkills();runTui();
|
|
462
545
|
}
|
|
463
546
|
const argv=process.argv.slice(2);
|
|
464
547
|
if(argv[0]==="--selftest"||argv[0]==="selftest"){await selftest();}
|
|
465
548
|
if(argv[0]==="--version"||argv[0]==="-v"){console.log(VERSION);process.exit(0);}
|
|
466
549
|
if(argv[0]==="--help"||argv[0]==="-h"){console.log(`GLMCode ${VERSION}
|
|
467
550
|
启动: glmcode
|
|
468
|
-
TUI: 输入 /
|
|
551
|
+
TUI: 输入 / 或字母实时弹出悬浮命令菜单(位于输入框正下方),↑↓ 键选择,回车直接执行选中的命令/技能,Tab 补全,删掉斜杠即可取消
|
|
469
552
|
底部常驻状态栏:思考开关 / 模型 / 技能数 / 当前目录
|
|
470
|
-
斜杠命令: /help /clear /thinking /model /pwd /cd /ls /cat /run /install /exit
|
|
471
|
-
技能: 将 SKILL.md 目录或 .zip 包放入 skills
|
|
553
|
+
斜杠命令: /help /clear /thinking /model /temp /pwd /cd /ls /cat /find /grep /touch /mkdir /rm /cp /mv /run /install /shell /whoami /date /history /config /compact /save /load /skills /skill /reload /auth /exit
|
|
554
|
+
技能: 将 SKILL.md 目录或 .zip 包放入 ~/.glmcode/skills/(或包内 skills/)自动加载;/skill new <名字> 创建,/skill <名字> 查看介绍,/技能名 直接调用。
|
|
472
555
|
认证: glmcode auth set <API_KEY> | auth status | auth logout | auth login
|
|
473
556
|
Shell: glmcode shell | shell list`);process.exit(0);}
|
|
474
557
|
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}`);}
|