project-knowledge 1.0.0 → 1.0.2
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 +22 -5
- package/_site/_test/package-startup-test.js +108 -0
- package/_site/index.html +53 -12
- package/_site/lib/job-orchestrator.js +3 -2
- package/_site/server.js +52 -11
- package/ai-profiles.json +17 -9
- package/bin/project-knowledge.js +51 -0
- package/package.json +5 -1
- package/scripts/gen-commit-doc.ps1 +1 -1
- package/scripts/list-features.ps1 +1 -1
- package/scripts/register-scheduled-task.bat +3 -1
package/README.md
CHANGED
|
@@ -60,9 +60,8 @@ Requirements:
|
|
|
60
60
|
- Git on `PATH`
|
|
61
61
|
- (Optional) [Claude Code CLI](https://docs.claude.com/en/docs/claude-code)
|
|
62
62
|
if you want the Claude adapter (the `mock-agent` adapter works without it)
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
the dashboard and AI flows but the Git-doc generator needs porting.
|
|
63
|
+
- Windows has the most complete hook/scheduled-task flow. The dashboard,
|
|
64
|
+
registry, AI profiles, and knowledge-store APIs use install-relative paths.
|
|
66
65
|
|
|
67
66
|
## Quick start
|
|
68
67
|
|
|
@@ -71,6 +70,7 @@ Global install:
|
|
|
71
70
|
```bash
|
|
72
71
|
npm install -g project-knowledge
|
|
73
72
|
project-knowledge # starts server on http://localhost:7777
|
|
73
|
+
project-knowledge start --port 9000
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
Or run from source:
|
|
@@ -88,11 +88,28 @@ Then open <http://localhost:7777>. Override the port with
|
|
|
88
88
|
### First-time setup
|
|
89
89
|
|
|
90
90
|
1. Open the dashboard → **Projects** → register a project (path + slug).
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
On first run, v1.0.1 creates missing runtime JSON files automatically. If
|
|
92
|
+
`projects.json` or `ai-profiles.json` is empty or invalid, the server recovers
|
|
93
|
+
with a safe default; invalid JSON is backed up as `*.invalid-<timestamp>.bak`.
|
|
94
|
+
|
|
95
|
+
2. Open **Settings** / **AI profiles** to add MiniMax / GLM / GPT /
|
|
96
|
+
Anthropic-compatible models. The npm package ships with a safe mock profile
|
|
97
|
+
and no API keys.
|
|
93
98
|
3. Run **Scan** → **Initial analysis** → review drafts → **Apply drafts**.
|
|
94
99
|
4. (Optional) **Install hook** so future commits trigger an analysis job.
|
|
95
100
|
|
|
101
|
+
### Upgrading from v1.0.0
|
|
102
|
+
|
|
103
|
+
v1.0.0 did not publish a `bin` command, so a global install could complete
|
|
104
|
+
without creating the `project-knowledge` executable. Reinstall v1.0.1:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm uninstall -g project-knowledge
|
|
108
|
+
npm install -g project-knowledge@latest
|
|
109
|
+
project-knowledge --version
|
|
110
|
+
project-knowledge
|
|
111
|
+
```
|
|
112
|
+
|
|
96
113
|
## Architecture
|
|
97
114
|
|
|
98
115
|
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Run: node _site/_test/package-startup-test.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawn, spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
7
|
+
const BIN = path.join(ROOT, 'bin', 'project-knowledge.js');
|
|
8
|
+
const PROJECTS_JSON = path.join(ROOT, 'projects.json');
|
|
9
|
+
const PORT = process.env.KB_PACKAGE_TEST_PORT || '7825';
|
|
10
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
11
|
+
|
|
12
|
+
function assert(cond, msg) { if (!cond) throw new Error(msg); }
|
|
13
|
+
|
|
14
|
+
function backup(file) {
|
|
15
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function restore(file, content) {
|
|
19
|
+
if (content == null) fs.rmSync(file, { force: true });
|
|
20
|
+
else fs.writeFileSync(file, content, 'utf-8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function waitForServer() {
|
|
24
|
+
const deadline = Date.now() + 15000;
|
|
25
|
+
let lastError;
|
|
26
|
+
while (Date.now() < deadline) {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${BASE_URL}/api/state`);
|
|
29
|
+
if (res.ok) return res.json();
|
|
30
|
+
lastError = new Error(`HTTP ${res.status}`);
|
|
31
|
+
} catch (e) { lastError = e; }
|
|
32
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
33
|
+
}
|
|
34
|
+
throw lastError || new Error('server did not start');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function stop(child) {
|
|
38
|
+
if (!child || child.killed) return;
|
|
39
|
+
child.kill('SIGTERM');
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
41
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function withServer(fn) {
|
|
45
|
+
const child = spawn(process.execPath, [BIN, 'start', '--port', PORT, '--host', '127.0.0.1'], {
|
|
46
|
+
cwd: ROOT,
|
|
47
|
+
env: { ...process.env },
|
|
48
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
});
|
|
51
|
+
let output = '';
|
|
52
|
+
child.stdout.on('data', d => { output += d.toString(); });
|
|
53
|
+
child.stderr.on('data', d => { output += d.toString(); });
|
|
54
|
+
try {
|
|
55
|
+
const state = await waitForServer();
|
|
56
|
+
await fn(state, output);
|
|
57
|
+
} finally {
|
|
58
|
+
await stop(child);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
(async () => {
|
|
63
|
+
const originalProjects = backup(PROJECTS_JSON);
|
|
64
|
+
const originalInvalidBackups = new Set(
|
|
65
|
+
fs.readdirSync(ROOT).filter(name => name.startsWith('projects.json.invalid-') && name.endsWith('.bak'))
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const version = spawnSync(process.execPath, [BIN, '--version'], { cwd: ROOT, encoding: 'utf-8' });
|
|
70
|
+
assert(version.status === 0, '--version should exit successfully');
|
|
71
|
+
assert(version.stdout.trim() === require(path.join(ROOT, 'package.json')).version, '--version should match package version');
|
|
72
|
+
|
|
73
|
+
fs.rmSync(PROJECTS_JSON, { force: true });
|
|
74
|
+
await withServer(async (state) => {
|
|
75
|
+
assert(state && state.projects && typeof state.projects === 'object', 'missing projects.json should still return projects object');
|
|
76
|
+
assert(fs.existsSync(PROJECTS_JSON), 'missing projects.json should be created on first API read');
|
|
77
|
+
assert(fs.readFileSync(PROJECTS_JSON, 'utf-8').trim() === '{}', 'missing projects.json should initialize to {}');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
fs.writeFileSync(PROJECTS_JSON, '', 'utf-8');
|
|
81
|
+
await withServer(async (state) => {
|
|
82
|
+
assert(state && state.projects && typeof state.projects === 'object', 'empty projects.json should still return projects object');
|
|
83
|
+
assert(fs.readFileSync(PROJECTS_JSON, 'utf-8').trim() === '{}', 'empty projects.json should be normalized to {}');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync(PROJECTS_JSON, '{', 'utf-8');
|
|
87
|
+
await withServer(async (state) => {
|
|
88
|
+
assert(state && state.projects && typeof state.projects === 'object', 'invalid projects.json should still return projects object');
|
|
89
|
+
assert(fs.readFileSync(PROJECTS_JSON, 'utf-8').trim() === '{}', 'invalid projects.json should be replaced with {}');
|
|
90
|
+
const newBackups = fs.readdirSync(ROOT)
|
|
91
|
+
.filter(name => name.startsWith('projects.json.invalid-') && name.endsWith('.bak'))
|
|
92
|
+
.filter(name => !originalInvalidBackups.has(name));
|
|
93
|
+
assert(newBackups.length >= 1, 'invalid projects.json should be backed up before recovery');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log('package-startup-test PASS');
|
|
97
|
+
} finally {
|
|
98
|
+
restore(PROJECTS_JSON, originalProjects);
|
|
99
|
+
for (const name of fs.readdirSync(ROOT)) {
|
|
100
|
+
if (name.startsWith('projects.json.invalid-') && name.endsWith('.bak') && !originalInvalidBackups.has(name)) {
|
|
101
|
+
fs.rmSync(path.join(ROOT, name), { force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})().catch(err => {
|
|
106
|
+
console.error(err && err.stack || err);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
package/_site/index.html
CHANGED
|
@@ -422,11 +422,12 @@
|
|
|
422
422
|
<h2 class="text-lg font-semibold">{{ t("importProject") }}</h2>
|
|
423
423
|
<p class="mt-1 text-sm muted">{{ t("importHelp") }}</p>
|
|
424
424
|
<div class="mt-5 grid gap-4">
|
|
425
|
-
<label class="grid gap-1 text-sm">{{ t("slug") }}
|
|
426
|
-
<input v-model="form.slug" required pattern="[a-z0-9][a-z0-9\-]*" class="input font-mono" placeholder="my-project" />
|
|
427
|
-
</label>
|
|
428
425
|
<label class="grid gap-1 text-sm">{{ t("displayName") }}
|
|
429
|
-
<input v-model="form.displayName" required class="input" placeholder="
|
|
426
|
+
<input v-model="form.displayName" required class="input" :placeholder="t('displayNamePlaceholder')" />
|
|
427
|
+
</label>
|
|
428
|
+
<label class="grid gap-1 text-sm">{{ t("slug") }} <span class="muted">{{ t("optional") }}</span>
|
|
429
|
+
<input v-model="form.slug" class="input font-mono" placeholder="my-project" />
|
|
430
|
+
<span class="text-xs muted">{{ t("slugHelp") }} <code>{{ importSlugPreview }}</code></span>
|
|
430
431
|
</label>
|
|
431
432
|
<label class="grid gap-1 text-sm">{{ t("localPath") }}
|
|
432
433
|
<input v-model="form.localPath" required class="input font-mono text-xs" placeholder="D:\SanQian.Xu\my-project" />
|
|
@@ -484,7 +485,7 @@
|
|
|
484
485
|
<aside class="panel rounded-xl border p-5">
|
|
485
486
|
<h2 class="font-semibold">{{ t("importTarget") }}</h2>
|
|
486
487
|
<div class="mt-4 space-y-3 text-sm muted">
|
|
487
|
-
<p>{{ t("kbPath") }}: <code>{{ displayProjectKbPath(
|
|
488
|
+
<p>{{ t("kbPath") }}: <code>{{ displayProjectKbPath(importSlugPreview || "<slug>") }}</code></p>
|
|
488
489
|
<p>{{ t("schemaNormalized") }}</p>
|
|
489
490
|
<p>{{ t("gitPopulate") }}</p>
|
|
490
491
|
</div>
|
|
@@ -1044,6 +1045,10 @@ const I18N = {
|
|
|
1044
1045
|
reset: "Reset",
|
|
1045
1046
|
slug: "Slug",
|
|
1046
1047
|
displayName: "Display name",
|
|
1048
|
+
displayNamePlaceholder: "Tokens Consumption Leaderboard",
|
|
1049
|
+
optional: "(optional)",
|
|
1050
|
+
slugHelp: "Internal id for URL and folders. Leave blank to auto-generate:",
|
|
1051
|
+
invalidSlug: "Internal id must be lowercase letters, numbers, and hyphens.",
|
|
1047
1052
|
localPath: "Local path",
|
|
1048
1053
|
gitPath: "Git path",
|
|
1049
1054
|
primaryLanguage: "Primary language",
|
|
@@ -1265,6 +1270,10 @@ const I18N = {
|
|
|
1265
1270
|
reset: "重置",
|
|
1266
1271
|
slug: "标识",
|
|
1267
1272
|
displayName: "显示名称",
|
|
1273
|
+
displayNamePlaceholder: "Tokens Consumption Leaderboard",
|
|
1274
|
+
optional: "(可选)",
|
|
1275
|
+
slugHelp: "URL 和目录使用的内部标识。留空会自动生成:",
|
|
1276
|
+
invalidSlug: "内部标识只能使用小写字母、数字和连字符。",
|
|
1268
1277
|
localPath: "本地路径",
|
|
1269
1278
|
gitPath: "Git 路径",
|
|
1270
1279
|
primaryLanguage: "主要语言",
|
|
@@ -1619,6 +1628,36 @@ createApp({
|
|
|
1619
1628
|
return String(value || "").replace(/[\\\/]+$/, "");
|
|
1620
1629
|
}
|
|
1621
1630
|
|
|
1631
|
+
function basenameFromPath(value) {
|
|
1632
|
+
return String(value || "").split(/[\\\/]+/).filter(Boolean).pop() || "";
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function normalizeSlug(value) {
|
|
1636
|
+
return String(value || "")
|
|
1637
|
+
.trim()
|
|
1638
|
+
.toLowerCase()
|
|
1639
|
+
.replace(/['"]/g, "")
|
|
1640
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1641
|
+
.replace(/^-+|-+$/g, "")
|
|
1642
|
+
.replace(/-{2,}/g, "-")
|
|
1643
|
+
.slice(0, 41)
|
|
1644
|
+
.replace(/-+$/g, "");
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function uniqueSlug(base) {
|
|
1648
|
+
const fallback = `project-${Date.now().toString(36)}`;
|
|
1649
|
+
const root = normalizeSlug(base) || fallback;
|
|
1650
|
+
let candidate = root;
|
|
1651
|
+
let index = 2;
|
|
1652
|
+
while (projects.value && projects.value[candidate]) {
|
|
1653
|
+
const suffix = `-${index++}`;
|
|
1654
|
+
candidate = `${root.slice(0, Math.max(1, 41 - suffix.length))}${suffix}`;
|
|
1655
|
+
}
|
|
1656
|
+
return candidate;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const importSlugPreview = computed(() => uniqueSlug(form.slug || form.displayName || basenameFromPath(form.localPath)));
|
|
1660
|
+
|
|
1622
1661
|
function projectKbPath(slug) {
|
|
1623
1662
|
const root = trimTrailingSlash(knowledgeStoreConfig.rootPath || `${kbRoot.value}\\projects`);
|
|
1624
1663
|
return `${root}\\${slug}`;
|
|
@@ -2567,7 +2606,9 @@ createApp({
|
|
|
2567
2606
|
formOk.value = "";
|
|
2568
2607
|
submitting.value = true;
|
|
2569
2608
|
try {
|
|
2570
|
-
|
|
2609
|
+
const slug = uniqueSlug(form.slug || form.displayName || basenameFromPath(form.localPath));
|
|
2610
|
+
if (!/^[a-z0-9][a-z0-9-]{0,40}$/.test(slug)) throw new Error(t("invalidSlug"));
|
|
2611
|
+
form.slug = slug;
|
|
2571
2612
|
const config = {
|
|
2572
2613
|
displayName: form.displayName,
|
|
2573
2614
|
localPath: form.localPath,
|
|
@@ -2576,7 +2617,7 @@ createApp({
|
|
|
2576
2617
|
tags: form.tagsStr.split(",").map(s => s.trim()).filter(Boolean),
|
|
2577
2618
|
isReference: !!form.isReference,
|
|
2578
2619
|
docConvention: "frontmatter-relations",
|
|
2579
|
-
kbPath: projectKbPath(
|
|
2620
|
+
kbPath: projectKbPath(slug),
|
|
2580
2621
|
enabled: true,
|
|
2581
2622
|
aiProfileId: aiConfig.defaultProfileId || "mock-agent",
|
|
2582
2623
|
knowledgeLanguage: form.knowledgeLanguage || "zh-CN",
|
|
@@ -2584,7 +2625,7 @@ createApp({
|
|
|
2584
2625
|
goalStatus: "not-created"
|
|
2585
2626
|
};
|
|
2586
2627
|
await api("PUT", "/api/projects", {
|
|
2587
|
-
slug
|
|
2628
|
+
slug,
|
|
2588
2629
|
config,
|
|
2589
2630
|
importOptions: {
|
|
2590
2631
|
initGit: !!form.initGit,
|
|
@@ -2592,10 +2633,10 @@ createApp({
|
|
|
2592
2633
|
remoteUrl: form.remoteUrl || "",
|
|
2593
2634
|
},
|
|
2594
2635
|
});
|
|
2595
|
-
if (form.initNow) await api("POST", `/api/projects/${
|
|
2596
|
-
formOk.value = `Imported ${form.slug}.`;
|
|
2636
|
+
if (form.initNow) await api("POST", `/api/projects/${slug}/init`);
|
|
2637
|
+
formOk.value = `Imported ${form.displayName || slug}.`;
|
|
2597
2638
|
await refreshAll();
|
|
2598
|
-
selectProject(
|
|
2639
|
+
selectProject(slug);
|
|
2599
2640
|
activeView.value = "dashboard";
|
|
2600
2641
|
} catch (e) {
|
|
2601
2642
|
formError.value = e.message;
|
|
@@ -2688,7 +2729,7 @@ createApp({
|
|
|
2688
2729
|
summaryPanel, pendingCommitItems, supervisionIssues, issuesByProject, structuredLogs, selectedLog, logFilters,
|
|
2689
2730
|
knowledgeStoreConfig, knowledgeMigrationPlan, loggingConfig,
|
|
2690
2731
|
loading, pollError, activeView, selectedSlug, selectedProject, pageTitle, projectList, summaryCards, navItems,
|
|
2691
|
-
theme, uiLanguage, t, toggleTheme, form, submitting, formError, formOk, addProject, resetForm, busySlug, initProject,
|
|
2732
|
+
theme, uiLanguage, t, toggleTheme, form, importSlugPreview, submitting, formError, formOk, addProject, resetForm, busySlug, initProject,
|
|
2692
2733
|
freq, scheduleMsg, applySchedule, deleteSchedule,
|
|
2693
2734
|
runs, selectedRunId, selectedRun, drafts, filteredDrafts, draftBranches, draftBranchFilter, applyDraftButtonLabel, draftSelection, previewDraftPath, draftPreview, allowGoalEdit,
|
|
2694
2735
|
removeDialog, removeProjectDisabled,
|
|
@@ -35,7 +35,8 @@ const { spawn } = require('child_process');
|
|
|
35
35
|
const { scanProject, applyScanResult } = require('./scanner');
|
|
36
36
|
const { runInitialAnalysis, runCommitAnalysis } = require('./analysis-orchestrator');
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const APP_ROOT = path.resolve(__dirname, '..', '..');
|
|
39
|
+
const LEGACY_SCRIPT = path.join(APP_ROOT, 'scripts', 'gen-commit-doc.ps1');
|
|
39
40
|
|
|
40
41
|
const KNOWN_MODES = new Set(['legacy', 'scan', 'analyze-initial', 'analyze-commits', 'safe']);
|
|
41
42
|
|
|
@@ -90,7 +91,7 @@ function projectList(projects, slug) {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function legacyDefaultProjectKbPath(slug) {
|
|
93
|
-
return path.join(
|
|
94
|
+
return path.join(APP_ROOT, 'projects', slug);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
async function runScan(projects, slug, job) {
|
package/_site/server.js
CHANGED
|
@@ -29,11 +29,12 @@ const SITE_ROOT = __dirname;
|
|
|
29
29
|
const PORT = parseInt(process.env.KB_SITE_PORT || '7777', 10);
|
|
30
30
|
const HOST = process.env.KB_SITE_HOST || '127.0.0.1';
|
|
31
31
|
const TASK_NAME = 'KB-GitCommits-Daily';
|
|
32
|
-
const SCRIPT = '
|
|
33
|
-
const SAFE_RUNNER = '
|
|
32
|
+
const SCRIPT = path.join(KB_ROOT, 'scripts', 'gen-commit-doc.ps1');
|
|
33
|
+
const SAFE_RUNNER = path.join(SITE_ROOT, 'scripts', 'safe-runner.js');
|
|
34
34
|
const PROJECT_SCHEMA_VERSION = 'v3';
|
|
35
35
|
const DEFAULT_AI_PROFILE_ID = 'mock-agent';
|
|
36
36
|
const DEFAULT_KNOWLEDGE_LANGUAGE = 'zh-CN';
|
|
37
|
+
const PROJECTS_PATH = path.join(KB_ROOT, 'projects.json');
|
|
37
38
|
const AI_PROFILES_PATH = path.join(KB_ROOT, 'ai-profiles.json');
|
|
38
39
|
const JOBS_LOG_PATH = path.join(KB_ROOT, '.jobs-log.json');
|
|
39
40
|
const KNOWLEDGE_STORE_PATH = path.join(KB_ROOT, 'knowledge-store.json');
|
|
@@ -46,7 +47,7 @@ const runningJobs = new Map();
|
|
|
46
47
|
// Read a fresh copy of projects.json (re-loaded on every dispatch so that
|
|
47
48
|
// background jobs see the latest registry state).
|
|
48
49
|
function readProjectsForJob() {
|
|
49
|
-
return
|
|
50
|
+
return readProjects({ persistMigrations: true });
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
// ---- helpers ----
|
|
@@ -77,9 +78,43 @@ function readJson(p) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function writeJson(p, obj) {
|
|
81
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
80
82
|
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
function backupInvalidJson(filePath, raw) {
|
|
86
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
87
|
+
const backupPath = `${filePath}.invalid-${stamp}.bak`;
|
|
88
|
+
try {
|
|
89
|
+
fs.writeFileSync(backupPath, raw, 'utf-8');
|
|
90
|
+
} catch {}
|
|
91
|
+
return backupPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readJsonOrDefault(filePath, defaultValue, options = {}) {
|
|
95
|
+
const { persistDefault = true, backupInvalid = true } = options;
|
|
96
|
+
let raw = '';
|
|
97
|
+
try {
|
|
98
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
101
|
+
if (persistDefault) writeJson(filePath, defaultValue);
|
|
102
|
+
return defaultValue;
|
|
103
|
+
}
|
|
104
|
+
if (!raw.trim()) {
|
|
105
|
+
if (persistDefault) writeJson(filePath, defaultValue);
|
|
106
|
+
return defaultValue;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(raw);
|
|
110
|
+
} catch {
|
|
111
|
+
const backupPath = backupInvalid ? backupInvalidJson(filePath, raw) : '';
|
|
112
|
+
if (persistDefault) writeJson(filePath, defaultValue);
|
|
113
|
+
console.warn(`[project-knowledge] Recovered invalid JSON at ${filePath}${backupPath ? `; backup: ${backupPath}` : ''}`);
|
|
114
|
+
return defaultValue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
83
118
|
function isSafeSlug(s) { return typeof s === 'string' && /^[a-z0-9][a-z0-9-]{0,40}$/.test(s); }
|
|
84
119
|
|
|
85
120
|
function normalizeKnowledgeLanguage(value) {
|
|
@@ -118,9 +153,8 @@ function isLegacyKbPath(value) {
|
|
|
118
153
|
}
|
|
119
154
|
|
|
120
155
|
// ---- AI profiles (TASK-005) ----
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
return { schema: 'ai-profiles/v1', defaultProfileId: DEFAULT_AI_PROFILE_ID, profiles: listAdapters().map(a => ({
|
|
156
|
+
function defaultAiProfilesConfig() {
|
|
157
|
+
return { schema: 'ai-profiles/v1', defaultProfileId: DEFAULT_AI_PROFILE_ID, profiles: listAdapters().map(a => ({
|
|
124
158
|
id: a.id,
|
|
125
159
|
name: a.name,
|
|
126
160
|
description: a.description,
|
|
@@ -136,8 +170,13 @@ function readAiProfiles() {
|
|
|
136
170
|
timeoutMs: 300000,
|
|
137
171
|
} : {}),
|
|
138
172
|
})) };
|
|
139
|
-
|
|
140
|
-
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readAiProfiles() {
|
|
176
|
+
return readJsonOrDefault(AI_PROFILES_PATH, defaultAiProfilesConfig(), {
|
|
177
|
+
persistDefault: true,
|
|
178
|
+
backupInvalid: true,
|
|
179
|
+
});
|
|
141
180
|
}
|
|
142
181
|
|
|
143
182
|
function writeAiProfiles(cfg) {
|
|
@@ -242,11 +281,13 @@ function normalizeProjects(rawProjects) {
|
|
|
242
281
|
}
|
|
243
282
|
|
|
244
283
|
function readProjects(options = {}) {
|
|
245
|
-
const
|
|
246
|
-
|
|
284
|
+
const rawProjects = readJsonOrDefault(PROJECTS_PATH, {}, {
|
|
285
|
+
persistDefault: true,
|
|
286
|
+
backupInvalid: true,
|
|
287
|
+
});
|
|
247
288
|
const result = normalizeProjects(rawProjects);
|
|
248
289
|
if (options.persistMigrations && result.changed) {
|
|
249
|
-
writeJson(
|
|
290
|
+
writeJson(PROJECTS_PATH, result.projects);
|
|
250
291
|
}
|
|
251
292
|
return result.projects;
|
|
252
293
|
}
|
package/ai-profiles.json
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "ai-profiles/v1",
|
|
3
|
-
"defaultProfileId": "
|
|
3
|
+
"defaultProfileId": "mock-agent",
|
|
4
4
|
"profiles": [
|
|
5
5
|
{
|
|
6
|
-
"id": "
|
|
7
|
-
"name": "
|
|
6
|
+
"id": "mock-agent",
|
|
7
|
+
"name": "Mock Agent",
|
|
8
|
+
"description": "Local deterministic adapter for smoke tests and first-run verification.",
|
|
8
9
|
"enabled": true,
|
|
10
|
+
"implementation": "mock-agent"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "claude-code-agent",
|
|
14
|
+
"name": "Claude Code Agent",
|
|
15
|
+
"description": "Runs Claude Code or an Anthropic-compatible endpoint with the configured model.",
|
|
16
|
+
"enabled": false,
|
|
9
17
|
"implementation": "claude-code-agent",
|
|
10
|
-
"baseUrl": "https://api.
|
|
11
|
-
"apiKey": "
|
|
12
|
-
"apiKeyEnv": "",
|
|
13
|
-
"model": "
|
|
18
|
+
"baseUrl": "https://api.anthropic.com",
|
|
19
|
+
"apiKey": "",
|
|
20
|
+
"apiKeyEnv": "ANTHROPIC_AUTH_TOKEN",
|
|
21
|
+
"model": "claude-haiku-4-5",
|
|
14
22
|
"version": "2023-06-01",
|
|
15
|
-
"temperature": 0,
|
|
23
|
+
"temperature": 0.2,
|
|
16
24
|
"maxTokens": 4096,
|
|
17
|
-
"timeoutMs":
|
|
25
|
+
"timeoutMs": 300000
|
|
18
26
|
}
|
|
19
27
|
]
|
|
20
28
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const pkg = require('../package.json');
|
|
5
|
+
|
|
6
|
+
function printHelp() {
|
|
7
|
+
console.log(`project-knowledge ${pkg.version}
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
project-knowledge [start] [--host 127.0.0.1] [--port 7777]
|
|
11
|
+
project-knowledge --version
|
|
12
|
+
project-knowledge --help
|
|
13
|
+
|
|
14
|
+
Starts the local Project Knowledge dashboard.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function takeFlag(args, longName, shortName) {
|
|
18
|
+
const index = args.findIndex(item => item === longName || item === shortName);
|
|
19
|
+
if (index === -1) return '';
|
|
20
|
+
const value = args[index + 1] || '';
|
|
21
|
+
args.splice(index, value ? 2 : 1);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
27
|
+
printHelp();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
31
|
+
console.log(pkg.version);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let command = 'start';
|
|
36
|
+
if (args[0] && !args[0].startsWith('-')) {
|
|
37
|
+
command = args.shift();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (command !== 'start' && command !== 'serve') {
|
|
41
|
+
console.error(`Unknown command: ${command}`);
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const port = takeFlag(args, '--port', '-p');
|
|
47
|
+
const host = takeFlag(args, '--host', '-H');
|
|
48
|
+
if (port) process.env.KB_SITE_PORT = port;
|
|
49
|
+
if (host) process.env.KB_SITE_HOST = host;
|
|
50
|
+
|
|
51
|
+
require(path.join(__dirname, '..', '_site', 'server.js'));
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-knowledge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Knowledge base manager with Git integration, AI-driven analysis, and bilingual (zh-CN/en-US) knowledge output",
|
|
5
5
|
"main": "_site/server.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node _site/server.js",
|
|
8
8
|
"test": "node _site/_test/run-all-tests.js"
|
|
9
9
|
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"project-knowledge": "bin/project-knowledge.js"
|
|
12
|
+
},
|
|
10
13
|
"engines": {
|
|
11
14
|
"node": ">=18"
|
|
12
15
|
},
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
"_site/_test/fixtures/",
|
|
21
24
|
"_site/start.bat",
|
|
22
25
|
"_site/stop.bat",
|
|
26
|
+
"bin/",
|
|
23
27
|
"templates/",
|
|
24
28
|
"scripts/",
|
|
25
29
|
"ai-profiles.json",
|
|
@@ -7,7 +7,7 @@ param(
|
|
|
7
7
|
[string] $ProjectSlug,
|
|
8
8
|
[ValidateSet("time", "count")]
|
|
9
9
|
[string] $SortBy = "time",
|
|
10
|
-
[string] $KbRoot =
|
|
10
|
+
[string] $KbRoot = (Split-Path -Parent $PSScriptRoot)
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
$commitsDir = Join-Path $KbRoot "projects\$ProjectSlug\commits"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
@echo off
|
|
2
|
-
|
|
2
|
+
@echo off
|
|
3
|
+
set "KB_ROOT=%~dp0.."
|
|
4
|
+
schtasks /create /tn KB-GitCommits-Daily /tr "powershell -ExecutionPolicy Bypass -File \"%KB_ROOT%\scripts\gen-commit-doc.ps1\" -ProjectSlug ALL" /sc daily /st 08:00 /f
|
|
3
5
|
echo Done.
|
|
4
6
|
schtasks /query /tn KB-GitCommits-Daily
|
|
5
7
|
pause
|