codeam-cli 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.js +394 -42
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `codeam-cli` are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.2.2] — 2026-05-02
8
+
9
+ ### Fixed
10
+
11
+ - **clients:** Recursive suffix search for read_file (v2.2.2)
12
+
13
+ ## [2.2.1] — 2026-05-02
14
+
15
+ ### Fixed
16
+
17
+ - **cli:** Subdir fallback for read_file when CLI cwd is a monorepo parent (v2.2.1)
18
+
7
19
  ## [2.1.0] — 2026-04-25
8
20
 
9
21
  ### Added
package/dist/index.js CHANGED
@@ -87,11 +87,11 @@ var require_src = __commonJS({
87
87
  });
88
88
 
89
89
  // src/commands/start.ts
90
- var fs6 = __toESM(require("fs"));
90
+ var fs7 = __toESM(require("fs"));
91
91
  var os5 = __toESM(require("os"));
92
- var path6 = __toESM(require("path"));
92
+ var path7 = __toESM(require("path"));
93
93
  var import_crypto = require("crypto");
94
- var import_child_process3 = require("child_process");
94
+ var import_child_process4 = require("child_process");
95
95
  var import_picocolors2 = __toESM(require("picocolors"));
96
96
 
97
97
  // src/config.ts
@@ -179,7 +179,7 @@ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
179
179
  // package.json
180
180
  var package_default = {
181
181
  name: "codeam-cli",
182
- version: "2.2.1",
182
+ version: "2.3.0",
183
183
  description: "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands \u2014 from anywhere.",
184
184
  main: "dist/index.js",
185
185
  bin: {
@@ -2163,7 +2163,15 @@ var startCommandSchema = import_zod2.z.object({
2163
2163
  // `path` is bounded to 4096 chars (a comfortable POSIX path max) so a
2164
2164
  // malformed payload can't blow up the disk-side validator.
2165
2165
  path: import_zod2.z.string().min(1).max(4096).optional(),
2166
- content: import_zod2.z.string().optional()
2166
+ content: import_zod2.z.string().optional(),
2167
+ // Mini-IDE / project ops. `paths` (plural, strings) is used for
2168
+ // git_commit's optional file selection — distinct from `files`
2169
+ // (FileEntry[]) used by `start_task` for attachments.
2170
+ query: import_zod2.z.string().max(256).optional(),
2171
+ message: import_zod2.z.string().max(8e3).optional(),
2172
+ paths: import_zod2.z.array(import_zod2.z.string().max(4096)).optional(),
2173
+ side: import_zod2.z.enum(["ours", "theirs"]).optional(),
2174
+ limit: import_zod2.z.number().int().min(1).max(500).optional()
2167
2175
  });
2168
2176
  function parsePayload(schema, raw) {
2169
2177
  const result = schema.safeParse(raw);
@@ -2174,6 +2182,8 @@ function parsePayload(schema, raw) {
2174
2182
  var fs5 = __toESM(require("fs/promises"));
2175
2183
  var path5 = __toESM(require("path"));
2176
2184
  var MAX_FILE_BYTES = 5 * 1024 * 1024;
2185
+ var MAX_WALK_DEPTH = 6;
2186
+ var MAX_VISITED_DIRS = 5e3;
2177
2187
  var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
2178
2188
  "node_modules",
2179
2189
  ".git",
@@ -2188,43 +2198,80 @@ var SUBDIR_IGNORE = /* @__PURE__ */ new Set([
2188
2198
  ".parcel-cache",
2189
2199
  ".idea",
2190
2200
  ".vscode",
2201
+ ".vscode-test",
2191
2202
  "ios",
2192
- "android"
2203
+ "android",
2193
2204
  // expo-managed native dirs are huge and rarely interesting
2205
+ ".gradle",
2206
+ ".cxx",
2207
+ ".intellijPlatform",
2208
+ ".kotlin",
2209
+ "tmp",
2210
+ "target",
2211
+ "venv",
2212
+ ".venv",
2213
+ ".mypy_cache",
2214
+ ".pytest_cache",
2215
+ "__pycache__"
2194
2216
  ]);
2195
- async function listSubdirs(dir) {
2217
+ function isUnder(parent, candidate) {
2218
+ const rel = path5.relative(parent, candidate);
2219
+ return rel === "" || !rel.startsWith("..") && !path5.isAbsolute(rel);
2220
+ }
2221
+ async function isExistingFile(absPath) {
2196
2222
  try {
2197
- const entries = await fs5.readdir(dir, { withFileTypes: true });
2198
- return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") ? true : !SUBDIR_IGNORE.has(e.name)).filter((e) => e.isDirectory() && !SUBDIR_IGNORE.has(e.name)).map((e) => path5.join(dir, e.name));
2223
+ const stat3 = await fs5.stat(absPath);
2224
+ return stat3.isFile();
2199
2225
  } catch {
2200
- return [];
2226
+ return false;
2201
2227
  }
2202
2228
  }
2203
- function isUnder(parent, candidate) {
2204
- const rel = path5.relative(parent, candidate);
2205
- return !rel.startsWith("..") && !path5.isAbsolute(rel);
2229
+ async function walkForSuffix(dir, needleVariants, depth, ctx) {
2230
+ if (depth > MAX_WALK_DEPTH) return;
2231
+ if (ctx.visited > MAX_VISITED_DIRS) return;
2232
+ if (ctx.matches.length >= ctx.cap) return;
2233
+ ctx.visited++;
2234
+ let entries = [];
2235
+ try {
2236
+ entries = await fs5.readdir(dir, { withFileTypes: true });
2237
+ } catch {
2238
+ return;
2239
+ }
2240
+ for (const e of entries) {
2241
+ if (!e.isFile()) continue;
2242
+ const full = path5.join(dir, e.name);
2243
+ if (needleVariants.some((needle) => full.endsWith(needle))) {
2244
+ ctx.matches.push(full);
2245
+ if (ctx.matches.length >= ctx.cap) return;
2246
+ }
2247
+ }
2248
+ for (const e of entries) {
2249
+ if (!e.isDirectory()) continue;
2250
+ if (SUBDIR_IGNORE.has(e.name)) continue;
2251
+ if (e.name.startsWith(".") && SUBDIR_IGNORE.has(e.name)) continue;
2252
+ await walkForSuffix(path5.join(dir, e.name), needleVariants, depth + 1, ctx);
2253
+ if (ctx.matches.length >= ctx.cap) return;
2254
+ }
2206
2255
  }
2207
2256
  async function findFile(rawPath) {
2208
2257
  const cwd = process.cwd();
2209
- const candidates = [];
2210
2258
  if (path5.isAbsolute(rawPath)) {
2211
- candidates.push(path5.normalize(rawPath));
2212
- } else {
2213
- candidates.push(path5.resolve(cwd, rawPath));
2214
- const subdirs = await listSubdirs(cwd);
2215
- for (const sub of subdirs) {
2216
- candidates.push(path5.resolve(sub, rawPath));
2217
- }
2218
- }
2219
- for (const cand of candidates) {
2220
- if (!isUnder(cwd, cand)) continue;
2221
- try {
2222
- const stat2 = await fs5.stat(cand);
2223
- if (stat2.isFile()) return cand;
2224
- } catch {
2225
- }
2226
- }
2227
- return null;
2259
+ const abs = path5.normalize(rawPath);
2260
+ if (isUnder(cwd, abs) && await isExistingFile(abs)) return abs;
2261
+ }
2262
+ const direct = path5.resolve(cwd, rawPath);
2263
+ if (isUnder(cwd, direct) && await isExistingFile(direct)) return direct;
2264
+ const normalized = path5.normalize(rawPath).replace(/^[./\\]+/, "");
2265
+ const needles = [
2266
+ `${path5.sep}${normalized}`,
2267
+ `/${normalized}`
2268
+ ].filter((v, i, a) => a.indexOf(v) === i);
2269
+ const ctx = { visited: 0, matches: [], cap: 16 };
2270
+ await walkForSuffix(cwd, needles, 0, ctx);
2271
+ const candidates = ctx.matches.filter((c2) => isUnder(cwd, c2));
2272
+ if (candidates.length === 0) return null;
2273
+ candidates.sort((a, b) => a.length - b.length);
2274
+ return candidates[0];
2228
2275
  }
2229
2276
  async function findWriteTarget(rawPath) {
2230
2277
  const found = await findFile(rawPath);
@@ -2247,9 +2294,9 @@ async function readProjectFile(rawPath) {
2247
2294
  if (!abs) {
2248
2295
  return { error: `File not found in the project tree: ${rawPath}` };
2249
2296
  }
2250
- const stat2 = await fs5.stat(abs);
2251
- if (stat2.size > MAX_FILE_BYTES) {
2252
- return { error: `File too large (${(stat2.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
2297
+ const stat3 = await fs5.stat(abs);
2298
+ if (stat3.size > MAX_FILE_BYTES) {
2299
+ return { error: `File too large (${(stat3.size / 1024 / 1024).toFixed(1)} MB > ${MAX_FILE_BYTES / 1024 / 1024} MB).` };
2253
2300
  }
2254
2301
  const buf = await fs5.readFile(abs);
2255
2302
  if (looksBinary(buf)) {
@@ -2279,12 +2326,261 @@ async function writeProjectFile(rawPath, content) {
2279
2326
  }
2280
2327
  }
2281
2328
 
2329
+ // src/services/project-ops.service.ts
2330
+ var import_child_process3 = require("child_process");
2331
+ var import_util = require("util");
2332
+ var fs6 = __toESM(require("fs/promises"));
2333
+ var path6 = __toESM(require("path"));
2334
+ var execFileP = (0, import_util.promisify)(import_child_process3.execFile);
2335
+ var PROJECT_IGNORE = /* @__PURE__ */ new Set([
2336
+ "node_modules",
2337
+ ".git",
2338
+ ".next",
2339
+ ".expo",
2340
+ "dist",
2341
+ "build",
2342
+ "out",
2343
+ ".cache",
2344
+ "coverage",
2345
+ ".turbo",
2346
+ ".parcel-cache",
2347
+ ".idea",
2348
+ ".vscode",
2349
+ ".vscode-test",
2350
+ "ios",
2351
+ "android",
2352
+ ".gradle",
2353
+ ".cxx",
2354
+ ".intellijPlatform",
2355
+ ".kotlin",
2356
+ "tmp",
2357
+ "target",
2358
+ "venv",
2359
+ ".venv",
2360
+ ".mypy_cache",
2361
+ ".pytest_cache",
2362
+ "__pycache__",
2363
+ ".DS_Store"
2364
+ ]);
2365
+ var MAX_TREE_FILES = 5e3;
2366
+ var MAX_DIFF_BYTES = 512 * 1024;
2367
+ var MAX_GIT_OUTPUT = 256 * 1024;
2368
+ async function listProjectFiles(opts = {}) {
2369
+ const root = opts.cwd ?? process.cwd();
2370
+ const cap = opts.cap ?? MAX_TREE_FILES;
2371
+ const q2 = (opts.query ?? "").trim().toLowerCase();
2372
+ const out = [];
2373
+ let truncated = false;
2374
+ async function walk(dir, depth) {
2375
+ if (out.length >= cap) {
2376
+ truncated = true;
2377
+ return;
2378
+ }
2379
+ let entries = [];
2380
+ try {
2381
+ entries = await fs6.readdir(dir, { withFileTypes: true });
2382
+ } catch {
2383
+ return;
2384
+ }
2385
+ for (const e of entries) {
2386
+ if (out.length >= cap) {
2387
+ truncated = true;
2388
+ return;
2389
+ }
2390
+ if (PROJECT_IGNORE.has(e.name)) continue;
2391
+ const full = path6.join(dir, e.name);
2392
+ if (e.isDirectory()) {
2393
+ if (depth >= 12) continue;
2394
+ await walk(full, depth + 1);
2395
+ } else if (e.isFile()) {
2396
+ const rel = path6.relative(root, full);
2397
+ if (q2 && !rel.toLowerCase().includes(q2) && !e.name.toLowerCase().includes(q2)) {
2398
+ continue;
2399
+ }
2400
+ let size = 0;
2401
+ try {
2402
+ const st3 = await fs6.stat(full);
2403
+ size = st3.size;
2404
+ } catch {
2405
+ }
2406
+ out.push({ path: rel, name: e.name, size });
2407
+ }
2408
+ }
2409
+ }
2410
+ await walk(root, 0);
2411
+ out.sort((a, b) => a.path.localeCompare(b.path));
2412
+ return { files: out, truncated, root };
2413
+ }
2414
+ async function git(args2, cwd) {
2415
+ try {
2416
+ const { stdout, stderr } = await execFileP("git", args2, {
2417
+ cwd: cwd ?? process.cwd(),
2418
+ maxBuffer: MAX_GIT_OUTPUT,
2419
+ timeout: 3e4
2420
+ });
2421
+ return { stdout, stderr, code: 0 };
2422
+ } catch (err) {
2423
+ const e = err;
2424
+ return {
2425
+ stdout: e.stdout ?? "",
2426
+ stderr: e.stderr ?? e.message ?? "git failed",
2427
+ code: typeof e.code === "number" ? e.code : 1
2428
+ };
2429
+ }
2430
+ }
2431
+ async function gitStatus(cwd) {
2432
+ const root = cwd ?? process.cwd();
2433
+ const r = await git(["status", "--porcelain=v2", "--branch"], root);
2434
+ if (r.code !== 0) {
2435
+ return {
2436
+ branch: null,
2437
+ upstream: null,
2438
+ ahead: 0,
2439
+ behind: 0,
2440
+ entries: [],
2441
+ hasMergeInProgress: false,
2442
+ error: r.stderr.trim()
2443
+ };
2444
+ }
2445
+ const lines = r.stdout.split("\n").filter(Boolean);
2446
+ let branch = null;
2447
+ let upstream = null;
2448
+ let ahead = 0;
2449
+ let behind = 0;
2450
+ const entries = [];
2451
+ for (const line of lines) {
2452
+ if (line.startsWith("# branch.head ")) branch = line.slice("# branch.head ".length).trim();
2453
+ else if (line.startsWith("# branch.upstream ")) upstream = line.slice("# branch.upstream ".length).trim();
2454
+ else if (line.startsWith("# branch.ab ")) {
2455
+ const m = line.match(/\+(\d+)\s+-(\d+)/);
2456
+ if (m) {
2457
+ ahead = parseInt(m[1], 10);
2458
+ behind = parseInt(m[2], 10);
2459
+ }
2460
+ } else if (line.startsWith("1 ")) {
2461
+ const parts = line.split(" ");
2462
+ const xy = parts[1];
2463
+ const p2 = parts.slice(8).join(" ");
2464
+ entries.push({
2465
+ code: xy,
2466
+ path: p2,
2467
+ staged: xy[0] !== ".",
2468
+ conflict: false
2469
+ });
2470
+ } else if (line.startsWith("2 ")) {
2471
+ const parts = line.split(" ");
2472
+ const xy = parts[1];
2473
+ const tail = parts.slice(9).join(" ");
2474
+ const [newPath, oldPath] = tail.split(" ");
2475
+ entries.push({
2476
+ code: xy,
2477
+ path: newPath ?? "",
2478
+ oldPath: oldPath ?? void 0,
2479
+ staged: xy[0] !== ".",
2480
+ conflict: false
2481
+ });
2482
+ } else if (line.startsWith("? ")) {
2483
+ entries.push({
2484
+ code: "??",
2485
+ path: line.slice(2),
2486
+ staged: false,
2487
+ conflict: false
2488
+ });
2489
+ } else if (line.startsWith("u ")) {
2490
+ const parts = line.split(" ");
2491
+ const xy = parts[1];
2492
+ const p2 = parts.slice(10).join(" ");
2493
+ entries.push({
2494
+ code: xy,
2495
+ path: p2,
2496
+ staged: false,
2497
+ conflict: true
2498
+ });
2499
+ }
2500
+ }
2501
+ let hasMergeInProgress = false;
2502
+ try {
2503
+ const gitDir = (await git(["rev-parse", "--git-dir"], root)).stdout.trim();
2504
+ const mergeHead = path6.isAbsolute(gitDir) ? path6.join(gitDir, "MERGE_HEAD") : path6.join(root, gitDir, "MERGE_HEAD");
2505
+ await fs6.access(mergeHead);
2506
+ hasMergeInProgress = true;
2507
+ } catch {
2508
+ }
2509
+ return { branch, upstream, ahead, behind, entries, hasMergeInProgress };
2510
+ }
2511
+ async function gitDiff(file, cwd) {
2512
+ const args2 = ["diff", "--no-color", "--patch"];
2513
+ if (file) args2.push("--", file);
2514
+ const r = await git(args2, cwd);
2515
+ if (r.code !== 0 && !r.stdout) {
2516
+ return { diff: "", truncated: false, error: r.stderr.trim() };
2517
+ }
2518
+ const truncated = r.stdout.length >= MAX_DIFF_BYTES;
2519
+ return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
2520
+ }
2521
+ async function gitDiffStaged(file, cwd) {
2522
+ const args2 = ["diff", "--cached", "--no-color", "--patch"];
2523
+ if (file) args2.push("--", file);
2524
+ const r = await git(args2, cwd);
2525
+ if (r.code !== 0 && !r.stdout) {
2526
+ return { diff: "", truncated: false, error: r.stderr.trim() };
2527
+ }
2528
+ const truncated = r.stdout.length >= MAX_DIFF_BYTES;
2529
+ return { diff: r.stdout.slice(0, MAX_DIFF_BYTES), truncated };
2530
+ }
2531
+ async function gitLog(limit = 30, cwd) {
2532
+ const sep2 = "";
2533
+ const fmt = ["%H", "%h", "%an", "%aI", "%s"].join(sep2);
2534
+ const r = await git(["log", `-n${Math.min(limit, 200)}`, `--pretty=format:${fmt}`], cwd);
2535
+ if (r.code !== 0) return { commits: [], error: r.stderr.trim() };
2536
+ const commits = r.stdout.split("\n").filter(Boolean).map((line) => {
2537
+ const [hash, shortHash, author, date, subject] = line.split(sep2);
2538
+ return { hash, shortHash, author, date, subject };
2539
+ });
2540
+ return { commits };
2541
+ }
2542
+ async function gitCommit(message, files, cwd) {
2543
+ if (!message || message.trim().length === 0) {
2544
+ return { error: "Commit message is required." };
2545
+ }
2546
+ if (files && files.length > 0) {
2547
+ const r2 = await git(["add", "--", ...files], cwd);
2548
+ if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
2549
+ } else {
2550
+ const r2 = await git(["add", "-A"], cwd);
2551
+ if (r2.code !== 0) return { error: `git add failed: ${r2.stderr.trim()}` };
2552
+ }
2553
+ const r = await git(["commit", "-m", message], cwd);
2554
+ if (r.code !== 0) {
2555
+ return { error: r.stderr.trim() || "git commit failed" };
2556
+ }
2557
+ const head = await git(["rev-parse", "HEAD"], cwd);
2558
+ return { ok: true, commit: head.stdout.trim() };
2559
+ }
2560
+ async function gitPush(cwd) {
2561
+ const r = await git(["push"], cwd);
2562
+ if (r.code !== 0) return { error: r.stderr.trim() || "git push failed" };
2563
+ return { ok: true, output: (r.stdout + r.stderr).trim() };
2564
+ }
2565
+ async function gitPull(cwd) {
2566
+ const r = await git(["pull", "--ff-only"], cwd);
2567
+ if (r.code !== 0) return { error: r.stderr.trim() || "git pull failed" };
2568
+ return { ok: true, output: (r.stdout + r.stderr).trim() };
2569
+ }
2570
+ async function gitResolve(file, side, cwd) {
2571
+ const r = await git(["checkout", `--${side}`, "--", file], cwd);
2572
+ if (r.code !== 0) return { error: r.stderr.trim() || `git checkout --${side} failed` };
2573
+ const add = await git(["add", "--", file], cwd);
2574
+ if (add.code !== 0) return { error: add.stderr.trim() || "git add (resolve) failed" };
2575
+ return { ok: true };
2576
+ }
2577
+
2282
2578
  // src/commands/start.ts
2283
2579
  function saveFilesTemp(files) {
2284
2580
  return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
2285
2581
  const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
2286
- const tmpPath = path6.join(os5.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
2287
- fs6.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
2582
+ const tmpPath = path7.join(os5.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
2583
+ fs7.writeFileSync(tmpPath, Buffer.from(base64, "base64"));
2288
2584
  return tmpPath;
2289
2585
  });
2290
2586
  }
@@ -2357,14 +2653,14 @@ try:
2357
2653
  sys.exit((st>>8)&0xFF)
2358
2654
  except Exception:sys.exit(0)
2359
2655
  `;
2360
- const helperPath = path6.join(os5.tmpdir(), "codeam-quota-helper.py");
2361
- fs6.writeFileSync(helperPath, helperScript, { mode: 420 });
2656
+ const helperPath = path7.join(os5.tmpdir(), "codeam-quota-helper.py");
2657
+ fs7.writeFileSync(helperPath, helperScript, { mode: 420 });
2362
2658
  const python = findInPath("python3") ?? findInPath("python");
2363
2659
  if (!python) {
2364
2660
  quotaFetchInProgress = false;
2365
2661
  return;
2366
2662
  }
2367
- const proc = (0, import_child_process3.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
2663
+ const proc = (0, import_child_process4.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
2368
2664
  stdio: ["pipe", "pipe", "ignore"],
2369
2665
  cwd: process.cwd(),
2370
2666
  env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
@@ -2390,7 +2686,7 @@ except Exception:sys.exit(0)
2390
2686
  } catch {
2391
2687
  }
2392
2688
  try {
2393
- fs6.unlinkSync(helperPath);
2689
+ fs7.unlinkSync(helperPath);
2394
2690
  } catch {
2395
2691
  }
2396
2692
  quotaFetchInProgress = false;
@@ -2440,7 +2736,7 @@ except Exception:sys.exit(0)
2440
2736
  setTimeout(() => {
2441
2737
  for (const p2 of paths) {
2442
2738
  try {
2443
- fs6.unlinkSync(p2);
2739
+ fs7.unlinkSync(p2);
2444
2740
  } catch {
2445
2741
  }
2446
2742
  }
@@ -2532,6 +2828,62 @@ except Exception:sys.exit(0)
2532
2828
  await relay.sendResult(cmd.id, "completed", result);
2533
2829
  break;
2534
2830
  }
2831
+ case "list_files": {
2832
+ const result = await listProjectFiles({ query: parsed.query });
2833
+ await relay.sendResult(cmd.id, "completed", result);
2834
+ break;
2835
+ }
2836
+ case "git_status": {
2837
+ const result = await gitStatus();
2838
+ await relay.sendResult(cmd.id, "completed", result);
2839
+ break;
2840
+ }
2841
+ case "git_diff": {
2842
+ const { path: filePath } = parsed;
2843
+ const result = await gitDiff(filePath ?? null);
2844
+ await relay.sendResult(cmd.id, "completed", result);
2845
+ break;
2846
+ }
2847
+ case "git_diff_staged": {
2848
+ const { path: filePath } = parsed;
2849
+ const result = await gitDiffStaged(filePath ?? null);
2850
+ await relay.sendResult(cmd.id, "completed", result);
2851
+ break;
2852
+ }
2853
+ case "git_log": {
2854
+ const result = await gitLog(parsed.limit ?? 30);
2855
+ await relay.sendResult(cmd.id, "completed", result);
2856
+ break;
2857
+ }
2858
+ case "git_commit": {
2859
+ if (!parsed.message) {
2860
+ await relay.sendResult(cmd.id, "failed", { error: "Missing message" });
2861
+ break;
2862
+ }
2863
+ const result = await gitCommit(parsed.message, parsed.paths);
2864
+ await relay.sendResult(cmd.id, "completed", result);
2865
+ break;
2866
+ }
2867
+ case "git_push": {
2868
+ const result = await gitPush();
2869
+ await relay.sendResult(cmd.id, "completed", result);
2870
+ break;
2871
+ }
2872
+ case "git_pull": {
2873
+ const result = await gitPull();
2874
+ await relay.sendResult(cmd.id, "completed", result);
2875
+ break;
2876
+ }
2877
+ case "git_resolve": {
2878
+ const { path: filePath, side } = parsed;
2879
+ if (!filePath || !side) {
2880
+ await relay.sendResult(cmd.id, "failed", { error: "Missing path or side" });
2881
+ break;
2882
+ }
2883
+ const result = await gitResolve(filePath, side);
2884
+ await relay.sendResult(cmd.id, "completed", result);
2885
+ break;
2886
+ }
2535
2887
  }
2536
2888
  });
2537
2889
  ws.addHandler({
@@ -2559,7 +2911,7 @@ except Exception:sys.exit(0)
2559
2911
  setTimeout(() => {
2560
2912
  for (const p2 of paths) {
2561
2913
  try {
2562
- fs6.unlinkSync(p2);
2914
+ fs7.unlinkSync(p2);
2563
2915
  } catch {
2564
2916
  }
2565
2917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Remote control Claude Code (and other AI coding agents) from your mobile phone. Pair your device, send prompts, stream responses in real-time, and approve commands — from anywhere.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {