@jayyuen66/dsh-dir-prep-organize
v0.1.2
Published
dsh 插件:对话输入框「整理」按钮(主键直接整理/▾ 以角色视角整理,结合当前目录 + session 模型整理优化草稿)+「模板」下拉(内置精选角色集生成自插件 agents/ 角色库,可按部门分组;设置卡可增删改/一键导入内置全部/导入本地角色 .md)
Readme
@jayyuen66/dsh-dir-prep-organize
What it does
- Adds two controls to the right of the dsh web chat input: a split "Organize" button and a "Template" dropdown.
- The main Organize key organizes directly, the caret key offers "organize from a role's point of view"; the Template dropdown groups entries by
group, and an empty group falls into "General". - "Organize" hands the draft plus the top level of the session cwd (file names, sizes, full text of key text files) plus the session's recent real turns to the current default model for a rewrite.
- On success the draft is replaced; on failure it stays untouched and the button shows an error state for 4 seconds (reason in its title).
- Picking a template inserts it into the draft: a whole fill when the draft is empty, an append after one blank line otherwise.
- The default list is 85 curated role templates generated by scripts/gen-defaults.mjs from this package's agents/ role library (22 department directories, 306 role .md files).
- That table stays host-side only: the client bundle no longer inlines it - the generated file is 640KB and used to push
client.jsto 711KB. - The browser fetches
GET /_dsh/dir-prep/default-templatesonce, lazily - on the first menu open or settings-card visit - and reuses it in-process afterwards. - While it has not arrived the menus show a loading row rather than the existing "no templates" hint, and a failure surfaces the host's own reason plus a Retry button. Nothing is fetched at all when the user already has their own templates.
- The "Prompt templates" card on the settings page covers per-entry add/edit/delete, two-step restore of defaults, one-click import of the whole built-in library, import of a local .md or directory by path, and registration of import allowed directories.
- Templates and allowed directories are read and written through the host settings with zero LLM calls, and no model tool is registered.
Install
The packages are published to the public npm registry (registry.npmjs.org), so installation needs no credentials:
dsh plugin --profile web add @jayyuen66/dsh-dir-prep-organize- Host range
>= 0.2.0-rc.2, written to bothpeerDependenciesandengines.dsh. OnlypeerDependenciesis actually enforced: since0.1.7-rc.1the host evaluates it when loading plugins and disables an entry that fails (compatibility-preflight.ts→deny()), whereasengines.dshhas no reader at this host version. - The
@deepseek-ai/dsh-*dependencies stay pinned at0.2.1-alpha.2, the version this package is built and tested against. The 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 now checks them separately. - The client half is
dsh.client.platform = webwithimmediately. - The publishing form is one repository per package,
repository.urlis git+https://github.com/JayYuen666/dsh-dir-prep-organize.git ; issues go to that repository's issue tracker (bugs). - Requires Node.js
>= 22.12. This is an ESM-only package: useimport { apply } from "@jayyuen66/dsh-dir-prep-organize"orrequire("@jayyuen66/dsh-dir-prep-organize"). There is no default export —import pkg from …fails withdoes not provide an export named 'default'; take the named exports instead. That Node floor is what makesrequire()of this ESM entry work at all, which is whyengines.nodeis declared. - TypeScript declarations ship as
types/host.d.ts, wired into bothtypesand the.export condition. TheConfigschema value carries an explicit annotation so the emitted file never has to name a package a consumer cannot resolve. - The published artifact ships only
host.js,client.js,templates.data.mjs,types/,cordis.patch.yml,agents/andTHIRD_PARTY_NOTICES.md(package.jsonfiles).LICENSE,README.mdandpackage.jsonare attached unconditionally by the registry client. - The built-in template table lives in 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 85 templates in memory even without opening the dropdown -host.jsis now roughly a tenth of its former size, and the browser already fetched this data lazily anyway. agents/is not authored here: it comes from the upstream projectagency-agents-zh(MIT). Its copyright and licence notice ship inTHIRD_PARTY_NOTICES.md, because MIT requires it in every distributed copy.- No
@deepseek-ai/*goes independencies, and that is not a style preference. Consumers of this package are dsh plugins installed into a profile (nodeLinker: hoisted,autoInstallPeers: false), so any host package listed underdependenciesis written into that 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, sotest/publish-manifest.test.tspins the invariant in the manifest instead. - The two host packages the shipped
host.jsimports as values are required (non-optional)peerDependencies:@deepseek-ai/schemastery(the host's fork — only it resolves 0.1.7.volatile()fields into live references) and@deepseek-ai/dsh-brand(0.2.1-alpha.2). The host provides them at runtime and a profile installs no peers, so no duplicate appears.build-host.mjsexternalizesdependenciesandpeerDependencies, so the bare specifiers inhost.jsstay exactly as they were — that is the ruling behindbrandString: the peer keeps the specifier external and the official function body out of the artifact, while adevDependencies-only entry lets rolldown inline it (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(4.0.5-alpha.1),@deepseek-ai/cosmokit(1.8.6-alpha.1, reached only through schemastery'sDict) and@deepseek-ai/dsh-agent-default-model,-client-locale,-client-ui-conversation,-client-ui-renderer,-client-ui-settings,-client-ui-slots,-fs,-llm,-session(all0.2.1-alpha.2). Declaring them is not ceremony: under pnpm's strict layout a bare specifier intypes/host.d.tsthat no manifest table names is unresolvable, so the declaration entry simply would not typecheck on the consumer's side while this repository's own typecheck stays green — here both tables carry it.test/publish-manifest.test.tsreads that roster offtypes/**/*.d.tsrather than off this paragraph, so the two cannot drift apart.@deepseek-ai/dshis the one peer nothing imports: it names the host, which is whatengines.dshsays in words. - Every peer is repeated in
devDependenciesat the same exact pin; that is what lets this repository's own typecheck, build and tests resolve them. The remaining@deepseek-ai/*entries here are type-only in source and leave no specifier in the declarations either, so they stay indevDependenciesalone. - What is left in
dependenciesis this package's own runtime code:@jayyuen66/dsh-plugin-shared.
Enabling it in dsh
- Installing the package contributes the bundle patch layer (
dsh.bundle.patchpoints at./cordis.patch.yml), which inserts- id: dir-prep-organizewithnameset to the package name. - Removal goes through
dsh plugin --profile web remove @jayyuen66/dsh-dir-prep-organize. - The host half hard-injects webServer and settings: without settings the card is disabled with a warning, the "Template" dropdown falls back to the built-in curated set, and the five endpoints keep working.
- The client half registers slot
conversation.input.right(organize iddir-prep-organizeorder 99, templates iddir-prep-organize-templatesorder 100) and the settings card slotplugins.bundle.config.- The card slot key = the bundle package name
@jayyuen66/dsh-dir-prep-organize: the host dispatchesentryKey: pkg.name(source of truth:dsh.profile.bundlesin~/.dsh/profiles/web/package.json), so a bare entry id never matches and the card renders nowhere. - The settings namespace and
configForms.get()stay the bare entry iddir-prep-organize- not the same identifier as that slot key.
- The card slot key = the bundle package name
Tools exposed to the model
| Aspect | Fact |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tool registration | None. The host half only injects webServer and settings and never touches the host tool registry |
| Organize call | Host llm.stream: one user text message (request-only input - no id, no source; see the three-evidence note at the top of host.ts) plus a system rule block; the model comes from agentDefaultModel.currentSelection() (provider/model/reasoningEffort) and its absence reports "No model selection is available" |
| Role point of view | Optional roleText injected into the "Reference role" part of the system block; it only borrows terminology and tone, it never executes the role's tasks |
| Limits | All three live in config and every request takes them through limits: organizeTimeoutMs defaults to 120000 ms (a volatile reference read via .get()), maxSnippetBytes to 262144 (256 KiB) and readConcurrency to 8 (plain values, no .get()); the client request timeout is organizeTimeoutMs + 5000 ms (125000 when the host value is absent; the other three endpoints fetch with the constant FETCH_TIMEOUT_MS of 125000 ms); the POST body cap is the config field importBodyMaxBytes at 8 MiB by default; directory/conversation/role text are never truncated; a finish reason other than stop (including a missing finish chunk) fails and never writes back |
Settings
| Field | Type | Default | Notes |
| ------------------- | ------------------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| templates | {id,name,description,text,group,emoji}[] | unset | All-string fields; description/group/emoji default to empty strings; entries with an empty name or body are dropped on save, emoji is cut to 12 characters, a missing or colliding id is deterministically replaced by tpl-<index>. The dropdown falls back to the 85 built-in curated entries when unset. ⚠ under implicit registration "unset" reaches the client as an empty array (schemastery casts a missing array field to []; omitting the default does not yield undefined), 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 |
| importAllowRoots | string[] | [] | Extra allowed roots for the import endpoint, registered directory by directory; non-strings and whitespace-only items are dropped and duplicates removed; with nothing registered only the session cwd and the built-in role library are allowed |
| organizeTimeoutMs | natural, .volatile() | 120000 | The LLM abort deadline in ms; the host reads it per request via config.organizeTimeoutMs.get(). Being volatile it is projected into this entry's form snapshot, and of the three it is the only one the client half reads (rawOrganizeTimeoutMsOf), used to set the organize fetch timeout to "this value + 5000 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 for it: src/client-entry.ts writes only templates and importAllowRoots, so changing it means settings.yaml or the row's config: |
| maxSnippetBytes | natural (non-volatile) | 262144 | Per-file cap for the key-file snippet; 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 it is not projected into the form snapshot, so there is no card row either - deployment value only (settings.yaml / row config:) |
| readConcurrency | natural (non-volatile) | 8 | Bounded read concurrency for the key-file snippets and for directory imports (sub-directory listings, third-level counting, candidate reads). Same as above: no projection, no card control. ⚠ Clamped to [1, 64] before use: 0 or a negative value made the bounded map recurse into itself forever until the host process was killed by the OOM killer, and a huge value simply defeats the fd ceiling this field exists to hold |
| importBodyMaxBytes | natural (non-volatile) | 8388608 | Byte cap on the bodies of the two POST routes, organize and import (depth-of-defense against a memory blow-up, not a functional truncation: those routes carry the organized draft and the template products, not the file payloads themselves); over the cap is 413, and guardBody meters the stream and returns early at the crossing, so the worst resident memory is exactly this watermark. Same as above: no projection, no card control |
| Restore defaults | — | — | Unsets templates back to the curated set and leaves importAllowRoots untouched; two-step confirm, the second click must come within 4 seconds |
| Storage | — | — | Settings namespace dir-prep-organize (0.1.7 implicit registration: the namespace is this entry's id, and the fields projected into the settings form are the .volatile() ones on Config in host.ts - templates, importAllowRoots, organizeTimeoutMs, of which the card writes the first two; maxSnippetBytes and readConcurrency stay plain values; the curated set stays on the host half and is served on demand, not as a host base), persisted in the settings.yaml under <dsh data dir> ($DSH_HOME) |
Public interface
| Route | Method | Input | Success payload |
| ------------------------- | ------ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| /_dsh/dir-prep/context | GET | query sessionId | ok/csrf/cwd/truncated/skipped/entries[{name,isDir,sizeBytes,snippet}] |
| /_dsh/dir-prep/model | GET | query sessionId | ok/csrf/provider/model/reasoningEffort |
| /_dsh/dir-prep/organize | POST | body prompt/sessionId/entriesSummary/optional roleText, header x-dir-prep-csrf | ok/content |
| /_dsh/dir-prep/import | POST | body path (exactly empty = whole built-in library)/sessionId, no CSRF | ok/count/truncated/skipped/entries |
| /_dsh/dir-prep/default-templates | GET | none, no CSRF | ok/templates[{id,name,description,text,group,emoji}] (the curated built-in set) |
| Case | Response |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Routing and token | All four are exact-match webServer routes (kind: "exact"); the first statement of every handler body is guardTrust(req, res, { servingNonLoopback }) from shared/lib/trust, which writes a single 403 and returns without reading the body; csrf is a per-apply random token, issued by the two GETs and verified on organize |
| Order of the trust checks | Host authority → the sec-fetch-site allowlist (only same-origin/none/absent) → a verbatim origin vs Host comparison; the isCrossOrigin branch inside this package's handlers became unreachable once the gate landed and is deleted (the identical check inside the shared guardBody now sits behind the gate too), so the gate is what rejects cross-site traffic and supplies the sentence |
| Host authority is not a trusted local address | 403 with {ok:false,error:"untrusted host authority"}; servingNonLoopback comes from isNonLoopbackBindHost(webServer.host) (shared/lib/trust) - the host takes any local interface address as Config.host and rejects the wildcard address at load, so comparing against "0.0.0.0" would be permanently false; a service that reports no bind address keeps the conservative loopback-only setting |
| sec-fetch-site outside the allowlist, or origin mismatch | 403 with {ok:false,error:"cross-origin request rejected"} (opaque origin: null sandbox pages, and a local request that carries origin but no Host, land here too) |
| Wrong method | 405 with Allow and a JSON body: {ok:false,error:"GET only"} (organize/import answer "POST only") |
| Missing or wrong CSRF | 403 with {ok:false,error:"invalid csrf token"} (the guardBody check, still behind the gate) |
| Body over the configured cap (8 MiB by default) | 413 with {ok:false,error:"request body too large"} |
| Broken stream or invalid JSON | 400 with {ok:false,error} ("request body unreadable" for a broken stream; invalid JSON uses the dictionary text) |
| Business failure | Still 200 with {ok:false,error}; the import endpoint's out-of-bounds and malformed-path branches are the exception - 403 and 400 (see Data and privacy) |
Data and privacy
| Aspect | Fact |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Network calls | Only target this process's same-origin /_dsh/*; nothing is requested from any external host and no environment variable is read |
| Data on disk | The only data written to disk is templates and importAllowRoots in settings |
| What one organize call sends | The full draft, the session cwd path, top-level directory entries with the full text of key text files, real user/assistant turns (framework-injected messages and tool results are filtered out), and the optional role body |
| Snippet extensions | 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 types are not whitelisted |
| Sensitive file names | Any file whose name contains credential/secret/id_rsa/.pem/.p12/.pfx/keystore/.env/.key never gets a snippet |
| Per-file cap and concurrency | A single file over maxSnippetBytes (default 256 KiB), or with unknown size, is skipped whole (no half summaries), reads run with readConcurrency (default 8); both are config fields and neither has a card control |
| Counters and echo | 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 |
FAQ
| Symptom | What happens |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Nothing changed after pressing "Organize"? | Failures never touch the draft; the button shows an error state for 4 seconds with the reason in its title. Usual causes: no session cwd (sessionId missing or session unknown), no model selection, a finish reason other than stop, the organizeTimeoutMs deadline (120 seconds by default) |
| The model reports a context overflow? | Directory/conversation/role text are deliberately not truncated, so the model error is the feedback and you slim the input yourself; a payload above the configured body cap (8 MiB by default) gets 413 from the endpoint instead |
| Importing local roles was refused? | Absolute paths are refused by default and the settings card has no session context (its POST sends sessionId as an empty string), so the path must live inside the built-in agents/ or a registered directory and contain no ./../hidden segments; the rejection names the allowed zones and echoes the resolved path |
| Fewer entries imported than expected? | Read the four skipped counters: directory imports cover the top level plus one sub-directory level (a third level is only counted as deeper), files over maxSnippetBytes (256 KiB by default) count as tooLarge, files whose name cannot be parsed from the first 20 frontmatter lines count as unnamed, and read failures as unreadable |
| Template edits not applied / wrong language? | Edits are staged locally and take effect for newly opened dropdowns after "Save"; the card, the two dock buttons next to the input (their labels and copy now live in the ui-messages.ts dictionary) and the host replies all follow the official locale plugin's preference (the host side reads it per request, no restart), while the agents/ role library and template bodies are user data and are never machine translated |
