@norman-else/dsh-claude 0.1.37 → 0.1.39
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.js +53 -5
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +129 -65
- 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,
|
|
@@ -3352,16 +3391,23 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3352
3391
|
const info = (message) => {
|
|
3353
3392
|
ctx.logger?.info?.(message);
|
|
3354
3393
|
};
|
|
3355
|
-
|
|
3394
|
+
/** Everything the host already knows, without touching the repository. */
|
|
3395
|
+
const localMeta = (sessionId) => {
|
|
3356
3396
|
const owned = ownsSession(sessionId);
|
|
3357
|
-
const repository = owned ? await repositoryForSession(sessionId) : void 0;
|
|
3358
3397
|
return {
|
|
3359
3398
|
owned,
|
|
3360
3399
|
commands: commandsForSession(sessionId),
|
|
3361
|
-
...repository === void 0 ? {} : { repository },
|
|
3362
3400
|
reviewComments: owned ? reviewCommentsForSession(sessionId) : []
|
|
3363
3401
|
};
|
|
3364
3402
|
};
|
|
3403
|
+
const assembleMeta = async (sessionId) => {
|
|
3404
|
+
const meta = localMeta(sessionId);
|
|
3405
|
+
const repository = meta.owned ? await repositoryForSession(sessionId) : void 0;
|
|
3406
|
+
return {
|
|
3407
|
+
...meta,
|
|
3408
|
+
...repository === void 0 ? {} : { repository }
|
|
3409
|
+
};
|
|
3410
|
+
};
|
|
3365
3411
|
const streamMulti = async (res, io, sessionIds) => {
|
|
3366
3412
|
info(`dsh-claude: projection stream opened for ${sessionIds.length} session(s)`);
|
|
3367
3413
|
let textDeltas = 0;
|
|
@@ -3383,8 +3429,20 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3383
3429
|
}
|
|
3384
3430
|
};
|
|
3385
3431
|
const metas = /* @__PURE__ */ new Map();
|
|
3432
|
+
const writeMeta = (sessionId, meta) => {
|
|
3433
|
+
metas.set(sessionId, meta);
|
|
3434
|
+
writeLine({
|
|
3435
|
+
type: "meta",
|
|
3436
|
+
session: sessionId,
|
|
3437
|
+
owned: meta.owned,
|
|
3438
|
+
commands: meta.commands,
|
|
3439
|
+
...meta.repository === void 0 ? {} : { repository: meta.repository },
|
|
3440
|
+
reviewComments: meta.reviewComments
|
|
3441
|
+
});
|
|
3442
|
+
};
|
|
3386
3443
|
const writeSnapshot = async (sessionId) => {
|
|
3387
|
-
const
|
|
3444
|
+
const projection = await sidecar.read(sessionId);
|
|
3445
|
+
const meta = metas.get(sessionId) ?? localMeta(sessionId);
|
|
3388
3446
|
metas.set(sessionId, meta);
|
|
3389
3447
|
writeLine({
|
|
3390
3448
|
type: "snapshot",
|
|
@@ -3392,6 +3450,9 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3392
3450
|
seq: sidecar.sequence(sessionId),
|
|
3393
3451
|
...envelope(projection, meta)
|
|
3394
3452
|
});
|
|
3453
|
+
const probed = await assembleMeta(sessionId);
|
|
3454
|
+
if (closed || JSON.stringify(probed) === JSON.stringify(metas.get(sessionId))) return;
|
|
3455
|
+
writeMeta(sessionId, probed);
|
|
3395
3456
|
};
|
|
3396
3457
|
const unsubscribes = sessionIds.map((sessionId) => sidecar.subscribe(sessionId, (delta) => {
|
|
3397
3458
|
switch (delta.kind) {
|
|
@@ -3458,15 +3519,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3458
3519
|
const next = await assembleMeta(sessionId);
|
|
3459
3520
|
if (closed) return;
|
|
3460
3521
|
if (JSON.stringify(next) === JSON.stringify(metas.get(sessionId))) continue;
|
|
3461
|
-
|
|
3462
|
-
writeLine({
|
|
3463
|
-
type: "meta",
|
|
3464
|
-
session: sessionId,
|
|
3465
|
-
owned: next.owned,
|
|
3466
|
-
commands: next.commands,
|
|
3467
|
-
...next.repository === void 0 ? {} : { repository: next.repository },
|
|
3468
|
-
reviewComments: next.reviewComments
|
|
3469
|
-
});
|
|
3522
|
+
writeMeta(sessionId, next);
|
|
3470
3523
|
}
|
|
3471
3524
|
writeLine({ type: "ping" });
|
|
3472
3525
|
})().catch(() => void 0);
|
|
@@ -4336,7 +4389,7 @@ var RepositorySetupService = class {
|
|
|
4336
4389
|
}
|
|
4337
4390
|
}
|
|
4338
4391
|
if (changed) await this.#writeLeases(retained);
|
|
4339
|
-
await this.#removeUnleasedDirectories(retained, active, now);
|
|
4392
|
+
await this.#removeUnleasedDirectories(retained, active, now, archiveSessions);
|
|
4340
4393
|
});
|
|
4341
4394
|
}
|
|
4342
4395
|
/** Remove directories under the plugin's own worktree root that no lease and
|
|
@@ -4344,7 +4397,7 @@ var RepositorySetupService = class {
|
|
|
4344
4397
|
* lease write failed, otherwise leaves a directory nothing will ever sweep.
|
|
4345
4398
|
* The grace period covers the gap between `worktree add` and the lease
|
|
4346
4399
|
* write, so a worktree being created right now is never taken. */
|
|
4347
|
-
async #removeUnleasedDirectories(retained, active, now) {
|
|
4400
|
+
async #removeUnleasedDirectories(retained, active, now, archiveSessions) {
|
|
4348
4401
|
const leased = new Set(retained.map((item) => comparablePath(item.path)));
|
|
4349
4402
|
let entries;
|
|
4350
4403
|
try {
|
|
@@ -4361,6 +4414,7 @@ var RepositorySetupService = class {
|
|
|
4361
4414
|
if (!info.isDirectory()) continue;
|
|
4362
4415
|
const created = info.birthtimeMs > 0 ? info.birthtimeMs : info.mtimeMs;
|
|
4363
4416
|
if (Math.max(0, now - created) < this.#cleanupGraceMs) continue;
|
|
4417
|
+
await archiveSessions?.(path).catch(() => void 0);
|
|
4364
4418
|
await rm(path, {
|
|
4365
4419
|
recursive: true,
|
|
4366
4420
|
force: true
|
|
@@ -5105,7 +5159,7 @@ async function streamSetup(res, service, input) {
|
|
|
5105
5159
|
* The prefix is registered as a stream because the setup POST holds its
|
|
5106
5160
|
* connection open for the whole worktree build; the short sibling paths ride
|
|
5107
5161
|
* the same registration and answer with `json` before releasing it. */
|
|
5108
|
-
function registerRepositorySetupRoute(ctx, service) {
|
|
5162
|
+
function registerRepositorySetupRoute(ctx, service, sweep) {
|
|
5109
5163
|
registerPluginRoute(ctx, {
|
|
5110
5164
|
mode: "stream",
|
|
5111
5165
|
kind: "prefix",
|
|
@@ -5139,6 +5193,11 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
5139
5193
|
const input = await readJson$5(io);
|
|
5140
5194
|
return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
|
|
5141
5195
|
}
|
|
5196
|
+
if (pathname === `/plugins/dsh-claude/repository/setup/sweep`) {
|
|
5197
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5198
|
+
sweep?.();
|
|
5199
|
+
return json(res, 200, { ok: true });
|
|
5200
|
+
}
|
|
5142
5201
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
5143
5202
|
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5144
5203
|
const input = await readJson$5(io);
|
|
@@ -7853,6 +7912,7 @@ async function apply(ctx, config) {
|
|
|
7853
7912
|
reviewComments.disposeSession(agent.id);
|
|
7854
7913
|
await supervisor.disposeSession(agent.id);
|
|
7855
7914
|
});
|
|
7915
|
+
let sweepWorktrees;
|
|
7856
7916
|
const injectWorkspaceRegistry = ctx.inject;
|
|
7857
7917
|
injectWorkspaceRegistry(["workspaceRegistry"], (sweepCtx) => {
|
|
7858
7918
|
const archiveSessions = async (worktreePath) => {
|
|
@@ -7873,9 +7933,13 @@ async function apply(ctx, config) {
|
|
|
7873
7933
|
};
|
|
7874
7934
|
sweepCtx.effect(() => {
|
|
7875
7935
|
sweep();
|
|
7936
|
+
sweepWorktrees = sweep;
|
|
7876
7937
|
const timer = setInterval(sweep, 6e4);
|
|
7877
7938
|
timer.unref?.();
|
|
7878
|
-
return () =>
|
|
7939
|
+
return () => {
|
|
7940
|
+
sweepWorktrees = void 0;
|
|
7941
|
+
clearInterval(timer);
|
|
7942
|
+
};
|
|
7879
7943
|
}, "dsh-claude: worktree reconciliation");
|
|
7880
7944
|
});
|
|
7881
7945
|
ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
|
|
@@ -7890,7 +7954,7 @@ async function apply(ctx, config) {
|
|
|
7890
7954
|
defaultLimits,
|
|
7891
7955
|
onUpdated: applySettingsOverrides
|
|
7892
7956
|
});
|
|
7893
|
-
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
7957
|
+
registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.());
|
|
7894
7958
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|
|
7895
7959
|
registerRepositoryFileRoute(webCtx, repositoryStatus);
|
|
7896
7960
|
registerJiraRoute(webCtx, new JiraService());
|