mulmoclaude 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mulmoclaude.js +1 -1
- package/client/assets/PluginScopedRoot-YjvQq0Nn.js +3 -0
- package/client/assets/{html2canvas-CDGcmOD3-BbPeutDg.js → html2canvas-CDGcmOD3-Bkf2uOth.js} +1 -1
- package/client/assets/{index-BbgSjFQ8.js → index-BwrlMMHr.js} +178 -141
- package/client/assets/index-CvvNuegU.css +2 -0
- package/client/assets/{index.es-DqtpmBm8-DJdTPdnc.js → index.es-DqtpmBm8-D9mAh_KQ.js} +1 -1
- package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-C1To4M3t.js +1 -0
- package/client/index.html +7 -6
- package/package.json +9 -7
- package/server/accounting/eventPublisher.ts +2 -1
- package/server/accounting/snapshotCache.ts +2 -1
- package/server/agent/activeTools.ts +16 -6
- package/server/agent/backend/claude-code.ts +1 -0
- package/server/agent/backend/types.ts +3 -0
- package/server/agent/config.ts +25 -2
- package/server/agent/index.ts +6 -0
- package/server/agent/mcp-server.ts +9 -6
- package/server/agent/mcp-tools/index.ts +15 -2
- package/server/agent/mcp-tools/notify.ts +20 -2
- package/server/agent/prompt.ts +37 -24
- package/server/api/routes/accounting.ts +31 -24
- package/server/api/routes/agent.ts +2 -2
- package/server/api/routes/config-refresh.ts +49 -0
- package/server/api/routes/config.ts +86 -68
- package/server/api/routes/files.ts +41 -17
- package/server/api/routes/hookLog.ts +95 -0
- package/server/api/routes/news.ts +39 -52
- package/server/api/routes/notifier.ts +14 -19
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/photo-locations.ts +79 -0
- package/server/api/routes/plugins.ts +11 -0
- package/server/api/routes/presentSvg.ts +107 -0
- package/server/api/routes/scheduler.ts +100 -98
- package/server/api/routes/schedulerTasks.ts +98 -95
- package/server/api/routes/sessions.ts +22 -27
- package/server/api/routes/sources.ts +45 -43
- package/server/api/routes/wiki/history.ts +6 -15
- package/server/api/routes/wiki.ts +73 -276
- package/server/events/file-change.ts +3 -2
- package/server/events/session-store/index.ts +2 -1
- package/server/index.ts +130 -8
- package/server/notifier/store.ts +3 -3
- package/server/plugins/preset-list.ts +16 -5
- package/server/plugins/runtime.ts +2 -2
- package/server/system/config.ts +138 -16
- package/server/utils/asyncHandler.ts +75 -0
- package/server/utils/exif.ts +321 -0
- package/server/utils/files/accounting-io.ts +19 -20
- package/server/utils/files/attachment-store.ts +69 -12
- package/server/utils/files/journal-io.ts +2 -1
- package/server/utils/files/json.ts +8 -1
- package/server/utils/files/reference-dirs-io.ts +2 -3
- package/server/utils/files/scheduler-overrides-io.ts +2 -3
- package/server/utils/files/svg-store.ts +27 -0
- package/server/utils/files/user-tasks-io.ts +2 -3
- package/server/utils/regex.ts +3 -12
- package/server/utils/text.ts +29 -0
- package/server/workspace/chat-index/summarizer.ts +5 -3
- package/server/workspace/cooking-recipes/migrate.ts +125 -0
- package/server/workspace/custom-dirs.ts +2 -2
- package/server/workspace/hooks/dispatcher.mjs +300 -0
- package/server/workspace/hooks/dispatcher.ts +55 -0
- package/server/workspace/hooks/handlers/configRefresh.ts +38 -0
- package/server/workspace/hooks/handlers/skillBridge.ts +223 -0
- package/server/workspace/hooks/handlers/wikiSnapshot.ts +43 -0
- package/server/workspace/hooks/provision.ts +222 -0
- package/server/workspace/hooks/shared/sidecar.ts +124 -0
- package/server/workspace/hooks/shared/stdin.ts +60 -0
- package/server/workspace/hooks/shared/workspace.ts +13 -0
- package/server/workspace/journal/dailyPass.ts +1 -6
- package/server/workspace/memory/io.ts +1 -34
- package/server/workspace/memory/migrate.ts +2 -1
- package/server/workspace/memory/snapshot.ts +26 -0
- package/server/workspace/memory/topic-io.ts +1 -18
- package/server/workspace/paths.ts +16 -0
- package/server/workspace/photo-locations/index.ts +149 -0
- package/server/workspace/photo-locations/list.ts +124 -0
- package/server/workspace/skills-preset/mc-cooking-coach/SKILL.md +217 -0
- package/server/workspace/skills-preset/mc-manage-automations/SKILL.md +119 -0
- package/server/workspace/skills-preset/mc-manage-skills/SKILL.md +128 -0
- package/server/workspace/skills-preset/mc-manage-sources/SKILL.md +106 -0
- package/server/workspace/skills-preset.ts +2 -1
- package/server/workspace/wiki-pages/io.ts +2 -1
- package/src/App.vue +78 -3
- package/src/components/ChatInput.vue +7 -8
- package/src/components/FileContentHeader.vue +1 -6
- package/src/components/FileDropOverlay.vue +18 -0
- package/src/components/NewsView.vue +2 -1
- package/src/components/RolesView.vue +14 -5
- package/src/components/SettingsMapTab.vue +140 -0
- package/src/components/SettingsMcpTab.vue +15 -10
- package/src/components/SettingsModal.vue +138 -112
- package/src/components/SettingsModelTab.vue +121 -0
- package/src/components/SettingsPhotosTab.vue +118 -0
- package/src/components/SourcesManager.vue +4 -3
- package/src/components/StackView.vue +43 -12
- package/src/composables/useContentDisplay.ts +16 -0
- package/src/composables/useFileDropZone.ts +148 -0
- package/src/composables/useImageErrorRepair.ts +29 -19
- package/src/composables/useSkillsList.ts +2 -1
- package/src/config/apiRoutes.ts +24 -0
- package/src/config/roles.ts +121 -70
- package/src/config/systemFileDescriptors.ts +2 -2
- package/src/config/toolNames.ts +26 -0
- package/src/index.css +26 -0
- package/src/lang/de.ts +70 -1
- package/src/lang/en.ts +69 -1
- package/src/lang/es.ts +69 -1
- package/src/lang/fr.ts +69 -1
- package/src/lang/ja.ts +69 -1
- package/src/lang/ko.ts +68 -1
- package/src/lang/pt-BR.ts +69 -1
- package/src/lang/zh.ts +67 -1
- package/src/lib/wiki-page/index-parse.ts +221 -0
- package/src/lib/wiki-page/link.ts +62 -0
- package/src/lib/wiki-page/lint.ts +105 -0
- package/src/lib/wiki-page/paths.ts +35 -0
- package/src/lib/wiki-page/slug.ts +28 -40
- package/src/main.ts +8 -0
- package/src/plugins/_extras.ts +6 -2
- package/src/plugins/_generated/metas.ts +4 -0
- package/src/plugins/_generated/registrations.ts +4 -0
- package/src/plugins/_generated/server-bindings.ts +6 -0
- package/src/plugins/accounting/Preview.vue +3 -6
- package/src/plugins/accounting/View.vue +2 -1
- package/src/plugins/accounting/components/AccountsModal.vue +3 -2
- package/src/plugins/accounting/components/JournalEntryForm.vue +2 -1
- package/src/plugins/accounting/components/JournalList.vue +2 -1
- package/src/plugins/accounting/components/OpeningBalancesForm.vue +2 -1
- package/src/plugins/accounting/currencies.ts +13 -0
- package/src/plugins/manageRoles/View.vue +16 -5
- package/src/plugins/manageSkills/View.vue +12 -4
- package/src/plugins/markdown/View.vue +6 -0
- package/src/plugins/photoLocations/View.vue +231 -0
- package/src/plugins/photoLocations/definition.ts +47 -0
- package/src/plugins/photoLocations/index.ts +38 -0
- package/src/plugins/photoLocations/meta.ts +35 -0
- package/src/plugins/presentMulmoScript/View.vue +76 -7
- package/src/plugins/presentMulmoScript/helpers.ts +15 -0
- package/src/plugins/presentSVG/Preview.vue +56 -0
- package/src/plugins/presentSVG/View.vue +465 -0
- package/src/plugins/presentSVG/definition.ts +29 -0
- package/src/plugins/presentSVG/index.ts +49 -0
- package/src/plugins/presentSVG/meta.ts +14 -0
- package/src/plugins/scheduler/View.vue +3 -7
- package/src/plugins/skill/View.vue +15 -16
- package/src/plugins/spreadsheet/View.vue +4 -0
- package/src/plugins/wiki/View.vue +1 -1
- package/src/plugins/wiki/helpers.ts +23 -5
- package/src/plugins/wiki/route.ts +12 -11
- package/src/tools/runtimeLoader.ts +75 -9
- package/src/utils/dom/iframeHeightClamp.ts +42 -0
- package/src/utils/format/bytes.ts +41 -0
- package/src/utils/format/date.ts +14 -2
- package/src/utils/image/imageRepairInlineScript.ts +192 -41
- package/src/utils/markdown/sanitize.ts +68 -0
- package/src/utils/markdown/setup.ts +36 -0
- package/src/utils/markdown/wikiEmbedHandlers.ts +170 -0
- package/src/utils/markdown/wikiEmbeds.ts +141 -0
- package/src/utils/markdown/workspaceLinkify.ts +73 -0
- package/src/utils/path/workspaceLinkRouter.ts +17 -1
- package/client/assets/index-ECD0lgIv.css +0 -2
- package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
- package/client/assets/runtime-protocol-vue-6WYa8hAs.js +0 -1
- package/server/workspace/wiki-history/hook/snapshot.mjs +0 -98
- package/server/workspace/wiki-history/hook/snapshot.ts +0 -135
- package/server/workspace/wiki-history/provision.ts +0 -181
- /package/client/assets/{chunk-D8eiyYIV-C1eAZMzz.js → chunk-D8eiyYIV-CAXpUwLd.js} +0 -0
- /package/client/assets/{purify.es-Fx1Nqyry-BSVNht6S.js → purify.es-Fx1Nqyry-Dwtk-9WZ.js} +0 -0
- /package/client/assets/{typeof-DBp4T-Ny-C2xoZtcz.js → typeof-DBp4T-Ny-CSr8wx1e.js} +0 -0
- /package/client/assets/{vue-1e_vz2LW.js → vue-C8UuIO9J.js} +0 -0
|
@@ -10,9 +10,11 @@ import chartDef from "../chart/definition";
|
|
|
10
10
|
import manageSkillsDef from "../manageSkills/definition";
|
|
11
11
|
import manageSourceDef from "../manageSource/definition";
|
|
12
12
|
import markdownDef from "../markdown/definition";
|
|
13
|
+
import photoLocationsDef from "../photoLocations/definition";
|
|
13
14
|
import presentFormDef from "../presentForm/definition";
|
|
14
15
|
import presentHtmlDef from "../presentHtml/definition";
|
|
15
16
|
import presentMulmoScriptDef from "../presentMulmoScript/definition";
|
|
17
|
+
import presentSVGDef from "../presentSVG/definition";
|
|
16
18
|
import schedulerAutomationsDef from "../scheduler/automationsDefinition";
|
|
17
19
|
import schedulerCalendarDef from "../scheduler/calendarDefinition";
|
|
18
20
|
import spreadsheetDef from "../spreadsheet/definition";
|
|
@@ -22,9 +24,11 @@ import { META as chartMeta } from "../chart/meta";
|
|
|
22
24
|
import { META as manageSkillsMeta } from "../manageSkills/meta";
|
|
23
25
|
import { META as manageSourceMeta } from "../manageSource/meta";
|
|
24
26
|
import { META as markdownMeta } from "../markdown/meta";
|
|
27
|
+
import { META as photoLocationsMeta } from "../photoLocations/meta";
|
|
25
28
|
import { META as presentFormMeta } from "../presentForm/meta";
|
|
26
29
|
import { META as presentHtmlMeta } from "../presentHtml/meta";
|
|
27
30
|
import { META as presentMulmoScriptMeta } from "../presentMulmoScript/meta";
|
|
31
|
+
import { META as presentSVGMeta } from "../presentSVG/meta";
|
|
28
32
|
import { META as schedulerCalendarMeta } from "../scheduler/calendarMeta";
|
|
29
33
|
import { META as spreadsheetMeta } from "../spreadsheet/meta";
|
|
30
34
|
|
|
@@ -38,9 +42,11 @@ export const GENERATED_SERVER_BINDINGS: readonly ServerPluginBinding[] = [
|
|
|
38
42
|
{ def: manageSkillsDef, endpoint: mcpEndpoint(manageSkillsMeta) },
|
|
39
43
|
{ def: manageSourceDef, endpoint: mcpEndpoint(manageSourceMeta) },
|
|
40
44
|
{ def: markdownDef, endpoint: mcpEndpoint(markdownMeta) },
|
|
45
|
+
{ def: photoLocationsDef, endpoint: mcpEndpoint(photoLocationsMeta) },
|
|
41
46
|
{ def: presentFormDef, endpoint: mcpEndpoint(presentFormMeta) },
|
|
42
47
|
{ def: presentHtmlDef, endpoint: mcpEndpoint(presentHtmlMeta) },
|
|
43
48
|
{ def: presentMulmoScriptDef, endpoint: mcpEndpoint(presentMulmoScriptMeta) },
|
|
49
|
+
{ def: presentSVGDef, endpoint: mcpEndpoint(presentSVGMeta) },
|
|
44
50
|
{ def: schedulerAutomationsDef, endpoint: mcpEndpoint(schedulerCalendarMeta) },
|
|
45
51
|
{ def: schedulerCalendarDef, endpoint: mcpEndpoint(schedulerCalendarMeta) },
|
|
46
52
|
{ def: spreadsheetDef, endpoint: mcpEndpoint(spreadsheetMeta) },
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<script setup lang="ts">
|
|
13
13
|
import { computed } from "vue";
|
|
14
14
|
import { useI18n } from "vue-i18n";
|
|
15
|
+
import { formatAmountNumeric } from "./currencies";
|
|
15
16
|
|
|
16
17
|
const { t } = useI18n();
|
|
17
18
|
|
|
@@ -34,10 +35,6 @@ interface BookLike {
|
|
|
34
35
|
book?: { id?: string; name?: string };
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function formatAmount(value: number): string {
|
|
38
|
-
return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
38
|
// Each summarise* helper returns null when its branch doesn't apply,
|
|
42
39
|
// keeping the dispatch in `summary` linear (no nested if-trees).
|
|
43
40
|
|
|
@@ -65,7 +62,7 @@ function summarisePl(json: Record<string, unknown>): string | null {
|
|
|
65
62
|
return t("pluginAccounting.preview.pl", {
|
|
66
63
|
from: profitLoss.from ?? "?",
|
|
67
64
|
to: profitLoss.to ?? "?",
|
|
68
|
-
net:
|
|
65
|
+
net: formatAmountNumeric(profitLoss.netIncome),
|
|
69
66
|
});
|
|
70
67
|
}
|
|
71
68
|
|
|
@@ -75,7 +72,7 @@ function summariseBs(json: Record<string, unknown>): string | null {
|
|
|
75
72
|
const assets = balanceSheet.sections.find((section) => section.type === "asset");
|
|
76
73
|
return t("pluginAccounting.preview.bs", {
|
|
77
74
|
date: balanceSheet.asOf,
|
|
78
|
-
assets: assets ?
|
|
75
|
+
assets: assets ? formatAmountNumeric(assets.total ?? 0) : "?",
|
|
79
76
|
});
|
|
80
77
|
}
|
|
81
78
|
|
|
@@ -138,6 +138,7 @@ import BookSettings from "./components/BookSettings.vue";
|
|
|
138
138
|
import { getOpeningBalances, getAccounts, getBooks, type Account, type BookSummary } from "./api";
|
|
139
139
|
import { ACCOUNTING_ACTIONS } from "./actions";
|
|
140
140
|
import { useAccountingChannel, useAccountingBooksChannel } from "../../composables/useAccountingChannel";
|
|
141
|
+
import { errorMessage } from "../../utils/errors";
|
|
141
142
|
|
|
142
143
|
const { t } = useI18n();
|
|
143
144
|
|
|
@@ -318,7 +319,7 @@ async function refetchBooks(): Promise<void> {
|
|
|
318
319
|
showFirstRunForm.value = true;
|
|
319
320
|
}
|
|
320
321
|
} catch (err) {
|
|
321
|
-
bookLoadError.value =
|
|
322
|
+
bookLoadError.value = errorMessage(err);
|
|
322
323
|
} finally {
|
|
323
324
|
loadingBooks.value = false;
|
|
324
325
|
}
|
|
@@ -73,6 +73,7 @@ import AccountEditor from "./AccountEditor.vue";
|
|
|
73
73
|
import type { AccountDraft } from "./accountDraft";
|
|
74
74
|
import { validateAccountDraft, type AccountValidationError } from "./accountValidation";
|
|
75
75
|
import { suggestNextCode } from "./accountNumbering";
|
|
76
|
+
import { errorMessage } from "../../../utils/errors";
|
|
76
77
|
|
|
77
78
|
const { t } = useI18n();
|
|
78
79
|
|
|
@@ -222,7 +223,7 @@ async function onSave(next: AccountDraft): Promise<void> {
|
|
|
222
223
|
// result.ok=false, so this is a belt-and-braces guard against
|
|
223
224
|
// a runtime failure that would otherwise leave the Save button
|
|
224
225
|
// stuck on "Saving…".
|
|
225
|
-
error.value =
|
|
226
|
+
error.value = errorMessage(err);
|
|
226
227
|
} finally {
|
|
227
228
|
saving.value = false;
|
|
228
229
|
}
|
|
@@ -268,7 +269,7 @@ async function onToggleActive(account: Account): Promise<void> {
|
|
|
268
269
|
}
|
|
269
270
|
emit("changed");
|
|
270
271
|
} catch (err) {
|
|
271
|
-
toggleError.value =
|
|
272
|
+
toggleError.value = errorMessage(err);
|
|
272
273
|
} finally {
|
|
273
274
|
toggleSaving.value = false;
|
|
274
275
|
}
|
|
@@ -182,6 +182,7 @@ import { localDateString } from "../dates";
|
|
|
182
182
|
import { countryHasFeature, type SupportedCountryCode } from "../countries";
|
|
183
183
|
import { isTaxAccountCode } from "./accountNumbering";
|
|
184
184
|
import AccountsModal from "./AccountsModal.vue";
|
|
185
|
+
import { errorMessage } from "../../../utils/errors";
|
|
185
186
|
|
|
186
187
|
const { t } = useI18n();
|
|
187
188
|
|
|
@@ -413,7 +414,7 @@ async function onSubmit(): Promise<void> {
|
|
|
413
414
|
// belt-and-braces guard against a runtime failure leaving the
|
|
414
415
|
// submit button stuck — the user gets a visible error
|
|
415
416
|
// instead of an unhandled rejection.
|
|
416
|
-
error.value =
|
|
417
|
+
error.value = errorMessage(err);
|
|
417
418
|
} finally {
|
|
418
419
|
submitting.value = false;
|
|
419
420
|
}
|
|
@@ -207,6 +207,7 @@ import type { SupportedCountryCode } from "../countries";
|
|
|
207
207
|
import { useLatestRequest } from "./useLatestRequest";
|
|
208
208
|
import DateRangePicker from "./DateRangePicker.vue";
|
|
209
209
|
import JournalEntryForm from "./JournalEntryForm.vue";
|
|
210
|
+
import { errorMessage } from "../../../utils/errors";
|
|
210
211
|
|
|
211
212
|
const { t } = useI18n();
|
|
212
213
|
|
|
@@ -460,7 +461,7 @@ async function onVoid(entry: JournalEntry): Promise<void> {
|
|
|
460
461
|
const result = await voidEntry({ entryId: entry.id, reason: reason || undefined, bookId: props.bookId });
|
|
461
462
|
if (!result.ok) error.value = result.error;
|
|
462
463
|
} catch (err) {
|
|
463
|
-
error.value =
|
|
464
|
+
error.value = errorMessage(err);
|
|
464
465
|
}
|
|
465
466
|
}
|
|
466
467
|
|
|
@@ -98,6 +98,7 @@ import { formatAmount, inputStepFor } from "../currencies";
|
|
|
98
98
|
import { localDateString } from "../dates";
|
|
99
99
|
import { useLatestRequest } from "./useLatestRequest";
|
|
100
100
|
import AccountsModal from "./AccountsModal.vue";
|
|
101
|
+
import { errorMessage } from "../../../utils/errors";
|
|
101
102
|
|
|
102
103
|
const { t } = useI18n();
|
|
103
104
|
|
|
@@ -238,7 +239,7 @@ async function onSubmit(): Promise<void> {
|
|
|
238
239
|
successMessage.value = t("pluginAccounting.openingForm.success");
|
|
239
240
|
emit("submitted");
|
|
240
241
|
} catch (err) {
|
|
241
|
-
error.value =
|
|
242
|
+
error.value = errorMessage(err);
|
|
242
243
|
} finally {
|
|
243
244
|
submitting.value = false;
|
|
244
245
|
}
|
|
@@ -62,3 +62,16 @@ export function formatAmount(value: number, currency: string, locale?: string):
|
|
|
62
62
|
return value.toFixed(fractionDigitsFor(currency));
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/** Currency-agnostic amount formatter — "1,130.00" — for places that
|
|
67
|
+
* don't carry the currency code on the data path (compact preview
|
|
68
|
+
* envelopes etc.). Use `formatAmount(value, currency)` whenever the
|
|
69
|
+
* currency IS available — the currency-aware path picks the right
|
|
70
|
+
* fraction-digit count automatically (JPY = 0, USD = 2).
|
|
71
|
+
*
|
|
72
|
+
* `locale` mirrors `formatAmount`'s signature: pass an explicit BCP-47
|
|
73
|
+
* locale (`"en-US"`, `"ja-JP"`, …) when the caller knows the desired
|
|
74
|
+
* grouping / digit-shape; omit to fall back to the runtime default. */
|
|
75
|
+
export function formatAmountNumeric(value: number, decimals = 2, locale?: string): string {
|
|
76
|
+
return value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
77
|
+
}
|
|
@@ -297,18 +297,29 @@ interface PluginEntry {
|
|
|
297
297
|
|
|
298
298
|
// Plugins the user can assign — exclude internal/auto-managed ones
|
|
299
299
|
const EXCLUDED = new Set(["text-response"]);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
// `pluginAllPluginNames()` reads from the reactive `runtimeRegistry`
|
|
301
|
+
// (src/tools/runtimeLoader.ts). Wrapping in `computed` makes this
|
|
302
|
+
// re-evaluate when `loadRuntimePlugins` populates the registry
|
|
303
|
+
// post-mount — runtime plugins (e.g. server-only `edgar`) only
|
|
304
|
+
// become visible to the role editor through this reactive read.
|
|
305
|
+
// Snapshotting once into a non-reactive const meant a setup() that
|
|
306
|
+
// ran before the loader resolved would never see them.
|
|
307
|
+
const guiPlugins = computed<PluginEntry[]>(() =>
|
|
308
|
+
pluginAllPluginNames()
|
|
309
|
+
.filter((name) => !EXCLUDED.has(name))
|
|
310
|
+
.map((name) => ({ name, enabled: true, requiredEnv: [] })),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const mcpTools = ref<PluginEntry[]>([]);
|
|
303
314
|
|
|
304
|
-
const availablePlugins =
|
|
315
|
+
const availablePlugins = computed<PluginEntry[]>(() => [...guiPlugins.value, ...mcpTools.value]);
|
|
305
316
|
|
|
306
317
|
const mcpEndpoints = pluginEndpoints<{ list: string }>("mcpTools");
|
|
307
318
|
|
|
308
319
|
onMounted(async () => {
|
|
309
320
|
const result = await apiGet<PluginEntry[]>(mcpEndpoints.list);
|
|
310
321
|
if (result.ok) {
|
|
311
|
-
|
|
322
|
+
mcpTools.value = result.data;
|
|
312
323
|
}
|
|
313
324
|
// Non-critical: MCP tools enrich the plugin palette for role editing
|
|
314
325
|
// but the view works fine with GUI plugins alone. No error banner needed.
|
|
@@ -129,8 +129,15 @@
|
|
|
129
129
|
</div>
|
|
130
130
|
</div>
|
|
131
131
|
<!-- View mode -->
|
|
132
|
-
<!-- eslint-disable
|
|
133
|
-
<div
|
|
132
|
+
<!-- eslint-disable vue/no-v-html -- sanitized via DOMPurify; multi-line element so disable/enable pair (CLAUDE.md UI rule) instead of -next-line -->
|
|
133
|
+
<div
|
|
134
|
+
v-else-if="detail && renderedBody"
|
|
135
|
+
class="markdown-content text-gray-700"
|
|
136
|
+
data-testid="skill-body-rendered"
|
|
137
|
+
@click="handleExternalLinkClick"
|
|
138
|
+
v-html="renderedBody"
|
|
139
|
+
></div>
|
|
140
|
+
<!-- eslint-enable vue/no-v-html -->
|
|
134
141
|
<p v-else-if="detail" class="text-sm text-gray-400 italic">{{ t("pluginManageSkills.emptyBody") }}</p>
|
|
135
142
|
</div>
|
|
136
143
|
</div>
|
|
@@ -142,11 +149,12 @@
|
|
|
142
149
|
import { computed, onMounted, ref, watch } from "vue";
|
|
143
150
|
import { useI18n } from "vue-i18n";
|
|
144
151
|
import { marked } from "marked";
|
|
145
|
-
import DOMPurify from "dompurify";
|
|
146
152
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
147
153
|
import type { ManageSkillsData, SkillSummary } from "./index";
|
|
148
154
|
import { useAppApi } from "../../composables/useAppApi";
|
|
149
155
|
import { apiGet, apiPut, apiDelete } from "../../utils/api";
|
|
156
|
+
import { handleExternalLinkClick } from "../../utils/dom/externalLink";
|
|
157
|
+
import { sanitizeMarkdownHtml } from "../../utils/markdown/sanitize";
|
|
150
158
|
import { pluginEndpoints } from "../api";
|
|
151
159
|
import { buildRouteUrl } from "../meta-types";
|
|
152
160
|
import type { SkillsEndpoints } from "./definition";
|
|
@@ -184,7 +192,7 @@ const selected = computed(() => skills.value.find((skill) => skill.name === sele
|
|
|
184
192
|
const renderedBody = computed(() => {
|
|
185
193
|
const body = detail.value?.body;
|
|
186
194
|
if (!body) return "";
|
|
187
|
-
return
|
|
195
|
+
return sanitizeMarkdownHtml(marked(body) as string);
|
|
188
196
|
});
|
|
189
197
|
|
|
190
198
|
// Reset the selection when the tool result is replaced (e.g. the
|
|
@@ -87,6 +87,7 @@ import { rewriteMarkdownImageRefs } from "../../utils/image/rewriteMarkdownImage
|
|
|
87
87
|
import { findTaskLines, makeTasksInteractive, toggleTaskAt } from "../../utils/markdown/taskList";
|
|
88
88
|
import { usePdfDownload } from "../../composables/usePdfDownload";
|
|
89
89
|
import { apiGet, apiPut } from "../../utils/api";
|
|
90
|
+
import { handleExternalLinkClick } from "../../utils/dom/externalLink";
|
|
90
91
|
import { pluginEndpoints } from "../api";
|
|
91
92
|
import { useClipboardCopy } from "../../composables/useClipboardCopy";
|
|
92
93
|
import { buildPdfFilename } from "../../utils/files/filename";
|
|
@@ -354,6 +355,11 @@ async function persistTaskMarkdown(relativePath: string, markdown: string): Prom
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
function onMarkdownClick(event: MouseEvent): void {
|
|
358
|
+
// External http(s) links: open in a new tab instead of letting the
|
|
359
|
+
// SPA navigate away. Same handler the wiki / textResponse renders
|
|
360
|
+
// use; without it, clicking an external link from a markdown file
|
|
361
|
+
// tore the user out of MulmoClaude (#1221).
|
|
362
|
+
if (handleExternalLinkClick(event)) return;
|
|
357
363
|
const { target } = event;
|
|
358
364
|
if (!(target instanceof HTMLInputElement)) return;
|
|
359
365
|
if (target.type !== "checkbox") return;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Photo-locations View (#1222 PR-B). A list of every captured
|
|
3
|
+
// sidecar — photo path, lat/lng, takenAt, camera. The map handoff
|
|
4
|
+
// is by chat (the LLM calls `mapControl({action: "addMarker", lat,
|
|
5
|
+
// lng})` once asked) rather than embedded here, so this view stays
|
|
6
|
+
// small + always-loadable regardless of whether the user has a
|
|
7
|
+
// Google Maps API key set.
|
|
8
|
+
|
|
9
|
+
import { computed, onMounted, ref } from "vue";
|
|
10
|
+
import { useI18n } from "vue-i18n";
|
|
11
|
+
import { apiPost } from "../../utils/api";
|
|
12
|
+
import { pluginEndpoints } from "../api";
|
|
13
|
+
import type { ResolvedRoute } from "../meta-types";
|
|
14
|
+
import { errorMessage as toErrorMessage } from "../../utils/errors";
|
|
15
|
+
import { formatDate } from "../../utils/format/date";
|
|
16
|
+
|
|
17
|
+
interface Sidecar {
|
|
18
|
+
version: 1;
|
|
19
|
+
photo: { relativePath: string; mimeType: string };
|
|
20
|
+
exif: {
|
|
21
|
+
lat?: number;
|
|
22
|
+
lng?: number;
|
|
23
|
+
altitude?: number;
|
|
24
|
+
takenAt?: string;
|
|
25
|
+
make?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
lens?: string;
|
|
28
|
+
};
|
|
29
|
+
capturedAt: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ListedSidecar {
|
|
33
|
+
id: string;
|
|
34
|
+
relativePath: string;
|
|
35
|
+
sidecar: Sidecar;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ListResult {
|
|
39
|
+
message?: string;
|
|
40
|
+
data?: { locations: ListedSidecar[]; total: number };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PhotoLocationsEndpoints {
|
|
44
|
+
dispatch: ResolvedRoute;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { t } = useI18n();
|
|
48
|
+
const endpoints = pluginEndpoints<PhotoLocationsEndpoints>("photoLocations");
|
|
49
|
+
|
|
50
|
+
const locations = ref<ListedSidecar[]>([]);
|
|
51
|
+
const loading = ref(true);
|
|
52
|
+
const errorMessage = ref<string>("");
|
|
53
|
+
|
|
54
|
+
async function refresh(): Promise<void> {
|
|
55
|
+
loading.value = true;
|
|
56
|
+
errorMessage.value = "";
|
|
57
|
+
try {
|
|
58
|
+
const result = await apiPost<ListResult>(endpoints.dispatch.url, { kind: "list" });
|
|
59
|
+
if (!result.ok) throw new Error(result.error);
|
|
60
|
+
locations.value = result.data.data?.locations ?? [];
|
|
61
|
+
} catch (err) {
|
|
62
|
+
errorMessage.value = toErrorMessage(err);
|
|
63
|
+
} finally {
|
|
64
|
+
loading.value = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const withGps = computed(() => locations.value.filter((row) => hasFiniteCoords(row.sidecar.exif)));
|
|
69
|
+
|
|
70
|
+
onMounted(() => {
|
|
71
|
+
void refresh();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function fmtCoord(value: unknown): string {
|
|
75
|
+
// The handler validates lat/lng on write, but a hand-edited
|
|
76
|
+
// sidecar can still ship a string / null past the type — guard
|
|
77
|
+
// before calling `toFixed` so one bad row doesn't crash the View.
|
|
78
|
+
// (Codex review on PR #1250.)
|
|
79
|
+
return typeof value === "number" && Number.isFinite(value) ? value.toFixed(5) : "—";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fmtAltitude(value: unknown): string | null {
|
|
83
|
+
return typeof value === "number" && Number.isFinite(value) ? value.toFixed(0) : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fmtDate(iso: string | undefined): string {
|
|
87
|
+
if (!iso) return "—";
|
|
88
|
+
return formatDate(iso);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasFiniteCoords(exif: { lat?: unknown; lng?: unknown }): boolean {
|
|
92
|
+
return typeof exif.lat === "number" && Number.isFinite(exif.lat) && typeof exif.lng === "number" && Number.isFinite(exif.lng);
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div class="photo-locations-view">
|
|
98
|
+
<header>
|
|
99
|
+
<h2>{{ t("photoLocations.title") }}</h2>
|
|
100
|
+
<span class="count" data-testid="photo-locations-count">{{ t("photoLocations.summary", { total: locations.length, withGps: withGps.length }) }}</span>
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<p class="hint">{{ t("photoLocations.mapHint") }}</p>
|
|
104
|
+
|
|
105
|
+
<div v-if="loading" class="message loading">{{ t("photoLocations.loading") }}</div>
|
|
106
|
+
<div v-else-if="errorMessage" class="message error">⚠ {{ errorMessage }}</div>
|
|
107
|
+
<div v-else-if="locations.length === 0" class="message empty">{{ t("photoLocations.empty") }}</div>
|
|
108
|
+
|
|
109
|
+
<ul v-else class="rows">
|
|
110
|
+
<li v-for="row in locations" :key="row.id" class="row" :data-testid="`photo-locations-row-${row.id}`">
|
|
111
|
+
<div class="row-main">
|
|
112
|
+
<code class="path">{{ row.sidecar.photo.relativePath }}</code>
|
|
113
|
+
<span class="taken">{{ fmtDate(row.sidecar.exif.takenAt) }}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="row-meta">
|
|
116
|
+
<span v-if="hasFiniteCoords(row.sidecar.exif)" class="coords">
|
|
117
|
+
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -- coordinates emoji + decimal pair + altitude unit are language-neutral numeric formatters, not user-facing prose -->
|
|
118
|
+
📍 {{ fmtCoord(row.sidecar.exif.lat) }}, {{ fmtCoord(row.sidecar.exif.lng) }}
|
|
119
|
+
<span v-if="fmtAltitude(row.sidecar.exif.altitude)" class="altitude">({{ fmtAltitude(row.sidecar.exif.altitude) }}m)</span>
|
|
120
|
+
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
|
121
|
+
</span>
|
|
122
|
+
<span v-else class="no-gps">{{ t("photoLocations.noGps") }}</span>
|
|
123
|
+
<span v-if="row.sidecar.exif.model" class="camera">{{ row.sidecar.exif.model }}</span>
|
|
124
|
+
</div>
|
|
125
|
+
</li>
|
|
126
|
+
</ul>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
|
|
130
|
+
<style scoped>
|
|
131
|
+
.photo-locations-view {
|
|
132
|
+
height: 100%;
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
gap: 0.5rem;
|
|
136
|
+
padding: 0.75rem 1rem;
|
|
137
|
+
font-family: system-ui, sans-serif;
|
|
138
|
+
overflow-y: auto;
|
|
139
|
+
}
|
|
140
|
+
header {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: baseline;
|
|
143
|
+
gap: 0.75rem;
|
|
144
|
+
}
|
|
145
|
+
h2 {
|
|
146
|
+
margin: 0;
|
|
147
|
+
font-size: 1rem;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
color: #1f2937;
|
|
150
|
+
}
|
|
151
|
+
.count {
|
|
152
|
+
font-size: 0.75rem;
|
|
153
|
+
color: #6b7280;
|
|
154
|
+
}
|
|
155
|
+
.hint {
|
|
156
|
+
margin: 0;
|
|
157
|
+
font-size: 0.75rem;
|
|
158
|
+
color: #6b7280;
|
|
159
|
+
font-style: italic;
|
|
160
|
+
}
|
|
161
|
+
.message {
|
|
162
|
+
padding: 0.75rem 1rem;
|
|
163
|
+
border-radius: 0.5rem;
|
|
164
|
+
font-size: 0.875rem;
|
|
165
|
+
}
|
|
166
|
+
.message.loading,
|
|
167
|
+
.message.empty {
|
|
168
|
+
color: #6b7280;
|
|
169
|
+
}
|
|
170
|
+
.message.error {
|
|
171
|
+
background: #fee2e2;
|
|
172
|
+
border: 1px solid #fecaca;
|
|
173
|
+
color: #991b1b;
|
|
174
|
+
}
|
|
175
|
+
.rows {
|
|
176
|
+
list-style: none;
|
|
177
|
+
margin: 0;
|
|
178
|
+
padding: 0;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 0.375rem;
|
|
182
|
+
}
|
|
183
|
+
.row {
|
|
184
|
+
padding: 0.5rem 0.75rem;
|
|
185
|
+
border-radius: 0.375rem;
|
|
186
|
+
background: #f9fafb;
|
|
187
|
+
border: 1px solid #e5e7eb;
|
|
188
|
+
}
|
|
189
|
+
.row-main {
|
|
190
|
+
display: flex;
|
|
191
|
+
justify-content: space-between;
|
|
192
|
+
gap: 0.5rem;
|
|
193
|
+
align-items: baseline;
|
|
194
|
+
}
|
|
195
|
+
.path {
|
|
196
|
+
font-family: monospace;
|
|
197
|
+
font-size: 0.75rem;
|
|
198
|
+
color: #374151;
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
text-overflow: ellipsis;
|
|
201
|
+
white-space: nowrap;
|
|
202
|
+
flex: 1;
|
|
203
|
+
min-width: 0;
|
|
204
|
+
}
|
|
205
|
+
.taken {
|
|
206
|
+
font-size: 0.75rem;
|
|
207
|
+
color: #6b7280;
|
|
208
|
+
flex-shrink: 0;
|
|
209
|
+
}
|
|
210
|
+
.row-meta {
|
|
211
|
+
margin-top: 0.25rem;
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-wrap: wrap;
|
|
214
|
+
gap: 0.5rem;
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
color: #4b5563;
|
|
217
|
+
}
|
|
218
|
+
.coords {
|
|
219
|
+
font-family: monospace;
|
|
220
|
+
}
|
|
221
|
+
.altitude {
|
|
222
|
+
color: #9ca3af;
|
|
223
|
+
}
|
|
224
|
+
.no-gps {
|
|
225
|
+
color: #d97706;
|
|
226
|
+
font-style: italic;
|
|
227
|
+
}
|
|
228
|
+
.camera {
|
|
229
|
+
color: #6b7280;
|
|
230
|
+
}
|
|
231
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// MCP tool definition for `managePhotoLocations` (#1222 PR-B).
|
|
2
|
+
//
|
|
3
|
+
// Two kinds for v1:
|
|
4
|
+
// - `list` — every sidecar, newest first.
|
|
5
|
+
// - `count` — quick scalar for the View's badge.
|
|
6
|
+
//
|
|
7
|
+
// `extractExif` (on-demand re-read for one photo) and `rescan`
|
|
8
|
+
// (backfill sidecars for old uploads) ride a follow-up PR.
|
|
9
|
+
|
|
10
|
+
import type { ToolDefinition } from "gui-chat-protocol";
|
|
11
|
+
import { META } from "./meta";
|
|
12
|
+
|
|
13
|
+
export const TOOL_NAME = META.toolName;
|
|
14
|
+
|
|
15
|
+
export const PHOTO_LOCATIONS_KINDS = {
|
|
16
|
+
list: "list",
|
|
17
|
+
count: "count",
|
|
18
|
+
} as const;
|
|
19
|
+
export type PhotoLocationsKind = (typeof PHOTO_LOCATIONS_KINDS)[keyof typeof PHOTO_LOCATIONS_KINDS];
|
|
20
|
+
|
|
21
|
+
export interface PhotoLocationsArgs {
|
|
22
|
+
kind: PhotoLocationsKind;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Single tool, kind-discriminated. Same shape as
|
|
26
|
+
* manageAccounting / manageScheduler so the MCP bridge plugs in
|
|
27
|
+
* unchanged. */
|
|
28
|
+
const TOOL_DEFINITION: ToolDefinition = {
|
|
29
|
+
type: "function",
|
|
30
|
+
name: TOOL_NAME,
|
|
31
|
+
description:
|
|
32
|
+
"Read the photo-location sidecars produced by the EXIF auto-capture hook. " +
|
|
33
|
+
"Use `list` to fetch every captured location (lat/lng/altitude/takenAt/camera) " +
|
|
34
|
+
"for queries like 'show last week's photos on a map' — the lat/lng pair is " +
|
|
35
|
+
'shape-compatible with `mapControl({action: "addMarker"})` so you can hand ' +
|
|
36
|
+
"the result straight to the Google Map plugin without reshape. " +
|
|
37
|
+
"Use `count` for a quick scalar.",
|
|
38
|
+
parameters: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
kind: { type: "string", enum: Object.values(PHOTO_LOCATIONS_KINDS) },
|
|
42
|
+
},
|
|
43
|
+
required: ["kind"],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default TOOL_DEFINITION;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PluginRegistration, ToolPlugin } from "../../tools/types";
|
|
2
|
+
import type { ToolResult } from "gui-chat-protocol";
|
|
3
|
+
import View from "./View.vue";
|
|
4
|
+
import toolDefinition from "./definition";
|
|
5
|
+
import { META } from "./meta";
|
|
6
|
+
import { wrapWithScope } from "../scope";
|
|
7
|
+
import { apiCall } from "../../utils/api";
|
|
8
|
+
import { makeUuid } from "../../utils/id";
|
|
9
|
+
|
|
10
|
+
// Same one-line pass-through pattern as accounting / scheduler:
|
|
11
|
+
// production tool calls flow Claude → MCP → REST, never through
|
|
12
|
+
// `execute()`. The body satisfies the gui-chat-protocol shape and
|
|
13
|
+
// supports any host that does call it.
|
|
14
|
+
export type PhotoLocationsActionData = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
const photoLocationsPlugin: ToolPlugin<PhotoLocationsActionData> = {
|
|
17
|
+
toolDefinition,
|
|
18
|
+
|
|
19
|
+
async execute(_context, args) {
|
|
20
|
+
const { method, path } = META.apiRoutes.dispatch;
|
|
21
|
+
const result = await apiCall<ToolResult<PhotoLocationsActionData>>(`/api/${META.apiNamespace}${path}`, { method, body: args });
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
return { toolName: toolDefinition.name, uuid: makeUuid(), message: result.error };
|
|
24
|
+
}
|
|
25
|
+
return { ...result.data, toolName: toolDefinition.name, uuid: result.data.uuid ?? makeUuid() };
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
isEnabled: () => true,
|
|
29
|
+
generatingMessage: "Reading photo locations…",
|
|
30
|
+
viewComponent: wrapWithScope("photoLocations", View),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default photoLocationsPlugin;
|
|
34
|
+
|
|
35
|
+
export const REGISTRATION: PluginRegistration = {
|
|
36
|
+
toolName: META.toolName,
|
|
37
|
+
entry: photoLocationsPlugin,
|
|
38
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Central-registry metadata for the photo-locations plugin
|
|
2
|
+
// (#1222 PR-B). Owns one tool — `managePhotoLocations` — exposing
|
|
3
|
+
// the sidecars produced by the post-save EXIF hook (PR-A) so the
|
|
4
|
+
// LLM can answer "where were my photos taken" / "show last week's
|
|
5
|
+
// shots on a map" without poking at `data/locations/` directly.
|
|
6
|
+
//
|
|
7
|
+
// `mapControl` (`@gui-chat-plugin/google-map`) is the natural
|
|
8
|
+
// downstream consumer: a sidecar's `lat` / `lng` flow into
|
|
9
|
+
// `mapControl({ action: "addMarker", lat, lng })` without reshape.
|
|
10
|
+
//
|
|
11
|
+
// Browser-safe: no Vue imports, no server-only imports.
|
|
12
|
+
|
|
13
|
+
import { definePluginMeta } from "../meta-types";
|
|
14
|
+
|
|
15
|
+
export const META = definePluginMeta({
|
|
16
|
+
toolName: "managePhotoLocations",
|
|
17
|
+
apiNamespace: "photoLocations",
|
|
18
|
+
apiRoutes: {
|
|
19
|
+
/** POST /api/photoLocations — single dispatch with action
|
|
20
|
+
* discriminator. Mirrors the accounting / scheduler
|
|
21
|
+
* convention so the MCP bridge plugs in unchanged. */
|
|
22
|
+
dispatch: { method: "POST", path: "" },
|
|
23
|
+
},
|
|
24
|
+
mcpDispatch: "dispatch",
|
|
25
|
+
// Sidecar storage already lives at the host-level
|
|
26
|
+
// `WORKSPACE_DIRS.locations` (declared by the host because the
|
|
27
|
+
// hook runs server-side on every saved attachment). No new
|
|
28
|
+
// directories belong to this plugin — it's a read surface over
|
|
29
|
+
// host data. This META therefore omits `workspaceDirs`.
|
|
30
|
+
staticChannels: {
|
|
31
|
+
/** Published whenever a sidecar is added / removed so an open
|
|
32
|
+
* View refreshes without polling. */
|
|
33
|
+
locationsChanged: "photoLocations:locations-changed",
|
|
34
|
+
},
|
|
35
|
+
});
|