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.
- package/dist/index.js +2 -2
- package/dist/template/.opencode/command/accessibility-check.md +7 -10
- package/dist/template/.opencode/command/analyze-mockup.md +3 -16
- package/dist/template/.opencode/command/analyze-project.md +57 -69
- package/dist/template/.opencode/command/brainstorm.md +3 -11
- package/dist/template/.opencode/command/commit.md +10 -18
- package/dist/template/.opencode/command/create.md +4 -8
- package/dist/template/.opencode/command/design-audit.md +24 -51
- package/dist/template/.opencode/command/design.md +10 -17
- package/dist/template/.opencode/command/finish.md +9 -9
- package/dist/template/.opencode/command/fix-ci.md +7 -28
- package/dist/template/.opencode/command/fix-types.md +3 -7
- package/dist/template/.opencode/command/fix-ui.md +5 -11
- package/dist/template/.opencode/command/fix.md +4 -10
- package/dist/template/.opencode/command/handoff.md +8 -14
- package/dist/template/.opencode/command/implement.md +13 -16
- package/dist/template/.opencode/command/import-plan.md +20 -38
- package/dist/template/.opencode/command/init.md +9 -13
- package/dist/template/.opencode/command/integration-test.md +11 -13
- package/dist/template/.opencode/command/issue.md +4 -8
- package/dist/template/.opencode/command/new-feature.md +20 -40
- package/dist/template/.opencode/command/plan.md +8 -12
- package/dist/template/.opencode/command/pr.md +29 -38
- package/dist/template/.opencode/command/quick-build.md +3 -7
- package/dist/template/.opencode/command/research-and-implement.md +4 -6
- package/dist/template/.opencode/command/research.md +10 -7
- package/dist/template/.opencode/command/resume.md +12 -24
- package/dist/template/.opencode/command/revert-feature.md +21 -56
- package/dist/template/.opencode/command/review-codebase.md +21 -23
- package/dist/template/.opencode/command/skill-create.md +1 -5
- package/dist/template/.opencode/command/skill-optimize.md +3 -10
- package/dist/template/.opencode/command/status.md +28 -25
- package/dist/template/.opencode/command/triage.md +19 -31
- package/dist/template/.opencode/command/ui-review.md +6 -13
- package/dist/template/.opencode/command.backup/analyze-project.md +465 -0
- package/dist/template/.opencode/command.backup/finish.md +167 -0
- package/dist/template/.opencode/command.backup/implement.md +143 -0
- package/dist/template/.opencode/command.backup/pr.md +252 -0
- package/dist/template/.opencode/command.backup/status.md +376 -0
- package/dist/template/.opencode/memory/project/SHELL_OUTPUT_MIGRATION_PLAN.md +551 -0
- package/dist/template/.opencode/memory/project/gotchas.md +33 -28
- package/dist/template/.opencode/opencode.json +14 -28
- package/dist/template/.opencode/package.json +1 -3
- package/dist/template/.opencode/plugin/compaction.ts +51 -129
- package/dist/template/.opencode/plugin/handoff.ts +18 -163
- package/dist/template/.opencode/plugin/notification.ts +1 -1
- package/dist/template/.opencode/plugin/package.json +7 -0
- package/dist/template/.opencode/plugin/sessions.ts +185 -651
- package/dist/template/.opencode/plugin/skill-mcp.ts +2 -1
- package/dist/template/.opencode/plugin/truncator.ts +19 -41
- package/dist/template/.opencode/plugin/tsconfig.json +14 -13
- package/dist/template/.opencode/tool/bd-inbox.ts +109 -0
- package/dist/template/.opencode/tool/bd-msg.ts +62 -0
- package/dist/template/.opencode/tool/bd-release.ts +71 -0
- package/dist/template/.opencode/tool/bd-reserve.ts +120 -0
- package/package.json +2 -2
- package/dist/template/.opencode/plugin/beads.ts +0 -1419
- package/dist/template/.opencode/plugin/compactor.ts +0 -107
- package/dist/template/.opencode/plugin/enforcer.ts +0 -190
- 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 {
|
|
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
|
-
*
|
|
3
|
+
* Warns when tools return large outputs under context pressure
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
|
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(
|
|
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)
|
|
35
|
+
if (sessionId) {
|
|
36
|
+
sessionContext.delete(sessionId);
|
|
37
|
+
}
|
|
55
38
|
}
|
|
56
39
|
},
|
|
57
40
|
|
|
58
41
|
"tool.execute.after": async (input, output) => {
|
|
59
|
-
const
|
|
60
|
-
|
|
42
|
+
const pct = sessionContext.get(input.sessionID) || 0;
|
|
43
|
+
if (pct < 70) return; // Only warn under pressure
|
|
61
44
|
|
|
62
|
-
//
|
|
63
|
-
const
|
|
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:
|
|
77
|
-
message: `Large output from ${
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.
|
|
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.
|
|
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",
|