@standardagents/builder 0.10.1-next.82e11d5 → 0.11.0-next.41deba4
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/built-in-routes.js +230 -66
- package/dist/built-in-routes.js.map +1 -1
- package/dist/client/assets/index.css +1 -1
- package/dist/client/index.js +25 -19
- package/dist/client/vendor.js +1 -1
- package/dist/client/vue.js +1 -1
- package/dist/index.d.ts +435 -814
- package/dist/index.js +4022 -2989
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +107 -62
- package/dist/plugin.js.map +1 -1
- package/package.json +5 -3
package/dist/plugin.d.ts
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -813,16 +813,6 @@ function generatePromptFile(data) {
|
|
|
813
813
|
const toolsCode = formatToolsArray(data.tools);
|
|
814
814
|
lines.push(` tools: ${toolsCode},`);
|
|
815
815
|
}
|
|
816
|
-
if (data.handoffAgents && data.handoffAgents.length > 0) {
|
|
817
|
-
const agentsStr = data.handoffAgents.map((a) => `'${escapeString2(a)}'`).join(", ");
|
|
818
|
-
lines.push(` handoffAgents: [${agentsStr}],`);
|
|
819
|
-
}
|
|
820
|
-
if (data.beforeTool) {
|
|
821
|
-
lines.push(` beforeTool: '${escapeString2(data.beforeTool)}',`);
|
|
822
|
-
}
|
|
823
|
-
if (data.afterTool) {
|
|
824
|
-
lines.push(` afterTool: '${escapeString2(data.afterTool)}',`);
|
|
825
|
-
}
|
|
826
816
|
if (data.reasoning && hasNonNullProperties(data.reasoning)) {
|
|
827
817
|
const reasoningCode = formatReasoningConfig(data.reasoning);
|
|
828
818
|
lines.push(` reasoning: ${reasoningCode},`);
|
|
@@ -1000,9 +990,6 @@ function generateAgentFile(data) {
|
|
|
1000
990
|
if (data.toolDescription) {
|
|
1001
991
|
lines.push(` toolDescription: '${escapeString3(data.toolDescription)}',`);
|
|
1002
992
|
}
|
|
1003
|
-
if (data.tags && data.tags.length > 0) {
|
|
1004
|
-
lines.push(` tags: ${JSON.stringify(data.tags)},`);
|
|
1005
|
-
}
|
|
1006
993
|
lines.push(`});`);
|
|
1007
994
|
lines.push("");
|
|
1008
995
|
return lines.join("\n");
|
|
@@ -1022,11 +1009,11 @@ function formatSideConfig(config) {
|
|
|
1022
1009
|
if (config.stopToolResponseProperty) {
|
|
1023
1010
|
parts.push(` stopToolResponseProperty: '${escapeString3(config.stopToolResponseProperty)}',`);
|
|
1024
1011
|
}
|
|
1025
|
-
if (config.
|
|
1026
|
-
parts.push(`
|
|
1012
|
+
if (config.maxSteps !== void 0) {
|
|
1013
|
+
parts.push(` maxSteps: ${config.maxSteps},`);
|
|
1027
1014
|
}
|
|
1028
|
-
if (config.
|
|
1029
|
-
parts.push(`
|
|
1015
|
+
if (config.endSessionTool) {
|
|
1016
|
+
parts.push(` endSessionTool: '${escapeString3(config.endSessionTool)}',`);
|
|
1030
1017
|
}
|
|
1031
1018
|
if (config.manualStopCondition !== void 0) {
|
|
1032
1019
|
parts.push(` manualStopCondition: ${config.manualStopCondition},`);
|
|
@@ -1126,7 +1113,7 @@ function validateModelData(data) {
|
|
|
1126
1113
|
if (!data.provider || typeof data.provider !== "string") {
|
|
1127
1114
|
return "Model provider is required and must be a string";
|
|
1128
1115
|
}
|
|
1129
|
-
const validProviders = ["openai", "openrouter", "anthropic", "google"];
|
|
1116
|
+
const validProviders = ["openai", "openrouter", "anthropic", "google", "test"];
|
|
1130
1117
|
if (!validProviders.includes(data.provider)) {
|
|
1131
1118
|
return `Invalid provider '${data.provider}'. Must be one of: ${validProviders.join(", ")}`;
|
|
1132
1119
|
}
|
|
@@ -1173,11 +1160,8 @@ function transformPromptData(data) {
|
|
|
1173
1160
|
required_schema: "requiredSchema",
|
|
1174
1161
|
include_chat: "includeChat",
|
|
1175
1162
|
include_past_tools: "includePastTools",
|
|
1176
|
-
before_tool: "beforeTool",
|
|
1177
|
-
after_tool: "afterTool",
|
|
1178
1163
|
parallel_tool_calls: "parallelToolCalls",
|
|
1179
1164
|
tool_choice: "toolChoice",
|
|
1180
|
-
handoff_agents: "handoffAgents",
|
|
1181
1165
|
reasoning_effort: "reasoningEffort",
|
|
1182
1166
|
reasoning_max_tokens: "reasoningMaxTokens",
|
|
1183
1167
|
reasoning_exclude: "reasoningExclude",
|
|
@@ -1328,9 +1312,6 @@ function validatePromptData(data) {
|
|
|
1328
1312
|
if (data.tools !== void 0 && !Array.isArray(data.tools)) {
|
|
1329
1313
|
return "tools must be an array";
|
|
1330
1314
|
}
|
|
1331
|
-
if (data.handoffAgents !== void 0 && !Array.isArray(data.handoffAgents)) {
|
|
1332
|
-
return "handoffAgents must be an array";
|
|
1333
|
-
}
|
|
1334
1315
|
if (data.reasoning !== void 0) {
|
|
1335
1316
|
if (typeof data.reasoning !== "object") {
|
|
1336
1317
|
return "reasoning must be an object";
|
|
@@ -1375,11 +1356,11 @@ function transformAgentData(data) {
|
|
|
1375
1356
|
if (data.side_a_stop_tool_response_property) {
|
|
1376
1357
|
transformed.sideA.stopToolResponseProperty = data.side_a_stop_tool_response_property;
|
|
1377
1358
|
}
|
|
1378
|
-
if (data.
|
|
1379
|
-
transformed.sideA.
|
|
1359
|
+
if (data.side_a_max_steps !== void 0) {
|
|
1360
|
+
transformed.sideA.maxSteps = data.side_a_max_steps;
|
|
1380
1361
|
}
|
|
1381
|
-
if (data.
|
|
1382
|
-
transformed.sideA.
|
|
1362
|
+
if (data.side_a_end_session_tool) {
|
|
1363
|
+
transformed.sideA.endSessionTool = data.side_a_end_session_tool;
|
|
1383
1364
|
}
|
|
1384
1365
|
if (data.side_a_manual_stop_condition !== void 0) {
|
|
1385
1366
|
transformed.sideA.manualStopCondition = data.side_a_manual_stop_condition;
|
|
@@ -1400,11 +1381,11 @@ function transformAgentData(data) {
|
|
|
1400
1381
|
if (data.side_b_stop_tool_response_property) {
|
|
1401
1382
|
transformed.sideB.stopToolResponseProperty = data.side_b_stop_tool_response_property;
|
|
1402
1383
|
}
|
|
1403
|
-
if (data.
|
|
1404
|
-
transformed.sideB.
|
|
1384
|
+
if (data.side_b_max_steps !== void 0) {
|
|
1385
|
+
transformed.sideB.maxSteps = data.side_b_max_steps;
|
|
1405
1386
|
}
|
|
1406
|
-
if (data.
|
|
1407
|
-
transformed.sideB.
|
|
1387
|
+
if (data.side_b_end_session_tool) {
|
|
1388
|
+
transformed.sideB.endSessionTool = data.side_b_end_session_tool;
|
|
1408
1389
|
}
|
|
1409
1390
|
if (data.side_b_manual_stop_condition !== void 0) {
|
|
1410
1391
|
transformed.sideB.manualStopCondition = data.side_b_manual_stop_condition;
|
|
@@ -1416,9 +1397,6 @@ function transformAgentData(data) {
|
|
|
1416
1397
|
if (data.tool_description) {
|
|
1417
1398
|
transformed.toolDescription = data.tool_description;
|
|
1418
1399
|
}
|
|
1419
|
-
if (data.tags) {
|
|
1420
|
-
transformed.tags = data.tags;
|
|
1421
|
-
}
|
|
1422
1400
|
return transformed;
|
|
1423
1401
|
}
|
|
1424
1402
|
function getAgentFilePath(agentsDir, name) {
|
|
@@ -1606,9 +1584,9 @@ function validateAgentData(data) {
|
|
|
1606
1584
|
if (data.sideA.stopTool && !data.sideA.stopToolResponseProperty) {
|
|
1607
1585
|
return "sideA.stopToolResponseProperty is required when sideA.stopTool is set";
|
|
1608
1586
|
}
|
|
1609
|
-
if (data.sideA.
|
|
1610
|
-
if (typeof data.sideA.
|
|
1611
|
-
return "sideA.
|
|
1587
|
+
if (data.sideA.maxSteps !== void 0) {
|
|
1588
|
+
if (typeof data.sideA.maxSteps !== "number" || data.sideA.maxSteps <= 0) {
|
|
1589
|
+
return "sideA.maxSteps must be a positive number";
|
|
1612
1590
|
}
|
|
1613
1591
|
}
|
|
1614
1592
|
if (data.type === "dual_ai") {
|
|
@@ -1621,9 +1599,9 @@ function validateAgentData(data) {
|
|
|
1621
1599
|
if (data.sideB.stopTool && !data.sideB.stopToolResponseProperty) {
|
|
1622
1600
|
return "sideB.stopToolResponseProperty is required when sideB.stopTool is set";
|
|
1623
1601
|
}
|
|
1624
|
-
if (data.sideB.
|
|
1625
|
-
if (typeof data.sideB.
|
|
1626
|
-
return "sideB.
|
|
1602
|
+
if (data.sideB.maxSteps !== void 0) {
|
|
1603
|
+
if (typeof data.sideB.maxSteps !== "number" || data.sideB.maxSteps <= 0) {
|
|
1604
|
+
return "sideB.maxSteps must be a positive number";
|
|
1627
1605
|
}
|
|
1628
1606
|
}
|
|
1629
1607
|
}
|
|
@@ -1635,16 +1613,6 @@ function validateAgentData(data) {
|
|
|
1635
1613
|
return "maxSessionTurns must be a positive number";
|
|
1636
1614
|
}
|
|
1637
1615
|
}
|
|
1638
|
-
if (data.tags !== void 0) {
|
|
1639
|
-
if (!Array.isArray(data.tags)) {
|
|
1640
|
-
return "tags must be an array";
|
|
1641
|
-
}
|
|
1642
|
-
for (const tag of data.tags) {
|
|
1643
|
-
if (typeof tag !== "string") {
|
|
1644
|
-
return "Each tag must be a string";
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
1616
|
return null;
|
|
1649
1617
|
}
|
|
1650
1618
|
|
|
@@ -1668,6 +1636,8 @@ var VIRTUAL_PROMPTS_ID = "virtual:@standardagents-prompts";
|
|
|
1668
1636
|
var RESOLVED_VIRTUAL_PROMPTS_ID = "\0" + VIRTUAL_PROMPTS_ID;
|
|
1669
1637
|
var VIRTUAL_AGENTS_ID = "virtual:@standardagents-agents";
|
|
1670
1638
|
var RESOLVED_VIRTUAL_AGENTS_ID = "\0" + VIRTUAL_AGENTS_ID;
|
|
1639
|
+
var VIRTUAL_EFFECTS_ID = "virtual:@standardagents-effects";
|
|
1640
|
+
var RESOLVED_VIRTUAL_EFFECTS_ID = "\0" + VIRTUAL_EFFECTS_ID;
|
|
1671
1641
|
var VIRTUAL_BUILDER_ID = "virtual:@standardagents/builder";
|
|
1672
1642
|
var RESOLVED_VIRTUAL_BUILDER_ID = "\0" + VIRTUAL_BUILDER_ID;
|
|
1673
1643
|
function scanApiDirectory(dir, baseRoute = "") {
|
|
@@ -1843,6 +1813,33 @@ async function scanPromptsDirectory(dir) {
|
|
|
1843
1813
|
async function scanAgentsDirectory(dir) {
|
|
1844
1814
|
return scanConfigDirectory(dir, /export\s+default\s+defineAgent/);
|
|
1845
1815
|
}
|
|
1816
|
+
async function scanEffectsDirectory(dir) {
|
|
1817
|
+
const effects = [];
|
|
1818
|
+
if (!fs2.existsSync(dir)) {
|
|
1819
|
+
return effects;
|
|
1820
|
+
}
|
|
1821
|
+
const entries = await fs2.promises.readdir(dir, { withFileTypes: true });
|
|
1822
|
+
for (const entry of entries) {
|
|
1823
|
+
if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
1824
|
+
const fileName = entry.name.replace(".ts", "");
|
|
1825
|
+
const filePath = path3.join(dir, entry.name);
|
|
1826
|
+
const importPath = "./" + path3.relative(process.cwd(), filePath).replace(/\\/g, "/");
|
|
1827
|
+
if (fileName === "CLAUDE" || fileName.startsWith("_")) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
try {
|
|
1831
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
1832
|
+
if (!content.includes("defineEffect")) {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
} catch {
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
effects.push({ name: fileName, importPath });
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
return effects;
|
|
1842
|
+
}
|
|
1846
1843
|
function parseRequestBody(req) {
|
|
1847
1844
|
return new Promise((resolve, reject) => {
|
|
1848
1845
|
let body = "";
|
|
@@ -1873,6 +1870,7 @@ function agentbuilder(options = {}) {
|
|
|
1873
1870
|
const modelsDir = options.modelsDir ? path3.resolve(process.cwd(), options.modelsDir) : path3.resolve(process.cwd(), "agents/models");
|
|
1874
1871
|
const promptsDir = options.promptsDir ? path3.resolve(process.cwd(), options.promptsDir) : path3.resolve(process.cwd(), "agents/prompts");
|
|
1875
1872
|
const agentsDir = options.agentsDir ? path3.resolve(process.cwd(), options.agentsDir) : path3.resolve(process.cwd(), "agents/agents");
|
|
1873
|
+
const effectsDir = options.effectsDir ? path3.resolve(process.cwd(), options.effectsDir) : path3.resolve(process.cwd(), "agents/effects");
|
|
1876
1874
|
const outputDir = path3.resolve(process.cwd(), ".agents");
|
|
1877
1875
|
const typeGenConfig = {
|
|
1878
1876
|
modelsDir,
|
|
@@ -2145,6 +2143,9 @@ function agentbuilder(options = {}) {
|
|
|
2145
2143
|
if (id === VIRTUAL_AGENTS_ID) {
|
|
2146
2144
|
return RESOLVED_VIRTUAL_AGENTS_ID;
|
|
2147
2145
|
}
|
|
2146
|
+
if (id === VIRTUAL_EFFECTS_ID) {
|
|
2147
|
+
return RESOLVED_VIRTUAL_EFFECTS_ID;
|
|
2148
|
+
}
|
|
2148
2149
|
if (id === VIRTUAL_BUILDER_ID) {
|
|
2149
2150
|
return RESOLVED_VIRTUAL_BUILDER_ID;
|
|
2150
2151
|
}
|
|
@@ -2462,6 +2463,31 @@ ${agentsCode}
|
|
|
2462
2463
|
};
|
|
2463
2464
|
|
|
2464
2465
|
export const agentNames = ${JSON.stringify(agents.filter((a) => !a.error).map((a) => a.name))};
|
|
2466
|
+
`;
|
|
2467
|
+
}
|
|
2468
|
+
if (id === RESOLVED_VIRTUAL_EFFECTS_ID) {
|
|
2469
|
+
const effects = await scanEffectsDirectory(effectsDir);
|
|
2470
|
+
const effectsCode = effects.map(({ name, importPath, error }) => {
|
|
2471
|
+
if (error) {
|
|
2472
|
+
const escapedError = error.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2473
|
+
return ` "${name}": async () => { throw new Error("${escapedError}"); },`;
|
|
2474
|
+
} else {
|
|
2475
|
+
return ` "${name}": async () => {
|
|
2476
|
+
try {
|
|
2477
|
+
return (await import("${importPath}")).default;
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
console.error('Failed to import effect ${name}:', error);
|
|
2480
|
+
throw error;
|
|
2481
|
+
}
|
|
2482
|
+
},`;
|
|
2483
|
+
}
|
|
2484
|
+
}).join("\n");
|
|
2485
|
+
return `// Virtual agent effects module
|
|
2486
|
+
export const effects = {
|
|
2487
|
+
${effectsCode}
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
export const effectNames = ${JSON.stringify(effects.filter((e) => !e.error).map((e) => e.name))};
|
|
2465
2491
|
`;
|
|
2466
2492
|
}
|
|
2467
2493
|
if (id === RESOLVED_VIRTUAL_BUILDER_ID) {
|
|
@@ -2470,6 +2496,7 @@ export const agentNames = ${JSON.stringify(agents.filter((a) => !a.error).map((a
|
|
|
2470
2496
|
const models = await scanModelsDirectory(modelsDir);
|
|
2471
2497
|
const prompts = await scanPromptsDirectory(promptsDir);
|
|
2472
2498
|
const agents = await scanAgentsDirectory(agentsDir);
|
|
2499
|
+
const effects = await scanEffectsDirectory(effectsDir);
|
|
2473
2500
|
const toAbsolutePath = (relativePath) => {
|
|
2474
2501
|
if (relativePath.startsWith("./")) {
|
|
2475
2502
|
return path3.resolve(process.cwd(), relativePath).replace(/\\/g, "/");
|
|
@@ -2548,6 +2575,22 @@ export const agentNames = ${JSON.stringify(agents.filter((a) => !a.error).map((a
|
|
|
2548
2575
|
console.error('Failed to import agent ${name}:', error);
|
|
2549
2576
|
throw error;
|
|
2550
2577
|
}
|
|
2578
|
+
},`;
|
|
2579
|
+
}
|
|
2580
|
+
}).join("\n");
|
|
2581
|
+
const effectsCode = effects.map(({ name, importPath, error }) => {
|
|
2582
|
+
const absPath = toAbsolutePath(importPath);
|
|
2583
|
+
if (error) {
|
|
2584
|
+
const escapedError = error.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2585
|
+
return ` "${name}": async () => { throw new Error("${escapedError}"); },`;
|
|
2586
|
+
} else {
|
|
2587
|
+
return ` "${name}": async () => {
|
|
2588
|
+
try {
|
|
2589
|
+
return (await import("${absPath}")).default;
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
console.error('Failed to import effect ${name}:', error);
|
|
2592
|
+
throw error;
|
|
2593
|
+
}
|
|
2551
2594
|
},`;
|
|
2552
2595
|
}
|
|
2553
2596
|
}).join("\n");
|
|
@@ -2585,6 +2628,10 @@ const _agents = {
|
|
|
2585
2628
|
${agentsCode}
|
|
2586
2629
|
};
|
|
2587
2630
|
|
|
2631
|
+
const _effects = {
|
|
2632
|
+
${effectsCode}
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2588
2635
|
/**
|
|
2589
2636
|
* DurableThread with all virtual module methods already implemented.
|
|
2590
2637
|
* Simply extend this class in your agents/Thread.ts file.
|
|
@@ -2619,6 +2666,10 @@ export class DurableThread extends _BaseDurableThread {
|
|
|
2619
2666
|
agents() {
|
|
2620
2667
|
return _agents;
|
|
2621
2668
|
}
|
|
2669
|
+
|
|
2670
|
+
effects() {
|
|
2671
|
+
return _effects;
|
|
2672
|
+
}
|
|
2622
2673
|
}
|
|
2623
2674
|
|
|
2624
2675
|
/**
|
|
@@ -2645,6 +2696,10 @@ export class DurableAgentBuilder extends _BaseDurableAgentBuilder {
|
|
|
2645
2696
|
agents() {
|
|
2646
2697
|
return _agents;
|
|
2647
2698
|
}
|
|
2699
|
+
|
|
2700
|
+
effects() {
|
|
2701
|
+
return _effects;
|
|
2702
|
+
}
|
|
2648
2703
|
}
|
|
2649
2704
|
`;
|
|
2650
2705
|
}
|
|
@@ -2657,6 +2712,7 @@ export class DurableAgentBuilder extends _BaseDurableAgentBuilder {
|
|
|
2657
2712
|
this.addWatchFile(modelsDir);
|
|
2658
2713
|
this.addWatchFile(promptsDir);
|
|
2659
2714
|
this.addWatchFile(agentsDir);
|
|
2715
|
+
this.addWatchFile(effectsDir);
|
|
2660
2716
|
},
|
|
2661
2717
|
configureServer(server) {
|
|
2662
2718
|
server.watcher.on("add", async (file) => {
|
|
@@ -2901,20 +2957,12 @@ export class DurableAgentBuilder extends _BaseDurableAgentBuilder {
|
|
|
2901
2957
|
const getIncludePastTools = (c) => c.match(/includePastTools:\s*(true|false)/)?.[1] === "true";
|
|
2902
2958
|
const getParallelToolCalls = (c) => c.match(/parallelToolCalls:\s*(true|false)/)?.[1] === "true";
|
|
2903
2959
|
const getToolChoice = (c) => c.match(/toolChoice:\s*['"]([^'"]+)['"]/)?.[1];
|
|
2904
|
-
const getBeforeTool = (c) => c.match(/beforeTool:\s*['"]([^'"]+)['"]/)?.[1];
|
|
2905
|
-
const getAfterTool = (c) => c.match(/afterTool:\s*['"]([^'"]+)['"]/)?.[1];
|
|
2906
2960
|
const getTools = (c) => {
|
|
2907
2961
|
const match = c.match(/tools:\s*\[([^\]]*)\]/);
|
|
2908
2962
|
if (!match) return [];
|
|
2909
2963
|
const items = match[1].match(/['"]([^'"]+)['"]/g);
|
|
2910
2964
|
return items ? items.map((s) => s.replace(/['"]/g, "")) : [];
|
|
2911
2965
|
};
|
|
2912
|
-
const getHandoffAgents = (c) => {
|
|
2913
|
-
const match = c.match(/handoffAgents:\s*\[([^\]]*)\]/);
|
|
2914
|
-
if (!match) return [];
|
|
2915
|
-
const items = match[1].match(/['"]([^'"]+)['"]/g);
|
|
2916
|
-
return items ? items.map((s) => s.replace(/['"]/g, "")) : [];
|
|
2917
|
-
};
|
|
2918
2966
|
const getPrompt = (c) => {
|
|
2919
2967
|
const backtickMatch = c.match(/prompt:\s*`([\s\S]*?)`/);
|
|
2920
2968
|
if (backtickMatch) return backtickMatch[1];
|
|
@@ -2942,10 +2990,7 @@ export class DurableAgentBuilder extends _BaseDurableAgentBuilder {
|
|
|
2942
2990
|
include_past_tools: getIncludePastTools(content),
|
|
2943
2991
|
parallel_tool_calls: getParallelToolCalls(content),
|
|
2944
2992
|
tool_choice: getToolChoice(content) || "auto",
|
|
2945
|
-
before_tool: getBeforeTool(content) || null,
|
|
2946
|
-
after_tool: getAfterTool(content) || null,
|
|
2947
2993
|
tools: getTools(content),
|
|
2948
|
-
prompts: getHandoffAgents(content),
|
|
2949
2994
|
reasoning: null,
|
|
2950
2995
|
// Complex to parse
|
|
2951
2996
|
created_at: Math.floor(Date.now() / 1e3)
|