mulmoclaude 2.5.0 → 2.6.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/server/index.ts CHANGED
@@ -59,6 +59,10 @@ import "./plugins/html-builtin.js";
59
59
  // presentShapeScript View's useRuntime().dispatch({ kind }) resolves — the
60
60
  // source editor's load / save against artifacts/shapes/.
61
61
  import "./plugins/shapescript-builtin.js";
62
+ // Side-effect: registers the built-in "kentei" dispatch handler so the
63
+ // manageKenteiScript View's useRuntime().dispatch({ kind: "grade" }) resolves
64
+ // (JEV grading with the server's TYPESAFE_API_KEY).
65
+ import "./plugins/kentei-builtin.js";
62
66
  import { enableShapeScriptManifold } from "./plugins/shapescript-csg.js";
63
67
  import { loadRuntimePlugins } from "./plugins/runtime-loader.js";
64
68
  import { evaluateDevPluginGate, loadDevPlugins, parseDevPluginsEnv } from "./plugins/dev-loader.js";
@@ -0,0 +1,47 @@
1
+ // MulmoClaude's host wiring for the manageKenteiScript plugin (`kenteiscript`
2
+ // npm package). Two entry points reach the package's executor:
3
+ //
4
+ // - the tool's actions (getGuide / getSchema / checkScript / present), posted
5
+ // by the MCP bridge to API_ROUTES.kentei.dispatch (server/api/routes/plugins.ts);
6
+ // - the View's `useRuntime().dispatch({ kind: "grade", … })`, registered here
7
+ // as the built-in "kentei" dispatch handler so it resolves on
8
+ // /api/plugins/runtime/kentei/dispatch.
9
+ //
10
+ // Both hand the package the same context. Its only capability is
11
+ // `files.byPath` (`.json` only), which is what makes the tool's `path`
12
+ // argument work — a runtime-plugin install gets an empty context
13
+ // (server/api/routes/runtime-plugin.ts), so `path` would be refused there.
14
+ // Grading reads TYPESAFE_API_KEY (and optional JEV_MODEL / JEV_URL) from this
15
+ // process's environment inside the package; the key never reaches the browser.
16
+ // Imported for side effect at boot (server/index.ts).
17
+
18
+ import { isGradeRequest, manageKenteiScript, type KenteiContext } from "kenteiscript/plugin";
19
+ import { describeKind, registerBuiltinDispatch } from "./builtin-dispatch.js";
20
+ import { makeByPathFileOps } from "../utils/files/by-path.js";
21
+
22
+ /** Scope name — matches `wrapWithScope("kentei", …)` in
23
+ * `src/plugins/manageKenteiScript/index.ts`, which is what the View's
24
+ * `useRuntime().dispatch` uses as the `:pkg` path segment. */
25
+ const KENTEI_SCOPE = "kentei";
26
+
27
+ /** A KenteiScript is a JSON file, so `path` may name only `.json`. */
28
+ export const KENTEI_SCRIPT_EXTENSIONS: readonly string[] = [".json"];
29
+
30
+ // Built once: by-path file ops hold no state.
31
+ const kenteiFiles = { byPath: makeByPathFileOps(KENTEI_SCRIPT_EXTENSIONS) };
32
+
33
+ /** Run the package's executor with MulmoClaude's capabilities added to
34
+ * whatever per-request context the caller has (e.g. `currentResult`). */
35
+ export function executeKenteiScript(context: KenteiContext, args: unknown): Promise<unknown> {
36
+ return manageKenteiScript({ ...context, files: kenteiFiles }, args);
37
+ }
38
+
39
+ // `args` is whatever the View put on the wire. The View dispatches only
40
+ // `{ kind: "grade" }`; tool actions arrive on the MCP route, so anything else
41
+ // here is refused rather than run.
42
+ registerBuiltinDispatch(KENTEI_SCOPE, async (args) => {
43
+ if (!isGradeRequest(args)) {
44
+ throw new Error(`kentei plugin: unrecognised dispatch payload (kind=${describeKind(args)})`);
45
+ }
46
+ return executeKenteiScript({}, args);
47
+ });
@@ -239,11 +239,12 @@ export const ROLES: [Role, ...Role[]] = [
239
239
  name: "Tutor",
240
240
  icon: "school",
241
241
  prompt:
242
- "You are an experienced tutor who adapts to each student's level. Before teaching any topic, you MUST first evaluate the student's current knowledge by asking them 4-5 relevant questions about the topic by calling the putQuestions API. Based on their answers, adjust your teaching approach to match their understanding level. When explaining something to the student, choose the best presentation method for the topic: use presentHTML for topics that benefit from interactive or visual elements (e.g. diagrams, animations, interactive demos, math visualizations, maps, timelines), and use presentDocument for topics that are best explained with structured text and sections (e.g. definitions, historical facts, step-by-step processes). Use generateImage to create visual aids when appropriate. Always encourage critical thinking by asking follow-up questions and checking for understanding throughout the lesson. To evaluate the student's understanding, you can use the presentForm API to create a form that the student can fill out.",
242
+ "You are an experienced tutor who adapts to each student's level. Before teaching any topic, you MUST first evaluate the student's current knowledge with a short quiz of 4-5 questions about the topic. If you have the manageKenteiScript tool, use it for this quiz (the student answers in their own words and each answer is graded); only if you do not have it, ask the questions with putQuestions instead. If manageKenteiScript returns an error, or the quiz results say its answers couldn't be graded, ask the questions again with putQuestions rather than leaving the student without an assessment. Based on the results, adjust your teaching approach to match their understanding level. When explaining something to the student, choose the best presentation method for the topic: use presentHTML for topics that benefit from interactive or visual elements (e.g. diagrams, animations, interactive demos, math visualizations, maps, timelines), and use presentDocument for topics that are best explained with structured text and sections (e.g. definitions, historical facts, step-by-step processes). Use generateImage to create visual aids when appropriate. Always encourage critical thinking by asking follow-up questions and checking for understanding throughout the lesson. To evaluate the student's understanding, you can use the presentForm API to create a form that the student can fill out. For a practice test after a lesson, use manageKenteiScript the same way (or putQuestions if you do not have it). With manageKenteiScript, call getGuide, then write the KenteiScript and call present with it, in one go without replying to the student in between; if present lists problems, fix them and call present again; then wait for the results the student sends.",
243
243
  availablePlugins: [
244
244
  TOOL_NAMES.putQuestions,
245
245
  TOOL_NAMES.presentDocument,
246
246
  TOOL_NAMES.presentForm,
247
+ TOOL_NAMES.manageKenteiScript,
247
248
  TOOL_NAMES.generateImage,
248
249
  TOOL_NAMES.presentHtml,
249
250
  TOOL_NAMES.presentChart,
package/src/main.ts CHANGED
@@ -60,6 +60,7 @@ const pluginEndpointRegistry: EndpointRegistry = {
60
60
  html: API_ROUTES.html,
61
61
  svg: API_ROUTES.svg,
62
62
  shapescript: API_ROUTES.shapescript,
63
+ kentei: API_ROUTES.kentei,
63
64
  chart: API_ROUTES.chart,
64
65
  accounting: API_ROUTES.accounting,
65
66
  canvas: API_ROUTES.canvas,
@@ -8,6 +8,7 @@ import { META as canvasMeta } from "../canvas/meta";
8
8
  import { META as chartMeta } from "../chart/meta";
9
9
  import { META as editImagesMeta } from "../editImages/meta";
10
10
  import { META as generateImageMeta } from "../generateImage/meta";
11
+ import { META as manageKenteiScriptMeta } from "../manageKenteiScript/meta";
11
12
  import { META as manageSkillsMeta } from "../manageSkills/meta";
12
13
  import { META as markdownMeta } from "../markdown/meta";
13
14
  import { META as photoLocationsMeta } from "../photoLocations/meta";
@@ -29,6 +30,7 @@ export const GENERATED_PLUGIN_METAS = [
29
30
  chartMeta,
30
31
  editImagesMeta,
31
32
  generateImageMeta,
33
+ manageKenteiScriptMeta,
32
34
  manageSkillsMeta,
33
35
  markdownMeta,
34
36
  photoLocationsMeta,
@@ -8,6 +8,7 @@ import { REGISTRATION as canvasRegistration } from "../canvas";
8
8
  import { REGISTRATION as chartRegistration } from "../chart";
9
9
  import { REGISTRATION as editImagesRegistration } from "../editImages";
10
10
  import { REGISTRATION as generateImageRegistration } from "../generateImage";
11
+ import { REGISTRATION as manageKenteiScriptRegistration } from "../manageKenteiScript";
11
12
  import { REGISTRATION as manageSkillsRegistration } from "../manageSkills";
12
13
  import { REGISTRATION as markdownRegistration } from "../markdown";
13
14
  import { REGISTRATION as photoLocationsRegistration } from "../photoLocations";
@@ -33,6 +34,7 @@ export const GENERATED_PLUGIN_REGISTRATIONS: readonly PluginRegistration[] = [
33
34
  chartRegistration,
34
35
  editImagesRegistration,
35
36
  generateImageRegistration,
37
+ manageKenteiScriptRegistration,
36
38
  manageSkillsRegistration,
37
39
  markdownRegistration,
38
40
  photoLocationsRegistration,
@@ -7,6 +7,7 @@ import { mcpBinding } from "../server-bindings-types";
7
7
  import accountingDef from "../accounting/definition";
8
8
  import canvasDef from "../canvas/definition";
9
9
  import chartDef from "../chart/definition";
10
+ import manageKenteiScriptDef from "../manageKenteiScript/definition";
10
11
  import manageSkillsDef from "../manageSkills/definition";
11
12
  import markdownDef from "../markdown/definition";
12
13
  import photoLocationsDef from "../photoLocations/definition";
@@ -21,6 +22,7 @@ import spreadsheetDef from "../spreadsheet/definition";
21
22
  import { META as accountingMeta } from "../accounting/meta";
22
23
  import { META as canvasMeta } from "../canvas/meta";
23
24
  import { META as chartMeta } from "../chart/meta";
25
+ import { META as manageKenteiScriptMeta } from "../manageKenteiScript/meta";
24
26
  import { META as manageSkillsMeta } from "../manageSkills/meta";
25
27
  import { META as markdownMeta } from "../markdown/meta";
26
28
  import { META as photoLocationsMeta } from "../photoLocations/meta";
@@ -40,6 +42,7 @@ export const GENERATED_SERVER_BINDINGS: readonly ServerPluginBinding[] = [
40
42
  mcpBinding(accountingDef, accountingMeta),
41
43
  mcpBinding(canvasDef, canvasMeta),
42
44
  mcpBinding(chartDef, chartMeta),
45
+ mcpBinding(manageKenteiScriptDef, manageKenteiScriptMeta),
43
46
  mcpBinding(manageSkillsDef, manageSkillsMeta),
44
47
  mcpBinding(markdownDef, markdownMeta),
45
48
  mcpBinding(photoLocationsDef, photoLocationsMeta),
@@ -0,0 +1,20 @@
1
+ import { META } from "./meta";
2
+ import type { ResolvedRoute } from "../meta-types";
3
+ import { TOOL_DEFINITION } from "kenteiscript/plugin";
4
+
5
+ // manageKenteiScript's tool schema, KenteiScript validation, JEV grading, View
6
+ // and Preview live in the `kenteiscript` npm package (also used by MulmoTerminal
7
+ // and MulmoChat). This built-in is a thin host adapter, like presentShapeScript:
8
+ // it keeps MulmoClaude's routing META and scoped-runtime wrapping, and exists
9
+ // (rather than a runtime plugin) so the server can hand the package
10
+ // `files.byPath`, which is what makes the tool's `path` argument work.
11
+ //
12
+ // Server-only: this imports the package's server entry. The browser side
13
+ // (index.ts) takes the definition from `kenteiscript/plugin/vue`.
14
+ export const TOOL_NAME = META.toolName;
15
+
16
+ /** Resolved-URL view of the plugin's routes (dispatch). Auto-derived from META. */
17
+ export type KenteiScriptEndpoints = { readonly [K in keyof typeof META.apiRoutes]: ResolvedRoute };
18
+
19
+ export { TOOL_DEFINITION };
20
+ export default TOOL_DEFINITION;
@@ -0,0 +1,39 @@
1
+ import type { ToolPlugin } from "gui-chat-protocol/vue";
2
+ import type { PluginRegistration } from "../../tools/types";
3
+ import { plugin as kenteiPlugin, type KenteiData, type KenteiJson } from "kenteiscript/plugin/vue";
4
+ // The View's scoped styles are a standalone stylesheet; Vite lib mode does not
5
+ // inject it, so the consumer imports it (same as presentShapeScript / presentForm).
6
+ import "kenteiscript/plugin/style.css";
7
+ import type { KenteiScriptEndpoints } from "./definition";
8
+ import { META } from "./meta";
9
+ import { makeRouteExecute } from "../execute";
10
+ import { wrapWithScope } from "../scope";
11
+
12
+ // Scope name: the View's `useRuntime().dispatch({ kind: "grade", … })` posts to
13
+ // /api/plugins/runtime/kentei/dispatch, answered by the built-in dispatch handler
14
+ // registered in server/plugins/kentei-builtin.ts (JEV grading with the server's
15
+ // TYPESAFE_API_KEY — the key never reaches the browser).
16
+ const KENTEI_SCOPE = "kentei";
17
+
18
+ const TOOL_NAME = META.toolName;
19
+
20
+ // Imported from the browser entry, not ./definition, so the server bundle (Zod
21
+ // and the grading code) stays out of the client.
22
+ const pkg: ToolPlugin<KenteiData, KenteiJson, object> = kenteiPlugin;
23
+
24
+ // `unknown` JSON slot: the route executor returns the server body as-is, so it
25
+ // cannot vouch for `jsonData`'s shape (the View reads `data`, not `jsonData`).
26
+ const kenteiScriptPlugin: ToolPlugin<KenteiData, unknown, object> = {
27
+ ...pkg,
28
+ // Every action runs on the server (POST /api/kentei), which owns the key
29
+ // check and the `files.byPath` capability for `path`.
30
+ execute: makeRouteExecute<KenteiScriptEndpoints, KenteiData>(KENTEI_SCOPE, "dispatch", TOOL_NAME),
31
+ ...(pkg.viewComponent ? { viewComponent: wrapWithScope(KENTEI_SCOPE, pkg.viewComponent) } : {}),
32
+ ...(pkg.previewComponent ? { previewComponent: wrapWithScope(KENTEI_SCOPE, pkg.previewComponent) } : {}),
33
+ };
34
+ export { TOOL_NAME };
35
+
36
+ export const REGISTRATION: PluginRegistration = {
37
+ toolName: TOOL_NAME,
38
+ entry: kenteiScriptPlugin,
39
+ };
@@ -0,0 +1,13 @@
1
+ import { definePluginMeta } from "../meta-types";
2
+
3
+ export const META = definePluginMeta({
4
+ toolName: "manageKenteiScript",
5
+ apiNamespace: "kentei",
6
+ apiRoutes: {
7
+ /** POST /api/kentei — a manageKenteiScript action (getGuide / getSchema /
8
+ * checkScript / present). The View's `{ kind: "grade" }` calls go through
9
+ * the built-in dispatch channel instead (server/plugins/kentei-builtin.ts). */
10
+ dispatch: { method: "POST", path: "" },
11
+ },
12
+ mcpDispatch: "dispatch",
13
+ });
@@ -11,6 +11,9 @@
11
11
  */
12
12
 
13
13
  import type { SpreadsheetCell } from "./types.js";
14
+ import { isCallableName } from "./registry.js";
15
+ // Registers the functions, so isCallableName knows LOG10 before any sheet is calculated.
16
+ import "./functions/index.js";
14
17
 
15
18
  /** Inputs to the cell builder. Mirrors the mini editor refs in the
16
19
  * View but as plain values so unit tests don't need a Vue runtime. */
@@ -29,7 +32,12 @@ export interface MiniEditorInput {
29
32
  // Anchored at the start of the input (after optional unary +/-) so we
30
33
  // only treat expressions that clearly begin with a function call as
31
34
  // formulas. Unanchored would match "abc FOO(" inside ordinary text.
32
- const FORMULA_FUNCTION_CALL = /^[-+]?\s*[A-Z]+\s*\(/i;
35
+ const FORMULA_FUNCTION_CALL = /^[-+]?\s*([A-Z][A-Z0-9]*)\s*\(/i;
36
+
37
+ const startsWithFunctionCall = (input: string): boolean => {
38
+ const name = input.match(FORMULA_FUNCTION_CALL)?.[1];
39
+ return name !== undefined && isCallableName(name);
40
+ };
33
41
 
34
42
  // `A1 + B2` style — cell reference next to an arithmetic operator.
35
43
  const FORMULA_CELL_OP = /[A-Z]+\d+\s*[+\-*/^]/;
@@ -49,7 +57,7 @@ const STRICT_NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
49
57
  * formula and gets the "=" prefix the engine expects.
50
58
  */
51
59
  export function looksLikeFormula(input: string): boolean {
52
- return FORMULA_FUNCTION_CALL.test(input) || FORMULA_CELL_OP.test(input) || FORMULA_NUMERIC_OP.test(input);
60
+ return startsWithFunctionCall(input) || FORMULA_CELL_OP.test(input) || FORMULA_NUMERIC_OP.test(input);
53
61
  }
54
62
 
55
63
  /**
@@ -4,7 +4,7 @@
4
4
  * Evaluates spreadsheet formulas including functions, cell references, and arithmetic
5
5
  */
6
6
 
7
- import { functionRegistry, tooFewArgumentsError } from "./registry";
7
+ import { functionRegistry, isCallableName, tooFewArgumentsError } from "./registry";
8
8
  import type { CellValue } from "./types";
9
9
  import { parseDate } from "./date-parser";
10
10
  import { caretToPow, replaceConcatOperator, rewriteComparisonEq, isSafeArithmetic, isSafeComparison } from "./translateFormula";
@@ -281,8 +281,8 @@ export function evaluateFormula(formula: string, context: EvaluatorContext): Cel
281
281
 
282
282
  // Check if it's a SIMPLE function call (not a complex expression)
283
283
  // We need to ensure the formula is JUST a function, not "FUNC(...) + something"
284
- const [, funcName, argsStr] = formula.match(/^([A-Z]+)\((.*)\)$/i) ?? [];
285
- if (funcName !== undefined && argsStr !== undefined) {
284
+ const [, funcName, argsStr] = formula.match(/^([A-Z][A-Z0-9]*)\((.*)\)$/i) ?? [];
285
+ if (funcName !== undefined && argsStr !== undefined && isCallableName(funcName)) {
286
286
  // Check that the closing paren is actually the end of the function
287
287
  // by counting parentheses in argsStr
288
288
  let parenDepth = 0;
@@ -346,8 +346,13 @@ export function evaluateFormula(formula: string, context: EvaluatorContext): Cel
346
346
 
347
347
  while (searchIndex < expr.length && iterations < maxIterations) {
348
348
  iterations++;
349
- const [, funcName] = expr.substring(searchIndex).match(/^([A-Z]+)\(/i) ?? [];
350
- if (funcName === undefined) {
349
+ // A call written inside a string literal is text, not a call.
350
+ if (expr[searchIndex] === '"' || expr[searchIndex] === "'") {
351
+ searchIndex = endOfStringLiteral(expr, searchIndex);
352
+ continue;
353
+ }
354
+ const [, funcName] = expr.substring(searchIndex).match(/^([A-Z][A-Z0-9]*)\(/i) ?? [];
355
+ if (funcName === undefined || !isCallableName(funcName)) {
351
356
  // No more functions found, move to next character
352
357
  searchIndex++;
353
358
  if (searchIndex >= expr.length) break;
@@ -101,6 +101,11 @@ class FunctionRegistry {
101
101
 
102
102
  export const functionRegistry = new FunctionRegistry();
103
103
 
104
+ /** Whether `name` written before "(" is a function call. A letters-only name is,
105
+ * registered or not (an unknown one reports #NAME?). A name with digits is only
106
+ * when registered: LOG10 is also shaped like a cell reference, so `A1(2)` is not. */
107
+ export const isCallableName = (name: string): boolean => /^[A-Z]+$/i.test(name) || functionRegistry.hasFunction(name);
108
+
104
109
  /**
105
110
  * Lenient numeric coercion for range aggregation: anything unreadable is 0.
106
111
  * PINNED behaviour (booleans are 0, not Excel's 1/0) — the string parsing lives