@duckmind/dm-linux-x64 0.64.2 → 0.65.2
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.md +9 -0
- package/dm +0 -0
- package/extensions/.dm-extensions.json +8 -7
- package/extensions/dm-mcp-adapter/init.js +16 -4
- package/extensions/dm-mcp-adapter/proxy-modes.js +12 -3
- package/extensions/dm-skills-manager/extensions/extensions-manager.js +332 -0
- package/extensions/dm-skills-manager/extensions/skills-manager.js +6 -6
- package/extensions/dm-skills-manager/package.json +1 -1
- package/package.json +1 -1
- package/theme/duckmind-classic.json +6 -6
package/README.md
CHANGED
|
@@ -3,3 +3,12 @@
|
|
|
3
3
|
<strong>DuckMind (dm)</strong> is a <strong>minimal, fast AI coworker</strong> that runs in your terminal.
|
|
4
4
|
No UI overhead. Just plan, run, and get things done.
|
|
5
5
|
</p>
|
|
6
|
+
|
|
7
|
+
## IDE integration
|
|
8
|
+
|
|
9
|
+
Configure a trusted project's MCP client (including Cursor) to run `dm`
|
|
10
|
+
with arguments `["lens-mcp", "--cwd=/absolute/path/to/project"]`.
|
|
11
|
+
This exposes DM Lens code diagnostics and navigation over stdio; it does
|
|
12
|
+
not replace the editor's native language server or require a model API key.
|
|
13
|
+
Use an absolute path to the installed `dm` launcher when the IDE lacks your
|
|
14
|
+
terminal PATH. Enable the server in the IDE before invoking its tools.
|
package/dm
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"status": "ok",
|
|
3
|
-
"prepared_at": "2026-09-
|
|
3
|
+
"prepared_at": "2026-09-12T09:29:01.010336+00:00",
|
|
4
4
|
"managed_entries": [
|
|
5
5
|
{
|
|
6
6
|
"id": "dm-context",
|
|
@@ -754,6 +754,7 @@
|
|
|
754
754
|
"tool": "bun build --no-bundle",
|
|
755
755
|
"mode": "module-preserving-transpile",
|
|
756
756
|
"files": [
|
|
757
|
+
"extensions/extensions-manager.js",
|
|
757
758
|
"extensions/skills-manager/components.js",
|
|
758
759
|
"extensions/skills-manager/constants.js",
|
|
759
760
|
"extensions/skills-manager/creation-fallback.js",
|
|
@@ -772,11 +773,11 @@
|
|
|
772
773
|
"extensions/skills-manager/ui.js",
|
|
773
774
|
"extensions/skills-manager.js"
|
|
774
775
|
],
|
|
775
|
-
"file_count":
|
|
776
|
-
"before_bytes":
|
|
777
|
-
"after_bytes":
|
|
776
|
+
"file_count": 18,
|
|
777
|
+
"before_bytes": 110893,
|
|
778
|
+
"after_bytes": 99433,
|
|
778
779
|
"runtime_specifier_rewrites": 0,
|
|
779
|
-
"runtime_filename_rewrites":
|
|
780
|
+
"runtime_filename_rewrites": 18,
|
|
780
781
|
"declaration_files_removed": 0
|
|
781
782
|
}
|
|
782
783
|
},
|
|
@@ -1428,8 +1429,8 @@
|
|
|
1428
1429
|
"utils.js"
|
|
1429
1430
|
],
|
|
1430
1431
|
"file_count": 81,
|
|
1431
|
-
"before_bytes":
|
|
1432
|
-
"after_bytes":
|
|
1432
|
+
"before_bytes": 1439473,
|
|
1433
|
+
"after_bytes": 1267119,
|
|
1433
1434
|
"runtime_specifier_rewrites": 39,
|
|
1434
1435
|
"runtime_filename_rewrites": 64,
|
|
1435
1436
|
"declaration_files_removed": 12
|
|
@@ -83,6 +83,13 @@ export function recordFailure(state, serverName, message) {
|
|
|
83
83
|
notifyToolMetadataUpdated(state, serverName, "failure-backoff-started");
|
|
84
84
|
publishMcpStatusSnapshot(state);
|
|
85
85
|
}
|
|
86
|
+
export function formatMcpConnectionFailure(message) {
|
|
87
|
+
const sanitized = sanitizeTerminalText(message);
|
|
88
|
+
if (/endpoint returned .*\(401\).*authentication may be required/i.test(sanitized)) {
|
|
89
|
+
return "Authentication may be required (HTTP 401). Check the server credentials or OAuth sign-in, then reconnect.";
|
|
90
|
+
}
|
|
91
|
+
return sanitized;
|
|
92
|
+
}
|
|
86
93
|
export function isTuiMode(ctx) {
|
|
87
94
|
return ctx.hasUI && ctx.mode === "tui";
|
|
88
95
|
}
|
|
@@ -311,8 +318,13 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
|
|
|
311
318
|
if (error || !connection) {
|
|
312
319
|
if (initialSignal?.aborted)
|
|
313
320
|
continue;
|
|
314
|
-
|
|
315
|
-
|
|
321
|
+
const displayError = formatMcpConnectionFailure(error ?? "Unknown connection failure");
|
|
322
|
+
if (error) {
|
|
323
|
+
recordFailure(state, name, displayError);
|
|
324
|
+
if (displayError !== sanitizeTerminalText(error)) {
|
|
325
|
+
logger.debug("MCP: startup connection probe diagnostic", { server: name, diagnostic: sanitizeTerminalText(error) });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
316
328
|
if (transient) {
|
|
317
329
|
const notice = `MCP: ${name} temporarily unavailable (HTTP 503); retry later`;
|
|
318
330
|
logger.debug(`MCP: startup connect hit transient upstream outage for ${name}; will retry`);
|
|
@@ -322,11 +334,11 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
|
|
|
322
334
|
console.error(notice);
|
|
323
335
|
continue;
|
|
324
336
|
}
|
|
325
|
-
const displayError = sanitizeTerminalText(error ?? "Unknown connection failure");
|
|
326
337
|
if (ui) {
|
|
327
338
|
ui.notify(`MCP: Failed to connect to ${name}: ${displayError}`, "error");
|
|
339
|
+
} else {
|
|
340
|
+
console.error(`MCP: Failed to connect to ${name}: ${displayError}`);
|
|
328
341
|
}
|
|
329
|
-
console.error(`MCP: Failed to connect to ${name}: ${displayError}`);
|
|
330
342
|
continue;
|
|
331
343
|
}
|
|
332
344
|
const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix, config.mcpServers, startupKnownMetadata, true);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { getServerPrefix, isServerDisabled, parseUiPromptHandoff } from "./types.js";
|
|
4
|
-
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated,
|
|
4
|
+
import { clearFailure, formatMcpConnectionFailure, getFailureAgeSeconds, lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, recordFailure, updateMetadataCache, updateServerMetadata, updateStatusBar } from "./init.js";
|
|
5
5
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
6
6
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
|
|
7
7
|
import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from "./tool-metadata.js";
|
|
@@ -17,6 +17,7 @@ import { paginate, rankSuggestions, rankToolMatches, resolveSearchKeywords } fro
|
|
|
17
17
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
|
|
18
18
|
import { isServerInActiveFailureBackoff } from "./failure-backoff.js";
|
|
19
19
|
import { getInputRequiredNeedsUiDetails } from "./errors.js";
|
|
20
|
+
import { logger } from "./logger.js";
|
|
20
21
|
const require = createRequire(import.meta.url);
|
|
21
22
|
const MAX_REGEX_SEARCH_QUERY_LENGTH = 256;
|
|
22
23
|
const INSTRUCTIONS_PREVIEW_LENGTH = 300;
|
|
@@ -812,7 +813,11 @@ export async function executeConnect(state, serverName, signal) {
|
|
|
812
813
|
updateStatusBar(state);
|
|
813
814
|
return executeList(state, serverName);
|
|
814
815
|
} catch (error) {
|
|
815
|
-
const
|
|
816
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
817
|
+
const message = formatMcpConnectionFailure(rawMessage);
|
|
818
|
+
if (message !== rawMessage) {
|
|
819
|
+
logger.debug("MCP: manual connection probe diagnostic", { server: serverName, diagnostic: rawMessage });
|
|
820
|
+
}
|
|
816
821
|
if (!isAbortError(error, ownedSignal))
|
|
817
822
|
recordFailure(state, serverName, message);
|
|
818
823
|
updateStatusBar(state);
|
|
@@ -1134,7 +1139,11 @@ export async function executeCall(state, toolName, args, serverOverride, getPiTo
|
|
|
1134
1139
|
};
|
|
1135
1140
|
}
|
|
1136
1141
|
} catch (error) {
|
|
1137
|
-
const
|
|
1142
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
1143
|
+
const message = formatMcpConnectionFailure(rawMessage);
|
|
1144
|
+
if (message !== rawMessage) {
|
|
1145
|
+
logger.debug("MCP: reconnect probe diagnostic", { server: serverName, diagnostic: rawMessage });
|
|
1146
|
+
}
|
|
1138
1147
|
if (!isAbortError(error, ownedSignal))
|
|
1139
1148
|
recordFailure(state, serverName, message);
|
|
1140
1149
|
updateStatusBar(state);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@duckmind/dm-coding-agent";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
export const DUCKMIND_EXTENSIONS_REMOTE = "https://github.com/DuckMind/extensions.git";
|
|
10
|
+
const REMOTE_BRANCH = "main";
|
|
11
|
+
const REGISTRY_FILE = ".duckmind-installed-extensions.json";
|
|
12
|
+
const SKIPPED_COPY_ENTRIES = new Set([".git", ".m", "node_modules"]);
|
|
13
|
+
function errorMessage(error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
return message.length > 220 ? `${message.slice(0, 219)}…` : message;
|
|
16
|
+
}
|
|
17
|
+
export function validateExtensionId(value) {
|
|
18
|
+
if (!/^dm-[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
19
|
+
throw new Error(`Invalid DM extension id: ${value}`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function isWithin(root, candidate) {
|
|
24
|
+
const rel = relative(root, candidate);
|
|
25
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
26
|
+
}
|
|
27
|
+
export function extensionRootFor(scope, cwd, agentDir = getAgentDir()) {
|
|
28
|
+
return scope === "project" ? join(resolve(cwd), CONFIG_DIR_NAME, "extensions") : join(resolve(agentDir), "extensions");
|
|
29
|
+
}
|
|
30
|
+
function registryPath(extensionRoot) {
|
|
31
|
+
return join(extensionRoot, REGISTRY_FILE);
|
|
32
|
+
}
|
|
33
|
+
async function readRegistry(extensionRoot) {
|
|
34
|
+
try {
|
|
35
|
+
const value = JSON.parse(await fs.readFile(registryPath(extensionRoot), "utf8"));
|
|
36
|
+
if (typeof value === "object" && value !== null && value.version === 1 && typeof value.extensions === "object" && value.extensions !== null) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code !== "ENOENT") {
|
|
41
|
+
throw new Error(`Could not read extension registry: ${errorMessage(error)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { version: 1, extensions: {} };
|
|
45
|
+
}
|
|
46
|
+
async function writeRegistry(extensionRoot, registry) {
|
|
47
|
+
const staging = `${registryPath(extensionRoot)}.${randomUUID()}.tmp`;
|
|
48
|
+
await fs.writeFile(staging, `${JSON.stringify(registry, null, 2)}
|
|
49
|
+
`, { encoding: "utf8", mode: 384 });
|
|
50
|
+
await fs.rename(staging, registryPath(extensionRoot));
|
|
51
|
+
}
|
|
52
|
+
async function readPackageManifest(packageDir) {
|
|
53
|
+
let manifest;
|
|
54
|
+
try {
|
|
55
|
+
manifest = JSON.parse(await fs.readFile(join(packageDir, "package.json"), "utf8"));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Could not read package.json: ${errorMessage(error)}`);
|
|
58
|
+
}
|
|
59
|
+
const candidate = manifest;
|
|
60
|
+
if (typeof candidate.name !== "string")
|
|
61
|
+
throw new Error("package.json must declare a string name.");
|
|
62
|
+
const extensions = candidate.dm?.extensions;
|
|
63
|
+
if (!Array.isArray(extensions) || extensions.length === 0 || !extensions.every((entry) => typeof entry === "string")) {
|
|
64
|
+
throw new Error("package.json must declare a non-empty dm.extensions array.");
|
|
65
|
+
}
|
|
66
|
+
for (const entry of extensions) {
|
|
67
|
+
const target = resolve(packageDir, entry);
|
|
68
|
+
if (!isWithin(packageDir, target))
|
|
69
|
+
throw new Error(`Extension entry escapes its package: ${entry}`);
|
|
70
|
+
const stat = await fs.lstat(target).catch(() => {
|
|
71
|
+
return;
|
|
72
|
+
});
|
|
73
|
+
if (!stat?.isFile() || stat.isSymbolicLink())
|
|
74
|
+
throw new Error(`Extension entry is missing or unsafe: ${entry}`);
|
|
75
|
+
}
|
|
76
|
+
return { name: candidate.name, extensions };
|
|
77
|
+
}
|
|
78
|
+
async function copyDirectory(source, destination) {
|
|
79
|
+
const sourceStat = await fs.lstat(source);
|
|
80
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink())
|
|
81
|
+
throw new Error("Extension source must be a real directory.");
|
|
82
|
+
await fs.mkdir(destination, { recursive: true, mode: 448 });
|
|
83
|
+
for (const entry of await fs.readdir(source, { withFileTypes: true })) {
|
|
84
|
+
if (SKIPPED_COPY_ENTRIES.has(entry.name))
|
|
85
|
+
continue;
|
|
86
|
+
const from = join(source, entry.name);
|
|
87
|
+
const to = join(destination, entry.name);
|
|
88
|
+
if (entry.isSymbolicLink())
|
|
89
|
+
throw new Error(`Extension source contains an unsupported symlink: ${entry.name}`);
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
await copyDirectory(from, to);
|
|
92
|
+
} else if (entry.isFile()) {
|
|
93
|
+
await fs.copyFile(from, to);
|
|
94
|
+
const mode = (await fs.stat(from)).mode & 511;
|
|
95
|
+
await fs.chmod(to, mode);
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error(`Extension source contains an unsupported entry: ${entry.name}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function listInstalledExtensions(cwd, agentDir = getAgentDir()) {
|
|
102
|
+
const result = [];
|
|
103
|
+
for (const scope of ["global", "project"]) {
|
|
104
|
+
const root = extensionRootFor(scope, cwd, agentDir);
|
|
105
|
+
const registry = await readRegistry(root);
|
|
106
|
+
const entries = await fs.readdir(root, { withFileTypes: true }).catch((error) => {
|
|
107
|
+
if (error.code === "ENOENT")
|
|
108
|
+
return;
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
111
|
+
if (!entries)
|
|
112
|
+
continue;
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !/^dm-/.test(entry.name))
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
const manifest = await readPackageManifest(join(root, entry.name));
|
|
118
|
+
if (manifest.name !== entry.name)
|
|
119
|
+
continue;
|
|
120
|
+
const origin = registry.extensions[entry.name];
|
|
121
|
+
result.push({
|
|
122
|
+
id: entry.name,
|
|
123
|
+
scope,
|
|
124
|
+
path: join(root, entry.name),
|
|
125
|
+
source: origin?.source ?? "local",
|
|
126
|
+
revision: origin?.revision
|
|
127
|
+
});
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result.sort((left, right) => left.scope.localeCompare(right.scope) || left.id.localeCompare(right.id));
|
|
132
|
+
}
|
|
133
|
+
export async function installExtensionDirectory(sourceDirectory, extensionRoot, options) {
|
|
134
|
+
const source = resolve(sourceDirectory);
|
|
135
|
+
const manifest = await readPackageManifest(source);
|
|
136
|
+
const id = validateExtensionId(manifest.name);
|
|
137
|
+
if (options.expectedId && id !== options.expectedId) {
|
|
138
|
+
throw new Error(`Remote directory ${options.expectedId} declares ${id} instead.`);
|
|
139
|
+
}
|
|
140
|
+
const root = resolve(extensionRoot);
|
|
141
|
+
const destination = join(root, id);
|
|
142
|
+
if (!isWithin(root, destination))
|
|
143
|
+
throw new Error("Extension destination escapes its configured root.");
|
|
144
|
+
if (await fs.lstat(destination).catch(() => {
|
|
145
|
+
return;
|
|
146
|
+
})) {
|
|
147
|
+
throw new Error(`${id} is already installed at ${destination}. Remove it before importing a replacement.`);
|
|
148
|
+
}
|
|
149
|
+
await fs.mkdir(root, { recursive: true, mode: 448 });
|
|
150
|
+
const staging = join(root, `.${id}.install-${randomUUID()}`);
|
|
151
|
+
try {
|
|
152
|
+
await copyDirectory(source, staging);
|
|
153
|
+
const copied = await readPackageManifest(staging);
|
|
154
|
+
if (copied.name !== id)
|
|
155
|
+
throw new Error("Copied extension manifest changed unexpectedly.");
|
|
156
|
+
await fs.rename(staging, destination);
|
|
157
|
+
const registry = await readRegistry(root);
|
|
158
|
+
registry.extensions[id] = { source: options.source, revision: options.revision };
|
|
159
|
+
await writeRegistry(root, registry);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
await fs.rm(staging, { recursive: true, force: true }).catch(() => {
|
|
162
|
+
return;
|
|
163
|
+
});
|
|
164
|
+
if (await fs.lstat(destination).catch(() => {
|
|
165
|
+
return;
|
|
166
|
+
})) {
|
|
167
|
+
await fs.rm(destination, { recursive: true, force: true }).catch(() => {
|
|
168
|
+
return;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
return { id, scope: options.scope, path: destination, source: options.source, revision: options.revision };
|
|
174
|
+
}
|
|
175
|
+
export async function removeInstalledExtension(extension, cwd, agentDir = getAgentDir()) {
|
|
176
|
+
const root = extensionRootFor(extension.scope, cwd, agentDir);
|
|
177
|
+
const target = resolve(extension.path);
|
|
178
|
+
if (!isWithin(root, target) || target === root || basename(target) !== extension.id) {
|
|
179
|
+
throw new Error("Refusing to remove an extension outside DM's managed extension roots.");
|
|
180
|
+
}
|
|
181
|
+
const stat = await fs.lstat(target).catch(() => {
|
|
182
|
+
return;
|
|
183
|
+
});
|
|
184
|
+
if (!stat?.isDirectory() || stat.isSymbolicLink())
|
|
185
|
+
throw new Error(`Installed extension is unavailable: ${extension.id}`);
|
|
186
|
+
await fs.rm(target, { recursive: true, force: false });
|
|
187
|
+
const registry = await readRegistry(root);
|
|
188
|
+
delete registry.extensions[extension.id];
|
|
189
|
+
await writeRegistry(root, registry);
|
|
190
|
+
}
|
|
191
|
+
async function withRemoteSnapshot(action) {
|
|
192
|
+
const workRoot = await fs.mkdtemp(join(tmpdir(), "dm-extensions-"));
|
|
193
|
+
const checkout = join(workRoot, "checkout");
|
|
194
|
+
try {
|
|
195
|
+
await execFileAsync("git", ["clone", "--depth", "1", "--branch", REMOTE_BRANCH, DUCKMIND_EXTENSIONS_REMOTE, checkout], {
|
|
196
|
+
timeout: 30000,
|
|
197
|
+
maxBuffer: 1024 * 1024
|
|
198
|
+
});
|
|
199
|
+
const { stdout } = await execFileAsync("git", ["-C", checkout, "rev-parse", "HEAD"], { timeout: 1e4 });
|
|
200
|
+
const revision = stdout.trim();
|
|
201
|
+
if (!/^[0-9a-f]{40}$/i.test(revision))
|
|
202
|
+
throw new Error("DuckMind extensions repository returned an invalid revision.");
|
|
203
|
+
return await action({ root: checkout, revision });
|
|
204
|
+
} finally {
|
|
205
|
+
await fs.rm(workRoot, { recursive: true, force: true }).catch(() => {
|
|
206
|
+
return;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function chooseScope(ctx) {
|
|
211
|
+
const selected = await ctx.ui.select("Install extension for", [
|
|
212
|
+
"Global — available in every DM workspace",
|
|
213
|
+
"Project — available only in this workspace"
|
|
214
|
+
]);
|
|
215
|
+
if (!selected)
|
|
216
|
+
return;
|
|
217
|
+
if (selected.startsWith("Project")) {
|
|
218
|
+
if (!ctx.isProjectTrusted()) {
|
|
219
|
+
ctx.ui.notify("Trust this project before writing .dm/extensions.", "warning");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
return "project";
|
|
223
|
+
}
|
|
224
|
+
return "global";
|
|
225
|
+
}
|
|
226
|
+
async function reloadAfterMutation(ctx, message) {
|
|
227
|
+
ctx.ui.notify(`${message} Reloading DM resources…`, "info");
|
|
228
|
+
await ctx.reload();
|
|
229
|
+
}
|
|
230
|
+
async function installRemoteExtension(ctx) {
|
|
231
|
+
await withRemoteSnapshot(async ({ root, revision }) => {
|
|
232
|
+
const candidates = (await fs.readdir(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && /^dm-/.test(entry.name)).map((entry) => entry.name).sort();
|
|
233
|
+
const id = await ctx.ui.select("Install from DuckMind extensions", candidates);
|
|
234
|
+
if (!id)
|
|
235
|
+
return;
|
|
236
|
+
const scope = await chooseScope(ctx);
|
|
237
|
+
if (!scope)
|
|
238
|
+
return;
|
|
239
|
+
const confirmed = await ctx.ui.confirm(`Install ${id}?`, `DM will copy ${id} from the reviewed DuckMind extensions repository at ${revision.slice(0, 12)} into ${scope} extensions.`);
|
|
240
|
+
if (!confirmed)
|
|
241
|
+
return;
|
|
242
|
+
const targetRoot = extensionRootFor(scope, ctx.cwd);
|
|
243
|
+
await installExtensionDirectory(join(root, id), targetRoot, { expectedId: id, scope, source: "duckmind", revision });
|
|
244
|
+
await reloadAfterMutation(ctx, `Installed ${id}.`);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function installLocalExtension(ctx) {
|
|
248
|
+
const input = await ctx.ui.input("Local DM extension directory", "Path containing package.json with dm.extensions");
|
|
249
|
+
if (!input?.trim())
|
|
250
|
+
return;
|
|
251
|
+
const source = resolve(ctx.cwd, input.trim());
|
|
252
|
+
const manifest = await readPackageManifest(source);
|
|
253
|
+
const id = validateExtensionId(manifest.name);
|
|
254
|
+
const scope = await chooseScope(ctx);
|
|
255
|
+
if (!scope)
|
|
256
|
+
return;
|
|
257
|
+
const confirmed = await ctx.ui.confirm(`Install ${id}?`, `DM will copy the local directory into ${scope} extensions. Symlinks and node_modules are rejected.`);
|
|
258
|
+
if (!confirmed)
|
|
259
|
+
return;
|
|
260
|
+
await installExtensionDirectory(source, extensionRootFor(scope, ctx.cwd), { scope, source: "local" });
|
|
261
|
+
await reloadAfterMutation(ctx, `Installed ${id}.`);
|
|
262
|
+
}
|
|
263
|
+
async function removeLocalExtension(ctx) {
|
|
264
|
+
const installed = await listInstalledExtensions(ctx.cwd);
|
|
265
|
+
if (installed.length === 0) {
|
|
266
|
+
ctx.ui.notify("No locally installed DM extension packages were found.", "info");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const labels = installed.map((extension) => `[${extension.scope}] ${extension.id} — ${extension.source}`);
|
|
270
|
+
const selected = await ctx.ui.select("Remove installed DM extension", labels);
|
|
271
|
+
if (!selected)
|
|
272
|
+
return;
|
|
273
|
+
const extension = installed[labels.indexOf(selected)];
|
|
274
|
+
if (!extension)
|
|
275
|
+
return;
|
|
276
|
+
if (extension.scope === "project" && !ctx.isProjectTrusted()) {
|
|
277
|
+
ctx.ui.notify("Trust this project before removing its .dm/extensions entry.", "warning");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const confirmed = await ctx.ui.confirm(`Remove ${extension.id}?`, `This permanently deletes only ${extension.path}, beneath DM's ${extension.scope} extension root.`);
|
|
281
|
+
if (!confirmed)
|
|
282
|
+
return;
|
|
283
|
+
await removeInstalledExtension(extension, ctx.cwd);
|
|
284
|
+
await reloadAfterMutation(ctx, `Removed ${extension.id}.`);
|
|
285
|
+
}
|
|
286
|
+
async function showInstalledExtensions(ctx) {
|
|
287
|
+
const installed = await listInstalledExtensions(ctx.cwd);
|
|
288
|
+
if (installed.length === 0) {
|
|
289
|
+
ctx.ui.notify("No locally installed DM extension packages were found.", "info");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
await ctx.ui.select("Installed DM extensions", installed.map((extension) => `[${extension.scope}] ${extension.id} — ${extension.source}${extension.revision ? ` @ ${extension.revision.slice(0, 12)}` : ""}`));
|
|
293
|
+
}
|
|
294
|
+
export default function extensionsManager(dm) {
|
|
295
|
+
dm.registerCommand("extensions", {
|
|
296
|
+
description: "Install, inspect, or remove local DM extension packages.",
|
|
297
|
+
handler: async (args, ctx) => {
|
|
298
|
+
if (args.trim()) {
|
|
299
|
+
ctx.ui.notify("Use /extensions with no arguments to open the extension manager.", "warning");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!ctx.hasUI) {
|
|
303
|
+
ctx.ui.notify("/extensions manager requires interactive mode.", "warning");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const action = await ctx.ui.select("DM Extensions", [
|
|
308
|
+
"Install from DuckMind extensions",
|
|
309
|
+
"Install a local extension directory",
|
|
310
|
+
"Remove an installed local extension",
|
|
311
|
+
"List installed local extensions"
|
|
312
|
+
]);
|
|
313
|
+
switch (action) {
|
|
314
|
+
case "Install from DuckMind extensions":
|
|
315
|
+
await installRemoteExtension(ctx);
|
|
316
|
+
break;
|
|
317
|
+
case "Install a local extension directory":
|
|
318
|
+
await installLocalExtension(ctx);
|
|
319
|
+
break;
|
|
320
|
+
case "Remove an installed local extension":
|
|
321
|
+
await removeLocalExtension(ctx);
|
|
322
|
+
break;
|
|
323
|
+
case "List installed local extensions":
|
|
324
|
+
await showInstalledExtensions(ctx);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
ctx.ui.notify(`Extensions manager failed: ${errorMessage(error)}`, "error");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
@@ -35,17 +35,17 @@ export default function skillsManager(pi) {
|
|
|
35
35
|
ctx.ui.notify("Skills Manager enabled. Reloading...", "info");
|
|
36
36
|
await ctx.reload();
|
|
37
37
|
};
|
|
38
|
-
pi.registerCommand("
|
|
38
|
+
pi.registerCommand("skills", {
|
|
39
39
|
description: "Skills manager recovery command.",
|
|
40
40
|
handler: async (args, ctx) => {
|
|
41
41
|
if (args.trim().toLowerCase() !== "enable") {
|
|
42
|
-
ctx.ui.notify("Skills Manager is disabled. Run /
|
|
42
|
+
ctx.ui.notify("Skills Manager is disabled. Run /skills:enable, then /reload.", "warning");
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
await enableRecovery(ctx);
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
-
pi.registerCommand("
|
|
48
|
+
pi.registerCommand("skills:enable", {
|
|
49
49
|
description: "Re-enable the skills manager",
|
|
50
50
|
handler: async (_args, ctx) => enableRecovery(ctx)
|
|
51
51
|
});
|
|
@@ -66,7 +66,7 @@ export default function skillsManager(pi) {
|
|
|
66
66
|
}
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
|
-
pi.registerCommand("
|
|
69
|
+
pi.registerCommand("skills", {
|
|
70
70
|
description: "DM skills manager view. Native skills remain /skill:name.",
|
|
71
71
|
handler: async (args, ctx) => {
|
|
72
72
|
const rawArgs = args.trim();
|
|
@@ -82,11 +82,11 @@ export default function skillsManager(pi) {
|
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
if (rawArgs) {
|
|
85
|
-
ctx.ui.notify("Use /skill:name for native skill invocation, or /
|
|
85
|
+
ctx.ui.notify("Use /skill:name for native skill invocation, or /skills with no arguments for the manager.", "warning");
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
88
|
if (!ctx.hasUI) {
|
|
89
|
-
ctx.ui.notify("/
|
|
89
|
+
ctx.ui.notify("/skills manager requires interactive mode", "warning");
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
try {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
{
|
|
26
26
|
"key": "enabled",
|
|
27
27
|
"label": "Enable skills manager",
|
|
28
|
-
"description": "Register the /
|
|
28
|
+
"description": "Register the /skills manager view, startup skill-list hiding, and skills management UI. If disabled, /skills enable remains available as a recovery command. Native /skill:name commands stay controlled by DM's Skill commands setting.",
|
|
29
29
|
"type": "boolean",
|
|
30
30
|
"default": true,
|
|
31
31
|
"category": "General",
|
package/package.json
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"tertiaryBg": "#1a1a28",
|
|
9
9
|
"elevatedBg": "#202030",
|
|
10
10
|
"hoverBg": "#252535",
|
|
11
|
-
"textPrimary": "#
|
|
11
|
+
"textPrimary": "#f4f4f8",
|
|
12
12
|
"textSecondary": "#9090a8",
|
|
13
|
-
"textMuted": "#
|
|
13
|
+
"textMuted": "#707084",
|
|
14
14
|
"textDim": "#404050",
|
|
15
15
|
"accentPrimary": "#667eea",
|
|
16
16
|
"accentSecondary": "#764ba2",
|
|
@@ -34,17 +34,17 @@
|
|
|
34
34
|
"muted": "textMuted",
|
|
35
35
|
"dim": "textDim",
|
|
36
36
|
"text": "textPrimary",
|
|
37
|
-
"thinkingText": "
|
|
37
|
+
"thinkingText": "textMuted",
|
|
38
38
|
"selectedBg": "elevatedBg",
|
|
39
39
|
"userMessageBg": "secondaryBg",
|
|
40
40
|
"userMessageText": "textPrimary",
|
|
41
41
|
"customMessageBg": "tertiaryBg",
|
|
42
42
|
"customMessageText": "textPrimary",
|
|
43
|
-
"customMessageLabel": "
|
|
43
|
+
"customMessageLabel": "purple",
|
|
44
44
|
"toolPendingBg": "primaryBg",
|
|
45
45
|
"toolSuccessBg": "secondaryBg",
|
|
46
46
|
"toolErrorBg": "secondaryBg",
|
|
47
|
-
"toolTitle": "
|
|
47
|
+
"toolTitle": "cyan",
|
|
48
48
|
"toolOutput": "textPrimary",
|
|
49
49
|
"mdHeading": "textPrimary",
|
|
50
50
|
"mdLink": "accentPrimary",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"thinkingMedium": "accentPrimary",
|
|
75
75
|
"thinkingHigh": "indigo",
|
|
76
76
|
"thinkingXhigh": "pink",
|
|
77
|
-
"bashMode": "
|
|
77
|
+
"bashMode": "info"
|
|
78
78
|
},
|
|
79
79
|
"export": {
|
|
80
80
|
"pageBg": "deepBlue",
|