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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Encore-scoped notifier wrapper.
|
|
2
|
+
//
|
|
3
|
+
// Closes over:
|
|
4
|
+
// - `pluginPkg`: a constant identity string so every Encore-owned
|
|
5
|
+
// bell entry is tagged uniformly, and `clearForPlugin` can
|
|
6
|
+
// enforce plugin-scoped clearing.
|
|
7
|
+
// - lifecycle: every Encore bell entry uses `lifecycle: "action"`
|
|
8
|
+
// so the host's NotificationBell.vue does NOT auto-clear on
|
|
9
|
+
// user click — clearing is owned by the LLM (markStepDone /
|
|
10
|
+
// markTargetSkipped close the underlying step which clears the
|
|
11
|
+
// bell). The host's `validateActionCoherence` rejects
|
|
12
|
+
// `action` + `info`, so the wrapper also maps DSL `info` (and
|
|
13
|
+
// `warning`) severities to host `nudge` at the publish
|
|
14
|
+
// boundary.
|
|
15
|
+
//
|
|
16
|
+
// Pure passthrough otherwise — no caching, no batching, no other
|
|
17
|
+
// semantics. Tick + handlers import this module rather than
|
|
18
|
+
// reaching for the raw host engine.
|
|
19
|
+
|
|
20
|
+
import * as engine from "../notifier/engine.js";
|
|
21
|
+
import type { Severity } from "../../src/types/encore-dsl/schema.js";
|
|
22
|
+
|
|
23
|
+
/** Identity string used as `pluginPkg` on every Encore bell entry.
|
|
24
|
+
* Stable across versions; lives next to the apiNamespace ("encore")
|
|
25
|
+
* and the tool name ("manageEncore") as part of the plugin's
|
|
26
|
+
* identity. */
|
|
27
|
+
export const ENCORE_PLUGIN_PKG = "encore" as const;
|
|
28
|
+
|
|
29
|
+
export interface PublishArgs {
|
|
30
|
+
severity: Severity;
|
|
31
|
+
title: string;
|
|
32
|
+
body?: string;
|
|
33
|
+
navigateTarget: string;
|
|
34
|
+
pluginData?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Map Encore's DSL-facing severity vocabulary
|
|
38
|
+
* (`info | warning | urgent`, picked for clarity in plain-language
|
|
39
|
+
* prompts the LLM composes against) to the host notifier's
|
|
40
|
+
* vocabulary (`info | nudge | urgent`).
|
|
41
|
+
*
|
|
42
|
+
* We deliberately avoid the host's `info` severity entirely:
|
|
43
|
+
* Encore always uses `lifecycle: "action"` so the LLM (not the
|
|
44
|
+
* bell) owns when the entry goes away — the user clicks the
|
|
45
|
+
* bell, lands in a chat, talks with the LLM, and the LLM calls
|
|
46
|
+
* `markStepDone` / `markTargetSkipped` to clear. The host's
|
|
47
|
+
* publish-time coherence check rejects `action` + `info`, so we
|
|
48
|
+
* map DSL `info` to host `nudge` (the mid-intensity bucket). We
|
|
49
|
+
* lose the visual distinction between info and warning at the
|
|
50
|
+
* bell level; the LLM still differentiates them in title / body /
|
|
51
|
+
* conversation tone. */
|
|
52
|
+
function toHostSeverity(severity: Severity): "nudge" | "urgent" {
|
|
53
|
+
if (severity === "urgent") return "urgent";
|
|
54
|
+
return "nudge";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Publish an Encore notification. Always emits
|
|
58
|
+
* `lifecycle: "action"` so the host's bell does NOT auto-clear on
|
|
59
|
+
* click — Encore owns the clear via markStepDone /
|
|
60
|
+
* markTargetSkipped. Returns the host-assigned notification id. */
|
|
61
|
+
export async function publish(args: PublishArgs): Promise<{ id: string }> {
|
|
62
|
+
return engine.publish({
|
|
63
|
+
pluginPkg: ENCORE_PLUGIN_PKG,
|
|
64
|
+
severity: toHostSeverity(args.severity),
|
|
65
|
+
lifecycle: "action",
|
|
66
|
+
title: args.title,
|
|
67
|
+
body: args.body,
|
|
68
|
+
navigateTarget: args.navigateTarget,
|
|
69
|
+
pluginData: args.pluginData,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** In-place update of an Encore notification's presentation —
|
|
74
|
+
* same id, same `pluginPkg`, same `lifecycle`, same `createdAt`,
|
|
75
|
+
* new severity / title / body. The reconciler's trim path uses
|
|
76
|
+
* this to flush DSL-driven content drift (an `amendDefinition`
|
|
77
|
+
* edited a `displayName`, the bundle shrunk so the body's member
|
|
78
|
+
* count changed, the cadence pushed us into a higher-severity
|
|
79
|
+
* phase) without flickering the bell entry through a clear +
|
|
80
|
+
* publish — which would litter history with `cleared` records
|
|
81
|
+
* and assign a fresh id every time. Mirrors the DSL → host
|
|
82
|
+
* vocabulary mapping in `publish` so a phase escalation from
|
|
83
|
+
* `info` to `urgent` ends up at host `nudge` → `urgent` rather
|
|
84
|
+
* than confusing the validator.
|
|
85
|
+
*
|
|
86
|
+
* No-op on unknown / cross-plugin ids, and on patches that would
|
|
87
|
+
* invalidate the entry (e.g. info severity on the action
|
|
88
|
+
* lifecycle we always use). Caller treats the failure modes
|
|
89
|
+
* uniformly — same isolation property as `clear`. */
|
|
90
|
+
export async function update(
|
|
91
|
+
entryId: string,
|
|
92
|
+
patch: {
|
|
93
|
+
severity?: Severity;
|
|
94
|
+
title?: string;
|
|
95
|
+
body?: string;
|
|
96
|
+
},
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
await engine.updateForPlugin(ENCORE_PLUGIN_PKG, entryId, {
|
|
99
|
+
...(patch.severity !== undefined ? { severity: toHostSeverity(patch.severity) } : {}),
|
|
100
|
+
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
101
|
+
...(patch.body !== undefined ? { body: patch.body } : {}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Clear an Encore notification. No-ops on unknown / cross-plugin
|
|
106
|
+
* ids — matches host `clearForPlugin` semantics, plugin can't
|
|
107
|
+
* dismiss another plugin's entries. */
|
|
108
|
+
export async function clear(entryId: string): Promise<void> {
|
|
109
|
+
await engine.clearForPlugin(ENCORE_PLUGIN_PKG, entryId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** True iff a live bell with this id still exists in the notifier
|
|
113
|
+
* AND belongs to Encore. Used by the reconciler to detect "ghost
|
|
114
|
+
* tickets" — a ticket whose bell was dismissed out-of-band by the
|
|
115
|
+
* host UI (or wiped by a crashed active.json) so that the next
|
|
116
|
+
* tick republishes instead of trusting ticket-existence as proof
|
|
117
|
+
* of bell-existence. Cross-plugin entries (theoretically
|
|
118
|
+
* impossible since the notifier ids are namespaced, but defense
|
|
119
|
+
* in depth) read as not-ours. */
|
|
120
|
+
export async function bellExists(entryId: string): Promise<boolean> {
|
|
121
|
+
const entry = await engine.get(entryId);
|
|
122
|
+
return entry !== undefined && entry.pluginPkg === ENCORE_PLUGIN_PKG;
|
|
123
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Helpers for the obligation `index.md` file (DSL frontmatter +
|
|
2
|
+
// free-form body). Symmetric with cycle.ts's parse/serialize.
|
|
3
|
+
|
|
4
|
+
import { parseEncoreFrontmatter, serializeEncoreFrontmatter } from "./yaml-fm.js";
|
|
5
|
+
import { EncoreDslInput, type EncoreDsl } from "../../src/types/encore-dsl/schema.js";
|
|
6
|
+
|
|
7
|
+
/** Parse an obligation's index.md raw markdown into (dsl, body).
|
|
8
|
+
* Re-runs the Zod validator so a hand-edited / corrupted file
|
|
9
|
+
* surfaces as a parse error rather than passing garbage downstream. */
|
|
10
|
+
export function parseIndexFile(raw: string): { dsl: EncoreDsl; body: string } {
|
|
11
|
+
const parsed = parseEncoreFrontmatter(raw);
|
|
12
|
+
if (!parsed.hasHeader) {
|
|
13
|
+
throw new Error("obligation index.md: missing YAML frontmatter");
|
|
14
|
+
}
|
|
15
|
+
const dsl = EncoreDslInput.parse(parsed.meta);
|
|
16
|
+
return { dsl, body: parsed.body };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Serialize a DSL + body back to markdown. The DSL's
|
|
20
|
+
* `EncoreDslInput.parse` output preserves all declared fields
|
|
21
|
+
* including the server-generated `id` and `createdAt`, so this is
|
|
22
|
+
* a straight YAML dump. */
|
|
23
|
+
export function serializeIndexFile(dsl: EncoreDsl, body: string): string {
|
|
24
|
+
return serializeEncoreFrontmatter(dsl as unknown as Record<string, unknown>, body);
|
|
25
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Workspace-relative path helpers for the Encore plugin. Pure
|
|
2
|
+
// string composition — no fs here; the io layer joins these with
|
|
3
|
+
// WORKSPACE_PATHS.encore to get absolute paths.
|
|
4
|
+
//
|
|
5
|
+
// Layout under data/plugins/encore/:
|
|
6
|
+
//
|
|
7
|
+
// obligations/<obligationId>/index.md ← DSL + free-form body
|
|
8
|
+
// obligations/<obligationId>/<cycleId>.md ← per-cycle state + body
|
|
9
|
+
// tickets/<pendingId>.json ← live-bell tickets (chat-on-mount)
|
|
10
|
+
//
|
|
11
|
+
// `obligationId` and `cycleId` flow through the kebab-id validator
|
|
12
|
+
// upstream (DSL schema) and the cadence id formatter — never raw
|
|
13
|
+
// user input, so the path-traversal hardening accounting-io.ts
|
|
14
|
+
// needs is unnecessary here. We still keep a safety assertion at
|
|
15
|
+
// each join to catch any future regression.
|
|
16
|
+
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
20
|
+
|
|
21
|
+
function assertSafeSegment(label: string, value: string): void {
|
|
22
|
+
if (!SAFE_SEGMENT.test(value)) {
|
|
23
|
+
throw new Error(`encore: invalid ${label} ${JSON.stringify(value)} (must match ${SAFE_SEGMENT.source})`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const OBLIGATIONS_DIRNAME = "obligations";
|
|
28
|
+
export const TICKETS_DIRNAME = "tickets";
|
|
29
|
+
|
|
30
|
+
/** "obligations" — relative to the plugin root. */
|
|
31
|
+
export function obligationsDir(): string {
|
|
32
|
+
return OBLIGATIONS_DIRNAME;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** "obligations/<id>" — directory holding index.md + cycle files. */
|
|
36
|
+
export function obligationDir(obligationId: string): string {
|
|
37
|
+
assertSafeSegment("obligationId", obligationId);
|
|
38
|
+
return path.join(OBLIGATIONS_DIRNAME, obligationId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** "obligations/<id>/index.md" — the DSL document. */
|
|
42
|
+
export function obligationIndexPath(obligationId: string): string {
|
|
43
|
+
return path.join(obligationDir(obligationId), "index.md");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** "obligations/<id>/<cycleId>.md" — the per-cycle state file. */
|
|
47
|
+
export function cycleFilePath(obligationId: string, cycleId: string): string {
|
|
48
|
+
assertSafeSegment("cycleId", cycleId);
|
|
49
|
+
return path.join(obligationDir(obligationId), `${cycleId}.md`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** "tickets/<pendingId>.json" — the live-bell ticket (carries the
|
|
53
|
+
* seed prompt for chat-on-mount, the severity baseline for
|
|
54
|
+
* escalation diff, and the chatSessionId binding once the user
|
|
55
|
+
* has clicked the bell at least once). */
|
|
56
|
+
export function ticketPath(pendingId: string): string {
|
|
57
|
+
assertSafeSegment("pendingId", pendingId);
|
|
58
|
+
return path.join(TICKETS_DIRNAME, `${pendingId}.json`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Slugify a display name into a kebab-case id. Deterministic,
|
|
62
|
+
* ASCII-only, safe for use as a filesystem segment. Used by setup
|
|
63
|
+
* to generate an obligation id from the user-supplied
|
|
64
|
+
* displayName. */
|
|
65
|
+
export function slugify(displayName: string): string {
|
|
66
|
+
const slug = displayName
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.normalize("NFKD")
|
|
69
|
+
.replace(/[̀-ͯ]/g, "") // strip diacritics
|
|
70
|
+
.replace(/[^a-z0-9]+/g, "-") // collapse non-alphanumerics
|
|
71
|
+
.replace(/^-/, "") // trim leading dash (only one possible: collapse-step above guarantees no consecutive dashes)
|
|
72
|
+
.replace(/-$/, ""); // trim trailing dash
|
|
73
|
+
if (!slug) return "obligation";
|
|
74
|
+
// Slug must start with a letter (KEBAB regex in schema). If the
|
|
75
|
+
// first char ended up as a digit, prefix "o-".
|
|
76
|
+
if (!/^[a-z]/.test(slug)) return `o-${slug}`;
|
|
77
|
+
return slug;
|
|
78
|
+
}
|