discord-agent-bridge 0.1.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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/app.js +210 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/app.test.js +117 -0
  6. package/dist/app.test.js.map +1 -0
  7. package/dist/cli.js +63 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/cli.test.js +91 -0
  10. package/dist/cli.test.js.map +1 -0
  11. package/dist/core/auditLog.js +65 -0
  12. package/dist/core/auditLog.js.map +1 -0
  13. package/dist/core/auditLog.test.js +124 -0
  14. package/dist/core/auditLog.test.js.map +1 -0
  15. package/dist/core/auth.js +106 -0
  16. package/dist/core/auth.js.map +1 -0
  17. package/dist/core/auth.test.js +216 -0
  18. package/dist/core/auth.test.js.map +1 -0
  19. package/dist/core/channelRegistry.js +119 -0
  20. package/dist/core/channelRegistry.js.map +1 -0
  21. package/dist/core/channelRegistry.test.js +114 -0
  22. package/dist/core/channelRegistry.test.js.map +1 -0
  23. package/dist/core/commandPolicy.js +6 -0
  24. package/dist/core/commandPolicy.js.map +1 -0
  25. package/dist/core/commandRouter.js +9 -0
  26. package/dist/core/commandRouter.js.map +1 -0
  27. package/dist/core/config.js +141 -0
  28. package/dist/core/config.js.map +1 -0
  29. package/dist/core/config.test.js +149 -0
  30. package/dist/core/config.test.js.map +1 -0
  31. package/dist/core/configResolver.js +90 -0
  32. package/dist/core/configResolver.js.map +1 -0
  33. package/dist/core/configResolver.test.js +138 -0
  34. package/dist/core/configResolver.test.js.map +1 -0
  35. package/dist/core/configSchema.js +164 -0
  36. package/dist/core/configSchema.js.map +1 -0
  37. package/dist/core/contracts.js +5 -0
  38. package/dist/core/contracts.js.map +1 -0
  39. package/dist/core/eventBus.js +42 -0
  40. package/dist/core/eventBus.js.map +1 -0
  41. package/dist/core/eventBus.test.js +72 -0
  42. package/dist/core/eventBus.test.js.map +1 -0
  43. package/dist/core/hookBridge.js +11 -0
  44. package/dist/core/hookBridge.js.map +1 -0
  45. package/dist/core/logger.js +116 -0
  46. package/dist/core/logger.js.map +1 -0
  47. package/dist/core/logger.test.js +88 -0
  48. package/dist/core/logger.test.js.map +1 -0
  49. package/dist/core/modeRegistry.js +33 -0
  50. package/dist/core/modeRegistry.js.map +1 -0
  51. package/dist/core/permissionResolver.js +43 -0
  52. package/dist/core/permissionResolver.js.map +1 -0
  53. package/dist/core/permissionResolver.test.js +136 -0
  54. package/dist/core/permissionResolver.test.js.map +1 -0
  55. package/dist/core/providerCatalog.js +240 -0
  56. package/dist/core/providerCatalog.js.map +1 -0
  57. package/dist/core/providerCatalog.test.js +230 -0
  58. package/dist/core/providerCatalog.test.js.map +1 -0
  59. package/dist/core/sessionOrchestrator.js +463 -0
  60. package/dist/core/sessionOrchestrator.js.map +1 -0
  61. package/dist/core/sessionOrchestrator.test.js +344 -0
  62. package/dist/core/sessionOrchestrator.test.js.map +1 -0
  63. package/dist/core/state/schema.js +49 -0
  64. package/dist/core/state/schema.js.map +1 -0
  65. package/dist/core/state/store.js +86 -0
  66. package/dist/core/state/store.js.map +1 -0
  67. package/dist/core/state/store.test.js +99 -0
  68. package/dist/core/state/store.test.js.map +1 -0
  69. package/dist/core/usageService.js +310 -0
  70. package/dist/core/usageService.js.map +1 -0
  71. package/dist/core/usageService.test.js +234 -0
  72. package/dist/core/usageService.test.js.map +1 -0
  73. package/dist/discord/alwaysAllow.test.js +181 -0
  74. package/dist/discord/alwaysAllow.test.js.map +1 -0
  75. package/dist/discord/client.js +666 -0
  76. package/dist/discord/client.js.map +1 -0
  77. package/dist/discord/client.test.js +256 -0
  78. package/dist/discord/client.test.js.map +1 -0
  79. package/dist/discord/configPanel.js +270 -0
  80. package/dist/discord/configPanel.js.map +1 -0
  81. package/dist/discord/configPanel.test.js +332 -0
  82. package/dist/discord/configPanel.test.js.map +1 -0
  83. package/dist/discord/directoryBrowser.js +151 -0
  84. package/dist/discord/directoryBrowser.js.map +1 -0
  85. package/dist/discord/directoryBrowser.test.js +163 -0
  86. package/dist/discord/directoryBrowser.test.js.map +1 -0
  87. package/dist/discord/favorites.js +10 -0
  88. package/dist/discord/favorites.js.map +1 -0
  89. package/dist/discord/fileDownload.js +94 -0
  90. package/dist/discord/fileDownload.js.map +1 -0
  91. package/dist/discord/fileDownload.test.js +59 -0
  92. package/dist/discord/fileDownload.test.js.map +1 -0
  93. package/dist/discord/format.js +104 -0
  94. package/dist/discord/format.js.map +1 -0
  95. package/dist/discord/format.test.js +40 -0
  96. package/dist/discord/format.test.js.map +1 -0
  97. package/dist/discord/guildChannels.js +97 -0
  98. package/dist/discord/guildChannels.js.map +1 -0
  99. package/dist/discord/guildChannels.test.js +231 -0
  100. package/dist/discord/guildChannels.test.js.map +1 -0
  101. package/dist/discord/i18n.js +207 -0
  102. package/dist/discord/i18n.js.map +1 -0
  103. package/dist/discord/i18n.test.js +31 -0
  104. package/dist/discord/i18n.test.js.map +1 -0
  105. package/dist/discord/interactionRouter.js +954 -0
  106. package/dist/discord/interactionRouter.js.map +1 -0
  107. package/dist/discord/interactionRouter.test.js +1245 -0
  108. package/dist/discord/interactionRouter.test.js.map +1 -0
  109. package/dist/discord/messageRouter.js +221 -0
  110. package/dist/discord/messageRouter.js.map +1 -0
  111. package/dist/discord/messageRouter.test.js +360 -0
  112. package/dist/discord/messageRouter.test.js.map +1 -0
  113. package/dist/discord/ports.js +12 -0
  114. package/dist/discord/ports.js.map +1 -0
  115. package/dist/discord/renderers/defaultSet.test.js +97 -0
  116. package/dist/discord/renderers/defaultSet.test.js.map +1 -0
  117. package/dist/discord/renderers/diffView.js +73 -0
  118. package/dist/discord/renderers/diffView.js.map +1 -0
  119. package/dist/discord/renderers/diffView.test.js +54 -0
  120. package/dist/discord/renderers/diffView.test.js.map +1 -0
  121. package/dist/discord/renderers/index.js +143 -0
  122. package/dist/discord/renderers/index.js.map +1 -0
  123. package/dist/discord/renderers/index.test.js +124 -0
  124. package/dist/discord/renderers/index.test.js.map +1 -0
  125. package/dist/discord/renderers/mentionOnComplete.js +19 -0
  126. package/dist/discord/renderers/mentionOnComplete.js.map +1 -0
  127. package/dist/discord/renderers/mentionOnComplete.test.js +35 -0
  128. package/dist/discord/renderers/mentionOnComplete.test.js.map +1 -0
  129. package/dist/discord/renderers/permissionButtons.js +150 -0
  130. package/dist/discord/renderers/permissionButtons.js.map +1 -0
  131. package/dist/discord/renderers/permissionButtons.test.js +108 -0
  132. package/dist/discord/renderers/permissionButtons.test.js.map +1 -0
  133. package/dist/discord/renderers/resultLine.js +25 -0
  134. package/dist/discord/renderers/resultLine.js.map +1 -0
  135. package/dist/discord/renderers/statusEmbed.js +20 -0
  136. package/dist/discord/renderers/statusEmbed.js.map +1 -0
  137. package/dist/discord/renderers/statusEmbed.test.js +32 -0
  138. package/dist/discord/renderers/statusEmbed.test.js.map +1 -0
  139. package/dist/discord/renderers/streamEmbed.js +134 -0
  140. package/dist/discord/renderers/streamEmbed.js.map +1 -0
  141. package/dist/discord/renderers/streamEmbed.test.js +97 -0
  142. package/dist/discord/renderers/streamEmbed.test.js.map +1 -0
  143. package/dist/discord/renderers/toolThread.js +65 -0
  144. package/dist/discord/renderers/toolThread.js.map +1 -0
  145. package/dist/discord/renderers/toolThread.test.js +54 -0
  146. package/dist/discord/renderers/toolThread.test.js.map +1 -0
  147. package/dist/discord/renderers/transcriptFeed.js +33 -0
  148. package/dist/discord/renderers/transcriptFeed.js.map +1 -0
  149. package/dist/discord/renderers/usageEmbed.js +78 -0
  150. package/dist/discord/renderers/usageEmbed.js.map +1 -0
  151. package/dist/discord/renderers/usageEmbed.test.js +42 -0
  152. package/dist/discord/renderers/usageEmbed.test.js.map +1 -0
  153. package/dist/discord/wiring.js +214 -0
  154. package/dist/discord/wiring.js.map +1 -0
  155. package/dist/discord/wiring.test.js +252 -0
  156. package/dist/discord/wiring.test.js.map +1 -0
  157. package/dist/discord/wizard/channelWizard.js +305 -0
  158. package/dist/discord/wizard/channelWizard.js.map +1 -0
  159. package/dist/discord/wizard/channelWizard.test.js +298 -0
  160. package/dist/discord/wizard/channelWizard.test.js.map +1 -0
  161. package/dist/discord/wizard/resumeWizard.js +123 -0
  162. package/dist/discord/wizard/resumeWizard.js.map +1 -0
  163. package/dist/modes/claude/index.js +80 -0
  164. package/dist/modes/claude/index.js.map +1 -0
  165. package/dist/modes/claude/listResumable.test.js +72 -0
  166. package/dist/modes/claude/listResumable.test.js.map +1 -0
  167. package/dist/modes/claude/mcpFileTool.js +82 -0
  168. package/dist/modes/claude/mcpFileTool.js.map +1 -0
  169. package/dist/modes/claude/permissions.js +35 -0
  170. package/dist/modes/claude/permissions.js.map +1 -0
  171. package/dist/modes/claude/plugins.js +56 -0
  172. package/dist/modes/claude/plugins.js.map +1 -0
  173. package/dist/modes/claude/session.js +324 -0
  174. package/dist/modes/claude/session.js.map +1 -0
  175. package/dist/modes/claude/session.test.js +415 -0
  176. package/dist/modes/claude/session.test.js.map +1 -0
  177. package/dist/modes/codex/discovery.js +221 -0
  178. package/dist/modes/codex/discovery.js.map +1 -0
  179. package/dist/modes/codex/discovery.test.js +213 -0
  180. package/dist/modes/codex/discovery.test.js.map +1 -0
  181. package/dist/modes/codex/eventMapper.js +262 -0
  182. package/dist/modes/codex/eventMapper.js.map +1 -0
  183. package/dist/modes/codex/index.js +132 -0
  184. package/dist/modes/codex/index.js.map +1 -0
  185. package/dist/modes/codex/index.test.js +170 -0
  186. package/dist/modes/codex/index.test.js.map +1 -0
  187. package/dist/modes/codex/runner.js +207 -0
  188. package/dist/modes/codex/runner.js.map +1 -0
  189. package/dist/modes/codex/runner.test.js +587 -0
  190. package/dist/modes/codex/runner.test.js.map +1 -0
  191. package/dist/modes/codex/sqliteReader.js +171 -0
  192. package/dist/modes/codex/sqliteReader.js.map +1 -0
  193. package/dist/modes/codex/sqliteReader.test.js +137 -0
  194. package/dist/modes/codex/sqliteReader.test.js.map +1 -0
  195. package/dist/setup/wizard.js +115 -0
  196. package/dist/setup/wizard.js.map +1 -0
  197. package/dist/setup/wizard.test.js +216 -0
  198. package/dist/setup/wizard.test.js.map +1 -0
  199. package/dist/startBot.test.js +78 -0
  200. package/dist/startBot.test.js.map +1 -0
  201. package/package.json +58 -0
@@ -0,0 +1,415 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { ClaudeMode } from './index.js';
6
+ import { ClaudeSession } from './session.js';
7
+ import { makeCanUseTool } from './permissions.js';
8
+ import { attachFileConfined } from './mcpFileTool.js';
9
+ // ---- Test doubles ------------------------------------------------------------
10
+ // A no-op logger matching the contract.
11
+ const nullLogger = {
12
+ debug() { },
13
+ info() { },
14
+ warn() { },
15
+ error() { },
16
+ };
17
+ function makeCtx(opts = {}) {
18
+ const events = [];
19
+ const permissionCalls = [];
20
+ const ctx = {
21
+ guildId: 'g1',
22
+ channelId: 'c1',
23
+ cwd: opts.cwd ?? '/tmp/ws',
24
+ ownerId: 'u1',
25
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
26
+ permMode: opts.permMode ?? 'default',
27
+ emit: (ev) => events.push(ev),
28
+ requestPermission: async (req) => {
29
+ permissionCalls.push(req);
30
+ return opts.permissionDecision ?? { behavior: 'allow' };
31
+ },
32
+ config: {
33
+ ...(opts.allowedTools !== undefined ? { allowedTools: opts.allowedTools } : {}),
34
+ ...(opts.autoAllowClaudeTools !== undefined
35
+ ? { autoAllowClaudeTools: opts.autoAllowClaudeTools }
36
+ : {}),
37
+ },
38
+ logger: nullLogger,
39
+ audit: () => { },
40
+ };
41
+ return { ctx, events, permissionCalls };
42
+ }
43
+ // A fake Query: yields the scripted messages once, then completes. Records
44
+ // getContextUsage() calls and whether close() ran. `stallForever` makes the
45
+ // stream never end (so the consume loop is still open when stop() aborts).
46
+ function makeFakeQuery(messages, opts = {}) {
47
+ const state = { closed: false, contextUsageCalls: 0 };
48
+ const query = {
49
+ async *[Symbol.asyncIterator]() {
50
+ for (const m of messages)
51
+ yield m;
52
+ if (opts.stallForever) {
53
+ // Park until close()/abort ends the test; never yields a result.
54
+ await new Promise(() => { });
55
+ }
56
+ },
57
+ close() {
58
+ state.closed = true;
59
+ },
60
+ async getContextUsage() {
61
+ state.contextUsageCalls++;
62
+ return { totalTokens: 1234, maxTokens: 200000, percentage: 0.617 };
63
+ },
64
+ };
65
+ return { query, state };
66
+ }
67
+ // Build an injectable queryFn that returns a fake query built from `messages`.
68
+ function fakeQueryFn(messages, opts = {}) {
69
+ const { query, state } = makeFakeQuery(messages, opts);
70
+ const captured = {};
71
+ const queryFn = ({ options }) => {
72
+ captured.options = options;
73
+ return query;
74
+ };
75
+ return { queryFn, captured, state };
76
+ }
77
+ // Poll until `pred()` is true or the budget elapses. The consume loop runs on
78
+ // microtasks, so a short poll is enough to let scripted messages flush.
79
+ async function waitFor(pred, budgetMs = 500) {
80
+ const start = Date.now();
81
+ while (!pred()) {
82
+ if (Date.now() - start > budgetMs)
83
+ throw new Error('waitFor timed out');
84
+ await new Promise((r) => setTimeout(r, 5));
85
+ }
86
+ }
87
+ // A scripted, ordered set of SDK messages covering every mapped kind.
88
+ function scriptedMessages() {
89
+ return [
90
+ { type: 'system', subtype: 'init', session_id: 'sess-abc', cwd: '/tmp/ws' },
91
+ {
92
+ type: 'stream_event',
93
+ event: { type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hel' } },
94
+ },
95
+ {
96
+ type: 'stream_event',
97
+ event: { type: 'content_block_delta', delta: { type: 'text_delta', text: 'lo' } },
98
+ },
99
+ {
100
+ type: 'stream_event',
101
+ event: {
102
+ type: 'content_block_delta',
103
+ delta: { type: 'thinking_delta', thinking: 'hmm' },
104
+ },
105
+ },
106
+ {
107
+ type: 'assistant',
108
+ message: {
109
+ role: 'assistant',
110
+ content: [{ type: 'tool_use', id: 'tu-1', name: 'Read', input: { file_path: '/tmp/ws/a.txt' } }],
111
+ },
112
+ },
113
+ {
114
+ type: 'user',
115
+ message: {
116
+ role: 'user',
117
+ content: [{ type: 'tool_result', tool_use_id: 'tu-1', content: 'file body', is_error: false }],
118
+ },
119
+ },
120
+ {
121
+ type: 'result',
122
+ subtype: 'success',
123
+ result: 'done',
124
+ total_cost_usd: 0.0123,
125
+ duration_ms: 4200,
126
+ usage: { input_tokens: 100, output_tokens: 42 },
127
+ },
128
+ {
129
+ type: 'rate_limit_event',
130
+ rate_limit_info: {
131
+ status: 'allowed_warning',
132
+ resetsAt: 1000,
133
+ rateLimitType: 'five_hour',
134
+ utilization: 87,
135
+ },
136
+ },
137
+ ];
138
+ }
139
+ // ---- Message → AgentEvent mapping -------------------------------------------
140
+ describe('ClaudeSession — SDK message mapping', () => {
141
+ it('maps each scripted SDK message to the correct AgentEvent, in order', async () => {
142
+ const { ctx, events } = makeCtx();
143
+ const { queryFn, state } = fakeQueryFn(scriptedMessages());
144
+ const session = new ClaudeSession(ctx, { queryFn });
145
+ // context_usage is emitted asynchronously after the result event.
146
+ await waitFor(() => events.some((e) => e.kind === 'context_usage'));
147
+ await session.stop();
148
+ expect(events).toEqual([
149
+ { kind: 'text', text: 'Hel', delta: true },
150
+ { kind: 'text', text: 'lo', delta: true },
151
+ { kind: 'thinking', text: 'hmm', delta: true },
152
+ { kind: 'tool_use', id: 'tu-1', name: 'Read', input: { file_path: '/tmp/ws/a.txt' } },
153
+ { kind: 'tool_result', id: 'tu-1', ok: true, content: 'file body' },
154
+ { kind: 'result', text: 'done', costUsd: 0.0123, tokensIn: 100, tokensOut: 42, durationMs: 4200 },
155
+ { kind: 'context_usage', totalTokens: 1234, maxTokens: 200000, percentage: 0.617 },
156
+ {
157
+ kind: 'error',
158
+ message: 'Rate limit updated.',
159
+ retryable: true,
160
+ rateLimit: {
161
+ resetAt: new Date(1000 * 1000).toISOString(),
162
+ rateLimitType: 'five_hour',
163
+ utilization: 87,
164
+ },
165
+ },
166
+ ]);
167
+ expect(state.contextUsageCalls).toBe(1);
168
+ });
169
+ it('captures sessionId from the init message', async () => {
170
+ const { ctx, events } = makeCtx();
171
+ const { queryFn } = fakeQueryFn(scriptedMessages());
172
+ const session = new ClaudeSession(ctx, { queryFn });
173
+ await waitFor(() => events.some((e) => e.kind === 'result'));
174
+ expect(session.sessionId).toBe('sess-abc');
175
+ await session.stop();
176
+ });
177
+ it('flags a tool_result with is_error:true as ok:false', async () => {
178
+ const { ctx, events } = makeCtx();
179
+ const { queryFn } = fakeQueryFn([
180
+ {
181
+ type: 'user',
182
+ message: {
183
+ role: 'user',
184
+ content: [{ type: 'tool_result', tool_use_id: 'tu-9', content: 'boom', is_error: true }],
185
+ },
186
+ },
187
+ ]);
188
+ const session = new ClaudeSession(ctx, { queryFn });
189
+ await waitFor(() => events.some((e) => e.kind === 'tool_result'));
190
+ expect(events).toContainEqual({ kind: 'tool_result', id: 'tu-9', ok: false, content: 'boom' });
191
+ await session.stop();
192
+ });
193
+ it('turns an SDK stream error into a single retryable error event', async () => {
194
+ const { ctx, events } = makeCtx();
195
+ // A query whose iterator throws mid-stream.
196
+ const query = {
197
+ async *[Symbol.asyncIterator]() {
198
+ yield { type: 'system', subtype: 'init', session_id: 's' };
199
+ throw new Error('stream exploded');
200
+ },
201
+ close() { },
202
+ async getContextUsage() {
203
+ return { totalTokens: 0, maxTokens: 0, percentage: 0 };
204
+ },
205
+ };
206
+ const queryFn = () => query;
207
+ new ClaudeSession(ctx, { queryFn });
208
+ await waitFor(() => events.some((e) => e.kind === 'error'));
209
+ const errs = events.filter((e) => e.kind === 'error');
210
+ expect(errs).toHaveLength(1);
211
+ expect(errs[0]).toMatchObject({ kind: 'error', message: 'stream exploded', retryable: true });
212
+ });
213
+ it('stop() aborts the query and closes it', async () => {
214
+ const { ctx } = makeCtx();
215
+ const { queryFn, captured, state } = fakeQueryFn([], { stallForever: true });
216
+ const session = new ClaudeSession(ctx, { queryFn });
217
+ const options = captured.options;
218
+ expect(options.abortController.signal.aborted).toBe(false);
219
+ await session.stop();
220
+ expect(options.abortController.signal.aborted).toBe(true);
221
+ expect(state.closed).toBe(true);
222
+ });
223
+ it('feeds a sent turn into the SDK prompt stream', async () => {
224
+ const { ctx } = makeCtx();
225
+ const received = [];
226
+ // A query that drains the prompt stream so we can observe the sent turn.
227
+ const query = {
228
+ async *[Symbol.asyncIterator]() {
229
+ // yield nothing; we only need the prompt consumed
230
+ },
231
+ close() { },
232
+ async getContextUsage() {
233
+ return { totalTokens: 0, maxTokens: 0, percentage: 0 };
234
+ },
235
+ };
236
+ const queryFn = ({ prompt }) => {
237
+ void (async () => {
238
+ for await (const msg of prompt) {
239
+ received.push(msg);
240
+ break; // one turn is enough for the assertion
241
+ }
242
+ })();
243
+ return query;
244
+ };
245
+ const session = new ClaudeSession(ctx, { queryFn });
246
+ await session.send({ text: 'hello there' });
247
+ await waitFor(() => received.length > 0);
248
+ expect(received[0]).toMatchObject({
249
+ type: 'user',
250
+ message: { role: 'user', content: 'hello there' },
251
+ });
252
+ await session.stop();
253
+ });
254
+ });
255
+ // ---- query() options: project-config loading --------------------------------
256
+ describe('ClaudeSession — query options', () => {
257
+ it('loads project settings via settingSources and passes cwd/model/permissionMode', async () => {
258
+ const { ctx } = makeCtx({ cwd: '/tmp/proj', model: 'opus', permMode: 'plan' });
259
+ const { queryFn, captured } = fakeQueryFn([]);
260
+ const session = new ClaudeSession(ctx, { queryFn });
261
+ const options = captured.options;
262
+ expect(options.cwd).toBe('/tmp/proj');
263
+ expect(options.model).toBe('opus');
264
+ expect(options.permissionMode).toBe('plan'); // plan passes through
265
+ expect(options.includePartialMessages).toBe(true);
266
+ // The critical option: load user + project + local .claude/ config so
267
+ // subagents/hooks/skills/project-MCP work like the terminal claude.
268
+ expect(options.settingSources).toEqual(['user', 'project', 'local']);
269
+ expect(typeof options.canUseTool).toBe('function');
270
+ await session.stop();
271
+ });
272
+ it('pins the model to the session cwd via the claude_code preset system prompt', async () => {
273
+ // Root cause of the live bug: the SDK forwards cwd to the CLI subprocess, but with
274
+ // an unqualified prompt the model resolves relative paths against $HOME, so files
275
+ // landed in HOME regardless of cwd. The fix appends the working directory to the
276
+ // claude_code preset so relative writes land in ctx.cwd. This asserts the exact
277
+ // option shape the real SDK requires (verified empirically against the live CLI).
278
+ const { ctx } = makeCtx({ cwd: '/tmp/selected-folder' });
279
+ const { queryFn, captured } = fakeQueryFn([]);
280
+ const session = new ClaudeSession(ctx, { queryFn });
281
+ const options = captured.options;
282
+ expect(options.systemPrompt.type).toBe('preset');
283
+ // Preserve the claude_code preset so CLAUDE.md / tools / dynamic sections stay intact.
284
+ expect(options.systemPrompt.preset).toBe('claude_code');
285
+ // The append must name the exact cwd so the model writes there, not to $HOME.
286
+ expect(options.systemPrompt.append).toContain('/tmp/selected-folder');
287
+ expect(options.systemPrompt.append).toMatch(/working directory/i);
288
+ await session.stop();
289
+ });
290
+ it('passes each PermMode natively to the SDK permissionMode', async () => {
291
+ for (const mode of ['default', 'acceptEdits', 'bypassPermissions', 'plan']) {
292
+ const { ctx } = makeCtx({ permMode: mode });
293
+ const { queryFn, captured } = fakeQueryFn([]);
294
+ const session = new ClaudeSession(ctx, { queryFn });
295
+ const options = captured.options;
296
+ expect(options.permissionMode).toBe(mode);
297
+ await session.stop();
298
+ }
299
+ });
300
+ it('sets allowDangerouslySkipPermissions only for bypassPermissions', async () => {
301
+ const { ctx: bypassCtx } = makeCtx({ permMode: 'bypassPermissions' });
302
+ const bypass = fakeQueryFn([]);
303
+ const bypassSession = new ClaudeSession(bypassCtx, { queryFn: bypass.queryFn });
304
+ expect(bypass.captured.options.allowDangerouslySkipPermissions).toBe(true);
305
+ await bypassSession.stop();
306
+ const { ctx: defaultCtx } = makeCtx({ permMode: 'default' });
307
+ const def = fakeQueryFn([]);
308
+ const defaultSession = new ClaudeSession(defaultCtx, { queryFn: def.queryFn });
309
+ expect(def.captured.options.allowDangerouslySkipPermissions).toBeUndefined();
310
+ await defaultSession.stop();
311
+ });
312
+ it('exposes the attach_file MCP tool and allowlists it when sendFile is wired', async () => {
313
+ const { ctx } = makeCtx();
314
+ const { queryFn, captured } = fakeQueryFn([]);
315
+ const session = new ClaudeSession(ctx, {
316
+ queryFn,
317
+ sendFile: async () => 'sent',
318
+ });
319
+ const options = captured.options;
320
+ expect(options.mcpServers?.discord).toBeDefined();
321
+ expect(options.allowedTools).toContain('mcp__discord__attach_file');
322
+ await session.stop();
323
+ });
324
+ });
325
+ // ---- canUseTool bridging ----------------------------------------------------
326
+ describe('makeCanUseTool', () => {
327
+ const opts = { signal: new AbortController().signal, toolUseID: 'tu-x' };
328
+ it('auto-allows a tool in the allowlist without calling requestPermission', async () => {
329
+ const { ctx, permissionCalls } = makeCtx({ allowedTools: ['Read', 'Grep'] });
330
+ const canUse = makeCanUseTool(ctx);
331
+ const result = await canUse('Read', { file_path: '/x' }, opts);
332
+ expect(result).toEqual({ behavior: 'allow', updatedInput: { file_path: '/x' } });
333
+ expect(permissionCalls).toHaveLength(0);
334
+ });
335
+ it('auto-allows a tool in autoAllowClaudeTools without prompting', async () => {
336
+ const { ctx, permissionCalls } = makeCtx({ autoAllowClaudeTools: ['WebFetch'] });
337
+ const canUse = makeCanUseTool(ctx);
338
+ const result = await canUse('WebFetch', {}, opts);
339
+ expect(result.behavior).toBe('allow');
340
+ expect(permissionCalls).toHaveLength(0);
341
+ });
342
+ it('prompts via requestPermission for a tool not on the allowlist and honors allow', async () => {
343
+ const { ctx, permissionCalls } = makeCtx({
344
+ allowedTools: ['Read'],
345
+ permissionDecision: { behavior: 'allow' },
346
+ });
347
+ const canUse = makeCanUseTool(ctx);
348
+ const result = await canUse('Bash', { command: 'ls' }, opts);
349
+ expect(permissionCalls).toEqual([{ toolName: 'Bash', input: { command: 'ls' } }]);
350
+ expect(result).toEqual({ behavior: 'allow', updatedInput: { command: 'ls' } });
351
+ });
352
+ it('honors a deny decision and carries its message', async () => {
353
+ const { ctx, permissionCalls } = makeCtx({
354
+ permissionDecision: { behavior: 'deny', message: 'nope' },
355
+ });
356
+ const canUse = makeCanUseTool(ctx);
357
+ const result = await canUse('Bash', { command: 'rm -rf /' }, opts);
358
+ expect(permissionCalls).toHaveLength(1);
359
+ expect(result).toEqual({ behavior: 'deny', message: 'nope' });
360
+ });
361
+ });
362
+ // ---- Capabilities (§5a) -----------------------------------------------------
363
+ describe('ClaudeMode capabilities', () => {
364
+ it('match the §5a Claude capability shape', () => {
365
+ const mode = new ClaudeMode();
366
+ expect(mode.name).toBe('claude');
367
+ expect(mode.capabilities).toEqual({
368
+ streaming: true,
369
+ thinking: true,
370
+ toolThreads: true,
371
+ permissionPrompts: true,
372
+ progress: false,
373
+ transcript: false,
374
+ sessionResume: true,
375
+ fileAttach: true,
376
+ fileDiff: true,
377
+ usagePanel: true,
378
+ // Full SDK-synced permission-mode set (incl. dontAsk/auto) from providerCatalog.
379
+ permissionModes: ['default', 'acceptEdits', 'bypassPermissions', 'plan', 'dontAsk', 'auto'],
380
+ });
381
+ });
382
+ });
383
+ // ---- mcpFileTool confinement ------------------------------------------------
384
+ describe('attachFileConfined — path confinement', () => {
385
+ let root;
386
+ beforeEach(() => {
387
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'dab-mcp-'));
388
+ });
389
+ afterEach(() => {
390
+ fs.rmSync(root, { recursive: true, force: true });
391
+ });
392
+ it('accepts a path inside the workspace and forwards it to sendFile', async () => {
393
+ const inside = path.join(root, 'report.txt');
394
+ fs.writeFileSync(inside, 'hi');
395
+ const forwarded = [];
396
+ const res = await attachFileConfined(root, async (abs) => {
397
+ forwarded.push(abs);
398
+ return 'ok';
399
+ }, 'report.txt');
400
+ expect(res.isError).toBeFalsy();
401
+ expect(forwarded).toHaveLength(1);
402
+ expect(fs.realpathSync(forwarded[0])).toBe(fs.realpathSync(inside));
403
+ });
404
+ it('rejects a path that escapes the workspace and never calls sendFile', async () => {
405
+ let called = false;
406
+ const res = await attachFileConfined(root, async () => {
407
+ called = true;
408
+ return 'ok';
409
+ }, '../../etc/passwd');
410
+ expect(res.isError).toBe(true);
411
+ expect(res.content[0].text).toMatch(/outside the session workspace/);
412
+ expect(called).toBe(false);
413
+ });
414
+ });
415
+ //# sourceMappingURL=session.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.test.js","sourceRoot":"","sources":["../../../src/modes/claude/session.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,221 @@
1
+ import { readFile as realReadFile, readdir as realReaddir } from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { CodexSqliteReader, } from './sqliteReader.js';
4
+ const realFs = {
5
+ readFile: (p) => realReadFile(p, 'utf8'),
6
+ readdir: (dir) => realReaddir(dir),
7
+ };
8
+ // The rollout filename UUID is the last 36 chars before .jsonl.
9
+ const ROLLOUT_UUID = /-([0-9a-f-]{36})\.jsonl$/i;
10
+ // Any 36-char UUID embedded in an archived filename (its naming may differ from a
11
+ // live rollout, so match the UUID anywhere, not just before .jsonl).
12
+ const ANY_UUID = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
13
+ export class CodexDiscovery {
14
+ reader;
15
+ fs;
16
+ logger;
17
+ constructor(options = {}) {
18
+ this.reader = options.reader ?? new CodexSqliteReader();
19
+ this.fs = options.fs ?? realFs;
20
+ if (options.logger)
21
+ this.logger = options.logger;
22
+ }
23
+ async listResumable(codexHome, opts = {}) {
24
+ const entries = await this.readSessionIndex(codexHome);
25
+ if (entries.length === 0)
26
+ return [];
27
+ const cwdById = await this.readRolloutCwds(codexHome);
28
+ // Try to read thread state; a failure (missing/corrupt db) means we degrade
29
+ // to index-only with no filtering (fallback rule).
30
+ let states = null;
31
+ try {
32
+ states = await this.reader.readThreadStates(codexHome);
33
+ }
34
+ catch (error) {
35
+ this.logger?.warn('Codex state db unreadable; falling back to session_index.jsonl (no archived/sub-agent filtering)', error);
36
+ states = null;
37
+ }
38
+ const stateAvailable = states !== null && states.rows.length > 0;
39
+ const rowById = new Map();
40
+ if (states) {
41
+ for (const row of states.rows)
42
+ rowById.set(row.id, row);
43
+ }
44
+ const subAgentChildIds = states?.subAgentChildIds ?? new Set();
45
+ // In the index-only fallback (sqlite unavailable → no `archived` column to
46
+ // read), still exclude ids whose UUID appears under ~/.codex/archived_sessions/
47
+ // so archived threads don't reappear when the db can't be read (P2-2 Q3).
48
+ // Best-effort: a missing/unreadable dir yields an empty set (tolerate ENOENT).
49
+ const archivedIds = stateAvailable ? new Set() : await this.readArchivedIds(codexHome);
50
+ const sessions = [];
51
+ for (const entry of entries) {
52
+ const row = rowById.get(entry.id);
53
+ if (stateAvailable) {
54
+ // Fail-safe exclude: state is available but this session has no thread row.
55
+ if (!row)
56
+ continue;
57
+ if (row.archived)
58
+ continue;
59
+ if (!opts.includeSubAgents && isSubAgent(entry.id, row, subAgentChildIds))
60
+ continue;
61
+ // Non-interactive sources are not resumable user threads.
62
+ if (row.source === 'exec')
63
+ continue;
64
+ }
65
+ else if (archivedIds.has(entry.id)) {
66
+ // Fallback path: the db is unreadable but the file layout still tells us
67
+ // this thread was archived — keep it excluded.
68
+ continue;
69
+ }
70
+ const cwd = row?.cwd ?? cwdById.get(entry.id) ?? '';
71
+ const label = row?.title || row?.preview || entry.threadName || entry.id;
72
+ const session = { sessionId: entry.id, cwd };
73
+ if (label)
74
+ session.label = label;
75
+ if (entry.updatedAt)
76
+ session.updatedAt = entry.updatedAt;
77
+ sessions.push(session);
78
+ }
79
+ sessions.sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
80
+ return sessions;
81
+ }
82
+ async readSessionIndex(codexHome) {
83
+ const indexPath = path.join(codexHome, 'session_index.jsonl');
84
+ let text;
85
+ try {
86
+ text = await this.fs.readFile(indexPath);
87
+ }
88
+ catch (error) {
89
+ if (isEnoent(error))
90
+ return [];
91
+ throw error;
92
+ }
93
+ const entries = [];
94
+ for (const line of text.split('\n')) {
95
+ if (!line.trim())
96
+ continue;
97
+ const entry = parseSessionIndexLine(line);
98
+ if (entry)
99
+ entries.push(entry);
100
+ }
101
+ return entries;
102
+ }
103
+ // Walk sessions/YYYY/MM/DD/rollout-*.jsonl; map each rollout UUID → its
104
+ // session_meta.payload.cwd (read from the first line only).
105
+ async readRolloutCwds(codexHome) {
106
+ const cwdById = new Map();
107
+ const files = await this.listRolloutFiles(path.join(codexHome, 'sessions'));
108
+ await Promise.all(files.map(async (file) => {
109
+ const match = ROLLOUT_UUID.exec(path.basename(file));
110
+ if (!match)
111
+ return;
112
+ const cwd = await this.readSessionMetaCwd(file);
113
+ if (cwd)
114
+ cwdById.set(match[1], cwd);
115
+ }));
116
+ return cwdById;
117
+ }
118
+ async listRolloutFiles(root) {
119
+ let names;
120
+ try {
121
+ names = await this.fs.readdir(root);
122
+ }
123
+ catch (error) {
124
+ if (isEnoent(error))
125
+ return [];
126
+ throw error;
127
+ }
128
+ const nested = await Promise.all(names.map(async (name) => {
129
+ const full = path.join(root, name);
130
+ if (name.endsWith('.jsonl'))
131
+ return [full];
132
+ // Not a jsonl file → treat as a nested directory (YYYY/MM/DD); ENOENT and
133
+ // ENOTDIR (a stray non-jsonl file) both collapse to no results.
134
+ return this.listRolloutFiles(full).catch((error) => isEnoent(error) || isNotDir(error) ? [] : Promise.reject(error));
135
+ }));
136
+ return nested.flat();
137
+ }
138
+ async readSessionMetaCwd(file) {
139
+ let text;
140
+ try {
141
+ text = await this.fs.readFile(file);
142
+ }
143
+ catch (error) {
144
+ if (isEnoent(error))
145
+ return undefined;
146
+ throw error;
147
+ }
148
+ const firstLine = text.split('\n', 1)[0];
149
+ if (!firstLine)
150
+ return undefined;
151
+ return parseSessionMetaCwd(firstLine);
152
+ }
153
+ // Best-effort scan of ~/.codex/archived_sessions/ for the UUIDs of archived
154
+ // threads (used only in the index-only fallback). Walks the tree the same way as
155
+ // rollout files and collects every 36-char UUID found in a filename. A missing
156
+ // dir (ENOENT) or any other read error yields an empty set — this must never
157
+ // fail discovery, since it is only a fallback refinement.
158
+ async readArchivedIds(codexHome) {
159
+ const ids = new Set();
160
+ let files;
161
+ try {
162
+ files = await this.listRolloutFiles(path.join(codexHome, 'archived_sessions'));
163
+ }
164
+ catch {
165
+ return ids;
166
+ }
167
+ for (const file of files) {
168
+ const match = ANY_UUID.exec(path.basename(file));
169
+ if (match)
170
+ ids.add(match[1]);
171
+ }
172
+ return ids;
173
+ }
174
+ }
175
+ // A session id is a sub-agent when its source is a {"subagent":…} blob OR it
176
+ // appears as a spawn edge's child_thread_id.
177
+ function isSubAgent(id, row, subAgentChildIds) {
178
+ if (subAgentChildIds.has(id))
179
+ return true;
180
+ return (row.source ?? '').trimStart().startsWith('{"subagent"');
181
+ }
182
+ function parseSessionIndexLine(line) {
183
+ try {
184
+ const parsed = JSON.parse(line);
185
+ if (typeof parsed.id !== 'string' || parsed.id.length === 0)
186
+ return null;
187
+ return {
188
+ id: parsed.id,
189
+ threadName: typeof parsed.thread_name === 'string' ? parsed.thread_name : '',
190
+ updatedAt: typeof parsed.updated_at === 'string' ? parsed.updated_at : '',
191
+ };
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ }
197
+ function parseSessionMetaCwd(line) {
198
+ try {
199
+ const parsed = JSON.parse(line);
200
+ if (parsed.type !== 'session_meta')
201
+ return undefined;
202
+ const cwd = parsed.payload?.cwd;
203
+ return typeof cwd === 'string' && cwd.length > 0 ? cwd : undefined;
204
+ }
205
+ catch {
206
+ return undefined;
207
+ }
208
+ }
209
+ function isEnoent(error) {
210
+ return hasCode(error, 'ENOENT');
211
+ }
212
+ function isNotDir(error) {
213
+ return hasCode(error, 'ENOTDIR');
214
+ }
215
+ function hasCode(error, code) {
216
+ return (typeof error === 'object' &&
217
+ error !== null &&
218
+ 'code' in error &&
219
+ error.code === code);
220
+ }
221
+ //# sourceMappingURL=discovery.js.map