@doingdev/opencode-claude-manager-plugin 0.1.35 → 0.1.43

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 (90) hide show
  1. package/dist/claude/claude-agent-sdk-adapter.js +1 -0
  2. package/dist/manager/git-operations.d.ts +10 -1
  3. package/dist/manager/git-operations.js +18 -3
  4. package/dist/manager/persistent-manager.d.ts +19 -3
  5. package/dist/manager/persistent-manager.js +21 -9
  6. package/dist/manager/session-controller.d.ts +8 -5
  7. package/dist/manager/session-controller.js +25 -20
  8. package/dist/metadata/claude-metadata.service.d.ts +12 -0
  9. package/dist/metadata/claude-metadata.service.js +38 -0
  10. package/dist/metadata/repo-claude-config-reader.d.ts +7 -0
  11. package/dist/metadata/repo-claude-config-reader.js +154 -0
  12. package/dist/plugin/agent-hierarchy.d.ts +9 -9
  13. package/dist/plugin/agent-hierarchy.js +25 -25
  14. package/dist/plugin/claude-manager.plugin.js +83 -46
  15. package/dist/plugin/orchestrator.plugin.d.ts +2 -0
  16. package/dist/plugin/orchestrator.plugin.js +116 -0
  17. package/dist/plugin/service-factory.js +3 -8
  18. package/dist/prompts/registry.js +100 -103
  19. package/dist/providers/claude-code-wrapper.d.ts +13 -0
  20. package/dist/providers/claude-code-wrapper.js +13 -0
  21. package/dist/safety/bash-safety.d.ts +21 -0
  22. package/dist/safety/bash-safety.js +62 -0
  23. package/dist/src/claude/claude-agent-sdk-adapter.d.ts +27 -0
  24. package/dist/src/claude/claude-agent-sdk-adapter.js +517 -0
  25. package/dist/src/claude/claude-session.service.d.ts +10 -0
  26. package/dist/src/claude/claude-session.service.js +18 -0
  27. package/dist/src/claude/session-live-tailer.d.ts +51 -0
  28. package/dist/src/claude/session-live-tailer.js +269 -0
  29. package/dist/src/claude/tool-approval-manager.d.ts +27 -0
  30. package/dist/src/claude/tool-approval-manager.js +232 -0
  31. package/dist/src/index.d.ts +6 -0
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/manager/context-tracker.d.ts +33 -0
  34. package/dist/src/manager/context-tracker.js +106 -0
  35. package/dist/src/manager/git-operations.d.ts +12 -0
  36. package/dist/src/manager/git-operations.js +76 -0
  37. package/dist/src/manager/persistent-manager.d.ts +77 -0
  38. package/dist/src/manager/persistent-manager.js +170 -0
  39. package/dist/src/manager/session-controller.d.ts +44 -0
  40. package/dist/src/manager/session-controller.js +147 -0
  41. package/dist/src/plugin/agent-hierarchy.d.ts +60 -0
  42. package/dist/src/plugin/agent-hierarchy.js +157 -0
  43. package/dist/src/plugin/claude-manager.plugin.d.ts +2 -0
  44. package/dist/src/plugin/claude-manager.plugin.js +563 -0
  45. package/dist/src/plugin/service-factory.d.ts +12 -0
  46. package/dist/src/plugin/service-factory.js +38 -0
  47. package/dist/src/prompts/registry.d.ts +11 -0
  48. package/dist/src/prompts/registry.js +260 -0
  49. package/dist/src/state/file-run-state-store.d.ts +14 -0
  50. package/dist/src/state/file-run-state-store.js +85 -0
  51. package/dist/src/state/transcript-store.d.ts +15 -0
  52. package/dist/src/state/transcript-store.js +44 -0
  53. package/dist/src/types/contracts.d.ts +200 -0
  54. package/dist/src/types/contracts.js +1 -0
  55. package/dist/src/util/fs-helpers.d.ts +2 -0
  56. package/dist/src/util/fs-helpers.js +10 -0
  57. package/dist/src/util/project-context.d.ts +10 -0
  58. package/dist/src/util/project-context.js +105 -0
  59. package/dist/src/util/transcript-append.d.ts +7 -0
  60. package/dist/src/util/transcript-append.js +29 -0
  61. package/dist/test/claude-agent-sdk-adapter.test.d.ts +1 -0
  62. package/dist/test/claude-agent-sdk-adapter.test.js +459 -0
  63. package/dist/test/claude-manager.plugin.test.d.ts +1 -0
  64. package/dist/test/claude-manager.plugin.test.js +331 -0
  65. package/dist/test/context-tracker.test.d.ts +1 -0
  66. package/dist/test/context-tracker.test.js +138 -0
  67. package/dist/test/file-run-state-store.test.d.ts +1 -0
  68. package/dist/test/file-run-state-store.test.js +82 -0
  69. package/dist/test/git-operations.test.d.ts +1 -0
  70. package/dist/test/git-operations.test.js +90 -0
  71. package/dist/test/persistent-manager.test.d.ts +1 -0
  72. package/dist/test/persistent-manager.test.js +208 -0
  73. package/dist/test/project-context.test.d.ts +1 -0
  74. package/dist/test/project-context.test.js +92 -0
  75. package/dist/test/prompt-registry.test.d.ts +1 -0
  76. package/dist/test/prompt-registry.test.js +256 -0
  77. package/dist/test/session-controller.test.d.ts +1 -0
  78. package/dist/test/session-controller.test.js +149 -0
  79. package/dist/test/session-live-tailer.test.d.ts +1 -0
  80. package/dist/test/session-live-tailer.test.js +313 -0
  81. package/dist/test/tool-approval-manager.test.d.ts +1 -0
  82. package/dist/test/tool-approval-manager.test.js +264 -0
  83. package/dist/test/transcript-append.test.d.ts +1 -0
  84. package/dist/test/transcript-append.test.js +37 -0
  85. package/dist/test/transcript-store.test.d.ts +1 -0
  86. package/dist/test/transcript-store.test.js +50 -0
  87. package/dist/types/contracts.d.ts +3 -4
  88. package/dist/vitest.config.d.ts +2 -0
  89. package/dist/vitest.config.js +11 -0
  90. package/package.json +2 -2
@@ -0,0 +1,105 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { isFileNotFoundError } from './fs-helpers.js';
4
+ /**
5
+ * Recursively discover all project-level Claude files:
6
+ * - every `CLAUDE.md` found anywhere in the repo tree
7
+ * - every file under `.claude/` recursively
8
+ *
9
+ * Returns a de-duplicated, deterministically sorted array of
10
+ * { relativePath, content } entries. Empty/blank files are skipped.
11
+ */
12
+ export async function discoverProjectClaudeFiles(cwd) {
13
+ const seen = new Set();
14
+ const results = [];
15
+ // 1. Walk the entire tree for CLAUDE.md files.
16
+ await walkForClaudeMd(cwd, cwd, seen, results);
17
+ // 2. Walk .claude/ recursively for any file.
18
+ await walkDotClaudeDir(cwd, seen, results);
19
+ // Deterministic: sort by relativePath (lexicographic).
20
+ results.sort((a, b) => a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0);
21
+ return results;
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Internal helpers
25
+ // ---------------------------------------------------------------------------
26
+ /** Recursively walk `dir` looking for files named `CLAUDE.md`. */
27
+ async function walkForClaudeMd(root, dir, seen, out) {
28
+ let entries;
29
+ try {
30
+ entries = await readdir(dir, { withFileTypes: true });
31
+ }
32
+ catch (err) {
33
+ if (isFileNotFoundError(err))
34
+ return;
35
+ throw err;
36
+ }
37
+ for (const entry of entries) {
38
+ const fullPath = join(dir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ // Skip node_modules, .git, and hidden dirs other than .claude
41
+ if (entry.name === 'node_modules' || entry.name === '.git')
42
+ continue;
43
+ await walkForClaudeMd(root, fullPath, seen, out);
44
+ }
45
+ else if (entry.name === 'CLAUDE.md') {
46
+ const rel = toForwardSlash(relative(root, fullPath));
47
+ if (seen.has(rel))
48
+ continue;
49
+ const content = await tryReadText(fullPath);
50
+ if (content !== null) {
51
+ seen.add(rel);
52
+ out.push({ relativePath: rel, content });
53
+ }
54
+ }
55
+ }
56
+ }
57
+ /** Recursively walk `.claude/` and collect every file. */
58
+ async function walkDotClaudeDir(root, seen, out) {
59
+ const dotClaudeDir = join(root, '.claude');
60
+ await walkAllFiles(root, dotClaudeDir, seen, out);
61
+ }
62
+ /** Recursively collect all files under `dir`. */
63
+ async function walkAllFiles(root, dir, seen, out) {
64
+ let entries;
65
+ try {
66
+ entries = await readdir(dir, { withFileTypes: true });
67
+ }
68
+ catch (err) {
69
+ if (isFileNotFoundError(err))
70
+ return;
71
+ throw err;
72
+ }
73
+ for (const entry of entries) {
74
+ const fullPath = join(dir, entry.name);
75
+ if (entry.isDirectory()) {
76
+ await walkAllFiles(root, fullPath, seen, out);
77
+ }
78
+ else if (entry.isFile()) {
79
+ const rel = toForwardSlash(relative(root, fullPath));
80
+ if (seen.has(rel))
81
+ continue;
82
+ const content = await tryReadText(fullPath);
83
+ if (content !== null) {
84
+ seen.add(rel);
85
+ out.push({ relativePath: rel, content });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ /** Read a file as UTF-8, returning trimmed content or null if empty/missing. */
91
+ async function tryReadText(filePath) {
92
+ try {
93
+ const raw = await readFile(filePath, 'utf-8');
94
+ const trimmed = raw.trim();
95
+ return trimmed.length > 0 ? trimmed : null;
96
+ }
97
+ catch (err) {
98
+ if (isFileNotFoundError(err))
99
+ return null;
100
+ throw err;
101
+ }
102
+ }
103
+ function toForwardSlash(p) {
104
+ return p.replace(/\\/g, '/');
105
+ }
@@ -0,0 +1,7 @@
1
+ import type { ClaudeSessionEvent } from '../types/contracts.js';
2
+ export declare function stripTrailingPartials(events: ClaudeSessionEvent[]): ClaudeSessionEvent[];
3
+ /**
4
+ * Append transcript events for run-state persistence: at most one trailing
5
+ * `partial`, dropped whenever a non-partial event is appended.
6
+ */
7
+ export declare function appendTranscriptEvents(events: ClaudeSessionEvent[], incoming: ClaudeSessionEvent[]): ClaudeSessionEvent[];
@@ -0,0 +1,29 @@
1
+ export function stripTrailingPartials(events) {
2
+ let end = events.length;
3
+ while (end > 0 && events[end - 1].type === 'partial') {
4
+ end -= 1;
5
+ }
6
+ return end === events.length ? events : events.slice(0, end);
7
+ }
8
+ /**
9
+ * Append transcript events for run-state persistence: at most one trailing
10
+ * `partial`, dropped whenever a non-partial event is appended.
11
+ */
12
+ export function appendTranscriptEvents(events, incoming) {
13
+ let next = events;
14
+ for (const event of incoming) {
15
+ if (event.type === 'partial') {
16
+ if (next.length > 0 && next[next.length - 1].type === 'partial') {
17
+ next = [...next.slice(0, -1), event];
18
+ }
19
+ else {
20
+ next = [...next, event];
21
+ }
22
+ }
23
+ else {
24
+ next = stripTrailingPartials(next);
25
+ next = [...next, event];
26
+ }
27
+ }
28
+ return next;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,459 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ClaudeAgentSdkAdapter } from '../src/claude/claude-agent-sdk-adapter.js';
3
+ function createFakeQuery(messages) {
4
+ return {
5
+ async *[Symbol.asyncIterator]() {
6
+ for (const message of messages) {
7
+ yield message;
8
+ }
9
+ },
10
+ close() {
11
+ return undefined;
12
+ },
13
+ supportedCommands: async () => [
14
+ {
15
+ name: 'review',
16
+ description: 'Review changes',
17
+ argumentHint: '<path>',
18
+ },
19
+ ],
20
+ supportedAgents: async () => [
21
+ {
22
+ name: 'researcher',
23
+ description: 'Looks around the repo',
24
+ model: 'claude-sonnet-4-5',
25
+ },
26
+ ],
27
+ supportedModels: async () => [{ value: 'claude-sonnet-4-5' }],
28
+ };
29
+ }
30
+ describe('ClaudeAgentSdkAdapter', () => {
31
+ it('normalizes streamed Claude session events', async () => {
32
+ const adapter = new ClaudeAgentSdkAdapter({
33
+ query: () => createFakeQuery([
34
+ { type: 'system', subtype: 'init', session_id: 'ses_123' },
35
+ {
36
+ type: 'assistant',
37
+ session_id: 'ses_123',
38
+ message: {
39
+ content: [{ type: 'text', text: 'Working through the task.' }],
40
+ },
41
+ },
42
+ {
43
+ type: 'result',
44
+ subtype: 'success',
45
+ session_id: 'ses_123',
46
+ is_error: false,
47
+ result: 'Done.',
48
+ num_turns: 2,
49
+ total_cost_usd: 0.42,
50
+ },
51
+ ]),
52
+ listSessions: async () => [],
53
+ getSessionMessages: async () => [],
54
+ });
55
+ const result = await adapter.runSession({
56
+ cwd: '/tmp/project',
57
+ prompt: 'Do the thing',
58
+ includePartialMessages: true,
59
+ });
60
+ expect(result.sessionId).toBe('ses_123');
61
+ expect(result.finalText).toBe('Done.');
62
+ expect(result.events.map((event) => event.type)).toEqual(['init', 'assistant', 'result']);
63
+ });
64
+ it('splits assistant turns into text and tool_call events', async () => {
65
+ const adapter = new ClaudeAgentSdkAdapter({
66
+ query: () => createFakeQuery([
67
+ { type: 'system', subtype: 'init', session_id: 'ses_tool' },
68
+ {
69
+ type: 'assistant',
70
+ session_id: 'ses_tool',
71
+ message: {
72
+ content: [
73
+ { type: 'text', text: 'Searching.' },
74
+ {
75
+ type: 'tool_use',
76
+ name: 'Grep',
77
+ id: 'toolu_1',
78
+ input: { pattern: 'foo', path: '.' },
79
+ },
80
+ ],
81
+ },
82
+ },
83
+ {
84
+ type: 'result',
85
+ subtype: 'success',
86
+ session_id: 'ses_tool',
87
+ is_error: false,
88
+ result: 'Found.',
89
+ num_turns: 1,
90
+ total_cost_usd: 0.02,
91
+ },
92
+ ]),
93
+ listSessions: async () => [],
94
+ getSessionMessages: async () => [],
95
+ });
96
+ const result = await adapter.runSession({
97
+ cwd: '/tmp/project',
98
+ prompt: 'Search',
99
+ includePartialMessages: true,
100
+ });
101
+ expect(result.events.map((e) => e.type)).toEqual(['init', 'assistant', 'tool_call', 'result']);
102
+ expect(result.events[2]).toMatchObject({ type: 'tool_call' });
103
+ expect(result.events[2]?.text).toContain('Grep');
104
+ expect(result.events[2]?.text).toContain('toolu_1');
105
+ });
106
+ it('emits user messages with tool_result content', async () => {
107
+ const adapter = new ClaudeAgentSdkAdapter({
108
+ query: () => createFakeQuery([
109
+ { type: 'system', subtype: 'init', session_id: 'ses_u' },
110
+ {
111
+ type: 'user',
112
+ session_id: 'ses_u',
113
+ parent_tool_use_id: null,
114
+ message: {
115
+ role: 'user',
116
+ content: [
117
+ {
118
+ type: 'tool_result',
119
+ tool_use_id: 'toolu_1',
120
+ content: 'file.ts:42: match',
121
+ },
122
+ ],
123
+ },
124
+ },
125
+ {
126
+ type: 'result',
127
+ subtype: 'success',
128
+ session_id: 'ses_u',
129
+ is_error: false,
130
+ result: 'OK',
131
+ num_turns: 1,
132
+ total_cost_usd: 0,
133
+ },
134
+ ]),
135
+ listSessions: async () => [],
136
+ getSessionMessages: async () => [],
137
+ });
138
+ const result = await adapter.runSession({
139
+ cwd: '/tmp/project',
140
+ prompt: 'Run',
141
+ includePartialMessages: true,
142
+ });
143
+ expect(result.events.map((e) => e.type)).toEqual(['init', 'user', 'result']);
144
+ expect(result.events[1]?.text).toContain('tool_result:toolu_1');
145
+ expect(result.events[1]?.text).toContain('file.ts');
146
+ });
147
+ it('drops stream_event payloads when includePartialMessages is false', async () => {
148
+ const adapter = new ClaudeAgentSdkAdapter({
149
+ query: () => createFakeQuery([
150
+ { type: 'system', subtype: 'init', session_id: 'ses_123' },
151
+ {
152
+ type: 'stream_event',
153
+ session_id: 'ses_123',
154
+ event: {
155
+ type: 'content_block_delta',
156
+ delta: { text: 'Partial output' },
157
+ },
158
+ },
159
+ {
160
+ type: 'result',
161
+ subtype: 'success',
162
+ session_id: 'ses_123',
163
+ is_error: false,
164
+ result: 'Done.',
165
+ num_turns: 1,
166
+ total_cost_usd: 0.01,
167
+ },
168
+ ]),
169
+ listSessions: async () => [],
170
+ getSessionMessages: async () => [],
171
+ });
172
+ const result = await adapter.runSession({
173
+ cwd: '/tmp/project',
174
+ prompt: 'Do the thing',
175
+ includePartialMessages: false,
176
+ });
177
+ expect(result.events.map((event) => event.type)).toEqual(['init', 'result']);
178
+ });
179
+ it('collapses partials and drops them before the final result', async () => {
180
+ const adapter = new ClaudeAgentSdkAdapter({
181
+ query: () => createFakeQuery([
182
+ { type: 'system', subtype: 'init', session_id: 'ses_123' },
183
+ {
184
+ type: 'stream_event',
185
+ session_id: 'ses_123',
186
+ event: {
187
+ type: 'content_block_delta',
188
+ delta: { text: 'Partial ' },
189
+ },
190
+ },
191
+ {
192
+ type: 'stream_event',
193
+ session_id: 'ses_123',
194
+ event: {
195
+ type: 'content_block_delta',
196
+ delta: { text: 'output' },
197
+ },
198
+ },
199
+ {
200
+ type: 'result',
201
+ subtype: 'success',
202
+ session_id: 'ses_123',
203
+ is_error: false,
204
+ result: 'Done.',
205
+ num_turns: 1,
206
+ total_cost_usd: 0.01,
207
+ },
208
+ ]),
209
+ listSessions: async () => [],
210
+ getSessionMessages: async () => [],
211
+ });
212
+ const result = await adapter.runSession({
213
+ cwd: '/tmp/project',
214
+ prompt: 'Do the thing',
215
+ includePartialMessages: true,
216
+ });
217
+ expect(result.events.map((event) => event.type)).toEqual(['init', 'result']);
218
+ });
219
+ it('ignores structural stream events without text payloads', async () => {
220
+ const adapter = new ClaudeAgentSdkAdapter({
221
+ query: () => createFakeQuery([
222
+ { type: 'system', subtype: 'init', session_id: 'ses_123' },
223
+ {
224
+ type: 'stream_event',
225
+ session_id: 'ses_123',
226
+ event: { type: 'message_start' },
227
+ },
228
+ {
229
+ type: 'stream_event',
230
+ session_id: 'ses_123',
231
+ event: {
232
+ type: 'content_block_delta',
233
+ delta: { text: 'Partial output' },
234
+ },
235
+ },
236
+ {
237
+ type: 'result',
238
+ subtype: 'success',
239
+ session_id: 'ses_123',
240
+ is_error: false,
241
+ result: 'Done.',
242
+ num_turns: 1,
243
+ total_cost_usd: 0.01,
244
+ },
245
+ ]),
246
+ listSessions: async () => [],
247
+ getSessionMessages: async () => [],
248
+ });
249
+ const result = await adapter.runSession({
250
+ cwd: '/tmp/project',
251
+ prompt: 'Do the thing',
252
+ includePartialMessages: true,
253
+ });
254
+ expect(result.events.map((event) => event.type)).toEqual(['init', 'result']);
255
+ });
256
+ it('probes supported commands, agents, and models', async () => {
257
+ const adapter = new ClaudeAgentSdkAdapter({
258
+ query: () => createFakeQuery([]),
259
+ listSessions: async () => [],
260
+ getSessionMessages: async () => [],
261
+ });
262
+ const capabilities = await adapter.probeCapabilities('/tmp/project');
263
+ expect(capabilities.commands[0]).toMatchObject({ name: 'review' });
264
+ expect(capabilities.agents[0]).toMatchObject({ name: 'researcher' });
265
+ expect(capabilities.models).toEqual(['claude-sonnet-4-5']);
266
+ });
267
+ it('defaults permissionMode to acceptEdits when omitted', async () => {
268
+ let capturedPermissionMode;
269
+ const adapter = new ClaudeAgentSdkAdapter({
270
+ query: (params) => {
271
+ capturedPermissionMode = params.options?.permissionMode;
272
+ return createFakeQuery([
273
+ {
274
+ type: 'result',
275
+ subtype: 'success',
276
+ session_id: 'ses_pm',
277
+ is_error: false,
278
+ result: 'ok',
279
+ num_turns: 1,
280
+ total_cost_usd: 0,
281
+ },
282
+ ]);
283
+ },
284
+ listSessions: async () => [],
285
+ getSessionMessages: async () => [],
286
+ });
287
+ await adapter.runSession({
288
+ cwd: '/tmp/project',
289
+ prompt: 'Do the thing',
290
+ });
291
+ expect(capturedPermissionMode).toBe('acceptEdits');
292
+ });
293
+ it('merges Skill into allowedTools for SDK Agent Skills', async () => {
294
+ let capturedAllowed;
295
+ const adapter = new ClaudeAgentSdkAdapter({
296
+ query: (params) => {
297
+ capturedAllowed = params.options?.allowedTools;
298
+ return createFakeQuery([
299
+ {
300
+ type: 'result',
301
+ subtype: 'success',
302
+ session_id: 'ses_skill',
303
+ is_error: false,
304
+ result: 'ok',
305
+ num_turns: 1,
306
+ total_cost_usd: 0,
307
+ },
308
+ ]);
309
+ },
310
+ listSessions: async () => [],
311
+ getSessionMessages: async () => [],
312
+ });
313
+ await adapter.runSession({
314
+ cwd: '/tmp/project',
315
+ prompt: 'Use a skill',
316
+ });
317
+ expect(capturedAllowed).toEqual(['Skill']);
318
+ });
319
+ it('appends Skill once when allowedTools is provided', async () => {
320
+ let capturedAllowed;
321
+ const adapter = new ClaudeAgentSdkAdapter({
322
+ query: (params) => {
323
+ capturedAllowed = params.options?.allowedTools;
324
+ return createFakeQuery([
325
+ {
326
+ type: 'result',
327
+ subtype: 'success',
328
+ session_id: 'ses_skill',
329
+ is_error: false,
330
+ result: 'ok',
331
+ num_turns: 1,
332
+ total_cost_usd: 0,
333
+ },
334
+ ]);
335
+ },
336
+ listSessions: async () => [],
337
+ getSessionMessages: async () => [],
338
+ });
339
+ await adapter.runSession({
340
+ cwd: '/tmp/project',
341
+ prompt: 'x',
342
+ allowedTools: ['Read', 'Grep'],
343
+ });
344
+ expect(capturedAllowed).toEqual(['Read', 'Grep', 'Skill']);
345
+ });
346
+ it('does not add Skill when Skill is disallowed', async () => {
347
+ let capturedAllowed;
348
+ const adapter = new ClaudeAgentSdkAdapter({
349
+ query: (params) => {
350
+ capturedAllowed = params.options?.allowedTools;
351
+ return createFakeQuery([
352
+ {
353
+ type: 'result',
354
+ subtype: 'success',
355
+ session_id: 'ses_skill',
356
+ is_error: false,
357
+ result: 'ok',
358
+ num_turns: 1,
359
+ total_cost_usd: 0,
360
+ },
361
+ ]);
362
+ },
363
+ listSessions: async () => [],
364
+ getSessionMessages: async () => [],
365
+ });
366
+ await adapter.runSession({
367
+ cwd: '/tmp/project',
368
+ prompt: 'x',
369
+ disallowedTools: ['Skill'],
370
+ });
371
+ expect(capturedAllowed).toBeUndefined();
372
+ });
373
+ it('extracts full usage from result with camelCase modelUsage', async () => {
374
+ const adapter = new ClaudeAgentSdkAdapter({
375
+ query: () => createFakeQuery([
376
+ { type: 'system', subtype: 'init', session_id: 'ses_usage' },
377
+ {
378
+ type: 'result',
379
+ subtype: 'success',
380
+ session_id: 'ses_usage',
381
+ is_error: false,
382
+ result: 'Done.',
383
+ num_turns: 5,
384
+ total_cost_usd: 1.23,
385
+ usage: { input_tokens: 50_000, output_tokens: 8_000 },
386
+ modelUsage: {
387
+ 'claude-opus-4-6': { contextWindow: 200_000 },
388
+ },
389
+ },
390
+ ]),
391
+ listSessions: async () => [],
392
+ getSessionMessages: async () => [],
393
+ });
394
+ const result = await adapter.runSession({
395
+ cwd: '/tmp/project',
396
+ prompt: 'Do work',
397
+ });
398
+ expect(result.turns).toBe(5);
399
+ expect(result.totalCostUsd).toBe(1.23);
400
+ expect(result.inputTokens).toBe(50_000);
401
+ expect(result.outputTokens).toBe(8_000);
402
+ expect(result.contextWindowSize).toBe(200_000);
403
+ });
404
+ it('extracts usage from snake_case model_usage fallback', async () => {
405
+ const adapter = new ClaudeAgentSdkAdapter({
406
+ query: () => createFakeQuery([
407
+ {
408
+ type: 'result',
409
+ subtype: 'success',
410
+ session_id: 'ses_snake',
411
+ is_error: false,
412
+ result: 'Ok.',
413
+ num_turns: 1,
414
+ total_cost_usd: 0.01,
415
+ usage: { input_tokens: 1000, output_tokens: 200 },
416
+ model_usage: {
417
+ 'claude-sonnet-4-5': { context_window: 180_000 },
418
+ },
419
+ },
420
+ ]),
421
+ listSessions: async () => [],
422
+ getSessionMessages: async () => [],
423
+ });
424
+ const result = await adapter.runSession({
425
+ cwd: '/tmp/project',
426
+ prompt: 'x',
427
+ });
428
+ expect(result.inputTokens).toBe(1000);
429
+ expect(result.outputTokens).toBe(200);
430
+ expect(result.contextWindowSize).toBe(180_000);
431
+ });
432
+ it('passes through explicit permissionMode', async () => {
433
+ let capturedPermissionMode;
434
+ const adapter = new ClaudeAgentSdkAdapter({
435
+ query: (params) => {
436
+ capturedPermissionMode = params.options?.permissionMode;
437
+ return createFakeQuery([
438
+ {
439
+ type: 'result',
440
+ subtype: 'success',
441
+ session_id: 'ses_pm',
442
+ is_error: false,
443
+ result: 'ok',
444
+ num_turns: 1,
445
+ total_cost_usd: 0,
446
+ },
447
+ ]);
448
+ },
449
+ listSessions: async () => [],
450
+ getSessionMessages: async () => [],
451
+ });
452
+ await adapter.runSession({
453
+ cwd: '/tmp/project',
454
+ prompt: 'Do the thing',
455
+ permissionMode: 'plan',
456
+ });
457
+ expect(capturedPermissionMode).toBe('plan');
458
+ });
459
+ });
@@ -0,0 +1 @@
1
+ export {};