@agi-fans/oh-my-dsh 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 D.Yang
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,38 @@
1
+ # @agi-fans/oh-my-dsh
2
+
3
+ The `omdsh` command: a TUI coding agent over the DeepSeek Harness core runtime. Boots the shipped `config/cordis.yml` composition through the harness boot machinery (`dsh-app-boot`), provides the command line and exit request, and leaves process lifetime to the mounted runner.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install --global @agi-fans/oh-my-dsh
9
+ omdsh
10
+ ```
11
+
12
+ Or run it without a global installation:
13
+
14
+ ```sh
15
+ npx @agi-fans/oh-my-dsh
16
+ ```
17
+
18
+ ## Develop
19
+
20
+ ```sh
21
+ pnpm install
22
+ pnpm omdsh "list files"
23
+
24
+ node lib/bin.js "list files"
25
+ ```
26
+
27
+ Model/provider routes come from `OMDSH_MODEL`/`OMDSH_PROVIDER` or the user's `$DSH_HOME/settings.yaml` (mounted via `dsh-settings-file`); credentials resolve through `dsh-credentials-local` (inherited env, managed `.credentials.yaml`, project/user `.env`). The default permission mode is trusted-local (`danger-full-access`); `OMDSH_PERMISSION_MODE` narrows it.
28
+
29
+ Run `/settings` to customize the TUI. The overlay controls themes, colors, tool expansion, and the composer status bar, including telemetry visibility, compact/full labels, and the order of Context, Cache, Tokens, Latency, Time, and Activity groups. Use up/down to navigate and left/right to change a value; preferences are stored in the user settings document.
30
+
31
+ Skills are discovered from project/user `.dsh/skills` and `.agents/skills` roots. MCP servers are loaded from user and project `.dsh/mcp.json` files; see [Skills and MCP](https://github.com/agi-fans/oh-my-dsh/blob/main/docs/skills-and-mcp.md) for the configuration shape and TUI commands.
32
+
33
+ ## Verify
34
+
35
+ ```sh
36
+ pnpm --filter @agi-fans/oh-my-dsh test # keyless pipe-mode e2e
37
+ pnpm smoke # keyless interactive PTY e2e
38
+ ```
@@ -0,0 +1,243 @@
1
+ # omdsh composition — the TUI coding agent over the DeepSeek Harness core
2
+ # runtime. No Host, no HTTP, no browser layer: local executors, the
3
+ # executor-less agent spine, the DeepSeek adapter, and the omdsh TUI seam
4
+ # (provider + interactive runner).
5
+ #
6
+ # Model/provider rows read the CLI/environment overrides through the
7
+ # harness's sanctioned dynamic-config mechanism (!!js under plugin config).
8
+
9
+ - id: timer
10
+ name: '@deepseek-ai/cordis-plugin-timer'
11
+
12
+ - id: llm
13
+ name: '@deepseek-ai/dsh-llm'
14
+
15
+ - id: llm-deepseek
16
+ name: '@deepseek-ai/dsh-llm-deepseek'
17
+
18
+ - id: session
19
+ name: '@deepseek-ai/dsh-session'
20
+
21
+ - id: session-persistence
22
+ name: '@deepseek-ai/dsh-session-persistence-jsonl'
23
+ config:
24
+ root: !!js "(process.env.OMDSH_HOME || process.env.DSH_HOME || process.env.HOME + '/.dsh') + '/sessions'"
25
+
26
+ - id: session-checkpoint
27
+ name: '@deepseek-ai/dsh-session-checkpoint-policy'
28
+
29
+ - id: session-projection
30
+ name: '@deepseek-ai/dsh-session-projection'
31
+
32
+ - id: session-title
33
+ name: '@deepseek-ai/dsh-session-title'
34
+ config:
35
+ fallbackMaxWords: 8
36
+ fallbackMaxBytes: 80
37
+ maxTitleBytes: 120
38
+
39
+ - id: session-stats
40
+ name: '@deepseek-ai/dsh-session-stats'
41
+
42
+ - id: session-query
43
+ name: '@deepseek-ai/dsh-session-query-sqlite'
44
+ config:
45
+ path: !!js "(process.env.OMDSH_HOME || process.env.DSH_HOME || process.env.HOME + '/.dsh') + '/sessions-query.sqlite'"
46
+ openAt: first-search
47
+
48
+ - id: attachments
49
+ name: '@deepseek-ai/dsh-attachment-local'
50
+ config:
51
+ dshHome: !!js process.env.OMDSH_HOME || process.env.DSH_HOME || undefined
52
+
53
+ # User settings document ($DSH_HOME/settings.yaml) and credential sources
54
+ # (inherited env, managed .credentials.yaml, project/user .env) — the same
55
+ # rows the harness base mounts, so omdsh honors the user's existing model
56
+ # route and API key setup out of the box.
57
+ - id: settings
58
+ name: '@deepseek-ai/dsh-settings-file'
59
+
60
+ - id: credentials
61
+ name: '@deepseek-ai/dsh-credentials-local'
62
+
63
+ - id: agent
64
+ name: '@deepseek-ai/dsh-agent'
65
+
66
+ - id: agent-default-model
67
+ name: '@deepseek-ai/dsh-agent-default-model'
68
+ config:
69
+ provider: !!js process.env.OMDSH_PROVIDER || 'deepseek-official'
70
+ model: !!js process.env.OMDSH_MODEL || 'deepseek-v4-flash'
71
+
72
+ - id: commands
73
+ name: '@deepseek-ai/dsh-commands'
74
+
75
+ - id: user-questions
76
+ name: '@deepseek-ai/dsh-user-questions'
77
+
78
+ - id: approval
79
+ name: '@deepseek-ai/dsh-user-approval'
80
+ config:
81
+ policy: !!js "(process.env.OMDSH_PERMISSION_MODE || 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'"
82
+
83
+ # Every session starts with one explicit access preset. The environment is a
84
+ # deployment override; the TUI switches it later through the /mode picker.
85
+ - id: sandbox
86
+ name: '@deepseek-ai/dsh-sandbox-local'
87
+
88
+ - id: sandbox-policy
89
+ name: '@deepseek-ai/dsh-sandbox-policy'
90
+ config:
91
+ mode: !!js process.env.OMDSH_PERMISSION_MODE || 'workspace-write'
92
+ workspaceRoot: !!js process.cwd()
93
+
94
+ - id: fs
95
+ name: '@deepseek-ai/dsh-fs-local'
96
+
97
+ - id: fs-observation-policy
98
+ name: '@deepseek-ai/dsh-fs-observation-policy'
99
+
100
+ - id: subprocess
101
+ name: '@deepseek-ai/dsh-subprocess-local'
102
+
103
+ - id: bash
104
+ name: '@deepseek-ai/dsh-bash-sandbox'
105
+
106
+ - id: permission
107
+ name: '@deepseek-ai/dsh-permission-presets'
108
+ config:
109
+ presets:
110
+ read-only:
111
+ sandbox: read-only
112
+ approval: ask
113
+ name: Read only
114
+ description: Inspect the workspace without writing; escalation requires approval.
115
+ workspace-write:
116
+ sandbox: workspace-write
117
+ approval: ask
118
+ name: Workspace write
119
+ description: Write inside the workspace; wider access requires approval.
120
+ danger-full-access:
121
+ sandbox: danger-full-access
122
+ approval: never
123
+ name: Full access
124
+ description: Full filesystem access without approval prompts.
125
+
126
+ - id: spine
127
+ name: '@deepseek-ai/dsh-agent-spine-demo'
128
+ config:
129
+ workspaceContext:
130
+ maxBytes: 65536
131
+ dshHome: !!js process.env.OMDSH_HOME || undefined
132
+ skills:
133
+ enabled: true
134
+ goals:
135
+ domain: {}
136
+ tool: {}
137
+
138
+ # Human and model workflow contributions. The TUI discovers human commands
139
+ # from dsh-commands and renders unknown tools through its safe fallback.
140
+ - id: command-goal
141
+ name: '@deepseek-ai/dsh-command-goal'
142
+
143
+ - id: tool-ask-user
144
+ name: '@deepseek-ai/dsh-tool-ask-user'
145
+
146
+ - id: plan-mode
147
+ name: '@deepseek-ai/dsh-plan-mode'
148
+ config:
149
+ section: |
150
+ You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
151
+
152
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
153
+
154
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
155
+
156
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
157
+
158
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public interface, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
159
+
160
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
161
+
162
+ - id: tool-fs
163
+ name: '@deepseek-ai/dsh-tool-fs'
164
+
165
+ - id: tool-fs-search
166
+ name: '@deepseek-ai/dsh-tool-fs-search'
167
+ config:
168
+ sampleOverCapGlobResults: false
169
+
170
+ - id: tool-todo
171
+ name: '@deepseek-ai/dsh-tool-todo'
172
+ config:
173
+ allowParallelInProgress: true
174
+
175
+ - id: tool-str-replace-editor
176
+ name: '@deepseek-ai/dsh-tool-str-replace-editor'
177
+ config:
178
+ maxOutputChars: 16000
179
+
180
+ - id: token-meter
181
+ name: '@deepseek-ai/dsh-token-meter'
182
+
183
+ - id: compaction
184
+ name: '@deepseek-ai/dsh-compaction-basic'
185
+
186
+ - id: command-compact
187
+ name: '@deepseek-ai/dsh-command-compact'
188
+
189
+ - id: subagent
190
+ name: '@deepseek-ai/dsh-subagent'
191
+
192
+ - id: subagent-spawn
193
+ name: '@deepseek-ai/dsh-subagent-spawn-in-process'
194
+ config:
195
+ providerName: spawn
196
+
197
+ - id: subagent-control
198
+ name: '@deepseek-ai/dsh-tool-subagent-control'
199
+
200
+ - id: subagent-list
201
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
202
+
203
+ - id: tool-subagent
204
+ name: '@deepseek-ai/dsh-tool-subagent'
205
+ config:
206
+ provider: spawn
207
+ toolName: subagent
208
+ backgroundMode: continuable
209
+
210
+ - id: tui
211
+ name: '@agi-fans/dsh-tui'
212
+ config:
213
+ model: !!js process.env.OMDSH_MODEL || 'deepseek-v4-flash'
214
+
215
+ - id: tui-tool-presentation
216
+ name: '@agi-fans/dsh-tui/tool-presentation'
217
+
218
+ - id: tui-session-runtime
219
+ name: '@agi-fans/dsh-tui/session-runtime'
220
+
221
+ - id: tui-human-interaction
222
+ name: '@agi-fans/dsh-tui/human-interaction'
223
+
224
+ - id: tui-command-session
225
+ name: '@agi-fans/dsh-tui/command-session'
226
+
227
+ - id: tui-command-queue
228
+ name: '@agi-fans/dsh-tui/command-queue'
229
+
230
+ - id: tui-command-model
231
+ name: '@agi-fans/dsh-tui/command-model'
232
+
233
+ - id: tui-command-mode
234
+ name: '@agi-fans/dsh-tui/command-mode'
235
+
236
+ - id: tui-command-transcript
237
+ name: '@agi-fans/dsh-tui/command-transcript'
238
+
239
+ - id: tui-command-integrations
240
+ name: '@agi-fans/dsh-tui/command-integrations'
241
+
242
+ - id: runner
243
+ name: '@agi-fans/dsh-tui/runner'
package/lib/args.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * omdsh argument parsing: one optional prompt (positional words joined)
3
+ * plus --model/--provider overrides, durable --resume, and help/version.
4
+ * @module @agi-fans/oh-my-dsh
5
+ */
6
+ export interface OmdshInvocation {
7
+ /** Positional prompt words, joined by spaces (empty when interactive only). */
8
+ prompt: string[];
9
+ /** --model override (process.env.OMDSH_MODEL). */
10
+ model: string | undefined;
11
+ /** --provider override (process.env.OMDSH_PROVIDER). */
12
+ provider: string | undefined;
13
+ /** Durable session selected by --resume/-r. */
14
+ resume: string | undefined;
15
+ /** --help requested. */
16
+ help: boolean;
17
+ }
18
+ /**
19
+ * Parse the omdsh command line.
20
+ * @param argv - arguments after the bin name.
21
+ * @param version - version string for --version output.
22
+ * @returns the invocation, or prints help/version and exits.
23
+ */
24
+ export declare function parseOmdshArgs(argv: readonly string[], version: string): OmdshInvocation;
package/lib/args.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * omdsh argument parsing: one optional prompt (positional words joined)
3
+ * plus --model/--provider overrides, durable --resume, and help/version.
4
+ * @module @agi-fans/oh-my-dsh
5
+ */
6
+ const HELP = `
7
+ omdsh — a TUI coding agent on the DeepSeek Harness runtime
8
+
9
+ Usage:
10
+ omdsh [options] [prompt...]
11
+
12
+ Options:
13
+ --model <name> model route (default deepseek-v4-flash)
14
+ --provider <name> provider route (default deepseek-official)
15
+ -r, --resume <id> resume a durable session
16
+ -h, --help show this help
17
+ --version show the version
18
+
19
+ Environment:
20
+ DEEPSEEK_API_KEY DeepSeek API key (required for live turns)
21
+ `;
22
+ /**
23
+ * Parse the omdsh command line.
24
+ * @param argv - arguments after the bin name.
25
+ * @param version - version string for --version output.
26
+ * @returns the invocation, or prints help/version and exits.
27
+ */
28
+ export function parseOmdshArgs(argv, version) {
29
+ const prompt = [];
30
+ let model;
31
+ let provider;
32
+ let resume;
33
+ let help = false;
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const arg = argv[i] ?? '';
36
+ if (arg === '-h' || arg === '--help') {
37
+ help = true;
38
+ }
39
+ else if (arg === '--version') {
40
+ console.log(version);
41
+ process.exit(0);
42
+ }
43
+ else if (arg === '--model') {
44
+ model = argv[i + 1];
45
+ i += 1;
46
+ if (model === undefined)
47
+ usageError('--model requires a value');
48
+ }
49
+ else if (arg === '--provider') {
50
+ provider = argv[i + 1];
51
+ i += 1;
52
+ if (provider === undefined)
53
+ usageError('--provider requires a value');
54
+ }
55
+ else if (arg === '--resume' || arg === '-r') {
56
+ resume = argv[i + 1];
57
+ i += 1;
58
+ if (resume === undefined || resume.startsWith('-'))
59
+ usageError(`${arg} requires a session id`);
60
+ }
61
+ else if (arg.startsWith('-')) {
62
+ usageError('unknown option ' + arg);
63
+ }
64
+ else {
65
+ prompt.push(arg);
66
+ }
67
+ }
68
+ if (help) {
69
+ console.log(HELP.trim());
70
+ process.exit(0);
71
+ }
72
+ if (resume !== undefined && prompt.length > 0)
73
+ usageError('--resume cannot be combined with a prompt');
74
+ return { prompt, model, provider, resume, help };
75
+ }
76
+ function usageError(message) {
77
+ console.error('omdsh: ' + message);
78
+ console.error('run: omdsh --help');
79
+ process.exit(2);
80
+ }
81
+ //# sourceMappingURL=args.js.map
package/lib/bin.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * omdsh — command-line entry. Parses flags, applies model/provider
4
+ * overrides to the environment the cordis.yml composition reads, and
5
+ * boots the tree; the mounted runner owns process lifetime after that.
6
+ * @module @agi-fans/oh-my-dsh
7
+ */
8
+ export {};
package/lib/bin.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * omdsh — command-line entry. Parses flags, applies model/provider
4
+ * overrides to the environment the cordis.yml composition reads, and
5
+ * boots the tree; the mounted runner owns process lifetime after that.
6
+ * @module @agi-fans/oh-my-dsh
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { parseOmdshArgs } from "./args.js";
11
+ /** This app's version, read from its checked-in package.json. */
12
+ function readVersion() {
13
+ const manifest = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8'));
14
+ return typeof manifest.version === 'string' ? manifest.version : '0.0.0';
15
+ }
16
+ const invocation = parseOmdshArgs(process.argv.slice(2), readVersion());
17
+ // CLI flags outrank every layered environment source; the cordis.yml
18
+ // rows read these overrides at entry-mount time.
19
+ if (invocation.model !== undefined)
20
+ process.env.OMDSH_MODEL = invocation.model;
21
+ if (invocation.provider !== undefined)
22
+ process.env.OMDSH_PROVIDER = invocation.provider;
23
+ const { runOmdsh } = await import("./boot.js");
24
+ await runOmdsh(invocation.prompt, invocation.resume);
25
+ //# sourceMappingURL=bin.js.map
package/lib/boot.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * omdsh tree boot: mounts the shipped cordis.yml composition through the
3
+ * harness boot machinery, providing the command line, the exit request,
4
+ * and the launch-environment snapshot before any entry mounts.
5
+ * @module @agi-fans/oh-my-dsh
6
+ */
7
+ import type { Context } from '@deepseek-ai/cordis';
8
+ import { type ProcessShutdown } from './process-shutdown.ts';
9
+ export declare const NAME = "omdsh";
10
+ /** Absolute path of the shipped composition (source and built layouts both sit one directory under apps/omdsh). */
11
+ export declare const CONFIG_PATH: string;
12
+ /**
13
+ * Boot the omdsh tree and leave process lifetime to the mounted runner.
14
+ * @param prompt - positional prompt words (empty for interactive only).
15
+ * @param resume - durable session id selected by the launcher.
16
+ * @returns the settled root context and the shutdown controller.
17
+ */
18
+ export declare function runOmdsh(prompt: readonly string[], resume?: string): Promise<{
19
+ ctx: Context;
20
+ shutdown: ProcessShutdown;
21
+ }>;
package/lib/boot.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * omdsh tree boot: mounts the shipped cordis.yml composition through the
3
+ * harness boot machinery, providing the command line, the exit request,
4
+ * and the launch-environment snapshot before any entry mounts.
5
+ * @module @agi-fans/oh-my-dsh
6
+ */
7
+ import { fileURLToPath } from 'node:url';
8
+ import { boot, installFailLoud, loadLayeredEnv } from '@deepseek-ai/dsh-app-boot';
9
+ import { provideCmdline } from '@deepseek-ai/dsh-cmdline';
10
+ import { DSH_LAUNCH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh-launch-environment';
11
+ import { createProcessShutdown } from "./process-shutdown.js";
12
+ import { loadMcpPatches } from "./mcp-config.js";
13
+ export const NAME = 'omdsh';
14
+ /** Absolute path of the shipped composition (source and built layouts both sit one directory under apps/omdsh). */
15
+ export const CONFIG_PATH = fileURLToPath(new URL('../config/cordis.yml', import.meta.url));
16
+ /**
17
+ * Boot the omdsh tree and leave process lifetime to the mounted runner.
18
+ * @param prompt - positional prompt words (empty for interactive only).
19
+ * @param resume - durable session id selected by the launcher.
20
+ * @returns the settled root context and the shutdown controller.
21
+ */
22
+ export async function runOmdsh(prompt, resume) {
23
+ const app = {};
24
+ const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose(); });
25
+ const signalShutdown = new AbortController();
26
+ const interrupt = (code) => {
27
+ signalShutdown.abort();
28
+ shutdown.interrupt(code);
29
+ };
30
+ // SIGINT only fires outside raw mode (a raw tty delivers Ctrl-C as a
31
+ // keypress the tui provider handles); SIGTERM is the supervisor's stop.
32
+ process.on('SIGTERM', () => { interrupt(0); });
33
+ process.on('SIGINT', () => { interrupt(130); });
34
+ installFailLoud(NAME, process, async () => { await app.current?.fiber.dispose(); });
35
+ const environment = loadLayeredEnv(NAME);
36
+ const ctx = await boot(NAME, CONFIG_PATH, loadMcpPatches(), (hostCtx) => {
37
+ app.current = hostCtx;
38
+ hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, environment);
39
+ provideCmdline(hostCtx, {
40
+ args: resume === undefined ? prompt : ['--resume', resume],
41
+ exit: (code) => { void shutdown.shutdown(code); },
42
+ });
43
+ });
44
+ app.current = ctx;
45
+ return { ctx, shutdown };
46
+ }
47
+ //# sourceMappingURL=boot.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Native omdsh MCP configuration adapter.
3
+ *
4
+ * This module owns only deployment discovery: it translates the familiar
5
+ * `mcpServers` JSON shape into one Harness `dsh-mcp-client` plugin row per
6
+ * server. Protocol, connection lifecycle, tool registration, and reconnects
7
+ * remain owned by the Harness MCP client.
8
+ * @module @agi-fans/oh-my-dsh
9
+ */
10
+ export interface McpPluginRow {
11
+ id: string;
12
+ name: '@deepseek-ai/dsh-mcp-client';
13
+ config: Record<string, unknown>;
14
+ }
15
+ export interface McpInsertPatch {
16
+ insert: McpPluginRow[];
17
+ }
18
+ /** Resolve the native user-level MCP document. */
19
+ export declare function omdshHome(environment?: NodeJS.ProcessEnv): string;
20
+ /**
21
+ * Load user and project MCP definitions and turn them into Loader insert
22
+ * patches. Project definitions override same-named user definitions.
23
+ */
24
+ export declare function loadMcpPatches(cwd?: string, environment?: NodeJS.ProcessEnv): McpInsertPatch[];
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Native omdsh MCP configuration adapter.
3
+ *
4
+ * This module owns only deployment discovery: it translates the familiar
5
+ * `mcpServers` JSON shape into one Harness `dsh-mcp-client` plugin row per
6
+ * server. Protocol, connection lifecycle, tool registration, and reconnects
7
+ * remain owned by the Harness MCP client.
8
+ * @module @agi-fans/oh-my-dsh
9
+ */
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
13
+ function configError(path, detail) {
14
+ return new Error(`omdsh: invalid MCP config ${path}: ${detail}`);
15
+ }
16
+ function object(value) {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
18
+ }
19
+ function readDocument(path) {
20
+ if (!existsSync(path))
21
+ return undefined;
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
25
+ }
26
+ catch (error) {
27
+ throw configError(path, error instanceof Error ? error.message : String(error));
28
+ }
29
+ if (!object(parsed) || !object(parsed.mcpServers)) {
30
+ throw configError(path, 'expected an object with an "mcpServers" object');
31
+ }
32
+ return { mcpServers: parsed.mcpServers };
33
+ }
34
+ function stringArray(value, path, field) {
35
+ if (value === undefined)
36
+ return undefined;
37
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
38
+ throw configError(path, `"${field}" must be an array of strings`);
39
+ }
40
+ return value;
41
+ }
42
+ function stringRecord(value, path, field) {
43
+ if (value === undefined)
44
+ return undefined;
45
+ if (!object(value) || Object.values(value).some(item => typeof item !== 'string')) {
46
+ throw configError(path, `"${field}" must be an object of string values`);
47
+ }
48
+ return value;
49
+ }
50
+ function optionalBoolean(value, path, field) {
51
+ if (value === undefined)
52
+ return undefined;
53
+ if (typeof value !== 'boolean')
54
+ throw configError(path, `"${field}" must be a boolean`);
55
+ return value;
56
+ }
57
+ function optionalNumber(value, path, field) {
58
+ if (value === undefined)
59
+ return undefined;
60
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
61
+ throw configError(path, `"${field}" must be a positive number`);
62
+ }
63
+ return value;
64
+ }
65
+ function commonConfig(server, path) {
66
+ const timeout = optionalNumber(server.toolCallTimeoutMs ?? server.timeout, path, 'toolCallTimeoutMs');
67
+ const fail = optionalBoolean(server.failOnStartupError, path, 'failOnStartupError');
68
+ if (server.reconnect !== undefined && !object(server.reconnect)) {
69
+ throw configError(path, '"reconnect" must be an object');
70
+ }
71
+ return {
72
+ ...(timeout === undefined ? {} : { toolCallTimeoutMs: timeout }),
73
+ ...(fail === undefined ? {} : { failOnStartupError: fail }),
74
+ ...(server.reconnect === undefined ? {} : { reconnect: server.reconnect }),
75
+ };
76
+ }
77
+ function expandEnvironment(value, environment) {
78
+ if (typeof value === 'string') {
79
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/gu, (token, name, fallback) => environment[name] ?? fallback ?? token);
80
+ }
81
+ if (Array.isArray(value))
82
+ return value.map(item => expandEnvironment(item, environment));
83
+ if (!object(value))
84
+ return value;
85
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandEnvironment(item, environment)]));
86
+ }
87
+ function serverConfig(name, raw, path, environment) {
88
+ if (!/^[A-Za-z0-9_-]{1,32}$/u.test(name)) {
89
+ throw configError(path, `server name "${name}" must match [A-Za-z0-9_-]{1,32}`);
90
+ }
91
+ const value = expandEnvironment(raw, environment);
92
+ if (!object(value))
93
+ throw configError(path, `server "${name}" must be an object`);
94
+ const enabled = optionalBoolean(value.enabled, path, `${name}.enabled`);
95
+ if (enabled === false)
96
+ return undefined;
97
+ const command = value.command;
98
+ const url = value.url;
99
+ if (typeof command === 'string' && command !== '') {
100
+ if (url !== undefined)
101
+ throw configError(path, `server "${name}" cannot set both "command" and "url"`);
102
+ const args = stringArray(value.args, path, `${name}.args`);
103
+ const env = stringRecord(value.env, path, `${name}.env`);
104
+ if (value.cwd !== undefined && typeof value.cwd !== 'string') {
105
+ throw configError(path, `"${name}.cwd" must be a string`);
106
+ }
107
+ return {
108
+ serverName: name,
109
+ transport: 'stdio',
110
+ command,
111
+ ...(args === undefined ? {} : { args }),
112
+ ...(env === undefined ? {} : { env }),
113
+ ...(value.cwd === undefined ? {} : { cwd: value.cwd }),
114
+ ...commonConfig(value, path),
115
+ };
116
+ }
117
+ if (typeof url === 'string' && url !== '') {
118
+ const headers = stringRecord(value.headers, path, `${name}.headers`);
119
+ return {
120
+ serverName: name,
121
+ transport: 'streamable-http',
122
+ url,
123
+ ...(headers === undefined ? {} : { headers }),
124
+ ...commonConfig(value, path),
125
+ };
126
+ }
127
+ throw configError(path, `server "${name}" requires either a non-empty "command" or "url"`);
128
+ }
129
+ function safeId(name) {
130
+ return 'mcp-' + name;
131
+ }
132
+ /** Resolve the native user-level MCP document. */
133
+ export function omdshHome(environment = process.env) {
134
+ const configured = environment.OMDSH_HOME?.trim() || environment.DSH_HOME?.trim();
135
+ return configured === undefined
136
+ ? join(homedir(), '.dsh')
137
+ : (isAbsolute(configured) ? configured : resolve(configured));
138
+ }
139
+ function projectRoot(cwd) {
140
+ const fallback = resolve(cwd);
141
+ let current = fallback;
142
+ for (;;) {
143
+ if (existsSync(join(current, '.git')))
144
+ return current;
145
+ const parent = dirname(current);
146
+ if (parent === current)
147
+ return fallback;
148
+ current = parent;
149
+ }
150
+ }
151
+ /**
152
+ * Load user and project MCP definitions and turn them into Loader insert
153
+ * patches. Project definitions override same-named user definitions.
154
+ */
155
+ export function loadMcpPatches(cwd = process.cwd(), environment = process.env) {
156
+ const files = [
157
+ join(omdshHome(environment), 'mcp.json'),
158
+ join(projectRoot(cwd), '.dsh', 'mcp.json'),
159
+ ];
160
+ const merged = new Map();
161
+ for (const path of files) {
162
+ const document = readDocument(path);
163
+ if (document === undefined)
164
+ continue;
165
+ for (const [name, value] of Object.entries(document.mcpServers))
166
+ merged.set(name, { value, path });
167
+ }
168
+ const rows = [];
169
+ for (const [name, source] of merged) {
170
+ const config = serverConfig(name, source.value, source.path, environment);
171
+ if (config === undefined)
172
+ continue;
173
+ rows.push({ id: safeId(name), name: '@deepseek-ai/dsh-mcp-client', config });
174
+ }
175
+ return rows.length === 0 ? [] : [{ insert: rows }];
176
+ }
177
+ //# sourceMappingURL=mcp-config.js.map
@@ -0,0 +1,16 @@
1
+ /** Bounded, escalating process shutdown (mirrors the dsh CLI's own controller). */
2
+ /** Maximum grace allowed for the application tree to dispose before process exit. */
3
+ export declare const PROCESS_SHUTDOWN_TIMEOUT_MS = 5000;
4
+ /** Process-exit controller shared by normal completion and Unix signal handlers. */
5
+ export interface ProcessShutdown {
6
+ /** Start or join graceful disposal before allowing natural completion with `code`. */
7
+ shutdown(code: number): Promise<void>;
8
+ /** Start graceful disposal followed by exit, or force exit when shutdown is already running. */
9
+ interrupt(code: number): void;
10
+ }
11
+ /**
12
+ * Create one process-exit controller around an application disposer.
13
+ * @param dispose - whole-application teardown that resolves at quiescence.
14
+ * @returns a controller whose normal calls coalesce and whose repeated signal call escalates.
15
+ */
16
+ export declare function createProcessShutdown(dispose: () => Promise<void>): ProcessShutdown;
@@ -0,0 +1,57 @@
1
+ /** Bounded, escalating process shutdown (mirrors the dsh CLI's own controller). */
2
+ /** Maximum grace allowed for the application tree to dispose before process exit. */
3
+ export const PROCESS_SHUTDOWN_TIMEOUT_MS = 5_000;
4
+ /**
5
+ * Create one process-exit controller around an application disposer.
6
+ * @param dispose - whole-application teardown that resolves at quiescence.
7
+ * @returns a controller whose normal calls coalesce and whose repeated signal call escalates.
8
+ */
9
+ export function createProcessShutdown(dispose) {
10
+ let pending;
11
+ let timeout;
12
+ let completed = false;
13
+ let forceExited = false;
14
+ const forceExitOnce = (code) => {
15
+ if (forceExited)
16
+ return;
17
+ forceExited = true;
18
+ if (timeout !== undefined)
19
+ clearTimeout(timeout);
20
+ process.exit(code);
21
+ };
22
+ const completeOnce = (code) => {
23
+ if (completed || forceExited)
24
+ return;
25
+ completed = true;
26
+ if (timeout !== undefined)
27
+ clearTimeout(timeout);
28
+ process.exitCode = code;
29
+ };
30
+ const start = (code, forceAfterDispose) => {
31
+ if (pending !== undefined)
32
+ return pending;
33
+ timeout = setTimeout(() => {
34
+ forceExitOnce(code);
35
+ }, PROCESS_SHUTDOWN_TIMEOUT_MS);
36
+ pending = Promise.resolve().then(dispose).then(() => {
37
+ if (forceAfterDispose)
38
+ forceExitOnce(code);
39
+ else
40
+ completeOnce(code);
41
+ }, () => { forceExitOnce(code); });
42
+ return pending;
43
+ };
44
+ return {
45
+ shutdown(code) {
46
+ return start(code, false);
47
+ },
48
+ interrupt(code) {
49
+ if (pending !== undefined) {
50
+ forceExitOnce(code);
51
+ return;
52
+ }
53
+ void start(code, true);
54
+ },
55
+ };
56
+ }
57
+ //# sourceMappingURL=process-shutdown.js.map
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@agi-fans/oh-my-dsh",
3
+ "description": "omdsh: TUI coding agent command over the DeepSeek Harness runtime",
4
+ "version": "0.1.0",
5
+ "homepage": "https://github.com/agi-fans/oh-my-dsh#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/agi-fans/oh-my-dsh.git",
9
+ "directory": "apps/omdsh"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/agi-fans/oh-my-dsh/issues"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "omdsh": "lib/bin.js"
17
+ },
18
+ "files": [
19
+ "lib/*.js",
20
+ "lib/*.d.ts",
21
+ "config"
22
+ ],
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@deepseek-ai/cordis-plugin-loader": "1.0.2",
29
+ "@deepseek-ai/cordis-plugin-timer": "1.1.3",
30
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
31
+ "@deepseek-ai/dsh-agent-default-model": "0.1.0-rc.6",
32
+ "@deepseek-ai/dsh-agent-spine-demo": "0.1.0-rc.6",
33
+ "@deepseek-ai/dsh-attachment-local": "0.1.0-rc.6",
34
+ "@deepseek-ai/dsh-command-compact": "0.1.0-rc.6",
35
+ "@deepseek-ai/dsh-command-goal": "0.1.0-rc.6",
36
+ "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
37
+ "@deepseek-ai/dsh-compaction-basic": "0.1.0-rc.6",
38
+ "@deepseek-ai/dsh-app-boot": "0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-bash-sandbox": "0.1.0-rc.6",
40
+ "@deepseek-ai/dsh-cmdline": "0.1.0-rc.6",
41
+ "@deepseek-ai/dsh-fs-local": "0.1.0-rc.6",
42
+ "@deepseek-ai/dsh-fs-observation-policy": "0.1.0-rc.6",
43
+ "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6",
44
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
45
+ "@deepseek-ai/dsh-llm-deepseek": "0.1.0-rc.6",
46
+ "@deepseek-ai/dsh-mcp-client": "0.1.0-rc.6",
47
+ "@deepseek-ai/dsh-permission-presets": "0.1.0-rc.6",
48
+ "@deepseek-ai/dsh-plan-mode": "0.1.0-rc.6",
49
+ "@deepseek-ai/dsh-sandbox-local": "0.1.0-rc.6",
50
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.0-rc.6",
51
+ "@deepseek-ai/dsh-session": "0.1.0-rc.6",
52
+ "@deepseek-ai/dsh-session-checkpoint-policy": "0.1.0-rc.6",
53
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.1.0-rc.6",
54
+ "@deepseek-ai/dsh-session-projection": "0.1.0-rc.6",
55
+ "@deepseek-ai/dsh-session-query-sqlite": "0.1.0-rc.6",
56
+ "@deepseek-ai/dsh-session-stats": "0.1.0-rc.6",
57
+ "@deepseek-ai/dsh-session-title": "0.1.0-rc.6",
58
+ "@deepseek-ai/dsh-subagent": "0.1.0-rc.6",
59
+ "@deepseek-ai/dsh-subagent-spawn-in-process": "0.1.0-rc.6",
60
+ "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6",
61
+ "@deepseek-ai/dsh-tool-ask-user": "0.1.0-rc.6",
62
+ "@deepseek-ai/dsh-tool-fs": "0.1.0-rc.6",
63
+ "@deepseek-ai/dsh-tool-fs-search": "0.1.0-rc.6",
64
+ "@deepseek-ai/dsh-tool-str-replace-editor": "0.1.0-rc.6",
65
+ "@deepseek-ai/dsh-tool-subagent": "0.1.0-rc.6",
66
+ "@deepseek-ai/dsh-tool-subagent-control": "0.1.0-rc.6",
67
+ "@deepseek-ai/dsh-tool-todo": "0.1.0-rc.6",
68
+ "@deepseek-ai/dsh-user-approval": "0.1.0-rc.6",
69
+ "@deepseek-ai/dsh-user-questions": "0.1.0-rc.6",
70
+ "@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.6",
71
+ "@deepseek-ai/dsh-settings-file": "0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-credentials-local": "0.1.0-rc.6",
73
+ "@agi-fans/dsh-tui": "^0.1.0"
74
+ },
75
+ "peerDependencies": {
76
+ "@deepseek-ai/cordis": "^4.0.1"
77
+ },
78
+ "devDependencies": {
79
+ "@deepseek-ai/cordis": "4.0.1",
80
+ "typescript": "^5.6.0",
81
+ "tsx": "^4.19.0",
82
+ "vitest": "^2.1.0",
83
+ "@types/node": "^22.10.0"
84
+ },
85
+ "scripts": {
86
+ "clean": "node -e \"const fs=require('node:fs');fs.rmSync('lib',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true})\"",
87
+ "build": "tsc -b tsconfig.json",
88
+ "test": "vitest run src",
89
+ "typecheck": "tsc -b tsconfig.json",
90
+ "dev": "tsx src/bin.ts",
91
+ "omdsh": "tsx src/bin.ts"
92
+ }
93
+ }