ygl-trees 0.76.0 → 0.77.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.
@@ -0,0 +1,172 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-C96Ew50Q.cjs");
2
+ //#region packages/core/text/src/text.ts
3
+ var text_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
4
+ clip: () => clip,
5
+ contentHash: () => contentHash,
6
+ ngram: () => ngram,
7
+ nowIso: () => nowIso,
8
+ parseFrontmatter: () => parseFrontmatter,
9
+ sha1: () => sha1,
10
+ shortId: () => shortId,
11
+ slug: () => slug,
12
+ splitTags: () => splitTags,
13
+ stampId: () => stampId
14
+ });
15
+ function nowIso() {
16
+ const d = /* @__PURE__ */ new Date();
17
+ const p = (n) => String(n).padStart(2, "0");
18
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
19
+ }
20
+ function stampId() {
21
+ const d = /* @__PURE__ */ new Date();
22
+ const p = (n) => String(n).padStart(2, "0");
23
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
24
+ }
25
+ function slug(text, limit = 40) {
26
+ const s = (text || "").trim().toLowerCase().replace(new RegExp(`[^\\w${CJK}]+`, "gu"), "-").replace(/^-+|-+$/g, "");
27
+ if (s.length <= limit) return s || "item";
28
+ return s.slice(0, limit).replace(/-+$/, "") || "item";
29
+ }
30
+ /**
31
+ * Tokenize mixed Chinese/English text for SQLite FTS5.
32
+ *
33
+ * FTS5's default unicode61 tokenizer treats a whole CJK sentence as one token,
34
+ * which makes Chinese search degrade to exact match. We expand CJK runs into
35
+ * character bigrams and keep latin words intact.
36
+ */
37
+ function ngram(text) {
38
+ const src = (text || "").normalize("NFKC").toLowerCase();
39
+ const out = [];
40
+ const segs = src.match(CJK_RUN_RE) || [];
41
+ for (const seg of segs) if (CJK_ONLY_RE.test(seg)) {
42
+ if (seg.length === 1) out.push(seg);
43
+ else for (let i = 0; i < seg.length - 1; i++) out.push(seg.slice(i, i + 2));
44
+ } else {
45
+ const w = seg.replace(/^[^\w]+|[^\w]+$/gu, "");
46
+ if (w) out.push(w);
47
+ }
48
+ return out.join(" ");
49
+ }
50
+ function sha1(text) {
51
+ return require("node:crypto").createHash("sha1").update(text || "").digest("hex");
52
+ }
53
+ /** Content hash for idempotent writes (single source: store writes and the
54
+ * TypeORM write service must produce identical hashes); --force salts it
55
+ * so a duplicate row is explicitly distinct (queryable via `db dups`). */
56
+ function contentHash(content, force) {
57
+ return sha1(content) + (force ? `#f${Date.now()}${Math.random()}` : "");
58
+ }
59
+ function shortId(parts, n = 8) {
60
+ return sha1(parts.join("|") + Date.now() + Math.random()).slice(0, n);
61
+ }
62
+ /** Truncate for table display, collapsing newlines. */
63
+ function clip(text, n = 40) {
64
+ const one = (text || "").replace(/\s+/g, " ").trim();
65
+ return one.length <= n ? one : one.slice(0, Math.max(1, n - 1)) + "…";
66
+ }
67
+ /**
68
+ * Minimal SKILL.md frontmatter parser: supports `key: value`, quoted scalars,
69
+ * folded blocks (`>-` / `>`) and inline `a,b` lists.
70
+ */
71
+ function parseFrontmatter(raw) {
72
+ const text = raw.replace(/^/, "");
73
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
74
+ if (!m) return {
75
+ meta: {},
76
+ body: text
77
+ };
78
+ const meta = {};
79
+ const lines = m[1].split(/\r?\n/);
80
+ let key = null;
81
+ let folded = null;
82
+ const flush = () => {
83
+ if (key && folded) meta[key] = folded.join(" ").replace(/\s+/g, " ").trim();
84
+ key = null;
85
+ folded = null;
86
+ };
87
+ for (const line of lines) {
88
+ const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line);
89
+ if (kv && !/^\s/.test(line)) {
90
+ flush();
91
+ key = kv[1];
92
+ const v = kv[2].trim();
93
+ if (v === ">-" || v === ">" || v === "|" || v === "|-") folded = [];
94
+ else {
95
+ meta[key] = v.replace(/^["']|["']$/g, "");
96
+ key = null;
97
+ }
98
+ continue;
99
+ }
100
+ if (key && folded) folded.push(line.trim());
101
+ }
102
+ flush();
103
+ return {
104
+ meta,
105
+ body: text.slice(m[0].length)
106
+ };
107
+ }
108
+ function splitTags(v) {
109
+ return (v || "").split(/[,,;;\s]+/).map((s) => s.trim()).filter(Boolean);
110
+ }
111
+ var CJK, CJK_RUN_RE, CJK_ONLY_RE;
112
+ var init_text = require_rolldown_runtime.__esmMin((() => {
113
+ CJK = "\\u4e00-\\u9fff\\u3400-\\u4dbf\\uf900-\\ufaff";
114
+ new RegExp(`[${CJK}]`, "u");
115
+ CJK_RUN_RE = new RegExp(`[${CJK}]+|[^\\s${CJK}]+`, "gu");
116
+ CJK_ONLY_RE = new RegExp(`^[${CJK}]+$`, "u");
117
+ }));
118
+ //#endregion
119
+ Object.defineProperty(exports, "clip", {
120
+ enumerable: true,
121
+ get: function() {
122
+ return clip;
123
+ }
124
+ });
125
+ Object.defineProperty(exports, "contentHash", {
126
+ enumerable: true,
127
+ get: function() {
128
+ return contentHash;
129
+ }
130
+ });
131
+ Object.defineProperty(exports, "init_text", {
132
+ enumerable: true,
133
+ get: function() {
134
+ return init_text;
135
+ }
136
+ });
137
+ Object.defineProperty(exports, "ngram", {
138
+ enumerable: true,
139
+ get: function() {
140
+ return ngram;
141
+ }
142
+ });
143
+ Object.defineProperty(exports, "nowIso", {
144
+ enumerable: true,
145
+ get: function() {
146
+ return nowIso;
147
+ }
148
+ });
149
+ Object.defineProperty(exports, "parseFrontmatter", {
150
+ enumerable: true,
151
+ get: function() {
152
+ return parseFrontmatter;
153
+ }
154
+ });
155
+ Object.defineProperty(exports, "sha1", {
156
+ enumerable: true,
157
+ get: function() {
158
+ return sha1;
159
+ }
160
+ });
161
+ Object.defineProperty(exports, "shortId", {
162
+ enumerable: true,
163
+ get: function() {
164
+ return shortId;
165
+ }
166
+ });
167
+ Object.defineProperty(exports, "text_exports", {
168
+ enumerable: true,
169
+ get: function() {
170
+ return text_exports;
171
+ }
172
+ });
@@ -49,4 +49,36 @@ class DatabaseService extends cordis_1.Service {
49
49
  exports.DatabaseService = DatabaseService;
50
50
  function apply(ctx, config) {
51
51
  new DatabaseService(ctx, config.file);
52
+ // 0.76 bridge: eventbus (internal) → ctx.emit (Cordis public API).
53
+ // This makes "ctx.on()/emit()" work as documented — plugins can listen
54
+ // with ctx.on("ygl", handler) or ctx.on("ygl:node.unlock", handler).
55
+ // The eventbus remains the internal write-path transport.
56
+ try {
57
+ const { onEvent, onEventKind } = require("../../../core/text/src/eventbus");
58
+ const unsub = onEvent((evt) => { try {
59
+ ctx.emit("ygl", evt);
60
+ }
61
+ catch { /* */ } });
62
+ // Also bridge kind-specific channels
63
+ const kindSubs = [unsub];
64
+ const knownKinds = ["node.unlock", "node.lock", "node.add", "node.link",
65
+ "kb.add", "kb.rm", "claim.resolve", "claim.add", "task.status",
66
+ "project.new", "skill.add"];
67
+ for (const kind of knownKinds) {
68
+ kindSubs.push(onEventKind(kind, (evt) => { try {
69
+ ctx.emit(`ygl:${kind}`, evt);
70
+ }
71
+ catch { /* */ } }));
72
+ }
73
+ // Cleanup on Context dispose (best-effort — this fork has no ctx.dispose)
74
+ ctx.fiber?.collect?.(() => {
75
+ for (const u of kindSubs) {
76
+ try {
77
+ u();
78
+ }
79
+ catch { /* */ }
80
+ }
81
+ });
82
+ }
83
+ catch { /* eventbus optional */ }
52
84
  }
@@ -187,13 +187,46 @@ function run(env, action, rest, args) {
187
187
  ]);
188
188
  return 0;
189
189
  case "link": {
190
- // One-command symlink: skills/ → .agents/skills/ (or ~/.agents/skills/ with --global)
191
- // so host agents (DSH, Claude, etc.) can discover them without manual junction setup.
190
+ // One-command symlink: skills/ → AI agent's skill directory.
191
+ // Supported targets:
192
+ // (default) ./.agents/skills/ (project-level, DSH/Claude)
193
+ // --global ~/.agents/skills/ (user-level, DSH/Claude)
194
+ // --ai pi ~/.pi/agent/skills/ (Pi agent)
195
+ // --ai claude ~/.claude/skills/ (Claude Code)
196
+ // --ai cursor ~/.cursor/skills/ (Cursor IDE)
197
+ // --ai custom --to <path> (any directory)
192
198
  const os = require("node:os");
193
199
  const globalFlag = args?.flags?.global === true;
194
- const targetDir = globalFlag
195
- ? path.join(os.homedir(), ".agents", "skills")
196
- : path.join(process.cwd(), ".agents", "skills");
200
+ const aiName = String(args?.flags?.ai || "").toLowerCase();
201
+ const customTo = String(args?.flags?.to || "");
202
+ // AI agent skill directory registry
203
+ const AI_SKILL_DIRS = {
204
+ pi: () => path.join(os.homedir(), ".pi", "agent", "skills"),
205
+ claude: () => path.join(os.homedir(), ".claude", "skills"),
206
+ cursor: () => path.join(os.homedir(), ".cursor", "skills"),
207
+ windsurf: () => path.join(os.homedir(), ".windsurf", "skills"),
208
+ };
209
+ let targetDir;
210
+ if (customTo) {
211
+ // --ai custom --to <path>
212
+ targetDir = path.resolve(customTo);
213
+ }
214
+ else if (aiName && AI_SKILL_DIRS[aiName]) {
215
+ // --ai <known-agent>
216
+ targetDir = AI_SKILL_DIRS[aiName]();
217
+ }
218
+ else if (aiName && aiName !== "custom") {
219
+ // --ai <unknown> → error with suggestions
220
+ (0, fmt_1.bad)(ctx, `unknown AI agent: ${aiName}`);
221
+ (0, fmt_1.out)(ctx, ` supported: ${Object.keys(AI_SKILL_DIRS).join(", ")} | custom (with --to <path>)`);
222
+ return 2;
223
+ }
224
+ else {
225
+ // default or --global
226
+ targetDir = globalFlag
227
+ ? path.join(os.homedir(), ".agents", "skills")
228
+ : path.join(process.cwd(), ".agents", "skills");
229
+ }
197
230
  // Find source skills directory (probe upward from cwd, same as repoRoot)
198
231
  let skillsDir = null;
199
232
  for (let dir = process.cwd(), i = 0; i < 6; i++, dir = path.dirname(dir)) {
@@ -162,9 +162,16 @@ function pnpm(home, args) {
162
162
  // there. Requires pnpm >= 9.4.
163
163
  const pm = process.env.YGL_PACKAGE_MANAGER || "pnpm";
164
164
  if (process.platform === "win32") {
165
- const q = (s) => `"${String(s).replace(/["&|<>^]/g, "")}"`;
166
165
  const exe = resolvePm(pm);
167
- (0, node_child_process_1.execFileSync)([q(exe), ...args.map(q), "--dir", q(profileDir(home)), "--ignore-workspace"].join(" "), { stdio: "pipe", shell: true });
166
+ // DEP0190 fix (a2a 7aa5c871 cosmetic): avoid shell:true with args —
167
+ // invoke cmd explicitly instead, args are properly escaped by spawnSync.
168
+ const { spawnSync } = require("node:child_process");
169
+ const r = spawnSync("cmd", ["/c", exe, ...args, "--dir", profileDir(home), "--ignore-workspace"], { stdio: "pipe" });
170
+ if (r.status !== 0) {
171
+ const err = new Error(String(r.stderr || r.stdout || `pnpm exited ${r.status}`));
172
+ err.status = r.status;
173
+ throw err;
174
+ }
168
175
  return;
169
176
  }
170
177
  (0, node_child_process_1.execFileSync)(pm, [...args, "--dir", profileDir(home), "--ignore-workspace"], { stdio: "pipe" });
@@ -194,6 +201,28 @@ const q2 = (s) => `"${String(s).replace(/["&|<>^]/g, "")}"`;
194
201
  async function installPlugin(home, spec) {
195
202
  initProfile(home);
196
203
  const abs = path.resolve(spec);
204
+ // P0 fix (a2a 7aa5c871): validate local paths BEFORE recording anything —
205
+ // a bad path previously degraded to basename "plugin" and bricked the home.
206
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
207
+ // P2 fix: read the real package name, never trust basename
208
+ const pkgPath = path.join(abs, "package.json");
209
+ if (!fs.existsSync(pkgPath)) {
210
+ throw new Error(`local plugin directory has no package.json: ${abs}`);
211
+ }
212
+ try {
213
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
214
+ if (!pkg.name || typeof pkg.name !== "string") {
215
+ throw new Error(`package.json missing "name" field: ${abs}`);
216
+ }
217
+ }
218
+ catch (e) {
219
+ throw new Error(`invalid package.json in ${abs}: ${e?.message || e}`);
220
+ }
221
+ }
222
+ else if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("\\") || /^[A-Z]:/.test(spec)) {
223
+ // Looks like a path but doesn't exist — reject before recording
224
+ throw new Error(`plugin path not found: ${abs}`);
225
+ }
197
226
  const installSpec = fs.existsSync(abs) && fs.statSync(abs).isDirectory() ? `link:${abs}` : spec;
198
227
  const existing = readUserBundle(home).find((p) => p.spec === installSpec || p.spec === spec);
199
228
  if (existing)
@@ -93,3 +93,22 @@ repo-root/
93
93
  | `plugin list` 显示 ✗ 不可解析 | `plugin remove <name>` 后重装;或 `doctor` 看 plugin_profile 项 |
94
94
  | 启动报 "N plugin issue(s) during startup" | 读终端给出的 report 路径(`$YGL_HOME/logs/startup-*.log`) |
95
95
  | 装了命令组不生效 | 命令组在**下次进程启动**时注册(CLI 每命令独立进程,自然生效) |
96
+
97
+
98
+ ## 事件监听(0.76+)
99
+
100
+ 插件可以通过 Cordis 标准 `ctx.on` 监听跨域事件:
101
+
102
+ ```typescript
103
+ // 监听全部事件
104
+ ctx.on("ygl", (evt) => {
105
+ console.log(evt.kind, evt.entityId); // e.g. "node.unlock" "sn123"
106
+ });
107
+
108
+ // 监听特定类型
109
+ ctx.on("ygl:node.unlock", (evt) => { /* ... */ });
110
+ ctx.on("ygl:kb.add", (evt) => { /* ... */ });
111
+ ctx.on("ygl:claim.resolve", (evt) => { /* ... */ });
112
+ ```
113
+
114
+ 事件在 logEvent 落表后即时发出(事件驱动 SSE 同通道)。支持的事件类型见 `packages/core/text/src/eventbus.ts`。
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # ygl-trees v0.74.0 — AI Agent Research Workbench
1
+ # ygl-trees v0.76.1 — 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.76.0",
3
+ "version": "0.77.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",
@@ -25,10 +25,10 @@
25
25
  "node-sqlite3-wasm": "^0.8.61",
26
26
  "reflect-metadata": "^0.2.2",
27
27
  "typeorm": "^1.1.2",
28
- "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
29
28
  "@deepseek-ai/cordis": "~4.0.5-alpha.2",
30
- "@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
31
- "@deepseek-ai/schemastery": "~3.18.5-alpha.2"
29
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.2",
30
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
31
+ "@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",