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.
Files changed (2) hide show
  1. package/knoxis-collab.js +175 -142
  2. 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
- // Resolve Claude binarycheck every reasonable location
116
- const IS_WIN = process.platform === 'win32';
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
- // 3. Global npm prefix bin (same dir where knoxis-collab bin lands)
119
+ async function loadClaudeAgentSdk() {
120
+ if (claudeAgentSdk) return claudeAgentSdk;
139
121
  try {
140
- const { status, stdout } = spawnSync('npm', ['prefix', '-g'], { stdio: 'pipe', shell: IS_WIN });
141
- if (status === 0) {
142
- const prefix = stdout.toString().trim();
143
- // npm global bins: prefix/bin on unix, prefix on windows
144
- const globalBin = existsAny(path.join(prefix, IS_WIN ? '' : 'bin', 'claude'));
145
- if (globalBin) return globalBin;
146
- }
147
- } catch (e) {}
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 Code (session-persistent)`);
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
- return CLAUDE_BIN !== null;
1121
+ async function checkClaudeInstalled() {
1122
+ try {
1123
+ await loadClaudeAgentSdk();
1124
+ return true;
1125
+ } catch (e) {
1126
+ return false;
1127
+ }
1164
1128
  }
1165
1129
 
1166
- function dispatchToClaude(prompt) {
1167
- return new Promise((resolve, reject) => {
1168
- claudeDispatches++;
1169
- isClaudeRunning = true;
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
- const args = ['-p', '--dangerously-skip-permissions'];
1172
- if (claudeSessionId) {
1173
- args.push('--resume', claudeSessionId);
1174
- } else {
1175
- claudeSessionId = SESSION_ID;
1176
- args.push('--session-id', claudeSessionId);
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
- log(`DISPATCH #${claudeDispatches}:\n${prompt}`);
1163
+ // Create abort controller for cancellation support
1164
+ activeAbortController = new AbortController();
1165
+ queryOptions.abortController = activeAbortController;
1180
1166
 
1181
- // On Windows, spawn with shell requires a single command string to avoid DEP0190
1182
- const spawnCmd = IS_WIN ? `"${CLAUDE_BIN}" ${args.join(' ')}` : CLAUDE_BIN;
1183
- const spawnArgs = IS_WIN ? [] : args;
1167
+ // Start spinner
1168
+ if (!verbose) {
1169
+ spinner.start('Claude is processing');
1170
+ }
1184
1171
 
1185
- const proc = spawn(spawnCmd, spawnArgs, {
1186
- cwd: WORKSPACE,
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
- activeClaudeProc = proc;
1193
- let stdout = '';
1194
- let stderr = '';
1195
- let outputLines = 0;
1196
- const startTime = Date.now();
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
- // Start spinner when Claude begins processing
1199
- if (!verbose) {
1200
- spinner.start('Claude is processing');
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
- proc.stdout.on('data', (chunk) => {
1204
- const text = chunk.toString();
1205
- stdout += text;
1206
- outputLines += text.split('\n').filter(l => l.trim()).length;
1207
- printClaudeLine(text);
1208
- if (!verbose) {
1209
- // Update spinner with progress info
1210
- spinner.update(`Claude is processing (${outputLines} lines)`);
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
- proc.stderr.on('data', (chunk) => {
1215
- const text = chunk.toString();
1216
- if (!text.includes('Debug:') && !text.includes('trace')) {
1217
- stderr += text;
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
- proc.on('close', (code) => {
1222
- isClaudeRunning = false;
1223
- activeClaudeProc = null;
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
- // Stop spinner
1226
- if (!verbose) {
1227
- spinner.stop();
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
- clearClaudeStatus();
1250
+ }
1230
1251
 
1231
- const elapsed = Math.round((Date.now() - startTime) / 1000);
1232
- log(`CLAUDE DONE (exit ${code}, ${elapsed}s, ${outputLines} lines):\n${stdout.slice(0, 2000)}`);
1252
+ isClaudeRunning = false;
1253
+ activeAbortController = null;
1233
1254
 
1234
- if (code === 0 || stdout.length > 0) {
1235
- resolve({ output: stdout.trim(), exitCode: code || 0, elapsed, lines: outputLines });
1236
- } else {
1237
- reject(new Error(`Claude exited with code ${code}${stderr ? ': ' + stderr.slice(0, 500) : ''}`));
1238
- }
1239
- });
1255
+ // Stop spinner
1256
+ if (!verbose) {
1257
+ spinner.stop();
1258
+ }
1259
+ clearClaudeStatus();
1240
1260
 
1241
- proc.on('error', (err) => {
1242
- isClaudeRunning = false;
1243
- activeClaudeProc = null;
1261
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
1262
+ const exitCode = (resultData && resultData.subtype === 'success') ? 0 : 1;
1244
1263
 
1245
- // Stop spinner on error
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
- proc.stdin.write(prompt);
1254
- proc.stdin.end();
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: 'claude' CLI not found.${C.reset}`);
1830
- console.error(` It should be bundled with this package. Try: npm install`);
1831
- console.error(` Or install globally: npm install -g @anthropic-ai/claude-code\n`);
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 — kill Claude if running, otherwise exit
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 && activeClaudeProc) {
1909
+ if (isClaudeRunning && activeAbortController) {
1874
1910
  console.log(`\n\n ${C.yellow}Cancelling Claude...${C.reset}\n`);
1875
- try { activeClaudeProc.kill('SIGTERM'); } catch (e) {}
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.4.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-code": "^1.0.0"
10
+ "@anthropic-ai/claude-agent-sdk": "^0.2.108"
11
11
  },
12
12
  "keywords": [
13
13
  "knoxis",