@relipa/ai-flow-kit 0.0.7 → 0.0.8-beta.1
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 +7 -6
- package/bin/aiflow.js +400 -360
- package/bin/ak.js +4 -0
- package/custom/skills/generate-spec/SKILL.md +1 -6
- package/custom/templates/php-plain.md +261 -261
- package/custom/templates/php.md +261 -0
- package/custom/templates/python.md +79 -0
- package/docs/common/CHANGELOG.md +53 -1
- package/docs/common/QUICK_START.md +57 -50
- package/docs/common/cli-reference.md +161 -196
- package/docs/common/getting-started.md +3 -3
- package/docs/common/troubleshooting.md +7 -7
- package/package.json +2 -1
- package/scripts/checkpoint.js +46 -46
- package/scripts/gitnexus-worker.js +94 -94
- package/scripts/hooks/block-git-write.js +52 -0
- package/scripts/hooks/session-stop.js +55 -55
- package/scripts/init.js +27 -5
- package/scripts/task.js +21 -0
- package/scripts/use.js +880 -625
- package/upstream/skills/brainstorming/SKILL.md +3 -3
package/scripts/checkpoint.js
CHANGED
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse hook for ai-flow-kit
|
|
4
|
+
*
|
|
5
|
+
* Hard-blocks AI from running git-write operations (commit/add/push/tag) via the
|
|
6
|
+
* Bash tool. The developer always controls commits manually in their own terminal.
|
|
7
|
+
*
|
|
8
|
+
* Stdin payload (from Claude Code):
|
|
9
|
+
* { tool_name: "Bash", tool_input: { command: "..." }, ... }
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* 0 — allow (stdout empty)
|
|
13
|
+
* 2 — block (stderr message is fed back to Claude as feedback)
|
|
14
|
+
*
|
|
15
|
+
* Override (for the kit's own maintenance scripts):
|
|
16
|
+
* export AIFLOW_ALLOW_GIT_WRITE=1
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
let raw = '';
|
|
20
|
+
process.stdin.setEncoding('utf-8');
|
|
21
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
22
|
+
process.stdin.on('end', () => {
|
|
23
|
+
try {
|
|
24
|
+
if (process.env.AIFLOW_ALLOW_GIT_WRITE === '1') process.exit(0);
|
|
25
|
+
|
|
26
|
+
const payload = raw ? JSON.parse(raw) : {};
|
|
27
|
+
if (payload.tool_name !== 'Bash') process.exit(0);
|
|
28
|
+
|
|
29
|
+
const command = (payload.tool_input && payload.tool_input.command) || '';
|
|
30
|
+
if (!command) process.exit(0);
|
|
31
|
+
|
|
32
|
+
// Match `git <subcommand>` where subcommand is a write operation.
|
|
33
|
+
// Tolerates leading args (e.g. `git -C path commit`, `git --no-pager add`).
|
|
34
|
+
// Captures occurrences inside compound shells: `&&`, `||`, `;`, `|`, `$(...)`, backticks, newline.
|
|
35
|
+
const BLOCK_RE = /(^|[\s;&|`(){}])git(\s+-[^\s]+)*\s+(commit|add|push|tag|reset|rebase|revert|cherry-pick|am|merge)\b/i;
|
|
36
|
+
const match = command.match(BLOCK_RE);
|
|
37
|
+
if (!match) process.exit(0);
|
|
38
|
+
|
|
39
|
+
const verb = match[3].toLowerCase();
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
`[aiflow] BLOCKED: \`git ${verb}\` is not allowed for AI.\n` +
|
|
42
|
+
`Reason: ai-flow-kit forbids AI-driven commits/pushes. The developer manages git state manually.\n` +
|
|
43
|
+
`If this is a legitimate maintenance task (running the kit's own CI), set AIFLOW_ALLOW_GIT_WRITE=1 in the environment.\n` +
|
|
44
|
+
`Otherwise, finish the task without committing; the developer will run \`git ${verb}\` themselves at Gate 5.\n`
|
|
45
|
+
);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// On parsing error, fail open (don't block) but log to stderr for visibility.
|
|
49
|
+
process.stderr.write(`[aiflow] block-git-write hook error: ${err.message}\n`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
];
|
|
@@ -182,6 +185,10 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
182
185
|
const hookStopDest = path.join(hooksDir, 'session-stop.js');
|
|
183
186
|
await fs.copy(hookStopSrc, hookStopDest, { overwrite: true });
|
|
184
187
|
|
|
188
|
+
const hookBlockGitSrc = path.join(PKG_DIR, 'scripts', 'hooks', 'block-git-write.js');
|
|
189
|
+
const hookBlockGitDest = path.join(hooksDir, 'block-git-write.js');
|
|
190
|
+
await fs.copy(hookBlockGitSrc, hookBlockGitDest, { overwrite: true });
|
|
191
|
+
|
|
185
192
|
const settingsFile = path.join(claudeDir, 'settings.json');
|
|
186
193
|
let settings = {};
|
|
187
194
|
if (await fs.pathExists(settingsFile)) {
|
|
@@ -191,6 +198,7 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
191
198
|
if (!settings.hooks) settings.hooks = {};
|
|
192
199
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
193
200
|
if (!settings.hooks.SessionStop) settings.hooks.SessionStop = [];
|
|
201
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
194
202
|
|
|
195
203
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
196
204
|
h => !(h._aiflowKit)
|
|
@@ -218,8 +226,22 @@ async function setupSuperpowersHook(_projectDir, claudeDir) {
|
|
|
218
226
|
]
|
|
219
227
|
});
|
|
220
228
|
|
|
229
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
230
|
+
h => !(h._aiflowKitBlockGitWrite)
|
|
231
|
+
);
|
|
232
|
+
settings.hooks.PreToolUse.push({
|
|
233
|
+
_aiflowKitBlockGitWrite: true,
|
|
234
|
+
matcher: 'Bash',
|
|
235
|
+
hooks: [
|
|
236
|
+
{
|
|
237
|
+
type: 'command',
|
|
238
|
+
command: `node "${hookBlockGitDest.replace(/\\/g, '/')}"`,
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
|
|
221
243
|
await fs.writeJson(settingsFile, settings, { spaces: 2 });
|
|
222
|
-
console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionStop) configured`));
|
|
244
|
+
console.log(chalk.green(`✓ Superpowers hooks (SessionStart, SessionStop, PreToolUse:block-git-write) configured`));
|
|
223
245
|
}
|
|
224
246
|
|
|
225
247
|
async function verifySuperpowersSkills(versionDir) {
|
|
@@ -468,7 +490,7 @@ async function setupAdapter(projectDir, adapter) {
|
|
|
468
490
|
}
|
|
469
491
|
globalData.mcp = { ...(globalData.mcp || {}), ...credentials };
|
|
470
492
|
await fs.writeJson(GLOBAL_CREDENTIALS_PATH, globalData, { spaces: 2 });
|
|
471
|
-
await
|
|
493
|
+
await ensureAiflowGitignored(projectDir);
|
|
472
494
|
|
|
473
495
|
console.log(chalk.green(`✓ MCP configuration saved (global + local + credentials).`));
|
|
474
496
|
}
|
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 = {
|