@bretwardjames/ghp-core 0.2.0-beta.3 → 0.2.0-beta.5
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/index.cjs +1536 -7
- package/dist/index.d.cts +780 -3
- package/dist/index.d.ts +780 -3
- package/dist/index.js +1501 -7
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -22,54 +32,79 @@ var index_exports = {};
|
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
BranchLinker: () => BranchLinker,
|
|
24
34
|
CLI_TO_VSCODE_MAP: () => CLI_TO_VSCODE_MAP,
|
|
35
|
+
ClaudeClient: () => ClaudeClient,
|
|
25
36
|
DEFAULT_VALUES: () => DEFAULT_VALUES,
|
|
37
|
+
GHP_TOOLS: () => GHP_TOOLS,
|
|
26
38
|
GitHubAPI: () => GitHubAPI,
|
|
27
39
|
SETTING_DISPLAY_NAMES: () => SETTING_DISPLAY_NAMES,
|
|
28
40
|
SYNCABLE_KEYS: () => SYNCABLE_KEYS,
|
|
41
|
+
SessionWatcher: () => SessionWatcher,
|
|
42
|
+
TOOL_NAMES: () => TOOL_NAMES,
|
|
29
43
|
VSCODE_TO_CLI_MAP: () => VSCODE_TO_CLI_MAP,
|
|
30
44
|
branchExists: () => branchExists,
|
|
45
|
+
buildConventionsContext: () => buildConventionsContext,
|
|
31
46
|
buildIssueUrl: () => buildIssueUrl,
|
|
32
47
|
buildOrgProjectUrl: () => buildOrgProjectUrl,
|
|
33
48
|
buildProjectUrl: () => buildProjectUrl,
|
|
34
49
|
buildPullRequestUrl: () => buildPullRequestUrl,
|
|
35
50
|
buildRepoUrl: () => buildRepoUrl,
|
|
51
|
+
checkTmuxForPermission: () => checkTmuxForPermission,
|
|
36
52
|
checkoutBranch: () => checkoutBranch,
|
|
53
|
+
claudePrompts: () => prompts_exports,
|
|
54
|
+
cleanupStaleAgents: () => cleanupStaleAgents,
|
|
37
55
|
computeSettingsDiff: () => computeSettingsDiff,
|
|
38
56
|
createBranch: () => createBranch,
|
|
57
|
+
createSessionWatcher: () => createSessionWatcher,
|
|
39
58
|
createWorktree: () => createWorktree,
|
|
40
59
|
detectRepository: () => detectRepository,
|
|
41
60
|
extractIssueNumberFromBranch: () => extractIssueNumberFromBranch,
|
|
42
61
|
fetchOrigin: () => fetchOrigin,
|
|
62
|
+
findSessionFile: () => findSessionFile,
|
|
63
|
+
formatAction: () => formatAction,
|
|
43
64
|
formatConflict: () => formatConflict,
|
|
44
65
|
generateBranchName: () => generateBranchName,
|
|
45
66
|
generateWorktreePath: () => generateWorktreePath,
|
|
67
|
+
getAgent: () => getAgent,
|
|
68
|
+
getAgentByIssue: () => getAgentByIssue,
|
|
69
|
+
getAgentSummaries: () => getAgentSummaries,
|
|
46
70
|
getAllBranches: () => getAllBranches,
|
|
47
71
|
getCommitsAhead: () => getCommitsAhead,
|
|
48
72
|
getCommitsBehind: () => getCommitsBehind,
|
|
49
73
|
getCurrentBranch: () => getCurrentBranch,
|
|
50
74
|
getDefaultBranch: () => getDefaultBranch,
|
|
51
75
|
getDiffSummary: () => getDiffSummary,
|
|
76
|
+
getIssueReferenceText: () => getIssueReferenceText,
|
|
52
77
|
getLocalBranches: () => getLocalBranches,
|
|
78
|
+
getRegistryPath: () => getRegistryPath,
|
|
53
79
|
getRemoteBranches: () => getRemoteBranches,
|
|
54
80
|
getRepositoryRoot: () => getRepositoryRoot,
|
|
81
|
+
getTools: () => getTools,
|
|
55
82
|
getWorktreeForBranch: () => getWorktreeForBranch,
|
|
56
83
|
hasDifferences: () => hasDifferences,
|
|
57
84
|
hasUncommittedChanges: () => hasUncommittedChanges,
|
|
58
85
|
isGitRepository: () => isGitRepository,
|
|
86
|
+
listAgents: () => listAgents,
|
|
59
87
|
listWorktrees: () => listWorktrees,
|
|
88
|
+
loadProjectConventions: () => loadProjectConventions,
|
|
89
|
+
loadRegistry: () => loadRegistry,
|
|
60
90
|
normalizeVSCodeSettings: () => normalizeVSCodeSettings,
|
|
61
91
|
parseBranchLink: () => parseBranchLink,
|
|
62
92
|
parseGitHubUrl: () => parseGitHubUrl,
|
|
63
93
|
parseIssueUrl: () => parseIssueUrl,
|
|
94
|
+
parseSessionLine: () => parseSessionLine,
|
|
64
95
|
pullLatest: () => pullLatest,
|
|
65
96
|
queries: () => queries_exports,
|
|
97
|
+
registerAgent: () => registerAgent,
|
|
66
98
|
removeBranchLinkFromBody: () => removeBranchLinkFromBody,
|
|
67
99
|
removeWorktree: () => removeWorktree,
|
|
68
100
|
resolveConflicts: () => resolveConflicts,
|
|
69
101
|
sanitizeForBranchName: () => sanitizeForBranchName,
|
|
102
|
+
saveRegistry: () => saveRegistry,
|
|
70
103
|
setBranchLinkInBody: () => setBranchLinkInBody,
|
|
71
104
|
skip: () => skip,
|
|
72
105
|
toVSCodeSettings: () => toVSCodeSettings,
|
|
106
|
+
unregisterAgent: () => unregisterAgent,
|
|
107
|
+
updateAgent: () => updateAgent,
|
|
73
108
|
useCli: () => useCli,
|
|
74
109
|
useCustom: () => useCustom,
|
|
75
110
|
useVSCode: () => useVSCode,
|
|
@@ -85,6 +120,7 @@ var queries_exports = {};
|
|
|
85
120
|
__export(queries_exports, {
|
|
86
121
|
ADD_COMMENT_MUTATION: () => ADD_COMMENT_MUTATION,
|
|
87
122
|
ADD_LABELS_MUTATION: () => ADD_LABELS_MUTATION,
|
|
123
|
+
ADD_SUB_ISSUE_MUTATION: () => ADD_SUB_ISSUE_MUTATION,
|
|
88
124
|
ADD_TO_PROJECT_MUTATION: () => ADD_TO_PROJECT_MUTATION,
|
|
89
125
|
COLLABORATORS_QUERY: () => COLLABORATORS_QUERY,
|
|
90
126
|
CREATE_ISSUE_MUTATION: () => CREATE_ISSUE_MUTATION,
|
|
@@ -93,6 +129,7 @@ __export(queries_exports, {
|
|
|
93
129
|
ISSUE_DETAILS_QUERY: () => ISSUE_DETAILS_QUERY,
|
|
94
130
|
ISSUE_FOR_UPDATE_QUERY: () => ISSUE_FOR_UPDATE_QUERY,
|
|
95
131
|
ISSUE_NODE_ID_QUERY: () => ISSUE_NODE_ID_QUERY,
|
|
132
|
+
ISSUE_RELATIONSHIPS_QUERY: () => ISSUE_RELATIONSHIPS_QUERY,
|
|
96
133
|
ISSUE_TYPES_QUERY: () => ISSUE_TYPES_QUERY,
|
|
97
134
|
LABEL_EXISTS_QUERY: () => LABEL_EXISTS_QUERY,
|
|
98
135
|
PROJECT_FIELDS_QUERY: () => PROJECT_FIELDS_QUERY,
|
|
@@ -100,6 +137,7 @@ __export(queries_exports, {
|
|
|
100
137
|
PROJECT_VIEWS_QUERY: () => PROJECT_VIEWS_QUERY,
|
|
101
138
|
RECENT_ISSUES_QUERY: () => RECENT_ISSUES_QUERY,
|
|
102
139
|
REMOVE_LABELS_MUTATION: () => REMOVE_LABELS_MUTATION,
|
|
140
|
+
REMOVE_SUB_ISSUE_MUTATION: () => REMOVE_SUB_ISSUE_MUTATION,
|
|
103
141
|
REPOSITORY_ID_QUERY: () => REPOSITORY_ID_QUERY,
|
|
104
142
|
REPOSITORY_PROJECTS_QUERY: () => REPOSITORY_PROJECTS_QUERY,
|
|
105
143
|
UPDATE_ISSUE_BODY_MUTATION: () => UPDATE_ISSUE_BODY_MUTATION,
|
|
@@ -180,6 +218,8 @@ var PROJECT_ITEMS_QUERY = `
|
|
|
180
218
|
assignees(first: 5) { nodes { login } }
|
|
181
219
|
labels(first: 10) { nodes { name color } }
|
|
182
220
|
repository { name }
|
|
221
|
+
parent { id number title state }
|
|
222
|
+
subIssues(first: 50) { nodes { id number title state } }
|
|
183
223
|
}
|
|
184
224
|
... on PullRequest {
|
|
185
225
|
title
|
|
@@ -468,6 +508,37 @@ var UPDATE_ISSUE_MUTATION = `
|
|
|
468
508
|
}
|
|
469
509
|
}
|
|
470
510
|
`;
|
|
511
|
+
var ADD_SUB_ISSUE_MUTATION = `
|
|
512
|
+
mutation($issueId: ID!, $subIssueId: ID!) {
|
|
513
|
+
addSubIssue(input: { issueId: $issueId, subIssueId: $subIssueId }) {
|
|
514
|
+
issue { id number title }
|
|
515
|
+
subIssue { id number title }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
`;
|
|
519
|
+
var REMOVE_SUB_ISSUE_MUTATION = `
|
|
520
|
+
mutation($issueId: ID!, $subIssueId: ID!) {
|
|
521
|
+
removeSubIssue(input: { issueId: $issueId, subIssueId: $subIssueId }) {
|
|
522
|
+
issue { id number title }
|
|
523
|
+
subIssue { id number title }
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
`;
|
|
527
|
+
var ISSUE_RELATIONSHIPS_QUERY = `
|
|
528
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
529
|
+
repository(owner: $owner, name: $name) {
|
|
530
|
+
issue(number: $number) {
|
|
531
|
+
id
|
|
532
|
+
number
|
|
533
|
+
title
|
|
534
|
+
parent { id number title state }
|
|
535
|
+
subIssues(first: 50) {
|
|
536
|
+
nodes { id number title state }
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
`;
|
|
471
542
|
|
|
472
543
|
// src/github-api.ts
|
|
473
544
|
function createAuthError(message, type, details) {
|
|
@@ -502,6 +573,7 @@ function checkAuthError(error) {
|
|
|
502
573
|
}
|
|
503
574
|
var GitHubAPI = class {
|
|
504
575
|
graphqlWithAuth = null;
|
|
576
|
+
graphqlWithSubIssues = null;
|
|
505
577
|
tokenProvider;
|
|
506
578
|
onAuthError;
|
|
507
579
|
username = null;
|
|
@@ -535,12 +607,19 @@ var GitHubAPI = class {
|
|
|
535
607
|
authorization: `token ${token}`
|
|
536
608
|
}
|
|
537
609
|
});
|
|
610
|
+
this.graphqlWithSubIssues = import_graphql.graphql.defaults({
|
|
611
|
+
headers: {
|
|
612
|
+
authorization: `token ${token}`,
|
|
613
|
+
"GraphQL-Features": "sub_issues"
|
|
614
|
+
}
|
|
615
|
+
});
|
|
538
616
|
try {
|
|
539
617
|
const response = await this.graphqlWithAuth(VIEWER_QUERY);
|
|
540
618
|
this.username = response.viewer.login;
|
|
541
619
|
return true;
|
|
542
620
|
} catch {
|
|
543
621
|
this.graphqlWithAuth = null;
|
|
622
|
+
this.graphqlWithSubIssues = null;
|
|
544
623
|
return false;
|
|
545
624
|
}
|
|
546
625
|
}
|
|
@@ -582,7 +661,7 @@ var GitHubAPI = class {
|
|
|
582
661
|
* Get items from a project
|
|
583
662
|
*/
|
|
584
663
|
async getProjectItems(projectId, projectTitle) {
|
|
585
|
-
if (!this.
|
|
664
|
+
if (!this.graphqlWithSubIssues) throw new Error("Not authenticated");
|
|
586
665
|
const statusField = await this.getStatusField(projectId);
|
|
587
666
|
const statusOrderMap = /* @__PURE__ */ new Map();
|
|
588
667
|
if (statusField) {
|
|
@@ -590,7 +669,7 @@ var GitHubAPI = class {
|
|
|
590
669
|
statusOrderMap.set(opt.name.toLowerCase(), idx);
|
|
591
670
|
});
|
|
592
671
|
}
|
|
593
|
-
const response = await this.
|
|
672
|
+
const response = await this.graphqlWithSubIssues(PROJECT_ITEMS_QUERY, { projectId });
|
|
594
673
|
return response.node.items.nodes.filter((item) => item.content).map((item) => {
|
|
595
674
|
const content = item.content;
|
|
596
675
|
const fields = {};
|
|
@@ -624,6 +703,8 @@ var GitHubAPI = class {
|
|
|
624
703
|
state = "closed";
|
|
625
704
|
}
|
|
626
705
|
}
|
|
706
|
+
const parent = content.parent || null;
|
|
707
|
+
const subIssues = content.subIssues?.nodes || [];
|
|
627
708
|
return {
|
|
628
709
|
id: item.id,
|
|
629
710
|
title: content.title || "Untitled",
|
|
@@ -639,7 +720,9 @@ var GitHubAPI = class {
|
|
|
639
720
|
url: content.url || null,
|
|
640
721
|
projectId,
|
|
641
722
|
projectTitle,
|
|
642
|
-
fields
|
|
723
|
+
fields,
|
|
724
|
+
parent,
|
|
725
|
+
subIssues
|
|
643
726
|
};
|
|
644
727
|
});
|
|
645
728
|
}
|
|
@@ -1097,6 +1180,101 @@ var GitHubAPI = class {
|
|
|
1097
1180
|
return false;
|
|
1098
1181
|
}
|
|
1099
1182
|
}
|
|
1183
|
+
// =========================================================================
|
|
1184
|
+
// Sub-Issue / Parent-Child Relationships
|
|
1185
|
+
// =========================================================================
|
|
1186
|
+
/**
|
|
1187
|
+
* Get the node ID for an issue
|
|
1188
|
+
*/
|
|
1189
|
+
async getIssueNodeId(repo, issueNumber) {
|
|
1190
|
+
if (!this.graphqlWithAuth) throw new Error("Not authenticated");
|
|
1191
|
+
try {
|
|
1192
|
+
const response = await this.graphqlWithAuth(ISSUE_FOR_UPDATE_QUERY, {
|
|
1193
|
+
owner: repo.owner,
|
|
1194
|
+
name: repo.name,
|
|
1195
|
+
number: issueNumber
|
|
1196
|
+
});
|
|
1197
|
+
return response.repository.issue?.id ?? null;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Add a sub-issue to a parent issue
|
|
1204
|
+
* @param repo Repository info
|
|
1205
|
+
* @param parentNumber Parent issue number
|
|
1206
|
+
* @param childNumber Child issue number to add as sub-issue
|
|
1207
|
+
* @returns true if successful
|
|
1208
|
+
*/
|
|
1209
|
+
async addSubIssue(repo, parentNumber, childNumber) {
|
|
1210
|
+
if (!this.graphqlWithSubIssues) throw new Error("Not authenticated");
|
|
1211
|
+
try {
|
|
1212
|
+
const [parentId, childId] = await Promise.all([
|
|
1213
|
+
this.getIssueNodeId(repo, parentNumber),
|
|
1214
|
+
this.getIssueNodeId(repo, childNumber)
|
|
1215
|
+
]);
|
|
1216
|
+
if (!parentId || !childId) {
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
await this.graphqlWithSubIssues(ADD_SUB_ISSUE_MUTATION, {
|
|
1220
|
+
issueId: parentId,
|
|
1221
|
+
subIssueId: childId
|
|
1222
|
+
});
|
|
1223
|
+
return true;
|
|
1224
|
+
} catch {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Remove a sub-issue from its parent issue
|
|
1230
|
+
* @param repo Repository info
|
|
1231
|
+
* @param parentNumber Parent issue number
|
|
1232
|
+
* @param childNumber Child issue number to remove
|
|
1233
|
+
* @returns true if successful
|
|
1234
|
+
*/
|
|
1235
|
+
async removeSubIssue(repo, parentNumber, childNumber) {
|
|
1236
|
+
if (!this.graphqlWithSubIssues) throw new Error("Not authenticated");
|
|
1237
|
+
try {
|
|
1238
|
+
const [parentId, childId] = await Promise.all([
|
|
1239
|
+
this.getIssueNodeId(repo, parentNumber),
|
|
1240
|
+
this.getIssueNodeId(repo, childNumber)
|
|
1241
|
+
]);
|
|
1242
|
+
if (!parentId || !childId) {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
await this.graphqlWithSubIssues(REMOVE_SUB_ISSUE_MUTATION, {
|
|
1246
|
+
issueId: parentId,
|
|
1247
|
+
subIssueId: childId
|
|
1248
|
+
});
|
|
1249
|
+
return true;
|
|
1250
|
+
} catch {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Get issue relationships (parent and sub-issues)
|
|
1256
|
+
*/
|
|
1257
|
+
async getIssueRelationships(repo, issueNumber) {
|
|
1258
|
+
if (!this.graphqlWithSubIssues) throw new Error("Not authenticated");
|
|
1259
|
+
try {
|
|
1260
|
+
const response = await this.graphqlWithSubIssues(ISSUE_RELATIONSHIPS_QUERY, {
|
|
1261
|
+
owner: repo.owner,
|
|
1262
|
+
name: repo.name,
|
|
1263
|
+
number: issueNumber
|
|
1264
|
+
});
|
|
1265
|
+
const issue = response.repository.issue;
|
|
1266
|
+
if (!issue) return null;
|
|
1267
|
+
return {
|
|
1268
|
+
id: issue.id,
|
|
1269
|
+
number: issue.number,
|
|
1270
|
+
title: issue.title,
|
|
1271
|
+
parent: issue.parent,
|
|
1272
|
+
subIssues: issue.subIssues.nodes
|
|
1273
|
+
};
|
|
1274
|
+
} catch {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1100
1278
|
};
|
|
1101
1279
|
|
|
1102
1280
|
// src/branch-linker.ts
|
|
@@ -1406,13 +1584,13 @@ async function getDefaultBranch(options = {}) {
|
|
|
1406
1584
|
}
|
|
1407
1585
|
return "master";
|
|
1408
1586
|
}
|
|
1409
|
-
function validatePath(
|
|
1410
|
-
if (!
|
|
1587
|
+
function validatePath(path2) {
|
|
1588
|
+
if (!path2 || path2.trim().length === 0) {
|
|
1411
1589
|
throw new Error("Path cannot be empty");
|
|
1412
1590
|
}
|
|
1413
1591
|
const dangerousChars = /[`$;|&<>(){}[\]'"\n\r]/;
|
|
1414
|
-
if (dangerousChars.test(
|
|
1415
|
-
throw new Error(`Path contains invalid characters: ${
|
|
1592
|
+
if (dangerousChars.test(path2)) {
|
|
1593
|
+
throw new Error(`Path contains invalid characters: ${path2}`);
|
|
1416
1594
|
}
|
|
1417
1595
|
}
|
|
1418
1596
|
async function createWorktree(worktreePath, branch, options = {}) {
|
|
@@ -1652,58 +1830,1409 @@ function getDiffSummary(diff) {
|
|
|
1652
1830
|
}
|
|
1653
1831
|
return parts.join(", ");
|
|
1654
1832
|
}
|
|
1833
|
+
|
|
1834
|
+
// src/claude/client.ts
|
|
1835
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
1836
|
+
|
|
1837
|
+
// src/claude/prompts/pr-description.ts
|
|
1838
|
+
var PR_DESCRIPTION_PROMPT = `You are a helpful assistant that generates clear, concise pull request descriptions.
|
|
1839
|
+
|
|
1840
|
+
Given a git diff and optionally a linked issue and commit history, generate a PR description that:
|
|
1841
|
+
|
|
1842
|
+
1. Summarizes what the PR does in 1-2 sentences
|
|
1843
|
+
2. Lists the key changes made
|
|
1844
|
+
3. Notes any breaking changes or migration steps needed
|
|
1845
|
+
4. References the linked issue if provided (following project conventions)
|
|
1846
|
+
|
|
1847
|
+
Format the output as markdown suitable for a GitHub PR description.
|
|
1848
|
+
|
|
1849
|
+
Guidelines:
|
|
1850
|
+
- Be concise but complete
|
|
1851
|
+
- Focus on the "why" not just the "what"
|
|
1852
|
+
- Use bullet points for lists of changes
|
|
1853
|
+
- Highlight any important decisions or trade-offs
|
|
1854
|
+
- If there are UI changes, note that screenshots may be needed
|
|
1855
|
+
- IMPORTANT: Follow any project-specific conventions provided (e.g., "Relates to" vs "Closes")
|
|
1856
|
+
|
|
1857
|
+
Output format:
|
|
1858
|
+
## Summary
|
|
1859
|
+
[1-2 sentence summary]
|
|
1860
|
+
|
|
1861
|
+
## Changes
|
|
1862
|
+
- [Change 1]
|
|
1863
|
+
- [Change 2]
|
|
1864
|
+
...
|
|
1865
|
+
|
|
1866
|
+
## Notes
|
|
1867
|
+
[Any additional notes, breaking changes, or migration steps - omit if none]
|
|
1868
|
+
|
|
1869
|
+
[Issue reference following project conventions]
|
|
1870
|
+
`;
|
|
1871
|
+
function buildPRDescriptionSystemPrompt(conventions) {
|
|
1872
|
+
let prompt = PR_DESCRIPTION_PROMPT;
|
|
1873
|
+
if (conventions) {
|
|
1874
|
+
prompt += `
|
|
1875
|
+
|
|
1876
|
+
## Project Conventions
|
|
1877
|
+
${conventions}
|
|
1878
|
+
`;
|
|
1879
|
+
}
|
|
1880
|
+
return prompt;
|
|
1881
|
+
}
|
|
1882
|
+
function buildPRDescriptionUserPrompt(options) {
|
|
1883
|
+
let prompt = "## Git Diff\n```diff\n" + options.diff + "\n```\n\n";
|
|
1884
|
+
if (options.issue) {
|
|
1885
|
+
prompt += "## Linked Issue\n";
|
|
1886
|
+
prompt += `**#${options.issue.number}: ${options.issue.title}**
|
|
1887
|
+
|
|
1888
|
+
`;
|
|
1889
|
+
prompt += options.issue.body + "\n\n";
|
|
1890
|
+
}
|
|
1891
|
+
if (options.commits && options.commits.length > 0) {
|
|
1892
|
+
prompt += "## Recent Commits\n";
|
|
1893
|
+
for (const commit of options.commits) {
|
|
1894
|
+
prompt += `- ${commit}
|
|
1895
|
+
`;
|
|
1896
|
+
}
|
|
1897
|
+
prompt += "\n";
|
|
1898
|
+
}
|
|
1899
|
+
if (options.context) {
|
|
1900
|
+
prompt += "## Additional Context\n" + options.context + "\n";
|
|
1901
|
+
}
|
|
1902
|
+
return prompt;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/claude/prompts/plan-epic.ts
|
|
1906
|
+
var PLAN_EPIC_SYSTEM_PROMPT = `You are an expert software project planner. Your job is to break down epics (large features) into well-structured, actionable GitHub issues.
|
|
1907
|
+
|
|
1908
|
+
When planning an epic, you should:
|
|
1909
|
+
|
|
1910
|
+
1. **Analyze the Epic**: Understand the scope and goals of the feature
|
|
1911
|
+
2. **Identify Components**: Break it into logical sub-tasks
|
|
1912
|
+
3. **Create Issues**: Generate issues that are:
|
|
1913
|
+
- Small enough to complete in 1-3 days
|
|
1914
|
+
- Independent where possible (minimize blocking dependencies)
|
|
1915
|
+
- Clear with acceptance criteria
|
|
1916
|
+
- Properly labeled and categorized
|
|
1917
|
+
|
|
1918
|
+
4. **Structure Hierarchy**:
|
|
1919
|
+
- Create a parent epic issue first
|
|
1920
|
+
- Create child issues that link to the parent
|
|
1921
|
+
- Set up any blocking dependencies
|
|
1922
|
+
|
|
1923
|
+
Issue Structure Guidelines:
|
|
1924
|
+
- Title: Clear, action-oriented (e.g., "Add user authentication endpoint")
|
|
1925
|
+
- Body should include:
|
|
1926
|
+
- ## Overview (1-2 sentences)
|
|
1927
|
+
- ## Tasks (checkbox list of specific implementation tasks)
|
|
1928
|
+
- ## Acceptance Criteria (what "done" looks like)
|
|
1929
|
+
- ## Dependencies (if any)
|
|
1930
|
+
|
|
1931
|
+
When you have tools available, use them to actually create the issues.
|
|
1932
|
+
When you don't have tools, output a structured plan in markdown format.
|
|
1933
|
+
|
|
1934
|
+
Keep issues focused and atomic. It's better to have more small issues than fewer large ones.
|
|
1935
|
+
`;
|
|
1936
|
+
function buildPlanEpicUserPrompt(options) {
|
|
1937
|
+
let prompt = `# Epic to Plan
|
|
1938
|
+
|
|
1939
|
+
${options.title}
|
|
1940
|
+
|
|
1941
|
+
`;
|
|
1942
|
+
if (options.context) {
|
|
1943
|
+
prompt += `## Project Context
|
|
1944
|
+
${options.context}
|
|
1945
|
+
|
|
1946
|
+
`;
|
|
1947
|
+
}
|
|
1948
|
+
if (options.existingIssues && options.existingIssues.length > 0) {
|
|
1949
|
+
prompt += "## Existing Issues in Project\n";
|
|
1950
|
+
prompt += "Consider these existing issues to avoid duplication:\n\n";
|
|
1951
|
+
for (const issue of options.existingIssues.slice(0, 20)) {
|
|
1952
|
+
prompt += `- #${issue.number}: ${issue.title}
|
|
1953
|
+
`;
|
|
1954
|
+
}
|
|
1955
|
+
prompt += "\n";
|
|
1956
|
+
}
|
|
1957
|
+
prompt += `## Instructions
|
|
1958
|
+
`;
|
|
1959
|
+
prompt += `Break down this epic into actionable issues. `;
|
|
1960
|
+
prompt += `First create the parent epic issue, then create child issues for each component.
|
|
1961
|
+
|
|
1962
|
+
`;
|
|
1963
|
+
prompt += `Use the available tools to create issues if they are available, `;
|
|
1964
|
+
prompt += `otherwise output a structured plan in markdown.
|
|
1965
|
+
`;
|
|
1966
|
+
return prompt;
|
|
1967
|
+
}
|
|
1968
|
+
var EPIC_ISSUE_TEMPLATE = `## Overview
|
|
1969
|
+
|
|
1970
|
+
{overview}
|
|
1971
|
+
|
|
1972
|
+
## Sub-Issues
|
|
1973
|
+
|
|
1974
|
+
This epic will be broken down into the following issues:
|
|
1975
|
+
|
|
1976
|
+
{subissues}
|
|
1977
|
+
|
|
1978
|
+
## Acceptance Criteria
|
|
1979
|
+
|
|
1980
|
+
- [ ] All sub-issues completed
|
|
1981
|
+
- [ ] Integration tested
|
|
1982
|
+
- [ ] Documentation updated
|
|
1983
|
+
`;
|
|
1984
|
+
var CHILD_ISSUE_TEMPLATE = `## Overview
|
|
1985
|
+
|
|
1986
|
+
{overview}
|
|
1987
|
+
|
|
1988
|
+
Part of #{parent_number}
|
|
1989
|
+
|
|
1990
|
+
## Tasks
|
|
1991
|
+
|
|
1992
|
+
{tasks}
|
|
1993
|
+
|
|
1994
|
+
## Acceptance Criteria
|
|
1995
|
+
|
|
1996
|
+
{acceptance_criteria}
|
|
1997
|
+
|
|
1998
|
+
## Dependencies
|
|
1999
|
+
|
|
2000
|
+
{dependencies}
|
|
2001
|
+
`;
|
|
2002
|
+
|
|
2003
|
+
// src/claude/prompts/expand-issue.ts
|
|
2004
|
+
var EXPAND_ISSUE_PROMPT = `You are a helpful assistant that expands brief issue descriptions into well-structured GitHub issues.
|
|
2005
|
+
|
|
2006
|
+
Given a brief description, generate a complete issue with:
|
|
2007
|
+
1. A clear, action-oriented title
|
|
2008
|
+
2. A detailed description with context
|
|
2009
|
+
3. Acceptance criteria as a checklist
|
|
2010
|
+
4. Suggested labels (if applicable)
|
|
2011
|
+
|
|
2012
|
+
Output your response as JSON with this structure:
|
|
2013
|
+
{
|
|
2014
|
+
"title": "Clear action-oriented title",
|
|
2015
|
+
"body": "Full markdown body with ## sections",
|
|
2016
|
+
"labels": ["suggested", "labels"],
|
|
2017
|
+
"assignees": []
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
Guidelines for the body:
|
|
2021
|
+
- Start with a brief ## Overview section (1-2 sentences)
|
|
2022
|
+
- Include ## Acceptance Criteria with checkbox items
|
|
2023
|
+
- Add ## Technical Notes if there are implementation considerations
|
|
2024
|
+
- Keep it concise but complete
|
|
2025
|
+
- Use markdown formatting appropriately
|
|
2026
|
+
|
|
2027
|
+
Common labels to suggest based on content:
|
|
2028
|
+
- bug: for bug fixes
|
|
2029
|
+
- enhancement: for new features
|
|
2030
|
+
- documentation: for docs changes
|
|
2031
|
+
- refactor: for code cleanup
|
|
2032
|
+
- performance: for optimization work
|
|
2033
|
+
- security: for security-related issues
|
|
2034
|
+
- testing: for test-related issues
|
|
2035
|
+
|
|
2036
|
+
Only output the JSON, no additional text.
|
|
2037
|
+
`;
|
|
2038
|
+
function buildExpandIssueUserPrompt(options) {
|
|
2039
|
+
let prompt = `Brief: ${options.brief}
|
|
2040
|
+
|
|
2041
|
+
`;
|
|
2042
|
+
if (options.projectContext) {
|
|
2043
|
+
prompt += `Project Context:
|
|
2044
|
+
${options.projectContext}
|
|
2045
|
+
|
|
2046
|
+
`;
|
|
2047
|
+
}
|
|
2048
|
+
if (options.repoContext) {
|
|
2049
|
+
prompt += `Repository Context:
|
|
2050
|
+
${options.repoContext}
|
|
2051
|
+
|
|
2052
|
+
`;
|
|
2053
|
+
}
|
|
2054
|
+
if (options.existingLabels && options.existingLabels.length > 0) {
|
|
2055
|
+
prompt += `Available Labels:
|
|
2056
|
+
${options.existingLabels.join(", ")}
|
|
2057
|
+
|
|
2058
|
+
`;
|
|
2059
|
+
}
|
|
2060
|
+
return prompt;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// src/claude/tools.ts
|
|
2064
|
+
var CREATE_ISSUE_TOOL = {
|
|
2065
|
+
name: "create_issue",
|
|
2066
|
+
description: "Create a new GitHub issue in the repository. Returns the created issue number and URL.",
|
|
2067
|
+
input_schema: {
|
|
2068
|
+
type: "object",
|
|
2069
|
+
properties: {
|
|
2070
|
+
title: {
|
|
2071
|
+
type: "string",
|
|
2072
|
+
description: "The title of the issue. Should be clear and action-oriented."
|
|
2073
|
+
},
|
|
2074
|
+
body: {
|
|
2075
|
+
type: "string",
|
|
2076
|
+
description: "The body/description of the issue in markdown format."
|
|
2077
|
+
},
|
|
2078
|
+
labels: {
|
|
2079
|
+
type: "array",
|
|
2080
|
+
description: "Labels to apply to the issue.",
|
|
2081
|
+
items: { type: "string" }
|
|
2082
|
+
},
|
|
2083
|
+
assignees: {
|
|
2084
|
+
type: "array",
|
|
2085
|
+
description: "GitHub usernames to assign to the issue.",
|
|
2086
|
+
items: { type: "string" }
|
|
2087
|
+
}
|
|
2088
|
+
},
|
|
2089
|
+
required: ["title", "body"]
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
var SET_PARENT_TOOL = {
|
|
2093
|
+
name: "set_parent",
|
|
2094
|
+
description: "Set a parent-child relationship between two issues. The child issue will be linked to the parent epic.",
|
|
2095
|
+
input_schema: {
|
|
2096
|
+
type: "object",
|
|
2097
|
+
properties: {
|
|
2098
|
+
child_number: {
|
|
2099
|
+
type: "number",
|
|
2100
|
+
description: "The issue number of the child issue."
|
|
2101
|
+
},
|
|
2102
|
+
parent_number: {
|
|
2103
|
+
type: "number",
|
|
2104
|
+
description: "The issue number of the parent epic."
|
|
2105
|
+
}
|
|
2106
|
+
},
|
|
2107
|
+
required: ["child_number", "parent_number"]
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
var SET_FIELD_TOOL = {
|
|
2111
|
+
name: "set_field",
|
|
2112
|
+
description: "Set a project field value on an issue (e.g., Status, Priority, Size).",
|
|
2113
|
+
input_schema: {
|
|
2114
|
+
type: "object",
|
|
2115
|
+
properties: {
|
|
2116
|
+
issue_number: {
|
|
2117
|
+
type: "number",
|
|
2118
|
+
description: "The issue number to update."
|
|
2119
|
+
},
|
|
2120
|
+
field_name: {
|
|
2121
|
+
type: "string",
|
|
2122
|
+
description: 'The name of the field to set (e.g., "Status", "Priority").'
|
|
2123
|
+
},
|
|
2124
|
+
value: {
|
|
2125
|
+
type: "string",
|
|
2126
|
+
description: "The value to set for the field."
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
2129
|
+
required: ["issue_number", "field_name", "value"]
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
var ADD_BLOCKER_TOOL = {
|
|
2133
|
+
name: "add_blocker",
|
|
2134
|
+
description: "Add a blocking dependency between issues. The blocker must be completed before the blocked issue.",
|
|
2135
|
+
input_schema: {
|
|
2136
|
+
type: "object",
|
|
2137
|
+
properties: {
|
|
2138
|
+
blocked_issue: {
|
|
2139
|
+
type: "number",
|
|
2140
|
+
description: "The issue number that is blocked."
|
|
2141
|
+
},
|
|
2142
|
+
blocking_issue: {
|
|
2143
|
+
type: "number",
|
|
2144
|
+
description: "The issue number that is blocking."
|
|
2145
|
+
}
|
|
2146
|
+
},
|
|
2147
|
+
required: ["blocked_issue", "blocking_issue"]
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
var ADD_TO_PROJECT_TOOL = {
|
|
2151
|
+
name: "add_to_project",
|
|
2152
|
+
description: "Add an issue to a GitHub Project board.",
|
|
2153
|
+
input_schema: {
|
|
2154
|
+
type: "object",
|
|
2155
|
+
properties: {
|
|
2156
|
+
issue_number: {
|
|
2157
|
+
type: "number",
|
|
2158
|
+
description: "The issue number to add to the project."
|
|
2159
|
+
},
|
|
2160
|
+
project_name: {
|
|
2161
|
+
type: "string",
|
|
2162
|
+
description: "The name of the project to add the issue to."
|
|
2163
|
+
}
|
|
2164
|
+
},
|
|
2165
|
+
required: ["issue_number"]
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
var ADD_LABELS_TOOL = {
|
|
2169
|
+
name: "add_labels",
|
|
2170
|
+
description: "Add labels to an existing issue.",
|
|
2171
|
+
input_schema: {
|
|
2172
|
+
type: "object",
|
|
2173
|
+
properties: {
|
|
2174
|
+
issue_number: {
|
|
2175
|
+
type: "number",
|
|
2176
|
+
description: "The issue number to add labels to."
|
|
2177
|
+
},
|
|
2178
|
+
labels: {
|
|
2179
|
+
type: "array",
|
|
2180
|
+
description: "The labels to add.",
|
|
2181
|
+
items: { type: "string" }
|
|
2182
|
+
}
|
|
2183
|
+
},
|
|
2184
|
+
required: ["issue_number", "labels"]
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
var GHP_TOOLS = [
|
|
2188
|
+
CREATE_ISSUE_TOOL,
|
|
2189
|
+
SET_PARENT_TOOL,
|
|
2190
|
+
SET_FIELD_TOOL,
|
|
2191
|
+
ADD_BLOCKER_TOOL,
|
|
2192
|
+
ADD_TO_PROJECT_TOOL,
|
|
2193
|
+
ADD_LABELS_TOOL
|
|
2194
|
+
];
|
|
2195
|
+
function getTools(names) {
|
|
2196
|
+
return GHP_TOOLS.filter((tool) => names.includes(tool.name));
|
|
2197
|
+
}
|
|
2198
|
+
var TOOL_NAMES = {
|
|
2199
|
+
CREATE_ISSUE: "create_issue",
|
|
2200
|
+
SET_PARENT: "set_parent",
|
|
2201
|
+
SET_FIELD: "set_field",
|
|
2202
|
+
ADD_BLOCKER: "add_blocker",
|
|
2203
|
+
ADD_TO_PROJECT: "add_to_project",
|
|
2204
|
+
ADD_LABELS: "add_labels"
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
// src/claude/client.ts
|
|
2208
|
+
var DEFAULT_MODEL = "claude-sonnet-4-20250514";
|
|
2209
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
2210
|
+
var ClaudeClient = class {
|
|
2211
|
+
client = null;
|
|
2212
|
+
options;
|
|
2213
|
+
config;
|
|
2214
|
+
constructor(options) {
|
|
2215
|
+
this.options = options;
|
|
2216
|
+
this.config = {
|
|
2217
|
+
model: options.model || DEFAULT_MODEL,
|
|
2218
|
+
maxTokens: options.maxTokens || DEFAULT_MAX_TOKENS
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Initialize the client with an API key
|
|
2223
|
+
*/
|
|
2224
|
+
async ensureClient() {
|
|
2225
|
+
if (this.client) {
|
|
2226
|
+
return this.client;
|
|
2227
|
+
}
|
|
2228
|
+
let apiKey = this.options.apiKey;
|
|
2229
|
+
if (!apiKey && this.options.apiKeyProvider) {
|
|
2230
|
+
apiKey = await this.options.apiKeyProvider.getApiKey() ?? void 0;
|
|
2231
|
+
}
|
|
2232
|
+
if (!apiKey) {
|
|
2233
|
+
throw new Error(
|
|
2234
|
+
"No API key provided. Set ANTHROPIC_API_KEY environment variable, pass apiKey option, or provide an apiKeyProvider."
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
this.client = new import_sdk.default({ apiKey });
|
|
2238
|
+
return this.client;
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Get the current model configuration
|
|
2242
|
+
*/
|
|
2243
|
+
get model() {
|
|
2244
|
+
return this.config.model;
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Get the current max tokens configuration
|
|
2248
|
+
*/
|
|
2249
|
+
get maxTokens() {
|
|
2250
|
+
return this.config.maxTokens;
|
|
2251
|
+
}
|
|
2252
|
+
// =========================================================================
|
|
2253
|
+
// Low-Level API
|
|
2254
|
+
// =========================================================================
|
|
2255
|
+
/**
|
|
2256
|
+
* Send a message to Claude and get a response.
|
|
2257
|
+
* This is the low-level API for custom prompts.
|
|
2258
|
+
*/
|
|
2259
|
+
async complete(options) {
|
|
2260
|
+
const client = await this.ensureClient();
|
|
2261
|
+
const maxTokens = options.maxTokens || this.config.maxTokens;
|
|
2262
|
+
const messages = options.messages.map((m) => ({
|
|
2263
|
+
role: m.role,
|
|
2264
|
+
content: m.content
|
|
2265
|
+
}));
|
|
2266
|
+
let text = "";
|
|
2267
|
+
const toolCalls = [];
|
|
2268
|
+
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
2269
|
+
let stopReason = "end_turn";
|
|
2270
|
+
if (options.callbacks?.onToken || options.callbacks?.onToolCall) {
|
|
2271
|
+
const stream = await client.messages.stream({
|
|
2272
|
+
model: this.config.model,
|
|
2273
|
+
max_tokens: maxTokens,
|
|
2274
|
+
system: options.system,
|
|
2275
|
+
messages,
|
|
2276
|
+
tools: options.tools
|
|
2277
|
+
});
|
|
2278
|
+
for await (const event of stream) {
|
|
2279
|
+
if (event.type === "content_block_delta") {
|
|
2280
|
+
const delta = event.delta;
|
|
2281
|
+
if ("text" in delta && delta.text) {
|
|
2282
|
+
text += delta.text;
|
|
2283
|
+
options.callbacks?.onToken?.(delta.text);
|
|
2284
|
+
}
|
|
2285
|
+
} else if (event.type === "message_stop") {
|
|
2286
|
+
} else if (event.type === "message_delta") {
|
|
2287
|
+
if (event.usage) {
|
|
2288
|
+
usage.output_tokens = event.usage.output_tokens;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const finalMessage = await stream.finalMessage();
|
|
2293
|
+
usage.input_tokens = finalMessage.usage.input_tokens;
|
|
2294
|
+
usage.output_tokens = finalMessage.usage.output_tokens;
|
|
2295
|
+
usage.total_tokens = usage.input_tokens + usage.output_tokens;
|
|
2296
|
+
stopReason = finalMessage.stop_reason;
|
|
2297
|
+
for (const block of finalMessage.content) {
|
|
2298
|
+
if (block.type === "tool_use") {
|
|
2299
|
+
toolCalls.push({
|
|
2300
|
+
name: block.name,
|
|
2301
|
+
input: block.input
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
} else {
|
|
2306
|
+
const response = await client.messages.create({
|
|
2307
|
+
model: this.config.model,
|
|
2308
|
+
max_tokens: maxTokens,
|
|
2309
|
+
system: options.system,
|
|
2310
|
+
messages,
|
|
2311
|
+
tools: options.tools
|
|
2312
|
+
});
|
|
2313
|
+
usage = {
|
|
2314
|
+
input_tokens: response.usage.input_tokens,
|
|
2315
|
+
output_tokens: response.usage.output_tokens,
|
|
2316
|
+
total_tokens: response.usage.input_tokens + response.usage.output_tokens
|
|
2317
|
+
};
|
|
2318
|
+
stopReason = response.stop_reason;
|
|
2319
|
+
for (const block of response.content) {
|
|
2320
|
+
if (block.type === "text") {
|
|
2321
|
+
text += block.text;
|
|
2322
|
+
} else if (block.type === "tool_use") {
|
|
2323
|
+
toolCalls.push({
|
|
2324
|
+
name: block.name,
|
|
2325
|
+
input: block.input
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (toolCalls.length > 0 && options.toolHandlers) {
|
|
2331
|
+
for (const toolCall of toolCalls) {
|
|
2332
|
+
const handler = options.toolHandlers[toolCall.name];
|
|
2333
|
+
if (handler) {
|
|
2334
|
+
options.callbacks?.onToolCall?.(toolCall.name, toolCall.input);
|
|
2335
|
+
try {
|
|
2336
|
+
const result = await handler(toolCall.input, options.toolContext || {});
|
|
2337
|
+
toolCall.result = result;
|
|
2338
|
+
options.callbacks?.onToolResult?.(toolCall.name, result);
|
|
2339
|
+
} catch (error) {
|
|
2340
|
+
toolCall.result = { error: error instanceof Error ? error.message : "Unknown error" };
|
|
2341
|
+
options.callbacks?.onToolResult?.(toolCall.name, toolCall.result);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return { text, toolCalls, usage, stopReason };
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Run a multi-turn conversation with tool use until completion.
|
|
2350
|
+
* Handles the tool use loop automatically.
|
|
2351
|
+
*/
|
|
2352
|
+
async runWithTools(options) {
|
|
2353
|
+
const maxTurns = options.maxTurns || 10;
|
|
2354
|
+
const messages = [
|
|
2355
|
+
{ role: "user", content: options.initialMessage }
|
|
2356
|
+
];
|
|
2357
|
+
let totalUsage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
2358
|
+
const allToolCalls = [];
|
|
2359
|
+
let finalText = "";
|
|
2360
|
+
let turn = 0;
|
|
2361
|
+
while (turn < maxTurns) {
|
|
2362
|
+
turn++;
|
|
2363
|
+
const result = await this.complete({
|
|
2364
|
+
system: options.system,
|
|
2365
|
+
messages,
|
|
2366
|
+
tools: options.tools,
|
|
2367
|
+
toolHandlers: options.toolHandlers,
|
|
2368
|
+
toolContext: options.toolContext,
|
|
2369
|
+
maxTokens: options.maxTokens,
|
|
2370
|
+
callbacks: options.callbacks
|
|
2371
|
+
});
|
|
2372
|
+
totalUsage.input_tokens += result.usage.input_tokens;
|
|
2373
|
+
totalUsage.output_tokens += result.usage.output_tokens;
|
|
2374
|
+
totalUsage.total_tokens += result.usage.total_tokens;
|
|
2375
|
+
if (result.toolCalls.length === 0 || result.stopReason !== "tool_use") {
|
|
2376
|
+
finalText = result.text;
|
|
2377
|
+
break;
|
|
2378
|
+
}
|
|
2379
|
+
const assistantContent = [];
|
|
2380
|
+
if (result.text) {
|
|
2381
|
+
assistantContent.push({ type: "text", text: result.text });
|
|
2382
|
+
}
|
|
2383
|
+
for (const tc of result.toolCalls) {
|
|
2384
|
+
assistantContent.push({
|
|
2385
|
+
type: "tool_use",
|
|
2386
|
+
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
2387
|
+
name: tc.name,
|
|
2388
|
+
input: tc.input
|
|
2389
|
+
});
|
|
2390
|
+
allToolCalls.push(tc);
|
|
2391
|
+
}
|
|
2392
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
2393
|
+
const toolResults = result.toolCalls.map((tc, i) => ({
|
|
2394
|
+
type: "tool_result",
|
|
2395
|
+
tool_use_id: assistantContent[assistantContent.length - result.toolCalls.length + i].id,
|
|
2396
|
+
content: JSON.stringify(tc.result ?? { success: true })
|
|
2397
|
+
}));
|
|
2398
|
+
messages.push({ role: "user", content: toolResults });
|
|
2399
|
+
}
|
|
2400
|
+
return {
|
|
2401
|
+
text: finalText,
|
|
2402
|
+
toolCalls: allToolCalls,
|
|
2403
|
+
usage: totalUsage,
|
|
2404
|
+
stopReason: "end_turn"
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
// =========================================================================
|
|
2408
|
+
// High-Level API
|
|
2409
|
+
// =========================================================================
|
|
2410
|
+
/**
|
|
2411
|
+
* Generate a PR description from a diff and optional issue context.
|
|
2412
|
+
*/
|
|
2413
|
+
async generatePRDescription(options) {
|
|
2414
|
+
const userMessage = this.buildPRDescriptionPrompt(options);
|
|
2415
|
+
const systemPrompt = buildPRDescriptionSystemPrompt(options.conventions);
|
|
2416
|
+
const result = await this.complete({
|
|
2417
|
+
system: systemPrompt,
|
|
2418
|
+
messages: [{ role: "user", content: userMessage }]
|
|
2419
|
+
});
|
|
2420
|
+
return result.text;
|
|
2421
|
+
}
|
|
2422
|
+
buildPRDescriptionPrompt(options) {
|
|
2423
|
+
let prompt = "## Git Diff\n```diff\n" + options.diff + "\n```\n\n";
|
|
2424
|
+
if (options.issue) {
|
|
2425
|
+
prompt += "## Linked Issue\n";
|
|
2426
|
+
prompt += `**#${options.issue.number}: ${options.issue.title}**
|
|
2427
|
+
|
|
2428
|
+
`;
|
|
2429
|
+
prompt += options.issue.body + "\n\n";
|
|
2430
|
+
}
|
|
2431
|
+
if (options.feedback) {
|
|
2432
|
+
prompt += "## User Feedback\n";
|
|
2433
|
+
prompt += "Please regenerate the PR description taking this feedback into account:\n";
|
|
2434
|
+
prompt += options.feedback + "\n\n";
|
|
2435
|
+
}
|
|
2436
|
+
if (options.commits && options.commits.length > 0) {
|
|
2437
|
+
prompt += "## Recent Commits\n";
|
|
2438
|
+
for (const commit of options.commits) {
|
|
2439
|
+
prompt += `- ${commit}
|
|
2440
|
+
`;
|
|
2441
|
+
}
|
|
2442
|
+
prompt += "\n";
|
|
2443
|
+
}
|
|
2444
|
+
if (options.context) {
|
|
2445
|
+
prompt += "## Additional Context\n" + options.context + "\n";
|
|
2446
|
+
}
|
|
2447
|
+
return prompt;
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Plan an epic by breaking it down into issues.
|
|
2451
|
+
* Can use tools to actually create the issues.
|
|
2452
|
+
*/
|
|
2453
|
+
async planEpic(options) {
|
|
2454
|
+
const userMessage = buildPlanEpicUserPrompt(options);
|
|
2455
|
+
const createdIssues = [];
|
|
2456
|
+
const wrappedHandlers = {};
|
|
2457
|
+
if (options.tools) {
|
|
2458
|
+
for (const [name, handler] of Object.entries(options.tools)) {
|
|
2459
|
+
wrappedHandlers[name] = async (input, context) => {
|
|
2460
|
+
const result2 = await handler(input, context);
|
|
2461
|
+
if (name === "create_issue" && result2 && typeof result2 === "object") {
|
|
2462
|
+
const issueResult = result2;
|
|
2463
|
+
if (issueResult.number) {
|
|
2464
|
+
createdIssues.push({
|
|
2465
|
+
number: issueResult.number,
|
|
2466
|
+
title: issueResult.title || input.title
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return result2;
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
const result = await this.runWithTools({
|
|
2475
|
+
system: PLAN_EPIC_SYSTEM_PROMPT,
|
|
2476
|
+
initialMessage: userMessage,
|
|
2477
|
+
tools: GHP_TOOLS,
|
|
2478
|
+
toolHandlers: wrappedHandlers,
|
|
2479
|
+
toolContext: options.tools ? {} : void 0,
|
|
2480
|
+
callbacks: options.callbacks
|
|
2481
|
+
});
|
|
2482
|
+
return {
|
|
2483
|
+
...result,
|
|
2484
|
+
createdIssues
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Expand a brief issue description into a full issue with acceptance criteria.
|
|
2489
|
+
*/
|
|
2490
|
+
async expandIssue(options) {
|
|
2491
|
+
let prompt = `Brief: ${options.brief}
|
|
2492
|
+
|
|
2493
|
+
`;
|
|
2494
|
+
if (options.projectContext) {
|
|
2495
|
+
prompt += `Project Context:
|
|
2496
|
+
${options.projectContext}
|
|
2497
|
+
|
|
2498
|
+
`;
|
|
2499
|
+
}
|
|
2500
|
+
if (options.repoContext) {
|
|
2501
|
+
prompt += `Repository Context:
|
|
2502
|
+
${options.repoContext}
|
|
2503
|
+
|
|
2504
|
+
`;
|
|
2505
|
+
}
|
|
2506
|
+
const result = await this.complete({
|
|
2507
|
+
system: EXPAND_ISSUE_PROMPT,
|
|
2508
|
+
messages: [{ role: "user", content: prompt }]
|
|
2509
|
+
});
|
|
2510
|
+
try {
|
|
2511
|
+
const parsed = JSON.parse(result.text);
|
|
2512
|
+
return {
|
|
2513
|
+
title: parsed.title || options.brief,
|
|
2514
|
+
body: parsed.body || result.text,
|
|
2515
|
+
suggestedLabels: parsed.labels,
|
|
2516
|
+
suggestedAssignees: parsed.assignees
|
|
2517
|
+
};
|
|
2518
|
+
} catch {
|
|
2519
|
+
return {
|
|
2520
|
+
title: options.brief,
|
|
2521
|
+
body: result.text
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
|
|
2527
|
+
// src/claude/prompts/index.ts
|
|
2528
|
+
var prompts_exports = {};
|
|
2529
|
+
__export(prompts_exports, {
|
|
2530
|
+
CHILD_ISSUE_TEMPLATE: () => CHILD_ISSUE_TEMPLATE,
|
|
2531
|
+
EPIC_ISSUE_TEMPLATE: () => EPIC_ISSUE_TEMPLATE,
|
|
2532
|
+
EXPAND_ISSUE_PROMPT: () => EXPAND_ISSUE_PROMPT,
|
|
2533
|
+
PLAN_EPIC_SYSTEM_PROMPT: () => PLAN_EPIC_SYSTEM_PROMPT,
|
|
2534
|
+
PR_DESCRIPTION_PROMPT: () => PR_DESCRIPTION_PROMPT,
|
|
2535
|
+
buildExpandIssueUserPrompt: () => buildExpandIssueUserPrompt,
|
|
2536
|
+
buildPRDescriptionSystemPrompt: () => buildPRDescriptionSystemPrompt,
|
|
2537
|
+
buildPRDescriptionUserPrompt: () => buildPRDescriptionUserPrompt,
|
|
2538
|
+
buildPlanEpicUserPrompt: () => buildPlanEpicUserPrompt
|
|
2539
|
+
});
|
|
2540
|
+
|
|
2541
|
+
// src/conventions.ts
|
|
2542
|
+
var import_fs = require("fs");
|
|
2543
|
+
var import_path = require("path");
|
|
2544
|
+
function extractIssueReferenceConvention(content) {
|
|
2545
|
+
const lowerContent = content.toLowerCase();
|
|
2546
|
+
if (lowerContent.includes("relates to") && lowerContent.includes("not") && (lowerContent.includes("closes") || lowerContent.includes("fixes"))) {
|
|
2547
|
+
return "relates";
|
|
2548
|
+
}
|
|
2549
|
+
if (lowerContent.includes('use "closes"') || lowerContent.includes("use 'closes'")) {
|
|
2550
|
+
return "closes";
|
|
2551
|
+
}
|
|
2552
|
+
if (lowerContent.includes('use "fixes"') || lowerContent.includes("use 'fixes'")) {
|
|
2553
|
+
return "fixes";
|
|
2554
|
+
}
|
|
2555
|
+
const relatesMatch = content.match(/relates to #\d+/i);
|
|
2556
|
+
const closesMatch = content.match(/closes #\d+/i);
|
|
2557
|
+
const fixesMatch = content.match(/fixes #\d+/i);
|
|
2558
|
+
if (relatesMatch && !closesMatch && !fixesMatch) {
|
|
2559
|
+
return "relates";
|
|
2560
|
+
}
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
function extractCoAuthorFormat(content) {
|
|
2564
|
+
const match = content.match(/Co-Authored-By:\s*[^\n]+/i);
|
|
2565
|
+
if (match) {
|
|
2566
|
+
return match[0];
|
|
2567
|
+
}
|
|
2568
|
+
return null;
|
|
2569
|
+
}
|
|
2570
|
+
function loadProjectConventions(repoRoot) {
|
|
2571
|
+
let claudeMd = null;
|
|
2572
|
+
try {
|
|
2573
|
+
const claudeMdPath = (0, import_path.join)(repoRoot, "CLAUDE.md");
|
|
2574
|
+
if ((0, import_fs.existsSync)(claudeMdPath)) {
|
|
2575
|
+
claudeMd = (0, import_fs.readFileSync)(claudeMdPath, "utf-8");
|
|
2576
|
+
}
|
|
2577
|
+
} catch {
|
|
2578
|
+
}
|
|
2579
|
+
const conventions = {
|
|
2580
|
+
claudeMd,
|
|
2581
|
+
pr: {
|
|
2582
|
+
issueReference: null,
|
|
2583
|
+
coAuthorFormat: null,
|
|
2584
|
+
titleFormat: null,
|
|
2585
|
+
bodyFormat: null
|
|
2586
|
+
},
|
|
2587
|
+
commit: {
|
|
2588
|
+
format: null,
|
|
2589
|
+
coAuthorFormat: null
|
|
2590
|
+
},
|
|
2591
|
+
issue: {
|
|
2592
|
+
titleFormat: null,
|
|
2593
|
+
bodyStructure: null
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
if (claudeMd) {
|
|
2597
|
+
conventions.pr.issueReference = extractIssueReferenceConvention(claudeMd);
|
|
2598
|
+
conventions.pr.coAuthorFormat = extractCoAuthorFormat(claudeMd);
|
|
2599
|
+
conventions.commit.coAuthorFormat = conventions.pr.coAuthorFormat;
|
|
2600
|
+
const commitSection = claudeMd.match(/## Commit Messages?\s*\n([\s\S]*?)(?=\n##|\n$|$)/i);
|
|
2601
|
+
if (commitSection) {
|
|
2602
|
+
conventions.commit.format = commitSection[1].trim();
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
return conventions;
|
|
2606
|
+
}
|
|
2607
|
+
function buildConventionsContext(conventions) {
|
|
2608
|
+
const parts = [];
|
|
2609
|
+
if (conventions.pr.issueReference) {
|
|
2610
|
+
const refMap = {
|
|
2611
|
+
"relates": 'Use "Relates to #XX" (not "Closes" or "Fixes") unless the PR actually closes the issue',
|
|
2612
|
+
"closes": 'Use "Closes #XX" to reference issues',
|
|
2613
|
+
"fixes": 'Use "Fixes #XX" to reference issues'
|
|
2614
|
+
};
|
|
2615
|
+
parts.push(`Issue References: ${refMap[conventions.pr.issueReference]}`);
|
|
2616
|
+
}
|
|
2617
|
+
if (conventions.pr.coAuthorFormat) {
|
|
2618
|
+
parts.push(`Co-Author Format: ${conventions.pr.coAuthorFormat}`);
|
|
2619
|
+
}
|
|
2620
|
+
if (conventions.commit.format) {
|
|
2621
|
+
parts.push(`Commit Format:
|
|
2622
|
+
${conventions.commit.format}`);
|
|
2623
|
+
}
|
|
2624
|
+
if (parts.length === 0 && conventions.claudeMd) {
|
|
2625
|
+
const relevantSections = conventions.claudeMd.split("\n").filter(
|
|
2626
|
+
(line) => line.toLowerCase().includes("commit") || line.toLowerCase().includes("pr") || line.toLowerCase().includes("pull request") || line.toLowerCase().includes("issue") || line.toLowerCase().includes("relates") || line.toLowerCase().includes("closes")
|
|
2627
|
+
).slice(0, 20).join("\n");
|
|
2628
|
+
if (relevantSections) {
|
|
2629
|
+
parts.push(`Project Guidelines:
|
|
2630
|
+
${relevantSections}`);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return parts.join("\n\n");
|
|
2634
|
+
}
|
|
2635
|
+
function getIssueReferenceText(issueNumber, conventions, actuallyCloses = false) {
|
|
2636
|
+
if (actuallyCloses) {
|
|
2637
|
+
if (conventions.pr.issueReference === "fixes") {
|
|
2638
|
+
return `Fixes #${issueNumber}`;
|
|
2639
|
+
}
|
|
2640
|
+
return `Closes #${issueNumber}`;
|
|
2641
|
+
}
|
|
2642
|
+
switch (conventions.pr.issueReference) {
|
|
2643
|
+
case "relates":
|
|
2644
|
+
return `Relates to #${issueNumber}`;
|
|
2645
|
+
case "closes":
|
|
2646
|
+
return `Closes #${issueNumber}`;
|
|
2647
|
+
case "fixes":
|
|
2648
|
+
return `Fixes #${issueNumber}`;
|
|
2649
|
+
default:
|
|
2650
|
+
return `Relates to #${issueNumber}`;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
// src/agents/registry.ts
|
|
2655
|
+
var import_fs2 = require("fs");
|
|
2656
|
+
var import_os2 = require("os");
|
|
2657
|
+
var import_path2 = require("path");
|
|
2658
|
+
var import_crypto = require("crypto");
|
|
2659
|
+
var REGISTRY_VERSION = 1;
|
|
2660
|
+
function getRegistryPath() {
|
|
2661
|
+
return (0, import_path2.join)((0, import_os2.homedir)(), ".ghp", "agents.json");
|
|
2662
|
+
}
|
|
2663
|
+
function loadRegistry() {
|
|
2664
|
+
const path2 = getRegistryPath();
|
|
2665
|
+
if (!(0, import_fs2.existsSync)(path2)) {
|
|
2666
|
+
return {
|
|
2667
|
+
version: REGISTRY_VERSION,
|
|
2668
|
+
agents: {},
|
|
2669
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
try {
|
|
2673
|
+
const content = (0, import_fs2.readFileSync)(path2, "utf-8");
|
|
2674
|
+
const registry = JSON.parse(content);
|
|
2675
|
+
if (registry.version !== REGISTRY_VERSION) {
|
|
2676
|
+
registry.version = REGISTRY_VERSION;
|
|
2677
|
+
}
|
|
2678
|
+
return registry;
|
|
2679
|
+
} catch (error) {
|
|
2680
|
+
console.error("Warning: Could not parse agents.json, starting fresh");
|
|
2681
|
+
return {
|
|
2682
|
+
version: REGISTRY_VERSION,
|
|
2683
|
+
agents: {},
|
|
2684
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
function saveRegistry(registry) {
|
|
2689
|
+
const path2 = getRegistryPath();
|
|
2690
|
+
const dir = (0, import_path2.dirname)(path2);
|
|
2691
|
+
if (!(0, import_fs2.existsSync)(dir)) {
|
|
2692
|
+
(0, import_fs2.mkdirSync)(dir, { recursive: true });
|
|
2693
|
+
}
|
|
2694
|
+
registry.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2695
|
+
(0, import_fs2.writeFileSync)(path2, JSON.stringify(registry, null, 2));
|
|
2696
|
+
}
|
|
2697
|
+
function registerAgent(options) {
|
|
2698
|
+
const registry = loadRegistry();
|
|
2699
|
+
const agent = {
|
|
2700
|
+
id: (0, import_crypto.randomUUID)(),
|
|
2701
|
+
issueNumber: options.issueNumber,
|
|
2702
|
+
issueTitle: options.issueTitle,
|
|
2703
|
+
pid: options.pid,
|
|
2704
|
+
port: options.port,
|
|
2705
|
+
worktreePath: options.worktreePath,
|
|
2706
|
+
branch: options.branch,
|
|
2707
|
+
status: "starting",
|
|
2708
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2709
|
+
};
|
|
2710
|
+
registry.agents[agent.id] = agent;
|
|
2711
|
+
saveRegistry(registry);
|
|
2712
|
+
return agent;
|
|
2713
|
+
}
|
|
2714
|
+
function updateAgent(id, options) {
|
|
2715
|
+
const registry = loadRegistry();
|
|
2716
|
+
const agent = registry.agents[id];
|
|
2717
|
+
if (!agent) {
|
|
2718
|
+
return null;
|
|
2719
|
+
}
|
|
2720
|
+
if (options.status !== void 0) {
|
|
2721
|
+
agent.status = options.status;
|
|
2722
|
+
}
|
|
2723
|
+
if (options.port !== void 0) {
|
|
2724
|
+
agent.port = options.port;
|
|
2725
|
+
}
|
|
2726
|
+
if (options.error !== void 0) {
|
|
2727
|
+
agent.error = options.error;
|
|
2728
|
+
}
|
|
2729
|
+
if (options.currentAction !== void 0) {
|
|
2730
|
+
agent.currentAction = options.currentAction;
|
|
2731
|
+
}
|
|
2732
|
+
if (options.waitingForInput !== void 0) {
|
|
2733
|
+
agent.waitingForInput = options.waitingForInput;
|
|
2734
|
+
}
|
|
2735
|
+
agent.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
2736
|
+
saveRegistry(registry);
|
|
2737
|
+
return agent;
|
|
2738
|
+
}
|
|
2739
|
+
function unregisterAgent(id) {
|
|
2740
|
+
const registry = loadRegistry();
|
|
2741
|
+
if (!registry.agents[id]) {
|
|
2742
|
+
return false;
|
|
2743
|
+
}
|
|
2744
|
+
delete registry.agents[id];
|
|
2745
|
+
saveRegistry(registry);
|
|
2746
|
+
return true;
|
|
2747
|
+
}
|
|
2748
|
+
function getAgent(id) {
|
|
2749
|
+
const registry = loadRegistry();
|
|
2750
|
+
return registry.agents[id] || null;
|
|
2751
|
+
}
|
|
2752
|
+
function getAgentByIssue(issueNumber) {
|
|
2753
|
+
const registry = loadRegistry();
|
|
2754
|
+
for (const agent of Object.values(registry.agents)) {
|
|
2755
|
+
if (agent.issueNumber === issueNumber) {
|
|
2756
|
+
return agent;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
return null;
|
|
2760
|
+
}
|
|
2761
|
+
function listAgents() {
|
|
2762
|
+
const registry = loadRegistry();
|
|
2763
|
+
return Object.values(registry.agents);
|
|
2764
|
+
}
|
|
2765
|
+
function formatUptime(startedAt) {
|
|
2766
|
+
const start = new Date(startedAt).getTime();
|
|
2767
|
+
const now = Date.now();
|
|
2768
|
+
const diffMs = now - start;
|
|
2769
|
+
const seconds = Math.floor(diffMs / 1e3);
|
|
2770
|
+
const minutes = Math.floor(seconds / 60);
|
|
2771
|
+
const hours = Math.floor(minutes / 60);
|
|
2772
|
+
const days = Math.floor(hours / 24);
|
|
2773
|
+
if (days > 0) {
|
|
2774
|
+
return `${days}d ${hours % 24}h`;
|
|
2775
|
+
}
|
|
2776
|
+
if (hours > 0) {
|
|
2777
|
+
return `${hours}h ${minutes % 60}m`;
|
|
2778
|
+
}
|
|
2779
|
+
if (minutes > 0) {
|
|
2780
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
2781
|
+
}
|
|
2782
|
+
return `${seconds}s`;
|
|
2783
|
+
}
|
|
2784
|
+
function getAgentSummaries() {
|
|
2785
|
+
const agents = listAgents();
|
|
2786
|
+
return agents.map((agent) => ({
|
|
2787
|
+
id: agent.id,
|
|
2788
|
+
issueNumber: agent.issueNumber,
|
|
2789
|
+
issueTitle: agent.issueTitle,
|
|
2790
|
+
status: agent.status,
|
|
2791
|
+
port: agent.port,
|
|
2792
|
+
branch: agent.branch,
|
|
2793
|
+
uptime: formatUptime(agent.startedAt),
|
|
2794
|
+
currentAction: agent.currentAction,
|
|
2795
|
+
waitingForInput: agent.waitingForInput
|
|
2796
|
+
}));
|
|
2797
|
+
}
|
|
2798
|
+
function cleanupStaleAgents() {
|
|
2799
|
+
return 0;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/agents/session-watcher.ts
|
|
2803
|
+
var fs = __toESM(require("fs"), 1);
|
|
2804
|
+
var path = __toESM(require("path"), 1);
|
|
2805
|
+
var os = __toESM(require("os"), 1);
|
|
2806
|
+
var import_events = require("events");
|
|
2807
|
+
var import_child_process2 = require("child_process");
|
|
2808
|
+
var import_util2 = require("util");
|
|
2809
|
+
var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
|
|
2810
|
+
function pathToClaudeProjectName(dirPath) {
|
|
2811
|
+
return dirPath.replace(/\//g, "-").replace(/-\./g, "--");
|
|
2812
|
+
}
|
|
2813
|
+
async function findSessionFile(worktreePath) {
|
|
2814
|
+
const claudeProjectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
2815
|
+
const projectName = pathToClaudeProjectName(worktreePath);
|
|
2816
|
+
const projectDir = path.join(claudeProjectsDir, projectName);
|
|
2817
|
+
try {
|
|
2818
|
+
const files = await fs.promises.readdir(projectDir);
|
|
2819
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
2820
|
+
if (jsonlFiles.length === 0) return null;
|
|
2821
|
+
let mostRecent = null;
|
|
2822
|
+
for (const file of jsonlFiles) {
|
|
2823
|
+
const filePath = path.join(projectDir, file);
|
|
2824
|
+
const stat = await fs.promises.stat(filePath);
|
|
2825
|
+
if (!mostRecent || stat.mtimeMs > mostRecent.mtime) {
|
|
2826
|
+
mostRecent = { file: filePath, mtime: stat.mtimeMs };
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
return mostRecent?.file || null;
|
|
2830
|
+
} catch {
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
function parseSessionLine(line) {
|
|
2835
|
+
try {
|
|
2836
|
+
const entry = JSON.parse(line);
|
|
2837
|
+
const timestamp = new Date(entry.timestamp || Date.now());
|
|
2838
|
+
if (entry.type === "progress" && entry.data?.message) {
|
|
2839
|
+
const innerMessage = entry.data.message;
|
|
2840
|
+
const messageType = innerMessage.type;
|
|
2841
|
+
const messageContent = innerMessage.message?.content;
|
|
2842
|
+
if (messageType === "assistant" && messageContent) {
|
|
2843
|
+
for (const block of messageContent) {
|
|
2844
|
+
if (block.type === "tool_use") {
|
|
2845
|
+
return {
|
|
2846
|
+
type: "tool_start",
|
|
2847
|
+
timestamp,
|
|
2848
|
+
toolName: block.name,
|
|
2849
|
+
toolInput: block.input
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
if (block.type === "text" && block.text) {
|
|
2853
|
+
return {
|
|
2854
|
+
type: "text",
|
|
2855
|
+
timestamp,
|
|
2856
|
+
text: block.text
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
if (block.type === "thinking") {
|
|
2860
|
+
return {
|
|
2861
|
+
type: "thinking",
|
|
2862
|
+
timestamp,
|
|
2863
|
+
text: block.thinking
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
if (messageType === "user" && messageContent) {
|
|
2869
|
+
for (const block of messageContent) {
|
|
2870
|
+
if (block.type === "tool_result") {
|
|
2871
|
+
if (typeof block.content === "string" && block.content.includes("rejected")) {
|
|
2872
|
+
return {
|
|
2873
|
+
type: "user_input",
|
|
2874
|
+
timestamp,
|
|
2875
|
+
interrupted: true
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
return {
|
|
2879
|
+
type: "tool_end",
|
|
2880
|
+
timestamp,
|
|
2881
|
+
interrupted: false
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
if (entry.type === "assistant" && entry.message?.content) {
|
|
2888
|
+
for (const block of entry.message.content) {
|
|
2889
|
+
if (block.type === "tool_use") {
|
|
2890
|
+
return {
|
|
2891
|
+
type: "tool_start",
|
|
2892
|
+
timestamp,
|
|
2893
|
+
toolName: block.name,
|
|
2894
|
+
toolInput: block.input
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
if (block.type === "text" && block.text) {
|
|
2898
|
+
return {
|
|
2899
|
+
type: "text",
|
|
2900
|
+
timestamp,
|
|
2901
|
+
text: block.text
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
if (block.type === "thinking") {
|
|
2905
|
+
return {
|
|
2906
|
+
type: "thinking",
|
|
2907
|
+
timestamp,
|
|
2908
|
+
text: block.thinking
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
if (entry.type === "user" && entry.toolUseResult !== void 0) {
|
|
2914
|
+
return {
|
|
2915
|
+
type: "tool_end",
|
|
2916
|
+
timestamp,
|
|
2917
|
+
interrupted: entry.toolUseResult?.interrupted === true,
|
|
2918
|
+
error: entry.toolUseResult?.is_error ? "Tool execution failed" : void 0
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
if (entry.type === "user" && entry.message?.content) {
|
|
2922
|
+
const content = entry.message.content;
|
|
2923
|
+
for (const block of content) {
|
|
2924
|
+
if (block.type === "tool_result" && block.content?.includes?.("rejected")) {
|
|
2925
|
+
return {
|
|
2926
|
+
type: "user_input",
|
|
2927
|
+
timestamp,
|
|
2928
|
+
interrupted: true
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return null;
|
|
2934
|
+
} catch {
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
function formatAction(toolName, toolInput) {
|
|
2939
|
+
switch (toolName) {
|
|
2940
|
+
case "Read":
|
|
2941
|
+
return `Reading ${toolInput?.file_path || "file"}`;
|
|
2942
|
+
case "Write":
|
|
2943
|
+
return `Writing ${toolInput?.file_path || "file"}`;
|
|
2944
|
+
case "Edit":
|
|
2945
|
+
return `Editing ${toolInput?.file_path || "file"}`;
|
|
2946
|
+
case "Bash":
|
|
2947
|
+
const cmd = toolInput?.command;
|
|
2948
|
+
if (cmd) {
|
|
2949
|
+
const shortCmd = cmd.length > 40 ? cmd.substring(0, 37) + "..." : cmd;
|
|
2950
|
+
return `Running: ${shortCmd}`;
|
|
2951
|
+
}
|
|
2952
|
+
return "Running command";
|
|
2953
|
+
case "Grep":
|
|
2954
|
+
return `Searching for "${toolInput?.pattern || "pattern"}"`;
|
|
2955
|
+
case "Glob":
|
|
2956
|
+
return `Finding files: ${toolInput?.pattern || "pattern"}`;
|
|
2957
|
+
case "WebFetch":
|
|
2958
|
+
return `Fetching ${toolInput?.url || "URL"}`;
|
|
2959
|
+
case "WebSearch":
|
|
2960
|
+
return `Searching: ${toolInput?.query || "query"}`;
|
|
2961
|
+
case "Task":
|
|
2962
|
+
return `Spawning agent: ${toolInput?.description || "task"}`;
|
|
2963
|
+
default:
|
|
2964
|
+
return `Using ${toolName}`;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
async function checkTmuxForPermission(windowName) {
|
|
2968
|
+
try {
|
|
2969
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(windowName)) {
|
|
2970
|
+
return null;
|
|
2971
|
+
}
|
|
2972
|
+
const { stdout } = await execAsync2(`tmux capture-pane -t "${windowName}" -p -S -50 2>/dev/null`);
|
|
2973
|
+
if (!stdout.includes("Do you want to proceed?")) {
|
|
2974
|
+
return null;
|
|
2975
|
+
}
|
|
2976
|
+
let toolName = "Unknown tool";
|
|
2977
|
+
let description;
|
|
2978
|
+
const mcpMatch = stdout.match(/Tool use\s*\n\s*\n\s*([^\n(]+)\(/);
|
|
2979
|
+
if (mcpMatch) {
|
|
2980
|
+
toolName = mcpMatch[1].trim().replace(/\s*\(MCP\)\s*$/, "");
|
|
2981
|
+
const argsMatch = stdout.match(/Tool use\s*\n\s*\n\s*[^\n(]+\(([^)]*)\)/);
|
|
2982
|
+
description = argsMatch ? argsMatch[1].substring(0, 40) : void 0;
|
|
2983
|
+
}
|
|
2984
|
+
if (toolName === "Unknown tool") {
|
|
2985
|
+
const bashMatch = stdout.match(/Bash command[^\n]*\n\s*\n\s*([^\n]+)/);
|
|
2986
|
+
if (bashMatch) {
|
|
2987
|
+
toolName = "Bash";
|
|
2988
|
+
description = bashMatch[1].trim().substring(0, 40);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
if (toolName === "Unknown tool") {
|
|
2992
|
+
const simpleMatch = stdout.match(/●\s*([A-Za-z]+)\s*\(/);
|
|
2993
|
+
if (simpleMatch) {
|
|
2994
|
+
toolName = simpleMatch[1];
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
return { toolName, description };
|
|
2998
|
+
} catch {
|
|
2999
|
+
return null;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
var SessionWatcher = class extends import_events.EventEmitter {
|
|
3003
|
+
filePath;
|
|
3004
|
+
watcher = null;
|
|
3005
|
+
position = 0;
|
|
3006
|
+
status;
|
|
3007
|
+
isWatching = false;
|
|
3008
|
+
isReading = false;
|
|
3009
|
+
tmuxWindow = null;
|
|
3010
|
+
tmuxPollInterval = null;
|
|
3011
|
+
constructor(sessionFilePath, tmuxWindowName) {
|
|
3012
|
+
super();
|
|
3013
|
+
this.filePath = sessionFilePath;
|
|
3014
|
+
this.tmuxWindow = tmuxWindowName || null;
|
|
3015
|
+
this.status = {
|
|
3016
|
+
waitingForInput: false,
|
|
3017
|
+
lastActivity: /* @__PURE__ */ new Date(),
|
|
3018
|
+
recentEvents: []
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
/**
|
|
3022
|
+
* Get current status
|
|
3023
|
+
*/
|
|
3024
|
+
getStatus() {
|
|
3025
|
+
return {
|
|
3026
|
+
...this.status,
|
|
3027
|
+
recentEvents: [...this.status.recentEvents]
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Start watching the session file
|
|
3032
|
+
*/
|
|
3033
|
+
async start() {
|
|
3034
|
+
if (this.isWatching) return;
|
|
3035
|
+
try {
|
|
3036
|
+
const stat = await fs.promises.stat(this.filePath);
|
|
3037
|
+
this.position = stat.size;
|
|
3038
|
+
this.watcher = fs.watch(this.filePath, async (eventType) => {
|
|
3039
|
+
if (eventType === "change") {
|
|
3040
|
+
await this.readNewLines();
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
3043
|
+
if (this.tmuxWindow) {
|
|
3044
|
+
this.startTmuxPolling();
|
|
3045
|
+
}
|
|
3046
|
+
this.isWatching = true;
|
|
3047
|
+
this.emit("started");
|
|
3048
|
+
} catch (error) {
|
|
3049
|
+
this.emit("error", error);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
/**
|
|
3053
|
+
* Stop watching
|
|
3054
|
+
*/
|
|
3055
|
+
stop() {
|
|
3056
|
+
if (this.watcher) {
|
|
3057
|
+
this.watcher.close();
|
|
3058
|
+
this.watcher = null;
|
|
3059
|
+
}
|
|
3060
|
+
if (this.tmuxPollInterval) {
|
|
3061
|
+
clearInterval(this.tmuxPollInterval);
|
|
3062
|
+
this.tmuxPollInterval = null;
|
|
3063
|
+
}
|
|
3064
|
+
this.isWatching = false;
|
|
3065
|
+
this.emit("stopped");
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Start polling tmux for permission prompts
|
|
3069
|
+
*/
|
|
3070
|
+
startTmuxPolling() {
|
|
3071
|
+
this.tmuxPollInterval = setInterval(async () => {
|
|
3072
|
+
if (!this.tmuxWindow) return;
|
|
3073
|
+
const prompt = await checkTmuxForPermission(this.tmuxWindow);
|
|
3074
|
+
if (prompt && !this.status.waitingForInput) {
|
|
3075
|
+
this.status.waitingForInput = true;
|
|
3076
|
+
this.status.currentAction = `Waiting: ${prompt.toolName}`;
|
|
3077
|
+
this.status.currentTool = prompt.toolName;
|
|
3078
|
+
this.emit("permission", prompt);
|
|
3079
|
+
this.emit("status", this.getStatus());
|
|
3080
|
+
} else if (!prompt && this.status.waitingForInput) {
|
|
3081
|
+
this.status.waitingForInput = false;
|
|
3082
|
+
this.emit("status", this.getStatus());
|
|
3083
|
+
}
|
|
3084
|
+
}, 2e3);
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Read new lines from the file
|
|
3088
|
+
*/
|
|
3089
|
+
async readNewLines() {
|
|
3090
|
+
if (this.isReading) return;
|
|
3091
|
+
this.isReading = true;
|
|
3092
|
+
try {
|
|
3093
|
+
const handle = await fs.promises.open(this.filePath, "r");
|
|
3094
|
+
const stat = await handle.stat();
|
|
3095
|
+
if (stat.size <= this.position) {
|
|
3096
|
+
await handle.close();
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
const buffer = Buffer.alloc(stat.size - this.position);
|
|
3100
|
+
await handle.read(buffer, 0, buffer.length, this.position);
|
|
3101
|
+
await handle.close();
|
|
3102
|
+
this.position = stat.size;
|
|
3103
|
+
const content = buffer.toString("utf-8");
|
|
3104
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
3105
|
+
for (const line of lines) {
|
|
3106
|
+
const event = parseSessionLine(line);
|
|
3107
|
+
if (event) {
|
|
3108
|
+
this.processEvent(event);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
} catch (error) {
|
|
3112
|
+
this.emit("error", error);
|
|
3113
|
+
} finally {
|
|
3114
|
+
this.isReading = false;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
/**
|
|
3118
|
+
* Process a parsed event and update status
|
|
3119
|
+
*/
|
|
3120
|
+
processEvent(event) {
|
|
3121
|
+
this.status.lastActivity = event.timestamp;
|
|
3122
|
+
this.status.recentEvents.push(event);
|
|
3123
|
+
if (this.status.recentEvents.length > 10) {
|
|
3124
|
+
this.status.recentEvents.shift();
|
|
3125
|
+
}
|
|
3126
|
+
switch (event.type) {
|
|
3127
|
+
case "tool_start":
|
|
3128
|
+
this.status.currentTool = event.toolName;
|
|
3129
|
+
this.status.currentAction = formatAction(event.toolName, event.toolInput);
|
|
3130
|
+
this.status.waitingForInput = false;
|
|
3131
|
+
break;
|
|
3132
|
+
case "tool_end":
|
|
3133
|
+
if (event.interrupted) {
|
|
3134
|
+
this.status.waitingForInput = true;
|
|
3135
|
+
this.status.currentAction = "Waiting for approval";
|
|
3136
|
+
} else {
|
|
3137
|
+
this.status.currentTool = void 0;
|
|
3138
|
+
this.status.currentAction = void 0;
|
|
3139
|
+
}
|
|
3140
|
+
break;
|
|
3141
|
+
case "user_input":
|
|
3142
|
+
if (event.interrupted) {
|
|
3143
|
+
this.status.waitingForInput = true;
|
|
3144
|
+
this.status.currentAction = "Action rejected";
|
|
3145
|
+
}
|
|
3146
|
+
break;
|
|
3147
|
+
case "text":
|
|
3148
|
+
break;
|
|
3149
|
+
}
|
|
3150
|
+
this.emit("event", event);
|
|
3151
|
+
this.emit("status", this.getStatus());
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
async function createSessionWatcher(worktreePath, tmuxWindowName) {
|
|
3155
|
+
const sessionFile = await findSessionFile(worktreePath);
|
|
3156
|
+
if (!sessionFile) return null;
|
|
3157
|
+
return new SessionWatcher(sessionFile, tmuxWindowName);
|
|
3158
|
+
}
|
|
1655
3159
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1656
3160
|
0 && (module.exports = {
|
|
1657
3161
|
BranchLinker,
|
|
1658
3162
|
CLI_TO_VSCODE_MAP,
|
|
3163
|
+
ClaudeClient,
|
|
1659
3164
|
DEFAULT_VALUES,
|
|
3165
|
+
GHP_TOOLS,
|
|
1660
3166
|
GitHubAPI,
|
|
1661
3167
|
SETTING_DISPLAY_NAMES,
|
|
1662
3168
|
SYNCABLE_KEYS,
|
|
3169
|
+
SessionWatcher,
|
|
3170
|
+
TOOL_NAMES,
|
|
1663
3171
|
VSCODE_TO_CLI_MAP,
|
|
1664
3172
|
branchExists,
|
|
3173
|
+
buildConventionsContext,
|
|
1665
3174
|
buildIssueUrl,
|
|
1666
3175
|
buildOrgProjectUrl,
|
|
1667
3176
|
buildProjectUrl,
|
|
1668
3177
|
buildPullRequestUrl,
|
|
1669
3178
|
buildRepoUrl,
|
|
3179
|
+
checkTmuxForPermission,
|
|
1670
3180
|
checkoutBranch,
|
|
3181
|
+
claudePrompts,
|
|
3182
|
+
cleanupStaleAgents,
|
|
1671
3183
|
computeSettingsDiff,
|
|
1672
3184
|
createBranch,
|
|
3185
|
+
createSessionWatcher,
|
|
1673
3186
|
createWorktree,
|
|
1674
3187
|
detectRepository,
|
|
1675
3188
|
extractIssueNumberFromBranch,
|
|
1676
3189
|
fetchOrigin,
|
|
3190
|
+
findSessionFile,
|
|
3191
|
+
formatAction,
|
|
1677
3192
|
formatConflict,
|
|
1678
3193
|
generateBranchName,
|
|
1679
3194
|
generateWorktreePath,
|
|
3195
|
+
getAgent,
|
|
3196
|
+
getAgentByIssue,
|
|
3197
|
+
getAgentSummaries,
|
|
1680
3198
|
getAllBranches,
|
|
1681
3199
|
getCommitsAhead,
|
|
1682
3200
|
getCommitsBehind,
|
|
1683
3201
|
getCurrentBranch,
|
|
1684
3202
|
getDefaultBranch,
|
|
1685
3203
|
getDiffSummary,
|
|
3204
|
+
getIssueReferenceText,
|
|
1686
3205
|
getLocalBranches,
|
|
3206
|
+
getRegistryPath,
|
|
1687
3207
|
getRemoteBranches,
|
|
1688
3208
|
getRepositoryRoot,
|
|
3209
|
+
getTools,
|
|
1689
3210
|
getWorktreeForBranch,
|
|
1690
3211
|
hasDifferences,
|
|
1691
3212
|
hasUncommittedChanges,
|
|
1692
3213
|
isGitRepository,
|
|
3214
|
+
listAgents,
|
|
1693
3215
|
listWorktrees,
|
|
3216
|
+
loadProjectConventions,
|
|
3217
|
+
loadRegistry,
|
|
1694
3218
|
normalizeVSCodeSettings,
|
|
1695
3219
|
parseBranchLink,
|
|
1696
3220
|
parseGitHubUrl,
|
|
1697
3221
|
parseIssueUrl,
|
|
3222
|
+
parseSessionLine,
|
|
1698
3223
|
pullLatest,
|
|
1699
3224
|
queries,
|
|
3225
|
+
registerAgent,
|
|
1700
3226
|
removeBranchLinkFromBody,
|
|
1701
3227
|
removeWorktree,
|
|
1702
3228
|
resolveConflicts,
|
|
1703
3229
|
sanitizeForBranchName,
|
|
3230
|
+
saveRegistry,
|
|
1704
3231
|
setBranchLinkInBody,
|
|
1705
3232
|
skip,
|
|
1706
3233
|
toVSCodeSettings,
|
|
3234
|
+
unregisterAgent,
|
|
3235
|
+
updateAgent,
|
|
1707
3236
|
useCli,
|
|
1708
3237
|
useCustom,
|
|
1709
3238
|
useVSCode,
|