skimpyclaw 0.3.8 → 0.3.9

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 @@
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
+ });
@@ -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").
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
- let weatherLocation = 'New York, NY';
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: addWeatherCron,
1103
+ cronWeather: false,
1112
1104
  timezone: cronTimezone,
1113
- weatherLocation,
1105
+ weatherLocation: '',
1114
1106
  skillDailyNotes: addDailyNotesSkill,
1115
- skillWeather: addWeatherSkill,
1116
- skillWebSearch: addWebSearchSkill,
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skimpyclaw",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "Lightweight personal AI assistant with Telegram and Discord integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -16,6 +16,25 @@
16
16
  "README.md",
17
17
  "LICENSE"
18
18
  ],
19
+ "scripts": {
20
+ "cli": "tsx src/cli.ts",
21
+ "start": "tsx src/index.ts",
22
+ "dev": "tsx watch src/index.ts",
23
+ "dashboard:dev": "pnpm --dir web/dashboard dev",
24
+ "dashboard:build": "pnpm --dir web/dashboard install --frozen-lockfile && pnpm --dir web/dashboard build",
25
+ "docs:dev": "pnpm --dir docs dev",
26
+ "docs:build": "pnpm --dir docs install --frozen-lockfile && pnpm --dir docs build",
27
+ "docs:preview": "pnpm --dir docs preview",
28
+ "setup": "tsx src/setup.ts",
29
+ "onboard": "tsx src/cli.ts onboard",
30
+ "build": "tsc && pnpm dashboard:build",
31
+ "release:check": "pnpm build && pnpm test",
32
+ "release:local": "bash ./scripts/release.sh",
33
+ "lint": "eslint \"src/**/*.ts\"",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run",
36
+ "ci": "pnpm run lint && pnpm run typecheck && pnpm run test"
37
+ },
19
38
  "dependencies": {
20
39
  "@anthropic-ai/sdk": "^0.52.0",
21
40
  "@grammyjs/runner": "^2.0.3",
@@ -37,6 +56,11 @@
37
56
  "openai": "^4.47.0",
38
57
  "playwright": "^1.49.0"
39
58
  },
59
+ "pnpm": {
60
+ "onlyBuiltDependencies": [
61
+ "esbuild"
62
+ ]
63
+ },
40
64
  "devDependencies": {
41
65
  "@eslint/js": "^9.39.2",
42
66
  "@types/node": "^20.11.0",
@@ -46,24 +70,5 @@
46
70
  "typescript": "^5.4.0",
47
71
  "typescript-eslint": "^8.54.0",
48
72
  "vitest": "^4.0.18"
49
- },
50
- "scripts": {
51
- "cli": "tsx src/cli.ts",
52
- "start": "tsx src/index.ts",
53
- "dev": "tsx watch src/index.ts",
54
- "dashboard:dev": "pnpm --dir web/dashboard dev",
55
- "dashboard:build": "pnpm --dir web/dashboard install --frozen-lockfile && pnpm --dir web/dashboard build",
56
- "docs:dev": "pnpm --dir docs dev",
57
- "docs:build": "pnpm --dir docs install --frozen-lockfile && pnpm --dir docs build",
58
- "docs:preview": "pnpm --dir docs preview",
59
- "setup": "tsx src/setup.ts",
60
- "onboard": "tsx src/cli.ts onboard",
61
- "build": "tsc && pnpm dashboard:build",
62
- "release:check": "pnpm build && pnpm test",
63
- "release:local": "bash ./scripts/release.sh",
64
- "lint": "eslint \"src/**/*.ts\"",
65
- "typecheck": "tsc --noEmit",
66
- "test": "vitest run",
67
- "ci": "pnpm run lint && pnpm run typecheck && pnpm run test"
68
73
  }
69
- }
74
+ }