Skip to content

Commit 53242b4

Browse files
authored
feat(daemon): standardize daemon naming around hunk daemon serve (#197)
1 parent f6de1cb commit 53242b4

38 files changed

+207
-158
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ CLI input
2626
- All input sources normalize into one internal changeset model.
2727
- Pager mode has two paths: full diff UI for patch-like stdin, plain-text fallback for non-diff pager content.
2828
- View defaults are layered through built-ins, user config, repo `.hunk/config.toml`, command sections, pager sections, and CLI flags.
29-
- `hunk mcp serve` runs one loopback daemon that brokers agent commands to many live Hunk sessions. Normal Hunk sessions should auto-start and register with that daemon when MCP is enabled. Keep it local-only and session-brokered rather than opening per-TUI ports.
29+
- `hunk daemon serve` runs one loopback daemon that brokers agent commands to many live Hunk sessions. Normal Hunk sessions should auto-start and register with that daemon when session brokering is enabled. Keep it local-only and session-brokered rather than opening per-TUI ports.
3030
- Agent rationale is optional sidecar JSON matched onto files/hunks.
3131
- The order of `files` in the sidecar is intentional. Hunk uses that order for the sidebar and main review stream.
3232
- Prefer one source of truth for each user-visible behavior. When rendering, navigation, scrolling, or note placement share the same model, derive them from the same planning layer rather than maintaining parallel implementations.

src/core/cli.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,19 @@ describe("parseCli", () => {
215215
});
216216
});
217217

218-
test("parses the MCP daemon command", async () => {
218+
test("parses the daemon serve command", async () => {
219+
const parsed = await parseCli(["bun", "hunk", "daemon", "serve"]);
220+
221+
expect(parsed).toEqual({
222+
kind: "daemon-serve",
223+
});
224+
});
225+
226+
test("parses the legacy MCP daemon alias", async () => {
219227
const parsed = await parseCli(["bun", "hunk", "mcp", "serve"]);
220228

221229
expect(parsed).toEqual({
222-
kind: "mcp-serve",
230+
kind: "daemon-serve",
223231
});
224232
});
225233

src/core/cli.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function renderCliHelp() {
136136
" hunk difftool <left> <right> [path] review Git difftool file pairs",
137137
" hunk session <subcommand> inspect or control a live Hunk session",
138138
" hunk skill path print the bundled Hunk review skill path",
139-
" hunk mcp serve run the local Hunk session daemon",
139+
" hunk daemon serve run the local Hunk session daemon",
140140
"",
141141
"Global options:",
142142
" -h, --help show help",
@@ -555,7 +555,7 @@ async function parseDifftoolCommand(tokens: string[], argv: string[]): Promise<P
555555
}
556556

557557
function requireReloadableCliInput(input: ParsedCliInput): CliInput {
558-
if (input.kind === "help" || input.kind === "pager" || input.kind === "mcp-serve") {
558+
if (input.kind === "help" || input.kind === "pager" || input.kind === "daemon-serve") {
559559
throw new Error(
560560
"Session reload requires a Hunk review command after --, such as `diff` or `show`.",
561561
);
@@ -1190,15 +1190,15 @@ async function parseSkillCommand(tokens: string[]): Promise<HelpCommandInput> {
11901190
};
11911191
}
11921192

1193-
/** Parse `hunk mcp serve` as the local daemon entrypoint. */
1194-
async function parseMcpCommand(tokens: string[]): Promise<ParsedCliInput> {
1193+
/** Parse `hunk daemon serve` as the canonical local daemon entrypoint. */
1194+
async function parseDaemonCommand(tokens: string[]): Promise<ParsedCliInput> {
11951195
const [subcommand, ...rest] = tokens;
11961196
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
11971197
return {
11981198
kind: "help",
11991199
text:
12001200
[
1201-
"Usage: hunk mcp serve",
1201+
"Usage: hunk daemon serve",
12021202
"",
12031203
"Run the local Hunk session daemon and websocket session broker.",
12041204
"",
@@ -1211,23 +1211,23 @@ async function parseMcpCommand(tokens: string[]): Promise<ParsedCliInput> {
12111211
}
12121212

12131213
if (subcommand !== "serve") {
1214-
throw new Error("Only `hunk mcp serve` is supported.");
1214+
throw new Error("Only `hunk daemon serve` is supported.");
12151215
}
12161216

12171217
if (rest.includes("--help") || rest.includes("-h")) {
12181218
return {
12191219
kind: "help",
12201220
text:
12211221
[
1222-
"Usage: hunk mcp serve",
1222+
"Usage: hunk daemon serve",
12231223
"",
12241224
"Run the local Hunk session daemon and websocket session broker.",
12251225
].join("\n") + "\n",
12261226
};
12271227
}
12281228

12291229
return {
1230-
kind: "mcp-serve",
1230+
kind: "daemon-serve",
12311231
};
12321232
}
12331233

@@ -1309,8 +1309,9 @@ export async function parseCli(argv: string[]): Promise<ParsedCliInput> {
13091309
return parseSessionCommand(rest);
13101310
case "skill":
13111311
return parseSkillCommand(rest);
1312+
case "daemon":
13121313
case "mcp":
1313-
return parseMcpCommand(rest);
1314+
return parseDaemonCommand(rest);
13141315
default:
13151316
throw new Error(`Unknown command: ${commandName}`);
13161317
}

src/core/liveComments.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Hunk } from "@pierre/diffs";
22
import type { DiffFile } from "./types";
3-
import type { CommentTargetInput, DiffSide, LiveComment } from "../mcp/types";
3+
import type { CommentTargetInput, DiffSide, LiveComment } from "../daemon/types";
44

55
export interface ResolvedCommentTarget {
66
hunkIndex: number;
@@ -114,7 +114,7 @@ export function resolveCommentTarget(
114114
};
115115
}
116116

117-
/** Convert one incoming MCP comment command into a live annotation. */
117+
/** Convert one incoming session-daemon comment command into a live annotation. */
118118
export function buildLiveComment(
119119
input: CommentTargetInput & { side: DiffSide; line: number },
120120
commentId: string,

src/core/startup.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ describe("startup planning", () => {
3232
expect(loaded).toBe(false);
3333
});
3434

35-
test("passes the MCP serve command through without app bootstrap work", async () => {
35+
test("passes the daemon serve command through without app bootstrap work", async () => {
3636
let loaded = false;
3737

38-
const plan = await prepareStartupPlan(["bun", "hunk", "mcp", "serve"], {
39-
parseCliImpl: async () => ({ kind: "mcp-serve" }),
38+
const plan = await prepareStartupPlan(["bun", "hunk", "daemon", "serve"], {
39+
parseCliImpl: async () => ({ kind: "daemon-serve" }),
4040
loadAppBootstrapImpl: async () => {
4141
loaded = true;
4242
throw new Error("unreachable");
4343
},
4444
});
4545

46-
expect(plan).toEqual({ kind: "mcp-serve" });
46+
expect(plan).toEqual({ kind: "daemon-serve" });
4747
expect(loaded).toBe(false);
4848
});
4949

src/core/startup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type StartupPlan =
1818
text: string;
1919
}
2020
| {
21-
kind: "mcp-serve";
21+
kind: "daemon-serve";
2222
}
2323
| {
2424
kind: "session-command";
@@ -70,9 +70,9 @@ export async function prepareStartupPlan(
7070
};
7171
}
7272

73-
if (parsedCliInput.kind === "mcp-serve") {
73+
if (parsedCliInput.kind === "daemon-serve") {
7474
return {
75-
kind: "mcp-serve",
75+
kind: "daemon-serve",
7676
};
7777
}
7878

src/core/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ export interface PagerCommandInput {
8484
options: CommonOptions;
8585
}
8686

87-
export interface McpServeCommandInput {
88-
kind: "mcp-serve";
87+
export interface DaemonServeCommandInput {
88+
kind: "daemon-serve";
8989
}
9090

9191
export type SessionCommandOutput = "text" | "json";
@@ -263,7 +263,7 @@ export type ParsedCliInput =
263263
| CliInput
264264
| HelpCommandInput
265265
| PagerCommandInput
266-
| McpServeCommandInput
266+
| DaemonServeCommandInput
267267
| SessionCommandInput;
268268

269269
export interface AppBootstrap {
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
createTestSessionRegistration,
55
createTestSessionReviewFile,
66
createTestSessionSnapshot,
7-
} from "../../test/helpers/mcp-fixtures";
7+
} from "../../test/helpers/session-daemon-fixtures";
88
import { HUNK_SESSION_API_VERSION, HUNK_SESSION_DAEMON_VERSION } from "../session/protocol";
99
import { HunkHostClient } from "./client";
1010

@@ -75,8 +75,8 @@ afterEach(() => {
7575
console.error = originalConsoleError;
7676
});
7777

78-
describe("Hunk MCP client", () => {
79-
test("logs one actionable warning when MCP is configured for a non-loopback host without opt-in", async () => {
78+
describe("Hunk session daemon client", () => {
79+
test("logs one actionable warning when the session daemon is configured for a non-loopback host without opt-in", async () => {
8080
process.env.HUNK_MCP_HOST = "0.0.0.0";
8181
process.env.HUNK_MCP_PORT = "47657";
8282
delete process.env.HUNK_MCP_UNSAFE_ALLOW_REMOTE;
@@ -91,10 +91,10 @@ describe("Hunk MCP client", () => {
9191

9292
try {
9393
client.start();
94-
await waitUntil("non-loopback MCP warning", () => messages.length === 1);
94+
await waitUntil("non-loopback session-daemon warning", () => messages.length === 1);
9595

9696
expect(messages[0]).toContain(
97-
"[hunk:mcp] Hunk MCP refuses to bind 0.0.0.0:47657 because the daemon is local-only by default.",
97+
"[hunk:session] Hunk session daemon refuses to bind 0.0.0.0:47657 because the daemon is local-only by default.",
9898
);
9999
expect(messages[0]).toContain("HUNK_MCP_UNSAFE_ALLOW_REMOTE=1");
100100
} finally {
@@ -228,7 +228,7 @@ describe("Hunk MCP client", () => {
228228
}
229229
}, 10_000);
230230

231-
test("logs one actionable warning when a non-Hunk listener owns the MCP port", async () => {
231+
test("logs one actionable warning when a non-Hunk listener owns the session daemon port", async () => {
232232
const conflictingListener = createServer((_request, response) => {
233233
response.writeHead(404, { "content-type": "text/plain" });
234234
response.end("not hunk");
@@ -253,14 +253,14 @@ describe("Hunk MCP client", () => {
253253

254254
try {
255255
client.start();
256-
await waitUntil("initial MCP conflict warning", () => messages.length === 1);
256+
await waitUntil("initial session-daemon conflict warning", () => messages.length === 1);
257257

258258
client.start();
259259
await Bun.sleep(2_000);
260260

261261
expect(messages).toHaveLength(1);
262262
expect(messages[0]).toContain(
263-
`[hunk:mcp] Hunk MCP port 127.0.0.1:${port} is already in use by another process.`,
263+
`[hunk:session] Hunk session daemon port 127.0.0.1:${port} is already in use by another process.`,
264264
);
265265
expect(messages[0]).toContain(
266266
"Stop the conflicting process or set HUNK_MCP_PORT to a different loopback port.",
Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import type {
1313
} from "./types";
1414
import {
1515
HUNK_SESSION_SOCKET_PATH,
16-
resolveHunkMcpConfig,
17-
type ResolvedHunkMcpConfig,
16+
resolveHunkSessionDaemonConfig,
17+
type ResolvedHunkSessionDaemonConfig,
1818
} from "./config";
1919
import {
2020
ensureHunkDaemonAvailable,
@@ -55,7 +55,7 @@ interface HunkAppBridge {
5555
) => Promise<ClearedCommentsResult>;
5656
}
5757

58-
/** Keep one running Hunk TUI session registered with the local MCP daemon. */
58+
/** Keep one running Hunk TUI session registered with the local session daemon. */
5959
export class HunkHostClient {
6060
private websocket: WebSocket | null = null;
6161
private bridge: HunkAppBridge | null = null;
@@ -121,7 +121,7 @@ export class HunkHostClient {
121121
}
122122

123123
private resolveConfig() {
124-
return resolveHunkMcpConfig();
124+
return resolveHunkSessionDaemonConfig();
125125
}
126126

127127
private async ensureDaemonAndConnect() {
@@ -130,7 +130,7 @@ export class HunkHostClient {
130130
this.connect(config);
131131
}
132132

133-
private async ensureDaemonAvailable(config: ResolvedHunkMcpConfig) {
133+
private async ensureDaemonAvailable(config: ResolvedHunkSessionDaemonConfig) {
134134
await ensureHunkDaemonAvailable({
135135
config,
136136
timeoutMs: DAEMON_STARTUP_TIMEOUT_MS,
@@ -155,7 +155,7 @@ export class HunkHostClient {
155155
this.lastConnectionWarning = null;
156156
}
157157

158-
private async restartIncompatibleDaemon(config: ResolvedHunkMcpConfig) {
158+
private async restartIncompatibleDaemon(config: ResolvedHunkSessionDaemonConfig) {
159159
reportHunkDaemonUpgradeRestart();
160160
const health = await readHunkDaemonHealth(config);
161161
const pid = health?.pid;
@@ -205,7 +205,7 @@ export class HunkHostClient {
205205
});
206206
}
207207

208-
private connect(config: ResolvedHunkMcpConfig) {
208+
private connect(config: ResolvedHunkSessionDaemonConfig) {
209209
if (this.stopped || this.websocket) {
210210
return;
211211
}
@@ -331,7 +331,7 @@ export class HunkHostClient {
331331

332332
private dispatchCommand(message: SessionServerMessage): Promise<SessionCommandResult> {
333333
if (!this.bridge) {
334-
throw new Error("Hunk MCP bridge is not connected.");
334+
throw new Error("Hunk session bridge is not connected.");
335335
}
336336

337337
switch (message.command) {
@@ -372,12 +372,13 @@ export class HunkHostClient {
372372
}
373373

374374
private warnUnavailable(error: unknown) {
375-
const message = error instanceof Error ? error.message : "Unknown Hunk MCP connection error.";
375+
const message =
376+
error instanceof Error ? error.message : "Unknown Hunk session daemon connection error.";
376377
if (message === this.lastConnectionWarning) {
377378
return;
378379
}
379380

380381
this.lastConnectionWarning = message;
381-
console.error(`[hunk:mcp] ${message}`);
382+
console.error(`[hunk:session] ${message}`);
382383
}
383384
}
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { describe, expect, test } from "bun:test";
22
import {
33
HUNK_MCP_UNSAFE_ALLOW_REMOTE_ENV,
4-
allowsUnsafeRemoteMcp,
4+
allowsUnsafeRemoteSessionDaemon,
55
isLoopbackHost,
6-
resolveHunkMcpConfig,
6+
resolveHunkSessionDaemonConfig,
77
} from "./config";
88

9-
describe("Hunk MCP config", () => {
9+
describe("Hunk session daemon config", () => {
1010
test("accepts loopback hosts without an unsafe override", () => {
1111
expect(isLoopbackHost("127.0.0.1")).toBe(true);
1212
expect(isLoopbackHost("127.1.2.3")).toBe(true);
@@ -21,14 +21,14 @@ describe("Hunk MCP config", () => {
2121

2222
test("refuses non-loopback binds unless the unsafe override is enabled", () => {
2323
expect(() =>
24-
resolveHunkMcpConfig({
24+
resolveHunkSessionDaemonConfig({
2525
HUNK_MCP_HOST: "0.0.0.0",
2626
HUNK_MCP_PORT: "49000",
2727
}),
2828
).toThrow("local-only by default");
2929

3030
expect(
31-
resolveHunkMcpConfig({
31+
resolveHunkSessionDaemonConfig({
3232
HUNK_MCP_HOST: "0.0.0.0",
3333
HUNK_MCP_PORT: "49000",
3434
[HUNK_MCP_UNSAFE_ALLOW_REMOTE_ENV]: "1",
@@ -39,9 +39,11 @@ describe("Hunk MCP config", () => {
3939
});
4040
});
4141

42-
test("reports whether unsafe remote MCP access was explicitly enabled", () => {
43-
expect(allowsUnsafeRemoteMcp({})).toBe(false);
44-
expect(allowsUnsafeRemoteMcp({ [HUNK_MCP_UNSAFE_ALLOW_REMOTE_ENV]: "0" })).toBe(false);
45-
expect(allowsUnsafeRemoteMcp({ [HUNK_MCP_UNSAFE_ALLOW_REMOTE_ENV]: "1" })).toBe(true);
42+
test("reports whether unsafe remote session-daemon access was explicitly enabled", () => {
43+
expect(allowsUnsafeRemoteSessionDaemon({})).toBe(false);
44+
expect(allowsUnsafeRemoteSessionDaemon({ [HUNK_MCP_UNSAFE_ALLOW_REMOTE_ENV]: "0" })).toBe(
45+
false,
46+
);
47+
expect(allowsUnsafeRemoteSessionDaemon({ [HUNK_MCP_UNSAFE_ALLOW_REMOTE_ENV]: "1" })).toBe(true);
4648
});
4749
});

0 commit comments

Comments
 (0)