@relipa/ai-flow-kit 0.0.7-beta.2 → 0.0.8-beta.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.
@@ -1,46 +1,46 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const chalk = require('chalk');
4
-
5
- const PROJECT_DIR = process.cwd();
6
- const AIFLOW_DIR = path.join(PROJECT_DIR, '.aiflow');
7
- const CONTEXT_FILE = path.join(AIFLOW_DIR, 'context', 'current.json');
8
-
9
- const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
10
-
11
- module.exports = async function checkpoint(options = {}) {
12
- const { gate, step, tokens, ticket: ticketOpt } = options;
13
-
14
- if (!gate || !step || tokens === undefined) {
15
- console.log(chalk.yellow('Usage: aiflow checkpoint --gate <N> --step <name> --tokens <N>'));
16
- console.log(chalk.gray(' Example: aiflow checkpoint --gate 3 --step "tests-written" --tokens 3200'));
17
- return;
18
- }
19
-
20
- // Resolve ticket ID
21
- let ticketId = ticketOpt;
22
- if (!ticketId) {
23
- const ctx = await fs.readJson(CONTEXT_FILE).catch(() => null);
24
- ticketId = ctx && ctx.taskId ? ctx.taskId : 'unknown';
25
- }
26
-
27
- const taskDir = path.join(AIFLOW_DIR, 'tasks', ticketId);
28
- await fs.ensureDir(taskDir);
29
-
30
- const checkpointPath = path.join(taskDir, 'checkpoints.json');
31
- const existing = (await fs.pathExists(checkpointPath))
32
- ? await fs.readJson(checkpointPath).catch(() => ({ checkpoints: [] }))
33
- : { checkpoints: [] };
34
-
35
- const entry = {
36
- gate: parseInt(gate),
37
- step,
38
- tokens: parseInt(tokens),
39
- timestamp: new Date().toISOString(),
40
- };
41
- existing.checkpoints.push(entry);
42
- await fs.writeJson(checkpointPath, existing, { spaces: 2 });
43
-
44
- const label = GATE_LABELS[parseInt(gate)] || `Gate ${gate}`;
45
- console.log(chalk.green(`✓ Checkpoint: Gate ${gate} (${label}) — ${step} — ~${parseInt(tokens).toLocaleString()} tokens`));
46
- };
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ const PROJECT_DIR = process.cwd();
6
+ const AIFLOW_DIR = path.join(PROJECT_DIR, '.aiflow');
7
+ const CONTEXT_FILE = path.join(AIFLOW_DIR, 'context', 'current.json');
8
+
9
+ const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
10
+
11
+ module.exports = async function checkpoint(options = {}) {
12
+ const { gate, step, tokens, ticket: ticketOpt } = options;
13
+
14
+ if (!gate || !step || tokens === undefined) {
15
+ console.log(chalk.yellow('Usage: aiflow checkpoint --gate <N> --step <name> --tokens <N>'));
16
+ console.log(chalk.gray(' Example: aiflow checkpoint --gate 3 --step "tests-written" --tokens 3200'));
17
+ return;
18
+ }
19
+
20
+ // Resolve ticket ID
21
+ let ticketId = ticketOpt;
22
+ if (!ticketId) {
23
+ const ctx = await fs.readJson(CONTEXT_FILE).catch(() => null);
24
+ ticketId = ctx && ctx.taskId ? ctx.taskId : 'unknown';
25
+ }
26
+
27
+ const taskDir = path.join(AIFLOW_DIR, 'tasks', ticketId);
28
+ await fs.ensureDir(taskDir);
29
+
30
+ const checkpointPath = path.join(taskDir, 'checkpoints.json');
31
+ const existing = (await fs.pathExists(checkpointPath))
32
+ ? await fs.readJson(checkpointPath).catch(() => ({ checkpoints: [] }))
33
+ : { checkpoints: [] };
34
+
35
+ const entry = {
36
+ gate: parseInt(gate),
37
+ step,
38
+ tokens: parseInt(tokens),
39
+ timestamp: new Date().toISOString(),
40
+ };
41
+ existing.checkpoints.push(entry);
42
+ await fs.writeJson(checkpointPath, existing, { spaces: 2 });
43
+
44
+ const label = GATE_LABELS[parseInt(gate)] || `Gate ${gate}`;
45
+ console.log(chalk.green(`✓ Checkpoint: Gate ${gate} (${label}) — ${step} — ~${parseInt(tokens).toLocaleString()} tokens`));
46
+ };
@@ -1,94 +1,94 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Background worker: runs `npx gitnexus analyze` and writes result to
4
- * .aiflow/gitnexus-status.json so session-start.js / use.js can notify the developer.
5
- *
6
- * Spawned as a detached process by init.js — never run directly.
7
- * Usage: node gitnexus-worker.js <projectDir>
8
- */
9
-
10
- const { spawn, execSync } = require('child_process');
11
- const fs = require('fs');
12
- const path = require('path');
13
-
14
- function commandExists(cmd) {
15
- try {
16
- const isWin = process.platform === 'win32';
17
- const checkCmd = isWin ? `where ${cmd}` : `which ${cmd}`;
18
- execSync(checkCmd, { stdio: 'ignore' });
19
- return true;
20
- } catch (_) {
21
- return false;
22
- }
23
- }
24
-
25
- const projectDir = process.argv[2];
26
- if (!projectDir) process.exit(1);
27
-
28
- const statusPath = path.join(projectDir, '.aiflow', 'gitnexus-status.json');
29
- const logPath = path.join(projectDir, '.aiflow', 'gitnexus-analyze.log');
30
-
31
- // Preserve startedAt written by init.js
32
- let startedAt = new Date().toISOString();
33
- try {
34
- if (fs.existsSync(statusPath)) {
35
- const existing = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
36
- if (existing.startedAt) startedAt = existing.startedAt;
37
- }
38
- } catch (_) {}
39
-
40
- // gitnexus requires Node >=20
41
- const nodeMajor = parseInt(process.version.slice(1).split('.')[0], 10);
42
- if (nodeMajor < 20) {
43
- try {
44
- fs.writeFileSync(statusPath, JSON.stringify({
45
- status: 'error',
46
- startedAt,
47
- completedAt: new Date().toISOString(),
48
- error: `Node.js version >=20 required (current: ${process.version})`,
49
- }, null, 2));
50
- } catch (_) {}
51
- process.exit(0);
52
- }
53
-
54
- // Status path and log path already defined above
55
-
56
- const logStream = fs.createWriteStream(logPath, { flags: 'w' });
57
-
58
- // On Windows use shell:true — avoids EINVAL when spawning .cmd scripts
59
- const isWin = process.platform === 'win32';
60
-
61
- const hasGlobalGitNexus = commandExists('gitnexus');
62
- const cmd = hasGlobalGitNexus ? 'gitnexus' : 'npx';
63
- const args = hasGlobalGitNexus ? ['analyze'] : ['-y', 'gitnexus@latest', 'analyze'];
64
-
65
- const child = spawn(cmd, args, {
66
- cwd: projectDir,
67
- shell: isWin,
68
- windowsHide: true,
69
- stdio: ['ignore', 'pipe', 'pipe'],
70
- });
71
-
72
- child.stdout.pipe(logStream);
73
- child.stderr.pipe(logStream);
74
-
75
- child.on('close', (code) => {
76
- const status = code === 0
77
- ? { status: 'done', startedAt, completedAt: new Date().toISOString() }
78
- : { status: 'error', startedAt, completedAt: new Date().toISOString(), exitCode: code };
79
- try {
80
- fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
81
- } catch (_) {}
82
- });
83
-
84
- child.on('error', (err) => {
85
- try {
86
- fs.writeFileSync(statusPath, JSON.stringify({
87
- status: 'error',
88
- startedAt,
89
- completedAt: new Date().toISOString(),
90
- exitCode: null,
91
- error: err.message,
92
- }, null, 2));
93
- } catch (_) {}
94
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Background worker: runs `npx gitnexus analyze` and writes result to
4
+ * .aiflow/gitnexus-status.json so session-start.js / use.js can notify the developer.
5
+ *
6
+ * Spawned as a detached process by init.js — never run directly.
7
+ * Usage: node gitnexus-worker.js <projectDir>
8
+ */
9
+
10
+ const { spawn, execSync } = require('child_process');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ function commandExists(cmd) {
15
+ try {
16
+ const isWin = process.platform === 'win32';
17
+ const checkCmd = isWin ? `where ${cmd}` : `which ${cmd}`;
18
+ execSync(checkCmd, { stdio: 'ignore' });
19
+ return true;
20
+ } catch (_) {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ const projectDir = process.argv[2];
26
+ if (!projectDir) process.exit(1);
27
+
28
+ const statusPath = path.join(projectDir, '.aiflow', 'gitnexus-status.json');
29
+ const logPath = path.join(projectDir, '.aiflow', 'gitnexus-analyze.log');
30
+
31
+ // Preserve startedAt written by init.js
32
+ let startedAt = new Date().toISOString();
33
+ try {
34
+ if (fs.existsSync(statusPath)) {
35
+ const existing = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
36
+ if (existing.startedAt) startedAt = existing.startedAt;
37
+ }
38
+ } catch (_) {}
39
+
40
+ // gitnexus requires Node >=20
41
+ const nodeMajor = parseInt(process.version.slice(1).split('.')[0], 10);
42
+ if (nodeMajor < 20) {
43
+ try {
44
+ fs.writeFileSync(statusPath, JSON.stringify({
45
+ status: 'error',
46
+ startedAt,
47
+ completedAt: new Date().toISOString(),
48
+ error: `Node.js version >=20 required (current: ${process.version})`,
49
+ }, null, 2));
50
+ } catch (_) {}
51
+ process.exit(0);
52
+ }
53
+
54
+ // Status path and log path already defined above
55
+
56
+ const logStream = fs.createWriteStream(logPath, { flags: 'w' });
57
+
58
+ // On Windows use shell:true — avoids EINVAL when spawning .cmd scripts
59
+ const isWin = process.platform === 'win32';
60
+
61
+ const hasGlobalGitNexus = commandExists('gitnexus');
62
+ const cmd = hasGlobalGitNexus ? 'gitnexus' : 'npx';
63
+ const args = hasGlobalGitNexus ? ['analyze'] : ['-y', 'gitnexus@latest', 'analyze'];
64
+
65
+ const child = spawn(cmd, args, {
66
+ cwd: projectDir,
67
+ shell: isWin,
68
+ windowsHide: true,
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ });
71
+
72
+ child.stdout.pipe(logStream);
73
+ child.stderr.pipe(logStream);
74
+
75
+ child.on('close', (code) => {
76
+ const status = code === 0
77
+ ? { status: 'done', startedAt, completedAt: new Date().toISOString() }
78
+ : { status: 'error', startedAt, completedAt: new Date().toISOString(), exitCode: code };
79
+ try {
80
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
81
+ } catch (_) {}
82
+ });
83
+
84
+ child.on('error', (err) => {
85
+ try {
86
+ fs.writeFileSync(statusPath, JSON.stringify({
87
+ status: 'error',
88
+ startedAt,
89
+ completedAt: new Date().toISOString(),
90
+ exitCode: null,
91
+ error: err.message,
92
+ }, null, 2));
93
+ } catch (_) {}
94
+ });
@@ -1,55 +1,55 @@
1
- #!/usr/bin/env node
2
- /**
3
- * SessionStop hook for ai-flow-kit
4
- * Reads checkpoints.json for the active task and prints a token usage summary.
5
- */
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
- const { record } = require('../telemetry/record');
10
-
11
- record('session.stop');
12
-
13
- const projectRoot = path.resolve(__dirname, '..', '..');
14
- const contextFile = path.join(projectRoot, '.aiflow', 'context', 'current.json');
15
- const tasksDir = path.join(projectRoot, '.aiflow', 'tasks');
16
-
17
- const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
18
-
19
- try {
20
- if (!fs.existsSync(contextFile)) process.exit(0);
21
- const ctx = JSON.parse(fs.readFileSync(contextFile, 'utf-8'));
22
- const ticketId = ctx && ctx.taskId;
23
- if (!ticketId) process.exit(0);
24
-
25
- const checkpointPath = path.join(tasksDir, ticketId, 'checkpoints.json');
26
- if (!fs.existsSync(checkpointPath)) process.exit(0);
27
-
28
- const data = JSON.parse(fs.readFileSync(checkpointPath, 'utf-8'));
29
- const checkpoints = data.checkpoints || [];
30
- if (checkpoints.length === 0) process.exit(0);
31
-
32
- // Aggregate latest token value per gate
33
- const gateTokens = {};
34
- for (const cp of checkpoints) {
35
- const g = cp.gate;
36
- if (!gateTokens[g] || cp.tokens > gateTokens[g]) {
37
- gateTokens[g] = cp.tokens;
38
- }
39
- }
40
-
41
- const gates = Object.keys(gateTokens).sort();
42
- const total = gates.reduce((sum, g) => sum + gateTokens[g], 0);
43
-
44
- process.stderr.write('\n[aiflow] Token usage this session:\n');
45
- for (const g of gates) {
46
- const label = GATE_LABELS[parseInt(g)] || `Gate ${g}`;
47
- const t = gateTokens[g];
48
- const bar = '█'.repeat(Math.round((t / total) * 20)).padEnd(20, '░');
49
- const pct = Math.round((t / total) * 100);
50
- process.stderr.write(`[aiflow] Gate ${g} ${label.padEnd(8)} ~${t.toLocaleString().padStart(6)} ${bar} ${pct}%\n`);
51
- }
52
- process.stderr.write(`[aiflow] Total estimated: ~${total.toLocaleString()} tokens\n\n`);
53
- } catch (_) {}
54
-
55
- process.exit(0);
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStop hook for ai-flow-kit
4
+ * Reads checkpoints.json for the active task and prints a token usage summary.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { record } = require('../telemetry/record');
10
+
11
+ record('session.stop');
12
+
13
+ const projectRoot = path.resolve(__dirname, '..', '..');
14
+ const contextFile = path.join(projectRoot, '.aiflow', 'context', 'current.json');
15
+ const tasksDir = path.join(projectRoot, '.aiflow', 'tasks');
16
+
17
+ const GATE_LABELS = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
18
+
19
+ try {
20
+ if (!fs.existsSync(contextFile)) process.exit(0);
21
+ const ctx = JSON.parse(fs.readFileSync(contextFile, 'utf-8'));
22
+ const ticketId = ctx && ctx.taskId;
23
+ if (!ticketId) process.exit(0);
24
+
25
+ const checkpointPath = path.join(tasksDir, ticketId, 'checkpoints.json');
26
+ if (!fs.existsSync(checkpointPath)) process.exit(0);
27
+
28
+ const data = JSON.parse(fs.readFileSync(checkpointPath, 'utf-8'));
29
+ const checkpoints = data.checkpoints || [];
30
+ if (checkpoints.length === 0) process.exit(0);
31
+
32
+ // Aggregate latest token value per gate
33
+ const gateTokens = {};
34
+ for (const cp of checkpoints) {
35
+ const g = cp.gate;
36
+ if (!gateTokens[g] || cp.tokens > gateTokens[g]) {
37
+ gateTokens[g] = cp.tokens;
38
+ }
39
+ }
40
+
41
+ const gates = Object.keys(gateTokens).sort();
42
+ const total = gates.reduce((sum, g) => sum + gateTokens[g], 0);
43
+
44
+ process.stderr.write('\n[aiflow] Token usage this session:\n');
45
+ for (const g of gates) {
46
+ const label = GATE_LABELS[parseInt(g)] || `Gate ${g}`;
47
+ const t = gateTokens[g];
48
+ const bar = '█'.repeat(Math.round((t / total) * 20)).padEnd(20, '░');
49
+ const pct = Math.round((t / total) * 100);
50
+ process.stderr.write(`[aiflow] Gate ${g} ${label.padEnd(8)} ~${t.toLocaleString().padStart(6)} ${bar} ${pct}%\n`);
51
+ }
52
+ process.stderr.write(`[aiflow] Total estimated: ~${total.toLocaleString()} tokens\n\n`);
53
+ } catch (_) {}
54
+
55
+ process.exit(0);
package/scripts/init.js CHANGED
@@ -24,7 +24,7 @@ function commandExists(cmd) {
24
24
  // Map framework → language family for picking the right rules
25
25
  const FRAMEWORK_LANGUAGE = {
26
26
  'laravel': 'php',
27
- 'php-plain': 'php',
27
+ 'php': 'php',
28
28
  'spring-boot': 'java',
29
29
  'reactjs': 'javascript',
30
30
  'nextjs': 'javascript',
@@ -80,7 +80,7 @@ async function detectFramework(projectDir) {
80
80
  if (await fs.pathExists(composerPath)) {
81
81
  const composer = await fs.readJson(composerPath).catch(() => ({}));
82
82
  if ((composer.require || {})['laravel/framework']) return 'laravel';
83
- return 'php-plain';
83
+ return 'php';
84
84
  }
85
85
  // Python
86
86
  if (await fs.pathExists(path.join(projectDir, 'manage.py'))) return 'python-django';
@@ -89,7 +89,9 @@ async function detectFramework(projectDir) {
89
89
  const reqs = (await fs.readFile(reqPath, 'utf-8').catch(() => '')).toLowerCase();
90
90
  if (reqs.includes('fastapi')) return 'python-fastapi';
91
91
  if (reqs.includes('django')) return 'python-django';
92
+ return 'python';
92
93
  }
94
+ if (await fs.pathExists(path.join(projectDir, 'pyproject.toml'))) return 'python';
93
95
  return null;
94
96
  }
95
97
 
@@ -99,9 +101,10 @@ const FRAMEWORK_CHOICES = [
99
101
  { name: 'Next.js', value: 'nextjs' },
100
102
  { name: 'Vue / Nuxt', value: 'vue-nuxt' },
101
103
  { name: 'Laravel (PHP)', value: 'laravel' },
102
- { name: 'PHP (no framework)', value: 'php-plain' },
104
+ { name: 'PHP', value: 'php' },
103
105
  { name: 'NestJS', value: 'nestjs' },
104
106
  { name: 'Node.js / Express', value: 'nodejs-express' },
107
+ { name: 'Python', value: 'python' },
105
108
  { name: 'Python Django', value: 'python-django' },
106
109
  { name: 'Python FastAPI', value: 'python-fastapi' },
107
110
  ];
@@ -468,7 +471,7 @@ async function setupAdapter(projectDir, adapter) {
468
471
  }
469
472
  globalData.mcp = { ...(globalData.mcp || {}), ...credentials };
470
473
  await fs.writeJson(GLOBAL_CREDENTIALS_PATH, globalData, { spaces: 2 });
471
- await ensureCredentialsGitignored(projectDir);
474
+ await ensureAiflowGitignored(projectDir);
472
475
 
473
476
  console.log(chalk.green(`✓ MCP configuration saved (global + local + credentials).`));
474
477
  }
package/scripts/task.js CHANGED
@@ -178,6 +178,9 @@ async function resumeTask(taskId) {
178
178
  await fs.ensureDir(CONTEXT_DIR);
179
179
  await fs.writeJson(CURRENT_FILE, ctx, { spaces: 2 });
180
180
 
181
+ // Deactivate any other tasks before marking this one active
182
+ await deactivateOtherTasks(taskId);
183
+
181
184
  // Mark as active
182
185
  const now = new Date().toISOString();
183
186
  taskState.status = 'active';
@@ -369,6 +372,21 @@ async function nextGate(taskId) {
369
372
  // Internal helpers
370
373
  // ──────────────────────────────────────────────────────────────
371
374
 
375
+ async function deactivateOtherTasks(exceptTaskId) {
376
+ if (!(await fs.pathExists(TASKS_DIR))) return;
377
+ const entries = await fs.readdir(TASKS_DIR, { withFileTypes: true });
378
+ for (const entry of entries) {
379
+ if (!entry.isDirectory() || entry.name === exceptTaskId) continue;
380
+ const statePath = path.join(TASKS_DIR, entry.name, 'task-state.json');
381
+ if (!(await fs.pathExists(statePath))) continue;
382
+ const state = await fs.readJson(statePath).catch(() => null);
383
+ if (state && state.status === 'active') {
384
+ const now = new Date().toISOString();
385
+ await fs.writeJson(statePath, { ...state, status: 'pending', updatedAt: now, pausedAt: now }, { spaces: 2 });
386
+ }
387
+ }
388
+ }
389
+
372
390
  async function loadAllTaskStates() {
373
391
  if (!(await fs.pathExists(TASKS_DIR))) return [];
374
392
  const entries = await fs.readdir(TASKS_DIR, { withFileTypes: true });
@@ -467,6 +485,9 @@ module.exports.createOrActivateTaskState = async function createOrActivateTaskSt
467
485
  // Detect current gate
468
486
  const currentGate = await detectCurrentGate(taskId);
469
487
 
488
+ // Deactivate any other tasks before marking this one active
489
+ await deactivateOtherTasks(taskId);
490
+
470
491
  const existing = (await fs.pathExists(statePath)) ? await fs.readJson(statePath).catch(() => ({})) : {};
471
492
  const now = new Date().toISOString();
472
493
  const taskState = {