@axhub/genie 0.2.2 → 0.2.4
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/dist/assets/App-BxazfNJn.js +484 -0
- package/dist/assets/{ReviewApp-Bp_y3xff.js → ReviewApp-CsqTAlGU.js} +1 -1
- package/dist/assets/{_basePickBy-C5f221Kr.js → _basePickBy-CFRQvihx.js} +1 -1
- package/dist/assets/{_baseUniq-CeEXFlBh.js → _baseUniq-Dhh8nCvs.js} +1 -1
- package/dist/assets/{arc-CZVQXROF.js → arc-DQ0v3dU4.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-D91MBXeh.js → architectureDiagram-2XIMDMQ5-DmUHdvQH.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-CsHP3zT2.js → blockDiagram-WCTKOSBZ-Bbxhj5KC.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-DakKlk21.js → c4Diagram-IC4MRINW-BOivDlQU.js} +1 -1
- package/dist/assets/channel-Cj8xVD0X.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-BXZoxrtv.js → chunk-4BX2VUAB-DlvtrM0q.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-V9_WXk3w.js → chunk-55IACEB6-DJUSHyTa.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-IgdHo6Dd.js → chunk-FMBD7UC4-C6Ch-htf.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-CnaAsDTd.js → chunk-JSJVCQXG-DzQIht58.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-D0qksU2H.js → chunk-KX2RTZJC-C05jARMH.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-rd6KG4-c.js → chunk-NQ4KR5QH-Ci-n7jfu.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Cyltgv4l.js → chunk-QZHKN3VN-jxti9HTX.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-DkNtSo86.js → chunk-WL4C6EOR-C559Mk71.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +1 -0
- package/dist/assets/clone-BEVqubrI.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-D_Wsd3iv.js → cose-bilkent-S5V4N54A-DNO9ncXL.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-CrrmZeGu.js → dagre-KLK3FWXG-DJ3dNSYk.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-WzO26pGn.js → diagram-E7M64L7V-Ba_LGLun.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-COynsdO3.js → diagram-IFDJBPK2-Da6K4aP-.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-CSqD5HJx.js → diagram-P4PSJMXO-vZZKB92A.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-BiFhS6xi.js → erDiagram-INFDFZHY-Csb8dFdP.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-9jdAJSs0.js → flowDiagram-PKNHOUZH-DUV13pHi.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-pGyMKWCo.js → ganttDiagram-A5KZAMGK-B5Kv9Wfz.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-D4jvwoW1.js → gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js} +1 -1
- package/dist/assets/{graph-DMknFkkX.js → graph-BbvHswRd.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-DhMGh1O7.js → highlighted-body-TPN3WLV5-DZJajMGm.js} +1 -1
- package/dist/assets/index-BFX9lxRB.css +1 -0
- package/dist/assets/index-BiErUGrv.js +2 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-CP5zYiVP.js → infoDiagram-LFFYTUFH-8auUIPKW.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-BplLZAQ5.js → ishikawaDiagram-PHBUUO56-JmsNlo2I.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-CYVgkl-y.js → journeyDiagram-4ABVD52K-Cuudv7Vv.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-D3on0q66.js → kanban-definition-K7BYSVSG-Bappd2YO.js} +1 -1
- package/dist/assets/{layout-DoKWZNVk.js → layout-BmbfFZKy.js} +1 -1
- package/dist/assets/{linear-D4YTLdon.js → linear-WZnF-PT6.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-KLW3VWsF.js → mermaid-O7DHMXV3-D-2fQRvw.js} +5 -5
- package/dist/assets/{mindmap-definition-YRQLILUH-EEAggxM3.js → mindmap-definition-YRQLILUH-BQHnzzud.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-Da-_fmYg.js → pieDiagram-SKSYHLDU-uxjlAy1t.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-Dq4gr7Sw.js → quadrantDiagram-337W2JSQ-DpwZU-f_.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-DNSXyCNU.js → requirementDiagram-Z7DCOOCP-C_9ClOWm.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-CT4ST2HW.js → sankeyDiagram-WA2Y5GQK-2-FHHM-R.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-CshVYjrF.js → sequenceDiagram-2WXFIKYE-egns-0XI.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-hsG5Yi2A.js → stateDiagram-RAJIS63D-DoW8U53H.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-EztFFK5F.js → timeline-definition-YZTLITO2-chPa8ppH.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-D5UZCcq_.js → treemap-KZPCXAKY-ajdAP-72.js} +1 -1
- package/dist/assets/{vennDiagram-LZ73GAT5-9Qwv8UTR.js → vennDiagram-LZ73GAT5-C9If0AT0.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-BBjpC1Qv.js → xychartDiagram-JWTSCODW-DD42U6Or.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/server/database/db.js +0 -45
- package/server/external-agent/service.js +39 -490
- package/server/external-agent/service.test.js +12 -0
- package/server/index.js +21 -43
- package/server/middleware/auth.js +15 -1
- package/server/routes/agent.js +15 -1136
- package/server/routes/cli-auth.js +60 -52
- package/server/routes/projects.js +10 -304
- package/server/routes/settings.js +4 -0
- package/server/routes/user.js +0 -102
- package/dist/assets/App-Cay5kE3A.js +0 -504
- package/dist/assets/channel-CnzaP2H9.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-CUcYxMZ8.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-CUcYxMZ8.js +0 -1
- package/dist/assets/clone-DJXJGSg2.js +0 -1
- package/dist/assets/index-2198VgsK.css +0 -1
- package/dist/assets/index-EMGPq9Uy.js +0 -2
- package/dist/assets/stateDiagram-v2-FVOUBMTO-BQa1Ppoo.js +0 -1
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
1
|
import { promises as fs } from 'fs';
|
|
5
|
-
import crypto from 'crypto';
|
|
6
|
-
import { Octokit } from '@octokit/rest';
|
|
7
2
|
|
|
8
|
-
import { githubTokensDb } from '../database/db.js';
|
|
9
3
|
import { addProjectManually } from '../projects.js';
|
|
10
4
|
import { queryClaudeSDK } from '../claude-sdk.js';
|
|
11
5
|
import { queryCodex } from '../openai-codex.js';
|
|
@@ -35,9 +29,15 @@ function parseBoolean(value, fallback) {
|
|
|
35
29
|
if (value === undefined) {
|
|
36
30
|
return fallback;
|
|
37
31
|
}
|
|
32
|
+
|
|
38
33
|
return value === true || value === 'true';
|
|
39
34
|
}
|
|
40
35
|
|
|
36
|
+
function getDeprecatedGitHubFields(body) {
|
|
37
|
+
const deprecatedFields = ['githubUrl', 'githubToken', 'branchName', 'createBranch', 'createPR'];
|
|
38
|
+
return deprecatedFields.filter((field) => body[field] !== undefined);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
41
|
export function buildSessionNavigation(sessionId) {
|
|
42
42
|
const normalizedSessionId = typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : null;
|
|
43
43
|
const sessionPath = normalizedSessionId ? `/session/${normalizedSessionId}` : null;
|
|
@@ -55,19 +55,25 @@ export function buildSessionNavigation(sessionId) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function normalizeExternalAgentRunRequest(body = {}) {
|
|
58
|
-
const {
|
|
58
|
+
const {
|
|
59
|
+
projectPath,
|
|
60
|
+
message,
|
|
61
|
+
model,
|
|
62
|
+
sessionId,
|
|
63
|
+
openOnly
|
|
64
|
+
} = body;
|
|
65
|
+
|
|
59
66
|
const provider = typeof body.provider === 'string' ? body.provider.trim() : 'claude';
|
|
60
67
|
const stream = parseBoolean(body.stream, true);
|
|
61
68
|
const requestedCleanup = parseBoolean(body.cleanup, true);
|
|
62
|
-
const createBranch = branchName ? true : parseBoolean(body.createBranch, false);
|
|
63
|
-
const createPR = parseBoolean(body.createPR, false);
|
|
64
69
|
const normalizedSessionId = typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : null;
|
|
65
70
|
const normalizedMessage = typeof message === 'string' ? message.trim() : '';
|
|
66
71
|
const requestedOpenOnly = openOnly === true || openOnly === 'true';
|
|
67
72
|
const isOpenOnly = requestedOpenOnly || (!normalizedMessage && !!normalizedSessionId);
|
|
68
73
|
const cleanup = isOpenOnly ? false : requestedCleanup;
|
|
69
74
|
const images = normalizeExternalImages(body.images);
|
|
70
|
-
const resolvedProjectPath =
|
|
75
|
+
const resolvedProjectPath = resolveWorkingDirectory({ projectPath });
|
|
76
|
+
const deprecatedGitHubFields = getDeprecatedGitHubFields(body);
|
|
71
77
|
|
|
72
78
|
let callbackConfig = null;
|
|
73
79
|
try {
|
|
@@ -84,6 +90,13 @@ export function normalizeExternalAgentRunRequest(body = {}) {
|
|
|
84
90
|
throw createRequestError('sessionId must be a non-empty string when provided', 400);
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
if (deprecatedGitHubFields.length > 0) {
|
|
94
|
+
throw createRequestError(
|
|
95
|
+
`Unsupported parameters: ${deprecatedGitHubFields.join(', ')}. GitHub repository cloning and automatic branch/PR creation have been removed from the external agent API.`,
|
|
96
|
+
400
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
if (isOpenOnly && !normalizedSessionId) {
|
|
88
101
|
throw createRequestError('sessionId is required when message is empty or openOnly=true', 400);
|
|
89
102
|
}
|
|
@@ -92,14 +105,6 @@ export function normalizeExternalAgentRunRequest(body = {}) {
|
|
|
92
105
|
throw createRequestError('message is required unless openOnly=true', 400);
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
if (isOpenOnly && githubUrl) {
|
|
96
|
-
throw createRequestError('githubUrl is not supported when openOnly=true', 400);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (isOpenOnly && (createBranch || createPR)) {
|
|
100
|
-
throw createRequestError('createBranch and createPR are not supported when openOnly=true', 400);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
108
|
if (isOpenOnly && callbackConfig) {
|
|
104
109
|
throw createRequestError('callback is not supported when openOnly=true', 400);
|
|
105
110
|
}
|
|
@@ -113,21 +118,16 @@ export function normalizeExternalAgentRunRequest(body = {}) {
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
return {
|
|
116
|
-
githubUrl,
|
|
117
121
|
projectPath: resolvedProjectPath,
|
|
118
122
|
message,
|
|
119
123
|
normalizedMessage,
|
|
120
124
|
provider,
|
|
121
125
|
model,
|
|
122
|
-
githubToken,
|
|
123
|
-
branchName,
|
|
124
126
|
sessionId,
|
|
125
127
|
normalizedSessionId,
|
|
126
128
|
openOnly,
|
|
127
129
|
requestedOpenOnly,
|
|
128
130
|
isOpenOnly,
|
|
129
|
-
createBranch,
|
|
130
|
-
createPR,
|
|
131
131
|
stream,
|
|
132
132
|
requestedCleanup,
|
|
133
133
|
cleanup,
|
|
@@ -156,262 +156,6 @@ export function normalizeExternalAgentAbortRequest(body = {}) {
|
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
async function getGitRemoteUrl(repoPath) {
|
|
160
|
-
return new Promise((resolve, reject) => {
|
|
161
|
-
const gitProcess = spawn('git', ['config', '--get', 'remote.origin.url'], {
|
|
162
|
-
cwd: repoPath,
|
|
163
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
let stdout = '';
|
|
167
|
-
let stderr = '';
|
|
168
|
-
|
|
169
|
-
gitProcess.stdout.on('data', (data) => {
|
|
170
|
-
stdout += data.toString();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
gitProcess.stderr.on('data', (data) => {
|
|
174
|
-
stderr += data.toString();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
gitProcess.on('close', (code) => {
|
|
178
|
-
if (code === 0) {
|
|
179
|
-
resolve(stdout.trim());
|
|
180
|
-
} else {
|
|
181
|
-
reject(new Error(`Failed to get git remote: ${stderr}`));
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
gitProcess.on('error', (error) => {
|
|
186
|
-
reject(new Error(`Failed to execute git: ${error.message}`));
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function normalizeGitHubUrl(url) {
|
|
192
|
-
let normalized = url.replace(/\.git$/, '');
|
|
193
|
-
normalized = normalized.replace(/^git@github\.com:/, 'https://github.com/');
|
|
194
|
-
normalized = normalized.replace(/\/$/, '');
|
|
195
|
-
return normalized.toLowerCase();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function parseGitHubUrl(url) {
|
|
199
|
-
const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
200
|
-
if (!match) {
|
|
201
|
-
throw new Error('Invalid GitHub URL format');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
owner: match[1],
|
|
206
|
-
repo: match[2].replace(/\.git$/, '')
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function autogenerateBranchName(message) {
|
|
211
|
-
let branchName = message
|
|
212
|
-
.toLowerCase()
|
|
213
|
-
.replace(/[^a-z0-9\s-]/g, '')
|
|
214
|
-
.replace(/\s+/g, '-')
|
|
215
|
-
.replace(/-+/g, '-')
|
|
216
|
-
.replace(/^-|-$/g, '');
|
|
217
|
-
|
|
218
|
-
if (!branchName) {
|
|
219
|
-
branchName = 'task';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const timestamp = Date.now().toString(36).slice(-6);
|
|
223
|
-
const suffix = `-${timestamp}`;
|
|
224
|
-
const maxBaseLength = 50 - suffix.length;
|
|
225
|
-
|
|
226
|
-
if (branchName.length > maxBaseLength) {
|
|
227
|
-
branchName = branchName.substring(0, maxBaseLength);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
branchName = branchName.replace(/-$/, '').replace(/^-+/, '');
|
|
231
|
-
|
|
232
|
-
if (!branchName || branchName.startsWith('-')) {
|
|
233
|
-
branchName = 'task';
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
branchName = `${branchName}${suffix}`;
|
|
237
|
-
|
|
238
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(branchName)) {
|
|
239
|
-
return `branch-${timestamp}`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return branchName;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function validateBranchName(branchName) {
|
|
246
|
-
if (!branchName || branchName.trim() === '') {
|
|
247
|
-
return { valid: false, error: 'Branch name cannot be empty' };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const invalidPatterns = [
|
|
251
|
-
{ pattern: /^\./, message: 'Branch name cannot start with a dot' },
|
|
252
|
-
{ pattern: /\.$/, message: 'Branch name cannot end with a dot' },
|
|
253
|
-
{ pattern: /\.\./, message: 'Branch name cannot contain consecutive dots (..)' },
|
|
254
|
-
{ pattern: /\s/, message: 'Branch name cannot contain spaces' },
|
|
255
|
-
{ pattern: /[~^:?*\[\\]/, message: 'Branch name cannot contain special characters: ~ ^ : ? * [ \\' },
|
|
256
|
-
{ pattern: /@{/, message: 'Branch name cannot contain @{' },
|
|
257
|
-
{ pattern: /\/$/, message: 'Branch name cannot end with a slash' },
|
|
258
|
-
{ pattern: /^\//, message: 'Branch name cannot start with a slash' },
|
|
259
|
-
{ pattern: /\/\//, message: 'Branch name cannot contain consecutive slashes' },
|
|
260
|
-
{ pattern: /\.lock$/, message: 'Branch name cannot end with .lock' }
|
|
261
|
-
];
|
|
262
|
-
|
|
263
|
-
for (const { pattern, message } of invalidPatterns) {
|
|
264
|
-
if (pattern.test(branchName)) {
|
|
265
|
-
return { valid: false, error: message };
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (/[\x00-\x1F\x7F]/.test(branchName)) {
|
|
270
|
-
return { valid: false, error: 'Branch name cannot contain control characters' };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return { valid: true };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function getCommitMessages(projectPath, limit = 5) {
|
|
277
|
-
return new Promise((resolve, reject) => {
|
|
278
|
-
const gitProcess = spawn('git', ['log', `-${limit}`, '--pretty=format:%s'], {
|
|
279
|
-
cwd: projectPath,
|
|
280
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
let stdout = '';
|
|
284
|
-
let stderr = '';
|
|
285
|
-
|
|
286
|
-
gitProcess.stdout.on('data', (data) => {
|
|
287
|
-
stdout += data.toString();
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
gitProcess.stderr.on('data', (data) => {
|
|
291
|
-
stderr += data.toString();
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
gitProcess.on('close', (code) => {
|
|
295
|
-
if (code === 0) {
|
|
296
|
-
resolve(stdout.trim().split('\n').filter((msg) => msg.length > 0));
|
|
297
|
-
} else {
|
|
298
|
-
reject(new Error(`Failed to get commit messages: ${stderr}`));
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
gitProcess.on('error', (error) => {
|
|
303
|
-
reject(new Error(`Failed to execute git: ${error.message}`));
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function createGitHubPR(octokit, owner, repo, branchName, title, body, baseBranch = 'main') {
|
|
309
|
-
const { data: pr } = await octokit.pulls.create({
|
|
310
|
-
owner,
|
|
311
|
-
repo,
|
|
312
|
-
title,
|
|
313
|
-
head: branchName,
|
|
314
|
-
base: baseBranch,
|
|
315
|
-
body
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
number: pr.number,
|
|
320
|
-
url: pr.html_url
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function cloneGitHubRepo(githubUrl, githubToken = null, projectPath) {
|
|
325
|
-
return new Promise(async (resolve, reject) => {
|
|
326
|
-
try {
|
|
327
|
-
if (!githubUrl || !githubUrl.includes('github.com')) {
|
|
328
|
-
throw new Error('Invalid GitHub URL');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const cloneDir = path.resolve(projectPath);
|
|
332
|
-
let directoryExists = false;
|
|
333
|
-
try {
|
|
334
|
-
await fs.access(cloneDir);
|
|
335
|
-
directoryExists = true;
|
|
336
|
-
} catch {
|
|
337
|
-
directoryExists = false;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (directoryExists) {
|
|
341
|
-
try {
|
|
342
|
-
const existingUrl = await getGitRemoteUrl(cloneDir);
|
|
343
|
-
const normalizedExisting = normalizeGitHubUrl(existingUrl);
|
|
344
|
-
const normalizedRequested = normalizeGitHubUrl(githubUrl);
|
|
345
|
-
|
|
346
|
-
if (normalizedExisting === normalizedRequested) {
|
|
347
|
-
return resolve(cloneDir);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
throw new Error(`Directory ${cloneDir} already exists with a different repository (${existingUrl}). Expected: ${githubUrl}`);
|
|
351
|
-
} catch (error) {
|
|
352
|
-
if (error.message && error.message.includes('different repository')) {
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
355
|
-
throw new Error(`Directory ${cloneDir} already exists but is not a valid git repository or git command failed`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
await fs.mkdir(path.dirname(cloneDir), { recursive: true });
|
|
360
|
-
|
|
361
|
-
let cloneUrl = githubUrl;
|
|
362
|
-
if (githubToken) {
|
|
363
|
-
cloneUrl = githubUrl.replace('https://github.com', `https://${githubToken}@github.com`);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const gitProcess = spawn('git', ['clone', '--depth', '1', cloneUrl, cloneDir], {
|
|
367
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
let stderr = '';
|
|
371
|
-
|
|
372
|
-
gitProcess.stderr.on('data', (data) => {
|
|
373
|
-
stderr += data.toString();
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
gitProcess.on('close', (code) => {
|
|
377
|
-
if (code === 0) {
|
|
378
|
-
resolve(cloneDir);
|
|
379
|
-
} else {
|
|
380
|
-
reject(new Error(`Git clone failed: ${stderr}`));
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
gitProcess.on('error', (error) => {
|
|
385
|
-
reject(new Error(`Failed to execute git: ${error.message}`));
|
|
386
|
-
});
|
|
387
|
-
} catch (error) {
|
|
388
|
-
reject(error);
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function cleanupProject(projectPath, sessionId = null) {
|
|
394
|
-
try {
|
|
395
|
-
if (!projectPath.includes('.claude/external-projects')) {
|
|
396
|
-
console.warn('Refusing to clean up non-external project:', projectPath);
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
await fs.rm(projectPath, { recursive: true, force: true });
|
|
401
|
-
|
|
402
|
-
if (sessionId) {
|
|
403
|
-
try {
|
|
404
|
-
const sessionPath = path.join(os.homedir(), '.claude', 'sessions', sessionId);
|
|
405
|
-
await fs.rm(sessionPath, { recursive: true, force: true });
|
|
406
|
-
} catch (error) {
|
|
407
|
-
console.error('Failed to clean up session directory:', error.message);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
} catch (error) {
|
|
411
|
-
console.error('Failed to clean up project:', error);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
159
|
export class SSEStreamWriter {
|
|
416
160
|
constructor(res) {
|
|
417
161
|
this.res = res;
|
|
@@ -607,7 +351,7 @@ export class CallbackCaptureWriter {
|
|
|
607
351
|
}
|
|
608
352
|
}
|
|
609
353
|
|
|
610
|
-
|
|
354
|
+
class TeeWriter {
|
|
611
355
|
constructor(primaryWriter, secondaryWriter) {
|
|
612
356
|
this.primaryWriter = primaryWriter;
|
|
613
357
|
this.secondaryWriter = secondaryWriter;
|
|
@@ -641,6 +385,14 @@ export class NoopWriter {
|
|
|
641
385
|
end() {}
|
|
642
386
|
}
|
|
643
387
|
|
|
388
|
+
async function ensureProjectPathExists(projectPath) {
|
|
389
|
+
try {
|
|
390
|
+
await fs.access(projectPath);
|
|
391
|
+
} catch {
|
|
392
|
+
throw new Error(`Project path does not exist: ${projectPath}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
644
396
|
async function runProviderSession({ provider, message, images, finalProjectPath, sessionId, model, writer }) {
|
|
645
397
|
if (provider === 'claude') {
|
|
646
398
|
await queryClaudeSDK(message, {
|
|
@@ -690,159 +442,14 @@ async function runProviderSession({ provider, message, images, finalProjectPath,
|
|
|
690
442
|
}
|
|
691
443
|
}
|
|
692
444
|
|
|
693
|
-
async function maybeCreateBranchAndPR({
|
|
694
|
-
user,
|
|
695
|
-
githubToken,
|
|
696
|
-
githubUrl,
|
|
697
|
-
finalProjectPath,
|
|
698
|
-
branchName,
|
|
699
|
-
message,
|
|
700
|
-
createBranch,
|
|
701
|
-
createPR,
|
|
702
|
-
transportWriter
|
|
703
|
-
}) {
|
|
704
|
-
let branchInfo = null;
|
|
705
|
-
let prInfo = null;
|
|
706
|
-
|
|
707
|
-
if (!createBranch && !createPR) {
|
|
708
|
-
return { branchInfo, prInfo };
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
try {
|
|
712
|
-
const tokenToUse = githubToken || githubTokensDb.getActiveGithubToken(user?.id);
|
|
713
|
-
if (!tokenToUse) {
|
|
714
|
-
throw new Error('GitHub token required for branch/PR creation. Please configure a GitHub token in settings.');
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const octokit = new Octokit({ auth: tokenToUse });
|
|
718
|
-
let repoUrl = githubUrl;
|
|
719
|
-
|
|
720
|
-
if (!repoUrl) {
|
|
721
|
-
repoUrl = await getGitRemoteUrl(finalProjectPath);
|
|
722
|
-
if (!repoUrl.includes('github.com')) {
|
|
723
|
-
throw new Error('Project does not have a GitHub remote configured');
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const { owner, repo } = parseGitHubUrl(repoUrl);
|
|
728
|
-
const finalBranchName = branchName || autogenerateBranchName(message);
|
|
729
|
-
|
|
730
|
-
if (branchName) {
|
|
731
|
-
const validation = validateBranchName(finalBranchName);
|
|
732
|
-
if (!validation.valid) {
|
|
733
|
-
throw new Error(`Invalid branch name: ${validation.error}`);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (createBranch) {
|
|
738
|
-
await new Promise((resolve, reject) => {
|
|
739
|
-
const checkoutProcess = spawn('git', ['checkout', '-b', finalBranchName], {
|
|
740
|
-
cwd: finalProjectPath,
|
|
741
|
-
stdio: 'pipe'
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
let stderr = '';
|
|
745
|
-
checkoutProcess.stderr.on('data', (data) => {
|
|
746
|
-
stderr += data.toString();
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
checkoutProcess.on('close', (code) => {
|
|
750
|
-
if (code === 0) {
|
|
751
|
-
resolve();
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (stderr.includes('already exists')) {
|
|
756
|
-
const checkoutExisting = spawn('git', ['checkout', finalBranchName], {
|
|
757
|
-
cwd: finalProjectPath,
|
|
758
|
-
stdio: 'pipe'
|
|
759
|
-
});
|
|
760
|
-
checkoutExisting.on('close', (checkoutCode) => {
|
|
761
|
-
if (checkoutCode === 0) {
|
|
762
|
-
resolve();
|
|
763
|
-
} else {
|
|
764
|
-
reject(new Error(`Failed to checkout existing branch: ${stderr}`));
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
reject(new Error(`Failed to create branch: ${stderr}`));
|
|
771
|
-
});
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
await new Promise((resolve, reject) => {
|
|
775
|
-
const pushProcess = spawn('git', ['push', '-u', 'origin', finalBranchName], {
|
|
776
|
-
cwd: finalProjectPath,
|
|
777
|
-
stdio: 'pipe'
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
let stderr = '';
|
|
781
|
-
pushProcess.stderr.on('data', (data) => {
|
|
782
|
-
stderr += data.toString();
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
pushProcess.on('close', (code) => {
|
|
786
|
-
if (code === 0 || stderr.includes('already exists') || stderr.includes('up-to-date')) {
|
|
787
|
-
resolve();
|
|
788
|
-
} else {
|
|
789
|
-
reject(new Error(`Failed to push branch: ${stderr}`));
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
branchInfo = {
|
|
795
|
-
name: finalBranchName,
|
|
796
|
-
url: `https://github.com/${owner}/${repo}/tree/${finalBranchName}`
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (createPR) {
|
|
801
|
-
const commitMessages = await getCommitMessages(finalProjectPath, 5);
|
|
802
|
-
const prTitle = commitMessages.length > 0 ? commitMessages[0] : message;
|
|
803
|
-
let prBody = '## Changes\n\n';
|
|
804
|
-
if (commitMessages.length > 0) {
|
|
805
|
-
prBody += commitMessages.map((msg) => `- ${msg}`).join('\n');
|
|
806
|
-
} else {
|
|
807
|
-
prBody += `Agent task: ${message}`;
|
|
808
|
-
}
|
|
809
|
-
prBody += '\n\n---\n*This pull request was automatically created by Axhub Genie Agent.*';
|
|
810
|
-
|
|
811
|
-
prInfo = await createGitHubPR(octokit, owner, repo, finalBranchName, prTitle, prBody, 'main');
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
if (branchInfo) {
|
|
815
|
-
transportWriter?.send?.({
|
|
816
|
-
type: 'github-branch',
|
|
817
|
-
branch: branchInfo
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
if (prInfo) {
|
|
822
|
-
transportWriter?.send?.({
|
|
823
|
-
type: 'github-pr',
|
|
824
|
-
pullRequest: prInfo
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
} catch (error) {
|
|
828
|
-
transportWriter?.send?.({
|
|
829
|
-
type: 'github-error',
|
|
830
|
-
error: error.message
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
branchInfo = { error: error.message };
|
|
834
|
-
prInfo = { error: error.message };
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return { branchInfo, prInfo };
|
|
838
|
-
}
|
|
839
|
-
|
|
840
445
|
export async function runExternalAgentRequest({
|
|
841
446
|
user,
|
|
842
447
|
request,
|
|
843
448
|
transportWriter,
|
|
844
449
|
endTransportOnFinish = false
|
|
845
450
|
}) {
|
|
451
|
+
void user;
|
|
452
|
+
|
|
846
453
|
const normalized = request?.normalizedMessage !== undefined
|
|
847
454
|
? request
|
|
848
455
|
: normalizeExternalAgentRunRequest(request);
|
|
@@ -870,32 +477,14 @@ export async function runExternalAgentRequest({
|
|
|
870
477
|
return response;
|
|
871
478
|
}
|
|
872
479
|
|
|
873
|
-
|
|
874
|
-
let branchInfo = null;
|
|
875
|
-
let prInfo = null;
|
|
480
|
+
const finalProjectPath = resolveWorkingDirectory({ projectPath: normalized.projectPath });
|
|
876
481
|
const callbackCaptureWriter = new CallbackCaptureWriter();
|
|
877
482
|
const writer = new TeeWriter(transportWriter, callbackCaptureWriter);
|
|
878
483
|
let callbackSessionId = normalized.normalizedSessionId;
|
|
879
484
|
let callbackResult = null;
|
|
880
485
|
|
|
881
486
|
try {
|
|
882
|
-
|
|
883
|
-
const tokenToUse = normalized.githubToken || githubTokensDb.getActiveGithubToken(user?.id);
|
|
884
|
-
let targetPath = normalized.projectPath;
|
|
885
|
-
if (!targetPath) {
|
|
886
|
-
const repoHash = crypto.createHash('md5').update(normalized.githubUrl + Date.now()).digest('hex');
|
|
887
|
-
targetPath = path.join(os.homedir(), '.claude', 'external-projects', repoHash);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
finalProjectPath = await cloneGitHubRepo(normalized.githubUrl.trim(), tokenToUse, targetPath);
|
|
891
|
-
} else {
|
|
892
|
-
finalProjectPath = resolveWorkingDirectory({ projectPath: normalized.projectPath });
|
|
893
|
-
try {
|
|
894
|
-
await fs.access(finalProjectPath);
|
|
895
|
-
} catch {
|
|
896
|
-
throw new Error(`Project path does not exist: ${finalProjectPath}`);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
487
|
+
await ensureProjectPathExists(finalProjectPath);
|
|
899
488
|
|
|
900
489
|
try {
|
|
901
490
|
await addProjectManually(finalProjectPath);
|
|
@@ -905,13 +494,9 @@ export async function runExternalAgentRequest({
|
|
|
905
494
|
}
|
|
906
495
|
}
|
|
907
496
|
|
|
908
|
-
const statusMessage = normalized.normalizedSessionId
|
|
909
|
-
? (normalized.githubUrl ? 'Repository cloned and session resumed' : 'Session resumed')
|
|
910
|
-
: (normalized.githubUrl ? 'Repository cloned and session started' : 'Session started');
|
|
911
|
-
|
|
912
497
|
transportWriter?.send?.({
|
|
913
498
|
type: 'status',
|
|
914
|
-
message:
|
|
499
|
+
message: normalized.normalizedSessionId ? 'Session resumed' : 'Session started',
|
|
915
500
|
projectPath: finalProjectPath
|
|
916
501
|
});
|
|
917
502
|
|
|
@@ -936,19 +521,6 @@ export async function runExternalAgentRequest({
|
|
|
936
521
|
throw new AgentSessionAbortedError();
|
|
937
522
|
}
|
|
938
523
|
|
|
939
|
-
({ branchInfo, prInfo } = await maybeCreateBranchAndPR({
|
|
940
|
-
user,
|
|
941
|
-
githubToken: normalized.githubToken,
|
|
942
|
-
githubUrl: normalized.githubUrl,
|
|
943
|
-
finalProjectPath,
|
|
944
|
-
branchName: normalized.branchName,
|
|
945
|
-
message: normalized.message,
|
|
946
|
-
createBranch: normalized.createBranch,
|
|
947
|
-
createPR: normalized.createPR,
|
|
948
|
-
transportWriter
|
|
949
|
-
}));
|
|
950
|
-
|
|
951
|
-
callbackSessionId = writer.getSessionId() || callbackCaptureWriter.getSessionId() || normalized.normalizedSessionId;
|
|
952
524
|
const navigation = buildSessionNavigation(callbackSessionId);
|
|
953
525
|
const response = {
|
|
954
526
|
success: true,
|
|
@@ -960,20 +532,11 @@ export async function runExternalAgentRequest({
|
|
|
960
532
|
projectPath: finalProjectPath
|
|
961
533
|
};
|
|
962
534
|
|
|
963
|
-
if (branchInfo) {
|
|
964
|
-
response.branch = branchInfo;
|
|
965
|
-
}
|
|
966
|
-
if (prInfo) {
|
|
967
|
-
response.pullRequest = prInfo;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
535
|
callbackResult = {
|
|
971
536
|
sessionPath: navigation.sessionPath,
|
|
972
537
|
sessionUrl: navigation.sessionUrl,
|
|
973
538
|
messages: response.messages,
|
|
974
|
-
tokens: response.tokens
|
|
975
|
-
branch: branchInfo,
|
|
976
|
-
pullRequest: prInfo
|
|
539
|
+
tokens: response.tokens
|
|
977
540
|
};
|
|
978
541
|
|
|
979
542
|
if (normalized.callbackConfig && shouldDeliverAgentCallback(normalized.callbackConfig, 'completed')) {
|
|
@@ -995,13 +558,6 @@ export async function runExternalAgentRequest({
|
|
|
995
558
|
});
|
|
996
559
|
}
|
|
997
560
|
|
|
998
|
-
if (normalized.cleanup && normalized.githubUrl) {
|
|
999
|
-
const sessionIdForCleanup = writer.getSessionId();
|
|
1000
|
-
setTimeout(() => {
|
|
1001
|
-
cleanupProject(finalProjectPath, sessionIdForCleanup);
|
|
1002
|
-
}, 5000);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
561
|
if (endTransportOnFinish) {
|
|
1006
562
|
transportWriter?.end?.();
|
|
1007
563
|
}
|
|
@@ -1012,16 +568,9 @@ export async function runExternalAgentRequest({
|
|
|
1012
568
|
callbackResult = {
|
|
1013
569
|
...buildSessionNavigation(callbackSessionId),
|
|
1014
570
|
messages: callbackCaptureWriter.getAssistantMessages(),
|
|
1015
|
-
tokens: callbackCaptureWriter.getTotalTokens()
|
|
1016
|
-
branch: branchInfo,
|
|
1017
|
-
pullRequest: prInfo
|
|
571
|
+
tokens: callbackCaptureWriter.getTotalTokens()
|
|
1018
572
|
};
|
|
1019
573
|
|
|
1020
|
-
if (finalProjectPath && normalized.cleanup && normalized.githubUrl) {
|
|
1021
|
-
const sessionIdForCleanup = writer.getSessionId();
|
|
1022
|
-
await cleanupProject(finalProjectPath, sessionIdForCleanup);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
574
|
const terminalEvent = error instanceof AgentSessionAbortedError ? 'aborted' : 'errored';
|
|
1026
575
|
if (normalized.callbackConfig && shouldDeliverAgentCallback(normalized.callbackConfig, terminalEvent)) {
|
|
1027
576
|
const callbackError = terminalEvent === 'aborted'
|
|
@@ -39,3 +39,15 @@ test('normalizeExternalAgentRunRequest falls back to process cwd when no project
|
|
|
39
39
|
|
|
40
40
|
assert.equal(normalized.projectPath, process.cwd());
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
test('normalizeExternalAgentRunRequest rejects deprecated GitHub parameters', () => {
|
|
44
|
+
assert.throws(() => {
|
|
45
|
+
normalizeExternalAgentRunRequest({
|
|
46
|
+
provider: 'codex',
|
|
47
|
+
message: 'Inspect the repository',
|
|
48
|
+
githubUrl: 'https://github.com/example/repo'
|
|
49
|
+
});
|
|
50
|
+
}, {
|
|
51
|
+
message: /Unsupported parameters: githubUrl/
|
|
52
|
+
});
|
|
53
|
+
});
|