selftune 0.2.21 → 0.2.23
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/README.md +15 -8
- package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
- package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
- package/apps/local-dashboard/dist/index.html +3 -3
- package/cli/selftune/adapters/cline/hook.ts +167 -0
- package/cli/selftune/adapters/cline/install.ts +197 -0
- package/cli/selftune/adapters/codex/hook.ts +296 -0
- package/cli/selftune/adapters/codex/install.ts +289 -0
- package/cli/selftune/adapters/opencode/hook.ts +222 -0
- package/cli/selftune/adapters/opencode/install.ts +543 -0
- package/cli/selftune/adapters/pi/hook.ts +273 -0
- package/cli/selftune/adapters/pi/install.ts +207 -0
- package/cli/selftune/constants.ts +10 -1
- package/cli/selftune/dashboard-contract.ts +14 -0
- package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
- package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
- package/cli/selftune/evolution/evidence.ts +2 -6
- package/cli/selftune/evolution/evolve-body.ts +73 -20
- package/cli/selftune/evolution/validate-body.ts +78 -42
- package/cli/selftune/evolution/validate-routing.ts +45 -104
- package/cli/selftune/hooks/auto-activate.ts +43 -37
- package/cli/selftune/hooks/skill-eval.ts +2 -1
- package/cli/selftune/hooks-shared/git-metadata.ts +149 -0
- package/cli/selftune/hooks-shared/hook-output.ts +105 -0
- package/cli/selftune/hooks-shared/normalize.ts +196 -0
- package/cli/selftune/hooks-shared/session-state.ts +76 -0
- package/cli/selftune/hooks-shared/skill-paths.ts +50 -0
- package/cli/selftune/hooks-shared/stdin-dispatch.ts +59 -0
- package/cli/selftune/hooks-shared/types.ts +91 -0
- package/cli/selftune/index.ts +76 -6
- package/cli/selftune/ingestors/pi-ingest.ts +726 -0
- package/cli/selftune/init.ts +11 -1
- package/cli/selftune/localdb/direct-write.ts +85 -0
- package/cli/selftune/localdb/materialize.ts +6 -7
- package/cli/selftune/localdb/queries.ts +126 -0
- package/cli/selftune/localdb/schema.ts +38 -0
- package/cli/selftune/observability.ts +8 -1
- package/cli/selftune/orchestrate.ts +43 -0
- package/cli/selftune/registry/client.ts +74 -0
- package/cli/selftune/registry/history.ts +54 -0
- package/cli/selftune/registry/index.ts +90 -0
- package/cli/selftune/registry/install.ts +141 -0
- package/cli/selftune/registry/list.ts +44 -0
- package/cli/selftune/registry/push.ts +171 -0
- package/cli/selftune/registry/rollback.ts +49 -0
- package/cli/selftune/registry/status.ts +62 -0
- package/cli/selftune/registry/sync.ts +125 -0
- package/cli/selftune/repair/skill-usage.ts +4 -1
- package/cli/selftune/status.ts +31 -0
- package/cli/selftune/sync.ts +127 -23
- package/cli/selftune/types.ts +2 -1
- package/cli/selftune/utils/jsonl.ts +1 -30
- package/cli/selftune/utils/llm-call.ts +99 -34
- package/cli/selftune/utils/skill-discovery.ts +22 -0
- package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/node_modules/@selftune/telemetry-contract/package.json +1 -1
- package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
- package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
- package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
- package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/package.json +1 -1
- package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
- package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
- package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
- package/packages/telemetry-contract/package.json +1 -1
- package/packages/telemetry-contract/src/index.ts +1 -0
- package/packages/telemetry-contract/src/schemas.ts +22 -4
- package/packages/telemetry-contract/src/types.ts +1 -12
- package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
- package/packages/ui/AGENTS.md +16 -0
- package/packages/ui/README.md +1 -1
- package/packages/ui/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
- package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
- package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
- package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
- package/packages/ui/src/components/InfoTip.tsx +1 -2
- package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
- package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
- package/packages/ui/src/components/OverviewPanels.tsx +652 -0
- package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
- package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
- package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
- package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
- package/packages/ui/src/components/index.ts +56 -1
- package/packages/ui/src/components/section-cards.tsx +18 -35
- package/packages/ui/src/components/skill-health-grid.tsx +47 -37
- package/packages/ui/src/lib/constants.tsx +0 -1
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/packages/ui/src/primitives/checkbox.tsx +1 -1
- package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
- package/packages/ui/src/primitives/select.tsx +2 -2
- package/packages/ui/src/types.ts +172 -4
- package/skill/SKILL.md +26 -2
- package/skill/Workflows/Ingest.md +60 -2
- package/skill/Workflows/Initialize.md +54 -9
- package/skill/Workflows/PlatformHooks.md +109 -0
- package/skill/Workflows/Registry.md +99 -0
- package/skill/Workflows/Sync.md +3 -1
- package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
- package/cli/selftune/utils/html.ts +0 -27
- package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizers that convert platform-specific hook payloads to UnifiedHookEvent.
|
|
3
|
+
*
|
|
4
|
+
* Each platform adapter maps its native payload shape and event names
|
|
5
|
+
* to the shared UnifiedHookEvent interface. The normalizers are intentionally
|
|
6
|
+
* lenient — unknown fields are ignored, missing fields become undefined.
|
|
7
|
+
*
|
|
8
|
+
* Fail-open: any parsing error returns a minimal event with what we have.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { HookEventType, HookPlatform, UnifiedHookEvent } from "./types.js";
|
|
12
|
+
import { PLATFORM_EVENT_MAP } from "./types.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Reverse lookup: native event name -> HookEventType
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** Build a reverse map from native event string to HookEventType for a platform. */
|
|
19
|
+
function buildReverseLookup(platform: HookPlatform): Map<string, HookEventType> {
|
|
20
|
+
const forward = PLATFORM_EVENT_MAP[platform];
|
|
21
|
+
const reverse = new Map<string, HookEventType>();
|
|
22
|
+
for (const [hookType, nativeName] of Object.entries(forward)) {
|
|
23
|
+
if (nativeName) {
|
|
24
|
+
reverse.set(nativeName, hookType as HookEventType);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return reverse;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const reverseLookups = new Map<HookPlatform, Map<string, HookEventType>>();
|
|
31
|
+
|
|
32
|
+
/** Resolve a native event type string to the normalized HookEventType. */
|
|
33
|
+
function resolveEventType(
|
|
34
|
+
platform: HookPlatform,
|
|
35
|
+
nativeEventType: string,
|
|
36
|
+
): HookEventType | undefined {
|
|
37
|
+
let lookup = reverseLookups.get(platform);
|
|
38
|
+
if (!lookup) {
|
|
39
|
+
lookup = buildReverseLookup(platform);
|
|
40
|
+
reverseLookups.set(platform, lookup);
|
|
41
|
+
}
|
|
42
|
+
return lookup.get(nativeEventType);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Shared field extraction helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function str(v: unknown): string | undefined {
|
|
50
|
+
return typeof v === "string" ? v : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function obj(v: unknown): Record<string, unknown> | undefined {
|
|
54
|
+
return typeof v === "object" && v !== null ? (v as Record<string, unknown>) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Per-platform field extraction config
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Describes how to extract prompt and last_message fields from a platform's
|
|
63
|
+
* payload. Most platforms use the same field names; Claude Code has a
|
|
64
|
+
* user_prompt fallback, and some use last_assistant_message vs last_message.
|
|
65
|
+
*/
|
|
66
|
+
interface PlatformFieldConfig {
|
|
67
|
+
/** Default event_type when resolution fails */
|
|
68
|
+
fallbackEvent: HookEventType;
|
|
69
|
+
/** Fields to try for prompt (in order, first non-undefined wins) */
|
|
70
|
+
promptFields: string[];
|
|
71
|
+
/** Field name for session-end last message */
|
|
72
|
+
lastMessageField: string;
|
|
73
|
+
/** Field name for post_tool_use output (e.g., "tool_response" or "tool_output") */
|
|
74
|
+
toolOutputField: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const FIELD_CONFIG: Record<HookPlatform, PlatformFieldConfig> = {
|
|
78
|
+
"claude-code": {
|
|
79
|
+
fallbackEvent: "prompt_submit",
|
|
80
|
+
promptFields: ["prompt", "user_prompt"],
|
|
81
|
+
lastMessageField: "last_assistant_message",
|
|
82
|
+
toolOutputField: "tool_response",
|
|
83
|
+
},
|
|
84
|
+
codex: {
|
|
85
|
+
fallbackEvent: "prompt_submit",
|
|
86
|
+
promptFields: ["prompt"],
|
|
87
|
+
lastMessageField: "last_assistant_message",
|
|
88
|
+
toolOutputField: "tool_response",
|
|
89
|
+
},
|
|
90
|
+
opencode: {
|
|
91
|
+
fallbackEvent: "session_end",
|
|
92
|
+
promptFields: ["prompt"],
|
|
93
|
+
lastMessageField: "last_message",
|
|
94
|
+
toolOutputField: "tool_output",
|
|
95
|
+
},
|
|
96
|
+
cline: {
|
|
97
|
+
fallbackEvent: "session_end",
|
|
98
|
+
promptFields: ["prompt"],
|
|
99
|
+
lastMessageField: "last_message",
|
|
100
|
+
toolOutputField: "tool_output",
|
|
101
|
+
},
|
|
102
|
+
pi: {
|
|
103
|
+
fallbackEvent: "session_end",
|
|
104
|
+
promptFields: ["prompt"],
|
|
105
|
+
lastMessageField: "last_message",
|
|
106
|
+
toolOutputField: "tool_output",
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Unified normalizer
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Normalize a platform-specific hook payload to UnifiedHookEvent.
|
|
116
|
+
*
|
|
117
|
+
* This single function replaces per-platform normalizers by using
|
|
118
|
+
* FIELD_CONFIG to handle platform-specific field names.
|
|
119
|
+
*/
|
|
120
|
+
function normalizeForPlatform(
|
|
121
|
+
platform: HookPlatform,
|
|
122
|
+
payload: unknown,
|
|
123
|
+
eventType: string,
|
|
124
|
+
): UnifiedHookEvent {
|
|
125
|
+
const raw = obj(payload) ?? {};
|
|
126
|
+
const config = FIELD_CONFIG[platform];
|
|
127
|
+
const resolved = resolveEventType(platform, eventType);
|
|
128
|
+
|
|
129
|
+
const base: UnifiedHookEvent = {
|
|
130
|
+
platform,
|
|
131
|
+
event_type: resolved ?? config.fallbackEvent,
|
|
132
|
+
session_id: str(raw.session_id) ?? "unknown",
|
|
133
|
+
cwd: str(raw.cwd),
|
|
134
|
+
transcript_path: str(raw.transcript_path),
|
|
135
|
+
raw_payload: payload,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (resolved === "pre_tool_use" || resolved === "post_tool_use") {
|
|
139
|
+
base.tool_name = str(raw.tool_name);
|
|
140
|
+
base.tool_input = obj(raw.tool_input);
|
|
141
|
+
if (resolved === "post_tool_use") {
|
|
142
|
+
base.tool_output = obj(raw[config.toolOutputField]);
|
|
143
|
+
}
|
|
144
|
+
} else if (resolved === "prompt_submit") {
|
|
145
|
+
for (const field of config.promptFields) {
|
|
146
|
+
const v = str(raw[field]);
|
|
147
|
+
if (v) {
|
|
148
|
+
base.prompt = v;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else if (resolved === "session_end") {
|
|
153
|
+
base.last_message = str(raw[config.lastMessageField]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return base;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Public API — named exports for backward compatibility + unified entry point
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
export function normalizeClaudeCode(payload: unknown, eventType: string): UnifiedHookEvent {
|
|
164
|
+
return normalizeForPlatform("claude-code", payload, eventType);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function normalizeCodex(payload: unknown, eventType: string): UnifiedHookEvent {
|
|
168
|
+
return normalizeForPlatform("codex", payload, eventType);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function normalizeOpenCode(payload: unknown, eventType: string): UnifiedHookEvent {
|
|
172
|
+
return normalizeForPlatform("opencode", payload, eventType);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function normalizeCline(payload: unknown, eventType: string): UnifiedHookEvent {
|
|
176
|
+
return normalizeForPlatform("cline", payload, eventType);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function normalizePi(payload: unknown, eventType: string): UnifiedHookEvent {
|
|
180
|
+
return normalizeForPlatform("pi", payload, eventType);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Auto-detect platform and normalize a hook payload to UnifiedHookEvent.
|
|
185
|
+
*
|
|
186
|
+
* @param payload Raw payload (typically parsed from stdin JSON)
|
|
187
|
+
* @param platform The host platform
|
|
188
|
+
* @param nativeEventType The platform-native event type string
|
|
189
|
+
*/
|
|
190
|
+
export function normalizeHookEvent(
|
|
191
|
+
payload: unknown,
|
|
192
|
+
platform: HookPlatform,
|
|
193
|
+
nativeEventType: string,
|
|
194
|
+
): UnifiedHookEvent {
|
|
195
|
+
return normalizeForPlatform(platform, payload, nativeEventType);
|
|
196
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic session state persistence for hooks.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the duplicate patterns in auto-activate.ts (loadSessionState/saveSessionState)
|
|
5
|
+
* and skill-change-guard.ts (loadGuardState/saveGuardState). Both follow the same pattern:
|
|
6
|
+
*
|
|
7
|
+
* 1. Read a JSON file keyed by session_id
|
|
8
|
+
* 2. If session_id matches, return persisted state; otherwise return defaults
|
|
9
|
+
* 3. Write state back after updates
|
|
10
|
+
*
|
|
11
|
+
* This module generalizes that pattern with a type-safe generic interface.
|
|
12
|
+
* Fail-open: corrupt or missing files return fresh defaults.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import type { SessionState } from "./types.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load session state from a JSON file.
|
|
22
|
+
*
|
|
23
|
+
* The file is located at `{dir}/{prefix}-{sessionId}.json`. If the file does not
|
|
24
|
+
* exist, is corrupt, or belongs to a different session, fresh defaults are returned.
|
|
25
|
+
*
|
|
26
|
+
* @param dir Directory to store state files (e.g., SELFTUNE_CONFIG_DIR)
|
|
27
|
+
* @param prefix Filename prefix (e.g., "session-state", "guard-state")
|
|
28
|
+
* @param sessionId Current session ID — state is invalidated when it changes
|
|
29
|
+
* @param defaults Factory function returning fresh default state data
|
|
30
|
+
*/
|
|
31
|
+
export function loadSessionState<T extends Record<string, unknown>>(
|
|
32
|
+
dir: string,
|
|
33
|
+
prefix: string,
|
|
34
|
+
sessionId: string,
|
|
35
|
+
defaults: () => T,
|
|
36
|
+
): SessionState<T> {
|
|
37
|
+
const safeName = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
38
|
+
const filePath = join(dir, `${prefix}-${safeName}.json`);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8")) as SessionState<T>;
|
|
42
|
+
if (raw.session_id === sessionId && typeof raw.data === "object" && raw.data !== null) {
|
|
43
|
+
return raw;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// ENOENT (missing) or corrupt JSON -- return fresh defaults
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
session_id: sessionId,
|
|
51
|
+
created_at: new Date().toISOString(),
|
|
52
|
+
data: defaults(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Save session state to a JSON file.
|
|
58
|
+
*
|
|
59
|
+
* The file is written to `{dir}/{prefix}-{state.session_id}.json`.
|
|
60
|
+
* The directory is created if it does not exist.
|
|
61
|
+
*
|
|
62
|
+
* @param dir Directory to store state files
|
|
63
|
+
* @param prefix Filename prefix (must match what was used in loadSessionState)
|
|
64
|
+
* @param state The session state to persist
|
|
65
|
+
*/
|
|
66
|
+
export function saveSessionState<T extends Record<string, unknown>>(
|
|
67
|
+
dir: string,
|
|
68
|
+
prefix: string,
|
|
69
|
+
state: SessionState<T>,
|
|
70
|
+
): void {
|
|
71
|
+
const safeName = state.session_id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
72
|
+
const filePath = join(dir, `${prefix}-${safeName}.json`);
|
|
73
|
+
|
|
74
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
75
|
+
writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
76
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared skill name and path extraction utilities for hooks.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from duplicated logic in:
|
|
5
|
+
* - skill-eval.ts: extractSkillName (checks SKILL.MD basename)
|
|
6
|
+
* - skill-change-guard.ts: isSkillMdWrite, extractSkillNameFromPath
|
|
7
|
+
* - evolution-guard.ts: isSkillMdWrite, extractSkillName (identical copies)
|
|
8
|
+
*
|
|
9
|
+
* All three files independently implement the same SKILL.md detection pattern.
|
|
10
|
+
* This module provides a single source of truth.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { basename, dirname } from "node:path";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract the skill folder name from a file path that ends in SKILL.md.
|
|
17
|
+
*
|
|
18
|
+
* The convention is that skill definitions live at `<skill-name>/SKILL.md`,
|
|
19
|
+
* so the parent directory name is the skill name.
|
|
20
|
+
*
|
|
21
|
+
* @param filePath Absolute or relative path to check
|
|
22
|
+
* @returns Skill folder name, or null if the path does not end in SKILL.md
|
|
23
|
+
*/
|
|
24
|
+
export function extractSkillName(filePath: string): string | null {
|
|
25
|
+
if (!isSkillMdFile(filePath)) return null;
|
|
26
|
+
return basename(dirname(filePath)) || "unknown";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a file path points to a SKILL.md file (case-insensitive).
|
|
31
|
+
*
|
|
32
|
+
* @param filePath Path to check
|
|
33
|
+
*/
|
|
34
|
+
export function isSkillMdFile(filePath: string): boolean {
|
|
35
|
+
return basename(filePath).toUpperCase() === "SKILL.MD";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a tool call is a Write or Edit operation targeting a SKILL.md file.
|
|
40
|
+
*
|
|
41
|
+
* Used by guard hooks (skill-change-guard, evolution-guard) to detect
|
|
42
|
+
* when an agent is about to modify a skill definition.
|
|
43
|
+
*
|
|
44
|
+
* @param toolName The tool being called (e.g., "Write", "Edit", "Read")
|
|
45
|
+
* @param filePath The file_path from tool_input
|
|
46
|
+
*/
|
|
47
|
+
export function isSkillMdWrite(toolName: string, filePath: string): boolean {
|
|
48
|
+
if (toolName !== "Write" && toolName !== "Edit") return false;
|
|
49
|
+
return isSkillMdFile(filePath);
|
|
50
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generalized stdin preview and keyword filtering for hooks.
|
|
3
|
+
*
|
|
4
|
+
* Hooks receive payloads via stdin. Most hooks only care about specific
|
|
5
|
+
* event types (e.g., skill-eval only handles PostToolUse). The existing
|
|
6
|
+
* stdin-preview.ts provides a fast-path optimization: read stdin once,
|
|
7
|
+
* check a preview slice for keywords, and skip JSON.parse entirely when
|
|
8
|
+
* the keyword is absent.
|
|
9
|
+
*
|
|
10
|
+
* This module wraps that pattern into a single readAndFilter call that
|
|
11
|
+
* combines the preview check with JSON parsing, returning null when
|
|
12
|
+
* the payload is irrelevant (caller should exit 0 immediately).
|
|
13
|
+
*
|
|
14
|
+
* Re-exports readStdinWithPreview for backward compatibility.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { readStdinWithPreview } from "../hooks/stdin-preview.js";
|
|
18
|
+
|
|
19
|
+
/** Default preview size in characters (covers envelope fields). */
|
|
20
|
+
const DEFAULT_PREVIEW_BYTES = 4096;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read stdin with fast-path keyword filtering.
|
|
24
|
+
*
|
|
25
|
+
* Reads all of stdin, checks the leading preview slice for the presence of
|
|
26
|
+
* ALL required keywords. If any keyword is missing, returns null (the caller
|
|
27
|
+
* should exit early). Otherwise, parses the full payload as JSON and returns it.
|
|
28
|
+
*
|
|
29
|
+
* This is the recommended way to read hook payloads when you know which
|
|
30
|
+
* keywords must appear in the envelope (e.g., event name, tool name).
|
|
31
|
+
*
|
|
32
|
+
* @param requiredKeywords Strings that must ALL appear in the preview slice.
|
|
33
|
+
* Typically quoted JSON values like '"PostToolUse"'.
|
|
34
|
+
* @param previewBytes Number of leading characters to check (default 4096).
|
|
35
|
+
* @returns Parsed payload and raw string, or null if keywords don't match.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const result = await readAndFilter<PostToolUsePayload>(['"PostToolUse"', '"Read"']);
|
|
40
|
+
* if (!result) process.exit(0);
|
|
41
|
+
* const { payload } = result;
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export async function readAndFilter<T = unknown>(
|
|
45
|
+
requiredKeywords: string[],
|
|
46
|
+
previewBytes: number = DEFAULT_PREVIEW_BYTES,
|
|
47
|
+
): Promise<{ payload: T; raw: string } | null> {
|
|
48
|
+
const raw = await Bun.stdin.text();
|
|
49
|
+
const preview = raw.slice(0, previewBytes);
|
|
50
|
+
|
|
51
|
+
for (const keyword of requiredKeywords) {
|
|
52
|
+
if (!preview.includes(keyword)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const payload = JSON.parse(raw) as T;
|
|
58
|
+
return { payload, raw };
|
|
59
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal hook types for multi-agent abstraction.
|
|
3
|
+
* All platform adapters normalize their native events to these types.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Supported agent platforms */
|
|
7
|
+
export type HookPlatform = "claude-code" | "codex" | "opencode" | "cline" | "pi";
|
|
8
|
+
|
|
9
|
+
/** Normalized event types across all platforms */
|
|
10
|
+
export type HookEventType = "pre_tool_use" | "post_tool_use" | "prompt_submit" | "session_end";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Platform-agnostic hook event. Each adapter normalizes its native payload to this shape.
|
|
14
|
+
* Fields are optional because not all platforms provide all data.
|
|
15
|
+
*/
|
|
16
|
+
export interface UnifiedHookEvent {
|
|
17
|
+
platform: HookPlatform;
|
|
18
|
+
event_type: HookEventType;
|
|
19
|
+
session_id: string;
|
|
20
|
+
cwd?: string;
|
|
21
|
+
transcript_path?: string;
|
|
22
|
+
|
|
23
|
+
// Tool-related (pre_tool_use / post_tool_use)
|
|
24
|
+
tool_name?: string;
|
|
25
|
+
tool_input?: Record<string, unknown>;
|
|
26
|
+
tool_output?: Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
// Prompt-related (prompt_submit)
|
|
29
|
+
prompt?: string;
|
|
30
|
+
|
|
31
|
+
// Session-related (session_end)
|
|
32
|
+
last_message?: string;
|
|
33
|
+
|
|
34
|
+
/** Original platform-specific payload, preserved for platform-specific logic */
|
|
35
|
+
raw_payload?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hook response returned to the host agent.
|
|
40
|
+
* Adapters translate this back to platform-specific format.
|
|
41
|
+
*/
|
|
42
|
+
export interface HookResponse {
|
|
43
|
+
/** Whether the hook modified the input */
|
|
44
|
+
modified: boolean;
|
|
45
|
+
/** Decision for PreToolUse guards */
|
|
46
|
+
decision?: "allow" | "block" | "skip";
|
|
47
|
+
/** Modified tool input (for pre_tool_use hooks that modify commands) */
|
|
48
|
+
updated_input?: Record<string, unknown>;
|
|
49
|
+
/** Advisory message (stderr suggestions) */
|
|
50
|
+
message?: string;
|
|
51
|
+
/** Additional context to inject (stdout JSON for Claude Code) */
|
|
52
|
+
context?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Generic session state for dedup/tracking across hook invocations */
|
|
56
|
+
export interface SessionState<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
57
|
+
session_id: string;
|
|
58
|
+
created_at: string;
|
|
59
|
+
data: T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Platform event mapping reference */
|
|
63
|
+
export const PLATFORM_EVENT_MAP: Record<HookPlatform, Partial<Record<HookEventType, string>>> = {
|
|
64
|
+
"claude-code": {
|
|
65
|
+
pre_tool_use: "PreToolUse",
|
|
66
|
+
post_tool_use: "PostToolUse",
|
|
67
|
+
prompt_submit: "UserPromptSubmit",
|
|
68
|
+
session_end: "Stop",
|
|
69
|
+
},
|
|
70
|
+
codex: {
|
|
71
|
+
pre_tool_use: "PreToolUse",
|
|
72
|
+
post_tool_use: "PostToolUse",
|
|
73
|
+
prompt_submit: "SessionStart",
|
|
74
|
+
session_end: "Stop",
|
|
75
|
+
},
|
|
76
|
+
opencode: {
|
|
77
|
+
pre_tool_use: "tool.execute.before",
|
|
78
|
+
post_tool_use: "tool.execute.after",
|
|
79
|
+
session_end: "session.idle",
|
|
80
|
+
},
|
|
81
|
+
cline: {
|
|
82
|
+
post_tool_use: "PostToolUse",
|
|
83
|
+
session_end: "TaskComplete",
|
|
84
|
+
},
|
|
85
|
+
pi: {
|
|
86
|
+
prompt_submit: "message",
|
|
87
|
+
pre_tool_use: "tool_call",
|
|
88
|
+
post_tool_use: "tool_result",
|
|
89
|
+
session_end: "session_shutdown",
|
|
90
|
+
},
|
|
91
|
+
};
|
package/cli/selftune/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* selftune CLI entry point.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* selftune ingest <agent> — Ingest agent sessions (claude, codex, opencode, openclaw, wrap-codex)
|
|
6
|
+
* selftune ingest <agent> — Ingest agent sessions (claude, codex, opencode, openclaw, pi, wrap-codex)
|
|
7
7
|
* selftune grade [mode] — Grade skill sessions (auto, baseline)
|
|
8
8
|
* selftune evolve [target] — Evolve skill descriptions (body, rollback)
|
|
9
9
|
* selftune eval <action> — Evaluation tools (generate, unit-test, import, composability, family-overlap)
|
|
@@ -28,8 +28,13 @@
|
|
|
28
28
|
* selftune export-canonical — Export canonical telemetry for downstream ingestion
|
|
29
29
|
* selftune recover — Recover SQLite from legacy/exported JSONL
|
|
30
30
|
* selftune telemetry — Manage anonymous usage analytics (status, enable, disable)
|
|
31
|
+
* selftune registry <sub> — Team skill distribution (push, install, sync, status, rollback, history, list)
|
|
31
32
|
* selftune alpha <subcommand> — Alpha program management (upload)
|
|
32
33
|
* selftune hook <name> — Run a hook by name (prompt-log, session-stop, etc.)
|
|
34
|
+
* selftune codex <subcommand> — Codex platform hooks (hook, install)
|
|
35
|
+
* selftune opencode <sub> — OpenCode platform hooks (hook, install)
|
|
36
|
+
* selftune cline <subcommand> — Cline platform hooks (hook, install)
|
|
37
|
+
* selftune pi <subcommand> — Pi platform hooks (hook, install)
|
|
33
38
|
*/
|
|
34
39
|
|
|
35
40
|
import { CLIError, handleCLIError } from "./utils/cli-error.js";
|
|
@@ -46,7 +51,7 @@ Usage:
|
|
|
46
51
|
selftune <command> [options]
|
|
47
52
|
|
|
48
53
|
Commands:
|
|
49
|
-
ingest <agent> Ingest agent sessions (claude, codex, opencode, openclaw, wrap-codex)
|
|
54
|
+
ingest <agent> Ingest agent sessions (claude, codex, opencode, openclaw, pi, wrap-codex)
|
|
50
55
|
grade [mode] Grade skill sessions (auto, baseline)
|
|
51
56
|
evolve [target] Evolve skill descriptions (body, rollback)
|
|
52
57
|
eval <action> Evaluation tools (generate, unit-test, import, composability, family-overlap)
|
|
@@ -70,23 +75,31 @@ Commands:
|
|
|
70
75
|
export Export SQLite data to JSONL snapshots
|
|
71
76
|
export-canonical Export canonical telemetry for downstream ingestion
|
|
72
77
|
recover Recover SQLite from legacy/exported JSONL
|
|
78
|
+
registry <sub> Team skill distribution (push, install, sync, status, rollback, history, list)
|
|
73
79
|
alpha <subcommand> Alpha program management (upload)
|
|
74
80
|
telemetry Manage anonymous usage analytics (status, enable, disable)
|
|
75
81
|
hook <name> Run a hook by name (prompt-log, session-stop, etc.)
|
|
82
|
+
codex <sub> Codex platform hooks (hook, install)
|
|
83
|
+
opencode <sub> OpenCode platform hooks (hook, install)
|
|
84
|
+
cline <sub> Cline platform hooks (hook, install)
|
|
85
|
+
pi <sub> Pi platform hooks (hook, install)
|
|
76
86
|
|
|
77
87
|
Run 'selftune <command> --help' for command-specific options.`);
|
|
78
88
|
process.exit(0);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
//
|
|
82
|
-
|
|
91
|
+
// Fast-path commands (real-time hooks) — skip analytics and auto-update to minimize latency
|
|
92
|
+
const FAST_COMMANDS: ReadonlySet<string> = new Set(["hook", "codex", "opencode", "cline", "pi"]);
|
|
93
|
+
|
|
94
|
+
// Track command usage (lazy import — skip for hooks and --help to avoid loading crypto/os)
|
|
95
|
+
if (command && !FAST_COMMANDS.has(command) && command !== "--help" && command !== "-h") {
|
|
83
96
|
import("./analytics.js")
|
|
84
97
|
.then(({ trackEvent }) => trackEvent("command_run", { command }))
|
|
85
98
|
.catch(() => {});
|
|
86
99
|
}
|
|
87
100
|
|
|
88
|
-
// Auto-update check (skip for hooks — they must be fast — and --help)
|
|
89
|
-
if (command && command
|
|
101
|
+
// Auto-update check (skip for hooks and platform hook commands — they must be fast — and --help)
|
|
102
|
+
if (command && !FAST_COMMANDS.has(command) && command !== "--help" && command !== "-h") {
|
|
90
103
|
const { autoUpdate } = await import("./auto-update.js");
|
|
91
104
|
await autoUpdate();
|
|
92
105
|
}
|
|
@@ -120,6 +133,7 @@ Agents:
|
|
|
120
133
|
codex Ingest Codex rollout logs (experimental)
|
|
121
134
|
opencode Ingest OpenCode sessions (experimental)
|
|
122
135
|
openclaw Ingest OpenClaw sessions (experimental)
|
|
136
|
+
pi Ingest Pi sessions (experimental)
|
|
123
137
|
wrap-codex Wrap codex exec with real-time telemetry (experimental)
|
|
124
138
|
|
|
125
139
|
Run 'selftune ingest <agent> --help' for agent-specific options.`);
|
|
@@ -148,6 +162,11 @@ Run 'selftune ingest <agent> --help' for agent-specific options.`);
|
|
|
148
162
|
cliMain();
|
|
149
163
|
break;
|
|
150
164
|
}
|
|
165
|
+
case "pi": {
|
|
166
|
+
const { cliMain } = await import("./ingestors/pi-ingest.js");
|
|
167
|
+
cliMain();
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
151
170
|
case "wrap-codex": {
|
|
152
171
|
const { cliMain } = await import("./ingestors/codex-wrapper.js");
|
|
153
172
|
await cliMain();
|
|
@@ -611,6 +630,11 @@ Options:
|
|
|
611
630
|
await cliMain();
|
|
612
631
|
break;
|
|
613
632
|
}
|
|
633
|
+
case "registry": {
|
|
634
|
+
const { cliMain } = await import("./registry/index.js");
|
|
635
|
+
await cliMain();
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
614
638
|
case "alpha": {
|
|
615
639
|
const sub = process.argv[2];
|
|
616
640
|
if (!sub || sub === "--help" || sub === "-h") {
|
|
@@ -815,6 +839,52 @@ Output:
|
|
|
815
839
|
process.exit(result.status ?? 1);
|
|
816
840
|
break;
|
|
817
841
|
}
|
|
842
|
+
// ── Platform hook adapters ─────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
case "codex":
|
|
845
|
+
case "opencode":
|
|
846
|
+
case "cline":
|
|
847
|
+
case "pi": {
|
|
848
|
+
const platform = command;
|
|
849
|
+
const displayName = { codex: "Codex", opencode: "OpenCode", cline: "Cline", pi: "Pi" }[
|
|
850
|
+
platform
|
|
851
|
+
];
|
|
852
|
+
const sub = process.argv[2];
|
|
853
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
854
|
+
console.log(`selftune ${platform} — ${displayName} platform hooks
|
|
855
|
+
|
|
856
|
+
Usage:
|
|
857
|
+
selftune ${platform} <subcommand> [options]
|
|
858
|
+
|
|
859
|
+
Subcommands:
|
|
860
|
+
hook Handle a real-time hook event from ${displayName}
|
|
861
|
+
install Install or remove selftune hooks in ${displayName} config
|
|
862
|
+
|
|
863
|
+
Run 'selftune ${platform} <subcommand> --help' for subcommand-specific options.`);
|
|
864
|
+
process.exit(0);
|
|
865
|
+
}
|
|
866
|
+
process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
|
|
867
|
+
switch (sub) {
|
|
868
|
+
case "hook": {
|
|
869
|
+
const { cliMain } = await import(`./adapters/${platform}/hook.js`);
|
|
870
|
+
await cliMain();
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
case "install": {
|
|
874
|
+
const { cliMain } = await import(`./adapters/${platform}/install.js`);
|
|
875
|
+
await cliMain();
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
default:
|
|
879
|
+
throw new CLIError(
|
|
880
|
+
`Unknown ${platform} subcommand: ${sub}`,
|
|
881
|
+
"UNKNOWN_COMMAND",
|
|
882
|
+
`selftune ${platform} --help`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
|
|
818
888
|
default:
|
|
819
889
|
throw new CLIError(`Unknown command: ${command}`, "UNKNOWN_COMMAND", "selftune --help");
|
|
820
890
|
}
|