letfixit 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "letfixit",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "LetFixIt — AI-based realtime performance analysis & suggestions for Flutter, Android & iOS, powered by the local Ultron n0-beta model.",
5
- "keywords": ["flutter", "android", "ios", "performance", "profiler", "devtools", "observability", "llm", "local-ai"],
5
+ "keywords": [
6
+ "flutter",
7
+ "android",
8
+ "ios",
9
+ "performance",
10
+ "profiler",
11
+ "devtools",
12
+ "observability",
13
+ "llm",
14
+ "local-ai"
15
+ ],
6
16
  "license": "MIT",
7
17
  "author": "JahidDev24",
8
18
  "main": "server/index.js",
@@ -49,7 +59,10 @@
49
59
  "setup:force": "node scripts/setup-model.js --force",
50
60
  "encrypt-model": "node scripts/encrypt-model.js",
51
61
  "fill-manifest": "node scripts/fill-manifest.js",
62
+ "verify": "node scripts/verify-chunk.js",
63
+ "clean": "node scripts/clean-model.js",
52
64
  "postinstall": "node scripts/setup-model.js",
65
+ "preuninstall": "node scripts/clean-model.js",
53
66
  "prepublishOnly": "npm run build"
54
67
  },
55
68
  "dependencies": {
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // ── Cleanup ───────────────────────────────────────────────────────────────────
3
+ // Removes the downloaded model and any temp/partial files so nothing is left on
4
+ // disk. Runs as a `preuninstall` hook (best-effort — npm's uninstall hooks are
5
+ // unreliable across versions) and is also exposed as `letfixit clean` /
6
+ // `npm run clean` for a guaranteed manual wipe.
7
+ const fs = require('fs');
8
+ const mc = require('../server/model_config');
9
+
10
+ function clean() {
11
+ let removed = false;
12
+ try {
13
+ if (fs.existsSync(mc.MODEL_DIR)) {
14
+ const gb = mc.sizeGB();
15
+ fs.rmSync(mc.MODEL_DIR, { recursive: true, force: true }); // model + .tmp partials
16
+ console.log(` ✓ Removed ${mc.MODEL_NAME}${gb ? ` (${gb} GB)` : ''} — ${mc.MODEL_DIR}`);
17
+ removed = true;
18
+ }
19
+ } catch (e) {
20
+ // Never fail an uninstall on cleanup trouble.
21
+ console.log(` ⚠ cleanup note: ${e.message}`);
22
+ }
23
+ if (!removed) console.log(' ✓ No model files to remove.');
24
+ }
25
+
26
+ if (require.main === module) clean();
27
+ module.exports = { clean };
@@ -142,7 +142,7 @@ async function fetchChunk(ch, encPath, baseLabel) {
142
142
  async function resolveKey() {
143
143
  if (process.env.PARITYFIX_MODEL_KEY) return process.env.PARITYFIX_MODEL_KEY.trim();
144
144
  if (!process.stdin.isTTY) return null; // can't prompt (postinstall in CI, piped stdin)
145
- const k = await promptHidden(` 🔑 Enter the ${mc.MODEL_NAME} decryption key: `);
145
+ const k = await promptHidden(' 🔑 Enter acceptance key: ');
146
146
  return k || null;
147
147
  }
148
148
 
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ // ── Quick verify ──────────────────────────────────────────────────────────────
3
+ // Downloads ONLY the first chunk from the manifest and decrypts it, to confirm
4
+ // the acceptance key + hosted chunks + manifest all line up — without pulling the
5
+ // full ~2.6 GB model.
6
+ //
7
+ // PARITYFIX_MODEL_KEY=<key> npm run verify (or: node scripts/verify-chunk.js)
8
+ // node scripts/verify-chunk.js <key> (pass the key as an argument)
9
+ // If no key is given it prompts (hidden input).
10
+ require('./load-env')();
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const readline = require('readline');
15
+ const mc = require('../server/model_config');
16
+ const { deriveKey } = require('./model_crypto');
17
+ const sm = require('./setup-model'); // fetchChunk, decryptChunk, sha256File
18
+
19
+ const C = { reset: '\x1b[0m', mag: '\x1b[95m', grn: '\x1b[92m', red: '\x1b[91m', dim: '\x1b[2m', cyan: '\x1b[96m' };
20
+ const c = (x, s) => `${x}${s}${C.reset}`;
21
+
22
+ function promptHidden(q) {
23
+ return new Promise((resolve) => {
24
+ process.stdout.write(c(C.mag, q));
25
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
26
+ rl._writeToOutput = () => {};
27
+ rl.question('', (a) => { rl.close(); process.stdout.write('\n'); resolve(a.trim()); });
28
+ });
29
+ }
30
+
31
+ async function run(keyArg) {
32
+ const manifest = mc.loadManifest();
33
+ if (!manifest || !manifest.chunks || !manifest.chunks.length) {
34
+ console.error(c(C.red, ' ❌ No manifest found (config/model.manifest.json).'));
35
+ process.exit(1);
36
+ }
37
+
38
+ let key = process.env.PARITYFIX_MODEL_KEY || keyArg;
39
+ if (!key && process.stdin.isTTY) key = await promptHidden(' 🔑 Enter acceptance key: ');
40
+ if (!key) {
41
+ console.error(c(C.red, ' ❌ No key. Set PARITYFIX_MODEL_KEY, pass it as an argument, or run in a terminal.'));
42
+ process.exit(1);
43
+ }
44
+
45
+ const ch = [...manifest.chunks].sort((a, b) => a.index - b.index)[0];
46
+ const tmp = path.join(os.tmpdir(), 'letfixit-verify');
47
+ fs.mkdirSync(tmp, { recursive: true });
48
+ const enc = path.join(tmp, 'chunk0.enc');
49
+ const dec = path.join(tmp, 'chunk0.dec');
50
+
51
+ console.log(c(C.dim, ` Verifying chunk 0 of ${manifest.chunks.length} — ${ch.url}\n`));
52
+ let ok = false;
53
+ try {
54
+ await sm.fetchChunk(ch, enc, c(C.cyan, 'chunk 0')); // downloads + verifies sha256
55
+ await sm.decryptChunk(enc, dec, deriveKey(key.trim(), manifest.salt), ch.iv, ch.authTag);
56
+ const size = fs.statSync(dec).size;
57
+ ok = size === (ch.bytes || size);
58
+ console.log('\n ' + c(C.grn, '✅ VERIFIED') + c(C.dim, ` — download + checksum + decrypt all passed (${(size / 1048576).toFixed(1)} MB plaintext).`));
59
+ console.log(' ' + c(C.dim, 'Your acceptance key and hosted chunks are correct; the full setup will work.'));
60
+ } catch (e) {
61
+ if (/authenticate|unable to authenticate|bad decrypt/i.test(e.message)) {
62
+ console.log('\n ' + c(C.red, '❌ Decryption failed — the acceptance key is incorrect for these chunks.'));
63
+ } else {
64
+ console.log('\n ' + c(C.red, `❌ Verify failed: ${e.message}`));
65
+ }
66
+ } finally {
67
+ fs.rmSync(tmp, { recursive: true, force: true });
68
+ }
69
+ process.exit(ok ? 0 : 1);
70
+ }
71
+
72
+ if (require.main === module) run(process.argv[2]); // node scripts/verify-chunk.js [key]
73
+ module.exports = { run };
package/server/index.js CHANGED
@@ -29,7 +29,7 @@ const ENABLE_LOCAL_AI = process.env.ENABLE_LOCAL_AI !== 'false';
29
29
  const MODEL_PATH = model.MODEL_PATH;
30
30
 
31
31
  // ── Terminal styling + branded startup ─────────────────────────────────────────
32
- const C = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', mag: '\x1b[95m', grn: '\x1b[92m', yel: '\x1b[93m', cyan: '\x1b[96m' };
32
+ const C = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', mag: '\x1b[95m', grn: '\x1b[92m', yel: '\x1b[93m', red: '\x1b[91m', cyan: '\x1b[96m' };
33
33
  const col = (x, s) => `${x}${s}${C.reset}`;
34
34
  const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
35
35
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -42,10 +42,11 @@ function bannerBox(lines) {
42
42
  }
43
43
 
44
44
  function printWelcome() {
45
+ const v = require('../package.json').version;
45
46
  console.log('');
46
47
  bannerBox([
47
48
  '',
48
- col(C.bold + C.mag, '✦ P A R I T Y F I X') + col(C.dim, ' v0.1.0'),
49
+ col(C.bold + C.mag, '✦ P A R I T Y F I X') + col(C.dim, ` v${v}`),
49
50
  col(C.reset, 'AI-based realtime performance analysis & suggestions'),
50
51
  col(C.dim, 'Flutter · Android · iOS powered by Ultron n0-beta'),
51
52
  '',
@@ -53,32 +54,40 @@ function printWelcome() {
53
54
  console.log('');
54
55
  }
55
56
 
56
- // "Ultron n0-beta initializing…" starts the local model and waits briefly for
57
- // it to answer, without blocking the whole boot on weight loading.
57
+ // Boot the local model. On first run (model not installed) this RUNS the setup
58
+ // download + key prompt + decrypt + merge then starts the model. Returns
59
+ // true if AI is available (local model ready, or a cloud provider configured).
58
60
  async function initUltron() {
59
61
  if (!ENABLE_LOCAL_AI) {
60
62
  console.log(' ' + col(C.dim, 'Ultron n0-beta is disabled (ENABLE_LOCAL_AI=false) — using cloud fallback if configured.\n'));
61
- return;
63
+ return aiStatus().anyAvailable;
62
64
  }
65
+
66
+ // First-time setup: install the model (prompts for the decryption key).
63
67
  if (!model.isReady()) {
64
- console.log(' ' + col(C.yel, '⚠ Ultron n0-beta not installed.') + ' Run ' + col(C.cyan, 'npm run setup') + ' to download it.');
65
- const s = aiStatus();
66
- console.log(' ' + col(C.dim, s.anyAvailable ? 'A cloud fallback key is set — analysis will use it for now.\n' : 'No AI provider yet — findings will show without suggestions until setup.\n'));
67
- return;
68
+ console.log(' ' + col(C.mag, ' First-time setup') + col(C.dim, ` installing ${model.MODEL_NAME}. This runs once.\n`));
69
+ try {
70
+ await require('../scripts/setup-model').setup();
71
+ } catch (e) {
72
+ const msg = String(e.message || e);
73
+ if (msg.startsWith('WRONG_KEY:')) console.log('\n ' + col(C.red, '❌ ' + msg.replace('WRONG_KEY: ', '')));
74
+ else console.log('\n ' + col(C.red, '❌ Setup failed: ' + msg));
75
+ }
76
+ console.log('');
68
77
  }
69
78
 
70
- process.stdout.write(' ' + col(C.mag, '✦ Ultron n0-beta') + col(C.dim, ' initializing'));
71
- startLlamafile();
72
- // Poll health for a few seconds; don't hang the boot if weights are still loading.
73
- let ok = false;
74
- for (let i = 0; i < 8 && !ok; i++) { process.stdout.write(col(C.dim, '.')); ok = await isLlamaHealthy(); if (!ok) await sleep(500); }
75
- process.stdout.write('\n');
76
- if (ok) {
77
- console.log(' ' + col(C.grn, ' ready') + col(C.dim, `${model.MODEL_NAME} running locally on :${LOCAL_AI_PORT} (${model.sizeGB()} GB, private)`));
78
- } else {
79
- console.log(' ' + col(C.yel, '… still loading') + col(C.dim, ' — weights load in the background (first start can take ~20s).'));
79
+ if (model.isReady()) {
80
+ process.stdout.write(' ' + col(C.mag, '✦ Ultron n0-beta') + col(C.dim, ' initializing'));
81
+ startLlamafile();
82
+ let ok = false;
83
+ for (let i = 0; i < 8 && !ok; i++) { process.stdout.write(col(C.dim, '.')); ok = await isLlamaHealthy(); if (!ok) await sleep(500); }
84
+ process.stdout.write('\n');
85
+ if (ok) console.log(' ' + col(C.grn, '✓ ready') + col(C.dim, ` — ${model.MODEL_NAME} running locally on :${LOCAL_AI_PORT} (${model.sizeGB()} GB, private)`));
86
+ else console.log(' ' + col(C.yel, ' still loading') + col(C.dim, 'weights load in the background (first start can take ~20s).'));
87
+ console.log(' ' + col(C.dim, model.DISCLAIMER.split(' — ')[0] + ' — treat suggestions as hints.\n'));
80
88
  }
81
- console.log(' ' + col(C.dim, model.DISCLAIMER.split(' — ')[0] + ' — treat suggestions as hints.\n'));
89
+
90
+ return aiStatus().anyAvailable; // local ready OR cloud key present
82
91
  }
83
92
 
84
93
  // ── Interactive startup ───────────────────────────────────────────────────────
@@ -569,9 +578,16 @@ server.on('error', (err) => {
569
578
  // ── Startup ───────────────────────────────────────────────────────────────────
570
579
  async function main() {
571
580
  printWelcome();
572
- // "Ultron n0-beta initializing…" warms the local model up front (idempotent)
573
- // so it's ready by the time analysis needs it, and owns the AI startup output.
574
- await initUltron();
581
+ // First-run: install + start the model. Returns true only if AI is available.
582
+ const aiReady = await initUltron();
583
+
584
+ // AI is required to analyze — don't show the menu without it.
585
+ if (!aiReady) {
586
+ console.log(' ' + col(C.red, '⚠ AI is required to run analysis, and it isn\'t set up yet.'));
587
+ console.log(' ' + col(C.dim, `Re-run ${col(C.cyan, 'letfixit')} and paste your ${model.MODEL_NAME} key when prompted,`));
588
+ console.log(' ' + col(C.dim, `or add a cloud key (GROQ/GEMINI/CLAUDE) to .env, then start again.\n`));
589
+ process.exit(0);
590
+ }
575
591
 
576
592
  const setup = await interactiveSetup();
577
593
 
@@ -616,4 +632,47 @@ function shutdown() {
616
632
  process.on('SIGINT', shutdown);
617
633
  process.on('SIGTERM', shutdown);
618
634
 
619
- main();
635
+ // ── CLI dispatch ────────────────────────────────────────────────────────────
636
+ function printHelp() {
637
+ const v = require('../package.json').version;
638
+ console.log(`
639
+ ${col(C.bold + C.mag, 'LetFixIt')} v${v} ${col(C.dim, '— AI-based realtime performance analysis for Flutter, Android & iOS')}
640
+
641
+ ${col(C.bold, 'Usage')}
642
+ ${col(C.cyan, 'letfixit')} Launch the analyzer (first run installs ${model.MODEL_NAME})
643
+ ${col(C.cyan, 'letfixit setup')} Download & set up the model (prompts for the decryption key)
644
+ ${col(C.cyan, 'letfixit setup --force')} Re-download / reinstall the model
645
+ ${col(C.cyan, 'letfixit verify')} Quick test: download + decrypt only chunk 0
646
+ ${col(C.cyan, 'letfixit clean')} Remove the downloaded model (free disk / no footprint)
647
+ ${col(C.cyan, 'letfixit version')} Print the installed version (or --version / -v)
648
+ ${col(C.cyan, 'letfixit --help')} Show this help
649
+
650
+ ${col(C.bold, 'Environment')}
651
+ ${col(C.dim, 'PARITYFIX_MODEL_KEY')} decryption key for non-interactive / CI installs
652
+ ${col(C.dim, 'DASHBOARD_PORT')} dashboard port (default 8081)
653
+ ${col(C.dim, 'LOCAL_AI_PORT')} local model port (default 8080)
654
+ ${col(C.dim, 'ENABLE_LOCAL_AI')} set to "false" to use cloud fallback only
655
+ `);
656
+ }
657
+
658
+ const cmd = process.argv[2];
659
+ if (cmd === '--version' || cmd === '-v') {
660
+ console.log(require('../package.json').version); // bare number (scripts)
661
+ } else if (cmd === 'version') {
662
+ console.log(`letfixit v${require('../package.json').version}`); // friendly line
663
+ } else if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
664
+ printHelp();
665
+ } else if (cmd === 'setup') {
666
+ // Explicit setup: run the installer directly and exit (like `npm run setup`).
667
+ require('../scripts/setup-model').setup()
668
+ .then(() => process.exit(0))
669
+ .catch((e) => { console.error(' ❌ setup failed:', e.message); process.exit(1); });
670
+ } else if (cmd === 'verify') {
671
+ // Quick test: download + decrypt only the first chunk to validate the key/host.
672
+ require('../scripts/verify-chunk').run(process.argv[3]);
673
+ } else if (cmd === 'clean') {
674
+ // Remove the downloaded model + temp files (no footprint).
675
+ require('../scripts/clean-model').clean();
676
+ } else {
677
+ main(); // default: launch the analyzer
678
+ }