opc-agent 1.4.0 → 2.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 (198) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -32
  3. package/dist/channels/email.d.ts +32 -26
  4. package/dist/channels/email.js +239 -62
  5. package/dist/channels/feishu.d.ts +21 -6
  6. package/dist/channels/feishu.js +225 -126
  7. package/dist/channels/telegram.d.ts +30 -9
  8. package/dist/channels/telegram.js +125 -33
  9. package/dist/channels/websocket.d.ts +46 -3
  10. package/dist/channels/websocket.js +306 -37
  11. package/dist/channels/wechat.d.ts +33 -13
  12. package/dist/channels/wechat.js +229 -42
  13. package/dist/cli.js +1127 -19
  14. package/dist/core/a2a.d.ts +17 -0
  15. package/dist/core/a2a.js +43 -1
  16. package/dist/core/agent.d.ts +39 -0
  17. package/dist/core/agent.js +228 -3
  18. package/dist/core/runtime.d.ts +7 -0
  19. package/dist/core/runtime.js +205 -2
  20. package/dist/core/sandbox.d.ts +26 -0
  21. package/dist/core/sandbox.js +117 -0
  22. package/dist/core/scheduler.d.ts +52 -0
  23. package/dist/core/scheduler.js +168 -0
  24. package/dist/core/subagent.d.ts +28 -0
  25. package/dist/core/subagent.js +65 -0
  26. package/dist/core/workflow-graph.d.ts +93 -0
  27. package/dist/core/workflow-graph.js +247 -0
  28. package/dist/daemon.d.ts +3 -0
  29. package/dist/daemon.js +134 -0
  30. package/dist/doctor.d.ts +15 -0
  31. package/dist/doctor.js +183 -0
  32. package/dist/eval/index.d.ts +65 -0
  33. package/dist/eval/index.js +191 -0
  34. package/dist/index.d.ts +37 -6
  35. package/dist/index.js +75 -3
  36. package/dist/plugins/content-filter.d.ts +7 -0
  37. package/dist/plugins/content-filter.js +25 -0
  38. package/dist/plugins/index.d.ts +42 -0
  39. package/dist/plugins/index.js +108 -2
  40. package/dist/plugins/logger.d.ts +6 -0
  41. package/dist/plugins/logger.js +20 -0
  42. package/dist/plugins/rate-limiter.d.ts +7 -0
  43. package/dist/plugins/rate-limiter.js +35 -0
  44. package/dist/protocols/a2a/client.d.ts +25 -0
  45. package/dist/protocols/a2a/client.js +115 -0
  46. package/dist/protocols/a2a/index.d.ts +6 -0
  47. package/dist/protocols/a2a/index.js +12 -0
  48. package/dist/protocols/a2a/server.d.ts +41 -0
  49. package/dist/protocols/a2a/server.js +295 -0
  50. package/dist/protocols/a2a/types.d.ts +91 -0
  51. package/dist/protocols/a2a/types.js +15 -0
  52. package/dist/protocols/a2a/utils.d.ts +6 -0
  53. package/dist/protocols/a2a/utils.js +47 -0
  54. package/dist/protocols/agui/client.d.ts +10 -0
  55. package/dist/protocols/agui/client.js +75 -0
  56. package/dist/protocols/agui/index.d.ts +4 -0
  57. package/dist/protocols/agui/index.js +25 -0
  58. package/dist/protocols/agui/server.d.ts +37 -0
  59. package/dist/protocols/agui/server.js +191 -0
  60. package/dist/protocols/agui/types.d.ts +107 -0
  61. package/dist/protocols/agui/types.js +17 -0
  62. package/dist/protocols/index.d.ts +2 -0
  63. package/dist/protocols/index.js +19 -0
  64. package/dist/protocols/mcp/agent-tools.d.ts +11 -0
  65. package/dist/protocols/mcp/agent-tools.js +129 -0
  66. package/dist/protocols/mcp/index.d.ts +5 -0
  67. package/dist/protocols/mcp/index.js +11 -0
  68. package/dist/protocols/mcp/server.d.ts +31 -0
  69. package/dist/protocols/mcp/server.js +248 -0
  70. package/dist/protocols/mcp/types.d.ts +92 -0
  71. package/dist/protocols/mcp/types.js +17 -0
  72. package/dist/providers/index.d.ts +5 -1
  73. package/dist/providers/index.js +16 -9
  74. package/dist/publish/index.d.ts +45 -0
  75. package/dist/publish/index.js +350 -0
  76. package/dist/schema/oad.d.ts +859 -67
  77. package/dist/schema/oad.js +47 -3
  78. package/dist/security/approval.d.ts +36 -0
  79. package/dist/security/approval.js +113 -0
  80. package/dist/security/index.d.ts +4 -0
  81. package/dist/security/index.js +8 -0
  82. package/dist/security/keys.d.ts +16 -0
  83. package/dist/security/keys.js +117 -0
  84. package/dist/skills/auto-learn.d.ts +28 -0
  85. package/dist/skills/auto-learn.js +257 -0
  86. package/dist/studio/server.d.ts +63 -0
  87. package/dist/studio/server.js +625 -0
  88. package/dist/studio-ui/index.html +662 -0
  89. package/dist/telemetry/index.d.ts +93 -0
  90. package/dist/telemetry/index.js +285 -0
  91. package/dist/tools/builtin/datetime.d.ts +3 -0
  92. package/dist/tools/builtin/datetime.js +44 -0
  93. package/dist/tools/builtin/file.d.ts +3 -0
  94. package/dist/tools/builtin/file.js +151 -0
  95. package/dist/tools/builtin/index.d.ts +15 -0
  96. package/dist/tools/builtin/index.js +30 -0
  97. package/dist/tools/builtin/shell.d.ts +3 -0
  98. package/dist/tools/builtin/shell.js +43 -0
  99. package/dist/tools/builtin/web.d.ts +3 -0
  100. package/dist/tools/builtin/web.js +37 -0
  101. package/dist/tools/mcp-client.d.ts +24 -0
  102. package/dist/tools/mcp-client.js +119 -0
  103. package/package.json +5 -3
  104. package/scripts/install.ps1 +31 -0
  105. package/scripts/install.sh +40 -0
  106. package/src/channels/email.ts +351 -177
  107. package/src/channels/feishu.ts +349 -236
  108. package/src/channels/telegram.ts +212 -90
  109. package/src/channels/websocket.ts +399 -87
  110. package/src/channels/wechat.ts +329 -149
  111. package/src/cli.ts +1201 -20
  112. package/src/core/a2a.ts +60 -0
  113. package/src/core/agent.ts +420 -152
  114. package/src/core/runtime.ts +174 -0
  115. package/src/core/sandbox.ts +143 -0
  116. package/src/core/scheduler.ts +187 -0
  117. package/src/core/subagent.ts +98 -0
  118. package/src/core/workflow-graph.ts +365 -0
  119. package/src/daemon.ts +96 -0
  120. package/src/doctor.ts +156 -0
  121. package/src/eval/index.ts +211 -0
  122. package/src/eval/suites/basic.json +16 -0
  123. package/src/eval/suites/memory.json +12 -0
  124. package/src/eval/suites/safety.json +14 -0
  125. package/src/index.ts +65 -6
  126. package/src/plugins/content-filter.ts +23 -0
  127. package/src/plugins/index.ts +133 -2
  128. package/src/plugins/logger.ts +18 -0
  129. package/src/plugins/rate-limiter.ts +38 -0
  130. package/src/protocols/a2a/client.ts +132 -0
  131. package/src/protocols/a2a/index.ts +8 -0
  132. package/src/protocols/a2a/server.ts +333 -0
  133. package/src/protocols/a2a/types.ts +88 -0
  134. package/src/protocols/a2a/utils.ts +50 -0
  135. package/src/protocols/agui/client.ts +83 -0
  136. package/src/protocols/agui/index.ts +4 -0
  137. package/src/protocols/agui/server.ts +218 -0
  138. package/src/protocols/agui/types.ts +153 -0
  139. package/src/protocols/index.ts +2 -0
  140. package/src/protocols/mcp/agent-tools.ts +134 -0
  141. package/src/protocols/mcp/index.ts +8 -0
  142. package/src/protocols/mcp/server.ts +262 -0
  143. package/src/protocols/mcp/types.ts +69 -0
  144. package/src/providers/index.ts +354 -339
  145. package/src/publish/index.ts +376 -0
  146. package/src/schema/oad.ts +204 -154
  147. package/src/security/approval.ts +131 -0
  148. package/src/security/index.ts +3 -0
  149. package/src/security/keys.ts +87 -0
  150. package/src/skills/auto-learn.ts +262 -0
  151. package/src/studio/server.ts +629 -0
  152. package/src/studio-ui/index.html +662 -0
  153. package/src/telemetry/index.ts +324 -0
  154. package/src/tools/builtin/datetime.ts +41 -0
  155. package/src/tools/builtin/file.ts +107 -0
  156. package/src/tools/builtin/index.ts +28 -0
  157. package/src/tools/builtin/shell.ts +43 -0
  158. package/src/tools/builtin/web.ts +35 -0
  159. package/src/tools/mcp-client.ts +131 -0
  160. package/src/types/agent-workstation.d.ts +2 -0
  161. package/tests/a2a-protocol.test.ts +285 -0
  162. package/tests/agui-protocol.test.ts +246 -0
  163. package/tests/auto-learn.test.ts +105 -0
  164. package/tests/builtin-tools.test.ts +83 -0
  165. package/tests/channels/discord.test.ts +79 -0
  166. package/tests/channels/email.test.ts +148 -0
  167. package/tests/channels/feishu.test.ts +123 -0
  168. package/tests/channels/telegram.test.ts +129 -0
  169. package/tests/channels/websocket.test.ts +53 -0
  170. package/tests/channels/wechat.test.ts +170 -0
  171. package/tests/chat-cli.test.ts +160 -0
  172. package/tests/cli.test.ts +46 -0
  173. package/tests/daemon.test.ts +135 -0
  174. package/tests/deepbrain-wire.test.ts +234 -0
  175. package/tests/doctor.test.ts +38 -0
  176. package/tests/eval.test.ts +173 -0
  177. package/tests/init-role.test.ts +124 -0
  178. package/tests/mcp-client.test.ts +92 -0
  179. package/tests/mcp-server.test.ts +178 -0
  180. package/tests/plugin-a2a-enhanced.test.ts +230 -0
  181. package/tests/publish.test.ts +231 -0
  182. package/tests/scheduler.test.ts +200 -0
  183. package/tests/security-enhanced.test.ts +233 -0
  184. package/tests/skill-learner.test.ts +161 -0
  185. package/tests/studio.test.ts +229 -0
  186. package/tests/subagent.test.ts +193 -0
  187. package/tests/telegram-discord.test.ts +60 -0
  188. package/tests/telemetry.test.ts +186 -0
  189. package/tests/tools/builtin-extended.test.ts +138 -0
  190. package/tests/workflow-graph.test.ts +279 -0
  191. package/tutorial/customer-service-agent/README.md +612 -0
  192. package/tutorial/customer-service-agent/SOUL.md +26 -0
  193. package/tutorial/customer-service-agent/agent.yaml +63 -0
  194. package/tutorial/customer-service-agent/package.json +19 -0
  195. package/tutorial/customer-service-agent/src/index.ts +69 -0
  196. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
  197. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
  198. package/tutorial/customer-service-agent/tsconfig.json +14 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * OPC Agent Telemetry — Lightweight OTel-compatible tracing & metrics.
3
+ * Zero external dependencies. Produces OTLP-compatible JSON.
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import * as crypto from 'crypto';
8
+
9
+ // ─── Types ───────────────────────────────────────────────────
10
+
11
+ export interface Span {
12
+ traceId: string;
13
+ spanId: string;
14
+ parentSpanId?: string;
15
+ name: string;
16
+ kind: 'internal' | 'client' | 'server';
17
+ startTime: number; // epoch ms
18
+ endTime?: number;
19
+ status: 'ok' | 'error' | 'unset';
20
+ attributes: Record<string, string | number | boolean>;
21
+ events: SpanEvent[];
22
+ }
23
+
24
+ export interface SpanEvent {
25
+ name: string;
26
+ timestamp: number;
27
+ attributes?: Record<string, string | number | boolean>;
28
+ }
29
+
30
+ export interface Metric {
31
+ name: string;
32
+ type: 'counter' | 'gauge' | 'histogram';
33
+ value: number;
34
+ timestamp: number;
35
+ labels: Record<string, string>;
36
+ }
37
+
38
+ export interface TraceExporter {
39
+ export(spans: Span[]): Promise<void>;
40
+ }
41
+
42
+ // ─── ID Generation ───────────────────────────────────────────
43
+
44
+ export function generateTraceId(): string {
45
+ return crypto.randomBytes(16).toString('hex'); // 32 hex chars
46
+ }
47
+
48
+ export function generateSpanId(): string {
49
+ return crypto.randomBytes(8).toString('hex'); // 16 hex chars
50
+ }
51
+
52
+ // ─── Tracer ──────────────────────────────────────────────────
53
+
54
+ export class Tracer {
55
+ private spans: Span[] = [];
56
+ private metrics: Metric[] = [];
57
+ private maxSpans: number;
58
+ private maxMetrics: number;
59
+ private exporters: TraceExporter[] = [];
60
+
61
+ constructor(options?: { maxSpans?: number; maxMetrics?: number }) {
62
+ this.maxSpans = options?.maxSpans || 10000;
63
+ this.maxMetrics = options?.maxMetrics || 50000;
64
+ }
65
+
66
+ startSpan(name: string, options?: {
67
+ parent?: Span;
68
+ attributes?: Record<string, string | number | boolean>;
69
+ kind?: Span['kind'];
70
+ }): Span {
71
+ const span: Span = {
72
+ traceId: options?.parent?.traceId || generateTraceId(),
73
+ spanId: generateSpanId(),
74
+ parentSpanId: options?.parent?.spanId,
75
+ name,
76
+ kind: options?.kind || 'internal',
77
+ startTime: Date.now(),
78
+ status: 'unset',
79
+ attributes: options?.attributes ? { ...options.attributes } : {},
80
+ events: [],
81
+ };
82
+ this.spans.push(span);
83
+
84
+ // Evict oldest spans if over limit
85
+ if (this.spans.length > this.maxSpans) {
86
+ const excess = this.spans.length - this.maxSpans;
87
+ this.spans.splice(0, excess);
88
+ }
89
+
90
+ return span;
91
+ }
92
+
93
+ endSpan(span: Span, status?: Span['status']): void {
94
+ span.endTime = Date.now();
95
+ span.status = status || 'ok';
96
+
97
+ // Notify exporters
98
+ for (const exporter of this.exporters) {
99
+ exporter.export([span]).catch(() => {});
100
+ }
101
+ }
102
+
103
+ addEvent(span: Span, name: string, attributes?: Record<string, string | number | boolean>): void {
104
+ span.events.push({ name, timestamp: Date.now(), attributes });
105
+ }
106
+
107
+ // ─── Metrics ─────────────────────────────────────────────
108
+
109
+ increment(name: string, value: number = 1, labels: Record<string, string> = {}): void {
110
+ this.addMetric(name, 'counter', value, labels);
111
+ }
112
+
113
+ gauge(name: string, value: number, labels: Record<string, string> = {}): void {
114
+ this.addMetric(name, 'gauge', value, labels);
115
+ }
116
+
117
+ histogram(name: string, value: number, labels: Record<string, string> = {}): void {
118
+ this.addMetric(name, 'histogram', value, labels);
119
+ }
120
+
121
+ private addMetric(name: string, type: Metric['type'], value: number, labels: Record<string, string>): void {
122
+ this.metrics.push({ name, type, value, timestamp: Date.now(), labels });
123
+ if (this.metrics.length > this.maxMetrics) {
124
+ this.metrics.splice(0, this.metrics.length - this.maxMetrics);
125
+ }
126
+ }
127
+
128
+ // ─── Query ───────────────────────────────────────────────
129
+
130
+ getSpans(options?: { limit?: number; traceId?: string; name?: string; since?: number }): Span[] {
131
+ let result = [...this.spans];
132
+
133
+ if (options?.traceId) result = result.filter(s => s.traceId === options.traceId);
134
+ if (options?.name) result = result.filter(s => s.name === options.name);
135
+ if (options?.since) result = result.filter(s => s.startTime >= options.since!);
136
+
137
+ // Most recent first
138
+ result.sort((a, b) => b.startTime - a.startTime);
139
+
140
+ if (options?.limit) result = result.slice(0, options.limit);
141
+ return result;
142
+ }
143
+
144
+ getMetrics(options?: { name?: string; since?: number }): Metric[] {
145
+ let result = [...this.metrics];
146
+ if (options?.name) result = result.filter(m => m.name === options.name);
147
+ if (options?.since) result = result.filter(m => m.timestamp >= options.since!);
148
+ return result;
149
+ }
150
+
151
+ getTrace(traceId: string): Span[] {
152
+ return this.spans.filter(s => s.traceId === traceId).sort((a, b) => a.startTime - b.startTime);
153
+ }
154
+
155
+ // ─── Export (OTLP-compatible) ────────────────────────────
156
+
157
+ addExporter(exporter: TraceExporter): void {
158
+ this.exporters.push(exporter);
159
+ }
160
+
161
+ exportOTLP(): object {
162
+ // OTLP JSON format: https://opentelemetry.io/docs/specs/otlp/
163
+ const spansByResource = this.spans.filter(s => s.endTime != null);
164
+
165
+ return {
166
+ resourceSpans: [{
167
+ resource: {
168
+ attributes: [
169
+ { key: 'service.name', value: { stringValue: 'opc-agent' } },
170
+ ],
171
+ },
172
+ scopeSpans: [{
173
+ scope: { name: 'opc-telemetry', version: '1.0.0' },
174
+ spans: spansByResource.map(s => ({
175
+ traceId: s.traceId,
176
+ spanId: s.spanId,
177
+ parentSpanId: s.parentSpanId || '',
178
+ name: s.name,
179
+ kind: s.kind === 'server' ? 2 : s.kind === 'client' ? 3 : 1,
180
+ startTimeUnixNano: String(s.startTime * 1_000_000),
181
+ endTimeUnixNano: String((s.endTime || s.startTime) * 1_000_000),
182
+ attributes: Object.entries(s.attributes).map(([key, value]) => ({
183
+ key,
184
+ value: typeof value === 'string' ? { stringValue: value }
185
+ : typeof value === 'number' ? (Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value })
186
+ : { boolValue: value },
187
+ })),
188
+ events: s.events.map(e => ({
189
+ timeUnixNano: String(e.timestamp * 1_000_000),
190
+ name: e.name,
191
+ attributes: e.attributes ? Object.entries(e.attributes).map(([key, value]) => ({
192
+ key,
193
+ value: typeof value === 'string' ? { stringValue: value }
194
+ : typeof value === 'number' ? { intValue: String(value) }
195
+ : { boolValue: value },
196
+ })) : [],
197
+ })),
198
+ status: {
199
+ code: s.status === 'ok' ? 1 : s.status === 'error' ? 2 : 0,
200
+ },
201
+ })),
202
+ }],
203
+ }],
204
+ };
205
+ }
206
+
207
+ // ─── Stats ───────────────────────────────────────────────
208
+
209
+ getStats(): {
210
+ totalSpans: number;
211
+ totalTraces: number;
212
+ avgDuration: number;
213
+ errorRate: number;
214
+ spansByName: Record<string, number>;
215
+ p50Latency: number;
216
+ p95Latency: number;
217
+ p99Latency: number;
218
+ } {
219
+ const completed = this.spans.filter(s => s.endTime != null);
220
+ const durations = completed.map(s => s.endTime! - s.startTime).sort((a, b) => a - b);
221
+ const traceIds = new Set(this.spans.map(s => s.traceId));
222
+ const errors = completed.filter(s => s.status === 'error').length;
223
+
224
+ const spansByName: Record<string, number> = {};
225
+ for (const s of this.spans) {
226
+ spansByName[s.name] = (spansByName[s.name] || 0) + 1;
227
+ }
228
+
229
+ const percentile = (arr: number[], p: number): number => {
230
+ if (arr.length === 0) return 0;
231
+ const idx = Math.ceil(arr.length * p / 100) - 1;
232
+ return arr[Math.max(0, idx)];
233
+ };
234
+
235
+ return {
236
+ totalSpans: this.spans.length,
237
+ totalTraces: traceIds.size,
238
+ avgDuration: durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0,
239
+ errorRate: completed.length > 0 ? errors / completed.length : 0,
240
+ spansByName,
241
+ p50Latency: percentile(durations, 50),
242
+ p95Latency: percentile(durations, 95),
243
+ p99Latency: percentile(durations, 99),
244
+ };
245
+ }
246
+
247
+ clear(): void {
248
+ this.spans = [];
249
+ this.metrics = [];
250
+ }
251
+ }
252
+
253
+ // ─── Exporters ───────────────────────────────────────────────
254
+
255
+ export class ConsoleExporter implements TraceExporter {
256
+ async export(spans: Span[]): Promise<void> {
257
+ for (const span of spans) {
258
+ const duration = span.endTime ? `${span.endTime - span.startTime}ms` : 'ongoing';
259
+ console.log(`[TRACE] ${span.name} (${duration}) [${span.status}] trace=${span.traceId.slice(0, 8)}`);
260
+ }
261
+ }
262
+ }
263
+
264
+ export class FileExporter implements TraceExporter {
265
+ private filePath: string;
266
+
267
+ constructor(filePath: string) {
268
+ this.filePath = filePath;
269
+ }
270
+
271
+ async export(spans: Span[]): Promise<void> {
272
+ const lines = spans.map(s => JSON.stringify(s)).join('\n') + '\n';
273
+ fs.appendFileSync(this.filePath, lines);
274
+ }
275
+ }
276
+
277
+ export class OTLPHttpExporter implements TraceExporter {
278
+ private endpoint: string;
279
+
280
+ constructor(endpoint: string) {
281
+ this.endpoint = endpoint.replace(/\/$/, '');
282
+ }
283
+
284
+ async export(spans: Span[]): Promise<void> {
285
+ const body = {
286
+ resourceSpans: [{
287
+ resource: {
288
+ attributes: [
289
+ { key: 'service.name', value: { stringValue: 'opc-agent' } },
290
+ ],
291
+ },
292
+ scopeSpans: [{
293
+ scope: { name: 'opc-telemetry', version: '1.0.0' },
294
+ spans: spans.filter(s => s.endTime).map(s => ({
295
+ traceId: s.traceId,
296
+ spanId: s.spanId,
297
+ parentSpanId: s.parentSpanId || '',
298
+ name: s.name,
299
+ kind: s.kind === 'server' ? 2 : s.kind === 'client' ? 3 : 1,
300
+ startTimeUnixNano: String(s.startTime * 1_000_000),
301
+ endTimeUnixNano: String((s.endTime || s.startTime) * 1_000_000),
302
+ attributes: Object.entries(s.attributes).map(([key, value]) => ({
303
+ key,
304
+ value: typeof value === 'string' ? { stringValue: value }
305
+ : typeof value === 'number' ? { intValue: String(value) }
306
+ : { boolValue: value },
307
+ })),
308
+ status: { code: s.status === 'ok' ? 1 : s.status === 'error' ? 2 : 0 },
309
+ })),
310
+ }],
311
+ }],
312
+ };
313
+
314
+ try {
315
+ await fetch(`${this.endpoint}/v1/traces`, {
316
+ method: 'POST',
317
+ headers: { 'Content-Type': 'application/json' },
318
+ body: JSON.stringify(body),
319
+ });
320
+ } catch {
321
+ // Best effort
322
+ }
323
+ }
324
+ }
@@ -0,0 +1,41 @@
1
+ import type { MCPTool, MCPToolResult } from '../mcp';
2
+
3
+ export const datetimeTool: MCPTool = {
4
+ name: 'datetime',
5
+ description: 'Get current date, time, timezone info',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ format: { type: 'string', default: 'iso' },
10
+ timezone: { type: 'string' },
11
+ },
12
+ },
13
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
14
+ const now = new Date();
15
+ const timezone = input.timezone as string | undefined;
16
+ const format = (input.format as string) || 'iso';
17
+
18
+ let content: string;
19
+ if (format === 'iso') {
20
+ content = now.toISOString();
21
+ } else if (format === 'locale') {
22
+ content = timezone
23
+ ? now.toLocaleString('en-US', { timeZone: timezone })
24
+ : now.toLocaleString();
25
+ } else if (format === 'unix') {
26
+ content = String(Math.floor(now.getTime() / 1000));
27
+ } else {
28
+ content = now.toISOString();
29
+ }
30
+
31
+ return {
32
+ content: JSON.stringify({
33
+ iso: now.toISOString(),
34
+ unix: Math.floor(now.getTime() / 1000),
35
+ formatted: content,
36
+ timezone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
37
+ }),
38
+ isError: false,
39
+ };
40
+ },
41
+ };
@@ -0,0 +1,107 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { MCPTool, MCPToolResult } from '../mcp';
4
+ import type { AgentContext } from '../../core/types';
5
+
6
+ function resolveSafe(basePath: string, targetPath: string): string | null {
7
+ const resolved = path.resolve(basePath, targetPath);
8
+ if (!resolved.startsWith(path.resolve(basePath))) return null;
9
+ return resolved;
10
+ }
11
+
12
+ function searchFiles(dir: string, query: string, results: string[] = [], maxResults = 20): string[] {
13
+ if (results.length >= maxResults) return results;
14
+ try {
15
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ if (results.length >= maxResults) break;
18
+ const full = path.join(dir, entry.name);
19
+ if (entry.isDirectory()) {
20
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
21
+ searchFiles(full, query, results, maxResults);
22
+ } else if (entry.isFile()) {
23
+ try {
24
+ const content = fs.readFileSync(full, 'utf-8');
25
+ const lines = content.split('\n');
26
+ for (let i = 0; i < lines.length; i++) {
27
+ if (lines[i].includes(query)) {
28
+ results.push(`${full}:${i + 1}: ${lines[i].trim()}`);
29
+ if (results.length >= maxResults) break;
30
+ }
31
+ }
32
+ } catch { /* skip binary/unreadable */ }
33
+ }
34
+ }
35
+ } catch { /* skip inaccessible dirs */ }
36
+ return results;
37
+ }
38
+
39
+ export const fileTool: MCPTool = {
40
+ name: 'file_operations',
41
+ description: 'Read, write, list, and search files in the workspace',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ action: { type: 'string', enum: ['read', 'write', 'list', 'search', 'exists'] },
46
+ path: { type: 'string' },
47
+ content: { type: 'string' },
48
+ query: { type: 'string' },
49
+ },
50
+ required: ['action'],
51
+ },
52
+ async execute(input: Record<string, unknown>, context?: AgentContext): Promise<MCPToolResult> {
53
+ const action = input.action as string;
54
+ const workspace = process.cwd();
55
+ const targetPath = input.path as string | undefined;
56
+
57
+ if (action === 'search') {
58
+ const query = input.query as string;
59
+ if (!query) return { content: 'query is required for search', isError: true };
60
+ const results = searchFiles(workspace, query);
61
+ return { content: results.length ? results.join('\n') : 'No matches found', isError: false };
62
+ }
63
+
64
+ if (action === 'list') {
65
+ const dir = targetPath ? resolveSafe(workspace, targetPath) : workspace;
66
+ if (!dir) return { content: 'Path outside workspace', isError: true };
67
+ try {
68
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
69
+ const listing = entries.map(e => `${e.isDirectory() ? '[DIR] ' : ''}${e.name}`).join('\n');
70
+ return { content: listing || '(empty directory)', isError: false };
71
+ } catch (err) {
72
+ return { content: `Error listing directory: ${err instanceof Error ? err.message : String(err)}`, isError: true };
73
+ }
74
+ }
75
+
76
+ if (!targetPath) return { content: 'path is required', isError: true };
77
+ const resolved = resolveSafe(workspace, targetPath);
78
+ if (!resolved) return { content: 'Path outside workspace', isError: true };
79
+
80
+ switch (action) {
81
+ case 'read': {
82
+ try {
83
+ const content = fs.readFileSync(resolved, 'utf-8');
84
+ return { content: content.slice(0, 50000), isError: false };
85
+ } catch (err) {
86
+ return { content: `Error reading file: ${err instanceof Error ? err.message : String(err)}`, isError: true };
87
+ }
88
+ }
89
+ case 'write': {
90
+ const content = input.content as string;
91
+ if (content === undefined) return { content: 'content is required for write', isError: true };
92
+ try {
93
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
94
+ fs.writeFileSync(resolved, content, 'utf-8');
95
+ return { content: `Written ${content.length} bytes to ${targetPath}`, isError: false };
96
+ } catch (err) {
97
+ return { content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`, isError: true };
98
+ }
99
+ }
100
+ case 'exists': {
101
+ return { content: String(fs.existsSync(resolved)), isError: false };
102
+ }
103
+ default:
104
+ return { content: `Unknown action: ${action}`, isError: true };
105
+ }
106
+ },
107
+ };
@@ -0,0 +1,28 @@
1
+ import type { MCPTool } from '../mcp';
2
+ import { fileTool } from './file';
3
+ import { webTool } from './web';
4
+ import { shellTool } from './shell';
5
+ import { datetimeTool } from './datetime';
6
+
7
+ export { fileTool, webTool, shellTool, datetimeTool };
8
+
9
+ const ALL_BUILTIN_TOOLS: MCPTool[] = [fileTool, webTool, shellTool, datetimeTool];
10
+
11
+ const BUILTIN_MAP = new Map<string, MCPTool>(
12
+ ALL_BUILTIN_TOOLS.map(t => [t.name, t])
13
+ );
14
+
15
+ /**
16
+ * Get all built-in tools.
17
+ */
18
+ export function getBuiltinTools(): MCPTool[] {
19
+ return [...ALL_BUILTIN_TOOLS];
20
+ }
21
+
22
+ /**
23
+ * Get specific built-in tools by name. If no names given, returns all.
24
+ */
25
+ export function getBuiltinToolsByName(names?: string[]): MCPTool[] {
26
+ if (!names || names.length === 0) return getBuiltinTools();
27
+ return names.map(n => BUILTIN_MAP.get(n)).filter((t): t is MCPTool => !!t);
28
+ }
@@ -0,0 +1,43 @@
1
+ import { execSync } from 'child_process';
2
+ import * as path from 'path';
3
+ import type { MCPTool, MCPToolResult } from '../mcp';
4
+
5
+ export const shellTool: MCPTool = {
6
+ name: 'shell_exec',
7
+ description: 'Execute a shell command (sandboxed to workspace)',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ command: { type: 'string' },
12
+ timeout: { type: 'number', default: 30000 },
13
+ },
14
+ required: ['command'],
15
+ },
16
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
17
+ const command = input.command as string;
18
+ const timeout = (input.timeout as number) || 30000;
19
+ const workspace = process.cwd();
20
+
21
+ // Block path traversal attempts
22
+ if (command.includes('..')) {
23
+ return { content: 'Commands with ".." are not allowed for security', isError: true };
24
+ }
25
+
26
+ try {
27
+ const output = execSync(command, {
28
+ cwd: workspace,
29
+ timeout,
30
+ encoding: 'utf-8',
31
+ maxBuffer: 1024 * 1024,
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ });
34
+ const result = (output || '').slice(0, 5000);
35
+ return { content: result || '(no output)', isError: false };
36
+ } catch (err: any) {
37
+ const stderr = err.stderr ? String(err.stderr).slice(0, 2500) : '';
38
+ const stdout = err.stdout ? String(err.stdout).slice(0, 2500) : '';
39
+ const output = [stdout, stderr].filter(Boolean).join('\n') || err.message;
40
+ return { content: `Command failed: ${output.slice(0, 5000)}`, isError: true };
41
+ }
42
+ },
43
+ };
@@ -0,0 +1,35 @@
1
+ import type { MCPTool, MCPToolResult } from '../mcp';
2
+
3
+ export const webTool: MCPTool = {
4
+ name: 'web_fetch',
5
+ description: 'Fetch content from a URL',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ url: { type: 'string' },
10
+ method: { type: 'string', enum: ['GET', 'POST'], default: 'GET' },
11
+ maxLength: { type: 'number', default: 5000 },
12
+ },
13
+ required: ['url'],
14
+ },
15
+ async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
16
+ const url = input.url as string;
17
+ const method = (input.method as string) || 'GET';
18
+ const maxLength = (input.maxLength as number) || 5000;
19
+
20
+ try {
21
+ const response = await fetch(url, { method, signal: AbortSignal.timeout(15000) });
22
+ const text = await response.text();
23
+ const truncated = text.length > maxLength ? text.slice(0, maxLength) + '\n...[truncated]' : text;
24
+ return {
25
+ content: `Status: ${response.status}\n\n${truncated}`,
26
+ isError: false,
27
+ };
28
+ } catch (err) {
29
+ return {
30
+ content: `Fetch error: ${err instanceof Error ? err.message : String(err)}`,
31
+ isError: true,
32
+ };
33
+ }
34
+ },
35
+ };