Files
zeekay d4d300d940 Serve Hanzo models to VS Code's model picker
A language model chat provider lists the models GET /v1/models marks as
owned by Hanzo that answer in text, and streams each request through
POST /v1/chat/completions on api.hanzo.ai with tool calls. Both carry the
extension's Hanzo IAM session, and the list is asked for again when the
person signs in or out.
2026-09-30 14:57:13 -07:00

90 lines
3.2 KiB
JavaScript

// Bundles the two files the .vsix runs: dist/extension.js (package.json
// `main`, loaded by the extension host) and dist/mcp-server.js (the stdio MCP
// server src/mcp/server.ts spawns on the extension host's runtime). vsce
// packages with --no-dependencies, so each bundle carries every module it
// needs; the .vsix has no node_modules.
const esbuild = require('esbuild');
// `vscode` is left as a runtime require(): the extension host supplies it, and
// mcp-server.js gets VSCODE_MOCK.
const common = {
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node18',
minify: true,
logLevel: 'warning',
define: { 'process.env.NODE_ENV': '"production"' },
};
// The vscode API the tools touch, for a server that runs outside VS Code. The
// extension sets HANZO_WORKSPACE to its first workspace folder and HANZO_CONFIG
// to its hanzo.* settings as JSON, which getConfiguration reads as VS Code does.
const VSCODE_MOCK = `
const fs = require('fs').promises;
const settings = { hanzo: JSON.parse(process.env.HANZO_CONFIG || '{}') };
module.exports = {
workspace: {
workspaceFolders: process.env.HANZO_WORKSPACE ? [{ uri: { fsPath: process.env.HANZO_WORKSPACE } }] : undefined,
getConfiguration: (section) => ({
get: (key, defaultValue) => {
const value = [...(section ? section.split('.') : []), ...key.split('.')].reduce((node, k) => node?.[k], settings);
return value === undefined ? defaultValue : value;
}
}),
findFiles: async () => [],
fs: {
readFile: async (uri) => Buffer.from(await fs.readFile(uri.fsPath || uri)),
writeFile: async (uri, content) => fs.writeFile(uri.fsPath || uri, content),
createDirectory: async (uri) => fs.mkdir(uri.fsPath || uri, { recursive: true })
}
},
window: {
showErrorMessage: console.error,
showInformationMessage: console.error,
visibleTextEditors: [],
createWebviewPanel: () => null,
showTextDocument: () => null,
createOutputChannel: (name) => ({
appendLine: (text) => console.error('[' + name + '] ' + text),
append: (text) => process.stderr.write(text),
clear: () => {},
dispose: () => {},
hide: () => {},
show: () => {}
})
},
env: { openExternal: async () => true },
EventEmitter: class { constructor() { this.event = () => ({ dispose() {} }); } fire() {} dispose() {} },
Uri: { file: (p) => ({ fsPath: p }), parse: (s) => ({ fsPath: s }) },
ViewColumn: { One: 1 },
version: '1.0.0',
commands: { executeCommand: async () => [] },
SymbolKind: { Function: 11, Class: 4, Method: 5, Variable: 12, Constant: 13, Interface: 10 }
};
`;
const vscodeMock = {
name: 'vscode-mock',
setup(build) {
build.onResolve({ filter: /^vscode$/ }, () => ({ path: 'vscode', namespace: 'vscode-mock' }));
build.onLoad({ filter: /.*/, namespace: 'vscode-mock' }, () => ({ contents: VSCODE_MOCK, loader: 'js' }));
},
};
Promise.all([
esbuild.build({
...common,
entryPoints: ['src/extension.ts'],
outfile: 'dist/extension.js',
external: ['vscode'],
}),
esbuild.build({
...common,
entryPoints: ['src/mcp-server.ts'],
outfile: 'dist/mcp-server.js',
plugins: [vscodeMock],
}),
]).catch(() => process.exit(1));