dsh-desktop-kit 1.0.0 → 2.0.1

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 (144) hide show
  1. package/README.md +106 -41
  2. package/client.js +780 -28
  3. package/docs/ARCHITECTURE.md +114 -0
  4. package/docs/COMPATIBILITY.md +61 -0
  5. package/docs/INSTALL.md +97 -0
  6. package/docs/PRIVACY.md +50 -0
  7. package/docs/ROADMAP.md +57 -0
  8. package/docs/TOKENS.md +118 -0
  9. package/docs/TROUBLESHOOTING.md +68 -0
  10. package/docs/images/hero.png +0 -0
  11. package/index.js +4 -0
  12. package/package.json +15 -5
  13. package/plugins/dsh-gkb-ru-ui/lib/client.js +276 -28
  14. package/plugins/dsh-gkb-ru-ui/lib/index.js +45 -1
  15. package/plugins/dsh-gkb-ru-ui/lib/skills.js +195 -1
  16. package/plugins/dsh-gkb-ru-ui/package.json +2 -2
  17. package/plugins/dsh-gkb-ru-ui/screenshots/01-home-tiles-1440-light.png +0 -0
  18. package/plugins/dsh-gkb-ru-ui/screenshots/02-skills-1440-light.png +0 -0
  19. package/plugins/dsh-gkb-ru-ui/screenshots/03-skills-editor-1440-light.png +0 -0
  20. package/plugins/dsh-gkb-ru-ui/screenshots/04-skills-1440-dark.png +0 -0
  21. package/plugins/dsh-gkb-ru-ui/screenshots/05-skills-390-light.png +0 -0
  22. package/plugins/dsh-gkb-ru-ui/screenshots/06-home-tiles-1440-dark.png +0 -0
  23. package/plugins/dsh-peak-guard/screenshots/01-real-time-peak-start.png +0 -0
  24. package/plugins/dsh-peak-guard/screenshots/02-real-time-peak-end.png +0 -0
  25. package/plugins/dsh-peak-guard/screenshots/03-peak-card-1440-light.png +0 -0
  26. package/plugins/dsh-peak-guard/screenshots/04-peak-card-1440-dark.png +0 -0
  27. package/plugins/dsh-peak-guard/screenshots/05-peak-card-390-light.png +0 -0
  28. package/plugins/dsh-peak-guard/screenshots/05-peak-card-768-light.png +0 -0
  29. package/plugins/dsh-peak-guard/screenshots/06-peak-card-390-light.png +0 -0
  30. package/plugins/dsh-peak-guard/screenshots/07-live-preview-check.png +0 -0
  31. package/plugins/dsh-peak-guard/screenshots/07-pre-peak-1440-light.png +0 -0
  32. package/plugins/dsh-peak-guard/screenshots/08-peak-start-1440-light.png +0 -0
  33. package/plugins/dsh-peak-guard/screenshots/09-peak-end-1440-light.png +0 -0
  34. package/plugins/dsh-peak-guard/screenshots/10-pre-peak-card-1440-light.png +0 -0
  35. package/plugins/dsh-peak-guard/screenshots/11-pre-peak-card-1440-dark.png +0 -0
  36. package/plugins/dsh-peak-guard/screenshots/12-pre-peak-card-390-light.png +0 -0
  37. package/plugins/dsh-peak-guard/screenshots/qa-report.json +74 -0
  38. package/plugins/dsh-profile-transfer/screenshots/01-settings-transfer-1440-light.png +0 -0
  39. package/plugins/dsh-profile-transfer/screenshots/02-settings-transfer-preview-1440-light.png +0 -0
  40. package/plugins/dsh-profile-transfer/screenshots/03-settings-transfer-1440-dark.png +0 -0
  41. package/plugins/dsh-profile-transfer/screenshots/04-settings-transfer-390-light.png +0 -0
  42. package/plugins/dsh-profile-transfer/screenshots/05-settings-transfer-768-light.png +0 -0
  43. package/plugins/dsh-profile-transfer/screenshots/06-settings-transfer-390-dark.png +0 -0
  44. package/plugins/dsh-profile-transfer/screenshots/07-settings-transfer-export-result.png +0 -0
  45. package/plugins/dsh-profile-transfer/screenshots/08-settings-transfer-workspaces-1440-light.png +0 -0
  46. package/plugins/dsh-profile-transfer/screenshots/09-settings-transfer-workspaces-1440-dark.png +0 -0
  47. package/plugins/dsh-profile-transfer/screenshots/10-settings-transfer-workspaces-390-light.png +0 -0
  48. package/plugins/dsh-session-move/screenshots/01-row-menu-1440-light.png +0 -0
  49. package/plugins/dsh-session-move/screenshots/02-dialog-1440-light.png +0 -0
  50. package/plugins/dsh-session-move/screenshots/03-dialog-390-light.png +0 -0
  51. package/plugins/dsh-session-move/screenshots/04-dialog-1440-dark.png +0 -0
  52. package/plugins/dsh-session-move/screenshots/05-drag-highlight-1440-light.png +0 -0
  53. package/plugins/dsh-session-move/screenshots/06-drop-ungrouped-highlight.png +0 -0
  54. package/plugins/dsh-session-move/screenshots/07-after-drop-ungrouped.png +0 -0
  55. package/plugins/dsh-session-move/screenshots/08-move-dialog.png +0 -0
  56. package/plugins/dsh-session-move/screenshots/09-moved-to-second-workspace.png +0 -0
  57. package/plugins/dsh-session-move/screenshots/qa-report.json +62 -0
  58. package/plugins/dsh-skill-autopilot/cordis.patch.yml +3 -0
  59. package/plugins/dsh-skill-autopilot/index.js +444 -0
  60. package/plugins/dsh-skill-autopilot/package.json +19 -0
  61. package/plugins/dsh-token-lab/cordis.patch.yml +3 -0
  62. package/plugins/dsh-token-lab/lib/client.js +504 -0
  63. package/plugins/dsh-token-lab/lib/index.js +275 -0
  64. package/plugins/dsh-token-lab/package.json +26 -0
  65. package/plugins/dsh-token-lab/screenshots/01-token-lab-1440-light.png +0 -0
  66. package/plugins/dsh-token-lab/screenshots/02-token-lab-1440-dark.png +0 -0
  67. package/skills/code-review-checklist/SKILL.md +48 -30
  68. package/skills/content-writing/SKILL.md +64 -327
  69. package/skills/content-writing/references/process.md +123 -0
  70. package/skills/content-writing/references/quick-recipes.md +99 -0
  71. package/skills/content-writing/references/rules.md +85 -0
  72. package/skills/content-writing/references/sources.md +49 -0
  73. package/skills/context-economy/SKILL.md +60 -0
  74. package/skills/context-economy/references/long-session.md +35 -0
  75. package/skills/context-economy/references/token-math.md +42 -0
  76. package/skills/context-economy/references/tool-recipes.md +34 -0
  77. package/skills/database-safety/SKILL.md +40 -24
  78. package/skills/debugging-playbook/SKILL.md +30 -27
  79. package/skills/design-critique/SKILL.md +57 -110
  80. package/skills/design-critique/references/a11y.md +25 -0
  81. package/skills/design-critique/references/diagnoses.md +33 -0
  82. package/skills/design-critique/references/quick-tests.md +30 -0
  83. package/skills/design-critique/references/report-template.md +63 -0
  84. package/skills/design-critique/references/rubric-fast.md +73 -0
  85. package/skills/dev-workflow/SKILL.md +41 -34
  86. package/skills/dev-workflow/references/definition-of-done.md +44 -0
  87. package/skills/dev-workflow/references/memory-and-boundaries.md +31 -0
  88. package/skills/dev-workflow/references/stages.md +67 -0
  89. package/skills/git-and-ci/SKILL.md +46 -26
  90. package/skills/pg-upgrade-verify/SKILL.md +54 -35
  91. package/skills/project-deep-dive/SKILL.md +39 -164
  92. package/skills/project-deep-dive/references/recon-commands.md +12 -0
  93. package/skills/project-deep-dive/references/section-templates.md +2 -0
  94. package/skills/project-deep-dive/references/tool-and-process.md +86 -0
  95. package/skills/project-memory-maintenance/SKILL.md +39 -154
  96. package/skills/project-memory-maintenance/references/api-and-checklist.md +99 -0
  97. package/skills/project-memory-maintenance/references/journal-template.md +2 -0
  98. package/skills/project-memory-maintenance/references/section-rules.md +2 -0
  99. package/skills/release-and-deploy/SKILL.md +46 -32
  100. package/skills/requirements-and-planning/SKILL.md +44 -110
  101. package/skills/requirements-and-planning/references/antipatterns-and-checklist.md +20 -0
  102. package/skills/requirements-and-planning/references/process-steps.md +52 -0
  103. package/skills/requirements-and-planning/references/templates-and-estimates.md +29 -0
  104. package/skills/security-review/SKILL.md +33 -29
  105. package/skills/seo/SKILL.md +53 -305
  106. package/skills/seo/references/antipatterns.md +66 -0
  107. package/skills/seo/references/audit-checklist.md +14 -0
  108. package/skills/seo/references/content-seo.md +180 -174
  109. package/skills/seo/references/eeat-medical.md +2 -0
  110. package/skills/seo/references/measurement.md +28 -1
  111. package/skills/seo/references/on-page.md +5 -0
  112. package/skills/seo/references/page-templates.md +56 -0
  113. package/skills/seo/references/stack-recipes.md +2 -0
  114. package/skills/seo/references/structured-data.md +2 -0
  115. package/skills/seo/references/technical-seo.md +2 -0
  116. package/skills/service-desk-conventions/SKILL.md +40 -28
  117. package/skills/service-desk-db-safety/SKILL.md +39 -28
  118. package/skills/service-desk-release/SKILL.md +40 -22
  119. package/skills/service-desk-testing/SKILL.md +47 -33
  120. package/skills/service-desk-ui/SKILL.md +43 -24
  121. package/skills/skills-router/SKILL.md +44 -82
  122. package/skills/skills-router/references/applying-skills.md +25 -0
  123. package/skills/skills-router/references/routing-matrix.md +52 -0
  124. package/skills/testing-strategy/SKILL.md +47 -32
  125. package/skills/ui-design-system/SKILL.md +43 -38
  126. package/skills/ui-design-system/references/accessibility-and-responsive.md +50 -0
  127. package/skills/ui-design-system/references/tokens-and-components.md +67 -0
  128. package/skills/ui-design-system/references/visual-qa.md +44 -0
  129. package/skills/web-research/SKILL.md +40 -33
  130. package/skills/web-research/references/answer-and-citations.md +60 -0
  131. package/skills/web-research/references/query-recipes.md +44 -0
  132. package/skills/wow-design/SKILL.md +82 -161
  133. package/skills/wow-design/references/anti-patterns.md +3 -0
  134. package/skills/wow-design/references/critique-rubric.md +3 -0
  135. package/skills/wow-design/references/design-brief-template.md +2 -0
  136. package/skills/wow-design/references/inspiration.md +3 -0
  137. package/skills/wow-design/references/presets.md +3 -0
  138. package/skills/wow-design/references/principles.md +3 -0
  139. package/skills/wow-design/references/quick-presets.md +50 -0
  140. package/skills/wow-design/references/report-template.md +21 -0
  141. package/skills/wow-design/references/stack-recipes.md +3 -0
  142. package/skills/wow-design/references/visual-qa.md +3 -0
  143. package/skills/wow-design/references/wow-levers.md +43 -0
  144. package/tools/skill-cost.mjs +76 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * dsh-skill-autopilot — skills that apply themselves.
3
+ *
4
+ * The problem: a skill catalog only lists names and descriptions. The model
5
+ * still has to remember to call `skill`, and when it does it usually loads the
6
+ * entire body. Both cost tokens and both get skipped under pressure.
7
+ *
8
+ * This host plugin reads the current user message on every prompt assembly and
9
+ * injects a tiny `<skill-autopilot>` context block naming the exact skills that
10
+ * match the request, with their token prices, plus the rule "load the core
11
+ * only". No match — no block at all, so an unrelated chat pays zero tokens.
12
+ *
13
+ * The matched skill names are filtered against the skills that actually exist
14
+ * in the DSH home, so a stale rule can never send the model after a missing
15
+ * skill.
16
+ */
17
+ import fs from "node:fs";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+
21
+ export const inject = ["systemPrompt"];
22
+
23
+ const STATE_FILE = "dsh-autopilot.json";
24
+ const MAX_HINT_TOKENS = 140;
25
+ const SKILL_CACHE_MS = 60_000;
26
+
27
+ /* ------------------------------------------------------------------ */
28
+ /* intent rules */
29
+ /* ------------------------------------------------------------------ */
30
+
31
+ /**
32
+ * `any` — at least one substring matches. `all` — every group matches at least
33
+ * one substring (used only where it removes obvious false positives).
34
+ * Substrings are compared against lowercased message text.
35
+ */
36
+ const RULES = [
37
+ {
38
+ id: "ui",
39
+ label: "UI/дизайн",
40
+ any: ["вёрстк", "верстк", "дизайн", "интерфейс", " ui ", "ux", "лендинг", "страниц", "компонент", "адаптив", "css", "tailwind", "макет", "красив", "темн", "светл", "кнопк", "форм"],
41
+ skills: ["wow-design", "ui-design-system"],
42
+ follow: { any: ["критик", "аудит", "оцен", "улучш", "передела"], skills: ["design-critique"] },
43
+ },
44
+ {
45
+ id: "seo",
46
+ label: "SEO",
47
+ any: ["seo", "поисков", "мета-тег", "метатег", "title", "description", "schema", "json-ld", "разметк", "индексац", "sitemap", "robots", "яндекс", "google"],
48
+ skills: ["seo"],
49
+ follow: { any: ["страниц", "лендинг", "вёрстк", "верстк", "интерфейс"], skills: ["wow-design"] },
50
+ },
51
+ {
52
+ id: "content",
53
+ label: "Текст/контент",
54
+ any: ["стать", "пост", "контент", "пресс-релиз", "новост", "заголов", "редакт", "текст для", "написать текст", "объявлен"],
55
+ skills: ["content-writing"],
56
+ },
57
+ {
58
+ id: "research",
59
+ label: "Актуальные факты",
60
+ any: ["актуал", "новост", "цена", "курс", "верси", "закон", "документац", "сравни", "найди в интернете", "последн", "сегодня", "2026", "рынок"],
61
+ skills: ["web-research"],
62
+ },
63
+ {
64
+ id: "plan",
65
+ label: "Планирование",
66
+ any: ["план", "тз", "требован", "декомпоз", "оцен", "архитектур", "вариант", "исследован", "предложи", "концепц"],
67
+ skills: ["requirements-and-planning", "dev-workflow"],
68
+ },
69
+ {
70
+ id: "debug",
71
+ label: "Отладка",
72
+ any: ["ошибк", "баг", "не работает", "падает", "сломал", "стектрейс", "exception", "traceback", "debug", "почему не"],
73
+ skills: ["debugging-playbook"],
74
+ follow: { any: ["тест", "проверь"], skills: ["testing-strategy"] },
75
+ },
76
+ {
77
+ id: "db",
78
+ label: "База данных",
79
+ any: ["база данных", "postgres", " postgresql", " sql", "миграц", "таблиц", "схем", "бэкап", "pg_dump", "truncate", "данные"],
80
+ skills: ["database-safety"],
81
+ follow: { any: ["обновлен", "апгрейд", "upgrade"], skills: ["pg-upgrade-verify"] },
82
+ },
83
+ {
84
+ id: "security",
85
+ label: "Безопасность",
86
+ any: ["безопасн", "авториз", "аутентиф", "csrf", "xss", "инъекц", "права доступа", "роли", "секрет", "уязвим"],
87
+ skills: ["security-review"],
88
+ },
89
+ {
90
+ id: "test",
91
+ label: "Тесты",
92
+ any: ["тест", "phpunit", "playwright", "e2e", "покрыт", "регресс", "baseline", "фикстур"],
93
+ skills: ["testing-strategy"],
94
+ },
95
+ {
96
+ id: "release",
97
+ label: "Релиз",
98
+ any: ["релиз", "деплой", "выкат", "publish", "npm pack", "сборк", "артефакт", "откат"],
99
+ skills: ["release-and-deploy", "git-and-ci"],
100
+ },
101
+ {
102
+ id: "git",
103
+ label: "Git/CI",
104
+ any: ["git", "ветк", "коммит", "merge", "rebase", "pull request", " pr ", "ci ", "conflict"],
105
+ skills: ["git-and-ci"],
106
+ },
107
+ {
108
+ id: "memory",
109
+ label: "Память проекта",
110
+ any: ["памят", "досье", "запомни", "project_memory", "журнал", "контекст проекта"],
111
+ skills: ["project-memory-maintenance", "project-deep-dive"],
112
+ },
113
+ {
114
+ id: "review",
115
+ label: "Ревью",
116
+ any: ["ревью", "code review", "review", "аудит код", "проверь код", "чек-лист"],
117
+ skills: ["code-review-checklist"],
118
+ },
119
+ {
120
+ id: "service-desk",
121
+ label: "Service Desk",
122
+ any: ["service desk", "заявк", "гкб", "servicedesk"],
123
+ skills: ["service-desk-conventions"],
124
+ follow: { any: ["бд", "миграц", "postgres"], skills: ["service-desk-db-safety"] },
125
+ },
126
+ {
127
+ id: "tokens",
128
+ label: "Экономия контекста",
129
+ any: ["токен", "контекст", "расход", "сжат", "compact", "compact", "лимит контекста"],
130
+ skills: ["context-economy"],
131
+ },
132
+ {
133
+ id: "skills",
134
+ label: "Навыки",
135
+ any: ["навык", "скилл", "skill", "skills", "каталог навыков"],
136
+ skills: ["skills-router", "context-economy"],
137
+ },
138
+ ];
139
+
140
+ /* ------------------------------------------------------------------ */
141
+ /* small helpers */
142
+ /* ------------------------------------------------------------------ */
143
+
144
+ function dshHome() {
145
+ return process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
146
+ }
147
+
148
+ function readState() {
149
+ const file = path.join(dshHome(), STATE_FILE);
150
+ try {
151
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
152
+ return {
153
+ mode: parsed?.mode === "off" ? "off" : "on",
154
+ maxSkills: Number.isFinite(parsed?.maxSkills) ? Math.max(1, Math.min(5, Math.trunc(parsed.maxSkills))) : 3,
155
+ };
156
+ } catch {
157
+ return { mode: process.env.DSH_AUTOPILOT === "off" ? "off" : "on", maxSkills: 3 };
158
+ }
159
+ }
160
+
161
+ function writeState(patch) {
162
+ const next = { ...readState(), ...patch };
163
+ next.maxSkills = Math.max(1, Math.min(5, Math.trunc(next.maxSkills)));
164
+ const file = path.join(dshHome(), STATE_FILE);
165
+ try {
166
+ fs.mkdirSync(path.dirname(file), { recursive: true });
167
+ fs.writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, "utf8");
168
+ } catch {
169
+ /* best effort */
170
+ }
171
+ return next;
172
+ }
173
+
174
+ function approxTokens(bytes) {
175
+ return Math.max(40, Math.round(bytes / 3.2));
176
+ }
177
+
178
+ function formatTokens(tokens) {
179
+ if (tokens >= 1000) return `${(tokens / 1000).toFixed(1).replace(".0", "")}k`;
180
+ return String(tokens);
181
+ }
182
+
183
+ /** Map of skill name -> approx SKILL.md tokens, cached for a minute. */
184
+ let skillCache = { at: 0, map: new Map() };
185
+ function loadSkillCosts() {
186
+ const now = Date.now();
187
+ if (now - skillCache.at < SKILL_CACHE_MS) return skillCache.map;
188
+ const map = new Map();
189
+ const directory = path.join(dshHome(), "skills");
190
+ let entries = [];
191
+ try {
192
+ entries = fs.readdirSync(directory, { withFileTypes: true });
193
+ } catch {
194
+ entries = [];
195
+ }
196
+ for (const entry of entries) {
197
+ if (!entry.isDirectory()) continue;
198
+ const file = path.join(directory, entry.name, "SKILL.md");
199
+ try {
200
+ const stat = fs.statSync(file);
201
+ map.set(entry.name, approxTokens(stat.size));
202
+ } catch {
203
+ /* not a skill folder */
204
+ }
205
+ }
206
+ skillCache = { at: now, map };
207
+ return map;
208
+ }
209
+
210
+ /** The latest durable user message of a session, read from the surface tail. */
211
+ const lastMessageCache = new Map();
212
+ function latestUserMessage(session) {
213
+ if (!session) return null;
214
+ const id = String(session.id);
215
+ const cached = lastMessageCache.get(id);
216
+ let lastSeq = 0;
217
+ try {
218
+ lastSeq = Number(session.seq) || 0;
219
+ } catch {
220
+ lastSeq = 0;
221
+ }
222
+ if (cached && cached.seq === lastSeq) return cached.value;
223
+ let value = null;
224
+ try {
225
+ const nodes = session.surface?.nodes ?? [];
226
+ for (let index = nodes.length - 1; index >= 0; index -= 1) {
227
+ const event = session.eventAt(nodes[index]);
228
+ if (!event || event.type !== "user/message") continue;
229
+ const text = messageText(event.data?.message);
230
+ if (text.length > 0) {
231
+ value = { seq: Number(event.seq), text };
232
+ break;
233
+ }
234
+ }
235
+ } catch {
236
+ value = null;
237
+ }
238
+ lastMessageCache.set(id, { seq: lastSeq, value });
239
+ return value;
240
+ }
241
+
242
+ function messageText(message) {
243
+ if (!message) return "";
244
+ const content = message.content;
245
+ if (typeof content === "string") return content;
246
+ if (!Array.isArray(content)) return "";
247
+ const parts = [];
248
+ for (const block of content) {
249
+ if (block && block.type === "text" && typeof block.text === "string") parts.push(block.text);
250
+ }
251
+ return parts.join("\n").trim();
252
+ }
253
+
254
+ function matches(text, needles) {
255
+ return needles.some((needle) => text.includes(needle));
256
+ }
257
+
258
+ /** Pick rules and skills for a message. Pure and exported for tests. */
259
+ export function routeMessage(rawText, available = loadSkillCosts(), maxSkills = 3) {
260
+ const text = ` ${String(rawText ?? "").toLowerCase()} `;
261
+ if (text.trim().length === 0) return { intents: [], skills: [] };
262
+ const hits = [];
263
+ for (const rule of RULES) {
264
+ if (!matches(text, rule.any)) continue;
265
+ const names = [...rule.skills];
266
+ if (rule.follow && matches(text, rule.follow.any)) names.push(...rule.follow.skills);
267
+ hits.push({ id: rule.id, label: rule.label, names });
268
+ }
269
+ if (hits.length === 0) return { intents: [], skills: [] };
270
+ /* Prefer skills from the most specific (later, rarer) rules? Keep rule order:
271
+ * it is ordered from the most common task families to the narrow ones. */
272
+ const ordered = [];
273
+ const seen = new Set();
274
+ for (const hit of hits) {
275
+ for (const name of hit.names) {
276
+ if (seen.has(name) || !available.has(name)) continue;
277
+ seen.add(name);
278
+ ordered.push({ name, tokens: available.get(name) });
279
+ }
280
+ }
281
+ const skills = ordered.sort((left, right) => (right.tokens || 0) - (left.tokens || 0)).slice(0, maxSkills);
282
+ return { intents: hits.map((hit) => hit.label), skills };
283
+ }
284
+
285
+ function renderHint(route) {
286
+ if (route.skills.length === 0) return "";
287
+ const skillList = route.skills.map((skill) => `${skill.name} ≈${formatTokens(skill.tokens)} токенов`).join(", ");
288
+ return [
289
+ "<skill-autopilot>",
290
+ `Запрос похож на: ${route.intents.join(" + ")}.`,
291
+ `Загрузи через tool skill только эти ядра: ${skillList}.`,
292
+ "Это ядра навыков: references внутри них грузи только при реальной необходимости.",
293
+ "Другие навыки и их references сейчас не нужны — не загружай их «на всякий случай».",
294
+ "</skill-autopilot>",
295
+ ].join("\n");
296
+ }
297
+
298
+ /* ------------------------------------------------------------------ */
299
+ /* plugin */
300
+ /* ------------------------------------------------------------------ */
301
+
302
+ export function apply(ctx) {
303
+ const logger = ctx.logger;
304
+ const lastMatches = new Map();
305
+ let lastHintAt = 0;
306
+
307
+ const connection = ctx.get("connection");
308
+ const routeBase = "/api/dsh-skill-autopilot";
309
+ const namespace = "dsh-skill-autopilot";
310
+ const sessions = () => ctx.get("sessions");
311
+
312
+ function reply(rpcId, value) {
313
+ return Response.json({ type: "server-response", rpcId, result: { ok: true, value } });
314
+ }
315
+ function fail(rpcId, code, message) {
316
+ return Response.json({ type: "server-response", rpcId, result: { ok: false, error: { code, message } } });
317
+ }
318
+
319
+ if (connection?.fetch?.register) {
320
+ const stateRoute = {
321
+ path: `${routeBase}/state`,
322
+ methods: ["POST"],
323
+ requestBody: "buffered",
324
+ fetch: async (request) => {
325
+ let rpcId = "invalid";
326
+ try {
327
+ const body = await request.json();
328
+ rpcId = typeof body?.rpcId === "string" ? body.rpcId : rpcId;
329
+ if (body?.method !== `${namespace}/state`) return fail(rpcId, "autopilot/bad-request", "method mismatch");
330
+ const state = readState();
331
+ const costs = loadSkillCosts();
332
+ const skills = [...costs.entries()].map(([name, tokens]) => ({ name, tokens })).sort((a, b) => b.tokens - a.tokens);
333
+ const matches = [...lastMatches.values()].sort((a, b) => b.at - a.at).slice(0, 5);
334
+ return reply(rpcId, {
335
+ ...state,
336
+ rules: RULES.length,
337
+ library: {
338
+ count: skills.length,
339
+ totalTokens: skills.reduce((sum, skill) => sum + skill.tokens, 0),
340
+ },
341
+ skills,
342
+ lastMatch: matches[0] ?? null,
343
+ matches,
344
+ });
345
+ } catch (error) {
346
+ return fail(rpcId, "autopilot/internal", String(error));
347
+ }
348
+ },
349
+ };
350
+ const setRoute = {
351
+ path: `${routeBase}/set`,
352
+ methods: ["POST"],
353
+ requestBody: "buffered",
354
+ fetch: async (request) => {
355
+ let rpcId = "invalid";
356
+ try {
357
+ const body = await request.json();
358
+ rpcId = typeof body?.rpcId === "string" ? body.rpcId : rpcId;
359
+ if (body?.method !== `${namespace}/set`) return fail(rpcId, "autopilot/bad-request", "method mismatch");
360
+ const payload = body?.payload ?? {};
361
+ const patch = {};
362
+ if (payload.mode === "on" || payload.mode === "off") patch.mode = payload.mode;
363
+ if (Number.isFinite(payload.maxSkills)) patch.maxSkills = payload.maxSkills;
364
+ const next = writeState(patch);
365
+ return reply(rpcId, next);
366
+ } catch (error) {
367
+ return fail(rpcId, "autopilot/internal", String(error));
368
+ }
369
+ },
370
+ };
371
+ const testRoute = {
372
+ path: `${routeBase}/route`,
373
+ methods: ["POST"],
374
+ requestBody: "buffered",
375
+ fetch: async (request) => {
376
+ let rpcId = "invalid";
377
+ try {
378
+ const body = await request.json();
379
+ rpcId = typeof body?.rpcId === "string" ? body.rpcId : rpcId;
380
+ if (body?.method !== `${namespace}/route`) return fail(rpcId, "autopilot/bad-request", "method mismatch");
381
+ const payload = body?.payload ?? {};
382
+ const route = routeMessage(String(payload.text ?? ""), loadSkillCosts(), readState().maxSkills);
383
+ return reply(rpcId, { ...route, hint: renderHint(route) });
384
+ } catch (error) {
385
+ return fail(rpcId, "autopilot/internal", String(error));
386
+ }
387
+ },
388
+ };
389
+ for (const route of [stateRoute, setRoute, testRoute]) {
390
+ ctx.effect(() => connection.fetch.register(route), `skill-autopilot: ${route.path}`);
391
+ }
392
+ } else {
393
+ logger?.debug?.("[skill-autopilot] no connection service; RPC disabled");
394
+ }
395
+
396
+ /* The routing hint itself: recomputed per prompt assembly, empty when idle. */
397
+ ctx.systemPrompt.context({
398
+ name: "dsh-desktop-kit:skill-autopilot",
399
+ order: 132,
400
+ interpolate: false,
401
+ required: false,
402
+ text: (assemblyContext) => {
403
+ try {
404
+ if (readState().mode === "off") return "";
405
+ const agent = assemblyContext?.agent;
406
+ if (!agent) return "";
407
+ const session = agent.session ?? sessions()?.get(agent.id);
408
+ if (!session) return "";
409
+ if (session.header?.origin === "subagent") return "";
410
+ const message = latestUserMessage(session);
411
+ if (!message) return "";
412
+ const route = routeMessage(message.text, loadSkillCosts(), readState().maxSkills);
413
+ if (route.skills.length === 0) return "";
414
+ const hint = renderHint(route);
415
+ if (hint.length > MAX_HINT_TOKENS * 4) return "";
416
+ const now = Date.now();
417
+ lastHintAt = now;
418
+ lastMatches.set(String(session.id), {
419
+ sessionId: String(session.id),
420
+ at: new Date(now).toISOString(),
421
+ intents: route.intents,
422
+ skills: route.skills,
423
+ chars: hint.length,
424
+ });
425
+ if (lastMatches.size > 40) {
426
+ const oldest = [...lastMatches.entries()].sort((a, b) => a[1].at.localeCompare(b[1].at))[0];
427
+ if (oldest) lastMatches.delete(oldest[0]);
428
+ }
429
+ return hint;
430
+ } catch (error) {
431
+ logger?.warn?.(`[skill-autopilot] route failed: ${String(error)}`);
432
+ return "";
433
+ }
434
+ },
435
+ });
436
+
437
+ ctx.effect(() => {
438
+ logger?.info?.(`[skill-autopilot] ${RULES.length} rules, mode=${readState().mode}`);
439
+ return () => {
440
+ lastMessageCache.clear();
441
+ lastMatches.clear();
442
+ };
443
+ }, "skill-autopilot: lifecycle");
444
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "dsh-skill-autopilot",
3
+ "version": "1.0.0",
4
+ "description": "Skill autopilot for DSH: reads the current user message and injects a ~80-token routing hint naming the exact skills to load, so skills apply themselves without bloating the context.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./index.js"
10
+ },
11
+ "./package.json": "./package.json"
12
+ },
13
+ "dsh": {
14
+ "manifestVersion": 1,
15
+ "bundle": {
16
+ "patch": "./cordis.patch.yml"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-token-lab
3
+ name: dsh-token-lab