knoxis-collab 1.4.1 → 1.5.0
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 +175 -142
- package/package.json +2 -2
package/knoxis-collab.js
CHANGED
|
@@ -112,63 +112,23 @@ const GROQ_API_KEY = process.env.GROQ_API_KEY || config.groqApiKey || '';
|
|
|
112
112
|
const BACKEND_URL = process.env.KNOXIS_BACKEND_URL || config.backendUrl || '';
|
|
113
113
|
const USER_ID = process.env.KNOXIS_USER_ID || config.userId || '';
|
|
114
114
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
function existsAny(basePath) {
|
|
119
|
-
// On Windows, .bin has claude.cmd / claude.ps1 rather than a bare 'claude'
|
|
120
|
-
const candidates = IS_WIN
|
|
121
|
-
? [basePath + '.cmd', basePath + '.ps1', basePath]
|
|
122
|
-
: [basePath];
|
|
123
|
-
for (const c of candidates) {
|
|
124
|
-
if (fs.existsSync(c)) return c;
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function resolveClaudeBin() {
|
|
130
|
-
// 1. Local node_modules (when installed as npm package)
|
|
131
|
-
const localBin = existsAny(path.join(__dirname, 'node_modules', '.bin', 'claude'));
|
|
132
|
-
if (localBin) return localBin;
|
|
133
|
-
|
|
134
|
-
// 2. Parent node_modules (when this is a dep of another package)
|
|
135
|
-
const parentBin = existsAny(path.join(__dirname, '..', '.bin', 'claude'));
|
|
136
|
-
if (parentBin) return parentBin;
|
|
115
|
+
// Claude Agent SDK — loaded via dynamic import (ESM package in CommonJS context)
|
|
116
|
+
// The SDK provides programmatic access to Claude Code's capabilities without CLI spawning.
|
|
117
|
+
let claudeAgentSdk = null;
|
|
137
118
|
|
|
138
|
-
|
|
119
|
+
async function loadClaudeAgentSdk() {
|
|
120
|
+
if (claudeAgentSdk) return claudeAgentSdk;
|
|
139
121
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 4. Resolve via require (finds the package even if bin symlink is missing)
|
|
150
|
-
try {
|
|
151
|
-
const claudePkg = path.dirname(require.resolve('@anthropic-ai/claude-code/package.json'));
|
|
152
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(claudePkg, 'package.json'), 'utf8'));
|
|
153
|
-
const binEntry = typeof pkg.bin === 'string' ? pkg.bin : (pkg.bin && pkg.bin.claude);
|
|
154
|
-
if (binEntry) {
|
|
155
|
-
const resolved = path.join(claudePkg, binEntry);
|
|
156
|
-
if (fs.existsSync(resolved)) return resolved;
|
|
157
|
-
}
|
|
158
|
-
} catch (e) {}
|
|
159
|
-
|
|
160
|
-
// 5. Fall back to global PATH
|
|
161
|
-
try {
|
|
162
|
-
const cmd = IS_WIN ? 'where' : 'which';
|
|
163
|
-
const { status, stdout } = spawnSync(cmd, ['claude'], { stdio: 'pipe', shell: IS_WIN });
|
|
164
|
-
if (status === 0 && stdout.toString().trim()) return stdout.toString().trim().split('\n')[0];
|
|
165
|
-
} catch (e) {}
|
|
166
|
-
|
|
167
|
-
return null;
|
|
122
|
+
claudeAgentSdk = await import('@anthropic-ai/claude-agent-sdk');
|
|
123
|
+
return claudeAgentSdk;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Failed to load @anthropic-ai/claude-agent-sdk: ${e.message}\n` +
|
|
127
|
+
` Install it with: npm install @anthropic-ai/claude-agent-sdk`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
168
130
|
}
|
|
169
131
|
|
|
170
|
-
const CLAUDE_BIN = resolveClaudeBin();
|
|
171
|
-
|
|
172
132
|
// ═══════════════════════════════════════════════════════════════
|
|
173
133
|
// STATE
|
|
174
134
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -177,7 +137,6 @@ let claudeSessionId = null;
|
|
|
177
137
|
let claudeDispatches = 0;
|
|
178
138
|
let verbose = cliArgs.verbose;
|
|
179
139
|
let isClaudeRunning = false;
|
|
180
|
-
let activeClaudeProc = null;
|
|
181
140
|
const conversationHistory = []; // Groq message history
|
|
182
141
|
const dispatchSummaries = []; // Short summaries of what Claude did each dispatch
|
|
183
142
|
const MAX_DISPATCH_SUMMARIES = 20; // Limit dispatch summaries for memory
|
|
@@ -521,7 +480,7 @@ function printHeader() {
|
|
|
521
480
|
console.log(` ${C.dim}Dir:${C.reset} ${WORKSPACE}`);
|
|
522
481
|
const groqMode = (BACKEND_URL && USER_ID) ? `via backend` : 'direct API';
|
|
523
482
|
console.log(` ${C.dim}Knoxis:${C.reset} Groq (${GROQ_MODEL}, ${groqMode})`);
|
|
524
|
-
console.log(` ${C.dim}Claude:${C.reset} Claude
|
|
483
|
+
console.log(` ${C.dim}Claude:${C.reset} Claude Agent SDK (session-persistent)`);
|
|
525
484
|
console.log(` ${C.dim}Log:${C.reset} ${path.basename(logFile)}`);
|
|
526
485
|
console.log('');
|
|
527
486
|
console.log(` ${C.dim}Talk to Knoxis naturally. He dispatches to Claude when needed.${C.reset}`);
|
|
@@ -1156,103 +1115,180 @@ function callGroq(messages, projectContext) {
|
|
|
1156
1115
|
}
|
|
1157
1116
|
|
|
1158
1117
|
// ═══════════════════════════════════════════════════════════════
|
|
1159
|
-
// CLAUDE CODE DISPATCH
|
|
1118
|
+
// CLAUDE CODE DISPATCH (via Agent SDK)
|
|
1160
1119
|
// ═══════════════════════════════════════════════════════════════
|
|
1161
1120
|
|
|
1162
|
-
function checkClaudeInstalled() {
|
|
1163
|
-
|
|
1121
|
+
async function checkClaudeInstalled() {
|
|
1122
|
+
try {
|
|
1123
|
+
await loadClaudeAgentSdk();
|
|
1124
|
+
return true;
|
|
1125
|
+
} catch (e) {
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1164
1128
|
}
|
|
1165
1129
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1130
|
+
// AbortController for the current dispatch — allows Ctrl+C cancellation
|
|
1131
|
+
let activeAbortController = null;
|
|
1132
|
+
|
|
1133
|
+
async function dispatchToClaude(prompt) {
|
|
1134
|
+
const sdk = await loadClaudeAgentSdk();
|
|
1135
|
+
claudeDispatches++;
|
|
1136
|
+
isClaudeRunning = true;
|
|
1137
|
+
|
|
1138
|
+
const startTime = Date.now();
|
|
1139
|
+
let outputLines = 0;
|
|
1140
|
+
let output = '';
|
|
1141
|
+
let resultData = null;
|
|
1142
|
+
|
|
1143
|
+
log(`DISPATCH #${claudeDispatches}:\n${prompt}`);
|
|
1144
|
+
|
|
1145
|
+
// Build SDK options
|
|
1146
|
+
const queryOptions = {
|
|
1147
|
+
cwd: WORKSPACE,
|
|
1148
|
+
permissionMode: 'bypassPermissions',
|
|
1149
|
+
persistSession: true,
|
|
1150
|
+
env: {
|
|
1151
|
+
...process.env,
|
|
1152
|
+
CLAUDE_AGENT_SDK_CLIENT_APP: `knoxis-collab/1.5.0`
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1170
1155
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1156
|
+
// Session management: resume existing or start new
|
|
1157
|
+
if (claudeSessionId) {
|
|
1158
|
+
queryOptions.resume = claudeSessionId;
|
|
1159
|
+
} else {
|
|
1160
|
+
queryOptions.sessionId = SESSION_ID;
|
|
1161
|
+
}
|
|
1178
1162
|
|
|
1179
|
-
|
|
1163
|
+
// Create abort controller for cancellation support
|
|
1164
|
+
activeAbortController = new AbortController();
|
|
1165
|
+
queryOptions.abortController = activeAbortController;
|
|
1180
1166
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1167
|
+
// Start spinner
|
|
1168
|
+
if (!verbose) {
|
|
1169
|
+
spinner.start('Claude is processing');
|
|
1170
|
+
}
|
|
1184
1171
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
env: { ...process.env },
|
|
1188
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1189
|
-
shell: IS_WIN
|
|
1190
|
-
});
|
|
1172
|
+
try {
|
|
1173
|
+
const conversation = sdk.query({ prompt, options: queryOptions });
|
|
1191
1174
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1175
|
+
for await (const message of conversation) {
|
|
1176
|
+
// Capture session ID from first message
|
|
1177
|
+
if (!claudeSessionId && message.session_id) {
|
|
1178
|
+
claudeSessionId = message.session_id;
|
|
1179
|
+
}
|
|
1197
1180
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1181
|
+
switch (message.type) {
|
|
1182
|
+
case 'assistant': {
|
|
1183
|
+
// Extract text content from the assistant message
|
|
1184
|
+
const textBlocks = (message.message.content || [])
|
|
1185
|
+
.filter(block => block.type === 'text')
|
|
1186
|
+
.map(block => block.text);
|
|
1187
|
+
|
|
1188
|
+
if (textBlocks.length > 0) {
|
|
1189
|
+
const text = textBlocks.join('\n');
|
|
1190
|
+
output += text + '\n';
|
|
1191
|
+
const newLines = text.split('\n').filter(l => l.trim()).length;
|
|
1192
|
+
outputLines += newLines;
|
|
1193
|
+
printClaudeLine(text);
|
|
1194
|
+
|
|
1195
|
+
if (!verbose) {
|
|
1196
|
+
spinner.update(`Claude is processing (${outputLines} lines)`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1202
1201
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1202
|
+
case 'tool_use_summary': {
|
|
1203
|
+
// Show what tool Claude just used (e.g., "Read src/main.js")
|
|
1204
|
+
const summary = message.summary || '';
|
|
1205
|
+
if (summary) {
|
|
1206
|
+
printClaudeLine(`[Tool] ${summary}`);
|
|
1207
|
+
if (!verbose) {
|
|
1208
|
+
spinner.update(`Claude: ${summary.slice(0, 50)}`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
1213
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1214
|
+
case 'tool_progress': {
|
|
1215
|
+
// Update spinner with tool progress (e.g., long-running Bash)
|
|
1216
|
+
if (!verbose) {
|
|
1217
|
+
const elapsed = Math.round(message.elapsed_time_seconds || 0);
|
|
1218
|
+
spinner.update(`Claude: ${message.tool_name} (${elapsed}s)`);
|
|
1219
|
+
}
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
1220
1222
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1223
|
+
case 'result': {
|
|
1224
|
+
resultData = message;
|
|
1225
|
+
if (message.subtype === 'success') {
|
|
1226
|
+
// The result field contains Claude's final text summary
|
|
1227
|
+
if (message.result) {
|
|
1228
|
+
output += message.result + '\n';
|
|
1229
|
+
outputLines += message.result.split('\n').filter(l => l.trim()).length;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1224
1234
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1235
|
+
case 'system': {
|
|
1236
|
+
// Log system messages for debugging
|
|
1237
|
+
if (message.subtype === 'api_retry') {
|
|
1238
|
+
log(`SDK API RETRY: attempt ${message.attempt}/${message.max_retries}`);
|
|
1239
|
+
if (!verbose) {
|
|
1240
|
+
spinner.update(`Claude: retrying API call (${message.attempt}/${message.max_retries})`);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
break;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Ignore other message types (stream_event, user, etc.)
|
|
1247
|
+
default:
|
|
1248
|
+
break;
|
|
1228
1249
|
}
|
|
1229
|
-
|
|
1250
|
+
}
|
|
1230
1251
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1252
|
+
isClaudeRunning = false;
|
|
1253
|
+
activeAbortController = null;
|
|
1233
1254
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
});
|
|
1255
|
+
// Stop spinner
|
|
1256
|
+
if (!verbose) {
|
|
1257
|
+
spinner.stop();
|
|
1258
|
+
}
|
|
1259
|
+
clearClaudeStatus();
|
|
1240
1260
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
activeClaudeProc = null;
|
|
1261
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
1262
|
+
const exitCode = (resultData && resultData.subtype === 'success') ? 0 : 1;
|
|
1244
1263
|
|
|
1245
|
-
|
|
1246
|
-
if (!verbose) {
|
|
1247
|
-
spinner.stop();
|
|
1248
|
-
}
|
|
1249
|
-
clearClaudeStatus();
|
|
1250
|
-
reject(err);
|
|
1251
|
-
});
|
|
1264
|
+
log(`CLAUDE DONE (exit ${exitCode}, ${elapsed}s, ${outputLines} lines):\n${output.slice(0, 2000)}`);
|
|
1252
1265
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1266
|
+
if (resultData && resultData.subtype !== 'success') {
|
|
1267
|
+
const errMsg = resultData.error || 'Claude returned an error result';
|
|
1268
|
+
throw new Error(errMsg);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
return { output: output.trim(), exitCode, elapsed, lines: outputLines };
|
|
1272
|
+
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
isClaudeRunning = false;
|
|
1275
|
+
activeAbortController = null;
|
|
1276
|
+
|
|
1277
|
+
// Stop spinner on error
|
|
1278
|
+
if (!verbose) {
|
|
1279
|
+
spinner.stop();
|
|
1280
|
+
}
|
|
1281
|
+
clearClaudeStatus();
|
|
1282
|
+
|
|
1283
|
+
// Differentiate abort from real errors
|
|
1284
|
+
if (err.name === 'AbortError' || activeAbortController?.signal?.aborted) {
|
|
1285
|
+
log(`CLAUDE ABORTED after ${Math.round((Date.now() - startTime) / 1000)}s`);
|
|
1286
|
+
throw new Error('Claude dispatch was cancelled');
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
log(`CLAUDE ERROR: ${err.message}`);
|
|
1290
|
+
throw err;
|
|
1291
|
+
}
|
|
1256
1292
|
}
|
|
1257
1293
|
|
|
1258
1294
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1825,10 +1861,10 @@ async function main() {
|
|
|
1825
1861
|
process.exit(1);
|
|
1826
1862
|
}
|
|
1827
1863
|
|
|
1828
|
-
if (!checkClaudeInstalled()) {
|
|
1829
|
-
console.error(`\n ${C.red}Error:
|
|
1830
|
-
console.error(`
|
|
1831
|
-
console.error(` Or
|
|
1864
|
+
if (!(await checkClaudeInstalled())) {
|
|
1865
|
+
console.error(`\n ${C.red}Error: @anthropic-ai/claude-agent-sdk not found.${C.reset}`);
|
|
1866
|
+
console.error(` Install it with: npm install @anthropic-ai/claude-agent-sdk`);
|
|
1867
|
+
console.error(` Or reinstall this package: npm install knoxis-collab\n`);
|
|
1832
1868
|
process.exit(1);
|
|
1833
1869
|
}
|
|
1834
1870
|
|
|
@@ -1865,17 +1901,14 @@ async function main() {
|
|
|
1865
1901
|
prompt: ` ${C.green}You:${C.reset} `,
|
|
1866
1902
|
});
|
|
1867
1903
|
|
|
1868
|
-
// Handle Ctrl+C —
|
|
1904
|
+
// Handle Ctrl+C — abort Claude SDK query if running, otherwise exit
|
|
1869
1905
|
process.on('SIGINT', () => {
|
|
1870
1906
|
// Stop any active spinner
|
|
1871
1907
|
spinner.stop();
|
|
1872
1908
|
|
|
1873
|
-
if (isClaudeRunning &&
|
|
1909
|
+
if (isClaudeRunning && activeAbortController) {
|
|
1874
1910
|
console.log(`\n\n ${C.yellow}Cancelling Claude...${C.reset}\n`);
|
|
1875
|
-
|
|
1876
|
-
setTimeout(() => {
|
|
1877
|
-
try { if (activeClaudeProc) activeClaudeProc.kill('SIGKILL'); } catch (e) {}
|
|
1878
|
-
}, 3000);
|
|
1911
|
+
activeAbortController.abort();
|
|
1879
1912
|
} else {
|
|
1880
1913
|
// Save feedback data before exit
|
|
1881
1914
|
if (aiInsights.feedbackHistory.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knoxis-collab",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "AI-enhanced collaborative programming with real-time code review and feedback learning — User + Knoxis + Claude Code with adaptive intelligence",
|
|
5
5
|
"main": "knoxis-collab.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"knoxis-collab": "./knoxis-collab.js"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@anthropic-ai/claude-
|
|
10
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.108"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
13
|
"knoxis",
|