toolfactory 0.1.2 → 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 (101) hide show
  1. package/README.md +24 -1
  2. package/dist/commands.d.ts +1 -1
  3. package/dist/distribution/native.d.ts +50 -0
  4. package/dist/distribution/native.js +414 -0
  5. package/dist/distribution/native.test.d.ts +1 -0
  6. package/dist/distribution/native.test.js +127 -0
  7. package/dist/distribution/system.d.ts +57 -0
  8. package/dist/distribution/system.js +637 -0
  9. package/dist/distribution/system.test.d.ts +1 -0
  10. package/dist/distribution/system.test.js +116 -0
  11. package/dist/e2e.test.js +7 -1
  12. package/dist/hosts/openclaw.js +6 -2
  13. package/dist/hosts/vscode.d.ts +1 -0
  14. package/dist/hosts/vscode.js +67 -0
  15. package/dist/model.d.ts +62 -2
  16. package/dist/model.js +9 -0
  17. package/dist/ops.d.ts +52 -0
  18. package/dist/project/apply.d.ts +1 -1
  19. package/dist/project/apply.js +194 -17
  20. package/dist/project/apply.test.js +103 -0
  21. package/dist/project/gate.d.ts +17 -2
  22. package/dist/project/gate.js +68 -3
  23. package/dist/project/history.d.ts +29 -1
  24. package/dist/project/lock.d.ts +5 -0
  25. package/dist/project/lock.js +9 -0
  26. package/dist/surfaces/agent-plugins.js +5 -4
  27. package/dist/surfaces/agents.js +21 -3
  28. package/dist/surfaces/agents.test.js +12 -0
  29. package/dist/surfaces/claude.js +8 -3
  30. package/dist/surfaces/codex.js +8 -3
  31. package/dist/surfaces/codex.test.js +8 -8
  32. package/dist/surfaces/cursor.js +2 -2
  33. package/dist/surfaces/distribution.test.js +3 -3
  34. package/dist/surfaces/dsh.js +27 -7
  35. package/dist/surfaces/dsh.test.js +37 -3
  36. package/dist/surfaces/gemini.js +2 -2
  37. package/dist/surfaces/gemini.test.js +4 -4
  38. package/dist/surfaces/hermes-native.js +38 -9
  39. package/dist/surfaces/hermes-native.test.js +32 -11
  40. package/dist/surfaces/mcp-registry.js +43 -5
  41. package/dist/surfaces/mcp-registry.test.js +18 -0
  42. package/dist/surfaces/openclaw-native.js +10 -8
  43. package/dist/surfaces/openclaw-native.test.js +30 -0
  44. package/dist/surfaces/readme.d.ts +16 -0
  45. package/dist/surfaces/readme.js +7 -0
  46. package/dist/surfaces/registry.js +6 -0
  47. package/dist/surfaces/vscode-extension.d.ts +14 -0
  48. package/dist/surfaces/vscode-extension.js +312 -0
  49. package/dist/surfaces/vscode-extension.test.d.ts +1 -0
  50. package/dist/surfaces/vscode-extension.test.js +81 -0
  51. package/dist/surfaces/vscode-scaffold.d.ts +43 -0
  52. package/dist/surfaces/vscode-scaffold.js +43 -0
  53. package/dist/surfaces/web.d.ts +1 -1
  54. package/dist/surfaces/web.js +1 -1
  55. package/dist/surfaces/workflows.js +49 -10
  56. package/dist/surfaces/workflows.test.js +3 -2
  57. package/dist/toolfactory/cli.js +1 -1
  58. package/dist/toolfactory/mcp.js +1 -1
  59. package/package.json +2 -1
  60. package/src/distribution/native.test.ts +161 -0
  61. package/src/distribution/native.ts +460 -0
  62. package/src/distribution/system.test.ts +136 -0
  63. package/src/distribution/system.ts +688 -0
  64. package/src/e2e.test.ts +7 -1
  65. package/src/hosts/openclaw.ts +4 -2
  66. package/src/hosts/vscode.ts +79 -0
  67. package/src/model.ts +15 -1
  68. package/src/project/apply.test.ts +120 -0
  69. package/src/project/apply.ts +257 -15
  70. package/src/project/gate.ts +96 -4
  71. package/src/project/lock.ts +12 -0
  72. package/src/surfaces/agent-plugins.ts +5 -4
  73. package/src/surfaces/agents.test.ts +16 -0
  74. package/src/surfaces/agents.ts +31 -3
  75. package/src/surfaces/claude.ts +7 -3
  76. package/src/surfaces/codex.test.ts +7 -7
  77. package/src/surfaces/codex.ts +8 -3
  78. package/src/surfaces/cursor.ts +2 -10
  79. package/src/surfaces/distribution.test.ts +2 -2
  80. package/src/surfaces/dsh.test.ts +38 -2
  81. package/src/surfaces/dsh.ts +28 -7
  82. package/src/surfaces/gemini.test.ts +3 -3
  83. package/src/surfaces/gemini.ts +1 -2
  84. package/src/surfaces/hermes-native.test.ts +39 -10
  85. package/src/surfaces/hermes-native.ts +41 -11
  86. package/src/surfaces/mcp-registry.test.ts +20 -0
  87. package/src/surfaces/mcp-registry.ts +46 -6
  88. package/src/surfaces/openclaw-native.test.ts +37 -0
  89. package/src/surfaces/openclaw-native.ts +49 -47
  90. package/src/surfaces/readme.ts +10 -0
  91. package/src/surfaces/registry.ts +6 -0
  92. package/src/surfaces/vscode-extension.test.ts +89 -0
  93. package/src/surfaces/vscode-extension.ts +345 -0
  94. package/src/surfaces/vscode-scaffold.ts +55 -0
  95. package/src/surfaces/web.ts +1 -1
  96. package/src/surfaces/workflows.test.ts +3 -4
  97. package/src/surfaces/workflows.ts +49 -10
  98. package/src/toolfactory/cli.ts +1 -1
  99. package/src/toolfactory/mcp.ts +1 -1
  100. package/web/dist/assets/{index-Spapkkvh.js → index-BHR6gCfm.js} +1 -1
  101. package/web/dist/index.html +1 -1
@@ -0,0 +1,345 @@
1
+ /** A native VS Code extension. The author owns activation and all native contributions. */
2
+ import { execArgv } from "node:process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { z } from "zod";
5
+ import { githubOwner, projectName } from "../identity/name.js";
6
+ import { isPortable, type Project, type Surface } from "../model.js";
7
+ import {
8
+ configProperties,
9
+ dataDirEnvName,
10
+ envName,
11
+ has,
12
+ isSensitive,
13
+ npmName,
14
+ pypiName,
15
+ rootEnvName,
16
+ } from "./shared.js";
17
+ import {
18
+ ESBUILD,
19
+ ESLINT,
20
+ GITIGNORE,
21
+ IGNORE,
22
+ LAUNCH,
23
+ TASKS,
24
+ VSCODE_SCAFFOLD,
25
+ } from "./vscode-scaffold.js";
26
+
27
+ export const HOST_DIR = "hosts/vscode";
28
+ export const vscodeConfigSchema = z.object({
29
+ publisher: z
30
+ .string()
31
+ .regex(/^[a-zA-Z0-9][a-zA-Z0-9-]*$/)
32
+ .optional()
33
+ .describe(
34
+ "Your registered Visual Studio Marketplace/Open VSX publisher; repository owner is the local packaging default.",
35
+ ),
36
+ marketplace: z.boolean().default(true),
37
+ openvsx: z.boolean().default(true),
38
+ });
39
+ export const VSCE_VERSION = "3.9.2";
40
+ export const OVSX_VERSION = "1.1.1";
41
+
42
+ export function extensionName(project: Project): string {
43
+ return project.identity.name.replaceAll(".", "-");
44
+ }
45
+
46
+ export function publisher(project: Project): string {
47
+ return (
48
+ project.tool.vscode?.publisher ??
49
+ githubOwner(project.identity.repository) ??
50
+ extensionName(project)
51
+ );
52
+ }
53
+
54
+ export function vsixName(project: Project): string {
55
+ return `${extensionName(project)}-${project.identity.version ?? "0.0.0"}.vsix`;
56
+ }
57
+
58
+ function operations(project: Project) {
59
+ return project.operations.filter(isPortable);
60
+ }
61
+
62
+ function runtime(project: Project): string {
63
+ const name = extensionName(project);
64
+ const python = project.tool.binding === "python";
65
+ const packageName = python ? pypiName(project) : npmName(project);
66
+ const version = project.identity.version ?? "0.0.0";
67
+ const defaults = python
68
+ ? { command: "uvx", args: ["--from", `${packageName}==${version}`, project.identity.name] }
69
+ : { command: "npx", args: ["--yes", `${packageName}@${version}`] };
70
+ const config = Object.entries(configProperties(project)).map(([key, property]) => ({
71
+ key,
72
+ env: envName(key),
73
+ secret: isSensitive(property),
74
+ }));
75
+ return `// Generated by toolfactory. Native extension code belongs in extension.ts.
76
+ import { mkdir } from 'node:fs/promises';
77
+ import { Client } from '@modelcontextprotocol/client';
78
+ import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
79
+ import * as vscode from 'vscode';
80
+
81
+ const namespace = ${JSON.stringify(name)};
82
+ const defaults = ${JSON.stringify(defaults)};
83
+ const config: Array<{key: string; env: string; secret: boolean}> = ${JSON.stringify(config)};
84
+ const operations: Array<{name: string; title: string}> = ${JSON.stringify(operations(project).map((op) => ({ name: op.name, title: op.title ?? op.name })))};
85
+
86
+ function launch(context: vscode.ExtensionContext) {
87
+ const settings = vscode.workspace.getConfiguration(namespace);
88
+ const root = vscode.Uri.joinPath(context.extensionUri, '../..').fsPath;
89
+ const development = context.extensionMode !== vscode.ExtensionMode.Production;
90
+ const local = ${python ? `{ command: 'uv', args: ['run', '--quiet', '--directory', root, 'python', '-m', '${projectName.pythonPackage(project.identity.name)}.toolfactory.cli'] }` : `{ command: 'node', args: ['--import', 'tsx', root + '/src/toolfactory/cli.ts'] }`};
91
+ const selected = development ? local : defaults;
92
+ const command = settings.get<string>('runtime.command');
93
+ return {
94
+ command: command || selected.command,
95
+ args: command ? settings.get<string[]>('runtime.args', []) : selected.args,
96
+ cwd: development ? root : undefined,
97
+ };
98
+ }
99
+
100
+ async function environment(context: vscode.ExtensionContext): Promise<Record<string, string>> {
101
+ const settings = vscode.workspace.getConfiguration(namespace);
102
+ const env: Record<string, string> = {};
103
+ for (const property of config) {
104
+ const value = (property.secret ? await context.secrets.get(property.env) : settings.get('config.' + property.key)) ?? process.env[property.env];
105
+ if (value !== undefined) env[property.env] = typeof value === 'string' ? value : JSON.stringify(value);
106
+ }
107
+ await mkdir(context.globalStorageUri.fsPath, { recursive: true });
108
+ env[${JSON.stringify(dataDirEnvName(project))}] = context.globalStorageUri.fsPath;
109
+ const folder = vscode.workspace.workspaceFolders?.find(folder => folder.uri.scheme === 'file');
110
+ if (folder) env[${JSON.stringify(rootEnvName(project))}] = folder.uri.fsPath;
111
+ return env;
112
+ }
113
+
114
+ export function registerGenerated(context: vscode.ExtensionContext): void {
115
+ const changes = new vscode.EventEmitter<void>();
116
+ context.subscriptions.push(changes,
117
+ vscode.workspace.onDidChangeConfiguration(event => { if (event.affectsConfiguration(namespace)) changes.fire(); }),
118
+ context.secrets.onDidChange(() => changes.fire()),
119
+ vscode.lm.registerMcpServerDefinitionProvider(namespace, {
120
+ onDidChangeMcpServerDefinitions: changes.event,
121
+ provideMcpServerDefinitions() {
122
+ const selected = launch(context);
123
+ const server = new vscode.McpStdioServerDefinition(
124
+ ${JSON.stringify(projectName.display(project.identity.name))}, selected.command,
125
+ [...selected.args, 'mcp'], {}, ${JSON.stringify(version)},
126
+ );
127
+ server.cwd = selected.cwd ? vscode.Uri.file(selected.cwd) : undefined;
128
+ return [server];
129
+ },
130
+ async resolveMcpServerDefinition(server) {
131
+ if (!vscode.workspace.isTrusted) throw new Error('Trust this workspace before starting the tool runtime.');
132
+ if (server instanceof vscode.McpStdioServerDefinition) server.env = { ...server.env, ...await environment(context) };
133
+ return server;
134
+ },
135
+ }),
136
+ );
137
+ ${
138
+ config.some((property) => property.secret)
139
+ ? `context.subscriptions.push(vscode.commands.registerCommand(namespace + '.configureSecrets', async () => {
140
+ const chosen = await vscode.window.showQuickPick(config.filter(property => property.secret).map(property => property.env), { title: 'Configure credentials' });
141
+ if (!chosen) return;
142
+ const value = await vscode.window.showInputBox({ title: chosen, password: true, prompt: 'Stored in VS Code SecretStorage. Leave empty to remove.' });
143
+ if (value === undefined) return;
144
+ if (value) await context.secrets.store(chosen, value); else await context.secrets.delete(chosen);
145
+ }));`
146
+ : ""
147
+ }
148
+ for (const operation of operations) {
149
+ context.subscriptions.push(vscode.commands.registerCommand(namespace + '.' + operation.name, async (args?: Record<string, unknown>) => {
150
+ if (!vscode.workspace.isTrusted) throw new Error('Trust this workspace before running tools.');
151
+ const interactive = args === undefined;
152
+ if (interactive) {
153
+ const input = await vscode.window.showInputBox({ title: operation.title, prompt: 'Operation arguments (JSON object)', value: '{}',
154
+ validateInput(value) { try { const parsed = JSON.parse(value); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? undefined : 'Enter a JSON object'; } catch { return 'Enter a JSON object'; } },
155
+ });
156
+ if (input === undefined) return;
157
+ args = JSON.parse(input) as Record<string, unknown>;
158
+ }
159
+ const selected = launch(context);
160
+ const client = new Client({ name: namespace + '-vscode', version: ${JSON.stringify(version)} });
161
+ try {
162
+ await client.connect(new StdioClientTransport({ command: selected.command, args: [...selected.args, 'mcp'],
163
+ cwd: selected.cwd, env: await environment(context),
164
+ }));
165
+ const response = await client.callTool({ name: operation.name, arguments: args });
166
+ if (response.isError) throw new Error(JSON.stringify(response.content));
167
+ const result = response.structuredContent ?? response.content;
168
+ if (interactive) await vscode.window.showTextDocument(await vscode.workspace.openTextDocument({ language: 'json', content: JSON.stringify(result, null, 2) }));
169
+ return result;
170
+ } finally {
171
+ await client.close();
172
+ }
173
+ }));
174
+ }
175
+ }
176
+ `;
177
+ }
178
+
179
+ function packagePatch(project: Project): Record<string, unknown> {
180
+ const name = extensionName(project);
181
+ const properties = Object.fromEntries(
182
+ Object.entries(configProperties(project))
183
+ .filter(([, property]) => !isSensitive(property))
184
+ .map(([key, property]) => {
185
+ const { "x-toolfactory": _metadata, ...schema } = property;
186
+ return [`${name}.config.${key}`, schema];
187
+ }),
188
+ );
189
+ const commands = operations(project).map((op) => ({
190
+ command: `${name}.${op.name}`,
191
+ title: op.title ?? op.name,
192
+ category: projectName.display(project.identity.name),
193
+ }));
194
+ if (Object.values(configProperties(project)).some(isSensitive))
195
+ commands.push({
196
+ command: `${name}.configureSecrets`,
197
+ title: "Configure credentials",
198
+ category: projectName.display(project.identity.name),
199
+ });
200
+ return {
201
+ name,
202
+ version: project.identity.version ?? "0.0.0",
203
+ displayName: projectName.display(project.identity.name),
204
+ description: project.identity.description ?? `${project.identity.name} tools for VS Code`,
205
+ publisher: publisher(project),
206
+ license: project.identity.license ?? "UNLICENSED",
207
+ ...(project.identity.repository
208
+ ? { repository: { type: "git", url: project.identity.repository } }
209
+ : {}),
210
+ engines: VSCODE_SCAFFOLD.engines,
211
+ main: "./dist/extension.js",
212
+ capabilities: { untrustedWorkspaces: { supported: false } },
213
+ contributes: {
214
+ commands,
215
+ mcpServerDefinitionProviders: [
216
+ { id: name, label: projectName.display(project.identity.name) },
217
+ ],
218
+ configuration: {
219
+ title: projectName.display(project.identity.name),
220
+ properties: {
221
+ ...properties,
222
+ [`${name}.runtime.command`]: {
223
+ type: "string",
224
+ scope: "machine",
225
+ description: "Override the tool runtime executable.",
226
+ },
227
+ [`${name}.runtime.args`]: {
228
+ type: "array",
229
+ items: { type: "string" },
230
+ scope: "machine",
231
+ description: "Arguments for runtime.command, before the mcp subcommand.",
232
+ },
233
+ },
234
+ },
235
+ },
236
+ scripts: { ...VSCODE_SCAFFOLD.scripts, vsix: "vsce package --out extension.vsix" },
237
+ dependencies: { "@modelcontextprotocol/client": "^2.0.0" },
238
+ devDependencies: { ...VSCODE_SCAFFOLD.devDependencies, "@vscode/vsce": `^${VSCE_VERSION}` },
239
+ overrides: VSCODE_SCAFFOLD.overrides,
240
+ };
241
+ }
242
+
243
+ export const surface: Surface = {
244
+ id: "vscode-extension",
245
+ plan(project) {
246
+ const registry = project.tool.binding === "python" ? "pypi" : "npm";
247
+ if (!has(project, registry))
248
+ throw new Error(`vscode-extension requires ${registry} for its installed runtime.`);
249
+ const entry = `import * as vscode from 'vscode';\nimport { registerGenerated } from './generated';\n`;
250
+ const probe = operations(project).find((op) => project.tool.tests.examples[op.name]);
251
+ return [
252
+ {
253
+ kind: "merge",
254
+ path: `${HOST_DIR}/package.json`,
255
+ format: "json",
256
+ patch: packagePatch(project),
257
+ keyedArrays: {
258
+ "contributes.commands": "command",
259
+ "contributes.mcpServerDefinitionProviders": "id",
260
+ },
261
+ },
262
+ { kind: "file", path: `${HOST_DIR}/src/generated.ts`, content: runtime(project) },
263
+ {
264
+ kind: "region",
265
+ path: `${HOST_DIR}/src/extension.ts`,
266
+ regions: [{ begin: "// tf:imports", end: "// /tf:imports", content: `\n${entry}` }],
267
+ template: `// tf:imports\n// /tf:imports\n\n// This is your extension entry. Use the complete VS Code API here.\nexport function activate(context: vscode.ExtensionContext) {\n registerGenerated(context);\n}\n\nexport function deactivate() {}\n`,
268
+ },
269
+ {
270
+ kind: "merge",
271
+ path: `${HOST_DIR}/tsconfig.json`,
272
+ format: "json",
273
+ patch: {
274
+ compilerOptions: {
275
+ module: "Node16",
276
+ target: "ES2022",
277
+ lib: ["ES2022"],
278
+ types: ["node", "mocha"],
279
+ sourceMap: true,
280
+ rootDir: "src",
281
+ strict: true,
282
+ },
283
+ },
284
+ },
285
+ ...[
286
+ ["esbuild.js", ESBUILD],
287
+ ["eslint.config.mjs", ESLINT],
288
+ [".vscode/launch.json", LAUNCH],
289
+ [".vscode/tasks.json", TASKS],
290
+ [".vscodeignore", IGNORE],
291
+ [".gitignore", GITIGNORE],
292
+ ].map(([path, content]) => ({
293
+ kind: "region" as const,
294
+ path: `${HOST_DIR}/${path}`,
295
+ regions: [
296
+ {
297
+ begin: path?.endsWith("ignore") ? "# tf:scaffold" : "// tf:scaffold",
298
+ end: path?.endsWith("ignore") ? "# /tf:scaffold" : "// /tf:scaffold",
299
+ content: "\n",
300
+ },
301
+ ],
302
+ template: `${path?.endsWith("ignore") ? "# tf:scaffold\n# /tf:scaffold" : "// tf:scaffold\n// /tf:scaffold"}\n${content}`,
303
+ })),
304
+ {
305
+ kind: "file",
306
+ path: `${HOST_DIR}/.vscode-test.mjs`,
307
+ content: `import { defineConfig } from '@vscode/test-cli';\nexport default defineConfig({ files: 'out/test/**/*.test.js', launchArgs: ['--disable-workspace-trust', '--skip-welcome', '--skip-release-notes', '--no-sandbox'] });\n`,
308
+ },
309
+ {
310
+ kind: "file",
311
+ path: `${HOST_DIR}/src/test/generated.test.ts`,
312
+ content: `import * as assert from 'node:assert/strict';\nimport * as vscode from 'vscode';\nsuite('Generated extension', () => {\n test('activates and registers the operations', async () => {\n const extension = vscode.extensions.getExtension(${JSON.stringify(`${publisher(project)}.${extensionName(project)}`)});\n assert.ok(extension);\n await extension.activate();\n const commands = await vscode.commands.getCommands(true);\n for (const command of ${JSON.stringify(operations(project).map((op) => `${extensionName(project)}.${op.name}`))}) assert.ok(commands.includes(command), command);\n ${probe ? `await vscode.commands.executeCommand(${JSON.stringify(`${extensionName(project)}.${probe.name}`)}, ${JSON.stringify(project.tool.tests.examples[probe.name])});` : ""}\n });\n});\n`,
313
+ },
314
+ {
315
+ kind: "file",
316
+ path: `${HOST_DIR}/README.md`,
317
+ content: `# ${projectName.display(project.identity.name)} for VS Code\n\n${project.identity.description ?? ""}\n\nInstall the VSIX with \`code --install-extension ${vsixName(project)}\`. The extension provides an MCP server and commands for each portable operation. ${registry === "npm" ? "Node.js/npm" : "uv"} must be on PATH; installed extensions use the matching published ${registry} version.\n\nDevelop with \`npm install\`, then open this directory in VS Code and press F5. Development and tests use the checkout's kernel. \`npm test\` runs a real Extension Development Host; \`npm run vsix\` creates the installable package.\n\nEdit \`src/extension.ts\` outside its import markers and add native files freely. Use all VS Code APIs: language-model tools, chat participants, views, webviews, commands, editors and language services. Add native contribution points and additional commands directly to \`package.json\`; generated command/provider entries are maintained by identity, preserving your entries. Build scripts, debugger settings and ignore rules are editable scaffold files. Use \`adopt\` only when replacing a generated integration itself.\n\nConfiguration is in VS Code Settings; credentials use SecretStorage through the Configure credentials command when declared. The runtime command/arguments can be overridden in machine settings.\n\nPublishers must be registered separately with Visual Studio Marketplace and Open VSX. Releases attach a VSIX even when no publishing credentials are configured.\n`,
318
+ },
319
+ ];
320
+ },
321
+ validate(project) {
322
+ const cwd = `${project.root}/${HOST_DIR}`;
323
+ const drift = fileURLToPath(new URL("../hosts/vscode.js", import.meta.url));
324
+ return [
325
+ {
326
+ label: "VS Code upstream scaffold",
327
+ command: "node",
328
+ args: [...execArgv, drift, project.root],
329
+ },
330
+ { label: "VS Code dependencies", command: "npm", args: ["install"], cwd },
331
+ { label: "VS Code VSIX", command: "npm", args: ["run", "vsix"], cwd },
332
+ {
333
+ label: "VS Code Extension Development Host",
334
+ command: process.platform === "linux" ? "xvfb-run" : "npm",
335
+ args: process.platform === "linux" ? ["-a", "npm", "test"] : ["test"],
336
+ cwd,
337
+ },
338
+ ];
339
+ },
340
+ verdict(operation) {
341
+ return isPortable(operation)
342
+ ? { kind: "bridged", reason: "bridged:vscode-mcp" }
343
+ : { kind: "excluded", reason: "excluded:implement-in-hosts" };
344
+ },
345
+ };
@@ -0,0 +1,55 @@
1
+ /** Captured by running generator-code@1.12.0 (TypeScript/esbuild); hosts/vscode.ts verifies it. */
2
+
3
+ export const VSCODE_GENERATOR = "1.12.0";
4
+ export const GITIGNORE = "out\ndist\nnode_modules\n.vscode-test/\n*.vsix\n";
5
+
6
+ export const VSCODE_SCAFFOLD = {
7
+ engines: {
8
+ vscode: "^1.136.0",
9
+ },
10
+ scripts: {
11
+ "vscode:prepublish": "npm run package",
12
+ compile: "npm run check-types && npm run lint && node esbuild.js",
13
+ watch: "npm-run-all -p watch:*",
14
+ "watch:esbuild": "node esbuild.js --watch",
15
+ "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
16
+ package: "npm run check-types && npm run lint && node esbuild.js --production",
17
+ "compile-tests": "tsc -p . --outDir out",
18
+ "watch-tests": "tsc -p . -w --outDir out",
19
+ pretest: "npm run compile-tests && npm run compile && npm run lint",
20
+ "check-types": "tsc --noEmit",
21
+ lint: "eslint src",
22
+ test: "vscode-test",
23
+ },
24
+ devDependencies: {
25
+ "@types/vscode": "^1.136.0",
26
+ "@types/mocha": "^10.0.10",
27
+ "@types/node": "24.x",
28
+ "typescript-eslint": "^8.61.1",
29
+ eslint: "^10.5.0",
30
+ esbuild: "^0.28.1",
31
+ "npm-run-all": "^4.1.5",
32
+ typescript: "^6.0.3",
33
+ "@vscode/test-cli": "^0.0.15",
34
+ "@vscode/test-electron": "^3.0.0",
35
+ },
36
+ overrides: {
37
+ diff: "^8.0.4",
38
+ "serialize-javascript": "^7.0.6",
39
+ },
40
+ } as const;
41
+
42
+ export const ESBUILD =
43
+ "const esbuild = require(\"esbuild\");\n\nconst production = process.argv.includes('--production');\nconst watch = process.argv.includes('--watch');\n\n/**\n * @type {import('esbuild').Plugin}\n */\nconst esbuildProblemMatcherPlugin = {\n\tname: 'esbuild-problem-matcher',\n\n\tsetup(build) {\n\t\tbuild.onStart(() => {\n\t\t\tconsole.log('[watch] build started');\n\t\t});\n\t\tbuild.onEnd((result) => {\n\t\t\tresult.errors.forEach(({ text, location }) => {\n\t\t\t\tconsole.error(`\u2718 [ERROR] ${text}`);\n\t\t\t\tconsole.error(` ${location.file}:${location.line}:${location.column}:`);\n\t\t\t});\n\t\t\tconsole.log('[watch] build finished');\n\t\t});\n\t},\n};\n\nasync function main() {\n\tconst ctx = await esbuild.context({\n\t\tentryPoints: [\n\t\t\t'src/extension.ts'\n\t\t],\n\t\tbundle: true,\n\t\tformat: 'cjs',\n\t\tminify: production,\n\t\tsourcemap: !production,\n\t\tsourcesContent: false,\n\t\tplatform: 'node',\n\t\toutfile: 'dist/extension.js',\n\t\texternal: ['vscode'],\n\t\tlogLevel: 'silent',\n\t\tplugins: [\n\t\t\t/* add to the end of plugins array */\n\t\t\tesbuildProblemMatcherPlugin,\n\t\t],\n\t});\n\tif (watch) {\n\t\tawait ctx.watch();\n\t} else {\n\t\tawait ctx.rebuild();\n\t\tawait ctx.dispose();\n\t}\n}\n\nmain().catch(e => {\n\tconsole.error(e);\n\tprocess.exit(1);\n});\n";
44
+
45
+ export const ESLINT =
46
+ 'import typescriptEslint from "typescript-eslint";\n\nexport default [{\n files: ["**/*.ts"],\n}, {\n plugins: {\n "@typescript-eslint": typescriptEslint.plugin,\n },\n\n languageOptions: {\n parser: typescriptEslint.parser,\n ecmaVersion: 2022,\n sourceType: "module",\n },\n\n rules: {\n "@typescript-eslint/naming-convention": ["warn", {\n selector: "import",\n format: ["camelCase", "PascalCase"],\n }],\n\n curly: "warn",\n eqeqeq: "warn",\n "no-throw-literal": "warn",\n semi: "warn",\n },\n}];';
47
+
48
+ export const IGNORE =
49
+ ".vscode/**\n.vscode-test/**\nout/**\nnode_modules/**\nsrc/**\n.gitignore\n.yarnrc\nesbuild.js\nvsc-extension-quickstart.md\n**/tsconfig.json\n**/eslint.config.mjs\n**/*.map\n**/*.ts\n**/.vscode-test.*\n";
50
+
51
+ export const LAUNCH =
52
+ '// A launch configuration that compiles the extension and then opens it inside a new window\n// Use IntelliSense to learn about possible attributes.\n// Hover to view descriptions of existing attributes.\n// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n{\n\t"version": "0.2.0",\n\t"configurations": [\n\t\t{\n\t\t\t"name": "Run Extension",\n\t\t\t"type": "extensionHost",\n\t\t\t"request": "launch",\n\t\t\t"args": [\n\t\t\t\t"--extensionDevelopmentPath=${workspaceFolder}"\n\t\t\t],\n\t\t\t"outFiles": [\n\t\t\t\t"${workspaceFolder}/dist/**/*.js"\n\t\t\t],\n\t\t\t"preLaunchTask": "${defaultBuildTask}"\n\t\t}\n\t]\n}\n';
53
+
54
+ export const TASKS =
55
+ '// See https://go.microsoft.com/fwlink/?LinkId=733558\n// for the documentation about the tasks.json format\n{\n\t"version": "2.0.0",\n\t"tasks": [\n\t\t{\n "label": "watch",\n "dependsOn": [\n "npm: watch:tsc",\n "npm: watch:esbuild"\n ],\n "presentation": {\n "reveal": "never"\n },\n "group": {\n "kind": "build",\n "isDefault": true\n }\n },\n {\n "type": "npm",\n "script": "watch:esbuild",\n "group": "build",\n "problemMatcher": "$esbuild-watch",\n "isBackground": true,\n "label": "npm: watch:esbuild",\n "presentation": {\n "group": "watch",\n "reveal": "never"\n }\n },\n\t\t{\n "type": "npm",\n "script": "watch:tsc",\n "group": "build",\n "problemMatcher": "$tsc-watch",\n "isBackground": true,\n "label": "npm: watch:tsc",\n "presentation": {\n "group": "watch",\n "reveal": "never"\n }\n },\n\t\t{\n\t\t\t"type": "npm",\n\t\t\t"script": "watch-tests",\n\t\t\t"problemMatcher": "$tsc-watch",\n\t\t\t"isBackground": true,\n\t\t\t"presentation": {\n\t\t\t\t"reveal": "never",\n\t\t\t\t"group": "watchers"\n\t\t\t},\n\t\t\t"group": "build"\n\t\t},\n\t\t{\n\t\t\t"label": "tasks: watch-tests",\n\t\t\t"dependsOn": [\n\t\t\t\t"npm: watch",\n\t\t\t\t"npm: watch-tests"\n\t\t\t],\n\t\t\t"problemMatcher": []\n\t\t}\n\t]\n}\n';
@@ -154,7 +154,7 @@ export default defineConfig({
154
154
  "@tailwindcss/vite": "^4.3.3",
155
155
  "class-variance-authority": "^0.7.1",
156
156
  cn: "^0.2.6",
157
- "lucide-react": "^1.42.0",
157
+ "lucide-react": "^1.43.0",
158
158
  react: "^19.2.8",
159
159
  "react-dom": "^19.2.8",
160
160
  shadcn: "^4.21.0",
@@ -193,12 +193,10 @@ describe("workflows", () => {
193
193
  {
194
194
  name: "Publish package distributions to PyPI",
195
195
  uses: "pypa/gh-action-pypi-publish@release/v1",
196
- with: { "packages-dir": "release-assets/pypi/" },
196
+ with: { "packages-dir": "release-assets/pypi/", "skip-existing": true },
197
197
  },
198
198
  ]);
199
- expect(release.jobs.release.steps.at(-1)?.with.files).toBe(
200
- "release-assets/*\nrelease-assets/pypi/*",
201
- );
199
+ expect(release.jobs.release.steps.at(-1)?.with.files).toBe("release-assets/**/*");
202
200
  expect(packageSteps(python).find((step) => step.name === "web build")).toBeDefined();
203
201
  expect(packageSteps(python).find((step) => step.name === "python distributions")?.run).toBe(
204
202
  "uv build --out-dir dist/release/pypi",
@@ -648,6 +646,7 @@ describe("gate", () => {
648
646
  "toolfactory validate",
649
647
  "author checks",
650
648
  "author tests",
649
+ "author integration tests",
651
650
  "openclaw end-to-end (scripted model, no LLM key)",
652
651
  ]);
653
652
 
@@ -19,6 +19,8 @@
19
19
  */
20
20
  import { stringify as yamlStringify } from "yaml";
21
21
  import { LIVE_TEST_COMMAND } from "../bindings/python.js";
22
+ import { nativePublishJobs } from "../distribution/native.js";
23
+ import { systemPackageJobs, systemPublishJobs } from "../distribution/system.js";
22
24
  import { githubOwner } from "../identity/name.js";
23
25
  import {
24
26
  isInstructionOnly,
@@ -62,6 +64,7 @@ import {
62
64
  requiredConfig,
63
65
  } from "./shared.js";
64
66
  import { skillPath } from "./skill.js";
67
+ import { OVSX_VERSION, VSCE_VERSION, vsixName } from "./vscode-extension.js";
65
68
 
66
69
  const YAML_HEADER =
67
70
  "# Generated by toolfactory. Do not edit; run `toolfactory build` to regenerate.\n\n";
@@ -236,6 +239,7 @@ function ciDocument(project: Project): Record<string, unknown> {
236
239
  const node = typescript ? "${{ matrix.node-version }}" : "24";
237
240
  const job = compact({
238
241
  "runs-on": "ubuntu-latest",
242
+ env: { TOOLFACTORY_SYSTEM_NATIVE_JOBS: "true" },
239
243
  strategy: typescript ? { matrix: { "node-version": ["22", "24"] } } : undefined,
240
244
  steps: [
241
245
  ...toolchainSteps(project, node),
@@ -253,7 +257,7 @@ function ciDocument(project: Project): Record<string, unknown> {
253
257
  workflow_dispatch: live ? {} : undefined,
254
258
  }),
255
259
  permissions: { contents: "read" },
256
- jobs: compact({ test: job, live }),
260
+ jobs: compact({ test: job, live, ...systemPackageJobs(project, "${{ github.sha }}") }),
257
261
  };
258
262
  }
259
263
 
@@ -436,11 +440,14 @@ function releaseDocument(
436
440
 
437
441
  const cli = toolfactoryCli(project);
438
442
  const assert = tagVersionAssert(project);
443
+ const systemJobs = systemPackageJobs(project, RELEASE_SHA);
444
+ for (const job of Object.values(systemJobs)) job.needs = "gate";
439
445
  const jobs: Record<string, unknown> = {
446
+ ...systemJobs,
440
447
  gate: compact({
441
448
  "runs-on": "ubuntu-latest",
442
449
  permissions: compact({ contents: "read", pages: has(project, "web") ? "read" : undefined }),
443
- env: { RELEASE_TAG: TAG_NAME },
450
+ env: { RELEASE_TAG: TAG_NAME, TOOLFACTORY_SYSTEM_NATIVE_JOBS: "true" },
444
451
  outputs: { sha: "${{ steps.tag.outputs.sha }}", ...presenceOutputs(rows) },
445
452
  steps: [
446
453
  // The whole history: the tag may exist (a re-run) or not yet (a dispatch cuts it), and
@@ -459,7 +466,7 @@ function releaseDocument(
459
466
  ],
460
467
  }),
461
468
  package: compact({
462
- needs: "gate",
469
+ needs: needsOf(["gate", ...Object.keys(systemJobs)]),
463
470
  "runs-on": "ubuntu-latest",
464
471
  permissions: { contents: "read" },
465
472
  // The Firefox self-hosted xpi packageSteps emits (gate.ts) is opt-in on this pair: present,
@@ -472,7 +479,11 @@ function releaseDocument(
472
479
  : undefined,
473
480
  steps: [
474
481
  ...toolchainSteps(project, "24", RELEASE_SHA),
475
- ...packageSteps(project).map((step) => actionStep(step, false)),
482
+ ...packageSteps(project, { system: false }).map((step) => actionStep(step, false)),
483
+ ...Object.keys(systemJobs).map((name) => ({
484
+ uses: "actions/download-artifact@v8",
485
+ with: { name, path: `${RELEASE_DIR}/system/${name.replace(/^system-/, "")}` },
486
+ })),
476
487
  {
477
488
  uses: "actions/upload-artifact@v7",
478
489
  with: { name: RELEASE_ARTIFACT, path: `${RELEASE_DIR}/` },
@@ -480,7 +491,10 @@ function releaseDocument(
480
491
  ],
481
492
  }),
482
493
  };
483
- const priorLegs: string[] = [];
494
+ Object.assign(jobs, systemPublishJobs(project, RELEASE_ARTIFACT, RELEASE_SHA));
495
+ const nativeJobs = nativePublishJobs(project, RELEASE_ARTIFACT, RELEASE_SHA);
496
+ Object.assign(jobs, nativeJobs);
497
+ const priorLegs: string[] = Object.keys(nativeJobs);
484
498
 
485
499
  if (npmSelected) {
486
500
  // npm treats a slash-containing bare operand as a Git spec; make the downloaded archive a
@@ -532,7 +546,7 @@ function releaseDocument(
532
546
  {
533
547
  name: "Publish package distributions to PyPI",
534
548
  uses: "pypa/gh-action-pypi-publish@release/v1",
535
- with: { "packages-dir": `${RELEASE_ARTIFACT}/pypi/` },
549
+ with: { "packages-dir": `${RELEASE_ARTIFACT}/pypi/`, "skip-existing": true },
536
550
  },
537
551
  ],
538
552
  });
@@ -682,6 +696,34 @@ function releaseDocument(
682
696
  }
683
697
  }
684
698
 
699
+ for (const id of ["vscode-marketplace", "open-vsx"] as const) {
700
+ const row = rows.find((entry) => entry.id === id);
701
+ if (!row) continue;
702
+ const file = `./${RELEASE_ARTIFACT}/${vsixName(project)}`;
703
+ const publish =
704
+ id === "vscode-marketplace"
705
+ ? `npx --yes @vscode/vsce@${VSCE_VERSION} publish --packagePath ${file} --skip-duplicate`
706
+ : `npx --yes ovsx@${OVSX_VERSION} publish ${file} --skip-duplicate`;
707
+ const job = `publish-${id}`;
708
+ const runtimeJob = project.tool.binding === "python" ? "publish-pypi" : "publish-npm";
709
+ jobs[job] = {
710
+ needs: ["gate", "package", runtimeJob],
711
+ if: `!cancelled() && !contains(needs.*.result, 'failure') && ${gated(id)}`,
712
+ "runs-on": "ubuntu-latest",
713
+ permissions: { contents: "read" },
714
+ env: Object.fromEntries(row.secrets.map((key) => [key, `\${{ secrets.${key} }}`])),
715
+ steps: [
716
+ { uses: "actions/setup-node@v7", with: { "node-version": "24" } },
717
+ {
718
+ uses: "actions/download-artifact@v8",
719
+ with: { name: RELEASE_ARTIFACT, path: RELEASE_ARTIFACT },
720
+ },
721
+ { name: `Publish ${id}`, run: publish },
722
+ ],
723
+ };
724
+ priorLegs.push(job);
725
+ }
726
+
685
727
  jobs.release = {
686
728
  // The tag's own release, after every leg: the assets are the same ones the registries got.
687
729
  // Skipped legs must not skip it, only failed ones; hence the explicit condition.
@@ -719,10 +761,7 @@ function releaseDocument(
719
761
  // A tag the dispatch is cutting does not exist yet: the Release creates it here.
720
762
  target_commitish: RELEASE_SHA,
721
763
  generate_release_notes: true,
722
- // PyPI distributions live one level below the other release assets.
723
- files: has(project, "pypi")
724
- ? `${RELEASE_ARTIFACT}/*\n${RELEASE_ARTIFACT}/pypi/*`
725
- : `${RELEASE_ARTIFACT}/*`,
764
+ files: `${RELEASE_ARTIFACT}/**/*`,
726
765
  fail_on_unmatched_files: true,
727
766
  },
728
767
  },
@@ -13,7 +13,7 @@ const operations = [...authored, web];
13
13
  const program = new Command()
14
14
  .name("toolfactory")
15
15
  .description("Build an agent tool once; ship it as Agent Skills, MCP servers, Agent Plugins / Claude / Codex / Cursor bundles, OpenClaw and Hermes plugins, CLIs, and packages, with the tests and releases to match.")
16
- .version("0.1.2");
16
+ .version("0.2.1");
17
17
 
18
18
  for (const op of operations) {
19
19
  if (!serves(op, "cli")) continue;
@@ -33,7 +33,7 @@ const introspecting = Boolean(process.env.TOOLFACTORY_INTROSPECT);
33
33
 
34
34
  /** Builds one server instance with every servable operation registered; called once for stdio, once per HTTP request. */
35
35
  function createServer(): McpServer {
36
- const server = new McpServer({ name: "toolfactory", version: "0.1.2" });
36
+ const server = new McpServer({ name: "toolfactory", version: "0.2.1" });
37
37
  for (const op of operations) {
38
38
  if (!introspecting && !serves(op, "mcp")) continue;
39
39
  server.registerTool(