ideaco 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- package/tailwind.config.mjs +11 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseAgent — Pure communication engine.
|
|
3
|
+
*
|
|
4
|
+
* This layer ONLY handles: sending/receiving messages via LLM or CLI,
|
|
5
|
+
* provider management, and availability checks.
|
|
6
|
+
*
|
|
7
|
+
* All business logic (identity, memory, personality, tasks, performance,
|
|
8
|
+
* org structure, serialization) lives in the Employee layer above.
|
|
9
|
+
*
|
|
10
|
+
* Subclasses: LLMAgent, CLIAgent, WebAgent
|
|
11
|
+
*/
|
|
12
|
+
export class BaseAgent {
|
|
13
|
+
constructor() {
|
|
14
|
+
if (new.target === BaseAgent) {
|
|
15
|
+
throw new Error('BaseAgent is abstract — use LLMAgent or CLIAgent');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ======================== Abstract Methods ========================
|
|
20
|
+
|
|
21
|
+
/** @returns {string} 'llm' | 'cli' | 'web' */
|
|
22
|
+
get agentType() {
|
|
23
|
+
throw new Error('Subclass must implement get agentType()');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether this agent can execute tasks. @returns {boolean} */
|
|
27
|
+
isAvailable() {
|
|
28
|
+
throw new Error('Subclass must implement isAvailable()');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether this agent can do lightweight LLM-style chat. @returns {boolean} */
|
|
32
|
+
canChat() {
|
|
33
|
+
throw new Error('Subclass must implement canChat()');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Send a chat request.
|
|
38
|
+
* @param {Array<{role: string, content: string}>} messages
|
|
39
|
+
* @param {object} [options] - { temperature, maxTokens }
|
|
40
|
+
* @returns {Promise<{content: string, usage: object|null}>}
|
|
41
|
+
*/
|
|
42
|
+
async chat(messages, options = {}) {
|
|
43
|
+
throw new Error('Subclass must implement chat()');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Send a chat request with tool calling support.
|
|
48
|
+
* @param {Array<{role: string, content: string}>} messages
|
|
49
|
+
* @param {object} toolExecutor - AgentToolKit instance
|
|
50
|
+
* @param {object} [options]
|
|
51
|
+
* @returns {Promise<{content: string, toolResults: Array, usage: object|null}>}
|
|
52
|
+
*/
|
|
53
|
+
async chatWithTools(messages, toolExecutor, options = {}) {
|
|
54
|
+
throw new Error('Subclass must implement chatWithTools()');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get display info about the agent's execution engine.
|
|
59
|
+
* @returns {{ name: string, provider: string, model: string, type: string }}
|
|
60
|
+
*/
|
|
61
|
+
getDisplayInfo() {
|
|
62
|
+
throw new Error('Subclass must implement getDisplayInfo()');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the provider display info for frontend rendering.
|
|
67
|
+
* @returns {{ id: string, name: string, provider: string }}
|
|
68
|
+
*/
|
|
69
|
+
getProviderDisplayInfo() {
|
|
70
|
+
throw new Error('Subclass must implement getProviderDisplayInfo()');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the fallback provider name (only meaningful for CLI agents).
|
|
75
|
+
* @returns {string|null}
|
|
76
|
+
*/
|
|
77
|
+
getFallbackProviderName() {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Switch this agent's provider.
|
|
83
|
+
* @param {object} newProvider - Provider config from ProviderRegistry
|
|
84
|
+
*/
|
|
85
|
+
switchProvider(newProvider) {
|
|
86
|
+
throw new Error('Subclass must implement switchProvider()');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Serialize agent-level (communication) fields.
|
|
91
|
+
* @returns {object}
|
|
92
|
+
*/
|
|
93
|
+
serializeAgent() {
|
|
94
|
+
throw new Error('Subclass must implement serializeAgent()');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get cost per token for this agent. Subclasses can override.
|
|
99
|
+
* @returns {number}
|
|
100
|
+
*/
|
|
101
|
+
getCostPerToken() {
|
|
102
|
+
return 0.001;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Store - Chat history file storage module
|
|
3
|
+
*
|
|
4
|
+
* Extracts chat history out of memory/company-state.json,
|
|
5
|
+
* storing it in a dedicated folder, partitioned by session chunks.
|
|
6
|
+
*
|
|
7
|
+
* Storage layout:
|
|
8
|
+
* data/chats/{sessionId}/
|
|
9
|
+
* ├── meta.json # Session metadata
|
|
10
|
+
* ├── chunk-0001.jsonl # Chat chunk file (max 50 messages per chunk)
|
|
11
|
+
* ├── chunk-0002.jsonl
|
|
12
|
+
* └── ...
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* 1. Chunk storage: one file per 50 messages to avoid single large files
|
|
16
|
+
* 2. Append writes: new messages are appended to the current chunk, no full rewrites
|
|
17
|
+
* 3. Fast recent message reads: only read the tail of the latest chunk
|
|
18
|
+
* 4. Keyword search: search historical messages for context
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
23
|
+
import { CHATS_DIR } from '../../lib/paths.js';
|
|
24
|
+
|
|
25
|
+
// Max messages per chunk
|
|
26
|
+
const MESSAGES_PER_CHUNK = 50;
|
|
27
|
+
|
|
28
|
+
// Ensure the directory exists
|
|
29
|
+
if (!fs.existsSync(CHATS_DIR)) {
|
|
30
|
+
fs.mkdirSync(CHATS_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the session directory path
|
|
35
|
+
*/
|
|
36
|
+
function getSessionDir(sessionId) {
|
|
37
|
+
return path.join(CHATS_DIR, sessionId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ensure the session directory exists
|
|
42
|
+
*/
|
|
43
|
+
function ensureSessionDir(sessionId) {
|
|
44
|
+
const dir = getSessionDir(sessionId);
|
|
45
|
+
if (!fs.existsSync(dir)) {
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate a chunk file name
|
|
53
|
+
*/
|
|
54
|
+
function chunkFileName(index) {
|
|
55
|
+
return `chunk-${String(index).padStart(4, '0')}.jsonl`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get all chunk files for a session (in order)
|
|
60
|
+
*/
|
|
61
|
+
function listChunkFiles(sessionDir) {
|
|
62
|
+
if (!fs.existsSync(sessionDir)) return [];
|
|
63
|
+
return fs.readdirSync(sessionDir)
|
|
64
|
+
.filter(f => f.startsWith('chunk-') && f.endsWith('.jsonl'))
|
|
65
|
+
.sort();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Read all messages from a JSONL file
|
|
70
|
+
*/
|
|
71
|
+
function readChunkFile(filePath) {
|
|
72
|
+
if (!fs.existsSync(filePath)) return [];
|
|
73
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
74
|
+
if (!content) return [];
|
|
75
|
+
return content.split('\n').map(line => {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(line);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}).filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Count messages in a JSONL file
|
|
86
|
+
*/
|
|
87
|
+
function countMessagesInChunk(filePath) {
|
|
88
|
+
if (!fs.existsSync(filePath)) return 0;
|
|
89
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
90
|
+
if (!content) return 0;
|
|
91
|
+
return content.split('\n').length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* ChatStore - Chat history management
|
|
96
|
+
*/
|
|
97
|
+
export class ChatStore {
|
|
98
|
+
constructor() {
|
|
99
|
+
// Cache: sessionId -> { currentChunkIndex, currentChunkCount }
|
|
100
|
+
this._cache = new Map();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a new session
|
|
105
|
+
* @param {string} sessionId - Session ID
|
|
106
|
+
* @param {object} meta - Session metadata { participants, type, title }
|
|
107
|
+
* @returns {string} sessionId
|
|
108
|
+
*/
|
|
109
|
+
createSession(sessionId, meta = {}) {
|
|
110
|
+
const dir = ensureSessionDir(sessionId);
|
|
111
|
+
const metaPath = path.join(dir, 'meta.json');
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(metaPath)) {
|
|
114
|
+
const metaData = {
|
|
115
|
+
sessionId,
|
|
116
|
+
title: meta.title || 'Conversation',
|
|
117
|
+
participants: meta.participants || [],
|
|
118
|
+
type: meta.type || 'boss-secretary',
|
|
119
|
+
createdAt: new Date().toISOString(),
|
|
120
|
+
lastActiveAt: new Date().toISOString(),
|
|
121
|
+
totalMessages: 0,
|
|
122
|
+
};
|
|
123
|
+
fs.writeFileSync(metaPath, JSON.stringify(metaData, null, 2), 'utf-8');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return sessionId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Append a message to a session
|
|
131
|
+
* @param {string} sessionId - Session ID
|
|
132
|
+
* @param {object} message - Message object { role, content, action?, time? }
|
|
133
|
+
*/
|
|
134
|
+
appendMessage(sessionId, message) {
|
|
135
|
+
const dir = ensureSessionDir(sessionId);
|
|
136
|
+
|
|
137
|
+
// Determine the current write chunk
|
|
138
|
+
const cacheEntry = this._getOrInitCache(sessionId, dir);
|
|
139
|
+
const chunkPath = path.join(dir, chunkFileName(cacheEntry.currentChunkIndex));
|
|
140
|
+
|
|
141
|
+
// Build the message record (preserving extended fields like fromAgentId, toAgentId, etc.)
|
|
142
|
+
const record = {
|
|
143
|
+
id: uuidv4(),
|
|
144
|
+
role: message.role,
|
|
145
|
+
content: message.content,
|
|
146
|
+
action: message.action || null,
|
|
147
|
+
time: message.time ? new Date(message.time).toISOString() : new Date().toISOString(),
|
|
148
|
+
};
|
|
149
|
+
// Preserve extended fields for agent-to-agent chat
|
|
150
|
+
if (message.fromAgentId) record.fromAgentId = message.fromAgentId;
|
|
151
|
+
if (message.fromAgentName) record.fromAgentName = message.fromAgentName;
|
|
152
|
+
if (message.toAgentId) record.toAgentId = message.toAgentId;
|
|
153
|
+
if (message.toAgentName) record.toAgentName = message.toAgentName;
|
|
154
|
+
|
|
155
|
+
// Append to the chunk file
|
|
156
|
+
fs.appendFileSync(chunkPath, JSON.stringify(record) + '\n', 'utf-8');
|
|
157
|
+
cacheEntry.currentChunkCount++;
|
|
158
|
+
|
|
159
|
+
// If the current chunk is full, advance to the next one
|
|
160
|
+
if (cacheEntry.currentChunkCount >= MESSAGES_PER_CHUNK) {
|
|
161
|
+
cacheEntry.currentChunkIndex++;
|
|
162
|
+
cacheEntry.currentChunkCount = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Update metadata
|
|
166
|
+
this._updateMeta(sessionId, dir);
|
|
167
|
+
|
|
168
|
+
return record;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the most recent N messages (used when an Agent wakes up to load context)
|
|
173
|
+
* @param {string} sessionId - Session ID
|
|
174
|
+
* @param {number} limit - Max messages to return
|
|
175
|
+
* @returns {Array} Message list (in ascending time order)
|
|
176
|
+
*/
|
|
177
|
+
getRecentMessages(sessionId, limit = 10) {
|
|
178
|
+
const dir = getSessionDir(sessionId);
|
|
179
|
+
if (!fs.existsSync(dir)) return [];
|
|
180
|
+
|
|
181
|
+
const chunks = listChunkFiles(dir);
|
|
182
|
+
if (chunks.length === 0) return [];
|
|
183
|
+
|
|
184
|
+
const messages = [];
|
|
185
|
+
// Read backwards from the most recent chunk
|
|
186
|
+
for (let i = chunks.length - 1; i >= 0 && messages.length < limit; i--) {
|
|
187
|
+
const chunkPath = path.join(dir, chunks[i]);
|
|
188
|
+
const chunkMessages = readChunkFile(chunkPath);
|
|
189
|
+
// Take from the tail of the chunk
|
|
190
|
+
messages.unshift(...chunkMessages);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Return only the most recent `limit` messages
|
|
194
|
+
return messages.slice(-limit);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get the total message count for a session
|
|
199
|
+
* @param {string} sessionId
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
getMessageCount(sessionId) {
|
|
203
|
+
const dir = getSessionDir(sessionId);
|
|
204
|
+
if (!fs.existsSync(dir)) return 0;
|
|
205
|
+
|
|
206
|
+
const chunks = listChunkFiles(dir);
|
|
207
|
+
let total = 0;
|
|
208
|
+
for (const chunk of chunks) {
|
|
209
|
+
total += countMessagesInChunk(path.join(dir, chunk));
|
|
210
|
+
}
|
|
211
|
+
return total;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Keyword search over historical messages
|
|
216
|
+
* Searches all chunks for messages containing the keywords, for providing Agent context
|
|
217
|
+
*
|
|
218
|
+
* @param {string} sessionId - Session ID
|
|
219
|
+
* @param {string} query - Search keywords
|
|
220
|
+
* @param {number} limit - Max messages to return
|
|
221
|
+
* @returns {Array} Matching messages (sorted by relevance)
|
|
222
|
+
*/
|
|
223
|
+
searchMessages(sessionId, query, limit = 10) {
|
|
224
|
+
const dir = getSessionDir(sessionId);
|
|
225
|
+
if (!fs.existsSync(dir)) return [];
|
|
226
|
+
|
|
227
|
+
const keywords = this._extractKeywords(query);
|
|
228
|
+
if (keywords.length === 0) return [];
|
|
229
|
+
|
|
230
|
+
const chunks = listChunkFiles(dir);
|
|
231
|
+
const scoredMessages = [];
|
|
232
|
+
|
|
233
|
+
for (const chunk of chunks) {
|
|
234
|
+
const chunkPath = path.join(dir, chunk);
|
|
235
|
+
const messages = readChunkFile(chunkPath);
|
|
236
|
+
|
|
237
|
+
for (const msg of messages) {
|
|
238
|
+
const score = this._calculateRelevance(msg, keywords);
|
|
239
|
+
if (score > 0) {
|
|
240
|
+
scoredMessages.push({ ...msg, _relevanceScore: score });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Sort by relevance and take the top N
|
|
246
|
+
scoredMessages.sort((a, b) => b._relevanceScore - a._relevanceScore);
|
|
247
|
+
return scoredMessages.slice(0, limit);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Search and return matching messages with surrounding context (window context)
|
|
252
|
+
* @param {string} sessionId
|
|
253
|
+
* @param {string} query
|
|
254
|
+
* @param {number} limit - Number of match groups to return
|
|
255
|
+
* @param {number} windowSize - How many messages to include before/after each match
|
|
256
|
+
* @returns {Array} List of message contexts
|
|
257
|
+
*/
|
|
258
|
+
searchWithContext(sessionId, query, limit = 5, windowSize = 2) {
|
|
259
|
+
const dir = getSessionDir(sessionId);
|
|
260
|
+
if (!fs.existsSync(dir)) return [];
|
|
261
|
+
|
|
262
|
+
const keywords = this._extractKeywords(query);
|
|
263
|
+
if (keywords.length === 0) return [];
|
|
264
|
+
|
|
265
|
+
// Read all messages (with global index)
|
|
266
|
+
const chunks = listChunkFiles(dir);
|
|
267
|
+
const allMessages = [];
|
|
268
|
+
for (const chunk of chunks) {
|
|
269
|
+
const chunkPath = path.join(dir, chunk);
|
|
270
|
+
allMessages.push(...readChunkFile(chunkPath));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Find matching message indices and scores
|
|
274
|
+
const matches = [];
|
|
275
|
+
for (let i = 0; i < allMessages.length; i++) {
|
|
276
|
+
const score = this._calculateRelevance(allMessages[i], keywords);
|
|
277
|
+
if (score > 0) {
|
|
278
|
+
matches.push({ index: i, score });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Sort by score
|
|
283
|
+
matches.sort((a, b) => b.score - a.score);
|
|
284
|
+
|
|
285
|
+
// Take the top N matches and return their window context
|
|
286
|
+
const contexts = [];
|
|
287
|
+
const usedIndices = new Set();
|
|
288
|
+
for (const match of matches.slice(0, limit)) {
|
|
289
|
+
const start = Math.max(0, match.index - windowSize);
|
|
290
|
+
const end = Math.min(allMessages.length - 1, match.index + windowSize);
|
|
291
|
+
|
|
292
|
+
// Deduplicate
|
|
293
|
+
if (usedIndices.has(match.index)) continue;
|
|
294
|
+
|
|
295
|
+
const contextMessages = [];
|
|
296
|
+
for (let i = start; i <= end; i++) {
|
|
297
|
+
usedIndices.add(i);
|
|
298
|
+
contextMessages.push(allMessages[i]);
|
|
299
|
+
}
|
|
300
|
+
contexts.push({
|
|
301
|
+
matchedMessage: allMessages[match.index],
|
|
302
|
+
score: match.score,
|
|
303
|
+
context: contextMessages,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return contexts;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* List all sessions
|
|
312
|
+
* @returns {Array} Session list
|
|
313
|
+
*/
|
|
314
|
+
listSessions() {
|
|
315
|
+
if (!fs.existsSync(CHATS_DIR)) return [];
|
|
316
|
+
|
|
317
|
+
const dirs = fs.readdirSync(CHATS_DIR).filter(d => {
|
|
318
|
+
const fullPath = path.join(CHATS_DIR, d);
|
|
319
|
+
return fs.statSync(fullPath).isDirectory();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return dirs.map(d => {
|
|
323
|
+
const metaPath = path.join(CHATS_DIR, d, 'meta.json');
|
|
324
|
+
if (!fs.existsSync(metaPath)) return null;
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}).filter(Boolean);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Mark a session as read (updates bossLastReadAt)
|
|
335
|
+
* @param {string} sessionId
|
|
336
|
+
*/
|
|
337
|
+
markSessionRead(sessionId) {
|
|
338
|
+
const dir = getSessionDir(sessionId);
|
|
339
|
+
const metaPath = path.join(dir, 'meta.json');
|
|
340
|
+
if (!fs.existsSync(metaPath)) return;
|
|
341
|
+
try {
|
|
342
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
343
|
+
meta.bossLastReadAt = new Date().toISOString();
|
|
344
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
345
|
+
} catch (e) {
|
|
346
|
+
// ignore
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get session metadata
|
|
352
|
+
* @param {string} sessionId
|
|
353
|
+
* @returns {object|null}
|
|
354
|
+
*/
|
|
355
|
+
getSessionMeta(sessionId) {
|
|
356
|
+
const dir = getSessionDir(sessionId);
|
|
357
|
+
const metaPath = path.join(dir, 'meta.json');
|
|
358
|
+
if (!fs.existsSync(metaPath)) return null;
|
|
359
|
+
try {
|
|
360
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Delete a session
|
|
368
|
+
* @param {string} sessionId
|
|
369
|
+
*/
|
|
370
|
+
deleteSession(sessionId) {
|
|
371
|
+
const dir = getSessionDir(sessionId);
|
|
372
|
+
if (fs.existsSync(dir)) {
|
|
373
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
374
|
+
this._cache.delete(sessionId);
|
|
375
|
+
console.log(`🗑️ Deleted session record: ${sessionId}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Migrate from a legacy chatHistory array to file storage
|
|
381
|
+
* @param {string} sessionId - Session ID
|
|
382
|
+
* @param {Array} chatHistory - Legacy chat history array
|
|
383
|
+
*/
|
|
384
|
+
migrateFromArray(sessionId, chatHistory) {
|
|
385
|
+
if (!chatHistory || chatHistory.length === 0) return;
|
|
386
|
+
|
|
387
|
+
this.createSession(sessionId, {
|
|
388
|
+
title: 'Migrated session',
|
|
389
|
+
type: 'boss-secretary',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
for (const msg of chatHistory) {
|
|
393
|
+
this.appendMessage(sessionId, {
|
|
394
|
+
role: msg.role,
|
|
395
|
+
content: msg.content,
|
|
396
|
+
action: msg.action || null,
|
|
397
|
+
time: msg.time,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
console.log(`📦 Migrated ${chatHistory.length} chat messages to file storage`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ========================================================================
|
|
404
|
+
// Group Chat Storage (Requirement / Department / Sprint group chats)
|
|
405
|
+
// ========================================================================
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Append a group chat message to file storage.
|
|
409
|
+
* The message is stored as-is (preserving from, type, visibility, etc.).
|
|
410
|
+
* @param {string} groupId - Group identifier, e.g. "req-{id}", "dept-{id}", "sprint-{id}"
|
|
411
|
+
* @param {object} message - Full group chat message object { id, from, content, type, visibility, time }
|
|
412
|
+
*/
|
|
413
|
+
appendGroupMessage(groupId, message) {
|
|
414
|
+
const sessionId = `group-${groupId}`;
|
|
415
|
+
const dir = ensureSessionDir(sessionId);
|
|
416
|
+
|
|
417
|
+
const cacheEntry = this._getOrInitCache(sessionId, dir);
|
|
418
|
+
const chunkPath = path.join(dir, chunkFileName(cacheEntry.currentChunkIndex));
|
|
419
|
+
|
|
420
|
+
// Store the message as-is, just ensure time is ISO string
|
|
421
|
+
const record = {
|
|
422
|
+
...message,
|
|
423
|
+
time: message.time ? new Date(message.time).toISOString() : new Date().toISOString(),
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
fs.appendFileSync(chunkPath, JSON.stringify(record) + '\n', 'utf-8');
|
|
427
|
+
cacheEntry.currentChunkCount++;
|
|
428
|
+
|
|
429
|
+
if (cacheEntry.currentChunkCount >= MESSAGES_PER_CHUNK) {
|
|
430
|
+
cacheEntry.currentChunkIndex++;
|
|
431
|
+
cacheEntry.currentChunkCount = 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Skip meta update for group chats (high frequency, meta is not critical)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Load group chat messages from file storage.
|
|
439
|
+
* @param {string} groupId - Group identifier
|
|
440
|
+
* @param {number} [limit=200] - Max messages to return (from tail)
|
|
441
|
+
* @returns {Array} Message list (ascending time order)
|
|
442
|
+
*/
|
|
443
|
+
getGroupMessages(groupId, limit = 200) {
|
|
444
|
+
const sessionId = `group-${groupId}`;
|
|
445
|
+
const dir = getSessionDir(sessionId);
|
|
446
|
+
if (!fs.existsSync(dir)) return [];
|
|
447
|
+
|
|
448
|
+
const chunks = listChunkFiles(dir);
|
|
449
|
+
if (chunks.length === 0) return [];
|
|
450
|
+
|
|
451
|
+
const messages = [];
|
|
452
|
+
for (let i = chunks.length - 1; i >= 0 && messages.length < limit; i--) {
|
|
453
|
+
const chunkPath = path.join(dir, chunks[i]);
|
|
454
|
+
const chunkMessages = readChunkFile(chunkPath);
|
|
455
|
+
messages.unshift(...chunkMessages);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return messages.slice(-limit);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Migrate an in-memory groupChat array to file storage (one-time migration).
|
|
463
|
+
* @param {string} groupId - Group identifier
|
|
464
|
+
* @param {Array} groupChat - Legacy in-memory groupChat array
|
|
465
|
+
*/
|
|
466
|
+
migrateGroupChat(groupId, groupChat) {
|
|
467
|
+
if (!groupChat || groupChat.length === 0) return;
|
|
468
|
+
const sessionId = `group-${groupId}`;
|
|
469
|
+
// Skip if already migrated
|
|
470
|
+
if (this.getGroupMessages(groupId, 1).length > 0) return;
|
|
471
|
+
|
|
472
|
+
ensureSessionDir(sessionId);
|
|
473
|
+
for (const msg of groupChat) {
|
|
474
|
+
this.appendGroupMessage(groupId, msg);
|
|
475
|
+
}
|
|
476
|
+
console.log(`📦 Migrated ${groupChat.length} group chat messages for ${groupId} to file storage`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Delete group chat file storage
|
|
481
|
+
* @param {string} groupId
|
|
482
|
+
*/
|
|
483
|
+
deleteGroupChat(groupId) {
|
|
484
|
+
this.deleteSession(`group-${groupId}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ========================================================================
|
|
488
|
+
// Internal Methods
|
|
489
|
+
// ========================================================================
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Get or initialize a cache entry
|
|
493
|
+
*/
|
|
494
|
+
_getOrInitCache(sessionId, dir) {
|
|
495
|
+
if (this._cache.has(sessionId)) {
|
|
496
|
+
return this._cache.get(sessionId);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const chunks = listChunkFiles(dir);
|
|
500
|
+
let currentChunkIndex = 1;
|
|
501
|
+
let currentChunkCount = 0;
|
|
502
|
+
|
|
503
|
+
if (chunks.length > 0) {
|
|
504
|
+
// Take the last chunk
|
|
505
|
+
const lastChunk = chunks[chunks.length - 1];
|
|
506
|
+
const match = lastChunk.match(/chunk-(\d+)\.jsonl/);
|
|
507
|
+
if (match) {
|
|
508
|
+
currentChunkIndex = parseInt(match[1], 10);
|
|
509
|
+
currentChunkCount = countMessagesInChunk(path.join(dir, lastChunk));
|
|
510
|
+
// If full, advance to the next
|
|
511
|
+
if (currentChunkCount >= MESSAGES_PER_CHUNK) {
|
|
512
|
+
currentChunkIndex++;
|
|
513
|
+
currentChunkCount = 0;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const entry = { currentChunkIndex, currentChunkCount };
|
|
519
|
+
this._cache.set(sessionId, entry);
|
|
520
|
+
return entry;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Update session metadata
|
|
525
|
+
*/
|
|
526
|
+
_updateMeta(sessionId, dir) {
|
|
527
|
+
const metaPath = path.join(dir, 'meta.json');
|
|
528
|
+
try {
|
|
529
|
+
let meta = {};
|
|
530
|
+
if (fs.existsSync(metaPath)) {
|
|
531
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
532
|
+
}
|
|
533
|
+
meta.lastActiveAt = new Date().toISOString();
|
|
534
|
+
meta.totalMessages = this.getMessageCount(sessionId);
|
|
535
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
536
|
+
} catch (e) {
|
|
537
|
+
// Metadata update failure does not affect the main flow
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Extract search keywords
|
|
543
|
+
* Language-agnostic: splits on whitespace, keeps the full query as an
|
|
544
|
+
* additional token so substring matches across word boundaries still work.
|
|
545
|
+
* Single-char tokens are dropped to reduce noise.
|
|
546
|
+
*/
|
|
547
|
+
_extractKeywords(query) {
|
|
548
|
+
if (!query || !query.trim()) return [];
|
|
549
|
+
|
|
550
|
+
const text = query.toLowerCase().trim();
|
|
551
|
+
const tokens = text.split(/\s+/).filter(t => t.length > 1);
|
|
552
|
+
|
|
553
|
+
// Also keep the full query itself when it differs from a single token,
|
|
554
|
+
// so multi-word phrases can match as a whole.
|
|
555
|
+
if (tokens.length > 1) {
|
|
556
|
+
tokens.push(text);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return [...new Set(tokens)];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Calculate relevance score between a message and keywords
|
|
564
|
+
* Simple BM25-style scoring
|
|
565
|
+
*/
|
|
566
|
+
_calculateRelevance(message, keywords) {
|
|
567
|
+
if (!message.content) return 0;
|
|
568
|
+
|
|
569
|
+
const content = message.content.toLowerCase();
|
|
570
|
+
let score = 0;
|
|
571
|
+
|
|
572
|
+
for (const keyword of keywords) {
|
|
573
|
+
// Exact match count
|
|
574
|
+
const regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
575
|
+
const matches = content.match(regex);
|
|
576
|
+
if (matches) {
|
|
577
|
+
// TF (term frequency) — more occurrences = higher score, with decay
|
|
578
|
+
const tf = Math.log(1 + matches.length);
|
|
579
|
+
// Length weighting (longer keywords are more meaningful)
|
|
580
|
+
const lengthBonus = Math.min(keyword.length / 3, 2);
|
|
581
|
+
score += tf * lengthBonus;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Role weighting: boss messages are more important (contain instructions/preferences)
|
|
586
|
+
if (message.role === 'boss') {
|
|
587
|
+
score *= 1.2;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Time decay (optional: more recent messages get a slight boost)
|
|
591
|
+
if (message.time) {
|
|
592
|
+
const ageHours = (Date.now() - new Date(message.time).getTime()) / (1000 * 60 * 60);
|
|
593
|
+
const freshness = Math.max(0.5, 1 - ageHours / (24 * 30)); // Linear decay to 0.5 over 30 days
|
|
594
|
+
score *= freshness;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return score;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Global singleton
|
|
602
|
+
export const chatStore = new ChatStore();
|