kcode-pi 0.1.0 → 0.1.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.
@@ -0,0 +1,48 @@
1
+ import { relative } from "node:path";
2
+ import type { SearchResult, TableSchema } from "./types.ts";
3
+
4
+ export function formatSearchResults(query: string, results: SearchResult[], basePath: string): string {
5
+ if (results.length === 0) {
6
+ return `No Kingdee knowledge results found for "${query}".`;
7
+ }
8
+
9
+ const lines = [`Kingdee knowledge results for "${query}":`, ""];
10
+ for (let i = 0; i < results.length; i++) {
11
+ const result = results[i];
12
+ lines.push(`[${i + 1}] ${result.section.heading}`);
13
+ lines.push(`Source: ${relative(basePath, result.file.path)}:${result.section.lineStart + 1}`);
14
+ lines.push(`Score: ${result.score.toFixed(2)}`);
15
+ lines.push(result.highlights[0] || result.section.content.trim().split("\n").slice(0, 5).join("\n"));
16
+ lines.push("");
17
+ }
18
+ return lines.join("\n").trim();
19
+ }
20
+
21
+ export function formatTableSchema(tableName: string, schema: TableSchema | undefined): string {
22
+ if (!schema) {
23
+ return `No table schema found for "${tableName}".`;
24
+ }
25
+
26
+ const lines = [
27
+ `Table: ${schema.name} (${tableName.toUpperCase()})`,
28
+ `Module: ${schema.module || "unknown"}`,
29
+ `Description: ${schema.description || "none"}`,
30
+ "",
31
+ "Fields:",
32
+ ];
33
+
34
+ for (const field of schema.fields) {
35
+ const nullable = field.nullable ? "nullable" : "required";
36
+ lines.push(`- ${field.name} ${field.type} ${nullable} - ${field.description}`);
37
+ }
38
+
39
+ if (schema.relatedTables.length > 0) {
40
+ lines.push("", "Related tables:");
41
+ for (const related of schema.relatedTables) {
42
+ lines.push(`- ${related.table}: ${related.relation} - ${related.description}`);
43
+ }
44
+ }
45
+
46
+ return lines.join("\n");
47
+ }
48
+
@@ -0,0 +1,147 @@
1
+ import { basename, extname, join } from "node:path";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import type { KnowledgeBase, KnowledgeFile, KnowledgeScope, KnowledgeSection, TableSchema } from "./types.ts";
4
+
5
+ interface CacheEntry {
6
+ mtime: number;
7
+ knowledge: KnowledgeBase;
8
+ }
9
+
10
+ const cache = new Map<string, CacheEntry>();
11
+
12
+ export function loadKnowledge(scope: KnowledgeScope, basePath: string): KnowledgeBase {
13
+ const cacheKey = `${basePath}:${scope}`;
14
+ const cached = cache.get(cacheKey);
15
+ if (cached && !isCacheStale(basePath, cached.mtime)) {
16
+ return cached.knowledge;
17
+ }
18
+
19
+ const commonPath = join(basePath, "common");
20
+ const editionPath = join(basePath, scope);
21
+ const knowledge: KnowledgeBase = {
22
+ common: loadDirectory(commonPath),
23
+ edition: loadDirectory(editionPath),
24
+ tables: loadTables(editionPath),
25
+ };
26
+
27
+ cache.set(cacheKey, { mtime: Date.now(), knowledge });
28
+ return knowledge;
29
+ }
30
+
31
+ function loadDirectory(dirPath: string): KnowledgeFile[] {
32
+ if (!existsSync(dirPath)) return [];
33
+
34
+ const files: KnowledgeFile[] = [];
35
+ for (const entry of readdirSync(dirPath)) {
36
+ const filePath = join(dirPath, entry);
37
+ const stat = statSync(filePath);
38
+ if (!stat.isFile()) continue;
39
+
40
+ const ext = extname(entry).toLowerCase();
41
+ if (ext !== ".md") continue;
42
+
43
+ const file = parseMarkdown(filePath, readFileSync(filePath, "utf8"), stat.mtime.toISOString());
44
+ files.push(file);
45
+ }
46
+ return files;
47
+ }
48
+
49
+ function parseMarkdown(filePath: string, content: string, lastModified: string): KnowledgeFile {
50
+ const lines = content.split("\n");
51
+ const sections: KnowledgeSection[] = [];
52
+ const tags: string[] = [];
53
+ let currentSection: Partial<KnowledgeSection> | undefined;
54
+ let title = "";
55
+
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i].replace(/\r$/, "");
58
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
59
+
60
+ if (headingMatch) {
61
+ if (currentSection) {
62
+ currentSection.lineEnd = i - 1;
63
+ sections.push(currentSection as KnowledgeSection);
64
+ }
65
+
66
+ const level = headingMatch[1].length;
67
+ const heading = headingMatch[2].trim();
68
+ if (level === 1 && !title) title = heading;
69
+ if (level <= 2) tags.push(heading);
70
+
71
+ currentSection = {
72
+ heading,
73
+ level,
74
+ content: "",
75
+ lineStart: i,
76
+ lineEnd: i,
77
+ };
78
+ } else if (currentSection) {
79
+ currentSection.content = `${currentSection.content ?? ""}${line}\n`;
80
+ }
81
+ }
82
+
83
+ if (currentSection) {
84
+ currentSection.lineEnd = lines.length - 1;
85
+ sections.push(currentSection as KnowledgeSection);
86
+ }
87
+
88
+ const fileName = basename(filePath, ".md");
89
+ tags.push(fileName);
90
+
91
+ return {
92
+ path: filePath,
93
+ title: title || fileName,
94
+ type: "markdown",
95
+ sections,
96
+ tags: [...new Set(tags)],
97
+ lastModified,
98
+ };
99
+ }
100
+
101
+ function loadTables(editionPath: string): Map<string, TableSchema> {
102
+ const tables = new Map<string, TableSchema>();
103
+ const tablesPath = join(editionPath, "tables.json");
104
+ if (!existsSync(tablesPath)) return tables;
105
+
106
+ const data = JSON.parse(readFileSync(tablesPath, "utf8")) as Record<string, unknown>;
107
+ for (const [tableName, tableData] of Object.entries(data)) {
108
+ const table = tableData as {
109
+ name?: string;
110
+ module?: string;
111
+ description?: string;
112
+ fields?: Array<{ name?: string; type?: string; nullable?: boolean; description?: string }>;
113
+ relatedTables?: Array<{ table?: string; relation?: string; description?: string }>;
114
+ };
115
+
116
+ tables.set(tableName.toUpperCase(), {
117
+ name: table.name || tableName,
118
+ module: table.module || "",
119
+ description: table.description || "",
120
+ fields: (table.fields || []).map((field) => ({
121
+ name: field.name || "",
122
+ type: field.type || "",
123
+ nullable: field.nullable ?? true,
124
+ description: field.description || "",
125
+ })),
126
+ relatedTables: (table.relatedTables || []).map((related) => ({
127
+ table: related.table || "",
128
+ relation: related.relation || "",
129
+ description: related.description || "",
130
+ })),
131
+ });
132
+ }
133
+
134
+ return tables;
135
+ }
136
+
137
+ function isCacheStale(knowledgePath: string, cachedMtime: number): boolean {
138
+ try {
139
+ return statSync(knowledgePath).mtimeMs > cachedMtime;
140
+ } catch {
141
+ return true;
142
+ }
143
+ }
144
+
145
+ export function clearKnowledgeCache(): void {
146
+ cache.clear();
147
+ }
@@ -0,0 +1,118 @@
1
+ import type { Edition, KnowledgeFile, KnowledgeScope, KnowledgeSection, SearchOptions, SearchResult, TableSchema } from "./types.ts";
2
+ import { loadKnowledge } from "./loader.ts";
3
+
4
+ export function searchKnowledge(query: string, options: SearchOptions = {}, basePath: string): SearchResult[] {
5
+ if (!query.trim()) return [];
6
+
7
+ const { tags, edition, scope, scopes, topK = 10, minScore = 0 } = options;
8
+ const targetScopes = normalizeScopes(scopes ?? [scope ?? edition ?? "flagship"]);
9
+ const results: SearchResult[] = [];
10
+
11
+ const seenFiles = new Set<string>();
12
+ for (const targetScope of targetScopes) {
13
+ const knowledge = loadKnowledge(targetScope, basePath);
14
+ for (const file of [...knowledge.common, ...knowledge.edition]) {
15
+ if (seenFiles.has(file.path)) continue;
16
+ seenFiles.add(file.path);
17
+ results.push(...searchFile(file, query, tags));
18
+ }
19
+ }
20
+
21
+ return results
22
+ .filter((result) => result.score >= minScore)
23
+ .sort((a, b) => b.score - a.score)
24
+ .slice(0, topK);
25
+ }
26
+
27
+ export function findTableSchema(tableName: string, edition: Edition, basePath: string): TableSchema | undefined {
28
+ const knowledge = loadKnowledge(edition, basePath);
29
+ return knowledge.tables.get(tableName.toUpperCase());
30
+ }
31
+
32
+ function normalizeScopes(scopes: KnowledgeScope[]): KnowledgeScope[] {
33
+ return [...new Set(scopes)];
34
+ }
35
+
36
+ function searchFile(file: KnowledgeFile, query: string, tags?: string[]): SearchResult[] {
37
+ if (tags?.length) {
38
+ const hasTag = tags.some((tag) => file.tags.some((fileTag) => fileTag.toLowerCase().includes(tag.toLowerCase())));
39
+ if (!hasTag) return [];
40
+ }
41
+
42
+ const queryTokens = tokenize(query);
43
+ const results: SearchResult[] = [];
44
+
45
+ for (const section of file.sections) {
46
+ const score = calculateScore(queryTokens, section, file);
47
+ if (score <= 0) continue;
48
+
49
+ results.push({
50
+ file,
51
+ section,
52
+ score,
53
+ highlights: highlightMatches(section.content, query),
54
+ });
55
+ }
56
+
57
+ return results;
58
+ }
59
+
60
+ export function tokenize(text: string): string[] {
61
+ return text
62
+ .toLowerCase()
63
+ .replace(/[^\w\u4e00-\u9fff]/g, " ")
64
+ .split(/\s+/)
65
+ .filter(Boolean);
66
+ }
67
+
68
+ function calculateScore(queryTokens: string[], section: KnowledgeSection, file: KnowledgeFile): number {
69
+ let score = 0;
70
+ const headingLower = section.heading.toLowerCase();
71
+ const contentLower = section.content.toLowerCase();
72
+ const titleLower = file.title.toLowerCase();
73
+ const fullQuery = queryTokens.join(" ");
74
+
75
+ if (fullQuery && titleLower.includes(fullQuery)) score += 5;
76
+ if (fullQuery && headingLower.includes(fullQuery)) score += 5;
77
+ if (fullQuery && contentLower.includes(fullQuery)) score += 5;
78
+
79
+ for (let i = 0; i < queryTokens.length; i++) {
80
+ const token = queryTokens[i];
81
+ const positionWeight = 1 + 1 / (i + 1);
82
+ if (titleLower.includes(token)) score += 3 * positionWeight;
83
+ if (headingLower.includes(token)) score += 2 * positionWeight;
84
+ if (contentLower.includes(token)) {
85
+ const matchCount = contentLower.split(token).length - 1;
86
+ score += (1 + Math.min(matchCount * 0.2, 2)) * positionWeight;
87
+ }
88
+ }
89
+
90
+ return score;
91
+ }
92
+
93
+ function highlightMatches(content: string, query: string): string[] {
94
+ const highlights: string[] = [];
95
+ const queryTokens = tokenize(query);
96
+ const lines = content.split("\n");
97
+ const matchedLines: number[] = [];
98
+
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const lineLower = lines[i].toLowerCase();
101
+ if (queryTokens.some((token) => lineLower.includes(token))) {
102
+ matchedLines.push(i);
103
+ }
104
+ }
105
+
106
+ const visited = new Set<number>();
107
+ for (const lineIdx of matchedLines) {
108
+ if (visited.has(lineIdx)) continue;
109
+ const start = Math.max(0, lineIdx - 2);
110
+ const end = Math.min(lines.length - 1, lineIdx + 2);
111
+ for (let i = start; i <= end; i++) visited.add(i);
112
+
113
+ const context = lines.slice(start, end + 1).join("\n").trim();
114
+ if (context) highlights.push(context);
115
+ }
116
+
117
+ return highlights.slice(0, 3);
118
+ }
@@ -0,0 +1,64 @@
1
+ export type KnowledgeScope = "enterprise" | "flagship" | "cosmic" | "xinghan" | "cangqiong";
2
+ export type Edition = Extract<KnowledgeScope, "enterprise" | "flagship">;
3
+
4
+ export type KnowledgeFileType = "markdown" | "json";
5
+
6
+ export interface KnowledgeFile {
7
+ path: string;
8
+ title: string;
9
+ type: KnowledgeFileType;
10
+ sections: KnowledgeSection[];
11
+ tags: string[];
12
+ lastModified: string;
13
+ }
14
+
15
+ export interface KnowledgeSection {
16
+ heading: string;
17
+ level: number;
18
+ content: string;
19
+ lineStart: number;
20
+ lineEnd: number;
21
+ }
22
+
23
+ export interface TableSchema {
24
+ name: string;
25
+ module: string;
26
+ description: string;
27
+ fields: FieldDefinition[];
28
+ relatedTables: RelatedTable[];
29
+ }
30
+
31
+ export interface FieldDefinition {
32
+ name: string;
33
+ type: string;
34
+ nullable: boolean;
35
+ description: string;
36
+ }
37
+
38
+ export interface RelatedTable {
39
+ table: string;
40
+ relation: string;
41
+ description: string;
42
+ }
43
+
44
+ export interface KnowledgeBase {
45
+ common: KnowledgeFile[];
46
+ edition: KnowledgeFile[];
47
+ tables: Map<string, TableSchema>;
48
+ }
49
+
50
+ export interface SearchResult {
51
+ file: KnowledgeFile;
52
+ section: KnowledgeSection;
53
+ score: number;
54
+ highlights: string[];
55
+ }
56
+
57
+ export interface SearchOptions {
58
+ tags?: string[];
59
+ edition?: Edition;
60
+ scope?: KnowledgeScope;
61
+ scopes?: KnowledgeScope[];
62
+ topK?: number;
63
+ minScore?: number;
64
+ }
@@ -0,0 +1,230 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { fileURLToPath } from "node:url";
6
+ import type { ProductProfile } from "../product/profile.ts";
7
+ import { readActiveRun } from "../harness/state.ts";
8
+ import { runArtifactPath } from "../harness/paths.ts";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ export type OfficialSkillKey = "ok-cosmic" | "ok-ksql";
13
+
14
+ export interface CommandSpec {
15
+ executable: string;
16
+ args: string[];
17
+ cwd: string;
18
+ display: string;
19
+ }
20
+
21
+ export interface CommandResult {
22
+ command: string;
23
+ exitCode: number;
24
+ stdout: string;
25
+ stderr: string;
26
+ }
27
+
28
+ export type OfficialEvidenceFile = "cosmic-config.txt" | "cosmic-metadata.json" | "cosmic-api.txt" | "ksql-lint.txt";
29
+
30
+ const SKILL_DIRS: Record<OfficialSkillKey, string> = {
31
+ "ok-cosmic": "ok-cosmic",
32
+ "ok-ksql": "ok-ksql",
33
+ };
34
+
35
+ const packageRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
36
+
37
+ export function isCosmicFamily(profile: ProductProfile): boolean {
38
+ return profile.platform === "cosmic";
39
+ }
40
+
41
+ export function officialSkillsSourceRoot(): string {
42
+ return officialSkillsSourceRoots()[0];
43
+ }
44
+
45
+ export function officialSkillsSourceRoots(): string[] {
46
+ return [
47
+ process.env.KCODE_KINGDEE_SKILLS_ROOT,
48
+ join(packageRoot, "vendor", "kingdee-skills"),
49
+ "E:\\projects\\kingdee\\skills",
50
+ ].filter((value): value is string => Boolean(value));
51
+ }
52
+
53
+ export function officialSkillsCacheRoot(cwd: string): string {
54
+ return join(cwd, ".pi", "kd", "official-skills");
55
+ }
56
+
57
+ export async function ensureOfficialSkillRoot(_cwd: string, skill: OfficialSkillKey): Promise<string> {
58
+ for (const sourceRoot of officialSkillsSourceRoots()) {
59
+ const expandedSource = join(sourceRoot, SKILL_DIRS[skill]);
60
+ if (existsSync(expandedSource)) return expandedSource;
61
+ }
62
+
63
+ throw new Error(`Official skill directory not found for ${skill}. Checked: ${officialSkillsSourceRoots().join(", ")}`);
64
+ }
65
+
66
+ export function resolveWorkspacePath(cwd: string, path: string): string {
67
+ return isAbsolute(path) ? path : resolve(cwd, path);
68
+ }
69
+
70
+ export function pythonExecutable(): string {
71
+ return process.env.KCODE_PYTHON ?? (process.platform === "win32" ? "python" : "python3");
72
+ }
73
+
74
+ export function buildPythonCommand(cwd: string, scriptPath: string, args: string[]): CommandSpec {
75
+ const executable = pythonExecutable();
76
+ const fullArgs = [scriptPath, ...args];
77
+ return {
78
+ executable,
79
+ args: fullArgs,
80
+ cwd,
81
+ display: formatCommand(executable, fullArgs),
82
+ };
83
+ }
84
+
85
+ export async function runCommand(command: CommandSpec, timeoutMs = 120_000): Promise<CommandResult> {
86
+ try {
87
+ const result = await execFileAsync(command.executable, command.args, {
88
+ cwd: command.cwd,
89
+ timeout: timeoutMs,
90
+ maxBuffer: 1024 * 1024 * 4,
91
+ });
92
+
93
+ return {
94
+ command: command.display,
95
+ exitCode: 0,
96
+ stdout: result.stdout ?? "",
97
+ stderr: result.stderr ?? "",
98
+ };
99
+ } catch (error) {
100
+ const err = error as {
101
+ code?: number | string;
102
+ stdout?: string;
103
+ stderr?: string;
104
+ message?: string;
105
+ };
106
+
107
+ return {
108
+ command: command.display,
109
+ exitCode: typeof err.code === "number" ? err.code : 1,
110
+ stdout: err.stdout ?? "",
111
+ stderr: err.stderr ?? err.message ?? "",
112
+ };
113
+ }
114
+ }
115
+
116
+ export async function cosmicConfigCommand(cwd: string, config?: string): Promise<CommandSpec> {
117
+ const root = await ensureOfficialSkillRoot(cwd, "ok-cosmic");
118
+ const script = join(root, "scripts", "cosmic-config-check.py");
119
+ const args = config ? ["--config", resolveWorkspacePath(cwd, config)] : [];
120
+ return buildPythonCommand(cwd, script, args);
121
+ }
122
+
123
+ export async function cosmicMetadataCommand(
124
+ cwd: string,
125
+ params: {
126
+ form: string;
127
+ config?: string;
128
+ fuzzy?: string;
129
+ typeFilter?: string;
130
+ sql?: boolean;
131
+ op?: boolean;
132
+ showDetail?: boolean;
133
+ },
134
+ ): Promise<CommandSpec> {
135
+ const root = await ensureOfficialSkillRoot(cwd, "ok-cosmic");
136
+ const script = join(root, "scripts", "cosmic-form-metadata.py");
137
+ const args = withConfig(["get", params.form], cwd, params.config);
138
+ if (params.sql) args.push("--sql");
139
+ if (params.op) args.push("--op");
140
+ if (params.showDetail) args.push("--show-detail");
141
+ if (params.fuzzy) args.push("--fuzzy", ...splitTerms(params.fuzzy));
142
+ if (params.typeFilter) args.push("--type", params.typeFilter);
143
+ return buildPythonCommand(cwd, script, args);
144
+ }
145
+
146
+ export async function cosmicApiCommand(
147
+ cwd: string,
148
+ params: {
149
+ mode: "search" | "search-method" | "detail";
150
+ query: string;
151
+ config?: string;
152
+ method?: string;
153
+ compact?: boolean;
154
+ },
155
+ ): Promise<CommandSpec> {
156
+ const root = await ensureOfficialSkillRoot(cwd, "ok-cosmic");
157
+ const script = join(root, "scripts", "cosmic-api-knowledge.py");
158
+ const args = withConfig([params.mode, params.query], cwd, params.config);
159
+ if (params.method) args.push("--method", params.method);
160
+ if (params.compact) args.push("--compact");
161
+ return buildPythonCommand(cwd, script, args);
162
+ }
163
+
164
+ export async function ksqlLintCommand(cwd: string, path: string): Promise<CommandSpec> {
165
+ const root = await ensureOfficialSkillRoot(cwd, "ok-ksql");
166
+ const script = join(root, "scripts", "ksql_lint.py");
167
+ return buildPythonCommand(cwd, script, [resolveWorkspacePath(cwd, path)]);
168
+ }
169
+
170
+ export function formatCommandResult(result: CommandResult): string {
171
+ return [
172
+ `Command: ${result.command}`,
173
+ `Exit: ${result.exitCode}`,
174
+ result.stdout.trim() ? `\nSTDOUT:\n${result.stdout.trim()}` : undefined,
175
+ result.stderr.trim() ? `\nSTDERR:\n${result.stderr.trim()}` : undefined,
176
+ ]
177
+ .filter(Boolean)
178
+ .join("\n");
179
+ }
180
+
181
+ export function writeOfficialEvidence(cwd: string, evidenceFile: OfficialEvidenceFile, result: CommandResult): string | undefined {
182
+ const run = readActiveRun(cwd);
183
+ if (!run) return undefined;
184
+
185
+ const path = runArtifactPath(cwd, run, join("evidence", evidenceFile));
186
+ mkdirSync(dirname(path), { recursive: true });
187
+
188
+ const content =
189
+ evidenceFile === "cosmic-metadata.json"
190
+ ? formatJsonEvidence(evidenceFile, result)
191
+ : `${formatCommandResult(result)}\nCaptured: ${new Date().toISOString()}\n`;
192
+ writeFileSync(path, content.endsWith("\n") ? content : `${content}\n`, "utf8");
193
+ return path;
194
+ }
195
+
196
+ function formatJsonEvidence(evidenceFile: OfficialEvidenceFile, result: CommandResult): string {
197
+ return `${JSON.stringify(
198
+ {
199
+ evidence: evidenceFile,
200
+ capturedAt: new Date().toISOString(),
201
+ command: result.command,
202
+ exitCode: result.exitCode,
203
+ stdout: result.stdout,
204
+ stderr: result.stderr,
205
+ },
206
+ null,
207
+ 2,
208
+ )}\n`;
209
+ }
210
+
211
+ function withConfig(args: string[], cwd: string, config?: string): string[] {
212
+ if (!config) return args;
213
+ return ["--config", resolveWorkspacePath(cwd, config), ...args];
214
+ }
215
+
216
+ function splitTerms(value: string): string[] {
217
+ return value
218
+ .split(/[,\s]+/)
219
+ .map((term) => term.trim())
220
+ .filter(Boolean);
221
+ }
222
+
223
+ function formatCommand(executable: string, args: string[]): string {
224
+ return [executable, ...args].map(quoteArg).join(" ");
225
+ }
226
+
227
+ function quoteArg(value: string): string {
228
+ if (!/[\s"'&|<>]/.test(value)) return value;
229
+ return `"${value.replace(/"/g, '\\"')}"`;
230
+ }