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.
- package/bin/cli.js +224 -22
- 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
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
338
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "umadev",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
41
|
-
"@umacloud/cli-darwin-x64": "1.0.
|
|
42
|
-
"@umacloud/cli-linux-x64": "1.0.
|
|
43
|
-
"@umacloud/cli-linux-arm64": "1.0.
|
|
44
|
-
"@umacloud/cli-win32-x64": "1.0.
|
|
45
|
-
"@umacloud/
|
|
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
|
}
|