stagent 0.4.0 → 0.6.0
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 +67 -31
- package/dist/cli.js +151 -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 +53 -71
- 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 +77 -41
- package/docs/features/settings.md +134 -51
- 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 +79 -47
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -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 +12 -3
- package/src/app/api/channels/[id]/route.ts +103 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +109 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
- package/src/app/api/channels/inbound/telegram/route.ts +76 -0
- package/src/app/api/channels/route.ts +71 -0
- package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +112 -0
- package/src/app/api/documents/[id]/file/route.ts +4 -1
- 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/projects/[id]/route.ts +119 -9
- package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
- 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/browser-tools/route.ts +68 -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/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +12 -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 +280 -0
- package/src/components/chat/chat-input.tsx +90 -10
- package/src/components/chat/chat-message.tsx +9 -3
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +31 -5
- package/src/components/chat/screenshot-gallery.tsx +96 -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/monitoring/log-entry.tsx +61 -27
- 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/projects/project-detail.tsx +15 -2
- package/src/components/schedules/schedule-create-sheet.tsx +32 -330
- package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
- package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
- package/src/components/schedules/schedule-form.tsx +749 -0
- package/src/components/schedules/schedule-list.tsx +31 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/browser-tools-section.tsx +247 -0
- 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/runtime-timeout-section.tsx +4 -4
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/command-palette.tsx +1 -30
- package/src/components/shared/screenshot-lightbox.tsx +151 -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-caret-position.ts +104 -0
- package/src/hooks/use-chat-autocomplete.ts +288 -0
- 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__/browser-mcp.test.ts +175 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +6 -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 +174 -0
- package/src/lib/agents/claude-agent.ts +83 -198
- 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 +43 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/command-data.ts +50 -0
- package/src/lib/chat/context-builder.ts +147 -3
- package/src/lib/chat/engine.ts +182 -19
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/slash-commands.ts +191 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +209 -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 +43 -6
- 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 +22 -6
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +11 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +38 -4
- 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 +167 -0
- package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -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 +192 -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/screenshots/__tests__/persist.test.ts +104 -0
- package/src/lib/screenshots/persist.ts +114 -0
- 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/utils/stagent-paths.ts +4 -0
- 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
|
@@ -3,31 +3,57 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getArtifacts } from "./data";
|
|
6
|
-
import { evaluateRules, type ProfileSuggestion } from "./profile-rules";
|
|
6
|
+
import { evaluateRules, generateTier2Suggestions, type ProfileSuggestion } from "./profile-rules";
|
|
7
7
|
import { listProfiles, createProfile } from "@/lib/agents/profiles/registry";
|
|
8
8
|
import type { ProfileConfig } from "@/lib/validators/profile";
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const MIN_CURATED_CONFIDENCE = 0.6;
|
|
11
|
+
|
|
12
|
+
export interface TieredSuggestions {
|
|
13
|
+
curated: ProfileSuggestion[];
|
|
14
|
+
discovered: ProfileSuggestion[];
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Suggest profiles based on artifacts from a scan.
|
|
19
|
+
* Returns both curated (Tier 1) and discovered (Tier 2) suggestions.
|
|
14
20
|
* Filters out suggestions that match existing profiles.
|
|
15
21
|
*/
|
|
16
|
-
export function
|
|
22
|
+
export function suggestProfilesTiered(scanId: string): TieredSuggestions {
|
|
17
23
|
const artifacts = getArtifacts({ scanId });
|
|
18
|
-
const suggestions = evaluateRules(artifacts);
|
|
19
|
-
|
|
20
|
-
// Filter by minimum confidence
|
|
21
|
-
const confident = suggestions.filter((s) => s.confidence >= MIN_CONFIDENCE);
|
|
22
24
|
|
|
23
|
-
//
|
|
25
|
+
// Tier 1: Curated rules
|
|
26
|
+
const curatedRaw = evaluateRules(artifacts);
|
|
24
27
|
const existing = listProfiles();
|
|
25
28
|
const existingIds = new Set(existing.map((p) => p.id));
|
|
26
29
|
const existingNames = new Set(existing.map((p) => p.name.toLowerCase()));
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
(s) =>
|
|
31
|
+
const curated = curatedRaw
|
|
32
|
+
.filter((s) => s.confidence >= MIN_CURATED_CONFIDENCE)
|
|
33
|
+
.filter(
|
|
34
|
+
(s) => !existingIds.has(s.ruleId) && !existingNames.has(s.name.toLowerCase())
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Tier 2: Unlinked skill artifacts (those without linkedProfileId)
|
|
38
|
+
const unlinkedSkills = artifacts.filter(
|
|
39
|
+
(a) => a.category === "skill" && !a.linkedProfileId
|
|
40
|
+
);
|
|
41
|
+
const discovered = generateTier2Suggestions(unlinkedSkills).filter(
|
|
42
|
+
(s) =>
|
|
43
|
+
!existingIds.has(s.ruleId) &&
|
|
44
|
+
!existingIds.has(`env-${s.ruleId}`) &&
|
|
45
|
+
!existingNames.has(s.name.toLowerCase())
|
|
30
46
|
);
|
|
47
|
+
|
|
48
|
+
return { curated, discovered };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Suggest profiles based on artifacts from a scan (legacy flat API).
|
|
53
|
+
* Returns only curated (Tier 1) suggestions for backward compatibility.
|
|
54
|
+
*/
|
|
55
|
+
export function suggestProfiles(scanId: string): ProfileSuggestion[] {
|
|
56
|
+
return suggestProfilesTiered(scanId).curated;
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
/**
|
|
@@ -98,4 +124,8 @@ export function createProfileFromSuggestion(
|
|
|
98
124
|
}
|
|
99
125
|
|
|
100
126
|
createProfile(config, skillMd);
|
|
127
|
+
|
|
128
|
+
// Note: the created profile will have author "stagent-env" which,
|
|
129
|
+
// combined with the env- prefix on the ID, identifies it as environment-originated.
|
|
130
|
+
// The profile registry can infer origin from the author field.
|
|
101
131
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile-Artifact Linker
|
|
3
|
+
*
|
|
4
|
+
* Reconciles environment skill artifacts with profile registry entries.
|
|
5
|
+
* Runs after each scan to populate linkedProfileId on skill artifacts,
|
|
6
|
+
* enabling the UI to show which skills are already profiles and which
|
|
7
|
+
* are candidates for promotion.
|
|
8
|
+
*
|
|
9
|
+
* Matching strategy: directory basename under ~/.claude/skills/ is the
|
|
10
|
+
* shared key between both systems.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { db } from "@/lib/db";
|
|
14
|
+
import { environmentArtifacts } from "@/lib/db/schema";
|
|
15
|
+
import { eq, and, isNull } from "drizzle-orm";
|
|
16
|
+
import { listAllProfiles } from "@/lib/agents/profiles/registry";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
export interface LinkResult {
|
|
20
|
+
linked: number;
|
|
21
|
+
unlinked: number;
|
|
22
|
+
unlinkedArtifactIds: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Link skill artifacts from a scan to their corresponding profiles.
|
|
27
|
+
*
|
|
28
|
+
* For each skill artifact, extracts the directory basename from its absPath
|
|
29
|
+
* (e.g., ~/.claude/skills/code-reviewer/SKILL.md → "code-reviewer") and
|
|
30
|
+
* matches it against profile IDs in the registry.
|
|
31
|
+
*/
|
|
32
|
+
export function linkArtifactsToProfiles(
|
|
33
|
+
scanId: string,
|
|
34
|
+
projectDir?: string
|
|
35
|
+
): LinkResult {
|
|
36
|
+
// Get all skill artifacts from this scan
|
|
37
|
+
const skillArtifacts = db
|
|
38
|
+
.select()
|
|
39
|
+
.from(environmentArtifacts)
|
|
40
|
+
.where(
|
|
41
|
+
and(
|
|
42
|
+
eq(environmentArtifacts.scanId, scanId),
|
|
43
|
+
eq(environmentArtifacts.category, "skill")
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
.all();
|
|
47
|
+
|
|
48
|
+
if (skillArtifacts.length === 0) {
|
|
49
|
+
return { linked: 0, unlinked: 0, unlinkedArtifactIds: [] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build a set of known profile IDs
|
|
53
|
+
const profiles = listAllProfiles(projectDir);
|
|
54
|
+
const profileIds = new Set(profiles.map((p) => p.id));
|
|
55
|
+
|
|
56
|
+
let linked = 0;
|
|
57
|
+
const unlinkedArtifactIds: string[] = [];
|
|
58
|
+
|
|
59
|
+
// Match each skill artifact to a profile by directory basename
|
|
60
|
+
for (const artifact of skillArtifacts) {
|
|
61
|
+
const dirBasename = extractProfileId(artifact.absPath);
|
|
62
|
+
if (!dirBasename) {
|
|
63
|
+
unlinkedArtifactIds.push(artifact.id);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (profileIds.has(dirBasename)) {
|
|
68
|
+
// Link this artifact to the profile
|
|
69
|
+
db.update(environmentArtifacts)
|
|
70
|
+
.set({ linkedProfileId: dirBasename })
|
|
71
|
+
.where(eq(environmentArtifacts.id, artifact.id))
|
|
72
|
+
.run();
|
|
73
|
+
linked++;
|
|
74
|
+
} else {
|
|
75
|
+
unlinkedArtifactIds.push(artifact.id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
linked,
|
|
81
|
+
unlinked: unlinkedArtifactIds.length,
|
|
82
|
+
unlinkedArtifactIds,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract the profile ID from a skill artifact's absolute path.
|
|
88
|
+
*
|
|
89
|
+
* The scanner stores absPath as either:
|
|
90
|
+
* - The directory: ~/.claude/skills/code-reviewer
|
|
91
|
+
* - Or the file: ~/.claude/skills/code-reviewer/SKILL.md
|
|
92
|
+
*
|
|
93
|
+
* The profile ID is the skill directory basename ("code-reviewer").
|
|
94
|
+
*/
|
|
95
|
+
function extractProfileId(absPath: string): string | null {
|
|
96
|
+
// The absPath may point to the skill directory itself or a file within it.
|
|
97
|
+
// Use the basename of the path first — if it looks like a directory name
|
|
98
|
+
// (no extension), use it directly. Otherwise, use the parent directory.
|
|
99
|
+
const basename = path.basename(absPath);
|
|
100
|
+
|
|
101
|
+
// If basename has a file extension (e.g., "SKILL.md"), go up one level
|
|
102
|
+
if (basename.includes(".")) {
|
|
103
|
+
const parentBasename = path.basename(path.dirname(absPath));
|
|
104
|
+
if (parentBasename === "skills" || parentBasename === ".claude") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return parentBasename;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// basename is a directory name — skip if it's the skills root
|
|
111
|
+
if (basename === "skills" || basename === ".claude") {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return basename;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get all unlinked skill artifact IDs for a scan.
|
|
120
|
+
* Useful for the suggestion engine to generate Tier 2 suggestions.
|
|
121
|
+
*/
|
|
122
|
+
export function getUnlinkedSkillArtifacts(
|
|
123
|
+
scanId: string
|
|
124
|
+
): Array<{ id: string; name: string; absPath: string; contentHash: string; preview: string | null; metadata: string | null }> {
|
|
125
|
+
return db
|
|
126
|
+
.select({
|
|
127
|
+
id: environmentArtifacts.id,
|
|
128
|
+
name: environmentArtifacts.name,
|
|
129
|
+
absPath: environmentArtifacts.absPath,
|
|
130
|
+
contentHash: environmentArtifacts.contentHash,
|
|
131
|
+
preview: environmentArtifacts.preview,
|
|
132
|
+
metadata: environmentArtifacts.metadata,
|
|
133
|
+
})
|
|
134
|
+
.from(environmentArtifacts)
|
|
135
|
+
.where(
|
|
136
|
+
and(
|
|
137
|
+
eq(environmentArtifacts.scanId, scanId),
|
|
138
|
+
eq(environmentArtifacts.category, "skill"),
|
|
139
|
+
isNull(environmentArtifacts.linkedProfileId)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
.all();
|
|
143
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Profile suggestion rules engine.
|
|
3
3
|
* Maps artifact clusters to agent profile suggestions.
|
|
4
|
+
*
|
|
5
|
+
* Two-tier system:
|
|
6
|
+
* - Tier 1 (Curated): 6 hardcoded rules with high confidence (0.65-1.0)
|
|
7
|
+
* - Tier 2 (Discovered): Any unlinked skill artifact with valid SKILL.md
|
|
8
|
+
* frontmatter becomes a generic suggestion at confidence 0.5
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
import type { EnvironmentArtifactRow } from "@/lib/db/schema";
|
|
@@ -22,11 +27,14 @@ export interface ProfileRule {
|
|
|
22
27
|
tags: string[];
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
export type SuggestionTier = "curated" | "discovered";
|
|
31
|
+
|
|
25
32
|
export interface ProfileSuggestion {
|
|
26
33
|
ruleId: string;
|
|
27
34
|
name: string;
|
|
28
35
|
description: string;
|
|
29
36
|
confidence: number;
|
|
37
|
+
tier: SuggestionTier;
|
|
30
38
|
matchedArtifacts: Array<{ id: string; name: string; category: string }>;
|
|
31
39
|
suggestedTools: string[];
|
|
32
40
|
systemPrompt: string;
|
|
@@ -190,6 +198,7 @@ export function evaluateRules(
|
|
|
190
198
|
name: rule.name,
|
|
191
199
|
description: rule.description,
|
|
192
200
|
confidence,
|
|
201
|
+
tier: "curated",
|
|
193
202
|
matchedArtifacts: matched,
|
|
194
203
|
suggestedTools: rule.suggestedTools,
|
|
195
204
|
systemPrompt: rule.systemPromptTemplate,
|
|
@@ -199,3 +208,90 @@ export function evaluateRules(
|
|
|
199
208
|
|
|
200
209
|
return suggestions.sort((a, b) => b.confidence - a.confidence);
|
|
201
210
|
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Tier 2: Auto-discovered suggestions from unlinked skill artifacts
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
const TIER2_CONFIDENCE = 0.5;
|
|
217
|
+
|
|
218
|
+
/** Parse YAML frontmatter from SKILL.md preview content. */
|
|
219
|
+
function parseFrontmatter(preview: string | null): { name?: string; description?: string } {
|
|
220
|
+
if (!preview) return {};
|
|
221
|
+
const match = preview.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
222
|
+
if (!match) return {};
|
|
223
|
+
|
|
224
|
+
const result: { name?: string; description?: string } = {};
|
|
225
|
+
const lines = match[1].split("\n");
|
|
226
|
+
for (const line of lines) {
|
|
227
|
+
const nameMatch = line.match(/^name:\s*(.+)/);
|
|
228
|
+
if (nameMatch) result.name = nameMatch[1].trim();
|
|
229
|
+
const descMatch = line.match(/^description:\s*(.+)/);
|
|
230
|
+
if (descMatch) result.description = descMatch[1].trim();
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Generate Tier 2 (discovered) suggestions from unlinked skill artifacts.
|
|
237
|
+
*
|
|
238
|
+
* Any skill artifact that:
|
|
239
|
+
* 1. Has no linked profile (linkedProfileId is null)
|
|
240
|
+
* 2. Has parseable SKILL.md frontmatter with at least a name
|
|
241
|
+
*
|
|
242
|
+
* becomes a suggestion with confidence 0.5 (below the Tier 1 minimum of 0.6).
|
|
243
|
+
*/
|
|
244
|
+
export function generateTier2Suggestions(
|
|
245
|
+
unlinkedArtifacts: EnvironmentArtifactRow[]
|
|
246
|
+
): ProfileSuggestion[] {
|
|
247
|
+
const suggestions: ProfileSuggestion[] = [];
|
|
248
|
+
|
|
249
|
+
for (const artifact of unlinkedArtifacts) {
|
|
250
|
+
if (artifact.category !== "skill") continue;
|
|
251
|
+
|
|
252
|
+
// Try to extract metadata from the artifact's preview or metadata field
|
|
253
|
+
let name: string | undefined;
|
|
254
|
+
let description: string | undefined;
|
|
255
|
+
|
|
256
|
+
// Parse from metadata (JSON) if available
|
|
257
|
+
if (artifact.metadata) {
|
|
258
|
+
try {
|
|
259
|
+
const meta = JSON.parse(artifact.metadata);
|
|
260
|
+
name = meta.name;
|
|
261
|
+
description = meta.description;
|
|
262
|
+
} catch {
|
|
263
|
+
// Fall through to preview parsing
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Fall back to preview (first 200 chars of SKILL.md) frontmatter parsing
|
|
268
|
+
if (!name) {
|
|
269
|
+
const fm = parseFrontmatter(artifact.preview);
|
|
270
|
+
name = fm.name;
|
|
271
|
+
description = fm.description;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// If we still don't have a name, use the artifact name (directory basename)
|
|
275
|
+
if (!name) {
|
|
276
|
+
name = artifact.name
|
|
277
|
+
.replace(/-/g, " ")
|
|
278
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
suggestions.push({
|
|
282
|
+
ruleId: `discovered-${artifact.name}`,
|
|
283
|
+
name,
|
|
284
|
+
description: description ?? `Discovered skill: ${artifact.name}`,
|
|
285
|
+
confidence: TIER2_CONFIDENCE,
|
|
286
|
+
tier: "discovered",
|
|
287
|
+
matchedArtifacts: [
|
|
288
|
+
{ id: artifact.id, name: artifact.name, category: artifact.category },
|
|
289
|
+
],
|
|
290
|
+
suggestedTools: ["Read", "Grep", "Glob", "Bash"],
|
|
291
|
+
systemPrompt: description ?? `You are a ${name} specialist.`,
|
|
292
|
+
tags: artifact.name.split("-").filter((t) => t.length > 2),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return suggestions.sort((a, b) => a.name.localeCompare(b.name));
|
|
297
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deduplication engine for profile import.
|
|
3
|
+
* Three-tier matching: exact ID, name match, content similarity.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ProfileConfig } from "@/lib/validators/profile";
|
|
7
|
+
import type { AgentProfile } from "@/lib/agents/profiles/types";
|
|
8
|
+
|
|
9
|
+
export interface DedupResult {
|
|
10
|
+
candidate: ProfileConfig;
|
|
11
|
+
candidateSkillMd: string;
|
|
12
|
+
status: "new" | "exact-match" | "near-match";
|
|
13
|
+
matchedProfile?: AgentProfile;
|
|
14
|
+
matchReason?: string;
|
|
15
|
+
similarity?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Common stop words to exclude from keyword extraction. */
|
|
19
|
+
const STOP_WORDS = new Set([
|
|
20
|
+
"the", "and", "for", "are", "but", "not", "you", "all", "can", "had",
|
|
21
|
+
"her", "was", "one", "our", "out", "has", "have", "that", "this", "with",
|
|
22
|
+
"from", "they", "been", "will", "each", "make", "like", "into", "them",
|
|
23
|
+
"some", "when", "what", "your", "should", "would", "could", "about",
|
|
24
|
+
"which", "their", "other", "than", "then", "more", "also", "been",
|
|
25
|
+
"only", "must", "does", "here", "just", "over", "such", "after",
|
|
26
|
+
"before", "between", "through", "where", "these", "those", "being",
|
|
27
|
+
"using", "ensure", "every", "following", "include",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/** Extract meaningful keywords from text. */
|
|
31
|
+
function extractKeywords(text: string, limit = 20): Set<string> {
|
|
32
|
+
const words = text
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
35
|
+
.split(/\s+/)
|
|
36
|
+
.filter((w) => w.length > 3 && w.length < 30 && !STOP_WORDS.has(w));
|
|
37
|
+
|
|
38
|
+
// Count frequency
|
|
39
|
+
const freq = new Map<string, number>();
|
|
40
|
+
for (const word of words) {
|
|
41
|
+
freq.set(word, (freq.get(word) ?? 0) + 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Sort by frequency, take top N
|
|
45
|
+
const sorted = Array.from(freq.entries())
|
|
46
|
+
.sort((a, b) => b[1] - a[1])
|
|
47
|
+
.slice(0, limit)
|
|
48
|
+
.map(([word]) => word);
|
|
49
|
+
|
|
50
|
+
return new Set(sorted);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Jaccard similarity between two sets. */
|
|
54
|
+
function jaccard(a: Set<string>, b: Set<string>): number {
|
|
55
|
+
if (a.size === 0 && b.size === 0) return 0;
|
|
56
|
+
let intersection = 0;
|
|
57
|
+
for (const item of a) {
|
|
58
|
+
if (b.has(item)) intersection++;
|
|
59
|
+
}
|
|
60
|
+
const union = a.size + b.size - intersection;
|
|
61
|
+
return union === 0 ? 0 : intersection / union;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Tag overlap ratio (how many of candidate's tags match existing). */
|
|
65
|
+
function tagOverlap(candidateTags: string[], existingTags: string[]): number {
|
|
66
|
+
if (candidateTags.length === 0) return 0;
|
|
67
|
+
const existingSet = new Set(existingTags.map((t) => t.toLowerCase()));
|
|
68
|
+
const matches = candidateTags.filter((t) => existingSet.has(t.toLowerCase()));
|
|
69
|
+
return matches.length / candidateTags.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check a batch of candidate profiles against all existing profiles for duplicates.
|
|
74
|
+
*/
|
|
75
|
+
export function checkDuplicates(
|
|
76
|
+
candidates: Array<{ config: ProfileConfig; skillMd: string }>,
|
|
77
|
+
existingProfiles: AgentProfile[]
|
|
78
|
+
): DedupResult[] {
|
|
79
|
+
return candidates.map(({ config, skillMd }) => {
|
|
80
|
+
// Tier 1: Exact ID match
|
|
81
|
+
const idMatch = existingProfiles.find(
|
|
82
|
+
(p) => p.id === config.id
|
|
83
|
+
);
|
|
84
|
+
if (idMatch) {
|
|
85
|
+
return {
|
|
86
|
+
candidate: config,
|
|
87
|
+
candidateSkillMd: skillMd,
|
|
88
|
+
status: "exact-match" as const,
|
|
89
|
+
matchedProfile: idMatch,
|
|
90
|
+
matchReason: `Same ID: "${config.id}"`,
|
|
91
|
+
similarity: 1.0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Tier 2: Name match (case-insensitive)
|
|
96
|
+
const nameMatch = existingProfiles.find(
|
|
97
|
+
(p) => p.name.toLowerCase() === config.name.toLowerCase()
|
|
98
|
+
);
|
|
99
|
+
if (nameMatch) {
|
|
100
|
+
return {
|
|
101
|
+
candidate: config,
|
|
102
|
+
candidateSkillMd: skillMd,
|
|
103
|
+
status: "exact-match" as const,
|
|
104
|
+
matchedProfile: nameMatch,
|
|
105
|
+
matchReason: `Same name: "${config.name}"`,
|
|
106
|
+
similarity: 1.0,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Tier 3: Content similarity via keyword Jaccard + tag overlap
|
|
111
|
+
const candidateKeywords = extractKeywords(skillMd);
|
|
112
|
+
let bestSimilarity = 0;
|
|
113
|
+
let bestMatch: AgentProfile | undefined;
|
|
114
|
+
|
|
115
|
+
for (const existing of existingProfiles) {
|
|
116
|
+
const existingKeywords = extractKeywords(existing.skillMd);
|
|
117
|
+
let similarity = jaccard(candidateKeywords, existingKeywords);
|
|
118
|
+
|
|
119
|
+
// Boost by tag overlap
|
|
120
|
+
const overlap = tagOverlap(config.tags, existing.tags);
|
|
121
|
+
if (overlap > 0.5) {
|
|
122
|
+
similarity = Math.min(1.0, similarity + 0.1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (similarity > bestSimilarity) {
|
|
126
|
+
bestSimilarity = similarity;
|
|
127
|
+
bestMatch = existing;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (bestSimilarity > 0.6 && bestMatch) {
|
|
132
|
+
return {
|
|
133
|
+
candidate: config,
|
|
134
|
+
candidateSkillMd: skillMd,
|
|
135
|
+
status: "near-match" as const,
|
|
136
|
+
matchedProfile: bestMatch,
|
|
137
|
+
matchReason: `Similar content to "${bestMatch.name}" (${Math.round(bestSimilarity * 100)}%)`,
|
|
138
|
+
similarity: bestSimilarity,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// No match found
|
|
143
|
+
return {
|
|
144
|
+
candidate: config,
|
|
145
|
+
candidateSkillMd: skillMd,
|
|
146
|
+
status: "new" as const,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|