sandtable 0.3.0 → 0.4.0
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/dashboard/dashboard.html +5 -3
- package/harness/install-hooks.sh +40 -4
- package/package.json +1 -1
- package/server.js +45 -3
- package/src/cli/sandtable.js +467 -129
- package/src/scanner/scan.js +3 -3
package/dashboard/dashboard.html
CHANGED
|
@@ -285,10 +285,12 @@ body.filter-active #left{border-right-color:#d5dce6}
|
|
|
285
285
|
<div id="tree-tabs">
|
|
286
286
|
<button class="on" data-c="all">全部</button>
|
|
287
287
|
<button data-c="roadmap">路线图</button>
|
|
288
|
+
<button data-c="todo">待办</button>
|
|
288
289
|
<button data-c="decision">决策</button>
|
|
289
290
|
<button data-c="spec">规格</button>
|
|
290
291
|
<button data-c="convention">纪律</button>
|
|
291
292
|
<button data-c="ops">运维</button>
|
|
293
|
+
|
|
292
294
|
<button data-c="archive">档案</button>
|
|
293
295
|
</div>
|
|
294
296
|
<div id="tree"></div>
|
|
@@ -358,9 +360,9 @@ function toggleTagCat(cn){
|
|
|
358
360
|
if(expandedTagCats[cn]){expandedTagCats[cn]=false}else{expandedTagCats[cn]=true}
|
|
359
361
|
renderTagBar();
|
|
360
362
|
}
|
|
361
|
-
var CLABEL = {roadmap:'路线图与进度',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
|
|
362
|
-
var PRIMARY = {roadmap:1,decision:1};
|
|
363
|
-
var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'
|
|
363
|
+
var CLABEL = {roadmap:'路线图与进度',todo:'待办清单',decision:'决策记录',spec:'业务规格',convention:'协作纪律',ops:'运维与基建',archive:'历史档案',template:'工具模板',unknown:'未分类'};
|
|
364
|
+
var PRIMARY = {roadmap:1,decision:1,todo:1};
|
|
365
|
+
var ETYPE = {phase:'roadmap',milestone:'roadmap',task:'roadmap',subtask:'roadmap',roadmap:'roadmap',backlog:'todo',todo:'todo',conclusion:'roadmap',decision:'decision',refactor:'decision',spec:'spec',intent:'spec',prompt:'spec',convention:'convention',agent:'ops',runbook:'ops',optimization:'roadmap',journal:'archive',handover:'archive',plan_doc:'archive',template:'template'};
|
|
364
366
|
function normCat(c){if(!c)return'unknown';var m={conventions:'convention',specs:'spec',decisions:'decision',plans:'roadmap',journals:'archive',journal:'archive'};return m[c]||c}
|
|
365
367
|
function normTg(tg){if(!tg||tg==='archived')return'past';return tg}
|
|
366
368
|
|
package/harness/install-hooks.sh
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Install sandtable git hooks into the current repo
|
|
3
|
-
# Usage: sh harness/install-hooks.sh [project-root]
|
|
3
|
+
# Usage: sh harness/install-hooks.sh [project-root] [--force]
|
|
4
|
+
|
|
5
|
+
ROOT=""
|
|
6
|
+
FORCE=0
|
|
7
|
+
for arg in "$@"; do
|
|
8
|
+
case "$arg" in
|
|
9
|
+
--force) FORCE=1 ;;
|
|
10
|
+
*) ROOT="$arg" ;;
|
|
11
|
+
esac
|
|
12
|
+
done
|
|
13
|
+
ROOT="${ROOT:-$(pwd)}"
|
|
4
14
|
|
|
5
|
-
ROOT="${1:-$(pwd)}"
|
|
6
15
|
HOOKS_DIR="$ROOT/.git/hooks"
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
17
|
|
|
8
18
|
if [ ! -d "$HOOKS_DIR" ]; then
|
|
9
19
|
echo "Error: not a git repository (no .git/hooks)"
|
|
10
20
|
exit 1
|
|
11
21
|
fi
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
# ---- Conflict detection: check for existing non-sandtable hooks ----
|
|
24
|
+
CONFLICTS=""
|
|
25
|
+
for hook in post-commit post-merge; do
|
|
26
|
+
HOOK_PATH="$HOOKS_DIR/$hook"
|
|
27
|
+
if [ -f "$HOOK_PATH" ] && ! grep -q "sandtable" "$HOOK_PATH" 2>/dev/null; then
|
|
28
|
+
CONFLICTS="$CONFLICTS $hook"
|
|
29
|
+
fi
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
if [ -n "$CONFLICTS" ] && [ "$FORCE" != "1" ]; then
|
|
33
|
+
echo "Error: 已有非 sandtable hook 存在:$CONFLICTS"
|
|
34
|
+
echo ""
|
|
35
|
+
echo "sandtable 不会覆盖已有 hook,以免破坏其他工具链。选项:"
|
|
36
|
+
echo " 1. 手动合并: 在现有 hook 末尾添加以下行:"
|
|
37
|
+
echo " sh .git/hooks/<hook>.sandtable"
|
|
38
|
+
echo " 2. 强制覆盖: sh $(basename "$0") --force"
|
|
39
|
+
echo ""
|
|
40
|
+
echo "已有 hook 内容预览:"
|
|
41
|
+
for hook in $CONFLICTS; do
|
|
42
|
+
echo " --- $hook ---"
|
|
43
|
+
sed 's/^/ | /' "$HOOKS_DIR/$hook"
|
|
44
|
+
echo " -------------"
|
|
45
|
+
done
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
cp "$SCRIPT_DIR/post-commit" "$HOOKS_DIR/post-commit.sandtable"
|
|
50
|
+
cp "$SCRIPT_DIR/post-merge" "$HOOKS_DIR/post-merge.sandtable"
|
|
15
51
|
chmod +x "$HOOKS_DIR/post-commit.sandtable"
|
|
16
52
|
chmod +x "$HOOKS_DIR/post-merge.sandtable"
|
|
17
53
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -3,7 +3,27 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
5
|
const ROOT = path.resolve(__dirname);
|
|
6
|
-
const PORT = parseInt(process.env.PORT || process.argv[2], 10) ||
|
|
6
|
+
const PORT = parseInt(process.env.PORT || process.argv[2], 10) || 5199;
|
|
7
|
+
|
|
8
|
+
// Parse --host and --token from CLI args (safe by default: 127.0.0.1)
|
|
9
|
+
let HOST = '127.0.0.1';
|
|
10
|
+
let TOKEN = '';
|
|
11
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
12
|
+
if (process.argv[i] === '--host' && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) {
|
|
13
|
+
HOST = process.argv[i + 1];
|
|
14
|
+
i++;
|
|
15
|
+
} else if (process.argv[i] === '--token') {
|
|
16
|
+
if (process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) {
|
|
17
|
+
TOKEN = process.argv[i + 1];
|
|
18
|
+
i++;
|
|
19
|
+
} else {
|
|
20
|
+
TOKEN = require('crypto').randomBytes(16).toString('hex');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if ((HOST === '0.0.0.0' || HOST === '::') && !TOKEN) {
|
|
25
|
+
TOKEN = require('crypto').randomBytes(16).toString('hex');
|
|
26
|
+
}
|
|
7
27
|
|
|
8
28
|
const MIME = {
|
|
9
29
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -31,6 +51,27 @@ function isAllowed(filePath) {
|
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
http.createServer((req, res) => {
|
|
54
|
+
// Token authentication (when binding to public interface)
|
|
55
|
+
if (TOKEN) {
|
|
56
|
+
let qIdx = req.url.indexOf('?');
|
|
57
|
+
let hasToken = false;
|
|
58
|
+
if (qIdx !== -1) {
|
|
59
|
+
let qs = req.url.substring(qIdx + 1);
|
|
60
|
+
let pairs = qs.split('&');
|
|
61
|
+
for (let pi = 0; pi < pairs.length; pi++) {
|
|
62
|
+
let kv = pairs[pi].split('=');
|
|
63
|
+
if (decodeURIComponent(kv[0]) === 'token' && kv[1] === TOKEN) {
|
|
64
|
+
hasToken = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!hasToken) {
|
|
70
|
+
res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
71
|
+
return res.end('401 Unauthorized — 请在 URL 后添加 ?token=' + TOKEN);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
let url = req.url === '/' ? '/dashboard/dashboard.html' : req.url;
|
|
35
76
|
url = url.replace(/^\/(css|js)\//, '/dashboard/$1/');
|
|
36
77
|
|
|
@@ -54,7 +95,8 @@ http.createServer((req, res) => {
|
|
|
54
95
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
55
96
|
res.end('500: ' + e.message);
|
|
56
97
|
}
|
|
57
|
-
}).listen(PORT, () => {
|
|
58
|
-
console.log('Sandtable: http://
|
|
98
|
+
}).listen(PORT, HOST, () => {
|
|
99
|
+
console.log('Sandtable: http://' + HOST + ':' + PORT);
|
|
100
|
+
if (TOKEN) console.log('Token: ' + TOKEN + ' (访问需携带 ?token=' + TOKEN + ')');
|
|
59
101
|
console.log('Project:', ROOT);
|
|
60
102
|
});
|
package/src/cli/sandtable.js
CHANGED
|
@@ -8,7 +8,7 @@ const { exec } = require('child_process');
|
|
|
8
8
|
const { scan } = require('../scanner/scan');
|
|
9
9
|
const { build, EVENT_TYPES, classifyEventPriority } = require('../builder/build');
|
|
10
10
|
|
|
11
|
-
const VERSION = '0.
|
|
11
|
+
const VERSION = '0.4.0';
|
|
12
12
|
const command = process.argv[2] || 'help';
|
|
13
13
|
const root = process.argv[3] || process.cwd();
|
|
14
14
|
|
|
@@ -194,33 +194,161 @@ function generateRuleContent(format, lang) {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// ---- injectToMainDoc: 向 CLAUDE.md / AGENTS.md
|
|
198
|
-
|
|
197
|
+
// ---- injectToMainDoc: 向 CLAUDE.md / AGENTS.md 注入事件日志规则 ----
|
|
198
|
+
// 使用 <!-- sandtable:begin -->...<!-- sandtable:end --> 标记包裹,uninstall 时精确擦除
|
|
199
|
+
function injectToMainDoc(filePath, lang, dryRun) {
|
|
199
200
|
var isZh = lang === 'zh';
|
|
200
|
-
var
|
|
201
|
+
var beginMark = '<!-- sandtable:begin -->';
|
|
202
|
+
var endMark = '<!-- sandtable:end -->';
|
|
201
203
|
var existing = '';
|
|
202
204
|
if (fs.existsSync(filePath)) {
|
|
203
205
|
existing = fs.readFileSync(filePath, 'utf-8');
|
|
204
|
-
if (existing.indexOf(
|
|
206
|
+
if (existing.indexOf(beginMark) !== -1) {
|
|
207
|
+
if (!dryRun) console.log(' - ' + path.basename(filePath) + ' 已有 sandtable 注入标记,跳过');
|
|
208
|
+
return false; // already injected
|
|
209
|
+
}
|
|
205
210
|
}
|
|
211
|
+
|
|
206
212
|
var lines = [];
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(beginMark);
|
|
207
215
|
if (isZh) {
|
|
208
|
-
lines.push('');
|
|
209
216
|
lines.push('## Sandtable 事件日志');
|
|
210
217
|
lines.push('');
|
|
211
218
|
lines.push('**每次完成可独立表述的代码/设计/决策动作后,立即调用 `sandtable event-log` 记录。不要等用户提醒。**');
|
|
212
219
|
lines.push('');
|
|
220
|
+
lines.push('命令速查: `sandtable event-log --type code-change --title "标题" --subtype code-done --impact medium --actor @AI --tags "标签" --thread threadId`');
|
|
221
|
+
lines.push('');
|
|
213
222
|
lines.push('详细规则见 `.sandtable/rules.md`。');
|
|
214
223
|
} else {
|
|
215
|
-
lines.push('');
|
|
216
224
|
lines.push('## Sandtable Event Logging');
|
|
217
225
|
lines.push('');
|
|
218
226
|
lines.push('**After every significant code/design/decision action, immediately call `sandtable event-log` to record it. Do not wait for a reminder.**');
|
|
219
227
|
lines.push('');
|
|
228
|
+
lines.push('Quick ref: `sandtable event-log --type code-change --title "title" --subtype code-done --impact medium --actor @AI --tags "tags" --thread threadId`');
|
|
229
|
+
lines.push('');
|
|
220
230
|
lines.push('See `.sandtable/rules.md` for details.');
|
|
221
231
|
}
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
lines.push(endMark);
|
|
233
|
+
|
|
234
|
+
var newContent = existing + lines.join('\n') + '\n';
|
|
235
|
+
|
|
236
|
+
if (dryRun) {
|
|
237
|
+
console.log(' [DRY-RUN] 将注入 ' + path.basename(filePath) + ':');
|
|
238
|
+
console.log(' --- diff ---');
|
|
239
|
+
console.log(' + ' + lines.slice(1, -1).join('\n + '));
|
|
240
|
+
console.log(' --- end ---');
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
writeWithBackup(filePath, newContent);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- removeFromMainDoc: uninstall 时识别标记精确擦除 ----
|
|
249
|
+
function removeFromMainDoc(filePath, dryRun) {
|
|
250
|
+
var beginMark = '<!-- sandtable:begin -->';
|
|
251
|
+
var endMark = '<!-- sandtable:end -->';
|
|
252
|
+
|
|
253
|
+
if (!fs.existsSync(filePath)) return false;
|
|
254
|
+
|
|
255
|
+
var content = fs.readFileSync(filePath, 'utf-8');
|
|
256
|
+
var beginIdx = content.indexOf(beginMark);
|
|
257
|
+
var endIdx = content.indexOf(endMark);
|
|
258
|
+
|
|
259
|
+
if (beginIdx === -1 || endIdx === -1) return false;
|
|
260
|
+
|
|
261
|
+
// Find the newline before beginMark (to clean up the blank line)
|
|
262
|
+
var beforeBegin = content.lastIndexOf('\n', beginIdx);
|
|
263
|
+
if (beforeBegin === -1) beforeBegin = 0;
|
|
264
|
+
|
|
265
|
+
var afterEnd = endIdx + endMark.length;
|
|
266
|
+
// Consume trailing newline if present
|
|
267
|
+
if (content[afterEnd] === '\n') afterEnd++;
|
|
268
|
+
|
|
269
|
+
if (dryRun) {
|
|
270
|
+
console.log(' [DRY-RUN] 将从 ' + path.basename(filePath) + ' 移除注入块:');
|
|
271
|
+
console.log(' --- to remove ---');
|
|
272
|
+
console.log(content.substring(beforeBegin, afterEnd).replace(/^/gm, ' - '));
|
|
273
|
+
console.log(' --- end ---');
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
var cleaned = content.substring(0, beforeBegin) + content.substring(afterEnd);
|
|
278
|
+
writeWithBackup(filePath, cleaned);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---- appendToGitignore: 自动追加 sandtable 条目到 .gitignore ----
|
|
283
|
+
// 使用 # sandtable:begin/end 标记包裹,uninstall 时精确擦除
|
|
284
|
+
function appendToGitignore(projectRoot, dryRun) {
|
|
285
|
+
var gitignorePath = path.join(projectRoot, '.gitignore');
|
|
286
|
+
var beginMark = '# sandtable:begin';
|
|
287
|
+
var endMark = '# sandtable:end';
|
|
288
|
+
var existing = '';
|
|
289
|
+
if (fs.existsSync(gitignorePath)) {
|
|
290
|
+
existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
291
|
+
if (existing.indexOf(beginMark) !== -1) {
|
|
292
|
+
if (!dryRun) console.log(' - .gitignore 已有 sandtable 标记,跳过');
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
var block = [
|
|
298
|
+
'',
|
|
299
|
+
beginMark,
|
|
300
|
+
'# Sandtable 自动生成条目 — 请勿手动编辑此块',
|
|
301
|
+
'data/',
|
|
302
|
+
'.sandtable/token-log.jsonl',
|
|
303
|
+
'.sandtable/audit.log',
|
|
304
|
+
'.sandtable/.token',
|
|
305
|
+
endMark,
|
|
306
|
+
'',
|
|
307
|
+
].join('\n');
|
|
308
|
+
|
|
309
|
+
if (dryRun) {
|
|
310
|
+
console.log(' [DRY-RUN] 将追加到 .gitignore:');
|
|
311
|
+
console.log(' --- diff ---');
|
|
312
|
+
console.log(' + ' + block.replace(/\n/g, '\n + '));
|
|
313
|
+
console.log(' --- end ---');
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
writeWithBackup(gitignorePath, existing + block);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- removeFromGitignore: uninstall 时擦除 sandtable gitignore 块 ----
|
|
322
|
+
function removeFromGitignore(projectRoot, dryRun) {
|
|
323
|
+
var gitignorePath = path.join(projectRoot, '.gitignore');
|
|
324
|
+
var beginMark = '# sandtable:begin';
|
|
325
|
+
var endMark = '# sandtable:end';
|
|
326
|
+
|
|
327
|
+
if (!fs.existsSync(gitignorePath)) return false;
|
|
328
|
+
|
|
329
|
+
var content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
330
|
+
var beginIdx = content.indexOf(beginMark);
|
|
331
|
+
var endIdx = content.indexOf(endMark);
|
|
332
|
+
|
|
333
|
+
if (beginIdx === -1 || endIdx === -1) return false;
|
|
334
|
+
|
|
335
|
+
var beforeBegin = content.lastIndexOf('\n', beginIdx);
|
|
336
|
+
if (beforeBegin === -1) beforeBegin = 0;
|
|
337
|
+
|
|
338
|
+
var afterEnd = endIdx + endMark.length;
|
|
339
|
+
if (content[afterEnd] === '\n') afterEnd++;
|
|
340
|
+
if (content[afterEnd] === '\n') afterEnd++; // consume trailing blank line
|
|
341
|
+
|
|
342
|
+
if (dryRun) {
|
|
343
|
+
console.log(' [DRY-RUN] 将从 .gitignore 移除 sandtable 块:');
|
|
344
|
+
console.log(' --- to remove ---');
|
|
345
|
+
console.log(content.substring(beforeBegin, afterEnd).replace(/^/gm, ' - '));
|
|
346
|
+
console.log(' --- end ---');
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
var cleaned = content.substring(0, beforeBegin) + content.substring(afterEnd);
|
|
351
|
+
writeWithBackup(gitignorePath, cleaned);
|
|
224
352
|
return true;
|
|
225
353
|
}
|
|
226
354
|
|
|
@@ -238,8 +366,8 @@ function writeClaudeCodeHook(projectRoot, lang) {
|
|
|
238
366
|
script += '【Sandtable 事件日志铁律】\n';
|
|
239
367
|
script += '本次 session 中,每完成一个独立动作(代码修改/设计定稿/决策拍板/错误修复),\n';
|
|
240
368
|
script += '必须立即调用 sandtable event-log 记录。禁止事后补录,禁止等用户提醒。\n';
|
|
241
|
-
script += '命令速查: sandtable event-log
|
|
242
|
-
script += '
|
|
369
|
+
script += '命令速查: sandtable event-log --type code-change --title "标题" --subtype code-done --impact medium --actor @AI --tags "标签"\n';
|
|
370
|
+
script += 'type: alignment | spec | code-change | test | approval | ops | lesson\n';
|
|
243
371
|
script += '详细规则见 .sandtable/rules.md\n';
|
|
244
372
|
script += '</system-reminder>\n';
|
|
245
373
|
script += 'SANDBTABLE_EOR\n';
|
|
@@ -250,7 +378,8 @@ function writeClaudeCodeHook(projectRoot, lang) {
|
|
|
250
378
|
script += '[Sandtable Event Log Rule]\n';
|
|
251
379
|
script += 'After every significant action (code change, design decision, bug fix) in this session,\n';
|
|
252
380
|
script += 'immediately call sandtable event-log to record it. No backfilling, no waiting for reminders.\n';
|
|
253
|
-
script += 'Quick ref: sandtable event-log
|
|
381
|
+
script += 'Quick ref: sandtable event-log --type code-change --title "title" --subtype code-done --impact medium --actor @AI --tags "tags"\n';
|
|
382
|
+
script += 'type: alignment | spec | code-change | test | approval | ops | lesson\n';
|
|
254
383
|
script += 'See .sandtable/rules.md for details.\n';
|
|
255
384
|
script += '</system-reminder>\n';
|
|
256
385
|
script += 'SANDBTABLE_EOR\n';
|
|
@@ -285,7 +414,7 @@ function writeClaudeCodeHook(projectRoot, lang) {
|
|
|
285
414
|
}
|
|
286
415
|
|
|
287
416
|
// ---- initCommand: dry-run 报告 or --apply 写入 ----
|
|
288
|
-
function initCommand(projectRoot, applyMode, lang, hooksMode) {
|
|
417
|
+
function initCommand(projectRoot, applyMode, lang, hooksMode, injectAgentsMd) {
|
|
289
418
|
var envs = detectEnvironment(projectRoot);
|
|
290
419
|
var sandtableDir = path.join(projectRoot, '.sandtable');
|
|
291
420
|
var configPath = path.join(sandtableDir, 'config.json');
|
|
@@ -325,7 +454,11 @@ function initCommand(projectRoot, applyMode, lang, hooksMode) {
|
|
|
325
454
|
}
|
|
326
455
|
if (autoHints.length > 0) {
|
|
327
456
|
console.log('');
|
|
328
|
-
|
|
457
|
+
if (injectAgentsMd) {
|
|
458
|
+
console.log('⚡ --inject-agents-md 启用 → 将注入主文档:');
|
|
459
|
+
} else {
|
|
460
|
+
console.log('💡 可选: --inject-agents-md 自动注入事件日志规则到主文档');
|
|
461
|
+
}
|
|
329
462
|
for (var i = 0; i < autoHints.length; i++) {
|
|
330
463
|
console.log(' ' + (i + 1) + '. ' + autoHints[i]);
|
|
331
464
|
}
|
|
@@ -352,9 +485,11 @@ function initCommand(projectRoot, applyMode, lang, hooksMode) {
|
|
|
352
485
|
if (!applyMode) {
|
|
353
486
|
console.log('');
|
|
354
487
|
console.log('以上为 dry-run 报告。要实际写入文件,请运行:');
|
|
355
|
-
console.log(' sandtable init --apply' + (lang ? ' --lang ' + lang : '') + (hasClaudeEnv ? ' [--hooks]' : ''));
|
|
488
|
+
console.log(' sandtable init --apply' + (lang ? ' --lang ' + lang : '') + (hasClaudeEnv ? ' [--hooks]' : '') + ' [--inject-agents-md]');
|
|
356
489
|
console.log('');
|
|
357
490
|
console.log('已存在文件将在写入前备份为 .bak.<timestamp>');
|
|
491
|
+
console.log('');
|
|
492
|
+
console.log('💡 --inject-agents-md: 注入事件日志规则到 CLAUDE.md/AGENTS.md(使用 <!-- sandtable:begin/end --> 标记包裹,uninstall 可精确擦除)');
|
|
358
493
|
return;
|
|
359
494
|
}
|
|
360
495
|
|
|
@@ -370,7 +505,7 @@ function initCommand(projectRoot, applyMode, lang, hooksMode) {
|
|
|
370
505
|
|
|
371
506
|
// 2. Write .sandtable/config.json
|
|
372
507
|
var config = {
|
|
373
|
-
version: '0.
|
|
508
|
+
version: '0.4.0',
|
|
374
509
|
createdAt: new Date().toISOString(),
|
|
375
510
|
projectRoot: projectRoot,
|
|
376
511
|
paths: {
|
|
@@ -414,25 +549,30 @@ function initCommand(projectRoot, applyMode, lang, hooksMode) {
|
|
|
414
549
|
writeWithBackup(fullPath, content);
|
|
415
550
|
}
|
|
416
551
|
|
|
417
|
-
// 6. Inject event-log rule into main doc (
|
|
552
|
+
// 6. Inject event-log rule into main doc (only with --inject-agents-md)
|
|
418
553
|
var mainDocInjected = false;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
554
|
+
if (injectAgentsMd) {
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log('注入主文档 (--inject-agents-md):');
|
|
557
|
+
for (var i = 0; i < envs.length; i++) {
|
|
558
|
+
var env = envs[i];
|
|
559
|
+
var mainDocPath = null;
|
|
560
|
+
if (env.type === 'agents') {
|
|
561
|
+
mainDocPath = path.join(projectRoot, 'AGENTS.md');
|
|
562
|
+
} else if (env.type === 'claude-md') {
|
|
563
|
+
mainDocPath = path.join(projectRoot, 'CLAUDE.md');
|
|
564
|
+
} else if (env.type === 'generic') {
|
|
565
|
+
mainDocPath = path.join(projectRoot, 'AGENTS.md');
|
|
566
|
+
}
|
|
567
|
+
if (mainDocPath) {
|
|
568
|
+
var usedDryRun = !applyMode; // In dry-run, show diff instead of writing
|
|
569
|
+
var injected = injectToMainDoc(mainDocPath, lang, usedDryRun);
|
|
570
|
+
if (injected) {
|
|
571
|
+
if (applyMode) console.log(' ✓ 注入事件日志规则 → ' + path.basename(mainDocPath));
|
|
572
|
+
mainDocInjected = true;
|
|
573
|
+
} else if (fs.existsSync(mainDocPath)) {
|
|
574
|
+
console.log(' - ' + path.basename(mainDocPath) + ' 已有 sandtable 规则,跳过');
|
|
575
|
+
}
|
|
436
576
|
}
|
|
437
577
|
}
|
|
438
578
|
}
|
|
@@ -443,6 +583,13 @@ function initCommand(projectRoot, applyMode, lang, hooksMode) {
|
|
|
443
583
|
console.log(' ✓ 安装 SessionStart 强提醒 hook → .claude/hooks/sandtable-reminder.sh');
|
|
444
584
|
}
|
|
445
585
|
|
|
586
|
+
// 8. Append sandtable entries to .gitignore (auto, always)
|
|
587
|
+
var gitignoreDryRun = !applyMode;
|
|
588
|
+
appendToGitignore(projectRoot, gitignoreDryRun);
|
|
589
|
+
if (applyMode && fs.existsSync(path.join(projectRoot, '.gitignore'))) {
|
|
590
|
+
console.log(' ✓ 追加 sandtable 条目至 .gitignore');
|
|
591
|
+
}
|
|
592
|
+
|
|
446
593
|
console.log('');
|
|
447
594
|
console.log('✓ 初始化完成。运行 sandtable build 生成数据,然后 sandtable serve 查看仪表盘。');
|
|
448
595
|
|
|
@@ -525,7 +672,9 @@ function enableWatchMode(projectRoot) {
|
|
|
525
672
|
}
|
|
526
673
|
}
|
|
527
674
|
|
|
528
|
-
function startServer(projectRoot, port, openBrowser, watchMode) {
|
|
675
|
+
function startServer(projectRoot, port, openBrowser, watchMode, host, token) {
|
|
676
|
+
host = host || '127.0.0.1';
|
|
677
|
+
token = token || '';
|
|
529
678
|
const MIME = {
|
|
530
679
|
'.html': 'text/html; charset=utf-8',
|
|
531
680
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -562,6 +711,27 @@ function startServer(projectRoot, port, openBrowser, watchMode) {
|
|
|
562
711
|
}
|
|
563
712
|
|
|
564
713
|
http.createServer(function(req, res) {
|
|
714
|
+
// Token authentication (when binding to public interface)
|
|
715
|
+
if (token) {
|
|
716
|
+
var qIdx = req.url.indexOf('?');
|
|
717
|
+
var hasToken = false;
|
|
718
|
+
if (qIdx !== -1) {
|
|
719
|
+
var qs = req.url.substring(qIdx + 1);
|
|
720
|
+
var pairs = qs.split('&');
|
|
721
|
+
for (var pi = 0; pi < pairs.length; pi++) {
|
|
722
|
+
var kv = pairs[pi].split('=');
|
|
723
|
+
if (decodeURIComponent(kv[0]) === 'token' && kv[1] === token) {
|
|
724
|
+
hasToken = true;
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (!hasToken) {
|
|
730
|
+
res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
731
|
+
return res.end('401 Unauthorized — 请在 URL 后添加 ?token=' + token);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
565
735
|
var url = req.url === '/' ? '/dashboard/dashboard.html' : req.url;
|
|
566
736
|
url = url.replace(/^\/(css|js)\//, '/dashboard/$1/');
|
|
567
737
|
|
|
@@ -599,9 +769,12 @@ function startServer(projectRoot, port, openBrowser, watchMode) {
|
|
|
599
769
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
600
770
|
res.end('500: ' + e.message);
|
|
601
771
|
}
|
|
602
|
-
}).listen(port, function() {
|
|
603
|
-
console.log('Sandtable v' + VERSION + ' — http://
|
|
772
|
+
}).listen(port, host, function() {
|
|
773
|
+
console.log('Sandtable v' + VERSION + ' — http://' + host + ':' + port);
|
|
604
774
|
console.log('Project: ' + projectRoot);
|
|
775
|
+
if (token) {
|
|
776
|
+
console.log('Token: ' + token + ' (访问需携带 ?token=' + token + ')');
|
|
777
|
+
}
|
|
605
778
|
|
|
606
779
|
if (watchMode) enableWatchMode(projectRoot);
|
|
607
780
|
|
|
@@ -632,12 +805,13 @@ switch (command) {
|
|
|
632
805
|
case 'init': {
|
|
633
806
|
var applyMode = process.argv.indexOf('--apply') !== -1;
|
|
634
807
|
var hooksMode = process.argv.indexOf('--hooks') !== -1;
|
|
808
|
+
var injectAgentsMd = process.argv.indexOf('--inject-agents-md') !== -1;
|
|
635
809
|
var langIdx = process.argv.indexOf('--lang');
|
|
636
810
|
var lang = 'zh';
|
|
637
811
|
if (langIdx !== -1 && process.argv[langIdx + 1]) {
|
|
638
812
|
lang = process.argv[langIdx + 1] === 'en' ? 'en' : 'zh';
|
|
639
813
|
}
|
|
640
|
-
initCommand(process.cwd(), applyMode, lang, hooksMode);
|
|
814
|
+
initCommand(process.cwd(), applyMode, lang, hooksMode, injectAgentsMd);
|
|
641
815
|
break;
|
|
642
816
|
}
|
|
643
817
|
case 'summarize': {
|
|
@@ -661,109 +835,227 @@ switch (command) {
|
|
|
661
835
|
console.log('sandtable v' + VERSION);
|
|
662
836
|
break;
|
|
663
837
|
case 'serve': {
|
|
664
|
-
var port = parseInt(process.argv[3], 10) ||
|
|
838
|
+
var port = parseInt(process.argv[3], 10) || 5199;
|
|
665
839
|
var watchMode = process.argv.indexOf('--watch') !== -1;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
var
|
|
676
|
-
var
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
840
|
+
|
|
841
|
+
// Parse --host (default: 127.0.0.1, safe by default)
|
|
842
|
+
var hostIdx = process.argv.indexOf('--host');
|
|
843
|
+
var host = '127.0.0.1';
|
|
844
|
+
if (hostIdx !== -1 && process.argv[hostIdx + 1] && process.argv[hostIdx + 1].indexOf('--') !== 0) {
|
|
845
|
+
host = process.argv[hostIdx + 1];
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Parse --token (required when binding to 0.0.0.0)
|
|
849
|
+
var tokenIdx = process.argv.indexOf('--token');
|
|
850
|
+
var token = '';
|
|
851
|
+
var tokenPath = path.join(process.cwd(), '.sandtable', '.token');
|
|
852
|
+
if (tokenIdx !== -1) {
|
|
853
|
+
var nextArg = process.argv[tokenIdx + 1];
|
|
854
|
+
if (nextArg && nextArg.indexOf('--') !== 0) {
|
|
855
|
+
token = nextArg;
|
|
856
|
+
// Persist user-provided token
|
|
857
|
+
try { fs.writeFileSync(tokenPath, token, 'utf-8'); } catch(e) {}
|
|
682
858
|
} else {
|
|
683
|
-
|
|
859
|
+
token = require('crypto').randomBytes(16).toString('hex');
|
|
860
|
+
try { fs.writeFileSync(tokenPath, token, 'utf-8'); } catch(e) {}
|
|
684
861
|
}
|
|
685
862
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
var threadId = contThreadId || cleanArgs[7] || '';
|
|
695
|
-
|
|
696
|
-
if (!title) {
|
|
697
|
-
console.log('用法: sandtable event-log <typeId> <title> [subtype] [impact] [actor] [tags] [refDoc] [threadId] [--tokens <in>,<out>] [--continue <threadId>]');
|
|
698
|
-
console.log(' typeId: 1=对齐与拍板 2=规格演进 3=代码变更 4=测试与质量 5=审批与交接 6=运维与基建 7=教训沉淀');
|
|
699
|
-
process.exit(1);
|
|
863
|
+
if ((host === '0.0.0.0' || host === '::') && !token) {
|
|
864
|
+
// Try to load persisted token first
|
|
865
|
+
try { if (fs.existsSync(tokenPath)) token = fs.readFileSync(tokenPath, 'utf-8').trim(); } catch(e) {}
|
|
866
|
+
if (!token) {
|
|
867
|
+
token = require('crypto').randomBytes(16).toString('hex');
|
|
868
|
+
try { fs.writeFileSync(tokenPath, token, 'utf-8'); } catch(e) {}
|
|
869
|
+
}
|
|
870
|
+
console.log('⚠ 绑定 ' + host + ' 将暴露服务到网络,token: ' + token);
|
|
700
871
|
}
|
|
701
872
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
873
|
+
startServer(process.cwd(), port, true, watchMode, host, token);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
case 'event-log': {
|
|
877
|
+
// Named flags (v0.3.1+, recommended):
|
|
878
|
+
// sandtable event-log --type <slug> --title <text> [--subtype <text>] [--impact <level>]
|
|
879
|
+
// [--actor <@AI|@user>] [--tags <tags>] [--ref <path>] [--thread <id>]
|
|
880
|
+
// [--tokens-in <n> --tokens-out <n>] [--project <path>]
|
|
881
|
+
//
|
|
882
|
+
// Legacy positional (deprecated):
|
|
883
|
+
// sandtable event-log <typeId> <title> [subtype] [impact] [actor] [tags] [refDoc] [threadId]
|
|
884
|
+
// [--tokens <in>,<out>] [--continue <threadId>]
|
|
885
|
+
|
|
886
|
+
// Type slug → typeId mapping
|
|
887
|
+
var TYPE_SLUGS = {
|
|
888
|
+
'alignment': '1', 'spec': '2', 'code-change': '3',
|
|
889
|
+
'test': '4', 'approval': '5', 'ops': '6', 'lesson': '7',
|
|
890
|
+
};
|
|
891
|
+
var TYPE_SLUG_NAMES = {
|
|
892
|
+
'1': 'alignment', '2': 'spec', '3': 'code-change',
|
|
893
|
+
'4': 'test', '5': 'approval', '6': 'ops', '7': 'lesson',
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
var rawArgs = process.argv.slice(3);
|
|
897
|
+
|
|
898
|
+
// Detect mode: if first arg starts with --, use named mode
|
|
899
|
+
var isNamedMode = rawArgs.length > 0 && rawArgs[0].indexOf('--') === 0;
|
|
900
|
+
|
|
901
|
+
var typeId, title, subtype, impact, actor, tagsRaw, refDoc, threadId, projectRoot;
|
|
902
|
+
var tokensIn = 0, tokensOut = 0;
|
|
903
|
+
|
|
904
|
+
if (isNamedMode) {
|
|
905
|
+
// ---- Named flag mode ----
|
|
906
|
+
var flagMap = {};
|
|
907
|
+
for (var ai = 0; ai < rawArgs.length; ai++) {
|
|
908
|
+
var arg = rawArgs[ai];
|
|
909
|
+
if (arg.indexOf('--') === 0) {
|
|
910
|
+
var key = arg.replace(/^--/, '');
|
|
911
|
+
var val = (ai + 1 < rawArgs.length && rawArgs[ai + 1].indexOf('--') !== 0) ? rawArgs[ai + 1] : 'true';
|
|
912
|
+
flagMap[key] = val;
|
|
913
|
+
if (val !== 'true') ai++;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
707
916
|
|
|
708
|
-
|
|
709
|
-
|
|
917
|
+
var typeSlug = flagMap['type'] || '';
|
|
918
|
+
typeId = TYPE_SLUGS[typeSlug] || '';
|
|
919
|
+
if (!typeId) {
|
|
920
|
+
console.log('错误:--type 必须是以下之一:');
|
|
921
|
+
console.log(' alignment | spec | code-change | test | approval | ops | lesson');
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
710
924
|
|
|
711
|
-
|
|
712
|
-
|
|
925
|
+
title = flagMap['title'] || '';
|
|
926
|
+
subtype = flagMap['subtype'] || 'manual';
|
|
927
|
+
impact = flagMap['impact'] || 'medium';
|
|
928
|
+
actor = flagMap['actor'] || 'AI';
|
|
929
|
+
tagsRaw = flagMap['tags'] || '';
|
|
930
|
+
refDoc = flagMap['ref'] || '';
|
|
931
|
+
threadId = flagMap['thread'] || '';
|
|
932
|
+
tokensIn = parseInt(flagMap['tokens-in'], 10) || 0;
|
|
933
|
+
tokensOut = parseInt(flagMap['tokens-out'], 10) || 0;
|
|
934
|
+
projectRoot = flagMap['project'] || process.cwd();
|
|
713
935
|
|
|
714
|
-
|
|
715
|
-
|
|
936
|
+
} else {
|
|
937
|
+
// ---- Legacy positional mode (deprecated) ----
|
|
938
|
+
var contThreadId = null;
|
|
939
|
+
var tokensVal = null;
|
|
940
|
+
var cleanArgs = [];
|
|
941
|
+
for (var ai = 0; ai < rawArgs.length; ai++) {
|
|
942
|
+
if (rawArgs[ai] === '--continue' && ai + 1 < rawArgs.length) {
|
|
943
|
+
contThreadId = rawArgs[ai + 1]; ai++;
|
|
944
|
+
} else if (rawArgs[ai] === '--tokens' && ai + 1 < rawArgs.length) {
|
|
945
|
+
tokensVal = rawArgs[ai + 1]; ai++;
|
|
946
|
+
} else {
|
|
947
|
+
cleanArgs.push(rawArgs[ai]);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
716
950
|
|
|
717
|
-
|
|
951
|
+
typeId = cleanArgs[0] || '3';
|
|
952
|
+
title = cleanArgs[1] || '';
|
|
953
|
+
subtype = cleanArgs[2] || 'manual';
|
|
954
|
+
impact = cleanArgs[3] || 'medium';
|
|
955
|
+
actor = cleanArgs[4] || 'AI';
|
|
956
|
+
tagsRaw = cleanArgs[5] || '';
|
|
957
|
+
refDoc = cleanArgs[6] || '';
|
|
958
|
+
threadId = contThreadId || cleanArgs[7] || '';
|
|
959
|
+
projectRoot = cleanArgs[8] || process.cwd();
|
|
960
|
+
|
|
961
|
+
if (tokensVal) {
|
|
962
|
+
var tokensParts = tokensVal.split(',');
|
|
963
|
+
tokensIn = parseInt(tokensParts[0], 10) || 0;
|
|
964
|
+
tokensOut = parseInt(tokensParts[1], 10) || 0;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
718
967
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
968
|
+
if (!title) {
|
|
969
|
+
if (isNamedMode) {
|
|
970
|
+
console.log('用法: sandtable event-log --type <slug> --title <text> [options]');
|
|
971
|
+
console.log('');
|
|
972
|
+
console.log('必选:');
|
|
973
|
+
console.log(' --type 事件类型: alignment | spec | code-change | test | approval | ops | lesson');
|
|
974
|
+
console.log(' --title 一句话标题');
|
|
975
|
+
console.log('');
|
|
976
|
+
console.log('可选:');
|
|
977
|
+
console.log(' --subtype 子类型 (如 code-done, review-pass, alignment)');
|
|
978
|
+
console.log(' --impact 影响级别: high | medium | low (默认 medium)');
|
|
979
|
+
console.log(' --actor 执行角色: @AI | @user (默认 @AI)');
|
|
980
|
+
console.log(' --tags 逗号分隔标签');
|
|
981
|
+
console.log(' --ref 关联文档路径');
|
|
982
|
+
console.log(' --thread 线程ID (关联同一线程事件)');
|
|
983
|
+
console.log(' --tokens-in 输入 token 数');
|
|
984
|
+
console.log(' --tokens-out 输出 token 数');
|
|
985
|
+
console.log(' --project 项目根目录 (默认当前目录)');
|
|
986
|
+
console.log('');
|
|
987
|
+
console.log('示例:');
|
|
988
|
+
console.log(' sandtable event-log --type code-change --title "完成登录接口" --subtype code-done --impact medium --actor @AI --tags "登录,API" --thread login-dev');
|
|
989
|
+
console.log(' sandtable event-log --type alignment --title "确定JWT方案" --impact high --actor @user');
|
|
990
|
+
} else {
|
|
991
|
+
console.log('用法: sandtable event-log <typeId> <title> [subtype] [impact] [actor] [tags] [refDoc] [threadId]');
|
|
992
|
+
console.log(' typeId: 1=对齐与拍板 2=规格演进 3=代码变更 4=测试与质量 5=审批与交接 6=运维与基建 7=教训沉淀');
|
|
993
|
+
console.log(' (位置参数已弃用,推荐使用 --type --title 命名参数)');
|
|
994
|
+
}
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
733
997
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
998
|
+
if (!EVENT_TYPES[typeId]) {
|
|
999
|
+
console.log('错误:--type 无效。有效值: alignment | spec | code-change | test | approval | ops | lesson');
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
737
1002
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1003
|
+
var sandtableDir = path.join(projectRoot, '.sandtable');
|
|
1004
|
+
if (!fs.existsSync(sandtableDir)) fs.mkdirSync(sandtableDir, { recursive: true });
|
|
1005
|
+
|
|
1006
|
+
var tags = tagsRaw ? tagsRaw.split(',').map(function(t) { return t.trim(); }) : [];
|
|
1007
|
+
var ref = refDoc ? { doc: refDoc } : {};
|
|
1008
|
+
|
|
1009
|
+
var eventId = 'evt-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
|
|
1010
|
+
|
|
1011
|
+
var event = {
|
|
1012
|
+
id: eventId,
|
|
1013
|
+
timestamp: new Date().toISOString(),
|
|
1014
|
+
type: EVENT_TYPES[typeId] || '代码变更',
|
|
1015
|
+
typeId: typeId,
|
|
1016
|
+
subtype: subtype,
|
|
1017
|
+
title: title,
|
|
1018
|
+
ref: ref,
|
|
1019
|
+
impact: impact,
|
|
1020
|
+
actor: actor,
|
|
1021
|
+
tags: tags,
|
|
1022
|
+
threadId: threadId || null,
|
|
1023
|
+
};
|
|
1024
|
+
event.priority = classifyEventPriority(event);
|
|
1025
|
+
|
|
1026
|
+
// Write event-log.jsonl
|
|
1027
|
+
var logPath = path.join(sandtableDir, 'event-log.jsonl');
|
|
1028
|
+
fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
|
|
1029
|
+
|
|
1030
|
+
// Write audit log
|
|
1031
|
+
var auditPath = path.join(sandtableDir, 'audit.log');
|
|
1032
|
+
var auditEntry = JSON.stringify({
|
|
1033
|
+
timestamp: event.timestamp,
|
|
1034
|
+
command: 'event-log',
|
|
1035
|
+
eventId: eventId,
|
|
1036
|
+
type: event.type,
|
|
1037
|
+
title: title,
|
|
1038
|
+
cwd: projectRoot,
|
|
1039
|
+
});
|
|
1040
|
+
fs.appendFileSync(auditPath, auditEntry + '\n');
|
|
749
1041
|
|
|
750
|
-
|
|
1042
|
+
var slugName = TYPE_SLUG_NAMES[typeId] || '';
|
|
1043
|
+
console.log('event-log: [' + event.priority + '] ' + event.type + ' — ' + title + (threadId ? ' (thread: ' + threadId + ')' : ''));
|
|
1044
|
+
if (!isNamedMode) {
|
|
1045
|
+
console.log(' (提示: 位置参数已弃用,下次推荐: --type ' + slugName + ' --title "...")');
|
|
1046
|
+
}
|
|
751
1047
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
var tOut = parseInt(tokensParts[1], 10) || 0;
|
|
757
|
-
if (tIn > 0 || tOut > 0) {
|
|
758
|
-
var COST_PER_1K = { input: 0.003, output: 0.006 }; // DeepSeek V4 Pro ¥/1K
|
|
759
|
-
var tCost = (tIn / 1000) * COST_PER_1K.input + (tOut / 1000) * COST_PER_1K.output;
|
|
1048
|
+
// --tokens-in + --tokens-out: auto-log token usage
|
|
1049
|
+
if (tokensIn > 0 || tokensOut > 0) {
|
|
1050
|
+
var COST_PER_1K = { input: 0.003, output: 0.006 };
|
|
1051
|
+
var tCost = (tokensIn / 1000) * COST_PER_1K.input + (tokensOut / 1000) * COST_PER_1K.output;
|
|
760
1052
|
tCost = Math.round(tCost * 10000) / 10000;
|
|
761
1053
|
var tokenEntry = {
|
|
762
1054
|
timestamp: event.timestamp,
|
|
763
1055
|
skill: 'event-log',
|
|
764
|
-
tokensIn:
|
|
765
|
-
tokensOut:
|
|
766
|
-
totalTokens:
|
|
1056
|
+
tokensIn: tokensIn,
|
|
1057
|
+
tokensOut: tokensOut,
|
|
1058
|
+
totalTokens: tokensIn + tokensOut,
|
|
767
1059
|
cost: tCost,
|
|
768
1060
|
source: 'event-log',
|
|
769
1061
|
eventId: eventId,
|
|
@@ -771,12 +1063,11 @@ switch (command) {
|
|
|
771
1063
|
};
|
|
772
1064
|
var tokenLogPath = path.join(sandtableDir, 'token-log.jsonl');
|
|
773
1065
|
fs.appendFileSync(tokenLogPath, JSON.stringify(tokenEntry) + '\n');
|
|
774
|
-
console.log(' token-log: ' + (
|
|
1066
|
+
console.log(' token-log: ' + (tokensIn + tokensOut).toLocaleString() + ' tokens (est. ¥' + tCost + ')');
|
|
775
1067
|
}
|
|
1068
|
+
break;
|
|
776
1069
|
}
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
case 'token-log': {
|
|
1070
|
+
case 'token-log': {
|
|
780
1071
|
var skill = process.argv[3] || '';
|
|
781
1072
|
var tokensIn = parseInt(process.argv[4], 10) || 0;
|
|
782
1073
|
var tokensOut = parseInt(process.argv[5], 10) || 0;
|
|
@@ -889,17 +1180,64 @@ switch (command) {
|
|
|
889
1180
|
console.log(' ' + (existing[i].isDir ? '[DIR] ' : '[ ] ') + existing[i].rel);
|
|
890
1181
|
}
|
|
891
1182
|
|
|
1183
|
+
// Check for sandtable injection markers in main docs
|
|
1184
|
+
var mainDocs = ['AGENTS.md', 'CLAUDE.md'];
|
|
1185
|
+
var hasInjections = false;
|
|
1186
|
+
for (var i = 0; i < mainDocs.length; i++) {
|
|
1187
|
+
var docPath = path.join(projectRoot, mainDocs[i]);
|
|
1188
|
+
if (fs.existsSync(docPath)) {
|
|
1189
|
+
var docContent = fs.readFileSync(docPath, 'utf-8');
|
|
1190
|
+
if (docContent.indexOf('<!-- sandtable:begin -->') !== -1) {
|
|
1191
|
+
hasInjections = true;
|
|
1192
|
+
if (dryRun) {
|
|
1193
|
+
console.log('');
|
|
1194
|
+
removeFromMainDoc(docPath, true); // dry-run: show diff
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (!hasInjections) {
|
|
1200
|
+
console.log('');
|
|
1201
|
+
console.log('主文档中未找到 sandtable 注入标记。');
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Check for sandtable gitignore block
|
|
1205
|
+
var gitignorePath = path.join(projectRoot, '.gitignore');
|
|
1206
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1207
|
+
var giContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1208
|
+
if (giContent.indexOf('# sandtable:begin') !== -1) {
|
|
1209
|
+
if (dryRun) {
|
|
1210
|
+
console.log('');
|
|
1211
|
+
removeFromGitignore(projectRoot, true);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
892
1216
|
if (dryRun) {
|
|
893
1217
|
console.log('');
|
|
894
1218
|
console.log('以上为 dry-run 预览。要实际删除,请运行:');
|
|
895
1219
|
console.log(' sandtable uninstall --apply');
|
|
896
1220
|
console.log('');
|
|
897
1221
|
console.log('注意:');
|
|
898
|
-
console.log(' - sandtable
|
|
899
|
-
console.log(' -
|
|
1222
|
+
console.log(' - sandtable 注入标记 (<!-- sandtable:begin/end -->) 可被 uninstall --apply 精确擦除');
|
|
1223
|
+
console.log(' - .gitignore 中 sandtable 条目 (# sandtable:begin/end) 也会被移除');
|
|
1224
|
+
console.log(' - 如果你在 AGENTS.md/CLAUDE.md 中手动添加了其他 sandtable 内容,请自行删除');
|
|
900
1225
|
break;
|
|
901
1226
|
}
|
|
902
1227
|
|
|
1228
|
+
// --apply: first clean up injected content from main docs
|
|
1229
|
+
for (var i = 0; i < mainDocs.length; i++) {
|
|
1230
|
+
var docPath = path.join(projectRoot, mainDocs[i]);
|
|
1231
|
+
if (fs.existsSync(docPath)) {
|
|
1232
|
+
var removed = removeFromMainDoc(docPath, false);
|
|
1233
|
+
if (removed) console.log(' ✓ 从 ' + mainDocs[i] + ' 移除 sandtable 注入块');
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// --apply: remove sandtable gitignore block
|
|
1238
|
+
var giRemoved = removeFromGitignore(projectRoot, false);
|
|
1239
|
+
if (giRemoved) console.log(' ✓ 从 .gitignore 移除 sandtable 条目');
|
|
1240
|
+
|
|
903
1241
|
// --apply: delete files
|
|
904
1242
|
console.log('');
|
|
905
1243
|
console.log('正在删除...');
|
|
@@ -952,7 +1290,7 @@ switch (command) {
|
|
|
952
1290
|
console.log(' sandtable init [--apply] [--lang zh|en] 扫描环境 + 准备规则文件');
|
|
953
1291
|
console.log(' sandtable scan [projectRoot] 扫描文档目录');
|
|
954
1292
|
console.log(' sandtable build [projectRoot] 生成 data/*.json');
|
|
955
|
-
console.log(' sandtable serve [port] [--watch]
|
|
1293
|
+
console.log(' sandtable serve [port] [--watch] [--host <ip>] [--token <token>] 启动服务 + 打开浏览器');
|
|
956
1294
|
console.log(' sandtable summarize [projectRoot] 列出缺少摘要块的文件');
|
|
957
1295
|
console.log(' sandtable event-log <typeId> <title> [...] 记录人机协同事件');
|
|
958
1296
|
console.log(' sandtable token-log <skill> <in> <out> [...] 追加 token 消耗日志');
|
package/src/scanner/scan.js
CHANGED
|
@@ -45,10 +45,10 @@ const PRIMARY_TYPES = new Set([
|
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
47
|
// ---- MECE display categories (PM 6 大分类 + template) ----
|
|
48
|
-
// §10 libero PM
|
|
48
|
+
// §10 libero PM 视角 — 9 大分类
|
|
49
49
|
// Primary-only 类别 (roadmap, decision) 不出现在 filterType checkbox 中
|
|
50
50
|
const DISPLAY_CATEGORIES = {
|
|
51
|
-
roadmap: { label: '路线图与进度', elementTypes: ['phase', 'milestone', 'task', 'subtask', 'roadmap', 'backlog'] },
|
|
51
|
+
roadmap: { label: '路线图与进度', elementTypes: ['phase', 'milestone', 'task', 'subtask', 'roadmap', 'backlog', 'optimization', 'todo'] },
|
|
52
52
|
decision: { label: '决策记录', elementTypes: ['decision', 'refactor'] },
|
|
53
53
|
spec: { label: '业务规格', elementTypes: ['spec', 'intent', 'prompt'] },
|
|
54
54
|
convention: { label: '协作纪律', elementTypes: ['convention'] },
|
|
@@ -58,7 +58,7 @@ const DISPLAY_CATEGORIES = {
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
// Primary-only categories (all elementTypes in these categories are kind=primary, show always)
|
|
61
|
-
const PRIMARY_CATEGORIES = new Set(['roadmap', 'decision']);
|
|
61
|
+
const PRIMARY_CATEGORIES = new Set(['roadmap', 'decision', 'todo']);
|
|
62
62
|
|
|
63
63
|
function normalizeCategory(elementType) {
|
|
64
64
|
for (const [cat, def] of Object.entries(DISPLAY_CATEGORIES)) {
|