@studiometa/productive-mcp 0.10.14 → 0.10.15
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/dist/handlers/api-read.d.ts +2 -1
- package/dist/handlers/api-read.d.ts.map +1 -1
- package/dist/handlers/api-utils.d.ts +20 -0
- package/dist/handlers/api-utils.d.ts.map +1 -1
- package/dist/handlers/help.d.ts +20 -0
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/run.d.ts +24 -0
- package/dist/handlers/run.d.ts.map +1 -0
- package/dist/handlers/search-docs.d.ts +22 -0
- package/dist/handlers/search-docs.d.ts.map +1 -0
- package/dist/{handlers-BE96O-uy.js → handlers-BEeOjytC.js} +974 -10
- package/dist/{handlers-BE96O-uy.js.map → handlers-BEeOjytC.js.map} +1 -1
- package/dist/handlers.js +1 -1
- package/dist/{http-DF9K8Fr4.js → http-BXP2cZn1.js} +4 -4
- package/dist/{http-DF9K8Fr4.js.map → http-BXP2cZn1.js.map} +1 -1
- package/dist/http.js +1 -1
- package/dist/index.js +2 -2
- package/dist/run/bridge.d.ts +53 -0
- package/dist/run/bridge.d.ts.map +1 -0
- package/dist/run/docs.d.ts +23 -0
- package/dist/run/docs.d.ts.map +1 -0
- package/dist/run/engine.d.ts +60 -0
- package/dist/run/engine.d.ts.map +1 -0
- package/dist/run/limits.d.ts +33 -0
- package/dist/run/limits.d.ts.map +1 -0
- package/dist/run/prelude.d.ts +27 -0
- package/dist/run/prelude.d.ts.map +1 -0
- package/dist/run/render.d.ts +30 -0
- package/dist/run/render.d.ts.map +1 -0
- package/dist/run/strip.d.ts +20 -0
- package/dist/run/strip.d.ts.map +1 -0
- package/dist/schema.d.ts +13 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/{stdio-BnfO285Q.js → stdio-pJj1QGos.js} +2 -2
- package/dist/{stdio-BnfO285Q.js.map → stdio-pJj1QGos.js.map} +1 -1
- package/dist/stdio.js +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +93 -4
- package/dist/tools.js.map +1 -1
- package/dist/{version-XQYsroYk.js → version-CQgSvXUX.js} +3 -3
- package/dist/{version-XQYsroYk.js.map → version-CQgSvXUX.js.map} +1 -1
- package/package.json +5 -3
- package/skills/SKILL.md +71 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ProductiveApi, formatActivity, formatAttachment, formatBooking, formatComment, formatCompany, formatCustomField, formatDeal, formatDiscussion, formatListResponse, formatPage, formatPerson, formatProject, formatService, formatTask, formatTimeEntry, formatTimer } from "@studiometa/productive-api";
|
|
2
2
|
import { ACTIONS, REPORT_TYPES, RESOURCES, ResolveError, VALID_REPORT_TYPES, completeTask, createBooking, createComment, createCompany, createDeal, createDiscussion, createPage, createTask, createTimeEntry, deleteAttachment, deleteDiscussion, deletePage, deleteTimeEntry, fromHandlerContext, getAttachment, getBooking, getComment, getCompany, getCustomField, getDeal, getDealContext, getDiscussion, getMyDaySummary, getPage, getPerson, getProject, getProjectContext, getProjectHealthSummary, getReport, getService, getTask, getTaskContext, getTeamPulseSummary, getTimeEntry, getTimer, listActivities, listAttachments, listBookings, listComments, listCompanies, listCustomFields, listDeals, listDiscussions, listPages, listPeople, listProjects, listServices, listTasks, listTimeEntries, listTimers, logDay, readApi, reopenDiscussion, resolveDiscussion, resolveResource, startTimer, stopTimer, updateBooking, updateComment, updateCompany, updateDeal, updateDiscussion, updatePage, updateTask, updateTimeEntry, weeklyStandup, writeApi } from "@studiometa/productive-core";
|
|
3
|
+
import variant from "@jitl/quickjs-singlefile-cjs-release-sync";
|
|
4
|
+
import { newQuickJSWASMModuleFromVariant } from "quickjs-emscripten-core";
|
|
5
|
+
import { stripTypeScriptTypes } from "node:module";
|
|
3
6
|
//#region src/errors.ts
|
|
4
7
|
/**
|
|
5
8
|
* Custom error classes for MCP server
|
|
@@ -2181,7 +2184,7 @@ var allowsEval = cached(() => {
|
|
|
2181
2184
|
return false;
|
|
2182
2185
|
}
|
|
2183
2186
|
});
|
|
2184
|
-
function isPlainObject(o) {
|
|
2187
|
+
function isPlainObject$1(o) {
|
|
2185
2188
|
if (isObject(o) === false) return false;
|
|
2186
2189
|
const ctor = o.constructor;
|
|
2187
2190
|
if (ctor === void 0) return true;
|
|
@@ -2192,7 +2195,7 @@ function isPlainObject(o) {
|
|
|
2192
2195
|
return true;
|
|
2193
2196
|
}
|
|
2194
2197
|
function shallowClone(o) {
|
|
2195
|
-
if (isPlainObject(o)) return { ...o };
|
|
2198
|
+
if (isPlainObject$1(o)) return { ...o };
|
|
2196
2199
|
if (Array.isArray(o)) return [...o];
|
|
2197
2200
|
return o;
|
|
2198
2201
|
}
|
|
@@ -2273,7 +2276,7 @@ function omit(schema, mask) {
|
|
|
2273
2276
|
}));
|
|
2274
2277
|
}
|
|
2275
2278
|
function extend(schema, shape) {
|
|
2276
|
-
if (!isPlainObject(shape)) throw new Error("Invalid input to extend: expected a plain object");
|
|
2279
|
+
if (!isPlainObject$1(shape)) throw new Error("Invalid input to extend: expected a plain object");
|
|
2277
2280
|
const checks = schema._zod.def.checks;
|
|
2278
2281
|
if (checks && checks.length > 0) {
|
|
2279
2282
|
const existingShape = schema._zod.def.shape;
|
|
@@ -2289,7 +2292,7 @@ function extend(schema, shape) {
|
|
|
2289
2292
|
} }));
|
|
2290
2293
|
}
|
|
2291
2294
|
function safeExtend(schema, shape) {
|
|
2292
|
-
if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
2295
|
+
if (!isPlainObject$1(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
2293
2296
|
return clone(schema, mergeDefs(schema._zod.def, { get shape() {
|
|
2294
2297
|
const _shape = {
|
|
2295
2298
|
...schema._zod.def.shape,
|
|
@@ -3803,7 +3806,7 @@ function mergeValues(a, b) {
|
|
|
3803
3806
|
valid: true,
|
|
3804
3807
|
data: a
|
|
3805
3808
|
};
|
|
3806
|
-
if (isPlainObject(a) && isPlainObject(b)) {
|
|
3809
|
+
if (isPlainObject$1(a) && isPlainObject$1(b)) {
|
|
3807
3810
|
const bKeys = Object.keys(b);
|
|
3808
3811
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
3809
3812
|
const newObj = {
|
|
@@ -3879,7 +3882,7 @@ var $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
|
|
|
3879
3882
|
$ZodType.init(inst, def);
|
|
3880
3883
|
inst._zod.parse = (payload, ctx) => {
|
|
3881
3884
|
const input = payload.value;
|
|
3882
|
-
if (!isPlainObject(input)) {
|
|
3885
|
+
if (!isPlainObject$1(input)) {
|
|
3883
3886
|
payload.issues.push({
|
|
3884
3887
|
expected: "record",
|
|
3885
3888
|
code: "invalid_type",
|
|
@@ -6196,7 +6199,8 @@ object({
|
|
|
6196
6199
|
* Full input schema for the raw api_read tool.
|
|
6197
6200
|
*/
|
|
6198
6201
|
var ApiReadToolInputSchema = object({
|
|
6199
|
-
path: string().trim().min(1, "Path cannot be empty").describe("Relative Productive API path"),
|
|
6202
|
+
path: string().trim().min(1, "Path cannot be empty").optional().describe("Relative Productive API path (required unless using \"search\")"),
|
|
6203
|
+
search: string().trim().min(1, "Search cannot be empty").optional().describe("Keyword search over the documented endpoint catalog; returns matching paths"),
|
|
6200
6204
|
describe: boolean().optional(),
|
|
6201
6205
|
filter: FilterSchema,
|
|
6202
6206
|
include: ParamInclude.optional(),
|
|
@@ -6205,7 +6209,7 @@ var ApiReadToolInputSchema = object({
|
|
|
6205
6209
|
per_page: ParamPerPage.optional(),
|
|
6206
6210
|
paginate: boolean().optional(),
|
|
6207
6211
|
max_pages: number().int().min(1).max(50).optional()
|
|
6208
|
-
});
|
|
6212
|
+
}).refine((data) => !!data.path || !!data.search, { message: "Provide either \"path\" (to read/describe) or \"search\" (to find endpoints)." });
|
|
6209
6213
|
/**
|
|
6210
6214
|
* Full input schema for the raw api_write tool.
|
|
6211
6215
|
*/
|
|
@@ -6222,6 +6226,15 @@ var ApiWriteToolInputSchema = object({
|
|
|
6222
6226
|
dry_run: boolean().optional()
|
|
6223
6227
|
});
|
|
6224
6228
|
/**
|
|
6229
|
+
* Full input schema for the sandboxed run_script tool.
|
|
6230
|
+
*/
|
|
6231
|
+
var RunScriptToolInputSchema = object({
|
|
6232
|
+
code: string().min(1, "Code cannot be empty").describe("JavaScript/TypeScript source to run"),
|
|
6233
|
+
args: array(unknown()).optional().describe("Positional arguments exposed as `args`"),
|
|
6234
|
+
flags: record(string(), unknown()).optional().describe("Named values exposed as `flags`"),
|
|
6235
|
+
dry_run: boolean().optional().describe("Record mutations instead of executing them")
|
|
6236
|
+
});
|
|
6237
|
+
/**
|
|
6225
6238
|
* Format Zod validation errors for LLM consumption
|
|
6226
6239
|
*/
|
|
6227
6240
|
function formatValidationErrors(error) {
|
|
@@ -38948,6 +38961,42 @@ function buildMethodExample(path, method, methodSpec) {
|
|
|
38948
38961
|
} };
|
|
38949
38962
|
return example;
|
|
38950
38963
|
}
|
|
38964
|
+
/** Number of documented endpoints in the catalog. */
|
|
38965
|
+
function apiEndpointCount() {
|
|
38966
|
+
return Object.keys(PRODUCTIVE_API_REFERENCE).length;
|
|
38967
|
+
}
|
|
38968
|
+
/** Whether a query matches anywhere in an endpoint's documented surface. */
|
|
38969
|
+
function endpointMatches(spec, q) {
|
|
38970
|
+
if (spec.path.toLowerCase().includes(q)) return true;
|
|
38971
|
+
return Object.values(spec.methods).some((method) => method?.summary?.toLowerCase().includes(q) || method?.description?.toLowerCase().includes(q) || method?.operationId?.toLowerCase().includes(q) || Object.keys(method?.filters ?? {}).some((f) => f.toLowerCase().includes(q)));
|
|
38972
|
+
}
|
|
38973
|
+
/**
|
|
38974
|
+
* Keyword-search the documented endpoint catalog, returning matching paths
|
|
38975
|
+
* (not their full specs) so an agent can drill in with `api_read describe`.
|
|
38976
|
+
* Pure — reused by `api_read` search and the global `search_docs` tool.
|
|
38977
|
+
*/
|
|
38978
|
+
function searchApiEndpoints(query, limit = 30) {
|
|
38979
|
+
const q = query.trim().toLowerCase();
|
|
38980
|
+
const all = [];
|
|
38981
|
+
for (const spec of Object.values(PRODUCTIVE_API_REFERENCE)) {
|
|
38982
|
+
if (!endpointMatches(spec, q)) continue;
|
|
38983
|
+
const methods = Object.keys(spec.methods);
|
|
38984
|
+
const summary = Object.values(spec.methods).map((m) => m?.summary).find(Boolean);
|
|
38985
|
+
all.push({
|
|
38986
|
+
path: spec.path,
|
|
38987
|
+
methods,
|
|
38988
|
+
summary
|
|
38989
|
+
});
|
|
38990
|
+
}
|
|
38991
|
+
all.sort((a, b) => a.path.localeCompare(b.path));
|
|
38992
|
+
const matches = all.slice(0, limit);
|
|
38993
|
+
return {
|
|
38994
|
+
query,
|
|
38995
|
+
total: all.length,
|
|
38996
|
+
matches,
|
|
38997
|
+
...all.length > matches.length ? { truncated: true } : {}
|
|
38998
|
+
};
|
|
38999
|
+
}
|
|
38951
39000
|
function describeApiEndpoint(path) {
|
|
38952
39001
|
const normalizedPath = normalizeApiPath(path);
|
|
38953
39002
|
const matches = Object.values(PRODUCTIVE_API_REFERENCE).filter((spec) => {
|
|
@@ -38978,6 +39027,11 @@ function describeApiEndpoint(path) {
|
|
|
38978
39027
|
//#region src/handlers/api-read.ts
|
|
38979
39028
|
async function handleApiRead(args, ctx) {
|
|
38980
39029
|
try {
|
|
39030
|
+
if (args.search) return jsonResult({
|
|
39031
|
+
...searchApiEndpoints(args.search),
|
|
39032
|
+
_tip: "Call api_read with describe=true and a matching path for its full spec (filters, sort, body fields)."
|
|
39033
|
+
});
|
|
39034
|
+
if (!args.path) throw new UserInputError("Provide \"path\" to read/describe an endpoint, or \"search\" to find one.");
|
|
38981
39035
|
if (args.describe) return jsonResult(describeApiEndpoint(args.path));
|
|
38982
39036
|
validatePagination(args);
|
|
38983
39037
|
const { methodSpec, normalizedPath } = resolveApiEndpoint(args.path, "GET");
|
|
@@ -40562,6 +40616,59 @@ function handleHelp(resource) {
|
|
|
40562
40616
|
...help
|
|
40563
40617
|
});
|
|
40564
40618
|
}
|
|
40619
|
+
/** Collect the places a query matches within one resource's help. */
|
|
40620
|
+
function matchResourceHelp(resource, help, q) {
|
|
40621
|
+
const hits = [];
|
|
40622
|
+
const has = (s) => !!s && s.toLowerCase().includes(q);
|
|
40623
|
+
if (resource.toLowerCase().includes(q)) hits.push("resource name");
|
|
40624
|
+
if (has(help.description)) hits.push("description");
|
|
40625
|
+
for (const [action, desc] of Object.entries(help.actions)) if (action.toLowerCase().includes(q) || has(desc)) hits.push(`action: ${action}`);
|
|
40626
|
+
for (const [filter, desc] of Object.entries(help.filters ?? {})) if (filter.toLowerCase().includes(q) || has(desc)) hits.push(`filter: ${filter}`);
|
|
40627
|
+
for (const [field, desc] of Object.entries(help.fields ?? {})) if (field.toLowerCase().includes(q) || has(desc)) hits.push(`field: ${field}`);
|
|
40628
|
+
for (const include of help.includes ?? []) if (include.toLowerCase().includes(q)) hits.push(`include: ${include}`);
|
|
40629
|
+
for (const example of help.examples ?? []) if (has(example.description)) hits.push(`example: ${example.description}`);
|
|
40630
|
+
return hits;
|
|
40631
|
+
}
|
|
40632
|
+
/** All resource names that have help documentation. */
|
|
40633
|
+
function helpResourceNames() {
|
|
40634
|
+
return Object.keys(RESOURCE_HELP);
|
|
40635
|
+
}
|
|
40636
|
+
/**
|
|
40637
|
+
* Search help across all resources, returning a compact ranked list of matching
|
|
40638
|
+
* resources (not their full docs). Pure — reused by both the `action=help`
|
|
40639
|
+
* query path and the global `search_docs` tool.
|
|
40640
|
+
*/
|
|
40641
|
+
function searchResourceHelp(query) {
|
|
40642
|
+
const q = query.trim().toLowerCase();
|
|
40643
|
+
const matches = [];
|
|
40644
|
+
for (const [resource, help] of Object.entries(RESOURCE_HELP)) {
|
|
40645
|
+
const hits = matchResourceHelp(resource, help, q);
|
|
40646
|
+
if (hits.length > 0) matches.push({
|
|
40647
|
+
resource,
|
|
40648
|
+
description: help.description,
|
|
40649
|
+
matched_in: hits.slice(0, 6)
|
|
40650
|
+
});
|
|
40651
|
+
}
|
|
40652
|
+
return matches.toSorted((a, b) => b.matched_in.length - a.matched_in.length || a.resource.localeCompare(b.resource));
|
|
40653
|
+
}
|
|
40654
|
+
/**
|
|
40655
|
+
* `action=help` query path: cross-resource help search as a tool result, so an
|
|
40656
|
+
* agent can drill into a specific resource with action="help".
|
|
40657
|
+
*/
|
|
40658
|
+
function handleHelpSearch(query) {
|
|
40659
|
+
const matches = searchResourceHelp(query);
|
|
40660
|
+
if (matches.length === 0) return jsonResult({
|
|
40661
|
+
query,
|
|
40662
|
+
matches: [],
|
|
40663
|
+
available_resources: helpResourceNames(),
|
|
40664
|
+
_tip: "No matches. Call action=\"help\" with a resource for its full documentation."
|
|
40665
|
+
});
|
|
40666
|
+
return jsonResult({
|
|
40667
|
+
query,
|
|
40668
|
+
matches,
|
|
40669
|
+
_tip: "Call action=\"help\" with resource=\"<name>\" for full filters, fields, and examples."
|
|
40670
|
+
});
|
|
40671
|
+
}
|
|
40565
40672
|
/**
|
|
40566
40673
|
* Get help for all resources (overview)
|
|
40567
40674
|
*/
|
|
@@ -40761,6 +40868,854 @@ async function handleReports(action, args, ctx) {
|
|
|
40761
40868
|
});
|
|
40762
40869
|
}
|
|
40763
40870
|
//#endregion
|
|
40871
|
+
//#region src/run/bridge.ts
|
|
40872
|
+
/** Productive actions that mutate data — intercepted in dry-run mode. */
|
|
40873
|
+
var MUTATING_ACTIONS = new Set([
|
|
40874
|
+
"create",
|
|
40875
|
+
"update",
|
|
40876
|
+
"delete",
|
|
40877
|
+
"start",
|
|
40878
|
+
"stop",
|
|
40879
|
+
"reopen",
|
|
40880
|
+
"complete_task",
|
|
40881
|
+
"log_day"
|
|
40882
|
+
]);
|
|
40883
|
+
/** Whether a call would mutate data (used for dry-run classification). */
|
|
40884
|
+
function isMutating(channel, payload) {
|
|
40885
|
+
if (channel === "api_write") return true;
|
|
40886
|
+
if (channel === "productive") return MUTATING_ACTIONS.has(String(payload.action));
|
|
40887
|
+
return false;
|
|
40888
|
+
}
|
|
40889
|
+
/** Map a channel to its tool name. */
|
|
40890
|
+
function toolNameFor(channel) {
|
|
40891
|
+
return channel === "productive" ? "productive" : channel;
|
|
40892
|
+
}
|
|
40893
|
+
/**
|
|
40894
|
+
* Extract the JSON text from a tool result, throwing on error results so that
|
|
40895
|
+
* guest-side `try/catch` works naturally. The text is returned verbatim (MCP
|
|
40896
|
+
* handlers already produce JSON via `jsonResult`); the guest parses it once.
|
|
40897
|
+
*/
|
|
40898
|
+
function toJsonText(result) {
|
|
40899
|
+
const content = result.content?.[0];
|
|
40900
|
+
const text = content && content.type === "text" ? content.text : void 0;
|
|
40901
|
+
if (result.isError) throw new Error(text ?? "Unknown tool error");
|
|
40902
|
+
if (text === void 0) throw new Error("Tool returned no content");
|
|
40903
|
+
return text;
|
|
40904
|
+
}
|
|
40905
|
+
/**
|
|
40906
|
+
* Create a bridge bound to a set of credentials and limits.
|
|
40907
|
+
*/
|
|
40908
|
+
function createBridge(opts) {
|
|
40909
|
+
let apiCalls = 0;
|
|
40910
|
+
const recorded = [];
|
|
40911
|
+
async function call(channel, payload) {
|
|
40912
|
+
if (channel !== "productive" && channel !== "api_read" && channel !== "api_write") throw new Error(`Unknown bridge channel: ${channel}`);
|
|
40913
|
+
if (opts.signal.aborted) throw new Error("Script execution timed out");
|
|
40914
|
+
if (apiCalls >= opts.limits.maxApiCalls) throw new Error(`API call budget exceeded (max ${opts.limits.maxApiCalls})`);
|
|
40915
|
+
apiCalls += 1;
|
|
40916
|
+
if (opts.dryRun && isMutating(channel, payload)) {
|
|
40917
|
+
recorded.push({
|
|
40918
|
+
channel,
|
|
40919
|
+
payload
|
|
40920
|
+
});
|
|
40921
|
+
return JSON.stringify({
|
|
40922
|
+
_dryRun: true,
|
|
40923
|
+
channel,
|
|
40924
|
+
payload
|
|
40925
|
+
});
|
|
40926
|
+
}
|
|
40927
|
+
return toJsonText(await opts.exec(toolNameFor(channel), payload, opts.credentials));
|
|
40928
|
+
}
|
|
40929
|
+
return {
|
|
40930
|
+
call,
|
|
40931
|
+
getStats: () => ({
|
|
40932
|
+
apiCalls,
|
|
40933
|
+
recorded: [...recorded]
|
|
40934
|
+
})
|
|
40935
|
+
};
|
|
40936
|
+
}
|
|
40937
|
+
//#endregion
|
|
40938
|
+
//#region src/run/prelude.ts
|
|
40939
|
+
/**
|
|
40940
|
+
* Guest-side JavaScript prelude injected into the sandbox before user code.
|
|
40941
|
+
*
|
|
40942
|
+
* It builds the `productive`, `api`, `output`, `args`, and `flags` globals on
|
|
40943
|
+
* top of two host primitives provided by the engine:
|
|
40944
|
+
*
|
|
40945
|
+
* - `__hostCall(channel, payloadJson)` → Promise<resultJson> — bridged API call
|
|
40946
|
+
* - `__emit(entryJson)` — synchronous output buffering
|
|
40947
|
+
*
|
|
40948
|
+
* The surface deliberately mirrors the `productive` tool's resource/action
|
|
40949
|
+
* model (what agents already know) rather than the fluent SDK, since no SDK
|
|
40950
|
+
* code runs inside the sandbox.
|
|
40951
|
+
*/
|
|
40952
|
+
/**
|
|
40953
|
+
* Resources that don't map to a simple list/get/create/update shape. These are
|
|
40954
|
+
* still reachable through the low-level `productive(resource, action, params)`
|
|
40955
|
+
* call, just without a convenience accessor.
|
|
40956
|
+
*/
|
|
40957
|
+
var NON_DATA_RESOURCES = new Set([
|
|
40958
|
+
"batch",
|
|
40959
|
+
"search",
|
|
40960
|
+
"summaries",
|
|
40961
|
+
"workflows",
|
|
40962
|
+
"reports"
|
|
40963
|
+
]);
|
|
40964
|
+
/** Resources that get a `productive.<resource>` convenience accessor. */
|
|
40965
|
+
var SCRIPT_RESOURCES = RESOURCES.filter((r) => !NON_DATA_RESOURCES.has(r));
|
|
40966
|
+
/**
|
|
40967
|
+
* Build the prelude source for a run.
|
|
40968
|
+
*
|
|
40969
|
+
* `args` and `flags` are embedded as JSON literals (safe — JSON is a subset of
|
|
40970
|
+
* JS expression syntax).
|
|
40971
|
+
*/
|
|
40972
|
+
function buildPrelude(opts) {
|
|
40973
|
+
return `
|
|
40974
|
+
const __channel = (channel, payload) =>
|
|
40975
|
+
__hostCall(channel, JSON.stringify(payload)).then((s) => JSON.parse(s));
|
|
40976
|
+
|
|
40977
|
+
const __out = (type, data) => {
|
|
40978
|
+
let payload;
|
|
40979
|
+
try {
|
|
40980
|
+
payload = JSON.stringify({ type, data });
|
|
40981
|
+
} catch (e) {
|
|
40982
|
+
// A circular structure / BigInt would otherwise abort the whole script;
|
|
40983
|
+
// emit a placeholder so output.* is best-effort instead.
|
|
40984
|
+
payload = JSON.stringify({ type, data: '[unserializable: ' + String((e && e.message) || e) + ']' });
|
|
40985
|
+
}
|
|
40986
|
+
__emit(payload);
|
|
40987
|
+
};
|
|
40988
|
+
|
|
40989
|
+
// Routing keys (resource/action/id/filter/path) are applied LAST so a
|
|
40990
|
+
// user-supplied param of the same name can never silently change routing.
|
|
40991
|
+
const productive = (resource, action, params = {}) =>
|
|
40992
|
+
__channel('productive', Object.assign({}, params, { resource, action }));
|
|
40993
|
+
|
|
40994
|
+
for (const __r of ${JSON.stringify(SCRIPT_RESOURCES)}) {
|
|
40995
|
+
productive[__r] = {
|
|
40996
|
+
list: (filter = {}, opts = {}) =>
|
|
40997
|
+
__channel('productive', Object.assign({}, opts, { resource: __r, action: 'list', filter })),
|
|
40998
|
+
get: (id, opts = {}) =>
|
|
40999
|
+
__channel('productive', Object.assign({}, opts, { resource: __r, action: 'get', id })),
|
|
41000
|
+
create: (params = {}) =>
|
|
41001
|
+
__channel('productive', Object.assign({}, params, { resource: __r, action: 'create' })),
|
|
41002
|
+
update: (id, params = {}) =>
|
|
41003
|
+
__channel('productive', Object.assign({}, params, { resource: __r, action: 'update', id })),
|
|
41004
|
+
};
|
|
41005
|
+
}
|
|
41006
|
+
|
|
41007
|
+
const api = {
|
|
41008
|
+
read: (path, opts = {}) => __channel('api_read', Object.assign({}, opts, { path })),
|
|
41009
|
+
write: (method, path, body) => __channel('api_write', { method, path, body, confirm: true }),
|
|
41010
|
+
};
|
|
41011
|
+
|
|
41012
|
+
const output = {
|
|
41013
|
+
json: (d) => __out('json', d),
|
|
41014
|
+
table: (d) => __out('table', d),
|
|
41015
|
+
csv: (d) => __out('csv', d),
|
|
41016
|
+
text: (t) => __out('text', String(t)),
|
|
41017
|
+
print: (t) => __out('text', String(t)),
|
|
41018
|
+
log: (...a) =>
|
|
41019
|
+
__out('log', a.map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join(' ')),
|
|
41020
|
+
info: (m) => __out('info', String(m)),
|
|
41021
|
+
warn: (m) => __out('warn', String(m)),
|
|
41022
|
+
error: (m) => __out('error', String(m)),
|
|
41023
|
+
success: (m) => __out('success', String(m)),
|
|
41024
|
+
};
|
|
41025
|
+
|
|
41026
|
+
const args = ${JSON.stringify(opts.args)};
|
|
41027
|
+
const flags = ${JSON.stringify(opts.flags)};
|
|
41028
|
+
|
|
41029
|
+
Object.assign(globalThis, { productive, api, output, args, flags });
|
|
41030
|
+
`;
|
|
41031
|
+
}
|
|
41032
|
+
//#endregion
|
|
41033
|
+
//#region src/run/engine.ts
|
|
41034
|
+
/**
|
|
41035
|
+
* QuickJS-WASM execution engine for sandboxed scripts.
|
|
41036
|
+
*
|
|
41037
|
+
* Each run gets a fresh QuickJS context (isolated globals + heap) created from
|
|
41038
|
+
* a single, process-wide cached WASM module. The sandbox has no ambient
|
|
41039
|
+
* capabilities: the only host functions exposed are `__hostCall` (bridged API
|
|
41040
|
+
* access, returns a real guest Promise so scripts can `await` naturally) and
|
|
41041
|
+
* `__emit` (output buffering).
|
|
41042
|
+
*
|
|
41043
|
+
* Limits enforced here:
|
|
41044
|
+
* - memory — `runtime.setMemoryLimit`
|
|
41045
|
+
* - CPU/loop — an interrupt handler with a wall-clock deadline (fires even
|
|
41046
|
+
* while the host event loop is blocked by a synchronous loop)
|
|
41047
|
+
* - hang — an abort-signal race around the async completion wait
|
|
41048
|
+
* - output — buffered output is capped and flagged as truncated
|
|
41049
|
+
*/
|
|
41050
|
+
/** Error raised when a script fails to compile, throws, or times out. */
|
|
41051
|
+
var ScriptError = class extends Error {
|
|
41052
|
+
stackTrace;
|
|
41053
|
+
constructor(message, stackTrace) {
|
|
41054
|
+
super(message);
|
|
41055
|
+
this.name = "ScriptError";
|
|
41056
|
+
this.stackTrace = stackTrace;
|
|
41057
|
+
}
|
|
41058
|
+
};
|
|
41059
|
+
var modulePromise;
|
|
41060
|
+
/**
|
|
41061
|
+
* Lazily create and cache the QuickJS WASM module (decision: cache the
|
|
41062
|
+
* compiled module across runs; each run still gets a fresh context).
|
|
41063
|
+
*/
|
|
41064
|
+
function getQuickJSModule() {
|
|
41065
|
+
if (!modulePromise) modulePromise = newQuickJSWASMModuleFromVariant(variant);
|
|
41066
|
+
return modulePromise;
|
|
41067
|
+
}
|
|
41068
|
+
/** Build the full source: prelude + user code wrapped in an async entrypoint. */
|
|
41069
|
+
function buildSource(input) {
|
|
41070
|
+
return `${buildPrelude({
|
|
41071
|
+
args: input.args,
|
|
41072
|
+
flags: input.flags
|
|
41073
|
+
})}
|
|
41074
|
+
async function __main() {
|
|
41075
|
+
${input.code}
|
|
41076
|
+
}
|
|
41077
|
+
__main().then(
|
|
41078
|
+
(v) => {
|
|
41079
|
+
try {
|
|
41080
|
+
__resolve(JSON.stringify(v === undefined ? null : v));
|
|
41081
|
+
} catch (e) {
|
|
41082
|
+
__reject(JSON.stringify({ message: 'Result is not serializable: ' + String((e && e.message) || e) }));
|
|
41083
|
+
}
|
|
41084
|
+
},
|
|
41085
|
+
(e) => __reject(JSON.stringify({ message: String((e && e.message) || e), stack: String((e && e.stack) || '') })),
|
|
41086
|
+
);`;
|
|
41087
|
+
}
|
|
41088
|
+
/** A promise that rejects when the abort signal fires. */
|
|
41089
|
+
function abortPromise(signal) {
|
|
41090
|
+
let onAbort = () => {};
|
|
41091
|
+
const promise = new Promise((_resolve, reject) => {
|
|
41092
|
+
if (signal.aborted) {
|
|
41093
|
+
reject(new ScriptError("Script execution timed out"));
|
|
41094
|
+
return;
|
|
41095
|
+
}
|
|
41096
|
+
onAbort = () => reject(new ScriptError("Script execution timed out"));
|
|
41097
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
41098
|
+
});
|
|
41099
|
+
promise.catch(() => {});
|
|
41100
|
+
return {
|
|
41101
|
+
promise,
|
|
41102
|
+
cleanup: () => signal.removeEventListener("abort", onAbort)
|
|
41103
|
+
};
|
|
41104
|
+
}
|
|
41105
|
+
/**
|
|
41106
|
+
* Register the synchronous host functions (`__emit`, `__resolve`, `__reject`)
|
|
41107
|
+
* and the async `__hostCall`, wiring output buffering and result capture.
|
|
41108
|
+
*/
|
|
41109
|
+
function installHostFunctions(ctx, input, state) {
|
|
41110
|
+
ctx.newFunction("__emit", (handle) => {
|
|
41111
|
+
const raw = ctx.getString(handle);
|
|
41112
|
+
state.bytes += Buffer.byteLength(raw, "utf8");
|
|
41113
|
+
if (state.bytes > input.limits.maxOutputBytes) {
|
|
41114
|
+
state.truncated = true;
|
|
41115
|
+
return;
|
|
41116
|
+
}
|
|
41117
|
+
try {
|
|
41118
|
+
state.output.push(JSON.parse(raw));
|
|
41119
|
+
} catch {}
|
|
41120
|
+
}).consume((f) => ctx.setProp(ctx.global, "__emit", f));
|
|
41121
|
+
ctx.newFunction("__resolve", (handle) => {
|
|
41122
|
+
const raw = ctx.getString(handle);
|
|
41123
|
+
try {
|
|
41124
|
+
state.captured = {
|
|
41125
|
+
ok: true,
|
|
41126
|
+
value: JSON.parse(raw)
|
|
41127
|
+
};
|
|
41128
|
+
} catch {
|
|
41129
|
+
state.captured = {
|
|
41130
|
+
ok: true,
|
|
41131
|
+
value: raw
|
|
41132
|
+
};
|
|
41133
|
+
}
|
|
41134
|
+
}).consume((f) => ctx.setProp(ctx.global, "__resolve", f));
|
|
41135
|
+
ctx.newFunction("__reject", (handle) => {
|
|
41136
|
+
const raw = ctx.getString(handle);
|
|
41137
|
+
try {
|
|
41138
|
+
const parsed = JSON.parse(raw);
|
|
41139
|
+
state.captured = {
|
|
41140
|
+
ok: false,
|
|
41141
|
+
message: parsed.message ?? "Error",
|
|
41142
|
+
stack: parsed.stack
|
|
41143
|
+
};
|
|
41144
|
+
} catch {
|
|
41145
|
+
state.captured = {
|
|
41146
|
+
ok: false,
|
|
41147
|
+
message: raw
|
|
41148
|
+
};
|
|
41149
|
+
}
|
|
41150
|
+
}).consume((f) => ctx.setProp(ctx.global, "__reject", f));
|
|
41151
|
+
ctx.newFunction("__hostCall", (channelHandle, payloadHandle) => {
|
|
41152
|
+
const channel = ctx.getString(channelHandle);
|
|
41153
|
+
const payloadStr = ctx.getString(payloadHandle);
|
|
41154
|
+
const deferred = ctx.newPromise();
|
|
41155
|
+
state.pendingDeferreds.add(deferred);
|
|
41156
|
+
(async () => {
|
|
41157
|
+
let payload = {};
|
|
41158
|
+
try {
|
|
41159
|
+
payload = JSON.parse(payloadStr);
|
|
41160
|
+
} catch {}
|
|
41161
|
+
return input.hostCall(channel, payload);
|
|
41162
|
+
})().then((jsonStr) => {
|
|
41163
|
+
if (state.disposed) return;
|
|
41164
|
+
state.pendingDeferreds.delete(deferred);
|
|
41165
|
+
const handle = ctx.newString(jsonStr);
|
|
41166
|
+
deferred.resolve(handle);
|
|
41167
|
+
handle.dispose();
|
|
41168
|
+
}, (err) => {
|
|
41169
|
+
if (state.disposed) return;
|
|
41170
|
+
state.pendingDeferreds.delete(deferred);
|
|
41171
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41172
|
+
const handle = ctx.newError(message);
|
|
41173
|
+
deferred.reject(handle);
|
|
41174
|
+
handle.dispose();
|
|
41175
|
+
});
|
|
41176
|
+
deferred.settled.then(() => {
|
|
41177
|
+
if (state.disposed) return;
|
|
41178
|
+
try {
|
|
41179
|
+
ctx.runtime.executePendingJobs();
|
|
41180
|
+
} catch {}
|
|
41181
|
+
});
|
|
41182
|
+
return deferred.handle;
|
|
41183
|
+
}).consume((f) => ctx.setProp(ctx.global, "__hostCall", f));
|
|
41184
|
+
}
|
|
41185
|
+
/**
|
|
41186
|
+
* Run a script to completion in a sandboxed QuickJS context.
|
|
41187
|
+
*
|
|
41188
|
+
* @throws {ScriptError} on compile error, uncaught guest error, or timeout.
|
|
41189
|
+
*/
|
|
41190
|
+
async function runScript(input) {
|
|
41191
|
+
const ctx = (await getQuickJSModule()).newContext();
|
|
41192
|
+
const runtime = ctx.runtime;
|
|
41193
|
+
runtime.setMemoryLimit(input.limits.memoryBytes);
|
|
41194
|
+
runtime.setMaxStackSize(1024 * 1024);
|
|
41195
|
+
const deadline = Date.now() + input.limits.timeoutMs;
|
|
41196
|
+
let interrupted = false;
|
|
41197
|
+
runtime.setInterruptHandler(() => {
|
|
41198
|
+
if (Date.now() > deadline || input.signal.aborted) {
|
|
41199
|
+
interrupted = true;
|
|
41200
|
+
return true;
|
|
41201
|
+
}
|
|
41202
|
+
return false;
|
|
41203
|
+
});
|
|
41204
|
+
const state = {
|
|
41205
|
+
output: [],
|
|
41206
|
+
bytes: 0,
|
|
41207
|
+
truncated: false,
|
|
41208
|
+
captured: void 0,
|
|
41209
|
+
disposed: false,
|
|
41210
|
+
pendingDeferreds: /* @__PURE__ */ new Set()
|
|
41211
|
+
};
|
|
41212
|
+
const abort = abortPromise(input.signal);
|
|
41213
|
+
try {
|
|
41214
|
+
installHostFunctions(ctx, input, state);
|
|
41215
|
+
const evalResult = ctx.evalCode(buildSource(input), "script.js");
|
|
41216
|
+
if (evalResult.error) {
|
|
41217
|
+
const dumped = safeDump(ctx, evalResult.error);
|
|
41218
|
+
evalResult.error.dispose();
|
|
41219
|
+
if (interrupted) throw new ScriptError("Script execution timed out");
|
|
41220
|
+
throw new ScriptError(formatGuestError(dumped));
|
|
41221
|
+
}
|
|
41222
|
+
const topPromise = evalResult.value;
|
|
41223
|
+
const native = ctx.resolvePromise(topPromise);
|
|
41224
|
+
topPromise.dispose();
|
|
41225
|
+
runtime.executePendingJobs();
|
|
41226
|
+
const settled = await Promise.race([native, abort.promise]);
|
|
41227
|
+
if (settled.error) settled.error.dispose();
|
|
41228
|
+
else settled.value.dispose();
|
|
41229
|
+
} finally {
|
|
41230
|
+
abort.cleanup();
|
|
41231
|
+
state.disposed = true;
|
|
41232
|
+
for (const deferred of state.pendingDeferreds) try {
|
|
41233
|
+
deferred.dispose();
|
|
41234
|
+
} catch {}
|
|
41235
|
+
ctx.dispose();
|
|
41236
|
+
}
|
|
41237
|
+
if (interrupted || input.signal.aborted) throw new ScriptError("Script execution timed out");
|
|
41238
|
+
if (!state.captured) throw new ScriptError("Script did not complete");
|
|
41239
|
+
if (!state.captured.ok) throw new ScriptError(state.captured.message ?? "Script error", state.captured.stack);
|
|
41240
|
+
return {
|
|
41241
|
+
result: state.captured.value,
|
|
41242
|
+
output: state.output,
|
|
41243
|
+
truncated: state.truncated
|
|
41244
|
+
};
|
|
41245
|
+
}
|
|
41246
|
+
/** Dump a guest handle to a host value, tolerating dump failures (e.g. OOM). */
|
|
41247
|
+
function safeDump(ctx, handle) {
|
|
41248
|
+
try {
|
|
41249
|
+
return ctx.dump(handle);
|
|
41250
|
+
} catch {
|
|
41251
|
+
return;
|
|
41252
|
+
}
|
|
41253
|
+
}
|
|
41254
|
+
/** Format a dumped guest error into a single-line message. */
|
|
41255
|
+
function formatGuestError(dumped) {
|
|
41256
|
+
if (dumped && typeof dumped === "object") {
|
|
41257
|
+
const err = dumped;
|
|
41258
|
+
if (err.message) return err.name ? `${err.name}: ${err.message}` : err.message;
|
|
41259
|
+
}
|
|
41260
|
+
if (typeof dumped === "string") return dumped;
|
|
41261
|
+
return "Script failed to compile";
|
|
41262
|
+
}
|
|
41263
|
+
//#endregion
|
|
41264
|
+
//#region src/run/limits.ts
|
|
41265
|
+
var DEFAULTS = {
|
|
41266
|
+
timeoutMs: 5e3,
|
|
41267
|
+
memoryMb: 64,
|
|
41268
|
+
maxApiCalls: 50,
|
|
41269
|
+
maxOutputKb: 256,
|
|
41270
|
+
maxCodeKb: 128
|
|
41271
|
+
};
|
|
41272
|
+
/**
|
|
41273
|
+
* Whether the `run_script` tool is enabled. Off unless explicitly turned on.
|
|
41274
|
+
*/
|
|
41275
|
+
function isRunScriptEnabled(env = process.env) {
|
|
41276
|
+
return env.PRODUCTIVE_MCP_ENABLE_RUN === "true";
|
|
41277
|
+
}
|
|
41278
|
+
/**
|
|
41279
|
+
* Parse a positive integer from an env var, falling back when missing/invalid.
|
|
41280
|
+
*/
|
|
41281
|
+
function parsePositiveInt(value, fallback) {
|
|
41282
|
+
if (value === void 0) return fallback;
|
|
41283
|
+
const n = Number(value);
|
|
41284
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
41285
|
+
}
|
|
41286
|
+
/**
|
|
41287
|
+
* Resolve the active resource limits from the environment.
|
|
41288
|
+
*/
|
|
41289
|
+
function resolveRunLimits(env = process.env) {
|
|
41290
|
+
return {
|
|
41291
|
+
timeoutMs: parsePositiveInt(env.PRODUCTIVE_MCP_RUN_TIMEOUT_MS, DEFAULTS.timeoutMs),
|
|
41292
|
+
memoryBytes: parsePositiveInt(env.PRODUCTIVE_MCP_RUN_MEMORY_MB, DEFAULTS.memoryMb) * 1024 * 1024,
|
|
41293
|
+
maxApiCalls: parsePositiveInt(env.PRODUCTIVE_MCP_RUN_MAX_API_CALLS, DEFAULTS.maxApiCalls),
|
|
41294
|
+
maxOutputBytes: parsePositiveInt(env.PRODUCTIVE_MCP_RUN_MAX_OUTPUT_KB, DEFAULTS.maxOutputKb) * 1024,
|
|
41295
|
+
maxCodeBytes: parsePositiveInt(env.PRODUCTIVE_MCP_RUN_MAX_CODE_KB, DEFAULTS.maxCodeKb) * 1024
|
|
41296
|
+
};
|
|
41297
|
+
}
|
|
41298
|
+
//#endregion
|
|
41299
|
+
//#region src/run/render.ts
|
|
41300
|
+
/** Labels for log-style output entries. */
|
|
41301
|
+
var LOG_LABELS = {
|
|
41302
|
+
info: "ℹ️",
|
|
41303
|
+
warn: "⚠️",
|
|
41304
|
+
error: "❌",
|
|
41305
|
+
success: "✅"
|
|
41306
|
+
};
|
|
41307
|
+
/** Escape a value for use inside a Markdown table cell. */
|
|
41308
|
+
function cell(value) {
|
|
41309
|
+
return (value === null || value === void 0 ? "" : typeof value === "object" ? JSON.stringify(value) : String(value)).replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
41310
|
+
}
|
|
41311
|
+
/** Whether a value is a plain (non-array, non-null) object. */
|
|
41312
|
+
function isPlainObject(value) {
|
|
41313
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41314
|
+
}
|
|
41315
|
+
/** Render an array of objects as a Markdown table. */
|
|
41316
|
+
function markdownTable(rows) {
|
|
41317
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
41318
|
+
if (columns.length === 0) return "_(no columns)_";
|
|
41319
|
+
return `${`| ${columns.join(" | ")} |`}\n${`| ${columns.map(() => "---").join(" | ")} |`}\n${rows.map((row) => `| ${columns.map((c) => cell(row[c])).join(" | ")} |`).join("\n")}`;
|
|
41320
|
+
}
|
|
41321
|
+
/** Build one row of CSV with RFC-4180-style quoting. */
|
|
41322
|
+
function csvRow(values) {
|
|
41323
|
+
return values.map((v) => {
|
|
41324
|
+
const text = v === null || v === void 0 ? "" : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
41325
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, "\"\"")}"` : text;
|
|
41326
|
+
}).join(",");
|
|
41327
|
+
}
|
|
41328
|
+
/** Render an array of objects as CSV. */
|
|
41329
|
+
function toCsv(rows) {
|
|
41330
|
+
const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
41331
|
+
return [csvRow(columns), ...rows.map((row) => csvRow(columns.map((c) => row[c])))].join("\n");
|
|
41332
|
+
}
|
|
41333
|
+
/** Fenced code block. */
|
|
41334
|
+
function fence(lang, body) {
|
|
41335
|
+
return `\`\`\`${lang}\n${body}\n\`\`\``;
|
|
41336
|
+
}
|
|
41337
|
+
/** Render a single output entry to Markdown. */
|
|
41338
|
+
function renderEntry(entry) {
|
|
41339
|
+
const { type, data } = entry;
|
|
41340
|
+
switch (type) {
|
|
41341
|
+
case "table": return Array.isArray(data) && data.length > 0 && data.every(isPlainObject) ? markdownTable(data) : fence("json", JSON.stringify(data, null, 2));
|
|
41342
|
+
case "csv": return Array.isArray(data) && data.length > 0 && data.every(isPlainObject) ? fence("csv", toCsv(data)) : fence("json", JSON.stringify(data, null, 2));
|
|
41343
|
+
case "json": return fence("json", JSON.stringify(data, null, 2));
|
|
41344
|
+
case "text":
|
|
41345
|
+
case "log": return String(data);
|
|
41346
|
+
default: return `${LOG_LABELS[type] ?? `[${type}]`} ${String(data)}`;
|
|
41347
|
+
}
|
|
41348
|
+
}
|
|
41349
|
+
/** Render the run footer (stats line). */
|
|
41350
|
+
function renderFooter(run) {
|
|
41351
|
+
const bits = [`${run.apiCalls} API call${run.apiCalls === 1 ? "" : "s"}`];
|
|
41352
|
+
if (run.dryRun) bits.push("dry run");
|
|
41353
|
+
if (run.outputTruncated) bits.push("output truncated");
|
|
41354
|
+
if (run.recorded && run.recorded.length > 0) bits.push(`${run.recorded.length} recorded mutation${run.recorded.length === 1 ? "" : "s"}`);
|
|
41355
|
+
return `—\n_${bits.join(" · ")}_`;
|
|
41356
|
+
}
|
|
41357
|
+
/**
|
|
41358
|
+
* Render a run result as human-facing Markdown.
|
|
41359
|
+
*/
|
|
41360
|
+
function renderRunResult(input) {
|
|
41361
|
+
const sections = [];
|
|
41362
|
+
if (input.output.length > 0) sections.push(input.output.map(renderEntry).join("\n\n"));
|
|
41363
|
+
if (input.result !== null && input.result !== void 0) {
|
|
41364
|
+
const body = typeof input.result === "string" ? input.result : fence("json", JSON.stringify(input.result, null, 2));
|
|
41365
|
+
sections.push(`**Result:**\n\n${body}`);
|
|
41366
|
+
}
|
|
41367
|
+
if (sections.length === 0) sections.push("_Script completed with no output._");
|
|
41368
|
+
sections.push(renderFooter(input.run));
|
|
41369
|
+
return sections.join("\n\n");
|
|
41370
|
+
}
|
|
41371
|
+
//#endregion
|
|
41372
|
+
//#region src/run/strip.ts
|
|
41373
|
+
/**
|
|
41374
|
+
* TypeScript type-stripping for submitted scripts.
|
|
41375
|
+
*
|
|
41376
|
+
* QuickJS executes JavaScript, so any TypeScript syntax in a submitted script
|
|
41377
|
+
* must be transformed away first. We use Node's built-in
|
|
41378
|
+
* {@link stripTypeScriptTypes} in `transform` mode, which also lowers `enum`
|
|
41379
|
+
* and parameter properties (not just erasing annotations).
|
|
41380
|
+
*
|
|
41381
|
+
* Plain JavaScript passes through unchanged. If the source cannot be parsed as
|
|
41382
|
+
* TypeScript at all, the original is returned untouched so the sandbox can
|
|
41383
|
+
* surface the real syntax error to the caller.
|
|
41384
|
+
*/
|
|
41385
|
+
/**
|
|
41386
|
+
* Strip TypeScript types from a script, returning runnable JavaScript.
|
|
41387
|
+
*
|
|
41388
|
+
* @param code - The submitted script source (TypeScript or JavaScript).
|
|
41389
|
+
* @returns JavaScript with type syntax removed.
|
|
41390
|
+
*/
|
|
41391
|
+
function stripTypes(code) {
|
|
41392
|
+
try {
|
|
41393
|
+
return stripTypeScriptTypes(code, { mode: "transform" });
|
|
41394
|
+
} catch {
|
|
41395
|
+
return code;
|
|
41396
|
+
}
|
|
41397
|
+
}
|
|
41398
|
+
//#endregion
|
|
41399
|
+
//#region src/handlers/run.ts
|
|
41400
|
+
/** Coerce the `args` argument into a string array. */
|
|
41401
|
+
function normalizeArgs(value) {
|
|
41402
|
+
if (!Array.isArray(value)) return [];
|
|
41403
|
+
return value.map((v) => String(v));
|
|
41404
|
+
}
|
|
41405
|
+
/** Coerce the `flags` argument into a plain object. */
|
|
41406
|
+
function normalizeFlags(value) {
|
|
41407
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
41408
|
+
return {};
|
|
41409
|
+
}
|
|
41410
|
+
/**
|
|
41411
|
+
* Execute the `run_script` tool.
|
|
41412
|
+
*/
|
|
41413
|
+
async function handleRunScript(rawArgs, credentials, exec, env = process.env) {
|
|
41414
|
+
if (!isRunScriptEnabled(env)) return inputErrorResult(new UserInputError("run_script is disabled. Set PRODUCTIVE_MCP_ENABLE_RUN=true to enable it."));
|
|
41415
|
+
if (typeof rawArgs.code !== "string" || rawArgs.code.trim() === "") return inputErrorResult(new UserInputError("code is required and must be a non-empty string.", [
|
|
41416
|
+
"Provide a JavaScript/TypeScript script in the \"code\" parameter.",
|
|
41417
|
+
"Available globals: productive(resource, action, params), api.read/write, output.*, args, flags.",
|
|
41418
|
+
"Return a value to surface it as the result; use output.json(...) for additional data."
|
|
41419
|
+
]));
|
|
41420
|
+
const limits = resolveRunLimits(env);
|
|
41421
|
+
const codeBytes = Buffer.byteLength(rawArgs.code, "utf8");
|
|
41422
|
+
if (codeBytes > limits.maxCodeBytes) return inputErrorResult(new UserInputError(`Script is too large (${codeBytes} bytes, max ${limits.maxCodeBytes}).`));
|
|
41423
|
+
const dryRun = rawArgs.dry_run === true;
|
|
41424
|
+
const controller = new AbortController();
|
|
41425
|
+
const timer = setTimeout(() => controller.abort(), limits.timeoutMs);
|
|
41426
|
+
try {
|
|
41427
|
+
const bridge = createBridge({
|
|
41428
|
+
credentials,
|
|
41429
|
+
exec,
|
|
41430
|
+
limits,
|
|
41431
|
+
dryRun,
|
|
41432
|
+
signal: controller.signal
|
|
41433
|
+
});
|
|
41434
|
+
const result = await runScript({
|
|
41435
|
+
code: stripTypes(rawArgs.code),
|
|
41436
|
+
args: normalizeArgs(rawArgs.args),
|
|
41437
|
+
flags: normalizeFlags(rawArgs.flags),
|
|
41438
|
+
limits,
|
|
41439
|
+
signal: controller.signal,
|
|
41440
|
+
hostCall: (channel, payload) => bridge.call(channel, payload)
|
|
41441
|
+
});
|
|
41442
|
+
const stats = bridge.getStats();
|
|
41443
|
+
const run = {
|
|
41444
|
+
apiCalls: stats.apiCalls,
|
|
41445
|
+
dryRun,
|
|
41446
|
+
...result.truncated ? { outputTruncated: true } : {},
|
|
41447
|
+
...dryRun ? { recorded: stats.recorded } : {}
|
|
41448
|
+
};
|
|
41449
|
+
return {
|
|
41450
|
+
content: [{
|
|
41451
|
+
type: "text",
|
|
41452
|
+
text: renderRunResult({
|
|
41453
|
+
result: result.result,
|
|
41454
|
+
output: result.output,
|
|
41455
|
+
run
|
|
41456
|
+
})
|
|
41457
|
+
}],
|
|
41458
|
+
structuredContent: {
|
|
41459
|
+
result: result.result,
|
|
41460
|
+
output: result.output,
|
|
41461
|
+
_run: run
|
|
41462
|
+
}
|
|
41463
|
+
};
|
|
41464
|
+
} catch (error) {
|
|
41465
|
+
if (error instanceof ScriptError) return errorResult(error.message);
|
|
41466
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
41467
|
+
} finally {
|
|
41468
|
+
clearTimeout(timer);
|
|
41469
|
+
}
|
|
41470
|
+
}
|
|
41471
|
+
//#endregion
|
|
41472
|
+
//#region src/run/docs.ts
|
|
41473
|
+
/**
|
|
41474
|
+
* Scripting-API reference content for `run_script`, surfaced through the global
|
|
41475
|
+
* `search_docs` tool (resources / endpoints / scripting all discoverable from
|
|
41476
|
+
* one place).
|
|
41477
|
+
*
|
|
41478
|
+
* This module owns the content as structured sections; `search_docs` is the
|
|
41479
|
+
* only consumer (it lists section titles in its table of contents and returns
|
|
41480
|
+
* matching section bodies on a query). The resource list is derived from
|
|
41481
|
+
* {@link SCRIPT_RESOURCES} so it can't drift from what the prelude exposes.
|
|
41482
|
+
*/
|
|
41483
|
+
var DOC_SECTIONS = [
|
|
41484
|
+
{
|
|
41485
|
+
title: "Overview",
|
|
41486
|
+
summary: "What run_script is and how the sandbox works.",
|
|
41487
|
+
keywords: [
|
|
41488
|
+
"overview",
|
|
41489
|
+
"intro",
|
|
41490
|
+
"sandbox",
|
|
41491
|
+
"how",
|
|
41492
|
+
"start"
|
|
41493
|
+
],
|
|
41494
|
+
body: [
|
|
41495
|
+
"`run_script` runs JavaScript/TypeScript in a sandboxed QuickJS isolate. There is no direct",
|
|
41496
|
+
"network or filesystem access — the injected client performs Productive API calls on the host.",
|
|
41497
|
+
"Write code using the globals below and `return` a value to surface it as the result.",
|
|
41498
|
+
"No `import`/`require`: use the injected globals only."
|
|
41499
|
+
].join(" ")
|
|
41500
|
+
},
|
|
41501
|
+
{
|
|
41502
|
+
title: "productive client",
|
|
41503
|
+
summary: "productive(...) and per-resource list/get/create/update accessors.",
|
|
41504
|
+
keywords: [
|
|
41505
|
+
"productive",
|
|
41506
|
+
"client",
|
|
41507
|
+
"resource",
|
|
41508
|
+
"action",
|
|
41509
|
+
"list",
|
|
41510
|
+
"get",
|
|
41511
|
+
"create",
|
|
41512
|
+
"update",
|
|
41513
|
+
"call"
|
|
41514
|
+
],
|
|
41515
|
+
body: [
|
|
41516
|
+
"`productive(resource, action, params)` — low-level call, mirrors the `productive` tool.",
|
|
41517
|
+
"Per-resource accessors (all async — `await` them):",
|
|
41518
|
+
"- `productive.<resource>.list(filter?, opts?)`",
|
|
41519
|
+
"- `productive.<resource>.get(id, opts?)`",
|
|
41520
|
+
"- `productive.<resource>.create(params)`",
|
|
41521
|
+
"- `productive.<resource>.update(id, params)`",
|
|
41522
|
+
"",
|
|
41523
|
+
"Example: `const tasks = await productive.tasks.list({ status: 'open' });`"
|
|
41524
|
+
].join("\n")
|
|
41525
|
+
},
|
|
41526
|
+
{
|
|
41527
|
+
title: "api client (raw)",
|
|
41528
|
+
summary: "api.read / api.write for raw API endpoints.",
|
|
41529
|
+
keywords: [
|
|
41530
|
+
"api",
|
|
41531
|
+
"read",
|
|
41532
|
+
"write",
|
|
41533
|
+
"raw",
|
|
41534
|
+
"endpoint",
|
|
41535
|
+
"path",
|
|
41536
|
+
"fetch"
|
|
41537
|
+
],
|
|
41538
|
+
body: ["`api.read(path, opts?)` — raw GET, e.g. `await api.read(\"/invoices\", { page: 2 })`.", "`api.write(method, path, body)` — raw POST/PATCH/PUT/DELETE (requires api_write enabled on the server)."].join("\n")
|
|
41539
|
+
},
|
|
41540
|
+
{
|
|
41541
|
+
title: "output helpers",
|
|
41542
|
+
summary: "output.json/table/csv/log/... and how they render.",
|
|
41543
|
+
keywords: [
|
|
41544
|
+
"output",
|
|
41545
|
+
"table",
|
|
41546
|
+
"csv",
|
|
41547
|
+
"json",
|
|
41548
|
+
"log",
|
|
41549
|
+
"print",
|
|
41550
|
+
"info",
|
|
41551
|
+
"warn",
|
|
41552
|
+
"error",
|
|
41553
|
+
"success",
|
|
41554
|
+
"render"
|
|
41555
|
+
],
|
|
41556
|
+
body: [
|
|
41557
|
+
"Buffer output that is returned alongside the result and rendered to Markdown:",
|
|
41558
|
+
"- `output.json(data)` — fenced JSON block",
|
|
41559
|
+
"- `output.table(rows)` — Markdown table (rows = array of objects)",
|
|
41560
|
+
"- `output.csv(rows)` — fenced CSV block",
|
|
41561
|
+
"- `output.text(s)` / `output.log(...args)` — plain lines",
|
|
41562
|
+
"- `output.info/warn/error/success(msg)` — labelled lines"
|
|
41563
|
+
].join("\n")
|
|
41564
|
+
},
|
|
41565
|
+
{
|
|
41566
|
+
title: "args, flags & result",
|
|
41567
|
+
summary: "Inputs (args, flags) and returning a result.",
|
|
41568
|
+
keywords: [
|
|
41569
|
+
"args",
|
|
41570
|
+
"flags",
|
|
41571
|
+
"input",
|
|
41572
|
+
"parameters",
|
|
41573
|
+
"return",
|
|
41574
|
+
"result"
|
|
41575
|
+
],
|
|
41576
|
+
body: ["`args` (string[]) and `flags` (object) are the values passed in the tool call.", "`return <value>` surfaces a JSON-serializable result (also available as `structuredContent.result`)."].join("\n")
|
|
41577
|
+
},
|
|
41578
|
+
{
|
|
41579
|
+
title: "resources & actions",
|
|
41580
|
+
summary: "Available resources and how to discover their filters/fields.",
|
|
41581
|
+
keywords: [
|
|
41582
|
+
"resources",
|
|
41583
|
+
"actions",
|
|
41584
|
+
"help",
|
|
41585
|
+
"schema",
|
|
41586
|
+
"filters",
|
|
41587
|
+
"fields",
|
|
41588
|
+
"includes"
|
|
41589
|
+
],
|
|
41590
|
+
body: [
|
|
41591
|
+
`Resource accessors: ${SCRIPT_RESOURCES.join(", ")}.`,
|
|
41592
|
+
"Actions, filters, and fields mirror the `productive` tool — call `productive` with action=\"help\"",
|
|
41593
|
+
"or action=\"schema\" for a resource to discover them. Use the low-level",
|
|
41594
|
+
"`productive(resource, action, params)` for actions without an accessor (e.g. delete, resolve,",
|
|
41595
|
+
"reports, summaries, workflows)."
|
|
41596
|
+
].join(" ")
|
|
41597
|
+
},
|
|
41598
|
+
{
|
|
41599
|
+
title: "dry run",
|
|
41600
|
+
summary: "Preview mutations without executing them (dry_run).",
|
|
41601
|
+
keywords: [
|
|
41602
|
+
"dry_run",
|
|
41603
|
+
"dry",
|
|
41604
|
+
"preview",
|
|
41605
|
+
"mutation",
|
|
41606
|
+
"safe"
|
|
41607
|
+
],
|
|
41608
|
+
body: "Pass `dry_run: true` to record mutating calls (create/update/delete/start/stop/...) instead of executing them; they are listed under `_run.recorded`."
|
|
41609
|
+
},
|
|
41610
|
+
{
|
|
41611
|
+
title: "limits & gating",
|
|
41612
|
+
summary: "Timeouts, memory, budgets, and the enable flag.",
|
|
41613
|
+
keywords: [
|
|
41614
|
+
"limit",
|
|
41615
|
+
"limits",
|
|
41616
|
+
"timeout",
|
|
41617
|
+
"memory",
|
|
41618
|
+
"budget",
|
|
41619
|
+
"gating",
|
|
41620
|
+
"enable",
|
|
41621
|
+
"disabled"
|
|
41622
|
+
],
|
|
41623
|
+
body: ["Disabled unless the server sets `PRODUCTIVE_MCP_ENABLE_RUN=true`.", "Operator-tunable per-run limits: wall-clock timeout, memory, API-call budget, output size, code size."].join(" ")
|
|
41624
|
+
},
|
|
41625
|
+
{
|
|
41626
|
+
title: "example",
|
|
41627
|
+
summary: "A complete example script.",
|
|
41628
|
+
keywords: [
|
|
41629
|
+
"example",
|
|
41630
|
+
"examples",
|
|
41631
|
+
"sample"
|
|
41632
|
+
],
|
|
41633
|
+
body: [
|
|
41634
|
+
"```js",
|
|
41635
|
+
"const projects = await productive.projects.list();",
|
|
41636
|
+
"const open = await productive.tasks.list({ status: \"open\" });",
|
|
41637
|
+
"output.json({ projects: projects, openTasks: open });",
|
|
41638
|
+
"return 'summary ready';",
|
|
41639
|
+
"```"
|
|
41640
|
+
].join("\n")
|
|
41641
|
+
}
|
|
41642
|
+
];
|
|
41643
|
+
/** Section titles, for the documentation table of contents. */
|
|
41644
|
+
function docSectionTitles() {
|
|
41645
|
+
return DOC_SECTIONS.map((section) => section.title);
|
|
41646
|
+
}
|
|
41647
|
+
/** Find scripting-doc sections matching a query (title, keywords, or body). */
|
|
41648
|
+
function findDocSections(query) {
|
|
41649
|
+
const q = query.trim().toLowerCase();
|
|
41650
|
+
if (q === "") return [];
|
|
41651
|
+
return DOC_SECTIONS.filter((section) => section.title.toLowerCase().includes(q) || section.keywords.some((k) => k.includes(q) || q.includes(k)) || section.body.toLowerCase().includes(q));
|
|
41652
|
+
}
|
|
41653
|
+
//#endregion
|
|
41654
|
+
//#region src/handlers/search-docs.ts
|
|
41655
|
+
/** Build the no-query table of contents across documentation domains. */
|
|
41656
|
+
function tableOfContents() {
|
|
41657
|
+
return jsonResult({
|
|
41658
|
+
message: "Documentation domains. Call search_docs with a query to search across all of them at once, or use the drill-in tool noted for each.",
|
|
41659
|
+
domains: [
|
|
41660
|
+
{
|
|
41661
|
+
domain: "resources",
|
|
41662
|
+
description: "Productive resources usable through the `productive` tool.",
|
|
41663
|
+
count: helpResourceNames().length,
|
|
41664
|
+
resources: helpResourceNames(),
|
|
41665
|
+
drill_in: "productive action=\"help\" resource=\"<name>\" (or action=\"help\" query=\"<term>\")"
|
|
41666
|
+
},
|
|
41667
|
+
{
|
|
41668
|
+
domain: "api_endpoints",
|
|
41669
|
+
description: "Documented raw API endpoints for the `api_read`/`api_write` tools.",
|
|
41670
|
+
count: apiEndpointCount(),
|
|
41671
|
+
drill_in: "api_read search=\"<term>\", then api_read describe=true path=\"<path>\""
|
|
41672
|
+
},
|
|
41673
|
+
{
|
|
41674
|
+
domain: "run_script",
|
|
41675
|
+
description: "Sandboxed scripting API (globals, output rendering, limits).",
|
|
41676
|
+
topics: docSectionTitles(),
|
|
41677
|
+
drill_in: "search_docs query=\"<topic>\" (returns the full scripting section)"
|
|
41678
|
+
}
|
|
41679
|
+
],
|
|
41680
|
+
_tip: "Example: search_docs query=\"invoices\" searches resources, endpoints, and scripting docs together."
|
|
41681
|
+
});
|
|
41682
|
+
}
|
|
41683
|
+
/**
|
|
41684
|
+
* Handle the `search_docs` tool: a table of contents with no query, or ranked
|
|
41685
|
+
* cross-domain matches with a query.
|
|
41686
|
+
*/
|
|
41687
|
+
function handleSearchDocs(query) {
|
|
41688
|
+
const q = typeof query === "string" ? query.trim() : "";
|
|
41689
|
+
if (q === "") return tableOfContents();
|
|
41690
|
+
const resources = searchResourceHelp(q);
|
|
41691
|
+
const endpoints = searchApiEndpoints(q);
|
|
41692
|
+
const scripting = findDocSections(q).map((s) => ({
|
|
41693
|
+
title: s.title,
|
|
41694
|
+
body: s.body
|
|
41695
|
+
}));
|
|
41696
|
+
const total = resources.length + endpoints.matches.length + scripting.length;
|
|
41697
|
+
return jsonResult({
|
|
41698
|
+
query: q,
|
|
41699
|
+
total,
|
|
41700
|
+
resources: {
|
|
41701
|
+
count: resources.length,
|
|
41702
|
+
matches: resources,
|
|
41703
|
+
drill_in: "productive action=\"help\" resource=\"<name>\""
|
|
41704
|
+
},
|
|
41705
|
+
api_endpoints: {
|
|
41706
|
+
count: endpoints.total,
|
|
41707
|
+
matches: endpoints.matches,
|
|
41708
|
+
...endpoints.truncated ? { truncated: true } : {},
|
|
41709
|
+
drill_in: "api_read describe=true path=\"<path>\""
|
|
41710
|
+
},
|
|
41711
|
+
run_script: {
|
|
41712
|
+
count: scripting.length,
|
|
41713
|
+
sections: scripting
|
|
41714
|
+
},
|
|
41715
|
+
_tip: total === 0 ? "No matches. Call search_docs without a query for a table of contents." : "For resources and endpoints, use the drill_in tool noted; run_script sections are returned in full."
|
|
41716
|
+
});
|
|
41717
|
+
}
|
|
41718
|
+
//#endregion
|
|
40764
41719
|
//#region src/handlers/search.ts
|
|
40765
41720
|
/**
|
|
40766
41721
|
* Resources that support the query filter for text search
|
|
@@ -41271,6 +42226,12 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
41271
42226
|
executor: () => execCtx
|
|
41272
42227
|
});
|
|
41273
42228
|
}
|
|
42229
|
+
if (name === "search_docs") return handleSearchDocs(typeof args.query === "string" ? args.query : void 0);
|
|
42230
|
+
if (name === "run_script") {
|
|
42231
|
+
const parsed = RunScriptToolInputSchema.safeParse(args);
|
|
42232
|
+
if (!parsed.success) return inputErrorResult(new UserInputError(formatValidationErrors(parsed.error)));
|
|
42233
|
+
return handleRunScript(parsed.data, credentials, executeToolWithCredentials);
|
|
42234
|
+
}
|
|
41274
42235
|
if (name !== "productive") return errorResult(`Unknown tool: ${name}`);
|
|
41275
42236
|
const typedArgs = args;
|
|
41276
42237
|
if (typedArgs.resource === "batch") return handleBatch(typedArgs.operations, credentials, executeToolWithCredentials);
|
|
@@ -41316,7 +42277,10 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
41316
42277
|
executor: () => execCtx
|
|
41317
42278
|
};
|
|
41318
42279
|
try {
|
|
41319
|
-
if (action === "help" && resource !== "summaries")
|
|
42280
|
+
if (action === "help" && resource !== "summaries") {
|
|
42281
|
+
if (query) return handleHelpSearch(query);
|
|
42282
|
+
return resource ? handleHelp(resource) : handleHelpOverview();
|
|
42283
|
+
}
|
|
41320
42284
|
if (action === "schema") return resource ? handleSchema(resource) : handleSchemaOverview();
|
|
41321
42285
|
return await routeToHandler(resource, action, restArgs, {
|
|
41322
42286
|
query,
|
|
@@ -41336,4 +42300,4 @@ async function executeToolWithCredentials(name, args, credentials) {
|
|
|
41336
42300
|
//#endregion
|
|
41337
42301
|
export { handleSchemaOverview as C, handleDeals as E, handleServices as S, handlePeople as T, union as _, boolean as a, handleTasks as b, intersection as c, number as d, object as f, string as g, record as h, array as i, literal as l, preprocess as m, _enum as n, custom as o, optional as p, _null as r, discriminatedUnion as s, executeToolWithCredentials as t, looseObject as u, unknown as v, handleProjects as w, handleSummaries as x, datetime as y };
|
|
41338
42302
|
|
|
41339
|
-
//# sourceMappingURL=handlers-
|
|
42303
|
+
//# sourceMappingURL=handlers-BEeOjytC.js.map
|