@theia/ai-claude-code 1.65.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 (128) hide show
  1. package/README.md +30 -0
  2. package/lib/browser/claude-code-chat-agent.d.ts +82 -0
  3. package/lib/browser/claude-code-chat-agent.d.ts.map +1 -0
  4. package/lib/browser/claude-code-chat-agent.js +518 -0
  5. package/lib/browser/claude-code-chat-agent.js.map +1 -0
  6. package/lib/browser/claude-code-command-contribution.d.ts +16 -0
  7. package/lib/browser/claude-code-command-contribution.d.ts.map +1 -0
  8. package/lib/browser/claude-code-command-contribution.js +86 -0
  9. package/lib/browser/claude-code-command-contribution.js.map +1 -0
  10. package/lib/browser/claude-code-edit-tool-service.d.ts +61 -0
  11. package/lib/browser/claude-code-edit-tool-service.d.ts.map +1 -0
  12. package/lib/browser/claude-code-edit-tool-service.js +219 -0
  13. package/lib/browser/claude-code-edit-tool-service.js.map +1 -0
  14. package/lib/browser/claude-code-file-edit-backup-service.d.ts +57 -0
  15. package/lib/browser/claude-code-file-edit-backup-service.d.ts.map +1 -0
  16. package/lib/browser/claude-code-file-edit-backup-service.js +92 -0
  17. package/lib/browser/claude-code-file-edit-backup-service.js.map +1 -0
  18. package/lib/browser/claude-code-frontend-module.d.ts +5 -0
  19. package/lib/browser/claude-code-frontend-module.d.ts.map +1 -0
  20. package/lib/browser/claude-code-frontend-module.js +83 -0
  21. package/lib/browser/claude-code-frontend-module.js.map +1 -0
  22. package/lib/browser/claude-code-frontend-service.d.ts +40 -0
  23. package/lib/browser/claude-code-frontend-service.d.ts.map +1 -0
  24. package/lib/browser/claude-code-frontend-service.js +190 -0
  25. package/lib/browser/claude-code-frontend-service.js.map +1 -0
  26. package/lib/browser/claude-code-slash-commands-contribution.d.ts +17 -0
  27. package/lib/browser/claude-code-slash-commands-contribution.d.ts.map +1 -0
  28. package/lib/browser/claude-code-slash-commands-contribution.js +154 -0
  29. package/lib/browser/claude-code-slash-commands-contribution.js.map +1 -0
  30. package/lib/browser/claude-code-tool-call-content.d.ts +8 -0
  31. package/lib/browser/claude-code-tool-call-content.d.ts.map +1 -0
  32. package/lib/browser/claude-code-tool-call-content.js +30 -0
  33. package/lib/browser/claude-code-tool-call-content.js.map +1 -0
  34. package/lib/browser/renderers/bash-tool-renderer.d.ts +10 -0
  35. package/lib/browser/renderers/bash-tool-renderer.d.ts.map +1 -0
  36. package/lib/browser/renderers/bash-tool-renderer.js +71 -0
  37. package/lib/browser/renderers/bash-tool-renderer.js.map +1 -0
  38. package/lib/browser/renderers/collapsible-tool-renderer.d.ts +13 -0
  39. package/lib/browser/renderers/collapsible-tool-renderer.d.ts.map +1 -0
  40. package/lib/browser/renderers/collapsible-tool-renderer.js +48 -0
  41. package/lib/browser/renderers/collapsible-tool-renderer.js.map +1 -0
  42. package/lib/browser/renderers/edit-tool-renderer.d.ts +16 -0
  43. package/lib/browser/renderers/edit-tool-renderer.d.ts.map +1 -0
  44. package/lib/browser/renderers/edit-tool-renderer.js +134 -0
  45. package/lib/browser/renderers/edit-tool-renderer.js.map +1 -0
  46. package/lib/browser/renderers/glob-tool-renderer.d.ts +14 -0
  47. package/lib/browser/renderers/glob-tool-renderer.d.ts.map +1 -0
  48. package/lib/browser/renderers/glob-tool-renderer.js +107 -0
  49. package/lib/browser/renderers/glob-tool-renderer.js.map +1 -0
  50. package/lib/browser/renderers/grep-tool-renderer.d.ts +14 -0
  51. package/lib/browser/renderers/grep-tool-renderer.d.ts.map +1 -0
  52. package/lib/browser/renderers/grep-tool-renderer.js +157 -0
  53. package/lib/browser/renderers/grep-tool-renderer.js.map +1 -0
  54. package/lib/browser/renderers/ls-tool-renderer.d.ts +16 -0
  55. package/lib/browser/renderers/ls-tool-renderer.d.ts.map +1 -0
  56. package/lib/browser/renderers/ls-tool-renderer.js +116 -0
  57. package/lib/browser/renderers/ls-tool-renderer.js.map +1 -0
  58. package/lib/browser/renderers/multiedit-tool-renderer.d.ts +16 -0
  59. package/lib/browser/renderers/multiedit-tool-renderer.d.ts.map +1 -0
  60. package/lib/browser/renderers/multiedit-tool-renderer.js +152 -0
  61. package/lib/browser/renderers/multiedit-tool-renderer.js.map +1 -0
  62. package/lib/browser/renderers/read-tool-renderer.d.ts +16 -0
  63. package/lib/browser/renderers/read-tool-renderer.d.ts.map +1 -0
  64. package/lib/browser/renderers/read-tool-renderer.js +121 -0
  65. package/lib/browser/renderers/read-tool-renderer.js.map +1 -0
  66. package/lib/browser/renderers/todo-write-renderer.d.ts +10 -0
  67. package/lib/browser/renderers/todo-write-renderer.d.ts.map +1 -0
  68. package/lib/browser/renderers/todo-write-renderer.js +132 -0
  69. package/lib/browser/renderers/todo-write-renderer.js.map +1 -0
  70. package/lib/browser/renderers/web-fetch-tool-renderer.d.ts +10 -0
  71. package/lib/browser/renderers/web-fetch-tool-renderer.d.ts.map +1 -0
  72. package/lib/browser/renderers/web-fetch-tool-renderer.js +82 -0
  73. package/lib/browser/renderers/web-fetch-tool-renderer.js.map +1 -0
  74. package/lib/browser/renderers/write-tool-renderer.d.ts +16 -0
  75. package/lib/browser/renderers/write-tool-renderer.d.ts.map +1 -0
  76. package/lib/browser/renderers/write-tool-renderer.js +113 -0
  77. package/lib/browser/renderers/write-tool-renderer.js.map +1 -0
  78. package/lib/common/claude-code-preferences.d.ts +4 -0
  79. package/lib/common/claude-code-preferences.d.ts.map +1 -0
  80. package/lib/common/claude-code-preferences.js +33 -0
  81. package/lib/common/claude-code-preferences.js.map +1 -0
  82. package/lib/common/claude-code-service.d.ts +231 -0
  83. package/lib/common/claude-code-service.d.ts.map +1 -0
  84. package/lib/common/claude-code-service.js +82 -0
  85. package/lib/common/claude-code-service.js.map +1 -0
  86. package/lib/common/index.d.ts +2 -0
  87. package/lib/common/index.d.ts.map +1 -0
  88. package/lib/common/index.js +20 -0
  89. package/lib/common/index.js.map +1 -0
  90. package/lib/node/claude-code-backend-module.d.ts +4 -0
  91. package/lib/node/claude-code-backend-module.d.ts.map +1 -0
  92. package/lib/node/claude-code-backend-module.js +35 -0
  93. package/lib/node/claude-code-backend-module.js.map +1 -0
  94. package/lib/node/claude-code-service-impl.d.ts +32 -0
  95. package/lib/node/claude-code-service-impl.d.ts.map +1 -0
  96. package/lib/node/claude-code-service-impl.js +426 -0
  97. package/lib/node/claude-code-service-impl.js.map +1 -0
  98. package/lib/package.spec.d.ts +1 -0
  99. package/lib/package.spec.d.ts.map +1 -0
  100. package/lib/package.spec.js +26 -0
  101. package/lib/package.spec.js.map +1 -0
  102. package/package.json +57 -0
  103. package/src/browser/claude-code-chat-agent.ts +591 -0
  104. package/src/browser/claude-code-command-contribution.ts +80 -0
  105. package/src/browser/claude-code-edit-tool-service.ts +313 -0
  106. package/src/browser/claude-code-file-edit-backup-service.ts +141 -0
  107. package/src/browser/claude-code-frontend-module.ts +100 -0
  108. package/src/browser/claude-code-frontend-service.ts +215 -0
  109. package/src/browser/claude-code-slash-commands-contribution.ts +175 -0
  110. package/src/browser/claude-code-tool-call-content.ts +30 -0
  111. package/src/browser/renderers/bash-tool-renderer.tsx +97 -0
  112. package/src/browser/renderers/collapsible-tool-renderer.tsx +78 -0
  113. package/src/browser/renderers/edit-tool-renderer.tsx +180 -0
  114. package/src/browser/renderers/glob-tool-renderer.tsx +136 -0
  115. package/src/browser/renderers/grep-tool-renderer.tsx +190 -0
  116. package/src/browser/renderers/ls-tool-renderer.tsx +160 -0
  117. package/src/browser/renderers/multiedit-tool-renderer.tsx +204 -0
  118. package/src/browser/renderers/read-tool-renderer.tsx +170 -0
  119. package/src/browser/renderers/todo-write-renderer.tsx +178 -0
  120. package/src/browser/renderers/web-fetch-tool-renderer.tsx +108 -0
  121. package/src/browser/renderers/write-tool-renderer.tsx +155 -0
  122. package/src/browser/style/claude-code-tool-renderers.css +487 -0
  123. package/src/common/claude-code-preferences.ts +33 -0
  124. package/src/common/claude-code-service.ts +303 -0
  125. package/src/common/index.ts +17 -0
  126. package/src/node/claude-code-backend-module.ts +42 -0
  127. package/src/node/claude-code-service-impl.ts +462 -0
  128. package/src/package.spec.ts +27 -0
@@ -0,0 +1,462 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { ILogger, generateUuid } from '@theia/core';
18
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
19
+ import { execSync } from 'child_process';
20
+ import { existsSync, realpathSync } from 'fs';
21
+ import * as fs from 'fs/promises';
22
+ import * as path from 'path';
23
+ import {
24
+ ClaudeCodeBackendRequest,
25
+ ClaudeCodeClient,
26
+ ClaudeCodeService,
27
+ ToolApprovalRequestMessage,
28
+ ToolApprovalResponseMessage
29
+ } from '../common/claude-code-service';
30
+
31
+ interface ToolApprovalResult {
32
+ behavior: 'allow' | 'deny';
33
+ message?: string;
34
+ updatedInput?: unknown;
35
+ }
36
+
37
+ @injectable()
38
+ export class ClaudeCodeServiceImpl implements ClaudeCodeService {
39
+
40
+ @inject(ILogger) @named('ClaudeCode')
41
+ private logger: ILogger;
42
+
43
+ private client: ClaudeCodeClient;
44
+ private abortControllers = new Map<string, AbortController>();
45
+ private pendingApprovals = new Map<string, (result: ToolApprovalResult) => void>();
46
+
47
+ setClient(client: ClaudeCodeClient): void {
48
+ this.client = client;
49
+ }
50
+
51
+ async send(request: ClaudeCodeBackendRequest, streamId: string): Promise<void> {
52
+ if (!this.client) {
53
+ throw new Error('Claude Code client not initialized');
54
+ }
55
+ this.sendMessages(streamId, request);
56
+ }
57
+
58
+ protected async sendMessages(streamId: string, request: ClaudeCodeBackendRequest): Promise<void> {
59
+ const abortController = new AbortController();
60
+ this.abortControllers.set(streamId, abortController);
61
+
62
+ try {
63
+ const cwd = request.options?.cwd || process.cwd();
64
+ await this.ensureFileBackupHook(cwd);
65
+ await this.ensureStopHook(cwd);
66
+ await this.ensureClaudeSettings(cwd);
67
+
68
+ let done = (_?: unknown) => { };
69
+ const receivedResult = new Promise(resolve => {
70
+ done = resolve;
71
+ });
72
+
73
+ const apiKey = request.apiKey || process.env.ANTHROPIC_API_KEY;
74
+ const { query, SDKUserMessage, Options } = await this.importClaudeCodeSDK(request.claudeCodePath);
75
+
76
+ const stream = (query as Function)({
77
+ prompt: (async function* (): AsyncGenerator<typeof SDKUserMessage> {
78
+ yield {
79
+ type: 'user',
80
+ message: {
81
+ role: 'user',
82
+ content: request.prompt
83
+ },
84
+ // eslint-disable-next-line no-null/no-null
85
+ parent_tool_use_id: null,
86
+ session_id: generateUuid()
87
+ };
88
+ await receivedResult;
89
+ })(),
90
+ options: <typeof Options>{
91
+ ...request.options,
92
+ abortController,
93
+ canUseTool: (toolName: string, toolInput: unknown) => this.requestToolApproval(streamId, toolName, toolInput),
94
+ env: { ...process.env, ANTHROPIC_API_KEY: apiKey, NODE_OPTIONS: '' },
95
+ stderr: (data: unknown) => {
96
+ let message = String(data);
97
+
98
+ // The current claude code CLI is quite verbose and outputs a lot of
99
+ // non-error info to stderr when spawning.
100
+ // We check for this and log it at debug level instead.
101
+ if (message.startsWith('Spawning Claude Code process:')) {
102
+ // Strip the system prompt if present
103
+ if (request.options?.appendSystemPrompt) {
104
+ const systemPrompt = request.options.appendSystemPrompt;
105
+ message = message.replace(systemPrompt, '').trim();
106
+ }
107
+
108
+ // Check if the remainder looks like it contains an actual error
109
+ if (message.toLowerCase().includes('error') || message.toLowerCase().includes('failed')) {
110
+ this.logger.error('Claude Code Std Error:', message);
111
+ } else {
112
+ this.logger.debug('Claude Code Verbose Output:', message);
113
+ }
114
+ return;
115
+ }
116
+
117
+ // Log all other content as error (actual errors)
118
+ this.logger.error('Claude Code Std Error:', message);
119
+ },
120
+ }
121
+ });
122
+
123
+ for await (const message of stream) {
124
+ this.client.sendToken(streamId, message);
125
+ if (message.type === 'result' || abortController.signal.aborted) {
126
+ done();
127
+ break;
128
+ }
129
+ }
130
+ // Signal stream completion by returning undefined
131
+ abortController.abort('closed after result');
132
+ this.client.sendToken(streamId, undefined);
133
+ } catch (e) {
134
+ this.logger.error('Claude Code error:', e);
135
+ this.client.sendError(streamId, e instanceof Error ? e : new Error(String(e)));
136
+ } finally {
137
+ this.abortControllers.delete(streamId);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Dynamically imports the Claude Code SDK from the local installation.
143
+ * @param customClaudeCodePath Optional custom path to Claude Code executable (cli.js)
144
+ * @returns An object containing the SDK's query function, user message type, and options type.
145
+ */
146
+ protected async importClaudeCodeSDK(customClaudeCodePath?: string): Promise<{ query: unknown; SDKUserMessage: unknown; Options: unknown }> {
147
+ let claudeCodePath: string;
148
+
149
+ if (customClaudeCodePath) {
150
+ if (!existsSync(customClaudeCodePath)) {
151
+ throw new Error(`Specified Claude Code executable not found at: ${customClaudeCodePath}`);
152
+ }
153
+ const realPath = realpathSync(customClaudeCodePath);
154
+ // Use the directory containing the cli.js file
155
+ claudeCodePath = path.dirname(realPath);
156
+ } else {
157
+ claudeCodePath = this.resolveClaudeCodePath();
158
+ }
159
+
160
+ const sdkPath = path.join(claudeCodePath, 'sdk.mjs');
161
+
162
+ // Check if file exists before importing
163
+ if (!existsSync(sdkPath)) {
164
+ throw new Error('Claude Code installation not found. ' +
165
+ 'Please install with: `npm install -g @anthropic-ai/claude-code` ' +
166
+ 'and/or specify the path to the executable in the settings. ' +
167
+ `We looked at ${sdkPath}`);
168
+ }
169
+
170
+ const importPath = `file://${sdkPath}`;
171
+ // We can not use dynamic import directly because webpack will try to
172
+ // bundle the module at build time, which we don't want.
173
+ // We also can't use a webpack ignore comment because the comment is stripped
174
+ // during the build and then webpack still tries to resolve the module.
175
+ const dynamicImport = new Function('path', 'return import(path)');
176
+ return dynamicImport(importPath);
177
+ }
178
+
179
+ protected resolveClaudeCodePath(): string {
180
+ try {
181
+ const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();
182
+ return path.join(globalPath, '@anthropic-ai/claude-code');
183
+ } catch (error) {
184
+ this.logger.error('Failed to resolve global npm path:', error);
185
+ throw new Error('Claude Code installation not found. ' +
186
+ 'Please install with: `npm install -g @anthropic-ai/claude-code` ' +
187
+ 'and/or specify the path to the executable in the settings.');
188
+ }
189
+ }
190
+
191
+ cancel(streamId: string): void {
192
+ const abortController = this.abortControllers.get(streamId);
193
+ if (abortController) {
194
+ abortController.abort('user canceled');
195
+ this.abortControllers.delete(streamId);
196
+ }
197
+ }
198
+
199
+ handleApprovalResponse(response: ToolApprovalResponseMessage): void {
200
+ const resolver = this.pendingApprovals.get(response.requestId);
201
+ if (resolver) {
202
+ resolver({
203
+ behavior: response.approved ? 'allow' : 'deny',
204
+ message: response.message,
205
+ updatedInput: response.updatedInput
206
+ });
207
+ }
208
+ }
209
+
210
+ protected async requestToolApproval(streamId: string, toolName: string, toolInput: unknown): Promise<{ behavior: 'allow' | 'deny', message?: string, updatedInput?: unknown }> {
211
+ this.logger.info('Requesting tool approval:', toolName, toolInput);
212
+
213
+ const requestId = generateUuid();
214
+ const approvalRequest: ToolApprovalRequestMessage = {
215
+ type: 'tool-approval-request',
216
+ toolName,
217
+ toolInput,
218
+ requestId
219
+ };
220
+
221
+ this.client.sendToken(streamId, approvalRequest);
222
+
223
+ const approvalPromise = new Promise<ToolApprovalResult>(resolve => {
224
+ this.pendingApprovals.set(requestId, resolve);
225
+ });
226
+
227
+ const result = await approvalPromise;
228
+ this.pendingApprovals.delete(requestId);
229
+
230
+ return result;
231
+ }
232
+
233
+ protected async ensureStopHook(cwd: string): Promise<void> {
234
+ const hookPath = path.join(cwd, '.claude', 'hooks', 'session-cleanup-hook.js');
235
+
236
+ try {
237
+ await fs.access(hookPath);
238
+ return;
239
+ } catch {
240
+ // Hook doesn't exist, create it
241
+ }
242
+
243
+ await fs.mkdir(path.dirname(hookPath), { recursive: true });
244
+
245
+ const hookContent = `#!/usr/bin/env node
246
+
247
+ const fs = require('fs').promises;
248
+ const path = require('path');
249
+
250
+ async function main() {
251
+ try {
252
+ const input = await new Promise((resolve, reject) => {
253
+ let data = '';
254
+ process.stdin.on('data', chunk => data += chunk);
255
+ process.stdin.on('end', () => resolve(data));
256
+ process.stdin.on('error', reject);
257
+ });
258
+
259
+ const hookData = JSON.parse(input);
260
+
261
+ // Delete backup directory for this session
262
+ const backupDir = path.join(hookData.cwd, '.claude', '.edit-baks', hookData.session_id);
263
+
264
+ try {
265
+ await fs.rm(backupDir, { recursive: true, force: true });
266
+ console.log(\`Cleaned up session backups: \${hookData.session_id}\`);
267
+ } catch (error) {
268
+ // Directory might not exist, which is fine
269
+ console.log(\`No backups to clean for session: \${hookData.session_id}\`);
270
+ }
271
+
272
+ } catch (error) {
273
+ console.error(\`Cleanup failed: \${error.message}\`, process.stderr);
274
+ process.exit(1);
275
+ }
276
+ }
277
+
278
+ main();
279
+ `;
280
+
281
+ await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
282
+ }
283
+
284
+ protected async ensureFileBackupHook(cwd: string): Promise<void> {
285
+ const hookPath = path.join(cwd, '.claude', 'hooks', 'file-backup-hook.js');
286
+
287
+ try {
288
+ await fs.access(hookPath);
289
+ // Hook already exists, no need to create it
290
+ return;
291
+ } catch {
292
+ // Hook doesn't exist, create it
293
+ }
294
+
295
+ // Ensure the hooks directory exists
296
+ await fs.mkdir(path.dirname(hookPath), { recursive: true });
297
+
298
+ const hookContent = `#!/usr/bin/env node
299
+
300
+ const fs = require('fs').promises;
301
+ const path = require('path');
302
+
303
+ async function main() {
304
+ try {
305
+ // Read input from stdin
306
+ const input = await new Promise((resolve, reject) => {
307
+ let data = '';
308
+ process.stdin.on('data', chunk => data += chunk);
309
+ process.stdin.on('end', () => resolve(data));
310
+ process.stdin.on('error', reject);
311
+ });
312
+
313
+ const hookData = JSON.parse(input);
314
+
315
+ // Only backup for file modification tools
316
+ const fileModifyingTools = ['Write', 'Edit', 'MultiEdit'];
317
+ if (!fileModifyingTools.includes(hookData.tool_name)) {
318
+ process.exit(0);
319
+ }
320
+
321
+ // Extract file path from tool input
322
+ let filePath;
323
+ if (hookData.tool_name === 'Write' || hookData.tool_name === 'Edit') {
324
+ filePath = hookData.tool_input?.file_path;
325
+ } else if (hookData.tool_name === 'MultiEdit') {
326
+ // MultiEdit has multiple files - we'll handle the first one for now
327
+ // You might want to extend this to handle all files
328
+ filePath = hookData.tool_input?.files?.[0]?.file_path;
329
+ }
330
+
331
+ if (!filePath) {
332
+ process.exit(0);
333
+ }
334
+
335
+ // Resolve absolute path
336
+ const absoluteFilePath = path.resolve(hookData.cwd, filePath);
337
+
338
+ // Check if file exists (can't backup what doesn't exist)
339
+ try {
340
+ await fs.access(absoluteFilePath);
341
+ } catch {
342
+ // File doesn't exist, nothing to backup
343
+ process.exit(0);
344
+ }
345
+
346
+ // Create backup directory structure
347
+ const backupDir = path.join(hookData.cwd, '.claude', '.edit-baks', hookData.session_id);
348
+ await fs.mkdir(backupDir, { recursive: true });
349
+
350
+ // Create backup file path (maintain relative structure)
351
+ const relativePath = path.relative(hookData.cwd, absoluteFilePath);
352
+ const backupFilePath = path.join(backupDir, relativePath);
353
+
354
+ // Ensure backup subdirectories exist
355
+ await fs.mkdir(path.dirname(backupFilePath), { recursive: true });
356
+
357
+ // Only create backup if it doesn't already exist for this session
358
+ try {
359
+ await fs.access(backupFilePath);
360
+ // Backup already exists for this session, don't overwrite
361
+ process.exit(0);
362
+ } catch {
363
+ // Backup doesn't exist, create it
364
+ }
365
+
366
+ // Copy the file
367
+ await fs.copyFile(absoluteFilePath, backupFilePath);
368
+
369
+ // Optional: Log the backup (visible in transcript mode with Ctrl-R)
370
+ console.log(\`Backed up: \${relativePath}\`);
371
+
372
+ } catch (error) {
373
+ console.error(\`Backup failed: \${error.message}\`, process.stderr);
374
+ process.exit(1); // Non-blocking error
375
+ }
376
+ }
377
+
378
+ main();
379
+ `;
380
+
381
+ await fs.writeFile(hookPath, hookContent, { mode: 0o755 });
382
+ }
383
+
384
+ private async ensureClaudeSettings(cwd: string): Promise<void> {
385
+ const settingsPath = path.join(cwd, '.claude', 'settings.local.json');
386
+
387
+ const hookConfig = {
388
+ hooks: {
389
+ PreToolUse: [
390
+ {
391
+ matcher: 'Write|Edit|MultiEdit',
392
+ hooks: [
393
+ {
394
+ type: 'command',
395
+ command: 'node $CLAUDE_PROJECT_DIR/.claude/hooks/file-backup-hook.js',
396
+ timeout: 10
397
+ }
398
+ ]
399
+ }
400
+ ],
401
+ Stop: [
402
+ {
403
+ matcher: '',
404
+ hooks: [
405
+ {
406
+ type: 'command',
407
+ command: 'node $CLAUDE_PROJECT_DIR/.claude/hooks/session-cleanup-hook.js'
408
+ }
409
+ ]
410
+ }
411
+ ]
412
+ }
413
+ };
414
+
415
+ try {
416
+ // Try to read existing settings
417
+ const existingContent = await fs.readFile(settingsPath, 'utf8');
418
+ const existingSettings = JSON.parse(existingContent);
419
+
420
+ // Check if hooks already exist and are properly configured
421
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
422
+ const hasPreHook = existingSettings.hooks?.PreToolUse?.some((hook: any) =>
423
+ hook.matcher === 'Write|Edit|MultiEdit' &&
424
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
425
+ hook.hooks?.some((h: any) => h.command?.includes('file-backup-hook.js'))
426
+ );
427
+
428
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
429
+ const hasStopHook = existingSettings.hooks?.Stop?.some((hook: any) =>
430
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
431
+ hook.hooks?.some((h: any) => h.command?.includes('session-cleanup-hook.js'))
432
+ );
433
+
434
+ if (hasPreHook && hasStopHook) {
435
+ // Hooks already configured, no need to modify
436
+ return;
437
+ }
438
+
439
+ // Merge with existing settings
440
+ const mergedSettings = {
441
+ ...existingSettings,
442
+ hooks: {
443
+ ...existingSettings.hooks,
444
+ PreToolUse: [
445
+ ...(existingSettings.hooks?.PreToolUse || []),
446
+ ...(hasPreHook ? [] : hookConfig.hooks.PreToolUse)
447
+ ],
448
+ Stop: [
449
+ ...(existingSettings.hooks?.Stop || []),
450
+ ...(hasStopHook ? [] : hookConfig.hooks.Stop)
451
+ ]
452
+ }
453
+ };
454
+
455
+ await fs.writeFile(settingsPath, JSON.stringify(mergedSettings, undefined, 2));
456
+ } catch {
457
+ // File doesn't exist or is invalid JSON, create new one
458
+ await fs.mkdir(path.dirname(settingsPath), { recursive: true });
459
+ await fs.writeFile(settingsPath, JSON.stringify(hookConfig, undefined, 2));
460
+ }
461
+ }
462
+ }
@@ -0,0 +1,27 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ /* note: this bogus test file is required so that
18
+ we are able to run mocha unit tests on this
19
+ package, without having any actual unit tests in it.
20
+ This way a coverage report will be generated,
21
+ showing 0% coverage, instead of no report.
22
+ This file can be removed once we have real unit
23
+ tests in place. */
24
+
25
+ describe('ai-claude-code package', () => {
26
+ it('support code coverage statistics', () => true);
27
+ });