@sumant.pathak/devjar 1.0.4 → 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 +1 -1
- package/src/config.js +14 -0
- package/src/setup.js +167 -28
package/package.json
CHANGED
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/setup.js
CHANGED
|
@@ -199,43 +199,182 @@ async function pullModel() {
|
|
|
199
199
|
|
|
200
200
|
// ── Step 5: Write prompt-normalizer.js ────────────────────────────────────
|
|
201
201
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|