@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
@@ -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 WorkflowNodeType = 'tool' | 'sequence' | 'parallel' | 'branch';
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, unknown>;
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 type WorkflowNode = ToolNode | SequenceNode | ParallelNode | BranchNode;
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, unknown>): this;
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;