umadev 1.0.7 → 1.0.8
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 +194 -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,202 @@ 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
|
+
// Download one URL to `dest`, following redirects (GitHub → CDN), drawing a
|
|
132
|
+
// progress bar when `withBar`. Resolves on success, rejects on any error.
|
|
133
|
+
function downloadTo(url, dest, withBar, label) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const req = https.get(
|
|
136
|
+
url,
|
|
137
|
+
{ headers: { 'User-Agent': 'umadev-cli', Accept: 'application/octet-stream' } },
|
|
138
|
+
(res) => {
|
|
139
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
140
|
+
res.resume();
|
|
141
|
+
downloadTo(res.headers.location, dest, withBar, label).then(resolve, reject);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (res.statusCode !== 200) {
|
|
145
|
+
res.resume();
|
|
146
|
+
reject(new Error('HTTP ' + res.statusCode));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const total = parseInt(res.headers['content-length'] || '0', 10);
|
|
150
|
+
let got = 0;
|
|
151
|
+
let lastPct = -1;
|
|
152
|
+
const tmp = dest + '.part';
|
|
153
|
+
const out = fs.createWriteStream(tmp);
|
|
154
|
+
res.on('data', (chunk) => {
|
|
155
|
+
got += chunk.length;
|
|
156
|
+
if (withBar && total > 0) {
|
|
157
|
+
const pct = Math.floor((got / total) * 100);
|
|
158
|
+
if (pct !== lastPct) {
|
|
159
|
+
lastPct = pct;
|
|
160
|
+
const w = 24;
|
|
161
|
+
const fill = Math.round((pct / 100) * w);
|
|
162
|
+
const bar = '#'.repeat(fill) + '-'.repeat(w - fill);
|
|
163
|
+
const mb = (got / 1048576).toFixed(0);
|
|
164
|
+
const tot = (total / 1048576).toFixed(0);
|
|
165
|
+
process.stderr.write(
|
|
166
|
+
'\r ' + label + ' [' + bar + '] ' + pct + '% (' + mb + '/' + tot + ' MB)',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
res.pipe(out);
|
|
172
|
+
out.on('finish', () =>
|
|
173
|
+
out.close((e) => {
|
|
174
|
+
if (e) return reject(e);
|
|
175
|
+
try {
|
|
176
|
+
fs.renameSync(tmp, dest);
|
|
177
|
+
} catch (er) {
|
|
178
|
+
return reject(er);
|
|
179
|
+
}
|
|
180
|
+
if (withBar) process.stderr.write('\n');
|
|
181
|
+
resolve();
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
out.on('error', reject);
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
req.on('error', reject);
|
|
188
|
+
req.setTimeout(120000, () => req.destroy(new Error('timeout')));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Ordered list of base URLs to try for the release assets. An explicit override
|
|
192
|
+
// (UMADEV_MODEL_BASE_URL) wins; otherwise zh-CN / China-timezone users get GitHub
|
|
193
|
+
// PROXY MIRRORS first (github.com's release CDN is frequently slow or blocked in
|
|
194
|
+
// mainland China), and everyone else gets github.com first — with the others as
|
|
195
|
+
// fallback either way, so a blocked github.com or a down mirror still recovers.
|
|
196
|
+
function releaseBases(version) {
|
|
197
|
+
if (process.env.UMADEV_MODEL_BASE_URL) {
|
|
198
|
+
return [process.env.UMADEV_MODEL_BASE_URL.replace(/\/+$/, '')];
|
|
199
|
+
}
|
|
200
|
+
const gh = 'https://github.com/umacloud/umadev/releases/download/v' + version;
|
|
201
|
+
const mirrors = [
|
|
202
|
+
'https://ghproxy.net/' + gh,
|
|
203
|
+
'https://ghfast.top/' + gh,
|
|
204
|
+
'https://gh-proxy.com/' + gh,
|
|
205
|
+
];
|
|
206
|
+
let cn = false;
|
|
207
|
+
try {
|
|
208
|
+
const opts = Intl.DateTimeFormat().resolvedOptions();
|
|
209
|
+
const tz = opts.timeZone || '';
|
|
210
|
+
const loc = (process.env.LANG || process.env.LC_ALL || '') + ' ' + (opts.locale || '');
|
|
211
|
+
cn =
|
|
212
|
+
/Shanghai|Chongqing|Urumqi|Harbin|Hong_Kong|Macau/.test(tz) ||
|
|
213
|
+
/zh[_-]?(CN|Hans)/i.test(loc);
|
|
214
|
+
} catch (_) {
|
|
215
|
+
/* default to direct-first */
|
|
216
|
+
}
|
|
217
|
+
return cn ? [...mirrors, gh] : [gh, ...mirrors];
|
|
218
|
+
}
|
|
219
|
+
// Try each base for `name` in order; resolve on first success, throw the last
|
|
220
|
+
// error if all fail. A China mirror can cover a blocked github.com (or vice
|
|
221
|
+
// versa) with zero user configuration.
|
|
222
|
+
async function downloadFile(bases, name, dest, withBar, label) {
|
|
223
|
+
let lastErr;
|
|
224
|
+
for (const base of bases) {
|
|
225
|
+
try {
|
|
226
|
+
await downloadTo(base + '/' + name, dest, withBar, label);
|
|
227
|
+
return;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
lastErr = e;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw lastErr || new Error('no source reachable');
|
|
233
|
+
}
|
|
234
|
+
async function ensureModel() {
|
|
235
|
+
const dir = modelTargetDir();
|
|
236
|
+
if (modelPresent(dir)) return dir; // already installed — fast path, no network
|
|
237
|
+
let version = '0.0.0';
|
|
238
|
+
try {
|
|
239
|
+
version = require('../package.json').version;
|
|
240
|
+
} catch (_) {
|
|
241
|
+
/* keep default */
|
|
242
|
+
}
|
|
243
|
+
const bases = releaseBases(version);
|
|
244
|
+
try {
|
|
245
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
246
|
+
process.stderr.write(
|
|
247
|
+
'\n 本地向量检索模型缺失,正在下载 (multilingual-e5-small · fp16 · ~224MB)…\n',
|
|
248
|
+
);
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
' 一次性下载;之后完全本地、运行时无需联网。失败不影响使用(降级为 BM25)。\n',
|
|
251
|
+
);
|
|
252
|
+
await downloadFile(bases, 'config.json', path.join(dir, 'config.json'), false, '');
|
|
253
|
+
await downloadFile(bases, 'tokenizer.json', path.join(dir, 'tokenizer.json'), false, '');
|
|
254
|
+
await downloadFile(
|
|
255
|
+
bases,
|
|
256
|
+
'model.safetensors',
|
|
257
|
+
path.join(dir, 'model.safetensors'),
|
|
258
|
+
true,
|
|
259
|
+
'下载向量模型',
|
|
260
|
+
);
|
|
261
|
+
process.stderr.write(' 本地向量模型就绪 ✓\n\n');
|
|
262
|
+
return dir;
|
|
263
|
+
} catch (e) {
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
'\n [提示] 向量模型下载未完成 (' +
|
|
266
|
+
e.message +
|
|
267
|
+
');本次用 BM25 检索,下次启动重试。\n\n',
|
|
268
|
+
);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
128
271
|
}
|
|
129
|
-
const result = spawnSync(binary, process.argv.slice(2), spawnOpts);
|
|
130
272
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
273
|
+
async function main() {
|
|
274
|
+
const binary = findBinary();
|
|
275
|
+
// npm artifact round-trips (upload/download-artifact in CI) can strip the
|
|
276
|
+
// executable bit off the prebuilt binary; restore it defensively before exec.
|
|
277
|
+
try {
|
|
278
|
+
fs.chmodSync(binary, 0o755);
|
|
279
|
+
} catch (_) {
|
|
280
|
+
// read-only install dir or already +x — spawnSync below reports real errors
|
|
281
|
+
}
|
|
282
|
+
const extraEnv = {};
|
|
283
|
+
// Prefer a bundled npm model package (dev / sibling layout); otherwise fetch
|
|
284
|
+
// it on demand into ~/.umadev/embed-model (the binary's model_dir() fallback).
|
|
285
|
+
let modelDir = findModelDir();
|
|
286
|
+
if (!modelDir) modelDir = await ensureModel();
|
|
287
|
+
if (modelDir && !process.env.UMADEV_EMBED_MODEL_DIR) {
|
|
288
|
+
extraEnv.UMADEV_EMBED_MODEL_DIR = modelDir;
|
|
289
|
+
}
|
|
290
|
+
const knowledgeDir = findKnowledgeDir();
|
|
291
|
+
if (knowledgeDir && !process.env.UMADEV_KNOWLEDGE_DIR) {
|
|
292
|
+
extraEnv.UMADEV_KNOWLEDGE_DIR = knowledgeDir;
|
|
293
|
+
}
|
|
294
|
+
const spawnOpts = { stdio: 'inherit' };
|
|
295
|
+
if (Object.keys(extraEnv).length > 0) {
|
|
296
|
+
spawnOpts.env = { ...process.env, ...extraEnv };
|
|
297
|
+
}
|
|
298
|
+
const result = spawnSync(binary, process.argv.slice(2), spawnOpts);
|
|
299
|
+
|
|
300
|
+
if (result.error) {
|
|
301
|
+
console.error(`umadev: failed to exec binary: ${result.error.message}`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
134
306
|
}
|
|
135
307
|
|
|
136
|
-
|
|
308
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "umadev",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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.8",
|
|
41
|
+
"@umacloud/cli-darwin-x64": "1.0.8",
|
|
42
|
+
"@umacloud/cli-linux-x64": "1.0.8",
|
|
43
|
+
"@umacloud/cli-linux-arm64": "1.0.8",
|
|
44
|
+
"@umacloud/cli-win32-x64": "1.0.8",
|
|
45
|
+
"@umacloud/knowledge": "1.0.8"
|
|
47
46
|
}
|
|
48
47
|
}
|