@sumant.pathak/devjar 1.0.2 → 1.0.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/bin/devjar.js CHANGED
@@ -6,6 +6,7 @@ import { prompt } from '../src/prompt.js';
6
6
  import { update } from '../src/update.js';
7
7
  import { stats } from '../src/stats.js';
8
8
  import { configCommand } from '../src/config.js';
9
+ import { setup } from '../src/setup.js';
9
10
 
10
11
  const program = new Command();
11
12
 
@@ -53,4 +54,10 @@ program
53
54
  .option('--show', 'show current config')
54
55
  .action(configCommand);
55
56
 
57
+ program
58
+ .command('setup')
59
+ .description('Full Jarvis install — Ollama + llama3.2 + Claude Code hooks (one command)')
60
+ .option('-y, --yes', 'skip confirmation prompt')
61
+ .action(setup);
62
+
56
63
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sumant.pathak/devjar",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Scan your project into a Claude-ready knowledge map. Use 60-70% fewer tokens on any AI coding tool.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/setup.js ADDED
@@ -0,0 +1,340 @@
1
+ // devjar setup — full Jarvis system installer
2
+ // Installs Ollama, pulls model, configures Claude Code hooks
3
+ // One command from zero to fully working Jarvis + STAR-C
4
+
5
+ import { execSync, spawn } from 'child_process';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import https from 'https';
10
+ import readline from 'readline';
11
+ import chalk from 'chalk';
12
+ import { saveConfig } from './providers/index.js';
13
+
14
+ const HOME = os.homedir();
15
+ const CLAUDE_DIR = path.join(HOME, '.claude');
16
+ const SCRIPTS_DIR = path.join(CLAUDE_DIR, 'scripts');
17
+ const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
18
+ const DEVJAR_DIR = path.join(HOME, '.devjar');
19
+ const MODEL = 'llama3.2';
20
+
21
+ // ── Helpers ────────────────────────────────────────────────────────────────
22
+
23
+ function step(msg) {
24
+ process.stdout.write(`\n${chalk.bold.blue('→')} ${chalk.white(msg)}\n`);
25
+ }
26
+
27
+ function ok(msg) {
28
+ process.stdout.write(` ${chalk.green('✓')} ${chalk.white(msg)}\n`);
29
+ }
30
+
31
+ function warn(msg) {
32
+ process.stdout.write(` ${chalk.yellow('⚠')} ${chalk.gray(msg)}\n`);
33
+ }
34
+
35
+ function fail(msg) {
36
+ process.stdout.write(` ${chalk.red('✗')} ${msg}\n`);
37
+ }
38
+
39
+ function spinner(msg) {
40
+ const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
41
+ let i = 0;
42
+ const id = setInterval(() => process.stdout.write(`\r ${chalk.cyan(frames[i++ % frames.length])} ${chalk.yellow(msg)}`), 80);
43
+ return () => { clearInterval(id); process.stdout.write('\r' + ' '.repeat(60) + '\r'); };
44
+ }
45
+
46
+ function run(cmd, opts = {}) {
47
+ try { execSync(cmd, { stdio: 'pipe', ...opts }); return true; }
48
+ catch { return false; }
49
+ }
50
+
51
+ function runOut(cmd) {
52
+ try { return execSync(cmd, { stdio: 'pipe' }).toString().trim(); }
53
+ catch { return ''; }
54
+ }
55
+
56
+ function ask(question) {
57
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
58
+ return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
59
+ }
60
+
61
+ function ollamaRunning() {
62
+ try {
63
+ const res = execSync('curl -s http://localhost:11434/api/tags', { stdio: 'pipe' }).toString();
64
+ return res.includes('models');
65
+ } catch { return false; }
66
+ }
67
+
68
+ function ollamaInstalled() {
69
+ return !!runOut('ollama --version') || !!runOut('where ollama') || !!runOut('which ollama');
70
+ }
71
+
72
+ // ── Step 1: Install Ollama ─────────────────────────────────────────────────
73
+
74
+ async function installOllama() {
75
+ step('Checking Ollama...');
76
+
77
+ if (ollamaInstalled()) { ok('Ollama already installed'); return true; }
78
+
79
+ const platform = process.platform;
80
+
81
+ if (platform === 'win32') {
82
+ // Try winget first
83
+ const hasWinget = run('winget --version');
84
+ if (hasWinget) {
85
+ const stop = spinner('Installing Ollama via winget...');
86
+ const ok2 = run('winget install Ollama.Ollama --accept-source-agreements --accept-package-agreements --silent');
87
+ stop();
88
+ if (ok2) { ok('Ollama installed via winget'); return true; }
89
+ }
90
+ // Fallback: direct download
91
+ warn('winget failed — download manually: https://ollama.com/download/OllamaSetup.exe');
92
+ return false;
93
+
94
+ } else if (platform === 'darwin') {
95
+ const hasBrew = run('brew --version');
96
+ if (hasBrew) {
97
+ const stop = spinner('Installing Ollama via brew...');
98
+ const ok2 = run('brew install ollama');
99
+ stop();
100
+ if (ok2) { ok('Ollama installed via brew'); return true; }
101
+ }
102
+ warn('Install manually: https://ollama.com/download');
103
+ return false;
104
+
105
+ } else {
106
+ // Linux
107
+ const stop = spinner('Installing Ollama via install script...');
108
+ const ok2 = run('curl -fsSL https://ollama.com/install.sh | sh');
109
+ stop();
110
+ if (ok2) { ok('Ollama installed'); return true; }
111
+ warn('Install manually: https://ollama.com/download');
112
+ return false;
113
+ }
114
+ }
115
+
116
+ // ── Step 2: Start Ollama ───────────────────────────────────────────────────
117
+
118
+ async function startOllama() {
119
+ step('Starting Ollama service...');
120
+
121
+ if (ollamaRunning()) { ok('Ollama already running'); return; }
122
+
123
+ // Start in background
124
+ const child = spawn('ollama', ['serve'], {
125
+ detached: true, stdio: 'ignore',
126
+ windowsHide: true,
127
+ });
128
+ child.unref();
129
+
130
+ // Wait up to 5s for it to start
131
+ const stop = spinner('Waiting for Ollama to start...');
132
+ for (let i = 0; i < 10; i++) {
133
+ await new Promise(r => setTimeout(r, 500));
134
+ if (ollamaRunning()) { stop(); ok('Ollama service started'); return; }
135
+ }
136
+ stop();
137
+ warn('Ollama not responding — you may need to run `ollama serve` manually');
138
+ }
139
+
140
+ // ── Step 3: Add to startup ─────────────────────────────────────────────────
141
+
142
+ function addToStartup() {
143
+ step('Adding Ollama to startup...');
144
+
145
+ if (process.platform === 'win32') {
146
+ const startupDir = path.join(HOME, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
147
+ const shortcut = path.join(startupDir, 'OllamaServe.lnk');
148
+ if (fs.existsSync(shortcut)) { ok('Already in startup'); return; }
149
+
150
+ const ps = `
151
+ $startup = "${startupDir.replace(/\\/g, '\\\\')}";
152
+ $s = (New-Object -ComObject WScript.Shell).CreateShortcut("$startup\\OllamaServe.lnk");
153
+ $s.TargetPath = "ollama"; $s.Arguments = "serve"; $s.Save()`;
154
+ const ok2 = run(`powershell -Command "${ps.replace(/\n/g, ' ')}"`);
155
+ if (ok2) ok('Added to Windows startup'); else warn('Could not add to startup — run `ollama serve` manually each session');
156
+
157
+ } else if (process.platform === 'darwin') {
158
+ const plist = path.join(HOME, 'Library', 'LaunchAgents', 'com.ollama.serve.plist');
159
+ if (!fs.existsSync(plist)) {
160
+ fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
161
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
162
+ <plist version="1.0"><dict>
163
+ <key>Label</key><string>com.ollama.serve</string>
164
+ <key>ProgramArguments</key><array><string>ollama</string><string>serve</string></array>
165
+ <key>RunAtLoad</key><true/>
166
+ </dict></plist>`);
167
+ run('launchctl load ~/Library/LaunchAgents/com.ollama.serve.plist');
168
+ }
169
+ ok('Added to macOS LaunchAgents');
170
+
171
+ } else {
172
+ // Linux systemd user service
173
+ const serviceDir = path.join(HOME, '.config', 'systemd', 'user');
174
+ fs.mkdirSync(serviceDir, { recursive: true });
175
+ fs.writeFileSync(path.join(serviceDir, 'ollama.service'),
176
+ `[Unit]\nDescription=Ollama\n[Service]\nExecStart=ollama serve\nRestart=always\n[Install]\nWantedBy=default.target\n`);
177
+ run('systemctl --user enable ollama');
178
+ run('systemctl --user start ollama');
179
+ ok('Added to systemd user services');
180
+ }
181
+ }
182
+
183
+ // ── Step 4: Pull model ─────────────────────────────────────────────────────
184
+
185
+ async function pullModel() {
186
+ step(`Pulling ${MODEL} model...`);
187
+
188
+ // Check if already pulled
189
+ const tags = runOut('ollama list');
190
+ if (tags.includes(MODEL)) { ok(`${MODEL} already pulled`); return true; }
191
+
192
+ const stop = spinner(`Downloading ${MODEL} (~2GB, this takes a few minutes)...`);
193
+ const ok2 = run(`ollama pull ${MODEL}`, { timeout: 600000 });
194
+ stop();
195
+ if (ok2) { ok(`${MODEL} ready`); return true; }
196
+ fail(`Failed to pull ${MODEL} — run: ollama pull ${MODEL}`);
197
+ return false;
198
+ }
199
+
200
+ // ── Step 5: Write prompt-normalizer.js ────────────────────────────────────
201
+
202
+ function writeNormalizer() {
203
+ step('Installing prompt normalizer...');
204
+ fs.mkdirSync(SCRIPTS_DIR, { recursive: true });
205
+
206
+ const normalizerPath = path.join(SCRIPTS_DIR, 'prompt-normalizer.js');
207
+ const content = `#!/usr/bin/env node
208
+ import http from 'http';
209
+ const OLLAMA_URL = 'http://localhost:11434';
210
+ const MODEL = '${MODEL}';
211
+ function done(ctx = '') {
212
+ process.stdout.write(JSON.stringify({continue:true,hookSpecificOutput:{hookEventName:'UserPromptSubmit',additionalContext:ctx}}));
213
+ process.exit(0);
214
+ }
215
+ let raw = '';
216
+ process.stdin.setEncoding('utf8');
217
+ process.stdin.on('data', c => { raw += c; });
218
+ process.stdin.on('end', () => {
219
+ const input = (() => { try { return JSON.parse(raw); } catch { return {}; } })();
220
+ const userPrompt = (input?.tool_input?.prompt || input?.prompt || '').slice(0, 800);
221
+ if (!userPrompt) return done();
222
+ const system = \`Normalize this dev prompt into STAR-C JSON. Rules: always interpret charitably, conversational=discuss action. Output ONLY JSON: {"s":"...","t":"...","a":"fix|add|explain|refactor|deploy|debug|discuss","r":"...","c":"...","complexity":"haiku|sonnet|opus","reason":"one line"}\`;
223
+ const body = JSON.stringify({model:MODEL,system,prompt:userPrompt,stream:false,options:{num_predict:150,temperature:0.1}});
224
+ const req = http.request(OLLAMA_URL+'/api/generate',{method:'POST',headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)},timeout:10000},(res)=>{
225
+ let d=''; res.on('data',c=>{d+=c;}); res.on('end',()=>{
226
+ try {
227
+ const t=JSON.parse(d).response?.trim()||'';
228
+ const p=JSON.parse(t.slice(t.indexOf('{'),t.lastIndexOf('}')+1));
229
+ const ctx=[\`[STAR-C]\`,\`S: \${p.s}\`,\`T: \${p.t}\`,\`A: \${p.a}\`,\`R: \${p.r}\`,p.c?\`C: \${p.c}\`:null,\`[COMPLEXITY]: \${p.complexity} — \${p.reason}\`].filter(Boolean).join('\\n');
230
+ done(ctx);
231
+ } catch { done(); }
232
+ });
233
+ });
234
+ req.on('error',()=>done()); req.on('timeout',()=>{req.destroy();done();});
235
+ req.write(body); req.end();
236
+ });
237
+ `;
238
+ fs.writeFileSync(normalizerPath, content, 'utf8');
239
+ ok(`prompt-normalizer.js → ${normalizerPath}`);
240
+ }
241
+
242
+ // ── Step 6: Write session-loader.js ───────────────────────────────────────
243
+
244
+ function writeSessionLoader() {
245
+ const loaderPath = path.join(SCRIPTS_DIR, 'session-loader.js');
246
+ if (fs.existsSync(loaderPath)) { ok('session-loader.js already exists'); return; }
247
+
248
+ const content = `#!/usr/bin/env node
249
+ import fs from 'fs'; import path from 'path'; import os from 'os';
250
+ const MAPS_DIR = path.join(os.homedir(), '.claude', 'scripts');
251
+ const PROJECT_MAPS = { 'care-home': path.join(MAPS_DIR, 'care-home-map.txt') };
252
+ function detect() {
253
+ const cwd = process.cwd().toLowerCase();
254
+ for (const [k,v] of Object.entries(PROJECT_MAPS)) { if (cwd.includes(k)) return v; }
255
+ return null;
256
+ }
257
+ try {
258
+ const f = detect();
259
+ if (!f || !fs.existsSync(f)) { process.exit(0); }
260
+ const map = fs.readFileSync(f, 'utf8');
261
+ process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'=== JARVIS PROJECT KNOWLEDGE ===\\nDO NOT re-read files unless making targeted edits.\\n\\n'+map+'\\n=== END ==='}}));
262
+ } catch { process.exit(0); }
263
+ `;
264
+ fs.writeFileSync(loaderPath, content, 'utf8');
265
+ ok(`session-loader.js → ${loaderPath}`);
266
+ }
267
+
268
+ // ── Step 7: Patch Claude Code settings.json ───────────────────────────────
269
+
270
+ function patchClaudeSettings() {
271
+ step('Configuring Claude Code hooks...');
272
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
273
+
274
+ let settings = {};
275
+ if (fs.existsSync(SETTINGS_FILE)) {
276
+ try { settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); } catch {}
277
+ }
278
+
279
+ settings.hooks = settings.hooks || {};
280
+
281
+ // SessionStart hook
282
+ if (!settings.hooks.SessionStart) {
283
+ settings.hooks.SessionStart = [{ hooks: [{ type: 'command', command: `node ${SCRIPTS_DIR.replace(/\\/g, '/')}/session-loader.js`, statusMessage: 'Loading project knowledge...' }] }];
284
+ ok('SessionStart hook added');
285
+ } else { ok('SessionStart hook already configured'); }
286
+
287
+ // UserPromptSubmit hook
288
+ if (!settings.hooks.UserPromptSubmit) {
289
+ settings.hooks.UserPromptSubmit = [{ hooks: [{ type: 'command', command: `node ${SCRIPTS_DIR.replace(/\\/g, '/')}/prompt-normalizer.js`, statusMessage: 'Structuring prompt...' }] }];
290
+ ok('UserPromptSubmit hook added');
291
+ } else { ok('UserPromptSubmit hook already configured'); }
292
+
293
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
294
+ }
295
+
296
+ // ── Step 8: Save devjar config ─────────────────────────────────────────────
297
+
298
+ function saveDevjarConfig() {
299
+ step('Saving devjar config...');
300
+ fs.mkdirSync(DEVJAR_DIR, { recursive: true });
301
+ saveConfig({ provider: 'ollama', model: MODEL });
302
+ ok(`Provider set: ollama / ${MODEL}`);
303
+ }
304
+
305
+ // ── Main Export ────────────────────────────────────────────────────────────
306
+
307
+ export async function setup(options = {}) {
308
+ console.log(chalk.bold.blue('\n⬡ devjar setup') + chalk.gray(' — Jarvis full install\n'));
309
+ console.log(chalk.gray('─'.repeat(52)));
310
+ console.log(chalk.gray('This will install Ollama, pull llama3.2, and configure'));
311
+ console.log(chalk.gray('Claude Code hooks for STAR-C prompt normalization.\n'));
312
+
313
+ if (!options.yes) {
314
+ const ans = await ask(chalk.bold('Continue?') + chalk.gray(' (Y/n) '));
315
+ if (ans.toLowerCase() === 'n') { console.log(chalk.gray('Cancelled.')); return; }
316
+ }
317
+
318
+ const installed = await installOllama();
319
+ if (installed) {
320
+ await startOllama();
321
+ addToStartup();
322
+ await pullModel();
323
+ } else {
324
+ warn('Skipping Ollama steps — install manually then re-run `devjar setup`');
325
+ }
326
+
327
+ writeNormalizer();
328
+ writeSessionLoader();
329
+ patchClaudeSettings();
330
+ saveDevjarConfig();
331
+
332
+ console.log(chalk.gray('\n─'.repeat(52)));
333
+ console.log(chalk.bold.green('\n✓ Jarvis system fully configured!\n'));
334
+ console.log(chalk.white(' Every Claude Code session now has:'));
335
+ console.log(chalk.gray(' • Project knowledge injected at start (SessionStart)'));
336
+ console.log(chalk.gray(' • Prompts structured via Ollama/llama3.2 (UserPromptSubmit)'));
337
+ console.log(chalk.gray(' • Zero API keys, zero cost, fully local\n'));
338
+ console.log(chalk.gray(' Next: cd into your project and run ') + chalk.cyan('devjar init'));
339
+ console.log(chalk.gray(' Then open Claude Code — Jarvis activates automatically.\n'));
340
+ }