sparkecoder 0.1.107 → 0.1.109

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 (111) hide show
  1. package/dist/agent/index.d.ts +3 -3
  2. package/dist/agent/index.js +14 -1
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +109 -12
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +2 -2
  7. package/dist/{index-D5l-DMGC.d.ts → index-Biy5JTop.d.ts} +96 -83
  8. package/dist/index.d.ts +5 -5
  9. package/dist/index.js +87 -10
  10. package/dist/index.js.map +1 -1
  11. package/dist/{schema-ecQSnCMz.d.ts → schema-CYSKJZ3m.d.ts} +3 -3
  12. package/dist/{search-DOzC4ojH.d.ts → search-CVVfuBPZ.d.ts} +4 -4
  13. package/dist/server/index.js +87 -10
  14. package/dist/server/index.js.map +1 -1
  15. package/dist/tools/index.d.ts +3 -3
  16. package/dist/tools/index.js +8 -0
  17. package/dist/tools/index.js.map +1 -1
  18. package/package.json +1 -1
  19. package/web/.next/BUILD_ID +1 -1
  20. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  21. package/web/.next/standalone/web/.next/build-manifest.json +2 -2
  22. package/web/.next/standalone/web/.next/prerender-manifest.json +3 -3
  23. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  24. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  25. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  26. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  31. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +1 -1
  32. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  33. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  34. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  35. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  37. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  38. package/web/.next/standalone/web/.next/server/app/agents.html +1 -1
  39. package/web/.next/standalone/web/.next/server/app/agents.rsc +1 -1
  40. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents/__PAGE__.segment.rsc +1 -1
  41. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p/agents.segment.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/agents.segments/!KG1haW4p.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/agents.segments/_full.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/agents.segments/_head.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/agents.segments/_index.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/agents.segments/_tree.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/docs/installation.html +2 -2
  48. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +1 -1
  49. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +1 -1
  50. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +1 -1
  51. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +1 -1
  52. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +1 -1
  53. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +1 -1
  54. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +1 -1
  56. package/web/.next/standalone/web/.next/server/app/docs/skills.html +2 -2
  57. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +1 -1
  59. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +1 -1
  60. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +1 -1
  62. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +1 -1
  63. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +1 -1
  64. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +1 -1
  65. package/web/.next/standalone/web/.next/server/app/docs/tools.html +2 -2
  66. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +1 -1
  67. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +1 -1
  68. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +1 -1
  69. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +1 -1
  70. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +1 -1
  71. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +1 -1
  72. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +1 -1
  73. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +1 -1
  74. package/web/.next/standalone/web/.next/server/app/docs.html +2 -2
  75. package/web/.next/standalone/web/.next/server/app/docs.rsc +1 -1
  76. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +1 -1
  77. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +1 -1
  78. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +1 -1
  79. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +1 -1
  80. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +1 -1
  81. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +1 -1
  82. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  83. package/web/.next/standalone/web/.next/server/app/index.rsc +1 -1
  84. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +1 -1
  85. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +1 -1
  86. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +1 -1
  87. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  88. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +1 -1
  89. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  90. package/web/.next/standalone/web/.next/server/app/settings.html +1 -1
  91. package/web/.next/standalone/web/.next/server/app/settings.rsc +1 -1
  92. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings/__PAGE__.segment.rsc +1 -1
  93. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p/settings.segment.rsc +1 -1
  94. package/web/.next/standalone/web/.next/server/app/settings.segments/!KG1haW4p.segment.rsc +1 -1
  95. package/web/.next/standalone/web/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  96. package/web/.next/standalone/web/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  97. package/web/.next/standalone/web/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  98. package/web/.next/standalone/web/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  99. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  100. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  101. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  102. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  103. /package/web/.next/standalone/web/.next/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_buildManifest.js +0 -0
  104. /package/web/.next/standalone/web/.next/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_clientMiddlewareManifest.json +0 -0
  105. /package/web/.next/standalone/web/.next/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_ssgManifest.js +0 -0
  106. /package/web/.next/standalone/web/.next/static/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_buildManifest.js +0 -0
  107. /package/web/.next/standalone/web/.next/static/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_clientMiddlewareManifest.json +0 -0
  108. /package/web/.next/standalone/web/.next/static/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_ssgManifest.js +0 -0
  109. /package/web/.next/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_buildManifest.js +0 -0
  110. /package/web/.next/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_clientMiddlewareManifest.json +0 -0
  111. /package/web/.next/static/{9B80CeRYeYvnsEougmuLs → 56Y89huUkFkd-4Ip1HcYY}/_ssgManifest.js +0 -0
@@ -85,7 +85,7 @@ declare const sessions: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
85
85
  tableName: "sessions";
86
86
  dataType: "string";
87
87
  columnType: "SQLiteText";
88
- data: "error" | "completed" | "active" | "waiting";
88
+ data: "completed" | "error" | "active" | "waiting";
89
89
  driverParam: string;
90
90
  notNull: true;
91
91
  hasDefault: true;
@@ -391,7 +391,7 @@ declare const toolExecutions: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
391
391
  tableName: "tool_executions";
392
392
  dataType: "string";
393
393
  columnType: "SQLiteText";
394
- data: "error" | "completed" | "pending" | "approved" | "rejected";
394
+ data: "completed" | "error" | "pending" | "approved" | "rejected";
395
395
  driverParam: string;
396
396
  notNull: true;
397
397
  hasDefault: true;
@@ -814,7 +814,7 @@ declare const terminals: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
814
814
  tableName: "terminals";
815
815
  dataType: "string";
816
816
  columnType: "SQLiteText";
817
- data: "error" | "running" | "stopped";
817
+ data: "running" | "error" | "stopped";
818
818
  driverParam: string;
819
819
  notNull: true;
820
820
  hasDefault: true;
@@ -16,11 +16,11 @@ interface BashToolOptions {
16
16
  declare function createBashTool(options: BashToolOptions): ai.Tool<{
17
17
  background: boolean;
18
18
  id?: string | undefined;
19
- command?: string | undefined;
20
19
  input?: string | undefined;
20
+ command?: string | undefined;
21
21
  kill?: boolean | undefined;
22
22
  tail?: number | undefined;
23
- key?: "y" | "Enter" | "Escape" | "Up" | "Down" | "Left" | "Right" | "Tab" | "C-c" | "C-d" | "n" | undefined;
23
+ key?: "Enter" | "Escape" | "Up" | "Down" | "Left" | "Right" | "Tab" | "C-c" | "C-d" | "y" | "n" | undefined;
24
24
  }, {
25
25
  success: boolean;
26
26
  id: string;
@@ -66,7 +66,7 @@ declare function createBashTool(options: BashToolOptions): ai.Tool<{
66
66
  id: string;
67
67
  output: string;
68
68
  exitCode: number;
69
- status: "error" | "completed" | "running" | "stopped";
69
+ status: "running" | "completed" | "error" | "stopped";
70
70
  message?: undefined;
71
71
  error?: undefined;
72
72
  } | {
@@ -218,8 +218,8 @@ interface SearchToolOptions {
218
218
  * Progress is streamed back to the UI so users can see exploration happening.
219
219
  */
220
220
  declare function createSearchTool(options: SearchToolOptions): ai.Tool<{
221
- context: string;
222
221
  query: string;
222
+ context: string;
223
223
  }, {
224
224
  success: boolean;
225
225
  error: string;
@@ -853,6 +853,14 @@ var init_types = __esm({
853
853
  // If not set, defaults to http://{host}:{port}
854
854
  publicUrl: z.string().url().optional()
855
855
  }).default({ port: 3141, host: "127.0.0.1" }),
856
+ // Per-tunnel unguessable webhook URL prefix. Set by
857
+ // `cloudflared-setup --remote` from the response of POST
858
+ // /tunnels/ensure. When present, the local server mounts public
859
+ // inbound webhooks (Slack events, inbox) under /w/<token>/...
860
+ // instead of /api/*. When absent, falls back to /api/*.
861
+ webhooks: z.object({
862
+ token: z.string().optional()
863
+ }).optional(),
856
864
  // Database path (used for local SQLite - ignored if remoteServer is configured)
857
865
  databasePath: z.string().optional().default("./sparkecoder.db"),
858
866
  // Remote server configuration (for centralized storage)
@@ -960,7 +968,9 @@ __export(config_exports, {
960
968
  saveAuthKey: () => saveAuthKey,
961
969
  setApiKey: () => setApiKey,
962
970
  setMcpServers: () => setMcpServers,
963
- setSlackConfig: () => setSlackConfig
971
+ setPublicUrl: () => setPublicUrl,
972
+ setSlackConfig: () => setSlackConfig,
973
+ setWebhookToken: () => setWebhookToken
964
974
  });
965
975
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
966
976
  import { resolve, dirname, join } from "path";
@@ -1222,6 +1232,55 @@ function setMcpServers(servers2) {
1222
1232
  console.warn("[config] failed to persist mcp config:", err?.message || err);
1223
1233
  }
1224
1234
  }
1235
+ function setWebhookToken(token) {
1236
+ if (cachedConfig) {
1237
+ cachedConfig.webhooks = {
1238
+ ...cachedConfig.webhooks ?? {},
1239
+ token
1240
+ };
1241
+ }
1242
+ try {
1243
+ const cwdPath = resolve(process.cwd(), "sparkecoder.config.json");
1244
+ const target = existsSync(cwdPath) ? cwdPath : join(ensureAppDataDirectory(), "sparkecoder.config.json");
1245
+ let raw = {};
1246
+ if (existsSync(target)) {
1247
+ try {
1248
+ raw = JSON.parse(readFileSync(target, "utf-8"));
1249
+ } catch {
1250
+ raw = {};
1251
+ }
1252
+ } else {
1253
+ raw = createDefaultConfig();
1254
+ }
1255
+ raw.webhooks = { ...raw.webhooks || {}, token };
1256
+ writeFileSync(target, JSON.stringify(raw, null, 2));
1257
+ } catch (err) {
1258
+ console.warn("[config] failed to persist webhook token:", err?.message || err);
1259
+ }
1260
+ }
1261
+ function setPublicUrl(publicUrl) {
1262
+ if (cachedConfig) {
1263
+ cachedConfig.server = { ...cachedConfig.server ?? {}, publicUrl };
1264
+ }
1265
+ try {
1266
+ const cwdPath = resolve(process.cwd(), "sparkecoder.config.json");
1267
+ const target = existsSync(cwdPath) ? cwdPath : join(ensureAppDataDirectory(), "sparkecoder.config.json");
1268
+ let raw = {};
1269
+ if (existsSync(target)) {
1270
+ try {
1271
+ raw = JSON.parse(readFileSync(target, "utf-8"));
1272
+ } catch {
1273
+ raw = {};
1274
+ }
1275
+ } else {
1276
+ raw = createDefaultConfig();
1277
+ }
1278
+ raw.server = { ...raw.server || {}, publicUrl };
1279
+ writeFileSync(target, JSON.stringify(raw, null, 2));
1280
+ } catch (err) {
1281
+ console.warn("[config] failed to persist publicUrl:", err?.message || err);
1282
+ }
1283
+ }
1225
1284
  function clearSlackConfig() {
1226
1285
  if (cachedConfig) cachedConfig.slack = {};
1227
1286
  try {
@@ -8802,7 +8861,9 @@ function buildScheduleTool(opts) {
8802
8861
  }
8803
8862
  function buildWebhookUrl(opts, token) {
8804
8863
  const base = opts.publicBaseUrl?.replace(/\/$/, "") || opts.baseUrl.replace(/\/$/, "");
8805
- return `${base}/api/inbox/${token}`;
8864
+ const cfg = getConfig();
8865
+ const webhookPrefix2 = cfg?.webhooks?.token ? `/w/${cfg.webhooks.token}` : "/api";
8866
+ return `${base}${webhookPrefix2}/inbox/${token}`;
8806
8867
  }
8807
8868
  function buildWebhookTool(opts) {
8808
8869
  return tool14({
@@ -8852,6 +8913,7 @@ var init_orchestrator_actions = __esm({
8852
8913
  "use strict";
8853
8914
  init_messenger();
8854
8915
  init_schedules_store();
8916
+ init_config();
8855
8917
  init_webhooks_store();
8856
8918
  AGENT_STATUS_ENUM = z15.enum(["running", "needs_attention", "completed", "failed", "idle"]);
8857
8919
  agentInputSchema = z15.object({
@@ -13815,8 +13877,8 @@ async function findOrCreateOrchestratorId() {
13815
13877
  const all = await sessionQueries.list(500, 0);
13816
13878
  const existing = all.find((s) => s.config?.role === "orchestrator");
13817
13879
  if (existing) return existing.id;
13818
- const { getConfig: getConfig3 } = await Promise.resolve().then(() => (init_config(), config_exports));
13819
- const cfg = getConfig3();
13880
+ const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
13881
+ const cfg = getConfig2();
13820
13882
  const created = await sessionQueries.create({
13821
13883
  name: getDefaultOrchestratorName() || "Orchestrator",
13822
13884
  workingDirectory: cfg.resolvedWorkingDirectory,
@@ -13941,6 +14003,11 @@ function publicBase() {
13941
14003
  const cfg = getConfig();
13942
14004
  return cfg.server?.publicUrl?.replace(/\/$/, "") || `http://${cfg.server?.host || "127.0.0.1"}:${cfg.server?.port || 3141}`;
13943
14005
  }
14006
+ function webhookPrefix() {
14007
+ const cfg = getConfig();
14008
+ const token = cfg?.webhooks?.token;
14009
+ return token ? `/w/${token}` : "/api";
14010
+ }
13944
14011
  integrations.get("/", async (c) => {
13945
14012
  const cfg = getConfig();
13946
14013
  return c.json({
@@ -13950,7 +14017,7 @@ integrations.get("/", async (c) => {
13950
14017
  botTokenSet: !!cfg?.slack?.botToken,
13951
14018
  signingSecretSet: !!cfg?.slack?.signingSecret,
13952
14019
  defaultOrchestratorName: cfg?.slack?.defaultOrchestratorName || null,
13953
- eventsUrl: `${publicBase()}/api/slack/events`,
14020
+ eventsUrl: `${publicBase()}${webhookPrefix()}/slack/events`,
13954
14021
  allowedUsers: cfg?.slack?.allowedUsers ?? [],
13955
14022
  allowedChannels: cfg?.slack?.allowedChannels ?? [],
13956
14023
  allowDmsFromAnyone: cfg?.slack?.allowDmsFromAnyone !== false,
@@ -14043,7 +14110,7 @@ schedulesRouter.delete("/:id", async (c) => {
14043
14110
  });
14044
14111
  var webhooksRouter = new Hono8();
14045
14112
  function withUrl(row) {
14046
- return { ...row, url: `${publicBase()}/api/inbox/${row.token}` };
14113
+ return { ...row, url: `${publicBase()}${webhookPrefix()}/inbox/${row.token}` };
14047
14114
  }
14048
14115
  webhooksRouter.get("/", async (c) => {
14049
14116
  const orcId = await getOrchestratorId();
@@ -14146,7 +14213,7 @@ mcpRouter.post("/:id/test", async (c) => {
14146
14213
  // src/server/auth/cf-access.ts
14147
14214
  init_config();
14148
14215
  import { createRemoteJWKSet, jwtVerify } from "jose";
14149
- var EXEMPT_PATH_PREFIXES = ["/health", "/api/slack/events", "/api/inbox/"];
14216
+ var EXEMPT_PATH_PREFIXES = ["/health", "/api/slack/events", "/api/inbox/", "/w/"];
14150
14217
  var cachedJWKS = null;
14151
14218
  var cachedJWKSUrl = null;
14152
14219
  function getOrCreateJWKS(teamDomain) {
@@ -14575,13 +14642,23 @@ async function createApp(options = {}) {
14575
14642
  app.route("/sessions", terminals);
14576
14643
  app.route("/terminals", terminals);
14577
14644
  app.route("/tasks", tasks_default);
14578
- app.route("/api/slack", slack);
14579
- app.route("/api/inbox", inbox);
14580
14645
  app.route("/api/integrations", integrations);
14581
14646
  app.route("/api/schedules", schedulesRouter);
14582
- app.route("/api/webhooks", webhooksRouter);
14583
14647
  app.route("/api/orchestrator", orchestratorRouter);
14584
14648
  app.route("/api/mcp", mcpRouter);
14649
+ app.route("/api/webhooks", webhooksRouter);
14650
+ const config = getConfig();
14651
+ const webhookToken = config?.webhooks?.token;
14652
+ if (webhookToken) {
14653
+ app.route(`/w/${webhookToken}/slack`, slack);
14654
+ app.route(`/w/${webhookToken}/inbox`, inbox);
14655
+ if (!options.quiet) {
14656
+ console.log(` \u2192 Public webhooks mounted at /w/${webhookToken.slice(0, 4)}\u2026/<service>`);
14657
+ }
14658
+ } else {
14659
+ app.route("/api/slack", slack);
14660
+ app.route("/api/inbox", inbox);
14661
+ }
14585
14662
  app.get("/openapi.json", async (c) => {
14586
14663
  return c.json(generateOpenAPISpec());
14587
14664
  });