@kitae9999/openlog-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/README.md +24 -0
- package/dist/api-client.js +64 -0
- package/dist/auth-store.js +43 -0
- package/dist/browser.js +20 -0
- package/dist/config.js +10 -0
- package/dist/index.js +123 -0
- package/dist/login.js +38 -0
- package/dist/mcp-install.js +199 -0
- package/dist/mcp-server.js +115 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# OpenLog CLI
|
|
2
|
+
|
|
3
|
+
Official CLI and MCP server for OpenLog.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx -y @kitae9999/openlog-cli login
|
|
7
|
+
npx -y @kitae9999/openlog-cli whoami
|
|
8
|
+
npx -y @kitae9999/openlog-cli mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
MCP client configuration:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"openlog": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "@kitae9999/openlog-cli", "mcp"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Set `OPENLOG_API_BASE_URL` to point at a non-production API.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getApiBaseUrl } from "./config.js";
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
url;
|
|
5
|
+
constructor(status, url, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.url = url;
|
|
9
|
+
this.name = "ApiError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class OpenLogApiClient {
|
|
13
|
+
apiBaseUrl;
|
|
14
|
+
accessToken;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.apiBaseUrl = (options.apiBaseUrl ?? getApiBaseUrl()).replace(/\/$/, "");
|
|
17
|
+
this.accessToken = options.accessToken;
|
|
18
|
+
}
|
|
19
|
+
async get(path) {
|
|
20
|
+
return this.request(path, { method: "GET" });
|
|
21
|
+
}
|
|
22
|
+
async post(path, body) {
|
|
23
|
+
return this.request(path, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
26
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async postNoContent(path, body) {
|
|
30
|
+
await this.request(path, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
33
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async request(path, init) {
|
|
37
|
+
const url = `${this.apiBaseUrl}${path}`;
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
...init,
|
|
40
|
+
headers: {
|
|
41
|
+
...(init.headers ?? {}),
|
|
42
|
+
...(this.accessToken
|
|
43
|
+
? { cookie: `openlog_access_token=${this.accessToken}` }
|
|
44
|
+
: {}),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new ApiError(response.status, url, await readErrorMessage(response));
|
|
49
|
+
}
|
|
50
|
+
if (response.status === 204) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return (await response.json());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function readErrorMessage(response) {
|
|
57
|
+
try {
|
|
58
|
+
const body = (await response.json());
|
|
59
|
+
return body.message ?? `OpenLog API request failed with ${response.status}`;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return `OpenLog API request failed with ${response.status}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile, chmod } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getApiBaseUrl, getAuthFilePath } from "./config.js";
|
|
4
|
+
export async function readAuthFile() {
|
|
5
|
+
try {
|
|
6
|
+
const content = await readFile(getAuthFilePath(), "utf8");
|
|
7
|
+
const parsed = JSON.parse(content);
|
|
8
|
+
if (!parsed.accessToken || !parsed.apiBaseUrl || !parsed.createdAt) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
accessToken: parsed.accessToken,
|
|
13
|
+
apiBaseUrl: parsed.apiBaseUrl,
|
|
14
|
+
createdAt: parsed.createdAt,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error &&
|
|
19
|
+
typeof error === "object" &&
|
|
20
|
+
"code" in error &&
|
|
21
|
+
error.code === "ENOENT") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function writeAuthFile(accessToken) {
|
|
28
|
+
const authFile = {
|
|
29
|
+
accessToken,
|
|
30
|
+
apiBaseUrl: getApiBaseUrl(),
|
|
31
|
+
createdAt: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
const authFilePath = getAuthFilePath();
|
|
34
|
+
await mkdir(path.dirname(authFilePath), { recursive: true, mode: 0o700 });
|
|
35
|
+
await writeFile(authFilePath, `${JSON.stringify(authFile, null, 2)}\n`, {
|
|
36
|
+
mode: 0o600,
|
|
37
|
+
});
|
|
38
|
+
await chmod(authFilePath, 0o600);
|
|
39
|
+
return authFile;
|
|
40
|
+
}
|
|
41
|
+
export async function deleteAuthFile() {
|
|
42
|
+
await rm(getAuthFilePath(), { force: true });
|
|
43
|
+
}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export function openBrowser(url) {
|
|
3
|
+
const command = process.platform === "darwin"
|
|
4
|
+
? "open"
|
|
5
|
+
: process.platform === "win32"
|
|
6
|
+
? "cmd"
|
|
7
|
+
: "xdg-open";
|
|
8
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
9
|
+
try {
|
|
10
|
+
const child = spawn(command, args, {
|
|
11
|
+
detached: true,
|
|
12
|
+
stdio: "ignore",
|
|
13
|
+
});
|
|
14
|
+
child.unref();
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const DEFAULT_API_BASE_URL = "https://api.openlog.kr/api";
|
|
4
|
+
export function getApiBaseUrl() {
|
|
5
|
+
return (process.env.OPENLOG_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
6
|
+
}
|
|
7
|
+
export function getAuthFilePath() {
|
|
8
|
+
return (process.env.OPENLOG_AUTH_FILE ??
|
|
9
|
+
path.join(os.homedir(), ".openlog", "auth.json"));
|
|
10
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { deleteAuthFile, readAuthFile } from "./auth-store.js";
|
|
3
|
+
import { login } from "./login.js";
|
|
4
|
+
import { runMcpServer } from "./mcp-server.js";
|
|
5
|
+
import { installMcp, printMcpInstallHelp } from "./mcp-install.js";
|
|
6
|
+
import { ApiError, OpenLogApiClient } from "./api-client.js";
|
|
7
|
+
const command = process.argv[2] ?? "help";
|
|
8
|
+
const subcommand = process.argv[3];
|
|
9
|
+
try {
|
|
10
|
+
if (command === "login") {
|
|
11
|
+
await login();
|
|
12
|
+
}
|
|
13
|
+
else if (command === "logout") {
|
|
14
|
+
await deleteAuthFile();
|
|
15
|
+
console.log("Logged out.");
|
|
16
|
+
}
|
|
17
|
+
else if (command === "whoami") {
|
|
18
|
+
await whoami();
|
|
19
|
+
}
|
|
20
|
+
else if (command === "mcp") {
|
|
21
|
+
if (subcommand === "install") {
|
|
22
|
+
await installMcpCommand();
|
|
23
|
+
}
|
|
24
|
+
else if (subcommand === undefined ||
|
|
25
|
+
subcommand === "serve" ||
|
|
26
|
+
subcommand === "start") {
|
|
27
|
+
printMcpStartupHint();
|
|
28
|
+
await runMcpServer();
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
printMcpInstallHelp();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
printHelp();
|
|
37
|
+
process.exit(command === "help" || command === "--help" || command === "-h" ? 0 : 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(formatCliError(error));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
async function installMcpCommand() {
|
|
45
|
+
const client = process.argv[4];
|
|
46
|
+
const printOnly = process.argv.includes("--print");
|
|
47
|
+
if (client !== "codex" &&
|
|
48
|
+
client !== "claude-code" &&
|
|
49
|
+
client !== "claude") {
|
|
50
|
+
printMcpInstallHelp();
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
await installMcp({
|
|
54
|
+
client,
|
|
55
|
+
printOnly,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function whoami() {
|
|
59
|
+
const authFile = await readAuthFile();
|
|
60
|
+
if (!authFile) {
|
|
61
|
+
throw new Error("Not logged in. Run `openlog login` first.");
|
|
62
|
+
}
|
|
63
|
+
const apiClient = new OpenLogApiClient({
|
|
64
|
+
accessToken: authFile.accessToken,
|
|
65
|
+
apiBaseUrl: authFile.apiBaseUrl,
|
|
66
|
+
});
|
|
67
|
+
const me = await apiClient.get("/auth/me");
|
|
68
|
+
console.log(JSON.stringify(me, null, 2));
|
|
69
|
+
}
|
|
70
|
+
function printMcpStartupHint() {
|
|
71
|
+
if (!process.stderr.isTTY) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.error(`OpenLog MCP server is running over stdio.
|
|
75
|
+
|
|
76
|
+
This terminal is now reserved for MCP protocol traffic.
|
|
77
|
+
Press Ctrl+C to stop it.
|
|
78
|
+
|
|
79
|
+
To use it from an MCP client, add:
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"openlog": {
|
|
83
|
+
"command": "openlog",
|
|
84
|
+
"args": ["mcp"]
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Available tools:
|
|
90
|
+
get_auth_status
|
|
91
|
+
get_me
|
|
92
|
+
list_my_notifications
|
|
93
|
+
list_my_liked_posts
|
|
94
|
+
get_post_detail
|
|
95
|
+
`);
|
|
96
|
+
}
|
|
97
|
+
function printHelp() {
|
|
98
|
+
console.log(`OpenLog CLI
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
openlog login Sign in to OpenLog from the CLI
|
|
102
|
+
openlog logout Remove local OpenLog credentials
|
|
103
|
+
openlog whoami Print the current OpenLog user
|
|
104
|
+
openlog mcp Start the OpenLog MCP stdio server
|
|
105
|
+
openlog mcp install codex
|
|
106
|
+
Register OpenLog MCP with Codex
|
|
107
|
+
openlog mcp install claude-code
|
|
108
|
+
Register OpenLog MCP with Claude Code
|
|
109
|
+
|
|
110
|
+
Environment:
|
|
111
|
+
OPENLOG_API_BASE_URL Override the OpenLog API base URL
|
|
112
|
+
OPENLOG_AUTH_FILE Override the local auth file path
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
function formatCliError(error) {
|
|
116
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
117
|
+
const hint = error.url.includes("localhost:8080/auth/")
|
|
118
|
+
? "\n\nLocal hint: if your backend uses SERVER_SERVLET_CONTEXT_PATH=/api, run with OPENLOG_API_BASE_URL=http://localhost:8080/api."
|
|
119
|
+
: "";
|
|
120
|
+
return `${error.message}\nRequested: ${error.url}${hint}`;
|
|
121
|
+
}
|
|
122
|
+
return error instanceof Error ? error.message : String(error);
|
|
123
|
+
}
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { OpenLogApiClient } from "./api-client.js";
|
|
2
|
+
import { openBrowser } from "./browser.js";
|
|
3
|
+
import { writeAuthFile } from "./auth-store.js";
|
|
4
|
+
export async function login() {
|
|
5
|
+
const apiClient = new OpenLogApiClient();
|
|
6
|
+
const deviceLogin = await apiClient.post("/auth/device/start");
|
|
7
|
+
const opened = openBrowser(deviceLogin.verificationUriComplete);
|
|
8
|
+
console.log("OpenLog CLI login");
|
|
9
|
+
console.log("");
|
|
10
|
+
console.log(`Code: ${deviceLogin.userCode}`);
|
|
11
|
+
console.log(`URL: ${deviceLogin.verificationUriComplete}`);
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log(opened
|
|
14
|
+
? "The approval page was opened in your browser."
|
|
15
|
+
: "Open the URL above in your browser.");
|
|
16
|
+
console.log("Waiting for approval...");
|
|
17
|
+
const accessToken = await pollForAccessToken(apiClient, deviceLogin);
|
|
18
|
+
await writeAuthFile(accessToken);
|
|
19
|
+
console.log("Login successful.");
|
|
20
|
+
}
|
|
21
|
+
async function pollForAccessToken(apiClient, deviceLogin) {
|
|
22
|
+
const deadline = Date.now() + deviceLogin.expiresIn * 1000;
|
|
23
|
+
let interval = deviceLogin.interval;
|
|
24
|
+
while (Date.now() < deadline) {
|
|
25
|
+
await sleep(interval * 1000);
|
|
26
|
+
const response = await apiClient.post("/auth/device/token", { deviceCode: deviceLogin.deviceCode });
|
|
27
|
+
if (response.status === "APPROVED") {
|
|
28
|
+
return response.accessToken;
|
|
29
|
+
}
|
|
30
|
+
interval = response.interval ?? interval;
|
|
31
|
+
}
|
|
32
|
+
throw new Error("CLI login expired before approval completed.");
|
|
33
|
+
}
|
|
34
|
+
function sleep(milliseconds) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
setTimeout(resolve, milliseconds);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { DEFAULT_API_BASE_URL, getApiBaseUrl } from "./config.js";
|
|
6
|
+
export async function installMcp(options) {
|
|
7
|
+
const serverConfig = buildServerConfig();
|
|
8
|
+
if (options.printOnly) {
|
|
9
|
+
printConfig(options.client, serverConfig);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (options.client === "codex") {
|
|
13
|
+
await installCodex(serverConfig);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await installClaudeCode(serverConfig);
|
|
17
|
+
}
|
|
18
|
+
export function printMcpInstallHelp() {
|
|
19
|
+
console.log(`OpenLog MCP install
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
openlog mcp install codex
|
|
23
|
+
openlog mcp install claude-code
|
|
24
|
+
openlog mcp install codex --print
|
|
25
|
+
openlog mcp install claude-code --print
|
|
26
|
+
|
|
27
|
+
Aliases:
|
|
28
|
+
claude Same as claude-code
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
If OPENLOG_API_BASE_URL is set, it is added to the client config.
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
function buildServerConfig() {
|
|
35
|
+
const apiBaseUrl = getApiBaseUrl();
|
|
36
|
+
const env = {};
|
|
37
|
+
if (apiBaseUrl !== DEFAULT_API_BASE_URL) {
|
|
38
|
+
env.OPENLOG_API_BASE_URL = apiBaseUrl;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
command: "openlog",
|
|
42
|
+
args: ["mcp"],
|
|
43
|
+
env,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function installCodex(serverConfig) {
|
|
47
|
+
const args = ["mcp", "add", "openlog"];
|
|
48
|
+
for (const [name, value] of Object.entries(serverConfig.env)) {
|
|
49
|
+
args.push("--env", `${name}=${value}`);
|
|
50
|
+
}
|
|
51
|
+
args.push("--", serverConfig.command, ...serverConfig.args);
|
|
52
|
+
let registeredWithCodexCli = false;
|
|
53
|
+
try {
|
|
54
|
+
registeredWithCodexCli =
|
|
55
|
+
(await runCommand("codex", args, { stdio: "pipe" })) === "executed";
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
warnCodexFallback(error);
|
|
59
|
+
}
|
|
60
|
+
if (registeredWithCodexCli) {
|
|
61
|
+
console.log("OpenLog MCP server registered with Codex.");
|
|
62
|
+
console.log("Run `/mcp` inside Codex to confirm it is connected.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await writeCodexConfig(serverConfig);
|
|
66
|
+
console.log("OpenLog MCP server added to ~/.codex/config.toml.");
|
|
67
|
+
console.log("Restart Codex or reload MCP servers, then run `/mcp` to confirm.");
|
|
68
|
+
}
|
|
69
|
+
async function installClaudeCode(serverConfig) {
|
|
70
|
+
const args = ["mcp", "add", "--transport", "stdio", "--scope", "user"];
|
|
71
|
+
for (const [name, value] of Object.entries(serverConfig.env)) {
|
|
72
|
+
args.push("--env", `${name}=${value}`);
|
|
73
|
+
}
|
|
74
|
+
args.push("openlog", "--", serverConfig.command, ...serverConfig.args);
|
|
75
|
+
const result = await runCommand("claude", args);
|
|
76
|
+
if (result !== "executed") {
|
|
77
|
+
throw new Error("Claude Code CLI was not found. Install Claude Code first, then run `openlog mcp install claude-code` again.");
|
|
78
|
+
}
|
|
79
|
+
console.log("OpenLog MCP server registered with Claude Code.");
|
|
80
|
+
console.log("Run `/mcp` inside Claude Code to confirm it is connected.");
|
|
81
|
+
}
|
|
82
|
+
async function writeCodexConfig(serverConfig) {
|
|
83
|
+
const configPath = path.join(os.homedir(), ".codex", "config.toml");
|
|
84
|
+
await mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
85
|
+
const currentConfig = await readTextIfExists(configPath);
|
|
86
|
+
const nextConfig = upsertCodexServerBlock(currentConfig, serverConfig);
|
|
87
|
+
await writeFile(configPath, nextConfig, { mode: 0o600 });
|
|
88
|
+
}
|
|
89
|
+
function upsertCodexServerBlock(currentConfig, serverConfig) {
|
|
90
|
+
const block = buildCodexServerBlock(serverConfig);
|
|
91
|
+
const blockPattern = /(?:^|\n)\[mcp_servers\.openlog]\n[\s\S]*?(?:\n\[mcp_servers\.openlog\.env]\n[\s\S]*?)?(?=\n\[(?!mcp_servers\.openlog(?:\.env)?])[^\]]+]|\s*$)/;
|
|
92
|
+
if (blockPattern.test(currentConfig)) {
|
|
93
|
+
return currentConfig.replace(blockPattern, `\n${block}`).trimStart() + "\n";
|
|
94
|
+
}
|
|
95
|
+
const separator = currentConfig.trim().length === 0 ? "" : "\n\n";
|
|
96
|
+
return `${currentConfig.trimEnd()}${separator}${block}\n`;
|
|
97
|
+
}
|
|
98
|
+
function buildCodexServerBlock(serverConfig) {
|
|
99
|
+
const lines = [
|
|
100
|
+
"[mcp_servers.openlog]",
|
|
101
|
+
`command = ${tomlString(serverConfig.command)}`,
|
|
102
|
+
`args = [${serverConfig.args.map(tomlString).join(", ")}]`,
|
|
103
|
+
];
|
|
104
|
+
if (Object.keys(serverConfig.env).length > 0) {
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push("[mcp_servers.openlog.env]");
|
|
107
|
+
for (const [name, value] of Object.entries(serverConfig.env)) {
|
|
108
|
+
lines.push(`${name} = ${tomlString(value)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
function printConfig(client, serverConfig) {
|
|
114
|
+
if (client === "codex") {
|
|
115
|
+
console.log(buildCodexServerBlock(serverConfig));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.log(JSON.stringify({
|
|
119
|
+
type: "stdio",
|
|
120
|
+
command: serverConfig.command,
|
|
121
|
+
args: serverConfig.args,
|
|
122
|
+
env: serverConfig.env,
|
|
123
|
+
}, null, 2));
|
|
124
|
+
}
|
|
125
|
+
async function readTextIfExists(filePath) {
|
|
126
|
+
try {
|
|
127
|
+
return await readFile(filePath, "utf8");
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error &&
|
|
131
|
+
typeof error === "object" &&
|
|
132
|
+
"code" in error &&
|
|
133
|
+
error.code === "ENOENT") {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function tomlString(value) {
|
|
140
|
+
return JSON.stringify(value);
|
|
141
|
+
}
|
|
142
|
+
class CommandFailedError extends Error {
|
|
143
|
+
exitCode;
|
|
144
|
+
stderr;
|
|
145
|
+
constructor(command, exitCode, stderr) {
|
|
146
|
+
super(`${command} exited with code ${exitCode ?? "unknown"}.`);
|
|
147
|
+
this.exitCode = exitCode;
|
|
148
|
+
this.stderr = stderr;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function runCommand(command, args, options = {}) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const stdioMode = options.stdio ?? "inherit";
|
|
154
|
+
const stdio = stdioMode === "pipe" ? ["ignore", "ignore", "pipe"] : "inherit";
|
|
155
|
+
let stderr = "";
|
|
156
|
+
const child = spawn(command, args, {
|
|
157
|
+
stdio,
|
|
158
|
+
});
|
|
159
|
+
child.stderr?.setEncoding("utf8");
|
|
160
|
+
child.stderr?.on("data", (chunk) => {
|
|
161
|
+
stderr += chunk;
|
|
162
|
+
});
|
|
163
|
+
child.on("error", (error) => {
|
|
164
|
+
if (error.code === "ENOENT") {
|
|
165
|
+
resolve("missing");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
reject(error);
|
|
169
|
+
});
|
|
170
|
+
child.on("exit", (code) => {
|
|
171
|
+
if (code === 0) {
|
|
172
|
+
resolve("executed");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
reject(new CommandFailedError(command, code, stderr));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function warnCodexFallback(error) {
|
|
180
|
+
const reason = summarizeCommandError(error);
|
|
181
|
+
const suffix = reason.length > 0 ? ` (${reason})` : "";
|
|
182
|
+
console.warn(`Codex CLI registration failed${suffix}. Falling back to ~/.codex/config.toml.`);
|
|
183
|
+
}
|
|
184
|
+
function summarizeCommandError(error) {
|
|
185
|
+
if (error instanceof CommandFailedError) {
|
|
186
|
+
const firstStderrLine = error.stderr
|
|
187
|
+
.split(/\r?\n/)
|
|
188
|
+
.map((line) => line.trim())
|
|
189
|
+
.find((line) => line.length > 0);
|
|
190
|
+
return truncateReason(firstStderrLine ?? error.message);
|
|
191
|
+
}
|
|
192
|
+
if (error instanceof Error) {
|
|
193
|
+
return truncateReason(error.message);
|
|
194
|
+
}
|
|
195
|
+
return truncateReason(String(error));
|
|
196
|
+
}
|
|
197
|
+
function truncateReason(reason) {
|
|
198
|
+
return reason.length > 180 ? `${reason.slice(0, 177)}...` : reason;
|
|
199
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { OpenLogApiClient } from "./api-client.js";
|
|
5
|
+
import { readAuthFile } from "./auth-store.js";
|
|
6
|
+
import { getApiBaseUrl } from "./config.js";
|
|
7
|
+
export async function runMcpServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "openlog",
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("get_auth_status", {
|
|
13
|
+
title: "Get OpenLog Auth Status",
|
|
14
|
+
description: "Check whether the local OpenLog CLI is authenticated.",
|
|
15
|
+
inputSchema: {},
|
|
16
|
+
}, async () => {
|
|
17
|
+
let apiBaseUrl = getApiBaseUrl();
|
|
18
|
+
try {
|
|
19
|
+
const authFile = await readAuthFile();
|
|
20
|
+
if (!authFile) {
|
|
21
|
+
return textResult({
|
|
22
|
+
authenticated: false,
|
|
23
|
+
apiBaseUrl,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
apiBaseUrl = authFile.apiBaseUrl;
|
|
27
|
+
const me = await createAuthenticatedClient().then((client) => client.get("/auth/me"));
|
|
28
|
+
return textResult({
|
|
29
|
+
authenticated: true,
|
|
30
|
+
apiBaseUrl: authFile.apiBaseUrl,
|
|
31
|
+
user: me,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return textResult({
|
|
36
|
+
authenticated: false,
|
|
37
|
+
apiBaseUrl,
|
|
38
|
+
error: error instanceof Error ? error.message : String(error),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.registerTool("get_me", {
|
|
43
|
+
title: "Get OpenLog Me",
|
|
44
|
+
description: "Return the currently authenticated OpenLog user.",
|
|
45
|
+
inputSchema: {},
|
|
46
|
+
}, async () => withAuthenticatedClient((client) => client.get("/auth/me")));
|
|
47
|
+
server.registerTool("list_my_notifications", {
|
|
48
|
+
title: "List My OpenLog Notifications",
|
|
49
|
+
description: "Return notifications for the authenticated OpenLog user.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
size: z.number().int().min(1).max(20).default(20),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ size }) => withAuthenticatedClient((client) => client.get(`/notifications?${new URLSearchParams({ size: String(size) })}`)));
|
|
54
|
+
server.registerTool("list_my_liked_posts", {
|
|
55
|
+
title: "List My Liked OpenLog Posts",
|
|
56
|
+
description: "Return posts liked by the authenticated OpenLog user.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
cursor: z.string().optional(),
|
|
59
|
+
size: z.number().int().min(1).max(20).default(10),
|
|
60
|
+
},
|
|
61
|
+
}, async ({ cursor, size }) => {
|
|
62
|
+
const params = new URLSearchParams({ size: String(size) });
|
|
63
|
+
if (cursor) {
|
|
64
|
+
params.set("cursor", cursor);
|
|
65
|
+
}
|
|
66
|
+
return withAuthenticatedClient((client) => client.get(`/users/me/liked-posts?${params}`));
|
|
67
|
+
});
|
|
68
|
+
server.registerTool("get_post_detail", {
|
|
69
|
+
title: "Get OpenLog Post Detail",
|
|
70
|
+
description: "Return a public OpenLog post detail by author username and post slug.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
username: z.string().min(1),
|
|
73
|
+
slug: z.string().min(1),
|
|
74
|
+
},
|
|
75
|
+
}, async ({ username, slug }) => withAuthenticatedClient((client) => client.get(`/users/${encodeURIComponent(username)}/posts/${encodeURIComponent(slug)}`)));
|
|
76
|
+
await server.connect(new StdioServerTransport());
|
|
77
|
+
}
|
|
78
|
+
async function createAuthenticatedClient() {
|
|
79
|
+
const authFile = await readAuthFile();
|
|
80
|
+
if (!authFile) {
|
|
81
|
+
throw new Error("Run `openlog login` before using OpenLog MCP tools.");
|
|
82
|
+
}
|
|
83
|
+
return new OpenLogApiClient({
|
|
84
|
+
accessToken: authFile.accessToken,
|
|
85
|
+
apiBaseUrl: authFile.apiBaseUrl,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function withAuthenticatedClient(callback) {
|
|
89
|
+
try {
|
|
90
|
+
const client = await createAuthenticatedClient();
|
|
91
|
+
const result = await callback(client);
|
|
92
|
+
return textResult(result);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: error instanceof Error ? error.message : String(error),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function textResult(value) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitae9999/openlog-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Official OpenLog CLI and MCP server.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/kitae9999/OpenLog.git",
|
|
10
|
+
"directory": "packages/openlog-cli"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"openlog": "dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && chmod 755 dist/index.js",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"prepack": "npm run build",
|
|
19
|
+
"start": "node dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.8.0",
|
|
34
|
+
"tsx": "^4.22.0",
|
|
35
|
+
"typescript": "^6.0.3"
|
|
36
|
+
}
|
|
37
|
+
}
|