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.

npm version npm downloads (18 months) ci MIT node dsh powered by dsh

tech stack

GitHub Email Twitter/X 简体中文

**A [dsh](https://github.com/deepseek-ai/deepseek-harness) 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](#what-it-does) - [Install](#install) - [Verify it is live](#verify-it-is-live) - [What it does not provide](#what-it-does-not-provide) - [Enabling it in dsh](#enabling-it-in-dsh) - [The polish call](#the-polish-call) - [Templates](#templates) - [Settings](#settings) - [Public interface](#public-interface) - [Data and privacy](#data-and-privacy) - [Known boundaries](#known-boundaries) - [FAQ](#faq) - [Development](#development) - [License](#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 `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 **no model tool is registered**. ## Install ```sh 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

allow-version @ --dsh-version --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. - **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/`, `locale/`, `docs/` 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`](https://github.com/jnMetaCode/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 `# == ` or `# == , patched by `; 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**: 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/.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 `` 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-` | | 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. ## 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`, 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 ```sh 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](https://github.com/JayYuen666/dsh-dir-prep-organize). Issues go to that repository's tracker. `agents/` is vendored from [`agency-agents-zh`](https://github.com/jnMetaCode/agency-agents-zh) (MIT); see [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md).