@jshookmcp/jshook 0.2.5 → 0.2.6

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 (210) hide show
  1. package/README.md +5 -5
  2. package/README.zh.md +5 -5
  3. package/dist/packages/extension-sdk/src/workflow.d.ts +17 -2
  4. package/dist/packages/extension-sdk/src/workflow.js +36 -0
  5. package/dist/src/modules/browser/BrowserPool.d.ts +49 -0
  6. package/dist/src/modules/browser/BrowserPool.js +288 -0
  7. package/dist/src/modules/deobfuscator/AdvancedDeobfuscator.d.ts +5 -0
  8. package/dist/src/modules/deobfuscator/AdvancedDeobfuscator.js +43 -2
  9. package/dist/src/modules/deobfuscator/Deobfuscator.js +5 -0
  10. package/dist/src/modules/external/ExternalToolRunner.js +1 -1
  11. package/dist/src/server/MCPServer.context.d.ts +1 -0
  12. package/dist/src/server/domains/browser/handlers/stealth-injection.d.ts +1 -0
  13. package/dist/src/server/domains/browser/handlers/stealth-injection.js +3 -0
  14. package/dist/src/server/domains/shared-state-board/definitions.d.ts +2 -0
  15. package/dist/src/server/domains/shared-state-board/definitions.js +78 -0
  16. package/dist/src/server/domains/shared-state-board/handlers.impl.d.ts +58 -0
  17. package/dist/src/server/domains/shared-state-board/handlers.impl.js +419 -0
  18. package/dist/src/server/domains/shared-state-board/index.d.ts +2 -0
  19. package/dist/src/server/domains/shared-state-board/index.js +2 -0
  20. package/dist/src/server/domains/shared-state-board/manifest.d.ts +57 -0
  21. package/dist/src/server/domains/shared-state-board/manifest.js +74 -0
  22. package/dist/src/server/http/SseStream.d.ts +21 -0
  23. package/dist/src/server/http/SseStream.js +129 -0
  24. package/dist/src/server/teams/TeamManager.d.ts +43 -0
  25. package/dist/src/server/teams/TeamManager.js +238 -0
  26. package/dist/src/server/teams/index.d.ts +1 -0
  27. package/dist/src/server/teams/index.js +1 -0
  28. package/dist/src/server/workflows/WorkflowContract.d.ts +20 -4
  29. package/dist/src/server/workflows/WorkflowContract.js +40 -0
  30. package/dist/src/server/workflows/WorkflowEngine.js +190 -13
  31. package/dist/src/types/deobfuscator.d.ts +1 -0
  32. package/dist/src/utils/cache/CachedDecorator.d.ts +8 -0
  33. package/dist/src/utils/cache/CachedDecorator.js +55 -0
  34. package/dist/src/utils/cache/PersistentCache.d.ts +33 -0
  35. package/dist/src/utils/cache/PersistentCache.js +246 -0
  36. package/dist/src/utils/cache/index.d.ts +2 -0
  37. package/dist/src/utils/cache/index.js +2 -0
  38. package/package.json +11 -12
  39. package/scripts/postinstall.cjs +54 -27
  40. package/workflows/anti-bot-diagnoser/.jshook-install.json +14 -0
  41. package/workflows/anti-bot-diagnoser/LICENSE +21 -0
  42. package/workflows/anti-bot-diagnoser/README.md +105 -0
  43. package/workflows/anti-bot-diagnoser/docs/agent-recipes.md +44 -0
  44. package/workflows/anti-bot-diagnoser/meta.yaml +6 -0
  45. package/workflows/anti-bot-diagnoser/package.json +22 -0
  46. package/workflows/anti-bot-diagnoser/tsconfig.json +15 -0
  47. package/workflows/anti-bot-diagnoser/workflow.ts +224 -0
  48. package/workflows/api-openapi-probe/.jshook-install.json +14 -0
  49. package/workflows/api-openapi-probe/meta.yaml +6 -0
  50. package/workflows/api-openapi-probe/package.json +22 -0
  51. package/workflows/api-openapi-probe/pnpm-lock.yaml +819 -0
  52. package/workflows/api-openapi-probe/tsconfig.json +15 -0
  53. package/workflows/api-openapi-probe/workflow.ts +40 -0
  54. package/workflows/api-probe-batch/.jshook-install.json +14 -0
  55. package/workflows/api-probe-batch/LICENSE +21 -0
  56. package/workflows/api-probe-batch/README.md +45 -0
  57. package/workflows/api-probe-batch/meta.yaml +4 -0
  58. package/workflows/api-probe-batch/package.json +23 -0
  59. package/workflows/api-probe-batch/tsconfig.json +16 -0
  60. package/workflows/api-probe-batch/workflow.ts +111 -0
  61. package/workflows/auth-bootstrap/.jshook-install.json +14 -0
  62. package/workflows/auth-bootstrap/LICENSE +21 -0
  63. package/workflows/auth-bootstrap/README.md +74 -0
  64. package/workflows/auth-bootstrap/meta.yaml +4 -0
  65. package/workflows/auth-bootstrap/package.json +23 -0
  66. package/workflows/auth-bootstrap/tsconfig.json +16 -0
  67. package/workflows/auth-bootstrap/workflow.ts +141 -0
  68. package/workflows/auth-extract/.jshook-install.json +14 -0
  69. package/workflows/auth-extract/meta.yaml +6 -0
  70. package/workflows/auth-extract/package.json +22 -0
  71. package/workflows/auth-extract/pnpm-lock.yaml +819 -0
  72. package/workflows/auth-extract/tsconfig.json +15 -0
  73. package/workflows/auth-extract/workflow.ts +36 -0
  74. package/workflows/auth-surface-mapper/.jshook-install.json +14 -0
  75. package/workflows/auth-surface-mapper/meta.yaml +6 -0
  76. package/workflows/auth-surface-mapper/package.json +22 -0
  77. package/workflows/auth-surface-mapper/pnpm-lock.yaml +819 -0
  78. package/workflows/auth-surface-mapper/tsconfig.json +15 -0
  79. package/workflows/auth-surface-mapper/workflow.ts +104 -0
  80. package/workflows/batch-register/.jshook-install.json +14 -0
  81. package/workflows/batch-register/LICENSE +21 -0
  82. package/workflows/batch-register/README.md +39 -0
  83. package/workflows/batch-register/meta.yaml +4 -0
  84. package/workflows/batch-register/package.json +23 -0
  85. package/workflows/batch-register/tsconfig.json +16 -0
  86. package/workflows/batch-register/workflow.ts +67 -0
  87. package/workflows/bundle-recovery/.jshook-install.json +14 -0
  88. package/workflows/bundle-recovery/LICENSE +21 -0
  89. package/workflows/bundle-recovery/README.md +105 -0
  90. package/workflows/bundle-recovery/docs/agent-recipes.md +44 -0
  91. package/workflows/bundle-recovery/meta.yaml +6 -0
  92. package/workflows/bundle-recovery/package.json +22 -0
  93. package/workflows/bundle-recovery/tsconfig.json +15 -0
  94. package/workflows/bundle-recovery/workflow.ts +179 -0
  95. package/workflows/challenge-detector/.jshook-install.json +14 -0
  96. package/workflows/challenge-detector/meta.yaml +14 -0
  97. package/workflows/challenge-detector/package.json +22 -0
  98. package/workflows/challenge-detector/pnpm-lock.yaml +819 -0
  99. package/workflows/challenge-detector/tsconfig.json +15 -0
  100. package/workflows/challenge-detector/workflow.ts +298 -0
  101. package/workflows/deobfuscation-pipeline/.jshook-install.json +14 -0
  102. package/workflows/deobfuscation-pipeline/meta.yaml +6 -0
  103. package/workflows/deobfuscation-pipeline/package.json +22 -0
  104. package/workflows/deobfuscation-pipeline/pnpm-lock.yaml +819 -0
  105. package/workflows/deobfuscation-pipeline/tsconfig.json +15 -0
  106. package/workflows/deobfuscation-pipeline/workflow.ts +119 -0
  107. package/workflows/electron-bridge-mapper/.jshook-install.json +14 -0
  108. package/workflows/electron-bridge-mapper/meta.yaml +6 -0
  109. package/workflows/electron-bridge-mapper/package.json +22 -0
  110. package/workflows/electron-bridge-mapper/pnpm-lock.yaml +819 -0
  111. package/workflows/electron-bridge-mapper/tsconfig.json +15 -0
  112. package/workflows/electron-bridge-mapper/workflow.ts +125 -0
  113. package/workflows/evidence-pack/.jshook-install.json +14 -0
  114. package/workflows/evidence-pack/LICENSE +21 -0
  115. package/workflows/evidence-pack/README.md +105 -0
  116. package/workflows/evidence-pack/docs/agent-recipes.md +44 -0
  117. package/workflows/evidence-pack/meta.yaml +6 -0
  118. package/workflows/evidence-pack/package.json +22 -0
  119. package/workflows/evidence-pack/tsconfig.json +15 -0
  120. package/workflows/evidence-pack/workflow.ts +154 -0
  121. package/workflows/js-bundle-search/.jshook-install.json +14 -0
  122. package/workflows/js-bundle-search/LICENSE +21 -0
  123. package/workflows/js-bundle-search/README.md +46 -0
  124. package/workflows/js-bundle-search/meta.yaml +4 -0
  125. package/workflows/js-bundle-search/package.json +23 -0
  126. package/workflows/js-bundle-search/tsconfig.json +16 -0
  127. package/workflows/js-bundle-search/workflow.ts +118 -0
  128. package/workflows/protocol-registry/.jshook-install.json +14 -0
  129. package/workflows/protocol-registry/meta.yaml +6 -0
  130. package/workflows/protocol-registry/package.json +22 -0
  131. package/workflows/protocol-registry/pnpm-lock.yaml +819 -0
  132. package/workflows/protocol-registry/tsconfig.json +15 -0
  133. package/workflows/protocol-registry/workflow.ts +107 -0
  134. package/workflows/qwen-mail-open-latest/meta.yaml +7 -0
  135. package/workflows/qwen-mail-open-latest/package.json +22 -0
  136. package/workflows/qwen-mail-open-latest/pnpm-lock.yaml +819 -0
  137. package/workflows/qwen-mail-open-latest/tsconfig.json +15 -0
  138. package/workflows/qwen-mail-open-latest/workflow.ts +77 -0
  139. package/workflows/register-account-flow/.jshook-install.json +14 -0
  140. package/workflows/register-account-flow/LICENSE +21 -0
  141. package/workflows/register-account-flow/README.md +64 -0
  142. package/workflows/register-account-flow/meta.yaml +4 -0
  143. package/workflows/register-account-flow/package.json +23 -0
  144. package/workflows/register-account-flow/tsconfig.json +16 -0
  145. package/workflows/register-account-flow/workflow.ts +127 -0
  146. package/workflows/replay-lab/.jshook-install.json +14 -0
  147. package/workflows/replay-lab/meta.yaml +6 -0
  148. package/workflows/replay-lab/package.json +22 -0
  149. package/workflows/replay-lab/pnpm-lock.yaml +819 -0
  150. package/workflows/replay-lab/tsconfig.json +15 -0
  151. package/workflows/replay-lab/workflow.ts +106 -0
  152. package/workflows/script-evidence-scan/.jshook-install.json +14 -0
  153. package/workflows/script-evidence-scan/LICENSE +21 -0
  154. package/workflows/script-evidence-scan/README.md +61 -0
  155. package/workflows/script-evidence-scan/meta.yaml +4 -0
  156. package/workflows/script-evidence-scan/package.json +23 -0
  157. package/workflows/script-evidence-scan/tsconfig.json +16 -0
  158. package/workflows/script-evidence-scan/workflow.ts +89 -0
  159. package/workflows/signature-hunter/.jshook-install.json +14 -0
  160. package/workflows/signature-hunter/LICENSE +21 -0
  161. package/workflows/signature-hunter/README.md +105 -0
  162. package/workflows/signature-hunter/docs/agent-recipes.md +44 -0
  163. package/workflows/signature-hunter/meta.yaml +6 -0
  164. package/workflows/signature-hunter/package.json +22 -0
  165. package/workflows/signature-hunter/tsconfig.json +15 -0
  166. package/workflows/signature-hunter/workflow.ts +170 -0
  167. package/workflows/signing-lineage/.jshook-install.json +14 -0
  168. package/workflows/signing-lineage/meta.yaml +6 -0
  169. package/workflows/signing-lineage/package.json +22 -0
  170. package/workflows/signing-lineage/pnpm-lock.yaml +819 -0
  171. package/workflows/signing-lineage/tsconfig.json +15 -0
  172. package/workflows/signing-lineage/workflow.ts +120 -0
  173. package/workflows/temp-mail-extract-link/.jshook-install.json +14 -0
  174. package/workflows/temp-mail-extract-link/LICENSE +21 -0
  175. package/workflows/temp-mail-extract-link/README.md +71 -0
  176. package/workflows/temp-mail-extract-link/meta.yaml +4 -0
  177. package/workflows/temp-mail-extract-link/package.json +23 -0
  178. package/workflows/temp-mail-extract-link/tsconfig.json +16 -0
  179. package/workflows/temp-mail-extract-link/workflow.ts +221 -0
  180. package/workflows/temp-mail-open-latest/.jshook-install.json +14 -0
  181. package/workflows/temp-mail-open-latest/LICENSE +21 -0
  182. package/workflows/temp-mail-open-latest/README.md +61 -0
  183. package/workflows/temp-mail-open-latest/meta.yaml +4 -0
  184. package/workflows/temp-mail-open-latest/package.json +23 -0
  185. package/workflows/temp-mail-open-latest/tsconfig.json +16 -0
  186. package/workflows/temp-mail-open-latest/workflow.ts +136 -0
  187. package/workflows/template/.jshook-install.json +14 -0
  188. package/workflows/template/LICENSE +21 -0
  189. package/workflows/template/README.md +45 -0
  190. package/workflows/template/docs/SKILL.md +111 -0
  191. package/workflows/template/meta.yaml +6 -0
  192. package/workflows/template/package.json +22 -0
  193. package/workflows/template/pnpm-lock.yaml +819 -0
  194. package/workflows/template/tsconfig.json +15 -0
  195. package/workflows/template/workflow.ts +73 -0
  196. package/workflows/web-api-capture-session/.jshook-install.json +14 -0
  197. package/workflows/web-api-capture-session/LICENSE +21 -0
  198. package/workflows/web-api-capture-session/README.md +64 -0
  199. package/workflows/web-api-capture-session/meta.yaml +4 -0
  200. package/workflows/web-api-capture-session/package.json +23 -0
  201. package/workflows/web-api-capture-session/tsconfig.json +16 -0
  202. package/workflows/web-api-capture-session/workflow.ts +124 -0
  203. package/workflows/ws-protocol-lifter/.jshook-install.json +14 -0
  204. package/workflows/ws-protocol-lifter/LICENSE +21 -0
  205. package/workflows/ws-protocol-lifter/README.md +105 -0
  206. package/workflows/ws-protocol-lifter/docs/agent-recipes.md +44 -0
  207. package/workflows/ws-protocol-lifter/meta.yaml +6 -0
  208. package/workflows/ws-protocol-lifter/package.json +22 -0
  209. package/workflows/ws-protocol-lifter/tsconfig.json +15 -0
  210. package/workflows/ws-protocol-lifter/workflow.ts +163 -0
@@ -1,6 +1,47 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { getEffectivePrerequisites } from '../ToolRouter.policy.js';
3
3
  import { getRoutingState } from '../ToolRouter.probe.js';
4
+ class WorkflowDataBus {
5
+ store = new Map();
6
+ set(key, value) {
7
+ this.store.set(key, value);
8
+ }
9
+ get(key) {
10
+ return this.store.get(key);
11
+ }
12
+ getValueAtPath(key, path) {
13
+ const value = this.store.get(key);
14
+ if (!value || typeof value !== 'object') {
15
+ return value;
16
+ }
17
+ const payload = parseToolPayload(value);
18
+ const obj = payload || value;
19
+ return path.split('.').reduce((current, segment) => {
20
+ if (current && typeof current === 'object') {
21
+ const arrayMatch = segment.match(/^(\d+)$/);
22
+ if (arrayMatch && Array.isArray(current)) {
23
+ return current[Number(arrayMatch[1])];
24
+ }
25
+ return current[segment];
26
+ }
27
+ return undefined;
28
+ }, obj);
29
+ }
30
+ resolve(template) {
31
+ const match = template.match(/^\$\{(.+)\}$/);
32
+ if (!match || !match[1]) {
33
+ return template;
34
+ }
35
+ const ref = match[1];
36
+ const dotIndex = ref.indexOf('.');
37
+ if (dotIndex === -1) {
38
+ return this.store.get(ref);
39
+ }
40
+ const stepId = ref.slice(0, dotIndex);
41
+ const fieldPath = ref.slice(dotIndex + 1);
42
+ return this.getValueAtPath(stepId, fieldPath);
43
+ }
44
+ }
4
45
  class PreflightError extends Error {
5
46
  warnings;
6
47
  constructor(warnings) {
@@ -87,28 +128,48 @@ function collectSuccessStats(value) {
87
128
  }
88
129
  return { success: 0, failure: 0 };
89
130
  }
90
- function resolveInputFrom(mapping, stepResults) {
131
+ function resolveInputFrom(mapping, dataBus) {
91
132
  const resolved = {};
92
133
  for (const [targetKey, sourceRef] of Object.entries(mapping)) {
93
- const dotIndex = sourceRef.indexOf('.');
94
- if (dotIndex === -1) {
95
- resolved[targetKey] = stepResults.get(sourceRef);
96
- continue;
97
- }
98
- const stepId = sourceRef.slice(0, dotIndex);
99
- const fieldPath = sourceRef.slice(dotIndex + 1);
100
- const stepResult = stepResults.get(stepId);
101
- const payload = parseToolPayload(stepResult) ?? stepResult;
102
- resolved[targetKey] = payload?.[fieldPath];
134
+ const template = sourceRef.startsWith('${') ? sourceRef : `\${${sourceRef}}`;
135
+ resolved[targetKey] = dataBus.resolve(template);
103
136
  }
104
137
  return resolved;
105
138
  }
139
+ function resolveInputValues(input, dataBus) {
140
+ if (!input)
141
+ return {};
142
+ const resolved = {};
143
+ for (const [key, value] of Object.entries(input)) {
144
+ resolved[key] = resolveValue(value, dataBus);
145
+ }
146
+ return resolved;
147
+ }
148
+ function resolveValue(value, dataBus) {
149
+ if (typeof value === 'string') {
150
+ return dataBus.resolve(value);
151
+ }
152
+ if (Array.isArray(value)) {
153
+ return value.map((item) => resolveValue(item, dataBus));
154
+ }
155
+ if (value && typeof value === 'object') {
156
+ const resolved = {};
157
+ for (const [k, v] of Object.entries(value)) {
158
+ resolved[k] = resolveValue(v, dataBus);
159
+ }
160
+ return resolved;
161
+ }
162
+ return value;
163
+ }
106
164
  async function runToolNode(ctx, node, overrides, executionContext) {
107
165
  const fromResolved = node.inputFrom
108
- ? resolveInputFrom(node.inputFrom, executionContext.stepResults)
166
+ ? resolveInputFrom(node.inputFrom, executionContext.dataBus)
167
+ : {};
168
+ const fromInputValues = node.input
169
+ ? resolveInputValues(node.input, executionContext.dataBus)
109
170
  : {};
110
171
  const mergedInput = {
111
- ...node.input,
172
+ ...fromInputValues,
112
173
  ...fromResolved,
113
174
  ...overrides?.[node.id],
114
175
  };
@@ -118,6 +179,7 @@ async function runToolNode(ctx, node, overrides, executionContext) {
118
179
  if (failure) {
119
180
  throw new Error(failure);
120
181
  }
182
+ executionContext.dataBus.set(node.id, response);
121
183
  return response;
122
184
  };
123
185
  const retry = node.retry;
@@ -175,6 +237,38 @@ async function runParallelNode(ctx, node, executionContext, options) {
175
237
  await Promise.all(Array.from({ length: Math.min(concurrency, node.steps.length) }, () => worker()));
176
238
  return results;
177
239
  }
240
+ function getWorkflowVariable(stepResults, keyPath) {
241
+ if (stepResults.has(keyPath)) {
242
+ return stepResults.get(keyPath);
243
+ }
244
+ const segments = keyPath.split('.');
245
+ const stepId = segments[0];
246
+ const fieldSegments = segments.slice(1);
247
+ if (!stepId || !stepResults.has(stepId)) {
248
+ return undefined;
249
+ }
250
+ let current = stepResults.get(stepId);
251
+ if (current && typeof current === 'object') {
252
+ const payload = parseToolPayload(current);
253
+ if (payload) {
254
+ current = payload;
255
+ }
256
+ }
257
+ for (const segment of fieldSegments) {
258
+ if (current && typeof current === 'object') {
259
+ const arrayMatch = segment.match(/^(\d+)$/);
260
+ if (arrayMatch && Array.isArray(current)) {
261
+ current = current[Number(arrayMatch[1])];
262
+ continue;
263
+ }
264
+ current = current[segment];
265
+ }
266
+ else {
267
+ return undefined;
268
+ }
269
+ }
270
+ return current;
271
+ }
178
272
  async function evaluatePredicate(node, ctx) {
179
273
  if (node.predicateFn) {
180
274
  return await node.predicateFn(ctx);
@@ -200,8 +294,66 @@ async function evaluatePredicate(node, ctx) {
200
294
  return false;
201
295
  return aggregate.success / total >= threshold / 100;
202
296
  }
297
+ const equalsMatch = node.predicateId.match(/^variable_equals_(.+?)_(.+)$/);
298
+ if (equalsMatch && equalsMatch[1] && equalsMatch[2]) {
299
+ const keyPath = equalsMatch[1];
300
+ const expectedValue = equalsMatch[2];
301
+ const actualValue = getWorkflowVariable(ctx.stepResults, keyPath);
302
+ return deepEquals(actualValue, expectedValue);
303
+ }
304
+ const containsMatch = node.predicateId.match(/^variable_contains_(.+?)_(.+)$/);
305
+ if (containsMatch && containsMatch[1] && containsMatch[2]) {
306
+ const keyPath = containsMatch[1];
307
+ const substring = containsMatch[2];
308
+ const value = getWorkflowVariable(ctx.stepResults, keyPath);
309
+ if (typeof value !== 'string' && !Array.isArray(value)) {
310
+ return false;
311
+ }
312
+ return String(value).includes(substring);
313
+ }
314
+ const matchesMatch = node.predicateId.match(/^variable_matches_(.+?)_(.+)$/);
315
+ if (matchesMatch && matchesMatch[1] && matchesMatch[2]) {
316
+ const keyPath = matchesMatch[1];
317
+ const pattern = matchesMatch[2];
318
+ const value = getWorkflowVariable(ctx.stepResults, keyPath);
319
+ if (typeof value !== 'string') {
320
+ return false;
321
+ }
322
+ try {
323
+ const regex = new RegExp(pattern);
324
+ return regex.test(value);
325
+ }
326
+ catch {
327
+ return false;
328
+ }
329
+ }
203
330
  throw new Error(`Unknown workflow predicateId "${node.predicateId}"`);
204
331
  }
332
+ function deepEquals(a, b) {
333
+ if (a === b) {
334
+ return true;
335
+ }
336
+ if (typeof a !== typeof b) {
337
+ return false;
338
+ }
339
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
340
+ if (Array.isArray(a) !== Array.isArray(b)) {
341
+ return false;
342
+ }
343
+ if (Array.isArray(a)) {
344
+ const arrA = a;
345
+ const arrB = b;
346
+ return arrA.length === arrB.length && arrA.every((v, i) => deepEquals(v, arrB[i]));
347
+ }
348
+ const keysA = Object.keys(a);
349
+ const keysB = Object.keys(b);
350
+ if (keysA.length !== keysB.length) {
351
+ return false;
352
+ }
353
+ return keysA.every((key) => deepEquals(a[key], b[key]));
354
+ }
355
+ return false;
356
+ }
205
357
  async function executeNode(ctx, node, executionContext, options) {
206
358
  executionContext.emitSpan('workflow.node.start', { nodeId: node.id, kind: node.kind });
207
359
  let result;
@@ -233,6 +385,22 @@ async function executeNode(ctx, node, executionContext, options) {
233
385
  }
234
386
  break;
235
387
  }
388
+ case 'fallback': {
389
+ const fallbackNode = node;
390
+ try {
391
+ result = await executeNode(ctx, fallbackNode.primary, executionContext, options);
392
+ }
393
+ catch (error) {
394
+ executionContext.emitSpan('workflow.node.fallback', {
395
+ nodeId: fallbackNode.id,
396
+ primaryNodeId: fallbackNode.primary.id,
397
+ fallbackNodeId: fallbackNode.fallback.id,
398
+ error: error instanceof Error ? error.message : String(error),
399
+ });
400
+ result = await executeNode(ctx, fallbackNode.fallback, executionContext, options);
401
+ }
402
+ break;
403
+ }
236
404
  default:
237
405
  throw new Error(`Unsupported workflow node kind: ${node.kind}`);
238
406
  }
@@ -252,6 +420,13 @@ function collectToolNodes(node) {
252
420
  ...collectToolNodes(node.whenTrue),
253
421
  ...(node.whenFalse ? collectToolNodes(node.whenFalse) : []),
254
422
  ];
423
+ case 'fallback': {
424
+ const fallbackNode = node;
425
+ return [
426
+ ...collectToolNodes(fallbackNode.primary),
427
+ ...collectToolNodes(fallbackNode.fallback),
428
+ ];
429
+ }
255
430
  default:
256
431
  return [];
257
432
  }
@@ -294,6 +469,7 @@ export async function executeExtensionWorkflow(ctx, workflow, options = {}) {
294
469
  const metrics = [];
295
470
  const spans = [];
296
471
  const stepResults = new Map();
472
+ const dataBus = new WorkflowDataBus();
297
473
  const mergedConfig = options.config
298
474
  ? { ...ctx.config, ...options.config }
299
475
  : ctx.config;
@@ -301,6 +477,7 @@ export async function executeExtensionWorkflow(ctx, workflow, options = {}) {
301
477
  workflowRunId: runId,
302
478
  profile,
303
479
  stepResults,
480
+ dataBus,
304
481
  invokeTool(toolName, args) {
305
482
  return ctx.executeToolWithTracking(toolName, args);
306
483
  },
@@ -60,4 +60,5 @@ export interface DeobfuscateResult {
60
60
  warnings?: string[];
61
61
  engine?: 'legacy' | 'webcrack' | 'hybrid';
62
62
  webcrackApplied?: boolean;
63
+ cached?: boolean;
63
64
  }
@@ -0,0 +1,8 @@
1
+ import { PersistentCache } from './PersistentCache.js';
2
+ export interface CachedOptions {
3
+ ttlMs?: number;
4
+ keyFn?: (...args: unknown[]) => string;
5
+ cache?: PersistentCache;
6
+ }
7
+ export declare function cached(options?: CachedOptions): (_target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => PropertyDescriptor;
8
+ export declare function withCache<T extends (...args: unknown[]) => Promise<unknown>>(fn: T, options?: CachedOptions): T;
@@ -0,0 +1,55 @@
1
+ import { PersistentCache } from './PersistentCache.js';
2
+ function generateDefaultKey(...args) {
3
+ try {
4
+ return JSON.stringify(args);
5
+ }
6
+ catch {
7
+ return args.map((arg) => String(arg)).join('|');
8
+ }
9
+ }
10
+ export function cached(options = {}) {
11
+ return function (_target, propertyKey, descriptor) {
12
+ const originalMethod = descriptor.value;
13
+ const cache = options.cache ??
14
+ new PersistentCache({
15
+ name: `cached-${String(propertyKey)}`,
16
+ dbPath: '.jshookmcp/cache.db',
17
+ });
18
+ descriptor.value = async function (...args) {
19
+ const keyGenerator = options.keyFn ?? generateDefaultKey;
20
+ const key = keyGenerator(...args);
21
+ if (!cache.isReady()) {
22
+ await cache.init();
23
+ }
24
+ const hasKey = await cache.has(key);
25
+ if (hasKey) {
26
+ return await cache.get(key);
27
+ }
28
+ const result = await originalMethod.apply(this, args);
29
+ await cache.set(key, result, options.ttlMs);
30
+ return result;
31
+ };
32
+ return descriptor;
33
+ };
34
+ }
35
+ export function withCache(fn, options = {}) {
36
+ const cache = options.cache ??
37
+ new PersistentCache({
38
+ name: `withCache-${fn.name}`,
39
+ dbPath: '.jshookmcp/cache.db',
40
+ });
41
+ const keyGenerator = options.keyFn ?? generateDefaultKey;
42
+ return async function (...args) {
43
+ const key = keyGenerator(...args);
44
+ if (!cache.isReady()) {
45
+ await cache.init();
46
+ }
47
+ const hasKey = await cache.has(key);
48
+ if (hasKey) {
49
+ return await cache.get(key);
50
+ }
51
+ const result = await fn.apply(this, args);
52
+ await cache.set(key, result, options.ttlMs);
53
+ return result;
54
+ };
55
+ }
@@ -0,0 +1,33 @@
1
+ export interface PersistentCacheOptions {
2
+ name?: string;
3
+ dbPath?: string;
4
+ defaultTTL?: number;
5
+ enabled?: boolean;
6
+ }
7
+ export declare class PersistentCache {
8
+ private db;
9
+ private options;
10
+ private initialized;
11
+ private stats;
12
+ constructor(options?: PersistentCacheOptions);
13
+ init(): Promise<void>;
14
+ private loadDatabase;
15
+ private resolveDbPath;
16
+ private createTables;
17
+ has(key: string): Promise<boolean>;
18
+ get<T>(key: string, ttlMs?: number): Promise<T | null>;
19
+ set<T>(key: string, value: T, ttlMs?: number): Promise<boolean>;
20
+ delete(key: string): Promise<boolean>;
21
+ clear(): Promise<void>;
22
+ cleanup(): Promise<number>;
23
+ getStats(): Promise<{
24
+ entries: number;
25
+ size: number;
26
+ hits: number;
27
+ misses: number;
28
+ hitRate: number;
29
+ ttl: number;
30
+ }>;
31
+ close(): Promise<void>;
32
+ isReady(): boolean;
33
+ }
@@ -0,0 +1,246 @@
1
+ import { join } from 'path';
2
+ import { mkdirSync, existsSync } from 'fs';
3
+ import { logger } from '../logger.js';
4
+ import { getProjectRoot } from '../outputPaths.js';
5
+ let Database;
6
+ try {
7
+ Database = require('better-sqlite3');
8
+ }
9
+ catch {
10
+ }
11
+ export class PersistentCache {
12
+ db = null;
13
+ options;
14
+ initialized = false;
15
+ stats = { hits: 0, misses: 0, sets: 0, deletes: 0 };
16
+ constructor(options = {}) {
17
+ this.options = {
18
+ name: options.name ?? 'default',
19
+ dbPath: options.dbPath ?? '.jshookmcp/cache.db',
20
+ defaultTTL: options.defaultTTL ?? 24 * 60 * 60 * 1000,
21
+ enabled: options.enabled ?? true,
22
+ };
23
+ }
24
+ async init() {
25
+ if (!this.options.enabled) {
26
+ logger.debug(`PersistentCache[${this.options.name}] is disabled`);
27
+ return;
28
+ }
29
+ if (this.initialized) {
30
+ return;
31
+ }
32
+ try {
33
+ const dbPath = this.resolveDbPath();
34
+ const dir = dbPath.substring(0, dbPath.lastIndexOf('/'));
35
+ if (dir && !existsSync(dir)) {
36
+ mkdirSync(dir, { recursive: true });
37
+ }
38
+ const DatabaseConstructor = await this.loadDatabase();
39
+ if (!DatabaseConstructor) {
40
+ logger.warn(`PersistentCache[${this.options.name}]: better-sqlite3 not available, using no-op cache`);
41
+ return;
42
+ }
43
+ this.db = new DatabaseConstructor(dbPath);
44
+ this.db.pragma('journal_mode = WAL');
45
+ this.db.pragma('synchronous = NORMAL');
46
+ this.createTables();
47
+ this.initialized = true;
48
+ logger.info(`PersistentCache[${this.options.name}] initialized at ${dbPath}`);
49
+ }
50
+ catch (error) {
51
+ logger.error(`Failed to initialize PersistentCache[${this.options.name}]:`, error);
52
+ this.db = null;
53
+ this.initialized = false;
54
+ }
55
+ }
56
+ async loadDatabase() {
57
+ if (Database) {
58
+ return Database;
59
+ }
60
+ try {
61
+ const mod = await import('better-sqlite3');
62
+ return (mod.default ?? mod);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ resolveDbPath() {
69
+ const { dbPath } = this.options;
70
+ if (dbPath.startsWith('/') || /^[A-Z]:\\/i.test(dbPath)) {
71
+ return dbPath;
72
+ }
73
+ const projectRoot = getProjectRoot();
74
+ return join(projectRoot, dbPath);
75
+ }
76
+ createTables() {
77
+ if (!this.db)
78
+ return;
79
+ this.db.exec(`
80
+ CREATE TABLE IF NOT EXISTS cache_entries (
81
+ key TEXT PRIMARY KEY,
82
+ value TEXT NOT NULL,
83
+ expiresAt INTEGER NOT NULL,
84
+ createdAt INTEGER NOT NULL,
85
+ accessCount INTEGER DEFAULT 0,
86
+ lastAccessedAt INTEGER DEFAULT 0
87
+ )
88
+ `);
89
+ this.db.exec(`
90
+ CREATE INDEX IF NOT EXISTS idx_expiresAt ON cache_entries(expiresAt)
91
+ `);
92
+ }
93
+ async has(key) {
94
+ if (!this.options.enabled || !this.db || !this.initialized) {
95
+ return false;
96
+ }
97
+ try {
98
+ const now = Date.now();
99
+ const stmt = this.db.prepare('SELECT COUNT(*) as count FROM cache_entries WHERE key = ? AND expiresAt > ?');
100
+ const result = stmt.get(key, now);
101
+ return (result?.count ?? 0) > 0;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ async get(key, ttlMs) {
108
+ if (!this.options.enabled || !this.db || !this.initialized) {
109
+ return null;
110
+ }
111
+ try {
112
+ const now = Date.now();
113
+ const stmt = this.db.prepare('SELECT * FROM cache_entries WHERE key = ? AND expiresAt > ?');
114
+ const entry = stmt.get(key, now);
115
+ if (!entry) {
116
+ this.stats.misses++;
117
+ return null;
118
+ }
119
+ if (ttlMs !== undefined) {
120
+ const remainingTTL = entry.expiresAt - now;
121
+ if (remainingTTL < ttlMs) {
122
+ this.stats.misses++;
123
+ return null;
124
+ }
125
+ }
126
+ this.db
127
+ .prepare('UPDATE cache_entries SET accessCount = accessCount + 1, lastAccessedAt = ? WHERE key = ?')
128
+ .run(now, key);
129
+ this.stats.hits++;
130
+ const wrapped = JSON.parse(entry.value);
131
+ return wrapped.data;
132
+ }
133
+ catch (error) {
134
+ logger.error(`PersistentCache[${this.options.name}] get error:`, error);
135
+ this.stats.misses++;
136
+ return null;
137
+ }
138
+ }
139
+ async set(key, value, ttlMs) {
140
+ if (!this.options.enabled || !this.db || !this.initialized) {
141
+ return false;
142
+ }
143
+ try {
144
+ const now = Date.now();
145
+ const ttl = ttlMs ?? this.options.defaultTTL;
146
+ const expiresAt = now + ttl;
147
+ const wrappedValue = { __cached: true, data: value };
148
+ const serialized = JSON.stringify(wrappedValue);
149
+ const stmt = this.db.prepare(`
150
+ INSERT OR REPLACE INTO cache_entries (key, value, expiresAt, createdAt, accessCount, lastAccessedAt)
151
+ VALUES (?, ?, ?, ?, 0, ?)
152
+ `);
153
+ stmt.run(key, serialized, expiresAt, now, now);
154
+ this.stats.sets++;
155
+ return true;
156
+ }
157
+ catch (error) {
158
+ logger.error(`PersistentCache[${this.options.name}] set error:`, error);
159
+ return false;
160
+ }
161
+ }
162
+ async delete(key) {
163
+ if (!this.options.enabled || !this.db || !this.initialized) {
164
+ return false;
165
+ }
166
+ try {
167
+ const stmt = this.db.prepare('DELETE FROM cache_entries WHERE key = ?');
168
+ const result = stmt.run(key);
169
+ if (result.changes > 0) {
170
+ this.stats.deletes++;
171
+ return true;
172
+ }
173
+ return false;
174
+ }
175
+ catch (error) {
176
+ logger.error(`PersistentCache[${this.options.name}] delete error:`, error);
177
+ return false;
178
+ }
179
+ }
180
+ async clear() {
181
+ if (!this.options.enabled || !this.db || !this.initialized) {
182
+ return;
183
+ }
184
+ try {
185
+ this.db.exec('DELETE FROM cache_entries');
186
+ }
187
+ catch (error) {
188
+ logger.error(`PersistentCache[${this.options.name}] clear error:`, error);
189
+ }
190
+ }
191
+ async cleanup() {
192
+ if (!this.options.enabled || !this.db || !this.initialized) {
193
+ return 0;
194
+ }
195
+ try {
196
+ const now = Date.now();
197
+ const stmt = this.db.prepare('DELETE FROM cache_entries WHERE expiresAt <= ?');
198
+ const result = stmt.run(now);
199
+ return result.changes;
200
+ }
201
+ catch (error) {
202
+ logger.error(`PersistentCache[${this.options.name}] cleanup error:`, error);
203
+ return 0;
204
+ }
205
+ }
206
+ async getStats() {
207
+ if (!this.options.enabled || !this.db || !this.initialized) {
208
+ return { entries: 0, size: 0, hits: 0, misses: 0, hitRate: 0, ttl: this.options.defaultTTL };
209
+ }
210
+ try {
211
+ const totalEntries = this.db.prepare('SELECT COUNT(*) as count FROM cache_entries').get();
212
+ const totalSize = this.db
213
+ .prepare('SELECT SUM(length(value)) as total FROM cache_entries')
214
+ .get();
215
+ const totalRequests = this.stats.hits + this.stats.misses;
216
+ const hitRate = totalRequests > 0 ? this.stats.hits / totalRequests : 0;
217
+ return {
218
+ entries: totalEntries?.count ?? 0,
219
+ size: totalSize?.total ?? 0,
220
+ hits: this.stats.hits,
221
+ misses: this.stats.misses,
222
+ hitRate,
223
+ ttl: this.options.defaultTTL,
224
+ };
225
+ }
226
+ catch (error) {
227
+ logger.error(`PersistentCache[${this.options.name}] getStats error:`, error);
228
+ return { entries: 0, size: 0, hits: 0, misses: 0, hitRate: 0, ttl: this.options.defaultTTL };
229
+ }
230
+ }
231
+ async close() {
232
+ if (!this.db)
233
+ return;
234
+ try {
235
+ this.db.close();
236
+ this.db = null;
237
+ this.initialized = false;
238
+ }
239
+ catch (error) {
240
+ logger.error(`PersistentCache[${this.options.name}] close error:`, error);
241
+ }
242
+ }
243
+ isReady() {
244
+ return this.options.enabled && this.initialized && this.db !== null;
245
+ }
246
+ }
@@ -0,0 +1,2 @@
1
+ export { PersistentCache, type PersistentCacheOptions } from './PersistentCache.js';
2
+ export { cached, withCache, type CachedOptions } from './CachedDecorator.js';
@@ -0,0 +1,2 @@
1
+ export { PersistentCache } from './PersistentCache.js';
2
+ export { cached, withCache } from './CachedDecorator.js';