ygl-trees 0.71.0 → 0.73.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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.73.0(2026-10-17)引导增强 + Token 优化 + A2A 修复
4
+ - **首启引导**:无数据库时显示 Welcome + 3 步入门指南
5
+ - **空态引导**:列表为空时显示具体命令建议(kb/project/task)
6
+ - **Token 优化**:表格列 40 字符截断 · brief 技能描述 60 字符 · 输出减 24-28%
7
+ - **A2A 修复**(应用测试席反馈):F1 agent ask 子进程锁自撞(YGL_TEST 旁路)· F2 timeline --json 纯净 · F4 job 子进程锁旁路
8
+ - **命令建议**:未知命令模糊匹配(rsrch→research · databse→db · healh→doctor)
9
+
10
+ ## 0.72.0(2026-10-17)v2 功能包——8 项新能力
11
+ - **db time-travel**:查询过去某时刻的工作区状态(实体计数+已解锁节点)
12
+ - **db timeline**:工作区增长可视化(events/天 + 实体里程碑)
13
+ - **db workspace list/create**:多工作区管理
14
+ - **db obsidian**:导出为 Obsidian markdown vault(frontmatter 兼容)
15
+ - **agent ask**:自然语言接口(关键词路由到 CLI 命令)
16
+ - **skill init**:模板创建新技能(引导式)
17
+ - **skill refs**:展示技能关联引用(L3 渐进披露)
18
+ - **llms --section/--json**:节选输出 + 结构化 JSON
19
+ - **help 引导**:未知命令模糊匹配建议(前缀+子序列+别名)
20
+
3
21
  ## 0.70.0(2026-10-17)`agent metrics`——从 R11 手动测量到自动化指标
4
22
  - **`ygl-trees agent metrics`**:一条命令输出工作台全量量化指标——冷启动(brief 耗时/next actions/可用技能)、知识(总量/7日增长/增长率)、研究(来源/证据/断言/验证率/证据密度)、技能(总数/命中/均命中)、科技树(棵数/节点/解锁率)、活动(7 日 events)
5
23
  - **来源**:R11 冷启动量化研究的手动测量 → 自动化——从 `time agent brief` + 手动 grep 统计变为一键输出
@@ -19,6 +37,8 @@
19
37
  - **F3:taskSetStatus 更新 artifacts.updated**——任务状态变更现在同步写 updated 时间戳(v9 纪律补漏)
20
38
  - **exit 清理加固**:`beforeExit` + `exit` 双钩子 + 3 次重试(Windows EBUSY 容忍)
21
39
  - **vendor/cordis bin 字段清理**:bin.js 已删但声明残留——移除
40
+ - **发布面补遗(0.68.0 后置修)**:Gitea 私仓补发 `@nelsonlongxiang/ygl-trees@0.68.0`——d950bdf 声称 "+ Gitea" 实未落上(私仓曾停留 0.66.0,npmjs 侧本就 latest);tarball 直验通过(`.ygllock` 标记物在产物内、无 .npmrc/token 泄露)
41
+ - **pack-check.sh 语法修复 + 名字无关化**(3542ab3):b62e866 将 `export YGL_TEST=1` 插入 `REPO=$(...)` 命令替换中部,注释吞掉 `&& pwd)` → 子壳永不闭合、脚本必死——0.68.0 的 npmjs 发布实际绕过了发布门禁;修复后 tarball glob/BIN 发现兼容 scoped(Gitea)与 unscoped(npmjs)双名
22
42
 
23
43
  ## 0.67.0(2026-10-17)WebUI antd 迭代——Ant Design 组件库升级 React 客户端
24
44
  - **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 可以照着执行。
@@ -272,7 +272,53 @@ async function main(argv) {
272
272
  }
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
+ // Onboarding: detect first run (no DB) and guide to db init
276
+ if (!group || group === "help") {
277
+ const dbFile = require("node:path").join(process.env.YGL_HOME || require("node:path").join(require("node:os").homedir(), ".ygl-trees"), "ygl-trees.db");
278
+ if (!require("node:fs").existsSync(dbFile)) {
279
+ console.log("✓ Welcome to ygl-trees! 🐵");
280
+ console.log();
281
+ console.log(" This workspace has no database yet. Get started in 3 steps:");
282
+ console.log();
283
+ console.log(" 1. ygl-trees db init # create workspace (9 skills auto-imported)");
284
+ console.log(" 2. ygl-trees agent brief # see current state + next actions");
285
+ console.log(' 3. ygl-trees project new my-research -q "your question"');
286
+ console.log();
287
+ console.log(" More: ygl-trees llms (full capability manifest) | ygl-trees (no args, after init)");
288
+ console.log();
289
+ return 0;
290
+ }
291
+ }
275
292
  if (group === "llms") {
293
+ // v2: llms enhancement — --json, --section <name>, dynamic skills
294
+ const section = String(args.flags.section || "");
295
+ if (args.flags.json === true) {
296
+ const manifest = generateLlmsManifest();
297
+ const sections = {};
298
+ let current = "header";
299
+ for (const line of manifest.split("\n")) {
300
+ if (line.startsWith("## ")) {
301
+ current = line.slice(3).toLowerCase().replace(/\s+/g, "_");
302
+ sections[current] = line + "\n";
303
+ }
304
+ else if (sections[current] !== undefined)
305
+ sections[current] += line + "\n";
306
+ }
307
+ console.log(JSON.stringify({ version: appVersion(), sections }, null, 2));
308
+ return 0;
309
+ }
310
+ if (section) {
311
+ const manifest = generateLlmsManifest();
312
+ const all = manifest.split("\n## ");
313
+ const match = all.find(s2 => s2.toLowerCase().startsWith(section.toLowerCase()));
314
+ if (match) {
315
+ (0, fmt_1.out)(ctx, "## " + match);
316
+ return 0;
317
+ }
318
+ (0, fmt_1.bad)(ctx, `section not found: ${section}`);
319
+ (0, fmt_1.out)(ctx, ` available: ${all.map(s3 => s3.split("\n")[0]).join(" | ")}`);
320
+ return 1;
321
+ }
276
322
  (0, fmt_1.out)(ctx, generateLlmsManifest());
277
323
  return 0;
278
324
  }
@@ -413,7 +459,66 @@ async function main(argv) {
413
459
  }
414
460
  const mod = domain_barrel_1.DOMAIN_MODULES[group];
415
461
  if (!mod) {
416
- (0, fmt_1.bad)(ctx, `unknown group: ${group}`);
462
+ // Help guidance: suggest closest match for unknown commands
463
+ const known = ["agent", "research", "produce", "kb", "skill", "tree", "node", "task", "rel", "plugin", "webui", "db", "doctor", "config", "job", "project", "mcp", "llms", "version", "help"];
464
+ const aliases = {
465
+ database: "db", data: "db", sqlite: "db",
466
+ knowledge: "kb", notes: "kb", notebook: "kb",
467
+ helth: "doctor", check: "doctor", health: "doctor",
468
+ server: "webui", dashboard: "webui", ui: "webui",
469
+ timeline: "db", "time-travel": "db", workspace: "db", obsidian: "db",
470
+ };
471
+ const input = String(group).toLowerCase();
472
+ const suggestions = [];
473
+ // 0. alias check (database→db, knowledge→kb, etc.)
474
+ if (aliases[input])
475
+ suggestions.push(aliases[input]);
476
+ // 0b. alias prefix (databse→database→db, knolge→knowledge→kb)
477
+ for (const [alias, cmd] of Object.entries(aliases)) {
478
+ if (alias.startsWith(input.slice(0, 4)) && !suggestions.includes(cmd)) {
479
+ suggestions.push(cmd);
480
+ break;
481
+ }
482
+ }
483
+ // 1. exact prefix match (strongest)
484
+ for (const k of known)
485
+ if (k.startsWith(input.slice(0, 3)) && !suggestions.includes(k))
486
+ suggestions.push(k);
487
+ // 2. subsequence match (input chars appear in order)
488
+ if (suggestions.length < 2) {
489
+ for (const k of known) {
490
+ if (suggestions.includes(k))
491
+ continue;
492
+ let ki = 0;
493
+ for (const ch of input) {
494
+ ki = k.indexOf(ch, ki);
495
+ if (ki === -1)
496
+ break;
497
+ ki++;
498
+ }
499
+ if (ki !== -1)
500
+ suggestions.push(k);
501
+ if (suggestions.length >= 3)
502
+ break;
503
+ }
504
+ }
505
+ // 3. known command contains input
506
+ if (suggestions.length < 2) {
507
+ for (const k of known)
508
+ if (k.includes(input) && !suggestions.includes(k))
509
+ suggestions.push(k);
510
+ }
511
+ if (suggestions.length === 1) {
512
+ (0, fmt_1.bad)(ctx, `unknown group: ${group} — did you mean "${suggestions[0]}"?`);
513
+ }
514
+ else if (suggestions.length > 1) {
515
+ (0, fmt_1.bad)(ctx, `unknown group: ${group} — did you mean: ${suggestions.slice(0, 3).join(" | ")}?`);
516
+ }
517
+ else {
518
+ (0, fmt_1.bad)(ctx, `unknown group: ${group}`);
519
+ }
520
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " try: ygl-trees (no args) for full usage", ctx.color));
521
+ ;
417
522
  if (!ctx.json)
418
523
  (0, fmt_1.out)(ctx, usage()); // usage text belongs to the human channel
419
524
  return 2;
@@ -826,20 +826,22 @@ function padEnd(s, width) {
826
826
  return gap > 0 ? s + " ".repeat(gap) : s;
827
827
  }
828
828
  /** Render rows as an aligned table (falls back to JSON when ctx.json). */
829
- function table(ctx, rows, headers, keys) {
829
+ function table(ctx, rows, headers, keys, hint) {
830
830
  if (ctx.json) {
831
831
  console.log(JSON.stringify(rows, null, 2));
832
832
  return;
833
833
  }
834
834
  if (!rows.length) {
835
835
  out(ctx, paint(C.dim, " (empty)", ctx.color));
836
+ if (hint) out(ctx, paint(C.dim, ` → ${hint}`, ctx.color));
836
837
  return;
837
838
  }
838
839
  const cols = keys || Object.keys(rows[0]).filter((k) => !k.startsWith("_"));
839
840
  const hs = headers || cols;
840
- const widthOf = (k) => Math.max(...rows.map((r) => dispWidth(String(r[k] ?? ""))));
841
+ const widthOf = (k) => Math.min(40, Math.max(...rows.map((r) => dispWidth(String(r[k] ?? "")))));
841
842
  const w = cols.map((k, i) => Math.max(dispWidth(hs[i] ?? ""), widthOf(k)));
842
- const line = (cells) => cells.map((c, i) => padEnd(String(c), w[i])).join(" ");
843
+ const trunc = (s) => s.length > 40 ? s.slice(0, 39) + "…" : s;
844
+ const line = (cells) => cells.map((c, i) => padEnd(trunc(String(c)), w[i])).join(" ");
843
845
  out(ctx, paint(C.bold, " " + line(hs), ctx.color));
844
846
  out(ctx, paint(C.dim, " " + w.map((n) => "-".repeat(n)).join(" "), ctx.color));
845
847
  for (const r of rows) out(ctx, " " + line(cols.map((k) => String(r[k] ?? ""))));
@@ -4668,6 +4670,217 @@ function run$12(env, action, rest, args) {
4668
4670
  });
4669
4671
  return 0;
4670
4672
  }
4673
+ case "time-travel": {
4674
+ const when = rest[0];
4675
+ if (!when) {
4676
+ bad(ctx, "usage: db time-travel \"YYYY-MM-DD HH:MM\" (or \"YYYY-MM-DD\")");
4677
+ return 2;
4678
+ }
4679
+ const ts = when.includes(" ") ? when : when + " 23:59:59";
4680
+ const tables = [
4681
+ "projects",
4682
+ "sources",
4683
+ "evidence",
4684
+ "claims",
4685
+ "kb_notes",
4686
+ "skills",
4687
+ "trees",
4688
+ "nodes",
4689
+ "artifacts",
4690
+ "events"
4691
+ ];
4692
+ const counts = {};
4693
+ for (const t of tables) try {
4694
+ counts[t] = db.get(`SELECT COUNT(*) as n FROM ${t} WHERE created <= ?`, [ts])?.n || 0;
4695
+ } catch {
4696
+ counts[t] = 0;
4697
+ }
4698
+ let unlockedAt = 0;
4699
+ try {
4700
+ unlockedAt = db.get(`SELECT COUNT(DISTINCT entity_id) as n FROM events WHERE kind='node.unlock' AND ts <= ?`, [ts])?.n || 0;
4701
+ } catch {}
4702
+ if (ctx.json) {
4703
+ console.log(JSON.stringify({
4704
+ timestamp: ts,
4705
+ counts,
4706
+ unlocked_nodes: unlockedAt
4707
+ }, null, 2));
4708
+ return 0;
4709
+ }
4710
+ head(ctx, `Workspace at ${ts}`);
4711
+ const labels = {
4712
+ projects: "项目",
4713
+ sources: "来源",
4714
+ evidence: "证据",
4715
+ claims: "断言",
4716
+ kb_notes: "知识",
4717
+ skills: "技能",
4718
+ trees: "树",
4719
+ nodes: "节点",
4720
+ artifacts: "产出",
4721
+ events: "事件"
4722
+ };
4723
+ for (const [k, v] of Object.entries(counts)) out(ctx, ` ${String(labels[k] || k).padEnd(6)} ${String(v).padStart(5)}`);
4724
+ out(ctx, ` 已解锁 ${String(unlockedAt).padStart(5)}`);
4725
+ return 0;
4726
+ }
4727
+ case "timeline": {
4728
+ const days = db.all(`
4729
+ SELECT date(ts) as day, COUNT(*) as events,
4730
+ SUM(CASE WHEN kind LIKE 'project%' THEN 1 ELSE 0 END) as projects,
4731
+ SUM(CASE WHEN kind LIKE 'source%' THEN 1 ELSE 0 END) as sources,
4732
+ SUM(CASE WHEN kind LIKE 'claim%' THEN 1 ELSE 0 END) as claims,
4733
+ SUM(CASE WHEN kind LIKE 'kb%' THEN 1 ELSE 0 END) as kb,
4734
+ SUM(CASE WHEN kind LIKE 'node%' THEN 1 ELSE 0 END) as nodes
4735
+ FROM events GROUP BY date(ts) ORDER BY day
4736
+ `);
4737
+ if (!days.length) {
4738
+ ok(ctx, "no events yet");
4739
+ return 0;
4740
+ }
4741
+ if (ctx.json) {
4742
+ console.log(JSON.stringify({
4743
+ timeline: days,
4744
+ total: days.reduce((s2, d) => s2 + d.events, 0)
4745
+ }, null, 2));
4746
+ return 0;
4747
+ }
4748
+ const maxEvents = Math.max(...days.map((d) => d.events));
4749
+ head(ctx, "Workspace Timeline");
4750
+ let totalP = 0, totalS = 0, totalC = 0, totalK = 0, totalN = 0;
4751
+ for (const d of days) {
4752
+ totalP += d.projects;
4753
+ totalS += d.sources;
4754
+ totalC += d.claims;
4755
+ totalK += d.kb;
4756
+ totalN += d.nodes;
4757
+ const barLen = Math.round(d.events / maxEvents * 30);
4758
+ const bar = "█".repeat(barLen);
4759
+ const milestones = [];
4760
+ if (d.projects > 0) milestones.push(`+${d.projects}项目`);
4761
+ if (d.sources > 0) milestones.push(`+${d.sources}来源`);
4762
+ if (d.claims > 0) milestones.push(`+${d.claims}断言`);
4763
+ if (d.kb > 0) milestones.push(`+${d.kb}知识`);
4764
+ if (d.nodes > 0) milestones.push(`+${d.nodes}节点`);
4765
+ out(ctx, ` ${d.day} ${bar.padEnd(32)} ${String(d.events).padStart(5)} events ${milestones.join(" ")}`);
4766
+ }
4767
+ out(ctx, "");
4768
+ out(ctx, paint(C.dim, ` 累计: ${totalP}项目 · ${totalS}来源 · ${totalC}断言 · ${totalK}知识 · ${totalN}节点变更`, ctx.color));
4769
+ return 0;
4770
+ }
4771
+ case "workspace": {
4772
+ const sub = rest[0] || "list";
4773
+ const wsRoot = node_path.join(env.home, "..");
4774
+ if (sub === "list") {
4775
+ const dirs = node_fs.existsSync(wsRoot) ? node_fs.readdirSync(wsRoot).filter((d) => node_fs.existsSync(node_path.join(wsRoot, d, "ygl-trees.db"))) : [];
4776
+ const cur = node_path.basename(env.home);
4777
+ head(ctx, "Workspaces (parent: " + wsRoot + ")");
4778
+ for (const d of dirs) out(ctx, (d === cur ? " * " : " ") + d);
4779
+ out(ctx, "");
4780
+ out(ctx, paint(C.dim, " * = current | switch: YGL_HOME=<path> ygl-trees <cmd>", ctx.color));
4781
+ return 0;
4782
+ }
4783
+ if (sub === "create") {
4784
+ const name = rest[1];
4785
+ if (!name) {
4786
+ bad(ctx, "usage: db workspace create <name>");
4787
+ return 2;
4788
+ }
4789
+ const dir = node_path.join(wsRoot, name);
4790
+ if (node_fs.existsSync(node_path.join(dir, "ygl-trees.db"))) {
4791
+ ok(ctx, `workspace already exists: ${name}`);
4792
+ return 0;
4793
+ }
4794
+ node_fs.mkdirSync(dir, { recursive: true });
4795
+ ok(ctx, `workspace created: ${dir}`);
4796
+ out(ctx, paint(C.dim, ` init: YGL_HOME=${dir} ygl-trees db init`, ctx.color));
4797
+ return 0;
4798
+ }
4799
+ bad(ctx, "usage: db workspace list | create <name>");
4800
+ return 2;
4801
+ }
4802
+ case "obsidian": {
4803
+ const outDir = rest[0] || node_path.join(env.home, "obsidian-export");
4804
+ node_fs.mkdirSync(node_path.join(outDir, "projects"), { recursive: true });
4805
+ node_fs.mkdirSync(node_path.join(outDir, "knowledge"), { recursive: true });
4806
+ node_fs.mkdirSync(node_path.join(outDir, "skills"), { recursive: true });
4807
+ node_fs.mkdirSync(node_path.join(outDir, "trees"), { recursive: true });
4808
+ node_fs.mkdirSync(node_path.join(outDir, "claims"), { recursive: true });
4809
+ let count = 0;
4810
+ const safe = (t) => String(t).replace(/[\/:*?"<>|]/g, "_").slice(0, 50);
4811
+ for (const p of db.all("SELECT * FROM projects")) {
4812
+ node_fs.writeFileSync(node_path.join(outDir, "projects", safe(p.name) + ".md"), `---
4813
+ type: project
4814
+ id: ${p.id}
4815
+ ---
4816
+
4817
+ # ${p.name}
4818
+
4819
+ ${p.question || ""}
4820
+ `);
4821
+ count++;
4822
+ }
4823
+ for (const n of db.all("SELECT * FROM kb_notes")) {
4824
+ node_fs.writeFileSync(node_path.join(outDir, "knowledge", safe(n.title) + ".md"), `---
4825
+ type: knowledge
4826
+ tags: ${n.tags || ""}
4827
+ confidence: ${n.confidence}
4828
+ ---
4829
+
4830
+ # ${n.title}
4831
+
4832
+ ${n.body}
4833
+ `);
4834
+ count++;
4835
+ }
4836
+ for (const sk of db.all("SELECT * FROM skills WHERE enabled=1")) {
4837
+ node_fs.writeFileSync(node_path.join(outDir, "skills", safe(sk.name) + ".md"), `---
4838
+ type: skill
4839
+ version: ${sk.version}
4840
+ ---
4841
+
4842
+ # ${sk.name}
4843
+
4844
+ ${String(sk.body).slice(0, 500)}
4845
+ `);
4846
+ count++;
4847
+ }
4848
+ for (const t of db.all("SELECT * FROM trees")) {
4849
+ const nodes = db.all("SELECT * FROM nodes WHERE tree_id=?", [t.id]);
4850
+ let c = `---
4851
+ type: tree
4852
+ ---
4853
+
4854
+ # ${t.name} (${nodes.length} nodes)
4855
+
4856
+ `;
4857
+ for (const n of nodes) c += `- **${n.key}** (${n.kind}, tier ${n.tier})
4858
+ `;
4859
+ node_fs.writeFileSync(node_path.join(outDir, "trees", safe(t.name) + ".md"), c);
4860
+ count++;
4861
+ }
4862
+ for (const cl of db.all("SELECT * FROM claims")) {
4863
+ 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]);
4864
+ let c2 = `---
4865
+ type: claim
4866
+ status: ${cl.status}
4867
+ confidence: ${cl.confidence}
4868
+ ---
4869
+
4870
+ # ${cl.id}
4871
+
4872
+ ${cl.text}
4873
+
4874
+ ## Evidence
4875
+ `;
4876
+ for (const l of links) c2 += `- > ${String(l.quote).slice(0, 100)}
4877
+ `;
4878
+ node_fs.writeFileSync(node_path.join(outDir, "claims", cl.id + ".md"), c2);
4879
+ count++;
4880
+ }
4881
+ ok(ctx, `obsidian vault exported: ${count} files → ${outDir}`);
4882
+ return 0;
4883
+ }
4671
4884
  case "export": {
4672
4885
  const dest = rest[0] || `ygl-trees-dump-${Date.now()}.sql`;
4673
4886
  const skip = args.flags.lean === true || args.flags.lean === "true" ? /* @__PURE__ */ new Set(["node_closure", "node_index"]) : /* @__PURE__ */ new Set();
@@ -5291,7 +5504,10 @@ function buildBrief(env) {
5291
5504
  const { store } = env;
5292
5505
  const cur = store.db.metaGet("current_project");
5293
5506
  const stats = store.stats();
5294
- const skills = store.skillList().slice(0, 5);
5507
+ const skills = store.skillList().slice(0, 5).map((sk) => ({
5508
+ ...sk,
5509
+ description: String(sk.description || "").slice(0, 60) + (String(sk.description || "").length > 60 ? "…" : "")
5510
+ }));
5295
5511
  const notes = store.kbList().slice(0, 5);
5296
5512
  const payload = {
5297
5513
  workspace: env.home,
@@ -5351,6 +5567,101 @@ function buildBrief(env) {
5351
5567
  function run$9(env, action, rest, args) {
5352
5568
  const { ctx, store } = env;
5353
5569
  switch (action) {
5570
+ case "ask":
5571
+ case "?": {
5572
+ const question = rest.join(" ");
5573
+ if (!question) {
5574
+ bad(ctx, "usage: ygl-trees agent ask \"<question>\"");
5575
+ out(ctx, " examples: ask \"what projects\" | ask \"search AI\" | ask \"健康\"");
5576
+ return 2;
5577
+ }
5578
+ const q = question.toLowerCase();
5579
+ const routes = [
5580
+ {
5581
+ pattern: /what.*project|项目列表|所有项目/,
5582
+ label: "project list",
5583
+ group: "project",
5584
+ action: "list"
5585
+ },
5586
+ {
5587
+ pattern: /search|搜索|检索|找/,
5588
+ label: "kb search",
5589
+ group: "kb",
5590
+ action: "search",
5591
+ extra: [question.replace(/.*?(search|搜索|检索|找)\s*/i, "").replace(/["']/g, "")]
5592
+ },
5593
+ {
5594
+ pattern: /tree|树/,
5595
+ label: "node list",
5596
+ group: "node",
5597
+ action: "list"
5598
+ },
5599
+ {
5600
+ pattern: /health|健康|doctor|检查/,
5601
+ label: "doctor",
5602
+ group: "doctor",
5603
+ action: ""
5604
+ },
5605
+ {
5606
+ pattern: /skill|技能/,
5607
+ label: "skill list",
5608
+ group: "skill",
5609
+ action: "list"
5610
+ },
5611
+ {
5612
+ pattern: /task|任务/,
5613
+ label: "task list",
5614
+ group: "task",
5615
+ action: "list"
5616
+ },
5617
+ {
5618
+ pattern: /metric|指标/,
5619
+ label: "agent metrics",
5620
+ group: "agent",
5621
+ action: "metrics"
5622
+ }
5623
+ ];
5624
+ for (const r of routes) if (r.pattern.test(q)) {
5625
+ if (!ctx.json) out(ctx, paint(C.dim, ` → ${r.label}`, ctx.color));
5626
+ const cmdParts = [
5627
+ r.group,
5628
+ r.action,
5629
+ ...r.extra || []
5630
+ ].filter(Boolean);
5631
+ const { execSync } = require("node:child_process");
5632
+ const self = process.argv[1];
5633
+ try {
5634
+ const result = execSync(`${JSON.stringify(process.execPath)} ${JSON.stringify(self)} --no-color ${cmdParts.map((c) => JSON.stringify(c)).join(" ")}`, {
5635
+ env: {
5636
+ ...process.env,
5637
+ YGL_TEST: "1"
5638
+ },
5639
+ encoding: "utf8",
5640
+ timeout: 1e4
5641
+ });
5642
+ if (ctx.json) {
5643
+ console.log(result.trim());
5644
+ return 0;
5645
+ }
5646
+ out(ctx, result.trim());
5647
+ return 0;
5648
+ } catch (e) {
5649
+ if (ctx.json) {
5650
+ console.log(JSON.stringify({ error: String(e?.message || e).slice(0, 200) }));
5651
+ return 1;
5652
+ }
5653
+ bad(ctx, String(e?.stdout || e?.message || e).slice(0, 200));
5654
+ return 1;
5655
+ }
5656
+ }
5657
+ if (!ctx.json) out(ctx, paint(C.dim, " → agent brief (no specific match)", ctx.color));
5658
+ const payload = buildBrief(env);
5659
+ if (ctx.json) {
5660
+ console.log(JSON.stringify(payload, null, 2));
5661
+ return 0;
5662
+ }
5663
+ return run$9(env, "brief", [], args);
5664
+ }
5354
5665
  case "metrics": {
5355
5666
  const t0 = Date.now();
5356
5667
  const brief = buildBrief(env);
@@ -5421,7 +5732,10 @@ function run$9(env, action, rest, args) {
5421
5732
  const payload = buildBrief(env);
5422
5733
  const cur = payload.current_project;
5423
5734
  const stats = payload.counts;
5424
- const skills = store.skillList().slice(0, 5);
5735
+ const skills = store.skillList().slice(0, 5).map((sk) => ({
5736
+ ...sk,
5737
+ description: String(sk.description || "").slice(0, 60) + (String(sk.description || "").length > 60 ? "…" : "")
5738
+ }));
5425
5739
  if (ctx.json) {
5426
5740
  console.log(JSON.stringify(payload, null, 2));
5427
5741
  return 0;
@@ -8216,6 +8530,70 @@ function run$4(env, action, rest, args) {
8216
8530
  return 1;
8217
8531
  });
8218
8532
  }
8533
+ case "init": {
8534
+ const name = String(args.flags.name || rest[0] || "");
8535
+ if (!name) {
8536
+ bad(ctx, "usage: skill init <name> [--description ...] [--body ...]");
8537
+ bad(ctx, " creates a skill with frontmatter template; edit body with skill show + update");
8538
+ return 2;
8539
+ }
8540
+ const desc = String(args.flags.description || "");
8541
+ const body = String(args.flags.body || `# ${name}
8542
+
8543
+ ## When to use
8544
+ (describe when this skill should activate)
8545
+
8546
+ ## What to do
8547
+ (step-by-step instructions)
8548
+
8549
+ ## Examples
8550
+ (concrete command examples)
8551
+ `);
8552
+ const tags = String(args.flags.tags || "");
8553
+ return getOrm(env.db.path).then((ds) => skillUpsert(ds, {
8554
+ name,
8555
+ description: desc,
8556
+ body,
8557
+ tags,
8558
+ version: "0.1.0"
8559
+ })).then((r) => {
8560
+ ok(ctx, `skill created: ${name} v0.1.0`);
8561
+ out(ctx, paint(C.dim, ` edit: ygl-trees skill show ${name}`, ctx.color));
8562
+ out(ctx, paint(C.dim, ` add refs: ygl-trees skill refs add ${name} <path> --body "..."`, ctx.color));
8563
+ return 0;
8564
+ }).catch((e) => {
8565
+ bad(ctx, String(e?.message || e));
8566
+ return 1;
8567
+ });
8568
+ }
8569
+ case "refs":
8570
+ case "references": {
8571
+ const skillName = rest[0];
8572
+ if (!skillName) {
8573
+ bad(ctx, "usage: skill refs <name>");
8574
+ return 2;
8575
+ }
8576
+ const sk = store.skillGet(skillName);
8577
+ if (!sk) {
8578
+ bad(ctx, `skill not found: ${skillName}`);
8579
+ return 1;
8580
+ }
8581
+ const refs = env.db.all("SELECT * FROM skill_refs WHERE skill_id=? ORDER BY path", [sk.id]);
8582
+ head(ctx, `Skill: ${skillName}`);
8583
+ if (ctx.json) {
8584
+ console.log(JSON.stringify(refs, null, 2));
8585
+ return 0;
8586
+ }
8587
+ if (!refs.length) {
8588
+ out(ctx, paint(C.dim, " (no references — add with: skill refs add <name> <path> --body ...)", ctx.color));
8589
+ return 0;
8590
+ }
8591
+ for (const r of refs) {
8592
+ out(ctx, ` ${r.path}`);
8593
+ if (r.body) out(ctx, paint(C.dim, ` ${String(r.body).slice(0, 80)}`, ctx.color));
8594
+ }
8595
+ return 0;
8596
+ }
8219
8597
  case "add": {
8220
8598
  const n = args.flags.name ? String(args.flags.name) : "";
8221
8599
  const file = args.flags.f ? String(args.flags.f) : "";
@@ -8560,7 +8938,7 @@ function run$3(env, action, rest, args) {
8560
8938
  "confidence",
8561
8939
  "reuse",
8562
8940
  "updated"
8563
- ]);
8941
+ ], "kb add --title ... --body ... | kb import <dir>");
8564
8942
  return 0;
8565
8943
  case "rm":
8566
8944
  case "remove": {
@@ -10441,7 +10819,49 @@ async function main(argv) {
10441
10819
  out(ctx, appVersion());
10442
10820
  return 0;
10443
10821
  }
10822
+ if (!group || group === "help") {
10823
+ const dbFile = require("node:path").join(process.env.YGL_HOME || require("node:path").join(require("node:os").homedir(), ".ygl-trees"), "ygl-trees.db");
10824
+ if (!require("node:fs").existsSync(dbFile)) {
10825
+ console.log("✓ Welcome to ygl-trees! 🐵");
10826
+ console.log();
10827
+ console.log(" This workspace has no database yet. Get started in 3 steps:");
10828
+ console.log();
10829
+ console.log(" 1. ygl-trees db init # create workspace (9 skills auto-imported)");
10830
+ console.log(" 2. ygl-trees agent brief # see current state + next actions");
10831
+ console.log(" 3. ygl-trees project new my-research -q \"your question\"");
10832
+ console.log();
10833
+ console.log(" More: ygl-trees llms (full capability manifest) | ygl-trees (no args, after init)");
10834
+ console.log();
10835
+ return 0;
10836
+ }
10837
+ }
10444
10838
  if (group === "llms") {
10839
+ const section = String(args.flags.section || "");
10840
+ if (args.flags.json === true) {
10841
+ const manifest = generateLlmsManifest();
10842
+ const sections = {};
10843
+ let current = "header";
10844
+ for (const line of manifest.split("\n")) if (line.startsWith("## ")) {
10845
+ current = line.slice(3).toLowerCase().replace(/\s+/g, "_");
10846
+ sections[current] = line + "\n";
10847
+ } else if (sections[current] !== void 0) sections[current] += line + "\n";
10848
+ console.log(JSON.stringify({
10849
+ version: appVersion(),
10850
+ sections
10851
+ }, null, 2));
10852
+ return 0;
10853
+ }
10854
+ if (section) {
10855
+ const all = generateLlmsManifest().split("\n## ");
10856
+ const match = all.find((s2) => s2.toLowerCase().startsWith(section.toLowerCase()));
10857
+ if (match) {
10858
+ out(ctx, "## " + match);
10859
+ return 0;
10860
+ }
10861
+ bad(ctx, `section not found: ${section}`);
10862
+ out(ctx, ` available: ${all.map((s3) => s3.split("\n")[0]).join(" | ")}`);
10863
+ return 1;
10864
+ }
10445
10865
  out(ctx, generateLlmsManifest());
10446
10866
  return 0;
10447
10867
  }
@@ -10579,7 +10999,72 @@ async function main(argv) {
10579
10999
  }
10580
11000
  const mod = DOMAIN_MODULES[group];
10581
11001
  if (!mod) {
10582
- bad(ctx, `unknown group: ${group}`);
11002
+ const known = [
11003
+ "agent",
11004
+ "research",
11005
+ "produce",
11006
+ "kb",
11007
+ "skill",
11008
+ "tree",
11009
+ "node",
11010
+ "task",
11011
+ "rel",
11012
+ "plugin",
11013
+ "webui",
11014
+ "db",
11015
+ "doctor",
11016
+ "config",
11017
+ "job",
11018
+ "project",
11019
+ "mcp",
11020
+ "llms",
11021
+ "version",
11022
+ "help"
11023
+ ];
11024
+ const aliases = {
11025
+ database: "db",
11026
+ data: "db",
11027
+ sqlite: "db",
11028
+ knowledge: "kb",
11029
+ notes: "kb",
11030
+ notebook: "kb",
11031
+ helth: "doctor",
11032
+ check: "doctor",
11033
+ health: "doctor",
11034
+ server: "webui",
11035
+ dashboard: "webui",
11036
+ ui: "webui",
11037
+ timeline: "db",
11038
+ "time-travel": "db",
11039
+ workspace: "db",
11040
+ obsidian: "db"
11041
+ };
11042
+ const input = String(group).toLowerCase();
11043
+ const suggestions = [];
11044
+ if (aliases[input]) suggestions.push(aliases[input]);
11045
+ for (const [alias, cmd] of Object.entries(aliases)) if (alias.startsWith(input.slice(0, 4)) && !suggestions.includes(cmd)) {
11046
+ suggestions.push(cmd);
11047
+ break;
11048
+ }
11049
+ for (const k of known) if (k.startsWith(input.slice(0, 3)) && !suggestions.includes(k)) suggestions.push(k);
11050
+ if (suggestions.length < 2) for (const k of known) {
11051
+ if (suggestions.includes(k)) continue;
11052
+ let ki = 0;
11053
+ for (const ch of input) {
11054
+ ki = k.indexOf(ch, ki);
11055
+ if (ki === -1) break;
11056
+ ki++;
11057
+ }
11058
+ if (ki !== -1) suggestions.push(k);
11059
+ if (suggestions.length >= 3) break;
11060
+ }
11061
+ if (suggestions.length < 2) {
11062
+ for (const k of known) if (k.includes(input) && !suggestions.includes(k)) suggestions.push(k);
11063
+ }
11064
+ if (suggestions.length === 1) bad(ctx, `unknown group: ${group} — did you mean "${suggestions[0]}"?`);
11065
+ else if (suggestions.length > 1) bad(ctx, `unknown group: ${group} — did you mean: ${suggestions.slice(0, 3).join(" | ")}?`);
11066
+ else bad(ctx, `unknown group: ${group}`);
11067
+ out(ctx, paint(C.dim, " try: ygl-trees (no args) for full usage", ctx.color));
10583
11068
  if (!ctx.json) out(ctx, usage());
10584
11069
  return 2;
10585
11070
  }
@@ -72,20 +72,23 @@ function padEnd(s, width) {
72
72
  return gap > 0 ? s + " ".repeat(gap) : s;
73
73
  }
74
74
  /** Render rows as an aligned table (falls back to JSON when ctx.json). */
75
- function table(ctx, rows, headers, keys) {
75
+ function table(ctx, rows, headers, keys, hint) {
76
76
  if (ctx.json) {
77
77
  console.log(JSON.stringify(rows, null, 2));
78
78
  return;
79
79
  }
80
80
  if (!rows.length) {
81
81
  out(ctx, paint(exports.C.dim, " (empty)", ctx.color));
82
+ if (hint)
83
+ out(ctx, paint(exports.C.dim, ` → ${hint}`, ctx.color));
82
84
  return;
83
85
  }
84
86
  const cols = keys || Object.keys(rows[0]).filter((k) => !k.startsWith("_"));
85
87
  const hs = headers || cols;
86
- const widthOf = (k) => Math.max(...rows.map((r) => dispWidth(String(r[k] ?? ""))));
88
+ const widthOf = (k) => Math.min(40, Math.max(...rows.map((r) => dispWidth(String(r[k] ?? "")))));
87
89
  const w = cols.map((k, i) => Math.max(dispWidth(hs[i] ?? ""), widthOf(k)));
88
- const line = (cells) => cells.map((c, i) => padEnd(String(c), w[i])).join(" ");
90
+ const trunc = (s) => s.length > 40 ? s.slice(0, 39) + "…" : s;
91
+ const line = (cells) => cells.map((c, i) => padEnd(trunc(String(c)), w[i])).join(" ");
89
92
  out(ctx, paint(exports.C.bold, " " + line(hs), ctx.color));
90
93
  out(ctx, paint(exports.C.dim, " " + w.map((n) => "-".repeat(n)).join(" "), ctx.color));
91
94
  for (const r of rows)
@@ -135,7 +135,7 @@ function run(env, action, rest, args) {
135
135
  "confidence",
136
136
  "reuse",
137
137
  "updated",
138
- ]);
138
+ ], "kb add --title ... --body ... | kb import <dir>");
139
139
  return 0;
140
140
  case "rm":
141
141
  case "remove": {
@@ -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) : "";
@@ -10,7 +10,7 @@ function buildBrief(env) {
10
10
  const { store } = env;
11
11
  const cur = store.db.metaGet("current_project");
12
12
  const stats = store.stats();
13
- const skills = store.skillList().slice(0, 5);
13
+ const skills = store.skillList().slice(0, 5).map((sk) => ({ ...sk, description: String(sk.description || "").slice(0, 60) + (String(sk.description || "").length > 60 ? "…" : "") }));
14
14
  const notes = store.kbList().slice(0, 5);
15
15
  const payload = {
16
16
  workspace: env.home,
@@ -74,6 +74,68 @@ 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
+ // F1 fix (a2a review): parent holds .ygllock → child self-deadlocks.
103
+ // Fix: child gets YGL_TEST=1 to skip lock (parent already holds it —
104
+ // no concurrent write risk for read-only routing).
105
+ const cmdParts = [r.group, r.action, ...(r.extra || [])].filter(Boolean);
106
+ const { execSync } = require("node:child_process");
107
+ const self = process.argv[1];
108
+ try {
109
+ const result = execSync(`${JSON.stringify(process.execPath)} ${JSON.stringify(self)} --no-color ${cmdParts.map(c => JSON.stringify(c)).join(" ")}`, { env: { ...process.env, YGL_TEST: "1" }, encoding: "utf8", timeout: 10000 });
110
+ // F2 fix: --json mode outputs only the inner result (contract ④)
111
+ if (ctx.json) {
112
+ console.log(result.trim());
113
+ return 0;
114
+ }
115
+ (0, fmt_1.out)(ctx, result.trim());
116
+ return 0;
117
+ }
118
+ catch (e) {
119
+ if (ctx.json) {
120
+ console.log(JSON.stringify({ error: String(e?.message || e).slice(0, 200) }));
121
+ return 1;
122
+ }
123
+ (0, fmt_1.bad)(ctx, String(e?.stdout || e?.message || e).slice(0, 200));
124
+ return 1;
125
+ }
126
+ }
127
+ }
128
+ // Default: brief
129
+ if (!ctx.json)
130
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " → agent brief (no specific match)", ctx.color));
131
+ const payload = buildBrief(env);
132
+ if (ctx.json) {
133
+ console.log(JSON.stringify(payload, null, 2));
134
+ return 0;
135
+ }
136
+ // Reuse brief rendering
137
+ return run(env, "brief", [], args);
138
+ }
77
139
  case "metrics": {
78
140
  // R11 cold-start research → automated measurement. Health/efficiency
79
141
  // metrics for the workspace — one command answers "how good is this
@@ -150,7 +212,7 @@ function run(env, action, rest, args) {
150
212
  const payload = buildBrief(env);
151
213
  const cur = payload.current_project;
152
214
  const stats = payload.counts;
153
- const skills = store.skillList().slice(0, 5);
215
+ const skills = store.skillList().slice(0, 5).map((sk) => ({ ...sk, description: String(sk.description || "").slice(0, 60) + (String(sk.description || "").length > 60 ? "…" : "") }));
154
216
  if (ctx.json) {
155
217
  console.log(JSON.stringify(payload, null, 2));
156
218
  return 0;
@@ -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,215 @@ 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
+ // F2 fix: --json outputs pure JSON (frozen contract ④)
194
+ if (ctx.json) {
195
+ console.log(JSON.stringify({ timeline: days, total: days.reduce((s2, d) => s2 + d.events, 0) }, null, 2));
196
+ return 0;
197
+ }
198
+ const maxEvents = Math.max(...days.map(d => d.events));
199
+ (0, fmt_1.head)(ctx, "Workspace Timeline");
200
+ let totalP = 0, totalS = 0, totalC = 0, totalK = 0, totalN = 0;
201
+ for (const d of days) {
202
+ totalP += d.projects;
203
+ totalS += d.sources;
204
+ totalC += d.claims;
205
+ totalK += d.kb;
206
+ totalN += d.nodes;
207
+ const barLen = Math.round(d.events / maxEvents * 30);
208
+ const bar = "█".repeat(barLen);
209
+ const milestones = [];
210
+ if (d.projects > 0)
211
+ milestones.push(`+${d.projects}项目`);
212
+ if (d.sources > 0)
213
+ milestones.push(`+${d.sources}来源`);
214
+ if (d.claims > 0)
215
+ milestones.push(`+${d.claims}断言`);
216
+ if (d.kb > 0)
217
+ milestones.push(`+${d.kb}知识`);
218
+ if (d.nodes > 0)
219
+ milestones.push(`+${d.nodes}节点`);
220
+ (0, fmt_1.out)(ctx, ` ${d.day} ${bar.padEnd(32)} ${String(d.events).padStart(5)} events ${milestones.join(" ")}`);
221
+ }
222
+ (0, fmt_1.out)(ctx, "");
223
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` 累计: ${totalP}项目 · ${totalS}来源 · ${totalC}断言 · ${totalK}知识 · ${totalN}节点变更`, ctx.color));
224
+ return 0;
225
+ }
226
+ case "workspace": {
227
+ // v2: multi-workspace management — each workspace is a YGL_HOME directory
228
+ const sub = rest[0] || "list";
229
+ const wsRoot = path.join(env.home, ".."); // parent of current workspace
230
+ if (sub === "list") {
231
+ const dirs = fs.existsSync(wsRoot) ? fs.readdirSync(wsRoot).filter((d) => fs.existsSync(path.join(wsRoot, d, "ygl-trees.db"))) : [];
232
+ const cur = path.basename(env.home);
233
+ (0, fmt_1.head)(ctx, "Workspaces (parent: " + wsRoot + ")");
234
+ for (const d of dirs) {
235
+ (0, fmt_1.out)(ctx, (d === cur ? " * " : " ") + d);
236
+ }
237
+ (0, fmt_1.out)(ctx, "");
238
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, " * = current | switch: YGL_HOME=<path> ygl-trees <cmd>", ctx.color));
239
+ return 0;
240
+ }
241
+ if (sub === "create") {
242
+ const name = rest[1];
243
+ if (!name) {
244
+ (0, fmt_1.bad)(ctx, "usage: db workspace create <name>");
245
+ return 2;
246
+ }
247
+ const dir = path.join(wsRoot, name);
248
+ if (fs.existsSync(path.join(dir, "ygl-trees.db"))) {
249
+ (0, fmt_1.ok)(ctx, `workspace already exists: ${name}`);
250
+ return 0;
251
+ }
252
+ fs.mkdirSync(dir, { recursive: true });
253
+ (0, fmt_1.ok)(ctx, `workspace created: ${dir}`);
254
+ (0, fmt_1.out)(ctx, (0, fmt_1.paint)(fmt_1.C.dim, ` init: YGL_HOME=${dir} ygl-trees db init`, ctx.color));
255
+ return 0;
256
+ }
257
+ (0, fmt_1.bad)(ctx, "usage: db workspace list | create <name>");
258
+ return 2;
259
+ }
260
+ case "obsidian": {
261
+ // v2 feature: export workspace as Obsidian-compatible markdown vault
262
+ const outDir = rest[0] || path.join(env.home, "obsidian-export");
263
+ fs.mkdirSync(path.join(outDir, "projects"), { recursive: true });
264
+ fs.mkdirSync(path.join(outDir, "knowledge"), { recursive: true });
265
+ fs.mkdirSync(path.join(outDir, "skills"), { recursive: true });
266
+ fs.mkdirSync(path.join(outDir, "trees"), { recursive: true });
267
+ fs.mkdirSync(path.join(outDir, "claims"), { recursive: true });
268
+ let count = 0;
269
+ const safe = (t) => String(t).replace(/[\/:*?"<>|]/g, "_").slice(0, 50);
270
+ for (const p of db.all("SELECT * FROM projects")) {
271
+ fs.writeFileSync(path.join(outDir, "projects", safe(p.name) + ".md"), `---
272
+ type: project
273
+ id: ${p.id}
274
+ ---
275
+
276
+ # ${p.name}
277
+
278
+ ${p.question || ""}
279
+ `);
280
+ count++;
281
+ }
282
+ for (const n of db.all("SELECT * FROM kb_notes")) {
283
+ fs.writeFileSync(path.join(outDir, "knowledge", safe(n.title) + ".md"), `---
284
+ type: knowledge
285
+ tags: ${n.tags || ""}
286
+ confidence: ${n.confidence}
287
+ ---
288
+
289
+ # ${n.title}
290
+
291
+ ${n.body}
292
+ `);
293
+ count++;
294
+ }
295
+ for (const sk of db.all("SELECT * FROM skills WHERE enabled=1")) {
296
+ fs.writeFileSync(path.join(outDir, "skills", safe(sk.name) + ".md"), `---
297
+ type: skill
298
+ version: ${sk.version}
299
+ ---
300
+
301
+ # ${sk.name}
302
+
303
+ ${String(sk.body).slice(0, 500)}
304
+ `);
305
+ count++;
306
+ }
307
+ for (const t of db.all("SELECT * FROM trees")) {
308
+ const nodes = db.all("SELECT * FROM nodes WHERE tree_id=?", [t.id]);
309
+ let c = `---
310
+ type: tree
311
+ ---
312
+
313
+ # ${t.name} (${nodes.length} nodes)
314
+
315
+ `;
316
+ for (const n of nodes)
317
+ c += `- **${n.key}** (${n.kind}, tier ${n.tier})
318
+ `;
319
+ fs.writeFileSync(path.join(outDir, "trees", safe(t.name) + ".md"), c);
320
+ count++;
321
+ }
322
+ for (const cl of db.all("SELECT * FROM claims")) {
323
+ 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]);
324
+ let c2 = `---
325
+ type: claim
326
+ status: ${cl.status}
327
+ confidence: ${cl.confidence}
328
+ ---
329
+
330
+ # ${cl.id}
331
+
332
+ ${cl.text}
333
+
334
+ ## Evidence
335
+ `;
336
+ for (const l of links)
337
+ c2 += `- > ${String(l.quote).slice(0, 100)}
338
+ `;
339
+ fs.writeFileSync(path.join(outDir, "claims", cl.id + ".md"), c2);
340
+ count++;
341
+ }
342
+ (0, fmt_1.ok)(ctx, `obsidian vault exported: ${count} files → ${outDir}`);
343
+ return 0;
344
+ }
135
345
  case "export": {
136
346
  const dest = rest[0] || `ygl-trees-dump-${Date.now()}.sql`;
137
347
  // --lean: skip derived-table data (node_closure / node_index are
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # ygl-trees v0.69.0 — AI Agent Research Workbench
1
+ # ygl-trees v0.73.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.71.0",
3
+ "version": "0.73.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,12 +21,12 @@
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/cosmokit": "~1.8.6-alpha.2",
29
28
  "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
29
+ "@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
30
30
  "@deepseek-ai/schemastery": "~3.18.5-alpha.2"
31
31
  },
32
32
  "devDependencies": {