feedeas 0.1.0-alpha.13 → 0.1.0-alpha.15
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 +13 -2
- package/dist/cli/index.js +1022 -331
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var playwright_installer_exports = {};
|
|
|
19
19
|
__export(playwright_installer_exports, {
|
|
20
20
|
ensurePlaywrightBrowsers: () => ensurePlaywrightBrowsers
|
|
21
21
|
});
|
|
22
|
-
import { spawn as
|
|
22
|
+
import { spawn as spawn3 } from "child_process";
|
|
23
23
|
import { createInterface } from "readline";
|
|
24
24
|
async function ensurePlaywrightBrowsers() {
|
|
25
25
|
try {
|
|
@@ -71,7 +71,7 @@ function promptUser(question) {
|
|
|
71
71
|
}
|
|
72
72
|
function installPlaywrightBrowsers() {
|
|
73
73
|
return new Promise((resolve) => {
|
|
74
|
-
const child =
|
|
74
|
+
const child = spawn3("npx", ["playwright", "install", "chromium", "--with-deps"], {
|
|
75
75
|
stdio: "inherit",
|
|
76
76
|
// Show installation progress to user
|
|
77
77
|
shell: true
|
|
@@ -97,10 +97,10 @@ import { Command as Command14 } from "commander";
|
|
|
97
97
|
// src/cli/commands/record.ts
|
|
98
98
|
import { Command } from "commander";
|
|
99
99
|
import { chromium } from "playwright-core";
|
|
100
|
-
import { spawn as
|
|
101
|
-
import
|
|
102
|
-
import
|
|
103
|
-
import { fileURLToPath as
|
|
100
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
101
|
+
import fs5 from "fs";
|
|
102
|
+
import path6 from "path";
|
|
103
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
104
104
|
|
|
105
105
|
// src/cli/services/ffprobe.ts
|
|
106
106
|
import { spawn } from "child_process";
|
|
@@ -256,9 +256,92 @@ import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
256
256
|
import { join, dirname } from "node:path";
|
|
257
257
|
import { existsSync } from "node:fs";
|
|
258
258
|
|
|
259
|
+
// src/cli/server/cli-runner.ts
|
|
260
|
+
import { spawn as spawn2 } from "child_process";
|
|
261
|
+
import path2 from "path";
|
|
262
|
+
import fs2 from "fs";
|
|
263
|
+
import { fileURLToPath } from "url";
|
|
264
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
265
|
+
var __dirname = path2.dirname(__filename);
|
|
266
|
+
async function runCliCommand(args, cwd) {
|
|
267
|
+
const isCompiled = __filename.endsWith(".js");
|
|
268
|
+
let command;
|
|
269
|
+
let spawnArgs;
|
|
270
|
+
if (isCompiled) {
|
|
271
|
+
const binPath = path2.resolve(__dirname, "../../../bin/feedeas.js");
|
|
272
|
+
if (!fs2.existsSync(binPath)) {
|
|
273
|
+
throw new Error(`Could not find compiled CLI binary at ${binPath}`);
|
|
274
|
+
}
|
|
275
|
+
command = "node";
|
|
276
|
+
spawnArgs = [binPath, ...args];
|
|
277
|
+
} else {
|
|
278
|
+
const srcPath = path2.resolve(__dirname, "../index.ts");
|
|
279
|
+
command = "bun";
|
|
280
|
+
spawnArgs = ["run", srcPath, ...args];
|
|
281
|
+
}
|
|
282
|
+
return new Promise((resolve, reject) => {
|
|
283
|
+
const child = spawn2(command, spawnArgs, {
|
|
284
|
+
cwd,
|
|
285
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
286
|
+
env: {
|
|
287
|
+
...process.env,
|
|
288
|
+
FORCE_COLOR: "0"
|
|
289
|
+
// Disable colored output for easier JSON parsing
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
let stdoutData = "";
|
|
293
|
+
let stderrData = "";
|
|
294
|
+
child.stdout.on("data", (chunk) => {
|
|
295
|
+
stdoutData += chunk.toString();
|
|
296
|
+
});
|
|
297
|
+
child.stderr.on("data", (chunk) => {
|
|
298
|
+
stderrData += chunk.toString();
|
|
299
|
+
});
|
|
300
|
+
child.on("close", (code) => {
|
|
301
|
+
let parsedJson = null;
|
|
302
|
+
try {
|
|
303
|
+
const lines = stdoutData.split("\n");
|
|
304
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
305
|
+
const line = lines[i].trim();
|
|
306
|
+
if (line.startsWith("{") || line.startsWith("[")) {
|
|
307
|
+
try {
|
|
308
|
+
parsedJson = JSON.parse(line);
|
|
309
|
+
break;
|
|
310
|
+
} catch {
|
|
311
|
+
const block = lines.slice(i).join("\n");
|
|
312
|
+
try {
|
|
313
|
+
parsedJson = JSON.parse(block);
|
|
314
|
+
break;
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch (e) {
|
|
321
|
+
}
|
|
322
|
+
if (code === 0) {
|
|
323
|
+
if (parsedJson) {
|
|
324
|
+
resolve(parsedJson);
|
|
325
|
+
} else {
|
|
326
|
+
resolve({ rawOutput: stdoutData.trim() });
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
if (parsedJson && parsedJson.error) {
|
|
330
|
+
reject(new Error(parsedJson.error));
|
|
331
|
+
} else {
|
|
332
|
+
reject(new Error(`CLI command failed with code ${code}:
|
|
333
|
+
${stderrData || stdoutData}`));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
child.on("error", (err) => {
|
|
338
|
+
reject(new Error(`Failed to spawn CLI process: ${err.message}`));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
259
343
|
// src/cli/services/taste.ts
|
|
260
|
-
import
|
|
261
|
-
import path2 from "node:path";
|
|
344
|
+
import path3 from "node:path";
|
|
262
345
|
var DEFAULT_TASTE_FILE_CONTENT = `# Taste File v1
|
|
263
346
|
## Brand Identity
|
|
264
347
|
Describe brand voice, tone, and core promise.
|
|
@@ -332,28 +415,16 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
332
415
|
]);
|
|
333
416
|
function safeResolveFromCwd(relativePath) {
|
|
334
417
|
const cwd = process.cwd();
|
|
335
|
-
const normalized =
|
|
418
|
+
const normalized = path3.normalize(relativePath);
|
|
336
419
|
if (normalized.includes("..")) {
|
|
337
420
|
throw new Error("Invalid path");
|
|
338
421
|
}
|
|
339
|
-
const fullPath =
|
|
340
|
-
if (fullPath !== cwd && !fullPath.startsWith(`${cwd}${
|
|
422
|
+
const fullPath = path3.resolve(cwd, normalized);
|
|
423
|
+
if (fullPath !== cwd && !fullPath.startsWith(`${cwd}${path3.sep}`)) {
|
|
341
424
|
throw new Error("Invalid path");
|
|
342
425
|
}
|
|
343
426
|
return fullPath;
|
|
344
427
|
}
|
|
345
|
-
function ensureTasteWorkspaceFiles(tasteFilePath, memoryFilePath) {
|
|
346
|
-
const tasteFull = safeResolveFromCwd(tasteFilePath);
|
|
347
|
-
const memoryFull = safeResolveFromCwd(memoryFilePath);
|
|
348
|
-
fs2.mkdirSync(path2.dirname(tasteFull), { recursive: true });
|
|
349
|
-
fs2.mkdirSync(path2.dirname(memoryFull), { recursive: true });
|
|
350
|
-
if (!fs2.existsSync(tasteFull)) {
|
|
351
|
-
fs2.writeFileSync(tasteFull, DEFAULT_TASTE_FILE_CONTENT, "utf-8");
|
|
352
|
-
}
|
|
353
|
-
if (!fs2.existsSync(memoryFull)) {
|
|
354
|
-
fs2.writeFileSync(memoryFull, DEFAULT_MEMORY_FILE_CONTENT, "utf-8");
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
428
|
function parseListValue(value) {
|
|
358
429
|
const trimmed = value.trim();
|
|
359
430
|
if (!trimmed) return [];
|
|
@@ -414,7 +485,10 @@ function parseTasteMemoryMarkdown(markdown) {
|
|
|
414
485
|
tags: parseListValue(map.tags || ""),
|
|
415
486
|
freshnessTerms: parseListValue(map.freshness_terms || ""),
|
|
416
487
|
summary: map.summary || "",
|
|
417
|
-
content: map.content || ""
|
|
488
|
+
content: map.content || "",
|
|
489
|
+
status: map.status || void 0,
|
|
490
|
+
reasonTags: parseListValue(map.reason_tags || ""),
|
|
491
|
+
notes: map.notes || ""
|
|
418
492
|
});
|
|
419
493
|
}
|
|
420
494
|
return entries;
|
|
@@ -432,30 +506,21 @@ function formatMemoryEntry(entry, id) {
|
|
|
432
506
|
`tags: [${(entry.tags || []).join(", ")}]`,
|
|
433
507
|
`freshness_terms: [${(entry.freshnessTerms || []).join(", ")}]`,
|
|
434
508
|
`summary: ${entry.summary || ""}`,
|
|
509
|
+
...entry.status ? [`status: ${entry.status}`] : [],
|
|
510
|
+
...entry.reasonTags && entry.reasonTags.length ? [`reason_tags: [${entry.reasonTags.join(", ")}]`] : [],
|
|
511
|
+
...entry.notes ? [`notes: ${entry.notes}`] : [],
|
|
435
512
|
"content: |",
|
|
436
513
|
contentLines || " ",
|
|
437
514
|
"```",
|
|
438
515
|
""
|
|
439
516
|
].join("\n");
|
|
440
517
|
}
|
|
441
|
-
function appendMemoryEntries(markdown, ideas) {
|
|
442
|
-
const base = markdown.trim().length > 0 ? markdown.trimEnd() : DEFAULT_MEMORY_FILE_CONTENT.trimEnd();
|
|
443
|
-
const appendedIds = [];
|
|
444
|
-
let output = `${base}
|
|
445
|
-
|
|
446
|
-
`;
|
|
447
|
-
for (const idea of ideas) {
|
|
448
|
-
const id = `mem_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}_${Math.floor(Math.random() * 900 + 100)}_${appendedIds.length + 1}`;
|
|
449
|
-
appendedIds.push(id);
|
|
450
|
-
output += formatMemoryEntry(idea, id);
|
|
451
|
-
}
|
|
452
|
-
return { markdown: output, appendedIds };
|
|
453
|
-
}
|
|
454
518
|
function queryMemoryItems(items, options) {
|
|
455
519
|
const limit = Math.max(1, options.limit ?? 20);
|
|
456
520
|
const sinceDate = options.since ? new Date(options.since) : null;
|
|
457
521
|
const text = options.text?.toLowerCase().trim();
|
|
458
522
|
const tag = options.tag?.toLowerCase().trim();
|
|
523
|
+
const status = options.status?.toLowerCase().trim();
|
|
459
524
|
return items.filter((item) => {
|
|
460
525
|
if (sinceDate && !Number.isNaN(sinceDate.getTime())) {
|
|
461
526
|
const createdAt = new Date(item.createdAt);
|
|
@@ -464,6 +529,9 @@ function queryMemoryItems(items, options) {
|
|
|
464
529
|
if (tag && !item.tags.map((t) => t.toLowerCase()).includes(tag)) {
|
|
465
530
|
return false;
|
|
466
531
|
}
|
|
532
|
+
if (status && (item.status || "").toLowerCase() !== status) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
467
535
|
if (text) {
|
|
468
536
|
const hay = `${item.title}
|
|
469
537
|
${item.summary}
|
|
@@ -473,6 +541,71 @@ ${item.content}`.toLowerCase();
|
|
|
473
541
|
return true;
|
|
474
542
|
}).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, limit);
|
|
475
543
|
}
|
|
544
|
+
function upsertFeedbackSection(tasteContent, sectionContent) {
|
|
545
|
+
const start = "<!-- taste-feedback:start -->";
|
|
546
|
+
const end = "<!-- taste-feedback:end -->";
|
|
547
|
+
const block = `${start}
|
|
548
|
+
${sectionContent.trim()}
|
|
549
|
+
${end}`;
|
|
550
|
+
const hasStart = tasteContent.includes(start);
|
|
551
|
+
const hasEnd = tasteContent.includes(end);
|
|
552
|
+
if (hasStart && hasEnd) {
|
|
553
|
+
return tasteContent.replace(new RegExp(`${start}[\\s\\S]*?${end}`, "m"), block);
|
|
554
|
+
}
|
|
555
|
+
const trimmed = tasteContent.trimEnd();
|
|
556
|
+
return `${trimmed}
|
|
557
|
+
|
|
558
|
+
${block}
|
|
559
|
+
`;
|
|
560
|
+
}
|
|
561
|
+
function buildTasteSuggestionFromMemory(tasteContent, memories) {
|
|
562
|
+
const recent = [...memories].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, 50);
|
|
563
|
+
const acceptedTags = /* @__PURE__ */ new Map();
|
|
564
|
+
const rejectedReasons = /* @__PURE__ */ new Map();
|
|
565
|
+
for (const item of recent) {
|
|
566
|
+
const status = (item.status || "").toLowerCase();
|
|
567
|
+
if (status === "accepted") {
|
|
568
|
+
for (const tag of item.tags) {
|
|
569
|
+
acceptedTags.set(tag, (acceptedTags.get(tag) || 0) + 1);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (status === "rejected") {
|
|
573
|
+
const reasons = item.reasonTags && item.reasonTags.length > 0 ? item.reasonTags : item.tags;
|
|
574
|
+
for (const reason of reasons) {
|
|
575
|
+
rejectedReasons.set(reason, (rejectedReasons.get(reason) || 0) + 1);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const topAccepted = [...acceptedTags.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
|
|
580
|
+
const topRejected = [...rejectedReasons.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name]) => name);
|
|
581
|
+
const sectionLines = [
|
|
582
|
+
"## Feedback Signals",
|
|
583
|
+
...topAccepted.length ? [`- Prioritize these themes: ${topAccepted.join(", ")}`] : ["- Prioritize themes with proven audience resonance from recent accepted ideas."],
|
|
584
|
+
...topRejected.length ? [`- Avoid these recurring issues: ${topRejected.join(", ")}`] : ["- Avoid repeating patterns that were consistently rejected."],
|
|
585
|
+
"- Keep hooks specific and concrete, not generic."
|
|
586
|
+
];
|
|
587
|
+
const proposedTasteContent = upsertFeedbackSection(tasteContent, sectionLines.join("\n"));
|
|
588
|
+
const patch = [
|
|
589
|
+
"--- taste/taste.md",
|
|
590
|
+
"+++ taste/taste.md",
|
|
591
|
+
"@@",
|
|
592
|
+
`+ ${sectionLines[0]}`,
|
|
593
|
+
`+ ${sectionLines[1]}`,
|
|
594
|
+
`+ ${sectionLines[2]}`,
|
|
595
|
+
`+ ${sectionLines[3]}`
|
|
596
|
+
].join("\n");
|
|
597
|
+
return {
|
|
598
|
+
title: "Feedback-driven taste refinement",
|
|
599
|
+
summary: "Update taste profile with recurring accepted/rejected memory patterns.",
|
|
600
|
+
rationale: [
|
|
601
|
+
`Analyzed ${recent.length} recent memory entries.`,
|
|
602
|
+
topAccepted.length ? `Accepted patterns concentrated around: ${topAccepted.join(", ")}.` : "No strong accepted-tag pattern found; using generic prioritization guidance.",
|
|
603
|
+
topRejected.length ? `Rejected patterns concentrated around: ${topRejected.join(", ")}.` : "No strong rejected-tag pattern found; using generic avoidance guidance."
|
|
604
|
+
].join("\n"),
|
|
605
|
+
patch,
|
|
606
|
+
proposedTasteContent
|
|
607
|
+
};
|
|
608
|
+
}
|
|
476
609
|
function tokenize(input) {
|
|
477
610
|
return new Set(
|
|
478
611
|
input.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).map((word) => word.trim()).filter((word) => word.length > 3 && !STOP_WORDS.has(word))
|
|
@@ -667,9 +800,298 @@ async function simulateIdeas(params) {
|
|
|
667
800
|
};
|
|
668
801
|
}
|
|
669
802
|
|
|
803
|
+
// src/cli/services/taste-store.ts
|
|
804
|
+
import fs3 from "node:fs";
|
|
805
|
+
import path4 from "node:path";
|
|
806
|
+
import { createHash } from "node:crypto";
|
|
807
|
+
var DEFAULT_SUGGESTIONS_FILE_CONTENT = "# Taste Suggestions v1\n";
|
|
808
|
+
function hashContent(content) {
|
|
809
|
+
return createHash("sha256").update(content).digest("hex");
|
|
810
|
+
}
|
|
811
|
+
function buildId(prefix) {
|
|
812
|
+
return `${prefix}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}_${Math.floor(Math.random() * 9e5 + 1e5)}`;
|
|
813
|
+
}
|
|
814
|
+
function formatMultilineField(name, value) {
|
|
815
|
+
const lines = (value || "").split("\n");
|
|
816
|
+
return [
|
|
817
|
+
`${name}: |`,
|
|
818
|
+
...(lines.length ? lines : [""]).map((line) => ` ${line}`)
|
|
819
|
+
];
|
|
820
|
+
}
|
|
821
|
+
function parseSuggestionMarkdown(markdown) {
|
|
822
|
+
const blockRegex = /```taste-suggestion\s*([\s\S]*?)```/g;
|
|
823
|
+
const entries = [];
|
|
824
|
+
let match;
|
|
825
|
+
while ((match = blockRegex.exec(markdown)) !== null) {
|
|
826
|
+
const block = match[1].trim();
|
|
827
|
+
const lines = block.split("\n");
|
|
828
|
+
const map = {};
|
|
829
|
+
let idx = 0;
|
|
830
|
+
while (idx < lines.length) {
|
|
831
|
+
const line = lines[idx];
|
|
832
|
+
const keyMatch = line.match(/^([a-z_]+):\s*(.*)$/i);
|
|
833
|
+
if (!keyMatch) {
|
|
834
|
+
idx += 1;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
const key = keyMatch[1];
|
|
838
|
+
const value = keyMatch[2] ?? "";
|
|
839
|
+
if ((key === "rationale" || key === "patch" || key === "proposed_taste_content") && value.trim() === "|") {
|
|
840
|
+
const contentLines = [];
|
|
841
|
+
idx += 1;
|
|
842
|
+
while (idx < lines.length) {
|
|
843
|
+
const contentLine = lines[idx];
|
|
844
|
+
if (contentLine.startsWith(" ")) {
|
|
845
|
+
contentLines.push(contentLine.slice(2));
|
|
846
|
+
idx += 1;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (contentLine.trim() === "") {
|
|
850
|
+
contentLines.push("");
|
|
851
|
+
idx += 1;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
map[key] = contentLines.join("\n").trimEnd();
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
map[key] = value.trim();
|
|
860
|
+
idx += 1;
|
|
861
|
+
}
|
|
862
|
+
if (!map.id || !map.created_at || !map.title) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const status = map.status || "pending";
|
|
866
|
+
entries.push({
|
|
867
|
+
id: map.id,
|
|
868
|
+
createdAt: map.created_at,
|
|
869
|
+
status: status === "applied" || status === "rejected" ? status : "pending",
|
|
870
|
+
title: map.title,
|
|
871
|
+
summary: map.summary || "",
|
|
872
|
+
rationale: map.rationale || "",
|
|
873
|
+
patch: map.patch || "",
|
|
874
|
+
proposedTasteContent: map.proposed_taste_content || "",
|
|
875
|
+
baseVersion: map.base_version || void 0,
|
|
876
|
+
appliedAt: map.applied_at || void 0,
|
|
877
|
+
rejectedAt: map.rejected_at || void 0,
|
|
878
|
+
reviewNote: map.review_note || void 0
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return entries;
|
|
882
|
+
}
|
|
883
|
+
function formatSuggestionEntry(entry) {
|
|
884
|
+
const output = [
|
|
885
|
+
"```taste-suggestion",
|
|
886
|
+
`id: ${entry.id}`,
|
|
887
|
+
`created_at: ${entry.createdAt}`,
|
|
888
|
+
`status: ${entry.status}`,
|
|
889
|
+
`title: ${entry.title}`,
|
|
890
|
+
`summary: ${entry.summary || ""}`,
|
|
891
|
+
...entry.baseVersion ? [`base_version: ${entry.baseVersion}`] : [],
|
|
892
|
+
...formatMultilineField("rationale", entry.rationale),
|
|
893
|
+
...formatMultilineField("patch", entry.patch),
|
|
894
|
+
...formatMultilineField("proposed_taste_content", entry.proposedTasteContent),
|
|
895
|
+
...entry.appliedAt ? [`applied_at: ${entry.appliedAt}`] : [],
|
|
896
|
+
...entry.rejectedAt ? [`rejected_at: ${entry.rejectedAt}`] : [],
|
|
897
|
+
...entry.reviewNote ? [`review_note: ${entry.reviewNote}`] : [],
|
|
898
|
+
"```",
|
|
899
|
+
""
|
|
900
|
+
];
|
|
901
|
+
return output.join("\n");
|
|
902
|
+
}
|
|
903
|
+
function formatSuggestionsMarkdown(entries) {
|
|
904
|
+
const base = DEFAULT_SUGGESTIONS_FILE_CONTENT.trimEnd();
|
|
905
|
+
if (entries.length === 0) {
|
|
906
|
+
return `${base}
|
|
907
|
+
`;
|
|
908
|
+
}
|
|
909
|
+
const blocks = entries.map((entry) => formatSuggestionEntry(entry)).join("");
|
|
910
|
+
return `${base}
|
|
911
|
+
|
|
912
|
+
${blocks}`;
|
|
913
|
+
}
|
|
914
|
+
var MarkdownTasteStore = class {
|
|
915
|
+
constructor(paths) {
|
|
916
|
+
this.paths = paths;
|
|
917
|
+
}
|
|
918
|
+
backend = "markdown";
|
|
919
|
+
resolveAll() {
|
|
920
|
+
return {
|
|
921
|
+
tastePath: safeResolveFromCwd(this.paths.tasteFilePath),
|
|
922
|
+
memoryPath: safeResolveFromCwd(this.paths.memoryFilePath),
|
|
923
|
+
suggestionsPath: safeResolveFromCwd(this.paths.suggestionsFilePath)
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
async ensureWorkspace() {
|
|
927
|
+
const { tastePath, memoryPath, suggestionsPath } = this.resolveAll();
|
|
928
|
+
fs3.mkdirSync(path4.dirname(tastePath), { recursive: true });
|
|
929
|
+
fs3.mkdirSync(path4.dirname(memoryPath), { recursive: true });
|
|
930
|
+
fs3.mkdirSync(path4.dirname(suggestionsPath), { recursive: true });
|
|
931
|
+
if (!fs3.existsSync(tastePath)) {
|
|
932
|
+
fs3.writeFileSync(tastePath, DEFAULT_TASTE_FILE_CONTENT, "utf-8");
|
|
933
|
+
}
|
|
934
|
+
if (!fs3.existsSync(memoryPath)) {
|
|
935
|
+
fs3.writeFileSync(memoryPath, DEFAULT_MEMORY_FILE_CONTENT, "utf-8");
|
|
936
|
+
}
|
|
937
|
+
if (!fs3.existsSync(suggestionsPath)) {
|
|
938
|
+
fs3.writeFileSync(suggestionsPath, DEFAULT_SUGGESTIONS_FILE_CONTENT, "utf-8");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async readTaste() {
|
|
942
|
+
await this.ensureWorkspace();
|
|
943
|
+
const { tastePath } = this.resolveAll();
|
|
944
|
+
const content = fs3.readFileSync(tastePath, "utf-8");
|
|
945
|
+
return { content, version: hashContent(content) };
|
|
946
|
+
}
|
|
947
|
+
async writeTaste(content, expectedVersion) {
|
|
948
|
+
await this.ensureWorkspace();
|
|
949
|
+
const { tastePath } = this.resolveAll();
|
|
950
|
+
const current = fs3.existsSync(tastePath) ? fs3.readFileSync(tastePath, "utf-8") : "";
|
|
951
|
+
const currentVersion = hashContent(current);
|
|
952
|
+
if (expectedVersion && expectedVersion !== currentVersion) {
|
|
953
|
+
throw new Error("Taste file changed since proposal creation. Re-run suggest before apply.");
|
|
954
|
+
}
|
|
955
|
+
fs3.writeFileSync(tastePath, content, "utf-8");
|
|
956
|
+
return { version: hashContent(content) };
|
|
957
|
+
}
|
|
958
|
+
async appendMemory(entry) {
|
|
959
|
+
await this.ensureWorkspace();
|
|
960
|
+
const { memoryPath } = this.resolveAll();
|
|
961
|
+
const current = fs3.existsSync(memoryPath) ? fs3.readFileSync(memoryPath, "utf-8") : "";
|
|
962
|
+
const id = buildId("mem");
|
|
963
|
+
const markdown = `${(current || DEFAULT_MEMORY_FILE_CONTENT).trimEnd()}
|
|
964
|
+
|
|
965
|
+
${formatMemoryEntry(entry, id)}`;
|
|
966
|
+
fs3.writeFileSync(memoryPath, markdown, "utf-8");
|
|
967
|
+
const parsed = parseTasteMemoryMarkdown(markdown).find((item) => item.id === id);
|
|
968
|
+
if (!parsed) {
|
|
969
|
+
throw new Error("Failed to append memory entry");
|
|
970
|
+
}
|
|
971
|
+
return parsed;
|
|
972
|
+
}
|
|
973
|
+
async queryMemory(options) {
|
|
974
|
+
await this.ensureWorkspace();
|
|
975
|
+
const { memoryPath } = this.resolveAll();
|
|
976
|
+
const content = fs3.existsSync(memoryPath) ? fs3.readFileSync(memoryPath, "utf-8") : "";
|
|
977
|
+
const items = parseTasteMemoryMarkdown(content);
|
|
978
|
+
return queryMemoryItems(items, options);
|
|
979
|
+
}
|
|
980
|
+
async appendSuggestion(suggestion) {
|
|
981
|
+
await this.ensureWorkspace();
|
|
982
|
+
const { suggestionsPath } = this.resolveAll();
|
|
983
|
+
const current = fs3.existsSync(suggestionsPath) ? fs3.readFileSync(suggestionsPath, "utf-8") : "";
|
|
984
|
+
const entry = {
|
|
985
|
+
id: buildId("sug"),
|
|
986
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
987
|
+
status: "pending",
|
|
988
|
+
title: suggestion.title,
|
|
989
|
+
summary: suggestion.summary,
|
|
990
|
+
rationale: suggestion.rationale,
|
|
991
|
+
patch: suggestion.patch,
|
|
992
|
+
proposedTasteContent: suggestion.proposedTasteContent,
|
|
993
|
+
baseVersion: suggestion.baseVersion,
|
|
994
|
+
appliedAt: void 0,
|
|
995
|
+
rejectedAt: void 0,
|
|
996
|
+
reviewNote: void 0
|
|
997
|
+
};
|
|
998
|
+
const existing = parseSuggestionMarkdown(current);
|
|
999
|
+
const markdown = formatSuggestionsMarkdown([...existing, entry]);
|
|
1000
|
+
fs3.writeFileSync(suggestionsPath, markdown, "utf-8");
|
|
1001
|
+
return entry;
|
|
1002
|
+
}
|
|
1003
|
+
async listSuggestions(status) {
|
|
1004
|
+
await this.ensureWorkspace();
|
|
1005
|
+
const { suggestionsPath } = this.resolveAll();
|
|
1006
|
+
const current = fs3.existsSync(suggestionsPath) ? fs3.readFileSync(suggestionsPath, "utf-8") : "";
|
|
1007
|
+
const entries = parseSuggestionMarkdown(current).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1008
|
+
if (!status) {
|
|
1009
|
+
return entries;
|
|
1010
|
+
}
|
|
1011
|
+
return entries.filter((item) => item.status === status);
|
|
1012
|
+
}
|
|
1013
|
+
async updateSuggestionStatus(id, status, metadata) {
|
|
1014
|
+
await this.ensureWorkspace();
|
|
1015
|
+
const { suggestionsPath } = this.resolveAll();
|
|
1016
|
+
const current = fs3.existsSync(suggestionsPath) ? fs3.readFileSync(suggestionsPath, "utf-8") : "";
|
|
1017
|
+
const entries = parseSuggestionMarkdown(current);
|
|
1018
|
+
const idx = entries.findIndex((entry) => entry.id === id);
|
|
1019
|
+
if (idx === -1) {
|
|
1020
|
+
throw new Error(`Suggestion not found: ${id}`);
|
|
1021
|
+
}
|
|
1022
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1023
|
+
const updated = {
|
|
1024
|
+
...entries[idx],
|
|
1025
|
+
status,
|
|
1026
|
+
appliedAt: status === "applied" ? metadata?.appliedAt || now : void 0,
|
|
1027
|
+
rejectedAt: status === "rejected" ? metadata?.rejectedAt || now : void 0,
|
|
1028
|
+
reviewNote: metadata?.reviewNote
|
|
1029
|
+
};
|
|
1030
|
+
entries[idx] = updated;
|
|
1031
|
+
fs3.writeFileSync(suggestionsPath, formatSuggestionsMarkdown(entries), "utf-8");
|
|
1032
|
+
return updated;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
var DbTasteStore = class {
|
|
1036
|
+
backend = "db";
|
|
1037
|
+
constructor(_) {
|
|
1038
|
+
}
|
|
1039
|
+
async ensureWorkspace() {
|
|
1040
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1041
|
+
}
|
|
1042
|
+
async readTaste() {
|
|
1043
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1044
|
+
}
|
|
1045
|
+
async writeTaste(_, __) {
|
|
1046
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1047
|
+
}
|
|
1048
|
+
async appendMemory(_) {
|
|
1049
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1050
|
+
}
|
|
1051
|
+
async queryMemory(_) {
|
|
1052
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1053
|
+
}
|
|
1054
|
+
async appendSuggestion(_) {
|
|
1055
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1056
|
+
}
|
|
1057
|
+
async listSuggestions(_) {
|
|
1058
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1059
|
+
}
|
|
1060
|
+
async updateSuggestionStatus(_, __, ___) {
|
|
1061
|
+
throw new Error("DB backend is not implemented yet. Use --storage markdown.");
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
function normalizeStorageBackend(value) {
|
|
1065
|
+
const lower = (value || process.env.TASTE_STORAGE_BACKEND || "markdown").toLowerCase();
|
|
1066
|
+
if (lower === "db") {
|
|
1067
|
+
return "db";
|
|
1068
|
+
}
|
|
1069
|
+
return "markdown";
|
|
1070
|
+
}
|
|
1071
|
+
function createTasteStore(params) {
|
|
1072
|
+
const backend = normalizeStorageBackend(params.backend);
|
|
1073
|
+
const paths = {
|
|
1074
|
+
tasteFilePath: params.tasteFilePath,
|
|
1075
|
+
memoryFilePath: params.memoryFilePath,
|
|
1076
|
+
suggestionsFilePath: params.suggestionsFilePath
|
|
1077
|
+
};
|
|
1078
|
+
if (backend === "db") {
|
|
1079
|
+
return new DbTasteStore(paths);
|
|
1080
|
+
}
|
|
1081
|
+
return new MarkdownTasteStore(paths);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
670
1084
|
// src/cli/server/api.ts
|
|
671
1085
|
var api = new Hono();
|
|
672
1086
|
var resolveSafePath = (relativePath) => safeResolveFromCwd(relativePath);
|
|
1087
|
+
function createStoreFromBody(body) {
|
|
1088
|
+
return createTasteStore({
|
|
1089
|
+
backend: body.storage ? String(body.storage) : void 0,
|
|
1090
|
+
tasteFilePath: body.tasteFilePath ? String(body.tasteFilePath) : "taste/taste.md",
|
|
1091
|
+
memoryFilePath: body.memoryFilePath ? String(body.memoryFilePath) : "taste/memory.md",
|
|
1092
|
+
suggestionsFilePath: body.suggestionsFilePath ? String(body.suggestionsFilePath) : "taste/suggestions.md"
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
673
1095
|
api.get("/fs/list", async (c) => {
|
|
674
1096
|
const cwd = process.cwd();
|
|
675
1097
|
try {
|
|
@@ -678,7 +1100,6 @@ api.get("/fs/list", async (c) => {
|
|
|
678
1100
|
name: f.name,
|
|
679
1101
|
isDirectory: f.isDirectory(),
|
|
680
1102
|
size: 0
|
|
681
|
-
// Simplification for now
|
|
682
1103
|
}));
|
|
683
1104
|
return c.json({ path: cwd, files: result });
|
|
684
1105
|
} catch (e) {
|
|
@@ -686,35 +1107,35 @@ api.get("/fs/list", async (c) => {
|
|
|
686
1107
|
}
|
|
687
1108
|
});
|
|
688
1109
|
api.get("/fs/read", async (c) => {
|
|
689
|
-
const
|
|
690
|
-
if (!
|
|
1110
|
+
const path19 = c.req.query("path");
|
|
1111
|
+
if (!path19) return c.json({ error: "Path required" }, 400);
|
|
691
1112
|
try {
|
|
692
|
-
const fullPath = resolveSafePath(
|
|
1113
|
+
const fullPath = resolveSafePath(path19);
|
|
693
1114
|
const content = await readFile(fullPath, "utf-8");
|
|
694
|
-
if (
|
|
1115
|
+
if (path19.endsWith(".json")) {
|
|
695
1116
|
try {
|
|
696
1117
|
const json = JSON.parse(content);
|
|
697
1118
|
if (json.meta && Array.isArray(json.entities)) {
|
|
698
|
-
console.debug(`[API] Resolving scene: ${
|
|
1119
|
+
console.debug(`[API] Resolving scene: ${path19}`);
|
|
699
1120
|
const resolved = await SceneResolver.resolve(json, process.cwd());
|
|
700
1121
|
return c.json({ content: JSON.stringify(resolved, null, 2) });
|
|
701
1122
|
}
|
|
702
1123
|
} catch (e) {
|
|
703
|
-
console.error(`[API] Scene resolution failed for ${
|
|
1124
|
+
console.error(`[API] Scene resolution failed for ${path19}:`, e.message);
|
|
704
1125
|
}
|
|
705
1126
|
}
|
|
706
1127
|
return c.json({ content });
|
|
707
1128
|
} catch (e) {
|
|
708
|
-
console.error(`[API] Error reading file ${
|
|
1129
|
+
console.error(`[API] Error reading file ${path19}:`, e.message, e.stack);
|
|
709
1130
|
return c.json({ error: e.message }, 500);
|
|
710
1131
|
}
|
|
711
1132
|
});
|
|
712
1133
|
api.post("/fs/write", async (c) => {
|
|
713
1134
|
const body = await c.req.json();
|
|
714
|
-
const { path:
|
|
715
|
-
if (!
|
|
1135
|
+
const { path: path19, content } = body;
|
|
1136
|
+
if (!path19 || content === void 0) return c.json({ error: "Path and content required" }, 400);
|
|
716
1137
|
try {
|
|
717
|
-
const fullPath = resolveSafePath(
|
|
1138
|
+
const fullPath = resolveSafePath(path19);
|
|
718
1139
|
const dir = dirname(fullPath);
|
|
719
1140
|
if (!existsSync(dir)) {
|
|
720
1141
|
await mkdir(dir, { recursive: true });
|
|
@@ -728,7 +1149,7 @@ api.post("/fs/write", async (c) => {
|
|
|
728
1149
|
api.post("/fs/upload", async (c) => {
|
|
729
1150
|
try {
|
|
730
1151
|
const body = await c.req.parseBody();
|
|
731
|
-
const file = body
|
|
1152
|
+
const file = body.file;
|
|
732
1153
|
if (!file || !(file instanceof File)) {
|
|
733
1154
|
return c.json({ error: "File required" }, 400);
|
|
734
1155
|
}
|
|
@@ -736,7 +1157,7 @@ api.post("/fs/upload", async (c) => {
|
|
|
736
1157
|
if (!existsSync(assetsDir)) {
|
|
737
1158
|
await mkdir(assetsDir, { recursive: true });
|
|
738
1159
|
}
|
|
739
|
-
const fileName = body
|
|
1160
|
+
const fileName = body.name || file.name;
|
|
740
1161
|
const filePath = join(assetsDir, fileName);
|
|
741
1162
|
const bytes = Buffer.from(await file.arrayBuffer());
|
|
742
1163
|
await writeFile(filePath, bytes);
|
|
@@ -744,7 +1165,6 @@ api.post("/fs/upload", async (c) => {
|
|
|
744
1165
|
success: true,
|
|
745
1166
|
path: `assets/${fileName}`,
|
|
746
1167
|
url: `/api/fs/assets/assets/${fileName}`
|
|
747
|
-
// Adjust URL based on routing
|
|
748
1168
|
});
|
|
749
1169
|
} catch (e) {
|
|
750
1170
|
console.error(e);
|
|
@@ -752,10 +1172,10 @@ api.post("/fs/upload", async (c) => {
|
|
|
752
1172
|
}
|
|
753
1173
|
});
|
|
754
1174
|
api.get("/fs/assets/*", async (c) => {
|
|
755
|
-
const
|
|
1175
|
+
const reqPath = c.req.path;
|
|
756
1176
|
const marker = "/fs/assets/";
|
|
757
|
-
const index =
|
|
758
|
-
const relativePath =
|
|
1177
|
+
const index = reqPath.lastIndexOf(marker);
|
|
1178
|
+
const relativePath = reqPath.substring(index + marker.length);
|
|
759
1179
|
if (relativePath.includes("..")) {
|
|
760
1180
|
console.error(`[Asset Server] Blocked directory traversal attempt: ${relativePath}`);
|
|
761
1181
|
return c.json({ error: "Invalid path" }, 403);
|
|
@@ -773,22 +1193,21 @@ api.get("/fs/assets/*", async (c) => {
|
|
|
773
1193
|
return c.json({ error: "Invalid path" }, 403);
|
|
774
1194
|
}
|
|
775
1195
|
}
|
|
776
|
-
console.log(`[Asset Server] Request: ${c.req.path} -> Resolved: ${fullPath} (exists: ${existsSync(fullPath)})`);
|
|
777
1196
|
if (!existsSync(fullPath)) {
|
|
778
1197
|
console.error(`[Asset Server] File not found: ${fullPath}`);
|
|
779
1198
|
return c.notFound();
|
|
780
1199
|
}
|
|
781
1200
|
const ext = fullPath.split(".").pop()?.toLowerCase();
|
|
782
1201
|
const mimeTypes = {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1202
|
+
png: "image/png",
|
|
1203
|
+
jpg: "image/jpeg",
|
|
1204
|
+
jpeg: "image/jpeg",
|
|
1205
|
+
gif: "image/gif",
|
|
1206
|
+
webp: "image/webp",
|
|
1207
|
+
svg: "image/svg+xml",
|
|
1208
|
+
mp3: "audio/mpeg",
|
|
1209
|
+
mp4: "video/mp4",
|
|
1210
|
+
webm: "video/webm"
|
|
792
1211
|
};
|
|
793
1212
|
const mimeType = mimeTypes[ext || ""] || "application/octet-stream";
|
|
794
1213
|
try {
|
|
@@ -808,18 +1227,17 @@ api.post("/taste/chat", async (c) => {
|
|
|
808
1227
|
try {
|
|
809
1228
|
const body = await c.req.json();
|
|
810
1229
|
const message = String(body.message || "").trim();
|
|
811
|
-
const tasteFilePath = String(body.tasteFilePath || "taste/taste.md");
|
|
812
|
-
const memoryFilePath = String(body.memoryFilePath || "taste/memory.md");
|
|
813
1230
|
const model = body.model ? String(body.model) : void 0;
|
|
814
1231
|
const apiKey = body.apiKey || process.env.GEMINI_API_KEY;
|
|
815
1232
|
if (!message) return c.json({ error: "message is required" }, 400);
|
|
816
1233
|
if (!apiKey) return c.json({ error: "Missing GEMINI_API_KEY" }, 400);
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
const
|
|
1234
|
+
const store = createStoreFromBody(body);
|
|
1235
|
+
await store.ensureWorkspace();
|
|
1236
|
+
const taste = await store.readTaste();
|
|
1237
|
+
const memories = body.memoryContent ? parseTasteMemoryMarkdown(String(body.memoryContent)).slice(0, 40) : await store.queryMemory({ limit: 40 });
|
|
820
1238
|
const result = await runTasteChat({
|
|
821
1239
|
message,
|
|
822
|
-
tasteContent,
|
|
1240
|
+
tasteContent: body.tasteContent ? String(body.tasteContent) : taste.content,
|
|
823
1241
|
memories,
|
|
824
1242
|
apiKey: String(apiKey),
|
|
825
1243
|
model
|
|
@@ -832,20 +1250,18 @@ api.post("/taste/chat", async (c) => {
|
|
|
832
1250
|
api.post("/taste/simulate", async (c) => {
|
|
833
1251
|
try {
|
|
834
1252
|
const body = await c.req.json();
|
|
835
|
-
const tasteFilePath = String(body.tasteFilePath || "taste/taste.md");
|
|
836
|
-
const memoryFilePath = String(body.memoryFilePath || "taste/memory.md");
|
|
837
1253
|
const mode = body.mode === "parallel" ? "parallel" : "sequential";
|
|
838
1254
|
const count = Math.max(1, Number(body.count || 1));
|
|
839
1255
|
const saveToMemory = Boolean(body.saveToMemory);
|
|
840
1256
|
const model = body.model ? String(body.model) : void 0;
|
|
841
1257
|
const apiKey = body.apiKey || process.env.GEMINI_API_KEY;
|
|
842
1258
|
if (!apiKey) return c.json({ error: "Missing GEMINI_API_KEY" }, 400);
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
const memories = parseTasteMemoryMarkdown(memoryContent);
|
|
1259
|
+
const store = createStoreFromBody(body);
|
|
1260
|
+
await store.ensureWorkspace();
|
|
1261
|
+
const taste = await store.readTaste();
|
|
1262
|
+
const memories = body.memoryContent ? parseTasteMemoryMarkdown(String(body.memoryContent)).slice(0, 40) : await store.queryMemory({ limit: 40 });
|
|
847
1263
|
const result = await simulateIdeas({
|
|
848
|
-
tasteContent,
|
|
1264
|
+
tasteContent: body.tasteContent ? String(body.tasteContent) : taste.content,
|
|
849
1265
|
memories,
|
|
850
1266
|
count,
|
|
851
1267
|
mode,
|
|
@@ -854,27 +1270,114 @@ api.post("/taste/simulate", async (c) => {
|
|
|
854
1270
|
});
|
|
855
1271
|
let appendedIds = [];
|
|
856
1272
|
if (saveToMemory && result.ideas.length > 0) {
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
1273
|
+
for (const idea of result.ideas) {
|
|
1274
|
+
const saved = await store.appendMemory({ ...idea, status: "generated" });
|
|
1275
|
+
appendedIds.push(saved.id);
|
|
1276
|
+
}
|
|
860
1277
|
}
|
|
861
1278
|
return c.json({ ...result, appendedIds });
|
|
862
1279
|
} catch (e) {
|
|
863
1280
|
return c.json({ error: e.message }, 500);
|
|
864
1281
|
}
|
|
865
1282
|
});
|
|
866
|
-
api.post("/taste/
|
|
1283
|
+
api.post("/taste/feedback", async (c) => {
|
|
1284
|
+
try {
|
|
1285
|
+
const body = await c.req.json();
|
|
1286
|
+
const decision = String(body.decision || "").toLowerCase();
|
|
1287
|
+
if (decision !== "accepted" && decision !== "rejected") {
|
|
1288
|
+
return c.json({ error: "decision must be accepted or rejected" }, 400);
|
|
1289
|
+
}
|
|
1290
|
+
if (!body.title) {
|
|
1291
|
+
return c.json({ error: "title is required" }, 400);
|
|
1292
|
+
}
|
|
1293
|
+
const store = createStoreFromBody(body);
|
|
1294
|
+
await store.ensureWorkspace();
|
|
1295
|
+
const item = {
|
|
1296
|
+
id: body.ideaId ? String(body.ideaId) : void 0,
|
|
1297
|
+
title: String(body.title),
|
|
1298
|
+
format: body.format ? String(body.format) : "daily-reel",
|
|
1299
|
+
tags: Array.isArray(body.tags) ? body.tags.map((x) => String(x)) : [],
|
|
1300
|
+
freshnessTerms: [],
|
|
1301
|
+
summary: body.summary ? String(body.summary) : "",
|
|
1302
|
+
content: body.content ? String(body.content) : "",
|
|
1303
|
+
status: decision,
|
|
1304
|
+
reasonTags: Array.isArray(body.reasonTags) ? body.reasonTags.map((x) => String(x)) : [],
|
|
1305
|
+
notes: body.notes ? String(body.notes) : ""
|
|
1306
|
+
};
|
|
1307
|
+
const saved = await store.appendMemory(item);
|
|
1308
|
+
return c.json({ item: saved });
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
return c.json({ error: e.message }, 500);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
api.post("/taste/suggest", async (c) => {
|
|
867
1314
|
try {
|
|
868
1315
|
const body = await c.req.json();
|
|
869
|
-
const
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
const
|
|
875
|
-
await
|
|
876
|
-
|
|
877
|
-
|
|
1316
|
+
const memoryWindow = Math.max(1, Number(body.memoryWindow || 50));
|
|
1317
|
+
const store = createStoreFromBody(body);
|
|
1318
|
+
await store.ensureWorkspace();
|
|
1319
|
+
const taste = await store.readTaste();
|
|
1320
|
+
const memories = await store.queryMemory({ limit: memoryWindow });
|
|
1321
|
+
const draft = buildTasteSuggestionFromMemory(taste.content, memories);
|
|
1322
|
+
const suggestion = await store.appendSuggestion({
|
|
1323
|
+
title: draft.title,
|
|
1324
|
+
summary: draft.summary,
|
|
1325
|
+
rationale: draft.rationale,
|
|
1326
|
+
patch: draft.patch,
|
|
1327
|
+
proposedTasteContent: draft.proposedTasteContent,
|
|
1328
|
+
baseVersion: taste.version
|
|
1329
|
+
});
|
|
1330
|
+
return c.json({ suggestion });
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
return c.json({ error: e.message }, 500);
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
api.post("/taste/suggestions/list", async (c) => {
|
|
1336
|
+
try {
|
|
1337
|
+
const body = await c.req.json();
|
|
1338
|
+
const store = createStoreFromBody(body);
|
|
1339
|
+
await store.ensureWorkspace();
|
|
1340
|
+
const status = body.status ? String(body.status) : void 0;
|
|
1341
|
+
const suggestions = await store.listSuggestions(
|
|
1342
|
+
status === "pending" || status === "applied" || status === "rejected" ? status : void 0
|
|
1343
|
+
);
|
|
1344
|
+
return c.json({ suggestions });
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
return c.json({ error: e.message }, 500);
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
api.post("/taste/apply", async (c) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const body = await c.req.json();
|
|
1352
|
+
const suggestionId = String(body.suggestionId || "").trim();
|
|
1353
|
+
const rejectOnly = Boolean(body.reject);
|
|
1354
|
+
if (!suggestionId) {
|
|
1355
|
+
return c.json({ error: "suggestionId is required" }, 400);
|
|
1356
|
+
}
|
|
1357
|
+
const store = createStoreFromBody(body);
|
|
1358
|
+
await store.ensureWorkspace();
|
|
1359
|
+
const all = await store.listSuggestions();
|
|
1360
|
+
const target = all.find((entry) => entry.id === suggestionId);
|
|
1361
|
+
if (!target) {
|
|
1362
|
+
return c.json({ error: `Suggestion not found: ${suggestionId}` }, 404);
|
|
1363
|
+
}
|
|
1364
|
+
if (target.status !== "pending") {
|
|
1365
|
+
return c.json({ error: `Suggestion ${target.id} is already ${target.status}` }, 400);
|
|
1366
|
+
}
|
|
1367
|
+
if (rejectOnly) {
|
|
1368
|
+
const suggestion2 = await store.updateSuggestionStatus(target.id, "rejected", {
|
|
1369
|
+
reviewNote: body.note ? String(body.note) : void 0
|
|
1370
|
+
});
|
|
1371
|
+
return c.json({ suggestion: suggestion2 });
|
|
1372
|
+
}
|
|
1373
|
+
if (!target.proposedTasteContent.trim()) {
|
|
1374
|
+
return c.json({ error: "Suggestion is missing proposedTasteContent" }, 400);
|
|
1375
|
+
}
|
|
1376
|
+
await store.writeTaste(target.proposedTasteContent, target.baseVersion);
|
|
1377
|
+
const suggestion = await store.updateSuggestionStatus(target.id, "applied", {
|
|
1378
|
+
reviewNote: body.note ? String(body.note) : void 0
|
|
1379
|
+
});
|
|
1380
|
+
return c.json({ suggestion });
|
|
878
1381
|
} catch (e) {
|
|
879
1382
|
return c.json({ error: e.message }, 500);
|
|
880
1383
|
}
|
|
@@ -882,32 +1385,46 @@ api.post("/taste/memory/append", async (c) => {
|
|
|
882
1385
|
api.post("/taste/memory/query", async (c) => {
|
|
883
1386
|
try {
|
|
884
1387
|
const body = await c.req.json();
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
const items = parseTasteMemoryMarkdown(content);
|
|
889
|
-
const filtered = queryMemoryItems(items, {
|
|
1388
|
+
const store = createStoreFromBody(body);
|
|
1389
|
+
await store.ensureWorkspace();
|
|
1390
|
+
const items = await store.queryMemory({
|
|
890
1391
|
tag: body.tag ? String(body.tag) : void 0,
|
|
1392
|
+
status: body.status ? String(body.status) : void 0,
|
|
891
1393
|
since: body.since ? String(body.since) : void 0,
|
|
892
1394
|
text: body.text ? String(body.text) : void 0,
|
|
893
1395
|
limit: body.limit ? Number(body.limit) : void 0
|
|
894
1396
|
});
|
|
895
|
-
return c.json({ items
|
|
1397
|
+
return c.json({ items });
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
return c.json({ error: e.message }, 500);
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
api.post("/cli/run", async (c) => {
|
|
1403
|
+
try {
|
|
1404
|
+
const body = await c.req.json();
|
|
1405
|
+
const args = Array.isArray(body.args) ? body.args : [];
|
|
1406
|
+
if (!args.length) {
|
|
1407
|
+
return c.json({ error: "args array is required" }, 400);
|
|
1408
|
+
}
|
|
1409
|
+
const cwd = body.cwd ? resolveSafePath(String(body.cwd)) : process.cwd();
|
|
1410
|
+
const result = await runCliCommand(args, cwd);
|
|
1411
|
+
return c.json(result);
|
|
896
1412
|
} catch (e) {
|
|
1413
|
+
console.error("[API] /cli/run error:", e.message);
|
|
897
1414
|
return c.json({ error: e.message }, 500);
|
|
898
1415
|
}
|
|
899
1416
|
});
|
|
900
1417
|
var api_default = api;
|
|
901
1418
|
|
|
902
1419
|
// src/cli/server/index.ts
|
|
903
|
-
import
|
|
904
|
-
import { fileURLToPath } from "url";
|
|
905
|
-
import
|
|
1420
|
+
import path5 from "path";
|
|
1421
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1422
|
+
import fs4 from "fs";
|
|
906
1423
|
var app = new Hono2();
|
|
907
1424
|
app.use("/*", cors());
|
|
908
1425
|
app.route("/api", api_default);
|
|
909
|
-
var
|
|
910
|
-
var
|
|
1426
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
1427
|
+
var __dirname2 = path5.dirname(__filename2);
|
|
911
1428
|
var createServer = (staticRoot) => {
|
|
912
1429
|
const app2 = new Hono2();
|
|
913
1430
|
app2.use("/*", cors());
|
|
@@ -938,10 +1455,10 @@ var createServer = (staticRoot) => {
|
|
|
938
1455
|
};
|
|
939
1456
|
app2.get("/*", async (c) => {
|
|
940
1457
|
const requestPath = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
941
|
-
const filePath =
|
|
1458
|
+
const filePath = path5.join(staticRoot, requestPath);
|
|
942
1459
|
try {
|
|
943
|
-
if (
|
|
944
|
-
const file = await
|
|
1460
|
+
if (fs4.existsSync(filePath)) {
|
|
1461
|
+
const file = await fs4.promises.readFile(filePath);
|
|
945
1462
|
const mimeType = getMimeType(filePath);
|
|
946
1463
|
return new Response(file, {
|
|
947
1464
|
headers: {
|
|
@@ -953,9 +1470,9 @@ var createServer = (staticRoot) => {
|
|
|
953
1470
|
}
|
|
954
1471
|
if (!requestPath.includes(".")) {
|
|
955
1472
|
try {
|
|
956
|
-
const indexPath =
|
|
957
|
-
if (
|
|
958
|
-
const indexFile = await
|
|
1473
|
+
const indexPath = path5.join(staticRoot, "index.html");
|
|
1474
|
+
if (fs4.existsSync(indexPath)) {
|
|
1475
|
+
const indexFile = await fs4.promises.readFile(indexPath);
|
|
959
1476
|
return new Response(indexFile, {
|
|
960
1477
|
headers: {
|
|
961
1478
|
"Content-Type": "text/html"
|
|
@@ -983,8 +1500,8 @@ function startServer(app2, port) {
|
|
|
983
1500
|
}
|
|
984
1501
|
|
|
985
1502
|
// src/cli/commands/record.ts
|
|
986
|
-
var
|
|
987
|
-
var
|
|
1503
|
+
var __filename3 = fileURLToPath3(import.meta.url);
|
|
1504
|
+
var __dirname3 = path6.dirname(__filename3);
|
|
988
1505
|
function formatBytes(bytes) {
|
|
989
1506
|
if (bytes === 0) return "0 B";
|
|
990
1507
|
const k = 1024;
|
|
@@ -993,21 +1510,21 @@ function formatBytes(bytes) {
|
|
|
993
1510
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
994
1511
|
}
|
|
995
1512
|
function getMissingUiAssets(staticRoot) {
|
|
996
|
-
const indexPath =
|
|
997
|
-
if (!
|
|
1513
|
+
const indexPath = path6.join(staticRoot, "index.html");
|
|
1514
|
+
if (!fs5.existsSync(indexPath)) {
|
|
998
1515
|
return {
|
|
999
1516
|
missingFiles: [indexPath],
|
|
1000
1517
|
expectedUrls: []
|
|
1001
1518
|
};
|
|
1002
1519
|
}
|
|
1003
|
-
const indexHtml =
|
|
1520
|
+
const indexHtml = fs5.readFileSync(indexPath, "utf-8");
|
|
1004
1521
|
const assetMatches = Array.from(indexHtml.matchAll(/(?:src|href)=["'](\/assets\/[^"']+)["']/g));
|
|
1005
1522
|
const assetPaths = [...new Set(assetMatches.map((match) => match[1]))];
|
|
1006
1523
|
const missingFiles = [];
|
|
1007
1524
|
const expectedUrls = [];
|
|
1008
1525
|
for (const assetPath of assetPaths) {
|
|
1009
|
-
const localPath =
|
|
1010
|
-
if (!
|
|
1526
|
+
const localPath = path6.join(staticRoot, assetPath.replace(/^\//, ""));
|
|
1527
|
+
if (!fs5.existsSync(localPath)) {
|
|
1011
1528
|
missingFiles.push(localPath);
|
|
1012
1529
|
expectedUrls.push(assetPath);
|
|
1013
1530
|
}
|
|
@@ -1097,17 +1614,17 @@ async function ensurePlaywrightReadyOrExit() {
|
|
|
1097
1614
|
}
|
|
1098
1615
|
var recordCommand = new Command("record").description("Record the project to a video file with audio").option("-o, --output <path>", "Output file path", "output.mp4").option("-u, --url <url>", "URL of the running server", "http://localhost:3331").option("-w, --width <number>", "Viewport width", "1920").option("-h, --height <number>", "Viewport height", "1080").option("-f, --fps <number>", "Frames per second", "30").option("--project <path>", "Path to project file", "scene_1.json").option("--dry-run", "Validate scene and show estimates without rendering").option("--debug", "Enable verbose logging (FFmpeg, browser console)").action(async (options) => {
|
|
1099
1616
|
const { output, url, width, height, fps, project: projectOption, debug, dryRun } = options;
|
|
1100
|
-
const projectPath =
|
|
1101
|
-
if (!
|
|
1617
|
+
const projectPath = path6.resolve(process.cwd(), projectOption);
|
|
1618
|
+
if (!fs5.existsSync(projectPath)) {
|
|
1102
1619
|
console.error(`Project file not found at ${projectPath}`);
|
|
1103
1620
|
process.exit(1);
|
|
1104
1621
|
}
|
|
1105
|
-
const projectDir =
|
|
1622
|
+
const projectDir = path6.dirname(projectPath);
|
|
1106
1623
|
process.chdir(projectDir);
|
|
1107
1624
|
console.log(`Working in project directory: ${projectDir}`);
|
|
1108
1625
|
let project;
|
|
1109
1626
|
try {
|
|
1110
|
-
const rawProject = JSON.parse(
|
|
1627
|
+
const rawProject = JSON.parse(fs5.readFileSync(projectPath, "utf-8"));
|
|
1111
1628
|
const resolvedCwd = process.cwd();
|
|
1112
1629
|
project = await SceneResolver.resolve(rawProject, resolvedCwd);
|
|
1113
1630
|
} catch (e) {
|
|
@@ -1128,9 +1645,9 @@ var recordCommand = new Command("record").description("Record the project to a v
|
|
|
1128
1645
|
if (entity.src.startsWith("/")) {
|
|
1129
1646
|
assetPath = entity.src;
|
|
1130
1647
|
} else {
|
|
1131
|
-
assetPath =
|
|
1648
|
+
assetPath = path6.resolve(process.cwd(), entity.src);
|
|
1132
1649
|
}
|
|
1133
|
-
if (!
|
|
1650
|
+
if (!fs5.existsSync(assetPath)) {
|
|
1134
1651
|
missingAssets.push(entity.src);
|
|
1135
1652
|
}
|
|
1136
1653
|
});
|
|
@@ -1154,10 +1671,10 @@ var recordCommand = new Command("record").description("Record the project to a v
|
|
|
1154
1671
|
Assets(${allAssets.length}):`);
|
|
1155
1672
|
allAssets.forEach((entity) => {
|
|
1156
1673
|
if (!entity.src) return;
|
|
1157
|
-
let assetPath =
|
|
1158
|
-
if (!
|
|
1159
|
-
if (
|
|
1160
|
-
const stats =
|
|
1674
|
+
let assetPath = path6.resolve(process.cwd(), "assets", entity.src);
|
|
1675
|
+
if (!fs5.existsSync(assetPath)) assetPath = path6.resolve(process.cwd(), entity.src);
|
|
1676
|
+
if (fs5.existsSync(assetPath)) {
|
|
1677
|
+
const stats = fs5.statSync(assetPath);
|
|
1161
1678
|
const sizeStr = formatBytes(stats.size);
|
|
1162
1679
|
console.log(` \u2713 ${entity.src} (${sizeStr})`);
|
|
1163
1680
|
}
|
|
@@ -1191,11 +1708,11 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1191
1708
|
ensureFfmpegAvailableOrExit();
|
|
1192
1709
|
await ensurePlaywrightReadyOrExit();
|
|
1193
1710
|
console.log("Starting temporary server...");
|
|
1194
|
-
let staticRoot =
|
|
1195
|
-
if (!
|
|
1196
|
-
staticRoot =
|
|
1711
|
+
let staticRoot = path6.resolve(__dirname3, "../../dist/ui");
|
|
1712
|
+
if (!fs5.existsSync(staticRoot)) {
|
|
1713
|
+
staticRoot = path6.resolve(__dirname3, "../ui");
|
|
1197
1714
|
}
|
|
1198
|
-
if (!
|
|
1715
|
+
if (!fs5.existsSync(staticRoot)) {
|
|
1199
1716
|
console.error(`Error: UI assets not found at ${staticRoot}. Please run 'bun run build' first.`);
|
|
1200
1717
|
process.exit(1);
|
|
1201
1718
|
}
|
|
@@ -1251,14 +1768,14 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1251
1768
|
let assetPath = "";
|
|
1252
1769
|
if (e.src.startsWith("/")) {
|
|
1253
1770
|
if (e.src.startsWith("/api/fs/assets/")) {
|
|
1254
|
-
assetPath =
|
|
1771
|
+
assetPath = path6.resolve(process.cwd(), "assets", e.src.replace("/api/fs/assets/", ""));
|
|
1255
1772
|
} else {
|
|
1256
1773
|
assetPath = e.src;
|
|
1257
1774
|
}
|
|
1258
1775
|
} else {
|
|
1259
|
-
assetPath =
|
|
1776
|
+
assetPath = path6.resolve(process.cwd(), e.src);
|
|
1260
1777
|
}
|
|
1261
|
-
if (
|
|
1778
|
+
if (fs5.existsSync(assetPath)) {
|
|
1262
1779
|
e.src = assetPath;
|
|
1263
1780
|
return true;
|
|
1264
1781
|
} else {
|
|
@@ -1312,7 +1829,7 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1312
1829
|
if (debug) {
|
|
1313
1830
|
console.log("FFmpeg args:", ffmpegArgs.join(" "));
|
|
1314
1831
|
}
|
|
1315
|
-
const ffmpeg =
|
|
1832
|
+
const ffmpeg = spawn4("ffmpeg", ffmpegArgs);
|
|
1316
1833
|
ffmpeg.stderr.on("data", (data) => {
|
|
1317
1834
|
if (debug) {
|
|
1318
1835
|
console.error(`FFmpeg: ${data.toString()}`);
|
|
@@ -1368,7 +1885,7 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1368
1885
|
try {
|
|
1369
1886
|
for (let i = 0; i < totalFrames; i++) {
|
|
1370
1887
|
const time = i / fpsNum;
|
|
1371
|
-
const projectRelativePath =
|
|
1888
|
+
const projectRelativePath = path6.relative(process.cwd(), projectPath);
|
|
1372
1889
|
const targetUrl = `${url}?mode=render&time=${time}&project=${encodeURIComponent(projectRelativePath)}`;
|
|
1373
1890
|
await page.goto(targetUrl, { waitUntil: "load" });
|
|
1374
1891
|
await page.waitForSelector('canvas[data-ready="true"]', { timeout: 3e4 });
|
|
@@ -1406,12 +1923,12 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1406
1923
|
|
|
1407
1924
|
// src/cli/commands/inspect.ts
|
|
1408
1925
|
import { Command as Command2 } from "commander";
|
|
1409
|
-
import
|
|
1410
|
-
import
|
|
1926
|
+
import fs6 from "fs";
|
|
1927
|
+
import path7 from "path";
|
|
1411
1928
|
var inspectCommand = new Command2("inspect");
|
|
1412
1929
|
inspectCommand.description("Inspect an asset file to get metadata (duration, format, etc.)").argument("<file>", "Path to the asset file").option("--json", "Output in JSON format").action(async (file, options) => {
|
|
1413
|
-
const filePath =
|
|
1414
|
-
if (!
|
|
1930
|
+
const filePath = path7.resolve(process.cwd(), file);
|
|
1931
|
+
if (!fs6.existsSync(filePath)) {
|
|
1415
1932
|
console.error(`File not found: ${filePath}`);
|
|
1416
1933
|
process.exit(1);
|
|
1417
1934
|
}
|
|
@@ -1440,8 +1957,8 @@ inspectCommand.description("Inspect an asset file to get metadata (duration, for
|
|
|
1440
1957
|
|
|
1441
1958
|
// src/cli/commands/validate.ts
|
|
1442
1959
|
import { Command as Command3 } from "commander";
|
|
1443
|
-
import
|
|
1444
|
-
import
|
|
1960
|
+
import fs7 from "fs";
|
|
1961
|
+
import path8 from "path";
|
|
1445
1962
|
var validateCommand = new Command3("validate");
|
|
1446
1963
|
function resolveRuntimeFit(fit, imageRatio, canvasRatio) {
|
|
1447
1964
|
if (fit === "cover" || fit === "contain") return fit;
|
|
@@ -1449,13 +1966,13 @@ function resolveRuntimeFit(fit, imageRatio, canvasRatio) {
|
|
|
1449
1966
|
return ratioDelta > 0.2 ? "contain" : "cover";
|
|
1450
1967
|
}
|
|
1451
1968
|
validateCommand.description("Validate a scene file").argument("<file>", "Path to the scene file").action(async (file) => {
|
|
1452
|
-
const filePath =
|
|
1453
|
-
if (!
|
|
1969
|
+
const filePath = path8.resolve(process.cwd(), file);
|
|
1970
|
+
if (!fs7.existsSync(filePath)) {
|
|
1454
1971
|
console.error(`File not found: ${filePath}`);
|
|
1455
1972
|
process.exit(1);
|
|
1456
1973
|
}
|
|
1457
1974
|
try {
|
|
1458
|
-
const rawScene = JSON.parse(
|
|
1975
|
+
const rawScene = JSON.parse(fs7.readFileSync(filePath, "utf-8"));
|
|
1459
1976
|
const data = await SceneResolver.resolve(rawScene, process.cwd());
|
|
1460
1977
|
const errors = [];
|
|
1461
1978
|
const warnings = [];
|
|
@@ -1495,13 +2012,13 @@ validateCommand.description("Validate a scene file").argument("<file>", "Path to
|
|
|
1495
2012
|
let fullPath = "";
|
|
1496
2013
|
if (entity.src.startsWith("/api/fs/assets/")) {
|
|
1497
2014
|
const stripped = entity.src.replace("/api/fs/assets/", "");
|
|
1498
|
-
fullPath =
|
|
2015
|
+
fullPath = path8.resolve(process.cwd(), stripped);
|
|
1499
2016
|
} else if (entity.src.startsWith("/")) {
|
|
1500
2017
|
fullPath = entity.src;
|
|
1501
2018
|
} else {
|
|
1502
|
-
fullPath =
|
|
2019
|
+
fullPath = path8.resolve(process.cwd(), entity.src);
|
|
1503
2020
|
}
|
|
1504
|
-
if (!
|
|
2021
|
+
if (!fs7.existsSync(fullPath)) {
|
|
1505
2022
|
errors.push(`${prefix} asset not found at ${fullPath} (derived from src: "${entity.src}")`);
|
|
1506
2023
|
} else {
|
|
1507
2024
|
try {
|
|
@@ -1535,13 +2052,13 @@ validateCommand.description("Validate a scene file").argument("<file>", "Path to
|
|
|
1535
2052
|
let fullPath = "";
|
|
1536
2053
|
if (entity.src.startsWith("/api/fs/assets/")) {
|
|
1537
2054
|
const stripped = entity.src.replace("/api/fs/assets/", "");
|
|
1538
|
-
fullPath =
|
|
2055
|
+
fullPath = path8.resolve(process.cwd(), stripped);
|
|
1539
2056
|
} else if (entity.src.startsWith("/")) {
|
|
1540
2057
|
fullPath = entity.src;
|
|
1541
2058
|
} else {
|
|
1542
|
-
fullPath =
|
|
2059
|
+
fullPath = path8.resolve(process.cwd(), entity.src);
|
|
1543
2060
|
}
|
|
1544
|
-
if (!
|
|
2061
|
+
if (!fs7.existsSync(fullPath)) {
|
|
1545
2062
|
errors.push(`${prefix} asset not found at ${fullPath} (derived from src: "${entity.src}")`);
|
|
1546
2063
|
}
|
|
1547
2064
|
}
|
|
@@ -1573,21 +2090,21 @@ validateCommand.description("Validate a scene file").argument("<file>", "Path to
|
|
|
1573
2090
|
|
|
1574
2091
|
// src/cli/commands/set-scene.ts
|
|
1575
2092
|
import { Command as Command4 } from "commander";
|
|
1576
|
-
import
|
|
1577
|
-
import
|
|
2093
|
+
import fs8 from "fs";
|
|
2094
|
+
import path9 from "path";
|
|
1578
2095
|
var setSceneCommand = new Command4("set-scene");
|
|
1579
2096
|
setSceneCommand.description("Update or create a scene file with new content").argument("<file>", "Path to the scene file to update").option("-c, --content <json>", "JSON content string").option("-i, --input <file>", "Input JSON file to copy from").action(async (file, options) => {
|
|
1580
|
-
const targetPath =
|
|
2097
|
+
const targetPath = path9.resolve(process.cwd(), file);
|
|
1581
2098
|
let contentStr = "";
|
|
1582
2099
|
if (options.content) {
|
|
1583
2100
|
contentStr = options.content;
|
|
1584
2101
|
} else if (options.input) {
|
|
1585
|
-
const inputPath =
|
|
1586
|
-
if (!
|
|
2102
|
+
const inputPath = path9.resolve(process.cwd(), options.input);
|
|
2103
|
+
if (!fs8.existsSync(inputPath)) {
|
|
1587
2104
|
console.error(`Input file not found: ${inputPath}`);
|
|
1588
2105
|
process.exit(1);
|
|
1589
2106
|
}
|
|
1590
|
-
contentStr =
|
|
2107
|
+
contentStr = fs8.readFileSync(inputPath, "utf-8");
|
|
1591
2108
|
} else {
|
|
1592
2109
|
console.error("Error: Please provide --content <json> or --input <file>");
|
|
1593
2110
|
process.exit(1);
|
|
@@ -1598,7 +2115,7 @@ setSceneCommand.description("Update or create a scene file with new content").ar
|
|
|
1598
2115
|
console.error('Error: Invalid scene format. Missing "meta" or "entities".');
|
|
1599
2116
|
process.exit(1);
|
|
1600
2117
|
}
|
|
1601
|
-
|
|
2118
|
+
fs8.writeFileSync(targetPath, JSON.stringify(data, null, 2));
|
|
1602
2119
|
console.log(`Scene updated at ${targetPath}`);
|
|
1603
2120
|
} catch (e) {
|
|
1604
2121
|
console.error("Error: Content is not valid JSON.", e.message);
|
|
@@ -1608,10 +2125,10 @@ setSceneCommand.description("Update or create a scene file with new content").ar
|
|
|
1608
2125
|
|
|
1609
2126
|
// src/cli/commands/imagine.ts
|
|
1610
2127
|
import { Command as Command5 } from "commander";
|
|
1611
|
-
import
|
|
1612
|
-
import
|
|
2128
|
+
import fs9 from "fs";
|
|
2129
|
+
import path10 from "path";
|
|
1613
2130
|
var imagineCommand = new Command5("imagine");
|
|
1614
|
-
imagineCommand.description("Generate images using Google Gemini AI (Imagen)").argument("<prompt-or-file>", "Text prompt or path to a file containing the prompt").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "generated.png").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("-n, --number <count>", "Number of images to generate", "1").option("--aspect-ratio <ratio>", "Aspect ratio (e.g., 9:16, 1:1, 16:9, or auto)", "auto").option("--image-size <size>", "Image size hint: 1K, 2K, or 4K", "2K").option("--project <path>", "Scene file path to infer aspect ratio from (reads meta.width/meta.height)", "scene_1.json").addHelpText("after", `
|
|
2131
|
+
imagineCommand.description("Generate images using Google Gemini AI (Imagen)").argument("<prompt-or-file>", "Text prompt or path to a file containing the prompt").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "generated.png").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("-n, --number <count>", "Number of images to generate", "1").option("--aspect-ratio <ratio>", "Aspect ratio (e.g., 9:16, 1:1, 16:9, or auto)", "auto").option("--image-size <size>", "Image size hint: 1K, 2K, or 4K", "2K").option("--project <path>", "Scene file path to infer aspect ratio from (reads meta.width/meta.height)", "scene_1.json").option("--json", "Emit machine-readable JSON output").addHelpText("after", `
|
|
1615
2132
|
|
|
1616
2133
|
Examples:
|
|
1617
2134
|
# Basic usage - saves to assets/generated.png
|
|
@@ -1659,18 +2176,26 @@ Note:
|
|
|
1659
2176
|
`).action(async (promptOrFile, options) => {
|
|
1660
2177
|
try {
|
|
1661
2178
|
let prompt;
|
|
1662
|
-
if (
|
|
2179
|
+
if (fs9.existsSync(promptOrFile)) {
|
|
1663
2180
|
console.log(`Reading prompt from file: ${promptOrFile}`);
|
|
1664
|
-
prompt =
|
|
2181
|
+
prompt = fs9.readFileSync(promptOrFile, "utf-8").trim();
|
|
1665
2182
|
} else {
|
|
1666
2183
|
prompt = promptOrFile;
|
|
1667
2184
|
}
|
|
1668
2185
|
if (!prompt) {
|
|
2186
|
+
if (options.json) {
|
|
2187
|
+
console.log(JSON.stringify({ error: "Prompt cannot be empty" }));
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
1669
2190
|
console.error("Error: Prompt cannot be empty");
|
|
1670
2191
|
process.exit(1);
|
|
1671
2192
|
}
|
|
1672
2193
|
const apiKey = options.apiKey || process.env.GEMINI_API_KEY;
|
|
1673
2194
|
if (!apiKey) {
|
|
2195
|
+
if (options.json) {
|
|
2196
|
+
console.log(JSON.stringify({ error: "Missing Gemini API key" }));
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
1674
2199
|
console.error("Error: Missing Gemini API key");
|
|
1675
2200
|
console.error("Please provide via:");
|
|
1676
2201
|
console.error(' 1. -k flag: feedeas imagine "prompt" -k YOUR_API_KEY');
|
|
@@ -1681,10 +2206,12 @@ Note:
|
|
|
1681
2206
|
const numberOfImages = Math.max(1, parseInt(options.number) || 1);
|
|
1682
2207
|
const imageSize = normalizeImageSize(options.imageSize);
|
|
1683
2208
|
const aspectRatio = resolveAspectRatio(options.aspectRatio, options.project);
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
2209
|
+
if (!options.json) {
|
|
2210
|
+
console.log("\u{1F3A8} Generating image with Gemini AI (Imagen)...");
|
|
2211
|
+
console.log(`\u{1F4DD} Prompt: "${prompt.substring(0, 80)}${prompt.length > 80 ? "..." : ""}"`);
|
|
2212
|
+
console.log(`\u{1F9ED} Aspect ratio: ${aspectRatio}`);
|
|
2213
|
+
console.log(`\u{1F4D0} Image size: ${imageSize}`);
|
|
2214
|
+
}
|
|
1688
2215
|
const images = await generateImages({
|
|
1689
2216
|
prompt,
|
|
1690
2217
|
numberOfImages,
|
|
@@ -1693,30 +2220,45 @@ Note:
|
|
|
1693
2220
|
imageSize
|
|
1694
2221
|
});
|
|
1695
2222
|
let outputPath;
|
|
1696
|
-
if (
|
|
2223
|
+
if (path10.isAbsolute(options.output)) {
|
|
1697
2224
|
outputPath = options.output;
|
|
1698
2225
|
} else {
|
|
1699
|
-
const assetsDir =
|
|
1700
|
-
if (!
|
|
2226
|
+
const assetsDir = path10.resolve(process.cwd(), "assets");
|
|
2227
|
+
if (!fs9.existsSync(assetsDir)) {
|
|
1701
2228
|
console.log(`\u{1F4C1} Creating assets directory: ${assetsDir}`);
|
|
1702
|
-
|
|
2229
|
+
fs9.mkdirSync(assetsDir, { recursive: true });
|
|
1703
2230
|
}
|
|
1704
|
-
outputPath =
|
|
2231
|
+
outputPath = path10.join(assetsDir, options.output);
|
|
1705
2232
|
}
|
|
1706
|
-
const outputDir =
|
|
1707
|
-
const outputExt =
|
|
1708
|
-
const outputBase =
|
|
1709
|
-
if (!
|
|
1710
|
-
|
|
2233
|
+
const outputDir = path10.dirname(outputPath);
|
|
2234
|
+
const outputExt = path10.extname(outputPath) || ".png";
|
|
2235
|
+
const outputBase = path10.basename(outputPath, outputExt);
|
|
2236
|
+
if (!fs9.existsSync(outputDir)) {
|
|
2237
|
+
fs9.mkdirSync(outputDir, { recursive: true });
|
|
1711
2238
|
}
|
|
2239
|
+
const savedFiles = [];
|
|
1712
2240
|
for (let i = 0; i < images.length; i++) {
|
|
1713
|
-
const filename = images.length > 1 ?
|
|
1714
|
-
|
|
1715
|
-
console.log(`\u2705 Image saved: ${filename}`);
|
|
2241
|
+
const filename = images.length > 1 ? path10.join(outputDir, `${outputBase}_${i + 1}${outputExt}`) : outputPath;
|
|
2242
|
+
fs9.writeFileSync(filename, images[i]);
|
|
2243
|
+
if (!options.json) console.log(`\u2705 Image saved: ${filename}`);
|
|
2244
|
+
savedFiles.push(filename);
|
|
2245
|
+
}
|
|
2246
|
+
if (options.json) {
|
|
2247
|
+
console.log(JSON.stringify({
|
|
2248
|
+
success: true,
|
|
2249
|
+
files: savedFiles,
|
|
2250
|
+
count: images.length,
|
|
2251
|
+
prompt: prompt.substring(0, 80)
|
|
2252
|
+
}, null, 2));
|
|
2253
|
+
return;
|
|
1716
2254
|
}
|
|
1717
2255
|
console.log(`
|
|
1718
2256
|
\u2728 Successfully generated ${images.length} image(s)!`);
|
|
1719
2257
|
} catch (error) {
|
|
2258
|
+
if (options.json) {
|
|
2259
|
+
console.log(JSON.stringify({ error: error.message || String(error) }));
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
1720
2262
|
console.error("\u274C Error generating image:", error.message);
|
|
1721
2263
|
if (error.cause) {
|
|
1722
2264
|
console.error("Details:", error.cause);
|
|
@@ -1805,10 +2347,10 @@ function resolveAspectRatio(input, projectFile) {
|
|
|
1805
2347
|
return normalizeAspectRatio(input);
|
|
1806
2348
|
}
|
|
1807
2349
|
function inferAspectRatioFromProject(projectFile) {
|
|
1808
|
-
const filePath =
|
|
1809
|
-
if (!
|
|
2350
|
+
const filePath = path10.resolve(process.cwd(), projectFile);
|
|
2351
|
+
if (!fs9.existsSync(filePath)) return null;
|
|
1810
2352
|
try {
|
|
1811
|
-
const scene = JSON.parse(
|
|
2353
|
+
const scene = JSON.parse(fs9.readFileSync(filePath, "utf-8"));
|
|
1812
2354
|
const width = scene?.meta?.width;
|
|
1813
2355
|
const height = scene?.meta?.height;
|
|
1814
2356
|
if (typeof width !== "number" || typeof height !== "number" || width <= 0 || height <= 0) {
|
|
@@ -1848,51 +2390,51 @@ function gcd(a, b) {
|
|
|
1848
2390
|
|
|
1849
2391
|
// src/cli/commands/audio.ts
|
|
1850
2392
|
import { Command as Command6 } from "commander";
|
|
1851
|
-
import
|
|
1852
|
-
import
|
|
2393
|
+
import fs11 from "fs";
|
|
2394
|
+
import path12 from "path";
|
|
1853
2395
|
|
|
1854
2396
|
// src/cli/services/whisper.ts
|
|
1855
|
-
import
|
|
1856
|
-
import
|
|
2397
|
+
import fs10 from "fs";
|
|
2398
|
+
import path11 from "path";
|
|
1857
2399
|
import os from "os";
|
|
1858
|
-
import { spawn as
|
|
2400
|
+
import { spawn as spawn5 } from "child_process";
|
|
1859
2401
|
import https from "https";
|
|
1860
2402
|
var WhisperService = class {
|
|
1861
2403
|
static getHomeDir() {
|
|
1862
2404
|
return os.homedir();
|
|
1863
2405
|
}
|
|
1864
2406
|
static getBaseDir() {
|
|
1865
|
-
return
|
|
2407
|
+
return path11.join(this.getHomeDir(), ".feedeas");
|
|
1866
2408
|
}
|
|
1867
2409
|
static getBinDir() {
|
|
1868
|
-
return
|
|
2410
|
+
return path11.join(this.getBaseDir(), "bin");
|
|
1869
2411
|
}
|
|
1870
2412
|
static getModelsDir() {
|
|
1871
|
-
return
|
|
2413
|
+
return path11.join(this.getBaseDir(), "models");
|
|
1872
2414
|
}
|
|
1873
2415
|
static getExecutablePath() {
|
|
1874
|
-
const repoDir =
|
|
2416
|
+
const repoDir = path11.join(this.getBaseDir(), "whisper.cpp-repo");
|
|
1875
2417
|
const possiblePaths = [
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2418
|
+
path11.join(repoDir, "build", "bin", "whisper-cli"),
|
|
2419
|
+
path11.join(repoDir, "build", "bin", "main"),
|
|
2420
|
+
path11.join(repoDir, "main"),
|
|
2421
|
+
path11.join(this.getBinDir(), "whisper-main")
|
|
1880
2422
|
// fallback to copied/downloaded
|
|
1881
2423
|
];
|
|
1882
2424
|
for (const p of possiblePaths) {
|
|
1883
|
-
if (
|
|
2425
|
+
if (fs10.existsSync(p)) return p;
|
|
1884
2426
|
}
|
|
1885
|
-
return
|
|
2427
|
+
return path11.join(this.getBinDir(), "whisper-main");
|
|
1886
2428
|
}
|
|
1887
2429
|
static getModelPath(modelName = "base.en") {
|
|
1888
|
-
return
|
|
2430
|
+
return path11.join(this.getModelsDir(), `ggml-${modelName}.bin`);
|
|
1889
2431
|
}
|
|
1890
2432
|
/**
|
|
1891
2433
|
* Download a file from a URL to a destination path
|
|
1892
2434
|
*/
|
|
1893
2435
|
static async downloadFile(url, destPath) {
|
|
1894
2436
|
return new Promise((resolve, reject) => {
|
|
1895
|
-
const file =
|
|
2437
|
+
const file = fs10.createWriteStream(destPath);
|
|
1896
2438
|
https.get(url, (response) => {
|
|
1897
2439
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
1898
2440
|
this.downloadFile(response.headers.location, destPath).then(resolve).catch(reject);
|
|
@@ -1908,7 +2450,7 @@ var WhisperService = class {
|
|
|
1908
2450
|
resolve();
|
|
1909
2451
|
});
|
|
1910
2452
|
}).on("error", (err) => {
|
|
1911
|
-
|
|
2453
|
+
fs10.unlink(destPath, () => {
|
|
1912
2454
|
});
|
|
1913
2455
|
reject(err);
|
|
1914
2456
|
});
|
|
@@ -1920,15 +2462,15 @@ var WhisperService = class {
|
|
|
1920
2462
|
static async ensureReady() {
|
|
1921
2463
|
const binDir = this.getBinDir();
|
|
1922
2464
|
const modelsDir = this.getModelsDir();
|
|
1923
|
-
if (!
|
|
1924
|
-
if (!
|
|
2465
|
+
if (!fs10.existsSync(binDir)) fs10.mkdirSync(binDir, { recursive: true });
|
|
2466
|
+
if (!fs10.existsSync(modelsDir)) fs10.mkdirSync(modelsDir, { recursive: true });
|
|
1925
2467
|
const execPath = this.getExecutablePath();
|
|
1926
2468
|
const modelPath = this.getModelPath();
|
|
1927
|
-
if (!
|
|
2469
|
+
if (!fs10.existsSync(execPath)) {
|
|
1928
2470
|
console.log("\u2B07\uFE0F Whisper binary not found. Attempting to build...");
|
|
1929
2471
|
try {
|
|
1930
|
-
const repoDir =
|
|
1931
|
-
if (!
|
|
2472
|
+
const repoDir = path11.join(this.getBaseDir(), "whisper.cpp-repo");
|
|
2473
|
+
if (!fs10.existsSync(repoDir)) {
|
|
1932
2474
|
console.log("\u{1F4E6} Cloning whisper.cpp...");
|
|
1933
2475
|
await runCommand("git", ["clone", "https://github.com/ggerganov/whisper.cpp.git", repoDir]);
|
|
1934
2476
|
} else {
|
|
@@ -1936,7 +2478,7 @@ var WhisperService = class {
|
|
|
1936
2478
|
console.log("\u{1F528} Building whisper.cpp (this may take a minute)...");
|
|
1937
2479
|
await runCommand("make", [], repoDir);
|
|
1938
2480
|
const newPath = this.getExecutablePath();
|
|
1939
|
-
if (
|
|
2481
|
+
if (fs10.existsSync(newPath)) {
|
|
1940
2482
|
console.log(`\u2705 Whisper binary built at: ${newPath}`);
|
|
1941
2483
|
} else {
|
|
1942
2484
|
throw new Error("Build command finished but binary not found in expected paths.");
|
|
@@ -1947,7 +2489,7 @@ var WhisperService = class {
|
|
|
1947
2489
|
Please install manually: 'brew install whisper-cpp'`);
|
|
1948
2490
|
}
|
|
1949
2491
|
}
|
|
1950
|
-
if (!
|
|
2492
|
+
if (!fs10.existsSync(modelPath)) {
|
|
1951
2493
|
console.log("\u2B07\uFE0F Downloading Whisper model (base.en)...");
|
|
1952
2494
|
const modelUrl = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin";
|
|
1953
2495
|
await this.downloadFile(modelUrl, modelPath);
|
|
@@ -1960,12 +2502,12 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
1960
2502
|
static async transcribe(audioPath) {
|
|
1961
2503
|
const execPath = this.getExecutablePath();
|
|
1962
2504
|
const modelPath = this.getModelPath();
|
|
1963
|
-
const ext =
|
|
2505
|
+
const ext = path11.extname(audioPath).toLowerCase();
|
|
1964
2506
|
let inputToWhisper = audioPath;
|
|
1965
2507
|
let isTempFile = false;
|
|
1966
2508
|
if (ext !== ".wav") {
|
|
1967
2509
|
console.log("\u{1F504} Converting audio to 16kHz WAV for Whisper...");
|
|
1968
|
-
const tempWav =
|
|
2510
|
+
const tempWav = path11.join(path11.dirname(audioPath), `temp_${Date.now()}.wav`);
|
|
1969
2511
|
await runCommand("ffmpeg", [
|
|
1970
2512
|
"-i",
|
|
1971
2513
|
audioPath,
|
|
@@ -1982,7 +2524,7 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
1982
2524
|
isTempFile = true;
|
|
1983
2525
|
}
|
|
1984
2526
|
try {
|
|
1985
|
-
const outputBase =
|
|
2527
|
+
const outputBase = path11.join(path11.dirname(audioPath), path11.basename(audioPath, path11.extname(audioPath)));
|
|
1986
2528
|
const baseArgs = [
|
|
1987
2529
|
"-m",
|
|
1988
2530
|
modelPath,
|
|
@@ -2019,15 +2561,15 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
2019
2561
|
words
|
|
2020
2562
|
};
|
|
2021
2563
|
} finally {
|
|
2022
|
-
if (isTempFile &&
|
|
2023
|
-
|
|
2564
|
+
if (isTempFile && fs10.existsSync(inputToWhisper)) {
|
|
2565
|
+
fs10.unlinkSync(inputToWhisper);
|
|
2024
2566
|
}
|
|
2025
2567
|
}
|
|
2026
2568
|
}
|
|
2027
2569
|
};
|
|
2028
2570
|
function runCommand(command, args, cwd) {
|
|
2029
2571
|
return new Promise((resolve, reject) => {
|
|
2030
|
-
const proc =
|
|
2572
|
+
const proc = spawn5(command, args, { cwd, stdio: "inherit" });
|
|
2031
2573
|
proc.on("close", (code) => {
|
|
2032
2574
|
if (code === 0) resolve();
|
|
2033
2575
|
else reject(new Error(`${command} exited with code ${code}`));
|
|
@@ -2037,7 +2579,7 @@ function runCommand(command, args, cwd) {
|
|
|
2037
2579
|
}
|
|
2038
2580
|
function runWhisper(execPath, args) {
|
|
2039
2581
|
return new Promise((resolve, reject) => {
|
|
2040
|
-
const proc =
|
|
2582
|
+
const proc = spawn5(execPath, args, { stdio: "inherit" });
|
|
2041
2583
|
proc.on("close", (code) => {
|
|
2042
2584
|
if (code !== 0) {
|
|
2043
2585
|
reject(new Error(`Whisper process exited with code ${code}`));
|
|
@@ -2046,12 +2588,12 @@ function runWhisper(execPath, args) {
|
|
|
2046
2588
|
const outputFileIndex = args.indexOf("-of");
|
|
2047
2589
|
const outputBase = outputFileIndex >= 0 ? args[outputFileIndex + 1] : void 0;
|
|
2048
2590
|
const jsonPath = outputBase ? `${outputBase}.json` : "";
|
|
2049
|
-
if (!jsonPath || !
|
|
2591
|
+
if (!jsonPath || !fs10.existsSync(jsonPath)) {
|
|
2050
2592
|
reject(new Error("Whisper output JSON not found"));
|
|
2051
2593
|
return;
|
|
2052
2594
|
}
|
|
2053
2595
|
try {
|
|
2054
|
-
const data = JSON.parse(
|
|
2596
|
+
const data = JSON.parse(fs10.readFileSync(jsonPath, "utf-8"));
|
|
2055
2597
|
resolve(data);
|
|
2056
2598
|
} catch (err) {
|
|
2057
2599
|
reject(err);
|
|
@@ -2104,9 +2646,9 @@ var audioCommand = new Command6("generate:audio");
|
|
|
2104
2646
|
audioCommand.alias("audio").description("Generate audio from text using Gemini API and extract metadata").argument("<text-or-file>", "Input text or path to text file").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "speech.mp3").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("--voice <name>", "Voice name (optional)").option("--no-transcribe", "Skip Whisper transcription/metadata generation").action(async (textOrFile, options) => {
|
|
2105
2647
|
try {
|
|
2106
2648
|
let text;
|
|
2107
|
-
if (
|
|
2649
|
+
if (fs11.existsSync(textOrFile)) {
|
|
2108
2650
|
console.log(`\u{1F4D6} Reading text from file: ${textOrFile}`);
|
|
2109
|
-
text =
|
|
2651
|
+
text = fs11.readFileSync(textOrFile, "utf-8").trim();
|
|
2110
2652
|
} else {
|
|
2111
2653
|
text = textOrFile;
|
|
2112
2654
|
}
|
|
@@ -2122,10 +2664,10 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
2122
2664
|
console.log("\u{1F5E3}\uFE0F Generating speech with Gemini...");
|
|
2123
2665
|
const audioBuffer = await generateGeminiAudio(text, apiKey, options.voice);
|
|
2124
2666
|
const outputPath = resolveOutputPath(options.output);
|
|
2125
|
-
const tempPcmPath =
|
|
2126
|
-
|
|
2667
|
+
const tempPcmPath = path12.join(path12.dirname(outputPath), `temp_${Date.now()}.pcm`);
|
|
2668
|
+
fs11.writeFileSync(tempPcmPath, audioBuffer);
|
|
2127
2669
|
try {
|
|
2128
|
-
console.log(`\u{1F504} Converting raw PCM to ${
|
|
2670
|
+
console.log(`\u{1F504} Converting raw PCM to ${path12.extname(outputPath)}...`);
|
|
2129
2671
|
await new Promise((resolve, reject) => {
|
|
2130
2672
|
const ffmpeg = __require("child_process").spawn("ffmpeg", [
|
|
2131
2673
|
"-f",
|
|
@@ -2147,15 +2689,15 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
2147
2689
|
});
|
|
2148
2690
|
console.log(`\u2705 Audio saved: ${outputPath}`);
|
|
2149
2691
|
} finally {
|
|
2150
|
-
if (
|
|
2692
|
+
if (fs11.existsSync(tempPcmPath)) fs11.unlinkSync(tempPcmPath);
|
|
2151
2693
|
}
|
|
2152
2694
|
if (options.transcribe) {
|
|
2153
2695
|
try {
|
|
2154
2696
|
console.log("\u{1F50D} Aligning audio with Whisper...");
|
|
2155
2697
|
await WhisperService.ensureReady();
|
|
2156
2698
|
const metadata = await WhisperService.transcribe(outputPath);
|
|
2157
|
-
const metaPath = outputPath.replace(
|
|
2158
|
-
|
|
2699
|
+
const metaPath = outputPath.replace(path12.extname(outputPath), ".json");
|
|
2700
|
+
fs11.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
2159
2701
|
console.log(`\u2705 Metadata saved: ${metaPath}`);
|
|
2160
2702
|
} catch (wErr) {
|
|
2161
2703
|
console.warn("\u26A0\uFE0F Whisper alignment failed:", wErr.message);
|
|
@@ -2167,10 +2709,10 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
2167
2709
|
}
|
|
2168
2710
|
});
|
|
2169
2711
|
function resolveOutputPath(outputOption) {
|
|
2170
|
-
if (
|
|
2171
|
-
const assetsDir =
|
|
2172
|
-
if (!
|
|
2173
|
-
return
|
|
2712
|
+
if (path12.isAbsolute(outputOption)) return outputOption;
|
|
2713
|
+
const assetsDir = path12.resolve(process.cwd(), "assets");
|
|
2714
|
+
if (!fs11.existsSync(assetsDir)) fs11.mkdirSync(assetsDir, { recursive: true });
|
|
2715
|
+
return path12.join(assetsDir, outputOption);
|
|
2174
2716
|
}
|
|
2175
2717
|
async function generateGeminiAudio(text, apiKey, voiceName) {
|
|
2176
2718
|
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent`;
|
|
@@ -2211,11 +2753,11 @@ async function generateGeminiAudio(text, apiKey, voiceName) {
|
|
|
2211
2753
|
|
|
2212
2754
|
// src/cli/commands/bgm.ts
|
|
2213
2755
|
import { Command as Command7 } from "commander";
|
|
2214
|
-
import
|
|
2215
|
-
import
|
|
2216
|
-
import { spawn as
|
|
2756
|
+
import fs12 from "fs";
|
|
2757
|
+
import path13 from "path";
|
|
2758
|
+
import { spawn as spawn6 } from "child_process";
|
|
2217
2759
|
var bgmCommand = new Command7("generate:bgm");
|
|
2218
|
-
bgmCommand.alias("bgm").alias("music").description("Generate background music with Gemini Lyria (Live Music API)").argument("<prompt-or-file>", "Music prompt text or path to a text file").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "bgm.mp3").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("-d, --duration <seconds>", "Target duration in seconds (5-300)", "30").option("--seed <number>", "Optional seed for reproducible output").action(async (promptOrFile, options) => {
|
|
2760
|
+
bgmCommand.alias("bgm").alias("music").description("Generate background music with Gemini Lyria (Live Music API)").argument("<prompt-or-file>", "Music prompt text or path to a text file").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "bgm.mp3").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("-d, --duration <seconds>", "Target duration in seconds (5-300)", "30").option("--seed <number>", "Optional seed for reproducible output").option("--json", "Emit machine-readable JSON output").action(async (promptOrFile, options) => {
|
|
2219
2761
|
try {
|
|
2220
2762
|
const prompt = resolvePrompt(promptOrFile);
|
|
2221
2763
|
const apiKey = options.apiKey || process.env.GEMINI_API_KEY;
|
|
@@ -2235,22 +2777,36 @@ bgmCommand.alias("bgm").alias("music").description("Generate background music wi
|
|
|
2235
2777
|
apiKey,
|
|
2236
2778
|
seed
|
|
2237
2779
|
};
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2780
|
+
if (!options.json) {
|
|
2781
|
+
console.log("\u{1F3B5} Generating BGM with Gemini Lyria...");
|
|
2782
|
+
console.log(`\u{1F4DD} Prompt: "${request.prompt.substring(0, 100)}${request.prompt.length > 100 ? "..." : ""}"`);
|
|
2783
|
+
console.log(`\u23F1\uFE0F Duration: ${request.durationSec}s`);
|
|
2784
|
+
if (typeof request.seed === "number") {
|
|
2785
|
+
console.log(`\u{1F331} Seed: ${request.seed}`);
|
|
2786
|
+
}
|
|
2243
2787
|
}
|
|
2244
2788
|
const pcmBuffer = await generateGeminiMusicPcm(request);
|
|
2245
2789
|
await convertPcmToOutput(pcmBuffer, request.outputPath, request.format, request.durationSec);
|
|
2790
|
+
if (options.json) {
|
|
2791
|
+
console.log(JSON.stringify({
|
|
2792
|
+
success: true,
|
|
2793
|
+
file: request.outputPath,
|
|
2794
|
+
durationSec: request.durationSec
|
|
2795
|
+
}, null, 2));
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2246
2798
|
console.log(`\u2705 BGM saved: ${request.outputPath}`);
|
|
2247
2799
|
} catch (error) {
|
|
2800
|
+
if (options.json) {
|
|
2801
|
+
console.log(JSON.stringify({ error: error?.message || String(error) }));
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2248
2804
|
console.error("\u274C Error generating BGM:", error?.message || error);
|
|
2249
2805
|
process.exit(1);
|
|
2250
2806
|
}
|
|
2251
2807
|
});
|
|
2252
2808
|
function resolvePrompt(promptOrFile) {
|
|
2253
|
-
const prompt =
|
|
2809
|
+
const prompt = fs12.existsSync(promptOrFile) ? fs12.readFileSync(promptOrFile, "utf-8").trim() : String(promptOrFile).trim();
|
|
2254
2810
|
if (!prompt) {
|
|
2255
2811
|
throw new Error("Prompt cannot be empty");
|
|
2256
2812
|
}
|
|
@@ -2272,13 +2828,13 @@ function parseSeed(input) {
|
|
|
2272
2828
|
return value;
|
|
2273
2829
|
}
|
|
2274
2830
|
function resolveOutputPath2(outputOption) {
|
|
2275
|
-
if (
|
|
2276
|
-
const assetsDir =
|
|
2277
|
-
if (!
|
|
2278
|
-
return
|
|
2831
|
+
if (path13.isAbsolute(outputOption)) return outputOption;
|
|
2832
|
+
const assetsDir = path13.resolve(process.cwd(), "assets");
|
|
2833
|
+
if (!fs12.existsSync(assetsDir)) fs12.mkdirSync(assetsDir, { recursive: true });
|
|
2834
|
+
return path13.join(assetsDir, outputOption);
|
|
2279
2835
|
}
|
|
2280
2836
|
function inferOutputFormat(outputPath) {
|
|
2281
|
-
const ext =
|
|
2837
|
+
const ext = path13.extname(outputPath).toLowerCase();
|
|
2282
2838
|
if (ext === ".wav") return "wav";
|
|
2283
2839
|
if (ext === ".mp3" || !ext) return "mp3";
|
|
2284
2840
|
throw new Error("Invalid output format. Use .mp3 or .wav.");
|
|
@@ -2366,14 +2922,14 @@ async function generateGeminiMusicPcm(request) {
|
|
|
2366
2922
|
});
|
|
2367
2923
|
}
|
|
2368
2924
|
async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
|
|
2369
|
-
const outputDir =
|
|
2370
|
-
if (!
|
|
2371
|
-
const tempPcmPath =
|
|
2372
|
-
|
|
2925
|
+
const outputDir = path13.dirname(outputPath);
|
|
2926
|
+
if (!fs12.existsSync(outputDir)) fs12.mkdirSync(outputDir, { recursive: true });
|
|
2927
|
+
const tempPcmPath = path13.join(outputDir, `temp_bgm_${Date.now()}.pcm`);
|
|
2928
|
+
fs12.writeFileSync(tempPcmPath, pcmBuffer);
|
|
2373
2929
|
const codecArgs = format === "wav" ? ["-c:a", "pcm_s16le"] : ["-c:a", "libmp3lame", "-q:a", "2"];
|
|
2374
2930
|
try {
|
|
2375
2931
|
await new Promise((resolve, reject) => {
|
|
2376
|
-
const ffmpeg =
|
|
2932
|
+
const ffmpeg = spawn6("ffmpeg", [
|
|
2377
2933
|
"-f",
|
|
2378
2934
|
"s16le",
|
|
2379
2935
|
"-ar",
|
|
@@ -2395,25 +2951,25 @@ async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
|
|
|
2395
2951
|
ffmpeg.on("error", reject);
|
|
2396
2952
|
});
|
|
2397
2953
|
} finally {
|
|
2398
|
-
if (
|
|
2954
|
+
if (fs12.existsSync(tempPcmPath)) fs12.unlinkSync(tempPcmPath);
|
|
2399
2955
|
}
|
|
2400
2956
|
}
|
|
2401
2957
|
|
|
2402
2958
|
// src/cli/commands/asset.ts
|
|
2403
2959
|
import { Command as Command8 } from "commander";
|
|
2404
|
-
import
|
|
2405
|
-
import
|
|
2406
|
-
import { spawn as
|
|
2960
|
+
import fs13 from "fs";
|
|
2961
|
+
import path14 from "path";
|
|
2962
|
+
import { spawn as spawn7 } from "child_process";
|
|
2407
2963
|
var assetCommand = new Command8("asset").description("Asset information and management");
|
|
2408
2964
|
assetCommand.command("info <file>").description("Show detailed information about an asset").action(async (file) => {
|
|
2409
|
-
const assetPath =
|
|
2410
|
-
if (!
|
|
2965
|
+
const assetPath = path14.resolve(process.cwd(), "assets", file);
|
|
2966
|
+
if (!fs13.existsSync(assetPath)) {
|
|
2411
2967
|
console.error(`Asset not found: ${file}`);
|
|
2412
2968
|
console.error(`Looked in: ${assetPath}`);
|
|
2413
2969
|
process.exit(1);
|
|
2414
2970
|
}
|
|
2415
|
-
const stats =
|
|
2416
|
-
const ext =
|
|
2971
|
+
const stats = fs13.statSync(assetPath);
|
|
2972
|
+
const ext = path14.extname(file).toLowerCase();
|
|
2417
2973
|
console.log(`
|
|
2418
2974
|
Asset: ${file}`);
|
|
2419
2975
|
console.log(`Size: ${formatBytes2(stats.size)}`);
|
|
@@ -2436,11 +2992,11 @@ Asset: ${file}`);
|
|
|
2436
2992
|
console.log(`Channels: ${audioInfo.channels}`);
|
|
2437
2993
|
console.log(`Bitrate: ${audioInfo.bitrate}`);
|
|
2438
2994
|
const metadataFile = file.replace(ext, ".json");
|
|
2439
|
-
const metadataPath =
|
|
2440
|
-
if (
|
|
2995
|
+
const metadataPath = path14.resolve(process.cwd(), "assets", metadataFile);
|
|
2996
|
+
if (fs13.existsSync(metadataPath)) {
|
|
2441
2997
|
console.log(`
|
|
2442
2998
|
Metadata: ${metadataFile}`);
|
|
2443
|
-
const metadata = JSON.parse(
|
|
2999
|
+
const metadata = JSON.parse(fs13.readFileSync(metadataPath, "utf-8"));
|
|
2444
3000
|
if (Array.isArray(metadata.words) && metadata.words.length > 0) {
|
|
2445
3001
|
console.log(`Word timings: ${metadata.words.length} words`);
|
|
2446
3002
|
const firstWord = metadata.words[0];
|
|
@@ -2485,7 +3041,7 @@ async function getImageDimensions(filePath) {
|
|
|
2485
3041
|
}
|
|
2486
3042
|
async function getAudioInfo(filePath) {
|
|
2487
3043
|
return new Promise((resolve, reject) => {
|
|
2488
|
-
const ffprobe =
|
|
3044
|
+
const ffprobe = spawn7("ffprobe", [
|
|
2489
3045
|
"-v",
|
|
2490
3046
|
"error",
|
|
2491
3047
|
"-show_entries",
|
|
@@ -2524,9 +3080,9 @@ async function getAudioInfo(filePath) {
|
|
|
2524
3080
|
|
|
2525
3081
|
// src/cli/index.ts
|
|
2526
3082
|
import open2 from "open";
|
|
2527
|
-
import
|
|
2528
|
-
import { fileURLToPath as
|
|
2529
|
-
import
|
|
3083
|
+
import path18 from "path";
|
|
3084
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3085
|
+
import fs17 from "fs";
|
|
2530
3086
|
|
|
2531
3087
|
// src/cli/commands/example.ts
|
|
2532
3088
|
import { Command as Command9 } from "commander";
|
|
@@ -2819,14 +3375,14 @@ function printSchema(name, schema) {
|
|
|
2819
3375
|
|
|
2820
3376
|
// src/cli/commands/create-scene.ts
|
|
2821
3377
|
import { Command as Command11 } from "commander";
|
|
2822
|
-
import
|
|
3378
|
+
import path15 from "path";
|
|
2823
3379
|
|
|
2824
3380
|
// src/cli/services/scene-builder.ts
|
|
2825
|
-
import
|
|
3381
|
+
import fs14 from "fs";
|
|
2826
3382
|
function loadScene(filePath) {
|
|
2827
|
-
if (
|
|
3383
|
+
if (fs14.existsSync(filePath)) {
|
|
2828
3384
|
try {
|
|
2829
|
-
return JSON.parse(
|
|
3385
|
+
return JSON.parse(fs14.readFileSync(filePath, "utf-8"));
|
|
2830
3386
|
} catch (e) {
|
|
2831
3387
|
throw new Error(`Failed to parse existing scene file: ${e}`);
|
|
2832
3388
|
}
|
|
@@ -2842,7 +3398,7 @@ function loadScene(filePath) {
|
|
|
2842
3398
|
}
|
|
2843
3399
|
}
|
|
2844
3400
|
function saveScene(filePath, scene) {
|
|
2845
|
-
|
|
3401
|
+
fs14.writeFileSync(filePath, JSON.stringify(scene, null, 2));
|
|
2846
3402
|
}
|
|
2847
3403
|
function addEntityToScene(scene, entity) {
|
|
2848
3404
|
if (!entity.id) {
|
|
@@ -2862,10 +3418,10 @@ function addEntityToScene(scene, entity) {
|
|
|
2862
3418
|
}
|
|
2863
3419
|
|
|
2864
3420
|
// src/cli/commands/create-scene.ts
|
|
2865
|
-
import
|
|
3421
|
+
import fs15 from "fs";
|
|
2866
3422
|
var createSceneCommand = new Command11("create-scene");
|
|
2867
3423
|
createSceneCommand.description("Create and modify scene files").argument("<file>", "Path to the scene file").action(async (file) => {
|
|
2868
|
-
const filePath =
|
|
3424
|
+
const filePath = path15.resolve(process.cwd(), file);
|
|
2869
3425
|
try {
|
|
2870
3426
|
const scene = loadScene(filePath);
|
|
2871
3427
|
saveScene(filePath, scene);
|
|
@@ -2877,8 +3433,8 @@ createSceneCommand.description("Create and modify scene files").argument("<file>
|
|
|
2877
3433
|
});
|
|
2878
3434
|
var addEntityCommand = new Command11("add-entity");
|
|
2879
3435
|
addEntityCommand.description("Add an entity to an existing scene file").argument("<file>", "Path to the scene file").requiredOption("-t, --type <type>", "Entity type (image, audio, text)").option("--src <path>", "Asset path (relative to CWD or absolute)").option("--text <content>", "Text content (for text)").option("--fit <mode>", "Image fit mode: smart | contain | cover", "smart").option("--start <number>", "Start time in seconds").option("--duration <string>", 'Duration in seconds or "auto"', "5").option("--at <position>", 'Position ("end" to append after last entity)', "end").action(async (file, options) => {
|
|
2880
|
-
const filePath =
|
|
2881
|
-
if (!
|
|
3436
|
+
const filePath = path15.resolve(process.cwd(), file);
|
|
3437
|
+
if (!fs15.existsSync(filePath)) {
|
|
2882
3438
|
console.error(`\u274C Scene file not found: ${filePath}`);
|
|
2883
3439
|
process.exit(1);
|
|
2884
3440
|
}
|
|
@@ -2909,7 +3465,7 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
2909
3465
|
startTime = 0;
|
|
2910
3466
|
}
|
|
2911
3467
|
if (options.duration === "auto" && options.src) {
|
|
2912
|
-
const assetPath =
|
|
3468
|
+
const assetPath = path15.resolve(process.cwd(), options.src);
|
|
2913
3469
|
try {
|
|
2914
3470
|
const metadata = await FFprobeService.getMetadata(assetPath);
|
|
2915
3471
|
duration = metadata.duration;
|
|
@@ -2956,19 +3512,19 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
2956
3512
|
|
|
2957
3513
|
// src/cli/services/telemetry.ts
|
|
2958
3514
|
import os2 from "os";
|
|
2959
|
-
import
|
|
2960
|
-
import { createHash } from "crypto";
|
|
3515
|
+
import path16 from "path";
|
|
3516
|
+
import { createHash as createHash2 } from "crypto";
|
|
2961
3517
|
var POSTHOG_CAPTURE_URL = "https://us.i.posthog.com/capture/";
|
|
2962
3518
|
var TELEMETRY_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
2963
3519
|
var BUILT_POSTHOG_KEY = String(
|
|
2964
|
-
""
|
|
3520
|
+
"phc_w3BnJp2sDXvSpIsMkofnwDSObboBd6DXhNetfZuyWVO"
|
|
2965
3521
|
).trim();
|
|
2966
3522
|
var PostHogTelemetryService = class {
|
|
2967
3523
|
trackFeedback(details) {
|
|
2968
3524
|
if (!this.enabled) return;
|
|
2969
3525
|
this.capture("cli_feedback", {
|
|
2970
3526
|
details: String(details).slice(0, 2e3),
|
|
2971
|
-
cwd_basename:
|
|
3527
|
+
cwd_basename: path16.basename(process.cwd())
|
|
2972
3528
|
});
|
|
2973
3529
|
}
|
|
2974
3530
|
apiKey;
|
|
@@ -2991,7 +3547,7 @@ var PostHogTelemetryService = class {
|
|
|
2991
3547
|
command_name: this.getCommandPath(actionCommand),
|
|
2992
3548
|
command_aliases: actionCommand.aliases(),
|
|
2993
3549
|
options_used: this.getUsedOptionNames(actionCommand),
|
|
2994
|
-
cwd_basename:
|
|
3550
|
+
cwd_basename: path16.basename(process.cwd()),
|
|
2995
3551
|
node_version: process.version,
|
|
2996
3552
|
platform: process.platform,
|
|
2997
3553
|
arch: process.arch
|
|
@@ -3004,7 +3560,7 @@ var PostHogTelemetryService = class {
|
|
|
3004
3560
|
status,
|
|
3005
3561
|
duration_ms: Math.max(0, Math.round(durationMs)),
|
|
3006
3562
|
error_message: errorMessage ? String(errorMessage).slice(0, 300) : void 0,
|
|
3007
|
-
cwd_basename:
|
|
3563
|
+
cwd_basename: path16.basename(process.cwd())
|
|
3008
3564
|
});
|
|
3009
3565
|
}
|
|
3010
3566
|
getCommandPath(command) {
|
|
@@ -3027,7 +3583,7 @@ var PostHogTelemetryService = class {
|
|
|
3027
3583
|
os2.hostname(),
|
|
3028
3584
|
os2.userInfo().username
|
|
3029
3585
|
].join("|");
|
|
3030
|
-
return
|
|
3586
|
+
return createHash2("sha256").update(seed).digest("hex");
|
|
3031
3587
|
}
|
|
3032
3588
|
capture(event, properties) {
|
|
3033
3589
|
if (!this.apiKey) return;
|
|
@@ -3057,26 +3613,26 @@ var PostHogTelemetryService = class {
|
|
|
3057
3613
|
|
|
3058
3614
|
// src/cli/commands/taste.ts
|
|
3059
3615
|
import { Command as Command12 } from "commander";
|
|
3060
|
-
import
|
|
3061
|
-
import
|
|
3062
|
-
import { fileURLToPath as
|
|
3616
|
+
import fs16 from "node:fs";
|
|
3617
|
+
import path17 from "node:path";
|
|
3618
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3063
3619
|
import open from "open";
|
|
3064
|
-
var
|
|
3065
|
-
var
|
|
3620
|
+
var __filename4 = fileURLToPath4(import.meta.url);
|
|
3621
|
+
var __dirname4 = path17.dirname(__filename4);
|
|
3066
3622
|
function resolveStaticRoot() {
|
|
3067
|
-
let staticRoot =
|
|
3068
|
-
if (!
|
|
3069
|
-
staticRoot =
|
|
3623
|
+
let staticRoot = path17.resolve(__dirname4, "../../../dist/ui");
|
|
3624
|
+
if (!fs16.existsSync(staticRoot)) {
|
|
3625
|
+
staticRoot = path17.resolve(__dirname4, "../../ui");
|
|
3070
3626
|
}
|
|
3071
3627
|
return staticRoot;
|
|
3072
3628
|
}
|
|
3073
3629
|
function prepareWorkingDirectory(pathArg) {
|
|
3074
3630
|
if (!pathArg) return;
|
|
3075
|
-
const targetPath =
|
|
3076
|
-
if (
|
|
3077
|
-
const stats =
|
|
3631
|
+
const targetPath = path17.resolve(process.cwd(), pathArg);
|
|
3632
|
+
if (fs16.existsSync(targetPath)) {
|
|
3633
|
+
const stats = fs16.statSync(targetPath);
|
|
3078
3634
|
if (stats.isFile()) {
|
|
3079
|
-
process.chdir(
|
|
3635
|
+
process.chdir(path17.dirname(targetPath));
|
|
3080
3636
|
return;
|
|
3081
3637
|
}
|
|
3082
3638
|
if (stats.isDirectory()) {
|
|
@@ -3084,20 +3640,33 @@ function prepareWorkingDirectory(pathArg) {
|
|
|
3084
3640
|
return;
|
|
3085
3641
|
}
|
|
3086
3642
|
}
|
|
3087
|
-
if (
|
|
3088
|
-
const dir =
|
|
3089
|
-
|
|
3643
|
+
if (path17.extname(pathArg)) {
|
|
3644
|
+
const dir = path17.dirname(targetPath);
|
|
3645
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
3090
3646
|
process.chdir(dir);
|
|
3091
3647
|
return;
|
|
3092
3648
|
}
|
|
3093
|
-
|
|
3649
|
+
fs16.mkdirSync(targetPath, { recursive: true });
|
|
3094
3650
|
process.chdir(targetPath);
|
|
3095
3651
|
}
|
|
3096
|
-
|
|
3652
|
+
function createStoreFromOptions(options) {
|
|
3653
|
+
return createTasteStore({
|
|
3654
|
+
backend: options.storage,
|
|
3655
|
+
tasteFilePath: options.tasteFile || "taste/taste.md",
|
|
3656
|
+
memoryFilePath: options.memoryFile || "taste/memory.md",
|
|
3657
|
+
suggestionsFilePath: options.suggestionsFile || "taste/suggestions.md"
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
function parseCsvList(value) {
|
|
3661
|
+
if (!value) return [];
|
|
3662
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
3663
|
+
}
|
|
3664
|
+
var tasteCommand = new Command12("taste").description("Launch and automate the Taste workspace").argument("[path]", "Optional workspace directory").option("-p, --port <number>", "Port to run on", "3331").option("--no-open", "Do not open browser").option("--storage <backend>", "Storage backend: markdown|db", "markdown").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("--suggestions-file <path>", "Suggestions file path", "taste/suggestions.md").action(async (pathArg, options) => {
|
|
3097
3665
|
prepareWorkingDirectory(pathArg);
|
|
3098
|
-
|
|
3666
|
+
const store = createStoreFromOptions(options);
|
|
3667
|
+
await store.ensureWorkspace();
|
|
3099
3668
|
const staticRoot = resolveStaticRoot();
|
|
3100
|
-
if (!
|
|
3669
|
+
if (!fs16.existsSync(staticRoot)) {
|
|
3101
3670
|
console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
|
|
3102
3671
|
}
|
|
3103
3672
|
const port = parseInt(options.port, 10);
|
|
@@ -3113,21 +3682,21 @@ var tasteCommand = new Command12("taste").description("Launch and automate the T
|
|
|
3113
3682
|
await open(url);
|
|
3114
3683
|
}
|
|
3115
3684
|
});
|
|
3116
|
-
tasteCommand.command("generate").description("Generate fresh ideas from taste and memory files").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("-n, --count <number>", "How many ideas to generate", "1").option("--mode <mode>", "Generation mode: sequential|parallel", "sequential").option("--no-save", "Do not append
|
|
3685
|
+
tasteCommand.command("generate").description("Generate fresh ideas from taste and memory files").option("--storage <backend>", "Storage backend: markdown|db").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("--suggestions-file <path>", "Suggestions file path", "taste/suggestions.md").option("-n, --count <number>", "How many ideas to generate", "1").option("--memory-window <number>", "Recent memory window for generation context", "40").option("--mode <mode>", "Generation mode: sequential|parallel", "sequential").option("--no-save", "Do not append generated ideas into memory.md").option("--json", "Emit machine-readable JSON output").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").action(async (options) => {
|
|
3117
3686
|
try {
|
|
3118
|
-
|
|
3687
|
+
const store = createStoreFromOptions(options);
|
|
3688
|
+
await store.ensureWorkspace();
|
|
3119
3689
|
const apiKey = options.apiKey || process.env.GEMINI_API_KEY;
|
|
3120
3690
|
if (!apiKey) {
|
|
3121
3691
|
throw new Error("Missing Gemini API key. Set GEMINI_API_KEY or pass --api-key.");
|
|
3122
3692
|
}
|
|
3123
|
-
const
|
|
3124
|
-
const
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
const memories = parseTasteMemoryMarkdown(memoryContent);
|
|
3693
|
+
const taste = await store.readTaste();
|
|
3694
|
+
const memories = await store.queryMemory({
|
|
3695
|
+
limit: Math.max(1, parseInt(options.memoryWindow, 10) || 40)
|
|
3696
|
+
});
|
|
3128
3697
|
const mode = options.mode === "parallel" ? "parallel" : "sequential";
|
|
3129
3698
|
const result = await simulateIdeas({
|
|
3130
|
-
tasteContent,
|
|
3699
|
+
tasteContent: taste.content,
|
|
3131
3700
|
memories,
|
|
3132
3701
|
count: Math.max(1, parseInt(options.count, 10) || 1),
|
|
3133
3702
|
mode,
|
|
@@ -3135,9 +3704,10 @@ tasteCommand.command("generate").description("Generate fresh ideas from taste an
|
|
|
3135
3704
|
});
|
|
3136
3705
|
let appendedIds = [];
|
|
3137
3706
|
if (options.save && result.ideas.length > 0) {
|
|
3138
|
-
const
|
|
3139
|
-
|
|
3140
|
-
|
|
3707
|
+
for (const idea of result.ideas) {
|
|
3708
|
+
const saved = await store.appendMemory({ ...idea, status: "generated" });
|
|
3709
|
+
appendedIds.push(saved.id);
|
|
3710
|
+
}
|
|
3141
3711
|
}
|
|
3142
3712
|
const output = {
|
|
3143
3713
|
...result,
|
|
@@ -3163,13 +3733,124 @@ tasteCommand.command("generate").description("Generate fresh ideas from taste an
|
|
|
3163
3733
|
process.exit(1);
|
|
3164
3734
|
}
|
|
3165
3735
|
});
|
|
3166
|
-
|
|
3736
|
+
tasteCommand.command("feedback").description("Append accept/reject feedback into memory").option("--storage <backend>", "Storage backend: markdown|db").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("--suggestions-file <path>", "Suggestions file path", "taste/suggestions.md").requiredOption("--decision <status>", "accepted|rejected").option("--idea-id <id>", "Optional original idea id").requiredOption("--title <text>", "Idea title").option("--format <text>", "Idea format", "daily-reel").option("--tags <csv>", "Comma separated tags").option("--reason-tags <csv>", "Comma separated reason tags").option("--summary <text>", "Optional summary", "").option("--content <text>", "Optional content", "").option("--notes <text>", "Optional reviewer notes", "").option("--json", "Emit machine-readable JSON output").action(async (options) => {
|
|
3167
3737
|
try {
|
|
3168
|
-
const
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3738
|
+
const decision = String(options.decision || "").toLowerCase();
|
|
3739
|
+
if (decision !== "accepted" && decision !== "rejected") {
|
|
3740
|
+
throw new Error("Invalid --decision. Use accepted|rejected.");
|
|
3741
|
+
}
|
|
3742
|
+
const store = createStoreFromOptions(options);
|
|
3743
|
+
await store.ensureWorkspace();
|
|
3744
|
+
const entry = {
|
|
3745
|
+
id: options.ideaId,
|
|
3746
|
+
title: options.title,
|
|
3747
|
+
format: options.format || "daily-reel",
|
|
3748
|
+
tags: parseCsvList(options.tags),
|
|
3749
|
+
freshnessTerms: [],
|
|
3750
|
+
summary: options.summary || "",
|
|
3751
|
+
content: options.content || "",
|
|
3752
|
+
status: decision,
|
|
3753
|
+
reasonTags: parseCsvList(options.reasonTags),
|
|
3754
|
+
notes: options.notes || ""
|
|
3755
|
+
};
|
|
3756
|
+
const saved = await store.appendMemory(entry);
|
|
3757
|
+
if (options.json) {
|
|
3758
|
+
console.log(JSON.stringify({ item: saved }, null, 2));
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
console.log(`Saved feedback: ${saved.id} (${saved.status || decision})`);
|
|
3762
|
+
} catch (error) {
|
|
3763
|
+
console.error(`Error: ${error.message}`);
|
|
3764
|
+
process.exit(1);
|
|
3765
|
+
}
|
|
3766
|
+
});
|
|
3767
|
+
tasteCommand.command("suggest").description("Generate pending taste suggestions from memory feedback").option("--storage <backend>", "Storage backend: markdown|db").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("--suggestions-file <path>", "Suggestions file path", "taste/suggestions.md").option("--memory-window <number>", "Recent memory window for suggestion generation", "50").option("--json", "Emit machine-readable JSON output").action(async (options) => {
|
|
3768
|
+
try {
|
|
3769
|
+
const store = createStoreFromOptions(options);
|
|
3770
|
+
await store.ensureWorkspace();
|
|
3771
|
+
const taste = await store.readTaste();
|
|
3772
|
+
const memories = await store.queryMemory({
|
|
3773
|
+
limit: Math.max(1, parseInt(options.memoryWindow, 10) || 50)
|
|
3774
|
+
});
|
|
3775
|
+
const draft = buildTasteSuggestionFromMemory(taste.content, memories);
|
|
3776
|
+
const suggestion = await store.appendSuggestion({
|
|
3777
|
+
title: draft.title,
|
|
3778
|
+
summary: draft.summary,
|
|
3779
|
+
rationale: draft.rationale,
|
|
3780
|
+
patch: draft.patch,
|
|
3781
|
+
proposedTasteContent: draft.proposedTasteContent,
|
|
3782
|
+
baseVersion: taste.version
|
|
3783
|
+
});
|
|
3784
|
+
if (options.json) {
|
|
3785
|
+
console.log(JSON.stringify({ suggestion }, null, 2));
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
console.log(`Created suggestion: ${suggestion.id} (${suggestion.status})`);
|
|
3789
|
+
} catch (error) {
|
|
3790
|
+
console.error(`Error: ${error.message}`);
|
|
3791
|
+
process.exit(1);
|
|
3792
|
+
}
|
|
3793
|
+
});
|
|
3794
|
+
tasteCommand.command("apply").description("Apply or reject a suggestion entry").requiredOption("--suggestion <id>", "Suggestion id").option("--reject", "Mark suggestion as rejected without applying").option("--note <text>", "Optional reviewer note").option("--storage <backend>", "Storage backend: markdown|db").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("--suggestions-file <path>", "Suggestions file path", "taste/suggestions.md").option("--json", "Emit machine-readable JSON output").action(async (options) => {
|
|
3795
|
+
try {
|
|
3796
|
+
const store = createStoreFromOptions(options);
|
|
3797
|
+
await store.ensureWorkspace();
|
|
3798
|
+
const suggestions = await store.listSuggestions();
|
|
3799
|
+
const target = suggestions.find((item) => item.id === options.suggestion);
|
|
3800
|
+
if (!target) {
|
|
3801
|
+
throw new Error(`Suggestion not found: ${options.suggestion}`);
|
|
3802
|
+
}
|
|
3803
|
+
if (target.status !== "pending") {
|
|
3804
|
+
throw new Error(`Suggestion ${target.id} is already ${target.status}.`);
|
|
3805
|
+
}
|
|
3806
|
+
if (options.reject) {
|
|
3807
|
+
const updated2 = await store.updateSuggestionStatus(target.id, "rejected", { reviewNote: options.note });
|
|
3808
|
+
if (options.json) {
|
|
3809
|
+
console.log(JSON.stringify({ suggestion: updated2 }, null, 2));
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
console.log(`Rejected suggestion: ${updated2.id}`);
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
if (!target.proposedTasteContent.trim()) {
|
|
3816
|
+
throw new Error("Suggestion is missing proposed taste content.");
|
|
3817
|
+
}
|
|
3818
|
+
await store.writeTaste(target.proposedTasteContent, target.baseVersion);
|
|
3819
|
+
const updated = await store.updateSuggestionStatus(target.id, "applied", { reviewNote: options.note });
|
|
3820
|
+
if (options.json) {
|
|
3821
|
+
console.log(JSON.stringify({ suggestion: updated }, null, 2));
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
console.log(`Applied suggestion: ${updated.id}`);
|
|
3825
|
+
} catch (error) {
|
|
3826
|
+
console.error(`Error: ${error.message}`);
|
|
3827
|
+
process.exit(1);
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
tasteCommand.command("read").description("Read the taste file and output its content (useful for agents)").option("--storage <backend>", "Storage backend: markdown|db").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("--suggestions-file <path>", "Suggestions file path", "taste/suggestions.md").option("--json", "Emit machine-readable JSON output").action(async (options) => {
|
|
3831
|
+
try {
|
|
3832
|
+
const store = createStoreFromOptions(options);
|
|
3833
|
+
const taste = await store.readTaste();
|
|
3834
|
+
if (!taste.content) {
|
|
3835
|
+
const fullPath = safeResolveFromCwd(options.tasteFile);
|
|
3836
|
+
console.warn(`Taste file not found or empty at: ${fullPath}`);
|
|
3837
|
+
}
|
|
3838
|
+
if (options.json) {
|
|
3839
|
+
console.log(JSON.stringify({ content: taste.content, version: taste.version }, null, 2));
|
|
3840
|
+
} else {
|
|
3841
|
+
console.log(taste.content);
|
|
3842
|
+
}
|
|
3843
|
+
} catch (error) {
|
|
3844
|
+
console.error(`Error: ${error.message}`);
|
|
3845
|
+
process.exit(1);
|
|
3846
|
+
}
|
|
3847
|
+
});
|
|
3848
|
+
var tasteMemoryQueryCommand = new Command12("query").description("Query structured taste memories").option("--storage <backend>", "Storage backend: markdown|db").option("--taste-file <path>", "Taste file path", "taste/taste.md").option("--memory-file <path>", "Memory file path", "taste/memory.md").option("--suggestions-file <path>", "Suggestions file path", "taste/suggestions.md").option("--tag <tag>", "Filter by tag").option("--status <status>", "Filter by status (generated|accepted|rejected)").option("--since <date>", "Filter by created_at >= YYYY-MM-DD").option("--limit <number>", "Maximum number of results", "20").option("--text <text>", "Search term in title/summary/content").option("--json", "Emit machine-readable JSON output").action(async (options) => {
|
|
3849
|
+
try {
|
|
3850
|
+
const store = createStoreFromOptions(options);
|
|
3851
|
+
const filtered = await store.queryMemory({
|
|
3172
3852
|
tag: options.tag,
|
|
3853
|
+
status: options.status,
|
|
3173
3854
|
since: options.since,
|
|
3174
3855
|
text: options.text,
|
|
3175
3856
|
limit: parseInt(options.limit, 10) || 20
|
|
@@ -3184,6 +3865,7 @@ var tasteMemoryQueryCommand = new Command12("query").description("Query structur
|
|
|
3184
3865
|
}
|
|
3185
3866
|
for (const item of filtered) {
|
|
3186
3867
|
console.log(`${item.id} | ${item.createdAt} | ${item.title}`);
|
|
3868
|
+
if (item.status) console.log(` status: ${item.status}`);
|
|
3187
3869
|
if (item.tags.length) console.log(` tags: ${item.tags.join(", ")}`);
|
|
3188
3870
|
if (item.summary) console.log(` summary: ${item.summary}`);
|
|
3189
3871
|
}
|
|
@@ -3210,8 +3892,8 @@ function createFeedbackCommand(telemetry2) {
|
|
|
3210
3892
|
}
|
|
3211
3893
|
|
|
3212
3894
|
// src/cli/index.ts
|
|
3213
|
-
var
|
|
3214
|
-
var
|
|
3895
|
+
var __filename5 = fileURLToPath5(import.meta.url);
|
|
3896
|
+
var __dirname5 = path18.dirname(__filename5);
|
|
3215
3897
|
var program = new Command14();
|
|
3216
3898
|
var telemetry = new PostHogTelemetryService();
|
|
3217
3899
|
var commandStartTimes = /* @__PURE__ */ new WeakMap();
|
|
@@ -3228,12 +3910,12 @@ program.command("edit [path]").alias("start").alias("init").description("Start t
|
|
|
3228
3910
|
const port = parseInt(options.port);
|
|
3229
3911
|
let sceneFile;
|
|
3230
3912
|
if (pathArg) {
|
|
3231
|
-
const targetPath =
|
|
3232
|
-
if (
|
|
3233
|
-
const stats =
|
|
3913
|
+
const targetPath = path18.resolve(process.cwd(), pathArg);
|
|
3914
|
+
if (fs17.existsSync(targetPath)) {
|
|
3915
|
+
const stats = fs17.statSync(targetPath);
|
|
3234
3916
|
if (stats.isFile()) {
|
|
3235
|
-
sceneFile =
|
|
3236
|
-
const dir =
|
|
3917
|
+
sceneFile = path18.basename(targetPath);
|
|
3918
|
+
const dir = path18.dirname(targetPath);
|
|
3237
3919
|
process.chdir(dir);
|
|
3238
3920
|
console.log(`Opening scene file: ${sceneFile}`);
|
|
3239
3921
|
} else if (stats.isDirectory()) {
|
|
@@ -3241,28 +3923,28 @@ program.command("edit [path]").alias("start").alias("init").description("Start t
|
|
|
3241
3923
|
}
|
|
3242
3924
|
} else {
|
|
3243
3925
|
if (pathArg.endsWith(".json")) {
|
|
3244
|
-
sceneFile =
|
|
3245
|
-
const dir =
|
|
3246
|
-
if (!
|
|
3926
|
+
sceneFile = path18.basename(pathArg);
|
|
3927
|
+
const dir = path18.dirname(path18.resolve(process.cwd(), pathArg));
|
|
3928
|
+
if (!fs17.existsSync(dir)) {
|
|
3247
3929
|
console.log(`Creating directory ${dir}...`);
|
|
3248
|
-
|
|
3930
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
3249
3931
|
}
|
|
3250
3932
|
process.chdir(dir);
|
|
3251
3933
|
console.log(`Will create new scene file: ${sceneFile}`);
|
|
3252
3934
|
} else {
|
|
3253
3935
|
console.log(`Creating directory ${targetPath}...`);
|
|
3254
|
-
|
|
3936
|
+
fs17.mkdirSync(targetPath, { recursive: true });
|
|
3255
3937
|
process.chdir(targetPath);
|
|
3256
3938
|
}
|
|
3257
3939
|
}
|
|
3258
3940
|
}
|
|
3259
3941
|
const cwd = process.cwd();
|
|
3260
3942
|
console.log(`Starting Feedeas in ${cwd}...`);
|
|
3261
|
-
let staticRoot =
|
|
3262
|
-
if (!
|
|
3263
|
-
staticRoot =
|
|
3943
|
+
let staticRoot = path18.resolve(__dirname5, "../../dist/ui");
|
|
3944
|
+
if (!fs17.existsSync(staticRoot)) {
|
|
3945
|
+
staticRoot = path18.resolve(__dirname5, "../ui");
|
|
3264
3946
|
}
|
|
3265
|
-
if (!
|
|
3947
|
+
if (!fs17.existsSync(staticRoot)) {
|
|
3266
3948
|
console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
|
|
3267
3949
|
}
|
|
3268
3950
|
const app2 = createServer(staticRoot);
|
|
@@ -3365,7 +4047,12 @@ Guided Flow (Agent-Friendly, End-to-End):
|
|
|
3365
4047
|
Agent Conversation Example:
|
|
3366
4048
|
User: Create a nice reel about small life habits that heal us.
|
|
3367
4049
|
Agent: I'll draft a short storyline, generate assets, build the scene, validate, and render.
|
|
3368
|
-
Agent: Let me create the narration script first.
|
|
4050
|
+
Agent: Let me create the narration script first, first check if the user has any taste preferences.
|
|
4051
|
+
Agent (tool): feedeas taste memory query --tag "content preferences" --limit 5
|
|
4052
|
+
Agent: Based on past preferences, I'll go with a warm, hopeful tone.
|
|
4053
|
+
Agent (tool): feedeas audio "Life changes in small daily choices. A morning ritual of mindfulness can set the tone for your day..." -o narration.mp3 --no-transcribe
|
|
4054
|
+
Agent: Now I'll generate some visuals to match the narration.
|
|
4055
|
+
Agent (tool): feedeas example documentary > scene.json
|
|
3369
4056
|
Agent (tool): feedeas imagine "warm cinematic kitchen morning..." --aspect-ratio 9:16 -o scene1.png
|
|
3370
4057
|
Agent (tool): feedeas imagine "A person meditating in a park..." --aspect-ratio 9:16 -o scene2.png
|
|
3371
4058
|
Agent (tool): feedeas audio "Life changes in small daily choices..." -o narration.mp3 --no-transcribe
|
|
@@ -3449,8 +4136,12 @@ Interactive Workflow (for Humans):
|
|
|
3449
4136
|
$ feedeas snap 2.5 -o frame.png # Take snapshot at 2.5s
|
|
3450
4137
|
|
|
3451
4138
|
Taste Workflow:
|
|
3452
|
-
$ feedeas taste
|
|
4139
|
+
$ feedeas taste # Start UI Workspace
|
|
4140
|
+
$ feedeas taste read --json # Read instructions/taste definitions
|
|
3453
4141
|
$ feedeas taste generate -n 3 --mode sequential --json
|
|
4142
|
+
$ feedeas taste feedback --decision accepted --title "Idea title"
|
|
4143
|
+
$ feedeas taste suggest --json
|
|
4144
|
+
$ feedeas taste apply --suggestion <id>
|
|
3454
4145
|
$ feedeas taste memory query --tag podcast --limit 10
|
|
3455
4146
|
`);
|
|
3456
4147
|
if (!process.argv.slice(2).length) {
|