@wendongfly/myhi 1.0.82 → 1.0.84

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.
Files changed (2) hide show
  1. package/dist/attach.js +1 -276
  2. package/package.json +1 -1
package/dist/attach.js CHANGED
@@ -1,277 +1,2 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * myhi attach — 将本地终端连接到运行中的 PTY 会话
4
- *
5
- * 用法:
6
- * node src/attach.js # 列出会话,然后选择
7
- * node src/attach.js <id> # 直接附加到指定会话
8
- */
9
- import { createRequire } from 'module';
10
- import { readFileSync } from 'fs';
11
- import { join } from 'path';
12
- import { homedir } from 'os';
13
- import { createInterface } from 'readline';
14
-
15
- const require = createRequire(import.meta.url);
16
- const { io } = require('socket.io-client');
17
-
18
- const SERVER = process.env.MYHI_SERVER || 'http://localhost:12300';
19
- const TOKEN = readFileSync(join(homedir(), '.myhi', 'token'), 'utf8').trim();
20
-
21
- function cleanup(socket) {
22
- try { process.stdin.setRawMode(false); } catch {}
23
- process.stdin.pause();
24
- socket.disconnect();
25
- }
26
-
27
- function attach(socket, sessionId) {
28
- socket.emit('join', sessionId);
29
-
30
- socket.on('joined', (session) => {
31
- const isAgent = session.mode === 'agent';
32
- process.stderr.write(`\r\n[myhi] 已附加到 "${session.title}" (${sessionId}) 模式=${isAgent ? 'agent' : 'pty'}\r\n`);
33
- process.stderr.write('[myhi] 按 Ctrl+] 分离\r\n\r\n');
34
-
35
- // 本地终端自动获取控制权
36
- socket.emit('take-control', { sessionId });
37
-
38
- if (isAgent) {
39
- // ── Agent 模式:显示格式化输出 ──
40
- let _streamText = '';
41
-
42
- socket.on('agent:message', (msg) => {
43
- if (!msg) return;
44
- switch (msg.type) {
45
- case 'system':
46
- if (msg.subtype === 'init') process.stderr.write('\x1b[90m[会话已连接]\x1b[0m\r\n');
47
- else if (msg.subtype === 'interrupted') process.stderr.write('\r\n\x1b[33m[已中断]\x1b[0m\r\n');
48
- break;
49
- case 'assistant':
50
- if (msg.message?.content) {
51
- for (const block of msg.message.content) {
52
- if (block.type === 'text') process.stdout.write(block.text + '\r\n');
53
- else if (block.type === 'tool_use') {
54
- process.stdout.write(`\r\n\x1b[36m[工具] ${block.name}\x1b[0m\r\n`);
55
- if (block.input) {
56
- const s = typeof block.input === 'string' ? block.input : JSON.stringify(block.input, null, 2);
57
- process.stdout.write('\x1b[90m' + s.slice(0, 500) + '\x1b[0m\r\n');
58
- }
59
- }
60
- }
61
- }
62
- break;
63
- case 'content_block_delta':
64
- if (msg.delta?.text) { process.stdout.write(msg.delta.text); _streamText += msg.delta.text; }
65
- break;
66
- case 'content_block_stop':
67
- if (_streamText) { process.stdout.write('\r\n'); _streamText = ''; }
68
- break;
69
- case 'tool_use':
70
- process.stdout.write(`\r\n\x1b[36m[工具] ${msg.tool_name || msg.name || '?'}\x1b[0m\r\n`);
71
- break;
72
- case 'tool_result':
73
- if (msg.content) {
74
- const text = typeof msg.content === 'string' ? msg.content
75
- : Array.isArray(msg.content) ? msg.content.map(b => b.text || '').join('') : '';
76
- if (text) process.stdout.write('\x1b[90m' + text.slice(0, 1000) + '\x1b[0m\r\n');
77
- }
78
- break;
79
- case 'result':
80
- process.stdout.write(`\r\n\x1b[32m[完成]\x1b[0m ${msg.duration_ms ? (msg.duration_ms/1000).toFixed(1)+'s' : ''} ${msg.total_cost_usd ? '$'+msg.total_cost_usd.toFixed(4) : ''}\r\n\r\n`);
81
- break;
82
- case 'control_request':
83
- process.stdout.write(`\r\n\x1b[33m[权限请求] ${msg.request?.tool_name || '?'}\x1b[0m\r\n`);
84
- break;
85
- }
86
- });
87
-
88
- socket.on('agent:history', (history) => {
89
- for (const msg of history) {
90
- if (msg.type === 'user') process.stdout.write(`\x1b[34m> ${msg.content}\x1b[0m\r\n`);
91
- else if (msg.type === 'result') process.stdout.write(`\x1b[32m[完成]\x1b[0m\r\n`);
92
- }
93
- process.stdout.write('\r\n');
94
- });
95
-
96
- socket.on('agent:busy', (busy) => {
97
- if (busy) process.stderr.write('\x1b[90m[思考中...]\x1b[0m\r\n');
98
- });
99
-
100
- socket.on('agent:error', (err) => {
101
- process.stderr.write(`\x1b[31m[错误] ${err.message}\x1b[0m\r\n`);
102
- });
103
-
104
- // Agent 模式用行编辑输入
105
- const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '\x1b[34m> \x1b[0m' });
106
- rl.prompt();
107
- rl.on('line', (line) => {
108
- const text = line.trim();
109
- if (!text) { rl.prompt(); return; }
110
- if (text === '/quit' || text === '/exit') {
111
- process.stderr.write('[myhi] 已分离\r\n');
112
- cleanup(socket);
113
- process.exit(0);
114
- }
115
- socket.emit('agent:query', { prompt: text });
116
- // result 消息到达后再显示 prompt
117
- socket.once('agent:busy', (busy) => { if (!busy) rl.prompt(); });
118
- });
119
- rl.on('close', () => { cleanup(socket); process.exit(0); });
120
-
121
- } else {
122
- // ── PTY 模式:原始终端 ──
123
- try { process.stdin.setRawMode(true); } catch {}
124
- process.stdin.resume();
125
- process.stdin.setEncoding('binary');
126
-
127
- process.stdin.on('data', (data) => {
128
- if (data === '\x1d') {
129
- process.stderr.write('\r\n[myhi] 已分离\r\n');
130
- cleanup(socket);
131
- process.exit(0);
132
- }
133
- socket.emit('input', data);
134
- });
135
-
136
- socket.on('output', (data) => {
137
- process.stdout.write(data, 'binary');
138
- });
139
-
140
- process.stdout.on('resize', () => {
141
- socket.emit('resize', {
142
- cols: process.stdout.columns || 80,
143
- rows: process.stdout.rows || 24,
144
- });
145
- });
146
- }
147
- });
148
-
149
- // 控制权被拒绝
150
- socket.on('control-denied', ({ reason }) => {
151
- process.stderr.write(`\r\n[myhi] 获取控制权失败: ${reason}\r\n`);
152
- });
153
-
154
- // 控制权被其他用户获取
155
- socket.on('control-changed', ({ holder, holderName }) => {
156
- if (holder && holder !== socket.id) {
157
- process.stderr.write(`\r\n[myhi] ${holderName || '其他用户'} 已获取控制权,当前为只读\r\n`);
158
- } else if (!holder) {
159
- process.stderr.write('\r\n[myhi] 控制权已释放\r\n');
160
- }
161
- });
162
-
163
- socket.on('kicked', ({ reason }) => {
164
- process.stderr.write(`\r\n[myhi] ${reason || '你已被管理员踢出'}\r\n`);
165
- cleanup(socket);
166
- process.exit(1);
167
- });
168
-
169
- socket.on('session-exit', ({ code }) => {
170
- process.stderr.write(`\r\n[myhi] 会话已退出 (code ${code})\r\n`);
171
- cleanup(socket);
172
- process.exit(0);
173
- });
174
-
175
- socket.on('error', ({ message }) => {
176
- process.stderr.write(`\r\n[myhi] 错误: ${message}\r\n`);
177
- cleanup(socket);
178
- process.exit(1);
179
- });
180
- }
181
-
182
- async function pickSession(socket) {
183
- return new Promise((resolve, reject) => {
184
- socket.emit('list');
185
- socket.once('sessions', (sessions) => {
186
- const alive = sessions.filter(s => s.alive);
187
- if (!alive.length) {
188
- process.stderr.write('[myhi] 没有活跃的会话。\n');
189
- cleanup(socket);
190
- process.exit(0);
191
- }
192
-
193
- process.stdout.write('\n活跃会话:\n');
194
- alive.forEach((s, i) => {
195
- const viewers = s.viewers > 0 ? ` (${s.viewers} 人在线)` : '';
196
- const mode = s.mode === 'agent' ? ' [Agent]' : ' [PTY]';
197
- process.stdout.write(` [${i + 1}] ${s.title}${mode}${viewers} — ${s.id}\n`);
198
- });
199
- process.stdout.write('\n');
200
-
201
- const rl = createInterface({ input: process.stdin, output: process.stdout });
202
- rl.question('选择会话编号: ', (answer) => {
203
- rl.close();
204
- const idx = parseInt(answer, 10) - 1;
205
- if (idx < 0 || idx >= alive.length) {
206
- process.stderr.write('[myhi] 无效的选择。\n');
207
- cleanup(socket);
208
- process.exit(1);
209
- }
210
- resolve(alive[idx].id);
211
- });
212
- });
213
- });
214
- }
215
-
216
- async function createAndAttach(socket, opts) {
217
- return new Promise((resolve, reject) => {
218
- socket.emit('create', opts, (res) => {
219
- if (!res?.ok) {
220
- process.stderr.write(`[myhi] 创建失败: ${res?.error || '未知错误'}\n`);
221
- cleanup(socket);
222
- process.exit(1);
223
- }
224
- process.stdout.write(`[myhi] 已创建会话 "${res.session.title}" — ${res.session.id}\n`);
225
- resolve(res.session.id);
226
- });
227
- });
228
- }
229
-
230
- async function promptNew(socket) {
231
- const rl = createInterface({ input: process.stdin, output: process.stdout });
232
- const ask = (q) => new Promise((r) => rl.question(q, r));
233
- const title = (await ask('会话名称 [shell]: ')).trim() || 'shell';
234
- const initCmd = (await ask('启动命令(可选): ')).trim() || undefined;
235
- rl.close();
236
- return createAndAttach(socket, { title, initCmd });
237
- }
238
-
239
- // ── 主入口 ──────────────────────────────────────────────
240
-
241
- const socket = io(SERVER, {
242
- transports: ['websocket'],
243
- auth: { token: TOKEN },
244
- });
245
-
246
- socket.on('connect_error', (err) => {
247
- process.stderr.write(`[myhi] 连接失败: ${err.message}\n`);
248
- process.exit(1);
249
- });
250
-
251
- socket.on('connect', async () => {
252
- const arg = process.argv[2];
253
-
254
- if (arg === '--new') {
255
- // --new [标题] [命令] → 创建后附加
256
- const title = process.argv[3];
257
- const initCmd = process.argv[4] || undefined;
258
- const sessionId = title
259
- ? await createAndAttach(socket, { title, initCmd })
260
- : await promptNew(socket);
261
- attach(socket, sessionId);
262
- } else {
263
- const sessionId = arg || await pickSession(socket);
264
- attach(socket, sessionId);
265
- }
266
- });
267
-
268
- // 退出时恢复终端状态
269
- for (const sig of ['SIGINT', 'SIGTERM']) {
270
- process.on(sig, () => {
271
- cleanup(socket);
272
- process.exit(0);
273
- });
274
- }
275
- process.on('exit', () => {
276
- try { process.stdin.setRawMode(false); } catch {}
277
- });
2
+ import{createRequire as e}from"module";if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=new URL(".",import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/)?1:0,-1)+"/";var t={};const s=e(import.meta.url)("module");const r=e(import.meta.url)("fs");const o=e(import.meta.url)("path");const n=e(import.meta.url)("os");const i=e(import.meta.url)("readline");const c=(0,s.createRequire)(import.meta.url);const{io:a}=c("socket.io-client");const p=process.env.MYHI_SERVER||"http://localhost:12300";const m=(0,r.readFileSync)((0,o.join)((0,n.homedir)(),".myhi","token"),"utf8").trim();function cleanup(e){try{process.stdin.setRawMode(false)}catch{}process.stdin.pause();e.disconnect()}function attach(e,t){e.emit("join",t);e.on("joined",(s=>{const r=s.mode==="agent";process.stderr.write(`\r\n[myhi] 已附加到 "${s.title}" (${t}) 模式=${r?"agent":"pty"}\r\n`);process.stderr.write("[myhi] 按 Ctrl+] 分离\r\n\r\n");e.emit("take-control",{sessionId:t});if(r){let t="";e.on("agent:message",(e=>{if(!e)return;switch(e.type){case"system":if(e.subtype==="init")process.stderr.write("[会话已连接]\r\n");else if(e.subtype==="interrupted")process.stderr.write("\r\n[已中断]\r\n");break;case"assistant":if(e.message?.content){for(const t of e.message.content){if(t.type==="text")process.stdout.write(t.text+"\r\n");else if(t.type==="tool_use"){process.stdout.write(`\r\n[工具] ${t.name}\r\n`);if(t.input){const e=typeof t.input==="string"?t.input:JSON.stringify(t.input,null,2);process.stdout.write(""+e.slice(0,500)+"\r\n")}}}}break;case"content_block_delta":if(e.delta?.text){process.stdout.write(e.delta.text);t+=e.delta.text}break;case"content_block_stop":if(t){process.stdout.write("\r\n");t=""}break;case"tool_use":process.stdout.write(`\r\n[工具] ${e.tool_name||e.name||"?"}\r\n`);break;case"tool_result":if(e.content){const t=typeof e.content==="string"?e.content:Array.isArray(e.content)?e.content.map((e=>e.text||"")).join(""):"";if(t)process.stdout.write(""+t.slice(0,1e3)+"\r\n")}break;case"result":process.stdout.write(`\r\n[完成] ${e.duration_ms?(e.duration_ms/1e3).toFixed(1)+"s":""} ${e.total_cost_usd?"$"+e.total_cost_usd.toFixed(4):""}\r\n\r\n`);break;case"control_request":process.stdout.write(`\r\n[权限请求] ${e.request?.tool_name||"?"}\r\n`);break}}));e.on("agent:history",(e=>{for(const t of e){if(t.type==="user")process.stdout.write(`> ${t.content}\r\n`);else if(t.type==="result")process.stdout.write(`[完成]\r\n`)}process.stdout.write("\r\n")}));e.on("agent:busy",(e=>{if(e)process.stderr.write("[思考中...]\r\n")}));e.on("agent:error",(e=>{process.stderr.write(`[错误] ${e.message}\r\n`)}));const s=(0,i.createInterface)({input:process.stdin,output:process.stdout,prompt:"> "});let r=false;e.on("agent:busy",(e=>{r=e;if(!e)s.prompt()}));s.prompt();s.on("line",(t=>{const o=t.trim();if(!o){if(!r)s.prompt();return}if(o==="/quit"||o==="/exit"||o==="/q"){process.stderr.write("[myhi] 已分离\r\n");cleanup(e);process.exit(0)}if(r){process.stderr.write("[正在处理中,请等待]\r\n");return}e.emit("agent:query",{prompt:o})}));s.on("close",(()=>{cleanup(e);process.exit(0)}))}else{try{process.stdin.setRawMode(true)}catch{}process.stdin.resume();process.stdin.setEncoding("binary");process.stdin.on("data",(t=>{if(t===""){process.stderr.write("\r\n[myhi] 已分离\r\n");cleanup(e);process.exit(0)}e.emit("input",t)}));e.on("output",(e=>{process.stdout.write(e,"binary")}));process.stdout.on("resize",(()=>{e.emit("resize",{cols:process.stdout.columns||80,rows:process.stdout.rows||24})}))}}));e.on("control-denied",(({reason:e})=>{process.stderr.write(`\r\n[myhi] 获取控制权失败: ${e}\r\n`)}));e.on("control-changed",(({holder:t,holderName:s})=>{if(t&&t!==e.id){process.stderr.write(`\r\n[myhi] ${s||"其他用户"} 已获取控制权,当前为只读\r\n`)}else if(!t){process.stderr.write("\r\n[myhi] 控制权已释放\r\n")}}));e.on("kicked",(({reason:t})=>{process.stderr.write(`\r\n[myhi] ${t||"你已被管理员踢出"}\r\n`);cleanup(e);process.exit(1)}));e.on("session-exit",(({code:t})=>{process.stderr.write(`\r\n[myhi] 会话已退出 (code ${t})\r\n`);cleanup(e);process.exit(0)}));e.on("error",(({message:t})=>{process.stderr.write(`\r\n[myhi] 错误: ${t}\r\n`);cleanup(e);process.exit(1)}))}async function pickSession(e){return new Promise(((t,s)=>{e.emit("list");e.once("sessions",(s=>{const r=s.filter((e=>e.alive));if(!r.length){process.stderr.write("[myhi] 没有活跃的会话。\n");cleanup(e);process.exit(0)}process.stdout.write("\n活跃会话:\n");r.forEach(((e,t)=>{const s=e.viewers>0?` (${e.viewers} 人在线)`:"";const r=e.mode==="agent"?" [Agent]":" [PTY]";process.stdout.write(` [${t+1}] ${e.title}${r}${s} — ${e.id}\n`)}));process.stdout.write("\n");const o=(0,i.createInterface)({input:process.stdin,output:process.stdout});o.question("选择会话编号: ",(s=>{o.close();const n=parseInt(s,10)-1;if(n<0||n>=r.length){process.stderr.write("[myhi] 无效的选择。\n");cleanup(e);process.exit(1)}t(r[n].id)}))}))}))}async function createAndAttach(e,t){return new Promise(((s,r)=>{e.emit("create",t,(t=>{if(!t?.ok){process.stderr.write(`[myhi] 创建失败: ${t?.error||"未知错误"}\n`);cleanup(e);process.exit(1)}process.stdout.write(`[myhi] 已创建会话 "${t.session.title}" — ${t.session.id}\n`);s(t.session.id)}))}))}async function promptNew(e){const t=(0,i.createInterface)({input:process.stdin,output:process.stdout});const ask=e=>new Promise((s=>t.question(e,s)));const s=(await ask("会话名称 [shell]: ")).trim()||"shell";const r=(await ask("启动命令(可选): ")).trim()||undefined;t.close();return createAndAttach(e,{title:s,initCmd:r})}const u=a(p,{transports:["websocket"],auth:{token:m}});u.on("connect_error",(e=>{process.stderr.write(`[myhi] 连接失败: ${e.message}\n`);process.exit(1)}));u.on("connect",(async()=>{const e=process.argv[2];if(e==="--new"){const e=process.argv[3];const t=process.argv[4]||undefined;const s=e?await createAndAttach(u,{title:e,initCmd:t}):await promptNew(u);attach(u,s)}else{const t=e||await pickSession(u);attach(u,t)}}));for(const e of["SIGINT","SIGTERM"]){process.on(e,(()=>{cleanup(u);process.exit(0)}))}process.on("exit",(()=>{try{process.stdin.setRawMode(false)}catch{}}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.0.82",
3
+ "version": "1.0.84",
4
4
  "description": "Web-based terminal sharing with chat UI — control your terminal from phone via LAN/Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",