gibi-bot 1.0.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.
- package/.context.json +185 -0
- package/.env.example +4 -0
- package/DISTRIBUTION.md +55 -0
- package/GEMINI.md +20 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/assets/gibi_avatar.png +0 -0
- package/conductor/code_styleguides/general.md +23 -0
- package/conductor/code_styleguides/ts.md +52 -0
- package/conductor/product-guidelines.md +28 -0
- package/conductor/product.md +20 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +15 -0
- package/conductor/tracks/slack_bot_20260107/metadata.json +8 -0
- package/conductor/tracks/slack_bot_20260107/plan.md +26 -0
- package/conductor/tracks/slack_bot_20260107/spec.md +18 -0
- package/conductor/tracks.md +8 -0
- package/conductor/workflow.md +338 -0
- package/dist/agents.js +90 -0
- package/dist/agents.test.js +65 -0
- package/dist/app.js +740 -0
- package/dist/config.js +102 -0
- package/dist/context.js +146 -0
- package/dist/context.test.js +95 -0
- package/dist/prompts.js +20 -0
- package/jest.config.js +11 -0
- package/nodemon.json +10 -0
- package/package.json +44 -0
- package/src/agents.test.ts +85 -0
- package/src/agents.ts +112 -0
- package/src/app.d.ts +2 -0
- package/src/app.d.ts.map +1 -0
- package/src/app.js +55 -0
- package/src/app.js.map +1 -0
- package/src/app.ts +809 -0
- package/src/config.ts +72 -0
- package/src/context.test.ts +75 -0
- package/src/context.ts +130 -0
- package/src/prompts.ts +17 -0
- package/test_gemini.js +23 -0
- package/test_gemini_approval.js +24 -0
- package/test_gemini_write.js +23 -0
- package/tsconfig.json +13 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.loadConfig = loadConfig;
|
|
40
|
+
exports.clearConfig = clearConfig;
|
|
41
|
+
const dotenv = __importStar(require("dotenv"));
|
|
42
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
43
|
+
const keytar = __importStar(require("keytar"));
|
|
44
|
+
dotenv.config();
|
|
45
|
+
const SERVICE_NAME = 'gibi-slack-bot';
|
|
46
|
+
const REQUIRED_VARS = [
|
|
47
|
+
{ name: 'SLACK_BOT_TOKEN', message: 'Enter your Slack Bot Token (xoxb-...):' },
|
|
48
|
+
{ name: 'SLACK_SIGNING_SECRET', message: 'Enter your Slack Signing Secret:' },
|
|
49
|
+
{ name: 'SLACK_APP_TOKEN', message: 'Enter your Slack App Token (xapp-...):' }
|
|
50
|
+
];
|
|
51
|
+
async function loadConfig() {
|
|
52
|
+
let updated = false;
|
|
53
|
+
for (const { name, message } of REQUIRED_VARS) {
|
|
54
|
+
if (process.env[name]) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Try fetching from keychain
|
|
58
|
+
let storedValue = null;
|
|
59
|
+
try {
|
|
60
|
+
storedValue = await keytar.getPassword(SERVICE_NAME, name);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.warn(`[WARN] Failed to access keychain for ${name}:`, err);
|
|
64
|
+
}
|
|
65
|
+
if (storedValue) {
|
|
66
|
+
process.env[name] = storedValue;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Prompt user
|
|
70
|
+
const response = await inquirer_1.default.prompt([
|
|
71
|
+
{
|
|
72
|
+
type: 'password',
|
|
73
|
+
name: 'value',
|
|
74
|
+
message: message,
|
|
75
|
+
mask: '*'
|
|
76
|
+
}
|
|
77
|
+
]);
|
|
78
|
+
const value = response.value;
|
|
79
|
+
if (value) {
|
|
80
|
+
process.env[name] = value;
|
|
81
|
+
await keytar.setPassword(SERVICE_NAME, name, value);
|
|
82
|
+
updated = true;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.error(`Error: ${name} is required.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (updated) {
|
|
90
|
+
console.log('Configuration saved to keychain for future runs.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function clearConfig(keysToClear) {
|
|
94
|
+
const varsToProcess = keysToClear
|
|
95
|
+
? REQUIRED_VARS.filter(v => keysToClear.includes(v.name))
|
|
96
|
+
: REQUIRED_VARS;
|
|
97
|
+
for (const { name } of varsToProcess) {
|
|
98
|
+
await keytar.deletePassword(SERVICE_NAME, name);
|
|
99
|
+
delete process.env[name];
|
|
100
|
+
}
|
|
101
|
+
console.log(`Cleared stored configuration for: ${varsToProcess.map(v => v.name).join(', ')}`);
|
|
102
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ContextManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class ContextManager {
|
|
40
|
+
contexts;
|
|
41
|
+
storagePath;
|
|
42
|
+
constructor() {
|
|
43
|
+
this.contexts = new Map();
|
|
44
|
+
this.storagePath = path.join(process.cwd(), '.context.json');
|
|
45
|
+
this.load();
|
|
46
|
+
}
|
|
47
|
+
load() {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(this.storagePath)) {
|
|
50
|
+
const rawData = fs.readFileSync(this.storagePath, 'utf8');
|
|
51
|
+
const parsed = JSON.parse(rawData);
|
|
52
|
+
// Convert plain objects back to GibiContext with Dates
|
|
53
|
+
Object.values(parsed).forEach((ctx) => {
|
|
54
|
+
this.contexts.set(ctx.id, {
|
|
55
|
+
...ctx,
|
|
56
|
+
createdAt: new Date(ctx.createdAt),
|
|
57
|
+
lastActiveAt: new Date(ctx.lastActiveAt),
|
|
58
|
+
messages: ctx.messages || [] // Ensure messages exists for old contexts
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
console.log(`[ContextManager] Loaded ${this.contexts.size} contexts from disk`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error('[ContextManager] Failed to load contexts:', error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
save() {
|
|
69
|
+
try {
|
|
70
|
+
// Convert Map to object for JSON serialization
|
|
71
|
+
const data = {};
|
|
72
|
+
this.contexts.forEach((value, key) => {
|
|
73
|
+
data[key] = value;
|
|
74
|
+
});
|
|
75
|
+
fs.writeFileSync(this.storagePath, JSON.stringify(data, null, 2));
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('[ContextManager] Failed to save contexts:', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Checks if a context exists for the given ID.
|
|
83
|
+
* @param id The context ID.
|
|
84
|
+
*/
|
|
85
|
+
hasContext(id) {
|
|
86
|
+
return this.contexts.has(id);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Retrieves or creates a context for the given ID.
|
|
90
|
+
* @param id The context ID (e.g., channel ID or thread timestamp).
|
|
91
|
+
*/
|
|
92
|
+
getContext(id) {
|
|
93
|
+
let context = this.contexts.get(id);
|
|
94
|
+
let shouldSave = false;
|
|
95
|
+
if (!context) {
|
|
96
|
+
context = {
|
|
97
|
+
id,
|
|
98
|
+
createdAt: new Date(),
|
|
99
|
+
lastActiveAt: new Date(),
|
|
100
|
+
data: {},
|
|
101
|
+
messages: []
|
|
102
|
+
};
|
|
103
|
+
this.contexts.set(id, context);
|
|
104
|
+
console.log(`[ContextManager] Created new context: ${id}`);
|
|
105
|
+
shouldSave = true;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log(`[ContextManager] Retrieved existing context: ${id}`);
|
|
109
|
+
}
|
|
110
|
+
// Update activity timestamp
|
|
111
|
+
context.lastActiveAt = new Date();
|
|
112
|
+
shouldSave = true; // Always save on access to update timestamp
|
|
113
|
+
if (shouldSave) {
|
|
114
|
+
this.save();
|
|
115
|
+
}
|
|
116
|
+
return context;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Sets arbitrary data on a context.
|
|
120
|
+
* @param id The context ID.
|
|
121
|
+
* @param data Key-value pairs to merge into the context data.
|
|
122
|
+
*/
|
|
123
|
+
setContextData(id, data) {
|
|
124
|
+
const context = this.getContext(id);
|
|
125
|
+
context.data = { ...context.data, ...data };
|
|
126
|
+
this.contexts.set(id, context);
|
|
127
|
+
this.save();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Retrieves all stored contexts.
|
|
131
|
+
*/
|
|
132
|
+
getAllContexts() {
|
|
133
|
+
return Array.from(this.contexts.values());
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Clears a specific context.
|
|
137
|
+
* @param id The context ID to remove.
|
|
138
|
+
*/
|
|
139
|
+
clearContext(id) {
|
|
140
|
+
if (this.contexts.delete(id)) {
|
|
141
|
+
console.log(`[ContextManager] Cleared context: ${id}`);
|
|
142
|
+
this.save();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.ContextManager = ContextManager;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const context_1 = require("./context");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
jest.mock('fs');
|
|
40
|
+
describe('ContextManager', () => {
|
|
41
|
+
let contextManager;
|
|
42
|
+
const mockStoragePath = path.join(process.cwd(), '.context.json');
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
fs.existsSync.mockReturnValue(false);
|
|
46
|
+
contextManager = new context_1.ContextManager();
|
|
47
|
+
});
|
|
48
|
+
it('should create a new context if it does not exist', () => {
|
|
49
|
+
const contextId = 'test-id';
|
|
50
|
+
const context = contextManager.getContext(contextId);
|
|
51
|
+
expect(context.id).toBe(contextId);
|
|
52
|
+
expect(context.messages).toEqual([]);
|
|
53
|
+
expect(context.data).toEqual({});
|
|
54
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
it('should retrieve an existing context', () => {
|
|
57
|
+
const contextId = 'test-id';
|
|
58
|
+
contextManager.getContext(contextId); // Create it
|
|
59
|
+
const context = contextManager.getContext(contextId); // Retrieve it
|
|
60
|
+
expect(context.id).toBe(contextId);
|
|
61
|
+
expect(context.messages).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
it('should set context data', () => {
|
|
64
|
+
const contextId = 'test-id';
|
|
65
|
+
contextManager.setContextData(contextId, { key: 'value' });
|
|
66
|
+
const context = contextManager.getContext(contextId);
|
|
67
|
+
expect(context.data.key).toBe('value');
|
|
68
|
+
});
|
|
69
|
+
it('should load contexts from disk on initialization', () => {
|
|
70
|
+
const mockData = {
|
|
71
|
+
'existing-id': {
|
|
72
|
+
id: 'existing-id',
|
|
73
|
+
createdAt: new Date().toISOString(),
|
|
74
|
+
lastActiveAt: new Date().toISOString(),
|
|
75
|
+
data: { foo: 'bar' },
|
|
76
|
+
messages: [{ role: 'user', content: 'hello' }]
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
fs.existsSync.mockReturnValue(true);
|
|
80
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockData));
|
|
81
|
+
const manager = new context_1.ContextManager();
|
|
82
|
+
const context = manager.getContext('existing-id');
|
|
83
|
+
expect(context.id).toBe('existing-id');
|
|
84
|
+
expect(context.data.foo).toBe('bar');
|
|
85
|
+
expect(context.messages[0].content).toBe('hello');
|
|
86
|
+
});
|
|
87
|
+
it('should clear a context', () => {
|
|
88
|
+
const contextId = 'test-id';
|
|
89
|
+
contextManager.getContext(contextId);
|
|
90
|
+
expect(contextManager.hasContext(contextId)).toBe(true);
|
|
91
|
+
contextManager.clearContext(contextId);
|
|
92
|
+
expect(contextManager.hasContext(contextId)).toBe(false);
|
|
93
|
+
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
});
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLAN_MODE_PROMPT = void 0;
|
|
4
|
+
exports.PLAN_MODE_PROMPT = `
|
|
5
|
+
You are an expert software architect and technical lead.
|
|
6
|
+
Your goal is to help the user plan and implement a software project or feature.
|
|
7
|
+
|
|
8
|
+
Follow this process:
|
|
9
|
+
1. **Analyze the Request**: Understand the user's high-level goal.
|
|
10
|
+
2. **Generate a Plan**: Create a comprehensive, step-by-step markdown plan.
|
|
11
|
+
* Break down the work into logical phases (e.g., Planning, Setup, Implementation, Verification).
|
|
12
|
+
* Include a checklist for each phase.
|
|
13
|
+
* Ask clarifying questions if requirements are vague.
|
|
14
|
+
3. **Refine**: Discuss the plan with the user. Answer their questions. Update the plan as needed.
|
|
15
|
+
4. **Implementation**: When the user is ready, guide them through the implementation. Provide code snippets, file structures, and commands.
|
|
16
|
+
5. **Completion**: When finished, ask if the user wants to "delete the plan" (which effectively means clearing this context or marking the task as done).
|
|
17
|
+
|
|
18
|
+
**Tone**: Professional, encouraging, and structured. Use Markdown effectively (headers, lists, code blocks).
|
|
19
|
+
**Important**: You are running inside a Slack bot. Keep responses concise where possible, but detailed enough for a plan.
|
|
20
|
+
`;
|
package/jest.config.js
ADDED
package/nodemon.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gibi-bot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/app.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gibi": "dist/app.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "ts-node src/app.ts",
|
|
11
|
+
"dev": "nodemon",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "jest"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/fa-krug/Gibi.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/fa-krug/Gibi/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/fa-krug/Gibi#readme",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@slack/bolt": "^4.6.0",
|
|
29
|
+
"@types/node": "^25.0.3",
|
|
30
|
+
"dotenv": "^17.2.3",
|
|
31
|
+
"inquirer": "^8.2.7",
|
|
32
|
+
"keytar": "^7.9.0",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/inquirer": "^9.0.9",
|
|
38
|
+
"@types/jest": "^30.0.0",
|
|
39
|
+
"@types/keytar": "^4.4.0",
|
|
40
|
+
"jest": "^30.2.0",
|
|
41
|
+
"nodemon": "^3.1.11",
|
|
42
|
+
"ts-jest": "^29.4.6"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { GeminiAgent } from './agents';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
|
|
5
|
+
jest.mock('child_process');
|
|
6
|
+
|
|
7
|
+
describe('GeminiAgent', () => {
|
|
8
|
+
let agent: GeminiAgent;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
agent = new GeminiAgent();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should run gemini with the correct arguments', async () => {
|
|
16
|
+
const mockChild = new EventEmitter() as any;
|
|
17
|
+
mockChild.stdout = new EventEmitter();
|
|
18
|
+
mockChild.stderr = new EventEmitter();
|
|
19
|
+
|
|
20
|
+
(spawn as jest.Mock).mockReturnValue(mockChild);
|
|
21
|
+
|
|
22
|
+
const messages = [{ role: 'user', content: 'hello' }];
|
|
23
|
+
const options = { model: 'test-model' };
|
|
24
|
+
|
|
25
|
+
const promise = agent.run(messages, options);
|
|
26
|
+
|
|
27
|
+
// Simulate CLI output
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
mockChild.stdout.emit('data', Buffer.from('hi there'));
|
|
30
|
+
mockChild.emit('close', 0);
|
|
31
|
+
}, 10);
|
|
32
|
+
|
|
33
|
+
const result = await promise;
|
|
34
|
+
|
|
35
|
+
expect(result).toBe('hi there');
|
|
36
|
+
expect(spawn).toHaveBeenCalledWith('gemini', [
|
|
37
|
+
'-m', 'test-model',
|
|
38
|
+
'--approval-mode', 'yolo',
|
|
39
|
+
'User: hello\nAssistant:'
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle errors from the CLI', async () => {
|
|
44
|
+
const mockChild = new EventEmitter() as any;
|
|
45
|
+
mockChild.stdout = new EventEmitter();
|
|
46
|
+
mockChild.stderr = new EventEmitter();
|
|
47
|
+
|
|
48
|
+
(spawn as jest.Mock).mockReturnValue(mockChild);
|
|
49
|
+
|
|
50
|
+
const messages = [{ role: 'user', content: 'hello' }];
|
|
51
|
+
const promise = agent.run(messages);
|
|
52
|
+
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
mockChild.stderr.emit('data', Buffer.from('something went wrong'));
|
|
55
|
+
mockChild.emit('close', 1);
|
|
56
|
+
}, 10);
|
|
57
|
+
|
|
58
|
+
await expect(promise).rejects.toThrow('gemini CLI exited with code 1: something went wrong');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should include PLAN_MODE_PROMPT in plan mode', async () => {
|
|
62
|
+
const mockChild = new EventEmitter() as any;
|
|
63
|
+
mockChild.stdout = new EventEmitter();
|
|
64
|
+
mockChild.stderr = new EventEmitter();
|
|
65
|
+
|
|
66
|
+
(spawn as jest.Mock).mockReturnValue(mockChild);
|
|
67
|
+
|
|
68
|
+
const messages = [{ role: 'user', content: 'plan this' }];
|
|
69
|
+
const options = { mode: 'plan' };
|
|
70
|
+
|
|
71
|
+
const promise = agent.run(messages, options);
|
|
72
|
+
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
mockChild.stdout.emit('data', Buffer.from('here is the plan'));
|
|
75
|
+
mockChild.emit('close', 0);
|
|
76
|
+
}, 10);
|
|
77
|
+
|
|
78
|
+
await promise;
|
|
79
|
+
|
|
80
|
+
const callArgs = (spawn as jest.Mock).mock.calls[0][1];
|
|
81
|
+
const lastArg = callArgs[callArgs.length - 1];
|
|
82
|
+
expect(lastArg).toContain('expert software architect');
|
|
83
|
+
expect(lastArg).toContain('User: plan this');
|
|
84
|
+
});
|
|
85
|
+
});
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { PLAN_MODE_PROMPT } from './prompts';
|
|
3
|
+
|
|
4
|
+
export interface Agent {
|
|
5
|
+
name: string;
|
|
6
|
+
run(messages: Array<{ role: string; content: string }>, options?: any): Promise<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class GeminiAgent implements Agent {
|
|
10
|
+
name = 'gemini';
|
|
11
|
+
|
|
12
|
+
async run(messages: Array<{ role: string; content: string }>, options: any = {}): Promise<string> {
|
|
13
|
+
const { model, mode } = options;
|
|
14
|
+
|
|
15
|
+
let prompt = messages
|
|
16
|
+
.map((m: any) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
|
|
17
|
+
.join('\n') + '\nAssistant:';
|
|
18
|
+
|
|
19
|
+
if (mode === 'plan') {
|
|
20
|
+
prompt = `${PLAN_MODE_PROMPT}\n\n${prompt}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const args = [];
|
|
24
|
+
if (model) {
|
|
25
|
+
args.push('-m', model);
|
|
26
|
+
}
|
|
27
|
+
args.push('--approval-mode', 'yolo');
|
|
28
|
+
args.push(prompt);
|
|
29
|
+
|
|
30
|
+
console.log(`[GeminiAgent] Spawning gemini with args: ${JSON.stringify(args)}`);
|
|
31
|
+
return this.spawnProcess('gemini', args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected spawnProcess(command: string, args: string[]): Promise<string> {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn(command, args);
|
|
37
|
+
let responseText = '';
|
|
38
|
+
let errorText = '';
|
|
39
|
+
|
|
40
|
+
child.stdout.on('data', (data: any) => {
|
|
41
|
+
responseText += data.toString();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
child.stderr.on('data', (data: any) => {
|
|
45
|
+
errorText += data.toString();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
child.on('close', (code: number) => {
|
|
49
|
+
if (code !== 0) {
|
|
50
|
+
reject(new Error(`${command} CLI exited with code ${code}: ${errorText}`));
|
|
51
|
+
} else {
|
|
52
|
+
resolve(responseText.trim());
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
child.on('error', (err: any) => reject(err));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ClaudeAgent extends GeminiAgent {
|
|
62
|
+
name = 'claude';
|
|
63
|
+
|
|
64
|
+
async run(messages: Array<{ role: string; content: string }>, options: any = {}): Promise<string> {
|
|
65
|
+
// Basic implementation for Claude - similar structure, different CLI
|
|
66
|
+
// Assuming 'claude' CLI accepts similar prompt structure or just a string
|
|
67
|
+
const { mode } = options;
|
|
68
|
+
|
|
69
|
+
let prompt = messages
|
|
70
|
+
.map((m: any) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
|
|
71
|
+
.join('\n') + '\nAssistant:';
|
|
72
|
+
|
|
73
|
+
if (mode === 'plan') {
|
|
74
|
+
prompt = `${PLAN_MODE_PROMPT}\n\n${prompt}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const args = [prompt];
|
|
78
|
+
// Add specific arguments if Claude CLI requires them, e.g. -p or similar.
|
|
79
|
+
// logic here assumes usage: claude "prompt"
|
|
80
|
+
|
|
81
|
+
console.log(`[ClaudeAgent] Spawning claude with args length: ${args.length}`);
|
|
82
|
+
return this.spawnProcess('claude', args);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class CursorAgent extends GeminiAgent {
|
|
87
|
+
name = 'cursor';
|
|
88
|
+
|
|
89
|
+
async run(messages: Array<{ role: string; content: string }>, options: any = {}): Promise<string> {
|
|
90
|
+
const { mode } = options;
|
|
91
|
+
|
|
92
|
+
let prompt = messages
|
|
93
|
+
.map((m: any) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
|
|
94
|
+
.join('\n') + '\nAssistant:';
|
|
95
|
+
|
|
96
|
+
if (mode === 'plan') {
|
|
97
|
+
prompt = `${PLAN_MODE_PROMPT}\n\n${prompt}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const args = [prompt];
|
|
101
|
+
// Logic assumes usage: cursor "prompt" - this is hypothetical as 'cursor' CLI might differ
|
|
102
|
+
|
|
103
|
+
console.log(`[CursorAgent] Spawning cursor with args length: ${args.length}`);
|
|
104
|
+
return this.spawnProcess('cursor', args);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const agentRegistry: Record<string, Agent> = {
|
|
109
|
+
gemini: new GeminiAgent(),
|
|
110
|
+
claude: new ClaudeAgent(),
|
|
111
|
+
cursor: new CursorAgent(),
|
|
112
|
+
};
|
package/src/app.d.ts
ADDED
package/src/app.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["app.ts"],"names":[],"mappings":""}
|