@kitae9999/openlog-cli 1.0.0 → 1.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 +20 -11
- package/dist/api-client.js +15 -1
- package/dist/config.js +4 -0
- package/dist/index.js +12 -32
- package/dist/mcp-install.js +5 -1
- package/dist/mcp-permissions-command.js +45 -0
- package/dist/mcp-permissions.js +103 -0
- package/dist/mcp-server.js +89 -93
- package/dist/mcp-toolkit.js +115 -0
- package/dist/mcp-workspace-tools.js +645 -0
- package/package.json +1 -1
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ApiError } from "./api-client.js";
|
|
3
|
+
import { createContentPreview, DELETE_TOOL_ANNOTATIONS, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, READ_TOOL_ANNOTATIONS, WRITE_TOOL_ANNOTATIONS, } from "./mcp-toolkit.js";
|
|
4
|
+
const POSITIVE_ID = z.number().int().positive();
|
|
5
|
+
// MCP 입력 단계에서 백엔드 계약과 동일한 날짜·enum 값만 허용한다.
|
|
6
|
+
const ISO_DATE = z
|
|
7
|
+
.string()
|
|
8
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Expected an ISO date in YYYY-MM-DD format.")
|
|
9
|
+
.refine(isValidIsoDate, "Expected a valid calendar date.");
|
|
10
|
+
const TASK_STATUS = z.enum(["TODO", "DOING", "DONE"]);
|
|
11
|
+
const LOG_KIND = z.enum(["ISSUE", "FIX", "DECISION", "NOTE"]);
|
|
12
|
+
const LOG_STATUS = z.enum(["NONE", "OPEN", "CLOSED"]);
|
|
13
|
+
const OUTPUT_STATUS = z.enum(["DRAFT", "EXPORTED", "PUBLISHED"]);
|
|
14
|
+
const TASK_LINK_RELATION = z.enum(["PRECEDES", "BLOCKS", "RELATES_TO"]);
|
|
15
|
+
const LOG_LINK_RELATION = z.enum(["FIXES", "RELATES_TO", "SUPERSEDES"]);
|
|
16
|
+
const WORKSPACE_NODE_TYPE = z.enum(["TASK", "LOG", "OUTPUT", "MEMORY"]);
|
|
17
|
+
const CROSS_LINK_RELATION = z.enum([
|
|
18
|
+
"RELATES_TO",
|
|
19
|
+
"REFERENCES",
|
|
20
|
+
"SUPPORTS",
|
|
21
|
+
"DERIVED_FROM",
|
|
22
|
+
]);
|
|
23
|
+
const TODO_LIST_INPUT = z
|
|
24
|
+
.object({
|
|
25
|
+
workspaceId: POSITIVE_ID,
|
|
26
|
+
plannedFor: ISO_DATE.optional(),
|
|
27
|
+
from: ISO_DATE.optional(),
|
|
28
|
+
to: ISO_DATE.optional(),
|
|
29
|
+
})
|
|
30
|
+
.superRefine((value, context) => {
|
|
31
|
+
// Todo 조회는 단일 날짜 또는 완전한 기간 중 한 방식만 받는다.
|
|
32
|
+
const usesSingleDate = value.plannedFor && !value.from && !value.to;
|
|
33
|
+
const usesRange = !value.plannedFor && value.from && value.to;
|
|
34
|
+
if (!usesSingleDate && !usesRange) {
|
|
35
|
+
context.addIssue({
|
|
36
|
+
code: "custom",
|
|
37
|
+
message: "Provide plannedFor, or provide both from and to.",
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (value.from && value.to) {
|
|
42
|
+
validateDateRange(value.from, value.to, context);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const ACTIVITY_RANGE_INPUT = z
|
|
46
|
+
.object({
|
|
47
|
+
workspaceId: POSITIVE_ID,
|
|
48
|
+
from: ISO_DATE,
|
|
49
|
+
to: ISO_DATE,
|
|
50
|
+
})
|
|
51
|
+
.superRefine((value, context) => {
|
|
52
|
+
validateDateRange(value.from, value.to, context);
|
|
53
|
+
});
|
|
54
|
+
export function registerWorkspaceTools(registry, webBaseUrl) {
|
|
55
|
+
registerWorkspaceReadTools(registry);
|
|
56
|
+
registerTaskTools(registry);
|
|
57
|
+
registerLogTools(registry);
|
|
58
|
+
registerTodoTools(registry);
|
|
59
|
+
registerMemoryTools(registry);
|
|
60
|
+
registerOutputTools(registry, webBaseUrl);
|
|
61
|
+
registerLinkTools(registry);
|
|
62
|
+
registerWorkspaceDeleteTools(registry);
|
|
63
|
+
}
|
|
64
|
+
function registerWorkspaceReadTools(registry) {
|
|
65
|
+
registry.registerAuthenticated("list_workspaces", "read", {
|
|
66
|
+
title: "List OpenLog Workspaces",
|
|
67
|
+
description: "List workspaces owned by the authenticated OpenLog user.",
|
|
68
|
+
inputSchema: {},
|
|
69
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
70
|
+
}, (client) => client.get("/workspaces"));
|
|
71
|
+
registry.registerAuthenticated("get_workspace", "read", {
|
|
72
|
+
title: "Get OpenLog Workspace",
|
|
73
|
+
description: "Return one owned OpenLog workspace.",
|
|
74
|
+
inputSchema: { workspaceId: POSITIVE_ID },
|
|
75
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
76
|
+
}, (client, { workspaceId }) => client.get(`/workspaces/${workspaceId}`));
|
|
77
|
+
registry.registerAuthenticated("get_working_brief", "read", {
|
|
78
|
+
title: "Get OpenLog Working Brief",
|
|
79
|
+
description: "Return the workspace Now Working brief, or null when no brief exists.",
|
|
80
|
+
inputSchema: { workspaceId: POSITIVE_ID },
|
|
81
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
82
|
+
}, async (client, { workspaceId }) => {
|
|
83
|
+
try {
|
|
84
|
+
return {
|
|
85
|
+
workingBrief: await client.get(`/workspaces/${workspaceId}/working-brief`),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
90
|
+
// working brief 미생성 상태는 MCP 오류가 아니라 명시적인 null로 노출한다.
|
|
91
|
+
return { workingBrief: null };
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
registry.registerAuthenticated("push_working_brief", "write", {
|
|
97
|
+
title: "Push OpenLog Working Brief",
|
|
98
|
+
description: "Overwrite the workspace Now Working brief with a short status update. Latest write wins.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
workspaceId: POSITIVE_ID,
|
|
101
|
+
title: z.string().min(1).max(255),
|
|
102
|
+
prose: z.string().min(1),
|
|
103
|
+
taskId: POSITIVE_ID.nullable().default(null),
|
|
104
|
+
branch: z.string().max(255).nullable().default(null),
|
|
105
|
+
},
|
|
106
|
+
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
107
|
+
}, (client, { workspaceId, title, prose, taskId, branch }) => client.put(`/workspaces/${workspaceId}/working-brief`, {
|
|
108
|
+
title,
|
|
109
|
+
prose,
|
|
110
|
+
taskId,
|
|
111
|
+
branch,
|
|
112
|
+
}));
|
|
113
|
+
registry.registerAuthenticated("get_workspace_activity", "read", {
|
|
114
|
+
title: "Get OpenLog Workspace Activity",
|
|
115
|
+
description: "Return daily log counts for an inclusive date range.",
|
|
116
|
+
inputSchema: ACTIVITY_RANGE_INPUT,
|
|
117
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
118
|
+
}, (client, { workspaceId, from, to }) => client.get(withQuery(`/workspaces/${workspaceId}/activity`, { from, to })));
|
|
119
|
+
registry.registerAuthenticated("get_workspace_activity_day_logs", "read", {
|
|
120
|
+
title: "Get OpenLog Workspace Activity Day Logs",
|
|
121
|
+
description: "Return workspace logs created on one calendar date.",
|
|
122
|
+
inputSchema: { workspaceId: POSITIVE_ID, date: ISO_DATE },
|
|
123
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
124
|
+
}, (client, { workspaceId, date }) => client.get(`/workspaces/${workspaceId}/activity/${date}/logs`));
|
|
125
|
+
}
|
|
126
|
+
function registerTaskTools(registry) {
|
|
127
|
+
registry.registerAuthenticated("list_workspace_tasks", "read", {
|
|
128
|
+
title: "List OpenLog Workspace Tasks",
|
|
129
|
+
description: "List workspace tasks with optional status and cursor filters.",
|
|
130
|
+
inputSchema: {
|
|
131
|
+
workspaceId: POSITIVE_ID,
|
|
132
|
+
status: TASK_STATUS.optional(),
|
|
133
|
+
cursor: z.string().min(1).optional(),
|
|
134
|
+
size: z.number().int().min(1).max(20).default(20),
|
|
135
|
+
},
|
|
136
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
137
|
+
}, (client, { workspaceId, status, cursor, size }) => client.get(withQuery(`/workspaces/${workspaceId}/tasks`, {
|
|
138
|
+
status,
|
|
139
|
+
cursor,
|
|
140
|
+
size,
|
|
141
|
+
})));
|
|
142
|
+
registry.registerAuthenticated("get_workspace_task", "read", {
|
|
143
|
+
title: "Get OpenLog Workspace Task",
|
|
144
|
+
description: "Return one workspace task in detail.",
|
|
145
|
+
inputSchema: { workspaceId: POSITIVE_ID, taskId: POSITIVE_ID },
|
|
146
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
147
|
+
}, (client, { workspaceId, taskId }) => client.get(`/workspaces/${workspaceId}/tasks/${taskId}`));
|
|
148
|
+
registry.registerAuthenticated("create_workspace_task", "write", {
|
|
149
|
+
title: "Create OpenLog Workspace Task",
|
|
150
|
+
description: "Create a task in an existing workspace.",
|
|
151
|
+
inputSchema: {
|
|
152
|
+
workspaceId: POSITIVE_ID,
|
|
153
|
+
title: z.string().min(1),
|
|
154
|
+
description: z.string().nullable().default(null),
|
|
155
|
+
content: z.string().nullable().default(null),
|
|
156
|
+
status: TASK_STATUS.default("TODO"),
|
|
157
|
+
},
|
|
158
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
159
|
+
}, (client, { workspaceId, title, description, content, status }) => client.post(`/workspaces/${workspaceId}/tasks`, {
|
|
160
|
+
title,
|
|
161
|
+
description,
|
|
162
|
+
content,
|
|
163
|
+
status,
|
|
164
|
+
}));
|
|
165
|
+
registry.registerAuthenticated("update_workspace_task", "write", {
|
|
166
|
+
title: "Update OpenLog Workspace Task",
|
|
167
|
+
description: "Replace the editable fields of an existing workspace task.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
workspaceId: POSITIVE_ID,
|
|
170
|
+
taskId: POSITIVE_ID,
|
|
171
|
+
title: z.string().min(1),
|
|
172
|
+
description: z.string().nullable().default(null),
|
|
173
|
+
content: z.string().nullable().default(null),
|
|
174
|
+
status: TASK_STATUS,
|
|
175
|
+
},
|
|
176
|
+
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
177
|
+
}, (client, { workspaceId, taskId, title, description, content, status }) => client.put(`/workspaces/${workspaceId}/tasks/${taskId}`, {
|
|
178
|
+
title,
|
|
179
|
+
description,
|
|
180
|
+
content,
|
|
181
|
+
status,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
function registerLogTools(registry) {
|
|
185
|
+
registry.registerAuthenticated("list_workspace_logs", "read", {
|
|
186
|
+
title: "List OpenLog Workspace Logs",
|
|
187
|
+
description: "List workspace logs with optional task and cursor filters.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
workspaceId: POSITIVE_ID,
|
|
190
|
+
taskId: POSITIVE_ID.optional(),
|
|
191
|
+
cursor: z.string().min(1).optional(),
|
|
192
|
+
size: z.number().int().min(1).max(20).default(20),
|
|
193
|
+
},
|
|
194
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
195
|
+
}, (client, { workspaceId, taskId, cursor, size }) => client.get(withQuery(`/workspaces/${workspaceId}/logs`, {
|
|
196
|
+
taskId,
|
|
197
|
+
cursor,
|
|
198
|
+
size,
|
|
199
|
+
})));
|
|
200
|
+
registry.registerAuthenticated("get_workspace_log", "read", {
|
|
201
|
+
title: "Get OpenLog Workspace Log",
|
|
202
|
+
description: "Return one workspace log in detail.",
|
|
203
|
+
inputSchema: { workspaceId: POSITIVE_ID, logId: POSITIVE_ID },
|
|
204
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
205
|
+
}, (client, { workspaceId, logId }) => client.get(`/workspaces/${workspaceId}/logs/${logId}`));
|
|
206
|
+
registry.registerAuthenticated("create_workspace_log", "write", {
|
|
207
|
+
title: "Create OpenLog Workspace Log",
|
|
208
|
+
description: "Create an ISSUE, FIX, DECISION, or NOTE log. ISSUE uses OPEN/CLOSED; other kinds use NONE.",
|
|
209
|
+
inputSchema: {
|
|
210
|
+
workspaceId: POSITIVE_ID,
|
|
211
|
+
kind: LOG_KIND,
|
|
212
|
+
title: z.string().min(1),
|
|
213
|
+
content: z.string().min(1),
|
|
214
|
+
summary: z.string().nullable().default(null),
|
|
215
|
+
taskId: POSITIVE_ID.nullable().default(null),
|
|
216
|
+
status: LOG_STATUS.nullable().default(null),
|
|
217
|
+
},
|
|
218
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
219
|
+
}, (client, { workspaceId, kind, title, content, summary, taskId, status }) => client.post(`/workspaces/${workspaceId}/logs`, {
|
|
220
|
+
kind,
|
|
221
|
+
title,
|
|
222
|
+
content,
|
|
223
|
+
summary,
|
|
224
|
+
taskId,
|
|
225
|
+
status,
|
|
226
|
+
}));
|
|
227
|
+
registry.registerAuthenticated("update_workspace_log", "write", {
|
|
228
|
+
title: "Update OpenLog Workspace Log",
|
|
229
|
+
description: "Replace the editable fields of a log. Its original kind is preserved.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
workspaceId: POSITIVE_ID,
|
|
232
|
+
logId: POSITIVE_ID,
|
|
233
|
+
title: z.string().min(1),
|
|
234
|
+
content: z.string().min(1),
|
|
235
|
+
summary: z.string().nullable().default(null),
|
|
236
|
+
taskId: POSITIVE_ID.nullable().default(null),
|
|
237
|
+
status: LOG_STATUS.nullable().default(null),
|
|
238
|
+
},
|
|
239
|
+
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
240
|
+
}, (client, { workspaceId, logId, title, content, summary, taskId, status }) => client.put(`/workspaces/${workspaceId}/logs/${logId}`, {
|
|
241
|
+
title,
|
|
242
|
+
content,
|
|
243
|
+
summary,
|
|
244
|
+
taskId,
|
|
245
|
+
status,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
function registerTodoTools(registry) {
|
|
249
|
+
registry.registerAuthenticated("list_workspace_todos", "read", {
|
|
250
|
+
title: "List OpenLog Workspace Todos",
|
|
251
|
+
description: "List todos for one date or an inclusive date range of at most 366 days.",
|
|
252
|
+
inputSchema: TODO_LIST_INPUT,
|
|
253
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
254
|
+
}, (client, { workspaceId, plannedFor, from, to }) => client.get(withQuery(`/workspaces/${workspaceId}/todos`, {
|
|
255
|
+
plannedFor,
|
|
256
|
+
from,
|
|
257
|
+
to,
|
|
258
|
+
})));
|
|
259
|
+
registry.registerAuthenticated("create_workspace_todo", "write", {
|
|
260
|
+
title: "Create OpenLog Workspace Todo",
|
|
261
|
+
description: "Create a dated todo, optionally linked to a task.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
workspaceId: POSITIVE_ID,
|
|
264
|
+
title: z.string().min(1),
|
|
265
|
+
plannedFor: ISO_DATE,
|
|
266
|
+
taskId: POSITIVE_ID.nullable().default(null),
|
|
267
|
+
},
|
|
268
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
269
|
+
}, (client, { workspaceId, title, plannedFor, taskId }) => client.post(`/workspaces/${workspaceId}/todos`, {
|
|
270
|
+
title,
|
|
271
|
+
plannedFor,
|
|
272
|
+
taskId,
|
|
273
|
+
}));
|
|
274
|
+
registry.registerAuthenticated("set_workspace_todo_done", "write", {
|
|
275
|
+
title: "Set OpenLog Workspace Todo Done",
|
|
276
|
+
description: "Mark a workspace todo done or reopen it.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
workspaceId: POSITIVE_ID,
|
|
279
|
+
todoId: POSITIVE_ID,
|
|
280
|
+
done: z.boolean(),
|
|
281
|
+
},
|
|
282
|
+
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
283
|
+
}, (client, { workspaceId, todoId, done }) => client.patch(`/workspaces/${workspaceId}/todos/${todoId}`, { done }));
|
|
284
|
+
}
|
|
285
|
+
function registerMemoryTools(registry) {
|
|
286
|
+
registry.registerAuthenticated("list_workspace_memories", "read", {
|
|
287
|
+
title: "List OpenLog Workspace Memories",
|
|
288
|
+
description: "List reusable workspace memories with cursor pagination.",
|
|
289
|
+
inputSchema: {
|
|
290
|
+
workspaceId: POSITIVE_ID,
|
|
291
|
+
cursor: z.string().min(1).optional(),
|
|
292
|
+
size: z.number().int().min(1).max(50).default(20),
|
|
293
|
+
},
|
|
294
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
295
|
+
}, (client, { workspaceId, cursor, size }) => client.get(withQuery(`/workspaces/${workspaceId}/memories`, { cursor, size })));
|
|
296
|
+
registry.registerAuthenticated("get_workspace_memory", "read", {
|
|
297
|
+
title: "Get OpenLog Workspace Memory",
|
|
298
|
+
description: "Return one workspace memory.",
|
|
299
|
+
inputSchema: { workspaceId: POSITIVE_ID, memoryId: POSITIVE_ID },
|
|
300
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
301
|
+
}, (client, { workspaceId, memoryId }) => client.get(`/workspaces/${workspaceId}/memories/${memoryId}`));
|
|
302
|
+
registry.registerAuthenticated("create_workspace_memory", "write", {
|
|
303
|
+
title: "Create OpenLog Workspace Memory",
|
|
304
|
+
description: "Create reusable workspace context, optionally linked to a task.",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
workspaceId: POSITIVE_ID,
|
|
307
|
+
title: z.string().min(1),
|
|
308
|
+
content: z.string().min(1),
|
|
309
|
+
taskId: POSITIVE_ID.nullable().default(null),
|
|
310
|
+
},
|
|
311
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
312
|
+
}, (client, { workspaceId, title, content, taskId }) => client.post(`/workspaces/${workspaceId}/memories`, {
|
|
313
|
+
title,
|
|
314
|
+
content,
|
|
315
|
+
taskId,
|
|
316
|
+
}));
|
|
317
|
+
registry.registerAuthenticated("create_workspace_memory_from_log", "write", {
|
|
318
|
+
title: "Create OpenLog Workspace Memory From Log",
|
|
319
|
+
description: "Create a memory from a log, or return the existing memory for that log.",
|
|
320
|
+
inputSchema: { workspaceId: POSITIVE_ID, logId: POSITIVE_ID },
|
|
321
|
+
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
322
|
+
}, (client, { workspaceId, logId }) => client.post(`/workspaces/${workspaceId}/logs/${logId}/memory`));
|
|
323
|
+
registry.registerAuthenticated("update_workspace_memory", "write", {
|
|
324
|
+
title: "Update OpenLog Workspace Memory",
|
|
325
|
+
description: "Replace the editable fields of a workspace memory.",
|
|
326
|
+
inputSchema: {
|
|
327
|
+
workspaceId: POSITIVE_ID,
|
|
328
|
+
memoryId: POSITIVE_ID,
|
|
329
|
+
title: z.string().min(1),
|
|
330
|
+
content: z.string().min(1),
|
|
331
|
+
taskId: POSITIVE_ID.nullable().default(null),
|
|
332
|
+
},
|
|
333
|
+
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
334
|
+
}, (client, { workspaceId, memoryId, title, content, taskId }) => client.put(`/workspaces/${workspaceId}/memories/${memoryId}`, {
|
|
335
|
+
title,
|
|
336
|
+
content,
|
|
337
|
+
taskId,
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
function registerOutputTools(registry, webBaseUrl) {
|
|
341
|
+
registry.registerAuthenticated("list_workspace_outputs", "read", {
|
|
342
|
+
title: "List OpenLog Workspace Outputs",
|
|
343
|
+
description: "List workspace outputs with an optional status filter.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
workspaceId: POSITIVE_ID,
|
|
346
|
+
status: OUTPUT_STATUS.optional(),
|
|
347
|
+
},
|
|
348
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
349
|
+
}, (client, { workspaceId, status }) => client.get(withQuery(`/workspaces/${workspaceId}/outputs`, { status })));
|
|
350
|
+
registry.registerAuthenticated("get_workspace_output", "read", {
|
|
351
|
+
title: "Get OpenLog Workspace Output",
|
|
352
|
+
description: "Return one output with its source tasks and logs.",
|
|
353
|
+
inputSchema: { workspaceId: POSITIVE_ID, outputId: POSITIVE_ID },
|
|
354
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
355
|
+
}, (client, { workspaceId, outputId }) => client.get(`/workspaces/${workspaceId}/outputs/${outputId}`));
|
|
356
|
+
registry.registerAuthenticated("create_workspace_output", "write", {
|
|
357
|
+
title: "Create OpenLog Workspace Output",
|
|
358
|
+
description: "Create a draft output from selected tasks and logs.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
workspaceId: POSITIVE_ID,
|
|
361
|
+
title: z.string().min(1),
|
|
362
|
+
content: z.string().min(1),
|
|
363
|
+
taskIds: z.array(POSITIVE_ID).default([]),
|
|
364
|
+
logIds: z.array(POSITIVE_ID).default([]),
|
|
365
|
+
},
|
|
366
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
367
|
+
}, (client, { workspaceId, title, content, taskIds, logIds }) => client.post(`/workspaces/${workspaceId}/outputs`, {
|
|
368
|
+
title,
|
|
369
|
+
content,
|
|
370
|
+
taskIds,
|
|
371
|
+
logIds,
|
|
372
|
+
}));
|
|
373
|
+
registry.registerAuthenticated("update_workspace_output", "write", {
|
|
374
|
+
title: "Update OpenLog Workspace Output",
|
|
375
|
+
description: "Replace a draft output and its selected source documents.",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
workspaceId: POSITIVE_ID,
|
|
378
|
+
outputId: POSITIVE_ID,
|
|
379
|
+
title: z.string().min(1),
|
|
380
|
+
content: z.string().min(1),
|
|
381
|
+
taskIds: z.array(POSITIVE_ID).default([]),
|
|
382
|
+
logIds: z.array(POSITIVE_ID).default([]),
|
|
383
|
+
},
|
|
384
|
+
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
385
|
+
}, (client, { workspaceId, outputId, title, content, taskIds, logIds }) => client.put(`/workspaces/${workspaceId}/outputs/${outputId}`, {
|
|
386
|
+
title,
|
|
387
|
+
content,
|
|
388
|
+
taskIds,
|
|
389
|
+
logIds,
|
|
390
|
+
}));
|
|
391
|
+
registry.registerAuthenticated("publish_workspace_output", "publish", {
|
|
392
|
+
title: "Publish OpenLog Workspace Output",
|
|
393
|
+
description: "Preview and publish a draft output. Requires confirm: true unless skipConfirmation is explicitly requested.",
|
|
394
|
+
inputSchema: {
|
|
395
|
+
workspaceId: POSITIVE_ID,
|
|
396
|
+
outputId: POSITIVE_ID,
|
|
397
|
+
description: z.string().min(1),
|
|
398
|
+
topics: z.array(z.string()).default([]),
|
|
399
|
+
confirm: z.boolean().optional(),
|
|
400
|
+
skipConfirmation: z.boolean().optional(),
|
|
401
|
+
},
|
|
402
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
403
|
+
}, async (client, { workspaceId, outputId, description, topics, confirm, skipConfirmation, }) => {
|
|
404
|
+
// 확인 전 호출은 output 조회만 수행하며 발행 endpoint는 호출하지 않는다.
|
|
405
|
+
if (confirm !== true && skipConfirmation !== true) {
|
|
406
|
+
const output = await client.get(`/workspaces/${workspaceId}/outputs/${outputId}`);
|
|
407
|
+
return {
|
|
408
|
+
requiresConfirmation: true,
|
|
409
|
+
preview: {
|
|
410
|
+
id: output.id,
|
|
411
|
+
status: output.status,
|
|
412
|
+
title: output.title,
|
|
413
|
+
contentPreview: createContentPreview(output.content),
|
|
414
|
+
contentLength: output.content.length,
|
|
415
|
+
taskIds: output.tasks.map((task) => task.id),
|
|
416
|
+
logIds: output.logs.map((log) => log.id),
|
|
417
|
+
description: description.trim(),
|
|
418
|
+
topics: normalizeTopics(topics),
|
|
419
|
+
},
|
|
420
|
+
nextStep: "Call publish_workspace_output again with confirm: true, or skipConfirmation: true if the user explicitly requested publishing without confirmation.",
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const published = await client.post(`/workspaces/${workspaceId}/outputs/${outputId}/publish`, {
|
|
424
|
+
description: description.trim(),
|
|
425
|
+
topics: normalizeTopics(topics),
|
|
426
|
+
});
|
|
427
|
+
const post = published.publishedPost;
|
|
428
|
+
if (!post) {
|
|
429
|
+
return published;
|
|
430
|
+
}
|
|
431
|
+
const path = buildPublicPostPath(post.authorUsername, post.slug);
|
|
432
|
+
return {
|
|
433
|
+
...published,
|
|
434
|
+
path,
|
|
435
|
+
url: new URL(path, `${webBaseUrl}/`).toString(),
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function registerLinkTools(registry) {
|
|
440
|
+
registry.registerAuthenticated("list_workspace_links", "read", {
|
|
441
|
+
title: "List OpenLog Workspace Links",
|
|
442
|
+
description: "Return task, log, and cross-type links for a workspace.",
|
|
443
|
+
inputSchema: { workspaceId: POSITIVE_ID },
|
|
444
|
+
annotations: READ_TOOL_ANNOTATIONS,
|
|
445
|
+
}, async (client, { workspaceId }) => {
|
|
446
|
+
// 서로 다른 세 링크 API를 병렬 조회해 하나의 workspace graph 응답으로 묶는다.
|
|
447
|
+
const [taskLinks, logLinks, crossLinks] = await Promise.all([
|
|
448
|
+
client.get(`/workspaces/${workspaceId}/task-links`),
|
|
449
|
+
client.get(`/workspaces/${workspaceId}/log-links`),
|
|
450
|
+
client.get(`/workspaces/${workspaceId}/cross-links`),
|
|
451
|
+
]);
|
|
452
|
+
return { taskLinks, logLinks, crossLinks };
|
|
453
|
+
});
|
|
454
|
+
registry.registerAuthenticated("create_workspace_task_link", "write", {
|
|
455
|
+
title: "Create OpenLog Workspace Task Link",
|
|
456
|
+
description: "Connect two different tasks in one workspace.",
|
|
457
|
+
inputSchema: {
|
|
458
|
+
workspaceId: POSITIVE_ID,
|
|
459
|
+
fromTaskId: POSITIVE_ID,
|
|
460
|
+
toTaskId: POSITIVE_ID,
|
|
461
|
+
relation: TASK_LINK_RELATION,
|
|
462
|
+
},
|
|
463
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
464
|
+
}, (client, { workspaceId, fromTaskId, toTaskId, relation }) => client.post(`/workspaces/${workspaceId}/task-links`, {
|
|
465
|
+
fromTaskId,
|
|
466
|
+
toTaskId,
|
|
467
|
+
relation,
|
|
468
|
+
}));
|
|
469
|
+
registry.registerAuthenticated("create_workspace_log_link", "write", {
|
|
470
|
+
title: "Create OpenLog Workspace Log Link",
|
|
471
|
+
description: "Connect two different logs in one workspace.",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
workspaceId: POSITIVE_ID,
|
|
474
|
+
fromLogId: POSITIVE_ID,
|
|
475
|
+
toLogId: POSITIVE_ID,
|
|
476
|
+
relation: LOG_LINK_RELATION,
|
|
477
|
+
},
|
|
478
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
479
|
+
}, async (client, { workspaceId, fromLogId, toLogId, relation }) => {
|
|
480
|
+
await client.postNoContent(`/workspaces/${workspaceId}/log-links`, {
|
|
481
|
+
fromLogId,
|
|
482
|
+
toLogId,
|
|
483
|
+
relation,
|
|
484
|
+
});
|
|
485
|
+
return { created: true, fromLogId, toLogId, relation };
|
|
486
|
+
});
|
|
487
|
+
registry.registerAuthenticated("create_workspace_cross_link", "write", {
|
|
488
|
+
title: "Create OpenLog Workspace Cross Link",
|
|
489
|
+
description: "Connect two workspace nodes of different document types.",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
workspaceId: POSITIVE_ID,
|
|
492
|
+
fromType: WORKSPACE_NODE_TYPE,
|
|
493
|
+
fromNodeId: POSITIVE_ID,
|
|
494
|
+
toType: WORKSPACE_NODE_TYPE,
|
|
495
|
+
toNodeId: POSITIVE_ID,
|
|
496
|
+
relation: CROSS_LINK_RELATION,
|
|
497
|
+
},
|
|
498
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
499
|
+
}, (client, { workspaceId, fromType, fromNodeId, toType, toNodeId, relation }) => client.post(`/workspaces/${workspaceId}/cross-links`, {
|
|
500
|
+
fromType,
|
|
501
|
+
fromNodeId,
|
|
502
|
+
toType,
|
|
503
|
+
toNodeId,
|
|
504
|
+
relation,
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
function registerWorkspaceDeleteTools(registry) {
|
|
508
|
+
// delete capability는 full 프로필에만 있으므로 아래 도구는 다른 프로필에서 등록되지 않는다.
|
|
509
|
+
registerDeleteTool(registry, "clear_working_brief", {
|
|
510
|
+
title: "Clear OpenLog Working Brief",
|
|
511
|
+
description: "Immediately clear the workspace Now Working brief.",
|
|
512
|
+
inputSchema: { workspaceId: POSITIVE_ID },
|
|
513
|
+
path: ({ workspaceId }) => `/workspaces/${workspaceId}/working-brief`,
|
|
514
|
+
result: ({ workspaceId }) => ({ cleared: true, workspaceId }),
|
|
515
|
+
});
|
|
516
|
+
registerDeleteTool(registry, "delete_workspace_task", {
|
|
517
|
+
title: "Delete OpenLog Workspace Task",
|
|
518
|
+
description: "Immediately delete one workspace task.",
|
|
519
|
+
inputSchema: { workspaceId: POSITIVE_ID, taskId: POSITIVE_ID },
|
|
520
|
+
path: ({ workspaceId, taskId }) => `/workspaces/${workspaceId}/tasks/${taskId}`,
|
|
521
|
+
result: ({ workspaceId, taskId }) => ({ deleted: true, workspaceId, taskId }),
|
|
522
|
+
});
|
|
523
|
+
registerDeleteTool(registry, "delete_workspace_log", {
|
|
524
|
+
title: "Delete OpenLog Workspace Log",
|
|
525
|
+
description: "Immediately delete one workspace log.",
|
|
526
|
+
inputSchema: { workspaceId: POSITIVE_ID, logId: POSITIVE_ID },
|
|
527
|
+
path: ({ workspaceId, logId }) => `/workspaces/${workspaceId}/logs/${logId}`,
|
|
528
|
+
result: ({ workspaceId, logId }) => ({ deleted: true, workspaceId, logId }),
|
|
529
|
+
});
|
|
530
|
+
registerDeleteTool(registry, "delete_workspace_todo", {
|
|
531
|
+
title: "Delete OpenLog Workspace Todo",
|
|
532
|
+
description: "Immediately delete one workspace todo.",
|
|
533
|
+
inputSchema: { workspaceId: POSITIVE_ID, todoId: POSITIVE_ID },
|
|
534
|
+
path: ({ workspaceId, todoId }) => `/workspaces/${workspaceId}/todos/${todoId}`,
|
|
535
|
+
result: ({ workspaceId, todoId }) => ({ deleted: true, workspaceId, todoId }),
|
|
536
|
+
});
|
|
537
|
+
registerDeleteTool(registry, "delete_workspace_memory", {
|
|
538
|
+
title: "Delete OpenLog Workspace Memory",
|
|
539
|
+
description: "Immediately delete one workspace memory.",
|
|
540
|
+
inputSchema: { workspaceId: POSITIVE_ID, memoryId: POSITIVE_ID },
|
|
541
|
+
path: ({ workspaceId, memoryId }) => `/workspaces/${workspaceId}/memories/${memoryId}`,
|
|
542
|
+
result: ({ workspaceId, memoryId }) => ({
|
|
543
|
+
deleted: true,
|
|
544
|
+
workspaceId,
|
|
545
|
+
memoryId,
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
548
|
+
registerDeleteTool(registry, "delete_workspace_output", {
|
|
549
|
+
title: "Delete OpenLog Workspace Output",
|
|
550
|
+
description: "Immediately delete one workspace output.",
|
|
551
|
+
inputSchema: { workspaceId: POSITIVE_ID, outputId: POSITIVE_ID },
|
|
552
|
+
path: ({ workspaceId, outputId }) => `/workspaces/${workspaceId}/outputs/${outputId}`,
|
|
553
|
+
result: ({ workspaceId, outputId }) => ({
|
|
554
|
+
deleted: true,
|
|
555
|
+
workspaceId,
|
|
556
|
+
outputId,
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
registerDeleteTool(registry, "delete_workspace_task_link", {
|
|
560
|
+
title: "Delete OpenLog Workspace Task Link",
|
|
561
|
+
description: "Immediately delete one workspace task link.",
|
|
562
|
+
inputSchema: { workspaceId: POSITIVE_ID, taskLinkId: POSITIVE_ID },
|
|
563
|
+
path: ({ workspaceId, taskLinkId }) => `/workspaces/${workspaceId}/task-links/${taskLinkId}`,
|
|
564
|
+
result: ({ workspaceId, taskLinkId }) => ({
|
|
565
|
+
deleted: true,
|
|
566
|
+
workspaceId,
|
|
567
|
+
taskLinkId,
|
|
568
|
+
}),
|
|
569
|
+
});
|
|
570
|
+
registerDeleteTool(registry, "delete_workspace_log_link", {
|
|
571
|
+
title: "Delete OpenLog Workspace Log Link",
|
|
572
|
+
description: "Immediately delete one workspace log link.",
|
|
573
|
+
inputSchema: { workspaceId: POSITIVE_ID, logLinkId: POSITIVE_ID },
|
|
574
|
+
path: ({ workspaceId, logLinkId }) => `/workspaces/${workspaceId}/log-links/${logLinkId}`,
|
|
575
|
+
result: ({ workspaceId, logLinkId }) => ({
|
|
576
|
+
deleted: true,
|
|
577
|
+
workspaceId,
|
|
578
|
+
logLinkId,
|
|
579
|
+
}),
|
|
580
|
+
});
|
|
581
|
+
registerDeleteTool(registry, "delete_workspace_cross_link", {
|
|
582
|
+
title: "Delete OpenLog Workspace Cross Link",
|
|
583
|
+
description: "Immediately delete one workspace cross-type link.",
|
|
584
|
+
inputSchema: { workspaceId: POSITIVE_ID, crossLinkId: POSITIVE_ID },
|
|
585
|
+
path: ({ workspaceId, crossLinkId }) => `/workspaces/${workspaceId}/cross-links/${crossLinkId}`,
|
|
586
|
+
result: ({ workspaceId, crossLinkId }) => ({
|
|
587
|
+
deleted: true,
|
|
588
|
+
workspaceId,
|
|
589
|
+
crossLinkId,
|
|
590
|
+
}),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
function registerDeleteTool(registry, name, config) {
|
|
594
|
+
// 삭제 도구의 권한·annotation·204 응답 처리를 한곳에서 동일하게 적용한다.
|
|
595
|
+
registry.registerAuthenticated(name, "delete", {
|
|
596
|
+
title: config.title,
|
|
597
|
+
description: config.description,
|
|
598
|
+
inputSchema: config.inputSchema,
|
|
599
|
+
annotations: DELETE_TOOL_ANNOTATIONS,
|
|
600
|
+
}, async (client, args) => {
|
|
601
|
+
const typedArgs = args;
|
|
602
|
+
await client.deleteNoContent(config.path(typedArgs));
|
|
603
|
+
return config.result(typedArgs);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
function withQuery(path, values) {
|
|
607
|
+
const params = new URLSearchParams();
|
|
608
|
+
for (const [name, value] of Object.entries(values)) {
|
|
609
|
+
// optional 값은 query에 문자열 "undefined"로 전달하지 않는다.
|
|
610
|
+
if (value !== undefined) {
|
|
611
|
+
params.set(name, String(value));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const query = params.toString();
|
|
615
|
+
return query ? `${path}?${query}` : path;
|
|
616
|
+
}
|
|
617
|
+
function normalizeTopics(topics) {
|
|
618
|
+
return [
|
|
619
|
+
...new Set(topics
|
|
620
|
+
.map((topic) => topic.trim().toLowerCase())
|
|
621
|
+
.filter((topic) => topic.length > 0)),
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
function buildPublicPostPath(username, slug) {
|
|
625
|
+
return `/@${encodeURIComponent(username)}/posts/${encodeURIComponent(slug)}`;
|
|
626
|
+
}
|
|
627
|
+
function isValidIsoDate(value) {
|
|
628
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
629
|
+
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
630
|
+
}
|
|
631
|
+
function validateDateRange(from, to, context) {
|
|
632
|
+
const fromTime = Date.parse(`${from}T00:00:00.000Z`);
|
|
633
|
+
const toTime = Date.parse(`${to}T00:00:00.000Z`);
|
|
634
|
+
if (toTime < fromTime) {
|
|
635
|
+
context.addIssue({ code: "custom", message: "to must not be before from." });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const inclusiveDays = (toTime - fromTime) / 86_400_000 + 1;
|
|
639
|
+
if (inclusiveDays > 366) {
|
|
640
|
+
context.addIssue({
|
|
641
|
+
code: "custom",
|
|
642
|
+
message: "The date range must not exceed 366 days.",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|