opc-agent 1.4.0 → 2.0.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -32
  3. package/dist/channels/telegram.d.ts +30 -9
  4. package/dist/channels/telegram.js +125 -33
  5. package/dist/cli.js +415 -8
  6. package/dist/core/agent.d.ts +23 -0
  7. package/dist/core/agent.js +120 -3
  8. package/dist/core/runtime.d.ts +1 -0
  9. package/dist/core/runtime.js +44 -0
  10. package/dist/core/scheduler.d.ts +52 -0
  11. package/dist/core/scheduler.js +168 -0
  12. package/dist/core/subagent.d.ts +28 -0
  13. package/dist/core/subagent.js +65 -0
  14. package/dist/daemon.d.ts +3 -0
  15. package/dist/daemon.js +134 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +17 -1
  18. package/dist/providers/index.d.ts +5 -1
  19. package/dist/providers/index.js +16 -9
  20. package/dist/schema/oad.d.ts +179 -4
  21. package/dist/schema/oad.js +12 -1
  22. package/dist/skills/auto-learn.d.ts +28 -0
  23. package/dist/skills/auto-learn.js +257 -0
  24. package/dist/tools/builtin/datetime.d.ts +3 -0
  25. package/dist/tools/builtin/datetime.js +44 -0
  26. package/dist/tools/builtin/file.d.ts +3 -0
  27. package/dist/tools/builtin/file.js +151 -0
  28. package/dist/tools/builtin/index.d.ts +15 -0
  29. package/dist/tools/builtin/index.js +30 -0
  30. package/dist/tools/builtin/shell.d.ts +3 -0
  31. package/dist/tools/builtin/shell.js +43 -0
  32. package/dist/tools/builtin/web.d.ts +3 -0
  33. package/dist/tools/builtin/web.js +37 -0
  34. package/dist/tools/mcp-client.d.ts +24 -0
  35. package/dist/tools/mcp-client.js +119 -0
  36. package/package.json +1 -1
  37. package/src/channels/telegram.ts +212 -90
  38. package/src/cli.ts +418 -8
  39. package/src/core/agent.ts +295 -152
  40. package/src/core/runtime.ts +47 -0
  41. package/src/core/scheduler.ts +187 -0
  42. package/src/core/subagent.ts +98 -0
  43. package/src/daemon.ts +96 -0
  44. package/src/index.ts +11 -0
  45. package/src/providers/index.ts +354 -339
  46. package/src/schema/oad.ts +167 -154
  47. package/src/skills/auto-learn.ts +262 -0
  48. package/src/tools/builtin/datetime.ts +41 -0
  49. package/src/tools/builtin/file.ts +107 -0
  50. package/src/tools/builtin/index.ts +28 -0
  51. package/src/tools/builtin/shell.ts +43 -0
  52. package/src/tools/builtin/web.ts +35 -0
  53. package/src/tools/mcp-client.ts +131 -0
  54. package/tests/auto-learn.test.ts +105 -0
  55. package/tests/builtin-tools.test.ts +83 -0
  56. package/tests/cli.test.ts +46 -0
  57. package/tests/subagent.test.ts +130 -0
  58. package/tests/telegram-discord.test.ts +60 -0
package/src/schema/oad.ts CHANGED
@@ -1,154 +1,167 @@
1
- import { z } from 'zod';
2
-
3
- // ─── OAD Schema v1 ───────────────────────────────────────────
4
-
5
- export const SkillRefSchema = z.object({
6
- name: z.string(),
7
- description: z.string().optional(),
8
- config: z.record(z.unknown()).optional(),
9
- });
10
-
11
- export const WorkflowStepSchema: z.ZodType<any> = z.lazy(() => z.object({
12
- id: z.string(),
13
- type: z.enum(['skill', 'tool', 'agent', 'condition', 'parallel']),
14
- name: z.string(),
15
- config: z.record(z.unknown()).optional(),
16
- condition: z.string().optional(),
17
- branches: z.object({ if: z.array(WorkflowStepSchema), else: z.array(WorkflowStepSchema).optional() }).optional(),
18
- parallel: z.array(WorkflowStepSchema).optional(),
19
- timeout: z.number().optional(),
20
- retries: z.number().optional(),
21
- }));
22
-
23
- export const WorkflowSchema = z.object({
24
- name: z.string(),
25
- description: z.string().optional(),
26
- version: z.string().optional(),
27
- steps: z.array(WorkflowStepSchema).default([]),
28
- onError: z.enum(['stop', 'skip', 'retry']).optional(),
29
- });
30
-
31
- export const VoiceSchema = z.object({
32
- enabled: z.boolean().default(false),
33
- sttProvider: z.string().optional(),
34
- ttsProvider: z.string().optional(),
35
- language: z.string().optional(),
36
- });
37
-
38
- export const WebhookSchema = z.object({
39
- path: z.string().optional(),
40
- secret: z.string().optional(),
41
- retryAttempts: z.number().optional(),
42
- });
43
-
44
- export const HITLSchema = z.object({
45
- enabled: z.boolean().default(false),
46
- requireApproval: z.array(z.string()).default([]),
47
- defaultTimeoutMs: z.number().default(60000),
48
- defaultAction: z.enum(['approve', 'deny']).default('deny'),
49
- });
50
-
51
- export const PluginRefSchema = z.object({
52
- name: z.string(),
53
- config: z.record(z.unknown()).optional(),
54
- });
55
-
56
- export const AuthSchema = z.object({
57
- enabled: z.boolean().default(false),
58
- apiKeys: z.array(z.string()).default([]),
59
- sessionIsolation: z.boolean().default(true),
60
- });
61
-
62
- export const ChannelSchema = z.object({
63
- type: z.enum(['web', 'websocket', 'telegram', 'cli', 'voice', 'webhook']),
64
- port: z.number().optional(),
65
- config: z.record(z.unknown()).optional(),
66
- });
67
-
68
- export const LongTermMemorySchema = z.object({
69
- provider: z.enum(['in-memory', 'deepbrain']).default('in-memory'),
70
- collection: z.string().optional(),
71
- config: z.record(z.unknown()).optional(),
72
- });
73
-
74
- export const MemorySchema = z.object({
75
- shortTerm: z.boolean().default(true),
76
- longTerm: z.union([z.boolean(), LongTermMemorySchema]).default(false),
77
- provider: z.string().optional(),
78
- });
79
-
80
- export const TrustLevel = z.enum(['sandbox', 'verified', 'certified', 'listed']);
81
-
82
- export const DTVSchema = z.object({
83
- trust: z.object({
84
- level: TrustLevel.default('sandbox'),
85
- }).optional(),
86
- value: z.object({
87
- metrics: z.array(z.string()).default([]),
88
- }).optional(),
89
- });
90
-
91
- export const ProviderSchema = z.object({
92
- default: z.string().default('deepseek'),
93
- allowed: z.array(z.string()).default(['openai', 'deepseek', 'qwen']),
94
- });
95
-
96
- export const MarketplaceSchema = z.object({
97
- certified: z.boolean().default(false),
98
- category: z.string().optional(),
99
- pricing: z.enum(['free', 'freemium', 'paid', 'enterprise']).optional(),
100
- tags: z.array(z.string()).optional(),
101
- });
102
-
103
- export const MetadataSchema = z.object({
104
- name: z.string(),
105
- version: z.string().default('1.0.0'),
106
- description: z.string().optional(),
107
- author: z.string().optional(),
108
- license: z.string().default('Apache-2.0'),
109
- marketplace: MarketplaceSchema.optional(),
110
- });
111
-
112
- export const RoomSchema = z.object({
113
- name: z.string(),
114
- agents: z.array(z.string()).default([]),
115
- topics: z.array(z.string()).default([]),
116
- });
117
-
118
- export const StreamingSchema = z.object({
119
- enabled: z.boolean().default(false),
120
- chunkSize: z.number().optional(),
121
- });
122
-
123
- export const SpecSchema = z.object({
124
- provider: ProviderSchema.optional(),
125
- model: z.string().default('deepseek-chat'),
126
- systemPrompt: z.string().optional(),
127
- skills: z.array(SkillRefSchema).default([]),
128
- channels: z.array(ChannelSchema).default([]),
129
- memory: MemorySchema.optional(),
130
- dtv: DTVSchema.optional(),
131
- room: RoomSchema.optional(),
132
- streaming: z.union([z.boolean(), StreamingSchema]).default(false),
133
- locale: z.enum(['en', 'zh-CN']).optional(),
134
- workflows: z.array(WorkflowSchema).optional(),
135
- voice: VoiceSchema.optional(),
136
- webhook: WebhookSchema.optional(),
137
- hitl: HITLSchema.optional(),
138
- auth: AuthSchema.optional(),
139
- plugins: z.array(PluginRefSchema).optional(),
140
- });
141
-
142
- export const OADSchema = z.object({
143
- apiVersion: z.literal('opc/v1'),
144
- kind: z.literal('Agent'),
145
- metadata: MetadataSchema,
146
- spec: SpecSchema,
147
- });
148
-
149
- export type OADDocument = z.infer<typeof OADSchema>;
150
- export type SkillRef = z.infer<typeof SkillRefSchema>;
151
- export type Channel = z.infer<typeof ChannelSchema>;
152
- export type Metadata = z.infer<typeof MetadataSchema>;
153
- export type Spec = z.infer<typeof SpecSchema>;
154
- export type TrustLevelType = string;
1
+ import { z } from 'zod';
2
+
3
+ // ─── OAD Schema v1 ───────────────────────────────────────────
4
+
5
+ export const SkillRefSchema = z.object({
6
+ name: z.string(),
7
+ description: z.string().optional(),
8
+ config: z.record(z.unknown()).optional(),
9
+ });
10
+
11
+ export const WorkflowStepSchema: z.ZodType<any> = z.lazy(() => z.object({
12
+ id: z.string(),
13
+ type: z.enum(['skill', 'tool', 'agent', 'condition', 'parallel']),
14
+ name: z.string(),
15
+ config: z.record(z.unknown()).optional(),
16
+ condition: z.string().optional(),
17
+ branches: z.object({ if: z.array(WorkflowStepSchema), else: z.array(WorkflowStepSchema).optional() }).optional(),
18
+ parallel: z.array(WorkflowStepSchema).optional(),
19
+ timeout: z.number().optional(),
20
+ retries: z.number().optional(),
21
+ }));
22
+
23
+ export const WorkflowSchema = z.object({
24
+ name: z.string(),
25
+ description: z.string().optional(),
26
+ version: z.string().optional(),
27
+ steps: z.array(WorkflowStepSchema).default([]),
28
+ onError: z.enum(['stop', 'skip', 'retry']).optional(),
29
+ });
30
+
31
+ export const VoiceSchema = z.object({
32
+ enabled: z.boolean().default(false),
33
+ sttProvider: z.string().optional(),
34
+ ttsProvider: z.string().optional(),
35
+ language: z.string().optional(),
36
+ });
37
+
38
+ export const WebhookSchema = z.object({
39
+ path: z.string().optional(),
40
+ secret: z.string().optional(),
41
+ retryAttempts: z.number().optional(),
42
+ });
43
+
44
+ export const HITLSchema = z.object({
45
+ enabled: z.boolean().default(false),
46
+ requireApproval: z.array(z.string()).default([]),
47
+ defaultTimeoutMs: z.number().default(60000),
48
+ defaultAction: z.enum(['approve', 'deny']).default('deny'),
49
+ });
50
+
51
+ export const PluginRefSchema = z.object({
52
+ name: z.string(),
53
+ config: z.record(z.unknown()).optional(),
54
+ });
55
+
56
+ export const AuthSchema = z.object({
57
+ enabled: z.boolean().default(false),
58
+ apiKeys: z.array(z.string()).default([]),
59
+ sessionIsolation: z.boolean().default(true),
60
+ });
61
+
62
+ export const ChannelSchema = z.object({
63
+ type: z.enum(['web', 'websocket', 'telegram', 'cli', 'voice', 'webhook']),
64
+ port: z.number().optional(),
65
+ config: z.record(z.unknown()).optional(),
66
+ });
67
+
68
+ export const LongTermMemorySchema = z.object({
69
+ provider: z.enum(['in-memory', 'deepbrain']).default('in-memory'),
70
+ collection: z.string().optional(),
71
+ config: z.record(z.unknown()).optional(),
72
+ });
73
+
74
+ export const MemorySchema = z.object({
75
+ shortTerm: z.boolean().default(true),
76
+ longTerm: z.union([z.boolean(), LongTermMemorySchema]).default(false),
77
+ provider: z.string().optional(),
78
+ });
79
+
80
+ export const TrustLevel = z.enum(['sandbox', 'verified', 'certified', 'listed']);
81
+
82
+ export const DTVSchema = z.object({
83
+ trust: z.object({
84
+ level: TrustLevel.default('sandbox'),
85
+ }).optional(),
86
+ value: z.object({
87
+ metrics: z.array(z.string()).default([]),
88
+ }).optional(),
89
+ });
90
+
91
+ export const ProviderSchema = z.object({
92
+ default: z.string().default('deepseek'),
93
+ allowed: z.array(z.string()).default(['openai', 'deepseek', 'qwen']),
94
+ });
95
+
96
+ export const MarketplaceSchema = z.object({
97
+ certified: z.boolean().default(false),
98
+ category: z.string().optional(),
99
+ pricing: z.enum(['free', 'freemium', 'paid', 'enterprise']).optional(),
100
+ tags: z.array(z.string()).optional(),
101
+ });
102
+
103
+ export const MetadataSchema = z.object({
104
+ name: z.string(),
105
+ version: z.string().default('1.0.0'),
106
+ description: z.string().optional(),
107
+ author: z.string().optional(),
108
+ license: z.string().default('Apache-2.0'),
109
+ marketplace: MarketplaceSchema.optional(),
110
+ });
111
+
112
+ export const RoomSchema = z.object({
113
+ name: z.string(),
114
+ agents: z.array(z.string()).default([]),
115
+ topics: z.array(z.string()).default([]),
116
+ });
117
+
118
+ export const StreamingSchema = z.object({
119
+ enabled: z.boolean().default(false),
120
+ chunkSize: z.number().optional(),
121
+ });
122
+
123
+ export const MCPServerSchema = z.object({
124
+ name: z.string(),
125
+ command: z.string(),
126
+ args: z.array(z.string()).optional(),
127
+ env: z.record(z.string()).optional(),
128
+ });
129
+
130
+ export const ToolsSchema = z.object({
131
+ builtin: z.array(z.string()).optional(),
132
+ mcp: z.array(MCPServerSchema).optional(),
133
+ });
134
+
135
+ export const SpecSchema = z.object({
136
+ provider: ProviderSchema.optional(),
137
+ model: z.string().default('deepseek-chat'),
138
+ systemPrompt: z.string().optional(),
139
+ skills: z.array(SkillRefSchema).default([]),
140
+ channels: z.array(ChannelSchema).default([]),
141
+ memory: MemorySchema.optional(),
142
+ tools: ToolsSchema.optional(),
143
+ dtv: DTVSchema.optional(),
144
+ room: RoomSchema.optional(),
145
+ streaming: z.union([z.boolean(), StreamingSchema]).default(false),
146
+ locale: z.enum(['en', 'zh-CN']).optional(),
147
+ workflows: z.array(WorkflowSchema).optional(),
148
+ voice: VoiceSchema.optional(),
149
+ webhook: WebhookSchema.optional(),
150
+ hitl: HITLSchema.optional(),
151
+ auth: AuthSchema.optional(),
152
+ plugins: z.array(PluginRefSchema).optional(),
153
+ });
154
+
155
+ export const OADSchema = z.object({
156
+ apiVersion: z.literal('opc/v1'),
157
+ kind: z.literal('Agent'),
158
+ metadata: MetadataSchema,
159
+ spec: SpecSchema,
160
+ });
161
+
162
+ export type OADDocument = z.infer<typeof OADSchema>;
163
+ export type SkillRef = z.infer<typeof SkillRefSchema>;
164
+ export type Channel = z.infer<typeof ChannelSchema>;
165
+ export type Metadata = z.infer<typeof MetadataSchema>;
166
+ export type Spec = z.infer<typeof SpecSchema>;
167
+ export type TrustLevelType = string;
@@ -0,0 +1,262 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import type { Message } from '../core/types';
4
+ import type { LLMProvider } from '../providers';
5
+
6
+ export interface LearnedSkill {
7
+ name: string;
8
+ description: string;
9
+ trigger: string;
10
+ instructions: string;
11
+ examples: string[];
12
+ createdAt: Date;
13
+ usageCount: number;
14
+ lastUsed?: Date;
15
+ version: number;
16
+ }
17
+
18
+ const SKILL_EXTRACTION_PROMPT = `Analyze this conversation and determine if it represents a repeatable task that should be saved as a reusable skill.
19
+
20
+ Criteria for creating a skill:
21
+ 1. The task is specific and well-defined
22
+ 2. It could reasonably happen again
23
+ 3. The solution has clear steps
24
+
25
+ If yes, extract:
26
+ - name: short kebab-case name
27
+ - description: one-line description
28
+ - trigger: regex pattern or keywords that would identify this task
29
+ - instructions: step-by-step instructions to complete the task
30
+ - examples: 2-3 example user inputs that would trigger this skill
31
+
32
+ If this is just casual chat or a one-off question, return null.
33
+
34
+ Respond in JSON format only: { "shouldCreate": boolean, "skill": { "name": string, "description": string, "trigger": string, "instructions": string, "examples": string[] } | null }
35
+
36
+ Conversation:
37
+ `;
38
+
39
+ const SKILL_IMPROVEMENT_PROMPT = `This skill was just used. Based on the outcome, suggest improvements:
40
+
41
+ Current skill:
42
+ `;
43
+
44
+ const SKILL_IMPROVEMENT_SUFFIX = `
45
+
46
+ Respond in JSON only: { "shouldImprove": boolean, "improvements": { "instructions"?: string, "trigger"?: string, "examples"?: string[] } | null }`;
47
+
48
+ export class SkillLearner {
49
+ private skillsDir: string;
50
+ private skills: LearnedSkill[] = [];
51
+ private loaded = false;
52
+
53
+ constructor(skillsDir: string) {
54
+ this.skillsDir = skillsDir;
55
+ }
56
+
57
+ async analyzeForSkillCreation(
58
+ conversation: Message[],
59
+ provider: LLMProvider,
60
+ ): Promise<LearnedSkill | null> {
61
+ const conversationText = conversation
62
+ .map((m) => `${m.role}: ${m.content}`)
63
+ .join('\n');
64
+
65
+ const prompt = SKILL_EXTRACTION_PROMPT + conversationText;
66
+
67
+ try {
68
+ const response = await provider.chat(
69
+ [{ id: 'analysis', role: 'user', content: prompt, timestamp: Date.now() }],
70
+ 'You are a skill extraction assistant. Respond only with valid JSON.',
71
+ );
72
+
73
+ const json = extractJson(response);
74
+ if (!json || !json.shouldCreate || !json.skill) return null;
75
+
76
+ const skill: LearnedSkill = {
77
+ name: json.skill.name,
78
+ description: json.skill.description,
79
+ trigger: json.skill.trigger,
80
+ instructions: json.skill.instructions,
81
+ examples: json.skill.examples || [],
82
+ createdAt: new Date(),
83
+ usageCount: 0,
84
+ version: 1,
85
+ };
86
+
87
+ return skill;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async saveSkill(skill: LearnedSkill): Promise<void> {
94
+ fs.mkdirSync(this.skillsDir, { recursive: true });
95
+ const filePath = path.join(this.skillsDir, `${skill.name}.md`);
96
+ fs.writeFileSync(filePath, skillToMarkdown(skill), 'utf-8');
97
+
98
+ // Update cache
99
+ const idx = this.skills.findIndex((s) => s.name === skill.name);
100
+ if (idx >= 0) {
101
+ this.skills[idx] = skill;
102
+ } else {
103
+ this.skills.push(skill);
104
+ }
105
+ }
106
+
107
+ async loadLearnedSkills(): Promise<LearnedSkill[]> {
108
+ if (!fs.existsSync(this.skillsDir)) return [];
109
+
110
+ const files = fs.readdirSync(this.skillsDir).filter((f) => f.endsWith('.md'));
111
+ this.skills = files
112
+ .map((f) => {
113
+ try {
114
+ const content = fs.readFileSync(path.join(this.skillsDir, f), 'utf-8');
115
+ return parseSkillMarkdown(content);
116
+ } catch {
117
+ return null;
118
+ }
119
+ })
120
+ .filter((s): s is LearnedSkill => s !== null);
121
+
122
+ this.loaded = true;
123
+ return this.skills;
124
+ }
125
+
126
+ matchSkill(message: string): LearnedSkill | null {
127
+ if (!this.loaded) return null;
128
+
129
+ for (const skill of this.skills) {
130
+ try {
131
+ const regex = new RegExp(skill.trigger, 'i');
132
+ if (regex.test(message)) return skill;
133
+ } catch {
134
+ // Fallback: keyword matching — split on common separators, strip non-word chars
135
+ const keywords = skill.trigger.split(/[\s,;|]+/).map(k => k.replace(/[^\w-]/g, '').toLowerCase()).filter(k => k.length > 2);
136
+ const lower = message.toLowerCase();
137
+ if (keywords.some((kw) => lower.includes(kw))) return skill;
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+
143
+ async improveSkill(
144
+ skill: LearnedSkill,
145
+ conversation: Message[],
146
+ provider: LLMProvider,
147
+ ): Promise<void> {
148
+ const conversationText = conversation
149
+ .map((m) => `${m.role}: ${m.content}`)
150
+ .join('\n');
151
+
152
+ const prompt =
153
+ SKILL_IMPROVEMENT_PROMPT +
154
+ skillToMarkdown(skill) +
155
+ '\n\nConversation where it was used:\n' +
156
+ conversationText +
157
+ SKILL_IMPROVEMENT_SUFFIX;
158
+
159
+ try {
160
+ const response = await provider.chat(
161
+ [{ id: 'improve', role: 'user', content: prompt, timestamp: Date.now() }],
162
+ 'You are a skill improvement assistant. Respond only with valid JSON.',
163
+ );
164
+
165
+ const json = extractJson(response);
166
+ if (!json || !json.shouldImprove || !json.improvements) return;
167
+
168
+ const { improvements } = json;
169
+ if (improvements.instructions) skill.instructions = improvements.instructions;
170
+ if (improvements.trigger) skill.trigger = improvements.trigger;
171
+ if (improvements.examples) skill.examples = [...skill.examples, ...improvements.examples];
172
+ skill.version++;
173
+
174
+ await this.saveSkill(skill);
175
+ } catch {
176
+ // Silently fail — improvement is best-effort
177
+ }
178
+ }
179
+
180
+ getSkills(): LearnedSkill[] {
181
+ return [...this.skills];
182
+ }
183
+ }
184
+
185
+ // ─── Helpers ────────────────────────────────────────────────
186
+
187
+ function extractJson(text: string): any {
188
+ // Try to extract JSON from response (may be wrapped in markdown code block)
189
+ const match = text.match(/\{[\s\S]*\}/);
190
+ if (!match) return null;
191
+ try {
192
+ return JSON.parse(match[0]);
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ export function skillToMarkdown(skill: LearnedSkill): string {
199
+ const lines = [
200
+ `# Skill: ${skill.name}`,
201
+ '',
202
+ '## Description',
203
+ skill.description,
204
+ '',
205
+ '## Trigger',
206
+ `Pattern: ${skill.trigger}`,
207
+ '',
208
+ '## Instructions',
209
+ skill.instructions,
210
+ '',
211
+ '## Examples',
212
+ ...skill.examples.map((e) => `- "${e}"`),
213
+ '',
214
+ '## Metadata',
215
+ `- Created: ${skill.createdAt.toISOString()}`,
216
+ `- Version: ${skill.version}`,
217
+ `- Usage Count: ${skill.usageCount}`,
218
+ `- Last Used: ${skill.lastUsed?.toISOString() ?? 'never'}`,
219
+ '',
220
+ ];
221
+ return lines.join('\n');
222
+ }
223
+
224
+ export function parseSkillMarkdown(content: string): LearnedSkill | null {
225
+ const nameMatch = content.match(/^# Skill:\s*(.+)$/m);
226
+ if (!nameMatch) return null;
227
+
228
+ const section = (heading: string): string => {
229
+ const re = new RegExp(`## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
230
+ const m = content.match(re);
231
+ return m ? m[1].trim() : '';
232
+ };
233
+
234
+ const description = section('Description');
235
+ const triggerLine = section('Trigger');
236
+ const trigger = triggerLine.replace(/^Pattern:\s*/i, '').trim();
237
+ const instructions = section('Instructions');
238
+
239
+ const examplesRaw = section('Examples');
240
+ const examples = examplesRaw
241
+ .split('\n')
242
+ .map((l) => l.replace(/^-\s*"?|"?\s*$/g, '').trim())
243
+ .filter(Boolean);
244
+
245
+ const metadata = section('Metadata');
246
+ const getMeta = (key: string): string => {
247
+ const m = metadata.match(new RegExp(`- ${key}:\\s*(.+)`, 'i'));
248
+ return m ? m[1].trim() : '';
249
+ };
250
+
251
+ return {
252
+ name: nameMatch[1].trim(),
253
+ description,
254
+ trigger,
255
+ instructions,
256
+ examples,
257
+ createdAt: new Date(getMeta('Created') || Date.now()),
258
+ version: parseInt(getMeta('Version') || '1', 10),
259
+ usageCount: parseInt(getMeta('Usage Count') || '0', 10),
260
+ lastUsed: getMeta('Last Used') !== 'never' ? new Date(getMeta('Last Used')) : undefined,
261
+ };
262
+ }
@@ -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
+ };