@chrisromp/copilot-bridge 0.6.0-dev.2

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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/bin/copilot-bridge.js +61 -0
  4. package/config.sample.json +100 -0
  5. package/dist/channels/mattermost/adapter.d.ts +55 -0
  6. package/dist/channels/mattermost/adapter.d.ts.map +1 -0
  7. package/dist/channels/mattermost/adapter.js +524 -0
  8. package/dist/channels/mattermost/adapter.js.map +1 -0
  9. package/dist/channels/mattermost/streaming.d.ts +29 -0
  10. package/dist/channels/mattermost/streaming.d.ts.map +1 -0
  11. package/dist/channels/mattermost/streaming.js +151 -0
  12. package/dist/channels/mattermost/streaming.js.map +1 -0
  13. package/dist/config.d.ts +107 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +817 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/core/bridge.d.ts +73 -0
  18. package/dist/core/bridge.d.ts.map +1 -0
  19. package/dist/core/bridge.js +166 -0
  20. package/dist/core/bridge.js.map +1 -0
  21. package/dist/core/channel-idle.d.ts +40 -0
  22. package/dist/core/channel-idle.d.ts.map +1 -0
  23. package/dist/core/channel-idle.js +120 -0
  24. package/dist/core/channel-idle.js.map +1 -0
  25. package/dist/core/command-handler.d.ts +51 -0
  26. package/dist/core/command-handler.d.ts.map +1 -0
  27. package/dist/core/command-handler.js +393 -0
  28. package/dist/core/command-handler.js.map +1 -0
  29. package/dist/core/inter-agent.d.ts +52 -0
  30. package/dist/core/inter-agent.d.ts.map +1 -0
  31. package/dist/core/inter-agent.js +179 -0
  32. package/dist/core/inter-agent.js.map +1 -0
  33. package/dist/core/onboarding.d.ts +44 -0
  34. package/dist/core/onboarding.d.ts.map +1 -0
  35. package/dist/core/onboarding.js +205 -0
  36. package/dist/core/onboarding.js.map +1 -0
  37. package/dist/core/scheduler.d.ts +38 -0
  38. package/dist/core/scheduler.d.ts.map +1 -0
  39. package/dist/core/scheduler.js +253 -0
  40. package/dist/core/scheduler.js.map +1 -0
  41. package/dist/core/session-manager.d.ts +166 -0
  42. package/dist/core/session-manager.d.ts.map +1 -0
  43. package/dist/core/session-manager.js +1732 -0
  44. package/dist/core/session-manager.js.map +1 -0
  45. package/dist/core/stream-formatter.d.ts +14 -0
  46. package/dist/core/stream-formatter.d.ts.map +1 -0
  47. package/dist/core/stream-formatter.js +198 -0
  48. package/dist/core/stream-formatter.js.map +1 -0
  49. package/dist/core/thread-utils.d.ts +22 -0
  50. package/dist/core/thread-utils.d.ts.map +1 -0
  51. package/dist/core/thread-utils.js +44 -0
  52. package/dist/core/thread-utils.js.map +1 -0
  53. package/dist/core/workspace-manager.d.ts +38 -0
  54. package/dist/core/workspace-manager.d.ts.map +1 -0
  55. package/dist/core/workspace-manager.js +230 -0
  56. package/dist/core/workspace-manager.js.map +1 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +1286 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/logger.d.ts +9 -0
  62. package/dist/logger.d.ts.map +1 -0
  63. package/dist/logger.js +34 -0
  64. package/dist/logger.js.map +1 -0
  65. package/dist/state/store.d.ts +124 -0
  66. package/dist/state/store.d.ts.map +1 -0
  67. package/dist/state/store.js +523 -0
  68. package/dist/state/store.js.map +1 -0
  69. package/dist/types.d.ts +185 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +2 -0
  72. package/dist/types.js.map +1 -0
  73. package/package.json +61 -0
  74. package/scripts/check.ts +267 -0
  75. package/scripts/com.copilot-bridge.plist +41 -0
  76. package/scripts/copilot-bridge.service +30 -0
  77. package/scripts/init.ts +250 -0
  78. package/scripts/install-service.ts +123 -0
  79. package/scripts/lib/config-gen.ts +129 -0
  80. package/scripts/lib/mattermost.ts +109 -0
  81. package/scripts/lib/output.ts +69 -0
  82. package/scripts/lib/prerequisites.ts +86 -0
  83. package/scripts/lib/prompts.ts +65 -0
  84. package/scripts/lib/service.ts +191 -0
  85. package/scripts/uninstall-service.ts +90 -0
  86. package/templates/admin/AGENTS.md +325 -0
  87. package/templates/admin/MEMORY.md +4 -0
  88. package/templates/agents/AGENTS.md +97 -0
  89. package/templates/agents/MEMORY.md +4 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Prerequisite checks shared between init and check commands.
3
+ * Validates Node.js version, GitHub Copilot CLI, and auth status.
4
+ */
5
+
6
+ import { execSync } from 'node:child_process';
7
+ import type { CheckResult } from './output.js';
8
+
9
+ export function checkNodeVersion(): CheckResult {
10
+ const version = process.version; // e.g., "v22.0.0"
11
+ const major = parseInt(version.slice(1).split('.')[0], 10);
12
+ if (major >= 20) {
13
+ return { status: 'pass', label: `Node.js ${version}` };
14
+ }
15
+ return { status: 'fail', label: `Node.js ${version}`, detail: 'requires v20 or higher' };
16
+ }
17
+
18
+ function tryCommand(cmd: string): string | null {
19
+ try {
20
+ return execSync(cmd, { encoding: 'utf-8', timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ export function checkCopilotCLI(): CheckResult {
27
+ // Try gh copilot first (GitHub CLI extension), then standalone copilot
28
+ const ghVersion = tryCommand('gh copilot --version 2>&1');
29
+ if (ghVersion) {
30
+ const ver = ghVersion.split('\n')[0];
31
+ return { status: 'pass', label: 'GitHub Copilot CLI', detail: ver };
32
+ }
33
+
34
+ const standaloneVersion = tryCommand('copilot --version 2>&1');
35
+ if (standaloneVersion) {
36
+ const ver = standaloneVersion.split('\n')[0];
37
+ return { status: 'pass', label: 'Copilot CLI (standalone)', detail: ver };
38
+ }
39
+
40
+ return {
41
+ status: 'fail',
42
+ label: 'GitHub Copilot CLI',
43
+ detail: 'not found — install via: gh extension install github/gh-copilot',
44
+ };
45
+ }
46
+
47
+ export function checkGitHubAuth(): CheckResult {
48
+ // Check for Copilot-specific env token first (highest priority)
49
+ if (process.env.COPILOT_GITHUB_TOKEN) {
50
+ return { status: 'pass', label: 'GitHub authenticated', detail: 'via COPILOT_GITHUB_TOKEN' };
51
+ }
52
+
53
+ // Check for general GitHub tokens
54
+ if (process.env.GH_TOKEN) {
55
+ return { status: 'pass', label: 'GitHub authenticated', detail: 'via GH_TOKEN' };
56
+ }
57
+ if (process.env.GITHUB_TOKEN) {
58
+ return { status: 'pass', label: 'GitHub authenticated', detail: 'via GITHUB_TOKEN' };
59
+ }
60
+
61
+ // Check if gh CLI is authenticated
62
+ const authStatus = tryCommand('gh auth status 2>&1');
63
+ if (authStatus && authStatus.includes('Logged in')) {
64
+ return { status: 'pass', label: 'GitHub authenticated', detail: 'via gh CLI' };
65
+ }
66
+
67
+ // Check if Copilot CLI has stored credentials
68
+ const copilotAuth = tryCommand('copilot auth status 2>&1');
69
+ if (copilotAuth && !copilotAuth.includes('not logged in')) {
70
+ return { status: 'pass', label: 'GitHub authenticated', detail: 'via Copilot CLI' };
71
+ }
72
+
73
+ return {
74
+ status: 'warn',
75
+ label: 'GitHub authentication',
76
+ detail: 'no token found — set COPILOT_GITHUB_TOKEN, run gh auth login, or run copilot auth login',
77
+ };
78
+ }
79
+
80
+ export function runAllPrereqs(): CheckResult[] {
81
+ return [
82
+ checkNodeVersion(),
83
+ checkCopilotCLI(),
84
+ checkGitHubAuth(),
85
+ ];
86
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Readline-based interactive prompts. No external dependencies.
3
+ */
4
+
5
+ import * as readline from 'node:readline';
6
+
7
+ let rl: readline.Interface | null = null;
8
+
9
+ function getRL(): readline.Interface {
10
+ if (!rl) {
11
+ rl = readline.createInterface({
12
+ input: process.stdin,
13
+ output: process.stdout,
14
+ });
15
+ }
16
+ return rl;
17
+ }
18
+
19
+ export function closePrompts(): void {
20
+ rl?.close();
21
+ rl = null;
22
+ }
23
+
24
+ export async function ask(question: string, defaultValue?: string): Promise<string> {
25
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
26
+ return new Promise((resolve) => {
27
+ getRL().question(`${question}${suffix}: `, (answer) => {
28
+ resolve(answer.trim() || defaultValue || '');
29
+ });
30
+ });
31
+ }
32
+
33
+ export async function askRequired(question: string): Promise<string> {
34
+ let answer = '';
35
+ while (!answer) {
36
+ answer = await ask(question);
37
+ if (!answer) console.log(' This field is required.');
38
+ }
39
+ return answer;
40
+ }
41
+
42
+ export async function confirm(question: string, defaultYes = true): Promise<boolean> {
43
+ const hint = defaultYes ? 'Y/n' : 'y/N';
44
+ const answer = await ask(`${question} (${hint})`);
45
+ if (!answer) return defaultYes;
46
+ return answer.toLowerCase().startsWith('y');
47
+ }
48
+
49
+ export async function choose(question: string, options: string[], defaultIndex = 0): Promise<number> {
50
+ console.log(`\n${question}`);
51
+ for (let i = 0; i < options.length; i++) {
52
+ const marker = i === defaultIndex ? '>' : ' ';
53
+ console.log(` ${marker} ${i + 1}. ${options[i]}`);
54
+ }
55
+ const answer = await ask('Choose', String(defaultIndex + 1));
56
+ const idx = parseInt(answer, 10) - 1;
57
+ if (idx >= 0 && idx < options.length) return idx;
58
+ return defaultIndex;
59
+ }
60
+
61
+ export async function askSecret(question: string): Promise<string> {
62
+ // For tokens — we don't mask input because readline doesn't support it
63
+ // without raw mode, but we label it clearly
64
+ return askRequired(question);
65
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * OS-specific service file generation and installation.
3
+ * Supports macOS (launchd) and Linux (systemd system-level units).
4
+ */
5
+
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import { execSync } from 'node:child_process';
10
+
11
+ export type Platform = 'macos' | 'linux' | 'unsupported';
12
+
13
+ export function detectPlatform(): Platform {
14
+ switch (process.platform) {
15
+ case 'darwin': return 'macos';
16
+ case 'linux': return 'linux';
17
+ default: return 'unsupported';
18
+ }
19
+ }
20
+
21
+ export function getNodePath(): string {
22
+ try {
23
+ return execSync('which node', { encoding: 'utf-8' }).trim();
24
+ } catch {
25
+ return '/usr/local/bin/node';
26
+ }
27
+ }
28
+
29
+ export function getSystemPath(): string {
30
+ // Include the directory containing the current node binary (e.g., nvm paths)
31
+ const nodeBinDir = path.dirname(getNodePath());
32
+ const platform = detectPlatform();
33
+ const basePath = platform === 'macos'
34
+ ? '/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
35
+ : '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';
36
+ // Prepend node's bin dir if not already in the base path
37
+ if (basePath.split(':').includes(nodeBinDir)) return basePath;
38
+ return `${nodeBinDir}:${basePath}`;
39
+ }
40
+
41
+ // --- launchd (macOS) ---
42
+
43
+ export interface LaunchdConfig {
44
+ label: string;
45
+ bridgePath: string;
46
+ homePath: string;
47
+ }
48
+
49
+ export function generateLaunchdPlist(config: LaunchdConfig): string {
50
+ const nodePath = getNodePath();
51
+ const tsxPath = path.join(config.bridgePath, 'node_modules', '.bin', 'tsx');
52
+ return `<?xml version="1.0" encoding="UTF-8"?>
53
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
54
+ <plist version="1.0">
55
+ <dict>
56
+ <key>Label</key>
57
+ <string>${config.label}</string>
58
+
59
+ <key>ProgramArguments</key>
60
+ <array>
61
+ <string>${nodePath}</string>
62
+ <string>${tsxPath}</string>
63
+ <string>dist/index.js</string>
64
+ </array>
65
+
66
+ <key>WorkingDirectory</key>
67
+ <string>${config.bridgePath}</string>
68
+
69
+ <key>EnvironmentVariables</key>
70
+ <dict>
71
+ <key>PATH</key>
72
+ <string>${getSystemPath()}</string>
73
+ <key>HOME</key>
74
+ <string>${config.homePath}</string>
75
+ </dict>
76
+
77
+ <key>RunAtLoad</key>
78
+ <true/>
79
+
80
+ <key>KeepAlive</key>
81
+ <true/>
82
+
83
+ <key>ThrottleInterval</key>
84
+ <integer>10</integer>
85
+
86
+ <key>StandardOutPath</key>
87
+ <string>/tmp/copilot-bridge.log</string>
88
+
89
+ <key>StandardErrorPath</key>
90
+ <string>/tmp/copilot-bridge.log</string>
91
+ </dict>
92
+ </plist>`;
93
+ }
94
+
95
+ export function getLaunchdInstallPath(): string {
96
+ return path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.copilot-bridge.plist');
97
+ }
98
+
99
+ export function installLaunchd(plistContent: string): { installed: boolean; path: string; error?: string } {
100
+ const installPath = getLaunchdInstallPath();
101
+ try {
102
+ const dir = path.dirname(installPath);
103
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
104
+ // Unload existing service before overwriting (ignore errors if not loaded)
105
+ try { execSync(`launchctl bootout gui/$(id -u) "${installPath}" 2>/dev/null`, { encoding: 'utf-8' }); } catch { /* not loaded */ }
106
+ fs.writeFileSync(installPath, plistContent, 'utf-8');
107
+ execSync(`launchctl load "${installPath}"`, { encoding: 'utf-8' });
108
+ return { installed: true, path: installPath };
109
+ } catch (err) {
110
+ return { installed: false, path: installPath, error: String(err) };
111
+ }
112
+ }
113
+
114
+ // --- systemd (Linux) ---
115
+
116
+ export interface SystemdConfig {
117
+ bridgePath: string;
118
+ homePath: string;
119
+ user: string;
120
+ }
121
+
122
+ export function generateSystemdUnit(config: SystemdConfig): string {
123
+ const nodePath = getNodePath();
124
+ const tsxPath = path.join(config.bridgePath, 'node_modules', '.bin', 'tsx');
125
+ return `[Unit]
126
+ Description=Copilot Bridge
127
+ After=network.target
128
+
129
+ [Service]
130
+ Type=simple
131
+ User=${config.user}
132
+ ExecStart=${nodePath} ${tsxPath} ${config.bridgePath}/dist/index.js
133
+ WorkingDirectory=${config.bridgePath}
134
+ Environment=HOME=${config.homePath}
135
+ Environment=PATH=${getSystemPath()}
136
+ Restart=always
137
+ RestartSec=10
138
+
139
+ [Install]
140
+ WantedBy=multi-user.target`;
141
+ }
142
+
143
+ export function getSystemdInstallPath(): string {
144
+ return '/etc/systemd/system/copilot-bridge.service';
145
+ }
146
+
147
+ // --- Service status ---
148
+
149
+ export function getServiceStatus(): { running: boolean; pid?: number; detail: string } {
150
+ const platform = detectPlatform();
151
+
152
+ if (platform === 'macos') {
153
+ try {
154
+ const output = execSync('launchctl list com.copilot-bridge 2>/dev/null', { encoding: 'utf-8' });
155
+ const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
156
+ if (pidMatch) {
157
+ return { running: true, pid: parseInt(pidMatch[1], 10), detail: `launchd, PID ${pidMatch[1]}` };
158
+ }
159
+ // launchctl list succeeded but no PID — service loaded but not running
160
+ return { running: false, detail: 'launchd: loaded but not running' };
161
+ } catch {
162
+ return { running: false, detail: 'launchd: not loaded' };
163
+ }
164
+ }
165
+
166
+ if (platform === 'linux') {
167
+ try {
168
+ // systemctl is-active exits non-zero for inactive/unknown services
169
+ const output = execSync('systemctl is-active copilot-bridge 2>/dev/null', { encoding: 'utf-8' }).trim();
170
+ if (output === 'active') {
171
+ try {
172
+ const pid = execSync('systemctl show copilot-bridge --property=MainPID --value 2>/dev/null', { encoding: 'utf-8' }).trim();
173
+ return { running: true, pid: parseInt(pid, 10), detail: `systemd, PID ${pid}` };
174
+ } catch {
175
+ return { running: true, detail: 'systemd: active' };
176
+ }
177
+ }
178
+ return { running: false, detail: `systemd: ${output}` };
179
+ } catch (err) {
180
+ // Exit code 3 = inactive/dead (unit exists but not running)
181
+ // Exit code 4 = no such unit file
182
+ const stdout = err instanceof Error && 'stdout' in err ? String((err as { stdout: unknown }).stdout).trim() : '';
183
+ if (stdout === 'inactive' || stdout === 'failed' || stdout === 'activating' || stdout === 'deactivating') {
184
+ return { running: false, detail: `systemd: ${stdout}` };
185
+ }
186
+ return { running: false, detail: 'systemd: not installed' };
187
+ }
188
+ }
189
+
190
+ return { running: false, detail: 'unsupported platform' };
191
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * copilot-bridge uninstall-service — Remove the bridge system service.
4
+ *
5
+ * macOS: unloads and removes the launchd plist
6
+ * Linux: stops, disables, and removes the systemd unit (requires sudo)
7
+ *
8
+ * Usage: npm run uninstall-service
9
+ * npx tsx scripts/uninstall-service.ts
10
+ */
11
+
12
+ import * as fs from 'node:fs';
13
+ import { heading, success, fail, info, dim, blank } from './lib/output.js';
14
+ import { detectPlatform, getLaunchdInstallPath, getSystemdInstallPath } from './lib/service.js';
15
+ import { execSync } from 'node:child_process';
16
+
17
+ function main() {
18
+ const osPlatform = detectPlatform();
19
+
20
+ heading('🗑️ copilot-bridge service uninstaller');
21
+ blank();
22
+
23
+ if (osPlatform === 'macos') {
24
+ const plistPath = getLaunchdInstallPath();
25
+
26
+ if (!fs.existsSync(plistPath)) {
27
+ info('No launchd service found — nothing to uninstall.');
28
+ return;
29
+ }
30
+
31
+ info(`Unloading and removing ${plistPath}`);
32
+
33
+ try {
34
+ // bootout is the modern replacement for unload
35
+ execSync(`launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null || launchctl unload "${plistPath}" 2>/dev/null`, {
36
+ stdio: 'inherit',
37
+ });
38
+ } catch {
39
+ // Service may not be loaded — that's fine
40
+ }
41
+
42
+ try {
43
+ fs.unlinkSync(plistPath);
44
+ blank();
45
+ success('Service uninstalled.');
46
+ } catch (err) {
47
+ fail(`Failed to remove plist: ${err}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ } else if (osPlatform === 'linux') {
52
+ const unitPath = getSystemdInstallPath();
53
+
54
+ if (!fs.existsSync(unitPath)) {
55
+ info('No systemd service found — nothing to uninstall.');
56
+ return;
57
+ }
58
+
59
+ const isRoot = process.getuid?.() === 0;
60
+ if (!isRoot) {
61
+ info('This requires sudo to remove from /etc/systemd/system/.');
62
+ blank();
63
+ }
64
+
65
+ try {
66
+ execSync('sudo systemctl stop copilot-bridge 2>/dev/null || true', { stdio: 'inherit' });
67
+ execSync('sudo systemctl disable copilot-bridge 2>/dev/null || true', { stdio: 'inherit' });
68
+ execSync(`sudo rm "${unitPath}"`, { stdio: 'inherit' });
69
+ execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
70
+ blank();
71
+ success('Service uninstalled.');
72
+ } catch {
73
+ blank();
74
+ fail('Automatic uninstall failed (sudo may have been denied).');
75
+ blank();
76
+ info('To uninstall manually:');
77
+ dim(' sudo systemctl stop copilot-bridge');
78
+ dim(' sudo systemctl disable copilot-bridge');
79
+ dim(` sudo rm ${unitPath}`);
80
+ dim(' sudo systemctl daemon-reload');
81
+ process.exit(1);
82
+ }
83
+
84
+ } else {
85
+ fail('Unsupported platform — no service to uninstall.');
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ main();