cognitive-modules 0.6.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 (78) hide show
  1. package/README.md +165 -0
  2. package/dist/cli.d.ts +16 -0
  3. package/dist/cli.js +335 -0
  4. package/dist/commands/add.d.ts +34 -0
  5. package/dist/commands/add.js +229 -0
  6. package/dist/commands/index.d.ts +11 -0
  7. package/dist/commands/index.js +11 -0
  8. package/dist/commands/init.d.ts +5 -0
  9. package/dist/commands/init.js +78 -0
  10. package/dist/commands/list.d.ts +5 -0
  11. package/dist/commands/list.js +28 -0
  12. package/dist/commands/pipe.d.ts +9 -0
  13. package/dist/commands/pipe.js +59 -0
  14. package/dist/commands/remove.d.ts +10 -0
  15. package/dist/commands/remove.js +47 -0
  16. package/dist/commands/run.d.ts +12 -0
  17. package/dist/commands/run.js +65 -0
  18. package/dist/commands/update.d.ts +14 -0
  19. package/dist/commands/update.js +105 -0
  20. package/dist/commands/versions.d.ts +13 -0
  21. package/dist/commands/versions.js +60 -0
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.js +11 -0
  24. package/dist/modules/index.d.ts +5 -0
  25. package/dist/modules/index.js +5 -0
  26. package/dist/modules/loader.d.ts +12 -0
  27. package/dist/modules/loader.js +197 -0
  28. package/dist/modules/runner.d.ts +12 -0
  29. package/dist/modules/runner.js +229 -0
  30. package/dist/providers/anthropic.d.ts +14 -0
  31. package/dist/providers/anthropic.js +70 -0
  32. package/dist/providers/base.d.ts +11 -0
  33. package/dist/providers/base.js +19 -0
  34. package/dist/providers/deepseek.d.ts +14 -0
  35. package/dist/providers/deepseek.js +66 -0
  36. package/dist/providers/gemini.d.ts +19 -0
  37. package/dist/providers/gemini.js +94 -0
  38. package/dist/providers/index.d.ts +19 -0
  39. package/dist/providers/index.js +74 -0
  40. package/dist/providers/minimax.d.ts +14 -0
  41. package/dist/providers/minimax.js +64 -0
  42. package/dist/providers/moonshot.d.ts +14 -0
  43. package/dist/providers/moonshot.js +65 -0
  44. package/dist/providers/ollama.d.ts +13 -0
  45. package/dist/providers/ollama.js +64 -0
  46. package/dist/providers/openai.d.ts +14 -0
  47. package/dist/providers/openai.js +67 -0
  48. package/dist/providers/qwen.d.ts +14 -0
  49. package/dist/providers/qwen.js +65 -0
  50. package/dist/types.d.ts +136 -0
  51. package/dist/types.js +5 -0
  52. package/package.json +48 -0
  53. package/src/cli.ts +375 -0
  54. package/src/commands/add.ts +315 -0
  55. package/src/commands/index.ts +12 -0
  56. package/src/commands/init.ts +94 -0
  57. package/src/commands/list.ts +33 -0
  58. package/src/commands/pipe.ts +76 -0
  59. package/src/commands/remove.ts +57 -0
  60. package/src/commands/run.ts +80 -0
  61. package/src/commands/update.ts +130 -0
  62. package/src/commands/versions.ts +79 -0
  63. package/src/index.ts +44 -0
  64. package/src/modules/index.ts +6 -0
  65. package/src/modules/loader.ts +219 -0
  66. package/src/modules/runner.ts +278 -0
  67. package/src/providers/anthropic.ts +89 -0
  68. package/src/providers/base.ts +29 -0
  69. package/src/providers/deepseek.ts +83 -0
  70. package/src/providers/gemini.ts +117 -0
  71. package/src/providers/index.ts +78 -0
  72. package/src/providers/minimax.ts +81 -0
  73. package/src/providers/moonshot.ts +82 -0
  74. package/src/providers/ollama.ts +83 -0
  75. package/src/providers/openai.ts +84 -0
  76. package/src/providers/qwen.ts +82 -0
  77. package/src/types.ts +184 -0
  78. package/tsconfig.json +17 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Module Loader - Load and parse Cognitive Modules
3
+ * Supports both v1 (MODULE.md) and v2 (module.yaml + prompt.md) formats
4
+ */
5
+
6
+ import * as fs from 'node:fs/promises';
7
+ import * as path from 'node:path';
8
+ import yaml from 'js-yaml';
9
+ import type { CognitiveModule, ModuleConstraints, ModulePolicies, ToolsPolicy, OutputContract, FailureContract, RuntimeRequirements } from '../types.js';
10
+
11
+ const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n([\s\S]*))?/;
12
+
13
+ /**
14
+ * Detect module format version
15
+ */
16
+ async function detectFormat(modulePath: string): Promise<'v1' | 'v2'> {
17
+ const v2Manifest = path.join(modulePath, 'module.yaml');
18
+ try {
19
+ await fs.access(v2Manifest);
20
+ return 'v2';
21
+ } catch {
22
+ return 'v1';
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Load v2 format module (module.yaml + prompt.md)
28
+ */
29
+ async function loadModuleV2(modulePath: string): Promise<CognitiveModule> {
30
+ const manifestFile = path.join(modulePath, 'module.yaml');
31
+ const promptFile = path.join(modulePath, 'prompt.md');
32
+ const schemaFile = path.join(modulePath, 'schema.json');
33
+
34
+ // Read module.yaml
35
+ const manifestContent = await fs.readFile(manifestFile, 'utf-8');
36
+ const manifest = yaml.load(manifestContent) as Record<string, unknown>;
37
+
38
+ // Read prompt.md
39
+ let prompt = '';
40
+ try {
41
+ prompt = await fs.readFile(promptFile, 'utf-8');
42
+ } catch {
43
+ // prompt.md is optional, manifest may include inline prompt
44
+ }
45
+
46
+ // Read schema.json
47
+ let inputSchema: object | undefined;
48
+ let outputSchema: object | undefined;
49
+ let errorSchema: object | undefined;
50
+
51
+ try {
52
+ const schemaContent = await fs.readFile(schemaFile, 'utf-8');
53
+ const schema = JSON.parse(schemaContent);
54
+ inputSchema = schema.input;
55
+ outputSchema = schema.output;
56
+ errorSchema = schema.error;
57
+ } catch {
58
+ // Schema file is optional but recommended
59
+ }
60
+
61
+ return {
62
+ name: manifest.name as string || path.basename(modulePath),
63
+ version: manifest.version as string || '1.0.0',
64
+ responsibility: manifest.responsibility as string || '',
65
+ excludes: (manifest.excludes as string[]) || [],
66
+ constraints: manifest.constraints as ModuleConstraints | undefined,
67
+ policies: manifest.policies as ModulePolicies | undefined,
68
+ tools: manifest.tools as ToolsPolicy | undefined,
69
+ output: manifest.output as OutputContract | undefined,
70
+ failure: manifest.failure as FailureContract | undefined,
71
+ runtimeRequirements: manifest.runtime_requirements as RuntimeRequirements | undefined,
72
+ context: manifest.context as 'fork' | 'main' | undefined,
73
+ prompt,
74
+ inputSchema,
75
+ outputSchema,
76
+ errorSchema,
77
+ location: modulePath,
78
+ format: 'v2',
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Load v1 format module (MODULE.md with frontmatter)
84
+ */
85
+ async function loadModuleV1(modulePath: string): Promise<CognitiveModule> {
86
+ const moduleFile = path.join(modulePath, 'MODULE.md');
87
+ const schemaFile = path.join(modulePath, 'schema.json');
88
+
89
+ // Read MODULE.md
90
+ const moduleContent = await fs.readFile(moduleFile, 'utf-8');
91
+
92
+ // Parse frontmatter
93
+ const match = moduleContent.match(FRONTMATTER_REGEX);
94
+ if (!match) {
95
+ throw new Error(`Invalid MODULE.md: missing YAML frontmatter in ${moduleFile}`);
96
+ }
97
+
98
+ const frontmatter = yaml.load(match[1]) as Record<string, unknown>;
99
+ const prompt = (match[2] || '').trim();
100
+
101
+ // Read schema.json
102
+ let inputSchema: object | undefined;
103
+ let outputSchema: object | undefined;
104
+
105
+ try {
106
+ const schemaContent = await fs.readFile(schemaFile, 'utf-8');
107
+ const schema = JSON.parse(schemaContent);
108
+ inputSchema = schema.input;
109
+ outputSchema = schema.output;
110
+ } catch {
111
+ // Schema file is optional
112
+ }
113
+
114
+ // Extract constraints from v1 format
115
+ const constraints: ModuleConstraints = {};
116
+ const v1Constraints = frontmatter.constraints as Record<string, boolean> | undefined;
117
+ if (v1Constraints) {
118
+ constraints.no_network = v1Constraints.no_network;
119
+ constraints.no_side_effects = v1Constraints.no_side_effects;
120
+ constraints.no_inventing_data = v1Constraints.no_inventing_data;
121
+ }
122
+
123
+ return {
124
+ name: frontmatter.name as string || path.basename(modulePath),
125
+ version: frontmatter.version as string || '1.0.0',
126
+ responsibility: frontmatter.responsibility as string || '',
127
+ excludes: (frontmatter.excludes as string[]) || [],
128
+ constraints: Object.keys(constraints).length > 0 ? constraints : undefined,
129
+ context: frontmatter.context as 'fork' | 'main' | undefined,
130
+ prompt,
131
+ inputSchema,
132
+ outputSchema,
133
+ location: modulePath,
134
+ format: 'v1',
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Load a Cognitive Module (auto-detects format)
140
+ */
141
+ export async function loadModule(modulePath: string): Promise<CognitiveModule> {
142
+ const format = await detectFormat(modulePath);
143
+
144
+ if (format === 'v2') {
145
+ return loadModuleV2(modulePath);
146
+ } else {
147
+ return loadModuleV1(modulePath);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Check if a directory contains a valid module
153
+ */
154
+ async function isValidModule(modulePath: string): Promise<boolean> {
155
+ const v2Manifest = path.join(modulePath, 'module.yaml');
156
+ const v1Module = path.join(modulePath, 'MODULE.md');
157
+
158
+ try {
159
+ await fs.access(v2Manifest);
160
+ return true;
161
+ } catch {
162
+ try {
163
+ await fs.access(v1Module);
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+ }
170
+
171
+ export async function findModule(name: string, searchPaths: string[]): Promise<CognitiveModule | null> {
172
+ for (const basePath of searchPaths) {
173
+ const modulePath = path.join(basePath, name);
174
+
175
+ if (await isValidModule(modulePath)) {
176
+ return await loadModule(modulePath);
177
+ }
178
+ }
179
+
180
+ return null;
181
+ }
182
+
183
+ export async function listModules(searchPaths: string[]): Promise<CognitiveModule[]> {
184
+ const modules: CognitiveModule[] = [];
185
+
186
+ for (const basePath of searchPaths) {
187
+ try {
188
+ const entries = await fs.readdir(basePath, { withFileTypes: true });
189
+
190
+ for (const entry of entries) {
191
+ if (entry.isDirectory()) {
192
+ const modulePath = path.join(basePath, entry.name);
193
+
194
+ if (await isValidModule(modulePath)) {
195
+ try {
196
+ const module = await loadModule(modulePath);
197
+ modules.push(module);
198
+ } catch {
199
+ // Skip invalid modules
200
+ }
201
+ }
202
+ }
203
+ }
204
+ } catch {
205
+ // Path doesn't exist, skip
206
+ }
207
+ }
208
+
209
+ return modules;
210
+ }
211
+
212
+ export function getDefaultSearchPaths(cwd: string): string[] {
213
+ const home = process.env.HOME || '';
214
+ return [
215
+ path.join(cwd, 'cognitive', 'modules'),
216
+ path.join(cwd, '.cognitive', 'modules'),
217
+ path.join(home, '.cognitive', 'modules'),
218
+ ];
219
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Module Runner - Execute Cognitive Modules
3
+ * v2.1: Envelope format support, clean input mapping
4
+ */
5
+
6
+ import type {
7
+ Provider,
8
+ CognitiveModule,
9
+ ModuleResult,
10
+ Message,
11
+ ModuleInput,
12
+ EnvelopeResponse,
13
+ ModuleResultData
14
+ } from '../types.js';
15
+
16
+ export interface RunOptions {
17
+ // Clean input (v2 style)
18
+ input?: ModuleInput;
19
+
20
+ // Legacy CLI args (v1 compatibility) - mapped to input.code or input.query
21
+ args?: string;
22
+
23
+ // Runtime options
24
+ verbose?: boolean;
25
+
26
+ // Force envelope format (default: auto-detect from module.output.envelope)
27
+ useEnvelope?: boolean;
28
+ }
29
+
30
+ export async function runModule(
31
+ module: CognitiveModule,
32
+ provider: Provider,
33
+ options: RunOptions = {}
34
+ ): Promise<ModuleResult> {
35
+ const { args, input, verbose = false, useEnvelope } = options;
36
+
37
+ // Determine if we should use envelope format
38
+ const shouldUseEnvelope = useEnvelope ?? (module.output?.envelope === true || module.format === 'v2');
39
+
40
+ // Build clean input data (v2 style: no $ARGUMENTS pollution)
41
+ const inputData: ModuleInput = input || {};
42
+
43
+ // Map legacy --args to clean input
44
+ if (args && !inputData.code && !inputData.query) {
45
+ // Determine if args looks like code or natural language
46
+ if (looksLikeCode(args)) {
47
+ inputData.code = args;
48
+ } else {
49
+ inputData.query = args;
50
+ }
51
+ }
52
+
53
+ // Build prompt with clean substitution
54
+ const prompt = buildPrompt(module, inputData);
55
+
56
+ if (verbose) {
57
+ console.error('--- Module ---');
58
+ console.error(`Name: ${module.name} (${module.format})`);
59
+ console.error(`Responsibility: ${module.responsibility}`);
60
+ console.error(`Envelope: ${shouldUseEnvelope}`);
61
+ console.error('--- Input ---');
62
+ console.error(JSON.stringify(inputData, null, 2));
63
+ console.error('--- Prompt ---');
64
+ console.error(prompt);
65
+ console.error('--- End ---');
66
+ }
67
+
68
+ // Build system message based on module config
69
+ const systemParts: string[] = [
70
+ `You are executing the "${module.name}" Cognitive Module.`,
71
+ '',
72
+ `RESPONSIBILITY: ${module.responsibility}`,
73
+ ];
74
+
75
+ if (module.excludes.length > 0) {
76
+ systemParts.push('', 'YOU MUST NOT:');
77
+ module.excludes.forEach(e => systemParts.push(`- ${e}`));
78
+ }
79
+
80
+ if (module.constraints) {
81
+ systemParts.push('', 'CONSTRAINTS:');
82
+ if (module.constraints.no_network) systemParts.push('- No network access');
83
+ if (module.constraints.no_side_effects) systemParts.push('- No side effects');
84
+ if (module.constraints.no_file_write) systemParts.push('- No file writes');
85
+ if (module.constraints.no_inventing_data) systemParts.push('- Do not invent data');
86
+ }
87
+
88
+ if (module.output?.require_behavior_equivalence) {
89
+ systemParts.push('', 'BEHAVIOR EQUIVALENCE:');
90
+ systemParts.push('- You MUST set behavior_equivalence=true ONLY if the output is functionally identical');
91
+ systemParts.push('- If unsure, set behavior_equivalence=false and explain in rationale');
92
+
93
+ const maxConfidence = module.constraints?.behavior_equivalence_false_max_confidence ?? 0.7;
94
+ systemParts.push(`- If behavior_equivalence=false, confidence MUST be <= ${maxConfidence}`);
95
+ }
96
+
97
+ // Add envelope format instructions
98
+ if (shouldUseEnvelope) {
99
+ systemParts.push('', 'RESPONSE FORMAT (Envelope):');
100
+ systemParts.push('- Wrap your response in the envelope format');
101
+ systemParts.push('- Success: { "ok": true, "data": { ...your output... } }');
102
+ systemParts.push('- Error: { "ok": false, "error": { "code": "ERROR_CODE", "message": "..." } }');
103
+ systemParts.push('- Include "confidence" (0-1) and "rationale" in data');
104
+ if (module.output?.require_behavior_equivalence) {
105
+ systemParts.push('- Include "behavior_equivalence" (boolean) in data');
106
+ }
107
+ } else {
108
+ systemParts.push('', 'OUTPUT FORMAT:');
109
+ systemParts.push('- Respond with ONLY valid JSON');
110
+ systemParts.push('- Include "confidence" (0-1) and "rationale" fields');
111
+ if (module.output?.require_behavior_equivalence) {
112
+ systemParts.push('- Include "behavior_equivalence" (boolean) field');
113
+ }
114
+ }
115
+
116
+ const messages: Message[] = [
117
+ { role: 'system', content: systemParts.join('\n') },
118
+ { role: 'user', content: prompt },
119
+ ];
120
+
121
+ // Invoke provider
122
+ const result = await provider.invoke({
123
+ messages,
124
+ jsonSchema: module.outputSchema,
125
+ temperature: 0.3,
126
+ });
127
+
128
+ if (verbose) {
129
+ console.error('--- Response ---');
130
+ console.error(result.content);
131
+ console.error('--- End Response ---');
132
+ }
133
+
134
+ // Parse response
135
+ let parsed: unknown;
136
+ try {
137
+ const jsonMatch = result.content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
138
+ const jsonStr = jsonMatch ? jsonMatch[1] : result.content;
139
+ parsed = JSON.parse(jsonStr.trim());
140
+ } catch {
141
+ throw new Error(`Failed to parse JSON response: ${result.content.substring(0, 500)}`);
142
+ }
143
+
144
+ // Handle envelope format
145
+ if (shouldUseEnvelope && isEnvelopeResponse(parsed)) {
146
+ return parseEnvelopeResponse(parsed, result.content);
147
+ }
148
+
149
+ // Handle legacy format (non-envelope)
150
+ return parseLegacyResponse(parsed, result.content);
151
+ }
152
+
153
+ /**
154
+ * Check if response is in envelope format
155
+ */
156
+ function isEnvelopeResponse(obj: unknown): obj is EnvelopeResponse {
157
+ if (typeof obj !== 'object' || obj === null) return false;
158
+ const o = obj as Record<string, unknown>;
159
+ return typeof o.ok === 'boolean';
160
+ }
161
+
162
+ /**
163
+ * Parse envelope format response
164
+ */
165
+ function parseEnvelopeResponse(response: EnvelopeResponse, raw: string): ModuleResult {
166
+ if (response.ok) {
167
+ const data = response.data as ModuleResultData;
168
+ return {
169
+ ok: true,
170
+ data: {
171
+ ...data,
172
+ confidence: typeof data.confidence === 'number' ? data.confidence : 0.5,
173
+ rationale: typeof data.rationale === 'string' ? data.rationale : '',
174
+ behavior_equivalence: data.behavior_equivalence,
175
+ },
176
+ raw,
177
+ };
178
+ } else {
179
+ return {
180
+ ok: false,
181
+ error: response.error,
182
+ partial_data: response.partial_data,
183
+ raw,
184
+ };
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Parse legacy (non-envelope) format response
190
+ */
191
+ function parseLegacyResponse(output: unknown, raw: string): ModuleResult {
192
+ const outputObj = output as Record<string, unknown>;
193
+ const confidence = typeof outputObj.confidence === 'number' ? outputObj.confidence : 0.5;
194
+ const rationale = typeof outputObj.rationale === 'string' ? outputObj.rationale : '';
195
+ const behaviorEquivalence = typeof outputObj.behavior_equivalence === 'boolean'
196
+ ? outputObj.behavior_equivalence
197
+ : undefined;
198
+
199
+ // Check if this is an error response (has error.code)
200
+ if (outputObj.error && typeof outputObj.error === 'object') {
201
+ const errorObj = outputObj.error as Record<string, unknown>;
202
+ if (typeof errorObj.code === 'string') {
203
+ return {
204
+ ok: false,
205
+ error: {
206
+ code: errorObj.code,
207
+ message: typeof errorObj.message === 'string' ? errorObj.message : 'Unknown error',
208
+ },
209
+ raw,
210
+ };
211
+ }
212
+ }
213
+
214
+ return {
215
+ ok: true,
216
+ data: {
217
+ ...outputObj,
218
+ confidence,
219
+ rationale,
220
+ behavior_equivalence: behaviorEquivalence,
221
+ } as ModuleResultData,
222
+ raw,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Build prompt with clean variable substitution
228
+ */
229
+ function buildPrompt(module: CognitiveModule, input: ModuleInput): string {
230
+ let prompt = module.prompt;
231
+
232
+ // v2 style: substitute ${variable} placeholders
233
+ for (const [key, value] of Object.entries(input)) {
234
+ const strValue = typeof value === 'string' ? value : JSON.stringify(value);
235
+ prompt = prompt.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), strValue);
236
+ }
237
+
238
+ // v1 compatibility: substitute $ARGUMENTS
239
+ const argsValue = input.code || input.query || '';
240
+ prompt = prompt.replace(/\$ARGUMENTS/g, argsValue);
241
+
242
+ // Substitute $N placeholders (v1 compatibility)
243
+ if (typeof argsValue === 'string') {
244
+ const argsList = argsValue.split(/\s+/);
245
+ argsList.forEach((arg, i) => {
246
+ prompt = prompt.replace(new RegExp(`\\$${i}\\b`, 'g'), arg);
247
+ });
248
+ }
249
+
250
+ // Append input summary if not already in prompt
251
+ if (!prompt.includes(argsValue) && argsValue) {
252
+ prompt += '\n\n## Input\n\n';
253
+ if (input.code) {
254
+ prompt += '```\n' + input.code + '\n```\n';
255
+ }
256
+ if (input.query) {
257
+ prompt += input.query + '\n';
258
+ }
259
+ if (input.language) {
260
+ prompt += `\nLanguage: ${input.language}\n`;
261
+ }
262
+ }
263
+
264
+ return prompt;
265
+ }
266
+
267
+ /**
268
+ * Heuristic to detect if input looks like code
269
+ */
270
+ function looksLikeCode(str: string): boolean {
271
+ const codeIndicators = [
272
+ /^(def|function|class|const|let|var|import|export|public|private)\s/,
273
+ /[{};()]/,
274
+ /=>/,
275
+ /\.(py|js|ts|go|rs|java|cpp|c|rb)$/,
276
+ ];
277
+ return codeIndicators.some(re => re.test(str));
278
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Anthropic Provider - Claude API
3
+ */
4
+
5
+ import { BaseProvider } from './base.js';
6
+ import type { InvokeParams, InvokeResult } from '../types.js';
7
+
8
+ export class AnthropicProvider extends BaseProvider {
9
+ name = 'anthropic';
10
+ private apiKey: string;
11
+ private model: string;
12
+ private baseUrl = 'https://api.anthropic.com/v1';
13
+
14
+ constructor(apiKey?: string, model = 'claude-sonnet-4-5-20250929') {
15
+ super();
16
+ this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY || '';
17
+ this.model = model;
18
+ }
19
+
20
+ isConfigured(): boolean {
21
+ return !!this.apiKey;
22
+ }
23
+
24
+ async invoke(params: InvokeParams): Promise<InvokeResult> {
25
+ if (!this.isConfigured()) {
26
+ throw new Error('Anthropic API key not configured. Set ANTHROPIC_API_KEY environment variable.');
27
+ }
28
+
29
+ const url = `${this.baseUrl}/messages`;
30
+
31
+ // Extract system message
32
+ const systemMessage = params.messages.find(m => m.role === 'system');
33
+ const otherMessages = params.messages.filter(m => m.role !== 'system');
34
+
35
+ // Add JSON schema instruction if provided
36
+ let messages = otherMessages;
37
+ if (params.jsonSchema) {
38
+ const lastUserIdx = messages.findLastIndex(m => m.role === 'user');
39
+ if (lastUserIdx >= 0) {
40
+ messages = [...messages];
41
+ messages[lastUserIdx] = {
42
+ ...messages[lastUserIdx],
43
+ content: messages[lastUserIdx].content + this.buildJsonPrompt(params.jsonSchema),
44
+ };
45
+ }
46
+ }
47
+
48
+ const body: Record<string, unknown> = {
49
+ model: this.model,
50
+ messages: messages.map(m => ({ role: m.role, content: m.content })),
51
+ max_tokens: params.maxTokens ?? 4096,
52
+ };
53
+
54
+ if (systemMessage) {
55
+ body.system = systemMessage.content;
56
+ }
57
+
58
+ const response = await fetch(url, {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ 'x-api-key': this.apiKey,
63
+ 'anthropic-version': '2023-06-01',
64
+ },
65
+ body: JSON.stringify(body),
66
+ });
67
+
68
+ if (!response.ok) {
69
+ const error = await response.text();
70
+ throw new Error(`Anthropic API error: ${response.status} - ${error}`);
71
+ }
72
+
73
+ const data = await response.json() as {
74
+ content?: Array<{ text?: string }>;
75
+ usage?: { input_tokens?: number; output_tokens?: number };
76
+ };
77
+
78
+ const content = data.content?.[0]?.text || '';
79
+
80
+ return {
81
+ content,
82
+ usage: data.usage ? {
83
+ promptTokens: data.usage.input_tokens || 0,
84
+ completionTokens: data.usage.output_tokens || 0,
85
+ totalTokens: (data.usage.input_tokens || 0) + (data.usage.output_tokens || 0),
86
+ } : undefined,
87
+ };
88
+ }
89
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Base Provider - Abstract class for all LLM providers
3
+ */
4
+
5
+ import type { Provider, InvokeParams, InvokeResult } from '../types.js';
6
+
7
+ export abstract class BaseProvider implements Provider {
8
+ abstract name: string;
9
+
10
+ abstract invoke(params: InvokeParams): Promise<InvokeResult>;
11
+
12
+ abstract isConfigured(): boolean;
13
+
14
+ protected buildJsonPrompt(schema: object): string {
15
+ return `\n\nYou MUST respond with valid JSON matching this schema:\n${JSON.stringify(schema, null, 2)}\n\nRespond with ONLY the JSON, no markdown code blocks.`;
16
+ }
17
+
18
+ protected parseJsonResponse(content: string): unknown {
19
+ // Try to extract JSON from markdown code blocks
20
+ const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
21
+ const jsonStr = jsonMatch ? jsonMatch[1] : content;
22
+
23
+ try {
24
+ return JSON.parse(jsonStr.trim());
25
+ } catch {
26
+ throw new Error(`Failed to parse JSON response: ${content.substring(0, 200)}`);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * DeepSeek Provider - DeepSeek API
3
+ */
4
+
5
+ import { BaseProvider } from './base.js';
6
+ import type { InvokeParams, InvokeResult } from '../types.js';
7
+
8
+ export class DeepSeekProvider extends BaseProvider {
9
+ name = 'deepseek';
10
+ private apiKey: string;
11
+ private model: string;
12
+ private baseUrl = 'https://api.deepseek.com/v1';
13
+
14
+ constructor(apiKey?: string, model = 'deepseek-chat') {
15
+ // deepseek-chat 自动映射到最新的 DeepSeek-V3.2
16
+ super();
17
+ this.apiKey = apiKey || process.env.DEEPSEEK_API_KEY || '';
18
+ this.model = model;
19
+ }
20
+
21
+ isConfigured(): boolean {
22
+ return !!this.apiKey;
23
+ }
24
+
25
+ async invoke(params: InvokeParams): Promise<InvokeResult> {
26
+ if (!this.isConfigured()) {
27
+ throw new Error('DeepSeek API key not configured. Set DEEPSEEK_API_KEY environment variable.');
28
+ }
29
+
30
+ const url = `${this.baseUrl}/chat/completions`;
31
+
32
+ const body: Record<string, unknown> = {
33
+ model: this.model,
34
+ messages: params.messages.map(m => ({ role: m.role, content: m.content })),
35
+ temperature: params.temperature ?? 0.7,
36
+ max_tokens: params.maxTokens ?? 4096,
37
+ };
38
+
39
+ // Add JSON mode if schema provided
40
+ if (params.jsonSchema) {
41
+ body.response_format = { type: 'json_object' };
42
+ const lastUserIdx = params.messages.findLastIndex(m => m.role === 'user');
43
+ if (lastUserIdx >= 0) {
44
+ const messages = [...params.messages];
45
+ messages[lastUserIdx] = {
46
+ ...messages[lastUserIdx],
47
+ content: messages[lastUserIdx].content + this.buildJsonPrompt(params.jsonSchema),
48
+ };
49
+ body.messages = messages.map(m => ({ role: m.role, content: m.content }));
50
+ }
51
+ }
52
+
53
+ const response = await fetch(url, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'Authorization': `Bearer ${this.apiKey}`,
58
+ },
59
+ body: JSON.stringify(body),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const error = await response.text();
64
+ throw new Error(`DeepSeek API error: ${response.status} - ${error}`);
65
+ }
66
+
67
+ const data = await response.json() as {
68
+ choices?: Array<{ message?: { content?: string } }>;
69
+ usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number };
70
+ };
71
+
72
+ const content = data.choices?.[0]?.message?.content || '';
73
+
74
+ return {
75
+ content,
76
+ usage: data.usage ? {
77
+ promptTokens: data.usage.prompt_tokens || 0,
78
+ completionTokens: data.usage.completion_tokens || 0,
79
+ totalTokens: data.usage.total_tokens || 0,
80
+ } : undefined,
81
+ };
82
+ }
83
+ }