@duckmind/dm-windows-x64 0.63.4 → 0.63.6

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.
Files changed (32) hide show
  1. package/extensions/.dm-extensions.json +64 -4
  2. package/extensions/dm-context/package.json +1 -1
  3. package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
  4. package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
  5. package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
  6. package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
  7. package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
  8. package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
  9. package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
  10. package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
  11. package/extensions/dm-context/src/context.js +3 -2
  12. package/extensions/dm-context/src/index.js +168 -84
  13. package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
  14. package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
  15. package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
  16. package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
  17. package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
  18. package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
  19. package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
  20. package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
  21. package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
  22. package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
  23. package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
  24. package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
  25. package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
  26. package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
  27. package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
  28. package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
  29. package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
  30. package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
  31. package/extensions/dm-skills-manager/package.json +121 -0
  32. package/package.json +1 -1
@@ -0,0 +1,15 @@
1
+ import * as piAi from "@duckmind/dm-ai";
2
+ const dynamicImport = (specifier) => import(specifier);
3
+ export async function completeSimple(model, context, options, deps = {}) {
4
+ const rootCompleteSimple = (deps.root ?? piAi).completeSimple;
5
+ if (typeof rootCompleteSimple === "function")
6
+ return await rootCompleteSimple(model, context, options);
7
+ const compat = deps.loadCompat ? await deps.loadCompat() : await dynamicImport("@duckmind/dm-ai/compat");
8
+ return await compat.completeSimple(model, context, options);
9
+ }
10
+ export async function retrySkillGenerationCompat(produce, signal, onRetryScheduled, deps = {}) {
11
+ const candidate = (deps.root ?? piAi).retryAssistantCall;
12
+ if (typeof candidate !== "function")
13
+ return produce();
14
+ return candidate(produce, { enabled: true, maxRetries: 3, baseDelayMs: 2000 }, signal, { onRetryScheduled });
15
+ }
@@ -0,0 +1,125 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAgentDir, parseFrontmatter, stripFrontmatter } from "@duckmind/dm-coding-agent";
4
+ import { findProjectDmDir } from "./paths.js";
5
+ export function normalizeSkillName(name) {
6
+ return name.toLowerCase().trim().replace(/[^a-z0-9-\s]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
7
+ }
8
+ export function getTargetDir(ctx, location, skillName) {
9
+ return location === "global" ? join(getAgentDir(), "skills", skillName) : join(findProjectDmDir(ctx.cwd), "skills", skillName);
10
+ }
11
+ function formatScalar(value) {
12
+ if (typeof value === "string") {
13
+ if (value.length === 0)
14
+ return '""';
15
+ if (/^[A-Za-z0-9_./@,+()[\]\- ]+$/.test(value) && !value.includes(": ") && !/^\s|\s$/.test(value))
16
+ return value;
17
+ return JSON.stringify(value);
18
+ }
19
+ if (typeof value === "number" || typeof value === "boolean")
20
+ return String(value);
21
+ if (value === null)
22
+ return "null";
23
+ return JSON.stringify(value);
24
+ }
25
+ export function formatYamlValue(key, value, indent = "") {
26
+ if (typeof value === "string" && value.includes(`
27
+ `)) {
28
+ return [`${indent}${key}: |`, ...value.split(`
29
+ `).map((line) => `${indent} ${line}`)];
30
+ }
31
+ if (Array.isArray(value)) {
32
+ if (value.length === 0)
33
+ return [`${indent}${key}: []`];
34
+ return [
35
+ `${indent}${key}:`,
36
+ ...value.flatMap((item) => {
37
+ if (item && typeof item === "object") {
38
+ return [`${indent} -`, ...Object.entries(item).flatMap(([nestedKey, nestedValue]) => formatYamlValue(nestedKey, nestedValue, `${indent} `))];
39
+ }
40
+ return [`${indent} - ${formatScalar(item)}`];
41
+ })
42
+ ];
43
+ }
44
+ if (value && typeof value === "object") {
45
+ const entries = Object.entries(value);
46
+ if (entries.length === 0)
47
+ return [`${indent}${key}: {}`];
48
+ return [`${indent}${key}:`, ...entries.flatMap(([nestedKey, nestedValue]) => formatYamlValue(nestedKey, nestedValue, `${indent} `))];
49
+ }
50
+ return [`${indent}${key}: ${formatScalar(value)}`];
51
+ }
52
+ export function buildFrontmatterBlock(skill) {
53
+ const frontmatter = skill.frontmatter ?? { name: skill.name, description: skill.description };
54
+ const lines = Object.entries(frontmatter).flatMap(([key, value]) => formatYamlValue(key, value));
55
+ return ["---", ...lines, "---"].join(`
56
+ `);
57
+ }
58
+ export function buildSkillDocument(skill) {
59
+ const frontmatter = buildFrontmatterBlock(skill);
60
+ const content = skill.content.trim();
61
+ return content ? `${frontmatter}
62
+
63
+ ${content}
64
+ ` : `${frontmatter}
65
+ `;
66
+ }
67
+ export function buildEditableSkillDocument(skill, raw) {
68
+ const source = raw ?? buildSkillDocument(skill);
69
+ const parsed = parseFrontmatter(source);
70
+ const frontmatter = { ...parsed.frontmatter };
71
+ delete frontmatter.name;
72
+ const editableBlock = ["---", ...Object.entries(frontmatter).flatMap(([key, value]) => formatYamlValue(key, value)), "---"].join(`
73
+ `);
74
+ const content = stripFrontmatter(source).trim();
75
+ return content ? `${editableBlock}
76
+
77
+ ${content}
78
+ ` : `${editableBlock}
79
+ `;
80
+ }
81
+ export function readSkillDocument(skill) {
82
+ try {
83
+ return readFileSync(skill.path, "utf8");
84
+ } catch {
85
+ return buildSkillDocument(skill);
86
+ }
87
+ }
88
+ export function frontmatterToRaw(frontmatter, content) {
89
+ const block = ["---", ...Object.entries(frontmatter).flatMap(([key, value]) => formatYamlValue(key, value)), "---"].join(`
90
+ `);
91
+ return content.trim() ? `${block}
92
+
93
+ ${content.trim()}
94
+ ` : `${block}
95
+ `;
96
+ }
97
+ export function parseSkillDocument(raw, expectedName) {
98
+ const parsed = parseFrontmatter(raw);
99
+ const name = typeof parsed.frontmatter.name === "string" ? parsed.frontmatter.name.trim() : "";
100
+ const description = typeof parsed.frontmatter.description === "string" ? parsed.frontmatter.description.trim() : "";
101
+ if (!name || !description)
102
+ throw new Error("Skill must include frontmatter fields 'name' and 'description'");
103
+ if (name !== expectedName)
104
+ throw new Error(`Frontmatter name must stay '${expectedName}'`);
105
+ const frontmatter = Object.fromEntries(Object.entries(parsed.frontmatter).filter(([, value]) => value !== undefined));
106
+ const content = stripFrontmatter(raw).trim();
107
+ return { name, description, frontmatter, content, raw: frontmatterToRaw(frontmatter, content) };
108
+ }
109
+ export function parseEditableSkillDocument(raw, expectedName) {
110
+ const parsed = parseFrontmatter(raw);
111
+ if (typeof parsed.frontmatter.name === "string")
112
+ throw new Error("Name is immutable here. Use Rename instead.");
113
+ const frontmatter = {
114
+ name: expectedName,
115
+ ...Object.fromEntries(Object.entries(parsed.frontmatter).filter(([, value]) => value !== undefined))
116
+ };
117
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
118
+ if (!description)
119
+ throw new Error("Skill must include frontmatter field 'description'");
120
+ const content = stripFrontmatter(raw).trim();
121
+ return { name: expectedName, description, frontmatter, content, raw: frontmatterToRaw(frontmatter, content) };
122
+ }
123
+ export function toUpdatedSkill(skill, parsed) {
124
+ return { ...skill, name: parsed.name, description: parsed.description, content: parsed.content, frontmatter: parsed.frontmatter };
125
+ }
@@ -0,0 +1,142 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ const LOCAL_CONFIG_ID = "@vanillagreen/dm-skills-manager";
5
+ const GLOBAL_CONFIG_ID = "@vanillagreen/pi-tool-renderer";
6
+ function expandHome(input) {
7
+ if (input === "~")
8
+ return homedir();
9
+ if (input.startsWith("~/"))
10
+ return join(homedir(), input.slice(2));
11
+ return input;
12
+ }
13
+ function projectSettingsPath(cwd) {
14
+ let current = resolve(cwd);
15
+ while (true) {
16
+ const candidate = join(current, ".dm", "settings.json");
17
+ if (existsSync(candidate))
18
+ return candidate;
19
+ if (existsSync(join(current, ".dm")) || existsSync(join(current, ".git")) || existsSync(join(current, ".kendex-lock.json")))
20
+ return candidate;
21
+ const parent = dirname(current);
22
+ if (parent === current)
23
+ return join(resolve(cwd), ".dm", "settings.json");
24
+ current = parent;
25
+ }
26
+ }
27
+ const PROJECT_TRUST_SYMBOL = Symbol.for("kendex.dm.project-trust");
28
+ function projectTrustRegistry() {
29
+ const host = globalThis;
30
+ const existing = host[PROJECT_TRUST_SYMBOL];
31
+ if (existing)
32
+ return existing;
33
+ const created = {};
34
+ host[PROJECT_TRUST_SYMBOL] = created;
35
+ return created;
36
+ }
37
+ export function recordProjectTrust(ctx) {
38
+ if (!ctx.cwd)
39
+ return;
40
+ let trusted = true;
41
+ try {
42
+ trusted = ctx.isProjectTrusted?.() === true;
43
+ } catch {
44
+ trusted = false;
45
+ }
46
+ const registry = projectTrustRegistry();
47
+ if (!registry.projectSettings)
48
+ registry.projectSettings = new Map;
49
+ registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
50
+ }
51
+ function projectSettingsTrusted(settingsPath) {
52
+ return projectTrustRegistry().projectSettings?.get(settingsPath) === true;
53
+ }
54
+ function dmSettingsPaths(cwd = process.cwd()) {
55
+ const userDir = resolve(expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "~/.dm/agent"));
56
+ const user = join(userDir, "settings.json");
57
+ const project = projectSettingsPath(cwd);
58
+ return projectSettingsTrusted(project) ? [user, project] : [user];
59
+ }
60
+ function readPackageConfig(packageId, cwd) {
61
+ const merged = {};
62
+ for (const settingsPath of dmSettingsPaths(cwd)) {
63
+ if (!existsSync(settingsPath))
64
+ continue;
65
+ try {
66
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
67
+ const config = parsed?.kendex?.extensionManager?.config?.[packageId];
68
+ if (config && typeof config === "object" && !Array.isArray(config))
69
+ Object.assign(merged, config);
70
+ } catch {}
71
+ }
72
+ return merged;
73
+ }
74
+ function asGlyphStyle(value) {
75
+ return value === "unicode" || value === "ascii" ? value : undefined;
76
+ }
77
+ export function glyphStyle(cwd) {
78
+ const globalOverride = readPackageConfig(GLOBAL_CONFIG_ID, cwd).globalGlyphStyleOverride;
79
+ const forced = asGlyphStyle(globalOverride);
80
+ if (forced)
81
+ return forced;
82
+ const local = readPackageConfig(LOCAL_CONFIG_ID, cwd);
83
+ return asGlyphStyle(local.glyphStyle) ?? asGlyphStyle(local.treeStyle) ?? "unicode";
84
+ }
85
+ export const GLYPHS = {
86
+ unicode: {
87
+ frame: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
88
+ line: "─",
89
+ tree: { mid: "├─ ", last: "└─ ", stem: "│ ", blank: " " },
90
+ bullet: "● ",
91
+ emptyBullet: "○ ",
92
+ dot: " · ",
93
+ ok: "✓",
94
+ fail: "✗",
95
+ warn: "▲",
96
+ diamond: "◆",
97
+ prompt: "DM",
98
+ ellipsis: "…",
99
+ arrow: "→",
100
+ codeBar: "▌"
101
+ },
102
+ ascii: {
103
+ frame: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|" },
104
+ line: "-",
105
+ tree: { mid: "|-- ", last: "`-- ", stem: "| ", blank: " " },
106
+ bullet: "* ",
107
+ emptyBullet: "o ",
108
+ dot: " - ",
109
+ ok: "+",
110
+ fail: "x",
111
+ warn: "!",
112
+ diamond: "*",
113
+ prompt: "dm",
114
+ ellipsis: "...",
115
+ arrow: "->",
116
+ codeBar: "|"
117
+ }
118
+ };
119
+ export function glyphs(cwd) {
120
+ return GLYPHS[glyphStyle(cwd)];
121
+ }
122
+ export function truncateIndicator(cwd) {
123
+ return glyphs(cwd).ellipsis;
124
+ }
125
+ export function truncateText(text, maxChars, cwd) {
126
+ if (text.length <= maxChars)
127
+ return text;
128
+ const indicator = truncateIndicator(cwd);
129
+ return `${text.slice(0, Math.max(0, maxChars - indicator.length))}${indicator}`;
130
+ }
131
+ export function dot(cwd) {
132
+ return glyphs(cwd).dot;
133
+ }
134
+ export function treeGlyph(branch, cwd) {
135
+ const tree = glyphs(cwd).tree;
136
+ if (branch === "│")
137
+ return tree.stem;
138
+ return branch === "└" ? tree.last : tree.mid;
139
+ }
140
+ export function frameGlyphs(cwd) {
141
+ return glyphs(cwd).frame;
142
+ }
@@ -0,0 +1,93 @@
1
+ import { DEFAULT_LIST_ROWS, DEFAULT_POPUP_MAX_HEIGHT } from "./constants.js";
2
+ export const BROWSE_FRAME_ROWS = 2;
3
+ export const BROWSE_SEARCH_ROWS = 1;
4
+ export const BROWSE_SEARCH_GAP_ROWS = 1;
5
+ export const BROWSE_FOOTER_GAP_ROWS = 1;
6
+ export const BROWSE_FOOTER_ROWS = 1;
7
+ export const BROWSE_NON_LIST_ROWS = BROWSE_FRAME_ROWS + BROWSE_SEARCH_ROWS + BROWSE_SEARCH_GAP_ROWS + BROWSE_FOOTER_GAP_ROWS + BROWSE_FOOTER_ROWS;
8
+ const DEFAULT_POPUP_MAX_HEIGHT_RATIO = 0.86;
9
+ const FALLBACK_TERMINAL_ROWS = Math.ceil((DEFAULT_LIST_ROWS + BROWSE_NON_LIST_ROWS) / DEFAULT_POPUP_MAX_HEIGHT_RATIO);
10
+ function finiteFloor(value) {
11
+ return typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : undefined;
12
+ }
13
+ function positiveFiniteFloor(value) {
14
+ const floored = finiteFloor(value);
15
+ return floored !== undefined && floored >= 1 ? floored : undefined;
16
+ }
17
+ function clamp(value, min, max) {
18
+ return Math.max(min, Math.min(max, value));
19
+ }
20
+ function parsePopupMaxHeight(maxHeight) {
21
+ const rows = positiveFiniteFloor(maxHeight);
22
+ if (rows !== undefined)
23
+ return { kind: "rows", rows };
24
+ if (typeof maxHeight !== "string")
25
+ return;
26
+ const trimmed = maxHeight.trim();
27
+ const percent = trimmed.match(/^(\d+(?:\.\d+)?)%$/);
28
+ if (percent) {
29
+ const value = Number(percent[1]);
30
+ if (Number.isFinite(value) && value > 0)
31
+ return { kind: "percent", ratio: value / 100, text: `${value}%` };
32
+ return;
33
+ }
34
+ if (/^\d+$/.test(trimmed)) {
35
+ const value = Number(trimmed);
36
+ if (Number.isFinite(value) && value >= 1)
37
+ return { kind: "rows", rows: value };
38
+ }
39
+ return;
40
+ }
41
+ function safeTerminalRows(terminalRows) {
42
+ return positiveFiniteFloor(terminalRows) ?? FALLBACK_TERMINAL_ROWS;
43
+ }
44
+ function resolvedPopupMaxHeight(maxHeight) {
45
+ return parsePopupMaxHeight(maxHeight) ?? parsePopupMaxHeight(DEFAULT_POPUP_MAX_HEIGHT) ?? { kind: "percent", ratio: DEFAULT_POPUP_MAX_HEIGHT_RATIO, text: "86%" };
46
+ }
47
+ export function sanitizePopupMaxHeight(maxHeight) {
48
+ const parsed = parsePopupMaxHeight(maxHeight);
49
+ if (parsed?.kind === "rows")
50
+ return parsed.rows;
51
+ if (parsed?.kind === "percent")
52
+ return parsed.text;
53
+ return DEFAULT_POPUP_MAX_HEIGHT;
54
+ }
55
+ export function normalizeListRows(rows, fallback = DEFAULT_LIST_ROWS) {
56
+ return positiveFiniteFloor(rows) ?? positiveFiniteFloor(fallback) ?? DEFAULT_LIST_ROWS;
57
+ }
58
+ export function resolveOverlayRows(terminalRows, maxHeight = DEFAULT_POPUP_MAX_HEIGHT) {
59
+ const terminal = safeTerminalRows(terminalRows);
60
+ const parsed = resolvedPopupMaxHeight(maxHeight);
61
+ if (parsed.kind === "rows")
62
+ return Math.max(1, Math.min(terminal, parsed.rows));
63
+ return Math.max(1, Math.min(terminal, Math.floor(terminal * parsed.ratio)));
64
+ }
65
+ export function responsiveBrowseListRows(configuredRows, terminalRows, maxHeight = DEFAULT_POPUP_MAX_HEIGHT) {
66
+ const configured = normalizeListRows(configuredRows);
67
+ const overlayRows = resolveOverlayRows(terminalRows, maxHeight);
68
+ const availableListRows = Math.max(1, overlayRows - BROWSE_NON_LIST_ROWS);
69
+ return Math.max(1, Math.min(configured, availableListRows));
70
+ }
71
+ export function browseWindow(entryCount, selectedDisplayIndex, listRows) {
72
+ const rows = normalizeListRows(listRows);
73
+ const count = Math.max(0, finiteFloor(entryCount) ?? 0);
74
+ if (count === 0)
75
+ return { listRows: rows, startIndex: 0, endIndex: 0 };
76
+ const selected = clamp(finiteFloor(selectedDisplayIndex) ?? 0, 0, count - 1);
77
+ const maxStartIndex = Math.max(0, count - rows);
78
+ const startIndex = clamp(selected - Math.floor(rows / 2), 0, maxStartIndex);
79
+ return { listRows: rows, startIndex, endIndex: Math.min(startIndex + rows, count) };
80
+ }
81
+ export function responsiveBrowseWindow(configuredRows, terminalRows, entryCount, selectedDisplayIndex, maxHeight = DEFAULT_POPUP_MAX_HEIGHT) {
82
+ const listRows = responsiveBrowseListRows(configuredRows, terminalRows, maxHeight);
83
+ return browseWindow(entryCount, selectedDisplayIndex, listRows);
84
+ }
85
+ export function pageBrowseSelection(selectedIndex, maxSelectedIndex, direction, listRows) {
86
+ const selected = Math.max(0, finiteFloor(selectedIndex) ?? 0);
87
+ const maxIndex = Math.max(0, finiteFloor(maxSelectedIndex) ?? 0);
88
+ const step = normalizeListRows(listRows);
89
+ return clamp(selected + direction * step, 0, maxIndex);
90
+ }
91
+ export function responsiveBrowsePageSelection(configuredRows, terminalRows, selectedIndex, maxSelectedIndex, direction, maxHeight = DEFAULT_POPUP_MAX_HEIGHT) {
92
+ return pageBrowseSelection(selectedIndex, maxSelectedIndex, direction, responsiveBrowseListRows(configuredRows, terminalRows, maxHeight));
93
+ }
@@ -0,0 +1,76 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getAgentDir } from "@duckmind/dm-coding-agent";
6
+ export function expandHome(input) {
7
+ if (input === "~")
8
+ return homedir();
9
+ if (input.startsWith("~/"))
10
+ return join(homedir(), input.slice(2));
11
+ return input;
12
+ }
13
+ export function userDmDir() {
14
+ return resolve(expandHome(process.env.PI_CODING_AGENT_DIR?.trim() || "~/.dm/agent"));
15
+ }
16
+ export function findProjectDmDir(cwd) {
17
+ let current = resolve(cwd);
18
+ while (true) {
19
+ const candidate = join(current, ".dm");
20
+ if (existsSync(candidate))
21
+ return candidate;
22
+ if (existsSync(join(current, ".git")) || existsSync(join(current, ".kendex-lock.json")))
23
+ return candidate;
24
+ const parent = dirname(current);
25
+ if (parent === current)
26
+ return join(resolve(cwd), ".dm");
27
+ current = parent;
28
+ }
29
+ }
30
+ export function projectSettingsPath(cwd) {
31
+ return join(findProjectDmDir(cwd), "settings.json");
32
+ }
33
+ const PROJECT_TRUST_SYMBOL = Symbol.for("kendex.dm.project-trust");
34
+ function projectTrustRegistry() {
35
+ const host = globalThis;
36
+ const existing = host[PROJECT_TRUST_SYMBOL];
37
+ if (existing)
38
+ return existing;
39
+ const created = {};
40
+ host[PROJECT_TRUST_SYMBOL] = created;
41
+ return created;
42
+ }
43
+ export function recordProjectTrust(ctx) {
44
+ if (!ctx.cwd)
45
+ return;
46
+ let trusted = true;
47
+ try {
48
+ trusted = ctx.isProjectTrusted?.() === true;
49
+ } catch {
50
+ trusted = false;
51
+ }
52
+ const registry = projectTrustRegistry();
53
+ if (!registry.projectSettings)
54
+ registry.projectSettings = new Map;
55
+ registry.projectSettings.set(projectSettingsPath(ctx.cwd), trusted);
56
+ }
57
+ export function projectSettingsTrusted(cwd = process.cwd()) {
58
+ return projectTrustRegistry().projectSettings?.get(projectSettingsPath(cwd)) === true;
59
+ }
60
+ function normalizeDir(path) {
61
+ const normalized = resolve(path);
62
+ return normalized.endsWith(sep) ? normalized : normalized + sep;
63
+ }
64
+ function isWithin(path, parent) {
65
+ return normalizeDir(path).startsWith(normalizeDir(parent));
66
+ }
67
+ export function detectExtensionInstallScope(cwd) {
68
+ try {
69
+ const extensionFile = fileURLToPath(import.meta.url);
70
+ if (isWithin(extensionFile, findProjectDmDir(cwd)))
71
+ return "project";
72
+ if (isWithin(extensionFile, getAgentDir()))
73
+ return "global";
74
+ } catch {}
75
+ return "global";
76
+ }
@@ -0,0 +1,95 @@
1
+ import { readFileSync, rmSync } from "node:fs";
2
+ import { basename, dirname } from "node:path";
3
+ import {
4
+ DefaultPackageManager,
5
+ getAgentDir,
6
+ parseFrontmatter,
7
+ SettingsManager,
8
+ stripFrontmatter
9
+ } from "@duckmind/dm-coding-agent";
10
+ import { projectSettingsTrusted } from "./paths.js";
11
+ function compareSkills(a, b) {
12
+ const scopeRank = (scope) => scope === "project" ? 0 : scope === "user" ? 1 : 2;
13
+ const rank = scopeRank(a.scope) - scopeRank(b.scope);
14
+ if (rank !== 0)
15
+ return rank;
16
+ if (a.origin !== b.origin)
17
+ return a.origin === "top-level" ? -1 : 1;
18
+ return a.name.localeCompare(b.name);
19
+ }
20
+ function parseSkillFile(path) {
21
+ try {
22
+ const raw = readFileSync(path, "utf8");
23
+ const { frontmatter } = parseFrontmatter(raw);
24
+ const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
25
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
26
+ if (!name || !description)
27
+ return null;
28
+ return {
29
+ name,
30
+ description,
31
+ content: stripFrontmatter(raw).trim(),
32
+ frontmatter: Object.fromEntries(Object.entries(frontmatter).filter(([, value]) => value !== undefined))
33
+ };
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function toSkillEntry(resource) {
39
+ const parsed = parseSkillFile(resource.path);
40
+ if (!parsed)
41
+ return null;
42
+ return {
43
+ name: parsed.name,
44
+ description: parsed.description,
45
+ content: parsed.content,
46
+ frontmatter: parsed.frontmatter,
47
+ path: resource.path,
48
+ scope: resource.metadata.scope,
49
+ origin: resource.metadata.origin,
50
+ source: resource.metadata.source,
51
+ baseDir: resource.metadata.baseDir,
52
+ enabled: resource.enabled
53
+ };
54
+ }
55
+ function dedupeByPath(skills) {
56
+ const seen = new Set;
57
+ const out = [];
58
+ for (const skill of skills) {
59
+ if (seen.has(skill.path))
60
+ continue;
61
+ seen.add(skill.path);
62
+ out.push(skill);
63
+ }
64
+ return out;
65
+ }
66
+ export async function loadSkillRegistry(cwd) {
67
+ const settingsManager = SettingsManager.create(cwd, getAgentDir(), { projectTrusted: projectSettingsTrusted(cwd) });
68
+ const packageManager = new DefaultPackageManager({ cwd, agentDir: getAgentDir(), settingsManager });
69
+ const resolved = await packageManager.resolve();
70
+ const allSkills = dedupeByPath(resolved.skills.map(toSkillEntry).filter((entry) => entry !== null)).sort(compareSkills);
71
+ const byName = new Map;
72
+ for (const skill of allSkills) {
73
+ if (!skill.enabled)
74
+ continue;
75
+ if (!byName.has(skill.name))
76
+ byName.set(skill.name, skill);
77
+ }
78
+ const skills = Array.from(byName.values()).sort(compareSkills);
79
+ return { skills, allSkills, byName: new Map(skills.map((skill) => [skill.name, skill])) };
80
+ }
81
+ export function isDeletableSkill(skill) {
82
+ return skill.origin === "top-level" && (skill.scope === "project" || skill.scope === "user");
83
+ }
84
+ export function skillStorageTarget(skill) {
85
+ return basename(skill.path).toLowerCase() === "skill.md" ? dirname(skill.path) : skill.path;
86
+ }
87
+ export async function deleteSkill(ctx, skill) {
88
+ if (!isDeletableSkill(skill)) {
89
+ ctx.ui.notify("Only your own project and global skills can be deleted", "warning");
90
+ return false;
91
+ }
92
+ rmSync(skillStorageTarget(skill), { recursive: true, force: true });
93
+ ctx.ui.notify(`Deleted skill: ${skill.name}`, "info");
94
+ return true;
95
+ }
@@ -0,0 +1,93 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { PACKAGE_ID } from "./constants.js";
4
+ import { detectExtensionInstallScope, projectSettingsPath, projectSettingsTrusted, userDmDir } from "./paths.js";
5
+ export function readJsonObject(path) {
6
+ if (!existsSync(path))
7
+ return { path, json: {}, exists: false };
8
+ const text = readFileSync(path, "utf8");
9
+ if (!text.trim())
10
+ return { path, json: {}, exists: true };
11
+ const parsed = JSON.parse(text);
12
+ return { path, json: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}, exists: true };
13
+ }
14
+ export function writeJsonFile(file) {
15
+ mkdirSync(dirname(file.path), { recursive: true });
16
+ writeFileSync(file.path, `${JSON.stringify(file.json, null, 2)}
17
+ `, "utf8");
18
+ file.exists = true;
19
+ }
20
+ function asRecord(value) {
21
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
22
+ }
23
+ function getOrCreateRecord(parent, key) {
24
+ const current = asRecord(parent[key]);
25
+ if (current)
26
+ return current;
27
+ const created = {};
28
+ parent[key] = created;
29
+ return created;
30
+ }
31
+ function piSettingsFiles(cwd = process.cwd()) {
32
+ const user = readJsonObject(join(userDmDir(), "settings.json"));
33
+ return projectSettingsTrusted(cwd) ? [user, readJsonObject(projectSettingsPath(cwd))] : [user];
34
+ }
35
+ function packageConfigFromFile(file) {
36
+ return asRecord(asRecord(asRecord(file.json.kendex)?.extensionManager)?.config)?.[PACKAGE_ID];
37
+ }
38
+ function readkendexConfig(cwd = process.cwd()) {
39
+ const merged = {};
40
+ for (const file of piSettingsFiles(cwd)) {
41
+ const config = packageConfigFromFile(file);
42
+ if (config && typeof config === "object" && !Array.isArray(config))
43
+ Object.assign(merged, config);
44
+ }
45
+ return merged;
46
+ }
47
+ export function settingBoolean(key, fallback, cwd = process.cwd()) {
48
+ const value = readkendexConfig(cwd)[key];
49
+ return typeof value === "boolean" ? value : fallback;
50
+ }
51
+ export function settingString(key, fallback, cwd = process.cwd()) {
52
+ const value = readkendexConfig(cwd)[key];
53
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
54
+ }
55
+ export function settingNumber(key, fallback, cwd = process.cwd()) {
56
+ const value = readkendexConfig(cwd)[key];
57
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
58
+ return Number.isFinite(parsed) ? parsed : fallback;
59
+ }
60
+ export function settingOverlaySize(key, fallback, cwd = process.cwd()) {
61
+ const value = readkendexConfig(cwd)[key];
62
+ if (typeof value === "number" && Number.isFinite(value))
63
+ return value;
64
+ if (typeof value === "string" && value.trim()) {
65
+ const trimmed = value.trim();
66
+ if (/^\d+$/.test(trimmed))
67
+ return Number(trimmed);
68
+ if (/^\d+(?:\.\d+)?%$/.test(trimmed))
69
+ return trimmed;
70
+ }
71
+ return fallback;
72
+ }
73
+ function writeScopeForConfigKey(cwd, key) {
74
+ const [user, project] = piSettingsFiles(cwd);
75
+ if (project && packageConfigFromFile(project)?.[key] !== undefined)
76
+ return "project";
77
+ if (packageConfigFromFile(user)?.[key] !== undefined)
78
+ return "global";
79
+ const detected = detectExtensionInstallScope(cwd);
80
+ return detected === "project" && !projectSettingsTrusted(cwd) ? "global" : detected;
81
+ }
82
+ export function updatePackageConfig(cwd, updates, scope) {
83
+ const firstKey = Object.keys(updates)[0] ?? "enabled";
84
+ const targetScope = scope ?? writeScopeForConfigKey(cwd, firstKey);
85
+ const path = targetScope === "global" ? join(userDmDir(), "settings.json") : projectSettingsPath(cwd);
86
+ const file = readJsonObject(path);
87
+ const kendex = getOrCreateRecord(file.json, "kendex");
88
+ const extensionManager = getOrCreateRecord(kendex, "extensionManager");
89
+ const config = getOrCreateRecord(extensionManager, "config");
90
+ const packageConfig = getOrCreateRecord(config, PACKAGE_ID);
91
+ Object.assign(packageConfig, updates);
92
+ writeJsonFile(file);
93
+ }
@@ -0,0 +1,32 @@
1
+ import { InteractiveMode } from "@duckmind/dm-coding-agent";
2
+ import { STARTUP_HIDE_ENABLED_SYMBOL, STARTUP_PATCH_SYMBOL } from "./constants.js";
3
+ export function setStartupHideEnabled(enabled) {
4
+ globalThis[STARTUP_HIDE_ENABLED_SYMBOL] = enabled;
5
+ }
6
+ function startupHideEnabled() {
7
+ return globalThis[STARTUP_HIDE_ENABLED_SYMBOL] === true;
8
+ }
9
+ export function patchInteractiveModeStartupSkillsBlock() {
10
+ const prototype = InteractiveMode.prototype;
11
+ if (prototype[STARTUP_PATCH_SYMBOL])
12
+ return;
13
+ const originalShowLoadedResources = prototype.showLoadedResources;
14
+ if (typeof originalShowLoadedResources !== "function")
15
+ return;
16
+ prototype.showLoadedResources = function patchedShowLoadedResources(...args) {
17
+ if (!startupHideEnabled())
18
+ return originalShowLoadedResources.apply(this, args);
19
+ const interactiveMode = this;
20
+ const resourceLoader = interactiveMode.session?.resourceLoader;
21
+ if (!resourceLoader || typeof resourceLoader.getSkills !== "function")
22
+ return originalShowLoadedResources.apply(this, args);
23
+ const originalGetSkills = resourceLoader.getSkills;
24
+ resourceLoader.getSkills = () => ({ ...originalGetSkills.call(resourceLoader), skills: [] });
25
+ try {
26
+ return originalShowLoadedResources.apply(this, args);
27
+ } finally {
28
+ resourceLoader.getSkills = originalGetSkills;
29
+ }
30
+ };
31
+ prototype[STARTUP_PATCH_SYMBOL] = true;
32
+ }