@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.
- package/README.md +5 -5
- package/README.zh.md +5 -5
- package/dist/packages/extension-sdk/src/workflow.d.ts +17 -2
- package/dist/packages/extension-sdk/src/workflow.js +36 -0
- package/dist/src/modules/browser/BrowserPool.d.ts +49 -0
- package/dist/src/modules/browser/BrowserPool.js +288 -0
- package/dist/src/modules/deobfuscator/AdvancedDeobfuscator.d.ts +5 -0
- package/dist/src/modules/deobfuscator/AdvancedDeobfuscator.js +43 -2
- package/dist/src/modules/deobfuscator/Deobfuscator.js +5 -0
- package/dist/src/modules/external/ExternalToolRunner.js +1 -1
- package/dist/src/server/MCPServer.context.d.ts +1 -0
- package/dist/src/server/domains/browser/handlers/stealth-injection.d.ts +1 -0
- package/dist/src/server/domains/browser/handlers/stealth-injection.js +3 -0
- package/dist/src/server/domains/shared-state-board/definitions.d.ts +2 -0
- package/dist/src/server/domains/shared-state-board/definitions.js +78 -0
- package/dist/src/server/domains/shared-state-board/handlers.impl.d.ts +58 -0
- package/dist/src/server/domains/shared-state-board/handlers.impl.js +419 -0
- package/dist/src/server/domains/shared-state-board/index.d.ts +2 -0
- package/dist/src/server/domains/shared-state-board/index.js +2 -0
- package/dist/src/server/domains/shared-state-board/manifest.d.ts +57 -0
- package/dist/src/server/domains/shared-state-board/manifest.js +74 -0
- package/dist/src/server/http/SseStream.d.ts +21 -0
- package/dist/src/server/http/SseStream.js +129 -0
- package/dist/src/server/teams/TeamManager.d.ts +43 -0
- package/dist/src/server/teams/TeamManager.js +238 -0
- package/dist/src/server/teams/index.d.ts +1 -0
- package/dist/src/server/teams/index.js +1 -0
- package/dist/src/server/workflows/WorkflowContract.d.ts +20 -4
- package/dist/src/server/workflows/WorkflowContract.js +40 -0
- package/dist/src/server/workflows/WorkflowEngine.js +190 -13
- package/dist/src/types/deobfuscator.d.ts +1 -0
- package/dist/src/utils/cache/CachedDecorator.d.ts +8 -0
- package/dist/src/utils/cache/CachedDecorator.js +55 -0
- package/dist/src/utils/cache/PersistentCache.d.ts +33 -0
- package/dist/src/utils/cache/PersistentCache.js +246 -0
- package/dist/src/utils/cache/index.d.ts +2 -0
- package/dist/src/utils/cache/index.js +2 -0
- package/package.json +11 -12
- package/scripts/postinstall.cjs +54 -27
- package/workflows/anti-bot-diagnoser/.jshook-install.json +14 -0
- package/workflows/anti-bot-diagnoser/LICENSE +21 -0
- package/workflows/anti-bot-diagnoser/README.md +105 -0
- package/workflows/anti-bot-diagnoser/docs/agent-recipes.md +44 -0
- package/workflows/anti-bot-diagnoser/meta.yaml +6 -0
- package/workflows/anti-bot-diagnoser/package.json +22 -0
- package/workflows/anti-bot-diagnoser/tsconfig.json +15 -0
- package/workflows/anti-bot-diagnoser/workflow.ts +224 -0
- package/workflows/api-openapi-probe/.jshook-install.json +14 -0
- package/workflows/api-openapi-probe/meta.yaml +6 -0
- package/workflows/api-openapi-probe/package.json +22 -0
- package/workflows/api-openapi-probe/pnpm-lock.yaml +819 -0
- package/workflows/api-openapi-probe/tsconfig.json +15 -0
- package/workflows/api-openapi-probe/workflow.ts +40 -0
- package/workflows/api-probe-batch/.jshook-install.json +14 -0
- package/workflows/api-probe-batch/LICENSE +21 -0
- package/workflows/api-probe-batch/README.md +45 -0
- package/workflows/api-probe-batch/meta.yaml +4 -0
- package/workflows/api-probe-batch/package.json +23 -0
- package/workflows/api-probe-batch/tsconfig.json +16 -0
- package/workflows/api-probe-batch/workflow.ts +111 -0
- package/workflows/auth-bootstrap/.jshook-install.json +14 -0
- package/workflows/auth-bootstrap/LICENSE +21 -0
- package/workflows/auth-bootstrap/README.md +74 -0
- package/workflows/auth-bootstrap/meta.yaml +4 -0
- package/workflows/auth-bootstrap/package.json +23 -0
- package/workflows/auth-bootstrap/tsconfig.json +16 -0
- package/workflows/auth-bootstrap/workflow.ts +141 -0
- package/workflows/auth-extract/.jshook-install.json +14 -0
- package/workflows/auth-extract/meta.yaml +6 -0
- package/workflows/auth-extract/package.json +22 -0
- package/workflows/auth-extract/pnpm-lock.yaml +819 -0
- package/workflows/auth-extract/tsconfig.json +15 -0
- package/workflows/auth-extract/workflow.ts +36 -0
- package/workflows/auth-surface-mapper/.jshook-install.json +14 -0
- package/workflows/auth-surface-mapper/meta.yaml +6 -0
- package/workflows/auth-surface-mapper/package.json +22 -0
- package/workflows/auth-surface-mapper/pnpm-lock.yaml +819 -0
- package/workflows/auth-surface-mapper/tsconfig.json +15 -0
- package/workflows/auth-surface-mapper/workflow.ts +104 -0
- package/workflows/batch-register/.jshook-install.json +14 -0
- package/workflows/batch-register/LICENSE +21 -0
- package/workflows/batch-register/README.md +39 -0
- package/workflows/batch-register/meta.yaml +4 -0
- package/workflows/batch-register/package.json +23 -0
- package/workflows/batch-register/tsconfig.json +16 -0
- package/workflows/batch-register/workflow.ts +67 -0
- package/workflows/bundle-recovery/.jshook-install.json +14 -0
- package/workflows/bundle-recovery/LICENSE +21 -0
- package/workflows/bundle-recovery/README.md +105 -0
- package/workflows/bundle-recovery/docs/agent-recipes.md +44 -0
- package/workflows/bundle-recovery/meta.yaml +6 -0
- package/workflows/bundle-recovery/package.json +22 -0
- package/workflows/bundle-recovery/tsconfig.json +15 -0
- package/workflows/bundle-recovery/workflow.ts +179 -0
- package/workflows/challenge-detector/.jshook-install.json +14 -0
- package/workflows/challenge-detector/meta.yaml +14 -0
- package/workflows/challenge-detector/package.json +22 -0
- package/workflows/challenge-detector/pnpm-lock.yaml +819 -0
- package/workflows/challenge-detector/tsconfig.json +15 -0
- package/workflows/challenge-detector/workflow.ts +298 -0
- package/workflows/deobfuscation-pipeline/.jshook-install.json +14 -0
- package/workflows/deobfuscation-pipeline/meta.yaml +6 -0
- package/workflows/deobfuscation-pipeline/package.json +22 -0
- package/workflows/deobfuscation-pipeline/pnpm-lock.yaml +819 -0
- package/workflows/deobfuscation-pipeline/tsconfig.json +15 -0
- package/workflows/deobfuscation-pipeline/workflow.ts +119 -0
- package/workflows/electron-bridge-mapper/.jshook-install.json +14 -0
- package/workflows/electron-bridge-mapper/meta.yaml +6 -0
- package/workflows/electron-bridge-mapper/package.json +22 -0
- package/workflows/electron-bridge-mapper/pnpm-lock.yaml +819 -0
- package/workflows/electron-bridge-mapper/tsconfig.json +15 -0
- package/workflows/electron-bridge-mapper/workflow.ts +125 -0
- package/workflows/evidence-pack/.jshook-install.json +14 -0
- package/workflows/evidence-pack/LICENSE +21 -0
- package/workflows/evidence-pack/README.md +105 -0
- package/workflows/evidence-pack/docs/agent-recipes.md +44 -0
- package/workflows/evidence-pack/meta.yaml +6 -0
- package/workflows/evidence-pack/package.json +22 -0
- package/workflows/evidence-pack/tsconfig.json +15 -0
- package/workflows/evidence-pack/workflow.ts +154 -0
- package/workflows/js-bundle-search/.jshook-install.json +14 -0
- package/workflows/js-bundle-search/LICENSE +21 -0
- package/workflows/js-bundle-search/README.md +46 -0
- package/workflows/js-bundle-search/meta.yaml +4 -0
- package/workflows/js-bundle-search/package.json +23 -0
- package/workflows/js-bundle-search/tsconfig.json +16 -0
- package/workflows/js-bundle-search/workflow.ts +118 -0
- package/workflows/protocol-registry/.jshook-install.json +14 -0
- package/workflows/protocol-registry/meta.yaml +6 -0
- package/workflows/protocol-registry/package.json +22 -0
- package/workflows/protocol-registry/pnpm-lock.yaml +819 -0
- package/workflows/protocol-registry/tsconfig.json +15 -0
- package/workflows/protocol-registry/workflow.ts +107 -0
- package/workflows/qwen-mail-open-latest/meta.yaml +7 -0
- package/workflows/qwen-mail-open-latest/package.json +22 -0
- package/workflows/qwen-mail-open-latest/pnpm-lock.yaml +819 -0
- package/workflows/qwen-mail-open-latest/tsconfig.json +15 -0
- package/workflows/qwen-mail-open-latest/workflow.ts +77 -0
- package/workflows/register-account-flow/.jshook-install.json +14 -0
- package/workflows/register-account-flow/LICENSE +21 -0
- package/workflows/register-account-flow/README.md +64 -0
- package/workflows/register-account-flow/meta.yaml +4 -0
- package/workflows/register-account-flow/package.json +23 -0
- package/workflows/register-account-flow/tsconfig.json +16 -0
- package/workflows/register-account-flow/workflow.ts +127 -0
- package/workflows/replay-lab/.jshook-install.json +14 -0
- package/workflows/replay-lab/meta.yaml +6 -0
- package/workflows/replay-lab/package.json +22 -0
- package/workflows/replay-lab/pnpm-lock.yaml +819 -0
- package/workflows/replay-lab/tsconfig.json +15 -0
- package/workflows/replay-lab/workflow.ts +106 -0
- package/workflows/script-evidence-scan/.jshook-install.json +14 -0
- package/workflows/script-evidence-scan/LICENSE +21 -0
- package/workflows/script-evidence-scan/README.md +61 -0
- package/workflows/script-evidence-scan/meta.yaml +4 -0
- package/workflows/script-evidence-scan/package.json +23 -0
- package/workflows/script-evidence-scan/tsconfig.json +16 -0
- package/workflows/script-evidence-scan/workflow.ts +89 -0
- package/workflows/signature-hunter/.jshook-install.json +14 -0
- package/workflows/signature-hunter/LICENSE +21 -0
- package/workflows/signature-hunter/README.md +105 -0
- package/workflows/signature-hunter/docs/agent-recipes.md +44 -0
- package/workflows/signature-hunter/meta.yaml +6 -0
- package/workflows/signature-hunter/package.json +22 -0
- package/workflows/signature-hunter/tsconfig.json +15 -0
- package/workflows/signature-hunter/workflow.ts +170 -0
- package/workflows/signing-lineage/.jshook-install.json +14 -0
- package/workflows/signing-lineage/meta.yaml +6 -0
- package/workflows/signing-lineage/package.json +22 -0
- package/workflows/signing-lineage/pnpm-lock.yaml +819 -0
- package/workflows/signing-lineage/tsconfig.json +15 -0
- package/workflows/signing-lineage/workflow.ts +120 -0
- package/workflows/temp-mail-extract-link/.jshook-install.json +14 -0
- package/workflows/temp-mail-extract-link/LICENSE +21 -0
- package/workflows/temp-mail-extract-link/README.md +71 -0
- package/workflows/temp-mail-extract-link/meta.yaml +4 -0
- package/workflows/temp-mail-extract-link/package.json +23 -0
- package/workflows/temp-mail-extract-link/tsconfig.json +16 -0
- package/workflows/temp-mail-extract-link/workflow.ts +221 -0
- package/workflows/temp-mail-open-latest/.jshook-install.json +14 -0
- package/workflows/temp-mail-open-latest/LICENSE +21 -0
- package/workflows/temp-mail-open-latest/README.md +61 -0
- package/workflows/temp-mail-open-latest/meta.yaml +4 -0
- package/workflows/temp-mail-open-latest/package.json +23 -0
- package/workflows/temp-mail-open-latest/tsconfig.json +16 -0
- package/workflows/temp-mail-open-latest/workflow.ts +136 -0
- package/workflows/template/.jshook-install.json +14 -0
- package/workflows/template/LICENSE +21 -0
- package/workflows/template/README.md +45 -0
- package/workflows/template/docs/SKILL.md +111 -0
- package/workflows/template/meta.yaml +6 -0
- package/workflows/template/package.json +22 -0
- package/workflows/template/pnpm-lock.yaml +819 -0
- package/workflows/template/tsconfig.json +15 -0
- package/workflows/template/workflow.ts +73 -0
- package/workflows/web-api-capture-session/.jshook-install.json +14 -0
- package/workflows/web-api-capture-session/LICENSE +21 -0
- package/workflows/web-api-capture-session/README.md +64 -0
- package/workflows/web-api-capture-session/meta.yaml +4 -0
- package/workflows/web-api-capture-session/package.json +23 -0
- package/workflows/web-api-capture-session/tsconfig.json +16 -0
- package/workflows/web-api-capture-session/workflow.ts +124 -0
- package/workflows/ws-protocol-lifter/.jshook-install.json +14 -0
- package/workflows/ws-protocol-lifter/LICENSE +21 -0
- package/workflows/ws-protocol-lifter/README.md +105 -0
- package/workflows/ws-protocol-lifter/docs/agent-recipes.md +44 -0
- package/workflows/ws-protocol-lifter/meta.yaml +6 -0
- package/workflows/ws-protocol-lifter/package.json +22 -0
- package/workflows/ws-protocol-lifter/tsconfig.json +15 -0
- 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,
|
|
131
|
+
function resolveInputFrom(mapping, dataBus) {
|
|
91
132
|
const resolved = {};
|
|
92
133
|
for (const [targetKey, sourceRef] of Object.entries(mapping)) {
|
|
93
|
-
const
|
|
94
|
-
|
|
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.
|
|
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
|
-
...
|
|
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
|
},
|
|
@@ -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
|
+
}
|