Skip to content
Open
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
150 changes: 139 additions & 11 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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({
Expand All @@ -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: {
Expand All @@ -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) {
Expand All @@ -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,
},
})

Expand All @@ -405,7 +420,12 @@ EXAMPLES:
},
})

return `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 {
Expand Down Expand Up @@ -470,6 +490,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 },
Expand All @@ -479,7 +506,12 @@ EXAMPLES:
},
})

return `Forked session with ${args.agent || "build"} agent - history preserved (ID: ${forkedSession.data.id})`
const sanitizeFork = (str: string) => str.replace(/\n/g, " ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This sanitizeFork function is identical to sanitizeForPrompt defined earlier in the case "new" block (line 423). To improve maintainability and reduce redundancy, you could define this function once in a higher scope (e.g., before the switch statement) and reuse it in both places.

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 =
Expand All @@ -498,6 +530,102 @@ EXAMPLES:
}
},
}),

session_list: tool({
description: `List all OpenCode sessions with optional filtering.

Returns a list of available sessions with metadata including title and creation date.

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 | Created |
|------------|-------|---------|
| ses_abc123 | API Research | 2026-02-21 |
| ses_def456 | Auth Implementation | 2026-02-20 |`,

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
})
Comment on lines +577 to +582
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When a session lacks a time.created property, you're defaulting its creation timestamp to 0. This can lead to incorrect filtering, as these sessions will be treated as if they were created at the Unix epoch (1970-01-01). It would be more robust to exclude sessions that don't have a creation date when filtering by date.

              sessions = sessions.filter((s) => {
                if (!s.time?.created) {
                  return false;
                }
                const created = new Date(s.time.created).getTime();
                return created >= fromDate;
              })

}

if (args.to_date) {
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 <= toTimestamp
})
Comment on lines +589 to +594
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the from_date filter, you're defaulting to a timestamp of 0 for sessions without a creation date. This can cause incorrect filtering. These sessions should be excluded from the results when a date filter is applied.

              sessions = sessions.filter((s) => {
                if (!s.time?.created) {
                  return false;
                }
                const created = new Date(s.time.created).getTime();
                return created <= toTimestamp;
              })

}

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 sanitizeForMarkdown = (str: string) =>
str.replace(/\|/g, "\\|").replace(/\n/g, " ")
Comment on lines +611 to +612
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The sanitizeForMarkdown function is vulnerable to markdown injection. It currently only escapes the pipe character (|) and newlines, but fails to escape other critical markdown special characters such as brackets ([ and ]) or parentheses (( and )). Since session titles are user-controlled, an attacker could inject malicious markdown, potentially leading to XSS or phishing attacks if rendered in a web-based UI. Additionally, for better performance, this function should be defined once outside the map loop, rather than being redefined on every iteration.

Suggested change
const sanitizeForMarkdown = (str: string) =>
str.replace(/\|/g, "\\|").replace(/\n/g, " ")
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"
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}`
}
},
}),
},
}
}