@kitae9999/openlog-cli 0.2.0 → 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/README.md +1 -0
- package/dist/api-client.js +48 -1
- package/dist/auth-store.js +26 -3
- package/dist/authenticated-client.js +34 -0
- package/dist/index.js +22 -10
- package/dist/login.js +4 -4
- package/dist/mcp-server.js +24 -14
- package/package.json +3 -2
package/README.md
CHANGED
package/dist/api-client.js
CHANGED
|
@@ -12,9 +12,14 @@ export class ApiError extends Error {
|
|
|
12
12
|
export class OpenLogApiClient {
|
|
13
13
|
apiBaseUrl;
|
|
14
14
|
accessToken;
|
|
15
|
+
refreshToken;
|
|
16
|
+
onTokenRefresh;
|
|
17
|
+
refreshPromise;
|
|
15
18
|
constructor(options = {}) {
|
|
16
19
|
this.apiBaseUrl = (options.apiBaseUrl ?? getApiBaseUrl()).replace(/\/$/, "");
|
|
17
20
|
this.accessToken = options.accessToken;
|
|
21
|
+
this.refreshToken = options.refreshToken;
|
|
22
|
+
this.onTokenRefresh = options.onTokenRefresh;
|
|
18
23
|
}
|
|
19
24
|
async get(path) {
|
|
20
25
|
return this.request(path, { method: "GET" });
|
|
@@ -26,6 +31,13 @@ export class OpenLogApiClient {
|
|
|
26
31
|
body: body ? JSON.stringify(body) : undefined,
|
|
27
32
|
});
|
|
28
33
|
}
|
|
34
|
+
async put(path, body) {
|
|
35
|
+
return this.request(path, {
|
|
36
|
+
method: "PUT",
|
|
37
|
+
headers: body ? { "content-type": "application/json" } : undefined,
|
|
38
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
29
41
|
async postNoContent(path, body) {
|
|
30
42
|
await this.request(path, {
|
|
31
43
|
method: "POST",
|
|
@@ -40,7 +52,7 @@ export class OpenLogApiClient {
|
|
|
40
52
|
body: body ? JSON.stringify(body) : undefined,
|
|
41
53
|
});
|
|
42
54
|
}
|
|
43
|
-
async request(path, init) {
|
|
55
|
+
async request(path, init, allowRefresh = true) {
|
|
44
56
|
const url = `${this.apiBaseUrl}${path}`;
|
|
45
57
|
const response = await fetch(url, {
|
|
46
58
|
...init,
|
|
@@ -51,6 +63,10 @@ export class OpenLogApiClient {
|
|
|
51
63
|
: {}),
|
|
52
64
|
},
|
|
53
65
|
});
|
|
66
|
+
if (response.status === 401 && allowRefresh && this.refreshToken) {
|
|
67
|
+
await this.refreshAccessToken();
|
|
68
|
+
return this.request(path, init, false);
|
|
69
|
+
}
|
|
54
70
|
if (!response.ok) {
|
|
55
71
|
throw new ApiError(response.status, url, await readErrorMessage(response));
|
|
56
72
|
}
|
|
@@ -59,6 +75,37 @@ export class OpenLogApiClient {
|
|
|
59
75
|
}
|
|
60
76
|
return (await response.json());
|
|
61
77
|
}
|
|
78
|
+
async refreshAccessToken() {
|
|
79
|
+
if (!this.refreshPromise) {
|
|
80
|
+
this.refreshPromise = this.performTokenRefresh().finally(() => {
|
|
81
|
+
this.refreshPromise = undefined;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
await this.refreshPromise;
|
|
85
|
+
}
|
|
86
|
+
async performTokenRefresh() {
|
|
87
|
+
const response = await fetch(`${this.apiBaseUrl}/auth/device/refresh`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "content-type": "application/json" },
|
|
90
|
+
body: JSON.stringify({ refreshToken: this.refreshToken }),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new ApiError(response.status, `${this.apiBaseUrl}/auth/device/refresh`, await readErrorMessage(response));
|
|
94
|
+
}
|
|
95
|
+
const tokens = (await response.json());
|
|
96
|
+
if (!isRefreshedTokens(tokens)) {
|
|
97
|
+
throw new Error("OpenLog refresh response is incomplete.");
|
|
98
|
+
}
|
|
99
|
+
this.accessToken = tokens.accessToken;
|
|
100
|
+
this.refreshToken = tokens.refreshToken;
|
|
101
|
+
await this.onTokenRefresh?.(tokens);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function isRefreshedTokens(value) {
|
|
105
|
+
return Boolean(value.accessToken &&
|
|
106
|
+
value.refreshToken &&
|
|
107
|
+
Number.isFinite(value.expiresIn) &&
|
|
108
|
+
Number.isFinite(value.refreshExpiresIn));
|
|
62
109
|
}
|
|
63
110
|
async function readErrorMessage(response) {
|
|
64
111
|
try {
|
package/dist/auth-store.js
CHANGED
|
@@ -10,8 +10,11 @@ export async function readAuthFile() {
|
|
|
10
10
|
}
|
|
11
11
|
return {
|
|
12
12
|
accessToken: parsed.accessToken,
|
|
13
|
+
refreshToken: parsed.refreshToken,
|
|
13
14
|
apiBaseUrl: parsed.apiBaseUrl,
|
|
14
15
|
createdAt: parsed.createdAt,
|
|
16
|
+
accessTokenExpiresAt: parsed.accessTokenExpiresAt,
|
|
17
|
+
refreshTokenExpiresAt: parsed.refreshTokenExpiresAt,
|
|
15
18
|
};
|
|
16
19
|
}
|
|
17
20
|
catch (error) {
|
|
@@ -24,20 +27,40 @@ export async function readAuthFile() {
|
|
|
24
27
|
throw error;
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
|
-
export async function writeAuthFile(
|
|
30
|
+
export async function writeAuthFile(tokens) {
|
|
28
31
|
const authFile = {
|
|
29
|
-
accessToken,
|
|
32
|
+
accessToken: tokens.accessToken,
|
|
33
|
+
refreshToken: tokens.refreshToken,
|
|
30
34
|
apiBaseUrl: getApiBaseUrl(),
|
|
31
35
|
createdAt: new Date().toISOString(),
|
|
36
|
+
accessTokenExpiresAt: expiresAt(tokens.expiresIn),
|
|
37
|
+
refreshTokenExpiresAt: expiresAt(tokens.refreshExpiresIn),
|
|
32
38
|
};
|
|
39
|
+
await persistAuthFile(authFile);
|
|
40
|
+
return authFile;
|
|
41
|
+
}
|
|
42
|
+
export async function updateAuthFileTokens(authFile, tokens) {
|
|
43
|
+
const updatedAuthFile = {
|
|
44
|
+
...authFile,
|
|
45
|
+
accessToken: tokens.accessToken,
|
|
46
|
+
refreshToken: tokens.refreshToken,
|
|
47
|
+
accessTokenExpiresAt: expiresAt(tokens.expiresIn),
|
|
48
|
+
refreshTokenExpiresAt: expiresAt(tokens.refreshExpiresIn),
|
|
49
|
+
};
|
|
50
|
+
await persistAuthFile(updatedAuthFile);
|
|
51
|
+
return updatedAuthFile;
|
|
52
|
+
}
|
|
53
|
+
async function persistAuthFile(authFile) {
|
|
33
54
|
const authFilePath = getAuthFilePath();
|
|
34
55
|
await mkdir(path.dirname(authFilePath), { recursive: true, mode: 0o700 });
|
|
35
56
|
await writeFile(authFilePath, `${JSON.stringify(authFile, null, 2)}\n`, {
|
|
36
57
|
mode: 0o600,
|
|
37
58
|
});
|
|
38
59
|
await chmod(authFilePath, 0o600);
|
|
39
|
-
return authFile;
|
|
40
60
|
}
|
|
41
61
|
export async function deleteAuthFile() {
|
|
42
62
|
await rm(getAuthFilePath(), { force: true });
|
|
43
63
|
}
|
|
64
|
+
function expiresAt(expiresIn) {
|
|
65
|
+
return new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { OpenLogApiClient } from "./api-client.js";
|
|
2
|
+
import { readAuthFile, updateAuthFileTokens, } from "./auth-store.js";
|
|
3
|
+
let cachedClient;
|
|
4
|
+
export async function createAuthenticatedApiClient() {
|
|
5
|
+
const authFile = await readAuthFile();
|
|
6
|
+
if (!authFile) {
|
|
7
|
+
throw new Error("Run `openlog login` before using OpenLog.");
|
|
8
|
+
}
|
|
9
|
+
const fingerprint = authFingerprint(authFile);
|
|
10
|
+
if (cachedClient?.fingerprint === fingerprint) {
|
|
11
|
+
return cachedClient.client;
|
|
12
|
+
}
|
|
13
|
+
const client = new OpenLogApiClient({
|
|
14
|
+
accessToken: authFile.accessToken,
|
|
15
|
+
refreshToken: authFile.refreshToken,
|
|
16
|
+
apiBaseUrl: authFile.apiBaseUrl,
|
|
17
|
+
onTokenRefresh: async (tokens) => {
|
|
18
|
+
const updatedAuthFile = await updateAuthFileTokens(authFile, tokens);
|
|
19
|
+
cachedClient = {
|
|
20
|
+
fingerprint: authFingerprint(updatedAuthFile),
|
|
21
|
+
client,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
cachedClient = { fingerprint, client };
|
|
26
|
+
return client;
|
|
27
|
+
}
|
|
28
|
+
function authFingerprint(authFile) {
|
|
29
|
+
return [
|
|
30
|
+
authFile.apiBaseUrl,
|
|
31
|
+
authFile.accessToken,
|
|
32
|
+
authFile.refreshToken ?? "",
|
|
33
|
+
].join("\u0000");
|
|
34
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { login } from "./login.js";
|
|
|
4
4
|
import { runMcpServer } from "./mcp-server.js";
|
|
5
5
|
import { installMcp, printMcpInstallHelp } from "./mcp-install.js";
|
|
6
6
|
import { ApiError, OpenLogApiClient } from "./api-client.js";
|
|
7
|
+
import { createAuthenticatedApiClient } from "./authenticated-client.js";
|
|
7
8
|
const command = process.argv[2] ?? "help";
|
|
8
9
|
const subcommand = process.argv[3];
|
|
9
10
|
try {
|
|
@@ -11,8 +12,7 @@ try {
|
|
|
11
12
|
await login();
|
|
12
13
|
}
|
|
13
14
|
else if (command === "logout") {
|
|
14
|
-
await
|
|
15
|
-
console.log("Logged out.");
|
|
15
|
+
await logout();
|
|
16
16
|
}
|
|
17
17
|
else if (command === "whoami") {
|
|
18
18
|
await whoami();
|
|
@@ -56,17 +56,28 @@ async function installMcpCommand() {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
async function whoami() {
|
|
59
|
-
const
|
|
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
|
-
});
|
|
59
|
+
const apiClient = await createAuthenticatedApiClient();
|
|
67
60
|
const me = await apiClient.get("/auth/me");
|
|
68
61
|
console.log(JSON.stringify(me, null, 2));
|
|
69
62
|
}
|
|
63
|
+
async function logout() {
|
|
64
|
+
const authFile = await readAuthFile();
|
|
65
|
+
if (authFile?.refreshToken) {
|
|
66
|
+
try {
|
|
67
|
+
const apiClient = new OpenLogApiClient({
|
|
68
|
+
apiBaseUrl: authFile.apiBaseUrl,
|
|
69
|
+
});
|
|
70
|
+
await apiClient.postNoContent("/auth/device/revoke", {
|
|
71
|
+
refreshToken: authFile.refreshToken,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error(`Warning: remote session revoke failed: ${formatCliError(error)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await deleteAuthFile();
|
|
79
|
+
console.log("Logged out.");
|
|
80
|
+
}
|
|
70
81
|
function printMcpStartupHint() {
|
|
71
82
|
if (!process.stderr.isTTY) {
|
|
72
83
|
return;
|
|
@@ -94,6 +105,7 @@ Available tools:
|
|
|
94
105
|
list_my_liked_posts
|
|
95
106
|
upload_post_image
|
|
96
107
|
publish_post
|
|
108
|
+
push_working_brief
|
|
97
109
|
get_post_detail
|
|
98
110
|
`);
|
|
99
111
|
}
|
package/dist/login.js
CHANGED
|
@@ -14,18 +14,18 @@ export async function login() {
|
|
|
14
14
|
? "The approval page was opened in your browser."
|
|
15
15
|
: "Open the URL above in your browser.");
|
|
16
16
|
console.log("Waiting for approval...");
|
|
17
|
-
const
|
|
18
|
-
await writeAuthFile(
|
|
17
|
+
const tokens = await pollForTokens(apiClient, deviceLogin);
|
|
18
|
+
await writeAuthFile(tokens);
|
|
19
19
|
console.log("Login successful.");
|
|
20
20
|
}
|
|
21
|
-
async function
|
|
21
|
+
async function pollForTokens(apiClient, deviceLogin) {
|
|
22
22
|
const deadline = Date.now() + deviceLogin.expiresIn * 1000;
|
|
23
23
|
let interval = deviceLogin.interval;
|
|
24
24
|
while (Date.now() < deadline) {
|
|
25
25
|
await sleep(interval * 1000);
|
|
26
26
|
const response = await apiClient.post("/auth/device/token", { deviceCode: deviceLogin.deviceCode });
|
|
27
27
|
if (response.status === "APPROVED") {
|
|
28
|
-
return response
|
|
28
|
+
return response;
|
|
29
29
|
}
|
|
30
30
|
interval = response.interval ?? interval;
|
|
31
31
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { OpenLogApiClient } from "./api-client.js";
|
|
5
4
|
import { readAuthFile } from "./auth-store.js";
|
|
5
|
+
import { createAuthenticatedApiClient } from "./authenticated-client.js";
|
|
6
6
|
import { getApiBaseUrl, getWebBaseUrl } from "./config.js";
|
|
7
7
|
import { uploadPostImage } from "./post-image-upload.js";
|
|
8
8
|
const WRITE_TOOL_ANNOTATIONS = {
|
|
@@ -14,7 +14,7 @@ const WRITE_TOOL_ANNOTATIONS = {
|
|
|
14
14
|
export async function runMcpServer() {
|
|
15
15
|
const server = new McpServer({
|
|
16
16
|
name: "openlog",
|
|
17
|
-
version: "0.
|
|
17
|
+
version: "1.0.0",
|
|
18
18
|
});
|
|
19
19
|
server.registerTool("get_auth_status", {
|
|
20
20
|
title: "Get OpenLog Auth Status",
|
|
@@ -31,7 +31,7 @@ export async function runMcpServer() {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
apiBaseUrl = authFile.apiBaseUrl;
|
|
34
|
-
const me = await
|
|
34
|
+
const me = await createAuthenticatedApiClient().then((client) => client.get("/auth/me"));
|
|
35
35
|
return textResult({
|
|
36
36
|
authenticated: true,
|
|
37
37
|
apiBaseUrl: authFile.apiBaseUrl,
|
|
@@ -153,6 +153,26 @@ export async function runMcpServer() {
|
|
|
153
153
|
url: new URL(postPath, `${getWebBaseUrl()}/`).toString(),
|
|
154
154
|
};
|
|
155
155
|
}));
|
|
156
|
+
server.registerTool("push_working_brief", {
|
|
157
|
+
title: "Push OpenLog Working Brief",
|
|
158
|
+
description: "Overwrite the workspace Now Working brief with a short status update: what was done, what is still open, and optional task/branch context. Latest write wins.",
|
|
159
|
+
annotations: {
|
|
160
|
+
...WRITE_TOOL_ANNOTATIONS,
|
|
161
|
+
idempotentHint: true,
|
|
162
|
+
},
|
|
163
|
+
inputSchema: {
|
|
164
|
+
workspaceId: z.number().int().positive(),
|
|
165
|
+
title: z.string().min(1).max(255),
|
|
166
|
+
prose: z.string().min(1),
|
|
167
|
+
taskId: z.number().int().positive().optional(),
|
|
168
|
+
branch: z.string().max(255).optional(),
|
|
169
|
+
},
|
|
170
|
+
}, async ({ workspaceId, title, prose, taskId, branch }) => withAuthenticatedClient((client) => client.put(`/workspaces/${workspaceId}/working-brief`, {
|
|
171
|
+
title,
|
|
172
|
+
prose,
|
|
173
|
+
taskId: taskId ?? null,
|
|
174
|
+
branch: branch ?? null,
|
|
175
|
+
})));
|
|
156
176
|
server.registerTool("get_post_detail", {
|
|
157
177
|
title: "Get OpenLog Post Detail",
|
|
158
178
|
description: "Return a public OpenLog post detail by author username and post slug.",
|
|
@@ -212,19 +232,9 @@ function createContentPreview(content) {
|
|
|
212
232
|
function buildPublicPostPath(username, slug) {
|
|
213
233
|
return `/@${encodeURIComponent(username)}/posts/${encodeURIComponent(slug)}`;
|
|
214
234
|
}
|
|
215
|
-
async function createAuthenticatedClient() {
|
|
216
|
-
const authFile = await readAuthFile();
|
|
217
|
-
if (!authFile) {
|
|
218
|
-
throw new Error("Run `openlog login` before using OpenLog MCP tools.");
|
|
219
|
-
}
|
|
220
|
-
return new OpenLogApiClient({
|
|
221
|
-
accessToken: authFile.accessToken,
|
|
222
|
-
apiBaseUrl: authFile.apiBaseUrl,
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
235
|
async function withAuthenticatedClient(callback) {
|
|
226
236
|
try {
|
|
227
|
-
const client = await
|
|
237
|
+
const client = await createAuthenticatedApiClient();
|
|
228
238
|
const result = await callback(client);
|
|
229
239
|
return textResult(result);
|
|
230
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitae9999/openlog-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Official OpenLog CLI and MCP server.",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"build": "tsc && chmod 755 dist/index.js",
|
|
17
17
|
"dev": "tsx src/index.ts",
|
|
18
18
|
"prepack": "npm run build",
|
|
19
|
-
"start": "node dist/index.js"
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"test": "tsx --test test/**/*.test.ts"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"dist",
|