@okostream/mcp 0.12.1 → 0.12.3

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.
@@ -1,324 +0,0 @@
1
- // Рабочие пространства: состав, права участников и приглашения.
2
- //
3
- // Девять способностей выдаются точечно; их разносит по событиям CF18, а не
4
- // клиент. Личное пространство имеет id, равный uid владельца, — искать его
5
- // отдельным запросом не нужно.
6
- //
7
- // oko_create_workspace_invite до 01.09 лежал в конце файла, оторванным от
8
- // своей группы; при разборе возвращён к остальным инструментам приглашений.
9
- import { z } from "zod";
10
- import { requireConfirm, safeString, serialize, textResult } from "../utils.js";
11
- import { authed, inviteUrl } from "./context.js";
12
- import { destructiveTool, idempotentWriteTool, MAX_WORKSPACE_INVITE_CODE_LENGTH, MAX_WORKSPACE_NAME_LENGTH, readOnlyTool, toolDescription, unknownRecordSchema, updateOutputSchema, workspaceAccessOutputSchema, workspacePermissionsSchema, workspacesOutputSchema, writeTool } from "./schemas.js";
13
- import { addDoc, collection, deleteDoc, doc, getDoc, getDocs, query, serverTimestamp, setDoc, updateDoc, where } from "firebase/firestore";
14
- import { firestore, functions } from "../firebase.js";
15
- import { httpsCallable } from "firebase/functions";
16
- import { listVisibleWorkspaces, normalizeWorkspacePermissions, randomInviteCode, readWorkspaceForOwner } from "./workspaceAccess.js";
17
- export function registerWorkspaceTools(server) {
18
- server.registerTool("oko_list_workspaces", {
19
- title: "List workspaces",
20
- description: toolDescription({
21
- summary: "List OKO workspaces visible to the authenticated organizer.",
22
- args: ["includeShared: include workspaces shared with this organizer, default true."],
23
- returns: "workspaces array with id, ownership flag, and permissions for shared workspaces, plus count.",
24
- examples: ["Use before oko_create_webinar when the user wants to create a webinar in a shared workspace."],
25
- }),
26
- inputSchema: { includeShared: z.boolean().default(true).describe("Include workspaces shared with this organizer.") },
27
- outputSchema: workspacesOutputSchema,
28
- annotations: readOnlyTool,
29
- }, async ({ includeShared }) => {
30
- const { uid } = await authed(["workspace:read"]);
31
- const out = await listVisibleWorkspaces(uid, includeShared);
32
- return textResult({ workspaces: out, count: out.length });
33
- });
34
- server.registerTool("oko_create_workspace", {
35
- title: "Create workspace",
36
- description: toolDescription({
37
- summary: "Create a shared OKO workspace owned by the authenticated organizer.",
38
- args: ["name: workspace display name."],
39
- returns: "workspaceId, name, and created=true.",
40
- examples: ["Use when the organizer wants to create a new team/workspace for events."],
41
- }),
42
- inputSchema: { name: z.string().min(1).max(MAX_WORKSPACE_NAME_LENGTH).describe("Workspace name.") },
43
- outputSchema: z.object({ workspaceId: z.string(), created: z.boolean() }).passthrough(),
44
- annotations: writeTool,
45
- }, async ({ name }) => {
46
- const { uid } = await authed(["workspace:write"]);
47
- const workspaceRef = await addDoc(collection(firestore, "workspaces"), {
48
- name: name.trim(),
49
- ownerId: uid,
50
- personal: false,
51
- createdAt: serverTimestamp(),
52
- });
53
- return textResult({ workspaceId: workspaceRef.id, name: name.trim(), created: true });
54
- });
55
- server.registerTool("oko_update_workspace", {
56
- title: "Update workspace",
57
- description: toolDescription({
58
- summary: "Rename a workspace owned by the authenticated organizer.",
59
- args: ["workspaceId: workspace to update.", "name: new workspace display name."],
60
- returns: "workspaceId and updated fields.",
61
- examples: ["Use for workspace rename requests."],
62
- }),
63
- inputSchema: { workspaceId: z.string().min(1), name: z.string().min(1).max(MAX_WORKSPACE_NAME_LENGTH).describe("New workspace name.") },
64
- outputSchema: updateOutputSchema,
65
- annotations: idempotentWriteTool,
66
- }, async ({ workspaceId, name }) => {
67
- const { uid } = await authed(["workspace:write"]);
68
- await readWorkspaceForOwner(workspaceId, uid);
69
- await updateDoc(doc(firestore, "workspaces", workspaceId), { name: name.trim() });
70
- return textResult({ workspaceId, updated: ["name"] });
71
- });
72
- server.registerTool("oko_delete_workspace", {
73
- title: "Delete workspace",
74
- description: toolDescription({
75
- summary: "Delete an empty non-personal workspace owned by the authenticated organizer.",
76
- args: ["workspaceId: workspace to delete.", "confirm: must be true."],
77
- returns: "workspaceId and deleted=true.",
78
- examples: ["Use after listing workspace access and confirming the workspace is empty."],
79
- notes: ["Personal workspaces cannot be deleted. This tool refuses to delete workspaces that still contain events; move/delete events first."],
80
- }),
81
- inputSchema: { workspaceId: z.string().min(1), confirm: z.boolean().optional().describe("Required true for workspace deletion.") },
82
- outputSchema: z.object({ workspaceId: z.string(), deleted: z.boolean() }).passthrough(),
83
- annotations: destructiveTool,
84
- }, async ({ workspaceId, confirm }) => {
85
- const { uid } = await authed(["workspace:write"]);
86
- const workspace = await readWorkspaceForOwner(workspaceId, uid);
87
- requireConfirm(confirm, "oko_delete_workspace", { workspaceId, name: workspace.name, personal: workspace.personal });
88
- if (workspace.personal === true)
89
- throw new Error("Personal workspace cannot be deleted.");
90
- // 🔴 Спрашиваем индекс доступа, а не коллекцию событий. `allow list` по
91
- // `events` требует `hasPlatformOwnerRole()`, который у MCP-сессии ложен по
92
- // построению (`mcp_<keyId>` выпускается с `owner: false`) — то есть
93
- // прежний запрос падал `permission-denied` ВСЕГДА, и инструмент не мог
94
- // удалить ни одного пространства. Индекс `eventAccess/{uid}/grants` — тот
95
- // же источник, которым пользуется `oko_list_my_events`.
96
- //
97
- // Ограничение названо честно: grants перечисляют события, к которым
98
- // доступ есть у ЭТОГО организатора. Событие в пространстве, к которому он
99
- // доступа не имеет, проверка не увидит — но владелец пространства
100
- // получает grant на все его события через CF18, так что для владельца,
101
- // а только он сюда и доходит (`readWorkspaceForOwner` выше), совпадает.
102
- // 🔴 Удаление идёт через CF57, а не `deleteDoc`: голое удаление корневого
103
- // документа оставляло живыми подколлекцию shares, зеркала
104
- // workspaceMembers и grants — участники сохраняли доступ, а убрать это
105
- // было уже нельзя, потому что правило shares проверяет владельца через
106
- // get() исчезнувшего родителя. Проверку «в пространстве нет событий»
107
- // функция делает сама, полным запросом, а не по индексу этого ключа.
108
- const removeWorkspace = httpsCallable(functions, "deleteWorkspaceCompletely");
109
- const removed = await removeWorkspace({ workspaceId });
110
- return textResult({ ...removed.data, workspaceId, deleted: true });
111
- });
112
- server.registerTool("oko_list_workspace_access", {
113
- title: "List workspace access",
114
- description: toolDescription({
115
- summary: "List workspace members/shares and pending invites.",
116
- args: ["workspaceId: workspace owned by the authenticated organizer."],
117
- returns: "workspace document, members array, invites array.",
118
- examples: ["Use before changing permissions, removing a member, or revoking an invite."],
119
- }),
120
- inputSchema: { workspaceId: z.string().min(1) },
121
- outputSchema: workspaceAccessOutputSchema,
122
- annotations: readOnlyTool,
123
- }, async ({ workspaceId }) => {
124
- const { uid } = await authed(["workspace:read"]);
125
- const workspace = await readWorkspaceForOwner(workspaceId, uid);
126
- const [sharesSnap, invitesSnap] = await Promise.all([
127
- getDocs(collection(firestore, "workspaces", workspaceId, "shares")),
128
- getDocs(query(collection(firestore, "workspaceInvites"), where("workspaceId", "==", workspaceId))),
129
- ]);
130
- const members = [];
131
- for (const share of sharesSnap.docs) {
132
- // Правила разрешают читать чужой профиль только владельцу платформы,
133
- // поэтому на любом рабочем пространстве с участниками это чтение
134
- // заканчивается отказом. Без обработки инструмент падал целиком вместо
135
- // того, чтобы вернуть состав команды без необязательных профилей.
136
- const profile = await getDoc(doc(firestore, "userProfiles", share.id))
137
- .then((snap) => (snap.exists() ? serialize(snap.data()) : null))
138
- .catch(() => null);
139
- members.push({
140
- uid: share.id,
141
- ...serialize(share.data()),
142
- profile,
143
- });
144
- }
145
- const invites = invitesSnap.docs.map((invite) => ({ code: invite.id, ...serialize(invite.data()) }));
146
- return textResult({ workspaceId, workspace: { id: workspaceId, ...serialize(workspace) }, members, invites });
147
- });
148
- server.registerTool("oko_update_workspace_member", {
149
- title: "Update workspace member",
150
- description: toolDescription({
151
- summary: "Create or update a workspace share/member permissions document.",
152
- args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "permissions: 9 workspace permission booleans.", "confirm: must be true."],
153
- returns: "workspaceId, uid, and updated=true.",
154
- examples: ["Use to change an existing member's permissions, or direct-add a known uid."],
155
- notes: ["Changing a share materializes lightweight event grants and updates RTDB workspace/chat mirrors."],
156
- }),
157
- inputSchema: {
158
- workspaceId: z.string().min(1),
159
- uid: z.string().min(1).describe("Member Firebase uid."),
160
- email: z.string().email().optional().describe("Optional member email cache."),
161
- comment: z.string().max(500).default(""),
162
- permissions: workspacePermissionsSchema,
163
- confirm: z.boolean().optional().describe("Required true for share updates."),
164
- },
165
- outputSchema: updateOutputSchema,
166
- annotations: writeTool,
167
- }, async (args) => {
168
- const { uid } = await authed(["workspace:write"]);
169
- const workspace = await readWorkspaceForOwner(args.workspaceId, uid);
170
- requireConfirm(args.confirm, "oko_update_workspace_member", { workspaceId: args.workspaceId, workspaceName: workspace.name, targetUid: args.uid, permissions: args.permissions });
171
- await setDoc(doc(firestore, "workspaces", args.workspaceId, "shares", args.uid), {
172
- email: args.email ?? "",
173
- comment: args.comment.trim(),
174
- permissions: normalizeWorkspacePermissions(args.permissions),
175
- sharedAt: serverTimestamp(),
176
- sharedBy: uid,
177
- }, { merge: true });
178
- // 🔴 `updateOutputSchema` объявляет `updated` МАССИВОМ имён полей, а не
179
- // флагом. Пока возвращался `true`, SDK отвергал ответ уже после записи
180
- // прав: клиент видел ошибку и считал, что доступ не выдан, — а он выдан.
181
- // Из всех мест Pattern 18 это самое опасное: расходятся представления о
182
- // том, кто что может.
183
- return textResult({
184
- workspaceId: args.workspaceId,
185
- uid: args.uid,
186
- updated: ["email", "comment", "permissions"],
187
- });
188
- });
189
- server.registerTool("oko_remove_workspace_member", {
190
- title: "Remove workspace member",
191
- description: toolDescription({
192
- summary: "Remove a member/share from a workspace.",
193
- args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "confirm: must be true."],
194
- returns: "workspaceId, uid, removed=true.",
195
- examples: ["Use after listing workspace access and confirming the exact uid."],
196
- notes: ["Removing a share revokes its event grants and RTDB workspace/chat mirrors."],
197
- }),
198
- inputSchema: { workspaceId: z.string().min(1), uid: z.string().min(1), confirm: z.boolean().optional().describe("Required true for member removal.") },
199
- outputSchema: z.object({ workspaceId: z.string(), uid: z.string(), removed: z.boolean() }).passthrough(),
200
- annotations: destructiveTool,
201
- }, async ({ workspaceId, uid: targetUid, confirm }) => {
202
- const { uid } = await authed(["workspace:write"]);
203
- const workspace = await readWorkspaceForOwner(workspaceId, uid);
204
- requireConfirm(confirm, "oko_remove_workspace_member", { workspaceId, workspaceName: workspace.name, targetUid });
205
- await deleteDoc(doc(firestore, "workspaces", workspaceId, "shares", targetUid));
206
- return textResult({ workspaceId, uid: targetUid, removed: true });
207
- });
208
- server.registerTool("oko_list_workspace_invites", {
209
- title: "List workspace invites",
210
- description: toolDescription({
211
- summary: "List workspace invite links for a workspace owned by the organizer.",
212
- args: ["workspaceId: workspace owned by the organizer."],
213
- returns: "invites array with code, claim status, permissions, and inviteUrl.",
214
- examples: ["Use before revoking or sharing an invite code."],
215
- }),
216
- inputSchema: { workspaceId: z.string().min(1) },
217
- outputSchema: z.object({ workspaceId: z.string(), invites: z.array(unknownRecordSchema), count: z.number().int().nonnegative() }).passthrough(),
218
- annotations: readOnlyTool,
219
- }, async ({ workspaceId }) => {
220
- const { uid } = await authed(["workspace:read"]);
221
- await readWorkspaceForOwner(workspaceId, uid);
222
- const snap = await getDocs(query(collection(firestore, "workspaceInvites"), where("workspaceId", "==", workspaceId)));
223
- const invites = snap.docs.map((invite) => ({ code: invite.id, ...serialize(invite.data()), inviteUrl: inviteUrl(invite.id) }));
224
- return textResult({ workspaceId, invites, count: invites.length });
225
- });
226
- server.registerTool("oko_revoke_workspace_invite", {
227
- title: "Revoke workspace invite",
228
- description: toolDescription({
229
- summary: "Delete a pending workspace invite code.",
230
- args: ["code: invite code.", "confirm: must be true."],
231
- returns: "code and revoked=true.",
232
- examples: ["Use after confirming the exact invite code."],
233
- }),
234
- inputSchema: { code: z.string().min(4).max(MAX_WORKSPACE_INVITE_CODE_LENGTH), confirm: z.boolean().optional().describe("Required true for invite revocation.") },
235
- outputSchema: z.object({ code: z.string(), revoked: z.boolean() }).passthrough(),
236
- annotations: destructiveTool,
237
- }, async ({ code, confirm }) => {
238
- const { uid } = await authed(["workspace:write"]);
239
- const normalizedCode = code.trim().toUpperCase();
240
- const inviteSnap = await getDoc(doc(firestore, "workspaceInvites", normalizedCode));
241
- if (!inviteSnap.exists())
242
- throw new Error("Workspace invite not found.");
243
- const invite = inviteSnap.data();
244
- if (invite.createdBy !== uid)
245
- throw new Error("Only the invite creator/workspace owner can revoke this invite.");
246
- requireConfirm(confirm, "oko_revoke_workspace_invite", { code: normalizedCode, workspaceId: invite.workspaceId, claimed: invite.claimed });
247
- await deleteDoc(doc(firestore, "workspaceInvites", normalizedCode));
248
- return textResult({ code: normalizedCode, revoked: true });
249
- });
250
- server.registerTool("oko_claim_workspace_invite", {
251
- title: "Claim workspace invite",
252
- description: toolDescription({
253
- summary: "Accept a workspace invite code for the authenticated organizer.",
254
- args: ["code: invite code from /invite/{code}.", "confirm: must be true."],
255
- returns: "success plus workspace id/name returned by OKO CF19.",
256
- examples: ["Use when the organizer wants to join a workspace from an invite code."],
257
- }),
258
- inputSchema: {
259
- code: z.string().min(4).max(MAX_WORKSPACE_INVITE_CODE_LENGTH),
260
- confirm: z.boolean().optional().describe("Required true to join the workspace."),
261
- },
262
- outputSchema: z.object({
263
- success: z.boolean(),
264
- workspace: z.object({ id: z.string(), name: z.string() }).passthrough(),
265
- }).passthrough(),
266
- annotations: writeTool,
267
- }, async ({ code, confirm }) => {
268
- await authed(["workspace:write"]);
269
- requireConfirm(confirm, "oko_claim_workspace_invite", { code: code.trim().toUpperCase() });
270
- const claimInvite = httpsCallable(functions, "claimWorkspaceInvite");
271
- const result = await claimInvite({ code: code.trim().toUpperCase() });
272
- return textResult({ ...serialize(result.data) });
273
- });
274
- server.registerTool("oko_create_workspace_invite", {
275
- title: "Create workspace invite",
276
- description: toolDescription({
277
- summary: "Create a workspace invite for a known OKO workspace.",
278
- args: [
279
- "workspaceId: workspace owned by the authenticated organizer.",
280
- "code: optional invite code, 16-64 chars; omitted means generated (recommended).",
281
- "email: optional intended recipient email.",
282
- "permissions: optional whitelist of workspace permission booleans.",
283
- "confirm: must be true.",
284
- ],
285
- returns: "workspaceId, invite code, inviteUrl, and created=true.",
286
- examples: ["Use only after confirming the recipient and permission set with the workspace owner."],
287
- }),
288
- inputSchema: {
289
- workspaceId: z.string().min(1),
290
- // 🔴 Минимум 16, а не 4: код приглашения — предъявительский секрет.
291
- // Правило Firestore допускает `get` любого приглашения по коду, а CF19
292
- // выдаёт по нему права, то есть короткий код перебирается. Генератор
293
- // платформы даёт 16 шестнадцатеричных знаков — заданный вручную не
294
- // должен быть слабее. У соседних инструментов (`claim`, `revoke`)
295
- // минимум остаётся 4 намеренно: они принимают УЖЕ существующие коды,
296
- // включая исторические шестисимвольные.
297
- code: z.string().min(16).max(MAX_WORKSPACE_INVITE_CODE_LENGTH).optional(),
298
- email: z.string().email().optional(),
299
- comment: z.string().max(500).default(""),
300
- permissions: workspacePermissionsSchema,
301
- confirm: z.boolean().optional(),
302
- },
303
- outputSchema: z.object({ workspaceId: z.string(), code: z.string(), inviteUrl: z.string().url(), created: z.boolean() }).passthrough(),
304
- annotations: writeTool,
305
- }, async (args) => {
306
- const { uid } = await authed(["workspace:write"]);
307
- const workspace = await readWorkspaceForOwner(args.workspaceId, uid);
308
- requireConfirm(args.confirm, "oko_create_workspace_invite", { workspaceId: args.workspaceId, code: args.code, email: args.email });
309
- const code = (args.code?.trim() || randomInviteCode()).toUpperCase();
310
- const url = inviteUrl(code);
311
- await setDoc(doc(firestore, "workspaceInvites", code), {
312
- workspaceId: args.workspaceId,
313
- workspaceName: safeString(workspace.name, "Workspace"),
314
- email: args.email ?? "",
315
- comment: args.comment,
316
- permissions: normalizeWorkspacePermissions(args.permissions),
317
- createdBy: uid,
318
- createdAt: serverTimestamp(),
319
- claimed: false,
320
- claimedBy: null,
321
- });
322
- return textResult({ workspaceId: args.workspaceId, code, inviteUrl: url, created: true });
323
- });
324
- }
package/dist/tools.js DELETED
@@ -1,38 +0,0 @@
1
- // Точка входа инструментов OKO MCP.
2
- //
3
- // Здесь нет ни одного обработчика — только порядок сборки: обёртка аудита,
4
- // затем одиннадцать доменов. Порядок вызовов задаёт порядок в tools/list,
5
- // поэтому он сохранён с дораборного файла.
6
- //
7
- // Инвентарь (63 инструмента) и области доступа описаны в public-contract.json;
8
- // его сверяет protocol.test, поднимая настоящий сервер.
9
- import { registerAccountTools } from "./tools/account.js";
10
- import { registerChatTools } from "./tools/chat.js";
11
- import { registerEventTools } from "./tools/events.js";
12
- import { registerImageTools } from "./tools/images.js";
13
- import { installToolInstrumentation } from "./tools/instrumentation.js";
14
- import { registerLeadTools } from "./tools/leads.js";
15
- import { registerLifecycleTools } from "./tools/lifecycle.js";
16
- import { registerLiveTools } from "./tools/live.js";
17
- import { registerNotificationTools } from "./tools/notifications.js";
18
- import { registerSessionTools } from "./tools/sessions.js";
19
- import { registerTelegramTools } from "./tools/telegram.js";
20
- import { registerWorkspaceTools } from "./tools/workspaces.js";
21
- export function registerTools(server) {
22
- // Обёртка ставится первой: она подменяет server.registerTool, и всё
23
- // зарегистрированное ПОСЛЕ получает аудит записей и человеческий ответ
24
- // на отказ авторизации.
25
- installToolInstrumentation(server);
26
- // Порядок сохранён с дораборного файла — он задаёт порядок в tools/list.
27
- registerAccountTools(server);
28
- registerWorkspaceTools(server);
29
- registerEventTools(server);
30
- registerImageTools(server);
31
- registerLifecycleTools(server);
32
- registerLiveTools(server);
33
- registerSessionTools(server);
34
- registerChatTools(server);
35
- registerLeadTools(server);
36
- registerNotificationTools(server);
37
- registerTelegramTools(server);
38
- }
package/dist/utils.js DELETED
@@ -1,76 +0,0 @@
1
- const DEFAULT_CHARACTER_LIMIT = 30_000;
2
- const MAX_COMPACT_ARRAY_ITEMS = 25;
3
- const MAX_COMPACT_OBJECT_KEYS = 60;
4
- const MAX_COMPACT_STRING_LENGTH = 2_000;
5
- export function serialize(value) {
6
- if (value == null)
7
- return value;
8
- if (typeof value !== "object")
9
- return value;
10
- if (typeof value.toDate === "function") {
11
- return value.toDate().toISOString();
12
- }
13
- if (Array.isArray(value))
14
- return value.map(serialize);
15
- const out = {};
16
- for (const [key, item] of Object.entries(value)) {
17
- if (item !== undefined)
18
- out[key] = serialize(item);
19
- }
20
- return out;
21
- }
22
- function compactForText(value, depth = 0) {
23
- if (typeof value === "string" && value.length > MAX_COMPACT_STRING_LENGTH) {
24
- return `${value.slice(0, MAX_COMPACT_STRING_LENGTH)}... [truncated ${value.length - MAX_COMPACT_STRING_LENGTH} chars]`;
25
- }
26
- if (value == null || typeof value !== "object")
27
- return value;
28
- if (Array.isArray(value)) {
29
- const items = value.slice(0, MAX_COMPACT_ARRAY_ITEMS).map((item) => compactForText(item, depth + 1));
30
- if (value.length > MAX_COMPACT_ARRAY_ITEMS) {
31
- items.push({ omittedItems: value.length - MAX_COMPACT_ARRAY_ITEMS });
32
- }
33
- return items;
34
- }
35
- if (depth >= 4)
36
- return "[nested object omitted]";
37
- const entries = Object.entries(value);
38
- const out = {};
39
- for (const [key, item] of entries.slice(0, MAX_COMPACT_OBJECT_KEYS)) {
40
- out[key] = compactForText(item, depth + 1);
41
- }
42
- if (entries.length > MAX_COMPACT_OBJECT_KEYS) {
43
- out.omittedKeys = entries.length - MAX_COMPACT_OBJECT_KEYS;
44
- }
45
- return out;
46
- }
47
- export function textResult(data, options = {}) {
48
- const maxChars = options.maxChars ?? DEFAULT_CHARACTER_LIMIT;
49
- let structuredContent = serialize(data);
50
- let text = JSON.stringify(structuredContent, null, 2);
51
- if (text.length > maxChars) {
52
- structuredContent = {
53
- ...compactForText(structuredContent),
54
- truncated: true,
55
- truncationMessage: `Response JSON exceeded ${maxChars} characters. ${options.truncationHint ?? "Use a smaller limit or cursor pagination."}`,
56
- };
57
- }
58
- return {
59
- content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }],
60
- structuredContent,
61
- };
62
- }
63
- export function requireConfirm(confirm, action, preview) {
64
- if (confirm === true)
65
- return;
66
- throw new Error(`${action} requires confirm:true. Preview: ${JSON.stringify(serialize(preview))}`);
67
- }
68
- export function csvCell(value) {
69
- let text = value == null ? "" : String(value);
70
- if (/^[=+\-@]/.test(text))
71
- text = `'${text}`;
72
- return `"${text.replace(/"/g, '""')}"`;
73
- }
74
- export function safeString(value, fallback = "") {
75
- return typeof value === "string" ? value : fallback;
76
- }
package/dist/version.js DELETED
@@ -1,2 +0,0 @@
1
- // Generated by scripts/generateMcpVersion.mjs from mcp/package.json. Do not edit.
2
- export const OKO_MCP_VERSION = "0.12.1";
package/dist/videoUrl.js DELETED
@@ -1,166 +0,0 @@
1
- // Домен, которому Twitch разрешает встраивать плеер. Это НЕ ссылка на нашу
2
- // страницу — параметр встраивания, поэтому он отдельная константа, а не
3
- // производная от APP_ORIGIN: сменится одно, второе останется прежним.
4
- const TWITCH_PARENT = "okostream.app";
5
- export function extractIframeSrc(input) {
6
- const trimmed = input.trim();
7
- if (!trimmed.startsWith("<iframe"))
8
- return null;
9
- const match = trimmed.match(/\bsrc=["']([^"']+)["']/i);
10
- return match?.[1] ?? null;
11
- }
12
- const YT_ID_RE = /^[\w-]{11}$/;
13
- function extractYouTubeId(url) {
14
- let parsed;
15
- try {
16
- parsed = new URL(url);
17
- }
18
- catch {
19
- return null;
20
- }
21
- const host = parsed.hostname.replace(/^www\./, "");
22
- if (host === "youtu.be") {
23
- const id = parsed.pathname.slice(1).split("/")[0] ?? "";
24
- return YT_ID_RE.test(id) ? id : null;
25
- }
26
- if (host !== "youtube.com" && host !== "m.youtube.com")
27
- return null;
28
- const path = parsed.pathname;
29
- if (path.startsWith("/embed/")) {
30
- const id = path.slice(7).split("/")[0] ?? "";
31
- return YT_ID_RE.test(id) ? id : null;
32
- }
33
- if (path.startsWith("/shorts/")) {
34
- const id = path.slice(8).split("/")[0] ?? "";
35
- return YT_ID_RE.test(id) ? id : null;
36
- }
37
- if (path.startsWith("/live/")) {
38
- const id = path.slice(6).split("/")[0] ?? "";
39
- return YT_ID_RE.test(id) ? id : null;
40
- }
41
- if (path === "/watch") {
42
- const id = parsed.searchParams.get("v") ?? "";
43
- return YT_ID_RE.test(id) ? id : null;
44
- }
45
- return null;
46
- }
47
- function hostOf(url) {
48
- try {
49
- return new URL(url).hostname.replace(/^www\./, "").replace(/^m\./, "");
50
- }
51
- catch {
52
- return null;
53
- }
54
- }
55
- function extractVkVideoIds(url) {
56
- let parsed;
57
- try {
58
- parsed = new URL(url);
59
- }
60
- catch {
61
- return null;
62
- }
63
- if (parsed.pathname.includes("video_ext.php")) {
64
- const oid = parsed.searchParams.get("oid");
65
- const id = parsed.searchParams.get("id");
66
- return oid && id ? { oid, id } : null;
67
- }
68
- const match = parsed.pathname.match(/\/video(-?\d+)_(\d+)/);
69
- return match ? { oid: match[1] ?? "", id: match[2] ?? "" } : null;
70
- }
71
- function extractPathId(url, host, patterns) {
72
- let parsed;
73
- try {
74
- parsed = new URL(url);
75
- }
76
- catch {
77
- return null;
78
- }
79
- if (parsed.hostname.replace(/^www\./, "") !== host && !parsed.hostname.endsWith(`.${host}`))
80
- return null;
81
- for (const pattern of patterns) {
82
- const match = parsed.pathname.match(pattern);
83
- if (match?.[1])
84
- return match[1];
85
- }
86
- return null;
87
- }
88
- function extractTwitchEmbed(url) {
89
- let parsed;
90
- try {
91
- parsed = new URL(url);
92
- }
93
- catch {
94
- return null;
95
- }
96
- const host = parsed.hostname.replace(/^www\./, "");
97
- if (host === "player.twitch.tv") {
98
- const channel = parsed.searchParams.get("channel");
99
- const video = parsed.searchParams.get("video");
100
- if (channel)
101
- return `https://player.twitch.tv/?channel=${channel}&parent=${TWITCH_PARENT}`;
102
- if (video)
103
- return `https://player.twitch.tv/?video=${video}&parent=${TWITCH_PARENT}`;
104
- }
105
- if (host === "twitch.tv") {
106
- const vodMatch = parsed.pathname.match(/\/videos\/(\d+)/);
107
- if (vodMatch?.[1])
108
- return `https://player.twitch.tv/?video=v${vodMatch[1]}&parent=${TWITCH_PARENT}`;
109
- const channelMatch = parsed.pathname.match(/^\/([a-zA-Z0-9_]+)$/);
110
- if (channelMatch?.[1])
111
- return `https://player.twitch.tv/?channel=${channelMatch[1]}&parent=${TWITCH_PARENT}`;
112
- }
113
- return null;
114
- }
115
- export function detectAndNormalize(url) {
116
- const trimmed = url.trim();
117
- if (!trimmed)
118
- return { provider: "unknown", normalizedUrl: null };
119
- const host = hostOf(trimmed);
120
- if (host === "youtube.com" || host === "youtu.be") {
121
- const id = extractYouTubeId(trimmed);
122
- return { provider: "youtube", normalizedUrl: id ? `https://www.youtube.com/embed/${id}` : null };
123
- }
124
- if (host === "vk.com" || host === "vkvideo.ru") {
125
- const ids = extractVkVideoIds(trimmed);
126
- return { provider: "vk", normalizedUrl: ids ? `https://vk.com/video_ext.php?oid=${ids.oid}&id=${ids.id}&hd=2` : null };
127
- }
128
- const rutubeId = extractPathId(trimmed, "rutube.ru", [/\/play\/embed\/([a-f0-9]+)/i, /\/video\/([a-f0-9]+)/i]);
129
- if (rutubeId)
130
- return { provider: "rutube", normalizedUrl: `https://rutube.ru/play/embed/${rutubeId}` };
131
- if (trimmed.includes("rutube.ru"))
132
- return { provider: "rutube", normalizedUrl: null };
133
- const vimeoId = extractPathId(trimmed, "vimeo.com", [/\/video\/(\d+)/, /^\/(\d+)/]);
134
- if (vimeoId)
135
- return { provider: "vimeo", normalizedUrl: `https://player.vimeo.com/video/${vimeoId}` };
136
- if (trimmed.includes("vimeo.com"))
137
- return { provider: "vimeo", normalizedUrl: null };
138
- const kinescopeId = extractPathId(trimmed, "kinescope.io", [/\/embed\/([a-zA-Z0-9]+)/, /^\/([a-zA-Z0-9]+)$/]);
139
- if (kinescopeId)
140
- return { provider: "kinescope", normalizedUrl: `https://kinescope.io/embed/${kinescopeId}` };
141
- if (trimmed.includes("kinescope.io"))
142
- return { provider: "kinescope", normalizedUrl: null };
143
- const dzenId = extractPathId(trimmed, "dzen.ru", [/\/embed\/([a-zA-Z0-9_-]+)/, /\/video\/watch\/([a-zA-Z0-9_-]+)/]);
144
- if (dzenId)
145
- return { provider: "dzen", normalizedUrl: `https://dzen.ru/embed/${dzenId}` };
146
- if (trimmed.includes("dzen.ru"))
147
- return { provider: "dzen", normalizedUrl: null };
148
- const twitchEmbed = extractTwitchEmbed(trimmed);
149
- if (twitchEmbed)
150
- return { provider: "twitch", normalizedUrl: twitchEmbed };
151
- if (trimmed.includes("twitch.tv"))
152
- return { provider: "twitch", normalizedUrl: null };
153
- const dailymotionId = extractPathId(trimmed, "dailymotion.com", [/\/embed\/video\/([a-zA-Z0-9]+)/, /\/video\/([a-zA-Z0-9]+)/])
154
- ?? extractPathId(trimmed, "dai.ly", [/^\/([a-zA-Z0-9]+)/]);
155
- if (dailymotionId)
156
- return { provider: "dailymotion", normalizedUrl: `https://www.dailymotion.com/embed/video/${dailymotionId}` };
157
- if (trimmed.includes("dailymotion.com") || trimmed.includes("dai.ly"))
158
- return { provider: "dailymotion", normalizedUrl: null };
159
- const wistiaId = extractPathId(trimmed, "wistia.com", [/\/medias\/([a-zA-Z0-9]+)/])
160
- ?? extractPathId(trimmed, "fast.wistia.net", [/\/embed\/iframe\/([a-zA-Z0-9]+)/]);
161
- if (wistiaId)
162
- return { provider: "wistia", normalizedUrl: `https://fast.wistia.net/embed/iframe/${wistiaId}` };
163
- if (trimmed.includes("wistia.com") || trimmed.includes("wistia.net"))
164
- return { provider: "wistia", normalizedUrl: null };
165
- return { provider: "unknown", normalizedUrl: null };
166
- }