@folio-agent/handler 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ingest/generate.js +23 -2
- package/dist/ingest/types.d.ts +7 -0
- package/dist/init/cli.d.ts +14 -1
- package/dist/init/cli.js +26 -15
- package/dist/sync/cli.d.ts +8 -0
- package/dist/sync/cli.js +36 -0
- package/package.json +3 -2
package/dist/ingest/generate.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
1
2
|
import { createUrlMatcher } from "./glob.js";
|
|
2
3
|
import { htmlToText } from "./html-to-text.js";
|
|
3
4
|
import { readTextFile, scanDist, scanKnowledgeDir } from "./scan-dist.js";
|
|
4
5
|
import { DEFAULT_TOKEN_WARNING_THRESHOLD, estimateTokens } from "./token-count.js";
|
|
5
6
|
import { scanZennArticles } from "./zenn.js";
|
|
7
|
+
async function pathExists(path) {
|
|
8
|
+
try {
|
|
9
|
+
await stat(path);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === "ENOENT")
|
|
14
|
+
return false;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
6
18
|
export async function generateKnowledge(config) {
|
|
7
19
|
const matchesUrl = createUrlMatcher(config);
|
|
8
20
|
const pages = [];
|
|
21
|
+
const warnings = [];
|
|
9
22
|
const distFiles = await scanDist(config.distDir);
|
|
10
23
|
for (const file of distFiles.filter((f) => matchesUrl(f.urlPath))) {
|
|
11
24
|
const html = await readTextFile(file.absolutePath);
|
|
@@ -20,12 +33,20 @@ export async function generateKnowledge(config) {
|
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
if (config.zenn) {
|
|
23
|
-
|
|
36
|
+
if (await pathExists(config.zenn.articlesDir)) {
|
|
37
|
+
pages.push(...(await scanZennArticles(config.zenn)));
|
|
38
|
+
}
|
|
39
|
+
else if (config.zennSnapshotPath) {
|
|
40
|
+
const snapshot = JSON.parse(await readFile(config.zennSnapshotPath, "utf-8"));
|
|
41
|
+
pages.push(...snapshot);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
warnings.push(`articlesDir not found: ${config.zenn.articlesDir} — skipping zenn ingest`);
|
|
45
|
+
}
|
|
24
46
|
}
|
|
25
47
|
pages.sort((a, b) => a.url.localeCompare(b.url));
|
|
26
48
|
const estimatedTokens = estimateTokens(pages.map((p) => p.text).join("\n"));
|
|
27
49
|
const threshold = config.tokenWarningThreshold ?? DEFAULT_TOKEN_WARNING_THRESHOLD;
|
|
28
|
-
const warnings = [];
|
|
29
50
|
if (estimatedTokens > threshold) {
|
|
30
51
|
warnings.push(`estimated knowledge size (${estimatedTokens} tokens) exceeds the warning threshold (${threshold}). ` +
|
|
31
52
|
"Consider narrowing include globs, or switching to a RAG-based approach.");
|
package/dist/ingest/types.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface IngestConfig {
|
|
|
10
10
|
knowledgeDir?: string;
|
|
11
11
|
/** Zenn CLI articles/ ingestion. Omit to skip Zenn articles entirely. */
|
|
12
12
|
zenn?: ZennIngestConfig;
|
|
13
|
+
/**
|
|
14
|
+
* Path to a snapshot JSON produced by `folio-agent-sync-zenn`. Used as a
|
|
15
|
+
* fallback source of Zenn pages in environments where `zenn.articlesDir`
|
|
16
|
+
* cannot be read (e.g. CI, where the articles directory often comes from a
|
|
17
|
+
* separate/private repo that isn't checked out).
|
|
18
|
+
*/
|
|
19
|
+
zennSnapshotPath?: string;
|
|
13
20
|
/** Token threshold above which the CLI warns that CAG may no longer fit. */
|
|
14
21
|
tokenWarningThreshold?: number;
|
|
15
22
|
/** UI language captured by `folio-agent-init`. Ignored by ingest itself. */
|
package/dist/init/cli.d.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {
|
|
2
|
+
export interface DevVarsAndGitignorePlan {
|
|
3
|
+
nextDevVars: string | undefined;
|
|
4
|
+
gitignoreResult: {
|
|
5
|
+
content: string;
|
|
6
|
+
changed: boolean;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Computes what to write to `.dev.vars` and `.gitignore`. `.gitignore` protection for
|
|
11
|
+
* `.dev.vars` is unconditional — independent of whether a Gemini API key was entered this run —
|
|
12
|
+
* because a skipped key is commonly filled in by hand afterward per the wizard's own guidance
|
|
13
|
+
* (see `geminiApiKeySkipNote`), and by then the file must already be gitignored.
|
|
14
|
+
*/
|
|
15
|
+
export declare function planDevVarsAndGitignore(geminiApiKey: string | undefined, devVarsContent: string, gitignoreContent: string): DevVarsAndGitignorePlan;
|
package/dist/init/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import * as clack from "@clack/prompts";
|
|
6
7
|
import { DEFAULT_WIZARD_ANSWERS, runWizard } from "./questions.js";
|
|
7
8
|
import { appendIngestToBuildScript, buildApiRouteTemplate, buildConfigJson, buildThemeCss, deriveEndpointPath, ensureGitignoreEntry, PUBLIC_DIR_NAME, resolveThemeCssPath, THEME_CSS_FILENAME, upsertDevVar, } from "./writers.js";
|
|
@@ -71,6 +72,17 @@ async function readJson(path) {
|
|
|
71
72
|
async function readTextOrEmpty(path) {
|
|
72
73
|
return existsSync(path) ? readFile(path, "utf-8") : "";
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Computes what to write to `.dev.vars` and `.gitignore`. `.gitignore` protection for
|
|
77
|
+
* `.dev.vars` is unconditional — independent of whether a Gemini API key was entered this run —
|
|
78
|
+
* because a skipped key is commonly filled in by hand afterward per the wizard's own guidance
|
|
79
|
+
* (see `geminiApiKeySkipNote`), and by then the file must already be gitignored.
|
|
80
|
+
*/
|
|
81
|
+
export function planDevVarsAndGitignore(geminiApiKey, devVarsContent, gitignoreContent) {
|
|
82
|
+
const nextDevVars = geminiApiKey ? upsertDevVar(devVarsContent, "GEMINI_API_KEY", geminiApiKey) : undefined;
|
|
83
|
+
const gitignoreResult = ensureGitignoreEntry(gitignoreContent, DEV_VARS_PATH);
|
|
84
|
+
return { nextDevVars, gitignoreResult };
|
|
85
|
+
}
|
|
74
86
|
function seedDefaults(previous) {
|
|
75
87
|
if (!previous)
|
|
76
88
|
return DEFAULT_WIZARD_ANSWERS;
|
|
@@ -107,10 +119,7 @@ async function main() {
|
|
|
107
119
|
const scripts = pkg.scripts ?? {};
|
|
108
120
|
const nextBuildScript = appendIngestToBuildScript(scripts.build, CONFIG_PATH, KNOWLEDGE_OUTPUT_PATH);
|
|
109
121
|
const buildScriptChanged = nextBuildScript !== (scripts.build ?? "");
|
|
110
|
-
const nextDevVars = answers.geminiApiKey
|
|
111
|
-
? upsertDevVar(await readTextOrEmpty(DEV_VARS_PATH), "GEMINI_API_KEY", answers.geminiApiKey)
|
|
112
|
-
: undefined;
|
|
113
|
-
const gitignoreResult = nextDevVars !== undefined ? ensureGitignoreEntry(await readTextOrEmpty(GITIGNORE_PATH), DEV_VARS_PATH) : undefined;
|
|
122
|
+
const { nextDevVars, gitignoreResult } = planDevVarsAndGitignore(answers.geminiApiKey, await readTextOrEmpty(DEV_VARS_PATH), await readTextOrEmpty(GITIGNORE_PATH));
|
|
114
123
|
const summaryLines = [
|
|
115
124
|
text.summaryConfigLine,
|
|
116
125
|
text.summaryThemeLine(themeCssPath),
|
|
@@ -123,8 +132,8 @@ async function main() {
|
|
|
123
132
|
];
|
|
124
133
|
if (nextDevVars !== undefined) {
|
|
125
134
|
summaryLines.push(text.summaryDevVarsLine);
|
|
126
|
-
summaryLines.push(gitignoreResult?.changed ? text.summaryGitignoreAppendedLine : text.summaryGitignoreAlreadyPresentLine);
|
|
127
135
|
}
|
|
136
|
+
summaryLines.push(gitignoreResult.changed ? text.summaryGitignoreAppendedLine : text.summaryGitignoreAlreadyPresentLine);
|
|
128
137
|
clack.note(summaryLines.join("\n"), text.summaryTitle);
|
|
129
138
|
const shouldWrite = await clack.confirm({ message: text.confirmMessage, initialValue: true });
|
|
130
139
|
if (clack.isCancel(shouldWrite) || !shouldWrite) {
|
|
@@ -143,9 +152,9 @@ async function main() {
|
|
|
143
152
|
}
|
|
144
153
|
if (nextDevVars !== undefined) {
|
|
145
154
|
await writeFile(DEV_VARS_PATH, nextDevVars);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
}
|
|
156
|
+
if (gitignoreResult.changed) {
|
|
157
|
+
await writeFile(GITIGNORE_PATH, gitignoreResult.content);
|
|
149
158
|
}
|
|
150
159
|
const endpoint = deriveEndpointPath(answers.apiRoutePath ?? DEFAULT_WIZARD_ANSWERS.apiRoutePath ?? "functions/api/chat.ts");
|
|
151
160
|
const snippetLines = [
|
|
@@ -169,13 +178,15 @@ async function main() {
|
|
|
169
178
|
}
|
|
170
179
|
clack.note(introNotes.join("\n\n"));
|
|
171
180
|
console.log(`\n${snippet}\n`);
|
|
172
|
-
|
|
173
|
-
clack.note(gitignoreResult?.changed ? text.gitignoreAppendedNote : text.gitignoreAlreadyPresentNote);
|
|
174
|
-
}
|
|
181
|
+
clack.note(gitignoreResult.changed ? text.gitignoreAppendedNote : text.gitignoreAlreadyPresentNote);
|
|
175
182
|
clack.note(text.d1Note);
|
|
176
183
|
clack.outro(text.outroTitle);
|
|
177
184
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
// Only run the wizard when executed directly (the bin entry point); importing this module for
|
|
186
|
+
// unit tests (e.g. `planDevVarsAndGitignore`) must not trigger the interactive CLI as a side effect.
|
|
187
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
188
|
+
main().catch((error) => {
|
|
189
|
+
console.error(error);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { KnowledgePage } from "../ingest/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reads `config.zenn` from `configPath`, scans the configured articles/
|
|
5
|
+
* directory, and writes the resulting `KnowledgePage[]` to `outputPath` as
|
|
6
|
+
* JSON. Throws if `config.zenn` is not set.
|
|
7
|
+
*/
|
|
8
|
+
export declare function syncZennSnapshot(configPath: string, outputPath: string): Promise<KnowledgePage[]>;
|
package/dist/sync/cli.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { scanZennArticles } from "../ingest/zenn.js";
|
|
4
|
+
/**
|
|
5
|
+
* Reads `config.zenn` from `configPath`, scans the configured articles/
|
|
6
|
+
* directory, and writes the resulting `KnowledgePage[]` to `outputPath` as
|
|
7
|
+
* JSON. Throws if `config.zenn` is not set.
|
|
8
|
+
*/
|
|
9
|
+
export async function syncZennSnapshot(configPath, outputPath) {
|
|
10
|
+
const config = JSON.parse(await readFile(configPath, "utf-8"));
|
|
11
|
+
if (!config.zenn) {
|
|
12
|
+
throw new Error("config.zenn is not set");
|
|
13
|
+
}
|
|
14
|
+
const pages = await scanZennArticles(config.zenn);
|
|
15
|
+
await writeFile(outputPath, JSON.stringify(pages, null, 2));
|
|
16
|
+
return pages;
|
|
17
|
+
}
|
|
18
|
+
async function main() {
|
|
19
|
+
const configPath = process.argv[2];
|
|
20
|
+
const outputPath = process.argv[3];
|
|
21
|
+
if (!configPath || !outputPath) {
|
|
22
|
+
console.error("usage: folio-agent-sync-zenn <config.json> <output.json>");
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const pages = await syncZennSnapshot(configPath, outputPath);
|
|
27
|
+
console.log(`wrote ${pages.length} zenn page(s) -> ${outputPath}`);
|
|
28
|
+
}
|
|
29
|
+
// Only run when executed directly (e.g. `folio-agent-sync-zenn ...`), not
|
|
30
|
+
// when imported by tests.
|
|
31
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
console.error(error);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
});
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@folio-agent/handler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Chat handler (LangGraph.js on Cloudflare Workers) + knowledge ingestion CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
],
|
|
13
13
|
"bin": {
|
|
14
14
|
"folio-agent-ingest": "dist/ingest/cli.js",
|
|
15
|
-
"folio-agent-init": "dist/init/cli.js"
|
|
15
|
+
"folio-agent-init": "dist/init/cli.js",
|
|
16
|
+
"folio-agent-sync-zenn": "dist/sync/cli.js"
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|