codeam-cli 2.2.2 → 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 +331 -18
  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.2",
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);
@@ -2212,8 +2220,8 @@ function isUnder(parent, candidate) {
2212
2220
  }
2213
2221
  async function isExistingFile(absPath) {
2214
2222
  try {
2215
- const stat2 = await fs5.stat(absPath);
2216
- return stat2.isFile();
2223
+ const stat3 = await fs5.stat(absPath);
2224
+ return stat3.isFile();
2217
2225
  } catch {
2218
2226
  return false;
2219
2227
  }
@@ -2286,9 +2294,9 @@ async function readProjectFile(rawPath) {
2286
2294
  if (!abs) {
2287
2295
  return { error: `File not found in the project tree: ${rawPath}` };
2288
2296
  }
2289
- const stat2 = await fs5.stat(abs);
2290
- if (stat2.size > MAX_FILE_BYTES) {
2291
- 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).` };
2292
2300
  }
2293
2301
  const buf = await fs5.readFile(abs);
2294
2302
  if (looksBinary(buf)) {
@@ -2318,12 +2326,261 @@ async function writeProjectFile(rawPath, content) {
2318
2326
  }
2319
2327
  }
2320
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
+
2321
2578
  // src/commands/start.ts
2322
2579
  function saveFilesTemp(files) {
2323
2580
  return files.filter(({ base64 }) => base64 && base64.length > 0).map(({ filename, base64 }) => {
2324
2581
  const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
2325
- const tmpPath = path6.join(os5.tmpdir(), `codeam-${(0, import_crypto.randomUUID)()}-${safeName}`);
2326
- 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"));
2327
2584
  return tmpPath;
2328
2585
  });
2329
2586
  }
@@ -2396,14 +2653,14 @@ try:
2396
2653
  sys.exit((st>>8)&0xFF)
2397
2654
  except Exception:sys.exit(0)
2398
2655
  `;
2399
- const helperPath = path6.join(os5.tmpdir(), "codeam-quota-helper.py");
2400
- fs6.writeFileSync(helperPath, helperScript, { mode: 420 });
2656
+ const helperPath = path7.join(os5.tmpdir(), "codeam-quota-helper.py");
2657
+ fs7.writeFileSync(helperPath, helperScript, { mode: 420 });
2401
2658
  const python = findInPath("python3") ?? findInPath("python");
2402
2659
  if (!python) {
2403
2660
  quotaFetchInProgress = false;
2404
2661
  return;
2405
2662
  }
2406
- const proc = (0, import_child_process3.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
2663
+ const proc = (0, import_child_process4.spawn)(python, [helperPath, claudeCmd, "--tools", ""], {
2407
2664
  stdio: ["pipe", "pipe", "ignore"],
2408
2665
  cwd: process.cwd(),
2409
2666
  env: { ...process.env, TERM: "dumb", COLUMNS: "120", LINES: "30" }
@@ -2429,7 +2686,7 @@ except Exception:sys.exit(0)
2429
2686
  } catch {
2430
2687
  }
2431
2688
  try {
2432
- fs6.unlinkSync(helperPath);
2689
+ fs7.unlinkSync(helperPath);
2433
2690
  } catch {
2434
2691
  }
2435
2692
  quotaFetchInProgress = false;
@@ -2479,7 +2736,7 @@ except Exception:sys.exit(0)
2479
2736
  setTimeout(() => {
2480
2737
  for (const p2 of paths) {
2481
2738
  try {
2482
- fs6.unlinkSync(p2);
2739
+ fs7.unlinkSync(p2);
2483
2740
  } catch {
2484
2741
  }
2485
2742
  }
@@ -2571,6 +2828,62 @@ except Exception:sys.exit(0)
2571
2828
  await relay.sendResult(cmd.id, "completed", result);
2572
2829
  break;
2573
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
+ }
2574
2887
  }
2575
2888
  });
2576
2889
  ws.addHandler({
@@ -2598,7 +2911,7 @@ except Exception:sys.exit(0)
2598
2911
  setTimeout(() => {
2599
2912
  for (const p2 of paths) {
2600
2913
  try {
2601
- fs6.unlinkSync(p2);
2914
+ fs7.unlinkSync(p2);
2602
2915
  } catch {
2603
2916
  }
2604
2917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeam-cli",
3
- "version": "2.2.2",
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": {