ygl-trees 0.70.0 → 0.72.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/CHANGELOG.md +13 -0
- package/README.md +7 -2
- package/dist/apps/cli/src/bin.js +89 -1
- package/dist/bundle/bin.cjs +498 -1
- package/dist/packages/knowledge/skills/src/skill-cmds.js +64 -0
- package/dist/packages/ops/tools/src/agent-cmds.js +51 -0
- package/dist/packages/ops/tools/src/db-cmds.js +205 -0
- package/dist/packages/ops/tools/src/webui.js +53 -0
- package/llms.txt +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.72.0(2026-10-17)v2 功能包——8 项新能力
|
|
4
|
+
- **db time-travel**:查询过去某时刻的工作区状态(实体计数+已解锁节点)
|
|
5
|
+
- **db timeline**:工作区增长可视化(events/天 + 实体里程碑)
|
|
6
|
+
- **db workspace list/create**:多工作区管理
|
|
7
|
+
- **db obsidian**:导出为 Obsidian markdown vault(frontmatter 兼容)
|
|
8
|
+
- **agent ask**:自然语言接口(关键词路由到 CLI 命令)
|
|
9
|
+
- **skill init**:模板创建新技能(引导式)
|
|
10
|
+
- **skill refs**:展示技能关联引用(L3 渐进披露)
|
|
11
|
+
- **llms --section/--json**:节选输出 + 结构化 JSON
|
|
12
|
+
- **help 引导**:未知命令模糊匹配建议(前缀+子序列+别名)
|
|
13
|
+
|
|
3
14
|
## 0.70.0(2026-10-17)`agent metrics`——从 R11 手动测量到自动化指标
|
|
4
15
|
- **`ygl-trees agent metrics`**:一条命令输出工作台全量量化指标——冷启动(brief 耗时/next actions/可用技能)、知识(总量/7日增长/增长率)、研究(来源/证据/断言/验证率/证据密度)、技能(总数/命中/均命中)、科技树(棵数/节点/解锁率)、活动(7 日 events)
|
|
5
16
|
- **来源**:R11 冷启动量化研究的手动测量 → 自动化——从 `time agent brief` + 手动 grep 统计变为一键输出
|
|
@@ -19,6 +30,8 @@
|
|
|
19
30
|
- **F3:taskSetStatus 更新 artifacts.updated**——任务状态变更现在同步写 updated 时间戳(v9 纪律补漏)
|
|
20
31
|
- **exit 清理加固**:`beforeExit` + `exit` 双钩子 + 3 次重试(Windows EBUSY 容忍)
|
|
21
32
|
- **vendor/cordis bin 字段清理**:bin.js 已删但声明残留——移除
|
|
33
|
+
- **发布面补遗(0.68.0 后置修)**:Gitea 私仓补发 `@nelsonlongxiang/ygl-trees@0.68.0`——d950bdf 声称 "+ Gitea" 实未落上(私仓曾停留 0.66.0,npmjs 侧本就 latest);tarball 直验通过(`.ygllock` 标记物在产物内、无 .npmrc/token 泄露)
|
|
34
|
+
- **pack-check.sh 语法修复 + 名字无关化**(3542ab3):b62e866 将 `export YGL_TEST=1` 插入 `REPO=$(...)` 命令替换中部,注释吞掉 `&& pwd)` → 子壳永不闭合、脚本必死——0.68.0 的 npmjs 发布实际绕过了发布门禁;修复后 tarball glob/BIN 发现兼容 scoped(Gitea)与 unscoped(npmjs)双名
|
|
22
35
|
|
|
23
36
|
## 0.67.0(2026-10-17)WebUI antd 迭代——Ant Design 组件库升级 React 客户端
|
|
24
37
|
- **antd v6 + @ant-design/icons**:专业组件库驱动的富客户端
|
package/README.md
CHANGED
|
@@ -235,7 +235,11 @@ CJK 检索是个坑:FTS5 默认 `unicode61` 会把整句中文当成一个 tok
|
|
|
235
235
|
|
|
236
236
|
- **写入幂等**:`claim / evidence / query / kb / job` 的写命令按内容 hash 判重,重复执行返回已存在行(`! duplicate, kept existing`),Agent 重试不产生脏数据;`--force` 显式保留重复(job 用标题序号后缀)
|
|
237
237
|
- **完整备份**:`db export <file>` 覆盖全部数据表(含七张科技树表)并输出逐表行数摘要;dump 自带 `PRAGMA foreign_keys` 开关,可直接重放恢复。`--lean` 跳过闭包/拓扑等派生表数据
|
|
238
|
-
- **PostgreSQL 镜像/迁移**:`db
|
|
238
|
+
- **PostgreSQL 镜像/迁移**:`db time-travel "<ts>" workspace state at past moment
|
|
239
|
+
db timeline growth visualization (events/day)
|
|
240
|
+
db workspace list|create multi-workspace management
|
|
241
|
+
db obsidian [dir] export as Obsidian markdown vault
|
|
242
|
+
db sync pg <url>` 把整库单向全量镜像到 PG(可刷新,供 SQL 工具/团队分析);`db migrate pg <url> [--fresh]` 一次性完整保真迁移(FK/UNIQUE/索引约束齐全,目标库可独立持有)。两者都不做双后端同写,中文检索在 PG 侧用 tsvector+GIN 等价召回(详见 docs/pg-compat.md)。url 可走环境变量 `YGL_PG_URL`;`pg` 为可选依赖,不用 PG 则零影响
|
|
239
243
|
- **TypeORM 查询层**(0.8.0 起,研究域试点 `project show`):实体是 schema v8 的类型化快照(`synchronize` 永久 false,迁移权威仍在 user_version 轨道;FTS5/PRAGMA 路径保持裸 SQL)。依赖 typeorm + better-sqlite3 + reflect-metadata
|
|
240
244
|
- **重复可见**:`db dups` 列出同内容多行(含迁移前存量重复)
|
|
241
245
|
- **自动迁移**:v2 老库首次打开自动升级 schema v3(补 content_hash 列 + 回填 + 唯一索引),无人工介入
|
|
@@ -259,7 +263,8 @@ ygl-trees job logs
|
|
|
259
263
|
```bash
|
|
260
264
|
ygl-trees agent brief # 我在哪、有什么、下一步做什么
|
|
261
265
|
ygl-trees agent match "…" # 这个任务该用哪个 skill
|
|
262
|
-
ygl-trees agent
|
|
266
|
+
ygl-trees agent ask "<question>" natural language interface
|
|
267
|
+
agent log # 审计事件流
|
|
263
268
|
```
|
|
264
269
|
|
|
265
270
|
`agent brief --json` 直接给出结构化状态和建议动作序列,Agent 可以照着执行。
|
package/dist/apps/cli/src/bin.js
CHANGED
|
@@ -273,6 +273,35 @@ async function main(argv) {
|
|
|
273
273
|
// llms: structured capability manifest for LLM consumption (Ant Design
|
|
274
274
|
// "for-agents" pattern — one output gives any Agent the full command surface).
|
|
275
275
|
if (group === "llms") {
|
|
276
|
+
// v2: llms enhancement — --json, --section <name>, dynamic skills
|
|
277
|
+
const section = String(args.flags.section || "");
|
|
278
|
+
if (args.flags.json === true) {
|
|
279
|
+
const manifest = generateLlmsManifest();
|
|
280
|
+
const sections = {};
|
|
281
|
+
let current = "header";
|
|
282
|
+
for (const line of manifest.split("\n")) {
|
|
283
|
+
if (line.startsWith("## ")) {
|
|
284
|
+
current = line.slice(3).toLowerCase().replace(/\s+/g, "_");
|
|
285
|
+
sections[current] = line + "\n";
|
|
286
|
+
}
|
|
287
|
+
else if (sections[current] !== undefined)
|
|
288
|
+
sections[current] += line + "\n";
|
|
289
|
+
}
|
|
290
|
+
console.log(JSON.stringify({ version: appVersion(), sections }, null, 2));
|
|
291
|
+
return 0;
|
|
292
|
+
}
|
|
293
|
+
if (section) {
|
|
294
|
+
const manifest = generateLlmsManifest();
|
|
295
|
+
const all = manifest.split("\n## ");
|
|
296
|
+
const match = all.find(s2 => s2.toLowerCase().startsWith(section.toLowerCase()));
|
|
297
|
+
if (match) {
|
|
298
|
+
(0, fmt_1.out)(ctx, "## " + match);
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
(0, fmt_1.bad)(ctx, `section not found: ${section}`);
|
|
302
|
+
(0, fmt_1.out)(ctx, ` available: ${all.map(s3 => s3.split("\n")[0]).join(" | ")}`);
|
|
303
|
+
return 1;
|
|
304
|
+
}
|
|
276
305
|
(0, fmt_1.out)(ctx, generateLlmsManifest());
|
|
277
306
|
return 0;
|
|
278
307
|
}
|
|
@@ -413,7 +442,66 @@ async function main(argv) {
|
|
|
413
442
|
}
|
|
414
443
|
const mod = domain_barrel_1.DOMAIN_MODULES[group];
|
|
415
444
|
if (!mod) {
|
|
416
|
-
|
|
445
|
+
// Help guidance: suggest closest match for unknown commands
|
|
446
|
+
const known = ["agent", "research", "produce", "kb", "skill", "tree", "node", "task", "rel", "plugin", "webui", "db", "doctor", "config", "job", "project", "mcp", "llms", "version", "help"];
|
|
447
|
+
const aliases = {
|
|
448
|
+
database: "db", data: "db", sqlite: "db",
|
|
449
|
+
knowledge: "kb", notes: "kb", notebook: "kb",
|
|
450
|
+
helth: "doctor", check: "doctor", health: "doctor",
|
|
451
|
+
server: "webui", dashboard: "webui", ui: "webui",
|
|
452
|
+
timeline: "db", "time-travel": "db", workspace: "db", obsidian: "db",
|
|
453
|
+
};
|
|
454
|
+
const input = String(group).toLowerCase();
|
|
455
|
+
const suggestions = [];
|
|
456
|
+
// 0. alias check (database→db, knowledge→kb, etc.)
|
|
457
|
+
if (aliases[input])
|
|
458
|
+
suggestions.push(aliases[input]);
|
|
459
|
+
// 0b. alias prefix (databse→database→db, knolge→knowledge→kb)
|
|
460
|
+
for (const [alias, cmd] of Object.entries(aliases)) {
|
|
461
|
+
if (alias.startsWith(input.slice(0, 4)) && !suggestions.includes(cmd)) {
|
|
462
|
+
suggestions.push(cmd);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// 1. exact prefix match (strongest)
|
|
467
|
+
for (const k of known)
|
|
468
|
+
if (k.startsWith(input.slice(0, 3)) && !suggestions.includes(k))
|
|
469
|
+
suggestions.push(k);
|
|
470
|
+
// 2. subsequence match (input chars appear in order)
|
|
471
|
+
if (suggestions.length < 2) {
|
|
472
|
+
for (const k of known) {
|
|
473
|
+
if (suggestions.includes(k))
|
|
474
|
+
continue;
|
|
475
|
+
let ki = 0;
|
|
476
|
+
for (const ch of input) {
|
|
477
|
+
ki = k.indexOf(ch, ki);
|
|
478
|
+
if (ki === -1)
|
|
479
|
+
break;
|
|
480
|
+
ki++;
|
|
481
|
+
}
|
|
482
|
+
if (ki !== -1)
|
|
483
|
+
suggestions.push(k);
|
|
484
|
+
if (suggestions.length >= 3)
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// 3. known command contains input
|
|
489
|
+
if (suggestions.length < 2) {
|
|
490
|
+
for (const k of known)
|
|
491
|
+
if (k.includes(input) && !suggestions.includes(k))
|
|
492
|
+
suggestions.push(k);
|
|
493
|
+
}
|
|
494
|
+
if (suggestions.length === 1) {
|
|
495
|
+
(0, fmt_1.bad)(ctx, `unknown group: ${group} — did you mean "${suggestions[0]}"?`);
|
|
496
|
+
}
|
|
497
|
+
else if (suggestions.length > 1) {
|
|
498
|
+
(0, fmt_1.bad)(ctx, `unknown group: ${group} — did you mean: ${suggestions.slice(0, 3).join(" | ")}?`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
(0, fmt_1.bad)(ctx, `unknown group: ${group}`);
|
|
502
|
+
}
|
|
503
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " try: ygl-trees (no args) for full usage", ctx.color));
|
|
504
|
+
;
|
|
417
505
|
if (!ctx.json)
|
|
418
506
|
(0, fmt_1.out)(ctx, usage()); // usage text belongs to the human channel
|
|
419
507
|
return 2;
|
package/dist/bundle/bin.cjs
CHANGED
|
@@ -4668,6 +4668,210 @@ function run$12(env, action, rest, args) {
|
|
|
4668
4668
|
});
|
|
4669
4669
|
return 0;
|
|
4670
4670
|
}
|
|
4671
|
+
case "time-travel": {
|
|
4672
|
+
const when = rest[0];
|
|
4673
|
+
if (!when) {
|
|
4674
|
+
bad(ctx, "usage: db time-travel \"YYYY-MM-DD HH:MM\" (or \"YYYY-MM-DD\")");
|
|
4675
|
+
return 2;
|
|
4676
|
+
}
|
|
4677
|
+
const ts = when.includes(" ") ? when : when + " 23:59:59";
|
|
4678
|
+
const tables = [
|
|
4679
|
+
"projects",
|
|
4680
|
+
"sources",
|
|
4681
|
+
"evidence",
|
|
4682
|
+
"claims",
|
|
4683
|
+
"kb_notes",
|
|
4684
|
+
"skills",
|
|
4685
|
+
"trees",
|
|
4686
|
+
"nodes",
|
|
4687
|
+
"artifacts",
|
|
4688
|
+
"events"
|
|
4689
|
+
];
|
|
4690
|
+
const counts = {};
|
|
4691
|
+
for (const t of tables) try {
|
|
4692
|
+
counts[t] = db.get(`SELECT COUNT(*) as n FROM ${t} WHERE created <= ?`, [ts])?.n || 0;
|
|
4693
|
+
} catch {
|
|
4694
|
+
counts[t] = 0;
|
|
4695
|
+
}
|
|
4696
|
+
let unlockedAt = 0;
|
|
4697
|
+
try {
|
|
4698
|
+
unlockedAt = db.get(`SELECT COUNT(DISTINCT entity_id) as n FROM events WHERE kind='node.unlock' AND ts <= ?`, [ts])?.n || 0;
|
|
4699
|
+
} catch {}
|
|
4700
|
+
if (ctx.json) {
|
|
4701
|
+
console.log(JSON.stringify({
|
|
4702
|
+
timestamp: ts,
|
|
4703
|
+
counts,
|
|
4704
|
+
unlocked_nodes: unlockedAt
|
|
4705
|
+
}, null, 2));
|
|
4706
|
+
return 0;
|
|
4707
|
+
}
|
|
4708
|
+
head(ctx, `Workspace at ${ts}`);
|
|
4709
|
+
const labels = {
|
|
4710
|
+
projects: "项目",
|
|
4711
|
+
sources: "来源",
|
|
4712
|
+
evidence: "证据",
|
|
4713
|
+
claims: "断言",
|
|
4714
|
+
kb_notes: "知识",
|
|
4715
|
+
skills: "技能",
|
|
4716
|
+
trees: "树",
|
|
4717
|
+
nodes: "节点",
|
|
4718
|
+
artifacts: "产出",
|
|
4719
|
+
events: "事件"
|
|
4720
|
+
};
|
|
4721
|
+
for (const [k, v] of Object.entries(counts)) out(ctx, ` ${String(labels[k] || k).padEnd(6)} ${String(v).padStart(5)}`);
|
|
4722
|
+
out(ctx, ` 已解锁 ${String(unlockedAt).padStart(5)}`);
|
|
4723
|
+
return 0;
|
|
4724
|
+
}
|
|
4725
|
+
case "timeline": {
|
|
4726
|
+
const days = db.all(`
|
|
4727
|
+
SELECT date(ts) as day, COUNT(*) as events,
|
|
4728
|
+
SUM(CASE WHEN kind LIKE 'project%' THEN 1 ELSE 0 END) as projects,
|
|
4729
|
+
SUM(CASE WHEN kind LIKE 'source%' THEN 1 ELSE 0 END) as sources,
|
|
4730
|
+
SUM(CASE WHEN kind LIKE 'claim%' THEN 1 ELSE 0 END) as claims,
|
|
4731
|
+
SUM(CASE WHEN kind LIKE 'kb%' THEN 1 ELSE 0 END) as kb,
|
|
4732
|
+
SUM(CASE WHEN kind LIKE 'node%' THEN 1 ELSE 0 END) as nodes
|
|
4733
|
+
FROM events GROUP BY date(ts) ORDER BY day
|
|
4734
|
+
`);
|
|
4735
|
+
if (!days.length) {
|
|
4736
|
+
ok(ctx, "no events yet");
|
|
4737
|
+
return 0;
|
|
4738
|
+
}
|
|
4739
|
+
const maxEvents = Math.max(...days.map((d) => d.events));
|
|
4740
|
+
head(ctx, "Workspace Timeline");
|
|
4741
|
+
let totalP = 0, totalS = 0, totalC = 0, totalK = 0, totalN = 0;
|
|
4742
|
+
for (const d of days) {
|
|
4743
|
+
totalP += d.projects;
|
|
4744
|
+
totalS += d.sources;
|
|
4745
|
+
totalC += d.claims;
|
|
4746
|
+
totalK += d.kb;
|
|
4747
|
+
totalN += d.nodes;
|
|
4748
|
+
const barLen = Math.round(d.events / maxEvents * 30);
|
|
4749
|
+
const bar = "█".repeat(barLen);
|
|
4750
|
+
const milestones = [];
|
|
4751
|
+
if (d.projects > 0) milestones.push(`+${d.projects}项目`);
|
|
4752
|
+
if (d.sources > 0) milestones.push(`+${d.sources}来源`);
|
|
4753
|
+
if (d.claims > 0) milestones.push(`+${d.claims}断言`);
|
|
4754
|
+
if (d.kb > 0) milestones.push(`+${d.kb}知识`);
|
|
4755
|
+
if (d.nodes > 0) milestones.push(`+${d.nodes}节点`);
|
|
4756
|
+
out(ctx, ` ${d.day} ${bar.padEnd(32)} ${String(d.events).padStart(5)} events ${milestones.join(" ")}`);
|
|
4757
|
+
}
|
|
4758
|
+
out(ctx, "");
|
|
4759
|
+
out(ctx, paint(C.dim, ` 累计: ${totalP}项目 · ${totalS}来源 · ${totalC}断言 · ${totalK}知识 · ${totalN}节点变更`, ctx.color));
|
|
4760
|
+
return 0;
|
|
4761
|
+
}
|
|
4762
|
+
case "workspace": {
|
|
4763
|
+
const sub = rest[0] || "list";
|
|
4764
|
+
const wsRoot = node_path.join(env.home, "..");
|
|
4765
|
+
if (sub === "list") {
|
|
4766
|
+
const dirs = node_fs.existsSync(wsRoot) ? node_fs.readdirSync(wsRoot).filter((d) => node_fs.existsSync(node_path.join(wsRoot, d, "ygl-trees.db"))) : [];
|
|
4767
|
+
const cur = node_path.basename(env.home);
|
|
4768
|
+
head(ctx, "Workspaces (parent: " + wsRoot + ")");
|
|
4769
|
+
for (const d of dirs) out(ctx, (d === cur ? " * " : " ") + d);
|
|
4770
|
+
out(ctx, "");
|
|
4771
|
+
out(ctx, paint(C.dim, " * = current | switch: YGL_HOME=<path> ygl-trees <cmd>", ctx.color));
|
|
4772
|
+
return 0;
|
|
4773
|
+
}
|
|
4774
|
+
if (sub === "create") {
|
|
4775
|
+
const name = rest[1];
|
|
4776
|
+
if (!name) {
|
|
4777
|
+
bad(ctx, "usage: db workspace create <name>");
|
|
4778
|
+
return 2;
|
|
4779
|
+
}
|
|
4780
|
+
const dir = node_path.join(wsRoot, name);
|
|
4781
|
+
if (node_fs.existsSync(node_path.join(dir, "ygl-trees.db"))) {
|
|
4782
|
+
ok(ctx, `workspace already exists: ${name}`);
|
|
4783
|
+
return 0;
|
|
4784
|
+
}
|
|
4785
|
+
node_fs.mkdirSync(dir, { recursive: true });
|
|
4786
|
+
ok(ctx, `workspace created: ${dir}`);
|
|
4787
|
+
out(ctx, paint(C.dim, ` init: YGL_HOME=${dir} ygl-trees db init`, ctx.color));
|
|
4788
|
+
return 0;
|
|
4789
|
+
}
|
|
4790
|
+
bad(ctx, "usage: db workspace list | create <name>");
|
|
4791
|
+
return 2;
|
|
4792
|
+
}
|
|
4793
|
+
case "obsidian": {
|
|
4794
|
+
const outDir = rest[0] || node_path.join(env.home, "obsidian-export");
|
|
4795
|
+
node_fs.mkdirSync(node_path.join(outDir, "projects"), { recursive: true });
|
|
4796
|
+
node_fs.mkdirSync(node_path.join(outDir, "knowledge"), { recursive: true });
|
|
4797
|
+
node_fs.mkdirSync(node_path.join(outDir, "skills"), { recursive: true });
|
|
4798
|
+
node_fs.mkdirSync(node_path.join(outDir, "trees"), { recursive: true });
|
|
4799
|
+
node_fs.mkdirSync(node_path.join(outDir, "claims"), { recursive: true });
|
|
4800
|
+
let count = 0;
|
|
4801
|
+
const safe = (t) => String(t).replace(/[\/:*?"<>|]/g, "_").slice(0, 50);
|
|
4802
|
+
for (const p of db.all("SELECT * FROM projects")) {
|
|
4803
|
+
node_fs.writeFileSync(node_path.join(outDir, "projects", safe(p.name) + ".md"), `---
|
|
4804
|
+
type: project
|
|
4805
|
+
id: ${p.id}
|
|
4806
|
+
---
|
|
4807
|
+
|
|
4808
|
+
# ${p.name}
|
|
4809
|
+
|
|
4810
|
+
${p.question || ""}
|
|
4811
|
+
`);
|
|
4812
|
+
count++;
|
|
4813
|
+
}
|
|
4814
|
+
for (const n of db.all("SELECT * FROM kb_notes")) {
|
|
4815
|
+
node_fs.writeFileSync(node_path.join(outDir, "knowledge", safe(n.title) + ".md"), `---
|
|
4816
|
+
type: knowledge
|
|
4817
|
+
tags: ${n.tags || ""}
|
|
4818
|
+
confidence: ${n.confidence}
|
|
4819
|
+
---
|
|
4820
|
+
|
|
4821
|
+
# ${n.title}
|
|
4822
|
+
|
|
4823
|
+
${n.body}
|
|
4824
|
+
`);
|
|
4825
|
+
count++;
|
|
4826
|
+
}
|
|
4827
|
+
for (const sk of db.all("SELECT * FROM skills WHERE enabled=1")) {
|
|
4828
|
+
node_fs.writeFileSync(node_path.join(outDir, "skills", safe(sk.name) + ".md"), `---
|
|
4829
|
+
type: skill
|
|
4830
|
+
version: ${sk.version}
|
|
4831
|
+
---
|
|
4832
|
+
|
|
4833
|
+
# ${sk.name}
|
|
4834
|
+
|
|
4835
|
+
${String(sk.body).slice(0, 500)}
|
|
4836
|
+
`);
|
|
4837
|
+
count++;
|
|
4838
|
+
}
|
|
4839
|
+
for (const t of db.all("SELECT * FROM trees")) {
|
|
4840
|
+
const nodes = db.all("SELECT * FROM nodes WHERE tree_id=?", [t.id]);
|
|
4841
|
+
let c = `---
|
|
4842
|
+
type: tree
|
|
4843
|
+
---
|
|
4844
|
+
|
|
4845
|
+
# ${t.name} (${nodes.length} nodes)
|
|
4846
|
+
|
|
4847
|
+
`;
|
|
4848
|
+
for (const n of nodes) c += `- **${n.key}** (${n.kind}, tier ${n.tier})
|
|
4849
|
+
`;
|
|
4850
|
+
node_fs.writeFileSync(node_path.join(outDir, "trees", safe(t.name) + ".md"), c);
|
|
4851
|
+
count++;
|
|
4852
|
+
}
|
|
4853
|
+
for (const cl of db.all("SELECT * FROM claims")) {
|
|
4854
|
+
const links = db.all("SELECT e.quote FROM claim_evidence ce JOIN evidence e ON ce.evidence_id=e.id WHERE ce.claim_id=?", [cl.id]);
|
|
4855
|
+
let c2 = `---
|
|
4856
|
+
type: claim
|
|
4857
|
+
status: ${cl.status}
|
|
4858
|
+
confidence: ${cl.confidence}
|
|
4859
|
+
---
|
|
4860
|
+
|
|
4861
|
+
# ${cl.id}
|
|
4862
|
+
|
|
4863
|
+
${cl.text}
|
|
4864
|
+
|
|
4865
|
+
## Evidence
|
|
4866
|
+
`;
|
|
4867
|
+
for (const l of links) c2 += `- > ${String(l.quote).slice(0, 100)}
|
|
4868
|
+
`;
|
|
4869
|
+
node_fs.writeFileSync(node_path.join(outDir, "claims", cl.id + ".md"), c2);
|
|
4870
|
+
count++;
|
|
4871
|
+
}
|
|
4872
|
+
ok(ctx, `obsidian vault exported: ${count} files → ${outDir}`);
|
|
4873
|
+
return 0;
|
|
4874
|
+
}
|
|
4671
4875
|
case "export": {
|
|
4672
4876
|
const dest = rest[0] || `ygl-trees-dump-${Date.now()}.sql`;
|
|
4673
4877
|
const skip = args.flags.lean === true || args.flags.lean === "true" ? /* @__PURE__ */ new Set(["node_closure", "node_index"]) : /* @__PURE__ */ new Set();
|
|
@@ -5351,6 +5555,89 @@ function buildBrief(env) {
|
|
|
5351
5555
|
function run$9(env, action, rest, args) {
|
|
5352
5556
|
const { ctx, store } = env;
|
|
5353
5557
|
switch (action) {
|
|
5558
|
+
case "ask":
|
|
5559
|
+
case "?": {
|
|
5560
|
+
const question = rest.join(" ");
|
|
5561
|
+
if (!question) {
|
|
5562
|
+
bad(ctx, "usage: ygl-trees agent ask \"<question>\"");
|
|
5563
|
+
out(ctx, " examples: ask \"what projects\" | ask \"search AI\" | ask \"健康\"");
|
|
5564
|
+
return 2;
|
|
5565
|
+
}
|
|
5566
|
+
const q = question.toLowerCase();
|
|
5567
|
+
const routes = [
|
|
5568
|
+
{
|
|
5569
|
+
pattern: /what.*project|项目列表|所有项目/,
|
|
5570
|
+
label: "project list",
|
|
5571
|
+
group: "project",
|
|
5572
|
+
action: "list"
|
|
5573
|
+
},
|
|
5574
|
+
{
|
|
5575
|
+
pattern: /search|搜索|检索|找/,
|
|
5576
|
+
label: "kb search",
|
|
5577
|
+
group: "kb",
|
|
5578
|
+
action: "search",
|
|
5579
|
+
extra: [question.replace(/.*?(search|搜索|检索|找)\s*/i, "").replace(/["']/g, "")]
|
|
5580
|
+
},
|
|
5581
|
+
{
|
|
5582
|
+
pattern: /tree|树/,
|
|
5583
|
+
label: "node list",
|
|
5584
|
+
group: "node",
|
|
5585
|
+
action: "list"
|
|
5586
|
+
},
|
|
5587
|
+
{
|
|
5588
|
+
pattern: /health|健康|doctor|检查/,
|
|
5589
|
+
label: "doctor",
|
|
5590
|
+
group: "doctor",
|
|
5591
|
+
action: ""
|
|
5592
|
+
},
|
|
5593
|
+
{
|
|
5594
|
+
pattern: /skill|技能/,
|
|
5595
|
+
label: "skill list",
|
|
5596
|
+
group: "skill",
|
|
5597
|
+
action: "list"
|
|
5598
|
+
},
|
|
5599
|
+
{
|
|
5600
|
+
pattern: /task|任务/,
|
|
5601
|
+
label: "task list",
|
|
5602
|
+
group: "task",
|
|
5603
|
+
action: "list"
|
|
5604
|
+
},
|
|
5605
|
+
{
|
|
5606
|
+
pattern: /metric|指标/,
|
|
5607
|
+
label: "agent metrics",
|
|
5608
|
+
group: "agent",
|
|
5609
|
+
action: "metrics"
|
|
5610
|
+
}
|
|
5611
|
+
];
|
|
5612
|
+
for (const r of routes) if (r.pattern.test(q)) {
|
|
5613
|
+
if (!ctx.json) out(ctx, paint(C.dim, ` → ${r.label}`, ctx.color));
|
|
5614
|
+
const cmdParts = [
|
|
5615
|
+
r.group,
|
|
5616
|
+
r.action,
|
|
5617
|
+
...r.extra || []
|
|
5618
|
+
].filter(Boolean);
|
|
5619
|
+
const { execSync } = require("node:child_process");
|
|
5620
|
+
const self = process.argv[1];
|
|
5621
|
+
try {
|
|
5622
|
+
out(ctx, execSync(`${JSON.stringify(process.execPath)} ${JSON.stringify(self)} --no-color ${cmdParts.map((c) => JSON.stringify(c)).join(" ")}`, {
|
|
5623
|
+
env: { ...process.env },
|
|
5624
|
+
encoding: "utf8",
|
|
5625
|
+
timeout: 1e4
|
|
5626
|
+
}).trim());
|
|
5627
|
+
return 0;
|
|
5628
|
+
} catch (e) {
|
|
5629
|
+
bad(ctx, String(e?.stdout || e?.message || e).slice(0, 200));
|
|
5630
|
+
return 1;
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
if (!ctx.json) out(ctx, paint(C.dim, " → agent brief (no specific match)", ctx.color));
|
|
5634
|
+
const payload = buildBrief(env);
|
|
5635
|
+
if (ctx.json) {
|
|
5636
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
5637
|
+
return 0;
|
|
5638
|
+
}
|
|
5639
|
+
return run$9(env, "brief", [], args);
|
|
5640
|
+
}
|
|
5354
5641
|
case "metrics": {
|
|
5355
5642
|
const t0 = Date.now();
|
|
5356
5643
|
const brief = buildBrief(env);
|
|
@@ -8216,6 +8503,70 @@ function run$4(env, action, rest, args) {
|
|
|
8216
8503
|
return 1;
|
|
8217
8504
|
});
|
|
8218
8505
|
}
|
|
8506
|
+
case "init": {
|
|
8507
|
+
const name = String(args.flags.name || rest[0] || "");
|
|
8508
|
+
if (!name) {
|
|
8509
|
+
bad(ctx, "usage: skill init <name> [--description ...] [--body ...]");
|
|
8510
|
+
bad(ctx, " creates a skill with frontmatter template; edit body with skill show + update");
|
|
8511
|
+
return 2;
|
|
8512
|
+
}
|
|
8513
|
+
const desc = String(args.flags.description || "");
|
|
8514
|
+
const body = String(args.flags.body || `# ${name}
|
|
8515
|
+
|
|
8516
|
+
## When to use
|
|
8517
|
+
(describe when this skill should activate)
|
|
8518
|
+
|
|
8519
|
+
## What to do
|
|
8520
|
+
(step-by-step instructions)
|
|
8521
|
+
|
|
8522
|
+
## Examples
|
|
8523
|
+
(concrete command examples)
|
|
8524
|
+
`);
|
|
8525
|
+
const tags = String(args.flags.tags || "");
|
|
8526
|
+
return getOrm(env.db.path).then((ds) => skillUpsert(ds, {
|
|
8527
|
+
name,
|
|
8528
|
+
description: desc,
|
|
8529
|
+
body,
|
|
8530
|
+
tags,
|
|
8531
|
+
version: "0.1.0"
|
|
8532
|
+
})).then((r) => {
|
|
8533
|
+
ok(ctx, `skill created: ${name} v0.1.0`);
|
|
8534
|
+
out(ctx, paint(C.dim, ` edit: ygl-trees skill show ${name}`, ctx.color));
|
|
8535
|
+
out(ctx, paint(C.dim, ` add refs: ygl-trees skill refs add ${name} <path> --body "..."`, ctx.color));
|
|
8536
|
+
return 0;
|
|
8537
|
+
}).catch((e) => {
|
|
8538
|
+
bad(ctx, String(e?.message || e));
|
|
8539
|
+
return 1;
|
|
8540
|
+
});
|
|
8541
|
+
}
|
|
8542
|
+
case "refs":
|
|
8543
|
+
case "references": {
|
|
8544
|
+
const skillName = rest[0];
|
|
8545
|
+
if (!skillName) {
|
|
8546
|
+
bad(ctx, "usage: skill refs <name>");
|
|
8547
|
+
return 2;
|
|
8548
|
+
}
|
|
8549
|
+
const sk = store.skillGet(skillName);
|
|
8550
|
+
if (!sk) {
|
|
8551
|
+
bad(ctx, `skill not found: ${skillName}`);
|
|
8552
|
+
return 1;
|
|
8553
|
+
}
|
|
8554
|
+
const refs = env.db.all("SELECT * FROM skill_refs WHERE skill_id=? ORDER BY path", [sk.id]);
|
|
8555
|
+
head(ctx, `Skill: ${skillName}`);
|
|
8556
|
+
if (ctx.json) {
|
|
8557
|
+
console.log(JSON.stringify(refs, null, 2));
|
|
8558
|
+
return 0;
|
|
8559
|
+
}
|
|
8560
|
+
if (!refs.length) {
|
|
8561
|
+
out(ctx, paint(C.dim, " (no references — add with: skill refs add <name> <path> --body ...)", ctx.color));
|
|
8562
|
+
return 0;
|
|
8563
|
+
}
|
|
8564
|
+
for (const r of refs) {
|
|
8565
|
+
out(ctx, ` ${r.path}`);
|
|
8566
|
+
if (r.body) out(ctx, paint(C.dim, ` ${String(r.body).slice(0, 80)}`, ctx.color));
|
|
8567
|
+
}
|
|
8568
|
+
return 0;
|
|
8569
|
+
}
|
|
8219
8570
|
case "add": {
|
|
8220
8571
|
const n = args.flags.name ? String(args.flags.name) : "";
|
|
8221
8572
|
const file = args.flags.f ? String(args.flags.f) : "";
|
|
@@ -9156,6 +9507,61 @@ function gatherData(env) {
|
|
|
9156
9507
|
}));
|
|
9157
9508
|
}
|
|
9158
9509
|
}
|
|
9510
|
+
const kgEdges = [];
|
|
9511
|
+
const kgMap = /* @__PURE__ */ new Map();
|
|
9512
|
+
try {
|
|
9513
|
+
const rels = db.all("SELECT src_type, src_id, dst_type, dst_id, rel FROM relations LIMIT 200");
|
|
9514
|
+
const colors = {
|
|
9515
|
+
node: "#7c9885",
|
|
9516
|
+
claim: "#6a994e",
|
|
9517
|
+
kb: "#8b7355",
|
|
9518
|
+
evidence: "#daa520",
|
|
9519
|
+
artifact: "#5b8db8",
|
|
9520
|
+
skill: "#9b7cb8"
|
|
9521
|
+
};
|
|
9522
|
+
for (const r of rels) {
|
|
9523
|
+
for (const side of ["src", "dst"]) {
|
|
9524
|
+
const type = r[`${side}_type`];
|
|
9525
|
+
const id = r[`${side}_id`];
|
|
9526
|
+
const key = `${type}:${id}`;
|
|
9527
|
+
if (kgMap.has(key)) continue;
|
|
9528
|
+
let label = id;
|
|
9529
|
+
try {
|
|
9530
|
+
if (type === "node") {
|
|
9531
|
+
const n = db.get("SELECT name FROM nodes WHERE id=?", [id]);
|
|
9532
|
+
if (n) label = n.name;
|
|
9533
|
+
} else if (type === "claim") {
|
|
9534
|
+
const c = db.get("SELECT substr(text,1,40) AS t FROM claims WHERE id=?", [id]);
|
|
9535
|
+
if (c) label = c.t;
|
|
9536
|
+
} else if (type === "kb") {
|
|
9537
|
+
const k = db.get("SELECT title FROM kb_notes WHERE id=?", [id]);
|
|
9538
|
+
if (k) label = k.title.slice(0, 40);
|
|
9539
|
+
} else if (type === "evidence") {
|
|
9540
|
+
const e = db.get("SELECT substr(quote,1,40) AS q FROM evidence WHERE id=?", [id]);
|
|
9541
|
+
if (e) label = e.q;
|
|
9542
|
+
} else if (type === "artifact") {
|
|
9543
|
+
const a = db.get("SELECT title FROM artifacts WHERE id=?", [id]);
|
|
9544
|
+
if (a) label = a.title;
|
|
9545
|
+
}
|
|
9546
|
+
} catch {}
|
|
9547
|
+
kgMap.set(key, {
|
|
9548
|
+
id: key,
|
|
9549
|
+
label,
|
|
9550
|
+
type,
|
|
9551
|
+
color: colors[type] || "#999"
|
|
9552
|
+
});
|
|
9553
|
+
}
|
|
9554
|
+
kgEdges.push({
|
|
9555
|
+
source: `${r.src_type}:${r.src_id}`,
|
|
9556
|
+
target: `${r.dst_type}:${r.dst_id}`,
|
|
9557
|
+
label: r.rel
|
|
9558
|
+
});
|
|
9559
|
+
}
|
|
9560
|
+
} catch {}
|
|
9561
|
+
data.knowledge_graph = {
|
|
9562
|
+
nodes: Array.from(kgMap.values()),
|
|
9563
|
+
edges: kgEdges
|
|
9564
|
+
};
|
|
9159
9565
|
return data;
|
|
9160
9566
|
}
|
|
9161
9567
|
/** Single-file dashboard HTML. `live` adds a fetch bootstrap + auto-refresh. */
|
|
@@ -10387,6 +10793,32 @@ async function main(argv) {
|
|
|
10387
10793
|
return 0;
|
|
10388
10794
|
}
|
|
10389
10795
|
if (group === "llms") {
|
|
10796
|
+
const section = String(args.flags.section || "");
|
|
10797
|
+
if (args.flags.json === true) {
|
|
10798
|
+
const manifest = generateLlmsManifest();
|
|
10799
|
+
const sections = {};
|
|
10800
|
+
let current = "header";
|
|
10801
|
+
for (const line of manifest.split("\n")) if (line.startsWith("## ")) {
|
|
10802
|
+
current = line.slice(3).toLowerCase().replace(/\s+/g, "_");
|
|
10803
|
+
sections[current] = line + "\n";
|
|
10804
|
+
} else if (sections[current] !== void 0) sections[current] += line + "\n";
|
|
10805
|
+
console.log(JSON.stringify({
|
|
10806
|
+
version: appVersion(),
|
|
10807
|
+
sections
|
|
10808
|
+
}, null, 2));
|
|
10809
|
+
return 0;
|
|
10810
|
+
}
|
|
10811
|
+
if (section) {
|
|
10812
|
+
const all = generateLlmsManifest().split("\n## ");
|
|
10813
|
+
const match = all.find((s2) => s2.toLowerCase().startsWith(section.toLowerCase()));
|
|
10814
|
+
if (match) {
|
|
10815
|
+
out(ctx, "## " + match);
|
|
10816
|
+
return 0;
|
|
10817
|
+
}
|
|
10818
|
+
bad(ctx, `section not found: ${section}`);
|
|
10819
|
+
out(ctx, ` available: ${all.map((s3) => s3.split("\n")[0]).join(" | ")}`);
|
|
10820
|
+
return 1;
|
|
10821
|
+
}
|
|
10390
10822
|
out(ctx, generateLlmsManifest());
|
|
10391
10823
|
return 0;
|
|
10392
10824
|
}
|
|
@@ -10524,7 +10956,72 @@ async function main(argv) {
|
|
|
10524
10956
|
}
|
|
10525
10957
|
const mod = DOMAIN_MODULES[group];
|
|
10526
10958
|
if (!mod) {
|
|
10527
|
-
|
|
10959
|
+
const known = [
|
|
10960
|
+
"agent",
|
|
10961
|
+
"research",
|
|
10962
|
+
"produce",
|
|
10963
|
+
"kb",
|
|
10964
|
+
"skill",
|
|
10965
|
+
"tree",
|
|
10966
|
+
"node",
|
|
10967
|
+
"task",
|
|
10968
|
+
"rel",
|
|
10969
|
+
"plugin",
|
|
10970
|
+
"webui",
|
|
10971
|
+
"db",
|
|
10972
|
+
"doctor",
|
|
10973
|
+
"config",
|
|
10974
|
+
"job",
|
|
10975
|
+
"project",
|
|
10976
|
+
"mcp",
|
|
10977
|
+
"llms",
|
|
10978
|
+
"version",
|
|
10979
|
+
"help"
|
|
10980
|
+
];
|
|
10981
|
+
const aliases = {
|
|
10982
|
+
database: "db",
|
|
10983
|
+
data: "db",
|
|
10984
|
+
sqlite: "db",
|
|
10985
|
+
knowledge: "kb",
|
|
10986
|
+
notes: "kb",
|
|
10987
|
+
notebook: "kb",
|
|
10988
|
+
helth: "doctor",
|
|
10989
|
+
check: "doctor",
|
|
10990
|
+
health: "doctor",
|
|
10991
|
+
server: "webui",
|
|
10992
|
+
dashboard: "webui",
|
|
10993
|
+
ui: "webui",
|
|
10994
|
+
timeline: "db",
|
|
10995
|
+
"time-travel": "db",
|
|
10996
|
+
workspace: "db",
|
|
10997
|
+
obsidian: "db"
|
|
10998
|
+
};
|
|
10999
|
+
const input = String(group).toLowerCase();
|
|
11000
|
+
const suggestions = [];
|
|
11001
|
+
if (aliases[input]) suggestions.push(aliases[input]);
|
|
11002
|
+
for (const [alias, cmd] of Object.entries(aliases)) if (alias.startsWith(input.slice(0, 4)) && !suggestions.includes(cmd)) {
|
|
11003
|
+
suggestions.push(cmd);
|
|
11004
|
+
break;
|
|
11005
|
+
}
|
|
11006
|
+
for (const k of known) if (k.startsWith(input.slice(0, 3)) && !suggestions.includes(k)) suggestions.push(k);
|
|
11007
|
+
if (suggestions.length < 2) for (const k of known) {
|
|
11008
|
+
if (suggestions.includes(k)) continue;
|
|
11009
|
+
let ki = 0;
|
|
11010
|
+
for (const ch of input) {
|
|
11011
|
+
ki = k.indexOf(ch, ki);
|
|
11012
|
+
if (ki === -1) break;
|
|
11013
|
+
ki++;
|
|
11014
|
+
}
|
|
11015
|
+
if (ki !== -1) suggestions.push(k);
|
|
11016
|
+
if (suggestions.length >= 3) break;
|
|
11017
|
+
}
|
|
11018
|
+
if (suggestions.length < 2) {
|
|
11019
|
+
for (const k of known) if (k.includes(input) && !suggestions.includes(k)) suggestions.push(k);
|
|
11020
|
+
}
|
|
11021
|
+
if (suggestions.length === 1) bad(ctx, `unknown group: ${group} — did you mean "${suggestions[0]}"?`);
|
|
11022
|
+
else if (suggestions.length > 1) bad(ctx, `unknown group: ${group} — did you mean: ${suggestions.slice(0, 3).join(" | ")}?`);
|
|
11023
|
+
else bad(ctx, `unknown group: ${group}`);
|
|
11024
|
+
out(ctx, paint(C.dim, " try: ygl-trees (no args) for full usage", ctx.color));
|
|
10528
11025
|
if (!ctx.json) out(ctx, usage());
|
|
10529
11026
|
return 2;
|
|
10530
11027
|
}
|
|
@@ -82,6 +82,70 @@ function run(env, action, rest, args) {
|
|
|
82
82
|
return 1;
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
|
+
case "init": {
|
|
86
|
+
// v2: create a new skill from template — guides the author
|
|
87
|
+
const name = String(args.flags.name || rest[0] || "");
|
|
88
|
+
if (!name) {
|
|
89
|
+
(0, fmt_1.bad)(ctx, "usage: skill init <name> [--description ...] [--body ...]");
|
|
90
|
+
(0, fmt_1.bad)(ctx, " creates a skill with frontmatter template; edit body with skill show + update");
|
|
91
|
+
return 2;
|
|
92
|
+
}
|
|
93
|
+
const desc = String(args.flags.description || "");
|
|
94
|
+
const body = String(args.flags.body || `# ${name}
|
|
95
|
+
|
|
96
|
+
## When to use
|
|
97
|
+
(describe when this skill should activate)
|
|
98
|
+
|
|
99
|
+
## What to do
|
|
100
|
+
(step-by-step instructions)
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
(concrete command examples)
|
|
104
|
+
`);
|
|
105
|
+
const tags = String(args.flags.tags || "");
|
|
106
|
+
return (0, orm_1.getOrm)(env.db.path)
|
|
107
|
+
.then((ds) => writes.skillUpsert(ds, { name, description: desc, body, tags, version: "0.1.0" }))
|
|
108
|
+
.then((r) => {
|
|
109
|
+
(0, fmt_1.ok)(ctx, `skill created: ${name} v0.1.0`);
|
|
110
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` edit: ygl-trees skill show ${name}`, ctx.color));
|
|
111
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` add refs: ygl-trees skill refs add ${name} <path> --body "..."`, ctx.color));
|
|
112
|
+
return 0;
|
|
113
|
+
})
|
|
114
|
+
.catch((e) => {
|
|
115
|
+
(0, fmt_1.bad)(ctx, String(e?.message || e));
|
|
116
|
+
return 1;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
case "refs":
|
|
120
|
+
case "references": {
|
|
121
|
+
// v2: show all references linked to a skill (L3 progressive disclosure)
|
|
122
|
+
const skillName = rest[0];
|
|
123
|
+
if (!skillName) {
|
|
124
|
+
(0, fmt_1.bad)(ctx, "usage: skill refs <name>");
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
const sk = store.skillGet(skillName);
|
|
128
|
+
if (!sk) {
|
|
129
|
+
(0, fmt_1.bad)(ctx, `skill not found: ${skillName}`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
const refs = env.db.all("SELECT * FROM skill_refs WHERE skill_id=? ORDER BY path", [sk.id]);
|
|
133
|
+
(0, fmt_1.head)(ctx, `Skill: ${skillName}`);
|
|
134
|
+
if (ctx.json) {
|
|
135
|
+
console.log(JSON.stringify(refs, null, 2));
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
if (!refs.length) {
|
|
139
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " (no references — add with: skill refs add <name> <path> --body ...)", ctx.color));
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
for (const r of refs) {
|
|
143
|
+
(0, fmt_1.out)(ctx, ` ${r.path}`);
|
|
144
|
+
if (r.body)
|
|
145
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` ${String(r.body).slice(0, 80)}`, ctx.color));
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
85
149
|
case "add": {
|
|
86
150
|
const n = args.flags.name ? String(args.flags.name) : "";
|
|
87
151
|
const file = args.flags.f ? String(args.flags.f) : "";
|
|
@@ -74,6 +74,57 @@ function buildBrief(env) {
|
|
|
74
74
|
function run(env, action, rest, args) {
|
|
75
75
|
const { ctx, store } = env;
|
|
76
76
|
switch (action) {
|
|
77
|
+
case "ask":
|
|
78
|
+
case "?": {
|
|
79
|
+
// v2: natural language interface — keyword routing to other commands
|
|
80
|
+
const question = rest.join(" ");
|
|
81
|
+
if (!question) {
|
|
82
|
+
(0, fmt_1.bad)(ctx, 'usage: ygl-trees agent ask "<question>"');
|
|
83
|
+
(0, fmt_1.out)(ctx, ' examples: ask "what projects" | ask "search AI" | ask "健康"');
|
|
84
|
+
return 2;
|
|
85
|
+
}
|
|
86
|
+
const q = question.toLowerCase();
|
|
87
|
+
const routes = [
|
|
88
|
+
{ pattern: /what.*project|项目列表|所有项目/, label: "project list", group: "project", action: "list" },
|
|
89
|
+
{ pattern: /search|搜索|检索|找/, label: "kb search", group: "kb", action: "search",
|
|
90
|
+
extra: [question.replace(/.*?(search|搜索|检索|找)\s*/i, "").replace(/["']/g, "")] },
|
|
91
|
+
{ pattern: /tree|树/, label: "node list", group: "node", action: "list" },
|
|
92
|
+
{ pattern: /health|健康|doctor|检查/, label: "doctor", group: "doctor", action: "" },
|
|
93
|
+
{ pattern: /skill|技能/, label: "skill list", group: "skill", action: "list" },
|
|
94
|
+
{ pattern: /task|任务/, label: "task list", group: "task", action: "list" },
|
|
95
|
+
{ pattern: /metric|指标/, label: "agent metrics", group: "agent", action: "metrics" },
|
|
96
|
+
];
|
|
97
|
+
let matched = false;
|
|
98
|
+
for (const r of routes) {
|
|
99
|
+
if (r.pattern.test(q)) {
|
|
100
|
+
if (!ctx.json)
|
|
101
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` → ${r.label}`, ctx.color));
|
|
102
|
+
// Execute via CLI sub-process
|
|
103
|
+
const cmdParts = [r.group, r.action, ...(r.extra || [])].filter(Boolean);
|
|
104
|
+
const { execSync } = require("node:child_process");
|
|
105
|
+
const self = process.argv[1];
|
|
106
|
+
try {
|
|
107
|
+
const result = execSync(`${JSON.stringify(process.execPath)} ${JSON.stringify(self)} --no-color ${cmdParts.map(c => JSON.stringify(c)).join(" ")}`, { env: { ...process.env }, encoding: "utf8", timeout: 10000 });
|
|
108
|
+
(0, fmt_1.out)(ctx, result.trim());
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
(0, fmt_1.bad)(ctx, String(e?.stdout || e?.message || e).slice(0, 200));
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Default: brief
|
|
118
|
+
if (!ctx.json)
|
|
119
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " → agent brief (no specific match)", ctx.color));
|
|
120
|
+
const payload = buildBrief(env);
|
|
121
|
+
if (ctx.json) {
|
|
122
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
// Reuse brief rendering
|
|
126
|
+
return run(env, "brief", [], args);
|
|
127
|
+
}
|
|
77
128
|
case "metrics": {
|
|
78
129
|
// R11 cold-start research → automated measurement. Health/efficiency
|
|
79
130
|
// metrics for the workspace — one command answers "how good is this
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.run = run;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
37
38
|
const fs = __importStar(require("node:fs"));
|
|
38
39
|
const fmt_1 = require("../../../core/text/src/fmt");
|
|
39
40
|
const db_1 = require("../../../core/database/src/db");
|
|
@@ -132,6 +133,210 @@ function run(env, action, rest, args) {
|
|
|
132
133
|
(0, fmt_1.show)(ctx, { file, size_kb: (fs.statSync(file).size / 1024).toFixed(1), ...s });
|
|
133
134
|
return 0;
|
|
134
135
|
}
|
|
136
|
+
case "time-travel": {
|
|
137
|
+
// v2: query workspace state at a past moment using entity created_at
|
|
138
|
+
const when = rest[0];
|
|
139
|
+
if (!when) {
|
|
140
|
+
(0, fmt_1.bad)(ctx, 'usage: db time-travel "YYYY-MM-DD HH:MM" (or "YYYY-MM-DD")');
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
const ts = when.includes(" ") ? when : when + " 23:59:59";
|
|
144
|
+
const tables = ["projects", "sources", "evidence", "claims", "kb_notes", "skills", "trees", "nodes", "artifacts", "events"];
|
|
145
|
+
const counts = {};
|
|
146
|
+
for (const t of tables) {
|
|
147
|
+
try {
|
|
148
|
+
const r = db.get(`SELECT COUNT(*) as n FROM ${t} WHERE created <= ?`, [ts]);
|
|
149
|
+
counts[t] = r?.n || 0;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
counts[t] = 0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// node unlock state at that time (via events)
|
|
156
|
+
let unlockedAt = 0;
|
|
157
|
+
try {
|
|
158
|
+
const r = db.get(`SELECT COUNT(DISTINCT entity_id) as n FROM events WHERE kind='node.unlock' AND ts <= ?`, [ts]);
|
|
159
|
+
unlockedAt = r?.n || 0;
|
|
160
|
+
}
|
|
161
|
+
catch { /* */ }
|
|
162
|
+
if (ctx.json) {
|
|
163
|
+
console.log(JSON.stringify({ timestamp: ts, counts, unlocked_nodes: unlockedAt }, null, 2));
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
(0, fmt_1.head)(ctx, `Workspace at ${ts}`);
|
|
167
|
+
const labels = {
|
|
168
|
+
projects: "项目", sources: "来源", evidence: "证据", claims: "断言",
|
|
169
|
+
kb_notes: "知识", skills: "技能", trees: "树", nodes: "节点",
|
|
170
|
+
artifacts: "产出", events: "事件",
|
|
171
|
+
};
|
|
172
|
+
for (const [k, v] of Object.entries(counts)) {
|
|
173
|
+
(0, fmt_1.out)(ctx, ` ${String(labels[k] || k).padEnd(6)} ${String(v).padStart(5)}`);
|
|
174
|
+
}
|
|
175
|
+
(0, fmt_1.out)(ctx, ` 已解锁 ${String(unlockedAt).padStart(5)}`);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
case "timeline": {
|
|
179
|
+
// v2: workspace growth timeline — events per day with entity milestones
|
|
180
|
+
const days = db.all(`
|
|
181
|
+
SELECT date(ts) as day, COUNT(*) as events,
|
|
182
|
+
SUM(CASE WHEN kind LIKE 'project%' THEN 1 ELSE 0 END) as projects,
|
|
183
|
+
SUM(CASE WHEN kind LIKE 'source%' THEN 1 ELSE 0 END) as sources,
|
|
184
|
+
SUM(CASE WHEN kind LIKE 'claim%' THEN 1 ELSE 0 END) as claims,
|
|
185
|
+
SUM(CASE WHEN kind LIKE 'kb%' THEN 1 ELSE 0 END) as kb,
|
|
186
|
+
SUM(CASE WHEN kind LIKE 'node%' THEN 1 ELSE 0 END) as nodes
|
|
187
|
+
FROM events GROUP BY date(ts) ORDER BY day
|
|
188
|
+
`);
|
|
189
|
+
if (!days.length) {
|
|
190
|
+
(0, fmt_1.ok)(ctx, "no events yet");
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
const maxEvents = Math.max(...days.map(d => d.events));
|
|
194
|
+
(0, fmt_1.head)(ctx, "Workspace Timeline");
|
|
195
|
+
let totalP = 0, totalS = 0, totalC = 0, totalK = 0, totalN = 0;
|
|
196
|
+
for (const d of days) {
|
|
197
|
+
totalP += d.projects;
|
|
198
|
+
totalS += d.sources;
|
|
199
|
+
totalC += d.claims;
|
|
200
|
+
totalK += d.kb;
|
|
201
|
+
totalN += d.nodes;
|
|
202
|
+
const barLen = Math.round(d.events / maxEvents * 30);
|
|
203
|
+
const bar = "█".repeat(barLen);
|
|
204
|
+
const milestones = [];
|
|
205
|
+
if (d.projects > 0)
|
|
206
|
+
milestones.push(`+${d.projects}项目`);
|
|
207
|
+
if (d.sources > 0)
|
|
208
|
+
milestones.push(`+${d.sources}来源`);
|
|
209
|
+
if (d.claims > 0)
|
|
210
|
+
milestones.push(`+${d.claims}断言`);
|
|
211
|
+
if (d.kb > 0)
|
|
212
|
+
milestones.push(`+${d.kb}知识`);
|
|
213
|
+
if (d.nodes > 0)
|
|
214
|
+
milestones.push(`+${d.nodes}节点`);
|
|
215
|
+
(0, fmt_1.out)(ctx, ` ${d.day} ${bar.padEnd(32)} ${String(d.events).padStart(5)} events ${milestones.join(" ")}`);
|
|
216
|
+
}
|
|
217
|
+
(0, fmt_1.out)(ctx, "");
|
|
218
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` 累计: ${totalP}项目 · ${totalS}来源 · ${totalC}断言 · ${totalK}知识 · ${totalN}节点变更`, ctx.color));
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
case "workspace": {
|
|
222
|
+
// v2: multi-workspace management — each workspace is a YGL_HOME directory
|
|
223
|
+
const sub = rest[0] || "list";
|
|
224
|
+
const wsRoot = path.join(env.home, ".."); // parent of current workspace
|
|
225
|
+
if (sub === "list") {
|
|
226
|
+
const dirs = fs.existsSync(wsRoot) ? fs.readdirSync(wsRoot).filter((d) => fs.existsSync(path.join(wsRoot, d, "ygl-trees.db"))) : [];
|
|
227
|
+
const cur = path.basename(env.home);
|
|
228
|
+
(0, fmt_1.head)(ctx, "Workspaces (parent: " + wsRoot + ")");
|
|
229
|
+
for (const d of dirs) {
|
|
230
|
+
(0, fmt_1.out)(ctx, (d === cur ? " * " : " ") + d);
|
|
231
|
+
}
|
|
232
|
+
(0, fmt_1.out)(ctx, "");
|
|
233
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " * = current | switch: YGL_HOME=<path> ygl-trees <cmd>", ctx.color));
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
if (sub === "create") {
|
|
237
|
+
const name = rest[1];
|
|
238
|
+
if (!name) {
|
|
239
|
+
(0, fmt_1.bad)(ctx, "usage: db workspace create <name>");
|
|
240
|
+
return 2;
|
|
241
|
+
}
|
|
242
|
+
const dir = path.join(wsRoot, name);
|
|
243
|
+
if (fs.existsSync(path.join(dir, "ygl-trees.db"))) {
|
|
244
|
+
(0, fmt_1.ok)(ctx, `workspace already exists: ${name}`);
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
248
|
+
(0, fmt_1.ok)(ctx, `workspace created: ${dir}`);
|
|
249
|
+
(0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` init: YGL_HOME=${dir} ygl-trees db init`, ctx.color));
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
(0, fmt_1.bad)(ctx, "usage: db workspace list | create <name>");
|
|
253
|
+
return 2;
|
|
254
|
+
}
|
|
255
|
+
case "obsidian": {
|
|
256
|
+
// v2 feature: export workspace as Obsidian-compatible markdown vault
|
|
257
|
+
const outDir = rest[0] || path.join(env.home, "obsidian-export");
|
|
258
|
+
fs.mkdirSync(path.join(outDir, "projects"), { recursive: true });
|
|
259
|
+
fs.mkdirSync(path.join(outDir, "knowledge"), { recursive: true });
|
|
260
|
+
fs.mkdirSync(path.join(outDir, "skills"), { recursive: true });
|
|
261
|
+
fs.mkdirSync(path.join(outDir, "trees"), { recursive: true });
|
|
262
|
+
fs.mkdirSync(path.join(outDir, "claims"), { recursive: true });
|
|
263
|
+
let count = 0;
|
|
264
|
+
const safe = (t) => String(t).replace(/[\/:*?"<>|]/g, "_").slice(0, 50);
|
|
265
|
+
for (const p of db.all("SELECT * FROM projects")) {
|
|
266
|
+
fs.writeFileSync(path.join(outDir, "projects", safe(p.name) + ".md"), `---
|
|
267
|
+
type: project
|
|
268
|
+
id: ${p.id}
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
# ${p.name}
|
|
272
|
+
|
|
273
|
+
${p.question || ""}
|
|
274
|
+
`);
|
|
275
|
+
count++;
|
|
276
|
+
}
|
|
277
|
+
for (const n of db.all("SELECT * FROM kb_notes")) {
|
|
278
|
+
fs.writeFileSync(path.join(outDir, "knowledge", safe(n.title) + ".md"), `---
|
|
279
|
+
type: knowledge
|
|
280
|
+
tags: ${n.tags || ""}
|
|
281
|
+
confidence: ${n.confidence}
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
# ${n.title}
|
|
285
|
+
|
|
286
|
+
${n.body}
|
|
287
|
+
`);
|
|
288
|
+
count++;
|
|
289
|
+
}
|
|
290
|
+
for (const sk of db.all("SELECT * FROM skills WHERE enabled=1")) {
|
|
291
|
+
fs.writeFileSync(path.join(outDir, "skills", safe(sk.name) + ".md"), `---
|
|
292
|
+
type: skill
|
|
293
|
+
version: ${sk.version}
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
# ${sk.name}
|
|
297
|
+
|
|
298
|
+
${String(sk.body).slice(0, 500)}
|
|
299
|
+
`);
|
|
300
|
+
count++;
|
|
301
|
+
}
|
|
302
|
+
for (const t of db.all("SELECT * FROM trees")) {
|
|
303
|
+
const nodes = db.all("SELECT * FROM nodes WHERE tree_id=?", [t.id]);
|
|
304
|
+
let c = `---
|
|
305
|
+
type: tree
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
# ${t.name} (${nodes.length} nodes)
|
|
309
|
+
|
|
310
|
+
`;
|
|
311
|
+
for (const n of nodes)
|
|
312
|
+
c += `- **${n.key}** (${n.kind}, tier ${n.tier})
|
|
313
|
+
`;
|
|
314
|
+
fs.writeFileSync(path.join(outDir, "trees", safe(t.name) + ".md"), c);
|
|
315
|
+
count++;
|
|
316
|
+
}
|
|
317
|
+
for (const cl of db.all("SELECT * FROM claims")) {
|
|
318
|
+
const links = db.all("SELECT e.quote FROM claim_evidence ce JOIN evidence e ON ce.evidence_id=e.id WHERE ce.claim_id=?", [cl.id]);
|
|
319
|
+
let c2 = `---
|
|
320
|
+
type: claim
|
|
321
|
+
status: ${cl.status}
|
|
322
|
+
confidence: ${cl.confidence}
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
# ${cl.id}
|
|
326
|
+
|
|
327
|
+
${cl.text}
|
|
328
|
+
|
|
329
|
+
## Evidence
|
|
330
|
+
`;
|
|
331
|
+
for (const l of links)
|
|
332
|
+
c2 += `- > ${String(l.quote).slice(0, 100)}
|
|
333
|
+
`;
|
|
334
|
+
fs.writeFileSync(path.join(outDir, "claims", cl.id + ".md"), c2);
|
|
335
|
+
count++;
|
|
336
|
+
}
|
|
337
|
+
(0, fmt_1.ok)(ctx, `obsidian vault exported: ${count} files → ${outDir}`);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
135
340
|
case "export": {
|
|
136
341
|
const dest = rest[0] || `ygl-trees-dump-${Date.now()}.sql`;
|
|
137
342
|
// --lean: skip derived-table data (node_closure / node_index are
|
|
@@ -145,6 +145,59 @@ function gatherData(env) {
|
|
|
145
145
|
.map((r) => ({ id: r.id, kind: r.kind, title: r.title, version: r.version, created: r.created }));
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
// R19: knowledge graph — entities and relations from the rel fabric
|
|
149
|
+
const kgNodes = [];
|
|
150
|
+
const kgEdges = [];
|
|
151
|
+
const kgMap = new Map();
|
|
152
|
+
try {
|
|
153
|
+
const rels = db.all("SELECT src_type, src_id, dst_type, dst_id, rel FROM relations LIMIT 200");
|
|
154
|
+
const colors = {
|
|
155
|
+
node: "#7c9885", claim: "#6a994e", kb: "#8b7355",
|
|
156
|
+
evidence: "#daa520", artifact: "#5b8db8", skill: "#9b7cb8",
|
|
157
|
+
};
|
|
158
|
+
for (const r of rels) {
|
|
159
|
+
for (const side of ["src", "dst"]) {
|
|
160
|
+
const type = r[`${side}_type`];
|
|
161
|
+
const id = r[`${side}_id`];
|
|
162
|
+
const key = `${type}:${id}`;
|
|
163
|
+
if (kgMap.has(key))
|
|
164
|
+
continue;
|
|
165
|
+
let label = id;
|
|
166
|
+
try {
|
|
167
|
+
if (type === "node") {
|
|
168
|
+
const n = db.get("SELECT name FROM nodes WHERE id=?", [id]);
|
|
169
|
+
if (n)
|
|
170
|
+
label = n.name;
|
|
171
|
+
}
|
|
172
|
+
else if (type === "claim") {
|
|
173
|
+
const c = db.get("SELECT substr(text,1,40) AS t FROM claims WHERE id=?", [id]);
|
|
174
|
+
if (c)
|
|
175
|
+
label = c.t;
|
|
176
|
+
}
|
|
177
|
+
else if (type === "kb") {
|
|
178
|
+
const k = db.get("SELECT title FROM kb_notes WHERE id=?", [id]);
|
|
179
|
+
if (k)
|
|
180
|
+
label = k.title.slice(0, 40);
|
|
181
|
+
}
|
|
182
|
+
else if (type === "evidence") {
|
|
183
|
+
const e = db.get("SELECT substr(quote,1,40) AS q FROM evidence WHERE id=?", [id]);
|
|
184
|
+
if (e)
|
|
185
|
+
label = e.q;
|
|
186
|
+
}
|
|
187
|
+
else if (type === "artifact") {
|
|
188
|
+
const a = db.get("SELECT title FROM artifacts WHERE id=?", [id]);
|
|
189
|
+
if (a)
|
|
190
|
+
label = a.title;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch { /* best-effort */ }
|
|
194
|
+
kgMap.set(key, { id: key, label, type, color: colors[type] || "#999" });
|
|
195
|
+
}
|
|
196
|
+
kgEdges.push({ source: `${r.src_type}:${r.src_id}`, target: `${r.dst_type}:${r.dst_id}`, label: r.rel });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* graph data best-effort */ }
|
|
200
|
+
data.knowledge_graph = { nodes: Array.from(kgMap.values()), edges: kgEdges };
|
|
148
201
|
return data;
|
|
149
202
|
}
|
|
150
203
|
/** Single-file dashboard HTML. `live` adds a fetch bootstrap + auto-refresh. */
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ygl-trees v0.
|
|
1
|
+
# ygl-trees v0.72.0 — AI Agent Research Workbench
|
|
2
2
|
|
|
3
3
|
> Single SQLite source of truth. Research → Production → Knowledge Reuse + Tech Tree.
|
|
4
4
|
> CLI for AI agents: every command supports --json (machine-readable, including errors).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ygl-trees",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
4
4
|
"description": "猿光 · 科技树 — database-centric CLI for research → production → knowledge reuse, built for AI agents.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/apps/cli/index.js",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@deepseek-ai/dsh-atomic-write": "0.0.1-rc.1",
|
|
23
23
|
"better-sqlite3": "^13.0.3",
|
|
24
|
-
"node-sqlite3-wasm": "^0.8.
|
|
24
|
+
"node-sqlite3-wasm": "^0.8.61",
|
|
25
25
|
"reflect-metadata": "^0.2.2",
|
|
26
26
|
"typeorm": "^1.1.2",
|
|
27
27
|
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
28
|
+
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
|
|
28
29
|
"@deepseek-ai/schemastery": "~3.18.5-alpha.2",
|
|
29
|
-
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
|
|
30
|
-
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2"
|
|
30
|
+
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^20.0.0",
|