@jaybeeuu/agent-uplink 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/.github/workflows/ci.yml +129 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/eslint.config.js +15 -0
- package/package.json +58 -0
- package/src/capabilities.test.ts +139 -0
- package/src/capabilities.ts +141 -0
- package/src/cli.ts +35 -0
- package/src/commands/capability.ts +98 -0
- package/src/commands/config.ts +108 -0
- package/src/commands/install.test.ts +155 -0
- package/src/commands/install.ts +323 -0
- package/src/commands/mcp.ts +35 -0
- package/src/commands/sync.ts +49 -0
- package/src/config.test.ts +84 -0
- package/src/config.ts +113 -0
- package/src/editor.ts +47 -0
- package/src/git.ts +84 -0
- package/src/logger.ts +60 -0
- package/src/mcp.ts +156 -0
- package/src/types.ts +40 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["**"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["**"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
install-and-build:
|
|
11
|
+
name: Install & Build
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: pnpm/action-setup@v4
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: 22
|
|
24
|
+
cache: pnpm
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: pnpm install --frozen-lockfile
|
|
28
|
+
|
|
29
|
+
- name: Cache node_modules
|
|
30
|
+
uses: actions/cache/save@v4
|
|
31
|
+
with:
|
|
32
|
+
path: node_modules
|
|
33
|
+
key: node_modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
34
|
+
|
|
35
|
+
- name: Build
|
|
36
|
+
run: pnpm build
|
|
37
|
+
|
|
38
|
+
- name: Cache build artifacts
|
|
39
|
+
uses: actions/cache/save@v4
|
|
40
|
+
with:
|
|
41
|
+
path: dist/
|
|
42
|
+
key: dist-${{ runner.os }}-${{ github.sha }}
|
|
43
|
+
|
|
44
|
+
lint:
|
|
45
|
+
name: Lint
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
needs: install-and-build
|
|
48
|
+
permissions:
|
|
49
|
+
contents: read
|
|
50
|
+
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
|
|
54
|
+
- uses: pnpm/action-setup@v4
|
|
55
|
+
|
|
56
|
+
- uses: actions/setup-node@v4
|
|
57
|
+
with:
|
|
58
|
+
node-version: 22
|
|
59
|
+
|
|
60
|
+
- name: Restore node_modules
|
|
61
|
+
uses: actions/cache/restore@v4
|
|
62
|
+
with:
|
|
63
|
+
path: node_modules
|
|
64
|
+
key: node_modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
65
|
+
|
|
66
|
+
- name: Lint
|
|
67
|
+
run: pnpm lint
|
|
68
|
+
|
|
69
|
+
typecheck:
|
|
70
|
+
name: Typecheck
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
needs: install-and-build
|
|
73
|
+
permissions:
|
|
74
|
+
contents: read
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
|
|
79
|
+
- uses: pnpm/action-setup@v4
|
|
80
|
+
|
|
81
|
+
- uses: actions/setup-node@v4
|
|
82
|
+
with:
|
|
83
|
+
node-version: 22
|
|
84
|
+
|
|
85
|
+
- name: Restore node_modules
|
|
86
|
+
uses: actions/cache/restore@v4
|
|
87
|
+
with:
|
|
88
|
+
path: node_modules
|
|
89
|
+
key: node_modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
90
|
+
|
|
91
|
+
- name: Restore build artifacts
|
|
92
|
+
uses: actions/cache/restore@v4
|
|
93
|
+
with:
|
|
94
|
+
path: dist/
|
|
95
|
+
key: dist-${{ runner.os }}-${{ github.sha }}
|
|
96
|
+
|
|
97
|
+
- name: Typecheck
|
|
98
|
+
run: pnpm typecheck
|
|
99
|
+
|
|
100
|
+
test:
|
|
101
|
+
name: Test
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
needs: install-and-build
|
|
104
|
+
permissions:
|
|
105
|
+
contents: read
|
|
106
|
+
|
|
107
|
+
steps:
|
|
108
|
+
- uses: actions/checkout@v4
|
|
109
|
+
|
|
110
|
+
- uses: pnpm/action-setup@v4
|
|
111
|
+
|
|
112
|
+
- uses: actions/setup-node@v4
|
|
113
|
+
with:
|
|
114
|
+
node-version: 22
|
|
115
|
+
|
|
116
|
+
- name: Restore node_modules
|
|
117
|
+
uses: actions/cache/restore@v4
|
|
118
|
+
with:
|
|
119
|
+
path: node_modules
|
|
120
|
+
key: node_modules-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
121
|
+
|
|
122
|
+
- name: Restore build artifacts
|
|
123
|
+
uses: actions/cache/restore@v4
|
|
124
|
+
with:
|
|
125
|
+
path: dist/
|
|
126
|
+
key: dist-${{ runner.os }}-${{ github.sha }}
|
|
127
|
+
|
|
128
|
+
- name: Test
|
|
129
|
+
run: pnpm test
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Josh Bickley-Wallace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# agent-uplink
|
|
2
|
+
|
|
3
|
+
A CLI and MCP server for managing AI capabilities — skills, agents, and instructions — for use with GitHub Copilot and other AI tooling.
|
|
4
|
+
|
|
5
|
+
Uses standard file structures (`.agents/`) rather than tool-specific directories, and supports MCP (Model Context Protocol) so AI agents can manage capabilities directly.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add -g @jaybeeuu/agent-uplink
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
# Create a skill
|
|
17
|
+
uplink skill create my-skill
|
|
18
|
+
|
|
19
|
+
# Edit an existing instruction
|
|
20
|
+
uplink instruction edit coding-style
|
|
21
|
+
|
|
22
|
+
# List all agents
|
|
23
|
+
uplink agent list
|
|
24
|
+
|
|
25
|
+
# Sync changes to git remote
|
|
26
|
+
uplink sync
|
|
27
|
+
|
|
28
|
+
# Install VS Code integration (user-level settings.json + mcp.json)
|
|
29
|
+
uplink install vscode
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### Resource management
|
|
35
|
+
|
|
36
|
+
Each resource type (`skill`, `agent`, `instruction`) supports the same subcommands:
|
|
37
|
+
|
|
38
|
+
| Command | Description |
|
|
39
|
+
|---------|-------------|
|
|
40
|
+
| `uplink <type> create <name>` | Create a new resource and open it in your editor |
|
|
41
|
+
| `uplink <type> edit <name>` | Open an existing resource in your editor |
|
|
42
|
+
| `uplink <type> list` | List all resources of this type |
|
|
43
|
+
| `uplink <type> show <name>` | Print the content of a resource |
|
|
44
|
+
| `uplink <type> delete <name>` | Delete a resource (prompts for confirmation) |
|
|
45
|
+
|
|
46
|
+
### Sync
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
uplink sync # Commit and push all changes
|
|
50
|
+
uplink sync --dry-run # Show what would be synced
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Commits all changes in the capabilities directory with a descriptive message and pushes to the configured remote.
|
|
54
|
+
|
|
55
|
+
### Configuration
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
uplink config list # Show all settings
|
|
59
|
+
uplink config get capabilitiesDir # Get a specific setting
|
|
60
|
+
uplink config set capabilitiesDir ~/caps # Set the capabilities directory
|
|
61
|
+
uplink config set editor "code --wait" # Set your preferred editor
|
|
62
|
+
uplink config set gitRemote upstream # Set git remote name
|
|
63
|
+
uplink config edit # Open config file in your editor
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Configuration is stored at `~/.agent-uplink/config.json`.
|
|
67
|
+
|
|
68
|
+
| Key | Default | Description |
|
|
69
|
+
|-----|---------|-------------|
|
|
70
|
+
| `capabilitiesDir` | `~/.agent-uplink/capabilities` | Directory where capability files are stored |
|
|
71
|
+
| `editor` | `$VISUAL` / `$EDITOR` / `vi` | Editor command to use |
|
|
72
|
+
| `gitRemote` | `origin` | Git remote to push to |
|
|
73
|
+
|
|
74
|
+
### Install / Uninstall integrations
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
uplink install vscode # Configure user-level VS Code / GitHub Copilot
|
|
78
|
+
uplink uninstall vscode # Remove the VS Code integration
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**VS Code** (`uplink install vscode`): Updates `~/.config/Code/User/settings.json` (Linux) with:
|
|
82
|
+
- `chat.agentSkillsLocations` — points at your `skills/` directory
|
|
83
|
+
- `chat.instructionsFilesLocations` — points at your `instructions/` directory
|
|
84
|
+
- `chat.agentFilesLocations` — points at your `agents/` directory
|
|
85
|
+
|
|
86
|
+
Also writes `~/.config/Code/User/mcp.json` to register the uplink MCP server so GitHub Copilot can use it without any further setup.
|
|
87
|
+
|
|
88
|
+
### MCP server
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
uplink mcp start # Start the MCP server (stdio mode)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The MCP server exposes the following tools to AI agents:
|
|
95
|
+
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
|------|-------------|
|
|
98
|
+
| `list_capabilities` | List capabilities of a given type |
|
|
99
|
+
| `get_capability` | Read the content of a capability |
|
|
100
|
+
| `create_capability` | Create a new capability |
|
|
101
|
+
| `update_capability` | Update an existing capability |
|
|
102
|
+
| `delete_capability` | Delete a capability |
|
|
103
|
+
| `sync_capabilities` | Commit and push all changes |
|
|
104
|
+
|
|
105
|
+
After `uplink install vscode`, the MCP server is automatically configured at user level so GitHub Copilot can use it without any further setup.
|
|
106
|
+
|
|
107
|
+
## Capabilities directory structure
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
<capabilitiesDir>/
|
|
111
|
+
├── skills/
|
|
112
|
+
│ └── <name>.md
|
|
113
|
+
├── agents/
|
|
114
|
+
│ └── <name>.md
|
|
115
|
+
└── instructions/
|
|
116
|
+
└── <name>.md
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The capabilities directory can be a git repository, enabling `uplink sync` to commit and push changes across machines.
|
|
120
|
+
|
|
121
|
+
## Debug logging
|
|
122
|
+
|
|
123
|
+
Set the `DEBUG` environment variable to enable verbose output:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
DEBUG=agent-uplink:* # Enable all modules
|
|
127
|
+
DEBUG=agent-uplink:config # Enable config module only
|
|
128
|
+
DEBUG=agent-uplink:install # Enable install module only
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Development
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
pnpm install # Install dependencies
|
|
135
|
+
pnpm build # Compile TypeScript
|
|
136
|
+
pnpm test # Run tests
|
|
137
|
+
pnpm dev -- <args> # Run CLI without building (uses tsx)
|
|
138
|
+
```
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { base } from "@jaybeeuu/eslint-config/base";
|
|
3
|
+
import globals from "globals";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{ ignores: ["dist/**", "node_modules/**"] },
|
|
7
|
+
...base,
|
|
8
|
+
{
|
|
9
|
+
languageOptions: {
|
|
10
|
+
globals: {
|
|
11
|
+
...globals.node,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jaybeeuu/agent-uplink",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI and MCP for managing skills, agents & instructions for AI tooling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"uplink": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/cli.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc --project tsconfig.build.json",
|
|
12
|
+
"dev": "tsx src/cli.ts",
|
|
13
|
+
"start": "node dist/cli.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"lint": "eslint ."
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"ai",
|
|
22
|
+
"copilot",
|
|
23
|
+
"agents",
|
|
24
|
+
"skills",
|
|
25
|
+
"mcp"
|
|
26
|
+
],
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Josh Bickley-Wallace",
|
|
29
|
+
"url": "https://jaybeeuu.dev"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"packageManager": "pnpm@10.32.1",
|
|
33
|
+
"pnpm": {
|
|
34
|
+
"onlyBuiltDependencies": [
|
|
35
|
+
"esbuild"
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
40
|
+
"chalk": "^5.6.2",
|
|
41
|
+
"commander": "^14.0.3",
|
|
42
|
+
"debug": "^4.4.3",
|
|
43
|
+
"enquirer": "^2.4.1",
|
|
44
|
+
"open": "^10.2.0",
|
|
45
|
+
"simple-git": "^3.33.0",
|
|
46
|
+
"zod": "^4.3.6"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@jaybeeuu/eslint-config": "^5.0.0",
|
|
50
|
+
"@types/debug": "^4.1.13",
|
|
51
|
+
"@types/node": "^25.5.0",
|
|
52
|
+
"eslint": "^10.0.3",
|
|
53
|
+
"globals": "^17.4.0",
|
|
54
|
+
"tsx": "^4.21.0",
|
|
55
|
+
"typescript": "^5.9.3",
|
|
56
|
+
"vitest": "^4.1.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import {
|
|
6
|
+
createCapability,
|
|
7
|
+
readCapability,
|
|
8
|
+
updateCapability,
|
|
9
|
+
deleteCapability,
|
|
10
|
+
listCapabilities,
|
|
11
|
+
capabilityExists,
|
|
12
|
+
getCapabilityPath,
|
|
13
|
+
} from "./capabilities.js";
|
|
14
|
+
|
|
15
|
+
const TMP = join(tmpdir(), `uplink-capabilities-test-${String(process.pid)}`);
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mkdirSync(TMP, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
rmSync(TMP, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("createCapability", () => {
|
|
26
|
+
it("creates a skill file with a default template", () => {
|
|
27
|
+
const cap = createCapability(TMP, "skill", "my-skill");
|
|
28
|
+
expect(cap.name).toBe("my-skill");
|
|
29
|
+
expect(cap.type).toBe("skill");
|
|
30
|
+
expect(existsSync(cap.path)).toBe(true);
|
|
31
|
+
expect(cap.content).toContain("name: my-skill");
|
|
32
|
+
expect(cap.content).toContain("description: Describe this skill here.");
|
|
33
|
+
expect(cap.content).toContain("---");
|
|
34
|
+
expect(cap.content).toContain("# my-skill");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("creates a capability with custom content", () => {
|
|
38
|
+
const cap = createCapability(TMP, "skill", "custom", "custom content");
|
|
39
|
+
expect(readFileSync(cap.path, "utf-8")).toBe("custom content");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("creates an agent file with the correct extension", () => {
|
|
43
|
+
const cap = createCapability(TMP, "agent", "my-agent");
|
|
44
|
+
expect(cap.path).toMatch(/\.md$/);
|
|
45
|
+
expect(cap.content).toContain("name: my-agent");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("throws if a capability already exists", () => {
|
|
49
|
+
createCapability(TMP, "skill", "dup");
|
|
50
|
+
expect(() => createCapability(TMP, "skill", "dup")).toThrow(
|
|
51
|
+
/already exists/
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("readCapability", () => {
|
|
57
|
+
it("reads an existing capability", () => {
|
|
58
|
+
createCapability(TMP, "skill", "readable", "hello world");
|
|
59
|
+
const cap = readCapability(TMP, "skill", "readable");
|
|
60
|
+
expect(cap.content).toBe("hello world");
|
|
61
|
+
expect(cap.name).toBe("readable");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws if capability does not exist", () => {
|
|
65
|
+
expect(() => readCapability(TMP, "skill", "missing")).toThrow(/not found/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("updateCapability", () => {
|
|
70
|
+
it("updates the content of an existing capability", () => {
|
|
71
|
+
createCapability(TMP, "instruction", "my-inst", "old content");
|
|
72
|
+
const cap = updateCapability(TMP, "instruction", "my-inst", "new content");
|
|
73
|
+
expect(cap.content).toBe("new content");
|
|
74
|
+
expect(readFileSync(cap.path, "utf-8")).toBe("new content");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("throws if capability does not exist", () => {
|
|
78
|
+
expect(() =>
|
|
79
|
+
updateCapability(TMP, "skill", "missing", "content")
|
|
80
|
+
).toThrow(/not found/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("deleteCapability", () => {
|
|
85
|
+
it("deletes an existing capability", () => {
|
|
86
|
+
const cap = createCapability(TMP, "skill", "deletable", "bye");
|
|
87
|
+
deleteCapability(TMP, "skill", "deletable");
|
|
88
|
+
expect(existsSync(cap.path)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("throws if capability does not exist", () => {
|
|
92
|
+
expect(() => {
|
|
93
|
+
deleteCapability(TMP, "skill", "missing");
|
|
94
|
+
}).toThrow(/not found/);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("listCapabilities", () => {
|
|
99
|
+
it("returns empty array when no capabilities exist", () => {
|
|
100
|
+
expect(listCapabilities(TMP, "skill")).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("lists capabilities sorted alphabetically", () => {
|
|
104
|
+
createCapability(TMP, "skill", "zebra", "z");
|
|
105
|
+
createCapability(TMP, "skill", "alpha", "a");
|
|
106
|
+
createCapability(TMP, "skill", "beta", "b");
|
|
107
|
+
expect(listCapabilities(TMP, "skill")).toEqual(["alpha", "beta", "zebra"]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("only lists capabilities of the correct type", () => {
|
|
111
|
+
createCapability(TMP, "skill", "a-skill", "");
|
|
112
|
+
createCapability(TMP, "agent", "an-agent", "");
|
|
113
|
+
expect(listCapabilities(TMP, "skill")).toEqual(["a-skill"]);
|
|
114
|
+
expect(listCapabilities(TMP, "agent")).toEqual(["an-agent"]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("capabilityExists", () => {
|
|
119
|
+
it("returns true for an existing capability", () => {
|
|
120
|
+
createCapability(TMP, "skill", "exists", "");
|
|
121
|
+
expect(capabilityExists(TMP, "skill", "exists")).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns false for a non-existent capability", () => {
|
|
125
|
+
expect(capabilityExists(TMP, "skill", "nope")).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("getCapabilityPath", () => {
|
|
130
|
+
it("returns the correct path for a skill", () => {
|
|
131
|
+
const path = getCapabilityPath(TMP, "skill", "my-skill");
|
|
132
|
+
expect(path).toBe(join(TMP, "skills", "my-skill.md"));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns the correct path for an agent", () => {
|
|
136
|
+
const path = getCapabilityPath(TMP, "agent", "my-agent");
|
|
137
|
+
expect(path).toBe(join(TMP, "agents", "my-agent.md"));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
} from "fs";
|
|
9
|
+
import { join, extname, basename } from "path";
|
|
10
|
+
import {
|
|
11
|
+
CAPABILITY_DIRS,
|
|
12
|
+
CAPABILITY_EXTENSIONS,
|
|
13
|
+
type Capability,
|
|
14
|
+
type CapabilityType,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
const SKILL_TEMPLATE = (name: string): string =>
|
|
18
|
+
`---\nname: ${name}\ndescription: Describe this skill here.\n---\n\n# ${name}\n\n## Steps\n\n1. Step one\n`;
|
|
19
|
+
|
|
20
|
+
const AGENT_TEMPLATE = (name: string): string =>
|
|
21
|
+
`---\nname: ${name}\ndescription: Describe this agent here.\ntools: []\n---\n\n# ${name}\n\nAgent instructions go here.\n`;
|
|
22
|
+
|
|
23
|
+
const INSTRUCTION_TEMPLATE = (name: string): string =>
|
|
24
|
+
`# ${name}\n\nInstruction content goes here.\n`;
|
|
25
|
+
|
|
26
|
+
const TEMPLATES = {
|
|
27
|
+
skill: SKILL_TEMPLATE,
|
|
28
|
+
agent: AGENT_TEMPLATE,
|
|
29
|
+
instruction: INSTRUCTION_TEMPLATE,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function getCapabilityDir(capabilitiesDir: string, type: CapabilityType): string {
|
|
33
|
+
return join(capabilitiesDir, CAPABILITY_DIRS[type]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getCapabilityPath(
|
|
37
|
+
capabilitiesDir: string,
|
|
38
|
+
type: CapabilityType,
|
|
39
|
+
name: string
|
|
40
|
+
): string {
|
|
41
|
+
const dir = getCapabilityDir(capabilitiesDir, type);
|
|
42
|
+
return join(dir, name + CAPABILITY_EXTENSIONS[type]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ensureCapabilitiesDir(capabilitiesDir: string): void {
|
|
46
|
+
for (const dir of Object.values(CAPABILITY_DIRS)) {
|
|
47
|
+
const fullPath = join(capabilitiesDir, dir);
|
|
48
|
+
if (!existsSync(fullPath)) {
|
|
49
|
+
mkdirSync(fullPath, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createCapability(
|
|
55
|
+
capabilitiesDir: string,
|
|
56
|
+
type: CapabilityType,
|
|
57
|
+
name: string,
|
|
58
|
+
content?: string
|
|
59
|
+
): Capability {
|
|
60
|
+
ensureCapabilitiesDir(capabilitiesDir);
|
|
61
|
+
const path = getCapabilityPath(capabilitiesDir, type, name);
|
|
62
|
+
|
|
63
|
+
if (existsSync(path)) {
|
|
64
|
+
throw new Error(`${type} '${name}' already exists at ${path}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fileContent = content ?? TEMPLATES[type](name);
|
|
68
|
+
writeFileSync(path, fileContent, "utf-8");
|
|
69
|
+
|
|
70
|
+
return { name, type, path, content: fileContent };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function readCapability(
|
|
74
|
+
capabilitiesDir: string,
|
|
75
|
+
type: CapabilityType,
|
|
76
|
+
name: string
|
|
77
|
+
): Capability {
|
|
78
|
+
const path = getCapabilityPath(capabilitiesDir, type, name);
|
|
79
|
+
|
|
80
|
+
if (!existsSync(path)) {
|
|
81
|
+
throw new Error(`${type} '${name}' not found`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const content = readFileSync(path, "utf-8");
|
|
85
|
+
return { name, type, path, content };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function updateCapability(
|
|
89
|
+
capabilitiesDir: string,
|
|
90
|
+
type: CapabilityType,
|
|
91
|
+
name: string,
|
|
92
|
+
content: string
|
|
93
|
+
): Capability {
|
|
94
|
+
const path = getCapabilityPath(capabilitiesDir, type, name);
|
|
95
|
+
|
|
96
|
+
if (!existsSync(path)) {
|
|
97
|
+
throw new Error(`${type} '${name}' not found`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
writeFileSync(path, content, "utf-8");
|
|
101
|
+
return { name, type, path, content };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function deleteCapability(
|
|
105
|
+
capabilitiesDir: string,
|
|
106
|
+
type: CapabilityType,
|
|
107
|
+
name: string
|
|
108
|
+
): void {
|
|
109
|
+
const path = getCapabilityPath(capabilitiesDir, type, name);
|
|
110
|
+
|
|
111
|
+
if (!existsSync(path)) {
|
|
112
|
+
throw new Error(`${type} '${name}' not found`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
unlinkSync(path);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function listCapabilities(
|
|
119
|
+
capabilitiesDir: string,
|
|
120
|
+
type: CapabilityType
|
|
121
|
+
): string[] {
|
|
122
|
+
const dir = getCapabilityDir(capabilitiesDir, type);
|
|
123
|
+
|
|
124
|
+
if (!existsSync(dir)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const ext = CAPABILITY_EXTENSIONS[type];
|
|
129
|
+
return readdirSync(dir)
|
|
130
|
+
.filter((f) => f.endsWith(ext))
|
|
131
|
+
.map((f) => basename(f, extname(f)))
|
|
132
|
+
.sort();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function capabilityExists(
|
|
136
|
+
capabilitiesDir: string,
|
|
137
|
+
type: CapabilityType,
|
|
138
|
+
name: string
|
|
139
|
+
): boolean {
|
|
140
|
+
return existsSync(getCapabilityPath(capabilitiesDir, type, name));
|
|
141
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { makeCapabilityCommand } from "./commands/capability.js";
|
|
4
|
+
import { makeSyncCommand } from "./commands/sync.js";
|
|
5
|
+
import { makeConfigCommand } from "./commands/config.js";
|
|
6
|
+
import { makeInstallCommand, makeUninstallCommand } from "./commands/install.js";
|
|
7
|
+
import { makeMcpCommand } from "./commands/mcp.js";
|
|
8
|
+
import { rootLogger } from "./logger.js";
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name("uplink")
|
|
14
|
+
.description(
|
|
15
|
+
"CLI and MCP for managing AI capabilities: skills, agents, and instructions"
|
|
16
|
+
)
|
|
17
|
+
.version("1.0.0");
|
|
18
|
+
|
|
19
|
+
// Resource management commands
|
|
20
|
+
program.addCommand(makeCapabilityCommand("skill"));
|
|
21
|
+
program.addCommand(makeCapabilityCommand("agent"));
|
|
22
|
+
program.addCommand(makeCapabilityCommand("instruction"));
|
|
23
|
+
|
|
24
|
+
// Utility commands
|
|
25
|
+
program.addCommand(makeSyncCommand());
|
|
26
|
+
program.addCommand(makeConfigCommand());
|
|
27
|
+
program.addCommand(makeInstallCommand());
|
|
28
|
+
program.addCommand(makeUninstallCommand());
|
|
29
|
+
program.addCommand(makeMcpCommand());
|
|
30
|
+
|
|
31
|
+
// Parse and execute
|
|
32
|
+
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
33
|
+
rootLogger.error(err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|