@lifenrir/dsh-external-task-context-patch 0.0.0-stage → 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/README-zh.md +103 -0
- package/README.md +124 -2
- package/install.mjs +543 -0
- package/lib.mjs +274 -0
- package/package.json +33 -4
- package/src/index.ts +189 -0
- package/src/log.ts +235 -0
- package/src/run.ts +650 -0
package/lib.mjs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for the patch installer: argument parsing, version/tag
|
|
3
|
+
* mapping, DSH home resolution, profile discovery, and install-record checks.
|
|
4
|
+
* These helpers are free of installer side effects so they can be unit tested.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-external-task-context-patch/lib
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { readdir } from 'node:fs/promises'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
12
|
+
import { join, resolve } from 'node:path'
|
|
13
|
+
|
|
14
|
+
/** npm name of the provider package this patch targets. */
|
|
15
|
+
export const PROVIDER_PACKAGE_NAME = '@deepseek-ai/dsh-subagent-claude-code'
|
|
16
|
+
|
|
17
|
+
/** git tag used when the caller does not pass `--tag`. */
|
|
18
|
+
export const DEFAULT_TAG = 'dsh-v0.2.2-rc.1'
|
|
19
|
+
|
|
20
|
+
/** Official provider source files fetched or read from `--source`. */
|
|
21
|
+
export const PROVIDER_SRC_FILES = ['index.ts', 'run.ts', 'process.ts']
|
|
22
|
+
|
|
23
|
+
/** Patched source files copied from this package's `src/` directory. */
|
|
24
|
+
export const PATCHED_SRC_FILES = ['index.ts', 'run.ts', 'log.ts']
|
|
25
|
+
|
|
26
|
+
/** Directory name under `<DSH_HOME>/backups` that owns all patch records. */
|
|
27
|
+
export const BACKUP_DIR_NAME = 'subagent-claude-code-patch'
|
|
28
|
+
|
|
29
|
+
/** Relative path of the run-log directory created by the patched provider. */
|
|
30
|
+
export const RUN_LOG_RELATIVE_PATH = ['logs', 'subagent-claude-code']
|
|
31
|
+
|
|
32
|
+
const VALUE_OPTIONS = new Set(['--dsh-home', '--profile', '--tag', '--source'])
|
|
33
|
+
const FLAG_OPTIONS = new Set(['--dry-run', '--force', '--restore', '--yes', '--help', '-h'])
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse installer CLI arguments.
|
|
37
|
+
* @param {readonly string[]} argv - process arguments without the executable and script.
|
|
38
|
+
* @returns {{ dshHome: string | undefined, profiles: string[], tag: string, source: string | undefined, dryRun: boolean, force: boolean, restore: boolean, yes: boolean, help: boolean }}
|
|
39
|
+
* @throws {Error} on an unknown option, a missing value, or an invalid profile name.
|
|
40
|
+
*/
|
|
41
|
+
export function parseArgs(argv) {
|
|
42
|
+
const options = {
|
|
43
|
+
dshHome: undefined,
|
|
44
|
+
profiles: [],
|
|
45
|
+
tag: DEFAULT_TAG,
|
|
46
|
+
source: undefined,
|
|
47
|
+
dryRun: false,
|
|
48
|
+
force: false,
|
|
49
|
+
restore: false,
|
|
50
|
+
yes: false,
|
|
51
|
+
help: false,
|
|
52
|
+
}
|
|
53
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
54
|
+
const argument = argv[index]
|
|
55
|
+
if (FLAG_OPTIONS.has(argument)) {
|
|
56
|
+
switch (argument) {
|
|
57
|
+
case '--dry-run': options.dryRun = true; break
|
|
58
|
+
case '--force': options.force = true; break
|
|
59
|
+
case '--restore': options.restore = true; break
|
|
60
|
+
case '--yes': options.yes = true; break
|
|
61
|
+
default: options.help = true
|
|
62
|
+
}
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let name = argument
|
|
67
|
+
let value
|
|
68
|
+
const equals = argument.indexOf('=')
|
|
69
|
+
if (argument.startsWith('--') && equals !== -1) {
|
|
70
|
+
name = argument.slice(0, equals)
|
|
71
|
+
value = argument.slice(equals + 1)
|
|
72
|
+
}
|
|
73
|
+
if (!VALUE_OPTIONS.has(name)) {
|
|
74
|
+
throw new Error(`unknown option: ${argument}`)
|
|
75
|
+
}
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
index += 1
|
|
78
|
+
value = argv[index]
|
|
79
|
+
if (value === undefined || value.startsWith('--')) {
|
|
80
|
+
throw new Error(`missing value for ${name}`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (value.trim().length === 0) {
|
|
84
|
+
throw new Error(`empty value for ${name}`)
|
|
85
|
+
}
|
|
86
|
+
switch (name) {
|
|
87
|
+
case '--dsh-home': options.dshHome = value; break
|
|
88
|
+
case '--profile': options.profiles.push(assertProfileName(value)); break
|
|
89
|
+
case '--tag': options.tag = value; break
|
|
90
|
+
case '--source': options.source = value; break
|
|
91
|
+
default: throw new Error(`unknown option: ${name}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return options
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reject profile names that could escape `<DSH_HOME>/profiles`.
|
|
99
|
+
* @param {string} name - profile directory name.
|
|
100
|
+
* @returns {string} the unchanged name when valid.
|
|
101
|
+
*/
|
|
102
|
+
export function assertProfileName(name) {
|
|
103
|
+
if (
|
|
104
|
+
name.length === 0
|
|
105
|
+
|| name === '.'
|
|
106
|
+
|| name === '..'
|
|
107
|
+
|| name.includes('/')
|
|
108
|
+
|| name.includes('\\')
|
|
109
|
+
) {
|
|
110
|
+
throw new Error(`invalid profile name: ${name}`)
|
|
111
|
+
}
|
|
112
|
+
return name
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the DeepSeek Harness home directory.
|
|
117
|
+
* @param {string | undefined} explicit - `--dsh-home` value, when supplied.
|
|
118
|
+
* @param {NodeJS.ProcessEnv} [env] - environment source.
|
|
119
|
+
* @param {string} [home] - operating-system home directory.
|
|
120
|
+
* @returns {string} absolute DSH home path.
|
|
121
|
+
* @throws {Error} when an explicit `--dsh-home` is blank.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveDshHome(explicit, env = process.env, home = homedir()) {
|
|
124
|
+
if (explicit !== undefined) {
|
|
125
|
+
if (explicit.trim().length === 0) {
|
|
126
|
+
throw new Error('--dsh-home must not be empty')
|
|
127
|
+
}
|
|
128
|
+
return resolve(explicit.trim())
|
|
129
|
+
}
|
|
130
|
+
const configured = env.DSH_HOME
|
|
131
|
+
if (configured !== undefined && configured.trim().length > 0) {
|
|
132
|
+
return resolve(configured.trim())
|
|
133
|
+
}
|
|
134
|
+
return join(home, '.dsh')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Map a provider package version to its deepseek-harness git tag.
|
|
139
|
+
* @param {string} version - package version, for example `0.2.2-rc.1`.
|
|
140
|
+
* @returns {string} tag, for example `dsh-v0.2.2-rc.1`.
|
|
141
|
+
*/
|
|
142
|
+
export function versionToTag(version) {
|
|
143
|
+
if (typeof version !== 'string' || version.trim().length === 0) {
|
|
144
|
+
throw new Error('provider version is empty')
|
|
145
|
+
}
|
|
146
|
+
return `dsh-v${version.trim()}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Map a deepseek-harness git tag to the provider package version.
|
|
151
|
+
* @param {string} tag - tag, for example `dsh-v0.2.2-rc.1`.
|
|
152
|
+
* @returns {string} version, for example `0.2.2-rc.1`.
|
|
153
|
+
*/
|
|
154
|
+
export function tagToVersion(tag) {
|
|
155
|
+
if (typeof tag !== 'string' || !tag.startsWith('dsh-v') || tag.length === 5) {
|
|
156
|
+
throw new Error(`invalid DSH release tag: ${String(tag)}`)
|
|
157
|
+
}
|
|
158
|
+
return tag.slice(5)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether an installed provider version matches the requested tag.
|
|
163
|
+
* @param {string} version - installed package version.
|
|
164
|
+
* @param {string} tag - requested git tag.
|
|
165
|
+
* @returns {boolean} true when both name the same release.
|
|
166
|
+
*/
|
|
167
|
+
export function versionMatchesTag(version, tag) {
|
|
168
|
+
return version === tagToVersion(tag)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Directory holding one profile's installed provider package.
|
|
173
|
+
* @param {string} profileDir - absolute profile directory.
|
|
174
|
+
* @returns {string} absolute provider package directory.
|
|
175
|
+
*/
|
|
176
|
+
export function providerDirFor(profileDir) {
|
|
177
|
+
return join(profileDir, 'node_modules', ...PROVIDER_PACKAGE_NAME.split('/'))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* List profiles that have the target provider installed.
|
|
182
|
+
* @param {string} dshHome - absolute DSH home.
|
|
183
|
+
* @param {{ readdir?: typeof readdir, exists?: (path: string) => boolean }} [io] - injection points for tests.
|
|
184
|
+
* @returns {Promise<string[]>} matching profile names, sorted.
|
|
185
|
+
*/
|
|
186
|
+
export async function discoverProfiles(dshHome, io = {}) {
|
|
187
|
+
const readdirImpl = io.readdir ?? readdir
|
|
188
|
+
const existsImpl = io.exists ?? existsSync
|
|
189
|
+
const profilesRoot = join(dshHome, 'profiles')
|
|
190
|
+
let entries
|
|
191
|
+
try {
|
|
192
|
+
entries = await readdirImpl(profilesRoot, { withFileTypes: true })
|
|
193
|
+
} catch {
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
const matches = []
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
199
|
+
if (existsImpl(providerDirFor(join(profilesRoot, entry.name)))) {
|
|
200
|
+
matches.push(entry.name)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return matches.sort()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Render a filesystem-safe UTC timestamp used as a backup directory name.
|
|
208
|
+
* @param {Date} [date] - instant to render.
|
|
209
|
+
* @returns {string} timestamp such as `2026-11-06T14-07-00-000Z`.
|
|
210
|
+
*/
|
|
211
|
+
export function timestampDir(date = new Date()) {
|
|
212
|
+
return date.toISOString().replace(/[:.]/g, '-')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build the GitHub raw URL for one official provider source file.
|
|
217
|
+
* @param {string} tag - release tag.
|
|
218
|
+
* @param {string} file - source file name.
|
|
219
|
+
* @returns {string} raw.githubusercontent.com URL.
|
|
220
|
+
*/
|
|
221
|
+
export function rawSourceUrl(tag, file) {
|
|
222
|
+
const path = [
|
|
223
|
+
'deepseek-ai',
|
|
224
|
+
'deepseek-harness',
|
|
225
|
+
tag,
|
|
226
|
+
'packages',
|
|
227
|
+
'subagent',
|
|
228
|
+
'subagent-claude-code',
|
|
229
|
+
'src',
|
|
230
|
+
file,
|
|
231
|
+
].map(part => encodeURIComponent(part)).join('/')
|
|
232
|
+
return `https://raw.githubusercontent.com/${path}`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Root directory that owns timestamped backup sets.
|
|
237
|
+
* @param {string} dshHome - absolute DSH home.
|
|
238
|
+
* @returns {string} absolute backup root.
|
|
239
|
+
*/
|
|
240
|
+
export function backupsRoot(dshHome) {
|
|
241
|
+
return join(dshHome, 'backups', BACKUP_DIR_NAME)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Backup directory for one profile inside a timestamped set.
|
|
246
|
+
* @param {string} dshHome - absolute DSH home.
|
|
247
|
+
* @param {string} timestamp - output of {@link timestampDir}.
|
|
248
|
+
* @param {string} profile - profile name.
|
|
249
|
+
* @returns {string} absolute backup directory.
|
|
250
|
+
*/
|
|
251
|
+
export function backupProfileDir(dshHome, timestamp, profile) {
|
|
252
|
+
return join(backupsRoot(dshHome), timestamp, assertProfileName(profile))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Run-log directory reported by the patched provider.
|
|
257
|
+
* @param {string} dshHome - absolute DSH home.
|
|
258
|
+
* @returns {string} absolute log directory.
|
|
259
|
+
*/
|
|
260
|
+
export function runLogDirectory(dshHome) {
|
|
261
|
+
return join(dshHome, ...RUN_LOG_RELATIVE_PATH)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Whether an install record represents an active patch for the given tag.
|
|
266
|
+
* @param {{ tag?: string, restoredAt?: string } | undefined} record - install record.
|
|
267
|
+
* @param {string} tag - requested tag.
|
|
268
|
+
* @returns {boolean} true when the record is unrestored and names the same tag.
|
|
269
|
+
*/
|
|
270
|
+
export function isMatchingActiveRecord(record, tag) {
|
|
271
|
+
return record !== undefined
|
|
272
|
+
&& record.tag === tag
|
|
273
|
+
&& (record.restoredAt === undefined || record.restoredAt === null)
|
|
274
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifenrir/dsh-external-task-context-patch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install a run-log-capable patched @deepseek-ai/dsh-subagent-claude-code into an existing DSH profile for @lifenrir/dsh-external-task-context live logs.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dsh-external-task-context-patch": "./install.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"install.mjs",
|
|
12
|
+
"lib.mjs",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"README-zh.md"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"esbuild": "^0.25.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test tests/lib.test.mjs"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/LiFenrir/dsh-external-task-context.git",
|
|
29
|
+
"directory": "patch"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/LiFenrir/dsh-external-task-context/tree/main/patch#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/LiFenrir/dsh-external-task-context/issues"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-named Claude Code one-shot subagent provider. Every accepted run
|
|
3
|
+
* invokes the official Agent SDK in the delegating Session's workspace and
|
|
4
|
+
* places the SDK-spawned real CLI under the shared subprocess owner.
|
|
5
|
+
*
|
|
6
|
+
* @module @deepseek-ai/dsh-subagent-claude-code
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
+
import z from '@deepseek-ai/schemastery'
|
|
11
|
+
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
|
12
|
+
import {
|
|
13
|
+
assertPositiveFinite,
|
|
14
|
+
NO_START_CAPABILITIES,
|
|
15
|
+
assertUsableCwd,
|
|
16
|
+
type ResolvedSubagentStartRequest,
|
|
17
|
+
type SubagentCapabilities,
|
|
18
|
+
type SubagentProvider,
|
|
19
|
+
} from '@deepseek-ai/dsh-subagent'
|
|
20
|
+
import { DEFAULT_LOG_RETENTION_DAYS } from './log.ts'
|
|
21
|
+
import {
|
|
22
|
+
CLAUDE_CODE_PERMISSION_MODES,
|
|
23
|
+
DEFAULT_CLAUDE_CODE_PERMISSION_MODE,
|
|
24
|
+
DEFAULT_DISPOSE_GRACE_MS,
|
|
25
|
+
claudeCodeStartupFailure,
|
|
26
|
+
startClaudeCodeRun,
|
|
27
|
+
type ClaudeCodePermissionMode,
|
|
28
|
+
type ClaudeCodeRunSpec,
|
|
29
|
+
} from './run.ts'
|
|
30
|
+
|
|
31
|
+
export const name = 'subagent-claude-code'
|
|
32
|
+
export const inject = ['subagents', 'subprocess']
|
|
33
|
+
|
|
34
|
+
const DEFAULT_PROVIDER_NAME = 'claude-code'
|
|
35
|
+
|
|
36
|
+
/* jscpd:ignore-start -- sibling product providers intentionally expose
|
|
37
|
+
* overlapping deployment-owned fields without adding a shared config owner. */
|
|
38
|
+
/** Deployment-owned model, permission, environment, and process-release settings. */
|
|
39
|
+
export interface Config {
|
|
40
|
+
/** Provider name on `ctx.subagents` (default `claude-code`). */
|
|
41
|
+
providerName?: string
|
|
42
|
+
/** Native Claude model fixed for this instance; omitted to inherit Claude settings. */
|
|
43
|
+
model?: string
|
|
44
|
+
/**
|
|
45
|
+
* Explicit environment entries layered over the subprocess seam's
|
|
46
|
+
* credential-scrubbed parent environment.
|
|
47
|
+
*/
|
|
48
|
+
env?: Record<string, string>
|
|
49
|
+
/**
|
|
50
|
+
* Native non-interactive mode fixed for this Provider instance. Defaults to
|
|
51
|
+
* `dontAsk`; `acceptEdits` accepts edits, `auto` uses the native classifier,
|
|
52
|
+
* `plan` returns a plan without approving execution, and
|
|
53
|
+
* `bypassPermissions` explicitly skips permission checks.
|
|
54
|
+
*/
|
|
55
|
+
permissionMode?: ClaudeCodePermissionMode
|
|
56
|
+
/** Grace in milliseconds between Claude Code managed-range termination tiers. */
|
|
57
|
+
disposeGraceMs?: number
|
|
58
|
+
/**
|
|
59
|
+
* Directory for per-run JSONL and metadata logs. Defaults, resolved at run
|
|
60
|
+
* time, to `<DSH_HOME>/logs/subagent-claude-code`.
|
|
61
|
+
*/
|
|
62
|
+
logDirectory?: string
|
|
63
|
+
/** Days to retain finished run logs (default 7; must be a positive finite number). */
|
|
64
|
+
logRetentionDays?: number
|
|
65
|
+
/**
|
|
66
|
+
* Whether Claude Code persists its native session transcript, which the SDK
|
|
67
|
+
* writes under `<CLAUDE_CONFIG_DIR>/projects/<cwd>/<sessionId>.jsonl`
|
|
68
|
+
* (default true).
|
|
69
|
+
*/
|
|
70
|
+
persistNativeTranscript?: boolean
|
|
71
|
+
/** Whether the SDK streams partial assistant messages so deltas reach the log (default true). */
|
|
72
|
+
includePartialMessages?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const Config: z<Config> = z.object({
|
|
76
|
+
providerName: z.string().min(1).default(DEFAULT_PROVIDER_NAME),
|
|
77
|
+
model: z.string().min(1),
|
|
78
|
+
env: z.dict(z.string()).default({}),
|
|
79
|
+
permissionMode: z.union([...CLAUDE_CODE_PERMISSION_MODES])
|
|
80
|
+
.default(DEFAULT_CLAUDE_CODE_PERMISSION_MODE),
|
|
81
|
+
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
|
|
82
|
+
logDirectory: z.string().min(1),
|
|
83
|
+
logRetentionDays: z.number().default(DEFAULT_LOG_RETENTION_DAYS),
|
|
84
|
+
persistNativeTranscript: z.boolean().default(true),
|
|
85
|
+
includePartialMessages: z.boolean().default(true),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
type ResolvedConfig = Omit<Required<Config>, 'model' | 'logDirectory'>
|
|
89
|
+
& Pick<Config, 'model' | 'logDirectory'>
|
|
90
|
+
/* jscpd:ignore-end */
|
|
91
|
+
|
|
92
|
+
/* jscpd:ignore-start -- Cordis registration and shared-seam plumbing mirror
|
|
93
|
+
* the Codex sibling; each product's lifecycle remains package-private. */
|
|
94
|
+
class ClaudeCodeProvider implements SubagentProvider {
|
|
95
|
+
readonly capabilities: SubagentCapabilities = NO_START_CAPABILITIES
|
|
96
|
+
readonly inheritsParentContext = false
|
|
97
|
+
|
|
98
|
+
constructor(
|
|
99
|
+
readonly name: string,
|
|
100
|
+
private readonly ctx: Context,
|
|
101
|
+
private readonly config: ResolvedConfig,
|
|
102
|
+
) {}
|
|
103
|
+
|
|
104
|
+
async start(request: ResolvedSubagentStartRequest) {
|
|
105
|
+
let cwd: string
|
|
106
|
+
try {
|
|
107
|
+
cwd = assertUsableCwd('subagent-claude-code', 'child cwd', request.cwd)
|
|
108
|
+
} catch (error: unknown) {
|
|
109
|
+
if (request.signal.aborted) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
'subagent-claude-code: request was aborted before SDK startup',
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
const failure = claudeCodeStartupFailure(error)
|
|
115
|
+
this.ctx.logger.warn(
|
|
116
|
+
`subagent-claude-code "${this.name}": child start failed: %o`,
|
|
117
|
+
failure,
|
|
118
|
+
)
|
|
119
|
+
throw failure
|
|
120
|
+
}
|
|
121
|
+
const spec: ClaudeCodeRunSpec = {
|
|
122
|
+
cwd,
|
|
123
|
+
...this.config.model === undefined ? {} : { model: this.config.model },
|
|
124
|
+
permissionMode: this.config.permissionMode,
|
|
125
|
+
env: this.config.env,
|
|
126
|
+
disposeGraceMs: this.config.disposeGraceMs,
|
|
127
|
+
...this.config.logDirectory === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { logDirectory: this.config.logDirectory },
|
|
130
|
+
logRetentionDays: this.config.logRetentionDays,
|
|
131
|
+
persistNativeTranscript: this.config.persistNativeTranscript,
|
|
132
|
+
includePartialMessages: this.config.includePartialMessages,
|
|
133
|
+
spawn: spawnSpec => this.ctx.subprocess.spawn(spawnSpec),
|
|
134
|
+
onError: (error, stopReason) => {
|
|
135
|
+
this.ctx.logger.warn(
|
|
136
|
+
`subagent-claude-code "${this.name}": child run failed (${stopReason}): %o`,
|
|
137
|
+
error,
|
|
138
|
+
)
|
|
139
|
+
},
|
|
140
|
+
onLogError: (error) => {
|
|
141
|
+
this.ctx.logger.warn(
|
|
142
|
+
`subagent-claude-code "${this.name}": run log write failed: %o`,
|
|
143
|
+
error,
|
|
144
|
+
)
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
return startClaudeCodeRun(request, spec)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Register one Profile-named Claude Code provider.
|
|
153
|
+
* @param ctx - context carrying shared subagent and subprocess services.
|
|
154
|
+
* @param config - registry name, optional model, permission mode, child environment, disposal grace, and run-log policy.
|
|
155
|
+
*/
|
|
156
|
+
export function apply(ctx: Context, config: Config): void {
|
|
157
|
+
const resolved: ResolvedConfig = {
|
|
158
|
+
providerName: config.providerName ?? DEFAULT_PROVIDER_NAME,
|
|
159
|
+
...config.model === undefined ? {} : { model: config.model },
|
|
160
|
+
env: config.env as Record<string, string>,
|
|
161
|
+
permissionMode: config.permissionMode ?? DEFAULT_CLAUDE_CODE_PERMISSION_MODE,
|
|
162
|
+
disposeGraceMs: config.disposeGraceMs as number,
|
|
163
|
+
logRetentionDays: config.logRetentionDays ?? DEFAULT_LOG_RETENTION_DAYS,
|
|
164
|
+
persistNativeTranscript: config.persistNativeTranscript ?? true,
|
|
165
|
+
includePartialMessages: config.includePartialMessages ?? true,
|
|
166
|
+
...config.logDirectory === undefined ? {} : { logDirectory: config.logDirectory },
|
|
167
|
+
}
|
|
168
|
+
assertPositiveFinite(
|
|
169
|
+
'subagent-claude-code',
|
|
170
|
+
'disposeGraceMs',
|
|
171
|
+
resolved.disposeGraceMs,
|
|
172
|
+
)
|
|
173
|
+
if (resolved.disposeGraceMs > MAX_TIMER_DELAY_MS) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`subagent-claude-code: disposeGraceMs must be no greater than ${MAX_TIMER_DELAY_MS}`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
assertPositiveFinite(
|
|
179
|
+
'subagent-claude-code',
|
|
180
|
+
'logRetentionDays',
|
|
181
|
+
resolved.logRetentionDays,
|
|
182
|
+
)
|
|
183
|
+
ctx.subagents.registerProvider(new ClaudeCodeProvider(
|
|
184
|
+
resolved.providerName,
|
|
185
|
+
ctx,
|
|
186
|
+
resolved,
|
|
187
|
+
))
|
|
188
|
+
}
|
|
189
|
+
/* jscpd:ignore-end */
|