skimpyclaw 0.3.14 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it, beforeEach, vi } from 'vitest';
2
+ // Mock fs before importing the module so the module never touches disk during import.
3
+ vi.mock('fs', async () => {
4
+ const actual = await vi.importActual('fs');
5
+ return {
6
+ ...actual,
7
+ existsSync: vi.fn(() => false),
8
+ readFileSync: vi.fn(() => '[]'),
9
+ writeFileSync: vi.fn(),
10
+ mkdirSync: vi.fn(),
11
+ };
12
+ });
13
+ import { addSession, getSession, updateStatus, enqueue, dequeue, markIdle, hasPending, listSessions, _resetForTesting, } from '../code-agents/interactive-sessions.js';
14
+ function makeSession(threadId = 't1', cliAgent = 'claude') {
15
+ return {
16
+ discordThreadId: threadId,
17
+ cliSessionId: 'uuid-' + threadId,
18
+ cliAgent,
19
+ status: 'active',
20
+ createdAt: new Date().toISOString(),
21
+ lastActivityAt: new Date().toISOString(),
22
+ initialTask: 'test task',
23
+ };
24
+ }
25
+ describe('interactive-sessions state store', () => {
26
+ beforeEach(() => {
27
+ _resetForTesting();
28
+ });
29
+ it('stores and retrieves a session', () => {
30
+ const s = makeSession();
31
+ addSession(s);
32
+ expect(getSession('t1')).toEqual(s);
33
+ expect(listSessions()).toHaveLength(1);
34
+ });
35
+ it('updates status and activity', () => {
36
+ addSession(makeSession());
37
+ updateStatus('t1', 'errored');
38
+ const s = getSession('t1');
39
+ expect(s?.status).toBe('errored');
40
+ });
41
+ it('returns undefined for unknown thread', () => {
42
+ expect(getSession('nope')).toBeUndefined();
43
+ });
44
+ it('enqueue returns shouldStart=true for first message', () => {
45
+ addSession(makeSession());
46
+ const r = enqueue('t1', 'hello');
47
+ expect(r.shouldStart).toBe(true);
48
+ });
49
+ it('enqueue returns shouldStart=false while in-flight', () => {
50
+ addSession(makeSession());
51
+ enqueue('t1', 'first'); // marks inFlight
52
+ const r = enqueue('t1', 'second');
53
+ expect(r.shouldStart).toBe(false);
54
+ });
55
+ it('dequeue returns messages FIFO', () => {
56
+ addSession(makeSession());
57
+ enqueue('t1', 'one');
58
+ enqueue('t1', 'two');
59
+ enqueue('t1', 'three');
60
+ // First was taken by the "in flight" starter; caller is responsible for
61
+ // processing it. The remaining two should dequeue in order.
62
+ // Our API returns the first via shouldStart=true — the caller then calls
63
+ // dequeue() to consume subsequent ones.
64
+ const first = dequeue('t1');
65
+ expect(first?.content).toBe('one');
66
+ const second = dequeue('t1');
67
+ expect(second?.content).toBe('two');
68
+ const third = dequeue('t1');
69
+ expect(third?.content).toBe('three');
70
+ expect(dequeue('t1')).toBeUndefined();
71
+ });
72
+ it('markIdle allows shouldStart=true on next enqueue', () => {
73
+ addSession(makeSession());
74
+ enqueue('t1', 'first');
75
+ markIdle('t1');
76
+ // Queue is empty AND not in flight → next enqueue starts
77
+ const r = enqueue('t1', 'next');
78
+ expect(r.shouldStart).toBe(true);
79
+ });
80
+ it('hasPending reflects queue state', () => {
81
+ addSession(makeSession());
82
+ expect(hasPending('t1')).toBe(false);
83
+ enqueue('t1', 'one'); // inFlight=true, queue empty (first message is the "flight")
84
+ expect(hasPending('t1')).toBe(true); // we pushed to queue first
85
+ dequeue('t1');
86
+ expect(hasPending('t1')).toBe(false);
87
+ });
88
+ it('per-thread queues are isolated', () => {
89
+ addSession(makeSession('t1'));
90
+ addSession(makeSession('t2'));
91
+ enqueue('t1', 'to t1');
92
+ enqueue('t2', 'to t2');
93
+ expect(dequeue('t1')?.content).toBe('to t1');
94
+ expect(dequeue('t2')?.content).toBe('to t2');
95
+ });
96
+ });
@@ -1,18 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { calculateUsageCost } from '../langfuse.js';
3
3
  describe('calculateUsageCost', () => {
4
- it('uses updated OpenAI pricing for gpt-4o', () => {
5
- const cost = calculateUsageCost('openai/gpt-4o', 1_000_000, 1_000_000);
6
- expect(cost.inputCost).toBe(2.5);
7
- expect(cost.outputCost).toBe(10);
8
- expect(cost.totalCost).toBe(12.5);
9
- });
10
- it('uses updated OpenAI pricing for gpt-4o-mini', () => {
11
- const cost = calculateUsageCost('gpt-4o-mini', 1_000_000, 1_000_000);
12
- expect(cost.inputCost).toBe(0.15);
13
- expect(cost.outputCost).toBe(0.6);
14
- expect(cost.totalCost).toBe(0.75);
15
- });
16
4
  it('resolves codex provider model names', () => {
17
5
  const cost = calculateUsageCost('codex/codex-5.3', 1_000_000, 1_000_000);
18
6
  expect(cost.inputCost).toBe(1.75);
@@ -25,16 +13,16 @@ describe('calculateUsageCost', () => {
25
13
  expect(cost.outputCost).toBe(14);
26
14
  expect(cost.totalCost).toBe(15.75);
27
15
  });
16
+ it('resolves codex5.5 alias pricing', () => {
17
+ const cost = calculateUsageCost('codex5.5', 1_000_000, 1_000_000);
18
+ expect(cost.inputCost).toBe(5);
19
+ expect(cost.outputCost).toBe(30);
20
+ expect(cost.totalCost).toBe(35);
21
+ });
28
22
  it('resolves codex5.1 model pricing', () => {
29
23
  const cost = calculateUsageCost('codex/gpt-5.1-codex', 1_000_000, 1_000_000);
30
24
  expect(cost.inputCost).toBe(1.25);
31
25
  expect(cost.outputCost).toBe(10);
32
26
  expect(cost.totalCost).toBe(11.25);
33
27
  });
34
- it('resolves minimax alias case correctly', () => {
35
- const cost = calculateUsageCost('minimax', 1_000_000, 1_000_000);
36
- expect(cost.inputCost).toBe(0.3);
37
- expect(cost.outputCost).toBe(1.2);
38
- expect(cost.totalCost).toBe(1.5);
39
- });
40
28
  });
@@ -4,7 +4,6 @@ function mockConfig() {
4
4
  return {
5
5
  models: {
6
6
  aliases: {
7
- 'claude-fast': 'anthropic/claude-haiku-4-5',
8
7
  codex5: 'codex/gpt-5.3-codex',
9
8
  },
10
9
  },
@@ -12,10 +11,10 @@ function mockConfig() {
12
11
  }
13
12
  describe('model-selection', () => {
14
13
  it('lists aliases sorted', () => {
15
- expect(listModelAliases(mockConfig())).toEqual(['claude-fast', 'codex5']);
14
+ expect(listModelAliases(mockConfig())).toEqual(['codex5']);
16
15
  });
17
16
  it('formats aliases', () => {
18
- expect(formatAliases(mockConfig())).toBe('claude-fast, codex5');
17
+ expect(formatAliases(mockConfig())).toBe('codex5');
19
18
  });
20
19
  it('returns model selection usage string', () => {
21
20
  expect(getModelSelectionUsage()).toBe('Use alias, provider/model, or model-id.');
@@ -56,7 +55,7 @@ describe('model-selection', () => {
56
55
  it('formats model selection errors with aliases and usage', () => {
57
56
  const text = formatModelSelectionError('Unknown model alias: "x"', mockConfig());
58
57
  expect(text).toContain('Unknown model alias: "x"');
59
- expect(text).toContain('Available aliases: claude-fast, codex5');
58
+ expect(text).toContain('Available aliases: codex5');
60
59
  expect(text).toContain('Use alias, provider/model, or model-id.');
61
60
  });
62
61
  });
@@ -1,9 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { initProviders, hasOpenAIClient, addResponsesApiProvider, isResponsesApiProvider, setUsingOAuth, isUsingOAuth, } from '../providers/index.js';
2
+ import { initProviders, addResponsesApiProvider, isResponsesApiProvider, setUsingOAuth, isUsingOAuth, } from '../providers/index.js';
3
3
  function baseConfig(providers) {
4
4
  return {
5
5
  gateway: { port: 18790, mode: 'local' },
6
- agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: 'openai/gpt-4o' } } },
6
+ agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: 'anthropic/claude-sonnet-4-6' } } },
7
7
  models: { providers, aliases: {} },
8
8
  channels: { telegram: { enabled: false, token: '', allowFrom: [] }, discord: { enabled: false, token: '', allowFrom: [] } },
9
9
  cron: { jobs: [] },
@@ -11,12 +11,6 @@ function baseConfig(providers) {
11
11
  };
12
12
  }
13
13
  describe('providers init reset behavior', () => {
14
- it('clears stale OpenAI clients on re-init', async () => {
15
- await initProviders(baseConfig({ openai: { apiKey: 'sk-test' } }));
16
- expect(hasOpenAIClient('openai')).toBe(true);
17
- await initProviders(baseConfig({}));
18
- expect(hasOpenAIClient('openai')).toBe(false);
19
- });
20
14
  it('clears stale codex provider registrations on re-init', async () => {
21
15
  addResponsesApiProvider('openai');
22
16
  expect(isResponsesApiProvider('openai')).toBe(true);
@@ -3,7 +3,7 @@ import { addResponsesApiProvider, chat, initProviders } from '../providers/index
3
3
  function baseConfig(providers) {
4
4
  return {
5
5
  gateway: { port: 18790, mode: 'local' },
6
- agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: 'openai/gpt-4o' } } },
6
+ agents: { default: 'main', list: { main: { identity: { name: 'bot', emoji: 'x' }, model: 'anthropic/claude-sonnet-4-6' } } },
7
7
  models: { providers, aliases: {} },
8
8
  channels: { telegram: { enabled: false, token: '', allowFrom: [] }, discord: { enabled: false, token: '', allowFrom: [] } },
9
9
  cron: { jobs: [] },
@@ -1,10 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { getProvider, stripProvider, resolveModel, resolveProviderRoute, shouldUseCodexAliasProvider, } from '../providers/utils.js';
2
+ import { getProvider, stripProvider, resolveModel, resolveProviderRoute, shouldUseCodexAliasProvider, buildThinkingConfig, } from '../providers/utils.js';
3
3
  describe('provider utils', () => {
4
4
  it('detects provider from explicit prefix', () => {
5
5
  expect(getProvider('openai/gpt-5.3-codex')).toBe('openai');
6
6
  expect(getProvider('codex/gpt-5.3-codex')).toBe('codex');
7
7
  expect(getProvider('anthropic/claude-sonnet-4-6')).toBe('anthropic');
8
+ expect(getProvider('gpt-5.5')).toBe('codex');
8
9
  });
9
10
  it('strips provider prefix when registries are omitted', () => {
10
11
  expect(stripProvider('openai/gpt-5.3-codex')).toBe('gpt-5.3-codex');
@@ -47,8 +48,14 @@ describe('provider utils', () => {
47
48
  });
48
49
  it('migrates deprecated claude opus 4 model ids', () => {
49
50
  const cfg = { models: { aliases: {} } };
50
- expect(resolveModel('claude-opus-4', cfg)).toBe('claude-opus-4-6');
51
- expect(resolveModel('anthropic/claude-opus-4', cfg)).toBe('anthropic/claude-opus-4-6');
51
+ expect(resolveModel('claude-opus-4', cfg)).toBe('claude-opus-4-7');
52
+ expect(resolveModel('anthropic/claude-opus-4', cfg)).toBe('anthropic/claude-opus-4-7');
53
+ });
54
+ it('preserves explicit claude opus 4.6 model ids', () => {
55
+ const cfg = { models: { aliases: {} } };
56
+ expect(resolveModel('claude-opus-4.6', cfg)).toBe('claude-opus-4-6');
57
+ expect(resolveModel('anthropic/claude-opus-4.6', cfg)).toBe('anthropic/claude-opus-4-6');
58
+ expect(resolveModel('anthropic/claude-opus-4-6', cfg)).toBe('anthropic/claude-opus-4-6');
52
59
  });
53
60
  it('normalizes provider route fields after deprecated model migration', () => {
54
61
  const cfg = { models: { aliases: {} } };
@@ -58,4 +65,7 @@ describe('provider utils', () => {
58
65
  expect(route.modelId).toBe('claude-sonnet-4-6');
59
66
  expect(route.isCodexModel).toBe(false);
60
67
  });
68
+ it('supports xhigh thinking budgets', () => {
69
+ expect(buildThinkingConfig('xhigh')).toEqual({ budget: 32768, maxTokens: 36864 });
70
+ });
61
71
  });
@@ -2,14 +2,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mkdirSync, rmSync, existsSync, readFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
- import { loadHistory, saveExchange, replaceWithSummary, clearHistory, setSessionsDir, MAX_HISTORY_PAIRS, } from '../sessions.js';
5
+ import { loadHistory, saveExchange, replaceWithSummary, clearHistory, setSessionsDir, clearSessionKeyCacheForTests, MAX_HISTORY_PAIRS, } from '../sessions.js';
6
6
  let testSessionsDir;
7
7
  beforeEach(() => {
8
+ clearSessionKeyCacheForTests();
9
+ process.env.SKIMPYCLAW_HISTORY_KEY = 'test-history-key';
8
10
  testSessionsDir = join(tmpdir(), `sk-sessions-test-${Date.now()}`);
9
11
  mkdirSync(testSessionsDir, { recursive: true });
10
12
  setSessionsDir(testSessionsDir);
11
13
  });
12
14
  afterEach(() => {
15
+ delete process.env.SKIMPYCLAW_HISTORY_KEY;
13
16
  if (existsSync(testSessionsDir)) {
14
17
  rmSync(testSessionsDir, { recursive: true, force: true });
15
18
  }
@@ -30,10 +33,10 @@ describe('saveExchange', () => {
30
33
  const filePath = join(testSessionsDir, 'telegram-111.jsonl');
31
34
  expect(existsSync(filePath)).toBe(true);
32
35
  const content = readFileSync(filePath, 'utf-8');
33
- const entry = JSON.parse(content.trim());
34
- expect(entry.user).toBe('hello');
35
- expect(entry.assistant).toBe('hi there');
36
- expect(entry.ts).toBeTruthy();
36
+ expect(content.trim().startsWith('ENCv1:')).toBe(true);
37
+ const messages = await loadHistory('telegram', '111');
38
+ expect(messages[0].content).toBe('hello');
39
+ expect(messages[1].content).toBe('hi there');
37
40
  });
38
41
  it('appends multiple entries', async () => {
39
42
  await saveExchange('telegram', '222', 'msg1', 'reply1');
@@ -42,8 +45,8 @@ describe('saveExchange', () => {
42
45
  const content = readFileSync(filePath, 'utf-8');
43
46
  const lines = content.trim().split('\n').filter(Boolean);
44
47
  expect(lines).toHaveLength(2);
45
- expect(JSON.parse(lines[0]).user).toBe('msg1');
46
- expect(JSON.parse(lines[1]).user).toBe('msg2');
48
+ expect(lines[0].startsWith('ENCv1:')).toBe(true);
49
+ expect(lines[1].startsWith('ENCv1:')).toBe(true);
47
50
  });
48
51
  });
49
52
  describe('loadHistory round-trip', () => {
@@ -96,9 +99,10 @@ describe('replaceWithSummary', () => {
96
99
  const content = readFileSync(filePath, 'utf-8');
97
100
  const lines = content.trim().split('\n').filter(Boolean);
98
101
  expect(lines).toHaveLength(1);
99
- const entry = JSON.parse(lines[0]);
100
- expect(entry.summary).toBe(true);
101
- expect(entry.assistant).toBe('We talked about greetings.');
102
+ expect(lines[0].startsWith('ENCv1:')).toBe(true);
103
+ const messages = await loadHistory('telegram', '777');
104
+ expect(messages[0].content).toBe('Summary of our previous conversation:');
105
+ expect(messages[1].content).toBe('We talked about greetings.');
102
106
  });
103
107
  it('loadHistory after replaceWithSummary returns the summary pair', async () => {
104
108
  await saveExchange('telegram', '888', 'hello', 'world');
@@ -11,7 +11,7 @@ describe('setup config generation', () => {
11
11
  selectedProviders,
12
12
  providerSecrets: { anthropicKey: 'sk-ant-test' },
13
13
  });
14
- expect(config.agents.list.main.model).toBe('claude-opus');
14
+ expect(config.agents.list.main.model).toBe('anthropic/claude-opus-4-7');
15
15
  expect(config.models.providers.anthropic.apiKey).toBe('${ANTHROPIC_API_KEY}');
16
16
  expect(config.models.providers.codex.authPath).toBe('${HOME}/.codex/auth.json');
17
17
  expect(config.channels.telegram.allowFrom).toEqual([12345]);
@@ -19,30 +19,11 @@ describe('setup config generation', () => {
19
19
  expect(config.channels.telegram.defaultAllowedPaths).toEqual(['${HOME}/.skimpyclaw']);
20
20
  expect(config.channels.discord.defaultAllowedPaths).toEqual(['${HOME}/.skimpyclaw']);
21
21
  expect(config.heartbeat.tools.allowedPaths).toEqual(['${HOME}/.skimpyclaw']);
22
- expect(config.models.aliases['claude-think']).toBe('anthropic/claude-sonnet-4-6');
23
- expect(config.models.aliases['claude-opus']).toBe('anthropic/claude-opus-4-6');
24
- expect(config.models.aliases.codex).toBe('codex/gpt-5.3-codex');
22
+ expect(config.models.aliases.codex).toBe('codex/gpt-5.5');
25
23
  expect(config.models.aliases['codex5.1']).toBe('codex/gpt-5.1-codex');
26
24
  expect(config.models.aliases['codex5.2']).toBe('codex/gpt-5.2-codex');
27
25
  expect(config.models.aliases['codex5.3']).toBe('codex/gpt-5.3-codex');
28
- });
29
- it('builds OpenAI-only config and env content', () => {
30
- const selectedProviders = new Set(['openai-api']);
31
- const { configJson, envContent } = buildSetupArtifacts({
32
- workspaceDir: '/tmp/workspace',
33
- telegramId: 'abc-user',
34
- telegramToken: 'tg-token',
35
- agentName: 'Claw',
36
- selectedProviders,
37
- providerSecrets: { openaiKey: 'sk-openai-test' },
38
- });
39
- const config = JSON.parse(configJson);
40
- expect(config.agents.list.main.model).toBe('openai/gpt-4o');
41
- expect(config.models.providers.openai.apiKey).toBe('${OPENAI_API_KEY}');
42
- expect(config.channels.telegram.allowFrom).toEqual(['abc-user']);
43
- expect(envContent).toContain('OPENAI_API_KEY=sk-openai-test');
44
- expect(envContent).toContain('TELEGRAM_BOT_TOKEN=tg-token');
45
- expect(envContent).not.toContain('ANTHROPIC_API_KEY=');
26
+ expect(config.models.aliases['codex5.5']).toBe('codex/gpt-5.5');
46
27
  });
47
28
  it('includes oauth placeholders when Anthropic OAuth is selected', () => {
48
29
  const selectedProviders = new Set(['anthropic-oauth']);
@@ -71,7 +52,7 @@ describe('setup config generation', () => {
71
52
  expect(config.gateway.host).toBe('127.0.0.1');
72
53
  expect(config.gateway.port).toBe(18790);
73
54
  });
74
- it('disables browser when features.browser is false', () => {
55
+ it('does not include browser tools in generated config', () => {
75
56
  const config = buildSetupConfig({
76
57
  workspaceDir: '/tmp/workspace',
77
58
  telegramId: '12345',
@@ -79,12 +60,14 @@ describe('setup config generation', () => {
79
60
  agentName: 'Claw',
80
61
  selectedProviders: new Set(['anthropic-api']),
81
62
  providerSecrets: { anthropicKey: 'sk-ant-test' },
82
- features: { browser: false, voice: false, mcp: false, sandbox: false },
63
+ features: { voice: false, mcp: false },
83
64
  });
84
- expect(config.heartbeat.tools.browser.enabled).toBe(false);
65
+ expect(config.heartbeat.tools.browser).toBeUndefined();
66
+ expect(config.channels.telegram.tools.browser).toBeUndefined();
67
+ expect(config.channels.discord.tools.browser).toBeUndefined();
85
68
  expect(config.voice).toBeUndefined();
86
69
  });
87
- it('enables browser and voice when features are true', () => {
70
+ it('enables voice when requested', () => {
88
71
  const config = buildSetupConfig({
89
72
  workspaceDir: '/tmp/workspace',
90
73
  telegramId: '12345',
@@ -92,9 +75,9 @@ describe('setup config generation', () => {
92
75
  agentName: 'Claw',
93
76
  selectedProviders: new Set(['anthropic-api']),
94
77
  providerSecrets: { anthropicKey: 'sk-ant-test' },
95
- features: { browser: true, voice: true, mcp: false, sandbox: false },
78
+ features: { voice: true, mcp: false },
96
79
  });
97
- expect(config.heartbeat.tools.browser.enabled).toBe(true);
80
+ expect(config.heartbeat.tools.browser).toBeUndefined();
98
81
  expect(config.voice).toBeDefined();
99
82
  expect(config.voice.enabled).toBe(true);
100
83
  expect(config.voice.channels.telegram.sendVoice).toBe(true);
@@ -135,7 +118,7 @@ describe('setup config generation', () => {
135
118
  });
136
119
  expect(config.cron.jobs).toHaveLength(3);
137
120
  expect(config.cron.jobs[0].id).toBe('memory-trim');
138
- expect(config.cron.jobs[0].model).toBe('claude-fast');
121
+ expect(config.cron.jobs[0].model).toBe('anthropic/claude-haiku-4-5');
139
122
  expect(config.cron.jobs[1].id).toBe('tech-digest');
140
123
  expect(config.cron.jobs[2].id).toBe('weather');
141
124
  expect(config.cron.jobs[2].schedule.tz).toBe('America/New_York');
@@ -2,14 +2,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mkdirSync, writeFileSync, rmSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
- import { loadSkills, getSkillsForContext, formatSkillsPrompt, checkEligibility } from '../skills.js';
5
+ import { randomUUID } from 'crypto';
6
+ import { loadSkills, getSkillsForContext, formatSkillsPrompt, checkEligibility, clearSkillsCache } from '../skills.js';
6
7
  // Create a temp skills directory for each test
7
8
  let skillsDir;
8
9
  beforeEach(() => {
9
- skillsDir = join(tmpdir(), `skimpyclaw-skills-test-${Date.now()}`);
10
+ clearSkillsCache();
11
+ skillsDir = join(tmpdir(), `skimpyclaw-skills-test-${Date.now()}-${randomUUID()}`);
10
12
  mkdirSync(skillsDir, { recursive: true });
11
13
  });
12
14
  afterEach(() => {
15
+ clearSkillsCache();
13
16
  rmSync(skillsDir, { recursive: true, force: true });
14
17
  });
15
18
  function writeSkill(name, content) {
@@ -198,17 +201,17 @@ describe('checkEligibility', () => {
198
201
  expect(result.reason).toContain('Missing path');
199
202
  });
200
203
  it('fails on tools requirement when tools not enabled', () => {
201
- const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Browser'] } }, { enabled: false, allowedPaths: [] });
204
+ const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Fetch'] } }, { enabled: false, allowedPaths: [] });
202
205
  expect(result.eligible).toBe(false);
203
206
  expect(result.reason).toContain('Tools not enabled');
204
207
  });
205
- it('fails on browser tool requirement when browser not enabled', () => {
208
+ it('fails on unsupported tool requirement', () => {
206
209
  const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Browser'] } }, { enabled: true, allowedPaths: ['/tmp'] });
207
210
  expect(result.eligible).toBe(false);
208
- expect(result.reason).toContain('Browser');
211
+ expect(result.reason).toContain('Unsupported tools requested: Browser');
209
212
  });
210
- it('passes on browser tool requirement when browser enabled', () => {
211
- const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Browser'] } }, { enabled: true, allowedPaths: ['/tmp'], browser: { enabled: true } });
213
+ it('passes on fetch tool requirement when tools are enabled', () => {
214
+ const result = checkEligibility({ name: 'test', description: 'test', requires: { tools: ['Fetch'] } }, { enabled: true, allowedPaths: ['/tmp'] });
212
215
  expect(result.eligible).toBe(true);
213
216
  });
214
217
  it('passes on code_with_agent requirement when tools enabled', () => {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { stripAnsi, chunkForDiscord, parseCodexJsonl, formatCodexOutput, } from '../code-agents/stream-formatter.js';
3
+ describe('stripAnsi', () => {
4
+ it('removes color codes', () => {
5
+ const input = '\x1B[31mred text\x1B[0m plain';
6
+ expect(stripAnsi(input)).toBe('red text plain');
7
+ });
8
+ it('removes cursor moves', () => {
9
+ const input = 'a\x1B[2Kb';
10
+ expect(stripAnsi(input)).toBe('ab');
11
+ });
12
+ it('passes through plain text unchanged', () => {
13
+ expect(stripAnsi('hello world')).toBe('hello world');
14
+ });
15
+ });
16
+ describe('chunkForDiscord', () => {
17
+ it('returns empty array for empty input', () => {
18
+ expect(chunkForDiscord('')).toEqual([]);
19
+ });
20
+ it('returns single chunk for short input', () => {
21
+ expect(chunkForDiscord('short text')).toEqual(['short text']);
22
+ });
23
+ it('splits on paragraph boundaries when possible', () => {
24
+ const p1 = 'a'.repeat(1000);
25
+ const p2 = 'b'.repeat(1000);
26
+ const input = p1 + '\n\n' + p2;
27
+ const chunks = chunkForDiscord(input, 1900);
28
+ // Both paragraphs fit in 1900 together (1000 + 2 + 1000 = 2002 > 1900)
29
+ expect(chunks).toHaveLength(2);
30
+ expect(chunks[0]).toBe(p1);
31
+ expect(chunks[1]).toBe(p2);
32
+ });
33
+ it('packs multiple small paragraphs together', () => {
34
+ const input = 'one\n\ntwo\n\nthree';
35
+ expect(chunkForDiscord(input, 1900)).toEqual(['one\n\ntwo\n\nthree']);
36
+ });
37
+ it('hard-splits a paragraph that is larger than max', () => {
38
+ const huge = 'x'.repeat(5000);
39
+ const chunks = chunkForDiscord(huge, 1900);
40
+ expect(chunks).toHaveLength(Math.ceil(5000 / 1900));
41
+ expect(chunks.every(c => c.length <= 1900)).toBe(true);
42
+ expect(chunks.join('')).toBe(huge);
43
+ });
44
+ it('strips ANSI before chunking', () => {
45
+ const input = '\x1B[31mred\x1B[0m and more';
46
+ expect(chunkForDiscord(input)).toEqual(['red and more']);
47
+ });
48
+ it('trims trailing whitespace but preserves internal', () => {
49
+ expect(chunkForDiscord('hello\n\n \n')).toEqual(['hello']);
50
+ });
51
+ });
52
+ describe('parseCodexJsonl', () => {
53
+ it('captures thread_id from thread.started', () => {
54
+ const input = JSON.stringify({ type: 'thread.started', thread_id: 'abc-123' });
55
+ const r = parseCodexJsonl(input);
56
+ expect(r.threadId).toBe('abc-123');
57
+ expect(r.messages).toEqual([]);
58
+ });
59
+ it('extracts agent_message text', () => {
60
+ const input = [
61
+ JSON.stringify({ type: 'thread.started', thread_id: 't1' }),
62
+ JSON.stringify({ type: 'turn.started' }),
63
+ JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'hi there' } }),
64
+ JSON.stringify({ type: 'turn.completed' }),
65
+ ].join('\n');
66
+ const r = parseCodexJsonl(input);
67
+ expect(r.threadId).toBe('t1');
68
+ expect(r.messages).toEqual(['hi there']);
69
+ });
70
+ it('condenses command_execution as tool-call line', () => {
71
+ const input = JSON.stringify({
72
+ type: 'item.completed',
73
+ item: { type: 'command_execution', command: 'ls -la', status: 'completed' },
74
+ });
75
+ const r = parseCodexJsonl(input);
76
+ expect(r.messages[0]).toContain('✓');
77
+ expect(r.messages[0]).toContain('ls -la');
78
+ });
79
+ it('condenses file_change with paths', () => {
80
+ const input = JSON.stringify({
81
+ type: 'item.completed',
82
+ item: { type: 'file_change', changes: [{ path: 'src/foo.ts' }, { path: 'src/bar.ts' }] },
83
+ });
84
+ const r = parseCodexJsonl(input);
85
+ expect(r.messages[0]).toContain('src/foo.ts');
86
+ expect(r.messages[0]).toContain('src/bar.ts');
87
+ });
88
+ it('ignores turn.started/completed', () => {
89
+ const input = [
90
+ JSON.stringify({ type: 'turn.started' }),
91
+ JSON.stringify({ type: 'turn.completed' }),
92
+ ].join('\n');
93
+ const r = parseCodexJsonl(input);
94
+ expect(r.messages).toEqual([]);
95
+ });
96
+ it('tolerates invalid JSON lines', () => {
97
+ const input = [
98
+ 'not json',
99
+ JSON.stringify({ type: 'item.completed', item: { type: 'agent_message', text: 'hi' } }),
100
+ '{"broken":',
101
+ ].join('\n');
102
+ const r = parseCodexJsonl(input);
103
+ expect(r.messages).toEqual(['hi']);
104
+ });
105
+ });
106
+ describe('formatCodexOutput', () => {
107
+ it('chunks each message independently', () => {
108
+ const short = 'hi';
109
+ const long = 'y'.repeat(5000);
110
+ const chunks = formatCodexOutput([short, long]);
111
+ expect(chunks[0]).toBe('hi');
112
+ expect(chunks.slice(1).every(c => c.length <= 1900)).toBe(true);
113
+ });
114
+ });