mocode-ai 0.1.1 → 0.1.2

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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { exitAltScreen } from './ui/layout.js';
2
+ import { checkAndMaybeUpdate } from './updater/index.js';
2
3
  // 终端恢复兜底:任一退出 / 中断 / 未捕获异常路径都要恢复 alt screen,避免残留备用屏 + 滚动区域。
3
4
  // exitAltScreen 幂等(未激活时空操作),故全局注册安全——进 alt screen 前的路径(如 --resume 列表、缺环境变量、`mocode config`)调用它无副作用。
4
5
  // 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(requireEnv 缺项即退出),改动态按需加载——`mocode config` 才能在零配置下跑。
@@ -63,12 +64,14 @@ async function main() {
63
64
  console.error(`[session] 找不到会话 ${id}(用 --resume 查看列表)`);
64
65
  process.exit(1);
65
66
  }
67
+ const updateNotice = checkAndMaybeUpdate();
66
68
  const { startRepl } = await import('./repl/index.js');
67
- await startRepl(loaded.history, loaded.id);
69
+ await startRepl(loaded.history, loaded.id, updateNotice);
68
70
  }
69
71
  else {
72
+ const updateNotice = checkAndMaybeUpdate();
70
73
  const { startRepl } = await import('./repl/index.js');
71
- await startRepl();
74
+ await startRepl(undefined, undefined, updateNotice);
72
75
  }
73
76
  process.exit(0);
74
77
  }
@@ -249,7 +249,7 @@ export function renderHistory(history) {
249
249
  * contentWrite 落入内容区(滚动区域内自动滚动,底栏不动)。history 由本模块持有,在轮次间持久;
250
250
  * agent 只读取并追加(+ 经 session/ 压缩)。每轮成功结束后自动落盘,退出后可用 --resume / /resume 续接。
251
251
  */
252
- export async function startRepl(initialHistory, sessionId) {
252
+ export async function startRepl(initialHistory, sessionId, updateNotice = null) {
253
253
  // 有预加载(--resume)则用它,并把 history[0] 刷成当前 system prompt(config 可能已变);
254
254
  // 否则新会话只塞 system 提示。
255
255
  const systemPrompt = effectiveSystemPrompt(config.systemPrompt);
@@ -287,6 +287,10 @@ export async function startRepl(initialHistory, sessionId) {
287
287
  else {
288
288
  layout.contentWrite(bannerString(banner()));
289
289
  }
290
+ if (updateNotice) {
291
+ // 自更新提示:开场静态段(进 INPUT 态前),dim 一行,不与流式 / 输入争用。
292
+ layout.contentWrite(` ${ui.gray}↳ ${updateNotice}${ui.reset}\n`);
293
+ }
290
294
  /**
291
295
  * 回滚子流程(由 /rollback 触发):菜单(↑/↓)选轮次 → 选中第 X 轮 = 删第 X 轮及之后 + 预填第 X 轮 user 输入
292
296
  * (仿 Claude Code rewind,Enter 重新跑该轮);被删轮次的文件改动仍逐个「保留/撤销」询问(cooked readline)。
package/dist/ui/render.js CHANGED
@@ -160,7 +160,7 @@ const LOGO_GAP = 4; // logo 与信息区之间的间隔
160
160
  const LOGO_PAD = ' '.repeat(LOGO_W + LOGO_GAP); // 无 logo 行的缩进(对齐信息区)
161
161
  // 坐着的小人:[●ᴗ●] 头 / | | 身 / ╲| |╱ 双手外撑 / | | 身 / OO 脚
162
162
  const LOGO_LINES = [
163
- ' [●ᴗ●] ',
163
+ ' [◕ᴗ◕] ',
164
164
  ' ●| |● ',
165
165
  ' OO ',
166
166
  ];
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ /**
7
+ * 启动时自动检测并后台自更新(仿 update-notifier,但**真更新**而非仅提示)。
8
+ *
9
+ * 设计要点:
10
+ * - **零启动延迟**:`checkAndMaybeUpdate()` 同步返回提示串或 null;联网刷新与 `npm i -g` 都是
11
+ * fire-and-forget 后台操作(不 await),不卡 REPL 启动。
12
+ * - **下次启动生效**:当前进程已把 dist/*.js 读入内存,运行中不持有文件句柄;后台 `npm i -g`
13
+ * 覆写磁盘包(含 mocode.cmd shim),当前进程不受影响,**下次** `mocode` 即新版本。
14
+ * - **节流**:检查 24h、spawn 6h(防每跑必联网 / 重复 spawn)。更新成功后下次重读 package.json
15
+ * 得 current=latest → 不再 spawn、不再提示。
16
+ * - **不依赖 config**:只用 stdlib + 全局 fetch,缺 LLM 配置也能跑(在 config 校验前调)。
17
+ * - **dev 跳过**:tsx 运行 `.ts`(`npm start`)不自更新;编译态 `.js`(`mocode` 全局命令)才更新。
18
+ * - **失败静默**:断网 / 无 npm / 权限不足都不阻断启动。
19
+ */
20
+ const PKG_NAME = 'mocode-ai';
21
+ /** 包根 package.json:从 dist/updater/ 或 src/updater/ 均 `../../package.json`(npm 必带 package.json)。 */
22
+ const PKG_PATH = fileURLToPath(new URL('../../package.json', import.meta.url));
23
+ const CACHE_PATH = path.join(os.homedir(), '.mocode', 'update-check.json');
24
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h:多久重新拉一次 registry
25
+ const SPAWN_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h:同一待更新版本多久内不重复 spawn
26
+ const FETCH_TIMEOUT_MS = 2500;
27
+ /** 缓存路径:MOCODE_UPDATE_CACHE 可重定向(测试用临时文件,避免污染真实 ~/.mocode 缓存)。 */
28
+ function cachePath() {
29
+ return process.env.MOCODE_UPDATE_CACHE || CACHE_PATH;
30
+ }
31
+ /** tsx 开发态(import.meta.url 以 .ts 结尾)跳过自更新;编译态 .js 才更新。 */
32
+ function isDevRun() {
33
+ try {
34
+ return fileURLToPath(import.meta.url).endsWith('.ts');
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /** 读当前安装版本(package.json 的 version)。读不到返 ''。 */
41
+ function readCurrentVersion() {
42
+ try {
43
+ const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
44
+ return pkg.version ?? '';
45
+ }
46
+ catch {
47
+ return '';
48
+ }
49
+ }
50
+ function readCache() {
51
+ try {
52
+ return JSON.parse(fs.readFileSync(cachePath(), 'utf8'));
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ /** 读-合并-写(各写者独立 merge,降低后台 fetch 与同步 spawn 写的竞态丢字段风险)。 */
59
+ function writeCache(patch) {
60
+ try {
61
+ const merged = { ...readCache(), ...patch };
62
+ fs.mkdirSync(path.dirname(cachePath()), { recursive: true });
63
+ fs.writeFileSync(cachePath(), JSON.stringify(merged), 'utf8');
64
+ }
65
+ catch {
66
+ // 写缓存失败不阻断
67
+ }
68
+ }
69
+ /** registry 取自 ~/.npmrc(国内镜像用户与 `npm i -g` 装的源一致);无则用官方端点。保证尾 `/`。 */
70
+ function readRegistry() {
71
+ let reg = 'https://registry.npmjs.org/';
72
+ try {
73
+ const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
74
+ const m = /^registry\s*=\s*["']?([^"'\s]+)["']?\s*$/m.exec(npmrc);
75
+ if (m && m[1])
76
+ reg = m[1];
77
+ }
78
+ catch {
79
+ // 无 ~/.npmrc:用默认
80
+ }
81
+ return reg.endsWith('/') ? reg : reg + '/';
82
+ }
83
+ /** 后台拉 registry 最新版(不 await):成功写 latest+lastCheck,失败只写 lastCheck(防离线每跑必联网)。 */
84
+ async function fetchLatestInBackground() {
85
+ try {
86
+ const ctrl = new AbortController();
87
+ const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
88
+ const res = await fetch(`${readRegistry()}${PKG_NAME}/latest`, { signal: ctrl.signal });
89
+ clearTimeout(t);
90
+ if (!res.ok)
91
+ throw new Error(`HTTP ${res.status}`);
92
+ const data = (await res.json());
93
+ writeCache(data.version ? { latest: data.version, lastCheck: Date.now() } : { lastCheck: Date.now() });
94
+ }
95
+ catch {
96
+ writeCache({ lastCheck: Date.now() });
97
+ }
98
+ }
99
+ /** 后台 spawn `npm i -g <pkg>@latest`(Win:shell 解析 npm.cmd + windowsHide 防控制台闪;detached+unref 跨进程存活)。 */
100
+ function spawnUpdate() {
101
+ if (process.env.MOCODE_NO_SPAWN)
102
+ return; // 测试门控:禁真跑 npm
103
+ try {
104
+ const child = spawn('npm', ['install', '-g', `${PKG_NAME}@latest`], {
105
+ shell: true,
106
+ detached: true,
107
+ stdio: 'ignore',
108
+ windowsHide: true,
109
+ });
110
+ child.unref();
111
+ child.on('error', () => { });
112
+ }
113
+ catch {
114
+ // 忽略
115
+ }
116
+ }
117
+ /** 简易 semver 比较(只管 X.Y.Z 数字,无 pre-release):a>b 返正,a<b 返负,等返 0。 */
118
+ export function compareSemver(a, b) {
119
+ const pa = a.split('.').map((x) => Number(x) || 0);
120
+ const pb = b.split('.').map((x) => Number(x) || 0);
121
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
122
+ const da = pa[i] ?? 0;
123
+ const db = pb[i] ?? 0;
124
+ if (da !== db)
125
+ return da - db;
126
+ }
127
+ return 0;
128
+ }
129
+ /** 纯决策:据当前版本 / 缓存 / 时间,算是否提示 / spawn / 后台刷新。无副作用,可离线测。 */
130
+ export function evaluateUpdate(current, cache, now) {
131
+ const refresh = !cache.latest || now - (cache.lastCheck ?? 0) > CHECK_INTERVAL_MS;
132
+ let notice = null;
133
+ let spawn = false;
134
+ if (cache.latest && compareSemver(cache.latest, current) > 0) {
135
+ notice = `检测到新版本 ${cache.latest},后台更新中,下次启动生效。`;
136
+ spawn = now - (cache.lastSpawn ?? 0) > SPAWN_INTERVAL_MS;
137
+ }
138
+ return { notice, spawn, refresh };
139
+ }
140
+ /**
141
+ * 启动时调(同步,零延迟):返一行纯文本更新提示或 null。
142
+ * 流程:dev 跳过 → 读当前版本 → 读缓存 → evaluateUpdate 决策 → 按需后台刷新 / spawn → 返提示。
143
+ * 提示由 repl 上色写入内容区开场段(进 INPUT 态前)。
144
+ */
145
+ export function checkAndMaybeUpdate() {
146
+ if (isDevRun())
147
+ return null;
148
+ const current = readCurrentVersion();
149
+ if (!current)
150
+ return null;
151
+ const cache = readCache();
152
+ const now = Date.now();
153
+ const d = evaluateUpdate(current, cache, now);
154
+ if (d.refresh)
155
+ void fetchLatestInBackground(); // fire-and-forget:刷新缓存供下次启动用
156
+ if (d.spawn) {
157
+ spawnUpdate();
158
+ writeCache({ lastSpawn: now });
159
+ }
160
+ return d.notice;
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 9 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {