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.
Files changed (2) hide show
  1. package/bin/cli.js +194 -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,202 @@ 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
+ // 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
- if (result.error) {
132
- console.error(`umadev: failed to exec binary: ${result.error.message}`);
133
- process.exit(1);
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
- process.exit(result.status === null ? 1 : result.status);
308
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umadev",
3
- "version": "1.0.7",
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.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.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
  }