@sweny-ai/core 0.1.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 (93) hide show
  1. package/dist/__tests__/claude.test.d.ts +1 -0
  2. package/dist/__tests__/claude.test.js +328 -0
  3. package/dist/__tests__/executor.test.d.ts +1 -0
  4. package/dist/__tests__/executor.test.js +296 -0
  5. package/dist/__tests__/integration/datadog.integration.test.d.ts +1 -0
  6. package/dist/__tests__/integration/datadog.integration.test.js +23 -0
  7. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +1 -0
  8. package/dist/__tests__/integration/e2e-workflow.integration.test.js +75 -0
  9. package/dist/__tests__/integration/github.integration.test.d.ts +1 -0
  10. package/dist/__tests__/integration/github.integration.test.js +37 -0
  11. package/dist/__tests__/integration/harness.d.ts +24 -0
  12. package/dist/__tests__/integration/harness.js +34 -0
  13. package/dist/__tests__/integration/linear.integration.test.d.ts +1 -0
  14. package/dist/__tests__/integration/linear.integration.test.js +15 -0
  15. package/dist/__tests__/integration/sentry.integration.test.d.ts +1 -0
  16. package/dist/__tests__/integration/sentry.integration.test.js +20 -0
  17. package/dist/__tests__/integration/slack.integration.test.d.ts +1 -0
  18. package/dist/__tests__/integration/slack.integration.test.js +22 -0
  19. package/dist/__tests__/schema.test.d.ts +1 -0
  20. package/dist/__tests__/schema.test.js +239 -0
  21. package/dist/__tests__/skills-index.test.d.ts +1 -0
  22. package/dist/__tests__/skills-index.test.js +122 -0
  23. package/dist/__tests__/skills.test.d.ts +1 -0
  24. package/dist/__tests__/skills.test.js +296 -0
  25. package/dist/__tests__/studio.test.d.ts +1 -0
  26. package/dist/__tests__/studio.test.js +172 -0
  27. package/dist/__tests__/testing.test.d.ts +1 -0
  28. package/dist/__tests__/testing.test.js +224 -0
  29. package/dist/browser.d.ts +17 -0
  30. package/dist/browser.js +22 -0
  31. package/dist/claude.d.ts +48 -0
  32. package/dist/claude.js +293 -0
  33. package/dist/cli/check.d.ts +11 -0
  34. package/dist/cli/check.js +237 -0
  35. package/dist/cli/config-file.d.ts +12 -0
  36. package/dist/cli/config-file.js +208 -0
  37. package/dist/cli/config.d.ts +77 -0
  38. package/dist/cli/config.js +565 -0
  39. package/dist/cli/main.d.ts +10 -0
  40. package/dist/cli/main.js +744 -0
  41. package/dist/cli/output.d.ts +26 -0
  42. package/dist/cli/output.js +357 -0
  43. package/dist/cli/renderer.d.ts +33 -0
  44. package/dist/cli/renderer.js +423 -0
  45. package/dist/cli/renderer.test.d.ts +1 -0
  46. package/dist/cli/renderer.test.js +302 -0
  47. package/dist/cli/setup.d.ts +11 -0
  48. package/dist/cli/setup.js +310 -0
  49. package/dist/executor.d.ts +29 -0
  50. package/dist/executor.js +173 -0
  51. package/dist/executor.test.d.ts +1 -0
  52. package/dist/executor.test.js +314 -0
  53. package/dist/index.d.ts +37 -0
  54. package/dist/index.js +36 -0
  55. package/dist/mcp.d.ts +11 -0
  56. package/dist/mcp.js +183 -0
  57. package/dist/mcp.test.d.ts +1 -0
  58. package/dist/mcp.test.js +334 -0
  59. package/dist/schema.d.ts +318 -0
  60. package/dist/schema.js +207 -0
  61. package/dist/skills/betterstack.d.ts +7 -0
  62. package/dist/skills/betterstack.js +114 -0
  63. package/dist/skills/datadog.d.ts +7 -0
  64. package/dist/skills/datadog.js +107 -0
  65. package/dist/skills/github.d.ts +8 -0
  66. package/dist/skills/github.js +155 -0
  67. package/dist/skills/index.d.ts +68 -0
  68. package/dist/skills/index.js +134 -0
  69. package/dist/skills/linear.d.ts +7 -0
  70. package/dist/skills/linear.js +89 -0
  71. package/dist/skills/notification.d.ts +11 -0
  72. package/dist/skills/notification.js +142 -0
  73. package/dist/skills/sentry.d.ts +7 -0
  74. package/dist/skills/sentry.js +105 -0
  75. package/dist/skills/slack.d.ts +8 -0
  76. package/dist/skills/slack.js +115 -0
  77. package/dist/studio.d.ts +124 -0
  78. package/dist/studio.js +174 -0
  79. package/dist/testing.d.ts +88 -0
  80. package/dist/testing.js +253 -0
  81. package/dist/types.d.ts +144 -0
  82. package/dist/types.js +11 -0
  83. package/dist/workflow-builder.d.ts +45 -0
  84. package/dist/workflow-builder.js +120 -0
  85. package/dist/workflow-builder.test.d.ts +1 -0
  86. package/dist/workflow-builder.test.js +117 -0
  87. package/dist/workflows/implement.d.ts +11 -0
  88. package/dist/workflows/implement.js +83 -0
  89. package/dist/workflows/index.d.ts +2 -0
  90. package/dist/workflows/index.js +2 -0
  91. package/dist/workflows/triage.d.ts +18 -0
  92. package/dist/workflows/triage.js +108 -0
  93. package/package.json +83 -0
@@ -0,0 +1,224 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
3
+ import * as path from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { MockClaude, createFileSkill } from "../testing.js";
6
+ // ─── Fixtures ────────────────────────────────────────────────────
7
+ const tmpBase = path.join(tmpdir(), "sweny-testing-test");
8
+ function freshDir(name) {
9
+ const dir = path.join(tmpBase, `${name}-${Date.now()}`);
10
+ rmSync(dir, { recursive: true, force: true });
11
+ mkdirSync(dir, { recursive: true });
12
+ return dir;
13
+ }
14
+ const testWorkflow = {
15
+ id: "test",
16
+ name: "Test",
17
+ description: "",
18
+ entry: "first",
19
+ nodes: {
20
+ first: { name: "First", instruction: "Do the first thing", skills: [] },
21
+ second: { name: "Second", instruction: "Do the second thing", skills: [] },
22
+ alt: { name: "Alt", instruction: "Alternative path", skills: [] },
23
+ },
24
+ edges: [
25
+ { from: "first", to: "second" },
26
+ { from: "first", to: "alt", when: "needs alternative" },
27
+ ],
28
+ };
29
+ // ─── MockClaude tests ────────────────────────────────────────────
30
+ describe("MockClaude", () => {
31
+ it("returns scripted responses in order", async () => {
32
+ const claude = new MockClaude({
33
+ responses: {
34
+ first: { data: { step: 1 } },
35
+ second: { data: { step: 2 } },
36
+ },
37
+ });
38
+ const r1 = await claude.run({ instruction: "Do first", context: {}, tools: [] });
39
+ expect(r1.data.step).toBe(1);
40
+ const r2 = await claude.run({ instruction: "Do second", context: {}, tools: [] });
41
+ expect(r2.data.step).toBe(2);
42
+ });
43
+ it("matches nodes by instruction when workflow is provided", async () => {
44
+ const claude = new MockClaude({
45
+ workflow: testWorkflow,
46
+ responses: {
47
+ first: { data: { step: "first" } },
48
+ alt: { data: { step: "alt" } },
49
+ },
50
+ });
51
+ // Call in non-sequential order using exact instructions
52
+ const r1 = await claude.run({
53
+ instruction: "Alternative path",
54
+ context: {},
55
+ tools: [],
56
+ });
57
+ expect(r1.data.step).toBe("alt");
58
+ const r2 = await claude.run({
59
+ instruction: "Do the first thing",
60
+ context: {},
61
+ tools: [],
62
+ });
63
+ expect(r2.data.step).toBe("first");
64
+ });
65
+ it("tracks execution order", async () => {
66
+ const claude = new MockClaude({
67
+ responses: {
68
+ a: { data: {} },
69
+ b: { data: {} },
70
+ },
71
+ });
72
+ await claude.run({ instruction: "x", context: {}, tools: [] });
73
+ await claude.run({ instruction: "y", context: {}, tools: [] });
74
+ expect(claude.executedNodes).toEqual(["a", "b"]);
75
+ });
76
+ it("returns default response for unknown nodes", async () => {
77
+ const claude = new MockClaude({ responses: {} });
78
+ const result = await claude.run({ instruction: "anything", context: {}, tools: [] });
79
+ expect(result.status).toBe("success");
80
+ expect(result.data.summary).toContain("no scripted response");
81
+ });
82
+ it("executes scripted tool calls", async () => {
83
+ const dir = freshDir("mock-tools");
84
+ const fileSkill = createFileSkill(dir);
85
+ const claude = new MockClaude({
86
+ responses: {
87
+ node1: {
88
+ toolCalls: [{ tool: "fs_write_json", input: { path: "test.json", data: { ok: true } } }],
89
+ data: { wrote: true },
90
+ },
91
+ },
92
+ });
93
+ const result = await claude.run({
94
+ instruction: "Write file",
95
+ context: {},
96
+ tools: fileSkill.tools,
97
+ });
98
+ expect(result.toolCalls).toHaveLength(1);
99
+ expect(result.toolCalls[0].tool).toBe("fs_write_json");
100
+ expect(existsSync(path.join(dir, "test.json"))).toBe(true);
101
+ });
102
+ it("records tool not found errors", async () => {
103
+ const claude = new MockClaude({
104
+ responses: {
105
+ node1: {
106
+ toolCalls: [{ tool: "nonexistent_tool", input: {} }],
107
+ data: {},
108
+ },
109
+ },
110
+ });
111
+ const result = await claude.run({ instruction: "x", context: {}, tools: [] });
112
+ expect(result.toolCalls[0].output).toEqual({ error: "tool not found" });
113
+ });
114
+ describe("evaluate", () => {
115
+ it("follows scripted routes", async () => {
116
+ const claude = new MockClaude({
117
+ responses: { first: { data: {} } },
118
+ routes: { first: "alt" },
119
+ });
120
+ await claude.run({ instruction: "x", context: {}, tools: [] });
121
+ const chosen = await claude.evaluate({
122
+ question: "Which path?",
123
+ context: {},
124
+ choices: [
125
+ { id: "second", description: "Normal path" },
126
+ { id: "alt", description: "Alternative path" },
127
+ ],
128
+ });
129
+ expect(chosen).toBe("alt");
130
+ });
131
+ it("defaults to first choice when no route is scripted", async () => {
132
+ const claude = new MockClaude({ responses: { a: { data: {} } } });
133
+ await claude.run({ instruction: "x", context: {}, tools: [] });
134
+ const chosen = await claude.evaluate({
135
+ question: "Where?",
136
+ context: {},
137
+ choices: [
138
+ { id: "x", description: "First" },
139
+ { id: "y", description: "Second" },
140
+ ],
141
+ });
142
+ expect(chosen).toBe("x");
143
+ });
144
+ it("ignores invalid scripted routes", async () => {
145
+ const claude = new MockClaude({
146
+ responses: { a: { data: {} } },
147
+ routes: { a: "nonexistent" },
148
+ });
149
+ await claude.run({ instruction: "x", context: {}, tools: [] });
150
+ const chosen = await claude.evaluate({
151
+ question: "Where?",
152
+ context: {},
153
+ choices: [{ id: "valid", description: "Valid" }],
154
+ });
155
+ expect(chosen).toBe("valid"); // falls back to first choice
156
+ });
157
+ });
158
+ });
159
+ // ─── File skill tests ────────────────────────────────────────────
160
+ describe("createFileSkill", () => {
161
+ let dir;
162
+ beforeEach(() => {
163
+ dir = freshDir("fileskill");
164
+ });
165
+ it("reads and writes JSON", async () => {
166
+ const skill = createFileSkill(dir);
167
+ const write = skill.tools.find((t) => t.name === "fs_write_json");
168
+ const read = skill.tools.find((t) => t.name === "fs_read_json");
169
+ const ctx = { config: {}, logger: console };
170
+ await write.handler({ path: "test.json", data: { hello: "world" } }, ctx);
171
+ const result = await read.handler({ path: "test.json" }, ctx);
172
+ expect(result).toEqual({ hello: "world" });
173
+ });
174
+ it("reads text files", async () => {
175
+ const skill = createFileSkill(dir);
176
+ writeFileSync(path.join(dir, "hello.txt"), "hello world");
177
+ const read = skill.tools.find((t) => t.name === "fs_read_text");
178
+ const ctx = { config: {}, logger: console };
179
+ const result = await read.handler({ path: "hello.txt" }, ctx);
180
+ expect(result).toBe("hello world");
181
+ });
182
+ it("writes markdown with nested dirs", async () => {
183
+ const skill = createFileSkill(dir);
184
+ const write = skill.tools.find((t) => t.name === "fs_write_markdown");
185
+ const ctx = { config: {}, logger: console };
186
+ await write.handler({ path: "issues/ISSUE-1.md", content: "# Bug\n\nBroken" }, ctx);
187
+ const content = readFileSync(path.join(dir, "issues/ISSUE-1.md"), "utf-8");
188
+ expect(content).toContain("# Bug");
189
+ });
190
+ it("lists directory contents", async () => {
191
+ const skill = createFileSkill(dir);
192
+ writeFileSync(path.join(dir, "a.txt"), "a");
193
+ writeFileSync(path.join(dir, "b.txt"), "b");
194
+ const list = skill.tools.find((t) => t.name === "fs_list_dir");
195
+ const ctx = { config: {}, logger: console };
196
+ const result = await list.handler({}, ctx);
197
+ expect(result).toContain("a.txt");
198
+ expect(result).toContain("b.txt");
199
+ });
200
+ it("returns error info for non-existent directory", async () => {
201
+ const skill = createFileSkill(dir);
202
+ const list = skill.tools.find((t) => t.name === "fs_list_dir");
203
+ const ctx = { config: {}, logger: console };
204
+ const result = await list.handler({ path: "nonexistent" }, ctx);
205
+ expect(result).toHaveProperty("error");
206
+ });
207
+ it("creates deeply nested directories for JSON", async () => {
208
+ const skill = createFileSkill(dir);
209
+ const write = skill.tools.find((t) => t.name === "fs_write_json");
210
+ const ctx = { config: {}, logger: console };
211
+ await write.handler({ path: "deep/nested/dir/file.json", data: { deep: true } }, ctx);
212
+ expect(existsSync(path.join(dir, "deep/nested/dir/file.json"))).toBe(true);
213
+ });
214
+ it("has correct skill metadata", () => {
215
+ const skill = createFileSkill(dir);
216
+ expect(skill.id).toBe("filesystem");
217
+ expect(skill.tools).toHaveLength(5);
218
+ expect(skill.tools.map((t) => t.name)).toContain("fs_read_json");
219
+ expect(skill.tools.map((t) => t.name)).toContain("fs_read_text");
220
+ expect(skill.tools.map((t) => t.name)).toContain("fs_write_json");
221
+ expect(skill.tools.map((t) => t.name)).toContain("fs_write_markdown");
222
+ expect(skill.tools.map((t) => t.name)).toContain("fs_list_dir");
223
+ });
224
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Browser-safe entry point for @sweny-ai/core
3
+ *
4
+ * Re-exports everything EXCEPT ClaudeClient (which depends on
5
+ * @anthropic-ai/claude-agent-sdk, a Node-only package). Use this entry point
6
+ * in browser environments (Vite, webpack, Astro, etc.).
7
+ */
8
+ export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, } from "./types.js";
9
+ export { consoleLogger } from "./types.js";
10
+ export { execute } from "./executor.js";
11
+ export type { ExecuteOptions } from "./executor.js";
12
+ export { github, linear, slack, sentry, datadog, betterstack, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
13
+ export type { SkillValidationResult } from "./skills/index.js";
14
+ export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
15
+ export type { WorkflowError } from "./schema.js";
16
+ export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
17
+ export type { FlowNode, SkillNodeData, FlowEdge } from "./studio.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Browser-safe entry point for @sweny-ai/core
3
+ *
4
+ * Re-exports everything EXCEPT ClaudeClient (which depends on
5
+ * @anthropic-ai/claude-agent-sdk, a Node-only package). Use this entry point
6
+ * in browser environments (Vite, webpack, Astro, etc.).
7
+ */
8
+ export { consoleLogger } from "./types.js";
9
+ // Executor
10
+ export { execute } from "./executor.js";
11
+ // Skills
12
+ export { github, linear, slack, sentry, datadog, betterstack, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
13
+ // Schema & validation
14
+ export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
15
+ // Studio adapter
16
+ export { workflowToFlow, flowToWorkflow, applyExecutionEvent, exportAsTypescript, getSkillCatalog } from "./studio.js";
17
+ // Workflow builder — NOT included in browser entry.
18
+ // buildWorkflow/refineWorkflow depend on the Claude interface (Node-only).
19
+ // Import from "@sweny-ai/core" in Node environments.
20
+ // Testing utilities — NOT included in browser entry.
21
+ // MockClaude and createFileSkill depend on node:fs.
22
+ // Import from "@sweny-ai/core/testing" in Node environments.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Claude Client — Headless Claude Code backend
3
+ *
4
+ * Uses the @anthropic-ai/claude-agent-sdk to run headless Claude Code
5
+ * as the LLM backend. Tools are exposed via an in-process MCP server
6
+ * that Claude Code calls during execution.
7
+ *
8
+ * This is the ONLY supported LLM backend. The whole point of sweny
9
+ * is to use headless Claude Code — never the raw Anthropic API.
10
+ */
11
+ import type { Claude, Tool, ToolContext, NodeResult, JSONSchema, Logger, McpServerConfig } from "./types.js";
12
+ export interface ClaudeClientOptions {
13
+ /** Model override */
14
+ model?: string;
15
+ /** Max turns for tool use loop (default: 20) */
16
+ maxTurns?: number;
17
+ /** Working directory for Claude Code (default: process.cwd()) */
18
+ cwd?: string;
19
+ /** Logger */
20
+ logger?: Logger;
21
+ /** Default tool context for standalone usage (not via executor) */
22
+ defaultContext?: ToolContext;
23
+ /** External MCP servers (GitHub, Linear, Sentry, etc.) — merged with core skill tools */
24
+ mcpServers?: Record<string, McpServerConfig>;
25
+ }
26
+ export declare class ClaudeClient implements Claude {
27
+ private model;
28
+ private maxTurns;
29
+ private cwd;
30
+ private logger;
31
+ private defaultContext;
32
+ private mcpServers;
33
+ constructor(opts?: ClaudeClientOptions);
34
+ run(opts: {
35
+ instruction: string;
36
+ context: Record<string, unknown>;
37
+ tools: Tool[];
38
+ outputSchema?: JSONSchema;
39
+ }): Promise<NodeResult>;
40
+ evaluate(opts: {
41
+ question: string;
42
+ context: Record<string, unknown>;
43
+ choices: {
44
+ id: string;
45
+ description: string;
46
+ }[];
47
+ }): Promise<string>;
48
+ }
package/dist/claude.js ADDED
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Claude Client — Headless Claude Code backend
3
+ *
4
+ * Uses the @anthropic-ai/claude-agent-sdk to run headless Claude Code
5
+ * as the LLM backend. Tools are exposed via an in-process MCP server
6
+ * that Claude Code calls during execution.
7
+ *
8
+ * This is the ONLY supported LLM backend. The whole point of sweny
9
+ * is to use headless Claude Code — never the raw Anthropic API.
10
+ */
11
+ import { query, createSdkMcpServer, tool as sdkTool } from "@anthropic-ai/claude-agent-sdk";
12
+ import { z } from "zod";
13
+ import { consoleLogger } from "./types.js";
14
+ const SYSTEM_PROMPT = `You are a step in an automated workflow. Execute the instruction precisely using the tools available to you. Be thorough but concise. When you're done, summarize your findings and results.`;
15
+ export class ClaudeClient {
16
+ model;
17
+ maxTurns;
18
+ cwd;
19
+ logger;
20
+ defaultContext;
21
+ mcpServers;
22
+ constructor(opts = {}) {
23
+ this.model = opts.model;
24
+ this.maxTurns = opts.maxTurns ?? 20;
25
+ this.cwd = opts.cwd ?? process.cwd();
26
+ this.logger = opts.logger ?? consoleLogger;
27
+ this.defaultContext = opts.defaultContext ?? { config: {}, logger: this.logger };
28
+ this.mcpServers = opts.mcpServers ?? {};
29
+ }
30
+ async run(opts) {
31
+ const { instruction, context, tools, outputSchema } = opts;
32
+ const toolCalls = [];
33
+ // Convert core tools to SDK MCP tools
34
+ const sdkTools = tools.map((t) => coreToolToSdkTool(t, this.defaultContext, toolCalls));
35
+ // Create in-process MCP server
36
+ const mcpServer = createSdkMcpServer({
37
+ name: "sweny-core",
38
+ tools: sdkTools,
39
+ });
40
+ // Build prompt
41
+ const prompt = [
42
+ `## Instruction\n\n${instruction}`,
43
+ `## Context\n\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\``,
44
+ outputSchema
45
+ ? `## Required Output\n\nYou MUST end with a JSON object matching this schema:\n\`\`\`json\n${JSON.stringify(outputSchema, null, 2)}\n\`\`\``
46
+ : "",
47
+ ]
48
+ .filter(Boolean)
49
+ .join("\n\n");
50
+ // Spread process.env so Claude Code inherits PATH, HOME, auth tokens, etc.
51
+ const env = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
52
+ let response = "";
53
+ try {
54
+ const allMcpServers = { ...this.mcpServers };
55
+ if (sdkTools.length > 0)
56
+ allMcpServers["sweny-core"] = mcpServer;
57
+ const stream = query({
58
+ prompt,
59
+ options: {
60
+ maxTurns: this.maxTurns,
61
+ systemPrompt: SYSTEM_PROMPT,
62
+ cwd: this.cwd,
63
+ env,
64
+ permissionMode: "bypassPermissions",
65
+ ...(this.model ? { model: this.model } : {}),
66
+ ...(Object.keys(allMcpServers).length > 0 ? { mcpServers: allMcpServers } : {}),
67
+ },
68
+ });
69
+ for await (const message of stream) {
70
+ if (message.type === "result") {
71
+ const resultMsg = message;
72
+ if (resultMsg.subtype === "success" && "result" in resultMsg) {
73
+ response = resultMsg.result;
74
+ }
75
+ else if ("errors" in resultMsg) {
76
+ const errors = resultMsg.errors;
77
+ return {
78
+ status: "failed",
79
+ data: { error: errors?.join("\n") ?? "Execution failed" },
80
+ toolCalls,
81
+ };
82
+ }
83
+ }
84
+ }
85
+ }
86
+ catch (err) {
87
+ this.logger.error(`Claude Code query failed: ${err.message}`);
88
+ return {
89
+ status: "failed",
90
+ data: { error: err.message },
91
+ toolCalls,
92
+ };
93
+ }
94
+ return {
95
+ status: "success",
96
+ data: { summary: response, ...tryParseJSON(response) },
97
+ toolCalls,
98
+ };
99
+ }
100
+ async evaluate(opts) {
101
+ const { question, context, choices } = opts;
102
+ const choiceList = choices.map((c) => `- "${c.id}": ${c.description}`).join("\n");
103
+ const prompt = [
104
+ question,
105
+ `\nContext:\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\``,
106
+ `\nChoices:\n${choiceList}`,
107
+ `\nRespond with ONLY the choice ID, nothing else.`,
108
+ ].join("\n");
109
+ const env = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null));
110
+ let response = "";
111
+ try {
112
+ const stream = query({
113
+ prompt,
114
+ options: {
115
+ maxTurns: 1,
116
+ cwd: this.cwd,
117
+ env,
118
+ permissionMode: "bypassPermissions",
119
+ ...(this.model ? { model: this.model } : {}),
120
+ },
121
+ });
122
+ for await (const message of stream) {
123
+ if (message.type === "result") {
124
+ const resultMsg = message;
125
+ if (resultMsg.subtype === "success" && "result" in resultMsg) {
126
+ response = resultMsg.result;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ catch (err) {
132
+ this.logger.warn(`Evaluate query failed: ${err.message}. Falling back to first choice.`);
133
+ return choices[0].id;
134
+ }
135
+ const text = response.trim().replace(/^["']|["']$/g, "");
136
+ const validIds = choices.map((c) => c.id);
137
+ // Exact match
138
+ if (validIds.includes(text))
139
+ return text;
140
+ // Fuzzy — look for an ID embedded in the response
141
+ const match = validIds.find((id) => text.includes(id));
142
+ if (match)
143
+ return match;
144
+ // Fallback
145
+ this.logger.warn(`Could not parse route choice from: "${text.slice(0, 100)}". Falling back to first choice.`);
146
+ return validIds[0];
147
+ }
148
+ }
149
+ // ─── Tool conversion ────────────────────────────────────────────
150
+ /**
151
+ * Convert a core Tool to an SDK MCP tool definition.
152
+ * Bridges JSON Schema → Zod and wraps the handler to return CallToolResult.
153
+ */
154
+ function coreToolToSdkTool(coreTool, defaultCtx, toolCalls) {
155
+ const zodShape = jsonSchemaToZodShape(coreTool.input_schema);
156
+ return sdkTool(coreTool.name, coreTool.description, zodShape, async (args) => {
157
+ try {
158
+ // The executor wraps handlers to inject ToolContext.
159
+ // When used standalone, defaultCtx is the fallback.
160
+ const output = await coreTool.handler(args, defaultCtx);
161
+ toolCalls.push({ tool: coreTool.name, input: args, output });
162
+ return {
163
+ content: [{ type: "text", text: typeof output === "string" ? output : JSON.stringify(output) }],
164
+ };
165
+ }
166
+ catch (err) {
167
+ toolCalls.push({ tool: coreTool.name, input: args, output: { error: err.message } });
168
+ return {
169
+ content: [{ type: "text", text: `Error: ${err.message}` }],
170
+ isError: true,
171
+ };
172
+ }
173
+ });
174
+ }
175
+ // ─── JSON Schema → Zod conversion ───────────────────────────────
176
+ /**
177
+ * Convert a JSON Schema object to a Zod raw shape for the agent SDK.
178
+ * Preserves property names, types, and descriptions so Claude sees
179
+ * accurate tool parameters through the MCP protocol.
180
+ */
181
+ function jsonSchemaToZodShape(schema) {
182
+ const props = schema?.properties ?? {};
183
+ const required = new Set(schema?.required ?? []);
184
+ const shape = {};
185
+ for (const [key, prop] of Object.entries(props)) {
186
+ let zodType = jsonPropertyToZod(prop);
187
+ if (!required.has(key)) {
188
+ zodType = zodType.optional();
189
+ }
190
+ shape[key] = zodType;
191
+ }
192
+ return shape;
193
+ }
194
+ function jsonPropertyToZod(prop) {
195
+ if (!prop || typeof prop !== "object")
196
+ return z.unknown();
197
+ const desc = typeof prop.description === "string" ? prop.description : undefined;
198
+ switch (prop.type) {
199
+ case "string": {
200
+ if (prop.enum && Array.isArray(prop.enum)) {
201
+ const e = z.enum(prop.enum);
202
+ return desc ? e.describe(desc) : e;
203
+ }
204
+ const s = z.string();
205
+ return desc ? s.describe(desc) : s;
206
+ }
207
+ case "number":
208
+ case "integer": {
209
+ const n = z.number();
210
+ return desc ? n.describe(desc) : n;
211
+ }
212
+ case "boolean": {
213
+ const b = z.boolean();
214
+ return desc ? b.describe(desc) : b;
215
+ }
216
+ case "array": {
217
+ const items = prop.items ? jsonPropertyToZod(prop.items) : z.unknown();
218
+ const a = z.array(items);
219
+ return desc ? a.describe(desc) : a;
220
+ }
221
+ case "object": {
222
+ if (prop.properties && typeof prop.properties === "object") {
223
+ const nested = jsonSchemaToZodShape(prop);
224
+ const o = z.object(nested);
225
+ return desc ? o.describe(desc) : o;
226
+ }
227
+ const r = z.record(z.string(), z.unknown());
228
+ return desc ? r.describe(desc) : r;
229
+ }
230
+ default: {
231
+ const u = z.unknown();
232
+ return desc ? u.describe(desc) : u;
233
+ }
234
+ }
235
+ }
236
+ // ─── JSON extraction ────────────────────────────────────────────
237
+ /**
238
+ * Extract a JSON object from Claude's text response.
239
+ *
240
+ * Strategy (in order):
241
+ * 1. ```json code block``` — most reliable, Claude often wraps JSON this way
242
+ * 2. Last brace-delimited `{...}` block — handles inline JSON at end of text
243
+ * 3. Full text parse — for responses that are pure JSON
244
+ * 4. Empty object — safe fallback
245
+ */
246
+ function tryParseJSON(text) {
247
+ if (!text)
248
+ return {};
249
+ // 1. Code block
250
+ const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
251
+ if (codeBlockMatch) {
252
+ try {
253
+ const parsed = JSON.parse(codeBlockMatch[1].trim());
254
+ if (typeof parsed === "object" && parsed !== null)
255
+ return parsed;
256
+ }
257
+ catch {
258
+ /* try next strategy */
259
+ }
260
+ }
261
+ // 2. Last brace-delimited block (scan backwards for matching braces)
262
+ const lastBrace = text.lastIndexOf("}");
263
+ if (lastBrace !== -1) {
264
+ let depth = 0;
265
+ for (let i = lastBrace; i >= 0; i--) {
266
+ if (text[i] === "}")
267
+ depth++;
268
+ else if (text[i] === "{")
269
+ depth--;
270
+ if (depth === 0) {
271
+ try {
272
+ const parsed = JSON.parse(text.slice(i, lastBrace + 1));
273
+ if (typeof parsed === "object" && parsed !== null)
274
+ return parsed;
275
+ }
276
+ catch {
277
+ /* try next strategy */
278
+ }
279
+ break;
280
+ }
281
+ }
282
+ }
283
+ // 3. Full text parse
284
+ try {
285
+ const parsed = JSON.parse(text.trim());
286
+ if (typeof parsed === "object" && parsed !== null)
287
+ return parsed;
288
+ }
289
+ catch {
290
+ /* fall through */
291
+ }
292
+ return {};
293
+ }
@@ -0,0 +1,11 @@
1
+ import type { CliConfig } from "./config.js";
2
+ export interface CheckResult {
3
+ name: string;
4
+ status: "ok" | "fail" | "skip";
5
+ detail: string;
6
+ }
7
+ /**
8
+ * Run lightweight connectivity checks for all configured providers.
9
+ * Uses raw fetch — does NOT import provider packages.
10
+ */
11
+ export declare function checkProviderConnectivity(config: CliConfig): Promise<CheckResult[]>;