@clipy/mcp
Advanced tools
| /** | ||
| * Range slicing for compiled AREC markdown (`recording.md`). | ||
| * | ||
| * The compiler emits a header block (title, metadata, visual gaps, frames) | ||
| * followed by `## Transcript` and then one `### MM:SS` heading every ~150s, | ||
| * with `[MM:SS] spoken text` lines under it. Slicing at heading granularity | ||
| * keeps a section's interleaved frame images attached to the words they | ||
| * illustrate, which per-line filtering would strip. | ||
| * | ||
| * Kept dependency-free and side-effect-free so it can be unit tested offline. | ||
| */ | ||
| const HEADING = /^###\s+(\d{1,2}:\d{2}(?::\d{2})?)\s*$/; | ||
| /** `MM:SS` or `H:MM:SS` → milliseconds. Returns null if it isn't a timestamp. */ | ||
| export function parseTimestamp(value) { | ||
| const parts = value.trim().split(":"); | ||
| if (parts.length < 2 || parts.length > 3) | ||
| return null; | ||
| const nums = parts.map((p) => Number(p)); | ||
| if (nums.some((n) => !Number.isFinite(n) || n < 0)) | ||
| return null; | ||
| const [h, m, s] = parts.length === 3 ? nums : [0, nums[0], nums[1]]; | ||
| return (h * 3600 + m * 60 + s) * 1000; | ||
| } | ||
| /** Splits compiled AREC markdown into its header block and timestamped sections. */ | ||
| export function parseArecMarkdown(markdown) { | ||
| const lines = markdown.split("\n"); | ||
| const headerLines = []; | ||
| const sections = []; | ||
| let current = null; | ||
| for (const line of lines) { | ||
| const m = HEADING.exec(line); | ||
| const ms = m ? parseTimestamp(m[1]) : null; | ||
| if (ms !== null) { | ||
| if (current) { | ||
| sections.push({ startMs: current.startMs, endMs: ms, text: current.lines.join("\n").trim() }); | ||
| } | ||
| current = { startMs: ms, lines: [line] }; | ||
| continue; | ||
| } | ||
| if (current) | ||
| current.lines.push(line); | ||
| else | ||
| headerLines.push(line); | ||
| } | ||
| if (current) { | ||
| sections.push({ | ||
| startMs: current.startMs, | ||
| endMs: Number.POSITIVE_INFINITY, | ||
| text: current.lines.join("\n").trim(), | ||
| }); | ||
| } | ||
| return { header: headerLines.join("\n").trim(), sections }; | ||
| } | ||
| /** Default ceiling on a full (unranged) read, in characters. */ | ||
| export const MAX_MARKDOWN_CHARS = 60_000; | ||
| /** | ||
| * Returns the header block plus every section overlapping [startMs, endMs). | ||
| * An open-ended range (either bound omitted) extends to the document edge. | ||
| */ | ||
| export function sliceArecMarkdown(markdown, range = {}, maxChars = MAX_MARKDOWN_CHARS) { | ||
| const { header, sections } = parseArecMarkdown(markdown); | ||
| const hasRange = range.startMs !== undefined || range.endMs !== undefined; | ||
| if (!hasRange) { | ||
| if (markdown.length <= maxChars) { | ||
| return { | ||
| markdown, | ||
| sectionsReturned: sections.length, | ||
| sectionsTotal: sections.length, | ||
| truncated: false, | ||
| }; | ||
| } | ||
| return { | ||
| markdown: `${markdown.slice(0, maxChars)}\n\n[... truncated at ${maxChars} characters. This recording is long — re-read a portion with startMs/endMs (the document is sectioned every ~150s, headings are the [MM:SS] markers).]\n`, | ||
| sectionsReturned: sections.length, | ||
| sectionsTotal: sections.length, | ||
| truncated: true, | ||
| note: "Truncated. Use startMs/endMs to read a specific span.", | ||
| }; | ||
| } | ||
| const from = range.startMs ?? 0; | ||
| const to = range.endMs ?? Number.POSITIVE_INFINITY; | ||
| const kept = sections.filter((s) => s.startMs < to && s.endMs > from); | ||
| const body = kept.length | ||
| ? kept.map((s) => s.text).join("\n\n") | ||
| : "_No transcript sections fall in that range._"; | ||
| let out = `${header}\n\n${body}\n`; | ||
| let truncated = false; | ||
| if (out.length > maxChars) { | ||
| out = `${out.slice(0, maxChars)}\n\n[... truncated at ${maxChars} characters. Narrow the range.]\n`; | ||
| truncated = true; | ||
| } | ||
| return { | ||
| markdown: out, | ||
| sectionsReturned: kept.length, | ||
| sectionsTotal: sections.length, | ||
| truncated, | ||
| }; | ||
| } |
+3
-2
| { | ||
| "name": "@clipy/mcp", | ||
| "version": "0.8.6", | ||
| "version": "0.9.0", | ||
| "mcpName": "online.clipy/mcp", | ||
@@ -35,3 +35,4 @@ "description": "Model Context Protocol (MCP) server for Clipy — give Claude, Cursor, and other AI agents read access to your screen recordings' transcripts, AI summaries, and key moments with frames.", | ||
| "prepublishOnly": "npm run build", | ||
| "start": "node dist/index.js" | ||
| "start": "node dist/index.js", | ||
| "test": "npm run build && node scripts/arec-range.test.mjs" | ||
| }, | ||
@@ -38,0 +39,0 @@ "dependencies": { |
+3
-0
@@ -114,2 +114,5 @@ # @clipy/mcp | ||
| | `abort_recording` | Discard the active session; nothing is uploaded. | | ||
| | `list_context_documents` | List the user's **context documents** — YouTube videos and local video files they imported with `clipy context import`, so agents can read them. A separate library from their own screen recordings. | | ||
| | `get_context_document` | One context document's metadata: source, duration, tags, the server's classification (video type, whether visual evidence is needed, planned moments), and which transcript/frames exist. Not the transcript itself. | | ||
| | `read_context_document` | Read a context document as compiled markdown — header, metadata, then the timestamped transcript with frame captions interleaved. Takes `startMs`/`endMs` so you can walk a two-hour video section by section instead of flooding your context. | | ||
| | `replace_transcript` | **Replace a recording's transcript** with text you author (needs the `ingest` scope). Fix a bad speech-to-text pass, translate, or enrich a silent agent capture; the summary regenerates automatically. Marked as agent-edited, never passed off as speech-to-text. | | ||
@@ -116,0 +119,0 @@ |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
150915
8.98%5
25%2687
11.4%1
-50%270
1.12%8
14.29%