diff --git a/.gitignore b/.gitignore index 3b4a272..3afd1fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ node_modules/ dist/ -# keep published client artifact; ignore other build junk under lib/ +# keep published host + client artifacts lib/* +!lib/index.js !lib/client.js *.log .DS_Store diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..1d2ff51 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,100 @@ +// src/index.ts +import z from "@deepseek-ai/schemastery"; +import { credentialRef } from "@deepseek-ai/dsh-credentials"; +import * as McpClient from "@deepseek-ai/dsh-mcp-client"; +var name = "netxops"; +var inject = ["tools"]; +var NETXOPS_SETTINGS_NAMESPACE = "netxops"; +var DEFAULT_TOKEN_REF = "NETX_API_TOKEN"; +var Config2 = z.object({ + apiUrl: z.string().default("http://127.0.0.1:8890"), + lang: z.string().default("zh"), + pythonCommand: z.string().default("python"), + tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF), + toolCallTimeoutMs: z.number().step(1).min(1000).default(120000), + failOnStartupError: z.boolean().default(false) +}); +async function resolveToken(ctx, refName) { + const credentials = ctx.get("credentials"); + if (credentials === undefined) + return ""; + const hit = await credentials.resolve(credentialRef(refName)); + return hit?.value ?? ""; +} +function apply(ctx, config = Config2({})) { + let source = () => config; + let mcpFiber; + let remounting = Promise.resolve(); + let generation = 0; + const remount = () => { + remounting = remounting.then(async () => { + const gen = ++generation; + const previous = mcpFiber; + mcpFiber = undefined; + if (previous !== undefined) { + try { + await previous.dispose(); + } catch (error) { + ctx.logger.warn("netxops: disposing previous mcp-client failed: %s", error); + } + } + if (gen !== generation) + return; + const current = source(); + const token = await resolveToken(ctx, current.tokenCredentialRef); + if (gen !== generation) + return; + const mcpConfig = McpClient.Config({ + transport: "stdio", + serverName: "netx", + command: current.pythonCommand, + args: ["-m", "netx_mcp"], + env: { + NETX_API_URL: current.apiUrl.replace(/\/$/, ""), + NETX_API_TOKEN: token, + NETX_LANG: current.lang + }, + toolCallTimeoutMs: current.toolCallTimeoutMs, + failOnStartupError: current.failOnStartupError + }); + try { + mcpFiber = await ctx.plugin(McpClient, mcpConfig); + } catch (error) { + ctx.logger.error("netxops: failed to mount mcp-client: %s", error); + if (current.failOnStartupError) + throw error; + } + }).catch((error) => { + ctx.logger.error("netxops: remount error: %s", error); + }); + }; + remount(); + ctx.inject(["settings"], (settingsCtx) => { + settingsCtx.settings.installSection(ctx, NETXOPS_SETTINGS_NAMESPACE, Config2, config, { + setSource: (current) => { + source = current; + }, + onChange: () => { + remount(); + } + }); + }); + ctx.on("credentials/reference-updated", (ref) => { + if (String(ref) === source().tokenCredentialRef) + remount(); + }); + ctx.effect(() => () => { + generation += 1; + const fiber = mcpFiber; + mcpFiber = undefined; + fiber?.dispose(); + }, "netxops: dispose mcp-client"); +} +export { + name, + inject, + apply, + NETXOPS_SETTINGS_NAMESPACE, + DEFAULT_TOKEN_REF, + Config2 as Config +}; diff --git a/package.json b/package.json index 0d3719f..179351b 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { "name": "dsh-netxops", - "version": "0.1.4", + "version": "0.1.5", "description": "DeepSeek Harness Netx Ops: host bridge + Plugins settings card + agent preset", "license": "MIT", "type": "module", "private": false, - "main": "./src/index.ts", + "main": "./lib/index.js", "exports": { - ".": "./src/index.ts", + ".": "./lib/index.js", "./client": "./lib/client.js", "./package.json": "./package.json" }, "files": [ "src/", + "lib/index.js", "lib/client.js", "presets/", "cordis.patch.yml", @@ -39,9 +40,10 @@ }, "homepage": "https://github.com/hansjone/netxops#readme", "scripts": { - "bundle": "bun run scripts/build-client.mjs" - }, - "dsh": { + "bundle": "bun run scripts/build-host.mjs && bun run scripts/build-client.mjs", + "bundle:host": "bun run scripts/build-host.mjs", + "bundle:client": "bun run scripts/build-client.mjs" + }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, diff --git a/scripts/build-host.mjs b/scripts/build-host.mjs new file mode 100644 index 0000000..da08cd1 --- /dev/null +++ b/scripts/build-host.mjs @@ -0,0 +1,40 @@ +/** + * Build lib/index.js (host Cordis plugin) for installs under node_modules. + * Node refuses to strip TypeScript inside node_modules, so GitHub/npm installs + * must ship prebuilt JS. Usage: bun run scripts/build-host.mjs + */ +import { mkdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const entry = join(root, 'src/index.ts') +const outFile = join(root, 'lib', 'index.js') + +const external = [ + '@deepseek-ai/cordis', + '@deepseek-ai/schemastery', + '@deepseek-ai/dsh-credentials', + '@deepseek-ai/dsh-settings', + '@deepseek-ai/dsh-mcp-client', + '@deepseek-ai/dsh-tools', +] + +const result = await Bun.build({ + entrypoints: [entry], + target: 'node', + format: 'esm', + sourcemap: 'none', + minify: false, + external, +}) + +if (!result.success) { + console.error(result.logs) + throw new Error('Bun.build host failed') +} + +const text = await result.outputs[0].text() +mkdirSync(dirname(outFile), { recursive: true }) +writeFileSync(outFile, text, 'utf8') +console.log(`wrote ${outFile} (${text.length} bytes)`)