From 6ffe837588606ff5ae1d60f162c6dfda6a9a9dd6 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 21 Feb 2026 15:37:59 -0600 Subject: [PATCH 1/3] feat: add name parameter to session tool for custom session titles --- index.ts | 48 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/index.ts b/index.ts index cabfe6c..522c56a 100644 --- a/index.ts +++ b/index.ts @@ -321,6 +321,11 @@ ${agentList} If omitted, continues with current agent. +NAME PARAMETER (optional): + +Custom name for new or forked sessions. Helps identify sessions in the session list. +Only used in 'new' and 'fork' modes. Ignored in 'message' and 'compact' modes. + EXAMPLES: # COLLABORATE: Multi-agent problem solving @@ -331,13 +336,14 @@ EXAMPLES: }) # Plan reviews, responds, can pass back to build - # HANDOFF: Clean phase transition + # HANDOFF: Clean phase transition with named session session({ mode: "new", agent: "researcher", + name: "API Research", text: "Research best practices for API design" }) - # Fresh session, no baggage from previous implementation work + # Fresh session titled "API Research", no baggage from previous work # COMPRESS: Long conversation with handoff session({ @@ -347,18 +353,20 @@ EXAMPLES: }) # Compacts history, adds handoff context, plan agent responds - # PARALLELIZE: Try multiple approaches + # PARALLELIZE: Named forked sessions for comparison session({ mode: "fork", - agent: "build", + agent: "plan", + name: "Redux Architecture", text: "Implement using Redux" }) session({ mode: "fork", - agent: "build", + agent: "plan", + name: "Context API Architecture", text: "Implement using Context API" }) - # Two independent sessions, compare results + # Two named sessions for easy identification, compare results `, args: { @@ -374,6 +382,12 @@ EXAMPLES: .describe( "Primary agent name (e.g., 'build', 'plan') for agent switching", ), + name: tool.schema + .string() + .optional() + .describe( + "Custom name for the session (used in 'new' and 'fork' modes)", + ), }, async execute(args, toolCtx) { @@ -388,11 +402,12 @@ EXAMPLES: case "new": // Create session via SDK for agent control + const sessionTitle = args.name || (args.agent + ? `Session via ${args.agent}` + : "New session") const newSession = await ctx.client.session.create({ body: { - title: args.agent - ? `Session via ${args.agent}` - : "New session", + title: sessionTitle, }, }) @@ -405,7 +420,9 @@ EXAMPLES: }, }) - return `New session created with ${args.agent || "build"} agent (ID: ${newSession.data.id})` + return args.name + ? `New session "${args.name}" created with ${args.agent || "build"} agent (ID: ${newSession.data.id})` + : `New session created with ${args.agent || "build"} agent (ID: ${newSession.data.id})` case "compact": try { @@ -470,6 +487,13 @@ EXAMPLES: body: {}, }) + if (args.name) { + await ctx.client.session.update({ + path: { id: forkedSession.data.id }, + body: { title: args.name }, + }) + } + // Send new message in forked session await ctx.client.session.prompt({ path: { id: forkedSession.data.id }, @@ -479,7 +503,9 @@ EXAMPLES: }, }) - return `Forked session with ${args.agent || "build"} agent - history preserved (ID: ${forkedSession.data.id})` + return args.name + ? `Forked session "${args.name}" with ${args.agent || "build"} agent - history preserved (ID: ${forkedSession.data.id})` + : `Forked session with ${args.agent || "build"} agent - history preserved (ID: ${forkedSession.data.id})` } } catch (error) { const message = From 3089f76b9726b4b64956e58dd80d4d0a87f5b975 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 21 Feb 2026 15:51:00 -0600 Subject: [PATCH 2/3] feat: add session_list tool with title support --- index.ts | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/index.ts b/index.ts index 522c56a..9f7b1a9 100644 --- a/index.ts +++ b/index.ts @@ -524,6 +524,98 @@ EXAMPLES: } }, }), + + session_list: tool({ + description: `List all OpenCode sessions with optional filtering. + +Returns a list of available sessions with metadata including title, message count, date range, and agents used. + +Arguments: +- limit (optional): Maximum number of sessions to return +- from_date (optional): Filter sessions from this date (ISO 8601 format) +- to_date (optional): Filter sessions until this date (ISO 8601 format) + +Example output: +| Session ID | Title | Messages | First | Last | Agents | +|------------|-------|----------|-------|------|--------| +| ses_abc123 | API Research | 45 | 2026-02-21 | 2026-02-21 | build, plan | +| ses_def456 | Auth Implementation | 12 | 2026-02-20 | 2026-02-20 | build |`, + + args: { + limit: tool.schema + .number() + .optional() + .describe("Maximum number of sessions to return"), + from_date: tool.schema + .string() + .optional() + .describe("Filter sessions from this date (ISO 8601 format)"), + to_date: tool.schema + .string() + .optional() + .describe("Filter sessions until this date (ISO 8601 format)"), + }, + + async execute(args) { + try { + const response = await ctx.client.session.list() + + if (!response.data) { + return "No sessions found." + } + + let sessions = response.data + + if (args.from_date) { + const fromDate = new Date(args.from_date).getTime() + sessions = sessions.filter((s) => { + const created = s.time?.created + ? new Date(s.time.created).getTime() + : 0 + return created >= fromDate + }) + } + + if (args.to_date) { + const toDate = new Date(args.to_date).getTime() + sessions = sessions.filter((s) => { + const created = s.time?.created + ? new Date(s.time.created).getTime() + : 0 + return created <= toDate + }) + } + + if (args.limit && args.limit > 0) { + sessions = sessions.slice(0, args.limit) + } + + if (sessions.length === 0) { + return "No sessions found matching criteria." + } + + const header = + "| Session ID | Title | Created |" + const separator = + "|------------|-------|---------|" + + const rows = sessions.map((s) => { + const id = s.id || "unknown" + const title = s.title || "Untitled" + const created = s.time?.created + ? new Date(s.time.created).toISOString().split("T")[0] + : "N/A" + return `| ${id} | ${title} | ${created} |` + }) + + return [header, separator, ...rows].join("\n") + } catch (error) { + const message = + error instanceof Error ? error.message : String(error) + return `Error listing sessions: ${message}` + } + }, + }), }, } } From 3971044aa685c1dd4613df85261174d71a7d88c3 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 21 Feb 2026 16:35:19 -0600 Subject: [PATCH 3/3] fix: address PR review feedback - Update session_list docs to match implementation output - Sanitize markdown table cells (| and newlines) to prevent injection - Sanitize session/agent names in return messages to prevent prompt injection - Fix to_date filtering to include full day (set to 23:59:59.999) --- index.ts | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/index.ts b/index.ts index 9f7b1a9..45e9f1a 100644 --- a/index.ts +++ b/index.ts @@ -420,9 +420,12 @@ EXAMPLES: }, }) - return args.name - ? `New session "${args.name}" created with ${args.agent || "build"} agent (ID: ${newSession.data.id})` - : `New session created with ${args.agent || "build"} agent (ID: ${newSession.data.id})` + const sanitizeForPrompt = (str: string) => str.replace(/\n/g, " ") + const safeName = args.name ? sanitizeForPrompt(args.name) : null + const safeAgent = sanitizeForPrompt(args.agent || "build") + return safeName + ? `New session "${safeName}" created with ${safeAgent} agent (ID: ${newSession.data.id})` + : `New session created with ${safeAgent} agent (ID: ${newSession.data.id})` case "compact": try { @@ -503,9 +506,12 @@ EXAMPLES: }, }) - return args.name - ? `Forked session "${args.name}" with ${args.agent || "build"} agent - history preserved (ID: ${forkedSession.data.id})` - : `Forked session with ${args.agent || "build"} agent - history preserved (ID: ${forkedSession.data.id})` + const sanitizeFork = (str: string) => str.replace(/\n/g, " ") + const forkSafeName = args.name ? sanitizeFork(args.name) : null + const forkSafeAgent = sanitizeFork(args.agent || "build") + return forkSafeName + ? `Forked session "${forkSafeName}" with ${forkSafeAgent} agent - history preserved (ID: ${forkedSession.data.id})` + : `Forked session with ${forkSafeAgent} agent - history preserved (ID: ${forkedSession.data.id})` } } catch (error) { const message = @@ -528,7 +534,7 @@ EXAMPLES: session_list: tool({ description: `List all OpenCode sessions with optional filtering. -Returns a list of available sessions with metadata including title, message count, date range, and agents used. +Returns a list of available sessions with metadata including title and creation date. Arguments: - limit (optional): Maximum number of sessions to return @@ -536,10 +542,10 @@ Arguments: - to_date (optional): Filter sessions until this date (ISO 8601 format) Example output: -| Session ID | Title | Messages | First | Last | Agents | -|------------|-------|----------|-------|------|--------| -| ses_abc123 | API Research | 45 | 2026-02-21 | 2026-02-21 | build, plan | -| ses_def456 | Auth Implementation | 12 | 2026-02-20 | 2026-02-20 | build |`, +| Session ID | Title | Created | +|------------|-------|---------| +| ses_abc123 | API Research | 2026-02-21 | +| ses_def456 | Auth Implementation | 2026-02-20 |`, args: { limit: tool.schema @@ -577,12 +583,14 @@ Example output: } if (args.to_date) { - const toDate = new Date(args.to_date).getTime() + const toDate = new Date(args.to_date) + toDate.setUTCHours(23, 59, 59, 999) + const toTimestamp = toDate.getTime() sessions = sessions.filter((s) => { const created = s.time?.created ? new Date(s.time.created).getTime() : 0 - return created <= toDate + return created <= toTimestamp }) } @@ -600,8 +608,10 @@ Example output: "|------------|-------|---------|" const rows = sessions.map((s) => { - const id = s.id || "unknown" - const title = s.title || "Untitled" + const sanitizeForMarkdown = (str: string) => + str.replace(/\|/g, "\\|").replace(/\n/g, " ") + const id = sanitizeForMarkdown(s.id || "unknown") + const title = sanitizeForMarkdown(s.title || "Untitled") const created = s.time?.created ? new Date(s.time.created).toISOString().split("T")[0] : "N/A"