ygl-trees 0.75.1 → 0.76.1
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 +11 -0
- package/README.md +1 -1
- package/dist/apps/cli/src/bin.js +15 -4
- package/dist/bundle/bin.cjs +1213 -1072
- package/dist/packages/core/database/src/plugin.js +54 -0
- package/dist/packages/core/text/src/eventbus.js +35 -0
- package/dist/packages/knowledge/skills/src/skill-cmds.js +38 -5
- package/dist/packages/ops/tools/src/webui.js +27 -1
- package/dist/packages/research/pipeline/src/writes.js +6 -0
- package/dist/packages/techtree/model/src/writes.js +19 -13
- package/docs/plugin-authoring.md +19 -0
- package/llms.txt +1 -1
- package/package.json +4 -4
|
@@ -12,6 +12,7 @@ const cordis_1 = require("@deepseek-ai/cordis");
|
|
|
12
12
|
const db_1 = require("./db");
|
|
13
13
|
class DatabaseService extends cordis_1.Service {
|
|
14
14
|
db;
|
|
15
|
+
disposed = false;
|
|
15
16
|
constructor(ctx, dbFile) {
|
|
16
17
|
super(ctx, "database");
|
|
17
18
|
// GPT-6.1 SOL fix: the DB lock was only applied in the profile-boot
|
|
@@ -23,8 +24,61 @@ class DatabaseService extends cordis_1.Service {
|
|
|
23
24
|
const lock = process.env.YGL_TEST !== "1" && process.env.YGL_SERVE !== "1";
|
|
24
25
|
this.db = new db_1.DB(dbFile, { lock });
|
|
25
26
|
}
|
|
27
|
+
/** Lifecycle: Cordis calls this when the Context is disposed — the
|
|
28
|
+
* "Services stop with Context, resources auto-clean" promise made real.
|
|
29
|
+
* Closes the SQLite handle AND the cached ORM DataSource. */
|
|
30
|
+
stop() {
|
|
31
|
+
if (this.disposed)
|
|
32
|
+
return;
|
|
33
|
+
this.disposed = true;
|
|
34
|
+
try {
|
|
35
|
+
// Close the cached ORM DataSource if it was opened on this file
|
|
36
|
+
const { closeOrm } = require("./orm");
|
|
37
|
+
// closeOrm is async but stop() is sync in Cordis — fire-and-forget is
|
|
38
|
+
// safe here because db.close() below releases the file handle the ORM
|
|
39
|
+
// depends on; the ORM will see a closed connection and GC naturally.
|
|
40
|
+
closeOrm().catch(() => { });
|
|
41
|
+
}
|
|
42
|
+
catch { /* orm module not loaded — fine */ }
|
|
43
|
+
try {
|
|
44
|
+
this.db.close(); // releases .ygllock + closes SQLite handle
|
|
45
|
+
}
|
|
46
|
+
catch { /* already closed */ }
|
|
47
|
+
}
|
|
26
48
|
}
|
|
27
49
|
exports.DatabaseService = DatabaseService;
|
|
28
50
|
function apply(ctx, config) {
|
|
29
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 */ }
|
|
30
84
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitEvent = emitEvent;
|
|
4
|
+
exports.onEvent = onEvent;
|
|
5
|
+
exports.onEventKind = onEventKind;
|
|
6
|
+
/**
|
|
7
|
+
* Cross-domain event bus — connects logEvent writes to real-time consumers.
|
|
8
|
+
*
|
|
9
|
+
* Architecture: a process-wide EventEmitter singleton. Write services call
|
|
10
|
+
* `emitEvent()` after logEvent persists to the events table; consumers
|
|
11
|
+
* (webui SSE, user plugins, future watchers) subscribe with `onEvent()`.
|
|
12
|
+
*
|
|
13
|
+
* This makes "ctx.on reactivity" real without requiring a full Cordis
|
|
14
|
+
* event system — a lightweight bridge that the webui and plugins can use.
|
|
15
|
+
*
|
|
16
|
+
* @module @ygl/core-text/eventbus
|
|
17
|
+
*/
|
|
18
|
+
const node_events_1 = require("node:events");
|
|
19
|
+
const bus = new node_events_1.EventEmitter();
|
|
20
|
+
bus.setMaxListeners(50); // SSE connections + plugins
|
|
21
|
+
/** Publish a cross-domain event (called after events table write). */
|
|
22
|
+
function emitEvent(evt) {
|
|
23
|
+
bus.emit("ygl", evt);
|
|
24
|
+
bus.emit(`ygl:${evt.kind}`, evt); // specific subscription channel
|
|
25
|
+
}
|
|
26
|
+
/** Subscribe to all events. Returns an unsubscribe function. */
|
|
27
|
+
function onEvent(handler) {
|
|
28
|
+
bus.on("ygl", handler);
|
|
29
|
+
return () => bus.off("ygl", handler);
|
|
30
|
+
}
|
|
31
|
+
/** Subscribe to a specific kind (e.g. "node.unlock"). Returns unsubscribe. */
|
|
32
|
+
function onEventKind(kind, handler) {
|
|
33
|
+
bus.on(`ygl:${kind}`, handler);
|
|
34
|
+
return () => bus.off(`ygl:${kind}`, handler);
|
|
35
|
+
}
|
|
@@ -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/ →
|
|
191
|
-
//
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
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)) {
|
|
@@ -852,6 +852,23 @@ function createDashboardServer(env) {
|
|
|
852
852
|
if (typeof h.unref === "function")
|
|
853
853
|
h.unref();
|
|
854
854
|
}
|
|
855
|
+
// ── 0.76 Event-driven SSE: subscribe to the cross-domain event bus.
|
|
856
|
+
// Writes (node unlock, claim resolve, kb add, etc.) now emit instantly —
|
|
857
|
+
// SSE pushes fresh data on write instead of waiting for the 1.5s tick.
|
|
858
|
+
// The fingerprint tick above stays as a fallback/degradation path.
|
|
859
|
+
let unsubBus = null;
|
|
860
|
+
try {
|
|
861
|
+
const { onEvent } = require("../../../core/text/src/eventbus");
|
|
862
|
+
unsubBus = onEvent((evt) => {
|
|
863
|
+
if (!sseClients.size)
|
|
864
|
+
return;
|
|
865
|
+
// Event received — push immediately (no polling delay)
|
|
866
|
+
push("data", gatherData(env));
|
|
867
|
+
// Also push the event itself as a named SSE event for fine-grained consumers
|
|
868
|
+
push(evt.kind.replace(/\//g, "-"), evt);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
catch { /* eventbus optional — fingerprint tick still covers */ }
|
|
855
872
|
const server = http.createServer((req, res) => {
|
|
856
873
|
const u = new URL(req.url || "/", "http://127.0.0.1");
|
|
857
874
|
const url = u.pathname;
|
|
@@ -962,7 +979,16 @@ function createDashboardServer(env) {
|
|
|
962
979
|
sseClients.clear();
|
|
963
980
|
};
|
|
964
981
|
server.on("close", dropLive);
|
|
965
|
-
server.closeLive =
|
|
982
|
+
server.closeLive = () => {
|
|
983
|
+
dropLive();
|
|
984
|
+
if (unsubBus) {
|
|
985
|
+
try {
|
|
986
|
+
unsubBus();
|
|
987
|
+
}
|
|
988
|
+
catch { /* */ }
|
|
989
|
+
unsubBus = null;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
966
992
|
return server;
|
|
967
993
|
}
|
|
968
994
|
/** Live dashboard: blocks until SIGINT/SIGTERM, then resolves so the CLI can
|
|
@@ -40,6 +40,12 @@ async function logEvent(mgr, kind, entity, entityId, payload, level = "info") {
|
|
|
40
40
|
payload: p,
|
|
41
41
|
level,
|
|
42
42
|
});
|
|
43
|
+
// 0.76: cross-domain event bus — notify real-time consumers (SSE/plugins)
|
|
44
|
+
try {
|
|
45
|
+
const { emitEvent } = require("../../../core/text/src/eventbus");
|
|
46
|
+
emitEvent({ kind, entity, entityId, payload: String(payload), ts: new Date().toISOString() });
|
|
47
|
+
}
|
|
48
|
+
catch { /* eventbus optional — never block the write path */ }
|
|
43
49
|
}
|
|
44
50
|
// ================================================================ queries
|
|
45
51
|
async function queryAdd(ds, projectId, text, engine = "", force = false) {
|
|
@@ -26,6 +26,12 @@ async function logEvent(mgr, kind, entityId, payload, level = "info") {
|
|
|
26
26
|
const run = process.env.YGL_RUN_ID || "";
|
|
27
27
|
const body = run ? JSON.stringify({ ...payload, run }) : JSON.stringify(payload ?? {});
|
|
28
28
|
await mgr.query(`INSERT INTO events(id,ts,actor,kind,entity,entity_id,payload,level) VALUES(?,?,?,?,?,?,?,?)`, [(0, text_1.shortId)([kind, entityId], 12), (0, text_1.nowIso)(), "cli", kind, "node", entityId, body, level]);
|
|
29
|
+
// 0.76: cross-domain event bus — notify real-time consumers (SSE/plugins)
|
|
30
|
+
try {
|
|
31
|
+
const { emitEvent } = require("../../../core/text/src/eventbus");
|
|
32
|
+
emitEvent({ kind, entity: "node", entityId, payload: String(body), ts: new Date().toISOString() });
|
|
33
|
+
}
|
|
34
|
+
catch { /* eventbus optional — never block the write path */ }
|
|
29
35
|
}
|
|
30
36
|
async function reaches(mgr, treeId, fromId, toId) {
|
|
31
37
|
const rows = (await mgr.query(`SELECT 1 AS hit FROM node_closure WHERE tree_id=? AND ancestor=? AND descendant=? AND depth>0`, [treeId, fromId, toId]));
|
|
@@ -134,8 +140,8 @@ async function buildIndex(mgr, treeId) {
|
|
|
134
140
|
inDegree[n.key] = (requiredParents.get(n.id) || []).length;
|
|
135
141
|
for (const n of nodes)
|
|
136
142
|
await mgr.query(`UPDATE nodes SET tier=? WHERE id=? AND tier<>?`, [tierOf.get(n.id) ?? 0, n.id, tierOf.get(n.id) ?? 0]);
|
|
137
|
-
await mgr.query(`INSERT INTO node_index(tree_id,topo_order,tiers,in_degree,built_at,dirty) VALUES(?,?,?,?,?,0)
|
|
138
|
-
ON CONFLICT(tree_id) DO UPDATE SET topo_order=excluded.topo_order, tiers=excluded.tiers,
|
|
143
|
+
await mgr.query(`INSERT INTO node_index(tree_id,topo_order,tiers,in_degree,built_at,dirty) VALUES(?,?,?,?,?,0)
|
|
144
|
+
ON CONFLICT(tree_id) DO UPDATE SET topo_order=excluded.topo_order, tiers=excluded.tiers,
|
|
139
145
|
in_degree=excluded.in_degree, built_at=excluded.built_at, dirty=0`, [treeId, JSON.stringify(topo), JSON.stringify(tiers), JSON.stringify(inDegree), (0, text_1.nowIso)()]);
|
|
140
146
|
return { topo, tiers, inDegree };
|
|
141
147
|
}
|
|
@@ -222,10 +228,10 @@ async function link(ds, treeId, parentKey, childKey, o = {}) {
|
|
|
222
228
|
const before = (await mgr.query(`SELECT id FROM node_edges WHERE parent_id=? AND child_id=? AND rel=?`, [p[0].id, c[0].id, rel]));
|
|
223
229
|
const duplicate = before.length > 0;
|
|
224
230
|
const id = duplicate ? before[0].id : (0, text_1.shortId)(["edge", p[0].id, c[0].id, rel], 12);
|
|
225
|
-
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
226
|
-
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
227
|
-
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
228
|
-
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
231
|
+
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
232
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
233
|
+
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
234
|
+
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
229
235
|
weight=excluded.weight, note=excluded.note`, [id, treeId, p[0].id, c[0].id, rel, mode, o.required === false ? 0 : 1, o.min_level ?? 1, o.weight ?? 1, o.note || "", (0, text_1.nowIso)()]);
|
|
230
236
|
await rebuildClosure(mgr, treeId);
|
|
231
237
|
await logEvent(mgr, "node.link", id, { tree: treeId, parent: parentKey, child: childKey, rel, mode, required: o.required !== false, duplicate });
|
|
@@ -256,7 +262,7 @@ async function condAdd(ds, treeId, keyOrId, o) {
|
|
|
256
262
|
if (!db_1.COND_KINDS.includes(kind))
|
|
257
263
|
throw new Error(`kind must be one of ${db_1.COND_KINDS.join("|")}`);
|
|
258
264
|
const id = (0, text_1.shortId)(["cond", n[0].id, o.name], 12);
|
|
259
|
-
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
265
|
+
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
260
266
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)`, [id, n[0].id, o.name, kind, o.expr || o.name, o.op || ">=", o.value || "", o.required === false ? 0 : 1, o.effect || "gate", o.weight ?? 1, o.note || ""]);
|
|
261
267
|
await markDirty(mgr, treeId);
|
|
262
268
|
await logEvent(mgr, "node.cond", id, { tree: treeId, node: keyOrId, name: o.name, kind, op: o.op || ">=", required: o.required !== false });
|
|
@@ -269,7 +275,7 @@ async function stateSet(ds, treeId, keyOrId, status, context = "default", level
|
|
|
269
275
|
const n = (await mgr.query(`SELECT id FROM nodes WHERE tree_id=? AND (key=? OR id=?)`, [treeId, keyOrId, keyOrId]));
|
|
270
276
|
if (!n.length)
|
|
271
277
|
throw new Error(`node not found: ${keyOrId}`);
|
|
272
|
-
await mgr.query(`INSERT INTO node_state(tree_id,context,node_id,status,level,progress,updated) VALUES(?,?,?,?,?,?,?)
|
|
278
|
+
await mgr.query(`INSERT INTO node_state(tree_id,context,node_id,status,level,progress,updated) VALUES(?,?,?,?,?,?,?)
|
|
273
279
|
ON CONFLICT(tree_id,context,node_id) DO UPDATE SET status=excluded.status, level=excluded.level, updated=excluded.updated`, [treeId, context, n[0].id, status, level, status === "unlocked" ? 1 : 0, (0, text_1.nowIso)()]);
|
|
274
280
|
// R10.1: unlock/lock are the timeline anchors for R11's time axis
|
|
275
281
|
await logEvent(mgr, status === "unlocked" ? "node.unlock" : "node.lock", n[0].id, {
|
|
@@ -414,10 +420,10 @@ async function importTree(ds, spec, opts = {}) {
|
|
|
414
420
|
for (const e of edges) {
|
|
415
421
|
const pid = nodeId.get(String(e.parent));
|
|
416
422
|
const cid = nodeId.get(String(e.child));
|
|
417
|
-
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
418
|
-
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
419
|
-
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
420
|
-
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
423
|
+
await mgr.query(`INSERT INTO node_edges(id,tree_id,parent_id,child_id,rel,mode,required,min_level,weight,note,created)
|
|
424
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?)
|
|
425
|
+
ON CONFLICT(parent_id,child_id,rel) DO UPDATE SET
|
|
426
|
+
mode=excluded.mode, required=excluded.required, min_level=excluded.min_level,
|
|
421
427
|
weight=excluded.weight, note=excluded.note`, [
|
|
422
428
|
(0, text_1.shortId)(["edge", tid, pid, cid, String(e.rel)], 12), tid, pid, cid,
|
|
423
429
|
String(e.rel), String(e.mode || "AND"), e.required === false ? 0 : 1,
|
|
@@ -427,7 +433,7 @@ async function importTree(ds, spec, opts = {}) {
|
|
|
427
433
|
for (const c of conds) {
|
|
428
434
|
const nid = nodeId.get(String(c.node));
|
|
429
435
|
await mgr.query(`DELETE FROM node_conditions WHERE node_id=? AND name=?`, [nid, String(c.name)]);
|
|
430
|
-
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
436
|
+
await mgr.query(`INSERT INTO node_conditions(id,node_id,name,kind,expr,op,value,required,effect,weight,note)
|
|
431
437
|
VALUES(?,?,?,?,?,?,?,?,?,?,?)`, [
|
|
432
438
|
(0, text_1.shortId)(["cond", tid, nid, String(c.name)], 10), nid, String(c.name),
|
|
433
439
|
String(c.kind || "flag"), String(c.expr || ""), String(c.op || ">="), String(c.value ?? ""),
|
package/docs/plugin-authoring.md
CHANGED
|
@@ -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.
|
|
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.
|
|
3
|
+
"version": "0.76.1",
|
|
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": "~4.0.5-alpha.2",
|
|
29
|
-
"@deepseek-ai/schemastery": "~3.18.5-alpha.2",
|
|
30
28
|
"@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.2",
|
|
31
|
-
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2"
|
|
29
|
+
"@deepseek-ai/cosmokit": "~1.8.6-alpha.2",
|
|
30
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.2",
|
|
31
|
+
"@deepseek-ai/schemastery": "~3.18.5-alpha.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^20.0.0",
|