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.
Files changed (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. 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();