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
Two buttons beside the dsh chat input: rewrite the draft with the current directory in view, and drop a role template into it.
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
- Install
- Verify it is live
- What it does not provide
- Enabling it in dsh
- The polish call
- Templates
- Settings
- Public interface
- Data and privacy
- Known boundaries
- FAQ
- Development
- License
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
groupfield; 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.mjsfrom this package'sagents/role library - a vendored collection of several hundred role.mdfiles 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.jsto ~711 KB. - The browser calls the Remote
defaultsmethod 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.
- That table stays host-side: the client bundle no longer inlines it. The generated file is ~640 KB and used to push
- 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
.mdor 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, soimport pkg from …fails withdoes not provide an export named 'default'. That Node floor is what makesrequire()of an ESM entry work at all, which is whyengines.nodeis declared. - Requires host
dshat the0.2.1-rc.2line. The requirement the host actually reads isengines.dsh(^0.2.1-rc.2), and it decides alone:evaluatePluginCompatibilityreturns on theengines.dshbranch before it ever looks atpeerDependencies. Measured against the real host code:0.2.0-rc.2is refused,0.2.1-rc.2and0.2.2pass,0.3.0is refused. A refused host needs the explicit risk grantdsh plugin --profile <p> allow-version <pkg>@<ver> --dsh-version <exact> --accept-risk. - The
@deepseek-ai/dsh-*pins stay at0.2.1-rc.2- the version this package is built and tested against. Theengines.dshrange 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/*underdependencies, by contract (test/publish-manifest.test.tsenforces it). Consumer profiles install withnodeLinker: hoistedandautoInstallPeers: false, so a host package underdependenciesis materialized as a real directory next to the copy the running host already loaded.@deepseek-ai/dsh-toolskeys its tool-runtime scheduler with a plainSymbol(notSymbol.for), so symbol identity is per module instance: the host reads the scheduler off the profile copy, getsundefined, 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.jsimports 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.mjsexternalizesdependenciesandpeerDependencies, 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.tspins 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 thedsh-agent-default-model/ client-ui /fs/llm/sessionfamily). Declaring them is not ceremony: under pnpm's strict layout a bare specifier intypes/host.d.tsthat no manifest table names is unresolvable on the consumer's side, while this repository's own typecheck stays green. The test reads that roster offtypes/**/*.d.tsrather than off this paragraph, so the two cannot drift apart. - What ships:
host.js,client.js,templates.data.mjs,types/,cordis.patch.yml,agents/andTHIRD_PARTY_NOTICES.md.package.json,README*andLICENSEare 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 fromagency-agents-zh(MIT). The copyright and licence notice ship inTHIRD_PARTY_NOTICES.md, because MIT requires it in every distributed copy. Do not hand-edit files underagents/.
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
- 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-configfor thedir-prep-organizelayer.--dump-configis 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.
- The dropdown fills: open "Template" and the built-in list arrives through the Remote
defaultsmethod. A loading row means the call is in flight; a failure shows the host's reason plus Retry. - 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.
- 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 noorganize_texttool 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/apigateway. 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
settingsservice (hard-injected),llm.stream,agentDefaultModel, and the session/filesystem services. Withoutsettingsthe 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-organizewithnameset to the package name. - The client half registers two slots plus the settings card:
conversation.input.right- the polish button (iddir-prep-organize, order 99) and the templates dropdown (iddir-prep-organize-templates, order 100).plugins.bundle.config- the settings card, keyed by the bundle package name@jayyuen66/dsh-dir-prep-organize. The host dispatchesentryKey: pkg.name(source of truth:dsh.profile.bundlesin the profile'spackage.json), so a bare entry id never matches and the card would render nowhere.- The settings namespace and
configForms.get()stay the bare entry iddir-prep-organize- deliberately not the same identifier as that slot key.
- The client half is
dsh.client.platform = webwithimmediately. - 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:falsewitherror. The import path-policy branches are the exception: an out-of-bounds path or a malformedpathfield also carriesstatus403 or 400. - Result parsing on the client keeps defensive defaults: a non-object payload or a missing
ok:truebecomes 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
sessionIdas an empty string, so an import must live inside the built-inagents/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
unnamedand 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-
stopfinish 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.tsis 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 editingscripts/gen-defaults-lib.mjsand re-runningpnpm 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.