gibi-bot 1.0.0 → 1.1.1

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 (48) hide show
  1. package/.context.json +47 -3
  2. package/.github/workflows/npm-publish.yml +33 -0
  3. package/.github/workflows/release.yml +45 -0
  4. package/.husky/commit-msg +1 -0
  5. package/.husky/pre-commit +2 -0
  6. package/.prettierignore +3 -0
  7. package/.prettierrc +7 -0
  8. package/CHANGELOG.md +45 -0
  9. package/DISTRIBUTION.md +9 -1
  10. package/GEMINI.md +28 -9
  11. package/README.md +55 -28
  12. package/commitlint.config.js +3 -0
  13. package/conductor/code_styleguides/general.md +6 -1
  14. package/conductor/code_styleguides/ts.md +42 -35
  15. package/conductor/product-guidelines.md +16 -12
  16. package/conductor/product.md +12 -7
  17. package/conductor/setup_state.json +1 -1
  18. package/conductor/tech-stack.md +4 -1
  19. package/conductor/tracks/slack_bot_20260107/metadata.json +1 -1
  20. package/conductor/tracks/slack_bot_20260107/plan.md +6 -1
  21. package/conductor/tracks/slack_bot_20260107/spec.md +9 -6
  22. package/conductor/tracks.md +2 -1
  23. package/conductor/workflow.md +74 -53
  24. package/dist/agents.js +7 -10
  25. package/dist/agents.test.js +17 -12
  26. package/dist/app.js +212 -135
  27. package/dist/config.js +5 -5
  28. package/dist/context.js +4 -3
  29. package/dist/context.test.js +2 -4
  30. package/eslint.config.mjs +17 -0
  31. package/jest.config.js +4 -3
  32. package/nodemon.json +5 -9
  33. package/package.json +34 -4
  34. package/release.config.js +22 -0
  35. package/src/agents.test.ts +62 -57
  36. package/src/agents.ts +94 -82
  37. package/src/app.d.ts +1 -1
  38. package/src/app.ts +298 -178
  39. package/src/config.ts +48 -48
  40. package/src/context.test.ts +54 -56
  41. package/src/context.ts +123 -114
  42. package/test_gemini.js +13 -9
  43. package/test_gemini_approval.js +13 -9
  44. package/test_gemini_write.js +19 -9
  45. package/tests/context.test.ts +145 -0
  46. package/tsconfig.json +1 -1
  47. package/src/app.js +0 -55
  48. package/src/app.js.map +0 -1
package/src/config.ts CHANGED
@@ -7,66 +7,66 @@ dotenv.config();
7
7
  const SERVICE_NAME = 'gibi-slack-bot';
8
8
 
9
9
  const REQUIRED_VARS = [
10
- { name: 'SLACK_BOT_TOKEN', message: 'Enter your Slack Bot Token (xoxb-...):' },
11
- { name: 'SLACK_SIGNING_SECRET', message: 'Enter your Slack Signing Secret:' },
12
- { name: 'SLACK_APP_TOKEN', message: 'Enter your Slack App Token (xapp-...):' }
10
+ { name: 'SLACK_BOT_TOKEN', message: 'Enter your Slack Bot Token (xoxb-...):' },
11
+ { name: 'SLACK_SIGNING_SECRET', message: 'Enter your Slack Signing Secret:' },
12
+ { name: 'SLACK_APP_TOKEN', message: 'Enter your Slack App Token (xapp-...):' },
13
13
  ];
14
14
 
15
15
  export async function loadConfig(): Promise<void> {
16
- let updated = false;
16
+ let updated = false;
17
17
 
18
- for (const { name, message } of REQUIRED_VARS) {
19
- if (process.env[name]) {
20
- continue;
21
- }
18
+ for (const { name, message } of REQUIRED_VARS) {
19
+ if (process.env[name]) {
20
+ continue;
21
+ }
22
22
 
23
- // Try fetching from keychain
24
- let storedValue: string | null = null;
25
- try {
26
- storedValue = await keytar.getPassword(SERVICE_NAME, name);
27
- } catch (err) {
28
- console.warn(`[WARN] Failed to access keychain for ${name}:`, err);
29
- }
23
+ // Try fetching from keychain
24
+ let storedValue: string | null = null;
25
+ try {
26
+ storedValue = await keytar.getPassword(SERVICE_NAME, name);
27
+ } catch (err) {
28
+ console.warn(`[WARN] Failed to access keychain for ${name}:`, err);
29
+ }
30
30
 
31
- if (storedValue) {
32
- process.env[name] = storedValue;
33
- continue;
34
- }
31
+ if (storedValue) {
32
+ process.env[name] = storedValue;
33
+ continue;
34
+ }
35
35
 
36
- // Prompt user
37
- const response = await inquirer.prompt([
38
- {
39
- type: 'password',
40
- name: 'value',
41
- message: message,
42
- mask: '*'
43
- }
44
- ]);
36
+ // Prompt user
37
+ const response = await inquirer.prompt([
38
+ {
39
+ type: 'password',
40
+ name: 'value',
41
+ message: message,
42
+ mask: '*',
43
+ },
44
+ ]);
45
45
 
46
- const value = response.value as string;
47
- if (value) {
48
- process.env[name] = value;
49
- await keytar.setPassword(SERVICE_NAME, name, value);
50
- updated = true;
51
- } else {
52
- console.error(`Error: ${name} is required.`);
53
- process.exit(1);
54
- }
46
+ const value = response.value as string;
47
+ if (value) {
48
+ process.env[name] = value;
49
+ await keytar.setPassword(SERVICE_NAME, name, value);
50
+ updated = true;
51
+ } else {
52
+ console.error(`Error: ${name} is required.`);
53
+ process.exit(1);
55
54
  }
55
+ }
56
56
 
57
- if (updated) {
58
- console.log('Configuration saved to keychain for future runs.');
59
- }
57
+ if (updated) {
58
+ console.log('Configuration saved to keychain for future runs.');
59
+ }
60
60
  }
61
61
 
62
62
  export async function clearConfig(keysToClear?: string[]): Promise<void> {
63
- const varsToProcess = keysToClear
64
- ? REQUIRED_VARS.filter(v => keysToClear.includes(v.name))
65
- : REQUIRED_VARS;
63
+ const varsToProcess = keysToClear
64
+ ? REQUIRED_VARS.filter((v) => keysToClear.includes(v.name))
65
+ : REQUIRED_VARS;
66
66
 
67
- for (const { name } of varsToProcess) {
68
- await keytar.deletePassword(SERVICE_NAME, name);
69
- delete process.env[name];
70
- }
71
- console.log(`Cleared stored configuration for: ${varsToProcess.map(v => v.name).join(', ')}`);
67
+ for (const { name } of varsToProcess) {
68
+ await keytar.deletePassword(SERVICE_NAME, name);
69
+ delete process.env[name];
70
+ }
71
+ console.log(`Cleared stored configuration for: ${varsToProcess.map((v) => v.name).join(', ')}`);
72
72
  }
@@ -1,75 +1,73 @@
1
1
  import { ContextManager } from './context';
2
2
  import * as fs from 'fs';
3
- import * as path from 'path';
4
3
 
5
4
  jest.mock('fs');
6
5
 
7
6
  describe('ContextManager', () => {
8
- let contextManager: ContextManager;
9
- const mockStoragePath = path.join(process.cwd(), '.context.json');
7
+ let contextManager: ContextManager;
10
8
 
11
- beforeEach(() => {
12
- jest.clearAllMocks();
13
- (fs.existsSync as jest.Mock).mockReturnValue(false);
14
- contextManager = new ContextManager();
15
- });
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ (fs.existsSync as jest.Mock).mockReturnValue(false);
12
+ contextManager = new ContextManager();
13
+ });
16
14
 
17
- it('should create a new context if it does not exist', () => {
18
- const contextId = 'test-id';
19
- const context = contextManager.getContext(contextId);
15
+ it('should create a new context if it does not exist', () => {
16
+ const contextId = 'test-id';
17
+ const context = contextManager.getContext(contextId);
20
18
 
21
- expect(context.id).toBe(contextId);
22
- expect(context.messages).toEqual([]);
23
- expect(context.data).toEqual({});
24
- expect(fs.writeFileSync).toHaveBeenCalled();
25
- });
19
+ expect(context.id).toBe(contextId);
20
+ expect(context.messages).toEqual([]);
21
+ expect(context.data).toEqual({});
22
+ expect(fs.writeFileSync).toHaveBeenCalled();
23
+ });
26
24
 
27
- it('should retrieve an existing context', () => {
28
- const contextId = 'test-id';
29
- contextManager.getContext(contextId); // Create it
30
-
31
- const context = contextManager.getContext(contextId); // Retrieve it
32
- expect(context.id).toBe(contextId);
33
- expect(context.messages).toEqual([]);
34
- });
25
+ it('should retrieve an existing context', () => {
26
+ const contextId = 'test-id';
27
+ contextManager.getContext(contextId); // Create it
35
28
 
36
- it('should set context data', () => {
37
- const contextId = 'test-id';
38
- contextManager.setContextData(contextId, { key: 'value' });
29
+ const context = contextManager.getContext(contextId); // Retrieve it
30
+ expect(context.id).toBe(contextId);
31
+ expect(context.messages).toEqual([]);
32
+ });
39
33
 
40
- const context = contextManager.getContext(contextId);
41
- expect(context.data.key).toBe('value');
42
- });
34
+ it('should set context data', () => {
35
+ const contextId = 'test-id';
36
+ contextManager.setContextData(contextId, { key: 'value' });
43
37
 
44
- it('should load contexts from disk on initialization', () => {
45
- const mockData = {
46
- 'existing-id': {
47
- id: 'existing-id',
48
- createdAt: new Date().toISOString(),
49
- lastActiveAt: new Date().toISOString(),
50
- data: { foo: 'bar' },
51
- messages: [{ role: 'user', content: 'hello' }]
52
- }
53
- };
38
+ const context = contextManager.getContext(contextId);
39
+ expect(context.data.key).toBe('value');
40
+ });
54
41
 
55
- (fs.existsSync as jest.Mock).mockReturnValue(true);
56
- (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockData));
42
+ it('should load contexts from disk on initialization', () => {
43
+ const mockData = {
44
+ 'existing-id': {
45
+ id: 'existing-id',
46
+ createdAt: new Date().toISOString(),
47
+ lastActiveAt: new Date().toISOString(),
48
+ data: { foo: 'bar' },
49
+ messages: [{ role: 'user', content: 'hello' }],
50
+ },
51
+ };
57
52
 
58
- const manager = new ContextManager();
59
- const context = manager.getContext('existing-id');
53
+ (fs.existsSync as jest.Mock).mockReturnValue(true);
54
+ (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockData));
60
55
 
61
- expect(context.id).toBe('existing-id');
62
- expect(context.data.foo).toBe('bar');
63
- expect(context.messages[0].content).toBe('hello');
64
- });
56
+ const manager = new ContextManager();
57
+ const context = manager.getContext('existing-id');
65
58
 
66
- it('should clear a context', () => {
67
- const contextId = 'test-id';
68
- contextManager.getContext(contextId);
69
- expect(contextManager.hasContext(contextId)).toBe(true);
59
+ expect(context.id).toBe('existing-id');
60
+ expect(context.data.foo).toBe('bar');
61
+ expect(context.messages[0].content).toBe('hello');
62
+ });
70
63
 
71
- contextManager.clearContext(contextId);
72
- expect(contextManager.hasContext(contextId)).toBe(false);
73
- expect(fs.writeFileSync).toHaveBeenCalled();
74
- });
64
+ it('should clear a context', () => {
65
+ const contextId = 'test-id';
66
+ contextManager.getContext(contextId);
67
+ expect(contextManager.hasContext(contextId)).toBe(true);
68
+
69
+ contextManager.clearContext(contextId);
70
+ expect(contextManager.hasContext(contextId)).toBe(false);
71
+ expect(fs.writeFileSync).toHaveBeenCalled();
72
+ });
75
73
  });
package/src/context.ts CHANGED
@@ -1,130 +1,139 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
 
4
+ export interface GibiContextData {
5
+ model?: string;
6
+ agent?: string;
7
+ cwd?: string;
8
+ mode?: string;
9
+ [key: string]: unknown; // Keep any for flexibility but typed common fields
10
+ }
11
+
4
12
  export interface GibiContext {
5
- id: string;
6
- createdAt: Date;
7
- lastActiveAt: Date;
8
- data: Record<string, any>;
9
- messages: Array<{ role: 'user' | 'assistant'; content: string }>;
13
+ id: string;
14
+ createdAt: Date;
15
+ lastActiveAt: Date;
16
+ data: GibiContextData;
17
+ messages: Array<{ role: 'user' | 'assistant'; content: string }>;
10
18
  }
11
19
 
12
20
  export class ContextManager {
13
- private contexts: Map<string, GibiContext>;
14
- private readonly storagePath: string;
15
-
16
- constructor() {
17
- this.contexts = new Map();
18
- this.storagePath = path.join(process.cwd(), '.context.json');
19
- this.load();
20
- }
21
-
22
- private load(): void {
23
- try {
24
- if (fs.existsSync(this.storagePath)) {
25
- const rawData = fs.readFileSync(this.storagePath, 'utf8');
26
- const parsed = JSON.parse(rawData);
27
-
28
- // Convert plain objects back to GibiContext with Dates
29
- Object.values(parsed).forEach((ctx: any) => {
30
- this.contexts.set(ctx.id, {
31
- ...ctx,
32
- createdAt: new Date(ctx.createdAt),
33
- lastActiveAt: new Date(ctx.lastActiveAt),
34
- messages: ctx.messages || [] // Ensure messages exists for old contexts
35
- });
36
- });
37
-
38
- console.log(`[ContextManager] Loaded ${this.contexts.size} contexts from disk`);
39
- }
40
- } catch (error) {
41
- console.error('[ContextManager] Failed to load contexts:', error);
42
- }
21
+ private contexts: Map<string, GibiContext>;
22
+ private readonly storagePath: string;
23
+
24
+ constructor() {
25
+ this.contexts = new Map();
26
+ this.storagePath = path.join(process.cwd(), '.context.json');
27
+ this.load();
28
+ }
29
+
30
+ private load(): void {
31
+ try {
32
+ if (fs.existsSync(this.storagePath)) {
33
+ const rawData = fs.readFileSync(this.storagePath, 'utf8');
34
+ const parsed = JSON.parse(rawData);
35
+
36
+ // Convert plain objects back to GibiContext with Dates
37
+ Object.values(parsed).forEach((c: unknown) => {
38
+ const ctx = c as any;
39
+ this.contexts.set(ctx.id, {
40
+ ...ctx,
41
+ createdAt: new Date(ctx.createdAt),
42
+ lastActiveAt: new Date(ctx.lastActiveAt),
43
+ messages: ctx.messages || [], // Ensure messages exists for old contexts
44
+ });
45
+ });
46
+
47
+ console.log(`[ContextManager] Loaded ${this.contexts.size} contexts from disk`);
48
+ }
49
+ } catch (error) {
50
+ console.error('[ContextManager] Failed to load contexts:', error);
43
51
  }
44
-
45
- private save(): void {
46
- try {
47
- // Convert Map to object for JSON serialization
48
- const data: Record<string, GibiContext> = {};
49
- this.contexts.forEach((value, key) => {
50
- data[key] = value;
51
- });
52
-
53
- fs.writeFileSync(this.storagePath, JSON.stringify(data, null, 2));
54
- } catch (error) {
55
- console.error('[ContextManager] Failed to save contexts:', error);
56
- }
52
+ }
53
+
54
+ private save(): void {
55
+ try {
56
+ // Convert Map to object for JSON serialization
57
+ const data: Record<string, GibiContext> = {};
58
+ this.contexts.forEach((value, key) => {
59
+ data[key] = value;
60
+ });
61
+
62
+ fs.writeFileSync(this.storagePath, JSON.stringify(data, null, 2));
63
+ } catch (error) {
64
+ console.error('[ContextManager] Failed to save contexts:', error);
57
65
  }
58
-
59
- /**
60
- * Checks if a context exists for the given ID.
61
- * @param id The context ID.
62
- */
63
- hasContext(id: string): boolean {
64
- return this.contexts.has(id);
66
+ }
67
+
68
+ /**
69
+ * Checks if a context exists for the given ID.
70
+ * @param id The context ID.
71
+ */
72
+ hasContext(id: string): boolean {
73
+ return this.contexts.has(id);
74
+ }
75
+
76
+ /**
77
+ * Retrieves or creates a context for the given ID.
78
+ * @param id The context ID (e.g., channel ID or thread timestamp).
79
+ */
80
+ getContext(id: string): GibiContext {
81
+ let context = this.contexts.get(id);
82
+ let shouldSave = false;
83
+
84
+ if (!context) {
85
+ context = {
86
+ id,
87
+ createdAt: new Date(),
88
+ lastActiveAt: new Date(),
89
+ data: {},
90
+ messages: [],
91
+ };
92
+ this.contexts.set(id, context);
93
+ console.log(`[ContextManager] Created new context: ${id}`);
94
+ shouldSave = true;
95
+ } else {
96
+ console.log(`[ContextManager] Retrieved existing context: ${id}`);
65
97
  }
66
98
 
67
- /**
68
- * Retrieves or creates a context for the given ID.
69
- * @param id The context ID (e.g., channel ID or thread timestamp).
70
- */
71
- getContext(id: string): GibiContext {
72
- let context = this.contexts.get(id);
73
- let shouldSave = false;
74
-
75
- if (!context) {
76
- context = {
77
- id,
78
- createdAt: new Date(),
79
- lastActiveAt: new Date(),
80
- data: {},
81
- messages: []
82
- };
83
- this.contexts.set(id, context);
84
- console.log(`[ContextManager] Created new context: ${id}`);
85
- shouldSave = true;
86
- } else {
87
- console.log(`[ContextManager] Retrieved existing context: ${id}`);
88
- }
89
-
90
- // Update activity timestamp
91
- context.lastActiveAt = new Date();
92
- shouldSave = true; // Always save on access to update timestamp
93
-
94
- if (shouldSave) {
95
- this.save();
96
- }
97
-
98
- return context;
99
- }
100
-
101
- /**
102
- * Sets arbitrary data on a context.
103
- * @param id The context ID.
104
- * @param data Key-value pairs to merge into the context data.
105
- */
106
- setContextData(id: string, data: Record<string, any>): void {
107
- const context = this.getContext(id);
108
- context.data = { ...context.data, ...data };
109
- this.contexts.set(id, context);
110
- this.save();
111
- }
99
+ // Update activity timestamp
100
+ context.lastActiveAt = new Date();
101
+ shouldSave = true; // Always save on access to update timestamp
112
102
 
113
- /**
114
- * Retrieves all stored contexts.
115
- */
116
- getAllContexts(): GibiContext[] {
117
- return Array.from(this.contexts.values());
103
+ if (shouldSave) {
104
+ this.save();
118
105
  }
119
106
 
120
- /**
121
- * Clears a specific context.
122
- * @param id The context ID to remove.
123
- */
124
- clearContext(id: string): void {
125
- if (this.contexts.delete(id)) {
126
- console.log(`[ContextManager] Cleared context: ${id}`);
127
- this.save();
128
- }
107
+ return context;
108
+ }
109
+
110
+ /**
111
+ * Sets arbitrary data on a context.
112
+ * @param id The context ID.
113
+ * @param data Key-value pairs to merge into the context data.
114
+ */
115
+ setContextData(id: string, data: Partial<GibiContextData>): void {
116
+ const context = this.getContext(id);
117
+ context.data = { ...context.data, ...data };
118
+ this.contexts.set(id, context);
119
+ this.save();
120
+ }
121
+
122
+ /**
123
+ * Retrieves all stored contexts.
124
+ */
125
+ getAllContexts(): GibiContext[] {
126
+ return Array.from(this.contexts.values());
127
+ }
128
+
129
+ /**
130
+ * Clears a specific context.
131
+ * @param id The context ID to remove.
132
+ */
133
+ clearContext(id: string): void {
134
+ if (this.contexts.delete(id)) {
135
+ console.log(`[ContextManager] Cleared context: ${id}`);
136
+ this.save();
129
137
  }
138
+ }
130
139
  }
package/test_gemini.js CHANGED
@@ -1,23 +1,27 @@
1
1
  const { spawn } = require('child_process');
2
2
 
3
- console.log("Starting gemini process...");
4
- const child = spawn('gemini', ['list files in current directory', '--output-format', 'stream-json', '--approval-mode', 'yolo'], {
5
- stdio: ['pipe', 'pipe', 'pipe'] // We need to control pipes
6
- });
3
+ console.log('Starting gemini process...');
4
+ const child = spawn(
5
+ 'gemini',
6
+ ['list files in current directory', '--output-format', 'stream-json', '--approval-mode', 'yolo'],
7
+ {
8
+ stdio: ['pipe', 'pipe', 'pipe'], // We need to control pipes
9
+ },
10
+ );
7
11
 
8
12
  child.stdout.on('data', (data) => {
9
- console.log(`STDOUT: ${data.toString()}`);
13
+ console.log(`STDOUT: ${data.toString()}`);
10
14
  });
11
15
 
12
16
  child.stderr.on('data', (data) => {
13
- console.log(`STDERR: ${data.toString()}`);
17
+ console.log(`STDERR: ${data.toString()}`);
14
18
  });
15
19
 
16
20
  child.on('close', (code) => {
17
- console.log(`Child exited with code ${code}`);
21
+ console.log(`Child exited with code ${code}`);
18
22
  });
19
23
 
20
24
  setTimeout(() => {
21
- console.log("Timeout reached. Killing process.");
22
- child.kill();
25
+ console.log('Timeout reached. Killing process.');
26
+ child.kill();
23
27
  }, 10000);
@@ -1,24 +1,28 @@
1
1
  const { spawn } = require('child_process');
2
2
 
3
- console.log("Starting gemini process...");
3
+ console.log('Starting gemini process...');
4
4
  // Attempt to force approval mode
5
- const child = spawn('gemini', ['list files', '--output-format', 'stream-json', '--approval-mode', 'yolo'], {
6
- stdio: ['pipe', 'pipe', 'pipe']
7
- });
5
+ const child = spawn(
6
+ 'gemini',
7
+ ['list files', '--output-format', 'stream-json', '--approval-mode', 'yolo'],
8
+ {
9
+ stdio: ['pipe', 'pipe', 'pipe'],
10
+ },
11
+ );
8
12
 
9
13
  child.stdout.on('data', (data) => {
10
- console.log(`STDOUT: ${data.toString()}`);
14
+ console.log(`STDOUT: ${data.toString()}`);
11
15
  });
12
16
 
13
17
  child.stderr.on('data', (data) => {
14
- console.log(`STDERR: ${data.toString()}`);
18
+ console.log(`STDERR: ${data.toString()}`);
15
19
  });
16
20
 
17
21
  child.on('close', (code) => {
18
- console.log(`Child exited with code ${code}`);
22
+ console.log(`Child exited with code ${code}`);
19
23
  });
20
24
 
21
25
  setTimeout(() => {
22
- console.log("Timeout reached. Killing process.");
23
- child.kill();
26
+ console.log('Timeout reached. Killing process.');
27
+ child.kill();
24
28
  }, 10000);
@@ -1,23 +1,33 @@
1
1
  const { spawn } = require('child_process');
2
2
 
3
- console.log("Starting gemini process...");
4
- const child = spawn('gemini', ['create a file named hello_test_123.txt with content "hello"', '--output-format', 'stream-json', '--approval-mode', 'yolo'], {
5
- stdio: ['pipe', 'pipe', 'pipe']
6
- });
3
+ console.log('Starting gemini process...');
4
+ const child = spawn(
5
+ 'gemini',
6
+ [
7
+ 'create a file named hello_test_123.txt with content "hello"',
8
+ '--output-format',
9
+ 'stream-json',
10
+ '--approval-mode',
11
+ 'yolo',
12
+ ],
13
+ {
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ },
16
+ );
7
17
 
8
18
  child.stdout.on('data', (data) => {
9
- console.log(`STDOUT: ${data.toString()}`);
19
+ console.log(`STDOUT: ${data.toString()}`);
10
20
  });
11
21
 
12
22
  child.stderr.on('data', (data) => {
13
- console.log(`STDERR: ${data.toString()}`);
23
+ console.log(`STDERR: ${data.toString()}`);
14
24
  });
15
25
 
16
26
  child.on('close', (code) => {
17
- console.log(`Child exited with code ${code}`);
27
+ console.log(`Child exited with code ${code}`);
18
28
  });
19
29
 
20
30
  setTimeout(() => {
21
- console.log("Timeout reached. Killing process.");
22
- child.kill();
31
+ console.log('Timeout reached. Killing process.');
32
+ child.kill();
23
33
  }, 15000);