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 insert a role template at the caret.
tech stack
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 model selected for that conversation (falling back to the host default) 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 model selected for that conversation (falling back to the host default), then replaces the draft with the rewrite. The result is written back only while the session, draft revision and input phase are unchanged.
- 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 at the caret through the input box's insert face: quoted blocks and attachments stay intact, and a stale insertion is refused instead of replacing the draft. Both controls are disabled while the input box is submitting.
- 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 settings card's "Role library" section 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 section 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 .
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 in the 0.2.1-rc.3 → 0.2.2-alpha.2 window. The requirement the host actually reads is engines.dsh (^0.2.1-rc.3), 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.3 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 support window rolls: this package is verified against the three most recently released host versions at publish time and re-verified once the window moves, so the version number in this bullet is not a standing promise.
- The
@deepseek-ai/dsh-* development pins are at 0.2.2-alpha.2 - the host line this package is built and tested against. engines.dsh and the optional peer ranges carry the separately checked support window (^0.2.1-rc.3); the pin is a fact about which host version it was compiled against, not a restatement of that window. 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.
查看图片 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-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.
- 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.
- 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: the "Role library" section 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, so the entry does not activate without it), llm.stream, agentDefaultModel, and the session/agent/filesystem services. When the settings form itself is not ready, the card renders disabled with a status hint while 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 plugin page reads its title, description and detail document from
locale/<lang>.json (meta.title, meta.description, meta.detail). docs/DETAIL.md and docs/DETAIL.zh.md ship in the tarball; without meta.detail the host falls back to the README.
查看图片 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 | The live agent's current selection first - agent.model.current() on current hosts, agent.options on older ones - then agentDefaultModel.currentSelection(). Its absence reports "No model selection is available" |
| Draft guard | The rewrite is written back only if the session id, input action face, draft revision and input phase (plain) are unchanged. Otherwise the call reports "The input box changed; the polished text was not applied" and leaves the draft alone |
| 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, a 2 MiB total snippet budget, and readConcurrency (8) for reads. A file over the per-file cap, or a snippet past the total budget, is skipped whole and counted in skipped.tooLarge. Directory listings, conversation text and role bodies are never truncated |
| No truncation, on purpose | Directory listings, conversation text and role bodies go in whole: 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 |
| Insertion | Uses the input box's version-checked insert face (captureInsertion() + insertText()). Text goes in at the caret, quoted blocks and attachments stay intact, and a stale insertion inserts nothing |
| "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. A role file whose code fence never closes is rejected instead of silently importing half its body |
查看图片 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. Values are truncated and clamped to [1, 2147483647]; non-finite values fall back to 120000. 0 passes the schema and clamps to 1 ms, so it really does mean an immediate timeout. 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.
model is the preflight read of the default selection; organize prefers the live agent's selection as described under "The polish call".
- 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. Collected snippets share a 2 MiB budget; once it is exhausted the rest are skipped whole and counted in skipped.tooLarge. 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. Snippets are omitted whole rather than shortened: one over the per-file cap or past the 2 MiB total budget is counted and left out. 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, unless the draft or input phase changed meanwhile and the write-back is refused.
| 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, the organizeTimeoutMs deadline, or the draft/selection changing while the request was in flight (the result is discarded) |
| 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.
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.