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.
- package/knoxis-collab.js +117 -25
- 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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
|
662
|
-
console.error(`
|
|
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.
|
|
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",
|