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.
Files changed (61) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +282 -0
  3. package/bin/cli.js +8 -0
  4. package/bin/daemon.js +7 -0
  5. package/package.json +70 -0
  6. package/src/agent/AGENTS.md +249 -0
  7. package/src/agent/agent.prompts.ts +66 -0
  8. package/src/agent/agent.test-runner.schemas.ts +158 -0
  9. package/src/agent/agent.test-runner.ts +436 -0
  10. package/src/agent/agent.ts +371 -0
  11. package/src/agent/agent.types.ts +94 -0
  12. package/src/backend/AGENTS.md +112 -0
  13. package/src/backend/backend.protocol.ts +95 -0
  14. package/src/backend/backend.schemas.ts +123 -0
  15. package/src/backend/backend.services.ts +151 -0
  16. package/src/backend/backend.ts +111 -0
  17. package/src/backend/backend.types.ts +34 -0
  18. package/src/cli/AGENTS.md +213 -0
  19. package/src/cli/cli.agent.ts +197 -0
  20. package/src/cli/cli.chat.ts +369 -0
  21. package/src/cli/cli.client.ts +55 -0
  22. package/src/cli/cli.collections.ts +491 -0
  23. package/src/cli/cli.config.ts +252 -0
  24. package/src/cli/cli.daemon.ts +160 -0
  25. package/src/cli/cli.documents.ts +413 -0
  26. package/src/cli/cli.mcp.ts +177 -0
  27. package/src/cli/cli.ts +28 -0
  28. package/src/cli/cli.utils.ts +122 -0
  29. package/src/client/AGENTS.md +135 -0
  30. package/src/client/client.adapters.ts +279 -0
  31. package/src/client/client.ts +86 -0
  32. package/src/client/client.types.ts +17 -0
  33. package/src/collections/AGENTS.md +185 -0
  34. package/src/collections/collections.schemas.ts +195 -0
  35. package/src/collections/collections.ts +1160 -0
  36. package/src/config/config.ts +118 -0
  37. package/src/daemon/AGENTS.md +168 -0
  38. package/src/daemon/daemon.config.ts +23 -0
  39. package/src/daemon/daemon.manager.ts +215 -0
  40. package/src/daemon/daemon.schemas.ts +22 -0
  41. package/src/daemon/daemon.ts +205 -0
  42. package/src/database/AGENTS.md +211 -0
  43. package/src/database/database.ts +64 -0
  44. package/src/database/migrations/migrations.001-init.ts +56 -0
  45. package/src/database/migrations/migrations.002-fts5.ts +32 -0
  46. package/src/database/migrations/migrations.ts +20 -0
  47. package/src/database/migrations/migrations.types.ts +9 -0
  48. package/src/documents/AGENTS.md +301 -0
  49. package/src/documents/documents.schemas.ts +190 -0
  50. package/src/documents/documents.ts +734 -0
  51. package/src/embedder/embedder.ts +53 -0
  52. package/src/exports.ts +0 -0
  53. package/src/mcp/AGENTS.md +264 -0
  54. package/src/mcp/mcp.ts +105 -0
  55. package/src/tools/AGENTS.md +228 -0
  56. package/src/tools/agent/agent.ts +45 -0
  57. package/src/tools/documents/documents.ts +401 -0
  58. package/src/tools/tools.langchain.ts +37 -0
  59. package/src/tools/tools.mcp.ts +46 -0
  60. package/src/tools/tools.types.ts +35 -0
  61. 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 };