cli-copilot-worker 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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/cli-copilot-worker.mjs +3 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +682 -0
  6. package/dist/src/cli.js.map +1 -0
  7. package/dist/src/core/copilot.d.ts +13 -0
  8. package/dist/src/core/copilot.js +56 -0
  9. package/dist/src/core/copilot.js.map +1 -0
  10. package/dist/src/core/failure-classifier.d.ts +8 -0
  11. package/dist/src/core/failure-classifier.js +148 -0
  12. package/dist/src/core/failure-classifier.js.map +1 -0
  13. package/dist/src/core/ids.d.ts +1 -0
  14. package/dist/src/core/ids.js +11 -0
  15. package/dist/src/core/ids.js.map +1 -0
  16. package/dist/src/core/markdown.d.ts +5 -0
  17. package/dist/src/core/markdown.js +14 -0
  18. package/dist/src/core/markdown.js.map +1 -0
  19. package/dist/src/core/paths.d.ts +18 -0
  20. package/dist/src/core/paths.js +42 -0
  21. package/dist/src/core/paths.js.map +1 -0
  22. package/dist/src/core/profile-faults.d.ts +15 -0
  23. package/dist/src/core/profile-faults.js +110 -0
  24. package/dist/src/core/profile-faults.js.map +1 -0
  25. package/dist/src/core/profile-manager.d.ts +25 -0
  26. package/dist/src/core/profile-manager.js +162 -0
  27. package/dist/src/core/profile-manager.js.map +1 -0
  28. package/dist/src/core/question-registry.d.ts +25 -0
  29. package/dist/src/core/question-registry.js +154 -0
  30. package/dist/src/core/question-registry.js.map +1 -0
  31. package/dist/src/core/store.d.ts +39 -0
  32. package/dist/src/core/store.js +206 -0
  33. package/dist/src/core/store.js.map +1 -0
  34. package/dist/src/core/types.d.ts +152 -0
  35. package/dist/src/core/types.js +2 -0
  36. package/dist/src/core/types.js.map +1 -0
  37. package/dist/src/daemon/client.d.ts +6 -0
  38. package/dist/src/daemon/client.js +117 -0
  39. package/dist/src/daemon/client.js.map +1 -0
  40. package/dist/src/daemon/server.d.ts +1 -0
  41. package/dist/src/daemon/server.js +149 -0
  42. package/dist/src/daemon/server.js.map +1 -0
  43. package/dist/src/daemon/service.d.ts +69 -0
  44. package/dist/src/daemon/service.js +800 -0
  45. package/dist/src/daemon/service.js.map +1 -0
  46. package/dist/src/doctor.d.ts +1 -0
  47. package/dist/src/doctor.js +74 -0
  48. package/dist/src/doctor.js.map +1 -0
  49. package/dist/src/index.d.ts +3 -0
  50. package/dist/src/index.js +4 -0
  51. package/dist/src/index.js.map +1 -0
  52. package/dist/src/output.d.ts +28 -0
  53. package/dist/src/output.js +307 -0
  54. package/dist/src/output.js.map +1 -0
  55. package/package.json +59 -0
  56. package/src/cli.ts +881 -0
  57. package/src/core/copilot.ts +75 -0
  58. package/src/core/failure-classifier.ts +202 -0
  59. package/src/core/ids.ts +11 -0
  60. package/src/core/markdown.ts +19 -0
  61. package/src/core/paths.ts +56 -0
  62. package/src/core/profile-faults.ts +140 -0
  63. package/src/core/profile-manager.ts +220 -0
  64. package/src/core/question-registry.ts +191 -0
  65. package/src/core/store.ts +273 -0
  66. package/src/core/types.ts +211 -0
  67. package/src/daemon/client.ts +137 -0
  68. package/src/daemon/server.ts +167 -0
  69. package/src/daemon/service.ts +968 -0
  70. package/src/doctor.ts +82 -0
  71. package/src/index.ts +3 -0
  72. package/src/output.ts +391 -0
@@ -0,0 +1,211 @@
1
+ export type ConversationStatus =
2
+ | 'idle'
3
+ | 'running'
4
+ | 'waiting_answer'
5
+ | 'failed'
6
+ | 'cancelled';
7
+
8
+ export type JobStatus =
9
+ | 'running'
10
+ | 'waiting_answer'
11
+ | 'completed'
12
+ | 'failed'
13
+ | 'cancelled';
14
+
15
+ export type JobKind = 'run' | 'send' | 'answer';
16
+
17
+ export type TranscriptRole =
18
+ | 'user'
19
+ | 'assistant'
20
+ | 'tool_use'
21
+ | 'tool_result'
22
+ | 'question'
23
+ | 'answer'
24
+ | 'status'
25
+ | 'error';
26
+
27
+ export type CopilotFailureCategory =
28
+ | 'auth'
29
+ | 'rate_limit'
30
+ | 'connection'
31
+ | 'transient'
32
+ | 'fatal';
33
+
34
+ export interface CopilotFailureInfo {
35
+ category: CopilotFailureCategory;
36
+ retryable: boolean;
37
+ message: string;
38
+ sessionErrorType?: string | undefined;
39
+ sessionStatusCode?: number | undefined;
40
+ }
41
+
42
+ export interface JobAttemptRecord {
43
+ profileId: string;
44
+ profileConfigDir: string;
45
+ startedAt: string;
46
+ endedAt?: string | undefined;
47
+ outcome: 'running' | 'completed' | 'failed';
48
+ failureCategory?: CopilotFailureCategory | undefined;
49
+ failureMessage?: string | undefined;
50
+ sessionErrorType?: string | undefined;
51
+ sessionStatusCode?: number | undefined;
52
+ }
53
+
54
+ export interface PendingQuestion {
55
+ question: string;
56
+ choices?: string[] | undefined;
57
+ allowFreeform: boolean;
58
+ askedAt: string;
59
+ sessionId: string;
60
+ }
61
+
62
+ export interface ConversationRecord {
63
+ id: string;
64
+ sessionId: string;
65
+ workspaceId: string;
66
+ cwd: string;
67
+ model: string;
68
+ status: ConversationStatus;
69
+ createdAt: string;
70
+ updatedAt: string;
71
+ hasStarted: boolean;
72
+ nextEntryIndex: number;
73
+ lastJobId?: string | undefined;
74
+ pendingQuestion?: PendingQuestion | undefined;
75
+ profileId?: string | undefined;
76
+ profileConfigDir?: string | undefined;
77
+ lastError?: string | undefined;
78
+ }
79
+
80
+ export interface JobRecord {
81
+ id: string;
82
+ conversationId: string;
83
+ kind: JobKind;
84
+ status: JobStatus;
85
+ inputFilePath: string;
86
+ createdAt: string;
87
+ startedAt: string;
88
+ endedAt?: string | undefined;
89
+ error?: string | undefined;
90
+ pid?: number | undefined;
91
+ turnIndex: number;
92
+ startEntryIndex: number;
93
+ endEntryIndex?: number | undefined;
94
+ attempts: JobAttemptRecord[];
95
+ }
96
+
97
+ export interface TranscriptEntry {
98
+ index: number;
99
+ conversationId: string;
100
+ jobId?: string | undefined;
101
+ role: TranscriptRole;
102
+ content: string;
103
+ timestamp: string;
104
+ data?: Record<string, unknown> | undefined;
105
+ }
106
+
107
+ export interface PersistedProfileState {
108
+ id: string;
109
+ configDir: string;
110
+ cooldownUntil?: number | undefined;
111
+ failureCount: number;
112
+ lastFailureReason?: string | undefined;
113
+ lastFailureCategory?: CopilotFailureCategory | undefined;
114
+ lastFailureAt?: string | undefined;
115
+ lastSuccessAt?: string | undefined;
116
+ }
117
+
118
+ export interface StateFile {
119
+ version: 1;
120
+ conversations: ConversationRecord[];
121
+ jobs: JobRecord[];
122
+ profiles: PersistedProfileState[];
123
+ }
124
+
125
+ export interface CopilotProfile {
126
+ id: string;
127
+ configDir: string;
128
+ cooldownUntil?: number | undefined;
129
+ failureCount: number;
130
+ lastFailureReason?: string | undefined;
131
+ lastFailureCategory?: CopilotFailureCategory | undefined;
132
+ lastFailureAt?: string | undefined;
133
+ lastSuccessAt?: string | undefined;
134
+ }
135
+
136
+ export interface DaemonMeta {
137
+ pid: number;
138
+ socketPath: string;
139
+ token: string;
140
+ startedAt: string;
141
+ }
142
+
143
+ export interface RunRequest {
144
+ inputFilePath: string;
145
+ content: string;
146
+ cwd: string;
147
+ model?: string | undefined;
148
+ timeoutMs?: number | undefined;
149
+ async?: boolean | undefined;
150
+ }
151
+
152
+ export interface SendRequest {
153
+ conversationId: string;
154
+ inputFilePath: string;
155
+ content: string;
156
+ timeoutMs?: number | undefined;
157
+ async?: boolean | undefined;
158
+ }
159
+
160
+ export interface AnswerRequest {
161
+ conversationId: string;
162
+ inputFilePath: string;
163
+ content: string;
164
+ }
165
+
166
+ export interface ReadRequest {
167
+ conversationId: string;
168
+ from?: number | undefined;
169
+ to?: number | undefined;
170
+ after?: number | undefined;
171
+ before?: number | undefined;
172
+ tokens?: number | undefined;
173
+ detail?: 'standard' | 'verbose' | 'full' | 'meta' | undefined;
174
+ }
175
+
176
+ export interface JobWaitRequest {
177
+ jobId: string;
178
+ timeoutSeconds?: number | undefined;
179
+ intervalSeconds?: number | undefined;
180
+ }
181
+
182
+ export type DaemonCommand =
183
+ | 'ping'
184
+ | 'run'
185
+ | 'send'
186
+ | 'answer'
187
+ | 'read'
188
+ | 'list'
189
+ | 'info'
190
+ | 'job.list'
191
+ | 'job.status'
192
+ | 'job.wait'
193
+ | 'job.read'
194
+ | 'job.cancel'
195
+ | 'daemon.stop'
196
+ | 'daemon.status';
197
+
198
+ export interface DaemonRequestEnvelope {
199
+ id: string;
200
+ token: string;
201
+ command: DaemonCommand;
202
+ args?: Record<string, unknown> | undefined;
203
+ }
204
+
205
+ export interface DaemonResponseEnvelope {
206
+ id: string;
207
+ type: 'event' | 'result' | 'error';
208
+ event?: string | undefined;
209
+ data?: Record<string, unknown> | undefined;
210
+ error?: string | undefined;
211
+ }
@@ -0,0 +1,137 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import net from 'node:net';
4
+ import { spawn } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { buildDaemonToken, ensureStateRoot } from '../core/paths.js';
8
+ import type { DaemonMeta, DaemonRequestEnvelope, DaemonResponseEnvelope } from '../core/types.js';
9
+
10
+ function delay(ms: number): Promise<void> {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+
14
+ function cliLaunchSpec(): { command: string; args: string[] } {
15
+ const compiledCliPath = fileURLToPath(new URL('../cli.js', import.meta.url));
16
+ if (existsSync(compiledCliPath)) {
17
+ return {
18
+ command: process.execPath,
19
+ args: ['--import', 'tsx', compiledCliPath, 'daemon-run'],
20
+ };
21
+ }
22
+
23
+ const sourceCliPath = fileURLToPath(new URL('../cli.ts', import.meta.url));
24
+ return {
25
+ command: process.execPath,
26
+ args: ['--import', 'tsx', sourceCliPath, 'daemon-run'],
27
+ };
28
+ }
29
+
30
+ async function readDaemonMeta(): Promise<DaemonMeta | null> {
31
+ const { daemonMetaPath } = ensureStateRoot();
32
+ if (!existsSync(daemonMetaPath)) {
33
+ return null;
34
+ }
35
+
36
+ try {
37
+ const raw = await readFile(daemonMetaPath, 'utf8');
38
+ return JSON.parse(raw) as DaemonMeta;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ async function canConnect(socketPath: string): Promise<boolean> {
45
+ return await new Promise((resolve) => {
46
+ const socket = net.createConnection(socketPath, () => {
47
+ socket.end();
48
+ resolve(true);
49
+ });
50
+ socket.on('error', () => resolve(false));
51
+ });
52
+ }
53
+
54
+ export async function ensureDaemonMeta(): Promise<DaemonMeta> {
55
+ const existing = await readDaemonMeta();
56
+ if (existing && await canConnect(existing.socketPath)) {
57
+ return existing;
58
+ }
59
+
60
+ const { socketPath } = ensureStateRoot();
61
+ const token = buildDaemonToken();
62
+ const launchSpec = cliLaunchSpec();
63
+ const child = spawn(launchSpec.command, launchSpec.args, {
64
+ detached: true,
65
+ stdio: 'ignore',
66
+ env: {
67
+ ...process.env,
68
+ CLI_COPILOT_WORKER_DAEMON_SOCKET: socketPath,
69
+ CLI_COPILOT_WORKER_DAEMON_TOKEN: token,
70
+ },
71
+ });
72
+ child.unref();
73
+
74
+ for (let attempt = 0; attempt < 50; attempt += 1) {
75
+ const current = await readDaemonMeta();
76
+ if (current && await canConnect(current.socketPath)) {
77
+ return current;
78
+ }
79
+ await delay(100);
80
+ }
81
+
82
+ throw new Error('Timed out waiting for cli-copilot-worker daemon to start');
83
+ }
84
+
85
+ export async function sendDaemonRequest(
86
+ command: DaemonRequestEnvelope['command'],
87
+ args?: Record<string, unknown>,
88
+ options?: {
89
+ onEvent?: ((event: string, data: Record<string, unknown>) => void) | undefined;
90
+ },
91
+ ): Promise<Record<string, unknown>> {
92
+ const meta = await ensureDaemonMeta();
93
+ const request: DaemonRequestEnvelope = {
94
+ id: `${Date.now()}`,
95
+ token: meta.token,
96
+ command,
97
+ args,
98
+ };
99
+
100
+ return await new Promise<Record<string, unknown>>((resolve, reject) => {
101
+ let buffer = '';
102
+ const socket = net.createConnection(meta.socketPath);
103
+
104
+ socket.on('connect', () => {
105
+ socket.write(`${JSON.stringify(request)}\n`);
106
+ });
107
+
108
+ socket.on('data', (chunk) => {
109
+ buffer += chunk.toString('utf8');
110
+ let newlineIndex = buffer.indexOf('\n');
111
+ while (newlineIndex >= 0) {
112
+ const line = buffer.slice(0, newlineIndex).trim();
113
+ buffer = buffer.slice(newlineIndex + 1);
114
+ if (line) {
115
+ const envelope = JSON.parse(line) as DaemonResponseEnvelope;
116
+ if (envelope.type === 'event') {
117
+ options?.onEvent?.(envelope.event ?? 'event', envelope.data ?? {});
118
+ } else if (envelope.type === 'result') {
119
+ socket.end();
120
+ resolve(envelope.data ?? {});
121
+ } else {
122
+ socket.end();
123
+ reject(new Error(envelope.error ?? 'Unknown daemon error'));
124
+ }
125
+ }
126
+ newlineIndex = buffer.indexOf('\n');
127
+ }
128
+ });
129
+
130
+ socket.on('error', reject);
131
+ });
132
+ }
133
+
134
+ export async function daemonIsRunning(): Promise<boolean> {
135
+ const meta = await readDaemonMeta();
136
+ return Boolean(meta && await canConnect(meta.socketPath));
137
+ }
@@ -0,0 +1,167 @@
1
+ import net from 'node:net';
2
+ import { existsSync } from 'node:fs';
3
+ import { unlink } from 'node:fs/promises';
4
+
5
+ import { ensureStateRoot } from '../core/paths.js';
6
+ import type { DaemonRequestEnvelope, DaemonResponseEnvelope } from '../core/types.js';
7
+ import { CliCopilotWorkerService } from './service.js';
8
+
9
+ function writeEnvelope(socket: net.Socket, envelope: DaemonResponseEnvelope): void {
10
+ socket.write(`${JSON.stringify(envelope)}\n`);
11
+ }
12
+
13
+ export async function runDaemonServer(): Promise<void> {
14
+ const socketPath = process.env.CLI_COPILOT_WORKER_DAEMON_SOCKET ?? ensureStateRoot().socketPath;
15
+ const token = process.env.CLI_COPILOT_WORKER_DAEMON_TOKEN;
16
+ if (!token) {
17
+ throw new Error('Missing CLI_COPILOT_WORKER_DAEMON_TOKEN');
18
+ }
19
+
20
+ if (existsSync(socketPath)) {
21
+ await unlink(socketPath).catch(() => {});
22
+ }
23
+
24
+ const service = new CliCopilotWorkerService();
25
+ await service.initialize();
26
+ await service.writeDaemonMeta(socketPath, token);
27
+
28
+ const shutdown = async () => {
29
+ await service.shutdown().catch(() => {});
30
+ server.close();
31
+ process.exit(0);
32
+ };
33
+
34
+ const server = net.createServer((socket) => {
35
+ let buffer = '';
36
+ socket.on('data', (chunk) => {
37
+ buffer += chunk.toString('utf8');
38
+ let newlineIndex = buffer.indexOf('\n');
39
+ while (newlineIndex >= 0) {
40
+ const line = buffer.slice(0, newlineIndex).trim();
41
+ buffer = buffer.slice(newlineIndex + 1);
42
+ if (line) {
43
+ void handleRequest(line, socket, token, service, shutdown);
44
+ }
45
+ newlineIndex = buffer.indexOf('\n');
46
+ }
47
+ });
48
+ });
49
+
50
+ await new Promise<void>((resolve, reject) => {
51
+ server.once('error', reject);
52
+ server.listen(socketPath, () => resolve());
53
+ });
54
+
55
+ process.on('SIGINT', () => void shutdown());
56
+ process.on('SIGTERM', () => void shutdown());
57
+ }
58
+
59
+ async function handleRequest(
60
+ line: string,
61
+ socket: net.Socket,
62
+ token: string,
63
+ service: CliCopilotWorkerService,
64
+ shutdown: () => Promise<void>,
65
+ ): Promise<void> {
66
+ let envelope: DaemonRequestEnvelope;
67
+ try {
68
+ envelope = JSON.parse(line) as DaemonRequestEnvelope;
69
+ } catch {
70
+ writeEnvelope(socket, {
71
+ id: 'unknown',
72
+ type: 'error',
73
+ error: 'Invalid JSON request',
74
+ });
75
+ return;
76
+ }
77
+
78
+ if (envelope.token !== token) {
79
+ writeEnvelope(socket, {
80
+ id: envelope.id,
81
+ type: 'error',
82
+ error: 'Unauthorized',
83
+ });
84
+ return;
85
+ }
86
+
87
+ const writer = {
88
+ event(name: string, data: Record<string, unknown>) {
89
+ writeEnvelope(socket, {
90
+ id: envelope.id,
91
+ type: 'event',
92
+ event: name,
93
+ data,
94
+ });
95
+ },
96
+ };
97
+
98
+ try {
99
+ let result: Record<string, unknown>;
100
+ let shouldShutdown = false;
101
+ switch (envelope.command) {
102
+ case 'ping':
103
+ result = await service.ping();
104
+ break;
105
+ case 'run':
106
+ result = await service.run(envelope.args as never, writer);
107
+ break;
108
+ case 'send':
109
+ result = await service.send(envelope.args as never, writer);
110
+ break;
111
+ case 'answer':
112
+ result = await service.answer(envelope.args as never);
113
+ break;
114
+ case 'read':
115
+ result = await service.read(envelope.args as never);
116
+ break;
117
+ case 'list':
118
+ result = await service.list();
119
+ break;
120
+ case 'info':
121
+ result = await service.info(String((envelope.args ?? {}).conversationId));
122
+ break;
123
+ case 'job.list':
124
+ result = await service.jobList();
125
+ break;
126
+ case 'job.status':
127
+ result = await service.jobStatus(String((envelope.args ?? {}).jobId));
128
+ break;
129
+ case 'job.wait':
130
+ result = await service.jobWait(envelope.args as never);
131
+ break;
132
+ case 'job.read':
133
+ result = await service.jobRead(String((envelope.args ?? {}).jobId));
134
+ break;
135
+ case 'job.cancel':
136
+ result = await service.jobCancel(String((envelope.args ?? {}).jobId));
137
+ break;
138
+ case 'daemon.status':
139
+ result = await service.daemonStatus();
140
+ break;
141
+ case 'daemon.stop':
142
+ result = await service.shutdown();
143
+ shouldShutdown = true;
144
+ break;
145
+ default:
146
+ throw new Error(`Unknown command: ${envelope.command}`);
147
+ }
148
+
149
+ writeEnvelope(socket, {
150
+ id: envelope.id,
151
+ type: 'result',
152
+ data: result,
153
+ });
154
+ if (shouldShutdown) {
155
+ socket.end();
156
+ setTimeout(() => {
157
+ void shutdown();
158
+ }, 10).unref();
159
+ }
160
+ } catch (error) {
161
+ writeEnvelope(socket, {
162
+ id: envelope.id,
163
+ type: 'error',
164
+ error: error instanceof Error ? error.message : String(error),
165
+ });
166
+ }
167
+ }