@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ServerResponse, IncomingMessage } from 'node:http';
|
|
2
|
+
import type { EventBus, ServerEventMap } from '../EventBus.js';
|
|
3
|
+
export interface SseStreamOptions {
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
heartbeatMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class SseStream {
|
|
8
|
+
private readonly eventBus;
|
|
9
|
+
private readonly options;
|
|
10
|
+
private res;
|
|
11
|
+
private heartbeatTimer;
|
|
12
|
+
private unsubscribe;
|
|
13
|
+
private closed;
|
|
14
|
+
constructor(eventBus: EventBus<ServerEventMap>, options?: SseStreamOptions);
|
|
15
|
+
start(res: ServerResponse): void;
|
|
16
|
+
sendEvent(event: string, data: unknown): void;
|
|
17
|
+
sendRaw(message: string): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
private startHeartbeat;
|
|
20
|
+
}
|
|
21
|
+
export declare function createProgressHandler(eventBus: EventBus<ServerEventMap>): (req: IncomingMessage, res: ServerResponse, sessionId?: string) => void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { logger } from '../../utils/logger.js';
|
|
2
|
+
export class SseStream {
|
|
3
|
+
eventBus;
|
|
4
|
+
options;
|
|
5
|
+
res = null;
|
|
6
|
+
heartbeatTimer = null;
|
|
7
|
+
unsubscribe = null;
|
|
8
|
+
closed = false;
|
|
9
|
+
constructor(eventBus, options = {}) {
|
|
10
|
+
this.eventBus = eventBus;
|
|
11
|
+
this.options = {
|
|
12
|
+
heartbeatMs: options.heartbeatMs ?? 30_000,
|
|
13
|
+
sessionId: options.sessionId,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
start(res) {
|
|
17
|
+
if (this.res) {
|
|
18
|
+
logger.warn('SseStream already started');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.res = res;
|
|
22
|
+
this.closed = false;
|
|
23
|
+
res.writeHead(200, {
|
|
24
|
+
'Content-Type': 'text/event-stream',
|
|
25
|
+
'Cache-Control': 'no-cache',
|
|
26
|
+
Connection: 'keep-alive',
|
|
27
|
+
'X-Accel-Buffering': 'no',
|
|
28
|
+
});
|
|
29
|
+
this.sendEvent('connected', {
|
|
30
|
+
timestamp: new Date().toISOString(),
|
|
31
|
+
sessionId: this.options.sessionId,
|
|
32
|
+
});
|
|
33
|
+
this.unsubscribe = this.eventBus.on('task:update', (payload) => {
|
|
34
|
+
const p = payload;
|
|
35
|
+
if (this.options.sessionId && p.sessionId !== this.options.sessionId) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.sendEvent('task:update', payload);
|
|
39
|
+
});
|
|
40
|
+
this.startHeartbeat();
|
|
41
|
+
res.on('close', () => {
|
|
42
|
+
this.close();
|
|
43
|
+
});
|
|
44
|
+
res.on('error', (err) => {
|
|
45
|
+
logger.warn('SSE stream error:', err);
|
|
46
|
+
this.close();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
sendEvent(event, data) {
|
|
50
|
+
if (!this.res || this.closed) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const eventData = JSON.stringify(data);
|
|
54
|
+
const lines = `event: ${event}\ndata: ${eventData}\n\n`;
|
|
55
|
+
try {
|
|
56
|
+
this.res.write(lines);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
logger.warn('Failed to write SSE event:', err);
|
|
60
|
+
this.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
sendRaw(message) {
|
|
64
|
+
if (!this.res || this.closed) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
this.res.write(`${message}\n\n`);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
logger.warn('Failed to write SSE message:', err);
|
|
72
|
+
this.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
close() {
|
|
76
|
+
if (this.closed) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.closed = true;
|
|
80
|
+
if (this.heartbeatTimer) {
|
|
81
|
+
clearInterval(this.heartbeatTimer);
|
|
82
|
+
this.heartbeatTimer = null;
|
|
83
|
+
}
|
|
84
|
+
if (this.unsubscribe) {
|
|
85
|
+
this.unsubscribe();
|
|
86
|
+
this.unsubscribe = null;
|
|
87
|
+
}
|
|
88
|
+
if (this.res && !this.res.writableEnded) {
|
|
89
|
+
try {
|
|
90
|
+
this.res.end();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.res = null;
|
|
96
|
+
logger.debug('SSE stream closed');
|
|
97
|
+
}
|
|
98
|
+
startHeartbeat() {
|
|
99
|
+
this.heartbeatTimer = setInterval(() => {
|
|
100
|
+
if (!this.closed && this.res) {
|
|
101
|
+
try {
|
|
102
|
+
this.res.write(`: heartbeat\n\n`);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
this.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}, this.options.heartbeatMs);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function createProgressHandler(eventBus) {
|
|
112
|
+
return (req, res, sessionId) => {
|
|
113
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
114
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
115
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
116
|
+
if (req.method === 'OPTIONS') {
|
|
117
|
+
res.writeHead(204);
|
|
118
|
+
res.end();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (req.method !== 'GET') {
|
|
122
|
+
res.writeHead(405, { 'Content-Type': 'text/plain' });
|
|
123
|
+
res.end('Method Not Allowed');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const stream = new SseStream(eventBus, { sessionId });
|
|
127
|
+
stream.start(res);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
export interface TeamSession {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
lastActivityAt: number;
|
|
7
|
+
status: 'active' | 'closing' | 'closed';
|
|
8
|
+
sessionIds: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface ForceDeleteOptions {
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
skipSessionCleanup?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ForceDeleteResult {
|
|
15
|
+
success: boolean;
|
|
16
|
+
teamName: string;
|
|
17
|
+
sessionsClosed: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class TeamManager extends EventEmitter {
|
|
21
|
+
private readonly teams;
|
|
22
|
+
private readonly cleanupTimeouts;
|
|
23
|
+
registerTeam(name: string, sessionId?: string): TeamSession;
|
|
24
|
+
addSessionToTeam(teamName: string, sessionId: string): void;
|
|
25
|
+
removeSessionFromTeam(teamName: string, sessionId: string): void;
|
|
26
|
+
getTeam(teamName: string): TeamSession | undefined;
|
|
27
|
+
listTeams(): TeamSession[];
|
|
28
|
+
getStats(): {
|
|
29
|
+
totalTeams: number;
|
|
30
|
+
activeTeams: number;
|
|
31
|
+
totalSessions: number;
|
|
32
|
+
};
|
|
33
|
+
forceDeleteTeam(teamName: string, options?: ForceDeleteOptions): Promise<ForceDeleteResult>;
|
|
34
|
+
private closeSession;
|
|
35
|
+
scheduleAutoCleanup(teamName: string, delayMs?: number): void;
|
|
36
|
+
cancelScheduledCleanup(teamName: string): void;
|
|
37
|
+
shutdown(timeoutMs?: number): Promise<{
|
|
38
|
+
closed: number;
|
|
39
|
+
failed: number;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
export declare function getTeamManager(): TeamManager;
|
|
43
|
+
export declare function resetTeamManager(): void;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
4
|
+
function validateTeamName(name) {
|
|
5
|
+
if (!name || typeof name !== 'string') {
|
|
6
|
+
return { valid: false, error: 'Team name must be a non-empty string' };
|
|
7
|
+
}
|
|
8
|
+
if (name.length > 64) {
|
|
9
|
+
return { valid: false, error: 'Team name must not exceed 64 characters' };
|
|
10
|
+
}
|
|
11
|
+
const safePattern = /^[a-zA-Z0-9._-]+$/;
|
|
12
|
+
if (!safePattern.test(name)) {
|
|
13
|
+
return {
|
|
14
|
+
valid: false,
|
|
15
|
+
error: 'Team name can only contain letters, numbers, dots, underscores, and dashes',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
19
|
+
return { valid: false, error: 'Path traversal detected in team name' };
|
|
20
|
+
}
|
|
21
|
+
return { valid: true };
|
|
22
|
+
}
|
|
23
|
+
async function execCodexCommand(args, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const child = spawn('codex', args, {
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
shell: true,
|
|
28
|
+
});
|
|
29
|
+
let stdout = '';
|
|
30
|
+
let stderr = '';
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
const timeoutId = setTimeout(() => {
|
|
33
|
+
timedOut = true;
|
|
34
|
+
child.kill('SIGKILL');
|
|
35
|
+
reject(new Error(`Codex command timed out after ${timeoutMs}ms: codex ${args.join(' ')}`));
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
child.stdout.on('data', (data) => {
|
|
38
|
+
stdout += data.toString();
|
|
39
|
+
});
|
|
40
|
+
child.stderr.on('data', (data) => {
|
|
41
|
+
stderr += data.toString();
|
|
42
|
+
});
|
|
43
|
+
child.on('close', (code) => {
|
|
44
|
+
if (!timedOut) {
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
child.on('error', (err) => {
|
|
50
|
+
if (!timedOut) {
|
|
51
|
+
clearTimeout(timeoutId);
|
|
52
|
+
reject(err);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export class TeamManager extends EventEmitter {
|
|
58
|
+
teams = new Map();
|
|
59
|
+
cleanupTimeouts = new Map();
|
|
60
|
+
registerTeam(name, sessionId) {
|
|
61
|
+
const validation = validateTeamName(name);
|
|
62
|
+
if (!validation.valid) {
|
|
63
|
+
throw new Error(`Invalid team name: ${validation.error}`);
|
|
64
|
+
}
|
|
65
|
+
const existing = this.teams.get(name);
|
|
66
|
+
if (existing) {
|
|
67
|
+
return existing;
|
|
68
|
+
}
|
|
69
|
+
const team = {
|
|
70
|
+
id: name,
|
|
71
|
+
name,
|
|
72
|
+
createdAt: Date.now(),
|
|
73
|
+
lastActivityAt: Date.now(),
|
|
74
|
+
status: 'active',
|
|
75
|
+
sessionIds: sessionId ? [sessionId] : [],
|
|
76
|
+
};
|
|
77
|
+
this.teams.set(name, team);
|
|
78
|
+
this.emit('team:registered', { name, sessionId });
|
|
79
|
+
return team;
|
|
80
|
+
}
|
|
81
|
+
addSessionToTeam(teamName, sessionId) {
|
|
82
|
+
const team = this.teams.get(teamName);
|
|
83
|
+
if (!team) {
|
|
84
|
+
throw new Error(`Team "${teamName}" not found`);
|
|
85
|
+
}
|
|
86
|
+
if (!team.sessionIds.includes(sessionId)) {
|
|
87
|
+
team.sessionIds.push(sessionId);
|
|
88
|
+
team.lastActivityAt = Date.now();
|
|
89
|
+
this.emit('session:added', { teamName, sessionId });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
removeSessionFromTeam(teamName, sessionId) {
|
|
93
|
+
const team = this.teams.get(teamName);
|
|
94
|
+
if (!team) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const index = team.sessionIds.indexOf(sessionId);
|
|
98
|
+
if (index !== -1) {
|
|
99
|
+
team.sessionIds.splice(index, 1);
|
|
100
|
+
team.lastActivityAt = Date.now();
|
|
101
|
+
this.emit('session:removed', { teamName, sessionId });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
getTeam(teamName) {
|
|
105
|
+
return this.teams.get(teamName);
|
|
106
|
+
}
|
|
107
|
+
listTeams() {
|
|
108
|
+
return [...this.teams.values()].filter((t) => t.status === 'active');
|
|
109
|
+
}
|
|
110
|
+
getStats() {
|
|
111
|
+
const values = [...this.teams.values()];
|
|
112
|
+
return {
|
|
113
|
+
totalTeams: values.length,
|
|
114
|
+
activeTeams: values.filter((t) => t.status === 'active').length,
|
|
115
|
+
totalSessions: values.reduce((sum, t) => sum + t.sessionIds.length, 0),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async forceDeleteTeam(teamName, options = {}) {
|
|
119
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS, skipSessionCleanup = false } = options;
|
|
120
|
+
const validation = validateTeamName(teamName);
|
|
121
|
+
if (!validation.valid) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
teamName,
|
|
125
|
+
sessionsClosed: 0,
|
|
126
|
+
error: `Invalid team name: ${validation.error}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const team = this.teams.get(teamName);
|
|
130
|
+
if (!team) {
|
|
131
|
+
return {
|
|
132
|
+
success: false,
|
|
133
|
+
teamName,
|
|
134
|
+
sessionsClosed: 0,
|
|
135
|
+
error: `Team "${teamName}" not found`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
team.status = 'closing';
|
|
139
|
+
this.emit('team:closing', { teamName });
|
|
140
|
+
const pendingTimeout = this.cleanupTimeouts.get(teamName);
|
|
141
|
+
if (pendingTimeout) {
|
|
142
|
+
clearTimeout(pendingTimeout);
|
|
143
|
+
this.cleanupTimeouts.delete(teamName);
|
|
144
|
+
}
|
|
145
|
+
let sessionsClosed = 0;
|
|
146
|
+
let error;
|
|
147
|
+
try {
|
|
148
|
+
if (!skipSessionCleanup && team.sessionIds.length > 0) {
|
|
149
|
+
const closePromises = team.sessionIds.map((sessionId) => this.closeSession(sessionId, timeoutMs));
|
|
150
|
+
const results = await Promise.allSettled(closePromises);
|
|
151
|
+
sessionsClosed = results.filter((r) => r.status === 'fulfilled').length;
|
|
152
|
+
const failures = results
|
|
153
|
+
.filter((r) => r.status === 'rejected')
|
|
154
|
+
.map((r) => r.reason)
|
|
155
|
+
.filter((e) => e instanceof Error)
|
|
156
|
+
.map((e) => e.message);
|
|
157
|
+
if (failures.length > 0) {
|
|
158
|
+
error = `Failed to close ${failures.length} session(s): ${failures.join('; ')}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.teams.delete(teamName);
|
|
162
|
+
team.status = 'closed';
|
|
163
|
+
this.emit('team:deleted', { teamName, sessionsClosed });
|
|
164
|
+
return {
|
|
165
|
+
success: !error,
|
|
166
|
+
teamName,
|
|
167
|
+
sessionsClosed,
|
|
168
|
+
error,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
teamName,
|
|
176
|
+
sessionsClosed,
|
|
177
|
+
error: errorMessage,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async closeSession(sessionId, timeoutMs) {
|
|
182
|
+
try {
|
|
183
|
+
await execCodexCommand(['cancel-session', sessionId], timeoutMs);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
187
|
+
throw new Error(`Failed to close session ${sessionId}: ${msg}`, { cause: e });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
scheduleAutoCleanup(teamName, delayMs = 300_000) {
|
|
191
|
+
const existing = this.cleanupTimeouts.get(teamName);
|
|
192
|
+
if (existing) {
|
|
193
|
+
clearTimeout(existing);
|
|
194
|
+
}
|
|
195
|
+
const timeoutId = setTimeout(() => {
|
|
196
|
+
this.forceDeleteTeam(teamName).catch(() => {
|
|
197
|
+
});
|
|
198
|
+
this.cleanupTimeouts.delete(teamName);
|
|
199
|
+
}, delayMs);
|
|
200
|
+
this.cleanupTimeouts.set(teamName, timeoutId);
|
|
201
|
+
}
|
|
202
|
+
cancelScheduledCleanup(teamName) {
|
|
203
|
+
const timeout = this.cleanupTimeouts.get(teamName);
|
|
204
|
+
if (timeout) {
|
|
205
|
+
clearTimeout(timeout);
|
|
206
|
+
this.cleanupTimeouts.delete(teamName);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async shutdown(timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
210
|
+
const teams = [...this.teams.keys()];
|
|
211
|
+
let closed = 0;
|
|
212
|
+
let failed = 0;
|
|
213
|
+
const results = await Promise.allSettled(teams.map((name) => this.forceDeleteTeam(name, { timeoutMs })));
|
|
214
|
+
for (const result of results) {
|
|
215
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
216
|
+
closed++;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
failed++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const [name, timeout] of this.cleanupTimeouts.entries()) {
|
|
223
|
+
clearTimeout(timeout);
|
|
224
|
+
this.cleanupTimeouts.delete(name);
|
|
225
|
+
}
|
|
226
|
+
return { closed, failed };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
let globalTeamManager;
|
|
230
|
+
export function getTeamManager() {
|
|
231
|
+
if (!globalTeamManager) {
|
|
232
|
+
globalTeamManager = new TeamManager();
|
|
233
|
+
}
|
|
234
|
+
return globalTeamManager;
|
|
235
|
+
}
|
|
236
|
+
export function resetTeamManager() {
|
|
237
|
+
globalTeamManager = undefined;
|
|
238
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './TeamManager.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './TeamManager.js';
|
|
@@ -3,12 +3,13 @@ export interface RetryPolicy {
|
|
|
3
3
|
backoffMs: number;
|
|
4
4
|
multiplier?: number;
|
|
5
5
|
}
|
|
6
|
-
export type
|
|
6
|
+
export type ToolNodeInput = string | number | boolean | null | undefined | Record<string, unknown> | unknown[];
|
|
7
|
+
export type WorkflowNodeType = 'tool' | 'sequence' | 'parallel' | 'branch' | 'fallback';
|
|
7
8
|
export interface ToolNode {
|
|
8
9
|
readonly kind: 'tool';
|
|
9
10
|
readonly id: string;
|
|
10
11
|
readonly toolName: string;
|
|
11
|
-
readonly input?: Record<string,
|
|
12
|
+
readonly input?: Record<string, ToolNodeInput>;
|
|
12
13
|
readonly inputFrom?: Record<string, string>;
|
|
13
14
|
readonly timeoutMs?: number;
|
|
14
15
|
readonly retry?: RetryPolicy;
|
|
@@ -33,7 +34,13 @@ export interface BranchNode {
|
|
|
33
34
|
readonly whenTrue: WorkflowNode;
|
|
34
35
|
readonly whenFalse?: WorkflowNode;
|
|
35
36
|
}
|
|
36
|
-
export
|
|
37
|
+
export interface FallbackNode {
|
|
38
|
+
readonly kind: 'fallback';
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly primary: WorkflowNode;
|
|
41
|
+
readonly fallback: WorkflowNode;
|
|
42
|
+
}
|
|
43
|
+
export type WorkflowNode = ToolNode | SequenceNode | ParallelNode | BranchNode | FallbackNode;
|
|
37
44
|
export interface WorkflowExecutionContext {
|
|
38
45
|
readonly workflowRunId: string;
|
|
39
46
|
readonly profile: string;
|
|
@@ -93,7 +100,7 @@ export declare class ToolNodeBuilder extends WorkflowNodeBuilder<ToolNode> {
|
|
|
93
100
|
private _retry?;
|
|
94
101
|
private _timeoutMs?;
|
|
95
102
|
constructor(id: string, toolName: string);
|
|
96
|
-
input(input: Record<string,
|
|
103
|
+
input(input: Record<string, ToolNodeInput>): this;
|
|
97
104
|
inputFrom(mapping: Record<string, string>): this;
|
|
98
105
|
retry(policy: RetryPolicy): this;
|
|
99
106
|
timeout(ms: number): this;
|
|
@@ -106,6 +113,7 @@ export declare class SequenceNodeBuilder extends WorkflowNodeBuilder<SequenceNod
|
|
|
106
113
|
sequence(id: string, config?: (b: SequenceNodeBuilder) => void): this;
|
|
107
114
|
parallel(id: string, config?: (b: ParallelNodeBuilder) => void): this;
|
|
108
115
|
branch(id: string, predicateId: string, config?: (b: BranchNodeBuilder) => void): this;
|
|
116
|
+
fallback(id: string, config?: (b: FallbackNodeBuilder) => void): this;
|
|
109
117
|
build(): SequenceNode;
|
|
110
118
|
}
|
|
111
119
|
export declare class ParallelNodeBuilder extends WorkflowNodeBuilder<ParallelNode> {
|
|
@@ -117,6 +125,7 @@ export declare class ParallelNodeBuilder extends WorkflowNodeBuilder<ParallelNod
|
|
|
117
125
|
sequence(id: string, config?: (b: SequenceNodeBuilder) => void): this;
|
|
118
126
|
parallel(id: string, config?: (b: ParallelNodeBuilder) => void): this;
|
|
119
127
|
branch(id: string, predicateId: string, config?: (b: BranchNodeBuilder) => void): this;
|
|
128
|
+
fallback(id: string, config?: (b: FallbackNodeBuilder) => void): this;
|
|
120
129
|
maxConcurrency(concurrency: number): this;
|
|
121
130
|
failFast(ff: boolean): this;
|
|
122
131
|
build(): ParallelNode;
|
|
@@ -132,6 +141,13 @@ export declare class BranchNodeBuilder extends WorkflowNodeBuilder<BranchNode> {
|
|
|
132
141
|
whenFalse(nodeBuilder: AnyWorkflowNodeBuilder): this;
|
|
133
142
|
build(): BranchNode;
|
|
134
143
|
}
|
|
144
|
+
export declare class FallbackNodeBuilder extends WorkflowNodeBuilder<FallbackNode> {
|
|
145
|
+
private _primary?;
|
|
146
|
+
private _fallback?;
|
|
147
|
+
primary(nodeBuilder: AnyWorkflowNodeBuilder): this;
|
|
148
|
+
fallback(nodeBuilder: AnyWorkflowNodeBuilder): this;
|
|
149
|
+
build(): FallbackNode;
|
|
150
|
+
}
|
|
135
151
|
export declare class WorkflowBuilder {
|
|
136
152
|
private _id;
|
|
137
153
|
private _displayName;
|
|
@@ -76,6 +76,13 @@ export class SequenceNodeBuilder extends WorkflowNodeBuilder {
|
|
|
76
76
|
this._steps.push(builder);
|
|
77
77
|
return this;
|
|
78
78
|
}
|
|
79
|
+
fallback(id, config) {
|
|
80
|
+
const builder = new FallbackNodeBuilder(id);
|
|
81
|
+
if (config)
|
|
82
|
+
config(builder);
|
|
83
|
+
this._steps.push(builder);
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
79
86
|
build() {
|
|
80
87
|
return {
|
|
81
88
|
kind: 'sequence',
|
|
@@ -120,6 +127,13 @@ export class ParallelNodeBuilder extends WorkflowNodeBuilder {
|
|
|
120
127
|
this._steps.push(builder);
|
|
121
128
|
return this;
|
|
122
129
|
}
|
|
130
|
+
fallback(id, config) {
|
|
131
|
+
const builder = new FallbackNodeBuilder(id);
|
|
132
|
+
if (config)
|
|
133
|
+
config(builder);
|
|
134
|
+
this._steps.push(builder);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
123
137
|
maxConcurrency(concurrency) {
|
|
124
138
|
this._maxConcurrency = concurrency;
|
|
125
139
|
return this;
|
|
@@ -173,6 +187,32 @@ export class BranchNodeBuilder extends WorkflowNodeBuilder {
|
|
|
173
187
|
};
|
|
174
188
|
}
|
|
175
189
|
}
|
|
190
|
+
export class FallbackNodeBuilder extends WorkflowNodeBuilder {
|
|
191
|
+
_primary;
|
|
192
|
+
_fallback;
|
|
193
|
+
primary(nodeBuilder) {
|
|
194
|
+
this._primary = nodeBuilder;
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
fallback(nodeBuilder) {
|
|
198
|
+
this._fallback = nodeBuilder;
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
build() {
|
|
202
|
+
if (!this._primary) {
|
|
203
|
+
throw new Error(`FallbackNode '${this.id}' requires a primary step`);
|
|
204
|
+
}
|
|
205
|
+
if (!this._fallback) {
|
|
206
|
+
throw new Error(`FallbackNode '${this.id}' requires a fallback step`);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
kind: 'fallback',
|
|
210
|
+
id: this.id,
|
|
211
|
+
primary: this._primary.build(),
|
|
212
|
+
fallback: this._fallback.build(),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
176
216
|
export class WorkflowBuilder {
|
|
177
217
|
_id;
|
|
178
218
|
_displayName;
|