Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
<!-- frontmcp:skills-start v1.0.1 -->

# Skills and Tools

> Auto-generated by `frontmcp skills install` (v1.0.1) — do not edit manually.

This project uses **FrontMCP skills** installed in `.claude/skills/`.
Before writing code, search the installed skills for relevant guidance:

When you need to implement something, **read the matching skill first** — it contains patterns, examples, and common mistakes to avoid.

<!-- frontmcp:skills-end -->

# FrontMCP Monorepo - Development Guide

## Repository Structure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ describe('generateCliEntry', () => {
expect(source).toContain("'skills'");
expect(source).toContain("'search [query]'");
expect(source).toContain("'load <ids...>'");
expect(source).toContain("'read <name>'");
expect(source).toContain('searchSkills');
expect(source).toContain('loadSkills');
expect(source).toContain('listSkills');
Expand Down Expand Up @@ -1074,13 +1075,27 @@ describe('generateCliEntry', () => {
});

describe('skills list command', () => {
it('should generate skills list subcommand with listSkills', () => {
it('should generate skills list subcommand with listSkills and next-action hints', () => {
const source = generateCliEntry(makeOptions({
schema: makeSchema({ capabilities: { skills: true, jobs: false, workflows: false } }),
}));

expect(source).toContain("'list'");
expect(source).toContain('listSkills');
expect(source).toContain('skills search <query>');
expect(source).toContain('skills read <name>');
});
});

describe('skills read command', () => {
it('should generate skills read subcommand with loadSkills', () => {
const source = generateCliEntry(makeOptions({
schema: makeSchema({ capabilities: { skills: true, jobs: false, workflows: false } }),
}));

expect(source).toContain("'read <name>'");
expect(source).toContain('loadSkills');
expect(source).toContain('skills load');
});
});
});
Expand Down
120 changes: 93 additions & 27 deletions libs/cli/src/commands/build/exec/cli-runtime/generate-cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@ async function getClient() {
return _client;
}

async function closeClient() {
if (_client && typeof _client.close === 'function') {
try { await _client.close(); } catch (_) {}
}
_client = null;
}

// Flag set by long-running commands (serve, daemon) to prevent the footer from calling process.exit().
var _isLongRunning = false;

var program = new Command();
program
.name(${JSON.stringify(appName)})
Expand Down Expand Up @@ -539,15 +549,20 @@ skillsCmd
if (mode === 'json') {
console.log(JSON.stringify(result, null, 2));
} else {
var skills = result.skills || result || [];
if (Array.isArray(skills) && skills.length === 0) { console.log('No skills found.'); return; }
if (Array.isArray(skills)) {
skills.forEach(function(s) {
console.log(' ' + (s.name || s.id || JSON.stringify(s)));
});
} else {
console.log(JSON.stringify(result, null, 2));
}
var skills = result.skills || [];
if (skills.length === 0) { console.log('No skills found.'); return; }
console.log('\\n Skills matching "' + (query || '') + '":\\n');
skills.forEach(function(s) {
var tags = (s.tags || []).slice(0, 3).join(', ');
var score = s.score != null ? ' [score: ' + Number(s.score).toFixed(2) + ']' : '';
console.log(' ' + (s.name || s.id) + score);
if (s.description) console.log(' ' + s.description.split('. Use when')[0]);
if (tags) console.log(' tags: ' + tags);
console.log('');
});
console.log(' ' + skills.length + ' result(s).');
console.log(" Use '" + program.name() + " skills read <name>' for full details.");
console.log(" Use '" + program.name() + " skills load <name>' to load a skill.\\n");
}
} catch (err) {
console.error('Error:', err.message || err);
Expand Down Expand Up @@ -577,6 +592,43 @@ skillsCmd
}
});

skillsCmd
.command('read <name>')
.description('Read full details for a skill')
.action(async function(name) {
try {
var client = await getClient();
var result = await client.loadSkills([name]);
var mode = program.opts().output || 'text';
if (mode === 'json') {
console.log(JSON.stringify(result, null, 2));
} else {
var skills = result.skills || [];
if (skills.length === 0) { console.log('Skill "' + name + '" not found.'); return; }
var sk = skills[0];
console.log('\\n ' + sk.name);
if (sk.description) console.log(' ' + sk.description);
console.log('');
if (sk.instructions) {
console.log(sk.instructions);
console.log('');
}
if (sk.tools && sk.tools.length > 0) {
console.log(' Tools (' + sk.tools.length + '):');
sk.tools.forEach(function(t) {
console.log(' ' + t.name + (t.available ? '' : ' (unavailable)'));
});
console.log('');
}
if (result.nextSteps) console.log(' ' + result.nextSteps);
console.log(" Load: " + program.name() + " skills load " + name + '\\n');
}
} catch (err) {
console.error('Error:', err.message || err);
process.exitCode = 1;
}
});

skillsCmd
.command('list')
.description('List available skills')
Expand All @@ -588,15 +640,17 @@ skillsCmd
if (mode === 'json') {
console.log(JSON.stringify(result, null, 2));
} else {
var skills = result.skills || result || [];
if (Array.isArray(skills) && skills.length === 0) { console.log('No skills available.'); return; }
if (Array.isArray(skills)) {
skills.forEach(function(s) {
console.log(' ' + (s.name || s.id || JSON.stringify(s)));
});
} else {
console.log(JSON.stringify(result, null, 2));
}
var skills = result.skills || [];
if (skills.length === 0) { console.log('No skills available.'); return; }
console.log('\\n Available Skills (' + skills.length + '):\\n');
skills.forEach(function(s) {
var desc = s.description ? s.description.split('. Use when')[0] : '';
console.log(' ' + (s.name || s.id));
if (desc) console.log(' ' + desc);
console.log('');
});
console.log(" Use '" + program.name() + " skills search <query>' for semantic search.");
console.log(" Use '" + program.name() + " skills read <name>' for full details.\\n");
}
} catch (err) {
console.error('Error:', err.message || err);
Expand Down Expand Up @@ -878,7 +932,9 @@ async function getSubscribeClient() {
// If connected via daemon, the onNotification/onResourceUpdated are no-ops.
// Reconnect via in-process for push support.
if (client._isDaemon) {
_client = null; // clear cached daemon client
// Close the daemon client before replacing with in-process client
if (typeof client.close === 'function') { try { await client.close(); } catch (_) {} }
_client = null;
var mod = require(SERVER_BUNDLE);
var configOrClass = mod.default || mod;
var sdk = require('@frontmcp/sdk');
Expand Down Expand Up @@ -907,6 +963,7 @@ subscribeCmd
process.on('SIGINT', async function() {
console.log('\\nUnsubscribing...');
try { await client.unsubscribeResource(uri); } catch (_) { /* ok */ }
await closeClient();
process.exit(0);
});
// Keep process alive — setInterval creates an active event loop handle
Expand All @@ -933,8 +990,9 @@ subscribeCmd
console.log(fmt.formatSubscriptionEvent({ type: 'notification', method: notification.method, params: notification.params, timestamp: new Date().toISOString() }, mode));
}
});
process.on('SIGINT', function() {
process.on('SIGINT', async function() {
console.log('\\nStopping...');
await closeClient();
process.exit(0);
});
// Keep process alive — setInterval creates an active event loop handle
Expand Down Expand Up @@ -1081,6 +1139,7 @@ function generateServeCommand(serverBundleFilename: string, selfContained?: bool
.description('Start the HTTP/SSE server')
.option('-p, --port <port>', 'Port number', function(v) { return parseInt(v, 10); })
.action(async function(opts) {
_isLongRunning = true;
var mod = ${requireExpr};
if (opts.port) process.env.PORT = String(opts.port);
// If the bundle exports a start() function (@FrontMcp-decorated class auto-bootstraps), use it
Expand Down Expand Up @@ -1370,6 +1429,10 @@ ${selfContained ? ` // SEA mode: spawn the binary itself in daemon mode — a
env: env
});`}

// Close inherited file descriptors in the parent — the child already has its own copy.
fs.closeSync(out);
fs.closeSync(err);

fs.writeFileSync(pidPath, JSON.stringify({
pid: child.pid,
socketPath: socketPath,
Expand Down Expand Up @@ -1453,14 +1516,17 @@ function generateFooter(): string {
console.error('Unknown command: ' + args[0]);
process.exitCode = 1;
});
program.parseAsync(process.argv).then(function() {
// Use exitCode instead of process.exit() so long-running commands
// (e.g., serve) keep the event loop alive while short-lived commands
// exit naturally when the event loop drains.
process.exitCode = process.exitCode || 0;
}).catch(function(err) {
program.parseAsync(process.argv).then(async function() {
// Long-running commands (serve) set _isLongRunning to keep the event loop alive.
// Short-lived commands close the client and exit explicitly to avoid hanging
// on unclosed handles (file loggers, in-memory transport, etc.).
if (_isLongRunning) return;
await closeClient();
process.exit(process.exitCode || 0);
}).catch(async function(err) {
console.error('Fatal:', err.message || err);
process.exitCode = 1;
await closeClient();
process.exit(1);
});`;
}

Expand Down
34 changes: 27 additions & 7 deletions libs/cli/src/commands/skills/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,21 @@

import * as fs from 'fs';
import * as path from 'path';
import { TFIDFVectoria } from 'vectoriadb';
import type { SkillReferenceEntry } from '@frontmcp/skills';
import type { TFIDFVectoria } from 'vectoriadb';

interface SkillReferenceExample {
name: string;
description: string;
level: 'basic' | 'intermediate' | 'advanced';
tags: string[];
features: string[];
}

interface SkillReference {
name: string;
description: string;
examples?: SkillReferenceExample[];
}

interface SkillEntry {
name: string;
Expand All @@ -19,7 +32,7 @@ interface SkillEntry {
hasResources: boolean;
tags: string[];
bundle?: string[];
references?: SkillReferenceEntry[];
references?: SkillReference[];
}

interface SkillManifest {
Expand Down Expand Up @@ -237,8 +250,14 @@ export function getCatalogDir(): string {
function getSearchIndex(): TFIDFVectoria<SkillDocMetadata> {
if (cachedIndex) return cachedIndex;

// Lazy-load vectoriadb to avoid triggering TF/HF model initialization
// when only loadCatalog() is needed (e.g., skills list command).
const { TFIDFVectoria: TFIDFVectoriaImpl } = require('vectoriadb') as {
TFIDFVectoria: typeof TFIDFVectoria;
};

const manifest = loadCatalog();
cachedIndex = new TFIDFVectoria<SkillDocMetadata>({
const index = new TFIDFVectoriaImpl<SkillDocMetadata>({
defaultTopK: 10,
defaultSimilarityThreshold: 0.0,
});
Expand All @@ -249,10 +268,11 @@ function getSearchIndex(): TFIDFVectoria<SkillDocMetadata> {
metadata: { id: skill.name, skill },
}));

cachedIndex.addDocuments(documents);
cachedIndex.reindex();
index.addDocuments(documents);
index.reindex();

return cachedIndex;
cachedIndex = index;
return index;
}

/**
Expand Down
6 changes: 2 additions & 4 deletions libs/cli/src/core/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ async function main(): Promise<void> {
try {
const program = createProgram();
await program.parseAsync(process.argv);
process.exitCode = 0;
return;
process.exit(0);
} catch (err: unknown) {
console.error('\n' + c('red', err instanceof Error ? err.stack || err.message : String(err)));
process.exitCode = 1;
return;
process.exit(1);
}
}

Expand Down
7 changes: 6 additions & 1 deletion libs/nx-plugin/src/utils/versions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
getNxDevDependencies,
} from './versions';

jest.mock('fs', () => ({
...jest.requireActual('fs'),
existsSync: jest.fn().mockReturnValue(true),
}));

jest.mock('@nx/devkit', () => ({
readJsonFile: jest.fn().mockReturnValue({ version: '0.11.1' }),
readJsonFile: jest.fn().mockReturnValue({ name: '@frontmcp/nx', version: '0.11.1' }),
}));

describe('versions', () => {
Expand Down
23 changes: 17 additions & 6 deletions libs/nx-plugin/src/utils/versions.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { readJsonFile } from '@nx/devkit';
import { join } from 'path';
import { existsSync } from 'fs';

let cachedVersion: string | undefined;

function readPluginVersion(): string {
if (cachedVersion) return cachedVersion;
const pkgPath = join(__dirname, '..', '..', 'package.json');
const pkg = readJsonFile<{ version?: unknown }>(pkgPath);
if (!pkg.version || typeof pkg.version !== 'string') {
throw new Error(`@frontmcp/nx package.json at ${pkgPath} is missing a valid "version" field`);

// Walk up from __dirname to find the @frontmcp/nx package.json.
// In monorepo (src/utils/ or dist/utils/): ../../package.json
// When published to npm (utils/): ../package.json
let dir = __dirname;
for (let i = 0; i < 4; i++) {
dir = join(dir, '..');
const candidate = join(dir, 'package.json');
if (!existsSync(candidate)) continue;
const pkg = readJsonFile<{ name?: string; version?: unknown }>(candidate);
if (pkg.name === '@frontmcp/nx' && typeof pkg.version === 'string') {
cachedVersion = pkg.version;
return cachedVersion;
}
}
cachedVersion = pkg.version;
return cachedVersion;

throw new Error(`@frontmcp/nx package.json not found (searched from ${__dirname})`);
}

export function getFrontmcpVersion(): string {
Expand Down
2 changes: 1 addition & 1 deletion libs/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"@enclave-vm/core": "^2.11.1",
"openai": "^4.0.0 || ^5.0.0 || ^6.0.0",
"@anthropic-ai/sdk": "^0.30.0 || ^0.78.0",
"@frontmcp/observability": "1.0.0",
"@frontmcp/observability": "1.0.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-trace-base": "^1.25.0"
},
Expand Down
2 changes: 1 addition & 1 deletion libs/sdk/src/skill/skill.instance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// file: libs/sdk/src/skill/skill.instance.ts

import { EntryOwnerRef, SkillEntry, SkillKind, SkillRecord, SkillToolRef, normalizeToolRef } from '../common';
import { SkillContent, SkillReferenceInfo, SkillExampleInfo } from '../common/interfaces';
import { SkillContent } from '../common/interfaces';
import { SkillVisibility } from '../common/metadata/skill.metadata';
import ProviderRegistry from '../provider/provider.registry';
import { ScopeEntry } from '../common';
Expand Down
Loading
Loading