dsh-ATLAS
English · 简体中文
Beyond the built-in @: line ranges like @src/a.ts:12-40, skills, plugins and any git repo as source groups — with the approximate token cost of each reference shown before you send.
A Side Quest project (工作室:支线任务, Side Quest Labs) — npm scope @sidequest-007/*, repository jameswatt139240-crypto/dsh-ATLAS; the DSH plugin id keeps the ecosystem's unscoped dsh-* form.
AT Last, All Sources.
One @ . Jump anywhere.
Every reference it draws is a clickable link (the theme's link colour, so blue in dark mode), and a session link jumps straight to that session.
Unified @ mentions for the DeepSeek Harness web GUI: type @ in the composer to reference workspace files and folders, discoverable skills, past chats, installed plugins, and workspace git changes — and any other plugin can add its own category to the same menu. Clicking a session mention switches the composer to that session.
- One
@, every source: five built-in categories plus the bundled@gitprovider — six resource families in this deployment — and every registered provider, in one list; typing letters searches all of them at once. - A platform, not a picker: third-party plugins register their own
@category through thectx.atlasseam. The menu rebuilds from the live registry on every open, so this package never has to change for a provider — see Write your own@source. - Provenance, never content: a committed mention injects a marker (
,, ``, …) and nothing else. The plugin reads directory metadata only; the agent reads the file itself, if and when it needs to. - Governed by construction:
scopesandtestedOnare required, a duplicateidthrows, a version mismatch registers asverified: false, and every provider body is budgeted (16 KiB per reference, 48 KiB per step). @gitships as the worked example, and as a real source: it lists the workspace's changes (path + added/removed lines) like any other category, and it is built exactly the way a third party would build one, in ~200 lines across its two halves.
It extends dsh-at-file (MIT), keeping its workspace path index, file filters, and paste protection, and adding four more categories, a category menu with shortcuts, mixed results, collapsible groups, and three model-facing query tools.
> Coming from dsh-at-file? Its last commit was 2026-09-01, and on current Harness it no longer starts: ctx.settings.register is not a function, so the plugin throws during load and its entries are dropped silently (upstream #45, open since 2026-09-25; the fix has been sitting as #47 since 2026-10-19). dsh-atlas is that same file index carried onto the current API — the workspace path index, the filters and the paste protection are all still here, with the categories above on top. Same trigger, so install one of the two.
Install
Requires a DSH install (dsh on PATH) with a web profile.
# from GitHub (this repository; lib/ is committed, so no build runs)
dsh plugin --profile web add github:jameswatt139240-crypto/dsh-ATLAS
# a local clone (development)
git clone https://github.com/jameswatt139240-crypto/dsh-ATLAS
dsh plugin --profile web add link:/path/to/dsh-ATLAS
# npm (published; `latest` is the current release)
dsh plugin --profile web add @sidequest-007/dsh-atlas
Restart dsh web after installing or updating so the Host and the browser client load the same version. Then type @ in the composer.
Coming from Cursor?
Cursor's @ menu taught a lot of people to type @file, @folder, @Chats, @Commit. The DSH composer resolves @path and @label on its own. This plugin widens that menu to the shape you already expect, and adds the two things Cursor does not do:
| Cursor | DSH built-in | dsh-ATLAS | |
|---|---|---|---|
@file |
yes | yes | yes |
@folder, with / to descend |
yes | yes | yes |
| previous conversations | @Chats |
@label |
yes |
| git diff | @Commit / @Branch |
— | @git |
| a skill as a source | via / |
— | yes |
| a plugin as a source | — | — | yes |
line range @src/a.ts:12-40 |
— | — | yes |
| token cost, before you send | — | — | yes |
@Terminals, @Browser |
yes | — | — |
The last two rows are the argument. In an editor, attaching @db.ts to reach one function drags the whole file into the window, and you find out what it cost once the window is already full. Here you write @src/db.ts:88-140, the dock prices it at ~348 tokens, and it stays 348 — TOKENS_PER_LINE = 12, capped by the whole-file estimate, so an over-long range can never look cheaper than the file itself.
The categories
The menu keeps the category rows — five built in, plus the bundled @git — in its bottom band, the rows nearest the composer, so pressing @ opens on them rather than on whatever else shares the trigger.
| Category | Menu prefix | Shortcut | Candidates from | Written into the draft |
|---|---|---|---|---|
| File | file: |
F |
The session workspace index | @src/index.ts |
| Folder | folder: |
D |
Same index | @src/client/ |
| Skill | skill: |
S |
The ctx.skills registry |
@skill:blender-modeling |
| Past chat | chat: |
C |
The official session-reference resolver | @[title](dsh-session:) |
| Plugin | plugin: |
P |
The official plugin inventory | @plugin:dsh-atlas |
One shortcut letter is a statement of intent, not a query. Typing it shows the category rows — five built-in plus the bundled @git — and a Tab 补全 → category: hint as the last row, with the highlight already on that hint; Tab or Enter then enters the category: prefix. A registered source can claim a shortcut letter too: the first letter of its id (@git → G), and only while no built-in category or earlier provider answers to that letter (the @: prefix always works). Filtering starts on anything more specific — a first letter that is not a shortcut, or a second character (which is what the fi/fo/sk/ch/pl spellings are). Clicking a category row or the back row enters/returns the same way and keeps the menu open — that is this plugin's own override; the normal pick path closes it.
Registered sources are the sixth row and beyond. @git ships built in (the workspace's changed files, with each file's diff as the referenced content), and any plugin can add its own category the same way — see Write your own @ source. They appear after the built-ins, in registration order, and disappear when the plugin that registered them is disposed. The plugin's group itself is registered with a positive order, so it is the last group in the menu: the bottom band belongs to this plugin, and the stock dsh-client-ui-reference source's file list stays above it.
Interaction
- Typing letters searches every category at once, split into a "recently referenced" section and an "all matches" section, with a category tag on each row; the category rows stay pinned and clickable.
- Folders: the
@folder:category lists directories; picking one writes@path/into the draft (the trailing slash is what marks a directory, and the plugin still reads directory metadata only). Keep typing after the slash to narrow the search inside it. While filtering, the rows are ranked in two tiers: folders whose NAME matches first — the workspace's own, then the ones outside it — and then, in a group of their own (路径匹配), the folders that only their PATH matches. Folding that group is your gesture; it opens by default and shows whatever fits. - A folder you name first scopes the file list:
@e:/work/docs/ @file:lists that folder's own files (one level, through the same directory listing the folder tab uses) under a group header that says which folder they came from and that it is outside the workspace, before the workspace's own matches. The scope is positional — the nearest folder reference typed before the category token — so no connective word is needed, and a draft with no folder reference behaves exactly as before. - Grouping: skills group by management tier (系统 / 用户 / 项目 / 自定义 / 插件) and then by domain (blender, competition, dsh, …); plugins group by npm scope (
@deepseek-ai/…or 其他); past chats group by workspace with the sidebar's relative times and children indented under their parent. Every parent row collapses or expands instantly on click, or on Enter while that group header is the highlighted row. Tab completion, ↑↓, and Esc are the harness's own keymap. Beyond those, the plugin adds exactly three keys of its own: Enter on a highlighted group header folds it, the category and back rows keep the menu open when picked, and PageUp/PageDown page the list by one measured screen — those two move the highlight (or scroll the list when the highlight belongs to another source), so the focused row is always the one you can see. - Long lists scroll instead of hiding rows: a category shows at most
MAX_CANDIDATES(20) rows, and when they do not fit the menu scrolls — PageUp/PageDown walk it a screen at a time, and the plugin re-asserts the focused row's visibility whenever the content changes (a scoped folder's listing arriving late, a group being folded). Rows are therefore never dropped merely to keep the menu short; the tiers above decide what comes first. - Initial view:
@file:lists recently referenced files first (most recent first, persisted per workspace), then alphabetical order. - Cost and staleness: each file reference in the dock shows its approximate token cost (
≈1.2k tokens, highlighted above 8 000); a reference whose path no longer resolves is marked Missing. Cost comes from the file size (~4 bytes per token) and the plugin reads entry metadata only — never file content. - Typo tolerance: a lightly mistyped filename (
veiw→view.ts,clinet→client/…) still matches. Exact ranking wins: the fuzzy pass only runs when the exact ranking found nothing, and queries shorter than three characters or containing/stay exact. - Line ranges: type
@src/a.ts:12-40to reference lines 12–40 of that file (a single line is@src/a.ts:7; a reversed40-12is normalized to12-40). Directories take no range. The Host validates syntax and path kind only and never opens the file, so whether the lines exist is the agent's business when it reads. - Reference dock: every reference in the draft appears above the composer; file, folder and provider rows open on click, and each row's × removes its token.
- Every draft reference the plugin can open is a link: the test is the PLUGIN's own — the token decodes into a reference and the Host has confirmed the target exists — not whether the framework happened to decorate it. A hand-typed
@AGENTS.mdbehaves exactly like one the editor recognised: the whole token is blue, a click anywhere inside it opens the reference, and the pointer turns into a hand. The colour is painted with the CSS Custom Highlight API (a Lexical text span expects exactly one text child, so wrapping any part of a token would break typing inside it) in the very colour the framework uses for its own references. A token whose target is gone is not left as ordinary text either: it is painted the dock's own Missing way — dimmed and struck through — because it is a reference that cannot open. When that verdict is drawn matters: only a finished token is judged (one followed by whitespace, or by another line, i.e. after Enter), because a token still being typed is every prefix of a path at once and@Nis always "missing" — striking it through mid-word would judge a word the user has not written yet. A token that CAN open turns blue the moment its name is complete, exactly as the framework's own decoration does. A token whose verdict has not arrived, whose provider declared noopen, or whose name cannot be placed claims nothing and stays ordinary text. On a browser without the highlight API nothing is painted at all — there only the part the framework itself coloured acts as a link, because nothing else is drawn as one. - A reference another source inserted as a chip is clickable too: the sidebar's file tree writes one into the draft as an atomic chip, which this client build draws in the framework's chip blue and leaves inert. Clicking one opens the file it names, and the chip then carries the plugin's link language. That source labels a chip with the file's basename (the full relative path lives only in the draft text), so a bare name is placed in two steps: first the plugin's own index, where a unique basename settles it; then, when the index itself is ambiguous (
index.tsexists twice in this workspace), the draft — one token with that basename decides, two leave the chip inert. Only a name the index does not know at all is handed to the click's own existence check, which marks the chip stale rather than opening something wrong. - References in sent messages are clickable: a chip in a message bubble opens on click (files in the right Sidebar, the Host opener when there is none; skills through the skill source; provider items through the
opentheir own provider declared). Only a chip that really opens is drawn as a link — the framework's own--dsw-alias-linkblue with a hover underline — and a build that wires the chips itself renders`, which this plugin leaves alone. The plugin also implements the framework'sopenReference` hook, so a client that activates a reference token in the composer asks its owning source to open it. The installed client activates nothing yet, so the plugin supplies that click itself, through the very action the chips run. - A session reference is a link: identity here is decided by VALUE, because the id is not in the path. The wire form
@[label](dsh-session:…)on its own names its session (the payload is decoded and must round-trip canonically), a label with spaces is still ONE token, and a hand-typed bare@is promoted only on exactly ONE match from the session list — an ambiguous or unknown name stays inert, so@AGENTS.mdstays the file reference it is. Clicking switches the current session through the same action a sidebar row performs. Boundary: a message already SENT can only be opened when its chip still carries the wire payload; once the Host folds it to a bare label the id is gone, which is why this plugin filed that upstream. - Icons: every row this plugin emits carries its own glyph — the same modern line set the reference dock uses, including per-file-type and per-language marks (TypeScript, Rust, PDF, image, archive, …) and a branch glyph for
@git. The framework's menu row can only draw three glyphs of its own, so the plugin reserves the slot (which is what indents every name by the same amount) and draws the glyph itself, monochrome, in a fixed layer above the list: nothing is inserted into a row the framework owns, and the framework's own glyph is hidden only in the slots this layer actually covers.
The menu (scrolled to its bottom band): a mixed, tagged result list — file rows with their directory, a collapsed plugin group — above the five category rows plus @git, each with the plugin's own glyph. The band is the last group, nearest the composer, and the menu is registered with a positive order so the stock reference source stays above it.
The reference dock above the composer: one row per mention in the draft, each with its own glyph, its approximate token cost (≈26k tokens in red is past the 8 000 highlight) and its remove button — and every token in the draft painted as a link.
History jump (Esc)
Two ways to reach back into the session without touching the scrollbar.
- Roam in place. With an empty composer, Esc puts the draft aside and shows a readout above it —
历史 第 3 / 19 轮, or… 条when the granularity is per message. ↑/↓ walk one stop at a time, PageUp/PageDown five, Home/End jump to the oldest stop or back to your own draft. Typing any printable character, or Backspace, Tab, leaves the roam and lets the key do its ordinary job. Enter on a stop you wrote reuses that text; on any other stop it only leaves the roam. - The palette. Ctrl/Cmd+Shift+H (the same chord without Shift also opens it) lists the stops with their turn number and a preview; type to filter, ↑/↓ to move, Enter to jump, Esc to close.
- One stop per turn by default — the same ruler as the turn rail down the right edge of the conversation, so
第 N / M 轮in the readout and tick N of the rail name the same turn. Switch History granularity to per message to stop on the prompt and on each answer separately. - A turn's answer is a stop too, not part of its prompt: landing on a turn lands on what was answered, so a session whose turns were all started for you is still walkable. System-initiated turns are stops as well, which is why a fresh session with one system turn shows
第 1 / 1 轮rather than nothing. - Keys that would surprise you yield. While the
@menu is open, a modal is up, or a turn is generating, Esc keeps its ordinary meaning — the roam never swallows the first Esc of a running turn. - Reuse writes the stop's text back into the composer: replace (default) or append, per Reuse mode. Ctrl/Cmd+↑/↓ reuses directly without entering the roam; it clamps at both ends instead of wrapping, and its position resets to your draft after you send, so the first Ctrl+↑ always starts from where you are. The roam keeps its own cursor and does wrap at both ends.
- Smart follow off by default: scrolling the conversation or clicking the rail does not move either cursor. Turn it on in settings if you want the roam to start wherever you last looked.
The roam readout above the composer after Esc: 历史 第 0 / 1 轮 · 当前草稿 — standing on your own draft, of one turn in this session, at turn granularity. ↑ takes you to that turn.
Injected references
Before each agent step, the plugin validates every reference in the draft and appends one message carrying only the reference — never file content. The agent reads referenced content on demand with the tools available to the session.
Review @docs/spec.pdf
Use @skill:blender-modeling
Recall @[Payment rates](dsh-session:xxx)
With @plugin:dsh-atlas
Take the diff of @atlas:git/src/extract.ts
| Category | Injected marker | Message source |
|---|---|---|
| File / Folder | ` (addslines="12-40"` for a range) |
at-file-mention |
| Skill | `` | atlas-skill |
| Plugin | `` | atlas-plugin |
| Past chat | The official session-reference read-only snapshot (replayable, budgeted) |
session-reference |
A registered @ source |
… |
atlas-provider |
- A referenced directory injects that directory only; its contents are never expanded here.
- Absolute paths and paths escaping the workspace are ignored.
- A referenced path must still exist; unknown or out-of-workspace tokens produce no marker.
- Skill bodies are not injected by default (see
injectSkillBody); the agent loads them through its skill tool. - Past-chat snapshots are budgeted by the official implementation (64 KB per source, at most 3 references, self-reference refused).
- An
@atlas:reference is the one marker that carries a body. That body is the provider's own answer, never a file this plugin opened: it is bounded to 16 KiB per reference and 48 KiB per step, and an over-long body is cut and markedtruncated="true".
Write your own @ source
The menu is the seam's only consumer. It rebuilds its category list from the live registry on every open, so a plugin adds a @ category by registering one — nothing in this package enumerates providers, and no rebuild of it is needed.
// Host half — what a committed reference turns into. `ctx.get('atlas')` is the seam.
import type { AtlasProvider, AtlasSeam } from '@sidequest-007/dsh-atlas'
const provider: AtlasProvider = {
id: 'diag', // becomes @atlas:diag/… in the draft
display: 'Diagnostics',
scopes: ['process:lsp'], // what you reach for (required)
testedOn: ['0.1.5-rc.1'], // DSH builds you actually ran (required)
async resolve(item, context) {
// context.cwd is the answered session's workspace; context.sessionId names it.
return await diagnosticsFor(context.cwd, item.id, context.signal)
},
}
const atlas = ctx.get('atlas') as AtlasSeam
atlas.register(provider) // returns a disposer
// Browser half — candidates for the open menu, from your own client bundle.
const provider: AtlasProvider = {
id: 'diag',
display: 'Diagnostics',
scopes: ['process:lsp'],
testedOn: ['0.1.5-rc.1'],
async list(query, context) {
return await askYourHost(query, context.sessionId, context.signal) // must stay cheap
},
open(item, context) {
// Optional: what a click on `@atlas:diag/…` in an ALREADY SENT message does.
// Only you know what your item means (a file path? a URL? a record id?), so
// the menu never interprets it — without `open`, the chip stays inert.
openInYourViewer(item)
},
}
A provider may register either half or both; id ties them together. What the user commits is plain text (@atlas:diag/src/a.ts:12), so provider content never enters the draft, and only the Host half decides what the model sees.
| Rule | Why |
|---|---|
| Registration is authorization | A plugin that never registers is invisible: nothing is scanned, guessed or discovered |
list runs while the menu is open |
It decides how the menu feels; cache it yourself if it is not free |
resolve runs once, at send |
It may be expensive, and it receives the session's cwd and an AbortSignal |
open is optional and menu-half only |
The click happens in the browser; without it the chip is inert, and never drawn as clickable |
scopes and testedOn are required |
register() refuses a declaration without them |
testedOn is compared for equality |
A mismatch registers as verified: false; an unknown running version is never verified |
id is unique |
A duplicate throws and names the first registrant — no silent overwrite |
| The seam bounds every body | 16 KiB per reference, 48 KiB per step; the overflow is marked, not passed on |
| An empty body injects nothing | A provider saying "nothing to add" costs no tokens |
The verdict is re-read every time the registry is read, so a provider that registers before the browser has learned the running version does not stay unverified.
What ATLAS will not carry
A source passes all three, or the answer is no:
| Refuse | Why |
|---|---|
| What the model can already get | @file and @session are built in; a second route to the same data is noise |
| What cannot be narrowed | A whole logcat or a full database dump poisons the context; a reference is a selection |
What is expensive to resolve |
Lazy loading is the contract, not a preference |
The test: narrowable · previewable · useful the moment it lands.
@git is the worked example, and it is built the way a third party would build one. See src/git.ts (Host: reads the change list and produces one diff, never opening a file) and src/client/git-provider.ts (browser: asks the Host over atlas/gitChanges, filters per keystroke, caps the list).
Model-facing tools
Beyond the markers injected at send time, the model can query these categories on demand:
| Tool | Purpose |
|---|---|
past_chats |
List past sessions by title or workspace — follow-up to an @past chat reference |
read_past_chat |
Read one referenced session's current user/assistant surface |
plugin_info |
List installed plugins and whether each is enabled, filtered by module name |
Settings
Managed in Settings → Workspace file mentions:
| Setting | Default | Meaning |
|---|---|---|
| Enable @ file mentions | on | Master switch; turning it off hides the @ menu and dock and stops injecting markers |
| Enable @Skill mentions | on | Show skills in the menu |
| Enable @past chats mentions | on | Show past sessions in the menu |
| Enable @plugin mentions | on | Show installed plugins in the menu |
| Candidate limit | 50 | Maximum candidates returned per request (1–200) |
| Ignore @ mentions in pasted text | on | @tokens pasted from other applications stay plain text |
| File filters | — | Global and per-workspace rules; each rule is Exact or Regex, with its own case setting |
| Enable history jump | on | Master switch for Esc and the palette; off hands every key back to the composer |
| History granularity | per turn | One stop per turn (the rail's own ruler), or one per message — a prompt and each answer separately |
| Reuse mode | replace | Whether a reused stop replaces the draft or is appended to it |
| Smart follow | off | Let the roam cursor start where the conversation was last scrolled to or the rail last clicked |
File filters match basenames only (never directory paths). Workspace rules apply alongside global rules; editing rules clears the index cache, so the next @ uses them.
Configuration
These parameters live in the profile's cordis.patch.yml (usually ~/.dsh/profiles/web/cordis.patch.yml):
- id: dsh-atlas
config:
maxIndexedFiles: 2000 # workspace index entry cap
ignoreDirs: [] # replaces the built-in ignore list; [] indexes every directory
injectSkillBody: false # also inject the skill body when a skill is referenced
Omitting ignoreDirs keeps the built-in list (version-control directories, IDE metadata, dependency directories, caches, and build output).
Design constraints
- Paths only, never content: the Host validates and injects paths, categories, and provenance markers. It never opens a referenced file and never lists a referenced directory.
- Paste protection: external text cannot forge a reference — only
source.kind === 'user'messages are scanned, and pasted content is marked as plain text by default. - Workspace confinement: references stay inside the session workspace; absolute paths and
..escapes are refused. - Model-visible means logged: every injected marker carries its source and is persisted with the session log.
- The display slot stays out of matching: the display slot beside the
@menu is fully decoupled from candidate computation. It never blocks or slows a search, and it reads no session data.
Development
Prerequisite: the dev dependencies are link: entries into a DSH source checkout, expected at ../deepseek-harness beside this repository. Without it pnpm install cannot resolve them, so a plain clone is not buildable on its own (the published packages — e.g. @deepseek-ai/dsh-client-ui-conversation@0.1.6-alpha.1 — are on npm, but this setup is pinned to the checkout on purpose: it is how the plugin is tested against the same source the Harness is built from).
git clone https://github.com/jameswatt139240-crypto/dsh-ATLAS
git clone ../deepseek-harness # or repoint the links in package.json
cd dsh-ATLAS
pnpm install
pnpm run check # typecheck + tests + ad-free build + publish-surface gate
pnpm run build # build only (no ad panel; lib/ is committed)
pnpm run build:ads # build the ad-bearing variant (not for the first release)
pnpm run verify:publish # publish-surface gate on its own
lib/ is committed, so profile installs run without a build. See CONTRIBUTING.md for the ladder a change has to pass.
Publishing
- The default build carries no ad.
pnpm run buildreplaces the ad panel and its
banner images with stubs, so neither ad code nor image bytes reachlib/client.js
(~726 KB ad-free vs ~978 KB with ads). Usepnpm run build:adsfor a later
ad-bearing release. - The publish-surface gate runs automatically before
npm publish
(prepublishOnly) and refuses: the internal plan directory (kept out of git and
out of the tarball), AI-assistant files
(AGENTS.md,CLAUDE.md,.agents/,.claude/,.cursor/,skills/),
TypeScript sources and tests, sourcemaps (they inline sources and local paths),
a bundle that still inlines an ad banner, a bundle that leaks a local machine path,
and any of the four places that must agree on the package name
(package.json,dsh.plugin.json,cordis.patch.yml, the client bundle id, plus
the invariant companion). SetDSH_ATLAS_ALLOW_ADS=1for a deliberate ad release. - Releasing: the tag is the decision.
pnpm run check # full ladder, needs ../deepseek-harness
git tag -a v1.0.0 -m "dsh-ATLAS 1.0.0"
git push origin main --tags # the release workflow publishes
.github/workflows/publish-surface.yml runs the self-contained gate on every push:
it needs no registry and no DSH checkout, because it packs the committed lib/ and
inspects the tarball. .github/workflows/release.yml publishes on a v* tag with
trusted publishing (OIDC) — no NPM_TOKEN, no OTP: GitHub mints a short-lived
identity (permissions: id-token: write) and npm verifies it against the trusted
publisher configured for this package. That configuration is one-time, lives at
https://www.npmjs.com/package/@sidequest-007/dsh-atlas/access → Trusted
Publisher (user jameswatt139240-crypto, repository dsh-ATLAS, workflow filename
release.yml, "publish directly" allowed), and could only be added after the
first version existed — npm attaches a trusted publisher to a package, so 1.0.0
itself was published interactively. The job also skips itself when the tagged
version is already on npm, and runs npm publish --ignore-scripts (the full ladder
needs the DSH source checkout, which CI does not have). To publish by hand instead,
run npm publish --registry https://registry.npmjs.org (a mirror such as
registry.npmmirror.com cannot accept a publish).
If this replaced something you were using
dsh-at-file stopped loading on Harness 0.2.x, and this carried its file index onto the current API. If that is why you are here, one thing helps more than a bug report: a star. The plugin markets sort by it, and a project sitting at zero reads as abandoned whether or not it is — a poor first impression for a plugin whose entire job is to be dependable about paths.
→ Star the repo · Report a bug
Compatibility
The plugin satisfies both generations of the Harness Typert codec validation (the installed build checks codec.schema.parse; the source checkout checks codec.create()), and opens referenced paths through ctx.remote.session.openWorkspacePath.
License
MIT (see LICENSE) — use it, modify it, ship it, sell it; keep the copyright notice with the copies you distribute. It is an OSI-approved license, so plugin markets that require one are fine with it.
This project is a derivative work of dsh-at-file (MIT). Its original copyright notice and license text are preserved in LICENSE-ORIGINAL, and NOTICE maps which files came from it.