@jenga-ai/agent 1.0.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.
Files changed (177) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +340 -0
  3. package/agents/ai_engineer.md +113 -0
  4. package/agents/developer.md +236 -0
  5. package/agents/scrum-master.md +349 -0
  6. package/agents/scrutiny-agent.md +137 -0
  7. package/agents/solution-assessor.md +185 -0
  8. package/agents/tester.md +339 -0
  9. package/bin/jenga.js +70 -0
  10. package/hooks/copilot_session_end.sh +29 -0
  11. package/hooks/on_session_end.sh +238 -0
  12. package/hooks/prompt_router.sh +11 -0
  13. package/hooks/prompt_router_helper.js +52 -0
  14. package/hooks/session_end_helper.js +29 -0
  15. package/hooks/session_end_watcher.sh +24 -0
  16. package/lib/commands/attach.js +47 -0
  17. package/lib/commands/init.js +207 -0
  18. package/lib/commands/start.js +16 -0
  19. package/lib/commands/status.js +53 -0
  20. package/lib/config-schema.js +72 -0
  21. package/lib/inject-settings.js +61 -0
  22. package/lib/mirror.js +244 -0
  23. package/lib/resolve-project-dir.sh +47 -0
  24. package/mcp/execute-ticket/index.js +10 -0
  25. package/mcp/execute-ticket/package.json +5 -0
  26. package/mcp/help/index.js +79 -0
  27. package/mcp/help/package.json +14 -0
  28. package/mcp/router/README.md +19 -0
  29. package/mcp/router/embedder.js +23 -0
  30. package/mcp/router/index.js +204 -0
  31. package/mcp/router/matcher.js +87 -0
  32. package/mcp/router/package-lock.json +1048 -0
  33. package/mcp/router/package.json +11 -0
  34. package/mcp/router/skill-index.js +104 -0
  35. package/package.json +47 -0
  36. package/scripts/board_resolver.sh +46 -0
  37. package/scripts/e25_s01_extract_board_graph.py +292 -0
  38. package/scripts/e25_s01_generate_synthetic_board.py +90 -0
  39. package/scripts/measurement-10x.json +50 -0
  40. package/scripts/measurement-10x.txt +4 -0
  41. package/scripts/measurement-real.json +50 -0
  42. package/scripts/measurement-real.txt +4 -0
  43. package/scripts/postinstall.js +165 -0
  44. package/scripts/todo_cleanup.sh +22 -0
  45. package/scripts/todo_manager.sh +86 -0
  46. package/scripts/validate-board.sh +190 -0
  47. package/scripts/validate-story-format.sh +53 -0
  48. package/skills/brainstorm/SKILL.md +47 -0
  49. package/skills/btw/SKILL.md +42 -0
  50. package/skills/commit/SKILL.md +29 -0
  51. package/skills/commit/assets/user_instructions_template.md +22 -0
  52. package/skills/continue/SKILL.md +29 -0
  53. package/skills/convert/SKILL.md +124 -0
  54. package/skills/convert/convert_cli.py +235 -0
  55. package/skills/convert/tests/sample.csv +4 -0
  56. package/skills/convert/tests/sample.json +5 -0
  57. package/skills/convert/tests/sample.jsonl +3 -0
  58. package/skills/convert/tests/sample.yaml +18 -0
  59. package/skills/convert/tests/sample_obj.csv +2 -0
  60. package/skills/convert/tests/sample_obj.json +9 -0
  61. package/skills/deep-dive/SKILL.md +167 -0
  62. package/skills/do/SKILL.md +88 -0
  63. package/skills/do/assets/sender_template.json +12 -0
  64. package/skills/doc/SKILL.md +314 -0
  65. package/skills/doc/assets/path-objectives.yaml +38 -0
  66. package/skills/doc-sync/SKILL.md +167 -0
  67. package/skills/doc-sync/assets/default_excludes.txt +21 -0
  68. package/skills/doc-sync/assets/doc_targets.md +14 -0
  69. package/skills/dooo/SKILL.md +60 -0
  70. package/skills/error/SKILL.md +29 -0
  71. package/skills/evaluate/SKILL.md +45 -0
  72. package/skills/evaluate/assets/evaluation_invokation_template.yml +3 -0
  73. package/skills/evaluate/assets/evaluation_rapport_template.md +24 -0
  74. package/skills/examplify/SKILL.md +42 -0
  75. package/skills/help/SKILL.md +36 -0
  76. package/skills/improve/SKILL.md +55 -0
  77. package/skills/index/scripts/board-index +4 -0
  78. package/skills/index/scripts/board_index.py +615 -0
  79. package/skills/index/scripts/smoke_test.sh +86 -0
  80. package/skills/init/SKILL.md +44 -0
  81. package/skills/init/assets/.gitignore_template +15 -0
  82. package/skills/init/assets/PROJECT_SUMMARY_template.md +13 -0
  83. package/skills/init/assets/directory_structure.txt +13 -0
  84. package/skills/init/assets/test-config_template.json +4 -0
  85. package/skills/init/assets/workflow_template.json +30 -0
  86. package/skills/init/scripts/init.sh +48 -0
  87. package/skills/jbp/SKILL.md +25 -0
  88. package/skills/jenga/SKILL.md +68 -0
  89. package/skills/lgtm/SKILL.md +21 -0
  90. package/skills/mirror-public/SKILL.md +237 -0
  91. package/skills/mirror-public/assets/config.json +5 -0
  92. package/skills/mirror-public/scripts/mirror.sh +374 -0
  93. package/skills/pi-plan/SKILL.md +62 -0
  94. package/skills/pi-plan/assets/epic.json +7 -0
  95. package/skills/pi-plan/assets/story_template.md +18 -0
  96. package/skills/proceed/SKILL.md +29 -0
  97. package/skills/publish/SKILL.md +351 -0
  98. package/skills/publish/adapters/droplet.md +200 -0
  99. package/skills/publish/adapters/mobile-ios.md +114 -0
  100. package/skills/publish/adapters/npm-ci.md +223 -0
  101. package/skills/publish/adapters/npm.md +121 -0
  102. package/skills/publish/assets/ExportOptions.plist.template +19 -0
  103. package/skills/publish/assets/ci-contract.md +111 -0
  104. package/skills/publish/assets/ownership-matrix.md +17 -0
  105. package/skills/publish/assets/publish.example.json +85 -0
  106. package/skills/publish/assets/publish.example.npm-ci.json +40 -0
  107. package/skills/publish/assets/publish.example.npm.json +41 -0
  108. package/skills/publish/assets/secrets-guide.md +104 -0
  109. package/skills/publish/schemas/fixtures/npm-ci-minimal.json +17 -0
  110. package/skills/publish/schemas/fixtures/npm-ci-with-empty-secrets.json +18 -0
  111. package/skills/publish/schemas/fixtures/npm-ci-with-workflow-path.json +18 -0
  112. package/skills/publish/schemas/publish.schema.json +428 -0
  113. package/skills/publish/scripts/check_target_config.sh +96 -0
  114. package/skills/publish/scripts/droplet_pipeline.sh +208 -0
  115. package/skills/publish/scripts/generate_release_notes.sh +200 -0
  116. package/skills/publish/scripts/ios_pipeline.sh +486 -0
  117. package/skills/publish/scripts/npm_ci_pipeline.sh +225 -0
  118. package/skills/publish/scripts/npm_pipeline.sh +249 -0
  119. package/skills/publish/scripts/publish_common.sh +253 -0
  120. package/skills/publish/scripts/publish_deploy.sh +538 -0
  121. package/skills/publish/scripts/reconcile_tags.sh +135 -0
  122. package/skills/publish/scripts/run_gates.sh +616 -0
  123. package/skills/publish/scripts/setup_wizard.sh +394 -0
  124. package/skills/publish/scripts/show_history.sh +95 -0
  125. package/skills/publish/scripts/suggest_semver_bump.sh +105 -0
  126. package/skills/publish/scripts/validate_config.sh +163 -0
  127. package/skills/publish/scripts/validate_droplet_env.sh +45 -0
  128. package/skills/publish/scripts/validate_ios_env.sh +68 -0
  129. package/skills/publish/scripts/validate_npm_ci_env.sh +71 -0
  130. package/skills/publish/scripts/validate_npm_env.sh +22 -0
  131. package/skills/publish/scripts/write_ledger_entry.sh +126 -0
  132. package/skills/publish/wizards/droplet.md +275 -0
  133. package/skills/publish/wizards/mobile-ios.md +157 -0
  134. package/skills/publish/wizards/npm-ci.md +240 -0
  135. package/skills/publish/wizards/npm.md +224 -0
  136. package/skills/reconcile/SKILL.md +93 -0
  137. package/skills/reconcile/assets/report_format.md +44 -0
  138. package/skills/reconcile-origin/SKILL.md +75 -0
  139. package/skills/reconcile-origin/scripts/reconcile-origin.sh +372 -0
  140. package/skills/redo/SKILL.md +70 -0
  141. package/skills/route/SKILL.md +180 -0
  142. package/skills/self-sync/SKILL.md +73 -0
  143. package/skills/self-sync/scripts/run.js +136 -0
  144. package/skills/skillify/SKILL.md +68 -0
  145. package/skills/skillify/assets/init-new/SKILL.md +35 -0
  146. package/skills/skillify/assets/init-new/assets/.gitignore_template +15 -0
  147. package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +13 -0
  148. package/skills/skillify/assets/init-new/assets/directory_structure.txt +10 -0
  149. package/skills/skillify/assets/init-new/assets/test-config_template.json +4 -0
  150. package/skills/skillify/assets/init-new/assets/workflow_template.json +17 -0
  151. package/skills/skillify/assets/init-new/scripts/init.sh +48 -0
  152. package/skills/skillify/assets/init-old/SKILL.md +124 -0
  153. package/skills/spinoff/SKILL.md +48 -0
  154. package/skills/status/SKILL.md +33 -0
  155. package/skills/status/assets/output_format.md +41 -0
  156. package/skills/todo/SKILL.md +46 -0
  157. package/skills/todo/assets/todo_handoff_template.md +22 -0
  158. package/skills/todo/assets/todo_template.md +3 -0
  159. package/skills/train/SKILL.md +116 -0
  160. package/skills/train/assets/dashboard-templates/classifiers.html +106 -0
  161. package/skills/train/assets/dashboard-templates/nlp.html +102 -0
  162. package/skills/train/assets/dashboard-templates/transformers.html +98 -0
  163. package/skills/train/assets/results-parsers/__init__.py +9 -0
  164. package/skills/train/assets/results-parsers/classifiers.py +84 -0
  165. package/skills/train/assets/results-parsers/nlp.py +88 -0
  166. package/skills/train/assets/results-parsers/reporter.py +154 -0
  167. package/skills/train/assets/results-parsers/transformers.py +120 -0
  168. package/skills/train/train_cli.py +786 -0
  169. package/templates/EXECUTION_PLAN_TEMPLATE.md +43 -0
  170. package/templates/EXECUTION_SUMMARY_TEMPLATE.md +50 -0
  171. package/templates/JENGA_CONFIG_TEMPLATE.json +23 -0
  172. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +88 -0
  173. package/templates/SCRUM_BOARD_SCHEMA.md +311 -0
  174. package/templates/SKILL.md +16 -0
  175. package/templates/SKILL_TEMPLATE.md +28 -0
  176. package/templates/USER_INSTRUCTIONS_TEMPLATE.md +22 -0
  177. package/templates/copilot-instructions.md.tpl +55 -0
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bash
2
+ # lib/resolve-project-dir.sh
3
+ # Sourced helper: exports JENGA_PROJECT_DIR, JENGA_AGENT_TYPE, JENGA_SESSION_ID
4
+ # by probing known agent environment variables in priority order.
5
+ # Idempotent — no-op if JENGA_PROJECT_DIR is already set.
6
+
7
+ if [ -n "${JENGA_PROJECT_DIR:-}" ]; then
8
+ return 0 2>/dev/null || true
9
+ fi
10
+
11
+ # --- JENGA_PROJECT_DIR ---
12
+ if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
13
+ JENGA_PROJECT_DIR="$CLAUDE_PROJECT_DIR"
14
+ elif [ -n "${COPILOT_WORKSPACE_FOLDER:-}" ]; then
15
+ JENGA_PROJECT_DIR="$COPILOT_WORKSPACE_FOLDER"
16
+ else
17
+ _git_root="$(git rev-parse --show-toplevel 2>/dev/null)"
18
+ if [ -n "$_git_root" ]; then
19
+ JENGA_PROJECT_DIR="$_git_root"
20
+ else
21
+ JENGA_PROJECT_DIR="$(pwd)"
22
+ fi
23
+ unset _git_root
24
+ fi
25
+ export JENGA_PROJECT_DIR
26
+
27
+ # --- JENGA_AGENT_TYPE ---
28
+ if [ -z "${JENGA_AGENT_TYPE:-}" ]; then
29
+ if [ -n "${CLAUDE_AGENT_TYPE:-}" ]; then
30
+ JENGA_AGENT_TYPE="$CLAUDE_AGENT_TYPE"
31
+ else
32
+ JENGA_AGENT_TYPE="generic"
33
+ fi
34
+ export JENGA_AGENT_TYPE
35
+ fi
36
+
37
+ # --- JENGA_SESSION_ID ---
38
+ if [ -z "${JENGA_SESSION_ID:-}" ]; then
39
+ if [ -n "${CLAUDE_SESSION_ID:-}" ]; then
40
+ JENGA_SESSION_ID="$CLAUDE_SESSION_ID"
41
+ elif command -v uuidgen >/dev/null 2>&1; then
42
+ JENGA_SESSION_ID="$(uuidgen)"
43
+ else
44
+ JENGA_SESSION_ID="session-$(date +%s)-$$"
45
+ fi
46
+ export JENGA_SESSION_ID
47
+ fi
@@ -0,0 +1,10 @@
1
+
2
+ /*** TODO
3
+ * Params: <Task identifier>
4
+ *
5
+ * Purpose:
6
+ * Carry out a specific task as specified by the task found in the "project/" directory
7
+ * - Create a subagent
8
+ * - Create git worktree based on task number and name
9
+ * - Name the session same as the worktree
10
+ ***/
@@ -0,0 +1,5 @@
1
+ /** TODO
2
+ *
3
+ * Describe the related MCP service
4
+ *
5
+ **/
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { existsSync, readdirSync, statSync } from "fs";
6
+ import { join, resolve } from "path";
7
+
8
+ const server = new McpServer({
9
+ name: "help",
10
+ version: "1.0.0",
11
+ });
12
+
13
+ server.tool(
14
+ "help",
15
+ "List all available agent skills by scanning the .claude/skills or .agents/skills directory in the given path.",
16
+ {
17
+ path: z
18
+ .string()
19
+ .optional()
20
+ .describe(
21
+ "Absolute or relative path to the project root to scan. Defaults to the current working directory."
22
+ ),
23
+ },
24
+ async ({ path: inputPath }) => {
25
+ const root = inputPath ? resolve(inputPath) : process.cwd();
26
+ // The jenga-agent postinstall mirrors skills/ into both .claude/ (Claude Code)
27
+ // and .agents/ (Copilot / custom agents). Prefer .claude/; fall back to .agents/.
28
+ const candidates = [
29
+ join(root, ".claude", "skills"),
30
+ join(root, ".agents", "skills"),
31
+ ];
32
+ const skillsDir = candidates.find(existsSync);
33
+
34
+ if (!skillsDir) {
35
+ return {
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text: `No skills found. Looked in:\n${candidates.map(p => ` - ${p}`).join("\n")}`,
40
+ },
41
+ ],
42
+ };
43
+ }
44
+
45
+ const entries = readdirSync(skillsDir);
46
+ const folders = entries.filter((entry) => {
47
+ try {
48
+ return statSync(join(skillsDir, entry)).isDirectory();
49
+ } catch {
50
+ return false;
51
+ }
52
+ });
53
+
54
+ if (folders.length === 0) {
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text",
59
+ text: `The skills directory exists at \`${skillsDir}\` but contains no skill folders.`,
60
+ },
61
+ ],
62
+ };
63
+ }
64
+
65
+ const list = folders.map((name) => `- ${name}`).join("\n");
66
+
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: `Available skills in \`${skillsDir}\`:\n\n${list}`,
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ );
77
+
78
+ const transport = new StdioServerTransport();
79
+ await server.connect(transport);
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "help-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that lists available agent skills from .agents/skills",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "start": "node index.js"
9
+ },
10
+ "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^1.10.1",
12
+ "zod": "^3.23.8"
13
+ }
14
+ }
@@ -0,0 +1,19 @@
1
+ # jenga-router
2
+
3
+ A stdio MCP server that routes incoming prompts to the appropriate Jenga skill.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ node mcp/router/index.js
9
+ ```
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `ping` | Health check — returns `{ ok: true, uptime: <ms> }` |
16
+
17
+ ## Protocol
18
+
19
+ Communicates over stdin/stdout using the Model Context Protocol (MCP) with `StdioServerTransport`.
@@ -0,0 +1,23 @@
1
+ import { pipeline } from "@xenova/transformers";
2
+
3
+ let _pipe = null;
4
+
5
+ /**
6
+ * Loads the feature-extraction pipeline for Xenova/all-MiniLM-L6-v2.
7
+ * Safe to call multiple times — no-op if already loaded.
8
+ */
9
+ export async function warmUp() {
10
+ if (_pipe) return;
11
+ const t = Date.now();
12
+ _pipe = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
13
+ process.stderr.write(`[jenga-router] Model warmed up in ${Date.now() - t}ms\n`);
14
+ }
15
+
16
+ /**
17
+ * Embeds text using the loaded pipeline.
18
+ * Returns a Float32Array of length 384.
19
+ */
20
+ export async function embed(text) {
21
+ const output = await _pipe(text, { pooling: "mean", normalize: true });
22
+ return output.data;
23
+ }
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { writeFileSync, unlinkSync, existsSync, readFileSync, mkdirSync } from "fs";
6
+ import { join, dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { buildSkillIndex } from "./skill-index.js";
9
+ import { findBestMatch } from "./matcher.js";
10
+ import { warmUp, embed } from "./embedder.js";
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const projectRoot = join(__dirname, "..", "..");
14
+ const jengaDir = join(process.cwd(), ".jenga");
15
+ const pidFile = join(jengaDir, "router.pid");
16
+ const sessionFile = join(jengaDir, "session.json");
17
+ const startTime = Date.now();
18
+
19
+ // Load config (optional — defaults used if absent)
20
+ let config = {
21
+ skillsPath: "skills/",
22
+ matchThreshold: 0.75,
23
+ sessionTimeout: 300000,
24
+ agentTarget: "claude",
25
+ };
26
+ const configPath = join(projectRoot, "jenga.cli.json");
27
+ if (existsSync(configPath)) {
28
+ try {
29
+ config = { ...config, ...JSON.parse(readFileSync(configPath, "utf8")) };
30
+ } catch (e) {
31
+ process.stderr.write(`Warning: failed to parse jenga.cli.json: ${e.message}\n`);
32
+ }
33
+ }
34
+
35
+ // Build skill index (with semantic embeddings)
36
+ let skillIndex = [];
37
+ if (config.skillsPath) {
38
+ await warmUp();
39
+ skillIndex = await buildSkillIndex(join(projectRoot, config.skillsPath), { embed });
40
+ }
41
+
42
+ // Write PID file
43
+ mkdirSync(jengaDir, { recursive: true });
44
+ writeFileSync(pidFile, String(process.pid), "utf8");
45
+
46
+ // Graceful shutdown
47
+ function shutdown() {
48
+ try { unlinkSync(pidFile); } catch {}
49
+ persistSession({ skill: null, startedAt: null });
50
+ process.exit(0);
51
+ }
52
+ process.on("SIGTERM", shutdown);
53
+ process.on("SIGINT", shutdown);
54
+
55
+ let activeSession = { skill: null, startedAt: null };
56
+ let sessionTimer = null;
57
+
58
+ function persistSession(state) {
59
+ try {
60
+ writeFileSync(sessionFile, JSON.stringify(state), "utf8");
61
+ } catch (e) {
62
+ process.stderr.write(`[jenga-router] Failed to write .session.json: ${e.message}\n`);
63
+ }
64
+ }
65
+
66
+ const server = new McpServer({ name: "jenga-router", version: "1.0.0" });
67
+
68
+ server.tool("ping", "Health check — returns ok and uptime in seconds.", {}, async () => {
69
+ try {
70
+ return {
71
+ content: [{ type: "text", text: JSON.stringify({ ok: true, uptime: Math.floor((Date.now() - startTime) / 1000), skill_count: skillIndex.length }) }],
72
+ };
73
+ } catch (e) {
74
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
75
+ }
76
+ });
77
+
78
+ server.tool(
79
+ "route_prompt",
80
+ "Route a prompt through the skill matching pipeline. Returns { action: 'invoke'|'passthrough', transformed: string, skill: string|null, confidence: number }.",
81
+ { text: z.string().describe("The user prompt to route") },
82
+ async ({ text }) => {
83
+ try {
84
+ // Stage 1: Passthrough rules
85
+ if (text.startsWith("/")) {
86
+ return {
87
+ content: [{
88
+ type: "text",
89
+ text: JSON.stringify({ action: "passthrough", transformed: text, skill: null, confidence: 0 }),
90
+ }],
91
+ };
92
+ }
93
+
94
+ // Stage 0: Active session — passthrough all prompts while session is active
95
+ if (activeSession.skill) {
96
+ if (sessionTimer) clearTimeout(sessionTimer);
97
+ sessionTimer = setTimeout(() => {
98
+ activeSession = { skill: null, startedAt: null };
99
+ sessionTimer = null;
100
+ persistSession(activeSession);
101
+ process.stderr.write(`[jenga-router] Session auto-expired after ${config.sessionTimeout}ms\n`);
102
+ }, config.sessionTimeout);
103
+ return {
104
+ content: [{ type: "text", text: JSON.stringify({ action: "passthrough", transformed: text, skill: activeSession.skill, confidence: 0 }) }],
105
+ };
106
+ }
107
+
108
+ // Stage 2: Semantic + keyword matching
109
+ const promptEmbedding = await embed(text);
110
+ const match = findBestMatch(text, skillIndex, config.matchThreshold, promptEmbedding);
111
+ if (match) {
112
+ activeSession = { skill: match.skill.name, startedAt: new Date().toISOString() };
113
+ persistSession(activeSession);
114
+ if (sessionTimer) clearTimeout(sessionTimer);
115
+ sessionTimer = setTimeout(() => {
116
+ activeSession = { skill: null, startedAt: null };
117
+ persistSession(activeSession);
118
+ sessionTimer = null;
119
+ process.stderr.write(`[jenga-router] Session auto-expired after ${config.sessionTimeout}ms\n`);
120
+ }, config.sessionTimeout);
121
+ return {
122
+ content: [{
123
+ type: "text",
124
+ text: JSON.stringify({
125
+ action: "invoke",
126
+ transformed: `/${match.skill.name} ${text}`,
127
+ skill: match.skill.name,
128
+ confidence: match.confidence,
129
+ }),
130
+ }],
131
+ };
132
+ }
133
+
134
+ // Default: passthrough
135
+ return {
136
+ content: [{
137
+ type: "text",
138
+ text: JSON.stringify({ action: "passthrough", transformed: text, skill: null, confidence: 0 }),
139
+ }],
140
+ };
141
+ } catch (e) {
142
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
143
+ }
144
+ }
145
+ );
146
+
147
+ server.tool("active_session", "Returns the currently active skill session, if any.", {}, async () => {
148
+ try {
149
+ return {
150
+ content: [{ type: "text", text: JSON.stringify({ skill: activeSession.skill, startedAt: activeSession.startedAt }) }],
151
+ };
152
+ } catch (e) {
153
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
154
+ }
155
+ });
156
+
157
+ server.tool(
158
+ "end_session",
159
+ "Clear the active session for the given skill. No-op if skill is not the active session.",
160
+ { skill: z.string().describe("The skill name to end the session for") },
161
+ async ({ skill }) => {
162
+ try {
163
+ if (activeSession.skill === skill) {
164
+ activeSession = { skill: null, startedAt: null };
165
+ persistSession(activeSession);
166
+ if (sessionTimer) { clearTimeout(sessionTimer); sessionTimer = null; }
167
+ }
168
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
169
+ } catch (e) {
170
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
171
+ }
172
+ }
173
+ );
174
+
175
+ server.tool(
176
+ "list_skills",
177
+ "Returns the current skill index — all loaded skills with their name, description, and keywords.",
178
+ {},
179
+ async () => {
180
+ try {
181
+ const skills = skillIndex.map(({ name, description, keywords }) => ({ name, description, keywords }));
182
+ return {
183
+ content: [{ type: "text", text: JSON.stringify({ skills }) }],
184
+ };
185
+ } catch (e) {
186
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
187
+ }
188
+ }
189
+ );
190
+
191
+ server.tool("reload_skills", "Re-scan the skills directory and rebuild the in-memory skill index.", {}, async () => {
192
+ try {
193
+ await warmUp();
194
+ skillIndex = await buildSkillIndex(join(projectRoot, config.skillsPath), { embed });
195
+ return {
196
+ content: [{ type: "text", text: JSON.stringify({ ok: true, skill_count: skillIndex.length }) }],
197
+ };
198
+ } catch (e) {
199
+ return { content: [{ type: "text", text: JSON.stringify({ error: e.message }) }], isError: true };
200
+ }
201
+ });
202
+
203
+ const transport = new StdioServerTransport();
204
+ await server.connect(transport);
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Scores a prompt against a skill using keyword and name matching.
3
+ * Returns a confidence score between 0 and 1.
4
+ */
5
+ function scoreSkill(prompt, skill) {
6
+ const text = prompt.toLowerCase();
7
+ let score = 0;
8
+ let matches = 0;
9
+ let total = 0;
10
+
11
+ // Check skill name
12
+ total++;
13
+ if (text.includes(skill.name.toLowerCase())) {
14
+ score += 0.9;
15
+ matches++;
16
+ }
17
+
18
+ // Check keywords
19
+ for (const kw of skill.keywords) {
20
+ total++;
21
+ if (text.includes(kw.toLowerCase())) {
22
+ score += 0.7;
23
+ matches++;
24
+ }
25
+ }
26
+
27
+ // Check examples (partial match)
28
+ for (const ex of skill.examples) {
29
+ const exWords = ex.toLowerCase().split(/\s+/).filter(w => w.length > 3);
30
+ const matchedWords = exWords.filter(w => text.includes(w));
31
+ if (matchedWords.length > 0 && exWords.length > 0) {
32
+ total++;
33
+ score += 0.5 * (matchedWords.length / exWords.length);
34
+ matches++;
35
+ }
36
+ }
37
+
38
+ if (total === 0) return 0;
39
+ // Normalize: max possible score per item varies, cap at 1.0
40
+ return Math.min(score / Math.max(total, 1), 1.0);
41
+ }
42
+
43
+ /**
44
+ * Computes cosine similarity between two Float32Array vectors.
45
+ * Result is clamped to [0, 1].
46
+ */
47
+ function cosineSimilarity(a, b) {
48
+ let dot = 0, normA = 0, normB = 0;
49
+ for (let i = 0; i < a.length; i++) {
50
+ dot += a[i] * b[i];
51
+ normA += a[i] * a[i];
52
+ normB += b[i] * b[i];
53
+ }
54
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
55
+ if (denom === 0) return 0;
56
+ return Math.max(0, Math.min(1, dot / denom));
57
+ }
58
+
59
+ /**
60
+ * Finds the best matching skill for a prompt.
61
+ * Blends keyword score with semantic cosine similarity when embeddings are available.
62
+ * Returns { skill, confidence } or null if no match above threshold.
63
+ */
64
+ export function findBestMatch(prompt, skillIndex, threshold = 0.75, promptEmbedding = null) {
65
+ let best = null;
66
+ let bestScore = 0;
67
+
68
+ for (const skill of skillIndex) {
69
+ const keywordScore = scoreSkill(prompt, skill);
70
+ let finalScore = keywordScore;
71
+
72
+ if (promptEmbedding !== null && skill.embedding !== null) {
73
+ const semanticScore = cosineSimilarity(promptEmbedding, skill.embedding);
74
+ finalScore = 0.4 * keywordScore + 0.6 * semanticScore;
75
+ }
76
+
77
+ if (finalScore > bestScore) {
78
+ bestScore = finalScore;
79
+ best = skill;
80
+ }
81
+ }
82
+
83
+ if (best && bestScore >= threshold) {
84
+ return { skill: best, confidence: bestScore };
85
+ }
86
+ return null;
87
+ }