@achieveai/hitl-mcp-server 2.11.0 → 2.12.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/dist/config.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { HitlConfig } from './types.js';
2
2
  /**
3
- * Read the HITL config from ~/.hitl/config.json.
3
+ * Read the HITL config from this install's config.json (see hitlDir).
4
4
  * Throws with a helpful message if the file doesn't exist.
5
5
  */
6
6
  export declare function loadConfig(): HitlConfig;
@@ -9,7 +9,7 @@ export declare function loadConfig(): HitlConfig;
9
9
  */
10
10
  export declare function generateDefaultConfig(): HitlConfig;
11
11
  /**
12
- * Save a config object to ~/.hitl/config.json.
12
+ * Save a config object to this install's config.json (see hitlDir).
13
13
  */
14
14
  export declare function saveConfig(config: HitlConfig): void;
15
15
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C;;;GAGG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAuBvC;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,UAAU,CAQlD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAGnD;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAmC7C;;;GAGG;AACH,wBAAgB,UAAU,IAAI,UAAU,CAyBvC;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,UAAU,CASlD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAcnD;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
package/dist/config.js CHANGED
@@ -1,25 +1,54 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from 'fs';
2
2
  import { homedir, hostname } from 'os';
3
3
  import path from 'path';
4
4
  import crypto from 'crypto';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
  import { DEFAULT_NTFY_URL } from './types.js';
7
- const CONFIG_DIR = path.join(homedir(), '.hitl');
8
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
7
  /**
10
- * Read the HITL config from ~/.hitl/config.json.
8
+ * Root of this install's state, mirroring `hitl_transport::paths::hitl_dir()`.
9
+ *
10
+ * `HITL_HOME` has to be honoured here, not just in Rust. The Rust side resolves
11
+ * `config.json` through `hitl_dir()`, while this — the only thing that ever
12
+ * *writes* that file, via `hitl init` — used to hardcode `~/.hitl`. Under
13
+ * `HITL_HOME` the two languages then read and wrote different files, and the
14
+ * Rust binaries answered with "Run 'hitl init' to create one", advice that
15
+ * rewrites the same wrong path forever. `ntfy-transport.ts` and
16
+ * `snapshot-store.ts` already resolve their subdirectories this way; config was
17
+ * the odd one out.
18
+ *
19
+ * Read per call rather than at import so tests and a relaunched process both
20
+ * see the current environment. Empty is rejected rather than treated as unset,
21
+ * matching the Rust rule — an empty value there would silently mean the process
22
+ * working directory.
23
+ */
24
+ function hitlDir() {
25
+ const override = process.env.HITL_HOME;
26
+ if (override !== undefined) {
27
+ if (override.trim() === '') {
28
+ throw new Error('HITL_HOME is set but empty; unset it or give it a real path');
29
+ }
30
+ return override;
31
+ }
32
+ return path.join(homedir(), '.hitl');
33
+ }
34
+ function configFile() {
35
+ return path.join(hitlDir(), 'config.json');
36
+ }
37
+ /**
38
+ * Read the HITL config from this install's config.json (see hitlDir).
11
39
  * Throws with a helpful message if the file doesn't exist.
12
40
  */
13
41
  export function loadConfig() {
14
- if (!existsSync(CONFIG_FILE)) {
15
- throw new Error(`HITL config not found at ${CONFIG_FILE}\n` +
42
+ const file = configFile();
43
+ if (!existsSync(file)) {
44
+ throw new Error(`HITL config not found at ${file}\n` +
16
45
  `Run "hitl init" to create one, or manually create the file with:\n` +
17
46
  JSON.stringify(generateDefaultConfig(), null, 2));
18
47
  }
19
- const raw = readFileSync(CONFIG_FILE, 'utf-8');
48
+ const raw = readFileSync(file, 'utf-8');
20
49
  const parsed = JSON.parse(raw);
21
50
  if (!parsed.topicId || typeof parsed.topicId !== 'string') {
22
- throw new Error(`Invalid config: "topicId" is required in ${CONFIG_FILE}`);
51
+ throw new Error(`Invalid config: "topicId" is required in ${file}`);
23
52
  }
24
53
  return {
25
54
  topicId: parsed.topicId,
@@ -27,6 +56,7 @@ export function loadConfig() {
27
56
  deviceName: parsed.deviceName || hostname(),
28
57
  soundEnabled: parsed.soundEnabled !== false,
29
58
  encryptionKey: parsed.encryptionKey || undefined,
59
+ identityEnabled: parsed.identityEnabled !== false,
30
60
  };
31
61
  }
32
62
  /**
@@ -39,19 +69,32 @@ export function generateDefaultConfig() {
39
69
  deviceName: hostname(),
40
70
  soundEnabled: true,
41
71
  encryptionKey: crypto.randomBytes(32).toString('hex'),
72
+ identityEnabled: true,
42
73
  };
43
74
  }
44
75
  /**
45
- * Save a config object to ~/.hitl/config.json.
76
+ * Save a config object to this install's config.json (see hitlDir).
46
77
  */
47
78
  export function saveConfig(config) {
48
- mkdirSync(CONFIG_DIR, { recursive: true });
49
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf-8');
79
+ mkdirSync(hitlDir(), { recursive: true });
80
+ // Keep the outgoing config. A regenerated default silently swaps in a
81
+ // public ntfyUrl, a fresh topic and a fresh encryption key; without this
82
+ // copy a self-hosted setup is unrecoverable once the original is gone.
83
+ const target = configFile();
84
+ if (existsSync(target)) {
85
+ try {
86
+ copyFileSync(target, `${target}.bak`);
87
+ }
88
+ catch {
89
+ // A backup that cannot be written must not block saving the config.
90
+ }
91
+ }
92
+ writeFileSync(target, JSON.stringify(config, null, 2) + '\n', 'utf-8');
50
93
  }
51
94
  /**
52
95
  * Get the path to the config file.
53
96
  */
54
97
  export function getConfigPath() {
55
- return CONFIG_FILE;
98
+ return configFile();
56
99
  }
57
100
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACvC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AACjD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAEzD;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,4BAA4B,WAAW,IAAI;YAC3C,oEAAoE;YACpE,IAAI,CAAC,SAAS,CAAC,qBAAqB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CACjD,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAwB,CAAC;IAEtD,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,4CAA4C,WAAW,EAAE,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,gBAAgB;QAC3C,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,QAAQ,EAAE;QAC3C,YAAY,EAAE,MAAM,CAAC,YAAY,KAAK,KAAK;QAC3C,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,SAAS;KACjD,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,OAAO,EAAE,QAAQ,MAAM,EAAE,EAAE;QAC3B,OAAO,EAAE,gBAAgB;QACzB,UAAU,EAAE,QAAQ,EAAE;QACtB,YAAY,EAAE,IAAI;QAClB,aAAa,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;KACtD,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,MAAkB;IAC3C,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AAC9E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,WAAW,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,18 @@ import type { RepoContext } from './types.js';
8
8
  * Returns null gracefully if git is unavailable or the directory is not a repo.
9
9
  */
10
10
  export declare function detectRepoContext(cwd?: string): RepoContext | null;
11
+ /**
12
+ * Whether `cwd` is inside a *linked* git worktree (created via `git worktree add`),
13
+ * as opposed to the main working tree or a plain non-worktree repo.
14
+ *
15
+ * `--git-dir` and `--git-common-dir` point at the same place in the main working
16
+ * tree; a linked worktree has its own `--git-dir` under `.git/worktrees/<name>`
17
+ * while `--git-common-dir` still points at the shared repo. Resolved to absolute
18
+ * paths before comparing since git may print either relative or absolute output
19
+ * depending on platform/version. Returns `false` (never throws) when `cwd` isn't
20
+ * a git repo at all.
21
+ */
22
+ export declare function isLinkedWorktree(cwd?: string): boolean;
11
23
  /**
12
24
  * Absolute path of the repository root containing `dir`, or null when `dir`
13
25
  * is not inside a work tree.
@@ -1 +1 @@
1
- {"version":3,"file":"git-context.d.ts","sourceRoot":"","sources":["../src/git-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAwBlE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY1D;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAYpE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYhF;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG/C"}
1
+ {"version":3,"file":"git-context.d.ts","sourceRoot":"","sources":["../src/git-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAclE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAKrE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY1D;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAYpE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYhF;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAExE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG/C"}
@@ -1,5 +1,14 @@
1
1
  import { execSync } from 'child_process';
2
2
  import path from 'path';
3
+ /** Run a git command in `cwd`, tolerating any failure. Trims stdout; empty output ⇒ undefined. */
4
+ function run(cmd, cwd) {
5
+ try {
6
+ return execSync(cmd, { cwd, encoding: 'utf-8', timeout: 5000, stdio: 'pipe' }).trim() || undefined;
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ }
3
12
  /**
4
13
  * Auto-detect git repository context.
5
14
  *
@@ -9,28 +18,35 @@ import path from 'path';
9
18
  * Returns null gracefully if git is unavailable or the directory is not a repo.
10
19
  */
11
20
  export function detectRepoContext(cwd) {
12
- const opts = { cwd: cwd ?? process.cwd(), encoding: 'utf-8', timeout: 5000 };
13
- try {
14
- // Quick check: are we in a git repo at all?
15
- execSync('git rev-parse --is-inside-work-tree', { ...opts, stdio: 'pipe' });
16
- }
17
- catch {
21
+ const resolvedCwd = cwd ?? process.cwd();
22
+ // Quick check: are we in a git repo at all?
23
+ if (run('git rev-parse --is-inside-work-tree', resolvedCwd) === undefined) {
18
24
  return null;
19
25
  }
20
- const run = (cmd) => {
21
- try {
22
- return execSync(cmd, { ...opts, stdio: 'pipe' }).trim() || undefined;
23
- }
24
- catch {
25
- return undefined;
26
- }
27
- };
28
- const toplevel = run('git rev-parse --show-toplevel');
26
+ const toplevel = run('git rev-parse --show-toplevel', resolvedCwd);
29
27
  const name = toplevel ? toplevel.split('/').pop() ?? toplevel.split('\\').pop() ?? 'unknown' : 'unknown';
30
- const branch = run('git branch --show-current') ?? 'HEAD';
31
- const remoteUrl = run('git remote get-url origin');
28
+ const branch = run('git branch --show-current', resolvedCwd) ?? 'HEAD';
29
+ const remoteUrl = run('git remote get-url origin', resolvedCwd);
32
30
  return { name, branch, remoteUrl };
33
31
  }
32
+ /**
33
+ * Whether `cwd` is inside a *linked* git worktree (created via `git worktree add`),
34
+ * as opposed to the main working tree or a plain non-worktree repo.
35
+ *
36
+ * `--git-dir` and `--git-common-dir` point at the same place in the main working
37
+ * tree; a linked worktree has its own `--git-dir` under `.git/worktrees/<name>`
38
+ * while `--git-common-dir` still points at the shared repo. Resolved to absolute
39
+ * paths before comparing since git may print either relative or absolute output
40
+ * depending on platform/version. Returns `false` (never throws) when `cwd` isn't
41
+ * a git repo at all.
42
+ */
43
+ export function isLinkedWorktree(cwd = process.cwd()) {
44
+ const gitDir = run(`git rev-parse --git-dir`, cwd);
45
+ const gitCommonDir = run(`git rev-parse --git-common-dir`, cwd);
46
+ if (gitDir === undefined || gitCommonDir === undefined)
47
+ return false;
48
+ return path.resolve(cwd, gitDir) !== path.resolve(cwd, gitCommonDir);
49
+ }
34
50
  /**
35
51
  * Absolute path of the repository root containing `dir`, or null when `dir`
36
52
  * is not inside a work tree.
@@ -1 +1 @@
1
- {"version":3,"file":"git-context.js","sourceRoot":"","sources":["../src/git-context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"git-context.js","sourceRoot":"","sources":["../src/git-context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { RepoContext, SenderIdentity } from './types.js';
2
+ /** Resolves a human-readable session name, or null when none is available. */
3
+ export type SessionNameResolver = () => string | null;
4
+ /**
5
+ * Builds a `SessionNameResolver` that prefers the Claude Code Remote Control
6
+ * bridge session id (`CLAUDE_CODE_BRIDGE_SESSION_ID`, set only while Remote
7
+ * Control is active, v2.1.199+) and falls back to a minted id otherwise
8
+ * (spec §5.2, §5.3).
9
+ *
10
+ * The bridge id can appear or disappear mid-process, so the result is
11
+ * resolved once, at first call, and cached — a label that changes mid-session
12
+ * would be worse than one that is merely opportunistic.
13
+ */
14
+ export declare function makeSessionNameResolver(opts: {
15
+ env: NodeJS.ProcessEnv;
16
+ mintedUuid: string;
17
+ }): SessionNameResolver;
18
+ export declare const defaultSessionNameResolver: SessionNameResolver;
19
+ /**
20
+ * Stable key for "which project" a session belongs to (spec §5.4).
21
+ * `CLAUDE_PROJECT_DIR` is documented and survives `--resume`/context
22
+ * compaction, unlike `process.cwd()`; `repo.remoteUrl` survives the repo
23
+ * being moved or re-cloned to a different path; `cwd` is the last resort.
24
+ */
25
+ export declare function resolveProjectKey(cwd: string, repo: RepoContext | null): string;
26
+ /**
27
+ * Resolve a display-ready sender identity for an outgoing message, in
28
+ * precedence order: session name override > linked-worktree branch > path
29
+ * fallback.
30
+ *
31
+ * Purely a function of its arguments — never reads `process.cwd()` or
32
+ * `os.hostname()` itself, so callers control (and tests can mock) both `cwd`
33
+ * and `deviceName`; per-tool cwd rules live at each call site.
34
+ */
35
+ export declare function resolveSenderIdentity(cwd: string, deviceName: string, sessionResolver?: SessionNameResolver): SenderIdentity;
36
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9D,8EAA8E;AAC9E,MAAM,MAAM,mBAAmB,GAAG,MAAM,MAAM,GAAG,IAAI,CAAC;AAKtD;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,mBAAmB,CAQtB;AAED,eAAO,MAAM,0BAA0B,EAAE,mBAGvC,CAAC;AAEH;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,MAAM,CAE/E;AAyCD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,eAAe,GAAE,mBAAgD,GAChE,cAAc,CAYhB"}
@@ -0,0 +1,96 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { detectRepoContext, isLinkedWorktree } from './git-context.js';
3
+ /** One MCP server process is exactly one Claude Code session. */
4
+ const PROCESS_SESSION_UUID = randomUUID();
5
+ /**
6
+ * Builds a `SessionNameResolver` that prefers the Claude Code Remote Control
7
+ * bridge session id (`CLAUDE_CODE_BRIDGE_SESSION_ID`, set only while Remote
8
+ * Control is active, v2.1.199+) and falls back to a minted id otherwise
9
+ * (spec §5.2, §5.3).
10
+ *
11
+ * The bridge id can appear or disappear mid-process, so the result is
12
+ * resolved once, at first call, and cached — a label that changes mid-session
13
+ * would be worse than one that is merely opportunistic.
14
+ */
15
+ export function makeSessionNameResolver(opts) {
16
+ let resolved = null;
17
+ return () => {
18
+ if (resolved === null) {
19
+ resolved = opts.env.CLAUDE_CODE_BRIDGE_SESSION_ID ?? opts.mintedUuid;
20
+ }
21
+ return resolved;
22
+ };
23
+ }
24
+ export const defaultSessionNameResolver = makeSessionNameResolver({
25
+ env: process.env,
26
+ mintedUuid: PROCESS_SESSION_UUID,
27
+ });
28
+ /**
29
+ * Stable key for "which project" a session belongs to (spec §5.4).
30
+ * `CLAUDE_PROJECT_DIR` is documented and survives `--resume`/context
31
+ * compaction, unlike `process.cwd()`; `repo.remoteUrl` survives the repo
32
+ * being moved or re-cloned to a different path; `cwd` is the last resort.
33
+ */
34
+ export function resolveProjectKey(cwd, repo) {
35
+ return process.env.CLAUDE_PROJECT_DIR ?? repo?.remoteUrl ?? cwd;
36
+ }
37
+ /** Last two `/`-joined path segments of `cwd`, tolerating either path separator. Never absolute (F-9). */
38
+ function lastTwoSegments(cwd) {
39
+ const segments = cwd.split(/[\\/]/).filter(Boolean);
40
+ return segments.slice(-2).join('/');
41
+ }
42
+ /**
43
+ * Compose the session-tier display label per spec §5.4:
44
+ * `<repoName> · <branch> · <first-4-of-id>`. A raw session id/UUID on its own
45
+ * is meaningless in a message list, so this always folds in repo context
46
+ * when one is available — preferring `CLAUDE_PROJECT_DIR` as the directory to
47
+ * inspect since it is stable across `--resume`/compaction, unlike `cwd` —
48
+ * and falls back to the path tier's own last-two-segments heuristic when
49
+ * that directory isn't a git repo at all.
50
+ */
51
+ /**
52
+ * The discriminating part of a session id.
53
+ *
54
+ * Claude Code's bridge ids are all shaped `session_<opaque>`, so taking the
55
+ * first four characters of one yields the literal `sess` for every session on
56
+ * every machine — a disambiguator that disambiguates nothing, which is the
57
+ * whole job this suffix exists to do. Minted UUIDs have no such prefix and are
58
+ * unaffected.
59
+ *
60
+ * Stripping a known prefix rather than, say, slicing from the end keeps the
61
+ * suffix recognisable as the head of an id a user can match against a real
62
+ * session, and leaves the UUID case byte-for-byte as it was.
63
+ */
64
+ function shortSessionId(sessionId) {
65
+ return sessionId.replace(/^session_/, '').slice(0, 4);
66
+ }
67
+ function composeSessionLabel(cwd, sessionId) {
68
+ const shortId = shortSessionId(sessionId);
69
+ const repo = detectRepoContext(process.env.CLAUDE_PROJECT_DIR ?? cwd);
70
+ if (repo)
71
+ return `${repo.name} · ${repo.branch} · ${shortId}`;
72
+ return `${lastTwoSegments(cwd)} · ${shortId}`;
73
+ }
74
+ /**
75
+ * Resolve a display-ready sender identity for an outgoing message, in
76
+ * precedence order: session name override > linked-worktree branch > path
77
+ * fallback.
78
+ *
79
+ * Purely a function of its arguments — never reads `process.cwd()` or
80
+ * `os.hostname()` itself, so callers control (and tests can mock) both `cwd`
81
+ * and `deviceName`; per-tool cwd rules live at each call site.
82
+ */
83
+ export function resolveSenderIdentity(cwd, deviceName, sessionResolver = defaultSessionNameResolver) {
84
+ const sessionName = sessionResolver();
85
+ if (sessionName !== null)
86
+ return { label: composeSessionLabel(cwd, sessionName), source: 'session' };
87
+ if (isLinkedWorktree(cwd)) {
88
+ const branch = detectRepoContext(cwd)?.branch;
89
+ if (branch)
90
+ return { label: `${deviceName} - ${branch}`, source: 'worktree' };
91
+ // isLinkedWorktree true but no branch (race/edge case) — fall through rather
92
+ // than emit a malformed label.
93
+ }
94
+ return { label: `${deviceName} ${lastTwoSegments(cwd)}`, source: 'path' };
95
+ }
96
+ //# sourceMappingURL=identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.js","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAMvE,iEAAiE;AACjE,MAAM,oBAAoB,GAAG,UAAU,EAAE,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAGvC;IACC,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,OAAO,GAAG,EAAE;QACV,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,6BAA6B,IAAI,IAAI,CAAC,UAAU,CAAC;QACvE,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAwB,uBAAuB,CAAC;IACrF,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,UAAU,EAAE,oBAAoB;CACjC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,IAAwB;IACrE,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,EAAE,SAAS,IAAI,GAAG,CAAC;AAClE,CAAC;AAED,0GAA0G;AAC1G,SAAS,eAAe,CAAC,GAAW;IAClC,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpD,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;GAQG;AACH;;;;;;;;;;;;GAYG;AACH,SAAS,cAAc,CAAC,SAAiB;IACvC,OAAO,SAAS,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,mBAAmB,CAAC,GAAW,EAAE,SAAiB;IACzD,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,GAAG,CAAC,CAAC;IACtE,IAAI,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,MAAM,OAAO,EAAE,CAAC;IAC9D,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,MAAM,OAAO,EAAE,CAAC;AAChD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAAW,EACX,UAAkB,EAClB,kBAAuC,0BAA0B;IAEjE,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;IACtC,IAAI,WAAW,KAAK,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAErG,IAAI,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAC9C,IAAI,MAAM;YAAE,OAAO,EAAE,KAAK,EAAE,GAAG,UAAU,MAAM,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;QAC9E,6EAA6E;QAC7E,+BAA+B;IACjC,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,GAAG,UAAU,IAAI,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5E,CAAC"}
@@ -4,10 +4,13 @@ export declare class HumanInTheLoopServer {
4
4
  private server;
5
5
  private transport;
6
6
  private config;
7
+ private autoLaunchClient;
7
8
  /** reviewIds still waiting on a human, so a graceful exit can release them (D-3). */
8
9
  private outstandingReviews;
9
10
  /** `config` is injectable so a test can construct a server without a real ~/.hitl. */
10
- constructor(config?: HitlConfig);
11
+ constructor(config?: HitlConfig, options?: {
12
+ autoLaunchClient?: boolean;
13
+ });
11
14
  private setupHandlers;
12
15
  /**
13
16
  * ReviewPlan: snapshot a markdown plan, publish it for line-anchored review,
@@ -30,6 +33,17 @@ export declare class HumanInTheLoopServer {
30
33
  */
31
34
  private consumeReviewResponse;
32
35
  private downloadResponseBody;
36
+ /**
37
+ * Publish the sender's own identity as a companion to a question or
38
+ * notification just published, unless the operator opted out.
39
+ *
40
+ * Shared by AskUserQuestion and Notify — both resolve identity from
41
+ * `process.cwd()` (ReviewPlan resolves from the plan's own directory
42
+ * instead, so it is not part of this shared path). Decoration only: never
43
+ * lets a publish failure escape past the tool call it decorates, matching
44
+ * `publishAck`'s log-and-continue pattern below.
45
+ */
46
+ private publishSenderIdentityFor;
33
47
  /** Tell the client whether its submission actually landed. */
34
48
  private publishAck;
35
49
  /**
@@ -53,4 +67,7 @@ export declare class HumanInTheLoopServer {
53
67
  /** Publish cancel_review for every review still waiting on a human. */
54
68
  private cancelOutstandingReviews;
55
69
  }
70
+ export declare function parseServerOptions(args: string[]): {
71
+ autoLaunchClient: boolean;
72
+ };
56
73
  //# sourceMappingURL=mcp-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AAcA,OAAO,KAAK,EAaV,UAAU,EAEX,MAAM,YAAY,CAAC;AAsDpB,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,MAAM,CAAa;IAC3B,qFAAqF;IACrF,OAAO,CAAC,kBAAkB,CAAqB;IAE/C,sFAAsF;gBAC1E,MAAM,GAAE,UAAyB;IAY7C,OAAO,CAAC,aAAa;IA4brB;;;OAGG;YACW,gBAAgB;IA0J9B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAIpB;;;;;;;OAOG;YACW,qBAAqB;YAiErB,oBAAoB;IAUlC,8DAA8D;YAChD,UAAU;IAuBxB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAyBhB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B1B,uEAAuE;YACzD,wBAAwB;CAkBvC"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AAcA,OAAO,KAAK,EAaV,UAAU,EAGX,MAAM,YAAY,CAAC;AAuDpB,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,gBAAgB,CAAU;IAClC,qFAAqF;IACrF,OAAO,CAAC,kBAAkB,CAAqB;IAE/C,sFAAsF;gBAEpF,MAAM,GAAE,UAAyB,EACjC,OAAO,GAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAO;IAc9C,OAAO,CAAC,aAAa;IAicrB;;;OAGG;YACW,gBAAgB;IAmK9B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAIpB;;;;;;;OAOG;YACW,qBAAqB;YAiErB,oBAAoB;IAUlC;;;;;;;;;OASG;YACW,wBAAwB;IAmBtC,8DAA8D;YAChD,UAAU;IAuBxB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAYrB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAyBhB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B1B,uEAAuE;YACzD,wBAAwB;CAkBvC;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;IAAE,gBAAgB,EAAE,OAAO,CAAA;CAAE,CAEhF"}
@@ -10,6 +10,7 @@ import { PROTOCOL_VERSION } from './types.js';
10
10
  import { NtfyTransport, AttachmentExpiredError, AbortedWaitError } from './ntfy-transport.js';
11
11
  import { loadConfig } from './config.js';
12
12
  import { detectRepoContext } from './git-context.js';
13
+ import { resolveSenderIdentity } from './identity.js';
13
14
  import { performSetup, ensureClientRunning } from './setup.js';
14
15
  import { SERVER_VERSION } from './version.js';
15
16
  import { readPlanFile, PlanFileError } from './plan-file.js';
@@ -51,11 +52,13 @@ export class HumanInTheLoopServer {
51
52
  server;
52
53
  transport;
53
54
  config;
55
+ autoLaunchClient;
54
56
  /** reviewIds still waiting on a human, so a graceful exit can release them (D-3). */
55
57
  outstandingReviews = new Set();
56
58
  /** `config` is injectable so a test can construct a server without a real ~/.hitl. */
57
- constructor(config = loadConfig()) {
59
+ constructor(config = loadConfig(), options = {}) {
58
60
  this.config = config;
61
+ this.autoLaunchClient = options.autoLaunchClient ?? true;
59
62
  this.server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
60
63
  this.transport = new NtfyTransport(this.config);
61
64
  this.setupHandlers();
@@ -288,7 +291,9 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
288
291
  // Handle setup tool
289
292
  if (request.params.name === SETUP_TOOL_NAME) {
290
293
  try {
291
- const result = await performSetup(SERVER_DIR);
294
+ const result = await performSetup(SERVER_DIR, {
295
+ autoLaunchClient: this.autoLaunchClient,
296
+ });
292
297
  return {
293
298
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
294
299
  };
@@ -314,6 +319,7 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
314
319
  context: args.context || undefined,
315
320
  };
316
321
  await this.transport.publish(notification);
322
+ await this.publishSenderIdentityFor(notification.messageId, 'notification');
317
323
  return {
318
324
  content: [{ type: 'text', text: JSON.stringify({ success: true, messageId: notification.messageId }) }],
319
325
  };
@@ -381,6 +387,7 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
381
387
  console.error(`Publishing question ${questionMsg.messageId} to ntfy...`);
382
388
  await this.transport.publish(questionMsg);
383
389
  console.error('Question published. Waiting for answer...');
390
+ await this.publishSenderIdentityFor(questionMsg.messageId, 'question');
384
391
  this.transport.pending.record({
385
392
  kind: 'question',
386
393
  id: questionMsg.messageId,
@@ -506,6 +513,12 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
506
513
  const reviewId = resumable?.id ?? uuidv4();
507
514
  const body = { content: plan.content, diff };
508
515
  const encoded = encodePayload(body, this.config.encryptionKey);
516
+ // Resolved from the plan's own directory, matching the `repo` field just
517
+ // above — not process.cwd(), and not named `identity` since that local is
518
+ // already bound to `resolvePlanIdentity(...)` a few lines up.
519
+ const senderIdentity = this.config.identityEnabled !== false
520
+ ? resolveSenderIdentity(path.dirname(plan.resolvedPath), this.config.deviceName)
521
+ : undefined;
509
522
  const reviewMsg = {
510
523
  type: 'plan_review',
511
524
  messageId: reviewId,
@@ -518,6 +531,7 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
518
531
  planId: identity.planId,
519
532
  revision: recorded.revision,
520
533
  isNewPlan: recorded.isNewPlan,
534
+ sender: senderIdentity,
521
535
  snapshotHash,
522
536
  body: encoded.ref,
523
537
  };
@@ -648,6 +662,32 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
648
662
  }
649
663
  return await this.transport.downloadAttachment(attachment);
650
664
  }
665
+ /**
666
+ * Publish the sender's own identity as a companion to a question or
667
+ * notification just published, unless the operator opted out.
668
+ *
669
+ * Shared by AskUserQuestion and Notify — both resolve identity from
670
+ * `process.cwd()` (ReviewPlan resolves from the plan's own directory
671
+ * instead, so it is not part of this shared path). Decoration only: never
672
+ * lets a publish failure escape past the tool call it decorates, matching
673
+ * `publishAck`'s log-and-continue pattern below.
674
+ */
675
+ async publishSenderIdentityFor(forMessageId, forType) {
676
+ if (this.config.identityEnabled === false)
677
+ return;
678
+ try {
679
+ const senderIdentity = resolveSenderIdentity(process.cwd(), this.config.deviceName);
680
+ await this.transport.publishSenderIdentity({
681
+ type: 'sender_identity',
682
+ forMessageId,
683
+ forType,
684
+ sender: senderIdentity,
685
+ });
686
+ }
687
+ catch (err) {
688
+ console.error(`Could not publish sender identity for ${forMessageId}: ${err}`);
689
+ }
690
+ }
651
691
  /** Tell the client whether its submission actually landed. */
652
692
  async publishAck(reviewId, responseId, status, reason) {
653
693
  const ack = {
@@ -675,6 +715,8 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
675
715
  * window to explain why (A-10).
676
716
  */
677
717
  requireClient() {
718
+ if (!this.autoLaunchClient)
719
+ return;
678
720
  const result = ensureClientRunning(SERVER_DIR);
679
721
  if (!result.ok) {
680
722
  throw new McpError(ErrorCode.InternalError, `No HITL client available, so nobody would see this. ${result.reason ?? ''}`.trim());
@@ -754,6 +796,9 @@ Blocking past 60 seconds requires the calling MCP host to opt into resetTimeoutO
754
796
  this.outstandingReviews.clear();
755
797
  }
756
798
  }
799
+ export function parseServerOptions(args) {
800
+ return { autoLaunchClient: !args.includes('--no-auto-launch-client') };
801
+ }
757
802
  /**
758
803
  * True when this file is what node was asked to run, rather than something
759
804
  * another module imported.
@@ -775,7 +820,7 @@ function isDirectlyExecuted() {
775
820
  }
776
821
  }
777
822
  if (isDirectlyExecuted()) {
778
- const server = new HumanInTheLoopServer();
823
+ const server = new HumanInTheLoopServer(loadConfig(), parseServerOptions(process.argv.slice(2)));
779
824
  server.run().catch((error) => {
780
825
  console.error('Server error:', error);
781
826
  process.exit(1);