@norman-else/dsh-claude 0.1.36 → 0.1.37
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/INSTALL.md +55 -55
- package/LICENSE +21 -21
- package/cordis.patch.yml +12 -12
- package/legacy-preset/agent.cordis.yml +11 -11
- package/legacy-preset/preset.yml +4 -4
- package/lib/bin.mjs +0 -0
- package/lib/bin.mjs.map +1 -1
- package/lib/client.d.ts +4 -0
- package/lib/client.js +123 -28
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +176 -41
- package/lib/index.mjs.map +1 -1
- package/lib/presenters-BBoM1Ju1.mjs.map +1 -1
- package/lib/preset-installer-loenwnLS.mjs.map +1 -1
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +195 -189
- package/preset/claude/agent.cordis.yml +11 -11
- package/preset/claude/preset.yml +4 -4
package/lib/index.mjs
CHANGED
|
@@ -3,12 +3,12 @@ import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicP
|
|
|
3
3
|
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-loenwnLS.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { chmod, mkdir, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
8
8
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
|
-
import {
|
|
11
|
+
import { LlmAdapter, ReasoningEffortId, ToolCallId, createToolResultMessage } from "@deepseek-ai/dsh-llm";
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
13
13
|
import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -2067,7 +2067,7 @@ var ClaudeSupervisor = class {
|
|
|
2067
2067
|
await active.agent.session.append("tool/call", {
|
|
2068
2068
|
turn: active.cursor.turn,
|
|
2069
2069
|
step: active.cursor.step,
|
|
2070
|
-
callId:
|
|
2070
|
+
callId: ToolCallId(message.toolUseId),
|
|
2071
2071
|
name: message.toolName,
|
|
2072
2072
|
arguments: safeDetail(message.input) ?? "{}"
|
|
2073
2073
|
});
|
|
@@ -2080,7 +2080,7 @@ var ClaudeSupervisor = class {
|
|
|
2080
2080
|
turn: active.cursor.turn,
|
|
2081
2081
|
step: active.cursor.step,
|
|
2082
2082
|
message: createToolResultMessage({
|
|
2083
|
-
callId:
|
|
2083
|
+
callId: ToolCallId(message.toolUseId),
|
|
2084
2084
|
content: [{
|
|
2085
2085
|
type: "text",
|
|
2086
2086
|
text
|
|
@@ -3977,6 +3977,90 @@ var RepositoryStatusService = class {
|
|
|
3977
3977
|
}
|
|
3978
3978
|
};
|
|
3979
3979
|
//#endregion
|
|
3980
|
+
//#region src/branch-name.ts
|
|
3981
|
+
/** Name a generated worktree branch after what the user is about to ask for.
|
|
3982
|
+
*
|
|
3983
|
+
* The composer draft is the only description of the work that exists before
|
|
3984
|
+
* the session starts, and it is usually not English and never branch-safe, so
|
|
3985
|
+
* a throwaway Haiku turn compresses it into a slug. Naming is a nicety: every
|
|
3986
|
+
* failure here returns `undefined` and the caller keeps its timestamped name. */
|
|
3987
|
+
/** Cheapest model that can translate and compress a sentence. */
|
|
3988
|
+
const BRANCH_SUMMARY_MODEL = "haiku";
|
|
3989
|
+
/** A branch name is not worth blocking worktree creation on for long. */
|
|
3990
|
+
const BRANCH_SUMMARY_TIMEOUT_MS = 15e3;
|
|
3991
|
+
const MAX_INTENT_CHARS = 2e3;
|
|
3992
|
+
/** A compliant reply is one short fragment; anything longer is prose. */
|
|
3993
|
+
const MAX_REPLY_CHARS$2 = 80;
|
|
3994
|
+
/** More words than this is a sentence, not the fragment we asked for. */
|
|
3995
|
+
const MAX_SLUG_WORDS = 6;
|
|
3996
|
+
const MAX_SLUG_CHARS = 48;
|
|
3997
|
+
function branchSummaryPrompt(intent) {
|
|
3998
|
+
return [
|
|
3999
|
+
"Summarize this software task as a Git branch name fragment.",
|
|
4000
|
+
"Reply with 2-5 lowercase English words joined by hyphens and nothing else:",
|
|
4001
|
+
"no quotes, no slashes, no prefix, no punctuation, no explanation.",
|
|
4002
|
+
"Translate the task to English if it is written in another language.",
|
|
4003
|
+
"",
|
|
4004
|
+
"Task:",
|
|
4005
|
+
`"""\n${intent.replaceAll("\"\"\"", "\" \" \"")}\n"""`
|
|
4006
|
+
].join("\n");
|
|
4007
|
+
}
|
|
4008
|
+
/** Branch-safe slug for a model reply, or `undefined` when the reply is not
|
|
4009
|
+
* the fragment we asked for. Mangling a refusal or a paragraph into a slug
|
|
4010
|
+
* would produce a worse name than the timestamped fallback. */
|
|
4011
|
+
function branchSlug(reply) {
|
|
4012
|
+
const line = reply.trim();
|
|
4013
|
+
if (line.length === 0 || line.length > MAX_REPLY_CHARS$2 || /[\r\n]/u.test(line)) return void 0;
|
|
4014
|
+
const words = line.toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/gu, "-").split("-").filter((word) => word.length > 0);
|
|
4015
|
+
if (words.length === 0 || words.length > MAX_SLUG_WORDS) return void 0;
|
|
4016
|
+
const slug = words.join("-").slice(0, MAX_SLUG_CHARS).replace(/-+$/u, "");
|
|
4017
|
+
return slug.length === 0 ? void 0 : slug;
|
|
4018
|
+
}
|
|
4019
|
+
/** First free name in `<candidate>`, `<candidate>-2`, `<candidate>-3`, … */
|
|
4020
|
+
function uniqueBranchName(candidate, taken) {
|
|
4021
|
+
const existing = new Set(taken);
|
|
4022
|
+
if (!existing.has(candidate)) return candidate;
|
|
4023
|
+
for (let index = 2; index < 100; index += 1) {
|
|
4024
|
+
const name = `${candidate}-${index}`;
|
|
4025
|
+
if (!existing.has(name)) return name;
|
|
4026
|
+
}
|
|
4027
|
+
return candidate;
|
|
4028
|
+
}
|
|
4029
|
+
/** Compress a composer draft into a branch slug with a throwaway Claude turn.
|
|
4030
|
+
*
|
|
4031
|
+
* Deliberately NOT routed through the supervisor, for the same reasons as the
|
|
4032
|
+
* plan-usage probe: there is no session to borrow yet. The turn is isolated
|
|
4033
|
+
* from filesystem settings as well, because a CLAUDE.md instruction ("always
|
|
4034
|
+
* reply in the user's language") turns the answer into an unusable slug. */
|
|
4035
|
+
async function summarizeBranchSlug(executablePath, intent, factory = query) {
|
|
4036
|
+
const task = intent.trim().slice(0, MAX_INTENT_CHARS);
|
|
4037
|
+
if (task.length === 0) return void 0;
|
|
4038
|
+
const lifetime = new AbortController();
|
|
4039
|
+
const timer = setTimeout(() => lifetime.abort(), BRANCH_SUMMARY_TIMEOUT_MS);
|
|
4040
|
+
timer.unref?.();
|
|
4041
|
+
try {
|
|
4042
|
+
const query = factory({
|
|
4043
|
+
prompt: branchSummaryPrompt(task),
|
|
4044
|
+
options: {
|
|
4045
|
+
cwd: process.cwd(),
|
|
4046
|
+
abortController: lifetime,
|
|
4047
|
+
model: BRANCH_SUMMARY_MODEL,
|
|
4048
|
+
allowedTools: [],
|
|
4049
|
+
settingSources: [],
|
|
4050
|
+
maxTurns: 1,
|
|
4051
|
+
...executablePath.length === 0 ? {} : { pathToClaudeCodeExecutable: executablePath }
|
|
4052
|
+
}
|
|
4053
|
+
});
|
|
4054
|
+
for await (const message of query) if (message.type === "result" && message.subtype === "success") return branchSlug(message.result);
|
|
4055
|
+
return;
|
|
4056
|
+
} catch {
|
|
4057
|
+
return;
|
|
4058
|
+
} finally {
|
|
4059
|
+
clearTimeout(timer);
|
|
4060
|
+
lifetime.abort();
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
//#endregion
|
|
3980
4064
|
//#region src/repository-setup.ts
|
|
3981
4065
|
const MAX_OUTPUT_BYTES$3 = 131072;
|
|
3982
4066
|
const GIT_TIMEOUT_MS$2 = 1e4;
|
|
@@ -4054,6 +4138,7 @@ var RepositorySetupService = class {
|
|
|
4054
4138
|
#leasePath;
|
|
4055
4139
|
#worktreeRoot;
|
|
4056
4140
|
#branchPrefix;
|
|
4141
|
+
#summarizeBranch;
|
|
4057
4142
|
#cleanupGraceMs;
|
|
4058
4143
|
#gitPath;
|
|
4059
4144
|
#pending = Promise.resolve();
|
|
@@ -4062,6 +4147,7 @@ var RepositorySetupService = class {
|
|
|
4062
4147
|
this.#leasePath = options.leasePath ?? dshHomePath("plugins", "dsh-claude", "worktrees.json");
|
|
4063
4148
|
this.#worktreeRoot = options.worktreeRoot ?? dshHomePath("plugins", "dsh-claude", "worktrees");
|
|
4064
4149
|
this.#branchPrefix = options.branchPrefix ?? (async () => "claude");
|
|
4150
|
+
this.#summarizeBranch = options.summarizeBranch ?? (async () => void 0);
|
|
4065
4151
|
this.#cleanupGraceMs = options.cleanupGraceMs ?? CLEANUP_GRACE_MS;
|
|
4066
4152
|
}
|
|
4067
4153
|
async listBranches(cwd) {
|
|
@@ -4108,7 +4194,7 @@ var RepositorySetupService = class {
|
|
|
4108
4194
|
remoteBranches
|
|
4109
4195
|
};
|
|
4110
4196
|
}
|
|
4111
|
-
async setup(cwd, branchValue, useWorktree, explicitBranchName, progress = () => {}) {
|
|
4197
|
+
async setup(cwd, branchValue, useWorktree, explicitBranchName, progress = () => {}, intent) {
|
|
4112
4198
|
progress("inspecting");
|
|
4113
4199
|
const branch = safeBranch(branchValue);
|
|
4114
4200
|
const info = await this.listBranches(cwd);
|
|
@@ -4116,7 +4202,7 @@ var RepositorySetupService = class {
|
|
|
4116
4202
|
const remote = info.remoteBranches.includes(branch);
|
|
4117
4203
|
if (!local && !remote) throw new RepositorySetupError("branch-not-found", "The selected local or remote-tracking branch does not exist.");
|
|
4118
4204
|
const requestedBranch = explicitBranchName === void 0 ? void 0 : safeBranch(explicitBranchName);
|
|
4119
|
-
if (useWorktree) return this.#createWorktree(info
|
|
4205
|
+
if (useWorktree) return this.#createWorktree(info, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, requestedBranch !== void 0 && info.branches.includes(requestedBranch), progress, intent);
|
|
4120
4206
|
progress("switching-branch");
|
|
4121
4207
|
return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
|
|
4122
4208
|
}
|
|
@@ -4199,11 +4285,17 @@ var RepositorySetupService = class {
|
|
|
4199
4285
|
});
|
|
4200
4286
|
}
|
|
4201
4287
|
/** Reconcile leases against the set of directories still referenced by a
|
|
4202
|
-
* workspace
|
|
4203
|
-
*
|
|
4204
|
-
*
|
|
4205
|
-
*
|
|
4206
|
-
|
|
4288
|
+
* workspace. Deleting a workspace is the user saying they are done with it,
|
|
4289
|
+
* so an unreferenced worktree goes even with uncommitted changes; its
|
|
4290
|
+
* sessions are archived first, through `archiveSessions`, or the Host
|
|
4291
|
+
* rebuilds the deleted workspace from their headers on the next boot.
|
|
4292
|
+
* Fresh leases are retained for a grace period so a worktree created
|
|
4293
|
+
* moments ago cannot be swept before its workspace registration lands.
|
|
4294
|
+
*
|
|
4295
|
+
* Every command runs from the repository root, never from the worktree
|
|
4296
|
+
* being removed: spawning in a directory that is already gone throws
|
|
4297
|
+
* ENOENT, which used to strand the lease forever. */
|
|
4298
|
+
cleanupOrphans(activePaths, archiveSessions) {
|
|
4207
4299
|
const active = new Set(activePaths.map(comparablePath));
|
|
4208
4300
|
return this.#serialize(async () => {
|
|
4209
4301
|
const leases = await this.#readLeases();
|
|
@@ -4218,32 +4310,19 @@ var RepositorySetupService = class {
|
|
|
4218
4310
|
continue;
|
|
4219
4311
|
}
|
|
4220
4312
|
try {
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4313
|
+
await archiveSessions?.(item.path).catch(() => void 0);
|
|
4314
|
+
if (await pathExists(item.path)) {
|
|
4315
|
+
if ((await this.#run(git, [
|
|
4316
|
+
"worktree",
|
|
4317
|
+
"remove",
|
|
4318
|
+
"--force",
|
|
4319
|
+
"--",
|
|
4320
|
+
item.path
|
|
4321
|
+
], item.root)).exitCode !== 0) {
|
|
4322
|
+
retained.push(item);
|
|
4323
|
+
continue;
|
|
4231
4324
|
}
|
|
4232
|
-
|
|
4233
|
-
}
|
|
4234
|
-
if (status.stdout.trim().length > 0) {
|
|
4235
|
-
retained.push(item);
|
|
4236
|
-
continue;
|
|
4237
|
-
}
|
|
4238
|
-
if ((await this.#run(git, [
|
|
4239
|
-
"worktree",
|
|
4240
|
-
"remove",
|
|
4241
|
-
"--",
|
|
4242
|
-
item.path
|
|
4243
|
-
], item.root)).exitCode !== 0) {
|
|
4244
|
-
retained.push(item);
|
|
4245
|
-
continue;
|
|
4246
|
-
}
|
|
4325
|
+
} else await this.#run(git, ["worktree", "prune"], item.root).catch(() => void 0);
|
|
4247
4326
|
if (item.pluginGeneratedBranch) await this.#run(git, [
|
|
4248
4327
|
"branch",
|
|
4249
4328
|
"-D",
|
|
@@ -4257,8 +4336,38 @@ var RepositorySetupService = class {
|
|
|
4257
4336
|
}
|
|
4258
4337
|
}
|
|
4259
4338
|
if (changed) await this.#writeLeases(retained);
|
|
4339
|
+
await this.#removeUnleasedDirectories(retained, active, now);
|
|
4260
4340
|
});
|
|
4261
4341
|
}
|
|
4342
|
+
/** Remove directories under the plugin's own worktree root that no lease and
|
|
4343
|
+
* no workspace claims. A lease file lost to a crash, or a worktree whose
|
|
4344
|
+
* lease write failed, otherwise leaves a directory nothing will ever sweep.
|
|
4345
|
+
* The grace period covers the gap between `worktree add` and the lease
|
|
4346
|
+
* write, so a worktree being created right now is never taken. */
|
|
4347
|
+
async #removeUnleasedDirectories(retained, active, now) {
|
|
4348
|
+
const leased = new Set(retained.map((item) => comparablePath(item.path)));
|
|
4349
|
+
let entries;
|
|
4350
|
+
try {
|
|
4351
|
+
entries = await readdir(this.#worktreeRoot);
|
|
4352
|
+
} catch {
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
for (const entry of entries) {
|
|
4356
|
+
const path = join(this.#worktreeRoot, entry);
|
|
4357
|
+
const key = comparablePath(path);
|
|
4358
|
+
if (leased.has(key) || active.has(key)) continue;
|
|
4359
|
+
try {
|
|
4360
|
+
const info = await stat(path);
|
|
4361
|
+
if (!info.isDirectory()) continue;
|
|
4362
|
+
const created = info.birthtimeMs > 0 ? info.birthtimeMs : info.mtimeMs;
|
|
4363
|
+
if (Math.max(0, now - created) < this.#cleanupGraceMs) continue;
|
|
4364
|
+
await rm(path, {
|
|
4365
|
+
recursive: true,
|
|
4366
|
+
force: true
|
|
4367
|
+
});
|
|
4368
|
+
} catch {}
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4262
4371
|
async #checkoutRemote(info, remoteBranch) {
|
|
4263
4372
|
const separator = remoteBranch.indexOf("/");
|
|
4264
4373
|
if (separator <= 0 || separator === remoteBranch.length - 1) throw new RepositorySetupError("invalid-branch", "The remote-tracking branch name is invalid.");
|
|
@@ -4325,13 +4434,14 @@ var RepositorySetupService = class {
|
|
|
4325
4434
|
branch
|
|
4326
4435
|
};
|
|
4327
4436
|
}
|
|
4328
|
-
async #createWorktree(
|
|
4437
|
+
async #createWorktree(info, baseBranch, baseRef, explicitBranchName, reuseExistingBranch, progress, intent) {
|
|
4438
|
+
const { root } = info;
|
|
4329
4439
|
const git = await this.#git();
|
|
4330
4440
|
progress("fetching");
|
|
4331
4441
|
await this.#fetchRemotes(git, root);
|
|
4332
4442
|
const suffix = randomUUID().slice(0, 8);
|
|
4333
4443
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
|
|
4334
|
-
const branch = explicitBranchName ??
|
|
4444
|
+
const branch = explicitBranchName ?? await this.#generatedBranch(info, baseBranch, intent, stamp, suffix, progress);
|
|
4335
4445
|
const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
|
|
4336
4446
|
progress("creating-worktree");
|
|
4337
4447
|
await mkdir(this.#worktreeRoot, { recursive: true });
|
|
@@ -4392,6 +4502,18 @@ var RepositorySetupService = class {
|
|
|
4392
4502
|
leaseId: item.id
|
|
4393
4503
|
};
|
|
4394
4504
|
}
|
|
4505
|
+
/** `<prefix>/<what the draft is about>`, falling back to
|
|
4506
|
+
* `<prefix>/<base branch>-<stamp>-<random>` whenever the summary is missing
|
|
4507
|
+
* or unusable. The prefix and the fallback shape are unchanged. */
|
|
4508
|
+
async #generatedBranch(info, baseBranch, intent, stamp, suffix, progress) {
|
|
4509
|
+
const prefix = safeBranch(await this.#branchPrefix());
|
|
4510
|
+
if (intent !== void 0 && intent.trim().length > 0) {
|
|
4511
|
+
progress("summarizing");
|
|
4512
|
+
const summary = await this.#summarizeBranch(intent).catch(() => void 0);
|
|
4513
|
+
if (summary !== void 0) return uniqueBranchName(`${prefix}/${summary}`, info.branches);
|
|
4514
|
+
}
|
|
4515
|
+
return `${prefix}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
|
|
4516
|
+
}
|
|
4395
4517
|
async #repositoryRoot(git, cwd) {
|
|
4396
4518
|
const result = await this.#run(git, [
|
|
4397
4519
|
"rev-parse",
|
|
@@ -4964,7 +5086,7 @@ async function streamSetup(res, service, input) {
|
|
|
4964
5086
|
type: "progress",
|
|
4965
5087
|
stage
|
|
4966
5088
|
});
|
|
4967
|
-
})
|
|
5089
|
+
}, optionalString$1(input, "intent"))
|
|
4968
5090
|
});
|
|
4969
5091
|
} catch (error) {
|
|
4970
5092
|
const setupError = error instanceof RepositorySetupError ? error : void 0;
|
|
@@ -7650,7 +7772,10 @@ async function apply(ctx, config) {
|
|
|
7650
7772
|
await applySettingsOverrides();
|
|
7651
7773
|
const sidecar = new ClaudeSidecarRepository();
|
|
7652
7774
|
const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
|
|
7653
|
-
const repositorySetup = new RepositorySetupService(ctx.subprocess, {
|
|
7775
|
+
const repositorySetup = new RepositorySetupService(ctx.subprocess, {
|
|
7776
|
+
branchPrefix: () => readWorktreeBranchPrefix(),
|
|
7777
|
+
summarizeBranch: (intent) => summarizeBranchSlug(supervisorConfig.executablePath, intent)
|
|
7778
|
+
});
|
|
7654
7779
|
const reviewComments = new ReviewCommentStore();
|
|
7655
7780
|
const commandCatalogs = /* @__PURE__ */ new Map();
|
|
7656
7781
|
const supervisor = new ClaudeSupervisor({
|
|
@@ -7730,10 +7855,20 @@ async function apply(ctx, config) {
|
|
|
7730
7855
|
});
|
|
7731
7856
|
const injectWorkspaceRegistry = ctx.inject;
|
|
7732
7857
|
injectWorkspaceRegistry(["workspaceRegistry"], (sweepCtx) => {
|
|
7858
|
+
const archiveSessions = async (worktreePath) => {
|
|
7859
|
+
const persistence = sweepCtx.get("sessionPersistence");
|
|
7860
|
+
if (persistence === void 0) return;
|
|
7861
|
+
const target = comparablePath(worktreePath);
|
|
7862
|
+
for (const header of await persistence.list()) {
|
|
7863
|
+
if (typeof header.id !== "string" || typeof header.cwd !== "string") continue;
|
|
7864
|
+
if (comparablePath(header.cwd) !== target) continue;
|
|
7865
|
+
await sweepCtx.workspaceRegistry.archiveSession(header.id).catch(() => void 0);
|
|
7866
|
+
}
|
|
7867
|
+
};
|
|
7733
7868
|
const sweep = () => {
|
|
7734
7869
|
try {
|
|
7735
7870
|
const paths = sweepCtx.workspaceRegistry.list().map((workspace) => workspace.path);
|
|
7736
|
-
repositorySetup.cleanupOrphans(paths).catch(() => void 0);
|
|
7871
|
+
repositorySetup.cleanupOrphans(paths, archiveSessions).catch(() => void 0);
|
|
7737
7872
|
} catch {}
|
|
7738
7873
|
};
|
|
7739
7874
|
sweepCtx.effect(() => {
|