@almyty/skills 1.0.11 → 1.0.14
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/client.js +72 -6
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/dist/__tests__/client.test.d.ts +0 -1
- package/dist/__tests__/client.test.js +0 -145
package/dist/client.js
CHANGED
|
@@ -114,16 +114,82 @@ export class AlmytyClient {
|
|
|
114
114
|
}
|
|
115
115
|
async executeSkill(gatewayId, toolId, parameters) {
|
|
116
116
|
// The execute endpoint expects a UUID. If toolId is a name slug,
|
|
117
|
-
// resolve
|
|
117
|
+
// resolve by listing the gateway's tools and matching either
|
|
118
|
+
// the raw tool name, its kebab slug, OR the new
|
|
119
|
+
// `${gateway-slug}-${tool-slug}` composed form (with shared
|
|
120
|
+
// head segments deduped — the same rule the backend uses to
|
|
121
|
+
// generate SKILL.md names).
|
|
118
122
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
119
123
|
let resolvedId = toolId;
|
|
120
124
|
if (!uuidRegex.test(toolId)) {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
125
|
+
const gateway = await this.request(`/gateways/${gatewayId}`).catch(() => null);
|
|
126
|
+
const gatewaySlug = gateway?.data?.endpoint?.replace(/^\/+/, '') || '';
|
|
127
|
+
// Walk every page — server caps limit at 100.
|
|
128
|
+
const allTools = [];
|
|
129
|
+
let page = 1;
|
|
130
|
+
while (true) {
|
|
131
|
+
const resp = await this.request(`/gateways/${gatewayId}/tools?limit=100&page=${page}`);
|
|
132
|
+
const data = resp?.data || resp;
|
|
133
|
+
const items = data?.gatewayTools || data?.data || (Array.isArray(data) ? data : []);
|
|
134
|
+
allTools.push(...(Array.isArray(items) ? items : []));
|
|
135
|
+
const totalPages = data?.totalPages ?? 1;
|
|
136
|
+
if (page >= totalPages || items.length === 0)
|
|
137
|
+
break;
|
|
138
|
+
page++;
|
|
139
|
+
if (page > 50)
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
const slugify = (s) => (s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
143
|
+
const dedupeShared = (head, tail) => {
|
|
144
|
+
const h = head.split('-').filter(Boolean);
|
|
145
|
+
const t = tail.split('-').filter(Boolean);
|
|
146
|
+
let i = 0;
|
|
147
|
+
while (i < t.length && i < h.length && t[i] === h[i])
|
|
148
|
+
i++;
|
|
149
|
+
return t.slice(i).join('-');
|
|
150
|
+
};
|
|
151
|
+
// Mirror the server's skill-generator truncation when
|
|
152
|
+
// `${gateway}-${suffix}` exceeds 64 chars: drop kebab
|
|
153
|
+
// segments off the *head* of the suffix until the whole
|
|
154
|
+
// thing fits, falling back to a tail substring if a single
|
|
155
|
+
// segment is itself longer than the remaining budget. Cutting
|
|
156
|
+
// from the head keeps the unique tail of long method names
|
|
157
|
+
// (e.g. ~40 google-translate gRPC methods that all share a
|
|
158
|
+
// long prefix) addressable from the CLI.
|
|
159
|
+
const composeAndTruncate = (gw, suffix) => {
|
|
160
|
+
if (!gw)
|
|
161
|
+
return suffix;
|
|
162
|
+
const combined = `${gw}-${suffix}`;
|
|
163
|
+
if (combined.length <= 64)
|
|
164
|
+
return combined;
|
|
165
|
+
const room = 64 - gw.length - 1;
|
|
166
|
+
if (room <= 0)
|
|
167
|
+
return gw.slice(0, 64);
|
|
168
|
+
const segments = suffix.split('-').filter(Boolean);
|
|
169
|
+
let trimmed = segments.join('-');
|
|
170
|
+
while (trimmed.length > room && segments.length > 1) {
|
|
171
|
+
segments.shift();
|
|
172
|
+
trimmed = segments.join('-');
|
|
173
|
+
}
|
|
174
|
+
if (trimmed.length > room)
|
|
175
|
+
trimmed = trimmed.slice(-room);
|
|
176
|
+
return `${gw}-${trimmed.replace(/^-+|-+$/g, '')}`;
|
|
177
|
+
};
|
|
178
|
+
const match = allTools.find((gt) => {
|
|
124
179
|
const t = gt.tool || gt;
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
if (!t)
|
|
181
|
+
return false;
|
|
182
|
+
const rawName = t.name || '';
|
|
183
|
+
const slug = slugify(rawName);
|
|
184
|
+
const dedupedSuffix = dedupeShared(gatewaySlug, slug);
|
|
185
|
+
const composed = gatewaySlug
|
|
186
|
+
? `${gatewaySlug}-${dedupedSuffix}`
|
|
187
|
+
: slug;
|
|
188
|
+
const truncated = composeAndTruncate(gatewaySlug, dedupedSuffix);
|
|
189
|
+
return (rawName === toolId ||
|
|
190
|
+
slug === toolId ||
|
|
191
|
+
composed === toolId ||
|
|
192
|
+
truncated === toolId);
|
|
127
193
|
});
|
|
128
194
|
if (!match) {
|
|
129
195
|
throw new Error(`Tool "${toolId}" not found in gateway ${gatewayId}`);
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { installSkills, removeSkills, listInstalledSkills } from './installer.js
|
|
|
6
6
|
import { loadConfig, resolveTargets } from './config.js';
|
|
7
7
|
import { generateMetaSkill } from './meta-skill.js';
|
|
8
8
|
import { selectInstallTargetsAuto, selectInstallTargetsInteractive, } from './target-selector.js';
|
|
9
|
-
const VERSION = '1.0.
|
|
9
|
+
const VERSION = '1.0.12';
|
|
10
10
|
function printHelp() {
|
|
11
11
|
console.log(`
|
|
12
12
|
almyty Skills CLI v${VERSION}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almyty/skills",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Install API skills into AI coding agents
|
|
3
|
+
"version": "1.0.14",
|
|
4
|
+
"description": "Install API skills into AI coding agents — Claude Code, Cursor, Windsurf, Copilot, Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -41,4 +41,4 @@
|
|
|
41
41
|
"typescript": "^5.3.0",
|
|
42
42
|
"vitest": "^4.1.0"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { parseRef } from '../client.js';
|
|
3
|
-
import { generateMetaSkill } from '../meta-skill.js';
|
|
4
|
-
import { loadConfig } from '../config.js';
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// parseRef()
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
describe('parseRef', () => {
|
|
9
|
-
it('parses @org/gateway as a gateway ref', () => {
|
|
10
|
-
const result = parseRef('@org/gateway');
|
|
11
|
-
expect(result).toEqual({
|
|
12
|
-
type: 'gateway',
|
|
13
|
-
orgSlug: 'org',
|
|
14
|
-
gatewaySlug: 'gateway',
|
|
15
|
-
raw: '@org/gateway',
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
it('parses @org/gateway/skill as a skill ref', () => {
|
|
19
|
-
const result = parseRef('@org/gateway/skill');
|
|
20
|
-
expect(result).toEqual({
|
|
21
|
-
type: 'skill',
|
|
22
|
-
orgSlug: 'org',
|
|
23
|
-
gatewaySlug: 'gateway',
|
|
24
|
-
skillName: 'skill',
|
|
25
|
-
raw: '@org/gateway/skill',
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
it('parses a UUID string as a uuid ref', () => {
|
|
29
|
-
const uuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
30
|
-
const result = parseRef(uuid);
|
|
31
|
-
expect(result).toEqual({
|
|
32
|
-
type: 'uuid',
|
|
33
|
-
uuid,
|
|
34
|
-
raw: uuid,
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
it('parses uppercase UUID correctly', () => {
|
|
38
|
-
const uuid = 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890';
|
|
39
|
-
const result = parseRef(uuid);
|
|
40
|
-
expect(result.type).toBe('uuid');
|
|
41
|
-
expect(result.uuid).toBe(uuid);
|
|
42
|
-
});
|
|
43
|
-
it('returns search for a random string', () => {
|
|
44
|
-
const result = parseRef('some-random-query');
|
|
45
|
-
expect(result).toEqual({
|
|
46
|
-
type: 'search',
|
|
47
|
-
raw: 'some-random-query',
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
it('returns search for an empty string', () => {
|
|
51
|
-
const result = parseRef('');
|
|
52
|
-
expect(result).toEqual({
|
|
53
|
-
type: 'search',
|
|
54
|
-
raw: '',
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
it('returns search for @ with only one segment', () => {
|
|
58
|
-
const result = parseRef('@org');
|
|
59
|
-
expect(result).toEqual({
|
|
60
|
-
type: 'search',
|
|
61
|
-
raw: '@org',
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
it('returns search for @ with more than three segments', () => {
|
|
65
|
-
const result = parseRef('@org/gw/skill/extra');
|
|
66
|
-
expect(result).toEqual({
|
|
67
|
-
type: 'search',
|
|
68
|
-
raw: '@org/gw/skill/extra',
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// generateMetaSkill()
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
describe('generateMetaSkill', () => {
|
|
76
|
-
it('returns a SkillFile with name "skills" and fileName "almyty-skills"', () => {
|
|
77
|
-
const skill = generateMetaSkill();
|
|
78
|
-
expect(skill.name).toBe('skills');
|
|
79
|
-
expect(skill.fileName).toBe('almyty-skills');
|
|
80
|
-
});
|
|
81
|
-
it('content includes npx @almyty/skills commands', () => {
|
|
82
|
-
const { content } = generateMetaSkill();
|
|
83
|
-
expect(content).toContain('npx @almyty/skills');
|
|
84
|
-
});
|
|
85
|
-
it('content references daemon command', () => {
|
|
86
|
-
const { content } = generateMetaSkill();
|
|
87
|
-
expect(content).toContain('daemon');
|
|
88
|
-
});
|
|
89
|
-
it('content references search command', () => {
|
|
90
|
-
const { content } = generateMetaSkill();
|
|
91
|
-
expect(content).toContain('search');
|
|
92
|
-
});
|
|
93
|
-
it('content references install command', () => {
|
|
94
|
-
const { content } = generateMetaSkill();
|
|
95
|
-
expect(content).toContain('install');
|
|
96
|
-
});
|
|
97
|
-
it('content references run command', () => {
|
|
98
|
-
const { content } = generateMetaSkill();
|
|
99
|
-
expect(content).toContain('run');
|
|
100
|
-
});
|
|
101
|
-
it('content includes SKILL.md front-matter', () => {
|
|
102
|
-
const { content } = generateMetaSkill();
|
|
103
|
-
expect(content).toContain('---');
|
|
104
|
-
expect(content).toContain('name: almyty-skills');
|
|
105
|
-
expect(content).toContain('description:');
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// loadConfig()
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
describe('loadConfig', () => {
|
|
112
|
-
const originalEnv = process.env;
|
|
113
|
-
beforeEach(() => {
|
|
114
|
-
// Reset env between tests
|
|
115
|
-
process.env = { ...originalEnv };
|
|
116
|
-
delete process.env.APIFAI_SKILLS_DIR;
|
|
117
|
-
});
|
|
118
|
-
afterEach(() => {
|
|
119
|
-
process.env = originalEnv;
|
|
120
|
-
vi.restoreAllMocks();
|
|
121
|
-
});
|
|
122
|
-
it('returns empty config when no .almytyrc exists and no env var', () => {
|
|
123
|
-
// Use a temp directory that definitely has no .almytyrc
|
|
124
|
-
const config = loadConfig('/tmp/nonexistent-dir-' + Date.now());
|
|
125
|
-
expect(config).toEqual({});
|
|
126
|
-
});
|
|
127
|
-
it('uses APIFAI_SKILLS_DIR env var when set', () => {
|
|
128
|
-
process.env.APIFAI_SKILLS_DIR = '/custom/skills/dir';
|
|
129
|
-
const config = loadConfig();
|
|
130
|
-
expect(config).toEqual({ skillsDir: '/custom/skills/dir' });
|
|
131
|
-
});
|
|
132
|
-
it('env var takes precedence over .almytyrc files', () => {
|
|
133
|
-
process.env.APIFAI_SKILLS_DIR = '/env/override';
|
|
134
|
-
// Even if we pass a projectDir, env should take priority
|
|
135
|
-
const config = loadConfig('/some/project');
|
|
136
|
-
expect(config.skillsDir).toBe('/env/override');
|
|
137
|
-
});
|
|
138
|
-
it('returns empty config for a directory with no rc file and no home rc', () => {
|
|
139
|
-
// Use a deeply nested temp path that certainly has no .almytyrc anywhere
|
|
140
|
-
// and also won't match ~/.almytyrc (already handled by the missing file)
|
|
141
|
-
const fakePath = `/tmp/almyty-test-${Date.now()}/deeply/nested/project`;
|
|
142
|
-
const config = loadConfig(fakePath);
|
|
143
|
-
expect(config).toEqual({});
|
|
144
|
-
});
|
|
145
|
-
});
|