@este.systems/dsc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Este Systems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,408 @@
1
+ # dsc
2
+
3
+ A CLI coding agent for [DeepSeek](https://api-docs.deepseek.com/).
4
+ Streams responses, calls tools (`bash`, `read_file`, `write_file`, `edit_file`,
5
+ `grep`, `glob`, `web_fetch`, `web_search`), keeps per-cwd sessions, and runs
6
+ in your terminal as a plain readline REPL — output stays selectable / pasteable
7
+ and approvals happen inline.
8
+
9
+ ## Install
10
+
11
+ dsc requires **Node 22+** (it uses `fs.promises.glob`, stable since 22.0).
12
+
13
+ ### Step 1 — install Node 22+
14
+
15
+ | Platform | One-liner |
16
+ |---|---|
17
+ | **Linux (Debian/Ubuntu)** | `curl -fsSL https://deb.nodesource.com/setup_22.x \| sudo -E bash - && sudo apt install -y nodejs` |
18
+ | **Linux (Fedora/RHEL)** | `sudo dnf install -y nodejs:22/common` |
19
+ | **Linux (Arch)** | `sudo pacman -S nodejs npm` |
20
+ | **macOS** | `brew install node@22 && brew link node@22` |
21
+ | **Windows (winget)** | `winget install OpenJS.NodeJS.LTS` |
22
+ | **Windows (scoop)** | `scoop install nodejs-lts` |
23
+ | **Cross-platform (nvm)** | `nvm install 22 && nvm use 22` |
24
+
25
+ Verify: `node --version` should print `v22.x.x` or higher.
26
+
27
+ ### Step 2 — install dsc
28
+
29
+ Three options. Once the package is published to npm, **(A)** is what you want.
30
+ Today, use **(B)** for a frozen tarball or **(C)** if you'll be hacking on the
31
+ source.
32
+
33
+ **(A) From npm** *(after publish)*:
34
+
35
+ ```sh
36
+ npm install -g @este.systems/dsc
37
+ ```
38
+
39
+ **(B) From a local tarball** *(works on all platforms with the same npm)*:
40
+
41
+ ```sh
42
+ git clone https://github.com/EsteSystems/dsc.git
43
+ cd dsc
44
+ npm install
45
+ npm run package # produces pkg/<name>-<version>.tgz
46
+ ```
47
+
48
+ then on **Linux / macOS**:
49
+
50
+ ```sh
51
+ scripts/install.sh
52
+ ```
53
+
54
+ or on **Windows PowerShell**:
55
+
56
+ ```powershell
57
+ .\scripts\install.ps1
58
+ ```
59
+
60
+ Both wrappers just call `npm install -g pkg/*.tgz` after auto-finding the
61
+ tarball.
62
+
63
+ **(C) From source for development** *(live edits, no rebuild step)*:
64
+
65
+ ```sh
66
+ git clone https://github.com/EsteSystems/dsc.git
67
+ cd dsc
68
+ npm install
69
+ npm link # exposes `dsc` globally
70
+ ```
71
+
72
+ The shim in `bin/dsc.mjs` runs the TypeScript sources directly through
73
+ `tsx`, so your next `dsc` launch picks up edits immediately. If `tsx`
74
+ isn't available it falls back to `dist/` (populate with `npm run build`).
75
+
76
+ ### Step 3 — verify
77
+
78
+ ```sh
79
+ dsc --help
80
+ ```
81
+
82
+ If you get "command not found", your shell's `PATH` doesn't include npm's
83
+ global-bin directory. Find it with `npm config get prefix`; the binary
84
+ lives in `<prefix>/bin` on Linux/macOS or `<prefix>` on Windows. Add that
85
+ to your `PATH` and reopen the terminal.
86
+
87
+ ## API key
88
+
89
+ dsc reads its DeepSeek key from `~/.config/deepseek/deepseek.json` on every
90
+ platform — Node's `os.homedir()` resolves to `C:\Users\<you>` on Windows, so
91
+ the actual file path is `C:\Users\<you>\.config\deepseek\deepseek.json`. Set
92
+ `XDG_CONFIG_HOME` if you'd rather put it elsewhere.
93
+
94
+ **Linux / macOS:**
95
+
96
+ ```sh
97
+ mkdir -p ~/.config/deepseek
98
+ cat > ~/.config/deepseek/deepseek.json <<'JSON'
99
+ {
100
+ "api_key": "sk-..."
101
+ }
102
+ JSON
103
+ chmod 600 ~/.config/deepseek/deepseek.json
104
+ ```
105
+
106
+ **Windows PowerShell:**
107
+
108
+ ```powershell
109
+ $dir = "$HOME\.config\deepseek"
110
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
111
+ '{"api_key":"sk-..."}' | Set-Content -Path "$dir\deepseek.json" -Encoding utf8
112
+ ```
113
+
114
+ Accepted shapes:
115
+
116
+ ```jsonc
117
+ { "api_key": "sk-..." } // simple
118
+ { "DEEPSEEK_API_KEY": "sk-..." } // alt key
119
+ { "env": { "DEEPSEEK_API_KEY": "sk-..." } } // env-style
120
+ { "env": { "ANTHROPIC_AUTH_TOKEN": "sk-..." } } // claude-switcher compat
121
+ ```
122
+
123
+ The env var `DEEPSEEK_API_KEY` takes priority over the file when set.
124
+
125
+ ## Quick start
126
+
127
+ ```sh
128
+ dsc # interactive REPL
129
+ dsc "summarize src/api.ts" # one-shot
130
+ dsc --yolo "rename Foo to Bar" # skip approval prompts
131
+ dsc -m deepseek-v4-flash # pick a model
132
+ dsc --no-resume # fresh session, ignore prior history
133
+ dsc --resume <id> # resume a specific session id
134
+ ```
135
+
136
+ ## REPL commands
137
+
138
+ | Command | What it does |
139
+ |---|---|
140
+ | `/clear` | Start a new session. Old session stays on disk. |
141
+ | `/cost` | Show token usage and estimated cost so far. |
142
+ | `/model [name]` | Show or switch model. Available: `deepseek-v4-pro`, `deepseek-v4-flash`. |
143
+ | `/yolo` | Toggle approval mode (write/edit/bash/web_fetch). |
144
+ | `/reasoning [on\|off]` | Show/hide `reasoning_content` from thinking models. Default on. |
145
+ | `/list` | List sessions in the current cwd. The active session is marked with `*`. |
146
+ | `/resume <#\|id\|last>` | Resume a session by index (from `/list`), id, or `last`. |
147
+ | `/audit` | Print the path of the JSONL audit log. |
148
+ | `/transcript` | Print the full conversation, including any messages compaction archived. |
149
+ | `/compact [N]` | Summarize older turns into a synthetic block (kept in the system prompt) and move them to the archive. Keeps the last `N` user turns verbatim (default 4). Cumulative across re-runs. |
150
+ | `/edit [text]` | Open `$VISUAL`/`$EDITOR`/`vi` on a tmp file; the saved content runs as the next prompt. |
151
+ | `/exit` | Quit. |
152
+
153
+ ### Multi-line input
154
+
155
+ End a line with a single `\` to continue on the next line (bash-style).
156
+ `\\` is treated as a literal trailing backslash. For longer or paste-heavy
157
+ drafts, use `/edit`.
158
+
159
+ ```
160
+ > please write a function\
161
+ … that takes (a, b, c)\
162
+ … and returns a + b + c
163
+ ```
164
+
165
+ ### Hotkeys
166
+
167
+ `Ctrl+C` aborts the current turn (first press), exits if pressed again
168
+ within 1 second. `Ctrl+D` exits cleanly. Up / down arrows recall past
169
+ prompts (persisted across sessions).
170
+
171
+ ### Session scoping (per-directory)
172
+
173
+ Sessions are tied to the directory you launched dsc from — that's how
174
+ auto-resume figures out which conversation to bring back.
175
+
176
+ - Run `dsc` in `~/code/foo` → it auto-resumes the most recent session
177
+ whose `cwd` is `~/code/foo`. If there isn't one, you start fresh.
178
+ - `cd` into `~/code/bar` and run `dsc` → you get bar's last session,
179
+ not foo's. Project conversations stay scoped to their project; you
180
+ can't accidentally bleed astrophysics notes into a CLI refactor.
181
+ - `/clear` starts a brand-new session **for the current cwd**. The old
182
+ one stays on disk and shows up in `/list` next time you're back.
183
+ - `/list` shows only sessions whose `cwd` matches the current
184
+ directory. `/resume <#>` resolves indices from that list.
185
+ - `/resume <id>` (or a `/save`'d name) **can** cross cwds — useful
186
+ if you want to revisit a session from elsewhere; the cwd it was
187
+ born in stays attached to it.
188
+ - `--no-resume` skips auto-resume entirely and gives you a fresh
189
+ session this launch.
190
+
191
+ Each session is a single JSON file under
192
+ `~/.local/share/dsc/sessions/`. Open one in your editor if you want to
193
+ see exactly what got persisted, including any compacted summary and
194
+ the archived messages from `/transcript`.
195
+
196
+ ## Tools the agent can use
197
+
198
+ | Tool | Approval | Notes |
199
+ |---|---|---|
200
+ | `read_file(path, offset?, limit?)` | none | 2000 lines default; long lines truncated. |
201
+ | `grep(pattern, path?, glob?, case_insensitive?)` | none | ripgrep when available, `grep -rn` fallback. |
202
+ | `glob(pattern, path?)` | none | Node 22+ `fs.glob`, capped at 500. |
203
+ | `web_search(query, count?, freshness?)` | none | Pluggable backends (Brave / Tavily / DuckDuckGo). |
204
+ | `write_file(path, content)` | yes (unless `--yolo`) | Side-by-side diff in the prompt. |
205
+ | `edit_file(path, old_string, new_string, replace_all?)` | yes | Exact substring replace; old_string must be unique unless `replace_all=true`. |
206
+ | `bash(command, description?, timeout_ms?)` | yes | `/bin/sh -c`, output capped at 16 KB. |
207
+ | `web_fetch(url)` | yes | HTML stripped to text, capped at 50 KB. |
208
+
209
+ Read-only tools never prompt. The rest do unless `--yolo` is on.
210
+
211
+ ## Sessions and history
212
+
213
+ Each session is a JSON file under `$XDG_DATA_HOME/dsc/sessions/`
214
+ (default `~/.local/share/dsc/sessions/`) keyed by id. It carries:
215
+
216
+ - `messages` — the active conversation log (sent to the API).
217
+ - `archivedMessages` — older messages that `/compact` has summarized away.
218
+ Persisted on disk for `/transcript`, never sent to the API.
219
+ - `compaction` — the cumulative summary text and metadata.
220
+ - `stats` — token / cost / tool-call counters.
221
+ - `model` — last selected model.
222
+
223
+ Saves happen on every `onTurn` callback (after each assistant message,
224
+ after each tool result), with a single-in-flight, coalescing writer —
225
+ so a Ctrl+C / OOM / power loss mid-turn won't cost you the latest
226
+ committed state.
227
+
228
+ ## Compaction
229
+
230
+ `/compact [N]` summarizes everything before the last `N` user turns,
231
+ stores the summary on the session, archives the original messages, and
232
+ trims `messages` to the kept tail. The summary appears in the dynamic
233
+ suffix of every subsequent system prompt (`Previously in this session:`),
234
+ so the model retains semantic context. Cumulative — re-running `/compact`
235
+ folds the prior summary into the new one.
236
+
237
+ Auto-compact runs the same routine after any successful turn whose
238
+ estimated context exceeds `DSC_AUTO_COMPACT_AT` tokens (default 50 000;
239
+ set `0` / `off` / `false` to disable).
240
+
241
+ `/transcript` prints the full conversation, including archived chunks,
242
+ so nothing is lost — just absent from the prompt the model sees.
243
+
244
+ ## Audit log
245
+
246
+ Every tool execution (including rejected ones) writes one JSONL line to
247
+ `$XDG_STATE_HOME/dsc/audit.log` (default `~/.local/state/dsc/audit.log`).
248
+ Each line carries `ts`, `session`, `cwd`, `tool`, `approved`, plus
249
+ tool-specific fields:
250
+
251
+ ```json
252
+ {"ts":"2026-05-09T15:32:01Z","session":"…","cwd":"/home/dann/code/dsc","tool":"bash","approved":true,"command":"npm test","exit":0,"stdout_bytes":4012,"stderr_bytes":0}
253
+ ```
254
+
255
+ Useful greps:
256
+
257
+ ```sh
258
+ # every bash command this week
259
+ jq 'select(.tool=="bash") | "\(.ts) \(.command)"' ~/.local/state/dsc/audit.log
260
+
261
+ # things rejected at approval
262
+ jq 'select(.approved==false)' ~/.local/state/dsc/audit.log
263
+
264
+ # files written by a specific session
265
+ jq 'select(.session=="abc1234" and .tool=="write_file") | .path' \
266
+ ~/.local/state/dsc/audit.log
267
+ ```
268
+
269
+ Disable with `DSC_NO_AUDIT=1`. There's no rotation — at gigabyte scale
270
+ you'll want to truncate it yourself.
271
+
272
+ ## File locations
273
+
274
+ | Path | What |
275
+ |---|---|
276
+ | `~/.config/deepseek/deepseek.json` | API key (and search-provider keys). 0600. |
277
+ | `~/.local/share/dsc/sessions/<id>.json` | One file per session. |
278
+ | `~/.local/state/dsc/history` | Up/down arrow recall (1000-line cap). |
279
+ | `~/.local/state/dsc/audit.log` | JSONL, append-only. |
280
+ | `/tmp/dsc-edit-*/prompt.md` | Transient; created by `/edit`, removed on close. |
281
+ | `<repo>/dist/` | Compiled output (only used as a fallback for the global shim). |
282
+
283
+ XDG variables observed: `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `XDG_DATA_HOME`.
284
+
285
+ ## Environment variables
286
+
287
+ | Var | Default | Purpose |
288
+ |---|---|---|
289
+ | `DEEPSEEK_API_KEY` | (read from config file) | Overrides the `api_key` in `deepseek.json`. |
290
+ | `DSC_AUTO_COMPACT_AT` | `50000` | Token threshold for auto-compact. `0`/`off`/`false` disables. |
291
+ | `DSC_NO_AUDIT` | (off) | `1` disables the JSONL audit log. |
292
+ | `DSC_SEARCH_PROVIDER` | (config or `ddg`) | `brave`, `tavily`, or `ddg`. |
293
+ | `BRAVE_API_KEY` | (config) | Brave Search key. |
294
+ | `TAVILY_API_KEY` | (config) | Tavily key. |
295
+ | `VISUAL` / `EDITOR` | (vi) | Used by `/edit`. |
296
+ | `XDG_CONFIG_HOME` | `~/.config` | Config root. |
297
+ | `XDG_STATE_HOME` | `~/.local/state` | State root. |
298
+ | `XDG_DATA_HOME` | `~/.local/share` | Data root. |
299
+
300
+ ## Search providers
301
+
302
+ Pick at runtime with `DSC_SEARCH_PROVIDER`:
303
+
304
+ - **brave** — [api-dashboard.search.brave.com](https://api-dashboard.search.brave.com), 2000 free queries/month. Recommended.
305
+ - **tavily** — [tavily.com](https://tavily.com), 1000 free/month, agent-tuned snippets.
306
+ - **ddg** — DuckDuckGo HTML scrape, no key, brittle.
307
+
308
+ Per-provider keys live in the config:
309
+
310
+ ```jsonc
311
+ {
312
+ "api_key": "sk-...",
313
+ "search": {
314
+ "provider": "brave",
315
+ "brave": { "api_key": "BSA..." }
316
+ }
317
+ }
318
+ ```
319
+
320
+ `{PROVIDER}_API_KEY` env var (e.g. `BRAVE_API_KEY`) overrides the
321
+ file value.
322
+
323
+ ## Packaging and distribution
324
+
325
+ For local global install (any platform with Node 22+):
326
+
327
+ ```sh
328
+ npm run package # produces pkg/<name>-<version>.tgz
329
+ scripts/install.sh # linux / macOS — wraps `npm install -g pkg/*.tgz`
330
+ .\scripts\install.ps1 # Windows PowerShell — same idea
331
+ ```
332
+
333
+ The build is driven by the `prepack` lifecycle hook (`scripts/build.mjs`),
334
+ which wipes `dist/` before recompiling. That keeps stale artifacts (e.g.
335
+ leftover from a branch switch) out of the tarball whether you ran
336
+ `npm pack`, `npm publish`, or `npm run package`.
337
+
338
+ What ships is controlled by the `files` field in `package.json` (currently
339
+ `bin/`, `dist/`, `README.md`, `LICENSE`). Source TypeScript and devDeps are
340
+ deliberately excluded.
341
+
342
+ ### Publishing to npm
343
+
344
+ The package is configured to publish as `@este.systems/dsc` with public
345
+ access. To release:
346
+
347
+ ```sh
348
+ # 1. Bump the version (semver). For a pre-1.0 patch:
349
+ npm version patch # → 0.1.1, also creates a git tag
350
+
351
+ # 2. Make sure you're logged in to npm:
352
+ npm whoami # should print your username
353
+ npm login # if not
354
+
355
+ # 3. (Optional) preview the tarball:
356
+ npm pack --dry-run
357
+
358
+ # 4. Publish:
359
+ npm publish # respects publishConfig.access=public
360
+
361
+ # 5. Push the version-bump commit and tag:
362
+ git push --follow-tags
363
+ ```
364
+
365
+ Notes:
366
+
367
+ - The package name is **scoped** (`@este.systems/dsc`), so `npm publish`
368
+ defaults to private. `publishConfig.access = "public"` in `package.json`
369
+ overrides that. Don't drop it.
370
+ - npm now nudges hard for **2FA**. Enable with
371
+ `npm profile enable-2fa auth-and-writes`. You'll be asked for an OTP on
372
+ every publish.
373
+ - Once published, anyone can install with
374
+ `npm install -g @este.systems/dsc`. The CLI binary is still just `dsc`.
375
+ - After publish, the `pkg/*.tgz` produced locally is identical to what
376
+ you uploaded — useful for offline installs (`scripts/install.sh`).
377
+
378
+ ## Development
379
+
380
+ ```sh
381
+ npm run dev # tsx src/index.ts
382
+ npm run typecheck # tsc --noEmit
383
+ npm run build # compiles to dist/
384
+ npm run package # build + npm pack into pkg/
385
+ ```
386
+
387
+ Source layout:
388
+
389
+ ```
390
+ src/
391
+ index.ts # REPL, slash commands, signal handling
392
+ agent.ts # tool-call loop, status formatting, repair logic
393
+ api.ts # DeepSeek client, retry/abort, prompt cache rates
394
+ tools.ts # tool schemas + executors (read/write/edit/bash/grep/glob/web_*)
395
+ approval.ts # confirmWrite/Edit/Bash/Fetch
396
+ audit.ts # JSONL audit logger
397
+ search.ts # Brave / Tavily / DDG dispatch
398
+ compact.ts # /compact summarization routine
399
+ history.ts # session save/load/list/migrate-legacy
400
+ repl_history.ts # ~/.local/state/dsc/history reader/writer
401
+ markdown.ts # streaming markdown→ANSI renderer (incl. tables, HR, LaTeX→Unicode)
402
+ ui.ts # Spinner with stall detection; one-line StatusBar
403
+ prompt.ts # SYSTEM_PROMPT + buildSystemPrompt
404
+ ```
405
+
406
+ The `ink-port` branch is a parked experiment — a React-based ink TUI
407
+ shell. `main` deliberately stays a plain REPL so output remains
408
+ selectable.
package/bin/dsc.mjs ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join } from "node:path";
6
+
7
+ // Resolve the package root from this file's location. With `npm link` the
8
+ // global symlink resolves through to the real repo, so this points at the
9
+ // editable source — no rebuild needed.
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+ const pkgRoot = dirname(here);
12
+ const tsxBin = join(pkgRoot, "node_modules", ".bin", "tsx");
13
+ const srcEntry = join(pkgRoot, "src", "index.ts");
14
+ const distEntry = join(pkgRoot, "dist", "index.js");
15
+
16
+ if (existsSync(tsxBin) && existsSync(srcEntry)) {
17
+ // Dev: run TS sources directly. Live changes pick up on next launch.
18
+ const child = spawn(tsxBin, [srcEntry, ...process.argv.slice(2)], {
19
+ stdio: "inherit",
20
+ });
21
+ child.on("exit", (code, signal) => {
22
+ if (signal) process.kill(process.pid, signal);
23
+ else process.exit(code ?? 0);
24
+ });
25
+ } else {
26
+ // Fallback: production-style install without devDeps. Use the compiled
27
+ // output instead. Run `npm run build` to refresh `dist/`.
28
+ await import(distEntry);
29
+ }