screenpipe-mcp 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +57 -1
- package/manifest.json +5 -1
- package/package.json +1 -1
- package/src/index.ts +57 -1
- package/ui/search.html +559 -0
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ const SCREENPIPE_API = `http://localhost:${port}`;
|
|
|
71
71
|
// Initialize server
|
|
72
72
|
const server = new index_js_1.Server({
|
|
73
73
|
name: "screenpipe",
|
|
74
|
-
version: "0.
|
|
74
|
+
version: "0.5.0",
|
|
75
75
|
}, {
|
|
76
76
|
capabilities: {
|
|
77
77
|
tools: {},
|
|
@@ -469,6 +469,12 @@ const RESOURCES = [
|
|
|
469
469
|
description: "How to use screenpipe search effectively",
|
|
470
470
|
mimeType: "text/markdown",
|
|
471
471
|
},
|
|
472
|
+
{
|
|
473
|
+
uri: "ui://search",
|
|
474
|
+
name: "Search Dashboard",
|
|
475
|
+
description: "Interactive search UI for exploring screen recordings and audio transcriptions",
|
|
476
|
+
mimeType: "text/html",
|
|
477
|
+
},
|
|
472
478
|
];
|
|
473
479
|
// List resources handler
|
|
474
480
|
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
@@ -541,6 +547,56 @@ server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) =
|
|
|
541
547
|
},
|
|
542
548
|
],
|
|
543
549
|
};
|
|
550
|
+
case "ui://search": {
|
|
551
|
+
// MCP App UI - Interactive search dashboard
|
|
552
|
+
const uiHtmlPath = path.join(__dirname, "..", "ui", "search.html");
|
|
553
|
+
let htmlContent;
|
|
554
|
+
try {
|
|
555
|
+
htmlContent = fs.readFileSync(uiHtmlPath, "utf-8");
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
// Fallback: serve embedded minimal UI if file not found
|
|
559
|
+
htmlContent = `<!DOCTYPE html>
|
|
560
|
+
<html>
|
|
561
|
+
<head>
|
|
562
|
+
<style>
|
|
563
|
+
body { font-family: system-ui; background: #0a0a0a; color: #fff; padding: 20px; }
|
|
564
|
+
input { width: 100%; padding: 10px; margin-bottom: 10px; background: #1a1a1a; border: 1px solid #333; color: #fff; border-radius: 6px; }
|
|
565
|
+
button { padding: 10px 20px; background: #fff; color: #000; border: none; border-radius: 6px; cursor: pointer; }
|
|
566
|
+
#results { margin-top: 20px; }
|
|
567
|
+
.result { background: #1a1a1a; padding: 12px; margin: 8px 0; border-radius: 8px; border: 1px solid #333; }
|
|
568
|
+
</style>
|
|
569
|
+
</head>
|
|
570
|
+
<body>
|
|
571
|
+
<h2>screenpipe search</h2>
|
|
572
|
+
<input id="q" placeholder="search..." onkeydown="if(event.key==='Enter')search()"/>
|
|
573
|
+
<button onclick="search()">search</button>
|
|
574
|
+
<div id="results"></div>
|
|
575
|
+
<script>
|
|
576
|
+
function search() {
|
|
577
|
+
window.parent.postMessage({jsonrpc:'2.0',method:'tools/call',params:{name:'search-content',arguments:{q:document.getElementById('q').value,limit:20}}},'*');
|
|
578
|
+
}
|
|
579
|
+
window.addEventListener('message',e=>{
|
|
580
|
+
if(e.data?.result||e.data?.method==='tool/result'){
|
|
581
|
+
const r=e.data.result||e.data.params?.result;
|
|
582
|
+
const d=r?.data||r||[];
|
|
583
|
+
document.getElementById('results').innerHTML=d.map(x=>'<div class="result"><b>'+((x.type||'')+'</b> '+(x.content?.app_name||'')+': '+(x.content?.text||x.content?.transcription||'').substring(0,200))+'</div>').join('');
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
</script>
|
|
587
|
+
</body>
|
|
588
|
+
</html>`;
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
contents: [
|
|
592
|
+
{
|
|
593
|
+
uri,
|
|
594
|
+
mimeType: "text/html",
|
|
595
|
+
text: htmlContent,
|
|
596
|
+
},
|
|
597
|
+
],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
544
600
|
default:
|
|
545
601
|
throw new Error(`Unknown resource: ${uri}`);
|
|
546
602
|
}
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "screenpipe",
|
|
4
4
|
"display_name": "Screenpipe",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.5.0",
|
|
6
6
|
"description": "Search your screen recordings, audio transcriptions, and control your computer with AI",
|
|
7
7
|
"long_description": "Screenpipe is a 24/7 screen and audio recorder that lets you search everything you've seen or heard. This extension connects Claude to your local screenpipe instance, enabling AI-powered search through your digital memory and computer control capabilities.",
|
|
8
8
|
"author": {
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"name": "search-content",
|
|
31
31
|
"description": "Search through recorded screen content, audio transcriptions, and UI elements"
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
"name": "export-video",
|
|
35
|
+
"description": "Export screen recordings as MP4 video for a specific time range"
|
|
36
|
+
},
|
|
33
37
|
{
|
|
34
38
|
"name": "pixel-control",
|
|
35
39
|
"description": "Control mouse and keyboard (type text, press keys, move mouse, click)"
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -51,7 +51,7 @@ const SCREENPIPE_API = `http://localhost:${port}`;
|
|
|
51
51
|
const server = new Server(
|
|
52
52
|
{
|
|
53
53
|
name: "screenpipe",
|
|
54
|
-
version: "0.
|
|
54
|
+
version: "0.5.0",
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
capabilities: {
|
|
@@ -466,6 +466,12 @@ const RESOURCES = [
|
|
|
466
466
|
description: "How to use screenpipe search effectively",
|
|
467
467
|
mimeType: "text/markdown",
|
|
468
468
|
},
|
|
469
|
+
{
|
|
470
|
+
uri: "ui://search",
|
|
471
|
+
name: "Search Dashboard",
|
|
472
|
+
description: "Interactive search UI for exploring screen recordings and audio transcriptions",
|
|
473
|
+
mimeType: "text/html",
|
|
474
|
+
},
|
|
469
475
|
];
|
|
470
476
|
|
|
471
477
|
// List resources handler
|
|
@@ -543,6 +549,56 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
543
549
|
],
|
|
544
550
|
};
|
|
545
551
|
|
|
552
|
+
case "ui://search": {
|
|
553
|
+
// MCP App UI - Interactive search dashboard
|
|
554
|
+
const uiHtmlPath = path.join(__dirname, "..", "ui", "search.html");
|
|
555
|
+
let htmlContent: string;
|
|
556
|
+
try {
|
|
557
|
+
htmlContent = fs.readFileSync(uiHtmlPath, "utf-8");
|
|
558
|
+
} catch {
|
|
559
|
+
// Fallback: serve embedded minimal UI if file not found
|
|
560
|
+
htmlContent = `<!DOCTYPE html>
|
|
561
|
+
<html>
|
|
562
|
+
<head>
|
|
563
|
+
<style>
|
|
564
|
+
body { font-family: system-ui; background: #0a0a0a; color: #fff; padding: 20px; }
|
|
565
|
+
input { width: 100%; padding: 10px; margin-bottom: 10px; background: #1a1a1a; border: 1px solid #333; color: #fff; border-radius: 6px; }
|
|
566
|
+
button { padding: 10px 20px; background: #fff; color: #000; border: none; border-radius: 6px; cursor: pointer; }
|
|
567
|
+
#results { margin-top: 20px; }
|
|
568
|
+
.result { background: #1a1a1a; padding: 12px; margin: 8px 0; border-radius: 8px; border: 1px solid #333; }
|
|
569
|
+
</style>
|
|
570
|
+
</head>
|
|
571
|
+
<body>
|
|
572
|
+
<h2>screenpipe search</h2>
|
|
573
|
+
<input id="q" placeholder="search..." onkeydown="if(event.key==='Enter')search()"/>
|
|
574
|
+
<button onclick="search()">search</button>
|
|
575
|
+
<div id="results"></div>
|
|
576
|
+
<script>
|
|
577
|
+
function search() {
|
|
578
|
+
window.parent.postMessage({jsonrpc:'2.0',method:'tools/call',params:{name:'search-content',arguments:{q:document.getElementById('q').value,limit:20}}},'*');
|
|
579
|
+
}
|
|
580
|
+
window.addEventListener('message',e=>{
|
|
581
|
+
if(e.data?.result||e.data?.method==='tool/result'){
|
|
582
|
+
const r=e.data.result||e.data.params?.result;
|
|
583
|
+
const d=r?.data||r||[];
|
|
584
|
+
document.getElementById('results').innerHTML=d.map(x=>'<div class="result"><b>'+((x.type||'')+'</b> '+(x.content?.app_name||'')+': '+(x.content?.text||x.content?.transcription||'').substring(0,200))+'</div>').join('');
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
</script>
|
|
588
|
+
</body>
|
|
589
|
+
</html>`;
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
contents: [
|
|
593
|
+
{
|
|
594
|
+
uri,
|
|
595
|
+
mimeType: "text/html",
|
|
596
|
+
text: htmlContent,
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
546
602
|
default:
|
|
547
603
|
throw new Error(`Unknown resource: ${uri}`);
|
|
548
604
|
}
|
package/ui/search.html
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>screenpipe search</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
16
|
+
background: #0a0a0a;
|
|
17
|
+
color: #fafafa;
|
|
18
|
+
padding: 16px;
|
|
19
|
+
min-height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.container {
|
|
23
|
+
max-width: 700px;
|
|
24
|
+
margin: 0 auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.header {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 10px;
|
|
31
|
+
margin-bottom: 20px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.header h1 {
|
|
35
|
+
font-size: 18px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.logo {
|
|
40
|
+
width: 24px;
|
|
41
|
+
height: 24px;
|
|
42
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
43
|
+
border-radius: 6px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.search-box {
|
|
47
|
+
display: flex;
|
|
48
|
+
gap: 8px;
|
|
49
|
+
margin-bottom: 16px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.search-input {
|
|
53
|
+
flex: 1;
|
|
54
|
+
padding: 12px 16px;
|
|
55
|
+
border: 1px solid #333;
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
background: #1a1a1a;
|
|
58
|
+
color: #fff;
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
outline: none;
|
|
61
|
+
transition: border-color 0.2s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.search-input:focus {
|
|
65
|
+
border-color: #666;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.search-input::placeholder {
|
|
69
|
+
color: #666;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.btn {
|
|
73
|
+
padding: 12px 20px;
|
|
74
|
+
border: none;
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
transition: all 0.2s;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.btn-primary {
|
|
83
|
+
background: #fff;
|
|
84
|
+
color: #000;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.btn-primary:hover {
|
|
88
|
+
background: #e0e0e0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.btn-secondary {
|
|
92
|
+
background: #1a1a1a;
|
|
93
|
+
color: #fff;
|
|
94
|
+
border: 1px solid #333;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.btn-secondary:hover {
|
|
98
|
+
background: #252525;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.filters {
|
|
102
|
+
display: flex;
|
|
103
|
+
gap: 8px;
|
|
104
|
+
margin-bottom: 20px;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.filter-btn {
|
|
109
|
+
padding: 6px 14px;
|
|
110
|
+
border: 1px solid #333;
|
|
111
|
+
border-radius: 20px;
|
|
112
|
+
background: transparent;
|
|
113
|
+
color: #888;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
transition: all 0.2s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.filter-btn:hover {
|
|
120
|
+
border-color: #555;
|
|
121
|
+
color: #fff;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.filter-btn.active {
|
|
125
|
+
background: #fff;
|
|
126
|
+
color: #000;
|
|
127
|
+
border-color: #fff;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.stats {
|
|
131
|
+
display: flex;
|
|
132
|
+
gap: 20px;
|
|
133
|
+
padding: 12px 16px;
|
|
134
|
+
background: #1a1a1a;
|
|
135
|
+
border-radius: 8px;
|
|
136
|
+
margin-bottom: 20px;
|
|
137
|
+
font-size: 13px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.stat {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 6px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.stat-value {
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
color: #fff;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.stat-label {
|
|
152
|
+
color: #666;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.results {
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
gap: 12px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.result-card {
|
|
162
|
+
background: #1a1a1a;
|
|
163
|
+
border: 1px solid #252525;
|
|
164
|
+
border-radius: 10px;
|
|
165
|
+
padding: 16px;
|
|
166
|
+
transition: border-color 0.2s;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.result-card:hover {
|
|
170
|
+
border-color: #333;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.result-header {
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
align-items: flex-start;
|
|
177
|
+
margin-bottom: 10px;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.result-type {
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
gap: 6px;
|
|
184
|
+
padding: 4px 10px;
|
|
185
|
+
border-radius: 4px;
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
letter-spacing: 0.5px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.type-ocr {
|
|
193
|
+
background: rgba(59, 130, 246, 0.2);
|
|
194
|
+
color: #60a5fa;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.type-audio {
|
|
198
|
+
background: rgba(34, 197, 94, 0.2);
|
|
199
|
+
color: #4ade80;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.type-ui {
|
|
203
|
+
background: rgba(168, 85, 247, 0.2);
|
|
204
|
+
color: #c084fc;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.result-time {
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
color: #666;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.result-app {
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
color: #888;
|
|
215
|
+
margin-bottom: 8px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.result-text {
|
|
219
|
+
font-size: 14px;
|
|
220
|
+
line-height: 1.5;
|
|
221
|
+
color: #ccc;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.result-actions {
|
|
225
|
+
display: flex;
|
|
226
|
+
gap: 8px;
|
|
227
|
+
margin-top: 12px;
|
|
228
|
+
padding-top: 12px;
|
|
229
|
+
border-top: 1px solid #252525;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.action-btn {
|
|
233
|
+
padding: 6px 12px;
|
|
234
|
+
border: 1px solid #333;
|
|
235
|
+
border-radius: 6px;
|
|
236
|
+
background: transparent;
|
|
237
|
+
color: #888;
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
transition: all 0.2s;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.action-btn:hover {
|
|
244
|
+
border-color: #555;
|
|
245
|
+
color: #fff;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.empty-state {
|
|
249
|
+
text-align: center;
|
|
250
|
+
padding: 60px 20px;
|
|
251
|
+
color: #666;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.empty-state h3 {
|
|
255
|
+
font-size: 16px;
|
|
256
|
+
margin-bottom: 8px;
|
|
257
|
+
color: #888;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.loading {
|
|
261
|
+
display: flex;
|
|
262
|
+
justify-content: center;
|
|
263
|
+
padding: 40px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.spinner {
|
|
267
|
+
width: 24px;
|
|
268
|
+
height: 24px;
|
|
269
|
+
border: 2px solid #333;
|
|
270
|
+
border-top-color: #fff;
|
|
271
|
+
border-radius: 50%;
|
|
272
|
+
animation: spin 0.8s linear infinite;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@keyframes spin {
|
|
276
|
+
to { transform: rotate(360deg); }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.hidden {
|
|
280
|
+
display: none;
|
|
281
|
+
}
|
|
282
|
+
</style>
|
|
283
|
+
</head>
|
|
284
|
+
<body>
|
|
285
|
+
<div class="container">
|
|
286
|
+
<div class="header">
|
|
287
|
+
<div class="logo"></div>
|
|
288
|
+
<h1>screenpipe search</h1>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="search-box">
|
|
292
|
+
<input
|
|
293
|
+
type="text"
|
|
294
|
+
id="query"
|
|
295
|
+
class="search-input"
|
|
296
|
+
placeholder="search your screen history, audio, and more..."
|
|
297
|
+
onkeydown="if(event.key==='Enter')search()"
|
|
298
|
+
/>
|
|
299
|
+
<button class="btn btn-primary" onclick="search()">search</button>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div class="filters">
|
|
303
|
+
<button class="filter-btn active" data-type="all" onclick="setFilter('all')">all</button>
|
|
304
|
+
<button class="filter-btn" data-type="ocr" onclick="setFilter('ocr')">screen</button>
|
|
305
|
+
<button class="filter-btn" data-type="audio" onclick="setFilter('audio')">audio</button>
|
|
306
|
+
<button class="filter-btn" data-type="ui" onclick="setFilter('ui')">ui elements</button>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div id="stats" class="stats hidden">
|
|
310
|
+
<div class="stat">
|
|
311
|
+
<span class="stat-value" id="result-count">0</span>
|
|
312
|
+
<span class="stat-label">results</span>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="stat">
|
|
315
|
+
<span class="stat-value" id="time-range">-</span>
|
|
316
|
+
<span class="stat-label">time range</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div id="loading" class="loading hidden">
|
|
321
|
+
<div class="spinner"></div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div id="results" class="results">
|
|
325
|
+
<div class="empty-state">
|
|
326
|
+
<h3>search your digital history</h3>
|
|
327
|
+
<p>type a query above to search through your screen recordings and audio transcriptions</p>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<script>
|
|
333
|
+
let currentFilter = 'all';
|
|
334
|
+
let pendingRequestId = null;
|
|
335
|
+
|
|
336
|
+
// MCP App communication layer
|
|
337
|
+
const mcp = {
|
|
338
|
+
requestId: 0,
|
|
339
|
+
|
|
340
|
+
callTool: function(name, args) {
|
|
341
|
+
this.requestId++;
|
|
342
|
+
pendingRequestId = this.requestId;
|
|
343
|
+
window.parent.postMessage({
|
|
344
|
+
jsonrpc: '2.0',
|
|
345
|
+
id: this.requestId,
|
|
346
|
+
method: 'tools/call',
|
|
347
|
+
params: { name, arguments: args }
|
|
348
|
+
}, '*');
|
|
349
|
+
return this.requestId;
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
sendMessage: function(text) {
|
|
353
|
+
window.parent.postMessage({
|
|
354
|
+
jsonrpc: '2.0',
|
|
355
|
+
method: 'message/send',
|
|
356
|
+
params: { content: text }
|
|
357
|
+
}, '*');
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// listen for messages from host
|
|
362
|
+
window.addEventListener('message', (event) => {
|
|
363
|
+
const data = event.data;
|
|
364
|
+
|
|
365
|
+
// handle tool results
|
|
366
|
+
if (data?.result || data?.method === 'tool/result') {
|
|
367
|
+
hideLoading();
|
|
368
|
+
const result = data.result || data.params?.result;
|
|
369
|
+
if (result) {
|
|
370
|
+
displayResults(result);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// handle errors
|
|
375
|
+
if (data?.error) {
|
|
376
|
+
hideLoading();
|
|
377
|
+
showError(data.error.message || 'search failed');
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
function setFilter(type) {
|
|
382
|
+
currentFilter = type;
|
|
383
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
384
|
+
btn.classList.toggle('active', btn.dataset.type === type);
|
|
385
|
+
});
|
|
386
|
+
// re-run search with new filter if there's a query
|
|
387
|
+
const query = document.getElementById('query').value;
|
|
388
|
+
if (query) {
|
|
389
|
+
search();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function search() {
|
|
394
|
+
const query = document.getElementById('query').value;
|
|
395
|
+
showLoading();
|
|
396
|
+
|
|
397
|
+
const args = {
|
|
398
|
+
q: query || undefined,
|
|
399
|
+
content_type: currentFilter,
|
|
400
|
+
limit: 20
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
mcp.callTool('search-content', args);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function showLoading() {
|
|
407
|
+
document.getElementById('loading').classList.remove('hidden');
|
|
408
|
+
document.getElementById('results').innerHTML = '';
|
|
409
|
+
document.getElementById('stats').classList.add('hidden');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function hideLoading() {
|
|
413
|
+
document.getElementById('loading').classList.add('hidden');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function showError(message) {
|
|
417
|
+
document.getElementById('results').innerHTML = `
|
|
418
|
+
<div class="empty-state">
|
|
419
|
+
<h3>error</h3>
|
|
420
|
+
<p>${escapeHtml(message)}</p>
|
|
421
|
+
</div>
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function displayResults(data) {
|
|
426
|
+
const results = data.data || data || [];
|
|
427
|
+
const container = document.getElementById('results');
|
|
428
|
+
const statsEl = document.getElementById('stats');
|
|
429
|
+
|
|
430
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
431
|
+
container.innerHTML = `
|
|
432
|
+
<div class="empty-state">
|
|
433
|
+
<h3>no results found</h3>
|
|
434
|
+
<p>try a different search term or adjust your filters</p>
|
|
435
|
+
</div>
|
|
436
|
+
`;
|
|
437
|
+
statsEl.classList.add('hidden');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// update stats
|
|
442
|
+
document.getElementById('result-count').textContent = results.length;
|
|
443
|
+
|
|
444
|
+
// calculate time range
|
|
445
|
+
const timestamps = results
|
|
446
|
+
.map(r => r.content?.timestamp)
|
|
447
|
+
.filter(Boolean)
|
|
448
|
+
.map(t => new Date(t).getTime())
|
|
449
|
+
.sort((a, b) => a - b);
|
|
450
|
+
|
|
451
|
+
if (timestamps.length > 1) {
|
|
452
|
+
const start = new Date(timestamps[0]);
|
|
453
|
+
const end = new Date(timestamps[timestamps.length - 1]);
|
|
454
|
+
document.getElementById('time-range').textContent = formatTimeRange(start, end);
|
|
455
|
+
} else {
|
|
456
|
+
document.getElementById('time-range').textContent = 'now';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
statsEl.classList.remove('hidden');
|
|
460
|
+
|
|
461
|
+
// render results
|
|
462
|
+
container.innerHTML = results.map(result => {
|
|
463
|
+
const content = result.content || {};
|
|
464
|
+
const type = result.type?.toLowerCase() || 'unknown';
|
|
465
|
+
const text = content.text || content.transcription || '';
|
|
466
|
+
const app = content.app_name || content.device_name || 'unknown';
|
|
467
|
+
const window = content.window_name || '';
|
|
468
|
+
const time = content.timestamp ? formatTime(new Date(content.timestamp)) : '';
|
|
469
|
+
|
|
470
|
+
return `
|
|
471
|
+
<div class="result-card">
|
|
472
|
+
<div class="result-header">
|
|
473
|
+
<span class="result-type type-${type}">${getTypeIcon(type)} ${type}</span>
|
|
474
|
+
<span class="result-time">${time}</span>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="result-app">${escapeHtml(app)}${window ? ' - ' + escapeHtml(window) : ''}</div>
|
|
477
|
+
<div class="result-text">${escapeHtml(truncate(text, 300))}</div>
|
|
478
|
+
<div class="result-actions">
|
|
479
|
+
<button class="action-btn" onclick="copyText('${escapeJs(text)}')">copy</button>
|
|
480
|
+
<button class="action-btn" onclick="askAbout('${escapeJs(text.substring(0, 100))}')">ask AI</button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
`;
|
|
484
|
+
}).join('');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getTypeIcon(type) {
|
|
488
|
+
switch(type) {
|
|
489
|
+
case 'ocr': return '';
|
|
490
|
+
case 'audio': return '';
|
|
491
|
+
case 'ui': return '';
|
|
492
|
+
default: return '';
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function formatTime(date) {
|
|
497
|
+
const now = new Date();
|
|
498
|
+
const diff = now - date;
|
|
499
|
+
|
|
500
|
+
if (diff < 60000) return 'just now';
|
|
501
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
|
|
502
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
|
|
503
|
+
|
|
504
|
+
return date.toLocaleDateString('en-US', {
|
|
505
|
+
month: 'short',
|
|
506
|
+
day: 'numeric',
|
|
507
|
+
hour: 'numeric',
|
|
508
|
+
minute: '2-digit'
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function formatTimeRange(start, end) {
|
|
513
|
+
const diff = end - start;
|
|
514
|
+
if (diff < 3600000) return Math.floor(diff / 60000) + ' min';
|
|
515
|
+
if (diff < 86400000) return Math.floor(diff / 3600000) + ' hours';
|
|
516
|
+
return Math.floor(diff / 86400000) + ' days';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function truncate(str, len) {
|
|
520
|
+
if (!str) return '';
|
|
521
|
+
if (str.length <= len) return str;
|
|
522
|
+
return str.substring(0, len) + '...';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function escapeHtml(str) {
|
|
526
|
+
if (!str) return '';
|
|
527
|
+
return str
|
|
528
|
+
.replace(/&/g, '&')
|
|
529
|
+
.replace(/</g, '<')
|
|
530
|
+
.replace(/>/g, '>')
|
|
531
|
+
.replace(/"/g, '"')
|
|
532
|
+
.replace(/'/g, ''');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function escapeJs(str) {
|
|
536
|
+
if (!str) return '';
|
|
537
|
+
return str
|
|
538
|
+
.replace(/\\/g, '\\\\')
|
|
539
|
+
.replace(/'/g, "\\'")
|
|
540
|
+
.replace(/"/g, '\\"')
|
|
541
|
+
.replace(/\n/g, '\\n')
|
|
542
|
+
.replace(/\r/g, '\\r');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function copyText(text) {
|
|
546
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
547
|
+
// could show a toast here
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function askAbout(text) {
|
|
552
|
+
mcp.sendMessage(`Tell me more about: "${text}"`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// auto-focus search input
|
|
556
|
+
document.getElementById('query').focus();
|
|
557
|
+
</script>
|
|
558
|
+
</body>
|
|
559
|
+
</html>
|