akm-opencode 0.0.0 → 0.0.19
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 +111 -5
- package/index.ts +866 -0
- package/package.json +29 -9
- package/index.js +0 -4
package/README.md
CHANGED
|
@@ -1,9 +1,115 @@
|
|
|
1
1
|
# akm-opencode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenCode plugin for the [Agentikit](https://github.com/itlackey/agentikit) CLI. Registers tools that let your AI agent **search**, **show**, and **manage** extension assets from stash directories and registries.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
tools, skills, commands, agents, and knowledge documents.
|
|
5
|
+
## Installation
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Add to your OpenCode config (`opencode.json`):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"plugin": ["akm-opencode"]
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
| Tool | Description |
|
|
18
|
+
|------|-------------|
|
|
19
|
+
| `akm_search` | Search the local stash, the registry, or both for scripts, skills, commands, agents, and knowledge |
|
|
20
|
+
| `akm_registry_search` | Search configured registries for installable kits and optional asset-level hits |
|
|
21
|
+
| `akm_show` | Show a stash asset by its ref |
|
|
22
|
+
| `akm_index` | Build or rebuild the search index |
|
|
23
|
+
| `akm_agent` | Dispatch a stash `agent:*` into OpenCode using the stash prompt and metadata |
|
|
24
|
+
| `akm_cmd` | Execute a stash `command:*` template in OpenCode via SDK session prompting |
|
|
25
|
+
| `akm_add` | Install kits from npm, GitHub, git URLs, or local directories |
|
|
26
|
+
| `akm_list` | List installed registry kits |
|
|
27
|
+
| `akm_remove` | Remove an installed registry kit and reindex |
|
|
28
|
+
| `akm_update` | Update one installed kit or all installed kits |
|
|
29
|
+
| `akm_clone` | Clone an asset into the working stash or a custom destination for editing |
|
|
30
|
+
| `akm_config` | Get, set, unset, list, or inspect akm configuration (including `config path --all`) |
|
|
31
|
+
| `akm_run` | Execute a stash script using its `run` field |
|
|
32
|
+
| `akm_sources` | List all resolved stash search paths |
|
|
33
|
+
| `akm_upgrade` | Check for or install akm CLI updates |
|
|
34
|
+
|
|
35
|
+
### Registry discovery
|
|
36
|
+
|
|
37
|
+
Use either:
|
|
38
|
+
|
|
39
|
+
- `akm_search` with `source: "registry"` or `source: "both"`
|
|
40
|
+
- `akm_registry_search` when you only want installable community kits
|
|
41
|
+
|
|
42
|
+
Registry hits include `id`, `installRef`, and `action` fields. Use `installRef` when passing a result into `akm_add`; registry-specific IDs are not installable refs. Use `assets: true` when you also want asset-level matches from registry v2 indexes.
|
|
43
|
+
|
|
44
|
+
## Agent Dispatch
|
|
45
|
+
|
|
46
|
+
Use `akm_agent` after retrieving an agent ref from `akm_search`.
|
|
47
|
+
|
|
48
|
+
Inputs:
|
|
49
|
+
- `ref` (optional): stash ref like `agent:coach.md`
|
|
50
|
+
- `query` (optional): resolve best matching stash agent when `ref` is omitted
|
|
51
|
+
- `task_prompt` (required): user task to run
|
|
52
|
+
- `dispatch_agent` (optional): OpenCode agent name (defaults to `general`)
|
|
53
|
+
- `as_subtask` (optional): create child session (defaults to `true`)
|
|
54
|
+
|
|
55
|
+
At least one of `ref` or `query` is required.
|
|
56
|
+
|
|
57
|
+
Behavior:
|
|
58
|
+
- Loads the stash agent via `akm show`
|
|
59
|
+
- Uses stash `prompt` verbatim as OpenCode `system`
|
|
60
|
+
- Applies stash `modelHint` when in `provider/model` format
|
|
61
|
+
- Applies stash `toolPolicy` when it maps to boolean tool flags
|
|
62
|
+
|
|
63
|
+
## Command Execution
|
|
64
|
+
|
|
65
|
+
Use `akm_cmd` to execute stash command templates through the OpenCode SDK.
|
|
66
|
+
|
|
67
|
+
Inputs:
|
|
68
|
+
- `ref` (optional): stash ref like `command:review.md`
|
|
69
|
+
- `query` (optional): resolve best matching stash command when `ref` is omitted
|
|
70
|
+
- `arguments` (optional): raw command arguments for `$ARGUMENTS`, `$1`, `$2`, etc.
|
|
71
|
+
- `dispatch_agent` (optional): OpenCode agent name (defaults to current agent)
|
|
72
|
+
- `as_subtask` (optional): create child session (defaults to `false`)
|
|
73
|
+
|
|
74
|
+
At least one of `ref` or `query` is required.
|
|
75
|
+
|
|
76
|
+
## Prerequisites
|
|
77
|
+
|
|
78
|
+
The plugin prefers an existing `akm` on PATH. If `akm` is missing and `bun` is available, it will attempt `bun install -g akm-cli` automatically. It does not run the standalone shell installers automatically. If Bun is not available, install `akm` from the [agentikit repo](https://github.com/itlackey/agentikit).
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
# macOS / Linux
|
|
82
|
+
curl -fsSL https://raw.githubusercontent.com/itlackey/agentikit/main/install.sh | bash
|
|
83
|
+
# PowerShell (Windows)
|
|
84
|
+
irm https://raw.githubusercontent.com/itlackey/agentikit/main/install.ps1 -OutFile install.ps1; ./install.ps1
|
|
85
|
+
|
|
86
|
+
# Or via Bun
|
|
87
|
+
bun install -g akm-cli
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Stash model
|
|
91
|
+
|
|
92
|
+
The stash directory is resolved automatically via a three-tier fallback: `AKM_STASH_DIR` env var (optional override) → `stashDir` in `config.json` → platform default. Set it persistently with:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
akm config set stashDir /abs/path/to/your-stash
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Expected layout:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
stash/
|
|
102
|
+
├── scripts/ # executable scripts (.sh, .ts, .js, .ps1, .cmd, .bat, .py, .rb, .go, .pl, .php, .lua, .r, .swift, .kt)
|
|
103
|
+
├── skills/ # skill directories containing SKILL.md
|
|
104
|
+
├── commands/ # markdown files
|
|
105
|
+
├── agents/ # markdown files
|
|
106
|
+
└── knowledge/ # markdown files
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Assets are resolved from three source types: **working** (local stash), **search paths** (additional dirs via `searchPaths` config), and **installed** (registry kits via `akm add`).
|
|
110
|
+
|
|
111
|
+
## Docs
|
|
112
|
+
|
|
113
|
+
- [Agentikit CLI](https://github.com/itlackey/agentikit)
|
|
114
|
+
- [OpenCode Plugins](https://opencode.ai/docs/plugins/)
|
|
115
|
+
- [OpenCode Custom Tools](https://opencode.ai/docs/custom-tools/)
|
package/index.ts
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { execFileSync, execSync } from "node:child_process"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
let resolvedAkmCommand = "akm"
|
|
6
|
+
let attemptedAutoInstall = false
|
|
7
|
+
|
|
8
|
+
type LogLevel = "debug" | "info" | "warn" | "error"
|
|
9
|
+
|
|
10
|
+
type LogCapableClient = {
|
|
11
|
+
app: {
|
|
12
|
+
log: (options: {
|
|
13
|
+
query?: { directory?: string }
|
|
14
|
+
body: {
|
|
15
|
+
service: string
|
|
16
|
+
level: LogLevel
|
|
17
|
+
message: string
|
|
18
|
+
extra?: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
}) => Promise<unknown>
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type CliLogMeta = {
|
|
25
|
+
toolName: string
|
|
26
|
+
directory?: string
|
|
27
|
+
sessionID?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatCliError(error: unknown): string {
|
|
31
|
+
if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") {
|
|
32
|
+
return "The 'akm' CLI was not found on PATH. Install it first from https://github.com/itlackey/agentikit."
|
|
33
|
+
}
|
|
34
|
+
return error instanceof Error ? error.message : String(error)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toLogString(value: unknown): string | undefined {
|
|
38
|
+
if (typeof value === "string") return value
|
|
39
|
+
if (value instanceof Buffer) return value.toString("utf8")
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getExecStatus(error: unknown): number | null {
|
|
44
|
+
if (!error || typeof error !== "object" || !("status" in error)) return null
|
|
45
|
+
const status = (error as { status?: unknown }).status
|
|
46
|
+
return typeof status === "number" ? status : null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function writePluginLog(client: LogCapableClient, level: LogLevel, message: string, extra: Record<string, unknown>) {
|
|
50
|
+
try {
|
|
51
|
+
await client.app.log({
|
|
52
|
+
query: typeof extra.directory === "string" ? { directory: extra.directory } : undefined,
|
|
53
|
+
body: {
|
|
54
|
+
service: "akm-opencode",
|
|
55
|
+
level,
|
|
56
|
+
message,
|
|
57
|
+
extra,
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
} catch {
|
|
61
|
+
// Avoid breaking the TUI if logging itself fails.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getCommandStatus(command: string): "ok" | "missing" | "error" {
|
|
66
|
+
try {
|
|
67
|
+
execFileSync(command, ["--version"], {
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
timeout: 10_000,
|
|
70
|
+
})
|
|
71
|
+
return "ok"
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") {
|
|
74
|
+
return "missing"
|
|
75
|
+
}
|
|
76
|
+
return "error"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveAkmCommand(): string | CliError {
|
|
81
|
+
const currentStatus = getCommandStatus(resolvedAkmCommand)
|
|
82
|
+
if (currentStatus === "ok" || currentStatus === "error") return resolvedAkmCommand
|
|
83
|
+
|
|
84
|
+
if (attemptedAutoInstall) {
|
|
85
|
+
return { ok: false, error: "The 'akm' CLI was not found on PATH and automatic installation was unsuccessful." }
|
|
86
|
+
}
|
|
87
|
+
attemptedAutoInstall = true
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
execFileSync("bun", ["--version"], {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
timeout: 10_000,
|
|
93
|
+
})
|
|
94
|
+
} catch {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: "The 'akm' CLI was not found on PATH, and Bun is not available for automatic installation. Install akm from https://github.com/itlackey/agentikit.",
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
execFileSync("bun", ["install", "-g", "akm-cli"], {
|
|
103
|
+
encoding: "utf8",
|
|
104
|
+
timeout: 120_000,
|
|
105
|
+
stdio: "pipe",
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const globalBin = execFileSync("bun", ["pm", "bin", "-g"], {
|
|
109
|
+
encoding: "utf8",
|
|
110
|
+
timeout: 10_000,
|
|
111
|
+
}).trim()
|
|
112
|
+
|
|
113
|
+
const candidate = path.join(globalBin, process.platform === "win32" ? "akm.exe" : "akm")
|
|
114
|
+
if (getCommandStatus(candidate) === "ok") {
|
|
115
|
+
resolvedAkmCommand = candidate
|
|
116
|
+
return resolvedAkmCommand
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (getCommandStatus("akm") === "ok") {
|
|
120
|
+
resolvedAkmCommand = "akm"
|
|
121
|
+
return resolvedAkmCommand
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
error: "Installed 'akm-cli' via Bun, but the 'akm' executable could not be resolved. Check your Bun global bin directory and PATH.",
|
|
127
|
+
}
|
|
128
|
+
} catch (error: unknown) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: `Failed to auto-install 'akm-cli' via Bun: ${formatCliError(error)}`,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function runCli(client: LogCapableClient, args: string[], meta: CliLogMeta): Promise<string> {
|
|
137
|
+
const command = resolveAkmCommand()
|
|
138
|
+
if (typeof command !== "string") {
|
|
139
|
+
await writePluginLog(client, "error", "AKM command resolution failed", {
|
|
140
|
+
subsystem: "akm",
|
|
141
|
+
toolName: meta.toolName,
|
|
142
|
+
sessionID: meta.sessionID,
|
|
143
|
+
directory: meta.directory,
|
|
144
|
+
command: resolvedAkmCommand,
|
|
145
|
+
args,
|
|
146
|
+
exitCode: null,
|
|
147
|
+
stdout: "",
|
|
148
|
+
stderr: command.error,
|
|
149
|
+
})
|
|
150
|
+
return JSON.stringify(command)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fullArgs = [...args, "--format", "json"]
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const stdout = execFileSync(command, fullArgs, {
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
timeout: 60_000,
|
|
159
|
+
})
|
|
160
|
+
await writePluginLog(client, "info", "AKM command completed", {
|
|
161
|
+
subsystem: "akm",
|
|
162
|
+
toolName: meta.toolName,
|
|
163
|
+
sessionID: meta.sessionID,
|
|
164
|
+
directory: meta.directory,
|
|
165
|
+
command,
|
|
166
|
+
args: fullArgs,
|
|
167
|
+
exitCode: 0,
|
|
168
|
+
stdout,
|
|
169
|
+
stderr: "",
|
|
170
|
+
})
|
|
171
|
+
return stdout
|
|
172
|
+
} catch (error: unknown) {
|
|
173
|
+
const message = formatCliError(error)
|
|
174
|
+
await writePluginLog(client, "error", "AKM command failed", {
|
|
175
|
+
subsystem: "akm",
|
|
176
|
+
toolName: meta.toolName,
|
|
177
|
+
sessionID: meta.sessionID,
|
|
178
|
+
directory: meta.directory,
|
|
179
|
+
command,
|
|
180
|
+
args: fullArgs,
|
|
181
|
+
exitCode: getExecStatus(error),
|
|
182
|
+
stdout: toLogString((error as { stdout?: unknown }).stdout) ?? "",
|
|
183
|
+
stderr: toLogString((error as { stderr?: unknown }).stderr) ?? message,
|
|
184
|
+
})
|
|
185
|
+
return JSON.stringify({ ok: false, error: message })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type CliError = { ok: false; error: string }
|
|
190
|
+
type AssetType = "skill" | "command" | "agent" | "knowledge" | "script"
|
|
191
|
+
|
|
192
|
+
type ShowAgentResponse = {
|
|
193
|
+
type: "agent"
|
|
194
|
+
name: string
|
|
195
|
+
path: string
|
|
196
|
+
description?: string
|
|
197
|
+
prompt?: string
|
|
198
|
+
toolPolicy?: unknown
|
|
199
|
+
modelHint?: unknown
|
|
200
|
+
editable?: boolean
|
|
201
|
+
origin?: string | null
|
|
202
|
+
action?: string
|
|
203
|
+
editHint?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type ShowCommandResponse = {
|
|
207
|
+
type: "command"
|
|
208
|
+
name: string
|
|
209
|
+
path: string
|
|
210
|
+
description?: string
|
|
211
|
+
template?: string
|
|
212
|
+
editable?: boolean
|
|
213
|
+
agent?: string
|
|
214
|
+
origin?: string | null
|
|
215
|
+
action?: string
|
|
216
|
+
parameters?: string[]
|
|
217
|
+
editHint?: string
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
type ShowToolResponse = {
|
|
221
|
+
type: "tool" | "script"
|
|
222
|
+
name: string
|
|
223
|
+
path?: string
|
|
224
|
+
description?: string
|
|
225
|
+
run?: string
|
|
226
|
+
setup?: string
|
|
227
|
+
cwd?: string
|
|
228
|
+
editable?: boolean
|
|
229
|
+
origin?: string | null
|
|
230
|
+
action?: string
|
|
231
|
+
editHint?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
type SearchHit = {
|
|
235
|
+
type: AssetType | "registry" | "registry-asset"
|
|
236
|
+
ref?: string
|
|
237
|
+
id?: string
|
|
238
|
+
installRef?: string
|
|
239
|
+
editable?: boolean
|
|
240
|
+
name?: string
|
|
241
|
+
description?: string
|
|
242
|
+
score?: number
|
|
243
|
+
whyMatched?: string[]
|
|
244
|
+
run?: string
|
|
245
|
+
origin?: string | null
|
|
246
|
+
size?: string
|
|
247
|
+
action?: string
|
|
248
|
+
editHint?: string
|
|
249
|
+
curated?: boolean
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type SearchResponse = {
|
|
253
|
+
hits?: SearchHit[]
|
|
254
|
+
source?: "local" | "registry" | "both"
|
|
255
|
+
stashDir?: string
|
|
256
|
+
timing?: { totalMs?: number; rankMs?: number; embedMs?: number }
|
|
257
|
+
warnings?: string[]
|
|
258
|
+
tip?: string
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isShowToolResponse(value: unknown): value is ShowToolResponse {
|
|
262
|
+
return !!value
|
|
263
|
+
&& typeof value === "object"
|
|
264
|
+
&& ((value as { type?: unknown }).type === "tool" || (value as { type?: unknown }).type === "script")
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isShowAgentResponse(value: unknown): value is ShowAgentResponse {
|
|
268
|
+
return !!value
|
|
269
|
+
&& typeof value === "object"
|
|
270
|
+
&& (value as { type?: unknown }).type === "agent"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isShowCommandResponse(value: unknown): value is ShowCommandResponse {
|
|
274
|
+
return !!value
|
|
275
|
+
&& typeof value === "object"
|
|
276
|
+
&& (value as { type?: unknown }).type === "command"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseCliJson<T>(raw: string): T | CliError {
|
|
280
|
+
try {
|
|
281
|
+
return JSON.parse(raw) as T
|
|
282
|
+
} catch {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
error: "akm CLI returned non-JSON output",
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isCliError(value: unknown): value is CliError {
|
|
291
|
+
return !!value
|
|
292
|
+
&& typeof value === "object"
|
|
293
|
+
&& "ok" in value
|
|
294
|
+
&& (value as { ok?: unknown }).ok === false
|
|
295
|
+
&& "error" in value
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function parseModelHint(modelHint: unknown): { providerID: string; modelID: string } | undefined {
|
|
299
|
+
if (typeof modelHint !== "string") return undefined
|
|
300
|
+
const [providerID, ...modelParts] = modelHint.split("/")
|
|
301
|
+
const modelID = modelParts.join("/")
|
|
302
|
+
if (!providerID || !modelID) return undefined
|
|
303
|
+
return { providerID, modelID }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parseToolPolicy(toolPolicy: unknown): Record<string, boolean> | undefined {
|
|
307
|
+
const result: Record<string, boolean> = {}
|
|
308
|
+
|
|
309
|
+
const assign = (key: string, value: unknown) => {
|
|
310
|
+
const normalizedKey = key.trim().toLowerCase()
|
|
311
|
+
if (!normalizedKey) return
|
|
312
|
+
if (typeof value === "boolean") {
|
|
313
|
+
result[normalizedKey] = value
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (typeof value === "string") {
|
|
317
|
+
if (value === "allow") result[normalizedKey] = true
|
|
318
|
+
if (value === "deny") result[normalizedKey] = false
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (typeof toolPolicy === "string") {
|
|
323
|
+
assign(toolPolicy, true)
|
|
324
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (Array.isArray(toolPolicy)) {
|
|
328
|
+
for (const item of toolPolicy) {
|
|
329
|
+
if (typeof item === "string") assign(item, true)
|
|
330
|
+
}
|
|
331
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!toolPolicy || typeof toolPolicy !== "object") return undefined
|
|
335
|
+
|
|
336
|
+
for (const [key, value] of Object.entries(toolPolicy as Record<string, unknown>)) {
|
|
337
|
+
assign(key, value)
|
|
338
|
+
}
|
|
339
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function extractText(parts: unknown): string {
|
|
343
|
+
if (!Array.isArray(parts)) return ""
|
|
344
|
+
const segments: string[] = []
|
|
345
|
+
for (const part of parts as Array<Record<string, unknown>>) {
|
|
346
|
+
if (part?.type === "text" && typeof part.text === "string") {
|
|
347
|
+
const text = part.text.trim()
|
|
348
|
+
if (text) segments.push(text)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return segments.join("\n\n")
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function resolveRefInput(
|
|
355
|
+
client: LogCapableClient,
|
|
356
|
+
input: { ref?: string; query?: string },
|
|
357
|
+
type: AssetType,
|
|
358
|
+
meta: CliLogMeta,
|
|
359
|
+
): Promise<{ ok: true; ref: string } | CliError> {
|
|
360
|
+
if (input.ref && input.ref.trim()) {
|
|
361
|
+
return { ok: true, ref: input.ref.trim() }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const query = input.query?.trim()
|
|
365
|
+
if (!query) {
|
|
366
|
+
return { ok: false, error: "Provide either 'ref' or 'query'." }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const raw = await runCli(client, ["search", query, "--type", type, "--limit", "1", "--detail", "normal", "--source", "local"], meta)
|
|
370
|
+
const parsed = parseCliJson<SearchResponse>(raw)
|
|
371
|
+
if (isCliError(parsed)) return parsed
|
|
372
|
+
|
|
373
|
+
const ref = parsed.hits?.[0]?.ref
|
|
374
|
+
if (!ref) {
|
|
375
|
+
return { ok: false, error: `No ${type} match found for query '${query}'.` }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { ok: true, ref }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function ensureTargetSessionID(input: {
|
|
382
|
+
useSubtask: boolean
|
|
383
|
+
context: { sessionID: string; directory: string }
|
|
384
|
+
title: string
|
|
385
|
+
client: PluginClient
|
|
386
|
+
}): Promise<{ ok: true; sessionID: string } | CliError> {
|
|
387
|
+
if (!input.useSubtask) return { ok: true, sessionID: input.context.sessionID }
|
|
388
|
+
|
|
389
|
+
const created = await input.client.session.create({
|
|
390
|
+
query: { directory: input.context.directory },
|
|
391
|
+
body: { parentID: input.context.sessionID, title: input.title },
|
|
392
|
+
})
|
|
393
|
+
if (created.error || !created.data?.id) {
|
|
394
|
+
const reason = created.error ? JSON.stringify(created.error) : "missing child session id"
|
|
395
|
+
return { ok: false, error: `Failed to create child session: ${reason}` }
|
|
396
|
+
}
|
|
397
|
+
return { ok: true, sessionID: created.data.id }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function splitArguments(raw: string): string[] {
|
|
401
|
+
if (!raw.trim()) return []
|
|
402
|
+
const args: string[] = []
|
|
403
|
+
const re = /"([^"]*)"|'([^']*)'|`([^`]*)`|(\S+)/g
|
|
404
|
+
let match: RegExpExecArray | null
|
|
405
|
+
while ((match = re.exec(raw)) !== null) {
|
|
406
|
+
args.push(match[1] ?? match[2] ?? match[3] ?? match[4] ?? "")
|
|
407
|
+
}
|
|
408
|
+
return args
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function renderCommandTemplate(template: string, rawArguments: string): string {
|
|
412
|
+
const args = splitArguments(rawArguments)
|
|
413
|
+
return template
|
|
414
|
+
.replace(/\$ARGUMENTS/g, rawArguments)
|
|
415
|
+
.replace(/\$(\d+)/g, (_m, index: string) => args[Number(index) - 1] ?? "")
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function createSearchArgs(input: {
|
|
419
|
+
query: string
|
|
420
|
+
type?: AssetType | "any"
|
|
421
|
+
limit?: number
|
|
422
|
+
source?: "local" | "registry" | "both"
|
|
423
|
+
defaultSource?: "local" | "registry" | "both"
|
|
424
|
+
}): string[] {
|
|
425
|
+
const args = ["search", input.query]
|
|
426
|
+
if (input.type) args.push("--type", input.type)
|
|
427
|
+
if (input.limit) args.push("--limit", String(input.limit))
|
|
428
|
+
if (input.source) {
|
|
429
|
+
args.push("--source", input.source)
|
|
430
|
+
} else if (input.defaultSource) {
|
|
431
|
+
args.push("--source", input.defaultSource)
|
|
432
|
+
}
|
|
433
|
+
args.push("--detail", "normal")
|
|
434
|
+
return args
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
type PluginClient = {
|
|
438
|
+
session: {
|
|
439
|
+
create: (input: {
|
|
440
|
+
query: { directory: string }
|
|
441
|
+
body: { parentID: string; title: string }
|
|
442
|
+
}) => Promise<{ data?: { id?: string }; error?: unknown }>
|
|
443
|
+
prompt: (input: {
|
|
444
|
+
query: { directory: string }
|
|
445
|
+
path: { id: string }
|
|
446
|
+
body: {
|
|
447
|
+
agent: string
|
|
448
|
+
parts: Array<{ type: "text"; text: string }>
|
|
449
|
+
system?: string
|
|
450
|
+
model?: { providerID: string; modelID: string }
|
|
451
|
+
tools?: Record<string, boolean>
|
|
452
|
+
}
|
|
453
|
+
}) => Promise<{ data?: { parts?: unknown }; error?: unknown }>
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export const AgentikitPlugin: Plugin = async ({ client }) => ({
|
|
458
|
+
tool: {
|
|
459
|
+
akm_search: tool({
|
|
460
|
+
description: "Search your local stash or the akm registry for scripts, skills, commands, agents, and knowledge. Use source='registry' or akm_registry_search for installable community kits.",
|
|
461
|
+
args: {
|
|
462
|
+
query: tool.schema.string().describe("Case-insensitive substring search."),
|
|
463
|
+
type: tool.schema
|
|
464
|
+
.enum(["skill", "command", "agent", "knowledge", "script", "any"])
|
|
465
|
+
.optional()
|
|
466
|
+
.describe("Optional type filter. Defaults to 'any'."),
|
|
467
|
+
limit: tool.schema.number().optional().describe("Maximum number of hits to return. Defaults to 20."),
|
|
468
|
+
source: tool.schema
|
|
469
|
+
.enum(["local", "registry", "both"])
|
|
470
|
+
.optional()
|
|
471
|
+
.describe("Search source. 'local' searches stash dirs, 'registry' searches npm/GitHub, 'both' searches all. Defaults to 'local'."),
|
|
472
|
+
},
|
|
473
|
+
async execute({ query, type, limit, source }) {
|
|
474
|
+
return runCli(client as unknown as LogCapableClient, createSearchArgs({ query, type, limit, source }), { toolName: "akm_search" })
|
|
475
|
+
},
|
|
476
|
+
}),
|
|
477
|
+
akm_registry_search: tool({
|
|
478
|
+
description: "Search configured akm registries only. Use this when you want installable kits without mixing in local stash results.",
|
|
479
|
+
args: {
|
|
480
|
+
query: tool.schema.string().describe("Search query for installable registry kits."),
|
|
481
|
+
type: tool.schema
|
|
482
|
+
.enum(["skill", "command", "agent", "knowledge", "script", "any"])
|
|
483
|
+
.optional()
|
|
484
|
+
.describe("Optional asset type filter. Defaults to 'any'."),
|
|
485
|
+
limit: tool.schema.number().optional().describe("Maximum number of registry hits to return. Defaults to 20."),
|
|
486
|
+
assets: tool.schema.boolean().optional().describe("Include asset-level results from registry index v2 payloads."),
|
|
487
|
+
},
|
|
488
|
+
async execute({ query, type, limit, assets }) {
|
|
489
|
+
const args = ["registry", "search", query]
|
|
490
|
+
if (limit) args.push("--limit", String(limit))
|
|
491
|
+
const assetTypeFilter = type && type !== "any" ? type : undefined
|
|
492
|
+
if (assets || assetTypeFilter) args.push("--assets")
|
|
493
|
+
|
|
494
|
+
const raw = await runCli(client as unknown as LogCapableClient, args, { toolName: "akm_registry_search" })
|
|
495
|
+
if (!assetTypeFilter) return raw
|
|
496
|
+
|
|
497
|
+
const parsed = parseCliJson<{
|
|
498
|
+
hits?: SearchHit[]
|
|
499
|
+
assetHits?: Array<SearchHit & { assetType?: AssetType }>
|
|
500
|
+
warnings?: string[]
|
|
501
|
+
query?: string
|
|
502
|
+
}>(raw)
|
|
503
|
+
if (isCliError(parsed)) return JSON.stringify(parsed)
|
|
504
|
+
|
|
505
|
+
return JSON.stringify({
|
|
506
|
+
...parsed,
|
|
507
|
+
hits: [],
|
|
508
|
+
assetHits: (parsed.assetHits ?? []).filter((hit) => hit.assetType === assetTypeFilter),
|
|
509
|
+
})
|
|
510
|
+
},
|
|
511
|
+
}),
|
|
512
|
+
akm_show: tool({
|
|
513
|
+
description: "Show a stash asset by ref. For knowledge assets, use view_mode to retrieve specific content (toc, section, lines, frontmatter).",
|
|
514
|
+
args: {
|
|
515
|
+
ref: tool.schema.string().describe("Asset reference returned by akm_search."),
|
|
516
|
+
view_mode: tool.schema
|
|
517
|
+
.enum(["full", "toc", "frontmatter", "section", "lines"])
|
|
518
|
+
.optional()
|
|
519
|
+
.describe("View mode for knowledge assets. Defaults to 'full'. Ignored for other types."),
|
|
520
|
+
heading: tool.schema.string().optional()
|
|
521
|
+
.describe("Section heading to extract (required when view_mode is 'section')."),
|
|
522
|
+
start_line: tool.schema.number().optional()
|
|
523
|
+
.describe("Start line number, 1-based (for view_mode 'lines')."),
|
|
524
|
+
end_line: tool.schema.number().optional()
|
|
525
|
+
.describe("End line number, 1-based inclusive (for view_mode 'lines')."),
|
|
526
|
+
},
|
|
527
|
+
async execute({ ref, view_mode, heading, start_line, end_line }) {
|
|
528
|
+
const args = ["show", ref]
|
|
529
|
+
if (view_mode) {
|
|
530
|
+
args.push(view_mode)
|
|
531
|
+
if (view_mode === "section" && heading) args.push(heading)
|
|
532
|
+
if (view_mode === "lines") {
|
|
533
|
+
if (start_line != null) args.push(String(start_line))
|
|
534
|
+
if (end_line != null) args.push(String(end_line))
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_show" })
|
|
538
|
+
},
|
|
539
|
+
}),
|
|
540
|
+
akm_index: tool({
|
|
541
|
+
description: "Build or rebuild the akm stash index. Scans stash directories, generates missing .stash.json metadata, and builds a semantic search index.",
|
|
542
|
+
args: {},
|
|
543
|
+
async execute() {
|
|
544
|
+
return runCli(client as unknown as LogCapableClient, ["index"], { toolName: "akm_index" })
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
akm_add: tool({
|
|
548
|
+
description: "Install a kit from npm, GitHub, another git host, or a local directory. Installed kits become searchable alongside local assets.",
|
|
549
|
+
args: {
|
|
550
|
+
package_ref: tool.schema.string().describe("Package reference such as npm:@scope/kit, github:<owner>/<repo>, git+https://host/repo, or ./local/kit."),
|
|
551
|
+
},
|
|
552
|
+
async execute({ package_ref }) {
|
|
553
|
+
return runCli(client as unknown as LogCapableClient, ["add", package_ref], { toolName: "akm_add" })
|
|
554
|
+
},
|
|
555
|
+
}),
|
|
556
|
+
akm_list: tool({
|
|
557
|
+
description: "List all kits installed from the registry.",
|
|
558
|
+
args: {},
|
|
559
|
+
async execute() {
|
|
560
|
+
return runCli(client as unknown as LogCapableClient, ["list"], { toolName: "akm_list" })
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
563
|
+
akm_remove: tool({
|
|
564
|
+
description: "Remove an installed registry kit by id or ref and reindex the stash.",
|
|
565
|
+
args: {
|
|
566
|
+
package_ref: tool.schema.string().describe("Installed kit id or ref, such as npm:@scope/kit or owner/repo."),
|
|
567
|
+
},
|
|
568
|
+
async execute({ package_ref }) {
|
|
569
|
+
return runCli(client as unknown as LogCapableClient, ["remove", package_ref], { toolName: "akm_remove" })
|
|
570
|
+
},
|
|
571
|
+
}),
|
|
572
|
+
akm_update: tool({
|
|
573
|
+
description: "Update one installed kit or all installed kits to the latest available version.",
|
|
574
|
+
args: {
|
|
575
|
+
package_ref: tool.schema.string().optional().describe("Installed kit id or ref to update."),
|
|
576
|
+
all: tool.schema.boolean().optional().describe("Update all installed kits."),
|
|
577
|
+
force: tool.schema.boolean().optional().describe("Force a fresh download even if the version is unchanged."),
|
|
578
|
+
},
|
|
579
|
+
async execute({ package_ref, all, force }) {
|
|
580
|
+
const args = ["update"]
|
|
581
|
+
const packageRef = package_ref?.trim()
|
|
582
|
+
if (all) {
|
|
583
|
+
args.push("--all")
|
|
584
|
+
} else if (packageRef) {
|
|
585
|
+
args.push(packageRef)
|
|
586
|
+
} else {
|
|
587
|
+
return JSON.stringify({ ok: false, error: "Provide 'package_ref' or set 'all' to true." })
|
|
588
|
+
}
|
|
589
|
+
if (force) args.push("--force")
|
|
590
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_update" })
|
|
591
|
+
},
|
|
592
|
+
}),
|
|
593
|
+
akm_clone: tool({
|
|
594
|
+
description: "Clone an asset from any source into the working stash or a custom destination for editing.",
|
|
595
|
+
args: {
|
|
596
|
+
ref: tool.schema.string().describe("Asset ref to clone, including optional origin such as npm:@scope/pkg//script:deploy.sh."),
|
|
597
|
+
name: tool.schema.string().optional().describe("Optional new asset name."),
|
|
598
|
+
dest: tool.schema.string().optional().describe("Optional destination directory. The type subdirectory is appended automatically by akm."),
|
|
599
|
+
force: tool.schema.boolean().optional().describe("Overwrite the destination if it already exists."),
|
|
600
|
+
},
|
|
601
|
+
async execute({ ref, name, dest, force }) {
|
|
602
|
+
const args = ["clone", ref]
|
|
603
|
+
if (name) args.push("--name", name)
|
|
604
|
+
if (dest) args.push("--dest", dest)
|
|
605
|
+
if (force) args.push("--force")
|
|
606
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_clone" })
|
|
607
|
+
},
|
|
608
|
+
}),
|
|
609
|
+
akm_agent: tool({
|
|
610
|
+
description: "Dispatch a stash agent by ref into a child OpenCode session, applying the agent prompt and metadata from akm_show.",
|
|
611
|
+
args: {
|
|
612
|
+
ref: tool.schema.string().optional().describe("Agent ref from akm_search (e.g. agent:my-agent.md)."),
|
|
613
|
+
query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash agent for this query."),
|
|
614
|
+
task_prompt: tool.schema.string().describe("Task prompt sent to the dispatched OpenCode agent."),
|
|
615
|
+
dispatch_agent: tool.schema.string().optional().describe("OpenCode agent to run the task with. Defaults to 'general'."),
|
|
616
|
+
as_subtask: tool.schema.boolean().optional().describe("Run in child session with parent context. Defaults to true."),
|
|
617
|
+
},
|
|
618
|
+
async execute({ ref, query, task_prompt, dispatch_agent, as_subtask }, context) {
|
|
619
|
+
const logMeta = {
|
|
620
|
+
toolName: "akm_agent",
|
|
621
|
+
directory: context.directory,
|
|
622
|
+
sessionID: context.sessionID,
|
|
623
|
+
}
|
|
624
|
+
const resolved = await resolveRefInput(client as unknown as LogCapableClient, { ref, query }, "agent", logMeta)
|
|
625
|
+
if (!resolved.ok) return JSON.stringify(resolved)
|
|
626
|
+
|
|
627
|
+
const shownRaw = await runCli(client as unknown as LogCapableClient, ["show", resolved.ref], logMeta)
|
|
628
|
+
const shown = parseCliJson<ShowAgentResponse | { type: string }>(shownRaw)
|
|
629
|
+
if (isCliError(shown)) {
|
|
630
|
+
return JSON.stringify(shown)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!isShowAgentResponse(shown)) {
|
|
634
|
+
return JSON.stringify({
|
|
635
|
+
ok: false,
|
|
636
|
+
error: `Ref ${ref} is not an agent payload from akm_show.`,
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (!shown.prompt || !shown.prompt.trim()) {
|
|
641
|
+
return JSON.stringify({
|
|
642
|
+
ok: false,
|
|
643
|
+
error: `Agent ${shown.name} is missing prompt content.`,
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const useSubtask = as_subtask ?? true
|
|
648
|
+
const targetAgent = dispatch_agent ?? "general"
|
|
649
|
+
const model = parseModelHint(shown.modelHint)
|
|
650
|
+
const tools = parseToolPolicy(shown.toolPolicy)
|
|
651
|
+
|
|
652
|
+
const targetSession = await ensureTargetSessionID({
|
|
653
|
+
useSubtask,
|
|
654
|
+
context: { sessionID: context.sessionID, directory: context.directory },
|
|
655
|
+
title: `akm:${shown.name}`,
|
|
656
|
+
client: client as unknown as PluginClient,
|
|
657
|
+
})
|
|
658
|
+
if (!targetSession.ok) return JSON.stringify(targetSession)
|
|
659
|
+
|
|
660
|
+
const promptBody: {
|
|
661
|
+
agent: string
|
|
662
|
+
system: string
|
|
663
|
+
parts: Array<{ type: "text"; text: string }>
|
|
664
|
+
model?: { providerID: string; modelID: string }
|
|
665
|
+
tools?: Record<string, boolean>
|
|
666
|
+
} = {
|
|
667
|
+
agent: targetAgent,
|
|
668
|
+
system: shown.prompt,
|
|
669
|
+
parts: [{ type: "text", text: task_prompt }],
|
|
670
|
+
}
|
|
671
|
+
if (model) promptBody.model = model
|
|
672
|
+
if (tools) promptBody.tools = tools
|
|
673
|
+
|
|
674
|
+
const promptResponse = await client.session.prompt({
|
|
675
|
+
query: { directory: context.directory },
|
|
676
|
+
path: { id: targetSession.sessionID },
|
|
677
|
+
body: promptBody,
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
if (promptResponse.error || !promptResponse.data) {
|
|
681
|
+
const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
|
|
682
|
+
return JSON.stringify({
|
|
683
|
+
ok: false,
|
|
684
|
+
error: `Failed to dispatch prompt for ${resolved.ref}: ${reason}`,
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return JSON.stringify({
|
|
689
|
+
ok: true,
|
|
690
|
+
ref: resolved.ref,
|
|
691
|
+
stashAgent: shown.name,
|
|
692
|
+
dispatchAgent: targetAgent,
|
|
693
|
+
usedSubtask: useSubtask,
|
|
694
|
+
sessionID: targetSession.sessionID,
|
|
695
|
+
model,
|
|
696
|
+
tools,
|
|
697
|
+
text: extractText(promptResponse.data.parts),
|
|
698
|
+
})
|
|
699
|
+
},
|
|
700
|
+
}),
|
|
701
|
+
akm_cmd: tool({
|
|
702
|
+
description: "Execute a stash command template through the OpenCode SDK in the current or child session.",
|
|
703
|
+
args: {
|
|
704
|
+
ref: tool.schema.string().optional().describe("Command ref from akm_search (e.g. command:review.md)."),
|
|
705
|
+
query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash command for this query."),
|
|
706
|
+
arguments: tool.schema.string().optional().describe("Command arguments used for $ARGUMENTS and positional placeholders ($1, $2, ...)."),
|
|
707
|
+
dispatch_agent: tool.schema.string().optional().describe("OpenCode agent to run the rendered command. Defaults to current agent."),
|
|
708
|
+
as_subtask: tool.schema.boolean().optional().describe("Run in child session with parent context. Defaults to false."),
|
|
709
|
+
},
|
|
710
|
+
async execute({ ref, query, arguments: commandArguments, dispatch_agent, as_subtask }, context) {
|
|
711
|
+
const logMeta = {
|
|
712
|
+
toolName: "akm_cmd",
|
|
713
|
+
directory: context.directory,
|
|
714
|
+
sessionID: context.sessionID,
|
|
715
|
+
}
|
|
716
|
+
const resolved = await resolveRefInput(client as unknown as LogCapableClient, { ref, query }, "command", logMeta)
|
|
717
|
+
if (!resolved.ok) return JSON.stringify(resolved)
|
|
718
|
+
|
|
719
|
+
const shownRaw = await runCli(client as unknown as LogCapableClient, ["show", resolved.ref], logMeta)
|
|
720
|
+
const shown = parseCliJson<ShowCommandResponse | { type: string }>(shownRaw)
|
|
721
|
+
if (isCliError(shown)) return JSON.stringify(shown)
|
|
722
|
+
if (!isShowCommandResponse(shown)) {
|
|
723
|
+
return JSON.stringify({ ok: false, error: `Ref ${resolved.ref} is not a command payload from akm_show.` })
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const template = shown.template?.trim()
|
|
727
|
+
if (!template) {
|
|
728
|
+
return JSON.stringify({ ok: false, error: `Command ${shown.name} is missing template content.` })
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const argsText = commandArguments ?? ""
|
|
732
|
+
const rendered = renderCommandTemplate(template, argsText)
|
|
733
|
+
const useSubtask = as_subtask ?? false
|
|
734
|
+
const targetAgent = dispatch_agent ?? context.agent
|
|
735
|
+
|
|
736
|
+
const targetSession = await ensureTargetSessionID({
|
|
737
|
+
useSubtask,
|
|
738
|
+
context: { sessionID: context.sessionID, directory: context.directory },
|
|
739
|
+
title: `akm:cmd:${shown.name}`,
|
|
740
|
+
client: client as unknown as PluginClient,
|
|
741
|
+
})
|
|
742
|
+
if (!targetSession.ok) return JSON.stringify(targetSession)
|
|
743
|
+
|
|
744
|
+
const promptResponse = await client.session.prompt({
|
|
745
|
+
query: { directory: context.directory },
|
|
746
|
+
path: { id: targetSession.sessionID },
|
|
747
|
+
body: {
|
|
748
|
+
agent: targetAgent,
|
|
749
|
+
parts: [{ type: "text", text: rendered }],
|
|
750
|
+
},
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
if (promptResponse.error || !promptResponse.data) {
|
|
754
|
+
const reason = promptResponse.error ? JSON.stringify(promptResponse.error) : "empty response"
|
|
755
|
+
return JSON.stringify({
|
|
756
|
+
ok: false,
|
|
757
|
+
error: `Failed to execute command ${resolved.ref}: ${reason}`,
|
|
758
|
+
})
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return JSON.stringify({
|
|
762
|
+
ok: true,
|
|
763
|
+
ref: resolved.ref,
|
|
764
|
+
stashCommand: shown.name,
|
|
765
|
+
dispatchAgent: targetAgent,
|
|
766
|
+
usedSubtask: useSubtask,
|
|
767
|
+
sessionID: targetSession.sessionID,
|
|
768
|
+
arguments: argsText,
|
|
769
|
+
renderedTemplate: rendered,
|
|
770
|
+
text: extractText(promptResponse.data.parts),
|
|
771
|
+
})
|
|
772
|
+
},
|
|
773
|
+
}),
|
|
774
|
+
akm_config: tool({
|
|
775
|
+
description: "View or update akm configuration settings.",
|
|
776
|
+
args: {
|
|
777
|
+
action: tool.schema.enum(["get", "set", "list", "unset", "path"]).describe("Config action: get, set, list, unset, or path."),
|
|
778
|
+
key: tool.schema.string().optional().describe("Config key (required for get/set)."),
|
|
779
|
+
value: tool.schema.string().optional().describe("Config value (required for set)."),
|
|
780
|
+
all: tool.schema.boolean().optional().describe("When action is 'path', include config, stash, cache, and index paths."),
|
|
781
|
+
},
|
|
782
|
+
async execute({ action, key, value, all }) {
|
|
783
|
+
const args = ["config", action]
|
|
784
|
+
if (key) args.push(key)
|
|
785
|
+
if (value) args.push(value)
|
|
786
|
+
if (action === "path" && all) args.push("--all")
|
|
787
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_config" })
|
|
788
|
+
},
|
|
789
|
+
}),
|
|
790
|
+
akm_run: tool({
|
|
791
|
+
description: "Execute a stash script by ref. Resolves via search, fetches metadata via show, and runs the run command.",
|
|
792
|
+
args: {
|
|
793
|
+
ref: tool.schema.string().optional().describe("Script ref from akm_search (e.g. script:deploy.sh)."),
|
|
794
|
+
query: tool.schema.string().optional().describe("If ref is omitted, resolve best matching stash script for this query."),
|
|
795
|
+
args: tool.schema.string().optional().describe("Arguments to append to the run command."),
|
|
796
|
+
},
|
|
797
|
+
async execute({ ref, query, args: runArgs }) {
|
|
798
|
+
const resolved = await resolveRefInput(client as unknown as LogCapableClient, { ref, query }, "script", { toolName: "akm_run" })
|
|
799
|
+
if (!resolved.ok) return JSON.stringify(resolved)
|
|
800
|
+
|
|
801
|
+
const shownRaw = await runCli(client as unknown as LogCapableClient, ["show", resolved.ref], { toolName: "akm_run" })
|
|
802
|
+
const shown = parseCliJson<ShowToolResponse | { type: string }>(shownRaw)
|
|
803
|
+
if (isCliError(shown)) return JSON.stringify(shown)
|
|
804
|
+
|
|
805
|
+
if (!isShowToolResponse(shown)) {
|
|
806
|
+
return JSON.stringify({
|
|
807
|
+
ok: false,
|
|
808
|
+
error: `Ref ${resolved.ref} is not a script payload from akm_show.`,
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (!shown.run || !shown.run.trim()) {
|
|
813
|
+
return JSON.stringify({
|
|
814
|
+
ok: false,
|
|
815
|
+
error: `Script ${shown.name} is missing run command.`,
|
|
816
|
+
})
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let cmd = shown.run
|
|
820
|
+
if (runArgs && runArgs.trim()) {
|
|
821
|
+
cmd = `${cmd} ${runArgs.trim()}`
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
const output = execSync(cmd, {
|
|
826
|
+
encoding: "utf8",
|
|
827
|
+
timeout: 120_000,
|
|
828
|
+
})
|
|
829
|
+
return JSON.stringify({
|
|
830
|
+
ok: true,
|
|
831
|
+
ref: resolved.ref,
|
|
832
|
+
script: shown.name,
|
|
833
|
+
run: cmd,
|
|
834
|
+
output,
|
|
835
|
+
})
|
|
836
|
+
} catch (error: unknown) {
|
|
837
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
838
|
+
return JSON.stringify({
|
|
839
|
+
ok: false,
|
|
840
|
+
error: `Failed to execute run command for ${shown.name}: ${message}`,
|
|
841
|
+
})
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
}),
|
|
845
|
+
akm_sources: tool({
|
|
846
|
+
description: "List all resolved stash search paths and their status.",
|
|
847
|
+
args: {},
|
|
848
|
+
async execute() {
|
|
849
|
+
return runCli(client as unknown as LogCapableClient, ["sources"], { toolName: "akm_sources" })
|
|
850
|
+
},
|
|
851
|
+
}),
|
|
852
|
+
akm_upgrade: tool({
|
|
853
|
+
description: "Check for or install akm CLI updates.",
|
|
854
|
+
args: {
|
|
855
|
+
check: tool.schema.boolean().optional().describe("Only check for updates without installing."),
|
|
856
|
+
force: tool.schema.boolean().optional().describe("Force upgrade even if already on latest version."),
|
|
857
|
+
},
|
|
858
|
+
async execute({ check, force }) {
|
|
859
|
+
const args = ["upgrade"]
|
|
860
|
+
if (check) args.push("--check")
|
|
861
|
+
if (force) args.push("--force")
|
|
862
|
+
return runCli(client as unknown as LogCapableClient, args, { toolName: "akm_upgrade" })
|
|
863
|
+
},
|
|
864
|
+
}),
|
|
865
|
+
},
|
|
866
|
+
})
|
package/package.json
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-opencode",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
"keywords": [
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
"version": "0.0.19",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenCode plugin for Agentikit - search and show extension assets via the akm CLI.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"opencode",
|
|
8
|
+
"opencode-ai",
|
|
9
|
+
"opencode-plugin",
|
|
10
|
+
"opencode-extensions",
|
|
11
|
+
"agentikit",
|
|
12
|
+
"ai-agent",
|
|
13
|
+
"developer-tools",
|
|
14
|
+
"plugin"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/itlackey/akm-plugins#readme",
|
|
9
17
|
"repository": {
|
|
10
18
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/itlackey/
|
|
19
|
+
"url": "git+https://github.com/itlackey/akm-plugins.git",
|
|
20
|
+
"directory": "opencode"
|
|
12
21
|
},
|
|
13
|
-
"homepage": "https://github.com/itlackey/agentikit#readme",
|
|
14
22
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/itlackey/
|
|
23
|
+
"url": "https://github.com/itlackey/akm-plugins/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MPL-2.0",
|
|
26
|
+
"files": [
|
|
27
|
+
"index.ts",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"main": "./index.ts",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@opencode-ai/plugin": "^1.2.20"
|
|
16
36
|
}
|
|
17
37
|
}
|
package/index.js
DELETED