@ryanfw/prompt-orchestration-pipeline 0.16.3 → 0.16.4
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/package.json +1 -1
- package/src/config/models.js +28 -1
- package/src/llm/index.js +59 -1
- package/src/providers/claude-code.js +156 -0
- package/src/ui/endpoints/task-save-endpoint.js +47 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryanfw/prompt-orchestration-pipeline",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
4
4
|
"description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/ui/server.js",
|
package/src/config/models.js
CHANGED
|
@@ -41,6 +41,11 @@ export const ModelAlias = Object.freeze({
|
|
|
41
41
|
ANTHROPIC_SONNET_4_5: "anthropic:sonnet-4-5",
|
|
42
42
|
ANTHROPIC_HAIKU_4_5: "anthropic:haiku-4-5",
|
|
43
43
|
ANTHROPIC_OPUS_4_1: "anthropic:opus-4-1", // Legacy, still available
|
|
44
|
+
|
|
45
|
+
// Claude Code (subscription-based, uses CLI)
|
|
46
|
+
CLAUDE_CODE_SONNET: "claudecode:sonnet",
|
|
47
|
+
CLAUDE_CODE_OPUS: "claudecode:opus",
|
|
48
|
+
CLAUDE_CODE_HAIKU: "claudecode:haiku",
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
// Consolidated model configuration with pricing metadata
|
|
@@ -199,6 +204,27 @@ export const MODEL_CONFIG = Object.freeze({
|
|
|
199
204
|
tokenCostInPerMillion: 15.0,
|
|
200
205
|
tokenCostOutPerMillion: 75.0,
|
|
201
206
|
},
|
|
207
|
+
|
|
208
|
+
// ─── Claude Code (Subscription) ───
|
|
209
|
+
// Uses existing Claude subscription via CLI, costs show $0.00
|
|
210
|
+
[ModelAlias.CLAUDE_CODE_SONNET]: {
|
|
211
|
+
provider: "claudecode",
|
|
212
|
+
model: "sonnet",
|
|
213
|
+
tokenCostInPerMillion: 0,
|
|
214
|
+
tokenCostOutPerMillion: 0,
|
|
215
|
+
},
|
|
216
|
+
[ModelAlias.CLAUDE_CODE_OPUS]: {
|
|
217
|
+
provider: "claudecode",
|
|
218
|
+
model: "opus",
|
|
219
|
+
tokenCostInPerMillion: 0,
|
|
220
|
+
tokenCostOutPerMillion: 0,
|
|
221
|
+
},
|
|
222
|
+
[ModelAlias.CLAUDE_CODE_HAIKU]: {
|
|
223
|
+
provider: "claudecode",
|
|
224
|
+
model: "haiku",
|
|
225
|
+
tokenCostInPerMillion: 0,
|
|
226
|
+
tokenCostOutPerMillion: 0,
|
|
227
|
+
},
|
|
202
228
|
});
|
|
203
229
|
|
|
204
230
|
// Validation set of all valid model aliases
|
|
@@ -211,6 +237,7 @@ export const DEFAULT_MODEL_BY_PROVIDER = Object.freeze({
|
|
|
211
237
|
gemini: ModelAlias.GEMINI_3_FLASH, // Updated: Gemini 3 Flash is new default
|
|
212
238
|
zhipu: ModelAlias.ZAI_GLM_4_6,
|
|
213
239
|
anthropic: ModelAlias.ANTHROPIC_OPUS_4_5, // Updated: Opus 4.5 available at better price
|
|
240
|
+
claudecode: ModelAlias.CLAUDE_CODE_SONNET,
|
|
214
241
|
});
|
|
215
242
|
|
|
216
243
|
/**
|
|
@@ -351,4 +378,4 @@ if (
|
|
|
351
378
|
throw new Error(
|
|
352
379
|
"VALID_MODEL_ALIASES does not exactly match MODEL_CONFIG keys"
|
|
353
380
|
);
|
|
354
|
-
}
|
|
381
|
+
}
|
package/src/llm/index.js
CHANGED
|
@@ -3,6 +3,10 @@ import { deepseekChat } from "../providers/deepseek.js";
|
|
|
3
3
|
import { anthropicChat } from "../providers/anthropic.js";
|
|
4
4
|
import { geminiChat } from "../providers/gemini.js";
|
|
5
5
|
import { zhipuChat } from "../providers/zhipu.js";
|
|
6
|
+
import {
|
|
7
|
+
claudeCodeChat,
|
|
8
|
+
isClaudeCodeAvailable,
|
|
9
|
+
} from "../providers/claude-code.js";
|
|
6
10
|
import { EventEmitter } from "node:events";
|
|
7
11
|
import { getConfig } from "../core/config.js";
|
|
8
12
|
import {
|
|
@@ -57,6 +61,7 @@ export function getAvailableProviders() {
|
|
|
57
61
|
anthropic: !!process.env.ANTHROPIC_API_KEY,
|
|
58
62
|
gemini: !!process.env.GEMINI_API_KEY,
|
|
59
63
|
zhipu: !!process.env.ZHIPU_API_KEY,
|
|
64
|
+
claudecode: isClaudeCodeAvailable(),
|
|
60
65
|
mock: !!mockProviderInstance,
|
|
61
66
|
};
|
|
62
67
|
}
|
|
@@ -524,6 +529,59 @@ export async function chat(options) {
|
|
|
524
529
|
totalTokens: promptTokens + completionTokens,
|
|
525
530
|
};
|
|
526
531
|
}
|
|
532
|
+
} else if (provider === "claudecode") {
|
|
533
|
+
logger.log("Using Claude Code provider");
|
|
534
|
+
const defaultAlias = DEFAULT_MODEL_BY_PROVIDER["claudecode"];
|
|
535
|
+
const defaultModelConfig = MODEL_CONFIG[defaultAlias];
|
|
536
|
+
const defaultModel = defaultModelConfig?.model;
|
|
537
|
+
|
|
538
|
+
const claudeCodeArgs = {
|
|
539
|
+
messages,
|
|
540
|
+
model: model || defaultModel,
|
|
541
|
+
maxTokens,
|
|
542
|
+
...rest,
|
|
543
|
+
};
|
|
544
|
+
logger.log("Claude Code call parameters:", {
|
|
545
|
+
model: claudeCodeArgs.model,
|
|
546
|
+
hasMessages: !!claudeCodeArgs.messages,
|
|
547
|
+
messageCount: claudeCodeArgs.messages?.length,
|
|
548
|
+
});
|
|
549
|
+
if (responseFormat !== undefined) {
|
|
550
|
+
claudeCodeArgs.responseFormat = responseFormat;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
logger.log("Calling claudeCodeChat()...");
|
|
554
|
+
const result = await claudeCodeChat(claudeCodeArgs);
|
|
555
|
+
logger.log("claudeCodeChat() returned:", {
|
|
556
|
+
hasResult: !!result,
|
|
557
|
+
hasContent: !!result?.content,
|
|
558
|
+
hasUsage: !!result?.usage,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
response = {
|
|
562
|
+
content: result.content,
|
|
563
|
+
raw: result.raw,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// Claude Code returns $0 for subscription users
|
|
567
|
+
if (result?.usage) {
|
|
568
|
+
const { prompt_tokens, completion_tokens, total_tokens } = result.usage;
|
|
569
|
+
usage = {
|
|
570
|
+
promptTokens: prompt_tokens,
|
|
571
|
+
completionTokens: completion_tokens,
|
|
572
|
+
totalTokens: total_tokens,
|
|
573
|
+
};
|
|
574
|
+
} else {
|
|
575
|
+
const promptTokens = estimateTokens(systemMsg + userMsg);
|
|
576
|
+
const completionTokens = estimateTokens(
|
|
577
|
+
typeof result === "string" ? result : JSON.stringify(result)
|
|
578
|
+
);
|
|
579
|
+
usage = {
|
|
580
|
+
promptTokens,
|
|
581
|
+
completionTokens,
|
|
582
|
+
totalTokens: promptTokens + completionTokens,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
527
585
|
} else {
|
|
528
586
|
logger.error("Unknown provider:", provider);
|
|
529
587
|
throw new Error(`Provider ${provider} not yet implemented`);
|
|
@@ -804,4 +862,4 @@ export function createHighLevelLLM(options = {}) {
|
|
|
804
862
|
// Include provider-grouped functions for backward compatibility
|
|
805
863
|
...providerFunctions,
|
|
806
864
|
};
|
|
807
|
-
}
|
|
865
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
|
+
import {
|
|
3
|
+
extractMessages,
|
|
4
|
+
isRetryableError,
|
|
5
|
+
sleep,
|
|
6
|
+
stripMarkdownFences,
|
|
7
|
+
tryParseJSON,
|
|
8
|
+
ensureJsonResponseFormat,
|
|
9
|
+
ProviderJsonParseError,
|
|
10
|
+
} from "./base.js";
|
|
11
|
+
import { createLogger } from "../core/logger.js";
|
|
12
|
+
|
|
13
|
+
const logger = createLogger("ClaudeCode");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if Claude Code CLI is available
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function isClaudeCodeAvailable() {
|
|
20
|
+
try {
|
|
21
|
+
const result = spawnSync("claude", ["--version"], {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
timeout: 5000,
|
|
24
|
+
});
|
|
25
|
+
return result.status === 0;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Chat with Claude via the Claude Code CLI
|
|
33
|
+
* @param {Object} options
|
|
34
|
+
* @param {Array} options.messages - Array of message objects with role and content
|
|
35
|
+
* @param {string} [options.model="sonnet"] - Model name: sonnet, opus, or haiku
|
|
36
|
+
* @param {number} [options.maxTokens] - Maximum tokens in response
|
|
37
|
+
* @param {number} [options.maxTurns=1] - Maximum conversation turns
|
|
38
|
+
* @param {string} [options.responseFormat="json"] - Response format
|
|
39
|
+
* @param {number} [options.maxRetries=3] - Maximum retry attempts
|
|
40
|
+
* @returns {Promise<{content: any, text: string, usage: Object, raw: any}>}
|
|
41
|
+
*/
|
|
42
|
+
export async function claudeCodeChat({
|
|
43
|
+
messages,
|
|
44
|
+
model = "sonnet",
|
|
45
|
+
maxTokens,
|
|
46
|
+
maxTurns = 1,
|
|
47
|
+
responseFormat = "json",
|
|
48
|
+
maxRetries = 3,
|
|
49
|
+
}) {
|
|
50
|
+
ensureJsonResponseFormat(responseFormat, "ClaudeCode");
|
|
51
|
+
|
|
52
|
+
const { systemMsg, userMsg } = extractMessages(messages);
|
|
53
|
+
|
|
54
|
+
const args = [
|
|
55
|
+
"-p",
|
|
56
|
+
userMsg,
|
|
57
|
+
"--output-format",
|
|
58
|
+
"json",
|
|
59
|
+
"--model",
|
|
60
|
+
model,
|
|
61
|
+
"--max-turns",
|
|
62
|
+
String(maxTurns),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (systemMsg) {
|
|
66
|
+
args.push("--system-prompt", systemMsg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (maxTokens) {
|
|
70
|
+
args.push("--max-tokens", String(maxTokens));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.log("Spawning claude CLI", { model, argsCount: args.length });
|
|
74
|
+
|
|
75
|
+
let lastError;
|
|
76
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
77
|
+
try {
|
|
78
|
+
const result = await spawnClaude(args);
|
|
79
|
+
return parseClaudeResponse(result, model);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
lastError = err;
|
|
82
|
+
if (attempt < maxRetries && isRetryableError(err)) {
|
|
83
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
84
|
+
logger.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
85
|
+
await sleep(delay);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw lastError;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Spawn the claude CLI and collect output
|
|
96
|
+
* @param {string[]} args - CLI arguments
|
|
97
|
+
* @returns {Promise<string>} - stdout content
|
|
98
|
+
*/
|
|
99
|
+
function spawnClaude(args) {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const proc = spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
102
|
+
|
|
103
|
+
let stdout = "";
|
|
104
|
+
let stderr = "";
|
|
105
|
+
|
|
106
|
+
proc.stdout.on("data", (data) => {
|
|
107
|
+
stdout += data.toString();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
proc.stderr.on("data", (data) => {
|
|
111
|
+
stderr += data.toString();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
proc.on("error", (err) => {
|
|
115
|
+
reject(new Error(`Failed to spawn claude CLI: ${err.message}`));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
proc.on("close", (code) => {
|
|
119
|
+
if (code === 0) {
|
|
120
|
+
resolve(stdout);
|
|
121
|
+
} else {
|
|
122
|
+
reject(new Error(`claude CLI exited with code ${code}: ${stderr}`));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse the JSON response from Claude CLI
|
|
130
|
+
* @param {string} stdout - Raw stdout from CLI
|
|
131
|
+
* @param {string} model - Model name for error reporting
|
|
132
|
+
* @returns {{content: any, text: string, usage: Object, raw: any}}
|
|
133
|
+
*/
|
|
134
|
+
function parseClaudeResponse(stdout, model) {
|
|
135
|
+
const jsonResponse = tryParseJSON(stdout);
|
|
136
|
+
if (!jsonResponse) {
|
|
137
|
+
throw new ProviderJsonParseError(
|
|
138
|
+
"claudecode",
|
|
139
|
+
model,
|
|
140
|
+
stdout.slice(0, 200),
|
|
141
|
+
"Failed to parse Claude CLI JSON response"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Extract text content from response
|
|
146
|
+
const rawText = jsonResponse.result ?? jsonResponse.text ?? "";
|
|
147
|
+
const cleanedText = stripMarkdownFences(rawText);
|
|
148
|
+
const parsed = tryParseJSON(cleanedText) ?? cleanedText;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
content: parsed,
|
|
152
|
+
text: rawText,
|
|
153
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
154
|
+
raw: jsonResponse,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -76,27 +76,62 @@ export async function handleTaskSave(req, res) {
|
|
|
76
76
|
const indexPath = taskRegistryPath;
|
|
77
77
|
let indexContent = await fs.readFile(indexPath, "utf8");
|
|
78
78
|
|
|
79
|
-
// Check if task name already exists in the index
|
|
80
|
-
const taskNamePattern = new RegExp(`^\\s
|
|
79
|
+
// Check if task name already exists in the index (handles both quoted and unquoted)
|
|
80
|
+
const taskNamePattern = new RegExp(`^\\s*"?${taskName}"?\\s*:`, "m");
|
|
81
81
|
if (taskNamePattern.test(indexContent)) {
|
|
82
82
|
return sendJson(res, 400, {
|
|
83
83
|
error: `Task "${taskName}" already exists in the registry`,
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
// Detect module format: ESM (export default {) or CommonJS (module.exports = {)
|
|
88
|
+
const esmPattern = /export\s+default\s+\{/;
|
|
89
|
+
const cjsPattern = /module\.exports\s*=\s*\{/;
|
|
90
|
+
const cjsTasksPattern = /module\.exports\s*=\s*\{\s*tasks\s*:\s*\{/;
|
|
91
|
+
|
|
92
|
+
let insertPosition;
|
|
93
|
+
let exportMatch;
|
|
94
|
+
let isNestedCjs = false;
|
|
95
|
+
|
|
96
|
+
if (esmPattern.test(indexContent)) {
|
|
97
|
+
// ESM format: export default { ... }
|
|
98
|
+
exportMatch = indexContent.match(esmPattern);
|
|
99
|
+
insertPosition = indexContent.indexOf("\n", exportMatch.index) + 1;
|
|
100
|
+
} else if (cjsTasksPattern.test(indexContent)) {
|
|
101
|
+
// CommonJS with nested tasks: module.exports = { tasks: { ... } }
|
|
102
|
+
exportMatch = indexContent.match(cjsTasksPattern);
|
|
103
|
+
insertPosition = exportMatch.index + exportMatch[0].length;
|
|
104
|
+
isNestedCjs = true;
|
|
105
|
+
} else if (cjsPattern.test(indexContent)) {
|
|
106
|
+
// CommonJS flat: module.exports = { ... }
|
|
107
|
+
exportMatch = indexContent.match(cjsPattern);
|
|
108
|
+
insertPosition = indexContent.indexOf("\n", exportMatch.index) + 1;
|
|
109
|
+
} else {
|
|
92
110
|
return sendJson(res, 500, {
|
|
93
|
-
error:
|
|
111
|
+
error:
|
|
112
|
+
"Failed to find export pattern in index.js (expected 'export default {' or 'module.exports = {')",
|
|
94
113
|
});
|
|
95
114
|
}
|
|
96
115
|
|
|
97
|
-
// Insert new task entry after the
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
// Insert new task entry after the opening brace line
|
|
117
|
+
// For nested CommonJS, check if we need to add newline to expand single-line format
|
|
118
|
+
let newEntry;
|
|
119
|
+
if (isNestedCjs) {
|
|
120
|
+
// Check if the tasks object already spans multiple lines
|
|
121
|
+
// (i.e., has whitespace and a newline after "tasks: {")
|
|
122
|
+
const remainingContent = indexContent.slice(insertPosition);
|
|
123
|
+
const whitespaceMatch = remainingContent.match(/^\s*\n/);
|
|
124
|
+
if (whitespaceMatch) {
|
|
125
|
+
// Multi-line format: skip whitespace and newline, insert at next position
|
|
126
|
+
insertPosition += whitespaceMatch[0].length;
|
|
127
|
+
newEntry = ` ${taskName}: "./${filename}",\n`;
|
|
128
|
+
} else {
|
|
129
|
+
// Single-line format: add newline to expand it
|
|
130
|
+
newEntry = `\n ${taskName}: "./${filename}",\n`;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
newEntry = ` ${taskName}: "./${filename}",\n`;
|
|
134
|
+
}
|
|
100
135
|
|
|
101
136
|
indexContent =
|
|
102
137
|
indexContent.slice(0, insertPosition) +
|
|
@@ -116,4 +151,4 @@ export async function handleTaskSave(req, res) {
|
|
|
116
151
|
error: error.message || "Failed to create task",
|
|
117
152
|
});
|
|
118
153
|
}
|
|
119
|
-
}
|
|
154
|
+
}
|