knoxis-helper 1.0.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.
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn, spawnSync } = require('child_process');
6
+
7
+ function parseArgs(argv) {
8
+ const args = {};
9
+ const multi = {};
10
+ for (let i = 2; i < argv.length; i++) {
11
+ const arg = argv[i];
12
+ if (arg.startsWith('--')) {
13
+ const key = arg.slice(2);
14
+ const next = argv[i + 1];
15
+ if (!next || next.startsWith('--')) {
16
+ args[key] = true;
17
+ } else {
18
+ if (multi[key]) {
19
+ multi[key].push(next);
20
+ } else if (args[key]) {
21
+ multi[key] = [args[key], next];
22
+ delete args[key];
23
+ } else {
24
+ args[key] = next;
25
+ }
26
+ i++;
27
+ }
28
+ }
29
+ }
30
+ Object.entries(multi).forEach(([key, list]) => {
31
+ args[key] = list;
32
+ });
33
+ return args;
34
+ }
35
+
36
+ function decodeBase64(value) {
37
+ try {
38
+ return Buffer.from(value, 'base64').toString('utf8');
39
+ } catch (err) {
40
+ console.error('Failed to decode base64 value:', err.message);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ function decodeJsonBase64(value, label) {
46
+ const decoded = decodeBase64(value);
47
+ try {
48
+ return JSON.parse(decoded);
49
+ } catch (err) {
50
+ console.error(`Failed to parse ${label} JSON: ${err.message}`);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ function commandExists(cmd) {
56
+ const detector = process.platform === 'win32' ? 'where' : 'which';
57
+ const result = spawnSync(detector, [cmd], { stdio: 'ignore' });
58
+ return result.status === 0;
59
+ }
60
+
61
+ // Resolve workspace name to path using knoxis registry
62
+ function resolveWorkspacePath(nameOrPath) {
63
+ const os = require('os');
64
+
65
+ // Direct path - check if exists
66
+ if (fs.existsSync(nameOrPath)) {
67
+ return path.resolve(nameOrPath);
68
+ }
69
+
70
+ // Try knoxis workspace registry
71
+ const workspacesFile = path.join(os.homedir(), '.knoxis', 'workspaces.json');
72
+ if (fs.existsSync(workspacesFile)) {
73
+ try {
74
+ const workspaces = JSON.parse(fs.readFileSync(workspacesFile, 'utf8'));
75
+
76
+ // Exact match
77
+ if (workspaces[nameOrPath]) {
78
+ return workspaces[nameOrPath];
79
+ }
80
+
81
+ // Fuzzy match
82
+ const lower = nameOrPath.toLowerCase();
83
+ for (const [name, wsPath] of Object.entries(workspaces)) {
84
+ if (name.toLowerCase().includes(lower)) {
85
+ console.log(`Matched workspace: ${name} -> ${wsPath}`);
86
+ return wsPath;
87
+ }
88
+ }
89
+ } catch (e) {
90
+ // Fall through
91
+ }
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ function resolveAiProvider(preference) {
98
+ const normalized = (preference || 'auto').toLowerCase();
99
+ if (normalized === 'claude' && commandExists('claude')) {
100
+ return { cmd: 'claude', args: ['--dangerously-skip-permissions'], label: 'Claude Code' };
101
+ }
102
+ if (normalized === 'codex' && commandExists('codex')) {
103
+ return { cmd: 'codex', args: [], label: 'Codex' };
104
+ }
105
+ if (normalized === 'auto') {
106
+ if (commandExists('claude')) {
107
+ return { cmd: 'claude', args: ['--dangerously-skip-permissions'], label: 'Claude Code' };
108
+ }
109
+ if (commandExists('codex')) {
110
+ return { cmd: 'codex', args: [], label: 'Codex' };
111
+ }
112
+ }
113
+ console.error('No supported AI provider found. Install the Claude or Codex CLI and try again.');
114
+ process.exit(1);
115
+ }
116
+
117
+ function formatSection(title, body) {
118
+ const border = '-'.repeat(title.length + 4);
119
+ return `${border}\n| ${title} |\n${border}\n${body.trim()}\n`;
120
+ }
121
+
122
+ function toArray(value) {
123
+ if (!value) return [];
124
+ if (Array.isArray(value)) {
125
+ const result = [];
126
+ value.forEach(item => {
127
+ result.push(...toArray(item));
128
+ });
129
+ return result;
130
+ }
131
+ return [value];
132
+ }
133
+
134
+ function gatherContext(workspace, inputs) {
135
+ const sections = [];
136
+ const labels = [];
137
+ const seen = new Set();
138
+
139
+ toArray(inputs).forEach(entry => {
140
+ if (typeof entry !== 'string') {
141
+ return;
142
+ }
143
+ const trimmed = entry.trim();
144
+ if (!trimmed) {
145
+ return;
146
+ }
147
+ const absolute = path.isAbsolute(trimmed) ? trimmed : path.join(workspace, trimmed);
148
+ if (!fs.existsSync(absolute)) {
149
+ return;
150
+ }
151
+ if (seen.has(absolute)) {
152
+ return;
153
+ }
154
+ seen.add(absolute);
155
+ const content = fs.readFileSync(absolute, 'utf8');
156
+ const title = path.relative(workspace, absolute) || path.basename(absolute);
157
+ labels.push(title);
158
+ sections.push(formatSection(title, content));
159
+ });
160
+
161
+ return { sections, labels };
162
+ }
163
+
164
+ function buildDefaultTemplates(stepInput) {
165
+ const defaults = [
166
+ {
167
+ key: 'understand',
168
+ title: 'Understanding',
169
+ template: ({ taskDescription }) => `Let's understand what we're working with before diving in.
170
+
171
+ The task: ${taskDescription}
172
+
173
+ First:
174
+ 1. Read the relevant code to understand the current state
175
+ 2. Identify what needs to change
176
+ 3. Note any potential issues or dependencies
177
+
178
+ Share your understanding briefly. For any ambiguities, state your assumption and move on - do not ask questions.`
179
+ },
180
+ {
181
+ key: 'plan',
182
+ title: 'Planning',
183
+ template: ({ taskDescription }) => `Good. Now create a concrete implementation plan for: ${taskDescription}
184
+
185
+ Plan requirements:
186
+ 1. What files need to be created or modified?
187
+ 2. What's the order of changes?
188
+ 3. What could go wrong?
189
+ 4. How will we verify it works?
190
+
191
+ For any open questions from the previous step, make your best judgment call and note the decision. Do not ask - decide and move forward.`
192
+ },
193
+ {
194
+ key: 'implement',
195
+ title: 'Implementation',
196
+ template: ({ taskDescription }) => `Now implement the plan. Work through it step by step.
197
+
198
+ Rules:
199
+ - Make one logical change at a time
200
+ - For any decisions or unknowns, pick the most standard approach and note your choice
201
+ - If you flagged questions earlier, answer them yourself with reasonable defaults and proceed
202
+ - Flag if you hit something truly blocking (missing credentials, broken dependencies)
203
+ - Otherwise, keep building
204
+
205
+ Start implementing now.`
206
+ },
207
+ {
208
+ key: 'verify',
209
+ title: 'Review',
210
+ template: () => `Let's review what we built.
211
+
212
+ Quick checklist:
213
+ - Does it solve the original problem?
214
+ - Any edge cases we missed?
215
+ - Is the code clean and following project patterns?
216
+ - Anything we should test?
217
+
218
+ Give me the summary and any follow-up recommendations.`
219
+ }
220
+ ];
221
+
222
+ if (!stepInput) {
223
+ return defaults;
224
+ }
225
+
226
+ const requested = (Array.isArray(stepInput) ? stepInput.join(',') : stepInput)
227
+ .split(',')
228
+ .map(s => s.trim().toLowerCase())
229
+ .filter(Boolean);
230
+
231
+ if (!requested.length) {
232
+ return defaults;
233
+ }
234
+
235
+ const resolved = [];
236
+ requested.forEach(key => {
237
+ const match = defaults.find(step => step.key === key);
238
+ if (match) {
239
+ resolved.push(match);
240
+ } else {
241
+ resolved.push({
242
+ key,
243
+ title: key.charAt(0).toUpperCase() + key.slice(1),
244
+ template: ({ taskDescription }) => `Directive (${key}): ${taskDescription}`
245
+ });
246
+ }
247
+ });
248
+
249
+ return resolved.length ? resolved : defaults;
250
+ }
251
+
252
+ function scheduleDefaultSteps(templates, taskDescription, agentLabel) {
253
+ const trimmedTask = taskDescription.trim();
254
+ return templates.map(template => ({
255
+ key: template.key,
256
+ title: template.title,
257
+ displayName: agentLabel,
258
+ persona: null,
259
+ instruction: template.template({ taskDescription: trimmedTask }),
260
+ contextPaths: [],
261
+ includeContext: false
262
+ }));
263
+ }
264
+
265
+ function buildPrompt(options) {
266
+ const {
267
+ systemIntro,
268
+ personaIntro,
269
+ conversation,
270
+ instructionLabel,
271
+ instructionTitle,
272
+ instruction,
273
+ includeContextBlock,
274
+ globalContext,
275
+ stepContext,
276
+ agentLabel
277
+ } = options;
278
+
279
+ const sections = [];
280
+
281
+ if (systemIntro) {
282
+ sections.push(systemIntro);
283
+ }
284
+
285
+ if (personaIntro) {
286
+ sections.push(personaIntro);
287
+ }
288
+
289
+ if (includeContextBlock && globalContext) {
290
+ sections.push(`Project context:\n${globalContext}`);
291
+ }
292
+
293
+ if (includeContextBlock && stepContext) {
294
+ sections.push(`Step-specific context:\n${stepContext}`);
295
+ }
296
+
297
+ if (conversation) {
298
+ sections.push(`Conversation so far:\n${conversation}`);
299
+ }
300
+
301
+ const heading = instructionTitle
302
+ ? `${instructionLabel} -> ${agentLabel} (${instructionTitle})`
303
+ : `${instructionLabel} -> ${agentLabel}`;
304
+
305
+ sections.push(`${heading}:\n${instruction}`);
306
+ sections.push(`${agentLabel}:`);
307
+
308
+ return sections.join('\n\n');
309
+ }
310
+
311
+ async function callAi(aiConfig, prompt, livePrinter) {
312
+ return new Promise((resolve, reject) => {
313
+ const proc = spawn(aiConfig.cmd, aiConfig.args, { stdio: ['pipe', 'pipe', 'pipe'] });
314
+ let stdout = '';
315
+ let stderr = '';
316
+ let pendingLine = '';
317
+
318
+ proc.stdout.on('data', chunk => {
319
+ const text = chunk.toString();
320
+ stdout += text;
321
+ pendingLine += text;
322
+ let index;
323
+ while ((index = pendingLine.indexOf('\n')) !== -1) {
324
+ const line = pendingLine.slice(0, index);
325
+ pendingLine = pendingLine.slice(index + 1);
326
+ livePrinter(line);
327
+ }
328
+ });
329
+
330
+ proc.stderr.on('data', chunk => {
331
+ const text = chunk.toString();
332
+ stderr += text;
333
+ });
334
+
335
+ proc.on('close', code => {
336
+ if (pendingLine.length) {
337
+ livePrinter(pendingLine);
338
+ pendingLine = '';
339
+ }
340
+ if (code === 0) {
341
+ resolve(stdout.trim());
342
+ } else {
343
+ reject(new Error(stderr.trim() || `AI command exited with status ${code}`));
344
+ }
345
+ });
346
+
347
+ proc.stdin.write(prompt);
348
+ proc.stdin.end();
349
+ });
350
+ }
351
+
352
+ async function run() {
353
+ const args = parseArgs(process.argv);
354
+ const workspaceArg = args.workspace || args['workspace-dir'] || args['working-directory'] || args.w;
355
+
356
+ if (!workspaceArg) {
357
+ console.error('Missing required --workspace argument.');
358
+ console.error('Usage: knoxis-pair-program --workspace <name-or-path> --prompt "task"');
359
+ console.error(' knoxis-pair-program -w voice-backend --prompt "add authentication"');
360
+ process.exit(1);
361
+ }
362
+
363
+ // Resolve workspace (supports names from knoxis registry)
364
+ let workspace = resolveWorkspacePath(workspaceArg);
365
+ if (!workspace) {
366
+ // Maybe it's a new path to create
367
+ workspace = path.resolve(workspaceArg);
368
+ console.log(`Creating new workspace: ${workspace}`);
369
+ }
370
+
371
+ if (!fs.existsSync(workspace)) {
372
+ fs.mkdirSync(workspace, { recursive: true });
373
+ }
374
+
375
+ const timeline = args['timeline-base64'] ? decodeJsonBase64(args['timeline-base64'], 'timeline') : null;
376
+
377
+ let task = args['prompt-base64'] ? decodeBase64(args['prompt-base64']) : args.prompt;
378
+ if ((!task || !task.trim()) && timeline && typeof timeline.task === 'string') {
379
+ task = timeline.task;
380
+ }
381
+
382
+ if (!task || !task.trim()) {
383
+ console.error('A task prompt is required via --prompt, --prompt-base64, or timeline.task.');
384
+ process.exit(1);
385
+ }
386
+
387
+ task = task.trim();
388
+
389
+ const aiConfig = resolveAiProvider(args['ai-provider'] || args.provider);
390
+
391
+ const globalContextInputs = [];
392
+ if (args['context']) {
393
+ globalContextInputs.push(args['context']);
394
+ }
395
+ if (args['context-file']) {
396
+ globalContextInputs.push(args['context-file']);
397
+ }
398
+ if (timeline && Array.isArray(timeline.sharedContext)) {
399
+ globalContextInputs.push(timeline.sharedContext);
400
+ }
401
+
402
+ const globalContext = gatherContext(workspace, globalContextInputs);
403
+ const globalContextBlock = globalContext.sections.join('\n\n');
404
+
405
+ let scheduledSteps;
406
+ if (timeline && Array.isArray(timeline.steps) && timeline.steps.length) {
407
+ scheduledSteps = timeline.steps.map((step, index) => ({
408
+ key: step.key || `step-${index + 1}`,
409
+ title: step.title || '',
410
+ displayName: step.displayName || step.agentId || `Agent ${index + 1}`,
411
+ persona: step.persona || null,
412
+ instruction: typeof step.instruction === 'string' && step.instruction.trim().length
413
+ ? step.instruction
414
+ : `Proceed with the shared task: ${task}`,
415
+ contextPaths: step.contextFiles || [],
416
+ includeContext: Boolean(step.includeContext)
417
+ }));
418
+ } else {
419
+ const templates = buildDefaultTemplates(args['steps']);
420
+ scheduledSteps = scheduleDefaultSteps(templates, task, aiConfig.label);
421
+ }
422
+
423
+ if (!scheduledSteps.length) {
424
+ console.error('No steps configured for the pair programming session.');
425
+ process.exit(1);
426
+ }
427
+
428
+ console.log('==============================================');
429
+ console.log('Knoxis Pair Programming Session');
430
+ console.log(`Workspace: ${workspace}`);
431
+ console.log(`AI Partner: ${aiConfig.label}`);
432
+ console.log(`Task: ${task}`);
433
+ console.log('==============================================');
434
+ console.log('');
435
+
436
+ if (globalContext.labels.length) {
437
+ console.log(`Shared context files: ${globalContext.labels.join(', ')}`);
438
+ console.log('');
439
+ }
440
+
441
+ const history = [];
442
+ const conversationLines = () => history.map(entry => `${entry.role}: ${entry.content}`).join('\n\n');
443
+
444
+ const systemIntro = `You are a senior software developer pair programming with your colleague Knoxis.
445
+
446
+ Your communication style:
447
+ - Be direct and concise - no fluff or excessive politeness
448
+ - Think out loud briefly before acting
449
+ - Do NOT ask clarifying questions. Make your best engineering judgment and proceed.
450
+ - If you need to choose between approaches, pick the most standard one and briefly note why.
451
+ - Warn about potential issues or gotchas you notice
452
+ - Suggest better approaches when you see them
453
+
454
+ Your technical approach:
455
+ - Read and understand existing code before making changes
456
+ - Follow existing patterns in the codebase
457
+ - Write clean, maintainable code
458
+ - Consider edge cases and error handling
459
+ - Don't over-engineer - solve the problem at hand
460
+ - Leave the codebase better than you found it
461
+
462
+ IMPORTANT: Work autonomously. Do not ask questions or wait for confirmation. Make decisions and implement.
463
+ Only work inside the provided workspace and preserve user data.`;
464
+
465
+ for (const step of scheduledSteps) {
466
+ const stepContext = gatherContext(workspace, step.contextPaths);
467
+ if (stepContext.labels.length) {
468
+ console.log(`Step context for ${step.displayName}: ${stepContext.labels.join(', ')}`);
469
+ console.log('');
470
+ }
471
+
472
+ const titleSuffix = step.title ? ` (${step.title})` : '';
473
+ console.log(`Coordinator -> ${step.displayName}${titleSuffix}:`);
474
+ console.log(step.instruction);
475
+ console.log('');
476
+
477
+ history.push({ role: 'Coordinator', content: step.instruction });
478
+
479
+ console.log(`${step.displayName}:`);
480
+
481
+ const includeContext = history.length <= 1 || step.includeContext || stepContext.sections.length > 0;
482
+ const prompt = buildPrompt({
483
+ systemIntro,
484
+ personaIntro: step.persona,
485
+ conversation: conversationLines(),
486
+ instructionLabel: 'Coordinator',
487
+ instructionTitle: step.title,
488
+ instruction: step.instruction,
489
+ includeContextBlock: includeContext,
490
+ globalContext: globalContextBlock,
491
+ stepContext: stepContext.sections.join('\n\n'),
492
+ agentLabel: step.displayName
493
+ });
494
+
495
+ try {
496
+ const response = await callAi(aiConfig, prompt, line => {
497
+ console.log(` ${line}`);
498
+ });
499
+ history.push({ role: step.displayName, content: response });
500
+ console.log('');
501
+ } catch (err) {
502
+ console.error(`Failed to complete step "${step.title || step.key}": ${err.message}`);
503
+ process.exit(1);
504
+ }
505
+ }
506
+
507
+ console.log('Session complete. Knoxis and the AI partner are standing by for further instructions.');
508
+ }
509
+
510
+ run().catch(err => {
511
+ console.error(err.message);
512
+ process.exit(1);
513
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "knoxis-helper",
3
+ "version": "1.0.0",
4
+ "description": "Local helper for Knoxis pair programming - connects your machine to Knoxis on qig.ai",
5
+ "bin": {
6
+ "knoxis-helper": "./bin/knoxis-helper.js"
7
+ },
8
+ "keywords": ["knoxis", "pair-programming", "claude", "qig"],
9
+ "license": "MIT",
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "lib/"
16
+ ]
17
+ }