ctxpkg 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +282 -0
- package/bin/cli.js +8 -0
- package/bin/daemon.js +7 -0
- package/package.json +70 -0
- package/src/agent/AGENTS.md +249 -0
- package/src/agent/agent.prompts.ts +66 -0
- package/src/agent/agent.test-runner.schemas.ts +158 -0
- package/src/agent/agent.test-runner.ts +436 -0
- package/src/agent/agent.ts +371 -0
- package/src/agent/agent.types.ts +94 -0
- package/src/backend/AGENTS.md +112 -0
- package/src/backend/backend.protocol.ts +95 -0
- package/src/backend/backend.schemas.ts +123 -0
- package/src/backend/backend.services.ts +151 -0
- package/src/backend/backend.ts +111 -0
- package/src/backend/backend.types.ts +34 -0
- package/src/cli/AGENTS.md +213 -0
- package/src/cli/cli.agent.ts +197 -0
- package/src/cli/cli.chat.ts +369 -0
- package/src/cli/cli.client.ts +55 -0
- package/src/cli/cli.collections.ts +491 -0
- package/src/cli/cli.config.ts +252 -0
- package/src/cli/cli.daemon.ts +160 -0
- package/src/cli/cli.documents.ts +413 -0
- package/src/cli/cli.mcp.ts +177 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/cli.utils.ts +122 -0
- package/src/client/AGENTS.md +135 -0
- package/src/client/client.adapters.ts +279 -0
- package/src/client/client.ts +86 -0
- package/src/client/client.types.ts +17 -0
- package/src/collections/AGENTS.md +185 -0
- package/src/collections/collections.schemas.ts +195 -0
- package/src/collections/collections.ts +1160 -0
- package/src/config/config.ts +118 -0
- package/src/daemon/AGENTS.md +168 -0
- package/src/daemon/daemon.config.ts +23 -0
- package/src/daemon/daemon.manager.ts +215 -0
- package/src/daemon/daemon.schemas.ts +22 -0
- package/src/daemon/daemon.ts +205 -0
- package/src/database/AGENTS.md +211 -0
- package/src/database/database.ts +64 -0
- package/src/database/migrations/migrations.001-init.ts +56 -0
- package/src/database/migrations/migrations.002-fts5.ts +32 -0
- package/src/database/migrations/migrations.ts +20 -0
- package/src/database/migrations/migrations.types.ts +9 -0
- package/src/documents/AGENTS.md +301 -0
- package/src/documents/documents.schemas.ts +190 -0
- package/src/documents/documents.ts +734 -0
- package/src/embedder/embedder.ts +53 -0
- package/src/exports.ts +0 -0
- package/src/mcp/AGENTS.md +264 -0
- package/src/mcp/mcp.ts +105 -0
- package/src/tools/AGENTS.md +228 -0
- package/src/tools/agent/agent.ts +45 -0
- package/src/tools/documents/documents.ts +401 -0
- package/src/tools/tools.langchain.ts +37 -0
- package/src/tools/tools.mcp.ts +46 -0
- package/src/tools/tools.types.ts +35 -0
- package/src/utils/utils.services.ts +46 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
import convict from 'convict';
|
|
6
|
+
import envPaths from 'env-paths';
|
|
7
|
+
|
|
8
|
+
const paths = envPaths('ctxpkg', { suffix: '' });
|
|
9
|
+
const configPath = join(paths.config, 'config.json');
|
|
10
|
+
|
|
11
|
+
// Use ~/.ctxpkg for runtime files to avoid spaces in path (ws library URL encoding issue)
|
|
12
|
+
const runtimeDir = join(homedir(), '.ctxpkg');
|
|
13
|
+
|
|
14
|
+
const config = convict({
|
|
15
|
+
database: {
|
|
16
|
+
path: {
|
|
17
|
+
doc: 'Path to the SQLite database file',
|
|
18
|
+
format: String,
|
|
19
|
+
default: join(paths.data, 'database.sqlite'),
|
|
20
|
+
env: 'CTXPKG_DATABASE_PATH',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
llm: {
|
|
24
|
+
provider: {
|
|
25
|
+
doc: 'OpenAI-compatible API base URL',
|
|
26
|
+
format: String,
|
|
27
|
+
default: 'https://api.openai.com/v1',
|
|
28
|
+
env: 'CTXPKG_LLM_PROVIDER',
|
|
29
|
+
},
|
|
30
|
+
model: {
|
|
31
|
+
doc: 'Model identifier to use for agent reasoning',
|
|
32
|
+
format: String,
|
|
33
|
+
default: 'gpt-4o-mini',
|
|
34
|
+
env: 'CTXPKG_LLM_MODEL',
|
|
35
|
+
},
|
|
36
|
+
apiKey: {
|
|
37
|
+
doc: 'API key for the LLM provider',
|
|
38
|
+
format: String,
|
|
39
|
+
default: '',
|
|
40
|
+
env: 'CTXPKG_LLM_API_KEY',
|
|
41
|
+
sensitive: true,
|
|
42
|
+
},
|
|
43
|
+
temperature: {
|
|
44
|
+
doc: 'Temperature for LLM responses (0-2)',
|
|
45
|
+
format: Number,
|
|
46
|
+
default: 0,
|
|
47
|
+
env: 'CTXPKG_LLM_TEMPERATURE',
|
|
48
|
+
},
|
|
49
|
+
maxTokens: {
|
|
50
|
+
doc: 'Maximum tokens for LLM responses',
|
|
51
|
+
format: 'nat',
|
|
52
|
+
default: 4096,
|
|
53
|
+
env: 'CTXPKG_LLM_MAX_TOKENS',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
daemon: {
|
|
57
|
+
socketPath: {
|
|
58
|
+
doc: 'Path to the daemon Unix socket file',
|
|
59
|
+
format: String,
|
|
60
|
+
default: join(runtimeDir, 'daemon.sock'),
|
|
61
|
+
env: 'CTXPKG_SOCKET_PATH',
|
|
62
|
+
},
|
|
63
|
+
pidFile: {
|
|
64
|
+
doc: 'Path to the daemon PID file',
|
|
65
|
+
format: String,
|
|
66
|
+
default: join(runtimeDir, 'daemon.pid'),
|
|
67
|
+
env: 'CTXPKG_PID_FILE',
|
|
68
|
+
},
|
|
69
|
+
idleTimeout: {
|
|
70
|
+
doc: 'Idle timeout in milliseconds before daemon shuts down (0 to disable)',
|
|
71
|
+
format: 'nat',
|
|
72
|
+
default: 0,
|
|
73
|
+
env: 'CTXPKG_IDLE_TIMEOUT',
|
|
74
|
+
},
|
|
75
|
+
autoStart: {
|
|
76
|
+
doc: 'Automatically start daemon when CLI commands need it',
|
|
77
|
+
format: Boolean,
|
|
78
|
+
default: true,
|
|
79
|
+
env: 'CTXPKG_AUTO_START',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
project: {
|
|
83
|
+
configFile: {
|
|
84
|
+
doc: 'Filename for project configuration file',
|
|
85
|
+
format: String,
|
|
86
|
+
default: 'context.json',
|
|
87
|
+
env: 'CTXPKG_PROJECT_CONFIG_FILE',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
global: {
|
|
91
|
+
configFile: {
|
|
92
|
+
doc: 'Path to global collections config file',
|
|
93
|
+
format: String,
|
|
94
|
+
default: join(paths.config, 'global-context.json'),
|
|
95
|
+
env: 'CTXPKG_GLOBAL_CONFIG_FILE',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Ensure config directory exists for future writes, but don't fail if we can't read yet
|
|
101
|
+
if (existsSync(configPath)) {
|
|
102
|
+
try {
|
|
103
|
+
config.loadFile(configPath);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn(`Failed to load config from ${configPath}:`, e);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
config.validate({ allowed: 'strict' });
|
|
110
|
+
|
|
111
|
+
export { config, configPath };
|
|
112
|
+
|
|
113
|
+
export const saveConfig = () => {
|
|
114
|
+
if (!existsSync(paths.config)) {
|
|
115
|
+
mkdirSync(paths.config, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
writeFileSync(configPath, JSON.stringify(config.get(), null, 2));
|
|
118
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Daemon — Agent Guidelines
|
|
2
|
+
|
|
3
|
+
This document describes the daemon module architecture for AI agents working on this codebase.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The daemon module provides a background process that hosts the Backend service. It listens on a Unix socket, accepts WebSocket connections, and routes JSON-RPC requests to the Backend. The daemon supports idle timeout for automatic shutdown and can be auto-started on demand.
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
|------|---------|
|
|
13
|
+
| `daemon.ts` | `Daemon` class — server lifecycle, connections, idle timeout |
|
|
14
|
+
| `daemon.manager.ts` | `DaemonManager` — start/stop/status from external processes |
|
|
15
|
+
| `daemon.config.ts` | Config accessors (socket path, PID file, timeouts) |
|
|
16
|
+
| `daemon.schemas.ts` | Zod schemas for `DaemonStatus` and `DaemonOptions` |
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
22
|
+
│ Daemon │
|
|
23
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
24
|
+
│ │ HTTP Server │ │
|
|
25
|
+
│ │ (Unix socket listener) │ │
|
|
26
|
+
│ └──────────────────────┬──────────────────────────────┘ │
|
|
27
|
+
│ │ │
|
|
28
|
+
│ ┌──────────────────────▼──────────────────────────────┐ │
|
|
29
|
+
│ │ WebSocket Server │ │
|
|
30
|
+
│ │ (handles client connections) │ │
|
|
31
|
+
│ └──────────────────────┬──────────────────────────────┘ │
|
|
32
|
+
│ │ │
|
|
33
|
+
│ ┌─────────────┼─────────────┐ │
|
|
34
|
+
│ ▼ ▼ ▼ │
|
|
35
|
+
│ [Client 1] [Client 2] [Client N] │
|
|
36
|
+
│ │ │ │ │
|
|
37
|
+
│ └─────────────┼─────────────┘ │
|
|
38
|
+
│ ▼ │
|
|
39
|
+
│ ┌──────────────────────────────────────────────────────┐ │
|
|
40
|
+
│ │ Backend │ │
|
|
41
|
+
│ │ (request handling) │ │
|
|
42
|
+
│ └──────────────────────────────────────────────────────┘ │
|
|
43
|
+
└─────────────────────────────────────────────────────────────┘
|
|
44
|
+
|
|
45
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ DaemonManager │
|
|
47
|
+
│ (runs in CLI/client process, controls daemon externally) │
|
|
48
|
+
│ │
|
|
49
|
+
│ isRunning() → ping via socket │
|
|
50
|
+
│ start() → spawn detached process │
|
|
51
|
+
│ stop() → send system.shutdown │
|
|
52
|
+
│ getStatus() → query system.status │
|
|
53
|
+
└─────────────────────────────────────────────────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Daemon Lifecycle
|
|
57
|
+
|
|
58
|
+
### Startup
|
|
59
|
+
|
|
60
|
+
1. Create data directory if needed
|
|
61
|
+
2. Remove stale socket file
|
|
62
|
+
3. Write PID file
|
|
63
|
+
4. Create HTTP server on Unix socket
|
|
64
|
+
5. Attach WebSocket server
|
|
65
|
+
6. Start idle timer (if no connections)
|
|
66
|
+
7. Register signal handlers (SIGTERM, SIGINT)
|
|
67
|
+
|
|
68
|
+
### Connections
|
|
69
|
+
|
|
70
|
+
- Each WebSocket connection is tracked
|
|
71
|
+
- Messages are parsed as JSON and routed to `Backend.handleRequest()`
|
|
72
|
+
- Responses are sent back as JSON
|
|
73
|
+
- Connection count is updated on connect/disconnect
|
|
74
|
+
|
|
75
|
+
### Idle Timeout
|
|
76
|
+
|
|
77
|
+
- Timer starts when connection count reaches 0
|
|
78
|
+
- Default: 5 minutes (configurable via `daemon.idleTimeout`)
|
|
79
|
+
- Set to 0 to disable auto-shutdown
|
|
80
|
+
- Timer is cleared when a client connects
|
|
81
|
+
|
|
82
|
+
### Shutdown
|
|
83
|
+
|
|
84
|
+
1. Clear idle timer
|
|
85
|
+
2. Close all WebSocket connections
|
|
86
|
+
3. Close WebSocket server
|
|
87
|
+
4. Close HTTP server
|
|
88
|
+
5. Cleanup Backend resources
|
|
89
|
+
6. Remove socket and PID files
|
|
90
|
+
7. Exit process
|
|
91
|
+
|
|
92
|
+
## DaemonManager
|
|
93
|
+
|
|
94
|
+
Used by CLI and client to control the daemon:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const manager = new DaemonManager();
|
|
98
|
+
|
|
99
|
+
// Check if running
|
|
100
|
+
if (await manager.isRunning()) {
|
|
101
|
+
console.log('Daemon is running');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Start (spawns detached process)
|
|
105
|
+
await manager.start();
|
|
106
|
+
|
|
107
|
+
// Stop (sends shutdown command)
|
|
108
|
+
await manager.stop();
|
|
109
|
+
|
|
110
|
+
// Get status
|
|
111
|
+
const status = await manager.getStatus();
|
|
112
|
+
// { running, socketPath, pid, uptime, connections }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Auto-Start
|
|
116
|
+
|
|
117
|
+
`DaemonManager.ensureRunning()` will start the daemon if not running (when `autoStart` is enabled). This is used by `DaemonAdapter` in the client.
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
Accessed via `daemon.config.ts`:
|
|
122
|
+
|
|
123
|
+
| Config Key | Default | Description |
|
|
124
|
+
|------------|---------|-------------|
|
|
125
|
+
| `daemon.socketPath` | `~/.ai-assist/daemon.sock` | Unix socket path |
|
|
126
|
+
| `daemon.pidFile` | `~/.ai-assist/daemon.pid` | PID file path |
|
|
127
|
+
| `daemon.idleTimeout` | `300000` (5 min) | Idle shutdown timeout (ms), 0 to disable |
|
|
128
|
+
| `daemon.autoStart` | `true` | Auto-start daemon when client connects |
|
|
129
|
+
|
|
130
|
+
## Entry Point
|
|
131
|
+
|
|
132
|
+
The daemon is started via `bin/daemon.js`:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { Daemon } from '#root/daemon/daemon.ts';
|
|
136
|
+
|
|
137
|
+
const daemon = new Daemon();
|
|
138
|
+
await daemon.start();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This script is spawned as a detached process by `DaemonManager.start()`.
|
|
142
|
+
|
|
143
|
+
## Key Patterns
|
|
144
|
+
|
|
145
|
+
### Process Management
|
|
146
|
+
|
|
147
|
+
- PID file tracks the daemon process
|
|
148
|
+
- Detached spawn ensures daemon outlives parent
|
|
149
|
+
- Socket file existence + ping confirms daemon is alive
|
|
150
|
+
|
|
151
|
+
### Graceful Shutdown
|
|
152
|
+
|
|
153
|
+
Always handle shutdown signals:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
process.on('SIGTERM', () => daemon.stop());
|
|
157
|
+
process.on('SIGINT', () => daemon.stop());
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Client Connection Format
|
|
161
|
+
|
|
162
|
+
Clients connect via WebSocket to the Unix socket:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const socket = new WebSocket(`ws+unix://${socketPath}:/.`);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This is handled by the `ws` library's Unix socket support.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { config } from '#root/config/config.ts';
|
|
2
|
+
|
|
3
|
+
const getSocketPath = (): string => {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
return (config as any).get('daemon.socketPath') as string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const getPidFile = (): string => {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
return (config as any).get('daemon.pidFile') as string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getIdleTimeout = (): number => {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
return (config as any).get('daemon.idleTimeout') as number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getAutoStart = (): boolean => {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
return (config as any).get('daemon.autoStart') as boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { getSocketPath, getPidFile, getIdleTimeout, getAutoStart };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access, readFile } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { WebSocket } from 'ws';
|
|
7
|
+
|
|
8
|
+
import { getSocketPath, getPidFile, getAutoStart } from './daemon.config.ts';
|
|
9
|
+
import type { DaemonStatus } from './daemon.schemas.ts';
|
|
10
|
+
|
|
11
|
+
type DaemonManagerOptions = {
|
|
12
|
+
socketPath?: string;
|
|
13
|
+
pidFile?: string;
|
|
14
|
+
autoStart?: boolean;
|
|
15
|
+
startTimeout?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
class DaemonManager {
|
|
19
|
+
#socketPath: string;
|
|
20
|
+
#pidFile: string;
|
|
21
|
+
#autoStart: boolean;
|
|
22
|
+
#startTimeout: number;
|
|
23
|
+
|
|
24
|
+
constructor(options?: DaemonManagerOptions) {
|
|
25
|
+
this.#socketPath = options?.socketPath ?? getSocketPath();
|
|
26
|
+
this.#pidFile = options?.pidFile ?? getPidFile();
|
|
27
|
+
this.#autoStart = options?.autoStart ?? getAutoStart();
|
|
28
|
+
this.#startTimeout = options?.startTimeout ?? 30000;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getSocketPath(): string {
|
|
32
|
+
return this.#socketPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async isRunning(): Promise<boolean> {
|
|
36
|
+
// Check if socket file exists
|
|
37
|
+
try {
|
|
38
|
+
await access(this.#socketPath);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try to connect and ping
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const socket = new WebSocket(`ws+unix://${this.#socketPath}:/.`);
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
socket.close();
|
|
48
|
+
resolve(false);
|
|
49
|
+
}, 2000);
|
|
50
|
+
|
|
51
|
+
socket.on('open', () => {
|
|
52
|
+
// Send ping request
|
|
53
|
+
socket.send(JSON.stringify({ id: 'ping', method: 'system.ping', params: {} }));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
socket.on('message', (data) => {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
try {
|
|
59
|
+
const response = JSON.parse(data.toString());
|
|
60
|
+
socket.close();
|
|
61
|
+
resolve(response.result?.pong === true);
|
|
62
|
+
} catch {
|
|
63
|
+
socket.close();
|
|
64
|
+
resolve(false);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
socket.on('error', () => {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
resolve(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async ensureRunning(): Promise<void> {
|
|
76
|
+
if (await this.isRunning()) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this.#autoStart) {
|
|
81
|
+
throw new Error('Daemon is not running and autoStart is disabled');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await this.start();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async start(): Promise<void> {
|
|
88
|
+
if (await this.isRunning()) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Find the daemon entry point
|
|
93
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
94
|
+
const projectRoot = dirname(dirname(dirname(currentFile)));
|
|
95
|
+
const daemonScript = join(projectRoot, 'bin', 'daemon.js');
|
|
96
|
+
|
|
97
|
+
// Spawn detached daemon process
|
|
98
|
+
const child = spawn(process.execPath, [daemonScript], {
|
|
99
|
+
detached: true,
|
|
100
|
+
stdio: 'ignore',
|
|
101
|
+
env: {
|
|
102
|
+
...process.env,
|
|
103
|
+
AI_ASSIST_DAEMON: '1',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
child.unref();
|
|
108
|
+
|
|
109
|
+
// Wait for socket to become available
|
|
110
|
+
await this.#waitForSocket();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #waitForSocket(): Promise<void> {
|
|
114
|
+
const startTime = Date.now();
|
|
115
|
+
const pollInterval = 100;
|
|
116
|
+
|
|
117
|
+
while (Date.now() - startTime < this.#startTimeout) {
|
|
118
|
+
if (await this.isRunning()) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
throw new Error(`Daemon failed to start within ${this.#startTimeout}ms`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async stop(): Promise<void> {
|
|
128
|
+
if (!(await this.isRunning())) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Send shutdown command
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const socket = new WebSocket(`ws+unix://${this.#socketPath}:/.`);
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
socket.close();
|
|
137
|
+
reject(new Error('Shutdown request timed out'));
|
|
138
|
+
}, 5000);
|
|
139
|
+
|
|
140
|
+
socket.on('open', () => {
|
|
141
|
+
socket.send(JSON.stringify({ id: 'shutdown', method: 'system.shutdown', params: {} }));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
socket.on('message', () => {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
socket.close();
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
socket.on('error', (error) => {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
reject(error);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
socket.on('close', () => {
|
|
156
|
+
clearTimeout(timeout);
|
|
157
|
+
resolve();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getStatus(): Promise<DaemonStatus | null> {
|
|
163
|
+
if (!(await this.isRunning())) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const socket = new WebSocket(`ws+unix://${this.#socketPath}:/.`);
|
|
169
|
+
const timeout = setTimeout(() => {
|
|
170
|
+
socket.close();
|
|
171
|
+
resolve(null);
|
|
172
|
+
}, 2000);
|
|
173
|
+
|
|
174
|
+
socket.on('open', () => {
|
|
175
|
+
socket.send(JSON.stringify({ id: 'status', method: 'system.status', params: {} }));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
socket.on('message', async (data) => {
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
try {
|
|
181
|
+
const response = JSON.parse(data.toString());
|
|
182
|
+
socket.close();
|
|
183
|
+
|
|
184
|
+
// Read PID from file
|
|
185
|
+
let pid = 0;
|
|
186
|
+
try {
|
|
187
|
+
const pidContent = await readFile(this.#pidFile, 'utf8');
|
|
188
|
+
pid = parseInt(pidContent, 10);
|
|
189
|
+
} catch {
|
|
190
|
+
// Ignore
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
resolve({
|
|
194
|
+
running: true,
|
|
195
|
+
socketPath: this.#socketPath,
|
|
196
|
+
pid,
|
|
197
|
+
uptime: response.result?.uptime ?? 0,
|
|
198
|
+
connections: response.result?.connections ?? 0,
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
socket.close();
|
|
202
|
+
resolve(null);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
socket.on('error', () => {
|
|
207
|
+
clearTimeout(timeout);
|
|
208
|
+
resolve(null);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { DaemonManager };
|
|
215
|
+
export type { DaemonManagerOptions };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const daemonStatusSchema = z.object({
|
|
4
|
+
running: z.boolean(),
|
|
5
|
+
socketPath: z.string(),
|
|
6
|
+
pid: z.number(),
|
|
7
|
+
uptime: z.number(),
|
|
8
|
+
connections: z.number(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
type DaemonStatus = z.infer<typeof daemonStatusSchema>;
|
|
12
|
+
|
|
13
|
+
const daemonOptionsSchema = z.object({
|
|
14
|
+
socketPath: z.string().optional(),
|
|
15
|
+
idleTimeout: z.number().default(300000), // 5 minutes
|
|
16
|
+
pidFile: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type DaemonOptions = z.infer<typeof daemonOptionsSchema>;
|
|
20
|
+
|
|
21
|
+
export type { DaemonStatus, DaemonOptions };
|
|
22
|
+
export { daemonStatusSchema, daemonOptionsSchema };
|