invokora 0.1.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/cli/app.d.ts +55 -0
- package/dist/cli/app.js +1087 -0
- package/dist/cli/config.d.ts +12 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/constants.d.ts +24 -0
- package/dist/cli/constants.js +52 -0
- package/dist/cli/http.d.ts +2 -0
- package/dist/cli/http.js +23 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +11 -0
- package/dist/cli/mcp/app.d.ts +12 -0
- package/dist/cli/mcp/app.js +85 -0
- package/dist/cli/mcp/backend_client.d.ts +10 -0
- package/dist/cli/mcp/backend_client.js +91 -0
- package/dist/cli/mcp/errors.d.ts +28 -0
- package/dist/cli/mcp/errors.js +139 -0
- package/dist/cli/mcp/progress.d.ts +12 -0
- package/dist/cli/mcp/progress.js +49 -0
- package/dist/cli/mcp/responses_session.d.ts +21 -0
- package/dist/cli/mcp/responses_session.js +233 -0
- package/dist/cli/mcp/schemas.d.ts +99 -0
- package/dist/cli/mcp/schemas.js +66 -0
- package/dist/cli/mcp/server.d.ts +4 -0
- package/dist/cli/mcp/server.js +3 -0
- package/dist/cli/mcp/session_store.d.ts +32 -0
- package/dist/cli/mcp/session_store.js +58 -0
- package/dist/cli/mcp/tool_handlers.d.ts +3 -0
- package/dist/cli/mcp/tool_handlers.js +26 -0
- package/dist/cli/mcp_setup.d.ts +33 -0
- package/dist/cli/mcp_setup.js +225 -0
- package/dist/cli/oauth.d.ts +45 -0
- package/dist/cli/oauth.js +594 -0
- package/dist/cli/prompts.d.ts +23 -0
- package/dist/cli/prompts.js +175 -0
- package/dist/cli/release.d.ts +3 -0
- package/dist/cli/release.js +3 -0
- package/dist/cli/skills.d.ts +43 -0
- package/dist/cli/skills.js +443 -0
- package/dist/cli/types.d.ts +183 -0
- package/dist/cli/types.js +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { cancel as clackCancel, confirm as clackConfirm, isCancel, multiselect, select } from '@clack/prompts';
|
|
3
|
+
import { DEFAULT_MCP_TARGET_INPUT, DEFAULT_SYNC_TARGET_INPUT, MCP_TARGET_ALIASES, MCP_TARGET_PICKER_OPTIONS, SYNC_TARGET_ALIASES, } from './constants.js';
|
|
4
|
+
export class CliPrompts {
|
|
5
|
+
async line(question) {
|
|
6
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
rl.question(question, (answer) => {
|
|
9
|
+
rl.close();
|
|
10
|
+
resolve(answer.trim());
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
async yesNo(question, defaultValue) {
|
|
15
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
16
|
+
const result = await clackConfirm({
|
|
17
|
+
message: question,
|
|
18
|
+
initialValue: defaultValue,
|
|
19
|
+
});
|
|
20
|
+
if (isCancel(result)) {
|
|
21
|
+
clackCancel('Operation cancelled.');
|
|
22
|
+
throw new Error('Cancelled');
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
const suffix = defaultValue ? ' [Y/n]: ' : ' [y/N]: ';
|
|
27
|
+
const answer = (await this.line(`${question}${suffix}`)).toLowerCase();
|
|
28
|
+
if (!answer)
|
|
29
|
+
return defaultValue;
|
|
30
|
+
if (answer === 'y' || answer === 'yes')
|
|
31
|
+
return true;
|
|
32
|
+
if (answer === 'n' || answer === 'no')
|
|
33
|
+
return false;
|
|
34
|
+
return defaultValue;
|
|
35
|
+
}
|
|
36
|
+
async pickMcpTargets() {
|
|
37
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
38
|
+
return this.defaultMcpTargets();
|
|
39
|
+
}
|
|
40
|
+
const result = await multiselect({
|
|
41
|
+
message: 'Choose where to install the MCP server config.',
|
|
42
|
+
options: MCP_TARGET_PICKER_OPTIONS.map((option) => ({
|
|
43
|
+
value: option.target,
|
|
44
|
+
label: option.label,
|
|
45
|
+
hint: `${option.summary} · ${option.path}`,
|
|
46
|
+
})),
|
|
47
|
+
initialValues: MCP_TARGET_PICKER_OPTIONS.map((option) => option.target),
|
|
48
|
+
required: true,
|
|
49
|
+
});
|
|
50
|
+
if (isCancel(result)) {
|
|
51
|
+
clackCancel('Operation cancelled.');
|
|
52
|
+
throw new Error('Cancelled');
|
|
53
|
+
}
|
|
54
|
+
return this.uniqueTargets(result);
|
|
55
|
+
}
|
|
56
|
+
async pickSyncScope() {
|
|
57
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
58
|
+
return 'project';
|
|
59
|
+
}
|
|
60
|
+
const result = await select({
|
|
61
|
+
message: 'Where should Invokora sync these local skills?',
|
|
62
|
+
options: [
|
|
63
|
+
{
|
|
64
|
+
value: 'project',
|
|
65
|
+
label: 'Project',
|
|
66
|
+
hint: 'Only this repository/workspace',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
value: 'user',
|
|
70
|
+
label: 'User',
|
|
71
|
+
hint: 'Available across your machine',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
initialValue: 'project',
|
|
75
|
+
});
|
|
76
|
+
if (isCancel(result)) {
|
|
77
|
+
clackCancel('Operation cancelled.');
|
|
78
|
+
throw new Error('Cancelled');
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
async pickSyncTargets(scope) {
|
|
83
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
84
|
+
return this.defaultSyncTargets();
|
|
85
|
+
}
|
|
86
|
+
const options = [
|
|
87
|
+
{
|
|
88
|
+
value: 'codex',
|
|
89
|
+
label: 'Codex',
|
|
90
|
+
hint: scope === 'user' ? '~/.codex/skills' : '.agents/skills',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
value: 'claude',
|
|
94
|
+
label: 'Claude Code',
|
|
95
|
+
hint: scope === 'user' ? '~/.claude/skills' : '.claude/skills',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
value: 'cursor',
|
|
99
|
+
label: 'Cursor',
|
|
100
|
+
hint: scope === 'user' ? '~/.cursor/skills' : '.cursor/skills',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
const result = await multiselect({
|
|
104
|
+
message: 'Choose which client(s) should receive the synced skills.',
|
|
105
|
+
options,
|
|
106
|
+
initialValues: ['codex', 'claude', 'cursor'],
|
|
107
|
+
required: true,
|
|
108
|
+
});
|
|
109
|
+
if (isCancel(result)) {
|
|
110
|
+
clackCancel('Operation cancelled.');
|
|
111
|
+
throw new Error('Cancelled');
|
|
112
|
+
}
|
|
113
|
+
return this.uniqueTargets(result);
|
|
114
|
+
}
|
|
115
|
+
async pickSyncableSkills(skills) {
|
|
116
|
+
return this.pickMany('Choose which Skill(s) to add or refresh locally.', skills);
|
|
117
|
+
}
|
|
118
|
+
async pickSyncableSkillAddMode() {
|
|
119
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
120
|
+
return 'all';
|
|
121
|
+
}
|
|
122
|
+
const result = await select({
|
|
123
|
+
message: 'How should Invokora add local Skills?',
|
|
124
|
+
options: [
|
|
125
|
+
{ value: 'all', label: 'All syncable Skills', hint: 'Add or refresh every Skill in the list' },
|
|
126
|
+
{ value: 'custom', label: 'Choose Skills', hint: 'Select individual Skills from the list' },
|
|
127
|
+
{ value: 'none', label: 'No Skills', hint: 'Leave local Skills unchanged' },
|
|
128
|
+
],
|
|
129
|
+
initialValue: 'custom',
|
|
130
|
+
});
|
|
131
|
+
if (isCancel(result)) {
|
|
132
|
+
clackCancel('Operation cancelled.');
|
|
133
|
+
throw new Error('Cancelled');
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
async pickSyncableWorkflows(workflows) {
|
|
138
|
+
return this.pickMany('Choose which Workflow(s) to add or refresh locally.', workflows);
|
|
139
|
+
}
|
|
140
|
+
async pickLocalManagedSkills(skills) {
|
|
141
|
+
return this.pickMany('Choose which local Skill(s) to remove.', skills);
|
|
142
|
+
}
|
|
143
|
+
async pickMany(message, choices) {
|
|
144
|
+
if (choices.length === 0) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
148
|
+
return choices.map((choice) => choice.value);
|
|
149
|
+
}
|
|
150
|
+
const result = await multiselect({
|
|
151
|
+
message,
|
|
152
|
+
options: choices,
|
|
153
|
+
initialValues: choices.map((choice) => choice.value),
|
|
154
|
+
required: true,
|
|
155
|
+
});
|
|
156
|
+
if (isCancel(result)) {
|
|
157
|
+
clackCancel('Operation cancelled.');
|
|
158
|
+
throw new Error('Cancelled');
|
|
159
|
+
}
|
|
160
|
+
return this.uniqueTargets(result);
|
|
161
|
+
}
|
|
162
|
+
defaultMcpTargets() {
|
|
163
|
+
return this.uniqueTargets(DEFAULT_MCP_TARGET_INPUT.split(',')
|
|
164
|
+
.map((item) => MCP_TARGET_ALIASES[item])
|
|
165
|
+
.filter((item) => Boolean(item)));
|
|
166
|
+
}
|
|
167
|
+
defaultSyncTargets() {
|
|
168
|
+
return this.uniqueTargets(DEFAULT_SYNC_TARGET_INPUT.split(',')
|
|
169
|
+
.map((item) => SYNC_TARGET_ALIASES[item])
|
|
170
|
+
.filter((item) => Boolean(item)));
|
|
171
|
+
}
|
|
172
|
+
uniqueTargets(targets) {
|
|
173
|
+
return [...new Set(targets)];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AccessibleSkill, Config, LocalSkillRemovalResult, LocalManagedSkill, SkillSummary, SyncDestination, SyncOptions, SyncResult, SyncScope, SyncTarget, EntitlementSummary, WorkflowDetailSummary, WorkflowSummary, SyncableSkillContent } from './types.js';
|
|
2
|
+
type SyncedSkillContentMap = Map<string, SyncableSkillContent>;
|
|
3
|
+
export declare class InvokoraApiClient {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private readonly fetchImpl;
|
|
6
|
+
constructor(config: Config, fetchImpl?: typeof fetch);
|
|
7
|
+
listSyncableDetails(): Promise<AccessibleSkill[]>;
|
|
8
|
+
getSyncableSkillContent(skillID: string): Promise<SyncableSkillContent>;
|
|
9
|
+
listEntitlements(): Promise<EntitlementSummary[]>;
|
|
10
|
+
getWorkflowDetail(workflowID: string): Promise<WorkflowDetailSummary>;
|
|
11
|
+
listPersonalWorkflows(): Promise<WorkflowSummary[]>;
|
|
12
|
+
getPersonalWorkflowDetail(workflowID: string): Promise<WorkflowDetailSummary>;
|
|
13
|
+
private request;
|
|
14
|
+
}
|
|
15
|
+
export declare class SkillShellManager {
|
|
16
|
+
private readonly baseDir;
|
|
17
|
+
constructor(baseDir?: string);
|
|
18
|
+
resolveSyncDestination(target: SyncTarget, scope: SyncScope): SyncDestination;
|
|
19
|
+
slugifySkillName(name: string): string;
|
|
20
|
+
resolveSkillShellPath(skillName: string): {
|
|
21
|
+
slug: string;
|
|
22
|
+
outDir: string;
|
|
23
|
+
outPath: string;
|
|
24
|
+
};
|
|
25
|
+
generateShellContent(skill: SkillSummary, slug: string, commandSlug?: string): string;
|
|
26
|
+
sync(skills: SkillSummary[], options: SyncOptions, syncedContentBySkill?: SyncedSkillContentMap): SyncResult;
|
|
27
|
+
private planSkillWrites;
|
|
28
|
+
private baseSkillSlug;
|
|
29
|
+
private uniqueDerivedSlug;
|
|
30
|
+
private localWritePriority;
|
|
31
|
+
listManagedSkills(rootDir: string): LocalManagedSkill[];
|
|
32
|
+
removeManagedSkills(rootDir: string, slugs?: string[]): LocalSkillRemovalResult;
|
|
33
|
+
private writeManagedMetadata;
|
|
34
|
+
private writeBundleFiles;
|
|
35
|
+
private resolveSafeBundlePath;
|
|
36
|
+
private buildManagedSkillWrite;
|
|
37
|
+
private isMarketplaceManagedSkill;
|
|
38
|
+
private managedContentKind;
|
|
39
|
+
private shouldEnsureLocalManagedFrontmatter;
|
|
40
|
+
private readManagedMetadata;
|
|
41
|
+
private hasMarketplaceShellMarker;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { API_V1, SKILL_SHELL_MARKER } from './constants.js';
|
|
5
|
+
import { errorMessageFromJson, readJsonResponse } from './http.js';
|
|
6
|
+
const MANAGED_SKILL_META_FILE = '.skilz-managed.json';
|
|
7
|
+
function quoteYamlSingleLineString(value) {
|
|
8
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
9
|
+
}
|
|
10
|
+
function unquoteYamlSingleLineString(value) {
|
|
11
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
12
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
13
|
+
}
|
|
14
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
15
|
+
return value.slice(1, -1).replace(/\\"/g, '"');
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function quoteSkillFrontmatterDescription(content) {
|
|
20
|
+
const lines = content.split('\n');
|
|
21
|
+
if (lines[0]?.trim() !== '---') {
|
|
22
|
+
return content;
|
|
23
|
+
}
|
|
24
|
+
for (let i = 1; i < lines.length; i++) {
|
|
25
|
+
const trimmed = lines[i].trim();
|
|
26
|
+
if (trimmed === '---') {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (!trimmed.startsWith('description:')) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const value = trimmed.slice('description:'.length).trim();
|
|
33
|
+
if (value.startsWith('|') || value.startsWith('>')) {
|
|
34
|
+
return content;
|
|
35
|
+
}
|
|
36
|
+
const unquotedValue = unquoteYamlSingleLineString(value);
|
|
37
|
+
const indent = lines[i].slice(0, lines[i].indexOf('description:'));
|
|
38
|
+
lines[i] = `${indent}description: ${quoteYamlSingleLineString(unquotedValue)}`;
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
function ensureLocalManagedSkillFrontmatter(content, skill, slug) {
|
|
44
|
+
const description = skill.description || skill.name;
|
|
45
|
+
const lines = content.split('\n');
|
|
46
|
+
if (lines[0]?.trim() !== '---') {
|
|
47
|
+
return `---
|
|
48
|
+
name: ${slug}
|
|
49
|
+
description: ${quoteYamlSingleLineString(description)}
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
${content}`;
|
|
53
|
+
}
|
|
54
|
+
const frontmatterEndIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
55
|
+
if (frontmatterEndIndex === -1) {
|
|
56
|
+
return `---
|
|
57
|
+
name: ${slug}
|
|
58
|
+
description: ${quoteYamlSingleLineString(description)}
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
${content}`;
|
|
62
|
+
}
|
|
63
|
+
let nameIndex = -1;
|
|
64
|
+
let descriptionIndex = -1;
|
|
65
|
+
for (let i = 1; i < frontmatterEndIndex; i++) {
|
|
66
|
+
const trimmed = lines[i].trim();
|
|
67
|
+
if (trimmed.startsWith('name:')) {
|
|
68
|
+
nameIndex = i;
|
|
69
|
+
}
|
|
70
|
+
else if (trimmed.startsWith('description:')) {
|
|
71
|
+
descriptionIndex = i;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (nameIndex === -1) {
|
|
75
|
+
lines.splice(1, 0, `name: ${slug}`);
|
|
76
|
+
if (descriptionIndex !== -1) {
|
|
77
|
+
descriptionIndex++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const value = lines[nameIndex].trim().slice('name:'.length).trim();
|
|
82
|
+
if (!value) {
|
|
83
|
+
const indent = lines[nameIndex].slice(0, lines[nameIndex].indexOf('name:'));
|
|
84
|
+
lines[nameIndex] = `${indent}name: ${slug}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const updatedFrontmatterEndIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
88
|
+
if (descriptionIndex === -1) {
|
|
89
|
+
lines.splice(updatedFrontmatterEndIndex, 0, `description: ${quoteYamlSingleLineString(description)}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const value = lines[descriptionIndex].trim().slice('description:'.length).trim();
|
|
93
|
+
if (!value.startsWith('|') && !value.startsWith('>')) {
|
|
94
|
+
const unquotedValue = unquoteYamlSingleLineString(value) || description;
|
|
95
|
+
const indent = lines[descriptionIndex].slice(0, lines[descriptionIndex].indexOf('description:'));
|
|
96
|
+
lines[descriptionIndex] = `${indent}description: ${quoteYamlSingleLineString(unquotedValue)}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
export class InvokoraApiClient {
|
|
102
|
+
config;
|
|
103
|
+
fetchImpl;
|
|
104
|
+
constructor(config, fetchImpl = fetch) {
|
|
105
|
+
this.config = config;
|
|
106
|
+
this.fetchImpl = fetchImpl;
|
|
107
|
+
}
|
|
108
|
+
async listSyncableDetails() {
|
|
109
|
+
const data = await this.request('GET', `${API_V1}/access/syncable`);
|
|
110
|
+
return data.skills ?? [];
|
|
111
|
+
}
|
|
112
|
+
async getSyncableSkillContent(skillID) {
|
|
113
|
+
return this.request('GET', `${API_V1}/access/syncable/${encodeURIComponent(skillID)}/content`);
|
|
114
|
+
}
|
|
115
|
+
async listEntitlements() {
|
|
116
|
+
const data = await this.request('GET', `${API_V1}/me/entitlements`);
|
|
117
|
+
return data.entitlements ?? [];
|
|
118
|
+
}
|
|
119
|
+
async getWorkflowDetail(workflowID) {
|
|
120
|
+
return this.request('GET', `${API_V1}/workflows/${encodeURIComponent(workflowID)}/detail`);
|
|
121
|
+
}
|
|
122
|
+
async listPersonalWorkflows() {
|
|
123
|
+
const data = await this.request('GET', `${API_V1}/personal-workflows`);
|
|
124
|
+
return data.workflows ?? [];
|
|
125
|
+
}
|
|
126
|
+
async getPersonalWorkflowDetail(workflowID) {
|
|
127
|
+
return this.request('GET', `${API_V1}/personal-workflows/${encodeURIComponent(workflowID)}`);
|
|
128
|
+
}
|
|
129
|
+
async request(method, path, body) {
|
|
130
|
+
const url = new URL(path, this.config.backend_url);
|
|
131
|
+
const response = await this.fetchImpl(url, {
|
|
132
|
+
method,
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
Authorization: `Bearer ${this.config.token}`,
|
|
136
|
+
},
|
|
137
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
138
|
+
});
|
|
139
|
+
const json = await readJsonResponse(response);
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(errorMessageFromJson(json, response.status));
|
|
142
|
+
}
|
|
143
|
+
return json;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export class SkillShellManager {
|
|
147
|
+
baseDir;
|
|
148
|
+
constructor(baseDir = process.cwd()) {
|
|
149
|
+
this.baseDir = baseDir;
|
|
150
|
+
}
|
|
151
|
+
resolveSyncDestination(target, scope) {
|
|
152
|
+
if (target === 'codex') {
|
|
153
|
+
return {
|
|
154
|
+
target,
|
|
155
|
+
scope,
|
|
156
|
+
rootDir: scope === 'user' ? join(homedir(), '.codex', 'skills') : join(this.baseDir, '.agents', 'skills'),
|
|
157
|
+
label: scope === 'user' ? 'codex:user' : 'codex:project',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (target === 'claude') {
|
|
161
|
+
return {
|
|
162
|
+
target,
|
|
163
|
+
scope,
|
|
164
|
+
rootDir: scope === 'user' ? join(homedir(), '.claude', 'skills') : join(this.baseDir, '.claude', 'skills'),
|
|
165
|
+
label: scope === 'user' ? 'claude:user' : 'claude:project',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
target,
|
|
170
|
+
scope,
|
|
171
|
+
rootDir: scope === 'user' ? join(homedir(), '.cursor', 'skills') : join(this.baseDir, '.cursor', 'skills'),
|
|
172
|
+
label: scope === 'user' ? 'cursor:user' : 'cursor:project',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
slugifySkillName(name) {
|
|
176
|
+
return name
|
|
177
|
+
.replace(/\//g, '-')
|
|
178
|
+
.replace(/[^a-z0-9-]/gi, '-')
|
|
179
|
+
.replace(/-+/g, '-')
|
|
180
|
+
.toLowerCase();
|
|
181
|
+
}
|
|
182
|
+
resolveSkillShellPath(skillName) {
|
|
183
|
+
const slug = this.slugifySkillName(skillName);
|
|
184
|
+
const outDir = join(this.baseDir, '.agents', 'skills', slug);
|
|
185
|
+
return {
|
|
186
|
+
slug,
|
|
187
|
+
outDir,
|
|
188
|
+
outPath: join(outDir, 'SKILL.md'),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
generateShellContent(skill, slug, commandSlug = slug) {
|
|
192
|
+
const description = quoteYamlSingleLineString(`${skill.description || skill.name} (via Invokora)`);
|
|
193
|
+
return `---
|
|
194
|
+
name: ${slug}
|
|
195
|
+
description: ${description}
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
When the user asks for help related to this skill, call the Invokora MCP \`chat\` tool.
|
|
199
|
+
Send the user's intent in \`messages\` and include the slash command \`/${commandSlug}\` in the latest user message, for example:
|
|
200
|
+
|
|
201
|
+
\`\`\`json
|
|
202
|
+
{"messages":[{"role":"user","content":"/${commandSlug} 帮我分析这个仓库"}]}
|
|
203
|
+
\`\`\`
|
|
204
|
+
|
|
205
|
+
If local files are relevant, inspect them with the host agent's own tools first, then include the necessary excerpts in the \`chat\` message. The Invokora MCP server itself does not read files, write files, or run shell commands.
|
|
206
|
+
|
|
207
|
+
${SKILL_SHELL_MARKER}
|
|
208
|
+
`.trimStart();
|
|
209
|
+
}
|
|
210
|
+
sync(skills, options, syncedContentBySkill = new Map()) {
|
|
211
|
+
const skillsRoot = options.rootDir ?? join(this.baseDir, '.agents', 'skills');
|
|
212
|
+
mkdirSync(skillsRoot, { recursive: true });
|
|
213
|
+
let written = 0;
|
|
214
|
+
const activeSlugs = new Set();
|
|
215
|
+
const plans = this.planSkillWrites(skills, syncedContentBySkill);
|
|
216
|
+
for (const plan of plans) {
|
|
217
|
+
const { skill, slug } = plan;
|
|
218
|
+
activeSlugs.add(slug);
|
|
219
|
+
const outDir = join(skillsRoot, slug);
|
|
220
|
+
if (existsSync(outDir) && this.isMarketplaceManagedSkill(outDir)) {
|
|
221
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
mkdirSync(outDir, { recursive: true });
|
|
224
|
+
const skillWrite = this.buildManagedSkillWrite(skill, slug, plan.syncedContent, plan.commandSlug);
|
|
225
|
+
writeFileSync(join(outDir, 'SKILL.md'), skillWrite.content, 'utf-8');
|
|
226
|
+
this.writeBundleFiles(outDir, skillWrite.files);
|
|
227
|
+
this.writeManagedMetadata(outDir, skillWrite.metadata);
|
|
228
|
+
written++;
|
|
229
|
+
}
|
|
230
|
+
let pruned = 0;
|
|
231
|
+
if (options.prune && existsSync(skillsRoot)) {
|
|
232
|
+
const entries = readdirSync(skillsRoot, { withFileTypes: true });
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
if (!entry.isDirectory())
|
|
235
|
+
continue;
|
|
236
|
+
if (activeSlugs.has(entry.name))
|
|
237
|
+
continue;
|
|
238
|
+
const dirPath = join(skillsRoot, entry.name);
|
|
239
|
+
// prune 只清理 Invokora 托管目录,避免误删用户自己写的本地技能目录。
|
|
240
|
+
if (!this.isMarketplaceManagedSkill(dirPath))
|
|
241
|
+
continue;
|
|
242
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
243
|
+
pruned++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { written, pruned, activeSlugs };
|
|
247
|
+
}
|
|
248
|
+
planSkillWrites(skills, syncedContentBySkill) {
|
|
249
|
+
const groups = new Map();
|
|
250
|
+
skills.forEach((skill, index) => {
|
|
251
|
+
const baseSlug = this.baseSkillSlug(skill);
|
|
252
|
+
const group = groups.get(baseSlug) ?? [];
|
|
253
|
+
group.push({ skill, index, syncedContent: syncedContentBySkill.get(skill.id) });
|
|
254
|
+
groups.set(baseSlug, group);
|
|
255
|
+
});
|
|
256
|
+
const preferredByBaseSlug = new Map();
|
|
257
|
+
for (const [baseSlug, group] of groups.entries()) {
|
|
258
|
+
const preferred = [...group].sort((a, b) => {
|
|
259
|
+
const priority = this.localWritePriority(a.skill, a.syncedContent) - this.localWritePriority(b.skill, b.syncedContent);
|
|
260
|
+
return priority !== 0 ? priority : a.index - b.index;
|
|
261
|
+
})[0];
|
|
262
|
+
if (preferred) {
|
|
263
|
+
preferredByBaseSlug.set(baseSlug, preferred.skill.id);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const usedSlugs = new Set();
|
|
267
|
+
return skills.map((skill, index) => {
|
|
268
|
+
const baseSlug = this.baseSkillSlug(skill);
|
|
269
|
+
const syncedContent = syncedContentBySkill.get(skill.id);
|
|
270
|
+
const preferredID = preferredByBaseSlug.get(baseSlug);
|
|
271
|
+
const slug = skill.id === preferredID && !usedSlugs.has(baseSlug)
|
|
272
|
+
? baseSlug
|
|
273
|
+
: this.uniqueDerivedSlug(baseSlug, skill.id || String(index), usedSlugs);
|
|
274
|
+
usedSlugs.add(slug);
|
|
275
|
+
return {
|
|
276
|
+
skill,
|
|
277
|
+
slug,
|
|
278
|
+
commandSlug: baseSlug,
|
|
279
|
+
syncedContent,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
baseSkillSlug(skill) {
|
|
284
|
+
return this.slugifySkillName(skill.name) || this.slugifySkillName(skill.id) || 'skill';
|
|
285
|
+
}
|
|
286
|
+
uniqueDerivedSlug(baseSlug, skillID, usedSlugs) {
|
|
287
|
+
const idSlug = this.slugifySkillName(skillID) || 'skill';
|
|
288
|
+
let candidate = `${baseSlug}-${idSlug}`;
|
|
289
|
+
let counter = 2;
|
|
290
|
+
while (usedSlugs.has(candidate)) {
|
|
291
|
+
candidate = `${baseSlug}-${idSlug}-${counter}`;
|
|
292
|
+
counter++;
|
|
293
|
+
}
|
|
294
|
+
return candidate;
|
|
295
|
+
}
|
|
296
|
+
localWritePriority(skill, syncedContent) {
|
|
297
|
+
if (syncedContent && skill.access_source === 'personal')
|
|
298
|
+
return 0;
|
|
299
|
+
if (syncedContent)
|
|
300
|
+
return 1;
|
|
301
|
+
return 2;
|
|
302
|
+
}
|
|
303
|
+
listManagedSkills(rootDir) {
|
|
304
|
+
if (!existsSync(rootDir)) {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
const skills = [];
|
|
308
|
+
const entries = readdirSync(rootDir, { withFileTypes: true });
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
if (!entry.isDirectory())
|
|
311
|
+
continue;
|
|
312
|
+
const dirPath = join(rootDir, entry.name);
|
|
313
|
+
if (!this.isMarketplaceManagedSkill(dirPath))
|
|
314
|
+
continue;
|
|
315
|
+
const metadata = this.readManagedMetadata(dirPath);
|
|
316
|
+
skills.push({
|
|
317
|
+
slug: entry.name,
|
|
318
|
+
path: dirPath,
|
|
319
|
+
skill_id: typeof metadata?.skill_id === 'string' ? metadata.skill_id : undefined,
|
|
320
|
+
kind: typeof metadata?.kind === 'string' ? metadata.kind : undefined,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return skills.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
324
|
+
}
|
|
325
|
+
removeManagedSkills(rootDir, slugs) {
|
|
326
|
+
if (!existsSync(rootDir)) {
|
|
327
|
+
return { removed: 0 };
|
|
328
|
+
}
|
|
329
|
+
let removed = 0;
|
|
330
|
+
const selectedSlugs = slugs ? new Set(slugs) : null;
|
|
331
|
+
for (const skill of this.listManagedSkills(rootDir)) {
|
|
332
|
+
if (selectedSlugs && !selectedSlugs.has(skill.slug))
|
|
333
|
+
continue;
|
|
334
|
+
// remove 只删除 Invokora 托管目录,避免误删用户自己维护的本地技能。
|
|
335
|
+
rmSync(skill.path, { recursive: true, force: true });
|
|
336
|
+
removed++;
|
|
337
|
+
}
|
|
338
|
+
return { removed };
|
|
339
|
+
}
|
|
340
|
+
writeManagedMetadata(outDir, metadata) {
|
|
341
|
+
writeFileSync(join(outDir, MANAGED_SKILL_META_FILE), `${JSON.stringify(metadata, null, 2)}\n`, 'utf-8');
|
|
342
|
+
}
|
|
343
|
+
writeBundleFiles(outDir, files) {
|
|
344
|
+
const root = resolve(outDir);
|
|
345
|
+
for (const file of files) {
|
|
346
|
+
if (file.path === 'SKILL.md')
|
|
347
|
+
continue;
|
|
348
|
+
const target = this.resolveSafeBundlePath(root, file.path);
|
|
349
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
350
|
+
writeFileSync(target, file.content, 'utf-8');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
resolveSafeBundlePath(root, filePath) {
|
|
354
|
+
if (!filePath || isAbsolute(filePath) || filePath.includes('\\')) {
|
|
355
|
+
throw new Error(`Unsafe skill bundle path: ${filePath}`);
|
|
356
|
+
}
|
|
357
|
+
const target = resolve(root, filePath);
|
|
358
|
+
if (target !== root && !target.startsWith(`${root}/`)) {
|
|
359
|
+
throw new Error(`Unsafe skill bundle path: ${filePath}`);
|
|
360
|
+
}
|
|
361
|
+
return target;
|
|
362
|
+
}
|
|
363
|
+
buildManagedSkillWrite(skill, slug, syncedContent, commandSlug = slug) {
|
|
364
|
+
if (syncedContent) {
|
|
365
|
+
const content = this.shouldEnsureLocalManagedFrontmatter(skill)
|
|
366
|
+
? ensureLocalManagedSkillFrontmatter(syncedContent.content, skill, slug)
|
|
367
|
+
: quoteSkillFrontmatterDescription(syncedContent.content);
|
|
368
|
+
const files = (syncedContent.files ?? [])
|
|
369
|
+
.filter((file) => typeof file.content === 'string' && file.path)
|
|
370
|
+
.map((file) => ({
|
|
371
|
+
path: file.path,
|
|
372
|
+
content: file.path === 'SKILL.md' ? content : (file.content ?? ''),
|
|
373
|
+
}));
|
|
374
|
+
return {
|
|
375
|
+
content,
|
|
376
|
+
files,
|
|
377
|
+
metadata: {
|
|
378
|
+
managed_by: 'skilz',
|
|
379
|
+
kind: this.managedContentKind(skill),
|
|
380
|
+
skill_id: skill.id,
|
|
381
|
+
resolved_version_id: syncedContent.resolved_version_id,
|
|
382
|
+
version: syncedContent.version,
|
|
383
|
+
content_hash: syncedContent.content_hash,
|
|
384
|
+
files: (syncedContent.files ?? []).map((file) => ({
|
|
385
|
+
path: file.path,
|
|
386
|
+
content_hash: file.content_hash,
|
|
387
|
+
size_bytes: file.size_bytes,
|
|
388
|
+
kind: file.kind,
|
|
389
|
+
})),
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
content: this.generateShellContent(skill, slug, commandSlug),
|
|
395
|
+
files: [],
|
|
396
|
+
metadata: {
|
|
397
|
+
managed_by: 'skilz',
|
|
398
|
+
kind: 'shell',
|
|
399
|
+
skill_id: skill.id,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
isMarketplaceManagedSkill(dirPath) {
|
|
404
|
+
const metadata = this.readManagedMetadata(dirPath);
|
|
405
|
+
if (metadata) {
|
|
406
|
+
return metadata.managed_by === 'skilz';
|
|
407
|
+
}
|
|
408
|
+
return this.hasMarketplaceShellMarker(dirPath);
|
|
409
|
+
}
|
|
410
|
+
managedContentKind(skill) {
|
|
411
|
+
if (skill.access_source === 'personal') {
|
|
412
|
+
return 'personal_content';
|
|
413
|
+
}
|
|
414
|
+
return 'public_content';
|
|
415
|
+
}
|
|
416
|
+
shouldEnsureLocalManagedFrontmatter(skill) {
|
|
417
|
+
return skill.access_source === 'personal';
|
|
418
|
+
}
|
|
419
|
+
readManagedMetadata(dirPath) {
|
|
420
|
+
const metadataPath = join(dirPath, MANAGED_SKILL_META_FILE);
|
|
421
|
+
if (!existsSync(metadataPath)) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
return JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
hasMarketplaceShellMarker(dirPath) {
|
|
432
|
+
const skillFilePath = join(dirPath, 'SKILL.md');
|
|
433
|
+
if (!existsSync(skillFilePath))
|
|
434
|
+
return false;
|
|
435
|
+
try {
|
|
436
|
+
const content = readFileSync(skillFilePath, 'utf-8');
|
|
437
|
+
return content.includes(SKILL_SHELL_MARKER);
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|