memstack-skill-loader 4.0.3__tar.gz → 4.0.5__tar.gz
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.
- {memstack_skill_loader-4.0.3/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.0.5}/PKG-INFO +1 -1
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/pyproject.toml +1 -1
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/__init__.py +1 -1
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/agent_runner.py +272 -56
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/dashboard.html +246 -4
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/dashboard.py +69 -1
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/MANIFEST.in +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/README.md +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/setup.cfg +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/__main__.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/categories.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/compression.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/config.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/indexer.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/license.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/memory_db.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/search.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/server.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/skill_config.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/stats.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/tfidf_search.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/version_check.py +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
- {memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
|
@@ -12,6 +12,8 @@ import uuid
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Optional
|
|
14
14
|
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
15
17
|
from .stats import log_agent_invocation
|
|
16
18
|
|
|
17
19
|
|
|
@@ -26,6 +28,8 @@ AGENT_TIMEOUT = 3600 # seconds per --print invocation (default 60 minutes)
|
|
|
26
28
|
MAX_ITERATIONS = 2
|
|
27
29
|
|
|
28
30
|
ANTHROPIC_BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "")
|
|
31
|
+
API_DEFAULT_MODEL = "claude-sonnet-4-20250514"
|
|
32
|
+
API_MAX_TOKENS = 16000
|
|
29
33
|
|
|
30
34
|
SYSTEM_PROMPTS = {
|
|
31
35
|
"manager": (
|
|
@@ -84,6 +88,49 @@ SYSTEM_PROMPTS = {
|
|
|
84
88
|
),
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
BUILDER_TOOLS_CONFIG_FILE = Path.home() / ".memstack" / "builder-tools-config.json"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# MCP server discovery & Builder tools config
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def discover_mcp_servers(workdir: str) -> list[str]:
|
|
99
|
+
"""Read .mcp.json from workdir and return the list of MCP server name keys."""
|
|
100
|
+
try:
|
|
101
|
+
mcp_file = Path(workdir) / ".mcp.json"
|
|
102
|
+
if not mcp_file.is_file():
|
|
103
|
+
return []
|
|
104
|
+
data = json.loads(mcp_file.read_text(encoding="utf-8"))
|
|
105
|
+
servers = data.get("mcpServers", {})
|
|
106
|
+
return sorted(servers.keys()) if isinstance(servers, dict) else []
|
|
107
|
+
except Exception:
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def load_builder_tools_config() -> dict:
|
|
112
|
+
"""Load per-project blocked MCP server lists from ~/.memstack/builder-tools-config.json."""
|
|
113
|
+
try:
|
|
114
|
+
if BUILDER_TOOLS_CONFIG_FILE.is_file():
|
|
115
|
+
return json.loads(BUILDER_TOOLS_CONFIG_FILE.read_text(encoding="utf-8"))
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def save_builder_tools_config(config: dict) -> None:
|
|
122
|
+
"""Save per-project blocked MCP server config."""
|
|
123
|
+
BUILDER_TOOLS_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
BUILDER_TOOLS_CONFIG_FILE.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_blocked_servers_for_project(workdir: str) -> list[str]:
|
|
128
|
+
"""Return blocked MCP servers for a project, falling back to global defaults."""
|
|
129
|
+
cfg = load_builder_tools_config()
|
|
130
|
+
if workdir in cfg:
|
|
131
|
+
return cfg[workdir]
|
|
132
|
+
return cfg.get("_global_defaults", [])
|
|
133
|
+
|
|
87
134
|
|
|
88
135
|
# ---------------------------------------------------------------------------
|
|
89
136
|
# Project context gathering
|
|
@@ -188,6 +235,7 @@ def _extract_commit_from_reviewer(reviewer_output: str) -> str:
|
|
|
188
235
|
def _build_env() -> dict:
|
|
189
236
|
"""Build environment for subprocess, ensuring Anthropic vars are passed."""
|
|
190
237
|
env = os.environ.copy()
|
|
238
|
+
env.pop("MEMSTACK_ENABLE_TTS", None)
|
|
191
239
|
if ANTHROPIC_BASE_URL:
|
|
192
240
|
env["ANTHROPIC_BASE_URL"] = ANTHROPIC_BASE_URL
|
|
193
241
|
return env
|
|
@@ -254,22 +302,145 @@ def _parse_stream_json(raw: str) -> tuple[str, int, int, float, int]:
|
|
|
254
302
|
return text, input_tokens, output_tokens, cost_usd, context_tokens
|
|
255
303
|
|
|
256
304
|
|
|
305
|
+
def _extract_text_from_stream_line(line: str) -> Optional[str]:
|
|
306
|
+
"""Extract user-visible text from a single stream-json line.
|
|
307
|
+
|
|
308
|
+
Returns the text if the line contains assistant text or a result, else None.
|
|
309
|
+
"""
|
|
310
|
+
try:
|
|
311
|
+
obj = json.loads(line.strip())
|
|
312
|
+
except (json.JSONDecodeError, ValueError):
|
|
313
|
+
return None
|
|
314
|
+
msg_type = obj.get("type")
|
|
315
|
+
if msg_type == "assistant":
|
|
316
|
+
parts = []
|
|
317
|
+
for block in obj.get("message", {}).get("content", []):
|
|
318
|
+
if block.get("type") == "text":
|
|
319
|
+
t = block.get("text", "")
|
|
320
|
+
if t:
|
|
321
|
+
parts.append(t)
|
|
322
|
+
return "\n".join(parts) if parts else None
|
|
323
|
+
if msg_type == "result":
|
|
324
|
+
t = obj.get("result", "")
|
|
325
|
+
return t if t else None
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# Direct API invocation (Manager / Reviewer — no file tools needed)
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
def _invoke_api_agent(name: str, prompt: str, system_prompt: str,
|
|
334
|
+
log_path: Optional[Path] = None, timeout: int = 600,
|
|
335
|
+
model: str = "", session_id: Optional[str] = None,
|
|
336
|
+
) -> tuple[str, int, int]:
|
|
337
|
+
"""Call the Anthropic Messages API directly via httpx.
|
|
338
|
+
|
|
339
|
+
Returns (text, input_tokens, output_tokens).
|
|
340
|
+
"""
|
|
341
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
342
|
+
base_url = ANTHROPIC_BASE_URL or "https://api.anthropic.com"
|
|
343
|
+
|
|
344
|
+
if not api_key and not ANTHROPIC_BASE_URL:
|
|
345
|
+
raise RuntimeError(
|
|
346
|
+
f"{name}: ANTHROPIC_API_KEY not set and no ANTHROPIC_BASE_URL proxy configured"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
url = f"{base_url.rstrip('/')}/v1/messages"
|
|
350
|
+
headers = {
|
|
351
|
+
"content-type": "application/json",
|
|
352
|
+
"anthropic-version": "2023-06-01",
|
|
353
|
+
}
|
|
354
|
+
if api_key:
|
|
355
|
+
headers["x-api-key"] = api_key
|
|
356
|
+
|
|
357
|
+
body = {
|
|
358
|
+
"model": model or API_DEFAULT_MODEL,
|
|
359
|
+
"max_tokens": API_MAX_TOKENS,
|
|
360
|
+
"system": system_prompt,
|
|
361
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if log_path:
|
|
365
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
ts = time.strftime("%H:%M:%S")
|
|
367
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
368
|
+
f.write(f"[{ts}] === API call: {name} ===\n")
|
|
369
|
+
f.write(f"[{ts}] Model: {body['model']}\n")
|
|
370
|
+
f.write(f"[{ts}] Prompt length: {len(prompt)} chars\n")
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
resp = httpx.post(url, json=body, headers=headers, timeout=timeout)
|
|
374
|
+
resp.raise_for_status()
|
|
375
|
+
except httpx.TimeoutException:
|
|
376
|
+
if log_path:
|
|
377
|
+
ts = time.strftime("%H:%M:%S")
|
|
378
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
379
|
+
f.write(f"[{ts}] API timeout after {timeout}s\n")
|
|
380
|
+
raise subprocess.TimeoutExpired(f"api:{name}", timeout)
|
|
381
|
+
except httpx.HTTPStatusError as exc:
|
|
382
|
+
if log_path:
|
|
383
|
+
ts = time.strftime("%H:%M:%S")
|
|
384
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
385
|
+
f.write(f"[{ts}] API error {exc.response.status_code}: {exc.response.text[:500]}\n")
|
|
386
|
+
raise RuntimeError(f"{name} API error {exc.response.status_code}: {exc.response.text[:200]}") from exc
|
|
387
|
+
except httpx.HTTPError as exc:
|
|
388
|
+
if log_path:
|
|
389
|
+
ts = time.strftime("%H:%M:%S")
|
|
390
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
391
|
+
f.write(f"[{ts}] HTTP error: {exc}\n")
|
|
392
|
+
raise RuntimeError(f"{name} HTTP error: {exc}") from exc
|
|
393
|
+
|
|
394
|
+
data = resp.json()
|
|
395
|
+
text_parts = []
|
|
396
|
+
for block in data.get("content", []):
|
|
397
|
+
if block.get("type") == "text":
|
|
398
|
+
text_parts.append(block["text"])
|
|
399
|
+
output = "\n".join(text_parts).strip()
|
|
400
|
+
|
|
401
|
+
usage = data.get("usage", {})
|
|
402
|
+
input_tokens = usage.get("input_tokens", 0)
|
|
403
|
+
output_tokens = usage.get("output_tokens", 0)
|
|
404
|
+
|
|
405
|
+
if log_path:
|
|
406
|
+
ts = time.strftime("%H:%M:%S")
|
|
407
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
408
|
+
f.write(f"[{ts}] Response: {len(output)} chars, "
|
|
409
|
+
f"in={input_tokens} out={output_tokens}\n")
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
log_agent_invocation(
|
|
413
|
+
name, len(prompt), len(output), session_id, "",
|
|
414
|
+
input_tokens=input_tokens, output_tokens=output_tokens, cost_usd=0.0,
|
|
415
|
+
)
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
return output, input_tokens, output_tokens
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
# CC subprocess invocation (Builder — needs file tools)
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
|
|
257
426
|
def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[Path] = None,
|
|
258
|
-
skip_permissions: bool = False,
|
|
427
|
+
skip_permissions: bool = False,
|
|
259
428
|
session_id: Optional[str] = None, timeout: int = AGENT_TIMEOUT,
|
|
260
|
-
model: str = ""
|
|
429
|
+
model: str = "", disallowed_tools: Optional[list[str]] = None,
|
|
430
|
+
session: Optional["Session"] = None) -> tuple[str, int, int, int]:
|
|
261
431
|
"""Run a single claude --print invocation and return the output."""
|
|
262
432
|
claude_bin = shutil.which("claude")
|
|
263
433
|
if not claude_bin:
|
|
264
434
|
raise FileNotFoundError("'claude' CLI not found on PATH")
|
|
265
435
|
|
|
266
436
|
cmd = [claude_bin, "--print", "--verbose", "--output-format", "stream-json"]
|
|
267
|
-
if bare and os.environ.get("ANTHROPIC_API_KEY"):
|
|
268
|
-
cmd.append("--bare")
|
|
269
437
|
if skip_permissions:
|
|
270
438
|
cmd.append("--dangerously-skip-permissions")
|
|
271
439
|
if model:
|
|
272
440
|
cmd.extend(["--model", model])
|
|
441
|
+
if disallowed_tools:
|
|
442
|
+
patterns = ",".join(f"mcp__{srv}__*" for srv in disallowed_tools)
|
|
443
|
+
cmd.extend(["--disallowedTools", patterns])
|
|
273
444
|
|
|
274
445
|
if log_path:
|
|
275
446
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -280,65 +451,96 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
|
|
|
280
451
|
f.write(f"[{ts}] === Invoking {name} ===\n")
|
|
281
452
|
f.write(f"[{ts}] Prompt length: {len(prompt)} chars\n")
|
|
282
453
|
|
|
454
|
+
prompt_dir = log_path.parent if log_path else Path.home() / ".memstack" / "agent-runner"
|
|
455
|
+
prompt_dir.mkdir(parents=True, exist_ok=True)
|
|
456
|
+
prompt_file = prompt_dir / f"{name}_prompt.txt"
|
|
457
|
+
prompt_file.write_text(prompt, encoding="utf-8")
|
|
458
|
+
|
|
459
|
+
stdin_fh = open(prompt_file, "r", encoding="utf-8") # noqa: SIM115
|
|
283
460
|
global _current_process
|
|
284
461
|
proc = subprocess.Popen(
|
|
285
462
|
cmd,
|
|
286
|
-
stdin=
|
|
463
|
+
stdin=stdin_fh,
|
|
287
464
|
stdout=subprocess.PIPE,
|
|
288
465
|
stderr=subprocess.PIPE,
|
|
289
466
|
cwd=working_dir,
|
|
290
|
-
text=True,
|
|
291
|
-
encoding="utf-8",
|
|
292
|
-
errors="replace",
|
|
293
467
|
env=_build_env(),
|
|
294
|
-
creationflags=
|
|
468
|
+
creationflags=0,
|
|
295
469
|
)
|
|
470
|
+
stdin_fh.close()
|
|
296
471
|
with _lock:
|
|
297
472
|
_current_process = proc
|
|
298
473
|
|
|
474
|
+
if log_path:
|
|
475
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
476
|
+
f.write(f"[{ts}] PID: {proc.pid}\n")
|
|
477
|
+
|
|
478
|
+
killed_by_watchdog = threading.Event()
|
|
479
|
+
|
|
480
|
+
def _watchdog_kill() -> None:
|
|
481
|
+
killed_by_watchdog.set()
|
|
482
|
+
proc.kill()
|
|
483
|
+
|
|
299
484
|
stdout_chunks: list[str] = []
|
|
300
485
|
stderr_chunks: list[str] = []
|
|
486
|
+
partial_text: list[str] = []
|
|
487
|
+
|
|
488
|
+
def _read_stdout() -> None:
|
|
489
|
+
for raw_line in proc.stdout:
|
|
490
|
+
line = raw_line.decode("utf-8", errors="replace")
|
|
491
|
+
stdout_chunks.append(line)
|
|
492
|
+
extracted = _extract_text_from_stream_line(line)
|
|
493
|
+
if extracted and session is not None:
|
|
494
|
+
partial_text.append(extracted)
|
|
495
|
+
try:
|
|
496
|
+
session.agents[name]["last_output"] = "".join(partial_text)[-500:]
|
|
497
|
+
session._save_state()
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
301
500
|
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
501
|
+
def _read_stderr() -> None:
|
|
502
|
+
for raw_line in proc.stderr:
|
|
503
|
+
stderr_chunks.append(raw_line.decode("utf-8", errors="replace"))
|
|
504
|
+
|
|
505
|
+
t_out = threading.Thread(target=_read_stdout, daemon=True)
|
|
506
|
+
t_err = threading.Thread(target=_read_stderr, daemon=True)
|
|
507
|
+
t_out.start()
|
|
508
|
+
t_err.start()
|
|
308
509
|
|
|
309
|
-
|
|
310
|
-
|
|
510
|
+
watchdog = threading.Timer(timeout, _watchdog_kill)
|
|
511
|
+
watchdog.daemon = True
|
|
512
|
+
watchdog.start()
|
|
311
513
|
|
|
312
514
|
try:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
while True:
|
|
324
|
-
t_out.join(timeout=30)
|
|
325
|
-
rc = proc.poll()
|
|
326
|
-
if rc is not None:
|
|
327
|
-
break
|
|
328
|
-
if time.monotonic() > deadline:
|
|
329
|
-
proc.kill()
|
|
330
|
-
t_out.join(5)
|
|
331
|
-
t_err.join(5)
|
|
332
|
-
raise subprocess.TimeoutExpired(cmd, timeout)
|
|
333
|
-
t_out.join(5)
|
|
334
|
-
t_err.join(5)
|
|
335
|
-
stdout_data = "".join(stdout_chunks)
|
|
336
|
-
stderr_data = "".join(stderr_chunks)
|
|
515
|
+
proc.wait()
|
|
516
|
+
except Exception as e:
|
|
517
|
+
watchdog.cancel()
|
|
518
|
+
with _lock:
|
|
519
|
+
if _current_process is proc:
|
|
520
|
+
_current_process = None
|
|
521
|
+
if log_path:
|
|
522
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
523
|
+
f.write(f"[{ts}] wait() error: {e}\n")
|
|
524
|
+
raise RuntimeError(f"{name} wait() failed: {e}") from e
|
|
337
525
|
finally:
|
|
526
|
+
watchdog.cancel()
|
|
338
527
|
with _lock:
|
|
339
528
|
if _current_process is proc:
|
|
340
529
|
_current_process = None
|
|
341
530
|
|
|
531
|
+
t_out.join(timeout=10)
|
|
532
|
+
t_err.join(timeout=10)
|
|
533
|
+
|
|
534
|
+
if killed_by_watchdog.is_set():
|
|
535
|
+
raise subprocess.TimeoutExpired(cmd, timeout)
|
|
536
|
+
|
|
537
|
+
stdout_data = "".join(stdout_chunks)
|
|
538
|
+
stderr_data = "".join(stderr_chunks)
|
|
539
|
+
|
|
540
|
+
if log_path:
|
|
541
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
542
|
+
f.write(f"[{ts}] Process exited: stdout={len(stdout_data)} stderr={len(stderr_data)}\n")
|
|
543
|
+
|
|
342
544
|
raw_stdout = stdout_data or ""
|
|
343
545
|
stderr = stderr_data or ""
|
|
344
546
|
output, input_tokens, output_tokens, cost_usd, context_tokens = _parse_stream_json(raw_stdout)
|
|
@@ -383,7 +585,8 @@ class Session:
|
|
|
383
585
|
"""Tracks the full state of an agent run session."""
|
|
384
586
|
|
|
385
587
|
def __init__(self, task: str, working_dir: str, context: Optional[str] = None, auto_commit: bool = False, timeout_minutes: int = 60,
|
|
386
|
-
manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = ""
|
|
588
|
+
manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = "",
|
|
589
|
+
blocked_mcp_servers: Optional[list[str]] = None):
|
|
387
590
|
self.session_id = uuid.uuid4().hex[:12]
|
|
388
591
|
self.task = task
|
|
389
592
|
self.working_dir = working_dir
|
|
@@ -391,6 +594,7 @@ class Session:
|
|
|
391
594
|
self.user_name = user_name
|
|
392
595
|
self.context = context
|
|
393
596
|
self.auto_commit = auto_commit
|
|
597
|
+
self.blocked_mcp_servers = blocked_mcp_servers or []
|
|
394
598
|
self.auto_committed = False
|
|
395
599
|
self.timeout = max(5, min(120, timeout_minutes)) * 60
|
|
396
600
|
self.status = "running"
|
|
@@ -505,6 +709,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
505
709
|
# Step 1: Manager analyzes the task
|
|
506
710
|
session.agents["manager"]["status"] = "busy"
|
|
507
711
|
session.agents["manager"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
712
|
+
session.agents["manager"]["last_output"] = ""
|
|
508
713
|
session._save_state()
|
|
509
714
|
|
|
510
715
|
context = _gather_project_context(session.working_dir)
|
|
@@ -515,17 +720,18 @@ def _orchestrate(session: Session) -> None:
|
|
|
515
720
|
if session.user_name:
|
|
516
721
|
manager_prompt = f"The user's name is {session.user_name}.\n\n" + manager_prompt
|
|
517
722
|
try:
|
|
518
|
-
manager_output, m_in, m_out
|
|
519
|
-
"manager", manager_prompt,
|
|
723
|
+
manager_output, m_in, m_out = _invoke_api_agent(
|
|
724
|
+
"manager", manager_prompt,
|
|
725
|
+
system_prompt=SYSTEM_PROMPTS["manager"],
|
|
520
726
|
log_path=session_log_dir / "manager.log",
|
|
521
|
-
|
|
522
|
-
timeout=min(180, session.timeout),
|
|
727
|
+
timeout=min(600, session.timeout),
|
|
523
728
|
model=session.models.get("manager", ""),
|
|
729
|
+
session_id=session.session_id,
|
|
524
730
|
)
|
|
525
731
|
except subprocess.TimeoutExpired:
|
|
526
732
|
session.agents["manager"]["status"] = "timeout"
|
|
527
733
|
session.status = "error"
|
|
528
|
-
session.result = "Manager timed out after
|
|
734
|
+
session.result = "Manager timed out after 10 minutes. Try a simpler task description or break the task into smaller pieces."
|
|
529
735
|
session._save_state()
|
|
530
736
|
return
|
|
531
737
|
except RuntimeError:
|
|
@@ -536,7 +742,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
536
742
|
return
|
|
537
743
|
session.agents["manager"]["input_tokens"] += m_in
|
|
538
744
|
session.agents["manager"]["output_tokens"] += m_out
|
|
539
|
-
session.agents["manager"]["context_tokens"] =
|
|
745
|
+
session.agents["manager"]["context_tokens"] = m_in
|
|
540
746
|
session.agents["manager"]["last_output"] = (manager_output or "")[:500]
|
|
541
747
|
|
|
542
748
|
session.agents["manager"]["status"] = "done"
|
|
@@ -563,6 +769,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
563
769
|
# Builder
|
|
564
770
|
session.agents["builder"]["status"] = "busy"
|
|
565
771
|
session.agents["builder"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
772
|
+
session.agents["builder"]["last_output"] = ""
|
|
566
773
|
session._save_state()
|
|
567
774
|
|
|
568
775
|
builder_prompt = (
|
|
@@ -578,9 +785,11 @@ def _orchestrate(session: Session) -> None:
|
|
|
578
785
|
builder_output, b_in, b_out, b_ctx = _invoke_agent(
|
|
579
786
|
"builder", builder_prompt, session.working_dir,
|
|
580
787
|
log_path=session_log_dir / "builder.log",
|
|
581
|
-
skip_permissions=True,
|
|
788
|
+
skip_permissions=True, session_id=session.session_id,
|
|
582
789
|
timeout=session.timeout,
|
|
583
790
|
model=session.models.get("builder", ""),
|
|
791
|
+
disallowed_tools=session.blocked_mcp_servers,
|
|
792
|
+
session=session,
|
|
584
793
|
)
|
|
585
794
|
except subprocess.TimeoutExpired:
|
|
586
795
|
session.agents["builder"]["status"] = "timeout"
|
|
@@ -630,6 +839,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
630
839
|
# Reviewer
|
|
631
840
|
session.agents["reviewer"]["status"] = "busy"
|
|
632
841
|
session.agents["reviewer"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
842
|
+
session.agents["reviewer"]["last_output"] = ""
|
|
633
843
|
session._save_state()
|
|
634
844
|
|
|
635
845
|
reviewer_prompt = (
|
|
@@ -638,12 +848,13 @@ def _orchestrate(session: Session) -> None:
|
|
|
638
848
|
+ f"\n\nBuilder output (iteration {iteration}):\n{builder_output}"
|
|
639
849
|
)
|
|
640
850
|
try:
|
|
641
|
-
reviewer_output, r_in, r_out
|
|
642
|
-
"reviewer", reviewer_prompt,
|
|
851
|
+
reviewer_output, r_in, r_out = _invoke_api_agent(
|
|
852
|
+
"reviewer", reviewer_prompt,
|
|
853
|
+
system_prompt=SYSTEM_PROMPTS["reviewer"],
|
|
643
854
|
log_path=session_log_dir / "reviewer.log",
|
|
644
|
-
bare=True, session_id=session.session_id,
|
|
645
855
|
timeout=session.timeout,
|
|
646
856
|
model=session.models.get("reviewer", ""),
|
|
857
|
+
session_id=session.session_id,
|
|
647
858
|
)
|
|
648
859
|
except subprocess.TimeoutExpired:
|
|
649
860
|
session.agents["reviewer"]["status"] = "timeout"
|
|
@@ -659,7 +870,7 @@ def _orchestrate(session: Session) -> None:
|
|
|
659
870
|
return
|
|
660
871
|
session.agents["reviewer"]["input_tokens"] += r_in
|
|
661
872
|
session.agents["reviewer"]["output_tokens"] += r_out
|
|
662
|
-
session.agents["reviewer"]["context_tokens"] =
|
|
873
|
+
session.agents["reviewer"]["context_tokens"] = r_in
|
|
663
874
|
session.agents["reviewer"]["last_output"] = (reviewer_output or "")[:500]
|
|
664
875
|
|
|
665
876
|
session.agents["reviewer"]["status"] = "done"
|
|
@@ -772,7 +983,8 @@ _BLOCKED_NIX = {"/etc", "/var", "/proc", "/sys", "/boot"}
|
|
|
772
983
|
|
|
773
984
|
|
|
774
985
|
def start_run(task: str, working_dir: Optional[str] = None, context: Optional[str] = None, auto_commit: bool = True, timeout_minutes: int = 60,
|
|
775
|
-
manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = ""
|
|
986
|
+
manager_model: str = "", builder_model: str = "", reviewer_model: str = "", user_name: str = "",
|
|
987
|
+
blocked_mcp_servers: Optional[list[str]] = None) -> dict:
|
|
776
988
|
"""Start a new agent run. Returns session info."""
|
|
777
989
|
global _current_session, _orchestration_thread
|
|
778
990
|
|
|
@@ -791,18 +1003,22 @@ def start_run(task: str, working_dir: Optional[str] = None, context: Optional[st
|
|
|
791
1003
|
if any(str_wd == bp or str_wd.startswith(bp + "/") for bp in _BLOCKED_NIX):
|
|
792
1004
|
return {"error": f"Working directory not allowed: {wd}"}
|
|
793
1005
|
|
|
1006
|
+
if blocked_mcp_servers is None:
|
|
1007
|
+
blocked_mcp_servers = get_blocked_servers_for_project(str(wd))
|
|
1008
|
+
|
|
794
1009
|
with _lock:
|
|
795
1010
|
if _current_session and _current_session.status == "running":
|
|
796
1011
|
return {"error": "A session is already running", "session_id": _current_session.session_id}
|
|
797
1012
|
|
|
798
1013
|
session = Session(task=task, working_dir=working_dir, context=context, auto_commit=auto_commit, timeout_minutes=timeout_minutes,
|
|
799
|
-
manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name
|
|
1014
|
+
manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name,
|
|
1015
|
+
blocked_mcp_servers=blocked_mcp_servers)
|
|
800
1016
|
_current_session = session
|
|
801
1017
|
|
|
802
1018
|
session._save_state()
|
|
803
1019
|
|
|
804
1020
|
_orchestration_thread = threading.Thread(
|
|
805
|
-
target=_orchestrate, args=(session,), daemon=
|
|
1021
|
+
target=_orchestrate, args=(session,), daemon=False, name="orchestrator"
|
|
806
1022
|
)
|
|
807
1023
|
_orchestration_thread.start()
|
|
808
1024
|
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/dashboard.html
RENAMED
|
@@ -835,6 +835,76 @@
|
|
|
835
835
|
font-size: 0.82rem;
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
+
/* ── Builder MCP Tools section ── */
|
|
839
|
+
.mcp-tools-details {
|
|
840
|
+
margin-bottom: 0.8rem;
|
|
841
|
+
}
|
|
842
|
+
.mcp-tools-summary {
|
|
843
|
+
cursor: pointer;
|
|
844
|
+
font-size: 0.82rem;
|
|
845
|
+
color: #8b949e;
|
|
846
|
+
font-weight: 600;
|
|
847
|
+
list-style: none;
|
|
848
|
+
display: flex;
|
|
849
|
+
align-items: center;
|
|
850
|
+
gap: 0.4rem;
|
|
851
|
+
}
|
|
852
|
+
.mcp-tools-summary::-webkit-details-marker { display: none; }
|
|
853
|
+
.mcp-tools-summary::before {
|
|
854
|
+
content: '▶';
|
|
855
|
+
font-size: 0.6rem;
|
|
856
|
+
transition: transform 0.2s;
|
|
857
|
+
}
|
|
858
|
+
.mcp-tools-details[open] > .mcp-tools-summary::before {
|
|
859
|
+
transform: rotate(90deg);
|
|
860
|
+
}
|
|
861
|
+
.mcp-tools-summary:hover { color: #c9d1d9; }
|
|
862
|
+
.mcp-tools-list {
|
|
863
|
+
margin-top: 0.5rem;
|
|
864
|
+
display: flex;
|
|
865
|
+
flex-direction: column;
|
|
866
|
+
gap: 0.3rem;
|
|
867
|
+
}
|
|
868
|
+
.mcp-tools-row {
|
|
869
|
+
display: flex;
|
|
870
|
+
align-items: center;
|
|
871
|
+
gap: 0.5rem;
|
|
872
|
+
font-size: 0.82rem;
|
|
873
|
+
color: #c9d1d9;
|
|
874
|
+
}
|
|
875
|
+
.mcp-tools-row input[type="checkbox"] {
|
|
876
|
+
accent-color: #238636;
|
|
877
|
+
width: 14px;
|
|
878
|
+
height: 14px;
|
|
879
|
+
cursor: pointer;
|
|
880
|
+
flex-shrink: 0;
|
|
881
|
+
}
|
|
882
|
+
.mcp-tools-row label { cursor: pointer; font-family: monospace; }
|
|
883
|
+
.mcp-tools-hint {
|
|
884
|
+
font-size: 0.72rem;
|
|
885
|
+
color: #484f58;
|
|
886
|
+
margin-top: 0.4rem;
|
|
887
|
+
}
|
|
888
|
+
.mcp-tools-save-btn {
|
|
889
|
+
margin-top: 0.5rem;
|
|
890
|
+
background: #21262d;
|
|
891
|
+
color: #8b949e;
|
|
892
|
+
border: 1px solid #30363d;
|
|
893
|
+
padding: 0.3rem 0.8rem;
|
|
894
|
+
border-radius: 6px;
|
|
895
|
+
font-size: 0.75rem;
|
|
896
|
+
cursor: pointer;
|
|
897
|
+
transition: background 0.2s, color 0.2s;
|
|
898
|
+
}
|
|
899
|
+
.mcp-tools-save-btn:hover { background: #30363d; color: #c9d1d9; }
|
|
900
|
+
.mcp-tools-saved {
|
|
901
|
+
font-size: 0.72rem;
|
|
902
|
+
color: #39d353;
|
|
903
|
+
margin-left: 0.5rem;
|
|
904
|
+
opacity: 0;
|
|
905
|
+
transition: opacity 0.3s;
|
|
906
|
+
}
|
|
907
|
+
|
|
838
908
|
/* ── Dancing status phrases ── */
|
|
839
909
|
.agent-status-phrase {
|
|
840
910
|
transition: opacity 0.3s ease;
|
|
@@ -1431,7 +1501,7 @@
|
|
|
1431
1501
|
<label style="display:block;font-size:0.82rem;color:#8b949e;margin-bottom:0.4rem;font-weight:600;">Task Description</label>
|
|
1432
1502
|
<div style="position:relative;margin-bottom:1rem;">
|
|
1433
1503
|
<textarea id="agent-task-input" rows="4" placeholder="Describe what you want the agents to build..." style="width:100%;background:#0d1117;border:1px solid #30363d;border-radius:6px;color:#e6edf3;padding:0.7rem;font-size:0.88rem;resize:vertical;font-family:inherit;margin:0;"></textarea>
|
|
1434
|
-
<button onclick="document.getElementById('agent-task-input').value='';document.getElementById('agent-workdir-input').value='';document.getElementById('agent-context-input').value='';document.getElementById('agent-task-input').focus();" style="position:absolute;top:4px;right:4px;background:#21262d;border:1px solid #30363d;color:#8b949e;padding:8px;border-radius:4px;cursor:pointer;font-size:14px;line-height:1;z-index:1;" onmouseenter="this.style.color='#e6edf3'" onmouseleave="this.style.color='#8b949e'" title="Clear task and working directory">×</button>
|
|
1504
|
+
<button onclick="document.getElementById('agent-task-input').value='';document.getElementById('agent-workdir-input').value='';document.getElementById('agent-context-input').value='';fetchMcpServers('');document.getElementById('agent-task-input').focus();" style="position:absolute;top:4px;right:4px;background:#21262d;border:1px solid #30363d;color:#8b949e;padding:8px;border-radius:4px;cursor:pointer;font-size:14px;line-height:1;z-index:1;" onmouseenter="this.style.color='#e6edf3'" onmouseleave="this.style.color='#8b949e'" title="Clear task and working directory">×</button>
|
|
1435
1505
|
</div>
|
|
1436
1506
|
<label style="display:block;font-size:0.82rem;color:#8b949e;margin-bottom:0.4rem;font-weight:600;">Working Directory</label>
|
|
1437
1507
|
<div style="display:flex;gap:0.5rem;margin-bottom:0.3rem;">
|
|
@@ -1474,6 +1544,20 @@
|
|
|
1474
1544
|
</div>
|
|
1475
1545
|
</div>
|
|
1476
1546
|
</details>
|
|
1547
|
+
<details class="mcp-tools-details" id="mcp-tools-details">
|
|
1548
|
+
<summary class="mcp-tools-summary" id="mcp-tools-summary">Builder MCP Tools</summary>
|
|
1549
|
+
<div id="mcp-tools-container">
|
|
1550
|
+
<div id="mcp-tools-loading" style="font-size:0.78rem;color:#484f58;padding:0.3rem 0;">Enter a working directory to discover MCP servers</div>
|
|
1551
|
+
<div id="mcp-tools-list" class="mcp-tools-list" style="display:none;"></div>
|
|
1552
|
+
<div id="mcp-tools-empty" style="display:none;font-size:0.78rem;color:#484f58;padding:0.3rem 0;">No MCP servers found in this project</div>
|
|
1553
|
+
<p class="mcp-tools-hint" id="mcp-tools-hint" style="display:none;">Unchecked servers will be excluded from the Builder's context to reduce token usage</p>
|
|
1554
|
+
<p id="mcp-tools-global-note" style="display:none;font-size:0.72rem;color:#d29922;margin:0.3rem 0 0;">Using global defaults</p>
|
|
1555
|
+
<div id="mcp-tools-actions" style="display:none;margin-top:0.4rem;">
|
|
1556
|
+
<button class="mcp-tools-save-btn" onclick="saveMcpToolsConfig()">Save as Default</button>
|
|
1557
|
+
<span class="mcp-tools-saved" id="mcp-tools-saved">Saved</span>
|
|
1558
|
+
</div>
|
|
1559
|
+
</div>
|
|
1560
|
+
</details>
|
|
1477
1561
|
<label for="agent-autocommit-checkbox" style="display:flex;align-items:center;gap:0.5rem;font-size:0.82rem;color:#8b949e;margin-bottom:1rem;cursor:pointer;position:relative;z-index:1;">
|
|
1478
1562
|
<input id="agent-autocommit-checkbox" type="checkbox" style="accent-color:#238636;width:15px;height:15px;cursor:pointer;flex-shrink:0;">
|
|
1479
1563
|
Auto-commit on approval
|
|
@@ -1644,6 +1728,36 @@
|
|
|
1644
1728
|
</div>
|
|
1645
1729
|
</div>
|
|
1646
1730
|
|
|
1731
|
+
<div class="panel">
|
|
1732
|
+
<h3>Agent Runner Setup</h3>
|
|
1733
|
+
<p style="color:#8b949e;font-size:0.82rem;margin:0 0 0.8rem;">
|
|
1734
|
+
The Agent Runner requires an Anthropic API key for the Manager and Reviewer agents.
|
|
1735
|
+
The Builder uses Claude Code directly. Headroom is optional but recommended for token compression.
|
|
1736
|
+
</p>
|
|
1737
|
+
<div style="display:grid;grid-template-columns:auto 1fr;gap:0.5rem 1rem;font-size:0.82rem;align-items:start;">
|
|
1738
|
+
<span style="color:#8b949e;">API Key</span>
|
|
1739
|
+
<span id="settings-api-key-status" style="color:#c9d1d9;">—</span>
|
|
1740
|
+
<span style="color:#8b949e;">Headroom Proxy</span>
|
|
1741
|
+
<span id="settings-proxy-status" style="color:#c9d1d9;">—</span>
|
|
1742
|
+
</div>
|
|
1743
|
+
</div>
|
|
1744
|
+
|
|
1745
|
+
<div class="panel">
|
|
1746
|
+
<details class="mcp-tools-details">
|
|
1747
|
+
<summary class="mcp-tools-summary">Default MCP Tools</summary>
|
|
1748
|
+
<div style="padding:0.3rem 0;">
|
|
1749
|
+
<p style="color:#8b949e;font-size:0.78rem;margin:0 0 0.6rem;">Set which MCP servers are blocked by default across all projects. Per-project settings override these defaults.</p>
|
|
1750
|
+
<label style="font-size:0.78rem;color:#8b949e;display:block;margin-bottom:0.3rem;">Blocked servers (comma-separated)</label>
|
|
1751
|
+
<input type="text" id="settings-global-mcp-blocked" class="settings-text-input" placeholder="e.g. agent-bridge, connectstack-uptimerobot">
|
|
1752
|
+
<div style="display:flex;gap:0.5rem;margin-top:0.5rem;align-items:center;">
|
|
1753
|
+
<button class="mcp-tools-save-btn" onclick="saveGlobalMcpDefaults(false)">Save Defaults</button>
|
|
1754
|
+
<button class="mcp-tools-save-btn" style="background:#30363d;" onclick="applyGlobalMcpToAll()">Apply to All Projects</button>
|
|
1755
|
+
<span id="settings-global-mcp-saved" class="mcp-tools-saved">Saved</span>
|
|
1756
|
+
</div>
|
|
1757
|
+
</div>
|
|
1758
|
+
</details>
|
|
1759
|
+
</div>
|
|
1760
|
+
|
|
1647
1761
|
<div class="panel">
|
|
1648
1762
|
<h3>Dashboard Info</h3>
|
|
1649
1763
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:0.4rem 1.2rem;font-size:0.82rem;">
|
|
@@ -2805,6 +2919,8 @@ async function startAgentTask() {
|
|
|
2805
2919
|
body.builder_model = document.getElementById('agent-model-builder').value;
|
|
2806
2920
|
body.reviewer_model = document.getElementById('agent-model-reviewer').value;
|
|
2807
2921
|
body.user_name = userProfile.user_name || '';
|
|
2922
|
+
const blockedMcp = getBlockedMcpServers();
|
|
2923
|
+
if (blockedMcp.length) body.blocked_mcp_servers = blockedMcp;
|
|
2808
2924
|
const res = await fetch('/api/agent-run', {method:'POST', headers: AUTH_HEADERS, body: JSON.stringify(body)});
|
|
2809
2925
|
const data = await res.json();
|
|
2810
2926
|
if (data.error) { alert(data.error); return; }
|
|
@@ -2843,6 +2959,12 @@ async function loadAgentMonitor() {
|
|
|
2843
2959
|
lbl.textContent = userProfile.agent_names[role];
|
|
2844
2960
|
}
|
|
2845
2961
|
}
|
|
2962
|
+
updateMcpToolsSummaryLabel();
|
|
2963
|
+
const wdInput = document.getElementById('agent-workdir-input');
|
|
2964
|
+
if (wdInput && !wdInput._mcpBound) {
|
|
2965
|
+
wdInput._mcpBound = true;
|
|
2966
|
+
wdInput.addEventListener('blur', () => fetchMcpServers(wdInput.value.trim()));
|
|
2967
|
+
}
|
|
2846
2968
|
fetchAgentStatus();
|
|
2847
2969
|
loadRecentProjects();
|
|
2848
2970
|
loadLastWorkdir();
|
|
@@ -2858,10 +2980,110 @@ async function loadLastWorkdir() {
|
|
|
2858
2980
|
if (input.value.trim()) return;
|
|
2859
2981
|
const res = await fetch('/api/last-workdir', {headers: {'X-Auth-Token': AUTH_TOKEN}});
|
|
2860
2982
|
const data = await res.json();
|
|
2861
|
-
if (data.path) input.value = data.path;
|
|
2983
|
+
if (data.path) { input.value = data.path; fetchMcpServers(data.path); }
|
|
2862
2984
|
} catch(e) { /* ignore */ }
|
|
2863
2985
|
}
|
|
2864
2986
|
|
|
2987
|
+
/* ─── Builder MCP Tools ─── */
|
|
2988
|
+
let _mcpServers = [];
|
|
2989
|
+
let _mcpBlocked = new Set();
|
|
2990
|
+
|
|
2991
|
+
function updateMcpToolsSummaryLabel() {
|
|
2992
|
+
const el = document.getElementById('mcp-tools-summary');
|
|
2993
|
+
if (!el) return;
|
|
2994
|
+
const name = (userProfile.agent_names && userProfile.agent_names.builder) || 'Builder';
|
|
2995
|
+
el.textContent = name + ' MCP Tools';
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
async function fetchMcpServers(workdir) {
|
|
2999
|
+
const listEl = document.getElementById('mcp-tools-list');
|
|
3000
|
+
const emptyEl = document.getElementById('mcp-tools-empty');
|
|
3001
|
+
const loadingEl = document.getElementById('mcp-tools-loading');
|
|
3002
|
+
const hintEl = document.getElementById('mcp-tools-hint');
|
|
3003
|
+
const actionsEl = document.getElementById('mcp-tools-actions');
|
|
3004
|
+
if (!workdir) {
|
|
3005
|
+
listEl.style.display = 'none'; emptyEl.style.display = 'none'; hintEl.style.display = 'none'; actionsEl.style.display = 'none';
|
|
3006
|
+
const gn = document.getElementById('mcp-tools-global-note'); if (gn) gn.style.display = 'none';
|
|
3007
|
+
loadingEl.style.display = ''; loadingEl.textContent = 'Enter a working directory to discover MCP servers';
|
|
3008
|
+
_mcpServers = []; _mcpBlocked = new Set();
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
const globalNoteEl = document.getElementById('mcp-tools-global-note');
|
|
3012
|
+
loadingEl.style.display = ''; loadingEl.textContent = 'Discovering MCP servers...';
|
|
3013
|
+
listEl.style.display = 'none'; emptyEl.style.display = 'none'; hintEl.style.display = 'none'; actionsEl.style.display = 'none';
|
|
3014
|
+
if (globalNoteEl) globalNoteEl.style.display = 'none';
|
|
3015
|
+
try {
|
|
3016
|
+
const res = await fetch('/api/mcp-servers?workdir=' + encodeURIComponent(workdir), {headers: AUTH_GET});
|
|
3017
|
+
const data = await res.json();
|
|
3018
|
+
_mcpServers = data.servers || [];
|
|
3019
|
+
_mcpBlocked = new Set(data.blocked || []);
|
|
3020
|
+
loadingEl.style.display = 'none';
|
|
3021
|
+
if (_mcpServers.length === 0) {
|
|
3022
|
+
emptyEl.style.display = ''; return;
|
|
3023
|
+
}
|
|
3024
|
+
listEl.innerHTML = _mcpServers.map(srv => {
|
|
3025
|
+
const checked = !_mcpBlocked.has(srv) ? 'checked' : '';
|
|
3026
|
+
const id = 'mcp-srv-' + srv.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3027
|
+
return `<div class="mcp-tools-row"><input type="checkbox" id="${id}" value="${escapeHtml(srv)}" ${checked}><label for="${id}">${escapeHtml(srv)}</label></div>`;
|
|
3028
|
+
}).join('');
|
|
3029
|
+
listEl.style.display = ''; hintEl.style.display = ''; actionsEl.style.display = '';
|
|
3030
|
+
if (globalNoteEl && !data.has_project_config && (data.global_blocked || []).length > 0) {
|
|
3031
|
+
globalNoteEl.style.display = '';
|
|
3032
|
+
}
|
|
3033
|
+
} catch(e) {
|
|
3034
|
+
loadingEl.style.display = 'none'; emptyEl.style.display = ''; emptyEl.textContent = 'Failed to discover MCP servers';
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
function getBlockedMcpServers() {
|
|
3039
|
+
const blocked = [];
|
|
3040
|
+
for (const srv of _mcpServers) {
|
|
3041
|
+
const id = 'mcp-srv-' + srv.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3042
|
+
const cb = document.getElementById(id);
|
|
3043
|
+
if (cb && !cb.checked) blocked.push(srv);
|
|
3044
|
+
}
|
|
3045
|
+
return blocked;
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
async function saveMcpToolsConfig() {
|
|
3049
|
+
const workdir = document.getElementById('agent-workdir-input').value.trim();
|
|
3050
|
+
if (!workdir) return;
|
|
3051
|
+
const blocked = getBlockedMcpServers();
|
|
3052
|
+
try {
|
|
3053
|
+
await fetch('/api/builder-tools-config', {method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify({workdir, blocked})});
|
|
3054
|
+
const savedEl = document.getElementById('mcp-tools-saved');
|
|
3055
|
+
savedEl.style.opacity = '1';
|
|
3056
|
+
setTimeout(() => { savedEl.style.opacity = '0'; }, 2000);
|
|
3057
|
+
} catch(e) { /* ignore */ }
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
async function loadGlobalMcpDefaults() {
|
|
3061
|
+
try {
|
|
3062
|
+
const res = await fetch('/api/global-mcp-defaults', {headers: AUTH_GET});
|
|
3063
|
+
const d = await res.json();
|
|
3064
|
+
const el = document.getElementById('settings-global-mcp-blocked');
|
|
3065
|
+
if (el) el.value = (d.blocked || []).join(', ');
|
|
3066
|
+
} catch(e) { /* ignore */ }
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
async function saveGlobalMcpDefaults(applyAll) {
|
|
3070
|
+
const el = document.getElementById('settings-global-mcp-blocked');
|
|
3071
|
+
if (!el) return;
|
|
3072
|
+
const blocked = el.value.split(',').map(s => s.trim()).filter(Boolean);
|
|
3073
|
+
try {
|
|
3074
|
+
await fetch('/api/global-mcp-defaults', {method: 'POST', headers: AUTH_HEADERS, body: JSON.stringify({blocked, apply_all: !!applyAll})});
|
|
3075
|
+
const savedEl = document.getElementById('settings-global-mcp-saved');
|
|
3076
|
+
savedEl.textContent = applyAll ? 'Applied to all' : 'Saved';
|
|
3077
|
+
savedEl.style.opacity = '1';
|
|
3078
|
+
setTimeout(() => { savedEl.style.opacity = '0'; }, 2000);
|
|
3079
|
+
} catch(e) { /* ignore */ }
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
function applyGlobalMcpToAll() {
|
|
3083
|
+
if (!confirm('This will overwrite all per-project MCP settings. Continue?')) return;
|
|
3084
|
+
saveGlobalMcpDefaults(true);
|
|
3085
|
+
}
|
|
3086
|
+
|
|
2865
3087
|
function autoResize(textarea) {
|
|
2866
3088
|
textarea.style.height = 'auto';
|
|
2867
3089
|
textarea.style.height = textarea.scrollHeight + 'px';
|
|
@@ -2970,7 +3192,7 @@ async function loadRecentProjects() {
|
|
|
2970
3192
|
if (!dirs.length) { dropdown.style.display = 'none'; return; }
|
|
2971
3193
|
dropdown.style.display = 'block';
|
|
2972
3194
|
dropdown.innerHTML = dirs.map(d =>
|
|
2973
|
-
`<div style="padding:0.4rem 0.7rem;cursor:pointer;font-size:0.8rem;font-family:monospace;color:#8b949e;border-bottom:1px solid #21262d;transition:background 0.15s;" onmouseenter="this.style.background='#161b22';this.style.color='#e6edf3'" onmouseleave="this.style.background='';this.style.color='#8b949e'" onclick="document.getElementById('agent-workdir-input').value=this.textContent">${escapeHtml(d)}</div>`
|
|
3195
|
+
`<div style="padding:0.4rem 0.7rem;cursor:pointer;font-size:0.8rem;font-family:monospace;color:#8b949e;border-bottom:1px solid #21262d;transition:background 0.15s;" onmouseenter="this.style.background='#161b22';this.style.color='#e6edf3'" onmouseleave="this.style.background='';this.style.color='#8b949e'" onclick="document.getElementById('agent-workdir-input').value=this.textContent;fetchMcpServers(this.textContent)">${escapeHtml(d)}</div>`
|
|
2974
3196
|
).join('');
|
|
2975
3197
|
} catch(e) { /* ignore */ }
|
|
2976
3198
|
}
|
|
@@ -2992,6 +3214,7 @@ function closeDirBrowser() {
|
|
|
2992
3214
|
function selectDirBrowser() {
|
|
2993
3215
|
document.getElementById('agent-workdir-input').value = dirBrowserCurrentPath;
|
|
2994
3216
|
closeDirBrowser();
|
|
3217
|
+
fetchMcpServers(dirBrowserCurrentPath);
|
|
2995
3218
|
}
|
|
2996
3219
|
|
|
2997
3220
|
function dirBrowserBack() {
|
|
@@ -3231,6 +3454,7 @@ function renderAgentUI(data) {
|
|
|
3231
3454
|
const pulseStyle = a.status === 'busy' ? 'animation:pulse-busy 2s infinite;' : '';
|
|
3232
3455
|
const roleIcon = role === 'manager' ? '💼' : role === 'builder' ? '🛠' : '🔍';
|
|
3233
3456
|
let statusDisplay;
|
|
3457
|
+
const hasStreamOutput = a.status === 'busy' && a.last_output && a.last_output.trim().length > 0;
|
|
3234
3458
|
if (a.status === 'busy' && BUSY_PHRASES[role]) {
|
|
3235
3459
|
const phrase = BUSY_PHRASES[role][busyPhraseIndices[role] % BUSY_PHRASES[role].length];
|
|
3236
3460
|
const customName = (userProfile.agent_names && userProfile.agent_names[role]) || role.charAt(0).toUpperCase() + role.slice(1);
|
|
@@ -3276,6 +3500,8 @@ function renderAgentUI(data) {
|
|
|
3276
3500
|
}
|
|
3277
3501
|
}
|
|
3278
3502
|
const errorBorder = (a.status === 'error' || a.status === 'timeout' || a.status === 'crashed') ? ' agent-card-error-border' : '';
|
|
3503
|
+
const outputMaxHeight = hasStreamOutput ? '6em' : '3em';
|
|
3504
|
+
const outputSnippet = escapeHtml((a.last_output || '').substring(0, hasStreamOutput ? 500 : 150));
|
|
3279
3505
|
return `<div class="agent-card${errorBorder}" style="${pulseStyle}">
|
|
3280
3506
|
<span class="agent-tooltip">${escapeHtml(ROLE_DESCRIPTIONS[role] || '')}</span>
|
|
3281
3507
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.6rem;">
|
|
@@ -3283,7 +3509,7 @@ function renderAgentUI(data) {
|
|
|
3283
3509
|
<span style="background:${color}20;color:${color};padding:2px 8px;border-radius:12px;font-size:0.7rem;font-weight:600;text-transform:uppercase;">${statusDisplay}</span>
|
|
3284
3510
|
</div>
|
|
3285
3511
|
<div style="font-size:0.75rem;color:#8b949e;line-height:1.6;">
|
|
3286
|
-
<div style="margin-top:0.3rem;color:#c9d1d9;font-size:0.72rem;max-height
|
|
3512
|
+
<div style="margin-top:0.3rem;color:#c9d1d9;font-size:0.72rem;max-height:${outputMaxHeight};overflow:hidden;text-overflow:ellipsis;white-space:pre-wrap;word-break:break-word;">${outputSnippet}</div>
|
|
3287
3513
|
</div>
|
|
3288
3514
|
${elapsedHtml}
|
|
3289
3515
|
${contextHtml}
|
|
@@ -3481,8 +3707,24 @@ async function loadSettings() {
|
|
|
3481
3707
|
document.getElementById('settings-pro-dir').textContent = d.pro_skills_dir || '—';
|
|
3482
3708
|
document.getElementById('settings-stats-db').textContent = d.stats_db || '—';
|
|
3483
3709
|
document.getElementById('settings-sessions-dir').textContent = d.sessions_dir || '—';
|
|
3710
|
+
|
|
3711
|
+
const apiKeyEl = document.getElementById('settings-api-key-status');
|
|
3712
|
+
if (d.api_key_set) {
|
|
3713
|
+
apiKeyEl.innerHTML = '<span style="color:#3fb950;">✓</span> API key detected';
|
|
3714
|
+
} else {
|
|
3715
|
+
apiKeyEl.innerHTML = '<span style="color:#f85149;">✗</span> Not set — run <code style="background:#161b22;padding:0.1rem 0.4rem;border-radius:3px;color:#c9d1d9;">set ANTHROPIC_API_KEY=sk-ant-...</code> before starting the dashboard';
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
const proxyEl = document.getElementById('settings-proxy-status');
|
|
3719
|
+
if (d.base_url) {
|
|
3720
|
+
proxyEl.innerHTML = '<span style="color:#3fb950;">✓</span> Headroom proxy active at <code style="background:#161b22;padding:0.1rem 0.4rem;border-radius:3px;color:#c9d1d9;">' + d.base_url.replace(/</g,'<') + '</code>';
|
|
3721
|
+
} else {
|
|
3722
|
+
proxyEl.innerHTML = 'Optional: Install <a href="https://github.com/chopratejas/headroom" target="_blank" style="color:#58a6ff;">Headroom</a> for ~34% token savings';
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3484
3725
|
loadModelPrefs();
|
|
3485
3726
|
await loadUserProfile();
|
|
3727
|
+
loadGlobalMcpDefaults();
|
|
3486
3728
|
settingsLoaded = true;
|
|
3487
3729
|
} catch (e) {
|
|
3488
3730
|
console.error('Settings load failed:', e);
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/dashboard.py
RENAMED
|
@@ -458,6 +458,27 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
458
458
|
pass
|
|
459
459
|
body = json.dumps({"path": path}).encode()
|
|
460
460
|
self._respond(200, "application/json", body)
|
|
461
|
+
elif self.path.startswith("/api/mcp-servers"):
|
|
462
|
+
parsed = urlparse(self.path)
|
|
463
|
+
qs = parse_qs(parsed.query)
|
|
464
|
+
workdir = qs.get("workdir", [""])[0]
|
|
465
|
+
servers = agent_runner.discover_mcp_servers(workdir) if workdir else []
|
|
466
|
+
cfg = agent_runner.load_builder_tools_config()
|
|
467
|
+
blocked: list[str] = []
|
|
468
|
+
has_project_config = False
|
|
469
|
+
if workdir:
|
|
470
|
+
if workdir in cfg:
|
|
471
|
+
blocked = cfg[workdir]
|
|
472
|
+
has_project_config = True
|
|
473
|
+
else:
|
|
474
|
+
blocked = cfg.get("_global_defaults", [])
|
|
475
|
+
global_blocked = cfg.get("_global_defaults", [])
|
|
476
|
+
body = json.dumps({"servers": servers, "blocked": blocked, "global_blocked": global_blocked, "has_project_config": has_project_config}).encode()
|
|
477
|
+
self._respond(200, "application/json", body)
|
|
478
|
+
elif self.path == "/api/global-mcp-defaults":
|
|
479
|
+
cfg = agent_runner.load_builder_tools_config()
|
|
480
|
+
body = json.dumps({"blocked": cfg.get("_global_defaults", [])}).encode()
|
|
481
|
+
self._respond(200, "application/json", body)
|
|
461
482
|
elif self.path == "/api/headroom-stats":
|
|
462
483
|
try:
|
|
463
484
|
req = urllib.request.Request("http://127.0.0.1:8787/stats")
|
|
@@ -511,6 +532,8 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
511
532
|
"pro_skills_dir": str(home / ".memstack" / "pro-skills"),
|
|
512
533
|
"stats_db": str(DB_PATH),
|
|
513
534
|
"sessions_dir": str(home / ".memstack" / "agent-runner" / "sessions"),
|
|
535
|
+
"api_key_set": bool(os.environ.get("ANTHROPIC_API_KEY")),
|
|
536
|
+
"base_url": os.environ.get("ANTHROPIC_BASE_URL", ""),
|
|
514
537
|
}
|
|
515
538
|
body = json.dumps(data).encode()
|
|
516
539
|
self._respond(200, "application/json", body)
|
|
@@ -723,8 +746,10 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
723
746
|
builder_model = data.get("builder_model", "")
|
|
724
747
|
reviewer_model = data.get("reviewer_model", "")
|
|
725
748
|
user_name = str(data.get("user_name", "")).strip()
|
|
749
|
+
blocked_mcp = data.get("blocked_mcp_servers", [])
|
|
726
750
|
result = agent_runner.start_run(task=task, working_dir=working_dir, context=context, auto_commit=auto_commit, timeout_minutes=timeout_minutes,
|
|
727
|
-
manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name
|
|
751
|
+
manager_model=manager_model, builder_model=builder_model, reviewer_model=reviewer_model, user_name=user_name,
|
|
752
|
+
blocked_mcp_servers=blocked_mcp)
|
|
728
753
|
body = json.dumps(result).encode()
|
|
729
754
|
status_code = 200 if "session_id" in result else 400
|
|
730
755
|
self._respond(status_code, "application/json", body)
|
|
@@ -766,6 +791,49 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
766
791
|
except Exception as exc:
|
|
767
792
|
body = json.dumps({"success": False, "error": str(exc)}).encode()
|
|
768
793
|
self._respond(500, "application/json", body)
|
|
794
|
+
elif self.path == "/api/builder-tools-config":
|
|
795
|
+
try:
|
|
796
|
+
content_len = int(self.headers.get("Content-Length", 0))
|
|
797
|
+
raw = self.rfile.read(content_len).decode("utf-8") if content_len else ""
|
|
798
|
+
data = json.loads(raw)
|
|
799
|
+
workdir = data.get("workdir", "").strip()
|
|
800
|
+
blocked = data.get("blocked", [])
|
|
801
|
+
if not workdir:
|
|
802
|
+
body = json.dumps({"success": False, "error": "Missing 'workdir' field."}).encode()
|
|
803
|
+
self._respond(400, "application/json", body)
|
|
804
|
+
return
|
|
805
|
+
cfg = agent_runner.load_builder_tools_config()
|
|
806
|
+
cfg[workdir] = blocked
|
|
807
|
+
agent_runner.save_builder_tools_config(cfg)
|
|
808
|
+
body = json.dumps({"success": True}).encode()
|
|
809
|
+
self._respond(200, "application/json", body)
|
|
810
|
+
except (json.JSONDecodeError, ValueError):
|
|
811
|
+
body = json.dumps({"success": False, "error": "Invalid JSON body."}).encode()
|
|
812
|
+
self._respond(400, "application/json", body)
|
|
813
|
+
except Exception as exc:
|
|
814
|
+
body = json.dumps({"success": False, "error": str(exc)}).encode()
|
|
815
|
+
self._respond(500, "application/json", body)
|
|
816
|
+
elif self.path == "/api/global-mcp-defaults":
|
|
817
|
+
try:
|
|
818
|
+
content_len = int(self.headers.get("Content-Length", 0))
|
|
819
|
+
raw = self.rfile.read(content_len).decode("utf-8") if content_len else ""
|
|
820
|
+
data = json.loads(raw)
|
|
821
|
+
blocked = data.get("blocked", [])
|
|
822
|
+
cfg = agent_runner.load_builder_tools_config()
|
|
823
|
+
cfg["_global_defaults"] = blocked
|
|
824
|
+
if data.get("apply_all"):
|
|
825
|
+
for key in list(cfg.keys()):
|
|
826
|
+
if key != "_global_defaults":
|
|
827
|
+
cfg[key] = list(blocked)
|
|
828
|
+
agent_runner.save_builder_tools_config(cfg)
|
|
829
|
+
body = json.dumps({"success": True}).encode()
|
|
830
|
+
self._respond(200, "application/json", body)
|
|
831
|
+
except (json.JSONDecodeError, ValueError):
|
|
832
|
+
body = json.dumps({"success": False, "error": "Invalid JSON body."}).encode()
|
|
833
|
+
self._respond(400, "application/json", body)
|
|
834
|
+
except Exception as exc:
|
|
835
|
+
body = json.dumps({"success": False, "error": str(exc)}).encode()
|
|
836
|
+
self._respond(500, "application/json", body)
|
|
769
837
|
elif self.path == "/api/burn-report/reset":
|
|
770
838
|
try:
|
|
771
839
|
from .stats import reset_burn_stats
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/__main__.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/categories.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/compression.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/config.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/indexer.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/license.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/memory_db.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/search.py
RENAMED
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/server.py
RENAMED
|
File without changes
|
|
File without changes
|
{memstack_skill_loader-4.0.3 → memstack_skill_loader-4.0.5}/src/memstack_skill_loader/stats.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|