@google/jules-merge 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +201 -0
  2. package/dist/cli/check-conflicts.command.mjs +387 -0
  3. package/dist/cli/index.mjs +28 -0
  4. package/dist/cli/init.command.mjs +159 -0
  5. package/dist/core/src/activities/client.d.ts +131 -0
  6. package/dist/core/src/activities/summary.d.ts +22 -0
  7. package/dist/core/src/activities/types.d.ts +79 -0
  8. package/dist/core/src/api.d.ts +49 -0
  9. package/dist/core/src/artifacts.d.ts +105 -0
  10. package/dist/core/src/caching.d.ts +31 -0
  11. package/dist/core/src/client.d.ts +180 -0
  12. package/dist/core/src/errors.d.ts +97 -0
  13. package/dist/core/src/index.d.ts +49 -0
  14. package/dist/core/src/mappers.d.ts +53 -0
  15. package/dist/core/src/network/adapter.d.ts +47 -0
  16. package/dist/core/src/platform/node.d.ts +45 -0
  17. package/dist/core/src/platform/types.d.ts +88 -0
  18. package/dist/core/src/polling.d.ts +43 -0
  19. package/dist/core/src/query/computed.d.ts +86 -0
  20. package/dist/core/src/query/projection.d.ts +80 -0
  21. package/dist/core/src/query/schema.d.ts +124 -0
  22. package/dist/core/src/query/select.d.ts +21 -0
  23. package/dist/core/src/query/validate.d.ts +68 -0
  24. package/dist/core/src/session.d.ts +195 -0
  25. package/dist/core/src/sessions.d.ts +87 -0
  26. package/dist/core/src/snapshot.d.ts +46 -0
  27. package/dist/core/src/sources.d.ts +23 -0
  28. package/dist/core/src/storage/cache-info.d.ts +43 -0
  29. package/dist/core/src/storage/memory.d.ts +69 -0
  30. package/dist/core/src/storage/node-fs.d.ts +95 -0
  31. package/dist/core/src/storage/root.d.ts +17 -0
  32. package/dist/core/src/storage/types.d.ts +115 -0
  33. package/dist/core/src/streaming.d.ts +47 -0
  34. package/dist/core/src/types.d.ts +1418 -0
  35. package/dist/core/src/utils/page-token.d.ts +55 -0
  36. package/dist/core/src/utils.d.ts +27 -0
  37. package/dist/index.mjs +395 -0
  38. package/dist/merge/src/__tests__/conflicts/git-handler.test.d.ts +1 -0
  39. package/dist/merge/src/__tests__/conflicts/git-spec.test.d.ts +1 -0
  40. package/dist/merge/src/__tests__/conflicts/session-handler.test.d.ts +1 -0
  41. package/dist/merge/src/__tests__/conflicts/session-spec.test.d.ts +1 -0
  42. package/dist/merge/src/__tests__/init/init-handler.test.d.ts +1 -0
  43. package/dist/merge/src/__tests__/init/init-spec.test.d.ts +1 -0
  44. package/dist/merge/src/__tests__/init/templates.test.d.ts +1 -0
  45. package/dist/merge/src/__tests__/shared/git.test.d.ts +1 -0
  46. package/dist/merge/src/__tests__/shared/github.test.d.ts +1 -0
  47. package/dist/merge/src/__tests__/shared/session.test.d.ts +1 -0
  48. package/dist/merge/src/cli/check-conflicts.command.d.ts +24 -0
  49. package/dist/merge/src/cli/index.d.ts +2 -0
  50. package/dist/merge/src/cli/init.command.d.ts +23 -0
  51. package/dist/merge/src/conflicts/git-handler.d.ts +4 -0
  52. package/dist/merge/src/conflicts/git-spec.d.ts +43 -0
  53. package/dist/merge/src/conflicts/index.d.ts +4 -0
  54. package/dist/merge/src/conflicts/session-handler.d.ts +9 -0
  55. package/dist/merge/src/conflicts/session-spec.d.ts +43 -0
  56. package/dist/merge/src/index.d.ts +5 -0
  57. package/dist/merge/src/init/index.d.ts +4 -0
  58. package/dist/merge/src/init/init-handler.d.ts +4 -0
  59. package/dist/merge/src/init/init-spec.d.ts +41 -0
  60. package/dist/merge/src/init/templates.d.ts +10 -0
  61. package/dist/merge/src/mcp/index.d.ts +1 -0
  62. package/dist/merge/src/mcp/server.d.ts +2 -0
  63. package/dist/merge/src/shared/git.d.ts +17 -0
  64. package/dist/merge/src/shared/github.d.ts +18 -0
  65. package/dist/merge/src/shared/result.d.ts +19 -0
  66. package/dist/merge/src/shared/session.d.ts +16 -0
  67. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # Jules Merge
2
+
3
+ Detect and surface merge conflicts between a coding agent's changes and the base branch — before or during CI.
4
+
5
+ ## Check for conflicts in CI
6
+
7
+ ```bash
8
+ npx @google/jules-merge check-conflicts \
9
+ --repo your-org/your-repo \
10
+ --pr 42 \
11
+ --sha abc123
12
+ ```
13
+
14
+ When a merge has already been attempted, `check-conflicts` reads conflict markers from the filesystem and returns structured JSON with the affected files, their conflict markers, and a task directive that tells the agent exactly what to resolve.
15
+
16
+ ## Check for conflicts proactively
17
+
18
+ ```bash
19
+ npx @google/jules-merge check-conflicts \
20
+ --session 7439826373470093109 \
21
+ --repo your-org/your-repo
22
+ ```
23
+
24
+ This queries the Jules SDK for the session's changed files and compares them against recent commits on the base branch. If files overlap, it returns the remote file content (`remoteShadowContent`) so the agent can resolve conflicts without needing `git pull`.
25
+
26
+ ## Generate a CI workflow
27
+
28
+ ```bash
29
+ npx @google/jules-merge init
30
+ ```
31
+
32
+ Writes `.github/workflows/jules-merge-check.yml` to your repo. The workflow runs on every pull request: it attempts a merge, and if conflicts exist, runs `check-conflicts` to produce structured output that Jules can act on.
33
+
34
+ ```bash
35
+ npx @google/jules-merge init --base-branch develop --force
36
+ ```
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm i @google/jules-merge
42
+ ```
43
+
44
+ For session-based checks, set authentication:
45
+
46
+ ```
47
+ JULES_API_KEY Required for session mode.
48
+ GITHUB_TOKEN Required. GitHub PAT with repo access.
49
+ ```
50
+
51
+ Or use GitHub App authentication:
52
+
53
+ ```
54
+ GITHUB_APP_ID App ID
55
+ GITHUB_APP_PRIVATE_KEY_BASE64 Base64-encoded private key
56
+ GITHUB_APP_INSTALLATION_ID Installation ID
57
+ ```
58
+
59
+ ## CLI Reference
60
+
61
+ ### `jules-merge check-conflicts`
62
+
63
+ Detect merge conflicts. Mode is inferred from the arguments provided.
64
+
65
+ ```
66
+ jules-merge check-conflicts [options]
67
+
68
+ Session mode (proactive):
69
+ --session <id> Jules session ID
70
+ --repo <owner/repo>
71
+ --base <branch> Base branch (default: main)
72
+
73
+ Git mode (CI failure):
74
+ --pr <number> Pull request number
75
+ --sha <sha> Failing commit SHA
76
+ --repo <owner/repo>
77
+ ```
78
+
79
+ **Session mode** queries the Jules SDK for changed files and compares them against remote commits. Returns `remoteShadowContent` for each conflicting file.
80
+
81
+ **Git mode** reads `git status` for unmerged files and extracts conflict markers. Returns a `taskDirective` with resolution instructions.
82
+
83
+ ### `jules-merge init`
84
+
85
+ Generate a GitHub Actions workflow for automated conflict detection.
86
+
87
+ ```
88
+ jules-merge init [options]
89
+
90
+ Options:
91
+ --output-dir <dir> Directory to write into (default: .)
92
+ --workflow-name <name> Filename without .yml (default: jules-merge-check)
93
+ --base-branch <branch> Branch to check against (default: main)
94
+ --force Overwrite existing file
95
+ ```
96
+
97
+ ## Programmatic API
98
+
99
+ All handlers are exported for use in scripts, CI pipelines, or other packages.
100
+
101
+ ```ts
102
+ import {
103
+ SessionCheckHandler,
104
+ GitCheckHandler,
105
+ InitHandler,
106
+ } from '@google/jules-merge';
107
+ ```
108
+
109
+ ### `SessionCheckHandler`
110
+
111
+ Compares a Jules session's changed files against remote commits on the base branch.
112
+
113
+ ```ts
114
+ const handler = new SessionCheckHandler(octokit, julesClient);
115
+ const result = await handler.execute({
116
+ sessionId: '7439826373470093109',
117
+ repo: 'your-org/your-repo',
118
+ base: 'main',
119
+ });
120
+
121
+ if (result.success && result.data.status === 'conflict') {
122
+ for (const conflict of result.data.conflicts) {
123
+ console.log(`${conflict.filePath}: ${conflict.conflictReason}`);
124
+ console.log(conflict.remoteShadowContent);
125
+ }
126
+ }
127
+ ```
128
+
129
+ Returns `{ status: 'clean' | 'conflict', conflicts: [...] }` on success. Each conflict includes `filePath`, `conflictReason`, and `remoteShadowContent`.
130
+
131
+ ### `GitCheckHandler`
132
+
133
+ Reads conflict markers from the local filesystem after a failed merge.
134
+
135
+ ```ts
136
+ const handler = new GitCheckHandler();
137
+ const result = await handler.execute({
138
+ repo: 'your-org/your-repo',
139
+ pullRequestNumber: 42,
140
+ failingCommitSha: 'abc123',
141
+ });
142
+
143
+ if (result.success) {
144
+ console.log(result.data.taskDirective);
145
+ for (const file of result.data.affectedFiles) {
146
+ console.log(`${file.filePath}: ${file.gitConflictMarkers}`);
147
+ }
148
+ }
149
+ ```
150
+
151
+ Returns `{ taskDirective, priority, affectedFiles: [...] }` on success. Each file includes `filePath`, `baseCommitSha`, and `gitConflictMarkers`.
152
+
153
+ ### `InitHandler`
154
+
155
+ Generates a GitHub Actions workflow file.
156
+
157
+ ```ts
158
+ const handler = new InitHandler();
159
+ const result = await handler.execute({
160
+ outputDir: '.',
161
+ workflowName: 'jules-merge-check',
162
+ baseBranch: 'main',
163
+ force: false,
164
+ });
165
+
166
+ if (result.success) {
167
+ console.log(`Created: ${result.data.filePath}`);
168
+ }
169
+ ```
170
+
171
+ Returns `{ filePath, content }` on success.
172
+
173
+ ### `buildWorkflowYaml`
174
+
175
+ Generate the workflow YAML string without writing to disk.
176
+
177
+ ```ts
178
+ import { buildWorkflowYaml } from '@google/jules-merge';
179
+
180
+ const yaml = buildWorkflowYaml({
181
+ workflowName: 'merge-check',
182
+ baseBranch: 'main',
183
+ });
184
+ ```
185
+
186
+ ## MCP Server
187
+
188
+ The package exposes an MCP server with two tools:
189
+
190
+ - **`check_conflicts`** — Detects merge conflicts (session or git mode)
191
+ - **`init_workflow`** — Generates a CI workflow file
192
+
193
+ ```bash
194
+ jules-merge mcp
195
+ ```
196
+
197
+ ## License
198
+
199
+ Apache-2.0
200
+
201
+ > **Note:** This is not an officially supported Google product. This project is not eligible for the [Google Open Source Software Vulnerability Rewards Program](https://bughunters.google.com/open-source-security).
@@ -0,0 +1,387 @@
1
+ // src/cli/check-conflicts.command.ts
2
+ import { defineCommand } from "citty";
3
+
4
+ // src/conflicts/session-spec.ts
5
+ import { z } from "zod";
6
+ var SessionCheckInputSchema = z.object({
7
+ sessionId: z.string().min(1, "Session ID is required"),
8
+ repo: z.string().min(1).refine((s) => s.includes("/"), "Must be in owner/repo format"),
9
+ base: z.string().default("main")
10
+ });
11
+ var SessionCheckErrorCode = z.enum([
12
+ "SESSION_QUERY_FAILED",
13
+ "GITHUB_API_ERROR",
14
+ "RATE_LIMIT_EXCEEDED",
15
+ "UNKNOWN_ERROR"
16
+ ]);
17
+
18
+ // src/shared/result.ts
19
+ function ok(data) {
20
+ return { success: true, data };
21
+ }
22
+ function fail(code, message, recoverable = false, suggestion) {
23
+ return { success: false, error: { code, message, recoverable, suggestion } };
24
+ }
25
+
26
+ // src/shared/session.ts
27
+ import { jules } from "@google/jules-sdk";
28
+ async function getSessionChangedFiles(client, sessionId) {
29
+ const session = client.session(sessionId);
30
+ await session.activities.hydrate();
31
+ const snapshot = await session.snapshot();
32
+ const isBusy = isBusyState(snapshot.state);
33
+ if (isBusy) {
34
+ return aggregateFromActivities(snapshot.activities ?? []);
35
+ }
36
+ const changeSet = typeof snapshot.changeSet === "function" ? snapshot.changeSet() : undefined;
37
+ if (!changeSet)
38
+ return [];
39
+ const parsed = changeSet.parsed();
40
+ return parsed.files.map((f) => ({
41
+ path: f.path,
42
+ changeType: f.changeType
43
+ }));
44
+ }
45
+ function isBusyState(state) {
46
+ const busy = new Set([
47
+ "queued",
48
+ "planning",
49
+ "inProgress",
50
+ "in_progress"
51
+ ]);
52
+ return busy.has(state);
53
+ }
54
+ function aggregateFromActivities(activities) {
55
+ const fileMap = new Map;
56
+ for (const activity of activities) {
57
+ for (const artifact of activity.artifacts) {
58
+ if (artifact.type === "changeSet") {
59
+ const parsed = artifact.parsed();
60
+ for (const file of parsed.files) {
61
+ const existing = fileMap.get(file.path);
62
+ if (existing) {
63
+ existing.latestChangeType = file.changeType;
64
+ } else {
65
+ fileMap.set(file.path, {
66
+ firstChangeType: file.changeType,
67
+ latestChangeType: file.changeType
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ const result = [];
75
+ for (const [path, info] of fileMap) {
76
+ if (info.firstChangeType === "created" && info.latestChangeType === "deleted") {
77
+ continue;
78
+ }
79
+ const netType = info.firstChangeType === "created" ? "created" : info.latestChangeType;
80
+ result.push({ path, changeType: netType });
81
+ }
82
+ return result;
83
+ }
84
+
85
+ // src/shared/github.ts
86
+ import { Octokit } from "@octokit/rest";
87
+ import { createAppAuth } from "@octokit/auth-app";
88
+ function createOctokit() {
89
+ const appId = process.env.GITHUB_APP_ID;
90
+ const privateKeyBase64 = process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
91
+ const privateKeyRaw = process.env.GITHUB_APP_PRIVATE_KEY;
92
+ const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
93
+ if (appId && (privateKeyBase64 || privateKeyRaw) && installationId) {
94
+ const privateKey = privateKeyBase64 ? Buffer.from(privateKeyBase64, "base64").toString("utf-8") : privateKeyRaw;
95
+ return new Octokit({
96
+ authStrategy: createAppAuth,
97
+ auth: {
98
+ appId,
99
+ privateKey,
100
+ installationId: Number(installationId)
101
+ }
102
+ });
103
+ }
104
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
105
+ if (token) {
106
+ return new Octokit({ auth: token });
107
+ }
108
+ throw new Error("GitHub auth not configured. Set GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY + GITHUB_APP_INSTALLATION_ID for App auth, or GITHUB_TOKEN for PAT auth.");
109
+ }
110
+ async function compareCommits(octokit, owner, repo, base, head) {
111
+ const { data } = await octokit.repos.compareCommits({
112
+ owner,
113
+ repo,
114
+ base,
115
+ head
116
+ });
117
+ return (data.files ?? []).map((f) => f.filename);
118
+ }
119
+ async function getFileContent(octokit, owner, repo, path, ref) {
120
+ try {
121
+ const { data } = await octokit.repos.getContent({
122
+ owner,
123
+ repo,
124
+ path,
125
+ ref
126
+ });
127
+ if ("content" in data && typeof data.content === "string") {
128
+ return Buffer.from(data.content, "base64").toString("utf-8");
129
+ }
130
+ return "";
131
+ } catch (error) {
132
+ if (error?.status === 404) {
133
+ return "";
134
+ }
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ // src/conflicts/session-handler.ts
140
+ class SessionCheckHandler {
141
+ octokit;
142
+ julesClient;
143
+ constructor(octokit, julesClient) {
144
+ this.octokit = octokit;
145
+ this.julesClient = julesClient;
146
+ }
147
+ async execute(input) {
148
+ try {
149
+ const [owner, repo] = input.repo.split("/");
150
+ let sessionPaths;
151
+ try {
152
+ const sessionFiles = await getSessionChangedFiles(this.julesClient, input.sessionId);
153
+ sessionPaths = sessionFiles.map((f) => f.path);
154
+ } catch (error) {
155
+ return fail("SESSION_QUERY_FAILED", `Failed to query session ${input.sessionId}: ${error.message}`, true, "Verify the session ID is correct and JULES_API_KEY is set.");
156
+ }
157
+ let remoteFiles;
158
+ try {
159
+ remoteFiles = await compareCommits(this.octokit, owner, repo, input.base, "HEAD");
160
+ } catch (error) {
161
+ return fail("GITHUB_API_ERROR", `Failed to compare commits: ${error.message}`, true, "Check GITHUB_TOKEN and repository access.");
162
+ }
163
+ const remoteSet = new Set(remoteFiles);
164
+ const overlapping = sessionPaths.filter((f) => remoteSet.has(f));
165
+ if (overlapping.length === 0) {
166
+ return ok({
167
+ status: "clean",
168
+ message: "No conflicts detected.",
169
+ conflicts: []
170
+ });
171
+ }
172
+ const conflicts = await Promise.all(overlapping.map(async (filePath) => {
173
+ const remoteShadowContent = await getFileContent(this.octokit, owner, repo, filePath, input.base);
174
+ return {
175
+ filePath,
176
+ conflictReason: "Remote commit modified this file since branch creation.",
177
+ remoteShadowContent
178
+ };
179
+ }));
180
+ return ok({
181
+ status: "conflict",
182
+ message: `The remote ${input.base} branch has advanced. Rebase required for ${overlapping.join(", ")}.`,
183
+ conflicts
184
+ });
185
+ } catch (error) {
186
+ return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
187
+ }
188
+ }
189
+ }
190
+
191
+ // src/conflicts/git-spec.ts
192
+ import { z as z2 } from "zod";
193
+ var GitCheckInputSchema = z2.object({
194
+ repo: z2.string().min(1).refine((s) => s.includes("/"), "Must be in owner/repo format"),
195
+ pullRequestNumber: z2.number().int().positive(),
196
+ failingCommitSha: z2.string().min(1)
197
+ });
198
+ var GitCheckErrorCode = z2.enum([
199
+ "NO_UNMERGED_FILES",
200
+ "GIT_STATUS_FAILED",
201
+ "FILE_READ_FAILED",
202
+ "UNKNOWN_ERROR"
203
+ ]);
204
+
205
+ // src/conflicts/git-handler.ts
206
+ import { readFile } from "node:fs/promises";
207
+
208
+ // src/shared/git.ts
209
+ import { execFile } from "node:child_process";
210
+ import { promisify } from "node:util";
211
+ var execFileAsync = promisify(execFile);
212
+ async function gitStatusUnmerged(cwd) {
213
+ try {
214
+ const { stdout } = await execFileAsync("git", ["status", "--porcelain"], { cwd });
215
+ const files = stdout.split(`
216
+ `).filter((line) => line.startsWith("UU ")).map((line) => line.slice(3).trim());
217
+ return { ok: true, data: files };
218
+ } catch (error) {
219
+ return {
220
+ ok: false,
221
+ error: error instanceof Error ? error.message : String(error)
222
+ };
223
+ }
224
+ }
225
+ async function gitMergeBase(head, base, cwd) {
226
+ try {
227
+ const { stdout } = await execFileAsync("git", ["merge-base", head, base], { cwd });
228
+ return { ok: true, data: stdout.trim() };
229
+ } catch (error) {
230
+ return {
231
+ ok: false,
232
+ error: error instanceof Error ? error.message : String(error)
233
+ };
234
+ }
235
+ }
236
+
237
+ // src/conflicts/git-handler.ts
238
+ class GitCheckHandler {
239
+ async execute(input) {
240
+ try {
241
+ const statusResult = await gitStatusUnmerged();
242
+ if (!statusResult.ok) {
243
+ return fail("GIT_STATUS_FAILED", `Failed to get git status: ${statusResult.error}`, true, "Ensure git is available and the working directory is a repository.");
244
+ }
245
+ const unmergedFiles = statusResult.data;
246
+ if (unmergedFiles.length === 0) {
247
+ return fail("NO_UNMERGED_FILES", "No unmerged files found. The merge conflict may have already been resolved.", false);
248
+ }
249
+ const affectedFiles = await Promise.all(unmergedFiles.map(async (filePath) => {
250
+ let content;
251
+ try {
252
+ content = await readFile(filePath, "utf-8");
253
+ } catch (error) {
254
+ return {
255
+ filePath,
256
+ baseCommitSha: "",
257
+ gitConflictMarkers: `[Error reading file: ${error.message}]`
258
+ };
259
+ }
260
+ const markers = extractConflictMarkers(content);
261
+ return {
262
+ filePath,
263
+ baseCommitSha: "",
264
+ gitConflictMarkers: markers
265
+ };
266
+ }));
267
+ const mergeBaseResult = await gitMergeBase(input.failingCommitSha, "HEAD");
268
+ const baseSha = mergeBaseResult.ok ? mergeBaseResult.data : "unknown";
269
+ for (const file of affectedFiles) {
270
+ file.baseCommitSha = baseSha;
271
+ }
272
+ const taskDirective = [
273
+ `MERGE CONFLICT RESOLUTION REQUIRED for PR #${input.pullRequestNumber}.`,
274
+ `Failing commit: ${input.failingCommitSha}.`,
275
+ `${affectedFiles.length} file(s) have unresolved conflicts.`,
276
+ `Review the gitConflictMarkers for each file and rewrite the code to resolve all conflicts.`
277
+ ].join(`
278
+ `);
279
+ return ok({
280
+ taskDirective,
281
+ priority: "critical",
282
+ affectedFiles
283
+ });
284
+ } catch (error) {
285
+ return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
286
+ }
287
+ }
288
+ }
289
+ function extractConflictMarkers(content) {
290
+ const lines = content.split(`
291
+ `);
292
+ const blocks = [];
293
+ let inConflict = false;
294
+ let currentBlock = [];
295
+ for (const line of lines) {
296
+ if (line.startsWith("<<<<<<<")) {
297
+ inConflict = true;
298
+ currentBlock = [line];
299
+ } else if (line.startsWith(">>>>>>>") && inConflict) {
300
+ currentBlock.push(line);
301
+ blocks.push(currentBlock.join(`
302
+ `));
303
+ inConflict = false;
304
+ currentBlock = [];
305
+ } else if (inConflict) {
306
+ currentBlock.push(line);
307
+ }
308
+ }
309
+ return blocks.join(`
310
+ ---
311
+ `);
312
+ }
313
+
314
+ // src/cli/check-conflicts.command.ts
315
+ var check_conflicts_command_default = defineCommand({
316
+ meta: {
317
+ name: "check-conflicts",
318
+ description: "Check for merge conflicts between a Jules session and the remote base branch, or parse an existing CI merge failure."
319
+ },
320
+ args: {
321
+ session: {
322
+ type: "string",
323
+ description: "Jules session ID (triggers session mode)"
324
+ },
325
+ repo: {
326
+ type: "string",
327
+ description: "Repository in owner/repo format"
328
+ },
329
+ base: {
330
+ type: "string",
331
+ description: "Base branch (session mode only)",
332
+ default: "main"
333
+ },
334
+ pr: {
335
+ type: "string",
336
+ description: "Pull request number (triggers git mode)"
337
+ },
338
+ sha: {
339
+ type: "string",
340
+ description: "Failing commit SHA (git mode only)"
341
+ }
342
+ },
343
+ async run({ args }) {
344
+ const isSessionMode = !!args.session;
345
+ const isGitMode = !!args.pr;
346
+ if (!isSessionMode && !isGitMode) {
347
+ console.error("Either --session (session mode) or --pr (git mode) is required.");
348
+ process.exit(2);
349
+ }
350
+ if (isSessionMode && isGitMode) {
351
+ console.error("Cannot use both --session and --pr. Pick one mode.");
352
+ process.exit(2);
353
+ }
354
+ let result;
355
+ if (isSessionMode) {
356
+ if (!args.repo) {
357
+ console.error("--repo is required in session mode.");
358
+ process.exit(2);
359
+ }
360
+ const input = SessionCheckInputSchema.parse({
361
+ sessionId: args.session,
362
+ repo: args.repo,
363
+ base: args.base
364
+ });
365
+ const handler = new SessionCheckHandler(createOctokit(), jules);
366
+ result = await handler.execute(input);
367
+ } else {
368
+ if (!args.repo || !args.sha) {
369
+ console.error("--repo and --sha are required in git mode.");
370
+ process.exit(2);
371
+ }
372
+ const input = GitCheckInputSchema.parse({
373
+ repo: args.repo,
374
+ pullRequestNumber: parseInt(args.pr, 10),
375
+ failingCommitSha: args.sha
376
+ });
377
+ const handler = new GitCheckHandler;
378
+ result = await handler.execute(input);
379
+ }
380
+ process.stdout.write(JSON.stringify(result, null, 2) + `
381
+ `);
382
+ process.exit(result.success ? 0 : 1);
383
+ }
384
+ });
385
+ export {
386
+ check_conflicts_command_default as default
387
+ };
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { readdirSync } from "fs";
5
+ import { join, dirname } from "path";
6
+ import { fileURLToPath, pathToFileURL } from "url";
7
+ import { defineCommand, runMain } from "citty";
8
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
9
+ async function discoverCommands() {
10
+ const commands = {};
11
+ const files = readdirSync(__dirname2).filter((f) => f.endsWith(".command.ts") || f.endsWith(".command.js") || f.endsWith(".command.mjs"));
12
+ for (const file of files) {
13
+ const name = file.replace(/\.command\.(ts|js|mjs)$/, "");
14
+ const mod = await import(pathToFileURL(join(__dirname2, file)).href);
15
+ commands[name] = mod.default;
16
+ }
17
+ return commands;
18
+ }
19
+ var subCommands = await discoverCommands();
20
+ var main = defineCommand({
21
+ meta: {
22
+ name: "jules-merge",
23
+ version: "0.0.1",
24
+ description: "Predictive conflict detection for parallel AI agents"
25
+ },
26
+ subCommands
27
+ });
28
+ runMain(main);