@norman-else/dsh-claude 0.1.38 → 0.1.40
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/lib/client.d.ts +1 -0
- package/lib/client.js +56 -38
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +132 -49
- package/lib/index.mjs.map +1 -1
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -1180,6 +1180,80 @@ function normalizeSdkMessage(message) {
|
|
|
1180
1180
|
}];
|
|
1181
1181
|
}
|
|
1182
1182
|
//#endregion
|
|
1183
|
+
//#region src/model-catalog.ts
|
|
1184
|
+
/** What the selector shows before any session has initialized in this Host
|
|
1185
|
+
* process -- a fresh app launch lands here. `default` is the only id that is
|
|
1186
|
+
* valid on every release and plan; the aliases after it are the stable
|
|
1187
|
+
* `/model` spellings Claude Code has kept across releases, so the menu is
|
|
1188
|
+
* usable at first paint instead of a single row. The first initialize
|
|
1189
|
+
* response replaces the whole list with the CLI's own lineup. */
|
|
1190
|
+
const SEED = [
|
|
1191
|
+
{
|
|
1192
|
+
id: "default",
|
|
1193
|
+
name: "Default (recommended)",
|
|
1194
|
+
description: ""
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
id: "opus[1m]",
|
|
1198
|
+
name: "Opus (1M context)",
|
|
1199
|
+
description: "",
|
|
1200
|
+
contextWindow: 1e6
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
id: "fable",
|
|
1204
|
+
name: "Fable",
|
|
1205
|
+
description: ""
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
id: "sonnet",
|
|
1209
|
+
name: "Sonnet",
|
|
1210
|
+
description: ""
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
id: "haiku",
|
|
1214
|
+
name: "Haiku",
|
|
1215
|
+
description: ""
|
|
1216
|
+
}
|
|
1217
|
+
];
|
|
1218
|
+
/** A 1M-context route spells it in the id (`opus[1m]`, `claude-fable-5[1m]`),
|
|
1219
|
+
* so this needs no capacity table either. It is only a floor: the supervisor
|
|
1220
|
+
* overrides it with the window the CLI reports once a turn has run. */
|
|
1221
|
+
function declaredContextWindow(row) {
|
|
1222
|
+
return /\[1m\]$/u.test(row.resolvedModel ?? row.value) ? 1e6 : void 0;
|
|
1223
|
+
}
|
|
1224
|
+
function projectModel(row) {
|
|
1225
|
+
const contextWindow = declaredContextWindow(row);
|
|
1226
|
+
return {
|
|
1227
|
+
id: row.value,
|
|
1228
|
+
name: row.displayName,
|
|
1229
|
+
description: row.description,
|
|
1230
|
+
...contextWindow === void 0 ? {} : { contextWindow }
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
let latest$1;
|
|
1234
|
+
/**
|
|
1235
|
+
* Learn the lineup from one session's initialize response.
|
|
1236
|
+
* @param models - the CLI's own `/model` rows; an empty list is ignored so a
|
|
1237
|
+
* CLI that answers without a catalog cannot blank the selector.
|
|
1238
|
+
*/
|
|
1239
|
+
function recordClaudeModels(models) {
|
|
1240
|
+
if (models.length === 0) return;
|
|
1241
|
+
latest$1 = models.map(projectModel);
|
|
1242
|
+
}
|
|
1243
|
+
/** The lineup to advertise: whatever the CLI last reported, else the seed. */
|
|
1244
|
+
function latestClaudeModels() {
|
|
1245
|
+
return latest$1 ?? SEED;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Look one id up in the current lineup.
|
|
1249
|
+
* @param id - the id DSH persisted on the session, which may name a model the
|
|
1250
|
+
* running CLI no longer lists.
|
|
1251
|
+
* @returns the row, or undefined when the lineup does not cover the id.
|
|
1252
|
+
*/
|
|
1253
|
+
function claudeModelRow(id) {
|
|
1254
|
+
return latestClaudeModels().find((row) => row.id === id);
|
|
1255
|
+
}
|
|
1256
|
+
//#endregion
|
|
1183
1257
|
//#region src/plan-usage.ts
|
|
1184
1258
|
/** Claude.ai plan rate-limit windows behind the CLI's `/usage` panel.
|
|
1185
1259
|
*
|
|
@@ -1508,9 +1582,11 @@ var ClaudeSupervisor = class {
|
|
|
1508
1582
|
return this.#runMetadata(agent, model, (query) => query.supportedCommands());
|
|
1509
1583
|
}
|
|
1510
1584
|
async contextUsage(agent, model = this.#config.defaultModel) {
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1585
|
+
return this.#runMetadata(agent, model, async (query, entry) => {
|
|
1586
|
+
const usage = await query.getContextUsage();
|
|
1587
|
+
this.#recordContextWindow(entry.model, usage);
|
|
1588
|
+
return usage;
|
|
1589
|
+
});
|
|
1514
1590
|
}
|
|
1515
1591
|
/** Cache a window under both the selector id the caller asked for and the
|
|
1516
1592
|
* concrete model the CLI reports, so either name resolves it later. */
|
|
@@ -1581,19 +1657,13 @@ var ClaudeSupervisor = class {
|
|
|
1581
1657
|
entry.idleTimer = void 0;
|
|
1582
1658
|
}
|
|
1583
1659
|
const model = request.model ?? this.#config.defaultModel;
|
|
1584
|
-
if (request.thinkingMode !== entry.thinkingMode) {
|
|
1660
|
+
if (request.thinkingMode !== entry.thinkingMode || model !== entry.model) {
|
|
1585
1661
|
this.#entries.delete(sessionId);
|
|
1586
1662
|
await this.#disposeEntry(entry);
|
|
1587
1663
|
entry = await this.#createEntry(request.agent, model, request.thinkingMode);
|
|
1588
1664
|
this.#entries.set(sessionId, entry);
|
|
1589
1665
|
await entry.sdkInitialization;
|
|
1590
|
-
} else
|
|
1591
|
-
await this.#syncPermissionMode(entry);
|
|
1592
|
-
if (model !== entry.model) {
|
|
1593
|
-
await this.#control(entry, entry.query.setModel(model), "Claude Code model switch");
|
|
1594
|
-
entry.model = model;
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1666
|
+
} else await this.#syncPermissionMode(entry);
|
|
1597
1667
|
const promptUuid = randomUUID();
|
|
1598
1668
|
const cursor = currentClaudeActivityCursor(request.agent.session.events);
|
|
1599
1669
|
cursor.nextOrdinal = (await this.#sidecar.read(sessionId)).activities.reduce((next, activity) => activity.turn === cursor.turn && activity.step === cursor.step ? Math.max(next, activity.ordinal + 1) : next, 0);
|
|
@@ -1692,10 +1762,6 @@ var ClaudeSupervisor = class {
|
|
|
1692
1762
|
entry.idleTimer = void 0;
|
|
1693
1763
|
}
|
|
1694
1764
|
await this.#syncPermissionMode(entry);
|
|
1695
|
-
if (model !== entry.model) {
|
|
1696
|
-
await this.#control(entry, entry.query.setModel(model), "Claude Code model switch");
|
|
1697
|
-
entry.model = model;
|
|
1698
|
-
}
|
|
1699
1765
|
return entry;
|
|
1700
1766
|
}
|
|
1701
1767
|
/** Run one SDK control request against a live entry, and discard the entry if
|
|
@@ -1831,7 +1897,8 @@ var ClaudeSupervisor = class {
|
|
|
1831
1897
|
options
|
|
1832
1898
|
});
|
|
1833
1899
|
entry.pump = this.#runDetached(() => this.#pump(entry));
|
|
1834
|
-
entry.sdkInitialization = withTimeout(entry.query.initializationResult(), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude SDK initialization").then(() => {
|
|
1900
|
+
entry.sdkInitialization = withTimeout(entry.query.initializationResult(), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude SDK initialization").then((initialization) => {
|
|
1901
|
+
recordClaudeModels(initialization.models);
|
|
1835
1902
|
if (entry.state === "starting") entry.state = "idle";
|
|
1836
1903
|
});
|
|
1837
1904
|
entry.sdkInitialization.catch((error) => this.#handleDisconnect(entry, error));
|
|
@@ -2604,34 +2671,6 @@ function formatReviewComments(comments) {
|
|
|
2604
2671
|
}
|
|
2605
2672
|
//#endregion
|
|
2606
2673
|
//#region src/adapter.ts
|
|
2607
|
-
const MODELS = [
|
|
2608
|
-
{
|
|
2609
|
-
id: "default",
|
|
2610
|
-
name: "Default (recommended)",
|
|
2611
|
-
description: ""
|
|
2612
|
-
},
|
|
2613
|
-
{
|
|
2614
|
-
id: "opus[1m]",
|
|
2615
|
-
name: "Opus (1M context)",
|
|
2616
|
-
description: "",
|
|
2617
|
-
contextWindow: 1e6
|
|
2618
|
-
},
|
|
2619
|
-
{
|
|
2620
|
-
id: "fable",
|
|
2621
|
-
name: "Fable",
|
|
2622
|
-
description: ""
|
|
2623
|
-
},
|
|
2624
|
-
{
|
|
2625
|
-
id: "sonnet",
|
|
2626
|
-
name: "Sonnet",
|
|
2627
|
-
description: ""
|
|
2628
|
-
},
|
|
2629
|
-
{
|
|
2630
|
-
id: "haiku",
|
|
2631
|
-
name: "Haiku",
|
|
2632
|
-
description: ""
|
|
2633
|
-
}
|
|
2634
|
-
];
|
|
2635
2674
|
const THINKING_MODES = [
|
|
2636
2675
|
{
|
|
2637
2676
|
id: "off",
|
|
@@ -2818,7 +2857,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2818
2857
|
return NO_RETRY_POLICY;
|
|
2819
2858
|
}
|
|
2820
2859
|
async listModels(provider) {
|
|
2821
|
-
return
|
|
2860
|
+
return latestClaudeModels().map((model) => ({
|
|
2822
2861
|
provider,
|
|
2823
2862
|
id: model.id,
|
|
2824
2863
|
name: model.name,
|
|
@@ -2827,8 +2866,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2827
2866
|
}));
|
|
2828
2867
|
}
|
|
2829
2868
|
async resolveModel(provider, model, _signal) {
|
|
2830
|
-
const known =
|
|
2831
|
-
const contextWindow = this.#supervisor.contextWindow(model) ??
|
|
2869
|
+
const known = claudeModelRow(model);
|
|
2870
|
+
const contextWindow = this.#supervisor.contextWindow(model) ?? known?.contextWindow;
|
|
2832
2871
|
return {
|
|
2833
2872
|
provider,
|
|
2834
2873
|
id: model,
|
|
@@ -4125,6 +4164,22 @@ function parseWorktreeBranches(value) {
|
|
|
4125
4164
|
function slug(value, fallback) {
|
|
4126
4165
|
return value.toLocaleLowerCase("en-US").replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || fallback;
|
|
4127
4166
|
}
|
|
4167
|
+
/** A branch name as one path segment: `/` and anything else unsafe folds to
|
|
4168
|
+
* `-`, case is kept so a ticket key stays scannable (`PSOS-5683`, not
|
|
4169
|
+
* `psos-5683`). Flattened rather than nested on purpose -- see
|
|
4170
|
+
* {@link worktreeDirectoryName}. */
|
|
4171
|
+
function branchSegment(branch) {
|
|
4172
|
+
return branch.replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/^[-.]+/u, "").replace(/-+$/u, "").slice(0, 48).replace(/-+$/u, "") || "branch";
|
|
4173
|
+
}
|
|
4174
|
+
/** Name a worktree directory after the repository and the branch it holds, so
|
|
4175
|
+
* the workspace list reads without opening a session.
|
|
4176
|
+
*
|
|
4177
|
+
* One flat segment, never a `feature/x` subdirectory: the orphan sweep lists
|
|
4178
|
+
* this root one level deep, and a prefix directory holds no lease of its own,
|
|
4179
|
+
* so it would be removed along with every live worktree inside it. */
|
|
4180
|
+
function worktreeDirectoryName(root, branch) {
|
|
4181
|
+
return `${slug(basename(root), "repository")}-${branchSegment(branch)}`;
|
|
4182
|
+
}
|
|
4128
4183
|
/** Comparable form for path identity: resolved, forward slashes, case-folded
|
|
4129
4184
|
* so Windows drive-letter or case spelling differences cannot hide a match. */
|
|
4130
4185
|
function comparablePath(value) {
|
|
@@ -4424,6 +4479,18 @@ var RepositorySetupService = class {
|
|
|
4424
4479
|
], root, GIT_FETCH_TIMEOUT_MS);
|
|
4425
4480
|
if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references.");
|
|
4426
4481
|
}
|
|
4482
|
+
/** The remote-tracking ref a local base branch follows, when that ref still
|
|
4483
|
+
* exists. Falls back to the local branch for a branch with no upstream. */
|
|
4484
|
+
async #upstreamRef(git, info, branch) {
|
|
4485
|
+
const upstream = await this.#run(git, [
|
|
4486
|
+
"for-each-ref",
|
|
4487
|
+
"--format=%(upstream:short)",
|
|
4488
|
+
`refs/heads/${branch}`
|
|
4489
|
+
], info.root);
|
|
4490
|
+
if (upstream.exitCode !== 0 || upstream.lossy) return void 0;
|
|
4491
|
+
const name = upstream.stdout.trim();
|
|
4492
|
+
return name.length > 0 && info.remoteBranches.includes(name) ? `refs/remotes/${name}` : void 0;
|
|
4493
|
+
}
|
|
4427
4494
|
async #checkout(info, branch) {
|
|
4428
4495
|
if (info.current !== branch) {
|
|
4429
4496
|
if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
|
|
@@ -4454,10 +4521,11 @@ var RepositorySetupService = class {
|
|
|
4454
4521
|
const git = await this.#git();
|
|
4455
4522
|
progress("fetching");
|
|
4456
4523
|
await this.#fetchRemotes(git, root);
|
|
4524
|
+
const startRef = reuseExistingBranch ? baseRef : await this.#upstreamRef(git, info, baseBranch) ?? baseRef;
|
|
4457
4525
|
const suffix = randomUUID().slice(0, 8);
|
|
4458
4526
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
|
|
4459
4527
|
const branch = explicitBranchName ?? await this.#generatedBranch(info, baseBranch, intent, stamp, suffix, progress);
|
|
4460
|
-
const path = join(this.#worktreeRoot,
|
|
4528
|
+
const path = join(this.#worktreeRoot, await this.#freeDirectoryName(root, branch));
|
|
4461
4529
|
progress("creating-worktree");
|
|
4462
4530
|
await mkdir(this.#worktreeRoot, { recursive: true });
|
|
4463
4531
|
if (reuseExistingBranch) await this.#run(git, ["worktree", "prune"], root).catch(() => void 0);
|
|
@@ -4473,7 +4541,7 @@ var RepositorySetupService = class {
|
|
|
4473
4541
|
"-b",
|
|
4474
4542
|
branch,
|
|
4475
4543
|
path,
|
|
4476
|
-
|
|
4544
|
+
startRef
|
|
4477
4545
|
], root);
|
|
4478
4546
|
if (created.exitCode !== 0) {
|
|
4479
4547
|
const detail = created.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
|
|
@@ -4520,6 +4588,21 @@ var RepositorySetupService = class {
|
|
|
4520
4588
|
/** `<prefix>/<what the draft is about>`, falling back to
|
|
4521
4589
|
* `<prefix>/<base branch>-<stamp>-<random>` whenever the summary is missing
|
|
4522
4590
|
* or unusable. The prefix and the fallback shape are unchanged. */
|
|
4591
|
+
/** The branch-named directory, or the first `-2`, `-3`, ... spelling free of
|
|
4592
|
+
* anything already on disk. Names no longer carry a timestamp, so a stale
|
|
4593
|
+
* directory a crash left behind -- or two branches that fold to the same
|
|
4594
|
+
* segment -- would otherwise fail `worktree add` outright. Compared
|
|
4595
|
+
* case-folded, since a case-insensitive filesystem would collide anyway. */
|
|
4596
|
+
async #freeDirectoryName(root, branch) {
|
|
4597
|
+
const base = worktreeDirectoryName(root, branch);
|
|
4598
|
+
const taken = await readdir(this.#worktreeRoot).then((entries) => new Set(entries.map((entry) => entry.toLocaleLowerCase("en-US"))), () => /* @__PURE__ */ new Set());
|
|
4599
|
+
if (!taken.has(base.toLocaleLowerCase("en-US"))) return base;
|
|
4600
|
+
for (let index = 2; index < 100; index += 1) {
|
|
4601
|
+
const candidate = `${base}-${index}`;
|
|
4602
|
+
if (!taken.has(candidate.toLocaleLowerCase("en-US"))) return candidate;
|
|
4603
|
+
}
|
|
4604
|
+
return base;
|
|
4605
|
+
}
|
|
4523
4606
|
async #generatedBranch(info, baseBranch, intent, stamp, suffix, progress) {
|
|
4524
4607
|
const prefix = safeBranch(await this.#branchPrefix());
|
|
4525
4608
|
if (intent !== void 0 && intent.trim().length > 0) {
|