Files
deepseek-harness/packages/experimental/claude-code-mods/src/define-mod.ts
T
Tianyi Cui 233032b8f4 fix(experimental): close the mods bridge review findings
- Disposal cancels detached runs before the engine waits for timer callbacks,
  so a callback parked in a $ call ends instead of blocking unload.
- A forgotten band wakes and ends its watch streams; a refresh asked for
  during a pass joins one more pass that settles after it; the Web band drops
  an ended stream so the next subscriber opens a fresh one.
- $ has Claude Code's namespaces: an unserved member rejects with
  'no implementation for <namespace>.<member>' instead of a TypeError.
- A .catch handler's next makes the rewrite check; a tool rename is refused
  like an argument rewrite; a null op answer is reported as no answer;
  defineMod returns its disposer from apply; the live turn record is copied
  before the chain freezes it; prompt.submit is raised only for batches with
  a human message and names the submitting mod as origin.
- $.http.fetch bounds the body while streaming; bandColumns/bandRows are
  Config; the client package drops its stale gateway edges; the source
  overlay lives in the package and anchors its entries to the file, and the
  dsh installation no longer depends on the experimental packages.
2026-10-03 05:18:32 +08:00

66 lines
2.8 KiB
TypeScript

/**
* `defineMod`: wrap a Claude Code mod's `register(on, options)` as a DSH
* plugin. Mounting the plugin registers the mod with the bridge service
* (`ctx.claudeCodeMods`); unmounting it removes the mod's hooks, timers, and
* registrations. Mods load in composition order, which is their chain order.
* @module
*/
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import type { ModDefinition, ModRegister, PluginOptions } from './types.ts'
/** The option values a composition may set for a mod: `userConfig`'s value types. */
export type ModConfig = Record<string, string | number | boolean | string[]>
/** What a mod's plugin declares: the plugin identity and the hooks module's `register`. */
export interface ModSpec {
/** Plugin name, as `.claude-plugin/plugin.json` names it: letters, digits, `_` and `-`. */
readonly name: string
readonly version?: string
/** Absolute directory the mod ships in, reported by `$.plugin.root`. */
readonly root?: string
/** `userConfig` defaults: the `options` `register` receives when the composition sets none. */
readonly userConfig?: PluginOptions
readonly register: ModRegister
}
/** A mod wrapped as a DSH plugin; its config is the mod's `options`. */
export interface ModPlugin {
readonly name: string
readonly inject: readonly string[]
readonly Config: z<ModConfig>
/** The mod as the bridge and the test kit load it, with `userConfig` as its options. */
readonly definition: ModDefinition
apply(ctx: Context, config: ModConfig): Promise<() => Promise<void>>
}
const optionValue = z.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
/**
* Wrap one mod as a DSH plugin.
* @param spec - the mod's identity, `userConfig` defaults, and `register`.
* @returns a plugin object for `cordis.yml` or `ctx.plugin`; its `config` overlays `userConfig` as `register`'s `options`.
*/
export function defineMod(spec: ModSpec): ModPlugin {
const userConfig: PluginOptions = Object.freeze({ ...spec.userConfig })
const definition: ModDefinition = Object.freeze({
name: spec.name,
...spec.version === undefined ? {} : { version: spec.version },
...spec.root === undefined ? {} : { root: spec.root },
options: userConfig,
register: spec.register,
})
return {
name: `claude-code-mod-${spec.name}`,
inject: ['claudeCodeMods'],
Config: z.dict(optionValue).default({}),
definition,
// A `register` that throws fails this plugin's mount, as any misconfigured plugin does; the rest of the composition continues.
// The disposer is the return value, which Cordis collects even when the plugin was removed while `register` ran.
apply(ctx: Context, config: ModConfig): Promise<() => Promise<void>> {
return ctx.claudeCodeMods.add({ ...definition, options: Object.freeze({ ...userConfig, ...config }) })
},
}
}