@thispointon/kondi-chat 0.1.2
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/LICENSE +21 -0
- package/README.md +556 -0
- package/bin/kondi-chat +56 -0
- package/bin/kondi-chat.js +72 -0
- package/package.json +55 -0
- package/scripts/demo.tape +49 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/audit/analytics.ts +261 -0
- package/src/audit/ledger.ts +253 -0
- package/src/audit/telemetry.ts +165 -0
- package/src/cli/backend.ts +675 -0
- package/src/cli/commands.ts +419 -0
- package/src/cli/help.ts +182 -0
- package/src/cli/submit-helpers.ts +159 -0
- package/src/cli/submit.ts +539 -0
- package/src/cli/wizard.ts +121 -0
- package/src/context/bootstrap.ts +138 -0
- package/src/context/budget.ts +100 -0
- package/src/context/manager.ts +666 -0
- package/src/context/memory.ts +160 -0
- package/src/context/preflight.ts +176 -0
- package/src/context/project-brain.ts +101 -0
- package/src/context/receipts.ts +108 -0
- package/src/context/skills.ts +154 -0
- package/src/context/symbol-index.ts +240 -0
- package/src/council/profiles.ts +137 -0
- package/src/council/tool.ts +138 -0
- package/src/council-engine/cli/council-artifacts.ts +230 -0
- package/src/council-engine/cli/council-config.ts +178 -0
- package/src/council-engine/cli/council-session-export.ts +116 -0
- package/src/council-engine/cli/kondi.ts +98 -0
- package/src/council-engine/cli/llm-caller.ts +229 -0
- package/src/council-engine/cli/localStorage-shim.ts +119 -0
- package/src/council-engine/cli/node-platform.ts +68 -0
- package/src/council-engine/cli/run-council.ts +481 -0
- package/src/council-engine/cli/run-pipeline.ts +772 -0
- package/src/council-engine/cli/session-export.ts +153 -0
- package/src/council-engine/configs/councils/analysis.json +101 -0
- package/src/council-engine/configs/councils/code-planning.json +86 -0
- package/src/council-engine/configs/councils/coding.json +89 -0
- package/src/council-engine/configs/councils/debate.json +97 -0
- package/src/council-engine/configs/councils/solo-claude.json +34 -0
- package/src/council-engine/configs/councils/solo-gpt.json +34 -0
- package/src/council-engine/council/coding-orchestrator.ts +1205 -0
- package/src/council-engine/council/context-bootstrap.ts +147 -0
- package/src/council-engine/council/context-inspection.ts +42 -0
- package/src/council-engine/council/context-store.ts +763 -0
- package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
- package/src/council-engine/council/factory.ts +164 -0
- package/src/council-engine/council/index.ts +201 -0
- package/src/council-engine/council/ledger-store.ts +438 -0
- package/src/council-engine/council/prompts.ts +1689 -0
- package/src/council-engine/council/storage-cleanup.ts +164 -0
- package/src/council-engine/council/store.ts +1110 -0
- package/src/council-engine/council/synthesis.ts +291 -0
- package/src/council-engine/council/types.ts +845 -0
- package/src/council-engine/council/validation.ts +613 -0
- package/src/council-engine/pipeline/build-detect.ts +73 -0
- package/src/council-engine/pipeline/executor.ts +1048 -0
- package/src/council-engine/pipeline/index.ts +9 -0
- package/src/council-engine/pipeline/install-detect.ts +84 -0
- package/src/council-engine/pipeline/memory-store.ts +182 -0
- package/src/council-engine/pipeline/output-parsers.ts +146 -0
- package/src/council-engine/pipeline/run-output.ts +149 -0
- package/src/council-engine/pipeline/session-import.ts +177 -0
- package/src/council-engine/pipeline/store.ts +753 -0
- package/src/council-engine/pipeline/test-detect.ts +82 -0
- package/src/council-engine/pipeline/types.ts +401 -0
- package/src/council-engine/services/deliberationSummary.ts +114 -0
- package/src/council-engine/tsconfig.json +16 -0
- package/src/council-engine/types/mcp.ts +122 -0
- package/src/council-engine/utils/filterTools.ts +73 -0
- package/src/engine/apply.ts +238 -0
- package/src/engine/checkpoints.ts +237 -0
- package/src/engine/consultants.ts +347 -0
- package/src/engine/diff.ts +171 -0
- package/src/engine/errors.ts +102 -0
- package/src/engine/git-tools.ts +246 -0
- package/src/engine/hooks.ts +181 -0
- package/src/engine/loop-guard.ts +155 -0
- package/src/engine/permissions.ts +293 -0
- package/src/engine/pipeline.ts +376 -0
- package/src/engine/sub-agents.ts +133 -0
- package/src/engine/task-card.ts +185 -0
- package/src/engine/task-router.ts +256 -0
- package/src/engine/task-store.ts +86 -0
- package/src/engine/tools.ts +783 -0
- package/src/engine/verify.ts +111 -0
- package/src/mcp/client.ts +225 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/tool-manager.ts +192 -0
- package/src/mcp/types.ts +61 -0
- package/src/providers/llm-caller.ts +943 -0
- package/src/providers/rate-limiter.ts +238 -0
- package/src/router/NOTES.md +28 -0
- package/src/router/collector.ts +474 -0
- package/src/router/embeddings.ts +286 -0
- package/src/router/index.ts +299 -0
- package/src/router/intent-router.ts +225 -0
- package/src/router/nn-router.ts +205 -0
- package/src/router/profiles.ts +309 -0
- package/src/router/registry.ts +565 -0
- package/src/router/rules.ts +274 -0
- package/src/router/train.py +408 -0
- package/src/session/store.ts +211 -0
- package/src/test-utils/mock-llm.ts +39 -0
- package/src/types.ts +322 -0
- package/src/web/manager.ts +311 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Router — API Only
|
|
3
|
+
*
|
|
4
|
+
* All providers use direct HTTP calls. No CLI binaries, no sandboxes.
|
|
5
|
+
* Requires API keys set as environment variables or in .env file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface CallerResult {
|
|
9
|
+
content: string;
|
|
10
|
+
tokensUsed: number;
|
|
11
|
+
latencyMs: number;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CallLLMOpts {
|
|
16
|
+
provider: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
systemPrompt: string;
|
|
19
|
+
userMessage: string;
|
|
20
|
+
workingDir?: string;
|
|
21
|
+
skipTools?: boolean;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
/** Stable context that can be cached (Anthropic prompt caching) */
|
|
24
|
+
cacheableContext?: string;
|
|
25
|
+
/** Max output tokens (defaults to 16384) */
|
|
26
|
+
maxOutputTokens?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_MODELS: Record<string, string> = {
|
|
30
|
+
'anthropic-api': 'claude-sonnet-4-5-20250929',
|
|
31
|
+
'openai-api': 'gpt-4o',
|
|
32
|
+
'deepseek': 'deepseek-chat',
|
|
33
|
+
'google': 'models/gemini-2.5-flash',
|
|
34
|
+
'xai': 'grok-3',
|
|
35
|
+
'ollama': 'llama3.1',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function getApiKey(provider: string): string | undefined {
|
|
39
|
+
switch (provider) {
|
|
40
|
+
case 'anthropic-api': return process.env.ANTHROPIC_API_KEY;
|
|
41
|
+
case 'openai-api': return process.env.OPENAI_API_KEY;
|
|
42
|
+
case 'deepseek': return process.env.DEEPSEEK_API_KEY;
|
|
43
|
+
case 'xai': return process.env.XAI_API_KEY;
|
|
44
|
+
case 'google': return process.env.GOOGLE_API_KEY;
|
|
45
|
+
default: return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Provider implementations
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
async function callAnthropicAPI(
|
|
54
|
+
apiKey: string,
|
|
55
|
+
model: string,
|
|
56
|
+
systemPrompt: string,
|
|
57
|
+
userMessage: string,
|
|
58
|
+
cacheableContext?: string,
|
|
59
|
+
maxOutputTokens = 16384,
|
|
60
|
+
): Promise<CallerResult> {
|
|
61
|
+
const start = Date.now();
|
|
62
|
+
|
|
63
|
+
// Build system content with optional prompt caching
|
|
64
|
+
const systemContent: Array<{ type: string; text: string; cache_control?: { type: string } }> = [];
|
|
65
|
+
if (cacheableContext) {
|
|
66
|
+
systemContent.push({
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: cacheableContext,
|
|
69
|
+
cache_control: { type: 'ephemeral' },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
systemContent.push({ type: 'text', text: systemPrompt });
|
|
73
|
+
|
|
74
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'anthropic-version': '2023-06-01',
|
|
79
|
+
'anthropic-beta': 'prompt-caching-2024-07-31',
|
|
80
|
+
'x-api-key': apiKey,
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
model,
|
|
84
|
+
max_tokens: maxOutputTokens,
|
|
85
|
+
system: systemContent,
|
|
86
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!resp.ok) {
|
|
91
|
+
const text = await resp.text();
|
|
92
|
+
throw new Error(`Anthropic API ${resp.status}: ${text.substring(0, 500)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const data = await resp.json();
|
|
96
|
+
const content = data.content
|
|
97
|
+
?.filter((b: any) => b.type === 'text')
|
|
98
|
+
.map((b: any) => b.text)
|
|
99
|
+
.join('\n') || '';
|
|
100
|
+
const usage = data.usage || {};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
content,
|
|
104
|
+
tokensUsed: (usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
105
|
+
latencyMs: Date.now() - start,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function callOpenAICompatible(
|
|
110
|
+
baseUrl: string,
|
|
111
|
+
apiKey: string,
|
|
112
|
+
model: string,
|
|
113
|
+
systemPrompt: string,
|
|
114
|
+
userMessage: string,
|
|
115
|
+
maxOutputTokens = 16384,
|
|
116
|
+
): Promise<CallerResult> {
|
|
117
|
+
const start = Date.now();
|
|
118
|
+
|
|
119
|
+
const resp = await fetch(`${baseUrl}/chat/completions`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
model,
|
|
127
|
+
messages: [
|
|
128
|
+
{ role: 'system', content: systemPrompt },
|
|
129
|
+
{ role: 'user', content: userMessage },
|
|
130
|
+
],
|
|
131
|
+
max_tokens: maxOutputTokens,
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!resp.ok) {
|
|
136
|
+
const text = await resp.text();
|
|
137
|
+
throw new Error(`OpenAI API ${resp.status}: ${text.substring(0, 500)}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const data = await resp.json();
|
|
141
|
+
const content = data.choices?.[0]?.message?.content || '';
|
|
142
|
+
const usage = data.usage || {};
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
content,
|
|
146
|
+
tokensUsed: (usage.prompt_tokens || 0) + (usage.completion_tokens || 0),
|
|
147
|
+
latencyMs: Date.now() - start,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function callGeminiAPI(
|
|
152
|
+
apiKey: string,
|
|
153
|
+
model: string,
|
|
154
|
+
systemPrompt: string,
|
|
155
|
+
userMessage: string,
|
|
156
|
+
): Promise<CallerResult> {
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
|
|
159
|
+
const resp = await fetch(
|
|
160
|
+
`https://generativelanguage.googleapis.com/v1beta/${model}:generateContent?key=${apiKey}`,
|
|
161
|
+
{
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
system_instruction: { parts: [{ text: systemPrompt }] },
|
|
166
|
+
contents: [{ role: 'user', parts: [{ text: userMessage }] }],
|
|
167
|
+
generationConfig: { maxOutputTokens: 16384 },
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (!resp.ok) {
|
|
173
|
+
const text = await resp.text();
|
|
174
|
+
throw new Error(`Gemini API ${resp.status}: ${text.substring(0, 500)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const data = await resp.json();
|
|
178
|
+
const content = data.candidates?.[0]?.content?.parts
|
|
179
|
+
?.map((p: any) => p.text)
|
|
180
|
+
.join('\n') || '';
|
|
181
|
+
const usage = data.usageMetadata || {};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
content,
|
|
185
|
+
tokensUsed: (usage.promptTokenCount || 0) + (usage.candidatesTokenCount || 0),
|
|
186
|
+
latencyMs: Date.now() - start,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// Unified router
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
export async function callLLM(opts: CallLLMOpts): Promise<CallerResult> {
|
|
195
|
+
const provider = opts.provider || 'anthropic-api';
|
|
196
|
+
const model = opts.model || DEFAULT_MODELS[provider] || 'claude-sonnet-4-5-20250929';
|
|
197
|
+
|
|
198
|
+
const apiKey = getApiKey(provider);
|
|
199
|
+
if (!apiKey && provider !== 'ollama') {
|
|
200
|
+
const envVar = provider === 'anthropic-api' ? 'ANTHROPIC_API_KEY'
|
|
201
|
+
: provider === 'openai-api' ? 'OPENAI_API_KEY'
|
|
202
|
+
: provider === 'deepseek' ? 'DEEPSEEK_API_KEY'
|
|
203
|
+
: provider === 'xai' ? 'XAI_API_KEY'
|
|
204
|
+
: provider === 'google' ? 'GOOGLE_API_KEY'
|
|
205
|
+
: 'API_KEY';
|
|
206
|
+
throw new Error(`No API key for "${provider}". Set ${envVar} in environment or .env file.`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (provider === 'anthropic-api') {
|
|
210
|
+
return callAnthropicAPI(apiKey!, model, opts.systemPrompt, opts.userMessage, opts.cacheableContext, opts.maxOutputTokens);
|
|
211
|
+
}
|
|
212
|
+
if (provider === 'openai-api') {
|
|
213
|
+
return callOpenAICompatible('https://api.openai.com/v1', apiKey!, model, opts.systemPrompt, opts.userMessage, opts.maxOutputTokens);
|
|
214
|
+
}
|
|
215
|
+
if (provider === 'deepseek') {
|
|
216
|
+
return callOpenAICompatible('https://api.deepseek.com/v1', apiKey!, model, opts.systemPrompt, opts.userMessage, opts.maxOutputTokens);
|
|
217
|
+
}
|
|
218
|
+
if (provider === 'xai') {
|
|
219
|
+
return callOpenAICompatible('https://api.x.ai/v1', apiKey!, model, opts.systemPrompt, opts.userMessage, opts.maxOutputTokens);
|
|
220
|
+
}
|
|
221
|
+
if (provider === 'google') {
|
|
222
|
+
return callGeminiAPI(apiKey!, model, opts.systemPrompt, opts.userMessage);
|
|
223
|
+
}
|
|
224
|
+
if (provider === 'ollama') {
|
|
225
|
+
return callOpenAICompatible('http://localhost:11434/v1', 'ollama', model, opts.systemPrompt, opts.userMessage);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
throw new Error(`Unknown provider "${provider}". Supported: anthropic-api, openai-api, deepseek, xai, google, ollama`);
|
|
229
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed localStorage polyfill for CLI mode.
|
|
3
|
+
* Must be imported BEFORE any store modules that use localStorage.
|
|
4
|
+
*
|
|
5
|
+
* Persists all state to a JSON file so execution data (councils, personas,
|
|
6
|
+
* step artifacts, deliberation logs) survives after the CLI run finishes.
|
|
7
|
+
*
|
|
8
|
+
* Default location: ~/.local/share/kondi/cli-state/localStorage.json
|
|
9
|
+
* Override with KONDI_CLI_STATE_DIR env var.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
|
|
16
|
+
const STATE_DIR = process.env.KONDI_CLI_STATE_DIR
|
|
17
|
+
|| path.join(os.homedir(), '.local', 'share', 'kondi', 'cli-state');
|
|
18
|
+
|
|
19
|
+
const STATE_FILE = path.join(STATE_DIR, 'localStorage.json');
|
|
20
|
+
|
|
21
|
+
function ensureDir() {
|
|
22
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
23
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadFromDisk(): Record<string, string> {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
30
|
+
const raw = fs.readFileSync(STATE_FILE, 'utf-8');
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('[localStorage-shim] Failed to load state file, starting fresh:', err);
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function saveToDisk(data: Record<string, string>) {
|
|
40
|
+
try {
|
|
41
|
+
ensureDir();
|
|
42
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.warn('[localStorage-shim] WARNING: Failed to save state file — data may be lost:', err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class FileBackedStorage implements Storage {
|
|
49
|
+
private data: Record<string, string>;
|
|
50
|
+
private dirty = false;
|
|
51
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
ensureDir();
|
|
55
|
+
this.data = loadFromDisk();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get length(): number {
|
|
59
|
+
return Object.keys(this.data).length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
key(index: number): string | null {
|
|
63
|
+
return Object.keys(this.data)[index] ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getItem(key: string): string | null {
|
|
67
|
+
return this.data[key] ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setItem(key: string, value: string): void {
|
|
71
|
+
this.data[key] = String(value);
|
|
72
|
+
this.scheduleSave();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
removeItem(key: string): void {
|
|
76
|
+
delete this.data[key];
|
|
77
|
+
this.scheduleSave();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
clear(): void {
|
|
81
|
+
this.data = {};
|
|
82
|
+
this.scheduleSave();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Debounced save — writes at most every 500ms to avoid thrashing disk */
|
|
86
|
+
private scheduleSave(): void {
|
|
87
|
+
this.dirty = true;
|
|
88
|
+
if (!this.flushTimer) {
|
|
89
|
+
this.flushTimer = setTimeout(() => {
|
|
90
|
+
this.flush();
|
|
91
|
+
this.flushTimer = null;
|
|
92
|
+
}, 500);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Force an immediate write to disk */
|
|
97
|
+
flush(): void {
|
|
98
|
+
if (this.dirty) {
|
|
99
|
+
saveToDisk(this.data);
|
|
100
|
+
this.dirty = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Get all data (for export/reporting) */
|
|
105
|
+
getAll(): Record<string, string> {
|
|
106
|
+
return { ...this.data };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Install globally before any module reads localStorage
|
|
111
|
+
const storage = new FileBackedStorage();
|
|
112
|
+
(globalThis as any).localStorage = storage;
|
|
113
|
+
|
|
114
|
+
// Flush on process exit so nothing is lost
|
|
115
|
+
process.on('exit', () => storage.flush());
|
|
116
|
+
process.on('SIGINT', () => { storage.flush(); process.exit(130); });
|
|
117
|
+
process.on('SIGTERM', () => { storage.flush(); process.exit(143); });
|
|
118
|
+
|
|
119
|
+
export { storage };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js PlatformAdapter implementation for CLI mode.
|
|
3
|
+
* Uses fs and child_process instead of Tauri invoke.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import type { PlatformAdapter } from '../pipeline/executor';
|
|
10
|
+
|
|
11
|
+
function assertSafePath(filePath: string, workingDir: string): void {
|
|
12
|
+
const resolved = path.resolve(filePath);
|
|
13
|
+
const base = path.resolve(workingDir);
|
|
14
|
+
if (!resolved.startsWith(base + path.sep) && resolved !== base) {
|
|
15
|
+
throw new Error(`Path traversal blocked: ${filePath} escapes working directory ${workingDir}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createNodePlatform(initialWorkingDir: string): PlatformAdapter {
|
|
20
|
+
let workingDir = initialWorkingDir;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
async writeFile(filePath: string, content: string): Promise<void> {
|
|
24
|
+
assertSafePath(filePath, workingDir);
|
|
25
|
+
const dir = path.dirname(filePath);
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async readFile(filePath: string): Promise<string | null> {
|
|
31
|
+
assertSafePath(filePath, workingDir);
|
|
32
|
+
try {
|
|
33
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async runCommand(cmd: string, cwd: string): Promise<{ stdout: string; stderr: string; exit_code: number; success: boolean }> {
|
|
40
|
+
try {
|
|
41
|
+
const stdout = execFileSync('/bin/sh', ['-c', cmd], {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
timeout: 300_000, // 5 minute timeout for test commands
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
return { stdout, stderr: '', exit_code: 0, success: true };
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
return {
|
|
50
|
+
stdout: err.stdout || '',
|
|
51
|
+
stderr: err.stderr || err.message || '',
|
|
52
|
+
exit_code: err.status ?? 1,
|
|
53
|
+
success: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
setWorkingDir(dir: string): void {
|
|
59
|
+
workingDir = dir;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
getWorkingDir(): string {
|
|
63
|
+
return workingDir;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// saveDeliberationOutput is optional in CLI — files are written directly via writeFile
|
|
67
|
+
};
|
|
68
|
+
}
|