@jetrabbits/agentic 0.2.0 → 0.3.1

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.
Files changed (36) hide show
  1. package/AGENTS.md +13 -15
  2. package/CHANGELOG.md +24 -0
  3. package/MEMORY.md +67 -0
  4. package/Makefile +96 -14
  5. package/README.md +2 -2
  6. package/agentic +1269 -99
  7. package/areas/devops/ci-cd/AGENTS.md +1 -15
  8. package/areas/devops/database-ops/AGENTS.md +1 -15
  9. package/areas/devops/devsecops/AGENTS.md +1 -15
  10. package/areas/devops/infrastructure/AGENTS.md +1 -15
  11. package/areas/devops/kubernetes/AGENTS.md +1 -15
  12. package/areas/devops/networking/AGENTS.md +1 -15
  13. package/areas/devops/observability/AGENTS.md +1 -15
  14. package/areas/devops/sre/AGENTS.md +1 -15
  15. package/areas/software/backend/AGENTS.md +1 -16
  16. package/areas/software/data-engineering/AGENTS.md +1 -16
  17. package/areas/software/frontend/AGENTS.md +1 -16
  18. package/areas/software/full-stack/AGENTS.md +1 -16
  19. package/areas/software/general/AGENTS.md +1 -7
  20. package/areas/software/mlops/AGENTS.md +1 -16
  21. package/areas/software/mobile/AGENTS.md +1 -16
  22. package/areas/software/platform/AGENTS.md +1 -16
  23. package/areas/software/qa/AGENTS.md +1 -16
  24. package/areas/software/security/AGENTS.md +1 -16
  25. package/areas/template/AGENTS.tmpl.md +1 -17
  26. package/docs/agentic-lifecycle.md +8 -4
  27. package/docs/agentic-stabilization/README.md +37 -0
  28. package/docs/agentic-token-minimization/README.md +7 -5
  29. package/docs/agentic-usage.md +17 -14
  30. package/docs/opencode_setup.md +8 -4
  31. package/extensions/opencode/opencode.json +1 -1
  32. package/extensions/opencode/plugins/agent-model-mapper.ts +117 -0
  33. package/extensions/opencode/plugins/telegram-notification.ts +30 -20
  34. package/package.json +2 -1
  35. package/extensions/opencode/plugins/model-checker.json +0 -13
  36. package/extensions/opencode/plugins/model-checker.ts +0 -302
@@ -1,302 +0,0 @@
1
- import type { Plugin } from "@opencode-ai/plugin"
2
- import { mkdir, readFile, writeFile } from "node:fs/promises"
3
- import { readFileSync } from "node:fs"
4
- import { join } from "node:path"
5
- import { tmpdir } from "node:os"
6
- import { spawn } from "node:child_process"
7
-
8
- interface ModelResult {
9
- model: string
10
- status: "passed" | "failed"
11
- error?: string
12
- }
13
-
14
- interface ModelCheckerConfig {
15
- models: string[]
16
- timeoutMs: number
17
- concurrency: number
18
- prompt: string
19
- probeDir: string
20
- }
21
-
22
- type CommandResult = {
23
- code: number | null
24
- output: string
25
- timedOut: boolean
26
- durationMs: number
27
- }
28
-
29
- type AgenticPluginConfig = {
30
- modelChecker?: {
31
- enabled?: boolean
32
- }
33
- }
34
-
35
- function readAgenticConfig(): AgenticPluginConfig {
36
- const configHome = process.env.XDG_CONFIG_HOME || join(process.env.HOME || "", ".config")
37
- const configPath = join(configHome, "agentic", "opencode-plugins.json")
38
-
39
- try {
40
- return JSON.parse(readFileSync(configPath, "utf-8")) as AgenticPluginConfig
41
- } catch {
42
- return {}
43
- }
44
- }
45
-
46
- async function readModelsJson(projectDir: string): Promise<ModelCheckerConfig> {
47
- const defaults: ModelCheckerConfig = {
48
- models: [],
49
- timeoutMs: 10000,
50
- concurrency: 3,
51
- prompt: "Reply with exactly: OK",
52
- probeDir: tmpdir(),
53
- }
54
-
55
- try {
56
- const filePath = join(projectDir, ".opencode", "plugins", "model-checker.json")
57
- const content = await readFile(filePath, "utf-8")
58
- const data = JSON.parse(content)
59
- if (!Array.isArray(data.models)) return defaults
60
-
61
- const models = [...new Set(data.models.map(String).filter(Boolean))]
62
- const timeoutMs = Number.isFinite(data.timeoutMs)
63
- ? Math.min(120000, Math.max(1000, Number(data.timeoutMs)))
64
- : defaults.timeoutMs
65
- const concurrency = Number.isFinite(data.concurrency)
66
- ? Math.min(10, Math.max(1, Number(data.concurrency)))
67
- : defaults.concurrency
68
- const prompt = typeof data.prompt === "string" && data.prompt.trim() ? data.prompt.trim() : defaults.prompt
69
- const probeDir = typeof data.probeDir === "string" && data.probeDir.trim() ? data.probeDir.trim() : defaults.probeDir
70
-
71
- return {
72
- models,
73
- timeoutMs,
74
- concurrency,
75
- prompt,
76
- probeDir,
77
- }
78
- } catch {
79
- return defaults
80
- }
81
- }
82
-
83
- async function runModelProbe(model: string, prompt: string, timeoutMs: number, probeDir: string): Promise<CommandResult> {
84
- return await new Promise((resolve) => {
85
- const startedAt = Date.now()
86
- const child = spawn("opencode", ["run", prompt, "-m", model, "--dir", probeDir, "--log-level", "ERROR"], {
87
- cwd: probeDir,
88
- stdio: ["ignore", "pipe", "pipe"],
89
- env: {
90
- ...process.env,
91
- OPENCODE_MODEL_CHECKER_ACTIVE: "1",
92
- },
93
- })
94
-
95
- let stdout = ""
96
- let stderr = ""
97
- let timedOut = false
98
-
99
- const timer = setTimeout(() => {
100
- timedOut = true
101
- child.kill("SIGKILL")
102
- }, timeoutMs)
103
-
104
- child.stdout.on("data", (chunk: Buffer | string) => {
105
- stdout += chunk.toString()
106
- })
107
-
108
- child.stderr.on("data", (chunk: Buffer | string) => {
109
- stderr += chunk.toString()
110
- })
111
-
112
- child.on("close", (code) => {
113
- clearTimeout(timer)
114
- resolve({
115
- code,
116
- timedOut,
117
- output: `${stdout}\n${stderr}`.toLowerCase(),
118
- durationMs: Date.now() - startedAt,
119
- })
120
- })
121
-
122
- child.on("error", (error) => {
123
- clearTimeout(timer)
124
- resolve({
125
- code: 1,
126
- timedOut,
127
- output: String(error).toLowerCase(),
128
- durationMs: Date.now() - startedAt,
129
- })
130
- })
131
- })
132
- }
133
-
134
- async function runModelChecks(
135
- models: string[],
136
- prompt: string,
137
- timeoutMs: number,
138
- concurrency: number,
139
- probeDir: string,
140
- ): Promise<CommandResult[]> {
141
- const workers = Math.min(concurrency, models.length)
142
- const results: CommandResult[] = new Array(models.length)
143
- let cursor = 0
144
-
145
- await Promise.all(
146
- Array.from({ length: workers }, async () => {
147
- while (true) {
148
- const index = cursor++
149
- if (index >= models.length) return
150
- results[index] = await runModelProbe(models[index], prompt, timeoutMs, probeDir)
151
- }
152
- }),
153
- )
154
-
155
- return results
156
- }
157
-
158
- function classifyResult(model: string, probe: CommandResult): ModelResult {
159
- if (probe.timedOut) return { model, status: "failed", error: "timeout" }
160
-
161
- if (probe.output.includes("quota") || probe.output.includes("insufficient") || probe.output.includes("429")) {
162
- return { model, status: "failed", error: "quota" }
163
- }
164
-
165
- if (probe.code !== 0) {
166
- if (/(\b4\d\d\b|\b5\d\d\b)/.test(probe.output)) {
167
- return { model, status: "failed", error: "provider error" }
168
- }
169
- return { model, status: "failed", error: `exit code ${probe.code ?? "null"}` }
170
- }
171
-
172
- if (!probe.output.includes("ok")) {
173
- return { model, status: "failed", error: "unexpected response" }
174
- }
175
-
176
- return { model, status: "passed" }
177
- }
178
-
179
- function printLists(passed: ModelResult[], failed: ModelResult[]) {
180
- console.log("\npassed_models:")
181
- for (const model of passed) {
182
- console.log(`- ${model.model}`)
183
- }
184
-
185
- console.log("\nfailed_models:")
186
- for (const model of failed) {
187
- console.log(`- ${model.model}${model.error ? ` (${model.error})` : ""}`)
188
- }
189
- }
190
-
191
- function formatTelegramMessage(passed: ModelResult[], failed: ModelResult[], selected: string): string {
192
- const passedText = passed.length ? passed.map((x) => `• ${x.model}`).join("\n") : "(none)"
193
- const failedText = failed.length
194
- ? failed.map((x) => `• ${x.model}${x.error ? ` (${x.error})` : ""}`).join("\n")
195
- : "(none)"
196
-
197
- return [
198
- "<b>LLM availability check</b>",
199
- "",
200
- `<b>passed_models (${passed.length})</b>`,
201
- passedText,
202
- "",
203
- `<b>failed_models (${failed.length})</b>`,
204
- failedText,
205
- "",
206
- `<b>selected model:</b> ${selected}`,
207
- ].join("\n")
208
- }
209
-
210
- async function sendTelegram(message: string) {
211
- const token = process.env.OPENCODE_TELEGRAM_BOT_TOKEN
212
- const chatID = process.env.OPENCODE_TELEGRAM_CHAT_ID
213
- if (!token || !chatID) return
214
-
215
- try {
216
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
217
- method: "POST",
218
- headers: { "Content-Type": "application/json" },
219
- body: JSON.stringify({
220
- chat_id: chatID,
221
- text: message,
222
- parse_mode: "HTML",
223
- }),
224
- })
225
- } catch {
226
- // no-op
227
- }
228
- }
229
-
230
- async function regenerateOpencodeJson(projectDir: string, selected: string, passed: string[]) {
231
- const filePath = join(projectDir, ".opencode", "opencode.json")
232
- try {
233
- const content = await readFile(filePath, "utf-8")
234
- const config = JSON.parse(content)
235
-
236
- if (!config.agent || typeof config.agent !== "object") return
237
-
238
- for (const [agentName, agentConfig] of Object.entries<any>(config.agent)) {
239
- if (!agentConfig || typeof agentConfig !== "object") continue
240
- if (agentConfig.mode !== "subagent") continue
241
-
242
- config.agent[agentName].model = selected
243
- }
244
-
245
- await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`, "utf-8")
246
- } catch {
247
- // no-op
248
- }
249
- }
250
-
251
- export const ModelCheckerPlugin: Plugin = async ({ directory }) => {
252
- const config = readAgenticConfig()
253
- if (!config.modelChecker?.enabled) {
254
- return {}
255
- }
256
-
257
- if (process.env.OPENCODE_MODEL_CHECKER_ACTIVE === "1") {
258
- return {}
259
- }
260
-
261
- if (process.argv[2] === "run") {
262
- return {}
263
- }
264
-
265
- async function runChecks() {
266
- const config = await readModelsJson(directory)
267
- const { models, prompt, timeoutMs, concurrency, probeDir } = config
268
- if (!models.length) return
269
-
270
- await mkdir(probeDir, { recursive: true })
271
-
272
- console.log("\nStarting LLM model availability check...")
273
- console.log(`- models: ${models.length}, concurrency: ${Math.min(concurrency, models.length)}, timeout: ${timeoutMs}ms`)
274
- const results: ModelResult[] = []
275
- const probes = await runModelChecks(models, prompt, timeoutMs, concurrency, probeDir)
276
- for (const [index, model] of models.entries()) {
277
- const probe = probes[index]
278
- const result = classifyResult(model, probe)
279
- results.push(result)
280
- console.log(
281
- `- checking ${model}... ${result.status === "passed" ? "OK" : `FAIL (${result.error})`} (${probe.durationMs}ms)`,
282
- )
283
- }
284
-
285
- const passedModels = results.filter((x) => x.status === "passed")
286
- const failedModels = results.filter((x) => x.status === "failed")
287
- const selected = passedModels[0]?.model ?? "none"
288
-
289
- printLists(passedModels, failedModels)
290
- console.log(`\nselected_model: ${selected}`)
291
-
292
- if (selected !== "none") {
293
- await regenerateOpencodeJson(directory, selected, passedModels.map((x) => x.model))
294
- }
295
-
296
- await sendTelegram(formatTelegramMessage(passedModels, failedModels, selected))
297
- }
298
-
299
- await runChecks()
300
-
301
- return {}
302
- }