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 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 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 则零影响
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 log # 审计事件流
266
+ ygl-trees agent ask "<question>" natural language interface
267
+ agent log # 审计事件流
263
268
  ```
264
269
 
265
270
  `agent brief --json` 直接给出结构化状态和建议动作序列,Agent 可以照着执行。
@@ -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
- (0, fmt_1.bad)(ctx, `unknown group: ${group}`);
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;
@@ -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
- bad(ctx, `unknown group: ${group}`);
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.69.0 — AI Agent Research Workbench
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.70.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.60",
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",