@hua-labs/tap 0.5.1 → 0.5.2

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
@@ -1,27 +1,30 @@
1
1
  # @hua-labs/tap
2
2
 
3
- Zero-dependency CLI for cross-model AI agent communication setup.
3
+ `tap` is a CLI that turns your repo into a shared workspace for Claude, Codex, and Gemini (experimental) so multiple AI agents can coordinate on the same codebase without custom glue code.
4
4
 
5
- One command to connect Claude, Codex, and Gemini agents through a shared file-based communication layer.
5
+ ## Why Would I Use It?
6
+
7
+ - You use more than one coding agent and want them to share context without copy-pasting prompts between tools.
8
+ - You want reviews, handoffs, and agent-to-agent messages to live in files inside the repo instead of hidden app state.
9
+ - You want a working multi-agent setup in minutes instead of hand-editing MCP configs and bridge processes yourself.
6
10
 
7
11
  ## Quick Start
8
12
 
9
- > `npx @hua-labs/tap` ships a bundled managed MCP server entry and runs that bundled `.mjs` with `node`. `bun` is only required when tap falls back to repo-local TypeScript sources during monorepo or local-dev workflows.
13
+ Try it in a fresh repo:
10
14
 
11
15
  ```bash
12
- # 1. Initialize comms directory and state
13
16
  npx @hua-labs/tap init
14
-
15
- # 2. Add runtimes
16
17
  npx @hua-labs/tap add claude
17
18
  npx @hua-labs/tap add codex
18
- npx @hua-labs/tap add gemini
19
-
20
- # 3. Check status
19
+ npx @hua-labs/tap add gemini # experimental
21
20
  npx @hua-labs/tap status
22
21
  ```
23
22
 
24
- Your agents can now communicate through the shared comms directory.
23
+ This creates a shared comms/state layer and wires supported runtimes into it.
24
+
25
+ Gemini support is currently experimental and polling-only.
26
+
27
+ > `npx @hua-labs/tap` ships a bundled managed MCP server entry and runs that bundled `.mjs` with `node`. `bun` is only required when tap falls back to repo-local TypeScript sources during monorepo or local-dev workflows.
25
28
 
26
29
  ## Commands
27
30
 
@@ -46,7 +49,7 @@ Add a runtime. Probes config, plans patches, applies, and verifies.
46
49
  ```bash
47
50
  npx @hua-labs/tap add claude
48
51
  npx @hua-labs/tap add codex
49
- npx @hua-labs/tap add gemini
52
+ npx @hua-labs/tap add gemini # experimental
50
53
  npx @hua-labs/tap add claude --force # re-install
51
54
  ```
52
55
 
@@ -99,7 +102,7 @@ For npm installs, `serve` runs the bundled `mcp-server.mjs` entry with `node`. I
99
102
  | ------- | ----------------------- | ---------------------- | ------------------ |
100
103
  | Claude | `.mcp.json` | native-push (fs.watch) | No daemon needed |
101
104
  | Codex | `~/.codex/config.toml` | WebSocket bridge | Daemon per session |
102
- | Gemini | `.gemini/settings.json` | polling | No daemon needed |
105
+ | Gemini (experimental) | `.gemini/settings.json` | polling | No daemon needed |
103
106
 
104
107
  ## `--json` Flag
105
108
 
package/dist/cli.mjs CHANGED
@@ -1432,16 +1432,22 @@ import * as os2 from "os";
1432
1432
  import * as path8 from "path";
1433
1433
  import { spawnSync as spawnSync2 } from "child_process";
1434
1434
  import { fileURLToPath as fileURLToPath2 } from "url";
1435
+ function resolveProbeCommand(candidate) {
1436
+ return resolveCommandPath(candidate) ?? candidate;
1437
+ }
1438
+ function probeCommandVersion(command) {
1439
+ return spawnSync2(command, ["--version"], {
1440
+ encoding: "utf-8",
1441
+ windowsHide: true
1442
+ });
1443
+ }
1435
1444
  function probeCommand(candidates) {
1436
1445
  for (const candidate of candidates) {
1437
- const result = spawnSync2(candidate, ["--version"], {
1438
- encoding: "utf-8",
1439
- shell: process.platform === "win32"
1440
- });
1446
+ const resolvedCommand = resolveProbeCommand(candidate);
1447
+ const result = probeCommandVersion(resolvedCommand);
1441
1448
  if (result.status === 0) {
1442
1449
  const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
1443
- const absolutePath = resolveCommandPath(candidate);
1444
- return { command: absolutePath ?? candidate, version: version2 };
1450
+ return { command: resolvedCommand, version: version2 };
1445
1451
  }
1446
1452
  }
1447
1453
  return { command: null, version: null };
@@ -1543,19 +1549,16 @@ function findPreferredBunCommand() {
1543
1549
  const candidates = process.platform === "win32" ? [path8.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path8.join(home, ".bun", "bin", "bun"), "bun"];
1544
1550
  for (const candidate of candidates) {
1545
1551
  if (path8.isAbsolute(candidate) && !fs9.existsSync(candidate)) continue;
1546
- const result = spawnSync2(candidate, ["--version"], {
1547
- encoding: "utf-8",
1548
- shell: process.platform === "win32"
1549
- });
1552
+ const resolvedCommand = resolveProbeCommand(candidate);
1553
+ const result = probeCommandVersion(resolvedCommand);
1550
1554
  if (result.status === 0) {
1551
- return path8.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
1555
+ return path8.isAbsolute(resolvedCommand) ? toForwardSlashPath(resolvedCommand) : resolvedCommand;
1552
1556
  }
1553
1557
  }
1554
1558
  return null;
1555
1559
  }
1556
1560
  function buildManagedMcpServerSpec(ctx, instanceId) {
1557
1561
  const sourcePath = findTapCommsServerEntry(ctx);
1558
- const bunCommand = findPreferredBunCommand();
1559
1562
  const warnings = [];
1560
1563
  const issues = [];
1561
1564
  const env = {
@@ -1575,7 +1578,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1575
1578
  }
1576
1579
  const isBundled = sourcePath.endsWith(".mjs");
1577
1580
  const isEphemeralSource = isEphemeralPath(sourcePath);
1578
- let command = null;
1581
+ let command;
1579
1582
  let args = [toForwardSlashPath(sourcePath)];
1580
1583
  if (isEphemeralSource && isBundled) {
1581
1584
  command = "npx";
@@ -1589,7 +1592,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1589
1592
  );
1590
1593
  command = nodeProbe.command ?? "node";
1591
1594
  } else {
1592
- command = bunCommand;
1595
+ command = findPreferredBunCommand();
1593
1596
  }
1594
1597
  if (!command) {
1595
1598
  issues.push(
@@ -8463,6 +8466,38 @@ function sameCommandToken(left, right) {
8463
8466
  function sameStringArray(left, right) {
8464
8467
  return left.length === right.length && left.every((value, index) => sameCommandToken(value, right[index] ?? ""));
8465
8468
  }
8469
+ function normalizeCommandBasename(command) {
8470
+ const token = command.split(/[\\/]/).pop() ?? command;
8471
+ return token.toLowerCase().replace(/\.(cmd|exe|ps1|bat)$/i, "");
8472
+ }
8473
+ function findFirstLauncherTarget(args) {
8474
+ for (const arg of args) {
8475
+ if (!arg || arg === "--" || arg.startsWith("-")) {
8476
+ continue;
8477
+ }
8478
+ return arg;
8479
+ }
8480
+ return null;
8481
+ }
8482
+ function looksLikePackageSpecifier(value) {
8483
+ const normalized = value.trim();
8484
+ if (!normalized || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("/") || normalized.startsWith("\\") || normalized.startsWith(".") || /\.(?:[cm]?js|tsx?|json|ps1|cmd|exe)$/i.test(normalized)) {
8485
+ return false;
8486
+ }
8487
+ return /^(?:@[^/\\]+\/)?[A-Za-z0-9][A-Za-z0-9._-]*(?:@[A-Za-z0-9][A-Za-z0-9._.-]*)?$/.test(
8488
+ normalized
8489
+ );
8490
+ }
8491
+ function getNpxPackageLauncher(command, args) {
8492
+ if (normalizeCommandBasename(command) !== "npx") {
8493
+ return null;
8494
+ }
8495
+ const packageName = findFirstLauncherTarget(args);
8496
+ if (!packageName || !looksLikePackageSpecifier(packageName)) {
8497
+ return null;
8498
+ }
8499
+ return [command, ...args].join(" ");
8500
+ }
8466
8501
  function appendWarningMessage(message, extra) {
8467
8502
  return message.includes(extra) ? message : `${message}; ${extra}`;
8468
8503
  }
@@ -8918,10 +8953,11 @@ function checkMessageLifecycle(commsDir) {
8918
8953
  const total = countFiles(inbox);
8919
8954
  const recent1h = recentFileCount(inbox, 60 * 60 * 1e3);
8920
8955
  const recent10m = recentFileCount(inbox, 10 * 60 * 1e3);
8956
+ const messageSummary = `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`;
8921
8957
  checks.push({
8922
8958
  name: "message flow",
8923
- status: recent10m > 0 ? PASS : total > 0 ? WARN : FAIL,
8924
- message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
8959
+ status: recent10m > 0 ? PASS : WARN,
8960
+ message: total === 0 ? `${messageSummary} (expected before first exchange)` : messageSummary
8925
8961
  });
8926
8962
  const receiptsPath = join28(commsDir, "receipts", "receipts.json");
8927
8963
  if (existsSync29(receiptsPath)) {
@@ -9001,15 +9037,7 @@ function checkMcpServer(repoRoot) {
9001
9037
  const cmd = hasTapComms.command;
9002
9038
  let cmdAvailable = existsSync29(cmd);
9003
9039
  if (!cmdAvailable) {
9004
- try {
9005
- const result = spawnSync6(cmd, ["--version"], {
9006
- stdio: "pipe",
9007
- timeout: 5e3,
9008
- shell: process.platform === "win32"
9009
- });
9010
- cmdAvailable = result.status === 0;
9011
- } catch {
9012
- }
9040
+ cmdAvailable = probeCommand([cmd]).command !== null;
9013
9041
  }
9014
9042
  checks.push({
9015
9043
  name: "MCP command binary",
@@ -9017,7 +9045,14 @@ function checkMcpServer(repoRoot) {
9017
9045
  message: cmdAvailable ? cmd : `Not found: ${cmd} (checked PATH and absolute)`
9018
9046
  });
9019
9047
  }
9020
- if (hasTapComms.args?.[0]) {
9048
+ const npxPackageLauncher = hasTapComms.command && hasTapComms.args ? getNpxPackageLauncher(hasTapComms.command, hasTapComms.args) : null;
9049
+ if (npxPackageLauncher) {
9050
+ checks.push({
9051
+ name: "MCP server script",
9052
+ status: PASS,
9053
+ message: `Package launcher: ${npxPackageLauncher}`
9054
+ });
9055
+ } else if (hasTapComms.args?.[0]) {
9021
9056
  const mcpScript = hasTapComms.args[0];
9022
9057
  checks.push({
9023
9058
  name: "MCP server script",
@@ -9483,7 +9518,7 @@ async function doctorCommand(args) {
9483
9518
  message: `${passes} passed, ${warns} warnings, ${fails} failures`,
9484
9519
  warnings: finalChecks.filter((c) => c.status === "warn").map((c) => `${c.name}: ${c.message}`),
9485
9520
  data: {
9486
- checks: finalChecks.map(({ fix, ...rest }) => rest),
9521
+ checks: finalChecks.map(({ fix: _fix, ...rest }) => rest),
9487
9522
  summary: { total: finalChecks.length, passes, warns, fails },
9488
9523
  fixed
9489
9524
  }