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.
- package/CHANGELOG.md +25 -0
- package/README.md +91 -32
- package/dist/channels/email.d.ts +32 -26
- package/dist/channels/email.js +239 -62
- package/dist/channels/feishu.d.ts +21 -6
- package/dist/channels/feishu.js +225 -126
- package/dist/channels/telegram.d.ts +30 -9
- package/dist/channels/telegram.js +125 -33
- package/dist/channels/websocket.d.ts +46 -3
- package/dist/channels/websocket.js +306 -37
- package/dist/channels/wechat.d.ts +33 -13
- package/dist/channels/wechat.js +229 -42
- package/dist/cli.js +1127 -19
- package/dist/core/a2a.d.ts +17 -0
- package/dist/core/a2a.js +43 -1
- package/dist/core/agent.d.ts +39 -0
- package/dist/core/agent.js +228 -3
- package/dist/core/runtime.d.ts +7 -0
- package/dist/core/runtime.js +205 -2
- package/dist/core/sandbox.d.ts +26 -0
- package/dist/core/sandbox.js +117 -0
- package/dist/core/scheduler.d.ts +52 -0
- package/dist/core/scheduler.js +168 -0
- package/dist/core/subagent.d.ts +28 -0
- package/dist/core/subagent.js +65 -0
- package/dist/core/workflow-graph.d.ts +93 -0
- package/dist/core/workflow-graph.js +247 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +134 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +183 -0
- package/dist/eval/index.d.ts +65 -0
- package/dist/eval/index.js +191 -0
- package/dist/index.d.ts +37 -6
- package/dist/index.js +75 -3
- package/dist/plugins/content-filter.d.ts +7 -0
- package/dist/plugins/content-filter.js +25 -0
- package/dist/plugins/index.d.ts +42 -0
- package/dist/plugins/index.js +108 -2
- package/dist/plugins/logger.d.ts +6 -0
- package/dist/plugins/logger.js +20 -0
- package/dist/plugins/rate-limiter.d.ts +7 -0
- package/dist/plugins/rate-limiter.js +35 -0
- package/dist/protocols/a2a/client.d.ts +25 -0
- package/dist/protocols/a2a/client.js +115 -0
- package/dist/protocols/a2a/index.d.ts +6 -0
- package/dist/protocols/a2a/index.js +12 -0
- package/dist/protocols/a2a/server.d.ts +41 -0
- package/dist/protocols/a2a/server.js +295 -0
- package/dist/protocols/a2a/types.d.ts +91 -0
- package/dist/protocols/a2a/types.js +15 -0
- package/dist/protocols/a2a/utils.d.ts +6 -0
- package/dist/protocols/a2a/utils.js +47 -0
- package/dist/protocols/agui/client.d.ts +10 -0
- package/dist/protocols/agui/client.js +75 -0
- package/dist/protocols/agui/index.d.ts +4 -0
- package/dist/protocols/agui/index.js +25 -0
- package/dist/protocols/agui/server.d.ts +37 -0
- package/dist/protocols/agui/server.js +191 -0
- package/dist/protocols/agui/types.d.ts +107 -0
- package/dist/protocols/agui/types.js +17 -0
- package/dist/protocols/index.d.ts +2 -0
- package/dist/protocols/index.js +19 -0
- package/dist/protocols/mcp/agent-tools.d.ts +11 -0
- package/dist/protocols/mcp/agent-tools.js +129 -0
- package/dist/protocols/mcp/index.d.ts +5 -0
- package/dist/protocols/mcp/index.js +11 -0
- package/dist/protocols/mcp/server.d.ts +31 -0
- package/dist/protocols/mcp/server.js +248 -0
- package/dist/protocols/mcp/types.d.ts +92 -0
- package/dist/protocols/mcp/types.js +17 -0
- package/dist/providers/index.d.ts +5 -1
- package/dist/providers/index.js +16 -9
- package/dist/publish/index.d.ts +45 -0
- package/dist/publish/index.js +350 -0
- package/dist/schema/oad.d.ts +859 -67
- package/dist/schema/oad.js +47 -3
- package/dist/security/approval.d.ts +36 -0
- package/dist/security/approval.js +113 -0
- package/dist/security/index.d.ts +4 -0
- package/dist/security/index.js +8 -0
- package/dist/security/keys.d.ts +16 -0
- package/dist/security/keys.js +117 -0
- package/dist/skills/auto-learn.d.ts +28 -0
- package/dist/skills/auto-learn.js +257 -0
- package/dist/studio/server.d.ts +63 -0
- package/dist/studio/server.js +625 -0
- package/dist/studio-ui/index.html +662 -0
- package/dist/telemetry/index.d.ts +93 -0
- package/dist/telemetry/index.js +285 -0
- package/dist/tools/builtin/datetime.d.ts +3 -0
- package/dist/tools/builtin/datetime.js +44 -0
- package/dist/tools/builtin/file.d.ts +3 -0
- package/dist/tools/builtin/file.js +151 -0
- package/dist/tools/builtin/index.d.ts +15 -0
- package/dist/tools/builtin/index.js +30 -0
- package/dist/tools/builtin/shell.d.ts +3 -0
- package/dist/tools/builtin/shell.js +43 -0
- package/dist/tools/builtin/web.d.ts +3 -0
- package/dist/tools/builtin/web.js +37 -0
- package/dist/tools/mcp-client.d.ts +24 -0
- package/dist/tools/mcp-client.js +119 -0
- package/package.json +5 -3
- package/scripts/install.ps1 +31 -0
- package/scripts/install.sh +40 -0
- package/src/channels/email.ts +351 -177
- package/src/channels/feishu.ts +349 -236
- package/src/channels/telegram.ts +212 -90
- package/src/channels/websocket.ts +399 -87
- package/src/channels/wechat.ts +329 -149
- package/src/cli.ts +1201 -20
- package/src/core/a2a.ts +60 -0
- package/src/core/agent.ts +420 -152
- package/src/core/runtime.ts +174 -0
- package/src/core/sandbox.ts +143 -0
- package/src/core/scheduler.ts +187 -0
- package/src/core/subagent.ts +98 -0
- package/src/core/workflow-graph.ts +365 -0
- package/src/daemon.ts +96 -0
- package/src/doctor.ts +156 -0
- package/src/eval/index.ts +211 -0
- package/src/eval/suites/basic.json +16 -0
- package/src/eval/suites/memory.json +12 -0
- package/src/eval/suites/safety.json +14 -0
- package/src/index.ts +65 -6
- package/src/plugins/content-filter.ts +23 -0
- package/src/plugins/index.ts +133 -2
- package/src/plugins/logger.ts +18 -0
- package/src/plugins/rate-limiter.ts +38 -0
- package/src/protocols/a2a/client.ts +132 -0
- package/src/protocols/a2a/index.ts +8 -0
- package/src/protocols/a2a/server.ts +333 -0
- package/src/protocols/a2a/types.ts +88 -0
- package/src/protocols/a2a/utils.ts +50 -0
- package/src/protocols/agui/client.ts +83 -0
- package/src/protocols/agui/index.ts +4 -0
- package/src/protocols/agui/server.ts +218 -0
- package/src/protocols/agui/types.ts +153 -0
- package/src/protocols/index.ts +2 -0
- package/src/protocols/mcp/agent-tools.ts +134 -0
- package/src/protocols/mcp/index.ts +8 -0
- package/src/protocols/mcp/server.ts +262 -0
- package/src/protocols/mcp/types.ts +69 -0
- package/src/providers/index.ts +354 -339
- package/src/publish/index.ts +376 -0
- package/src/schema/oad.ts +204 -154
- package/src/security/approval.ts +131 -0
- package/src/security/index.ts +3 -0
- package/src/security/keys.ts +87 -0
- package/src/skills/auto-learn.ts +262 -0
- package/src/studio/server.ts +629 -0
- package/src/studio-ui/index.html +662 -0
- package/src/telemetry/index.ts +324 -0
- package/src/tools/builtin/datetime.ts +41 -0
- package/src/tools/builtin/file.ts +107 -0
- package/src/tools/builtin/index.ts +28 -0
- package/src/tools/builtin/shell.ts +43 -0
- package/src/tools/builtin/web.ts +35 -0
- package/src/tools/mcp-client.ts +131 -0
- package/src/types/agent-workstation.d.ts +2 -0
- package/tests/a2a-protocol.test.ts +285 -0
- package/tests/agui-protocol.test.ts +246 -0
- package/tests/auto-learn.test.ts +105 -0
- package/tests/builtin-tools.test.ts +83 -0
- package/tests/channels/discord.test.ts +79 -0
- package/tests/channels/email.test.ts +148 -0
- package/tests/channels/feishu.test.ts +123 -0
- package/tests/channels/telegram.test.ts +129 -0
- package/tests/channels/websocket.test.ts +53 -0
- package/tests/channels/wechat.test.ts +170 -0
- package/tests/chat-cli.test.ts +160 -0
- package/tests/cli.test.ts +46 -0
- package/tests/daemon.test.ts +135 -0
- package/tests/deepbrain-wire.test.ts +234 -0
- package/tests/doctor.test.ts +38 -0
- package/tests/eval.test.ts +173 -0
- package/tests/init-role.test.ts +124 -0
- package/tests/mcp-client.test.ts +92 -0
- package/tests/mcp-server.test.ts +178 -0
- package/tests/plugin-a2a-enhanced.test.ts +230 -0
- package/tests/publish.test.ts +231 -0
- package/tests/scheduler.test.ts +200 -0
- package/tests/security-enhanced.test.ts +233 -0
- package/tests/skill-learner.test.ts +161 -0
- package/tests/studio.test.ts +229 -0
- package/tests/subagent.test.ts +193 -0
- package/tests/telegram-discord.test.ts +60 -0
- package/tests/telemetry.test.ts +186 -0
- package/tests/tools/builtin-extended.test.ts +138 -0
- package/tests/workflow-graph.test.ts +279 -0
- package/tutorial/customer-service-agent/README.md +612 -0
- package/tutorial/customer-service-agent/SOUL.md +26 -0
- package/tutorial/customer-service-agent/agent.yaml +63 -0
- package/tutorial/customer-service-agent/package.json +19 -0
- package/tutorial/customer-service-agent/src/index.ts +69 -0
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
- 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
|
+
};
|