knoxis-collab 1.0.0 → 1.1.1

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/knoxis-collab.js +117 -25
  2. package/package.json +4 -1
package/knoxis-collab.js CHANGED
@@ -18,7 +18,9 @@
18
18
  * KNOXIS_TASK_FILE=/tmp/task.txt node knoxis-collab.js
19
19
  *
20
20
  * Env:
21
- * GROQ_API_KEY - Required (or set in ~/.knoxis/config.json)
21
+ * KNOXIS_BACKEND_URL - Backend URL (or backendUrl in ~/.knoxis/config.json)
22
+ * KNOXIS_USER_ID - User UUID (or userId in ~/.knoxis/config.json)
23
+ * GROQ_API_KEY - Direct Groq key (optional — backend proxy is preferred)
22
24
  * KNOXIS_WORKSPACE - Workspace directory (default: cwd)
23
25
  * KNOXIS_TASK_FILE - Path to file containing initial task
24
26
  * KNOXIS_GROQ_MODEL - Groq model (default: llama-3.3-70b-versatile)
@@ -80,6 +82,53 @@ function loadConfig() {
80
82
 
81
83
  const config = loadConfig();
82
84
  const GROQ_API_KEY = process.env.GROQ_API_KEY || config.groqApiKey || '';
85
+ const BACKEND_URL = process.env.KNOXIS_BACKEND_URL || config.backendUrl || '';
86
+ const USER_ID = process.env.KNOXIS_USER_ID || config.userId || '';
87
+
88
+ // Resolve Claude binary — check every reasonable location
89
+ function resolveClaudeBin() {
90
+ // 1. Local node_modules (when installed as npm package)
91
+ const localBin = path.join(__dirname, 'node_modules', '.bin', 'claude');
92
+ if (fs.existsSync(localBin)) return localBin;
93
+
94
+ // 2. Parent node_modules (when this is a dep of another package)
95
+ const parentBin = path.join(__dirname, '..', '.bin', 'claude');
96
+ if (fs.existsSync(parentBin)) return parentBin;
97
+
98
+ // 3. Global npm prefix bin (same dir where knoxis-collab bin lands)
99
+ try {
100
+ const { status, stdout } = spawnSync('npm', ['prefix', '-g'], { stdio: 'pipe' });
101
+ if (status === 0) {
102
+ const prefix = stdout.toString().trim();
103
+ // npm global bins land in prefix/bin on unix, prefix on windows
104
+ const globalBin = path.join(prefix, 'bin', 'claude');
105
+ if (fs.existsSync(globalBin)) return globalBin;
106
+ const globalBinWin = path.join(prefix, 'claude');
107
+ if (fs.existsSync(globalBinWin)) return globalBinWin;
108
+ }
109
+ } catch (e) {}
110
+
111
+ // 4. Resolve via require (finds the package even if bin symlink is missing)
112
+ try {
113
+ const claudePkg = path.dirname(require.resolve('@anthropic-ai/claude-code/package.json'));
114
+ const pkg = JSON.parse(fs.readFileSync(path.join(claudePkg, 'package.json'), 'utf8'));
115
+ const binEntry = typeof pkg.bin === 'string' ? pkg.bin : (pkg.bin && pkg.bin.claude);
116
+ if (binEntry) {
117
+ const resolved = path.join(claudePkg, binEntry);
118
+ if (fs.existsSync(resolved)) return resolved;
119
+ }
120
+ } catch (e) {}
121
+
122
+ // 5. Fall back to global PATH
123
+ try {
124
+ const { status, stdout } = spawnSync('which', ['claude'], { stdio: 'pipe' });
125
+ if (status === 0 && stdout.toString().trim()) return 'claude';
126
+ } catch (e) {}
127
+
128
+ return null;
129
+ }
130
+
131
+ const CLAUDE_BIN = resolveClaudeBin();
83
132
 
84
133
  // ═══════════════════════════════════════════════════════════════
85
134
  // STATE
@@ -135,7 +184,8 @@ function printHeader() {
135
184
  console.log('');
136
185
  console.log(` ${C.dim}Session:${C.reset} ${SESSION_ID.slice(0, 8)}`);
137
186
  console.log(` ${C.dim}Dir:${C.reset} ${WORKSPACE}`);
138
- console.log(` ${C.dim}Knoxis:${C.reset} Groq (${GROQ_MODEL})`);
187
+ const groqMode = (BACKEND_URL && USER_ID) ? `via backend` : 'direct API';
188
+ console.log(` ${C.dim}Knoxis:${C.reset} Groq (${GROQ_MODEL}, ${groqMode})`);
139
189
  console.log(` ${C.dim}Claude:${C.reset} Claude Code (session-persistent)`);
140
190
  console.log(` ${C.dim}Log:${C.reset} ${path.basename(logFile)}`);
141
191
  console.log('');
@@ -293,12 +343,19 @@ PERSONALITY: Direct, technical, efficient. You're a developer talking to a devel
293
343
 
294
344
  function callGroq(messages, projectContext) {
295
345
  return new Promise((resolve, reject) => {
296
- if (!GROQ_API_KEY) {
297
- reject(new Error('GROQ_API_KEY not set'));
346
+ const useBackend = BACKEND_URL && USER_ID;
347
+ const useDirectGroq = GROQ_API_KEY && !useBackend;
348
+
349
+ if (!useBackend && !useDirectGroq) {
350
+ reject(new Error(
351
+ 'No Groq access configured.\n' +
352
+ ' Option 1 (recommended): Set backendUrl + userId in ~/.knoxis/config.json\n' +
353
+ ' Option 2 (direct): Set GROQ_API_KEY env var or groqApiKey in config.json'
354
+ ));
298
355
  return;
299
356
  }
300
357
 
301
- const payload = JSON.stringify({
358
+ const groqPayload = {
302
359
  model: GROQ_MODEL,
303
360
  messages: [
304
361
  { role: 'system', content: buildSystemPrompt(projectContext) },
@@ -307,20 +364,51 @@ function callGroq(messages, projectContext) {
307
364
  temperature: 0.3,
308
365
  max_tokens: 2048,
309
366
  response_format: { type: 'json_object' }
310
- });
311
-
312
- const options = {
313
- hostname: 'api.groq.com',
314
- path: '/openai/v1/chat/completions',
315
- method: 'POST',
316
- headers: {
317
- 'Content-Type': 'application/json',
318
- 'Authorization': `Bearer ${GROQ_API_KEY}`,
319
- 'Content-Length': Buffer.byteLength(payload)
320
- }
321
367
  };
322
368
 
323
- const req = https.request(options, (res) => {
369
+ // If backend is available, route through the proxy (no local key needed)
370
+ if (useBackend) {
371
+ groqPayload.userId = USER_ID;
372
+ }
373
+
374
+ const payload = JSON.stringify(groqPayload);
375
+
376
+ let options;
377
+ if (useBackend) {
378
+ // Route through backend proxy
379
+ const backendClean = BACKEND_URL
380
+ .replace(/^wss:\/\//, 'https://')
381
+ .replace(/^ws:\/\//, 'http://')
382
+ .replace(/\/+$/, '');
383
+ const url = new URL(backendClean + '/api/knoxis/collab/chat');
384
+ options = {
385
+ hostname: url.hostname,
386
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
387
+ path: url.pathname,
388
+ method: 'POST',
389
+ headers: {
390
+ 'Content-Type': 'application/json',
391
+ 'Content-Length': Buffer.byteLength(payload)
392
+ }
393
+ };
394
+ } else {
395
+ // Direct Groq API call (legacy / developer override)
396
+ options = {
397
+ hostname: 'api.groq.com',
398
+ path: '/openai/v1/chat/completions',
399
+ method: 'POST',
400
+ headers: {
401
+ 'Content-Type': 'application/json',
402
+ 'Authorization': `Bearer ${GROQ_API_KEY}`,
403
+ 'Content-Length': Buffer.byteLength(payload)
404
+ }
405
+ };
406
+ }
407
+
408
+ const transport = options.hostname === 'localhost' || options.port === 80
409
+ ? require('http') : https;
410
+
411
+ const req = transport.request(options, (res) => {
324
412
  let data = '';
325
413
  res.on('data', chunk => data += chunk);
326
414
  res.on('end', () => {
@@ -373,8 +461,7 @@ function callGroq(messages, projectContext) {
373
461
  // ═══════════════════════════════════════════════════════════════
374
462
 
375
463
  function checkClaudeInstalled() {
376
- const result = spawnSync('which', ['claude'], { stdio: 'pipe' });
377
- return result.status === 0;
464
+ return CLAUDE_BIN !== null;
378
465
  }
379
466
 
380
467
  function dispatchToClaude(prompt) {
@@ -392,7 +479,7 @@ function dispatchToClaude(prompt) {
392
479
 
393
480
  log(`DISPATCH #${claudeDispatches}:\n${prompt}`);
394
481
 
395
- const proc = spawn('claude', args, {
482
+ const proc = spawn(CLAUDE_BIN, args, {
396
483
  cwd: WORKSPACE,
397
484
  env: { ...process.env },
398
485
  stdio: ['pipe', 'pipe', 'pipe']
@@ -651,15 +738,20 @@ async function handleUserInput(input, rl) {
651
738
 
652
739
  async function main() {
653
740
  // Preflight checks
654
- if (!GROQ_API_KEY) {
655
- console.error(`\n ${C.red}Error: GROQ_API_KEY not found.${C.reset}`);
656
- console.error(` Set it via environment variable or in ~/.knoxis/config.json\n`);
741
+ const hasBackend = BACKEND_URL && USER_ID;
742
+ const hasDirectKey = !!GROQ_API_KEY;
743
+
744
+ if (!hasBackend && !hasDirectKey) {
745
+ console.error(`\n ${C.red}Error: No Groq access configured.${C.reset}`);
746
+ console.error(` Set ${C.bold}backendUrl${C.reset} + ${C.bold}userId${C.reset} in ~/.knoxis/config.json (recommended)`);
747
+ console.error(` Or set GROQ_API_KEY for direct access.\n`);
657
748
  process.exit(1);
658
749
  }
659
750
 
660
751
  if (!checkClaudeInstalled()) {
661
- console.error(`\n ${C.red}Error: 'claude' CLI not found in PATH.${C.reset}`);
662
- console.error(` Install Claude Code: npm install -g @anthropic-ai/claude-code\n`);
752
+ console.error(`\n ${C.red}Error: 'claude' CLI not found.${C.reset}`);
753
+ console.error(` It should be bundled with this package. Try: npm install`);
754
+ console.error(` Or install globally: npm install -g @anthropic-ai/claude-code\n`);
663
755
  process.exit(1);
664
756
  }
665
757
 
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "knoxis-collab",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Three-way collaborative programming — User + Knoxis + Claude Code in a persistent session",
5
5
  "main": "knoxis-collab.js",
6
6
  "bin": {
7
7
  "knoxis-collab": "./knoxis-collab.js"
8
8
  },
9
+ "dependencies": {
10
+ "@anthropic-ai/claude-code": "^1.0.0"
11
+ },
9
12
  "keywords": [
10
13
  "knoxis",
11
14
  "pair-programming",