opencode-forgekeeper 2.2.1 → 2.4.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/README.md +8 -3
- package/dist/index.js +45 -8
- package/dist/tui.js +26 -22
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ It prefixes the session title with their references, shows the PR/MR status in t
|
|
|
5
5
|
footer, and tells the agent about new review feedback. It supports GitLab and GitHub. The status
|
|
6
6
|
and review notifications are optional; see [Keep only session titles](#keep-only-session-titles).
|
|
7
7
|
|
|
8
|
-
Requires OpenCode 2. It replaces
|
|
8
|
+
Requires OpenCode 2.2 or later. It replaces
|
|
9
9
|
[`opencode-forge-session-title`](https://www.npmjs.com/package/opencode-forge-session-title); see
|
|
10
10
|
[Migrating from opencode-forge-session-title](#migrating-from-opencode-forge-session-title).
|
|
11
11
|
|
|
@@ -46,7 +46,9 @@ what's missing, such as `glab is not installed`.
|
|
|
46
46
|
## Session targets
|
|
47
47
|
|
|
48
48
|
The server registers the `set_session_target` tool and adds instructions to the agent's
|
|
49
|
-
context.
|
|
49
|
+
context. The current target is stored in the `forgekeeper-session-target` session instruction
|
|
50
|
+
entry. Updates take effect at the next step without changing the system prompt.
|
|
51
|
+
When you establish or change the primary issue, PR, or MR, the agent sets its full
|
|
50
52
|
URL as the session target. Background references, comparisons, and dependencies keep the
|
|
51
53
|
current target. After the agent creates a PR/MR for the current task, it sets the new PR/MR
|
|
52
54
|
as the target, passing the current issue as `issue_url`.
|
|
@@ -224,7 +226,9 @@ For each explicit target that is an open PR/MR, the server tells the agent about
|
|
|
224
226
|
A target that merges or closes gets no more notifications, and a notification waiting for a
|
|
225
227
|
retry is dropped.
|
|
226
228
|
Each notification is a queued synthetic message, followed by a toast in the CLIs that have
|
|
227
|
-
shown the session.
|
|
229
|
+
shown the session. With session tabs on, those CLIs also reopen the session's tab if you
|
|
230
|
+
closed it, without leaving the session you're in. The message resumes the session unless
|
|
231
|
+
`resumeSession` is `false`.
|
|
228
232
|
Messages include the full URL of each PR/MR. PRs/MRs from the title or the branch don't get
|
|
229
233
|
notifications.
|
|
230
234
|
|
|
@@ -384,6 +388,7 @@ notifications resume the session. Set `reviewStatus` to `false` to keep only tit
|
|
|
384
388
|
and a PR/MR's source branch.
|
|
385
389
|
- `src/status-client.ts` holds the CLI's leases, snapshots, and per-directory settings.
|
|
386
390
|
`src/tui.tsx` connects it to the RPC and renders the footer, dialog, commands, and notices.
|
|
391
|
+
`src/notice.ts` decides which notices a CLI shows and which tabs it reopens.
|
|
387
392
|
- `src/forge.ts` defines the forge-neutral types, the `Forge` adapter interface, and
|
|
388
393
|
`ForgeTraits` with its optional `automatedReview` and `feedback` capabilities.
|
|
389
394
|
- `src/gitlab-feedback.ts` and `src/github-feedback.ts` read each forge's comments for its
|
package/dist/index.js
CHANGED
|
@@ -2423,6 +2423,7 @@ var shorten = (text, length) => text.length > length ? `${text.slice(0, length -
|
|
|
2423
2423
|
|
|
2424
2424
|
// packages/forgekeeper/src/server.ts
|
|
2425
2425
|
var SKIP_BRANCHES = new Set(["master", "main", "HEAD", "develop"]);
|
|
2426
|
+
var TARGET_KEY = "forgekeeper-session-target";
|
|
2426
2427
|
var GUIDANCE = [
|
|
2427
2428
|
"When the user establishes or changes the primary issue, PR, or MR for this session, call set_session_target with its full URL before starting that work.",
|
|
2428
2429
|
"When the user asks you to work on several issues, PRs, or MRs together, call set_session_target with all of their full URLs in targets. If the user describes them instead of linking them, find them first, then set them before starting that work. When they are all PRs/MRs for one established issue, pass it as issue_url.",
|
|
@@ -2430,7 +2431,8 @@ var GUIDANCE = [
|
|
|
2430
2431
|
'When the user drops some of several targets from the task, call set_session_target with operation "remove" and their URLs.',
|
|
2431
2432
|
"Background references, comparisons, and dependencies do not change the target. Follow-ups without a new primary target keep the current target.",
|
|
2432
2433
|
"Include issue_url only when its relationship to the target PRs/MRs is established. Never carry over the checked-out branch\u2019s issue to another target.",
|
|
2433
|
-
'When the user explicitly returns to work on the checked-out branch, call set_session_target with target "branch".'
|
|
2434
|
+
'When the user explicitly returns to work on the checked-out branch, call set_session_target with target "branch".',
|
|
2435
|
+
`The current session target is in the <context> entry "${TARGET_KEY}". The plugin updates it when it changes, so trust the latest value.`
|
|
2434
2436
|
];
|
|
2435
2437
|
function describeTargets(targets) {
|
|
2436
2438
|
if (targets.length === 0)
|
|
@@ -2459,6 +2461,7 @@ async function drain(tasks, limit) {
|
|
|
2459
2461
|
var json = (value) => JSON.parse(JSON.stringify(value));
|
|
2460
2462
|
async function setupServer(ctx, lookups, status2) {
|
|
2461
2463
|
const pending = new Map;
|
|
2464
|
+
const instructing = new Map;
|
|
2462
2465
|
const controller = new AbortController;
|
|
2463
2466
|
let closing = false;
|
|
2464
2467
|
const known = new Map;
|
|
@@ -2560,6 +2563,7 @@ async function setupServer(ctx, lookups, status2) {
|
|
|
2560
2563
|
const tools = await ctx.tool.transform((editor) => {
|
|
2561
2564
|
editor.add({
|
|
2562
2565
|
name: "set_session_target",
|
|
2566
|
+
options: { codemode: false },
|
|
2563
2567
|
description: 'Set the issues, PRs, or MRs this session works on, for its title and PR/MR status. Use target for one full URL, or "branch" to return to branch-based naming. Use targets for several full URLs. The default operation, "replace", sets exactly the given targets; "add" keeps the current targets, and "remove" drops the given ones. Call it after creating a PR/MR for the current task. Without issue_url, the related issue is inferred from the PR/MR source branch name. This only changes local session metadata.',
|
|
2564
2568
|
input: {
|
|
2565
2569
|
type: "object",
|
|
@@ -2603,6 +2607,7 @@ async function setupServer(ctx, lookups, status2) {
|
|
|
2603
2607
|
...state.prefix ? { prefix: state.prefix } : {}
|
|
2604
2608
|
});
|
|
2605
2609
|
watcher?.targetsChanged(sessionID, result.targets);
|
|
2610
|
+
await instruct(sessionID).catch(() => {});
|
|
2606
2611
|
await rpc.events.emit("targetChanged", { sessionID, ...targetOutput(result.targets) });
|
|
2607
2612
|
await update(sessionID);
|
|
2608
2613
|
});
|
|
@@ -2610,21 +2615,53 @@ async function setupServer(ctx, lookups, status2) {
|
|
|
2610
2615
|
}
|
|
2611
2616
|
});
|
|
2612
2617
|
});
|
|
2618
|
+
async function writeInstructions(sessionID) {
|
|
2619
|
+
if (closing)
|
|
2620
|
+
return [];
|
|
2621
|
+
const state = await stateFor(sessionID);
|
|
2622
|
+
const value = `Current session target: ${describeTargets(state.targets)}.`;
|
|
2623
|
+
const entries = await ctx.session.instructions.entry.list({ sessionID });
|
|
2624
|
+
if (entries.some((entry) => entry.key === TARGET_KEY && entry.value === value))
|
|
2625
|
+
return [];
|
|
2626
|
+
if (closing)
|
|
2627
|
+
return [];
|
|
2628
|
+
await ctx.session.instructions.entry.put({ sessionID, key: TARGET_KEY, value });
|
|
2629
|
+
return [value];
|
|
2630
|
+
}
|
|
2631
|
+
function instruct(sessionID) {
|
|
2632
|
+
const next = (instructing.get(sessionID) ?? Promise.resolve()).then(() => writeInstructions(sessionID));
|
|
2633
|
+
const settled = next.catch(() => {});
|
|
2634
|
+
instructing.set(sessionID, settled);
|
|
2635
|
+
settled.then(() => {
|
|
2636
|
+
if (instructing.get(sessionID) === settled)
|
|
2637
|
+
instructing.delete(sessionID);
|
|
2638
|
+
});
|
|
2639
|
+
return next;
|
|
2640
|
+
}
|
|
2613
2641
|
await ctx.session.hook("context", async (event) => {
|
|
2614
2642
|
try {
|
|
2615
2643
|
const session = await ctx.session.get({ sessionID: event.sessionID });
|
|
2616
2644
|
if (session.parentID || session.location.directory !== ctx.location.directory)
|
|
2617
2645
|
return;
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
`
|
|
2623
|
-
|
|
2646
|
+
event.system.push({ type: "text", text: GUIDANCE.join(`
|
|
2647
|
+
`) });
|
|
2648
|
+
const written = await instruct(event.sessionID);
|
|
2649
|
+
if (written.length)
|
|
2650
|
+
event.system.push({ type: "text", text: written.join(`
|
|
2651
|
+
`) });
|
|
2624
2652
|
} catch {}
|
|
2625
2653
|
});
|
|
2626
2654
|
(async () => {
|
|
2627
|
-
for await (const event of ctx.event.subscribe({
|
|
2655
|
+
for await (const event of ctx.event.subscribe({
|
|
2656
|
+
types: [
|
|
2657
|
+
"session.renamed",
|
|
2658
|
+
"session.moved",
|
|
2659
|
+
"session.execution.succeeded",
|
|
2660
|
+
"session.execution.failed",
|
|
2661
|
+
"vcs.branch.updated"
|
|
2662
|
+
],
|
|
2663
|
+
signal: controller.signal
|
|
2664
|
+
})) {
|
|
2628
2665
|
const { type } = event;
|
|
2629
2666
|
const sessionID = event.data?.sessionID;
|
|
2630
2667
|
if ((type === "session.renamed" || type === "session.execution.succeeded") && sessionID) {
|
package/dist/tui.js
CHANGED
|
@@ -1106,6 +1106,20 @@ function detailsMessage(snapshot) {
|
|
|
1106
1106
|
`);
|
|
1107
1107
|
}
|
|
1108
1108
|
|
|
1109
|
+
// packages/forgekeeper/src/notice.ts
|
|
1110
|
+
function showNotice(notice, host) {
|
|
1111
|
+
const { sessionID } = notice;
|
|
1112
|
+
if (sessionID && !host.hasShown(sessionID))
|
|
1113
|
+
return;
|
|
1114
|
+
if (sessionID && notice.variant === "info")
|
|
1115
|
+
host.openTab(sessionID);
|
|
1116
|
+
host.toast({
|
|
1117
|
+
...notice.title ? { title: notice.title } : {},
|
|
1118
|
+
message: notice.message,
|
|
1119
|
+
variant: notice.variant
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1109
1123
|
// packages/forgekeeper/src/rpc.ts
|
|
1110
1124
|
var urls = {
|
|
1111
1125
|
url: { type: "string" },
|
|
@@ -2066,28 +2080,18 @@ var tui_default = {
|
|
|
2066
2080
|
return null;
|
|
2067
2081
|
}
|
|
2068
2082
|
});
|
|
2069
|
-
const stops = [
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
return;
|
|
2082
|
-
context.ui.toast.show({
|
|
2083
|
-
...notice.title ? {
|
|
2084
|
-
title: notice.title
|
|
2085
|
-
} : {},
|
|
2086
|
-
message: notice.message,
|
|
2087
|
-
variant: notice.variant
|
|
2088
|
-
});
|
|
2089
|
-
})
|
|
2090
|
-
];
|
|
2083
|
+
const stops = [rpc.events.on("status", (event) => {
|
|
2084
|
+
const {
|
|
2085
|
+
directory,
|
|
2086
|
+
key: key2,
|
|
2087
|
+
snapshot: snapshot2
|
|
2088
|
+
} = event.data;
|
|
2089
|
+
client.receive(directory, key2, snapshot2);
|
|
2090
|
+
}), rpc.events.on("notice", (event) => showNotice(event.data, {
|
|
2091
|
+
hasShown: client.hasShown,
|
|
2092
|
+
openTab: (sessionID) => context.ui.tabs.open(sessionID),
|
|
2093
|
+
toast: context.ui.toast.show
|
|
2094
|
+
}))];
|
|
2091
2095
|
return () => {
|
|
2092
2096
|
clearInterval(heartbeat);
|
|
2093
2097
|
for (const stop of stops)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-forgekeeper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "OpenCode plugin that tracks each session's GitHub and GitLab issues and PRs/MRs: title prefixes, a PR/MR status footer, and review notifications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pedro Pombeiro",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
],
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@opencode/plugin": "^2.0
|
|
45
|
-
"@opentui/core": ">=0.
|
|
46
|
-
"@opentui/solid": ">=0.
|
|
44
|
+
"@opencode/plugin": "^2.2.0",
|
|
45
|
+
"@opentui/core": ">=0.7.0",
|
|
46
|
+
"@opentui/solid": ">=0.7.0",
|
|
47
47
|
"solid-js": ">=1.9.0"
|
|
48
48
|
}
|
|
49
49
|
}
|