@continuedev/continuous-ai 1.0.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.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @continuedev/continuous-ai
2
+
3
+ CLI tool for interacting with Continue agent checks on pull requests.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @continuedev/continuous-ai
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Setup
14
+
15
+ Get your API key from [hub.continue.dev/settings/api-keys](https://hub.continue.dev/settings/api-keys) and set it:
16
+
17
+ ```bash
18
+ export CONTINUE_API_KEY="con-xxx"
19
+ ```
20
+
21
+ Or pass it with the `--token` flag to each command.
22
+
23
+ ### Commands
24
+
25
+ #### `cai status <pr-url>`
26
+
27
+ List all agent checks for a pull request with their current status.
28
+
29
+ ```bash
30
+ cai status https://github.com/owner/repo/pull/123
31
+ ```
32
+
33
+ Options:
34
+ - `--org <slug>` - Organization context
35
+ - `--token <key>` - API key (or use CONTINUE_API_KEY env var)
36
+ - `--format json|table` - Output format (default: table)
37
+
38
+ #### `cai wait <pr-url>`
39
+
40
+ Block until all agent checks complete or fail.
41
+
42
+ ```bash
43
+ cai wait https://github.com/owner/repo/pull/123
44
+ ```
45
+
46
+ Options:
47
+ - `--org <slug>` - Organization context
48
+ - `--token <key>` - API key
49
+ - `--timeout <seconds>` - Max wait time (default: 600)
50
+ - `--fail-fast` - Exit immediately on first failure
51
+ - `--format json|table` - Output format
52
+
53
+ #### `cai logs <session-id>`
54
+
55
+ View logs/state for a specific agent session.
56
+
57
+ ```bash
58
+ cai logs A0042
59
+ ```
60
+
61
+ Options:
62
+ - `--org <slug>` - Organization context
63
+ - `--token <key>` - API key
64
+ - `--tail <lines>` - Show last N lines (default: all)
65
+
66
+ #### `cai diff <session-id>`
67
+
68
+ View code diff for an agent session.
69
+
70
+ ```bash
71
+ cai diff A0042
72
+ ```
73
+
74
+ Options:
75
+ - `--org <slug>` - Organization context
76
+ - `--token <key>` - API key
77
+ - `--format unified|json` - Output format (default: unified)
78
+
79
+ #### `cai approve <pr-url>`
80
+
81
+ Approve changes and optionally merge the PR.
82
+
83
+ ```bash
84
+ cai approve https://github.com/owner/repo/pull/123 --merge
85
+ ```
86
+
87
+ Options:
88
+ - `--org <slug>` - Organization context
89
+ - `--token <key>` - API key
90
+ - `--merge` - Also merge the PR (default: just mark ready)
91
+ - `--merge-method squash|merge|rebase` - Merge method (default: squash)
92
+
93
+ #### `cai reject <pr-url>`
94
+
95
+ Reject changes and close the PR.
96
+
97
+ ```bash
98
+ cai reject https://github.com/owner/repo/pull/123 --reason "Does not meet requirements"
99
+ ```
100
+
101
+ Options:
102
+ - `--org <slug>` - Organization context
103
+ - `--token <key>` - API key
104
+ - `--reason <text>` - Optional rejection reason
105
+
106
+ ## Exit Codes
107
+
108
+ - `0` - Success
109
+ - `1` - General error (API error, not found, etc.)
110
+ - `2` - No sessions found for PR
111
+ - `124` - Timeout reached
112
+
113
+ ## Configuration
114
+
115
+ You can save your API key in `~/.continue/cli-config.json`:
116
+
117
+ ```json
118
+ {
119
+ "apiKey": "con-xxx",
120
+ "apiUrl": "https://api.continue.dev"
121
+ }
122
+ ```
123
+
124
+ ### Using Staging Environment
125
+
126
+ For internal testing with the staging environment, configure the staging API URL:
127
+
128
+ ```json
129
+ {
130
+ "apiKey": "con-xxx",
131
+ "apiUrl": "https://api.continue-stage.tools"
132
+ }
133
+ ```
134
+
135
+ Get your staging API key from [hub.continue-stage.tools/settings/api-keys](https://hub.continue-stage.tools/settings/api-keys).
136
+
137
+ You can also use environment variables:
138
+
139
+ ```bash
140
+ export CONTINUE_API_KEY="con-your-staging-key"
141
+ export CONTINUE_API_URL="https://api.continue-stage.tools"
142
+ ```
143
+
144
+ ## Example Workflow
145
+
146
+ ```bash
147
+ #!/bin/bash
148
+ # Wait for agents, then approve and merge if successful
149
+
150
+ export CONTINUE_API_KEY="con-xxx"
151
+ PR_URL="https://github.com/owner/repo/pull/123"
152
+
153
+ # Wait for completion with 5 minute timeout
154
+ cai wait "$PR_URL" --timeout 300 --fail-fast
155
+
156
+ # If successful, approve and merge
157
+ if [ $? -eq 0 ]; then
158
+ echo "All agents passed! Merging PR..."
159
+ cai approve "$PR_URL" --merge --merge-method squash
160
+ else
161
+ echo "Agents failed. Checking logs..."
162
+ # Get first failed session ID from status output
163
+ SESSION_ID=$(cai status "$PR_URL" --format json | jq -r '.sessions[] | select(.status == "failed") | .id' | head -1)
164
+ cai logs "$SESSION_ID"
165
+ fi
166
+ ```
167
+
168
+ ## License
169
+
170
+ MIT
package/bin/cai.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@continuedev/continuous-ai",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for interacting with Continue agent checks on pull requests",
5
+ "type": "module",
6
+ "bin": {
7
+ "cai": "./bin/cai.js",
8
+ "continuous-ai": "./bin/continuous-ai.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "echo 'No build step needed - continuous-ai uses plain JavaScript'",
18
+ "test": "echo \"Error: no test specified\" && exit 1"
19
+ },
20
+ "keywords": [
21
+ "continue",
22
+ "agent",
23
+ "cli",
24
+ "github",
25
+ "pull-request"
26
+ ],
27
+ "author": "Continue",
28
+ "license": "Apache-2.0",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/continuedev/remote-config-server",
32
+ "directory": "packages/continuous-ai"
33
+ },
34
+ "dependencies": {
35
+ "commander": "^12.0.0",
36
+ "chalk": "^5.3.0",
37
+ "ora": "^8.0.1",
38
+ "node-fetch": "^3.3.2"
39
+ },
40
+ "devDependencies": {
41
+ "@semantic-release/changelog": "^6.0.3",
42
+ "@semantic-release/commit-analyzer": "^13.0.1",
43
+ "@semantic-release/git": "^10.0.1",
44
+ "@semantic-release/github": "^11.0.3",
45
+ "@semantic-release/npm": "^12.0.2",
46
+ "@semantic-release/release-notes-generator": "^14.0.3",
47
+ "semantic-release": "^24.2.7",
48
+ "semantic-release-monorepo": "^8.0.2"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }
@@ -0,0 +1,52 @@
1
+ import chalk from 'chalk';
2
+ import { getApiKey, getApiUrl, requireApiKey } from '../lib/auth.js';
3
+ import { ApiClient } from '../lib/api-client.js';
4
+ import { parsePrUrl } from '../lib/pr-parser.js';
5
+ import { createSpinner } from '../lib/formatters.js';
6
+
7
+ export function approveCommand(program) {
8
+ program
9
+ .command('approve <pr-url>')
10
+ .description('Approve changes and optionally merge the PR')
11
+ .option('--org <slug>', 'Organization context')
12
+ .option('--token <key>', 'API key')
13
+ .option('--merge', 'Also merge the PR', false)
14
+ .option('--merge-method <method>', 'Merge method (squash|merge|rebase)', 'squash')
15
+ .action(async (prUrl, options) => {
16
+ const spinner = createSpinner('Approving pull request...');
17
+ try {
18
+ // Get API key
19
+ const apiKey = getApiKey(options.token);
20
+ requireApiKey(apiKey);
21
+
22
+ // Parse PR URL
23
+ const pr = parsePrUrl(prUrl);
24
+
25
+ // Create API client
26
+ const client = new ApiClient(getApiUrl(), apiKey);
27
+
28
+ spinner.start();
29
+
30
+ // Approve PR
31
+ const result = await client.approvePr(pr.url, {
32
+ organizationSlug: options.org,
33
+ autoMerge: options.merge,
34
+ mergeMethod: options.mergeMethod,
35
+ });
36
+
37
+ spinner.stop();
38
+
39
+ if (result.merged) {
40
+ console.log(chalk.green('✓') + ' Pull request approved and merged!');
41
+ console.log(` ${prUrl}`);
42
+ } else {
43
+ console.log(chalk.green('✓') + ' Pull request marked as ready for review!');
44
+ console.log(` ${prUrl}`);
45
+ }
46
+ } catch (error) {
47
+ spinner.stop();
48
+ console.error('ERROR:', error.message);
49
+ process.exit(1);
50
+ }
51
+ });
52
+ }
@@ -0,0 +1,40 @@
1
+ import { getApiKey, getApiUrl, requireApiKey } from '../lib/auth.js';
2
+ import { ApiClient } from '../lib/api-client.js';
3
+
4
+ export function diffCommand(program) {
5
+ program
6
+ .command('diff <session-id>')
7
+ .description('View code diff for an agent session')
8
+ .option('--token <key>', 'API key')
9
+ .option('--format <type>', 'Output format (unified|json)', 'unified')
10
+ .action(async (sessionId, options) => {
11
+ try {
12
+ // Get API key
13
+ const apiKey = getApiKey(options.token);
14
+ requireApiKey(apiKey);
15
+
16
+ // Create API client
17
+ const client = new ApiClient(getApiUrl(), apiKey);
18
+
19
+ // Get session diff
20
+ const diff = await client.getSessionDiff(sessionId);
21
+
22
+ // Format output
23
+ if (options.format === 'json') {
24
+ console.log(JSON.stringify(diff, null, 2));
25
+ } else if (typeof diff === 'string') {
26
+ // Already a unified diff string
27
+ console.log(diff);
28
+ } else if (diff.diff) {
29
+ // Diff is in a property
30
+ console.log(diff.diff);
31
+ } else {
32
+ // Pretty print whatever we got
33
+ console.log(JSON.stringify(diff, null, 2));
34
+ }
35
+ } catch (error) {
36
+ console.error('ERROR:', error.message);
37
+ process.exit(1);
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,42 @@
1
+ import { getApiKey, getApiUrl, requireApiKey } from '../lib/auth.js';
2
+ import { ApiClient } from '../lib/api-client.js';
3
+
4
+ export function logsCommand(program) {
5
+ program
6
+ .command('logs <session-id>')
7
+ .description('View logs/state for a specific agent session')
8
+ .option('--token <key>', 'API key')
9
+ .option('--tail <lines>', 'Show last N lines', parseInt)
10
+ .action(async (sessionId, options) => {
11
+ try {
12
+ // Get API key
13
+ const apiKey = getApiKey(options.token);
14
+ requireApiKey(apiKey);
15
+
16
+ // Create API client
17
+ const client = new ApiClient(getApiUrl(), apiKey);
18
+
19
+ // Get session state
20
+ const state = await client.getSessionState(sessionId);
21
+
22
+ // Format state as readable output
23
+ if (typeof state === 'string') {
24
+ console.log(state);
25
+ } else {
26
+ // Pretty print JSON state
27
+ const stateStr = JSON.stringify(state, null, 2);
28
+
29
+ if (options.tail) {
30
+ const lines = stateStr.split('\n');
31
+ const tailLines = lines.slice(-options.tail);
32
+ console.log(tailLines.join('\n'));
33
+ } else {
34
+ console.log(stateStr);
35
+ }
36
+ }
37
+ } catch (error) {
38
+ console.error('ERROR:', error.message);
39
+ process.exit(1);
40
+ }
41
+ });
42
+ }
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk';
2
+ import { getApiKey, getApiUrl, requireApiKey } from '../lib/auth.js';
3
+ import { ApiClient } from '../lib/api-client.js';
4
+ import { parsePrUrl } from '../lib/pr-parser.js';
5
+ import { createSpinner } from '../lib/formatters.js';
6
+
7
+ export function rejectCommand(program) {
8
+ program
9
+ .command('reject <pr-url>')
10
+ .description('Reject changes and close the PR')
11
+ .option('--org <slug>', 'Organization context')
12
+ .option('--token <key>', 'API key')
13
+ .option('--reason <text>', 'Optional rejection reason')
14
+ .action(async (prUrl, options) => {
15
+ const spinner = createSpinner('Rejecting pull request...');
16
+ try {
17
+ // Get API key
18
+ const apiKey = getApiKey(options.token);
19
+ requireApiKey(apiKey);
20
+
21
+ // Parse PR URL
22
+ const pr = parsePrUrl(prUrl);
23
+
24
+ // Create API client
25
+ const client = new ApiClient(getApiUrl(), apiKey);
26
+
27
+ spinner.start();
28
+
29
+ // Reject PR
30
+ const result = await client.rejectPr(pr.url, {
31
+ organizationSlug: options.org,
32
+ reason: options.reason,
33
+ });
34
+
35
+ spinner.stop();
36
+
37
+ console.log(chalk.red('✗') + ' Pull request rejected and closed.');
38
+ console.log(` ${prUrl}`);
39
+ if (options.reason) {
40
+ console.log(` Reason: ${options.reason}`);
41
+ }
42
+ } catch (error) {
43
+ spinner.stop();
44
+ console.error('ERROR:', error.message);
45
+ process.exit(1);
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,58 @@
1
+ import { getApiKey, getApiUrl, requireApiKey } from '../lib/auth.js';
2
+ import { ApiClient } from '../lib/api-client.js';
3
+ import { parsePrUrl } from '../lib/pr-parser.js';
4
+ import { formatSessionsTable, formatSessionsJson } from '../lib/formatters.js';
5
+
6
+ export function statusCommand(program) {
7
+ program
8
+ .command('status <pr-url>')
9
+ .description('List all agent checks for a pull request')
10
+ .option('--org <slug>', 'Organization context')
11
+ .option('--token <key>', 'API key')
12
+ .option('--format <type>', 'Output format (table|json)', 'table')
13
+ .action(async (prUrl, options) => {
14
+ try {
15
+ // Get API key
16
+ const apiKey = getApiKey(options.token);
17
+ requireApiKey(apiKey);
18
+
19
+ // Parse PR URL
20
+ const pr = parsePrUrl(prUrl);
21
+
22
+ // Create API client
23
+ const client = new ApiClient(getApiUrl(), apiKey);
24
+
25
+ // Get agent sessions
26
+ const result = await client.getAgentSessions(pr.url, {
27
+ organizationId: options.org,
28
+ });
29
+
30
+ const sessions = result.sessions || [];
31
+
32
+ // Calculate summary
33
+ const terminalStates = ['completed', 'failed', 'cancelled', 'suspended'];
34
+ const summary = {
35
+ total: sessions.length,
36
+ completed: sessions.filter(s => s.status === 'completed').length,
37
+ running: sessions.filter(s => !terminalStates.includes(s.status)).length,
38
+ failed: sessions.filter(s => s.status === 'failed').length,
39
+ pending: sessions.filter(s => s.status === 'pending').length,
40
+ };
41
+
42
+ // Format output
43
+ if (options.format === 'json') {
44
+ console.log(formatSessionsJson(sessions, summary));
45
+ } else {
46
+ console.log(formatSessionsTable(sessions, summary));
47
+ }
48
+
49
+ // Exit with error if no sessions found
50
+ if (sessions.length === 0) {
51
+ process.exit(2);
52
+ }
53
+ } catch (error) {
54
+ console.error('ERROR:', error.message);
55
+ process.exit(1);
56
+ }
57
+ });
58
+ }
@@ -0,0 +1,77 @@
1
+ import { getApiKey, getApiUrl, requireApiKey } from '../lib/auth.js';
2
+ import { ApiClient } from '../lib/api-client.js';
3
+ import { parsePrUrl } from '../lib/pr-parser.js';
4
+ import { formatSessionsTable, formatSessionsJson, createSpinner } from '../lib/formatters.js';
5
+
6
+ export function waitCommand(program) {
7
+ program
8
+ .command('wait <pr-url>')
9
+ .description('Block until all agent checks complete or fail')
10
+ .option('--org <slug>', 'Organization context')
11
+ .option('--token <key>', 'API key')
12
+ .option('--timeout <seconds>', 'Max wait time', parseInt, 600)
13
+ .option('--fail-fast', 'Exit immediately on first failure', false)
14
+ .option('--format <type>', 'Output format (table|json)', 'table')
15
+ .action(async (prUrl, options) => {
16
+ let spinner;
17
+ try {
18
+ // Get API key
19
+ const apiKey = getApiKey(options.token);
20
+ requireApiKey(apiKey);
21
+
22
+ // Parse PR URL
23
+ const pr = parsePrUrl(prUrl);
24
+
25
+ // Create API client
26
+ const client = new ApiClient(getApiUrl(), apiKey);
27
+
28
+ // Show spinner for table format
29
+ if (options.format !== 'json') {
30
+ spinner = createSpinner('Waiting for agent checks to complete...');
31
+ spinner.start();
32
+ }
33
+
34
+ // Wait for completion
35
+ const result = await client.waitForCompletion(pr.url, {
36
+ timeout: options.timeout,
37
+ failFast: options.failFast,
38
+ organizationId: options.org,
39
+ });
40
+
41
+ if (spinner) {
42
+ spinner.stop();
43
+ }
44
+
45
+ const sessions = result.sessions || [];
46
+ const summary = result.summary;
47
+
48
+ // Format output
49
+ if (options.format === 'json') {
50
+ console.log(formatSessionsJson(sessions, summary));
51
+ } else {
52
+ console.log(formatSessionsTable(sessions, summary));
53
+ }
54
+
55
+ // Exit codes
56
+ if (summary.failed > 0) {
57
+ process.exit(1); // Any failed
58
+ } else if (result.shouldContinuePolling) {
59
+ process.exit(124); // Timeout
60
+ } else {
61
+ process.exit(0); // Success
62
+ }
63
+ } catch (error) {
64
+ if (spinner) {
65
+ spinner.stop();
66
+ }
67
+
68
+ if (error.message.includes('Timeout')) {
69
+ console.error('ERROR: Timeout reached. Agent checks are still running.');
70
+ process.exit(124);
71
+ } else {
72
+ console.error('ERROR:', error.message);
73
+ process.exit(1);
74
+ }
75
+ }
76
+ });
77
+ }
package/src/index.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { statusCommand } from './commands/status.js';
4
+ import { waitCommand } from './commands/wait.js';
5
+ import { logsCommand } from './commands/logs.js';
6
+ import { diffCommand } from './commands/diff.js';
7
+ import { approveCommand } from './commands/approve.js';
8
+ import { rejectCommand } from './commands/reject.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('cai')
14
+ .description('CLI tool for interacting with Continue agent checks on pull requests')
15
+ .version('0.1.0');
16
+
17
+ // Register commands
18
+ statusCommand(program);
19
+ waitCommand(program);
20
+ logsCommand(program);
21
+ diffCommand(program);
22
+ approveCommand(program);
23
+ rejectCommand(program);
24
+
25
+ program.parse(process.argv);
@@ -0,0 +1,149 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ /**
4
+ * API client wrapper for Continue API
5
+ */
6
+ export class ApiClient {
7
+ constructor(apiUrl, apiKey) {
8
+ this.apiUrl = apiUrl;
9
+ this.apiKey = apiKey;
10
+ }
11
+
12
+ /**
13
+ * Make a GET request
14
+ */
15
+ async get(path, queryParams = {}) {
16
+ const url = new URL(path, this.apiUrl);
17
+ Object.entries(queryParams).forEach(([key, value]) => {
18
+ if (value !== undefined && value !== null) {
19
+ url.searchParams.append(key, String(value));
20
+ }
21
+ });
22
+
23
+ const response = await fetch(url.toString(), {
24
+ method: 'GET',
25
+ headers: {
26
+ 'Authorization': `Bearer ${this.apiKey}`,
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ });
30
+
31
+ if (!response.ok) {
32
+ const error = await response.text();
33
+ throw new Error(`API Error (${response.status}): ${error}`);
34
+ }
35
+
36
+ return response.json();
37
+ }
38
+
39
+ /**
40
+ * Make a POST request
41
+ */
42
+ async post(path, body = {}) {
43
+ const url = new URL(path, this.apiUrl);
44
+
45
+ const response = await fetch(url.toString(), {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Authorization': `Bearer ${this.apiKey}`,
49
+ 'Content-Type': 'application/json',
50
+ },
51
+ body: JSON.stringify(body),
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const error = await response.text();
56
+ throw new Error(`API Error (${response.status}): ${error}`);
57
+ }
58
+
59
+ return response.json();
60
+ }
61
+
62
+ /**
63
+ * Wait for agent completion (with polling)
64
+ */
65
+ async waitForCompletion(pullRequestUrl, options = {}) {
66
+ const { timeout = 600, failFast = false, organizationId } = options;
67
+ const startTime = Date.now();
68
+ const maxTime = timeout * 1000;
69
+ let backoffDelay = 1000; // Start with 1s
70
+
71
+ while (Date.now() - startTime < maxTime) {
72
+ const result = await this.get('/agents/wait-for-completion', {
73
+ pullRequestUrl,
74
+ organizationId,
75
+ timeout: 30, // Server-side timeout
76
+ failFast: failFast ? 'true' : 'false',
77
+ });
78
+
79
+ if (!result.shouldContinuePolling) {
80
+ return result;
81
+ }
82
+
83
+ // Wait with exponential backoff
84
+ await new Promise(resolve => setTimeout(resolve, Math.min(backoffDelay, 5000)));
85
+ backoffDelay = Math.min(backoffDelay * 1.5, 5000);
86
+ }
87
+
88
+ throw new Error('Timeout reached waiting for agent completion');
89
+ }
90
+
91
+ /**
92
+ * Get agent sessions for a PR
93
+ */
94
+ async getAgentSessions(pullRequestUrl, options = {}) {
95
+ const { organizationId, limit = 100, offset = 0 } = options;
96
+ return this.get('/agents', {
97
+ pullRequestUrl,
98
+ organizationId,
99
+ limit,
100
+ offset,
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Get agent session summary
106
+ */
107
+ async getSessionSummary(sessionId) {
108
+ return this.get(`/agents/${sessionId}/summary`);
109
+ }
110
+
111
+ /**
112
+ * Get agent session state (logs)
113
+ */
114
+ async getSessionState(sessionId) {
115
+ return this.get(`/agents/${sessionId}/state`);
116
+ }
117
+
118
+ /**
119
+ * Get agent session diff
120
+ */
121
+ async getSessionDiff(sessionId) {
122
+ return this.get(`/agents/${sessionId}/diff`);
123
+ }
124
+
125
+ /**
126
+ * Approve a PR and optionally merge
127
+ */
128
+ async approvePr(pullRequestUrl, options = {}) {
129
+ const { organizationSlug, autoMerge = false, mergeMethod = 'squash' } = options;
130
+ return this.post('/agents/approve-pr', {
131
+ pullRequestUrl,
132
+ organizationSlug,
133
+ autoMerge,
134
+ mergeMethod,
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Reject a PR (close it)
140
+ */
141
+ async rejectPr(pullRequestUrl, options = {}) {
142
+ const { organizationSlug, reason } = options;
143
+ return this.post('/agents/reject-pr', {
144
+ pullRequestUrl,
145
+ organizationSlug,
146
+ reason,
147
+ });
148
+ }
149
+ }
@@ -0,0 +1,71 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ /**
6
+ * Get API key from multiple sources (priority order):
7
+ * 1. --token flag
8
+ * 2. CONTINUE_API_KEY env var
9
+ * 3. ~/.continue/cli-config.json
10
+ */
11
+ export function getApiKey(tokenFlag) {
12
+ // 1. Check --token flag
13
+ if (tokenFlag) {
14
+ return tokenFlag;
15
+ }
16
+
17
+ // 2. Check environment variable
18
+ if (process.env.CONTINUE_API_KEY) {
19
+ return process.env.CONTINUE_API_KEY;
20
+ }
21
+
22
+ // 3. Check config file
23
+ const configPath = path.join(os.homedir(), '.continue', 'cli-config.json');
24
+ try {
25
+ if (fs.existsSync(configPath)) {
26
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
27
+ if (config.apiKey) {
28
+ return config.apiKey;
29
+ }
30
+ }
31
+ } catch (error) {
32
+ // Ignore config file errors, will show error below
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Get API URL from config or use default
40
+ */
41
+ export function getApiUrl() {
42
+ const configPath = path.join(os.homedir(), '.continue', 'cli-config.json');
43
+ try {
44
+ if (fs.existsSync(configPath)) {
45
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
46
+ if (config.apiUrl) {
47
+ return config.apiUrl;
48
+ }
49
+ }
50
+ } catch (error) {
51
+ // Ignore config file errors, use default
52
+ }
53
+
54
+ return process.env.CONTINUE_API_URL || 'https://api.continue.dev';
55
+ }
56
+
57
+ /**
58
+ * Validate that an API key is provided and show helpful error if not
59
+ */
60
+ export function requireApiKey(apiKey) {
61
+ if (!apiKey) {
62
+ // Use hub.continue.dev for the settings page (web UI), not the API URL
63
+ console.error(`ERROR: No API key provided. Get one at https://hub.continue.dev/settings/api-keys (or your self-hosted hub)`);
64
+ console.error('');
65
+ console.error('Set it with:');
66
+ console.error(' export CONTINUE_API_KEY="con-xxx"');
67
+ console.error('Or pass it with:');
68
+ console.error(' cai <command> --token "con-xxx"');
69
+ process.exit(1);
70
+ }
71
+ }
@@ -0,0 +1,126 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Format agent session status as a table
5
+ */
6
+ export function formatSessionsTable(sessions, summary) {
7
+ if (sessions.length === 0) {
8
+ return 'No agent sessions found for this PR.';
9
+ }
10
+
11
+ // Header
12
+ let output = '\nAgent Checks\n\n';
13
+
14
+ // Table header
15
+ const headers = ['Session', 'Status', 'Updated'];
16
+ const columnWidths = [12, 20, 20];
17
+
18
+ output += headers.map((h, i) => h.padEnd(columnWidths[i])).join(' ') + '\n';
19
+ output += columnWidths.map(w => '-'.repeat(w)).join(' ') + '\n';
20
+
21
+ // Table rows
22
+ for (const session of sessions) {
23
+ const sessionId = session.shortId || session.id.substring(0, 8);
24
+ const status = formatStatus(session.status);
25
+ const updated = formatRelativeTime(session.updatedAt);
26
+
27
+ output += [
28
+ sessionId.padEnd(columnWidths[0]),
29
+ status.padEnd(columnWidths[1] + 10), // +10 for ANSI codes
30
+ updated.padEnd(columnWidths[2]),
31
+ ].join(' ') + '\n';
32
+ }
33
+
34
+ // Summary
35
+ output += '\n';
36
+ output += `Summary: ${summary.completed} completed, ${summary.running} running, ${summary.failed} failed`;
37
+ if (summary.pending > 0) {
38
+ output += `, ${summary.pending} pending`;
39
+ }
40
+ output += '\n';
41
+
42
+ return output;
43
+ }
44
+
45
+ /**
46
+ * Format status with color and symbol
47
+ */
48
+ function formatStatus(status) {
49
+ switch (status) {
50
+ case 'completed':
51
+ return chalk.green('✓ Completed');
52
+ case 'failed':
53
+ return chalk.red('✗ Failed');
54
+ case 'running':
55
+ case 'in_progress':
56
+ return chalk.yellow('⏳ Running');
57
+ case 'pending':
58
+ return chalk.gray('⏸ Pending');
59
+ case 'suspended':
60
+ case 'cancelled':
61
+ return chalk.gray('⚠ Cancelled');
62
+ default:
63
+ return chalk.gray(status);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Format timestamp as relative time
69
+ */
70
+ function formatRelativeTime(timestamp) {
71
+ const now = Date.now();
72
+ const then = new Date(timestamp).getTime();
73
+ const diff = Math.abs(now - then) / 1000; // seconds
74
+
75
+ if (diff < 60) {
76
+ return 'just now';
77
+ } else if (diff < 3600) {
78
+ const mins = Math.floor(diff / 60);
79
+ return `${mins} minute${mins > 1 ? 's' : ''} ago`;
80
+ } else if (diff < 86400) {
81
+ const hours = Math.floor(diff / 3600);
82
+ return `${hours} hour${hours > 1 ? 's' : ''} ago`;
83
+ } else {
84
+ const days = Math.floor(diff / 86400);
85
+ return `${days} day${days > 1 ? 's' : ''} ago`;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Format sessions as JSON
91
+ */
92
+ export function formatSessionsJson(sessions, summary) {
93
+ return JSON.stringify({ sessions, summary }, null, 2);
94
+ }
95
+
96
+ /**
97
+ * Show spinner with message
98
+ */
99
+ export function createSpinner(message) {
100
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
101
+ let i = 0;
102
+ let interval;
103
+
104
+ return {
105
+ start() {
106
+ interval = setInterval(() => {
107
+ process.stdout.write(`\r${chalk.cyan(frames[i])} ${message}`);
108
+ i = (i + 1) % frames.length;
109
+ }, 80);
110
+ },
111
+ stop() {
112
+ if (interval) {
113
+ clearInterval(interval);
114
+ process.stdout.write('\r');
115
+ }
116
+ },
117
+ succeed(text) {
118
+ this.stop();
119
+ console.log(chalk.green('✓') + ' ' + text);
120
+ },
121
+ fail(text) {
122
+ this.stop();
123
+ console.log(chalk.red('✗') + ' ' + text);
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Parse a GitHub PR URL into its components
3
+ */
4
+ export function parsePrUrl(url) {
5
+ try {
6
+ // More strict regex: must be HTTPS GitHub URL with proper format
7
+ const match = url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)(?:[?#].*)?$/);
8
+ if (!match) {
9
+ throw new Error('Invalid GitHub PR URL format. Expected: https://github.com/owner/repo/pull/123');
10
+ }
11
+
12
+ const [, owner, repo, prNumber] = match;
13
+ return {
14
+ owner,
15
+ repo,
16
+ prNumber: parseInt(prNumber, 10),
17
+ url,
18
+ };
19
+ } catch (error) {
20
+ throw new Error(`Failed to parse PR URL: ${error.message}`);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Validate a PR URL
26
+ */
27
+ export function isValidPrUrl(url) {
28
+ try {
29
+ parsePrUrl(url);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }