@tiens.nguyen/gu-cli 1.0.738 → 1.0.740
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/gonext_agent_chat.py +82 -0
- package/gu-cli.mjs +35 -0
- package/gu-repl.mjs +112 -6
- package/gu_mcp.py +486 -0
- package/gu_skills.py +93 -0
- package/mcp-command.mjs +324 -0
- package/mcp-config.mjs +329 -0
- package/package.json +6 -1
- package/skills.mjs +175 -0
package/gonext_agent_chat.py
CHANGED
|
@@ -4764,6 +4764,9 @@ _RAG_SKIP_DIRS = {
|
|
|
4764
4764
|
# .github, .vscode or .claude — those carry CI config, editor settings and project
|
|
4765
4765
|
# instructions a coding agent legitimately benefits from reading.
|
|
4766
4766
|
".claude-memory", ".specstory", ".aider.tags.cache.v3", ".goose",
|
|
4767
|
+
# gu's OWN memory store (the memory skill's .gu-memory/, task #269): the agent reaches it
|
|
4768
|
+
# through the memory MCP tools, which query it — not by grepping raw checkpoint markdown.
|
|
4769
|
+
".gu-memory",
|
|
4767
4770
|
}
|
|
4768
4771
|
# Machine-generated files that are TEXT but never worth reading. A lockfile is the whole
|
|
4769
4772
|
# problem in miniature: in the reported Next.js project package-lock.json is 239KB while every
|
|
@@ -6695,6 +6698,9 @@ def _ws_request_approval(api_base, worker_key, job_id, command, reason, deadline
|
|
|
6695
6698
|
_emit({"type": "step", "text": "Awaiting your choice: compact the context?"})
|
|
6696
6699
|
elif command.startswith("__S3SETUP__::"):
|
|
6697
6700
|
_emit({"type": "step", "text": "Awaiting your choice: set up S3, or save on this computer?"})
|
|
6701
|
+
elif command.startswith("__MCPTOOL__::"):
|
|
6702
|
+
_srv, _, _rest = command[len("__MCPTOOL__::"):].partition("\t")
|
|
6703
|
+
_emit({"type": "step", "text": f"Awaiting your approval: MCP tool {_srv}/{_rest.split(chr(9))[0]}"})
|
|
6698
6704
|
else:
|
|
6699
6705
|
_emit({"type": "step", "text": f"Awaiting your approval to run: {command[:70]}"})
|
|
6700
6706
|
deadline = _t.time() + max(30, int(deadline_s)) # per-call; risky=180s, __MAXSTEP__ long
|
|
@@ -7073,6 +7079,12 @@ def run_agent_chat(cfg):
|
|
|
7073
7079
|
# risky command and asks the user via the API instead of hard-blocking it.
|
|
7074
7080
|
_job_id = (cfg.get("jobId") or "").strip()
|
|
7075
7081
|
_interactive_approval = bool(cfg.get("interactiveApproval"))
|
|
7082
|
+
# Skills and MCP servers (task #269). The worker scans ~/.gonext/skills, ~/.gonext/mcp.json and
|
|
7083
|
+
# this folder's .gonext/skills + .mcp.json for every turn; none present = neither appears
|
|
7084
|
+
# anywhere in the prompt. Names only here — a skill's body is read when it is used.
|
|
7085
|
+
import gu_skills as _skills_mod
|
|
7086
|
+
_skills = _skills_mod.clean_list(cfg.get("skills"))
|
|
7087
|
+
_mcp_cfg = cfg.get("mcp") if isinstance(cfg.get("mcp"), dict) else {}
|
|
7076
7088
|
|
|
7077
7089
|
def _ask_s3_setup() -> bool:
|
|
7078
7090
|
"""Private mode, a file to hand over, no S3 of the user's own (task #252): ask in the
|
|
@@ -7230,6 +7242,14 @@ def run_agent_chat(cfg):
|
|
|
7230
7242
|
# below. Otherwise the keyword router matches URLs/"api"/"GET" from prior turns
|
|
7231
7243
|
# and fires the agent on trivial replies like "thanks" or "good".
|
|
7232
7244
|
latest_user_text = task_text
|
|
7245
|
+
# `/<skill> request` typed in the terminal (task #269): the skill's instructions go in with
|
|
7246
|
+
# the request, for this turn only — the history keeps the short line the user typed.
|
|
7247
|
+
_skill_call = _skills_mod.invocation(task_text, _skills)
|
|
7248
|
+
if _skill_call:
|
|
7249
|
+
_sk, _sk_body, _sk_req = _skill_call
|
|
7250
|
+
task_text = (_skills_mod.render(_sk, _sk_body) + "\n\nThe user's request: "
|
|
7251
|
+
+ (_sk_req or "(nothing more — do what the skill describes)"))
|
|
7252
|
+
_log(f"skill invoked: {_sk['name']}")
|
|
7233
7253
|
|
|
7234
7254
|
# Walk prior turns newest-first, keeping condensed lines until the budget is
|
|
7235
7255
|
# spent, then restore chronological (oldest→newest) order.
|
|
@@ -7862,6 +7882,11 @@ def run_agent_chat(cfg):
|
|
|
7862
7882
|
needs_agent = True
|
|
7863
7883
|
else:
|
|
7864
7884
|
needs_agent = _route(latest_user_text, agent_base_url, agent_api_key, agent_model_id)
|
|
7885
|
+
# A skill the user typed is work to DO with tools — plain chat cannot follow one.
|
|
7886
|
+
if not needs_agent and _skill_call:
|
|
7887
|
+
_log("router → YES (skill invoked)")
|
|
7888
|
+
_emit({"type": "step", "text": "→ Agent mode (skill)"})
|
|
7889
|
+
needs_agent = True
|
|
7865
7890
|
# A pending email awaiting 'confirm' must reach the agent even though a bare
|
|
7866
7891
|
# 'confirm' / 'send it' is not a network keyword — otherwise the send never fires.
|
|
7867
7892
|
if not needs_agent and _email_confirm_pending():
|
|
@@ -8270,6 +8295,7 @@ def run_agent_chat(cfg):
|
|
|
8270
8295
|
"When you have enough to answer, call final_answer(<your answer>) in a code block. "
|
|
8271
8296
|
"Prefer FEWER steps, and never repeat a tool call that already succeeded.\n\n"
|
|
8272
8297
|
"{{TOOL_LIST}}\n" # filled in below, once from the REAL registered tool objects
|
|
8298
|
+
+ _skills_mod.prompt_block(_skills)
|
|
8273
8299
|
+ _rag_tool_block + _ws_tool_block + _auto_test_block +
|
|
8274
8300
|
"\n"
|
|
8275
8301
|
"http_request RETURN FORMAT: 'HTTP 200\\n{body}' — first line is 'HTTP <code>', body follows.\n"
|
|
@@ -12405,6 +12431,62 @@ def run_agent_chat(cfg):
|
|
|
12405
12431
|
# (S3 indexing) isn't configured.
|
|
12406
12432
|
if not _RAG_AVAILABLE:
|
|
12407
12433
|
agent_tools += [list_dir, read_text_file]
|
|
12434
|
+
# SKILLS (task #269): only offered when at least one exists, like every tool above.
|
|
12435
|
+
if _skills:
|
|
12436
|
+
@tool
|
|
12437
|
+
def use_skill(name: str) -> str:
|
|
12438
|
+
"""Load a skill's full instructions by its name from the SKILLS list. Call it FIRST when the task matches a skill, then follow what it returns.
|
|
12439
|
+
|
|
12440
|
+
Args:
|
|
12441
|
+
name: the skill's name, exactly as listed under SKILLS
|
|
12442
|
+
"""
|
|
12443
|
+
return _skills_mod.use_skill_text(_skills, name)
|
|
12444
|
+
|
|
12445
|
+
agent_tools.append(use_skill)
|
|
12446
|
+
# MCP SERVERS (task #269): every tool of every server that starts, named
|
|
12447
|
+
# mcp__<server>__<tool>. A server that does not start is a note for this turn, not a
|
|
12448
|
+
# failure of it. Each call passes _mcp_approve first. Not for the between-turns index job,
|
|
12449
|
+
# which calls no model and would start the servers for nothing.
|
|
12450
|
+
_mcp_servers = [s for s in (_mcp_cfg.get("servers") or [])
|
|
12451
|
+
if isinstance(s, dict) and s.get("name") and s.get("command")]
|
|
12452
|
+
if _mcp_servers and not cfg.get("ragIndexOnly"):
|
|
12453
|
+
import atexit as _atexit
|
|
12454
|
+
import gu_mcp as _mcp_mod
|
|
12455
|
+
from smolagents import Tool as _SmolTool
|
|
12456
|
+
_mcp_turn_allowed: set = set()
|
|
12457
|
+
|
|
12458
|
+
def _mcp_approve(server: str, tool_name: str, args: dict):
|
|
12459
|
+
"""deny → allow → ask (Claude Code's order). The rules are re-read from the file on
|
|
12460
|
+
every call: an "Always allow" answered a moment ago was written there."""
|
|
12461
|
+
allow, deny = _mcp_mod.read_rules(_mcp_cfg.get("configPath"),
|
|
12462
|
+
_mcp_cfg.get("allow") or [], _mcp_cfg.get("deny") or [])
|
|
12463
|
+
verdict = _mcp_mod.decide(allow, deny, server, tool_name)
|
|
12464
|
+
if verdict == "deny":
|
|
12465
|
+
return False, (f"MCP tool {server}/{tool_name} is blocked by a deny rule in "
|
|
12466
|
+
"~/.gonext/mcp.json. Do not call it again; carry on without it.")
|
|
12467
|
+
if verdict == "allow" or (server, tool_name) in _mcp_turn_allowed:
|
|
12468
|
+
return True, ""
|
|
12469
|
+
if not (_interactive_approval and _job_id and worker_api_base and worker_api_key):
|
|
12470
|
+
return False, (f"MCP tool {server}/{tool_name} needs the user's approval and this "
|
|
12471
|
+
"session cannot ask. Tell the user to allow it in the gu terminal "
|
|
12472
|
+
f"(/mcp allow {server}), then carry on without it.")
|
|
12473
|
+
preview = json.dumps(args, ensure_ascii=False, default=str)
|
|
12474
|
+
if len(preview) > 240:
|
|
12475
|
+
preview = preview[:240] + "…"
|
|
12476
|
+
ok = _ws_request_approval(worker_api_base, worker_api_key, _job_id,
|
|
12477
|
+
f"__MCPTOOL__::{server}\t{tool_name}\t{preview}", "")
|
|
12478
|
+
if ok:
|
|
12479
|
+
_mcp_turn_allowed.add((server, tool_name))
|
|
12480
|
+
return True, ""
|
|
12481
|
+
return False, (f"The user declined MCP tool {server}/{tool_name}. Do not call it again "
|
|
12482
|
+
"this turn; carry on without it or ask the user.")
|
|
12483
|
+
|
|
12484
|
+
_emit({"type": "step", "text": f"Starting {len(_mcp_servers)} MCP server(s)…"})
|
|
12485
|
+
_mcp_conn, _mcp_notes = _mcp_mod.connect_all(_mcp_servers, _WS_ACTIVE or None, log=_log)
|
|
12486
|
+
_atexit.register(lambda: [c.close() for c, _t in _mcp_conn])
|
|
12487
|
+
for _note in _mcp_notes:
|
|
12488
|
+
_emit({"type": "step", "text": _note})
|
|
12489
|
+
agent_tools += _mcp_mod.build_tools(_mcp_conn, _SmolTool, _mcp_approve)
|
|
12408
12490
|
# Fill in the numbered tool list now that the REAL tool objects exist — see
|
|
12409
12491
|
# _render_numbered_tool_list and the {{TOOL_LIST}} placeholder left in tool_hint.
|
|
12410
12492
|
task_with_hint = task_with_hint.replace(
|
package/gu-cli.mjs
CHANGED
|
@@ -33,6 +33,27 @@ import { createRequire } from "node:module";
|
|
|
33
33
|
import dotenv from "dotenv";
|
|
34
34
|
import OpenAI from "openai";
|
|
35
35
|
import { resolvePython } from "./platform-tools.mjs";
|
|
36
|
+
import { skillsForTurn } from "./skills.mjs";
|
|
37
|
+
import { mcpForTurn } from "./mcp-config.mjs";
|
|
38
|
+
|
|
39
|
+
/** The `skills` / `mcp` fields of one job's agent config (task #269). A broken config is a log
|
|
40
|
+
* line, never a failed turn — the turn runs without them. */
|
|
41
|
+
function skillsAndMcpForJob(cwd) {
|
|
42
|
+
const out = {};
|
|
43
|
+
try {
|
|
44
|
+
const skills = skillsForTurn({ cwd });
|
|
45
|
+
if (skills.length) out.skills = skills;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.log(`[gu-worker] skills skipped: ${e?.message || e}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const mcp = mcpForTurn({ cwd });
|
|
51
|
+
if (mcp.servers.length) out.mcp = mcp;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.log(`[gu-worker] MCP servers skipped: ${e?.message || e}`);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
36
57
|
|
|
37
58
|
const WORKER_DIR = dirname(fileURLToPath(import.meta.url));
|
|
38
59
|
|
|
@@ -223,6 +244,16 @@ if (args[0] === "token") {
|
|
|
223
244
|
process.exit(0);
|
|
224
245
|
}
|
|
225
246
|
|
|
247
|
+
if (args[0] === "mcp") {
|
|
248
|
+
// #269. MCP servers from a shell — `claude mcp add`'s shape. Same code as /mcp in the terminal UI.
|
|
249
|
+
const { runMcpCommand } = await import("./mcp-command.mjs");
|
|
250
|
+
const code = await runMcpCommand(args.slice(1), {
|
|
251
|
+
cwd: process.cwd(),
|
|
252
|
+
print: (line, kind) => (kind === "warn" ? console.error(line) : console.log(line)),
|
|
253
|
+
});
|
|
254
|
+
process.exit(code);
|
|
255
|
+
}
|
|
256
|
+
|
|
226
257
|
if (args[0] === "full-log") {
|
|
227
258
|
// #250. An administrator switches Enable Full Log (#249) from a terminal — after typing an admin
|
|
228
259
|
// token minted on the web admin page. Same code as /full-log in the terminal UI.
|
|
@@ -3836,6 +3867,10 @@ async function runAgentChatJob(job) {
|
|
|
3836
3867
|
// Task #134: set when the terminal's folder is on ANOTHER machine. The agent surfaces
|
|
3837
3868
|
// it to the user — a silent downgrade to "no file tools" would just look broken.
|
|
3838
3869
|
...(remoteWorkspace ? { remoteWorkspace } : {}),
|
|
3870
|
+
// Skills and MCP servers (task #269), read from disk for EVERY job so an edited, added or
|
|
3871
|
+
// deleted SKILL.md / server takes effect on the next turn with no restart. The project's
|
|
3872
|
+
// own ones only when its folder is on this machine. None = the keys are absent.
|
|
3873
|
+
...skillsAndMcpForJob(terminalActiveWs && wsExistsHere ? terminalActiveWs : null),
|
|
3839
3874
|
});
|
|
3840
3875
|
// Agent run time budget (task #81). Default 60 min; user-configurable in web Settings
|
|
3841
3876
|
// (agentTimeoutMinutes → payload.agentTimeoutMs). Multi-step ReAct on a 14B/31B with a
|
package/gu-repl.mjs
CHANGED
|
@@ -119,6 +119,9 @@ import {
|
|
|
119
119
|
shouldCancelPrompt,
|
|
120
120
|
} from "./terminal-layout.mjs";
|
|
121
121
|
import { completedFlush } from "./completed-flush.mjs";
|
|
122
|
+
import { discoverSkills, matchSkillCommand } from "./skills.mjs";
|
|
123
|
+
import { addAllowRule, listServers as listMcpServers } from "./mcp-config.mjs";
|
|
124
|
+
import { unapprovedProjectServers } from "./mcp-command.mjs";
|
|
122
125
|
import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
123
126
|
import { codingChoiceTags, coderDisplayName, coderWhere, whereOfActiveCoder } from "./coding-choice-tags.mjs";
|
|
124
127
|
import {
|
|
@@ -532,7 +535,7 @@ const argv = process.argv.slice(2);
|
|
|
532
535
|
// process.argv and exits inside its own subcommand branch, before it ever starts polling.
|
|
533
536
|
const CLI_SUBCOMMANDS = [
|
|
534
537
|
"status", "start", "stop", "restart", "logs", "doctor", "login", "set", "rag",
|
|
535
|
-
"clear", "reset", "api", "token-usage", "tokens", "token", "full-log",
|
|
538
|
+
"clear", "reset", "api", "token-usage", "tokens", "token", "full-log", "mcp",
|
|
536
539
|
];
|
|
537
540
|
// `gu logz` USED TO START THE REPL (task #181). Silently: an unrecognised word was not in
|
|
538
541
|
// the list, so it fell through to "no subcommand given" and a full agent session opened in that
|
|
@@ -807,6 +810,7 @@ const COMMANDS = [
|
|
|
807
810
|
{ name: "/doctor", desc: "check every configured model ANSWERS — especially the embedder, which a turn only reaches after several steps" },
|
|
808
811
|
{ name: "/tokens", desc: "break down the last prompt sent to the coding model — how much of it was RAG" },
|
|
809
812
|
{ name: "/full-log", usage: "/full-log [on|off|status] [account]", desc: "administrators: Enable Full Log for an account — asks for an admin token from the web admin page first" },
|
|
813
|
+
{ name: "/mcp", usage: "/mcp [list|tools|add|remove|enable|disable|approve|allow|disallow|reconnect]", desc: "MCP servers: see which connect, add one (asks step by step), choose which tools run without asking" },
|
|
810
814
|
{ name: "/help", desc: "list these commands" },
|
|
811
815
|
{ name: "/exit", aliases: ["/quit"], desc: "quit" },
|
|
812
816
|
];
|
|
@@ -814,22 +818,50 @@ const COMMANDS = [
|
|
|
814
818
|
// terminal-layout.mjs — their handlers are below, above the unknown-command guard. Not
|
|
815
819
|
// imported here: nothing in the REPL reads the list, the tests do (task #153).
|
|
816
820
|
const COMMAND_NAMES = COMMANDS.flatMap((c) => [c.name, ...(c.aliases ?? [])]);
|
|
821
|
+
// SKILLS ARE COMMANDS TOO (task #269): each ~/.gonext/skills/<name>/SKILL.md (and this folder's
|
|
822
|
+
// .gonext/skills) is /<name>, in Tab-completion and /help like any other. Scanned from disk — kept
|
|
823
|
+
// for 2s only, because the overlay asks on every keystroke — so a skill added, edited or DELETED
|
|
824
|
+
// while gu runs shows up (or quietly disappears) without a restart. A built-in name always wins.
|
|
825
|
+
let _skillScan = { at: 0, skills: [] };
|
|
826
|
+
function currentSkills() {
|
|
827
|
+
if (Date.now() - _skillScan.at > 2000) {
|
|
828
|
+
try {
|
|
829
|
+
_skillScan = { at: Date.now(), skills: discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES }).skills };
|
|
830
|
+
} catch {
|
|
831
|
+
_skillScan = { at: Date.now(), skills: [] };
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return _skillScan.skills;
|
|
835
|
+
}
|
|
836
|
+
const skillCommands = () =>
|
|
837
|
+
currentSkills()
|
|
838
|
+
.filter((s) => s.slash)
|
|
839
|
+
.map((s) => ({ name: `/${s.name}`, usage: `/${s.name} [request]`, desc: `skill — ${firstSentence(s.description)}`, skill: true }));
|
|
840
|
+
/** A skill's description is written for the MODEL and often runs a paragraph; /help shows one line. */
|
|
841
|
+
function firstSentence(text, max = 100) {
|
|
842
|
+
const one = String(text ?? "").split(/(?<=[.!?])\s/)[0].trim();
|
|
843
|
+
return one.length > max ? one.slice(0, max - 1).trimEnd() + "…" : one;
|
|
844
|
+
}
|
|
845
|
+
const allCommands = () => [...COMMANDS, ...skillCommands()];
|
|
846
|
+
const allCommandNames = () => [...COMMAND_NAMES, ...skillCommands().map((c) => c.name)];
|
|
817
847
|
// Tab-completion: when the line starts with "/", complete against the command names.
|
|
818
848
|
function slashCompleter(line) {
|
|
819
849
|
if (!line.startsWith("/")) return [[], line];
|
|
820
|
-
const
|
|
821
|
-
|
|
850
|
+
const names = allCommandNames();
|
|
851
|
+
const hits = names.filter((n) => n.startsWith(line));
|
|
852
|
+
return [hits.length ? hits : names, line];
|
|
822
853
|
}
|
|
823
854
|
// Commands whose name/alias matches the typed prefix (all when prefix is "" or "/").
|
|
824
855
|
function filteredCommands(prefix = "") {
|
|
825
856
|
const p = (prefix ?? "").trim();
|
|
826
|
-
const
|
|
857
|
+
const cmds = allCommands();
|
|
858
|
+
const m = cmds.filter(
|
|
827
859
|
(c) =>
|
|
828
860
|
!p ||
|
|
829
861
|
p === "/" ||
|
|
830
862
|
[c.name, ...(c.aliases ?? [])].some((n) => n.startsWith(p))
|
|
831
863
|
);
|
|
832
|
-
return m.length ? m :
|
|
864
|
+
return m.length ? m : cmds;
|
|
833
865
|
}
|
|
834
866
|
// Display rows for a filtered command set; row `sel` (if >=0) is highlighted (❯ + bold).
|
|
835
867
|
function hintRows(cmds, sel) {
|
|
@@ -7185,6 +7217,21 @@ async function runFullLogCommand(line) {
|
|
|
7185
7217
|
console.log("");
|
|
7186
7218
|
}
|
|
7187
7219
|
|
|
7220
|
+
/**
|
|
7221
|
+
* /mcp (#269) — the same code as `gu mcp`. `/mcp add` alone asks step by step; its questions go
|
|
7222
|
+
* through ask(), whose label is drawn in the frozen row (a readline prompt alone is invisible there).
|
|
7223
|
+
*/
|
|
7224
|
+
async function runMcpSlashCommand(line) {
|
|
7225
|
+
const { runMcpCommand, splitWords } = await import("./mcp-command.mjs");
|
|
7226
|
+
const style = { ok: accent, warn: yellow, dim };
|
|
7227
|
+
await runMcpCommand(splitWords(line).slice(1), {
|
|
7228
|
+
cwd: process.cwd(),
|
|
7229
|
+
ask: (label) => ask(label + " "),
|
|
7230
|
+
print: (l, kind) => console.log(GUTTER + (style[kind] ?? ((x) => x))(l)),
|
|
7231
|
+
});
|
|
7232
|
+
console.log("");
|
|
7233
|
+
}
|
|
7234
|
+
|
|
7188
7235
|
async function runProxyCommand(line) {
|
|
7189
7236
|
const cmd = await import("./proxy-command.mjs");
|
|
7190
7237
|
const store = await import("./proxy-store.mjs");
|
|
@@ -8250,6 +8297,10 @@ const COMPACT_PREFIX = "__COMPACT__::";
|
|
|
8250
8297
|
// Private mode, a file to hand over, and no S3 of the user's own (task #252): set it up now, or
|
|
8251
8298
|
// save the file on this computer. Allow = S3 is now set up AND passed its test.
|
|
8252
8299
|
const S3SETUP_PREFIX = "__S3SETUP__::";
|
|
8300
|
+
// An MCP tool about to run for the first time (task #269): "<server>\t<tool>\t<args preview>".
|
|
8301
|
+
// "Always" writes an allow rule to ~/.gonext/mcp.json — the agent re-reads that file before every
|
|
8302
|
+
// call, so it counts from the next call of this same turn, and for every turn after.
|
|
8303
|
+
const MCPTOOL_PREFIX = "__MCPTOOL__::";
|
|
8253
8304
|
async function approvalPrompt(command) {
|
|
8254
8305
|
// Wipe the live "thinking" status block BEFORE any picker draws, and stop the ticker from
|
|
8255
8306
|
// redrawing it (the tick early-returns while a picker is active) — otherwise the block
|
|
@@ -8307,6 +8358,29 @@ async function approvalPrompt(command) {
|
|
|
8307
8358
|
);
|
|
8308
8359
|
return { allow: idx === 0, dontAsk: false };
|
|
8309
8360
|
}
|
|
8361
|
+
if (command.startsWith(MCPTOOL_PREFIX)) {
|
|
8362
|
+
const [server = "", tool = "", preview = ""] = command.slice(MCPTOOL_PREFIX.length).split("\t");
|
|
8363
|
+
if (!process.stdin.isTTY) return { allow: false, dontAsk: false };
|
|
8364
|
+
process.stdout.write(
|
|
8365
|
+
"\n" + yellow(`⚙ Allow the MCP tool ${server}/${tool}?`) + dim(" (no answer in ~3 min = No)") + "\n" +
|
|
8366
|
+
(preview ? dim(" " + preview) + "\n" : "")
|
|
8367
|
+
);
|
|
8368
|
+
const idx = await pickFromList(
|
|
8369
|
+
["Yes", `Yes, and always allow ${server}/${tool}`, `Yes, and always allow every ${server} tool`, "No"],
|
|
8370
|
+
0,
|
|
8371
|
+
{ timeoutMs: 170000, timeoutIndex: 3 }
|
|
8372
|
+
);
|
|
8373
|
+
if (idx === 1 || idx === 2) {
|
|
8374
|
+
const rule = idx === 1 ? `mcp__${server}__${tool}` : `mcp__${server}__*`;
|
|
8375
|
+
try {
|
|
8376
|
+
addAllowRule({ rule });
|
|
8377
|
+
process.stdout.write(dim(` (won't ask again: ${rule} — undo with /mcp disallow ${server})\n`));
|
|
8378
|
+
} catch (e) {
|
|
8379
|
+
process.stdout.write(yellow(` could not save that (${e?.message || e}) — allowed this once\n`));
|
|
8380
|
+
}
|
|
8381
|
+
}
|
|
8382
|
+
return { allow: idx >= 0 && idx <= 2, dontAsk: false };
|
|
8383
|
+
}
|
|
8310
8384
|
const isMax = command.startsWith(MAXSTEP_PREFIX);
|
|
8311
8385
|
if (isMax) {
|
|
8312
8386
|
const label = command.slice(MAXSTEP_PREFIX.length);
|
|
@@ -10665,8 +10739,33 @@ async function main() {
|
|
|
10665
10739
|
const plan = acct.plan ? (acct.pro ? accent(acct.plan) : dim(acct.plan)) : "";
|
|
10666
10740
|
statusRows.push(acct.email ? dim(`account ${acct.email}`) + (plan ? dim(" · ") + plan : "") : dim("plan ") + plan);
|
|
10667
10741
|
}
|
|
10742
|
+
// Skills and MCP servers (task #269) — one row, only when there are any.
|
|
10743
|
+
try {
|
|
10744
|
+
const nSkills = currentSkills().length;
|
|
10745
|
+
const nMcp = listMcpServers({ cwd: process.cwd() }).servers.filter((x) => x.runnable).length;
|
|
10746
|
+
const bits = [nSkills ? `${nSkills} skill${nSkills === 1 ? "" : "s"}` : "", nMcp ? `${nMcp} MCP server${nMcp === 1 ? "" : "s"}` : ""].filter(Boolean);
|
|
10747
|
+
if (bits.length) statusRows.push(dim(bits.join(" · ") + " (/help · /mcp)"));
|
|
10748
|
+
} catch {
|
|
10749
|
+
/* a broken file is reported below; the banner is not the place to fail */
|
|
10750
|
+
}
|
|
10668
10751
|
console.log(renderBanner(statusRows));
|
|
10669
10752
|
}
|
|
10753
|
+
// Skills and MCP (task #269): say what is BROKEN or WAITING, and nothing when all is well. A
|
|
10754
|
+
// deleted skill is not broken — it is simply gone, and says nothing.
|
|
10755
|
+
try {
|
|
10756
|
+
for (const w of discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES }).warnings) {
|
|
10757
|
+
console.log(GUTTER + yellow(w));
|
|
10758
|
+
}
|
|
10759
|
+
const waiting = unapprovedProjectServers(process.cwd());
|
|
10760
|
+
if (waiting.length) {
|
|
10761
|
+
console.log(
|
|
10762
|
+
GUTTER + yellow(`This folder's .mcp.json names ${waiting.length} MCP server(s) that will not start until you approve them: ${waiting.map((x) => x.name).join(", ")}`) +
|
|
10763
|
+
"\n" + GUTTER + dim(" /mcp shows the command each one runs · /mcp approve <name>")
|
|
10764
|
+
);
|
|
10765
|
+
}
|
|
10766
|
+
} catch {
|
|
10767
|
+
/* never block startup on this */
|
|
10768
|
+
}
|
|
10670
10769
|
// Task #127, Phase 3: if the base chat model server isn't answering, offer to set it up
|
|
10671
10770
|
// (download + start mlx_lm.server). BC3: only runs when NOTHING answers — an already-up
|
|
10672
10771
|
// model is left untouched, and this is skipped entirely. Interactive TTYs only.
|
|
@@ -11220,6 +11319,10 @@ async function main() {
|
|
|
11220
11319
|
await runFullLogCommand(line);
|
|
11221
11320
|
continue;
|
|
11222
11321
|
}
|
|
11322
|
+
if (line === "/mcp" || line.startsWith("/mcp ")) {
|
|
11323
|
+
await runMcpSlashCommand(line);
|
|
11324
|
+
continue;
|
|
11325
|
+
}
|
|
11223
11326
|
if (line === "/proxy" || line.startsWith("/proxy ")) {
|
|
11224
11327
|
await runProxyCommand(line);
|
|
11225
11328
|
continue;
|
|
@@ -11381,7 +11484,10 @@ async function main() {
|
|
|
11381
11484
|
}
|
|
11382
11485
|
// Any OTHER "/…" is a typo/unknown command — don't send it to the agent as a
|
|
11383
11486
|
// question; point the user at /help (all real commands were handled above).
|
|
11384
|
-
|
|
11487
|
+
// …EXCEPT a skill (task #269): "/<skill> request" goes to the agent as typed, and the agent
|
|
11488
|
+
// loads that skill's SKILL.md for the turn. Checked against the disk NOW, so a skill deleted a
|
|
11489
|
+
// moment ago is an unknown command like any other, not an error.
|
|
11490
|
+
if (line.startsWith("/") && !matchSkillCommand(line, currentSkills())) {
|
|
11385
11491
|
const cmd = line.split(/\s+/)[0];
|
|
11386
11492
|
console.log(GUTTER + dim(`unknown command: ${cmd} — type /help (or / then Tab) to list commands\n`));
|
|
11387
11493
|
continue;
|