@tyvm/knowhow 0.0.130 → 0.0.132
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/package.json +1 -1
- package/src/agents/configurable/ConfigAgent.ts +0 -1
- package/src/agents/tools/startAgentTask.ts +17 -13
- package/src/ai.ts +55 -24
- package/src/chat/CliChatService.ts +7 -1
- package/src/chat/modules/AgentModule.ts +72 -14
- package/src/chat/renderer/PlainRenderer.ts +218 -0
- package/src/chat/renderer/index.ts +1 -0
- package/src/chat/renderer/loadRenderer.ts +2 -0
- package/src/chat.ts +2 -2
- package/src/cli.ts +23 -0
- package/src/clients/anthropic.ts +9 -1
- package/src/clients/fireworks.ts +10 -1
- package/src/clients/index.ts +6 -3
- package/src/clients/meta.ts +9 -0
- package/src/clients/pricing/anthropic.ts +11 -0
- package/src/clients/pricing/fireworks.ts +12 -0
- package/src/commands/agent.ts +53 -20
- package/src/commands/agents.ts +556 -0
- package/src/commands/hash.ts +170 -0
- package/src/commands/misc.ts +157 -3
- package/src/commands/skills.ts +36 -0
- package/src/config.ts +54 -2
- package/src/generate.ts +507 -0
- package/src/index.ts +32 -216
- package/src/services/BehaviorsService.ts +32 -0
- package/src/services/SkillsService.ts +198 -0
- package/src/types.ts +36 -4
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/configurable/ConfigAgent.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -2
- package/ts_build/src/agents/tools/startAgentTask.js +13 -12
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/ai.d.ts +10 -4
- package/ts_build/src/ai.js +27 -25
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/CliChatService.d.ts +1 -1
- package/ts_build/src/chat/CliChatService.js +4 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +61 -4
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/renderer/PlainRenderer.d.ts +28 -0
- package/ts_build/src/chat/renderer/PlainRenderer.js +169 -0
- package/ts_build/src/chat/renderer/PlainRenderer.js.map +1 -0
- package/ts_build/src/chat/renderer/index.d.ts +1 -0
- package/ts_build/src/chat/renderer/index.js +3 -1
- package/ts_build/src/chat/renderer/index.js.map +1 -1
- package/ts_build/src/chat/renderer/loadRenderer.js +2 -0
- package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -1
- package/ts_build/src/chat.d.ts +1 -1
- package/ts_build/src/chat.js +2 -2
- package/ts_build/src/chat.js.map +1 -1
- package/ts_build/src/cli.js +16 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +4 -1
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/fireworks.js +7 -0
- package/ts_build/src/clients/fireworks.js.map +1 -1
- package/ts_build/src/clients/index.js +5 -1
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/meta.d.ts +2 -0
- package/ts_build/src/clients/meta.js +5 -0
- package/ts_build/src/clients/meta.js.map +1 -1
- package/ts_build/src/clients/pricing/anthropic.d.ts +3 -0
- package/ts_build/src/clients/pricing/anthropic.js +6 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.d.ts +1 -0
- package/ts_build/src/clients/pricing/fireworks.js +5 -1
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/commands/agent.js +44 -17
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +2 -0
- package/ts_build/src/commands/agents.js +460 -0
- package/ts_build/src/commands/agents.js.map +1 -0
- package/ts_build/src/commands/hash.d.ts +2 -0
- package/ts_build/src/commands/hash.js +150 -0
- package/ts_build/src/commands/hash.js.map +1 -0
- package/ts_build/src/commands/misc.js +108 -2
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/skills.js +24 -0
- package/ts_build/src/commands/skills.js.map +1 -1
- package/ts_build/src/config.js +37 -1
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/generate.d.ts +16 -0
- package/ts_build/src/generate.js +304 -0
- package/ts_build/src/generate.js.map +1 -0
- package/ts_build/src/index.d.ts +1 -3
- package/ts_build/src/index.js +9 -118
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/services/BehaviorsService.d.ts +2 -0
- package/ts_build/src/services/BehaviorsService.js +24 -0
- package/ts_build/src/services/BehaviorsService.js.map +1 -1
- package/ts_build/src/services/SkillsService.d.ts +7 -0
- package/ts_build/src/services/SkillsService.js +163 -0
- package/ts_build/src/services/SkillsService.js.map +1 -1
- package/ts_build/src/types.d.ts +14 -4
- package/ts_build/src/types.js.map +1 -1
package/src/generate.ts
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { summarizeFiles, summarizeFile } from "./ai";
|
|
2
|
+
import type { AgentOptions } from "./ai";
|
|
3
|
+
import {
|
|
4
|
+
saveAllFileHashes,
|
|
5
|
+
getHashes,
|
|
6
|
+
checkNoFilesChanged,
|
|
7
|
+
} from "./hashes";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as crypto from "crypto";
|
|
11
|
+
import { globSync } from "glob";
|
|
12
|
+
|
|
13
|
+
import { GenerationSource } from "./types";
|
|
14
|
+
import { readFile, writeFile } from "./utils";
|
|
15
|
+
import { getConfig, loadPrompt } from "./config";
|
|
16
|
+
import { services } from "./services/";
|
|
17
|
+
import { convertToText } from "./conversion";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Input pattern normalization
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalizes an input pattern to a valid glob pattern.
|
|
25
|
+
* Supports:
|
|
26
|
+
* - Standard glob patterns (e.g. "src/**\/*.ts")
|
|
27
|
+
* - Brace expansion (e.g. "{src/a.ts,src/b.ts}")
|
|
28
|
+
* - Comma-separated file paths (e.g. "src/a.ts,src/b.ts") — auto-converted to brace expansion
|
|
29
|
+
* - Mixed comma-separated list with globs (e.g. "src/a.ts,src/commands/**\/*.ts")
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeInputPattern(input: string): string {
|
|
32
|
+
if (input.includes("{")) return input;
|
|
33
|
+
if (input.includes(",")) {
|
|
34
|
+
const parts = input.split(",").map((p) => p.trim());
|
|
35
|
+
return `{${parts.join(",")}}`;
|
|
36
|
+
}
|
|
37
|
+
return input;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// GenerateOptions
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface GenerateOptions {
|
|
45
|
+
/** Only run sources whose `name` matches exactly this value */
|
|
46
|
+
name?: string;
|
|
47
|
+
/** Only run sources whose name/input/output contains this substring */
|
|
48
|
+
filter?: string;
|
|
49
|
+
/**
|
|
50
|
+
* When true, agent-driven sources create .knowhow/processes/agents/<taskId>/
|
|
51
|
+
* so they appear in `knowhow agents list`. Overrides the per-source syncFs flag.
|
|
52
|
+
*/
|
|
53
|
+
syncFs?: boolean;
|
|
54
|
+
/** When true, skip hash checks and regenerate all matching sources regardless of changes */
|
|
55
|
+
force?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Maximum number of generation sources to run in parallel within a single
|
|
58
|
+
* dependency wave. Defaults to 3. Set to 1 for fully sequential execution.
|
|
59
|
+
*/
|
|
60
|
+
concurrency?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Filter helper
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns true when a source should run given the provided filter options.
|
|
69
|
+
*/
|
|
70
|
+
function sourceMatchesFilter(
|
|
71
|
+
source: GenerationSource,
|
|
72
|
+
options: GenerateOptions
|
|
73
|
+
): boolean {
|
|
74
|
+
if (options.name) return source.name === options.name;
|
|
75
|
+
if (options.filter) {
|
|
76
|
+
const needle = options.filter.toLowerCase();
|
|
77
|
+
return [source.name, source.input, source.output]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.some((v) => v!.toLowerCase().includes(needle));
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Dependency / concurrency helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Detects whether sourceB's input references sourceA's output directory or file.
|
|
90
|
+
*
|
|
91
|
+
* Heuristic: if sourceA produces an output path and sourceB reads from that
|
|
92
|
+
* same path (or a sub-path of it) then B depends on A.
|
|
93
|
+
*/
|
|
94
|
+
function ioOverlaps(sourceA: GenerationSource, sourceB: GenerationSource): boolean {
|
|
95
|
+
const aOut = sourceA.output;
|
|
96
|
+
if (!aOut) return false;
|
|
97
|
+
|
|
98
|
+
const bInputPattern = normalizeInputPattern(sourceB.input || "");
|
|
99
|
+
|
|
100
|
+
// Strip brace expansion to individual paths for comparison
|
|
101
|
+
const bPaths = bInputPattern.startsWith("{")
|
|
102
|
+
? bInputPattern.slice(1, -1).split(",").map((p) => p.trim())
|
|
103
|
+
: [bInputPattern];
|
|
104
|
+
|
|
105
|
+
for (const bPath of bPaths) {
|
|
106
|
+
// If aOut is a directory (ends with /) check if bPath starts with it
|
|
107
|
+
if (aOut.endsWith("/") && bPath.startsWith(aOut)) return true;
|
|
108
|
+
// If aOut is a file, check if bPath starts with aOut's directory
|
|
109
|
+
const aDir = aOut.endsWith("/") ? aOut : path.dirname(aOut) + "/";
|
|
110
|
+
if (bPath.startsWith(aDir)) return true;
|
|
111
|
+
// Direct prefix/glob match: e.g. aOut = "autodoc/" bPath = "autodoc/**/*.md"
|
|
112
|
+
if (bPath.startsWith(aOut.replace(/\/$/, ""))) return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds a topological dependency graph and returns sources grouped into
|
|
119
|
+
* sequential "waves". All sources within a wave have no un-satisfied
|
|
120
|
+
* dependencies and can run concurrently. Each subsequent wave waits for
|
|
121
|
+
* the previous wave to complete.
|
|
122
|
+
*
|
|
123
|
+
* Dependency resolution:
|
|
124
|
+
* 1. Explicit `dependsOn: string[]` — names listed there must finish first.
|
|
125
|
+
* 2. Automatic I/O overlap detection — if A writes to a path that B reads,
|
|
126
|
+
* B is automatically placed after A.
|
|
127
|
+
*/
|
|
128
|
+
export function buildWaves(sources: GenerationSource[]): GenerationSource[][] {
|
|
129
|
+
const nameIndex = new Map<string, GenerationSource>();
|
|
130
|
+
for (const s of sources) {
|
|
131
|
+
if (s.name) nameIndex.set(s.name, s);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build adjacency: deps[i] = set of indices that source[i] depends on
|
|
135
|
+
const n = sources.length;
|
|
136
|
+
const deps: Set<number>[] = Array.from({ length: n }, () => new Set());
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < n; i++) {
|
|
139
|
+
const b = sources[i];
|
|
140
|
+
|
|
141
|
+
// Explicit dependsOn
|
|
142
|
+
for (const depName of b.dependsOn ?? []) {
|
|
143
|
+
const depIdx = sources.findIndex((s) => s.name === depName);
|
|
144
|
+
if (depIdx !== -1 && depIdx !== i) deps[i].add(depIdx);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Auto I/O overlap
|
|
148
|
+
for (let j = 0; j < n; j++) {
|
|
149
|
+
if (i === j) continue;
|
|
150
|
+
if (ioOverlaps(sources[j], b)) deps[i].add(j);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Kahn's algorithm to produce waves
|
|
155
|
+
const waves: GenerationSource[][] = [];
|
|
156
|
+
const completed = new Set<number>();
|
|
157
|
+
|
|
158
|
+
while (completed.size < n) {
|
|
159
|
+
const wave: number[] = [];
|
|
160
|
+
for (let i = 0; i < n; i++) {
|
|
161
|
+
if (completed.has(i)) continue;
|
|
162
|
+
const remaining = [...deps[i]].filter((d) => !completed.has(d));
|
|
163
|
+
if (remaining.length === 0) wave.push(i);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (wave.length === 0) {
|
|
167
|
+
// Cycle detected — fall back to running everything remaining sequentially
|
|
168
|
+
console.warn(
|
|
169
|
+
"⚠️ Dependency cycle detected in generation sources — running remaining sources sequentially."
|
|
170
|
+
);
|
|
171
|
+
for (let i = 0; i < n; i++) {
|
|
172
|
+
if (!completed.has(i)) wave.push(i);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
waves.push(wave.map((i) => sources[i]));
|
|
177
|
+
for (const i of wave) completed.add(i);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return waves;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Runs up to `limit` async tasks concurrently, preserving order of
|
|
185
|
+
* completion relative to input order.
|
|
186
|
+
*/
|
|
187
|
+
async function runWithConcurrency<T>(
|
|
188
|
+
items: T[],
|
|
189
|
+
limit: number,
|
|
190
|
+
fn: (item: T, index: number) => Promise<void>
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
// Use a simple semaphore-style approach: keep up to `limit` promises running
|
|
193
|
+
// at the same time. As each finishes, the next item is started immediately.
|
|
194
|
+
const entries = [...items.entries()]; // [[0, item0], [1, item1], ...]
|
|
195
|
+
let cursor = 0;
|
|
196
|
+
|
|
197
|
+
async function worker(): Promise<void> {
|
|
198
|
+
while (cursor < entries.length) {
|
|
199
|
+
const [index, item] = entries[cursor++];
|
|
200
|
+
await fn(item, index);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Spawn exactly `limit` workers (or fewer if there aren't enough items).
|
|
205
|
+
const workers: Promise<void>[] = [];
|
|
206
|
+
for (let i = 0; i < Math.min(limit, entries.length); i++) {
|
|
207
|
+
workers.push(worker());
|
|
208
|
+
}
|
|
209
|
+
await Promise.all(workers);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Output helpers
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Appends an instruction telling an assigned agent exactly which file to write
|
|
218
|
+
* its generated output to.
|
|
219
|
+
*/
|
|
220
|
+
export function withOutputTarget(prompt: string, outputFile: string): string {
|
|
221
|
+
return `${prompt}\n\n---\nGENERATION OUTPUT TARGET: Write your finished result to the file at \`${outputFile}\` using your file-writing tools (e.g. writeFileChunk). Create any parent directories as needed. Once the file is written with the complete, final content, call finalAnswer with a brief summary. Do NOT write to any other path.`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* For agent-driven generation: if the agent already wrote the output file
|
|
226
|
+
* itself, keep it. Otherwise, fall back to writing the returned content.
|
|
227
|
+
*/
|
|
228
|
+
export async function writeAgentOrSummaryOutput(
|
|
229
|
+
outputFile: string,
|
|
230
|
+
summary: string,
|
|
231
|
+
agent?: string
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
const outputDir = path.dirname(outputFile);
|
|
234
|
+
if (outputDir && !fs.existsSync(outputDir)) {
|
|
235
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (agent && fs.existsSync(outputFile)) {
|
|
239
|
+
console.log(
|
|
240
|
+
`Agent wrote output file directly: ${outputFile} (skipping summary write)`
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log("Writing summary to", outputFile);
|
|
246
|
+
await writeFile(outputFile, summary ?? "");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Multi-output generation
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
export async function handleMultiOutputGeneration(
|
|
254
|
+
model: string,
|
|
255
|
+
inputPattern: string,
|
|
256
|
+
files: string[],
|
|
257
|
+
prompt: string,
|
|
258
|
+
output: string,
|
|
259
|
+
outputExt = "mdx",
|
|
260
|
+
outputName?: string,
|
|
261
|
+
kind?: string,
|
|
262
|
+
agent?: string,
|
|
263
|
+
agentOpts?: AgentOptions,
|
|
264
|
+
force?: boolean
|
|
265
|
+
) {
|
|
266
|
+
const promptHash = crypto.createHash("md5").update(prompt).digest("hex");
|
|
267
|
+
const hashes = await getHashes();
|
|
268
|
+
|
|
269
|
+
const inputPath = inputPattern.includes("**")
|
|
270
|
+
? inputPattern.split("**")[0]
|
|
271
|
+
: "";
|
|
272
|
+
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
const fileContent = await convertToText(file);
|
|
275
|
+
const fileHash = crypto.createHash("md5").update(fileContent).digest("hex");
|
|
276
|
+
|
|
277
|
+
if (!hashes[file]) {
|
|
278
|
+
hashes[file] = { promptHash: "", fileHash: "" };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { name, ext, dir } = path.parse(file);
|
|
282
|
+
const nestedFolder = inputPath ? (dir + "/").replace(inputPath, "") : "";
|
|
283
|
+
const outputFolder = path.join(output, nestedFolder);
|
|
284
|
+
|
|
285
|
+
if (!fs.existsSync(outputFolder)) {
|
|
286
|
+
fs.mkdirSync(outputFolder, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const outputFileName = outputName || name;
|
|
290
|
+
const outputFile = path.join(outputFolder, outputFileName + "." + outputExt);
|
|
291
|
+
console.log({ dir, inputPath, nestedFolder, outputFile });
|
|
292
|
+
|
|
293
|
+
const toCheck = [file, outputFile];
|
|
294
|
+
const noChanges = await checkNoFilesChanged(toCheck, promptHash, hashes);
|
|
295
|
+
if (noChanges && !force) {
|
|
296
|
+
console.log("Skipping file", file, "because it hasn't changed");
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log("Summarizing", file);
|
|
301
|
+
const effectivePrompt =
|
|
302
|
+
prompt && agent ? withOutputTarget(prompt, outputFile) : prompt;
|
|
303
|
+
const summary = prompt
|
|
304
|
+
? await summarizeFile(file, effectivePrompt, model, agent, agentOpts)
|
|
305
|
+
: fileContent;
|
|
306
|
+
|
|
307
|
+
await writeAgentOrSummaryOutput(outputFile, summary, agent);
|
|
308
|
+
await saveAllFileHashes(toCheck, promptHash);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Single-output generation
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
export async function handleSingleOutputGeneration(
|
|
317
|
+
model: string,
|
|
318
|
+
files: string[],
|
|
319
|
+
prompt: string,
|
|
320
|
+
outputFile: string,
|
|
321
|
+
kind?: string,
|
|
322
|
+
agent?: string,
|
|
323
|
+
agentOpts?: AgentOptions,
|
|
324
|
+
force?: boolean
|
|
325
|
+
) {
|
|
326
|
+
const hashes = await getHashes();
|
|
327
|
+
const promptHash = crypto.createHash("md5").update(prompt).digest("hex");
|
|
328
|
+
|
|
329
|
+
const filesToCheck = [outputFile, ...files];
|
|
330
|
+
const noChanges = await checkNoFilesChanged(filesToCheck, promptHash, hashes);
|
|
331
|
+
if (noChanges && !force) {
|
|
332
|
+
console.log(`Skipping ${files.length} files because they haven't changed`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log("Summarizing", files.length, "files");
|
|
337
|
+
const outputDir = path.dirname(outputFile);
|
|
338
|
+
if (outputDir && !fs.existsSync(outputDir)) {
|
|
339
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const effectivePrompt =
|
|
343
|
+
prompt && agent ? withOutputTarget(prompt, outputFile) : prompt;
|
|
344
|
+
|
|
345
|
+
const summary = prompt
|
|
346
|
+
? await summarizeFiles(files, effectivePrompt, model, agent, agentOpts)
|
|
347
|
+
: (await Promise.all(files.map(convertToText))).join("\n\n");
|
|
348
|
+
|
|
349
|
+
await writeAgentOrSummaryOutput(outputFile, summary, agent);
|
|
350
|
+
await saveAllFileHashes(filesToCheck, promptHash);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// All-kinds generation (plugin + file fallback)
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
async function handleAllKindsGeneration(source: GenerationSource) {
|
|
358
|
+
const { Plugins } = services();
|
|
359
|
+
const { kind, input } = source;
|
|
360
|
+
if (Plugins.isPlugin(kind)) {
|
|
361
|
+
const data = await Plugins.call(kind, input);
|
|
362
|
+
if (source.output.endsWith("/")) {
|
|
363
|
+
throw new Error(`Plugin ${kind} can only output to a single file`);
|
|
364
|
+
}
|
|
365
|
+
const pluginOutputDir = path.dirname(source.output);
|
|
366
|
+
if (pluginOutputDir && !fs.existsSync(pluginOutputDir)) {
|
|
367
|
+
fs.mkdirSync(pluginOutputDir, { recursive: true });
|
|
368
|
+
}
|
|
369
|
+
await writeFile(source.output, data);
|
|
370
|
+
}
|
|
371
|
+
return handleFileKindGeneration(source);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function handleFileKindGeneration(
|
|
375
|
+
source: GenerationSource,
|
|
376
|
+
agentOpts?: AgentOptions
|
|
377
|
+
) {
|
|
378
|
+
const prompt = await loadPrompt(source.prompt);
|
|
379
|
+
const files = globSync(normalizeInputPattern(source.input));
|
|
380
|
+
console.log("Analyzing files: ", files);
|
|
381
|
+
|
|
382
|
+
if (source.output.endsWith("/")) {
|
|
383
|
+
await handleMultiOutputGeneration(
|
|
384
|
+
source.model,
|
|
385
|
+
source.input,
|
|
386
|
+
files,
|
|
387
|
+
prompt,
|
|
388
|
+
source.output,
|
|
389
|
+
source.outputExt,
|
|
390
|
+
source.outputName,
|
|
391
|
+
source.kind,
|
|
392
|
+
source.agent,
|
|
393
|
+
agentOpts
|
|
394
|
+
);
|
|
395
|
+
} else {
|
|
396
|
+
await handleSingleOutputGeneration(
|
|
397
|
+
source.model,
|
|
398
|
+
files,
|
|
399
|
+
prompt,
|
|
400
|
+
source.output,
|
|
401
|
+
source.kind,
|
|
402
|
+
source.agent,
|
|
403
|
+
agentOpts
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// Run a single source
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
async function runSource(
|
|
413
|
+
source: GenerationSource,
|
|
414
|
+
options: GenerateOptions
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
console.log("Generating", source.input, "to", source.output);
|
|
417
|
+
|
|
418
|
+
const effectiveSyncFs = options.syncFs ?? source.syncFs;
|
|
419
|
+
const agentOpts: AgentOptions | undefined = source.agent
|
|
420
|
+
? {
|
|
421
|
+
syncFs: effectiveSyncFs,
|
|
422
|
+
taskId:
|
|
423
|
+
source.taskId ??
|
|
424
|
+
(effectiveSyncFs
|
|
425
|
+
? `generate:${source.output.replace(/[^a-zA-Z0-9_.-]/g, "_")}`
|
|
426
|
+
: undefined),
|
|
427
|
+
maxTimeLimit: source.maxTimeLimit,
|
|
428
|
+
maxSpendLimit: source.maxSpendLimit,
|
|
429
|
+
}
|
|
430
|
+
: undefined;
|
|
431
|
+
|
|
432
|
+
if (source.kind === "file" || !source.kind) {
|
|
433
|
+
const files = globSync(normalizeInputPattern(source.input));
|
|
434
|
+
const prompt = await loadPrompt(source.prompt);
|
|
435
|
+
|
|
436
|
+
if (source.output.endsWith("/")) {
|
|
437
|
+
await handleMultiOutputGeneration(
|
|
438
|
+
source.model,
|
|
439
|
+
source.input,
|
|
440
|
+
files,
|
|
441
|
+
prompt,
|
|
442
|
+
source.output,
|
|
443
|
+
source.outputExt,
|
|
444
|
+
source.outputName,
|
|
445
|
+
source.kind,
|
|
446
|
+
source.agent,
|
|
447
|
+
agentOpts,
|
|
448
|
+
options.force
|
|
449
|
+
);
|
|
450
|
+
} else {
|
|
451
|
+
await handleSingleOutputGeneration(
|
|
452
|
+
source.model,
|
|
453
|
+
files,
|
|
454
|
+
prompt,
|
|
455
|
+
source.output,
|
|
456
|
+
source.kind,
|
|
457
|
+
source.agent,
|
|
458
|
+
agentOpts,
|
|
459
|
+
options.force
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
await handleAllKindsGeneration(source);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Main generate entry point
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
export async function generate(options: GenerateOptions = {}): Promise<void> {
|
|
472
|
+
const config = await getConfig();
|
|
473
|
+
const allSources = config.sources ?? [];
|
|
474
|
+
const sources = allSources.filter((s) => sourceMatchesFilter(s, options));
|
|
475
|
+
|
|
476
|
+
if ((options.name || options.filter) && sources.length === 0) {
|
|
477
|
+
console.warn(
|
|
478
|
+
`No generation sources matched ${
|
|
479
|
+
options.name ? `name="${options.name}"` : `filter="${options.filter}"`
|
|
480
|
+
}. Available names: ${allSources
|
|
481
|
+
.map((s) => s.name)
|
|
482
|
+
.filter(Boolean)
|
|
483
|
+
.join(", ") || "(none defined)"}`
|
|
484
|
+
);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const concurrency = options.concurrency ?? 3;
|
|
489
|
+
const waves = buildWaves(sources);
|
|
490
|
+
|
|
491
|
+
for (let wi = 0; wi < waves.length; wi++) {
|
|
492
|
+
const wave = waves[wi];
|
|
493
|
+
if (waves.length > 1) {
|
|
494
|
+
console.log(
|
|
495
|
+
`\n[Wave ${wi + 1}/${waves.length}] Running ${wave.length} source${wave.length !== 1 ? "s" : ""} (concurrency: ${concurrency})...`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await runWithConcurrency(wave, concurrency, (source) =>
|
|
500
|
+
runSource(source, options)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (waves.length > 1) {
|
|
505
|
+
console.log(`\n✅ Generation complete (${waves.length} wave${waves.length !== 1 ? "s" : ""}, ${sources.length} sources).`);
|
|
506
|
+
}
|
|
507
|
+
}
|