mulmoclaude 2.7.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/assets/Graph-ChAxkn9o.js +1 -0
- package/client/assets/dist-Bij44LdC.js +2 -0
- package/client/assets/echarts-GV3U9sVf.js +1 -0
- package/client/assets/echarts-gl-BXx4On0Y.js +4282 -0
- package/client/assets/echarts-wordcloud-_9uQUAfp.js +1 -0
- package/client/assets/emscripten-module-Q67P5WYC-BU3Hlttz.js +2160 -0
- package/client/assets/ffi-4V2b7xu3.js +1 -0
- package/client/assets/{index-lkpgbKtk.js → index-D6xzCZhP.js} +600 -589
- package/client/assets/index-DzMsXDnj.css +2 -0
- package/client/assets/{install-wxcddUsO.js → install-C1moNcrn.js} +1 -1
- package/client/assets/{marp-DcTywifN.js → marp-Du5Sym5b.js} +1 -1
- package/client/assets/module-ES6BEMUI-CDTIpMiA.js +6 -0
- package/client/index.html +4 -3
- package/package.json +5 -4
- package/server/api/routes/plugins.ts +8 -59
- package/server/build/mcp-server.mjs +609 -608
- package/server/index.ts +1 -0
- package/server/plugins/confined-file-ops.ts +136 -0
- package/server/plugins/runtime.ts +8 -115
- package/server/plugins/spreadsheet-builtin.ts +26 -0
- package/server/plugins/spreadsheet-save-outcome.ts +7 -0
- package/server/utils/files/store-resolvers.ts +1 -1
- package/src/lang/de.ts +0 -20
- package/src/lang/en.ts +0 -20
- package/src/lang/es.ts +0 -20
- package/src/lang/fr.ts +0 -20
- package/src/lang/ja.ts +0 -20
- package/src/lang/ko.ts +0 -20
- package/src/lang/pt-BR.ts +0 -20
- package/src/lang/zh.ts +0 -20
- package/src/plugins/spreadsheet/definition.ts +9 -127
- package/src/plugins/spreadsheet/index.ts +7 -4
- package/src/plugins/spreadsheet/meta.ts +2 -4
- package/client/assets/echarts-gl-Cq76Z6DA.js +0 -4282
- package/client/assets/index-Cz7JLE0k.css +0 -2
- package/server/utils/files/spreadsheet-store.ts +0 -26
- package/src/plugins/spreadsheet/Preview.vue +0 -31
- package/src/plugins/spreadsheet/View.vue +0 -1007
- package/src/plugins/spreadsheet/cellHighlights.ts +0 -78
- package/src/plugins/spreadsheet/engine/calculator.ts +0 -427
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +0 -89
- package/src/plugins/spreadsheet/engine/cellEmpty.ts +0 -26
- package/src/plugins/spreadsheet/engine/cellFormatting.ts +0 -59
- package/src/plugins/spreadsheet/engine/coerce-boolean.ts +0 -26
- package/src/plugins/spreadsheet/engine/condition.ts +0 -182
- package/src/plugins/spreadsheet/engine/date-locale.ts +0 -47
- package/src/plugins/spreadsheet/engine/date-parser.ts +0 -234
- package/src/plugins/spreadsheet/engine/date-utils.ts +0 -73
- package/src/plugins/spreadsheet/engine/datedif.ts +0 -93
- package/src/plugins/spreadsheet/engine/engine.ts +0 -177
- package/src/plugins/spreadsheet/engine/evaluator.ts +0 -494
- package/src/plugins/spreadsheet/engine/financial-math.ts +0 -114
- package/src/plugins/spreadsheet/engine/formatter.ts +0 -158
- package/src/plugins/spreadsheet/engine/formulaError.ts +0 -67
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +0 -202
- package/src/plugins/spreadsheet/engine/functions/date.ts +0 -212
- package/src/plugins/spreadsheet/engine/functions/financial.ts +0 -244
- package/src/plugins/spreadsheet/engine/functions/index.ts +0 -16
- package/src/plugins/spreadsheet/engine/functions/logical.ts +0 -221
- package/src/plugins/spreadsheet/engine/functions/lookup-math.ts +0 -28
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +0 -257
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +0 -283
- package/src/plugins/spreadsheet/engine/functions/statistical-math.ts +0 -91
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +0 -290
- package/src/plugins/spreadsheet/engine/functions/text.ts +0 -398
- package/src/plugins/spreadsheet/engine/index.ts +0 -34
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +0 -111
- package/src/plugins/spreadsheet/engine/math-ops.ts +0 -98
- package/src/plugins/spreadsheet/engine/numericCoercion.ts +0 -61
- package/src/plugins/spreadsheet/engine/parser.ts +0 -40
- package/src/plugins/spreadsheet/engine/registry.ts +0 -201
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +0 -68
- package/src/plugins/spreadsheet/engine/spreadsheet-errors.ts +0 -82
- package/src/plugins/spreadsheet/engine/textFormat.ts +0 -108
- package/src/plugins/spreadsheet/engine/translateFormula.ts +0 -69
- package/src/plugins/spreadsheet/engine/types.ts +0 -81
- package/src/plugins/spreadsheet/keyboardNav.ts +0 -38
package/server/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ import "./plugins/markdown-builtin.js";
|
|
|
55
55
|
// Side-effect: registers the built-in "html" dispatch handler so the
|
|
56
56
|
// presentHtml View's useRuntime().dispatch({ kind }) resolves (phase 2).
|
|
57
57
|
import "./plugins/html-builtin.js";
|
|
58
|
+
import "./plugins/spreadsheet-builtin.js";
|
|
58
59
|
// Side-effect: registers the built-in "shapescript" dispatch handler so the
|
|
59
60
|
// presentShapeScript View's useRuntime().dispatch({ kind }) resolves — the
|
|
60
61
|
// source editor's load / save against artifacts/shapes/.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Scoped FileOps: a lexical check on the plugin-supplied path, then a realpath check so a
|
|
2
|
+
// symlink inside the scope root cannot lead out of it. Check-then-use, like `resolveWithinRoot`: not atomic
|
|
3
|
+
// against a path swapped between the check and the call (Node fs has no openat / no-follow-parent).
|
|
4
|
+
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { mkdir, readFile, readdir, realpath, stat as fsStat, unlink as fsUnlink } from "node:fs/promises";
|
|
7
|
+
import type { FileOps } from "gui-chat-protocol";
|
|
8
|
+
import { writeFileAtomic } from "../utils/files/atomic.js";
|
|
9
|
+
import { isEnoent, resolveWriteWithinRoot } from "../utils/files/safe.js";
|
|
10
|
+
import { ensureInsideBase } from "./runtime-loader.js";
|
|
11
|
+
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Path normalisation contract (see plans/done/feat-plugin-runtime-extensions-1110.md)
|
|
14
|
+
//
|
|
15
|
+
// 1. Replace `\` with `/` (Windows path.join leak repair).
|
|
16
|
+
// 2. `path.posix.normalize` (folds `..`, `.`, `//`).
|
|
17
|
+
// 3. Reject if the normalised form starts with `..` or `/`
|
|
18
|
+
// (would escape the scope root or be absolute).
|
|
19
|
+
// 4. `path.join(scopeRoot, ...segments)` — platform-aware join so
|
|
20
|
+
// the returned absolute path uses the host OS separator
|
|
21
|
+
// (backslashes on Windows, forward slashes on POSIX). Critical
|
|
22
|
+
// for `ensureInsideBase` to accept the result.
|
|
23
|
+
//
|
|
24
|
+
// Plugin authors should never need `node:path`; the platform meets
|
|
25
|
+
// them where they are even if they mis-import it on Windows.
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** Resolve a plugin-supplied path to an absolute path inside the
|
|
29
|
+
* plugin's scope root. Throws on traversal. Lexical only — symlinks
|
|
30
|
+
* are checked by the FileOps below. Exported for tests. */
|
|
31
|
+
export function normalizePluginPath(scopeRoot: string, rel: string): string {
|
|
32
|
+
const slashed = rel.replace(/\\/g, "/");
|
|
33
|
+
const normalised = path.posix.normalize(slashed);
|
|
34
|
+
// `path.posix.normalize` produces `..` alone if the input is `..` itself, hence the equality branch.
|
|
35
|
+
if (normalised === ".." || normalised.startsWith("../") || normalised.startsWith("/")) {
|
|
36
|
+
throw new Error(`path escapes plugin scope: ${rel}`);
|
|
37
|
+
}
|
|
38
|
+
const segments = normalised === "." ? [] : normalised.split("/");
|
|
39
|
+
const absolute = path.join(scopeRoot, ...segments);
|
|
40
|
+
// Defence in depth so a future change to the lexical step cannot silently regress.
|
|
41
|
+
if (!ensureInsideBase(absolute, scopeRoot)) {
|
|
42
|
+
throw new Error(`path escapes plugin scope: ${rel}`);
|
|
43
|
+
}
|
|
44
|
+
return absolute;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The lexically checked path, relative to the scope root in host-native form ("" for the root). */
|
|
48
|
+
const scopeRelative = (scopeRoot: string, rel: string): string => path.relative(scopeRoot, normalizePluginPath(scopeRoot, rel));
|
|
49
|
+
|
|
50
|
+
/** An escape reads as a missing file, so callers that treat ENOENT as "not there" stay closed. */
|
|
51
|
+
const notInScope = (rel: string): Error & { code: string } => Object.assign(new Error(`not inside the scope: ${rel}`), { code: "ENOENT" });
|
|
52
|
+
|
|
53
|
+
/** Only a missing path reads as null; EACCES, ELOOP, ENOTDIR … propagate as before. */
|
|
54
|
+
const realpathIfPresent = async (absPath: string): Promise<string | null> => {
|
|
55
|
+
try {
|
|
56
|
+
return await realpath(absPath);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (isEnoent(err)) return null;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isWithin = (rootReal: string, candidateReal: string): boolean => candidateReal === rootReal || candidateReal.startsWith(rootReal + path.sep);
|
|
64
|
+
|
|
65
|
+
/** Real location of `relative` (host-native, from `scopeRelative`) if it exists inside the root; null when missing or outside. */
|
|
66
|
+
const resolveExisting = async (rootReal: string, relative: string): Promise<string | null> => {
|
|
67
|
+
const candidateReal = await realpathIfPresent(path.resolve(rootReal, relative));
|
|
68
|
+
return candidateReal !== null && isWithin(rootReal, candidateReal) ? candidateReal : null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Only a write creates the scope root; a read-only call against an absent root sees "missing".
|
|
72
|
+
const realRoot = async (scopeRoot: string): Promise<string> => {
|
|
73
|
+
await mkdir(scopeRoot, { recursive: true });
|
|
74
|
+
return realpath(scopeRoot);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const existingPath = async (scopeRoot: string, rel: string): Promise<string | null> => {
|
|
78
|
+
const relative = scopeRelative(scopeRoot, rel);
|
|
79
|
+
const rootReal = await realpathIfPresent(scopeRoot);
|
|
80
|
+
return rootReal === null ? null : resolveExisting(rootReal, relative);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const requireExisting = async (scopeRoot: string, rel: string): Promise<string> => {
|
|
84
|
+
const resolved = await existingPath(scopeRoot, rel);
|
|
85
|
+
if (resolved === null) throw notInScope(rel);
|
|
86
|
+
return resolved;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const writeTarget = async (scopeRoot: string, rel: string): Promise<string> => {
|
|
90
|
+
const target = await resolveWriteWithinRoot(await realRoot(scopeRoot), scopeRelative(scopeRoot, rel));
|
|
91
|
+
if (target === null) throw new Error(`refusing to write outside the scope: ${rel}`);
|
|
92
|
+
return target;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// unlink does not follow the leaf, so only the parent is confined: a link is removed, never its target.
|
|
96
|
+
const unlinkTarget = async (scopeRoot: string, rel: string): Promise<string | null> => {
|
|
97
|
+
const relative = scopeRelative(scopeRoot, rel);
|
|
98
|
+
const rootReal = await realpathIfPresent(scopeRoot);
|
|
99
|
+
if (rootReal === null) return null;
|
|
100
|
+
if (relative === "") return rootReal;
|
|
101
|
+
const parentReal = await resolveExisting(rootReal, path.dirname(relative));
|
|
102
|
+
return parentReal === null ? null : path.join(parentReal, path.basename(relative));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const ignoreEnoent = async <T>(operation: () => Promise<T>, fallback: T): Promise<T> => {
|
|
106
|
+
try {
|
|
107
|
+
return await operation();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (isEnoent(err)) return fallback;
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export function makeConfinedFileOps(scopeRoot: string): FileOps {
|
|
115
|
+
return {
|
|
116
|
+
read: async (rel) => readFile(await requireExisting(scopeRoot, rel), "utf-8"),
|
|
117
|
+
readBytes: async (rel) => {
|
|
118
|
+
const buf = await readFile(await requireExisting(scopeRoot, rel));
|
|
119
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
120
|
+
},
|
|
121
|
+
write: async (rel, content) => writeFileAtomic(await writeTarget(scopeRoot, rel), content),
|
|
122
|
+
readDir: async (rel) => {
|
|
123
|
+
const resolved = await existingPath(scopeRoot, rel);
|
|
124
|
+
return resolved === null ? [] : ignoreEnoent(() => readdir(resolved), []);
|
|
125
|
+
},
|
|
126
|
+
stat: async (rel) => {
|
|
127
|
+
const { mtimeMs, size } = await fsStat(await requireExisting(scopeRoot, rel));
|
|
128
|
+
return { mtimeMs, size };
|
|
129
|
+
},
|
|
130
|
+
exists: async (rel) => (await existingPath(scopeRoot, rel)) !== null,
|
|
131
|
+
unlink: async (rel) => {
|
|
132
|
+
const target = await unlinkTarget(scopeRoot, rel);
|
|
133
|
+
if (target !== null) await ignoreEnoent(() => fsUnlink(target), undefined);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -3,25 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Every helper closes over `pkgName` so a plugin's pubsub channel,
|
|
5
5
|
// data dir, log prefix etc. cannot leak across plugins. Path arguments
|
|
6
|
-
// to `files.{data,config}` are
|
|
7
|
-
//
|
|
8
|
-
// `
|
|
9
|
-
// rejected.
|
|
6
|
+
// to `files.{data,config,artifacts}` are confined to their scope root
|
|
7
|
+
// by `makeConfinedFileOps` — lexically, then by realpath, so neither
|
|
8
|
+
// `"../../etc/passwd"` nor a symlink inside the root leads out.
|
|
10
9
|
//
|
|
11
10
|
// This module is server-side only. The browser-side counterpart lives
|
|
12
11
|
// at `src/utils/plugin/runtime.ts`.
|
|
13
12
|
|
|
14
13
|
import path from "node:path";
|
|
15
|
-
import { readFile, readdir, stat as fsStat, unlink as fsUnlink } from "node:fs/promises";
|
|
16
14
|
import { randomUUID } from "node:crypto";
|
|
17
15
|
import type { FileOps, PluginFetchJsonOptions, PluginFetchOptions, PluginRuntime } from "gui-chat-protocol";
|
|
18
16
|
|
|
19
17
|
import { WORKSPACE_PATHS, workspacePath } from "../workspace/paths.js";
|
|
20
18
|
import { writeFileAtomic } from "../utils/files/atomic.js";
|
|
21
19
|
import { errorMessage } from "../utils/errors.js";
|
|
22
|
-
import {
|
|
20
|
+
import { isRecord } from "../utils/types.js";
|
|
23
21
|
import { log as hostLog, type Logger } from "../system/logger/index.js";
|
|
24
|
-
import {
|
|
22
|
+
import { makeConfinedFileOps } from "./confined-file-ops.js";
|
|
25
23
|
import { ONE_SECOND_MS } from "../utils/time.js";
|
|
26
24
|
import type { IPubSub } from "../events/pub-sub/index.js";
|
|
27
25
|
import * as notifierEngine from "../notifier/engine.js";
|
|
@@ -35,59 +33,6 @@ import { BUILTIN_ROLE_IDS } from "../../src/config/roles.js";
|
|
|
35
33
|
|
|
36
34
|
const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;
|
|
37
35
|
|
|
38
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
39
|
-
// Path normalisation contract (see plans/done/feat-plugin-runtime-extensions-1110.md)
|
|
40
|
-
//
|
|
41
|
-
// 1. Replace `\` with `/` (Windows path.join leak repair).
|
|
42
|
-
// 2. `path.posix.normalize` (folds `..`, `.`, `//`).
|
|
43
|
-
// 3. Reject if the normalised form starts with `..` or `/`
|
|
44
|
-
// (would escape the scope root or be absolute).
|
|
45
|
-
// 4. `path.join(scopeRoot, ...segments)` — platform-aware join so
|
|
46
|
-
// the returned absolute path uses the host OS separator
|
|
47
|
-
// (backslashes on Windows, forward slashes on POSIX). Critical
|
|
48
|
-
// for `ensureInsideBase` to accept the result.
|
|
49
|
-
//
|
|
50
|
-
// Plugin authors should never need `node:path`; the platform meets
|
|
51
|
-
// them where they are even if they mis-import it on Windows.
|
|
52
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
/** Resolve a plugin-supplied path to an absolute path inside the
|
|
55
|
-
* plugin's scope root. Throws on traversal. Exported for tests.
|
|
56
|
-
*
|
|
57
|
-
* Cross-platform: `scopeRoot` is the host OS's native form (drive
|
|
58
|
-
* letter + backslashes on Windows, forward slashes on POSIX). The
|
|
59
|
-
* user-supplied `rel` is POSIX-relative by contract; we sanitise
|
|
60
|
-
* for the lexical traversal check via `path.posix.*`, then join
|
|
61
|
-
* with `path.join` so the returned absolute path is in the host's
|
|
62
|
-
* native form (which `ensureInsideBase` requires). */
|
|
63
|
-
export function normalizePluginPath(scopeRoot: string, rel: string): string {
|
|
64
|
-
// Defang Windows path.join output and any mixed-separator inputs.
|
|
65
|
-
const slashed = rel.replace(/\\/g, "/");
|
|
66
|
-
// Fold `..` / `.` / repeated `/` lexically (no fs touch yet).
|
|
67
|
-
const normalised = path.posix.normalize(slashed);
|
|
68
|
-
// Reject lexically — anything that escapes the scope root after
|
|
69
|
-
// normalisation either starts with `..` (parent-traversal) or is
|
|
70
|
-
// absolute (`/etc/passwd`). `path.posix.normalize` produces `..`
|
|
71
|
-
// alone if the input is `..` itself, hence the equality branch.
|
|
72
|
-
if (normalised === ".." || normalised.startsWith("../") || normalised.startsWith("/")) {
|
|
73
|
-
throw new Error(`path escapes plugin scope: ${rel}`);
|
|
74
|
-
}
|
|
75
|
-
const segments = normalised === "." ? [] : normalised.split("/");
|
|
76
|
-
// `path.join` (platform-aware) so the result uses the host
|
|
77
|
-
// separator. On Windows that's backslashes; on POSIX, forward
|
|
78
|
-
// slashes — matching `scopeRoot`'s existing form so
|
|
79
|
-
// `ensureInsideBase`'s `path.resolve` + `path.sep` comparison
|
|
80
|
-
// works across both platforms.
|
|
81
|
-
const absolute = path.join(scopeRoot, ...segments);
|
|
82
|
-
// Defence in depth: even though the lexical check above should
|
|
83
|
-
// catch every escape, run `ensureInsideBase` so a future change to
|
|
84
|
-
// either step doesn't silently regress the safety guarantee.
|
|
85
|
-
if (!ensureInsideBase(absolute, scopeRoot)) {
|
|
86
|
-
throw new Error(`path escapes plugin scope: ${rel}`);
|
|
87
|
-
}
|
|
88
|
-
return absolute;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
36
|
// ─────────────────────────────────────────────────────────────────────
|
|
92
37
|
// Scoped FileOps factory
|
|
93
38
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -99,59 +44,7 @@ export function normalizePluginPath(scopeRoot: string, rel: string): string {
|
|
|
99
44
|
* category subdir (`charts/`, …) and all write logic; the host only
|
|
100
45
|
* provides this generic primitive. */
|
|
101
46
|
export function makeArtifactsFileOps(): FileOps {
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function makeFileOps(scopeRoot: string): FileOps {
|
|
106
|
-
// The scope root may not exist on first use. Lazy-create at write
|
|
107
|
-
// time (writeFileAtomic does its own mkdir for the file's parent;
|
|
108
|
-
// we ensure the scope root exists for readDir() / stat() etc.).
|
|
109
|
-
// Reads against a never-written plugin throw ENOENT — exposed as
|
|
110
|
-
// `exists() === false` rather than handled here.
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
async read(rel) {
|
|
114
|
-
return readFile(normalizePluginPath(scopeRoot, rel), "utf-8");
|
|
115
|
-
},
|
|
116
|
-
async readBytes(rel) {
|
|
117
|
-
const buf = await readFile(normalizePluginPath(scopeRoot, rel));
|
|
118
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
119
|
-
},
|
|
120
|
-
async write(rel, content) {
|
|
121
|
-
const abs = normalizePluginPath(scopeRoot, rel);
|
|
122
|
-
await writeFileAtomic(abs, content);
|
|
123
|
-
},
|
|
124
|
-
async readDir(rel) {
|
|
125
|
-
const abs = normalizePluginPath(scopeRoot, rel);
|
|
126
|
-
try {
|
|
127
|
-
return await readdir(abs);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
if (isErrorWithCode(err) && err.code === "ENOENT") return [];
|
|
130
|
-
throw err;
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
async stat(rel) {
|
|
134
|
-
const { mtimeMs, size } = await fsStat(normalizePluginPath(scopeRoot, rel));
|
|
135
|
-
return { mtimeMs, size };
|
|
136
|
-
},
|
|
137
|
-
async exists(rel) {
|
|
138
|
-
try {
|
|
139
|
-
await fsStat(normalizePluginPath(scopeRoot, rel));
|
|
140
|
-
return true;
|
|
141
|
-
} catch (err) {
|
|
142
|
-
if (isErrorWithCode(err) && err.code === "ENOENT") return false;
|
|
143
|
-
throw err;
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
async unlink(rel) {
|
|
147
|
-
try {
|
|
148
|
-
await fsUnlink(normalizePluginPath(scopeRoot, rel));
|
|
149
|
-
} catch (err) {
|
|
150
|
-
if (isErrorWithCode(err) && err.code === "ENOENT") return;
|
|
151
|
-
throw err;
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
};
|
|
47
|
+
return makeConfinedFileOps(path.join(workspacePath, "artifacts"));
|
|
155
48
|
}
|
|
156
49
|
|
|
157
50
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -395,8 +288,8 @@ export function makePluginRuntime(deps: MakePluginRuntimeDeps): MulmoclaudeRunti
|
|
|
395
288
|
pubsub: makeScopedPubSub(pkgName, pubsub),
|
|
396
289
|
locale,
|
|
397
290
|
files: {
|
|
398
|
-
data:
|
|
399
|
-
config:
|
|
291
|
+
data: makeConfinedFileOps(dataRoot),
|
|
292
|
+
config: makeConfinedFileOps(configRoot),
|
|
400
293
|
artifacts: makeArtifactsFileOps(),
|
|
401
294
|
},
|
|
402
295
|
log: makeScopedLogger(pkgName),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// MulmoClaude's host wiring for the presentSpreadsheet View's dispatch channel.
|
|
2
|
+
// The @gui-chat-plugin/spreadsheet View loads and saves its workbook through
|
|
3
|
+
// `useRuntime().dispatch({ kind: "loadSpreadsheet" | "saveSpreadsheet", … })`;
|
|
4
|
+
// this routes those calls to the package's `executeSpreadsheetDispatch` against
|
|
5
|
+
// the workspace `artifacts/` (confined by realpath, so a symlink cannot lead out), then publishes a file-change event
|
|
6
|
+
// after a save that wrote the file. Imported for side effect at boot (server/index.ts).
|
|
7
|
+
|
|
8
|
+
import { executeSpreadsheetDispatch, isSpreadsheetDispatchArgs } from "@gui-chat-plugin/spreadsheet";
|
|
9
|
+
import { makeArtifactsFileOps } from "./runtime.js";
|
|
10
|
+
import { publishFileChange } from "../events/file-change.js";
|
|
11
|
+
import { describeKind, registerBuiltinDispatch } from "./builtin-dispatch.js";
|
|
12
|
+
import { wroteSpreadsheetFile } from "./spreadsheet-save-outcome.js";
|
|
13
|
+
|
|
14
|
+
/** Matches `wrapWithScope("spreadsheet", …)` in `src/plugins/spreadsheet/index.ts`. */
|
|
15
|
+
const SPREADSHEET_SCOPE = "spreadsheet";
|
|
16
|
+
|
|
17
|
+
registerBuiltinDispatch(SPREADSHEET_SCOPE, async (args) => {
|
|
18
|
+
if (!isSpreadsheetDispatchArgs(args)) {
|
|
19
|
+
throw new Error(`spreadsheet plugin: unrecognised dispatch payload (kind=${describeKind(args)})`);
|
|
20
|
+
}
|
|
21
|
+
const result = await executeSpreadsheetDispatch(makeArtifactsFileOps(), args);
|
|
22
|
+
if (wroteSpreadsheetFile(args, result.data)) {
|
|
23
|
+
void publishFileChange(args.path);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Whether a spreadsheet dispatch changed the file on disk, so the host should announce it.
|
|
2
|
+
// A save refused for a conflict (the file moved on since the View's baseVersion) wrote nothing.
|
|
3
|
+
|
|
4
|
+
import type { SpreadsheetDispatchArgs, SpreadsheetFileData } from "@gui-chat-plugin/spreadsheet";
|
|
5
|
+
|
|
6
|
+
export const wroteSpreadsheetFile = (args: SpreadsheetDispatchArgs, data: SpreadsheetFileData | undefined): boolean =>
|
|
7
|
+
args.kind === "saveSpreadsheet" && data !== undefined && data.conflict !== true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Shared resolver factory for file-store modules (attachment-store,
|
|
2
|
-
// image-store
|
|
2
|
+
// image-store). Each store used to define its own
|
|
3
3
|
// `safeResolve(relativePath)` wrapper of identical shape: ensure the
|
|
4
4
|
// root dir exists, cache its realpath, strip the leading store
|
|
5
5
|
// prefix, hand off to `resolveWithinRoot`. That left three name-cloned
|
package/src/lang/de.ts
CHANGED
|
@@ -1099,26 +1099,6 @@ const deMessages = {
|
|
|
1099
1099
|
pluginSkill: {
|
|
1100
1100
|
noDescription: "(keine Beschreibung)",
|
|
1101
1101
|
},
|
|
1102
|
-
pluginSpreadsheet: {
|
|
1103
|
-
previewUntitled: "Tabelle",
|
|
1104
|
-
previewSheets: "{count} Blatt | {count} Blätter",
|
|
1105
|
-
untitled: "Tabelle",
|
|
1106
|
-
excel: "Excel",
|
|
1107
|
-
valuePlaceholder: "Wert",
|
|
1108
|
-
valueOrFormulaPlaceholder: "Wert oder Formel (z. B. 100 oder SUM(B2:B11))",
|
|
1109
|
-
formatPlaceholder: "Format (z. B. $#,##0.00)",
|
|
1110
|
-
loading: "Tabelle wird geladen...",
|
|
1111
|
-
noData: "Keine Tabellendaten verfügbar",
|
|
1112
|
-
editData: "Tabellendaten bearbeiten",
|
|
1113
|
-
applyChanges: "Änderungen übernehmen",
|
|
1114
|
-
dataMustBeArray: "Daten müssen ein Array von Blättern sein",
|
|
1115
|
-
loadFailed: "Laden der Tabelle fehlgeschlagen: {error}",
|
|
1116
|
-
invalidJsonAlert: "Ungültiges JSON-Format: {error}",
|
|
1117
|
-
unknownError: "Unbekannter Fehler",
|
|
1118
|
-
update: "Aktualisieren",
|
|
1119
|
-
stringType: "Zeichenkette",
|
|
1120
|
-
formulaType: "Formel",
|
|
1121
|
-
},
|
|
1122
1102
|
app: {
|
|
1123
1103
|
startConversation: "Gespräch beginnen",
|
|
1124
1104
|
thinking: "Denkt nach…",
|
package/src/lang/en.ts
CHANGED
|
@@ -1109,26 +1109,6 @@ const enMessages = {
|
|
|
1109
1109
|
pluginSkill: {
|
|
1110
1110
|
noDescription: "(no description)",
|
|
1111
1111
|
},
|
|
1112
|
-
pluginSpreadsheet: {
|
|
1113
|
-
previewUntitled: "Spreadsheet",
|
|
1114
|
-
previewSheets: "{count} sheet | {count} sheets",
|
|
1115
|
-
untitled: "Spreadsheet",
|
|
1116
|
-
excel: "Excel",
|
|
1117
|
-
valuePlaceholder: "Value",
|
|
1118
|
-
valueOrFormulaPlaceholder: "Value or Formula (e.g., 100 or SUM(B2:B11))",
|
|
1119
|
-
formatPlaceholder: "Format (e.g., $#,##0.00)",
|
|
1120
|
-
loading: "Loading spreadsheet...",
|
|
1121
|
-
noData: "No spreadsheet data available",
|
|
1122
|
-
editData: "Edit Spreadsheet Data",
|
|
1123
|
-
applyChanges: "Apply Changes",
|
|
1124
|
-
dataMustBeArray: "Data must be an array of sheets",
|
|
1125
|
-
loadFailed: "Failed to load spreadsheet: {error}",
|
|
1126
|
-
invalidJsonAlert: "Invalid JSON format: {error}",
|
|
1127
|
-
unknownError: "Unknown error",
|
|
1128
|
-
update: "Update",
|
|
1129
|
-
stringType: "String",
|
|
1130
|
-
formulaType: "Formula",
|
|
1131
|
-
},
|
|
1132
1112
|
app: {
|
|
1133
1113
|
startConversation: "Start a conversation",
|
|
1134
1114
|
thinking: "Thinking…",
|
package/src/lang/es.ts
CHANGED
|
@@ -1097,26 +1097,6 @@ const esMessages = {
|
|
|
1097
1097
|
pluginSkill: {
|
|
1098
1098
|
noDescription: "(sin descripción)",
|
|
1099
1099
|
},
|
|
1100
|
-
pluginSpreadsheet: {
|
|
1101
|
-
previewUntitled: "Hoja de cálculo",
|
|
1102
|
-
previewSheets: "{count} hoja | {count} hojas",
|
|
1103
|
-
untitled: "Hoja de cálculo",
|
|
1104
|
-
excel: "Excel",
|
|
1105
|
-
valuePlaceholder: "Valor",
|
|
1106
|
-
valueOrFormulaPlaceholder: "Valor o Fórmula (ej. 100 o SUM(B2:B11))",
|
|
1107
|
-
formatPlaceholder: "Formato (ej. $#,##0.00)",
|
|
1108
|
-
loading: "Cargando la hoja de cálculo...",
|
|
1109
|
-
noData: "No hay datos de hoja de cálculo disponibles",
|
|
1110
|
-
editData: "Editar datos de la hoja de cálculo",
|
|
1111
|
-
applyChanges: "Aplicar cambios",
|
|
1112
|
-
dataMustBeArray: "Los datos deben ser una matriz de hojas",
|
|
1113
|
-
loadFailed: "Error al cargar la hoja de cálculo: {error}",
|
|
1114
|
-
invalidJsonAlert: "Formato JSON no válido: {error}",
|
|
1115
|
-
unknownError: "Error desconocido",
|
|
1116
|
-
update: "Actualizar",
|
|
1117
|
-
stringType: "Texto",
|
|
1118
|
-
formulaType: "Fórmula",
|
|
1119
|
-
},
|
|
1120
1100
|
app: {
|
|
1121
1101
|
startConversation: "Inicia una conversación",
|
|
1122
1102
|
thinking: "Pensando…",
|
package/src/lang/fr.ts
CHANGED
|
@@ -1089,26 +1089,6 @@ const frMessages = {
|
|
|
1089
1089
|
pluginSkill: {
|
|
1090
1090
|
noDescription: "(aucune description)",
|
|
1091
1091
|
},
|
|
1092
|
-
pluginSpreadsheet: {
|
|
1093
|
-
previewUntitled: "Tableur",
|
|
1094
|
-
previewSheets: "{count} feuille | {count} feuilles",
|
|
1095
|
-
untitled: "Tableur",
|
|
1096
|
-
excel: "Excel",
|
|
1097
|
-
valuePlaceholder: "Valeur",
|
|
1098
|
-
valueOrFormulaPlaceholder: "Valeur ou formule (ex. 100 ou SUM(B2:B11))",
|
|
1099
|
-
formatPlaceholder: "Format (ex. $#,##0.00)",
|
|
1100
|
-
loading: "Chargement du tableur...",
|
|
1101
|
-
noData: "Aucune donnée de tableur disponible",
|
|
1102
|
-
editData: "Modifier les données du tableur",
|
|
1103
|
-
applyChanges: "Appliquer les modifications",
|
|
1104
|
-
dataMustBeArray: "Les données doivent être un tableau de feuilles",
|
|
1105
|
-
loadFailed: "Échec du chargement du tableur : {error}",
|
|
1106
|
-
invalidJsonAlert: "Format JSON non valide : {error}",
|
|
1107
|
-
unknownError: "Erreur inconnue",
|
|
1108
|
-
update: "Mettre à jour",
|
|
1109
|
-
stringType: "Texte",
|
|
1110
|
-
formulaType: "Formule",
|
|
1111
|
-
},
|
|
1112
1092
|
app: {
|
|
1113
1093
|
startConversation: "Démarrer une conversation",
|
|
1114
1094
|
thinking: "Réflexion…",
|
package/src/lang/ja.ts
CHANGED
|
@@ -1083,26 +1083,6 @@ const jaMessages = {
|
|
|
1083
1083
|
pluginSkill: {
|
|
1084
1084
|
noDescription: "(説明なし)",
|
|
1085
1085
|
},
|
|
1086
|
-
pluginSpreadsheet: {
|
|
1087
|
-
previewUntitled: "スプレッドシート",
|
|
1088
|
-
previewSheets: "{count} シート",
|
|
1089
|
-
untitled: "スプレッドシート",
|
|
1090
|
-
excel: "Excel",
|
|
1091
|
-
valuePlaceholder: "値",
|
|
1092
|
-
valueOrFormulaPlaceholder: "値または数式(例: 100, SUM(B2:B11))",
|
|
1093
|
-
formatPlaceholder: "書式(例: $#,##0.00)",
|
|
1094
|
-
loading: "スプレッドシート読み込み中...",
|
|
1095
|
-
noData: "スプレッドシートのデータがありません",
|
|
1096
|
-
editData: "スプレッドシートデータを編集",
|
|
1097
|
-
applyChanges: "変更を適用",
|
|
1098
|
-
dataMustBeArray: "データはシートの配列である必要があります",
|
|
1099
|
-
loadFailed: "スプレッドシートの読み込みに失敗: {error}",
|
|
1100
|
-
invalidJsonAlert: "不正な JSON 形式です: {error}",
|
|
1101
|
-
unknownError: "不明なエラー",
|
|
1102
|
-
update: "更新",
|
|
1103
|
-
stringType: "文字列",
|
|
1104
|
-
formulaType: "数式",
|
|
1105
|
-
},
|
|
1106
1086
|
app: {
|
|
1107
1087
|
startConversation: "会話を開始してください",
|
|
1108
1088
|
thinking: "考え中…",
|
package/src/lang/ko.ts
CHANGED
|
@@ -1081,26 +1081,6 @@ const koMessages = {
|
|
|
1081
1081
|
pluginSkill: {
|
|
1082
1082
|
noDescription: "(설명 없음)",
|
|
1083
1083
|
},
|
|
1084
|
-
pluginSpreadsheet: {
|
|
1085
|
-
previewUntitled: "스프레드시트",
|
|
1086
|
-
previewSheets: "{count}개 시트",
|
|
1087
|
-
untitled: "스프레드시트",
|
|
1088
|
-
excel: "Excel",
|
|
1089
|
-
valuePlaceholder: "값",
|
|
1090
|
-
valueOrFormulaPlaceholder: "값 또는 수식 (예: 100 또는 SUM(B2:B11))",
|
|
1091
|
-
formatPlaceholder: "형식 (예: $#,##0.00)",
|
|
1092
|
-
loading: "스프레드시트를 불러오는 중...",
|
|
1093
|
-
noData: "사용 가능한 스프레드시트 데이터가 없습니다",
|
|
1094
|
-
editData: "스프레드시트 데이터 편집",
|
|
1095
|
-
applyChanges: "변경 사항 적용",
|
|
1096
|
-
dataMustBeArray: "데이터는 시트 배열이어야 합니다",
|
|
1097
|
-
loadFailed: "스프레드시트 불러오기 실패: {error}",
|
|
1098
|
-
invalidJsonAlert: "잘못된 JSON 형식: {error}",
|
|
1099
|
-
unknownError: "알 수 없는 오류",
|
|
1100
|
-
update: "업데이트",
|
|
1101
|
-
stringType: "문자열",
|
|
1102
|
-
formulaType: "수식",
|
|
1103
|
-
},
|
|
1104
1084
|
app: {
|
|
1105
1085
|
startConversation: "대화 시작",
|
|
1106
1086
|
thinking: "생각 중…",
|
package/src/lang/pt-BR.ts
CHANGED
|
@@ -1085,26 +1085,6 @@ const ptBRMessages = {
|
|
|
1085
1085
|
pluginSkill: {
|
|
1086
1086
|
noDescription: "(sem descrição)",
|
|
1087
1087
|
},
|
|
1088
|
-
pluginSpreadsheet: {
|
|
1089
|
-
previewUntitled: "Planilha",
|
|
1090
|
-
previewSheets: "{count} aba | {count} abas",
|
|
1091
|
-
untitled: "Planilha",
|
|
1092
|
-
excel: "Excel",
|
|
1093
|
-
valuePlaceholder: "Valor",
|
|
1094
|
-
valueOrFormulaPlaceholder: "Valor ou Fórmula (ex.: 100 ou SUM(B2:B11))",
|
|
1095
|
-
formatPlaceholder: "Formato (ex.: $#,##0.00)",
|
|
1096
|
-
loading: "Carregando planilha...",
|
|
1097
|
-
noData: "Nenhum dado de planilha disponível",
|
|
1098
|
-
editData: "Editar dados da planilha",
|
|
1099
|
-
applyChanges: "Aplicar alterações",
|
|
1100
|
-
dataMustBeArray: "Os dados devem ser uma matriz de planilhas",
|
|
1101
|
-
loadFailed: "Falha ao carregar a planilha: {error}",
|
|
1102
|
-
invalidJsonAlert: "Formato JSON inválido: {error}",
|
|
1103
|
-
unknownError: "Erro desconhecido",
|
|
1104
|
-
update: "Atualizar",
|
|
1105
|
-
stringType: "Texto",
|
|
1106
|
-
formulaType: "Fórmula",
|
|
1107
|
-
},
|
|
1108
1088
|
app: {
|
|
1109
1089
|
startConversation: "Iniciar uma conversa",
|
|
1110
1090
|
thinking: "Pensando…",
|
package/src/lang/zh.ts
CHANGED
|
@@ -1065,26 +1065,6 @@ const zhMessages = {
|
|
|
1065
1065
|
pluginSkill: {
|
|
1066
1066
|
noDescription: "(无描述)",
|
|
1067
1067
|
},
|
|
1068
|
-
pluginSpreadsheet: {
|
|
1069
|
-
previewUntitled: "电子表格",
|
|
1070
|
-
previewSheets: "{count} 个工作表",
|
|
1071
|
-
untitled: "电子表格",
|
|
1072
|
-
excel: "Excel",
|
|
1073
|
-
valuePlaceholder: "值",
|
|
1074
|
-
valueOrFormulaPlaceholder: "值或公式(例如 100 或 SUM(B2:B11))",
|
|
1075
|
-
formatPlaceholder: "格式(例如 $#,##0.00)",
|
|
1076
|
-
loading: "正在加载电子表格...",
|
|
1077
|
-
noData: "没有可用的电子表格数据",
|
|
1078
|
-
editData: "编辑电子表格数据",
|
|
1079
|
-
applyChanges: "应用更改",
|
|
1080
|
-
dataMustBeArray: "数据必须是工作表数组",
|
|
1081
|
-
loadFailed: "加载电子表格失败: {error}",
|
|
1082
|
-
invalidJsonAlert: "无效的 JSON 格式: {error}",
|
|
1083
|
-
unknownError: "未知错误",
|
|
1084
|
-
update: "更新",
|
|
1085
|
-
stringType: "字符串",
|
|
1086
|
-
formulaType: "公式",
|
|
1087
|
-
},
|
|
1088
1068
|
app: {
|
|
1089
1069
|
startConversation: "开始对话",
|
|
1090
1070
|
thinking: "思考中…",
|