@sylphx/flow 3.24.2 → 3.26.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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # @sylphx/flow
2
2
 
3
+ ## 3.26.0 (2026-04-04)
4
+
5
+ ### ✨ Features
6
+
7
+ - **agent:** elevate builder standard — SOTA, scale-first, GitOps (#144) ([25593b9](https://github.com/SylphxAI/flow/commit/25593b94001840f8d14c8649c020fa7d5fb7de70))
8
+ - **agent:** add never-stop-mid-task directive to ignore context warnings (#143) ([da9b3e2](https://github.com/SylphxAI/flow/commit/da9b3e242116462c06f4c62bf195a1a26e2edc3d))
9
+
10
+ ## 3.25.0 (2026-02-17)
11
+
12
+ ### ✨ Features
13
+
14
+ - **flow:** smart --resume with auto-detection and sessions command ([ce6a3f8](https://github.com/SylphxAI/flow/commit/ce6a3f820c99fe47cb5c6701fc219bd599583470))
15
+
16
+ ### 🔧 Chores
17
+
18
+ - fix biome formatting in settings.json and package.json ([02bd051](https://github.com/SylphxAI/flow/commit/02bd051b88d29b645786c8812610400960adbcfe))
19
+
3
20
  ## 3.24.2 (2026-02-17)
4
21
 
5
22
  ### 🐛 Bug Fixes
@@ -18,16 +18,27 @@ Build something world-class. Something you'd stake your reputation on.
18
18
 
19
19
  ## Standard
20
20
 
21
- **Production-ready only.** No MVPs. No prototypes. No "good enough for now."
21
+ **State-of-the-art. Industrial standard. Production-ready. Commercial grade.** Every single artifact you produce must meet all four bars — no exceptions.
22
22
 
23
+ **Build for scale, not for now.** Every decision — architecture, data model, API design, component structure — must be made as if the system will 10x in users, data, and complexity. Never settle for what works today. Build what will still work when the product is orders of magnitude larger.
24
+
25
+ **Zero tolerance:**
23
26
  - No workarounds — solve the actual problem
24
27
  - No hacks — do it properly or don't do it
28
+ - No patches — fix the root cause, not the symptom
25
29
  - No TODOs — finish what you start
26
30
  - No fake data — real implementations, real integrations
27
31
  - No placeholders — every feature is complete
28
32
  - No dead code — remove what's unused
33
+ - No shortcuts — the right way is the only way
34
+
35
+ **Non-negotiable engineering qualities:**
36
+ - **Deduplication** — one source of truth for every piece of logic; extract and reuse ruthlessly
37
+ - **Modularisation** — single responsibility, clear boundaries, independently testable and deployable
38
+ - **Maintainability** — code must be readable, well-structured, and easy to change six months from now by someone who didn't write it
39
+ - **Composability** — build small, focused primitives that combine into powerful systems; prefer composition over inheritance at every layer
29
40
 
30
- State-of-the-art industrial standard. Every time. Would you stake your reputation on this? If not, keep going.
41
+ Would you stake your reputation on this? Would you ship this to a paying enterprise customer? If not, keep going.
31
42
 
32
43
  ## Mindset
33
44
 
@@ -52,6 +63,8 @@ State-of-the-art industrial standard. Every time. Would you stake your reputatio
52
63
 
53
64
  **Database & Infrastructure:** Neon PostgreSQL, Atlas (schema & migrations), Upstash Workflow, Vercel, Vercel Blob, Modal (serverless long-running)
54
65
 
66
+ **Infrastructure as Code:** Everything GitOps. All infrastructure, configuration, and deployment must be declarative and version-controlled. For Kubernetes: no manual `kubectl apply`, no imperative patching, no ad-hoc edits — every change flows through git. Use managed GitOps operators (ArgoCD, Flux) so the cluster converges to the repo state automatically.
67
+
55
68
  **UI & Styling:** Base UI, Tailwind CSS v4 (CSS-first), Motion v12 (animation)
56
69
 
57
70
  **Forms:** React Hook Form + @hookform/resolvers
@@ -94,6 +107,8 @@ State-of-the-art industrial standard. Every time. Would you stake your reputatio
94
107
  - Use TaskUpdate to mark in_progress when starting, completed when done
95
108
  - If interrupted, leave clear notes in task description
96
109
 
110
+ **Never stop mid-task.** Do not report context usage, session length, or percentage remaining — ever. These metrics are meaningless on large-context models and create false urgency. You have more than enough context. Auto-compaction handles memory management automatically. Your only job is to finish the task. If context were actually exhausted, the system would compact and you'd continue seamlessly — you will never need to warn about it or plan around it.
111
+
97
112
  **Document decisions.** Every significant choice needs rationale:
98
113
  - Why this approach over alternatives?
99
114
  - What trade-offs were considered?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/flow",
3
- "version": "3.24.2",
3
+ "version": "3.26.0",
4
4
  "description": "One CLI to rule them all. Unified orchestration layer for AI coding assistants. Auto-detection, auto-installation, auto-upgrade.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Sessions Command
3
+ *
4
+ * Lists Claude Code sessions for the current project so users can
5
+ * pick one to resume, instead of relying on Claude Code's unreliable picker.
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { Command } from 'commander';
10
+ import { type SessionInfo, listProjectSessions } from '../targets/functional/claude-session.js';
11
+
12
+ /**
13
+ * Format bytes into a human-readable size string.
14
+ */
15
+ function formatSize(bytes: number): string {
16
+ if (bytes < 1024) return `${bytes} B`;
17
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
18
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
19
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
20
+ }
21
+
22
+ /**
23
+ * Format a date into a relative time string.
24
+ */
25
+ function formatRelativeTime(date: Date): string {
26
+ const now = Date.now();
27
+ const diffMs = now - date.getTime();
28
+ const diffSec = Math.floor(diffMs / 1000);
29
+ const diffMin = Math.floor(diffSec / 60);
30
+ const diffHour = Math.floor(diffMin / 60);
31
+ const diffDay = Math.floor(diffHour / 24);
32
+
33
+ if (diffSec < 60) return 'just now';
34
+ if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
35
+ if (diffHour < 24) return `${diffHour} hour${diffHour === 1 ? '' : 's'} ago`;
36
+ if (diffDay === 1) return 'yesterday';
37
+ if (diffDay < 7) return `${diffDay} days ago`;
38
+ if (diffDay < 30) return `${Math.floor(diffDay / 7)} week${Math.floor(diffDay / 7) === 1 ? '' : 's'} ago`;
39
+ return date.toLocaleDateString();
40
+ }
41
+
42
+ /**
43
+ * Format a session row for the table output.
44
+ */
45
+ function formatSessionRow(session: SessionInfo, isLatest: boolean): string {
46
+ const id = chalk.white(session.id);
47
+ const time = formatRelativeTime(session.modifiedAt).padEnd(18);
48
+ const size = formatSize(session.sizeBytes).padStart(8);
49
+ const marker = isLatest ? chalk.green(' \u2190 latest') : '';
50
+
51
+ return ` ${id} ${chalk.dim(time)} ${chalk.dim(size)}${marker}`;
52
+ }
53
+
54
+ export const sessionsCommand = new Command('sessions')
55
+ .description('List Claude Code sessions for the current project')
56
+ .action(async () => {
57
+ const cwd = process.cwd();
58
+ const sessions = await listProjectSessions(cwd);
59
+
60
+ if (sessions.length === 0) {
61
+ console.log(chalk.yellow('\n No sessions found for this project.\n'));
62
+ console.log(chalk.dim(` Project: ${cwd}`));
63
+ console.log(chalk.dim(' Start a new session with: flow\n'));
64
+ return;
65
+ }
66
+
67
+ console.log(`\n${chalk.cyan.bold(' Sessions')} ${chalk.dim(`for ${cwd}`)}\n`);
68
+
69
+ // Header
70
+ console.log(
71
+ ` ${chalk.dim.bold('ID'.padEnd(36))} ${chalk.dim.bold('Last active'.padEnd(18))} ${chalk.dim.bold('Size'.padStart(8))}`
72
+ );
73
+ console.log(chalk.dim(` ${'─'.repeat(36)} ${'─'.repeat(18)} ${'─'.repeat(8)}`));
74
+
75
+ // Rows
76
+ for (let i = 0; i < sessions.length; i++) {
77
+ console.log(formatSessionRow(sessions[i], i === 0));
78
+ }
79
+
80
+ // Footer hint
81
+ const latestId = sessions[0].id;
82
+ const shortId = latestId.slice(0, 8);
83
+ console.log('');
84
+ console.log(chalk.dim(` Resume latest: ${chalk.white(`flow --resume`)}`));
85
+ console.log(chalk.dim(` Resume by ID: ${chalk.white(`flow --resume ${shortId}`)}`));
86
+ console.log('');
87
+ });
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  upgradeCommand,
18
18
  } from './commands/flow-command.js';
19
19
  import { hookCommand } from './commands/hook-command.js';
20
+ import { sessionsCommand } from './commands/sessions-command.js';
20
21
  import { settingsCommand } from './commands/settings-command.js';
21
22
  import { UserCancelledError } from './utils/errors.js';
22
23
 
@@ -73,6 +74,7 @@ export function createCLI(): Command {
73
74
  program.addCommand(doctorCommand);
74
75
  program.addCommand(upgradeCommand);
75
76
  program.addCommand(hookCommand);
77
+ program.addCommand(sessionsCommand);
76
78
  program.addCommand(settingsCommand);
77
79
 
78
80
  return program;
@@ -14,6 +14,7 @@ import {
14
14
  import { CLIError, ChildProcessExitError } from '../utils/errors.js';
15
15
  import { sanitize } from '../utils/security/security.js';
16
16
  import { DEFAULT_CLAUDE_CODE_ENV } from './functional/claude-code-logic.js';
17
+ import { findLastSessionId } from './functional/claude-session.js';
17
18
  import {
18
19
  detectTargetConfig,
19
20
  stripFrontMatter,
@@ -206,6 +207,24 @@ export const claudeCodeTarget: Target = {
206
207
 
207
208
  Please begin your response with a comprehensive summary of all the instructions and context provided above.`;
208
209
 
210
+ // Resolve session ID for --resume (auto-detect or passthrough)
211
+ let resolvedResumeId: string | undefined;
212
+ if (options.resume) {
213
+ if (typeof options.resume === 'string') {
214
+ resolvedResumeId = options.resume;
215
+ } else {
216
+ // Auto-resolve: find last session for current project
217
+ const lastSession = await findLastSessionId(process.cwd());
218
+ if (lastSession) {
219
+ resolvedResumeId = lastSession;
220
+ if (options.verbose) {
221
+ console.log(chalk.dim(` Resolved session: ${resolvedResumeId}`));
222
+ }
223
+ }
224
+ // If null, fall through to bare --resume (Claude's own picker as fallback)
225
+ }
226
+ }
227
+
209
228
  if (options.dryRun) {
210
229
  // Build the command for display
211
230
  const dryRunArgs = ['claude', '--dangerously-skip-permissions'];
@@ -217,8 +236,8 @@ Please begin your response with a comprehensive summary of all the instructions
217
236
  }
218
237
  if (options.resume) {
219
238
  dryRunArgs.push('--resume');
220
- if (typeof options.resume === 'string') {
221
- dryRunArgs.push(options.resume);
239
+ if (resolvedResumeId) {
240
+ dryRunArgs.push(resolvedResumeId);
222
241
  }
223
242
  }
224
243
  dryRunArgs.push('--system-prompt', '"<agent content>"');
@@ -250,8 +269,8 @@ Please begin your response with a comprehensive summary of all the instructions
250
269
  }
251
270
  if (options.resume) {
252
271
  args.push('--resume');
253
- if (typeof options.resume === 'string') {
254
- args.push(options.resume);
272
+ if (resolvedResumeId) {
273
+ args.push(resolvedResumeId);
255
274
  }
256
275
  }
257
276
 
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Claude Code Session Resolver
3
+ *
4
+ * Resolves session IDs from Claude Code's data files, bypassing the
5
+ * built-in session picker which is unreliable with large project directories.
6
+ *
7
+ * Data model:
8
+ * - ~/.claude/projects/{encoded-path}/ — session .jsonl files (filename = session ID)
9
+ * - ~/.claude/history.jsonl — global log with {timestamp, project, sessionId} per user message
10
+ * - Snapshot-only files have type: "file-history-snapshot" and are tiny (<5KB)
11
+ * - Real sessions have type: "user" messages and are much larger
12
+ */
13
+
14
+ import fs from 'node:fs/promises';
15
+ import { open } from 'node:fs/promises';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+
19
+ /** Minimum file size in bytes to consider a session file "real" (not just a snapshot) */
20
+ const MIN_SESSION_SIZE_BYTES = 2048;
21
+
22
+ /** Number of bytes to read from the tail of history.jsonl */
23
+ const HISTORY_TAIL_BYTES = 65_536;
24
+
25
+ /** Session info returned by listProjectSessions */
26
+ export interface SessionInfo {
27
+ id: string;
28
+ modifiedAt: Date;
29
+ sizeBytes: number;
30
+ }
31
+
32
+ /**
33
+ * Encode a project path the same way Claude Code does.
34
+ * /Users/kyle/flow → -Users-kyle-flow
35
+ */
36
+ function encodeProjectPath(projectPath: string): string {
37
+ return projectPath.replace(/\//g, '-');
38
+ }
39
+
40
+ /**
41
+ * Get the Claude Code home directory.
42
+ */
43
+ function getClaudeHome(): string {
44
+ return path.join(os.homedir(), '.claude');
45
+ }
46
+
47
+ /**
48
+ * Find the most recent session ID for a project by reading the tail of history.jsonl.
49
+ *
50
+ * Reads only the last ~64KB of the file (not the full 20MB+ file) for performance.
51
+ * Returns null if no sessions found for the given project path.
52
+ */
53
+ export async function findLastSessionId(projectPath: string): Promise<string | null> {
54
+ const historyPath = path.join(getClaudeHome(), 'history.jsonl');
55
+
56
+ let fd: Awaited<ReturnType<typeof open>> | null = null;
57
+ try {
58
+ fd = await open(historyPath, 'r');
59
+ const stat = await fd.stat();
60
+
61
+ if (stat.size === 0) {
62
+ return null;
63
+ }
64
+
65
+ // Read the tail of the file
66
+ const readSize = Math.min(HISTORY_TAIL_BYTES, stat.size);
67
+ const offset = stat.size - readSize;
68
+ const buffer = Buffer.alloc(readSize);
69
+ await fd.read(buffer, 0, readSize, offset);
70
+
71
+ const content = buffer.toString('utf-8');
72
+ const lines = content.split('\n').filter(Boolean);
73
+
74
+ // Walk backwards through lines to find the last session for this project
75
+ for (let i = lines.length - 1; i >= 0; i--) {
76
+ const line = lines[i];
77
+
78
+ // Skip partial lines at the start of the buffer (if we started mid-line)
79
+ if (i === 0 && offset > 0) {
80
+ // First line in buffer may be truncated — skip it
81
+ continue;
82
+ }
83
+
84
+ try {
85
+ const entry = JSON.parse(line);
86
+ if (entry.sessionId && entry.project === projectPath) {
87
+ return entry.sessionId;
88
+ }
89
+ } catch {
90
+ // Skip malformed lines
91
+ continue;
92
+ }
93
+ }
94
+
95
+ return null;
96
+ } catch (error) {
97
+ // File doesn't exist or can't be read — no sessions
98
+ if (isNodeError(error) && error.code === 'ENOENT') {
99
+ return null;
100
+ }
101
+ throw error;
102
+ } finally {
103
+ await fd?.close();
104
+ }
105
+ }
106
+
107
+ /**
108
+ * List all real sessions for a project from the project directory.
109
+ *
110
+ * Reads ~/.claude/projects/{encoded-path}/, stats each .jsonl file,
111
+ * filters out snapshot-only files (< 2KB), returns sorted by mtime descending.
112
+ */
113
+ export async function listProjectSessions(projectPath: string): Promise<SessionInfo[]> {
114
+ const encoded = encodeProjectPath(projectPath);
115
+ const projectDir = path.join(getClaudeHome(), 'projects', encoded);
116
+
117
+ let entries: Awaited<ReturnType<typeof fs.readdir>>;
118
+ try {
119
+ entries = await fs.readdir(projectDir);
120
+ } catch (error) {
121
+ if (isNodeError(error) && error.code === 'ENOENT') {
122
+ return [];
123
+ }
124
+ throw error;
125
+ }
126
+
127
+ const jsonlFiles = entries.filter((f) => f.endsWith('.jsonl'));
128
+
129
+ // Stat all files in parallel
130
+ const statResults = await Promise.all(
131
+ jsonlFiles.map(async (filename) => {
132
+ const filePath = path.join(projectDir, filename);
133
+ try {
134
+ const stat = await fs.stat(filePath);
135
+ return {
136
+ id: filename.replace(/\.jsonl$/, ''),
137
+ modifiedAt: stat.mtime,
138
+ sizeBytes: stat.size,
139
+ };
140
+ } catch {
141
+ // File may have been deleted between readdir and stat
142
+ return null;
143
+ }
144
+ })
145
+ );
146
+
147
+ return statResults
148
+ .filter((s): s is SessionInfo => s !== null && s.sizeBytes >= MIN_SESSION_SIZE_BYTES)
149
+ .sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
150
+ }
151
+
152
+ /** Type guard for Node.js errors with errno/code properties */
153
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
154
+ return error instanceof Error && 'code' in error;
155
+ }