@vivipilot/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/api.d.ts +86 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +77 -0
- package/dist/api.js.map +1 -0
- package/dist/args.d.ts +11 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +53 -0
- package/dist/args.js.map +1 -0
- package/dist/browser.d.ts +31 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +162 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +536 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +40 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +90 -0
- package/dist/manifest.js.map +1 -0
- package/dist/mcp.d.ts +13 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +392 -0
- package/dist/mcp.js.map +1 -0
- package/dist/render.d.ts +21 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +369 -0
- package/dist/render.js.map +1 -0
- package/package.json +42 -0
- package/src/api.ts +163 -0
- package/src/args.test.ts +21 -0
- package/src/args.ts +64 -0
- package/src/browser.test.ts +103 -0
- package/src/browser.ts +174 -0
- package/src/cli.ts +656 -0
- package/src/config.test.ts +30 -0
- package/src/config.ts +71 -0
- package/src/errors.ts +14 -0
- package/src/index.ts +25 -0
- package/src/manifest.test.ts +105 -0
- package/src/manifest.ts +126 -0
- package/src/mcp.test.ts +48 -0
- package/src/mcp.ts +438 -0
- package/src/render.ts +424 -0
- package/tsconfig.json +26 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_API_URL = "https://vivipilot.com";
|
|
6
|
+
|
|
7
|
+
export type CliConfig = {
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
publicKeys?: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Env = Record<string, string | undefined>;
|
|
14
|
+
|
|
15
|
+
function configRoot(env: Env): string {
|
|
16
|
+
if (env.XDG_CONFIG_HOME) return env.XDG_CONFIG_HOME;
|
|
17
|
+
if (env.APPDATA) return env.APPDATA;
|
|
18
|
+
return join(homedir(), ".config");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defaultConfigPath(env: Env = process.env): string {
|
|
22
|
+
return env.VIVIPILOT_CONFIG ?? join(configRoot(env), "vivipilot", "config.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isNotFound(error: unknown): boolean {
|
|
26
|
+
return typeof error === "object"
|
|
27
|
+
&& error !== null
|
|
28
|
+
&& "code" in error
|
|
29
|
+
&& (error as { code?: string }).code === "ENOENT";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readConfig(path = defaultConfigPath()): Promise<CliConfig> {
|
|
33
|
+
try {
|
|
34
|
+
const raw = await readFile(path, "utf8");
|
|
35
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
36
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as CliConfig : {};
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (isNotFound(error)) return {};
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function writeConfig(config: CliConfig, path = defaultConfigPath()): Promise<void> {
|
|
44
|
+
await mkdir(dirname(path), { recursive: true });
|
|
45
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function deleteConfig(path = defaultConfigPath()): Promise<void> {
|
|
49
|
+
await rm(path, { force: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveApiUrl(config: CliConfig, env: Env = process.env): string {
|
|
53
|
+
return (env.VIVIPILOT_API_URL ?? config.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveApiKey(config: CliConfig, env: Env = process.env): string | undefined {
|
|
57
|
+
const key = env.VIVIPILOT_API_KEY ?? config.apiKey;
|
|
58
|
+
return key && key.trim().length > 0 ? key.trim() : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolvePublicKeys(config: CliConfig, env: Env = process.env): Record<string, string> {
|
|
62
|
+
if (env.VIVIPILOT_MANIFEST_PUBLIC_KEYS) {
|
|
63
|
+
const parsed = JSON.parse(env.VIVIPILOT_MANIFEST_PUBLIC_KEYS) as unknown;
|
|
64
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
if (env.VIVIPILOT_MANIFEST_PUBLIC_KEY_ID && env.VIVIPILOT_MANIFEST_PUBLIC_KEY) {
|
|
67
|
+
return { [env.VIVIPILOT_MANIFEST_PUBLIC_KEY_ID]: env.VIVIPILOT_MANIFEST_PUBLIC_KEY };
|
|
68
|
+
}
|
|
69
|
+
return config.publicKeys ?? {};
|
|
70
|
+
}
|
|
71
|
+
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
readonly exitCode: number;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, exitCode = 1) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "CliError";
|
|
7
|
+
this.exitCode = exitCode;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isCliError(error: unknown): error is CliError {
|
|
12
|
+
return error instanceof CliError;
|
|
13
|
+
}
|
|
14
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { parseArgv, flagBoolean, flagNumber, flagString, type ParsedArgs } from "./args.js";
|
|
2
|
+
export {
|
|
3
|
+
DEFAULT_API_URL,
|
|
4
|
+
defaultConfigPath,
|
|
5
|
+
deleteConfig,
|
|
6
|
+
readConfig,
|
|
7
|
+
resolveApiKey,
|
|
8
|
+
resolveApiUrl,
|
|
9
|
+
resolvePublicKeys,
|
|
10
|
+
writeConfig,
|
|
11
|
+
type CliConfig,
|
|
12
|
+
type Env,
|
|
13
|
+
} from "./config.js";
|
|
14
|
+
export {
|
|
15
|
+
VivipilotApiClient,
|
|
16
|
+
type BalanceResponse,
|
|
17
|
+
type EstimateRequest,
|
|
18
|
+
type EstimateResponse,
|
|
19
|
+
type GenerateRequest,
|
|
20
|
+
type GenerateResponse,
|
|
21
|
+
type StatusResponse,
|
|
22
|
+
} from "./api.js";
|
|
23
|
+
export { readManifestFile, verifyManifestFile } from "./manifest.js";
|
|
24
|
+
export { startMcpServer, type McpServerOptions } from "./mcp.js";
|
|
25
|
+
export { CliError, isCliError } from "./errors.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { bytesToBase64Url, signRenderManifest, type UnsignedVivipilotRenderManifestV1 } from "@vivipilot/render-manifest";
|
|
3
|
+
import { checkRenderEntitlement, assertNotRevoked } from "./manifest.js";
|
|
4
|
+
|
|
5
|
+
async function createTestKeyPair() {
|
|
6
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
7
|
+
{ name: "Ed25519" },
|
|
8
|
+
true,
|
|
9
|
+
["sign", "verify"],
|
|
10
|
+
) as CryptoKeyPair;
|
|
11
|
+
return {
|
|
12
|
+
publicKey: bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("raw", keyPair.publicKey))),
|
|
13
|
+
privateKey: bytesToBase64Url(new Uint8Array(await crypto.subtle.exportKey("pkcs8", keyPair.privateKey))),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function baseManifest(entitlement: UnsignedVivipilotRenderManifestV1["entitlement"]): UnsignedVivipilotRenderManifestV1 {
|
|
18
|
+
return {
|
|
19
|
+
schema: "vivipilot.renderManifest.v1",
|
|
20
|
+
manifestId: "manifest_123",
|
|
21
|
+
generationId: "gen_123",
|
|
22
|
+
userId: "user_123",
|
|
23
|
+
createdAt: "2026-06-27T10:00:00.000Z",
|
|
24
|
+
engineVersion: "vivipilot-renderer@0.1.0",
|
|
25
|
+
entitlement,
|
|
26
|
+
billing: {
|
|
27
|
+
creditTransactionId: "txn_123",
|
|
28
|
+
creditsCharged: 40,
|
|
29
|
+
idempotencyKey: "idem_123",
|
|
30
|
+
creditSource: "paid_topup",
|
|
31
|
+
},
|
|
32
|
+
render: {
|
|
33
|
+
engine: "pixi-dw-scene-v1",
|
|
34
|
+
canvas: { width: 1280, height: 720, fps: 30 },
|
|
35
|
+
durationFrames: 120,
|
|
36
|
+
overlays: [],
|
|
37
|
+
},
|
|
38
|
+
integrity: {
|
|
39
|
+
canonicalPayloadHash: "",
|
|
40
|
+
sceneHash: "b".repeat(64),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("checkRenderEntitlement", () => {
|
|
46
|
+
it("allows render within entitlement", async () => {
|
|
47
|
+
const keys = await createTestKeyPair();
|
|
48
|
+
const signed = await signRenderManifest(baseManifest({
|
|
49
|
+
maxResolution: "1080p",
|
|
50
|
+
watermark: false,
|
|
51
|
+
commercialUse: true,
|
|
52
|
+
allowedFormats: ["mp4", "webm"],
|
|
53
|
+
}), { keyId: "test", privateKey: keys.privateKey });
|
|
54
|
+
|
|
55
|
+
const result = checkRenderEntitlement(signed, { format: "mp4", height: 1080 });
|
|
56
|
+
expect(result.ok).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects render exceeding max resolution", async () => {
|
|
60
|
+
const keys = await createTestKeyPair();
|
|
61
|
+
const signed = await signRenderManifest(baseManifest({
|
|
62
|
+
maxResolution: "720p",
|
|
63
|
+
watermark: false,
|
|
64
|
+
commercialUse: true,
|
|
65
|
+
allowedFormats: ["mp4", "webm"],
|
|
66
|
+
}), { keyId: "test", privateKey: keys.privateKey });
|
|
67
|
+
|
|
68
|
+
const result = checkRenderEntitlement(signed, { height: 1080 });
|
|
69
|
+
expect(result.ok).toBe(false);
|
|
70
|
+
if (!result.ok) expect(result.reason).toBe("resolution_exceeds_entitlement");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rejects render with disallowed format", async () => {
|
|
74
|
+
const keys = await createTestKeyPair();
|
|
75
|
+
const signed = await signRenderManifest(baseManifest({
|
|
76
|
+
maxResolution: "1080p",
|
|
77
|
+
watermark: false,
|
|
78
|
+
commercialUse: true,
|
|
79
|
+
allowedFormats: ["mp4", "webm"],
|
|
80
|
+
}), { keyId: "test", privateKey: keys.privateKey });
|
|
81
|
+
|
|
82
|
+
const result = checkRenderEntitlement(signed, { format: "gif" });
|
|
83
|
+
expect(result.ok).toBe(false);
|
|
84
|
+
if (!result.ok) expect(result.reason).toBe("format_not_allowed");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("assertNotRevoked", () => {
|
|
89
|
+
it("does not throw when not checked (offline)", () => {
|
|
90
|
+
expect(() => assertNotRevoked({ revoked: false, checked: false })).not.toThrow();
|
|
91
|
+
expect(() => assertNotRevoked({ revoked: false, checked: false, error: "http_500" })).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("does not throw when checked and not revoked", () => {
|
|
95
|
+
expect(() => assertNotRevoked({ revoked: false, checked: true, revokedAt: null })).not.toThrow();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("throws when checked and revoked", () => {
|
|
99
|
+
expect(() => assertNotRevoked({ revoked: true, checked: true, revokedAt: 1234 })).toThrow(/revoked/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not throw when revoked but not checked (network failure)", () => {
|
|
103
|
+
expect(() => assertNotRevoked({ revoked: true, checked: false, error: "timeout" })).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
verifyRenderManifest,
|
|
4
|
+
type RenderManifestResolution,
|
|
5
|
+
type RenderManifestVerificationResult,
|
|
6
|
+
type VivipilotRenderManifestV1,
|
|
7
|
+
} from "@vivipilot/render-manifest";
|
|
8
|
+
import { type CliConfig, type Env, resolveApiUrl, resolveApiKey, resolvePublicKeys } from "./config.js";
|
|
9
|
+
import { CliError } from "./errors.js";
|
|
10
|
+
|
|
11
|
+
export async function readManifestFile(path: string): Promise<unknown> {
|
|
12
|
+
return JSON.parse(await readFile(path, "utf8")) as unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function verifyManifestFile(path: string, config: CliConfig, env: Env = process.env): Promise<RenderManifestVerificationResult> {
|
|
16
|
+
const manifest = await readManifestFile(path);
|
|
17
|
+
return verifyRenderManifest(manifest, {
|
|
18
|
+
publicKeys: resolvePublicKeys(config, env),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort online revocation check. Calls
|
|
24
|
+
* `GET /api/v1/manifests/{manifestId}/revocation`. Returns `revoked: false`
|
|
25
|
+
* when the network is unavailable or the endpoint errors — the offline
|
|
26
|
+
* cryptographic signature verification remains authoritative.
|
|
27
|
+
*
|
|
28
|
+
* Pass `apiKey` to authenticate the request. When no key is configured the
|
|
29
|
+
* check is skipped (offline mode).
|
|
30
|
+
*/
|
|
31
|
+
export async function checkManifestRevocationOnline(
|
|
32
|
+
manifest: VivipilotRenderManifestV1,
|
|
33
|
+
config: CliConfig,
|
|
34
|
+
env: Env = process.env,
|
|
35
|
+
fetchImpl: typeof fetch = fetch,
|
|
36
|
+
): Promise<{ revoked: boolean; revokedAt: number | null; checked: boolean; error?: string }> {
|
|
37
|
+
const apiKey = resolveApiKey(config, env);
|
|
38
|
+
if (!apiKey) return { revoked: false, revokedAt: null, checked: false, error: "no_api_key" };
|
|
39
|
+
|
|
40
|
+
const apiUrl = resolveApiUrl(config, env);
|
|
41
|
+
const url = `${apiUrl}/api/v1/manifests/${encodeURIComponent(manifest.manifestId)}/revocation`;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetchImpl(url, {
|
|
46
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
return { revoked: false, revokedAt: null, checked: false, error: `http_${response.status}` };
|
|
51
|
+
}
|
|
52
|
+
const payload = await response.json() as { revoked?: boolean; revokedAt?: number | null };
|
|
53
|
+
return {
|
|
54
|
+
revoked: payload.revoked === true,
|
|
55
|
+
revokedAt: payload.revokedAt ?? null,
|
|
56
|
+
checked: true,
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
revoked: false,
|
|
61
|
+
revokedAt: null,
|
|
62
|
+
checked: false,
|
|
63
|
+
error: error instanceof Error ? error.message : String(error),
|
|
64
|
+
};
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timeout);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function assertNotRevoked(result: { revoked: boolean; checked: boolean; revokedAt?: number | null; error?: string }): void {
|
|
71
|
+
if (result.checked && result.revoked) {
|
|
72
|
+
throw new CliError(`Manifest has been revoked server-side${result.revokedAt ? ` at ${new Date(result.revokedAt).toISOString()}` : ""}. Generate a new manifest with \`vivipilot generate\`.`, 1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const RESOLUTION_HEIGHT_CEILINGS: Record<RenderManifestResolution, number> = {
|
|
77
|
+
"720p": 720,
|
|
78
|
+
"1080p": 1080,
|
|
79
|
+
"2k": 1440,
|
|
80
|
+
"4k": 2160,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type RenderEntitlementRequest = {
|
|
84
|
+
format?: "mp4" | "webm" | "gif" | "mov";
|
|
85
|
+
width?: number;
|
|
86
|
+
height?: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type RenderEntitlementViolation = {
|
|
90
|
+
ok: false;
|
|
91
|
+
reason: "resolution_exceeds_entitlement" | "format_not_allowed";
|
|
92
|
+
message: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type RenderEntitlementResult =
|
|
96
|
+
| { ok: true }
|
|
97
|
+
| RenderEntitlementViolation;
|
|
98
|
+
|
|
99
|
+
export function checkRenderEntitlement(
|
|
100
|
+
manifest: VivipilotRenderManifestV1,
|
|
101
|
+
request: RenderEntitlementRequest,
|
|
102
|
+
): RenderEntitlementResult {
|
|
103
|
+
const maxCeiling = RESOLUTION_HEIGHT_CEILINGS[manifest.entitlement.maxResolution];
|
|
104
|
+
const requestedHeight = request.height ?? manifest.render.canvas.height;
|
|
105
|
+
if (requestedHeight > maxCeiling) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: "resolution_exceeds_entitlement",
|
|
109
|
+
message: `Requested height ${requestedHeight}px exceeds the manifest entitlement (${manifest.entitlement.maxResolution}, max ${maxCeiling}px). Top up or generate a higher-entitlement manifest.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (request.format && Array.isArray(manifest.entitlement.allowedFormats) && manifest.entitlement.allowedFormats.length > 0) {
|
|
114
|
+
if (!manifest.entitlement.allowedFormats.includes(request.format)) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
reason: "format_not_allowed",
|
|
118
|
+
message: `Format ${request.format} is not allowed by this manifest entitlement. Allowed: ${manifest.entitlement.allowedFormats.join(", ")}.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { ok: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
package/src/mcp.test.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { startMcpServer } from "./mcp.js";
|
|
4
|
+
|
|
5
|
+
function jsonRpc(id: number, method: string, params?: unknown): string {
|
|
6
|
+
return `${JSON.stringify({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) })}\n`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("mcp stdio server", () => {
|
|
10
|
+
it("responds to initialize and lists Vivipilot tools", async () => {
|
|
11
|
+
const stdin = new PassThrough();
|
|
12
|
+
const stdout = new PassThrough();
|
|
13
|
+
const stderr = new PassThrough();
|
|
14
|
+
const chunks: Buffer[] = [];
|
|
15
|
+
stdout.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
16
|
+
|
|
17
|
+
const server = startMcpServer({
|
|
18
|
+
config: {},
|
|
19
|
+
env: {},
|
|
20
|
+
stdin,
|
|
21
|
+
stdout,
|
|
22
|
+
stderr,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
stdin.write(jsonRpc(1, "initialize", { protocolVersion: "2025-06-18" }));
|
|
26
|
+
stdin.write(jsonRpc(2, "tools/list"));
|
|
27
|
+
stdin.end();
|
|
28
|
+
await server;
|
|
29
|
+
|
|
30
|
+
const responses = Buffer.concat(chunks)
|
|
31
|
+
.toString("utf8")
|
|
32
|
+
.trim()
|
|
33
|
+
.split("\n")
|
|
34
|
+
.map((line) => JSON.parse(line) as any);
|
|
35
|
+
|
|
36
|
+
expect(responses[0].id).toBe(1);
|
|
37
|
+
expect(responses[0].result.serverInfo.name).toBe("vivipilot");
|
|
38
|
+
expect(responses[1].id).toBe(2);
|
|
39
|
+
expect(responses[1].result.tools.map((tool: any) => tool.name)).toEqual([
|
|
40
|
+
"vivipilot_get_balance",
|
|
41
|
+
"vivipilot_estimate_motion_layout",
|
|
42
|
+
"vivipilot_generate_motion_layout",
|
|
43
|
+
"vivipilot_verify_manifest",
|
|
44
|
+
"vivipilot_render_video",
|
|
45
|
+
"vivipilot_get_generation_status",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
});
|