Tool to send cross-session opencode messages, including as request-response pattern
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

convert CLI to use Gunshi with subCommands

rektide d0d4ead7 7042cbb6

+231 -209
+21 -194
cli.ts
··· 1 1 #!/usr/bin/env node 2 - 3 - import { readdir, readFile, access, writeFile, mkdir } from "node:fs/promises"; 4 - import { join } from "node:path"; 5 - import { homedir } from "node:os"; 6 - 7 - async function getOpenCodeStoragePath(): Promise<string> { 8 - const override = process.env.OPENCODE_TEST_HOME; 9 - if (override) { 10 - return join(override, "storage"); 11 - } 12 - return join(homedir(), ".local", "share", "opencode", "storage"); 13 - } 14 - 15 - interface Session { 16 - id: string; 17 - slug: string; 18 - projectID: string; 19 - directory: string; 20 - parentID?: string; 21 - title: string; 22 - version: string; 23 - time: { 24 - created: number; 25 - updated: number; 26 - compacting?: number; 27 - archived?: number; 28 - }; 29 - summary?: { 30 - additions: number; 31 - deletions: number; 32 - files: number; 33 - }; 34 - share?: { 35 - url: string; 36 - }; 37 - } 38 - 39 - function matchesPattern(sessionDir: string, pattern: string): boolean { 40 - const hasWildcard = pattern.includes("*"); 41 - 42 - if (!hasWildcard) { 43 - return sessionDir === pattern; 44 - } 45 - 46 - const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, "."); 47 - const regex = new RegExp(`^${regexPattern}$`); 48 - 49 - return regex.test(sessionDir); 50 - } 51 - 52 - async function listSessions(dir?: string): Promise<Session[]> { 53 - const storagePath = await getOpenCodeStoragePath(); 54 - const sessionPath = join(storagePath, "session"); 55 - 56 - try { 57 - await access(sessionPath); 58 - } catch { 59 - return []; 60 - } 61 - 62 - const projectDirs = await readdir(sessionPath, { withFileTypes: true }); 63 - const sessions: Session[] = []; 64 - 65 - for (const projectDir of projectDirs) { 66 - if (!projectDir.isDirectory()) continue; 67 - 68 - const projectPath = join(sessionPath, projectDir.name); 69 - const sessionFiles = await readdir(projectPath); 70 - 71 - for (const sessionFile of sessionFiles) { 72 - if (!sessionFile.endsWith(".json")) continue; 73 - 74 - const sessionFilePath = join(projectPath, sessionFile); 75 - const content = await readFile(sessionFilePath, "utf-8"); 76 - const session: Session = JSON.parse(content); 77 - 78 - if (dir && !matchesPattern(session.directory, dir)) continue; 79 - 80 - sessions.push(session); 81 - } 82 - } 2 + import { cli, define } from "gunshi"; 3 + import { list, zeroconf } from "./src/commands/index.ts"; 83 4 84 - return sessions.sort((a, b) => b.time.updated - a.time.updated); 85 - } 5 + const main = define({ 6 + name: "opencode-call-response", 7 + description: "Tool to send cross-session opencode messages", 8 + async run() { 9 + console.log("Usage: opencode-call-response <command>"); 10 + console.log(""); 11 + console.log("Commands:"); 12 + console.log(" list List all OpenCode sessions"); 13 + console.log(" zeroconf Enable mDNS zeroconf setting"); 14 + }, 15 + }); 86 16 87 - function displaySessions(sessions: Session[]) { 88 - if (sessions.length === 0) { 89 - return; 90 - } 91 - 92 - console.log("id\ttitle\tupdated\tdirectory"); 93 - 94 - for (const session of sessions) { 95 - const title = (session.title || "Untitled").replace(/\n/g, "\\n"); 96 - const updated = new Date(session.time.updated).toISOString(); 97 - const directory = session.directory; 98 - 99 - console.log(`${session.id}\t${title}\t${updated}\t${directory}`); 100 - } 101 - } 102 - 103 - async function enableMdns(): Promise<void> { 104 - const configPath1 = join(homedir(), ".config", "opencode", "opencode.json"); 105 - const configPath2 = join(homedir(), ".opencode", "opencode.json"); 106 - 107 - let config1Exists = false; 108 - let config2Exists = false; 109 - let config1HasMdns = false; 110 - let config2HasMdns = false; 111 - 112 - try { 113 - await access(configPath1); 114 - config1Exists = true; 115 - const content = await readFile(configPath1, "utf-8"); 116 - const config = JSON.parse(content); 117 - config1HasMdns = config.server?.mdns !== undefined; 118 - } catch {} 119 - 120 - try { 121 - await access(configPath2); 122 - config2Exists = true; 123 - const content = await readFile(configPath2, "utf-8"); 124 - const config = JSON.parse(content); 125 - config2HasMdns = config.server?.mdns !== undefined; 126 - } catch {} 127 - 128 - let targetFile: string; 129 - let existingConfig: any = {}; 130 - 131 - if (config1HasMdns) { 132 - targetFile = configPath1; 133 - const content = await readFile(configPath1, "utf-8"); 134 - existingConfig = JSON.parse(content); 135 - } else if (config2HasMdns) { 136 - targetFile = configPath2; 137 - const content = await readFile(configPath2, "utf-8"); 138 - existingConfig = JSON.parse(content); 139 - } else if (config1Exists && !config2Exists) { 140 - targetFile = configPath1; 141 - const content = await readFile(configPath1, "utf-8"); 142 - existingConfig = JSON.parse(content); 143 - } else if (config2Exists && !config1Exists) { 144 - targetFile = configPath2; 145 - const content = await readFile(configPath2, "utf-8"); 146 - existingConfig = JSON.parse(content); 147 - } else { 148 - targetFile = configPath1; 149 - if (config1Exists) { 150 - const content = await readFile(configPath1, "utf-8"); 151 - existingConfig = JSON.parse(content); 152 - } 153 - } 154 - 155 - existingConfig.server = existingConfig.server || {}; 156 - existingConfig.server.mdns = true; 157 - 158 - if (!existingConfig.$schema) { 159 - existingConfig.$schema = "https://opencode.ai/config.json"; 160 - } 161 - 162 - const targetDir = join(targetFile, ".."); 163 - await mkdir(targetDir, { recursive: true }); 164 - await writeFile(targetFile, JSON.stringify(existingConfig, null, 2)); 165 - console.log(`Enabled mdns in ${targetFile}`); 166 - } 167 - 168 - const command = process.argv[2]; 169 - const options: { dir?: string } = {}; 170 - 171 - for (let i = 3; i < process.argv.length; i++) { 172 - if (process.argv[i] === "-d" || process.argv[i] === "--dir") { 173 - options.dir = process.argv[++i]; 174 - } 175 - } 176 - 177 - async function main() { 178 - if (command === "list") { 179 - const dir = options.dir; 180 - const sessions = await listSessions(dir); 181 - displaySessions(sessions); 182 - } else if (command === "zeroconf") { 183 - await enableMdns(); 184 - } else { 185 - console.log("Usage: node cli.ts <command>"); 186 - console.log(""); 187 - console.log("Commands:"); 188 - console.log(" list List all sessions"); 189 - console.log(" zeroconf Enable mDNS zeroconf setting in OpenCode config"); 190 - console.log(""); 191 - console.log("List options:"); 192 - console.log(" -d, --dir Filter sessions by directory pattern"); 193 - process.exit(1); 194 - } 195 - } 196 - 197 - main(); 17 + await cli(process.argv.slice(2), main, { 18 + name: "opencode-call-response", 19 + version: "1.0.0", 20 + subCommands: { 21 + list, 22 + zeroconf, 23 + }, 24 + });
+2
src/commands/index.ts
··· 1 + export { list } from "./list.ts"; 2 + export { zeroconf } from "./zeroconf.ts";
+120
src/commands/list.ts
··· 1 + import { readdir, readFile, access } from "node:fs/promises"; 2 + import { join } from "node:path"; 3 + import { homedir } from "node:os"; 4 + import { define } from "gunshi"; 5 + 6 + interface Session { 7 + id: string; 8 + slug: string; 9 + projectID: string; 10 + directory: string; 11 + parentID?: string; 12 + title: string; 13 + version: string; 14 + time: { 15 + created: number; 16 + updated: number; 17 + compacting?: number; 18 + archived?: number; 19 + }; 20 + summary?: { 21 + additions: number; 22 + deletions: number; 23 + files: number; 24 + }; 25 + share?: { 26 + url: string; 27 + }; 28 + } 29 + 30 + interface ListOptions { 31 + dir?: string; 32 + } 33 + 34 + async function getOpenCodeStoragePath(): Promise<string> { 35 + const override = process.env.OPENCODE_TEST_HOME; 36 + if (override) { 37 + return join(override, "storage"); 38 + } 39 + return join(homedir(), ".local", "share", "opencode", "storage"); 40 + } 41 + 42 + function matchesPattern(sessionDir: string, pattern: string): boolean { 43 + const hasWildcard = pattern.includes("*"); 44 + 45 + if (!hasWildcard) { 46 + return sessionDir === pattern; 47 + } 48 + 49 + const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, "."); 50 + const regex = new RegExp(`^${regexPattern}$`); 51 + 52 + return regex.test(sessionDir); 53 + } 54 + 55 + async function listSessions(dir?: string): Promise<Session[]> { 56 + const storagePath = await getOpenCodeStoragePath(); 57 + const sessionPath = join(storagePath, "session"); 58 + 59 + try { 60 + await access(sessionPath); 61 + } catch { 62 + return []; 63 + } 64 + 65 + const projectDirs = await readdir(sessionPath, { withFileTypes: true }); 66 + const sessions: Session[] = []; 67 + 68 + for (const projectDir of projectDirs) { 69 + if (!projectDir.isDirectory()) continue; 70 + 71 + const projectPath = join(sessionPath, projectDir.name); 72 + const sessionFiles = await readdir(projectPath); 73 + 74 + for (const sessionFile of sessionFiles) { 75 + if (!sessionFile.endsWith(".json")) continue; 76 + 77 + const sessionFilePath = join(projectPath, sessionFile); 78 + const content = await readFile(sessionFilePath, "utf-8"); 79 + const session: Session = JSON.parse(content); 80 + 81 + if (dir && !matchesPattern(session.directory, dir)) continue; 82 + 83 + sessions.push(session); 84 + } 85 + } 86 + 87 + return sessions.sort((a, b) => b.time.updated - a.time.updated); 88 + } 89 + 90 + function displaySessions(sessions: Session[]) { 91 + if (sessions.length === 0) { 92 + return; 93 + } 94 + 95 + console.log("id\ttitle\tupdated\tdirectory"); 96 + 97 + for (const session of sessions) { 98 + const title = (session.title || "Untitled").replace(/\n/g, "\\n"); 99 + const updated = new Date(session.time.updated).toISOString(); 100 + const directory = session.directory; 101 + 102 + console.log(`${session.id}\t${title}\t${updated}\t${directory}`); 103 + } 104 + } 105 + 106 + export const list = define<ListOptions>({ 107 + name: "list", 108 + description: "List all OpenCode sessions", 109 + options: { 110 + dir: { 111 + type: "string", 112 + alias: "d", 113 + description: "Filter sessions by directory pattern", 114 + }, 115 + }, 116 + async run({ dir }) { 117 + const sessions = await listSessions(dir); 118 + displaySessions(sessions); 119 + }, 120 + });
+73
src/commands/zeroconf.ts
··· 1 + import { readFile, access, writeFile, mkdir } from "node:fs/promises"; 2 + import { join } from "node:path"; 3 + import { homedir } from "node:os"; 4 + import { define } from "gunshi"; 5 + 6 + export const zeroconf = define({ 7 + name: "zeroconf", 8 + description: "Enable mDNS zeroconf setting in OpenCode config", 9 + async run() { 10 + const configPath1 = join(homedir(), ".config", "opencode", "opencode.json"); 11 + const configPath2 = join(homedir(), ".opencode", "opencode.json"); 12 + 13 + let config1Exists = false; 14 + let config2Exists = false; 15 + let config1HasMdns = false; 16 + let config2HasMdns = false; 17 + 18 + try { 19 + await access(configPath1); 20 + config1Exists = true; 21 + const content = await readFile(configPath1, "utf-8"); 22 + const config = JSON.parse(content); 23 + config1HasMdns = config.server?.mdns !== undefined; 24 + } catch {} 25 + 26 + try { 27 + await access(configPath2); 28 + config2Exists = true; 29 + const content = await readFile(configPath2, "utf-8"); 30 + const config = JSON.parse(content); 31 + config2HasMdns = config.server?.mdns !== undefined; 32 + } catch {} 33 + 34 + let targetFile: string; 35 + let existingConfig: any = {}; 36 + 37 + if (config1HasMdns) { 38 + targetFile = configPath1; 39 + const content = await readFile(configPath1, "utf-8"); 40 + existingConfig = JSON.parse(content); 41 + } else if (config2HasMdns) { 42 + targetFile = configPath2; 43 + const content = await readFile(configPath2, "utf-8"); 44 + existingConfig = JSON.parse(content); 45 + } else if (config1Exists && !config2Exists) { 46 + targetFile = configPath1; 47 + const content = await readFile(configPath1, "utf-8"); 48 + existingConfig = JSON.parse(content); 49 + } else if (config2Exists && !config1Exists) { 50 + targetFile = configPath2; 51 + const content = await readFile(configPath2, "utf-8"); 52 + existingConfig = JSON.parse(content); 53 + } else { 54 + targetFile = configPath1; 55 + if (config1Exists) { 56 + const content = await readFile(configPath1, "utf-8"); 57 + existingConfig = JSON.parse(content); 58 + } 59 + } 60 + 61 + existingConfig.server = existingConfig.server || {}; 62 + existingConfig.server.mdns = true; 63 + 64 + if (!existingConfig.$schema) { 65 + existingConfig.$schema = "https://opencode.ai/config.json"; 66 + } 67 + 68 + const targetDir = join(targetFile, ".."); 69 + await mkdir(targetDir, { recursive: true }); 70 + await writeFile(targetFile, JSON.stringify(existingConfig, null, 2)); 71 + console.log(`Enabled mdns in ${targetFile}`); 72 + }, 73 + });
+2 -2
src/sensor/cache.ts
··· 1 - import { Instance, Sensor } from "./trait.js"; 2 - import { mergeGenerators } from "../util/generator.js"; 1 + import { Instance, Sensor } from "./trait.ts"; 2 + import { mergeGenerators } from "../util/generator.ts"; 3 3 4 4 export interface CacheOptions { 5 5 sensors: Iterable<Sensor>;
+4 -4
src/sensor/index.ts
··· 1 - export { Instance, Sensor } from "./trait.js"; 2 - export { MdnsSensor } from "./mdns.js"; 3 - export { ProcSensor } from "./proc.js"; 4 - export { CacheSensor, type CacheOptions } from "./cache.js"; 1 + export { Instance, Sensor } from "./trait.ts"; 2 + export { MdnsSensor } from "./mdns.ts"; 3 + export { ProcSensor } from "./proc.ts"; 4 + export { CacheSensor, type CacheOptions } from "./cache.ts";
+1 -1
src/sensor/mdns.ts
··· 1 1 import { Browser } from "bonjour-service"; 2 - import { Instance, Sensor } from "./trait.js"; 2 + import { Instance, Sensor } from "./trait.ts"; 3 3 4 4 export class MdnsSensor implements Sensor { 5 5 private browser?: Browser;
+1 -1
src/sensor/proc.ts
··· 1 1 import { readdir, readFile } from "node:fs/promises"; 2 2 import { join } from "node:path"; 3 - import { Instance, Sensor } from "./trait.js"; 3 + import { Instance, Sensor } from "./trait.ts"; 4 4 5 5 interface ProcInfo { 6 6 pid: number;
+2 -2
src/session/active.ts
··· 1 - import { Instance } from "../sensor/trait.js"; 2 - import { Session } from "./session.js"; 1 + import { Instance } from "../sensor/trait.ts"; 2 + import { Session } from "./session.ts"; 3 3 4 4 export type ActiveSession = Session & { 5 5 port: number;
+1 -1
src/session/filter.ts
··· 1 - import { Session } from "./session.js"; 1 + import { Session } from "./session.ts"; 2 2 3 3 export type SessionPredicate = (session: Session) => boolean; 4 4
+3 -3
src/session/index.ts
··· 1 - export { Session } from "./session.js"; 2 - export { ActiveSession, getActiveSessions } from "./active.js"; 3 - export { SessionFilter, filterSessions } from "./filter.js"; 1 + export { Session } from "./session.ts"; 2 + export { ActiveSession, getActiveSessions } from "./active.ts"; 3 + export { SessionFilter, filterSessions } from "./filter.ts";
+1 -1
src/util/index.ts
··· 1 - export { mergeGenerators } from "./generator.js"; 1 + export { mergeGenerators } from "./generator.ts";