@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.
Files changed (53) hide show
  1. package/dist/handlers/api-read.d.ts +2 -1
  2. package/dist/handlers/api-read.d.ts.map +1 -1
  3. package/dist/handlers/api-utils.d.ts +20 -0
  4. package/dist/handlers/api-utils.d.ts.map +1 -1
  5. package/dist/handlers/help.d.ts +20 -0
  6. package/dist/handlers/help.d.ts.map +1 -1
  7. package/dist/handlers/index.d.ts.map +1 -1
  8. package/dist/handlers/run-endpoint.d.ts +27 -0
  9. package/dist/handlers/run-endpoint.d.ts.map +1 -0
  10. package/dist/handlers/run.d.ts +28 -0
  11. package/dist/handlers/run.d.ts.map +1 -0
  12. package/dist/handlers/search-docs.d.ts +22 -0
  13. package/dist/handlers/search-docs.d.ts.map +1 -0
  14. package/dist/{handlers-BE96O-uy.js → handlers-Ca2x4dM8.js} +1050 -11
  15. package/dist/handlers-Ca2x4dM8.js.map +1 -0
  16. package/dist/handlers.js +1 -1
  17. package/dist/{http-DF9K8Fr4.js → http-xNZOdN_t.js} +72 -4
  18. package/dist/http-xNZOdN_t.js.map +1 -0
  19. package/dist/http.d.ts.map +1 -1
  20. package/dist/http.js +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/oauth.js +1 -1
  23. package/dist/run/bridge.d.ts +53 -0
  24. package/dist/run/bridge.d.ts.map +1 -0
  25. package/dist/run/docs.d.ts +23 -0
  26. package/dist/run/docs.d.ts.map +1 -0
  27. package/dist/run/engine.d.ts +60 -0
  28. package/dist/run/engine.d.ts.map +1 -0
  29. package/dist/run/limits.d.ts +33 -0
  30. package/dist/run/limits.d.ts.map +1 -0
  31. package/dist/run/prelude.d.ts +27 -0
  32. package/dist/run/prelude.d.ts.map +1 -0
  33. package/dist/run/remote.d.ts +44 -0
  34. package/dist/run/remote.d.ts.map +1 -0
  35. package/dist/run/render.d.ts +30 -0
  36. package/dist/run/render.d.ts.map +1 -0
  37. package/dist/run/strip.d.ts +20 -0
  38. package/dist/run/strip.d.ts.map +1 -0
  39. package/dist/schema.d.ts +13 -2
  40. package/dist/schema.d.ts.map +1 -1
  41. package/dist/server.js +2 -2
  42. package/dist/{stdio-BnfO285Q.js → stdio-jqfOnE6-.js} +2 -2
  43. package/dist/{stdio-BnfO285Q.js.map → stdio-jqfOnE6-.js.map} +1 -1
  44. package/dist/stdio.js +1 -1
  45. package/dist/tools.d.ts.map +1 -1
  46. package/dist/tools.js +93 -4
  47. package/dist/tools.js.map +1 -1
  48. package/dist/{version-XQYsroYk.js → version-Jj_0Ypf8.js} +3 -3
  49. package/dist/{version-XQYsroYk.js.map → version-Jj_0Ypf8.js.map} +1 -1
  50. package/package.json +5 -3
  51. package/skills/SKILL.md +84 -1
  52. package/dist/handlers-BE96O-uy.js.map +0 -1
  53. 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") return resource ? handleHelp(resource) : handleHelpOverview();
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 { 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 };
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-BE96O-uy.js.map
42378
+ //# sourceMappingURL=handlers-Ca2x4dM8.js.map