DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
K

@qianmo978/dsh-kilo-gateway

Kilo Gateway

将 Kilo Code Gateway 作为官方模型提供商:提供专用的 API 密钥设置页面、设备登录、匿名免费层级、按模型区分的上下文窗口、支持自我修复重试的流式聊天,以及账户、补全、嵌入和通知工具。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:qianmo978/dsh-kilo-gateway#96eb526dcf7f655d2829e0f0b9c84a15c896b8b6
README兼容性版本

兼容性与来源证明

Kilo Gateway 以 @qianmo978/dsh-kilo-gateway 发布,当前版本为 1.9.0。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/10/22

版本

1.9.0stable
2026/10/22

相关插件

正在加载相关插件…

最新版
1.9.0
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
未声明
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/10/22
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 models-usage 分类下经过校验的插件。

Experimental Cot Translation Bundle@deepseek-ai/dsh-experimental-cot-translation-bundle扩展推理的实验性机器翻译Whale Widgetdsh-whale-widgetDSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义Our Free Model@ebony-vinyl/dsh-our-free-modelOur Free Model — 在 dsh 里装上这个插件即可,无需登录、注册或填 API Key,就能使用包括 DeepSeek V4.1 Flash、Kimi K3 在内的前沿模型,桌面端另有 EAC 协付渠道;2.0 起接入 LobsterAI、Qoder、TRAE、Cline、Loomy、Raccoon、MiniMax、ZCode、Gemini 十个账号渠道(原有匿名免费通道保留)(登录、账号池、积分领取、本地 OpenAI 网关与局域网转发、数据看板与运行日志),六页毛玻璃界面统一呈现。Usage@linxin666/dsh-usage用于 dsh Web GUI 的使用统计插件:检测各提供商的余额和编程套餐配额,并实时记录令牌使用情况;侧边栏 Settings 行上方显示紧凑的价格概览卡片

README

Kilo Gateway

A DeepSeek Harness plugin that turns the Kilo Code Gateway into a first-party model provider, ported from @kilocode/kilo-gateway (MIT).

Authentication

Three sources, in precedence order:

OrderSourceNotes
1Credential storeWritten by this bundle's own settings page, or by kilo_login. The normal path.
2KILO_API_KEYAmbient environment, for a shell or CI.
3KILOCODE_TOKENOlder device/session token.
4AnonymousNothing configured. Bearer anonymous, free models only, 200 requests/hour per IP.

KILOCODE_ORGANIZATIONID sets X-KiloCode-OrganizationId for organization-scoped requests.

Entering the key

The key is entered on this bundle's own page in the Settings panel — Settings → Kilo Gateway — which the browser half of this bundle contributes. The page writes the secret through the generated remote.credentials namespace (the same surface the built-in Models page uses) under the KILO_API_KEY reference, so the value lands in the credential store and never in a patch file.

Why a page of its own, instead of the Models settings card: the Models page is not a generic schema renderer. Its editor picks one of exactly two curated layouts from the settings namespace, and renders nothing but a hint for any other:

function layoutOf(ns) {
  if (ns === 'llm-deepseek') return 'deepseek'
  if (ns === 'llm-pi-ai')   return 'pi-ai'
  return 'unknown'   // <- "Other fields live in cordis.patch.yml" and no inputs at all
}

A private namespace can therefore never show an API-key input there, whatever a plugin's own Config schema marks as role('credential-ref'). That marker only tells a renderer how to treat a field it is already being asked to render; it does not enroll a namespace into the curated families. So this bundle does not declare itself into the Models directory at all: it serves the kilo route with its own adapter (below) and puts the key form where a form is actually renderable.

Three ways to supply the key

  1. Settings page — open Settings, pick Kilo Gateway, and paste the key into the secret field. This is the intended path. The secret is stored under the KILO_API_KEY reference.
  2. Reuse an existing reference — set apiKeyEnv to a name you already store the key under, and the plugin reads it in place. No copy is made.
  3. Environment — KILO_API_KEY or KILOCODE_TOKEN, layered under the store, for a shell or CI.

A configured reference that is blank, unknown, or not a legal reference name is skipped rather than raising, so a typo degrades to the next source instead of breaking dispatch.

The browser half's contract

The client loader is strict, and a violation of any of these rules fails the whole client boot (web boot: 1 entry did not activate), which takes the entire web shell down — not just this plugin's page. The rules, each taken from the built-in Models page (dsh-client-ui-settings-models), which registers into the same slot and works:

RuleConsequence of getting it wrong
The plugin's inject names full service keys (remote.credentials, not just remote).An undeclared service throws inside apply(), and the fiber ends in state failed.
A namespace is read through the declared property, never through ctx.get.The client context exposes a service only when the plugin declared it, and the remote namespaces are Typert remotes rather than cordis Services — ctx.get('remote') finds nothing, and the page silently shows "credential store unavailable".
The manifest's dsh.client.inject names the packages that provide those services (@deepseek-ai/dsh-api-remotes, @deepseek-ai/dsh-client-locale, @deepseek-ai/dsh-client-ui-renderer).The bundle is composed before its providers exist, and the services never arrive.
The manifest's dsh.client carries platform and inject, and no immediately.immediately: true is legal, but it makes the entry's activation part of the boot's critical path.
ctx.locale.register(ns, { zh, en }) takes one dictionary per language in a single call.The three-argument register(ns, locale, dict) form is a different overload with different rules.
Every contribution is guarded at both boundaries: the eager ctx.slots.inject(...) call, and the deferred callback the slot runtime invokes later.A throw in the deferred callback happens long after apply() returned, where no enclosing try/catch can catch it.
The slot registration options are { name, id, order, label }, and label is a thunk.A shape break here throws synchronously inside slots.inject.

The slots service notes one more trap, quoted from a sibling plugin:

Callback effects are synchronous disposers; iterable effects install transactionally and dispose in reverse order.

so the slots.inject callback must return the disposer slots.register hands back — never undefined.

What this plugin depends on

Only services the Harness itself ships. @deepseek-ai/dsh-api-remotes is a protected core module: it is a dependency of the app itself, and the plugin hub refuses to let anyone toggle it off, so every host that has a settings panel also has the credential remote. There is no dependency on a plugin that can be uninstalled, and nothing in this bundle reads or writes any file outside the profile's own credential store.

No external package. The bundle imports nothing but Node builtins and, in the browser half, react (which the web shell's module table provides). The schema library is vendored under vendor/, and tools/verify-installable.mjs enforces that rule.

Why the schema library is vendored

A plugin that imports @deepseek-ai/schemastery fails to activate on most machines:

failed to import: Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@deepseek-ai/schemastery'

The Harness routes a plugin's @deepseek-ai/* imports to the host's own copy — but only for the packages the host ships as host-core:

for (const entry of readdirSync(join(hostNodeModulesRoot(dir), '@deepseek-ai'))) {
  if (/^(?:dsh|cordis)/.test(entry.name)) names.add(`@deepseek-ai/${entry.name}`)
}

schemastery matches neither prefix, so it is never routed. It has to be resolvable from the plugin's own directory, the way Node resolves it.

A plugin installed with link: — the form the Harness documents, dsh plugin add "link:$PWD" — has no node_modules of its own: pnpm does not install the dependencies of a linked package. So the bundle activates on a machine where some other plugin happens to have installed schemastery, and fails everywhere else. Declaring it as a dependency does not help, for the same reason: the dependency is never installed.

The fix is to carry what the bundle imports:

vendor/schemastery/lib/index.mjs   # @deepseek-ai/schemastery 3.18.4, MIT
vendor/cosmokit/lib/index.js       # @deepseek-ai/cosmokit 1.8.5, MIT

tools/vendor-schemastery.mjs is the record of where they came from: it re-vendors from a local install, rewrites schemastery's bare @deepseek-ai/cosmokit import to a relative path, and --check verifies the copies are current. Both upstream LICENSE files are kept.

The host half's contract

The adapter is handed a GenerateOptions object by the Harness. Its shape is fixed by the shipped @deepseek-ai/dsh-llm, and one field in it is easy to get wrong:

interface GenerateOptions {
  provider: string
  model: string
  messages: RequestMessage[]
  system?: string
  tools?: ToolSchema[]
  toolHistory?: ToolHistory      // an OBJECT, not an array
  temperature?: number
  maxTokens?: number
  stop?: string[]
  signal?: AbortSignal
  purpose?: 'compaction' | 'session-title'
}

interface ToolHistory {
  readonly tools: readonly ToolSchema[]
  readonly updates: readonly { readonly messageId: MessageId; readonly additions: readonly ToolSchema[] }[]
}

toolHistory is that object. Reading it as an array of updates throws toolHistory is not iterable on every dispatch that carries a tool history — and a toolHistory.length === 0 guard does not catch the mistake, because the length of an object is undefined, so the comparison is false and the loop runs anyway.

The host reports that failure as LlmError: toolHistory is not iterable with code UNKNOWN, thrown from ReactLoopAgent.step. That stack is where the host re-throws an adapter failure, not where the failure originated: the host wraps a non-LlmError as { message: errorChain(error), code: 'UNKNOWN' }. A bare TypeError message with code UNKNOWN therefore points at the adapter, and the real throw site is wherever the plugin read the option.

Two more traps in the same interface:

  • streamCall(options, model) receives the model as a string id, not a model object. A model object stringifies to [object Object] in every message that mentions the model.
  • ToolSchema carries a deferLoading flag that is a Harness loading strategy, not provider wire content. It must not be forwarded to the provider.

The stream chunks the adapter emits

The adapter yields StreamChunks, and the host validates each one as it arrives:

type StreamChunk =
  | { type: 'block-start'; index: number; blockType: ContentBlockType }
  | { type: 'text-delta'; index: number; text: string }
  | { type: 'reasoning-delta'; index: number; text: string }
  | { type: 'tool-call-delta'; index: number; id: ToolCallId; name?: string; argumentsDelta: string }
  | { type: 'block-end'; index: number; block: ContentBlock }
  | { type: 'usage'; usage: TokenUsage }
  | { type: 'finish'; reason: FinishReason; replayState?: ReplayEnvelope }

The id on a tool-call-delta is required, and required on every delta:

if (typeof chunk.id !== "string") throw new TypeError("tool-call-delta id must be a string")

An OpenAI-shaped stream sends the id only on the first chunk of a call and carries nothing but arguments on the rest, so a delta that forwards the provider's id as-is omits it from every continuation chunk and the host rejects the stream. The adapter therefore remembers the id per block and repeats it, deriving a stable one for a provider that never sent any — the same id the finished tool-call block carries, so the deltas and the block agree.

The host's assembly is more forgiving than its validation: it falls back to brandString('call-' + index) when no id was seen. That fallback is unreachable for a provider that streams a tool call in more than one chunk, because the validation rejects the second chunk first.

Keeping a long-lived install working

A credential that worked once can stop working: a key gets rotated, a device grant expires, or a token is revoked out of band. Failing identically on every later turn would make the provider unusable until the process restarted, so a rejected credential is demoted rather than re-tried forever:

  • A token that comes back 401/403 is remembered as bad for ten minutes and skipped on later dispatches, so a stale KILO_API_KEY in the environment stops shadowing a valid stored grant.
  • A successful sign-in or key write clears that memory immediately.
  • The model catalogue is cached for fifteen minutes rather than per request, and a failed refresh keeps the previous catalogue, so a transient outage degrades to slightly stale metadata instead of an empty model list.

Anonymous vs. keyed

Anonymous access is genuinely usable: the plugin filters the catalog down to models it can actually reach, so kilo-auto/free and the :free ids are selectable without any credential. Selecting a paid model while anonymous fails immediately with a clear AUTH error naming the fix, instead of a bare 401 from the gateway.

Note Kilo's own warning about kilo-auto/free: it may route to providers that log prompts and use them for training. Do not send confidential data through the free tier.

Credential records

The record half of the credentials seam is a discriminated union, not a free-form bag:

type CredentialRecord = ApiKeyRecord | GrantRecord
interface ApiKeyRecord { kind: 'api-key'; key?: string; env?: Record<string,string> }
interface GrantRecord  { kind: 'grant';   payload: unknown }

Two consequences the plugin is careful about:

  • A record written without a kind passes the write guard (assertStorableApiKey only inspects key/env) and then throws at the next boot (record "..." has no kind), failing the credentials plugin's activation for the whole profile. describeRecord still reports configured: true in the meantime, so the mistake is silent until restart.
  • Readers must check kind === 'api-key'; a grant record is valid and configured but carries no readable secret.

Tokens are therefore written as { kind: 'api-key', key: token } and read with a kind check.

Per-model context windows

Every Kilo model declares its own context_length and top_provider.max_completion_tokens, and they differ by orders of magnitude — a 1M-token router sits next to a 256K one. Getting these into the harness correctly is what stops the gateway rejecting a request with an opaque 400.

resolveModel reports each model's real capacity in the exact shape the runtime validates:

{
  provider, id, name,
  inputModalities: ['text', 'image'],
  context: { contextWindow: 1_000_000 },   // nested — a top-level contextWindow is ignored
  defaultMaxTokens: 65_536,                // not `maxTokens`
}

@deepseek-ai/dsh-compaction-basic then compacts proactively: on every step it reads context.contextWindow, budgets contextWindow − reservedCompletionTokens − 65536 headroom, and summarizes once measured history passes min(window × 0.8, that budget).

Two consequences drive lib/limits.js:

  1. A window too small to absorb the harness's 64K headroom would make compaction throw. The error is caught and logged, and the turn continues with the request untruncated until the provider rejects it. So limitsOf never reports a window below MINIMUM_REPORTABLE_CONTEXT_WINDOW (98 304), and caps the output reservation to leave both the headroom and a 32 768-token pressure floor. declaredContextWindow keeps the true catalog figure alongside.
  2. The output cap is derived, not squeezed per-call. The harness already reserved output while compacting, so max_tokens is the model's own cap, trimmed only if the assembled request would otherwise overflow.

The pressure floor matters for a specific real case: mistralai/devstral-2512 declares a 209 715-token completion cap against a 262 144-token window. Clamping output to window − headroom alone lands the reservation at exactly 65 536, leaving pressureBudget = 0 and making compaction throw — the very failure the accounting exists to prevent. Reserving 32 768 on top keeps it positive.

compactionBudget(limits) mirrors the harness arithmetic, so you can check whether a given model will actually compact rather than discovering it from a failed turn.

listModels deliberately carries no capacity fields: the runtime validates that projection as { provider, id, name, description?, inputModalities? } and silently drops anything else.

Streaming and reasoning

The SSE reader is line-based, not blank-line-split: SSE permits \n, \r\n, or \r terminators, and Kilo's gateway streams CRLF. A naive indexOf('\n\n') finds no boundary in that stream and silently yields nothing, which reads downstream as an empty response. Frames are terminated by an empty line, consecutive data: lines are joined with \n per spec, and a stream that ends without its terminating blank line is flushed.

Kilo sends reasoning twice — a plain delta.reasoning string and a structured reasoning_details array mixing readable reasoning.text entries with encrypted reasoning.encrypted ones. The adapter prefers the string and falls back to the readable entries.

An answer-less response is therefore not automatically a failure, and is classified by what actually arrived:

OutcomeReported as
Reasoning arrived, answer did notfinish with kind max-tokens
Provider billed output but streamed nothing readableMALFORMED_RESPONSE
Entirely empty streamEMPTY_RESPONSE (retried)

Token estimation is deliberately pessimistic (3 characters per token); over-estimating only costs margin, under-estimating causes the failure this exists to prevent.

Block lifecycle and usage

Every opened block is closed with block-end carrying the finished ContentBlock, and token usage is emitted as its own usage chunk rather than attached to finish. Both matter:

  • block-end is the only way a block's content reaches the session log. A stream that never closes its blocks streams text the user can see but that never becomes history, so the agent loop loses the reply.
  • Usage arrives after the terminal finish_reason on OpenRouter-compatible streams. The adapter therefore records the reason and keeps reading until the stream actually ends, then emits blocks, usage, and finish in that order — otherwise every such response reports zero tokens.

Failure handling

lib/resilience.js splits failures into two families, because they need opposite treatment.

Parameter faults are deterministic. The same body is rejected the same way every time, so replaying it verbatim only wastes money. Each carries an exact repair:

FaultRepair
Context length exceededTrim the oldest half of the history, keeping the system message
Tool definitions rejectedDrop the tool declarations
Images rejected by a text-only modelStrip image parts, keep every message
A named invalid fieldDrop that field
Any other validator rejectionTrim history

Repairs only ever remove something, which is what makes replaying one safe: removal cannot introduce a rejection the original body did not already have. The budget is two rounds by default (repairBudget), because a request needing three different repairs is one the agent should see rather than keep paying for.

Stream faults are stochastic. A reset socket, a truncated body, a 5xx, or a stalled connection is retried. Retries are silent: nothing is printed, nothing reaches the agent, and no error is logged unless every attempt fails.

What decides whether a retry is acceptable is whether the user is already looking at an answer:

StateAction
Nothing emitted yetRetry silently — the failure is invisible
Text already streamedNever restart it; keep the partial answer
A tool call already emittedNever retry — a second call could duplicate a side effect

A caller cancel is never retried and is rethrown as the caller's own error, so the harness sees a cancellation rather than a mislabelled gateway failure. A stream that delivers nothing for streamIdleTimeoutMs is treated as stalled, so a wedged connection surfaces as a retry rather than a long freeze.

MALFORMED_RESPONSE is deliberately excluded from retryableCodes: the provider billed output it never sent, so replaying would spend the same money for the same empty result.

What it does

  • Provider route kilo — registered on ctx.llm with this bundle's own adapter, which speaks the OpenRouter-compatible wire protocol exposed at https://ai-gateway.kilo.ai/api/openrouter (chat completions, streaming SSE, tool calls, reasoning deltas, usage accounting, self-healing retries).
  • Dedicated settings page — the browser half contributes a "Kilo Gateway" page in the Settings panel holding the API-key form, because the Models page cannot render one for a private namespace.
  • Device-authorization sign-in — a flow on the authorization seam keyed to the credential record it writes, so the Kilo token lands in the Harness credential store and every surface that renders sign-in flows shows "Kilo Gateway" beside the others.
  • Agent tools — account state, code completion, the embedding catalogue, and notifications. See below.
  • Anonymous fallback — the route is registered before sign-in, so a session can select kilo immediately and run on Kilo's free tier.

Agent tools

ToolPurpose
kilo_accountProfile, selected organization, Kilo Pass credits, coding-plan subscriptions and quotas, BYOK entries
kilo_completeCode completion via Kilo FIM (fim/completions) or Next Edit (edit), with prompt/suffix
kilo_embedding_modelsThe embedding catalogue Kilo advertises, with dimensions and score thresholds
kilo_notificationsAccount notifications: plan changes, credit grants, service incidents
kilo_loginDevice-code sign-in when the agent needs to renew a token

kilo_complete defaults to inception/mercury-next-edit and accepts kilo/mistralai/codestral-2508 and kilo/inception/mercury-edit-2. The direct-BYOK autocomplete models (Mistral, Inception) are listed upstream but deliberately not routable here: they need the caller's own provider key, not the Kilo Gateway credential, so the tool refuses them with provider_not_supported.

Sign-in

kilo_login needs an existing credential to authorize the device-code request, exactly like kilo_login in the Harness's own OAuth tool: it renews a token that exists but was rejected, and cannot create a first credential. For a first-time sign-in, use the Kilo Gateway entry on the account page — the authorization flow is what writes the first token.

Why it does not import @deepseek-ai/dsh-*

A bundle resolves its peer dependencies to its own copies under the profile's node_modules. Importing @deepseek-ai/dsh-llm for LlmAdapter/resolveRetryPolicy, or @deepseek-ai/dsh-tools for defineTool, would give this package a different Service/Context class identity than the one the Loader is running, so Context.is() inside TypertRemoteService fails and activation dies with Cannot read properties of undefined (reading 'ctx').

The adapter therefore duck-types the LlmAdapter surface and reconstructs the two things it needs structurally (KiloLlmError carrying a stable code, and DEFAULT_RETRY_POLICY). Tool definitions are written as raw JSON Schema, which is what ctx.tools.register validates directly. ctx.llm, ctx.authorization, ctx.credentials, and ctx.tools are reached through the context, not through imports. This is the same shape the published @opencode2dsh/dsh-plugin bundle uses.

Layout

FileRole
index.jsHost half: registers the kilo adapter, the auth flow, and the tools
client.jsBrowser half: the "Kilo Gateway" settings page holding the API-key form
lib/constants.jsEndpoints, headers, timeouts, defaults
lib/url.jsGateway / OpenRouter / API base-URL resolution
lib/headers.jsX-KILOCODE-* attribution headers
lib/token-store.jsToken and organization id over the credentials seam
lib/api.jsProfile, balance, device authorization, embedding catalogue
lib/models.jsChat model catalog
lib/adapter.jsThe chat adapter: streaming, block lifecycle, usage, self-healing retries
lib/resilience.jsFailure classification, repairs, and the self-healing policy
lib/authorization.jsThe device-code sign-in flow
lib/account.jsKilo Pass, coding-plan quotas, BYOK, notifications
lib/fim.jsAutocomplete model catalogue and FIM / Next Edit requests
lib/limits.jsPer-model context windows, free-tier gating, request fitting
lib/tools.jskilo_account, kilo_complete, kilo_embedding_models, kilo_notifications
lib/login-tool.jskilo_login
tools/verify.mjsOffline verification of the streaming and resilience contracts

An earlier revision of this bundle served chat through @deepseek-ai/dsh-llm-pi-ai and seeded a kilo profile into that plugin's settings document. That was wrong twice over — it wrote another plugin's namespace, and it made pi-ai claim the kilo route this bundle needs. index.js now removes exactly that seeded profile on activation (identified by the endpoint it wrote), so the leftover cannot cause DUPLICATE_ADAPTER on an upgraded install.

Configuration

Environment variables, read at request time:

VariableDefaultMeaning
KILO_API_KEY—Kilo Gateway API key. Normally entered on the bundle's own Settings page instead.
KILOCODE_TOKEN—Older device/session token, also honoured.
KILOCODE_ORGANIZATIONID—Organization scope, sent as X-KiloCode-OrganizationId.
KILO_AI_GATEWAY_URLhttps://ai-gateway.kilo.ai/api/v1Gateway traffic root.
KILO_API_URLhttps://api.kilo.aiProfile / balance / device-auth root.
KILOCODE_EDITOR_NAMEDeepSeek HarnessX-KILOCODE-EDITORNAME value.
KILOCODE_FEATURE—Opt-in X-KILOCODE-FEATURE attribution.
Config fieldDefaultMeaning
apiKeyEnvKILO_API_KEYCredential reference holding the key. Resolution tries this first, then KILO_API_KEY.
baseURL—Override the gateway base URL.
organizationId—Organization scope, overriding the environment and the stored record.
repairBudget2Repair-and-replay rounds per request. 0 disables self-healing.
streamIdleTimeoutMs90000Silence after which a stream counts as stalled and is retried.

All fields are volatile(), so editing one takes effect without rewriting the patch.

Verify

node tools/verify.mjs
node tools/verify-client.mjs
node tools/verify-publishable.mjs
node tools/verify-host-contract.mjs
node tools/verify-installable.mjs
node tools/vendor-schemastery.mjs --check

tools/verify.mjs runs the real adapter against a stubbed fetch: it asserts the chunk sequence and ordering, the block-end and usage contracts, the classifier's parameter-versus-stream split, the recovery policy, and that a retry is silent, a repair drops the right thing, and an abort is never replayed.

tools/verify-client.mjs runs the real browser half against a stub React and a client-shaped ctx: it asserts the plugin's service list, the slot registration, and that a missing remote namespace, a missing locale service, or a broken slot seam each degrade to a message instead of failing the client boot.

tools/verify-publishable.mjs checks the shipped Harness's own rules against this bundle's declarations: that an unknown package in the manifest's inject list is skipped rather than fatal, that a missing ./client export is fatal, and that dsh.client.inject and dsh.client.immediately are both optional.

tools/verify-host-contract.mjs reads the shipped @deepseek-ai/dsh-llm out of the app's asar and asserts the adapter still agrees with it — in particular that toolHistory is the ToolHistory object and is never iterated as an array. A stubbed fetch cannot catch that class of mistake, because the stub feeds the adapter whatever shape the test author believed in; this probe reads the shape the host actually sends.

tools/verify-installable.mjs asserts the bundle imports nothing it does not carry: every bare import is a Node builtin or react, the vendored copies are present with their licences, and the whole host-side tree imports cleanly with no package installed alongside it — the situation on a machine that has only this plugin.

Neither suite needs a network or a credential.

Install

plugin_manager install_bundle <absolute path to this directory>

Attribution

Kilo Code is developed by Kilo-Org and licensed MIT. This plugin is an independent port of its gateway client and is not affiliated with or endorsed by Kilo-Org.