ideaco 1.1.5
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/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- package/tailwind.config.mjs +11 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Backend Registry - Core registry and utility functions
|
|
3
|
+
*
|
|
4
|
+
* Manages all CLI backends (built-in + custom), providing detection, registration, and execution.
|
|
5
|
+
* Each CLI model's configuration is loaded from its own folder's config.js.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Auto-detect installed CLIs
|
|
9
|
+
* - Per-agent independent memory files (no sharing)
|
|
10
|
+
* - Generic configuration with custom CLI support
|
|
11
|
+
* - Child process I/O monitoring
|
|
12
|
+
* - Seamless integration with existing Agent system
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
15
|
+
import { tmpdir } from 'os';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { exec, spawn } from 'child_process';
|
|
18
|
+
import { promisify } from 'util';
|
|
19
|
+
import { knowledgeManager } from '../../../employee/knowledge.js';
|
|
20
|
+
|
|
21
|
+
// Import each CLI model's configuration
|
|
22
|
+
import { claudeCodeConfig } from './claude-code/config.js';
|
|
23
|
+
import { codexConfig } from './codex/config.js';
|
|
24
|
+
import { codebuddyConfig } from './codebuddy/config.js';
|
|
25
|
+
|
|
26
|
+
const execAsync = promisify(exec);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* CLI backend states
|
|
30
|
+
*/
|
|
31
|
+
export const CLIBackendState = {
|
|
32
|
+
DETECTED: 'detected', // Detected as installed
|
|
33
|
+
NOT_FOUND: 'not_found', // Not detected
|
|
34
|
+
CONFIGURED: 'configured', // Configured
|
|
35
|
+
ERROR: 'error', // Detection error
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Built-in CLI backend configuration list
|
|
40
|
+
* Aggregated from each folder's config.js
|
|
41
|
+
*/
|
|
42
|
+
const BUILTIN_BACKENDS = [
|
|
43
|
+
claudeCodeConfig,
|
|
44
|
+
codexConfig,
|
|
45
|
+
codebuddyConfig,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build Agent Memory file content
|
|
50
|
+
* Merges agent prompt, long-term memories, skills, knowledge base, etc. into markdown format
|
|
51
|
+
*
|
|
52
|
+
* @param {object} agent - Agent instance
|
|
53
|
+
* @returns {string} Markdown-formatted memory file content
|
|
54
|
+
*/
|
|
55
|
+
export function buildAgentMemoryContent(agent) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
|
|
58
|
+
lines.push(`# ${agent.name} - ${agent.role}`);
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push('> This file is auto-generated by AI Enterprise. Do not edit manually.');
|
|
61
|
+
lines.push('');
|
|
62
|
+
|
|
63
|
+
// Role definition
|
|
64
|
+
lines.push('## Role & Identity');
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push(agent.prompt);
|
|
67
|
+
lines.push('');
|
|
68
|
+
|
|
69
|
+
// Identity information
|
|
70
|
+
lines.push('## Personal Info');
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push(`- **Name**: ${agent.name}`);
|
|
73
|
+
lines.push(`- **Role**: ${agent.role}`);
|
|
74
|
+
lines.push(`- **Gender**: ${agent.gender === 'female' ? 'Female' : 'Male'}`);
|
|
75
|
+
lines.push(`- **Age**: ${agent.age}`);
|
|
76
|
+
lines.push(`- **Signature**: ${agent.signature}`);
|
|
77
|
+
lines.push(`- **Skills**: ${agent.skills.join(', ')}`);
|
|
78
|
+
lines.push('');
|
|
79
|
+
|
|
80
|
+
// Personality traits
|
|
81
|
+
if (agent.personality) {
|
|
82
|
+
lines.push('## Personality');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push(`- **Trait**: ${agent.personality.trait}`);
|
|
85
|
+
lines.push(`- **Tone**: ${agent.personality.tone}`);
|
|
86
|
+
lines.push(`- **Quirk**: ${agent.personality.quirk}`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Long-term memories
|
|
91
|
+
const longTermMemories = agent.memory.searchLongTerm();
|
|
92
|
+
if (longTermMemories.length > 0) {
|
|
93
|
+
lines.push('## Long-term Memories');
|
|
94
|
+
lines.push('');
|
|
95
|
+
const recent = longTermMemories.slice(-30);
|
|
96
|
+
recent.forEach(m => {
|
|
97
|
+
lines.push(`- [${m.category}] ${m.content}`);
|
|
98
|
+
});
|
|
99
|
+
lines.push('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Short-term memories
|
|
103
|
+
if (agent.memory.shortTerm.length > 0) {
|
|
104
|
+
lines.push('## Current Context (Short-term)');
|
|
105
|
+
lines.push('');
|
|
106
|
+
agent.memory.shortTerm.forEach(m => {
|
|
107
|
+
lines.push(`- ${m.content}`);
|
|
108
|
+
});
|
|
109
|
+
lines.push('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
// Knowledge base
|
|
115
|
+
try {
|
|
116
|
+
const kbPrompt = knowledgeManager.buildKnowledgePrompt(agent.id, agent.department);
|
|
117
|
+
if (kbPrompt) {
|
|
118
|
+
lines.push('## Knowledge Base');
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push(kbPrompt);
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
|
|
125
|
+
// Teamwork rules
|
|
126
|
+
lines.push('## Teamwork Rules');
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push('- You are part of a team. Coordinate with colleagues when needed.');
|
|
129
|
+
lines.push('- When working in parallel, avoid duplicate work and share discoveries.');
|
|
130
|
+
lines.push('- Produce real, working code output. Do not just describe what you would do.');
|
|
131
|
+
lines.push('- Be efficient: plan all needed operations at once, minimize tool call rounds.');
|
|
132
|
+
lines.push('');
|
|
133
|
+
|
|
134
|
+
return lines.join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get shell command prefix for nvm environment
|
|
139
|
+
* Used to execute CLI commands under a specific Node version
|
|
140
|
+
*
|
|
141
|
+
* @param {string|null} nodeVersion - Node version, e.g. '20'
|
|
142
|
+
* @returns {string} Shell command prefix
|
|
143
|
+
*/
|
|
144
|
+
function getNvmPrefix(nodeVersion) {
|
|
145
|
+
if (!nodeVersion) return '';
|
|
146
|
+
return `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm use ${nodeVersion} > /dev/null 2>&1 && `;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* CLI Backend Registry - Manages all CLI backends
|
|
151
|
+
*/
|
|
152
|
+
export class CLIBackendRegistry {
|
|
153
|
+
constructor() {
|
|
154
|
+
/** @type {Map<string, {config: object, state: string, version: string|null, lastDetected: Date|null}>} */
|
|
155
|
+
this.backends = new Map();
|
|
156
|
+
|
|
157
|
+
// Register built-in backends
|
|
158
|
+
BUILTIN_BACKENDS.forEach(config => {
|
|
159
|
+
this.backends.set(config.id, {
|
|
160
|
+
config: { ...config },
|
|
161
|
+
state: CLIBackendState.NOT_FOUND,
|
|
162
|
+
version: null,
|
|
163
|
+
lastDetected: null,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Register a custom CLI backend
|
|
170
|
+
* @param {object} config - CLI configuration
|
|
171
|
+
*/
|
|
172
|
+
register(config) {
|
|
173
|
+
if (!config.id || !config.execCommand) {
|
|
174
|
+
throw new Error('CLI backend config requires at least id and execCommand');
|
|
175
|
+
}
|
|
176
|
+
const fullConfig = {
|
|
177
|
+
id: config.id,
|
|
178
|
+
name: config.name || config.id,
|
|
179
|
+
description: config.description || '',
|
|
180
|
+
icon: config.icon || '🔧',
|
|
181
|
+
detectCommand: config.detectCommand || `${config.execCommand} --version`,
|
|
182
|
+
execCommand: config.execCommand,
|
|
183
|
+
execArgs: config.execArgs || ['-p', '{prompt}'],
|
|
184
|
+
interactiveArgs: config.interactiveArgs || [],
|
|
185
|
+
memoryDir: config.memoryDir || `.${config.id}`,
|
|
186
|
+
memoryFile: config.memoryFile || 'MEMORY.md',
|
|
187
|
+
memoryFormat: config.memoryFormat || 'markdown',
|
|
188
|
+
workingDirSupport: config.workingDirSupport !== false,
|
|
189
|
+
pipeMode: config.pipeMode || 'args',
|
|
190
|
+
outputMode: config.outputMode || 'stdout',
|
|
191
|
+
initCommand: config.initCommand || null,
|
|
192
|
+
customEnv: config.customEnv || {},
|
|
193
|
+
nvmNode: config.nvmNode || null,
|
|
194
|
+
builtin: false,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
this.backends.set(fullConfig.id, {
|
|
198
|
+
config: fullConfig,
|
|
199
|
+
state: CLIBackendState.NOT_FOUND,
|
|
200
|
+
version: null,
|
|
201
|
+
lastDetected: null,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
console.log(`🔧 CLI Backend registered: ${fullConfig.name} (${fullConfig.id})`);
|
|
205
|
+
return fullConfig;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Unregister a custom CLI backend
|
|
210
|
+
*/
|
|
211
|
+
unregister(backendId) {
|
|
212
|
+
const backend = this.backends.get(backendId);
|
|
213
|
+
if (!backend) return false;
|
|
214
|
+
if (backend.config.builtin) {
|
|
215
|
+
throw new Error('Cannot unregister built-in CLI backends');
|
|
216
|
+
}
|
|
217
|
+
this.backends.delete(backendId);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Detect if a single CLI is installed
|
|
223
|
+
* @param {string} backendId
|
|
224
|
+
* @returns {Promise<{detected: boolean, version: string|null}>}
|
|
225
|
+
*/
|
|
226
|
+
async detect(backendId) {
|
|
227
|
+
const backend = this.backends.get(backendId);
|
|
228
|
+
if (!backend) throw new Error(`CLI backend not found: ${backendId}`);
|
|
229
|
+
|
|
230
|
+
const { config } = backend;
|
|
231
|
+
const nvmPrefix = getNvmPrefix(config.nvmNode);
|
|
232
|
+
const detectCmd = nvmPrefix + config.detectCommand;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const { stdout } = await execAsync(detectCmd, {
|
|
236
|
+
timeout: 10000,
|
|
237
|
+
shell: '/bin/bash',
|
|
238
|
+
env: { ...process.env, ...config.customEnv },
|
|
239
|
+
});
|
|
240
|
+
const version = stdout.trim().split('\n')[0];
|
|
241
|
+
backend.state = CLIBackendState.DETECTED;
|
|
242
|
+
backend.version = version;
|
|
243
|
+
backend.lastDetected = new Date();
|
|
244
|
+
console.log(`✅ CLI detected: ${config.name} (${version})`);
|
|
245
|
+
return { detected: true, version };
|
|
246
|
+
} catch (error) {
|
|
247
|
+
backend.state = CLIBackendState.NOT_FOUND;
|
|
248
|
+
backend.version = null;
|
|
249
|
+
backend.lastDetected = new Date();
|
|
250
|
+
return { detected: false, version: null };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Detect all registered CLIs
|
|
256
|
+
* @returns {Promise<object[]>} Detection results list
|
|
257
|
+
*/
|
|
258
|
+
async detectAll() {
|
|
259
|
+
const results = [];
|
|
260
|
+
const promises = [...this.backends.keys()].map(async (id) => {
|
|
261
|
+
const result = await this.detect(id);
|
|
262
|
+
results.push({ id, ...result, name: this.backends.get(id).config.name });
|
|
263
|
+
});
|
|
264
|
+
await Promise.all(promises);
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get all available (detected) backends
|
|
270
|
+
*/
|
|
271
|
+
getAvailableBackends() {
|
|
272
|
+
return [...this.backends.values()]
|
|
273
|
+
.filter(b => b.state === CLIBackendState.DETECTED || b.state === CLIBackendState.CONFIGURED)
|
|
274
|
+
.map(b => ({
|
|
275
|
+
id: b.config.id,
|
|
276
|
+
name: b.config.name,
|
|
277
|
+
description: b.config.description,
|
|
278
|
+
icon: b.config.icon,
|
|
279
|
+
version: b.version,
|
|
280
|
+
builtin: b.config.builtin,
|
|
281
|
+
state: b.state,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* List all backends with their states
|
|
287
|
+
*/
|
|
288
|
+
listAll() {
|
|
289
|
+
return [...this.backends.values()].map(b => ({
|
|
290
|
+
id: b.config.id,
|
|
291
|
+
name: b.config.name,
|
|
292
|
+
description: b.config.description,
|
|
293
|
+
icon: b.config.icon,
|
|
294
|
+
rating: b.config.rating || 80,
|
|
295
|
+
version: b.version,
|
|
296
|
+
state: b.state,
|
|
297
|
+
builtin: b.config.builtin,
|
|
298
|
+
lastDetected: b.lastDetected,
|
|
299
|
+
nvmNode: b.config.nvmNode,
|
|
300
|
+
execCommand: b.config.execCommand,
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Write memory file for an Agent
|
|
306
|
+
* Creates the CLI's memory file under the Agent's working directory
|
|
307
|
+
*
|
|
308
|
+
* @param {string} backendId - CLI backend ID
|
|
309
|
+
* @param {object} agent - Agent instance
|
|
310
|
+
* @param {string} workspaceDir - Agent's workspace directory
|
|
311
|
+
*/
|
|
312
|
+
writeAgentMemory(backendId, agent, workspaceDir) {
|
|
313
|
+
const backend = this.backends.get(backendId);
|
|
314
|
+
if (!backend) throw new Error(`CLI backend not found: ${backendId}`);
|
|
315
|
+
|
|
316
|
+
const { config } = backend;
|
|
317
|
+
const memoryContent = buildAgentMemoryContent(agent);
|
|
318
|
+
|
|
319
|
+
const agentDir = path.join(workspaceDir, `_cli_${agent.id.slice(0, 8)}`);
|
|
320
|
+
const memDir = path.join(agentDir, config.memoryDir);
|
|
321
|
+
|
|
322
|
+
if (!existsSync(memDir)) {
|
|
323
|
+
mkdirSync(memDir, { recursive: true });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const memFilePath = path.join(memDir, config.memoryFile);
|
|
327
|
+
writeFileSync(memFilePath, memoryContent, 'utf-8');
|
|
328
|
+
|
|
329
|
+
console.log(`📝 CLI memory written: ${config.name} -> ${memFilePath}`);
|
|
330
|
+
return { agentDir, memFilePath };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Execute a task via CLI
|
|
335
|
+
* Core method: executes the Agent's task through a CLI subprocess
|
|
336
|
+
*
|
|
337
|
+
* @param {string} backendId - CLI backend ID
|
|
338
|
+
* @param {object} agent - Agent instance
|
|
339
|
+
* @param {object} task - Task description { title, description, context, requirements }
|
|
340
|
+
* @param {string} workspaceDir - Workspace directory
|
|
341
|
+
* @param {object} callbacks - Callbacks { onOutput, onError, onComplete }
|
|
342
|
+
* @param {object} options - Optional parameters { timeout }
|
|
343
|
+
* @returns {Promise<{output: string, exitCode: number, duration: number}>}
|
|
344
|
+
*/
|
|
345
|
+
async executeTask(backendId, agent, task, workspaceDir, callbacks = {}, options = {}) {
|
|
346
|
+
const backend = this.backends.get(backendId);
|
|
347
|
+
if (!backend) throw new Error(`CLI backend not found: ${backendId}`);
|
|
348
|
+
if (backend.state !== CLIBackendState.DETECTED && backend.state !== CLIBackendState.CONFIGURED) {
|
|
349
|
+
throw new Error(`CLI backend ${backendId} is not available (state: ${backend.state})`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const { config } = backend;
|
|
353
|
+
|
|
354
|
+
// 1. Write memory file
|
|
355
|
+
const { agentDir } = this.writeAgentMemory(backendId, agent, workspaceDir);
|
|
356
|
+
|
|
357
|
+
// 2. Build prompt
|
|
358
|
+
const prompt = this._buildTaskPrompt(task);
|
|
359
|
+
|
|
360
|
+
// 3. Build command-line arguments
|
|
361
|
+
const hasPromptPlaceholder = config.execArgs.some(arg => arg.includes('{prompt}'));
|
|
362
|
+
const isNvmMode = !!config.nvmNode;
|
|
363
|
+
const useStdin = !hasPromptPlaceholder || isNvmMode;
|
|
364
|
+
const args = isNvmMode
|
|
365
|
+
? config.execArgs.filter(arg => arg !== '{prompt}').map(arg => arg.replace('{prompt}', ''))
|
|
366
|
+
: config.execArgs.map(arg => arg.replace('{prompt}', prompt));
|
|
367
|
+
|
|
368
|
+
// 4. Build execution environment (per-agent isolated XDG dirs to prevent CLI memory sharing)
|
|
369
|
+
const agentConfigDir = path.join(agentDir, '.config');
|
|
370
|
+
const agentDataDir = path.join(agentDir, '.local', 'share');
|
|
371
|
+
const agentStateDir = path.join(agentDir, '.local', 'state');
|
|
372
|
+
const agentCacheDir = path.join(agentDir, '.cache');
|
|
373
|
+
for (const dir of [agentConfigDir, agentDataDir, agentStateDir, agentCacheDir]) {
|
|
374
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
375
|
+
}
|
|
376
|
+
const env = {
|
|
377
|
+
...process.env,
|
|
378
|
+
...config.customEnv,
|
|
379
|
+
XDG_CONFIG_HOME: agentConfigDir,
|
|
380
|
+
XDG_DATA_HOME: agentDataDir,
|
|
381
|
+
XDG_STATE_HOME: agentStateDir,
|
|
382
|
+
XDG_CACHE_HOME: agentCacheDir,
|
|
383
|
+
};
|
|
384
|
+
const cwd = workspaceDir;
|
|
385
|
+
|
|
386
|
+
// 5. Ensure working directory exists
|
|
387
|
+
if (!existsSync(cwd)) {
|
|
388
|
+
mkdirSync(cwd, { recursive: true });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const timeoutMs = options.timeout || 600000;
|
|
392
|
+
const inactivityTimeoutMs = options.inactivityTimeout || 120000;
|
|
393
|
+
|
|
394
|
+
const promptMode = useStdin ? 'stdin' : 'args';
|
|
395
|
+
console.log(`🚀 CLI executing: ${config.execCommand} (${promptMode} prompt, ${Math.round(timeoutMs/1000)}s timeout, ${Math.round(inactivityTimeoutMs/1000)}s inactivity limit)`);
|
|
396
|
+
console.log(` Working dir: ${cwd}`);
|
|
397
|
+
console.log(` Prompt length: ${prompt.length} chars, mode: ${promptMode}`);
|
|
398
|
+
|
|
399
|
+
const startTime = Date.now();
|
|
400
|
+
|
|
401
|
+
return new Promise((resolve, reject) => {
|
|
402
|
+
let output = '';
|
|
403
|
+
let errorOutput = '';
|
|
404
|
+
let lastActivityTime = Date.now();
|
|
405
|
+
let totalTimer = null;
|
|
406
|
+
let inactivityTimer = null;
|
|
407
|
+
let killed = false;
|
|
408
|
+
|
|
409
|
+
const killChild = (reason) => {
|
|
410
|
+
if (killed) return;
|
|
411
|
+
killed = true;
|
|
412
|
+
const elapsed = Date.now() - startTime;
|
|
413
|
+
console.warn(`⏰ CLI killed: ${config.name} — ${reason} (after ${Math.round(elapsed/1000)}s)`);
|
|
414
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
417
|
+
}, 5000);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const resetInactivityTimer = () => {
|
|
421
|
+
lastActivityTime = Date.now();
|
|
422
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
423
|
+
inactivityTimer = setTimeout(() => {
|
|
424
|
+
killChild(`no output for ${Math.round(inactivityTimeoutMs/1000)}s`);
|
|
425
|
+
}, inactivityTimeoutMs);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
let child;
|
|
429
|
+
const nvmPrefix = getNvmPrefix(config.nvmNode);
|
|
430
|
+
|
|
431
|
+
let promptTmpFile = null;
|
|
432
|
+
if (useStdin && isNvmMode) {
|
|
433
|
+
promptTmpFile = path.join(tmpdir(), `ai-ent-prompt-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
434
|
+
writeFileSync(promptTmpFile, prompt, 'utf-8');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (nvmPrefix) {
|
|
438
|
+
const argsStr = args.filter(a => a.length > 0).map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
|
|
439
|
+
const stdinPipe = promptTmpFile ? `cat '${promptTmpFile}' | ` : '';
|
|
440
|
+
const fullCommand = `${nvmPrefix}${stdinPipe}${config.execCommand} ${argsStr}`;
|
|
441
|
+
child = spawn('bash', ['-c', fullCommand], {
|
|
442
|
+
cwd,
|
|
443
|
+
env,
|
|
444
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
445
|
+
});
|
|
446
|
+
} else {
|
|
447
|
+
child = spawn(config.execCommand, args, {
|
|
448
|
+
cwd,
|
|
449
|
+
env,
|
|
450
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
totalTimer = setTimeout(() => {
|
|
455
|
+
killChild(`total timeout ${Math.round(timeoutMs/1000)}s reached`);
|
|
456
|
+
}, timeoutMs);
|
|
457
|
+
|
|
458
|
+
resetInactivityTimer();
|
|
459
|
+
|
|
460
|
+
if (useStdin && !isNvmMode) {
|
|
461
|
+
child.stdin.write(prompt);
|
|
462
|
+
}
|
|
463
|
+
child.stdin.end();
|
|
464
|
+
|
|
465
|
+
child.stdout.on('data', (data) => {
|
|
466
|
+
const chunk = data.toString();
|
|
467
|
+
output += chunk;
|
|
468
|
+
resetInactivityTimer();
|
|
469
|
+
if (callbacks.onOutput) {
|
|
470
|
+
try { callbacks.onOutput(chunk); } catch {}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
child.stderr.on('data', (data) => {
|
|
475
|
+
const chunk = data.toString();
|
|
476
|
+
errorOutput += chunk;
|
|
477
|
+
resetInactivityTimer();
|
|
478
|
+
if (callbacks.onError) {
|
|
479
|
+
try { callbacks.onError(chunk); } catch {}
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
child.on('close', (code) => {
|
|
484
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
485
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
486
|
+
if (promptTmpFile) { try { unlinkSync(promptTmpFile); } catch {} }
|
|
487
|
+
|
|
488
|
+
const duration = Date.now() - startTime;
|
|
489
|
+
const result = {
|
|
490
|
+
output: output.trim(),
|
|
491
|
+
errorOutput: errorOutput.trim(),
|
|
492
|
+
exitCode: killed ? (code ?? -1) : code,
|
|
493
|
+
duration,
|
|
494
|
+
backendId: config.id,
|
|
495
|
+
backendName: config.name,
|
|
496
|
+
killed,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
console.log(`✅ CLI completed: ${config.name}, exit code ${code}${killed ? ' (killed)' : ''}, ${duration}ms`);
|
|
500
|
+
|
|
501
|
+
if (callbacks.onComplete) {
|
|
502
|
+
try { callbacks.onComplete(result); } catch {}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
resolve(result);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
child.on('error', (error) => {
|
|
509
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
510
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
511
|
+
if (promptTmpFile) { try { unlinkSync(promptTmpFile); } catch {} }
|
|
512
|
+
|
|
513
|
+
const duration = Date.now() - startTime;
|
|
514
|
+
console.error(`❌ CLI error: ${config.name} - ${error.message}`);
|
|
515
|
+
reject({
|
|
516
|
+
output: output.trim(),
|
|
517
|
+
errorOutput: error.message,
|
|
518
|
+
exitCode: -1,
|
|
519
|
+
duration,
|
|
520
|
+
backendId: config.id,
|
|
521
|
+
backendName: config.name,
|
|
522
|
+
error: error.message,
|
|
523
|
+
killed,
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Build the task prompt (complete instructions for the CLI)
|
|
531
|
+
*/
|
|
532
|
+
_buildTaskPrompt(task) {
|
|
533
|
+
let prompt = `Please complete the following task:\n\n`;
|
|
534
|
+
prompt += `**Task**: ${task.title}\n`;
|
|
535
|
+
if (task.description) {
|
|
536
|
+
prompt += `**Description**: ${task.description}\n`;
|
|
537
|
+
}
|
|
538
|
+
if (task.context) {
|
|
539
|
+
prompt += `\n**Context**:\n${task.context}\n`;
|
|
540
|
+
}
|
|
541
|
+
if (task.requirements) {
|
|
542
|
+
prompt += `\n**Requirements**:\n${task.requirements}\n`;
|
|
543
|
+
}
|
|
544
|
+
prompt += `\nPlease complete the task diligently. Produce real, working output. Be efficient and give a final summary after completing the work.`;
|
|
545
|
+
return prompt;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Serialize (for persistence)
|
|
550
|
+
*/
|
|
551
|
+
serialize() {
|
|
552
|
+
const customBackends = [];
|
|
553
|
+
for (const [id, backend] of this.backends) {
|
|
554
|
+
if (!backend.config.builtin) {
|
|
555
|
+
customBackends.push(backend.config);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
customBackends,
|
|
560
|
+
savedAt: new Date().toISOString(),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Restore custom backends from serialized data
|
|
566
|
+
*/
|
|
567
|
+
restore(data) {
|
|
568
|
+
if (!data || !data.customBackends) return;
|
|
569
|
+
for (const config of data.customBackends) {
|
|
570
|
+
try {
|
|
571
|
+
this.register(config);
|
|
572
|
+
} catch (e) {
|
|
573
|
+
console.error(`[CLIBackendRegistry] Failed to restore backend "${config.id}":`, e.message);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Global singleton
|
|
580
|
+
export const cliBackendRegistry = new CLIBackendRegistry();
|