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,75 @@
1
+ import { accessSync, constants } from 'node:fs';
2
+ import { delimiter, join } from 'node:path';
3
+
4
+ import { CopilotClient, type CopilotClientOptions } from '@github/copilot-sdk';
5
+
6
+ import type { CopilotProfile } from './types.js';
7
+
8
+ function canExecute(path: string): boolean {
9
+ try {
10
+ accessSync(path, constants.X_OK);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export function resolveCopilotCliPath(explicitPath?: string): string {
18
+ if (explicitPath && canExecute(explicitPath)) {
19
+ return explicitPath;
20
+ }
21
+
22
+ const configuredPath = process.env.COPILOT_CLI_PATH;
23
+ if (configuredPath && canExecute(configuredPath)) {
24
+ return configuredPath;
25
+ }
26
+
27
+ for (const segment of (process.env.PATH ?? '').split(delimiter)) {
28
+ if (!segment) {
29
+ continue;
30
+ }
31
+
32
+ const candidate = join(segment, 'copilot');
33
+ if (canExecute(candidate)) {
34
+ return candidate;
35
+ }
36
+ }
37
+
38
+ return explicitPath ?? configuredPath ?? 'copilot';
39
+ }
40
+
41
+ export function buildCopilotClientOptions(input: {
42
+ cwd: string;
43
+ profile: CopilotProfile;
44
+ cliPath?: string | undefined;
45
+ }): CopilotClientOptions {
46
+ return {
47
+ cliPath: resolveCopilotCliPath(input.cliPath),
48
+ cliArgs: ['--config-dir', input.profile.configDir],
49
+ cwd: input.cwd,
50
+ useLoggedInUser: true,
51
+ useStdio: false,
52
+ autoStart: true,
53
+ autoRestart: false,
54
+ logLevel: 'error',
55
+ };
56
+ }
57
+
58
+ export async function buildAuthenticatedClient(input: {
59
+ cwd: string;
60
+ profile: CopilotProfile;
61
+ cliPath?: string | undefined;
62
+ }): Promise<CopilotClient> {
63
+ const client = new CopilotClient(buildCopilotClientOptions(input));
64
+ await client.start();
65
+ const auth = await client.getAuthStatus();
66
+ if (!auth.isAuthenticated) {
67
+ await client.stop().catch(async () => {
68
+ await client.forceStop().catch(() => {});
69
+ });
70
+ throw new Error(
71
+ `Copilot profile "${input.profile.configDir}" is not authenticated. Run \`copilot login --config-dir ${input.profile.configDir}\`.`,
72
+ );
73
+ }
74
+ return client;
75
+ }
@@ -0,0 +1,202 @@
1
+ import type { CopilotFailureInfo, CopilotFailureCategory } from './types.js';
2
+
3
+ export interface SessionErrorPayload {
4
+ errorType?: string | undefined;
5
+ message: string;
6
+ statusCode?: number | undefined;
7
+ }
8
+
9
+ const RATE_LIMIT_ERROR_TYPES = new Set(['quota', 'rate_limit', 'ratelimit']);
10
+ const AUTH_ERROR_TYPES = new Set(['auth', 'authentication', 'authorization']);
11
+ const CONNECTION_ERROR_TYPES = new Set(['connection', 'connect', 'network']);
12
+ const TRANSIENT_ERROR_TYPES = new Set(['timeout', 'transient', 'temporary']);
13
+
14
+ function normalizeMessage(input: unknown): string {
15
+ if (input instanceof Error) {
16
+ return input.message || input.name;
17
+ }
18
+
19
+ if (typeof input === 'string') {
20
+ return input;
21
+ }
22
+
23
+ if (input && typeof input === 'object' && 'message' in input) {
24
+ const value = (input as { message?: unknown }).message;
25
+ if (typeof value === 'string' && value.length > 0) {
26
+ return value;
27
+ }
28
+ }
29
+
30
+ return 'Unknown Copilot failure';
31
+ }
32
+
33
+ function buildFailureInfo(
34
+ category: CopilotFailureCategory,
35
+ message: string,
36
+ details?: Pick<CopilotFailureInfo, 'sessionErrorType' | 'sessionStatusCode'>,
37
+ ): CopilotFailureInfo {
38
+ return {
39
+ category,
40
+ retryable: category !== 'fatal',
41
+ message,
42
+ ...details,
43
+ };
44
+ }
45
+
46
+ function classifyStatusCode(statusCode: number | undefined): CopilotFailureCategory | undefined {
47
+ if (statusCode === undefined) {
48
+ return undefined;
49
+ }
50
+
51
+ if (statusCode === 429) {
52
+ return 'rate_limit';
53
+ }
54
+
55
+ if (statusCode === 401 || statusCode === 403) {
56
+ return 'auth';
57
+ }
58
+
59
+ if (statusCode >= 500 && statusCode <= 599) {
60
+ return 'transient';
61
+ }
62
+
63
+ return undefined;
64
+ }
65
+
66
+ function normalizeErrorType(errorType: string): string {
67
+ return errorType.toLowerCase().trim();
68
+ }
69
+
70
+ function compactErrorType(errorType: string): string {
71
+ return normalizeErrorType(errorType).replace(/[^a-z0-9]+/g, '');
72
+ }
73
+
74
+ function matchesErrorType(errorType: string, prefixes: string[], includes: string[] = []): boolean {
75
+ const normalized = normalizeErrorType(errorType);
76
+ const compact = compactErrorType(errorType);
77
+
78
+ return prefixes.some((prefix) => {
79
+ const normalizedPrefix = normalizeErrorType(prefix);
80
+ const compactPrefix = normalizedPrefix.replace(/[^a-z0-9]+/g, '');
81
+ return (
82
+ normalized === normalizedPrefix ||
83
+ normalized.startsWith(`${normalizedPrefix}_`) ||
84
+ normalized.startsWith(`${normalizedPrefix}-`) ||
85
+ compact === compactPrefix ||
86
+ compact.startsWith(compactPrefix)
87
+ );
88
+ }) || includes.some((fragment) => normalized.includes(fragment) || compact.includes(fragment.replace(/[^a-z0-9]+/g, '')));
89
+ }
90
+
91
+ function classifyTextualFailure(message: string): CopilotFailureCategory {
92
+ const text = message.toLowerCase();
93
+
94
+ if (
95
+ (text.includes('config dir') || text.includes('config directory') || text.includes('profile dir') || text.includes('profile directory')) &&
96
+ (
97
+ text.includes('invalid') ||
98
+ text.includes('missing') ||
99
+ text.includes('bad') ||
100
+ text.includes('not found') ||
101
+ text.includes('unreadable') ||
102
+ text.includes('permission denied')
103
+ )
104
+ ) {
105
+ return 'auth';
106
+ }
107
+
108
+ if (
109
+ text.includes('unauth') ||
110
+ text.includes('not authenticated') ||
111
+ text.includes('authentication') ||
112
+ text.includes('authorization') ||
113
+ text.includes('invalid token') ||
114
+ text.includes('token expired')
115
+ ) {
116
+ return 'auth';
117
+ }
118
+
119
+ if (
120
+ text.includes('429') ||
121
+ text.includes('rate limit') ||
122
+ text.includes('rate_limit') ||
123
+ text.includes('quota')
124
+ ) {
125
+ return 'rate_limit';
126
+ }
127
+
128
+ if (
129
+ text.includes('connect') ||
130
+ text.includes('connection') ||
131
+ text.includes('socket') ||
132
+ text.includes('network') ||
133
+ text.includes('cli server exited') ||
134
+ text.includes('exited with code') ||
135
+ text.includes('spawn ') ||
136
+ text.includes('econnrefused') ||
137
+ text.includes('econnreset')
138
+ ) {
139
+ return 'connection';
140
+ }
141
+
142
+ if (
143
+ text.includes('timeout') ||
144
+ text.includes('temporarily') ||
145
+ text.includes('temporary') ||
146
+ text.includes('try again')
147
+ ) {
148
+ return 'transient';
149
+ }
150
+
151
+ return 'fatal';
152
+ }
153
+
154
+ export function classifySessionErrorEvent(input: SessionErrorPayload): CopilotFailureInfo {
155
+ const errorType = input.errorType?.trim() ?? '';
156
+ const statusCodeCategory = classifyStatusCode(input.statusCode);
157
+
158
+ if (statusCodeCategory === 'rate_limit' || matchesErrorType(errorType, ['rate_limit', 'quota'], ['ratelimit', 'rate limit'])) {
159
+ return buildFailureInfo('rate_limit', input.message, {
160
+ sessionErrorType: input.errorType,
161
+ sessionStatusCode: input.statusCode,
162
+ });
163
+ }
164
+
165
+ if (statusCodeCategory === 'auth' || matchesErrorType(errorType, ['auth', 'authentication', 'authorization'], ['auth', 'login'])) {
166
+ return buildFailureInfo('auth', input.message, {
167
+ sessionErrorType: input.errorType,
168
+ sessionStatusCode: input.statusCode,
169
+ });
170
+ }
171
+
172
+ if (statusCodeCategory === 'transient') {
173
+ return buildFailureInfo('transient', input.message, {
174
+ sessionErrorType: input.errorType,
175
+ sessionStatusCode: input.statusCode,
176
+ });
177
+ }
178
+
179
+ if (matchesErrorType(errorType, ['connection', 'connect', 'network'], ['connection', 'socket'])) {
180
+ return buildFailureInfo('connection', input.message, {
181
+ sessionErrorType: input.errorType,
182
+ sessionStatusCode: input.statusCode,
183
+ });
184
+ }
185
+
186
+ if (matchesErrorType(errorType, ['timeout', 'transient', 'temporary'], ['retry', 'temporary'])) {
187
+ return buildFailureInfo('transient', input.message, {
188
+ sessionErrorType: input.errorType,
189
+ sessionStatusCode: input.statusCode,
190
+ });
191
+ }
192
+
193
+ return buildFailureInfo(classifyTextualFailure(input.message), input.message, {
194
+ sessionErrorType: input.errorType,
195
+ sessionStatusCode: input.statusCode,
196
+ });
197
+ }
198
+
199
+ export function classifyCopilotStartupFailure(error: unknown): CopilotFailureInfo {
200
+ const message = normalizeMessage(error);
201
+ return buildFailureInfo(classifyTextualFailure(message), message);
202
+ }
@@ -0,0 +1,11 @@
1
+ import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
2
+
3
+ export function generateId(): string {
4
+ const suffix = Date.now().toString().slice(-4);
5
+ return uniqueNamesGenerator({
6
+ dictionaries: [adjectives, animals],
7
+ separator: '-',
8
+ length: 2,
9
+ style: 'lowerCase',
10
+ }) + `-${suffix}`;
11
+ }
@@ -0,0 +1,19 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
4
+ export interface MarkdownInput {
5
+ path: string;
6
+ content: string;
7
+ }
8
+
9
+ export async function readMarkdownFile(filePath: string, cwd = process.cwd()): Promise<MarkdownInput> {
10
+ const absolutePath = resolve(cwd, filePath);
11
+ const content = (await readFile(absolutePath, 'utf8')).trim();
12
+ if (!content) {
13
+ throw new Error(`Markdown file is empty: ${filePath}`);
14
+ }
15
+ return {
16
+ path: absolutePath,
17
+ content,
18
+ };
19
+ }
@@ -0,0 +1,56 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ export interface StatePaths {
7
+ rootDir: string;
8
+ registryPath: string;
9
+ daemonMetaPath: string;
10
+ socketPath: string;
11
+ }
12
+
13
+ export function stateRootDir(): string {
14
+ const explicitRoot = process.env.CLI_COPILOT_WORKER_STATE_DIR?.trim();
15
+ return explicitRoot || join(homedir(), '.cli-copilot-worker');
16
+ }
17
+
18
+ export function ensureStateRoot(): StatePaths {
19
+ const rootDir = stateRootDir();
20
+ mkdirSync(rootDir, { recursive: true });
21
+ return {
22
+ rootDir,
23
+ registryPath: join(rootDir, 'registry.json'),
24
+ daemonMetaPath: join(rootDir, 'daemon.json'),
25
+ socketPath: join(rootDir, 'daemon.sock'),
26
+ };
27
+ }
28
+
29
+ export function workspaceIdForCwd(cwd: string): string {
30
+ return createHash('sha256').update(cwd).digest('hex').slice(0, 12);
31
+ }
32
+
33
+ export function workspaceDir(cwd: string): string {
34
+ return join(ensureStateRoot().rootDir, 'workspaces', workspaceIdForCwd(cwd));
35
+ }
36
+
37
+ export function ensureWorkspaceDirs(cwd: string): { workspaceDir: string; transcriptDir: string; logDir: string } {
38
+ const base = workspaceDir(cwd);
39
+ const transcriptDir = join(base, 'transcripts');
40
+ const logDir = join(base, 'logs');
41
+ mkdirSync(transcriptDir, { recursive: true });
42
+ mkdirSync(logDir, { recursive: true });
43
+ return { workspaceDir: base, transcriptDir, logDir };
44
+ }
45
+
46
+ export function transcriptPath(cwd: string, conversationId: string): string {
47
+ return join(ensureWorkspaceDirs(cwd).transcriptDir, `${conversationId}.jsonl`);
48
+ }
49
+
50
+ export function logPath(cwd: string, conversationId: string): string {
51
+ return join(ensureWorkspaceDirs(cwd).logDir, `${conversationId}.log`);
52
+ }
53
+
54
+ export function buildDaemonToken(): string {
55
+ return randomUUID();
56
+ }
@@ -0,0 +1,140 @@
1
+ import type { CopilotProfile, CopilotFailureCategory } from './types.js';
2
+
3
+ export interface InjectedProfileFault {
4
+ category: CopilotFailureCategory;
5
+ remaining?: number | undefined;
6
+ message?: string | undefined;
7
+ errorType?: string | undefined;
8
+ statusCode?: number | undefined;
9
+ }
10
+
11
+ type StoredFault = InjectedProfileFault & {
12
+ remaining?: number | undefined;
13
+ };
14
+
15
+ function defaultErrorType(category: CopilotFailureCategory): string {
16
+ switch (category) {
17
+ case 'auth':
18
+ return 'authentication';
19
+ case 'rate_limit':
20
+ return 'rate_limit';
21
+ case 'connection':
22
+ return 'connection';
23
+ case 'transient':
24
+ return 'transient';
25
+ case 'fatal':
26
+ return 'fatal';
27
+ }
28
+ }
29
+
30
+ function defaultStatusCode(category: CopilotFailureCategory): number | undefined {
31
+ switch (category) {
32
+ case 'auth':
33
+ return 401;
34
+ case 'rate_limit':
35
+ return 429;
36
+ case 'connection':
37
+ return undefined;
38
+ case 'transient':
39
+ return 504;
40
+ case 'fatal':
41
+ return undefined;
42
+ }
43
+ }
44
+
45
+ function defaultMessage(profile: Pick<CopilotProfile, 'id' | 'configDir'>, category: CopilotFailureCategory): string {
46
+ return `Injected ${category} failure for profile ${profile.id} (${profile.configDir})`;
47
+ }
48
+
49
+ function normalizeFault(value: unknown): StoredFault | undefined {
50
+ if (typeof value === 'string') {
51
+ return { category: value as CopilotFailureCategory };
52
+ }
53
+
54
+ if (!value || typeof value !== 'object') {
55
+ return undefined;
56
+ }
57
+
58
+ const raw = value as Partial<InjectedProfileFault> & { category?: unknown };
59
+ if (typeof raw.category !== 'string') {
60
+ return undefined;
61
+ }
62
+
63
+ const remaining = typeof raw.remaining === 'number' && Number.isFinite(raw.remaining)
64
+ ? Math.max(0, Math.trunc(raw.remaining))
65
+ : undefined;
66
+
67
+ return {
68
+ category: raw.category as CopilotFailureCategory,
69
+ remaining,
70
+ message: typeof raw.message === 'string' ? raw.message : undefined,
71
+ errorType: typeof raw.errorType === 'string' ? raw.errorType : undefined,
72
+ statusCode: typeof raw.statusCode === 'number' && Number.isFinite(raw.statusCode)
73
+ ? Math.trunc(raw.statusCode)
74
+ : undefined,
75
+ };
76
+ }
77
+
78
+ export class ProfileFaultPlanner {
79
+ private readonly faults = new Map<string, StoredFault>();
80
+
81
+ constructor(raw = process.env.CLI_COPILOT_WORKER_PROFILE_FAULTS) {
82
+ if (!raw) {
83
+ return;
84
+ }
85
+
86
+ let parsed: unknown;
87
+ try {
88
+ parsed = JSON.parse(raw) as unknown;
89
+ } catch (error) {
90
+ throw new Error(`Failed to parse CLI_COPILOT_WORKER_PROFILE_FAULTS: ${error instanceof Error ? error.message : String(error)}`);
91
+ }
92
+
93
+ if (!parsed || typeof parsed !== 'object') {
94
+ throw new Error('CLI_COPILOT_WORKER_PROFILE_FAULTS must be a JSON object');
95
+ }
96
+
97
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
98
+ const fault = normalizeFault(value);
99
+ if (fault) {
100
+ this.faults.set(key, fault);
101
+ }
102
+ }
103
+ }
104
+
105
+ static fromEnvironment(): ProfileFaultPlanner {
106
+ return new ProfileFaultPlanner();
107
+ }
108
+
109
+ takeFault(profile: Pick<CopilotProfile, 'id' | 'configDir'>): InjectedProfileFault | undefined {
110
+ const fault = this.findFault(profile);
111
+ if (!fault) {
112
+ return undefined;
113
+ }
114
+
115
+ if (fault.remaining !== undefined) {
116
+ fault.remaining -= 1;
117
+ if (fault.remaining <= 0) {
118
+ this.faults.delete(profile.configDir);
119
+ this.faults.delete(profile.id);
120
+ }
121
+ }
122
+
123
+ return {
124
+ category: fault.category,
125
+ remaining: fault.remaining,
126
+ message: fault.message ?? defaultMessage(profile, fault.category),
127
+ errorType: fault.errorType ?? defaultErrorType(fault.category),
128
+ statusCode: fault.statusCode ?? defaultStatusCode(fault.category),
129
+ };
130
+ }
131
+
132
+ private findFault(profile: Pick<CopilotProfile, 'id' | 'configDir'>): StoredFault | undefined {
133
+ const byConfigDir = this.faults.get(profile.configDir);
134
+ if (byConfigDir) {
135
+ return byConfigDir;
136
+ }
137
+
138
+ return this.faults.get(profile.id);
139
+ }
140
+ }