memstack-skill-loader 4.0.7__tar.gz → 4.1.0__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.
Files changed (28) hide show
  1. {memstack_skill_loader-4.0.7/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.1.0}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/pyproject.toml +1 -1
  3. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/__init__.py +1 -1
  4. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/agent_runner.py +155 -34
  5. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/dashboard.html +410 -15
  6. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/dashboard.py +479 -86
  7. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  8. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/MANIFEST.in +0 -0
  9. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/README.md +0 -0
  10. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/setup.cfg +0 -0
  11. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/__main__.py +0 -0
  12. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/categories.py +0 -0
  13. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/compression.py +0 -0
  14. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/config.py +0 -0
  15. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/indexer.py +0 -0
  16. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/license.py +0 -0
  17. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/memory_db.py +0 -0
  18. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/search.py +0 -0
  19. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/server.py +0 -0
  20. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/skill_config.py +0 -0
  21. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/stats.py +0 -0
  22. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/tfidf_search.py +0 -0
  23. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader/version_check.py +0 -0
  24. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
  25. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  26. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  27. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  28. {memstack_skill_loader-4.0.7 → memstack_skill_loader-4.1.0}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.7
3
+ Version: 4.1.0
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.0.7"
7
+ version = "4.1.0"
8
8
  description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  """MemStack Skill Loader — MCP server for semantic skill search."""
2
2
 
3
- __version__ = "4.0.7"
3
+ __version__ = "4.1.0"
@@ -28,9 +28,15 @@ AGENT_TIMEOUT = 3600 # seconds per --print invocation (default 60 minutes)
28
28
  MAX_ITERATIONS = 2
29
29
 
30
30
  ANTHROPIC_BASE_URL = os.environ.get("ANTHROPIC_BASE_URL", "")
31
+ API_KEY_FILE = Path.home() / ".memstack" / "api_key"
31
32
  API_DEFAULT_MODEL = "claude-sonnet-4-20250514"
32
33
  API_MAX_TOKENS = 16000
33
34
 
35
+ if not os.environ.get("ANTHROPIC_API_KEY") and API_KEY_FILE.is_file():
36
+ _stored_key = API_KEY_FILE.read_text(encoding="utf-8").strip()
37
+ if _stored_key:
38
+ os.environ["ANTHROPIC_API_KEY"] = _stored_key
39
+
34
40
  SYSTEM_PROMPTS = {
35
41
  "manager": (
36
42
  "You are a Manager agent. You receive a task and project context (directory listing "
@@ -109,30 +115,105 @@ def discover_mcp_servers(workdir: str) -> list[str]:
109
115
  return []
110
116
 
111
117
 
118
+ _VALID_GIT_WORKFLOWS = ("auto", "single_branch", "dev_to_default", "feature_branch")
119
+
120
+
121
+ def _migrate_config(cfg: dict) -> dict:
122
+ """Migrate per-project values from plain arrays to objects with blocked_mcp key."""
123
+ changed = False
124
+ for key, val in list(cfg.items()):
125
+ if key.startswith("_"):
126
+ continue
127
+ if isinstance(val, list):
128
+ cfg[key] = {"blocked_mcp": val}
129
+ changed = True
130
+ if changed:
131
+ save_builder_tools_config(cfg)
132
+ return cfg
133
+
134
+
112
135
  def load_builder_tools_config() -> dict:
113
- """Load per-project blocked MCP server lists from ~/.memstack/builder-tools-config.json."""
136
+ """Load per-project config from ~/.memstack/builder-tools-config.json, migrating if needed."""
114
137
  try:
115
138
  if BUILDER_TOOLS_CONFIG_FILE.is_file():
116
- return json.loads(BUILDER_TOOLS_CONFIG_FILE.read_text(encoding="utf-8"))
139
+ cfg = json.loads(BUILDER_TOOLS_CONFIG_FILE.read_text(encoding="utf-8"))
140
+ return _migrate_config(cfg)
117
141
  except Exception:
118
142
  pass
119
143
  return {}
120
144
 
121
145
 
122
146
  def save_builder_tools_config(config: dict) -> None:
123
- """Save per-project blocked MCP server config."""
147
+ """Save per-project config."""
124
148
  BUILDER_TOOLS_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
125
149
  BUILDER_TOOLS_CONFIG_FILE.write_text(json.dumps(config, indent=2), encoding="utf-8")
126
150
 
127
151
 
152
+ def _get_project_config(cfg: dict, workdir: str) -> dict:
153
+ """Return the project config dict for workdir, or empty dict."""
154
+ val = cfg.get(workdir)
155
+ if isinstance(val, dict):
156
+ return val
157
+ return {}
158
+
159
+
128
160
  def get_blocked_servers_for_project(workdir: str) -> list[str]:
129
161
  """Return blocked MCP servers for a project, falling back to global defaults."""
130
162
  cfg = load_builder_tools_config()
131
- if workdir in cfg:
132
- return cfg[workdir]
163
+ proj = _get_project_config(cfg, workdir)
164
+ if proj:
165
+ return proj.get("blocked_mcp", [])
133
166
  return cfg.get("_global_defaults", [])
134
167
 
135
168
 
169
+ def get_git_workflow_for_project(workdir: str) -> tuple[str, bool]:
170
+ """Return (workflow, is_project_config) for a working directory."""
171
+ cfg = load_builder_tools_config()
172
+ proj = _get_project_config(cfg, workdir)
173
+ if proj and "git_workflow" in proj:
174
+ wf = proj["git_workflow"]
175
+ if wf in _VALID_GIT_WORKFLOWS:
176
+ return wf, True
177
+ global_wf = cfg.get("_git_workflow", "auto")
178
+ if global_wf not in _VALID_GIT_WORKFLOWS:
179
+ global_wf = "auto"
180
+ return global_wf, False
181
+
182
+
183
+ _VALID_AGENT_MODES = ("api", "subscription")
184
+
185
+
186
+ def _default_agent_modes() -> dict:
187
+ """Return default execution modes based on whether an API key is available."""
188
+ has_key = bool(os.environ.get("ANTHROPIC_API_KEY") or ANTHROPIC_BASE_URL)
189
+ if has_key:
190
+ return {"manager": "api", "builder": "subscription", "reviewer": "api"}
191
+ return {"manager": "subscription", "builder": "subscription", "reviewer": "subscription"}
192
+
193
+
194
+ def load_agent_modes() -> dict:
195
+ """Load agent execution modes from config, falling back to defaults."""
196
+ cfg = load_builder_tools_config()
197
+ stored = cfg.get("_agent_modes", {})
198
+ defaults = _default_agent_modes()
199
+ modes = {}
200
+ for agent in ("manager", "builder", "reviewer"):
201
+ val = stored.get(agent, "")
202
+ modes[agent] = val if val in _VALID_AGENT_MODES else defaults[agent]
203
+ return modes
204
+
205
+
206
+ def save_agent_modes(modes: dict) -> None:
207
+ """Save agent execution modes to config."""
208
+ cfg = load_builder_tools_config()
209
+ clean = {}
210
+ for agent in ("manager", "builder", "reviewer"):
211
+ val = modes.get(agent, "")
212
+ clean[agent] = val if val in _VALID_AGENT_MODES else "subscription"
213
+ cfg["_agent_modes"] = clean
214
+ save_builder_tools_config(cfg)
215
+
216
+
136
217
  # ---------------------------------------------------------------------------
137
218
  # Project context gathering
138
219
  # ---------------------------------------------------------------------------
@@ -727,6 +808,14 @@ def _orchestrate(session: Session) -> None:
727
808
  session_log_dir = STATE_DIR / "sessions" / session.session_id
728
809
 
729
810
  try:
811
+ # Load execution modes for this run
812
+ agent_modes = load_agent_modes()
813
+ has_api_key = bool(os.environ.get("ANTHROPIC_API_KEY") or ANTHROPIC_BASE_URL)
814
+ for _ag in ("manager", "builder", "reviewer"):
815
+ if agent_modes[_ag] == "api" and not has_api_key:
816
+ agent_modes[_ag] = "subscription"
817
+ print(f"[agent-runner] {_ag}: mode=api but no API key set, falling back to subscription", file=sys.stderr)
818
+
730
819
  # Step 1: Manager analyzes the task
731
820
  session.agents["manager"]["status"] = "busy"
732
821
  session.agents["manager"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
@@ -741,15 +830,25 @@ def _orchestrate(session: Session) -> None:
741
830
  if session.user_name:
742
831
  manager_prompt = f"The user's name is {session.user_name}.\n\n" + manager_prompt
743
832
  try:
744
- manager_output, m_in, m_out = _invoke_api_agent(
745
- "manager", manager_prompt,
746
- system_prompt=SYSTEM_PROMPTS["manager"],
747
- log_path=session_log_dir / "manager.log",
748
- timeout=min(600, session.timeout),
749
- model=session.models.get("manager", ""),
750
- session_id=session.session_id,
751
- working_dir=session.working_dir,
752
- )
833
+ if agent_modes["manager"] == "api":
834
+ manager_output, m_in, m_out = _invoke_api_agent(
835
+ "manager", manager_prompt,
836
+ system_prompt=SYSTEM_PROMPTS["manager"],
837
+ log_path=session_log_dir / "manager.log",
838
+ timeout=min(600, session.timeout),
839
+ model=session.models.get("manager", ""),
840
+ session_id=session.session_id,
841
+ working_dir=session.working_dir,
842
+ )
843
+ m_ctx = m_in
844
+ else:
845
+ manager_output, m_in, m_out, m_ctx = _invoke_agent(
846
+ "manager", manager_prompt, session.working_dir,
847
+ log_path=session_log_dir / "manager.log",
848
+ skip_permissions=True, session_id=session.session_id,
849
+ timeout=min(600, session.timeout),
850
+ model=session.models.get("manager", ""),
851
+ )
753
852
  except subprocess.TimeoutExpired:
754
853
  session.agents["manager"]["status"] = "timeout"
755
854
  session.status = "error"
@@ -764,7 +863,7 @@ def _orchestrate(session: Session) -> None:
764
863
  return
765
864
  session.agents["manager"]["input_tokens"] += m_in
766
865
  session.agents["manager"]["output_tokens"] += m_out
767
- session.agents["manager"]["context_tokens"] = m_in
866
+ session.agents["manager"]["context_tokens"] = m_ctx
768
867
  session.agents["manager"]["last_output"] = (manager_output or "")[:500]
769
868
 
770
869
  session.agents["manager"]["status"] = "done"
@@ -804,15 +903,27 @@ def _orchestrate(session: Session) -> None:
804
903
  if session.user_name:
805
904
  builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
806
905
  try:
807
- builder_output, b_in, b_out, b_ctx = _invoke_agent(
808
- "builder", builder_prompt, session.working_dir,
809
- log_path=session_log_dir / "builder.log",
810
- skip_permissions=True, session_id=session.session_id,
811
- timeout=session.timeout,
812
- model=session.models.get("builder", ""),
813
- disallowed_tools=session.blocked_mcp_servers,
814
- session=session,
815
- )
906
+ if agent_modes["builder"] == "subscription":
907
+ builder_output, b_in, b_out, b_ctx = _invoke_agent(
908
+ "builder", builder_prompt, session.working_dir,
909
+ log_path=session_log_dir / "builder.log",
910
+ skip_permissions=True, session_id=session.session_id,
911
+ timeout=session.timeout,
912
+ model=session.models.get("builder", ""),
913
+ disallowed_tools=session.blocked_mcp_servers,
914
+ session=session,
915
+ )
916
+ else:
917
+ builder_output, b_in, b_out = _invoke_api_agent(
918
+ "builder", builder_prompt,
919
+ system_prompt=SYSTEM_PROMPTS["builder"].format(working_dir=session.working_dir),
920
+ log_path=session_log_dir / "builder.log",
921
+ timeout=session.timeout,
922
+ model=session.models.get("builder", ""),
923
+ session_id=session.session_id,
924
+ working_dir=session.working_dir,
925
+ )
926
+ b_ctx = b_in
816
927
  except subprocess.TimeoutExpired:
817
928
  session.agents["builder"]["status"] = "timeout"
818
929
  session.status = "error"
@@ -870,15 +981,25 @@ def _orchestrate(session: Session) -> None:
870
981
  + f"\n\nBuilder output (iteration {iteration}):\n{builder_output}"
871
982
  )
872
983
  try:
873
- reviewer_output, r_in, r_out = _invoke_api_agent(
874
- "reviewer", reviewer_prompt,
875
- system_prompt=SYSTEM_PROMPTS["reviewer"],
876
- log_path=session_log_dir / "reviewer.log",
877
- timeout=session.timeout,
878
- model=session.models.get("reviewer", ""),
879
- session_id=session.session_id,
880
- working_dir=session.working_dir,
881
- )
984
+ if agent_modes["reviewer"] == "api":
985
+ reviewer_output, r_in, r_out = _invoke_api_agent(
986
+ "reviewer", reviewer_prompt,
987
+ system_prompt=SYSTEM_PROMPTS["reviewer"],
988
+ log_path=session_log_dir / "reviewer.log",
989
+ timeout=session.timeout,
990
+ model=session.models.get("reviewer", ""),
991
+ session_id=session.session_id,
992
+ working_dir=session.working_dir,
993
+ )
994
+ r_ctx = r_in
995
+ else:
996
+ reviewer_output, r_in, r_out, r_ctx = _invoke_agent(
997
+ "reviewer", reviewer_prompt, session.working_dir,
998
+ log_path=session_log_dir / "reviewer.log",
999
+ skip_permissions=True, session_id=session.session_id,
1000
+ timeout=session.timeout,
1001
+ model=session.models.get("reviewer", ""),
1002
+ )
882
1003
  except subprocess.TimeoutExpired:
883
1004
  session.agents["reviewer"]["status"] = "timeout"
884
1005
  session.status = "error"
@@ -893,7 +1014,7 @@ def _orchestrate(session: Session) -> None:
893
1014
  return
894
1015
  session.agents["reviewer"]["input_tokens"] += r_in
895
1016
  session.agents["reviewer"]["output_tokens"] += r_out
896
- session.agents["reviewer"]["context_tokens"] = r_in
1017
+ session.agents["reviewer"]["context_tokens"] = r_ctx
897
1018
  session.agents["reviewer"]["last_output"] = (reviewer_output or "")[:500]
898
1019
 
899
1020
  session.agents["reviewer"]["status"] = "done"