@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.
- package/LICENSE +201 -0
- package/README.md +340 -0
- package/agents/ai_engineer.md +113 -0
- package/agents/developer.md +236 -0
- package/agents/scrum-master.md +349 -0
- package/agents/scrutiny-agent.md +137 -0
- package/agents/solution-assessor.md +185 -0
- package/agents/tester.md +339 -0
- package/bin/jenga.js +70 -0
- package/hooks/copilot_session_end.sh +29 -0
- package/hooks/on_session_end.sh +238 -0
- package/hooks/prompt_router.sh +11 -0
- package/hooks/prompt_router_helper.js +52 -0
- package/hooks/session_end_helper.js +29 -0
- package/hooks/session_end_watcher.sh +24 -0
- package/lib/commands/attach.js +47 -0
- package/lib/commands/init.js +207 -0
- package/lib/commands/start.js +16 -0
- package/lib/commands/status.js +53 -0
- package/lib/config-schema.js +72 -0
- package/lib/inject-settings.js +61 -0
- package/lib/mirror.js +244 -0
- package/lib/resolve-project-dir.sh +47 -0
- package/mcp/execute-ticket/index.js +10 -0
- package/mcp/execute-ticket/package.json +5 -0
- package/mcp/help/index.js +79 -0
- package/mcp/help/package.json +14 -0
- package/mcp/router/README.md +19 -0
- package/mcp/router/embedder.js +23 -0
- package/mcp/router/index.js +204 -0
- package/mcp/router/matcher.js +87 -0
- package/mcp/router/package-lock.json +1048 -0
- package/mcp/router/package.json +11 -0
- package/mcp/router/skill-index.js +104 -0
- package/package.json +47 -0
- package/scripts/board_resolver.sh +46 -0
- package/scripts/e25_s01_extract_board_graph.py +292 -0
- package/scripts/e25_s01_generate_synthetic_board.py +90 -0
- package/scripts/measurement-10x.json +50 -0
- package/scripts/measurement-10x.txt +4 -0
- package/scripts/measurement-real.json +50 -0
- package/scripts/measurement-real.txt +4 -0
- package/scripts/postinstall.js +165 -0
- package/scripts/todo_cleanup.sh +22 -0
- package/scripts/todo_manager.sh +86 -0
- package/scripts/validate-board.sh +190 -0
- package/scripts/validate-story-format.sh +53 -0
- package/skills/brainstorm/SKILL.md +47 -0
- package/skills/btw/SKILL.md +42 -0
- package/skills/commit/SKILL.md +29 -0
- package/skills/commit/assets/user_instructions_template.md +22 -0
- package/skills/continue/SKILL.md +29 -0
- package/skills/convert/SKILL.md +124 -0
- package/skills/convert/convert_cli.py +235 -0
- package/skills/convert/tests/sample.csv +4 -0
- package/skills/convert/tests/sample.json +5 -0
- package/skills/convert/tests/sample.jsonl +3 -0
- package/skills/convert/tests/sample.yaml +18 -0
- package/skills/convert/tests/sample_obj.csv +2 -0
- package/skills/convert/tests/sample_obj.json +9 -0
- package/skills/deep-dive/SKILL.md +167 -0
- package/skills/do/SKILL.md +88 -0
- package/skills/do/assets/sender_template.json +12 -0
- package/skills/doc/SKILL.md +314 -0
- package/skills/doc/assets/path-objectives.yaml +38 -0
- package/skills/doc-sync/SKILL.md +167 -0
- package/skills/doc-sync/assets/default_excludes.txt +21 -0
- package/skills/doc-sync/assets/doc_targets.md +14 -0
- package/skills/dooo/SKILL.md +60 -0
- package/skills/error/SKILL.md +29 -0
- package/skills/evaluate/SKILL.md +45 -0
- package/skills/evaluate/assets/evaluation_invokation_template.yml +3 -0
- package/skills/evaluate/assets/evaluation_rapport_template.md +24 -0
- package/skills/examplify/SKILL.md +42 -0
- package/skills/help/SKILL.md +36 -0
- package/skills/improve/SKILL.md +55 -0
- package/skills/index/scripts/board-index +4 -0
- package/skills/index/scripts/board_index.py +615 -0
- package/skills/index/scripts/smoke_test.sh +86 -0
- package/skills/init/SKILL.md +44 -0
- package/skills/init/assets/.gitignore_template +15 -0
- package/skills/init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/init/assets/directory_structure.txt +13 -0
- package/skills/init/assets/test-config_template.json +4 -0
- package/skills/init/assets/workflow_template.json +30 -0
- package/skills/init/scripts/init.sh +48 -0
- package/skills/jbp/SKILL.md +25 -0
- package/skills/jenga/SKILL.md +68 -0
- package/skills/lgtm/SKILL.md +21 -0
- package/skills/mirror-public/SKILL.md +237 -0
- package/skills/mirror-public/assets/config.json +5 -0
- package/skills/mirror-public/scripts/mirror.sh +374 -0
- package/skills/pi-plan/SKILL.md +62 -0
- package/skills/pi-plan/assets/epic.json +7 -0
- package/skills/pi-plan/assets/story_template.md +18 -0
- package/skills/proceed/SKILL.md +29 -0
- package/skills/publish/SKILL.md +351 -0
- package/skills/publish/adapters/droplet.md +200 -0
- package/skills/publish/adapters/mobile-ios.md +114 -0
- package/skills/publish/adapters/npm-ci.md +223 -0
- package/skills/publish/adapters/npm.md +121 -0
- package/skills/publish/assets/ExportOptions.plist.template +19 -0
- package/skills/publish/assets/ci-contract.md +111 -0
- package/skills/publish/assets/ownership-matrix.md +17 -0
- package/skills/publish/assets/publish.example.json +85 -0
- package/skills/publish/assets/publish.example.npm-ci.json +40 -0
- package/skills/publish/assets/publish.example.npm.json +41 -0
- package/skills/publish/assets/secrets-guide.md +104 -0
- package/skills/publish/schemas/fixtures/npm-ci-minimal.json +17 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-empty-secrets.json +18 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-workflow-path.json +18 -0
- package/skills/publish/schemas/publish.schema.json +428 -0
- package/skills/publish/scripts/check_target_config.sh +96 -0
- package/skills/publish/scripts/droplet_pipeline.sh +208 -0
- package/skills/publish/scripts/generate_release_notes.sh +200 -0
- package/skills/publish/scripts/ios_pipeline.sh +486 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +225 -0
- package/skills/publish/scripts/npm_pipeline.sh +249 -0
- package/skills/publish/scripts/publish_common.sh +253 -0
- package/skills/publish/scripts/publish_deploy.sh +538 -0
- package/skills/publish/scripts/reconcile_tags.sh +135 -0
- package/skills/publish/scripts/run_gates.sh +616 -0
- package/skills/publish/scripts/setup_wizard.sh +394 -0
- package/skills/publish/scripts/show_history.sh +95 -0
- package/skills/publish/scripts/suggest_semver_bump.sh +105 -0
- package/skills/publish/scripts/validate_config.sh +163 -0
- package/skills/publish/scripts/validate_droplet_env.sh +45 -0
- package/skills/publish/scripts/validate_ios_env.sh +68 -0
- package/skills/publish/scripts/validate_npm_ci_env.sh +71 -0
- package/skills/publish/scripts/validate_npm_env.sh +22 -0
- package/skills/publish/scripts/write_ledger_entry.sh +126 -0
- package/skills/publish/wizards/droplet.md +275 -0
- package/skills/publish/wizards/mobile-ios.md +157 -0
- package/skills/publish/wizards/npm-ci.md +240 -0
- package/skills/publish/wizards/npm.md +224 -0
- package/skills/reconcile/SKILL.md +93 -0
- package/skills/reconcile/assets/report_format.md +44 -0
- package/skills/reconcile-origin/SKILL.md +75 -0
- package/skills/reconcile-origin/scripts/reconcile-origin.sh +372 -0
- package/skills/redo/SKILL.md +70 -0
- package/skills/route/SKILL.md +180 -0
- package/skills/self-sync/SKILL.md +73 -0
- package/skills/self-sync/scripts/run.js +136 -0
- package/skills/skillify/SKILL.md +68 -0
- package/skills/skillify/assets/init-new/SKILL.md +35 -0
- package/skills/skillify/assets/init-new/assets/.gitignore_template +15 -0
- package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +10 -0
- package/skills/skillify/assets/init-new/assets/test-config_template.json +4 -0
- package/skills/skillify/assets/init-new/assets/workflow_template.json +17 -0
- package/skills/skillify/assets/init-new/scripts/init.sh +48 -0
- package/skills/skillify/assets/init-old/SKILL.md +124 -0
- package/skills/spinoff/SKILL.md +48 -0
- package/skills/status/SKILL.md +33 -0
- package/skills/status/assets/output_format.md +41 -0
- package/skills/todo/SKILL.md +46 -0
- package/skills/todo/assets/todo_handoff_template.md +22 -0
- package/skills/todo/assets/todo_template.md +3 -0
- package/skills/train/SKILL.md +116 -0
- package/skills/train/assets/dashboard-templates/classifiers.html +106 -0
- package/skills/train/assets/dashboard-templates/nlp.html +102 -0
- package/skills/train/assets/dashboard-templates/transformers.html +98 -0
- package/skills/train/assets/results-parsers/__init__.py +9 -0
- package/skills/train/assets/results-parsers/classifiers.py +84 -0
- package/skills/train/assets/results-parsers/nlp.py +88 -0
- package/skills/train/assets/results-parsers/reporter.py +154 -0
- package/skills/train/assets/results-parsers/transformers.py +120 -0
- package/skills/train/train_cli.py +786 -0
- package/templates/EXECUTION_PLAN_TEMPLATE.md +43 -0
- package/templates/EXECUTION_SUMMARY_TEMPLATE.md +50 -0
- package/templates/JENGA_CONFIG_TEMPLATE.json +23 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +88 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +311 -0
- package/templates/SKILL.md +16 -0
- package/templates/SKILL_TEMPLATE.md +28 -0
- package/templates/USER_INSTRUCTIONS_TEMPLATE.md +22 -0
- 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,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
|
+
}
|