opc-agent 0.6.0 → 0.7.0

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.
@@ -0,0 +1,21 @@
1
+ import { BaseSkill } from './base';
2
+ import type { AgentContext, Message, SkillResult } from '../core/types';
3
+ export interface ScheduledTask {
4
+ id: string;
5
+ name: string;
6
+ cronExpr: string;
7
+ action: string;
8
+ enabled: boolean;
9
+ lastRun?: number;
10
+ nextRun?: number;
11
+ }
12
+ export declare class SchedulerSkill extends BaseSkill {
13
+ name: string;
14
+ description: string;
15
+ private tasks;
16
+ private timers;
17
+ execute(context: AgentContext, message: Message): Promise<SkillResult>;
18
+ private parseCronToInterval;
19
+ destroy(): void;
20
+ }
21
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SchedulerSkill = void 0;
4
+ const base_1 = require("./base");
5
+ class SchedulerSkill extends base_1.BaseSkill {
6
+ name = 'scheduler';
7
+ description = 'Schedule recurring tasks. Usage: schedule list | schedule add <name> <cron> <action> | schedule remove <id>';
8
+ tasks = new Map();
9
+ timers = new Map();
10
+ async execute(context, message) {
11
+ const text = message.content.trim();
12
+ if (/^schedule\s+list$/i.test(text)) {
13
+ if (this.tasks.size === 0)
14
+ return this.match('No scheduled tasks.');
15
+ const lines = Array.from(this.tasks.values()).map(t => `• ${t.name} (${t.id}) — ${t.cronExpr} — ${t.enabled ? '✅' : '❌'} — Last: ${t.lastRun ? new Date(t.lastRun).toISOString() : 'never'}`);
16
+ return this.match(`Scheduled tasks:\n${lines.join('\n')}`);
17
+ }
18
+ const addMatch = text.match(/^schedule\s+add\s+(\S+)\s+(".*?"|\S+)\s+(.+)$/i);
19
+ if (addMatch) {
20
+ const [, name, cronExpr, action] = addMatch;
21
+ const id = `task_${Date.now().toString(36)}`;
22
+ const task = {
23
+ id, name, cronExpr: cronExpr.replace(/"/g, ''), action, enabled: true,
24
+ };
25
+ this.tasks.set(id, task);
26
+ // Simple interval-based scheduling (parse cron for interval in minutes)
27
+ const intervalMs = this.parseCronToInterval(task.cronExpr);
28
+ if (intervalMs > 0) {
29
+ const timer = setInterval(() => {
30
+ task.lastRun = Date.now();
31
+ }, intervalMs);
32
+ this.timers.set(id, timer);
33
+ }
34
+ return this.match(`Task scheduled: ${name} (${id}) — ${task.cronExpr} → "${action}"`);
35
+ }
36
+ const rmMatch = text.match(/^schedule\s+remove\s+(\S+)$/i);
37
+ if (rmMatch) {
38
+ const id = rmMatch[1];
39
+ const timer = this.timers.get(id);
40
+ if (timer)
41
+ clearInterval(timer);
42
+ this.timers.delete(id);
43
+ const removed = this.tasks.delete(id);
44
+ return this.match(removed ? `Task ${id} removed.` : `Task ${id} not found.`);
45
+ }
46
+ return this.noMatch();
47
+ }
48
+ parseCronToInterval(expr) {
49
+ // Simple: support "every Xm" or "every Xh" or basic intervals
50
+ const m = expr.match(/every\s+(\d+)\s*(m|min|h|hr|s|sec)/i);
51
+ if (m) {
52
+ const val = parseInt(m[1]);
53
+ const unit = m[2].toLowerCase();
54
+ if (unit.startsWith('h'))
55
+ return val * 3600_000;
56
+ if (unit.startsWith('m'))
57
+ return val * 60_000;
58
+ if (unit.startsWith('s'))
59
+ return val * 1000;
60
+ }
61
+ return 0; // Unknown cron format, no auto-schedule
62
+ }
63
+ destroy() {
64
+ for (const timer of this.timers.values())
65
+ clearInterval(timer);
66
+ this.timers.clear();
67
+ }
68
+ }
69
+ exports.SchedulerSkill = SchedulerSkill;
70
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1,17 @@
1
+ import { BaseSkill } from './base';
2
+ import type { AgentContext, Message, SkillResult } from '../core/types';
3
+ export interface WebhookTarget {
4
+ name: string;
5
+ url: string;
6
+ method?: string;
7
+ headers?: Record<string, string>;
8
+ secret?: string;
9
+ }
10
+ export declare class WebhookTriggerSkill extends BaseSkill {
11
+ name: string;
12
+ description: string;
13
+ private targets;
14
+ registerTarget(target: WebhookTarget): void;
15
+ execute(context: AgentContext, message: Message): Promise<SkillResult>;
16
+ }
17
+ //# sourceMappingURL=webhook-trigger.d.ts.map
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookTriggerSkill = void 0;
4
+ const base_1 = require("./base");
5
+ class WebhookTriggerSkill extends base_1.BaseSkill {
6
+ name = 'webhook-trigger';
7
+ description = 'Trigger external webhooks. Usage: webhook <name> [payload JSON]';
8
+ targets = new Map();
9
+ registerTarget(target) {
10
+ this.targets.set(target.name, target);
11
+ }
12
+ async execute(context, message) {
13
+ const match = message.content.trim().match(/^webhook\s+(\S+)(?:\s+(.+))?$/is);
14
+ if (!match)
15
+ return this.noMatch();
16
+ const [, name, payloadStr] = match;
17
+ const target = this.targets.get(name);
18
+ if (!target) {
19
+ const available = Array.from(this.targets.keys()).join(', ') || 'none';
20
+ return this.match(`Unknown webhook "${name}". Available: ${available}`);
21
+ }
22
+ try {
23
+ const headers = {
24
+ 'Content-Type': 'application/json',
25
+ 'User-Agent': 'OPC-Agent/0.7.0',
26
+ ...target.headers,
27
+ };
28
+ if (target.secret) {
29
+ headers['X-Webhook-Secret'] = target.secret;
30
+ }
31
+ const body = payloadStr ?? JSON.stringify({
32
+ agent: context.agentName,
33
+ timestamp: Date.now(),
34
+ trigger: 'manual',
35
+ });
36
+ const res = await fetch(target.url, {
37
+ method: target.method ?? 'POST',
38
+ headers,
39
+ body,
40
+ });
41
+ return this.match(`Webhook "${name}" triggered → ${res.status} ${res.statusText}`);
42
+ }
43
+ catch (err) {
44
+ return this.match(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);
45
+ }
46
+ }
47
+ }
48
+ exports.WebhookTriggerSkill = WebhookTriggerSkill;
49
+ //# sourceMappingURL=webhook-trigger.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opc-agent",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Open Agent Framework — Build, test, and run AI Agents for business workstations",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -4,6 +4,59 @@ import type { Message } from '../core/types';
4
4
  import { BaseChannel } from './index';
5
5
  import { KnowledgeBase } from '../core/knowledge';
6
6
  import { createProvider, type LLMProvider } from '../providers';
7
+ import { createAuthMiddleware, type AuthConfig } from '../core/auth';
8
+
9
+ const AGENT_TEMPLATES = [
10
+ { id: 'customer-service', name: 'Customer Service', description: 'Handle support tickets, FAQs, and customer inquiries', icon: '🎧', category: 'Business' },
11
+ { id: 'code-reviewer', name: 'Code Reviewer', description: 'Review PRs, suggest improvements, check for bugs', icon: '🔍', category: 'Engineering' },
12
+ { id: 'content-writer', name: 'Content Writer', description: 'Write blogs, social media posts, and marketing copy', icon: '✍️', category: 'Marketing' },
13
+ { id: 'executive-assistant', name: 'Executive Assistant', description: 'Schedule management, email drafting, meeting prep', icon: '📋', category: 'Business' },
14
+ { id: 'knowledge-base', name: 'Knowledge Base', description: 'RAG-powered Q&A over your documents', icon: '📚', category: 'Knowledge' },
15
+ { id: 'project-manager', name: 'Project Manager', description: 'Track tasks, milestones, and team coordination', icon: '📊', category: 'Business' },
16
+ { id: 'sales-assistant', name: 'Sales Assistant', description: 'Lead qualification, outreach drafting, CRM updates', icon: '💼', category: 'Sales' },
17
+ { id: 'financial-advisor', name: 'Financial Advisor', description: 'Budget analysis, financial planning, cost optimization', icon: '💰', category: 'Finance' },
18
+ { id: 'hr-recruiter', name: 'HR Recruiter', description: 'Resume screening, interview scheduling, candidate comms', icon: '👥', category: 'HR' },
19
+ { id: 'legal-assistant', name: 'Legal Assistant', description: 'Contract review, compliance checks, legal research', icon: '⚖️', category: 'Legal' },
20
+ ];
21
+
22
+ const TEMPLATES_HTML = `<!DOCTYPE html>
23
+ <html lang="en">
24
+ <head>
25
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
26
+ <title>Agent Templates</title>
27
+ <style>
28
+ *{margin:0;padding:0;box-sizing:border-box}
29
+ body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
30
+ h1{font-size:28px;margin-bottom:8px;color:#fff}
31
+ .sub{color:#888;margin-bottom:32px;font-size:14px}
32
+ nav{margin-bottom:24px}
33
+ nav a{color:#818cf8;text-decoration:none;margin-right:16px;font-size:14px}
34
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
35
+ .card{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:24px;cursor:pointer;transition:all .2s}
36
+ .card:hover{border-color:#818cf8;transform:translateY(-2px)}
37
+ .card .icon{font-size:32px;margin-bottom:12px}
38
+ .card h3{font-size:16px;color:#fff;margin-bottom:8px}
39
+ .card p{font-size:13px;color:#888;line-height:1.5}
40
+ .card .cat{font-size:11px;color:#818cf8;text-transform:uppercase;letter-spacing:1px;margin-top:12px}
41
+ .btn{display:inline-block;background:#2563eb;color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer;margin-top:12px}
42
+ .btn:hover{background:#1d4ed8}
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <nav><a href="/">← Chat</a><a href="/dashboard">Dashboard</a><a href="/templates">Templates</a></nav>
47
+ <h1>🧩 Agent Templates</h1>
48
+ <p class="sub">Create a new agent from a pre-built template in one click.</p>
49
+ <div class="grid" id="grid"></div>
50
+ <script>
51
+ fetch('/api/templates').then(r=>r.json()).then(d=>{
52
+ const g=document.getElementById('grid');
53
+ d.templates.forEach(t=>{
54
+ g.innerHTML+=\`<div class="card"><div class="icon">\${t.icon}</div><h3>\${t.name}</h3><p>\${t.description}</p><div class="cat">\${t.category}</div><button class="btn" onclick="alert('Creating agent from template: '+'\${t.id}'+'\\\\nRun: opc init --template \${t.id}')">Use Template</button></div>\`;
55
+ });
56
+ });
57
+ </script>
58
+ </body>
59
+ </html>`;
7
60
 
8
61
  const CHAT_HTML = `<!DOCTYPE html>
9
62
  <html lang="en">
@@ -157,8 +210,12 @@ export class WebChannel extends BaseChannel {
157
210
  private streamHandler: ((msg: Message, res: Response) => Promise<void>) | null = null;
158
211
  private agentName: string = 'OPC Agent';
159
212
  private currentProvider: string = 'openai';
160
- private stats = { sessions: 0, messages: 0, totalResponseMs: 0, tokenUsage: 0, knowledgeFiles: 0, startedAt: Date.now() };
213
+ private stats = { sessions: 0, messages: 0, totalResponseMs: 0, tokenUsage: 0, knowledgeFiles: 0, startedAt: Date.now(), errors: 0 };
161
214
  private eventHandlers: Map<string, Function[]> = new Map();
215
+ private conversations: Map<string, Message[]> = new Map();
216
+ private requestCount = 0;
217
+ private llmLatencySum = 0;
218
+ private llmCalls = 0;
162
219
 
163
220
  private emit(event: string, data: any): void {
164
221
  const handlers = this.eventHandlers.get(event) ?? [];
@@ -175,15 +232,23 @@ export class WebChannel extends BaseChannel {
175
232
  this.stats.messages++;
176
233
  this.stats.totalResponseMs += responseMs;
177
234
  this.stats.tokenUsage += tokens;
235
+ this.requestCount++;
236
+ this.llmLatencySum += responseMs;
237
+ this.llmCalls++;
178
238
  }
179
239
 
240
+ trackError(): void { this.stats.errors++; }
241
+
180
242
  trackSession(): void { this.stats.sessions++; }
181
243
 
182
- constructor(port: number = 3000) {
244
+ constructor(port: number = 3000, authConfig?: AuthConfig) {
183
245
  super();
184
246
  this.port = port;
185
247
  this.app = express();
186
- this.app.use(express.json());
248
+ this.app.use(express.json({ limit: '10mb' }));
249
+ if (authConfig && authConfig.apiKeys.length > 0) {
250
+ this.app.use(createAuthMiddleware(authConfig));
251
+ }
187
252
  this.setupRoutes();
188
253
  }
189
254
 
@@ -211,6 +276,7 @@ export class WebChannel extends BaseChannel {
211
276
  // Streaming chat endpoint
212
277
  this.app.post('/api/chat', async (req: Request, res: Response) => {
213
278
  const { message, sessionId } = req.body;
279
+ const sid = sessionId ?? 'default';
214
280
  if (!message) {
215
281
  res.status(400).json({ error: 'message is required' });
216
282
  return;
@@ -221,9 +287,13 @@ export class WebChannel extends BaseChannel {
221
287
  role: 'user',
222
288
  content: message,
223
289
  timestamp: Date.now(),
224
- metadata: { sessionId: sessionId ?? 'default' },
290
+ metadata: { sessionId: sid },
225
291
  };
226
292
 
293
+ // Track conversation
294
+ if (!this.conversations.has(sid)) this.conversations.set(sid, []);
295
+ this.conversations.get(sid)!.push(msg);
296
+
227
297
  if (this.streamHandler) {
228
298
  try {
229
299
  await this.streamHandler(msg, res);
@@ -301,6 +371,107 @@ export class WebChannel extends BaseChannel {
301
371
  } catch { res.json({ totalEntries: 0, sources: [] }); }
302
372
  });
303
373
 
374
+ // --- Health Check (detailed) ---
375
+ this.app.get('/api/health', (_req: Request, res: Response) => {
376
+ const uptimeMs = Date.now() - this.stats.startedAt;
377
+ res.json({
378
+ status: 'ok',
379
+ timestamp: Date.now(),
380
+ uptime: uptimeMs,
381
+ uptimeHuman: `${Math.floor(uptimeMs / 3600000)}h ${Math.floor((uptimeMs % 3600000) / 60000)}m`,
382
+ version: '0.7.0',
383
+ agent: this.agentName,
384
+ stats: {
385
+ sessions: this.stats.sessions,
386
+ messages: this.stats.messages,
387
+ errors: this.stats.errors,
388
+ avgResponseMs: this.stats.messages > 0 ? Math.round(this.stats.totalResponseMs / this.stats.messages) : 0,
389
+ },
390
+ memory: {
391
+ rss: process.memoryUsage().rss,
392
+ heapUsed: process.memoryUsage().heapUsed,
393
+ },
394
+ });
395
+ });
396
+
397
+ // --- Prometheus Metrics ---
398
+ this.app.get('/api/metrics', (_req: Request, res: Response) => {
399
+ const uptimeMs = Date.now() - this.stats.startedAt;
400
+ const avgLatency = this.llmCalls > 0 ? this.llmLatencySum / this.llmCalls : 0;
401
+ const mem = process.memoryUsage();
402
+ res.type('text/plain').send(
403
+ `# HELP opc_uptime_seconds Agent uptime in seconds\n` +
404
+ `# TYPE opc_uptime_seconds gauge\n` +
405
+ `opc_uptime_seconds ${(uptimeMs / 1000).toFixed(1)}\n` +
406
+ `# HELP opc_requests_total Total requests\n` +
407
+ `# TYPE opc_requests_total counter\n` +
408
+ `opc_requests_total ${this.requestCount}\n` +
409
+ `# HELP opc_messages_total Total messages processed\n` +
410
+ `# TYPE opc_messages_total counter\n` +
411
+ `opc_messages_total ${this.stats.messages}\n` +
412
+ `# HELP opc_errors_total Total errors\n` +
413
+ `# TYPE opc_errors_total counter\n` +
414
+ `opc_errors_total ${this.stats.errors}\n` +
415
+ `# HELP opc_llm_latency_avg_ms Average LLM response latency\n` +
416
+ `# TYPE opc_llm_latency_avg_ms gauge\n` +
417
+ `opc_llm_latency_avg_ms ${avgLatency.toFixed(1)}\n` +
418
+ `# HELP opc_sessions_total Total sessions\n` +
419
+ `# TYPE opc_sessions_total counter\n` +
420
+ `opc_sessions_total ${this.stats.sessions}\n` +
421
+ `# HELP opc_token_usage_total Total token usage\n` +
422
+ `# TYPE opc_token_usage_total counter\n` +
423
+ `opc_token_usage_total ${this.stats.tokenUsage}\n` +
424
+ `# HELP process_resident_memory_bytes Resident memory size\n` +
425
+ `# TYPE process_resident_memory_bytes gauge\n` +
426
+ `process_resident_memory_bytes ${mem.rss}\n`
427
+ );
428
+ });
429
+
430
+ // --- Conversation tracking & export ---
431
+ this.app.get('/api/conversations/export', (req: Request, res: Response) => {
432
+ const sessionId = req.query.sessionId as string;
433
+ const format = (req.query.format as string) ?? 'json';
434
+
435
+ const messages = sessionId ? (this.conversations.get(sessionId) ?? []) : Array.from(this.conversations.values()).flat();
436
+
437
+ if (format === 'markdown') {
438
+ const md = messages.map(m => `**${m.role}** (${new Date(m.timestamp).toISOString()}):\n${m.content}`).join('\n\n---\n\n');
439
+ res.type('text/markdown').send(md);
440
+ } else if (format === 'csv') {
441
+ const header = 'id,role,content,timestamp\n';
442
+ const rows = messages.map(m => `"${m.id}","${m.role}","${m.content.replace(/"/g, '""')}",${m.timestamp}`).join('\n');
443
+ res.type('text/csv').send(header + rows);
444
+ } else {
445
+ res.json({ sessionId: sessionId ?? 'all', messages, count: messages.length });
446
+ }
447
+ });
448
+
449
+ // --- Document Upload ---
450
+ this.app.post('/api/documents/upload', async (req: Request, res: Response) => {
451
+ try {
452
+ const { content, filename, mimeType } = req.body;
453
+ if (!content || !filename) {
454
+ res.status(400).json({ error: 'content and filename are required' });
455
+ return;
456
+ }
457
+ const kb = new KnowledgeBase('.');
458
+ const result = await kb.addText(content, filename);
459
+ this.stats.knowledgeFiles++;
460
+ res.json({ ok: true, filename, chunks: result.chunks, chars: content.length });
461
+ } catch (err) {
462
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Upload failed' });
463
+ }
464
+ });
465
+
466
+ // --- Agent Templates Gallery ---
467
+ this.app.get('/api/templates', (_req: Request, res: Response) => {
468
+ res.json({ templates: AGENT_TEMPLATES });
469
+ });
470
+
471
+ this.app.get('/templates', (_req: Request, res: Response) => {
472
+ res.type('html').send(TEMPLATES_HTML);
473
+ });
474
+
304
475
  // Legacy endpoint
305
476
  this.app.post('/chat', async (req: Request, res: Response) => {
306
477
  if (!this.handler) {
@@ -0,0 +1,57 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+
3
+ export interface AuthConfig {
4
+ apiKeys: string[];
5
+ sessionIsolation?: boolean;
6
+ }
7
+
8
+ export interface AuthSession {
9
+ apiKey: string;
10
+ userId: string;
11
+ createdAt: number;
12
+ }
13
+
14
+ const sessions = new Map<string, AuthSession>();
15
+
16
+ export function createAuthMiddleware(config: AuthConfig) {
17
+ return (req: Request, res: Response, next: NextFunction): void => {
18
+ // Skip auth for non-API routes and health/metrics
19
+ if (!req.path.startsWith('/api/') || req.path === '/api/health' || req.path === '/api/metrics') {
20
+ next();
21
+ return;
22
+ }
23
+
24
+ const apiKey = req.headers['x-api-key'] as string
25
+ ?? req.headers['authorization']?.replace(/^Bearer\s+/i, '')
26
+ ?? (req.query as any).apiKey;
27
+
28
+ if (!apiKey || !config.apiKeys.includes(apiKey)) {
29
+ res.status(401).json({ error: 'Unauthorized. Provide a valid API key via X-API-Key header, Bearer token, or ?apiKey query.' });
30
+ return;
31
+ }
32
+
33
+ // Derive userId from API key for session isolation
34
+ const userId = `user_${hashKey(apiKey)}`;
35
+ if (!sessions.has(apiKey)) {
36
+ sessions.set(apiKey, { apiKey, userId, createdAt: Date.now() });
37
+ }
38
+
39
+ // Attach user info to request
40
+ (req as any).userId = userId;
41
+ (req as any).sessionPrefix = config.sessionIsolation ? `${userId}:` : '';
42
+
43
+ next();
44
+ };
45
+ }
46
+
47
+ function hashKey(key: string): string {
48
+ let h = 0;
49
+ for (let i = 0; i < key.length; i++) {
50
+ h = ((h << 5) - h + key.charCodeAt(i)) | 0;
51
+ }
52
+ return Math.abs(h).toString(36);
53
+ }
54
+
55
+ export function getActiveSessions(): AuthSession[] {
56
+ return Array.from(sessions.values());
57
+ }
package/src/index.ts CHANGED
@@ -54,3 +54,14 @@ export { deployToHermes } from './deploy/hermes';
54
54
  export type { HermesDeployOptions, HermesDeployResult } from './deploy/hermes';
55
55
  export { publishAgent, installAgent } from './marketplace';
56
56
  export type { AgentManifest, PublishOptions, InstallOptions } from './marketplace';
57
+
58
+ // v0.7.0 modules
59
+ export { createAuthMiddleware, getActiveSessions } from './core/auth';
60
+ export type { AuthConfig, AuthSession } from './core/auth';
61
+ export { HttpSkill } from './skills/http';
62
+ export { WebhookTriggerSkill } from './skills/webhook-trigger';
63
+ export type { WebhookTarget } from './skills/webhook-trigger';
64
+ export { SchedulerSkill } from './skills/scheduler';
65
+ export type { ScheduledTask } from './skills/scheduler';
66
+ export { DocumentSkill } from './skills/document';
67
+ export type { DocumentChunk } from './skills/document';
package/src/schema/oad.ts CHANGED
@@ -48,6 +48,12 @@ export const HITLSchema = z.object({
48
48
  defaultAction: z.enum(['approve', 'deny']).default('deny'),
49
49
  });
50
50
 
51
+ export const AuthSchema = z.object({
52
+ enabled: z.boolean().default(false),
53
+ apiKeys: z.array(z.string()).default([]),
54
+ sessionIsolation: z.boolean().default(true),
55
+ });
56
+
51
57
  export const ChannelSchema = z.object({
52
58
  type: z.enum(['web', 'websocket', 'telegram', 'cli', 'voice', 'webhook']),
53
59
  port: z.number().optional(),
@@ -124,6 +130,7 @@ export const SpecSchema = z.object({
124
130
  voice: VoiceSchema.optional(),
125
131
  webhook: WebhookSchema.optional(),
126
132
  hitl: HITLSchema.optional(),
133
+ auth: AuthSchema.optional(),
127
134
  });
128
135
 
129
136
  export const OADSchema = z.object({
@@ -0,0 +1,100 @@
1
+ import { BaseSkill } from './base';
2
+ import type { AgentContext, Message, SkillResult } from '../core/types';
3
+ import { KnowledgeBase } from '../core/knowledge';
4
+
5
+ export interface DocumentChunk {
6
+ content: string;
7
+ metadata: {
8
+ filename: string;
9
+ mimeType: string;
10
+ chunkIndex: number;
11
+ totalChunks: number;
12
+ };
13
+ }
14
+
15
+ export class DocumentSkill extends BaseSkill {
16
+ name = 'document';
17
+ description = 'Process uploaded documents (PDF, TXT, MD, DOCX). Chunks content and adds to knowledge base.';
18
+ private knowledgeBase: KnowledgeBase;
19
+
20
+ constructor(kbPath: string = '.') {
21
+ super();
22
+ this.knowledgeBase = new KnowledgeBase(kbPath);
23
+ }
24
+
25
+ async execute(context: AgentContext, message: Message): Promise<SkillResult> {
26
+ // Check if message has document attachment metadata
27
+ const meta = message.metadata;
28
+ if (!meta?.document) return this.noMatch();
29
+
30
+ const { content, filename, mimeType } = meta.document as {
31
+ content: string; filename: string; mimeType: string;
32
+ };
33
+
34
+ try {
35
+ const text = this.extractText(content, mimeType ?? this.guessMime(filename));
36
+ const chunks = this.chunk(text, 1000, 100);
37
+
38
+ const result = await this.knowledgeBase.addText(
39
+ chunks.map(c => c.content).join('\n\n---\n\n'),
40
+ filename
41
+ );
42
+
43
+ return this.match(
44
+ `📄 Processed "${filename}": ${chunks.length} chunks, ${text.length} chars → added to knowledge base (${result.chunks} KB entries)`
45
+ );
46
+ } catch (err) {
47
+ return this.match(`Document processing error: ${err instanceof Error ? err.message : String(err)}`);
48
+ }
49
+ }
50
+
51
+ extractText(content: string, mimeType: string): string {
52
+ // For plain text formats, content is already text
53
+ if (mimeType.includes('text/') || mimeType.includes('markdown')) {
54
+ return content;
55
+ }
56
+ // For other formats, assume base64-encoded or pre-extracted text
57
+ // In a real implementation, you'd use pdf-parse, mammoth, etc.
58
+ return content;
59
+ }
60
+
61
+ chunk(text: string, size: number = 1000, overlap: number = 100): DocumentChunk[] {
62
+ const chunks: DocumentChunk[] = [];
63
+ let start = 0;
64
+ while (start < text.length) {
65
+ const end = Math.min(start + size, text.length);
66
+ chunks.push({
67
+ content: text.slice(start, end),
68
+ metadata: {
69
+ filename: '',
70
+ mimeType: '',
71
+ chunkIndex: chunks.length,
72
+ totalChunks: 0, // filled after
73
+ },
74
+ });
75
+ start = end - overlap;
76
+ if (start >= text.length) break;
77
+ }
78
+ // Fill totalChunks
79
+ for (const c of chunks) c.metadata.totalChunks = chunks.length;
80
+ return chunks;
81
+ }
82
+
83
+ private guessMime(filename: string): string {
84
+ const ext = filename.split('.').pop()?.toLowerCase();
85
+ switch (ext) {
86
+ case 'pdf': return 'application/pdf';
87
+ case 'txt': return 'text/plain';
88
+ case 'md': return 'text/markdown';
89
+ case 'docx': return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
90
+ default: return 'text/plain';
91
+ }
92
+ }
93
+
94
+ /** Process raw text content directly (for API uploads) */
95
+ async processText(content: string, filename: string): Promise<{ chunks: number; chars: number }> {
96
+ const chunks = this.chunk(content);
97
+ const result = await this.knowledgeBase.addText(content, filename);
98
+ return { chunks: chunks.length, chars: content.length };
99
+ }
100
+ }
@@ -0,0 +1,35 @@
1
+ import { BaseSkill } from './base';
2
+ import type { AgentContext, Message, SkillResult } from '../core/types';
3
+
4
+ export class HttpSkill extends BaseSkill {
5
+ name = 'http';
6
+ description = 'Make HTTP requests to external APIs. Usage: http GET|POST|PUT|DELETE <url> [body]';
7
+
8
+ async execute(context: AgentContext, message: Message): Promise<SkillResult> {
9
+ const text = message.content.trim();
10
+ const match = text.match(/^http\s+(GET|POST|PUT|PATCH|DELETE)\s+(\S+)(?:\s+(.+))?$/is);
11
+ if (!match) return this.noMatch();
12
+
13
+ const [, method, url, bodyStr] = match;
14
+
15
+ try {
16
+ const opts: RequestInit = {
17
+ method: method.toUpperCase(),
18
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'OPC-Agent/0.7.0' },
19
+ };
20
+
21
+ if (bodyStr && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
22
+ opts.body = bodyStr;
23
+ }
24
+
25
+ const res = await fetch(url, opts);
26
+ const contentType = res.headers.get('content-type') ?? '';
27
+ const body = contentType.includes('json') ? JSON.stringify(await res.json(), null, 2) : await res.text();
28
+
29
+ const truncated = body.length > 4000 ? body.slice(0, 4000) + '\n...[truncated]' : body;
30
+ return this.match(`HTTP ${res.status} ${res.statusText}\n\n${truncated}`);
31
+ } catch (err) {
32
+ return this.match(`HTTP Error: ${err instanceof Error ? err.message : String(err)}`);
33
+ }
34
+ }
35
+ }