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.
Files changed (84) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +136 -2
  3. package/dist/api/routes.js +166 -0
  4. package/dist/api/security.js +86 -0
  5. package/dist/api/sse.js +55 -0
  6. package/dist/api/static.js +32 -0
  7. package/dist/catalog/fields.js +146 -0
  8. package/dist/catalog/index.js +133 -0
  9. package/dist/catalog/scanner.js +185 -0
  10. package/dist/catalog/uri.js +41 -0
  11. package/dist/catalog/watcher.js +59 -0
  12. package/dist/cli/config-writer.js +255 -0
  13. package/dist/cli/doctor.js +197 -0
  14. package/dist/cli/eval.js +64 -0
  15. package/dist/cli/export-import.js +66 -0
  16. package/dist/cli/init.js +271 -0
  17. package/dist/cli/main.js +165 -0
  18. package/dist/cli/open.js +24 -0
  19. package/dist/cli/serve.js +144 -0
  20. package/dist/config.js +78 -0
  21. package/dist/dashboard/assets/app.js +256 -0
  22. package/dist/dashboard/assets/chunk-4U4GNPH4.js +17 -0
  23. package/dist/dashboard/assets/chunk-5DFWNJC2.js +1 -0
  24. package/dist/dashboard/assets/chunk-JPIEF5XO.js +17 -0
  25. package/dist/dashboard/assets/chunk-ZBUKVJOI.js +1 -0
  26. package/dist/dashboard/assets/styles.css +487 -0
  27. package/dist/dashboard/index.html +15 -0
  28. package/dist/ingest/attribution.js +55 -0
  29. package/dist/ingest/hooks.js +168 -0
  30. package/dist/ingest/mcp-recorder.js +41 -0
  31. package/dist/ingest/otlp.js +160 -0
  32. package/dist/ingest/pipeline.js +100 -0
  33. package/dist/ingest/redact.js +36 -0
  34. package/dist/ingest/runtimes.js +25 -0
  35. package/dist/mcp/http.js +84 -0
  36. package/dist/mcp/skills-extension.js +35 -0
  37. package/dist/mcp/stdio-proxy.js +53 -0
  38. package/dist/mcp/tools.js +164 -0
  39. package/dist/packs/claude-code.js +157 -0
  40. package/dist/packs/codex.js +148 -0
  41. package/dist/packs/cursor.js +95 -0
  42. package/dist/packs/gajae.js +7 -0
  43. package/dist/packs/gemini.js +88 -0
  44. package/dist/packs/json-config.js +51 -0
  45. package/dist/packs/omo.js +149 -0
  46. package/dist/packs/openclaw.js +119 -0
  47. package/dist/packs/registry.js +13 -0
  48. package/dist/search/fts.js +61 -0
  49. package/dist/search/fusion.js +50 -0
  50. package/dist/search/guidance.js +7 -0
  51. package/dist/search/index.js +74 -0
  52. package/dist/search/lexical.js +69 -0
  53. package/dist/server.js +216 -0
  54. package/dist/store/db.js +74 -0
  55. package/dist/store/export.js +199 -0
  56. package/dist/store/index.js +13 -0
  57. package/dist/store/queries.js +458 -0
  58. package/dist/store/schema.sql +96 -0
  59. package/dist/store/writes.js +172 -0
  60. package/dist/types.js +1 -0
  61. package/dist/version.js +19 -0
  62. package/package.json +46 -4
  63. package/packs/claude-code/README.md +5 -0
  64. package/packs/claude-code/settings.template.json +24 -0
  65. package/packs/codex/README.md +5 -0
  66. package/packs/codex/config.template.toml +11 -0
  67. package/packs/cursor/hooks.example.json +8 -0
  68. package/packs/cursor/mcp.example.json +8 -0
  69. package/packs/gajae/extension/discovery.md +5 -0
  70. package/packs/gajae/extension/index.js +44 -0
  71. package/packs/gemini/settings.example.json +16 -0
  72. package/packs/hook-post.mjs +29 -0
  73. package/packs/omo/extension/discovery.md +5 -0
  74. package/packs/omo/extension/index.js +44 -0
  75. package/packs/openclaw/.codex-plugin/plugin.json +11 -0
  76. package/packs/openclaw/.mcp.json +12 -0
  77. package/packs/openclaw/gisool.json +1 -0
  78. package/packs/openclaw/hooks/gisool-discovery/HOOK.md +8 -0
  79. package/packs/openclaw/hooks/gisool-discovery/handler.js +27 -0
  80. package/packs/openclaw/index.mjs +47 -0
  81. package/packs/openclaw/openclaw.plugin.json +22 -0
  82. package/packs/openclaw/package.json +11 -0
  83. package/src/store/schema.sql +96 -0
  84. package/src/store/task-types.json +15 -0
@@ -0,0 +1,168 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const SKILL_FILE_IN_PATH = /[\\/]([^\\/]+)[\\/]SKILL\.md$/;
3
+ const SKILL_FILE_IN_COMMAND = /[\\/]([^\\/\s'"`]+)[\\/]SKILL\.md(?![\w.])/g;
4
+ export function str(value) {
5
+ return typeof value === "string" && value.length > 0 ? value : undefined;
6
+ }
7
+ function obj(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
9
+ }
10
+ export function skillNameFromPath(path) {
11
+ return path ? SKILL_FILE_IN_PATH.exec(path)?.[1] : undefined;
12
+ }
13
+ export function skillNamesInCommand(command) {
14
+ if (!command)
15
+ return [];
16
+ const names = new Set();
17
+ for (const match of command.matchAll(SKILL_FILE_IN_COMMAND))
18
+ names.add(match[1]);
19
+ return [...names];
20
+ }
21
+ function commandText(value) {
22
+ if (Array.isArray(value))
23
+ return value.filter((v) => typeof v === "string").join(" ");
24
+ return str(value);
25
+ }
26
+ export function makeEvent(kind, source, ref, ts, extra = {}) {
27
+ return { id: randomUUID(), ts, kind, source, ref, data: {}, ...extra };
28
+ }
29
+ function promptEvent(ref, ts, prompt, data = {}) {
30
+ const text = typeof prompt === "string" ? prompt : undefined;
31
+ return makeEvent("prompt", "hook", ref, ts, text === undefined ? { data: { ...data, promptUnavailable: true } } : { text, data });
32
+ }
33
+ function nativeSkill(ref, ts, name, via, extra = {}, data = {}) {
34
+ return makeEvent("native_skill", "hook", ref, ts, { ...extra, data: { ...data, name, via } });
35
+ }
36
+ function fileReadSkills(ref, ts, toolName, input, toolUseId) {
37
+ const extra = toolUseId ? { toolUseId } : {};
38
+ if (toolName === "Read") {
39
+ const filePath = str(input?.file_path);
40
+ const name = skillNameFromPath(filePath);
41
+ return name ? [nativeSkill(ref, ts, name, "file_read", extra, { path: filePath, tool: "Read" })] : [];
42
+ }
43
+ if (toolName === "Bash" || toolName === "shell" || toolName === "local_shell") {
44
+ return skillNamesInCommand(commandText(input?.command)).map((name) => nativeSkill(ref, ts, name, "file_read", extra, { tool: toolName }));
45
+ }
46
+ return [];
47
+ }
48
+ function claudeCode(p, ts) {
49
+ const ref = { runtime: "claude-code", runtimeSessionId: str(p.session_id), cwd: str(p.cwd) };
50
+ const events = [];
51
+ const bindings = [];
52
+ switch (p.hook_event_name) {
53
+ case "UserPromptSubmit":
54
+ events.push(promptEvent(ref, ts, p.prompt, str(p.prompt_id) ? { promptId: p.prompt_id } : {}));
55
+ break;
56
+ case "SessionStart":
57
+ events.push(makeEvent("session_start", "hook", ref, ts, { data: { source: p.source } }));
58
+ break;
59
+ case "SessionEnd":
60
+ events.push(makeEvent("session_end", "hook", ref, ts, { data: { reason: p.reason } }));
61
+ break;
62
+ case "UserPromptExpansion": {
63
+ const name = str(p.command_name);
64
+ if (name && p.expansion_type === "slash_command")
65
+ events.push(nativeSkill(ref, ts, name, "expansion", {}, { commandSource: p.command_source }));
66
+ break;
67
+ }
68
+ case "PostToolUse": {
69
+ const toolName = str(p.tool_name);
70
+ const input = obj(p.tool_input);
71
+ const toolUseId = str(p.tool_use_id);
72
+ if (toolName?.startsWith("mcp__gisool__")) {
73
+ if (toolUseId && ref.runtimeSessionId)
74
+ bindings.push({ toolUseId, ref });
75
+ }
76
+ else if (toolName === "Skill") {
77
+ const name = str(input?.skill) ?? str(input?.skill_name) ?? str(input?.name);
78
+ if (name)
79
+ events.push(nativeSkill(ref, ts, name, "tool", toolUseId ? { toolUseId } : {}));
80
+ }
81
+ else {
82
+ events.push(...fileReadSkills(ref, ts, toolName, input, toolUseId));
83
+ }
84
+ break;
85
+ }
86
+ }
87
+ return { events, bindings };
88
+ }
89
+ function codex(p, ts) {
90
+ const ref = { runtime: "codex", runtimeSessionId: str(p.session_id), cwd: str(p.cwd) };
91
+ const events = [];
92
+ if (p.hook_event_name === "UserPromptSubmit")
93
+ events.push(promptEvent(ref, ts, p.prompt, str(p.turn_id) ? { turnId: p.turn_id } : {}));
94
+ else if (p.hook_event_name === "PostToolUse" && p.tool_name === "Bash")
95
+ events.push(...fileReadSkills(ref, ts, "Bash", obj(p.tool_input)));
96
+ else if (p.hook_event_name === "SessionStart")
97
+ events.push(makeEvent("session_start", "hook", ref, ts));
98
+ return { events, bindings: [] };
99
+ }
100
+ function gemini(p, ts) {
101
+ const ref = { runtime: "gemini", runtimeSessionId: str(p.session_id), cwd: str(p.cwd) };
102
+ const events = [];
103
+ switch (p.hook_event_name) {
104
+ case "BeforeAgent":
105
+ events.push(promptEvent(ref, ts, p.prompt));
106
+ break;
107
+ case "AfterTool": {
108
+ const input = obj(p.tool_input);
109
+ const name = p.tool_name === "activate_skill" ? str(input?.name) ?? str(input?.skill) : undefined;
110
+ if (name)
111
+ events.push(nativeSkill(ref, ts, name, "tool"));
112
+ break;
113
+ }
114
+ case "SessionStart":
115
+ events.push(makeEvent("session_start", "hook", ref, ts));
116
+ break;
117
+ case "SessionEnd":
118
+ events.push(makeEvent("session_end", "hook", ref, ts));
119
+ break;
120
+ }
121
+ return { events, bindings: [] };
122
+ }
123
+ function cursor(p, ts) {
124
+ const roots = Array.isArray(p.workspace_roots) ? p.workspace_roots : [];
125
+ const ref = { runtime: "cursor", conversationId: str(p.conversation_id), cwd: str(roots[0]) ?? str(p.cwd) };
126
+ const events = [];
127
+ if (p.hook_event_name === "beforeSubmitPrompt")
128
+ events.push(promptEvent(ref, ts, p.prompt, str(p.generation_id) ? { generationId: p.generation_id } : {}));
129
+ else if (p.hook_event_name === "sessionStart")
130
+ events.push(makeEvent("session_start", "hook", ref, ts));
131
+ else if (p.hook_event_name === "sessionEnd")
132
+ events.push(makeEvent("session_end", "hook", ref, ts));
133
+ return { events, bindings: [] };
134
+ }
135
+ function piForwarder(runtime, p, ts) {
136
+ if (p.event !== "before_agent_start")
137
+ return { events: [], bindings: [] };
138
+ const ref = { runtime, runtimeSessionId: str(p.sessionId), cwd: str(p.cwd) };
139
+ return { events: [promptEvent(ref, ts, p.prompt)], bindings: [] };
140
+ }
141
+ function openclaw(p, ts) {
142
+ if (p.event !== "before_agent_run" && p.event !== "message_received")
143
+ return { events: [], bindings: [] };
144
+ const sessionKey = str(p.sessionKey);
145
+ const ref = { runtime: "openclaw", runtimeSessionId: str(p.sessionId) ?? sessionKey };
146
+ return { events: [promptEvent(ref, ts, p.prompt, { hook: p.event, ...(sessionKey ? { sessionKey } : {}) })], bindings: [] };
147
+ }
148
+ /** Normalizes one raw hook payload; unrecognized events yield no events (the pipeline counts them as dropped). */
149
+ export function normalizeHook(runtime, payload, now) {
150
+ const p = payload;
151
+ switch (runtime) {
152
+ case "claude-code":
153
+ return claudeCode(p, now);
154
+ case "codex":
155
+ return codex(p, now);
156
+ case "gemini":
157
+ return gemini(p, now);
158
+ case "cursor":
159
+ return cursor(p, now);
160
+ case "omo":
161
+ case "gajae":
162
+ return piForwarder(runtime, p, now);
163
+ case "openclaw":
164
+ return openclaw(p, now);
165
+ case "unknown":
166
+ return { events: [], bindings: [] };
167
+ }
168
+ }
@@ -0,0 +1,41 @@
1
+ import { makeEvent, str } from "./hooks.js";
2
+ import { runtimeFromClientName } from "./runtimes.js";
3
+ /**
4
+ * Vendor _meta keys: Codex sends unprefixed `threadId` (-> conversationId) and `sessionId` (-> runtimeSessionId);
5
+ * Claude Code sends `claudecode/toolUseId`; OpenCode dev builds send `ai.opencode/sessionID`.
6
+ */
7
+ export function mcpBase(ctx) {
8
+ const meta = ctx.meta ?? {};
9
+ const ref = { runtime: runtimeFromClientName(ctx.clientName) };
10
+ if (ctx.clientName)
11
+ ref.clientName = ctx.clientName;
12
+ const runtimeSessionId = str(meta.sessionId) ?? str(meta["ai.opencode/sessionID"]);
13
+ const conversationId = str(meta.threadId);
14
+ if (runtimeSessionId)
15
+ ref.runtimeSessionId = runtimeSessionId;
16
+ if (conversationId)
17
+ ref.conversationId = conversationId;
18
+ const toolUseId = str(meta["claudecode/toolUseId"]);
19
+ const data = ctx.mcpSessionId ? { mcpSessionId: ctx.mcpSessionId } : {};
20
+ return toolUseId ? { ref, toolUseId, data } : { ref, data };
21
+ }
22
+ function build(kind, ctx, ts, extra, data) {
23
+ const base = mcpBase(ctx);
24
+ return makeEvent(kind, "mcp", base.ref, ts, { ...(base.toolUseId ? { toolUseId: base.toolUseId } : {}), ...extra, data: { ...base.data, ...data } });
25
+ }
26
+ export function searchEvent(ctx, outcome, opts, ts) {
27
+ const data = {
28
+ query: outcome.query, mode: outcome.mode, candidates: outcome.candidates, elapsedMs: outcome.elapsedMs, total: opts.total, chosen: opts.chosen ?? [],
29
+ ...(outcome.abstained ? { abstained: outcome.abstained } : {}),
30
+ };
31
+ return build("search", ctx, ts, { traceId: outcome.traceId, text: outcome.query }, data);
32
+ }
33
+ export function loadEvent(ctx, load, ts) {
34
+ return build("load", ctx, ts, { skillUri: load.uri, loadId: load.loadId, ...(load.traceId ? { traceId: load.traceId } : {}) }, { uri: load.uri, loadId: load.loadId, manifestDigest: load.manifestDigest });
35
+ }
36
+ export function readFileEvent(ctx, read, ts) {
37
+ return build("read_file", ctx, ts, { skillUri: read.skillUri, ...(read.loadId ? { loadId: read.loadId } : {}), ...(read.traceId ? { traceId: read.traceId } : {}) }, { skillUri: read.skillUri, uri: read.uri, bytes: read.bytes });
38
+ }
39
+ export function errorEvent(ctx, err, ts) {
40
+ return build("error", ctx, ts, { ...(err.traceId ? { traceId: err.traceId } : {}), ...(err.skillUri ? { skillUri: err.skillUri } : {}), ...(err.loadId ? { loadId: err.loadId } : {}) }, { code: err.code, message: err.message, ...(err.tool ? { tool: err.tool } : {}) });
41
+ }
@@ -0,0 +1,160 @@
1
+ import { makeEvent, skillNameFromPath, skillNamesInCommand, str } from "./hooks.js";
2
+ function decode(value) {
3
+ if (!value || typeof value !== "object")
4
+ return undefined;
5
+ if (value.stringValue !== undefined)
6
+ return value.stringValue;
7
+ if (value.boolValue !== undefined)
8
+ return value.boolValue;
9
+ if (value.intValue !== undefined)
10
+ return Number(value.intValue);
11
+ if (value.doubleValue !== undefined)
12
+ return value.doubleValue;
13
+ if (value.arrayValue)
14
+ return (value.arrayValue.values ?? []).map(decode);
15
+ if (value.kvlistValue)
16
+ return toAttrs(value.kvlistValue.values);
17
+ if (value.bytesValue !== undefined)
18
+ return value.bytesValue;
19
+ return undefined;
20
+ }
21
+ function toAttrs(list) {
22
+ const out = {};
23
+ if (!Array.isArray(list))
24
+ return out;
25
+ for (const kv of list)
26
+ if (kv && typeof kv.key === "string")
27
+ out[kv.key] = decode(kv.value);
28
+ return out;
29
+ }
30
+ function parseJsonObject(value) {
31
+ if (typeof value !== "string")
32
+ return undefined;
33
+ try {
34
+ const parsed = JSON.parse(value);
35
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
36
+ }
37
+ catch {
38
+ return undefined; // tool_input is truncated at ~4 KB by Claude Code, so unparseable JSON is expected and simply carries no path.
39
+ }
40
+ }
41
+ function tsOf(record, now) {
42
+ const nanos = Number(record.timeUnixNano ?? 0) || Number(record.observedTimeUnixNano ?? 0);
43
+ return nanos > 0 ? Math.floor(nanos / 1e6) : now;
44
+ }
45
+ function eventNameOf(record, attrs, serviceHint) {
46
+ const candidates = [record.eventName, attrs["event.name"], decode(record.body)].filter((v) => typeof v === "string" && v.length > 0);
47
+ const prefixed = candidates.find((n) => n.startsWith("claude_code.") || n.startsWith("codex."));
48
+ if (prefixed)
49
+ return prefixed;
50
+ const bare = candidates[0];
51
+ if (!bare)
52
+ return undefined;
53
+ return serviceHint.includes("codex") ? `codex.${bare}` : `claude_code.${bare}`;
54
+ }
55
+ function promptText(attrs) {
56
+ const text = str(attrs.prompt) ?? str(attrs.prompt_text);
57
+ return text === "<REDACTED>" ? undefined : text;
58
+ }
59
+ // Claude Code tool_result attributes (https://code.claude.com/docs/en/monitoring-usage.md, "Tool result event"):
60
+ // `tool_name`, `tool_use_id`, and with OTEL_LOG_TOOL_DETAILS=1 `tool_parameters` (JSON string: Bash `bash_command`/`full_command`,
61
+ // MCP `mcp_server_name`/`mcp_tool_name`, Skill `skill_name`) and `tool_input` (JSON-serialized arguments, e.g. Read `file_path`).
62
+ function claudeToolResult(attrs, ref, ts) {
63
+ const toolName = str(attrs.tool_name);
64
+ const params = parseJsonObject(attrs.tool_parameters) ?? {};
65
+ const input = parseJsonObject(attrs.tool_input) ?? {};
66
+ const mcpServer = str(attrs.mcp_server_name) ?? str(params.mcp_server_name) ?? "";
67
+ if (mcpServer.toLowerCase().includes("gisool") || toolName?.startsWith("mcp__gisool__"))
68
+ return "ignored";
69
+ const toolUseId = str(attrs.tool_use_id);
70
+ const extra = toolUseId ? { toolUseId } : {};
71
+ if (toolName === "Read") {
72
+ const filePath = str(input.file_path) ?? str(params.file_path);
73
+ const name = skillNameFromPath(filePath);
74
+ return name ? [makeEvent("native_skill", "otel", ref, ts, { ...extra, data: { name, via: "file_read", path: filePath, tool: "Read" } })] : [];
75
+ }
76
+ if (toolName === "Bash") {
77
+ const command = str(params.full_command) ?? str(params.bash_command) ?? str(input.command);
78
+ return skillNamesInCommand(command).map((name) => makeEvent("native_skill", "otel", ref, ts, { ...extra, data: { name, via: "file_read", tool: "Bash" } }));
79
+ }
80
+ return [];
81
+ }
82
+ function mapRecord(name, attrs, ts) {
83
+ if (name.startsWith("claude_code.")) {
84
+ const ref = { runtime: "claude-code", runtimeSessionId: str(attrs["session.id"]) };
85
+ switch (name) {
86
+ case "claude_code.user_prompt": {
87
+ const text = promptText(attrs);
88
+ const promptLength = typeof attrs.prompt_length === "number" ? attrs.prompt_length : undefined;
89
+ const data = { promptId: str(attrs["prompt.id"]) };
90
+ if (text === undefined)
91
+ Object.assign(data, { promptUnavailable: true, promptLength });
92
+ return [makeEvent("prompt", "otel", ref, ts, { ...(text !== undefined ? { text } : {}), data })];
93
+ }
94
+ case "claude_code.skill_activated": {
95
+ const skill = str(attrs["skill.name"]);
96
+ if (!skill)
97
+ return "ignored";
98
+ return [makeEvent("native_skill", "otel", ref, ts, { data: { name: skill, via: "otel", trigger: str(attrs.invocation_trigger), source: str(attrs["skill.source"]) } })];
99
+ }
100
+ case "claude_code.tool_result":
101
+ return claudeToolResult(attrs, ref, ts);
102
+ default:
103
+ return "unknown";
104
+ }
105
+ }
106
+ if (name.startsWith("codex.")) {
107
+ const ref = { runtime: "codex", conversationId: str(attrs["conversation.id"]) };
108
+ switch (name) {
109
+ case "codex.user_prompt": {
110
+ const text = promptText(attrs);
111
+ return [makeEvent("prompt", "otel", ref, ts, text !== undefined ? { text } : { data: { promptUnavailable: true } })];
112
+ }
113
+ case "codex.skill_invocation": {
114
+ const skill = str(attrs["skill.name"]);
115
+ if (!skill)
116
+ return "ignored";
117
+ return [makeEvent("native_skill", "otel", ref, ts, { data: { name: skill, via: "otel", scope: str(attrs["skill.scope"]), invocationType: str(attrs["skill.invocation_type"]) } })];
118
+ }
119
+ case "codex.conversation_starts":
120
+ return [makeEvent("session_start", "otel", ref, ts)];
121
+ default:
122
+ return "unknown";
123
+ }
124
+ }
125
+ return "unknown";
126
+ }
127
+ function badOtlp(message) {
128
+ return { ok: false, status: 400, error: { code: "bad_otlp", message } };
129
+ }
130
+ /** Parses an OTLP/HTTP JSON logs export into CanonicalEvents; unknown event names are counted, not stored. */
131
+ export function parseOtlpLogs(body, now) {
132
+ if (body === null || typeof body !== "object" || Array.isArray(body))
133
+ return badOtlp("body must be an ExportLogsServiceRequest JSON object");
134
+ const resourceLogs = body.resourceLogs;
135
+ if (!Array.isArray(resourceLogs))
136
+ return badOtlp("resourceLogs must be an array");
137
+ const events = [];
138
+ let unknown = 0;
139
+ let ignored = 0;
140
+ for (const rl of resourceLogs) {
141
+ const resourceAttrs = toAttrs(rl?.resource?.attributes);
142
+ const scopeLogs = Array.isArray(rl?.scopeLogs) ? rl.scopeLogs : [];
143
+ for (const sl of scopeLogs) {
144
+ const serviceHint = `${String(resourceAttrs["service.name"] ?? "")} ${sl?.scope?.name ?? ""}`.toLowerCase();
145
+ const records = Array.isArray(sl?.logRecords) ? sl.logRecords : [];
146
+ for (const record of records) {
147
+ const attrs = { ...resourceAttrs, ...toAttrs(record?.attributes) };
148
+ const name = record ? eventNameOf(record, attrs, serviceHint) : undefined;
149
+ const mapped = name ? mapRecord(name, attrs, tsOf(record, now)) : "unknown";
150
+ if (mapped === "unknown")
151
+ unknown += 1;
152
+ else if (mapped === "ignored")
153
+ ignored += 1;
154
+ else
155
+ events.push(...mapped);
156
+ }
157
+ }
158
+ }
159
+ return { ok: true, events, unknown, ignored, response: { partialSuccess: {} } };
160
+ }
@@ -0,0 +1,100 @@
1
+ import { attribute, createAttributionState, remember } from "./attribution.js";
2
+ import { normalizeHook } from "./hooks.js";
3
+ import { parseOtlpLogs } from "./otlp.js";
4
+ import { applyCapture } from "./redact.js";
5
+ export function createIngestDeps(store, opts = {}) {
6
+ return { store, captureLevel: opts.captureLevel ?? "full", state: opts.state ?? createAttributionState(), publish: opts.publish, log: opts.log, now: opts.now };
7
+ }
8
+ function nowOf(deps) {
9
+ return deps.now ? deps.now() : Date.now();
10
+ }
11
+ function logError(deps, message, error) {
12
+ if (deps.log)
13
+ deps.log(message, error);
14
+ else
15
+ console.error(`[gisool ingest] ${message}`, error);
16
+ }
17
+ function count(deps, kind, code) {
18
+ try {
19
+ deps.store.countIngest(kind, code);
20
+ }
21
+ catch (error) {
22
+ logError(deps, `countIngest(${kind}) failed`, error);
23
+ }
24
+ }
25
+ function capture(event, level) {
26
+ if (event.text === undefined)
27
+ return event;
28
+ const captured = applyCapture(level, event.text);
29
+ const { text: _dropped, ...rest } = event;
30
+ const next = { ...rest, textDigest: captured.textDigest, textLength: captured.textLength };
31
+ if (captured.text !== undefined)
32
+ next.text = captured.text;
33
+ if (event.kind === "search" && typeof event.data.query === "string")
34
+ next.data = { ...event.data, query: captured.text ?? "" };
35
+ return next;
36
+ }
37
+ /**
38
+ * capture -> attribute -> store.append -> publish, fully synchronous. Errors are logged, counted as
39
+ * rejected `ingest_error`, and swallowed so one bad event never breaks a batch or blocks an agent.
40
+ */
41
+ export function ingest(event, deps) {
42
+ try {
43
+ const level = typeof deps.captureLevel === "function" ? deps.captureLevel() : deps.captureLevel;
44
+ const captured = capture(event, level);
45
+ const attribution = attribute(captured, deps.store, nowOf(deps), deps.state);
46
+ deps.store.append(captured, attribution);
47
+ if (captured.loadId && attribution.sessionId !== "unattributed")
48
+ remember(deps.state.loadSessions, captured.loadId, attribution.sessionId);
49
+ count(deps, "accepted");
50
+ if (deps.publish && attribution.sessionId !== "unattributed")
51
+ deps.publish({ sessionId: attribution.sessionId, attribution, event: captured });
52
+ return attribution;
53
+ }
54
+ catch (error) {
55
+ logError(deps, `failed to ingest ${event.kind} event ${event.id}`, error);
56
+ count(deps, "rejected", "ingest_error");
57
+ return undefined;
58
+ }
59
+ }
60
+ /** Binds Claude tool_use_ids to sessions in either arrival order and ingests the hook's events. */
61
+ export function ingestHook(runtime, payload, deps) {
62
+ const now = nowOf(deps);
63
+ const { events, bindings } = normalizeHook(runtime, payload, now);
64
+ for (const binding of bindings) {
65
+ try {
66
+ const sessionId = deps.store.upsertSession(binding.ref, now);
67
+ remember(deps.state.toolUseSessions, binding.toolUseId, sessionId);
68
+ const eventId = deps.store.findEventByToolUseId(binding.toolUseId);
69
+ if (eventId) {
70
+ deps.store.reattribute(eventId, sessionId, "vendor_meta");
71
+ deps.publish?.({ sessionId, attribution: { sessionId, method: "vendor_meta" } });
72
+ }
73
+ }
74
+ catch (error) {
75
+ logError(deps, `failed to bind tool_use_id ${binding.toolUseId}`, error);
76
+ count(deps, "rejected", "ingest_error");
77
+ }
78
+ }
79
+ if (events.length === 0 && bindings.length === 0) {
80
+ count(deps, "dropped", "unknown_event");
81
+ return { accepted: 0, dropped: 1 };
82
+ }
83
+ let accepted = 0;
84
+ for (const event of events)
85
+ if (ingest(event, deps))
86
+ accepted += 1;
87
+ return { accepted, dropped: 0 };
88
+ }
89
+ export function ingestOtlp(body, deps) {
90
+ const parsed = parseOtlpLogs(body, nowOf(deps));
91
+ if (!parsed.ok) {
92
+ count(deps, "rejected", parsed.error.code);
93
+ return { status: 400, body: parsed.error };
94
+ }
95
+ for (let i = 0; i < parsed.unknown; i += 1)
96
+ count(deps, "dropped", "unknown_event");
97
+ for (const event of parsed.events)
98
+ ingest(event, deps);
99
+ return { status: 200, body: parsed.response };
100
+ }
@@ -0,0 +1,36 @@
1
+ import { createHash } from "node:crypto";
2
+ export const REDACTED_MAX_CHARS = 2048;
3
+ export const MASK = "[REDACTED]";
4
+ export const SECRET_PATTERNS = [
5
+ /sk-[A-Za-z0-9]{20,}/g,
6
+ /ghp_[A-Za-z0-9]{30,}/g,
7
+ /AKIA[0-9A-Z]{16}/g,
8
+ /xox[baprs]-[A-Za-z0-9-]+/g,
9
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
10
+ /Bearer [A-Za-z0-9._-]{20,}/g,
11
+ ];
12
+ export function sha256Hex(text) {
13
+ return createHash("sha256").update(text, "utf8").digest("hex");
14
+ }
15
+ export function maskSecrets(text) {
16
+ let out = text;
17
+ for (const pattern of SECRET_PATTERNS)
18
+ out = out.replace(pattern, MASK);
19
+ return out;
20
+ }
21
+ /**
22
+ * full -> text unchanged; redacted -> secrets masked then truncated to 2048 chars;
23
+ * metadata -> text dropped. Digest and length always describe the original text.
24
+ */
25
+ export function applyCapture(level, text) {
26
+ const textDigest = sha256Hex(text);
27
+ const textLength = text.length;
28
+ switch (level) {
29
+ case "full":
30
+ return { text, textDigest, textLength };
31
+ case "redacted":
32
+ return { text: maskSecrets(text).slice(0, REDACTED_MAX_CHARS), textDigest, textLength };
33
+ case "metadata":
34
+ return { textDigest, textLength };
35
+ }
36
+ }
@@ -0,0 +1,25 @@
1
+ const EXACT = {
2
+ "claude-code": "claude-code",
3
+ "codex-mcp-client": "codex",
4
+ "gemini-cli-mcp-client": "gemini",
5
+ "senpi-mcp-client": "omo", // verified OmO 5 client name
6
+ opencode: "omo",
7
+ "gjc-coding-agent": "gajae",
8
+ };
9
+ export function runtimeFromClientName(name) {
10
+ if (!name)
11
+ return "unknown";
12
+ const exact = EXACT[name];
13
+ if (exact)
14
+ return exact;
15
+ const lower = name.toLowerCase();
16
+ if (lower.startsWith("cursor"))
17
+ return "cursor";
18
+ if (lower.includes("openclaw"))
19
+ return "openclaw";
20
+ return "unknown";
21
+ }
22
+ export const RUNTIME_IDS = ["claude-code", "codex", "gemini", "cursor", "omo", "gajae", "openclaw", "unknown"];
23
+ export function isRuntimeId(value) {
24
+ return RUNTIME_IDS.includes(value);
25
+ }
@@ -0,0 +1,84 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import express from "express";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
5
+ import { bearerOf, safeEqual } from "../api/security.js";
6
+ import { createMcpServer } from "./tools.js";
7
+ export const SESSION_IDLE_MS = 24 * 60 * 60 * 1000;
8
+ function rpcError(res, status, message) {
9
+ res.status(status).json({ jsonrpc: "2.0", error: { code: -32000, message }, id: null });
10
+ }
11
+ export function registerMcp(app, deps) {
12
+ const sessions = new Map();
13
+ const idleMs = deps.idleMs ?? SESSION_IDLE_MS;
14
+ const auth = (req, res, next) => {
15
+ if (deps.token && !safeEqual(bearerOf(req), deps.token)) {
16
+ res.status(401).json({ error: "unauthorized" });
17
+ return;
18
+ }
19
+ next();
20
+ };
21
+ const existing = (req) => {
22
+ const id = req.header("mcp-session-id");
23
+ const session = id ? sessions.get(id) : undefined;
24
+ if (session)
25
+ session.lastSeen = Date.now();
26
+ return session;
27
+ };
28
+ const closeSession = async (id) => {
29
+ const session = sessions.get(id);
30
+ if (!session)
31
+ return;
32
+ sessions.delete(id);
33
+ await session.transport.close().catch(() => undefined);
34
+ await session.server.close().catch(() => undefined);
35
+ };
36
+ app.post("/mcp", auth, express.json({ limit: "4mb" }), async (req, res) => {
37
+ const id = req.header("mcp-session-id");
38
+ let session = existing(req);
39
+ if (id && !session)
40
+ return rpcError(res, 404, "Unknown or expired MCP session; reinitialize");
41
+ if (!session) {
42
+ if (!isInitializeRequest(req.body))
43
+ return rpcError(res, 400, "Missing mcp-session-id; send initialize first");
44
+ const server = createMcpServer(deps);
45
+ const transport = new StreamableHTTPServerTransport({
46
+ sessionIdGenerator: randomUUID,
47
+ onsessioninitialized: (sid) => { sessions.set(sid, created); },
48
+ });
49
+ const created = { server, transport, lastSeen: Date.now() };
50
+ transport.onclose = () => {
51
+ const sid = transport.sessionId;
52
+ if (sid && sessions.get(sid) === created)
53
+ sessions.delete(sid);
54
+ };
55
+ await server.connect(transport);
56
+ session = created;
57
+ }
58
+ await session.transport.handleRequest(req, res, req.body);
59
+ });
60
+ const handleSessionRequest = async (req, res) => {
61
+ const session = existing(req);
62
+ if (!session)
63
+ return rpcError(res, req.header("mcp-session-id") ? 404 : 400, "Unknown or missing mcp-session-id");
64
+ await session.transport.handleRequest(req, res);
65
+ };
66
+ app.get("/mcp", auth, handleSessionRequest);
67
+ app.delete("/mcp", auth, handleSessionRequest);
68
+ const sweep = (now = Date.now()) => {
69
+ const stale = [...sessions.entries()].filter(([, s]) => now - s.lastSeen > idleMs).map(([sid]) => sid);
70
+ for (const sid of stale)
71
+ void closeSession(sid);
72
+ return stale.length;
73
+ };
74
+ const timer = setInterval(sweep, deps.sweepMs ?? 60 * 60 * 1000);
75
+ timer.unref();
76
+ return {
77
+ sessionCount: () => sessions.size,
78
+ sweep,
79
+ async close() {
80
+ clearInterval(timer);
81
+ await Promise.all([...sessions.keys()].map(closeSession));
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,35 @@
1
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
+ import { z } from "zod";
3
+ export const SKILLS_EXTENSION_ID = "io.modelcontextprotocol/skills";
4
+ const SkillsListRequestSchema = z.object({
5
+ method: z.literal("skills/list"),
6
+ params: z.object({ cursor: z.string().optional() }).passthrough().optional(),
7
+ });
8
+ const SkillsGetRequestSchema = z.object({
9
+ method: z.literal("skills/get"),
10
+ params: z.object({ uri: z.string() }).passthrough(),
11
+ });
12
+ function toWire(entry) {
13
+ return {
14
+ uri: entry.uri,
15
+ frontmatter: {
16
+ name: entry.name,
17
+ description: entry.description,
18
+ ...(entry.invocation === "explicit" ? { "disable-model-invocation": true } : {}),
19
+ ...(entry.tags.length ? { keywords: entry.tags } : {}),
20
+ },
21
+ resources: entry.resources.map(({ uri, digest, size }) => ({ uri, digest, size })),
22
+ };
23
+ }
24
+ export function registerSkillsExtension(server, catalog) {
25
+ server.server.registerCapabilities({ extensions: { [SKILLS_EXTENSION_ID]: {} } });
26
+ server.server.setRequestHandler(SkillsListRequestSchema, async () => ({
27
+ skills: [...catalog.list()].sort((a, b) => a.name.localeCompare(b.name)).map(toWire),
28
+ }));
29
+ server.server.setRequestHandler(SkillsGetRequestSchema, async (request) => {
30
+ const entry = catalog.get(request.params.uri);
31
+ if (!entry)
32
+ throw new McpError(ErrorCode.InvalidParams, `Not a skill served by this server: ${request.params.uri}`);
33
+ return { skill: toWire(entry) };
34
+ });
35
+ }