Skip to content

dsh-dir-prep-organize

Verified

@jayyuen66/dsh-dir-prep-organize · v0.1.5 · MIT · Web UI

dsh 插件:对话输入框「润色」按钮(主键直接润色/▾ 以角色风格润色,结合当前目录 + session 模型润色草稿)+「模板」下拉(内置精选角色集生成自插件 agents/ 角色库,可按部门分组;设置卡可增删改/一键导入内置全部/导入本地角色 .md)

Install

dsh plugin add @jayyuen66/dsh-dir-prep-organize

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Readme

dsh-dir-prep-organize logo

dsh-dir-prep-organize

Two buttons beside the dsh chat input: rewrite the draft with the current directory in view, and drop a role template into it.

npm version npm downloads ci MIT node dsh powered by dsh

tech stack

GitHub Email Twitter/X 简体中文

A dsh plugin: a split "Polish" button that hands your draft, the top level of the session directory and the recent real turns to the current default model for a rewrite, plus a "Template" dropdown of role templates. Zero model tools, zero HTTP routes of its own.

Contents

What it does

  • Adds two controls to the right of the web chat input: a split "Polish" button (main key + caret) and a "Template" dropdown.
    • The main key polishes directly; the caret key offers "polish in a role's style".
    • The dropdown groups entries by their group field; an entry with an empty group lands in "General".
  • Polish sends the draft, the top level of the session cwd (file names, sizes, and the full text of key text files), and the session's real user/assistant turns to the current default model, then replaces the draft with the rewrite.
    • On failure the draft is never touched: the button shows an error state for 4 seconds with the reason in its tooltip.
  • Templates insert text into the draft: a whole fill when the draft is empty, an append after one blank line otherwise.
  • The built-in set is 85 curated role templates, generated by scripts/gen-defaults.mjs from this package's agents/ role library - a vendored collection of several hundred role .md files across more than twenty department directories. The curated 85 is the number that matters to a user; the library size is not pinned here because it changes whenever the upstream project is refreshed.
    • That table stays host-side: the client bundle no longer inlines it. The generated file is ~640 KB and used to push client.js to ~711 KB.
    • The browser calls the Remote defaults method once, lazily (first menu open or first settings-card visit) and reuses it in-process. Nothing is fetched at all when the user already has their own templates.
    • While it has not arrived the menu shows a loading row - not the "no templates" hint. A failure surfaces the host's own reason plus a Retry button.
  • The "Template library" card covers per-entry add/edit/delete, a two-step restore of the defaults, one-click import of the whole built-in library, import of a local .md or directory by path, and registration of extra allowed import directories. That card is one list feeding two places: the input-box "Template" dropdown and the "Polish" ▾ role menu.
  • Templates and allowed directories are read and written through the host settings with zero LLM calls, and no model tool is registered.

Install

dsh plugin --profile web add @jayyuen66/dsh-dir-prep-organize

Remove with dsh plugin --profile web remove @jayyuen66/dsh-dir-prep-organize. The package is on the public npm registry, so installation needs no credentials and no scoped-registry line.

  • Requires Node.js >= 22.12. This is an ESM-only package. Use the named exports (import { apply } from "@jayyuen66/dsh-dir-prep-organize"); there is no default export, so import pkg from … fails with does not provide an export named 'default'. That Node floor is what makes require() of an ESM entry work at all, which is why engines.node is declared.
  • Requires host dsh at the 0.2.1-rc.2 line. The requirement the host actually reads is engines.dsh (^0.2.1-rc.2), and it decides alone: evaluatePluginCompatibility returns on the engines.dsh branch before it ever looks at peerDependencies. Measured against the real host code: 0.2.0-rc.2 is refused, 0.2.1-rc.2 and 0.2.2 pass, 0.3.0 is refused. A refused host needs the explicit risk grant dsh plugin --profile <p> allow-version <pkg>@<ver> --dsh-version <exact> --accept-risk.
  • The @deepseek-ai/dsh-* pins stay at 0.2.1-rc.2 - the version this package is built and tested against. The engines.dsh range is a claim about which hosts it runs on; the pin is a fact about which host version it was compiled against. Mixing the two is why the publish gate checks them separately.
  • No @deepseek-ai/* under dependencies, by contract (test/publish-manifest.test.ts enforces it). Consumer profiles install with nodeLinker: hoisted and autoInstallPeers: false, so a host package under dependencies is materialized as a real directory next to the copy the running host already loaded. @deepseek-ai/dsh-tools keys its tool-runtime scheduler with a plain Symbol (not Symbol.for), so symbol identity is per module instance: the host reads the scheduler off the profile copy, gets undefined, and every native tool call throws - while this package's build, typecheck and coverage gates stay green right up to that moment.
  • The two host packages host.js imports as values are required peers: @deepseek-ai/schemastery (the host's fork - only it resolves the .volatile() fields) and @deepseek-ai/dsh-brand. build-host.mjs externalizes dependencies and peerDependencies, so the bare specifiers stay in the artifact; a devDependencies-only entry would let the bundler inline the official function body. test/build-host.test.ts pins both "specifier present" and "function body absent".
  • Every host package the emitted declarations name is declared under peerDependencies, and each one reached only there is optional (@deepseek-ai/cordis, cosmokit, and the dsh-agent-default-model / client-ui / fs / llm / session family). Declaring them is not ceremony: under pnpm's strict layout a bare specifier in types/host.d.ts that no manifest table names is unresolvable on the consumer's side, while this repository's own typecheck stays green. The test reads that roster off types/**/*.d.ts rather than off this paragraph, so the two cannot drift apart.
  • What ships: host.js, client.js, templates.data.mjs, types/, cordis.patch.yml, agents/ and THIRD_PARTY_NOTICES.md. package.json, README* and LICENSE are attached unconditionally by the registry client.
  • The built-in table ships as its own artifact and is loaded on first use, not at plugin load. It used to be bundled into host.js, so every user paid for the templates in memory even without opening the dropdown.
  • agents/ is not authored here: it comes from agency-agents-zh (MIT). The copyright and licence notice ship in THIRD_PARTY_NOTICES.md, because MIT requires it in every distributed copy. Do not hand-edit files under agents/.

Shared version floor

If your profile pins @jayyuen66/dsh-plugin-shared with a pnpm overrides entry, that pin must be at least the version this package declares. A lower pin installs a copy without the names imported at runtime, and the host then reports N entries did not activate together with does not provide an export named ... at boot - the package installs fine and never loads, and no per-repository CI run can see it.

Verify it is live

  1. Two controls appear to the right of the chat input. If they do not, the bundle layer is not mounted - check dsh --profile web --dump-config for the dir-prep-organize layer.
    • --dump-config is the only accepted "it is in the layer stack" signal, and it is not read-only - it rewrites that profile's root assembly file. Read layer lines from stdout and check stderr separately: an unmatched patch target appears only on stderr, so the exit code alone proves nothing.
    • A layer line may read # == <layer> or # == <layer>, patched by <path>; accept both forms.
  2. The dropdown fills: open "Template" and the built-in list arrives through the Remote defaults method. A loading row means the call is in flight; a failure shows the host's reason plus Retry.
  3. Polish works: press the main key. The draft is replaced on success; on failure it is untouched and the button turns into an error state whose tooltip carries the reason.
  4. The settings card renders: "Template library" appears on the settings page, edits stage locally and take effect after Save.

What it does not provide

Worth knowing before you look for them:

  • It registers no model tools. The host half only injects settings; it never touches the tool registry. There is no organize_text tool to call.
  • It opens no HTTP route of its own. The data plane is the official Typert Remote namespace dirPrepOrganize, registered through an injected child fiber, reached through the host /api gateway. Authentication, Host/DNS-rebinding/Origin checks, the signed browser-session cookie and body limits are all the gateway's - this package keeps no CSRF token and no route table.
  • It adds no CLI subcommand.
  • It reads no environment variable and makes no outbound request of its own.
  • What it does consume: the host settings service (hard-injected), llm.stream, agentDefaultModel, and the session/filesystem services. Without settings the card is disabled with a warning and the dropdown falls back to the built-in curated set.

Enabling it in dsh

  • Installing contributes the bundle patch layer (dsh.bundle.patch → ./cordis.patch.yml), which inserts - id: dir-prep-organize with name set to the package name.
  • The client half registers two slots plus the settings card:
    • conversation.input.right - the polish button (id dir-prep-organize, order 99) and the templates dropdown (id dir-prep-organize-templates, order 100).
    • plugins.bundle.config - the settings card, keyed by the bundle package name @jayyuen66/dsh-dir-prep-organize. The host dispatches entryKey: pkg.name (source of truth: dsh.profile.bundles in the profile's package.json), so a bare entry id never matches and the card would render nowhere.
    • The settings namespace and configForms.get() stay the bare entry id dir-prep-organize - deliberately not the same identifier as that slot key.
  • The client half is dsh.client.platform = web with immediately.
  • The card follows the official locale plugin's preference for its own labels and copy; the host half reads the preference per request, so switching language needs no restart.

The polish call

Aspect Fact
Transport Host llm.stream: one request-only user text message (no id, no source) plus a system rule block
Model agentDefaultModel.currentSelection() - provider / model / reasoningEffort. Its absence reports "No model selection is available"
Role style An optional roleText injected into the user message's <data> fence (never into the system block - see the trust-order note above buildOrganizeSystem). It borrows terminology and tone only; it never executes the role's tasks
Finish reason Anything other than stop - including a missing finish chunk - fails and never writes back to the draft
Timeout organizeTimeoutMs, default 120000 ms, read per request via .get(). The client fetch timeout is that value + 5000 ms
Input caps maxSnippetBytes (262144 = 256 KiB) per key file, readConcurrency (8) for reads. Directory, conversation and role text are never truncated
No truncation, on purpose A model context-overflow error is the feedback, and the user slims the input. A payload above the endpoint body cap gets a 413 instead

Templates

Aspect Fact
Entry shape { id, name, description, text, group, emoji }, all strings. description / group / emoji default to empty strings
Validation on save Entries with an empty name or text are dropped; emoji is cut to 12 characters; a missing or colliding id is deterministically replaced by tpl-<index>
Built-in set 85 curated templates generated from the agents/ role library. The dropdown falls back to them when templates is unset
"Unset" is an empty array Under implicit registration schemastery casts a missing array field to [], so an empty list always means "fall back to built-in". Only a non-empty array whose items are all invalid renders as an empty list
Restore defaults Unsets templates back to the curated set and leaves importAllowRoots untouched. Two-step confirm; the second click must come within 4 seconds
Import By path: a local .md file or a directory. path exactly empty imports the whole built-in library

Settings

Namespace dir-prep-organize equals the settings entry id. Fields projected into the settings form are the .volatile() ones.

Field Type Default Card control Notes
templates {id,name,description,text,group,emoji}[] unset yes The user's own templates. Unset falls back to the 85 built-in entries
importAllowRoots string[] [] yes Extra allowed roots for the import endpoint. Non-strings and whitespace-only items are dropped; duplicates removed. With nothing registered, only the session cwd and the built-in role library are allowed
organizeTimeoutMs natural, volatile 120000 no The LLM abort deadline in ms. Clamped to [1, 2147483647]: past 2^31-1 Node's setTimeout prints a TimeoutOverflowWarning and silently uses 1 ms, which turns "raise the timeout" into "fail instantly". The card has no control, so changing it means settings.yaml or the row's config:
maxSnippetBytes natural, non-volatile 262144 no Per-file cap for key-file snippets; a larger file, or one whose size is unknown, is skipped whole and counted in skipped.tooLarge. Import candidates use the same cap. Non-volatile means no form projection, hence no card row - deployment value only
readConcurrency natural, non-volatile 8 no Bounded read concurrency for snippets and directory imports. Clamped to [1, 64]: 0 or a negative value made the bounded map recurse forever until the host was killed by the OOM killer, and a huge value defeats the fd ceiling this field exists to hold

Settings are persisted in settings.yaml under the dsh data directory.

Public interface

Official Typert Remote namespace dirPrepOrganize, registered through an injected child fiber. The descriptors are hand-written, parameters go through strict shape validation, results go through src-json.

Method Input Success payload
defaults - ok / templates[…] (the curated set)
context { sessionId } ok / cwd / truncated / skipped / entries[{name,isDir,sizeBytes,snippet}]
model { sessionId } ok / provider / model / reasoningEffort
organize { sessionId, prompt, entriesSummary, roleText? } ok / content
importTemplates { path, sessionId } (path exactly empty = whole built-in library) ok / count / truncated / skipped / entries
  • Business failures stay ok:false with error. The import path-policy branches are the exception: an out-of-bounds path or a malformed path field also carries status 403 or 400.
  • Result parsing on the client keeps defensive defaults: a non-object payload or a missing ok:true becomes a visible error, never an empty list.

Data and privacy

Aspect Fact
Network No outbound requests of its own: the data plane is the host gateway's RPC channel. No environment variable is read
On disk The only data written is templates and importAllowRoots, in the host settings file
What one polish call sends The full draft, the session cwd path, top-level directory entries with the full text of key text files, the real user/assistant turns (framework-injected messages and tool results are filtered out), and the optional role body
Which files get a snippet Only 20 code/document extensions (ts/tsx/js/jsx/mjs/cjs/py/rs/go/java/rb/json/toml/md/sql/graphql/proto/css/scss/less). yaml / env are not whitelisted
Sensitive file names Any file whose name matches credentials / secret(s) / id_rsa / pem / p12 / pfx / keystore / env / key(s) / token(s) / auth / passwd / password / account(s) at a separator boundary never gets a snippet. The boundary is deliberate: a naive substring match would drop monkey.js and AUTHORS.md, while a miss would send credential text to a third-party model
Caps and concurrency A single file over maxSnippetBytes, or with unknown size, is skipped whole - never half-summarized. Reads run with readConcurrency
Counters Too-large / unreadable / deeper-level / non-role cases are counted in skipped, shown in the UI, and reflected honestly by truncated
Import endpoint Read-only: no model call, no disk writes. Absolute paths are refused by default; only paths inside the session cwd, the package's agents/ and importAllowRoots pass. . / .. / hidden segments (such as .ssh) are always rejected, and an out-of-bounds path answers 403 echoing the resolved path

Known boundaries

Registered honestly, one entry per class.

  • Nothing is truncated for you. Directory listings, conversation text and role bodies go in whole. A context overflow is the model's error to report and yours to slim - this plugin will not silently drop half your draft.
  • The import card has no session context. Its POST sends sessionId as an empty string, so an import must live inside the built-in agents/ or a registered root, and must contain no . / .. / hidden segments. The rejection names the allowed zones and echoes the resolved path.
  • Directory import reaches two levels. The top level plus one sub-directory level; a third level is only counted as deeper, never imported.
  • A file whose name cannot be parsed from the first 20 frontmatter lines counts as unnamed and is skipped rather than guessed.
  • Machine translation does not exist here. The agents/ role library and template bodies are user data; they are never translated, whichever interface language is active.
  • The polish path is best-effort about the model's answer. A non-stop finish reason fails the whole call; a model that returns prose instead of a rewrite replaces your draft with that prose.

FAQ

Symptom What happens
Nothing changed after pressing "Polish" Failures never touch the draft; the button shows an error state for a few seconds with the reason in its tooltip. Usual causes: no session cwd (sessionId missing or session unknown), no model selection, a finish reason other than stop, or the organizeTimeoutMs deadline
The model reports a context overflow Working as designed: input is deliberately not truncated, so the model error is the feedback. A payload above the endpoint body cap gets 413 instead
Importing local roles was refused Absolute paths are refused by default and the card has no session context. The path must live inside the built-in agents/ or a registered directory and contain no . / .. / hidden segments
Fewer entries imported than expected Read the four skipped counters: deeper (third level, counted only), tooLarge (over maxSnippetBytes), unnamed (no name in the first 20 frontmatter lines), unreadable (read failure)
Template edits not applied / wrong language Edits stage locally and take effect for newly opened dropdowns after Save. Labels and copy follow the official locale preference; role and template bodies are user data and are never machine translated

Development

pnpm install
pnpm run check        # typecheck → lint → client build → host build → types → tests → format
pnpm run gen:defaults # regenerate the curated set from agents/ (dev-time only)
  • test/gen-defaults.test.ts is a drift lock. It compares the generated file field by field against the role library, checks both mirror tables' key order, and requires byte-level identity with what the generator produces. Editing the parsing or extraction logic means editing scripts/gen-defaults-lib.mjs and re-running pnpm run gen:defaults; anything else shows up as a red test rather than a silently stale dropdown.
  • The generator exits non-zero when a curated slug is missing or fails to parse, so a role can never disappear silently.
  • The rest of the suite splits the classes of defect a stub cannot see: the host half, the client half, the Remote contract (test/dir-prep-remote.test.ts), the two build artifacts (test/build-host.test.ts, test/build-client.test.ts), and an integration layer.
  • Coverage thresholds are 100% across four metrics, so a new branch needs a case that reaches it.

License

MIT. Source at github.com/JayYuen666/dsh-dir-prep-organize. Issues go to that repository's tracker.

agents/ is vendored from agency-agents-zh (MIT); see THIRD_PARTY_NOTICES.md.