umadev 1.0.7 → 1.0.9

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/bin/cli.js +224 -22
  2. package/package.json +7 -8
package/bin/cli.js CHANGED
@@ -16,6 +16,8 @@
16
16
  const { spawnSync } = require('node:child_process');
17
17
  const fs = require('node:fs');
18
18
  const path = require('node:path');
19
+ const https = require('node:https');
20
+ const os = require('node:os');
19
21
 
20
22
  // Node platform/arch → our sub-package name.
21
23
  const PLATFORM_PACKAGES = {
@@ -105,32 +107,232 @@ function findKnowledgeDir() {
105
107
  return null;
106
108
  }
107
109
 
108
- const binary = findBinary();
109
- // npm artifact round-trips (upload/download-artifact in CI) can strip the
110
- // executable bit off the prebuilt binary; restore it defensively before exec.
111
- try {
112
- fs.chmodSync(binary, 0o755);
113
- } catch (_) {
114
- // read-only install dir or already +x — spawnSync below reports real errors
110
+ // ── Local embedding model — ensure it's on disk, else download it (with a
111
+ // progress bar) from THIS version's GitHub Release. Checked on EVERY launch
112
+ // (a cheap stat); the ~224MB fp16 model is too large for npm, so it's a
113
+ // one-time fetch into ~/.umadev/embed-model. Fail-open: any failure launches
114
+ // anyway and the binary degrades to BM25 lexical retrieval, retrying next time.
115
+ function homeDir() {
116
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
115
117
  }
116
- const extraEnv = {};
117
- const modelDir = findModelDir();
118
- if (modelDir && !process.env.UMADEV_EMBED_MODEL_DIR) {
119
- extraEnv.UMADEV_EMBED_MODEL_DIR = modelDir;
118
+ function modelTargetDir() {
119
+ return path.join(homeDir(), '.umadev', 'embed-model');
120
120
  }
121
- const knowledgeDir = findKnowledgeDir();
122
- if (knowledgeDir && !process.env.UMADEV_KNOWLEDGE_DIR) {
123
- extraEnv.UMADEV_KNOWLEDGE_DIR = knowledgeDir;
121
+ const MODEL_FILES = ['config.json', 'tokenizer.json', 'model.safetensors'];
122
+ function modelPresent(dir) {
123
+ return MODEL_FILES.every((f) => {
124
+ try {
125
+ return fs.statSync(path.join(dir, f)).size > 0;
126
+ } catch (_) {
127
+ return false;
128
+ }
129
+ });
124
130
  }
125
- const spawnOpts = { stdio: 'inherit' };
126
- if (Object.keys(extraEnv).length > 0) {
127
- spawnOpts.env = { ...process.env, ...extraEnv };
131
+ // Render one frame of the download progress bar (in place, via \r). Block-glyph
132
+ // bar + percent + downloaded/total + live speed; ANSI-colored only on a TTY so a
133
+ // piped/redirected install stays clean.
134
+ function drawBar(label, got, total, startTime) {
135
+ const tty = process.stderr.isTTY;
136
+ const c = (code) => (tty ? '\x1b[' + code + 'm' : '');
137
+ const w = 22;
138
+ const ratio = total > 0 ? Math.min(1, got / total) : 0;
139
+ const fill = Math.round(ratio * w);
140
+ const bar = c('38;5;45') + '█'.repeat(fill) + c('0') + c('38;5;238') + '░'.repeat(w - fill) + c('0');
141
+ const pct = String(Math.floor(ratio * 100)).padStart(3);
142
+ const mb = (got / 1048576).toFixed(1);
143
+ const tot = (total / 1048576).toFixed(0);
144
+ const sec = (Date.now() - startTime) / 1000;
145
+ const spd = sec > 0.3 ? (got / 1048576 / sec).toFixed(1) + ' MB/s' : '…';
146
+ process.stderr.write(
147
+ '\r ' + c('1') + label + c('0') + ' ' + bar + ' ' + c('1') + pct + '%' + c('0') +
148
+ c('2') + ' · ' + mb + '/' + tot + ' MB · ' + spd + c('0') + ' ',
149
+ );
150
+ }
151
+ // Download one URL to `dest`, following redirects (GitHub → CDN), drawing a
152
+ // progress bar when `withBar`. Resolves on success, rejects on any error.
153
+ function downloadTo(url, dest, withBar, label) {
154
+ return new Promise((resolve, reject) => {
155
+ const req = https.get(
156
+ url,
157
+ { headers: { 'User-Agent': 'umadev-cli', Accept: 'application/octet-stream' } },
158
+ (res) => {
159
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
160
+ res.resume();
161
+ downloadTo(res.headers.location, dest, withBar, label).then(resolve, reject);
162
+ return;
163
+ }
164
+ if (res.statusCode !== 200) {
165
+ res.resume();
166
+ reject(new Error('HTTP ' + res.statusCode));
167
+ return;
168
+ }
169
+ const total = parseInt(res.headers['content-length'] || '0', 10);
170
+ let got = 0;
171
+ let lastPct = -1;
172
+ let lastDraw = 0;
173
+ const startTime = Date.now();
174
+ const tmp = dest + '.part';
175
+ const out = fs.createWriteStream(tmp);
176
+ // Draw the bar at 0% the instant the response starts — on a slow link the
177
+ // first 1% can take a while, and a silent gap reads as "stuck / failed".
178
+ if (withBar && total > 0) drawBar(label, 0, total, startTime);
179
+ res.on('data', (chunk) => {
180
+ got += chunk.length;
181
+ if (withBar && total > 0) {
182
+ const now = Date.now();
183
+ const pct = Math.floor((got / total) * 100);
184
+ // Redraw on each new percent OR every ~250ms — keeps the live speed
185
+ // ticking even while a single percent of a 224MB file streams in.
186
+ if (pct !== lastPct || now - lastDraw > 250) {
187
+ lastPct = pct;
188
+ lastDraw = now;
189
+ drawBar(label, got, total, startTime);
190
+ }
191
+ }
192
+ });
193
+ res.pipe(out);
194
+ out.on('finish', () =>
195
+ out.close((e) => {
196
+ if (e) return reject(e);
197
+ try {
198
+ fs.renameSync(tmp, dest);
199
+ } catch (er) {
200
+ return reject(er);
201
+ }
202
+ if (withBar && total > 0) {
203
+ drawBar(label, total, total, startTime);
204
+ process.stderr.write('\n');
205
+ }
206
+ resolve();
207
+ }),
208
+ );
209
+ out.on('error', reject);
210
+ },
211
+ );
212
+ req.on('error', reject);
213
+ req.setTimeout(120000, () => req.destroy(new Error('timeout')));
214
+ });
215
+ }
216
+ // Ordered list of base URLs to try for the release assets. An explicit override
217
+ // (UMADEV_MODEL_BASE_URL) wins; otherwise zh-CN / China-timezone users get GitHub
218
+ // PROXY MIRRORS first (github.com's release CDN is frequently slow or blocked in
219
+ // mainland China), and everyone else gets github.com first — with the others as
220
+ // fallback either way, so a blocked github.com or a down mirror still recovers.
221
+ function releaseBases(version) {
222
+ if (process.env.UMADEV_MODEL_BASE_URL) {
223
+ return [process.env.UMADEV_MODEL_BASE_URL.replace(/\/+$/, '')];
224
+ }
225
+ // GitHub Release ships the quantized fp16 model (~224MB, smaller). HuggingFace
226
+ // and its China mirror hf-mirror.com serve the upstream f32 model (~448MB —
227
+ // bigger, but the candle loader handles either). hf-mirror is the FAST + reliable
228
+ // source inside mainland China, where github.com's release CDN is slow and the
229
+ // community GitHub proxies are flaky for release-asset URLs.
230
+ const gh = 'https://github.com/umacloud/umadev/releases/download/v' + version;
231
+ const ghProxies = ['https://ghproxy.net/' + gh, 'https://ghfast.top/' + gh];
232
+ const hf = 'https://huggingface.co/intfloat/multilingual-e5-small/resolve/main';
233
+ const hfMirror = 'https://hf-mirror.com/intfloat/multilingual-e5-small/resolve/main';
234
+ let cn = false;
235
+ try {
236
+ const opts = Intl.DateTimeFormat().resolvedOptions();
237
+ const tz = opts.timeZone || '';
238
+ const loc = (process.env.LANG || process.env.LC_ALL || '') + ' ' + (opts.locale || '');
239
+ cn =
240
+ /Shanghai|Chongqing|Urumqi|Harbin|Hong_Kong|Macau/.test(tz) ||
241
+ /zh[_-]?(CN|Hans)/i.test(loc);
242
+ } catch (_) {
243
+ /* default to international order */
244
+ }
245
+ // China: hf-mirror first (fast + reliable in CN), then GitHub proxies + direct.
246
+ // International: GitHub Release first (smaller fp16), then HuggingFace + mirror.
247
+ return cn ? [hfMirror, ...ghProxies, gh, hf] : [gh, hf, hfMirror, ...ghProxies];
248
+ }
249
+ // Try each base for `name` in order; resolve on first success, throw the last
250
+ // error if all fail. A China mirror can cover a blocked github.com (or vice
251
+ // versa) with zero user configuration.
252
+ async function downloadFile(bases, name, dest, withBar, label) {
253
+ let lastErr;
254
+ for (const base of bases) {
255
+ try {
256
+ await downloadTo(base + '/' + name, dest, withBar, label);
257
+ return;
258
+ } catch (e) {
259
+ lastErr = e;
260
+ }
261
+ }
262
+ throw lastErr || new Error('no source reachable');
263
+ }
264
+ async function ensureModel() {
265
+ const dir = modelTargetDir();
266
+ if (modelPresent(dir)) return dir; // already installed — fast path, no network
267
+ let version = '0.0.0';
268
+ try {
269
+ version = require('../package.json').version;
270
+ } catch (_) {
271
+ /* keep default */
272
+ }
273
+ const bases = releaseBases(version);
274
+ try {
275
+ fs.mkdirSync(dir, { recursive: true });
276
+ process.stderr.write(
277
+ '\n 本地向量检索模型缺失,正在下载 multilingual-e5-small(国内自动走镜像)…\n',
278
+ );
279
+ process.stderr.write(
280
+ ' 一次性下载;之后完全本地、运行时无需联网。失败不影响使用(降级为 BM25)。\n',
281
+ );
282
+ await downloadFile(bases, 'config.json', path.join(dir, 'config.json'), false, '');
283
+ await downloadFile(bases, 'tokenizer.json', path.join(dir, 'tokenizer.json'), true, '下载分词器 ');
284
+ await downloadFile(
285
+ bases,
286
+ 'model.safetensors',
287
+ path.join(dir, 'model.safetensors'),
288
+ true,
289
+ '下载向量模型',
290
+ );
291
+ process.stderr.write(' 本地向量模型就绪 ✓\n\n');
292
+ return dir;
293
+ } catch (e) {
294
+ process.stderr.write(
295
+ '\n [提示] 向量模型下载未完成 (' +
296
+ e.message +
297
+ ');本次用 BM25 检索,下次启动重试。\n\n',
298
+ );
299
+ return null;
300
+ }
128
301
  }
129
- const result = spawnSync(binary, process.argv.slice(2), spawnOpts);
130
302
 
131
- if (result.error) {
132
- console.error(`umadev: failed to exec binary: ${result.error.message}`);
133
- process.exit(1);
303
+ async function main() {
304
+ const binary = findBinary();
305
+ // npm artifact round-trips (upload/download-artifact in CI) can strip the
306
+ // executable bit off the prebuilt binary; restore it defensively before exec.
307
+ try {
308
+ fs.chmodSync(binary, 0o755);
309
+ } catch (_) {
310
+ // read-only install dir or already +x — spawnSync below reports real errors
311
+ }
312
+ const extraEnv = {};
313
+ // Prefer a bundled npm model package (dev / sibling layout); otherwise fetch
314
+ // it on demand into ~/.umadev/embed-model (the binary's model_dir() fallback).
315
+ let modelDir = findModelDir();
316
+ if (!modelDir) modelDir = await ensureModel();
317
+ if (modelDir && !process.env.UMADEV_EMBED_MODEL_DIR) {
318
+ extraEnv.UMADEV_EMBED_MODEL_DIR = modelDir;
319
+ }
320
+ const knowledgeDir = findKnowledgeDir();
321
+ if (knowledgeDir && !process.env.UMADEV_KNOWLEDGE_DIR) {
322
+ extraEnv.UMADEV_KNOWLEDGE_DIR = knowledgeDir;
323
+ }
324
+ const spawnOpts = { stdio: 'inherit' };
325
+ if (Object.keys(extraEnv).length > 0) {
326
+ spawnOpts.env = { ...process.env, ...extraEnv };
327
+ }
328
+ const result = spawnSync(binary, process.argv.slice(2), spawnOpts);
329
+
330
+ if (result.error) {
331
+ console.error(`umadev: failed to exec binary: ${result.error.message}`);
332
+ process.exit(1);
333
+ }
334
+
335
+ process.exit(result.status === null ? 1 : result.status);
134
336
  }
135
337
 
136
- process.exit(result.status === null ? 1 : result.status);
338
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umadev",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "A project-director Agent for AI coding hosts — drives your logged-in Claude Code / Codex through a 9-phase commercial delivery pipeline with governance. No API key needed.",
5
5
  "keywords": [
6
6
  "ai",
@@ -37,12 +37,11 @@
37
37
  "node": ">=18"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@umacloud/cli-darwin-arm64": "1.0.7",
41
- "@umacloud/cli-darwin-x64": "1.0.7",
42
- "@umacloud/cli-linux-x64": "1.0.7",
43
- "@umacloud/cli-linux-arm64": "1.0.7",
44
- "@umacloud/cli-win32-x64": "1.0.7",
45
- "@umacloud/model-e5-small": "1.0.7",
46
- "@umacloud/knowledge": "1.0.7"
40
+ "@umacloud/cli-darwin-arm64": "1.0.9",
41
+ "@umacloud/cli-darwin-x64": "1.0.9",
42
+ "@umacloud/cli-linux-x64": "1.0.9",
43
+ "@umacloud/cli-linux-arm64": "1.0.9",
44
+ "@umacloud/cli-win32-x64": "1.0.9",
45
+ "@umacloud/knowledge": "1.0.9"
47
46
  }
48
47
  }