@sumant.pathak/devjar 1.0.3 → 1.0.5

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sumant.pathak/devjar",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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/config.js CHANGED
@@ -1,8 +1,20 @@
1
1
  // devjar config — provider setup, wizard, config display
2
2
 
3
3
  import readline from 'readline';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
4
7
  import chalk from 'chalk';
5
8
  import { loadConfig, saveConfig } from './providers/index.js';
9
+ import { buildNormalizerContent } from './setup.js';
10
+
11
+ const SCRIPTS_DIR = path.join(os.homedir(), '.claude', 'scripts');
12
+
13
+ function regenerateHook() {
14
+ const hookPath = path.join(SCRIPTS_DIR, 'prompt-normalizer.js');
15
+ if (!fs.existsSync(SCRIPTS_DIR)) return;
16
+ fs.writeFileSync(hookPath, buildNormalizerContent(), 'utf8');
17
+ }
6
18
 
7
19
  const PROVIDERS = {
8
20
  1: { name: 'ollama', label: 'Ollama (free, local, recommended)', defaultModel: 'llama3.2', needsKey: false },
@@ -46,6 +58,7 @@ export async function runWizard() {
46
58
 
47
59
  const cfg = { provider: provider.name, model: provider.defaultModel, ...(apiKey && { apiKey }) };
48
60
  saveConfig(cfg);
61
+ regenerateHook();
49
62
 
50
63
  console.log();
51
64
  console.log(chalk.green('✓ Configured: ') + chalk.white(`${provider.name} / ${provider.defaultModel}`));
@@ -93,6 +106,7 @@ export async function configCommand(options) {
93
106
  ...(options.url && { ollamaUrl: options.url }),
94
107
  };
95
108
  saveConfig(cfg);
109
+ regenerateHook();
96
110
 
97
111
  console.log(chalk.green('✓ Saved: ') + chalk.white(`${cfg.provider} / ${cfg.model}`));
98
112
  return;
package/src/init.js CHANGED
@@ -285,7 +285,7 @@ function generateClaudeMd(opts) {
285
285
  - **State management**: ${patterns.stateManagement}
286
286
  ` : '';
287
287
 
288
- return `# ${projectName} — Claude Code Orientation
288
+ return `# ⬡ devjar — ${projectName}
289
289
 
290
290
  ## Stack
291
291
  ${stackLine}
package/src/setup.js CHANGED
@@ -199,43 +199,182 @@ async function pullModel() {
199
199
 
200
200
  // ── Step 5: Write prompt-normalizer.js ────────────────────────────────────
201
201
 
202
- function writeNormalizer() {
203
- step('Installing prompt normalizer...');
204
- fs.mkdirSync(SCRIPTS_DIR, { recursive: true });
202
+ export function buildNormalizerContent() {
203
+ return `#!/usr/bin/env node
204
+ /**
205
+ * STAR-C prompt normalizer — UserPromptSubmit hook
206
+ * Multi-provider: reads ~/.devjar/config.json at runtime
207
+ * Supports: ollama | anthropic | gemini | openai
208
+ * NEVER blocks. Always outputs continue: true.
209
+ */
205
210
 
206
- const normalizerPath = path.join(SCRIPTS_DIR, 'prompt-normalizer.js');
207
- const content = `#!/usr/bin/env node
208
211
  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}}));
212
+ import https from 'https';
213
+ import fs from 'fs';
214
+ import path from 'path';
215
+ import os from 'os';
216
+
217
+ const DEVJAR_DIR = path.join(os.homedir(), '.devjar');
218
+ const CONFIG_FILE = path.join(DEVJAR_DIR, 'config.json');
219
+ const HISTORY_FILE = path.join(DEVJAR_DIR, 'history.json');
220
+
221
+ function loadConfig() {
222
+ try {
223
+ if (fs.existsSync(CONFIG_FILE)) return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
224
+ } catch {}
225
+ return { provider: 'ollama', model: 'llama3.2' };
226
+ }
227
+
228
+ function logHistory(project, rawChars, structuredChars, provider) {
229
+ try {
230
+ const entry = {
231
+ timestamp: new Date().toISOString(),
232
+ project,
233
+ rawChars,
234
+ structuredChars,
235
+ tokensSaved: Math.max(0, Math.floor((rawChars - 200) * 0.25)),
236
+ provider,
237
+ };
238
+ let history = [];
239
+ if (fs.existsSync(HISTORY_FILE)) {
240
+ try { history = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8')); } catch {}
241
+ }
242
+ history.push(entry);
243
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2), 'utf8');
244
+ } catch {}
245
+ }
246
+
247
+ function done(additionalContext = '') {
248
+ process.stdout.write(JSON.stringify({
249
+ continue: true,
250
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext },
251
+ }));
213
252
  process.exit(0);
214
253
  }
254
+
255
+ const SYSTEM_PROMPT = \`Normalize this dev prompt into STAR-C JSON.
256
+ Rules: always interpret charitably, conversational=discuss action.
257
+ Output ONLY valid JSON:
258
+ {"s":"...","t":"...","a":"fix|add|explain|refactor|deploy|debug|discuss","r":"...","c":"...","complexity":"haiku|sonnet|opus","reason":"one line"}\`;
259
+
260
+ function parseStarC(text) {
261
+ const jsonStr = text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1);
262
+ const p = JSON.parse(jsonStr);
263
+ return [
264
+ \`[STAR-C]\`,
265
+ \`S: \${p.s}\`, \`T: \${p.t}\`, \`A: \${p.a}\`, \`R: \${p.r}\`,
266
+ p.c ? \`C: \${p.c}\` : null,
267
+ \`[COMPLEXITY]: \${p.complexity} — \${p.reason}\`,
268
+ ].filter(Boolean).join('\\n');
269
+ }
270
+
271
+ function callOllama(userPrompt, config) {
272
+ return new Promise((resolve) => {
273
+ const baseUrl = config.ollamaUrl || 'http://localhost:11434';
274
+ const model = config.model || 'llama3.2';
275
+ const body = JSON.stringify({ model, system: SYSTEM_PROMPT, prompt: userPrompt, stream: false, options: { num_predict: 200, temperature: 0.1 } });
276
+ const req = http.request(\`\${baseUrl}/api/generate\`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 10000 }, (res) => {
277
+ let data = ''; res.on('data', c => { data += c; }); res.on('end', () => { try { resolve(parseStarC(JSON.parse(data).response?.trim() || '')); } catch { resolve(''); } });
278
+ });
279
+ req.on('error', () => resolve('')); req.on('timeout', () => { req.destroy(); resolve(''); });
280
+ req.write(body); req.end();
281
+ });
282
+ }
283
+
284
+ function httpsPost(options, body) {
285
+ return new Promise((resolve) => {
286
+ const req = https.request(options, (res) => {
287
+ let data = ''; res.on('data', c => { data += c; }); res.on('end', () => resolve(data));
288
+ });
289
+ req.setTimeout(15000, () => { req.destroy(); resolve(''); }); req.on('error', () => resolve(''));
290
+ req.write(body); req.end();
291
+ });
292
+ }
293
+
294
+ async function callAnthropic(userPrompt, config) {
295
+ const apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY;
296
+ if (!apiKey) return '';
297
+ const model = config.model || 'claude-haiku-4-5-20251001';
298
+ const body = JSON.stringify({ model, max_tokens: 300, system: SYSTEM_PROMPT, messages: [{ role: 'user', content: userPrompt }] });
299
+ const data = await httpsPost({ hostname: 'api.anthropic.com', path: '/v1/messages', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' } }, body);
300
+ try {
301
+ const parsed = JSON.parse(data);
302
+ if (parsed.error?.type === 'rate_limit_error' || parsed.error?.type === 'overloaded_error') return null;
303
+ return parseStarC(parsed.content[0].text);
304
+ } catch { return ''; }
305
+ }
306
+
307
+ async function callGemini(userPrompt, config) {
308
+ const apiKey = config.apiKey || process.env.GEMINI_API_KEY;
309
+ if (!apiKey) return '';
310
+ const model = config.model || 'gemini-2.0-flash';
311
+ const body = JSON.stringify({ contents: [{ role: 'user', parts: [{ text: \`\${SYSTEM_PROMPT}\\n\\n\${userPrompt}\` }] }], generationConfig: { maxOutputTokens: 300, temperature: 0.1 } });
312
+ const data = await httpsPost({ hostname: 'generativelanguage.googleapis.com', path: \`/v1beta/models/\${model}:generateContent?key=\${apiKey}\`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, body);
313
+ try {
314
+ const parsed = JSON.parse(data);
315
+ if (parsed.error) {
316
+ const code = parsed.error.code || parsed.error.status;
317
+ if (code === 429 || code === 'RESOURCE_EXHAUSTED') return null;
318
+ throw new Error(parsed.error.message);
319
+ }
320
+ return parseStarC(parsed.candidates[0].content.parts[0].text);
321
+ } catch (e) {
322
+ if (e.message === 'quota') return null;
323
+ return '';
324
+ }
325
+ }
326
+
327
+ async function callOpenAI(userPrompt, config) {
328
+ const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
329
+ if (!apiKey) return '';
330
+ const model = config.model || 'gpt-4o-mini';
331
+ const body = JSON.stringify({ model, max_tokens: 300, temperature: 0.1, messages: [{ role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: userPrompt }] });
332
+ const data = await httpsPost({ hostname: 'api.openai.com', path: '/v1/chat/completions', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Authorization': \`Bearer \${apiKey}\` } }, body);
333
+ try {
334
+ const parsed = JSON.parse(data);
335
+ if (parsed.error?.code === 'rate_limit_exceeded' || parsed.error?.code === 'insufficient_quota') return null;
336
+ return parseStarC(parsed.choices[0].message.content);
337
+ } catch { return ''; }
338
+ }
339
+
215
340
  let raw = '';
216
341
  process.stdin.setEncoding('utf8');
217
342
  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();
343
+ process.stdin.on('end', async () => {
344
+ try {
345
+ const input = (() => { try { return JSON.parse(raw); } catch { return {}; } })();
346
+ const userPrompt = (input?.tool_input?.prompt || input?.prompt || '').slice(0, 800);
347
+ if (!userPrompt) return done();
348
+
349
+ const config = loadConfig();
350
+ const provider = config.provider || 'ollama';
351
+
352
+ let ctx = '';
353
+ let usedProvider = provider;
354
+
355
+ if (provider === 'anthropic') ctx = await callAnthropic(userPrompt, config);
356
+ else if (provider === 'gemini') ctx = await callGemini(userPrompt, config);
357
+ else if (provider === 'openai') ctx = await callOpenAI(userPrompt, config);
358
+ else ctx = await callOllama(userPrompt, config);
359
+
360
+ // null = quota/rate-limit → fall back to local Ollama
361
+ if (ctx === null) {
362
+ usedProvider = 'ollama(fallback)';
363
+ ctx = await callOllama(userPrompt, { model: 'llama3.2' });
364
+ }
365
+
366
+ if (ctx) logHistory(process.cwd(), userPrompt.length, ctx.length, usedProvider);
367
+ done(ctx || '');
368
+ } catch { done(); }
236
369
  });
237
370
  `;
238
- fs.writeFileSync(normalizerPath, content, 'utf8');
371
+ }
372
+
373
+ function writeNormalizer() {
374
+ step('Installing prompt normalizer...');
375
+ fs.mkdirSync(SCRIPTS_DIR, { recursive: true });
376
+ const normalizerPath = path.join(SCRIPTS_DIR, 'prompt-normalizer.js');
377
+ fs.writeFileSync(normalizerPath, buildNormalizerContent(), 'utf8');
239
378
  ok(`prompt-normalizer.js → ${normalizerPath}`);
240
379
  }
241
380