@sammorrowdrums/mcpi-ext 0.2.0 → 0.2.1

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 (208) hide show
  1. package/dist/code-mode/code-mode-integration.test.d.ts +2 -0
  2. package/dist/code-mode/code-mode-integration.test.d.ts.map +1 -0
  3. package/dist/code-mode/code-mode-integration.test.js +116 -0
  4. package/dist/code-mode/code-mode-integration.test.js.map +1 -0
  5. package/dist/code-mode/eligibility.d.ts +10 -0
  6. package/dist/code-mode/eligibility.d.ts.map +1 -0
  7. package/dist/code-mode/eligibility.js +14 -0
  8. package/dist/code-mode/eligibility.js.map +1 -0
  9. package/dist/code-mode/eligibility.test.d.ts +2 -0
  10. package/dist/code-mode/eligibility.test.d.ts.map +1 -0
  11. package/dist/code-mode/eligibility.test.js +53 -0
  12. package/dist/code-mode/eligibility.test.js.map +1 -0
  13. package/dist/code-mode/executor.d.ts +35 -0
  14. package/dist/code-mode/executor.d.ts.map +1 -0
  15. package/dist/code-mode/executor.js +120 -0
  16. package/dist/code-mode/executor.js.map +1 -0
  17. package/dist/code-mode/executor.test.d.ts +2 -0
  18. package/dist/code-mode/executor.test.d.ts.map +1 -0
  19. package/dist/code-mode/executor.test.js +130 -0
  20. package/dist/code-mode/executor.test.js.map +1 -0
  21. package/dist/code-mode/index.d.ts +65 -0
  22. package/dist/code-mode/index.d.ts.map +1 -0
  23. package/dist/code-mode/index.js +175 -0
  24. package/dist/code-mode/index.js.map +1 -0
  25. package/dist/code-mode/tools.d.ts +45 -0
  26. package/dist/code-mode/tools.d.ts.map +1 -0
  27. package/dist/code-mode/tools.js +70 -0
  28. package/dist/code-mode/tools.js.map +1 -0
  29. package/dist/code-mode/type-hints.d.ts +20 -0
  30. package/dist/code-mode/type-hints.d.ts.map +1 -0
  31. package/dist/code-mode/type-hints.js +189 -0
  32. package/dist/code-mode/type-hints.js.map +1 -0
  33. package/dist/code-mode/type-hints.test.d.ts +2 -0
  34. package/dist/code-mode/type-hints.test.d.ts.map +1 -0
  35. package/dist/code-mode/type-hints.test.js +157 -0
  36. package/dist/code-mode/type-hints.test.js.map +1 -0
  37. package/dist/docker-e2e.d.ts +55 -0
  38. package/dist/docker-e2e.d.ts.map +1 -0
  39. package/dist/docker-e2e.js +235 -0
  40. package/dist/docker-e2e.js.map +1 -0
  41. package/dist/docker-e2e.test.d.ts +2 -0
  42. package/dist/docker-e2e.test.d.ts.map +1 -0
  43. package/dist/docker-e2e.test.js +176 -0
  44. package/dist/docker-e2e.test.js.map +1 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +148 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/index.test.d.ts +2 -0
  50. package/dist/index.test.d.ts.map +1 -0
  51. package/dist/index.test.js +8 -0
  52. package/dist/index.test.js.map +1 -0
  53. package/dist/mcp/client-manager.d.ts +46 -0
  54. package/dist/mcp/client-manager.d.ts.map +1 -0
  55. package/dist/mcp/client-manager.js +148 -0
  56. package/dist/mcp/client-manager.js.map +1 -0
  57. package/dist/mcp/client-manager.test.d.ts +2 -0
  58. package/dist/mcp/client-manager.test.d.ts.map +1 -0
  59. package/dist/mcp/client-manager.test.js +204 -0
  60. package/dist/mcp/client-manager.test.js.map +1 -0
  61. package/dist/mcp/config-loader.d.ts +8 -0
  62. package/dist/mcp/config-loader.d.ts.map +1 -0
  63. package/dist/mcp/config-loader.js +43 -0
  64. package/dist/mcp/config-loader.js.map +1 -0
  65. package/dist/mcp/config-loader.test.d.ts +2 -0
  66. package/dist/mcp/config-loader.test.d.ts.map +1 -0
  67. package/dist/mcp/config-loader.test.js +93 -0
  68. package/dist/mcp/config-loader.test.js.map +1 -0
  69. package/dist/mcp/config.d.ts +46 -0
  70. package/dist/mcp/config.d.ts.map +1 -0
  71. package/dist/mcp/config.js +28 -0
  72. package/dist/mcp/config.js.map +1 -0
  73. package/{src/mcp/index.ts → dist/mcp/index.d.ts} +1 -0
  74. package/dist/mcp/index.d.ts.map +1 -0
  75. package/dist/mcp/index.js +4 -0
  76. package/dist/mcp/index.js.map +1 -0
  77. package/dist/skills/discover.d.ts +10 -0
  78. package/dist/skills/discover.d.ts.map +1 -0
  79. package/dist/skills/discover.js +60 -0
  80. package/dist/skills/discover.js.map +1 -0
  81. package/dist/skills/format.d.ts +10 -0
  82. package/dist/skills/format.d.ts.map +1 -0
  83. package/dist/skills/format.js +38 -0
  84. package/dist/skills/format.js.map +1 -0
  85. package/dist/skills/format.test.d.ts +2 -0
  86. package/dist/skills/format.test.d.ts.map +1 -0
  87. package/dist/skills/format.test.js +63 -0
  88. package/dist/skills/format.test.js.map +1 -0
  89. package/{src/skills/index.ts → dist/skills/index.d.ts} +2 -5
  90. package/dist/skills/index.d.ts.map +1 -0
  91. package/dist/skills/index.js +6 -0
  92. package/dist/skills/index.js.map +1 -0
  93. package/dist/skills/load-skill-tool.d.ts +40 -0
  94. package/dist/skills/load-skill-tool.d.ts.map +1 -0
  95. package/dist/skills/load-skill-tool.js +104 -0
  96. package/dist/skills/load-skill-tool.js.map +1 -0
  97. package/dist/skills/mcp-tool-proxy.d.ts +11 -0
  98. package/dist/skills/mcp-tool-proxy.d.ts.map +1 -0
  99. package/dist/skills/mcp-tool-proxy.js +116 -0
  100. package/dist/skills/mcp-tool-proxy.js.map +1 -0
  101. package/dist/skills/skill-integration.test.d.ts +2 -0
  102. package/dist/skills/skill-integration.test.d.ts.map +1 -0
  103. package/dist/skills/skill-integration.test.js +119 -0
  104. package/dist/skills/skill-integration.test.js.map +1 -0
  105. package/dist/skills/skill-registry.d.ts +39 -0
  106. package/dist/skills/skill-registry.d.ts.map +1 -0
  107. package/dist/skills/skill-registry.js +48 -0
  108. package/dist/skills/skill-registry.js.map +1 -0
  109. package/dist/skills/skill-registry.test.d.ts +2 -0
  110. package/dist/skills/skill-registry.test.d.ts.map +1 -0
  111. package/dist/skills/skill-registry.test.js +67 -0
  112. package/dist/skills/skill-registry.test.js.map +1 -0
  113. package/dist/test-servers/weather-server.d.ts +12 -0
  114. package/dist/test-servers/weather-server.d.ts.map +1 -0
  115. package/dist/test-servers/weather-server.js +139 -0
  116. package/dist/test-servers/weather-server.js.map +1 -0
  117. package/dist/test-servers/weather-stdio.d.ts +2 -0
  118. package/dist/test-servers/weather-stdio.d.ts.map +1 -0
  119. package/{src/test-servers/weather-stdio.ts → dist/test-servers/weather-stdio.js} +1 -1
  120. package/dist/test-servers/weather-stdio.js.map +1 -0
  121. package/dist/tool-cli/cli.d.ts +3 -0
  122. package/dist/tool-cli/cli.d.ts.map +1 -0
  123. package/dist/tool-cli/cli.js +179 -0
  124. package/dist/tool-cli/cli.js.map +1 -0
  125. package/dist/tool-cli/constants.d.ts +7 -0
  126. package/dist/tool-cli/constants.d.ts.map +1 -0
  127. package/dist/tool-cli/constants.js +15 -0
  128. package/dist/tool-cli/constants.js.map +1 -0
  129. package/dist/tool-cli/format.d.ts +8 -0
  130. package/dist/tool-cli/format.d.ts.map +1 -0
  131. package/{src/tool-cli/format.ts → dist/tool-cli/format.js} +5 -4
  132. package/dist/tool-cli/format.js.map +1 -0
  133. package/dist/tool-cli/format.test.d.ts +2 -0
  134. package/dist/tool-cli/format.test.d.ts.map +1 -0
  135. package/dist/tool-cli/format.test.js +30 -0
  136. package/dist/tool-cli/format.test.js.map +1 -0
  137. package/{src/tool-cli/index.ts → dist/tool-cli/index.d.ts} +1 -0
  138. package/dist/tool-cli/index.d.ts.map +1 -0
  139. package/dist/tool-cli/index.js +3 -0
  140. package/dist/tool-cli/index.js.map +1 -0
  141. package/dist/tool-cli/rpc-client.d.ts +6 -0
  142. package/dist/tool-cli/rpc-client.d.ts.map +1 -0
  143. package/dist/tool-cli/rpc-client.js +25 -0
  144. package/dist/tool-cli/rpc-client.js.map +1 -0
  145. package/dist/tool-cli/rpc-server.d.ts +48 -0
  146. package/dist/tool-cli/rpc-server.d.ts.map +1 -0
  147. package/dist/tool-cli/rpc-server.js +220 -0
  148. package/dist/tool-cli/rpc-server.js.map +1 -0
  149. package/dist/tool-cli/rpc-server.test.d.ts +2 -0
  150. package/dist/tool-cli/rpc-server.test.d.ts.map +1 -0
  151. package/dist/tool-cli/rpc-server.test.js +154 -0
  152. package/dist/tool-cli/rpc-server.test.js.map +1 -0
  153. package/package.json +5 -1
  154. package/.dockerignore +0 -6
  155. package/.github/dependabot.yml +0 -25
  156. package/.github/workflows/ci.yml +0 -50
  157. package/.hadolint.yaml +0 -6
  158. package/.prettierignore +0 -4
  159. package/.prettierrc +0 -7
  160. package/AGENTS.md +0 -118
  161. package/DECISIONS.md +0 -99
  162. package/Dockerfile +0 -39
  163. package/aube-lock.yaml +0 -4146
  164. package/docs/code-mode.md +0 -38
  165. package/docs/skills.md +0 -57
  166. package/docs/tool-cli.md +0 -54
  167. package/eslint.config.js +0 -27
  168. package/images/banner.webp +0 -0
  169. package/images/code-c-maude.webp +0 -0
  170. package/images/code-mode-histogram.png +0 -0
  171. package/images/nuclear-mcp-football.webp +0 -0
  172. package/images/skills-enabling-mcp-tools.png +0 -0
  173. package/images/the-skill-dealer.webp +0 -0
  174. package/images/tool-cli-grep.png +0 -0
  175. package/mise.toml +0 -53
  176. package/scripts/docker-entrypoint.sh +0 -29
  177. package/scripts/test-echo-server.mjs +0 -28
  178. package/scripts/test-mcp-integration.mjs +0 -59
  179. package/scripts/test-tool-cli-smoke.mjs +0 -93
  180. package/src/code-mode/code-mode-integration.test.ts +0 -135
  181. package/src/code-mode/eligibility.test.ts +0 -60
  182. package/src/code-mode/eligibility.ts +0 -16
  183. package/src/code-mode/executor.test.ts +0 -168
  184. package/src/code-mode/executor.ts +0 -163
  185. package/src/code-mode/index.ts +0 -208
  186. package/src/code-mode/tools.ts +0 -110
  187. package/src/code-mode/type-hints.test.ts +0 -187
  188. package/src/code-mode/type-hints.ts +0 -249
  189. package/src/docker-e2e.test.ts +0 -195
  190. package/src/docker-e2e.ts +0 -277
  191. package/src/index.test.ts +0 -8
  192. package/src/index.ts +0 -179
  193. package/src/mcp/client-manager.test.ts +0 -250
  194. package/src/mcp/client-manager.ts +0 -199
  195. package/src/mcp/config-loader.test.ts +0 -115
  196. package/src/mcp/config-loader.ts +0 -46
  197. package/src/mcp/config.ts +0 -40
  198. package/src/skills/discover.ts +0 -81
  199. package/src/skills/format.test.ts +0 -70
  200. package/src/skills/format.ts +0 -42
  201. package/src/skills/load-skill-tool.ts +0 -136
  202. package/src/skills/mcp-tool-proxy.ts +0 -143
  203. package/src/skills/skill-integration.test.ts +0 -139
  204. package/src/skills/skill-registry.test.ts +0 -83
  205. package/src/skills/skill-registry.ts +0 -69
  206. package/src/test-servers/weather-server.ts +0 -171
  207. package/src/tool-cli/format.test.ts +0 -33
  208. package/tsconfig.json +0 -21
@@ -1,187 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { McpTool } from "../mcp/index.js";
3
- import { generateTypeHints, jsonSchemaToTypeString, sanitizeToolName } from "./type-hints.js";
4
-
5
- describe("jsonSchemaToTypeString", () => {
6
- it("converts string type", () => {
7
- expect(jsonSchemaToTypeString({ type: "string" })).toBe("string");
8
- });
9
-
10
- it("converts number type", () => {
11
- expect(jsonSchemaToTypeString({ type: "number" })).toBe("number");
12
- });
13
-
14
- it("converts integer type", () => {
15
- expect(jsonSchemaToTypeString({ type: "integer" })).toBe("number");
16
- });
17
-
18
- it("converts boolean type", () => {
19
- expect(jsonSchemaToTypeString({ type: "boolean" })).toBe("boolean");
20
- });
21
-
22
- it("converts null type", () => {
23
- expect(jsonSchemaToTypeString({ type: "null" })).toBe("null");
24
- });
25
-
26
- it("converts type array", () => {
27
- expect(jsonSchemaToTypeString({ type: ["string", "null"] })).toBe("string | null");
28
- });
29
-
30
- it("converts enum to union", () => {
31
- expect(jsonSchemaToTypeString({ enum: ["a", "b", "c"] })).toBe('"a" | "b" | "c"');
32
- });
33
-
34
- it("converts const", () => {
35
- expect(jsonSchemaToTypeString({ const: "fixed" })).toBe('"fixed"');
36
- });
37
-
38
- it("converts simple object", () => {
39
- const result = jsonSchemaToTypeString({
40
- type: "object",
41
- properties: {
42
- name: { type: "string" },
43
- age: { type: "number" },
44
- },
45
- required: ["name"],
46
- });
47
- expect(result).toContain("name: string;");
48
- expect(result).toContain("age?: number;");
49
- });
50
-
51
- it("converts empty object to Record", () => {
52
- expect(jsonSchemaToTypeString({ type: "object" })).toBe("Record<string, unknown>");
53
- });
54
-
55
- it("converts array with items", () => {
56
- expect(jsonSchemaToTypeString({ type: "array", items: { type: "string" } })).toBe("string[]");
57
- });
58
-
59
- it("converts array without items", () => {
60
- expect(jsonSchemaToTypeString({ type: "array" })).toBe("unknown[]");
61
- });
62
-
63
- it("handles anyOf", () => {
64
- const result = jsonSchemaToTypeString({
65
- anyOf: [{ type: "string" }, { type: "number" }],
66
- });
67
- expect(result).toBe("string | number");
68
- });
69
-
70
- it("handles oneOf", () => {
71
- const result = jsonSchemaToTypeString({
72
- oneOf: [{ type: "boolean" }, { type: "null" }],
73
- });
74
- expect(result).toBe("boolean | null");
75
- });
76
-
77
- it("handles allOf", () => {
78
- const result = jsonSchemaToTypeString({
79
- allOf: [
80
- { type: "object", properties: { a: { type: "string" } }, required: ["a"] },
81
- { type: "object", properties: { b: { type: "number" } }, required: ["b"] },
82
- ],
83
- });
84
- expect(result).toContain("a: string;");
85
- expect(result).toContain("b: number;");
86
- expect(result).toContain("&");
87
- });
88
-
89
- it("resolves $ref", () => {
90
- const result = jsonSchemaToTypeString(
91
- { $ref: "#/definitions/Foo" },
92
- { Foo: { type: "string" } },
93
- );
94
- expect(result).toBe("string");
95
- });
96
-
97
- it("handles unknown $ref gracefully", () => {
98
- expect(jsonSchemaToTypeString({ $ref: "#/definitions/Missing" }, {})).toBe("unknown");
99
- });
100
-
101
- it("guards against deep recursion", () => {
102
- // Create a deeply nested schema
103
- let schema: Record<string, unknown> = { type: "string" };
104
- for (let i = 0; i < 25; i++) {
105
- schema = { type: "object", properties: { nested: schema }, required: ["nested"] };
106
- }
107
- const result = jsonSchemaToTypeString(schema);
108
- expect(result).toContain("unknown"); // Should hit depth guard
109
- });
110
-
111
- it("includes description as JSDoc in object properties", () => {
112
- const result = jsonSchemaToTypeString({
113
- type: "object",
114
- properties: {
115
- query: { type: "string", description: "Search term" },
116
- },
117
- required: ["query"],
118
- });
119
- expect(result).toContain("/** Search term */");
120
- });
121
- });
122
-
123
- describe("sanitizeToolName", () => {
124
- it("replaces hyphens with underscores", () => {
125
- expect(sanitizeToolName("list-issues")).toBe("list_issues");
126
- });
127
-
128
- it("replaces dots with underscores", () => {
129
- expect(sanitizeToolName("github.search")).toBe("github_search");
130
- });
131
-
132
- it("keeps valid identifiers unchanged", () => {
133
- expect(sanitizeToolName("searchDocs")).toBe("searchDocs");
134
- });
135
- });
136
-
137
- describe("generateTypeHints", () => {
138
- it("returns empty string for no tools", () => {
139
- expect(generateTypeHints([])).toBe("");
140
- });
141
-
142
- it("generates declaration for a simple tool", () => {
143
- const tools: McpTool[] = [
144
- {
145
- name: "search_docs",
146
- description: "Search documentation",
147
- inputSchema: {
148
- type: "object",
149
- properties: {
150
- query: { type: "string", description: "Search term" },
151
- },
152
- required: ["query"],
153
- },
154
- outputSchema: {
155
- type: "object",
156
- properties: {
157
- results: { type: "array", items: { type: "string" } },
158
- },
159
- required: ["results"],
160
- },
161
- serverName: "docs",
162
- },
163
- ];
164
-
165
- const result = generateTypeHints(tools);
166
- expect(result).toContain("declare const codemode");
167
- expect(result).toContain("search_docs:");
168
- expect(result).toContain("Promise<");
169
- expect(result).toContain("listTools");
170
- expect(result).toContain("describeTools");
171
- });
172
-
173
- it("sanitizes tool names with special characters", () => {
174
- const tools: McpTool[] = [
175
- {
176
- name: "github.list-repos",
177
- description: "List repos",
178
- inputSchema: { type: "object", properties: {} },
179
- outputSchema: { type: "object", properties: {} },
180
- serverName: "github",
181
- },
182
- ];
183
-
184
- const result = generateTypeHints(tools);
185
- expect(result).toContain("github_list_repos:");
186
- });
187
- });
@@ -1,249 +0,0 @@
1
- import type { McpTool } from "../mcp/index.js";
2
-
3
- type JsonSchema = Record<string, unknown>;
4
-
5
- /**
6
- * Convert a JSON Schema to a TypeScript type string.
7
- *
8
- * Handles objects, arrays, primitives, enums, anyOf/oneOf/allOf, $ref,
9
- * and circular references (depth guard at 20).
10
- */
11
- export function jsonSchemaToTypeString(
12
- schema: JsonSchema,
13
- definitions?: Record<string, JsonSchema>,
14
- depth = 0,
15
- seen = new Set<JsonSchema>(),
16
- ): string {
17
- if (depth > 20) return "unknown";
18
- if (seen.has(schema)) return "unknown";
19
- seen.add(schema);
20
-
21
- // Handle $ref
22
- if (typeof schema.$ref === "string") {
23
- const refPath = schema.$ref;
24
- const refName = refPath.replace(/^#\/(definitions|components\/schemas|\\$defs)\//, "");
25
- const resolved = definitions?.[refName];
26
- if (resolved) {
27
- return jsonSchemaToTypeString(resolved, definitions, depth + 1, seen);
28
- }
29
- return "unknown";
30
- }
31
-
32
- // Handle enum
33
- if (Array.isArray(schema.enum)) {
34
- return schema.enum.map((v: unknown) => JSON.stringify(v)).join(" | ");
35
- }
36
-
37
- // Handle const
38
- if ("const" in schema) {
39
- return JSON.stringify(schema.const);
40
- }
41
-
42
- // Handle anyOf / oneOf
43
- const unionKey = schema.anyOf ? "anyOf" : schema.oneOf ? "oneOf" : null;
44
- if (unionKey && Array.isArray(schema[unionKey])) {
45
- const variants = (schema[unionKey] as JsonSchema[]).map((s) =>
46
- jsonSchemaToTypeString(s, definitions, depth + 1, seen),
47
- );
48
- return variants.join(" | ");
49
- }
50
-
51
- // Handle allOf
52
- if (Array.isArray(schema.allOf)) {
53
- const parts = (schema.allOf as JsonSchema[]).map((s) =>
54
- jsonSchemaToTypeString(s, definitions, depth + 1, seen),
55
- );
56
- return parts.join(" & ");
57
- }
58
-
59
- const type = schema.type as string | string[] | undefined;
60
-
61
- // Handle type arrays (e.g. ["string", "null"])
62
- if (Array.isArray(type)) {
63
- const types = type.map((t: string) => primitiveToTs(t));
64
- return types.join(" | ");
65
- }
66
-
67
- switch (type) {
68
- case "object":
69
- return objectToTs(schema, definitions, depth, seen);
70
- case "array":
71
- return arrayToTs(schema, definitions, depth, seen);
72
- case "string":
73
- case "number":
74
- case "integer":
75
- case "boolean":
76
- case "null":
77
- return primitiveToTs(type);
78
- default:
79
- // No type specified — try to infer from properties
80
- if (schema.properties) {
81
- return objectToTs(schema, definitions, depth, seen);
82
- }
83
- return "unknown";
84
- }
85
- }
86
-
87
- function primitiveToTs(type: string): string {
88
- switch (type) {
89
- case "string":
90
- return "string";
91
- case "number":
92
- case "integer":
93
- return "number";
94
- case "boolean":
95
- return "boolean";
96
- case "null":
97
- return "null";
98
- default:
99
- return "unknown";
100
- }
101
- }
102
-
103
- function objectToTs(
104
- schema: JsonSchema,
105
- definitions: Record<string, JsonSchema> | undefined,
106
- depth: number,
107
- seen: Set<JsonSchema>,
108
- ): string {
109
- const properties = schema.properties as Record<string, JsonSchema> | undefined;
110
- if (!properties || Object.keys(properties).length === 0) {
111
- return "Record<string, unknown>";
112
- }
113
-
114
- const required = new Set<string>(
115
- Array.isArray(schema.required) ? (schema.required as string[]) : [],
116
- );
117
- const lines: string[] = [];
118
-
119
- for (const [key, propSchema] of Object.entries(properties)) {
120
- const desc = propSchema.description as string | undefined;
121
- if (desc) {
122
- lines.push(` /** ${desc} */`);
123
- }
124
- const optional = required.has(key) ? "" : "?";
125
- const typeStr = jsonSchemaToTypeString(propSchema, definitions, depth + 1, seen);
126
- lines.push(` ${safeName(key)}${optional}: ${typeStr};`);
127
- }
128
-
129
- return `{\n${lines.join("\n")}\n}`;
130
- }
131
-
132
- function arrayToTs(
133
- schema: JsonSchema,
134
- definitions: Record<string, JsonSchema> | undefined,
135
- depth: number,
136
- seen: Set<JsonSchema>,
137
- ): string {
138
- const items = schema.items as JsonSchema | undefined;
139
- if (!items) return "unknown[]";
140
-
141
- // Tuple form
142
- if (Array.isArray(items)) {
143
- const tupleTypes = items.map((s: JsonSchema) =>
144
- jsonSchemaToTypeString(s, definitions, depth + 1, seen),
145
- );
146
- return `[${tupleTypes.join(", ")}]`;
147
- }
148
-
149
- const itemType = jsonSchemaToTypeString(items, definitions, depth + 1, seen);
150
- return `${itemType}[]`;
151
- }
152
-
153
- /** Ensure property name is a valid JS identifier, quote otherwise. */
154
- function safeName(name: string): string {
155
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
156
- }
157
-
158
- /** Sanitize a tool name to be a valid JS identifier. */
159
- export function sanitizeToolName(name: string): string {
160
- return name.replace(/[^a-zA-Z0-9_$]/g, "_");
161
- }
162
-
163
- /**
164
- * Generate TypeScript type declarations for a set of MCP tools.
165
- *
166
- * Produces a `declare const codemode: { ... }` block with type-safe
167
- * method signatures the model can use when writing code.
168
- */
169
- export function generateTypeHints(tools: McpTool[]): string {
170
- if (tools.length === 0) return "";
171
-
172
- const methods: string[] = [];
173
-
174
- for (const tool of tools) {
175
- const safeName = sanitizeToolName(tool.name);
176
- const inputSchema = tool.inputSchema as JsonSchema;
177
- const outputSchema = tool.outputSchema as JsonSchema | undefined;
178
- const definitions = (inputSchema.$defs ??
179
- inputSchema.definitions ??
180
- outputSchema?.$defs ??
181
- outputSchema?.definitions) as Record<string, JsonSchema> | undefined;
182
-
183
- // Build input type
184
- const inputType = generateInputType(safeName, inputSchema, definitions);
185
-
186
- // Build output type
187
- const outputType = outputSchema ? jsonSchemaToTypeString(outputSchema, definitions) : "unknown";
188
-
189
- // Build JSDoc
190
- const jsdoc = buildJsDoc(tool, inputSchema);
191
-
192
- methods.push(`${jsdoc} ${safeName}: (input: ${inputType}) => Promise<${outputType}>;`);
193
- }
194
-
195
- const toolListType = tools.map((t) => `"${escapeStr(t.name)}"`).join(" | ");
196
-
197
- return [
198
- "// Code mode type hints — auto-generated from MCP tool schemas",
199
- "// Available tools are accessed via the `codemode` namespace",
200
- "",
201
- `declare const codemode: {`,
202
- ` /** List all available code mode tool names. */`,
203
- ` listTools: () => Promise<(${toolListType})[]>;`,
204
- ` /** Get full type information for specific tools. */`,
205
- ` describeTools: (names: string[]) => Promise<string>;`,
206
- ...methods.map((m) => m),
207
- `};`,
208
- ].join("\n");
209
- }
210
-
211
- function generateInputType(
212
- _toolSafeName: string,
213
- inputSchema: JsonSchema,
214
- definitions: Record<string, JsonSchema> | undefined,
215
- ): string {
216
- const properties = inputSchema.properties as Record<string, JsonSchema> | undefined;
217
- if (!properties || Object.keys(properties).length === 0) {
218
- return "Record<string, never>";
219
- }
220
-
221
- // Always inline the type — avoids emitting unreferenced named type aliases
222
- const typeStr = jsonSchemaToTypeString(inputSchema, definitions);
223
- return typeStr;
224
- }
225
-
226
- function buildJsDoc(tool: McpTool, inputSchema: JsonSchema): string {
227
- const lines: string[] = [" /**"];
228
-
229
- if (tool.description) {
230
- lines.push(` * ${tool.description}`);
231
- }
232
-
233
- const properties = inputSchema.properties as Record<string, JsonSchema> | undefined;
234
- if (properties) {
235
- for (const [key, propSchema] of Object.entries(properties)) {
236
- const desc = propSchema.description as string | undefined;
237
- if (desc) {
238
- lines.push(` * @param input.${key} - ${desc}`);
239
- }
240
- }
241
- }
242
-
243
- lines.push(" */");
244
- return lines.join("\n") + "\n";
245
- }
246
-
247
- function escapeStr(s: string): string {
248
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ");
249
- }
@@ -1,195 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- parseJsonlLine,
4
- extractAssistantText,
5
- extractUsage,
6
- extractModel,
7
- buildDockerArgs,
8
- } from "./docker-e2e.js";
9
-
10
- describe("parseJsonlLine", () => {
11
- it("parses valid JSON event", () => {
12
- const line = '{"type":"message_start","message":{"role":"assistant"}}';
13
- const result = parseJsonlLine(line);
14
- expect(result).not.toBeNull();
15
- expect(result?.type).toBe("message_start");
16
- expect(result?.raw).toBe(line);
17
- });
18
-
19
- it("returns raw type for non-JSON", () => {
20
- const result = parseJsonlLine("some plain text output");
21
- expect(result).not.toBeNull();
22
- expect(result?.type).toBe("raw");
23
- expect(result?.raw).toBe("some plain text output");
24
- });
25
-
26
- it("returns null for empty lines", () => {
27
- expect(parseJsonlLine("")).toBeNull();
28
- expect(parseJsonlLine(" ")).toBeNull();
29
- });
30
-
31
- it("defaults to unknown type when type field missing", () => {
32
- const result = parseJsonlLine('{"data":"hello"}');
33
- expect(result?.type).toBe("unknown");
34
- });
35
- });
36
-
37
- describe("extractAssistantText", () => {
38
- it("extracts text from message_end events", () => {
39
- const log = [
40
- {
41
- type: "message_end",
42
- timestamp: 0,
43
- raw: JSON.stringify({
44
- type: "message_end",
45
- message: {
46
- role: "assistant",
47
- content: [{ type: "text", text: "Hello world" }],
48
- },
49
- }),
50
- },
51
- ];
52
- expect(extractAssistantText(log)).toBe("Hello world");
53
- });
54
-
55
- it("concatenates multiple assistant messages", () => {
56
- const makeEnd = (text: string) => ({
57
- type: "message_end",
58
- timestamp: 0,
59
- raw: JSON.stringify({
60
- type: "message_end",
61
- message: {
62
- role: "assistant",
63
- content: [{ type: "text", text }],
64
- },
65
- }),
66
- });
67
- const log = [makeEnd("First"), makeEnd("Second")];
68
- expect(extractAssistantText(log)).toBe("First\nSecond");
69
- });
70
-
71
- it("ignores non-assistant messages", () => {
72
- const log = [
73
- {
74
- type: "message_end",
75
- timestamp: 0,
76
- raw: JSON.stringify({
77
- type: "message_end",
78
- message: {
79
- role: "user",
80
- content: [{ type: "text", text: "ignored" }],
81
- },
82
- }),
83
- },
84
- ];
85
- expect(extractAssistantText(log)).toBe("");
86
- });
87
-
88
- it("ignores non-message_end events", () => {
89
- const log = [{ type: "message_start", timestamp: 0, raw: "{}" }];
90
- expect(extractAssistantText(log)).toBe("");
91
- });
92
- });
93
-
94
- describe("extractUsage", () => {
95
- it("accumulates usage from assistant messages", () => {
96
- const log = [
97
- {
98
- type: "message_end",
99
- timestamp: 0,
100
- raw: JSON.stringify({
101
- type: "message_end",
102
- message: {
103
- role: "assistant",
104
- content: [],
105
- usage: {
106
- input: 100,
107
- output: 50,
108
- cacheRead: 10,
109
- cacheWrite: 5,
110
- cost: { total: 0.01 },
111
- },
112
- },
113
- }),
114
- },
115
- {
116
- type: "message_end",
117
- timestamp: 0,
118
- raw: JSON.stringify({
119
- type: "message_end",
120
- message: {
121
- role: "assistant",
122
- content: [],
123
- usage: {
124
- input: 200,
125
- output: 100,
126
- cacheRead: 20,
127
- cacheWrite: 10,
128
- cost: { total: 0.02 },
129
- },
130
- },
131
- }),
132
- },
133
- ];
134
- const usage = extractUsage(log);
135
- expect(usage.turns).toBe(2);
136
- expect(usage.input).toBe(300);
137
- expect(usage.output).toBe(150);
138
- expect(usage.cacheRead).toBe(30);
139
- expect(usage.cacheWrite).toBe(15);
140
- expect(usage.cost).toBeCloseTo(0.03);
141
- });
142
-
143
- it("returns zeroes for empty log", () => {
144
- const usage = extractUsage([]);
145
- expect(usage.turns).toBe(0);
146
- expect(usage.input).toBe(0);
147
- });
148
- });
149
-
150
- describe("extractModel", () => {
151
- it("extracts model from message_end", () => {
152
- const log = [
153
- {
154
- type: "message_end",
155
- timestamp: 0,
156
- raw: JSON.stringify({
157
- type: "message_end",
158
- message: { model: "claude-sonnet-4-5", role: "assistant", content: [] },
159
- }),
160
- },
161
- ];
162
- expect(extractModel(log)).toBe("claude-sonnet-4-5");
163
- });
164
-
165
- it("returns undefined for empty log", () => {
166
- expect(extractModel([])).toBeUndefined();
167
- });
168
- });
169
-
170
- describe("buildDockerArgs", () => {
171
- it("builds basic args", () => {
172
- const args = buildDockerArgs("my-image", {
173
- task: "Fix the bug",
174
- token: "ghp_abc123",
175
- });
176
- expect(args).toEqual([
177
- "run",
178
- "--rm",
179
- "-e",
180
- "GITHUB_TOKEN=ghp_abc123",
181
- "my-image",
182
- "Fix the bug",
183
- ]);
184
- });
185
-
186
- it("includes model env var when specified", () => {
187
- const args = buildDockerArgs("my-image", {
188
- task: "Fix it",
189
- token: "ghp_abc",
190
- model: "claude-opus-4-5",
191
- });
192
- expect(args).toContain("-e");
193
- expect(args).toContain("PI_MODEL=claude-opus-4-5");
194
- });
195
- });