@tiens.nguyen/gu-cli 1.0.783 → 1.0.785
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/completed-flush.mjs +47 -0
- package/gonext_agent_chat.py +1 -1
- package/gu-cli.mjs +9 -3
- package/gu-repl.mjs +473 -35
- package/gu_hooks.py +102 -4
- package/gu_skills.py +64 -4
- package/hooks-command.mjs +14 -9
- package/hooks-config.mjs +130 -33
- package/mcp-command.mjs +11 -5
- package/mcp-config.mjs +99 -11
- package/model-prices.mjs +484 -0
- package/package.json +4 -2
- package/skill-source.mjs +60 -0
- package/skills.mjs +24 -12
- package/step-tokens.mjs +45 -0
- package/terminal-layout.mjs +14 -0
- package/terminal-viewport.mjs +65 -0
- package/turn-stop.mjs +17 -0
package/mcp-config.mjs
CHANGED
|
@@ -18,13 +18,27 @@
|
|
|
18
18
|
* tool is read-only (readOnlyHint) is NOT a rule: the MCP spec says clients must treat those hints
|
|
19
19
|
* as untrusted, and a hostile server would simply claim it.
|
|
20
20
|
*
|
|
21
|
+
* CLAUDE CODE'S SERVERS (#325). A folder that chose "Claude's skills" (/switch-skill) takes YOUR
|
|
22
|
+
* servers from Claude Code's own file instead of ~/.gonext/mcp.json:
|
|
23
|
+
* ~/.claude.json projects["<this folder>"].mcpServers (Claude's "local" scope — this folder only)
|
|
24
|
+
* ~/.claude.json mcpServers (Claude's "user" scope — every folder)
|
|
25
|
+
* The project's .mcp.json is the same file either way. Yours still win over the project's. gu's own
|
|
26
|
+
* bookkeeping — disabled, the allow/deny rules, which project servers you approved — stays in
|
|
27
|
+
* ~/.gonext/mcp.json whichever source is chosen: Claude Code's file has no place for it, and gu
|
|
28
|
+
* reads Claude's setup without Claude ever having to read gu's.
|
|
29
|
+
*
|
|
30
|
+
* ~/.claude.json IS CLAUDE CODE'S WHOLE STATE FILE, not a config file of ours. It is only written
|
|
31
|
+
* when you add or remove a server yourself, and then only its mcpServers entry changes: re-read,
|
|
32
|
+
* change one key, write back atomically with the file's own permissions.
|
|
33
|
+
*
|
|
21
34
|
* ONE WRITER SHAPE. Both `gu mcp` and `/mcp` go through updateUserConfig, which re-reads the file,
|
|
22
35
|
* changes one thing and writes it back atomically — keys it does not know are kept. Rebuilding the
|
|
23
36
|
* object from what one side knows is how a settings screen once deleted the other side's entries.
|
|
24
37
|
*/
|
|
25
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
26
39
|
import { homedir } from "node:os";
|
|
27
40
|
import { dirname, join, resolve } from "node:path";
|
|
41
|
+
import { resolveSkillSource } from "./skill-source.mjs";
|
|
28
42
|
|
|
29
43
|
export const SERVER_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
30
44
|
|
|
@@ -34,6 +48,13 @@ export function userConfigPath(home = homedir()) {
|
|
|
34
48
|
export function projectConfigPath(cwd) {
|
|
35
49
|
return join(cwd, ".mcp.json");
|
|
36
50
|
}
|
|
51
|
+
/** Claude Code's state file — where its user- and local-scope MCP servers are (#325). */
|
|
52
|
+
export function claudeConfigPath(home = homedir()) {
|
|
53
|
+
return join(home, ".claude.json");
|
|
54
|
+
}
|
|
55
|
+
/** The file YOUR servers are in, for the source in use. */
|
|
56
|
+
export const userServersPath = (home = homedir(), source = "gu") =>
|
|
57
|
+
source === "claude" ? claudeConfigPath(home) : userConfigPath(home);
|
|
37
58
|
|
|
38
59
|
function readJson(file) {
|
|
39
60
|
let text;
|
|
@@ -67,6 +88,39 @@ export function readUserConfig(home = homedir()) {
|
|
|
67
88
|
|
|
68
89
|
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
69
90
|
|
|
91
|
+
/** Claude Code's servers for this folder → { local, user, error? }. A missing file is no servers. */
|
|
92
|
+
export function readClaudeServers({ cwd, home = homedir() } = {}) {
|
|
93
|
+
const { data, error } = readJson(claudeConfigPath(home));
|
|
94
|
+
const proj = cwd && isObj(data?.projects) ? data.projects[resolve(cwd)] : null;
|
|
95
|
+
return {
|
|
96
|
+
error,
|
|
97
|
+
local: isObj(proj?.mcpServers) ? proj.mcpServers : {},
|
|
98
|
+
user: isObj(data?.mcpServers) ? data.mcpServers : {},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Change ~/.claude.json: re-read, edit in place, write back atomically, permissions kept. Refused
|
|
104
|
+
* when the file is not valid JSON — it is Claude Code's, and not ours to "repair".
|
|
105
|
+
*/
|
|
106
|
+
export function updateClaudeConfig(mutate, home = homedir()) {
|
|
107
|
+
const file = claudeConfigPath(home);
|
|
108
|
+
const { data, error } = readJson(file);
|
|
109
|
+
if (error) throw new Error(`${file} is ${error} — fix it first (it is Claude Code's own file)`);
|
|
110
|
+
const raw = data ?? {};
|
|
111
|
+
mutate(raw);
|
|
112
|
+
let mode = 0o600;
|
|
113
|
+
try {
|
|
114
|
+
mode = statSync(file).mode & 0o777;
|
|
115
|
+
} catch {
|
|
116
|
+
/* a new file: private, as Claude Code makes it */
|
|
117
|
+
}
|
|
118
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
119
|
+
writeFileSync(tmp, JSON.stringify(raw, null, 2) + "\n", { encoding: "utf8", mode });
|
|
120
|
+
renameSync(tmp, file);
|
|
121
|
+
return raw;
|
|
122
|
+
}
|
|
123
|
+
|
|
70
124
|
/** Write ~/.gonext/mcp.json atomically: a crash mid-write leaves the old file, never half of one. */
|
|
71
125
|
function writeUserRaw(raw, home) {
|
|
72
126
|
const file = userConfigPath(home);
|
|
@@ -103,12 +157,17 @@ export function normalizeServer(entry) {
|
|
|
103
157
|
|
|
104
158
|
/**
|
|
105
159
|
* Every server this folder knows of, with its state.
|
|
106
|
-
*
|
|
160
|
+
* `source` = whose servers ("gu" | "claude"); left out, it is the folder's own choice.
|
|
161
|
+
* scope: "user" (yours, every folder), "local" (Claude's: yours, this folder only), "project".
|
|
162
|
+
* → { servers: [{ name, scope, type, command, args, env, disabled, approved, runnable, error? }], warnings, source }
|
|
107
163
|
*/
|
|
108
|
-
export function listServers({ cwd = process.cwd(), home = homedir() } = {}) {
|
|
164
|
+
export function listServers({ cwd = process.cwd(), home = homedir(), source } = {}) {
|
|
165
|
+
const src = resolveSkillSource({ source, cwd, home });
|
|
109
166
|
const user = readUserConfig(home);
|
|
110
167
|
const warnings = [];
|
|
111
|
-
if (user.error)
|
|
168
|
+
if (user.error) {
|
|
169
|
+
warnings.push(`${userConfigPath(home)} is ${user.error} — ${src === "claude" ? "its rules and approvals are skipped" : "its servers are skipped"}`);
|
|
170
|
+
}
|
|
112
171
|
const servers = [];
|
|
113
172
|
const seen = new Set();
|
|
114
173
|
const add = (name, entry, scope, approved) => {
|
|
@@ -129,7 +188,14 @@ export function listServers({ cwd = process.cwd(), home = homedir() } = {}) {
|
|
|
129
188
|
runnable: !n.error && !disabled && approved,
|
|
130
189
|
});
|
|
131
190
|
};
|
|
132
|
-
|
|
191
|
+
if (src === "claude") {
|
|
192
|
+
const claude = readClaudeServers({ cwd, home });
|
|
193
|
+
if (claude.error) warnings.push(`${claudeConfigPath(home)} is ${claude.error} — its servers are skipped`);
|
|
194
|
+
for (const [name, entry] of Object.entries(claude.local)) add(name, entry, "local", true);
|
|
195
|
+
for (const [name, entry] of Object.entries(claude.user)) add(name, entry, "user", true);
|
|
196
|
+
} else {
|
|
197
|
+
for (const [name, entry] of Object.entries(user.mcpServers)) add(name, entry, "user", true);
|
|
198
|
+
}
|
|
133
199
|
if (cwd) {
|
|
134
200
|
const proj = readJson(projectConfigPath(cwd));
|
|
135
201
|
if (proj.error) warnings.push(`${projectConfigPath(cwd)} is ${proj.error} — its servers are skipped`);
|
|
@@ -138,12 +204,12 @@ export function listServers({ cwd = process.cwd(), home = homedir() } = {}) {
|
|
|
138
204
|
add(name, entry, "project", approvedHere.includes(name));
|
|
139
205
|
}
|
|
140
206
|
}
|
|
141
|
-
return { servers, warnings };
|
|
207
|
+
return { servers, warnings, source: src };
|
|
142
208
|
}
|
|
143
209
|
|
|
144
210
|
/** What the worker hands the agent for one turn: the servers it may start, and the rules. */
|
|
145
|
-
export function mcpForTurn({ cwd, home = homedir() } = {}) {
|
|
146
|
-
const { servers } = listServers({ cwd, home });
|
|
211
|
+
export function mcpForTurn({ cwd, home = homedir(), source } = {}) {
|
|
212
|
+
const { servers } = listServers({ cwd, home, source });
|
|
147
213
|
const user = readUserConfig(home);
|
|
148
214
|
return {
|
|
149
215
|
servers: servers
|
|
@@ -177,7 +243,7 @@ export function decide({ allow = [], deny = [] }, server, tool) {
|
|
|
177
243
|
|
|
178
244
|
/* ---------------------------------------------------------------- changes -------------- */
|
|
179
245
|
|
|
180
|
-
export function addServer({ name, command, args = [], env = {}, scope = "user", cwd, home = homedir() }) {
|
|
246
|
+
export function addServer({ name, command, args = [], env = {}, scope = "user", cwd, home = homedir(), source }) {
|
|
181
247
|
if (!SERVER_NAME_RE.test(name ?? "")) throw new Error(`"${name}" is not a valid name — use letters, digits, _ or -`);
|
|
182
248
|
if (!String(command ?? "").trim()) throw new Error("no command given");
|
|
183
249
|
const entry = { type: "stdio", command: String(command), args: args.map(String), env };
|
|
@@ -196,6 +262,15 @@ export function addServer({ name, command, args = [], env = {}, scope = "user",
|
|
|
196
262
|
approveProjectServer({ name, cwd, home });
|
|
197
263
|
return { file };
|
|
198
264
|
}
|
|
265
|
+
if (resolveSkillSource({ source, cwd, home }) === "claude") {
|
|
266
|
+
// Claude Code's user scope. Its entry, its file; gu's file only forgets a stale "disabled".
|
|
267
|
+
updateClaudeConfig((raw) => {
|
|
268
|
+
raw.mcpServers = isObj(raw.mcpServers) ? raw.mcpServers : {};
|
|
269
|
+
raw.mcpServers[name] = entry;
|
|
270
|
+
}, home);
|
|
271
|
+
if (readUserConfig(home).disabled.includes(name)) setDisabled({ name, disabled: false, home });
|
|
272
|
+
return { file: claudeConfigPath(home) };
|
|
273
|
+
}
|
|
199
274
|
updateUserConfig((raw) => {
|
|
200
275
|
raw.mcpServers = isObj(raw.mcpServers) ? raw.mcpServers : {};
|
|
201
276
|
raw.mcpServers[name] = entry;
|
|
@@ -205,10 +280,23 @@ export function addServer({ name, command, args = [], env = {}, scope = "user",
|
|
|
205
280
|
}
|
|
206
281
|
|
|
207
282
|
/** Remove from YOUR file. A project's server cannot be removed from here — only not approved. */
|
|
208
|
-
export function removeServer({ name, home = homedir() }) {
|
|
283
|
+
export function removeServer({ name, home = homedir(), cwd, source }) {
|
|
209
284
|
let found = false;
|
|
285
|
+
const claude = resolveSkillSource({ source, cwd, home }) === "claude";
|
|
286
|
+
if (claude) {
|
|
287
|
+
// From Claude Code's file: this folder's own entry and the every-folder one.
|
|
288
|
+
const has = readClaudeServers({ cwd, home });
|
|
289
|
+
if (name in has.local || name in has.user) {
|
|
290
|
+
updateClaudeConfig((raw) => {
|
|
291
|
+
if (isObj(raw.mcpServers) && name in raw.mcpServers) delete raw.mcpServers[name];
|
|
292
|
+
const proj = cwd && isObj(raw.projects) ? raw.projects[resolve(cwd)] : null;
|
|
293
|
+
if (isObj(proj?.mcpServers) && name in proj.mcpServers) delete proj.mcpServers[name];
|
|
294
|
+
}, home);
|
|
295
|
+
found = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
210
298
|
updateUserConfig((raw) => {
|
|
211
|
-
if (isObj(raw.mcpServers) && name in raw.mcpServers) {
|
|
299
|
+
if (!claude && isObj(raw.mcpServers) && name in raw.mcpServers) {
|
|
212
300
|
delete raw.mcpServers[name];
|
|
213
301
|
found = true;
|
|
214
302
|
}
|