memstack-skill-loader 4.0.0__tar.gz → 4.0.2__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.0/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.0.2}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/pyproject.toml +1 -1
  3. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/__init__.py +1 -1
  4. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/agent_runner.py +104 -29
  5. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/config.py +2 -2
  6. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/dashboard.html +42 -6
  7. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  8. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/MANIFEST.in +0 -0
  9. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/README.md +0 -0
  10. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/setup.cfg +0 -0
  11. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/__main__.py +0 -0
  12. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/categories.py +0 -0
  13. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/compression.py +0 -0
  14. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/dashboard.py +0 -0
  15. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/indexer.py +0 -0
  16. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/license.py +0 -0
  17. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/memory_db.py +0 -0
  18. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/search.py +0 -0
  19. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/server.py +0 -0
  20. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/skill_config.py +0 -0
  21. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/stats.py +0 -0
  22. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/tfidf_search.py +0 -0
  23. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader/version_check.py +0 -0
  24. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
  25. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  26. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  27. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  28. {memstack_skill_loader-4.0.0 → memstack_skill_loader-4.0.2}/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.0
3
+ Version: 4.0.2
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.0"
7
+ version = "4.0.2"
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.0"
3
+ __version__ = "4.0.2"
@@ -295,12 +295,45 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
295
295
  )
296
296
  with _lock:
297
297
  _current_process = proc
298
+
299
+ stdout_chunks: list[str] = []
300
+ stderr_chunks: list[str] = []
301
+
302
+ def _reader(stream: object, dest: list[str]) -> None:
303
+ try:
304
+ for line in stream: # type: ignore[union-attr]
305
+ dest.append(line)
306
+ except Exception:
307
+ pass
308
+
309
+ t_out = threading.Thread(target=_reader, args=(proc.stdout, stdout_chunks), daemon=True)
310
+ t_err = threading.Thread(target=_reader, args=(proc.stderr, stderr_chunks), daemon=True)
311
+
298
312
  try:
299
- stdout_data, stderr_data = proc.communicate(input=prompt, timeout=timeout)
300
- except subprocess.TimeoutExpired:
301
- proc.kill()
302
- proc.communicate()
303
- raise
313
+ if proc.stdin:
314
+ try:
315
+ proc.stdin.write(prompt)
316
+ proc.stdin.close()
317
+ except OSError:
318
+ pass
319
+ t_out.start()
320
+ t_err.start()
321
+
322
+ deadline = time.monotonic() + timeout
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)
304
337
  finally:
305
338
  with _lock:
306
339
  if _current_process is proc:
@@ -367,9 +400,9 @@ class Session:
367
400
  self.started_at = time.strftime("%Y-%m-%d %H:%M:%S")
368
401
  self.messages: list[dict] = []
369
402
  self.agents: dict[str, dict] = {
370
- "manager": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": ""},
371
- "builder": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": ""},
372
- "reviewer": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": ""},
403
+ "manager": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": "", "started_at": None},
404
+ "builder": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": "", "started_at": None},
405
+ "reviewer": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": "", "started_at": None},
373
406
  }
374
407
 
375
408
  def add_message(self, from_agent: str, to_agent: str, content: str) -> None:
@@ -471,6 +504,7 @@ def _orchestrate(session: Session) -> None:
471
504
  try:
472
505
  # Step 1: Manager analyzes the task
473
506
  session.agents["manager"]["status"] = "busy"
507
+ session.agents["manager"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
474
508
  session._save_state()
475
509
 
476
510
  context = _gather_project_context(session.working_dir)
@@ -480,13 +514,26 @@ def _orchestrate(session: Session) -> None:
480
514
  manager_prompt += "\n\nAdditional context from user:\n" + session.context
481
515
  if session.user_name:
482
516
  manager_prompt = f"The user's name is {session.user_name}.\n\n" + manager_prompt
483
- manager_output, m_in, m_out, m_ctx = _invoke_agent(
484
- "manager", manager_prompt, session.working_dir,
485
- log_path=session_log_dir / "manager.log",
486
- skip_permissions=True, session_id=session.session_id,
487
- timeout=session.timeout,
488
- model=session.models.get("manager", ""),
489
- )
517
+ try:
518
+ manager_output, m_in, m_out, m_ctx = _invoke_agent(
519
+ "manager", manager_prompt, session.working_dir,
520
+ log_path=session_log_dir / "manager.log",
521
+ skip_permissions=True, session_id=session.session_id,
522
+ timeout=min(180, session.timeout),
523
+ model=session.models.get("manager", ""),
524
+ )
525
+ except subprocess.TimeoutExpired:
526
+ session.agents["manager"]["status"] = "timeout"
527
+ session.status = "error"
528
+ session.result = "Manager timed out after 3 minutes. Try a simpler task description or break the task into smaller pieces."
529
+ session._save_state()
530
+ return
531
+ except RuntimeError:
532
+ session.agents["manager"]["status"] = "crashed"
533
+ session.status = "error"
534
+ session.result = "Manager agent stopped unexpectedly."
535
+ session._save_state()
536
+ return
490
537
  session.agents["manager"]["input_tokens"] += m_in
491
538
  session.agents["manager"]["output_tokens"] += m_out
492
539
  session.agents["manager"]["context_tokens"] = m_ctx
@@ -515,6 +562,7 @@ def _orchestrate(session: Session) -> None:
515
562
 
516
563
  # Builder
517
564
  session.agents["builder"]["status"] = "busy"
565
+ session.agents["builder"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
518
566
  session._save_state()
519
567
 
520
568
  builder_prompt = (
@@ -526,13 +574,26 @@ def _orchestrate(session: Session) -> None:
526
574
  )
527
575
  if session.user_name:
528
576
  builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
529
- builder_output, b_in, b_out, b_ctx = _invoke_agent(
530
- "builder", builder_prompt, session.working_dir,
531
- log_path=session_log_dir / "builder.log",
532
- skip_permissions=True, bare=True, session_id=session.session_id,
533
- timeout=session.timeout,
534
- model=session.models.get("builder", ""),
535
- )
577
+ try:
578
+ builder_output, b_in, b_out, b_ctx = _invoke_agent(
579
+ "builder", builder_prompt, session.working_dir,
580
+ log_path=session_log_dir / "builder.log",
581
+ skip_permissions=True, bare=True, session_id=session.session_id,
582
+ timeout=session.timeout,
583
+ model=session.models.get("builder", ""),
584
+ )
585
+ except subprocess.TimeoutExpired:
586
+ session.agents["builder"]["status"] = "timeout"
587
+ session.status = "error"
588
+ session.result = "Builder timed out. The task may be too large for a single agent pass."
589
+ session._save_state()
590
+ return
591
+ except RuntimeError:
592
+ session.agents["builder"]["status"] = "crashed"
593
+ session.status = "error"
594
+ session.result = "Builder agent stopped unexpectedly."
595
+ session._save_state()
596
+ return
536
597
  session.agents["builder"]["input_tokens"] += b_in
537
598
  session.agents["builder"]["output_tokens"] += b_out
538
599
  session.agents["builder"]["context_tokens"] = b_ctx
@@ -568,6 +629,7 @@ def _orchestrate(session: Session) -> None:
568
629
 
569
630
  # Reviewer
570
631
  session.agents["reviewer"]["status"] = "busy"
632
+ session.agents["reviewer"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
571
633
  session._save_state()
572
634
 
573
635
  reviewer_prompt = (
@@ -575,13 +637,26 @@ def _orchestrate(session: Session) -> None:
575
637
  + f"\n\nOriginal task from user:\n{session.task}"
576
638
  + f"\n\nBuilder output (iteration {iteration}):\n{builder_output}"
577
639
  )
578
- reviewer_output, r_in, r_out, r_ctx = _invoke_agent(
579
- "reviewer", reviewer_prompt, session.working_dir,
580
- log_path=session_log_dir / "reviewer.log",
581
- bare=True, session_id=session.session_id,
582
- timeout=session.timeout,
583
- model=session.models.get("reviewer", ""),
584
- )
640
+ try:
641
+ reviewer_output, r_in, r_out, r_ctx = _invoke_agent(
642
+ "reviewer", reviewer_prompt, session.working_dir,
643
+ log_path=session_log_dir / "reviewer.log",
644
+ bare=True, session_id=session.session_id,
645
+ timeout=session.timeout,
646
+ model=session.models.get("reviewer", ""),
647
+ )
648
+ except subprocess.TimeoutExpired:
649
+ session.agents["reviewer"]["status"] = "timeout"
650
+ session.status = "error"
651
+ session.result = "Reviewer timed out."
652
+ session._save_state()
653
+ return
654
+ except RuntimeError:
655
+ session.agents["reviewer"]["status"] = "crashed"
656
+ session.status = "error"
657
+ session.result = "Reviewer agent stopped unexpectedly."
658
+ session._save_state()
659
+ return
585
660
  session.agents["reviewer"]["input_tokens"] += r_in
586
661
  session.agents["reviewer"]["output_tokens"] += r_out
587
662
  session.agents["reviewer"]["context_tokens"] = r_ctx
@@ -158,8 +158,8 @@ def load_config(config_path: Path | None = None) -> Config:
158
158
  _config_dir=config_path.parent.resolve(),
159
159
  )
160
160
 
161
- # Auto-detect pro-skills if license key is set and directory exists
162
- if os.environ.get("MEMSTACK_PRO_LICENSE_KEY"):
161
+ # Auto-detect pro-skills if license key is set (env var or license.json)
162
+ if os.environ.get("MEMSTACK_PRO_LICENSE_KEY") or (Path.home() / ".memstack" / "license.json").exists():
163
163
  config = config.with_pro_skills()
164
164
 
165
165
  return config
@@ -2730,7 +2730,8 @@ document.addEventListener('visibilitychange', () => {
2730
2730
  const STATUS_COLORS = {
2731
2731
  idle: '#8b949e', busy: '#58a6ff', done: '#3fb950',
2732
2732
  running: '#3fb950', stopped: '#6e7681', error: '#f85149',
2733
- starting: '#d2a8ff', completed: '#3fb950'
2733
+ starting: '#d2a8ff', completed: '#3fb950',
2734
+ timeout: '#f0883e', crashed: '#f85149'
2734
2735
  };
2735
2736
 
2736
2737
  const ROLE_NAME_COLORS = {manager: '#d2a8ff', builder: '#58a6ff', reviewer: '#7ee787'};
@@ -2742,6 +2743,7 @@ const BUSY_PHRASES = {
2742
2743
  };
2743
2744
  let busyPhraseIntervals = {};
2744
2745
  let busyPhraseIndices = {manager: 0, builder: 0, reviewer: 0};
2746
+ let elapsedIntervals = {};
2745
2747
 
2746
2748
  const MODEL_DEFAULTS = {manager: 'claude-opus-4-6', builder: 'claude-sonnet-4-6', reviewer: 'claude-sonnet-4-6'};
2747
2749
 
@@ -3111,10 +3113,17 @@ function renderAgentUI(data) {
3111
3113
  launcher.style.display = 'none';
3112
3114
  active.style.display = 'none';
3113
3115
  completed.style.display = '';
3114
- const icon = status === 'completed' ? '✅' : status === 'stopped' ? '⏹' : '⚠️';
3116
+ const hasTimeout = Object.values(agents).some(ag => ag.status === 'timeout');
3117
+ const hasCrash = Object.values(agents).some(ag => ag.status === 'crashed');
3118
+ let icon, title;
3119
+ if (status === 'completed') { icon = '✅'; title = 'Task Completed'; }
3120
+ else if (status === 'stopped') { icon = '⏹'; title = 'Task Stopped'; }
3121
+ else if (hasTimeout) { icon = '⏳'; title = 'Task Timed Out'; }
3122
+ else if (hasCrash) { icon = '⚠️'; title = 'Task Failed'; }
3123
+ else { icon = '⚠️'; title = 'Task Failed'; }
3115
3124
  document.querySelector('#agent-completed div div:first-child').innerHTML = icon;
3116
3125
  const h2 = document.querySelector('#agent-completed h2');
3117
- if (h2) h2.textContent = status === 'completed' ? 'Task Completed' : status === 'stopped' ? 'Task Stopped' : 'Task Failed';
3126
+ if (h2) h2.textContent = title;
3118
3127
  document.getElementById('agent-result-summary').textContent = status === 'stopped' ? (data.result || 'Task stopped by user') : (data.result || `Session ${status}`);
3119
3128
  const COMPACTION_LIMIT = 200000;
3120
3129
  const COMPACTION_WARN = 155000;
@@ -3218,8 +3227,12 @@ function renderAgentUI(data) {
3218
3227
  const phrase = BUSY_PHRASES[role][busyPhraseIndices[role] % BUSY_PHRASES[role].length];
3219
3228
  const customName = (userProfile.agent_names && userProfile.agent_names[role]) || role.charAt(0).toUpperCase() + role.slice(1);
3220
3229
  statusDisplay = `<span class="agent-status-phrase" id="agent-phrase-${role}"><span class="agent-name-label" style="color:${ROLE_NAME_COLORS[role] || '#e6edf3'}">${escapeHtml(customName)}:</span> ${escapeHtml(phrase)}</span>`;
3221
- } else if ((a.status === 'done' || a.status === 'completed') && data.started_at) {
3222
- const startMs = new Date(data.started_at).getTime();
3230
+ } else if (a.status === 'timeout') {
3231
+ statusDisplay = 'Timed out';
3232
+ } else if (a.status === 'crashed') {
3233
+ statusDisplay = 'Crashed';
3234
+ } else if ((a.status === 'done' || a.status === 'completed') && (a.started_at || data.started_at)) {
3235
+ const startMs = new Date(a.started_at || data.started_at).getTime();
3223
3236
  const elapsed = Math.max(0, Math.round((Date.now() - startMs) / 1000));
3224
3237
  const mins = Math.floor(elapsed / 60);
3225
3238
  const secs = elapsed % 60;
@@ -3227,6 +3240,15 @@ function renderAgentUI(data) {
3227
3240
  } else {
3228
3241
  statusDisplay = a.status;
3229
3242
  }
3243
+ let elapsedHtml = '';
3244
+ if (a.status === 'busy' && a.started_at) {
3245
+ const secs = Math.max(0, Math.floor((Date.now() - new Date(a.started_at).getTime()) / 1000));
3246
+ elapsedHtml = `<div class="agent-elapsed" id="agent-elapsed-${role}" style="font-size:0.7rem;color:#8b949e;margin-top:0.3rem;">Running: ${Math.floor(secs/60)}m ${secs%60}s</div>`;
3247
+ } else if ((a.status === 'done' || a.status === 'completed' || a.status === 'timeout' || a.status === 'crashed') && a.started_at) {
3248
+ const secs = Math.max(0, Math.floor((Date.now() - new Date(a.started_at).getTime()) / 1000));
3249
+ const label = a.status === 'timeout' ? 'Timed out after' : a.status === 'crashed' ? 'Crashed after' : 'Completed in';
3250
+ elapsedHtml = `<div style="font-size:0.7rem;color:#8b949e;margin-top:0.3rem;">${label} ${Math.floor(secs/60)}m ${secs%60}s</div>`;
3251
+ }
3230
3252
  const COMPACTION_LIMIT = 200000;
3231
3253
  const tokens = (a.context_tokens || 0);
3232
3254
  const pct = tokens > 0 ? Math.min(100, Math.round(tokens / COMPACTION_LIMIT * 100)) : -1;
@@ -3245,7 +3267,8 @@ function renderAgentUI(data) {
3245
3267
  showToast(role + ' context at ' + pct + '% — diary auto-saved', 'warning', 5000);
3246
3268
  }
3247
3269
  }
3248
- return `<div class="agent-card${a.status === 'error' ? ' agent-card-error-border' : ''}" style="${pulseStyle}">
3270
+ const errorBorder = (a.status === 'error' || a.status === 'timeout' || a.status === 'crashed') ? ' agent-card-error-border' : '';
3271
+ return `<div class="agent-card${errorBorder}" style="${pulseStyle}">
3249
3272
  <span class="agent-tooltip">${escapeHtml(ROLE_DESCRIPTIONS[role] || '')}</span>
3250
3273
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.6rem;">
3251
3274
  <strong style="color:#e6edf3;font-size:0.95rem;">${roleIcon} ${escapeHtml((userProfile.agent_names && userProfile.agent_names[role]) || role.charAt(0).toUpperCase() + role.slice(1))}</strong>
@@ -3254,6 +3277,7 @@ function renderAgentUI(data) {
3254
3277
  <div style="font-size:0.75rem;color:#8b949e;line-height:1.6;">
3255
3278
  <div style="margin-top:0.3rem;color:#c9d1d9;font-size:0.72rem;max-height:3em;overflow:hidden;text-overflow:ellipsis;">${escapeHtml((a.last_output || '').substring(0, 150))}</div>
3256
3279
  </div>
3280
+ ${elapsedHtml}
3257
3281
  ${contextHtml}
3258
3282
  </div>`;
3259
3283
  }).join('');
@@ -3276,6 +3300,18 @@ function renderAgentUI(data) {
3276
3300
  busyPhraseIntervals[role] = null;
3277
3301
  busyPhraseIndices[role] = 0;
3278
3302
  }
3303
+ if (a.status === 'busy' && a.started_at && !elapsedIntervals[role]) {
3304
+ const startMs = new Date(a.started_at).getTime();
3305
+ elapsedIntervals[role] = setInterval(() => {
3306
+ const el = document.getElementById('agent-elapsed-' + role);
3307
+ if (!el) return;
3308
+ const secs = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
3309
+ el.textContent = 'Running: ' + Math.floor(secs / 60) + 'm ' + (secs % 60) + 's';
3310
+ }, 1000);
3311
+ } else if (a.status !== 'busy' && elapsedIntervals[role]) {
3312
+ clearInterval(elapsedIntervals[role]);
3313
+ elapsedIntervals[role] = null;
3314
+ }
3279
3315
  });
3280
3316
 
3281
3317
  renderMessageLog(document.getElementById('agent-messages'), messages);
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.0
3
+ Version: 4.0.2
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