@supaku/agentfactory-server 0.1.2 → 0.2.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/dist/src/env-validation.d.ts +65 -0
- package/dist/src/env-validation.d.ts.map +1 -0
- package/dist/src/env-validation.js +134 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +16 -0
- package/dist/src/logger.d.ts +76 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +218 -0
- package/dist/src/orphan-cleanup.d.ts +64 -0
- package/dist/src/orphan-cleanup.d.ts.map +1 -0
- package/dist/src/orphan-cleanup.js +335 -0
- package/dist/src/pending-prompts.d.ts +67 -0
- package/dist/src/pending-prompts.d.ts.map +1 -0
- package/dist/src/pending-prompts.js +176 -0
- package/dist/src/rate-limit.d.ts +111 -0
- package/dist/src/rate-limit.d.ts.map +1 -0
- package/dist/src/rate-limit.js +171 -0
- package/dist/src/session-hash.d.ts +48 -0
- package/dist/src/session-hash.d.ts.map +1 -0
- package/dist/src/session-hash.js +80 -0
- package/dist/src/token-storage.d.ts +118 -0
- package/dist/src/token-storage.d.ts.map +1 -0
- package/dist/src/token-storage.js +263 -0
- package/dist/src/worker-auth.d.ts +29 -0
- package/dist/src/worker-auth.d.ts.map +1 -0
- package/dist/src/worker-auth.js +49 -0
- package/package.json +3 -3
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates required environment variables on startup.
|
|
5
|
+
* Fails fast if critical security variables are missing in production.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for environment validation
|
|
9
|
+
*/
|
|
10
|
+
export interface EnvValidationConfig {
|
|
11
|
+
/** Variable names required in production */
|
|
12
|
+
requiredVars?: string[];
|
|
13
|
+
/** Variables that need minimum length validation */
|
|
14
|
+
minLengthVars?: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
minLength: number;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validation result
|
|
21
|
+
*/
|
|
22
|
+
export interface EnvValidationResult {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
missing: string[];
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate environment variables
|
|
29
|
+
*
|
|
30
|
+
* In production: All required vars must be present
|
|
31
|
+
* In development: Log warnings for missing vars but don't fail
|
|
32
|
+
*
|
|
33
|
+
* @param config - Optional configuration to override defaults
|
|
34
|
+
* @returns Validation result with missing vars
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateEnv(config?: EnvValidationConfig): EnvValidationResult;
|
|
37
|
+
/**
|
|
38
|
+
* Validate and fail fast if critical vars are missing
|
|
39
|
+
*
|
|
40
|
+
* Call this at application startup to ensure required
|
|
41
|
+
* environment variables are configured.
|
|
42
|
+
*
|
|
43
|
+
* @param config - Optional configuration to override defaults
|
|
44
|
+
* @throws Error if required vars are missing in production
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateEnvOrThrow(config?: EnvValidationConfig): void;
|
|
47
|
+
/**
|
|
48
|
+
* Check if webhook signature verification is configured
|
|
49
|
+
*/
|
|
50
|
+
export declare function isWebhookSecretConfigured(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Check if cron authentication is configured
|
|
53
|
+
*/
|
|
54
|
+
export declare function isCronSecretConfigured(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Check if session hashing is configured
|
|
57
|
+
*/
|
|
58
|
+
export declare function isSessionHashConfigured(): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Get session hash salt
|
|
61
|
+
* @param saltEnvVar - Environment variable name for the salt (default: SESSION_HASH_SALT)
|
|
62
|
+
* @throws Error if not configured
|
|
63
|
+
*/
|
|
64
|
+
export declare function getSessionHashSalt(saltEnvVar?: string): string;
|
|
65
|
+
//# sourceMappingURL=env-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-validation.d.ts","sourceRoot":"","sources":["../../src/env-validation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,oDAAoD;IACpD,aAAa,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC3D;AA8BD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,CAkC7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAsBrE;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAGjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,SAAsB,GAAG,MAAM,CAM3E"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates required environment variables on startup.
|
|
5
|
+
* Fails fast if critical security variables are missing in production.
|
|
6
|
+
*/
|
|
7
|
+
import { createLogger } from './logger';
|
|
8
|
+
const log = createLogger('env-validation');
|
|
9
|
+
/**
|
|
10
|
+
* Default required environment variables for production
|
|
11
|
+
* These are critical for security and must be present
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_REQUIRED_VARS = [
|
|
14
|
+
'LINEAR_WEBHOOK_SECRET',
|
|
15
|
+
'CRON_SECRET',
|
|
16
|
+
'WORKER_API_KEY',
|
|
17
|
+
'SESSION_HASH_SALT',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Default minimum length validations
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_MIN_LENGTH_VARS = [
|
|
23
|
+
{ name: 'SESSION_HASH_SALT', minLength: 32 },
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Check if running in production environment
|
|
27
|
+
*/
|
|
28
|
+
function isProduction() {
|
|
29
|
+
return (process.env.NODE_ENV === 'production' ||
|
|
30
|
+
process.env.VERCEL_ENV === 'production');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate environment variables
|
|
34
|
+
*
|
|
35
|
+
* In production: All required vars must be present
|
|
36
|
+
* In development: Log warnings for missing vars but don't fail
|
|
37
|
+
*
|
|
38
|
+
* @param config - Optional configuration to override defaults
|
|
39
|
+
* @returns Validation result with missing vars
|
|
40
|
+
*/
|
|
41
|
+
export function validateEnv(config) {
|
|
42
|
+
const missing = [];
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const requiredVars = config?.requiredVars ?? DEFAULT_REQUIRED_VARS;
|
|
45
|
+
const minLengthVars = config?.minLengthVars ?? DEFAULT_MIN_LENGTH_VARS;
|
|
46
|
+
for (const varName of requiredVars) {
|
|
47
|
+
if (!process.env[varName]) {
|
|
48
|
+
if (isProduction()) {
|
|
49
|
+
missing.push(varName);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
warnings.push(varName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Additional validation for minimum length variables
|
|
57
|
+
for (const { name, minLength } of minLengthVars) {
|
|
58
|
+
const value = process.env[name];
|
|
59
|
+
if (value && value.length < minLength) {
|
|
60
|
+
const msg = `${name} should be at least ${minLength} characters for security`;
|
|
61
|
+
if (isProduction()) {
|
|
62
|
+
missing.push(`${name} (too short)`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
warnings.push(msg);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
valid: missing.length === 0,
|
|
71
|
+
missing,
|
|
72
|
+
warnings,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validate and fail fast if critical vars are missing
|
|
77
|
+
*
|
|
78
|
+
* Call this at application startup to ensure required
|
|
79
|
+
* environment variables are configured.
|
|
80
|
+
*
|
|
81
|
+
* @param config - Optional configuration to override defaults
|
|
82
|
+
* @throws Error if required vars are missing in production
|
|
83
|
+
*/
|
|
84
|
+
export function validateEnvOrThrow(config) {
|
|
85
|
+
const result = validateEnv(config);
|
|
86
|
+
// Log warnings for development
|
|
87
|
+
if (result.warnings.length > 0) {
|
|
88
|
+
log.warn('Missing environment variables (development mode)', {
|
|
89
|
+
variables: result.warnings,
|
|
90
|
+
hint: 'These are required in production',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Fail in production if required vars are missing
|
|
94
|
+
if (!result.valid) {
|
|
95
|
+
const errorMsg = `Missing required environment variables: ${result.missing.join(', ')}`;
|
|
96
|
+
log.error(errorMsg, {
|
|
97
|
+
missing: result.missing,
|
|
98
|
+
environment: process.env.NODE_ENV,
|
|
99
|
+
});
|
|
100
|
+
throw new Error(errorMsg);
|
|
101
|
+
}
|
|
102
|
+
log.debug('Environment validation passed');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if webhook signature verification is configured
|
|
106
|
+
*/
|
|
107
|
+
export function isWebhookSecretConfigured() {
|
|
108
|
+
return !!process.env.LINEAR_WEBHOOK_SECRET;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if cron authentication is configured
|
|
112
|
+
*/
|
|
113
|
+
export function isCronSecretConfigured() {
|
|
114
|
+
return !!process.env.CRON_SECRET;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if session hashing is configured
|
|
118
|
+
*/
|
|
119
|
+
export function isSessionHashConfigured() {
|
|
120
|
+
const salt = process.env.SESSION_HASH_SALT;
|
|
121
|
+
return !!salt && salt.length >= 32;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get session hash salt
|
|
125
|
+
* @param saltEnvVar - Environment variable name for the salt (default: SESSION_HASH_SALT)
|
|
126
|
+
* @throws Error if not configured
|
|
127
|
+
*/
|
|
128
|
+
export function getSessionHashSalt(saltEnvVar = 'SESSION_HASH_SALT') {
|
|
129
|
+
const salt = process.env[saltEnvVar];
|
|
130
|
+
if (!salt) {
|
|
131
|
+
throw new Error(`${saltEnvVar} not configured`);
|
|
132
|
+
}
|
|
133
|
+
return salt;
|
|
134
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from './logger';
|
|
1
2
|
export * from './types';
|
|
2
3
|
export * from './redis';
|
|
3
4
|
export * from './session-storage';
|
|
@@ -6,4 +7,11 @@ export * from './worker-storage';
|
|
|
6
7
|
export * from './issue-lock';
|
|
7
8
|
export * from './agent-tracking';
|
|
8
9
|
export * from './webhook-idempotency';
|
|
10
|
+
export * from './pending-prompts';
|
|
11
|
+
export * from './orphan-cleanup';
|
|
12
|
+
export * from './worker-auth';
|
|
13
|
+
export * from './session-hash';
|
|
14
|
+
export * from './rate-limit';
|
|
15
|
+
export * from './token-storage';
|
|
16
|
+
export * from './env-validation';
|
|
9
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,SAAS,CAAA;AAGvB,cAAc,SAAS,CAAA;AAGvB,cAAc,mBAAmB,CAAA;AAGjC,cAAc,cAAc,CAAA;AAG5B,cAAc,kBAAkB,CAAA;AAGhC,cAAc,cAAc,CAAA;AAG5B,cAAc,kBAAkB,CAAA;AAGhC,cAAc,uBAAuB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,UAAU,CAAA;AAGxB,cAAc,SAAS,CAAA;AAGvB,cAAc,SAAS,CAAA;AAGvB,cAAc,mBAAmB,CAAA;AAGjC,cAAc,cAAc,CAAA;AAG5B,cAAc,kBAAkB,CAAA;AAGhC,cAAc,cAAc,CAAA;AAG5B,cAAc,kBAAkB,CAAA;AAGhC,cAAc,uBAAuB,CAAA;AAGrC,cAAc,mBAAmB,CAAA;AAGjC,cAAc,kBAAkB,CAAA;AAGhC,cAAc,eAAe,CAAA;AAG7B,cAAc,gBAAgB,CAAA;AAG9B,cAAc,cAAc,CAAA;AAG5B,cAAc,iBAAiB,CAAA;AAG/B,cAAc,kBAAkB,CAAA"}
|
package/dist/src/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
export * from './logger';
|
|
1
3
|
// Types
|
|
2
4
|
export * from './types';
|
|
3
5
|
// Redis client
|
|
@@ -14,3 +16,17 @@ export * from './issue-lock';
|
|
|
14
16
|
export * from './agent-tracking';
|
|
15
17
|
// Webhook idempotency
|
|
16
18
|
export * from './webhook-idempotency';
|
|
19
|
+
// Pending prompts
|
|
20
|
+
export * from './pending-prompts';
|
|
21
|
+
// Orphan cleanup
|
|
22
|
+
export * from './orphan-cleanup';
|
|
23
|
+
// Worker authentication
|
|
24
|
+
export * from './worker-auth';
|
|
25
|
+
// Session hashing
|
|
26
|
+
export * from './session-hash';
|
|
27
|
+
// Rate limiting
|
|
28
|
+
export * from './rate-limit';
|
|
29
|
+
// Token storage
|
|
30
|
+
export * from './token-storage';
|
|
31
|
+
// Environment validation
|
|
32
|
+
export * from './env-validation';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured JSON Logger for AgentFactory Server
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent, structured logging with:
|
|
5
|
+
* - JSON output format for log aggregation
|
|
6
|
+
* - Log levels (debug, info, warn, error)
|
|
7
|
+
* - Context fields (requestId, sessionId, issueId, etc.)
|
|
8
|
+
* - Automatic timestamps
|
|
9
|
+
*/
|
|
10
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
11
|
+
export interface LogContext {
|
|
12
|
+
/** Unique request identifier for tracing */
|
|
13
|
+
requestId?: string;
|
|
14
|
+
/** Linear agent session ID */
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
/** Linear issue ID */
|
|
17
|
+
issueId?: string;
|
|
18
|
+
/** Linear issue identifier (e.g., SUP-123) */
|
|
19
|
+
issueIdentifier?: string;
|
|
20
|
+
/** Linear workspace/organization ID */
|
|
21
|
+
workspaceId?: string;
|
|
22
|
+
/** Duration in milliseconds */
|
|
23
|
+
durationMs?: number;
|
|
24
|
+
/** Error object for error logs */
|
|
25
|
+
error?: Error | unknown;
|
|
26
|
+
/** Any additional context fields */
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Logger class for structured logging
|
|
31
|
+
*/
|
|
32
|
+
declare class Logger {
|
|
33
|
+
private service;
|
|
34
|
+
private defaultContext;
|
|
35
|
+
private minLevel;
|
|
36
|
+
private jsonEnabled;
|
|
37
|
+
constructor(service: string, defaultContext?: LogContext);
|
|
38
|
+
/**
|
|
39
|
+
* Create a child logger with additional default context
|
|
40
|
+
*/
|
|
41
|
+
child(context: LogContext): Logger;
|
|
42
|
+
/**
|
|
43
|
+
* Check if a log level should be output
|
|
44
|
+
*/
|
|
45
|
+
private shouldLog;
|
|
46
|
+
/**
|
|
47
|
+
* Format and output a log entry
|
|
48
|
+
*/
|
|
49
|
+
private log;
|
|
50
|
+
/**
|
|
51
|
+
* Output JSON formatted log
|
|
52
|
+
*/
|
|
53
|
+
private outputJson;
|
|
54
|
+
/**
|
|
55
|
+
* Output human-readable log for development
|
|
56
|
+
*/
|
|
57
|
+
private outputPretty;
|
|
58
|
+
debug(message: string, context?: LogContext): void;
|
|
59
|
+
info(message: string, context?: LogContext): void;
|
|
60
|
+
warn(message: string, context?: LogContext): void;
|
|
61
|
+
error(message: string, context?: LogContext): void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a logger instance for a service
|
|
65
|
+
*/
|
|
66
|
+
export declare function createLogger(service: string, defaultContext?: LogContext): Logger;
|
|
67
|
+
/**
|
|
68
|
+
* Generate a unique request ID
|
|
69
|
+
*/
|
|
70
|
+
export declare function generateRequestId(): string;
|
|
71
|
+
/**
|
|
72
|
+
* Default logger instance for the server
|
|
73
|
+
*/
|
|
74
|
+
export declare const logger: Logger;
|
|
75
|
+
export {};
|
|
76
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAE1D,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8CAA8C;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+BAA+B;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kCAAkC;IAClC,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,CAAA;IACvB,oCAAoC;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAgFD;;GAEG;AACH,cAAM,MAAM;IACV,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,cAAc,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,EAAE,MAAM,EAAE,cAAc,GAAE,UAAe;IAO5D;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM;IAQlC;;OAEG;IACH,OAAO,CAAC,SAAS;IAIjB;;OAEG;IACH,OAAO,CAAC,GAAG;IAuBX;;OAEG;IACH,OAAO,CAAC,UAAU;IAclB;;OAEG;IACH,OAAO,CAAC,YAAY;IAiDpB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI;IAIlD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI;IAIjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI;IAIjD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,IAAI;CAGnD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,cAAc,GAAE,UAAe,GAC9B,MAAM,CAER;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;GAEG;AACH,eAAO,MAAM,MAAM,QAAsC,CAAA"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured JSON Logger for AgentFactory Server
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent, structured logging with:
|
|
5
|
+
* - JSON output format for log aggregation
|
|
6
|
+
* - Log levels (debug, info, warn, error)
|
|
7
|
+
* - Context fields (requestId, sessionId, issueId, etc.)
|
|
8
|
+
* - Automatic timestamps
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Log level priority for filtering
|
|
12
|
+
*/
|
|
13
|
+
const LOG_LEVEL_PRIORITY = {
|
|
14
|
+
debug: 0,
|
|
15
|
+
info: 1,
|
|
16
|
+
warn: 2,
|
|
17
|
+
error: 3,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Get the minimum log level from environment
|
|
21
|
+
* Defaults to 'info' in production, 'debug' in development
|
|
22
|
+
*/
|
|
23
|
+
function getMinLogLevel() {
|
|
24
|
+
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
25
|
+
if (envLevel && envLevel in LOG_LEVEL_PRIORITY) {
|
|
26
|
+
return envLevel;
|
|
27
|
+
}
|
|
28
|
+
return process.env.NODE_ENV === 'production' ? 'info' : 'debug';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if JSON logging is enabled
|
|
32
|
+
* Defaults to true in production, false in development for readability
|
|
33
|
+
*/
|
|
34
|
+
function isJsonLoggingEnabled() {
|
|
35
|
+
const envValue = process.env.LOG_JSON;
|
|
36
|
+
if (envValue !== undefined) {
|
|
37
|
+
return envValue === 'true' || envValue === '1';
|
|
38
|
+
}
|
|
39
|
+
return process.env.NODE_ENV === 'production';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format an error for logging
|
|
43
|
+
*/
|
|
44
|
+
function formatError(error) {
|
|
45
|
+
if (error instanceof Error) {
|
|
46
|
+
return {
|
|
47
|
+
name: error.name,
|
|
48
|
+
message: error.message,
|
|
49
|
+
stack: error.stack,
|
|
50
|
+
...(error.cause ? { cause: formatError(error.cause) } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { message: String(error) };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Format context for logging, handling special cases
|
|
57
|
+
*/
|
|
58
|
+
function formatContext(context) {
|
|
59
|
+
const formatted = {};
|
|
60
|
+
for (const [key, value] of Object.entries(context)) {
|
|
61
|
+
if (value === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
if (key === 'error') {
|
|
64
|
+
formatted.error = formatError(value);
|
|
65
|
+
}
|
|
66
|
+
else if (value instanceof Error) {
|
|
67
|
+
formatted[key] = formatError(value);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
formatted[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return formatted;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Logger class for structured logging
|
|
77
|
+
*/
|
|
78
|
+
class Logger {
|
|
79
|
+
service;
|
|
80
|
+
defaultContext;
|
|
81
|
+
minLevel;
|
|
82
|
+
jsonEnabled;
|
|
83
|
+
constructor(service, defaultContext = {}) {
|
|
84
|
+
this.service = service;
|
|
85
|
+
this.defaultContext = defaultContext;
|
|
86
|
+
this.minLevel = getMinLogLevel();
|
|
87
|
+
this.jsonEnabled = isJsonLoggingEnabled();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Create a child logger with additional default context
|
|
91
|
+
*/
|
|
92
|
+
child(context) {
|
|
93
|
+
const child = new Logger(this.service, {
|
|
94
|
+
...this.defaultContext,
|
|
95
|
+
...context,
|
|
96
|
+
});
|
|
97
|
+
return child;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if a log level should be output
|
|
101
|
+
*/
|
|
102
|
+
shouldLog(level) {
|
|
103
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.minLevel];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Format and output a log entry
|
|
107
|
+
*/
|
|
108
|
+
log(level, message, context = {}) {
|
|
109
|
+
if (!this.shouldLog(level))
|
|
110
|
+
return;
|
|
111
|
+
const mergedContext = { ...this.defaultContext, ...context };
|
|
112
|
+
const formattedContext = formatContext(mergedContext);
|
|
113
|
+
const entry = {
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
level,
|
|
116
|
+
message,
|
|
117
|
+
service: this.service,
|
|
118
|
+
...(Object.keys(formattedContext).length > 0
|
|
119
|
+
? { context: formattedContext }
|
|
120
|
+
: {}),
|
|
121
|
+
};
|
|
122
|
+
if (this.jsonEnabled) {
|
|
123
|
+
this.outputJson(level, entry);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.outputPretty(level, entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Output JSON formatted log
|
|
131
|
+
*/
|
|
132
|
+
outputJson(level, entry) {
|
|
133
|
+
const output = JSON.stringify(entry);
|
|
134
|
+
switch (level) {
|
|
135
|
+
case 'error':
|
|
136
|
+
console.error(output);
|
|
137
|
+
break;
|
|
138
|
+
case 'warn':
|
|
139
|
+
console.warn(output);
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
console.log(output);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Output human-readable log for development
|
|
147
|
+
*/
|
|
148
|
+
outputPretty(level, entry) {
|
|
149
|
+
const levelColors = {
|
|
150
|
+
debug: '\x1b[36m', // cyan
|
|
151
|
+
info: '\x1b[32m', // green
|
|
152
|
+
warn: '\x1b[33m', // yellow
|
|
153
|
+
error: '\x1b[31m', // red
|
|
154
|
+
};
|
|
155
|
+
const reset = '\x1b[0m';
|
|
156
|
+
const dim = '\x1b[2m';
|
|
157
|
+
const color = levelColors[level];
|
|
158
|
+
const levelStr = level.toUpperCase().padEnd(5);
|
|
159
|
+
const time = entry.timestamp.split('T')[1].replace('Z', '');
|
|
160
|
+
let output = `${dim}${time}${reset} ${color}${levelStr}${reset} [${entry.service}] ${entry.message}`;
|
|
161
|
+
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
162
|
+
const contextStr = Object.entries(entry.context)
|
|
163
|
+
.filter(([key]) => key !== 'error')
|
|
164
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
165
|
+
.join(' ');
|
|
166
|
+
if (contextStr) {
|
|
167
|
+
output += ` ${dim}${contextStr}${reset}`;
|
|
168
|
+
}
|
|
169
|
+
// Print error details on separate lines
|
|
170
|
+
if (entry.context.error) {
|
|
171
|
+
const err = entry.context.error;
|
|
172
|
+
output += `\n ${color}${err.name}: ${err.message}${reset}`;
|
|
173
|
+
if (err.stack) {
|
|
174
|
+
const stackLines = err.stack.split('\n').slice(1, 4);
|
|
175
|
+
output += `\n${dim}${stackLines.join('\n')}${reset}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
switch (level) {
|
|
180
|
+
case 'error':
|
|
181
|
+
console.error(output);
|
|
182
|
+
break;
|
|
183
|
+
case 'warn':
|
|
184
|
+
console.warn(output);
|
|
185
|
+
break;
|
|
186
|
+
default:
|
|
187
|
+
console.log(output);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
debug(message, context) {
|
|
191
|
+
this.log('debug', message, context);
|
|
192
|
+
}
|
|
193
|
+
info(message, context) {
|
|
194
|
+
this.log('info', message, context);
|
|
195
|
+
}
|
|
196
|
+
warn(message, context) {
|
|
197
|
+
this.log('warn', message, context);
|
|
198
|
+
}
|
|
199
|
+
error(message, context) {
|
|
200
|
+
this.log('error', message, context);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Create a logger instance for a service
|
|
205
|
+
*/
|
|
206
|
+
export function createLogger(service, defaultContext = {}) {
|
|
207
|
+
return new Logger(service, defaultContext);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Generate a unique request ID
|
|
211
|
+
*/
|
|
212
|
+
export function generateRequestId() {
|
|
213
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Default logger instance for the server
|
|
217
|
+
*/
|
|
218
|
+
export const logger = createLogger('agentfactory-server');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan Cleanup Module
|
|
3
|
+
*
|
|
4
|
+
* Detects and handles orphaned sessions - sessions marked as running/claimed
|
|
5
|
+
* but whose worker is no longer active (heartbeat timeout).
|
|
6
|
+
*
|
|
7
|
+
* When a worker disconnects, the work is re-queued for another worker to resume.
|
|
8
|
+
* The Linear issue status is NOT rolled back - the issue remains in its current
|
|
9
|
+
* workflow state and the next worker will resume from where the previous one left off.
|
|
10
|
+
*/
|
|
11
|
+
import { type AgentSessionState } from './session-storage';
|
|
12
|
+
/**
|
|
13
|
+
* Callback for when an orphaned session is re-queued
|
|
14
|
+
*/
|
|
15
|
+
export interface OrphanCleanupCallbacks {
|
|
16
|
+
/** Called when an orphaned session is re-queued. Use to post Linear comments, etc. */
|
|
17
|
+
onOrphanRequeued?: (session: AgentSessionState) => Promise<void>;
|
|
18
|
+
/** Called when a zombie pending session is recovered. Use to post Linear comments, etc. */
|
|
19
|
+
onZombieRecovered?: (session: AgentSessionState) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface OrphanCleanupResult {
|
|
22
|
+
checked: number;
|
|
23
|
+
orphaned: number;
|
|
24
|
+
requeued: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
details: Array<{
|
|
27
|
+
sessionId: string;
|
|
28
|
+
issueIdentifier: string;
|
|
29
|
+
action: 'requeued' | 'failed';
|
|
30
|
+
reason?: string;
|
|
31
|
+
/** Path to worktree that may need cleanup (if on worker machine) */
|
|
32
|
+
worktreePath?: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Worktree paths that need cleanup on worker machines */
|
|
35
|
+
worktreePathsToCleanup: string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Find sessions that are orphaned (running/claimed but worker is gone)
|
|
39
|
+
*/
|
|
40
|
+
export declare function findOrphanedSessions(): Promise<AgentSessionState[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Find zombie pending sessions — sessions stuck in `pending` status
|
|
43
|
+
* that have no corresponding entry in the work queue or any issue-pending queue.
|
|
44
|
+
*
|
|
45
|
+
* These arise when:
|
|
46
|
+
* - claimWork() removes from queue, but claimSession() fails and requeue also fails
|
|
47
|
+
* - Issue lock expires but promotion fails silently
|
|
48
|
+
*/
|
|
49
|
+
export declare function findZombiePendingSessions(): Promise<AgentSessionState[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Clean up orphaned sessions by re-queuing them
|
|
52
|
+
*
|
|
53
|
+
* @param callbacks - Optional callbacks for external integrations (e.g., posting Linear comments)
|
|
54
|
+
*/
|
|
55
|
+
export declare function cleanupOrphanedSessions(callbacks?: OrphanCleanupCallbacks): Promise<OrphanCleanupResult>;
|
|
56
|
+
export declare function shouldRunCleanup(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Run cleanup if enough time has passed (debounced)
|
|
59
|
+
* Safe to call frequently - will only actually run periodically
|
|
60
|
+
*
|
|
61
|
+
* @param callbacks - Optional callbacks for external integrations
|
|
62
|
+
*/
|
|
63
|
+
export declare function maybeCleanupOrphans(callbacks?: OrphanCleanupCallbacks): Promise<OrphanCleanupResult | null>;
|
|
64
|
+
//# sourceMappingURL=orphan-cleanup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-cleanup.d.ts","sourceRoot":"","sources":["../../src/orphan-cleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAA;AAmB1B;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClE;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,KAAK,CAAC;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,eAAe,EAAE,MAAM,CAAA;QACvB,MAAM,EAAE,UAAU,GAAG,QAAQ,CAAA;QAC7B,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,oEAAoE;QACpE,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAC,CAAA;IACF,0DAA0D;IAC1D,sBAAsB,EAAE,MAAM,EAAE,CAAA;CACjC;AAED;;GAEG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA4CzE;AAKD;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAiC9E;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA+N9B;AASD,wBAAgB,gBAAgB,IAAI,OAAO,CAO1C;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,SAAS,CAAC,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAKrC"}
|