opencodekit 0.9.2 → 0.11.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/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +116 -47
- package/dist/template/.opencode/agent/build.md +16 -48
- package/dist/template/.opencode/agent/explore.md +13 -34
- package/dist/template/.opencode/agent/planner.md +41 -11
- package/dist/template/.opencode/agent/review.md +2 -23
- package/dist/template/.opencode/agent/rush.md +24 -65
- package/dist/template/.opencode/agent/scout.md +5 -21
- package/dist/template/.opencode/agent/vision.md +0 -14
- package/dist/template/.opencode/command/accessibility-check.md +293 -30
- package/dist/template/.opencode/command/analyze-mockup.md +406 -20
- package/dist/template/.opencode/command/analyze-project.md +439 -30
- package/dist/template/.opencode/command/brainstorm.md +288 -5
- package/dist/template/.opencode/command/commit.md +226 -17
- package/dist/template/.opencode/command/create.md +138 -35
- package/dist/template/.opencode/command/design-audit.md +477 -29
- package/dist/template/.opencode/command/design.md +609 -6
- package/dist/template/.opencode/command/edit-image.md +223 -20
- package/dist/template/.opencode/command/finish.md +162 -71
- package/dist/template/.opencode/command/fix-ci.md +296 -24
- package/dist/template/.opencode/command/fix-types.md +345 -13
- package/dist/template/.opencode/command/fix-ui.md +293 -13
- package/dist/template/.opencode/command/fix.md +256 -9
- package/dist/template/.opencode/command/generate-diagram.md +327 -26
- package/dist/template/.opencode/command/generate-icon.md +266 -22
- package/dist/template/.opencode/command/generate-image.md +232 -12
- package/dist/template/.opencode/command/generate-pattern.md +234 -20
- package/dist/template/.opencode/command/generate-storyboard.md +231 -21
- package/dist/template/.opencode/command/handoff.md +202 -30
- package/dist/template/.opencode/command/implement.md +162 -50
- package/dist/template/.opencode/command/import-plan.md +247 -51
- package/dist/template/.opencode/command/init.md +154 -35
- package/dist/template/.opencode/command/integration-test.md +405 -24
- package/dist/template/.opencode/command/issue.md +171 -21
- package/dist/template/.opencode/command/new-feature.md +382 -54
- package/dist/template/.opencode/command/plan.md +144 -118
- package/dist/template/.opencode/command/pr.md +229 -28
- package/dist/template/.opencode/command/quick-build.md +234 -5
- package/dist/template/.opencode/command/research-and-implement.md +436 -12
- package/dist/template/.opencode/command/research-ui.md +444 -34
- package/dist/template/.opencode/command/research.md +173 -45
- package/dist/template/.opencode/command/restore-image.md +416 -22
- package/dist/template/.opencode/command/resume.md +439 -63
- package/dist/template/.opencode/command/revert-feature.md +341 -64
- package/dist/template/.opencode/command/review-codebase.md +193 -4
- package/dist/template/.opencode/command/skill-create.md +506 -14
- package/dist/template/.opencode/command/skill-optimize.md +487 -16
- package/dist/template/.opencode/command/status.md +320 -60
- package/dist/template/.opencode/command/summarize.md +374 -33
- package/dist/template/.opencode/command/triage.md +355 -0
- package/dist/template/.opencode/command/ui-review.md +292 -25
- package/dist/template/.opencode/plugin/README.md +110 -98
- package/dist/template/.opencode/plugin/compactor.ts +95 -171
- package/dist/template/.opencode/plugin/enforcer.ts +177 -127
- package/dist/template/.opencode/plugin/injector.ts +150 -0
- package/dist/template/.opencode/plugin/lib/notify.ts +86 -0
- package/dist/template/.opencode/plugin/notification.ts +57 -123
- package/dist/template/.opencode/plugin/truncator.ts +60 -166
- package/dist/template/.opencode/skill/mqdh/SKILL.md +161 -0
- package/dist/template/.opencode/skill/playwriter/SKILL.md +148 -0
- package/dist/template/.opencode/skill/v0/SKILL.md +154 -0
- package/package.json +1 -1
|
@@ -1,140 +1,190 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode Enforcer Plugin
|
|
3
|
-
*
|
|
3
|
+
* ENFORCES continuation when session idles with incomplete TODOs
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Upgrade from notification-only to actual enforcement:
|
|
6
|
+
* Uses client.session.promptAsync() to inject continuation prompt
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
9
|
-
import {
|
|
10
|
-
import { readFileSync } from "fs";
|
|
11
|
-
|
|
12
|
-
// Notification helpers
|
|
13
|
-
function isWSL(): boolean {
|
|
14
|
-
try {
|
|
15
|
-
const release = readFileSync("/proc/version", "utf8").toLowerCase();
|
|
16
|
-
return release.includes("microsoft") || release.includes("wsl");
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
10
|
+
import { notify } from "./lib/notify";
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
interface TodoItem {
|
|
13
|
+
id: string;
|
|
14
|
+
content: string;
|
|
15
|
+
status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
16
|
+
priority: "high" | "medium" | "low";
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const safeTitle = title ? String(title) : "Notification";
|
|
29
|
-
const safeMessage = message ? String(message) : "";
|
|
30
|
-
const escapedTitle = escapeShell(safeTitle);
|
|
31
|
-
const escapedMessage = escapeShell(safeMessage);
|
|
32
|
-
|
|
33
|
-
let command: string;
|
|
34
|
-
|
|
35
|
-
if (platform === "darwin") {
|
|
36
|
-
command = `osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}"'`;
|
|
37
|
-
} else if (platform === "linux") {
|
|
38
|
-
command = `notify-send "${escapedTitle}" "${escapedMessage}"`;
|
|
39
|
-
if (isWSL()) {
|
|
40
|
-
command = `(${command}) 2>&1 || echo "WSL notification failed"`;
|
|
41
|
-
}
|
|
42
|
-
} else if (platform === "win32") {
|
|
43
|
-
command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${escapedMessage}', '${escapedTitle}')"`;
|
|
44
|
-
} else {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
exec(command, () => {});
|
|
49
|
-
}
|
|
19
|
+
// Cooldown to prevent rapid-fire enforcement (5 minutes)
|
|
20
|
+
const ENFORCEMENT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
50
21
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
22
|
+
// Track last enforcement time per session
|
|
23
|
+
const lastEnforcement = new Map<string, number>();
|
|
24
|
+
|
|
25
|
+
// Track todos per session
|
|
26
|
+
const sessionTodos = new Map<string, TodoItem[]>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build continuation prompt based on incomplete TODOs
|
|
30
|
+
*/
|
|
31
|
+
function buildContinuationPrompt(incomplete: TodoItem[]): string {
|
|
32
|
+
const highPriority = incomplete.filter((t) => t.priority === "high");
|
|
33
|
+
const inProgress = incomplete.filter((t) => t.status === "in_progress");
|
|
34
|
+
|
|
35
|
+
let prompt = "You stopped with incomplete work. Continue immediately.\n\n";
|
|
36
|
+
|
|
37
|
+
if (inProgress.length > 0) {
|
|
38
|
+
prompt += "**In Progress (finish these first):**\n";
|
|
39
|
+
inProgress.forEach((t) => {
|
|
40
|
+
prompt += `- ${t.content}\n`;
|
|
41
|
+
});
|
|
42
|
+
prompt += "\n";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (highPriority.length > 0) {
|
|
46
|
+
const remaining = highPriority.filter((t) => t.status !== "in_progress");
|
|
47
|
+
if (remaining.length > 0) {
|
|
48
|
+
prompt += "**High Priority:**\n";
|
|
49
|
+
remaining.forEach((t) => {
|
|
50
|
+
prompt += `- ${t.content}\n`;
|
|
51
|
+
});
|
|
52
|
+
prompt += "\n";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const others = incomplete.filter(
|
|
57
|
+
(t) => t.priority !== "high" && t.status !== "in_progress",
|
|
58
|
+
);
|
|
59
|
+
if (others.length > 0) {
|
|
60
|
+
prompt += "**Remaining:**\n";
|
|
61
|
+
others.slice(0, 5).forEach((t) => {
|
|
62
|
+
prompt += `- ${t.content}\n`;
|
|
63
|
+
});
|
|
64
|
+
if (others.length > 5) {
|
|
65
|
+
prompt += `- ... and ${others.length - 5} more\n`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
prompt += "\nPick up where you left off. Do not ask for confirmation.";
|
|
70
|
+
|
|
71
|
+
return prompt;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
|
-
export const EnforcerPlugin: Plugin = async ({ client }) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
74
|
+
export const EnforcerPlugin: Plugin = async ({ client, $ }) => {
|
|
75
|
+
return {
|
|
76
|
+
event: async ({ event }) => {
|
|
77
|
+
const props = event.properties as Record<string, unknown>;
|
|
78
|
+
|
|
79
|
+
// Track TODO updates
|
|
80
|
+
if (event.type === "todo.updated") {
|
|
81
|
+
const sessionId = props?.sessionID as string | undefined;
|
|
82
|
+
const todos = props?.todos as TodoItem[] | undefined;
|
|
83
|
+
|
|
84
|
+
if (sessionId && todos) {
|
|
85
|
+
sessionTodos.set(sessionId, todos);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Enforce on session idle
|
|
90
|
+
if (event.type === "session.idle") {
|
|
91
|
+
const sessionId = props?.sessionID as string | undefined;
|
|
92
|
+
if (!sessionId) return;
|
|
93
|
+
|
|
94
|
+
const todos = sessionTodos.get(sessionId) || [];
|
|
95
|
+
const incomplete = todos.filter(
|
|
96
|
+
(t) => t.status === "pending" || t.status === "in_progress",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// No incomplete TODOs - nothing to enforce
|
|
100
|
+
if (incomplete.length === 0) return;
|
|
101
|
+
|
|
102
|
+
// Check cooldown to prevent spam
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const lastTime = lastEnforcement.get(sessionId) || 0;
|
|
105
|
+
if (now - lastTime < ENFORCEMENT_COOLDOWN_MS) {
|
|
106
|
+
return; // Still in cooldown
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Count high priority and in-progress
|
|
110
|
+
const highPriority = incomplete.filter((t) => t.priority === "high");
|
|
111
|
+
const inProgress = incomplete.filter((t) => t.status === "in_progress");
|
|
112
|
+
|
|
113
|
+
// Only enforce for high-priority or in-progress items
|
|
114
|
+
// Low priority pending items don't warrant forced continuation
|
|
115
|
+
if (highPriority.length === 0 && inProgress.length === 0) {
|
|
116
|
+
// Just notify for low-priority items
|
|
117
|
+
await notify(
|
|
118
|
+
$,
|
|
119
|
+
"Session Idle",
|
|
120
|
+
`📋 ${incomplete.length} TODO${incomplete.length > 1 ? "s" : ""} remaining`,
|
|
121
|
+
);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Update cooldown
|
|
126
|
+
lastEnforcement.set(sessionId, now);
|
|
127
|
+
|
|
128
|
+
// Build and send continuation prompt
|
|
129
|
+
const continuationPrompt = buildContinuationPrompt(incomplete);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await client.session.promptAsync({
|
|
133
|
+
path: { id: sessionId },
|
|
134
|
+
body: {
|
|
135
|
+
parts: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: continuationPrompt,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
client.app
|
|
145
|
+
.log({
|
|
146
|
+
body: {
|
|
147
|
+
service: "enforcer",
|
|
148
|
+
level: "info",
|
|
149
|
+
message: `Enforced continuation: ${inProgress.length} in-progress, ${highPriority.length} high-priority TODOs`,
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
.catch(() => {});
|
|
153
|
+
|
|
154
|
+
await notify(
|
|
155
|
+
$,
|
|
156
|
+
"Enforcer",
|
|
157
|
+
`Continuing ${inProgress.length + highPriority.length} incomplete TODOs`,
|
|
158
|
+
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
// Fallback to notification if prompt fails
|
|
161
|
+
client.app
|
|
162
|
+
.log({
|
|
163
|
+
body: {
|
|
164
|
+
service: "enforcer",
|
|
165
|
+
level: "warn",
|
|
166
|
+
message: `Enforcement failed: ${(error as Error).message}`,
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
.catch(() => {});
|
|
170
|
+
|
|
171
|
+
const message =
|
|
172
|
+
highPriority.length > 0
|
|
173
|
+
? `🔴 ${highPriority.length} high-priority TODO${highPriority.length > 1 ? "s" : ""} incomplete`
|
|
174
|
+
: `🟡 ${inProgress.length} TODO${inProgress.length > 1 ? "s" : ""} still in progress`;
|
|
175
|
+
|
|
176
|
+
await notify($, "Session Idle", message);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Cleanup on session delete
|
|
181
|
+
if (event.type === "session.deleted") {
|
|
182
|
+
const sessionId = props?.sessionID as string | undefined;
|
|
183
|
+
if (sessionId) {
|
|
184
|
+
sessionTodos.delete(sessionId);
|
|
185
|
+
lastEnforcement.delete(sessionId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
140
190
|
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode AGENTS.md Hierarchy Injector Plugin
|
|
3
|
+
*
|
|
4
|
+
* Walks up from read file's directory to project root, collecting ALL AGENTS.md
|
|
5
|
+
* files and injecting them into context. Solves the limitation where OpenCode's
|
|
6
|
+
* findUp only finds the first match.
|
|
7
|
+
*
|
|
8
|
+
* Injection order: root → specific (T-shaped context loading)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync } from "fs";
|
|
12
|
+
import { dirname, join, resolve } from "path";
|
|
13
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
14
|
+
|
|
15
|
+
// Cache injected directories per session to avoid duplicates
|
|
16
|
+
const sessionInjectedDirs = new Map<string, Set<string>>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Walk up from a directory to root, collecting all AGENTS.md files
|
|
20
|
+
*/
|
|
21
|
+
function collectAgentsMdFiles(
|
|
22
|
+
startDir: string,
|
|
23
|
+
projectRoot: string,
|
|
24
|
+
): { path: string; content: string }[] {
|
|
25
|
+
const files: { path: string; content: string }[] = [];
|
|
26
|
+
let currentDir = resolve(startDir);
|
|
27
|
+
const root = resolve(projectRoot);
|
|
28
|
+
|
|
29
|
+
// Walk up until we hit project root or filesystem root
|
|
30
|
+
while (currentDir.startsWith(root) || currentDir === root) {
|
|
31
|
+
const agentsMdPath = join(currentDir, "AGENTS.md");
|
|
32
|
+
|
|
33
|
+
if (existsSync(agentsMdPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const content = readFileSync(agentsMdPath, "utf-8");
|
|
36
|
+
files.push({ path: agentsMdPath, content });
|
|
37
|
+
} catch {
|
|
38
|
+
// Skip unreadable files
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Move up one directory
|
|
43
|
+
const parentDir = dirname(currentDir);
|
|
44
|
+
if (parentDir === currentDir) break; // Hit filesystem root
|
|
45
|
+
currentDir = parentDir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Reverse so root comes first (T-shaped: general → specific)
|
|
49
|
+
return files.reverse();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a file path is a code file (not AGENTS.md itself)
|
|
54
|
+
*/
|
|
55
|
+
function isCodeFile(filePath: string): boolean {
|
|
56
|
+
const lowerPath = filePath.toLowerCase();
|
|
57
|
+
|
|
58
|
+
// Skip AGENTS.md files themselves
|
|
59
|
+
if (lowerPath.endsWith("agents.md")) return false;
|
|
60
|
+
|
|
61
|
+
// Skip common non-code files
|
|
62
|
+
if (lowerPath.includes("node_modules/")) return false;
|
|
63
|
+
if (lowerPath.includes("/.git/")) return false;
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Format injected AGENTS.md content
|
|
70
|
+
*/
|
|
71
|
+
function formatInjection(
|
|
72
|
+
files: { path: string; content: string }[],
|
|
73
|
+
projectRoot: string,
|
|
74
|
+
): string {
|
|
75
|
+
if (files.length === 0) return "";
|
|
76
|
+
|
|
77
|
+
const sections = files.map((f) => {
|
|
78
|
+
const relativePath = f.path.replace(projectRoot, "").replace(/^\//, "");
|
|
79
|
+
return `<!-- AGENTS.md from: ${relativePath} -->\n${f.content}`;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
"\n\n---\n## Injected AGENTS.md Hierarchy\n\n" +
|
|
84
|
+
sections.join("\n\n---\n\n") +
|
|
85
|
+
"\n---\n\n"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const InjectorPlugin: Plugin = async ({ directory, worktree }) => {
|
|
90
|
+
// Use worktree if available (git worktree), otherwise use directory
|
|
91
|
+
const projectRoot = worktree || directory;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"tool.execute.after": async (input, output) => {
|
|
95
|
+
// Only process read tool
|
|
96
|
+
if (input.tool !== "read") return;
|
|
97
|
+
|
|
98
|
+
// Get the file path from metadata
|
|
99
|
+
const filePath = output.metadata?.filePath as string | undefined;
|
|
100
|
+
if (!filePath) return;
|
|
101
|
+
|
|
102
|
+
// Skip non-code files
|
|
103
|
+
if (!isCodeFile(filePath)) return;
|
|
104
|
+
|
|
105
|
+
// Get or create session cache
|
|
106
|
+
let injectedDirs = sessionInjectedDirs.get(input.sessionID);
|
|
107
|
+
if (!injectedDirs) {
|
|
108
|
+
injectedDirs = new Set();
|
|
109
|
+
sessionInjectedDirs.set(input.sessionID, injectedDirs);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Get the directory of the read file
|
|
113
|
+
const fileDir = dirname(resolve(filePath));
|
|
114
|
+
|
|
115
|
+
// Check if we've already injected for this directory chain
|
|
116
|
+
if (injectedDirs.has(fileDir)) return;
|
|
117
|
+
|
|
118
|
+
// Collect AGENTS.md files
|
|
119
|
+
const agentsFiles = collectAgentsMdFiles(fileDir, projectRoot);
|
|
120
|
+
|
|
121
|
+
// Skip if no AGENTS.md files found
|
|
122
|
+
if (agentsFiles.length === 0) return;
|
|
123
|
+
|
|
124
|
+
// Mark this directory as injected
|
|
125
|
+
injectedDirs.add(fileDir);
|
|
126
|
+
|
|
127
|
+
// Also mark parent directories to avoid redundant injections
|
|
128
|
+
let dir = fileDir;
|
|
129
|
+
while (dir.startsWith(projectRoot) && dir !== projectRoot) {
|
|
130
|
+
injectedDirs.add(dir);
|
|
131
|
+
dir = dirname(dir);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Inject at the beginning of output
|
|
135
|
+
const injection = formatInjection(agentsFiles, projectRoot);
|
|
136
|
+
output.output = injection + output.output;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
event: async ({ event }) => {
|
|
140
|
+
// Clean up cache when session is deleted
|
|
141
|
+
if (event.type === "session.deleted") {
|
|
142
|
+
const props = event.properties as Record<string, unknown>;
|
|
143
|
+
const sessionId = props?.sessionID as string | undefined;
|
|
144
|
+
if (sessionId) {
|
|
145
|
+
sessionInjectedDirs.delete(sessionId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared notification utilities for OpenCode plugins
|
|
3
|
+
* Uses Bun's shell API ($) as recommended by official docs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Cache WSL detection result
|
|
7
|
+
let _isWSL: boolean | null = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if running in WSL (cached)
|
|
11
|
+
*/
|
|
12
|
+
export function isWSL(): boolean {
|
|
13
|
+
if (_isWSL !== null) return _isWSL;
|
|
14
|
+
try {
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const release = fs.readFileSync("/proc/version", "utf8").toLowerCase();
|
|
17
|
+
_isWSL = release.includes("microsoft") || release.includes("wsl");
|
|
18
|
+
} catch {
|
|
19
|
+
_isWSL = false;
|
|
20
|
+
}
|
|
21
|
+
return _isWSL ?? false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Send native notification using Bun shell API
|
|
26
|
+
* @param $ - Bun shell from plugin context
|
|
27
|
+
* @param title - Notification title
|
|
28
|
+
* @param message - Notification body
|
|
29
|
+
*/
|
|
30
|
+
export async function notify(
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
$: any,
|
|
33
|
+
title: string,
|
|
34
|
+
message: string,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
const platform = process.platform;
|
|
37
|
+
const safeTitle = title || "OpenCode";
|
|
38
|
+
const safeMessage = message || "";
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (platform === "darwin") {
|
|
42
|
+
await $`osascript -e ${'display notification "' + safeMessage + '" with title "' + safeTitle + '"'}`;
|
|
43
|
+
} else if (platform === "linux") {
|
|
44
|
+
if (isWSL()) {
|
|
45
|
+
// WSL: try notify-send, fail silently
|
|
46
|
+
await $`notify-send ${safeTitle} ${safeMessage}`.catch(() => {});
|
|
47
|
+
} else {
|
|
48
|
+
await $`notify-send ${safeTitle} ${safeMessage}`;
|
|
49
|
+
}
|
|
50
|
+
} else if (platform === "win32") {
|
|
51
|
+
// Windows: PowerShell toast (fire and forget)
|
|
52
|
+
await $`powershell -Command "Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${safeMessage}', '${safeTitle}')"`.catch(
|
|
53
|
+
() => {},
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Notifications are best-effort, never throw
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Threshold configuration for context monitoring
|
|
63
|
+
*/
|
|
64
|
+
export const THRESHOLDS = {
|
|
65
|
+
MODERATE: 70,
|
|
66
|
+
URGENT: 85,
|
|
67
|
+
CRITICAL: 95,
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Token statistics from session events
|
|
72
|
+
*/
|
|
73
|
+
export interface TokenStats {
|
|
74
|
+
used: number;
|
|
75
|
+
limit: number;
|
|
76
|
+
percentage?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Calculate context percentage from token stats
|
|
81
|
+
*/
|
|
82
|
+
export function getContextPercentage(stats: TokenStats): number {
|
|
83
|
+
if (stats.percentage) return stats.percentage;
|
|
84
|
+
if (stats.limit > 0) return Math.round((stats.used / stats.limit) * 100);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|