@fruition/fcp-mcp-server 1.21.0 → 1.23.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/dist/index.d.ts +10 -0
- package/dist/index.js +4481 -0
- package/dist/skills-sync-cli.d.ts +23 -0
- package/dist/skills-sync-cli.js +121 -0
- package/dist/skills-sync.d.ts +144 -0
- package/dist/skills-sync.js +712 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills sync CLI subcommand handler.
|
|
3
|
+
*
|
|
4
|
+
* Invoked when the MCP server binary is called with the `sync-skills` argv:
|
|
5
|
+
* fcp-mcp-server sync-skills [--push|--pull|--sync] [--dry-run] [--enable] [--once]
|
|
6
|
+
*
|
|
7
|
+
* Designed for both:
|
|
8
|
+
* - Direct user invocation (one-shot, exit when done).
|
|
9
|
+
* - Background invocation from main() at MCP server startup (non-blocking,
|
|
10
|
+
* never throws).
|
|
11
|
+
*/
|
|
12
|
+
interface CliArgs {
|
|
13
|
+
mode: 'push' | 'pull' | 'sync';
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
enable: boolean;
|
|
16
|
+
help: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseArgs(argv: string[]): CliArgs;
|
|
19
|
+
/**
|
|
20
|
+
* Main entry. Returns process exit code.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runSkillsCli(argv: string[]): Promise<number>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills sync CLI subcommand handler.
|
|
3
|
+
*
|
|
4
|
+
* Invoked when the MCP server binary is called with the `sync-skills` argv:
|
|
5
|
+
* fcp-mcp-server sync-skills [--push|--pull|--sync] [--dry-run] [--enable] [--once]
|
|
6
|
+
*
|
|
7
|
+
* Designed for both:
|
|
8
|
+
* - Direct user invocation (one-shot, exit when done).
|
|
9
|
+
* - Background invocation from main() at MCP server startup (non-blocking,
|
|
10
|
+
* never throws).
|
|
11
|
+
*/
|
|
12
|
+
import { pushSkills, pullSkills, syncSkills, recordOptIn, defaultSkillsDir, defaultStateFile, } from './skills-sync.js';
|
|
13
|
+
export function parseArgs(argv) {
|
|
14
|
+
const args = { mode: 'sync', enable: false, help: false };
|
|
15
|
+
for (const a of argv) {
|
|
16
|
+
if (a === '--push')
|
|
17
|
+
args.mode = 'push';
|
|
18
|
+
else if (a === '--pull')
|
|
19
|
+
args.mode = 'pull';
|
|
20
|
+
else if (a === '--sync')
|
|
21
|
+
args.mode = 'sync';
|
|
22
|
+
else if (a === '--dry-run')
|
|
23
|
+
args.dryRun = true;
|
|
24
|
+
else if (a === '--no-dry-run')
|
|
25
|
+
args.dryRun = false;
|
|
26
|
+
else if (a === '--enable')
|
|
27
|
+
args.enable = true;
|
|
28
|
+
else if (a === '--help' || a === '-h')
|
|
29
|
+
args.help = true;
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
function printHelp() {
|
|
34
|
+
const help = `
|
|
35
|
+
fcp-mcp-server sync-skills — sync ~/.claude/skills/<slug>/SKILL.md with the
|
|
36
|
+
shared Fruition Unroo knowledge graph.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
fcp-mcp-server sync-skills [options]
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--push Only push local skills up to Unroo
|
|
43
|
+
--pull Only pull team skills from Unroo down to local
|
|
44
|
+
--sync Pull then push (default)
|
|
45
|
+
--dry-run Print what would happen without making changes/network calls
|
|
46
|
+
--no-dry-run Force real sync even before --enable opt-in (for scripting)
|
|
47
|
+
--enable Persist the one-time opt-in flag and run a real sync
|
|
48
|
+
--help, -h Show this help
|
|
49
|
+
|
|
50
|
+
Environment (proxy mode — default, recommended):
|
|
51
|
+
FCP_API_TOKEN FCP API key. With this set and no UNROO_API_KEY, sync routes
|
|
52
|
+
through FCP's Unroo proxy — no personal Unroo key needed.
|
|
53
|
+
FCP_API_URL FCP base URL (default https://fcp.fru.io).
|
|
54
|
+
|
|
55
|
+
Environment (direct mode — optional, legacy):
|
|
56
|
+
UNROO_API_KEY Personal Unroo key. If set, sync talks to Unroo directly.
|
|
57
|
+
FCP_USER_EMAIL Your Fruition team email; required for direct-mode attribution.
|
|
58
|
+
UNROO_API_URL Unroo base URL override (default https://app.unroo.io).
|
|
59
|
+
|
|
60
|
+
State file: ${defaultStateFile(defaultSkillsDir())}
|
|
61
|
+
`.trim();
|
|
62
|
+
console.error(help);
|
|
63
|
+
}
|
|
64
|
+
function summarize(result, prefix) {
|
|
65
|
+
const tag = result.dryRun ? `${prefix} (dry run)` : prefix;
|
|
66
|
+
if (result.reason) {
|
|
67
|
+
console.error(`${tag} ${result.reason}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.error(`${tag} pulled=${result.pulled.length} pushed=${result.pushed.length} ` +
|
|
71
|
+
`skipped=${result.skipped.length} conflicts=${result.conflicts.length} ` +
|
|
72
|
+
`refused=${result.refused.length}`);
|
|
73
|
+
for (const c of result.conflicts) {
|
|
74
|
+
console.error(` conflict: ${c.slug} — ${c.why}`);
|
|
75
|
+
}
|
|
76
|
+
for (const r of result.refused) {
|
|
77
|
+
console.error(` refused: ${r.slug} — ${r.why}`);
|
|
78
|
+
}
|
|
79
|
+
if (result.dryRun && (result.pulled.length || result.pushed.length)) {
|
|
80
|
+
console.error('\nTo enable real sync, run: fcp-mcp-server sync-skills --enable');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Main entry. Returns process exit code.
|
|
85
|
+
*/
|
|
86
|
+
export async function runSkillsCli(argv) {
|
|
87
|
+
const args = parseArgs(argv);
|
|
88
|
+
if (args.help) {
|
|
89
|
+
printHelp();
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
// --enable flips the opt-in flag, then runs a real sync in the requested
|
|
93
|
+
// mode (--push, --pull, or --sync). Previously --enable always ran full
|
|
94
|
+
// sync; that turned `--enable --pull` into a surprise push for the user.
|
|
95
|
+
if (args.enable) {
|
|
96
|
+
recordOptIn();
|
|
97
|
+
console.error(`[skills-sync] Opt-in recorded. Running real sync (mode=${args.mode})...`);
|
|
98
|
+
}
|
|
99
|
+
const opts = {
|
|
100
|
+
unrooApiKey: process.env.UNROO_API_KEY ?? '',
|
|
101
|
+
userEmail: process.env.FCP_USER_EMAIL ?? '',
|
|
102
|
+
unrooApiUrl: process.env.UNROO_API_URL,
|
|
103
|
+
// FCP key enables proxy mode when no personal UNROO_API_KEY is set.
|
|
104
|
+
fcpApiUrl: process.env.FCP_API_URL,
|
|
105
|
+
fcpApiToken: process.env.FCP_API_TOKEN,
|
|
106
|
+
// --enable forces a real run; otherwise let lib decide based on opt-in flag
|
|
107
|
+
dryRun: args.enable ? false : args.dryRun,
|
|
108
|
+
};
|
|
109
|
+
let result;
|
|
110
|
+
if (args.mode === 'push') {
|
|
111
|
+
result = await pushSkills(opts);
|
|
112
|
+
}
|
|
113
|
+
else if (args.mode === 'pull') {
|
|
114
|
+
result = await pullSkills(opts);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
result = await syncSkills(opts);
|
|
118
|
+
}
|
|
119
|
+
summarize(result, '[skills-sync]');
|
|
120
|
+
return result.ok ? 0 : 1;
|
|
121
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills Sync — bidirectional sync between local ~/.claude/skills/<slug>/SKILL.md
|
|
3
|
+
* files and the Fruition team's shared knowledge graph hosted in Unroo.
|
|
4
|
+
*
|
|
5
|
+
* Distribution model: every developer's workstation runs `@fruition/fcp-mcp-server`
|
|
6
|
+
* via `npx` on each `claude` startup. This module hooks into that startup to
|
|
7
|
+
* converge skills across the team — push local additions/edits up, pull team
|
|
8
|
+
* additions down — without ever clobbering local edits.
|
|
9
|
+
*
|
|
10
|
+
* Endpoints (all gated by X-API-Key + X-FCP-User-Email Fruition-org check):
|
|
11
|
+
* GET https://app.unroo.io/api/external/fcp/knowledge/search?node_type=skill
|
|
12
|
+
* POST https://app.unroo.io/api/external/fcp/knowledge/capture-skill
|
|
13
|
+
*
|
|
14
|
+
* State file: ~/.claude/skills/.sync-state.json — tracks last-pushed mtime and
|
|
15
|
+
* last-pulled body hash per slug so we can detect three-way conflicts.
|
|
16
|
+
*
|
|
17
|
+
* Safety:
|
|
18
|
+
* - First invocation on a fresh workstation is dry-run; user must opt in once.
|
|
19
|
+
* - Bodies containing apparent secrets are refused (loud warning, no upload).
|
|
20
|
+
* - Skills under directories starting with a digit, dot, or underscore are
|
|
21
|
+
* scratch/private and never pushed.
|
|
22
|
+
* - `private: true` in frontmatter also opts out.
|
|
23
|
+
* - Network failures and missing auth never block startup — we log + continue.
|
|
24
|
+
*/
|
|
25
|
+
export interface SkillFrontmatter {
|
|
26
|
+
name?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
triggers?: string[];
|
|
29
|
+
private?: boolean;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface ParsedSkill {
|
|
33
|
+
slug: string;
|
|
34
|
+
filePath: string;
|
|
35
|
+
frontmatter: SkillFrontmatter;
|
|
36
|
+
body: string;
|
|
37
|
+
mtimeMs: number;
|
|
38
|
+
}
|
|
39
|
+
export interface SyncStateEntry {
|
|
40
|
+
lastPushedMtimeMs?: number;
|
|
41
|
+
lastPulledBodyHash?: string;
|
|
42
|
+
lastPulledRemoteUpdatedAt?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SyncState {
|
|
45
|
+
optedIn?: boolean;
|
|
46
|
+
skills: Record<string, SyncStateEntry>;
|
|
47
|
+
version: number;
|
|
48
|
+
}
|
|
49
|
+
export interface SyncOptions {
|
|
50
|
+
skillsDir?: string;
|
|
51
|
+
stateFile?: string;
|
|
52
|
+
unrooApiUrl?: string;
|
|
53
|
+
unrooApiKey?: string;
|
|
54
|
+
fcpApiUrl?: string;
|
|
55
|
+
fcpApiToken?: string;
|
|
56
|
+
userEmail?: string;
|
|
57
|
+
dryRun?: boolean;
|
|
58
|
+
log?: (msg: string) => void;
|
|
59
|
+
fetchImpl?: typeof fetch;
|
|
60
|
+
now?: () => Date;
|
|
61
|
+
}
|
|
62
|
+
export interface SyncResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
reason?: string;
|
|
65
|
+
pushed: string[];
|
|
66
|
+
pulled: string[];
|
|
67
|
+
skipped: Array<{
|
|
68
|
+
slug: string;
|
|
69
|
+
why: string;
|
|
70
|
+
}>;
|
|
71
|
+
conflicts: Array<{
|
|
72
|
+
slug: string;
|
|
73
|
+
why: string;
|
|
74
|
+
}>;
|
|
75
|
+
refused: Array<{
|
|
76
|
+
slug: string;
|
|
77
|
+
why: string;
|
|
78
|
+
}>;
|
|
79
|
+
dryRun: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare function defaultSkillsDir(): string;
|
|
82
|
+
export declare function defaultStateFile(skillsDir: string): string;
|
|
83
|
+
export declare function loadState(stateFile: string): SyncState;
|
|
84
|
+
export declare function saveState(stateFile: string, state: SyncState): void;
|
|
85
|
+
/**
|
|
86
|
+
* Parse the YAML-ish frontmatter block from a SKILL.md.
|
|
87
|
+
*
|
|
88
|
+
* We support exactly the shape used in ~/.claude/skills/*\/SKILL.md today:
|
|
89
|
+
* - `key: value` scalars (string, boolean)
|
|
90
|
+
* - `key: |` multi-line block scalars (folded into one space-joined string)
|
|
91
|
+
* - `key:` followed by ` - item` list entries (parsed as string[])
|
|
92
|
+
* - `triggers: a, b, c` inline comma list (also parsed as string[])
|
|
93
|
+
*
|
|
94
|
+
* Anything fancier (anchors, nested maps, JSON-style flow) is out of scope —
|
|
95
|
+
* if a skill author needs that we'd pull in `js-yaml`, but every existing
|
|
96
|
+
* SKILL.md in the team library fits this subset.
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseFrontmatter(source: string): {
|
|
99
|
+
frontmatter: SkillFrontmatter;
|
|
100
|
+
body: string;
|
|
101
|
+
};
|
|
102
|
+
export declare function discoverLocalSkills(skillsDir: string): Promise<ParsedSkill[]>;
|
|
103
|
+
/**
|
|
104
|
+
* Decide whether a skill should be pushed.
|
|
105
|
+
* Returns null if pushable, or a reason string if not.
|
|
106
|
+
*/
|
|
107
|
+
export declare function pushSkipReason(skill: ParsedSkill): string | null;
|
|
108
|
+
/**
|
|
109
|
+
* Detect control characters (other than tab, LF, CR) in the body. The Unroo
|
|
110
|
+
* capture-skill endpoint 500s on null bytes — and they're never legitimate
|
|
111
|
+
* skill content anyway. Returns a short description of the first hit, or
|
|
112
|
+
* null if clean.
|
|
113
|
+
*/
|
|
114
|
+
export declare function detectControlChars(body: string): string | null;
|
|
115
|
+
/**
|
|
116
|
+
* Detect apparent secrets in a skill body. Returns the matched substring
|
|
117
|
+
* (truncated) for logging, or null if clean.
|
|
118
|
+
*/
|
|
119
|
+
export declare function detectSecret(body: string): string | null;
|
|
120
|
+
export declare function hashBody(body: string): string;
|
|
121
|
+
/** Push local skills up to Unroo. */
|
|
122
|
+
export declare function pushSkills(opts?: SyncOptions): Promise<SyncResult>;
|
|
123
|
+
/** Pull team skills from Unroo down to local. */
|
|
124
|
+
export declare function pullSkills(opts?: SyncOptions): Promise<SyncResult>;
|
|
125
|
+
/** Bidirectional sync: pull then push. */
|
|
126
|
+
export declare function syncSkills(opts?: SyncOptions): Promise<SyncResult>;
|
|
127
|
+
/**
|
|
128
|
+
* Persist the user's opt-in. Called by the CLI when the user runs
|
|
129
|
+
* `fcp-mcp-server sync-skills --enable` for the first time.
|
|
130
|
+
*/
|
|
131
|
+
export declare function recordOptIn(stateFile?: string, skillsDir?: string): void;
|
|
132
|
+
/**
|
|
133
|
+
* Background entry called from the MCP server's main(). Never throws — any
|
|
134
|
+
* failure is logged and swallowed. Default behavior on a fresh workstation:
|
|
135
|
+
* dry-run only. The user opts in by running `fcp-mcp-server sync-skills --enable`.
|
|
136
|
+
*/
|
|
137
|
+
export declare function runBackgroundSync(opts?: SyncOptions): Promise<void>;
|
|
138
|
+
export declare const __test__: {
|
|
139
|
+
parseFrontmatter: typeof parseFrontmatter;
|
|
140
|
+
detectSecret: typeof detectSecret;
|
|
141
|
+
pushSkipReason: typeof pushSkipReason;
|
|
142
|
+
hashBody: typeof hashBody;
|
|
143
|
+
basename: (path: string, suffix?: string) => string;
|
|
144
|
+
};
|