@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 +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -12
- package/dist/config.js.map +1 -1
- package/dist/git-context.d.ts +12 -0
- package/dist/git-context.d.ts.map +1 -1
- package/dist/git-context.js +33 -17
- package/dist/git-context.js.map +1 -1
- package/dist/identity.d.ts +36 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +96 -0
- package/dist/identity.js.map +1 -0
- package/dist/mcp-server.d.ts +18 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +48 -3
- package/dist/mcp-server.js.map +1 -1
- package/dist/ntfy-transport.d.ts +9 -1
- package/dist/ntfy-transport.d.ts.map +1 -1
- package/dist/ntfy-transport.js +18 -0
- package/dist/ntfy-transport.js.map +1 -1
- package/dist/setup.d.ts +4 -2
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +43 -19
- package/dist/setup.js.map +1 -1
- package/dist/types.d.ts +41 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +64 -64
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
|
|
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
|
|
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
|
/**
|
package/dist/config.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
*
|
|
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
|
-
|
|
15
|
-
|
|
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(
|
|
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 ${
|
|
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
|
|
76
|
+
* Save a config object to this install's config.json (see hitlDir).
|
|
46
77
|
*/
|
|
47
78
|
export function saveConfig(config) {
|
|
48
|
-
mkdirSync(
|
|
49
|
-
|
|
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
|
|
98
|
+
return configFile();
|
|
56
99
|
}
|
|
57
100
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/git-context.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/git-context.js
CHANGED
|
@@ -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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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.
|
package/dist/git-context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-context.js","sourceRoot":"","sources":["../src/git-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB
|
|
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"}
|
package/dist/identity.js
ADDED
|
@@ -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"}
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -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
|
package/dist/mcp-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AAcA,OAAO,KAAK,EAaV,UAAU,
|
|
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"}
|
package/dist/mcp-server.js
CHANGED
|
@@ -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);
|