@studiometa/productive-mcp 0.10.14 → 0.10.15

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