mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
## Summary - Rename the OpenCode config template into a versioned npm-packable CodeNomad plugin package. - Build and package the plugin through the server bundle, with Electron/Tauri carrying it via existing server resources. - Replace OPENCODE_CONFIG_DIR injection with JSONC-aware OPENCODE_CONFIG_CONTENT merging that appends the CodeNomad plugin while preserving user config. ## Validation - npm run build --workspace @codenomad/codenomad-opencode-plugin - npm run prepare-plugin --workspace @neuralnomads/codenomad - npm run typecheck --workspace @neuralnomads/codenomad - npm run typecheck --workspace @neuralnomads/codenomad-electron-app - node --import tsx --test \"src/opencode-plugin.test.ts\" \"src/workspaces/__tests__/spawn.test.ts\" ## Notes - Production plugin loading uses an explicit npm file alias for the packaged tarball. - Dev loading still references the TypeScript plugin entry directly. --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
73 lines
3 KiB
TypeScript
73 lines
3 KiB
TypeScript
import type { PluginInput } from "@opencode-ai/plugin"
|
|
import { createCodeNomadClient, getCodeNomadConfig } from "./lib/client.js"
|
|
import { createBackgroundProcessTools } from "./lib/background-process.js"
|
|
|
|
let voiceModeEnabled = false
|
|
|
|
export async function CodeNomadPlugin(input: PluginInput): Promise<{
|
|
tool: ReturnType<typeof createBackgroundProcessTools>
|
|
"chat.message": CodeNomadChatMessageHook
|
|
event: CodeNomadEventHook
|
|
}> {
|
|
const config = getCodeNomadConfig()
|
|
const client = createCodeNomadClient(config)
|
|
const backgroundProcessTools = createBackgroundProcessTools(config, { baseDir: input.directory })
|
|
|
|
await client.startEvents((event) => {
|
|
if (event.type === "codenomad.ping") {
|
|
void client.postEvent({
|
|
type: "codenomad.pong",
|
|
properties: {
|
|
ts: Date.now(),
|
|
pingTs: (event.properties as any)?.ts,
|
|
},
|
|
}).catch(() => {})
|
|
return
|
|
}
|
|
|
|
if (event.type === "codenomad.voiceMode") {
|
|
voiceModeEnabled = Boolean((event.properties as { enabled?: unknown } | undefined)?.enabled)
|
|
}
|
|
})
|
|
|
|
return {
|
|
tool: {
|
|
...backgroundProcessTools,
|
|
},
|
|
async "chat.message"(_input: { sessionID: string }, output: { message: { system?: string } }) {
|
|
if (!voiceModeEnabled) {
|
|
return
|
|
}
|
|
|
|
output.message.system = [output.message.system, buildVoiceModePrompt()].filter(Boolean).join("\n\n")
|
|
},
|
|
async event(input: { event: any }) {
|
|
const opencodeEvent = input?.event
|
|
if (!opencodeEvent || typeof opencodeEvent !== "object") return
|
|
|
|
},
|
|
}
|
|
}
|
|
|
|
type CodeNomadChatMessageHook = (
|
|
_input: { sessionID: string },
|
|
output: { message: { system?: string } },
|
|
) => Promise<void>
|
|
|
|
type CodeNomadEventHook = (input: { event: any }) => Promise<void>
|
|
|
|
function buildVoiceModePrompt(): string {
|
|
return [
|
|
"Voice conversation mode is enabled.",
|
|
"Prepend your reply with a fenced code block using language `spoken`.",
|
|
"The `spoken` block should be the natural conversational reply you would say out loud to the user. It should be a concise spoken gist of the full response in 2 to 4 natural sentences.",
|
|
"In the spoken block, summarize the main outcome, recommendation, or next step. Sound conversational and natural, not like a document summary.",
|
|
"Do not include code, bullet lists, markdown formatting, or long technical detail in the spoken block.",
|
|
"Do not add generic phrases about whether the user should read more.",
|
|
"Only mention additional written detail when there is something specific that may matter for the user's next response, such as a tradeoff, caveat, risk, open question, exact diff, or test result.",
|
|
"When referring to that written detail, say `below` or `in the message` rather than `detailed section`.",
|
|
"After the `spoken` block, continue with your normal detailed response.",
|
|
"Example:",
|
|
"```spoken\nI implemented the relay-based voice-mode flow and it works with the current plugin bridge. The reconnect caveat is explained below.\n```",
|
|
].join("\n\n")
|
|
}
|