mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Adds `mode: 'deepseek'` alongside claude/shell/opencode/codex/gemini/ antigravity/pi/grok, plus a shortcut that opens the harness's own browser UI as a Codeman web tab. DeepSeek is wired unlike its siblings in three ways, each of which is the reason for a design decision rather than an accident: 1. The agent is a PROFILE, not the binary. `dsh` is a launcher over $DSH_HOME/profiles/<name>, and DeepSeek ships only `web`, `headless` and `base` -- the interactive terminal front door is always a third-party plugin. So availability is two questions: `isDeepSeekAvailable()` (binary) and `isDeepSeekRunnable()` (binary AND a pane-capable profile). The Run button gates on the latter, because reporting only the binary would spawn a pane that dies on arrival. When the binary is present but no profile is, the run menu offers to install one (POST /api/deepseek/install-profile). 2. The permission switch is an env var, not a flag. The harness has no command-line permission option; its sandbox/approval rows read DSH_PERMISSION_MODE (read-only / workspace-write / danger-full-access). Exported via `tmux setenv`, never on the spawn line. Absent = the harness's own workspace-write, which still asks, so the multi-user clamp is the only-if-sent branch and clamps to workspace-write, never read-only. 3. It is the only non-claude mode that passes hooksAvailableForMode(), and it earned that. The terminal front door reports idle/working/blocked to a supervising process over a generic env-gated contract; a generated shim (deepseek-status-shim.ts) makes Codeman that supervisor and forwards each report to /api/hook-event as stop / agent_working / permission_prompt. So a dsh session gets definitive respawn triggers, real wait-endpoint signals and real Approvals Inbox items instead of output-stabilization guesswork. `agent_working` is new (157th SSE constant) and joins APPROVAL_RESOLVING_EVENTS so a dialog answered in the terminal clears its alert at once. The resolver needs the strictest identity probe of the family: `dsh` is not merely a squattable npm name, Debian ships an unrelated `dsh` (dancer's shell), so `dsh --help` must print the harness's own banner before a candidate is handed a spawn line. Model is deliberately not a session field -- it is a composition entry in the profile's config tree. Env allowlist gains DSH_* and DEEPSEEK_* only; provider keys named by a settings-file `apiKeyEnv` stay out, which is pi's 34-provider-key problem in a new shape. Verified live against dsh 0.1.1-rc.2 and @deepseek-harness-tui/dsh-tui: the status endpoint's two-part answer, the no-profile refusal, the profile bootstrap, a real session whose pane runs `dsh --profile dsh-tui` with the permission mode injected via setenv, and the full status bridge -- a send-and-wait returned signal "stop" from a real turn, and blocked/working created and cleared an Approvals Inbox item. Docs: docs/deepseek-integration.md (guide), docs/deepseek-integration-plan.md (decisions + honest gaps). Tests: test/deepseek-mode.test.ts, test/deepseek-cli-resolver.test.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
241 lines
7.8 KiB
TypeScript
241 lines
7.8 KiB
TypeScript
/**
|
|
* @fileoverview Static registry of downstream tool dependencies probed by
|
|
* `codeman doctor`. Each entry declares per-environment resolvers and the
|
|
* skills that use it. EXTENSION POINT: skill-manifest-driven discovery
|
|
* (COD follow-up) will merge dynamically-found entries into this list.
|
|
*
|
|
* @module config/dependency-registry
|
|
*/
|
|
|
|
import { PI_VERSION_REGEX } from '../utils/pi-cli-resolver.js';
|
|
import { GROK_VERSION_REGEX } from '../utils/grok-cli-resolver.js';
|
|
import { DEEPSEEK_VERSION_REGEX } from '../utils/deepseek-cli-resolver.js';
|
|
|
|
export type ProbeEnvironment = 'linux' | 'darwin' | 'win32' | 'wsl';
|
|
|
|
/** The valid `--category` filter values; single source of truth for the type, the CLI
|
|
* help text, and CLI input validation. */
|
|
export const TOOL_CATEGORIES = ['core', 'office', 'other'] as const;
|
|
export type ToolCategory = (typeof TOOL_CATEGORIES)[number];
|
|
|
|
/** Resolve a binary on the PATH and read its version. */
|
|
export interface PathResolver {
|
|
kind: 'path';
|
|
bins: string[];
|
|
versionArg?: string; // default '--version'
|
|
versionRegex?: RegExp; // default matches first \d+.\d+(.\d+)?
|
|
/**
|
|
* Treat a binary whose version output does not match as NOT INSTALLED, instead of
|
|
* reporting it with an unknown version. Only for tools with a short, generic binary
|
|
* name (`pi`), where a `which` hit is not by itself evidence the right program is
|
|
* there and a false "installed" contradicts the run mode's own resolver.
|
|
*/
|
|
requireVersionMatch?: boolean;
|
|
}
|
|
|
|
/** Resolve a Windows-installed app reachable from win32 or WSL. */
|
|
export interface WindowsSideResolver {
|
|
kind: 'windows-side';
|
|
appDirs: string[]; // relative to a Program Files root
|
|
exes: string[]; // candidate executables; first found wins
|
|
}
|
|
|
|
export interface ResolverSpec {
|
|
match: ProbeEnvironment[];
|
|
resolver: PathResolver | WindowsSideResolver;
|
|
}
|
|
|
|
export interface ToolDependency {
|
|
id: string;
|
|
label: string;
|
|
category: ToolCategory;
|
|
required: boolean;
|
|
usedBy?: string[];
|
|
minVersion?: string;
|
|
resolvers: ResolverSpec[];
|
|
installHint?: Partial<Record<ProbeEnvironment, string>>;
|
|
}
|
|
|
|
const ALL: ProbeEnvironment[] = ['linux', 'darwin', 'wsl', 'win32'];
|
|
|
|
export const DEPENDENCY_REGISTRY: ToolDependency[] = [
|
|
{
|
|
id: 'node',
|
|
label: 'Node.js',
|
|
category: 'core',
|
|
required: true,
|
|
minVersion: '22.0.0',
|
|
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['node'], versionArg: '--version' } }],
|
|
installHint: { linux: 'https://nodejs.org', darwin: 'brew install node', wsl: 'https://nodejs.org' },
|
|
},
|
|
{
|
|
id: 'claude',
|
|
label: 'Claude CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['Claude Code sessions (default backend)'],
|
|
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['claude'], versionArg: '--version' } }],
|
|
installHint: { linux: 'https://docs.claude.com/claude-code', darwin: 'https://docs.claude.com/claude-code' },
|
|
},
|
|
{
|
|
id: 'tmux',
|
|
label: 'tmux',
|
|
category: 'core',
|
|
required: true,
|
|
resolvers: [{ match: ['linux', 'darwin', 'wsl'], resolver: { kind: 'path', bins: ['tmux'], versionArg: '-V' } }],
|
|
installHint: { linux: 'sudo apt install tmux', darwin: 'brew install tmux', wsl: 'sudo apt install tmux' },
|
|
},
|
|
{
|
|
id: 'opencode',
|
|
label: 'OpenCode CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['OpenCode sessions'],
|
|
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['opencode'], versionArg: '--version' } }],
|
|
},
|
|
{
|
|
id: 'codex',
|
|
label: 'Codex CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['Codex sessions'],
|
|
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['codex'], versionArg: '--version' } }],
|
|
},
|
|
{
|
|
id: 'gemini',
|
|
label: 'Gemini CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['Gemini sessions'],
|
|
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['gemini'], versionArg: '--version' } }],
|
|
},
|
|
{
|
|
id: 'antigravity',
|
|
label: 'Antigravity CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['Antigravity sessions'],
|
|
resolvers: [{ match: ALL, resolver: { kind: 'path', bins: ['agy'], versionArg: '--version' } }],
|
|
},
|
|
{
|
|
id: 'pi',
|
|
label: 'Pi CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['Pi sessions'],
|
|
// The only entry that requires a version match, for the same reason
|
|
// pi-cli-resolver.ts probes: `pi` is a short generic name (Raspberry Pi tooling,
|
|
// personal scripts), so a `which pi` hit alone is not the coding agent. Both sides
|
|
// share PI_VERSION_REGEX, so the doctor and the run mode cannot drift into telling
|
|
// the user opposite things about the same binary.
|
|
resolvers: [
|
|
{
|
|
match: ALL,
|
|
resolver: {
|
|
kind: 'path',
|
|
bins: ['pi'],
|
|
versionArg: '--version',
|
|
versionRegex: PI_VERSION_REGEX,
|
|
requireVersionMatch: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'grok',
|
|
label: 'Grok CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['Grok sessions'],
|
|
// Version match required for the same reason as pi: `grok` has known squatters
|
|
// (the unrelated @vibe-kit/grok-cli npm package also installs a `grok` bin), so a
|
|
// bare `which grok` hit is not the coding agent. Both sides share
|
|
// GROK_VERSION_REGEX, so the doctor and the run mode cannot drift.
|
|
resolvers: [
|
|
{
|
|
match: ALL,
|
|
resolver: {
|
|
kind: 'path',
|
|
bins: ['grok'],
|
|
versionArg: '--version',
|
|
versionRegex: GROK_VERSION_REGEX,
|
|
requireVersionMatch: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'dsh',
|
|
label: 'DeepSeek Harness CLI',
|
|
category: 'core',
|
|
required: false,
|
|
usedBy: ['DeepSeek sessions'],
|
|
// Version match required, and for a sharper reason than pi or grok: `dsh` is
|
|
// not merely a squattable npm name, it is an existing Debian program
|
|
// (dancer's shell, `apt install dsh`). The run mode's resolver additionally
|
|
// demands the harness's own help banner before it will point a spawn line at
|
|
// a candidate; the doctor is advisory and settles for the shared
|
|
// DEEPSEEK_VERSION_REGEX, so the two cannot disagree about the VERSION even
|
|
// though the resolver is the stricter of the pair about IDENTITY.
|
|
resolvers: [
|
|
{
|
|
match: ALL,
|
|
resolver: {
|
|
kind: 'path',
|
|
bins: ['dsh'],
|
|
versionArg: '--version',
|
|
versionRegex: DEEPSEEK_VERSION_REGEX,
|
|
requireVersionMatch: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'libreoffice',
|
|
label: 'LibreOffice',
|
|
category: 'office',
|
|
required: false,
|
|
usedBy: ['document preview', 'thumbnails'],
|
|
resolvers: [
|
|
{
|
|
match: ['linux', 'darwin', 'wsl'],
|
|
resolver: { kind: 'path', bins: ['libreoffice', 'soffice'], versionArg: '--version' },
|
|
},
|
|
],
|
|
installHint: { linux: 'sudo apt install libreoffice', darwin: 'brew install --cask libreoffice' },
|
|
},
|
|
{
|
|
id: 'pdftoppm',
|
|
label: 'pdftoppm',
|
|
category: 'office',
|
|
required: false,
|
|
usedBy: ['document preview', 'PDF/Office first-page thumbnails'],
|
|
// poppler's pdftoppm prints its version to stderr; presence is what matters here.
|
|
resolvers: [
|
|
{ match: ['linux', 'darwin', 'wsl'], resolver: { kind: 'path', bins: ['pdftoppm'], versionArg: '-v' } },
|
|
],
|
|
installHint: {
|
|
linux: 'sudo apt install poppler-utils',
|
|
darwin: 'brew install poppler',
|
|
wsl: 'sudo apt install poppler-utils',
|
|
},
|
|
},
|
|
{
|
|
id: 'msoffice',
|
|
label: 'MS Office',
|
|
category: 'office',
|
|
required: false,
|
|
usedBy: ['document preview', 'thumbnails'],
|
|
resolvers: [
|
|
{
|
|
match: ['wsl', 'win32'],
|
|
resolver: {
|
|
kind: 'windows-side',
|
|
appDirs: ['Microsoft Office/root/Office16'],
|
|
exes: ['WINWORD.EXE', 'POWERPNT.EXE', 'EXCEL.EXE'],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|