@kernel.chat/kbot 3.33.1 → 3.34.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.
@@ -0,0 +1,196 @@
1
+ // kbot OpenClaw Connect — One-command OpenClaw Gateway setup
2
+ //
3
+ // Checks gateway availability, creates a SOUL.md agent definition,
4
+ // and verifies connectivity with a test message.
5
+ //
6
+ // Usage: kbot openclaw connect
7
+ import { join } from 'node:path';
8
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
9
+ import chalk from 'chalk';
10
+ import { KBOT_DIR } from './auth.js';
11
+ // ── Constants ──
12
+ const DEFAULT_GATEWAY_URL = 'http://127.0.0.1:18789';
13
+ const GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL || DEFAULT_GATEWAY_URL;
14
+ const SOUL_PATH = join(KBOT_DIR, 'openclaw-soul.md');
15
+ // ── Helpers ──
16
+ /** Check if the OpenClaw Gateway is reachable */
17
+ async function checkGateway() {
18
+ try {
19
+ const res = await fetch(`${GATEWAY_URL}/health`, {
20
+ signal: AbortSignal.timeout(5000),
21
+ });
22
+ if (!res.ok)
23
+ return null;
24
+ const data = await res.json();
25
+ return data;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** Fetch full gateway status (models, platforms, etc.) */
32
+ async function fetchGatewayStatus() {
33
+ try {
34
+ const res = await fetch(`${GATEWAY_URL}/status`, {
35
+ signal: AbortSignal.timeout(5000),
36
+ });
37
+ if (!res.ok)
38
+ return {};
39
+ return await res.json();
40
+ }
41
+ catch {
42
+ return {};
43
+ }
44
+ }
45
+ /** Send a test message to verify connectivity */
46
+ async function sendTestMessage() {
47
+ try {
48
+ const res = await fetch(`${GATEWAY_URL}/v1/chat/completions`, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify({
52
+ model: 'auto',
53
+ messages: [
54
+ { role: 'system', content: 'You are kbot, confirming OpenClaw connectivity. Respond with exactly: connected' },
55
+ { role: 'user', content: 'ping' },
56
+ ],
57
+ max_tokens: 16,
58
+ }),
59
+ signal: AbortSignal.timeout(10000),
60
+ });
61
+ return res.ok;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ // ── SOUL.md Generation ──
68
+ const SPECIALIST_IDS = [
69
+ 'kernel', 'researcher', 'coder', 'writer', 'analyst',
70
+ 'aesthete', 'guardian', 'curator', 'strategist',
71
+ 'infrastructure', 'quant', 'investigator', 'oracle',
72
+ 'chronist', 'sage', 'communicator', 'adapter', 'immune',
73
+ ];
74
+ function generateSoulMd() {
75
+ const specialistList = SPECIALIST_IDS
76
+ .map(id => `- **${id}** — specialist agent`)
77
+ .join('\n');
78
+ return `# SOUL.md — kbot OpenClaw Agent Definition
79
+
80
+ ## Identity
81
+
82
+ - **Name**: kbot
83
+ - **Type**: Terminal AI agent
84
+ - **Version**: OpenClaw-connected
85
+ - **Source**: https://github.com/isaacsight/kernel
86
+ - **License**: MIT
87
+
88
+ ## Purpose
89
+
90
+ kbot is an open-source terminal AI agent that connects to the OpenClaw Gateway
91
+ to access local and remote AI models. It brings ${SPECIALIST_IDS.length} specialist agents,
92
+ 290+ tools, and 20 AI providers into a single CLI interface.
93
+
94
+ ## Specialists
95
+
96
+ ${specialistList}
97
+
98
+ ## Capabilities
99
+
100
+ - Multi-provider AI (Anthropic, OpenAI, Google, + 17 more)
101
+ - 290+ built-in tools (file ops, git, GitHub, web search, browser, sandbox)
102
+ - Autonomous planning and execution
103
+ - Learning engine (patterns, solutions, user profile)
104
+ - Session persistence and memory synthesis
105
+ - MCP server/client support
106
+ - Local model support (Ollama, LM Studio, embedded llama.cpp)
107
+
108
+ ## OpenClaw Integration
109
+
110
+ - **Gateway URL**: ${GATEWAY_URL}
111
+ - **Protocol**: OpenAI-compatible chat completions (v1/chat/completions)
112
+ - **Agent routing**: kbot routes to specialists based on intent classification
113
+ - **Tool execution**: Local-first, then API calls as needed
114
+ - **Memory**: Persistent across sessions at ~/.kbot/
115
+
116
+ ## Interaction Style
117
+
118
+ - Act, don't advise — execute tasks directly
119
+ - Local-first — use local tools before making API calls
120
+ - Failures trigger fallbacks, not stops
121
+ - Route work to the right specialist
122
+ - Compound improvements — each session leaves things better
123
+ `;
124
+ }
125
+ // ── Main ──
126
+ export async function runOpenClawConnect() {
127
+ const DIM = chalk.dim;
128
+ const ACCENT = chalk.hex('#A78BFA');
129
+ const GREEN = chalk.hex('#4ADE80');
130
+ const RED = chalk.hex('#F87171');
131
+ const CYAN = chalk.hex('#67E8F9');
132
+ console.log();
133
+ console.log(` ${ACCENT('◉')} ${chalk.bold('kbot OpenClaw Connect')}`);
134
+ console.log(` ${DIM('─'.repeat(40))}`);
135
+ console.log();
136
+ // Step 1: Check if gateway is running
137
+ console.log(` ${DIM('Checking OpenClaw Gateway at')} ${CYAN(GATEWAY_URL)}${DIM('...')}`);
138
+ const health = await checkGateway();
139
+ if (!health) {
140
+ console.log();
141
+ console.log(` ${RED('✗')} OpenClaw Gateway is not running.`);
142
+ console.log();
143
+ console.log(` ${chalk.bold('To install and start OpenClaw:')}`);
144
+ console.log();
145
+ console.log(` ${CYAN('1.')} Install the gateway:`);
146
+ console.log(` ${chalk.white('pip install openclaw')}`);
147
+ console.log(` ${DIM('or: brew install openclaw/tap/openclaw')}`);
148
+ console.log();
149
+ console.log(` ${CYAN('2.')} Start the gateway:`);
150
+ console.log(` ${chalk.white('openclaw serve')}`);
151
+ console.log(` ${DIM('Default: http://127.0.0.1:18789')}`);
152
+ console.log();
153
+ console.log(` ${CYAN('3.')} Set a custom URL (optional):`);
154
+ console.log(` ${chalk.white('export OPENCLAW_GATEWAY_URL=http://your-host:port')}`);
155
+ console.log();
156
+ console.log(` ${DIM('Then run')} ${chalk.white('kbot openclaw connect')} ${DIM('again.')}`);
157
+ console.log();
158
+ return;
159
+ }
160
+ console.log(` ${GREEN('✓')} Gateway is running${health.version ? ` (v${health.version})` : ''}`);
161
+ // Step 2: Fetch full status
162
+ const status = await fetchGatewayStatus();
163
+ const platforms = status.platforms || ['local'];
164
+ const models = status.models || [];
165
+ if (models.length > 0) {
166
+ console.log(` ${GREEN('✓')} Models available: ${models.slice(0, 5).join(', ')}${models.length > 5 ? ` +${models.length - 5} more` : ''}`);
167
+ }
168
+ // Step 3: Create SOUL.md
169
+ if (!existsSync(KBOT_DIR)) {
170
+ mkdirSync(KBOT_DIR, { recursive: true });
171
+ }
172
+ const soulContent = generateSoulMd();
173
+ writeFileSync(SOUL_PATH, soulContent, 'utf-8');
174
+ console.log(` ${GREEN('✓')} Agent definition saved to ${DIM(SOUL_PATH)}`);
175
+ // Step 4: Send test message
176
+ console.log(` ${DIM('Sending test message...')}`);
177
+ const testOk = await sendTestMessage();
178
+ if (testOk) {
179
+ console.log(` ${GREEN('✓')} Connectivity verified`);
180
+ }
181
+ else {
182
+ console.log(` ${chalk.hex('#FBBF24')('⚠')} Test message failed — gateway is up but may not have a model loaded.`);
183
+ console.log(` ${DIM('Try: ollama pull llama3.1:8b')}`);
184
+ }
185
+ // Step 5: Print success summary
186
+ console.log();
187
+ console.log(` ${GREEN('kbot is now connected to OpenClaw.')}`);
188
+ console.log(` ${DIM('Your agents are available on:')} ${platforms.map(p => chalk.white(p)).join(', ')}`);
189
+ console.log();
190
+ console.log(` ${DIM('Usage:')}`);
191
+ console.log(` ${chalk.white('kbot')} ${DIM('— start interactive session via OpenClaw')}`);
192
+ console.log(` ${chalk.white('kbot --agent coder')} ${DIM('— use a specific specialist')}`);
193
+ console.log(` ${chalk.white('kbot openclaw status')} ${DIM('— check gateway status')}`);
194
+ console.log();
195
+ }
196
+ //# sourceMappingURL=openclaw-connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openclaw-connect.js","sourceRoot":"","sources":["../src/openclaw-connect.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,mEAAmE;AACnE,iDAAiD;AACjD,EAAE;AACF,+BAA+B;AAG/B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEpC,kBAAkB;AAElB,MAAM,mBAAmB,GAAG,wBAAwB,CAAA;AACpD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,mBAAmB,CAAA;AAC3E,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAA;AAWpD,gBAAgB;AAEhB,iDAAiD;AACjD,KAAK,UAAU,YAAY;IACzB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,WAAW,SAAS,EAAE;YAC/C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;SAClC,CAAC,CAAA;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,IAAI,CAAA;QACxB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAmB,CAAA;QAC9C,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,0DAA0D;AAC1D,KAAK,UAAU,kBAAkB;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,WAAW,SAAS,EAAE;YAC/C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;SAClC,CAAC,CAAA;QACF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,EAAE,CAAA;QACtB,OAAO,MAAM,GAAG,CAAC,IAAI,EAAmB,CAAA;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,iDAAiD;AACjD,KAAK,UAAU,eAAe;IAC5B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,WAAW,sBAAsB,EAAE;YAC5D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,MAAM;gBACb,QAAQ,EAAE;oBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,iFAAiF,EAAE;oBAC9G,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;iBAClC;gBACD,UAAU,EAAE,EAAE;aACf,CAAC;YACF,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SACnC,CAAC,CAAA;QACF,OAAO,GAAG,CAAC,EAAE,CAAA;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,2BAA2B;AAE3B,MAAM,cAAc,GAAG;IACrB,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS;IACpD,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY;IAC/C,gBAAgB,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ;IACnD,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,QAAQ;CAC/C,CAAA;AAEV,SAAS,cAAc;IACrB,MAAM,cAAc,GAAG,cAAc;SAClC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,CAAC;SAC3C,IAAI,CAAC,IAAI,CAAC,CAAA;IAEb,OAAO;;;;;;;;;;;;;kDAayC,cAAc,CAAC,MAAM;;;;;EAKrE,cAAc;;;;;;;;;;;;;;qBAcK,WAAW;;;;;;;;;;;;;CAa/B,CAAA;AACD,CAAC;AAED,aAAa;AAEb,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAA;IACrB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACnC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAClC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAEjC,OAAO,CAAC,GAAG,EAAE,CAAA;IACb,OAAO,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,EAAE,CAAC,CAAA;IACtE,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;IACvC,OAAO,CAAC,GAAG,EAAE,CAAA;IAEb,sCAAsC;IACtC,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,8BAA8B,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACzF,MAAM,MAAM,GAAG,MAAM,YAAY,EAAE,CAAA;IAEnC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,EAAE,CAAA;QACb,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAA;QAC7D,OAAO,CAAC,GAAG,EAAE,CAAA;QACb,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,gCAAgC,CAAC,EAAE,CAAC,CAAA;QAChE,OAAO,CAAC,GAAG,EAAE,CAAA;QACb,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAA;QACnD,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAA;QAC1D,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,wCAAwC,CAAC,EAAE,CAAC,CAAA;QACpE,OAAO,CAAC,GAAG,EAAE,CAAA;QACb,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;QACjD,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;QACpD,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,CAAC,iCAAiC,CAAC,EAAE,CAAC,CAAA;QAC7D,OAAO,CAAC,GAAG,EAAE,CAAA;QACb,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAA;QAC3D,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,CAAC,KAAK,CAAC,mDAAmD,CAAC,EAAE,CAAC,CAAA;QACvF,OAAO,CAAC,GAAG,EAAE,CAAA;QACb,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,uBAAuB,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;QAC5F,OAAO,CAAC,GAAG,EAAE,CAAA;QACb,OAAM;IACR,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEjG,4BAA4B;IAC5B,MAAM,MAAM,GAAG,MAAM,kBAAkB,EAAE,CAAA;IACzC,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAA;IAElC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,sBAAsB,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC5I,CAAC;IAED,yBAAyB;IACzB,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC1C,CAAC;IAED,MAAM,WAAW,GAAG,cAAc,EAAE,CAAA;IACpC,aAAa,CAAC,SAAS,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;IAC9C,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,8BAA8B,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;IAE1E,4BAA4B;IAC5B,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,yBAAyB,CAAC,EAAE,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,eAAe,EAAE,CAAA;IAEtC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;IACtD,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,uEAAuE,CAAC,CAAA;QAClH,OAAO,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,8BAA8B,CAAC,EAAE,CAAC,CAAA;IAC3D,CAAC;IAED,gCAAgC;IAChC,OAAO,CAAC,GAAG,EAAE,CAAA;IACb,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,oCAAoC,CAAC,EAAE,CAAC,CAAA;IAC/D,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,+BAA+B,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACzG,OAAO,CAAC,GAAG,EAAE,CAAA;IACb,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IACjC,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,qBAAqB,GAAG,CAAC,0CAA0C,CAAC,EAAE,CAAC,CAAA;IAC7G,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,CAAC,KAAK,CAAC,oBAAoB,CAAC,OAAO,GAAG,CAAC,6BAA6B,CAAC,EAAE,CAAC,CAAA;IAChG,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,CAAC,KAAK,CAAC,sBAAsB,CAAC,KAAK,GAAG,CAAC,wBAAwB,CAAC,EAAE,CAAC,CAAA;IAC3F,OAAO,CAAC,GAAG,EAAE,CAAA;AACf,CAAC"}
package/dist/pair.d.ts CHANGED
@@ -47,7 +47,17 @@ declare let sessionStats: {
47
47
  suggestionsShown: number;
48
48
  fixesApplied: number;
49
49
  errorsFound: number;
50
+ testsRun: number;
51
+ testsFailed: number;
50
52
  };
53
+ /**
54
+ * Get files that frequently change together with the given file.
55
+ * Returns top co-changing files sorted by frequency.
56
+ */
57
+ export declare function getCoChangePatterns(targetFile?: string): Array<{
58
+ pair: [string, string];
59
+ count: number;
60
+ }>;
51
61
  export declare function startPairMode(options?: PairOptions): Promise<void>;
52
62
  export declare function stopPairMode(): void;
53
63
  /**
@@ -69,6 +79,18 @@ export declare function analyzeWithAgent(filePath: string, agentOptions?: {
69
79
  model?: string;
70
80
  tier?: string;
71
81
  }): Promise<string>;
82
+ /**
83
+ * runPair() — Start AI pair programming watch mode.
84
+ *
85
+ * 1. Watches cwd (or specified path) for file changes using fs.watch
86
+ * 2. When .ts/.js/.py/.rs/.go files change:
87
+ * - Runs the relevant test if one exists (detects vitest/jest/pytest/cargo/go)
88
+ * - If test fails, shows the failure with fix suggestions
89
+ * - Tracks which files change together (co-change pattern extraction)
90
+ * 3. Simple terminal output: file changed, action taken, result
91
+ * 4. Runs until Ctrl+C
92
+ */
93
+ export declare function runPair(path?: string, options?: Omit<PairOptions, 'path'>): Promise<void>;
72
94
  /**
73
95
  * Register the `kbot pair` command with the CLI program.
74
96
  *
@@ -1 +1 @@
1
- {"version":3,"file":"pair.d.ts","sourceRoot":"","sources":["../src/pair.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAMxC,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,kEAAkE;IAClE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,oCAAoC;IACpC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,mCAAmC;IACnC,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,oCAAoC;IACpC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,+CAA+C;IAC/C,YAAY,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACjE;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,CAAA;IAC1C,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,IAAI,CAAA;IAC3E,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,UAAU,CAAA;IAClB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,OAAO,CAAA;CACf;AA4GD,QAAA,IAAI,YAAY;;;;;CAA6E,CAAA;AAkuB7F,wBAAsB,aAAa,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4G5E;AAED,wBAAgB,YAAY,IAAI,IAAI,CAsBnC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,YAAY,CAElD;AAMD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAMD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmE1D"}
1
+ {"version":3,"file":"pair.d.ts","sourceRoot":"","sources":["../src/pair.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAMxC,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,kEAAkE;IAClE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,oCAAoC;IACpC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,mCAAmC;IACnC,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,oCAAoC;IACpC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,+CAA+C;IAC/C,YAAY,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACjE;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,KAAK,CAAA;IAC1C,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,GAAG,IAAI,CAAA;IAC3E,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,UAAU,CAAA;IAClB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,OAAO,CAAA;CACf;AA4GD,QAAA,IAAI,YAAY;;;;;;;CAA0G,CAAA;AAmB1H;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAqBzG;AAw8BD,wBAAsB,aAAa,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+G5E;AAED,wBAAgB,YAAY,IAAI,IAAI,CAmCnC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,YAAY,CAElD;AAMD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/F;AAMD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmE1D"}
package/dist/pair.js CHANGED
@@ -103,7 +103,243 @@ const SECURITY_PATTERNS = [
103
103
  let activeWatcher = null;
104
104
  let debounceTimer = null;
105
105
  const pendingChanges = new Set();
106
- let sessionStats = { filesAnalyzed: 0, suggestionsShown: 0, fixesApplied: 0, errorsFound: 0 };
106
+ let sessionStats = { filesAnalyzed: 0, suggestionsShown: 0, fixesApplied: 0, errorsFound: 0, testsRun: 0, testsFailed: 0 };
107
+ // ---------------------------------------------------------------------------
108
+ // Co-change pattern tracking
109
+ // ---------------------------------------------------------------------------
110
+ /** Tracks which files change together within a short time window. */
111
+ const coChangeHistory = [];
112
+ const CO_CHANGE_WINDOW_MS = 5000; // files changing within 5s are "together"
113
+ const MAX_CO_CHANGE_HISTORY = 200;
114
+ function recordCoChange(files) {
115
+ if (files.length < 2)
116
+ return;
117
+ coChangeHistory.push({ files: files.sort(), timestamp: Date.now() });
118
+ if (coChangeHistory.length > MAX_CO_CHANGE_HISTORY) {
119
+ coChangeHistory.splice(0, coChangeHistory.length - MAX_CO_CHANGE_HISTORY);
120
+ }
121
+ }
122
+ /**
123
+ * Get files that frequently change together with the given file.
124
+ * Returns top co-changing files sorted by frequency.
125
+ */
126
+ export function getCoChangePatterns(targetFile) {
127
+ const pairCounts = new Map();
128
+ for (const entry of coChangeHistory) {
129
+ for (let i = 0; i < entry.files.length; i++) {
130
+ for (let j = i + 1; j < entry.files.length; j++) {
131
+ if (targetFile && entry.files[i] !== targetFile && entry.files[j] !== targetFile)
132
+ continue;
133
+ const key = `${entry.files[i]}||${entry.files[j]}`;
134
+ pairCounts.set(key, (pairCounts.get(key) || 0) + 1);
135
+ }
136
+ }
137
+ }
138
+ return Array.from(pairCounts.entries())
139
+ .map(([key, count]) => {
140
+ const [a, b] = key.split('||');
141
+ return { pair: [a, b], count };
142
+ })
143
+ .filter(e => e.count >= 2)
144
+ .sort((a, b) => b.count - a.count)
145
+ .slice(0, 10);
146
+ }
147
+ function detectTestRunner(projectRoot) {
148
+ const pkgPath = join(projectRoot, 'package.json');
149
+ if (existsSync(pkgPath)) {
150
+ try {
151
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
152
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
153
+ const scripts = pkg.scripts || {};
154
+ // Vitest
155
+ if (deps.vitest || (scripts.test && scripts.test.includes('vitest'))) {
156
+ return {
157
+ name: 'vitest',
158
+ command: 'npx vitest run',
159
+ singleFileCommand: (f) => `npx vitest run "${f}" --reporter=verbose`,
160
+ };
161
+ }
162
+ // Jest
163
+ if (deps.jest || deps['@jest/core'] || (scripts.test && scripts.test.includes('jest'))) {
164
+ return {
165
+ name: 'jest',
166
+ command: 'npx jest',
167
+ singleFileCommand: (f) => `npx jest "${f}" --verbose`,
168
+ };
169
+ }
170
+ // Mocha
171
+ if (deps.mocha) {
172
+ return {
173
+ name: 'mocha',
174
+ command: 'npx mocha',
175
+ singleFileCommand: (f) => `npx mocha "${f}"`,
176
+ };
177
+ }
178
+ }
179
+ catch { /* ignore */ }
180
+ }
181
+ // Python: pytest
182
+ if (existsSync(join(projectRoot, 'pytest.ini')) ||
183
+ existsSync(join(projectRoot, 'pyproject.toml')) ||
184
+ existsSync(join(projectRoot, 'setup.cfg'))) {
185
+ try {
186
+ const content = existsSync(join(projectRoot, 'pyproject.toml'))
187
+ ? readFileSync(join(projectRoot, 'pyproject.toml'), 'utf-8')
188
+ : '';
189
+ if (content.includes('pytest') || existsSync(join(projectRoot, 'pytest.ini'))) {
190
+ return {
191
+ name: 'pytest',
192
+ command: 'python -m pytest',
193
+ singleFileCommand: (f) => `python -m pytest "${f}" -v`,
194
+ };
195
+ }
196
+ }
197
+ catch { /* ignore */ }
198
+ }
199
+ // Rust: cargo test
200
+ if (existsSync(join(projectRoot, 'Cargo.toml'))) {
201
+ return {
202
+ name: 'cargo test',
203
+ command: 'cargo test',
204
+ singleFileCommand: (_f) => 'cargo test',
205
+ };
206
+ }
207
+ // Go: go test
208
+ if (existsSync(join(projectRoot, 'go.mod'))) {
209
+ return {
210
+ name: 'go test',
211
+ command: 'go test ./...',
212
+ singleFileCommand: (f) => `go test -v -run . "${dirname(f)}"`,
213
+ };
214
+ }
215
+ return null;
216
+ }
217
+ /**
218
+ * Find the test file corresponding to a source file, if it exists.
219
+ */
220
+ function findTestFile(sourcePath) {
221
+ const ext = extname(sourcePath);
222
+ const dir = dirname(sourcePath);
223
+ const nameWithoutExt = basename(sourcePath, ext);
224
+ // Skip test/spec files themselves
225
+ if (nameWithoutExt.endsWith('.test') || nameWithoutExt.endsWith('.spec'))
226
+ return null;
227
+ if (nameWithoutExt.endsWith('_test'))
228
+ return null;
229
+ // JS/TS patterns
230
+ const jsTsPatterns = [
231
+ join(dir, `${nameWithoutExt}.test${ext}`),
232
+ join(dir, `${nameWithoutExt}.spec${ext}`),
233
+ join(dir, `${nameWithoutExt}.test.ts`),
234
+ join(dir, `${nameWithoutExt}.spec.ts`),
235
+ join(dir, '__tests__', `${nameWithoutExt}.test${ext}`),
236
+ join(dir, '__tests__', `${nameWithoutExt}.spec${ext}`),
237
+ ];
238
+ // Python patterns
239
+ const pyPatterns = [
240
+ join(dir, `test_${nameWithoutExt}.py`),
241
+ join(dir, `${nameWithoutExt}_test.py`),
242
+ join(dir, 'tests', `test_${nameWithoutExt}.py`),
243
+ ];
244
+ // Go patterns
245
+ const goPatterns = [
246
+ join(dir, `${nameWithoutExt}_test.go`),
247
+ ];
248
+ // Rust tests are inline (in the same file), so just return the source
249
+ if (ext === '.rs') {
250
+ try {
251
+ const content = readFileSync(sourcePath, 'utf-8');
252
+ if (content.includes('#[cfg(test)]') || content.includes('#[test]')) {
253
+ return sourcePath;
254
+ }
255
+ }
256
+ catch { /* ignore */ }
257
+ return null;
258
+ }
259
+ const allPatterns = [...jsTsPatterns, ...pyPatterns, ...goPatterns];
260
+ for (const p of allPatterns) {
261
+ if (existsSync(p))
262
+ return p;
263
+ }
264
+ return null;
265
+ }
266
+ /**
267
+ * Run the test file for a changed source file.
268
+ * Returns suggestions about the test result.
269
+ */
270
+ function runTestForFile(change) {
271
+ const projectRoot = findProjectRoot(change.fullPath);
272
+ if (!projectRoot)
273
+ return [];
274
+ const testRunner = detectTestRunner(projectRoot);
275
+ if (!testRunner)
276
+ return [];
277
+ const testFile = findTestFile(change.fullPath);
278
+ if (!testFile)
279
+ return [];
280
+ const suggestions = [];
281
+ const relativeTestFile = testFile.replace(projectRoot + '/', '');
282
+ try {
283
+ execSync(testRunner.singleFileCommand(relativeTestFile), {
284
+ encoding: 'utf-8',
285
+ timeout: 30000,
286
+ cwd: projectRoot,
287
+ stdio: ['pipe', 'pipe', 'pipe'],
288
+ });
289
+ sessionStats.testsRun++;
290
+ suggestions.push({
291
+ type: 'info',
292
+ category: 'test',
293
+ file: change.file,
294
+ message: `Tests passed (${testRunner.name}: ${relativeTestFile})`,
295
+ });
296
+ }
297
+ catch (err) {
298
+ sessionStats.testsRun++;
299
+ sessionStats.testsFailed++;
300
+ const stdout = err instanceof Error && 'stdout' in err
301
+ ? String(err.stdout)
302
+ : '';
303
+ const stderr = err instanceof Error && 'stderr' in err
304
+ ? String(err.stderr)
305
+ : '';
306
+ const output = (stdout + '\n' + stderr).trim();
307
+ // Extract the most relevant failure line
308
+ const failureLines = output.split('\n')
309
+ .filter(l => /fail|error|assert|expect/i.test(l) && !l.includes('node_modules'))
310
+ .slice(0, 3);
311
+ suggestions.push({
312
+ type: 'error',
313
+ category: 'test',
314
+ file: change.file,
315
+ message: `Test failed (${testRunner.name}: ${relativeTestFile})`,
316
+ });
317
+ for (const line of failureLines) {
318
+ const trimmed = line.trim().slice(0, 120);
319
+ if (trimmed) {
320
+ suggestions.push({
321
+ type: 'error',
322
+ category: 'test',
323
+ file: change.file,
324
+ message: ` ${trimmed}`,
325
+ });
326
+ }
327
+ }
328
+ if (failureLines.length === 0 && output.length > 0) {
329
+ // Show last few lines as fallback
330
+ const lastLines = output.split('\n').filter(l => l.trim()).slice(-3);
331
+ for (const line of lastLines) {
332
+ suggestions.push({
333
+ type: 'error',
334
+ category: 'test',
335
+ file: change.file,
336
+ message: ` ${line.trim().slice(0, 120)}`,
337
+ });
338
+ }
339
+ }
340
+ }
341
+ return suggestions;
342
+ }
107
343
  // ---------------------------------------------------------------------------
108
344
  // Config management
109
345
  // ---------------------------------------------------------------------------
@@ -703,6 +939,10 @@ async function analyzeChanges(changes, config, options) {
703
939
  if (isSource && config.checks.style) {
704
940
  suggestions.push(...checkStyle(change));
705
941
  }
942
+ // Run tests for changed source files
943
+ if (isSource && config.checks.missingTests) {
944
+ suggestions.push(...runTestForFile(change));
945
+ }
706
946
  // Apply auto-fixes if enabled
707
947
  let autoFixed = [];
708
948
  if (config.autoFix || options.autoFix) {
@@ -779,7 +1019,7 @@ export async function startPairMode(options = {}) {
779
1019
  return;
780
1020
  }
781
1021
  // Reset session stats
782
- sessionStats = { filesAnalyzed: 0, suggestionsShown: 0, fixesApplied: 0, errorsFound: 0 };
1022
+ sessionStats = { filesAnalyzed: 0, suggestionsShown: 0, fixesApplied: 0, errorsFound: 0, testsRun: 0, testsFailed: 0 };
783
1023
  // Print banner
784
1024
  process.stderr.write('\n');
785
1025
  process.stderr.write(` ${ACCENT('kbot')} ${chalk.bold('pair')} ${DIM('— watching for changes')}\n`);
@@ -825,6 +1065,8 @@ export async function startPairMode(options = {}) {
825
1065
  const full = join(watchPath, file);
826
1066
  return classifyChange(full, file);
827
1067
  });
1068
+ // Track co-change patterns
1069
+ recordCoChange(batch);
828
1070
  // Run analysis pipeline
829
1071
  await analyzeChanges(changes, config, options);
830
1072
  }, DEBOUNCE_MS);
@@ -862,9 +1104,20 @@ export function stopPairMode() {
862
1104
  process.stderr.write(` ${DIM('Files analyzed:')} ${sessionStats.filesAnalyzed}\n`);
863
1105
  process.stderr.write(` ${DIM('Suggestions:')} ${sessionStats.suggestionsShown}\n`);
864
1106
  process.stderr.write(` ${DIM('Errors found:')} ${sessionStats.errorsFound}\n`);
1107
+ if (sessionStats.testsRun > 0) {
1108
+ process.stderr.write(` ${DIM('Tests run:')} ${sessionStats.testsRun}${sessionStats.testsFailed > 0 ? ` (${RED(String(sessionStats.testsFailed) + ' failed')})` : ` (${GREEN('all passed')})`}\n`);
1109
+ }
865
1110
  if (sessionStats.fixesApplied > 0) {
866
1111
  process.stderr.write(` ${DIM('Auto-fixed:')} ${GREEN(String(sessionStats.fixesApplied))}\n`);
867
1112
  }
1113
+ // Print co-change patterns if any were detected
1114
+ const patterns = getCoChangePatterns();
1115
+ if (patterns.length > 0) {
1116
+ process.stderr.write(`\n ${DIM('Co-change patterns detected:')}\n`);
1117
+ for (const p of patterns.slice(0, 5)) {
1118
+ process.stderr.write(` ${CYAN(p.pair[0])} ${DIM('<->')} ${CYAN(p.pair[1])} ${DIM(`(${p.count}x)`)}\n`);
1119
+ }
1120
+ }
868
1121
  process.stderr.write('\n');
869
1122
  }
870
1123
  }
@@ -927,6 +1180,26 @@ export async function analyzeWithAgent(filePath, agentOptions) {
927
1180
  }
928
1181
  }
929
1182
  // ---------------------------------------------------------------------------
1183
+ // runPair() — convenience entry point
1184
+ // ---------------------------------------------------------------------------
1185
+ /**
1186
+ * runPair() — Start AI pair programming watch mode.
1187
+ *
1188
+ * 1. Watches cwd (or specified path) for file changes using fs.watch
1189
+ * 2. When .ts/.js/.py/.rs/.go files change:
1190
+ * - Runs the relevant test if one exists (detects vitest/jest/pytest/cargo/go)
1191
+ * - If test fails, shows the failure with fix suggestions
1192
+ * - Tracks which files change together (co-change pattern extraction)
1193
+ * 3. Simple terminal output: file changed, action taken, result
1194
+ * 4. Runs until Ctrl+C
1195
+ */
1196
+ export async function runPair(path, options) {
1197
+ return startPairMode({
1198
+ path: path || process.cwd(),
1199
+ ...options,
1200
+ });
1201
+ }
1202
+ // ---------------------------------------------------------------------------
930
1203
  // CLI registration
931
1204
  // ---------------------------------------------------------------------------
932
1205
  /**