lean-claudient-daemon 0.1.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.
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Input Validation Middleware
3
+ * Express middleware for validating request bodies and parameters
4
+ */
5
+ import { z } from 'zod';
6
+ /**
7
+ * Create validation middleware for a given schema
8
+ */
9
+ export function createValidationMiddleware(schema) {
10
+ return (req, res, next) => {
11
+ try {
12
+ const validated = schema.parse(req.body);
13
+ req.validatedBody = validated;
14
+ next();
15
+ }
16
+ catch (error) {
17
+ if (error instanceof z.ZodError) {
18
+ const errors = error.errors.map((e) => ({
19
+ field: e.path.join('.'),
20
+ message: e.message,
21
+ }));
22
+ return res.status(400).json({
23
+ error: 'Validation failed',
24
+ code: 'VALIDATION_ERROR',
25
+ details: errors,
26
+ });
27
+ }
28
+ return res.status(400).json({
29
+ error: 'Invalid request',
30
+ code: 'INVALID_REQUEST',
31
+ });
32
+ }
33
+ };
34
+ }
35
+ /**
36
+ * Validate request body
37
+ */
38
+ export function validateRequestBody(data, schema) {
39
+ try {
40
+ const parsed = schema.parse(data);
41
+ return { valid: true, data: parsed };
42
+ }
43
+ catch (error) {
44
+ if (error instanceof z.ZodError) {
45
+ const errors = error.errors.map((e) => ({
46
+ field: e.path.join('.'),
47
+ message: e.message,
48
+ }));
49
+ return { valid: false, errors };
50
+ }
51
+ return {
52
+ valid: false,
53
+ errors: [{ field: 'unknown', message: 'Validation failed' }],
54
+ };
55
+ }
56
+ }
57
+ /**
58
+ * Validate UUID in path parameter
59
+ */
60
+ export function validatePathUUID(req, res, next, id, paramName = 'id') {
61
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
62
+ if (!uuidPattern.test(id)) {
63
+ return res.status(400).json({
64
+ error: 'Invalid UUID format',
65
+ code: 'INVALID_UUID',
66
+ details: {
67
+ field: paramName,
68
+ message: 'Must be a valid UUID (36 characters with hyphens)',
69
+ },
70
+ });
71
+ }
72
+ next();
73
+ }
74
+ /**
75
+ * Middleware to validate UUID parameter 'id'
76
+ */
77
+ export function validateIdParam(req, res, next) {
78
+ const { id } = req.params;
79
+ if (!id) {
80
+ return res.status(400).json({
81
+ error: 'Missing id parameter',
82
+ code: 'MISSING_PARAM',
83
+ });
84
+ }
85
+ validatePathUUID(req, res, next, id, 'id');
86
+ }
87
+ /**
88
+ * Middleware to validate UUID parameter 'sessionId'
89
+ */
90
+ export function validateSessionIdParam(req, res, next) {
91
+ const { sessionId } = req.params;
92
+ if (!sessionId) {
93
+ return res.status(400).json({
94
+ error: 'Missing sessionId parameter',
95
+ code: 'MISSING_PARAM',
96
+ });
97
+ }
98
+ validatePathUUID(req, res, next, sessionId, 'sessionId');
99
+ }
100
+ /**
101
+ * Middleware to ensure Content-Type is application/json
102
+ */
103
+ export function validateContentType(req, res, next) {
104
+ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
105
+ const contentType = req.get('Content-Type');
106
+ if (!contentType || !contentType.includes('application/json')) {
107
+ return res.status(415).json({
108
+ error: 'Unsupported Media Type',
109
+ code: 'UNSUPPORTED_MEDIA_TYPE',
110
+ details: {
111
+ message: 'Content-Type must be application/json',
112
+ },
113
+ });
114
+ }
115
+ }
116
+ next();
117
+ }
118
+ /**
119
+ * Middleware to limit request body size
120
+ */
121
+ export function validateRequestSize(maxSize = '1mb') {
122
+ return (req, res, next) => {
123
+ if (!req.get('Content-Length')) {
124
+ return next();
125
+ }
126
+ const sizeBytes = parseSize(maxSize);
127
+ const contentLength = parseInt(req.get('Content-Length') || '0', 10);
128
+ if (contentLength > sizeBytes) {
129
+ return res.status(413).json({
130
+ error: 'Payload Too Large',
131
+ code: 'PAYLOAD_TOO_LARGE',
132
+ details: {
133
+ maxSize,
134
+ received: `${(contentLength / 1024).toFixed(2)}kb`,
135
+ },
136
+ });
137
+ }
138
+ next();
139
+ };
140
+ }
141
+ /**
142
+ * Parse size string to bytes
143
+ */
144
+ function parseSize(size) {
145
+ const units = {
146
+ b: 1,
147
+ kb: 1024,
148
+ mb: 1024 * 1024,
149
+ gb: 1024 * 1024 * 1024,
150
+ };
151
+ const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(kb|mb|gb|b)?$/);
152
+ if (!match)
153
+ return 1024 * 1024; // default 1mb
154
+ const [, number, unit = 'b'] = match;
155
+ return Math.floor(parseFloat(number) * (units[unit] || 1));
156
+ }
157
+ /**
158
+ * Validate query parameters
159
+ */
160
+ export function validateQueryParams(data, allowedKeys) {
161
+ if (typeof data !== 'object' || data === null) {
162
+ return { valid: false, errors: [{ field: 'query', message: 'Invalid query parameters' }] };
163
+ }
164
+ const errors = [];
165
+ const validated = {};
166
+ for (const [key, value] of Object.entries(data)) {
167
+ if (!allowedKeys.includes(key)) {
168
+ errors.push({
169
+ field: key,
170
+ message: `Unknown parameter: ${key}`,
171
+ });
172
+ }
173
+ else {
174
+ validated[key] = value;
175
+ }
176
+ }
177
+ if (errors.length > 0) {
178
+ return { valid: false, errors };
179
+ }
180
+ return { valid: true, data: validated };
181
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Environment Validation
3
+ * Validates daemon security configuration at startup
4
+ */
5
+ import { existsSync, accessSync, constants } from 'fs';
6
+ import { dirname } from 'path';
7
+ // Mock detectSecrets function since core/security is not accessible from daemon
8
+ function detectSecrets(text) {
9
+ // Placeholder for secrets detection
10
+ return [];
11
+ }
12
+ /**
13
+ * Check if process environment contains suspicious secrets
14
+ */
15
+ export function checkEnvironmentSecrets() {
16
+ const envString = JSON.stringify(process.env);
17
+ const findings = detectSecrets(envString);
18
+ if (findings.length === 0) {
19
+ return { status: 'PASS', message: 'No hardcoded secrets in environment' };
20
+ }
21
+ const critical = findings.filter((f) => f.severity === 'CRITICAL');
22
+ const high = findings.filter((f) => f.severity === 'HIGH');
23
+ if (critical.length > 0) {
24
+ return {
25
+ status: 'WARN',
26
+ message: `Found ${critical.length} CRITICAL and ${high.length} HIGH severity secrets in environment`,
27
+ };
28
+ }
29
+ return {
30
+ status: 'PASS',
31
+ message: `Found ${high.length} potential high-entropy strings (may be expected secrets)`,
32
+ };
33
+ }
34
+ /**
35
+ * Validate daemon port is not privileged
36
+ */
37
+ export function checkDaemonPort(port) {
38
+ if (port < 1024) {
39
+ return {
40
+ status: 'FAIL',
41
+ message: `Port ${port} is privileged (<1024). Use unprivileged port or run as root (not recommended).`,
42
+ };
43
+ }
44
+ if (port > 65535) {
45
+ return {
46
+ status: 'FAIL',
47
+ message: `Port ${port} is invalid (>65535)`,
48
+ };
49
+ }
50
+ return {
51
+ status: 'PASS',
52
+ message: `Daemon port ${port} is valid`,
53
+ };
54
+ }
55
+ /**
56
+ * Check if log directory is writable
57
+ */
58
+ export function checkLogDirectory(logDir) {
59
+ // Create directory if it doesn't exist
60
+ if (!existsSync(logDir)) {
61
+ try {
62
+ const parentDir = dirname(logDir);
63
+ if (!existsSync(parentDir)) {
64
+ return {
65
+ status: 'WARN',
66
+ message: `Log directory parent doesn't exist: ${parentDir}`,
67
+ };
68
+ }
69
+ accessSync(parentDir, constants.W_OK);
70
+ return {
71
+ status: 'PASS',
72
+ message: `Log directory can be created: ${logDir}`,
73
+ };
74
+ }
75
+ catch {
76
+ return {
77
+ status: 'WARN',
78
+ message: `Cannot write to log directory parent: ${dirname(logDir)}`,
79
+ };
80
+ }
81
+ }
82
+ try {
83
+ accessSync(logDir, constants.W_OK);
84
+ return {
85
+ status: 'PASS',
86
+ message: `Log directory is writable: ${logDir}`,
87
+ };
88
+ }
89
+ catch {
90
+ return {
91
+ status: 'WARN',
92
+ message: `Log directory is not writable: ${logDir}`,
93
+ };
94
+ }
95
+ }
96
+ /**
97
+ * Check if database path is writable
98
+ */
99
+ export function checkDatabasePath(dbPath) {
100
+ const dbDir = dirname(dbPath);
101
+ if (!existsSync(dbDir)) {
102
+ const parentDir = dirname(dbDir);
103
+ try {
104
+ accessSync(parentDir, constants.W_OK);
105
+ return {
106
+ status: 'PASS',
107
+ message: `Database directory can be created: ${dbDir}`,
108
+ };
109
+ }
110
+ catch {
111
+ return {
112
+ status: 'WARN',
113
+ message: `Cannot write to database directory parent: ${parentDir}`,
114
+ };
115
+ }
116
+ }
117
+ try {
118
+ accessSync(dbDir, constants.W_OK);
119
+ return {
120
+ status: 'PASS',
121
+ message: `Database directory is writable: ${dbDir}`,
122
+ };
123
+ }
124
+ catch {
125
+ return {
126
+ status: 'WARN',
127
+ message: `Database directory is not writable: ${dbDir}`,
128
+ };
129
+ }
130
+ }
131
+ /**
132
+ * Check required environment variables
133
+ */
134
+ export function checkRequiredEnvVars(required) {
135
+ const missing = required.filter((key) => !process.env[key]);
136
+ if (missing.length === 0) {
137
+ return {
138
+ status: 'PASS',
139
+ message: `All required environment variables are set`,
140
+ };
141
+ }
142
+ return {
143
+ status: 'FAIL',
144
+ message: `Missing environment variables: ${missing.join(', ')}`,
145
+ };
146
+ }
147
+ /**
148
+ * Check for deprecated or insecure environment variables
149
+ */
150
+ export function checkDeprecatedEnvVars() {
151
+ const deprecated = [
152
+ 'DEBUG',
153
+ 'VERBOSE',
154
+ 'LOG_LEVEL', // prefer structured logging
155
+ ];
156
+ const found = deprecated.filter((key) => process.env[key]);
157
+ if (found.length === 0) {
158
+ return {
159
+ status: 'PASS',
160
+ message: 'No deprecated environment variables detected',
161
+ };
162
+ }
163
+ return {
164
+ status: 'WARN',
165
+ message: `Found deprecated environment variables: ${found.join(', ')}`,
166
+ };
167
+ }
168
+ /**
169
+ * Validate NODE_ENV is set appropriately
170
+ */
171
+ export function checkNodeEnv() {
172
+ const nodeEnv = process.env.NODE_ENV || 'development';
173
+ if (nodeEnv === 'production' || nodeEnv === 'staging' || nodeEnv === 'development') {
174
+ return {
175
+ status: 'PASS',
176
+ message: `NODE_ENV is set to: ${nodeEnv}`,
177
+ };
178
+ }
179
+ return {
180
+ status: 'WARN',
181
+ message: `NODE_ENV has unexpected value: ${nodeEnv}. Should be production, staging, or development.`,
182
+ };
183
+ }
184
+ /**
185
+ * Check memory and resource limits
186
+ */
187
+ export function checkResourceLimits() {
188
+ const memUsage = process.memoryUsage();
189
+ const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
190
+ const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
191
+ if (heapTotalMB > 4096) {
192
+ return {
193
+ status: 'WARN',
194
+ message: `Daemon has high heap allocation: ${heapTotalMB}MB (${heapUsedMB}MB used)`,
195
+ };
196
+ }
197
+ return {
198
+ status: 'PASS',
199
+ message: `Memory usage: ${heapUsedMB}MB / ${heapTotalMB}MB`,
200
+ };
201
+ }
202
+ /**
203
+ * Full validation suite
204
+ */
205
+ export function validateEnvironment(config) {
206
+ const checks = [
207
+ { name: 'Environment Secrets', ...checkEnvironmentSecrets() },
208
+ { name: 'Daemon Port', ...checkDaemonPort(config.port) },
209
+ { name: 'Log Directory', ...checkLogDirectory(config.logDir) },
210
+ { name: 'Database Path', ...checkDatabasePath(config.dbPath) },
211
+ { name: 'NODE_ENV', ...checkNodeEnv() },
212
+ { name: 'Deprecated Variables', ...checkDeprecatedEnvVars() },
213
+ { name: 'Resource Limits', ...checkResourceLimits() },
214
+ ];
215
+ if (config.requiredEnvVars && config.requiredEnvVars.length > 0) {
216
+ checks.push({
217
+ name: 'Required Variables',
218
+ ...checkRequiredEnvVars(config.requiredEnvVars),
219
+ });
220
+ }
221
+ const failures = checks.filter((c) => c.status === 'FAIL');
222
+ const warnings = checks.filter((c) => c.status === 'WARN');
223
+ const errors = failures.map((c) => c.message);
224
+ // If critical mode and failures exist, fail entire validation
225
+ const passed = config.critical ? failures.length === 0 : true;
226
+ return {
227
+ passed,
228
+ checks,
229
+ errors,
230
+ warnings: warnings.map((c) => c.message),
231
+ };
232
+ }
233
+ /**
234
+ * Format validation result for console output
235
+ */
236
+ export function formatValidationResult(result) {
237
+ const lines = [];
238
+ lines.push('\n=== Security Environment Validation ===\n');
239
+ for (const check of result.checks) {
240
+ const symbol = check.status === 'PASS' ? '✓' : check.status === 'WARN' ? '⚠' : '✗';
241
+ lines.push(`${symbol} ${check.name}: ${check.message}`);
242
+ }
243
+ if (result.errors.length > 0) {
244
+ lines.push('\nErrors:');
245
+ for (const error of result.errors) {
246
+ lines.push(` ✗ ${error}`);
247
+ }
248
+ }
249
+ if (result.warnings.length > 0) {
250
+ lines.push('\nWarnings:');
251
+ for (const warning of result.warnings) {
252
+ lines.push(` ⚠ ${warning}`);
253
+ }
254
+ }
255
+ lines.push(`\nValidation Status: ${result.passed ? 'PASSED' : 'FAILED'}\n`);
256
+ return lines.join('\n');
257
+ }
258
+ /**
259
+ * Exit with validation error
260
+ */
261
+ export function exitOnValidationFailure(result, exitCode = 1) {
262
+ if (!result.passed) {
263
+ console.error(formatValidationResult(result));
264
+ process.exit(exitCode);
265
+ }
266
+ }
@@ -0,0 +1,47 @@
1
+ import express from 'express';
2
+ import { MetricsStore } from './metricsStore.js';
3
+ import { createApiRouter } from './api.js';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ export class LeanClaudientDaemon {
7
+ constructor(port = 9831, logPath) {
8
+ this.app = express();
9
+ this.port = port;
10
+ this.logPath = logPath || path.join(process.env.HOME, '.lean-claudient', 'daemon.log');
11
+ this.metricsStore = new MetricsStore();
12
+ this.setupExpress();
13
+ }
14
+ setupExpress() {
15
+ this.app.use(express.json());
16
+ this.app.use('/api', createApiRouter(this.metricsStore));
17
+ this.app.get('/health', (req, res) => {
18
+ res.json({ status: 'ok', uptime: process.uptime(), pid: process.pid });
19
+ });
20
+ }
21
+ async start() {
22
+ const logDir = path.dirname(this.logPath);
23
+ if (!fs.existsSync(logDir)) {
24
+ fs.mkdirSync(logDir, { recursive: true });
25
+ }
26
+ this.log('Lean Claudient Daemon starting...');
27
+ await this.metricsStore.initialize();
28
+ this.app.listen(this.port, () => {
29
+ this.log(`Daemon listening on port ${this.port}`);
30
+ });
31
+ process.on('SIGTERM', () => this.shutdown());
32
+ process.on('SIGINT', () => this.shutdown());
33
+ await new Promise(() => { });
34
+ }
35
+ async shutdown() {
36
+ this.log('Shutting down daemon...');
37
+ await this.metricsStore.close();
38
+ this.log('Daemon stopped cleanly');
39
+ process.exit(0);
40
+ }
41
+ log(msg) {
42
+ const timestamp = new Date().toISOString();
43
+ const line = `[${timestamp}] ${msg}\n`;
44
+ console.log(line);
45
+ fs.appendFileSync(this.logPath, line);
46
+ }
47
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Input Validation Schemas
3
+ * Zod schemas for all API inputs and daemon operations
4
+ */
5
+ import { z } from 'zod';
6
+ /**
7
+ * UUID validation - 36 characters with hyphens
8
+ */
9
+ export const uuidSchema = z.string().uuid().describe('Valid UUID format');
10
+ /**
11
+ * Session ID schema
12
+ */
13
+ export const sessionIdSchema = z
14
+ .string()
15
+ .uuid()
16
+ .describe('Session ID must be a valid UUID');
17
+ /**
18
+ * Metrics record schema
19
+ */
20
+ export const metricsRecordSchema = z.object({
21
+ sessionId: sessionIdSchema,
22
+ timestamp: z.number().int().positive().describe('Unix timestamp'),
23
+ inputTokens: z.number().int().nonnegative().describe('Input tokens used'),
24
+ outputTokens: z.number().int().nonnegative().describe('Output tokens generated'),
25
+ totalTokens: z.number().int().nonnegative().describe('Total tokens (input + output)'),
26
+ cost: z.number().nonnegative().describe('Cost in USD'),
27
+ savedTokens: z
28
+ .number()
29
+ .int()
30
+ .nonnegative()
31
+ .describe('Tokens saved through compression'),
32
+ compressionRatio: z.number().nonnegative().describe('Compression ratio percentage'),
33
+ subagentCount: z.number().int().nonnegative().describe('Number of subagents used'),
34
+ });
35
+ /**
36
+ * Budget status schema
37
+ */
38
+ export const budgetStatusSchema = z.object({
39
+ sessionId: sessionIdSchema,
40
+ remainingTokens: z.number().int().nonnegative(),
41
+ totalBudget: z.number().int().positive(),
42
+ usedTokens: z.number().int().nonnegative(),
43
+ percentUsed: z.number().min(0).max(100),
44
+ status: z.enum(['ACTIVE', 'WARNING', 'CRITICAL', 'EXCEEDED']),
45
+ });
46
+ /**
47
+ * Metrics payload schema for POST /api/metrics
48
+ */
49
+ export const metricsPayloadSchema = z.object({
50
+ sessionId: sessionIdSchema,
51
+ metrics: metricsRecordSchema.omit({ sessionId: true }),
52
+ });
53
+ /**
54
+ * Checkpoint schema - valid git commit hash or checkpoint identifier
55
+ */
56
+ export const checkpointSchema = z
57
+ .object({
58
+ checkpoint: z.string().min(1).max(1000).describe('Checkpoint identifier or hash'),
59
+ })
60
+ .strict();
61
+ /**
62
+ * Budget schema for budget updates
63
+ */
64
+ export const budgetSchema = z
65
+ .object({
66
+ sessionId: sessionIdSchema,
67
+ totalBudget: z.number().int().positive().describe('Total token budget'),
68
+ softLimit: z.number().int().positive().optional().describe('Warning threshold'),
69
+ hardLimit: z.number().int().positive().optional().describe('Absolute maximum'),
70
+ })
71
+ .strict();
72
+ /**
73
+ * Session creation schema
74
+ */
75
+ export const sessionCreationSchema = z
76
+ .object({
77
+ sessionId: sessionIdSchema,
78
+ budget: z.number().int().positive().describe('Initial token budget'),
79
+ metadata: z.record(z.any()).optional().describe('Custom metadata'),
80
+ })
81
+ .strict();
82
+ /**
83
+ * Query validation schema
84
+ */
85
+ export const queryParamSchema = z.object({
86
+ sessionId: sessionIdSchema.optional(),
87
+ startTime: z.number().int().nonnegative().optional(),
88
+ endTime: z.number().int().nonnegative().optional(),
89
+ limit: z.number().int().min(1).max(1000).optional(),
90
+ });
91
+ /**
92
+ * Error response schema
93
+ */
94
+ export const errorResponseSchema = z.object({
95
+ error: z.string(),
96
+ code: z.string().optional(),
97
+ details: z.any().optional(),
98
+ });
99
+ /**
100
+ * Success response schema
101
+ */
102
+ export const successResponseSchema = z.object({
103
+ success: z.boolean(),
104
+ data: z.any().optional(),
105
+ message: z.string().optional(),
106
+ });
107
+ /**
108
+ * Health check response schema
109
+ */
110
+ export const healthCheckSchema = z.object({
111
+ running: z.boolean(),
112
+ uptime: z.number().int().nonnegative(),
113
+ memory: z.number().nonnegative(),
114
+ pid: z.number().int().positive(),
115
+ timestamp: z.string().datetime().optional(),
116
+ version: z.string().optional(),
117
+ });
118
+ /**
119
+ * Aggregate metrics schema
120
+ */
121
+ export const aggregateMetricsSchema = z.object({
122
+ totalInputTokens: z.number().int().nonnegative(),
123
+ totalOutputTokens: z.number().int().nonnegative(),
124
+ totalTokens: z.number().int().nonnegative(),
125
+ totalCost: z.string(),
126
+ totalSavedTokens: z.number().int().nonnegative(),
127
+ averageCompressionRatio: z.number().nonnegative(),
128
+ sessionCount: z.number().int().nonnegative(),
129
+ averageSubagentCount: z.number().int().nonnegative(),
130
+ });
131
+ /**
132
+ * Validate UUID in path parameter
133
+ */
134
+ export function validateUUID(id) {
135
+ try {
136
+ return uuidSchema.parse(id);
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ }
142
+ /**
143
+ * Safe parse helper - returns [data, error]
144
+ */
145
+ export function safeParse(schema, data) {
146
+ try {
147
+ const result = schema.parse(data);
148
+ return [result, null];
149
+ }
150
+ catch (error) {
151
+ if (error instanceof z.ZodError) {
152
+ const messages = error.errors.map((e) => `${e.path.join('.')}: ${e.message}`);
153
+ return [null, messages.join('; ')];
154
+ }
155
+ return [null, String(error)];
156
+ }
157
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "lean-claudient-daemon",
3
+ "version": "0.1.0",
4
+ "description": "Universal status daemon for Lean Claudient: token budget enforcement across CLIs",
5
+ "type": "module",
6
+ "main": "dist/service.js",
7
+ "bin": {
8
+ "lean-claudient-daemon": "dist/bin/daemon.js"
9
+ },
10
+ "author": "Lean Claudient Contributors",
11
+ "license": "MIT",
12
+ "homepage": "https://github.com/Claudient/LeanClaudient#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/Claudient/LeanClaudient.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/Claudient/LeanClaudient/issues"
19
+ },
20
+ "keywords": [
21
+ "claude",
22
+ "daemon",
23
+ "token-budget",
24
+ "llm",
25
+ "anthropic"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "dev": "tsc --watch",
30
+ "test": "echo 'tests pending'",
31
+ "test:watch": "echo 'tests pending'",
32
+ "test:coverage": "echo 'tests pending'",
33
+ "lint": "echo 'linter pending'",
34
+ "format": "echo 'formatter pending'",
35
+ "type-check": "tsc --noEmit",
36
+ "start": "node dist/bin/daemon.js"
37
+ },
38
+ "dependencies": {
39
+ "express": "^4.18.2",
40
+ "sqlite3": "^5.1.6",
41
+ "commander": "^11.1.0",
42
+ "zod": "^3.22.4"
43
+ },
44
+ "devDependencies": {
45
+ "@types/express": "^4.17.21",
46
+ "@types/node": "^20.10.0",
47
+ "typescript": "^5.3.2"
48
+ }
49
+ }