@bridge_gpt/mcp-server 0.2.68 → 0.2.69

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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Internal `check-update` subcommand (BAPI-1446).
3
+ *
4
+ * The Claude Code SessionStart hook runs `check-update --hook claude-session-start`
5
+ * and reads one JSON object from stdout. This command answers from the local
6
+ * update cache only: it never constructs an MCP server and never touches the
7
+ * network, so a cold start stays well inside the hook's timeout.
8
+ *
9
+ * Protocol:
10
+ * - eligible: exactly one newline-terminated JSON object whose
11
+ * `systemMessage` (shown to the person) and
12
+ * `hookSpecificOutput.additionalContext` (seen by the model) carry the same
13
+ * notice text;
14
+ * - silent: no output at all;
15
+ * - always exit 0 in the supported mode, even after an unexpected error, which
16
+ * is contained without logging. A hook must never block a session.
17
+ *
18
+ * Any other argument form is a usage error: a concise stderr message and a
19
+ * nonzero exit.
20
+ */
21
+ import { getNoticeDecision } from "./session-upgrade-notice.js";
22
+ import { formatSessionUpgradeNotice } from "./upgrade-advice.js";
23
+ import { VERSION } from "./version.generated.js";
24
+ /** The only supported hook mode. */
25
+ export const CLAUDE_SESSION_START_HOOK_MODE = "claude-session-start";
26
+ /** The usage line printed to stderr for an unsupported argument form. */
27
+ export const CHECK_UPDATE_USAGE = `Usage: check-update --hook ${CLAUDE_SESSION_START_HOOK_MODE}`;
28
+ function writeStdout(text) {
29
+ return new Promise((resolve, reject) => {
30
+ process.stdout.write(text, (error) => {
31
+ if (error) {
32
+ reject(error);
33
+ return;
34
+ }
35
+ resolve();
36
+ });
37
+ });
38
+ }
39
+ /**
40
+ * Run the command. Resolves to the process exit code: 0 for the supported hook
41
+ * mode (whether or not it printed anything), 2 for any other argument form.
42
+ */
43
+ export async function runCheckUpdateCli(argv, options = {}) {
44
+ const stderr = options.stderr ?? ((text) => void process.stderr.write(`${text}\n`));
45
+ const supported = argv.length === 2 && argv[0] === "--hook" && argv[1] === CLAUDE_SESSION_START_HOOK_MODE;
46
+ if (!supported) {
47
+ try {
48
+ stderr(CHECK_UPDATE_USAGE);
49
+ }
50
+ catch {
51
+ // A stderr failure cannot change the usage-error result.
52
+ }
53
+ return 2;
54
+ }
55
+ try {
56
+ const env = options.env ?? process.env;
57
+ const decision = getNoticeDecision(env, options.version ?? VERSION);
58
+ if (decision.show) {
59
+ const notice = formatSessionUpgradeNotice(decision.current, decision.latest);
60
+ const payload = {
61
+ systemMessage: notice,
62
+ hookSpecificOutput: {
63
+ hookEventName: "SessionStart",
64
+ additionalContext: notice,
65
+ },
66
+ };
67
+ await (options.stdout ?? writeStdout)(`${JSON.stringify(payload)}\n`);
68
+ }
69
+ }
70
+ catch {
71
+ // Contained without logging: a hook must never fail a session start.
72
+ }
73
+ return 0;
74
+ }
@@ -62,8 +62,16 @@ export function mergeClaudeSettingsWithCommandHook(settings, command, events, op
62
62
  hooks[event] = entries;
63
63
  continue;
64
64
  }
65
- const newEntry = { hooks: [{ type: "command", command }] };
66
- if (event === "PreToolUse") {
65
+ const leaf = { type: "command", command };
66
+ const timeout = options.eventTimeouts?.[event];
67
+ if (timeout !== undefined)
68
+ leaf.timeout = timeout;
69
+ const newEntry = { hooks: [leaf] };
70
+ const eventMatcher = options.eventMatchers?.[event];
71
+ if (eventMatcher !== undefined) {
72
+ newEntry.matcher = eventMatcher;
73
+ }
74
+ else if (event === "PreToolUse") {
67
75
  // Explicit matcher wins; else mirror any existing PreToolUse matcher; else
68
76
  // fall back to the broad default.
69
77
  newEntry.matcher =
@@ -75,6 +83,80 @@ export function mergeClaudeSettingsWithCommandHook(settings, command, events, op
75
83
  }
76
84
  return { ...settings, hooks };
77
85
  }
86
+ function isPlainObject(value) {
87
+ return value !== null && typeof value === "object" && !Array.isArray(value);
88
+ }
89
+ /**
90
+ * Shape guard for the hook containers a provisioner is about to write into
91
+ * (BAPI-1446). An ABSENT `hooks` object or event array is fine: it will be
92
+ * created. A PRESENT one of the wrong type (`hooks` that is an array, a string, or
93
+ * null; an event that is an object or a string) is `malformed`, and must be
94
+ * reported and left alone rather than replaced with an empty container.
95
+ */
96
+ export function inspectClaudeHookContainers(settings, events) {
97
+ const hooks = settings.hooks;
98
+ if (hooks === undefined)
99
+ return "ok";
100
+ if (!isPlainObject(hooks))
101
+ return "malformed";
102
+ for (const event of events) {
103
+ const entries = hooks[event];
104
+ if (entries !== undefined && !Array.isArray(entries))
105
+ return "malformed";
106
+ }
107
+ return "ok";
108
+ }
109
+ /**
110
+ * {@link mergeClaudeSettingsWithCommandHook} behind the shape guard. A malformed
111
+ * existing container is refused: nothing is merged and the caller's settings are
112
+ * not touched. Use this for new provisioning; the legacy merge keeps its
113
+ * original lenient behavior for its existing callers.
114
+ */
115
+ export function mergeClaudeSettingsWithCommandHookChecked(settings, command, events, options = {}) {
116
+ const checked = options.enablePreToolUse && !events.includes("PreToolUse")
117
+ ? [...events, "PreToolUse"]
118
+ : events;
119
+ if (inspectClaudeHookContainers(settings, checked) === "malformed") {
120
+ return { ok: false, reason: "malformed-hooks" };
121
+ }
122
+ return { ok: true, settings: mergeClaudeSettingsWithCommandHook(settings, command, events, options) };
123
+ }
124
+ /**
125
+ * Remove ONE exact command from an event, immutably. Only command leaves whose
126
+ * command string equals `command` are removed; an entry left with no hooks is
127
+ * dropped, and everything else (other leaves, other entries, other events, other
128
+ * settings) is kept as is. Returns the SAME object when nothing matches or the
129
+ * relevant structure is malformed, so a caller can detect "no change" by identity.
130
+ * An emptied event array or `hooks` object is left in place: the user may own it.
131
+ */
132
+ export function removeClaudeSettingsCommandHook(settings, event, command) {
133
+ const hooks = settings.hooks;
134
+ if (!isPlainObject(hooks))
135
+ return settings;
136
+ const entries = hooks[event];
137
+ if (!Array.isArray(entries))
138
+ return settings;
139
+ let changed = false;
140
+ const nextEntries = [];
141
+ for (const entry of entries) {
142
+ if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
143
+ nextEntries.push(entry);
144
+ continue;
145
+ }
146
+ const leaves = entry.hooks;
147
+ const kept = leaves.filter((leaf) => !(isPlainObject(leaf) && leaf.type === "command" && leaf.command === command));
148
+ if (kept.length === leaves.length) {
149
+ nextEntries.push(entry);
150
+ continue;
151
+ }
152
+ changed = true;
153
+ if (kept.length > 0)
154
+ nextEntries.push({ ...entry, hooks: kept });
155
+ }
156
+ if (!changed)
157
+ return settings;
158
+ return { ...settings, hooks: { ...hooks, [event]: nextEntries } };
159
+ }
78
160
  /**
79
161
  * Provision (merge into) ONE worktree's `.claude/settings.local.json`. Missing
80
162
  * settings are treated as `{}`; malformed JSON is a safety stop (the file is
package/build/cli-help.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
2
2
  /** The one recovery command shared by terminal guidance surfaces. */
3
3
  export const INSTALL_RECOVERY_COMMAND = `npx -y ${MCP_PACKAGE_NAME} install`;
4
- /** Returns static CLI orientation text without resolving configuration or doing I/O. */
4
+ /**
5
+ * Returns static CLI orientation text without resolving configuration or doing I/O.
6
+ *
7
+ * Internal subcommands (`mcp-invoke`, `check-update`) are routed by the
8
+ * dispatcher but are intentionally not listed: they are launched by tools and
9
+ * hooks, not typed by a person.
10
+ */
5
11
  export function getTopLevelCliUsage() {
6
12
  return `Usage: bridge-api-mcp-server [command]
7
13
 
@@ -1,7 +1,15 @@
1
1
  import path from "node:path";
2
2
  import os from "node:os";
3
+ /**
4
+ * True when `CODEX_HOME` is set to an absolute path, which is the only override
5
+ * Codex's user config root accepts. A relative or blank value is ignored.
6
+ */
7
+ export function isCodexHomeOverrideHonored(env) {
8
+ const override = env.CODEX_HOME?.trim();
9
+ return Boolean(override) && path.isAbsolute(override);
10
+ }
3
11
  /** Codex's user config root; only an absolute CODEX_HOME is accepted. */
4
12
  export function codexHome(env, homedir = os.homedir()) {
5
13
  const override = env.CODEX_HOME?.trim();
6
- return override && path.isAbsolute(override) ? override : path.join(homedir, ".codex");
14
+ return isCodexHomeOverrideHonored(env) ? override : path.join(homedir, ".codex");
7
15
  }