Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions enchiridion-ts/src/cli.run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,40 @@ test(
}
},
);

// save-session: OPENCODE_SESSION_ID visible to run() via process.env (#399)
//
// The wiki-enchiridion OpenCode plugin runs the bundle in-process — the
// session-tracker's shell.env hook never fires for it. The plugin therefore
// injects context.sessionID into process.env.OPENCODE_SESSION_ID before
// calling run(). This test simulates the result of that injection by setting
// OPENCODE_SESSION_ID directly in process.env, then calling run(['save-session',
// ...]). We assert that the command moves past the "neither ID is set" check
// (i.e., it reads the env var) before failing on the tracker state check,
// which is the expected failure when no .opencode/wiki-knowledge/sessions/
// directory exists in cwd's ancestor chain.
test(
"run(['save-session']): reads OPENCODE_SESSION_ID from process.env (not 'neither ID' error)",
{ skip: skipReason },
async () => {
const prevSessionID = process.env.OPENCODE_SESSION_ID;
process.env.OPENCODE_SESSION_ID = "test-opencode-session-id";
try {
const result = await run(["save-session", "--slug", "test-session"]);
// Must not succeed (no real OpenCode session), but the failure must NOT be
// the "neither $CLAUDE_CODE_SESSION_ID nor $OPENCODE_SESSION_ID" error —
// that error means the env var was invisible, i.e. the bug is present.
assert.notEqual(result.exitCode, 0);
assert.ok(
!result.stderr.includes("Neither $CLAUDE_CODE_SESSION_ID"),
`Expected OPENCODE_SESSION_ID to be read; got: ${result.stderr.trim()}`,
);
// The expected error is about the tracker state (state not located), not
// about the ID being absent.
assert.match(result.stderr, /OPENCODE_SESSION_ID|session-tracker/);
} finally {
if (prevSessionID === undefined) delete process.env.OPENCODE_SESSION_ID;
else process.env.OPENCODE_SESSION_ID = prevSessionID;
}
},
);
40 changes: 33 additions & 7 deletions wiki-plugin/wiring/opencode/plugins/wiki-enchiridion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
* `@opencode-ai/plugin` as a runtime dependency — this plugin imports `tool`
* as a value (not just `type Plugin`), and OpenCode runs `bun install` on the
* config directory's package.json at startup.
*
* Session-id propagation (#399): OpenCode's `shell.env` hook (session-tracker
* plugin) injects `OPENCODE_SESSION_ID` into every *shell* command's
* environment, but this tool runs the enchiridion bundle in-process, so the
* hook never fires for it. The tool therefore injects `context.sessionID`
* directly into `process.env.OPENCODE_SESSION_ID` before the `run()` call and
* restores the prior value in a `finally`, making `save-session` and any other
* subcommand that reads the session id work correctly.
*/
import { readFileSync } from "node:fs"
import { dirname, join } from "node:path"
Expand Down Expand Up @@ -78,15 +86,33 @@ export const WikiEnchiridion: Plugin = async ({ directory }) => {
const bundle = await resolveBundle(dir)
const mod = await import(bundle) // CJS → default-interop namespace
const { run } = mod as { run: (argv: string[]) => Promise<unknown> }
const result = (await run(input.args)) as {
stdout: string
stderr: string
exitCode: number
// Propagate the session id into the in-process environment so that
// `save-session` (and any other subcommand that reads
// OPENCODE_SESSION_ID) can find it. The session-tracker plugin's
// shell.env hook covers *shell* commands; this tool runs the bundle
// in-process and must bridge the gap itself. Restored in a finally so
// a concurrent tool call that set a different value is unaffected.
const prevSessionID = process.env.OPENCODE_SESSION_ID
if (context.sessionID) {
process.env.OPENCODE_SESSION_ID = context.sessionID
}
if (result.exitCode !== 0) {
return `enchiridion exited ${result.exitCode}\n${result.stderr.trim()}`
try {
const result = (await run(input.args)) as {
stdout: string
stderr: string
exitCode: number
}
if (result.exitCode !== 0) {
return `enchiridion exited ${result.exitCode}\n${result.stderr.trim()}`
}
return result.stdout
} finally {
if (prevSessionID === undefined) {
delete process.env.OPENCODE_SESSION_ID
} else {
process.env.OPENCODE_SESSION_ID = prevSessionID
}
}
return result.stdout
},
}),
},
Expand Down
Loading