A library package shared by one author's dsh plugin family. It is only imported, never enabled by users.
package.jsoncarries nodshfield and the package directory has nocordis.patch.yml, so sitting innode_modulesit is never registered as a plugin.- No settings card, no client half, no
/_dshroute of its own. - Admission rule: a piece of boilerplate is only pulled in when it repeats verbatim in two or more places. Domain judgement (which tools count as edits, path filtering, budgets, gate logic) stays in the plugins — this layer marks, it does not decide.
Published on the public npm registry, so installing needs no credentials.
npm install @jayyuen66/dsh-plugin-shared
# or
pnpm add @jayyuen66/dsh-plugin-sharedDeclare it as an ordinary dependency of your plugin package and import it by bare-package subpath:
import { isRecord, fieldOf } from "@jayyuen66/dsh-plugin-shared/lib/record";End users do not install or enable it separately: any consumer package pulls it in. When it is missing the failure is ERR_MODULE_NOT_FOUND on a @jayyuen66/dsh-plugin-shared/... specifier, never a silent downgrade.
| Node.js | >=22.12.0 — the publish-side floor the nine-package family shares, so a plugin that declares the same engines.node never rejects this library |
| dsh | ^0.2.1-rc.2, declared as an optional peer (this package needs no host at import time) |
| Module format | ESM only. require() works only where Node supports require(esm) |
| Tree shaking | "sideEffects": false |
| Behaviour | Where | Scope |
|---|---|---|
Reads a directory entry through realpathSync |
lib/project-key.ts |
Only the path handed to deriveProjectKey; a failure (ENOENT/EACCES) falls back to path.resolve and never throws |
| Reads nothing else | — | No other file access in lib/**, no writes at all, no environment variables read, no outbound network requests, no eval/new Function/dynamic import |
lib/card-apply.ts reads and writes one globalThis key by design — that is what survives a duplicated module instance under HMR.
The subpath column is the in-package specifier; value imports must prefix it with the bare package name. exports also carries ./package.json, which the host reads for display metadata.
| Subpath | Provides |
|---|---|
. |
Barrel over the runtime facets listed below. canonicalize-region-paths deliberately stays out, so no runtime consumer pays an import for it |
./lib/tool-events |
scanToolEvents → the calls/results tables, toolEventRowsOf (its single-event step, same code; the records it hands back already carry normalized arguments and badArguments), editPathOf and EditTarget (discriminated on kind, only write / read-view; path is undefined when no path could be read); every field of the three ledger interfaces is readonly; re-exports the official SessionEvent and the two PTC event types |
./lib/card-apply |
claimApply + CardApplyCtx: apply idempotency guard (an own boolean flag on globalThis plus ctx.effect cleanup) |
./lib/card-diff |
diffTouched: which touched fields really differ from the snapshot (value comparison, so undefined, null and an absent key are one and the same value) |
./lib/project-key |
deriveProjectKey + ProjectKeyOptions: cwd → last-segment-<first 8 hex of sha256(norm)>, fallback bucket default |
./lib/locale |
Locale (an alias of the official BuiltInLocaleId), DEFAULT_LOCALE, resolveLocale, resolveLocalePreference, messagesFor, MessagesCatalog, plus LOCALE_SETTINGS_NAMESPACE / LOCALE_PREFERENCE_FIELD |
./lib/lesson-bus |
settleLessonCall: routes synchronous throws and asynchronous rejections into one onFailure exit; function-thenables count as awaitable (missing them leaves a rejection unhandled); an onFailure that itself throws is contained, so it never forks into "thrown at the caller" or "unhandled rejection" |
./lib/text |
truncateEnd (delegates to the official truncateWithoutSplittingSurrogatePair) and truncateStart (back cut; the official surface has no back-cut form) |
./lib/record |
isRecord, fieldOf, hasMethods (service-surface check: the value is a record and every named method is callable) |
./lib/errors |
errorText: semantics aligned with the official @deepseek-ai/dsh-llm errorChain (cause chain, AggregateError members, empty message falling back to name, cross-realm message read off the value itself, hostile getters degraded), implemented locally with zero dependencies; isSettingsConflict recognizes the host's settings-conflict shape from code / name, so a cross-realm error still classifies |
./lib/jsonl |
shrinkJsonlTail: the pure decision half of JSONL tail halving; trigger policy and error exit stay with the caller |
./lib/job-outcome |
jobOutcomeOf + SettledProcess + JobOutcome: maps a settled subprocess onto the official job registry's outcome; killProc + KillableProcess terminates a child handle and ignores a kill() that throws on an already-reaped process |
./lib/canonicalize-region-paths |
Build-time string helper that folds rolldown's //#region <path> markers into a process.cwd()-independent form. Exported as a subpath precisely because runtime consumers should not pay an import for it |
These are lint/test baselines for plugin authors, not runtime code. They pull in tooling your package must provide itself:
| Subpath | Needs installed |
|---|---|
./config/oxlint |
oxlint, and eslint-plugin-sonarjs if you want the sonarjs rule set mounted |
./config/vitest.base |
vitest, plus @vitest/coverage-v8 because the baseline sets coverage.provider: "v8" |
./config/tsconfig.base.json, ./config/tsconfig.client.base.json |
Nothing — plain JSON for extends |
Those three tools are not declared as peerDependencies. npm 7+ tries to satisfy optional peers too, and oxlint's own peerOptional vite-plus pins vitest to a version disjoint from >=5.0.2, which made a plain npm install of this package fail with ERESOLVE. The requirement is therefore stated here instead of in the manifest.
The sonarjs entry point is resolved lazily, inside definePluginConfig(). Importing ./config/oxlint without sonarjs installed succeeds; calling definePluginConfig() throws a message naming both specifiers it tried and the jsPlugins escape hatch.
definePackageConfig imposes 100% coverage thresholds on four metrics and a 20-second test timeout. It will not relax them for you: an exception belongs in the package that needs it, with a reason.
dependencies is empty; every @deepseek-ai/* entry lives only in peerDependencies and devDependencies. test/publish-manifest.test.ts enforces this — it is not a style preference:
- Putting them in
dependenciesdisables the consumer's host. Consumers are dsh plugins installed into a profile (nodeLinker: hoisted,autoInstallPeers: false). Any host package listed underdependenciesis written into the profile as a real directory, coexisting with the copy the host process already loaded.@deepseek-ai/dsh-toolskeys itsTOOL_RUNTIME_SCHEDULERwith a plainSymbol(notSymbol.for), so symbol identity is per module instance: the host'sdsh-agent-loopreads aToolRuntimemounted from the duplicate using its own copy's symbol, getsundefined, and every native tool call throwsCannot read properties of undefined (reading 'prepare')— the whole profile loses its tools. This package's build, typecheck and tests all stay green right up to that point, so the invariant has to live in the manifest. @deepseek-ai/dsh-output-retentionis a value import (truncateEnd) and is provided by the host at runtime, so it is the one required peer — stated as"optional": falseinpeerDependenciesMetarather than left implicit, because an unlisted peer is a hard requirement for npm 7+ (ERESOLVE, not a warning).- The remaining peers are referenced only by the published
.d.ts, so they are optional peers plus devDependencies: the peer keeps the type surface honest —lib/job-outcomeused to takeShellProcessandJobOutcomefrom packages no consumer tree was required to contain, so withskipLibCheck: truea wrongstatusorexitCodeproduced no error at all andjobOutcomeOf's return type collapsed toany— while devDependencies let this repository compile and test. Profiles never install peers, so no duplicate appears. - Versions follow the host: exact pins throughout the
dsh-*family and forcordis, matching what the host bundle itself declares so the same copy is reused rather than duplicated.
Known upstream limitation: @deepseek-ai/dsh-llm's own declarations import @deepseek-ai/dsh-attachment, which it does not declare. It is reached transitively through dsh-session, so a consumer running with skipLibCheck: false sees TS2307 from that package, not from this one.
main and exports point at dist/ only. There is no second, source-shaped manifest.
publishConfigcarriesaccessandregistryand nothing else. Puttingmain/exportsthere was wrong: pnpm merges those onto the top level when publishing and npm does not, so publishing with npm shipped a package whose fourteen entry points named files that were not in the tarball.filesisicon.svg,dist,config/*.json,README.zh-CN.md. npm always addspackage.json,LICENSEandREADME.md;README.zh-CN.mdis listed explicitly so the Chinese copy ships too.buildstarts by deletingdist/, becausefilestakes the whole directory and a renamed facet would otherwise keep publishing its orphan artifact.preparerunsbuild. It does not run when a consumer installs from the registry; it exists so packing, publishing and workspace links always ship a freshdist/.- Node throws
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPINGfor.tsinsidenode_modules, which is exactly where a published package is loaded from, so an artifact is mandatory. The barrel's declaration file is generated bybuild-host.mjsbecause source-formexport * from "./record.ts"specifiers are not resolvable by consumers. - Adding a facet means touching three places:
exports, thefacetsarray inbuild-host.mjs,includeintsconfig.build.json, plustest/<module>.test.ts.
- Semantic versioning applies to the public surface: subpaths, exported value symbols, exported types, and error/reason enums.
0.xranges admit patches only, so touching any of those is already breaking under the ranges consumers wrote. deriveProjectKey's hash algorithm, the 8-hex slice and the trailing-segment whitespace cleaning are the untouchable part: changing one digit moves existing lessons and memories out of their buckets.- Windows path normalization changed the bucket key shape. On Windows the same directory now yields
last-segment-<hash>regardless of whether it is spelled with\or/, and a drive root falls intodefault. Windows buckets written before normalization no longer match; POSIX buckets are byte-for-byte unchanged and are pinned by literal assertions intest/project-key.test.ts. Localeis now an alias of the officialBuiltInLocaleIdinstead of a hand-written union, so it tracks the host'sLOCALE_IDS.errorTextnow returns more text (same name and signature). It went from a singlemessagelayer to the officialerrorChainsemantics: a cause chain renders asouter: inner, anAggregateErrorappends[e1; e2], an empty message falls back toError.name, and a cross-realmErroryields its ownmessageinstead ofString()'s"Error: ...". Callers that regex the old text must re-check (aexecthat pulls a number out of parentheses, for example).editPathOfreturns akind-discriminatedEditTargetwith onlywrite/read-view:"skip"was never produced, and leaving it in the type only pushed consumers into writing unreachable branches. When no path can be read it still returns the matching arm withpathset toundefined../lib/httpand./lib/trustare gone. The plugin family's settings-card data plane moved onto the official Typert Remote, whose/apigateway owns authentication, the Host / DNS-rebinding / Origin checks and the body cap; the two facets kept a hand-written copy of rules the host now enforces. No consumer package imports them any more. A plugin that still wants a rawwebServerroute has to write the gate itself — that is deliberate: the published surface no longer offers a second, divergent answer to "is this request allowed".
npm run check= typecheck → lint → build → test (coverage thresholds 100 for lines, statements, functions and branches) → format check.test/publish-manifest.test.tsis the release-shape gate: everyexportstarget must be inside whatfilesactually ships, entries must point at build output rather than source, andpublishConfigmust not carry entry fields. It also pins facet/exportsparity in both directions: everylib/*.tsneeds a matching subpath (a missing one means that facet is not importable by any consumer, while the build — which follows the builder's own entries array — stays green), and no dangling subpath may outlive its facet. And it pins peer optionality: everypeerDependenciesentry must appear inpeerDependenciesMeta, because a peer left out of that table is a hard requirement for npm 7+ consumers while typecheck, build, tests and the parity checks all stay green../lib/canonicalize-region-pathsis the one facet that lives inexportsbut not in the barrel, and that subpath is load-bearing: the sibling packages' rolldownbuild-*.mjsimport it (every sibling except the Python-bridge package), so removing it breaks their builds at once. One set of "internal reference" patterns scans both planes: artifacts must contain no absolute paths, home-directory references, dates, or sibling-package identifiers; source and docs must contain no dates and no "this round / next round" wording that only made sense during collaboration. The artifact pass cannot see comments, so the source pass is what keeps that cleanup from being a one-off.test/build-host.test.tscompares the bytes on disk against an in-memory build, so editinglib/*.tswithout rebuilding goes red.test/setup-logs.ts(a vitest setup file) is the ledger for runtime logs: it takes overconsole.*, so theconsole.errorcalls left inlib/neither leak into the test report (that stack trace is pure noise) nor go unclaimed — every[shared/*]line must match a template fragment intest/log-templates.tsor the run goes red. Do not substitute vitest'ssilent: it only hides, and a newly added log would still go unnoticed..github/workflows/ci.ymlruns the gate on push and pull request across a Node matrix and on Windows; the install matrix there additionally packs, installs into a clean directory with no flags, imports every subpath and type-checks a consumer probe.
- Should I
dsh plugin addit? No. It has neither a config layer nor a client half, so there is nothing to enable; declare it as a dependency. 401/403when publishing usually means the npm token lacks publish rights.404means the package or version is not there yet.- I changed this package and nothing happened. In local development through a workspace link, consumers load
dist/, so rebuild first (npm run build); a stale artifact is what the freshness gate catches. - Want something shared here? First confirm the duplication really is verbatim. Anything carrying domain judgement stays in the plugins.
MIT. LICENSE ships with the package.