@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 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-04T16:25:23.069046+00:00",
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": 17,
776
- "before_bytes": 95706,
777
- "after_bytes": 85484,
776
+ "file_count": 18,
777
+ "before_bytes": 110893,
778
+ "after_bytes": 99433,
778
779
  "runtime_specifier_rewrites": 0,
779
- "runtime_filename_rewrites": 17,
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": 1438212,
1432
- "after_bytes": 1266049,
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
- if (error)
315
- recordFailure(state, name, error);
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, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.js";
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 message = error instanceof Error ? error.message : String(error);
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 message = error instanceof Error ? error.message : String(error);
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("skill", {
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 /skill:enable, then /reload.", "warning");
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("skill:enable", {
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("skill", {
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 /skill with no arguments for the manager.", "warning");
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("/skill manager requires interactive mode", "warning");
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 /skill manager view, startup skill-list hiding, and skills management UI. If disabled, /skill enable remains available as a recovery command. Native /skill:name commands stay controlled by DM's Skill commands setting.",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckmind/dm-linux-x64",
3
- "version": "0.64.2",
3
+ "version": "0.65.2",
4
4
  "description": "DuckMind (dm) binary payload for linux x64",
5
5
  "license": "MIT",
6
6
  "os": [
@@ -8,9 +8,9 @@
8
8
  "tertiaryBg": "#1a1a28",
9
9
  "elevatedBg": "#202030",
10
10
  "hoverBg": "#252535",
11
- "textPrimary": "#e8e8f0",
11
+ "textPrimary": "#f4f4f8",
12
12
  "textSecondary": "#9090a8",
13
- "textMuted": "#606078",
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": "textSecondary",
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": "accentPrimary",
43
+ "customMessageLabel": "purple",
44
44
  "toolPendingBg": "primaryBg",
45
45
  "toolSuccessBg": "secondaryBg",
46
46
  "toolErrorBg": "secondaryBg",
47
- "toolTitle": "accentPrimary",
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": "warning"
77
+ "bashMode": "info"
78
78
  },
79
79
  "export": {
80
80
  "pageBg": "deepBlue",