@geminilight/mindos 0.6.21 → 0.6.22

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.
@@ -74,6 +74,7 @@ export async function GET() {
74
74
  scope: status.scope,
75
75
  transport: status.transport,
76
76
  configPath: status.configPath,
77
+ url: status.url, // Store URL for verification
77
78
  hasProjectScope: !!agent.project,
78
79
  hasGlobalScope: !!agent.global,
79
80
  preferredTransport: agent.preferredTransport,
@@ -102,6 +103,29 @@ export async function GET() {
102
103
  const mindos = agents.find(a => a.key === 'mindos');
103
104
  if (mindos) enrichMindOsAgent(mindos as unknown as Record<string, unknown>);
104
105
 
106
+ // Runtime verification: for agents marked as installed with HTTP endpoint,
107
+ // verify endpoint is reachable (1s timeout to avoid blocking)
108
+ await Promise.all(agents.map(async (agent) => {
109
+ if (agent.installed && agent.url && agent.transport?.startsWith('http')) {
110
+ try {
111
+ const controller = new AbortController();
112
+ const timeout = setTimeout(() => controller.abort(), 1000);
113
+ try {
114
+ const response = await fetch(agent.url, { method: 'HEAD', signal: controller.signal });
115
+ // Accept 200-299 or 405 (HEAD not allowed). Others = unreachable
116
+ if (response.status >= 300 && response.status !== 405) {
117
+ agent.installed = false;
118
+ }
119
+ } finally {
120
+ clearTimeout(timeout);
121
+ }
122
+ } catch {
123
+ // Timeout, network error, or abort — mark as not installed (false positive prevention)
124
+ agent.installed = false;
125
+ }
126
+ }
127
+ }));
128
+
105
129
  // Sort: mindos first, then installed, then detected, then not found
106
130
  agents.sort((a, b) => {
107
131
  if (a.key === 'mindos') return -1;
@@ -345,9 +345,10 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
345
345
  }, [renaming, router, node.path, onNavigate]);
346
346
 
347
347
  const handleDoubleClick = useCallback((e: React.MouseEvent) => {
348
- if (isSpace) return;
349
- startRename(e);
350
- }, [startRename, isSpace]);
348
+ e.stopPropagation();
349
+ // Double-click to toggle expand/collapse
350
+ toggle();
351
+ }, [toggle]);
351
352
 
352
353
  const handleContextMenu = useCallback((e: React.MouseEvent) => {
353
354
  e.preventDefault();
@@ -306,6 +306,7 @@ export default function AgentsSkillsSection({
306
306
  onToggle={mcp.toggleSkill}
307
307
  onDelete={handleDeleteFromPopover}
308
308
  onRefresh={mcp.refresh}
309
+ allAgentNames={mcp.agents.filter(a => a.present && a.installed).map(a => a.name)}
309
310
  />
310
311
  </section>
311
312
  );
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useCallback, useEffect, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import {
5
5
  BookOpen,
6
6
  Code2,
@@ -8,6 +8,7 @@ import {
8
8
  Check,
9
9
  FileText,
10
10
  Loader2,
11
+ Plus,
11
12
  Zap,
12
13
  Search,
13
14
  Server,
@@ -58,6 +59,7 @@ interface SkillDetailPopoverProps {
58
59
  skillName: string | null;
59
60
  skill?: SkillInfo | null;
60
61
  agentNames?: string[];
62
+ allAgentNames?: string[];
61
63
  isNative?: boolean;
62
64
  nativeSourcePath?: string;
63
65
  copy: SkillDetailPopoverCopy;
@@ -65,6 +67,8 @@ interface SkillDetailPopoverProps {
65
67
  onToggle?: (name: string, enabled: boolean) => Promise<boolean>;
66
68
  onDelete?: (name: string) => Promise<void>;
67
69
  onRefresh?: () => Promise<void>;
70
+ onAddAgent?: (skillName: string, agentName: string) => void;
71
+ onRemoveAgent?: (skillName: string, agentName: string) => void;
68
72
  }
69
73
 
70
74
  /* ────────── Capability Icon ────────── */
@@ -77,6 +81,90 @@ const CAPABILITY_ICONS: Record<SkillCapability, React.ComponentType<{ size?: num
77
81
  memory: BookOpen,
78
82
  };
79
83
 
84
+ /* ────────── Content Parser ────────── */
85
+
86
+ interface ParsedContent {
87
+ triggerConditions: string;
88
+ instructions: string;
89
+ }
90
+
91
+ function parseSkillContent(raw: string, description: string): ParsedContent {
92
+ // Strip YAML frontmatter
93
+ let body = raw;
94
+ const fmMatch = raw.match(/^---\n[\s\S]*?\n---\n?/);
95
+ if (fmMatch) body = raw.slice(fmMatch[0].length).trim();
96
+
97
+ // Extract trigger conditions from description
98
+ // The description field usually contains trigger/usage info
99
+ const triggerConditions = description || '';
100
+
101
+ return { triggerConditions, instructions: body };
102
+ }
103
+
104
+ /* ────────── Simple Markdown Renderer ────────── */
105
+
106
+ function MarkdownContent({ text, className = '' }: { text: string; className?: string }) {
107
+ const html = useMemo(() => renderMarkdown(text), [text]);
108
+ return (
109
+ <div
110
+ className={`prose prose-sm prose-invert max-w-none
111
+ prose-headings:text-foreground prose-headings:font-semibold prose-headings:mt-4 prose-headings:mb-2
112
+ prose-h1:text-sm prose-h2:text-xs prose-h3:text-xs
113
+ prose-p:text-xs prose-p:text-foreground/80 prose-p:leading-relaxed prose-p:my-1.5
114
+ prose-li:text-xs prose-li:text-foreground/80 prose-li:my-0.5
115
+ prose-strong:text-foreground prose-strong:font-semibold
116
+ prose-code:text-[var(--amber)] prose-code:bg-muted/50 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-2xs prose-code:font-mono
117
+ prose-pre:bg-muted/30 prose-pre:border prose-pre:border-border/50 prose-pre:rounded-lg prose-pre:p-3 prose-pre:text-2xs prose-pre:overflow-x-auto
118
+ prose-ul:my-1 prose-ol:my-1
119
+ prose-hr:border-border/30 prose-hr:my-3
120
+ ${className}`}
121
+ dangerouslySetInnerHTML={{ __html: html }}
122
+ />
123
+ );
124
+ }
125
+
126
+ /** Minimal markdown to HTML — no external deps */
127
+ function renderMarkdown(md: string): string {
128
+ let html = md
129
+ // Code blocks (fenced)
130
+ .replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) =>
131
+ `<pre><code>${escHtml(code.trimEnd())}</code></pre>`)
132
+ // Inline code
133
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
134
+ // Headers
135
+ .replace(/^#### (.+)$/gm, '<h4>$1</h4>')
136
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
137
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
138
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
139
+ // Bold
140
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
141
+ // Italic
142
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
143
+ // HR
144
+ .replace(/^---$/gm, '<hr/>')
145
+ // List items
146
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
147
+ .replace(/^\* (.+)$/gm, '<li>$1</li>')
148
+ .replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
149
+
150
+ // Wrap consecutive <li> in <ul>
151
+ html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
152
+
153
+ // Paragraphs: lines not wrapped in a block tag
154
+ html = html.split('\n').map(line => {
155
+ const trimmed = line.trim();
156
+ if (!trimmed) return '';
157
+ if (/^<(h[1-4]|ul|ol|li|pre|hr|div|p|blockquote)/.test(trimmed)) return line;
158
+ return `<p>${trimmed}</p>`;
159
+ }).join('\n');
160
+
161
+ return html;
162
+ }
163
+
164
+ function escHtml(s: string): string {
165
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
166
+ }
167
+
80
168
  /* ────────── Component ────────── */
81
169
 
82
170
  export default function SkillDetailPopover({
@@ -84,6 +172,7 @@ export default function SkillDetailPopover({
84
172
  skillName,
85
173
  skill,
86
174
  agentNames = [],
175
+ allAgentNames = [],
87
176
  isNative = false,
88
177
  nativeSourcePath,
89
178
  copy,
@@ -91,6 +180,8 @@ export default function SkillDetailPopover({
91
180
  onToggle,
92
181
  onDelete,
93
182
  onRefresh,
183
+ onAddAgent,
184
+ onRemoveAgent,
94
185
  }: SkillDetailPopoverProps) {
95
186
  const [content, setContent] = useState<string | null>(null);
96
187
  const [nativeDesc, setNativeDesc] = useState<string>('');
@@ -101,7 +192,8 @@ export default function SkillDetailPopover({
101
192
  const [deleting, setDeleting] = useState(false);
102
193
  const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
103
194
  const [toggleBusy, setToggleBusy] = useState(false);
104
- const [descriptionExpanded, setDescriptionExpanded] = useState(false);
195
+ const [showAddAgent, setShowAddAgent] = useState(false);
196
+ const [contentExpanded, setContentExpanded] = useState(false);
105
197
 
106
198
  const fetchContent = useCallback(async () => {
107
199
  if (!skillName) return;
@@ -140,6 +232,8 @@ export default function SkillDetailPopover({
140
232
  setDeleteMsg(null);
141
233
  setDeleting(false);
142
234
  setToggleBusy(false);
235
+ setShowAddAgent(false);
236
+ setContentExpanded(false);
143
237
  fetchContent();
144
238
  }
145
239
  }, [open, skillName, fetchContent]);
@@ -202,6 +296,12 @@ export default function SkillDetailPopover({
202
296
  const description = skill?.description || nativeDesc || '';
203
297
  const skillPath = skill?.path || (isNative && nativeSourcePath ? `${nativeSourcePath}/${skillName}/SKILL.md` : '');
204
298
 
299
+ // Parse content into structured sections
300
+ const parsed = content ? parseSkillContent(content, description) : null;
301
+
302
+ // Available agents to add (not already assigned)
303
+ const availableAgents = allAgentNames.filter(a => !agentNames.includes(a));
304
+
205
305
  return (
206
306
  <>
207
307
  {/* Backdrop */}
@@ -250,78 +350,103 @@ export default function SkillDetailPopover({
250
350
 
251
351
  {/* ─── Body (scrollable) ─── */}
252
352
  <div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
253
- {/* Description */}
254
- {description ? (
255
- <div className="space-y-2">
256
- <p className={`text-sm text-foreground leading-relaxed whitespace-pre-wrap ${!descriptionExpanded ? 'line-clamp-3' : ''}`}>
257
- {description}
258
- </p>
259
- {description.split('\n').length > 3 && (
260
- <button
261
- type="button"
262
- onClick={() => setDescriptionExpanded(!descriptionExpanded)}
263
- className="inline-flex items-center gap-1 text-2xs font-medium text-[var(--amber)] hover:opacity-80 transition-opacity cursor-pointer"
264
- >
265
- <ChevronDown size={12} className={`transition-transform duration-200 ${descriptionExpanded ? 'rotate-180' : ''}`} />
266
- <span>{descriptionExpanded ? '收起' : '查看全部'}</span>
267
- </button>
268
- )}
269
- </div>
270
- ) : !isNative ? (
271
- <p className="text-sm text-muted-foreground italic">{copy.noDescription}</p>
272
- ) : null}
273
-
274
- {/* Quick meta */}
275
- <div className="grid grid-cols-2 gap-3">
276
- {!isNative && skill && (
277
- <MetaCard label={copy.enabled} value={skill.enabled ? '✓' : '—'} tone={skill.enabled ? 'ok' : 'muted'} />
278
- )}
279
- <MetaCard label={copy.capability} value={capability} />
280
- <MetaCard label={copy.source} value={sourceLabel} />
281
- <MetaCard label={copy.agents} value={String(agentNames.length)} />
282
- </div>
283
353
 
284
- {/* Path */}
285
- {skillPath && (
286
- <div className="rounded-xl border border-border/50 bg-muted/[0.03] p-3.5">
287
- <span className="text-2xs text-muted-foreground/60 block mb-1.5 uppercase tracking-wider">{copy.path}</span>
288
- <code className="text-xs text-foreground/80 font-mono break-all leading-relaxed">{skillPath}</code>
289
- </div>
354
+ {/* ── Section: Trigger Conditions ── */}
355
+ {description && (
356
+ <section>
357
+ <SectionTitle label="Trigger Conditions" />
358
+ <div className="rounded-lg border border-border/40 bg-muted/[0.03] p-3.5">
359
+ <p className="text-xs text-foreground/80 leading-relaxed whitespace-pre-wrap">
360
+ {description}
361
+ </p>
362
+ </div>
363
+ </section>
364
+ )}
365
+ {!description && !isNative && (
366
+ <p className="text-sm text-muted-foreground italic">{copy.noDescription}</p>
290
367
  )}
291
368
 
292
- {/* Agents */}
293
- {agentNames.length > 0 && (
294
- <div>
295
- <h3 className="text-xs font-medium text-muted-foreground mb-2">{copy.agents}</h3>
296
- <div className="flex flex-wrap gap-2">
297
- {agentNames.map((name) => (
298
- <AgentAvatar key={name} name={name} size="sm" />
299
- ))}
300
- </div>
369
+ {/* ── Section: Quick Meta ── */}
370
+ <section>
371
+ <div className="grid grid-cols-2 gap-3">
372
+ {!isNative && skill && (
373
+ <MetaCard label={copy.enabled} value={skill.enabled ? '✓' : '—'} tone={skill.enabled ? 'ok' : 'muted'} />
374
+ )}
375
+ <MetaCard label={copy.capability} value={capability} />
376
+ <MetaCard label={copy.source} value={sourceLabel} />
377
+ <MetaCard label={copy.agents} value={String(agentNames.length)} />
301
378
  </div>
302
- )}
303
- {agentNames.length === 0 && (
304
- <div className="rounded-lg border border-dashed border-border p-3 text-center">
305
- <p className="text-xs text-muted-foreground">{copy.noAgents}</p>
379
+ </section>
380
+
381
+ {/* ── Section: Connected Agents ── */}
382
+ <section>
383
+ <div className="flex items-center justify-between mb-2">
384
+ <SectionTitle label={copy.agents} noMargin />
385
+ {onAddAgent && availableAgents.length > 0 && (
386
+ <button
387
+ type="button"
388
+ onClick={() => setShowAddAgent(!showAddAgent)}
389
+ className="inline-flex items-center gap-1 text-2xs font-medium text-[var(--amber)] hover:opacity-80 transition-opacity cursor-pointer"
390
+ >
391
+ <Plus size={12} />
392
+ <span>Add</span>
393
+ </button>
394
+ )}
306
395
  </div>
307
- )}
308
396
 
309
- {/* Content */}
310
- {(content !== null || loading || loadError) && (
311
- <div>
312
- <div className="flex items-center justify-between mb-2">
313
- <h3 className="text-xs font-medium text-muted-foreground">{copy.content}</h3>
314
- {content && (
397
+ {/* Add agent picker */}
398
+ {showAddAgent && availableAgents.length > 0 && (
399
+ <div className="flex flex-wrap gap-1.5 mb-2 p-2 rounded-lg border border-dashed border-[var(--amber)]/30 bg-[var(--amber)]/[0.03]">
400
+ {availableAgents.map((name) => (
315
401
  <button
402
+ key={name}
316
403
  type="button"
317
- onClick={handleCopy}
318
- className="inline-flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1.5 py-0.5"
319
- aria-label={copy.copyContent}
404
+ onClick={() => { onAddAgent?.(skillName, name); setShowAddAgent(false); }}
405
+ className="inline-flex items-center gap-1 px-2 py-1 text-2xs rounded-md border border-border bg-card hover:bg-muted cursor-pointer transition-colors"
320
406
  >
321
- {copied ? <Check size={11} /> : <Copy size={11} />}
322
- {copied ? copy.copied : copy.copyContent}
407
+ <Plus size={10} className="text-[var(--amber)]" />
408
+ <span className="text-foreground/80">{name}</span>
323
409
  </button>
324
- )}
410
+ ))}
411
+ </div>
412
+ )}
413
+
414
+ {agentNames.length > 0 ? (
415
+ <div className="flex flex-wrap gap-2">
416
+ {agentNames.map((name) => (
417
+ <AgentAvatar
418
+ key={name}
419
+ name={name}
420
+ size="sm"
421
+ onRemove={onRemoveAgent ? () => onRemoveAgent(skillName, name) : undefined}
422
+ />
423
+ ))}
424
+ </div>
425
+ ) : (
426
+ <div className="rounded-lg border border-dashed border-border p-3 text-center">
427
+ <p className="text-xs text-muted-foreground">{copy.noAgents}</p>
428
+ </div>
429
+ )}
430
+ </section>
431
+
432
+ {/* ── Section: Skill Instructions (markdown) ── */}
433
+ {(content !== null || loading || loadError) && (
434
+ <section>
435
+ <div className="flex items-center justify-between mb-2">
436
+ <SectionTitle label="Instructions" noMargin />
437
+ <div className="flex items-center gap-2">
438
+ {content && (
439
+ <button
440
+ type="button"
441
+ onClick={handleCopy}
442
+ className="inline-flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground cursor-pointer transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1.5 py-0.5"
443
+ aria-label={copy.copyContent}
444
+ >
445
+ {copied ? <Check size={11} /> : <Copy size={11} />}
446
+ {copied ? copy.copied : copy.copyContent}
447
+ </button>
448
+ )}
449
+ </div>
325
450
  </div>
326
451
 
327
452
  {loading && (
@@ -344,12 +469,32 @@ export default function SkillDetailPopover({
344
469
  </div>
345
470
  )}
346
471
 
347
- {content && !loading && !loadError && (
348
- <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground font-mono overflow-x-auto max-h-80 leading-relaxed whitespace-pre-wrap break-words">
349
- {content}
350
- </pre>
472
+ {parsed && !loading && !loadError && (
473
+ <div className={`rounded-lg border border-border/50 bg-muted/[0.02] p-4 overflow-hidden transition-all duration-200 ${!contentExpanded ? 'max-h-60' : ''}`}>
474
+ <MarkdownContent text={parsed.instructions} />
475
+ </div>
351
476
  )}
352
- </div>
477
+ {parsed && !loading && !loadError && parsed.instructions.split('\n').length > 15 && (
478
+ <button
479
+ type="button"
480
+ onClick={() => setContentExpanded(!contentExpanded)}
481
+ className="inline-flex items-center gap-1 mt-2 text-2xs font-medium text-[var(--amber)] hover:opacity-80 transition-opacity cursor-pointer"
482
+ >
483
+ <ChevronDown size={12} className={`transition-transform duration-200 ${contentExpanded ? 'rotate-180' : ''}`} />
484
+ <span>{contentExpanded ? 'Collapse' : 'View All'}</span>
485
+ </button>
486
+ )}
487
+ </section>
488
+ )}
489
+
490
+ {/* ── Section: File Path ── */}
491
+ {skillPath && (
492
+ <section>
493
+ <SectionTitle label={copy.path} />
494
+ <div className="rounded-lg border border-border/50 bg-muted/[0.03] p-3">
495
+ <code className="text-2xs text-foreground/60 font-mono break-all leading-relaxed">{skillPath}</code>
496
+ </div>
497
+ </section>
353
498
  )}
354
499
 
355
500
  {/* Delete message */}
@@ -410,6 +555,16 @@ export default function SkillDetailPopover({
410
555
  );
411
556
  }
412
557
 
558
+ /* ────────── Section Title ────────── */
559
+
560
+ function SectionTitle({ label, noMargin }: { label: string; noMargin?: boolean }) {
561
+ return (
562
+ <h3 className={`text-xs font-semibold text-muted-foreground uppercase tracking-wider ${noMargin ? '' : 'mb-2'}`}>
563
+ {label}
564
+ </h3>
565
+ );
566
+ }
567
+
413
568
  /* ────────── Meta Card ────────── */
414
569
 
415
570
  function MetaCard({
@@ -494,7 +494,7 @@ export function detectAgentRuntimeSignals(agentKey: string): AgentRuntimeSignals
494
494
 
495
495
  /* ── MindOS MCP Install Detection ──────────────────────────────────────── */
496
496
 
497
- export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string } {
497
+ export function detectInstalled(agentKey: string): { installed: boolean; scope?: string; transport?: string; configPath?: string; url?: string } {
498
498
  const agent = MCP_AGENTS[agentKey];
499
499
  if (!agent) return { installed: false };
500
500
 
@@ -510,7 +510,7 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
510
510
  if (result.found && result.entry) {
511
511
  const entry = result.entry;
512
512
  const transport = entry.type === 'stdio' ? 'stdio' : entry.url ? 'http' : 'unknown';
513
- return { installed: true, scope: scopeType, transport, configPath: cfgPath };
513
+ return { installed: true, scope: scopeType, transport, configPath: cfgPath, url: entry.url };
514
514
  }
515
515
  } else {
516
516
  // JSON format (default)
@@ -521,7 +521,7 @@ export function detectInstalled(agentKey: string): { installed: boolean; scope?:
521
521
  if (servers?.mindos) {
522
522
  const entry = servers.mindos as Record<string, unknown>;
523
523
  const transport = entry.type === 'stdio' ? 'stdio' : entry.url ? 'http' : 'unknown';
524
- return { installed: true, scope: scopeType, transport, configPath: cfgPath };
524
+ return { installed: true, scope: scopeType, transport, configPath: cfgPath, url: entry.url as string | undefined };
525
525
  }
526
526
  }
527
527
  } catch { /* ignore parse errors */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.6.21",
3
+ "version": "0.6.22",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",