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,543 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Install selftune hooks into OpenCode environment.
|
|
4
|
+
*
|
|
5
|
+
* OpenCode uses a plugin system for hooks and a strict config schema.
|
|
6
|
+
* This installer:
|
|
7
|
+
* 1. Writes a plugin file (selftune-opencode-plugin.ts) into the
|
|
8
|
+
* plugins directory (auto-discovered by OpenCode at startup)
|
|
9
|
+
* 2. Registers selftune agents in the `agent` config key
|
|
10
|
+
*
|
|
11
|
+
* Plugin locations (OpenCode auto-discovers these):
|
|
12
|
+
* - ~/.config/opencode/plugins/ (global)
|
|
13
|
+
* - ./.opencode/plugins/ (project-level)
|
|
14
|
+
*
|
|
15
|
+
* Config locations (checked in order):
|
|
16
|
+
* 1. ./opencode.json (project-level)
|
|
17
|
+
* 2. ~/.config/opencode/opencode.json (user-level)
|
|
18
|
+
*
|
|
19
|
+
* Usage: selftune opencode install [--dry-run] [--uninstall]
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
unlinkSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { dirname, join, resolve } from "node:path";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Constants
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const PLUGIN_FILENAME = "selftune-opencode-plugin.ts";
|
|
38
|
+
const SELFTUNE_AGENT_PREFIX = "[selftune]";
|
|
39
|
+
|
|
40
|
+
function getProjectConfigPath(): string {
|
|
41
|
+
return join(process.cwd(), "opencode.json");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getUserConfigPath(): string {
|
|
45
|
+
return join(process.env.HOME ?? homedir(), ".config", "opencode", "opencode.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Global plugins directory — OpenCode auto-discovers plugins here. */
|
|
49
|
+
function getGlobalPluginsDir(): string {
|
|
50
|
+
return join(process.env.HOME ?? homedir(), ".config", "opencode", "plugins");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Project-level plugins directory — OpenCode auto-discovers plugins here. */
|
|
54
|
+
function getProjectPluginsDir(): string {
|
|
55
|
+
return join(process.cwd(), ".opencode", "plugins");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Plugin content
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function buildPluginContent(): string {
|
|
63
|
+
return `// selftune-managed — Written by selftune. Do not edit.
|
|
64
|
+
// OpenCode plugin that pipes hook events to selftune for processing.
|
|
65
|
+
// Auto-discovered from plugins/ directory by OpenCode at startup.
|
|
66
|
+
|
|
67
|
+
export const SelftunePlugin = async ({ $ }) => {
|
|
68
|
+
/** Resolve the selftune CLI as an argv array for Bun.spawn. */
|
|
69
|
+
const resolveSelftune = () => {
|
|
70
|
+
if (process.env.SELFTUNE_CLI_PATH) return [process.env.SELFTUNE_CLI_PATH];
|
|
71
|
+
try {
|
|
72
|
+
const result = Bun.spawnSync(["which", "selftune"]);
|
|
73
|
+
const path = result.stdout?.toString().trim();
|
|
74
|
+
if (path) return [path];
|
|
75
|
+
} catch {}
|
|
76
|
+
return ["npx", "-y", "selftune@latest"];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const selftuneCmd = resolveSelftune();
|
|
80
|
+
|
|
81
|
+
/** Pipe a JSON payload to \`selftune opencode hook\` via Bun.spawn. */
|
|
82
|
+
const runHook = async (payload) => {
|
|
83
|
+
try {
|
|
84
|
+
const proc = Bun.spawn([...selftuneCmd, "opencode", "hook"], {
|
|
85
|
+
stdin: "pipe",
|
|
86
|
+
stdout: "ignore",
|
|
87
|
+
stderr: "ignore",
|
|
88
|
+
});
|
|
89
|
+
proc.stdin.write(payload);
|
|
90
|
+
proc.stdin.end();
|
|
91
|
+
await proc.exited;
|
|
92
|
+
} catch {}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"tool.execute.before": async (input, output) => {
|
|
97
|
+
await runHook(JSON.stringify({
|
|
98
|
+
event: "tool.execute.before",
|
|
99
|
+
session_id: input.metadata?.sessionId ?? "unknown",
|
|
100
|
+
tool: { name: input.tool, args: output.args },
|
|
101
|
+
cwd: input.metadata?.cwd,
|
|
102
|
+
}));
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
"tool.execute.after": async (input, output) => {
|
|
106
|
+
await runHook(JSON.stringify({
|
|
107
|
+
event: "tool.execute.after",
|
|
108
|
+
session_id: input.metadata?.sessionId ?? "unknown",
|
|
109
|
+
tool: { name: input.tool, args: input.args, result: output.result },
|
|
110
|
+
cwd: input.metadata?.cwd,
|
|
111
|
+
}));
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
event: async ({ event }) => {
|
|
115
|
+
if (event.type === "session.idle") {
|
|
116
|
+
await runHook(JSON.stringify({
|
|
117
|
+
event: "session.idle",
|
|
118
|
+
session_id: event.properties?.sessionId ?? "unknown",
|
|
119
|
+
cwd: event.properties?.cwd,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Config helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
interface OpenCodeAgentConfig {
|
|
133
|
+
description?: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
mode?: string;
|
|
136
|
+
model?: string;
|
|
137
|
+
prompt?: string;
|
|
138
|
+
tools?: Record<string, boolean>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface OpenCodeConfig {
|
|
142
|
+
agent?: Record<string, OpenCodeAgentConfig>;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
147
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function detectConfigPath(): string {
|
|
151
|
+
const projectConfig = getProjectConfigPath();
|
|
152
|
+
if (existsSync(projectConfig)) return projectConfig;
|
|
153
|
+
return getUserConfigPath();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readConfig(configPath: string): OpenCodeConfig {
|
|
157
|
+
if (!existsSync(configPath)) return {};
|
|
158
|
+
let parsed: unknown;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`OpenCode config at ${configPath} is not valid JSON; refusing to overwrite it.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!isPlainRecord(parsed)) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`OpenCode config at ${configPath} must be a JSON object; refusing to overwrite it.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return parsed as OpenCodeConfig;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeConfig(configPath: string, config: OpenCodeConfig): void {
|
|
177
|
+
const dir = dirname(configPath);
|
|
178
|
+
if (!existsSync(dir)) {
|
|
179
|
+
mkdirSync(dir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Install logic
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
interface InstallOptions {
|
|
189
|
+
dryRun: boolean;
|
|
190
|
+
uninstall: boolean;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const KNOWN_FLAGS = new Set(["--dry-run", "--uninstall", "--help", "-h"]);
|
|
194
|
+
|
|
195
|
+
function parseFlags(args: string[]): InstallOptions | null {
|
|
196
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
197
|
+
console.log(`Usage: selftune opencode install [--dry-run] [--uninstall]
|
|
198
|
+
|
|
199
|
+
Options:
|
|
200
|
+
--dry-run Preview changes without writing to disk
|
|
201
|
+
--uninstall Remove selftune plugin and agents from OpenCode config
|
|
202
|
+
--help, -h Show this help message`);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const unknown = args.filter((a) => a.startsWith("-") && !KNOWN_FLAGS.has(a));
|
|
207
|
+
if (unknown.length > 0) {
|
|
208
|
+
console.error(`[selftune] Unknown flag(s): ${unknown.join(", ")}`);
|
|
209
|
+
console.error(`Run 'selftune opencode install --help' for usage.`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
dryRun: args.includes("--dry-run"),
|
|
215
|
+
uninstall: args.includes("--uninstall"),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Agent registration
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
/** Map Claude Code tool names to OpenCode tool permissions. */
|
|
224
|
+
function mapToolPermissions(tools?: string[], disallowed?: string[]): Record<string, boolean> {
|
|
225
|
+
const defaults: Record<string, boolean> = {
|
|
226
|
+
write: false,
|
|
227
|
+
edit: false,
|
|
228
|
+
bash: true,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (tools) {
|
|
232
|
+
if (tools.includes("Write")) defaults.write = true;
|
|
233
|
+
if (tools.includes("Edit")) defaults.edit = true;
|
|
234
|
+
if (!tools.includes("Bash")) defaults.bash = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (disallowed) {
|
|
238
|
+
if (disallowed.includes("Write")) defaults.write = false;
|
|
239
|
+
if (disallowed.includes("Edit")) defaults.edit = false;
|
|
240
|
+
if (disallowed.includes("Bash")) defaults.bash = false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return defaults;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** OpenCode model format (provider/model). */
|
|
247
|
+
const OPENCODE_MODEL_MAP: Record<string, string> = {
|
|
248
|
+
haiku: "anthropic/claude-haiku-4-5-20251001",
|
|
249
|
+
sonnet: "anthropic/claude-sonnet-4-20250514",
|
|
250
|
+
opus: "anthropic/claude-opus-4-20250514",
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
interface AgentFrontmatter {
|
|
254
|
+
name: string;
|
|
255
|
+
description?: string;
|
|
256
|
+
tools?: string[];
|
|
257
|
+
disallowedTools?: string[];
|
|
258
|
+
model?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Parse YAML-like frontmatter from agent markdown files. */
|
|
262
|
+
function parseFrontmatter(content: string): AgentFrontmatter | null {
|
|
263
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
264
|
+
if (!match) return null;
|
|
265
|
+
|
|
266
|
+
const fm: Record<string, string> = {};
|
|
267
|
+
for (const line of match[1].split("\n")) {
|
|
268
|
+
const colonIdx = line.indexOf(":");
|
|
269
|
+
if (colonIdx === -1) continue;
|
|
270
|
+
const key = line.slice(0, colonIdx).trim();
|
|
271
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
272
|
+
fm[key] = value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!fm.name) return null;
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
name: fm.name,
|
|
279
|
+
description: fm.description,
|
|
280
|
+
tools: fm.tools ? fm.tools.split(",").map((t) => t.trim()) : undefined,
|
|
281
|
+
disallowedTools: fm.disallowedTools
|
|
282
|
+
? fm.disallowedTools.split(",").map((t) => t.trim())
|
|
283
|
+
: undefined,
|
|
284
|
+
model: fm.model,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const BUNDLED_AGENT_DIR = resolve(
|
|
289
|
+
dirname(import.meta.path),
|
|
290
|
+
"..",
|
|
291
|
+
"..",
|
|
292
|
+
"..",
|
|
293
|
+
"..",
|
|
294
|
+
"skill",
|
|
295
|
+
"agents",
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
/** Check if an agent entry was created by selftune. */
|
|
299
|
+
function isSelftuneAgent(entry: OpenCodeAgentConfig): boolean {
|
|
300
|
+
return (
|
|
301
|
+
typeof entry.description === "string" && entry.description.startsWith(SELFTUNE_AGENT_PREFIX)
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Discover agent definitions from skill/agents/ and build OpenCode agent config entries. */
|
|
306
|
+
export function buildAgentEntries(
|
|
307
|
+
agentsDir: string = BUNDLED_AGENT_DIR,
|
|
308
|
+
): Record<string, OpenCodeAgentConfig> {
|
|
309
|
+
const entries: Record<string, OpenCodeAgentConfig> = {};
|
|
310
|
+
|
|
311
|
+
if (!existsSync(agentsDir)) return entries;
|
|
312
|
+
|
|
313
|
+
const files = readdirSync(agentsDir).filter((f: string) => f.endsWith(".md"));
|
|
314
|
+
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
const filePath = join(agentsDir, file);
|
|
317
|
+
const content = readFileSync(filePath, "utf-8");
|
|
318
|
+
const fm = parseFrontmatter(content);
|
|
319
|
+
if (!fm) continue;
|
|
320
|
+
|
|
321
|
+
// Strip frontmatter to get the body as the prompt
|
|
322
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
|
|
323
|
+
|
|
324
|
+
entries[fm.name] = {
|
|
325
|
+
description: `${SELFTUNE_AGENT_PREFIX} ${fm.description ?? fm.name}`,
|
|
326
|
+
mode: "subagent",
|
|
327
|
+
model: fm.model ? (OPENCODE_MODEL_MAP[fm.model] ?? fm.model) : undefined,
|
|
328
|
+
prompt: body,
|
|
329
|
+
tools: mapToolPermissions(fm.tools, fm.disallowedTools),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return entries;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Plugin path helpers
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Determine where to write the plugin file.
|
|
342
|
+
* Uses the global plugins dir (~/.config/opencode/plugins/) since it
|
|
343
|
+
* works regardless of which project the user is in.
|
|
344
|
+
*/
|
|
345
|
+
function getPluginInstallPath(): string {
|
|
346
|
+
return join(getGlobalPluginsDir(), PLUGIN_FILENAME);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** All candidate plugin locations to check during uninstall. */
|
|
350
|
+
function getPluginCandidatePaths(): string[] {
|
|
351
|
+
return [
|
|
352
|
+
join(getGlobalPluginsDir(), PLUGIN_FILENAME),
|
|
353
|
+
join(getProjectPluginsDir(), PLUGIN_FILENAME),
|
|
354
|
+
// Legacy locations from previous installer versions
|
|
355
|
+
join(dirname(getUserConfigPath()), PLUGIN_FILENAME),
|
|
356
|
+
join(process.cwd(), PLUGIN_FILENAME),
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// Install / Uninstall
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
function doInstall(options: InstallOptions): void {
|
|
365
|
+
const configPath = detectConfigPath();
|
|
366
|
+
const pluginPath = getPluginInstallPath();
|
|
367
|
+
const agentEntries = buildAgentEntries();
|
|
368
|
+
|
|
369
|
+
// Validate config before touching filesystem
|
|
370
|
+
const config = readConfig(configPath);
|
|
371
|
+
|
|
372
|
+
if (options.dryRun) {
|
|
373
|
+
console.log(`[selftune] dry-run: would write plugin to ${pluginPath}`);
|
|
374
|
+
console.log(`[selftune] dry-run: would update config at ${configPath}`);
|
|
375
|
+
for (const name of Object.keys(agentEntries)) {
|
|
376
|
+
console.log(`[selftune] dry-run: would register agent '${name}'`);
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Write plugin file to plugins directory (auto-discovered by OpenCode)
|
|
382
|
+
const pluginDir = dirname(pluginPath);
|
|
383
|
+
if (!existsSync(pluginDir)) {
|
|
384
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
385
|
+
}
|
|
386
|
+
writeFileSync(pluginPath, buildPluginContent(), { mode: 0o644 });
|
|
387
|
+
|
|
388
|
+
// Register agents in config (no plugin array entry needed — plugins dir is auto-discovered)
|
|
389
|
+
let configChanged = false;
|
|
390
|
+
if (Object.keys(agentEntries).length > 0) {
|
|
391
|
+
if (!config.agent) {
|
|
392
|
+
config.agent = {};
|
|
393
|
+
}
|
|
394
|
+
for (const [name, entry] of Object.entries(agentEntries)) {
|
|
395
|
+
const existing = config.agent[name];
|
|
396
|
+
if (existing && !isSelftuneAgent(existing)) {
|
|
397
|
+
console.log(`[selftune] Warning: agent '${name}' already configured by user; skipping.`);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
config.agent[name] = entry;
|
|
401
|
+
configChanged = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Clean up any legacy plugin array entries from previous installer versions
|
|
406
|
+
if (Array.isArray(config.plugin)) {
|
|
407
|
+
const before = config.plugin.length;
|
|
408
|
+
config.plugin = (config.plugin as string[]).filter((p: string) => !p.includes(PLUGIN_FILENAME));
|
|
409
|
+
if (config.plugin.length === 0) {
|
|
410
|
+
delete config.plugin;
|
|
411
|
+
}
|
|
412
|
+
if (config.plugin?.length !== before) {
|
|
413
|
+
configChanged = true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (configChanged) {
|
|
418
|
+
writeConfig(configPath, config);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log(`[selftune] Installed OpenCode plugin:`);
|
|
422
|
+
console.log(` plugin: ${pluginPath}`);
|
|
423
|
+
console.log(` config: ${configPath}`);
|
|
424
|
+
if (Object.keys(agentEntries).length > 0) {
|
|
425
|
+
console.log(`[selftune] Registered agents:`);
|
|
426
|
+
for (const name of Object.keys(agentEntries)) {
|
|
427
|
+
console.log(` ${name}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function doUninstall(options: InstallOptions): void {
|
|
433
|
+
const configPath = detectConfigPath();
|
|
434
|
+
|
|
435
|
+
if (options.dryRun) {
|
|
436
|
+
console.log(`[selftune] dry-run: would remove plugin from plugins directories`);
|
|
437
|
+
console.log(`[selftune] dry-run: would remove agent entries from ${configPath}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Update config first — remove agents and any legacy plugin array entries
|
|
442
|
+
if (existsSync(configPath)) {
|
|
443
|
+
const config = readConfig(configPath);
|
|
444
|
+
let changed = false;
|
|
445
|
+
|
|
446
|
+
// Remove legacy plugin array entries
|
|
447
|
+
if (Array.isArray(config.plugin)) {
|
|
448
|
+
const before = config.plugin.length;
|
|
449
|
+
config.plugin = (config.plugin as string[]).filter(
|
|
450
|
+
(p: string) => !p.includes(PLUGIN_FILENAME),
|
|
451
|
+
);
|
|
452
|
+
if (config.plugin.length === 0) {
|
|
453
|
+
delete config.plugin;
|
|
454
|
+
}
|
|
455
|
+
if (config.plugin?.length !== before) {
|
|
456
|
+
changed = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Remove selftune-managed agents
|
|
461
|
+
if (config.agent) {
|
|
462
|
+
for (const [name, entry] of Object.entries(config.agent)) {
|
|
463
|
+
if (!isSelftuneAgent(entry)) continue;
|
|
464
|
+
delete config.agent[name];
|
|
465
|
+
changed = true;
|
|
466
|
+
}
|
|
467
|
+
if (Object.keys(config.agent).length === 0) {
|
|
468
|
+
delete config.agent;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (changed) {
|
|
473
|
+
writeConfig(configPath, config);
|
|
474
|
+
console.log(`[selftune] Removed agent entries from: ${configPath}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Remove plugin files from all candidate locations
|
|
479
|
+
for (const pluginPath of getPluginCandidatePaths()) {
|
|
480
|
+
if (existsSync(pluginPath)) {
|
|
481
|
+
unlinkSync(pluginPath);
|
|
482
|
+
console.log(`[selftune] Removed plugin: ${pluginPath}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Clean up legacy shim if present
|
|
487
|
+
for (const dir of [dirname(getUserConfigPath()), process.cwd()]) {
|
|
488
|
+
const legacyShim = join(dir, "selftune-opencode-hook.sh");
|
|
489
|
+
if (existsSync(legacyShim)) {
|
|
490
|
+
unlinkSync(legacyShim);
|
|
491
|
+
console.log(`[selftune] Removed legacy shim: ${legacyShim}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Clean up legacy config.json if it exists (old installer wrote to wrong filename)
|
|
496
|
+
const legacyConfig = join(dirname(getUserConfigPath()), "config.json");
|
|
497
|
+
if (existsSync(legacyConfig)) {
|
|
498
|
+
try {
|
|
499
|
+
const content = JSON.parse(readFileSync(legacyConfig, "utf-8"));
|
|
500
|
+
// Only remove if it looks like our leftover (tiny file with just autoupdate/schema)
|
|
501
|
+
const keys = Object.keys(content).filter((k) => k !== "$schema");
|
|
502
|
+
if (keys.length <= 1 && (keys[0] === "autoupdate" || keys.length === 0)) {
|
|
503
|
+
unlinkSync(legacyConfig);
|
|
504
|
+
console.log(`[selftune] Removed legacy config: ${legacyConfig}`);
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
// Not valid JSON or can't read — leave it alone
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
console.log(`[selftune] OpenCode plugin and agents uninstalled.`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
// CLI entry
|
|
516
|
+
// ---------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
export async function cliMain(): Promise<void> {
|
|
519
|
+
const args = process.argv.slice(2);
|
|
520
|
+
const options = parseFlags(args);
|
|
521
|
+
if (!options) return; // --help was shown
|
|
522
|
+
|
|
523
|
+
if (options.uninstall) {
|
|
524
|
+
doUninstall(options);
|
|
525
|
+
} else {
|
|
526
|
+
doInstall(options);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// stdin main (only when executed directly, not when imported)
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
if (import.meta.main) {
|
|
535
|
+
try {
|
|
536
|
+
await cliMain();
|
|
537
|
+
} catch (err) {
|
|
538
|
+
console.error(
|
|
539
|
+
`[selftune] OpenCode install failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
540
|
+
);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
}
|