mulmoclaude 1.2.0 → 1.3.0
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/client/assets/{PluginScopedRoot-CjiZ2FJI.js → PluginScopedRoot-B53YSjaC.js} +1 -1
- package/client/assets/index-D3czxq1I.css +2 -0
- package/client/assets/{index-yQkLdtMk.js → index-DUdqhWg6.js} +42 -43
- package/client/assets/{marp-Bs_qwoKZ.js → marp-CyWxL3bc.js} +12 -3
- package/client/index.html +3 -3
- package/package.json +6 -5
- package/server/agent/attachmentConverter.ts +0 -25
- package/server/api/auth/viewToken.ts +10 -8
- package/server/api/routes/agent.ts +35 -10
- package/server/api/routes/collections.ts +150 -96
- package/server/api/routes/files.ts +57 -54
- package/server/api/routes/mulmo-script.ts +257 -895
- package/server/api/routes/wiki/history.ts +17 -19
- package/server/events/collection-change.ts +0 -5
- package/server/events/file-change.ts +1 -6
- package/server/index.ts +25 -76
- package/server/plugins/diagnostics.ts +0 -6
- package/server/plugins/mulmoscript-server.ts +58 -0
- package/server/prompts/system/system.md +2 -2
- package/server/remoteHost/handlers/getCollection.ts +5 -4
- package/server/remoteHost/handlers/googleCalendar.ts +50 -3
- package/server/remoteHost/handlers/index.ts +3 -1
- package/server/utils/files/html-store.ts +2 -16
- package/server/utils/files/journal-io.ts +0 -12
- package/server/utils/files/plugins-io.ts +2 -11
- package/server/utils/files/safe.ts +39 -0
- package/server/utils/httpError.ts +6 -0
- package/server/utils/time.ts +0 -3
- package/server/workspace/collections/index.ts +1 -0
- package/server/workspace/collections/remoteView.ts +13 -4
- package/server/workspace/journal/state.ts +1 -9
- package/server/workspace/memory/topic-index-hook.ts +0 -9
- package/server/workspace/memory/topic-io.ts +0 -4
- package/server/workspace/skills/external/catalog.ts +24 -13
- package/server/workspace/skills/writer.ts +20 -16
- package/server/workspace/wiki-pages/snapshot.ts +13 -12
- package/src/composables/useFileDropZone.ts +3 -15
- package/src/config/apiRoutes.ts +6 -0
- package/src/config/roles.ts +0 -4
- package/src/lang/de.ts +0 -30
- package/src/lang/en.ts +0 -30
- package/src/lang/es.ts +0 -30
- package/src/lang/fr.ts +0 -30
- package/src/lang/ja.ts +0 -30
- package/src/lang/ko.ts +0 -30
- package/src/lang/pt-BR.ts +0 -30
- package/src/lang/zh.ts +0 -30
- package/src/plugins/accounting/index.ts +0 -2
- package/src/plugins/api.ts +0 -6
- package/src/plugins/canvas/index.ts +0 -2
- package/src/plugins/chart/index.ts +0 -2
- package/src/plugins/editImages/index.ts +0 -2
- package/src/plugins/generateImage/index.ts +0 -2
- package/src/plugins/manageSkills/index.ts +0 -2
- package/src/plugins/markdown/index.ts +0 -2
- package/src/plugins/photoLocations/index.ts +0 -2
- package/src/plugins/presentCollection/index.ts +0 -2
- package/src/plugins/presentForm/index.ts +0 -2
- package/src/plugins/presentHtml/index.ts +0 -2
- package/src/plugins/presentMulmoScript/definition.ts +11 -116
- package/src/plugins/presentMulmoScript/index.ts +62 -12
- package/src/plugins/presentSVG/index.ts +0 -2
- package/src/plugins/skill/index.ts +0 -3
- package/src/plugins/spreadsheet/engine/parser.ts +0 -103
- package/src/plugins/spreadsheet/index.ts +0 -2
- package/src/plugins/textResponse/index.ts +0 -3
- package/src/plugins/wiki/index.ts +0 -2
- package/src/types/notification.ts +0 -10
- package/src/utils/api.ts +0 -4
- package/client/assets/index-D5nJbMO1.css +0 -2
- package/server/api/routes/mulmoScriptValidate.ts +0 -101
- package/server/utils/mulmoErrorCapture.ts +0 -93
- package/src/plugins/presentMulmoScript/Preview.vue +0 -23
- package/src/plugins/presentMulmoScript/View.vue +0 -1949
- package/src/plugins/presentMulmoScript/helpers.ts +0 -206
|
@@ -23,12 +23,13 @@ import {
|
|
|
23
23
|
} from "@mulmoclaude/core/remote-view";
|
|
24
24
|
import { enrichItems } from "@mulmoclaude/core/collection/server";
|
|
25
25
|
import {
|
|
26
|
+
collectionWritable,
|
|
26
27
|
deleteItem,
|
|
27
|
-
listItems,
|
|
28
28
|
readCustomViewHtml,
|
|
29
29
|
readCustomViewI18n,
|
|
30
30
|
readItem,
|
|
31
31
|
safeRecordId,
|
|
32
|
+
storeFor,
|
|
32
33
|
writeItem,
|
|
33
34
|
type CollectionCustomView,
|
|
34
35
|
type CollectionItem,
|
|
@@ -108,6 +109,7 @@ export type MutateRemoteViewResult =
|
|
|
108
109
|
| { kind: "view-not-found"; viewId: string }
|
|
109
110
|
| { kind: "not-mobile"; viewId: string }
|
|
110
111
|
| { kind: "not-writable"; viewId: string }
|
|
112
|
+
| { kind: "read-only-collection" }
|
|
111
113
|
| { kind: "field-not-editable"; field: string }
|
|
112
114
|
| { kind: "delete-not-allowed" }
|
|
113
115
|
| { kind: "invalid-patch" }
|
|
@@ -129,6 +131,9 @@ export const createMutateRemoteView =
|
|
|
129
131
|
const view = (collection.schema.views ?? []).find((entry) => entry.id === viewId);
|
|
130
132
|
if (!view) return { kind: "view-not-found", viewId };
|
|
131
133
|
if (view.target !== "mobile") return { kind: "not-mobile", viewId };
|
|
134
|
+
// A dataSource collection is read-only regardless of what write surface
|
|
135
|
+
// the view declares — the collection-level rule outranks the view's.
|
|
136
|
+
if (!collectionWritable(collection)) return { kind: "read-only-collection" };
|
|
132
137
|
if (!isWritableView(view)) return { kind: "not-writable", viewId };
|
|
133
138
|
return request.op === "delete" ? deleteViaView(deps, collection, view.allowDelete === true, request.id) : updateViaView(deps, collection, view, request);
|
|
134
139
|
};
|
|
@@ -213,7 +218,9 @@ export type RemoteViewItemsResult =
|
|
|
213
218
|
| { kind: "too-large"; bytes: number };
|
|
214
219
|
|
|
215
220
|
export interface RemoteViewItemsDeps {
|
|
216
|
-
|
|
221
|
+
/** Load every record of the collection — store-aware (file records or a
|
|
222
|
+
* `dataSource` CSV's rows), unlike a raw dataDir `listItems`. */
|
|
223
|
+
listRecords: (collection: LoadedCollection) => Promise<CollectionItem[]>;
|
|
217
224
|
enrichItems: typeof enrichItems;
|
|
218
225
|
resolveThumbnail: typeof resolveThumbnail;
|
|
219
226
|
}
|
|
@@ -271,7 +278,7 @@ export const createRemoteViewItems =
|
|
|
271
278
|
// formulas evaluated with a full ref cache (`ticker.price`, `shares * ticker.price`
|
|
272
279
|
// resolve), toggles projected, embeds resolved. The phone gets plain resolved
|
|
273
280
|
// scalars — no network, no dataUrl — so mobile numbers match desktop exactly.
|
|
274
|
-
const items = await deps.
|
|
281
|
+
const items = await deps.listRecords(collection);
|
|
275
282
|
const derived = (await deps.enrichItems(collection, items)) as RemoteViewItem[];
|
|
276
283
|
const page = pageFromItems(derived, request, collection.schema.primaryKey);
|
|
277
284
|
// Resolving an `embed` column attaches a whole target record per row, so the
|
|
@@ -289,7 +296,7 @@ export const createRemoteViewItems =
|
|
|
289
296
|
return { kind: "ok", page, inlined, omitted };
|
|
290
297
|
};
|
|
291
298
|
|
|
292
|
-
export const remoteViewItems = createRemoteViewItems({
|
|
299
|
+
export const remoteViewItems = createRemoteViewItems({ listRecords: (collection) => storeFor(collection).list(), enrichItems, resolveThumbnail });
|
|
293
300
|
|
|
294
301
|
/** Message per non-ok item-page kind — shared by the channel handler (throws)
|
|
295
302
|
* and the HTTP route (sends with the matching status). */
|
|
@@ -307,6 +314,8 @@ export function mutateRemoteViewFailureMessage(result: Exclude<MutateRemoteViewR
|
|
|
307
314
|
if (result.kind === "not-mobile") return `custom view '${result.viewId}' is not a mobile view — declare target: "mobile" in its views[] entry`;
|
|
308
315
|
if (result.kind === "not-writable")
|
|
309
316
|
return `mobile view '${result.viewId}' is read-only — declare editableFields and/or allowDelete in its views[] entry to allow writes`;
|
|
317
|
+
if (result.kind === "read-only-collection")
|
|
318
|
+
return `collection '${slug}' is read-only (backed by an external dataSource) — update the data file itself instead`;
|
|
310
319
|
if (result.kind === "field-not-editable")
|
|
311
320
|
return `field '${result.field}' is not editable from this view — add it to the view's editableFields (the primary key is never editable)`;
|
|
312
321
|
if (result.kind === "delete-not-allowed") return `this view may not delete records — set allowDelete: true in its views[] entry`;
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
readJournalState as readJournalStateRaw,
|
|
3
|
-
writeJournalState as writeJournalStateRaw,
|
|
4
|
-
journalStateExists as journalStateExistsRaw,
|
|
5
|
-
} from "../../utils/files/journal-io.js";
|
|
1
|
+
import { readJournalState as readJournalStateRaw, writeJournalState as writeJournalStateRaw } from "../../utils/files/journal-io.js";
|
|
6
2
|
import { ONE_HOUR_MS, ONE_DAY_MS } from "../../utils/time.js";
|
|
7
3
|
import { log } from "../../system/logger/index.js";
|
|
8
4
|
import { isRecord } from "../../utils/types.js";
|
|
@@ -106,7 +102,3 @@ export async function readState(workspaceRoot: string): Promise<JournalState> {
|
|
|
106
102
|
export async function writeState(workspaceRoot: string, state: JournalState): Promise<void> {
|
|
107
103
|
await writeJournalStateRaw(state, workspaceRoot);
|
|
108
104
|
}
|
|
109
|
-
|
|
110
|
-
export function stateFileExists(workspaceRoot: string): boolean {
|
|
111
|
-
return journalStateExistsRaw(workspaceRoot);
|
|
112
|
-
}
|
|
@@ -117,12 +117,3 @@ export async function maybeRegenerateTopicIndex(relativePath: string): Promise<b
|
|
|
117
117
|
return false;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
/** Test-only — drop the per-workspace regen chain so each test
|
|
122
|
-
* starts with a fresh queue. Without this, a chain rejection from
|
|
123
|
-
* one test could carry into the next (the .catch in `chainRegen`
|
|
124
|
-
* swallows it for the next caller, but the test runner sees the
|
|
125
|
-
* unhandled-rejection from the original promise). */
|
|
126
|
-
export function _resetTopicIndexRegenChainForTesting(): void {
|
|
127
|
-
regenChains.clear();
|
|
128
|
-
}
|
|
@@ -33,10 +33,6 @@ export function topicMemoryIndexPath(workspaceRoot: string): string {
|
|
|
33
33
|
return path.join(workspaceRoot, WORKSPACE_FILES.memoryIndex);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function topicFilePath(workspaceRoot: string, type: MemoryType, topic: string): string {
|
|
37
|
-
return path.join(topicMemoryRoot(workspaceRoot), type, `${topic}.md`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
36
|
// Both loaders walk types in `MEMORY_TYPES` order (a stable
|
|
41
37
|
// constant) and sort filenames within each type by name. This pins
|
|
42
38
|
// the order of entries in the agent's system prompt so a `readdir`
|
|
@@ -198,19 +198,33 @@ async function resolveSource(repoIdRaw: string, skillFolderRaw: string, workspac
|
|
|
198
198
|
return { repoId, skillFolder, sourceDir, url: meta.url };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// Resolve a catalog source, or classify why it failed: a bad-shape id
|
|
202
|
+
// (rejected upstream) → `invalid-id`, otherwise missing-on-disk →
|
|
203
|
+
// `not-found`. The detail-read and star routes share this preamble; the
|
|
204
|
+
// error variants are a subset of both routes' result types.
|
|
205
|
+
type SourceError = { kind: "invalid-id" } | { kind: "not-found"; repoId: string; skillFolder: string };
|
|
206
|
+
|
|
207
|
+
async function resolveSourceOrError(
|
|
208
|
+
repoIdRaw: string,
|
|
209
|
+
skillFolderRaw: string,
|
|
210
|
+
workspaceRoot: string,
|
|
211
|
+
): Promise<{ ok: true; resolved: ResolvedSource } | { ok: false; error: SourceError }> {
|
|
212
|
+
const resolved = await resolveSource(repoIdRaw, skillFolderRaw, workspaceRoot);
|
|
213
|
+
if (resolved) return { ok: true, resolved };
|
|
214
|
+
if (!isSafeRepoId(repoIdRaw)) return { ok: false, error: { kind: "invalid-id" } };
|
|
215
|
+
if (skillFolderRaw !== "." && safeFolderName(skillFolderRaw) === null) return { ok: false, error: { kind: "invalid-id" } };
|
|
216
|
+
return { ok: false, error: { kind: "not-found", repoId: repoIdRaw, skillFolder: skillFolderRaw } };
|
|
217
|
+
}
|
|
218
|
+
|
|
201
219
|
export async function readExternalCatalogDetail(
|
|
202
220
|
repoIdRaw: string,
|
|
203
221
|
skillFolderRaw: string,
|
|
204
222
|
opts: ExternalCatalogOptions = {},
|
|
205
223
|
): Promise<ExternalCatalogDetailResult> {
|
|
206
224
|
const workspaceRoot = opts.workspaceRoot ?? workspacePath;
|
|
207
|
-
const
|
|
208
|
-
if (!
|
|
209
|
-
|
|
210
|
-
if (!isSafeRepoId(repoIdRaw)) return { kind: "invalid-id" };
|
|
211
|
-
if (skillFolderRaw !== "." && safeFolderName(skillFolderRaw) === null) return { kind: "invalid-id" };
|
|
212
|
-
return { kind: "not-found", repoId: repoIdRaw, skillFolder: skillFolderRaw };
|
|
213
|
-
}
|
|
225
|
+
const source = await resolveSourceOrError(repoIdRaw, skillFolderRaw, workspaceRoot);
|
|
226
|
+
if (!source.ok) return source.error;
|
|
227
|
+
const { resolved } = source;
|
|
214
228
|
const skillMd = path.join(resolved.sourceDir, "SKILL.md");
|
|
215
229
|
let raw: string;
|
|
216
230
|
try {
|
|
@@ -265,12 +279,9 @@ export type ExternalStarResult =
|
|
|
265
279
|
|
|
266
280
|
export async function starExternalCatalogEntry(repoIdRaw: string, skillFolderRaw: string, opts: ExternalCatalogOptions = {}): Promise<ExternalStarResult> {
|
|
267
281
|
const workspaceRoot = opts.workspaceRoot ?? workspacePath;
|
|
268
|
-
const
|
|
269
|
-
if (!
|
|
270
|
-
|
|
271
|
-
if (skillFolderRaw !== "." && safeFolderName(skillFolderRaw) === null) return { kind: "invalid-id" };
|
|
272
|
-
return { kind: "not-found", repoId: repoIdRaw, skillFolder: skillFolderRaw };
|
|
273
|
-
}
|
|
282
|
+
const source = await resolveSourceOrError(repoIdRaw, skillFolderRaw, workspaceRoot);
|
|
283
|
+
if (!source.ok) return source.error;
|
|
284
|
+
const { resolved } = source;
|
|
274
285
|
const activeId = deriveActiveId(resolved.url, resolved.skillFolder === "." ? null : resolved.skillFolder);
|
|
275
286
|
if (!activeId) return { kind: "invalid-id" };
|
|
276
287
|
const activeSlugDir = path.join(activeDir(workspaceRoot), activeId);
|
|
@@ -35,21 +35,31 @@ export type SaveResult =
|
|
|
35
35
|
| { kind: "missing-field"; field: "description" | "body" }
|
|
36
36
|
| { kind: "exists"; name: string };
|
|
37
37
|
|
|
38
|
+
// The slug + required-field checks shared by save and update. Returns
|
|
39
|
+
// the error variant to return verbatim, or null when the input is
|
|
40
|
+
// valid. Its variants are a subset of both SaveResult and UpdateResult.
|
|
41
|
+
type SkillInputProblem = { kind: "invalid-slug"; slug: string } | { kind: "missing-field"; field: "description" | "body" };
|
|
42
|
+
|
|
43
|
+
function validateSkillInput(input: SaveSkillInput): SkillInputProblem | null {
|
|
44
|
+
if (!isValidSlug(input.name)) return { kind: "invalid-slug", slug: input.name };
|
|
45
|
+
if (typeof input.description !== "string" || input.description.trim().length === 0) {
|
|
46
|
+
return { kind: "missing-field", field: "description" };
|
|
47
|
+
}
|
|
48
|
+
if (typeof input.body !== "string") {
|
|
49
|
+
return { kind: "missing-field", field: "body" };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
/**
|
|
39
55
|
* Write a new SKILL.md atomically. Refuses to overwrite — if the
|
|
40
56
|
* skill already exists at either scope, returns `kind: "exists"`.
|
|
41
57
|
*/
|
|
42
58
|
export async function saveProjectSkill(input: SaveSkillInput): Promise<SaveResult> {
|
|
59
|
+
const problem = validateSkillInput(input);
|
|
60
|
+
if (problem) return problem;
|
|
43
61
|
const { workspaceRoot, name, description, body } = input;
|
|
44
62
|
|
|
45
|
-
if (!isValidSlug(name)) return { kind: "invalid-slug", slug: name };
|
|
46
|
-
if (typeof description !== "string" || description.trim().length === 0) {
|
|
47
|
-
return { kind: "missing-field", field: "description" };
|
|
48
|
-
}
|
|
49
|
-
if (typeof body !== "string") {
|
|
50
|
-
return { kind: "missing-field", field: "body" };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
63
|
// Conflict check across BOTH scopes — we don't want to shadow a
|
|
54
64
|
// user-scope skill with the same name (project would silently
|
|
55
65
|
// override it via the precedence rule).
|
|
@@ -87,16 +97,10 @@ export type UpdateResult =
|
|
|
87
97
|
* user-scope skills and rejects names that don't exist.
|
|
88
98
|
*/
|
|
89
99
|
export async function updateProjectSkill(input: SaveSkillInput): Promise<UpdateResult> {
|
|
100
|
+
const problem = validateSkillInput(input);
|
|
101
|
+
if (problem) return problem;
|
|
90
102
|
const { workspaceRoot, name, description, body } = input;
|
|
91
103
|
|
|
92
|
-
if (!isValidSlug(name)) return { kind: "invalid-slug", slug: name };
|
|
93
|
-
if (typeof description !== "string" || description.trim().length === 0) {
|
|
94
|
-
return { kind: "missing-field", field: "description" };
|
|
95
|
-
}
|
|
96
|
-
if (typeof body !== "string") {
|
|
97
|
-
return { kind: "missing-field", field: "body" };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
104
|
const existing = await discoverSkills({ workspaceRoot });
|
|
101
105
|
const skill = existing.find((candidate) => candidate.name === name);
|
|
102
106
|
if (!skill) return { kind: "not-found", name };
|
|
@@ -219,12 +219,7 @@ export async function gcSnapshots(slug: string, now: Date, opts: SnapshotPathOpt
|
|
|
219
219
|
// lexicographically sortable because it's zero-padded ISO with
|
|
220
220
|
// colons swapped. Same-millisecond writes resolve via the
|
|
221
221
|
// shortId tail in `stamp` for tie-break consistency.
|
|
222
|
-
entries.sort(
|
|
223
|
-
if (left.filenameStamp !== right.filenameStamp) {
|
|
224
|
-
return left.filenameStamp < right.filenameStamp ? 1 : -1;
|
|
225
|
-
}
|
|
226
|
-
return left.stamp < right.stamp ? 1 : left.stamp > right.stamp ? -1 : 0;
|
|
227
|
-
});
|
|
222
|
+
entries.sort(compareSnapshotsNewestFirst);
|
|
228
223
|
|
|
229
224
|
const cutoffMs = now.getTime() - SNAPSHOT_RETAIN_DAYS * ONE_DAY_MS;
|
|
230
225
|
|
|
@@ -252,6 +247,17 @@ interface SnapshotEntry {
|
|
|
252
247
|
fileName: string;
|
|
253
248
|
}
|
|
254
249
|
|
|
250
|
+
// Newest-first ordering: by `filenameStamp` (zero-padded ISO, so
|
|
251
|
+
// lexicographically sortable), then by the shortId-tailed `stamp` to
|
|
252
|
+
// break same-millisecond ties consistently. The one ordering rule shared
|
|
253
|
+
// by every snapshot listing.
|
|
254
|
+
function compareSnapshotsNewestFirst(left: SnapshotEntry, right: SnapshotEntry): number {
|
|
255
|
+
if (left.filenameStamp !== right.filenameStamp) {
|
|
256
|
+
return left.filenameStamp < right.filenameStamp ? 1 : -1;
|
|
257
|
+
}
|
|
258
|
+
return left.stamp < right.stamp ? 1 : left.stamp > right.stamp ? -1 : 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
255
261
|
async function readSnapshotEntries(dir: string): Promise<SnapshotEntry[]> {
|
|
256
262
|
// Defence in depth: refuse to read if the directory itself is a
|
|
257
263
|
// symlink (codex review iter-3 / iter-4 #917). See historyDirIsSafe.
|
|
@@ -326,12 +332,7 @@ function entryEditor(meta: Record<string, unknown>): WikiPageEditor {
|
|
|
326
332
|
export async function listSnapshots(slug: string, opts: SnapshotPathOptions = {}): Promise<SnapshotSummary[]> {
|
|
327
333
|
const dir = historyDir(slug, opts);
|
|
328
334
|
const entries = await readSnapshotEntries(dir);
|
|
329
|
-
entries.sort(
|
|
330
|
-
if (left.filenameStamp !== right.filenameStamp) {
|
|
331
|
-
return left.filenameStamp < right.filenameStamp ? 1 : -1;
|
|
332
|
-
}
|
|
333
|
-
return left.stamp < right.stamp ? 1 : left.stamp > right.stamp ? -1 : 0;
|
|
334
|
-
});
|
|
335
|
+
entries.sort(compareSnapshotsNewestFirst);
|
|
335
336
|
|
|
336
337
|
const summaries: SnapshotSummary[] = [];
|
|
337
338
|
for (const entry of entries) {
|
|
@@ -36,10 +36,9 @@ export interface UseFileDropZoneOptions {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Module-scope so the install-once contract holds across multiple
|
|
39
|
-
// `useFileDropZone` consumers. The handler reference is retained
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// across test runs — Sourcery review on PR #1331).
|
|
39
|
+
// `useFileDropZone` consumers. The handler reference is retained so the
|
|
40
|
+
// listeners it installs can be removed again (a flag-only guard would leak
|
|
41
|
+
// handlers — Sourcery review on PR #1331).
|
|
43
42
|
let windowGuardHandler: ((event: DragEvent) => void) | null = null;
|
|
44
43
|
|
|
45
44
|
function isFileDrag(event: DragEvent): boolean {
|
|
@@ -132,14 +131,3 @@ export function useFileDropZone(opts: UseFileDropZoneOptions): UseFileDropZoneRe
|
|
|
132
131
|
|
|
133
132
|
return { isDragging, onDragenter, onDragover, onDragleave, onDrop };
|
|
134
133
|
}
|
|
135
|
-
|
|
136
|
-
/** Test-only reset. Removes the window listeners installed by
|
|
137
|
-
* `installWindowDefaultGuard` so a test can verify the install-once
|
|
138
|
-
* contract without leaking handlers across cases. */
|
|
139
|
-
export function _resetFileDropZoneForTests(): void {
|
|
140
|
-
if (windowGuardHandler !== null && typeof window !== "undefined") {
|
|
141
|
-
window.removeEventListener("dragover", windowGuardHandler);
|
|
142
|
-
window.removeEventListener("drop", windowGuardHandler);
|
|
143
|
-
}
|
|
144
|
-
windowGuardHandler = null;
|
|
145
|
-
}
|
package/src/config/apiRoutes.ts
CHANGED
|
@@ -357,6 +357,12 @@ const HOST_API_ROUTES = {
|
|
|
357
357
|
* capability on the view token; mutate kind ONLY — a view token must
|
|
358
358
|
* never be able to start LLM work (chat/agent stay bearer-guarded). */
|
|
359
359
|
viewDataAction: "/api/collections/:slug/view-data/actions/:actionId",
|
|
360
|
+
/** POST → run a structured aggregation query (body: { query }) over a
|
|
361
|
+
* dataSource collection's WHOLE data file → { collection, count, rows }.
|
|
362
|
+
* Requires only the `read` capability on the view token — the DSL is
|
|
363
|
+
* read-only by construction (see core/queryZ.ts); 400 on a file-backed
|
|
364
|
+
* collection (no query engine). Backs chart-drawing custom views. */
|
|
365
|
+
viewDataQuery: "/api/collections/:slug/view-data/query",
|
|
360
366
|
/** DELETE → remove one custom view: drop it from schema.json `views[]` and
|
|
361
367
|
* unlink its `views/<file>.html` (global-bearer auth) → { deleted, viewId }.
|
|
362
368
|
* Source-aware; refuses user-scope + preset collections. */
|
package/src/config/roles.ts
CHANGED
|
@@ -452,7 +452,3 @@ export const BUILTIN_ROLE_IDS = {
|
|
|
452
452
|
export type BuiltInRoleId = (typeof BUILTIN_ROLE_IDS)[keyof typeof BUILTIN_ROLE_IDS];
|
|
453
453
|
|
|
454
454
|
export const DEFAULT_ROLE_ID: BuiltInRoleId = BUILTIN_ROLE_IDS.general;
|
|
455
|
-
|
|
456
|
-
export function getRole(roleId: string): Role {
|
|
457
|
-
return ROLES.find((role) => role.id === roleId) ?? ROLES[0];
|
|
458
|
-
}
|
package/src/lang/de.ts
CHANGED
|
@@ -969,36 +969,6 @@ const deMessages = {
|
|
|
969
969
|
pluginUiImage: {
|
|
970
970
|
promptLabel: "{label}:",
|
|
971
971
|
},
|
|
972
|
-
pluginMulmoScript: {
|
|
973
|
-
beatCount: "{count} Beat | {count} Beats",
|
|
974
|
-
movie: "Video",
|
|
975
|
-
generating: "Wird generiert…",
|
|
976
|
-
rendering: "Wird gerendert…",
|
|
977
|
-
saving: "Wird gespeichert…",
|
|
978
|
-
update: "Aktualisieren",
|
|
979
|
-
characters: "Charaktere",
|
|
980
|
-
drop: "Ablegen",
|
|
981
|
-
gen: "Generieren",
|
|
982
|
-
play: "▶ Abspielen",
|
|
983
|
-
stop: "■ Stoppen",
|
|
984
|
-
playPresentation: "Präsentation abspielen",
|
|
985
|
-
regenerateMovie: "Video neu generieren",
|
|
986
|
-
movieGenerationFailed: "Videoerstellung fehlgeschlagen",
|
|
987
|
-
pdf: "PDF",
|
|
988
|
-
regeneratePdf: "PDF neu generieren",
|
|
989
|
-
generatingPdf: "PDF wird erstellt…",
|
|
990
|
-
retry: "Erneut versuchen",
|
|
991
|
-
errPrefix: "⚠ Fehler",
|
|
992
|
-
noBeats: "Keine Beats im Skript gefunden",
|
|
993
|
-
editSource: "Skript-Quelle bearbeiten",
|
|
994
|
-
applyChanges: "Änderungen übernehmen",
|
|
995
|
-
generateAll: "Alle generieren",
|
|
996
|
-
orDropImage: "oder Bild ablegen",
|
|
997
|
-
generate: "Generieren",
|
|
998
|
-
generateAudio: "♪ Generieren",
|
|
999
|
-
saveErrorInvalidJson: "⚠ Ungültiges JSON: {error}",
|
|
1000
|
-
saveErrorSaveFailed: "⚠ Speichern fehlgeschlagen: {error}",
|
|
1001
|
-
},
|
|
1002
972
|
pluginMarkdown: {
|
|
1003
973
|
loading: "Dokument wird geladen...",
|
|
1004
974
|
loadFailed: "⚠ Laden des Dokuments fehlgeschlagen: {error}",
|
package/src/lang/en.ts
CHANGED
|
@@ -980,36 +980,6 @@ const enMessages = {
|
|
|
980
980
|
pluginUiImage: {
|
|
981
981
|
promptLabel: "{label}:",
|
|
982
982
|
},
|
|
983
|
-
pluginMulmoScript: {
|
|
984
|
-
beatCount: "{count} beat | {count} beats",
|
|
985
|
-
movie: "Movie",
|
|
986
|
-
generating: "Generating…",
|
|
987
|
-
rendering: "Rendering…",
|
|
988
|
-
saving: "Saving…",
|
|
989
|
-
update: "Update",
|
|
990
|
-
characters: "Characters",
|
|
991
|
-
drop: "Drop",
|
|
992
|
-
gen: "Gen",
|
|
993
|
-
play: "▶ Play",
|
|
994
|
-
stop: "■ Stop",
|
|
995
|
-
playPresentation: "Play presentation",
|
|
996
|
-
regenerateMovie: "Regenerate movie",
|
|
997
|
-
movieGenerationFailed: "Movie generation failed",
|
|
998
|
-
pdf: "PDF",
|
|
999
|
-
regeneratePdf: "Regenerate PDF",
|
|
1000
|
-
generatingPdf: "Generating PDF…",
|
|
1001
|
-
retry: "Retry",
|
|
1002
|
-
errPrefix: "⚠ Error",
|
|
1003
|
-
noBeats: "No beats found in script",
|
|
1004
|
-
editSource: "Edit Script Source",
|
|
1005
|
-
applyChanges: "Apply Changes",
|
|
1006
|
-
generateAll: "Generate All",
|
|
1007
|
-
orDropImage: "or drop image",
|
|
1008
|
-
generate: "Generate",
|
|
1009
|
-
generateAudio: "♪ Generate",
|
|
1010
|
-
saveErrorInvalidJson: "⚠ Invalid JSON: {error}",
|
|
1011
|
-
saveErrorSaveFailed: "⚠ Save failed: {error}",
|
|
1012
|
-
},
|
|
1013
983
|
pluginMarkdown: {
|
|
1014
984
|
loading: "Loading document...",
|
|
1015
985
|
loadFailed: "⚠ Failed to load document: {error}",
|
package/src/lang/es.ts
CHANGED
|
@@ -967,36 +967,6 @@ const esMessages = {
|
|
|
967
967
|
pluginUiImage: {
|
|
968
968
|
promptLabel: "{label}:",
|
|
969
969
|
},
|
|
970
|
-
pluginMulmoScript: {
|
|
971
|
-
beatCount: "{count} beat | {count} beats",
|
|
972
|
-
movie: "Vídeo",
|
|
973
|
-
generating: "Generando…",
|
|
974
|
-
rendering: "Renderizando…",
|
|
975
|
-
saving: "Guardando…",
|
|
976
|
-
update: "Actualizar",
|
|
977
|
-
characters: "Personajes",
|
|
978
|
-
drop: "Soltar",
|
|
979
|
-
gen: "Generar",
|
|
980
|
-
play: "▶ Reproducir",
|
|
981
|
-
stop: "■ Detener",
|
|
982
|
-
playPresentation: "Reproducir presentación",
|
|
983
|
-
regenerateMovie: "Regenerar vídeo",
|
|
984
|
-
movieGenerationFailed: "Error al generar el vídeo",
|
|
985
|
-
pdf: "PDF",
|
|
986
|
-
regeneratePdf: "Regenerar PDF",
|
|
987
|
-
generatingPdf: "Generando PDF…",
|
|
988
|
-
retry: "Reintentar",
|
|
989
|
-
errPrefix: "⚠ Error",
|
|
990
|
-
noBeats: "No se encontraron beats en el script",
|
|
991
|
-
editSource: "Editar fuente del script",
|
|
992
|
-
applyChanges: "Aplicar cambios",
|
|
993
|
-
generateAll: "Generar todo",
|
|
994
|
-
orDropImage: "o arrastra una imagen",
|
|
995
|
-
generate: "Generar",
|
|
996
|
-
generateAudio: "♪ Generar",
|
|
997
|
-
saveErrorInvalidJson: "⚠ JSON no válido: {error}",
|
|
998
|
-
saveErrorSaveFailed: "⚠ Error al guardar: {error}",
|
|
999
|
-
},
|
|
1000
970
|
pluginMarkdown: {
|
|
1001
971
|
loading: "Cargando documento...",
|
|
1002
972
|
loadFailed: "⚠ Error al cargar el documento: {error}",
|
package/src/lang/fr.ts
CHANGED
|
@@ -958,36 +958,6 @@ const frMessages = {
|
|
|
958
958
|
pluginUiImage: {
|
|
959
959
|
promptLabel: "{label} :",
|
|
960
960
|
},
|
|
961
|
-
pluginMulmoScript: {
|
|
962
|
-
beatCount: "{count} beat | {count} beats",
|
|
963
|
-
movie: "Film",
|
|
964
|
-
generating: "Génération…",
|
|
965
|
-
rendering: "Rendu…",
|
|
966
|
-
saving: "Enregistrement…",
|
|
967
|
-
update: "Mettre à jour",
|
|
968
|
-
characters: "Personnages",
|
|
969
|
-
drop: "Déposer",
|
|
970
|
-
gen: "Générer",
|
|
971
|
-
play: "▶ Lire",
|
|
972
|
-
stop: "■ Arrêter",
|
|
973
|
-
playPresentation: "Lire la présentation",
|
|
974
|
-
regenerateMovie: "Régénérer la vidéo",
|
|
975
|
-
movieGenerationFailed: "Échec de la génération de la vidéo",
|
|
976
|
-
pdf: "PDF",
|
|
977
|
-
regeneratePdf: "Régénérer le PDF",
|
|
978
|
-
generatingPdf: "Génération du PDF…",
|
|
979
|
-
retry: "Réessayer",
|
|
980
|
-
errPrefix: "⚠ Erreur",
|
|
981
|
-
noBeats: "Aucun beat trouvé dans le script",
|
|
982
|
-
editSource: "Modifier la source du script",
|
|
983
|
-
applyChanges: "Appliquer les modifications",
|
|
984
|
-
generateAll: "Tout générer",
|
|
985
|
-
orDropImage: "ou déposez une image",
|
|
986
|
-
generate: "Générer",
|
|
987
|
-
generateAudio: "♪ Générer",
|
|
988
|
-
saveErrorInvalidJson: "⚠ JSON invalide : {error}",
|
|
989
|
-
saveErrorSaveFailed: "⚠ Échec de la sauvegarde : {error}",
|
|
990
|
-
},
|
|
991
961
|
pluginMarkdown: {
|
|
992
962
|
loading: "Chargement du document...",
|
|
993
963
|
loadFailed: "⚠ Échec du chargement du document : {error}",
|
package/src/lang/ja.ts
CHANGED
|
@@ -954,36 +954,6 @@ const jaMessages = {
|
|
|
954
954
|
pluginUiImage: {
|
|
955
955
|
promptLabel: "{label}:",
|
|
956
956
|
},
|
|
957
|
-
pluginMulmoScript: {
|
|
958
|
-
beatCount: "{count} ビート",
|
|
959
|
-
movie: "動画",
|
|
960
|
-
generating: "生成中…",
|
|
961
|
-
rendering: "レンダリング中…",
|
|
962
|
-
saving: "保存中…",
|
|
963
|
-
update: "更新",
|
|
964
|
-
characters: "キャラクター",
|
|
965
|
-
drop: "ドロップ",
|
|
966
|
-
gen: "生成",
|
|
967
|
-
play: "▶ 再生",
|
|
968
|
-
stop: "■ 停止",
|
|
969
|
-
playPresentation: "プレゼンテーション再生",
|
|
970
|
-
regenerateMovie: "動画を再生成",
|
|
971
|
-
movieGenerationFailed: "動画の生成に失敗しました",
|
|
972
|
-
pdf: "PDF",
|
|
973
|
-
regeneratePdf: "PDF を再生成",
|
|
974
|
-
generatingPdf: "PDF を生成中…",
|
|
975
|
-
retry: "再試行",
|
|
976
|
-
errPrefix: "⚠ エラー",
|
|
977
|
-
noBeats: "スクリプトにビートが見つかりません",
|
|
978
|
-
editSource: "スクリプトソースを編集",
|
|
979
|
-
applyChanges: "変更を適用",
|
|
980
|
-
generateAll: "すべて生成",
|
|
981
|
-
orDropImage: "画像をドロップ",
|
|
982
|
-
generate: "生成",
|
|
983
|
-
generateAudio: "♪ 生成",
|
|
984
|
-
saveErrorInvalidJson: "⚠ 不正な JSON: {error}",
|
|
985
|
-
saveErrorSaveFailed: "⚠ 保存失敗: {error}",
|
|
986
|
-
},
|
|
987
957
|
pluginMarkdown: {
|
|
988
958
|
loading: "ドキュメント読み込み中...",
|
|
989
959
|
loadFailed: "⚠ ドキュメントの読み込みに失敗: {error}",
|
package/src/lang/ko.ts
CHANGED
|
@@ -952,36 +952,6 @@ const koMessages = {
|
|
|
952
952
|
pluginUiImage: {
|
|
953
953
|
promptLabel: "{label}:",
|
|
954
954
|
},
|
|
955
|
-
pluginMulmoScript: {
|
|
956
|
-
beatCount: "{count}개 비트",
|
|
957
|
-
movie: "영상",
|
|
958
|
-
generating: "생성 중…",
|
|
959
|
-
rendering: "렌더링 중…",
|
|
960
|
-
saving: "저장 중…",
|
|
961
|
-
update: "업데이트",
|
|
962
|
-
characters: "캐릭터",
|
|
963
|
-
drop: "드롭",
|
|
964
|
-
gen: "생성",
|
|
965
|
-
play: "▶ 재생",
|
|
966
|
-
stop: "■ 정지",
|
|
967
|
-
playPresentation: "프레젠테이션 재생",
|
|
968
|
-
regenerateMovie: "동영상 재생성",
|
|
969
|
-
movieGenerationFailed: "동영상 생성에 실패했습니다",
|
|
970
|
-
pdf: "PDF",
|
|
971
|
-
regeneratePdf: "PDF 재생성",
|
|
972
|
-
generatingPdf: "PDF 생성 중…",
|
|
973
|
-
retry: "다시 시도",
|
|
974
|
-
errPrefix: "⚠ 오류",
|
|
975
|
-
noBeats: "스크립트에서 비트를 찾을 수 없습니다",
|
|
976
|
-
editSource: "스크립트 원본 편집",
|
|
977
|
-
applyChanges: "변경 사항 적용",
|
|
978
|
-
generateAll: "전체 생성",
|
|
979
|
-
orDropImage: "또는 이미지 드롭",
|
|
980
|
-
generate: "생성",
|
|
981
|
-
generateAudio: "♪ 생성",
|
|
982
|
-
saveErrorInvalidJson: "⚠ 잘못된 JSON: {error}",
|
|
983
|
-
saveErrorSaveFailed: "⚠ 저장 실패: {error}",
|
|
984
|
-
},
|
|
985
955
|
pluginMarkdown: {
|
|
986
956
|
loading: "문서를 불러오는 중...",
|
|
987
957
|
loadFailed: "⚠ 문서 불러오기 실패: {error}",
|
package/src/lang/pt-BR.ts
CHANGED
|
@@ -955,36 +955,6 @@ const ptBRMessages = {
|
|
|
955
955
|
pluginUiImage: {
|
|
956
956
|
promptLabel: "{label}:",
|
|
957
957
|
},
|
|
958
|
-
pluginMulmoScript: {
|
|
959
|
-
beatCount: "{count} beat | {count} beats",
|
|
960
|
-
movie: "Vídeo",
|
|
961
|
-
generating: "Gerando…",
|
|
962
|
-
rendering: "Renderizando…",
|
|
963
|
-
saving: "Salvando…",
|
|
964
|
-
update: "Atualizar",
|
|
965
|
-
characters: "Personagens",
|
|
966
|
-
drop: "Soltar",
|
|
967
|
-
gen: "Gerar",
|
|
968
|
-
play: "▶ Reproduzir",
|
|
969
|
-
stop: "■ Parar",
|
|
970
|
-
playPresentation: "Reproduzir apresentação",
|
|
971
|
-
regenerateMovie: "Regenerar vídeo",
|
|
972
|
-
movieGenerationFailed: "Falha ao gerar o vídeo",
|
|
973
|
-
pdf: "PDF",
|
|
974
|
-
regeneratePdf: "Regenerar PDF",
|
|
975
|
-
generatingPdf: "Gerando PDF…",
|
|
976
|
-
retry: "Tentar novamente",
|
|
977
|
-
errPrefix: "⚠ Erro",
|
|
978
|
-
noBeats: "Nenhum beat encontrado no script",
|
|
979
|
-
editSource: "Editar fonte do script",
|
|
980
|
-
applyChanges: "Aplicar alterações",
|
|
981
|
-
generateAll: "Gerar tudo",
|
|
982
|
-
orDropImage: "ou solte uma imagem",
|
|
983
|
-
generate: "Gerar",
|
|
984
|
-
generateAudio: "♪ Gerar",
|
|
985
|
-
saveErrorInvalidJson: "⚠ JSON inválido: {error}",
|
|
986
|
-
saveErrorSaveFailed: "⚠ Falha ao salvar: {error}",
|
|
987
|
-
},
|
|
988
958
|
pluginMarkdown: {
|
|
989
959
|
loading: "Carregando documento...",
|
|
990
960
|
loadFailed: "⚠ Falha ao carregar o documento: {error}",
|
package/src/lang/zh.ts
CHANGED
|
@@ -939,36 +939,6 @@ const zhMessages = {
|
|
|
939
939
|
pluginUiImage: {
|
|
940
940
|
promptLabel: "{label}:",
|
|
941
941
|
},
|
|
942
|
-
pluginMulmoScript: {
|
|
943
|
-
beatCount: "{count} 个 beat",
|
|
944
|
-
movie: "视频",
|
|
945
|
-
generating: "生成中…",
|
|
946
|
-
rendering: "渲染中…",
|
|
947
|
-
saving: "保存中…",
|
|
948
|
-
update: "更新",
|
|
949
|
-
characters: "角色",
|
|
950
|
-
drop: "拖放",
|
|
951
|
-
gen: "生成",
|
|
952
|
-
play: "▶ 播放",
|
|
953
|
-
stop: "■ 停止",
|
|
954
|
-
playPresentation: "播放演示",
|
|
955
|
-
regenerateMovie: "重新生成视频",
|
|
956
|
-
movieGenerationFailed: "视频生成失败",
|
|
957
|
-
pdf: "PDF",
|
|
958
|
-
regeneratePdf: "重新生成 PDF",
|
|
959
|
-
generatingPdf: "生成 PDF…",
|
|
960
|
-
retry: "重试",
|
|
961
|
-
errPrefix: "⚠ 错误",
|
|
962
|
-
noBeats: "脚本中没有找到 beat",
|
|
963
|
-
editSource: "编辑脚本源",
|
|
964
|
-
applyChanges: "应用更改",
|
|
965
|
-
generateAll: "全部生成",
|
|
966
|
-
orDropImage: "或拖入图片",
|
|
967
|
-
generate: "生成",
|
|
968
|
-
generateAudio: "♪ 生成",
|
|
969
|
-
saveErrorInvalidJson: "⚠ JSON 无效: {error}",
|
|
970
|
-
saveErrorSaveFailed: "⚠ 保存失败: {error}",
|
|
971
|
-
},
|
|
972
942
|
pluginMarkdown: {
|
|
973
943
|
loading: "正在加载文档...",
|
|
974
944
|
loadFailed: "⚠ 加载文档失败: {error}",
|
|
@@ -44,8 +44,6 @@ const accountingPlugin: ToolPlugin<AccountingActionData> = {
|
|
|
44
44
|
previewComponent: wrapWithScope("accounting", AccountingPreview),
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
export default accountingPlugin;
|
|
48
|
-
|
|
49
47
|
export const REGISTRATION: PluginRegistration = {
|
|
50
48
|
toolName: META.toolName,
|
|
51
49
|
entry: accountingPlugin,
|
package/src/plugins/api.ts
CHANGED
|
@@ -117,9 +117,3 @@ export function pluginPageRoute(name: string): string {
|
|
|
117
117
|
export function pluginAllPluginNames(): readonly string[] {
|
|
118
118
|
return requireContext().getAllPluginNames();
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
/** Test-only: drop the installed context so the next `installHostContext`
|
|
122
|
-
* starts from a clean slate. Production code should not call this. */
|
|
123
|
-
export function resetHostContextForTest(): void {
|
|
124
|
-
installedContext = null;
|
|
125
|
-
}
|
|
@@ -34,8 +34,6 @@ const canvasPlugin: ToolPlugin<ImageToolData> = {
|
|
|
34
34
|
viewComponent: wrapWithScope("canvas", View),
|
|
35
35
|
previewComponent: wrapWithScope("canvas", Preview),
|
|
36
36
|
};
|
|
37
|
-
|
|
38
|
-
export default canvasPlugin;
|
|
39
37
|
export { TOOL_NAME };
|
|
40
38
|
|
|
41
39
|
export const REGISTRATION: PluginRegistration = {
|