@bamptee/aia-code 2.0.11 → 2.0.13

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 CHANGED
@@ -4,6 +4,19 @@ CLI tool that orchestrates AI-assisted development workflows using a `.aia` fold
4
4
 
5
5
  AIA structures your feature development into steps (brief, spec, tech-spec, dev-plan, implement, etc.), builds rich prompts from project context and knowledge files, and delegates execution to AI CLI tools (Claude Code, Codex CLI, Gemini CLI) with weighted random model selection.
6
6
 
7
+ ## Table of contents
8
+
9
+ - [Quick start](#quick-start)
10
+ - [Prerequisites](#prerequisites)
11
+ - [Commands](#commands)
12
+ - [Integrate into an existing project](#integrate-into-an-existing-project)
13
+ - [Web UI](#web-ui)
14
+ - [Feature workflow](#feature-workflow)
15
+ - [Prompt assembly](#prompt-assembly)
16
+ - [Project structure](#project-structure)
17
+ - [Dependencies](#dependencies)
18
+ - [Worktrunk Integration](#worktrunk-integration)
19
+
7
20
  ## Quick start
8
21
 
9
22
  ```bash
@@ -400,6 +413,37 @@ aia repo scan
400
413
 
401
414
  Generates `.aia/repo-map.json` -- a categorized index of your source files (services, models, routes, controllers, middleware, utils, config). Useful as additional context for prompts.
402
415
 
416
+ ## Web UI
417
+
418
+ Launch the local web interface to manage features visually:
419
+
420
+ ```bash
421
+ aia ui
422
+ # Opens http://localhost:3000
423
+ ```
424
+
425
+ ### Dashboard
426
+
427
+ - View all features with their current step and progress
428
+ - Create new features
429
+ - Delete features
430
+ - Quick access to run next step
431
+
432
+ ### Feature detail
433
+
434
+ - Execute steps with real-time log streaming (SSE)
435
+ - View step outputs (specs, plans, code)
436
+ - Reset steps to re-run them
437
+ - Edit `init.md` directly in the UI
438
+
439
+ ### Integrated terminal
440
+
441
+ The UI includes a full terminal emulator (xterm.js + node-pty). Open a shell directly in your project directory without leaving the browser.
442
+
443
+ ### Config editor
444
+
445
+ Edit your `.aia/config.yaml` directly in the UI with syntax highlighting and validation.
446
+
403
447
  ## Feature workflow
404
448
 
405
449
  Each feature follows a fixed pipeline of 8 steps:
@@ -470,48 +514,378 @@ The full prompt is piped to the CLI tool via stdin, so there are no argument len
470
514
 
471
515
  ```
472
516
  bin/
473
- aia.js # CLI entrypoint
517
+ aia.js # CLI entrypoint
474
518
  src/
475
- cli.js # Commander program, registers commands
476
- constants.js # Shared constants (dirs, steps, scan config)
477
- models.js # Config loader + validation, weighted model selection
478
- logger.js # Execution log writer
479
- knowledge-loader.js # Recursive markdown loader by category
480
- prompt-builder.js # Assembles full prompt from all sources
481
- utils.js # Shared filesystem helpers
519
+ cli.js # Commander program, registers commands
520
+ constants.js # Shared constants (dirs, steps, icons)
521
+ models.js # Config loader + validation, weighted model selection
522
+ logger.js # Execution log writer
523
+ knowledge-loader.js # Recursive markdown loader by category
524
+ prompt-builder.js # Assembles full prompt from all sources
525
+ utils.js # Shared filesystem helpers
482
526
  commands/
483
- init.js # aia init
484
- feature.js # aia feature <name>
485
- run.js # aia run <step> <feature>
486
- next.js # aia next <feature>
487
- iterate.js # aia iterate <step> <feature> <instructions>
488
- quick.js # aia quick <name> [description]
489
- status.js # aia status <feature>
490
- reset.js # aia reset <step> <feature>
491
- repo.js # aia repo scan
527
+ init.js # aia init
528
+ feature.js # aia feature <name>
529
+ run.js # aia run <step> <feature>
530
+ next.js # aia next <feature>
531
+ iterate.js # aia iterate <step> <feature> <instructions>
532
+ quick.js # aia quick <name> [description]
533
+ status.js # aia status <feature>
534
+ reset.js # aia reset <step> <feature>
535
+ repo.js # aia repo scan
536
+ ui.js # aia ui
492
537
  providers/
493
- registry.js # Model name + aliases -> provider routing
494
- cli-runner.js # Shared CLI spawn (streaming, idle timeout, verbose)
495
- openai.js # codex exec
496
- anthropic.js # claude -p
497
- gemini.js # gemini
538
+ registry.js # Model name + aliases -> provider routing
539
+ cli-runner.js # Shared CLI spawn (streaming, idle timeout, verbose)
540
+ openai.js # codex exec
541
+ anthropic.js # claude -p
542
+ gemini.js # gemini
498
543
  services/
499
- scaffold.js # .aia/ folder creation
500
- config.js # Default config generation
501
- feature.js # Feature workspace creation + validation
502
- status.js # status.yaml read/write/reset
503
- runner.js # Step execution orchestrator
504
- model-call.js # Provider dispatch
505
- repo-scan.js # Codebase scanner + categorizer
544
+ scaffold.js # .aia/ folder creation
545
+ config.js # Default config generation
546
+ feature.js # Feature workspace creation + validation
547
+ status.js # status.yaml read/write/reset
548
+ runner.js # Step execution orchestrator
549
+ model-call.js # Provider dispatch
550
+ repo-scan.js # Codebase scanner + categorizer
551
+ agent-sessions.js # Real-time agent session tracking (SSE)
552
+ apps.js # Monorepo app/submodule detection
553
+ worktrunk.js # Worktrunk git worktree integration
554
+ types/
555
+ test-quick.js # Type definitions and validators
556
+ ui/
557
+ server.js # Express server for web UI
558
+ router.js # API route registration
559
+ api/
560
+ features.js # Feature CRUD + step execution
561
+ config.js # Config read/write endpoints
562
+ worktrunk.js # Worktree management endpoints
563
+ logs.js # Log streaming
564
+ public/
565
+ index.html # SPA entry point
566
+ main.js # App initialization
567
+ components/
568
+ dashboard.js # Feature list + status overview
569
+ feature-detail.js # Step execution + outputs
570
+ config-view.js # Config editor
571
+ terminal.js # Integrated xterm terminal
572
+ worktrunk-panel.js # Worktree management UI
506
573
  ```
507
574
 
508
575
  ## Dependencies
509
576
 
510
- Only four runtime dependencies:
577
+ Runtime dependencies:
511
578
 
512
- - `commander` -- CLI framework
513
- - `yaml` -- YAML parse/stringify
514
- - `fs-extra` -- filesystem utilities
515
- - `chalk` -- terminal colors
579
+ | Package | Purpose |
580
+ |---------|---------|
581
+ | `commander` | CLI framework |
582
+ | `yaml` | YAML parse/stringify |
583
+ | `fs-extra` | Filesystem utilities |
584
+ | `chalk` | Terminal colors |
585
+ | `@iarna/toml` | TOML parsing (for `wt.toml`) |
586
+ | `ws` | WebSocket server (UI real-time updates) |
587
+ | `node-pty` | Pseudo-terminal (UI integrated terminal) |
588
+ | `xterm` + `xterm-addon-fit` | Terminal emulator (UI) |
589
+ | `busboy` | Multipart form parsing |
516
590
 
517
591
  AI calls use `child_process.spawn` to delegate to installed CLI tools. No API keys needed -- each CLI manages its own authentication.
592
+
593
+ ## Worktrunk Integration
594
+
595
+ AIA integrates with [Worktrunk](https://github.com/bamptee/worktrunk) (`wt`) to create isolated development environments for each feature using git worktrees.
596
+
597
+ ### Why Worktrunk?
598
+
599
+ - **Isolation**: Each feature gets its own directory and branch, no stashing needed
600
+ - **Services**: Run separate Docker containers per feature (database, cache, etc.)
601
+ - **Parallel work**: Work on multiple features simultaneously without conflicts
602
+ - **Clean state**: Delete the worktree when done, main branch stays untouched
603
+
604
+ ### Installation
605
+
606
+ ```bash
607
+ # Install Worktrunk CLI
608
+ cargo install worktrunk
609
+
610
+ # Verify installation
611
+ wt --version
612
+ ```
613
+
614
+ ### Quick Start
615
+
616
+ ```bash
617
+ # In the AIA UI, click "Create Worktree" on any feature
618
+ # Or via CLI:
619
+ wt switch -c feature/my-feature
620
+ ```
621
+
622
+ ### Configuration
623
+
624
+ Create `wt.toml` at the root of your project:
625
+
626
+ ```toml
627
+ # wt.toml - Worktrunk configuration
628
+
629
+ [worktree]
630
+ # Directory where worktrees are created (relative to repo root)
631
+ # Default: "../<repo-name>-wt"
632
+ base_path = "../my-project-wt"
633
+
634
+ # Branch prefix for feature worktrees
635
+ # AIA uses "feature/" by default
636
+ branch_prefix = "feature/"
637
+
638
+ [hooks]
639
+ # Hooks run automatically when creating/removing worktrees
640
+ # Available hooks: post_create, pre_remove, post_remove
641
+
642
+ # Run after worktree is created
643
+ post_create = [
644
+ "cp .env.example .env",
645
+ "docker-compose -f docker-compose.wt.yml up -d",
646
+ "npm install",
647
+ ]
648
+
649
+ # Run before worktree is removed
650
+ pre_remove = [
651
+ "docker-compose -f docker-compose.wt.yml down -v",
652
+ ]
653
+ ```
654
+
655
+ ### Docker Services per Feature
656
+
657
+ Create `docker-compose.wt.yml` for services that should run in each worktree:
658
+
659
+ ```yaml
660
+ # docker-compose.wt.yml - Services for isolated development
661
+
662
+ version: '3.8'
663
+
664
+ # Use environment variable for unique container names
665
+ # WT_BRANCH is set by worktrunk (e.g., "feature-my-feature")
666
+ x-branch: &branch ${WT_BRANCH:-dev}
667
+
668
+ services:
669
+ postgres:
670
+ image: postgres:16-alpine
671
+ container_name: ${WT_BRANCH:-dev}-postgres
672
+ environment:
673
+ POSTGRES_DB: myapp_dev
674
+ POSTGRES_USER: dev
675
+ POSTGRES_PASSWORD: dev
676
+ ports:
677
+ - "${DB_PORT:-5432}:5432"
678
+ volumes:
679
+ - postgres_data:/var/lib/postgresql/data
680
+
681
+ redis:
682
+ image: redis:7-alpine
683
+ container_name: ${WT_BRANCH:-dev}-redis
684
+ ports:
685
+ - "${REDIS_PORT:-6379}:6379"
686
+
687
+ mailhog:
688
+ image: mailhog/mailhog
689
+ container_name: ${WT_BRANCH:-dev}-mailhog
690
+ ports:
691
+ - "${MAIL_UI_PORT:-8025}:8025"
692
+ - "${MAIL_SMTP_PORT:-1025}:1025"
693
+
694
+ volumes:
695
+ postgres_data:
696
+ name: ${WT_BRANCH:-dev}-postgres-data
697
+ ```
698
+
699
+ ### Port Management
700
+
701
+ To avoid port conflicts between worktrees, use a `.env` file with dynamic ports:
702
+
703
+ ```bash
704
+ # .env.example - Copy to .env in each worktree
705
+
706
+ # Each worktree should use different ports
707
+ # Tip: Use feature hash or manual assignment
708
+ DB_PORT=5432
709
+ REDIS_PORT=6379
710
+ MAIL_UI_PORT=8025
711
+ MAIL_SMTP_PORT=1025
712
+ ```
713
+
714
+ Or use a hook to auto-assign ports:
715
+
716
+ ```toml
717
+ # wt.toml
718
+ [hooks]
719
+ post_create = [
720
+ # Generate random ports based on branch name hash
721
+ '''
722
+ HASH=$(echo "$WT_BRANCH" | md5sum | cut -c1-4)
723
+ PORT_OFFSET=$((16#$HASH % 1000))
724
+ cat > .env << EOF
725
+ DB_PORT=$((5432 + PORT_OFFSET))
726
+ REDIS_PORT=$((6379 + PORT_OFFSET))
727
+ MAIL_UI_PORT=$((8025 + PORT_OFFSET))
728
+ EOF
729
+ ''',
730
+ "docker-compose -f docker-compose.wt.yml up -d",
731
+ ]
732
+ ```
733
+
734
+ ### Full Example Setup
735
+
736
+ Here's a complete setup for a Node.js project with PostgreSQL, Redis, and S3 (MinIO):
737
+
738
+ ```
739
+ my-project/
740
+ ├── wt.toml # Worktrunk config
741
+ ├── docker-compose.wt.yml # Services template
742
+ ├── .env.example # Environment template
743
+ ├── scripts/
744
+ │ └── setup-worktree.sh # Custom setup script
745
+ └── .aia/
746
+ └── features/
747
+ └── my-feature/
748
+ ```
749
+
750
+ **wt.toml**:
751
+ ```toml
752
+ [worktree]
753
+ base_path = "../my-project-wt"
754
+
755
+ [hooks]
756
+ post_create = [
757
+ "bash scripts/setup-worktree.sh",
758
+ ]
759
+
760
+ pre_remove = [
761
+ "docker-compose -f docker-compose.wt.yml down -v --remove-orphans",
762
+ ]
763
+ ```
764
+
765
+ **scripts/setup-worktree.sh**:
766
+ ```bash
767
+ #!/bin/bash
768
+ set -e
769
+
770
+ echo "🔧 Setting up worktree: $WT_BRANCH"
771
+
772
+ # Copy environment template
773
+ cp .env.example .env
774
+
775
+ # Generate unique ports based on branch
776
+ HASH=$(echo "$WT_BRANCH" | md5sum | cut -c1-4)
777
+ OFFSET=$((16#$HASH % 900 + 100))
778
+
779
+ sed -i "s/DB_PORT=.*/DB_PORT=$((5000 + OFFSET))/" .env
780
+ sed -i "s/REDIS_PORT=.*/REDIS_PORT=$((6000 + OFFSET))/" .env
781
+ sed -i "s/MINIO_PORT=.*/MINIO_PORT=$((9000 + OFFSET))/" .env
782
+ sed -i "s/APP_PORT=.*/APP_PORT=$((3000 + OFFSET))/" .env
783
+
784
+ echo "📦 Starting Docker services..."
785
+ docker-compose -f docker-compose.wt.yml up -d
786
+
787
+ echo "📚 Installing dependencies..."
788
+ npm install
789
+
790
+ echo "🗃️ Running migrations..."
791
+ npm run db:migrate
792
+
793
+ echo "✅ Worktree ready!"
794
+ echo " App: http://localhost:$((3000 + OFFSET))"
795
+ echo " Database: localhost:$((5000 + OFFSET))"
796
+ ```
797
+
798
+ **docker-compose.wt.yml**:
799
+ ```yaml
800
+ version: '3.8'
801
+
802
+ services:
803
+ postgres:
804
+ image: postgres:16-alpine
805
+ container_name: ${WT_BRANCH:-dev}-postgres
806
+ environment:
807
+ POSTGRES_DB: app_dev
808
+ POSTGRES_USER: dev
809
+ POSTGRES_PASSWORD: dev
810
+ ports:
811
+ - "${DB_PORT:-5432}:5432"
812
+ volumes:
813
+ - pg_data:/var/lib/postgresql/data
814
+ healthcheck:
815
+ test: ["CMD-SHELL", "pg_isready -U dev"]
816
+ interval: 5s
817
+ timeout: 5s
818
+ retries: 5
819
+
820
+ redis:
821
+ image: redis:7-alpine
822
+ container_name: ${WT_BRANCH:-dev}-redis
823
+ ports:
824
+ - "${REDIS_PORT:-6379}:6379"
825
+ healthcheck:
826
+ test: ["CMD", "redis-cli", "ping"]
827
+ interval: 5s
828
+ timeout: 5s
829
+ retries: 5
830
+
831
+ minio:
832
+ image: minio/minio
833
+ container_name: ${WT_BRANCH:-dev}-minio
834
+ command: server /data --console-address ":9001"
835
+ environment:
836
+ MINIO_ROOT_USER: minioadmin
837
+ MINIO_ROOT_PASSWORD: minioadmin
838
+ ports:
839
+ - "${MINIO_PORT:-9000}:9000"
840
+ - "${MINIO_CONSOLE_PORT:-9001}:9001"
841
+ volumes:
842
+ - minio_data:/data
843
+
844
+ volumes:
845
+ pg_data:
846
+ name: ${WT_BRANCH:-dev}-pg-data
847
+ minio_data:
848
+ name: ${WT_BRANCH:-dev}-minio-data
849
+ ```
850
+
851
+ ### Using Worktrunk in AIA UI
852
+
853
+ 1. **Create a feature**: `aia feature my-feature` or via UI
854
+ 2. **Open the feature** in the UI
855
+ 3. **Click "Create Worktree"** in the Worktrunk panel
856
+ - Runs `wt switch -c feature/my-feature`
857
+ - Executes `post_create` hooks (Docker services, npm install, etc.)
858
+ 4. **Open Terminal** to work in the worktree directory
859
+ 5. **View Docker Containers** directly in the UI
860
+ - Start/Stop individual containers
861
+ - Open a shell inside any running container
862
+ 6. **When done**: Click "Remove" to clean up
863
+ - Runs `pre_remove` hooks (docker-compose down)
864
+ - Removes the worktree directory
865
+
866
+ ### Troubleshooting
867
+
868
+ **"Worktrunk not installed"**
869
+ ```bash
870
+ cargo install worktrunk
871
+ # Make sure ~/.cargo/bin is in your PATH
872
+ ```
873
+
874
+ **Containers not showing in UI**
875
+ - Containers must have names matching pattern: `feature-<name>-*`
876
+ - Check Docker is running: `docker ps`
877
+ - Click "Refresh Containers" in the UI
878
+
879
+ **Port conflicts**
880
+ - Each worktree needs unique ports
881
+ - Use the port auto-assignment hook above
882
+ - Or manually set ports in `.env` per worktree
883
+
884
+ **Worktree creation fails**
885
+ ```bash
886
+ # Check git status - uncommitted changes can block
887
+ git status
888
+
889
+ # Manual worktree creation
890
+ wt switch -c feature/my-feature --force
891
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamptee/aia-code",
3
- "version": "2.0.11",
3
+ "version": "2.0.13",
4
4
  "description": "AI Architecture Assistant - orchestrate AI-assisted development workflows via CLI tools (Claude, Codex, Gemini)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,16 @@
30
30
  "url": "git+https://github.com/bamptee/aia-code.git"
31
31
  },
32
32
  "scripts": {
33
- "start": "node bin/aia.js"
33
+ "start": "node bin/aia.js",
34
+ "test": "node --test tests/*.test.js",
35
+ "test:service": "node --test tests/test-quick.service.test.js",
36
+ "test:api": "node --test tests/test-quick.api.test.js",
37
+ "test:delete": "node --test tests/feature-delete.*.test.js",
38
+ "test:delete:service": "node --test tests/feature-delete.service.test.js",
39
+ "test:delete:api": "node --test tests/feature-delete.api.test.js",
40
+ "test:ux": "node --test tests/feature-list-ux.*.test.js",
41
+ "test:ux:unit": "node --test tests/feature-list-ux.unit.test.js",
42
+ "test:ux:api": "node --test tests/feature-list-ux.api.test.js"
34
43
  },
35
44
  "dependencies": {
36
45
  "@iarna/toml": "^2.2.5",
package/src/constants.js CHANGED
@@ -48,3 +48,27 @@ export const STEP_STATUS = {
48
48
  DONE: 'done',
49
49
  ERROR: 'error',
50
50
  };
51
+
52
+ export const FEATURE_TYPES = Object.freeze(['feature', 'bug']);
53
+ export const DEFAULT_FEATURE_TYPE = 'feature';
54
+
55
+ /**
56
+ * Feature deletion status filter options
57
+ */
58
+ export const DELETION_FILTER = Object.freeze({
59
+ ACTIVE: 'active',
60
+ DELETED: 'deleted',
61
+ ALL: 'all',
62
+ });
63
+
64
+ export const APP_ICONS = {
65
+ react: '\u269B',
66
+ vue: '\uD83D\uDC9A',
67
+ angular: '\uD83C\uDD70',
68
+ node: '\uD83D\uDCE6',
69
+ go: '\uD83D\uDD27',
70
+ java: '\u2615',
71
+ python: '\uD83D\uDC0D',
72
+ rust: '\uD83E\uDD80',
73
+ generic: '\uD83D\uDCC1',
74
+ };
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import chalk from 'chalk';
6
6
  import { runCli } from './cli-runner.js';
7
7
 
8
- export async function generate(prompt, model, { verbose = false, apply = false, onData } = {}) {
8
+ export async function generate(prompt, model, { verbose = false, apply = false, onData, cwd } = {}) {
9
9
  const args = ['-p'];
10
10
  if (model) {
11
11
  args.push('--model', model);
@@ -17,24 +17,5 @@ export async function generate(prompt, model, { verbose = false, apply = false,
17
17
  args.push('--verbose');
18
18
  }
19
19
 
20
- // In agent mode, large prompts via stdin can hang Claude CLI.
21
- // Write context to a temp file and give a short prompt to read it.
22
- if (apply) {
23
- const tmpFile = path.join(tmpdir(), `aia-prompt-${randomBytes(6).toString('hex')}.md`);
24
- writeFileSync(tmpFile, prompt, 'utf-8');
25
- console.error(chalk.gray(`[AI] Prompt written to temp file (${(prompt.length / 1024).toFixed(1)}KB): ${tmpFile}`));
26
- const shortPrompt = `Read the file at ${tmpFile} — it contains your full instructions and context. Follow them exactly.`;
27
- args.push('-');
28
- console.error(chalk.gray(`[AI] Spawning: claude ${args.join(' ')}`));
29
- console.error(chalk.gray(`[AI] Waiting for Claude agent response...`));
30
- try {
31
- return await runCli('claude', args, { stdin: shortPrompt, verbose: verbose || apply, apply, onData });
32
- } finally {
33
- try { unlinkSync(tmpFile); } catch {}
34
- }
35
- }
36
-
37
- args.push('-');
38
- console.error(chalk.gray(`[AI] Spawning: claude ${args.join(' ')} (${(prompt.length / 1024).toFixed(1)}KB prompt)`));
39
- return runCli('claude', args, { stdin: prompt, verbose: verbose || apply, apply, onData });
20
+ return runCli('claude', args, { stdin: prompt, verbose: verbose || apply, apply, onData, cwd });
40
21
  }
@@ -1,10 +1,11 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { Readable } from 'node:stream';
2
3
  import chalk from 'chalk';
3
4
 
4
5
  const DEFAULT_IDLE_TIMEOUT_MS = 180_000;
5
6
  const AGENT_IDLE_TIMEOUT_MS = 600_000;
6
7
 
7
- export function runCli(command, args, { stdin: stdinData, verbose = false, apply = false, idleTimeoutMs, onData } = {}) {
8
+ export function runCli(command, args, { stdin: stdinData, verbose = false, apply = false, idleTimeoutMs, onData, cwd } = {}) {
8
9
  if (!idleTimeoutMs) {
9
10
  idleTimeoutMs = apply ? AGENT_IDLE_TIMEOUT_MS : DEFAULT_IDLE_TIMEOUT_MS;
10
11
  }
@@ -12,7 +13,8 @@ export function runCli(command, args, { stdin: stdinData, verbose = false, apply
12
13
  const { CLAUDECODE, ...cleanEnv } = process.env;
13
14
  const child = spawn(command, args, {
14
15
  stdio: ['pipe', 'pipe', 'pipe'],
15
- env: { ...cleanEnv, FORCE_COLOR: '0' },
16
+ env: { ...process.env, FORCE_COLOR: '0' },
17
+ cwd,
16
18
  });
17
19
 
18
20
  const chunks = [];
@@ -45,7 +47,7 @@ export function runCli(command, args, { stdin: stdinData, verbose = false, apply
45
47
  console.error(chalk.gray('[AI] First stdout received — agent is running'));
46
48
  }
47
49
  const text = data.toString();
48
- process.stdout.write(text);
50
+ if (verbose) process.stdout.write(text);
49
51
  chunks.push(text);
50
52
  if (onData) onData({ type: 'stdout', text });
51
53
  resetTimer();
@@ -0,0 +1,110 @@
1
+ // Agent session tracking - in-memory Map with log buffer
2
+ // Map<featureName, { step, startedAt, logs: Array<{text, type, ts}>, sseClients: Set<Response> }>
3
+ const sessions = new Map();
4
+ const MAX_LOGS = 500;
5
+
6
+ /**
7
+ * Start a new agent session for a feature
8
+ * @param {string} feature - Feature name
9
+ * @param {string} step - Step name
10
+ */
11
+ export function startSession(feature, step) {
12
+ sessions.set(feature, {
13
+ step,
14
+ startedAt: Date.now(),
15
+ logs: [],
16
+ sseClients: new Set(),
17
+ });
18
+ }
19
+
20
+ /**
21
+ * End an agent session and notify all SSE clients
22
+ * @param {string} feature - Feature name
23
+ */
24
+ export function endSession(feature) {
25
+ const session = sessions.get(feature);
26
+ if (session) {
27
+ // Notify all SSE clients that session ended
28
+ for (const client of session.sseClients) {
29
+ try {
30
+ client.write(`event: done\ndata: {}\n\n`);
31
+ } catch {
32
+ // Client may have disconnected
33
+ }
34
+ }
35
+ sessions.delete(feature);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Get an active session for a feature
41
+ * @param {string} feature - Feature name
42
+ * @returns {Object|null} Session object or null
43
+ */
44
+ export function getSession(feature) {
45
+ return sessions.get(feature) || null;
46
+ }
47
+
48
+ /**
49
+ * Append a log entry to a session's buffer
50
+ * @param {string} feature - Feature name
51
+ * @param {string} text - Log text
52
+ * @param {string} type - Log type ('stdout' or 'stderr')
53
+ */
54
+ export function appendLog(feature, text, type = 'stdout') {
55
+ const session = sessions.get(feature);
56
+ if (!session) return;
57
+
58
+ session.logs.push({ text, type, ts: Date.now() });
59
+ if (session.logs.length > MAX_LOGS) session.logs.shift();
60
+
61
+ // Broadcast to all SSE clients
62
+ for (const client of session.sseClients) {
63
+ try {
64
+ client.write(`event: log\ndata: ${JSON.stringify({ text, type })}\n\n`);
65
+ } catch {
66
+ // Client may have disconnected
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Register an SSE client for a session
73
+ * @param {string} feature - Feature name
74
+ * @param {Response} res - HTTP response object
75
+ */
76
+ export function addSseClient(feature, res) {
77
+ const session = sessions.get(feature);
78
+ if (session) session.sseClients.add(res);
79
+ }
80
+
81
+ /**
82
+ * Unregister an SSE client from a session
83
+ * @param {string} feature - Feature name
84
+ * @param {Response} res - HTTP response object
85
+ */
86
+ export function removeSseClient(feature, res) {
87
+ const session = sessions.get(feature);
88
+ if (session) session.sseClients.delete(res);
89
+ }
90
+
91
+ /**
92
+ * Check if a feature has an active session
93
+ * @param {string} feature - Feature name
94
+ * @returns {boolean}
95
+ */
96
+ export function isRunning(feature) {
97
+ return sessions.has(feature);
98
+ }
99
+
100
+ /**
101
+ * Get all running sessions (for dashboard)
102
+ * @returns {Object} Map of feature name to session summary
103
+ */
104
+ export function getAllRunningSessions() {
105
+ const result = {};
106
+ for (const [feature, session] of sessions) {
107
+ result[feature] = { step: session.step, startedAt: session.startedAt };
108
+ }
109
+ return result;
110
+ }