opc-agent 3.0.1 → 4.0.1

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 (216) hide show
  1. package/README.md +404 -74
  2. package/README.zh-CN.md +82 -0
  3. package/dist/channels/dingtalk.d.ts +17 -0
  4. package/dist/channels/dingtalk.js +38 -0
  5. package/dist/channels/googlechat.d.ts +14 -0
  6. package/dist/channels/googlechat.js +37 -0
  7. package/dist/channels/imessage.d.ts +13 -0
  8. package/dist/channels/imessage.js +28 -0
  9. package/dist/channels/irc.d.ts +20 -0
  10. package/dist/channels/irc.js +71 -0
  11. package/dist/channels/line.d.ts +14 -0
  12. package/dist/channels/line.js +28 -0
  13. package/dist/channels/matrix.d.ts +15 -0
  14. package/dist/channels/matrix.js +28 -0
  15. package/dist/channels/mattermost.d.ts +18 -0
  16. package/dist/channels/mattermost.js +49 -0
  17. package/dist/channels/msteams.d.ts +14 -0
  18. package/dist/channels/msteams.js +28 -0
  19. package/dist/channels/nostr.d.ts +14 -0
  20. package/dist/channels/nostr.js +28 -0
  21. package/dist/channels/qq.d.ts +15 -0
  22. package/dist/channels/qq.js +28 -0
  23. package/dist/channels/signal.d.ts +14 -0
  24. package/dist/channels/signal.js +28 -0
  25. package/dist/channels/sms.d.ts +15 -0
  26. package/dist/channels/sms.js +28 -0
  27. package/dist/channels/twitch.d.ts +17 -0
  28. package/dist/channels/twitch.js +59 -0
  29. package/dist/channels/voice-call.d.ts +27 -0
  30. package/dist/channels/voice-call.js +82 -0
  31. package/dist/channels/whatsapp.d.ts +14 -0
  32. package/dist/channels/whatsapp.js +28 -0
  33. package/dist/cli/chat.d.ts +2 -0
  34. package/dist/cli/chat.js +134 -0
  35. package/dist/cli/setup.d.ts +4 -0
  36. package/dist/cli/setup.js +303 -0
  37. package/dist/cli.js +142 -6
  38. package/dist/core/api-server.d.ts +25 -0
  39. package/dist/core/api-server.js +286 -0
  40. package/dist/core/audio.d.ts +50 -0
  41. package/dist/core/audio.js +68 -0
  42. package/dist/core/context-discovery.d.ts +16 -0
  43. package/dist/core/context-discovery.js +107 -0
  44. package/dist/core/context-refs.d.ts +29 -0
  45. package/dist/core/context-refs.js +162 -0
  46. package/dist/core/gateway.d.ts +53 -0
  47. package/dist/core/gateway.js +80 -0
  48. package/dist/core/heartbeat.d.ts +19 -0
  49. package/dist/core/heartbeat.js +50 -0
  50. package/dist/core/hooks.d.ts +28 -0
  51. package/dist/core/hooks.js +82 -0
  52. package/dist/core/ide-bridge.d.ts +53 -0
  53. package/dist/core/ide-bridge.js +97 -0
  54. package/dist/core/node-network.d.ts +23 -0
  55. package/dist/core/node-network.js +77 -0
  56. package/dist/core/profiles.d.ts +27 -0
  57. package/dist/core/profiles.js +131 -0
  58. package/dist/core/sandbox.d.ts +25 -0
  59. package/dist/core/sandbox.js +84 -1
  60. package/dist/core/session-manager.d.ts +33 -0
  61. package/dist/core/session-manager.js +157 -0
  62. package/dist/core/vision.d.ts +45 -0
  63. package/dist/core/vision.js +177 -0
  64. package/dist/hub/brain-seed.d.ts +14 -0
  65. package/dist/hub/brain-seed.js +77 -0
  66. package/dist/hub/client.d.ts +25 -0
  67. package/dist/hub/client.js +44 -0
  68. package/dist/index.d.ts +66 -1
  69. package/dist/index.js +95 -3
  70. package/dist/memory/context-compressor.d.ts +43 -0
  71. package/dist/memory/context-compressor.js +167 -0
  72. package/dist/memory/index.d.ts +4 -0
  73. package/dist/memory/index.js +5 -1
  74. package/dist/memory/user-profiler.d.ts +50 -0
  75. package/dist/memory/user-profiler.js +201 -0
  76. package/dist/providers/index.d.ts +1 -1
  77. package/dist/providers/index.js +54 -1
  78. package/dist/scheduler/cron-engine.d.ts +41 -0
  79. package/dist/scheduler/cron-engine.js +200 -0
  80. package/dist/scheduler/index.d.ts +3 -0
  81. package/dist/scheduler/index.js +7 -0
  82. package/dist/schema/oad.d.ts +12 -12
  83. package/dist/security/approvals.d.ts +53 -0
  84. package/dist/security/approvals.js +115 -0
  85. package/dist/security/elevated.d.ts +41 -0
  86. package/dist/security/elevated.js +89 -0
  87. package/dist/security/index.d.ts +6 -0
  88. package/dist/security/index.js +7 -1
  89. package/dist/security/secrets.d.ts +34 -0
  90. package/dist/security/secrets.js +115 -0
  91. package/dist/skills/builtin/index.d.ts +6 -0
  92. package/dist/skills/builtin/index.js +402 -0
  93. package/dist/skills/marketplace.d.ts +30 -0
  94. package/dist/skills/marketplace.js +142 -0
  95. package/dist/skills/types.d.ts +34 -0
  96. package/dist/skills/types.js +16 -0
  97. package/dist/studio/server.d.ts +25 -0
  98. package/dist/studio/server.js +780 -0
  99. package/dist/studio/templates-data.d.ts +21 -0
  100. package/dist/studio/templates-data.js +148 -0
  101. package/dist/studio-ui/index.html +2502 -1073
  102. package/dist/tools/builtin/browser.d.ts +47 -0
  103. package/dist/tools/builtin/browser.js +284 -0
  104. package/dist/tools/builtin/home-assistant.d.ts +12 -0
  105. package/dist/tools/builtin/home-assistant.js +126 -0
  106. package/dist/tools/builtin/index.d.ts +7 -1
  107. package/dist/tools/builtin/index.js +23 -2
  108. package/dist/tools/builtin/rl-tools.d.ts +13 -0
  109. package/dist/tools/builtin/rl-tools.js +228 -0
  110. package/dist/tools/builtin/vision.d.ts +6 -0
  111. package/dist/tools/builtin/vision.js +61 -0
  112. package/dist/tools/builtin/web-search.d.ts +9 -0
  113. package/dist/tools/builtin/web-search.js +150 -0
  114. package/dist/tools/document-processor.d.ts +39 -0
  115. package/dist/tools/document-processor.js +188 -0
  116. package/dist/tools/image-generator.d.ts +42 -0
  117. package/dist/tools/image-generator.js +136 -0
  118. package/dist/tools/web-scraper.d.ts +20 -0
  119. package/dist/tools/web-scraper.js +148 -0
  120. package/dist/tools/web-search.d.ts +51 -0
  121. package/dist/tools/web-search.js +152 -0
  122. package/install.ps1 +154 -0
  123. package/install.sh +164 -0
  124. package/package.json +63 -52
  125. package/src/channels/dingtalk.ts +46 -0
  126. package/src/channels/googlechat.ts +42 -0
  127. package/src/channels/imessage.ts +32 -0
  128. package/src/channels/irc.ts +82 -0
  129. package/src/channels/line.ts +33 -0
  130. package/src/channels/matrix.ts +34 -0
  131. package/src/channels/mattermost.ts +57 -0
  132. package/src/channels/msteams.ts +33 -0
  133. package/src/channels/nostr.ts +33 -0
  134. package/src/channels/qq.ts +34 -0
  135. package/src/channels/signal.ts +33 -0
  136. package/src/channels/sms.ts +34 -0
  137. package/src/channels/twitch.ts +65 -0
  138. package/src/channels/voice-call.ts +100 -0
  139. package/src/channels/whatsapp.ts +33 -0
  140. package/src/cli/chat.ts +99 -0
  141. package/src/cli/setup.ts +314 -0
  142. package/src/cli.ts +148 -6
  143. package/src/core/api-server.ts +277 -0
  144. package/src/core/audio.ts +98 -0
  145. package/src/core/context-discovery.ts +85 -0
  146. package/src/core/context-refs.ts +140 -0
  147. package/src/core/gateway.ts +106 -0
  148. package/src/core/heartbeat.ts +51 -0
  149. package/src/core/hooks.ts +105 -0
  150. package/src/core/ide-bridge.ts +133 -0
  151. package/src/core/node-network.ts +86 -0
  152. package/src/core/profiles.ts +122 -0
  153. package/src/core/sandbox.ts +100 -0
  154. package/src/core/session-manager.ts +137 -0
  155. package/src/core/vision.ts +180 -0
  156. package/src/hub/brain-seed.ts +54 -0
  157. package/src/hub/client.ts +60 -0
  158. package/src/index.ts +86 -1
  159. package/src/memory/context-compressor.ts +189 -0
  160. package/src/memory/index.ts +4 -0
  161. package/src/memory/user-profiler.ts +215 -0
  162. package/src/providers/index.ts +64 -1
  163. package/src/scheduler/cron-engine.ts +191 -0
  164. package/src/scheduler/index.ts +2 -0
  165. package/src/security/approvals.ts +143 -0
  166. package/src/security/elevated.ts +105 -0
  167. package/src/security/index.ts +6 -0
  168. package/src/security/secrets.ts +129 -0
  169. package/src/skills/builtin/index.ts +408 -0
  170. package/src/skills/marketplace.ts +113 -0
  171. package/src/skills/types.ts +42 -0
  172. package/src/studio/server.ts +1591 -791
  173. package/src/studio/templates-data.ts +178 -0
  174. package/src/studio-ui/index.html +2502 -1073
  175. package/src/tools/builtin/browser.ts +299 -0
  176. package/src/tools/builtin/home-assistant.ts +116 -0
  177. package/src/tools/builtin/index.ts +37 -28
  178. package/src/tools/builtin/rl-tools.ts +243 -0
  179. package/src/tools/builtin/vision.ts +64 -0
  180. package/src/tools/builtin/web-search.ts +126 -0
  181. package/src/tools/document-processor.ts +213 -0
  182. package/src/tools/image-generator.ts +150 -0
  183. package/src/tools/web-scraper.ts +179 -0
  184. package/src/tools/web-search.ts +180 -0
  185. package/tests/api-server.test.ts +148 -0
  186. package/tests/approvals.test.ts +89 -0
  187. package/tests/audio.test.ts +40 -0
  188. package/tests/browser.test.ts +179 -0
  189. package/tests/builtin-tools.test.ts +83 -83
  190. package/tests/channels-extra.test.ts +45 -0
  191. package/tests/context-compressor.test.ts +172 -0
  192. package/tests/context-refs.test.ts +121 -0
  193. package/tests/cron-engine.test.ts +101 -0
  194. package/tests/document-processor.test.ts +69 -0
  195. package/tests/e2e-nocode.test.ts +442 -0
  196. package/tests/elevated.test.ts +69 -0
  197. package/tests/gateway.test.ts +63 -71
  198. package/tests/home-assistant.test.ts +40 -0
  199. package/tests/hooks.test.ts +79 -0
  200. package/tests/ide-bridge.test.ts +38 -0
  201. package/tests/image-generator.test.ts +84 -0
  202. package/tests/node-network.test.ts +74 -0
  203. package/tests/profiles.test.ts +61 -0
  204. package/tests/rl-tools.test.ts +93 -0
  205. package/tests/sandbox-manager.test.ts +46 -0
  206. package/tests/secrets.test.ts +107 -0
  207. package/tests/settings-api.test.ts +148 -0
  208. package/tests/setup.test.ts +73 -0
  209. package/tests/studio.test.ts +402 -229
  210. package/tests/tools/builtin-extended.test.ts +138 -138
  211. package/tests/user-profiler.test.ts +169 -0
  212. package/tests/v090-features.test.ts +254 -0
  213. package/tests/vision.test.ts +61 -0
  214. package/tests/voice-call.test.ts +47 -0
  215. package/tests/voice-interaction.test.ts +38 -0
  216. package/tests/web-search.test.ts +155 -0
@@ -0,0 +1,243 @@
1
+ import type { MCPTool, MCPToolResult } from '../mcp';
2
+
3
+ // In-memory storage fallback
4
+ interface Trajectory {
5
+ id: string;
6
+ taskType: string;
7
+ actions: Array<{ action: string; timestamp: number; reward?: number }>;
8
+ outcome?: 'success' | 'partial' | 'failure';
9
+ totalReward: number;
10
+ }
11
+
12
+ interface PolicyEntry {
13
+ taskType: string;
14
+ preferredActions: string[];
15
+ weights: Record<string, number>;
16
+ }
17
+
18
+ const trajectories: Trajectory[] = [];
19
+ const policies = new Map<string, PolicyEntry>();
20
+ let currentEpisode: Trajectory | null = null;
21
+
22
+ function getOrCreateEpisode(taskType: string): Trajectory {
23
+ if (!currentEpisode || currentEpisode.taskType !== taskType) {
24
+ currentEpisode = {
25
+ id: `ep_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
26
+ taskType,
27
+ actions: [],
28
+ totalReward: 0,
29
+ };
30
+ trajectories.push(currentEpisode);
31
+ }
32
+ return currentEpisode;
33
+ }
34
+
35
+ export const rlRecordTrajectory: MCPTool = {
36
+ name: 'rl_record_trajectory',
37
+ description: 'Record action sequences and outcomes for RL training',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ taskType: { type: 'string' },
42
+ action: { type: 'string' },
43
+ outcome: { type: 'string', enum: ['success', 'partial', 'failure'] },
44
+ },
45
+ required: ['taskType', 'action'],
46
+ },
47
+ async execute(input): Promise<MCPToolResult> {
48
+ const ep = getOrCreateEpisode(input.taskType as string);
49
+ ep.actions.push({ action: input.action as string, timestamp: Date.now() });
50
+ if (input.outcome) ep.outcome = input.outcome as Trajectory['outcome'];
51
+ return { content: JSON.stringify({ episodeId: ep.id, actionsRecorded: ep.actions.length }) };
52
+ },
53
+ };
54
+
55
+ export const rlEvaluateOutcome: MCPTool = {
56
+ name: 'rl_evaluate_outcome',
57
+ description: "Score an action's outcome (success/partial/failure)",
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ episodeId: { type: 'string' },
62
+ outcome: { type: 'string', enum: ['success', 'partial', 'failure'] },
63
+ },
64
+ required: ['outcome'],
65
+ },
66
+ async execute(input): Promise<MCPToolResult> {
67
+ const ep = input.episodeId
68
+ ? trajectories.find(t => t.id === input.episodeId)
69
+ : currentEpisode;
70
+ if (!ep) return { content: 'No active episode found', isError: true };
71
+ ep.outcome = input.outcome as Trajectory['outcome'];
72
+ const score = ep.outcome === 'success' ? 1 : ep.outcome === 'partial' ? 0.5 : 0;
73
+ return { content: JSON.stringify({ episodeId: ep.id, outcome: ep.outcome, score }) };
74
+ },
75
+ };
76
+
77
+ export const rlGetBestStrategy: MCPTool = {
78
+ name: 'rl_get_best_strategy',
79
+ description: 'Retrieve best-performing strategy for a task type',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: { taskType: { type: 'string' } },
83
+ required: ['taskType'],
84
+ },
85
+ async execute(input): Promise<MCPToolResult> {
86
+ const taskType = input.taskType as string;
87
+ const relevant = trajectories.filter(t => t.taskType === taskType && t.outcome === 'success');
88
+ if (relevant.length === 0) return { content: JSON.stringify({ strategy: null, message: 'No successful strategies found' }) };
89
+ const best = relevant.reduce((a, b) => a.totalReward >= b.totalReward ? a : b);
90
+ return { content: JSON.stringify({ strategy: best.actions.map(a => a.action), totalReward: best.totalReward }) };
91
+ },
92
+ };
93
+
94
+ export const rlCompareStrategies: MCPTool = {
95
+ name: 'rl_compare_strategies',
96
+ description: 'Compare multiple strategies by success rate',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: { taskType: { type: 'string' } },
100
+ required: ['taskType'],
101
+ },
102
+ async execute(input): Promise<MCPToolResult> {
103
+ const taskType = input.taskType as string;
104
+ const relevant = trajectories.filter(t => t.taskType === taskType && t.outcome);
105
+ const total = relevant.length;
106
+ const successes = relevant.filter(t => t.outcome === 'success').length;
107
+ const partials = relevant.filter(t => t.outcome === 'partial').length;
108
+ const failures = relevant.filter(t => t.outcome === 'failure').length;
109
+ return {
110
+ content: JSON.stringify({
111
+ taskType, total, successRate: total ? successes / total : 0,
112
+ breakdown: { successes, partials, failures },
113
+ }),
114
+ };
115
+ },
116
+ };
117
+
118
+ export const rlGenerateTrainingData: MCPTool = {
119
+ name: 'rl_generate_training_data',
120
+ description: 'Export trajectories as fine-tuning JSONL',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: { taskType: { type: 'string' }, minReward: { type: 'number' } },
124
+ },
125
+ async execute(input): Promise<MCPToolResult> {
126
+ let data = trajectories;
127
+ if (input.taskType) data = data.filter(t => t.taskType === input.taskType);
128
+ if (input.minReward != null) data = data.filter(t => t.totalReward >= (input.minReward as number));
129
+ const jsonl = data.map(t => JSON.stringify({
130
+ messages: [
131
+ { role: 'system', content: `Task: ${t.taskType}` },
132
+ ...t.actions.map(a => ({ role: 'assistant', content: a.action })),
133
+ ],
134
+ outcome: t.outcome,
135
+ reward: t.totalReward,
136
+ })).join('\n');
137
+ return { content: jsonl || '(no data)' };
138
+ },
139
+ };
140
+
141
+ export const rlRewardSignal: MCPTool = {
142
+ name: 'rl_reward_signal',
143
+ description: 'Record positive/negative reward for last action',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: { reward: { type: 'number' }, reason: { type: 'string' } },
147
+ required: ['reward'],
148
+ },
149
+ async execute(input): Promise<MCPToolResult> {
150
+ if (!currentEpisode || currentEpisode.actions.length === 0) {
151
+ return { content: 'No current episode or actions to reward', isError: true };
152
+ }
153
+ const lastAction = currentEpisode.actions[currentEpisode.actions.length - 1];
154
+ lastAction.reward = input.reward as number;
155
+ currentEpisode.totalReward += input.reward as number;
156
+ return { content: JSON.stringify({ action: lastAction.action, reward: input.reward, totalReward: currentEpisode.totalReward }) };
157
+ },
158
+ };
159
+
160
+ export const rlExplorationSuggest: MCPTool = {
161
+ name: 'rl_exploration_suggest',
162
+ description: 'Suggest alternative approaches (exploration)',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: { taskType: { type: 'string' }, currentAction: { type: 'string' } },
166
+ required: ['taskType'],
167
+ },
168
+ async execute(input): Promise<MCPToolResult> {
169
+ const taskType = input.taskType as string;
170
+ const allActions = new Set<string>();
171
+ trajectories.filter(t => t.taskType === taskType).forEach(t => t.actions.forEach(a => allActions.add(a.action)));
172
+ const suggestions = Array.from(allActions).filter(a => a !== input.currentAction).slice(0, 5);
173
+ if (suggestions.length === 0) {
174
+ return { content: JSON.stringify({ suggestions: [], message: 'No alternative actions found. Try a completely new approach.' }) };
175
+ }
176
+ return { content: JSON.stringify({ suggestions }) };
177
+ },
178
+ };
179
+
180
+ export const rlUpdatePolicy: MCPTool = {
181
+ name: 'rl_update_policy',
182
+ description: "Update agent's action preferences based on rewards",
183
+ inputSchema: {
184
+ type: 'object',
185
+ properties: { taskType: { type: 'string' }, action: { type: 'string' }, weight: { type: 'number' } },
186
+ required: ['taskType', 'action', 'weight'],
187
+ },
188
+ async execute(input): Promise<MCPToolResult> {
189
+ const taskType = input.taskType as string;
190
+ let policy = policies.get(taskType);
191
+ if (!policy) {
192
+ policy = { taskType, preferredActions: [], weights: {} };
193
+ policies.set(taskType, policy);
194
+ }
195
+ const action = input.action as string;
196
+ policy.weights[action] = (policy.weights[action] || 0) + (input.weight as number);
197
+ policy.preferredActions = Object.entries(policy.weights)
198
+ .sort(([, a], [, b]) => b - a)
199
+ .map(([k]) => k);
200
+ return { content: JSON.stringify({ taskType, preferredActions: policy.preferredActions.slice(0, 5), weights: policy.weights }) };
201
+ },
202
+ };
203
+
204
+ export const rlGetStatistics: MCPTool = {
205
+ name: 'rl_get_statistics',
206
+ description: 'Get success/failure stats by task type',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: { taskType: { type: 'string' } },
210
+ },
211
+ async execute(input): Promise<MCPToolResult> {
212
+ let data = trajectories;
213
+ if (input.taskType) data = data.filter(t => t.taskType === input.taskType);
214
+ const stats: Record<string, { total: number; success: number; partial: number; failure: number; avgReward: number }> = {};
215
+ for (const t of data) {
216
+ if (!stats[t.taskType]) stats[t.taskType] = { total: 0, success: 0, partial: 0, failure: 0, avgReward: 0 };
217
+ const s = stats[t.taskType];
218
+ s.total++;
219
+ if (t.outcome === 'success') s.success++;
220
+ else if (t.outcome === 'partial') s.partial++;
221
+ else if (t.outcome === 'failure') s.failure++;
222
+ s.avgReward = (s.avgReward * (s.total - 1) + t.totalReward) / s.total;
223
+ }
224
+ return { content: JSON.stringify(stats) };
225
+ },
226
+ };
227
+
228
+ export const rlResetEpisode: MCPTool = {
229
+ name: 'rl_reset_episode',
230
+ description: 'Clear current episode state',
231
+ inputSchema: { type: 'object', properties: {} },
232
+ async execute(): Promise<MCPToolResult> {
233
+ const had = currentEpisode != null;
234
+ currentEpisode = null;
235
+ return { content: JSON.stringify({ reset: true, hadActiveEpisode: had }) };
236
+ },
237
+ };
238
+
239
+ export const rlTools: MCPTool[] = [
240
+ rlRecordTrajectory, rlEvaluateOutcome, rlGetBestStrategy, rlCompareStrategies,
241
+ rlGenerateTrainingData, rlRewardSignal, rlExplorationSuggest, rlUpdatePolicy,
242
+ rlGetStatistics, rlResetEpisode,
243
+ ];
@@ -0,0 +1,64 @@
1
+ import type { MCPTool, MCPToolResult } from '../mcp';
2
+ import { VisionManager } from '../../core/vision';
3
+ import type { ImageInput } from '../../core/vision';
4
+
5
+ const manager = new VisionManager();
6
+
7
+ export const visionAnalyzeTool: MCPTool = {
8
+ name: 'vision_analyze',
9
+ description: 'Analyze an image using vision AI. Provide image as URL or base64.',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ image_url: { type: 'string', description: 'URL of the image to analyze' },
14
+ image_base64: { type: 'string', description: 'Base64-encoded image data' },
15
+ prompt: { type: 'string', description: 'Optional prompt for analysis' },
16
+ },
17
+ },
18
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
19
+ const imgInput: ImageInput = input.image_url
20
+ ? { type: 'url', data: input.image_url as string }
21
+ : { type: 'base64', data: input.image_base64 as string };
22
+ const result = await manager.analyze(imgInput, input.prompt as string | undefined);
23
+ return { content: JSON.stringify(result) };
24
+ },
25
+ };
26
+
27
+ export const visionExtractTextTool: MCPTool = {
28
+ name: 'vision_extract_text',
29
+ description: 'Extract text (OCR) from an image.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ image_url: { type: 'string', description: 'URL of the image' },
34
+ image_base64: { type: 'string', description: 'Base64-encoded image data' },
35
+ },
36
+ },
37
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
38
+ const imgInput: ImageInput = input.image_url
39
+ ? { type: 'url', data: input.image_url as string }
40
+ : { type: 'base64', data: input.image_base64 as string };
41
+ const text = await manager.extractText(imgInput);
42
+ return { content: text };
43
+ },
44
+ };
45
+
46
+ export const visionCompareTool: MCPTool = {
47
+ name: 'vision_compare',
48
+ description: 'Compare multiple images.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ image_urls: { type: 'array', items: { type: 'string' }, description: 'URLs of images to compare' },
53
+ prompt: { type: 'string', description: 'Optional comparison prompt' },
54
+ },
55
+ },
56
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
57
+ const urls = input.image_urls as string[];
58
+ const images: ImageInput[] = urls.map(url => ({ type: 'url' as const, data: url }));
59
+ const result = await manager.compareImages(images, input.prompt as string | undefined);
60
+ return { content: result };
61
+ },
62
+ };
63
+
64
+ export const visionTools: MCPTool[] = [visionAnalyzeTool, visionExtractTextTool, visionCompareTool];
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Web Search & Read Built-in Tools - v0.10.0
3
+ * Registers web_search and web_read as agent-callable tools.
4
+ */
5
+
6
+ import type { MCPTool, MCPToolResult } from '../mcp';
7
+ import { webSearch, DEFAULT_SEARCH_CONFIG, type WebSearchConfig, type SearchEngine } from '../web-search';
8
+ import { scrapeUrl } from '../web-scraper';
9
+ import { existsSync, readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import * as os from 'os';
12
+
13
+ function loadSearchConfig(): WebSearchConfig {
14
+ try {
15
+ const cfgPath = join(os.homedir(), '.opc', 'config.json');
16
+ if (existsSync(cfgPath)) {
17
+ const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
18
+ if (cfg.webSearch) {
19
+ return { ...DEFAULT_SEARCH_CONFIG, ...cfg.webSearch };
20
+ }
21
+ }
22
+ } catch { /* ignore */ }
23
+ return DEFAULT_SEARCH_CONFIG;
24
+ }
25
+
26
+ export const webSearchTool: MCPTool = {
27
+ name: 'web_search',
28
+ description: 'Search the internet for information. Returns titles, URLs, and snippets from search results. Use when you need current information or facts you\'re unsure about.',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ query: {
33
+ type: 'string',
34
+ description: 'Search query string',
35
+ },
36
+ maxResults: {
37
+ type: 'number',
38
+ description: 'Maximum number of results to return (default: 5)',
39
+ },
40
+ engine: {
41
+ type: 'string',
42
+ enum: ['duckduckgo', 'brave', 'searxng', 'google'],
43
+ description: 'Search engine to use (default: configured engine)',
44
+ },
45
+ },
46
+ required: ['query'],
47
+ },
48
+
49
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
50
+ const query = String(input.query ?? '');
51
+ if (!query.trim()) {
52
+ return { content: 'Error: empty search query', isError: true };
53
+ }
54
+
55
+ const config = loadSearchConfig();
56
+ if (!config.enabled) {
57
+ return { content: 'Web search is disabled in settings.', isError: true };
58
+ }
59
+
60
+ try {
61
+ const results = await webSearch(query, config, {
62
+ maxResults: (input.maxResults as number) || 5,
63
+ engine: input.engine as SearchEngine | undefined,
64
+ });
65
+
66
+ if (results.length === 0) {
67
+ return { content: `No results found for: ${query}` };
68
+ }
69
+
70
+ const formatted = results.map((r, i) =>
71
+ `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}`
72
+ ).join('\n\n');
73
+
74
+ return {
75
+ content: `Search results for "${query}":\n\n${formatted}`,
76
+ metadata: { resultCount: results.length, query },
77
+ };
78
+ } catch (err) {
79
+ return {
80
+ content: `Search error: ${err instanceof Error ? err.message : String(err)}`,
81
+ isError: true,
82
+ };
83
+ }
84
+ },
85
+ };
86
+
87
+ export const webReadTool: MCPTool = {
88
+ name: 'web_read',
89
+ description: 'Read and extract the main content from a web page URL. Returns clean markdown text. Use to get detailed information from a specific page.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ url: {
94
+ type: 'string',
95
+ description: 'URL of the web page to read',
96
+ },
97
+ maxLength: {
98
+ type: 'number',
99
+ description: 'Maximum content length in characters (default: 5000)',
100
+ },
101
+ },
102
+ required: ['url'],
103
+ },
104
+
105
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
106
+ const url = String(input.url ?? '');
107
+ if (!url.trim()) {
108
+ return { content: 'Error: empty URL', isError: true };
109
+ }
110
+
111
+ try {
112
+ const result = await scrapeUrl(url, (input.maxLength as number) || 5000);
113
+ return {
114
+ content: `# ${result.title}\n\nSource: ${result.url}\nWords: ${result.wordCount}\n\n---\n\n${result.content}`,
115
+ metadata: { title: result.title, url: result.url, wordCount: result.wordCount },
116
+ };
117
+ } catch (err) {
118
+ return {
119
+ content: `Scrape error: ${err instanceof Error ? err.message : String(err)}`,
120
+ isError: true,
121
+ };
122
+ }
123
+ },
124
+ };
125
+
126
+ export const webSearchTools: MCPTool[] = [webSearchTool, webReadTool];
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Document Processor - Parse and chunk documents for knowledge learning
3
+ * Supports: PDF, TXT, MD, DOCX, CSV, JSON
4
+ */
5
+
6
+ export interface DocumentChunk {
7
+ title: string;
8
+ content: string;
9
+ metadata: {
10
+ source: string;
11
+ format: string;
12
+ chunkIndex: number;
13
+ totalChunks?: number;
14
+ page?: number;
15
+ };
16
+ }
17
+
18
+ export interface ProcessedDocument {
19
+ id: string;
20
+ filename: string;
21
+ format: string;
22
+ size: number;
23
+ chunks: DocumentChunk[];
24
+ processedAt: string;
25
+ }
26
+
27
+ const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
28
+ const CHUNK_TARGET_CHARS = 2000; // ~500 tokens
29
+ const CHUNK_MAX_CHARS = 4000; // ~1000 tokens
30
+
31
+ export class DocumentProcessor {
32
+ /**
33
+ * Process a file buffer into chunks
34
+ */
35
+ async process(buffer: Buffer, filename: string): Promise<ProcessedDocument> {
36
+ if (buffer.length > MAX_FILE_SIZE) {
37
+ throw new Error(`File too large: ${(buffer.length / 1024 / 1024).toFixed(1)}MB (max 50MB)`);
38
+ }
39
+
40
+ const ext = filename.split('.').pop()?.toLowerCase() || '';
41
+ let rawText: string;
42
+
43
+ switch (ext) {
44
+ case 'pdf':
45
+ rawText = await this.parsePDF(buffer);
46
+ break;
47
+ case 'docx':
48
+ rawText = await this.parseDOCX(buffer);
49
+ break;
50
+ case 'csv':
51
+ rawText = this.parseCSV(buffer.toString('utf-8'));
52
+ break;
53
+ case 'json':
54
+ rawText = this.parseJSON(buffer.toString('utf-8'));
55
+ break;
56
+ case 'txt':
57
+ case 'md':
58
+ case 'markdown':
59
+ rawText = buffer.toString('utf-8');
60
+ break;
61
+ default:
62
+ // Try as plain text
63
+ rawText = buffer.toString('utf-8');
64
+ }
65
+
66
+ const chunks = this.chunkText(rawText, filename, ext);
67
+
68
+ return {
69
+ id: `doc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
70
+ filename,
71
+ format: ext,
72
+ size: buffer.length,
73
+ chunks,
74
+ processedAt: new Date().toISOString(),
75
+ };
76
+ }
77
+
78
+ private async parsePDF(buffer: Buffer): Promise<string> {
79
+ try {
80
+ const pdfParse = require('pdf-parse');
81
+ const data = await pdfParse(buffer);
82
+ return data.text || '';
83
+ } catch (e: any) {
84
+ throw new Error(`PDF parse failed: ${e.message}`);
85
+ }
86
+ }
87
+
88
+ private async parseDOCX(buffer: Buffer): Promise<string> {
89
+ try {
90
+ const mammoth = require('mammoth');
91
+ const result = await mammoth.extractRawText({ buffer });
92
+ return result.value || '';
93
+ } catch (e: any) {
94
+ throw new Error(`DOCX parse failed: ${e.message}`);
95
+ }
96
+ }
97
+
98
+ private parseCSV(text: string): string {
99
+ const lines = text.split('\n').filter(l => l.trim());
100
+ if (lines.length === 0) return '';
101
+
102
+ const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
103
+ const rows = lines.slice(1);
104
+
105
+ // Convert CSV to readable text
106
+ return rows.map((row, i) => {
107
+ const values = this.parseCSVLine(row);
108
+ const pairs = headers.map((h, j) => `${h}: ${values[j] || ''}`);
109
+ return `Record ${i + 1}:\n${pairs.join('\n')}`;
110
+ }).join('\n\n');
111
+ }
112
+
113
+ private parseCSVLine(line: string): string[] {
114
+ const result: string[] = [];
115
+ let current = '';
116
+ let inQuotes = false;
117
+ for (const ch of line) {
118
+ if (ch === '"') { inQuotes = !inQuotes; }
119
+ else if (ch === ',' && !inQuotes) { result.push(current.trim()); current = ''; }
120
+ else { current += ch; }
121
+ }
122
+ result.push(current.trim());
123
+ return result;
124
+ }
125
+
126
+ private parseJSON(text: string): string {
127
+ try {
128
+ const data = JSON.parse(text);
129
+ if (Array.isArray(data)) {
130
+ return data.map((item, i) => `Item ${i + 1}:\n${JSON.stringify(item, null, 2)}`).join('\n\n');
131
+ }
132
+ return JSON.stringify(data, null, 2);
133
+ } catch {
134
+ return text;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Smart chunking: split by headings/paragraphs, respecting size limits
140
+ */
141
+ private chunkText(text: string, filename: string, format: string): DocumentChunk[] {
142
+ if (!text.trim()) return [];
143
+
144
+ // Split by markdown headings or double newlines
145
+ const sections = text.split(/\n(?=#{1,3}\s)|(?:\n\s*\n)/).filter(s => s.trim());
146
+ const chunks: DocumentChunk[] = [];
147
+ let currentChunk = '';
148
+ let currentTitle = filename;
149
+
150
+ for (const section of sections) {
151
+ const headingMatch = section.match(/^(#{1,3})\s+(.+)/);
152
+ if (headingMatch) {
153
+ currentTitle = headingMatch[2].trim();
154
+ }
155
+
156
+ if (currentChunk.length + section.length > CHUNK_MAX_CHARS && currentChunk.length > 0) {
157
+ chunks.push({
158
+ title: currentTitle,
159
+ content: currentChunk.trim(),
160
+ metadata: { source: filename, format, chunkIndex: chunks.length },
161
+ });
162
+ currentChunk = '';
163
+ }
164
+
165
+ currentChunk += section + '\n\n';
166
+
167
+ if (currentChunk.length >= CHUNK_TARGET_CHARS) {
168
+ chunks.push({
169
+ title: currentTitle,
170
+ content: currentChunk.trim(),
171
+ metadata: { source: filename, format, chunkIndex: chunks.length },
172
+ });
173
+ currentChunk = '';
174
+ }
175
+ }
176
+
177
+ if (currentChunk.trim()) {
178
+ chunks.push({
179
+ title: currentTitle,
180
+ content: currentChunk.trim(),
181
+ metadata: { source: filename, format, chunkIndex: chunks.length },
182
+ });
183
+ }
184
+
185
+ // If we got no chunks from section splitting (e.g. dense text), force-split
186
+ if (chunks.length === 0 && text.trim()) {
187
+ const words = text.split(/\s+/);
188
+ let buf = '';
189
+ for (const w of words) {
190
+ if (buf.length + w.length + 1 > CHUNK_MAX_CHARS && buf) {
191
+ chunks.push({
192
+ title: filename,
193
+ content: buf.trim(),
194
+ metadata: { source: filename, format, chunkIndex: chunks.length },
195
+ });
196
+ buf = '';
197
+ }
198
+ buf += w + ' ';
199
+ }
200
+ if (buf.trim()) {
201
+ chunks.push({
202
+ title: filename,
203
+ content: buf.trim(),
204
+ metadata: { source: filename, format, chunkIndex: chunks.length },
205
+ });
206
+ }
207
+ }
208
+
209
+ // Set totalChunks
210
+ for (const c of chunks) c.metadata.totalChunks = chunks.length;
211
+ return chunks;
212
+ }
213
+ }