@renseiai/agentfactory-cli 0.8.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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/src/agent.d.ts +20 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +109 -0
- package/dist/src/analyze-logs.d.ts +26 -0
- package/dist/src/analyze-logs.d.ts.map +1 -0
- package/dist/src/analyze-logs.js +152 -0
- package/dist/src/cleanup.d.ts +17 -0
- package/dist/src/cleanup.d.ts.map +1 -0
- package/dist/src/cleanup.js +111 -0
- package/dist/src/governor.d.ts +26 -0
- package/dist/src/governor.d.ts.map +1 -0
- package/dist/src/governor.js +305 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +76 -0
- package/dist/src/lib/agent-runner.d.ts +28 -0
- package/dist/src/lib/agent-runner.d.ts.map +1 -0
- package/dist/src/lib/agent-runner.js +272 -0
- package/dist/src/lib/analyze-logs-runner.d.ts +47 -0
- package/dist/src/lib/analyze-logs-runner.d.ts.map +1 -0
- package/dist/src/lib/analyze-logs-runner.js +216 -0
- package/dist/src/lib/auto-updater.d.ts +40 -0
- package/dist/src/lib/auto-updater.d.ts.map +1 -0
- package/dist/src/lib/auto-updater.js +109 -0
- package/dist/src/lib/cleanup-runner.d.ts +29 -0
- package/dist/src/lib/cleanup-runner.d.ts.map +1 -0
- package/dist/src/lib/cleanup-runner.js +295 -0
- package/dist/src/lib/governor-dependencies.d.ts +23 -0
- package/dist/src/lib/governor-dependencies.d.ts.map +1 -0
- package/dist/src/lib/governor-dependencies.js +361 -0
- package/dist/src/lib/governor-logger.d.ts +30 -0
- package/dist/src/lib/governor-logger.d.ts.map +1 -0
- package/dist/src/lib/governor-logger.js +210 -0
- package/dist/src/lib/governor-runner.d.ts +103 -0
- package/dist/src/lib/governor-runner.d.ts.map +1 -0
- package/dist/src/lib/governor-runner.js +210 -0
- package/dist/src/lib/linear-runner.d.ts +8 -0
- package/dist/src/lib/linear-runner.d.ts.map +1 -0
- package/dist/src/lib/linear-runner.js +7 -0
- package/dist/src/lib/orchestrator-runner.d.ts +51 -0
- package/dist/src/lib/orchestrator-runner.d.ts.map +1 -0
- package/dist/src/lib/orchestrator-runner.js +151 -0
- package/dist/src/lib/queue-admin-runner.d.ts +30 -0
- package/dist/src/lib/queue-admin-runner.d.ts.map +1 -0
- package/dist/src/lib/queue-admin-runner.js +378 -0
- package/dist/src/lib/sync-routes-runner.d.ts +28 -0
- package/dist/src/lib/sync-routes-runner.d.ts.map +1 -0
- package/dist/src/lib/sync-routes-runner.js +110 -0
- package/dist/src/lib/version.d.ts +35 -0
- package/dist/src/lib/version.d.ts.map +1 -0
- package/dist/src/lib/version.js +168 -0
- package/dist/src/lib/worker-fleet-runner.d.ts +32 -0
- package/dist/src/lib/worker-fleet-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-fleet-runner.js +256 -0
- package/dist/src/lib/worker-runner.d.ts +33 -0
- package/dist/src/lib/worker-runner.d.ts.map +1 -0
- package/dist/src/lib/worker-runner.js +781 -0
- package/dist/src/linear.d.ts +37 -0
- package/dist/src/linear.d.ts.map +1 -0
- package/dist/src/linear.js +118 -0
- package/dist/src/orchestrator.d.ts +21 -0
- package/dist/src/orchestrator.d.ts.map +1 -0
- package/dist/src/orchestrator.js +190 -0
- package/dist/src/queue-admin.d.ts +25 -0
- package/dist/src/queue-admin.d.ts.map +1 -0
- package/dist/src/queue-admin.js +96 -0
- package/dist/src/sync-routes.d.ts +17 -0
- package/dist/src/sync-routes.d.ts.map +1 -0
- package/dist/src/sync-routes.js +100 -0
- package/dist/src/worker-fleet.d.ts +25 -0
- package/dist/src/worker-fleet.d.ts.map +1 -0
- package/dist/src/worker-fleet.js +140 -0
- package/dist/src/worker.d.ts +26 -0
- package/dist/src/worker.d.ts.map +1 -0
- package/dist/src/worker.js +135 -0
- package/package.json +175 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Runner -- Programmatic API for the af-agent CLI.
|
|
3
|
+
*
|
|
4
|
+
* Provides stop, chat, status, and reconnect commands for managing running
|
|
5
|
+
* agent sessions. Works by updating Redis state directly — workers poll for
|
|
6
|
+
* status changes and pending prompts every 5 seconds.
|
|
7
|
+
*/
|
|
8
|
+
import { getRedisClient, getAllSessions, updateSessionStatus, storeSessionState, storePendingPrompt, disconnectRedis, } from '@renseiai/agentfactory-server';
|
|
9
|
+
import { createLinearAgentClient } from '@renseiai/agentfactory-linear';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// ANSI colors
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export const C = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
red: '\x1b[31m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
gray: '\x1b[90m',
|
|
20
|
+
};
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
function ensureRedis() {
|
|
25
|
+
if (!process.env.REDIS_URL) {
|
|
26
|
+
throw new Error('REDIS_URL environment variable is not set');
|
|
27
|
+
}
|
|
28
|
+
getRedisClient();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Find a session by issue identifier (e.g., SUP-674) or partial session ID.
|
|
32
|
+
* Prefers active sessions (running/claimed) over inactive ones.
|
|
33
|
+
*/
|
|
34
|
+
async function findSession(issueId) {
|
|
35
|
+
const sessions = await getAllSessions();
|
|
36
|
+
const normalizedInput = issueId.toUpperCase();
|
|
37
|
+
const activeStatuses = new Set(['running', 'claimed', 'pending']);
|
|
38
|
+
let activeMatch = null;
|
|
39
|
+
let fallback = null;
|
|
40
|
+
for (const session of sessions) {
|
|
41
|
+
const matchesIdentifier = session.issueIdentifier?.toUpperCase() === normalizedInput;
|
|
42
|
+
const matchesSessionId = session.linearSessionId.includes(issueId);
|
|
43
|
+
if (!matchesIdentifier && !matchesSessionId)
|
|
44
|
+
continue;
|
|
45
|
+
if (activeStatuses.has(session.status)) {
|
|
46
|
+
activeMatch = session;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
if (!fallback) {
|
|
50
|
+
fallback = session;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return activeMatch ?? fallback;
|
|
54
|
+
}
|
|
55
|
+
function formatSession(session) {
|
|
56
|
+
const statusColors = {
|
|
57
|
+
pending: C.yellow,
|
|
58
|
+
claimed: C.cyan,
|
|
59
|
+
running: C.green,
|
|
60
|
+
completed: C.gray,
|
|
61
|
+
failed: C.red,
|
|
62
|
+
stopped: C.yellow,
|
|
63
|
+
finalizing: C.cyan,
|
|
64
|
+
};
|
|
65
|
+
const color = statusColors[session.status] ?? '';
|
|
66
|
+
const identifier = session.issueIdentifier ?? session.issueId.slice(0, 8);
|
|
67
|
+
const sessionShort = session.linearSessionId.slice(0, 12);
|
|
68
|
+
const worker = session.workerId ? ` worker:${session.workerId.slice(0, 8)}` : '';
|
|
69
|
+
const workType = session.workType ? ` (${session.workType})` : '';
|
|
70
|
+
return `${identifier} [${color}${session.status}${C.reset}]${workType} session:${sessionShort}${worker}`;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Command handlers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
async function stopWork(issueId) {
|
|
76
|
+
ensureRedis();
|
|
77
|
+
const session = await findSession(issueId);
|
|
78
|
+
if (!session) {
|
|
79
|
+
console.error(`${C.red}No session found for: ${issueId}${C.reset}`);
|
|
80
|
+
await disconnectRedis();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`Found: ${formatSession(session)}`);
|
|
84
|
+
if (session.status === 'stopped') {
|
|
85
|
+
console.log(`${C.yellow}Session already stopped${C.reset}`);
|
|
86
|
+
await disconnectRedis();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (session.status === 'completed' || session.status === 'failed') {
|
|
90
|
+
console.log(`${C.yellow}Session already in terminal state: ${session.status}${C.reset}`);
|
|
91
|
+
await disconnectRedis();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const updated = await updateSessionStatus(session.linearSessionId, 'stopped');
|
|
95
|
+
if (updated) {
|
|
96
|
+
console.log(`${C.green}Stop signal sent${C.reset} — worker will abort within ~5 seconds`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error(`${C.red}Failed to update session status${C.reset}`);
|
|
100
|
+
}
|
|
101
|
+
await disconnectRedis();
|
|
102
|
+
}
|
|
103
|
+
async function chatWithAgent(issueId, message) {
|
|
104
|
+
ensureRedis();
|
|
105
|
+
const session = await findSession(issueId);
|
|
106
|
+
if (!session) {
|
|
107
|
+
console.error(`${C.red}No session found for: ${issueId}${C.reset}`);
|
|
108
|
+
await disconnectRedis();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(`Found: ${formatSession(session)}`);
|
|
112
|
+
if (session.status !== 'running' && session.status !== 'claimed') {
|
|
113
|
+
console.error(`${C.red}Cannot chat — session is ${session.status}, not running${C.reset}`);
|
|
114
|
+
await disconnectRedis();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const prompt = await storePendingPrompt(session.linearSessionId, session.issueId, message);
|
|
118
|
+
if (prompt) {
|
|
119
|
+
console.log(`${C.green}Message queued${C.reset} (id: ${prompt.id}) — worker will pick up within ~5 seconds`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
console.error(`${C.red}Failed to store pending prompt${C.reset}`);
|
|
123
|
+
}
|
|
124
|
+
await disconnectRedis();
|
|
125
|
+
}
|
|
126
|
+
async function showStatus(issueId) {
|
|
127
|
+
ensureRedis();
|
|
128
|
+
const session = await findSession(issueId);
|
|
129
|
+
if (!session) {
|
|
130
|
+
console.error(`${C.red}No session found for: ${issueId}${C.reset}`);
|
|
131
|
+
await disconnectRedis();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
console.log(`\n${C.cyan}Session Details${C.reset}`);
|
|
135
|
+
console.log('='.repeat(50));
|
|
136
|
+
console.log(` Issue: ${session.issueIdentifier ?? session.issueId}`);
|
|
137
|
+
console.log(` Status: ${session.status}`);
|
|
138
|
+
console.log(` Work Type: ${session.workType ?? 'development'}`);
|
|
139
|
+
console.log(` Session: ${session.linearSessionId}`);
|
|
140
|
+
console.log(` Worker: ${session.workerId ?? '(none)'}`);
|
|
141
|
+
console.log(` Provider: ${session.provider ?? '(unknown)'}`);
|
|
142
|
+
console.log(` Worktree: ${session.worktreePath}`);
|
|
143
|
+
if (session.providerSessionId) {
|
|
144
|
+
console.log(` Provider ID: ${session.providerSessionId}`);
|
|
145
|
+
}
|
|
146
|
+
if (session.totalCostUsd !== undefined) {
|
|
147
|
+
console.log(` Cost: $${session.totalCostUsd.toFixed(4)}`);
|
|
148
|
+
}
|
|
149
|
+
console.log(` Created: ${new Date(session.createdAt * 1000).toISOString()}`);
|
|
150
|
+
console.log(` Updated: ${new Date(session.updatedAt * 1000).toISOString()}`);
|
|
151
|
+
await disconnectRedis();
|
|
152
|
+
}
|
|
153
|
+
async function reconnectSession(issueId) {
|
|
154
|
+
ensureRedis();
|
|
155
|
+
const session = await findSession(issueId);
|
|
156
|
+
if (!session) {
|
|
157
|
+
console.error(`${C.red}No session found for: ${issueId}${C.reset}`);
|
|
158
|
+
await disconnectRedis();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.log(`Found: ${formatSession(session)}`);
|
|
162
|
+
// Create a new Linear agent session on the issue
|
|
163
|
+
const apiKey = process.env.LINEAR_API_KEY;
|
|
164
|
+
if (!apiKey) {
|
|
165
|
+
console.error(`${C.red}LINEAR_API_KEY is required for reconnect${C.reset}`);
|
|
166
|
+
await disconnectRedis();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const linearClient = createLinearAgentClient({ apiKey });
|
|
170
|
+
console.log('Creating new Linear agent session...');
|
|
171
|
+
const result = await linearClient.createAgentSessionOnIssue({
|
|
172
|
+
issueId: session.issueId,
|
|
173
|
+
});
|
|
174
|
+
if (!result.success || !result.sessionId) {
|
|
175
|
+
console.error(`${C.red}Failed to create Linear agent session${C.reset}`);
|
|
176
|
+
await disconnectRedis();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const newSessionId = result.sessionId;
|
|
180
|
+
console.log(`New Linear session: ${newSessionId.slice(0, 12)}...`);
|
|
181
|
+
// Store a new Redis session state with the new Linear session ID,
|
|
182
|
+
// preserving the existing state (worktree, worker, provider session, etc.)
|
|
183
|
+
await storeSessionState(newSessionId, {
|
|
184
|
+
issueId: session.issueId,
|
|
185
|
+
issueIdentifier: session.issueIdentifier,
|
|
186
|
+
providerSessionId: session.providerSessionId,
|
|
187
|
+
worktreePath: session.worktreePath,
|
|
188
|
+
status: session.status,
|
|
189
|
+
workerId: session.workerId,
|
|
190
|
+
queuedAt: session.queuedAt,
|
|
191
|
+
claimedAt: session.claimedAt,
|
|
192
|
+
priority: session.priority,
|
|
193
|
+
promptContext: session.promptContext,
|
|
194
|
+
organizationId: session.organizationId,
|
|
195
|
+
workType: session.workType,
|
|
196
|
+
agentId: session.agentId,
|
|
197
|
+
projectName: session.projectName,
|
|
198
|
+
provider: session.provider,
|
|
199
|
+
totalCostUsd: session.totalCostUsd,
|
|
200
|
+
inputTokens: session.inputTokens,
|
|
201
|
+
outputTokens: session.outputTokens,
|
|
202
|
+
});
|
|
203
|
+
// Mark the old session as stopped so the worker picks up the new one
|
|
204
|
+
await updateSessionStatus(session.linearSessionId, 'stopped');
|
|
205
|
+
console.log(`${C.green}Reconnected${C.reset}`);
|
|
206
|
+
console.log(` Old session: ${session.linearSessionId.slice(0, 12)}... (stopped)`);
|
|
207
|
+
console.log(` New session: ${newSessionId.slice(0, 12)}... (${session.status})`);
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log(`The agent's Linear issue view will now show a fresh session.`);
|
|
210
|
+
console.log(`Worker activities will be reported to the new session.`);
|
|
211
|
+
await disconnectRedis();
|
|
212
|
+
}
|
|
213
|
+
async function listSessions(showAll) {
|
|
214
|
+
ensureRedis();
|
|
215
|
+
const sessions = await getAllSessions();
|
|
216
|
+
const activeStatuses = new Set(['running', 'claimed', 'pending', 'finalizing']);
|
|
217
|
+
const filtered = showAll
|
|
218
|
+
? sessions
|
|
219
|
+
: sessions.filter((s) => activeStatuses.has(s.status));
|
|
220
|
+
const label = showAll ? 'All Sessions' : 'Active Sessions';
|
|
221
|
+
console.log(`\n${C.cyan}${label}${C.reset} (${filtered.length}${showAll ? '' : ` of ${sessions.length}`})`);
|
|
222
|
+
console.log('='.repeat(60));
|
|
223
|
+
if (filtered.length === 0) {
|
|
224
|
+
console.log(showAll ? '(none)' : '(no active sessions)');
|
|
225
|
+
await disconnectRedis();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
for (const session of filtered) {
|
|
229
|
+
const elapsed = Math.round(Date.now() / 1000 - session.createdAt);
|
|
230
|
+
const mins = Math.floor(elapsed / 60);
|
|
231
|
+
const duration = mins < 60
|
|
232
|
+
? `${mins}m`
|
|
233
|
+
: `${Math.floor(mins / 60)}h${mins % 60}m`;
|
|
234
|
+
const cost = session.totalCostUsd !== undefined
|
|
235
|
+
? ` $${session.totalCostUsd.toFixed(2)}`
|
|
236
|
+
: '';
|
|
237
|
+
console.log(` ${formatSession(session)} ${C.gray}${duration}${cost}${C.reset}`);
|
|
238
|
+
}
|
|
239
|
+
await disconnectRedis();
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Public entry point
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
export async function runAgent(config) {
|
|
245
|
+
switch (config.command) {
|
|
246
|
+
case 'list':
|
|
247
|
+
await listSessions(config.all ?? false);
|
|
248
|
+
break;
|
|
249
|
+
case 'stop':
|
|
250
|
+
if (!config.issueId)
|
|
251
|
+
throw new Error('stop command requires an issue ID');
|
|
252
|
+
await stopWork(config.issueId);
|
|
253
|
+
break;
|
|
254
|
+
case 'chat':
|
|
255
|
+
if (!config.issueId)
|
|
256
|
+
throw new Error('chat command requires an issue ID');
|
|
257
|
+
if (!config.message)
|
|
258
|
+
throw new Error('chat command requires a message');
|
|
259
|
+
await chatWithAgent(config.issueId, config.message);
|
|
260
|
+
break;
|
|
261
|
+
case 'status':
|
|
262
|
+
if (!config.issueId)
|
|
263
|
+
throw new Error('status command requires an issue ID');
|
|
264
|
+
await showStatus(config.issueId);
|
|
265
|
+
break;
|
|
266
|
+
case 'reconnect':
|
|
267
|
+
if (!config.issueId)
|
|
268
|
+
throw new Error('reconnect command requires an issue ID');
|
|
269
|
+
await reconnectSession(config.issueId);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Analyzer Runner -- Programmatic API for the log analyzer CLI.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the core logic from the analyze-logs bin script so that it can be
|
|
5
|
+
* invoked programmatically (e.g. from a Next.js route handler or test) without
|
|
6
|
+
* process.exit / dotenv / argv coupling.
|
|
7
|
+
*/
|
|
8
|
+
export interface AnalyzerRunnerConfig {
|
|
9
|
+
/** Analyze a specific session */
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
/** Watch for new sessions and analyze as they complete */
|
|
12
|
+
follow?: boolean;
|
|
13
|
+
/** Poll interval in milliseconds (default: 5000) */
|
|
14
|
+
interval?: number;
|
|
15
|
+
/** Show what would be created without creating issues */
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
/** Cleanup old logs based on retention policy */
|
|
18
|
+
cleanup?: boolean;
|
|
19
|
+
/** Show detailed analysis output */
|
|
20
|
+
verbose?: boolean;
|
|
21
|
+
/** Base directory for logs (default: {gitRoot}/.agent-logs or AGENT_LOGS_DIR env) */
|
|
22
|
+
logsDir?: string;
|
|
23
|
+
/** Git root for default paths (default: auto-detect) */
|
|
24
|
+
gitRoot?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface AnalyzerResult {
|
|
27
|
+
sessionsAnalyzed: number;
|
|
28
|
+
totalErrors: number;
|
|
29
|
+
totalPatterns: number;
|
|
30
|
+
issuesCreated: number;
|
|
31
|
+
issuesUpdated: number;
|
|
32
|
+
}
|
|
33
|
+
/** Detect the git repository root. Falls back to cwd. */
|
|
34
|
+
export declare function getGitRoot(): string;
|
|
35
|
+
export declare function printSummary(stats: AnalyzerResult, dryRun: boolean): void;
|
|
36
|
+
/**
|
|
37
|
+
* Run the log analyzer programmatically.
|
|
38
|
+
*
|
|
39
|
+
* For one-shot mode (default) the returned promise resolves with the analysis
|
|
40
|
+
* result once all sessions have been processed.
|
|
41
|
+
*
|
|
42
|
+
* For follow mode (`config.follow = true`) the analyzer keeps running until
|
|
43
|
+
* the optional `signal` is aborted. When the signal fires the current poll
|
|
44
|
+
* cycle finishes and the accumulated stats are returned.
|
|
45
|
+
*/
|
|
46
|
+
export declare function runLogAnalyzer(config?: AnalyzerRunnerConfig, signal?: AbortSignal): Promise<AnalyzerResult>;
|
|
47
|
+
//# sourceMappingURL=analyze-logs-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-logs-runner.d.ts","sourceRoot":"","sources":["../../../src/lib/analyze-logs-runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,WAAW,oBAAoB;IACnC,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;CACtB;AAQD,yDAAyD;AACzD,wBAAgB,UAAU,IAAI,MAAM,CASnC;AA0GD,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CASzE;AAqED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,MAAM,GAAE,oBAAyB,EACjC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAkDzB"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Analyzer Runner -- Programmatic API for the log analyzer CLI.
|
|
3
|
+
*
|
|
4
|
+
* Extracts the core logic from the analyze-logs bin script so that it can be
|
|
5
|
+
* invoked programmatically (e.g. from a Next.js route handler or test) without
|
|
6
|
+
* process.exit / dotenv / argv coupling.
|
|
7
|
+
*/
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { createLogAnalyzer } from '@renseiai/agentfactory';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const DEFAULT_POLL_INTERVAL = 5000;
|
|
14
|
+
/** Detect the git repository root. Falls back to cwd. */
|
|
15
|
+
export function getGitRoot() {
|
|
16
|
+
try {
|
|
17
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
}).trim();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return process.cwd();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function formatTime() {
|
|
27
|
+
return new Date().toLocaleTimeString();
|
|
28
|
+
}
|
|
29
|
+
function emptyStats() {
|
|
30
|
+
return {
|
|
31
|
+
sessionsAnalyzed: 0,
|
|
32
|
+
totalErrors: 0,
|
|
33
|
+
totalPatterns: 0,
|
|
34
|
+
issuesCreated: 0,
|
|
35
|
+
issuesUpdated: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function analyzeAndPrintSession(analyzer, sessionId, options, stats) {
|
|
39
|
+
console.log(`\nAnalyzing session: ${sessionId}`);
|
|
40
|
+
console.log('-'.repeat(50));
|
|
41
|
+
const result = analyzer.analyzeSession(sessionId);
|
|
42
|
+
if (!result) {
|
|
43
|
+
console.log(' [SKIP] Session not found or incomplete');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
console.log(` Issue: ${result.metadata.issueIdentifier}`);
|
|
47
|
+
console.log(` Work Type: ${result.metadata.workType}`);
|
|
48
|
+
console.log(` Status: ${result.metadata.status}`);
|
|
49
|
+
console.log(` Events: ${result.eventsAnalyzed}`);
|
|
50
|
+
console.log(` Errors: ${result.errorsFound}`);
|
|
51
|
+
console.log(` Patterns: ${result.patterns.length}`);
|
|
52
|
+
stats.sessionsAnalyzed++;
|
|
53
|
+
stats.totalErrors += result.errorsFound;
|
|
54
|
+
stats.totalPatterns += result.patterns.length;
|
|
55
|
+
if (options.verbose && result.patterns.length > 0) {
|
|
56
|
+
console.log('\n Detected Patterns:');
|
|
57
|
+
for (const pattern of result.patterns) {
|
|
58
|
+
console.log(` - [${pattern.severity}] ${pattern.title}`);
|
|
59
|
+
console.log(` Type: ${pattern.type}, Occurrences: ${pattern.occurrences}`);
|
|
60
|
+
if (pattern.tool) {
|
|
61
|
+
console.log(` Tool: ${pattern.tool}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (result.suggestedIssues.length > 0) {
|
|
66
|
+
console.log(`\n Suggested Issues: ${result.suggestedIssues.length}`);
|
|
67
|
+
if (options.verbose) {
|
|
68
|
+
for (const issue of result.suggestedIssues) {
|
|
69
|
+
console.log(` - ${issue.title}`);
|
|
70
|
+
console.log(` Signature: ${issue.signature}`);
|
|
71
|
+
console.log(` Labels: ${issue.labels.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const issueResults = await analyzer.createIssues(result.suggestedIssues, sessionId, options.dryRun);
|
|
76
|
+
for (const issueResult of issueResults) {
|
|
77
|
+
if (issueResult.created) {
|
|
78
|
+
console.log(` [${options.dryRun ? 'WOULD CREATE' : 'CREATED'}] ${issueResult.identifier}`);
|
|
79
|
+
stats.issuesCreated++;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(` [${options.dryRun ? 'WOULD UPDATE' : 'UPDATED'}] ${issueResult.identifier}`);
|
|
83
|
+
stats.issuesUpdated++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.log(` [ERROR] Failed to create issues: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!options.dryRun) {
|
|
92
|
+
analyzer.markProcessed(sessionId, result);
|
|
93
|
+
console.log(' [PROCESSED]');
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Summary printer
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
export function printSummary(stats, dryRun) {
|
|
101
|
+
console.log('\n' + '='.repeat(50));
|
|
102
|
+
console.log('=== Summary ===\n');
|
|
103
|
+
console.log(` Sessions analyzed: ${stats.sessionsAnalyzed}`);
|
|
104
|
+
console.log(` Total errors found: ${stats.totalErrors}`);
|
|
105
|
+
console.log(` Total patterns detected: ${stats.totalPatterns}`);
|
|
106
|
+
console.log(` Issues created: ${stats.issuesCreated}${dryRun ? ' (dry run)' : ''}`);
|
|
107
|
+
console.log(` Issues updated: ${stats.issuesUpdated}${dryRun ? ' (dry run)' : ''}`);
|
|
108
|
+
console.log('');
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Follow mode (watch)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
async function runFollowMode(analyzer, options, interval, signal) {
|
|
114
|
+
const stats = emptyStats();
|
|
115
|
+
const processedInSession = new Set();
|
|
116
|
+
console.log(`[${formatTime()}] Watching for new sessions (poll interval: ${interval}ms)`);
|
|
117
|
+
console.log(`[${formatTime()}] Press Ctrl+C to stop\n`);
|
|
118
|
+
const isAborted = () => signal?.aborted ?? false;
|
|
119
|
+
// Initial check for existing unprocessed sessions
|
|
120
|
+
const initialSessions = analyzer.getUnprocessedSessions();
|
|
121
|
+
if (initialSessions.length > 0) {
|
|
122
|
+
console.log(`[${formatTime()}] Found ${initialSessions.length} existing unprocessed session(s)`);
|
|
123
|
+
for (const sid of initialSessions) {
|
|
124
|
+
if (isAborted())
|
|
125
|
+
break;
|
|
126
|
+
await analyzeAndPrintSession(analyzer, sid, options, stats);
|
|
127
|
+
processedInSession.add(sid);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Poll loop
|
|
131
|
+
while (!isAborted()) {
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
const timer = setTimeout(resolve, interval);
|
|
134
|
+
// If an abort signal fires while waiting, resolve immediately
|
|
135
|
+
if (signal) {
|
|
136
|
+
const onAbort = () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
resolve();
|
|
139
|
+
};
|
|
140
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (isAborted())
|
|
144
|
+
break;
|
|
145
|
+
const sessions = analyzer.getUnprocessedSessions();
|
|
146
|
+
const newSessions = sessions.filter((s) => !processedInSession.has(s));
|
|
147
|
+
if (newSessions.length > 0) {
|
|
148
|
+
console.log(`[${formatTime()}] Found ${newSessions.length} new session(s) ready for analysis`);
|
|
149
|
+
for (const sid of newSessions) {
|
|
150
|
+
if (isAborted())
|
|
151
|
+
break;
|
|
152
|
+
const analyzed = await analyzeAndPrintSession(analyzer, sid, options, stats);
|
|
153
|
+
if (analyzed) {
|
|
154
|
+
processedInSession.add(sid);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.log(`\n[${formatTime()}] Stopping...`);
|
|
160
|
+
return stats;
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Public entry point
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
/**
|
|
166
|
+
* Run the log analyzer programmatically.
|
|
167
|
+
*
|
|
168
|
+
* For one-shot mode (default) the returned promise resolves with the analysis
|
|
169
|
+
* result once all sessions have been processed.
|
|
170
|
+
*
|
|
171
|
+
* For follow mode (`config.follow = true`) the analyzer keeps running until
|
|
172
|
+
* the optional `signal` is aborted. When the signal fires the current poll
|
|
173
|
+
* cycle finishes and the accumulated stats are returned.
|
|
174
|
+
*/
|
|
175
|
+
export async function runLogAnalyzer(config = {}, signal) {
|
|
176
|
+
const gitRoot = config.gitRoot ?? getGitRoot();
|
|
177
|
+
const logsDir = config.logsDir ?? process.env.AGENT_LOGS_DIR ?? `${gitRoot}/.agent-logs`;
|
|
178
|
+
const dryRun = config.dryRun ?? false;
|
|
179
|
+
const verbose = config.verbose ?? false;
|
|
180
|
+
const interval = config.interval ?? DEFAULT_POLL_INTERVAL;
|
|
181
|
+
console.log('\n=== AgentFactory Log Analyzer ===\n');
|
|
182
|
+
if (dryRun) {
|
|
183
|
+
console.log('[DRY RUN MODE - No issues will be created]\n');
|
|
184
|
+
}
|
|
185
|
+
const analyzer = createLogAnalyzer({ logsDir });
|
|
186
|
+
// Cleanup mode
|
|
187
|
+
if (config.cleanup) {
|
|
188
|
+
console.log('Cleaning up old logs...\n');
|
|
189
|
+
const deleted = analyzer.cleanupOldLogs();
|
|
190
|
+
console.log(`Deleted ${deleted} old log entries.\n`);
|
|
191
|
+
return emptyStats();
|
|
192
|
+
}
|
|
193
|
+
// Follow (watch) mode
|
|
194
|
+
if (config.follow) {
|
|
195
|
+
return runFollowMode(analyzer, { dryRun, verbose }, interval, signal);
|
|
196
|
+
}
|
|
197
|
+
// Standard one-shot mode
|
|
198
|
+
const stats = emptyStats();
|
|
199
|
+
let sessionsToAnalyze;
|
|
200
|
+
if (config.sessionId) {
|
|
201
|
+
sessionsToAnalyze = [config.sessionId];
|
|
202
|
+
console.log(`Analyzing session: ${config.sessionId}\n`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
sessionsToAnalyze = analyzer.getUnprocessedSessions();
|
|
206
|
+
console.log(`Found ${sessionsToAnalyze.length} unprocessed session(s)\n`);
|
|
207
|
+
}
|
|
208
|
+
if (sessionsToAnalyze.length === 0) {
|
|
209
|
+
console.log('No sessions to analyze.\n');
|
|
210
|
+
return stats;
|
|
211
|
+
}
|
|
212
|
+
for (const sid of sessionsToAnalyze) {
|
|
213
|
+
await analyzeAndPrintSession(analyzer, sid, { dryRun, verbose }, stats);
|
|
214
|
+
}
|
|
215
|
+
return stats;
|
|
216
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Updater for AgentFactory CLI.
|
|
3
|
+
*
|
|
4
|
+
* When enabled, checks for new versions and can automatically update
|
|
5
|
+
* the CLI package. For long-running processes (fleet, governor), it
|
|
6
|
+
* waits until there are no active workers before restarting.
|
|
7
|
+
*
|
|
8
|
+
* Configuration (in order of precedence):
|
|
9
|
+
* 1. CLI flag: --auto-update / --no-auto-update
|
|
10
|
+
* 2. .env.local: AF_AUTO_UPDATE=true
|
|
11
|
+
* 3. .agentfactory/config.yaml: autoUpdate: true
|
|
12
|
+
*/
|
|
13
|
+
import type { UpdateCheckResult } from './version.js';
|
|
14
|
+
export interface AutoUpdateConfig {
|
|
15
|
+
/** CLI flag override (highest precedence) */
|
|
16
|
+
cliFlag?: boolean;
|
|
17
|
+
/** Whether there are active workers/agents to wait for */
|
|
18
|
+
hasActiveWorkers?: () => boolean | Promise<boolean>;
|
|
19
|
+
/** Callback invoked right before the process exits for restart */
|
|
20
|
+
onBeforeRestart?: () => void | Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve whether auto-update is enabled from all config sources.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isAutoUpdateEnabled(cliFlag?: boolean): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Perform auto-update if enabled and an update is available.
|
|
28
|
+
*
|
|
29
|
+
* For long-running processes (fleet, governor), this should be called
|
|
30
|
+
* periodically. It will:
|
|
31
|
+
* 1. Check if auto-update is enabled
|
|
32
|
+
* 2. Verify an update is available
|
|
33
|
+
* 3. Wait for active workers to finish (if hasActiveWorkers provided)
|
|
34
|
+
* 4. Install the update
|
|
35
|
+
* 5. Exit the process so the service manager can restart with the new version
|
|
36
|
+
*
|
|
37
|
+
* Returns true if an update was applied (process will exit shortly after).
|
|
38
|
+
*/
|
|
39
|
+
export declare function maybeAutoUpdate(updateCheck: UpdateCheckResult | null, config: AutoUpdateConfig): Promise<boolean>;
|
|
40
|
+
//# sourceMappingURL=auto-updater.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-updater.d.ts","sourceRoot":"","sources":["../../../src/lib/auto-updater.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAmBrD,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,0DAA0D;IAC1D,gBAAgB,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACnD,kEAAkE;IAClE,eAAe,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAW9D;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,iBAAiB,GAAG,IAAI,EACrC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,OAAO,CAAC,CA+BlB"}
|