@sanqianx/project-knowledge 5.0.26 → 5.0.28

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,374 @@
1
+ 'use strict';
2
+ // P-PROFILE-BLANK-REAL-R49 driver: SYS-003 凭据留空 against the REAL
3
+ // Workbench 0.13.9 module bytes (the root's exact-pinned install), spawned
4
+ // only through qa-autopilot/tools/product-probes/qa-r49-run.cjs under the
5
+ // audited full-env isolation harness. This file is a probe entry (-probe, not
6
+ // -test) so the default suite runner never launches it.
7
+ //
8
+ // Case (qa-autopilot/config/test-cases.json SYS-003):
9
+ // step: 已有凭据后留空保存,再读取mask元数据
10
+ // expected: 留空不覆盖; 响应、日志不泄露secret
11
+ //
12
+ // What is REAL here (all product bytes from node_modules/claude-ai-workbench):
13
+ // A. profile HTTP half — create + set FAKE credential + blank-save via the
14
+ // exact client serializer contract (PUT /profiles/:id with the form's
15
+ // profile JSON and NO secret — precisely what client.saveProfile sends
16
+ // when the real form emits credential: ""); disk credential bytes must
17
+ // stay identical; masked GET metadata; explicit-clear contrast
18
+ // (PUT .../credential {secret:""} must 400 — blank is NOT a clear);
19
+ // B. embedding HTTP half — seed FAKE secret, blank-save with secret:""
20
+ // (the root UI serializer contract, ui/product.js:609, sends the raw
21
+ // empty value), FAKE preserved, {enabled:false} is the real clear;
22
+ // C. UI half — the real /profile-settings/ form (real ai-profile-editor
23
+ // custom element + real host page.js persist()) in a real isolated
24
+ // browser: create a fictional profile with a FAKE key through the form,
25
+ // then 更换 (replace-key) → the password input is shown blank → save
26
+ // other-field changes with the key left blank → stored FAKE unchanged;
27
+ // D. leak scan — every HTTP response body, the server stdout/stderr tails,
28
+ // dataDir/diagnostics.jsonl and the live UI DOM must not contain any
29
+ // FAKE secret raw value;
30
+ // E. identity chain — executed module file hashes must equal the
31
+ // module-runtime-manifest.json terminal (0.13.9) pinned hashes.
32
+ // No /test, /quota, /detect-models call is ever made; every invented endpoint
33
+ // host is under the reserved .invalid TLD, so no dial can reach a provider.
34
+
35
+ const assert = require('node:assert/strict');
36
+ const fs = require('node:fs');
37
+ const http = require('node:http');
38
+ const path = require('node:path');
39
+ const crypto = require('node:crypto');
40
+ const { spawn } = require('node:child_process');
41
+
42
+ const ROOT = path.resolve(__dirname, '..', '..');
43
+ const MODULE_ROOT = path.join(ROOT, 'node_modules', 'claude-ai-workbench');
44
+ const EVIDENCE = String(process.env.PK_R49_EVIDENCE || '').trim();
45
+ const API = '/api/claude-workbench/v1';
46
+
47
+ const FAKE_KEY = 'r49-fake-profile-key-DO-NOT-DIAL-774411';
48
+ const FAKE_EMBED = 'r49-fake-embedding-secret-8811ZZCANNOT-be-real';
49
+ const FAKE_KEY_UI = 'r49-fake-ui-key-5566CANNOT-be-real-9931';
50
+
51
+ const sha256 = data => crypto.createHash('sha256').update(data).digest('hex');
52
+ const maskOf = secret => {
53
+ const value = String(secret || '');
54
+ if (!value) return '';
55
+ if (value.length <= 12) return `${value.slice(0, 2)}****${value.slice(-2)}`;
56
+ return `${value.slice(0, 6)}****${value.slice(-6)}`;
57
+ };
58
+
59
+ function record(name, body) {
60
+ fs.writeFileSync(path.join(EVIDENCE, name), typeof body === 'string' ? body : JSON.stringify(body, null, 2) + '\n');
61
+ }
62
+ function log(step, detail) {
63
+ console.log(`[r49] ${step}${detail ? ' :: ' + detail : ''}`);
64
+ }
65
+
66
+ // ---- HTTP helper against the real module server -------------------------
67
+
68
+ const responseLog = [];
69
+ function request(method, pathname, body) {
70
+ return new Promise((resolve, reject) => {
71
+ const payload = body === undefined ? null : JSON.stringify(body);
72
+ const req = http.request({
73
+ host: '127.0.0.1', port, path: pathname, method,
74
+ headers: payload ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) } : {},
75
+ }, res => {
76
+ const chunks = [];
77
+ res.on('data', chunk => chunks.push(chunk));
78
+ res.on('end', () => {
79
+ const text = Buffer.concat(chunks).toString('utf8');
80
+ responseLog.push({ method, path: pathname, status: res.statusCode, bodyText: text });
81
+ let json = null;
82
+ try { json = JSON.parse(text); } catch { /* keep text */ }
83
+ resolve({ status: res.statusCode, text, json });
84
+ });
85
+ });
86
+ req.on('error', reject);
87
+ if (payload) req.write(payload);
88
+ req.end();
89
+ });
90
+ }
91
+
92
+ async function waitFor(fn, label, timeoutMs = 20000) {
93
+ const deadline = Date.now() + timeoutMs;
94
+ let lastError;
95
+ while (Date.now() < deadline) {
96
+ try { const value = await fn(); if (value) return value; } catch (error) { lastError = error; }
97
+ await new Promise(resolve => setTimeout(resolve, 150));
98
+ }
99
+ throw new Error('Timed out waiting for ' + label + (lastError ? ': ' + lastError.message : ''));
100
+ }
101
+
102
+ // ---- state ----------------------------------------------------------------
103
+
104
+ let port = 0;
105
+ let hostChild = null;
106
+ let hostStdout = '';
107
+ let hostStderr = '';
108
+ let browser = null;
109
+ const dataDir = String(process.env.PK_R49_DATA_DIR || '').trim();
110
+ const credFile = path.join(dataDir, 'credentials.v1.json');
111
+ const profileFile = path.join(dataDir, 'profiles.v2.json');
112
+
113
+ function readCreds() { return JSON.parse(fs.readFileSync(credFile, 'utf8')); }
114
+
115
+ // ---- E. identity chain (before touching anything) ------------------------
116
+
117
+ function identityChain() {
118
+ const modulePkg = JSON.parse(fs.readFileSync(path.join(MODULE_ROOT, 'package.json'), 'utf8'));
119
+ assert.equal(modulePkg.name, '@sanqianx/claude-ai-workbench');
120
+ assert.equal(modulePkg.version, '0.13.9', 'module must be the root-pinned 0.13.9');
121
+ const rootPkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
122
+ assert.equal(rootPkg.dependencies['claude-ai-workbench'], 'npm:@sanqianx/claude-ai-workbench@0.13.9', 'root dependency pin');
123
+ const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'module-runtime-manifest.json'), 'utf8'));
124
+ assert.equal(manifest.modules.terminal.version, '0.13.9');
125
+ const files = [
126
+ 'packages/server/index.js',
127
+ 'packages/server/lib/profile-store.js',
128
+ 'packages/server/lib/http-handler.js',
129
+ 'packages/server/lib/workbench-service.js',
130
+ 'packages/contracts/index.js',
131
+ 'packages/client/index.js',
132
+ 'packages/ui/ai-profile-settings.js',
133
+ 'apps/ai-profile-settings-host/index.html',
134
+ 'apps/ai-profile-settings-host/page.js',
135
+ ];
136
+ const entries = {};
137
+ for (const rel of files) {
138
+ const digest = sha256(fs.readFileSync(path.join(MODULE_ROOT, rel)));
139
+ const pinned = manifest.modules.terminal.files[rel];
140
+ assert.ok(pinned, `manifest pin missing for ${rel}`);
141
+ assert.equal(digest, pinned, `executed bytes must equal the 0.13.9 manifest pin for ${rel}`);
142
+ entries[rel] = digest;
143
+ }
144
+ return { moduleVersion: modulePkg.version, rootPin: rootPkg.dependencies['claude-ai-workbench'], manifestTerminal: manifest.modules.terminal.version, files: entries, verified: files.length };
145
+ }
146
+
147
+ // ---- sections -------------------------------------------------------------
148
+
149
+ async function sectionA() {
150
+ log('A1 create fictional profile via HTTP (client contract)');
151
+ const create = await request('PUT', `${API}/profiles/r49fake`, {
152
+ id: 'r49fake', name: 'R49-Fake', provider: 'r49-fake-provider',
153
+ baseUrl: 'https://r49-fake.invalid/anthropic', models: { default: 'r49-fake-model' },
154
+ });
155
+ assert.equal(create.status, 200);
156
+ assert.equal(create.json.credentialConfigured, false);
157
+ assert.ok(!('apiKey' in create.json) && !('secret' in create.json) && !('authToken' in create.json), 'create response must not carry secret-shaped fields');
158
+
159
+ log('A2 set FAKE credential via the explicit credential endpoint');
160
+ const set = await request('PUT', `${API}/profiles/r49fake/credential`, { secret: FAKE_KEY });
161
+ assert.equal(set.status, 204);
162
+ assert.equal(readCreds().credentials['credential:r49fake'], FAKE_KEY);
163
+ const credHash0 = sha256(fs.readFileSync(credFile));
164
+
165
+ log('A3 masked metadata read (GET /profiles)');
166
+ const list = await request('GET', `${API}/profiles`);
167
+ assert.equal(list.status, 200);
168
+ const listed = list.json.profiles.find(p => p.id === 'r49fake');
169
+ assert.ok(listed, 'created profile must be listed');
170
+ assert.equal(listed.credentialConfigured, true);
171
+ assert.equal(listed.credentialMasked, maskOf(FAKE_KEY));
172
+ assert.ok(!list.text.includes(FAKE_KEY), 'masked read must not contain the raw FAKE key');
173
+
174
+ log('A4 blank-save: profile saved with the password left blank (serializer contract), other field changed');
175
+ const blankSave = await request('PUT', `${API}/profiles/r49fake`, {
176
+ id: 'r49fake', name: 'R49-Fake', provider: 'r49-fake-provider',
177
+ baseUrl: 'https://r49-fake.invalid/anthropic', models: { default: 'r49-fake-model-v2' },
178
+ });
179
+ assert.equal(blankSave.status, 200);
180
+ assert.equal(blankSave.json.models.default, 'r49-fake-model-v2', 'other-field change must persist');
181
+ assert.equal(blankSave.json.credentialConfigured, true, 'blank save must not unset the credential');
182
+ assert.ok(!blankSave.text.includes(FAKE_KEY));
183
+
184
+ log('A5 disk bytes unchanged across the blank save');
185
+ assert.equal(sha256(fs.readFileSync(credFile)), credHash0, '留空不覆盖: stored credential bytes must be identical');
186
+ assert.equal(readCreds().credentials['credential:r49fake'], FAKE_KEY);
187
+
188
+ log('A6 explicit-clear contrast: blank secret on the credential endpoint must be rejected (400), not a clear');
189
+ const blankCred = await request('PUT', `${API}/profiles/r49fake/credential`, { secret: '' });
190
+ assert.equal(blankCred.status, 400, 'blank credential must be rejected, not treated as clear');
191
+ assert.equal(readCreds().credentials['credential:r49fake'], FAKE_KEY, 'rejected blank must leave the stored value intact');
192
+
193
+ return { credHash0 };
194
+ }
195
+
196
+ async function sectionB() {
197
+ log('B1 seed FAKE embedding secret');
198
+ const seed = await request('PUT', `${API}/embedding-config`, {
199
+ provider: 'openai', model: 'r49-embed-model', endpoint: 'https://r49-embed.invalid/v1', secret: FAKE_EMBED,
200
+ });
201
+ assert.equal(seed.status, 200);
202
+ assert.equal(seed.json.configured, true);
203
+ assert.equal(readCreds().credentials['knowledge-embedding'], FAKE_EMBED);
204
+ const credHash = sha256(fs.readFileSync(credFile));
205
+
206
+ log('B2 blank-save embedding (secret: "" — the root UI serializer contract sends the raw empty value)');
207
+ const blankSave = await request('PUT', `${API}/embedding-config`, {
208
+ provider: 'openai', model: 'r49-embed-model-v2', endpoint: 'https://r49-embed.invalid/v1', secret: '',
209
+ });
210
+ assert.equal(blankSave.status, 200);
211
+ assert.equal(blankSave.json.configured, true, 'blank secret must not unset the embedding credential');
212
+ assert.equal(blankSave.json.config.model, 'r49-embed-model-v2');
213
+ assert.ok(!blankSave.text.includes(FAKE_EMBED));
214
+
215
+ log('B3 stored FAKE embedding secret preserved');
216
+ assert.equal(readCreds().credentials['knowledge-embedding'], FAKE_EMBED, '留空不覆盖 (embedding)');
217
+ assert.equal(sha256(fs.readFileSync(credFile)), credHash, 'credential file bytes must be identical across the embedding blank save');
218
+
219
+ log('B4 explicit clear contrast: {enabled:false} is the real clear operation');
220
+ const clear = await request('PUT', `${API}/embedding-config`, { enabled: false });
221
+ assert.equal(clear.status, 200);
222
+ assert.equal(clear.json.configured, false);
223
+ assert.ok(!readCreds().credentials['knowledge-embedding'], 'the explicit clear must remove the credential');
224
+ }
225
+
226
+ async function sectionC() {
227
+ const { findChrome } = require('./helpers/find-chrome');
228
+ const { launchCdpBrowser } = require('./helpers/cdp-browser');
229
+ const base = `http://127.0.0.1:${port}`;
230
+
231
+ log('C1 launch isolated browser on the real /profile-settings/ form');
232
+ browser = await launchCdpBrowser({ chrome: findChrome(), profileDir: path.join(EVIDENCE, 'browser-profile'), url: `${base}/profile-settings/`, width: 1360, height: 900 });
233
+ const run = expression => browser.evaluate(expression);
234
+ await waitFor(() => run("Boolean(document.querySelector('#addProfile') && document.querySelector('ai-profile-list') && document.querySelector('ai-profile-list').shadowRoot)"), 'settings page loaded');
235
+ await browser.screenshot(path.join(EVIDENCE, 'ui-01-initial.png'));
236
+
237
+ log('C2 create a fictional profile through the real form (FAKE key typed once)');
238
+ await run("document.querySelector('#addProfile').click()");
239
+ await waitFor(() => run("document.querySelector('#profileEditor').shadowRoot && document.querySelector('#profileEditor').shadowRoot.querySelector('form [name=\"name\"]')"), 'editor form rendered');
240
+ await run(`(() => {
241
+ const form = document.querySelector('#profileEditor').shadowRoot;
242
+ const set = (name, value) => { form.querySelector('form [name="' + name + '"]').value = value; };
243
+ set('name', 'R49-UI-Fake'); set('provider', 'r49-ui-provider');
244
+ set('baseUrl', 'https://r49-ui-fake.invalid/anthropic');
245
+ set('model', 'r49-ui-model'); set('apiKey', ${JSON.stringify(FAKE_KEY_UI)});
246
+ })()`);
247
+ await run("document.querySelector('#profileEditor').shadowRoot.querySelector('[data-action=\"save\"]').click()");
248
+ await waitFor(() => run("document.querySelector('#profileList').shadowRoot && document.querySelector('#profileList').shadowRoot.textContent.includes('R49-UI-Fake')"), 'created profile appears in the list');
249
+ assert(await run("document.querySelector('#profileList').shadowRoot.textContent.includes('Key 已配置')"), 'the real list must report the key as configured');
250
+ await browser.screenshot(path.join(EVIDENCE, 'ui-02-created.png'));
251
+
252
+ const uiProfileId = await run("(() => { const list = document.querySelector('#profileList'); const hit = (list.profiles || []).find(p => p.name === 'R49-UI-Fake'); return hit ? hit.id : null; })()");
253
+ assert.ok(uiProfileId && uiProfileId.startsWith('profile-'), `the form must have generated the profile id (got ${uiProfileId})`);
254
+
255
+ log('C3 masked display: the editor must show the mask, never the raw key');
256
+ await run(`(() => { const rows = document.querySelector('#profileList').shadowRoot.querySelectorAll('.item'); for (const row of rows) { if (row.textContent.includes('R49-UI-Fake')) { row.querySelector('[data-action=\"edit\"]').click(); return true; } } return false; })()`);
257
+ await waitFor(() => run("document.querySelector('#profileEditor').shadowRoot && document.querySelector('#profileEditor').shadowRoot.querySelector('form')"), 'editor opened for the saved profile');
258
+ const maskedShown = await run(`document.querySelector('#profileEditor').shadowRoot.querySelector('input[readonly][aria-label="API Key 已保存"]')?.value || ''`);
259
+ assert.equal(maskedShown, maskOf(FAKE_KEY_UI), 'the editor must display the masked value');
260
+ assert.ok(!(await run("JSON.stringify(document.querySelector('#profileEditor').shadowRoot.innerHTML)")).includes(FAKE_KEY_UI), 'editor DOM must not contain the raw key');
261
+
262
+ log('C4 the blank-save: 更换 (replace-key) shows the password input blank; keep it blank, change another field, save');
263
+ await run("document.querySelector('#profileEditor').shadowRoot.querySelector('[data-action=\"replace-key\"]').click()");
264
+ const blankInput = await waitFor(() => run("(() => { const input = document.querySelector('#profileEditor').shadowRoot.querySelector('form [name=\"apiKey\"]'); return input ? ('present:' + JSON.stringify(input.value)) : null; })()"), 'blank apiKey input visible');
265
+ assert.equal(blankInput, 'present:""', 'the replace-key input must be present and blank (密码输入留空)');
266
+ await run(`(() => { document.querySelector('#profileEditor').shadowRoot.querySelector('form [name=\"model\"]').value = 'r49-ui-model-v2'; })()`);
267
+ const credBeforeSave = sha256(fs.readFileSync(credFile));
268
+ await run("document.querySelector('#profileEditor').shadowRoot.querySelector('[data-action=\"save\"]').click()");
269
+ await waitFor(() => run("document.querySelector('#listFeedback') && document.querySelector('#listFeedback').textContent.includes('已保存')"), 'save confirmed by the real host page');
270
+ await browser.screenshot(path.join(EVIDENCE, 'ui-03-after-blank-save.png'));
271
+
272
+ log('C5 the stored FAKE key survived the blank save');
273
+ assert.equal(sha256(fs.readFileSync(credFile)), credBeforeSave, 'credential file bytes must be identical across the UI blank save');
274
+ assert.equal(readCreds().credentials[`credential:${uiProfileId}`], FAKE_KEY_UI, 'stored runtime credential must keep the FAKE value');
275
+ assert(await run("document.querySelector('#profileList').shadowRoot.textContent.includes('Key 已配置')"), 'the list must still report the key as configured');
276
+
277
+ const verify = await request('GET', `${API}/profiles`);
278
+ const uiProfile = verify.json.profiles.find(p => p.id === uiProfileId);
279
+ assert.equal(uiProfile.models.default, 'r49-ui-model-v2', 'the other-field change must persist');
280
+ assert.equal(uiProfile.credentialConfigured, true);
281
+ assert.equal(uiProfile.credentialMasked, maskOf(FAKE_KEY_UI));
282
+ assert.ok(!verify.text.includes(FAKE_KEY_UI));
283
+
284
+ log('C6 UI DOM leak scan');
285
+ const domDump = await run("(() => { const parts = [document.documentElement.outerHTML]; for (const el of document.querySelectorAll('*')) { if (el.shadowRoot) parts.push(el.shadowRoot.innerHTML); } return parts.join('\\n'); })()");
286
+ assert.ok(!domDump.includes(FAKE_KEY_UI), 'the live UI DOM must not contain the raw key');
287
+ fs.writeFileSync(path.join(EVIDENCE, 'ui-dom-dump.html'), domDump);
288
+ return { uiProfileId };
289
+ }
290
+
291
+ function sectionD() {
292
+ log('D leak scan over responses, server output, diagnostics, DOM');
293
+ const diagnosticsFile = path.join(dataDir, 'diagnostics.jsonl');
294
+ const diagnostics = fs.existsSync(diagnosticsFile) ? fs.readFileSync(diagnosticsFile, 'utf8') : '';
295
+ const surfaces = {
296
+ responses: responseLog.map(r => `[${r.status}] ${r.bodyText}`).join('\n'),
297
+ hostStdout,
298
+ hostStderr,
299
+ diagnostics,
300
+ uiDom: fs.existsSync(path.join(EVIDENCE, 'ui-dom-dump.html')) ? fs.readFileSync(path.join(EVIDENCE, 'ui-dom-dump.html'), 'utf8') : '',
301
+ };
302
+ const hits = [];
303
+ for (const [name, text] of Object.entries(surfaces)) {
304
+ for (const secret of [FAKE_KEY, FAKE_EMBED, FAKE_KEY_UI]) {
305
+ if (String(text).includes(secret)) hits.push({ surface: name, secretPrefix: secret.slice(0, 12) });
306
+ }
307
+ }
308
+ record('leak-scan.json', { surfaces: Object.fromEntries(Object.entries(surfaces).map(([k, v]) => [k, { bytes: String(v).length}])), hits, clean: hits.length === 0 });
309
+ assert.equal(hits.length, 0, `raw secrets leaked to: ${JSON.stringify(hits)}`);
310
+ return { hits: 0 };
311
+ }
312
+
313
+ // ---- main -----------------------------------------------------------------
314
+
315
+ (async () => {
316
+ if (!EVIDENCE || !dataDir) throw new Error('PK_R49_EVIDENCE and PK_R49_DATA_DIR are required');
317
+ fs.mkdirSync(EVIDENCE, { recursive: true });
318
+ const startedAt = new Date().toISOString();
319
+ const identity = identityChain();
320
+ log('identity', `${identity.verified} module files == manifest 0.13.9 pins`);
321
+
322
+ const hostEnv = { ...process.env, PK_R49_MODULE_ROOT: MODULE_ROOT, PK_R49_DATA_DIR: dataDir, PK_R49_WORKSPACE: path.join(EVIDENCE, 'workspace') };
323
+ fs.mkdirSync(hostEnv.PK_R49_WORKSPACE, { recursive: true });
324
+ hostChild = spawn(process.execPath, [path.join(__dirname, 'helpers', 'qa-r49-wb-host.js')], { env: hostEnv, windowsHide: true });
325
+ hostChild.stdout.on('data', chunk => { hostStdout += String(chunk); });
326
+ hostChild.stderr.on('data', chunk => { hostStderr += String(chunk); });
327
+
328
+ const readyLine = await waitFor(() => (hostStdout.match(/R49_WB_HOST_READY http:\/\/127\.0\.0\.1:(\d+)/) || [])[1], 'real module server READY', 30000);
329
+ port = Number(readyLine);
330
+ log('host ready', `port ${port}`);
331
+
332
+ const sections = { startedAt, identity };
333
+ try {
334
+ sections.A = await sectionA();
335
+ sections.B = await sectionB();
336
+ sections.C = await sectionC();
337
+ sections.D = sectionD();
338
+ sections.finishedAt = new Date().toISOString();
339
+ record('r49-evidence.json', sections);
340
+ log('ALL SECTIONS PASS');
341
+ } finally {
342
+ record('responses.json', responseLog);
343
+ record('host-stdout.txt', hostStdout);
344
+ record('host-stderr.txt', hostStderr);
345
+ const diagFile = path.join(dataDir, 'diagnostics.jsonl');
346
+ if (fs.existsSync(diagFile)) fs.copyFileSync(diagFile, path.join(EVIDENCE, 'diagnostics.jsonl'));
347
+ if (fs.existsSync(credFile)) fs.copyFileSync(credFile, path.join(EVIDENCE, 'credentials.v1.json.final'));
348
+ if (fs.existsSync(profileFile)) fs.copyFileSync(profileFile, path.join(EVIDENCE, 'profiles.v2.json.final'));
349
+ if (browser) await browser.close();
350
+ if (hostChild && hostChild.exitCode === null) {
351
+ hostChild.kill('SIGTERM');
352
+ await new Promise(resolve => { hostChild.once('exit', resolve); setTimeout(resolve, 5000).unref(); });
353
+ }
354
+ }
355
+ process.exit(0);
356
+ })().catch(async error => {
357
+ console.error('R49 PROBE FAILED:', error && error.stack || error);
358
+ try {
359
+ record('first-failure.json', {
360
+ at: new Date().toISOString(),
361
+ message: String(error && error.message || error),
362
+ stack: String(error && error.stack || ''),
363
+ responsesSoFar: responseLog,
364
+ hostStdoutTail: hostStdout.slice(-4000),
365
+ hostStderrTail: hostStderr.slice(-4000),
366
+ });
367
+ } catch { /* evidence write best-effort */ }
368
+ try { if (browser) await browser.close(); } catch { /* best-effort */ }
369
+ if (hostChild && hostChild.exitCode === null) {
370
+ try { hostChild.kill('SIGTERM'); } catch { /* already gone */ }
371
+ await new Promise(resolve => { hostChild.once('exit', resolve); setTimeout(resolve, 5000).unref(); });
372
+ }
373
+ process.exit(1);
374
+ });
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "schema": "project-knowledge/module-content/v1",
3
3
  "productFiles": {
4
- "ui/index.html": "f7e87c2006f27f4d1824c0f3778a03160a530ccb15346323944a07584e05e8d8",
4
+ "ui/index.html": "e7758ae86968ea78f6f7ad591def9695bb4551056d7a6ac54adad6aafe381733",
5
5
  "ui/knowledge-panel.html": "dfd85f15759472252d797853263e9da0054b251c56bb9ccdc8a75e6301334ca8",
6
- "ui/product.js": "26b2cc5438fdc331c0aeb1f77bdb1c8340ca1468211f78a5348d84706e953feb",
6
+ "ui/product.js": "8c0d9b64872f5770f14e525401f9cce8b5dcd3c4c78977c83ade6258b093c57d",
7
7
  "ui/live-client.js": "0d81fa894c629bb839e1931fc3c5ebad4d831aae8232d6ce8c0f542e9e3559e8",
8
- "ui/product.css": "7347c47148891b7799b5d3711baff1c7fe5db08c6209c30c391d6aee7eb30be8",
8
+ "ui/product.css": "d24e57fcd49024ca44a49b80099a60258a34f553029e60c203a54527645d7e27",
9
9
  "ui/favicon.svg": "c48be946b224112ebb0238b44263f7a61bda31e531afa895c2e9801c0ff24f32",
10
10
  "plugins/project-knowledge/hooks/claude.json": "d61510327e3e1facbb4c645228e9f50cad05a88b33d80c4c22e5ba9f8b9f5359",
11
11
  "plugins/project-knowledge/hooks/codex.json": "216e03c07295606ef6ebb2c4f669ad0d790b5e7c4f3f34600488033db75e4239",
12
12
  "plugins/project-knowledge/scripts/context-hook.cjs": "da666153ad5723592cce42d25337ae649f7d93e43fab76b3c6d1263933de063f",
13
13
  "plugins/project-knowledge/scripts/mcp-launcher.cjs": "7c68830b5c73eb53ef030756a4de33987f7eab6b1a6cc15602835aac3f954893",
14
- "plugins/project-knowledge/.claude-plugin/plugin.json": "263dbc9dc31bc57df32e9b0fad04a39389d53b20adc4cb470250cfa367a3e725",
15
- "plugins/project-knowledge/.codex-plugin/plugin.json": "267f885713a07bbbab92ad0547c9ec75c7b3554ed9f6ac08d44e4a827d2aa2b3",
16
- "plugins/project-knowledge/.mcp.json": "70508cc2b26c51e7786f4dbadd7e16d1eddc79e96e19688b8d1001946fabfeca",
14
+ "plugins/project-knowledge/.claude-plugin/plugin.json": "553a4f35076893958f7ebbf105448aa84202718430c29785f478ffe8ef5a727d",
15
+ "plugins/project-knowledge/.codex-plugin/plugin.json": "fca626d8a10467bcbc33473c0b22f982c0e7257edc0d5d035241b06ea507ba43",
16
+ "plugins/project-knowledge/.mcp.json": "4eb3f49bb33a85becb5615fa9da6e88c035a15e4175c613ba07b179a0678382e",
17
17
  "ui/version-info.js": "01a82217b031de54b80d6c7974da9c70888c4c90a046bd4248a808f7989ddce7",
18
18
  "ui/version-info.css": "209fad8e42b661f72d2aba3280e1ca47768b5cefb776fe5459538d34fbad120f"
19
19
  },
@@ -21,14 +21,14 @@
21
21
  "terminal": {
22
22
  "name": "@sanqianx/claude-ai-workbench",
23
23
  "installName": "claude-ai-workbench",
24
- "version": "0.13.9",
24
+ "version": "0.13.11",
25
25
  "files": {
26
26
  "LICENSE": "c1b9df1275e769f3dbab000d1e457a2d4b0f28eb5da6c77e48dc37eeba202ed7",
27
- "README.md": "43938c8a64397d340dcf633edc40a7cea7dd8a95b5f82cf612b2c15b66c380b6",
28
- "apps/agent-terminal/app.css": "4d757c7d812035a89d9f709b94c7fa7bd005207ccbdfd11efa1d48f744a8d071",
29
- "apps/agent-terminal/app.js": "63f5a44e0539855f1e0fd0f4bbf4efabd2d616ada61b886b6289bfe0814a6766",
27
+ "README.md": "557784a0c9c60e57c2ab0ad7d4d3a34f6c35198d021165e3adc961008baed056",
28
+ "apps/agent-terminal/app.css": "6198b425dc1957380e7adc25212b4dc9345b58331976d19b374d2b22b682a18b",
29
+ "apps/agent-terminal/app.js": "4f469f3e83d9778bb70d1d3ed737b6269f007f0764ae9e7da8dcc2594b23033c",
30
30
  "apps/agent-terminal/conversation-composer.js": "4f0135fa61f3a19de9bc518cb7001dcc333a793db6de643d4ee6eaa629489c58",
31
- "apps/agent-terminal/index.html": "95c45bfccba80018cdf01b81ea18b486f8d7ecc862bf1f6e3c5dc272382bf30c",
31
+ "apps/agent-terminal/index.html": "8850d107bc2b9b118be09b4bdd0a57cbd56774ae2a9e68770fc907791ca0dd30",
32
32
  "apps/agent-terminal/panel-host-proof.html": "b4b5c69cbd4d019002dd63b364523875f9b5e58c75c8fa873ed05d3717229fa6",
33
33
  "apps/agent-terminal/panel-proof-content.html": "87f59cf76fc5e78aa24d51432d358aa15fc8242f469cabb164bf181d23f85778",
34
34
  "apps/agent-terminal/right-dock-panel-registry.js": "07a4b6d31b1b2d678f92b6f0786cf49129708bd38ef4a15976d824150cbadec6",
@@ -38,29 +38,30 @@
38
38
  "apps/ai-profile-settings-host/index.html": "bc158f68954506d2b8e43c690f28dbe931eff9a02b1524855ac17e74ea4df736",
39
39
  "apps/ai-profile-settings-host/page.css": "8b75cbf7f1ea5aaa92f2889390720ece310a8fd4b9ca38c890fa447d4323a557",
40
40
  "apps/ai-profile-settings-host/page.js": "8ab508cb50f9765d148cd93f4d4f4f440f94731dbf43850cfb51cca1c869b070",
41
+ "apps/minimal-test-host/README.md": "fa73136f4ee3865c2d91eb97b2e8c03679aa4688e44c1e1c8ec6e27d604b0de5",
41
42
  "apps/minimal-test-host/host.css": "b870f0816a1fcd27b73ed20b4785b46f95729e55a6a5a56624234c74a012a915",
42
43
  "apps/minimal-test-host/host.js": "c316871e616afe66898eb22a9fa71d8ed530886cdad1b1eb6ae870373994962a",
43
44
  "apps/minimal-test-host/index.html": "a43ca39a89d9877bc658efc02f9de26ecd68356bd35078dfa440e11b7ba11394",
44
45
  "apps/minimal-test-host/optional-mock/fault-injection.js": "4749c8210e5a2b9eab5c8b8188b9c9b2ce9dfc9bcb5eeb0d2d745042453f87f5",
45
- "apps/minimal-test-host/README.md": "fa73136f4ee3865c2d91eb97b2e8c03679aa4688e44c1e1c8ec6e27d604b0de5",
46
46
  "docs/sdk/FILE_LAYOUT.md": "17882f96505c8161bfb66876684b9d2d3f50021a111b8b844502d3cb4e97870a",
47
47
  "docs/sdk/INTEGRATION.md": "d4c0c80d2d2fc610205102d703a429bfaa20f9f3a48f1718f5bab02ee90138df",
48
- "docs/sdk/openapi.yaml": "00d262af4b4943f06b07b0b2dde65db9eefd6766135e2970fd35941ca7dd19ee",
49
48
  "docs/sdk/README.md": "7ab591d39c36a3b84cfdf487ff567040ec453975f3ef0fba86dc822094667b61",
50
49
  "docs/sdk/SESSION_EVENTS.md": "a964f85616ae55e0743e5cf75bad43dee2f9055acad301b134b3553e825102d7",
51
- "package.json": "83f3a496fe839074ddb4daad9bde782f60c19d25e13b0c55a54349700a7cbd95",
52
- "packages/client/index.d.ts": "0366cc96ff2e83814d0dcad34899a26068f7ab5d0b15f9f1c02a7e976db28215",
53
- "packages/client/index.js": "493938f0ff357aea6db136fca216c6cfcb542d51f6628e1994a179b5ec296a12",
54
- "packages/client/package.json": "ab7a346c60f483343f82cb0b990498cc087daa6f9b2d738c95017ef1a34aa3dd",
50
+ "docs/sdk/openapi.yaml": "073a2e29af6290eb54d59fdd7d93d37b84a322c6ed2f490bda4d311638a7cb60",
51
+ "package.json": "626e07c8d4ae80750de5420fbcb009338710be4268a7be195dfef045093a2484",
55
52
  "packages/client/README.md": "e0962d561dad6b50837b0f8099f6931aad14d2dabb683556b88aae0ff485b675",
56
- "packages/contracts/index.d.ts": "6cdfadc74582b6cc84412e4bea6e0d0e65101c2b087d38580fbb1a384e21dca3",
57
- "packages/contracts/index.js": "2640531e4ef5fd4ea5f56e63cc099f9d6357de7306ef9e6fe3dff3b97f3d9304",
58
- "packages/contracts/package.json": "731e60a68b97e550a6c583eee399fdcac94d9ab0cd147b426ff997de44595bde",
53
+ "packages/client/index.d.ts": "4c4a00577ad427215d9b91d6794a98a4c0ede06bafcf7c9904e53363c7c8bf9d",
54
+ "packages/client/index.js": "5458fa35fd260178927ad6eb6e1a84cd8cce3e173260acf46872554e3e36ad65",
55
+ "packages/client/package.json": "0aed211c439916f1dc242331de035af110432677433932a0719f07d52248a8d6",
59
56
  "packages/contracts/README.md": "44d83a3cc508cec6c5c20dfa1f2c08d5ea03e3afb2853639fdc0a9cc8eb5b9c8",
57
+ "packages/contracts/index.d.ts": "2947ab3e5d4408afa7af5095e23733e983c6845994217cab202e737268f61da1",
58
+ "packages/contracts/index.js": "253f530ce74028e77748a09b5d32d775a9fb3e7f09c867613b06e074db7b8ab9",
59
+ "packages/contracts/package.json": "2f023f593f15edeb6131f73c32b887f9b5f042e959c8cb2110d702aa95f34a49",
60
+ "packages/core/README.md": "e222c546104a168f1038d95c6c4efb8e5603edd6145ab2cab79cab977fb710e5",
60
61
  "packages/core/index.d.ts": "c95b24a13f8aeeb128497833cf298790563baca6036ea5d29a9d8daff53199d7",
61
62
  "packages/core/index.js": "825640d0edc7dd130f22da1f580d2cda355566a40b0b36e42e5f38909605bbfa",
62
- "packages/core/package.json": "c824bd4bff923419e381e6e6dd7a4daa92624291521edf747e2c7d1b64d2fa28",
63
- "packages/core/README.md": "e222c546104a168f1038d95c6c4efb8e5603edd6145ab2cab79cab977fb710e5",
63
+ "packages/core/package.json": "bb3caae0ac92af2273367a9c181ff48d1d698a3e290cc899efc7ecc091051f2a",
64
+ "packages/server/README.md": "a34cfaa4f4e859008b44e2be8e1aafb56e97fe83e6129c968a8562945fa843f3",
64
65
  "packages/server/bin/agent-terminal-server.js": "01ac54215f1fcbe64c4b78ce9cd83f66215177f67cdccddad30491c94f3b6746",
65
66
  "packages/server/bin/claude-workbench-server.js": "73fc7f1022b8cd5bbe8aed0eb105223e83627960e7208cffa6c29a2879932f38",
66
67
  "packages/server/bin/claude-workbench.js": "98dbd550d4d657056a75a190e7ee0dffd9f760fba58783f3ffb8d389ad7a32de",
@@ -73,7 +74,7 @@
73
74
  "packages/server/lib/bridges/app-server-bridge.js": "c860c6b9e0f63ead9f4f289dad1fe05fc93314e89b29cdf7722fba0472c9af11",
74
75
  "packages/server/lib/bridges/opencode-server-bridge.js": "23b6065599bece60cc8d3d4779be90d62a6936ec437424287cc87325d250e02b",
75
76
  "packages/server/lib/capture-settings-store.js": "eb18885d7bedd3e353287dd7d1cf81e815e5d65d5d82170aa8fe5506a03de8bd",
76
- "packages/server/lib/claude-history-recovery.js": "a1c15ef2c863d85ab6447d6b70bda0d69ed1a65dd14e375cb2cad1252068a17e",
77
+ "packages/server/lib/claude-history-recovery.js": "e8d6000d0f3de1a1dfe5854dc8dbad833700ffdf6b245e6f6c3cc2029b700507",
77
78
  "packages/server/lib/cli-session-recovery.js": "e66dc0ad83af18841bf3a598bb27932af42a20cf9395c16dcbaf9b022a5201b5",
78
79
  "packages/server/lib/context-resolver.js": "a89fa2b55a86a62396a59052fb394d08532e528c87160d5bd6057bd8bdc26952",
79
80
  "packages/server/lib/conversation-capture.js": "c1474d1a3c148b68ac8974f9252a2c996b9a7378c9ab8272b9115480cb5143a2",
@@ -87,7 +88,9 @@
87
88
  "packages/server/lib/drivers/zcode-profile-runtime.js": "77c5ac83551cc424375918863d4a31039984492c3a1c987a615a072ac7cb1f14",
88
89
  "packages/server/lib/event-stream.js": "cfbe8207594f004ed0ad65a907401eef833f641b94064fa88852b9ae8c15fe59",
89
90
  "packages/server/lib/folder-picker.js": "da2caf8b6a4c00a22556fe355d28d735d78dd814fa0aa7edfc6c4cde8ca8c384",
90
- "packages/server/lib/http-handler.js": "143892a94e6a9d7a1e58a6dfba18f4e11f52d15465687a99addfcadf62b662f0",
91
+ "packages/server/lib/history-prepare-worker.js": "f46a444e0711d539830aec427833645965f871ae056a2f27482a899b14ff85cb",
92
+ "packages/server/lib/history-prepare.js": "27f9b67b1f4012d002c4464dfa837e5144dab9adcb5a41b0b9ae3b0d1d1c18b7",
93
+ "packages/server/lib/http-handler.js": "6a5d2d44527d05d5e03b6608d6209a55f5b8e1ae6c55896ead347706017de667",
91
94
  "packages/server/lib/json-file.js": "4252b57abcd0aafa45c584bac98fcbaf09174774addbb5a4798b381a7aeb35cc",
92
95
  "packages/server/lib/knowledge-context.js": "c3d8a9545547c636e6cdf908be53bef4968e674d2aab235562e762e25c210c06",
93
96
  "packages/server/lib/model-detection.js": "fa52213200c1f1a1f00e463ada57c415bf1bd4e2aedfebd04de50750509d06cf",
@@ -97,21 +100,21 @@
97
100
  "packages/server/lib/project-store.js": "5225c78f3216340f01b99356496c8e2c2d9cfc6247954a3f00b273dd2cc5b692",
98
101
  "packages/server/lib/runtime-loader.js": "33e152cb630d986581946928d7ab7d25d53e63647a433b0c1ff4d02c32a6298f",
99
102
  "packages/server/lib/session-archive-store.js": "d3335b832a6e9fb9792991056f6596a997a4959efa6e62b60fb60133fb724196",
100
- "packages/server/lib/session-history.js": "84f8a0b6195e074721b057c8a2a8354f7e073496984734d904283efcddee7e97",
103
+ "packages/server/lib/session-history-main-index.js": "aef9a208bfbecad022e3eeef90d31f7c78ca711cd94ded48054cf4062e2eb839",
104
+ "packages/server/lib/session-history.js": "3ca434bd3a198d7be09b7eb8d8029477366d8acb7718e4c3e1b2b7ff0cb2694c",
101
105
  "packages/server/lib/system-doctor.js": "d098a046f425e90ef25209be69d78beefdfd9c2af92f08068a9bfc52053a8f7d",
102
106
  "packages/server/lib/ui-preferences-store.js": "e108cb22e7cc1f3438704913d5401e01de2c0e717eb2abd3bdc4727209393819",
103
107
  "packages/server/lib/user-questions.js": "dc30f92095113279ab0bafe5aa4c16a48f97bad18bd13f0ca3ac74018254005a",
104
- "packages/server/lib/workbench-service.js": "e72686c654e0373c562fbbe3c858c46fc104e198d5f6e44f59153247292bfea9",
105
- "packages/server/package.json": "35eff12bca7e5344b62db6978537706249fb368994f93767a037357aa5590ec9",
106
- "packages/server/README.md": "a34cfaa4f4e859008b44e2be8e1aafb56e97fe83e6129c968a8562945fa843f3",
108
+ "packages/server/lib/workbench-service.js": "dce4df6765961ec19d51652374e79070bbc33873c9a0e6338cbf8cfabdc89a9e",
109
+ "packages/server/package.json": "637874c1f7fa0ebb6dabcfff2bc1de37a9a98bca54b2812a999c8af13faa7a95",
110
+ "packages/ui/README.md": "793c4a9c286fee23f39062a1976ca3afe9d2816dcdb55e156ba940e1ef514f89",
107
111
  "packages/ui/ai-profile-settings.d.ts": "10cdc96fb20c1f47a18a984638a26bd69972fc0cacf7bd56f4f64ead593e7a36",
108
112
  "packages/ui/ai-profile-settings.js": "3c7a75833a3c4f607739ecf71a0716e27c8594cecd9d84e2f8ac99498a59e0e7",
109
113
  "packages/ui/claude-terminal.d.ts": "2e8b4ae14f69ec791e2aacbec48ea2bc6e0076e0445a38e99cc477cf2e0a3011",
110
114
  "packages/ui/claude-terminal.js": "96bd217a4a7e4792ae3fbaaf46249ecee13b0f4e01fc6d25a1b8b22d8f8fe00c",
111
115
  "packages/ui/index.d.ts": "e547ab39640fe7519fb367b78442dc8a2c51b2efd17b66f8b0a003b154b36ead",
112
116
  "packages/ui/index.js": "f4394aabd395938f5c3fe587e430471d4033e83945a9b9c7e0149ad4040982c9",
113
- "packages/ui/package.json": "3e91a0569ed33190bc3230363dfe4560b200fbd9c40e2034b408dd393ac38108",
114
- "packages/ui/README.md": "793c4a9c286fee23f39062a1976ca3afe9d2816dcdb55e156ba940e1ef514f89",
117
+ "packages/ui/package.json": "77658dd7e346c4669aea944b166d6d2d615146234138d63ee04fe194914b2be6",
115
118
  "packages/ui/theme.css": "8f03a081272a7db11b196f7a70d0fafb1f8d954778f329f1ba53d110d9d5fd98",
116
119
  "src/assets/claude-prompts.json": "cb111f2e0a45afe8a31c251a504d1ab5375966ed61291660e61fd2efd77f6d9f",
117
120
  "src/backend/http/ai-profile-routes.js": "4b13a8e9ccbb0317f2ae7b3d468c060c33a9e1ffa49b781a495193a8eebaca68",
@@ -123,7 +126,7 @@
123
126
  "src/backend/lib/ai-vendor-presets.js": "02c6de9c2e89ad0286426e8eb4900c3c6a4960ef0e8f9d27ffb5836062abfbb2",
124
127
  "src/backend/lib/ai-workspace.js": "969917f26cd0b0a272932714819d838a2bab2a5aac5c4e946647dfa3afa479bf",
125
128
  "src/backend/lib/automation-config.js": "00da7ec85a4d665695cc56cbfe353b11912af5486bc7d81469425ec93c4c0ce7",
126
- "src/backend/lib/claude-cli-runner.js": "fce69259a2fb25503e629c7605cf0ef90f8bc590b1a74cbb47d6e651bee14aed",
129
+ "src/backend/lib/claude-cli-runner.js": "8c4922d9706e9ee312dcfaf9608b3bbe837c17ea8c43e3fb0dffc7aea60309bd",
127
130
  "src/backend/lib/data-dir.js": "a62d96d1ab538e37690ad31fe4f51e84120b55b6bc767661ed98ad9242f879db",
128
131
  "src/backend/lib/llm-client.js": "834cb8be7081e38b3a593e8559e6698a5211199e5652526a4f50d305a2134bbc",
129
132
  "src/backend/lib/model-context-windows.js": "f64f7c995143407b7390105087e3ebcd56a5a3ba2fd3019478a86f9e3c6b7896",
@@ -137,7 +140,7 @@
137
140
  "src/backend/lib/native-task-transcript.js": "b23cae1c34c0c83893f2436825f2009bc0b87ae1ec93c982d3fb8af78ed5e128",
138
141
  "src/backend/lib/prompt-registry.js": "9af496af837c7ae98cea2c5f55e81485dcaedf6f27c94b1d6d0384de897f94b9",
139
142
  "src/backend/lib/runtime-config.js": "8335525d2aff757f8022ab037dd00eb7bef7ff87a75ac946d599c2666b092323",
140
- "src/backend/lib/sdk-session-stream.js": "4e1b7dc007cf40762e4f52413509695b171e071ad029ac9da755092d1b2b7350",
143
+ "src/backend/lib/sdk-session-stream.js": "fc8b177417b71d47d8c1b645784910f955af099b82c9aa0934a8c26f3b87c4ba",
141
144
  "src/backend/lib/sdk-turn-stream.js": "3177bbec81cc0d4b1b578305056e4547b26069dde4f0133766a8a16fa3d8f923",
142
145
  "src/backend/lib/store-scope.js": "c07640abe888490b87e9287c77dbc1972cd9b414d7669a07e5e22bf5266cd051",
143
146
  "src/backend/lib/task-identity.js": "5fbea33d0e31552e98dbea277f7f3fe8c46ba47d53e84451161615b50c297c09",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanqianx/project-knowledge",
3
- "version": "5.0.26",
3
+ "version": "5.0.28",
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": {
@@ -93,7 +93,7 @@
93
93
  "@sanqianx/vector-hub": "1.0.8-rc.8",
94
94
  "buffer": "^6.0.3",
95
95
  "cheerio": "^1.2.0",
96
- "claude-ai-workbench": "npm:@sanqianx/claude-ai-workbench@0.13.9",
96
+ "claude-ai-workbench": "npm:@sanqianx/claude-ai-workbench@0.13.11",
97
97
  "cross-spawn": "7.0.6",
98
98
  "dotenv": "^17.4.2",
99
99
  "gpt-tokenizer": "^3.4.0",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "project-knowledge",
4
4
  "hooks": "./hooks/claude.json",
5
- "version": "5.0.26",
5
+ "version": "5.0.28",
6
6
  "description": "Read durable project decisions and implementation history from Project Knowledge.",
7
7
  "author": {
8
8
  "name": "SanQianX",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-knowledge",
3
- "version": "5.0.26",
3
+ "version": "5.0.28",
4
4
  "description": "Read durable project decisions and implementation history from Project Knowledge.",
5
5
  "author": {
6
6
  "name": "SanQianX",
@@ -8,7 +8,7 @@
8
8
  "const fs=require('fs'),path=require('path'),os=require('os');const PKG='@sanqianx/project-knowledge',SC='project-knowledge/integration-runtime/v1';let f='';const root=process.env.CLAUDE_PLUGIN_ROOT||process.env.PLUGIN_ROOT;if(root){f=path.join(root,'scripts','mcp-launcher.cjs');if(!fs.existsSync(f))f=''}if(!f){const dd=process.env.KB_DATA_DIR||path.join(os.homedir(),'.project-knowledge-v5');try{const bp=path.join(dd,'integration-runtime.json');const st=fs.statSync(bp);if(st.isFile()&&st.size<=32768){const b=JSON.parse(fs.readFileSync(bp,'utf8'));if(b&&b.schema===SC&&b.packageName===PKG&&typeof b.packageRoot==='string'&&path.isAbsolute(b.packageRoot)){const c=path.join(b.packageRoot,'plugins','project-knowledge','scripts','mcp-launcher.cjs');const rr=fs.realpathSync(b.packageRoot),cr=fs.realpathSync(c);const rel=path.relative(rr,cr);if(rel&&rel!=='..'&&!rel.startsWith('..'+path.sep)&&!path.isAbsolute(rel)&&fs.statSync(cr).isFile())f=cr}}}catch(e){}}if(!f){console.error('[project-knowledge-mcp] 未能通过受信任插件根或已验证的安装绑定定位启动器,已停止;不访问网络,也不加载工作目录源码。');process.exit(127)}require(f);"
9
9
  ],
10
10
  "env": {
11
- "KB_PLUGIN_VERSION": "5.0.26"
11
+ "KB_PLUGIN_VERSION": "5.0.28"
12
12
  }
13
13
  }
14
14
  }
package/ui/index.html CHANGED
@@ -58,6 +58,7 @@
58
58
  <dialog id="importDialog"><form id="importForm"><div class="dialog-heading"><h2>导入项目</h2><button type="button" data-close="importDialog" aria-label="关闭">×</button></div><p class="muted">绑定代码与新的空知识目录。只建立跟踪基线,不读取旧知识、不分析全部历史。</p><label>代码目录<div class="input-with-action"><input name="localPath" id="importCode" required placeholder="D:\SanQian.Xu\my-project"><button type="button" id="pickCode">选择</button></div></label><label>项目名称<input name="displayName" id="importName" required maxlength="200"></label><label>知识目录<div class="input-with-action"><input name="knowledgePath" id="importKnowledge" required placeholder="D:\SanQian.Xu\knowledge\my-project"><button type="button" id="pickKnowledge">选择</button></div></label><label>所属领域<select name="domainId" id="importDomain"></select></label><label id="gitInitChoice" class="checkbox" hidden><input type="checkbox" name="confirmGitInit">这个目录不是 Git 仓库,我确认初始化 Git。</label><label id="replaceHookChoice" class="checkbox" hidden><input type="checkbox" name="confirmReplaceHook">目录留有旧知识库的托管 Hook,我确认替换为新项目接线。不会覆盖第三方 Hook。</label><div id="preflightStatus" class="feedback" role="status">填写目录后检查导入条件。</div><button type="button" id="setupImportRoot" hidden>设置知识根目录</button><div class="dialog-actions"><button type="button" data-close="importDialog">取消</button><button type="submit" class="primary" id="confirmImport" disabled>导入项目</button></div></form></dialog>
59
59
  <dialog id="domainDialog"><form id="domainForm"><div class="dialog-heading"><h2>创建领域</h2><button type="button" data-close="domainDialog" aria-label="关闭">×</button></div><label>领域名称<input name="name" required maxlength="100" placeholder="例如:开发工具"></label><p class="muted">领域用于共享相关项目的知识,不是新的代码项目。</p><div class="dialog-actions"><button type="button" data-close="domainDialog">取消</button><button class="primary" type="submit">创建领域</button></div></form></dialog>
60
60
  <dialog id="memoryDialog"><form id="memoryForm"><div class="dialog-heading"><h2 id="memoryDialogTitle">审核记忆</h2><button type="button" data-close="memoryDialog" aria-label="关闭">×</button></div><p id="manualMemoryHint" class="muted" hidden>来源为手动添加。确认后保存为已确认记忆,无需等待提交或 Agent 提取。</p><label>记忆内容<textarea id="memoryText" maxlength="2000" required rows="5" placeholder="例如:对外错误信息使用中文;优先复用已有模块。"></textarea></label><label>适用范围<select id="memoryScope"><option value="global">全局:所有领域与项目</option><option value="domain">领域:指定领域内的项目</option><option value="project">项目:仅指定项目</option></select></label><label id="memoryTargetLabel">适用目标<select id="memoryTarget" required></select></label><p id="memoryScopeHint" class="muted"></p><details class="memory-evidence" id="memoryEvidenceDetails"><summary>原始总结与来源依据(只读)</summary><p id="memoryOriginal"></p><p id="memoryEvidence" class="muted"></p><blockquote id="memoryQuote"></blockquote></details><p id="memoryFeedback" class="feedback" hidden role="status"></p><div class="dialog-actions"><button type="button" data-close="memoryDialog">取消</button><button type="submit" id="saveMemoryCandidate" data-mode="candidate">保存候选</button><button type="submit" id="confirmMemory" class="primary" data-mode="confirmed">确认并保存</button></div></form></dialog>
61
+ <dialog id="memoryDeleteDialog"><div class="dialog-heading"><h2>删除记忆</h2><button type="button" data-close="memoryDeleteDialog" aria-label="关闭">×</button></div><p id="memoryDeleteText"></p><p id="memoryDeleteScope" class="muted"></p><p class="muted">删除后这条记忆不再作为长期记忆使用,也不会出现在记忆列表或新任务的参考内容中。原始对话和知识来源保持不变。之后仍可随时手动重新添加。</p><p id="memoryDeleteFeedback" class="feedback" hidden role="status"></p><div class="dialog-actions"><button type="button" data-close="memoryDeleteDialog">取消</button><button type="button" class="danger" id="confirmMemoryDelete">删除</button></div></dialog>
61
62
  <dialog id="unbindDialog"><form id="unbindForm"><div class="dialog-heading"><h2>解绑项目</h2><button type="button" data-close="unbindDialog" aria-label="关闭">×</button></div><p id="unbindProjectName"></p><p class="muted">项目将从列表移除,取消尚未开始的维护任务。代码、知识文件、提交证据和会话历史仍保留。正在执行的知识写入结束前不会解绑。</p><p id="unbindFeedback" class="feedback" role="status"></p><div class="dialog-actions"><button type="button" data-close="unbindDialog">取消</button><button class="primary" type="submit" id="confirmUnbind">解绑并保留数据</button></div></form></dialog>
62
63
  <dialog id="referenceDialog" class="reference-dialog"><div class="dialog-heading"><h2 id="referenceTitle">知识引用</h2><div class="actions"><button id="copyReference">复制引用</button><button data-close="referenceDialog" aria-label="关闭">×</button></div></div><p id="referenceMeta" class="muted"></p><pre id="referenceContent"></pre></dialog>
63
64
  <dialog id="engineDialog" class="engine-dialog"><div class="dialog-heading"><h2>知识引擎诊断</h2><button data-close="engineDialog" aria-label="关闭">×</button></div><iframe id="engineFrame" title="知识引擎诊断" referrerpolicy="same-origin"></iframe></dialog>