@going-haywire/farmhand4claude 0.1.0 → 0.1.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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +59 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,9 +27,9 @@ All logs go to stderr; stdout carries only MCP frames.
27
27
 
28
28
  You normally don't run this directly — install the plugin and it runs the proxy:
29
29
 
30
- ```
30
+ ```text
31
31
  /plugin marketplace add going-haywire/farmhand4claude
32
- /plugin install farmhand4claude
32
+ /plugin install farmhand4claude@farmhand-marketplace
33
33
  ```
34
34
 
35
35
  ## Configuration (environment variables)
package/dist/index.js CHANGED
@@ -15,9 +15,9 @@
15
15
  * - client.setNotificationHandler(ToolListChangedNotificationSchema) — example simpleStreamableHttp.ts
16
16
  * - stdio MUST NOT write non-MCP data to stdout (spec/transports) — hence all logs -> stderr
17
17
  */
18
- import { readFileSync } from "node:fs";
18
+ import { readFileSync, readdirSync } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { resolve } from "node:path";
20
+ import { join, resolve } from "node:path";
21
21
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
23
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -26,30 +26,61 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
26
26
  // ---- config (env-overridable; sensible Haywire defaults) --------------------
27
27
  const UPSTREAM_URL = process.env.FARMHAND_URL ?? "http://127.0.0.1:8082/mcp";
28
28
  const POLL_MS = Number(process.env.FARMHAND_POLL_MS ?? 2000);
29
+ const TOKEN_REL = join(".haywire", "farmhand_token");
29
30
  /**
30
- * Candidate token paths, in priority order. The token lives at
31
- * `<workspace>/.haywire/farmhand_token`, so the whole game is knowing the
32
- * workspace. Claude Code does NOT run the MCP server with cwd = workspace
33
- * (it inherits the launch dir / $HOME see CC issues #17565, #75266), so we
34
- * do NOT trust process.cwd(). Instead we read the workspace from env vars that
35
- * CC exports to the MCP subprocess and interpolates in plugin.json's env block:
36
- * FARMHAND_TOKEN_PATH (explicit override) > HAYWIRE_WORKSPACE >
37
- * CLAUDE_PROJECT_DIR. homedir() is a last resort for manual/standalone runs.
31
+ * Where to look for the bearer token, in priority order. The token lives at
32
+ * `<project>/.haywire/farmhand_token` the whole game is finding the project.
33
+ *
34
+ * Claude Code does NOT run the MCP server with cwd = workspace (it inherits the
35
+ * launch dir / $HOME CC issues #17565, #75266), so we do NOT trust
36
+ * process.cwd(). We read the workspace from env vars CC exports to the MCP
37
+ * subprocess (interpolated in plugin.json's env block):
38
+ * FARMHAND_TOKEN_PATH (explicit override) > HAYWIRE_WORKSPACE > CLAUDE_PROJECT_DIR
39
+ * with homedir() as a last resort for manual/standalone runs.
40
+ *
41
+ * Crucially, the onboarding flow scaffolds the project into a SUBDIRECTORY of
42
+ * the opened workspace (e.g. workspace `/testbed`, project `/testbed/demo`), so
43
+ * the token is one level DOWN from the workspace, not directly in it. We
44
+ * therefore check the token under `<base>` AND under each immediate subdir of
45
+ * `<base>`.
46
+ *
47
+ * Computed at CALL TIME, not module load: the studio (and its token) may not
48
+ * exist yet when the proxy starts — the project is created mid-session.
38
49
  */
39
50
  function tokenCandidates() {
40
51
  const paths = [];
41
- const add = (base) => {
42
- if (base)
43
- paths.push(resolve(`${base}/.haywire/farmhand_token`));
52
+ const seen = new Set();
53
+ const push = (p) => {
54
+ const r = resolve(p);
55
+ if (!seen.has(r)) {
56
+ seen.add(r);
57
+ paths.push(r);
58
+ }
44
59
  };
45
60
  if (process.env.FARMHAND_TOKEN_PATH)
46
- paths.push(resolve(process.env.FARMHAND_TOKEN_PATH));
47
- add(process.env.HAYWIRE_WORKSPACE);
48
- add(process.env.CLAUDE_PROJECT_DIR);
49
- add(homedir());
61
+ push(process.env.FARMHAND_TOKEN_PATH);
62
+ const bases = [process.env.HAYWIRE_WORKSPACE, process.env.CLAUDE_PROJECT_DIR, homedir()];
63
+ for (const base of bases) {
64
+ if (!base)
65
+ continue;
66
+ push(join(base, TOKEN_REL)); // CC opened directly in the project
67
+ // …and one level down: CC opened in the parent, project in a subdir.
68
+ for (const child of immediateSubdirs(base))
69
+ push(join(base, child, TOKEN_REL));
70
+ }
50
71
  return paths;
51
72
  }
52
- const TOKEN_PATHS = tokenCandidates();
73
+ /** Immediate subdirectory names of `dir` (best-effort; [] on any error). */
74
+ function immediateSubdirs(dir) {
75
+ try {
76
+ return readdirSync(dir, { withFileTypes: true })
77
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
78
+ .map((e) => e.name);
79
+ }
80
+ catch {
81
+ return [];
82
+ }
83
+ }
53
84
  const log = (...a) => console.error("[farmhand-proxy]", ...a); // stderr only
54
85
  // ---- upstream connection state ----------------------------------------------
55
86
  let upstream = null;
@@ -61,7 +92,7 @@ let upstreamResources = [];
61
92
  * return which path it came from (for diagnostics).
62
93
  */
63
94
  function readTokenFrom() {
64
- for (const p of TOKEN_PATHS) {
95
+ for (const p of tokenCandidates()) {
65
96
  try {
66
97
  const t = readFileSync(p, "utf8").trim();
67
98
  if (t.length > 0)
@@ -77,7 +108,7 @@ function readToken() {
77
108
  return readTokenFrom()?.token ?? null;
78
109
  }
79
110
  /** The stdio-facing server Claude Code talks to. Created first, always up. */
80
- const proxy = new Server({ name: "farmhand-mcp-server", version: "0.1.0" }, { capabilities: { tools: { listChanged: true }, resources: { listChanged: true } } });
111
+ const proxy = new Server({ name: "farmhand-mcp-server", version: "0.1.1" }, { capabilities: { tools: { listChanged: true }, resources: { listChanged: true } } });
81
112
  // Sentinel so the model has an affordance while the studio is down.
82
113
  const STATUS_TOOL = {
83
114
  name: "farmhand_studio_status",
@@ -107,7 +138,7 @@ proxy.setRequestHandler(CallToolRequestSchema, async (req) => {
107
138
  : `Haywire studio not running (no connection to ${UPSTREAM_URL}). ` +
108
139
  (found
109
140
  ? `Token present (${found.path}).`
110
- : `No token found. Looked in: ${TOKEN_PATHS.join(", ")}.`);
141
+ : `No token found. Looked in: ${tokenCandidates().join(", ")}.`);
111
142
  return { content: [{ type: "text", text }], structuredContent: { up, url: UPSTREAM_URL } };
112
143
  }
113
144
  if (!upstream) {
@@ -129,7 +160,7 @@ async function tryConnectUpstream() {
129
160
  const transport = new StreamableHTTPClientTransport(new URL(UPSTREAM_URL), {
130
161
  requestInit: token ? { headers: { Authorization: `Bearer ${token}` } } : undefined,
131
162
  });
132
- const client = new Client({ name: "farmhand-proxy-client", version: "0.1.0" });
163
+ const client = new Client({ name: "farmhand-proxy-client", version: "0.1.1" });
133
164
  // Re-emit upstream list_changed across the stdio boundary — the whole point.
134
165
  client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
135
166
  await refreshTools();
@@ -211,7 +242,12 @@ async function refreshResources() {
211
242
  // ---- main -------------------------------------------------------------------
212
243
  async function main() {
213
244
  await proxy.connect(new StdioServerTransport());
214
- log(`up. bridging stdio -> ${UPSTREAM_URL} (poll ${POLL_MS}ms). token candidates: ${TOKEN_PATHS.join(", ")}`);
245
+ const bases = [process.env.HAYWIRE_WORKSPACE, process.env.CLAUDE_PROJECT_DIR, homedir()]
246
+ .filter(Boolean)
247
+ .join(", ");
248
+ log(`up. bridging stdio -> ${UPSTREAM_URL} (poll ${POLL_MS}ms). ` +
249
+ `token search bases (+ their immediate subdirs): ${bases}` +
250
+ (process.env.FARMHAND_TOKEN_PATH ? ` [override: ${process.env.FARMHAND_TOKEN_PATH}]` : ""));
215
251
  // Poll so tools auto-appear the moment the studio comes up mid-session,
216
252
  // and disappear the moment it dies.
217
253
  const tick = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@going-haywire/farmhand4claude",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Farmhand — the AI harness for Haywire. A stdio↔HTTP MCP proxy that bridges Claude Code to a running Haywire studio, so studio tools appear mid-session with no reconnect.",
5
5
  "type": "module",
6
6
  "bin": {