@folio-agent/handler 0.3.1 → 0.4.1
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/chat/graph.d.ts +7 -7
- package/dist/chat/graph.js +7 -8
- package/dist/chat/handler.js +3 -1
- package/dist/chat/rate-limit.js +5 -5
- package/dist/chat/types.d.ts +4 -2
- package/dist/chat/types.js +3 -2
- package/dist/ingest/cli.d.ts +1 -1
- package/dist/ingest/cli.js +13 -5
- package/dist/ingest/generate.js +23 -2
- package/dist/ingest/types.d.ts +7 -0
- package/dist/init/cli.d.ts +15 -1
- package/dist/init/cli.js +31 -17
- package/dist/sync/cli.d.ts +8 -0
- package/dist/sync/cli.js +42 -0
- package/package.json +8 -7
package/dist/chat/graph.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare function buildChatGraph(deps: ChatGraphDeps): import("@langchain/
|
|
|
11
11
|
route?: ChatRoute | undefined;
|
|
12
12
|
overLimit?: boolean | undefined;
|
|
13
13
|
answer?: string | undefined;
|
|
14
|
-
}, "__start__" | "
|
|
14
|
+
}, "__start__" | "generate" | "input_guard" | "log" | "route_message", {
|
|
15
15
|
input: import("@langchain/langgraph").LastValue<string>;
|
|
16
16
|
ip: import("@langchain/langgraph").LastValue<string>;
|
|
17
17
|
route: import("@langchain/langgraph").LastValue<ChatRoute | undefined>;
|
|
@@ -24,18 +24,18 @@ export declare function buildChatGraph(deps: ChatGraphDeps): import("@langchain/
|
|
|
24
24
|
overLimit: import("@langchain/langgraph").LastValue<boolean>;
|
|
25
25
|
answer: import("@langchain/langgraph").LastValue<string | undefined>;
|
|
26
26
|
}, import("@langchain/langgraph").StateDefinition, {
|
|
27
|
+
generate: {
|
|
28
|
+
answer: string;
|
|
29
|
+
};
|
|
27
30
|
input_guard: {
|
|
28
31
|
overLimit: true;
|
|
29
32
|
answer: string;
|
|
30
33
|
} | {
|
|
31
|
-
overLimit: false;
|
|
32
34
|
answer?: undefined;
|
|
35
|
+
overLimit: false;
|
|
33
36
|
};
|
|
37
|
+
log: {};
|
|
34
38
|
route_message: {
|
|
35
|
-
route: "
|
|
39
|
+
route: "inquiry" | "thoughts" | "works";
|
|
36
40
|
};
|
|
37
|
-
generate: {
|
|
38
|
-
answer: string;
|
|
39
|
-
};
|
|
40
|
-
log: {};
|
|
41
41
|
}, unknown, unknown, []>;
|
package/dist/chat/graph.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Annotation, END, START, StateGraph } from "@langchain/langgraph";
|
|
2
2
|
import { classifyRoute } from "./route.js";
|
|
3
3
|
import { DEFAULT_LANGUAGE } from "./types.js";
|
|
4
|
-
const RATE_LIMIT_REASON_TEXT = {
|
|
5
|
-
ja: { daily: "本日の上限", shortWindow: "直近の質問数の上限" },
|
|
6
|
-
en: { daily: "today's limit", shortWindow: "the recent question limit" },
|
|
7
|
-
};
|
|
8
4
|
const OVER_LIMIT_MESSAGE = {
|
|
9
|
-
ja: (
|
|
10
|
-
|
|
5
|
+
ja: (reason, config) => reason === "short_window"
|
|
6
|
+
? `${config.shortWindowMinutes}分間に${config.shortWindowMax}件までのご質問上限に達しました。少し時間を置いてから再度お試しください。お急ぎの場合はContactからお問い合わせください。`
|
|
7
|
+
: `${config.longWindowHours}時間に${config.longWindowMax}件までのご質問上限に達しました。時間を置いてから再度お試しください。お急ぎの場合はContactからお問い合わせください。`,
|
|
8
|
+
en: (reason, config) => reason === "short_window"
|
|
9
|
+
? `You've reached the limit of ${config.shortWindowMax} questions per ${config.shortWindowMinutes} minutes. Please try again in a little while. If it's urgent, please contact us via the Contact page.`
|
|
10
|
+
: `You've reached the limit of ${config.longWindowMax} questions per ${config.longWindowHours} hours. Please try again later. If it's urgent, please contact us via the Contact page.`,
|
|
11
11
|
};
|
|
12
12
|
const GENERATION_FAILED_MESSAGE = {
|
|
13
13
|
ja: "本日の受付上限に達したか、一時的な不具合が発生しています。恐れ入りますがContactからお問い合わせください。",
|
|
@@ -26,10 +26,9 @@ export function buildChatGraph(deps) {
|
|
|
26
26
|
.addNode("input_guard", async (state) => {
|
|
27
27
|
const result = await deps.checkRateLimit(state.ip);
|
|
28
28
|
if (!result.allowed) {
|
|
29
|
-
const reasonText = result.reason === "daily" ? RATE_LIMIT_REASON_TEXT[language].daily : RATE_LIMIT_REASON_TEXT[language].shortWindow;
|
|
30
29
|
return {
|
|
31
30
|
overLimit: true,
|
|
32
|
-
answer: OVER_LIMIT_MESSAGE[language](
|
|
31
|
+
answer: OVER_LIMIT_MESSAGE[language](result.reason, deps.rateLimitConfig),
|
|
33
32
|
};
|
|
34
33
|
}
|
|
35
34
|
return { overLimit: false };
|
package/dist/chat/handler.js
CHANGED
|
@@ -4,10 +4,12 @@ import { checkRateLimit } from "./rate-limit.js";
|
|
|
4
4
|
import { DEFAULT_RATE_LIMIT_CONFIG } from "./types.js";
|
|
5
5
|
const MAX_INPUT_LENGTH = 1000;
|
|
6
6
|
export function createChatHandler(config) {
|
|
7
|
+
const rateLimitConfig = config.rateLimitConfig ?? DEFAULT_RATE_LIMIT_CONFIG;
|
|
7
8
|
const graph = buildChatGraph({
|
|
8
|
-
checkRateLimit: (ip) => checkRateLimit(config.db, ip, new Date(),
|
|
9
|
+
checkRateLimit: (ip) => checkRateLimit(config.db, ip, new Date(), rateLimitConfig),
|
|
9
10
|
generateAnswer: config.generateAnswer,
|
|
10
11
|
logChat: (entry) => logChat(config.db, entry),
|
|
12
|
+
rateLimitConfig,
|
|
11
13
|
language: config.language,
|
|
12
14
|
});
|
|
13
15
|
return async (request) => {
|
package/dist/chat/rate-limit.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_RATE_LIMIT_CONFIG } from "./types.js";
|
|
2
2
|
const MINUTE_MS = 60_000;
|
|
3
|
-
const
|
|
3
|
+
const HOUR_MS = 60 * MINUTE_MS;
|
|
4
4
|
async function countSince(db, ip, sinceIso) {
|
|
5
5
|
const row = await db
|
|
6
6
|
.prepare("SELECT COUNT(*) as count FROM chat_logs WHERE ip = ? AND created_at >= ? AND over_limit = 0")
|
|
@@ -14,10 +14,10 @@ export async function checkRateLimit(db, ip, now = new Date(), config = DEFAULT_
|
|
|
14
14
|
if (shortCount >= config.shortWindowMax) {
|
|
15
15
|
return { allowed: false, reason: "short_window" };
|
|
16
16
|
}
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
return { allowed: false, reason: "
|
|
17
|
+
const longWindowStart = new Date(now.getTime() - config.longWindowHours * HOUR_MS).toISOString();
|
|
18
|
+
const longWindowCount = await countSince(db, ip, longWindowStart);
|
|
19
|
+
if (longWindowCount >= config.longWindowMax) {
|
|
20
|
+
return { allowed: false, reason: "long_window" };
|
|
21
21
|
}
|
|
22
22
|
return { allowed: true };
|
|
23
23
|
}
|
package/dist/chat/types.d.ts
CHANGED
|
@@ -4,10 +4,11 @@ export declare const DEFAULT_LANGUAGE: Language;
|
|
|
4
4
|
export interface RateLimitConfig {
|
|
5
5
|
shortWindowMinutes: number;
|
|
6
6
|
shortWindowMax: number;
|
|
7
|
-
|
|
7
|
+
longWindowHours: number;
|
|
8
|
+
longWindowMax: number;
|
|
8
9
|
}
|
|
9
10
|
export declare const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig;
|
|
10
|
-
export type RateLimitReason = "short_window" | "
|
|
11
|
+
export type RateLimitReason = "short_window" | "long_window";
|
|
11
12
|
export interface RateLimitResult {
|
|
12
13
|
allowed: boolean;
|
|
13
14
|
reason?: RateLimitReason;
|
|
@@ -23,5 +24,6 @@ export interface ChatGraphDeps {
|
|
|
23
24
|
checkRateLimit: (ip: string) => Promise<RateLimitResult>;
|
|
24
25
|
generateAnswer: (input: string, route: ChatRoute) => Promise<string>;
|
|
25
26
|
logChat: (entry: ChatLogEntry) => Promise<void>;
|
|
27
|
+
rateLimitConfig: RateLimitConfig;
|
|
26
28
|
language?: Language;
|
|
27
29
|
}
|
package/dist/chat/types.js
CHANGED
package/dist/ingest/cli.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export
|
|
2
|
+
export declare function main(): Promise<void>;
|
package/dist/ingest/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { generateKnowledge } from "./generate.js";
|
|
4
|
-
async function main() {
|
|
6
|
+
export async function main() {
|
|
5
7
|
const configPath = process.argv[2];
|
|
6
8
|
const outputPath = process.argv[3];
|
|
7
9
|
if (!configPath || !outputPath) {
|
|
@@ -17,7 +19,13 @@ async function main() {
|
|
|
17
19
|
console.warn(`warning: ${warning}`);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Only run when executed directly (e.g. `folio-agent-ingest ...`), not when imported by tests.
|
|
23
|
+
// Compares realpaths, not raw paths: npm's node_modules/.bin/ entries are symlinks, so
|
|
24
|
+
// process.argv[1] stays the symlink path while import.meta.url is already the resolved target
|
|
25
|
+
// (see sync/cli.ts for the fuller rationale).
|
|
26
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {
|
|
27
|
+
main().catch((error) => {
|
|
28
|
+
console.error(error);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
});
|
|
31
|
+
}
|
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,16 @@
|
|
|
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;
|
|
16
|
+
export declare function main(): Promise<void>;
|
package/dist/init/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { existsSync, realpathSync, 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;
|
|
@@ -84,7 +96,7 @@ function seedDefaults(previous) {
|
|
|
84
96
|
apiRoutePath: undefined,
|
|
85
97
|
};
|
|
86
98
|
}
|
|
87
|
-
async function main() {
|
|
99
|
+
export async function main() {
|
|
88
100
|
const previousConfig = await readJson(CONFIG_PATH);
|
|
89
101
|
const answers = await runWizard(seedDefaults(previousConfig));
|
|
90
102
|
const text = CLI_TEXT[answers.language];
|
|
@@ -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,18 @@ 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
|
+
// Compares realpaths, not raw paths: npm's node_modules/.bin/ entries are symlinks, so
|
|
188
|
+
// process.argv[1] stays the symlink path while import.meta.url is already the resolved target
|
|
189
|
+
// (see sync/cli.ts for the fuller rationale).
|
|
190
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
console.error(error);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
@@ -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,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { scanZennArticles } from "../ingest/zenn.js";
|
|
6
|
+
/**
|
|
7
|
+
* Reads `config.zenn` from `configPath`, scans the configured articles/
|
|
8
|
+
* directory, and writes the resulting `KnowledgePage[]` to `outputPath` as
|
|
9
|
+
* JSON. Throws if `config.zenn` is not set.
|
|
10
|
+
*/
|
|
11
|
+
export async function syncZennSnapshot(configPath, outputPath) {
|
|
12
|
+
const config = JSON.parse(await readFile(configPath, "utf-8"));
|
|
13
|
+
if (!config.zenn) {
|
|
14
|
+
throw new Error("config.zenn is not set");
|
|
15
|
+
}
|
|
16
|
+
const pages = await scanZennArticles(config.zenn);
|
|
17
|
+
await writeFile(outputPath, JSON.stringify(pages, null, 2));
|
|
18
|
+
return pages;
|
|
19
|
+
}
|
|
20
|
+
async function main() {
|
|
21
|
+
const configPath = process.argv[2];
|
|
22
|
+
const outputPath = process.argv[3];
|
|
23
|
+
if (!configPath || !outputPath) {
|
|
24
|
+
console.error("usage: folio-agent-sync-zenn <config.json> <output.json>");
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const pages = await syncZennSnapshot(configPath, outputPath);
|
|
29
|
+
console.log(`wrote ${pages.length} zenn page(s) -> ${outputPath}`);
|
|
30
|
+
}
|
|
31
|
+
// Only run when executed directly (e.g. `folio-agent-sync-zenn ...`), not
|
|
32
|
+
// when imported by tests. Compares realpaths: npm's node_modules/.bin/
|
|
33
|
+
// entries are symlinks, so process.argv[1] stays the symlink path while
|
|
34
|
+
// import.meta.url is already the resolved target — a plain string/URL
|
|
35
|
+
// comparison of the two (matching ingest/cli.ts as-is) never matches, and
|
|
36
|
+
// main() silently never runs when invoked via the published bin name.
|
|
37
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {
|
|
38
|
+
main().catch((error) => {
|
|
39
|
+
console.error(error);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
});
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@folio-agent/handler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -39,14 +40,14 @@
|
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"@clack/prompts": "^1.7.0",
|
|
42
|
-
"@google/genai": "^2.
|
|
43
|
-
"@langchain/langgraph": "^1.4.
|
|
44
|
-
"node-html-parser": "^
|
|
43
|
+
"@google/genai": "^2.15.0",
|
|
44
|
+
"@langchain/langgraph": "^1.4.8",
|
|
45
|
+
"node-html-parser": "^9.0.1",
|
|
45
46
|
"picomatch": "^4.0.5"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@cloudflare/workers-types": "^5.
|
|
49
|
+
"@cloudflare/workers-types": "^5.20260801.1",
|
|
49
50
|
"@types/picomatch": "^4.0.2",
|
|
50
|
-
"wrangler": "^4.
|
|
51
|
+
"wrangler": "^4.118.0"
|
|
51
52
|
}
|
|
52
53
|
}
|