v0.2.0: add message operation, role filter, showTools, full session IDs
- Add message operation to retrieve a single message by ID (cleaner output, higher default maxLength of 8000) - Add role filter to messages operation (user/assistant/system) - Hide tool-call parts by default in messages/message output; showTools:true to include them - Show full session IDs instead of truncating to 12 chars - Make per-message maxLength configurable (default 2000 for messages, 8000 for message) - Increase search snippet length from 300 to 500 chars - Add local development & testing docs to AGENTS.md with symlink instructions - Bump version from 0.1.0 to 0.2.0
This commit is contained in:
@@ -6,7 +6,7 @@ export const formatSessionList = (rows: Record<string, unknown>[]): string => {
|
||||
lines.push("|----|-------|---------|----------|");
|
||||
|
||||
for (const row of rows) {
|
||||
const id = `${String(row.id ?? "").slice(0, 12)}...`;
|
||||
const id = String(row.id ?? "");
|
||||
const title = String(row.title ?? "untitled").slice(0, 40);
|
||||
const updated = String(row.updated ?? "");
|
||||
const msgs = String(row.msgs ?? "0");
|
||||
@@ -22,9 +22,14 @@ export const formatSessionList = (rows: Record<string, unknown>[]): string => {
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
export const formatMessageList = (rows: Record<string, unknown>[]): string => {
|
||||
export const formatMessageList = (
|
||||
rows: Record<string, unknown>[],
|
||||
options?: { maxLength?: number },
|
||||
): string => {
|
||||
if (rows.length === 0) return "No messages found.";
|
||||
|
||||
const maxLen = options?.maxLength ?? 2000;
|
||||
|
||||
const lines: string[] = ["# Conversation\n"];
|
||||
|
||||
for (const row of rows) {
|
||||
@@ -35,9 +40,34 @@ export const formatMessageList = (rows: Record<string, unknown>[]): string => {
|
||||
const header = `${icon} **${role}** _${time}_`;
|
||||
|
||||
lines.push(header);
|
||||
lines.push(text.slice(0, 2000));
|
||||
const truncated = text.length > maxLen;
|
||||
lines.push(
|
||||
truncated
|
||||
? text.slice(0, maxLen) +
|
||||
`\n... (${text.length} chars total, use maxLength or message tool for full content)`
|
||||
: text,
|
||||
);
|
||||
lines.push("---");
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
export const formatSingleMessage = (
|
||||
row: Record<string, unknown>,
|
||||
options?: { maxLength?: number },
|
||||
): string => {
|
||||
const maxLen = options?.maxLength ?? 8000;
|
||||
const role = String(row.role ?? "unknown");
|
||||
const time = String(row.time ?? "");
|
||||
const text = String(row.text ?? "");
|
||||
const icon = role === "user" ? "👤" : role === "assistant" ? "🤖" : "📝";
|
||||
|
||||
const truncated = text.length > maxLen;
|
||||
const displayText = truncated
|
||||
? text.slice(0, maxLen) +
|
||||
`\n... (${text.length} chars total, increase maxLength for full content)`
|
||||
: text;
|
||||
|
||||
return [`${icon} **${role}** _${time}_\n`, displayText].join("\n");
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export const searchConversations = (searchTerm: string, limit: number): string =
|
||||
COALESCE(s.title, 'untitled') AS title,
|
||||
json_extract(m.data, '$.role') AS role,
|
||||
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||
substr(json_extract(p.data, '$.text'), 1, 300) AS snippet
|
||||
substr(json_extract(p.data, '$.text'), 1, 500) AS snippet
|
||||
FROM part p
|
||||
JOIN message m ON m.id = p.message_id
|
||||
JOIN session s ON s.id = m.session_id
|
||||
@@ -38,14 +38,14 @@ export const searchConversations = (searchTerm: string, limit: number): string =
|
||||
const lines: string[] = [`# Search: "${searchTerm}"\n`];
|
||||
|
||||
for (const row of rows) {
|
||||
const sessionId = String(row.session_id ?? "").slice(0, 16);
|
||||
const sessionId = String(row.session_id ?? "");
|
||||
const title = String(row.title ?? "untitled");
|
||||
const time = String(row.time ?? "");
|
||||
const role = String(row.role ?? "unknown");
|
||||
const snippet = String(row.snippet ?? "");
|
||||
|
||||
lines.push(`### ${title} (${time})`);
|
||||
lines.push(`- Session: \`${sessionId}...\``);
|
||||
lines.push(`- Session: \`${sessionId}\``);
|
||||
lines.push(`- Role: ${role}`);
|
||||
lines.push(`- Snippet: ${snippet}...`);
|
||||
lines.push("");
|
||||
|
||||
73
src/tools.ts
73
src/tools.ts
@@ -1,7 +1,7 @@
|
||||
import type { PluginInput, ToolDefinition } from "@opencode-ai/plugin";
|
||||
import { tool } from "@opencode-ai/plugin";
|
||||
import type { ContextTracker } from "./context/tracker.js";
|
||||
import { formatMessageList, formatSessionList } from "./history/format.js";
|
||||
import { formatMessageList, formatSessionList, formatSingleMessage } from "./history/format.js";
|
||||
import { runQuery } from "./history/queries.js";
|
||||
import { searchConversations } from "./history/search.js";
|
||||
|
||||
@@ -19,7 +19,8 @@ Call \`memory({tool: "<name>", args: {...}})\` to use one.
|
||||
|------|-------------|----------|
|
||||
| summary | Count of projects, sessions, messages, todos | — |
|
||||
| sessions | List recent sessions, optionally filtered by project | limit, projectPath |
|
||||
| messages | Read messages from a session as formatted conversation | sessionId, limit |
|
||||
| messages | Read messages from a session as formatted conversation | sessionId, limit, role, showTools, maxLength |
|
||||
| message | Read a single message by ID (cleaner output, no tool-call noise) | messageId, maxLength |
|
||||
| search | Text search across all conversations | query, limit |
|
||||
| compactions | List/read compaction checkpoints for a session | sessionId, read (1-based index) |
|
||||
| context | Current context window usage (% , tokens, status) | — |
|
||||
@@ -29,6 +30,8 @@ Call \`memory({tool: "<name>", args: {...}})\` to use one.
|
||||
Examples:
|
||||
- \`memory({tool: "search", args: {query: "safetensors"}})\`
|
||||
- \`memory({tool: "compactions", args: {sessionId: "ses_abc", read: 1}})\`
|
||||
- \`memory({tool: "messages", args: {sessionId: "ses_abc", role: "assistant"}})\`
|
||||
- \`memory({tool: "message", args: {messageId: "msg_abc"}})\`
|
||||
- \`memory({tool: "help", args: {tool: "search"}})\``;
|
||||
|
||||
const TOOL_HELP: Record<string, string> = {
|
||||
@@ -36,7 +39,10 @@ const TOOL_HELP: Record<string, string> = {
|
||||
sessions: `**sessions** — List recent sessions with titles, update times, message counts.
|
||||
Args: limit (number, default 10), projectPath (string, optional filter by worktree path).`,
|
||||
messages: `**messages** — Read messages from a specific session as formatted conversation.
|
||||
Args: sessionId (string, required), limit (number, default 50).`,
|
||||
By default, tool-call parts (Read, Write, Bash, etc.) are hidden to reduce noise. Set showTools: true to include them.
|
||||
Args: sessionId (string, required), limit (number, default 50), role (string, optional filter: "user", "assistant", "system"), showTools (boolean, default false), maxLength (number, default 2000 per message).`,
|
||||
message: `**message** — Read a single message by ID. Shows the message with tool calls hidden by default, formatted cleanly.
|
||||
Args: messageId (string, required), showTools (boolean, default false), maxLength (number, default 8000).`,
|
||||
search: `**search** — Text search across all conversations. Returns matching snippets with session references.
|
||||
Args: query (string, required), limit (number, default 10).`,
|
||||
compactions: `**compactions** — List compaction checkpoints for a session. Compactions are summaries created when context was freed. Use 'read' to get the full summary text — these act as checkpoints showing what was important at that point.
|
||||
@@ -133,22 +139,71 @@ const handlers: Record<string, MemoryHandler> = {
|
||||
messages(args) {
|
||||
const sessionId = args.sessionId as string;
|
||||
const limit = (args.limit as number) ?? 50;
|
||||
const roleFilter = args.role as string | undefined;
|
||||
const showTools = (args.showTools as boolean) ?? false;
|
||||
const maxLength = args.maxLength as number | undefined;
|
||||
if (!sessionId) return "sessionId is required.";
|
||||
|
||||
try {
|
||||
type MessageRow = { role: string; time: string; text: string };
|
||||
let rows: MessageRow[];
|
||||
|
||||
if (roleFilter) {
|
||||
rows = runQuery<MessageRow>(
|
||||
`SELECT json_extract(m.data, '$.role') AS role,
|
||||
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||
GROUP_CONCAT(json_extract(p.data, '$.text'), char(10)) AS text
|
||||
FROM message m LEFT JOIN part p ON p.message_id = m.id ${showTools ? "" : "AND json_extract(p.data, '$.type') = 'text'"}
|
||||
WHERE m.session_id = $sessionId AND json_extract(m.data, '$.role') = $role
|
||||
GROUP BY m.id ORDER BY m.time_created ASC LIMIT $limit`,
|
||||
{ $sessionId: sessionId, $role: roleFilter, $limit: limit },
|
||||
);
|
||||
} else {
|
||||
rows = runQuery<MessageRow>(
|
||||
`SELECT json_extract(m.data, '$.role') AS role,
|
||||
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||
GROUP_CONCAT(json_extract(p.data, '$.text'), char(10)) AS text
|
||||
FROM message m LEFT JOIN part p ON p.message_id = m.id ${showTools ? "" : "AND json_extract(p.data, '$.type') = 'text'"}
|
||||
WHERE m.session_id = $sessionId
|
||||
GROUP BY m.id ORDER BY m.time_created ASC LIMIT $limit`,
|
||||
{ $sessionId: sessionId, $limit: limit },
|
||||
);
|
||||
}
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
const filterDesc = roleFilter ? ` with role="${roleFilter}"` : "";
|
||||
return `No messages found for session ${sessionId}${filterDesc}.`;
|
||||
}
|
||||
return formatMessageList(rows, { maxLength });
|
||||
} catch (err) {
|
||||
return `Failed to query messages: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
|
||||
message(args) {
|
||||
const messageId = args.messageId as string;
|
||||
const showTools = (args.showTools as boolean) ?? false;
|
||||
const maxLength = args.maxLength as number | undefined;
|
||||
if (!messageId) return "messageId is required.";
|
||||
|
||||
try {
|
||||
type MessageRow = { role: string; time: string; text: string };
|
||||
const partFilter = showTools ? "" : "AND json_extract(p.data, '$.type') = 'text'";
|
||||
|
||||
const rows = runQuery<MessageRow>(
|
||||
`SELECT json_extract(m.data, '$.role') AS role,
|
||||
datetime(m.time_created/1000, 'unixepoch', 'localtime') AS time,
|
||||
GROUP_CONCAT(json_extract(p.data, '$.text'), char(10)) AS text
|
||||
FROM message m LEFT JOIN part p ON p.message_id = m.id AND json_extract(p.data, '$.type') = 'text'
|
||||
WHERE m.session_id = $sessionId GROUP BY m.id ORDER BY m.time_created ASC LIMIT $limit`,
|
||||
{ $sessionId: sessionId, $limit: limit },
|
||||
FROM message m LEFT JOIN part p ON p.message_id = m.id ${partFilter}
|
||||
WHERE m.id = $messageId
|
||||
GROUP BY m.id`,
|
||||
{ $messageId: messageId },
|
||||
);
|
||||
if (!rows || rows.length === 0) return `No messages found for session ${sessionId}.`;
|
||||
return formatMessageList(rows);
|
||||
|
||||
if (!rows || rows.length === 0) return `No message found with ID ${messageId}.`;
|
||||
return formatSingleMessage(rows[0], { maxLength });
|
||||
} catch (err) {
|
||||
return `Failed to query messages: ${err instanceof Error ? err.message : String(err)}`;
|
||||
return `Failed to query message: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user