mulmoclaude 0.6.2 → 0.6.4
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 +26 -0
- package/bin/mulmoclaude.js +11 -1
- package/client/assets/JsonEditor-D6WBWLoa.js +10 -0
- package/client/assets/JsonEditor-Di5xGeZY.css +1 -0
- package/client/assets/_plugin-vue_export-helper-BOai-rQB.js +1 -0
- package/client/assets/chunk-D8eiyYIV-LcKZGJv5.js +1 -0
- package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-XVrO-eyz.js} +1 -1
- package/client/assets/index-CyBr8Mkr.css +2 -0
- package/client/assets/index-zZIqEbNX.js +5106 -0
- package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DHT6q10o.js} +1 -1
- package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-D6kcV0wa.js +1 -0
- package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
- package/client/assets/{vue-C8UuIO9J.js → vue-D4w8THF_.js} +1 -1
- package/client/assets/vue-i18n-CQbxVmNs.js +3 -0
- package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
- package/client/index.html +10 -10
- package/package.json +9 -8
- package/server/agent/backend/claude-code.ts +34 -0
- package/server/agent/backend/fake-echo.ts +370 -0
- package/server/agent/backend/index.ts +16 -1
- package/server/agent/config.ts +74 -24
- package/server/agent/index.ts +104 -80
- package/server/agent/mcpFailureMonitor.ts +167 -0
- package/server/agent/mcpPreflight.ts +185 -0
- package/server/agent/prompt.ts +50 -359
- package/server/agent/stdioHttpShim.ts +171 -0
- package/server/agent/stream.ts +12 -1
- package/server/api/routes/encore.ts +55 -0
- package/server/api/routes/files.ts +22 -0
- package/server/api/routes/mulmo-script.ts +19 -1
- package/server/api/routes/schedulerHandlers.ts +52 -4
- package/server/api/routes/sessions.ts +15 -0
- package/server/api/routes/skills.ts +263 -0
- package/server/build/dispatcher.mjs +299 -0
- package/server/encore/INVARIANTS.md +272 -0
- package/server/encore/boot.ts +39 -0
- package/server/encore/closure.ts +36 -0
- package/server/encore/cycle.ts +276 -0
- package/server/encore/dispatch.ts +103 -0
- package/server/encore/handlers/amend.ts +99 -0
- package/server/encore/handlers/appendNote.ts +74 -0
- package/server/encore/handlers/defineEncore.ts +42 -0
- package/server/encore/handlers/listTickets.ts +107 -0
- package/server/encore/handlers/markStepDone.ts +41 -0
- package/server/encore/handlers/markTargetSkipped.ts +33 -0
- package/server/encore/handlers/query.ts +138 -0
- package/server/encore/handlers/recordValues.ts +44 -0
- package/server/encore/handlers/resolveNotification.ts +121 -0
- package/server/encore/handlers/setup.ts +81 -0
- package/server/encore/handlers/shared.ts +137 -0
- package/server/encore/handlers/snooze.ts +87 -0
- package/server/encore/handlers/startObligationChat.ts +64 -0
- package/server/encore/handlers/startSetupChat.ts +50 -0
- package/server/encore/lock.ts +61 -0
- package/server/encore/notifier.ts +123 -0
- package/server/encore/obligation.ts +25 -0
- package/server/encore/paths.ts +78 -0
- package/server/encore/reconcile.ts +661 -0
- package/server/encore/tick.ts +191 -0
- package/server/encore/yaml-fm.ts +63 -0
- package/server/events/notifications.ts +19 -91
- package/server/index.ts +94 -9
- package/server/notifier/engine.ts +102 -1
- package/server/notifier/macosReminderAdapter.ts +30 -0
- package/server/notifier/runtime-api.ts +41 -1
- package/server/notifier/types.ts +15 -2
- package/server/plugins/runtime.ts +11 -2
- package/server/prompts/index.ts +39 -0
- package/server/prompts/system/journal-pointer.md +12 -0
- package/server/prompts/system/memory-management-atomic.md +33 -0
- package/server/prompts/system/memory-management-topic.md +60 -0
- package/server/prompts/system/news-concierge.md +24 -0
- package/server/prompts/system/sandbox-tools.md +10 -0
- package/server/prompts/system/sources-context.md +16 -0
- package/server/prompts/system/system.md +91 -0
- package/server/system/announceOptionalDeps.ts +57 -0
- package/server/system/appVersion.ts +34 -0
- package/server/system/config.ts +17 -1
- package/server/system/docker.ts +14 -6
- package/server/system/env.ts +18 -5
- package/server/system/optionalDeps.ts +129 -0
- package/server/utils/cli-flags.d.mts +14 -0
- package/server/utils/cli-flags.mjs +53 -0
- package/server/utils/files/encore-io.ts +111 -0
- package/server/utils/time.ts +6 -0
- package/server/workspace/helps/business.md +2 -2
- package/server/workspace/helps/encore-dsl.md +482 -0
- package/server/workspace/helps/index.md +15 -13
- package/server/workspace/helps/mulmoscript.md +3 -3
- package/server/workspace/helps/sandbox.md +2 -2
- package/server/workspace/hooks/dispatcher.ts +7 -5
- package/server/workspace/hooks/provision.ts +6 -3
- package/server/workspace/paths.ts +13 -4
- package/server/workspace/skills/catalog.ts +355 -0
- package/server/workspace/skills/external/catalog.ts +283 -0
- package/server/workspace/skills/external/clone.ts +129 -0
- package/server/workspace/skills/external/id.ts +194 -0
- package/server/workspace/skills/external/install.ts +417 -0
- package/server/workspace/skills/external/presets.ts +50 -0
- package/server/workspace/skills-preset.ts +29 -17
- package/server/workspace/workspace.ts +10 -5
- package/src/App.vue +37 -8
- package/src/components/FileContentRenderer.vue +102 -9
- package/src/components/JsonEditor.vue +160 -0
- package/src/components/NotificationBell.vue +35 -3
- package/src/components/PluginLauncher.vue +20 -41
- package/src/components/RightSidebar.vue +19 -0
- package/src/components/SettingsMcpTab.vue +58 -11
- package/src/components/SettingsModal.vue +22 -1
- package/src/components/StackView.vue +10 -1
- package/src/components/TodoExplorer.vue +16 -0
- package/src/components/todo/TodoKanbanView.vue +34 -6
- package/src/composables/useNotifications.ts +21 -1
- package/src/config/apiRoutes.ts +0 -6
- package/src/config/mcpCatalog.ts +12 -7
- package/src/config/mcpTypes.ts +5 -0
- package/src/config/roles.ts +52 -15
- package/src/config/systemFileDescriptors.ts +12 -0
- package/src/lang/de.ts +108 -12
- package/src/lang/en.ts +105 -11
- package/src/lang/es.ts +106 -11
- package/src/lang/fr.ts +106 -11
- package/src/lang/ja.ts +104 -11
- package/src/lang/ko.ts +105 -11
- package/src/lang/pt-BR.ts +106 -11
- package/src/lang/zh.ts +103 -11
- package/src/main.ts +1 -0
- package/src/plugins/_generated/metas.ts +4 -0
- package/src/plugins/_generated/registrations.ts +2 -0
- package/src/plugins/_generated/server-bindings.ts +5 -0
- package/src/plugins/encore/EncoreDashboard.vue +504 -0
- package/src/plugins/encore/EncoreRedirect.vue +116 -0
- package/src/plugins/encore/View.vue +36 -0
- package/src/plugins/encore/defineEncoreDefinition.ts +74 -0
- package/src/plugins/encore/defineEncoreMeta.ts +13 -0
- package/src/plugins/encore/index.ts +93 -0
- package/src/plugins/encore/manageEncoreDefinition.ts +100 -0
- package/src/plugins/encore/manageEncoreMeta.ts +36 -0
- package/src/plugins/manageSkills/View.vue +832 -30
- package/src/plugins/manageSkills/categories.ts +125 -0
- package/src/plugins/manageSkills/meta.ts +30 -0
- package/src/plugins/markdown/definition.ts +3 -3
- package/src/plugins/meta-types.ts +5 -0
- package/src/plugins/presentMulmoScript/Preview.vue +3 -3
- package/src/plugins/presentMulmoScript/View.vue +157 -33
- package/src/plugins/presentMulmoScript/meta.ts +4 -0
- package/src/plugins/scheduler/View.vue +45 -9
- package/src/plugins/scheduler/calendarDefinition.ts +6 -2
- package/src/plugins/scheduler/multiDayHelpers.ts +95 -0
- package/src/plugins/skill/View.vue +1 -5
- package/src/plugins/spreadsheet/View.vue +3 -3
- package/src/plugins/spreadsheet/definition.ts +1 -1
- package/src/plugins/textResponse/Preview.vue +14 -1
- package/src/plugins/textResponse/View.vue +39 -24
- package/src/plugins/wiki/components/WikiPageBody.vue +4 -0
- package/src/router/index.ts +11 -0
- package/src/router/pageRoutes.ts +1 -0
- package/src/types/encore-dsl/at-expression.ts +120 -0
- package/src/types/encore-dsl/at-resolver.ts +32 -0
- package/src/types/encore-dsl/cadence.ts +289 -0
- package/src/types/encore-dsl/schema.ts +288 -0
- package/src/types/notification.ts +2 -1
- package/src/types/session.ts +6 -0
- package/src/types/sse.ts +5 -0
- package/src/types/toolCallHistory.ts +7 -0
- package/src/utils/agent/eventDispatch.ts +26 -5
- package/src/utils/agent/mcpHint.ts +50 -0
- package/src/utils/image/htmlSrcAttrs.ts +117 -13
- package/src/utils/session/sessionEntries.ts +8 -32
- package/client/assets/PluginScopedRoot-YjvQq0Nn.js +0 -3
- package/client/assets/chunk-CernVdwh.js +0 -1
- package/client/assets/chunk-D8eiyYIV-CAXpUwLd.js +0 -1
- package/client/assets/index-BwrlMMHr.js +0 -5005
- package/client/assets/index-CvvNuegU.css +0 -2
- package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-C1To4M3t.js +0 -1
- package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +0 -4
- package/server/api/routes/notifications.ts +0 -195
- package/server/notifier/legacy-adapters.ts +0 -76
- package/server/workspace/hooks/dispatcher.mjs +0 -300
- package/src/composables/useSelectedResult.ts +0 -49
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
import { Router, Request, Response } from "express";
|
|
16
16
|
import { deleteProjectSkill, discoverSkills, saveProjectSkill, updateProjectSkill } from "../../workspace/skills/index.js";
|
|
17
17
|
import type { Skill, SkillSummary } from "../../workspace/skills/index.js";
|
|
18
|
+
import {
|
|
19
|
+
isCatalogSource,
|
|
20
|
+
listCatalogEntries,
|
|
21
|
+
readCatalogEntryDetail,
|
|
22
|
+
readExternalDetailAsCatalog,
|
|
23
|
+
starCatalogEntry,
|
|
24
|
+
starExternalAsCatalog,
|
|
25
|
+
type CatalogEntry,
|
|
26
|
+
type CatalogEntryDetail,
|
|
27
|
+
type CatalogDetailResult,
|
|
28
|
+
type StarResult,
|
|
29
|
+
} from "../../workspace/skills/catalog.js";
|
|
30
|
+
import { installExternalRepo, listInstalledRepos, uninstallExternalRepo, type InstalledRepo } from "../../workspace/skills/external/install.js";
|
|
31
|
+
import { EXTERNAL_PRESETS, type ExternalPresetSuggestion } from "../../workspace/skills/external/presets.js";
|
|
18
32
|
import { workspacePath } from "../../workspace/workspace.js";
|
|
19
33
|
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
20
34
|
import { bindRoute } from "../../utils/router.js";
|
|
@@ -65,6 +79,255 @@ bindRoute(router, API_ROUTES.skills.list, async (_req: Request, res: Response<Sk
|
|
|
65
79
|
});
|
|
66
80
|
});
|
|
67
81
|
|
|
82
|
+
// Catalog endpoints (#1335 PR-B). Reads from
|
|
83
|
+
// `<workspace>/data/skills/catalog/<source>/<slug>/` (populated by
|
|
84
|
+
// `syncPresetSkills`); the star endpoint copies catalog entries
|
|
85
|
+
// into `.claude/skills/<slug>/` so Claude Code's discovery picks
|
|
86
|
+
// them up. Catalog entries themselves are NOT in `.claude/skills/`
|
|
87
|
+
// by design — that's the prompt-bloat fix from #1335.
|
|
88
|
+
//
|
|
89
|
+
// Route ordering matters: these `/catalog*` routes register
|
|
90
|
+
// BEFORE `GET /:name` below because Express matches in
|
|
91
|
+
// registration order. A request for `/catalog` would otherwise
|
|
92
|
+
// land in the detail handler with `req.params.name = "catalog"`
|
|
93
|
+
// and 404. Keep all `/catalog*` specifics ahead of any `:name`
|
|
94
|
+
// parameter route in this file.
|
|
95
|
+
|
|
96
|
+
interface CatalogListResponse {
|
|
97
|
+
entries: CatalogEntry[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface StarResponse {
|
|
101
|
+
starred: true;
|
|
102
|
+
slug: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
bindRoute(router, API_ROUTES.skills.catalogList, async (_req: Request, res: Response<CatalogListResponse>) => {
|
|
106
|
+
const entries = await listCatalogEntries();
|
|
107
|
+
log.info("skills", "catalog list: ok", { count: entries.length });
|
|
108
|
+
res.json({ entries });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
interface CatalogPreviewQuery {
|
|
112
|
+
source?: unknown;
|
|
113
|
+
slug?: unknown;
|
|
114
|
+
/** External-source only — repoId + skillFolder identify the entry
|
|
115
|
+
* in place of `slug`. */
|
|
116
|
+
repoId?: unknown;
|
|
117
|
+
skillFolder?: unknown;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface CatalogPreviewResponse {
|
|
121
|
+
detail: CatalogEntryDetail;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function previewResponse(result: CatalogDetailResult, source: string, ident: string, res: Response<CatalogPreviewResponse | ErrorResponse>): void {
|
|
125
|
+
if (result.kind === "ok") {
|
|
126
|
+
log.info("skills", "catalog preview: ok", { source, slug: result.detail.slug });
|
|
127
|
+
res.json({ detail: result.detail });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (result.kind === "not-found") {
|
|
131
|
+
log.warn("skills", "catalog preview: not found", { source, ident });
|
|
132
|
+
notFound(res, `catalog entry not found: ${result.source}/${result.slug}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
log.warn("skills", "catalog preview: invalid slug", { ident });
|
|
136
|
+
badRequest(res, `invalid slug: ${result.slug}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
bindRoute(
|
|
140
|
+
router,
|
|
141
|
+
API_ROUTES.skills.catalogPreview,
|
|
142
|
+
async (req: Request<object, unknown, unknown, CatalogPreviewQuery>, res: Response<CatalogPreviewResponse | ErrorResponse>) => {
|
|
143
|
+
const { source, slug, repoId, skillFolder } = req.query;
|
|
144
|
+
if (!isCatalogSource(source)) {
|
|
145
|
+
badRequest(res, "source must be a known catalog source");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (source === "external") {
|
|
149
|
+
if (typeof repoId !== "string" || repoId.length === 0 || typeof skillFolder !== "string" || skillFolder.length === 0) {
|
|
150
|
+
badRequest(res, "repoId and skillFolder are required for external preview");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const result = await readExternalDetailAsCatalog(repoId, skillFolder);
|
|
154
|
+
previewResponse(result, source, `${repoId}/${skillFolder}`, res);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (typeof slug !== "string" || slug.length === 0) {
|
|
158
|
+
badRequest(res, "slug is required");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const result = await readCatalogEntryDetail(source, slug);
|
|
162
|
+
previewResponse(result, source, slug, res);
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
function starResponse(result: StarResult, source: string, ident: string, res: Response<StarResponse | ErrorResponse>): void {
|
|
167
|
+
if (result.kind === "starred") {
|
|
168
|
+
log.info("skills", "catalog star: ok", { source, slug: result.slug });
|
|
169
|
+
res.json({ starred: true, slug: result.slug });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (result.kind === "already-active") {
|
|
173
|
+
log.info("skills", "catalog star: already-active", { source, slug: result.slug });
|
|
174
|
+
conflict(res, `skill "${result.slug}" is already active`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (result.kind === "not-found") {
|
|
178
|
+
log.warn("skills", "catalog star: not found", { source, ident });
|
|
179
|
+
notFound(res, `catalog entry not found: ${result.source}/${result.slug}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
log.warn("skills", "catalog star: invalid slug", { ident });
|
|
183
|
+
badRequest(res, `invalid slug: ${result.slug}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface ExternalStarBody {
|
|
187
|
+
source?: unknown;
|
|
188
|
+
repoId?: unknown;
|
|
189
|
+
skillFolder?: unknown;
|
|
190
|
+
slug?: unknown;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
bindRoute(router, API_ROUTES.skills.catalogStar, async (req: Request<object, unknown, ExternalStarBody>, res: Response<StarResponse | ErrorResponse>) => {
|
|
194
|
+
const { source, slug, repoId, skillFolder } = req.body;
|
|
195
|
+
if (!isCatalogSource(source)) {
|
|
196
|
+
badRequest(res, "source must be a known catalog source");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (source === "external") {
|
|
200
|
+
if (typeof repoId !== "string" || repoId.length === 0 || typeof skillFolder !== "string" || skillFolder.length === 0) {
|
|
201
|
+
badRequest(res, "repoId and skillFolder are required for external star");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const result = await starExternalAsCatalog(repoId, skillFolder);
|
|
205
|
+
starResponse(result, source, `${repoId}/${skillFolder}`, res);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (typeof slug !== "string" || slug.length === 0) {
|
|
209
|
+
badRequest(res, "slug is required");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const result = await starCatalogEntry(source, slug);
|
|
213
|
+
starResponse(result, source, slug, res);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// External-repo lifecycle endpoints (#1383 / #1335 PR-C). They live
|
|
217
|
+
// under `/api/skills/external/*` so they sort cleanly alongside the
|
|
218
|
+
// catalog endpoints AND register BEFORE the `/:name` detail handler
|
|
219
|
+
// below. Express matches in registration order — a `:name` route
|
|
220
|
+
// declared first would swallow `/external/...` as a skill named
|
|
221
|
+
// "external".
|
|
222
|
+
|
|
223
|
+
interface ExternalSuggestionsResponse {
|
|
224
|
+
suggestions: readonly ExternalPresetSuggestion[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface ExternalReposResponse {
|
|
228
|
+
repos: InstalledRepo[];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface InstallRepoBody {
|
|
232
|
+
url?: unknown;
|
|
233
|
+
subpath?: unknown;
|
|
234
|
+
ref?: unknown;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface InstallRepoResponse {
|
|
238
|
+
installed: true;
|
|
239
|
+
repoId: string;
|
|
240
|
+
url: string;
|
|
241
|
+
sha: string;
|
|
242
|
+
skillCount: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
interface UninstallRepoResponse {
|
|
246
|
+
uninstalled: true;
|
|
247
|
+
repoId: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
bindRoute(router, API_ROUTES.skills.externalSuggestions, (_req: Request, res: Response<ExternalSuggestionsResponse>) => {
|
|
251
|
+
res.json({ suggestions: EXTERNAL_PRESETS });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
bindRoute(router, API_ROUTES.skills.externalReposList, async (_req: Request, res: Response<ExternalReposResponse>) => {
|
|
255
|
+
const repos = await listInstalledRepos();
|
|
256
|
+
log.info("skills", "external repos list: ok", { count: repos.length });
|
|
257
|
+
res.json({ repos });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
bindRoute(
|
|
261
|
+
router,
|
|
262
|
+
API_ROUTES.skills.externalReposInstall,
|
|
263
|
+
async (req: Request<object, unknown, InstallRepoBody>, res: Response<InstallRepoResponse | ErrorResponse>) => {
|
|
264
|
+
const { url, subpath, ref } = req.body ?? {};
|
|
265
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
266
|
+
badRequest(res, "url is required");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (subpath !== undefined && typeof subpath !== "string") {
|
|
270
|
+
badRequest(res, "subpath must be a string when provided");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (ref !== undefined && typeof ref !== "string") {
|
|
274
|
+
badRequest(res, "ref must be a string when provided");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const result = await installExternalRepo({ url, subpath, ref });
|
|
278
|
+
if (result.kind === "installed") {
|
|
279
|
+
log.info("skills", "external install: ok", { repoId: result.detail.repoId, skillCount: result.detail.skillCount });
|
|
280
|
+
res.json({
|
|
281
|
+
installed: true,
|
|
282
|
+
repoId: result.detail.repoId,
|
|
283
|
+
url: result.detail.url,
|
|
284
|
+
sha: result.detail.sha,
|
|
285
|
+
skillCount: result.detail.skillCount,
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (result.kind === "no-skills") {
|
|
290
|
+
log.warn("skills", "external install: no skills discovered", { repoId: result.repoId });
|
|
291
|
+
res.status(422).json({ error: `no SKILL.md found in repo (${result.repoId})` });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (result.kind === "invalid-url") {
|
|
295
|
+
log.warn("skills", "external install: invalid url", { url: result.url });
|
|
296
|
+
badRequest(res, "url must be a github.com HTTPS URL: https://github.com/<owner>/<repo>");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (result.kind === "invalid-subpath") {
|
|
300
|
+
log.warn("skills", "external install: invalid subpath", { subpath: result.subpath });
|
|
301
|
+
badRequest(res, "subpath must be a relative path with no '..', leading '/', or backslash segments");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (result.kind === "id-collision") {
|
|
305
|
+
log.warn("skills", "external install: repoId collision", { repoId: result.repoId, existingUrl: result.existingUrl });
|
|
306
|
+
conflict(res, `repo id "${result.repoId}" is already in use by ${result.existingUrl}. Uninstall it first if you intend to replace it.`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
log.warn("skills", "external install: error", { reason: result.reason });
|
|
310
|
+
res.status(502).json({ error: `external install failed: ${result.reason}` });
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
bindRoute(router, API_ROUTES.skills.externalReposRemove, async (req: Request<{ repoId: string }>, res: Response<UninstallRepoResponse | ErrorResponse>) => {
|
|
315
|
+
const { repoId } = req.params;
|
|
316
|
+
const result = await uninstallExternalRepo(repoId);
|
|
317
|
+
if (result.kind === "uninstalled") {
|
|
318
|
+
log.info("skills", "external uninstall: ok", { repoId: result.repoId });
|
|
319
|
+
res.json({ uninstalled: true, repoId: result.repoId });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (result.kind === "not-found") {
|
|
323
|
+
log.warn("skills", "external uninstall: not found", { repoId: result.repoId });
|
|
324
|
+
notFound(res, `external repo not installed: ${result.repoId}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
log.warn("skills", "external uninstall: invalid repoId", { repoId: result.repoId });
|
|
328
|
+
badRequest(res, `invalid repoId: ${result.repoId}`);
|
|
329
|
+
});
|
|
330
|
+
|
|
68
331
|
bindRoute(router, API_ROUTES.skills.detail, async (req: Request<{ name: string }>, res: Response<SkillDetailResponse | ErrorResponse>) => {
|
|
69
332
|
log.info("skills", "detail: start", { name: req.params.name });
|
|
70
333
|
const skills = await discoverSkills({ workspaceRoot: workspacePath });
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// server/workspace/hooks/shared/sidecar.ts
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import path2 from "node:path";
|
|
6
|
+
|
|
7
|
+
// server/utils/time.ts
|
|
8
|
+
var ONE_SECOND_MS = 1e3;
|
|
9
|
+
var ONE_MINUTE_MS = 6e4;
|
|
10
|
+
var SUBPROCESS_PROBE_TIMEOUT_MS = 5 * ONE_SECOND_MS;
|
|
11
|
+
var STARTUP_FAILURE_FORCE_EXIT_MS = 5 * ONE_SECOND_MS;
|
|
12
|
+
var CLI_SUBPROCESS_TIMEOUT_MS = 5 * ONE_MINUTE_MS;
|
|
13
|
+
|
|
14
|
+
// server/workspace/hooks/shared/workspace.ts
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
function workspaceRoot() {
|
|
18
|
+
return process.env.CLAUDE_PROJECT_DIR ?? path.join(homedir(), "mulmoclaude");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// server/workspace/hooks/shared/sidecar.ts
|
|
22
|
+
var TOKEN_FILE = ".session-token";
|
|
23
|
+
var PORT_FILE = ".server-port";
|
|
24
|
+
function serverHost() {
|
|
25
|
+
return process.env.MULMOCLAUDE_HOST ?? "127.0.0.1";
|
|
26
|
+
}
|
|
27
|
+
function readSidecar(rel) {
|
|
28
|
+
try {
|
|
29
|
+
return readFileSync(path2.join(workspaceRoot(), rel), "utf-8").trim();
|
|
30
|
+
} catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function readToken() {
|
|
35
|
+
return readSidecar(TOKEN_FILE);
|
|
36
|
+
}
|
|
37
|
+
function readPort() {
|
|
38
|
+
const raw = readSidecar(PORT_FILE);
|
|
39
|
+
if (!raw) return null;
|
|
40
|
+
const port = Number.parseInt(raw, 10);
|
|
41
|
+
return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
|
|
42
|
+
}
|
|
43
|
+
function buildAuthPost(pathname, body) {
|
|
44
|
+
const token = readToken();
|
|
45
|
+
const port = readPort();
|
|
46
|
+
if (!token || port === null) return null;
|
|
47
|
+
const headers = {
|
|
48
|
+
Authorization: `Bearer ${token}`
|
|
49
|
+
};
|
|
50
|
+
const init = { method: "POST", headers };
|
|
51
|
+
if (body !== void 0) {
|
|
52
|
+
headers["Content-Type"] = "application/json";
|
|
53
|
+
init.body = JSON.stringify(body);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
url: `http://${serverHost()}:${port}${pathname}`,
|
|
57
|
+
init
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
var DEFAULT_TIMEOUT_MS = 2 * ONE_SECOND_MS;
|
|
61
|
+
async function safePost(req, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
62
|
+
if (!req) return;
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
65
|
+
try {
|
|
66
|
+
await fetch(req.url, { ...req.init, signal: controller.signal });
|
|
67
|
+
} catch {
|
|
68
|
+
} finally {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var LOG_TIMEOUT_MS = ONE_SECOND_MS;
|
|
73
|
+
async function serverLog(namespace, message, options = {}) {
|
|
74
|
+
const body = {
|
|
75
|
+
namespace,
|
|
76
|
+
message,
|
|
77
|
+
level: options.level ?? "info",
|
|
78
|
+
...options.data ? { data: options.data } : {}
|
|
79
|
+
};
|
|
80
|
+
const req = buildAuthPost("/api/hooks/log", body);
|
|
81
|
+
await safePost(req, LOG_TIMEOUT_MS);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// server/workspace/hooks/shared/stdin.ts
|
|
85
|
+
async function readHookPayload() {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
for await (const chunk of process.stdin) {
|
|
88
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
89
|
+
}
|
|
90
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
91
|
+
if (!raw.trim()) return null;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(raw);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function extractFilePath(payload) {
|
|
99
|
+
const fromInput = payload.tool_input?.file_path;
|
|
100
|
+
if (typeof fromInput === "string") return fromInput;
|
|
101
|
+
const fromResponse = payload.tool_response?.filePath;
|
|
102
|
+
if (typeof fromResponse === "string") return fromResponse;
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
function extractCommand(payload) {
|
|
106
|
+
const command = payload.tool_input?.command;
|
|
107
|
+
return typeof command === "string" ? command : "";
|
|
108
|
+
}
|
|
109
|
+
function extractToolName(payload) {
|
|
110
|
+
return typeof payload.tool_name === "string" ? payload.tool_name : "";
|
|
111
|
+
}
|
|
112
|
+
function extractSessionId(payload) {
|
|
113
|
+
const sessionId = payload.session_id;
|
|
114
|
+
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : void 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// server/workspace/hooks/handlers/configRefresh.ts
|
|
118
|
+
var PATTERNS = [/[\\/]\.claude[\\/]skills[\\/][^\\/]+[\\/]SKILL\.md$/, /[\\/]config[\\/]scheduler[\\/]tasks\.json$/];
|
|
119
|
+
async function handleConfigRefresh(payload) {
|
|
120
|
+
const tool = extractToolName(payload);
|
|
121
|
+
if (tool !== "Write" && tool !== "Edit") return;
|
|
122
|
+
const filePath = extractFilePath(payload);
|
|
123
|
+
if (!filePath) return;
|
|
124
|
+
if (!PATTERNS.some((pattern) => pattern.test(filePath))) return;
|
|
125
|
+
const req = buildAuthPost("/api/config/refresh");
|
|
126
|
+
await safePost(req);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// server/workspace/hooks/handlers/skillBridge.ts
|
|
130
|
+
import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
131
|
+
import path3 from "node:path";
|
|
132
|
+
|
|
133
|
+
// server/utils/errors.ts
|
|
134
|
+
function errorMessage(err, fallback) {
|
|
135
|
+
if (err instanceof Error) return err.message;
|
|
136
|
+
if (err !== null && typeof err === "object") {
|
|
137
|
+
const obj = err;
|
|
138
|
+
if (typeof obj.details === "string" && obj.details) return obj.details;
|
|
139
|
+
if (typeof obj.message === "string" && obj.message) return obj.message;
|
|
140
|
+
}
|
|
141
|
+
if (fallback !== void 0) return fallback;
|
|
142
|
+
return String(err);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// server/workspace/hooks/handlers/skillBridge.ts
|
|
146
|
+
var DATA_SKILLS_DIR = path3.join("data", "skills");
|
|
147
|
+
var CLAUDE_SKILLS_DIR = path3.join(".claude", "skills");
|
|
148
|
+
var SKILL_FILENAME = "SKILL.md";
|
|
149
|
+
var SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
150
|
+
var RM_RE = /^\s*rm\s+((?:-[a-zA-Z]+\s+)+)['"]?data\/skills\/([a-z0-9-]+)\/?['"]?\s*$/;
|
|
151
|
+
var RECURSIVE_FLAG_RE = /[rR]/;
|
|
152
|
+
function dataSkillDir(slug) {
|
|
153
|
+
return path3.join(workspaceRoot(), DATA_SKILLS_DIR, slug);
|
|
154
|
+
}
|
|
155
|
+
function dataSkillFilePath(slug) {
|
|
156
|
+
return path3.join(dataSkillDir(slug), SKILL_FILENAME);
|
|
157
|
+
}
|
|
158
|
+
function claudeSkillDir(slug) {
|
|
159
|
+
return path3.join(workspaceRoot(), CLAUDE_SKILLS_DIR, slug);
|
|
160
|
+
}
|
|
161
|
+
function claudeSkillFilePath(slug) {
|
|
162
|
+
return path3.join(claudeSkillDir(slug), SKILL_FILENAME);
|
|
163
|
+
}
|
|
164
|
+
function slugFromDataPath(filePath) {
|
|
165
|
+
const root = workspaceRoot();
|
|
166
|
+
const staging = path3.join(root, DATA_SKILLS_DIR);
|
|
167
|
+
const rel = path3.relative(staging, filePath);
|
|
168
|
+
if (!rel || rel.startsWith("..")) return null;
|
|
169
|
+
const segments = rel.split(path3.sep);
|
|
170
|
+
if (segments.length !== 2) return null;
|
|
171
|
+
const [slug, basename] = segments;
|
|
172
|
+
if (basename !== SKILL_FILENAME) return null;
|
|
173
|
+
return SLUG_RE.test(slug) ? slug : null;
|
|
174
|
+
}
|
|
175
|
+
function slugFromRmCommand(command) {
|
|
176
|
+
const match = RM_RE.exec(command);
|
|
177
|
+
if (!match) return null;
|
|
178
|
+
const [, flags, slug] = match;
|
|
179
|
+
if (!RECURSIVE_FLAG_RE.test(flags)) return null;
|
|
180
|
+
return SLUG_RE.test(slug) ? slug : null;
|
|
181
|
+
}
|
|
182
|
+
function mirrorWrite(slug) {
|
|
183
|
+
const content = readFileSync2(dataSkillFilePath(slug), "utf-8");
|
|
184
|
+
const destDir = claudeSkillDir(slug);
|
|
185
|
+
mkdirSync(destDir, { recursive: true });
|
|
186
|
+
const dest = claudeSkillFilePath(slug);
|
|
187
|
+
const tmp = path3.join(destDir, `.SKILL.md.${process.pid}.tmp`);
|
|
188
|
+
writeFileSync(tmp, content, "utf-8");
|
|
189
|
+
renameSync(tmp, dest);
|
|
190
|
+
}
|
|
191
|
+
function mirrorDelete(slug) {
|
|
192
|
+
rmSync(claudeSkillDir(slug), { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
async function refreshConfig() {
|
|
195
|
+
await safePost(buildAuthPost("/api/config/refresh"));
|
|
196
|
+
}
|
|
197
|
+
async function handleWriteOrEdit(payload) {
|
|
198
|
+
const filePath = extractFilePath(payload);
|
|
199
|
+
if (!filePath) return;
|
|
200
|
+
const slug = slugFromDataPath(filePath);
|
|
201
|
+
if (slug === null) return;
|
|
202
|
+
try {
|
|
203
|
+
mirrorWrite(slug);
|
|
204
|
+
await refreshConfig();
|
|
205
|
+
await serverLog("skill-bridge", `mirrored ${dataSkillFilePath(slug)} \u2192 ${claudeSkillFilePath(slug)}`, { data: { slug, op: "write" } });
|
|
206
|
+
} catch (err) {
|
|
207
|
+
await serverLog("skill-bridge", `mirror write failed for slug=${slug}`, {
|
|
208
|
+
level: "error",
|
|
209
|
+
data: { slug, error: errorMessage(err) }
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function handleBash(payload) {
|
|
214
|
+
const command = extractCommand(payload);
|
|
215
|
+
if (!command) return;
|
|
216
|
+
const slug = slugFromRmCommand(command);
|
|
217
|
+
if (slug === null) return;
|
|
218
|
+
try {
|
|
219
|
+
mirrorDelete(slug);
|
|
220
|
+
await refreshConfig();
|
|
221
|
+
await serverLog("skill-bridge", `removed ${claudeSkillDir(slug)}`, { data: { slug, op: "delete" } });
|
|
222
|
+
} catch (err) {
|
|
223
|
+
await serverLog("skill-bridge", `mirror delete failed for slug=${slug}`, {
|
|
224
|
+
level: "error",
|
|
225
|
+
data: { slug, error: errorMessage(err) }
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function handleSkillBridge(payload) {
|
|
230
|
+
const tool = extractToolName(payload);
|
|
231
|
+
if (tool === "Write" || tool === "Edit") {
|
|
232
|
+
await handleWriteOrEdit(payload);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (tool === "Bash") {
|
|
236
|
+
await handleBash(payload);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// server/workspace/hooks/handlers/wikiSnapshot.ts
|
|
241
|
+
import path5 from "node:path";
|
|
242
|
+
|
|
243
|
+
// src/lib/wiki-page/paths.ts
|
|
244
|
+
import path4 from "node:path";
|
|
245
|
+
|
|
246
|
+
// src/lib/wiki-page/slug.ts
|
|
247
|
+
function isSafeSlug(slug) {
|
|
248
|
+
if (slug.length === 0) return false;
|
|
249
|
+
if (slug === "." || slug === "..") return false;
|
|
250
|
+
if (slug.includes("/") || slug.includes("\\")) return false;
|
|
251
|
+
if (slug.includes("\0")) return false;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/lib/wiki-page/paths.ts
|
|
256
|
+
function wikiSlugFromAbsPath(absPath, pagesDir) {
|
|
257
|
+
const rel = path4.relative(pagesDir, absPath);
|
|
258
|
+
if (rel.length === 0) return null;
|
|
259
|
+
if (path4.isAbsolute(rel)) return null;
|
|
260
|
+
if (rel.includes(path4.sep)) return null;
|
|
261
|
+
if (!rel.endsWith(".md")) return null;
|
|
262
|
+
const slug = rel.slice(0, -".md".length);
|
|
263
|
+
if (!isSafeSlug(slug)) return null;
|
|
264
|
+
return slug;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// server/workspace/hooks/handlers/wikiSnapshot.ts
|
|
268
|
+
var WIKI_PAGES_REL = path5.join("data", "wiki", "pages");
|
|
269
|
+
async function handleWikiSnapshot(payload) {
|
|
270
|
+
const tool = extractToolName(payload);
|
|
271
|
+
if (tool !== "Write" && tool !== "Edit") return;
|
|
272
|
+
const filePath = extractFilePath(payload);
|
|
273
|
+
if (!filePath) return;
|
|
274
|
+
const wikiPagesDir = path5.join(workspaceRoot(), WIKI_PAGES_REL);
|
|
275
|
+
const slug = wikiSlugFromAbsPath(filePath, wikiPagesDir);
|
|
276
|
+
if (slug === null) return;
|
|
277
|
+
const envChatSessionId = process.env.MULMOCLAUDE_CHAT_SESSION_ID;
|
|
278
|
+
const payloadSessionId = extractSessionId(payload);
|
|
279
|
+
const sessionId = envChatSessionId && envChatSessionId.length > 0 ? envChatSessionId : payloadSessionId;
|
|
280
|
+
const body = sessionId === void 0 ? { slug } : { slug, sessionId };
|
|
281
|
+
const req = buildAuthPost("/api/wiki/internal/snapshot", body);
|
|
282
|
+
await safePost(req);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// server/workspace/hooks/dispatcher.ts
|
|
286
|
+
var HANDLERS = [handleWikiSnapshot, handleConfigRefresh, handleSkillBridge];
|
|
287
|
+
async function runHandler(handler, payload) {
|
|
288
|
+
try {
|
|
289
|
+
await handler(payload);
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function main() {
|
|
294
|
+
const payload = await readHookPayload();
|
|
295
|
+
if (!payload) return;
|
|
296
|
+
await Promise.all(HANDLERS.map((handler) => runHandler(handler, payload)));
|
|
297
|
+
}
|
|
298
|
+
main().catch(() => {
|
|
299
|
+
});
|