fix(cli): restore shipped surface capabilities

This commit is contained in:
Turtle
2026-07-29 17:34:50 +08:00
parent bee0a77aad
commit 857a4941be
13 changed files with 133 additions and 54 deletions

View File

@@ -8,8 +8,8 @@
# A patch replaces the targeted row's whole `config` rather than merging into
# it, so a row whose value differs per surface does NOT live here: it belongs to
# each overlay, keeping any single row down to one overlay layer plus the user's.
# That is why `agent-loop`, `system-prompt`, `tools`, `fs-local`, and
# `llm-deepseek` thinking defaults are absent below.
# Rows with surface-specific values appear below only with shared plugin identity
# and neutral defaults; each overlay restates the complete surface configuration.
#
# Row order carries no load semantics (activation is service-availability
# driven); the grouping is for readers.
@@ -51,9 +51,20 @@
- id: llm-retry
name: '@deepseek-ai/dsh-llm-retry'
- id: llm-pi-ai
name: '@deepseek-ai/dsh-llm-pi-ai'
config:
providers:
- provider: openai
apiKey: !!js process.env.OPENAI_API_KEY
baseURL: !!js process.env.OPENAI_BASE_URL
- provider: anthropic
apiKey: !!js process.env.ANTHROPIC_API_KEY
baseURL: !!js process.env.ANTHROPIC_BASE_URL
# The session store root is the launcher's policy, not a plugin's: `dsh` shares
# one store under the Harness home across every cwd, so `/resume` and
# `/resume` spans workspaces. Without a launcher the project-local fallback keeps
# one store under the Harness home across every cwd, so `/resume` spans
# workspaces. Without a launcher the project-local fallback keeps
# an embedder's sessions beside its project.
- id: session-persistence-jsonl
name: '@deepseek-ai/dsh-session-persistence-jsonl'

View File

@@ -7,7 +7,7 @@ The TUI agent combines the real DeepSeek adapter, coding tools, compaction, suba
```mermaid
flowchart LR
cfg["apps/cli (dsh)<br/>cordis.yml"]
cfg["apps/cli (dsh)<br/>base.cordis.yml + tui.cordis.yml"]
plugin_tui_timer["timer<br/>@cordisjs/plugin-timer"]
cfg --> plugin_tui_timer
plugin_tui_llm["llm<br/>@deepseek-ai/dsh-llm"]
@@ -26,6 +26,8 @@ flowchart LR
cfg --> plugin_tui_tasks
plugin_tui_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
cfg --> plugin_tui_llm_retry
plugin_tui_llm_pi_ai["llm-pi-ai<br/>@deepseek-ai/dsh-llm-pi-ai"]
cfg --> plugin_tui_llm_pi_ai
plugin_tui_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
cfg --> plugin_tui_session_persistence_jsonl
plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
@@ -94,6 +96,8 @@ flowchart LR
cfg --> plugin_tui_fs_local
plugin_tui_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
cfg --> plugin_tui_llm_deepseek
plugin_tui_tools["tools<br/>@deepseek-ai/dsh-tool-ask-user"]
cfg --> plugin_tui_tools
```
| Plugin id | Package / module |
@@ -107,6 +111,7 @@ flowchart LR
| `agent` | `@deepseek-ai/dsh-agent` |
| `tasks` | `@deepseek-ai/dsh-tasks-local` |
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
| `bash-local` | `@deepseek-ai/dsh-bash-local` |
@@ -141,7 +146,8 @@ flowchart LR
| `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
| `fs-local` | `@deepseek-ai/dsh-fs-local` |
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
| `tools` | `@deepseek-ai/dsh-tool-ask-user` |
Source config: [`apps/cli/base.cordis.yml`](base.cordis.yml).
Source configs: [`apps/cli/base.cordis.yml`](base.cordis.yml), [`apps/cli/tui.cordis.yml`](tui.cordis.yml).
Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

View File

@@ -22,6 +22,7 @@
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-app-boot": "workspace:^",
"@deepseek-ai/dsh-bash-local": "workspace:^",
"@deepseek-ai/dsh-bash-sandbox": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-hmr": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -33,6 +34,7 @@
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
"@deepseek-ai/dsh-client-ui-model": "workspace:^",
"@deepseek-ai/dsh-client-ui-models": "workspace:^",
"@deepseek-ai/dsh-client-ui-permission": "workspace:^",
"@deepseek-ai/dsh-client-ui-plan": "workspace:^",
"@deepseek-ai/dsh-client-ui-question": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
@@ -52,10 +54,11 @@
"@deepseek-ai/dsh-frontend": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-fs-policy": "workspace:^",
"@deepseek-ai/dsh-fs-sandbox": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-goal-session": "workspace:^",
"@deepseek-ai/dsh-helper": "workspace:^",
"@deepseek-ai/dsh-host-apiproxy": "workspace:^",
"@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
@@ -63,7 +66,10 @@
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
"@deepseek-ai/dsh-llm-retry": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-permission": "workspace:^",
"@deepseek-ai/dsh-plan-mode": "workspace:^",
"@deepseek-ai/dsh-sandbox-local": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
@@ -102,6 +108,7 @@
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-tui": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-user-interaction": "workspace:^",
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
"@deepseek-ai/dsh-workspace": "workspace:^",

View File

@@ -213,27 +213,12 @@ Examples:
resolved = resolveWeb(options)
})
// `--resume` is NOT redeclared here: an option a subcommand shares with its
// parent parses into `program.opts()` and leaves the subcommand's own options
// empty, so redeclaring it would silently drop the id. Commander therefore
// omits it from this subcommand's option list, hence the trailing help text.
program
.command('meta')
.description('work on the dsh source that runs this command, from any directory')
.addHelpText('after', '\nAccepts --resume <id> to resume a persisted session from this checkout.\n')
.action(() => {
// Commander parses the parent (default-surface) options on either side of
// the subcommand into `program.opts()`. `meta` accepts only `--resume`, so
// a leaked `--config`/`-p` is a mistyped invocation that must fail loud
// rather than silently be dropped.
const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
if (parent.config !== undefined || parent.configReplace !== undefined || parent.prompt !== undefined) {
program.error('error: meta takes none of --config, --config-replace, or -p/--prompt')
}
// Same reason as the default surface: an empty id would start a fresh
// session downstream instead of failing the mistyped resume.
if (parent.resume === '') program.error('error: --resume needs a session id')
resolved = { mode: 'meta', ...parent.resume !== undefined && { resume: parent.resume } }
rejectParentOptions('meta')
resolved = { mode: 'meta' }
})
try {

View File

@@ -18,7 +18,7 @@
*/
import { randomUUID } from 'node:crypto'
import { join } from 'node:path'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import {
addHarnessSourceSection,
@@ -155,6 +155,8 @@ export async function runTui(
// selected session may belong to another workspace, so the handoff also enters
// that directory. The host is offered only when Node exposes `process.execve`
// and knows its own entry.
const resolvedConfig = config === undefined ? undefined : resolve(config)
const resolvedConfigReplace = configReplace === undefined ? undefined : resolve(configReplace)
const entry = process.argv[1]
const execve = process.execve?.bind(process)
const app: { current?: Context } = {}
@@ -172,8 +174,8 @@ export async function runTui(
`--resume=${sessionId}`,
// Both config flags must survive the handoff: resuming into a different
// tree than the session was created in would silently change the agent.
...config !== undefined ? ['--config', config] : [],
...configReplace !== undefined ? ['--config-replace', configReplace] : [],
...resolvedConfig !== undefined ? ['--config', resolvedConfig] : [],
...resolvedConfigReplace !== undefined ? ['--config-replace', resolvedConfigReplace] : [],
]
// Mint the fresh id here rather than in the app bundle: the exit line names
// the session to resume, so the launcher must know it before the tree boots.
@@ -223,11 +225,11 @@ export async function runTui(
...loadOverlayPatches(NAME, TUI_OVERLAY),
...config === undefined
? loadPersonalPatches(NAME) ?? []
: loadOverlayPatches(NAME, resolveConfigPath(config, undefined)),
: loadOverlayPatches(NAME, resolveConfigPath(resolve(config), undefined)),
]
const ctx = await boot(
NAME,
replaceTree ? resolveConfigPath(configReplace, undefined) : BASE_CONFIG,
replaceTree ? resolveConfigPath(resolve(configReplace), undefined) : BASE_CONFIG,
patches,
(hostCtx) => {
// The launcher owns session identity and the exit line: a config-mounted

View File

@@ -30,12 +30,7 @@ describe('parseDshArgs', () => {
expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
// `meta` accepts `--resume` but does not redeclare it: a shared option parses
// into program.opts() on either side of the subcommand, and redeclaring it
// would leave the subcommand's own options empty and drop the id.
expect(parse(['meta'])).toEqual({ mode: 'meta' })
expect(parse(['meta', '--resume', 'sess'])).toEqual({ mode: 'meta', resume: 'sess' })
expect(parse(['--resume', 'sess', 'meta'])).toEqual({ mode: 'meta', resume: 'sess' })
// Credential setup is option-free: it writes the Harness-home .env, so
// there is nothing for a flag to select.
// Bare `web` carries no host/port: the shipped cordis.yml owns the default.
@@ -72,9 +67,9 @@ describe('parseDshArgs', () => {
expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
// Same rule for credential setup: it shares no option with the default
// surface, so a leaked flag is a typo, not something to ignore.
// `meta` fixes its own config tree and is interactive, so --config/-p are
// rejected; an empty id is swallowed downstream exactly as above.
expect(exitCode(['meta', '--resume='])).toBe(1)
// `meta` fixes its own config tree and always starts fresh, so every
// default-surface option is rejected.
expect(exitCode(['meta', '--resume', 's'])).toBe(1)
expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
expect(exitCode(['meta', '-p', 'task'])).toBe(1)

View File

@@ -94,9 +94,8 @@
- id: session-reference
name: '@deepseek-ai/dsh-session-reference'
# Refuses write/edit inside the dsh checkout this launcher runs from, on that
# checkout's own branch, until the session loads dsh-customize. Inert
# everywhere else, so an ordinary project sees no change.
# Compacts oversized tool results before the broader conversation compactor
# runs, preserving the model-visible result within the configured budget.
- id: tool-result-prune
name: '@deepseek-ai/dsh-compact-tool-result-prune'

View File

@@ -31,7 +31,49 @@
apiKey: !!js process.env.DEEPSEEK_API_KEY
baseURL: !!js process.env.DEEPSEEK_BASE_URL
# The web surface replaces the unrestricted local executors with the shared
# sandbox policy. Its default preserves the previous unrestricted behavior;
# DSH_PERMISSION_MODE and the browser permission picker can confine a session.
- insert:
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: sandbox-policy
name: '@deepseek-ai/dsh-sandbox-policy'
config:
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'
workspaceRoot: !!js process.cwd()
- id: bash-sandbox
name: '@deepseek-ai/dsh-bash-sandbox'
- id: approval
name: '@deepseek-ai/dsh-user-approval'
config:
policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'danger-full-access') === 'danger-full-access' ? 'never' : 'ask'"
- id: permission
name: '@deepseek-ai/dsh-permission'
config:
presets:
read-only:
sandbox: read-only
approval: ask
workspace-write:
sandbox: workspace-write
approval: ask
danger-full-access:
sandbox: danger-full-access
approval: never
- id: fs-sandbox
name: '@deepseek-ai/dsh-fs-sandbox'
- id: bash-local
remove: true
- id: fs-local
remove: true
# ── web-only host rows, the transport layer, and the browser roster ─────────
@@ -72,6 +114,9 @@
# The API gateway: the transport-agnostic dispatch face every client shape
# shares. provider/model are the host default routing — the profile json's
# mapping target (user config overrides these engineering defaults).
- id: directory-picker
name: '@deepseek-ai/dsh-host-directory-picker-browse'
- id: api-gateway
name: '@deepseek-ai/dsh-host-apiproxy'
config:
@@ -156,6 +201,9 @@
- id: ui-model
name: '@deepseek-ai/dsh-client-ui-model'
- id: ui-permission
name: '@deepseek-ai/dsh-client-ui-permission'
# Plan control: the composer plan seat over the plan projection + /plan channel.
- id: ui-plan
name: '@deepseek-ai/dsh-client-ui-plan'

View File

@@ -94,4 +94,4 @@ flowchart LR
Source config: [`examples/acp-agent/cordis.yml`](cordis.yml).
Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

View File

@@ -76,4 +76,4 @@ flowchart LR
Source config: [`examples/headless-agent/cordis.yml`](cordis.yml).
Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

27
pnpm-lock.yaml generated
View File

@@ -134,6 +134,9 @@ importers:
'@deepseek-ai/dsh-bash-local':
specifier: workspace:^
version: link:../../packages/bash/bash-local
'@deepseek-ai/dsh-bash-sandbox':
specifier: workspace:^
version: link:../../packages/bash/bash-sandbox
'@deepseek-ai/dsh-client-connection':
specifier: workspace:^
version: link:../../packages/client/connection
@@ -167,6 +170,9 @@ importers:
'@deepseek-ai/dsh-client-ui-models':
specifier: workspace:^
version: link:../../packages/client/ui-models
'@deepseek-ai/dsh-client-ui-permission':
specifier: workspace:^
version: link:../../packages/client/ui-permission
'@deepseek-ai/dsh-client-ui-plan':
specifier: workspace:^
version: link:../../packages/client/ui-plan
@@ -224,18 +230,21 @@ importers:
'@deepseek-ai/dsh-fs-policy':
specifier: workspace:^
version: link:../../packages/fs/fs-policy
'@deepseek-ai/dsh-fs-sandbox':
specifier: workspace:^
version: link:../../packages/fs/fs-sandbox
'@deepseek-ai/dsh-goal':
specifier: workspace:^
version: link:../../packages/goal/goal
'@deepseek-ai/dsh-goal-session':
specifier: workspace:^
version: link:../../packages/goal/goal-session
'@deepseek-ai/dsh-helper':
specifier: workspace:^
version: link:../../packages/sdk/helper
'@deepseek-ai/dsh-host-apiproxy':
specifier: workspace:^
version: link:../../packages/host/apiproxy
'@deepseek-ai/dsh-host-directory-picker-browse':
specifier: workspace:^
version: link:../../packages/host/directory-picker-browse
'@deepseek-ai/dsh-host-webserver':
specifier: workspace:^
version: link:../../packages/host/webserver
@@ -257,9 +266,18 @@ importers:
'@deepseek-ai/dsh-paths':
specifier: workspace:^
version: link:../../packages/util/paths
'@deepseek-ai/dsh-permission':
specifier: workspace:^
version: link:../../packages/ui/permission
'@deepseek-ai/dsh-plan-mode':
specifier: workspace:^
version: link:../../packages/plan/plan-mode
'@deepseek-ai/dsh-sandbox-local':
specifier: workspace:^
version: link:../../packages/sandbox/sandbox-local
'@deepseek-ai/dsh-sandbox-policy':
specifier: workspace:^
version: link:../../packages/sandbox/sandbox-policy
'@deepseek-ai/dsh-scope':
specifier: workspace:^
version: link:../../packages/core/scope
@@ -374,6 +392,9 @@ importers:
'@deepseek-ai/dsh-tui':
specifier: workspace:^
version: link:../../packages/ui/tui
'@deepseek-ai/dsh-user-approval':
specifier: workspace:^
version: link:../../packages/ui/user-approval
'@deepseek-ai/dsh-user-interaction':
specifier: workspace:^
version: link:../../packages/ui/user-interaction

View File

@@ -597,7 +597,7 @@ const APP_EXAMPLES = [
rel: 'apps/cli/composition.md',
title: 'dsh TUI Composition',
label: 'apps/cli (dsh)',
config: 'apps/cli/base.cordis.yml',
configs: ['apps/cli/base.cordis.yml', 'apps/cli/tui.cordis.yml'],
summary: 'The TUI agent combines the real DeepSeek adapter, coding tools, compaction, subagents, and workflows with the full-screen terminal app package.',
},
{
@@ -605,7 +605,7 @@ const APP_EXAMPLES = [
rel: 'examples/headless-agent/composition.md',
title: 'Headless Agent App Composition',
label: 'examples/headless-agent',
config: 'examples/headless-agent/cordis.yml',
configs: ['examples/headless-agent/cordis.yml'],
summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
},
{
@@ -613,7 +613,7 @@ const APP_EXAMPLES = [
rel: 'examples/acp-agent/composition.md',
title: 'ACP Automation App Composition',
label: 'examples/acp-agent',
config: 'examples/acp-agent/cordis.yml',
configs: ['examples/acp-agent/cordis.yml'],
summary: 'The ACP demo exposes fresh baseline-prompt agent sessions to programmatic clients over JSON-RPC stdio, with no stdout logger, human UI, or pre-created agent.',
},
]
@@ -639,15 +639,15 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
}
function renderAppComposition(example: AppExample): string {
const plugins = parseExampleCordis(example.config)
const maintenance = 'hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source'
const plugins = example.configs.flatMap(parseExampleCordis)
const maintenance = 'hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source'
const lines = generatedHeader(example.title)
lines.push(
example.summary,
'',
'```mermaid',
'flowchart LR',
` cfg["${escLabel(example.label)}<br/>cordis.yml"]`,
` cfg["${escLabel(example.label)}<br/>${escLabel(example.configs.map(config => config.split('/').at(-1)).join(' + '))}"]`,
)
for (const plugin of plugins) {
const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
@@ -664,7 +664,7 @@ function renderAppComposition(example: AppExample): string {
'| --- | --- |',
...plugins.map(plugin => `| \`${plugin.id}\` | \`${plugin.name}\` |`),
'',
`Source config: [\`${example.config}\`](${linkFromDoc(example.rel, example.config)}).`,
`Source config${example.configs.length === 1 ? '' : 's'}: ${example.configs.map(config => `[\`${config}\`](${linkFromDoc(example.rel, config)})`).join(', ')}.`,
)
lines.push('', ...maintenanceFooter(maintenance))
return lines.join('\n')

View File

@@ -78,6 +78,11 @@ function validateEntry(value: unknown, file: string, path: string): void {
validateEntry(value.config[index], file, `${path}.config[${index}]`)
}
}
if (isUnknownArray(value.insert)) {
for (let index = 0; index < value.insert.length; index++) {
validateEntry(value.insert[index], file, `${path}.insert[${index}]`)
}
}
if (value.name !== '@cordisjs/plugin-include') return
const config = value.config
if (!isRecord(config) || !isUnknownArray(config.patches)) return
@@ -124,7 +129,7 @@ function validateExampleResolution(): string[] {
function validateAppResolution(): string[] {
const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
const references = pluginReferences.filter(reference => reference.file === 'apps/cli/base.cordis.yml')
const references = pluginReferences.filter(reference => reference.file.startsWith('apps/cli/'))
return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
}