gisool 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/README.md +136 -2
- package/dist/api/routes.js +166 -0
- package/dist/api/security.js +86 -0
- package/dist/api/sse.js +55 -0
- package/dist/api/static.js +32 -0
- package/dist/catalog/fields.js +146 -0
- package/dist/catalog/index.js +133 -0
- package/dist/catalog/scanner.js +185 -0
- package/dist/catalog/uri.js +41 -0
- package/dist/catalog/watcher.js +59 -0
- package/dist/cli/config-writer.js +255 -0
- package/dist/cli/doctor.js +197 -0
- package/dist/cli/eval.js +64 -0
- package/dist/cli/export-import.js +66 -0
- package/dist/cli/init.js +271 -0
- package/dist/cli/main.js +165 -0
- package/dist/cli/open.js +24 -0
- package/dist/cli/serve.js +144 -0
- package/dist/config.js +78 -0
- package/dist/dashboard/assets/app.js +256 -0
- package/dist/dashboard/assets/chunk-4U4GNPH4.js +17 -0
- package/dist/dashboard/assets/chunk-5DFWNJC2.js +1 -0
- package/dist/dashboard/assets/chunk-JPIEF5XO.js +17 -0
- package/dist/dashboard/assets/chunk-ZBUKVJOI.js +1 -0
- package/dist/dashboard/assets/styles.css +487 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/ingest/attribution.js +55 -0
- package/dist/ingest/hooks.js +168 -0
- package/dist/ingest/mcp-recorder.js +41 -0
- package/dist/ingest/otlp.js +160 -0
- package/dist/ingest/pipeline.js +100 -0
- package/dist/ingest/redact.js +36 -0
- package/dist/ingest/runtimes.js +25 -0
- package/dist/mcp/http.js +84 -0
- package/dist/mcp/skills-extension.js +35 -0
- package/dist/mcp/stdio-proxy.js +53 -0
- package/dist/mcp/tools.js +164 -0
- package/dist/packs/claude-code.js +157 -0
- package/dist/packs/codex.js +148 -0
- package/dist/packs/cursor.js +95 -0
- package/dist/packs/gajae.js +7 -0
- package/dist/packs/gemini.js +88 -0
- package/dist/packs/json-config.js +51 -0
- package/dist/packs/omo.js +149 -0
- package/dist/packs/openclaw.js +119 -0
- package/dist/packs/registry.js +13 -0
- package/dist/search/fts.js +61 -0
- package/dist/search/fusion.js +50 -0
- package/dist/search/guidance.js +7 -0
- package/dist/search/index.js +74 -0
- package/dist/search/lexical.js +69 -0
- package/dist/server.js +216 -0
- package/dist/store/db.js +74 -0
- package/dist/store/export.js +199 -0
- package/dist/store/index.js +13 -0
- package/dist/store/queries.js +458 -0
- package/dist/store/schema.sql +96 -0
- package/dist/store/writes.js +172 -0
- package/dist/types.js +1 -0
- package/dist/version.js +19 -0
- package/package.json +46 -4
- package/packs/claude-code/README.md +5 -0
- package/packs/claude-code/settings.template.json +24 -0
- package/packs/codex/README.md +5 -0
- package/packs/codex/config.template.toml +11 -0
- package/packs/cursor/hooks.example.json +8 -0
- package/packs/cursor/mcp.example.json +8 -0
- package/packs/gajae/extension/discovery.md +5 -0
- package/packs/gajae/extension/index.js +44 -0
- package/packs/gemini/settings.example.json +16 -0
- package/packs/hook-post.mjs +29 -0
- package/packs/omo/extension/discovery.md +5 -0
- package/packs/omo/extension/index.js +44 -0
- package/packs/openclaw/.codex-plugin/plugin.json +11 -0
- package/packs/openclaw/.mcp.json +12 -0
- package/packs/openclaw/gisool.json +1 -0
- package/packs/openclaw/hooks/gisool-discovery/HOOK.md +8 -0
- package/packs/openclaw/hooks/gisool-discovery/handler.js +27 -0
- package/packs/openclaw/index.mjs +47 -0
- package/packs/openclaw/openclaw.plugin.json +22 -0
- package/packs/openclaw/package.json +11 -0
- package/src/store/schema.sql +96 -0
- package/src/store/task-types.json +15 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { nameFromUri, parseJson, readIngestCounters, storeError } from "./writes.js";
|
|
3
|
+
const DAY_MS = 86_400_000;
|
|
4
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
5
|
+
const RUNTIMES = ["claude-code", "codex", "gemini", "cursor", "omo", "gajae", "openclaw", "unknown"];
|
|
6
|
+
let rulesCache;
|
|
7
|
+
/** tsc does not copy JSON into dist, so the rules are read beside the compiled module first, then from src/store. */
|
|
8
|
+
export function taskTypeRules() {
|
|
9
|
+
if (rulesCache)
|
|
10
|
+
return rulesCache;
|
|
11
|
+
const candidates = [new URL("./task-types.json", import.meta.url), new URL("../../src/store/task-types.json", import.meta.url)];
|
|
12
|
+
const found = candidates.find((u) => existsSync(u));
|
|
13
|
+
if (!found)
|
|
14
|
+
throw storeError("task_types_missing", "task-types.json not found next to the store module or in src/store");
|
|
15
|
+
rulesCache = JSON.parse(readFileSync(found, "utf8"));
|
|
16
|
+
return rulesCache;
|
|
17
|
+
}
|
|
18
|
+
function tokens(s) {
|
|
19
|
+
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
function keywordInText(keyword, text) {
|
|
22
|
+
const k = keyword.toLowerCase();
|
|
23
|
+
if (/^[\x20-\x7e]+$/.test(k)) {
|
|
24
|
+
const escaped = k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
return new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`).test(text);
|
|
26
|
+
}
|
|
27
|
+
return text.includes(k);
|
|
28
|
+
}
|
|
29
|
+
function skillMatches(rule, skill) {
|
|
30
|
+
const tags = skill.tags.map((t) => t.toLowerCase());
|
|
31
|
+
if (rule.anyTags?.some((t) => tags.includes(t.toLowerCase())))
|
|
32
|
+
return true;
|
|
33
|
+
if (skill.groupId && rule.anyGroup?.some((g) => g.toLowerCase() === skill.groupId.toLowerCase()))
|
|
34
|
+
return true;
|
|
35
|
+
const nameTokens = tokens(skill.name);
|
|
36
|
+
const name = skill.name.toLowerCase();
|
|
37
|
+
return rule.anyNameKeywords?.some((k) => nameTokens.includes(k.toLowerCase()) || (/[-_ ]/.test(k) && name.includes(k.toLowerCase()))) ?? false;
|
|
38
|
+
}
|
|
39
|
+
export function classifyTaskType(skill, prompt, rules = taskTypeRules()) {
|
|
40
|
+
if (skill) {
|
|
41
|
+
const hit = rules.find((r) => skillMatches(r, skill));
|
|
42
|
+
if (hit)
|
|
43
|
+
return hit.taskType;
|
|
44
|
+
if (skill.groupId)
|
|
45
|
+
return skill.groupId;
|
|
46
|
+
}
|
|
47
|
+
if (prompt) {
|
|
48
|
+
const text = prompt.toLowerCase();
|
|
49
|
+
const hit = rules.find((r) => r.anyPromptKeywords?.some((k) => keywordInText(k, text)));
|
|
50
|
+
if (hit)
|
|
51
|
+
return hit.taskType;
|
|
52
|
+
}
|
|
53
|
+
return "unclassified";
|
|
54
|
+
}
|
|
55
|
+
const fmtCache = new Map();
|
|
56
|
+
const WEEKDAYS = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
|
|
57
|
+
export function localDowHour(ts, tz) {
|
|
58
|
+
let fmt = fmtCache.get(tz);
|
|
59
|
+
if (!fmt) {
|
|
60
|
+
try {
|
|
61
|
+
fmt = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short", hour: "numeric", hourCycle: "h23" });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw storeError("invalid_tz", `unknown time zone: ${tz}`);
|
|
65
|
+
}
|
|
66
|
+
fmtCache.set(tz, fmt);
|
|
67
|
+
}
|
|
68
|
+
let dow = 0;
|
|
69
|
+
let hour = 0;
|
|
70
|
+
for (const p of fmt.formatToParts(ts)) {
|
|
71
|
+
if (p.type === "weekday")
|
|
72
|
+
dow = WEEKDAYS[p.value] ?? 0;
|
|
73
|
+
else if (p.type === "hour")
|
|
74
|
+
hour = Number(p.value) % 24;
|
|
75
|
+
}
|
|
76
|
+
return { dow, hour };
|
|
77
|
+
}
|
|
78
|
+
function candidatesOf(data) {
|
|
79
|
+
return Array.isArray(data.candidates) ? data.candidates : [];
|
|
80
|
+
}
|
|
81
|
+
/** Adoption precedence: loaded > exposed_only > no_match > aborted (error after a search) > not_used. */
|
|
82
|
+
export function adoptionState(events) {
|
|
83
|
+
if (events.some((e) => e.kind === "load"))
|
|
84
|
+
return "loaded";
|
|
85
|
+
const searches = events.filter((e) => e.kind === "search");
|
|
86
|
+
if (searches.some((e) => candidatesOf(e.data).length >= 1))
|
|
87
|
+
return "exposed_only";
|
|
88
|
+
if (searches.some((e) => e.data.abstained !== undefined && e.data.abstained !== null))
|
|
89
|
+
return "no_match";
|
|
90
|
+
const firstSearch = events.findIndex((e) => e.kind === "search");
|
|
91
|
+
if (firstSearch >= 0 && events.slice(firstSearch + 1).some((e) => e.kind === "error"))
|
|
92
|
+
return "aborted";
|
|
93
|
+
return "not_used";
|
|
94
|
+
}
|
|
95
|
+
function isoWeekStart(ts) {
|
|
96
|
+
const d = new Date(ts);
|
|
97
|
+
const mondayOffset = (d.getUTCDay() + 6) % 7;
|
|
98
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) - mondayOffset * DAY_MS;
|
|
99
|
+
}
|
|
100
|
+
function isoWeekLabel(weekStart) {
|
|
101
|
+
const thursday = new Date(weekStart + 3 * DAY_MS);
|
|
102
|
+
const year = thursday.getUTCFullYear();
|
|
103
|
+
const week1 = isoWeekStart(Date.UTC(year, 0, 4));
|
|
104
|
+
const week = 1 + Math.round((weekStart - week1) / WEEK_MS);
|
|
105
|
+
return `${year}-W${String(week).padStart(2, "0")}`;
|
|
106
|
+
}
|
|
107
|
+
function shortDigest(d) {
|
|
108
|
+
const s = typeof d === "string" ? d : "";
|
|
109
|
+
return s.replace(/^sha256[:-]/, "").slice(0, 12);
|
|
110
|
+
}
|
|
111
|
+
export function createQueries(ctx) {
|
|
112
|
+
const { db } = ctx;
|
|
113
|
+
const defaultTz = () => ctx.opts.tz ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
114
|
+
function skillMeta() {
|
|
115
|
+
const rows = db.prepare("SELECT uri, name, tags_json, group_id FROM skills").all();
|
|
116
|
+
return new Map(rows.map((r) => [r.uri, { uri: r.uri, name: r.name, tags: parseJson(r.tags_json, []), ...(r.group_id ? { groupId: r.group_id } : {}) }]));
|
|
117
|
+
}
|
|
118
|
+
function metaFor(skills, uri) {
|
|
119
|
+
return skills.get(uri) ?? { uri, name: nameFromUri(uri), tags: [] };
|
|
120
|
+
}
|
|
121
|
+
function loadSessions(f, ignoreCellFilter = false) {
|
|
122
|
+
const where = [];
|
|
123
|
+
const params = [];
|
|
124
|
+
if (f.from !== undefined) {
|
|
125
|
+
where.push("started_at >= ?");
|
|
126
|
+
params.push(f.from);
|
|
127
|
+
}
|
|
128
|
+
if (f.to !== undefined) {
|
|
129
|
+
where.push("started_at < ?");
|
|
130
|
+
params.push(f.to);
|
|
131
|
+
}
|
|
132
|
+
if (f.runtime && f.runtime.length > 0) {
|
|
133
|
+
where.push(`runtime IN (${f.runtime.map(() => "?").join(",")})`);
|
|
134
|
+
params.push(...f.runtime);
|
|
135
|
+
}
|
|
136
|
+
const clause = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
137
|
+
const rows = db.prepare(`SELECT id, runtime, started_at, ended_at, first_prompt FROM sessions ${clause}`).all(...params);
|
|
138
|
+
const events = db.prepare(`SELECT * FROM events WHERE session_id IN (SELECT id FROM sessions ${clause}) ORDER BY ts, rowid`).all(...params);
|
|
139
|
+
const bySession = new Map();
|
|
140
|
+
for (const e of events) {
|
|
141
|
+
const list = bySession.get(e.session_id);
|
|
142
|
+
if (list)
|
|
143
|
+
list.push(e);
|
|
144
|
+
else
|
|
145
|
+
bySession.set(e.session_id, [e]);
|
|
146
|
+
}
|
|
147
|
+
const tz = f.tz ?? defaultTz();
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const r of rows) {
|
|
150
|
+
const evs = bySession.get(r.id) ?? [];
|
|
151
|
+
const parsed = evs.map((e) => ({ kind: e.kind, data: parseJson(e.data_json, {}) }));
|
|
152
|
+
const loaded = [];
|
|
153
|
+
const exposed = [];
|
|
154
|
+
let firstTop;
|
|
155
|
+
const texts = [];
|
|
156
|
+
evs.forEach((e, i) => {
|
|
157
|
+
const data = parsed[i].data;
|
|
158
|
+
if (e.kind === "load") {
|
|
159
|
+
const uri = e.skill_uri ?? (typeof data.uri === "string" ? data.uri : undefined);
|
|
160
|
+
if (uri && !loaded.includes(uri))
|
|
161
|
+
loaded.push(uri);
|
|
162
|
+
}
|
|
163
|
+
else if (e.kind === "search") {
|
|
164
|
+
const cands = [...candidatesOf(data)].sort((a, b) => a.rank - b.rank);
|
|
165
|
+
if (firstTop === undefined && cands[0])
|
|
166
|
+
firstTop = cands[0].uri;
|
|
167
|
+
for (const c of cands)
|
|
168
|
+
if (!exposed.includes(c.uri))
|
|
169
|
+
exposed.push(c.uri);
|
|
170
|
+
if (typeof data.query === "string")
|
|
171
|
+
texts.push(data.query);
|
|
172
|
+
}
|
|
173
|
+
else if (e.kind === "prompt" && e.text)
|
|
174
|
+
texts.push(e.text);
|
|
175
|
+
});
|
|
176
|
+
const attributed = evs.find((e) => e.attribution_method !== "none");
|
|
177
|
+
const info = {
|
|
178
|
+
id: r.id, runtime: r.runtime, startedAt: r.started_at, events: evs, loaded, exposed, state: adoptionState(parsed),
|
|
179
|
+
attribution: attributed?.attribution_method ?? "none", texts,
|
|
180
|
+
...(r.ended_at !== null ? { endedAt: r.ended_at } : {}),
|
|
181
|
+
...(r.first_prompt !== null ? { firstPrompt: r.first_prompt } : {}),
|
|
182
|
+
...(firstTop !== undefined ? { firstTop } : {}),
|
|
183
|
+
};
|
|
184
|
+
if (f.state && f.state.length > 0 && !f.state.includes(info.state))
|
|
185
|
+
continue;
|
|
186
|
+
if (f.skill && f.skill.length > 0 && !f.skill.some((u) => loaded.includes(u) || exposed.includes(u)))
|
|
187
|
+
continue;
|
|
188
|
+
if (f.q) {
|
|
189
|
+
const q = f.q.toLowerCase();
|
|
190
|
+
if (![info.firstPrompt ?? "", ...texts].some((t) => t.toLowerCase().includes(q)))
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!ignoreCellFilter && (f.dow !== undefined || f.hour !== undefined)) {
|
|
194
|
+
const local = localDowHour(r.started_at, tz);
|
|
195
|
+
if (f.dow !== undefined && local.dow !== f.dow)
|
|
196
|
+
continue;
|
|
197
|
+
if (f.hour !== undefined) {
|
|
198
|
+
const span = f.hourSpan ?? 1;
|
|
199
|
+
if (local.hour < f.hour || local.hour >= f.hour + span)
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
out.push(info);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function sessionSkills(s) {
|
|
208
|
+
if (s.loaded.length > 0)
|
|
209
|
+
return s.loaded;
|
|
210
|
+
return s.firstTop ? [s.firstTop] : [];
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
sessions(f) {
|
|
214
|
+
return loadSessions(f)
|
|
215
|
+
.sort((a, b) => b.startedAt - a.startedAt)
|
|
216
|
+
.map((s) => ({
|
|
217
|
+
id: s.id, runtime: s.runtime, startedAt: s.startedAt, skills: sessionSkills(s), state: s.state, attribution: s.attribution,
|
|
218
|
+
...(s.endedAt !== undefined ? { endedAt: s.endedAt } : {}),
|
|
219
|
+
...(s.firstPrompt !== undefined ? { firstPrompt: s.firstPrompt } : {}),
|
|
220
|
+
}));
|
|
221
|
+
},
|
|
222
|
+
trace(sessionId) {
|
|
223
|
+
const evs = db.prepare("SELECT * FROM events WHERE session_id = ? ORDER BY ts, rowid").all(sessionId);
|
|
224
|
+
const skills = skillMeta();
|
|
225
|
+
const loadsByTrace = new Map();
|
|
226
|
+
for (const e of evs) {
|
|
227
|
+
if (e.kind !== "load" || !e.trace_id || !e.skill_uri)
|
|
228
|
+
continue;
|
|
229
|
+
const list = loadsByTrace.get(e.trace_id) ?? [];
|
|
230
|
+
list.push({ ts: e.ts, uri: e.skill_uri });
|
|
231
|
+
loadsByTrace.set(e.trace_id, list);
|
|
232
|
+
}
|
|
233
|
+
const loadRow = db.prepare("SELECT uri, manifest_digest FROM loads WHERE load_id = ?");
|
|
234
|
+
return evs.map((e) => {
|
|
235
|
+
const data = parseJson(e.data_json, {});
|
|
236
|
+
switch (e.kind) {
|
|
237
|
+
case "prompt": {
|
|
238
|
+
const textLength = e.text_length ?? e.text?.length;
|
|
239
|
+
const summary = e.text ? e.text.slice(0, 200) : `(prompt not captured${textLength !== undefined ? `, ${textLength} chars` : ""})`;
|
|
240
|
+
return { ts: e.ts, kind: e.kind, summary, detail: { kind: "prompt", ...(e.text !== null ? { text: e.text } : {}), ...(textLength !== undefined ? { textLength } : {}) } };
|
|
241
|
+
}
|
|
242
|
+
case "search": {
|
|
243
|
+
const traceId = e.trace_id ?? (typeof data.traceId === "string" ? data.traceId : "");
|
|
244
|
+
const later = (loadsByTrace.get(traceId) ?? []).filter((l) => l.ts >= e.ts).map((l) => l.uri);
|
|
245
|
+
const cands = candidatesOf(data).map((c) => ({ ...c, chosen: later.includes(c.uri) }));
|
|
246
|
+
const chosen = cands.filter((c) => c.chosen).map((c) => c.uri);
|
|
247
|
+
const query = typeof data.query === "string" ? data.query : "";
|
|
248
|
+
const abstained = data.abstained;
|
|
249
|
+
const summary = abstained
|
|
250
|
+
? `search "${query}" abstained (${abstained.reason}) in ${Number(data.elapsedMs ?? 0)} ms`
|
|
251
|
+
: `search "${query}" -> ${cands.length} candidates in ${Number(data.elapsedMs ?? 0)} ms`;
|
|
252
|
+
return { ts: e.ts, kind: e.kind, summary, detail: { ...data, kind: "search", traceId, chosen, candidates: cands } };
|
|
253
|
+
}
|
|
254
|
+
case "load": {
|
|
255
|
+
const loadId = e.load_id ?? (typeof data.loadId === "string" ? data.loadId : "");
|
|
256
|
+
const stored = loadId ? loadRow.get(loadId) : undefined;
|
|
257
|
+
const uri = e.skill_uri ?? stored?.uri ?? (typeof data.uri === "string" ? data.uri : "");
|
|
258
|
+
const manifestDigest = typeof data.manifestDigest === "string" ? data.manifestDigest : stored?.manifest_digest ?? "";
|
|
259
|
+
return { ts: e.ts, kind: e.kind, summary: `load ${metaFor(skills, uri).name} (${uri}) @${shortDigest(manifestDigest)}`,
|
|
260
|
+
detail: { kind: "load", uri, loadId, manifestDigest } };
|
|
261
|
+
}
|
|
262
|
+
case "read_file": {
|
|
263
|
+
const uri = typeof data.uri === "string" ? data.uri : "";
|
|
264
|
+
const bytes = Number(data.bytes ?? data.size ?? 0);
|
|
265
|
+
const skillUri = e.skill_uri ?? (typeof data.skillUri === "string" ? data.skillUri : "");
|
|
266
|
+
return { ts: e.ts, kind: e.kind, summary: `read ${uri} (${bytes} bytes)`, detail: { kind: "read_file", skillUri, uri, bytes } };
|
|
267
|
+
}
|
|
268
|
+
case "native_skill": {
|
|
269
|
+
const name = typeof data.name === "string" ? data.name : e.skill_uri ? nameFromUri(e.skill_uri) : "unknown";
|
|
270
|
+
const via = typeof data.via === "string" ? data.via : "tool";
|
|
271
|
+
return { ts: e.ts, kind: e.kind, summary: `native skill ${name} via ${via}`, detail: { kind: "native_skill", name, via } };
|
|
272
|
+
}
|
|
273
|
+
case "error": {
|
|
274
|
+
const code = typeof data.code === "string" ? data.code : "error";
|
|
275
|
+
const message = typeof data.message === "string" ? data.message : e.text ?? "";
|
|
276
|
+
return { ts: e.ts, kind: e.kind, summary: `error ${code}${message ? `: ${message.slice(0, 200)}` : ""}`, detail: { kind: "error", code, message } };
|
|
277
|
+
}
|
|
278
|
+
default:
|
|
279
|
+
return { ts: e.ts, kind: e.kind, summary: e.kind === "session_start" ? "session started" : "session ended", detail: { kind: e.kind } };
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
flow(f, unit, top = 7) {
|
|
284
|
+
const limit = Math.min(15, Math.max(1, Math.floor(top)));
|
|
285
|
+
const sessions = loadSessions(f);
|
|
286
|
+
const skills = skillMeta();
|
|
287
|
+
const paths = [];
|
|
288
|
+
for (const s of sessions) {
|
|
289
|
+
const primary = s.loaded[0] ?? s.firstTop;
|
|
290
|
+
const task = classifyTaskType(primary ? metaFor(skills, primary) : undefined, s.firstPrompt ?? s.texts[0]);
|
|
291
|
+
const skillList = unit === "all_loads" && s.loaded.length > 0 ? s.loaded : primary ? [primary] : ["(none)"];
|
|
292
|
+
for (const sk of skillList)
|
|
293
|
+
paths.push({ session: s.id, task, skill: sk, state: s.state });
|
|
294
|
+
}
|
|
295
|
+
const OTHER = "__other__";
|
|
296
|
+
const otherMembers = [new Set(), new Set(), new Set()];
|
|
297
|
+
const keep = (values, column) => {
|
|
298
|
+
const sessionsBy = new Map();
|
|
299
|
+
paths.forEach((p, i) => {
|
|
300
|
+
const v = values[i];
|
|
301
|
+
const set = sessionsBy.get(v) ?? new Set();
|
|
302
|
+
set.add(p.session);
|
|
303
|
+
sessionsBy.set(v, set);
|
|
304
|
+
});
|
|
305
|
+
const ranked = [...sessionsBy.entries()].sort((a, b) => b[1].size - a[1].size || a[0].localeCompare(b[0]));
|
|
306
|
+
const map = new Map();
|
|
307
|
+
ranked.forEach(([v], i) => {
|
|
308
|
+
if (i < limit)
|
|
309
|
+
map.set(v, v);
|
|
310
|
+
else {
|
|
311
|
+
map.set(v, OTHER);
|
|
312
|
+
otherMembers[column].add(v);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return map;
|
|
316
|
+
};
|
|
317
|
+
const taskMap = keep(paths.map((p) => p.task), 0);
|
|
318
|
+
const skillMap = keep(paths.map((p) => p.skill), 1);
|
|
319
|
+
const stateMap = keep(paths.map((p) => p.state), 2);
|
|
320
|
+
const mapped = paths.map((p) => ({ session: p.session, state: p.state, task: taskMap.get(p.task), skill: skillMap.get(p.skill), stateNode: stateMap.get(p.state) }));
|
|
321
|
+
const countLinks = () => {
|
|
322
|
+
const left = new Map();
|
|
323
|
+
const right = new Map();
|
|
324
|
+
const add = (m, k, s) => { const set = m.get(k) ?? new Set(); set.add(s); m.set(k, set); };
|
|
325
|
+
for (const p of mapped) {
|
|
326
|
+
add(left, `${p.task}\u0000${p.skill}\u0000${p.state}`, p.session);
|
|
327
|
+
add(right, `${p.skill}\u0000${p.stateNode}\u0000${p.state}`, p.session);
|
|
328
|
+
}
|
|
329
|
+
return { left, right };
|
|
330
|
+
};
|
|
331
|
+
const before = countLinks();
|
|
332
|
+
for (const p of mapped) {
|
|
333
|
+
if (p.skill === OTHER)
|
|
334
|
+
continue;
|
|
335
|
+
const weakLeft = (before.left.get(`${p.task}\u0000${p.skill}\u0000${p.state}`)?.size ?? 0) < 2;
|
|
336
|
+
const weakRight = (before.right.get(`${p.skill}\u0000${p.stateNode}\u0000${p.state}`)?.size ?? 0) < 2;
|
|
337
|
+
if (weakLeft || weakRight) {
|
|
338
|
+
otherMembers[1].add(p.skill);
|
|
339
|
+
p.skill = OTHER;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const after = countLinks();
|
|
343
|
+
const nodeId = (column, v) => `${["t", "s", "a"][column]}:${v}`;
|
|
344
|
+
const nodeSessions = new Map();
|
|
345
|
+
const touch = (column, v, s) => {
|
|
346
|
+
const id = nodeId(column, v);
|
|
347
|
+
const n = nodeSessions.get(id) ?? { column, value: v, sessions: new Set() };
|
|
348
|
+
n.sessions.add(s);
|
|
349
|
+
nodeSessions.set(id, n);
|
|
350
|
+
};
|
|
351
|
+
for (const p of mapped) {
|
|
352
|
+
touch(0, p.task, p.session);
|
|
353
|
+
touch(1, p.skill, p.session);
|
|
354
|
+
touch(2, p.stateNode, p.session);
|
|
355
|
+
}
|
|
356
|
+
const label = (column, v) => v === OTHER ? `Other (${otherMembers[column].size})` : column === 1 ? (v === "(none)" ? "(no skill)" : metaFor(skills, v).name) : v;
|
|
357
|
+
const nodes = [...nodeSessions.entries()]
|
|
358
|
+
.sort((a, b) => a[1].column - b[1].column || Number(a[1].value === OTHER) - Number(b[1].value === OTHER) || b[1].sessions.size - a[1].sessions.size)
|
|
359
|
+
.map(([id, n]) => ({ id, column: n.column, label: label(n.column, n.value), count: n.sessions.size, ...(n.value === OTHER ? { other: true } : {}) }));
|
|
360
|
+
const links = [];
|
|
361
|
+
for (const [k, set] of after.left) {
|
|
362
|
+
const [task, skill, state] = k.split("\u0000");
|
|
363
|
+
links.push({ source: nodeId(0, task), target: nodeId(1, skill), count: set.size, state });
|
|
364
|
+
}
|
|
365
|
+
for (const [k, set] of after.right) {
|
|
366
|
+
const [skill, stateNode, state] = k.split("\u0000");
|
|
367
|
+
links.push({ source: nodeId(1, skill), target: nodeId(2, stateNode), count: set.size, state });
|
|
368
|
+
}
|
|
369
|
+
links.sort((a, b) => b.count - a.count);
|
|
370
|
+
return { unit, columns: ["task_type", "skill", "state"], nodes, links, sessionCount: sessions.length };
|
|
371
|
+
},
|
|
372
|
+
heatmap(f, tz, weekStart, bucketHours) {
|
|
373
|
+
const buckets = 24 / bucketHours;
|
|
374
|
+
const grid = Array.from({ length: 7 }, () => new Array(buckets).fill(0));
|
|
375
|
+
for (const s of loadSessions(f, true)) {
|
|
376
|
+
const { dow, hour } = localDowHour(s.startedAt, tz);
|
|
377
|
+
grid[dow][Math.floor(hour / bucketHours)] += 1;
|
|
378
|
+
}
|
|
379
|
+
const cells = [];
|
|
380
|
+
for (let i = 0; i < 7; i++) {
|
|
381
|
+
const dow = (weekStart + i) % 7;
|
|
382
|
+
for (let b = 0; b < buckets; b++)
|
|
383
|
+
cells.push({ dow, hour: b * bucketHours, count: grid[dow][b] });
|
|
384
|
+
}
|
|
385
|
+
const rowTotals = grid.map((row) => row.reduce((a, n) => a + n, 0));
|
|
386
|
+
const colTotals = Array.from({ length: buckets }, (_, b) => grid.reduce((a, row) => a + row[b], 0));
|
|
387
|
+
return { tz, weekStart, bucketHours, cells, rowTotals, colTotals };
|
|
388
|
+
},
|
|
389
|
+
skillWeek(f) {
|
|
390
|
+
const lastWeek = isoWeekStart(f.to !== undefined ? f.to - 1 : ctx.now());
|
|
391
|
+
const starts = Array.from({ length: 8 }, (_, i) => lastWeek - (7 - i) * WEEK_MS);
|
|
392
|
+
const from = starts[0];
|
|
393
|
+
const to = lastWeek + WEEK_MS;
|
|
394
|
+
const where = ["kind = 'load'", "skill_uri IS NOT NULL", "ts >= ?", "ts < ?"];
|
|
395
|
+
const params = [from, to];
|
|
396
|
+
if (f.runtime && f.runtime.length > 0) {
|
|
397
|
+
where.push(`runtime IN (${f.runtime.map(() => "?").join(",")})`);
|
|
398
|
+
params.push(...f.runtime);
|
|
399
|
+
}
|
|
400
|
+
if (f.skill && f.skill.length > 0) {
|
|
401
|
+
where.push(`skill_uri IN (${f.skill.map(() => "?").join(",")})`);
|
|
402
|
+
params.push(...f.skill);
|
|
403
|
+
}
|
|
404
|
+
const rows = db.prepare(`SELECT skill_uri, ts FROM events WHERE ${where.join(" AND ")}`).all(...params);
|
|
405
|
+
const skills = skillMeta();
|
|
406
|
+
const counts = new Map();
|
|
407
|
+
for (const r of rows) {
|
|
408
|
+
const idx = Math.floor((r.ts - from) / WEEK_MS);
|
|
409
|
+
const arr = counts.get(r.skill_uri) ?? new Array(8).fill(0);
|
|
410
|
+
arr[idx] += 1;
|
|
411
|
+
counts.set(r.skill_uri, arr);
|
|
412
|
+
}
|
|
413
|
+
const out = [...counts.entries()]
|
|
414
|
+
.map(([uri, c]) => ({ uri, name: metaFor(skills, uri).name, counts: c }))
|
|
415
|
+
.sort((a, b) => b.counts.reduce((x, y) => x + y, 0) - a.counts.reduce((x, y) => x + y, 0) || a.name.localeCompare(b.name));
|
|
416
|
+
return { weeks: starts.map(isoWeekLabel), rows: out };
|
|
417
|
+
},
|
|
418
|
+
stats(f = {}) {
|
|
419
|
+
const attributionRate = Object.fromEntries(RUNTIMES.map((r) => [r, null]));
|
|
420
|
+
const mcp = db.prepare(`SELECT runtime, COUNT(*) AS total, SUM(CASE WHEN session_id != 'unattributed' THEN 1 ELSE 0 END) AS attributed
|
|
421
|
+
FROM events WHERE source = 'mcp' GROUP BY runtime`).all();
|
|
422
|
+
for (const r of mcp)
|
|
423
|
+
attributionRate[r.runtime] = r.total > 0 ? r.attributed / r.total : null;
|
|
424
|
+
const where = ["source = 'mcp'", "session_id = 'unattributed'", "kind IN ('search', 'load')"];
|
|
425
|
+
const params = [];
|
|
426
|
+
if (f.from !== undefined) {
|
|
427
|
+
where.push("ts >= ?");
|
|
428
|
+
params.push(f.from);
|
|
429
|
+
}
|
|
430
|
+
if (f.to !== undefined) {
|
|
431
|
+
where.push("ts < ?");
|
|
432
|
+
params.push(f.to);
|
|
433
|
+
}
|
|
434
|
+
if (f.runtime && f.runtime.length > 0) {
|
|
435
|
+
where.push(`runtime IN (${f.runtime.map(() => "?").join(",")})`);
|
|
436
|
+
params.push(...f.runtime);
|
|
437
|
+
}
|
|
438
|
+
const orphan = db.prepare(`SELECT kind, COUNT(*) AS n FROM events WHERE ${where.join(" AND ")} GROUP BY kind`).all(...params);
|
|
439
|
+
const unattributed = { searches: orphan.find((o) => o.kind === "search")?.n ?? 0, loads: orphan.find((o) => o.kind === "load")?.n ?? 0 };
|
|
440
|
+
const searches = db.prepare("SELECT data_json FROM events WHERE kind = 'search'").all();
|
|
441
|
+
const abstained = searches.filter((s) => parseJson(s.data_json, {}).abstained).length;
|
|
442
|
+
const cutoff = ctx.now() - 56 * DAY_MS;
|
|
443
|
+
const unused = db.prepare(`SELECT uri FROM skills WHERE removed_at IS NULL AND uri NOT IN
|
|
444
|
+
(SELECT skill_uri FROM events WHERE kind = 'load' AND skill_uri IS NOT NULL AND ts >= ?) ORDER BY name`).all(cutoff);
|
|
445
|
+
const last = db.prepare("SELECT MAX(ts) AS ts FROM events").get();
|
|
446
|
+
const byRuntime = db.prepare("SELECT runtime, MAX(ts) AS ts FROM events GROUP BY runtime").all();
|
|
447
|
+
return {
|
|
448
|
+
attributionRate,
|
|
449
|
+
abstainRate: searches.length > 0 ? abstained / searches.length : 0,
|
|
450
|
+
unusedSkills: unused.map((u) => u.uri),
|
|
451
|
+
...(last.ts !== null ? { lastEventTs: last.ts } : {}),
|
|
452
|
+
lastEventByRuntime: Object.fromEntries(byRuntime.map((r) => [r.runtime, r.ts])),
|
|
453
|
+
ingest: readIngestCounters(db),
|
|
454
|
+
unattributed,
|
|
455
|
+
};
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
-- gisool store schema, user_version 1
|
|
2
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
3
|
+
key TEXT PRIMARY KEY,
|
|
4
|
+
value TEXT
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
8
|
+
uri TEXT PRIMARY KEY,
|
|
9
|
+
root_id TEXT NOT NULL,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
description TEXT NOT NULL DEFAULT '',
|
|
12
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
13
|
+
when_to_use TEXT,
|
|
14
|
+
examples_json TEXT NOT NULL DEFAULT '[]',
|
|
15
|
+
aliases_ko_json TEXT NOT NULL DEFAULT '[]',
|
|
16
|
+
group_id TEXT,
|
|
17
|
+
group_stage TEXT,
|
|
18
|
+
invocation TEXT NOT NULL DEFAULT 'automatic',
|
|
19
|
+
body_head TEXT NOT NULL DEFAULT '',
|
|
20
|
+
digest TEXT NOT NULL,
|
|
21
|
+
manifest_digest TEXT NOT NULL,
|
|
22
|
+
resources_json TEXT NOT NULL DEFAULT '[]',
|
|
23
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
removed_at INTEGER
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- uri is stored UNINDEXED so rows join back to skills(uri).
|
|
28
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
29
|
+
uri UNINDEXED,
|
|
30
|
+
name,
|
|
31
|
+
description,
|
|
32
|
+
when_to_use,
|
|
33
|
+
examples,
|
|
34
|
+
aliases_ko,
|
|
35
|
+
body_head,
|
|
36
|
+
tokenize = 'trigram'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
runtime TEXT NOT NULL,
|
|
42
|
+
runtime_session_id TEXT,
|
|
43
|
+
conversation_id TEXT,
|
|
44
|
+
client_name TEXT,
|
|
45
|
+
cwd TEXT,
|
|
46
|
+
started_at INTEGER NOT NULL,
|
|
47
|
+
ended_at INTEGER,
|
|
48
|
+
first_prompt TEXT
|
|
49
|
+
);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS sessions_runtime_session ON sessions(runtime, runtime_session_id);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS sessions_runtime_conversation ON sessions(runtime, conversation_id);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS sessions_started ON sessions(started_at);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
ts INTEGER NOT NULL,
|
|
57
|
+
kind TEXT NOT NULL,
|
|
58
|
+
source TEXT NOT NULL,
|
|
59
|
+
session_id TEXT NOT NULL,
|
|
60
|
+
attribution_method TEXT NOT NULL,
|
|
61
|
+
attribution_reason TEXT,
|
|
62
|
+
runtime TEXT NOT NULL,
|
|
63
|
+
trace_id TEXT,
|
|
64
|
+
load_id TEXT,
|
|
65
|
+
skill_uri TEXT,
|
|
66
|
+
tool_use_id TEXT,
|
|
67
|
+
text TEXT,
|
|
68
|
+
text_digest TEXT,
|
|
69
|
+
text_length INTEGER,
|
|
70
|
+
data_json TEXT NOT NULL DEFAULT '{}'
|
|
71
|
+
);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS events_session_ts ON events(session_id, ts);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS events_ts ON events(ts);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS events_trace ON events(trace_id);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS events_skill ON events(skill_uri);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS events_tool_use ON events(tool_use_id);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS loads (
|
|
79
|
+
load_id TEXT PRIMARY KEY,
|
|
80
|
+
trace_id TEXT,
|
|
81
|
+
uri TEXT NOT NULL,
|
|
82
|
+
manifest_digest TEXT NOT NULL,
|
|
83
|
+
resources_json TEXT NOT NULL DEFAULT '[]',
|
|
84
|
+
created_at INTEGER NOT NULL
|
|
85
|
+
);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS loads_uri_created ON loads(uri, created_at);
|
|
87
|
+
|
|
88
|
+
CREATE TABLE IF NOT EXISTS eval_cases (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
query TEXT NOT NULL,
|
|
91
|
+
expected_uri TEXT,
|
|
92
|
+
from_trace_id TEXT,
|
|
93
|
+
created_at INTEGER NOT NULL
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
PRAGMA user_version = 1;
|