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