@duckmind/dm-darwin-arm64 0.13.2 → 0.13.5
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/CHANGELOG.md +49 -2
- package/README.md +5 -5
- package/dm +0 -0
- package/docs/settings.md +1 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/extensions/.dm-extensions.json +15 -15
- package/extensions/dm-multicodex/README.md +3 -1
- package/extensions/dm-multicodex/account-manager.test.ts +3 -1
- package/extensions/dm-multicodex/account-manager.ts +27 -1
- package/extensions/dm-multicodex/commands.test.ts +1 -0
- package/extensions/dm-multicodex/commands.ts +81 -1
- package/extensions/dm-multicodex/index.ts +7 -0
- package/extensions/dm-multicodex/node_modules/.package-lock.json +28 -28
- package/extensions/dm-multicodex/package-lock.json +11145 -9633
- package/extensions/dm-multicodex/package.json +56 -56
- package/extensions/dm-multicodex/storage.ts +2 -2
- package/extensions/dm-multicodex/sync.test.ts +114 -0
- package/extensions/dm-multicodex/sync.ts +249 -0
- package/extensions/dm-multicodex/tsconfig.json +17 -0
- package/extensions/dm-subagents/async-execution.ts +2 -0
- package/extensions/dm-subagents/execution.ts +1 -1
- package/extensions/dm-subagents/intercom-bridge.ts +8 -0
- package/extensions/dm-subagents/package.json +1 -1
- package/extensions/dm-subagents/skills.ts +117 -25
- package/extensions/dm-subagents/subagent-executor.ts +2 -6
- package/extensions/dm-subagents/subagent-runner.ts +10 -2
- package/extensions/dm-subagents/worktree.ts +27 -9
- package/extensions/dm-thinking-timer/README.md +1 -1
- package/extensions/dm-ultrathink/src/naming.ts +151 -10
- package/package.json +1 -1
- package/theme/duckmind.json +77 -65
|
@@ -6,7 +6,6 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { loadSkills, type Skill } from "@mariozechner/pi-coding-agent";
|
|
10
9
|
|
|
11
10
|
export type SkillSource =
|
|
12
11
|
| "project"
|
|
@@ -86,6 +85,7 @@ function getPackageSkillPaths(packageRoot: string): string[] {
|
|
|
86
85
|
.filter((s: unknown) => typeof s === "string")
|
|
87
86
|
.map((s: string) => path.resolve(packageRoot, s));
|
|
88
87
|
} catch {
|
|
88
|
+
// Package scanning is opportunistic; ignore malformed/missing package metadata.
|
|
89
89
|
return [];
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -98,6 +98,7 @@ function getGlobalNpmRoot(): string | null {
|
|
|
98
98
|
cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
|
|
99
99
|
return cachedGlobalNpmRoot;
|
|
100
100
|
} catch {
|
|
101
|
+
// Global npm root is optional in constrained environments.
|
|
101
102
|
cachedGlobalNpmRoot = ""; // Empty string means "tried but failed"
|
|
102
103
|
return null;
|
|
103
104
|
}
|
|
@@ -123,6 +124,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
|
|
|
123
124
|
try {
|
|
124
125
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
126
|
} catch {
|
|
127
|
+
// Ignore unreadable package roots and continue scanning other roots.
|
|
126
128
|
continue;
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -136,6 +138,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
|
|
|
136
138
|
try {
|
|
137
139
|
scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
138
140
|
} catch {
|
|
141
|
+
// Ignore unreadable scoped package directories and continue.
|
|
139
142
|
continue;
|
|
140
143
|
}
|
|
141
144
|
for (const scopeEntry of scopeEntries) {
|
|
@@ -178,7 +181,9 @@ function collectSettingsSkillPaths(cwd: string): string[] {
|
|
|
178
181
|
}
|
|
179
182
|
results.push(resolved);
|
|
180
183
|
}
|
|
181
|
-
} catch {
|
|
184
|
+
} catch {
|
|
185
|
+
// Settings-provided skills are optional; ignore malformed or missing settings files.
|
|
186
|
+
}
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
return results;
|
|
@@ -196,21 +201,22 @@ function buildSkillPaths(cwd: string): string[] {
|
|
|
196
201
|
return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
|
|
197
202
|
}
|
|
198
203
|
|
|
199
|
-
function inferSkillSource(
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
function inferSkillSource(filePath: string, cwd: string): SkillSource {
|
|
205
|
+
const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
|
|
206
|
+
const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
|
|
207
|
+
const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
|
|
208
|
+
const projectAgentsRoot = path.resolve(cwd, ".agents");
|
|
209
|
+
const userSkillsRoot = path.resolve(AGENT_DIR, "skills");
|
|
210
|
+
const userPackagesRoot = path.resolve(AGENT_DIR, "npm", "node_modules");
|
|
211
|
+
const userAgentsRoot = path.resolve(os.homedir(), ".agents");
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const altProjectRoot = path.resolve(cwd, ".agents");
|
|
209
|
-
const isProjectScoped = isWithinPath(filePath, projectRoot) || isWithinPath(filePath, altProjectRoot);
|
|
210
|
-
if (isProjectScoped) return "project";
|
|
213
|
+
if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
|
|
214
|
+
if (isWithinPath(filePath, projectSkillsRoot) || isWithinPath(filePath, projectAgentsRoot)) return "project";
|
|
215
|
+
if (isWithinPath(filePath, projectConfigRoot)) return "project-settings";
|
|
211
216
|
|
|
212
|
-
|
|
213
|
-
if (
|
|
217
|
+
if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
|
|
218
|
+
if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
|
|
219
|
+
if (isWithinPath(filePath, AGENT_DIR)) return "user-settings";
|
|
214
220
|
|
|
215
221
|
const globalRoot = getGlobalNpmRoot();
|
|
216
222
|
if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
|
|
@@ -227,6 +233,99 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
|
|
|
227
233
|
return candidate.order < existing.order ? candidate : existing;
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
function maybeReadSkillDescription(filePath: string): string | undefined {
|
|
237
|
+
try {
|
|
238
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
239
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
240
|
+
if (!normalized.startsWith("---")) return undefined;
|
|
241
|
+
|
|
242
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
243
|
+
if (endIndex === -1) return undefined;
|
|
244
|
+
|
|
245
|
+
const frontmatter = normalized.slice(3, endIndex).trim();
|
|
246
|
+
const match = frontmatter.match(/^description:\s*(.+)$/m);
|
|
247
|
+
if (!match) return undefined;
|
|
248
|
+
return match[1]?.trim().replace(/^['\"]|['\"]$/g, "");
|
|
249
|
+
} catch {
|
|
250
|
+
// Description parsing is best-effort metadata extraction.
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function collectFilesystemSkills(cwd: string, skillPaths: string[]): CachedSkillEntry[] {
|
|
256
|
+
const entries: CachedSkillEntry[] = [];
|
|
257
|
+
const seen = new Set<string>();
|
|
258
|
+
let order = 0;
|
|
259
|
+
|
|
260
|
+
const pushEntry = (name: string, filePath: string) => {
|
|
261
|
+
const resolvedFile = path.resolve(filePath);
|
|
262
|
+
if (seen.has(resolvedFile)) return;
|
|
263
|
+
if (!fs.existsSync(resolvedFile)) return;
|
|
264
|
+
seen.add(resolvedFile);
|
|
265
|
+
entries.push({
|
|
266
|
+
name,
|
|
267
|
+
filePath: resolvedFile,
|
|
268
|
+
source: inferSkillSource(resolvedFile, cwd),
|
|
269
|
+
description: maybeReadSkillDescription(resolvedFile),
|
|
270
|
+
order: order++,
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
for (const skillPath of skillPaths) {
|
|
275
|
+
if (!fs.existsSync(skillPath)) continue;
|
|
276
|
+
|
|
277
|
+
let stat: fs.Stats;
|
|
278
|
+
try {
|
|
279
|
+
stat = fs.statSync(skillPath);
|
|
280
|
+
} catch {
|
|
281
|
+
// Ignore paths that disappear or become unreadable during discovery.
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (stat.isFile()) {
|
|
286
|
+
const fileName = path.basename(skillPath);
|
|
287
|
+
if (!fileName.toLowerCase().endsWith(".md")) continue;
|
|
288
|
+
const skillName = fileName.toLowerCase() === "skill.md"
|
|
289
|
+
? path.basename(path.dirname(skillPath))
|
|
290
|
+
: path.basename(fileName, path.extname(fileName));
|
|
291
|
+
pushEntry(skillName, skillPath);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!stat.isDirectory()) continue;
|
|
296
|
+
|
|
297
|
+
const rootSkillFile = path.join(skillPath, "SKILL.md");
|
|
298
|
+
if (fs.existsSync(rootSkillFile)) {
|
|
299
|
+
pushEntry(path.basename(skillPath), rootSkillFile);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let childEntries: fs.Dirent[];
|
|
303
|
+
try {
|
|
304
|
+
childEntries = fs.readdirSync(skillPath, { withFileTypes: true });
|
|
305
|
+
} catch {
|
|
306
|
+
// Ignore unreadable skill directories and continue scanning.
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const child of childEntries) {
|
|
311
|
+
if (child.name.startsWith(".")) continue;
|
|
312
|
+
const childPath = path.join(skillPath, child.name);
|
|
313
|
+
if (child.isDirectory() || child.isSymbolicLink()) {
|
|
314
|
+
const nestedSkillPath = path.join(childPath, "SKILL.md");
|
|
315
|
+
if (fs.existsSync(nestedSkillPath)) {
|
|
316
|
+
pushEntry(child.name, nestedSkillPath);
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (child.isFile() && child.name.toLowerCase().endsWith(".md")) {
|
|
321
|
+
pushEntry(path.basename(child.name, path.extname(child.name)), childPath);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return entries;
|
|
327
|
+
}
|
|
328
|
+
|
|
230
329
|
function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
231
330
|
const now = Date.now();
|
|
232
331
|
if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
|
|
@@ -234,18 +333,10 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
|
|
|
234
333
|
}
|
|
235
334
|
|
|
236
335
|
const skillPaths = buildSkillPaths(cwd);
|
|
237
|
-
const loaded =
|
|
336
|
+
const loaded = collectFilesystemSkills(cwd, skillPaths);
|
|
238
337
|
const dedupedByName = new Map<string, CachedSkillEntry>();
|
|
239
338
|
|
|
240
|
-
for (
|
|
241
|
-
const skill = loaded.skills[i] as Skill;
|
|
242
|
-
const entry: CachedSkillEntry = {
|
|
243
|
-
name: skill.name,
|
|
244
|
-
filePath: skill.filePath,
|
|
245
|
-
source: inferSkillSource(skill.sourceInfo, skill.filePath, cwd),
|
|
246
|
-
description: skill.description,
|
|
247
|
-
order: i,
|
|
248
|
-
};
|
|
339
|
+
for (const entry of loaded) {
|
|
249
340
|
const current = dedupedByName.get(entry.name);
|
|
250
341
|
dedupedByName.set(entry.name, chooseHigherPrioritySkill(current, entry));
|
|
251
342
|
}
|
|
@@ -294,6 +385,7 @@ export function readSkill(
|
|
|
294
385
|
|
|
295
386
|
return skill;
|
|
296
387
|
} catch {
|
|
388
|
+
// Treat unreadable skill files as unresolved so callers can surface as missing.
|
|
297
389
|
return undefined;
|
|
298
390
|
}
|
|
299
391
|
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
|
|
23
23
|
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
|
|
24
24
|
import { createForkContextResolver } from "./fork-context.ts";
|
|
25
|
-
import { applyIntercomBridgeToAgent, resolveIntercomBridge } from "./intercom-bridge.ts";
|
|
25
|
+
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
|
|
26
26
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
27
27
|
import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
|
|
28
28
|
import {
|
|
@@ -1117,11 +1117,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1117
1117
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1118
1118
|
deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1119
1119
|
const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
|
|
1120
|
-
|
|
1121
|
-
if (!sessionName) {
|
|
1122
|
-
sessionName = `session-${ctx.sessionManager.getSessionId().slice(0, 8)}`;
|
|
1123
|
-
deps.pi.setSessionName(sessionName);
|
|
1124
|
-
}
|
|
1120
|
+
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
1125
1121
|
const intercomBridge = resolveIntercomBridge({
|
|
1126
1122
|
config: deps.config.intercomBridge,
|
|
1127
1123
|
context: normalizedParams.context,
|
|
@@ -53,6 +53,7 @@ interface SubagentRunConfig {
|
|
|
53
53
|
asyncDir: string;
|
|
54
54
|
sessionId?: string | null;
|
|
55
55
|
piPackageRoot?: string;
|
|
56
|
+
piArgv1?: string;
|
|
56
57
|
worktreeSetupHook?: string;
|
|
57
58
|
worktreeSetupHookTimeoutMs?: number;
|
|
58
59
|
}
|
|
@@ -156,12 +157,16 @@ function runPiStreaming(
|
|
|
156
157
|
outputFile: string,
|
|
157
158
|
env?: Record<string, string | undefined>,
|
|
158
159
|
piPackageRoot?: string,
|
|
160
|
+
piArgv1?: string,
|
|
159
161
|
maxSubagentDepth?: number,
|
|
160
162
|
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
|
|
161
163
|
return new Promise((resolve) => {
|
|
162
164
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
163
165
|
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
|
|
164
|
-
const spawnSpec = getPiSpawnCommand(args,
|
|
166
|
+
const spawnSpec = getPiSpawnCommand(args, {
|
|
167
|
+
...(piPackageRoot ? { piPackageRoot } : {}),
|
|
168
|
+
...(piArgv1 ? { argv1: piArgv1 } : {}),
|
|
169
|
+
});
|
|
165
170
|
const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
|
|
166
171
|
let stdout = "";
|
|
167
172
|
let stderr = "";
|
|
@@ -349,6 +354,7 @@ interface SingleStepContext {
|
|
|
349
354
|
flatStepCount: number;
|
|
350
355
|
outputFile: string;
|
|
351
356
|
piPackageRoot?: string;
|
|
357
|
+
piArgv1?: string;
|
|
352
358
|
}
|
|
353
359
|
|
|
354
360
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -410,7 +416,7 @@ async function runSingleStep(
|
|
|
410
416
|
promptFileStem: step.agent,
|
|
411
417
|
});
|
|
412
418
|
const outputFile = index === 0 ? ctx.outputFile : `${ctx.outputFile}.attempt-${index + 1}`;
|
|
413
|
-
const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, step.maxSubagentDepth);
|
|
419
|
+
const run = await runPiStreaming(args, step.cwd ?? ctx.cwd, outputFile, env, ctx.piPackageRoot, ctx.piArgv1, step.maxSubagentDepth);
|
|
414
420
|
cleanupTempDir(tempDir);
|
|
415
421
|
|
|
416
422
|
const parsed = parseRunOutput(run.stdout);
|
|
@@ -768,6 +774,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
768
774
|
flatIndex: fi, flatStepCount: flatSteps.length,
|
|
769
775
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
770
776
|
piPackageRoot: config.piPackageRoot,
|
|
777
|
+
piArgv1: config.piArgv1,
|
|
771
778
|
});
|
|
772
779
|
if (task.sessionFile) {
|
|
773
780
|
latestSessionFile = task.sessionFile;
|
|
@@ -888,6 +895,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
888
895
|
flatIndex, flatStepCount: flatSteps.length,
|
|
889
896
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
890
897
|
piPackageRoot: config.piPackageRoot,
|
|
898
|
+
piArgv1: config.piArgv1,
|
|
891
899
|
});
|
|
892
900
|
if (seqStep.sessionFile) {
|
|
893
901
|
latestSessionFile = seqStep.sessionFile;
|
|
@@ -106,9 +106,11 @@ function resolveRepoState(cwd: string): RepoState {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
109
|
+
const rawPrefix = runGitChecked(cwd, ["rev-parse", "--show-prefix"]).trim();
|
|
110
|
+
const normalizedPrefix = rawPrefix
|
|
111
|
+
? path.normalize(rawPrefix.replace(/[\\/]+$/, ""))
|
|
112
|
+
: "";
|
|
113
|
+
const cwdRelative = normalizedPrefix === "." ? "" : normalizedPrefix;
|
|
112
114
|
|
|
113
115
|
const status = runGitChecked(toplevel, ["status", "--porcelain"]);
|
|
114
116
|
if (status.trim().length > 0) {
|
|
@@ -124,6 +126,7 @@ function normalizeComparableCwd(cwd: string): string {
|
|
|
124
126
|
try {
|
|
125
127
|
return fs.realpathSync(resolved);
|
|
126
128
|
} catch {
|
|
129
|
+
// Use the unresolved absolute path when realpath resolution is unavailable.
|
|
127
130
|
return resolved;
|
|
128
131
|
}
|
|
129
132
|
}
|
|
@@ -169,6 +172,7 @@ function linkNodeModulesIfPresent(toplevel: string, worktreePath: string): boole
|
|
|
169
172
|
fs.symlinkSync(nodeModulesPath, nodeModulesLinkPath);
|
|
170
173
|
return true;
|
|
171
174
|
} catch {
|
|
175
|
+
// Symlink creation is optional (e.g., unsupported filesystems on CI runners).
|
|
172
176
|
return false;
|
|
173
177
|
}
|
|
174
178
|
}
|
|
@@ -344,8 +348,12 @@ function createSingleWorktree(
|
|
|
344
348
|
syntheticPaths,
|
|
345
349
|
};
|
|
346
350
|
} catch (error) {
|
|
347
|
-
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
|
|
348
|
-
|
|
351
|
+
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
|
|
352
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
353
|
+
}
|
|
354
|
+
try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {
|
|
355
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
356
|
+
}
|
|
349
357
|
throw error;
|
|
350
358
|
}
|
|
351
359
|
}
|
|
@@ -453,12 +461,18 @@ function captureWorktreeDiff(
|
|
|
453
461
|
function writeEmptyPatch(patchPath: string): void {
|
|
454
462
|
try {
|
|
455
463
|
fs.writeFileSync(patchPath, "", "utf-8");
|
|
456
|
-
} catch {
|
|
464
|
+
} catch {
|
|
465
|
+
// Diff artifact writing is best-effort in error paths.
|
|
466
|
+
}
|
|
457
467
|
}
|
|
458
468
|
|
|
459
469
|
function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
|
|
460
|
-
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
461
|
-
|
|
470
|
+
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
471
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
472
|
+
}
|
|
473
|
+
try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {
|
|
474
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
475
|
+
}
|
|
462
476
|
}
|
|
463
477
|
|
|
464
478
|
function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
@@ -502,6 +516,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
502
516
|
try {
|
|
503
517
|
fs.mkdirSync(diffsDir, { recursive: true });
|
|
504
518
|
} catch {
|
|
519
|
+
// Returning no diffs is safer than failing the whole command on artifact-dir issues.
|
|
505
520
|
return [];
|
|
506
521
|
}
|
|
507
522
|
|
|
@@ -513,6 +528,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
513
528
|
try {
|
|
514
529
|
diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
|
|
515
530
|
} catch {
|
|
531
|
+
// Preserve execution flow; failed diff capture maps to an empty per-task patch.
|
|
516
532
|
writeEmptyPatch(patchPath);
|
|
517
533
|
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
|
|
518
534
|
}
|
|
@@ -525,7 +541,9 @@ export function cleanupWorktrees(setup: WorktreeSetup): void {
|
|
|
525
541
|
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
526
542
|
cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
|
|
527
543
|
}
|
|
528
|
-
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
|
|
544
|
+
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
|
|
545
|
+
// Pruning is best-effort cleanup.
|
|
546
|
+
}
|
|
529
547
|
}
|
|
530
548
|
|
|
531
549
|
export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|
|
@@ -4,4 +4,4 @@ Bundled DM extension that shows a live timer next to collapsed Thinking blocks.
|
|
|
4
4
|
|
|
5
5
|
## Bundled with dm
|
|
6
6
|
|
|
7
|
-
No separate install step is required. Install `dm
|
|
7
|
+
No separate install step is required. Install `dm` and restart the TUI if the timer patch is not already active.
|
|
@@ -45,6 +45,55 @@ type NamingTestOverrides = {
|
|
|
45
45
|
generateMergeCommitMessage?: (args: GenerateMergeCommitMessageArgs) => Promise<GeneratedCommitMessage>;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
type NamingModelChoice = {
|
|
49
|
+
label: string;
|
|
50
|
+
config: NamingModelConfig;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type DuckMindPresetAlias = "free" | "auto" | "lite" | "smart" | "deep";
|
|
54
|
+
|
|
55
|
+
type DuckMindPreset = {
|
|
56
|
+
alias: DuckMindPresetAlias;
|
|
57
|
+
envKey: string;
|
|
58
|
+
fallbackModelId: string;
|
|
59
|
+
displayName: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const DUCKMIND_PROVIDER = "duckmind";
|
|
63
|
+
const OPENROUTER_PROVIDER = "openrouter";
|
|
64
|
+
const DUCKMIND_PRESETS: readonly DuckMindPreset[] = [
|
|
65
|
+
{
|
|
66
|
+
alias: "free",
|
|
67
|
+
envKey: "DM_FREE_MODEL",
|
|
68
|
+
fallbackModelId: "google/gemini-3.1-flash-lite-preview",
|
|
69
|
+
displayName: "DuckMind Free",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
alias: "auto",
|
|
73
|
+
envKey: "DM_AUTO_MODEL",
|
|
74
|
+
fallbackModelId: "auto",
|
|
75
|
+
displayName: "DuckMind Auto",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
alias: "lite",
|
|
79
|
+
envKey: "DM_LITE_MODEL",
|
|
80
|
+
fallbackModelId: "google/gemini-2.5-flash",
|
|
81
|
+
displayName: "DuckMind Lite",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
alias: "smart",
|
|
85
|
+
envKey: "DM_SMART_MODEL",
|
|
86
|
+
fallbackModelId: "anthropic/claude-sonnet-4.5",
|
|
87
|
+
displayName: "DuckMind Smart",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
alias: "deep",
|
|
91
|
+
envKey: "DM_DEEP_MODEL",
|
|
92
|
+
fallbackModelId: "openai/o3",
|
|
93
|
+
displayName: "DuckMind Deep",
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
48
97
|
let testOverrides: NamingTestOverrides | undefined;
|
|
49
98
|
|
|
50
99
|
export function setNamingTestOverrides(overrides?: NamingTestOverrides): void {
|
|
@@ -102,8 +151,90 @@ function normalizeCommitMessage(message: GeneratedCommitMessage): GeneratedCommi
|
|
|
102
151
|
return { subject, body };
|
|
103
152
|
}
|
|
104
153
|
|
|
154
|
+
function resolveDuckMindPresetModelId(envKey: string, fallbackModelId: string): string {
|
|
155
|
+
const override = process.env[envKey]?.trim();
|
|
156
|
+
return override ? override : fallbackModelId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function pickOpenRouterModel(
|
|
160
|
+
models: Model<any>[],
|
|
161
|
+
actualModelId: string,
|
|
162
|
+
fallbackModelId: string,
|
|
163
|
+
displayName: string,
|
|
164
|
+
): Model<any> | undefined {
|
|
165
|
+
if (models.length === 0) return undefined;
|
|
166
|
+
|
|
167
|
+
const exact = models.find((model) => model.id.toLowerCase() === actualModelId.toLowerCase());
|
|
168
|
+
if (exact) {
|
|
169
|
+
return exact;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const fallback =
|
|
173
|
+
models.find((model) => model.id.toLowerCase() === fallbackModelId.toLowerCase()) ?? models[0];
|
|
174
|
+
return {
|
|
175
|
+
...fallback,
|
|
176
|
+
id: actualModelId,
|
|
177
|
+
name: displayName,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function buildDefaultNamingChoice(model: Model<any>): NamingModelChoice {
|
|
182
|
+
return {
|
|
183
|
+
label: `${model.provider}/${model.id}`,
|
|
184
|
+
config: {
|
|
185
|
+
provider: model.provider,
|
|
186
|
+
modelId: model.id,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildDuckMindNamingChoices(availableModels: Model<any>[]): NamingModelChoice[] {
|
|
192
|
+
const openrouterModels = availableModels.filter((model) => model.provider === OPENROUTER_PROVIDER);
|
|
193
|
+
return DUCKMIND_PRESETS.flatMap((preset) => {
|
|
194
|
+
const actualModelId = resolveDuckMindPresetModelId(preset.envKey, preset.fallbackModelId);
|
|
195
|
+
const model = pickOpenRouterModel(openrouterModels, actualModelId, preset.fallbackModelId, preset.displayName);
|
|
196
|
+
if (!model) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
{
|
|
201
|
+
label: `${DUCKMIND_PROVIDER}/${preset.alias}`,
|
|
202
|
+
config: {
|
|
203
|
+
provider: DUCKMIND_PROVIDER,
|
|
204
|
+
modelId: preset.alias,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resolveDuckMindAliasModel(availableModels: Model<any>[], config: NamingModelConfig): Model<any> | undefined {
|
|
212
|
+
const normalizedProvider = config.provider.trim().toLowerCase();
|
|
213
|
+
const normalizedModelId = config.modelId.trim().toLowerCase();
|
|
214
|
+
if (normalizedProvider !== DUCKMIND_PROVIDER && normalizedProvider !== OPENROUTER_PROVIDER) {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const preset = DUCKMIND_PRESETS.find(
|
|
219
|
+
(entry) => normalizedModelId === entry.alias || normalizedModelId === `@preset/${entry.alias}`,
|
|
220
|
+
);
|
|
221
|
+
if (!preset) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const actualModelId = resolveDuckMindPresetModelId(preset.envKey, preset.fallbackModelId);
|
|
226
|
+
return pickOpenRouterModel(
|
|
227
|
+
availableModels.filter((model) => model.provider === OPENROUTER_PROVIDER),
|
|
228
|
+
actualModelId,
|
|
229
|
+
preset.fallbackModelId,
|
|
230
|
+
preset.displayName,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
105
234
|
async function resolveNamingModel(ctx: ExtensionContext, config: NamingModelConfig): Promise<{ model: Model<any>; apiKey: string }> {
|
|
106
|
-
const model =
|
|
235
|
+
const model =
|
|
236
|
+
resolveDuckMindAliasModel(ctx.modelRegistry.getAvailable(), config) ??
|
|
237
|
+
ctx.modelRegistry.find(config.provider, config.modelId);
|
|
107
238
|
if (!model) {
|
|
108
239
|
throw new Error(`Ultrathink naming model ${config.provider}/${config.modelId} is not available in DM.`);
|
|
109
240
|
}
|
|
@@ -149,14 +280,19 @@ function formatModelOption(model: Model<any>): string {
|
|
|
149
280
|
return `${model.provider}/${model.id}`;
|
|
150
281
|
}
|
|
151
282
|
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
155
|
-
|
|
283
|
+
function resolveNamingChoice(selection: string, choices: NamingModelChoice[]): NamingModelConfig {
|
|
284
|
+
const choice = choices.find((entry) => entry.label === selection);
|
|
285
|
+
if (choice) {
|
|
286
|
+
return choice.config;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const slash = selection.indexOf("/");
|
|
290
|
+
if (slash <= 0 || slash === selection.length - 1) {
|
|
291
|
+
throw new Error(`Invalid model option: ${selection}`);
|
|
156
292
|
}
|
|
157
293
|
return {
|
|
158
|
-
provider:
|
|
159
|
-
modelId:
|
|
294
|
+
provider: selection.slice(0, slash),
|
|
295
|
+
modelId: selection.slice(slash + 1),
|
|
160
296
|
};
|
|
161
297
|
}
|
|
162
298
|
|
|
@@ -181,20 +317,25 @@ export async function ensureNamingModel(
|
|
|
181
317
|
.filter((model) => model.input.includes("text"))
|
|
182
318
|
.sort((a, b) => formatModelOption(a).localeCompare(formatModelOption(b)));
|
|
183
319
|
|
|
184
|
-
|
|
320
|
+
const choices =
|
|
321
|
+
ctx.model?.provider === OPENROUTER_PROVIDER || ctx.model?.provider === DUCKMIND_PROVIDER
|
|
322
|
+
? buildDuckMindNamingChoices(availableModels)
|
|
323
|
+
: availableModels.map(buildDefaultNamingChoice);
|
|
324
|
+
|
|
325
|
+
if (choices.length === 0) {
|
|
185
326
|
throw new Error("Ultrathink could not find any available DM models to use for branch and commit naming.");
|
|
186
327
|
}
|
|
187
328
|
|
|
188
329
|
const selection = await ctx.ui.select(
|
|
189
330
|
"Choose the small model Ultrathink should use for branch names and commit descriptions",
|
|
190
|
-
|
|
331
|
+
choices.map((choice) => choice.label),
|
|
191
332
|
);
|
|
192
333
|
|
|
193
334
|
if (!selection) {
|
|
194
335
|
return null;
|
|
195
336
|
}
|
|
196
337
|
|
|
197
|
-
const namingModel =
|
|
338
|
+
const namingModel = resolveNamingChoice(selection, choices);
|
|
198
339
|
await saveUltrathinkNamingConfig(namingModel);
|
|
199
340
|
return namingModel;
|
|
200
341
|
}
|