locker-cli 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/auth/client.d.ts +18 -0
- package/dist/auth/client.js +66 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +69 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/config.test.d.ts +1 -0
- package/dist/auth/config.test.js +73 -0
- package/dist/auth/config.test.js.map +1 -0
- package/dist/commands/api.test.d.ts +1 -0
- package/dist/commands/api.test.js +116 -0
- package/dist/commands/api.test.js.map +1 -0
- package/dist/commands/commands.test.d.ts +1 -0
- package/dist/commands/commands.test.js +104 -0
- package/dist/commands/commands.test.js.map +1 -0
- package/dist/commands/get.d.ts +3 -0
- package/dist/commands/get.js +25 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +27 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +87 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +15 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +83 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/revoke.d.ts +1 -0
- package/dist/commands/revoke.js +19 -0
- package/dist/commands/revoke.js.map +1 -0
- package/dist/commands/set.d.ts +1 -0
- package/dist/commands/set.js +15 -0
- package/dist/commands/set.js.map +1 -0
- package/dist/commands/whoami.d.ts +1 -0
- package/dist/commands/whoami.js +9 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -0
- package/src/auth/client.ts +86 -0
- package/src/auth/config.test.ts +78 -0
- package/src/auth/config.ts +66 -0
- package/src/commands/api.test.ts +143 -0
- package/src/commands/commands.test.ts +87 -0
- package/src/commands/get.ts +32 -0
- package/src/commands/list.ts +38 -0
- package/src/commands/login.ts +93 -0
- package/src/commands/logout.ts +12 -0
- package/src/commands/mcp.ts +87 -0
- package/src/commands/revoke.ts +23 -0
- package/src/commands/set.ts +20 -0
- package/src/commands/whoami.ts +6 -0
- package/src/index.ts +72 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { apiRequest, authRequest, getDefaultApiUrl } from "../auth/client";
|
|
3
|
+
|
|
4
|
+
// Mock global fetch
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
7
|
+
|
|
8
|
+
const testConfig = {
|
|
9
|
+
token: "jwt-test-token",
|
|
10
|
+
email: "test@example.com",
|
|
11
|
+
apiUrl: "http://localhost:3001",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("apiRequest", () => {
|
|
20
|
+
it("sends authenticated request with Bearer token", async () => {
|
|
21
|
+
mockFetch.mockResolvedValueOnce({
|
|
22
|
+
ok: true,
|
|
23
|
+
status: 200,
|
|
24
|
+
json: async () => ({ services: [] }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const res = await apiRequest("GET", "/keys", testConfig);
|
|
28
|
+
|
|
29
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
30
|
+
"http://localhost:3001/keys",
|
|
31
|
+
expect.objectContaining({
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: expect.objectContaining({
|
|
34
|
+
Authorization: "Bearer jwt-test-token",
|
|
35
|
+
}),
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
expect(res.ok).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("sends custom headers (e.g. X-Agent-Identifier)", async () => {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({
|
|
43
|
+
ok: true,
|
|
44
|
+
status: 200,
|
|
45
|
+
json: async () => ({ key: "sk-123" }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await apiRequest("GET", "/keys/resend", testConfig, undefined, {
|
|
49
|
+
"X-Agent-Identifier": "claude-code",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
53
|
+
"http://localhost:3001/keys/resend",
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
headers: expect.objectContaining({
|
|
56
|
+
"X-Agent-Identifier": "claude-code",
|
|
57
|
+
Authorization: "Bearer jwt-test-token",
|
|
58
|
+
}),
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("sends JSON body for POST requests", async () => {
|
|
64
|
+
mockFetch.mockResolvedValueOnce({
|
|
65
|
+
ok: true,
|
|
66
|
+
status: 201,
|
|
67
|
+
json: async () => ({ service: "resend", message: "Key stored" }),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await apiRequest("POST", "/keys", testConfig, {
|
|
71
|
+
service: "resend",
|
|
72
|
+
key: "sk-resend-123",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
76
|
+
"http://localhost:3001/keys",
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
method: "POST",
|
|
79
|
+
body: JSON.stringify({ service: "resend", key: "sk-resend-123" }),
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns error data on non-ok response", async () => {
|
|
85
|
+
mockFetch.mockResolvedValueOnce({
|
|
86
|
+
ok: false,
|
|
87
|
+
status: 404,
|
|
88
|
+
json: async () => ({ error: "No key found for service: unknown" }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const res = await apiRequest("GET", "/keys/unknown", testConfig);
|
|
92
|
+
expect(res.ok).toBe(false);
|
|
93
|
+
expect(res.status).toBe(404);
|
|
94
|
+
expect(res.data.error).toContain("No key found");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("authRequest", () => {
|
|
99
|
+
it("sends unauthenticated request", async () => {
|
|
100
|
+
mockFetch.mockResolvedValueOnce({
|
|
101
|
+
ok: true,
|
|
102
|
+
status: 200,
|
|
103
|
+
json: async () => ({
|
|
104
|
+
token: "new-jwt",
|
|
105
|
+
user: { id: "1", email: "test@example.com" },
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const res = await authRequest("POST", "/auth/login", "http://localhost:3001", {
|
|
110
|
+
email: "test@example.com",
|
|
111
|
+
password: "password123",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(res.ok).toBe(true);
|
|
115
|
+
expect(res.data.token).toBe("new-jwt");
|
|
116
|
+
|
|
117
|
+
// Should NOT have Authorization header
|
|
118
|
+
const callArgs = mockFetch.mock.calls[0];
|
|
119
|
+
expect(callArgs[1].headers).not.toHaveProperty("Authorization");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("getDefaultApiUrl", () => {
|
|
124
|
+
const originalEnv = process.env.LOCKER_API_URL;
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
if (originalEnv) {
|
|
128
|
+
process.env.LOCKER_API_URL = originalEnv;
|
|
129
|
+
} else {
|
|
130
|
+
delete process.env.LOCKER_API_URL;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns default URL when env var is not set", () => {
|
|
135
|
+
delete process.env.LOCKER_API_URL;
|
|
136
|
+
expect(getDefaultApiUrl()).toBe("http://localhost:3001");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("returns env var when set", () => {
|
|
140
|
+
process.env.LOCKER_API_URL = "https://api.locker.dev";
|
|
141
|
+
expect(getDefaultApiUrl()).toBe("https://api.locker.dev");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { logoutCommand } from "./logout";
|
|
3
|
+
import { whoamiCommand } from "./whoami";
|
|
4
|
+
|
|
5
|
+
// Mock the config module
|
|
6
|
+
vi.mock("../auth/config", () => {
|
|
7
|
+
let mockConfig: any = null;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
readConfig: vi.fn(() => mockConfig),
|
|
11
|
+
writeConfig: vi.fn(),
|
|
12
|
+
clearConfig: vi.fn(),
|
|
13
|
+
requireAuth: vi.fn(() => {
|
|
14
|
+
if (!mockConfig) {
|
|
15
|
+
// Simulate process.exit by throwing
|
|
16
|
+
throw new Error("NOT_LOGGED_IN");
|
|
17
|
+
}
|
|
18
|
+
return mockConfig;
|
|
19
|
+
}),
|
|
20
|
+
__setMockConfig: (config: any) => {
|
|
21
|
+
mockConfig = config;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Get the mock helpers
|
|
27
|
+
import * as configModule from "../auth/config";
|
|
28
|
+
|
|
29
|
+
const setMockConfig = (configModule as any).__setMockConfig;
|
|
30
|
+
const clearConfigMock = configModule.clearConfig as ReturnType<typeof vi.fn>;
|
|
31
|
+
|
|
32
|
+
describe("logout", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("clears config and confirms logout", () => {
|
|
38
|
+
setMockConfig({
|
|
39
|
+
token: "jwt-123",
|
|
40
|
+
email: "test@example.com",
|
|
41
|
+
apiUrl: "http://localhost:3001",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
45
|
+
logoutCommand();
|
|
46
|
+
|
|
47
|
+
expect(clearConfigMock).toHaveBeenCalled();
|
|
48
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
49
|
+
expect.stringContaining("test@example.com")
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("handles already logged out", () => {
|
|
54
|
+
setMockConfig(null);
|
|
55
|
+
|
|
56
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
57
|
+
logoutCommand();
|
|
58
|
+
|
|
59
|
+
expect(clearConfigMock).toHaveBeenCalled();
|
|
60
|
+
expect(consoleSpy).toHaveBeenCalledWith("Already logged out.");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("whoami", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.restoreAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("prints the logged-in email", () => {
|
|
70
|
+
setMockConfig({
|
|
71
|
+
token: "jwt-123",
|
|
72
|
+
email: "user@locker.dev",
|
|
73
|
+
apiUrl: "http://localhost:3001",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
77
|
+
whoamiCommand();
|
|
78
|
+
|
|
79
|
+
expect(consoleSpy).toHaveBeenCalledWith("user@locker.dev");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("throws when not logged in", () => {
|
|
83
|
+
setMockConfig(null);
|
|
84
|
+
|
|
85
|
+
expect(() => whoamiCommand()).toThrow("NOT_LOGGED_IN");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { requireAuth } from "../auth/config";
|
|
2
|
+
import { apiRequest } from "../auth/client";
|
|
3
|
+
|
|
4
|
+
export async function getCommand(service: string, options: { agent?: string }) {
|
|
5
|
+
const config = requireAuth();
|
|
6
|
+
|
|
7
|
+
const headers: Record<string, string> = {};
|
|
8
|
+
if (options.agent) {
|
|
9
|
+
headers["X-Agent-Identifier"] = options.agent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const res = await apiRequest<{ service: string; key: string }>(
|
|
13
|
+
"GET",
|
|
14
|
+
`/keys/${encodeURIComponent(service)}`,
|
|
15
|
+
config,
|
|
16
|
+
undefined,
|
|
17
|
+
headers
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
if (res.status === 404) {
|
|
22
|
+
console.error(`No key found for service: ${service}`);
|
|
23
|
+
console.error(`Store one with: locker set ${service} <key>`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
console.error(res.data.error || "Failed to retrieve key.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Print key to stdout only — no extra formatting, no logging to disk
|
|
31
|
+
process.stdout.write(res.data.key);
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { requireAuth } from "../auth/config";
|
|
2
|
+
import { apiRequest } from "../auth/client";
|
|
3
|
+
|
|
4
|
+
interface Service {
|
|
5
|
+
service: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
lastUsed: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function listCommand() {
|
|
11
|
+
const config = requireAuth();
|
|
12
|
+
|
|
13
|
+
const res = await apiRequest<{ services: Service[] }>(
|
|
14
|
+
"GET",
|
|
15
|
+
"/keys",
|
|
16
|
+
config
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
console.error(res.data.error || "Failed to list keys.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const services = res.data.services;
|
|
25
|
+
if (services.length === 0) {
|
|
26
|
+
console.log("No keys stored yet.");
|
|
27
|
+
console.log("Store one with: locker set <service> <key>");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`${services.length} key${services.length === 1 ? "" : "s"} stored:\n`);
|
|
32
|
+
for (const svc of services) {
|
|
33
|
+
const lastUsed = svc.lastUsed
|
|
34
|
+
? `last used ${new Date(svc.lastUsed).toLocaleDateString()}`
|
|
35
|
+
: "never used";
|
|
36
|
+
console.log(` ${svc.service} (${lastUsed})`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { writeConfig } from "../auth/config";
|
|
3
|
+
import { authRequest, getDefaultApiUrl } from "../auth/client";
|
|
4
|
+
|
|
5
|
+
function prompt(question: string, hidden = false): Promise<string> {
|
|
6
|
+
const rl = readline.createInterface({
|
|
7
|
+
input: process.stdin,
|
|
8
|
+
output: process.stdout,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
if (hidden && process.stdin.isTTY) {
|
|
13
|
+
// Hide password input
|
|
14
|
+
process.stdout.write(question);
|
|
15
|
+
const stdin = process.stdin;
|
|
16
|
+
stdin.setRawMode(true);
|
|
17
|
+
stdin.resume();
|
|
18
|
+
stdin.setEncoding("utf8");
|
|
19
|
+
|
|
20
|
+
let input = "";
|
|
21
|
+
const onData = (ch: string) => {
|
|
22
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
23
|
+
stdin.setRawMode(false);
|
|
24
|
+
stdin.removeListener("data", onData);
|
|
25
|
+
stdin.pause();
|
|
26
|
+
rl.close();
|
|
27
|
+
process.stdout.write("\n");
|
|
28
|
+
resolve(input);
|
|
29
|
+
} else if (ch === "\u0003") {
|
|
30
|
+
// Ctrl-C
|
|
31
|
+
process.exit(0);
|
|
32
|
+
} else if (ch === "\u007F" || ch === "\b") {
|
|
33
|
+
// Backspace
|
|
34
|
+
if (input.length > 0) {
|
|
35
|
+
input = input.slice(0, -1);
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
input += ch;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
stdin.on("data", onData);
|
|
42
|
+
} else {
|
|
43
|
+
rl.question(question, (answer) => {
|
|
44
|
+
rl.close();
|
|
45
|
+
resolve(answer);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loginCommand(options: { register?: boolean; api?: string }) {
|
|
52
|
+
const apiUrl = options.api || getDefaultApiUrl();
|
|
53
|
+
const isRegister = options.register || false;
|
|
54
|
+
|
|
55
|
+
console.log(isRegister ? "Create a new Locker account" : "Log in to Locker");
|
|
56
|
+
console.log();
|
|
57
|
+
|
|
58
|
+
const email = await prompt("Email: ");
|
|
59
|
+
const password = await prompt("Password: ", true);
|
|
60
|
+
|
|
61
|
+
if (!email || !password) {
|
|
62
|
+
console.error("Email and password are required.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isRegister && password.length < 8) {
|
|
67
|
+
console.error("Password must be at least 8 characters.");
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const endpoint = isRegister ? "/auth/register" : "/auth/login";
|
|
72
|
+
const res = await authRequest<{ token: string; user: { id: string; email: string } }>(
|
|
73
|
+
"POST",
|
|
74
|
+
endpoint,
|
|
75
|
+
apiUrl,
|
|
76
|
+
{ email, password }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
console.error(res.data.error || "Authentication failed.");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeConfig({
|
|
85
|
+
token: res.data.token,
|
|
86
|
+
email: res.data.user.email,
|
|
87
|
+
apiUrl,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log();
|
|
91
|
+
console.log(`Logged in as ${res.data.user.email}`);
|
|
92
|
+
console.log("Token stored in ~/.locker/config");
|
|
93
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clearConfig, readConfig } from "../auth/config";
|
|
2
|
+
|
|
3
|
+
export function logoutCommand() {
|
|
4
|
+
const config = readConfig();
|
|
5
|
+
clearConfig();
|
|
6
|
+
|
|
7
|
+
if (config) {
|
|
8
|
+
console.log(`Logged out (${config.email}). Token cleared.`);
|
|
9
|
+
} else {
|
|
10
|
+
console.log("Already logged out.");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
interface McpConfig {
|
|
6
|
+
mcpServers?: Record<string, { command: string; args: string[] }>;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TARGETS: Record<string, string> = {
|
|
11
|
+
claude: path.join(os.homedir(), ".claude", "claude_desktop_config.json"),
|
|
12
|
+
cursor: path.join(os.homedir(), ".cursor", "mcp.json"),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getMcpEntry() {
|
|
16
|
+
// Use the published npm package — npx resolves it globally
|
|
17
|
+
return { command: "npx", args: ["locker-mcp-server"] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function installToTarget(name: string, configPath: string): boolean {
|
|
21
|
+
const dir = path.dirname(configPath);
|
|
22
|
+
if (!fs.existsSync(dir)) {
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let config: McpConfig = {};
|
|
27
|
+
if (fs.existsSync(configPath)) {
|
|
28
|
+
try {
|
|
29
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
30
|
+
} catch {
|
|
31
|
+
console.error(` Could not parse ${configPath}`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!config.mcpServers) {
|
|
37
|
+
config.mcpServers = {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const entry = getMcpEntry();
|
|
41
|
+
config.mcpServers.locker = entry;
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
44
|
+
console.log(` ✓ ${name} — ${configPath}`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function mcpInstallCommand() {
|
|
49
|
+
console.log("Installing Locker MCP server...\n");
|
|
50
|
+
|
|
51
|
+
let installed = 0;
|
|
52
|
+
for (const [name, configPath] of Object.entries(TARGETS)) {
|
|
53
|
+
const dir = path.dirname(configPath);
|
|
54
|
+
// Only install if the tool's config directory exists (tool is installed)
|
|
55
|
+
if (fs.existsSync(dir) || name === "claude") {
|
|
56
|
+
if (installToTarget(name, configPath)) installed++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (installed === 0) {
|
|
61
|
+
console.log("No supported AI tools detected.");
|
|
62
|
+
console.log("Manually add to your tool's MCP config:");
|
|
63
|
+
const entry = getMcpEntry();
|
|
64
|
+
console.log(JSON.stringify({ locker: entry }, null, 2));
|
|
65
|
+
} else {
|
|
66
|
+
console.log(`\nDone. Restart your AI tool to activate.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function mcpUninstallCommand() {
|
|
71
|
+
console.log("Removing Locker MCP server...\n");
|
|
72
|
+
|
|
73
|
+
for (const [name, configPath] of Object.entries(TARGETS)) {
|
|
74
|
+
if (!fs.existsSync(configPath)) continue;
|
|
75
|
+
try {
|
|
76
|
+
const config: McpConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
77
|
+
if (config.mcpServers?.locker) {
|
|
78
|
+
delete config.mcpServers.locker;
|
|
79
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
80
|
+
console.log(` ✓ Removed from ${name}`);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Skip
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log("\nDone.");
|
|
87
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { requireAuth } from "../auth/config";
|
|
2
|
+
import { apiRequest } from "../auth/client";
|
|
3
|
+
|
|
4
|
+
export async function revokeCommand(service: string) {
|
|
5
|
+
const config = requireAuth();
|
|
6
|
+
|
|
7
|
+
const res = await apiRequest(
|
|
8
|
+
"DELETE",
|
|
9
|
+
`/keys/${encodeURIComponent(service)}`,
|
|
10
|
+
config
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
if (res.status === 404) {
|
|
15
|
+
console.error(`No key found for service: ${service}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
console.error(res.data.error || "Failed to revoke key.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`Key revoked for ${service}`);
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { requireAuth } from "../auth/config";
|
|
2
|
+
import { apiRequest } from "../auth/client";
|
|
3
|
+
|
|
4
|
+
export async function setCommand(service: string, key: string) {
|
|
5
|
+
const config = requireAuth();
|
|
6
|
+
|
|
7
|
+
const res = await apiRequest(
|
|
8
|
+
"POST",
|
|
9
|
+
"/keys",
|
|
10
|
+
config,
|
|
11
|
+
{ service, key }
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
console.error(res.data.error || "Failed to store key.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`Key stored for ${service}`);
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { loginCommand } from "./commands/login";
|
|
5
|
+
import { logoutCommand } from "./commands/logout";
|
|
6
|
+
import { whoamiCommand } from "./commands/whoami";
|
|
7
|
+
import { getCommand } from "./commands/get";
|
|
8
|
+
import { setCommand } from "./commands/set";
|
|
9
|
+
import { listCommand } from "./commands/list";
|
|
10
|
+
import { revokeCommand } from "./commands/revoke";
|
|
11
|
+
import { mcpInstallCommand, mcpUninstallCommand } from "./commands/mcp";
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name("locker")
|
|
17
|
+
.description("Secure API credential manager for AI agents")
|
|
18
|
+
.version("0.1.0");
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("login")
|
|
22
|
+
.description("Log in to Locker")
|
|
23
|
+
.option("--register", "Create a new account")
|
|
24
|
+
.option("--api <url>", "API server URL")
|
|
25
|
+
.action(loginCommand);
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("logout")
|
|
29
|
+
.description("Log out and clear stored token")
|
|
30
|
+
.action(logoutCommand);
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command("whoami")
|
|
34
|
+
.description("Show the currently logged-in user")
|
|
35
|
+
.action(whoamiCommand);
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command("get <service>")
|
|
39
|
+
.description("Retrieve an API key (prints to stdout)")
|
|
40
|
+
.option("--agent <name>", "Agent identifier for audit log")
|
|
41
|
+
.action(getCommand);
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command("set <service> <key>")
|
|
45
|
+
.description("Store an API key")
|
|
46
|
+
.action(setCommand);
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command("list")
|
|
50
|
+
.description("List all stored services")
|
|
51
|
+
.action(listCommand);
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command("revoke <service>")
|
|
55
|
+
.description("Delete a stored API key")
|
|
56
|
+
.action(revokeCommand);
|
|
57
|
+
|
|
58
|
+
const mcp = program
|
|
59
|
+
.command("mcp")
|
|
60
|
+
.description("Manage the Locker MCP server");
|
|
61
|
+
|
|
62
|
+
mcp
|
|
63
|
+
.command("install")
|
|
64
|
+
.description("Install Locker MCP server into Claude Code, Cursor, etc.")
|
|
65
|
+
.action(mcpInstallCommand);
|
|
66
|
+
|
|
67
|
+
mcp
|
|
68
|
+
.command("uninstall")
|
|
69
|
+
.description("Remove Locker MCP server from AI tools")
|
|
70
|
+
.action(mcpUninstallCommand);
|
|
71
|
+
|
|
72
|
+
program.parse();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|