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,596 @@
1
+ /**
2
+ * Agent Tool System - Callable tool set for Agents
3
+ *
4
+ * Follows the Codex/OpenAI Agents tool_use pattern:
5
+ * - file_read: Read file contents
6
+ * - file_write: Write/create files
7
+ * - file_list: List directory contents
8
+ * - file_delete: Delete files
9
+ * - shell_exec: Execute shell commands (restricted)
10
+ * - send_message: Send messages to other Agents
11
+ */
12
+ import fs from 'fs/promises';
13
+ import { existsSync, mkdirSync } from 'fs';
14
+ import path from 'path';
15
+ import { exec } from 'child_process';
16
+ import { promisify } from 'util';
17
+ import { securityGuard } from '../system/audit.js';
18
+ import { pluginRegistry, HookPoint } from '../system/plugin.js';
19
+ // chatStore recording is handled centrally by requirement.js messageHandler
20
+
21
+ const execAsync = promisify(exec);
22
+
23
+ /**
24
+ * Agent Tool Kit - Each Agent instance holds one ToolKit
25
+ * Tool operations are restricted to the specified workspace directory
26
+ */
27
+ export class AgentToolKit {
28
+ /**
29
+ * @param {string} workspaceDir - Agent's workspace root directory
30
+ * @param {object} messageBus - Message bus reference
31
+ * @param {string} agentId - Current Agent's ID
32
+ * @param {string} agentName - Current Agent's display name
33
+ * @param {object} [employee] - Back-reference to the owning Employee (for memory access)
34
+ */
35
+ constructor(workspaceDir, messageBus = null, agentId = null, agentName = '', employee = null) {
36
+ this.workspaceDir = workspaceDir;
37
+ this.messageBus = messageBus;
38
+ this.agentId = agentId;
39
+ this.agentName = agentName;
40
+ this.employee = employee;
41
+
42
+ // Ensure workspace directory exists
43
+ if (!existsSync(workspaceDir)) {
44
+ mkdirSync(workspaceDir, { recursive: true });
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Safe path resolution: ensure all file ops stay within workspace
50
+ */
51
+ _safePath(filePath) {
52
+ const resolved = path.resolve(this.workspaceDir, filePath);
53
+ if (!resolved.startsWith(path.resolve(this.workspaceDir))) {
54
+ throw new Error(`Security restriction: path "${filePath}" is outside workspace`);
55
+ }
56
+ return resolved;
57
+ }
58
+
59
+ /**
60
+ * Get tool definitions in OpenAI function calling format
61
+ */
62
+ get definitions() {
63
+ return [
64
+ {
65
+ type: 'function',
66
+ function: {
67
+ name: 'file_read',
68
+ description: 'Read the contents of a file. For large files, use offset and limit to read specific line ranges to avoid context overflow. If the file is too large (>800 lines), it will be automatically truncated with a hint to use offset/limit for the remaining content.',
69
+ parameters: {
70
+ type: 'object',
71
+ properties: {
72
+ path: { type: 'string', description: 'File path (relative to workspace)' },
73
+ offset: { type: 'integer', description: 'Start reading from this line number (1-based). Omit to start from beginning.' },
74
+ limit: { type: 'integer', description: 'Maximum number of lines to read. Omit to read all (subject to auto-truncation for large files).' },
75
+ },
76
+ required: ['path'],
77
+ },
78
+ },
79
+ },
80
+ {
81
+ type: 'function',
82
+ function: {
83
+ name: 'file_write',
84
+ description: 'Create or overwrite a file. Directories are auto-created if needed. Path is relative to workspace.',
85
+ parameters: {
86
+ type: 'object',
87
+ properties: {
88
+ path: { type: 'string', description: 'File path (relative to workspace)' },
89
+ content: { type: 'string', description: 'File content' },
90
+ },
91
+ required: ['path', 'content'],
92
+ },
93
+ },
94
+ },
95
+ {
96
+ type: 'function',
97
+ function: {
98
+ name: 'file_list',
99
+ description: 'List files and subdirectories under the given path. Path is relative to workspace.',
100
+ parameters: {
101
+ type: 'object',
102
+ properties: {
103
+ path: { type: 'string', description: 'Directory path (relative to workspace), defaults to root' },
104
+ },
105
+ required: [],
106
+ },
107
+ },
108
+ },
109
+ {
110
+ type: 'function',
111
+ function: {
112
+ name: 'file_delete',
113
+ description: 'Delete the specified file. Path is relative to workspace.',
114
+ parameters: {
115
+ type: 'object',
116
+ properties: {
117
+ path: { type: 'string', description: 'Path of the file to delete' },
118
+ },
119
+ required: ['path'],
120
+ },
121
+ },
122
+ },
123
+ {
124
+ type: 'function',
125
+ function: {
126
+ name: 'shell_exec',
127
+ description: 'Execute a shell command in the workspace directory. Only safe commands are allowed (e.g. ls, cat, grep, node, npm).',
128
+ parameters: {
129
+ type: 'object',
130
+ properties: {
131
+ command: { type: 'string', description: 'Shell command to execute' },
132
+ },
133
+ required: ['command'],
134
+ },
135
+ },
136
+ },
137
+ {
138
+ type: 'function',
139
+ function: {
140
+ name: 'send_message',
141
+ description: 'Send a message to another Agent in the team for collaboration, asking questions, sharing results, or requesting feedback. Use this to communicate with colleagues when you need their input or want to share your work output.',
142
+ parameters: {
143
+ type: 'object',
144
+ properties: {
145
+ targetAgentId: { type: 'string', description: 'Target Agent ID' },
146
+ content: { type: 'string', description: 'Message content. You can mention colleagues with @Name format.' },
147
+ type: { type: 'string', enum: ['task', 'question', 'report', 'review', 'feedback'], description: 'Message type' },
148
+ },
149
+ required: ['targetAgentId', 'content'],
150
+ },
151
+ },
152
+ },
153
+ {
154
+ type: 'function',
155
+ function: {
156
+ name: 'file_stats',
157
+ description: 'Get file metadata (size, line count, last modified) WITHOUT reading the content. Use this to check file size before reading, especially for large files. This helps you decide whether to use offset/limit with file_read.',
158
+ parameters: {
159
+ type: 'object',
160
+ properties: {
161
+ path: { type: 'string', description: 'File path (relative to workspace)' },
162
+ },
163
+ required: ['path'],
164
+ },
165
+ },
166
+ },
167
+ {
168
+ type: 'function',
169
+ function: {
170
+ name: 'file_append',
171
+ description: 'Append content to the end of an existing file (or create it if it does not exist). Useful for building up long files incrementally without having to rewrite the entire content.',
172
+ parameters: {
173
+ type: 'object',
174
+ properties: {
175
+ path: { type: 'string', description: 'File path (relative to workspace)' },
176
+ content: { type: 'string', description: 'Content to append' },
177
+ },
178
+ required: ['path', 'content'],
179
+ },
180
+ },
181
+ },
182
+ {
183
+ type: 'function',
184
+ function: {
185
+ name: 'file_patch',
186
+ description: 'Replace a specific text segment in a file. Use this to edit part of a file without rewriting the entire content. The old_text must match exactly (including whitespace).',
187
+ parameters: {
188
+ type: 'object',
189
+ properties: {
190
+ path: { type: 'string', description: 'File path (relative to workspace)' },
191
+ old_text: { type: 'string', description: 'The exact text to find and replace (must be unique in the file)' },
192
+ new_text: { type: 'string', description: 'The replacement text' },
193
+ },
194
+ required: ['path', 'old_text', 'new_text'],
195
+ },
196
+ },
197
+ },
198
+ {
199
+ type: 'function',
200
+ function: {
201
+ name: 'workspace_files',
202
+ description: 'List ALL files in the workspace recursively. Returns a flat list of all file paths relative to workspace root. Use this to see what files exist before referencing them in your messages. This is especially useful before writing [[file:path]] references to ensure the file actually exists.',
203
+ parameters: {
204
+ type: 'object',
205
+ properties: {},
206
+ required: [],
207
+ },
208
+ },
209
+ },
210
+ {
211
+ type: 'function',
212
+ function: {
213
+ name: 'file_search',
214
+ description: 'Search for files by name pattern in the workspace. Returns matching file paths. Use this when you want to find a file but are unsure of its exact name or location. Supports partial name matching (case-insensitive).',
215
+ parameters: {
216
+ type: 'object',
217
+ properties: {
218
+ query: { type: 'string', description: 'Search query — partial filename or keyword to match against file names (case-insensitive)' },
219
+ },
220
+ required: ['query'],
221
+ },
222
+ },
223
+ },
224
+ // Include tools from enabled plugins
225
+ ...pluginRegistry.getPluginTools(),
226
+ ];
227
+ }
228
+
229
+ /**
230
+ * Execute a tool call
231
+ * @param {string} name - Tool name
232
+ * @param {object} args - Tool arguments
233
+ * @returns {Promise<string>} Tool execution result
234
+ */
235
+ async execute(name, args) {
236
+ // Parameter safety check
237
+ if (!args || typeof args !== 'object') {
238
+ args = {};
239
+ }
240
+
241
+ // Security audit: log all tool calls
242
+ securityGuard.logToolCall(name, args, this.agentId, this.agentName);
243
+
244
+ // Fire plugin hooks: before tool call
245
+ await pluginRegistry.fireHook(HookPoint.BEFORE_TOOL_CALL, {
246
+ toolName: name, args, agentId: this.agentId, agentName: this.agentName,
247
+ });
248
+
249
+ // Parameter name compatibility: LLMs sometimes use filePath, file_path, etc. instead of path
250
+ const resolvePath = (a) => a.path || a.filePath || a.file_path || a.filename || a.fileName || null;
251
+
252
+ let result;
253
+ switch (name) {
254
+ case 'file_read': {
255
+ const filePath = resolvePath(args);
256
+ if (!filePath) throw new Error(`Missing required parameter: path (received args: ${JSON.stringify(args)})`);
257
+ result = await this._fileRead(filePath, args.offset, args.limit);
258
+ break;
259
+ }
260
+ case 'file_write': {
261
+ const filePath = resolvePath(args);
262
+ const content = args.content ?? args.text ?? args.data ?? null;
263
+ if (!filePath) throw new Error(`Missing required parameter: path (received args: ${JSON.stringify(Object.keys(args))})`);
264
+ if (content === undefined || content === null) throw new Error(`Missing required parameter: content (received args: ${JSON.stringify(Object.keys(args))})`);
265
+ // Security: validate file write permission and scan for secrets
266
+ const writeCheck = securityGuard.validateFileWrite(filePath, content, this.agentId, this.agentName);
267
+ if (!writeCheck.allowed) return `Security blocked: ${writeCheck.reason}`;
268
+ securityGuard.scanForSecrets(content, `file_write:${filePath}`, this.agentId);
269
+ result = await this._fileWrite(filePath, content);
270
+ break;
271
+ }
272
+ case 'file_list':
273
+ result = await this._fileList(resolvePath(args) || args.dir || args.directory || '.');
274
+ break;
275
+ case 'file_delete': {
276
+ const filePath = resolvePath(args);
277
+ if (!filePath) throw new Error(`Missing required parameter: path (received args: ${JSON.stringify(args)})`);
278
+ result = await this._fileDelete(filePath);
279
+ break;
280
+ }
281
+ case 'shell_exec': {
282
+ const command = args.command || args.cmd || null;
283
+ if (!command) throw new Error(`Missing required parameter: command (received args: ${JSON.stringify(Object.keys(args))})`);
284
+ // Security: validate shell command before execution
285
+ const shellCheck = securityGuard.validateShellCommand(command, this.agentId, this.agentName);
286
+ if (!shellCheck.allowed) return `Security blocked: ${shellCheck.reason}`;
287
+ result = await this._shellExec(command);
288
+ // Security: scan command output for leaked secrets
289
+ securityGuard.scanForSecrets(result, `shell_output:${command}`, this.agentId);
290
+ break;
291
+ }
292
+ case 'send_message':
293
+ result = await this._sendMessage(args.targetAgentId, args.content, args.type);
294
+ break;
295
+ case 'file_stats': {
296
+ const filePath = resolvePath(args);
297
+ if (!filePath) throw new Error(`Missing required parameter: path (received args: ${JSON.stringify(args)})`);
298
+ result = await this._fileStats(filePath);
299
+ break;
300
+ }
301
+ case 'file_append': {
302
+ const filePath = resolvePath(args);
303
+ const content = args.content ?? args.text ?? args.data ?? null;
304
+ if (!filePath) throw new Error(`Missing required parameter: path (received args: ${JSON.stringify(Object.keys(args))})`);
305
+ if (content === undefined || content === null) throw new Error(`Missing required parameter: content`);
306
+ const writeCheck = securityGuard.validateFileWrite(filePath, content, this.agentId, this.agentName);
307
+ if (!writeCheck.allowed) return `Security blocked: ${writeCheck.reason}`;
308
+ securityGuard.scanForSecrets(content, `file_append:${filePath}`, this.agentId);
309
+ result = await this._fileAppend(filePath, content);
310
+ break;
311
+ }
312
+ case 'file_patch': {
313
+ const filePath = resolvePath(args);
314
+ const oldText = args.old_text || args.oldText || null;
315
+ const newText = args.new_text ?? args.newText ?? null;
316
+ if (!filePath) throw new Error(`Missing required parameter: path`);
317
+ if (!oldText) throw new Error(`Missing required parameter: old_text`);
318
+ if (newText === null || newText === undefined) throw new Error(`Missing required parameter: new_text`);
319
+ result = await this._filePatch(filePath, oldText, newText);
320
+ break;
321
+ }
322
+ case 'workspace_files':
323
+ result = await this._workspaceFiles();
324
+ break;
325
+ case 'file_search':
326
+ result = await this._fileSearch(args.query || args.keyword || args.pattern || '');
327
+ break;
328
+ default: {
329
+ // Try plugin tools before giving up
330
+ const pluginTools = pluginRegistry.getPluginTools();
331
+ const hasPluginTool = pluginTools.some(t => t.function?.name === name);
332
+ if (hasPluginTool) {
333
+ result = await pluginRegistry.executePluginTool(name, args);
334
+ break;
335
+ }
336
+ throw new Error(`Unknown tool: ${name}`);
337
+ }
338
+ }
339
+
340
+ // Fire plugin hooks: after tool call
341
+ await pluginRegistry.fireHook(HookPoint.AFTER_TOOL_CALL, {
342
+ toolName: name, args, result, agentId: this.agentId, agentName: this.agentName,
343
+ });
344
+
345
+ return result;
346
+ }
347
+
348
+ /**
349
+ * Read a file with optional line-based offset/limit and auto-truncation for large files
350
+ */
351
+ async _fileRead(filePath, offset, limit) {
352
+ const fullPath = this._safePath(filePath);
353
+ const MAX_LINES = 800; // Auto-truncation threshold
354
+ try {
355
+ const content = await fs.readFile(fullPath, 'utf-8');
356
+ const lines = content.split('\n');
357
+ const totalLines = lines.length;
358
+
359
+ // Apply offset/limit if provided
360
+ if (offset || limit) {
361
+ const startLine = Math.max(1, offset || 1);
362
+ const endLine = limit ? Math.min(totalLines, startLine + limit - 1) : totalLines;
363
+ const slice = lines.slice(startLine - 1, endLine);
364
+ const header = `[Lines ${startLine}-${endLine} of ${totalLines} total]\n`;
365
+ return header + slice.join('\n');
366
+ }
367
+
368
+ // Auto-truncation for large files
369
+ if (totalLines > MAX_LINES) {
370
+ const truncated = lines.slice(0, MAX_LINES).join('\n');
371
+ return `${truncated}\n\n--- FILE TRUNCATED ---\nShowing ${MAX_LINES} of ${totalLines} lines. Use file_read with offset=${MAX_LINES + 1} to read more, or file_stats to check file info.`;
372
+ }
373
+
374
+ return content;
375
+ } catch (error) {
376
+ if (error.code === 'ENOENT') {
377
+ return `Error: file not found "${filePath}". Use workspace_files or file_search to find available files.`;
378
+ }
379
+ throw error;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Write a file (auto-creates directories)
385
+ */
386
+ async _fileWrite(filePath, content) {
387
+ const fullPath = this._safePath(filePath);
388
+ const dir = path.dirname(fullPath);
389
+
390
+ // Auto-create directory
391
+ if (!existsSync(dir)) {
392
+ mkdirSync(dir, { recursive: true });
393
+ }
394
+
395
+ await fs.writeFile(fullPath, content, { encoding: 'utf-8', mode: 0o644 });
396
+ return `File written: ${filePath} (${content.length} chars)`;
397
+ }
398
+
399
+ /**
400
+ * List directory contents
401
+ */
402
+ async _fileList(dirPath) {
403
+ const fullPath = this._safePath(dirPath);
404
+ try {
405
+ const entries = await fs.readdir(fullPath, { withFileTypes: true });
406
+ const items = entries.map(entry => ({
407
+ name: entry.name,
408
+ type: entry.isDirectory() ? 'directory' : 'file',
409
+ }));
410
+ return JSON.stringify(items, null, 2);
411
+ } catch (error) {
412
+ if (error.code === 'ENOENT') {
413
+ return `Error: directory not found "${dirPath}"`;
414
+ }
415
+ throw error;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Delete a file
421
+ */
422
+ async _fileDelete(filePath) {
423
+ const fullPath = this._safePath(filePath);
424
+ try {
425
+ await fs.unlink(fullPath);
426
+ return `File deleted: ${filePath}`;
427
+ } catch (error) {
428
+ if (error.code === 'ENOENT') {
429
+ return `Error: file not found "${filePath}"`;
430
+ }
431
+ throw error;
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Execute shell command (security restricted)
437
+ */
438
+ async _shellExec(command) {
439
+ // Safety whitelist: only allow specific command prefixes
440
+ const allowedPrefixes = [
441
+ 'ls', 'cat', 'head', 'tail', 'grep', 'find', 'wc',
442
+ 'node', 'npm', 'npx', 'echo', 'mkdir', 'cp', 'mv',
443
+ 'tree', 'pwd', 'which', 'git',
444
+ 'curl', 'wget', 'date', 'python', 'python3', 'env', 'sort', 'uniq', 'awk', 'sed', 'jq',
445
+ ];
446
+
447
+ const cmdName = command.trim().split(/\s+/)[0];
448
+ if (!allowedPrefixes.includes(cmdName)) {
449
+ return `Security restriction: command "${cmdName}" not allowed. Allowed commands: ${allowedPrefixes.join(', ')}`;
450
+ }
451
+
452
+ try {
453
+ const { stdout, stderr } = await execAsync(command, {
454
+ cwd: this.workspaceDir,
455
+ timeout: 30000, // 30s timeout
456
+ maxBuffer: 1024 * 1024, // 1MB output limit
457
+ });
458
+ return stdout + (stderr ? `\n[stderr]: ${stderr}` : '');
459
+ } catch (error) {
460
+ return `Command execution failed: ${error.message}`;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Send message to another Agent
466
+ */
467
+ async _sendMessage(targetAgentId, content, type = 'task') {
468
+ if (!this.messageBus) {
469
+ return 'Error: message bus not initialized';
470
+ }
471
+ this.messageBus.send({
472
+ from: this.agentId,
473
+ to: targetAgentId,
474
+ content,
475
+ type: type || 'task',
476
+ });
477
+
478
+ // chatStore recording is handled centrally by _recordAgentChat in requirement.js messageHandler
479
+ // Avoid double recording
480
+
481
+ return `Message sent to ${targetAgentId}`;
482
+ }
483
+
484
+ /**
485
+ * Save a memory for the owning Employee
486
+ */
487
+ /**
488
+ * Get file stats without reading content
489
+ */
490
+ async _fileStats(filePath) {
491
+ const fullPath = this._safePath(filePath);
492
+ try {
493
+ const stat = await fs.stat(fullPath);
494
+ const content = await fs.readFile(fullPath, 'utf-8');
495
+ const lineCount = content.split('\n').length;
496
+ const sizeKB = (stat.size / 1024).toFixed(1);
497
+ return `File: ${filePath}\nSize: ${stat.size} bytes (${sizeKB} KB)\nLines: ${lineCount}\nLast modified: ${stat.mtime.toISOString()}\n${lineCount > 800 ? `⚠️ Large file — use file_read with offset/limit to read in sections.` : `✓ File is small enough to read in full.`}`;
498
+ } catch (error) {
499
+ if (error.code === 'ENOENT') {
500
+ return `Error: file not found "${filePath}". Use workspace_files or file_search to find available files.`;
501
+ }
502
+ throw error;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Append content to a file (creates if not exists)
508
+ */
509
+ async _fileAppend(filePath, content) {
510
+ const fullPath = this._safePath(filePath);
511
+ const dir = path.dirname(fullPath);
512
+ if (!existsSync(dir)) {
513
+ mkdirSync(dir, { recursive: true });
514
+ }
515
+ await fs.appendFile(fullPath, content, { encoding: 'utf-8' });
516
+ return `Content appended to ${filePath} (${content.length} chars added)`;
517
+ }
518
+
519
+ /**
520
+ * Patch a file by replacing a specific text segment
521
+ */
522
+ async _filePatch(filePath, oldText, newText) {
523
+ const fullPath = this._safePath(filePath);
524
+ try {
525
+ const content = await fs.readFile(fullPath, 'utf-8');
526
+ const occurrences = content.split(oldText).length - 1;
527
+ if (occurrences === 0) {
528
+ return `Error: old_text not found in "${filePath}". Make sure the text matches exactly (including whitespace and newlines). Use file_read to check the current content.`;
529
+ }
530
+ if (occurrences > 1) {
531
+ return `Error: old_text found ${occurrences} times in "${filePath}". It must be unique. Provide more surrounding context to make it unique.`;
532
+ }
533
+ const patched = content.replace(oldText, newText);
534
+ await fs.writeFile(fullPath, patched, { encoding: 'utf-8', mode: 0o644 });
535
+ return `File patched: ${filePath} (replaced ${oldText.length} chars with ${newText.length} chars)`;
536
+ } catch (error) {
537
+ if (error.code === 'ENOENT') {
538
+ return `Error: file not found "${filePath}"`;
539
+ }
540
+ throw error;
541
+ }
542
+ }
543
+
544
+ /**
545
+ * List all files in workspace recursively
546
+ */
547
+ async _workspaceFiles() {
548
+ const files = [];
549
+ const walk = async (dir, prefix = '') => {
550
+ try {
551
+ const entries = await fs.readdir(dir, { withFileTypes: true });
552
+ for (const entry of entries) {
553
+ // Skip hidden dirs and common noise
554
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__') continue;
555
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
556
+ if (entry.isDirectory()) {
557
+ await walk(path.join(dir, entry.name), rel);
558
+ } else {
559
+ files.push(rel);
560
+ }
561
+ }
562
+ } catch { /* ignore unreadable dirs */ }
563
+ };
564
+ await walk(this.workspaceDir);
565
+ if (files.length === 0) return 'Workspace is empty — no files found.';
566
+ return `Files in workspace (${files.length} total):\n${files.map(f => ` ${f}`).join('\n')}`;
567
+ }
568
+
569
+ /**
570
+ * Search for files by name pattern (case-insensitive partial match)
571
+ */
572
+ async _fileSearch(query) {
573
+ if (!query) return 'Error: query parameter is required';
574
+ const queryLower = query.toLowerCase();
575
+ const matches = [];
576
+ const walk = async (dir, prefix = '') => {
577
+ try {
578
+ const entries = await fs.readdir(dir, { withFileTypes: true });
579
+ for (const entry of entries) {
580
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__') continue;
581
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
582
+ if (entry.isDirectory()) {
583
+ await walk(path.join(dir, entry.name), rel);
584
+ } else {
585
+ if (entry.name.toLowerCase().includes(queryLower)) {
586
+ matches.push(rel);
587
+ }
588
+ }
589
+ }
590
+ } catch { /* ignore unreadable dirs */ }
591
+ };
592
+ await walk(this.workspaceDir);
593
+ if (matches.length === 0) return `No files found matching "${query}". Use workspace_files to see all available files.`;
594
+ return `Files matching "${query}" (${matches.length} found):\n${matches.map(f => ` ${f}`).join('\n')}`;
595
+ }
596
+ }