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,172 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function parseJson(text, fallback) {
|
|
3
|
+
if (typeof text !== "string" || text === "")
|
|
4
|
+
return fallback;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(text);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function nameFromUri(uri) {
|
|
13
|
+
const parts = uri.split("/").filter(Boolean);
|
|
14
|
+
const last = parts[parts.length - 1];
|
|
15
|
+
return (last === "SKILL.md" ? parts[parts.length - 2] : last) ?? uri;
|
|
16
|
+
}
|
|
17
|
+
export function storeError(code, message = code) {
|
|
18
|
+
return Object.assign(new Error(message), { code });
|
|
19
|
+
}
|
|
20
|
+
const DAY_MS = 86_400_000;
|
|
21
|
+
function refFromRow(row) {
|
|
22
|
+
const ref = { runtime: row.runtime };
|
|
23
|
+
if (row.runtime_session_id !== null)
|
|
24
|
+
ref.runtimeSessionId = row.runtime_session_id;
|
|
25
|
+
if (row.conversation_id !== null)
|
|
26
|
+
ref.conversationId = row.conversation_id;
|
|
27
|
+
if (row.client_name !== null)
|
|
28
|
+
ref.clientName = row.client_name;
|
|
29
|
+
if (row.cwd !== null)
|
|
30
|
+
ref.cwd = row.cwd;
|
|
31
|
+
return ref;
|
|
32
|
+
}
|
|
33
|
+
function getMeta(db, key) {
|
|
34
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
35
|
+
return row?.value ?? undefined;
|
|
36
|
+
}
|
|
37
|
+
function setMeta(db, key, value) {
|
|
38
|
+
db.prepare("INSERT INTO meta(key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
|
|
39
|
+
}
|
|
40
|
+
export function readIngestCounters(db) {
|
|
41
|
+
const num = (k) => Number(getMeta(db, `ingest.${k}`) ?? 0);
|
|
42
|
+
const lastError = parseJson(getMeta(db, "ingest.lastError"), undefined);
|
|
43
|
+
return { accepted: num("accepted"), rejected: num("rejected"), dropped: num("dropped"), ...(lastError ? { lastError } : {}) };
|
|
44
|
+
}
|
|
45
|
+
export function readAliasesOverrides(db) {
|
|
46
|
+
return parseJson(getMeta(db, "aliases_overrides"), {});
|
|
47
|
+
}
|
|
48
|
+
export function writeAliasesOverrides(db, value) {
|
|
49
|
+
setMeta(db, "aliases_overrides", JSON.stringify(value));
|
|
50
|
+
}
|
|
51
|
+
export function createWrites(ctx) {
|
|
52
|
+
const { db } = ctx;
|
|
53
|
+
const insertEvent = db.prepare(`INSERT OR IGNORE INTO events
|
|
54
|
+
(id, ts, kind, source, session_id, attribution_method, attribution_reason, runtime, trace_id, load_id, skill_uri, tool_use_id, text, text_digest, text_length, data_json)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
56
|
+
const setFirstPrompt = db.prepare("UPDATE sessions SET first_prompt = ? WHERE id = ? AND first_prompt IS NULL");
|
|
57
|
+
const setEnded = db.prepare("UPDATE sessions SET ended_at = MAX(COALESCE(ended_at, 0), ?) WHERE id = ?");
|
|
58
|
+
function sessionSideEffects(sessionId, kind, ts, text) {
|
|
59
|
+
if (sessionId === "unattributed")
|
|
60
|
+
return;
|
|
61
|
+
if (kind === "prompt" && text !== null)
|
|
62
|
+
setFirstPrompt.run(text, sessionId);
|
|
63
|
+
if (kind === "session_end")
|
|
64
|
+
setEnded.run(ts, sessionId);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
append(event, attribution) {
|
|
68
|
+
const text = event.text ?? null;
|
|
69
|
+
insertEvent.run(event.id, event.ts, event.kind, event.source, attribution.sessionId, attribution.method, attribution.reason ?? null, event.ref.runtime, event.traceId ?? null, event.loadId ?? null, event.skillUri ?? null, event.toolUseId ?? null, text, event.textDigest ?? null, event.textLength ?? null, JSON.stringify(event.data ?? {}));
|
|
70
|
+
sessionSideEffects(attribution.sessionId, event.kind, event.ts, text);
|
|
71
|
+
},
|
|
72
|
+
openSessions(runtime, since) {
|
|
73
|
+
const rows = db.prepare(`SELECT s.*, MAX(s.started_at, COALESCE(MAX(e.ts), s.started_at)) AS last_activity
|
|
74
|
+
FROM sessions s LEFT JOIN events e ON e.session_id = s.id
|
|
75
|
+
WHERE s.runtime = ? AND s.ended_at IS NULL GROUP BY s.id HAVING last_activity >= ? ORDER BY last_activity DESC`)
|
|
76
|
+
.all(runtime, since);
|
|
77
|
+
return rows.map((r) => ({ id: r.id, startedAt: r.started_at, lastActivityAt: r.last_activity, ref: refFromRow(r) }));
|
|
78
|
+
},
|
|
79
|
+
reattribute(eventId, sessionId, method) {
|
|
80
|
+
db.prepare("UPDATE events SET session_id = ?, attribution_method = ?, attribution_reason = NULL WHERE id = ?").run(sessionId, method, eventId);
|
|
81
|
+
const ev = db.prepare("SELECT kind, ts, text FROM events WHERE id = ?").get(eventId);
|
|
82
|
+
if (ev)
|
|
83
|
+
sessionSideEffects(sessionId, ev.kind, ev.ts, ev.text);
|
|
84
|
+
},
|
|
85
|
+
findEventByToolUseId(toolUseId) {
|
|
86
|
+
const row = db.prepare("SELECT id FROM events WHERE tool_use_id = ? ORDER BY ts DESC, rowid DESC LIMIT 1").get(toolUseId);
|
|
87
|
+
return row?.id;
|
|
88
|
+
},
|
|
89
|
+
upsertSession(ref, startedAt) {
|
|
90
|
+
const key = ref.runtimeSessionId ?? ref.conversationId;
|
|
91
|
+
if (key !== undefined) {
|
|
92
|
+
const column = ref.runtimeSessionId !== undefined ? "runtime_session_id" : "conversation_id";
|
|
93
|
+
const existing = db.prepare(`SELECT * FROM sessions WHERE runtime = ? AND ${column} = ? ORDER BY started_at LIMIT 1`).get(ref.runtime, key);
|
|
94
|
+
if (existing) {
|
|
95
|
+
db.prepare(`UPDATE sessions SET started_at = MIN(started_at, ?), conversation_id = COALESCE(conversation_id, ?),
|
|
96
|
+
runtime_session_id = COALESCE(runtime_session_id, ?), client_name = COALESCE(client_name, ?), cwd = COALESCE(cwd, ?) WHERE id = ?`)
|
|
97
|
+
.run(startedAt, ref.conversationId ?? null, ref.runtimeSessionId ?? null, ref.clientName ?? null, ref.cwd ?? null, existing.id);
|
|
98
|
+
return existing.id;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const id = randomUUID();
|
|
102
|
+
db.prepare("INSERT INTO sessions (id, runtime, runtime_session_id, conversation_id, client_name, cwd, started_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
103
|
+
.run(id, ref.runtime, ref.runtimeSessionId ?? null, ref.conversationId ?? null, ref.clientName ?? null, ref.cwd ?? null, startedAt);
|
|
104
|
+
return id;
|
|
105
|
+
},
|
|
106
|
+
putLoad(load) {
|
|
107
|
+
db.prepare(`INSERT OR REPLACE INTO loads (load_id, trace_id, uri, manifest_digest, resources_json, created_at) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
108
|
+
.run(load.loadId, load.traceId ?? null, load.uri, load.manifestDigest, JSON.stringify(load.resources), ctx.now());
|
|
109
|
+
},
|
|
110
|
+
getLoad(loadId) {
|
|
111
|
+
const row = db.prepare("SELECT uri, manifest_digest, resources_json FROM loads WHERE load_id = ?").get(loadId);
|
|
112
|
+
if (!row)
|
|
113
|
+
return undefined;
|
|
114
|
+
return { uri: row.uri, manifestDigest: row.manifest_digest, resources: parseJson(row.resources_json, []) };
|
|
115
|
+
},
|
|
116
|
+
latestLoad(uri) {
|
|
117
|
+
const row = db.prepare("SELECT load_id FROM loads WHERE uri = ? ORDER BY created_at DESC, rowid DESC LIMIT 1").get(uri);
|
|
118
|
+
return row?.load_id;
|
|
119
|
+
},
|
|
120
|
+
traceSession(traceId) {
|
|
121
|
+
const row = db.prepare("SELECT session_id FROM events WHERE trace_id = ? AND session_id != 'unattributed' ORDER BY ts, rowid LIMIT 1").get(traceId);
|
|
122
|
+
return row?.session_id;
|
|
123
|
+
},
|
|
124
|
+
upsertSkills(entries) {
|
|
125
|
+
const stmt = db.prepare(`INSERT INTO skills (uri, root_id, name, description, tags_json, when_to_use, examples_json, aliases_ko_json, group_id, group_stage,
|
|
126
|
+
invocation, body_head, digest, manifest_digest, resources_json, mtime_ms, removed_at)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
128
|
+
ON CONFLICT(uri) DO UPDATE SET root_id = excluded.root_id, name = excluded.name, description = excluded.description, tags_json = excluded.tags_json,
|
|
129
|
+
when_to_use = excluded.when_to_use, examples_json = excluded.examples_json, aliases_ko_json = excluded.aliases_ko_json, group_id = excluded.group_id,
|
|
130
|
+
group_stage = excluded.group_stage, invocation = excluded.invocation, body_head = excluded.body_head, digest = excluded.digest,
|
|
131
|
+
manifest_digest = excluded.manifest_digest, resources_json = excluded.resources_json, mtime_ms = excluded.mtime_ms, removed_at = NULL`);
|
|
132
|
+
db.exec("BEGIN");
|
|
133
|
+
try {
|
|
134
|
+
for (const e of entries) {
|
|
135
|
+
stmt.run(e.uri, e.rootId, e.name, e.description, JSON.stringify(e.tags), e.whenToUse ?? null, JSON.stringify(e.examples), JSON.stringify(e.aliasesKo), e.group?.id ?? null, e.group?.stage ?? null, e.invocation, e.bodyHead, e.digest, e.manifestDigest, JSON.stringify(e.resources), e.mtimeMs);
|
|
136
|
+
}
|
|
137
|
+
db.exec("COMMIT");
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
db.exec("ROLLBACK");
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
markRemoved(uris) {
|
|
145
|
+
const stmt = db.prepare("UPDATE skills SET removed_at = ? WHERE uri = ? AND removed_at IS NULL");
|
|
146
|
+
const ts = ctx.now();
|
|
147
|
+
for (const uri of uris)
|
|
148
|
+
stmt.run(ts, uri);
|
|
149
|
+
},
|
|
150
|
+
countIngest(kind, code) {
|
|
151
|
+
db.prepare(`INSERT INTO meta(key, value) VALUES (?, '1') ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)`).run(`ingest.${kind}`);
|
|
152
|
+
if (kind !== "accepted" && code !== undefined)
|
|
153
|
+
setMeta(db, "ingest.lastError", JSON.stringify({ ts: ctx.now(), code }));
|
|
154
|
+
},
|
|
155
|
+
addEvalCase(c) {
|
|
156
|
+
db.prepare("INSERT INTO eval_cases (id, query, expected_uri, from_trace_id, created_at) VALUES (?, ?, ?, ?, ?)")
|
|
157
|
+
.run(randomUUID(), c.query, c.expectedUri ?? null, c.fromTraceId ?? null, ctx.now());
|
|
158
|
+
},
|
|
159
|
+
evalCases() {
|
|
160
|
+
const rows = db.prepare("SELECT query, expected_uri FROM eval_cases ORDER BY created_at, rowid").all();
|
|
161
|
+
return rows.map((r) => (r.expected_uri === null ? { query: r.query } : { query: r.query, expectedUri: r.expected_uri }));
|
|
162
|
+
},
|
|
163
|
+
retention(days) {
|
|
164
|
+
if (ctx.opts.config && ctx.opts.config.retentionDays === null)
|
|
165
|
+
return 0;
|
|
166
|
+
const cutoff = ctx.now() - days * DAY_MS;
|
|
167
|
+
const result = db.prepare("UPDATE events SET text = NULL WHERE ts < ? AND text IS NOT NULL").run(cutoff);
|
|
168
|
+
db.prepare("UPDATE sessions SET first_prompt = NULL WHERE started_at < ? AND first_prompt IS NOT NULL").run(cutoff);
|
|
169
|
+
return Number(result.changes);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class GisoolStartupError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message ? `${code}: ${message}` : code);
|
|
5
|
+
this.name = "GisoolStartupError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export const MIN_NODE = [24, 15];
|
|
10
|
+
export function assertNodeVersion(version = process.versions.node) {
|
|
11
|
+
const [major = 0, minor = 0] = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
12
|
+
if (major < MIN_NODE[0] || (major === MIN_NODE[0] && minor < MIN_NODE[1])) {
|
|
13
|
+
throw new GisoolStartupError("node_too_old", `gisool needs Node >= ${MIN_NODE.join(".")}, found ${version}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function assertPlatform(platform = process.platform) {
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
throw new GisoolStartupError("unsupported_platform", "gisool supports macOS and Linux");
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gisool",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Local skill router for coding agents: search your skills, trace how agents use them, and see the flow.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/changeroa/gisool.git",
|
|
10
|
+
"directory": "local"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/changeroa/gisool/tree/main/local#readme",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"packs",
|
|
16
|
+
"src/store/schema.sql",
|
|
17
|
+
"src/store/task-types.json",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"gisool": "dist/cli/main.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24.15"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json && node build-dashboard.mjs",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p dashboard/tsconfig.json",
|
|
30
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
31
|
+
"start": "node dist/cli/main.js serve"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
35
|
+
"express": "^5.2.1",
|
|
36
|
+
"yaml": "^2.9.0",
|
|
37
|
+
"zod": "^4.1.11"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/d3-sankey": "^0.12.4",
|
|
41
|
+
"@types/express": "^5.0.5",
|
|
42
|
+
"@types/node": "^24.3.0",
|
|
43
|
+
"d3-sankey": "0.12.3",
|
|
44
|
+
"esbuild": "^0.28.2",
|
|
45
|
+
"preact": "^10.27.2",
|
|
46
|
+
"typescript": "^5.9.2"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Claude Code capture pack
|
|
2
|
+
|
|
3
|
+
Template merged into `~/.claude/settings.json` by `gisool init`: OTel log export (`env`) plus
|
|
4
|
+
`PostToolUse` / `UserPromptExpansion` hooks that run `packs/hook-post.mjs`. `~/.claude.json` is never edited;
|
|
5
|
+
the MCP server is registered with `claude mcp add --scope user` (or printed as a manual step).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
|
|
4
|
+
"OTEL_LOGS_EXPORTER": "otlp",
|
|
5
|
+
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": "http/json",
|
|
6
|
+
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://127.0.0.1:{{PORT}}/ingest/otlp/v1/logs",
|
|
7
|
+
"OTEL_EXPORTER_OTLP_LOGS_HEADERS": "Authorization=Bearer {{TOKEN}}",
|
|
8
|
+
"OTEL_LOG_USER_PROMPTS": "1",
|
|
9
|
+
"OTEL_LOG_TOOL_DETAILS": "1"
|
|
10
|
+
},
|
|
11
|
+
"hooks": {
|
|
12
|
+
"PostToolUse": [
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Skill|mcp__gisool__.*",
|
|
15
|
+
"hooks": [{ "type": "command", "command": "\"{{NODE}}\" \"{{HOOK}}\" claude-code", "timeout": 5 }]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"UserPromptExpansion": [
|
|
19
|
+
{
|
|
20
|
+
"hooks": [{ "type": "command", "command": "\"{{NODE}}\" \"{{HOOK}}\" claude-code", "timeout": 5 }]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Codex capture pack
|
|
2
|
+
|
|
3
|
+
`config.template.toml` is the block `gisool init` keeps between `# >>> gisool managed` and `# <<< gisool managed`
|
|
4
|
+
in `~/.codex/config.toml` (MCP entry and `[otel]`). No Codex hooks are installed (they need trust review).
|
|
5
|
+
Note: `codex exec` exports OTel events only partially.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# >>> gisool managed
|
|
2
|
+
# Managed by gisool; edit via `gisool init`, not by hand.
|
|
3
|
+
[mcp_servers.gisool]
|
|
4
|
+
url = "http://127.0.0.1:{{PORT}}/mcp"
|
|
5
|
+
http_headers = { Authorization = "Bearer {{TOKEN}}" }
|
|
6
|
+
|
|
7
|
+
[otel]
|
|
8
|
+
log_user_prompt = true
|
|
9
|
+
exporter = { otlp-http = { endpoint = "http://127.0.0.1:{{PORT}}/ingest/otlp/v1/logs", protocol = "json", headers = { Authorization = "Bearer {{TOKEN}}" } } }
|
|
10
|
+
metrics_exporter = "none"
|
|
11
|
+
# <<< gisool managed
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"hooks": {
|
|
4
|
+
"beforeSubmitPrompt": [{ "command": "\"<nodePath>\" \"<packageRoot>/packs/hook-post.mjs\" cursor" }],
|
|
5
|
+
"sessionStart": [{ "command": "\"<nodePath>\" \"<packageRoot>/packs/hook-post.mjs\" cursor" }],
|
|
6
|
+
"sessionEnd": [{ "command": "\"<nodePath>\" \"<packageRoot>/packs/hook-post.mjs\" cursor" }]
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# gisool discovery
|
|
2
|
+
|
|
3
|
+
gisool = local skills on this machine; gisul = remote team skills. They are separate catalogs: search both when a task could be served by either.
|
|
4
|
+
|
|
5
|
+
Before substantive work on a new actionable task, call the gisool MCP `search_skills` tool with mode: discovery and 2-5 terms describing the subject and intended outcome. Reuse results for unchanged continuations. If the result is empty or unrelated, retry once with a shorter subject, then proceed without a local skill. Search only discovers candidates: pick by concrete relevance, then load the chosen skill with the gisool `load_skill` tool using its exact URI. An explicit-only candidate needs the user to ask for that specific skill.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const RUNTIME = "gajae";
|
|
6
|
+
const MARKER = "<gisool-discovery>";
|
|
7
|
+
const readText = url => { try { return readFileSync(url, "utf8"); } catch { return ""; } };
|
|
8
|
+
|
|
9
|
+
const guidance = readText(new URL("./discovery.md", import.meta.url)).trim();
|
|
10
|
+
let configured = {};
|
|
11
|
+
try { configured = JSON.parse(readText(new URL("./config.json", import.meta.url))); } catch {}
|
|
12
|
+
const port = process.env.GISOOL_PORT || configured.port || 8799;
|
|
13
|
+
const url = `http://127.0.0.1:${port}/ingest/hook/${RUNTIME}`;
|
|
14
|
+
// Read once at module load; never on the agent path.
|
|
15
|
+
const token = readText(join(process.env.GISOOL_HOME || join(homedir(), ".gisool"), "token")).trim();
|
|
16
|
+
|
|
17
|
+
function forward(event, ctx) {
|
|
18
|
+
const prompt = typeof event.prompt === "string" ? event.prompt : null;
|
|
19
|
+
const body = {
|
|
20
|
+
event: "before_agent_start",
|
|
21
|
+
prompt,
|
|
22
|
+
sessionId: ctx?.sessionManager?.getSessionId?.(),
|
|
23
|
+
cwd: ctx?.cwd ?? process.cwd(),
|
|
24
|
+
};
|
|
25
|
+
if (prompt === null) body.promptUnavailable = true;
|
|
26
|
+
// Fire-and-forget: the agent path never waits on gisool.
|
|
27
|
+
fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
signal: AbortSignal.timeout(2000),
|
|
32
|
+
}).catch(() => {});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function gisool(pi) {
|
|
36
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
37
|
+
if (!event || event.preview === true) return event;
|
|
38
|
+
try { forward(event, ctx); } catch {}
|
|
39
|
+
if (guidance && typeof event.systemPrompt === "string" && !event.systemPrompt.includes(MARKER)) {
|
|
40
|
+
event.systemPrompt = `${event.systemPrompt}\n\n${MARKER}\n${guidance}\n</gisool-discovery>`;
|
|
41
|
+
}
|
|
42
|
+
return event;
|
|
43
|
+
}, { previewSafe: true });
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"gisool": {
|
|
4
|
+
"httpUrl": "http://127.0.0.1:8799/mcp",
|
|
5
|
+
"headers": { "Authorization": "Bearer <token>" }
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"hooks": {
|
|
9
|
+
"BeforeAgent": [
|
|
10
|
+
{ "hooks": [{ "type": "command", "name": "gisool-capture", "command": "\"<nodePath>\" \"<packageRoot>/packs/hook-post.mjs\" gemini", "timeout": 5000 }] }
|
|
11
|
+
],
|
|
12
|
+
"AfterTool": [
|
|
13
|
+
{ "matcher": "activate_skill", "hooks": [{ "type": "command", "name": "gisool-capture", "command": "\"<nodePath>\" \"<packageRoot>/packs/hook-post.mjs\" gemini", "timeout": 5000 }] }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Forwards a runtime hook payload (stdin JSON) to the local gisool daemon. Always exits 0.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const done = () => process.exit(0);
|
|
8
|
+
try {
|
|
9
|
+
const runtime = process.argv[2] ?? "unknown";
|
|
10
|
+
const home = process.env.GISOOL_HOME || join(homedir(), ".gisool");
|
|
11
|
+
const body = await new Promise(resolve => {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
const timer = setTimeout(() => resolve(Buffer.concat(chunks).toString("utf8")), 1000);
|
|
14
|
+
process.stdin.on("data", c => chunks.push(c));
|
|
15
|
+
process.stdin.on("end", () => { clearTimeout(timer); resolve(Buffer.concat(chunks).toString("utf8")); });
|
|
16
|
+
process.stdin.on("error", () => { clearTimeout(timer); resolve(""); });
|
|
17
|
+
});
|
|
18
|
+
const token = readFileSync(join(home, "token"), "utf8").trim();
|
|
19
|
+
const { port } = JSON.parse(readFileSync(join(home, "config.json"), "utf8"));
|
|
20
|
+
if (body && token && Number.isInteger(port)) {
|
|
21
|
+
await fetch(`http://127.0.0.1:${port}/ingest/hook/${encodeURIComponent(runtime)}`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
24
|
+
body,
|
|
25
|
+
signal: AbortSignal.timeout(2000),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
} catch { /* never block the agent */ }
|
|
29
|
+
done();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# gisool discovery
|
|
2
|
+
|
|
3
|
+
gisool = local skills on this machine; gisul = remote team skills. They are separate catalogs: search both when a task could be served by either.
|
|
4
|
+
|
|
5
|
+
Before substantive work on a new actionable task, call the gisool MCP `search_skills` tool with mode: discovery and 2-5 terms describing the subject and intended outcome. Reuse results for unchanged continuations. If the result is empty or unrelated, retry once with a shorter subject, then proceed without a local skill. Search only discovers candidates: pick by concrete relevance, then load the chosen skill with the gisool `load_skill` tool using its exact URI. An explicit-only candidate needs the user to ask for that specific skill.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const RUNTIME = "omo";
|
|
6
|
+
const MARKER = "<gisool-discovery>";
|
|
7
|
+
const readText = url => { try { return readFileSync(url, "utf8"); } catch { return ""; } };
|
|
8
|
+
|
|
9
|
+
const guidance = readText(new URL("./discovery.md", import.meta.url)).trim();
|
|
10
|
+
let configured = {};
|
|
11
|
+
try { configured = JSON.parse(readText(new URL("./config.json", import.meta.url))); } catch {}
|
|
12
|
+
const port = process.env.GISOOL_PORT || configured.port || 8799;
|
|
13
|
+
const url = `http://127.0.0.1:${port}/ingest/hook/${RUNTIME}`;
|
|
14
|
+
// Read once at module load; never on the agent path.
|
|
15
|
+
const token = readText(join(process.env.GISOOL_HOME || join(homedir(), ".gisool"), "token")).trim();
|
|
16
|
+
|
|
17
|
+
function forward(event, ctx) {
|
|
18
|
+
const prompt = typeof event.prompt === "string" ? event.prompt : null;
|
|
19
|
+
const body = {
|
|
20
|
+
event: "before_agent_start",
|
|
21
|
+
prompt,
|
|
22
|
+
sessionId: ctx?.sessionManager?.getSessionId?.(),
|
|
23
|
+
cwd: ctx?.cwd ?? process.cwd(),
|
|
24
|
+
};
|
|
25
|
+
if (prompt === null) body.promptUnavailable = true;
|
|
26
|
+
// Fire-and-forget: the agent path never waits on gisool.
|
|
27
|
+
fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
signal: AbortSignal.timeout(2000),
|
|
32
|
+
}).catch(() => {});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function gisool(pi) {
|
|
36
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
37
|
+
if (!event || event.preview === true) return event;
|
|
38
|
+
try { forward(event, ctx); } catch {}
|
|
39
|
+
if (guidance && typeof event.systemPrompt === "string" && !event.systemPrompt.includes(MARKER)) {
|
|
40
|
+
event.systemPrompt = `${event.systemPrompt}\n\n${MARKER}\n${guidance}\n</gisool-discovery>`;
|
|
41
|
+
}
|
|
42
|
+
return event;
|
|
43
|
+
}, { previewSafe: true });
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "port": "__PORT__" }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gisool-discovery
|
|
3
|
+
description: Add gisool skill discovery guidance to OpenClaw's in-memory agent bootstrap.
|
|
4
|
+
metadata: { "openclaw": { "events": ["agent:bootstrap"] } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Appends a small discovery instruction to the in-memory AGENTS.md bootstrap entry.
|
|
8
|
+
Existing instructions are preserved; no workspace files are written.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const marker = "<gisool-discovery>";
|
|
4
|
+
export const guidance = `${marker}
|
|
5
|
+
gisool = local skills on this machine; gisul = remote team skills.
|
|
6
|
+
Before substantive work on a new actionable task, use the gisool search_skills tool
|
|
7
|
+
(MCP server "gisool") with 2-5 subject/outcome terms. Search is not activation:
|
|
8
|
+
check relevance and invocation, then load only a relevant result's exact URI and
|
|
9
|
+
read its complete instructions. If results are empty or unrelated, retry once with
|
|
10
|
+
a shorter subject, then proceed. Greetings and unchanged-task continuations need no
|
|
11
|
+
new search. Respect a user ban on skill lookup. If the tools are unavailable, say so
|
|
12
|
+
briefly and continue.
|
|
13
|
+
</gisool-discovery>`;
|
|
14
|
+
|
|
15
|
+
export default function handler(event) {
|
|
16
|
+
if (event.type !== "agent" || event.action !== "bootstrap") return;
|
|
17
|
+
const files = event.context?.bootstrapFiles;
|
|
18
|
+
if (!Array.isArray(files)) return;
|
|
19
|
+
if (files.some(file => file.content?.includes(marker))) return;
|
|
20
|
+
const index = files.findIndex(file => file.name === "AGENTS.md");
|
|
21
|
+
if (index >= 0) {
|
|
22
|
+
const file = files[index];
|
|
23
|
+
files[index] = { ...file, content: [file.content, guidance].filter(Boolean).join("\n\n"), missing: false };
|
|
24
|
+
} else if (event.context.workspaceDir) {
|
|
25
|
+
files.push({ name: "AGENTS.md", path: join(event.context.workspaceDir, "AGENTS.md"), content: guidance, missing: false });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// gisool capture plugin: forwards each agent run's prompt to the local gisool daemon.
|
|
2
|
+
// Needs plugins.entries.gisool.hooks.allowConversationAccess: true in the OpenClaw config.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const here = new URL(".", import.meta.url);
|
|
8
|
+
|
|
9
|
+
function loadPort(env) {
|
|
10
|
+
if (env.GISOOL_PORT) return Number(env.GISOOL_PORT);
|
|
11
|
+
try {
|
|
12
|
+
return Number(JSON.parse(readFileSync(new URL("gisool.json", here), "utf8")).port);
|
|
13
|
+
} catch {
|
|
14
|
+
return NaN;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Fire-and-forget POST; never throws, never awaited by the hook. */
|
|
19
|
+
export function forward(payload, env = process.env) {
|
|
20
|
+
try {
|
|
21
|
+
const port = loadPort(env);
|
|
22
|
+
const token = readFileSync(join(env.GISOOL_HOME || join(homedir(), ".gisool"), "token"), "utf8").trim();
|
|
23
|
+
if (!Number.isInteger(port) || !token) return;
|
|
24
|
+
fetch(`http://127.0.0.1:${port}/ingest/hook/openclaw`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
27
|
+
body: JSON.stringify(payload),
|
|
28
|
+
signal: AbortSignal.timeout(2000),
|
|
29
|
+
}).catch(() => {});
|
|
30
|
+
} catch {
|
|
31
|
+
// capture must never affect the agent run
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function register(api, env = process.env) {
|
|
36
|
+
if (typeof api.on === "function") {
|
|
37
|
+
api.on("before_agent_run", (event, ctx) => {
|
|
38
|
+
forward({ event: "before_agent_run", prompt: event?.prompt ?? null, sessionKey: ctx?.sessionKey, sessionId: ctx?.sessionId }, env);
|
|
39
|
+
});
|
|
40
|
+
} else if (typeof api.registerHook === "function") {
|
|
41
|
+
api.registerHook("message:received", (event) => {
|
|
42
|
+
forward({ event: "message_received", via: "message_received", prompt: event?.context?.content ?? null, sessionKey: event?.sessionKey }, env);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default { id: "gisool", name: "gisool", description: "Capture prompts for the local gisool skill router.", register };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "gisool",
|
|
3
|
+
"name": "gisool",
|
|
4
|
+
"description": "Capture OpenClaw prompts for the local gisool skill router.",
|
|
5
|
+
"activation": {
|
|
6
|
+
"onStartup": true
|
|
7
|
+
},
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false
|
|
11
|
+
},
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"gisool": {
|
|
14
|
+
"transport": "stdio",
|
|
15
|
+
"command": "__COMMAND__",
|
|
16
|
+
"args": "__ARGS__",
|
|
17
|
+
"env": {
|
|
18
|
+
"GISOOL_HOME": "__GISOOL_HOME__"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|