stagent 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/cli.js +146 -2
- package/docs/.coverage-gaps.json +21 -0
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +36 -14
- package/docs/features/chat.md +33 -56
- package/docs/features/cost-usage.md +14 -10
- package/docs/features/dashboard-kanban.md +30 -13
- package/docs/features/delivery-channels.md +198 -0
- package/docs/features/design-system.md +10 -10
- package/docs/features/documents.md +8 -8
- package/docs/features/home-workspace.md +20 -15
- package/docs/features/inbox-notifications.md +22 -10
- package/docs/features/keyboard-navigation.md +11 -11
- package/docs/features/monitoring.md +1 -1
- package/docs/features/playbook.md +30 -32
- package/docs/features/profiles.md +33 -11
- package/docs/features/projects.md +2 -2
- package/docs/features/provider-runtimes.md +58 -14
- package/docs/features/schedules.md +70 -40
- package/docs/features/settings.md +74 -46
- package/docs/features/shared-components.md +7 -15
- package/docs/features/tool-permissions.md +9 -9
- package/docs/features/workflows.md +32 -21
- package/docs/getting-started.md +33 -9
- package/docs/index.md +25 -16
- package/docs/journeys/developer.md +124 -207
- package/docs/journeys/personal-use.md +70 -79
- package/docs/journeys/power-user.md +107 -151
- package/docs/journeys/work-use.md +81 -113
- package/docs/manifest.json +77 -45
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/use-cases/agency-operator.md +84 -0
- package/docs/use-cases/solo-founder.md +75 -0
- package/docs/why-stagent.md +59 -0
- package/package.json +10 -3
- package/src/app/api/channels/[id]/route.ts +104 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +116 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
- package/src/app/api/channels/inbound/telegram/route.ts +87 -0
- package/src/app/api/channels/route.ts +72 -0
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +46 -31
- package/src/app/api/data/clear/route.ts +4 -0
- package/src/app/api/data/seed/route.ts +4 -0
- package/src/app/api/documents/route.ts +36 -6
- package/src/app/api/environment/profiles/suggest/route.ts +19 -3
- package/src/app/api/environment/scan/route.ts +8 -1
- package/src/app/api/handoffs/[id]/route.ts +76 -0
- package/src/app/api/handoffs/route.ts +89 -0
- package/src/app/api/memory/route.ts +181 -0
- package/src/app/api/profiles/[id]/route.ts +16 -1
- package/src/app/api/profiles/[id]/test/route.ts +4 -0
- package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
- package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
- package/src/app/api/profiles/assist/route.ts +35 -0
- package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
- package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
- package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
- package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
- package/src/app/api/profiles/import-repo/route.ts +29 -0
- package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
- package/src/app/api/profiles/route.ts +73 -22
- package/src/app/api/runtimes/ollama/route.ts +86 -0
- package/src/app/api/runtimes/suggest/route.ts +29 -0
- package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
- package/src/app/api/schedules/[id]/route.ts +41 -3
- package/src/app/api/schedules/parse/route.ts +66 -0
- package/src/app/api/schedules/route.ts +71 -12
- package/src/app/api/settings/author-default/route.ts +7 -0
- package/src/app/api/settings/learning/route.ts +41 -0
- package/src/app/api/settings/ollama/route.ts +34 -0
- package/src/app/api/settings/providers/route.ts +57 -0
- package/src/app/api/settings/routing/route.ts +24 -0
- package/src/app/api/settings/web-search/route.ts +28 -0
- package/src/app/api/tasks/[id]/execute/route.ts +13 -1
- package/src/app/api/tasks/[id]/respond/route.ts +23 -1
- package/src/app/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +10 -4
- package/src/app/workflows/[id]/edit/page.tsx +2 -0
- package/src/app/workflows/new/page.tsx +2 -0
- package/src/components/chat/chat-command-popover.tsx +22 -19
- package/src/components/chat/chat-input.tsx +5 -0
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +2 -0
- package/src/components/dashboard/welcome-landing.tsx +9 -9
- package/src/components/environment/artifact-card.tsx +27 -1
- package/src/components/environment/environment-dashboard.tsx +50 -2
- package/src/components/environment/environment-summary-card.tsx +5 -2
- package/src/components/environment/suggested-profiles.tsx +117 -52
- package/src/components/handoffs/handoff-approval-card.tsx +159 -0
- package/src/components/memory/memory-browser.tsx +315 -0
- package/src/components/profiles/learned-context-panel.tsx +4 -4
- package/src/components/profiles/profile-assist-panel.tsx +512 -0
- package/src/components/profiles/profile-browser.tsx +109 -8
- package/src/components/profiles/profile-card.tsx +29 -1
- package/src/components/profiles/profile-detail-view.tsx +200 -28
- package/src/components/profiles/profile-form-view.tsx +220 -82
- package/src/components/profiles/repo-import-wizard.tsx +648 -0
- package/src/components/profiles/smoke-test-editor.tsx +106 -0
- package/src/components/schedules/schedule-create-sheet.tsx +9 -1
- package/src/components/schedules/schedule-form.tsx +348 -9
- package/src/components/schedules/schedule-list.tsx +15 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/budget-guardrails-section.tsx +111 -48
- package/src/components/settings/channels-section.tsx +526 -0
- package/src/components/settings/chat-settings-section.tsx +27 -1
- package/src/components/settings/data-management-section.tsx +8 -6
- package/src/components/settings/learning-context-section.tsx +124 -0
- package/src/components/settings/ollama-section.tsx +270 -0
- package/src/components/settings/providers-runtimes-section.tsx +499 -0
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/tag-input.tsx +156 -0
- package/src/components/tasks/kanban-board.tsx +32 -0
- package/src/components/tasks/kanban-column.tsx +4 -2
- package/src/components/tasks/task-card.tsx +1 -0
- package/src/components/tasks/task-chip-bar.tsx +6 -1
- package/src/components/tasks/task-create-panel.tsx +55 -5
- package/src/components/workflows/workflow-form-view.tsx +38 -3
- package/src/hooks/use-chat-autocomplete.ts +24 -26
- package/src/hooks/use-project-skills.ts +66 -0
- package/src/hooks/use-tag-suggestions.ts +31 -0
- package/src/instrumentation.ts +4 -1
- package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
- package/src/lib/agents/agentic-loop.ts +235 -0
- package/src/lib/agents/browser-mcp.ts +59 -4
- package/src/lib/agents/claude-agent.ts +27 -200
- package/src/lib/agents/handoff/bus.ts +164 -0
- package/src/lib/agents/handoff/governance.ts +47 -0
- package/src/lib/agents/handoff/types.ts +16 -0
- package/src/lib/agents/learned-context.ts +27 -7
- package/src/lib/agents/memory/decay.ts +61 -0
- package/src/lib/agents/memory/extractor.ts +181 -0
- package/src/lib/agents/memory/retrieval.ts +96 -0
- package/src/lib/agents/memory/types.ts +6 -0
- package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/project-profiles.ts +193 -0
- package/src/lib/agents/profiles/registry.ts +130 -6
- package/src/lib/agents/profiles/types.ts +28 -0
- package/src/lib/agents/router.ts +174 -2
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
- package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
- package/src/lib/agents/runtime/catalog.ts +57 -2
- package/src/lib/agents/runtime/claude.ts +205 -1
- package/src/lib/agents/runtime/index.ts +22 -0
- package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
- package/src/lib/agents/runtime/openai-direct.ts +514 -0
- package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
- package/src/lib/agents/runtime/types.ts +2 -0
- package/src/lib/agents/tool-permissions.ts +203 -0
- package/src/lib/channels/gateway.ts +321 -0
- package/src/lib/channels/poller.ts +268 -0
- package/src/lib/channels/registry.ts +90 -0
- package/src/lib/channels/slack-adapter.ts +188 -0
- package/src/lib/channels/telegram-adapter.ts +218 -0
- package/src/lib/channels/types.ts +75 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/context-builder.ts +22 -2
- package/src/lib/chat/engine.ts +95 -13
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +24 -0
- package/src/lib/chat/tool-registry.ts +90 -0
- package/src/lib/chat/tools/chat-history-tools.ts +4 -4
- package/src/lib/chat/tools/document-tools.ts +7 -7
- package/src/lib/chat/tools/handoff-tools.ts +70 -0
- package/src/lib/chat/tools/notification-tools.ts +4 -4
- package/src/lib/chat/tools/profile-tools.ts +3 -3
- package/src/lib/chat/tools/project-tools.ts +3 -3
- package/src/lib/chat/tools/schedule-tools.ts +29 -13
- package/src/lib/chat/tools/settings-tools.ts +2 -2
- package/src/lib/chat/tools/task-tools.ts +66 -11
- package/src/lib/chat/tools/usage-tools.ts +2 -2
- package/src/lib/chat/tools/workflow-tools.ts +8 -8
- package/src/lib/chat/types.ts +11 -5
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +7 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +22 -0
- package/src/lib/data/profile-test-results.ts +48 -0
- package/src/lib/data/seed-data/conversations.ts +196 -0
- package/src/lib/data/seed-data/learned-context.ts +99 -0
- package/src/lib/data/seed-data/notifications.ts +54 -1
- package/src/lib/data/seed-data/profile-test-results.ts +96 -0
- package/src/lib/data/seed-data/repo-imports.ts +51 -0
- package/src/lib/data/seed-data/views.ts +60 -0
- package/src/lib/data/seed.ts +51 -0
- package/src/lib/db/bootstrap.ts +162 -0
- package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
- package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
- package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
- package/src/lib/db/schema.ts +190 -1
- package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
- package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
- package/src/lib/environment/auto-scan.ts +48 -0
- package/src/lib/environment/data.ts +25 -0
- package/src/lib/environment/profile-generator.ts +40 -10
- package/src/lib/environment/profile-linker.ts +143 -0
- package/src/lib/environment/profile-rules.ts +96 -0
- package/src/lib/import/dedup.ts +149 -0
- package/src/lib/import/format-adapter.ts +631 -0
- package/src/lib/import/github-api.ts +219 -0
- package/src/lib/import/repo-scanner.ts +251 -0
- package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
- package/src/lib/schedules/active-hours.ts +120 -0
- package/src/lib/schedules/heartbeat-parser.ts +224 -0
- package/src/lib/schedules/heartbeat-prompt.ts +153 -0
- package/src/lib/schedules/nlp-parser.ts +357 -0
- package/src/lib/schedules/scheduler.ts +218 -3
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
- package/src/lib/settings/helpers.ts +6 -0
- package/src/lib/settings/routing.ts +24 -0
- package/src/lib/settings/runtime-setup.ts +28 -1
- package/src/lib/usage/ledger.ts +2 -1
- package/src/lib/validators/__tests__/settings.test.ts +9 -0
- package/src/lib/validators/profile.ts +39 -0
- package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
- package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
- package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
- package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
- package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts non-Stagent skill formats into valid ProfileConfig + SKILL.md pairs.
|
|
3
|
+
* Handles gstack-style SKILL.md-with-frontmatter → profile.yaml generation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
import { ProfileConfigSchema, type ProfileConfig } from "@/lib/validators/profile";
|
|
9
|
+
import type { ImportMeta } from "@/lib/validators/profile";
|
|
10
|
+
import type { DiscoveredSkill } from "./repo-scanner";
|
|
11
|
+
|
|
12
|
+
export interface AdaptedProfile {
|
|
13
|
+
config: ProfileConfig;
|
|
14
|
+
skillMd: string;
|
|
15
|
+
importMeta: ImportMeta;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RepoMeta {
|
|
19
|
+
repoUrl: string;
|
|
20
|
+
owner: string;
|
|
21
|
+
repo: string;
|
|
22
|
+
branch: string;
|
|
23
|
+
commitSha: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Extra context from README files used to enrich profile metadata. */
|
|
27
|
+
export interface ReadmeContext {
|
|
28
|
+
/** Per-skill README.md (if the skill directory has one) */
|
|
29
|
+
skillReadme: string | null;
|
|
30
|
+
/** Repo-level README.md */
|
|
31
|
+
repoReadme: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Slugify a name to a valid profile ID. */
|
|
35
|
+
function slugify(name: string): string {
|
|
36
|
+
return name
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
39
|
+
.replace(/-+/g, "-")
|
|
40
|
+
.replace(/^-|-$/g, "")
|
|
41
|
+
.slice(0, 64);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Infer domain from description/name keywords. */
|
|
45
|
+
function inferDomain(description: string, name: string): "work" | "personal" {
|
|
46
|
+
const text = `${description} ${name}`.toLowerCase();
|
|
47
|
+
const personalKeywords = [
|
|
48
|
+
"personal", "health", "fitness", "travel", "shopping",
|
|
49
|
+
"nutrition", "workout", "hobby", "recipe", "meditation",
|
|
50
|
+
];
|
|
51
|
+
return personalKeywords.some((kw) => text.includes(kw)) ? "personal" : "work";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Common stop words to exclude from tag extraction. */
|
|
55
|
+
const TAG_STOP_WORDS = new Set([
|
|
56
|
+
"the", "and", "for", "are", "but", "not", "you", "all", "can", "had",
|
|
57
|
+
"was", "one", "our", "out", "has", "have", "that", "this", "with",
|
|
58
|
+
"from", "they", "been", "will", "each", "make", "like", "into", "them",
|
|
59
|
+
"some", "when", "what", "your", "should", "would", "could", "about",
|
|
60
|
+
"which", "their", "other", "than", "then", "more", "also", "only",
|
|
61
|
+
"must", "does", "here", "just", "over", "such", "after", "before",
|
|
62
|
+
"using", "ensure", "every", "following", "include", "note", "step",
|
|
63
|
+
"always", "never", "check", "first", "output", "input", "file",
|
|
64
|
+
"true", "false", "null", "undefined", "return", "function", "class",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract a rich description from SKILL.md body content + optional README.
|
|
69
|
+
*
|
|
70
|
+
* Priority:
|
|
71
|
+
* 1. Frontmatter `description` (if multi-word and meaningful)
|
|
72
|
+
* 2. Per-skill README.md first paragraph
|
|
73
|
+
* 3. First meaningful paragraph from SKILL.md body (after frontmatter)
|
|
74
|
+
* 4. Fallback to frontmatter description or name
|
|
75
|
+
*/
|
|
76
|
+
function extractDescription(
|
|
77
|
+
frontmatter: Record<string, string>,
|
|
78
|
+
skillMdBody: string,
|
|
79
|
+
readmeCtx: ReadmeContext | null,
|
|
80
|
+
skillName: string,
|
|
81
|
+
repoReadmeSkillDesc: string | null,
|
|
82
|
+
): string {
|
|
83
|
+
// 1. If frontmatter description is already rich (> 20 chars), use it
|
|
84
|
+
const fmDesc = frontmatter.description ?? "";
|
|
85
|
+
if (fmDesc.length > 20) return fmDesc;
|
|
86
|
+
|
|
87
|
+
// 2. Try per-skill README first paragraph
|
|
88
|
+
if (readmeCtx?.skillReadme) {
|
|
89
|
+
const para = extractFirstParagraph(readmeCtx.skillReadme);
|
|
90
|
+
if (para) return para;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Try description extracted from repo README (skill-specific section)
|
|
94
|
+
if (repoReadmeSkillDesc) return repoReadmeSkillDesc;
|
|
95
|
+
|
|
96
|
+
// 4. Extract from SKILL.md body — first non-heading, non-empty paragraph
|
|
97
|
+
const bodyPara = extractFirstParagraph(skillMdBody);
|
|
98
|
+
if (bodyPara) return bodyPara;
|
|
99
|
+
|
|
100
|
+
// 5. Fallback
|
|
101
|
+
return fmDesc || skillName;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Patterns that indicate a paragraph is AI instruction text, not a human-readable description.
|
|
106
|
+
* These are common across skill repos (not specific to any one repo).
|
|
107
|
+
*/
|
|
108
|
+
const INSTRUCTION_PATTERNS = [
|
|
109
|
+
/^you are\b/i,
|
|
110
|
+
/^you must\b/i,
|
|
111
|
+
/^you should\b/i,
|
|
112
|
+
/^you will\b/i,
|
|
113
|
+
/^your (?:role|job|task|goal)\b/i,
|
|
114
|
+
/^run the\b/i,
|
|
115
|
+
/^execute\b/i,
|
|
116
|
+
/^always\b/i,
|
|
117
|
+
/^never\b/i,
|
|
118
|
+
/^when the user\b/i,
|
|
119
|
+
/^when asked\b/i,
|
|
120
|
+
/^this skill\b/i,
|
|
121
|
+
/^this tool\b/i,
|
|
122
|
+
/^this agent\b/i,
|
|
123
|
+
/^if the user\b/i,
|
|
124
|
+
/^before (?:you|running|starting)\b/i,
|
|
125
|
+
/^after (?:you|running|completing)\b/i,
|
|
126
|
+
/^do not\b/i,
|
|
127
|
+
/^don't\b/i,
|
|
128
|
+
/^make sure\b/i,
|
|
129
|
+
/^important:/i,
|
|
130
|
+
/^note:/i,
|
|
131
|
+
/^⚠/,
|
|
132
|
+
/^warning/i,
|
|
133
|
+
/^todo/i,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/** Check if a paragraph looks like AI skill instructions rather than a description. */
|
|
137
|
+
function isInstructionText(text: string): boolean {
|
|
138
|
+
// Check against known instruction patterns
|
|
139
|
+
if (INSTRUCTION_PATTERNS.some((p) => p.test(text))) return true;
|
|
140
|
+
|
|
141
|
+
// Heavy use of second-person "you" suggests instruction text
|
|
142
|
+
const youCount = (text.match(/\byou\b/gi) ?? []).length;
|
|
143
|
+
const wordCount = text.split(/\s+/).length;
|
|
144
|
+
if (youCount >= 3 || (wordCount > 0 && youCount / wordCount > 0.08)) return true;
|
|
145
|
+
|
|
146
|
+
// References to tool names suggest internal skill instructions
|
|
147
|
+
const toolRefs = /\b(Bash|Read|Write|Edit|Glob|Grep|WebFetch|WebSearch|Agent|AskUserQuestion)\b/;
|
|
148
|
+
if (toolRefs.test(text)) return true;
|
|
149
|
+
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract the first meaningful, non-instruction paragraph from markdown.
|
|
155
|
+
* Skips headings, lists, code blocks, HTML comments, and AI instruction text.
|
|
156
|
+
*/
|
|
157
|
+
function extractFirstParagraph(md: string): string | null {
|
|
158
|
+
const lines = md.split("\n");
|
|
159
|
+
let inCodeBlock = false;
|
|
160
|
+
let inComment = false;
|
|
161
|
+
|
|
162
|
+
// Collect candidate paragraphs, return the first non-instruction one
|
|
163
|
+
let paraLines: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
if (line.startsWith("```")) {
|
|
167
|
+
inCodeBlock = !inCodeBlock;
|
|
168
|
+
if (paraLines.length > 0) { paraLines = []; } // discard partial
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (inCodeBlock) continue;
|
|
172
|
+
|
|
173
|
+
const trimmed = line.trim();
|
|
174
|
+
|
|
175
|
+
// Skip HTML comments (single-line and multi-line)
|
|
176
|
+
if (trimmed.startsWith("<!--")) {
|
|
177
|
+
if (!trimmed.includes("-->")) inComment = true;
|
|
178
|
+
if (paraLines.length > 0) { paraLines = []; }
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (inComment) {
|
|
182
|
+
if (trimmed.includes("-->")) inComment = false;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Skip headings, HR, HTML tags
|
|
187
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("---") || trimmed.startsWith("<")) {
|
|
188
|
+
if (paraLines.length > 0) {
|
|
189
|
+
// Try this paragraph
|
|
190
|
+
const candidate = paraLines.join(" ").trim();
|
|
191
|
+
if (isGoodDescription(candidate)) return formatDescription(candidate);
|
|
192
|
+
paraLines = [];
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Skip list items and blockquotes
|
|
197
|
+
if (trimmed.startsWith("- ") || trimmed.startsWith("* ") || trimmed.startsWith("> ") || /^\d+\.\s/.test(trimmed)) {
|
|
198
|
+
if (paraLines.length > 0) {
|
|
199
|
+
const candidate = paraLines.join(" ").trim();
|
|
200
|
+
if (isGoodDescription(candidate)) return formatDescription(candidate);
|
|
201
|
+
paraLines = [];
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (trimmed === "") {
|
|
207
|
+
if (paraLines.length > 0) {
|
|
208
|
+
const candidate = paraLines.join(" ").trim();
|
|
209
|
+
if (isGoodDescription(candidate)) return formatDescription(candidate);
|
|
210
|
+
paraLines = [];
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
paraLines.push(trimmed);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check final paragraph
|
|
219
|
+
if (paraLines.length > 0) {
|
|
220
|
+
const candidate = paraLines.join(" ").trim();
|
|
221
|
+
if (isGoodDescription(candidate)) return formatDescription(candidate);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Check if text is a good human-readable description (not instruction text). */
|
|
228
|
+
function isGoodDescription(text: string): boolean {
|
|
229
|
+
if (text.length < 15) return false;
|
|
230
|
+
if (text.startsWith("```") || text.startsWith("$")) return false;
|
|
231
|
+
if (isInstructionText(text)) return false;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Trim description to max length. */
|
|
236
|
+
function formatDescription(text: string): string {
|
|
237
|
+
return text.length > 200 ? text.slice(0, 197) + "..." : text;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Strip markdown inline formatting: **bold**, *italic*, `code`, [links](url) */
|
|
241
|
+
function stripMarkdownFormatting(text: string): string {
|
|
242
|
+
return text
|
|
243
|
+
.replace(/\*\*(.+?)\*\*/g, "$1") // **bold**
|
|
244
|
+
.replace(/\*(.+?)\*/g, "$1") // *italic*
|
|
245
|
+
.replace(/`(.+?)`/g, "$1") // `code`
|
|
246
|
+
.replace(/\[(.+?)\]\(.+?\)/g, "$1") // [text](url)
|
|
247
|
+
.trim();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Search the repo README for a section or table row that describes a specific skill.
|
|
252
|
+
* Generalized for any repo format:
|
|
253
|
+
* - N-column tables: `| /name | role | description |` or `| name | description |`
|
|
254
|
+
* - List items: `- **name**: description` or `- \`name\` — description`
|
|
255
|
+
* - Headings: `### name\n description paragraph`
|
|
256
|
+
*/
|
|
257
|
+
function findSkillInRepoReadme(repoReadme: string, skillName: string): string | null {
|
|
258
|
+
if (!repoReadme) return null;
|
|
259
|
+
|
|
260
|
+
const nameL = skillName.toLowerCase();
|
|
261
|
+
const namePat = skillName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
262
|
+
|
|
263
|
+
// Strategy 1: Table rows — find any row where a cell matches the skill name,
|
|
264
|
+
// then take the LAST non-name cell as the description (works for 2, 3, or N columns).
|
|
265
|
+
const tableLines = repoReadme.split("\n").filter((l) => l.trim().startsWith("|"));
|
|
266
|
+
for (const line of tableLines) {
|
|
267
|
+
// Skip separator rows (| --- | --- |)
|
|
268
|
+
if (/^\|[\s-:|]+\|$/.test(line.trim())) continue;
|
|
269
|
+
|
|
270
|
+
const cells = line
|
|
271
|
+
.split("|")
|
|
272
|
+
.map((c) => c.trim())
|
|
273
|
+
.filter((c) => c.length > 0);
|
|
274
|
+
|
|
275
|
+
// Check if any cell contains the skill name (with or without / prefix)
|
|
276
|
+
const nameCell = cells.findIndex((c) => {
|
|
277
|
+
const stripped = stripMarkdownFormatting(c).replace(/^\//, "").toLowerCase();
|
|
278
|
+
return stripped === nameL || stripped === namePat.toLowerCase();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (nameCell >= 0 && cells.length > nameCell + 1) {
|
|
282
|
+
// Take the last cell as description (skip the name cell and any role/middle cells)
|
|
283
|
+
const descCell = stripMarkdownFormatting(cells[cells.length - 1]);
|
|
284
|
+
// But if the last cell IS the name cell, try the one before it
|
|
285
|
+
const desc = nameCell === cells.length - 1
|
|
286
|
+
? stripMarkdownFormatting(cells[Math.max(0, cells.length - 2)])
|
|
287
|
+
: descCell;
|
|
288
|
+
|
|
289
|
+
if (desc.length > 10 && desc.toLowerCase() !== nameL) {
|
|
290
|
+
return formatDescription(desc);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Strategy 2: List items — `- **name** — desc` or `- \`name\`: desc` etc.
|
|
296
|
+
const backtick = "`";
|
|
297
|
+
const listPatterns = [
|
|
298
|
+
// - **name** — description or - **name**: description
|
|
299
|
+
new RegExp("[-*]\\s+\\*\\*/?\\s*" + namePat + "\\s*\\*\\*\\s*[—:\\-–|]\\s*(.+)", "i"),
|
|
300
|
+
// - `name` — description or - `/name`: description
|
|
301
|
+
new RegExp("[-*]\\s+" + backtick + "/?" + namePat + backtick + "\\s*[—:\\-–|]\\s*(.+)", "i"),
|
|
302
|
+
// - name: description (plain)
|
|
303
|
+
new RegExp("[-*]\\s+/?" + namePat + "\\s*[—:\\-–]\\s*(.+)", "i"),
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
for (const pattern of listPatterns) {
|
|
307
|
+
const match = repoReadme.match(pattern);
|
|
308
|
+
if (match) {
|
|
309
|
+
const desc = stripMarkdownFormatting(match[1].trim());
|
|
310
|
+
if (desc.length > 10) return formatDescription(desc);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Strategy 3: Section heading containing the skill name, then next paragraph
|
|
315
|
+
const lines = repoReadme.split("\n");
|
|
316
|
+
for (let i = 0; i < lines.length; i++) {
|
|
317
|
+
const line = lines[i].trim();
|
|
318
|
+
if (/^#{1,4}\s/.test(line)) {
|
|
319
|
+
const headingText = line.replace(/^#+\s+/, "").toLowerCase();
|
|
320
|
+
// Check if heading matches skill name (exact word, not substring of a longer word)
|
|
321
|
+
const headingWords = headingText.split(/[\s/]+/);
|
|
322
|
+
if (!headingWords.includes(nameL) && !headingText.includes(`/${nameL}`)) continue;
|
|
323
|
+
|
|
324
|
+
// Grab the next non-empty, non-heading line as description
|
|
325
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
326
|
+
const next = lines[j].trim();
|
|
327
|
+
if (!next || next.startsWith("#") || next.startsWith("---") || next.startsWith("|")) continue;
|
|
328
|
+
if (next.length > 15) {
|
|
329
|
+
return formatDescription(stripMarkdownFormatting(next));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Extract semantic tags from SKILL.md body content + README context.
|
|
340
|
+
* Looks at headings, role keywords, domain terms, and tool names.
|
|
341
|
+
*/
|
|
342
|
+
function extractTags(
|
|
343
|
+
frontmatter: Record<string, string>,
|
|
344
|
+
skillMdBody: string,
|
|
345
|
+
dirName: string,
|
|
346
|
+
readmeCtx: ReadmeContext | null,
|
|
347
|
+
): string[] {
|
|
348
|
+
const tags = new Set<string>();
|
|
349
|
+
const text = skillMdBody.toLowerCase();
|
|
350
|
+
|
|
351
|
+
// Directory name is always a tag
|
|
352
|
+
if (dirName && dirName.length > 1) tags.add(dirName.toLowerCase());
|
|
353
|
+
|
|
354
|
+
// Role/persona keywords found in content
|
|
355
|
+
const rolePatterns: Array<[RegExp, string]> = [
|
|
356
|
+
[/\bcode review/i, "code-review"],
|
|
357
|
+
[/\bsecurity\b/i, "security"],
|
|
358
|
+
[/\bqa\b|\bquality assurance/i, "qa"],
|
|
359
|
+
[/\btesting\b|\btest suite/i, "testing"],
|
|
360
|
+
[/\bdesign\b|\bui\/ux\b|\bfrontend design/i, "design"],
|
|
361
|
+
[/\bdeployment\b|\bci\/cd\b|\binfrastructure/i, "devops"],
|
|
362
|
+
[/\bship\b|\brelease\b|\bchangelog/i, "shipping"],
|
|
363
|
+
[/\bresearch\b|\binvestigat/i, "research"],
|
|
364
|
+
[/\bplanning\b|\barchitect/i, "planning"],
|
|
365
|
+
[/\brefactor/i, "refactoring"],
|
|
366
|
+
[/\bperformance\b|\bbenchmark/i, "performance"],
|
|
367
|
+
[/\baccessibility\b|\ba11y/i, "accessibility"],
|
|
368
|
+
[/\bdocument/i, "documentation"],
|
|
369
|
+
[/\bapi\b/i, "api"],
|
|
370
|
+
[/\bdatabase\b|\bsql\b/i, "database"],
|
|
371
|
+
[/\bbrowser\b|\bplaywright\b|\bpuppeteer/i, "browser"],
|
|
372
|
+
[/\bautomation\b/i, "automation"],
|
|
373
|
+
[/\bworkflow/i, "workflow"],
|
|
374
|
+
[/\blint/i, "linting"],
|
|
375
|
+
[/\bmigrat/i, "migration"],
|
|
376
|
+
[/\bmonitor/i, "monitoring"],
|
|
377
|
+
[/\bdebug/i, "debugging"],
|
|
378
|
+
[/\bowasp\b|\bvulnerabilit/i, "security"],
|
|
379
|
+
[/\bprompt\b|\bllm\b|\bai\b/i, "ai"],
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
for (const [pattern, tag] of rolePatterns) {
|
|
383
|
+
if (pattern.test(text)) tags.add(tag);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Extract heading-level topics (## headings become tags)
|
|
387
|
+
const headings = skillMdBody.match(/^#{2,3}\s+(.+)$/gm) ?? [];
|
|
388
|
+
for (const h of headings.slice(0, 5)) {
|
|
389
|
+
const topic = h.replace(/^#+\s+/, "").trim().toLowerCase();
|
|
390
|
+
// Only use short, meaningful heading words as tags
|
|
391
|
+
const words = topic.split(/\s+/).filter(
|
|
392
|
+
(w) => w.length > 3 && w.length < 20 && !TAG_STOP_WORDS.has(w)
|
|
393
|
+
);
|
|
394
|
+
for (const w of words.slice(0, 2)) tags.add(w);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Parse allowed-tools from frontmatter (but as capability tags, not raw tool names)
|
|
398
|
+
const tools = frontmatter["allowed-tools"];
|
|
399
|
+
if (tools) {
|
|
400
|
+
const toolList = tools
|
|
401
|
+
.split(/[,\n]/)
|
|
402
|
+
.map((t) => t.replace(/^-\s*/, "").trim().toLowerCase())
|
|
403
|
+
.filter(Boolean);
|
|
404
|
+
if (toolList.includes("bash")) tags.add("cli");
|
|
405
|
+
if (toolList.includes("webfetch") || toolList.includes("websearch")) tags.add("web");
|
|
406
|
+
if (toolList.includes("agent")) tags.add("orchestration");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Look for keywords in README context
|
|
410
|
+
if (readmeCtx?.skillReadme) {
|
|
411
|
+
const readmeText = readmeCtx.skillReadme.toLowerCase();
|
|
412
|
+
for (const [pattern, tag] of rolePatterns) {
|
|
413
|
+
if (pattern.test(readmeText)) tags.add(tag);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return Array.from(tags).slice(0, 12);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Strip YAML frontmatter from markdown, returning just the body. */
|
|
421
|
+
function stripFrontmatter(md: string): string {
|
|
422
|
+
return md.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "").trim();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Parse allowed-tools from frontmatter (comma-separated, YAML array, or newline-separated). */
|
|
426
|
+
function parseAllowedTools(frontmatter: Record<string, string>): string[] | undefined {
|
|
427
|
+
const raw = frontmatter["allowed-tools"];
|
|
428
|
+
if (!raw) return undefined;
|
|
429
|
+
|
|
430
|
+
const tools = raw
|
|
431
|
+
.split(/[,\n]/)
|
|
432
|
+
.map((t) => t.replace(/^-\s*/, "").trim())
|
|
433
|
+
.filter(Boolean);
|
|
434
|
+
|
|
435
|
+
return tools.length > 0 ? tools : undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Compute SHA-256 content hash. */
|
|
439
|
+
export function contentHash(content: string): string {
|
|
440
|
+
return createHash("sha256").update(content).digest("hex");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Adapt a SKILL.md-only format (e.g., gstack) into a Stagent ProfileConfig.
|
|
445
|
+
* Reads SKILL.md body + README context for rich descriptions and tags.
|
|
446
|
+
*/
|
|
447
|
+
export function adaptSkillMdOnly(
|
|
448
|
+
skill: DiscoveredSkill,
|
|
449
|
+
skillMd: string,
|
|
450
|
+
repoMeta: RepoMeta,
|
|
451
|
+
readmeCtx: ReadmeContext | null = null,
|
|
452
|
+
): AdaptedProfile {
|
|
453
|
+
const fm = skill.frontmatter;
|
|
454
|
+
const dirName = skill.path.split("/").pop() ?? skill.name;
|
|
455
|
+
const id = slugify(fm.name ?? dirName);
|
|
456
|
+
const name = fm.name ?? dirName;
|
|
457
|
+
const body = stripFrontmatter(skillMd);
|
|
458
|
+
|
|
459
|
+
// Extract rich description from content + README
|
|
460
|
+
const repoReadmeSkillDesc = readmeCtx
|
|
461
|
+
? findSkillInRepoReadme(readmeCtx.repoReadme, name)
|
|
462
|
+
: null;
|
|
463
|
+
const description = extractDescription(fm, body, readmeCtx, name, repoReadmeSkillDesc);
|
|
464
|
+
|
|
465
|
+
// Extract semantic tags from content
|
|
466
|
+
const tags = extractTags(fm, body, dirName, readmeCtx);
|
|
467
|
+
|
|
468
|
+
// Inject description into SKILL.md frontmatter so registry picks it up
|
|
469
|
+
const enrichedSkillMd = ensureSkillMdDescription(skillMd, description);
|
|
470
|
+
|
|
471
|
+
const config: ProfileConfig = {
|
|
472
|
+
id,
|
|
473
|
+
name: name.charAt(0).toUpperCase() + name.slice(1),
|
|
474
|
+
version: fm.version ?? "1.0.0",
|
|
475
|
+
domain: inferDomain(description, name),
|
|
476
|
+
tags,
|
|
477
|
+
allowedTools: parseAllowedTools(fm),
|
|
478
|
+
author: repoMeta.owner,
|
|
479
|
+
source: `https://github.com/${repoMeta.owner}/${repoMeta.repo}/tree/${repoMeta.branch}/${skill.path}`,
|
|
480
|
+
importMeta: {
|
|
481
|
+
repoUrl: repoMeta.repoUrl,
|
|
482
|
+
repoOwner: repoMeta.owner,
|
|
483
|
+
repoName: repoMeta.repo,
|
|
484
|
+
branch: repoMeta.branch,
|
|
485
|
+
filePath: skill.path,
|
|
486
|
+
commitSha: repoMeta.commitSha,
|
|
487
|
+
contentHash: contentHash(skillMd),
|
|
488
|
+
importedAt: new Date().toISOString(),
|
|
489
|
+
sourceFormat: "skillmd-only",
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
return { config, skillMd: enrichedSkillMd, importMeta: config.importMeta! };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Adapt a Stagent-native format (profile.yaml + SKILL.md) from a remote repo.
|
|
498
|
+
* Enriches tags/description from SKILL.md body + README if the profile.yaml values are weak.
|
|
499
|
+
*/
|
|
500
|
+
export function adaptStagentNative(
|
|
501
|
+
skill: DiscoveredSkill,
|
|
502
|
+
skillMd: string,
|
|
503
|
+
profileYamlContent: string,
|
|
504
|
+
repoMeta: RepoMeta,
|
|
505
|
+
readmeCtx: ReadmeContext | null = null,
|
|
506
|
+
): AdaptedProfile {
|
|
507
|
+
const parsed = yaml.load(profileYamlContent) as Record<string, unknown>;
|
|
508
|
+
const result = ProfileConfigSchema.safeParse(parsed);
|
|
509
|
+
|
|
510
|
+
if (!result.success) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Invalid profile.yaml in ${skill.path}: ${result.error.issues.map((i) => i.message).join(", ")}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const config = result.data;
|
|
517
|
+
const body = stripFrontmatter(skillMd);
|
|
518
|
+
const dirName = skill.path.split("/").pop() ?? skill.name;
|
|
519
|
+
|
|
520
|
+
// Enrich tags if the profile has fewer than 3
|
|
521
|
+
if (config.tags.length < 3) {
|
|
522
|
+
const enrichedTags = extractTags(skill.frontmatter, body, dirName, readmeCtx);
|
|
523
|
+
// Merge — keep originals, add new ones
|
|
524
|
+
const merged = new Set([...config.tags, ...enrichedTags]);
|
|
525
|
+
config.tags = Array.from(merged).slice(0, 12);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Ensure SKILL.md frontmatter has a rich description
|
|
529
|
+
const repoReadmeSkillDesc = readmeCtx
|
|
530
|
+
? findSkillInRepoReadme(readmeCtx.repoReadme, config.name)
|
|
531
|
+
: null;
|
|
532
|
+
const richDescription = extractDescription(
|
|
533
|
+
skill.frontmatter, body, readmeCtx, config.name, repoReadmeSkillDesc
|
|
534
|
+
);
|
|
535
|
+
const enrichedSkillMd = ensureSkillMdDescription(skillMd, richDescription);
|
|
536
|
+
|
|
537
|
+
// Inject importMeta
|
|
538
|
+
config.importMeta = {
|
|
539
|
+
repoUrl: repoMeta.repoUrl,
|
|
540
|
+
repoOwner: repoMeta.owner,
|
|
541
|
+
repoName: repoMeta.repo,
|
|
542
|
+
branch: repoMeta.branch,
|
|
543
|
+
filePath: skill.path,
|
|
544
|
+
commitSha: repoMeta.commitSha,
|
|
545
|
+
contentHash: contentHash(skillMd),
|
|
546
|
+
importedAt: new Date().toISOString(),
|
|
547
|
+
sourceFormat: "stagent",
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// Set source URL if not already set
|
|
551
|
+
if (!config.source) {
|
|
552
|
+
config.source = `https://github.com/${repoMeta.owner}/${repoMeta.repo}/tree/${repoMeta.branch}/${skill.path}`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Set author if not already set
|
|
556
|
+
if (!config.author) {
|
|
557
|
+
config.author = repoMeta.owner;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { config, skillMd: enrichedSkillMd, importMeta: config.importMeta };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Ensure SKILL.md has a `description:` in its frontmatter.
|
|
565
|
+
* If frontmatter exists but has no description (or a weak one), inject one.
|
|
566
|
+
* If no frontmatter exists, prepend one with name + description.
|
|
567
|
+
*/
|
|
568
|
+
function ensureSkillMdDescription(skillMd: string, description: string): string {
|
|
569
|
+
const fmMatch = skillMd.match(/^(---\s*\n)([\s\S]*?)\n(---)/);
|
|
570
|
+
if (!fmMatch) {
|
|
571
|
+
// No frontmatter — prepend one
|
|
572
|
+
const name = description.split(/[.—:,]/, 1)[0].trim().slice(0, 40);
|
|
573
|
+
return `---\nname: ${name}\ndescription: ${description}\n---\n\n${skillMd}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const [fullMatch, open, body, close] = fmMatch;
|
|
577
|
+
const descLine = body.match(/^description:\s*(.*)$/m);
|
|
578
|
+
|
|
579
|
+
if (descLine && descLine[1].trim().length > 20) {
|
|
580
|
+
// Already has a rich description — don't touch
|
|
581
|
+
return skillMd;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (descLine) {
|
|
585
|
+
// Replace weak description
|
|
586
|
+
const newBody = body.replace(
|
|
587
|
+
/^description:\s*.*$/m,
|
|
588
|
+
`description: ${description}`
|
|
589
|
+
);
|
|
590
|
+
return skillMd.replace(fullMatch, `${open}${newBody}\n${close}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// No description line — add one
|
|
594
|
+
return skillMd.replace(fullMatch, `${open}${body}\ndescription: ${description}\n${close}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Re-extract description and tags for an already-imported profile during update.
|
|
599
|
+
* Used by apply-updates to refresh metadata when upstream SKILL.md changes.
|
|
600
|
+
*/
|
|
601
|
+
export function enrichProfileFromContent(
|
|
602
|
+
skillMd: string,
|
|
603
|
+
currentTags: string[],
|
|
604
|
+
name: string,
|
|
605
|
+
dirName: string,
|
|
606
|
+
readmeCtx: ReadmeContext | null = null,
|
|
607
|
+
): { enrichedSkillMd: string; tags: string[]; description: string } {
|
|
608
|
+
const fmMatch = skillMd.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
609
|
+
const fm: Record<string, string> = {};
|
|
610
|
+
if (fmMatch) {
|
|
611
|
+
for (const line of fmMatch[1].split("\n")) {
|
|
612
|
+
const colonIdx = line.indexOf(":");
|
|
613
|
+
if (colonIdx > 0) {
|
|
614
|
+
const key = line.slice(0, colonIdx).trim();
|
|
615
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
616
|
+
if (key && value) fm[key] = value;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const body = stripFrontmatter(skillMd);
|
|
622
|
+
const repoReadmeSkillDesc = readmeCtx
|
|
623
|
+
? findSkillInRepoReadme(readmeCtx.repoReadme, name)
|
|
624
|
+
: null;
|
|
625
|
+
const description = extractDescription(fm, body, readmeCtx, name, repoReadmeSkillDesc);
|
|
626
|
+
const newTags = extractTags(fm, body, dirName, readmeCtx);
|
|
627
|
+
const mergedTags = Array.from(new Set([...currentTags, ...newTags])).slice(0, 12);
|
|
628
|
+
const enrichedSkillMd = ensureSkillMdDescription(skillMd, description);
|
|
629
|
+
|
|
630
|
+
return { enrichedSkillMd, tags: mergedTags, description };
|
|
631
|
+
}
|