opencodekit 0.12.4 → 0.12.5

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 (60) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/template/.opencode/command/accessibility-check.md +7 -10
  3. package/dist/template/.opencode/command/analyze-mockup.md +3 -16
  4. package/dist/template/.opencode/command/analyze-project.md +57 -69
  5. package/dist/template/.opencode/command/brainstorm.md +3 -11
  6. package/dist/template/.opencode/command/commit.md +10 -18
  7. package/dist/template/.opencode/command/create.md +4 -8
  8. package/dist/template/.opencode/command/design-audit.md +24 -51
  9. package/dist/template/.opencode/command/design.md +10 -17
  10. package/dist/template/.opencode/command/finish.md +9 -9
  11. package/dist/template/.opencode/command/fix-ci.md +7 -28
  12. package/dist/template/.opencode/command/fix-types.md +3 -7
  13. package/dist/template/.opencode/command/fix-ui.md +5 -11
  14. package/dist/template/.opencode/command/fix.md +4 -10
  15. package/dist/template/.opencode/command/handoff.md +8 -14
  16. package/dist/template/.opencode/command/implement.md +13 -16
  17. package/dist/template/.opencode/command/import-plan.md +20 -38
  18. package/dist/template/.opencode/command/init.md +9 -13
  19. package/dist/template/.opencode/command/integration-test.md +11 -13
  20. package/dist/template/.opencode/command/issue.md +4 -8
  21. package/dist/template/.opencode/command/new-feature.md +20 -40
  22. package/dist/template/.opencode/command/plan.md +8 -12
  23. package/dist/template/.opencode/command/pr.md +29 -38
  24. package/dist/template/.opencode/command/quick-build.md +3 -7
  25. package/dist/template/.opencode/command/research-and-implement.md +4 -6
  26. package/dist/template/.opencode/command/research.md +10 -7
  27. package/dist/template/.opencode/command/resume.md +12 -24
  28. package/dist/template/.opencode/command/revert-feature.md +21 -56
  29. package/dist/template/.opencode/command/review-codebase.md +21 -23
  30. package/dist/template/.opencode/command/skill-create.md +1 -5
  31. package/dist/template/.opencode/command/skill-optimize.md +3 -10
  32. package/dist/template/.opencode/command/status.md +28 -25
  33. package/dist/template/.opencode/command/triage.md +19 -31
  34. package/dist/template/.opencode/command/ui-review.md +6 -13
  35. package/dist/template/.opencode/command.backup/analyze-project.md +465 -0
  36. package/dist/template/.opencode/command.backup/finish.md +167 -0
  37. package/dist/template/.opencode/command.backup/implement.md +143 -0
  38. package/dist/template/.opencode/command.backup/pr.md +252 -0
  39. package/dist/template/.opencode/command.backup/status.md +376 -0
  40. package/dist/template/.opencode/memory/project/SHELL_OUTPUT_MIGRATION_PLAN.md +551 -0
  41. package/dist/template/.opencode/memory/project/gotchas.md +33 -28
  42. package/dist/template/.opencode/opencode.json +14 -28
  43. package/dist/template/.opencode/package.json +1 -3
  44. package/dist/template/.opencode/plugin/compaction.ts +51 -129
  45. package/dist/template/.opencode/plugin/handoff.ts +18 -163
  46. package/dist/template/.opencode/plugin/notification.ts +1 -1
  47. package/dist/template/.opencode/plugin/package.json +7 -0
  48. package/dist/template/.opencode/plugin/sessions.ts +185 -651
  49. package/dist/template/.opencode/plugin/skill-mcp.ts +2 -1
  50. package/dist/template/.opencode/plugin/truncator.ts +19 -41
  51. package/dist/template/.opencode/plugin/tsconfig.json +14 -13
  52. package/dist/template/.opencode/tool/bd-inbox.ts +109 -0
  53. package/dist/template/.opencode/tool/bd-msg.ts +62 -0
  54. package/dist/template/.opencode/tool/bd-release.ts +71 -0
  55. package/dist/template/.opencode/tool/bd-reserve.ts +120 -0
  56. package/package.json +2 -2
  57. package/dist/template/.opencode/plugin/beads.ts +0 -1419
  58. package/dist/template/.opencode/plugin/compactor.ts +0 -107
  59. package/dist/template/.opencode/plugin/enforcer.ts +0 -190
  60. package/dist/template/.opencode/plugin/injector.ts +0 -150
@@ -2,7 +2,8 @@ import { type ChildProcess, spawn } from "child_process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
- import { type Plugin, tool } from "@opencode-ai/plugin";
5
+ import type { Plugin } from "@opencode-ai/plugin";
6
+ import { tool } from "@opencode-ai/plugin/tool";
6
7
 
7
8
  interface McpServerConfig {
8
9
  command: string;
@@ -1,80 +1,58 @@
1
1
  /**
2
2
  * OpenCode Truncator Plugin
3
- * Monitors tool output sizes and warns about large outputs
3
+ * Warns when tools return large outputs under context pressure
4
4
  *
5
- * NOTE: tool.execute.after hook is observation-only (returns void).
6
- * Actual truncation would require OpenCode core changes.
7
- * This plugin logs warnings when outputs are large under context pressure.
5
+ * Note: This doesn't actually truncate - OpenCode handles that via compaction.prune.
6
+ * This only adds WARNINGS that OpenCode doesn't provide.
8
7
  */
9
8
 
10
9
  import type { Plugin } from "@opencode-ai/plugin";
11
- import {
12
- THRESHOLDS,
13
- type TokenStats,
14
- getContextPercentage,
15
- } from "./lib/notify";
16
-
17
- // Warning thresholds for output size (chars)
18
- const OUTPUT_WARN = {
19
- MODERATE: 20000, // Warn if output > 20k at 70%+ context
20
- URGENT: 10000, // Warn if output > 10k at 85%+ context
21
- CRITICAL: 5000, // Warn if output > 5k at 95%+ context
22
- } as const;
23
-
24
- function getOutputThreshold(percentage: number): number | null {
25
- if (percentage >= THRESHOLDS.CRITICAL) return OUTPUT_WARN.CRITICAL;
26
- if (percentage >= THRESHOLDS.URGENT) return OUTPUT_WARN.URGENT;
27
- if (percentage >= THRESHOLDS.MODERATE) return OUTPUT_WARN.MODERATE;
28
- return null; // No warning under 70%
29
- }
30
10
 
31
11
  export const TruncatorPlugin: Plugin = async ({ client }) => {
32
- // Track context percentage per session
33
12
  const sessionContext = new Map<string, number>();
34
13
 
35
14
  return {
36
15
  event: async ({ event }) => {
37
16
  const props = event.properties as Record<string, unknown>;
38
17
 
39
- // Update context tracking from session updates
40
18
  if (event.type === "session.updated") {
41
19
  const info = props?.info as Record<string, unknown> | undefined;
42
20
  const tokenStats = (info?.tokens || props?.tokens) as
43
- | TokenStats
21
+ | { used: number; limit: number }
44
22
  | undefined;
45
23
  const sessionId = (info?.id || props?.sessionID) as string | undefined;
46
24
 
47
25
  if (sessionId && tokenStats?.used && tokenStats?.limit) {
48
- sessionContext.set(sessionId, getContextPercentage(tokenStats));
26
+ sessionContext.set(
27
+ sessionId,
28
+ Math.round((tokenStats.used / tokenStats.limit) * 100),
29
+ );
49
30
  }
50
31
  }
51
32
 
52
33
  if (event.type === "session.deleted") {
53
34
  const sessionId = props?.sessionID as string | undefined;
54
- if (sessionId) sessionContext.delete(sessionId);
35
+ if (sessionId) {
36
+ sessionContext.delete(sessionId);
37
+ }
55
38
  }
56
39
  },
57
40
 
58
41
  "tool.execute.after": async (input, output) => {
59
- const toolName = input.tool;
60
- const sessionId = input.sessionID;
42
+ const pct = sessionContext.get(input.sessionID) || 0;
43
+ if (pct < 70) return; // Only warn under pressure
61
44
 
62
- // Get current context pressure
63
- const percentage = sessionContext.get(sessionId) || 0;
64
- const threshold = getOutputThreshold(percentage);
65
-
66
- // Only check when under pressure
67
- if (!threshold) return;
68
-
69
- // Check output size
45
+ // Thresholds get tighter as context fills up
46
+ const threshold = pct >= 95 ? 5000 : pct >= 85 ? 10000 : 20000;
70
47
  const outputStr = output.output || "";
48
+
71
49
  if (outputStr.length > threshold) {
72
- client.app
50
+ await client.app
73
51
  .log({
74
52
  body: {
75
53
  service: "truncator",
76
- level: percentage >= THRESHOLDS.CRITICAL ? "warn" : "info",
77
- message: `Large output from ${toolName}: ${outputStr.length} chars (threshold: ${threshold}, context: ${percentage}%)`,
54
+ level: pct >= 95 ? "warn" : "info",
55
+ message: `Large output from ${input.tool}: ${outputStr.length} chars (threshold: ${threshold}, context: ${pct}%)`,
78
56
  },
79
57
  })
80
58
  .catch(() => {});
@@ -1,15 +1,16 @@
1
1
  {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "allowSyntheticDefaultImports": true,
7
- "esModuleInterop": true,
8
- "strict": true,
9
- "skipLibCheck": true,
10
- "forceConsistentCasingInFileNames": true,
11
- "types": ["node"]
12
- },
13
- "include": ["**/*.ts"],
14
- "exclude": ["node_modules"]
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "nodenext",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "noImplicitAny": false,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["**/*.ts"],
15
+ "exclude": ["node_modules"]
15
16
  }
@@ -0,0 +1,109 @@
1
+ import path from "path";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import fs from "fs/promises";
4
+
5
+ const RESERVATIONS_DIR = ".reservations";
6
+ const MESSAGES_FILE = "messages.jsonl";
7
+
8
+ interface Message {
9
+ id: string;
10
+ from: string;
11
+ to: string;
12
+ subj: string;
13
+ body?: string;
14
+ importance: string;
15
+ at: number;
16
+ read: boolean;
17
+ }
18
+
19
+ export default tool({
20
+ description:
21
+ "Read messages from other agents. Returns most recent messages addressed to you or broadcast to 'all'.",
22
+ args: {
23
+ n: tool.schema
24
+ .number()
25
+ .optional()
26
+ .default(5)
27
+ .describe("Max messages to return"),
28
+ unread: tool.schema
29
+ .boolean()
30
+ .optional()
31
+ .default(false)
32
+ .describe("Only show unread messages"),
33
+ ack: tool.schema
34
+ .array(tool.schema.string())
35
+ .optional()
36
+ .describe("Message IDs to mark as read"),
37
+ },
38
+ execute: async (args, context) => {
39
+ const cwd = process.cwd();
40
+ const agentId = context?.agent || `agent-${process.pid}`;
41
+ const messagesPath = path.join(cwd, RESERVATIONS_DIR, MESSAGES_FILE);
42
+
43
+ try {
44
+ const content = await fs.readFile(messagesPath, "utf-8");
45
+ if (!content.trim()) {
46
+ return JSON.stringify({ msgs: [], count: 0 });
47
+ }
48
+
49
+ const idsToAck = new Set(args.ack || []);
50
+ let messages: Message[] = [];
51
+ const lines = content.trim().split("\n");
52
+
53
+ for (const line of lines) {
54
+ if (!line.trim()) continue;
55
+ try {
56
+ const msg = JSON.parse(line) as Message;
57
+ // Filter to messages for this agent or broadcast
58
+ if (msg.to === "all" || msg.to === agentId) {
59
+ // Mark as read if in ack list
60
+ if (idsToAck.has(msg.id)) {
61
+ msg.read = true;
62
+ }
63
+ messages.push(msg);
64
+ }
65
+ } catch {
66
+ // Skip invalid lines
67
+ }
68
+ }
69
+
70
+ // If acking, rewrite the file
71
+ if (idsToAck.size > 0) {
72
+ const allMsgs: Message[] = [];
73
+ for (const line of lines) {
74
+ if (!line.trim()) continue;
75
+ try {
76
+ const msg = JSON.parse(line) as Message;
77
+ if (idsToAck.has(msg.id)) {
78
+ msg.read = true;
79
+ }
80
+ allMsgs.push(msg);
81
+ } catch {
82
+ // Skip
83
+ }
84
+ }
85
+ await fs.writeFile(
86
+ messagesPath,
87
+ allMsgs.map((m) => JSON.stringify(m)).join("\n") + "\n",
88
+ "utf-8",
89
+ );
90
+ }
91
+
92
+ // Filter unread if requested
93
+ if (args.unread) {
94
+ messages = messages.filter((m) => !m.read);
95
+ }
96
+
97
+ // Return most recent N
98
+ const limit = args.n || 5;
99
+ messages = messages.slice(-limit).reverse();
100
+
101
+ return JSON.stringify({ msgs: messages, count: messages.length });
102
+ } catch (e: any) {
103
+ if (e.code === "ENOENT") {
104
+ return JSON.stringify({ msgs: [], count: 0 });
105
+ }
106
+ return JSON.stringify({ error: e.message });
107
+ }
108
+ },
109
+ });
@@ -0,0 +1,62 @@
1
+ import path from "path";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import fs from "fs/promises";
4
+
5
+ const RESERVATIONS_DIR = ".reservations";
6
+ const MESSAGES_FILE = "messages.jsonl";
7
+
8
+ interface Message {
9
+ id: string;
10
+ from: string;
11
+ to: string;
12
+ subj: string;
13
+ body?: string;
14
+ importance: string;
15
+ at: number;
16
+ read: boolean;
17
+ }
18
+
19
+ export default tool({
20
+ description:
21
+ "Send message to other agents or broadcast to all. Messages stored in .reservations/messages.jsonl.",
22
+ args: {
23
+ subj: tool.schema.string().describe("Message subject"),
24
+ body: tool.schema.string().optional().describe("Message body"),
25
+ to: tool.schema
26
+ .string()
27
+ .optional()
28
+ .default("all")
29
+ .describe("Recipient agent ID or 'all' for broadcast"),
30
+ importance: tool.schema
31
+ .string()
32
+ .optional()
33
+ .default("normal")
34
+ .describe("Priority: low | normal | high"),
35
+ },
36
+ execute: async (args, context) => {
37
+ if (!args.subj) {
38
+ return JSON.stringify({ error: "subj required" });
39
+ }
40
+
41
+ const cwd = process.cwd();
42
+ const agentId = context?.agent || `agent-${process.pid}`;
43
+ const messagesPath = path.join(cwd, RESERVATIONS_DIR, MESSAGES_FILE);
44
+
45
+ // Ensure dir exists
46
+ await fs.mkdir(path.join(cwd, RESERVATIONS_DIR), { recursive: true });
47
+
48
+ const msg: Message = {
49
+ id: `msg-${Date.now().toString(36)}`,
50
+ from: agentId,
51
+ to: args.to || "all",
52
+ subj: args.subj,
53
+ body: args.body,
54
+ importance: args.importance || "normal",
55
+ at: Date.now(),
56
+ read: false,
57
+ };
58
+
59
+ await fs.appendFile(messagesPath, JSON.stringify(msg) + "\n", "utf-8");
60
+ return JSON.stringify({ ok: 1, id: msg.id });
61
+ },
62
+ });
@@ -0,0 +1,71 @@
1
+ import path from "path";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import fs from "fs/promises";
4
+
5
+ const RESERVATIONS_DIR = ".reservations";
6
+
7
+ function lockDir(filePath: string): string {
8
+ const safe = filePath.replace(/[/\\]/g, "_").replace(/\.\./g, "_");
9
+ return path.join(RESERVATIONS_DIR, `${safe}.lock`);
10
+ }
11
+
12
+ export default tool({
13
+ description:
14
+ "Release file locks. If no paths specified, lists all active locks.",
15
+ args: {
16
+ paths: tool.schema
17
+ .array(tool.schema.string())
18
+ .optional()
19
+ .describe("File paths to unlock (empty = list locks)"),
20
+ },
21
+ execute: async (args) => {
22
+ const cwd = process.cwd();
23
+ const reservationsPath = path.join(cwd, RESERVATIONS_DIR);
24
+
25
+ // If no paths, list locks
26
+ if (!args.paths?.length) {
27
+ try {
28
+ const entries = await fs.readdir(reservationsPath);
29
+ const locks = [];
30
+ const now = Date.now();
31
+
32
+ for (const entry of entries) {
33
+ if (entry.endsWith(".lock")) {
34
+ const metaPath = path.join(reservationsPath, entry, "meta.json");
35
+ try {
36
+ const content = await fs.readFile(metaPath, "utf-8");
37
+ const lock = JSON.parse(content);
38
+ if (lock.expires > now) {
39
+ locks.push({
40
+ path: lock.path,
41
+ agent: lock.agent,
42
+ expires: new Date(lock.expires).toISOString(),
43
+ });
44
+ }
45
+ } catch {
46
+ // Skip invalid locks
47
+ }
48
+ }
49
+ }
50
+
51
+ return JSON.stringify({ locks, count: locks.length });
52
+ } catch {
53
+ return JSON.stringify({ locks: [], count: 0 });
54
+ }
55
+ }
56
+
57
+ // Release specified paths
58
+ const released: string[] = [];
59
+ for (const filePath of args.paths) {
60
+ const lockPath = path.join(cwd, lockDir(filePath));
61
+ try {
62
+ await fs.rm(lockPath, { recursive: true });
63
+ released.push(filePath);
64
+ } catch {
65
+ // Already released or never locked
66
+ }
67
+ }
68
+
69
+ return JSON.stringify({ released });
70
+ },
71
+ });
@@ -0,0 +1,120 @@
1
+ import path from "path";
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import fs from "fs/promises";
4
+
5
+ const RESERVATIONS_DIR = ".reservations";
6
+
7
+ interface LockData {
8
+ path: string;
9
+ agent: string;
10
+ reason?: string;
11
+ created: number;
12
+ expires: number;
13
+ }
14
+
15
+ function lockDir(filePath: string): string {
16
+ const safe = filePath.replace(/[/\\]/g, "_").replace(/\.\./g, "_");
17
+ return path.join(RESERVATIONS_DIR, `${safe}.lock`);
18
+ }
19
+
20
+ export default tool({
21
+ description:
22
+ "Lock files for editing to prevent conflicts between agents. Uses atomic mkdir-based locking.",
23
+ args: {
24
+ paths: tool.schema
25
+ .array(tool.schema.string())
26
+ .describe("File paths to lock"),
27
+ reason: tool.schema
28
+ .string()
29
+ .optional()
30
+ .describe("Why reserving these files"),
31
+ ttl: tool.schema
32
+ .number()
33
+ .optional()
34
+ .default(600)
35
+ .describe("Lock TTL in seconds (default 600 = 10 min)"),
36
+ },
37
+ execute: async (args, context) => {
38
+ if (!args.paths?.length) {
39
+ return JSON.stringify({ error: "paths required" });
40
+ }
41
+
42
+ const cwd = process.cwd();
43
+ const agentId = context?.agent || `agent-${process.pid}`;
44
+ const ttlSeconds = args.ttl || 600;
45
+ const now = Date.now();
46
+ const expires = now + ttlSeconds * 1000;
47
+
48
+ // Ensure reservations dir exists
49
+ await fs.mkdir(path.join(cwd, RESERVATIONS_DIR), { recursive: true });
50
+
51
+ const granted: string[] = [];
52
+ const conflicts: { path: string; holder?: string }[] = [];
53
+
54
+ for (const filePath of args.paths) {
55
+ const lockPath = path.join(cwd, lockDir(filePath));
56
+ const metaPath = path.join(lockPath, "meta.json");
57
+
58
+ try {
59
+ // Atomic: mkdir fails if dir exists
60
+ await fs.mkdir(lockPath, { recursive: false });
61
+
62
+ // Lock acquired - write metadata
63
+ const lockData: LockData = {
64
+ path: filePath,
65
+ agent: agentId,
66
+ reason: args.reason,
67
+ created: now,
68
+ expires,
69
+ };
70
+ await fs.writeFile(metaPath, JSON.stringify(lockData), "utf-8");
71
+ granted.push(filePath);
72
+ } catch (e: any) {
73
+ if (e.code === "EEXIST") {
74
+ // Lock exists - check if expired or ours
75
+ try {
76
+ const content = await fs.readFile(metaPath, "utf-8");
77
+ const lock: LockData = JSON.parse(content);
78
+
79
+ if (lock.expires < now) {
80
+ // Expired - remove and retry
81
+ await fs.rm(lockPath, { recursive: true });
82
+ // Retry acquisition
83
+ try {
84
+ await fs.mkdir(lockPath, { recursive: false });
85
+ const lockData: LockData = {
86
+ path: filePath,
87
+ agent: agentId,
88
+ reason: args.reason,
89
+ created: now,
90
+ expires,
91
+ };
92
+ await fs.writeFile(metaPath, JSON.stringify(lockData), "utf-8");
93
+ granted.push(filePath);
94
+ } catch {
95
+ conflicts.push({ path: filePath });
96
+ }
97
+ } else if (lock.agent === agentId) {
98
+ // We already hold it - refresh
99
+ lock.expires = expires;
100
+ await fs.writeFile(metaPath, JSON.stringify(lock), "utf-8");
101
+ granted.push(filePath);
102
+ } else {
103
+ conflicts.push({ path: filePath, holder: lock.agent });
104
+ }
105
+ } catch {
106
+ // Corrupted lock - remove and retry
107
+ await fs.rm(lockPath, { recursive: true, force: true });
108
+ conflicts.push({ path: filePath });
109
+ }
110
+ } else {
111
+ conflicts.push({ path: filePath });
112
+ }
113
+ }
114
+ }
115
+
116
+ const response: Record<string, unknown> = { granted };
117
+ if (conflicts.length) response.conflicts = conflicts;
118
+ return JSON.stringify(response);
119
+ },
120
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodekit",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "type": "module",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@clack/prompts": "^0.7.0",
37
- "@opencode-ai/plugin": "^1.0.141",
37
+ "@opencode-ai/plugin": "^1.1.2",
38
38
  "beads-village": "^1.3.3",
39
39
  "cac": "^6.7.14",
40
40
  "cli-table3": "^0.6.5",