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
package/src/lib/hooks.js
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle Hook System - Event-driven extensibility framework
|
|
3
|
+
*
|
|
4
|
+
* Distilled from OpenClaw's hook system (vendor/openclaw/src/hooks/internal-hooks.ts)
|
|
5
|
+
* Re-implemented as an enterprise "business process automation" hook system
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Type-safe event registration and triggering
|
|
9
|
+
* - Wildcard and specific event:action subscriptions
|
|
10
|
+
* - Error isolation (one handler failure doesn't block others)
|
|
11
|
+
* - Hook priority ordering
|
|
12
|
+
* - Async handler support with timeout protection
|
|
13
|
+
* - Debug introspection for registered hooks
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hook event types — the broad categories of lifecycle events
|
|
18
|
+
*/
|
|
19
|
+
export const HookEventType = {
|
|
20
|
+
AGENT: 'agent',
|
|
21
|
+
TASK: 'task',
|
|
22
|
+
REQUIREMENT: 'requirement',
|
|
23
|
+
DEPARTMENT: 'department',
|
|
24
|
+
SYSTEM: 'system',
|
|
25
|
+
MESSAGE: 'message',
|
|
26
|
+
LLM: 'llm',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pre-defined hook event keys (type:action combinations)
|
|
31
|
+
*/
|
|
32
|
+
export const HookEvent = {
|
|
33
|
+
// Agent lifecycle
|
|
34
|
+
AGENT_CREATED: 'agent:created',
|
|
35
|
+
AGENT_TASK_START: 'agent:task_start',
|
|
36
|
+
AGENT_TASK_END: 'agent:task_end',
|
|
37
|
+
AGENT_DISMISSED: 'agent:dismissed',
|
|
38
|
+
AGENT_ERROR: 'agent:error',
|
|
39
|
+
|
|
40
|
+
// Task lifecycle
|
|
41
|
+
TASK_ASSIGNED: 'task:assigned',
|
|
42
|
+
TASK_PROGRESS: 'task:progress',
|
|
43
|
+
TASK_COMPLETED: 'task:completed',
|
|
44
|
+
TASK_FAILED: 'task:failed',
|
|
45
|
+
TASK_REVIEW: 'task:review',
|
|
46
|
+
|
|
47
|
+
// Requirement lifecycle
|
|
48
|
+
REQ_CREATED: 'requirement:created',
|
|
49
|
+
REQ_TEAM_FORMED: 'requirement:team_formed',
|
|
50
|
+
REQ_PHASE_START: 'requirement:phase_start',
|
|
51
|
+
REQ_PHASE_END: 'requirement:phase_end',
|
|
52
|
+
REQ_COMPLETED: 'requirement:completed',
|
|
53
|
+
REQ_CANCELLED: 'requirement:cancelled',
|
|
54
|
+
|
|
55
|
+
// Department lifecycle
|
|
56
|
+
DEPT_CREATED: 'department:created',
|
|
57
|
+
DEPT_MEMBER_ADDED: 'department:member_added',
|
|
58
|
+
DEPT_MEMBER_REMOVED: 'department:member_removed',
|
|
59
|
+
|
|
60
|
+
// System events
|
|
61
|
+
SYSTEM_STARTUP: 'system:startup',
|
|
62
|
+
SYSTEM_SHUTDOWN: 'system:shutdown',
|
|
63
|
+
SYSTEM_HEALTH_CHECK: 'system:health_check',
|
|
64
|
+
SYSTEM_CONFIG_CHANGE: 'system:config_change',
|
|
65
|
+
|
|
66
|
+
// Message events
|
|
67
|
+
MESSAGE_RECEIVED: 'message:received',
|
|
68
|
+
MESSAGE_SENT: 'message:sent',
|
|
69
|
+
MESSAGE_BROADCAST: 'message:broadcast',
|
|
70
|
+
|
|
71
|
+
// LLM interaction events
|
|
72
|
+
LLM_REQUEST_START: 'llm:request_start',
|
|
73
|
+
LLM_REQUEST_END: 'llm:request_end',
|
|
74
|
+
LLM_TOKEN_USAGE: 'llm:token_usage',
|
|
75
|
+
LLM_ERROR: 'llm:error',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default handler timeout (ms)
|
|
80
|
+
*/
|
|
81
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Internal handler entry with metadata
|
|
85
|
+
* @typedef {object} HandlerEntry
|
|
86
|
+
* @property {string} id - Unique handler ID
|
|
87
|
+
* @property {Function} handler - The actual handler function
|
|
88
|
+
* @property {number} priority - Execution priority (lower = first)
|
|
89
|
+
* @property {string} source - Who registered this handler (plugin ID, module name, etc.)
|
|
90
|
+
* @property {number} timeout - Max execution time in ms
|
|
91
|
+
* @property {Date} registeredAt - When this handler was registered
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hook Event Payload - passed to every handler
|
|
96
|
+
* @typedef {object} HookEventPayload
|
|
97
|
+
* @property {string} type - Event type (agent, task, etc.)
|
|
98
|
+
* @property {string} action - Specific action (created, completed, etc.)
|
|
99
|
+
* @property {string} eventKey - Full event key (type:action)
|
|
100
|
+
* @property {object} context - Event-specific context data
|
|
101
|
+
* @property {Date} timestamp - When the event occurred
|
|
102
|
+
* @property {string[]} messages - Handlers can push messages here for feedback
|
|
103
|
+
* @property {object} meta - Mutable metadata that handlers can enrich
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
let handlerIdCounter = 0;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Hook Registry - Manages all lifecycle hooks
|
|
110
|
+
*
|
|
111
|
+
* Uses a singleton pattern (similar to OpenClaw's globalThis approach)
|
|
112
|
+
* to ensure hooks work across dynamic imports.
|
|
113
|
+
*/
|
|
114
|
+
class HookRegistry {
|
|
115
|
+
constructor() {
|
|
116
|
+
/** @type {Map<string, HandlerEntry[]>} */
|
|
117
|
+
this.handlers = new Map();
|
|
118
|
+
|
|
119
|
+
/** @type {Map<string, number>} - Track how many times each event has fired */
|
|
120
|
+
this.eventCounts = new Map();
|
|
121
|
+
|
|
122
|
+
/** @type {Array<{eventKey: string, timestamp: Date, handlerCount: number, errors: number}>} */
|
|
123
|
+
this.recentFires = [];
|
|
124
|
+
this.maxRecentFires = 100;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Register a hook handler for a specific event key
|
|
129
|
+
*
|
|
130
|
+
* @param {string} eventKey - Event type (e.g., 'agent') or specific (e.g., 'agent:created')
|
|
131
|
+
* @param {Function} handler - async (event: HookEventPayload) => void
|
|
132
|
+
* @param {object} options
|
|
133
|
+
* @param {number} options.priority - Execution priority (default: 100, lower = first)
|
|
134
|
+
* @param {string} options.source - Who registered this (for debugging)
|
|
135
|
+
* @param {number} options.timeout - Handler timeout in ms
|
|
136
|
+
* @returns {string} Handler ID for later removal
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // Listen to all agent events
|
|
140
|
+
* hookRegistry.on('agent', async (event) => {
|
|
141
|
+
* console.log('Agent event:', event.action);
|
|
142
|
+
* });
|
|
143
|
+
*
|
|
144
|
+
* // Listen to specific task completion
|
|
145
|
+
* hookRegistry.on('task:completed', async (event) => {
|
|
146
|
+
* await notifyStakeholders(event.context);
|
|
147
|
+
* });
|
|
148
|
+
*/
|
|
149
|
+
on(eventKey, handler, options = {}) {
|
|
150
|
+
const id = `hook_${++handlerIdCounter}`;
|
|
151
|
+
const entry = {
|
|
152
|
+
id,
|
|
153
|
+
handler,
|
|
154
|
+
priority: options.priority ?? 100,
|
|
155
|
+
source: options.source || 'unknown',
|
|
156
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT,
|
|
157
|
+
registeredAt: new Date(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (!this.handlers.has(eventKey)) {
|
|
161
|
+
this.handlers.set(eventKey, []);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const list = this.handlers.get(eventKey);
|
|
165
|
+
list.push(entry);
|
|
166
|
+
|
|
167
|
+
// Keep sorted by priority (lower first)
|
|
168
|
+
list.sort((a, b) => a.priority - b.priority);
|
|
169
|
+
|
|
170
|
+
return id;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Remove a specific handler by its ID
|
|
175
|
+
* @param {string} handlerId
|
|
176
|
+
* @returns {boolean} Whether the handler was found and removed
|
|
177
|
+
*/
|
|
178
|
+
off(handlerId) {
|
|
179
|
+
for (const [eventKey, entries] of this.handlers) {
|
|
180
|
+
const index = entries.findIndex(e => e.id === handlerId);
|
|
181
|
+
if (index !== -1) {
|
|
182
|
+
entries.splice(index, 1);
|
|
183
|
+
if (entries.length === 0) {
|
|
184
|
+
this.handlers.delete(eventKey);
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Remove all handlers for an event key
|
|
194
|
+
* @param {string} eventKey
|
|
195
|
+
*/
|
|
196
|
+
offAll(eventKey) {
|
|
197
|
+
this.handlers.delete(eventKey);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Remove all handlers from a specific source
|
|
202
|
+
* @param {string} source
|
|
203
|
+
* @returns {number} Number of handlers removed
|
|
204
|
+
*/
|
|
205
|
+
offBySource(source) {
|
|
206
|
+
let removed = 0;
|
|
207
|
+
for (const [eventKey, entries] of this.handlers) {
|
|
208
|
+
const before = entries.length;
|
|
209
|
+
const filtered = entries.filter(e => e.source !== source);
|
|
210
|
+
if (filtered.length !== before) {
|
|
211
|
+
removed += before - filtered.length;
|
|
212
|
+
if (filtered.length === 0) {
|
|
213
|
+
this.handlers.delete(eventKey);
|
|
214
|
+
} else {
|
|
215
|
+
this.handlers.set(eventKey, filtered);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return removed;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Trigger a hook event
|
|
224
|
+
*
|
|
225
|
+
* Calls all handlers registered for:
|
|
226
|
+
* 1. The general event type (e.g., 'agent')
|
|
227
|
+
* 2. The specific event:action combination (e.g., 'agent:created')
|
|
228
|
+
*
|
|
229
|
+
* Handlers execute in priority order. Errors are caught and logged
|
|
230
|
+
* but don't prevent other handlers from running.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} eventKey - The event key (e.g., 'agent:created')
|
|
233
|
+
* @param {object} context - Event-specific context data
|
|
234
|
+
* @returns {Promise<{messages: string[], errors: Array<{handlerId: string, source: string, error: string}>, meta: object}>}
|
|
235
|
+
*/
|
|
236
|
+
async trigger(eventKey, context = {}) {
|
|
237
|
+
const [type, action] = eventKey.includes(':')
|
|
238
|
+
? eventKey.split(':', 2)
|
|
239
|
+
: [eventKey, 'unknown'];
|
|
240
|
+
|
|
241
|
+
const event = {
|
|
242
|
+
type,
|
|
243
|
+
action,
|
|
244
|
+
eventKey,
|
|
245
|
+
context,
|
|
246
|
+
timestamp: new Date(),
|
|
247
|
+
messages: [],
|
|
248
|
+
meta: {},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Collect handlers: type-level + specific event key
|
|
252
|
+
const typeHandlers = this.handlers.get(type) || [];
|
|
253
|
+
const specificHandlers = eventKey !== type
|
|
254
|
+
? (this.handlers.get(eventKey) || [])
|
|
255
|
+
: [];
|
|
256
|
+
|
|
257
|
+
// Merge and sort by priority
|
|
258
|
+
const allHandlers = [...typeHandlers, ...specificHandlers]
|
|
259
|
+
.sort((a, b) => a.priority - b.priority);
|
|
260
|
+
|
|
261
|
+
// Track event firing
|
|
262
|
+
this.eventCounts.set(eventKey, (this.eventCounts.get(eventKey) || 0) + 1);
|
|
263
|
+
|
|
264
|
+
const errors = [];
|
|
265
|
+
|
|
266
|
+
if (allHandlers.length === 0) {
|
|
267
|
+
this._recordFire(eventKey, 0, 0);
|
|
268
|
+
return { messages: event.messages, errors, meta: event.meta };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Execute handlers sequentially (in priority order)
|
|
272
|
+
for (const entry of allHandlers) {
|
|
273
|
+
try {
|
|
274
|
+
const result = await this._executeWithTimeout(
|
|
275
|
+
entry.handler,
|
|
276
|
+
event,
|
|
277
|
+
entry.timeout,
|
|
278
|
+
);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
281
|
+
console.error(`[Hooks] Error in handler "${entry.id}" (${entry.source}) for ${eventKey}: ${message}`);
|
|
282
|
+
errors.push({
|
|
283
|
+
handlerId: entry.id,
|
|
284
|
+
source: entry.source,
|
|
285
|
+
error: message,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this._recordFire(eventKey, allHandlers.length, errors.length);
|
|
291
|
+
|
|
292
|
+
return { messages: event.messages, errors, meta: event.meta };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Execute a handler with timeout protection
|
|
297
|
+
* @param {Function} handler
|
|
298
|
+
* @param {object} event
|
|
299
|
+
* @param {number} timeoutMs
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
async _executeWithTimeout(handler, event, timeoutMs) {
|
|
303
|
+
return Promise.race([
|
|
304
|
+
handler(event),
|
|
305
|
+
new Promise((_, reject) =>
|
|
306
|
+
setTimeout(
|
|
307
|
+
() => reject(new Error(`Hook handler timed out after ${timeoutMs}ms`)),
|
|
308
|
+
timeoutMs,
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Record a fire event for introspection
|
|
316
|
+
*/
|
|
317
|
+
_recordFire(eventKey, handlerCount, errors) {
|
|
318
|
+
this.recentFires.push({
|
|
319
|
+
eventKey,
|
|
320
|
+
timestamp: new Date(),
|
|
321
|
+
handlerCount,
|
|
322
|
+
errors,
|
|
323
|
+
});
|
|
324
|
+
if (this.recentFires.length > this.maxRecentFires) {
|
|
325
|
+
this.recentFires.shift();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Clear all registered hooks (useful for testing)
|
|
331
|
+
*/
|
|
332
|
+
clear() {
|
|
333
|
+
this.handlers.clear();
|
|
334
|
+
this.eventCounts.clear();
|
|
335
|
+
this.recentFires = [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get all registered event keys (for debugging)
|
|
340
|
+
* @returns {string[]}
|
|
341
|
+
*/
|
|
342
|
+
getRegisteredKeys() {
|
|
343
|
+
return Array.from(this.handlers.keys());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get handler count for a specific event key
|
|
348
|
+
* @param {string} eventKey
|
|
349
|
+
* @returns {number}
|
|
350
|
+
*/
|
|
351
|
+
getHandlerCount(eventKey) {
|
|
352
|
+
return (this.handlers.get(eventKey) || []).length;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get full diagnostic summary
|
|
357
|
+
* @returns {object}
|
|
358
|
+
*/
|
|
359
|
+
getSummary() {
|
|
360
|
+
const keys = this.getRegisteredKeys();
|
|
361
|
+
const handlersByKey = {};
|
|
362
|
+
let totalHandlers = 0;
|
|
363
|
+
|
|
364
|
+
for (const key of keys) {
|
|
365
|
+
const entries = this.handlers.get(key) || [];
|
|
366
|
+
handlersByKey[key] = entries.map(e => ({
|
|
367
|
+
id: e.id,
|
|
368
|
+
source: e.source,
|
|
369
|
+
priority: e.priority,
|
|
370
|
+
}));
|
|
371
|
+
totalHandlers += entries.length;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
totalHandlers,
|
|
376
|
+
registeredKeys: keys.length,
|
|
377
|
+
handlersByKey,
|
|
378
|
+
eventCounts: Object.fromEntries(this.eventCounts),
|
|
379
|
+
recentFires: this.recentFires.slice(-20),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// Convenience factory for creating hook event payloads
|
|
386
|
+
// ============================================================================
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create a hook event payload with common fields filled in
|
|
390
|
+
*
|
|
391
|
+
* @param {string} type - Event type
|
|
392
|
+
* @param {string} action - Action within that type
|
|
393
|
+
* @param {object} context - Additional context
|
|
394
|
+
* @returns {HookEventPayload}
|
|
395
|
+
*/
|
|
396
|
+
export function createHookEvent(type, action, context = {}) {
|
|
397
|
+
return {
|
|
398
|
+
type,
|
|
399
|
+
action,
|
|
400
|
+
eventKey: `${type}:${action}`,
|
|
401
|
+
context,
|
|
402
|
+
timestamp: new Date(),
|
|
403
|
+
messages: [],
|
|
404
|
+
meta: {},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// Global singleton
|
|
410
|
+
// ============================================================================
|
|
411
|
+
|
|
412
|
+
export const hookRegistry = new HookRegistry();
|
|
413
|
+
|
|
414
|
+
export { HookRegistry };
|
package/src/lib/i18n.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
|
|
5
|
+
import en from '@/locales/en';
|
|
6
|
+
import zh from '@/locales/zh';
|
|
7
|
+
import ja from '@/locales/ja';
|
|
8
|
+
import ko from '@/locales/ko';
|
|
9
|
+
import es from '@/locales/es';
|
|
10
|
+
import fr from '@/locales/fr';
|
|
11
|
+
import de from '@/locales/de';
|
|
12
|
+
|
|
13
|
+
const translations = { en, zh, ja, ko, es, fr, de };
|
|
14
|
+
|
|
15
|
+
export const LANGUAGES = [
|
|
16
|
+
{ code: 'en', label: 'English', flag: '🇺🇸' },
|
|
17
|
+
{ code: 'zh', label: '中文', flag: '🇨🇳' },
|
|
18
|
+
{ code: 'ja', label: '日本語', flag: '🇯🇵' },
|
|
19
|
+
{ code: 'ko', label: '한국어', flag: '🇰🇷' },
|
|
20
|
+
{ code: 'es', label: 'Español', flag: '🇪🇸' },
|
|
21
|
+
{ code: 'fr', label: 'Français', flag: '🇫🇷' },
|
|
22
|
+
{ code: 'de', label: 'Deutsch', flag: '🇩🇪' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const DEFAULT_LANG = 'en';
|
|
26
|
+
|
|
27
|
+
const I18nContext = createContext(null);
|
|
28
|
+
|
|
29
|
+
function getNestedValue(obj, path) {
|
|
30
|
+
return path.split('.').reduce((o, k) => o?.[k], obj);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function I18nProvider({ children }) {
|
|
34
|
+
const [lang, setLangState] = useState(DEFAULT_LANG);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const saved = localStorage.getItem('idea-unlimited-lang');
|
|
38
|
+
if (saved && translations[saved]) {
|
|
39
|
+
setLangState(saved);
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const setLang = useCallback((code) => {
|
|
44
|
+
if (translations[code]) {
|
|
45
|
+
setLangState(code);
|
|
46
|
+
localStorage.setItem('idea-unlimited-lang', code);
|
|
47
|
+
document.documentElement.lang = code === 'zh' ? 'zh-CN' : code;
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const t = useCallback((key, params) => {
|
|
52
|
+
let str = getNestedValue(translations[lang], key)
|
|
53
|
+
|| getNestedValue(translations[DEFAULT_LANG], key)
|
|
54
|
+
|| key;
|
|
55
|
+
if (params) {
|
|
56
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
57
|
+
str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return str;
|
|
61
|
+
}, [lang]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<I18nContext.Provider value={{ lang, setLang, t }}>
|
|
65
|
+
{children}
|
|
66
|
+
</I18nContext.Provider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useI18n() {
|
|
71
|
+
const ctx = useContext(I18nContext);
|
|
72
|
+
if (!ctx) throw new Error('useI18n must be used within I18nProvider');
|
|
73
|
+
return ctx;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function LanguageSelector({ className = '', direction = 'up' }) {
|
|
77
|
+
const { lang, setLang } = useI18n();
|
|
78
|
+
const [open, setOpen] = useState(false);
|
|
79
|
+
const btnRef = useRef(null);
|
|
80
|
+
const [menuPos, setMenuPos] = useState({ bottom: 0, left: 0, top: 0 });
|
|
81
|
+
const current = LANGUAGES.find(l => l.code === lang) || LANGUAGES[0];
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (open && btnRef.current) {
|
|
85
|
+
const rect = btnRef.current.getBoundingClientRect();
|
|
86
|
+
setMenuPos({
|
|
87
|
+
bottom: window.innerHeight - rect.top + 4,
|
|
88
|
+
left: rect.left,
|
|
89
|
+
top: rect.bottom + 4,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}, [open]);
|
|
93
|
+
|
|
94
|
+
const menuStyle = direction === 'down'
|
|
95
|
+
? { top: menuPos.top, left: menuPos.left }
|
|
96
|
+
: { bottom: menuPos.bottom, left: menuPos.left };
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className={`relative ${className}`}>
|
|
100
|
+
<button
|
|
101
|
+
ref={btnRef}
|
|
102
|
+
onClick={() => setOpen(!open)}
|
|
103
|
+
className="flex items-center gap-1.5 px-2 py-1 rounded-lg text-xs bg-white/5 hover:bg-white/10 transition-all text-[var(--muted)] hover:text-white"
|
|
104
|
+
>
|
|
105
|
+
<span>{current.flag}</span>
|
|
106
|
+
<span>{current.label}</span>
|
|
107
|
+
<span className="text-[10px]">▾</span>
|
|
108
|
+
</button>
|
|
109
|
+
{open && (
|
|
110
|
+
<>
|
|
111
|
+
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
112
|
+
<div
|
|
113
|
+
className="fixed bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-50 py-1 min-w-[140px]"
|
|
114
|
+
style={menuStyle}
|
|
115
|
+
>
|
|
116
|
+
{LANGUAGES.map(l => (
|
|
117
|
+
<button
|
|
118
|
+
key={l.code}
|
|
119
|
+
onClick={() => { setLang(l.code); setOpen(false); }}
|
|
120
|
+
className={`w-full text-left px-3 py-1.5 text-xs flex items-center gap-2 hover:bg-white/5 transition-all ${
|
|
121
|
+
lang === l.code ? 'text-[var(--accent)]' : 'text-[var(--muted)]'
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
<span>{l.flag}</span>
|
|
125
|
+
<span>{l.label}</span>
|
|
126
|
+
{lang === l.code && <span className="ml-auto">✓</span>}
|
|
127
|
+
</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized path configuration
|
|
3
|
+
*
|
|
4
|
+
* In Electron packaged mode, data and workspace directories are set via
|
|
5
|
+
* environment variables (IDEACO_DATA_DIR / IDEACO_WORKSPACE_DIR) to point
|
|
6
|
+
* to the user's persistent userData folder, so that app updates don't
|
|
7
|
+
* overwrite user data.
|
|
8
|
+
*
|
|
9
|
+
* In dev mode or plain `next dev`, falls back to process.cwd() based paths.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
export const DATA_DIR = process.env.IDEACO_DATA_DIR
|
|
14
|
+
|| path.resolve(process.cwd(), 'data');
|
|
15
|
+
|
|
16
|
+
export const WORKSPACE_DIR = process.env.IDEACO_WORKSPACE_DIR
|
|
17
|
+
|| path.resolve(process.cwd(), 'workspace');
|
|
18
|
+
|
|
19
|
+
export const MEMORY_DIR = path.join(DATA_DIR, 'memories');
|
|
20
|
+
export const CHATS_DIR = path.join(DATA_DIR, 'chats');
|
|
21
|
+
export const AUDIT_DIR = path.join(DATA_DIR, 'audit');
|
|
22
|
+
export const STATE_FILE = path.join(DATA_DIR, 'company-state.json');
|
|
23
|
+
export const BACKUP_FILE = path.join(DATA_DIR, 'company-state.backup.json');
|
package/src/lib/store.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global State Management - Server-side Singleton
|
|
3
|
+
* Shares Company instance across Next.js API Routes
|
|
4
|
+
*
|
|
5
|
+
* Persistence:
|
|
6
|
+
* - Auto-restores from data/company-state.json on startup
|
|
7
|
+
* - Auto-saves to disk on state changes
|
|
8
|
+
* - Uses globalThis to prevent state loss during hot reload
|
|
9
|
+
*/
|
|
10
|
+
import { loadState, saveState, clearState } from '@/core/organization/persistence.js';
|
|
11
|
+
import { Company } from '@/core/index.js';
|
|
12
|
+
import { initPluginRuntime } from '@/core/system/plugin.js';
|
|
13
|
+
import { sessionManager } from '@/core/agent/session.js';
|
|
14
|
+
import { cronScheduler } from '@/core/system/cron.js';
|
|
15
|
+
import { knowledgeManager } from '@/core/employee/knowledge.js';
|
|
16
|
+
import { llmClient } from '@/core/agent/llm-agent/client.js';
|
|
17
|
+
|
|
18
|
+
const globalStore = globalThis;
|
|
19
|
+
|
|
20
|
+
if (!globalStore.__aiEnterprise) {
|
|
21
|
+
globalStore.__aiEnterprise = {
|
|
22
|
+
company: null,
|
|
23
|
+
loaded: false,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Initialize plugin runtime: inject real singletons into the plugin system
|
|
27
|
+
initPluginRuntime({
|
|
28
|
+
sessionManager,
|
|
29
|
+
cronScheduler,
|
|
30
|
+
knowledgeManager,
|
|
31
|
+
llmClient,
|
|
32
|
+
messageBus: null, // messageBus is created after Company, injected lazily
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Try restoring from disk on first startup
|
|
36
|
+
try {
|
|
37
|
+
const savedData = loadState();
|
|
38
|
+
if (savedData) {
|
|
39
|
+
globalStore.__aiEnterprise.company = Company.deserialize(savedData);
|
|
40
|
+
// Inject messageBus after restoration
|
|
41
|
+
if (globalStore.__aiEnterprise.company?.messageBus) {
|
|
42
|
+
initPluginRuntime({ messageBus: globalStore.__aiEnterprise.company.messageBus });
|
|
43
|
+
}
|
|
44
|
+
console.log('🔄 Company state restored from disk successfully');
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error('⚠️ Failed to restore state, starting with empty state:', e.message);
|
|
48
|
+
globalStore.__aiEnterprise.company = null;
|
|
49
|
+
}
|
|
50
|
+
globalStore.__aiEnterprise.loaded = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getCompany() {
|
|
54
|
+
return globalStore.__aiEnterprise.company;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setCompany(company) {
|
|
58
|
+
globalStore.__aiEnterprise.company = company;
|
|
59
|
+
// Inject messageBus into plugin runtime (only available after Company is created)
|
|
60
|
+
if (company && company.messageBus) {
|
|
61
|
+
initPluginRuntime({ messageBus: company.messageBus });
|
|
62
|
+
}
|
|
63
|
+
// Save to disk immediately
|
|
64
|
+
if (company) {
|
|
65
|
+
saveState(company);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resetCompany() {
|
|
70
|
+
globalStore.__aiEnterprise.company = null;
|
|
71
|
+
clearState();
|
|
72
|
+
}
|