mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-01 06:23:28 +08:00
feat: add loop-status watch mode
This commit is contained in:
committed by
Affaan Mustafa
parent
b1456bd954
commit
38f4265a1c
@@ -40,10 +40,15 @@ tool calls that have no matching `tool_result`.
|
|||||||
directly.
|
directly.
|
||||||
- `ecc loop-status --bash-timeout-seconds 1800` adjusts the stale Bash
|
- `ecc loop-status --bash-timeout-seconds 1800` adjusts the stale Bash
|
||||||
threshold.
|
threshold.
|
||||||
|
- `ecc loop-status --watch` refreshes status until interrupted.
|
||||||
|
- `ecc loop-status --watch --watch-count 3` emits a bounded watch stream for
|
||||||
|
scripts and handoffs.
|
||||||
|
|
||||||
## Watch Mode
|
## Watch Mode
|
||||||
|
|
||||||
When `--watch` is present, refresh status periodically and surface state changes.
|
When `--watch` is present, refresh status periodically. With `--json`, each
|
||||||
|
refresh is emitted as one JSON object per line so another terminal or script can
|
||||||
|
consume the stream.
|
||||||
|
|
||||||
## Arguments
|
## Arguments
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ const path = require('path');
|
|||||||
const DEFAULT_BASH_TIMEOUT_SECONDS = 30 * 60;
|
const DEFAULT_BASH_TIMEOUT_SECONDS = 30 * 60;
|
||||||
const DEFAULT_LIMIT = 10;
|
const DEFAULT_LIMIT = 10;
|
||||||
const DEFAULT_WAKE_GRACE_MULTIPLIER = 2;
|
const DEFAULT_WAKE_GRACE_MULTIPLIER = 2;
|
||||||
|
const DEFAULT_WATCH_INTERVAL_SECONDS = 5;
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
console.log([
|
console.log([
|
||||||
'Usage:',
|
'Usage:',
|
||||||
' node scripts/loop-status.js [--json] [--home <dir>] [--limit <n>]',
|
' node scripts/loop-status.js [--json] [--home <dir>] [--limit <n>] [--watch]',
|
||||||
' node scripts/loop-status.js --transcript <session.jsonl> [--json]',
|
' node scripts/loop-status.js --transcript <session.jsonl> [--json] [--watch]',
|
||||||
'',
|
'',
|
||||||
'Options:',
|
'Options:',
|
||||||
' --json Emit machine-readable status JSON',
|
' --json Emit machine-readable status JSON',
|
||||||
@@ -23,6 +24,9 @@ function usage() {
|
|||||||
' --bash-timeout-seconds <n> Age before a pending Bash call is stale (default: 1800)',
|
' --bash-timeout-seconds <n> Age before a pending Bash call is stale (default: 1800)',
|
||||||
' --wake-grace-multiplier <n> ScheduleWakeup grace multiplier (default: 2)',
|
' --wake-grace-multiplier <n> ScheduleWakeup grace multiplier (default: 2)',
|
||||||
' --now <time> Override current time (ISO, epoch ms, or "now")',
|
' --now <time> Override current time (ISO, epoch ms, or "now")',
|
||||||
|
' --watch Refresh status until interrupted',
|
||||||
|
' --watch-count <n> Stop after n watch refreshes',
|
||||||
|
' --watch-interval-seconds <n> Seconds between watch refreshes (default: 5)',
|
||||||
'',
|
'',
|
||||||
'Examples:',
|
'Examples:',
|
||||||
' node scripts/loop-status.js --json',
|
' node scripts/loop-status.js --json',
|
||||||
@@ -64,7 +68,10 @@ function parseArgs(argv) {
|
|||||||
now: null,
|
now: null,
|
||||||
showHelp: false,
|
showHelp: false,
|
||||||
transcriptPaths: [],
|
transcriptPaths: [],
|
||||||
|
watch: false,
|
||||||
|
watchCount: null,
|
||||||
wakeGraceMultiplier: DEFAULT_WAKE_GRACE_MULTIPLIER,
|
wakeGraceMultiplier: DEFAULT_WAKE_GRACE_MULTIPLIER,
|
||||||
|
watchIntervalSeconds: DEFAULT_WATCH_INTERVAL_SECONDS,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let index = 0; index < args.length; index += 1) {
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
@@ -92,6 +99,14 @@ function parseArgs(argv) {
|
|||||||
} else if (arg === '--now') {
|
} else if (arg === '--now') {
|
||||||
options.now = readValue(args, index, arg);
|
options.now = readValue(args, index, arg);
|
||||||
index += 1;
|
index += 1;
|
||||||
|
} else if (arg === '--watch') {
|
||||||
|
options.watch = true;
|
||||||
|
} else if (arg === '--watch-count') {
|
||||||
|
options.watchCount = readPositiveInteger(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
|
} else if (arg === '--watch-interval-seconds') {
|
||||||
|
options.watchIntervalSeconds = readPositiveNumber(readValue(args, index, arg), arg);
|
||||||
|
index += 1;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown option: ${arg}`);
|
throw new Error(`Unknown option: ${arg}`);
|
||||||
}
|
}
|
||||||
@@ -106,7 +121,10 @@ function normalizeOptions(options = {}) {
|
|||||||
bashTimeoutSeconds: options.bashTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS,
|
bashTimeoutSeconds: options.bashTimeoutSeconds ?? DEFAULT_BASH_TIMEOUT_SECONDS,
|
||||||
limit: options.limit ?? DEFAULT_LIMIT,
|
limit: options.limit ?? DEFAULT_LIMIT,
|
||||||
transcriptPaths: options.transcriptPaths || [],
|
transcriptPaths: options.transcriptPaths || [],
|
||||||
|
watch: Boolean(options.watch),
|
||||||
|
watchCount: options.watchCount ?? null,
|
||||||
wakeGraceMultiplier: options.wakeGraceMultiplier ?? DEFAULT_WAKE_GRACE_MULTIPLIER,
|
wakeGraceMultiplier: options.wakeGraceMultiplier ?? DEFAULT_WAKE_GRACE_MULTIPLIER,
|
||||||
|
watchIntervalSeconds: options.watchIntervalSeconds ?? DEFAULT_WATCH_INTERVAL_SECONDS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,28 +594,57 @@ function formatText(payload) {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStatus(payload, options) {
|
||||||
|
if (options.json) {
|
||||||
|
console.log(options.watch ? JSON.stringify(payload) : JSON.stringify(payload, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log(formatText(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runWatch(options) {
|
||||||
|
const normalizedOptions = normalizeOptions(options);
|
||||||
|
let iteration = 0;
|
||||||
|
|
||||||
|
while (normalizedOptions.watchCount === null || iteration < normalizedOptions.watchCount) {
|
||||||
|
if (iteration > 0 && !normalizedOptions.json) {
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
writeStatus(buildStatus(normalizedOptions), normalizedOptions);
|
||||||
|
iteration += 1;
|
||||||
|
|
||||||
|
if (normalizedOptions.watchCount !== null && iteration >= normalizedOptions.watchCount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(normalizedOptions.watchIntervalSeconds * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
const options = parseArgs(process.argv);
|
const options = parseArgs(process.argv);
|
||||||
if (options.showHelp) {
|
if (options.showHelp) {
|
||||||
usage();
|
usage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = buildStatus(options);
|
if (options.watch) {
|
||||||
if (options.json) {
|
await runWatch(options);
|
||||||
console.log(JSON.stringify(payload, null, 2));
|
return;
|
||||||
} else {
|
|
||||||
console.log(formatText(payload));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeStatus(buildStatus(options), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
try {
|
main().catch(error => {
|
||||||
main();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[loop-status] ${error.message}`);
|
console.error(`[loop-status] ${error.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -606,4 +653,5 @@ module.exports = {
|
|||||||
extractToolResultIds,
|
extractToolResultIds,
|
||||||
extractToolUses,
|
extractToolUses,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
|
runWatch,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const path = require('path');
|
|||||||
const { execFileSync } = require('child_process');
|
const { execFileSync } = require('child_process');
|
||||||
|
|
||||||
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'loop-status.js');
|
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'loop-status.js');
|
||||||
const { analyzeTranscript, buildStatus } = require('../../scripts/loop-status');
|
const { analyzeTranscript, buildStatus, parseArgs } = require('../../scripts/loop-status');
|
||||||
const NOW = '2026-04-30T10:00:00.000Z';
|
const NOW = '2026-04-30T10:00:00.000Z';
|
||||||
|
|
||||||
function run(args = [], options = {}) {
|
function run(args = [], options = {}) {
|
||||||
@@ -396,6 +396,58 @@ function runTests() {
|
|||||||
assert.match(result.stderr, /--limit must be a positive integer/);
|
assert.match(result.stderr, /--limit must be a positive integer/);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('parses watch mode controls', () => {
|
||||||
|
const options = parseArgs([
|
||||||
|
'node',
|
||||||
|
'scripts/loop-status.js',
|
||||||
|
'--watch',
|
||||||
|
'--watch-count',
|
||||||
|
'2',
|
||||||
|
'--watch-interval-seconds',
|
||||||
|
'0.01',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(options.watch, true);
|
||||||
|
assert.strictEqual(options.watchCount, 2);
|
||||||
|
assert.strictEqual(options.watchIntervalSeconds, 0.01);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('watch mode emits repeated JSON status frames', () => {
|
||||||
|
const homeDir = createTempHome();
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeTranscript(homeDir, '-Users-affoon-project-watch', 'session-watch.jsonl', [
|
||||||
|
toolUse('2026-04-30T09:00:00.000Z', 'session-watch', 'toolu_watch', 'ScheduleWakeup', {
|
||||||
|
delaySeconds: 300,
|
||||||
|
reason: 'Loop checkpoint',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = run([
|
||||||
|
'--home',
|
||||||
|
homeDir,
|
||||||
|
'--now',
|
||||||
|
NOW,
|
||||||
|
'--json',
|
||||||
|
'--watch',
|
||||||
|
'--watch-count',
|
||||||
|
'2',
|
||||||
|
'--watch-interval-seconds',
|
||||||
|
'0.01',
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.strictEqual(result.code, 0, result.stderr);
|
||||||
|
const frames = result.stdout.trim().split(/\r?\n/).map(line => JSON.parse(line));
|
||||||
|
assert.strictEqual(frames.length, 2);
|
||||||
|
assert.strictEqual(frames[0].schemaVersion, 'ecc.loop-status.v1');
|
||||||
|
assert.strictEqual(frames[1].schemaVersion, 'ecc.loop-status.v1');
|
||||||
|
assert.strictEqual(frames[0].sessions[0].sessionId, 'session-watch');
|
||||||
|
assert.strictEqual(frames[1].sessions[0].sessionId, 'session-watch');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user