@almyty/skills 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/dist/__tests__/client.test.d.ts +1 -0
- package/dist/__tests__/client.test.js +145 -0
- package/dist/agents.d.ts +37 -0
- package/dist/agents.js +123 -0
- package/dist/auth.d.ts +41 -0
- package/dist/auth.js +117 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.js +128 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +502 -0
- package/dist/installer.d.ts +24 -0
- package/dist/installer.js +55 -0
- package/dist/meta-skill.d.ts +2 -0
- package/dist/meta-skill.js +53 -0
- package/package.json +40 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
});
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent directory detection and mapping.
|
|
3
|
+
*
|
|
4
|
+
* Detects which AI coding agents are present in the current project
|
|
5
|
+
* by looking for their config directories. Skills are installed into
|
|
6
|
+
* agent-specific skill directories.
|
|
7
|
+
*
|
|
8
|
+
* Agent list aligned with https://github.com/vercel-labs/skills
|
|
9
|
+
*/
|
|
10
|
+
export interface AgentTarget {
|
|
11
|
+
name: string;
|
|
12
|
+
configDir: string;
|
|
13
|
+
skillsDir: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Detect which AI agents are configured in the given project directory.
|
|
17
|
+
*/
|
|
18
|
+
export declare function detectAgents(projectDir: string): AgentTarget[];
|
|
19
|
+
/**
|
|
20
|
+
* If no agents are detected, return defaults:
|
|
21
|
+
* - .claude/skills/ (Claude Code native)
|
|
22
|
+
* - .agents/skills/ (cross-client standard per agentskills.io spec)
|
|
23
|
+
*/
|
|
24
|
+
export declare function getDefaultTargets(projectDir: string): AgentTarget[];
|
|
25
|
+
/**
|
|
26
|
+
* Get ALL detected targets plus cross-client .agents/skills/.
|
|
27
|
+
* Used by watch/daemon mode to ensure maximum coverage.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getAllTargets(projectDir: string): AgentTarget[];
|
|
30
|
+
/**
|
|
31
|
+
* Filter agents by name (case-insensitive partial match).
|
|
32
|
+
*/
|
|
33
|
+
export declare function filterAgents(agents: AgentTarget[], filter: string[]): AgentTarget[];
|
|
34
|
+
/**
|
|
35
|
+
* Ensure a skills directory exists.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensureSkillsDir(skillsDir: string): void;
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent directory detection and mapping.
|
|
3
|
+
*
|
|
4
|
+
* Detects which AI coding agents are present in the current project
|
|
5
|
+
* by looking for their config directories. Skills are installed into
|
|
6
|
+
* agent-specific skill directories.
|
|
7
|
+
*
|
|
8
|
+
* Agent list aligned with https://github.com/vercel-labs/skills
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
/**
|
|
13
|
+
* Known agent configurations (30+ agents).
|
|
14
|
+
* Aligned with Vercel's skills CLI.
|
|
15
|
+
* See: https://github.com/vercel-labs/skills/blob/main/src/agents.ts
|
|
16
|
+
*/
|
|
17
|
+
const AGENT_CONFIGS = [
|
|
18
|
+
// --- Major agents ---
|
|
19
|
+
{ name: 'Claude Code', detectDirs: ['.claude'], skillsDir: '.claude/skills' },
|
|
20
|
+
{ name: 'Cursor', detectDirs: ['.cursor', '.cursorrc'], skillsDir: '.agents/skills' },
|
|
21
|
+
{ name: 'GitHub Copilot', detectDirs: ['.github/copilot'], skillsDir: '.agents/skills' },
|
|
22
|
+
{ name: 'Windsurf', detectDirs: ['.windsurf'], skillsDir: '.windsurf/skills' },
|
|
23
|
+
{ name: 'Codex', detectDirs: ['.codex'], skillsDir: '.agents/skills' },
|
|
24
|
+
// --- Additional agents ---
|
|
25
|
+
{ name: 'Amp', detectDirs: ['.amp'], skillsDir: '.agents/skills' },
|
|
26
|
+
{ name: 'Augment', detectDirs: ['.augment'], skillsDir: '.augment/skills' },
|
|
27
|
+
{ name: 'Cline', detectDirs: ['.cline'], skillsDir: '.agents/skills' },
|
|
28
|
+
{ name: 'CodeBuddy', detectDirs: ['.codebuddy'], skillsDir: '.codebuddy/skills' },
|
|
29
|
+
{ name: 'Command Code', detectDirs: ['.commandcode'], skillsDir: '.commandcode/skills' },
|
|
30
|
+
{ name: 'Continue', detectDirs: ['.continue'], skillsDir: '.continue/skills' },
|
|
31
|
+
{ name: 'Cortex', detectDirs: ['.cortex'], skillsDir: '.cortex/skills' },
|
|
32
|
+
{ name: 'Droid (Factory)', detectDirs: ['.factory'], skillsDir: '.factory/skills' },
|
|
33
|
+
{ name: 'Gemini CLI', detectDirs: ['.gemini'], skillsDir: '.agents/skills' },
|
|
34
|
+
{ name: 'Goose', detectDirs: ['.goose'], skillsDir: '.goose/skills' },
|
|
35
|
+
{ name: 'Junie', detectDirs: ['.junie'], skillsDir: '.junie/skills' },
|
|
36
|
+
{ name: 'Kilo Code', detectDirs: ['.kilocode'], skillsDir: '.kilocode/skills' },
|
|
37
|
+
{ name: 'Kiro CLI', detectDirs: ['.kiro'], skillsDir: '.kiro/skills' },
|
|
38
|
+
{ name: 'Kode', detectDirs: ['.kode'], skillsDir: '.kode/skills' },
|
|
39
|
+
{ name: 'MCPJam', detectDirs: ['.mcpjam'], skillsDir: '.mcpjam/skills' },
|
|
40
|
+
{ name: 'Mistral Vibe', detectDirs: ['.vibe'], skillsDir: '.vibe/skills' },
|
|
41
|
+
{ name: 'Mux', detectDirs: ['.mux'], skillsDir: '.mux/skills' },
|
|
42
|
+
{ name: 'OpenCode', detectDirs: ['.opencode'], skillsDir: '.agents/skills' },
|
|
43
|
+
{ name: 'OpenHands', detectDirs: ['.openhands'], skillsDir: '.openhands/skills' },
|
|
44
|
+
{ name: 'Pi', detectDirs: ['.pi'], skillsDir: '.pi/skills' },
|
|
45
|
+
{ name: 'Qoder', detectDirs: ['.qoder'], skillsDir: '.qoder/skills' },
|
|
46
|
+
{ name: 'Qwen Code', detectDirs: ['.qwen'], skillsDir: '.qwen/skills' },
|
|
47
|
+
{ name: 'Roo Code', detectDirs: ['.roo'], skillsDir: '.roo/skills' },
|
|
48
|
+
{ name: 'Trae', detectDirs: ['.trae'], skillsDir: '.trae/skills' },
|
|
49
|
+
{ name: 'Zencoder', detectDirs: ['.zencoder'], skillsDir: '.zencoder/skills' },
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Detect which AI agents are configured in the given project directory.
|
|
53
|
+
*/
|
|
54
|
+
export function detectAgents(projectDir) {
|
|
55
|
+
const agents = [];
|
|
56
|
+
const seenSkillsDirs = new Set();
|
|
57
|
+
for (const config of AGENT_CONFIGS) {
|
|
58
|
+
const detected = config.detectDirs.some(dir => existsSync(join(projectDir, dir)));
|
|
59
|
+
if (detected) {
|
|
60
|
+
const fullSkillsDir = join(projectDir, config.skillsDir);
|
|
61
|
+
// Avoid duplicates when multiple agents share the same skills dir
|
|
62
|
+
if (!seenSkillsDirs.has(fullSkillsDir)) {
|
|
63
|
+
seenSkillsDirs.add(fullSkillsDir);
|
|
64
|
+
agents.push({
|
|
65
|
+
name: config.name,
|
|
66
|
+
configDir: config.detectDirs[0],
|
|
67
|
+
skillsDir: fullSkillsDir,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return agents;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* If no agents are detected, return defaults:
|
|
76
|
+
* - .claude/skills/ (Claude Code native)
|
|
77
|
+
* - .agents/skills/ (cross-client standard per agentskills.io spec)
|
|
78
|
+
*/
|
|
79
|
+
export function getDefaultTargets(projectDir) {
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
name: 'Claude Code',
|
|
83
|
+
configDir: '.claude',
|
|
84
|
+
skillsDir: join(projectDir, '.claude/skills'),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Universal (.agents/skills)',
|
|
88
|
+
configDir: '.agents',
|
|
89
|
+
skillsDir: join(projectDir, '.agents/skills'),
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get ALL detected targets plus cross-client .agents/skills/.
|
|
95
|
+
* Used by watch/daemon mode to ensure maximum coverage.
|
|
96
|
+
*/
|
|
97
|
+
export function getAllTargets(projectDir) {
|
|
98
|
+
const detected = detectAgents(projectDir);
|
|
99
|
+
const seenDirs = new Set(detected.map(a => a.skillsDir));
|
|
100
|
+
// Always include cross-client .agents/skills/ if not already covered
|
|
101
|
+
const universalDir = join(projectDir, '.agents/skills');
|
|
102
|
+
if (!seenDirs.has(universalDir)) {
|
|
103
|
+
detected.push({
|
|
104
|
+
name: 'Universal (.agents/skills)',
|
|
105
|
+
configDir: '.agents',
|
|
106
|
+
skillsDir: universalDir,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return detected;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Filter agents by name (case-insensitive partial match).
|
|
113
|
+
*/
|
|
114
|
+
export function filterAgents(agents, filter) {
|
|
115
|
+
const normalized = filter.map(f => f.toLowerCase());
|
|
116
|
+
return agents.filter(a => normalized.some(f => a.name.toLowerCase().includes(f)));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Ensure a skills directory exists.
|
|
120
|
+
*/
|
|
121
|
+
export function ensureSkillsDir(skillsDir) {
|
|
122
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
123
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication helpers for the almyty skills CLI.
|
|
3
|
+
*
|
|
4
|
+
* Shares credentials with @almyty/mcp-server via ~/.almyty/credentials.json.
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* 1. Environment variable (APIFAI_TOKEN) — for CI/scripts
|
|
8
|
+
* 2. Stored credentials (~/.almyty/credentials.json) — for interactive use
|
|
9
|
+
* 3. Interactive login (npx @almyty/skills login) — stores credentials
|
|
10
|
+
*/
|
|
11
|
+
interface StoredCredentials {
|
|
12
|
+
url: string;
|
|
13
|
+
token: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
expiresAt?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Load stored credentials from ~/.almyty/credentials.json
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadCredentials(): StoredCredentials | null;
|
|
21
|
+
/**
|
|
22
|
+
* Save credentials to ~/.almyty/credentials.json
|
|
23
|
+
*/
|
|
24
|
+
export declare function saveCredentials(creds: StoredCredentials): void;
|
|
25
|
+
/**
|
|
26
|
+
* Remove stored credentials
|
|
27
|
+
*/
|
|
28
|
+
export declare function logout(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve token: env var > stored credentials
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveAuth(): {
|
|
33
|
+
url: string;
|
|
34
|
+
token: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Interactive login: prompts for email/password, authenticates against
|
|
38
|
+
* the almyty backend, and stores the JWT token.
|
|
39
|
+
*/
|
|
40
|
+
export declare function login(baseUrl: string): Promise<void>;
|
|
41
|
+
export {};
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication helpers for the almyty skills CLI.
|
|
3
|
+
*
|
|
4
|
+
* Shares credentials with @almyty/mcp-server via ~/.almyty/credentials.json.
|
|
5
|
+
*
|
|
6
|
+
* Supports:
|
|
7
|
+
* 1. Environment variable (APIFAI_TOKEN) — for CI/scripts
|
|
8
|
+
* 2. Stored credentials (~/.almyty/credentials.json) — for interactive use
|
|
9
|
+
* 3. Interactive login (npx @almyty/skills login) — stores credentials
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from 'fs';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { createInterface } from 'readline';
|
|
15
|
+
const CREDENTIALS_DIR = join(homedir(), '.almyty');
|
|
16
|
+
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, 'credentials.json');
|
|
17
|
+
/**
|
|
18
|
+
* Load stored credentials from ~/.almyty/credentials.json
|
|
19
|
+
*/
|
|
20
|
+
export function loadCredentials() {
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(CREDENTIALS_FILE))
|
|
23
|
+
return null;
|
|
24
|
+
const data = readFileSync(CREDENTIALS_FILE, 'utf-8');
|
|
25
|
+
return JSON.parse(data);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Save credentials to ~/.almyty/credentials.json
|
|
33
|
+
*/
|
|
34
|
+
export function saveCredentials(creds) {
|
|
35
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
36
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Remove stored credentials
|
|
40
|
+
*/
|
|
41
|
+
export function logout() {
|
|
42
|
+
try {
|
|
43
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
44
|
+
unlinkSync(CREDENTIALS_FILE);
|
|
45
|
+
console.log('Logged out. Credentials removed.');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log('No stored credentials found.');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
console.error('Failed to remove credentials.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve token: env var > stored credentials
|
|
57
|
+
*/
|
|
58
|
+
export function resolveAuth() {
|
|
59
|
+
const envToken = process.env.APIFAI_TOKEN;
|
|
60
|
+
const envUrl = process.env.APIFAI_URL || 'https://api.almyty.com';
|
|
61
|
+
if (envToken) {
|
|
62
|
+
return { url: envUrl, token: envToken };
|
|
63
|
+
}
|
|
64
|
+
const creds = loadCredentials();
|
|
65
|
+
if (creds?.token) {
|
|
66
|
+
return { url: creds.url, token: creds.token };
|
|
67
|
+
}
|
|
68
|
+
console.error('Not authenticated. Run one of:');
|
|
69
|
+
console.error(' npx @almyty/skills login');
|
|
70
|
+
console.error(' export APIFAI_TOKEN=<your-token>');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Interactive login: prompts for email/password, authenticates against
|
|
75
|
+
* the almyty backend, and stores the JWT token.
|
|
76
|
+
*/
|
|
77
|
+
export async function login(baseUrl) {
|
|
78
|
+
const rl = createInterface({
|
|
79
|
+
input: process.stdin,
|
|
80
|
+
output: process.stderr,
|
|
81
|
+
});
|
|
82
|
+
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
|
|
83
|
+
try {
|
|
84
|
+
console.error(`Logging in to ${baseUrl}...\n`);
|
|
85
|
+
const email = await ask('Email: ');
|
|
86
|
+
const password = await ask('Password: ');
|
|
87
|
+
console.error('\nAuthenticating...');
|
|
88
|
+
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ email, password }),
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errBody = await response.json().catch(() => ({ message: 'Authentication failed' }));
|
|
95
|
+
console.error(`Error: ${errBody.message || 'Authentication failed'}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
const token = data.accessToken || data.token;
|
|
100
|
+
if (!token) {
|
|
101
|
+
console.error('Error: No token received from server');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
saveCredentials({
|
|
105
|
+
url: baseUrl,
|
|
106
|
+
token,
|
|
107
|
+
email,
|
|
108
|
+
});
|
|
109
|
+
console.error(`\n✓ Logged in as ${email}`);
|
|
110
|
+
console.error(` Credentials saved to ${CREDENTIALS_FILE}`);
|
|
111
|
+
console.error('\nYou can now install skills:');
|
|
112
|
+
console.error(' npx @almyty/skills install --gateway <id>');
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
rl.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface SkillFile {
|
|
2
|
+
name: string;
|
|
3
|
+
fileName: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GatewayInfo {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ParsedRef {
|
|
12
|
+
type: 'gateway' | 'skill' | 'search' | 'uuid';
|
|
13
|
+
orgSlug?: string;
|
|
14
|
+
gatewaySlug?: string;
|
|
15
|
+
skillName?: string;
|
|
16
|
+
uuid?: string;
|
|
17
|
+
raw: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseRef(ref: string): ParsedRef;
|
|
20
|
+
export declare class AlmytyClient {
|
|
21
|
+
private baseUrl;
|
|
22
|
+
private token;
|
|
23
|
+
constructor(baseUrl: string, token: string);
|
|
24
|
+
private request;
|
|
25
|
+
private post;
|
|
26
|
+
resolveGateway(orgSlug: string, gatewaySlug: string): Promise<GatewayInfo>;
|
|
27
|
+
fetchSkills(gatewayIdOrRef: string): Promise<SkillFile[]>;
|
|
28
|
+
fetchGateway(gatewayIdOrRef: string): Promise<GatewayInfo>;
|
|
29
|
+
listGateways(): Promise<GatewayInfo[]>;
|
|
30
|
+
searchSkills(query: string): Promise<any[]>;
|
|
31
|
+
fetchAllSkills(): Promise<SkillFile[]>;
|
|
32
|
+
executeSkill(gatewayId: string, toolId: string, parameters: Record<string, any>): Promise<any>;
|
|
33
|
+
private resolveRef;
|
|
34
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function parseRef(ref) {
|
|
2
|
+
if (!ref)
|
|
3
|
+
return { type: 'search', raw: ref };
|
|
4
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
if (uuidRegex.test(ref))
|
|
6
|
+
return { type: 'uuid', uuid: ref, raw: ref };
|
|
7
|
+
if (ref.startsWith('@')) {
|
|
8
|
+
const parts = ref.slice(1).split('/');
|
|
9
|
+
if (parts.length === 3) {
|
|
10
|
+
return { type: 'skill', orgSlug: parts[0], gatewaySlug: parts[1], skillName: parts[2], raw: ref };
|
|
11
|
+
}
|
|
12
|
+
if (parts.length === 2) {
|
|
13
|
+
return { type: 'gateway', orgSlug: parts[0], gatewaySlug: parts[1], raw: ref };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { type: 'search', raw: ref };
|
|
17
|
+
}
|
|
18
|
+
export class AlmytyClient {
|
|
19
|
+
baseUrl;
|
|
20
|
+
token;
|
|
21
|
+
constructor(baseUrl, token) {
|
|
22
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
23
|
+
this.token = token;
|
|
24
|
+
}
|
|
25
|
+
async request(path) {
|
|
26
|
+
const url = `${this.baseUrl}/api${path}`;
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
headers: {
|
|
29
|
+
'Authorization': `Bearer ${this.token}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
if (response.status === 401) {
|
|
35
|
+
throw new Error('Authentication failed. Run: npx @almyty/skills login');
|
|
36
|
+
}
|
|
37
|
+
const text = await response.text();
|
|
38
|
+
throw new Error(`API error (${response.status}): ${text}`);
|
|
39
|
+
}
|
|
40
|
+
return response.json();
|
|
41
|
+
}
|
|
42
|
+
async post(path, body) {
|
|
43
|
+
const url = `${this.baseUrl}/api${path}`;
|
|
44
|
+
const resp = await fetch(url, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
'Authorization': `Bearer ${this.token}`,
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
});
|
|
52
|
+
if (!resp.ok) {
|
|
53
|
+
const text = await resp.text();
|
|
54
|
+
throw new Error(`API error ${resp.status}: ${text}`);
|
|
55
|
+
}
|
|
56
|
+
return resp.json();
|
|
57
|
+
}
|
|
58
|
+
async resolveGateway(orgSlug, gatewaySlug) {
|
|
59
|
+
const data = await this.request(`/gateways/resolve/${orgSlug}/${gatewaySlug}`);
|
|
60
|
+
const gw = data?.data || data;
|
|
61
|
+
return {
|
|
62
|
+
id: gw.id,
|
|
63
|
+
name: gw.name,
|
|
64
|
+
type: gw.type,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async fetchSkills(gatewayIdOrRef) {
|
|
68
|
+
const gatewayId = await this.resolveRef(gatewayIdOrRef);
|
|
69
|
+
const data = await this.request(`/gateways/${gatewayId}/skills/individual`);
|
|
70
|
+
return data?.data?.skills || [];
|
|
71
|
+
}
|
|
72
|
+
async fetchGateway(gatewayIdOrRef) {
|
|
73
|
+
if (gatewayIdOrRef.startsWith('@')) {
|
|
74
|
+
const parsed = parseRef(gatewayIdOrRef);
|
|
75
|
+
if (parsed.orgSlug && parsed.gatewaySlug) {
|
|
76
|
+
return this.resolveGateway(parsed.orgSlug, parsed.gatewaySlug);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const data = await this.request(`/gateways/${gatewayIdOrRef}`);
|
|
80
|
+
const gw = data?.data || data;
|
|
81
|
+
return {
|
|
82
|
+
id: gw.id,
|
|
83
|
+
name: gw.name,
|
|
84
|
+
type: gw.type,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
async listGateways() {
|
|
88
|
+
const data = await this.request('/gateways');
|
|
89
|
+
const gateways = data?.data?.data?.gateways || data?.data?.data || data?.data || [];
|
|
90
|
+
return gateways.map((gw) => ({
|
|
91
|
+
id: gw.id,
|
|
92
|
+
name: gw.name,
|
|
93
|
+
type: gw.type,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
async searchSkills(query) {
|
|
97
|
+
const resp = await this.request(`/gateways/skills/search?q=${encodeURIComponent(query)}`);
|
|
98
|
+
return resp?.data || [];
|
|
99
|
+
}
|
|
100
|
+
async fetchAllSkills() {
|
|
101
|
+
const resp = await this.request('/gateways/all-skills');
|
|
102
|
+
const gateways = resp?.data || [];
|
|
103
|
+
const skills = [];
|
|
104
|
+
for (const gw of gateways) {
|
|
105
|
+
for (const skill of gw.skills || []) {
|
|
106
|
+
skill.gateway = gw.gatewayName;
|
|
107
|
+
skill.gatewayId = gw.gatewayId;
|
|
108
|
+
skill.orgSlug = gw.orgSlug;
|
|
109
|
+
skill.gatewaySlug = gw.gatewaySlug;
|
|
110
|
+
skills.push(skill);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return skills;
|
|
114
|
+
}
|
|
115
|
+
async executeSkill(gatewayId, toolId, parameters) {
|
|
116
|
+
return this.post(`/gateways/${gatewayId}/skills/${toolId}/execute`, { parameters });
|
|
117
|
+
}
|
|
118
|
+
async resolveRef(ref) {
|
|
119
|
+
if (ref.startsWith('@')) {
|
|
120
|
+
const parsed = parseRef(ref);
|
|
121
|
+
if (parsed.orgSlug && parsed.gatewaySlug) {
|
|
122
|
+
const gw = await this.resolveGateway(parsed.orgSlug, parsed.gatewaySlug);
|
|
123
|
+
return gw.id;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return ref;
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AgentTarget } from './agents.js';
|
|
2
|
+
export interface AlmytyConfig {
|
|
3
|
+
skillsDir?: string;
|
|
4
|
+
agents?: string[];
|
|
5
|
+
url?: string;
|
|
6
|
+
interval?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function loadConfig(projectDir?: string): AlmytyConfig;
|
|
9
|
+
export declare function resolveTargets(projectDir: string, config: AlmytyConfig): AgentTarget[];
|