faceless-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/faceless.js +2 -0
- package/package.json +37 -0
- package/src/client.mjs +129 -0
- package/src/config.mjs +47 -0
- package/src/generated/.gitkeep +0 -0
- package/src/generated/operations.json +1759 -0
- package/src/index.mjs +767 -0
- package/src/mcp/stdio.mjs +113 -0
- package/src/output.mjs +109 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { resolveApiKey, resolveBaseUrl } from "../config.mjs";
|
|
6
|
+
import { CliError, request } from "../client.mjs";
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
9
|
+
let spec;
|
|
10
|
+
try {
|
|
11
|
+
spec = JSON.parse(
|
|
12
|
+
fs.readFileSync(new URL("../generated/operations.json", import.meta.url), "utf8")
|
|
13
|
+
);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'cli/src/generated/operations.json is missing. Run "npm run generate:agents" in the repo root to generate it.'
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const operations = spec.operations.filter((op) => op.mcp && op.mcp.enabled);
|
|
20
|
+
|
|
21
|
+
function toolInputSchema(op) {
|
|
22
|
+
const properties = {
|
|
23
|
+
...((op.querySchema && op.querySchema.properties) || {}),
|
|
24
|
+
...((op.requestSchema && op.requestSchema.properties) || {}),
|
|
25
|
+
};
|
|
26
|
+
const required = [
|
|
27
|
+
...new Set([
|
|
28
|
+
...((op.querySchema && op.querySchema.required) || []),
|
|
29
|
+
...((op.requestSchema && op.requestSchema.required) || []),
|
|
30
|
+
]),
|
|
31
|
+
];
|
|
32
|
+
const schema = { type: "object", properties, additionalProperties: false };
|
|
33
|
+
if (required.length) schema.required = required;
|
|
34
|
+
return schema;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function errorResult(type, message) {
|
|
38
|
+
return {
|
|
39
|
+
isError: true,
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: JSON.stringify({ success: false, error: { type, message } }),
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runMcpServer() {
|
|
50
|
+
const server = new Server(
|
|
51
|
+
{ name: "faceless", version: pkg.version },
|
|
52
|
+
{ capabilities: { tools: {} } }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const tools = operations.map((op) => ({
|
|
56
|
+
name: op.mcp.name,
|
|
57
|
+
description: `${op.summary}. ${op.description}`,
|
|
58
|
+
inputSchema: toolInputSchema(op),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
62
|
+
|
|
63
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
64
|
+
const name = req.params.name;
|
|
65
|
+
const args = { ...(req.params.arguments || {}) };
|
|
66
|
+
const op = operations.find((o) => o.mcp.name === name);
|
|
67
|
+
if (!op) return errorResult("invalid_input", `Unknown tool: ${name}`);
|
|
68
|
+
|
|
69
|
+
const apiKey = resolveApiKey({});
|
|
70
|
+
if (!apiKey) {
|
|
71
|
+
return errorResult(
|
|
72
|
+
"unauthorized",
|
|
73
|
+
'No API key configured. Set the FACELESS_API_KEY environment variable or run "faceless login". Create keys in your team settings at https://faceless.so/team.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let path = op.path;
|
|
78
|
+
for (const match of op.path.matchAll(/\{(\w+)\}/g)) {
|
|
79
|
+
const param = match[1];
|
|
80
|
+
if (args[param] === undefined || args[param] === null) {
|
|
81
|
+
return errorResult("invalid_input", `Missing required parameter: ${param}`);
|
|
82
|
+
}
|
|
83
|
+
path = path.replace(`{${param}}`, encodeURIComponent(String(args[param])));
|
|
84
|
+
delete args[param];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const method = op.method.toLowerCase();
|
|
89
|
+
const hasBody = method !== "get" && method !== "delete";
|
|
90
|
+
// Transport-level replay key (same semantics as the HTTP Idempotency-Key
|
|
91
|
+
// header and the remote MCP's idempotencyKey argument), not operation input.
|
|
92
|
+
const { idempotencyKey, ...opArgs } = args;
|
|
93
|
+
const result = await request({
|
|
94
|
+
method: op.method,
|
|
95
|
+
path,
|
|
96
|
+
query: hasBody ? undefined : opArgs,
|
|
97
|
+
body: hasBody ? opArgs : undefined,
|
|
98
|
+
apiKey,
|
|
99
|
+
baseUrl: resolveBaseUrl({}),
|
|
100
|
+
idempotencyKey,
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
104
|
+
};
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof CliError) return errorResult(err.type, err.message);
|
|
107
|
+
return errorResult("internal_error", err.message || "Unknown error");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const transport = new StdioServerTransport();
|
|
112
|
+
await server.connect(transport);
|
|
113
|
+
}
|
package/src/output.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const PREFERRED_COLUMNS = [
|
|
2
|
+
"id",
|
|
3
|
+
"name",
|
|
4
|
+
"platform",
|
|
5
|
+
"channelName",
|
|
6
|
+
"username",
|
|
7
|
+
"status",
|
|
8
|
+
"renderStatus",
|
|
9
|
+
"model",
|
|
10
|
+
"source",
|
|
11
|
+
"niche",
|
|
12
|
+
"type",
|
|
13
|
+
"credits",
|
|
14
|
+
"paused",
|
|
15
|
+
"scheduledTime",
|
|
16
|
+
"postedAt",
|
|
17
|
+
"createdAt",
|
|
18
|
+
"value",
|
|
19
|
+
"label",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const MAX_COLUMNS = 6;
|
|
23
|
+
const MAX_CELL = 40;
|
|
24
|
+
|
|
25
|
+
function cell(value) {
|
|
26
|
+
if (value === undefined || value === null) return "";
|
|
27
|
+
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
28
|
+
const flat = text.replace(/\s+/g, " ");
|
|
29
|
+
return flat.length > MAX_CELL ? flat.slice(0, MAX_CELL - 3) + "..." : flat;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pickColumns(rows) {
|
|
33
|
+
const keys = [];
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
for (const key of Object.keys(row)) {
|
|
36
|
+
if (!keys.includes(key)) keys.push(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const preferred = PREFERRED_COLUMNS.filter((key) => keys.includes(key));
|
|
40
|
+
const rest = keys.filter((key) => !preferred.includes(key));
|
|
41
|
+
return [...preferred, ...rest].slice(0, MAX_COLUMNS);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printTable(rows, indent = "") {
|
|
45
|
+
if (!rows.length) {
|
|
46
|
+
process.stdout.write(indent + "(no results)\n");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (rows.some((row) => !row || typeof row !== "object")) {
|
|
50
|
+
for (const row of rows) process.stdout.write(indent + cell(row) + "\n");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const columns = pickColumns(rows);
|
|
54
|
+
const widths = columns.map((col) =>
|
|
55
|
+
Math.max(col.length, ...rows.map((row) => cell(row[col]).length))
|
|
56
|
+
);
|
|
57
|
+
const line = (values) =>
|
|
58
|
+
indent + values.map((value, i) => String(value).padEnd(widths[i])).join(" ") + "\n";
|
|
59
|
+
process.stdout.write(line(columns));
|
|
60
|
+
process.stdout.write(line(widths.map((w) => "-".repeat(w))));
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
process.stdout.write(line(columns.map((col) => cell(row[col]))));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printObject(obj, indent = "") {
|
|
67
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
68
|
+
if (value === undefined) continue;
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
if (value.length && value.every((v) => v && typeof v === "object")) {
|
|
71
|
+
process.stdout.write(`${indent}${key}:\n`);
|
|
72
|
+
printTable(value, indent + " ");
|
|
73
|
+
} else {
|
|
74
|
+
process.stdout.write(`${indent}${key}: ${value.map(cell).join(", ")}\n`);
|
|
75
|
+
}
|
|
76
|
+
} else if (value && typeof value === "object") {
|
|
77
|
+
process.stdout.write(`${indent}${key}:\n`);
|
|
78
|
+
printObject(value, indent + " ");
|
|
79
|
+
} else {
|
|
80
|
+
process.stdout.write(`${indent}${key}: ${value}\n`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function print(result, { json = false } = {}) {
|
|
86
|
+
if (json || !process.stdout.isTTY) {
|
|
87
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const data = result && typeof result === "object" && "data" in result ? result.data : result;
|
|
91
|
+
if (Array.isArray(data)) {
|
|
92
|
+
printTable(data);
|
|
93
|
+
} else if (data && typeof data === "object") {
|
|
94
|
+
printObject(data);
|
|
95
|
+
} else {
|
|
96
|
+
process.stdout.write(String(data) + "\n");
|
|
97
|
+
}
|
|
98
|
+
if (result && typeof result === "object" && result.pagination) {
|
|
99
|
+
const p = result.pagination;
|
|
100
|
+
const pages = p.pages || (p.limit ? Math.ceil((p.total || 0) / p.limit) : undefined);
|
|
101
|
+
process.stdout.write(`page ${p.page}${pages ? ` of ${pages}` : ""} (${p.total} total)\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function printError(err) {
|
|
106
|
+
const type = (err && err.type) || "internal_error";
|
|
107
|
+
const message = (err && err.message) || "Unknown error";
|
|
108
|
+
process.stderr.write(`error (${type}): ${message}\n`);
|
|
109
|
+
}
|