@sylphx/flow 3.19.0 → 3.20.0
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/CHANGELOG.md +31 -0
- package/assets/agents/builder.md +12 -3
- package/package.json +1 -3
- package/src/commands/flow/execute-v2.ts +126 -128
- package/src/commands/flow-command.ts +52 -42
- package/src/commands/hook-command.ts +161 -20
- package/src/config/index.ts +0 -20
- package/src/core/agent-loader.ts +2 -2
- package/src/core/attach-manager.ts +5 -1
- package/src/core/cleanup-handler.ts +20 -16
- package/src/core/flow-executor.ts +93 -62
- package/src/core/functional/index.ts +0 -11
- package/src/core/index.ts +1 -1
- package/src/core/project-manager.ts +14 -29
- package/src/core/secrets-manager.ts +15 -18
- package/src/core/session-manager.ts +4 -8
- package/src/core/target-manager.ts +6 -3
- package/src/core/upgrade-manager.ts +1 -1
- package/src/index.ts +1 -1
- package/src/services/auto-upgrade.ts +6 -14
- package/src/services/config-service.ts +7 -23
- package/src/services/index.ts +1 -1
- package/src/targets/claude-code.ts +14 -8
- package/src/targets/functional/claude-code-logic.ts +11 -7
- package/src/targets/opencode.ts +61 -39
- package/src/targets/shared/mcp-transforms.ts +20 -43
- package/src/types/agent.types.ts +5 -3
- package/src/types/mcp.types.ts +38 -1
- package/src/types.ts +4 -0
- package/src/utils/agent-enhancer.ts +1 -1
- package/src/utils/errors.ts +13 -0
- package/src/utils/files/file-operations.ts +16 -0
- package/src/utils/index.ts +1 -1
- package/src/core/error-handling.ts +0 -482
- package/src/core/functional/async.ts +0 -101
- package/src/core/functional/either.ts +0 -109
- package/src/core/functional/error-handler.ts +0 -135
- package/src/core/functional/pipe.ts +0 -189
- package/src/core/functional/validation.ts +0 -138
- package/src/types/mcp-config.types.ts +0 -448
- package/src/utils/error-handler.ts +0 -53
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Hook command -
|
|
3
|
+
* Hook command - Claude Code hook handlers
|
|
4
4
|
*
|
|
5
|
-
* Purpose:
|
|
5
|
+
* Purpose: Handle Claude Code lifecycle hooks:
|
|
6
|
+
* - notification: OS-level notification when Claude Code starts
|
|
7
|
+
* - session-start: Load durable memory (MEMORY.md + recent daily logs)
|
|
6
8
|
*
|
|
7
9
|
* DESIGN RATIONALE:
|
|
8
|
-
* -
|
|
9
|
-
* - Cross-platform
|
|
10
|
-
* -
|
|
10
|
+
* - Each hook type returns content to stdout (Claude Code injects as context)
|
|
11
|
+
* - Cross-platform notifications (macOS, Linux, Windows)
|
|
12
|
+
* - SessionStart hook loads cross-session memory at startup
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { exec } from 'node:child_process';
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
14
17
|
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
15
19
|
import { promisify } from 'node:util';
|
|
16
20
|
import { Command } from 'commander';
|
|
17
21
|
import { cli } from '../utils/display/cli-output.js';
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Hook input from Claude Code via stdin.
|
|
25
|
+
* Claude Code sends JSON context about the event — we read what we need.
|
|
26
|
+
*/
|
|
27
|
+
interface HookInput {
|
|
28
|
+
/** Notification message text */
|
|
29
|
+
message?: string;
|
|
30
|
+
/** Notification title */
|
|
31
|
+
title?: string;
|
|
32
|
+
/** Notification type (permission_prompt, idle_prompt, etc.) */
|
|
33
|
+
notification_type?: string;
|
|
34
|
+
/** How the session started (startup, resume, clear, compact) */
|
|
35
|
+
source?: string;
|
|
36
|
+
/** Current working directory */
|
|
37
|
+
cwd?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
const execAsync = promisify(exec);
|
|
20
41
|
|
|
21
42
|
/**
|
|
22
|
-
* Hook types supported
|
|
43
|
+
* Hook types supported — single source of truth for both type and runtime validation
|
|
23
44
|
*/
|
|
24
|
-
|
|
45
|
+
const VALID_HOOK_TYPES = ['notification', 'session-start'] as const;
|
|
46
|
+
type HookType = (typeof VALID_HOOK_TYPES)[number];
|
|
47
|
+
const VALID_HOOK_TYPE_SET = new Set<string>(VALID_HOOK_TYPES);
|
|
25
48
|
|
|
26
49
|
/**
|
|
27
50
|
* Target platforms supported
|
|
@@ -32,18 +55,20 @@ type TargetPlatform = 'claude-code';
|
|
|
32
55
|
* Create the hook command
|
|
33
56
|
*/
|
|
34
57
|
export const hookCommand = new Command('hook')
|
|
35
|
-
.description('
|
|
36
|
-
.requiredOption('--type <type>', 'Hook type (notification)')
|
|
58
|
+
.description('Handle Claude Code lifecycle hooks (notification, memory)')
|
|
59
|
+
.requiredOption('--type <type>', 'Hook type (notification | session-start)')
|
|
37
60
|
.option('--target <target>', 'Target platform (claude-code)', 'claude-code')
|
|
38
61
|
.option('--verbose', 'Show verbose output', false)
|
|
39
62
|
.action(async (options) => {
|
|
40
63
|
try {
|
|
41
|
-
const hookType = options.type as
|
|
64
|
+
const hookType = options.type as string;
|
|
42
65
|
const target = options.target as TargetPlatform;
|
|
43
66
|
|
|
44
67
|
// Validate hook type
|
|
45
|
-
if (hookType
|
|
46
|
-
throw new Error(
|
|
68
|
+
if (!VALID_HOOK_TYPE_SET.has(hookType)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid hook type: ${hookType}. Must be one of: ${VALID_HOOK_TYPES.join(', ')}`
|
|
71
|
+
);
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
// Validate target
|
|
@@ -51,8 +76,11 @@ export const hookCommand = new Command('hook')
|
|
|
51
76
|
throw new Error(`Invalid target: ${target}. Only 'claude-code' is currently supported`);
|
|
52
77
|
}
|
|
53
78
|
|
|
79
|
+
// Read hook input from stdin (Claude Code passes JSON context)
|
|
80
|
+
const hookInput = await readStdinInput();
|
|
81
|
+
|
|
54
82
|
// Load and display content based on hook type
|
|
55
|
-
const content = await loadHookContent(hookType, target, options.verbose);
|
|
83
|
+
const content = await loadHookContent(hookType as HookType, target, hookInput, options.verbose);
|
|
56
84
|
|
|
57
85
|
// Output the content (no extra formatting, just the content)
|
|
58
86
|
console.log(content);
|
|
@@ -72,27 +100,140 @@ export const hookCommand = new Command('hook')
|
|
|
72
100
|
}
|
|
73
101
|
});
|
|
74
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Read JSON input from stdin (non-blocking, returns empty object if no input)
|
|
105
|
+
* Claude Code sends event context as JSON on stdin for all hook types.
|
|
106
|
+
*/
|
|
107
|
+
async function readStdinInput(): Promise<HookInput> {
|
|
108
|
+
// stdin is not a TTY when piped from Claude Code
|
|
109
|
+
if (process.stdin.isTTY) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
let data = '';
|
|
115
|
+
process.stdin.setEncoding('utf-8');
|
|
116
|
+
process.stdin.on('data', (chunk) => {
|
|
117
|
+
data += chunk;
|
|
118
|
+
});
|
|
119
|
+
process.stdin.on('end', () => {
|
|
120
|
+
if (!data.trim()) {
|
|
121
|
+
resolve({});
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
resolve(JSON.parse(data) as HookInput);
|
|
126
|
+
} catch {
|
|
127
|
+
resolve({});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// Safety timeout — don't hang if stdin never closes
|
|
131
|
+
setTimeout(() => resolve({}), 1000);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
75
135
|
/**
|
|
76
136
|
* Load content for a specific hook type and target
|
|
77
137
|
*/
|
|
78
138
|
async function loadHookContent(
|
|
79
139
|
hookType: HookType,
|
|
80
140
|
_target: TargetPlatform,
|
|
141
|
+
input: HookInput,
|
|
81
142
|
verbose: boolean = false
|
|
82
143
|
): Promise<string> {
|
|
83
|
-
|
|
84
|
-
|
|
144
|
+
switch (hookType) {
|
|
145
|
+
case 'notification':
|
|
146
|
+
return await sendNotification(input, verbose);
|
|
147
|
+
case 'session-start':
|
|
148
|
+
return await loadSessionStartContent(verbose);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Read a file and return its contents, or empty string if not found
|
|
154
|
+
*/
|
|
155
|
+
async function readFileIfExists(filePath: string): Promise<string> {
|
|
156
|
+
try {
|
|
157
|
+
return await readFile(filePath, 'utf-8');
|
|
158
|
+
} catch {
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a date as YYYY-MM-DD
|
|
165
|
+
*/
|
|
166
|
+
function formatDate(date: Date): string {
|
|
167
|
+
const year = date.getFullYear();
|
|
168
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
169
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
170
|
+
return `${year}-${month}-${day}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Load memory files for session start:
|
|
175
|
+
* - MEMORY.md (curated long-term memory)
|
|
176
|
+
* - memory/{today}.md (today's daily log)
|
|
177
|
+
* - memory/{yesterday}.md (yesterday's daily log)
|
|
178
|
+
*
|
|
179
|
+
* Returns concatenated content to stdout for Claude Code context injection
|
|
180
|
+
*/
|
|
181
|
+
async function loadSessionStartContent(verbose: boolean): Promise<string> {
|
|
182
|
+
const cwd = process.cwd();
|
|
183
|
+
const sections: string[] = [];
|
|
184
|
+
|
|
185
|
+
// Load MEMORY.md
|
|
186
|
+
const memoryPath = path.join(cwd, 'MEMORY.md');
|
|
187
|
+
const memoryContent = await readFileIfExists(memoryPath);
|
|
188
|
+
if (memoryContent.trim()) {
|
|
189
|
+
sections.push(`## MEMORY.md\n${memoryContent.trim()}`);
|
|
190
|
+
if (verbose) {
|
|
191
|
+
cli.info('Loaded MEMORY.md');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Calculate today and yesterday dates
|
|
196
|
+
const today = new Date();
|
|
197
|
+
const yesterday = new Date(today);
|
|
198
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
199
|
+
|
|
200
|
+
const todayStr = formatDate(today);
|
|
201
|
+
const yesterdayStr = formatDate(yesterday);
|
|
202
|
+
|
|
203
|
+
// Load today's daily log
|
|
204
|
+
const todayPath = path.join(cwd, 'memory', `${todayStr}.md`);
|
|
205
|
+
const todayContent = await readFileIfExists(todayPath);
|
|
206
|
+
if (todayContent.trim()) {
|
|
207
|
+
sections.push(`## memory/${todayStr}.md\n${todayContent.trim()}`);
|
|
208
|
+
if (verbose) {
|
|
209
|
+
cli.info(`Loaded memory/${todayStr}.md`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Load yesterday's daily log
|
|
214
|
+
const yesterdayPath = path.join(cwd, 'memory', `${yesterdayStr}.md`);
|
|
215
|
+
const yesterdayContent = await readFileIfExists(yesterdayPath);
|
|
216
|
+
if (yesterdayContent.trim()) {
|
|
217
|
+
sections.push(`## memory/${yesterdayStr}.md\n${yesterdayContent.trim()}`);
|
|
218
|
+
if (verbose) {
|
|
219
|
+
cli.info(`Loaded memory/${yesterdayStr}.md`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (verbose && sections.length === 0) {
|
|
224
|
+
cli.info('No memory files found');
|
|
85
225
|
}
|
|
86
226
|
|
|
87
|
-
return '';
|
|
227
|
+
return sections.join('\n\n');
|
|
88
228
|
}
|
|
89
229
|
|
|
90
230
|
/**
|
|
91
|
-
* Send OS-level notification
|
|
231
|
+
* Send OS-level notification using event data from Claude Code.
|
|
232
|
+
* Falls back to generic message when stdin input is missing.
|
|
92
233
|
*/
|
|
93
|
-
async function sendNotification(verbose: boolean): Promise<string> {
|
|
94
|
-
const title = '🔮 Sylphx Flow';
|
|
95
|
-
const message = 'Claude Code is ready';
|
|
234
|
+
async function sendNotification(input: HookInput, verbose: boolean): Promise<string> {
|
|
235
|
+
const title = input.title || '🔮 Sylphx Flow';
|
|
236
|
+
const message = input.message || 'Claude Code is ready';
|
|
96
237
|
const platform = os.platform();
|
|
97
238
|
|
|
98
239
|
if (verbose) {
|
package/src/config/index.ts
CHANGED
|
@@ -1,27 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Configuration modules barrel export
|
|
3
|
-
* Centralized access to configuration-related functionality
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
// Rules configuration
|
|
7
5
|
export * from './rules.js';
|
|
8
|
-
export {
|
|
9
|
-
getDefaultRules,
|
|
10
|
-
loadRuleConfiguration,
|
|
11
|
-
validateRuleConfiguration,
|
|
12
|
-
} from './rules.js';
|
|
13
|
-
// MCP server configurations
|
|
14
6
|
export * from './servers.js';
|
|
15
|
-
export {
|
|
16
|
-
configureServer,
|
|
17
|
-
getServerConfigurations,
|
|
18
|
-
validateServerConfiguration,
|
|
19
|
-
} from './servers.js';
|
|
20
|
-
// Target configurations
|
|
21
7
|
export * from './targets.js';
|
|
22
|
-
// Re-export commonly used configuration functions with better naming
|
|
23
|
-
export {
|
|
24
|
-
configureTargetDefaults,
|
|
25
|
-
getTargetDefaults,
|
|
26
|
-
validateTargetConfiguration,
|
|
27
|
-
} from './targets.js';
|
package/src/core/agent-loader.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { homedir } from 'node:os';
|
|
|
8
8
|
import { dirname, join, parse, relative } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import matter from 'gray-matter';
|
|
11
|
-
import type { Agent,
|
|
11
|
+
import type { Agent, AgentDefinition } from '../types/agent.types.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Load a single agent from a markdown file
|
|
@@ -28,7 +28,7 @@ export async function loadAgentFromFile(
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const metadata:
|
|
31
|
+
const metadata: AgentDefinition = {
|
|
32
32
|
name: data.name,
|
|
33
33
|
description: data.description || '',
|
|
34
34
|
};
|
|
@@ -10,6 +10,7 @@ import fs from 'node:fs/promises';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { MCP_SERVER_REGISTRY, type MCPServerID } from '../config/servers.js';
|
|
12
12
|
import { GlobalConfigService } from '../services/global-config.js';
|
|
13
|
+
import type { MCPServerConfigUnion } from '../types/mcp.types.js';
|
|
13
14
|
import type { Target } from '../types/target.types.js';
|
|
14
15
|
import { attachItemsToDir, attachRulesFile } from './attach/index.js';
|
|
15
16
|
import type { BackupManifest } from './backup-manager.js';
|
|
@@ -320,7 +321,10 @@ export class AttachManager {
|
|
|
320
321
|
// Add Flow MCP servers
|
|
321
322
|
for (const server of mcpServers) {
|
|
322
323
|
// Transform the server config for this target
|
|
323
|
-
const transformedConfig = target.transformMCPConfig(
|
|
324
|
+
const transformedConfig = target.transformMCPConfig(
|
|
325
|
+
server.config as MCPServerConfigUnion,
|
|
326
|
+
server.name
|
|
327
|
+
);
|
|
324
328
|
|
|
325
329
|
if (mcpContainer[server.name]) {
|
|
326
330
|
// Conflict: user has same MCP server
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
import { existsSync } from 'node:fs';
|
|
17
17
|
import fs from 'node:fs/promises';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
+
import createDebug from 'debug';
|
|
19
20
|
import type { BackupManager } from './backup-manager.js';
|
|
20
21
|
import type { GitStashManager } from './git-stash-manager.js';
|
|
21
22
|
import type { ProjectManager } from './project-manager.js';
|
|
22
23
|
import type { SecretsManager } from './secrets-manager.js';
|
|
23
24
|
import type { SessionManager } from './session-manager.js';
|
|
24
25
|
|
|
26
|
+
const debug = createDebug('flow:cleanup');
|
|
27
|
+
|
|
25
28
|
export class CleanupHandler {
|
|
26
29
|
private projectManager: ProjectManager;
|
|
27
30
|
private sessionManager: SessionManager;
|
|
@@ -132,8 +135,8 @@ export class CleanupHandler {
|
|
|
132
135
|
await this.gitStashManager.popSettingsChanges(session.projectPath);
|
|
133
136
|
await this.secretsManager.clearSecrets(this.currentProjectHash);
|
|
134
137
|
}
|
|
135
|
-
} catch (
|
|
136
|
-
|
|
138
|
+
} catch (error) {
|
|
139
|
+
debug('signal cleanup failed:', error);
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -156,8 +159,8 @@ export class CleanupHandler {
|
|
|
156
159
|
await this.gitStashManager.popSettingsChanges(session.projectPath);
|
|
157
160
|
await this.secretsManager.clearSecrets(projectHash);
|
|
158
161
|
await this.backupManager.cleanupOldBackups(projectHash, 3);
|
|
159
|
-
} catch (
|
|
160
|
-
|
|
162
|
+
} catch (error) {
|
|
163
|
+
debug('startup recovery failed for session:', error);
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
|
|
@@ -168,8 +171,8 @@ export class CleanupHandler {
|
|
|
168
171
|
this.sessionManager.cleanupSessionHistory(50),
|
|
169
172
|
this.cleanupOrphanedProjects(),
|
|
170
173
|
]);
|
|
171
|
-
} catch {
|
|
172
|
-
|
|
174
|
+
} catch (error) {
|
|
175
|
+
debug('periodic cleanup failed:', error);
|
|
173
176
|
}
|
|
174
177
|
await this.updateCleanupTimestamp();
|
|
175
178
|
}
|
|
@@ -185,7 +188,6 @@ export class CleanupHandler {
|
|
|
185
188
|
const hoursSinceLastCleanup = (Date.now() - stat.mtimeMs) / (1000 * 60 * 60);
|
|
186
189
|
return hoursSinceLastCleanup >= 24;
|
|
187
190
|
} catch {
|
|
188
|
-
// Marker doesn't exist — first run or deleted
|
|
189
191
|
return true;
|
|
190
192
|
}
|
|
191
193
|
}
|
|
@@ -197,8 +199,8 @@ export class CleanupHandler {
|
|
|
197
199
|
const markerPath = path.join(this.projectManager.getFlowHomeDir(), '.last-cleanup');
|
|
198
200
|
try {
|
|
199
201
|
await fs.writeFile(markerPath, new Date().toISOString());
|
|
200
|
-
} catch {
|
|
201
|
-
|
|
202
|
+
} catch (error) {
|
|
203
|
+
debug('failed to update cleanup timestamp:', error);
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
|
|
@@ -259,7 +261,8 @@ export class CleanupHandler {
|
|
|
259
261
|
try {
|
|
260
262
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
261
263
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
262
|
-
} catch {
|
|
264
|
+
} catch (error) {
|
|
265
|
+
debug('failed to scan directory %s: %O', dir, error);
|
|
263
266
|
return [];
|
|
264
267
|
}
|
|
265
268
|
};
|
|
@@ -292,7 +295,8 @@ export class CleanupHandler {
|
|
|
292
295
|
const manifestPath = path.join(paths.backupsDir, sessions[0].name, 'manifest.json');
|
|
293
296
|
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf-8'));
|
|
294
297
|
return manifest.projectPath || null;
|
|
295
|
-
} catch {
|
|
298
|
+
} catch (error) {
|
|
299
|
+
debug('failed to read backup manifest for %s: %O', projectHash, error);
|
|
296
300
|
return null;
|
|
297
301
|
}
|
|
298
302
|
}
|
|
@@ -306,22 +310,22 @@ export class CleanupHandler {
|
|
|
306
310
|
// Remove backups directory
|
|
307
311
|
try {
|
|
308
312
|
await fs.rm(paths.backupsDir, { recursive: true, force: true });
|
|
309
|
-
} catch {
|
|
310
|
-
|
|
313
|
+
} catch (error) {
|
|
314
|
+
debug('failed to remove backups for %s: %O', projectHash, error);
|
|
311
315
|
}
|
|
312
316
|
|
|
313
317
|
// Remove secrets directory
|
|
314
318
|
try {
|
|
315
319
|
await fs.rm(paths.secretsDir, { recursive: true, force: true });
|
|
316
|
-
} catch {
|
|
317
|
-
|
|
320
|
+
} catch (error) {
|
|
321
|
+
debug('failed to remove secrets for %s: %O', projectHash, error);
|
|
318
322
|
}
|
|
319
323
|
|
|
320
324
|
// Remove session file if exists
|
|
321
325
|
try {
|
|
322
326
|
await fs.unlink(paths.sessionFile);
|
|
323
327
|
} catch {
|
|
324
|
-
//
|
|
328
|
+
// Expected: file may not exist
|
|
325
329
|
}
|
|
326
330
|
}
|
|
327
331
|
}
|
|
@@ -8,7 +8,11 @@ import { existsSync } from 'node:fs';
|
|
|
8
8
|
import fs from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
+
import createDebug from 'debug';
|
|
11
12
|
import type { Target } from '../types/target.types.js';
|
|
13
|
+
|
|
14
|
+
const debug = createDebug('flow:executor');
|
|
15
|
+
|
|
12
16
|
import { AttachManager } from './attach-manager.js';
|
|
13
17
|
import { BackupManager } from './backup-manager.js';
|
|
14
18
|
import { CleanupHandler } from './cleanup-handler.js';
|
|
@@ -55,70 +59,62 @@ export class FlowExecutor {
|
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
|
-
*
|
|
59
|
-
* Returns summary for caller to display
|
|
62
|
+
* Try to join an existing session. Returns summary if joined, null otherwise.
|
|
60
63
|
*/
|
|
61
|
-
async
|
|
64
|
+
private async tryJoinExistingSession(
|
|
62
65
|
projectPath: string,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
commands?: number;
|
|
68
|
-
skills?: number;
|
|
69
|
-
mcp?: number;
|
|
70
|
-
}> {
|
|
71
|
-
// Initialize Flow directories
|
|
72
|
-
await this.projectManager.initialize();
|
|
73
|
-
|
|
74
|
-
// Step 1: Crash recovery on startup
|
|
75
|
-
await this.cleanupHandler.recoverOnStartup();
|
|
76
|
-
|
|
77
|
-
// Step 2: Get project hash and paths
|
|
78
|
-
const projectHash = this.projectManager.getProjectHash(projectPath);
|
|
79
|
-
const target = await this.projectManager.detectTarget(projectPath);
|
|
80
|
-
|
|
81
|
-
if (options.verbose) {
|
|
82
|
-
console.log(chalk.dim(`Project: ${projectPath}`));
|
|
83
|
-
console.log(chalk.dim(`Hash: ${projectHash}`));
|
|
84
|
-
console.log(chalk.dim(`Target: ${target}\n`));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Check for existing session
|
|
66
|
+
projectHash: string,
|
|
67
|
+
target: string,
|
|
68
|
+
verbose?: boolean
|
|
69
|
+
): Promise<{ joined: true } | null> {
|
|
88
70
|
const existingSession = await this.sessionManager.getActiveSession(projectHash);
|
|
71
|
+
if (!existingSession) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
89
74
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.cleanupHandler.registerCleanupHooks(projectHash);
|
|
105
|
-
return { joined: true };
|
|
106
|
-
}
|
|
75
|
+
const targetObj = resolveTargetOrId(target);
|
|
76
|
+
const agentsDir = path.join(projectPath, targetObj.config.agentDir);
|
|
77
|
+
const filesExist = existsSync(agentsDir) && (await fs.readdir(agentsDir)).length > 0;
|
|
78
|
+
|
|
79
|
+
if (filesExist) {
|
|
80
|
+
await this.sessionManager.startSession(
|
|
81
|
+
projectPath,
|
|
82
|
+
projectHash,
|
|
83
|
+
target,
|
|
84
|
+
existingSession.backupPath
|
|
85
|
+
);
|
|
86
|
+
this.cleanupHandler.registerCleanupHooks(projectHash);
|
|
87
|
+
return { joined: true };
|
|
88
|
+
}
|
|
107
89
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log(chalk.dim('Session files missing, re-attaching...'));
|
|
111
|
-
}
|
|
112
|
-
await this.sessionManager.endSession(projectHash);
|
|
90
|
+
if (verbose) {
|
|
91
|
+
console.log(chalk.dim('Session files missing, re-attaching...'));
|
|
113
92
|
}
|
|
93
|
+
await this.sessionManager.endSession(projectHash);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
114
96
|
|
|
115
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Create a new session: backup, extract secrets, attach templates
|
|
99
|
+
*/
|
|
100
|
+
private async createNewSession(
|
|
101
|
+
projectPath: string,
|
|
102
|
+
projectHash: string,
|
|
103
|
+
target: string,
|
|
104
|
+
options: FlowExecutorOptions
|
|
105
|
+
): Promise<{
|
|
106
|
+
agents: number;
|
|
107
|
+
commands: number;
|
|
108
|
+
skills: number;
|
|
109
|
+
mcp: number;
|
|
110
|
+
}> {
|
|
111
|
+
// Run independent setup steps in parallel
|
|
116
112
|
await Promise.all([
|
|
117
113
|
options.skipProjectDocs ? Promise.resolve() : this.ensureProjectDocs(projectPath),
|
|
118
114
|
this.gitStashManager.stashSettingsChanges(projectPath),
|
|
119
115
|
]);
|
|
120
116
|
|
|
121
|
-
// Backup and extract secrets in parallel
|
|
117
|
+
// Backup and extract secrets in parallel
|
|
122
118
|
const [backup] = await Promise.all([
|
|
123
119
|
this.backupManager.createBackup(projectPath, projectHash, target),
|
|
124
120
|
options.skipSecrets
|
|
@@ -132,7 +128,6 @@ export class FlowExecutor {
|
|
|
132
128
|
}),
|
|
133
129
|
]);
|
|
134
130
|
|
|
135
|
-
// Start session
|
|
136
131
|
const { session } = await this.sessionManager.startSession(
|
|
137
132
|
projectPath,
|
|
138
133
|
projectHash,
|
|
@@ -143,7 +138,6 @@ export class FlowExecutor {
|
|
|
143
138
|
|
|
144
139
|
this.cleanupHandler.registerCleanupHooks(projectHash);
|
|
145
140
|
|
|
146
|
-
// Clear and attach (silent)
|
|
147
141
|
if (!options.merge) {
|
|
148
142
|
await this.clearUserSettings(projectPath, target);
|
|
149
143
|
}
|
|
@@ -157,22 +151,19 @@ export class FlowExecutor {
|
|
|
157
151
|
|
|
158
152
|
const attachResult = await this.attachManager.attach(projectPath, target, templates, manifest);
|
|
159
153
|
|
|
160
|
-
// Apply target-specific settings (
|
|
161
|
-
// Non-fatal: CLI can still run without these settings
|
|
154
|
+
// Apply target-specific settings (non-fatal)
|
|
162
155
|
const targetObj = resolveTargetOrId(target);
|
|
163
156
|
if (targetObj.applySettings) {
|
|
164
157
|
try {
|
|
165
158
|
await targetObj.applySettings(projectPath, {});
|
|
166
|
-
} catch {
|
|
167
|
-
|
|
159
|
+
} catch (error) {
|
|
160
|
+
debug('applySettings failed:', error);
|
|
168
161
|
}
|
|
169
162
|
}
|
|
170
163
|
|
|
171
164
|
await this.backupManager.updateManifest(projectHash, session.sessionId, manifest);
|
|
172
165
|
|
|
173
|
-
// Return summary for caller to display
|
|
174
166
|
return {
|
|
175
|
-
joined: false,
|
|
176
167
|
agents: attachResult.agentsAdded.length,
|
|
177
168
|
commands: attachResult.commandsAdded.length,
|
|
178
169
|
skills: attachResult.skillsAdded.length,
|
|
@@ -180,6 +171,46 @@ export class FlowExecutor {
|
|
|
180
171
|
};
|
|
181
172
|
}
|
|
182
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Execute complete flow with attach mode (with multi-session support)
|
|
176
|
+
* Returns summary for caller to display
|
|
177
|
+
*/
|
|
178
|
+
async execute(
|
|
179
|
+
projectPath: string,
|
|
180
|
+
options: FlowExecutorOptions = {}
|
|
181
|
+
): Promise<{
|
|
182
|
+
joined: boolean;
|
|
183
|
+
agents?: number;
|
|
184
|
+
commands?: number;
|
|
185
|
+
skills?: number;
|
|
186
|
+
mcp?: number;
|
|
187
|
+
}> {
|
|
188
|
+
await this.projectManager.initialize();
|
|
189
|
+
await this.cleanupHandler.recoverOnStartup();
|
|
190
|
+
|
|
191
|
+
const projectHash = this.projectManager.getProjectHash(projectPath);
|
|
192
|
+
const target = await this.projectManager.detectTarget(projectPath);
|
|
193
|
+
|
|
194
|
+
if (options.verbose) {
|
|
195
|
+
console.log(chalk.dim(`Project: ${projectPath}`));
|
|
196
|
+
console.log(chalk.dim(`Hash: ${projectHash}`));
|
|
197
|
+
console.log(chalk.dim(`Target: ${target}\n`));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const joinResult = await this.tryJoinExistingSession(
|
|
201
|
+
projectPath,
|
|
202
|
+
projectHash,
|
|
203
|
+
target,
|
|
204
|
+
options.verbose
|
|
205
|
+
);
|
|
206
|
+
if (joinResult) {
|
|
207
|
+
return joinResult;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = await this.createNewSession(projectPath, projectHash, target, options);
|
|
211
|
+
return { joined: false, ...result };
|
|
212
|
+
}
|
|
213
|
+
|
|
183
214
|
/**
|
|
184
215
|
* Clear user settings in replace mode
|
|
185
216
|
* This ensures a clean slate for Flow's configuration
|
|
@@ -255,8 +286,8 @@ export class FlowExecutor {
|
|
|
255
286
|
if (content.includes('Sylphx Flow') || content.includes('Silent Execution Style')) {
|
|
256
287
|
await fs.unlink(filePath);
|
|
257
288
|
}
|
|
258
|
-
} catch {
|
|
259
|
-
|
|
289
|
+
} catch (error) {
|
|
290
|
+
debug('failed to clean legacy file %s: %O', fileName, error);
|
|
260
291
|
}
|
|
261
292
|
}
|
|
262
293
|
}
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Functional programming utilities
|
|
3
3
|
* Core abstractions for composable, type-safe error handling
|
|
4
|
-
*
|
|
5
|
-
* PRINCIPLES:
|
|
6
|
-
* - Pure functions (no side effects)
|
|
7
|
-
* - Explicit error handling (no exceptions in business logic)
|
|
8
|
-
* - Composable through map/flatMap
|
|
9
|
-
* - Type-safe (leverages TypeScript's type system)
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
export * from './async.js';
|
|
13
|
-
export * from './either.js';
|
|
14
|
-
export * from './error-handler.js';
|
|
15
6
|
export * from './error-types.js';
|
|
16
7
|
export * from './option.js';
|
|
17
|
-
export * from './pipe.js';
|
|
18
8
|
export * from './result.js';
|
|
19
|
-
export * from './validation.js';
|
package/src/core/index.ts
CHANGED