project-knowledge 1.0.0 → 1.0.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/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
- - Currently Windows-oriented: the server references Windows paths for the
64
- PowerShell commit-doc generator and the safe runner. macOS/Linux work for
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
- 2. Edit `ai-profiles.json` to pick the AI profile you want (start with
92
- `mock-agent` to see the flow without an LLM).
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
+ });
@@ -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 LEGACY_SCRIPT = 'D:\\SanQian.Xu\\project-knowledge-base\\scripts\\gen-commit-doc.ps1';
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('D:\\SanQian.Xu\\project-knowledge-base', 'projects', slug);
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 = 'D:\\SanQian.Xu\\project-knowledge-base\\scripts\\gen-commit-doc.ps1';
33
- const SAFE_RUNNER = 'D:\\SanQian.Xu\\project-knowledge-base\\_site\\scripts\\safe-runner.js';
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 JSON.parse(fs.readFileSync(path.join(KB_ROOT, 'projects.json'), 'utf-8'));
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 readAiProfiles() {
122
- if (!fs.existsSync(AI_PROFILES_PATH)) {
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
- return JSON.parse(fs.readFileSync(AI_PROFILES_PATH, 'utf-8'));
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 projectsPath = path.join(KB_ROOT, 'projects.json');
246
- const rawProjects = readJson(projectsPath);
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(projectsPath, result.projects);
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": "minimax-m3",
3
+ "defaultProfileId": "mock-agent",
4
4
  "profiles": [
5
5
  {
6
- "id": "minimax-m3",
7
- "name": "MiniMax M3",
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.minimaxi.com/anthropic",
11
- "apiKey": "sk-cp-q4xMs0h5OQ-gCHnKi6P_9I8IkBVLUuWvs0v1J0t7CH7m8hgq6jvshtPsjQ3X00Ula-_M7jRQYKWvsKyZT3BECNgWRx9CcWAIY_aMyaJ5XAtf87uS_s1XaXU",
12
- "apiKeyEnv": "",
13
- "model": "MiniMax-M3",
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": 3000000
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.0",
3
+ "version": "1.0.1",
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",
@@ -5,7 +5,7 @@
5
5
  param(
6
6
  [Parameter(Mandatory = $true)]
7
7
  [string] $ProjectSlug,
8
- [string] $KbRoot = "D:\SanQian.Xu\project-knowledge-base",
8
+ [string] $KbRoot = (Split-Path -Parent $PSScriptRoot),
9
9
  [int] $MaxCommits = 0
10
10
  )
11
11
 
@@ -7,7 +7,7 @@ param(
7
7
  [string] $ProjectSlug,
8
8
  [ValidateSet("time", "count")]
9
9
  [string] $SortBy = "time",
10
- [string] $KbRoot = "D:\SanQian.Xu\project-knowledge-base"
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
- schtasks /create /tn KB-GitCommits-Daily /tr "powershell -ExecutionPolicy Bypass -File D:\SanQian.Xu\project-knowledge-base\scripts\gen-commit-doc.ps1 -ProjectSlug ALL" /sc daily /st 08:00 /f
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