@mirrowel/opencode-souk 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/IMPLEMENTATION_PLAN.md +176 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/config.d.ts +1093 -0
- package/dist/config.js +496 -0
- package/dist/forge.d.ts +3 -0
- package/dist/forge.js +78 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +303 -0
- package/dist/install.d.ts +18 -0
- package/dist/install.js +719 -0
- package/dist/registry.d.ts +67 -0
- package/dist/registry.js +447 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +2 -0
- package/dist/tui.d.ts +6 -0
- package/dist/tui.js +1686 -0
- package/docs/CONFIG.md +67 -0
- package/package.json +86 -0
- package/souk.example.jsonc +68 -0
- package/src/skill/souk-installer/SKILL.md +313 -0
- package/src/tui.tsx +1892 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
4
|
+
import { debugLog, loadSidecarSafe, personalityDescription, RegistryItem as RegistryItemSchema, splitModelRef } from "./config.js";
|
|
5
|
+
import { nativeInstall, previewNativeInstall, scopeLabel } from "./install.js";
|
|
6
|
+
import { loadRegistry } from "./registry.js";
|
|
7
|
+
const KAF_PLAN = "kāf-plan";
|
|
8
|
+
const KAF_ACTION = "kāf-action";
|
|
9
|
+
const ACTION_TOOL = "souk_request_action";
|
|
10
|
+
const REGISTRY_SEARCH_TOOL = "souk_registry_search";
|
|
11
|
+
const NATIVE_PREVIEW_TOOL = "souk_native_preview";
|
|
12
|
+
const NATIVE_INSTALL_TOOL = "souk_native_install";
|
|
13
|
+
const plugin = async ({ client }) => {
|
|
14
|
+
return {
|
|
15
|
+
async config(config) {
|
|
16
|
+
installSoukSkillPath(config);
|
|
17
|
+
installKafAgents(config);
|
|
18
|
+
installToolPermissions(config);
|
|
19
|
+
},
|
|
20
|
+
tool: {
|
|
21
|
+
[ACTION_TOOL]: tool({
|
|
22
|
+
description: "Kāf-only tool. Ask the user yes/no before switching a Souk Forge session from plan mode to action mode. Include security risks and planned writes/actions.",
|
|
23
|
+
args: {
|
|
24
|
+
plan: tool.schema.string().describe("The approved plan Kāf wants to execute."),
|
|
25
|
+
security_risks: tool.schema.array(tool.schema.string()).describe("Security risks the user must review before action mode."),
|
|
26
|
+
planned_actions: tool.schema.array(tool.schema.string()).describe("Concrete writes, installs, commands, or config changes planned."),
|
|
27
|
+
},
|
|
28
|
+
async execute(args, context) {
|
|
29
|
+
if (context.agent !== KAF_PLAN && context.agent !== KAF_ACTION) {
|
|
30
|
+
return {
|
|
31
|
+
title: "Souk action denied",
|
|
32
|
+
output: "This tool is exclusive to Kāf Forge agents and cannot be used by this agent.",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (context.agent === KAF_ACTION) {
|
|
36
|
+
return {
|
|
37
|
+
title: "Already in action mode",
|
|
38
|
+
output: "This Forge session is already using Kāf action mode. Continue executing only the approved plan.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const risks = args.security_risks.length ? args.security_risks : ["No specific risks were supplied. Treat this as incomplete and continue planning if unsure."];
|
|
42
|
+
const actions = args.planned_actions.length ? args.planned_actions : ["No concrete actions were supplied. Do not proceed without a concrete plan."];
|
|
43
|
+
const summary = [
|
|
44
|
+
"Kāf is asking to switch this Souk Forge session from plan mode to action mode.",
|
|
45
|
+
"",
|
|
46
|
+
"Security risks:",
|
|
47
|
+
...risks.map((risk) => `- ${risk}`),
|
|
48
|
+
"",
|
|
49
|
+
"Planned actions:",
|
|
50
|
+
...actions.map((action) => `- ${action}`),
|
|
51
|
+
"",
|
|
52
|
+
"The souk can make mistakes. Approve only if the plan and risks are acceptable.",
|
|
53
|
+
].join("\n");
|
|
54
|
+
try {
|
|
55
|
+
await context.ask({
|
|
56
|
+
permission: ACTION_TOOL,
|
|
57
|
+
patterns: ["Switch Kāf Forge session to action mode"],
|
|
58
|
+
always: [],
|
|
59
|
+
metadata: {
|
|
60
|
+
title: "Approve Kāf action mode?",
|
|
61
|
+
summary,
|
|
62
|
+
risks,
|
|
63
|
+
actions,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return {
|
|
69
|
+
title: "Action mode not approved",
|
|
70
|
+
output: [
|
|
71
|
+
"The user chose no. Nothing changed.",
|
|
72
|
+
"Continue planning in Kāf plan mode. Address the user's concerns, refine the plan, and propose switching again only when appropriate.",
|
|
73
|
+
].join("\n"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
queueActionContinuation(client, context.sessionID, args.plan, risks, actions);
|
|
77
|
+
return {
|
|
78
|
+
title: "Kāf action approved",
|
|
79
|
+
output: "The user approved action mode. A same-session Kāf action continuation has been queued. Execute only the approved plan and keep asking before risky writes.",
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
[REGISTRY_SEARCH_TOOL]: tool({
|
|
84
|
+
description: "Kāf-only read-only tool. Search Souk's deduped registry cache/source results and return combo item metadata with all source provenance.",
|
|
85
|
+
args: {
|
|
86
|
+
query: tool.schema.string().describe("Search text. Matches name, description, kind, tags, source, aliases, repo URL, and install specs."),
|
|
87
|
+
limit: tool.schema.number().int().min(1).max(50).default(10).describe("Maximum result count."),
|
|
88
|
+
refresh: tool.schema.boolean().default(false).describe("Fetch sources before searching instead of using cache."),
|
|
89
|
+
},
|
|
90
|
+
async execute(args, context) {
|
|
91
|
+
if (!isKaf(context.agent))
|
|
92
|
+
return deniedTool(REGISTRY_SEARCH_TOOL);
|
|
93
|
+
const loaded = loadSidecarSafe();
|
|
94
|
+
const cache = await loadRegistry(loaded.config, { force: args.refresh });
|
|
95
|
+
const terms = args.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
96
|
+
const results = cache.items.filter((item) => terms.every((term) => registryHaystack(item).includes(term))).slice(0, args.limit);
|
|
97
|
+
return {
|
|
98
|
+
title: `Souk registry search: ${results.length} result(s)`,
|
|
99
|
+
output: JSON.stringify({ fetchedAt: cache.fetchedAt, sourceStatus: cache.sourceStatus, results }, null, 2),
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
[NATIVE_PREVIEW_TOOL]: tool({
|
|
104
|
+
description: "Kāf-only read-only tool. Preview Souk deterministic native install actions for selected registry item JSON and scope.",
|
|
105
|
+
args: nativeToolArgs(),
|
|
106
|
+
async execute(args, context) {
|
|
107
|
+
if (!isKaf(context.agent))
|
|
108
|
+
return deniedTool(NATIVE_PREVIEW_TOOL);
|
|
109
|
+
const loaded = loadSidecarSafe();
|
|
110
|
+
const scope = parseToolScope(args.scope_kind, args.project_path, context);
|
|
111
|
+
const items = parseToolItems(args.items_json);
|
|
112
|
+
const preview = await previewNativeInstall(items, scope, loaded.config.install);
|
|
113
|
+
return {
|
|
114
|
+
title: `Souk native preview: ${preview.supported ? "supported" : "unsupported"}`,
|
|
115
|
+
output: [`Scope: ${scopeLabel(scope)}`, "", preview.summary, "", "Warnings:", ...preview.warnings.map((warning) => `- ${warning}`)].join("\n"),
|
|
116
|
+
metadata: { supported: preview.supported, warnings: preview.warnings },
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
[NATIVE_INSTALL_TOOL]: tool({
|
|
121
|
+
description: "Kāf action-mode-only tool. Execute Souk deterministic native install actions for approved registry item JSON and scope. Prefer this over manual config edits for supported items.",
|
|
122
|
+
args: nativeToolArgs(),
|
|
123
|
+
async execute(args, context) {
|
|
124
|
+
if (context.agent !== KAF_ACTION) {
|
|
125
|
+
return {
|
|
126
|
+
title: "Souk native install denied",
|
|
127
|
+
output: "souk_native_install is only available to Kāf action mode after the user approves souk_request_action. In plan mode, use souk_native_preview instead.",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const loaded = loadSidecarSafe();
|
|
131
|
+
const scope = parseToolScope(args.scope_kind, args.project_path, context);
|
|
132
|
+
const items = parseToolItems(args.items_json);
|
|
133
|
+
const preview = await previewNativeInstall(items, scope, loaded.config.install);
|
|
134
|
+
await context.ask({
|
|
135
|
+
permission: NATIVE_INSTALL_TOOL,
|
|
136
|
+
patterns: [`Native install ${items.length} item(s) to ${scopeLabel(scope)}`],
|
|
137
|
+
always: [],
|
|
138
|
+
metadata: { title: "Approve Souk native install?", scope: scopeLabel(scope), summary: preview.summary, warnings: preview.warnings },
|
|
139
|
+
});
|
|
140
|
+
const result = await nativeInstall({}, items, scope, {
|
|
141
|
+
confirm: async (title, message) => {
|
|
142
|
+
await context.ask({ permission: NATIVE_INSTALL_TOOL, patterns: [title], always: [], metadata: { title, summary: message } });
|
|
143
|
+
return true;
|
|
144
|
+
},
|
|
145
|
+
}, loaded.config.install);
|
|
146
|
+
return {
|
|
147
|
+
title: "Souk native install result",
|
|
148
|
+
output: ["Installed:", ...result.installed.map((line) => `- ${line}`), "", "Failed:", ...result.failed.map((line) => `- ${line}`)].join("\n"),
|
|
149
|
+
metadata: result,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
function nativeToolArgs() {
|
|
157
|
+
return {
|
|
158
|
+
items_json: tool.schema.string().describe("JSON array of Souk RegistryItem objects, usually copied from souk_registry_search or the Forge prompt selected items."),
|
|
159
|
+
scope_kind: tool.schema.enum(["global", "project"]).describe("Install scope."),
|
|
160
|
+
project_path: tool.schema.string().optional().describe("Project root for project scope. Defaults to current worktree when omitted."),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function isKaf(agent) {
|
|
164
|
+
return agent === KAF_PLAN || agent === KAF_ACTION;
|
|
165
|
+
}
|
|
166
|
+
function deniedTool(name) {
|
|
167
|
+
return { title: "Souk tool denied", output: `${name} is exclusive to Kāf Forge agents and cannot be used by this agent.` };
|
|
168
|
+
}
|
|
169
|
+
function parseToolItems(json) {
|
|
170
|
+
const parsed = JSON.parse(json);
|
|
171
|
+
const list = Array.isArray(parsed) ? parsed : [parsed];
|
|
172
|
+
return list.map((item) => RegistryItemSchema.parse(item));
|
|
173
|
+
}
|
|
174
|
+
function parseToolScope(kind, projectPath, context) {
|
|
175
|
+
if (kind === "global")
|
|
176
|
+
return { kind: "global" };
|
|
177
|
+
return { kind: "project", path: projectPath?.trim() || context.worktree };
|
|
178
|
+
}
|
|
179
|
+
function registryHaystack(item) {
|
|
180
|
+
return [
|
|
181
|
+
item.name,
|
|
182
|
+
item.description,
|
|
183
|
+
item.kind,
|
|
184
|
+
item.confidence,
|
|
185
|
+
item.source,
|
|
186
|
+
item.sourceType,
|
|
187
|
+
item.repoUrl,
|
|
188
|
+
item.homepageUrl,
|
|
189
|
+
item.install?.spec,
|
|
190
|
+
...(item.install?.specs ?? []),
|
|
191
|
+
...item.tags,
|
|
192
|
+
...item.aliases,
|
|
193
|
+
...item.alternateKinds,
|
|
194
|
+
...item.sources.flatMap((source) => [source.source, source.sourceType, source.name, source.description, source.repoUrl, source.kind, source.confidence]),
|
|
195
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
196
|
+
}
|
|
197
|
+
function installSoukSkillPath(config) {
|
|
198
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
199
|
+
const skillDir = join(base, "..", "src", "skill");
|
|
200
|
+
const cfg = config;
|
|
201
|
+
cfg.skills = cfg.skills ?? {};
|
|
202
|
+
cfg.skills.paths = Array.from(new Set([...(cfg.skills.paths ?? []), skillDir]));
|
|
203
|
+
}
|
|
204
|
+
function installKafAgents(config) {
|
|
205
|
+
const loaded = loadSidecarSafe();
|
|
206
|
+
const sidecar = loaded.config;
|
|
207
|
+
if (loaded.error)
|
|
208
|
+
debugLog("Souk config parse failed", `Using defaults for Kāf agents: ${loaded.error}`, true);
|
|
209
|
+
const model = splitModelRef(sidecar.forge.agent.model);
|
|
210
|
+
const promptBase = [
|
|
211
|
+
"You are Kāf, the dedicated OpenCode Souk Forge agent.",
|
|
212
|
+
`Personality preset: ${sidecar.forge.agent.personality}`,
|
|
213
|
+
personalityDescription(sidecar.forge.agent.personality),
|
|
214
|
+
"Always load and use the customize-opencode and souk-installer skills before planning or installing.",
|
|
215
|
+
"Security analysis is mandatory before any install or config change.",
|
|
216
|
+
"The souk can make mistakes; be explicit about uncertainty and ask before risky writes.",
|
|
217
|
+
].join("\n");
|
|
218
|
+
const cfg = config;
|
|
219
|
+
cfg.agent = cfg.agent ?? {};
|
|
220
|
+
cfg.agent[KAF_PLAN] = {
|
|
221
|
+
description: "Kāf Forge planning agent for OpenCode Souk installs. Read-only; asks before action mode.",
|
|
222
|
+
mode: "primary",
|
|
223
|
+
hidden: true,
|
|
224
|
+
...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
|
|
225
|
+
...(sidecar.forge.agent.variant ? { variant: sidecar.forge.agent.variant } : {}),
|
|
226
|
+
...(sidecar.forge.agent.temperature !== undefined ? { temperature: sidecar.forge.agent.temperature } : {}),
|
|
227
|
+
...(sidecar.forge.agent.top_p !== undefined ? { top_p: sidecar.forge.agent.top_p } : {}),
|
|
228
|
+
options: sidecar.forge.agent.options ?? {},
|
|
229
|
+
prompt: [promptBase, "You are in plan mode. Do not edit, write, patch, run install commands, or mutate the system. You may use MCP tools for research, inspection, and appraisal, but not for mutation. Use souk_request_action when ready to ask for action mode."].join("\n\n"),
|
|
230
|
+
permission: {
|
|
231
|
+
edit: "deny",
|
|
232
|
+
bash: "deny",
|
|
233
|
+
skill: "allow",
|
|
234
|
+
read: "allow",
|
|
235
|
+
grep: "allow",
|
|
236
|
+
glob: "allow",
|
|
237
|
+
webfetch: "allow",
|
|
238
|
+
websearch: "allow",
|
|
239
|
+
[ACTION_TOOL]: "allow",
|
|
240
|
+
[REGISTRY_SEARCH_TOOL]: "allow",
|
|
241
|
+
[NATIVE_PREVIEW_TOOL]: "allow",
|
|
242
|
+
[NATIVE_INSTALL_TOOL]: "deny",
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
cfg.agent[KAF_ACTION] = {
|
|
246
|
+
description: "Kāf Forge action agent for approved OpenCode Souk install plans.",
|
|
247
|
+
mode: "primary",
|
|
248
|
+
hidden: true,
|
|
249
|
+
...(model ? { model: `${model.providerID}/${model.modelID}` } : {}),
|
|
250
|
+
...(sidecar.forge.agent.variant ? { variant: sidecar.forge.agent.variant } : {}),
|
|
251
|
+
...(sidecar.forge.agent.temperature !== undefined ? { temperature: sidecar.forge.agent.temperature } : {}),
|
|
252
|
+
...(sidecar.forge.agent.top_p !== undefined ? { top_p: sidecar.forge.agent.top_p } : {}),
|
|
253
|
+
options: sidecar.forge.agent.options ?? {},
|
|
254
|
+
prompt: [promptBase, "You are in action mode. Execute only the approved Souk Forge plan. Ask before risky writes, network installs, or ambiguous changes."].join("\n\n"),
|
|
255
|
+
permission: {
|
|
256
|
+
edit: "ask",
|
|
257
|
+
bash: "ask",
|
|
258
|
+
skill: "allow",
|
|
259
|
+
read: "allow",
|
|
260
|
+
grep: "allow",
|
|
261
|
+
glob: "allow",
|
|
262
|
+
webfetch: "allow",
|
|
263
|
+
websearch: "allow",
|
|
264
|
+
[ACTION_TOOL]: "allow",
|
|
265
|
+
[REGISTRY_SEARCH_TOOL]: "allow",
|
|
266
|
+
[NATIVE_PREVIEW_TOOL]: "allow",
|
|
267
|
+
[NATIVE_INSTALL_TOOL]: "ask",
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function installToolPermissions(config) {
|
|
272
|
+
const cfg = config;
|
|
273
|
+
if (cfg.permission === undefined)
|
|
274
|
+
cfg.permission = { [ACTION_TOOL]: "deny" };
|
|
275
|
+
if (cfg.permission && typeof cfg.permission === "object" && !Array.isArray(cfg.permission)) {
|
|
276
|
+
;
|
|
277
|
+
cfg.permission[ACTION_TOOL] = "deny";
|
|
278
|
+
cfg.permission[REGISTRY_SEARCH_TOOL] = "deny";
|
|
279
|
+
cfg.permission[NATIVE_PREVIEW_TOOL] = "deny";
|
|
280
|
+
cfg.permission[NATIVE_INSTALL_TOOL] = "deny";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function queueActionContinuation(client, sessionID, plan, risks, actions) {
|
|
284
|
+
const api = client;
|
|
285
|
+
const prompt = [
|
|
286
|
+
"User approved Kāf action mode for this Souk Forge session.",
|
|
287
|
+
"Execute only the approved plan below. Do not expand scope without asking.",
|
|
288
|
+
"",
|
|
289
|
+
"Approved plan:",
|
|
290
|
+
plan,
|
|
291
|
+
"",
|
|
292
|
+
"Security risks acknowledged:",
|
|
293
|
+
...risks.map((risk) => `- ${risk}`),
|
|
294
|
+
"",
|
|
295
|
+
"Planned actions:",
|
|
296
|
+
...actions.map((action) => `- ${action}`),
|
|
297
|
+
].join("\n");
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
const payload = { sessionID, agent: KAF_ACTION, parts: [{ type: "text", text: prompt }] };
|
|
300
|
+
void (api.session?.promptAsync?.(payload) ?? api.session?.prompt?.(payload))?.catch(() => undefined);
|
|
301
|
+
}, 0);
|
|
302
|
+
}
|
|
303
|
+
export default plugin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
2
|
+
import type { RegistryItem, ScopeChoice, SidecarConfig } from "./config.js";
|
|
3
|
+
export type NativePreview = {
|
|
4
|
+
supported: boolean;
|
|
5
|
+
summary: string;
|
|
6
|
+
warnings: string[];
|
|
7
|
+
};
|
|
8
|
+
export type NativeInstallCallbacks = {
|
|
9
|
+
confirm?: (title: string, message: string) => Promise<boolean>;
|
|
10
|
+
};
|
|
11
|
+
type InstallSettings = SidecarConfig["install"];
|
|
12
|
+
export declare function previewNativeInstall(items: RegistryItem[], scope: ScopeChoice, settings?: InstallSettings): Promise<NativePreview>;
|
|
13
|
+
export declare function nativeInstall(api: Partial<TuiPluginApi>, items: RegistryItem[], scope: ScopeChoice, callbacks?: NativeInstallCallbacks, settings?: InstallSettings): Promise<{
|
|
14
|
+
installed: string[];
|
|
15
|
+
failed: string[];
|
|
16
|
+
}>;
|
|
17
|
+
declare function scopeLabel(scope: ScopeChoice): string;
|
|
18
|
+
export { scopeLabel };
|