skimpyclaw 0.3.8 → 0.3.10
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/dist/__tests__/code-agents-preflight.test.d.ts +1 -0
- package/dist/__tests__/code-agents-preflight.test.js +88 -0
- package/dist/__tests__/code-agents-utils.test.js +12 -1
- package/dist/__tests__/skills.test.js +53 -26
- package/dist/__tests__/token-efficiency.test.js +37 -15
- package/dist/agent.js +1 -1
- package/dist/code-agents/index.js +7 -1
- package/dist/code-agents/orchestrator.js +21 -0
- package/dist/code-agents/utils.d.ts +4 -0
- package/dist/code-agents/utils.js +26 -0
- package/dist/providers/utils.d.ts +6 -2
- package/dist/providers/utils.js +35 -3
- package/dist/service.js +25 -0
- package/dist/setup.js +10 -13
- package/dist/skills-types.d.ts +6 -0
- package/dist/skills.d.ts +5 -1
- package/dist/skills.js +25 -2
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const PRECHECK_ERROR = 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`), Claude Code CLI (`claude` or `claude-code`), or Kimi CLI (`kimi`).';
|
|
3
|
+
const toolConfig = {
|
|
4
|
+
enabled: true,
|
|
5
|
+
allowedPaths: [process.cwd()],
|
|
6
|
+
maxIterations: 5,
|
|
7
|
+
bashTimeout: 5000,
|
|
8
|
+
};
|
|
9
|
+
async function loadSubject(preflightError) {
|
|
10
|
+
vi.resetModules();
|
|
11
|
+
const runCodeAgentBackground = vi.fn().mockResolvedValue(undefined);
|
|
12
|
+
const runTeamOrchestrator = vi.fn().mockResolvedValue(undefined);
|
|
13
|
+
vi.doMock('../code-agents/utils.js', async () => {
|
|
14
|
+
const actual = await vi.importActual('../code-agents/utils.js');
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
getCodingCliPreflightError: () => preflightError,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
vi.doMock('../code-agents/executor.js', () => ({
|
|
21
|
+
runCodeAgentBackground,
|
|
22
|
+
runValidation: vi.fn(),
|
|
23
|
+
buildValidationCommand: vi.fn(() => 'pnpm build && pnpm test'),
|
|
24
|
+
}));
|
|
25
|
+
vi.doMock('../code-agents/orchestrator.js', () => ({
|
|
26
|
+
runTeamOrchestrator,
|
|
27
|
+
computeWaves: vi.fn(),
|
|
28
|
+
decomposeTask: vi.fn(),
|
|
29
|
+
synthesizeResults: vi.fn(),
|
|
30
|
+
gatherCodebaseContext: vi.fn(),
|
|
31
|
+
}));
|
|
32
|
+
vi.doMock('../code-agents/registry.js', () => ({
|
|
33
|
+
getNextCodeAgentId: vi.fn(() => 'ca_test_1'),
|
|
34
|
+
storeCodeAgentTask: vi.fn(),
|
|
35
|
+
writeCodeAgentTask: vi.fn(),
|
|
36
|
+
getActiveCodeAgents: vi.fn(() => []),
|
|
37
|
+
getRecentCodeAgents: vi.fn(() => []),
|
|
38
|
+
getAllCodeAgents: vi.fn(() => []),
|
|
39
|
+
getCodeAgent: vi.fn(() => null),
|
|
40
|
+
cancelCodeAgent: vi.fn(),
|
|
41
|
+
restoreCodeAgentTasks: vi.fn(),
|
|
42
|
+
getCodeAgentsDir: vi.fn(() => process.cwd()),
|
|
43
|
+
}));
|
|
44
|
+
const subject = await import('../code-agents/index.js');
|
|
45
|
+
return { ...subject, runCodeAgentBackground, runTeamOrchestrator };
|
|
46
|
+
}
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.doUnmock('../code-agents/utils.js');
|
|
49
|
+
vi.doUnmock('../code-agents/executor.js');
|
|
50
|
+
vi.doUnmock('../code-agents/orchestrator.js');
|
|
51
|
+
vi.doUnmock('../code-agents/registry.js');
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
vi.resetModules();
|
|
54
|
+
});
|
|
55
|
+
describe('coding CLI preflight guard', () => {
|
|
56
|
+
it('fails code_with_agent before spawning when no supported CLI is available', async () => {
|
|
57
|
+
const { executeCodeWithAgent, runCodeAgentBackground } = await loadSubject(PRECHECK_ERROR);
|
|
58
|
+
const result = await executeCodeWithAgent({ task: 'Fix bug', workdir: process.cwd() }, toolConfig, {
|
|
59
|
+
fullConfig: { codeAgents: { maxConcurrent: 99 } },
|
|
60
|
+
});
|
|
61
|
+
expect(result).toBe(PRECHECK_ERROR);
|
|
62
|
+
expect(runCodeAgentBackground).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
it('fails code_with_team before spawning when no supported CLI is available', async () => {
|
|
65
|
+
const { executeCodeWithTeam, runTeamOrchestrator } = await loadSubject(PRECHECK_ERROR);
|
|
66
|
+
const result = await executeCodeWithTeam({ task: 'Refactor auth', workdir: process.cwd() }, toolConfig, {
|
|
67
|
+
fullConfig: { codeAgents: { maxConcurrent: 99 } },
|
|
68
|
+
});
|
|
69
|
+
expect(result).toBe(PRECHECK_ERROR);
|
|
70
|
+
expect(runTeamOrchestrator).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
it('allows code_with_agent when at least one supported CLI exists', async () => {
|
|
73
|
+
const { executeCodeWithAgent, runCodeAgentBackground } = await loadSubject(null);
|
|
74
|
+
const result = await executeCodeWithAgent({ task: 'Fix bug', workdir: process.cwd() }, toolConfig, {
|
|
75
|
+
fullConfig: { codeAgents: { maxConcurrent: 99 } },
|
|
76
|
+
});
|
|
77
|
+
expect(result).toContain('Started coding agent');
|
|
78
|
+
expect(runCodeAgentBackground).toHaveBeenCalledTimes(1);
|
|
79
|
+
});
|
|
80
|
+
it('allows code_with_team when at least one supported CLI exists', async () => {
|
|
81
|
+
const { executeCodeWithTeam, runTeamOrchestrator } = await loadSubject(null);
|
|
82
|
+
const result = await executeCodeWithTeam({ task: 'Refactor auth', workdir: process.cwd() }, toolConfig, {
|
|
83
|
+
fullConfig: { codeAgents: { maxConcurrent: 99 } },
|
|
84
|
+
});
|
|
85
|
+
expect(result).toContain('Started coding team');
|
|
86
|
+
expect(runTeamOrchestrator).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { normalizeCodeAgent, resolveSelectedCodeAgent } from '../code-agents/utils.js';
|
|
2
|
+
import { normalizeCodeAgent, resolveSelectedCodeAgent, getAvailableCodingCliTools, getCodingCliPreflightError, } from '../code-agents/utils.js';
|
|
3
3
|
describe('normalizeCodeAgent', () => {
|
|
4
4
|
it('accepts strict ids', () => {
|
|
5
5
|
expect(normalizeCodeAgent('claude')).toBe('claude');
|
|
@@ -39,3 +39,14 @@ describe('resolveSelectedCodeAgent', () => {
|
|
|
39
39
|
expect(resolveSelectedCodeAgent('claude', 'claude', 'gpt-4.1')).toBe('claude');
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
|
+
describe('coding CLI preflight', () => {
|
|
43
|
+
it('returns a clear error when no supported CLI is found on PATH', () => {
|
|
44
|
+
expect(getAvailableCodingCliTools(() => false)).toEqual([]);
|
|
45
|
+
expect(getCodingCliPreflightError(() => false)).toBe('Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`), Claude Code CLI (`claude` or `claude-code`), or Kimi CLI (`kimi`).');
|
|
46
|
+
});
|
|
47
|
+
it('accepts claude-code binary as claude support', () => {
|
|
48
|
+
const hasCommand = (name) => name === 'claude-code';
|
|
49
|
+
expect(getAvailableCodingCliTools(hasCommand)).toEqual(['claude']);
|
|
50
|
+
expect(getCodingCliPreflightError(hasCommand)).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -294,31 +294,58 @@ describe('formatSkillsPrompt', () => {
|
|
|
294
294
|
it('returns empty string for no skills', () => {
|
|
295
295
|
expect(formatSkillsPrompt([])).toBe('');
|
|
296
296
|
});
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
makeSkill('
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
297
|
+
describe('dynamic loading (default)', () => {
|
|
298
|
+
it('formats skill catalog with names, descriptions, and paths', () => {
|
|
299
|
+
const result = formatSkillsPrompt([makeSkill('greet', 'Say hello.')]);
|
|
300
|
+
expect(result).toContain('## Skills');
|
|
301
|
+
expect(result).toContain('read the SKILL.md file');
|
|
302
|
+
expect(result).toContain('greet');
|
|
303
|
+
expect(result).toContain('`/fake/greet/SKILL.md`');
|
|
304
|
+
});
|
|
305
|
+
it('includes emoji in catalog', () => {
|
|
306
|
+
const result = formatSkillsPrompt([makeSkill('greet', 'Say hello.', '👋')]);
|
|
307
|
+
expect(result).toContain('👋 greet');
|
|
308
|
+
});
|
|
309
|
+
it('does not include skill body', () => {
|
|
310
|
+
const result = formatSkillsPrompt([makeSkill('greet', 'Say hello.')]);
|
|
311
|
+
expect(result).not.toContain('Say hello.');
|
|
312
|
+
});
|
|
313
|
+
it('lists multiple skills', () => {
|
|
314
|
+
const result = formatSkillsPrompt([
|
|
315
|
+
makeSkill('a', 'A body'),
|
|
316
|
+
makeSkill('b', 'B body'),
|
|
317
|
+
]);
|
|
318
|
+
expect(result).toContain('| a |');
|
|
319
|
+
expect(result).toContain('| b |');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
describe('inline loading (dynamicLoading=false)', () => {
|
|
323
|
+
it('formats single skill with header and body', () => {
|
|
324
|
+
const result = formatSkillsPrompt([makeSkill('greet', 'Say hello.')], undefined, false);
|
|
325
|
+
expect(result).toContain('## Active Skills');
|
|
326
|
+
expect(result).toContain('### greet');
|
|
327
|
+
expect(result).toContain('Say hello.');
|
|
328
|
+
});
|
|
329
|
+
it('includes emoji in header when present', () => {
|
|
330
|
+
const result = formatSkillsPrompt([makeSkill('greet', 'Say hello.', '👋')], undefined, false);
|
|
331
|
+
expect(result).toContain('### 👋 greet');
|
|
332
|
+
});
|
|
333
|
+
it('separates multiple skills with dividers', () => {
|
|
334
|
+
const result = formatSkillsPrompt([
|
|
335
|
+
makeSkill('a', 'A body'),
|
|
336
|
+
makeSkill('b', 'B body'),
|
|
337
|
+
], undefined, false);
|
|
338
|
+
expect(result).toContain('---');
|
|
339
|
+
expect(result).toContain('### a');
|
|
340
|
+
expect(result).toContain('### b');
|
|
341
|
+
});
|
|
342
|
+
it('does not skip skills based on prompt budget argument', () => {
|
|
343
|
+
const result = formatSkillsPrompt([
|
|
344
|
+
makeSkill('small', 'tiny'),
|
|
345
|
+
makeSkill('big', 'x'.repeat(50000)),
|
|
346
|
+
], 100, false);
|
|
347
|
+
expect(result).toContain('### small');
|
|
348
|
+
expect(result).toContain('### big');
|
|
349
|
+
});
|
|
323
350
|
});
|
|
324
351
|
});
|
|
@@ -1,27 +1,49 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, afterAll } from 'vitest';
|
|
2
2
|
import { truncateToolResult } from '../providers/utils.js';
|
|
3
|
+
import { existsSync, readdirSync, unlinkSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
const scratchDir = join(homedir(), '.skimpyclaw', 'scratch');
|
|
7
|
+
// Clean up scratch files created during tests
|
|
8
|
+
afterAll(() => {
|
|
9
|
+
try {
|
|
10
|
+
if (existsSync(scratchDir)) {
|
|
11
|
+
for (const f of readdirSync(scratchDir)) {
|
|
12
|
+
try {
|
|
13
|
+
unlinkSync(join(scratchDir, f));
|
|
14
|
+
}
|
|
15
|
+
catch { /* ignore */ }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { /* ignore */ }
|
|
20
|
+
});
|
|
3
21
|
describe('token efficiency', () => {
|
|
4
22
|
describe('truncateToolResult', () => {
|
|
5
23
|
it('returns short results unchanged', () => {
|
|
6
24
|
const result = 'short result';
|
|
7
25
|
expect(truncateToolResult(result)).toBe(result);
|
|
8
26
|
});
|
|
9
|
-
it('
|
|
10
|
-
const result = 'x'.repeat(
|
|
11
|
-
expect(truncateToolResult(result)).toBe(result);
|
|
27
|
+
it('returns results under mask threshold unchanged', () => {
|
|
28
|
+
const result = 'x'.repeat(7_999);
|
|
29
|
+
expect(truncateToolResult(result)).toBe(result);
|
|
12
30
|
});
|
|
13
|
-
it('
|
|
14
|
-
const result = 'x'.repeat(
|
|
15
|
-
const
|
|
16
|
-
expect(
|
|
17
|
-
expect(
|
|
18
|
-
expect(
|
|
31
|
+
it('masks large results to scratch file with summary', () => {
|
|
32
|
+
const result = 'START' + 'x'.repeat(10_000) + 'END';
|
|
33
|
+
const masked = truncateToolResult(result);
|
|
34
|
+
expect(masked.length).toBeLessThan(result.length);
|
|
35
|
+
expect(masked).toContain('[Full output');
|
|
36
|
+
expect(masked).toContain('saved to');
|
|
37
|
+
expect(masked).toContain('.skimpyclaw/scratch/');
|
|
38
|
+
expect(masked).toContain('use Read tool to access');
|
|
39
|
+
// Summary includes head and tail
|
|
40
|
+
expect(masked).toContain('START');
|
|
41
|
+
expect(masked).toContain('END');
|
|
19
42
|
});
|
|
20
|
-
it('
|
|
21
|
-
const result = '
|
|
22
|
-
const
|
|
23
|
-
expect(
|
|
24
|
-
expect(truncated.startsWith('abcde')).toBe(true);
|
|
43
|
+
it('includes char count in masked output', () => {
|
|
44
|
+
const result = 'y'.repeat(20_000);
|
|
45
|
+
const masked = truncateToolResult(result);
|
|
46
|
+
expect(masked).toContain('20000 chars');
|
|
25
47
|
});
|
|
26
48
|
});
|
|
27
49
|
describe('retry prompt compression', () => {
|
package/dist/agent.js
CHANGED
|
@@ -52,7 +52,7 @@ export function buildSystemPrompt(agentId, skillsContext) {
|
|
|
52
52
|
cronJobId: skillsContext?.cronJobId,
|
|
53
53
|
tags: skillsContext?.tags,
|
|
54
54
|
});
|
|
55
|
-
skillsSection = formatSkillsPrompt(contextSkills, skillsContext?.skillConfig?.maxPromptTokens);
|
|
55
|
+
skillsSection = formatSkillsPrompt(contextSkills, skillsContext?.skillConfig?.maxPromptTokens, skillsContext?.skillConfig?.dynamicLoading);
|
|
56
56
|
}
|
|
57
57
|
const base = [soul, identity, tools, skillsSection].filter(Boolean).join('\n\n---\n\n');
|
|
58
58
|
const userContext = [user, memory].filter(Boolean).join('\n\n');
|
|
@@ -5,7 +5,7 @@ import { isPathAllowed } from '../tools/path-utils.js';
|
|
|
5
5
|
import { getNextCodeAgentId, storeCodeAgentTask, writeCodeAgentTask, getActiveCodeAgents, getRecentCodeAgents, getCodeAgent, } from './registry.js';
|
|
6
6
|
import { runCodeAgentBackground } from './executor.js';
|
|
7
7
|
import { runTeamOrchestrator } from './orchestrator.js';
|
|
8
|
-
import { resolveSelectedCodeAgent, resolveWorkdir, resolveModelAlias, } from './utils.js';
|
|
8
|
+
import { resolveSelectedCodeAgent, resolveWorkdir, resolveModelAlias, getCodingCliPreflightError, } from './utils.js';
|
|
9
9
|
// Re-export timeout constants
|
|
10
10
|
export { CODE_AGENT_TIMEOUT_MS, VALIDATE_TIMEOUT_MS } from './types.js';
|
|
11
11
|
// Re-export registry functions
|
|
@@ -105,6 +105,9 @@ export async function executeCodeWithAgent(input, config, context) {
|
|
|
105
105
|
if (activeCount >= maxConcurrent) {
|
|
106
106
|
return `Error: Concurrency limit reached (${activeCount}/${maxConcurrent} coding agents running). Wait for one to finish or increase codeAgents.maxConcurrent.`;
|
|
107
107
|
}
|
|
108
|
+
const cliPreflightError = getCodingCliPreflightError();
|
|
109
|
+
if (cliPreflightError)
|
|
110
|
+
return cliPreflightError;
|
|
108
111
|
const validate = input.validate !== false; // default true
|
|
109
112
|
// Create task with unique ID
|
|
110
113
|
const id = getNextCodeAgentId();
|
|
@@ -176,6 +179,9 @@ export async function executeCodeWithTeam(input, config, context) {
|
|
|
176
179
|
if (activeCount + teamSize > maxConcurrent) {
|
|
177
180
|
return `Error: Concurrency limit — need ${teamSize} slots but only ${maxConcurrent - activeCount} available (${activeCount}/${maxConcurrent} running). Wait for agents to finish.`;
|
|
178
181
|
}
|
|
182
|
+
const cliPreflightError = getCodingCliPreflightError();
|
|
183
|
+
if (cliPreflightError)
|
|
184
|
+
return cliPreflightError;
|
|
179
185
|
// Create parent task
|
|
180
186
|
const id = getNextCodeAgentId();
|
|
181
187
|
const startedAt = new Date();
|
|
@@ -567,6 +567,27 @@ export async function runTeamOrchestrator(parentId, task, teamSize, workdir, val
|
|
|
567
567
|
if (getCodeAgent(parentId)?.status === 'cancelled')
|
|
568
568
|
throw new Error(CANCELLED_MESSAGE);
|
|
569
569
|
parentTask.liveOutput = 'Phase: Synthesizing results...';
|
|
570
|
+
// Aggregate cost/tokens from all children into parent
|
|
571
|
+
let totalCost = 0;
|
|
572
|
+
let totalInput = 0;
|
|
573
|
+
let totalOutput = 0;
|
|
574
|
+
let hasCostData = false;
|
|
575
|
+
for (const cid of childIds) {
|
|
576
|
+
const child = getCodeAgent(cid);
|
|
577
|
+
if (child?.totalCost != null) {
|
|
578
|
+
totalCost += child.totalCost;
|
|
579
|
+
hasCostData = true;
|
|
580
|
+
}
|
|
581
|
+
if (child?.inputTokens != null)
|
|
582
|
+
totalInput += child.inputTokens;
|
|
583
|
+
if (child?.outputTokens != null)
|
|
584
|
+
totalOutput += child.outputTokens;
|
|
585
|
+
}
|
|
586
|
+
if (hasCostData) {
|
|
587
|
+
parentTask.totalCost = totalCost;
|
|
588
|
+
parentTask.inputTokens = totalInput;
|
|
589
|
+
parentTask.outputTokens = totalOutput;
|
|
590
|
+
}
|
|
570
591
|
writeCodeAgentTask(parentTask);
|
|
571
592
|
const childResults = childIds.map(id => {
|
|
572
593
|
const child = getCodeAgent(id);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { BuildCodeAgentArgsInput, CodeAgentTask } from './types.js';
|
|
2
2
|
import type { Config } from '../types.js';
|
|
3
|
+
/** Return supported coding CLIs currently available on PATH. */
|
|
4
|
+
export declare function getAvailableCodingCliTools(commandChecker?: (name: string) => boolean): Array<'codex' | 'claude' | 'kimi'>;
|
|
5
|
+
/** Return preflight error when no supported coding CLI is installed. */
|
|
6
|
+
export declare function getCodingCliPreflightError(commandChecker?: (name: string) => boolean): string | null;
|
|
3
7
|
/**
|
|
4
8
|
* Normalize legacy/default agent values to supported CLI agent IDs.
|
|
5
9
|
* Accepts strict IDs and older alias-like values (e.g. "claude-think").
|
|
@@ -12,9 +12,35 @@ function resolveCliPath(name) {
|
|
|
12
12
|
return name;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
function isCommandAvailable(name) {
|
|
16
|
+
try {
|
|
17
|
+
execSync(`command -v ${name}`, { stdio: 'ignore' });
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
15
24
|
const CLAUDE_CLI_PATH = resolveCliPath('claude');
|
|
16
25
|
const CODEX_CLI_PATH = resolveCliPath('codex');
|
|
17
26
|
const KIMI_CLI_PATH = resolveCliPath('kimi');
|
|
27
|
+
/** Return supported coding CLIs currently available on PATH. */
|
|
28
|
+
export function getAvailableCodingCliTools(commandChecker = isCommandAvailable) {
|
|
29
|
+
const available = [];
|
|
30
|
+
if (commandChecker('codex'))
|
|
31
|
+
available.push('codex');
|
|
32
|
+
if (commandChecker('claude') || commandChecker('claude-code'))
|
|
33
|
+
available.push('claude');
|
|
34
|
+
if (commandChecker('kimi'))
|
|
35
|
+
available.push('kimi');
|
|
36
|
+
return available;
|
|
37
|
+
}
|
|
38
|
+
/** Return preflight error when no supported coding CLI is installed. */
|
|
39
|
+
export function getCodingCliPreflightError(commandChecker = isCommandAvailable) {
|
|
40
|
+
if (getAvailableCodingCliTools(commandChecker).length > 0)
|
|
41
|
+
return null;
|
|
42
|
+
return 'Error: No supported coding CLI found on PATH. Install Codex CLI (`codex`), Claude Code CLI (`claude` or `claude-code`), or Kimi CLI (`kimi`).';
|
|
43
|
+
}
|
|
18
44
|
/**
|
|
19
45
|
* Normalize legacy/default agent values to supported CLI agent IDs.
|
|
20
46
|
* Accepts strict IDs and older alias-like values (e.g. "claude-think").
|
|
@@ -54,8 +54,12 @@ export declare function getProvider(model: string): string;
|
|
|
54
54
|
* Strip provider prefix from model name.
|
|
55
55
|
*/
|
|
56
56
|
export declare function stripProvider(model: string, openaiClients?: Map<string, unknown>, responsesApiProviders?: Set<string>): string;
|
|
57
|
-
/**
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Mask large tool outputs by writing to scratch files.
|
|
59
|
+
* Returns the original result if small enough, or a summary + file path if large.
|
|
60
|
+
* Falls back to simple truncation if file write fails.
|
|
61
|
+
*/
|
|
62
|
+
export declare function truncateToolResult(result: string, _maxBytes?: number): string;
|
|
59
63
|
/**
|
|
60
64
|
* Build thinking config based on thinking level.
|
|
61
65
|
*/
|
package/dist/providers/utils.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// Provider Utilities
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
2
5
|
// Anti-hallucination instructions injected between the Claude Code identity
|
|
3
6
|
// block and the actual system prompt. Prevents the model from roleplaying
|
|
4
7
|
// Claude Code's full behavior (XML tool calls, fabricated output, etc.)
|
|
@@ -178,10 +181,39 @@ export function stripProvider(model, openaiClients, responsesApiProviders) {
|
|
|
178
181
|
return model;
|
|
179
182
|
}
|
|
180
183
|
/** Truncate tool result to maxBytes. Appends truncation notice. */
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Observation masking threshold. Tool outputs above this size are written to
|
|
186
|
+
* a scratch file and replaced with a compact summary + file path.
|
|
187
|
+
* Outputs below this are returned inline (no file I/O overhead).
|
|
188
|
+
*/
|
|
189
|
+
const MASK_THRESHOLD = 8_000; // ~2000 tokens
|
|
190
|
+
/**
|
|
191
|
+
* Mask large tool outputs by writing to scratch files.
|
|
192
|
+
* Returns the original result if small enough, or a summary + file path if large.
|
|
193
|
+
* Falls back to simple truncation if file write fails.
|
|
194
|
+
*/
|
|
195
|
+
export function truncateToolResult(result, _maxBytes = 10_240) {
|
|
196
|
+
if (result.length <= MASK_THRESHOLD)
|
|
183
197
|
return result;
|
|
184
|
-
|
|
198
|
+
try {
|
|
199
|
+
const scratchDir = join(homedir(), '.skimpyclaw', 'scratch');
|
|
200
|
+
if (!existsSync(scratchDir))
|
|
201
|
+
mkdirSync(scratchDir, { recursive: true });
|
|
202
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
203
|
+
const filePath = join(scratchDir, `${id}.txt`);
|
|
204
|
+
writeFileSync(filePath, result);
|
|
205
|
+
// Build a compact summary: first 500 chars + last 500 chars
|
|
206
|
+
const head = result.slice(0, 500);
|
|
207
|
+
const tail = result.slice(-500);
|
|
208
|
+
const summary = head + (result.length > 1000 ? '\n...\n' + tail : '');
|
|
209
|
+
console.log(`[context-manager] Masked ${result.length} chars → ${filePath}`);
|
|
210
|
+
return `${summary}\n\n[Full output (${result.length} chars) saved to ${filePath} — use Read tool to access]`;
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
// Fallback: simple truncation
|
|
214
|
+
console.warn(`[context-manager] Masking failed: ${err instanceof Error ? err.message : err}`);
|
|
215
|
+
return result.slice(0, MASK_THRESHOLD) + `\n\n[Truncated: ${result.length} chars total]`;
|
|
216
|
+
}
|
|
185
217
|
}
|
|
186
218
|
/**
|
|
187
219
|
* Build thinking config based on thinking level.
|
package/dist/service.js
CHANGED
|
@@ -6,12 +6,37 @@ import { initProviders } from './agent.js';
|
|
|
6
6
|
import { initLangfuse, shutdownLangfuse } from './langfuse.js';
|
|
7
7
|
import { restoreCodeAgentTasks, setCodeAgentConfig } from './tools.js';
|
|
8
8
|
import { releaseAll, cleanupOrphans, setRuntime, probeRuntime } from './sandbox/index.js';
|
|
9
|
+
/** Clean up old scratch files (observation masking). Keeps files < 24h. */
|
|
10
|
+
function cleanupScratch() {
|
|
11
|
+
try {
|
|
12
|
+
const { readdirSync, statSync, unlinkSync } = require('fs');
|
|
13
|
+
const { join } = require('path');
|
|
14
|
+
const { homedir } = require('os');
|
|
15
|
+
const dir = join(homedir(), '.skimpyclaw', 'scratch');
|
|
16
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
17
|
+
let count = 0;
|
|
18
|
+
for (const f of readdirSync(dir)) {
|
|
19
|
+
const p = join(dir, f);
|
|
20
|
+
try {
|
|
21
|
+
if (statSync(p).mtimeMs < cutoff) {
|
|
22
|
+
unlinkSync(p);
|
|
23
|
+
count++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch { /* skip */ }
|
|
27
|
+
}
|
|
28
|
+
if (count > 0)
|
|
29
|
+
console.log(`[scratch] Cleaned up ${count} old file(s)`);
|
|
30
|
+
}
|
|
31
|
+
catch { /* dir doesn't exist yet, fine */ }
|
|
32
|
+
}
|
|
9
33
|
export async function startRuntime(config) {
|
|
10
34
|
const smokeTest = process.env.SKIMPYCLAW_SMOKE_TEST === '1';
|
|
11
35
|
initLangfuse(config);
|
|
12
36
|
initProviders(config);
|
|
13
37
|
restoreCodeAgentTasks();
|
|
14
38
|
setCodeAgentConfig(config);
|
|
39
|
+
cleanupScratch();
|
|
15
40
|
// Initialize sandbox runtime if configured — auto-disable if no runtime available
|
|
16
41
|
if (config.sandbox?.enabled) {
|
|
17
42
|
const detected = probeRuntime(config.sandbox.runtime);
|
package/dist/setup.js
CHANGED
|
@@ -1085,10 +1085,8 @@ export async function runSetup(options = {}) {
|
|
|
1085
1085
|
sectionHeader('Starter Packs (optional)');
|
|
1086
1086
|
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
1087
1087
|
const addTechNewsCron = /^y(es)?$/i.test(await ask(rl, ' Add starter cron: top 10 Hacker News daily? [y/N]: '));
|
|
1088
|
-
const addWeatherCron = /^y(es)?$/i.test(await ask(rl, ' Add starter cron: weather check daily at 7:00am? [y/N]: '));
|
|
1089
1088
|
let cronTimezone = localTz;
|
|
1090
|
-
|
|
1091
|
-
if (addTechNewsCron || addWeatherCron) {
|
|
1089
|
+
if (addTechNewsCron) {
|
|
1092
1090
|
const tzInput = await ask(rl, ` Timezone for starter cron jobs [${localTz}]: `);
|
|
1093
1091
|
cronTimezone = tzInput || localTz;
|
|
1094
1092
|
try {
|
|
@@ -1099,21 +1097,15 @@ export async function runSetup(options = {}) {
|
|
|
1099
1097
|
cronTimezone = localTz;
|
|
1100
1098
|
}
|
|
1101
1099
|
}
|
|
1102
|
-
if (addWeatherCron) {
|
|
1103
|
-
const locationInput = await ask(rl, ' Weather location (city, state/country) [New York, NY]: ');
|
|
1104
|
-
weatherLocation = locationInput || 'New York, NY';
|
|
1105
|
-
}
|
|
1106
1100
|
const addDailyNotesSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: daily-notes? [y/N]: '));
|
|
1107
|
-
const addWeatherSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: weather? [y/N]: '));
|
|
1108
|
-
const addWebSearchSkill = /^y(es)?$/i.test(await ask(rl, ' Add starter skill: web-search (uses Browser tool)? [y/N]: '));
|
|
1109
1101
|
const starters = {
|
|
1110
1102
|
cronTechNews: addTechNewsCron,
|
|
1111
|
-
cronWeather:
|
|
1103
|
+
cronWeather: false,
|
|
1112
1104
|
timezone: cronTimezone,
|
|
1113
|
-
weatherLocation,
|
|
1105
|
+
weatherLocation: '',
|
|
1114
1106
|
skillDailyNotes: addDailyNotesSkill,
|
|
1115
|
-
skillWeather:
|
|
1116
|
-
skillWebSearch:
|
|
1107
|
+
skillWeather: false,
|
|
1108
|
+
skillWebSearch: false,
|
|
1117
1109
|
};
|
|
1118
1110
|
const { envContent, config: generatedConfig } = buildSetupArtifacts({
|
|
1119
1111
|
workspaceDir: extraAllowedPaths[0] || join(homedir(), '.skimpyclaw'),
|
|
@@ -1320,6 +1312,11 @@ export async function runSetup(options = {}) {
|
|
|
1320
1312
|
console.log(' skimpyclaw status');
|
|
1321
1313
|
console.log(`${step++}. Optional daemon controls: skimpyclaw stop | skimpyclaw restart`);
|
|
1322
1314
|
console.log(`${step++}. Send /help in your ${useDiscord ? 'Discord bot DM/server' : 'Telegram bot'}`);
|
|
1315
|
+
console.log('');
|
|
1316
|
+
console.log(`${c.yellow('Note:')} The /team and /code tools require an external coding CLI on your PATH:`);
|
|
1317
|
+
console.log(' • Claude Code CLI → https://docs.anthropic.com/en/docs/claude-code');
|
|
1318
|
+
console.log(' • Codex CLI → https://github.com/openai/codex');
|
|
1319
|
+
console.log(' Install at least one to use code_with_agent / code_with_team.');
|
|
1323
1320
|
console.log('\n👙🦞 Enjoy!');
|
|
1324
1321
|
}
|
|
1325
1322
|
finally {
|
package/dist/skills-types.d.ts
CHANGED
|
@@ -62,4 +62,10 @@ export interface SkillConfig {
|
|
|
62
62
|
entries?: Record<string, boolean>;
|
|
63
63
|
/** Max approximate tokens for injected skills prompt (default: 4000) */
|
|
64
64
|
maxPromptTokens?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Dynamic loading: only include skill names and descriptions in the system prompt.
|
|
67
|
+
* Full skill content is loaded on-demand via the Read tool.
|
|
68
|
+
* Default: true (progressive disclosure)
|
|
69
|
+
*/
|
|
70
|
+
dynamicLoading?: boolean;
|
|
65
71
|
}
|
package/dist/skills.d.ts
CHANGED
|
@@ -27,5 +27,9 @@ export declare function getSkillsForContext(skills: LoadedSkill[], context?: {
|
|
|
27
27
|
}): LoadedSkill[];
|
|
28
28
|
/**
|
|
29
29
|
* Format eligible, context-filtered skills into a markdown prompt section.
|
|
30
|
+
*
|
|
31
|
+
* When dynamicLoading is true (default), only skill names, descriptions, and
|
|
32
|
+
* file paths are included. The agent loads full content on-demand via the Read tool.
|
|
33
|
+
* When false, full skill bodies are inlined (legacy behavior).
|
|
30
34
|
*/
|
|
31
|
-
export declare function formatSkillsPrompt(skills: LoadedSkill[], _maxTokens?: number): string;
|
|
35
|
+
export declare function formatSkillsPrompt(skills: LoadedSkill[], _maxTokens?: number, dynamicLoading?: boolean): string;
|
package/dist/skills.js
CHANGED
|
@@ -237,12 +237,35 @@ export function getSkillsForContext(skills, context) {
|
|
|
237
237
|
}
|
|
238
238
|
/**
|
|
239
239
|
* Format eligible, context-filtered skills into a markdown prompt section.
|
|
240
|
+
*
|
|
241
|
+
* When dynamicLoading is true (default), only skill names, descriptions, and
|
|
242
|
+
* file paths are included. The agent loads full content on-demand via the Read tool.
|
|
243
|
+
* When false, full skill bodies are inlined (legacy behavior).
|
|
240
244
|
*/
|
|
241
|
-
export function formatSkillsPrompt(skills, _maxTokens) {
|
|
245
|
+
export function formatSkillsPrompt(skills, _maxTokens, dynamicLoading) {
|
|
242
246
|
if (skills.length === 0)
|
|
243
247
|
return '';
|
|
248
|
+
// Default to dynamic loading
|
|
249
|
+
const useDynamic = dynamicLoading !== false;
|
|
250
|
+
if (useDynamic) {
|
|
251
|
+
const lines = [
|
|
252
|
+
'## Skills',
|
|
253
|
+
'',
|
|
254
|
+
'Available skills — read the SKILL.md file with the Read tool when the task matches a skill\'s description.',
|
|
255
|
+
'',
|
|
256
|
+
'| Skill | Description | Path |',
|
|
257
|
+
'|-------|-------------|------|',
|
|
258
|
+
];
|
|
259
|
+
for (const skill of skills) {
|
|
260
|
+
const emoji = skill.frontmatter.emoji ? `${skill.frontmatter.emoji} ` : '';
|
|
261
|
+
const desc = skill.frontmatter.description || '(no description)';
|
|
262
|
+
const path = join(skill.dirPath, 'SKILL.md');
|
|
263
|
+
lines.push(`| ${emoji}${skill.name} | ${desc} | \`${path}\` |`);
|
|
264
|
+
}
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
// Legacy: inline full skill bodies
|
|
244
268
|
const sections = [];
|
|
245
|
-
// Header
|
|
246
269
|
const header = '## Active Skills\n';
|
|
247
270
|
for (const skill of skills) {
|
|
248
271
|
const emoji = skill.frontmatter.emoji ? `${skill.frontmatter.emoji} ` : '';
|