coderemote 0.2.1 → 0.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.
package/README.md CHANGED
@@ -95,6 +95,8 @@ coderemote kill <id>... # end terminal sessions (an id prefix from `sta
95
95
  coderemote kill --all # end every session
96
96
  coderemote stop # stop the agent for this folder (--all: every agent)
97
97
  coderemote serve # share on 127.0.0.1 only, no tunnel (local testing)
98
+ coderemote requests [PORT] # requests that reached a previewed port (--grep, --failed, --json)
99
+ coderemote request <ID> # headers and bodies of one request (--show-secrets, --json)
98
100
  coderemote rotate-token # revoke every paired device
99
101
  coderemote login # sign in again (--microsoft, or --device-code without a browser)
100
102
  coderemote update # install the latest version (`up` says when there's one)
@@ -104,6 +106,12 @@ coderemote help # all options
104
106
  Options: `--root DIR` shares another folder; `--port N` picks the local port (default
105
107
  `31580`). The tunnel URL stays the same between runs, so paired phones reconnect on their own.
106
108
 
109
+ `requests` and `request` read the traffic log of a port you preview from the app, so a coding
110
+ agent in a terminal (Claude Code, Codex, …) can debug with it: run them in the shared folder.
111
+ The log lives in the agent's memory only (the latest 200 requests per port, bodies up to 64 KB).
112
+ Auth headers, cookies and token-like fields are masked; `--show-secrets` prints them, for
113
+ example to reuse a session token in a test.
114
+
107
115
  ## Requirements
108
116
 
109
117
  - **Node.js 20** or newer on **macOS** (Linux is expected to work but not yet tested)
@@ -1587,7 +1587,7 @@ function installCommand(installedAt) {
1587
1587
  // package.json
1588
1588
  var package_default = {
1589
1589
  name: "coderemote",
1590
- version: "0.2.1",
1590
+ version: "0.3.0",
1591
1591
  description: "Use your computer from your iPhone: files, git and a real terminal over a secure Microsoft Dev Tunnel. No SSH, no port forwarding, no VPN.",
1592
1592
  keywords: [
1593
1593
  "remote",
@@ -1654,6 +1654,148 @@ var package_default = {
1654
1654
  }
1655
1655
  };
1656
1656
 
1657
+ // src/requests.js
1658
+ var MASK = "[redacted]";
1659
+ var SECRET_HEADER = /^(authorization|proxy-authorization|cookie|set-cookie)$|token|secret|api[-_]?key|session|signature|csrf|xsrf/i;
1660
+ var SECRET_FIELD = /pass(word|wd)?$|secret|token|api[-_]?key|authorization|cookie|session|credential|private[-_]?key|^otp$|^code$|signature/i;
1661
+ var Redactor = class {
1662
+ hidden = 0;
1663
+ constructor({ show: show2 = false } = {}) {
1664
+ this.show = show2;
1665
+ }
1666
+ #mask() {
1667
+ this.hidden++;
1668
+ return MASK;
1669
+ }
1670
+ /** Query parameters with secret-looking names, e.g. ?token=…. */
1671
+ path(value) {
1672
+ if (this.show) return value;
1673
+ const q = value.indexOf("?");
1674
+ if (q < 0) return value;
1675
+ const params = value.slice(q + 1).split("&").map((pair) => {
1676
+ const eq = pair.indexOf("=");
1677
+ if (eq < 0 || !SECRET_FIELD.test(decodeSafe(pair.slice(0, eq)))) return pair;
1678
+ return `${pair.slice(0, eq)}=${this.#mask()}`;
1679
+ });
1680
+ return `${value.slice(0, q)}?${params.join("&")}`;
1681
+ }
1682
+ header(name, value) {
1683
+ if (this.show || !SECRET_HEADER.test(name)) return value;
1684
+ const lower = name.toLowerCase();
1685
+ if (lower === "authorization" || lower === "proxy-authorization") {
1686
+ const space = value.indexOf(" ");
1687
+ return space > 0 ? `${value.slice(0, space)} ${this.#mask()}` : this.#mask();
1688
+ }
1689
+ if (lower === "cookie") return value.split(/;\s*/).map((c) => this.#cookie(c)).join("; ");
1690
+ if (lower === "set-cookie") {
1691
+ const [first, ...attributes] = value.split(/;\s*/);
1692
+ return [this.#cookie(first), ...attributes].join("; ");
1693
+ }
1694
+ return this.#mask();
1695
+ }
1696
+ #cookie(pair) {
1697
+ const eq = pair.indexOf("=");
1698
+ return eq < 0 ? pair : `${pair.slice(0, eq)}=${this.#mask()}`;
1699
+ }
1700
+ /** JSON and form bodies get secret fields masked; other text is left as sent. */
1701
+ body(text, contentType = "", { pretty = true } = {}) {
1702
+ if (/json/i.test(contentType) || /^\s*[[{]/.test(text)) {
1703
+ try {
1704
+ const value = JSON.parse(text);
1705
+ return JSON.stringify(this.show ? value : this.#json(value), null, pretty ? 2 : void 0);
1706
+ } catch {
1707
+ }
1708
+ }
1709
+ if (!this.show && /x-www-form-urlencoded/i.test(contentType)) return this.path(`?${text}`).slice(1);
1710
+ return text;
1711
+ }
1712
+ #json(value) {
1713
+ if (Array.isArray(value)) return value.map((v) => this.#json(v));
1714
+ if (!value || typeof value !== "object") return value;
1715
+ return Object.fromEntries(
1716
+ Object.entries(value).map(([k, v]) => [
1717
+ k,
1718
+ SECRET_FIELD.test(k) && v != null && typeof v !== "object" && typeof v !== "boolean" ? this.#mask() : this.#json(v)
1719
+ ])
1720
+ );
1721
+ }
1722
+ };
1723
+ function decodeSafe(s) {
1724
+ try {
1725
+ return decodeURIComponent(s);
1726
+ } catch {
1727
+ return s;
1728
+ }
1729
+ }
1730
+ function filterRequests(requests, { grep, failed } = {}) {
1731
+ const needle = grep?.toLowerCase();
1732
+ return requests.filter(
1733
+ (r) => (!needle || `${r.method} ${r.path}`.toLowerCase().includes(needle)) && (!failed || r.error || (r.status ?? 0) >= 400)
1734
+ );
1735
+ }
1736
+ function time(ms) {
1737
+ return new Date(ms).toTimeString().slice(0, 8);
1738
+ }
1739
+ function bytes(n) {
1740
+ if (n < 1024) return `${n} B`;
1741
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
1742
+ return `${(n / 1024 / 1024).toFixed(1)} MB`;
1743
+ }
1744
+ function formatList(requests, redactor, { showPort = false } = {}) {
1745
+ return requests.map((r) => {
1746
+ const status2 = r.error ? "ERR" : r.status == null ? "..." : String(r.status);
1747
+ const parts = [
1748
+ `#${r.id}`.padEnd(6),
1749
+ status2.padEnd(4),
1750
+ `${r.method} ${redactor.path(r.path)}`,
1751
+ r.duration == null ? "pending" : `${r.duration} ms`,
1752
+ bytes(r.responseSize),
1753
+ time(r.time)
1754
+ ];
1755
+ if (showPort) parts.push(`port ${r.port}`);
1756
+ if (r.error) parts.push(r.error);
1757
+ return parts.join(" ");
1758
+ }).join("\n");
1759
+ }
1760
+ function formatDetail(d, redactor) {
1761
+ const status2 = d.error ? `error: ${d.error}` : d.status == null ? "no response yet" : String(d.status);
1762
+ const lines = [
1763
+ `#${d.id} ${d.method} ${redactor.path(d.path)} -> ${status2}`,
1764
+ `${new Date(d.time).toISOString()}${d.duration == null ? "" : ` ${d.duration} ms`}`
1765
+ ];
1766
+ for (const [title, message] of [
1767
+ ["Request", d.request],
1768
+ ["Response", d.response]
1769
+ ]) {
1770
+ lines.push("", `${title} headers:`);
1771
+ for (const name of Object.keys(message.headers).sort()) {
1772
+ const values = [].concat(message.headers[name]);
1773
+ for (const value of values) lines.push(` ${name}: ${redactor.header(name, String(value))}`);
1774
+ }
1775
+ lines.push("", `${title} body: ${describeBody(message.body, redactor, message.headers["content-type"])}`);
1776
+ }
1777
+ return lines.join("\n");
1778
+ }
1779
+ function describeBody(body, redactor, contentType) {
1780
+ if (!body.size) return "(empty)";
1781
+ if (body.truncated) return `${bytes(body.size)}, too large to keep (only bodies up to 64 KB are recorded)`;
1782
+ if (body.text == null) return `${bytes(body.size)} of binary data`;
1783
+ return `${bytes(body.size)}
1784
+ ${indent(redactor.body(body.text, contentType))}`;
1785
+ }
1786
+ function indent(text) {
1787
+ return text.replace(/^/gm, " ");
1788
+ }
1789
+ function redactDetail(d, redactor) {
1790
+ const message = (m) => ({
1791
+ headers: Object.fromEntries(
1792
+ Object.entries(m.headers).map(([k, v]) => [k, Array.isArray(v) ? v.map((x) => redactor.header(k, String(x))) : redactor.header(k, String(v))])
1793
+ ),
1794
+ body: m.body.text == null ? m.body : { ...m.body, text: redactor.body(m.body.text, m.headers["content-type"], { pretty: false }) }
1795
+ });
1796
+ return { ...d, path: redactor.path(d.path), request: message(d.request), response: message(d.response) };
1797
+ }
1798
+
1657
1799
  // src/spinner.js
1658
1800
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1659
1801
  var dim = (s) => `\x1B[2m${s}\x1B[22m`;
@@ -1977,6 +2119,12 @@ var USAGE = `Usage: coderemote <command> [options]
1977
2119
  stop [--all] stop the agent for this folder (or every agent)
1978
2120
  rotate-token revoke every paired device
1979
2121
  update install the latest coderemote (up says when there's one)
2122
+ requests [PORT] list the requests that reached a previewed port, oldest first;
2123
+ --grep TEXT, --failed (errors, 4xx and 5xx), --limit N (default 50)
2124
+ requests clear [PORT] forget the recorded requests
2125
+ request <ID> [PORT] headers and bodies of one request (bodies up to 64 KB are kept);
2126
+ auth headers, cookies and token-like fields are masked unless
2127
+ --show-secrets is given; --json on either for machine-readable output
1980
2128
  version print this coderemote's version (also --version, -v)
1981
2129
  login sign in to Microsoft Dev Tunnels with GitHub (up does this when needed);
1982
2130
  --microsoft for a Microsoft account, --device-code without a browser
@@ -2092,6 +2240,26 @@ async function sessionsOf(agent) {
2092
2240
  return [];
2093
2241
  }
2094
2242
  }
2243
+ function currentAgent() {
2244
+ const agents = registry.list();
2245
+ if (flag("--port")) return agents.find((a) => a.port === port);
2246
+ const here = process.cwd();
2247
+ const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path7.sep)).sort((a, b) => b.root.length - a.root.length);
2248
+ return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
2249
+ }
2250
+ async function previewPorts(agent, given) {
2251
+ if (given) return [given];
2252
+ let ports = [];
2253
+ try {
2254
+ ports = (await agentApi(agent, "GET", "/api/preview")).ports.map((p) => p.port);
2255
+ } catch {
2256
+ }
2257
+ if (!ports.length) {
2258
+ console.error("No port is being previewed. Open a preview from the app, or name the port (a closed preview keeps its log).");
2259
+ process.exit(1);
2260
+ }
2261
+ return ports;
2262
+ }
2095
2263
  function ago(ms) {
2096
2264
  const s = Math.round((Date.now() - ms) / 1e3);
2097
2265
  if (s < 60) return `${s}s`;
@@ -2122,8 +2290,11 @@ Token: ${cfg.token}`);
2122
2290
  Sharing: ${root}`);
2123
2291
  printPairing(url, cfg.token, access, vscodeUrl);
2124
2292
  const newer = await update;
2125
- if (newer) console.log(` Update available: ${VERSION} \u2192 ${newer}. Run \`coderemote update\` to install it.
2293
+ if (newer) {
2294
+ const c = (code, s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
2295
+ console.log(` ${c("1;33", "\u2B06 Update available:")} ${c("2", VERSION)} \u2192 ${c("1;32", newer)}. Run ${c("36", "coderemote update")} to install it.
2126
2296
  `);
2297
+ }
2127
2298
  break;
2128
2299
  }
2129
2300
  case "status":
@@ -2227,6 +2398,79 @@ Updated to coderemote ${latest}.`);
2227
2398
  }
2228
2399
  break;
2229
2400
  }
2401
+ case "requests":
2402
+ case "request": {
2403
+ const agent = currentAgent();
2404
+ if (!agent) {
2405
+ const running = registry.list();
2406
+ console.error(
2407
+ flag("--port") ? `No agent is running on port ${port}.` : running.length ? `Several agents are running; run this inside a shared folder or pick one with --port (${running.map((a) => a.port).join(", ")}).` : "No agent is running. Start one with `coderemote up`."
2408
+ );
2409
+ process.exit(1);
2410
+ }
2411
+ const valued = /* @__PURE__ */ new Set(["--grep", "--limit", "--port"]);
2412
+ const words = rest.filter((a, i) => !a.startsWith("--") && !valued.has(rest[i - 1]));
2413
+ const json = rest.includes("--json");
2414
+ const redactor = new Redactor({ show: rest.includes("--show-secrets") });
2415
+ const hint = () => {
2416
+ if (redactor.hidden && !json) console.error(`
2417
+ (${redactor.hidden} secret value${redactor.hidden === 1 ? "" : "s"} masked; add --show-secrets to print them)`);
2418
+ };
2419
+ if (cmd === "request") {
2420
+ const id = parseInt(words[0], 10);
2421
+ if (!id) {
2422
+ console.error("Usage: coderemote request <ID> [PORT] [--show-secrets] [--json]");
2423
+ process.exit(1);
2424
+ }
2425
+ let found;
2426
+ for (const p of await previewPorts(agent, words[1])) {
2427
+ try {
2428
+ found = await agentApi(agent, "GET", "/api/preview/request", { port: p, id });
2429
+ break;
2430
+ } catch {
2431
+ }
2432
+ }
2433
+ if (!found) {
2434
+ console.error(`No request #${id} (only the latest 200 per port are kept).`);
2435
+ process.exit(1);
2436
+ }
2437
+ console.log(json ? JSON.stringify(redactDetail(found, redactor), null, 2) : formatDetail(found, redactor));
2438
+ hint();
2439
+ break;
2440
+ }
2441
+ if (words[0] === "clear") {
2442
+ for (const p of await previewPorts(agent, words[1])) {
2443
+ await agentApi(agent, "DELETE", "/api/preview/requests", { port: p });
2444
+ console.log(`cleared the requests to port ${p}`);
2445
+ }
2446
+ break;
2447
+ }
2448
+ const ports = await previewPorts(agent, words[0]);
2449
+ let requests = [];
2450
+ for (const p of ports) {
2451
+ const { requests: list } = await agentApi(agent, "GET", "/api/preview/requests", { port: p });
2452
+ requests.push(...list.map((r) => ({ ...r, port: Number(p) })));
2453
+ }
2454
+ requests.sort((a, b) => a.id - b.id);
2455
+ requests = filterRequests(requests, { grep: flag("--grep"), failed: rest.includes("--failed") });
2456
+ const limit = parseInt(flag("--limit"), 10) || 50;
2457
+ const total = requests.length;
2458
+ requests = requests.slice(-limit);
2459
+ if (json) {
2460
+ console.log(JSON.stringify(requests.map((r) => ({ ...r, path: redactor.path(r.path) })), null, 2));
2461
+ break;
2462
+ }
2463
+ if (!total) {
2464
+ console.log("No matching requests.");
2465
+ break;
2466
+ }
2467
+ console.log(formatList(requests, redactor, { showPort: ports.length > 1 }));
2468
+ if (total > requests.length) console.error(`
2469
+ (${total - requests.length} older not shown; raise --limit)`);
2470
+ console.error("\nDetails: coderemote request <ID>");
2471
+ hint();
2472
+ break;
2473
+ }
2230
2474
  case "version":
2231
2475
  console.log(VERSION);
2232
2476
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderemote",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Use your computer from your iPhone: files, git and a real terminal over a secure Microsoft Dev Tunnel. No SSH, no port forwarding, no VPN.",
5
5
  "keywords": [
6
6
  "remote",