@kynver-app/mcp-agent-os 0.3.63 → 0.3.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/context-budget-receipt.d.ts +1 -1
- package/dist/context/context-budget-receipt.js +1 -1
- package/dist/lib/kynver-client.js +6 -1
- package/dist/lib/mcp-sse-server.d.ts +2 -2
- package/dist/lib/mcp-sse-server.js +169 -7
- package/dist/tools/context-preview.js +1 -1
- package/dist/tools/graph.js +10 -2
- package/dist/tools/workbench.js +34 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { estimateTokensFromChars, RUNTIME_CONTEXT_BUDGET_RECEIPT_V2_SCHEMA_VERSION, type ContextBudgetReceiptSurface, type RuntimeContextBudgetReceiptV2 } from "@kynver-app/runtime";
|
|
1
|
+
import { estimateTokensFromChars, RUNTIME_CONTEXT_BUDGET_RECEIPT_V2_SCHEMA_VERSION, type ContextBudgetReceiptSurface, type RuntimeContextBudgetReceiptV2 } from "@kynver-app/runtime/server/context-budget-receipt";
|
|
2
2
|
/** MCP tool-context measurement adapter (PR2) — metadata only; does not mutate payloads. */
|
|
3
3
|
export declare function measureMcpToolContextReceipt(payload: unknown, input?: {
|
|
4
4
|
agentOsId?: string | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildContextBudgetReceiptV2, estimateTokensFromChars, sectionReceiptFromMarkdown, RUNTIME_CONTEXT_BUDGET_RECEIPT_V2_SCHEMA_VERSION, } from "@kynver-app/runtime";
|
|
1
|
+
import { buildContextBudgetReceiptV2, estimateTokensFromChars, sectionReceiptFromMarkdown, RUNTIME_CONTEXT_BUDGET_RECEIPT_V2_SCHEMA_VERSION, } from "@kynver-app/runtime/server/context-budget-receipt";
|
|
2
2
|
const MCP_TOOL_RESULT_BUDGET_BYTES = 12_288;
|
|
3
3
|
function utf8ByteLength(value) {
|
|
4
4
|
return new TextEncoder().encode(JSON.stringify(value)).length;
|
|
@@ -49,8 +49,13 @@ function getHeaders() {
|
|
|
49
49
|
const userId = ctx?.userId;
|
|
50
50
|
const serviceSecret = process.env.KYNVER_SERVICE_SECRET;
|
|
51
51
|
if (userId) {
|
|
52
|
+
const runnerToken = process.env.KYNVER_RUNNER_TOKEN?.trim();
|
|
53
|
+
if (runnerToken) {
|
|
54
|
+
headers["X-Kynver-Runner-Token"] = runnerToken;
|
|
55
|
+
return headers;
|
|
56
|
+
}
|
|
52
57
|
if (!serviceSecret) {
|
|
53
|
-
throw new Error("
|
|
58
|
+
throw new Error("User context requires authentication: set KYNVER_RUNNER_TOKEN (preferred) or KYNVER_SERVICE_SECRET in the MCP server environment.");
|
|
54
59
|
}
|
|
55
60
|
headers["X-Kynver-User-Id"] = userId;
|
|
56
61
|
headers["X-Kynver-Service-Secret"] = serviceSecret;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type IncomingMessage } from "node:http";
|
|
1
|
+
import { createServer, type IncomingMessage } from "node:http";
|
|
2
2
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
export declare const MAX_SSE_BODY_BYTES: number;
|
|
4
4
|
export type SessionAuth = {
|
|
@@ -40,5 +40,5 @@ type RateLimiter = {
|
|
|
40
40
|
export declare function createFixedWindowRateLimiter(limit: number, windowMs: number): RateLimiter;
|
|
41
41
|
export declare function parseJsonBody(req: IncomingMessage, maxBytes?: number): Promise<unknown>;
|
|
42
42
|
export declare function validateMcporterServerName(name: string): string;
|
|
43
|
-
export declare function startKynverMcpSseServer(options: StartKynverMcpSseServerOptions):
|
|
43
|
+
export declare function startKynverMcpSseServer(options: StartKynverMcpSseServerOptions): ReturnType<typeof createServer>;
|
|
44
44
|
export {};
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
// @generated — synced from packages/kynver-mcp-sse-lib/src/index.ts via scripts/sync-mcp-sse-server.mjs
|
|
2
2
|
// Do not edit by hand; change the canonical file and re-run the sync script.
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
-
import { timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
7
|
export const MAX_SSE_BODY_BYTES = 1 * 1024 * 1024;
|
|
7
8
|
const FORBIDDEN_QUERY_KEYS = ["apiKey", "secret", "userId"];
|
|
9
|
+
const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
|
|
10
|
+
function isServiceSecretImpersonationRejectedInProduction() {
|
|
11
|
+
const override = process.env.KYNVER_ALLOW_SERVICE_SECRET_IMPERSONATION?.trim().toLowerCase();
|
|
12
|
+
if (override !== undefined && TRUTHY_ENV.has(override))
|
|
13
|
+
return false;
|
|
14
|
+
return process.env.NODE_ENV === "production" || process.env.VERCEL_ENV === "production";
|
|
15
|
+
}
|
|
8
16
|
export function headerValue(req, name) {
|
|
9
17
|
const v = req.headers[name];
|
|
10
18
|
return typeof v === "string" ? v : Array.isArray(v) ? (v[0] ?? null) : null;
|
|
@@ -121,6 +129,8 @@ function resolveInternalAuth(req, serviceSecret) {
|
|
|
121
129
|
const internalSecret = headerValue(req, "x-kynver-service-secret");
|
|
122
130
|
if (!internalUserId || !serviceSecret || !internalSecret)
|
|
123
131
|
return null;
|
|
132
|
+
if (isServiceSecretImpersonationRejectedInProduction())
|
|
133
|
+
return null;
|
|
124
134
|
if (!timingSafeEqualString(internalSecret, serviceSecret))
|
|
125
135
|
return null;
|
|
126
136
|
return { userId: internalUserId, auth: { kind: "internal", userId: internalUserId } };
|
|
@@ -162,13 +172,15 @@ export function startKynverMcpSseServer(options) {
|
|
|
162
172
|
const serviceSecret = process.env.KYNVER_SERVICE_SECRET ?? "";
|
|
163
173
|
const host = resolveListenHost();
|
|
164
174
|
const sessions = {};
|
|
175
|
+
const streamableSessions = {};
|
|
165
176
|
// Resource / abuse limits (env-overridable; in-process per server).
|
|
166
177
|
const maxSessions = intFromEnv("MCP_SSE_MAX_SESSIONS", 200);
|
|
167
178
|
const maxSessionsPerUser = intFromEnv("MCP_SSE_MAX_SESSIONS_PER_USER", 20);
|
|
168
179
|
const idleTimeoutMs = intFromEnv("MCP_SSE_IDLE_TIMEOUT_MS", 5 * 60 * 1000);
|
|
169
180
|
const rateLimitPerMin = intFromEnv("MCP_SSE_RATE_LIMIT_PER_MIN", 120);
|
|
170
181
|
const rateLimiter = createFixedWindowRateLimiter(rateLimitPerMin, 60 * 1000);
|
|
171
|
-
// Close idle
|
|
182
|
+
// Close idle sessions (both transports) so a flood of half-open connections
|
|
183
|
+
// can't pin memory.
|
|
172
184
|
const idleSweep = setInterval(() => {
|
|
173
185
|
const now = Date.now();
|
|
174
186
|
for (const [sid, entry] of Object.entries(sessions)) {
|
|
@@ -177,8 +189,17 @@ export function startKynverMcpSseServer(options) {
|
|
|
177
189
|
void entry.transport.close().catch(() => { });
|
|
178
190
|
}
|
|
179
191
|
}
|
|
192
|
+
for (const [sid, entry] of Object.entries(streamableSessions)) {
|
|
193
|
+
if (now - entry.lastActivity > idleTimeoutMs) {
|
|
194
|
+
delete streamableSessions[sid];
|
|
195
|
+
void entry.transport.close().catch(() => { });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
180
198
|
}, 60 * 1000);
|
|
181
199
|
idleSweep.unref?.();
|
|
200
|
+
const activeSessionCount = () => Object.keys(sessions).length + Object.keys(streamableSessions).length;
|
|
201
|
+
const activeSessionCountForUser = (userId) => Object.values(sessions).filter((s) => s.userId === userId).length +
|
|
202
|
+
Object.values(streamableSessions).filter((s) => s.userId === userId).length;
|
|
182
203
|
const httpServer = createServer(async (req, res) => {
|
|
183
204
|
const url = parseUrl(req, port);
|
|
184
205
|
const path = url.pathname;
|
|
@@ -215,14 +236,14 @@ export function startKynverMcpSseServer(options) {
|
|
|
215
236
|
});
|
|
216
237
|
return;
|
|
217
238
|
}
|
|
218
|
-
// Connection caps — bound total + per-user concurrent
|
|
219
|
-
// authenticated caller can't exhaust
|
|
220
|
-
|
|
221
|
-
if (
|
|
239
|
+
// Connection caps — bound total + per-user concurrent sessions (across
|
|
240
|
+
// both transports) so an authenticated caller can't exhaust
|
|
241
|
+
// memory/sockets by opening many.
|
|
242
|
+
if (activeSessionCount() >= maxSessions) {
|
|
222
243
|
json(res, 503, { error: "Server at capacity. Try again shortly." });
|
|
223
244
|
return;
|
|
224
245
|
}
|
|
225
|
-
if (
|
|
246
|
+
if (activeSessionCountForUser(userId) >= maxSessionsPerUser) {
|
|
226
247
|
json(res, 429, { error: "Too many concurrent connections for this account." });
|
|
227
248
|
return;
|
|
228
249
|
}
|
|
@@ -266,6 +287,146 @@ export function startKynverMcpSseServer(options) {
|
|
|
266
287
|
}
|
|
267
288
|
return;
|
|
268
289
|
}
|
|
290
|
+
// Streamable HTTP transport (MCP spec 2025-03-26+) — the transport modern
|
|
291
|
+
// connector clients (Grok, ChatGPT, Gemini, Claude) expect for remote
|
|
292
|
+
// servers. One path, three verbs: POST carries JSON-RPC (initialize opens a
|
|
293
|
+
// session; the response's mcp-session-id header identifies it), GET opens
|
|
294
|
+
// the standalone server→client SSE stream, DELETE terminates the session.
|
|
295
|
+
// Auth model mirrors the legacy /sse pair: credentials are verified on the
|
|
296
|
+
// initialize POST, bound to the session, and every subsequent request must
|
|
297
|
+
// re-present the same credentials.
|
|
298
|
+
if (path === "/mcp") {
|
|
299
|
+
if (req.method !== "POST" && req.method !== "GET" && req.method !== "DELETE") {
|
|
300
|
+
res.writeHead(405, { "Content-Type": "application/json", Allow: "GET, POST, DELETE" });
|
|
301
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (hasForbiddenQueryAuth(url)) {
|
|
305
|
+
json(res, 400, {
|
|
306
|
+
error: "Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers.",
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const sessionId = headerValue(req, "mcp-session-id");
|
|
311
|
+
if (sessionId) {
|
|
312
|
+
const entry = streamableSessions[sessionId];
|
|
313
|
+
if (!entry) {
|
|
314
|
+
// 404 (not 400) per spec so a well-behaved client re-initializes.
|
|
315
|
+
json(res, 404, { error: "Unknown or expired MCP session. Re-initialize." });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!verifyPostAuth(req, url, entry.auth, serviceSecret)) {
|
|
319
|
+
json(res, 401, {
|
|
320
|
+
error: "Unauthorized. Re-send the same credentials used to initialize the MCP session.",
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!rateLimiter.check(`msg:${entry.userId}`)) {
|
|
325
|
+
json(res, 429, { error: "Too many requests. Slow down and retry." });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
entry.lastActivity = Date.now();
|
|
329
|
+
try {
|
|
330
|
+
const body = req.method === "POST" ? await parseJsonBody(req) : undefined;
|
|
331
|
+
await options.requestContext.run({ userId: entry.userId }, async () => {
|
|
332
|
+
await entry.transport.handleRequest(req, res, body);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (!res.headersSent) {
|
|
337
|
+
json(res, 400, {
|
|
338
|
+
error: error instanceof Error ? error.message : "Invalid request body",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// No session header — only an initialize POST may open a session. (The
|
|
345
|
+
// transport itself rejects a sessionless non-initialize POST with 400.)
|
|
346
|
+
if (req.method !== "POST") {
|
|
347
|
+
json(res, 400, {
|
|
348
|
+
error: "Missing mcp-session-id header. Open a session with an initialize POST first.",
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Pre-auth rate limit by client IP (we don't know the user yet).
|
|
353
|
+
if (!rateLimiter.check(`mcp:${clientIp(req)}`)) {
|
|
354
|
+
json(res, 429, { error: "Too many requests. Slow down and retry." });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
let userId = null;
|
|
358
|
+
let auth = null;
|
|
359
|
+
const internal = serviceSecret ? resolveInternalAuth(req, serviceSecret) : null;
|
|
360
|
+
if (internal) {
|
|
361
|
+
userId = internal.userId;
|
|
362
|
+
auth = internal.auth;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
const apiKey = bearerToken(req);
|
|
366
|
+
if (apiKey) {
|
|
367
|
+
userId = await resolveUserFromApiKey(baseUrl, apiKey);
|
|
368
|
+
if (userId)
|
|
369
|
+
auth = { kind: "apiKey", apiKey };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!userId || !auth) {
|
|
373
|
+
json(res, 401, {
|
|
374
|
+
error: "Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers.",
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (activeSessionCount() >= maxSessions) {
|
|
379
|
+
json(res, 503, { error: "Server at capacity. Try again shortly." });
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (activeSessionCountForUser(userId) >= maxSessionsPerUser) {
|
|
383
|
+
json(res, 429, { error: "Too many concurrent connections for this account." });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
let body;
|
|
387
|
+
try {
|
|
388
|
+
body = await parseJsonBody(req);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
json(res, 400, {
|
|
392
|
+
error: error instanceof Error ? error.message : "Invalid request body",
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const boundUserId = userId;
|
|
397
|
+
const boundAuth = auth;
|
|
398
|
+
const transport = new StreamableHTTPServerTransport({
|
|
399
|
+
sessionIdGenerator: () => randomUUID(),
|
|
400
|
+
onsessioninitialized: (sid) => {
|
|
401
|
+
streamableSessions[sid] = {
|
|
402
|
+
transport,
|
|
403
|
+
userId: boundUserId,
|
|
404
|
+
auth: boundAuth,
|
|
405
|
+
lastActivity: Date.now(),
|
|
406
|
+
};
|
|
407
|
+
console.log(`[MCP HTTP] New session (${options.label}, user: ${boundUserId.slice(0, 8)}...)`);
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
transport.onclose = () => {
|
|
411
|
+
if (transport.sessionId)
|
|
412
|
+
delete streamableSessions[transport.sessionId];
|
|
413
|
+
};
|
|
414
|
+
const server = await options.createMcpServer();
|
|
415
|
+
await server.connect(transport);
|
|
416
|
+
try {
|
|
417
|
+
await options.requestContext.run({ userId: boundUserId }, async () => {
|
|
418
|
+
await transport.handleRequest(req, res, body);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (!res.headersSent) {
|
|
423
|
+
json(res, 400, {
|
|
424
|
+
error: error instanceof Error ? error.message : "Invalid request",
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
269
430
|
if (req.method === "GET" && path === "/health") {
|
|
270
431
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
271
432
|
res.end("ok");
|
|
@@ -276,4 +437,5 @@ export function startKynverMcpSseServer(options) {
|
|
|
276
437
|
httpServer.listen(port, host, () => {
|
|
277
438
|
console.log(`[MCP SSE] ${options.label} listening on ${host}:${port}`);
|
|
278
439
|
});
|
|
440
|
+
return httpServer;
|
|
279
441
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { buildChatMemoryShadowPreview, buildLegacyChatMemorySection, } from "@kynver-app/runtime";
|
|
2
|
+
import { buildChatMemoryShadowPreview, buildLegacyChatMemorySection, } from "@kynver-app/runtime/server/context-budget-receipt";
|
|
3
3
|
function envTruthy(value) {
|
|
4
4
|
if (!value)
|
|
5
5
|
return false;
|
package/dist/tools/graph.js
CHANGED
|
@@ -125,10 +125,18 @@ export function registerGraphTools({ register, resolveToolSlug, jsonResult }) {
|
|
|
125
125
|
grantToken: z
|
|
126
126
|
.string()
|
|
127
127
|
.describe("Signed expansion grant (kve:g:…) bound to this graph handle and envelope."),
|
|
128
|
-
purpose: z
|
|
128
|
+
purpose: z
|
|
129
|
+
.string()
|
|
130
|
+
.describe("Informational only — the server enforces the graph-expansion purpose bound into the grant and ignores this value."),
|
|
129
131
|
envelopeId: z.string().describe("Active context envelope id binding this expansion."),
|
|
130
132
|
asOf: z.string().optional().describe("Optional UTC ISO as-of instant for temporal filtering."),
|
|
131
|
-
maxDepth: z
|
|
133
|
+
maxDepth: z
|
|
134
|
+
.number()
|
|
135
|
+
.int()
|
|
136
|
+
.min(1)
|
|
137
|
+
.max(1)
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("Traversal depth; the server caps expansion depth at 1."),
|
|
132
140
|
maxNodes: z.number().int().min(1).max(36).optional(),
|
|
133
141
|
}, async (args) => {
|
|
134
142
|
const slug = await resolveToolSlug(args.slug);
|
package/dist/tools/workbench.js
CHANGED
|
@@ -19,6 +19,20 @@ const commandKind = z.enum([
|
|
|
19
19
|
"yield",
|
|
20
20
|
"take",
|
|
21
21
|
]);
|
|
22
|
+
/** Keep in sync with COMMAND_CENTER_VIEW_MODES in command-center-url-state.ts */
|
|
23
|
+
const workbenchView = z.enum([
|
|
24
|
+
"overview",
|
|
25
|
+
"flow",
|
|
26
|
+
"marketing",
|
|
27
|
+
"projects",
|
|
28
|
+
"tasks",
|
|
29
|
+
"goals",
|
|
30
|
+
"dq",
|
|
31
|
+
"web",
|
|
32
|
+
"review",
|
|
33
|
+
"system",
|
|
34
|
+
"health",
|
|
35
|
+
]);
|
|
22
36
|
export function registerWorkbenchTools({ register, resolveToolSlug, jsonResult, }) {
|
|
23
37
|
register("agent_os_workbench_query", [
|
|
24
38
|
"Read the operator's current workbench focus (view mode, drilldown stack, focused entity)",
|
|
@@ -37,14 +51,26 @@ export function registerWorkbenchTools({ register, resolveToolSlug, jsonResult,
|
|
|
37
51
|
].join(" "), {
|
|
38
52
|
slug: z.string().optional(),
|
|
39
53
|
kind: commandKind,
|
|
40
|
-
view:
|
|
54
|
+
view: workbenchView.optional(),
|
|
41
55
|
target: z.string().optional().describe("EntityRef e.g. task:<id>, plan:<id>"),
|
|
42
56
|
push: z.boolean().optional().describe("For drill: push onto stack (default true)"),
|
|
43
|
-
panel: z
|
|
57
|
+
panel: z
|
|
58
|
+
.string()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("For open_panel: plan-detail | task-detail | goal-detail | project-detail | dq | lane-experts | graph-rail | system-console | system-group:<groupId> | system:<panelId>"),
|
|
44
61
|
tab: z.string().optional(),
|
|
45
62
|
personaSlug: z.string().optional(),
|
|
46
63
|
planId: z.string().optional(),
|
|
47
64
|
prUrl: z.string().optional(),
|
|
65
|
+
refs: z
|
|
66
|
+
.array(z.string())
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("For highlight: EntityRefs to point at (max 20) — renders a visible callout"),
|
|
69
|
+
ttlMs: z
|
|
70
|
+
.number()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("For highlight: callout duration ms (2000–120000, default 12000)"),
|
|
73
|
+
note: z.string().optional().describe("For annotate: note text (≤500 chars) shown with target"),
|
|
48
74
|
chip: z.string().optional(),
|
|
49
75
|
review: z.string().optional(),
|
|
50
76
|
q: z.string().optional(),
|
|
@@ -81,6 +107,12 @@ export function registerWorkbenchTools({ register, resolveToolSlug, jsonResult,
|
|
|
81
107
|
case "open_pr_history":
|
|
82
108
|
body.open_pr_history = { planId: args.planId, prUrl: args.prUrl };
|
|
83
109
|
break;
|
|
110
|
+
case "highlight":
|
|
111
|
+
body.highlight = { refs: args.refs, ttlMs: args.ttlMs };
|
|
112
|
+
break;
|
|
113
|
+
case "annotate":
|
|
114
|
+
body.annotate = { target: args.target, note: args.note };
|
|
115
|
+
break;
|
|
84
116
|
case "filter":
|
|
85
117
|
body.filter = { chip: args.chip, review: args.review, q: args.q };
|
|
86
118
|
break;
|
package/package.json
CHANGED