@agentproto/apps 0.4.0 → 0.5.1
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/README.md +15 -0
- package/dist/bin/sync.d.ts +23 -0
- package/dist/bin/sync.d.ts.map +1 -0
- package/dist/bin/sync.mjs +944 -0
- package/dist/bin/sync.mjs.map +1 -0
- package/dist/code-team/index.d.ts.map +1 -1
- package/dist/code-team.mjs +4 -0
- package/dist/code-team.mjs.map +1 -1
- package/dist/content-team/index.d.ts.map +1 -1
- package/dist/content-team.mjs +4 -0
- package/dist/content-team.mjs.map +1 -1
- package/dist/index.mjs +607 -2
- package/dist/index.mjs.map +1 -1
- package/dist/mail-triage/index.d.ts.map +1 -1
- package/dist/mail-triage/ui.d.ts +15 -0
- package/dist/mail-triage/ui.d.ts.map +1 -0
- package/dist/mail-triage.mjs +348 -1
- package/dist/mail-triage.mjs.map +1 -1
- package/dist/media-viewer/index.d.ts.map +1 -1
- package/dist/media-viewer/ui.d.ts +14 -0
- package/dist/media-viewer/ui.d.ts.map +1 -0
- package/dist/media-viewer.mjs +251 -1
- package/dist/media-viewer.mjs.map +1 -1
- package/package.json +7 -4
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { parseArgs } from 'util';
|
|
6
|
+
import { defineApp } from '@agentproto/app-kit';
|
|
7
|
+
import { defineAgent } from '@agentproto/agent';
|
|
8
|
+
import { defineWorkflow } from '@agentproto/workflow';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @agentproto/apps v0.1.0
|
|
12
|
+
* Ready-made agentproto apps (teams of agents + workflows) built on app-kit.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var implementer = {
|
|
16
|
+
agent: defineAgent({
|
|
17
|
+
schema: "agent/v1",
|
|
18
|
+
id: "@agentproto/implementer",
|
|
19
|
+
description: "Implements the requested code change.",
|
|
20
|
+
model: "claude-sonnet-5",
|
|
21
|
+
boundaries: ["Change only what the task asks", "Match the surrounding style"],
|
|
22
|
+
tools: ["list_dir", "read_file", "write_file", "edit_file", "run_command"],
|
|
23
|
+
workflows: [{ ref: "deliver-change" }]
|
|
24
|
+
}),
|
|
25
|
+
body: "You implement the requested change. Read the task and the surrounding code first, make the smallest correct change that satisfies it, then run the tests. Do not touch anything the task didn't ask for."
|
|
26
|
+
};
|
|
27
|
+
var reviewer = {
|
|
28
|
+
agent: defineAgent({
|
|
29
|
+
schema: "agent/v1",
|
|
30
|
+
id: "@agentproto/reviewer",
|
|
31
|
+
description: "Reviews the diff and reports findings.",
|
|
32
|
+
model: "claude-sonnet-5",
|
|
33
|
+
boundaries: ["Report findings only", "Never edit files", "Never run gh pr merge"],
|
|
34
|
+
// Read-only + run_command (to inspect the diff / run tests). No write/edit
|
|
35
|
+
// tools — the "never edit files" boundary is enforced by capability too.
|
|
36
|
+
tools: ["list_dir", "read_file", "run_command"],
|
|
37
|
+
workflows: [{ ref: "deliver-change" }]
|
|
38
|
+
}),
|
|
39
|
+
body: "You are a rigorous reviewer. Read the diff, and report each real issue as `file:line \u2014 why`. Change nothing; you only report."
|
|
40
|
+
};
|
|
41
|
+
var fixer = {
|
|
42
|
+
agent: defineAgent({
|
|
43
|
+
schema: "agent/v1",
|
|
44
|
+
id: "@agentproto/fixer",
|
|
45
|
+
description: "Applies the reviewer's findings, precisely and minimally.",
|
|
46
|
+
model: "claude-sonnet-5",
|
|
47
|
+
boundaries: ["Apply only the listed findings", "No unrelated refactors"],
|
|
48
|
+
tools: ["list_dir", "read_file", "write_file", "edit_file", "run_command"],
|
|
49
|
+
workflows: [{ ref: "deliver-change" }]
|
|
50
|
+
}),
|
|
51
|
+
body: "You apply the reviewer's findings. Address each listed finding exactly, add nothing extra, and re-run the tests when done."
|
|
52
|
+
};
|
|
53
|
+
var deliverChange = defineWorkflow({
|
|
54
|
+
id: "deliver-change",
|
|
55
|
+
name: "Deliver a code change",
|
|
56
|
+
description: "Implement the change, review the diff, apply fixes, verify.",
|
|
57
|
+
version: "0.1.0",
|
|
58
|
+
inputs: {},
|
|
59
|
+
outputs: {},
|
|
60
|
+
steps: [
|
|
61
|
+
{
|
|
62
|
+
id: "implement",
|
|
63
|
+
kind: "agent",
|
|
64
|
+
agent: { ref: "@agentproto/implementer" },
|
|
65
|
+
prompt: "Implement the requested change."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "review",
|
|
69
|
+
kind: "agent",
|
|
70
|
+
agent: { ref: "@agentproto/reviewer" },
|
|
71
|
+
prompt: "Review the diff the implementer just produced. Report every real issue as `file:line \u2014 why`."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "fix",
|
|
75
|
+
kind: "agent",
|
|
76
|
+
agent: { ref: "@agentproto/fixer" },
|
|
77
|
+
prompt: "Apply the reviewer's findings, then re-run the tests."
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// src/code-team/index.ts
|
|
83
|
+
var codeTeam = defineApp({
|
|
84
|
+
id: "@agentproto/code-team",
|
|
85
|
+
name: "Code Team",
|
|
86
|
+
version: "0.1.0",
|
|
87
|
+
description: "Implement, review, and fix code changes.",
|
|
88
|
+
agents: [implementer, reviewer, fixer],
|
|
89
|
+
workflows: [deliverChange]
|
|
90
|
+
});
|
|
91
|
+
var researcher = {
|
|
92
|
+
agent: defineAgent({
|
|
93
|
+
schema: "agent/v1",
|
|
94
|
+
id: "@agentproto/researcher",
|
|
95
|
+
description: "Reads the provided sources and summarizes them for the writer.",
|
|
96
|
+
model: "claude-sonnet-5",
|
|
97
|
+
boundaries: ["Cite every claim", "Prefer primary sources"],
|
|
98
|
+
tools: ["list_dir", "read_file", "run_command"],
|
|
99
|
+
workflows: [{ ref: "produce-content" }]
|
|
100
|
+
}),
|
|
101
|
+
body: "You read the source material already in the workspace (list_dir, then read_file the sources), extract the key facts, and hand the writer a tight brief with a citation for every claim. Use run_command only to fetch a URL already named in the task; do not invent sources."
|
|
102
|
+
};
|
|
103
|
+
var writer = {
|
|
104
|
+
agent: defineAgent({
|
|
105
|
+
schema: "agent/v1",
|
|
106
|
+
id: "@agentproto/writer",
|
|
107
|
+
description: "Drafts the piece from the researcher's brief.",
|
|
108
|
+
model: "claude-sonnet-5",
|
|
109
|
+
boundaries: ["Stay on the brief", "One idea per paragraph"],
|
|
110
|
+
tools: ["list_dir", "read_file", "write_file", "edit_file"],
|
|
111
|
+
workflows: [{ ref: "produce-content" }]
|
|
112
|
+
}),
|
|
113
|
+
body: "You draft the piece from the brief and its sources. Lead with the point, keep one idea per paragraph, and don't invent facts the brief doesn't have."
|
|
114
|
+
};
|
|
115
|
+
var editor = {
|
|
116
|
+
agent: defineAgent({
|
|
117
|
+
schema: "agent/v1",
|
|
118
|
+
id: "@agentproto/editor",
|
|
119
|
+
description: "Edits the draft for voice, clarity, and length.",
|
|
120
|
+
model: "claude-sonnet-5",
|
|
121
|
+
boundaries: ["Preserve the writer's meaning", "Cut, don't pad"],
|
|
122
|
+
tools: ["read_file", "edit_file"],
|
|
123
|
+
workflows: [{ ref: "produce-content" }]
|
|
124
|
+
}),
|
|
125
|
+
body: "You edit the draft. Tighten the voice, cut fluff, fix clarity \u2014 without changing what the writer meant. Leave it shorter than you found it."
|
|
126
|
+
};
|
|
127
|
+
var illustrator = {
|
|
128
|
+
agent: defineAgent({
|
|
129
|
+
schema: "agent/v1",
|
|
130
|
+
id: "@agentproto/illustrator",
|
|
131
|
+
description: "Art-directs the cover illustration for each article.",
|
|
132
|
+
model: "claude-sonnet-5",
|
|
133
|
+
boundaries: ["One cover per article", "Text-free prompts only"],
|
|
134
|
+
tools: ["read_file", "write_file", "run_command"],
|
|
135
|
+
workflows: [{ ref: "produce-cover" }]
|
|
136
|
+
}),
|
|
137
|
+
body: "You art-direct a single cover illustration for the article. Read the article (read_file) to extract the one visual idea that best represents the piece, then write the art direction (write_file) as a clean, text-free image prompt. Keep the visual discipline tight: bold flat shapes, limited risograph-style palettes, strong negative space, and a single focal subject. Never include text, lettering, logos, watermarks, or UI chrome in the prompt. Produce exactly one cover per article; if the article calls for a literal diagram or chart, fall back to a precise gpt-image-1 style description and treat the diagram itself as the image subject. Use run_command only to inspect generated assets or move files during the upload step."
|
|
138
|
+
};
|
|
139
|
+
var produceContent = defineWorkflow({
|
|
140
|
+
id: "produce-content",
|
|
141
|
+
name: "Produce a piece of content",
|
|
142
|
+
description: "Read the sources, draft the piece, edit it, then commit it.",
|
|
143
|
+
version: "0.1.0",
|
|
144
|
+
inputs: {},
|
|
145
|
+
outputs: {},
|
|
146
|
+
// Concrete workspace tools (fs + exec), so the workflow describes a process
|
|
147
|
+
// a standard runtime can actually run — not abstract tools nothing provides.
|
|
148
|
+
steps: [
|
|
149
|
+
{ id: "research", kind: "tool", tool: "read_file" },
|
|
150
|
+
{ id: "draft", kind: "tool", tool: "write_file" },
|
|
151
|
+
{ id: "edit", kind: "tool", tool: "edit_file" },
|
|
152
|
+
{ id: "publish", kind: "tool", tool: "run_command" }
|
|
153
|
+
]
|
|
154
|
+
});
|
|
155
|
+
var produceCover = defineWorkflow({
|
|
156
|
+
id: "produce-cover",
|
|
157
|
+
name: "Produce a cover illustration",
|
|
158
|
+
description: "Read the article, derive the cover concept, generate the base image, clean it up, and upload the final cover.",
|
|
159
|
+
version: "0.1.0",
|
|
160
|
+
inputs: {},
|
|
161
|
+
outputs: {},
|
|
162
|
+
steps: [
|
|
163
|
+
{ id: "concept-from-article", kind: "tool", tool: "content.cover-concept" },
|
|
164
|
+
{ id: "base-gen", kind: "tool", tool: "content.cover-base-gen" },
|
|
165
|
+
{ id: "cleanup", kind: "tool", tool: "content.cover-cleanup" },
|
|
166
|
+
{ id: "upload", kind: "tool", tool: "content.cover-upload" }
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// src/content-team/index.ts
|
|
171
|
+
var contentTeam = defineApp({
|
|
172
|
+
id: "@agentproto/content-team",
|
|
173
|
+
name: "Content Team",
|
|
174
|
+
version: "0.1.0",
|
|
175
|
+
description: "Research, draft, edit, and illustrate content.",
|
|
176
|
+
agents: [researcher, writer, editor, illustrator],
|
|
177
|
+
workflows: [produceContent, produceCover]
|
|
178
|
+
});
|
|
179
|
+
var triager = {
|
|
180
|
+
agent: defineAgent({
|
|
181
|
+
schema: "agent/v1",
|
|
182
|
+
id: "@agentproto/triager",
|
|
183
|
+
description: "Triages your inbox \u2014 finds unread mail, categorizes, labels, archives.",
|
|
184
|
+
model: "claude-sonnet-5",
|
|
185
|
+
boundaries: [
|
|
186
|
+
"Always show the triage plan before applying",
|
|
187
|
+
"Never delete \u2014 only archive, label, or mark read",
|
|
188
|
+
"Ask before applying bulk actions on more than 20 messages"
|
|
189
|
+
],
|
|
190
|
+
tools: [
|
|
191
|
+
"mailbox_list",
|
|
192
|
+
"mailbox_search",
|
|
193
|
+
"mailbox_list_threads",
|
|
194
|
+
"mailbox_get_thread",
|
|
195
|
+
"mailbox_labels_list",
|
|
196
|
+
"mailbox_label_create",
|
|
197
|
+
"mailbox_triage_plan",
|
|
198
|
+
"mailbox_triage_apply"
|
|
199
|
+
],
|
|
200
|
+
workflows: [{ ref: "triage-inbox" }]
|
|
201
|
+
}),
|
|
202
|
+
body: "You scan the inbox for unread messages using mailbox_search. Categorize them (urgent, needs-reply, newsletter, notification, spam). Create a triage plan: label each category, archive newsletters+notifications, keep urgent+needs-reply in inbox. Show the plan before applying. Use mailbox_triage_plan to build the plan and mailbox_triage_apply to execute it after user confirmation."
|
|
203
|
+
};
|
|
204
|
+
var triageInbox = defineWorkflow({
|
|
205
|
+
id: "triage-inbox",
|
|
206
|
+
name: "Triage Inbox",
|
|
207
|
+
description: "Scan inbox, categorize unread messages, create and apply triage plan.",
|
|
208
|
+
version: "0.1.0",
|
|
209
|
+
inputs: {},
|
|
210
|
+
outputs: {},
|
|
211
|
+
steps: [
|
|
212
|
+
{
|
|
213
|
+
id: "triage",
|
|
214
|
+
kind: "agent",
|
|
215
|
+
agent: { ref: "@agentproto/triager" },
|
|
216
|
+
prompt: "Scan the inbox for unread messages. Categorize them (urgent, needs-reply, newsletter, notification, spam). Create a triage plan: label each category, archive newsletters+notifications, keep urgent+needs-reply in inbox. Show me the plan before applying."
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/mail-triage/ui.ts
|
|
222
|
+
var MAIL_TRIAGE_TOOLS = [
|
|
223
|
+
"imported:agentpush/mailbox_list",
|
|
224
|
+
"imported:agentpush/mailbox_search",
|
|
225
|
+
"imported:agentpush/mailbox_triage_plan",
|
|
226
|
+
"imported:agentpush/mailbox_triage_apply",
|
|
227
|
+
"app_run",
|
|
228
|
+
"app_status",
|
|
229
|
+
"agent_output"
|
|
230
|
+
];
|
|
231
|
+
var APP_ID = "@agentproto/mail-triage";
|
|
232
|
+
var AGENT_ID = "@agentproto/triager";
|
|
233
|
+
var CATEGORIES = ["urgent", "needs-reply", "newsletter", "notification"];
|
|
234
|
+
var MAIL_TRIAGE_HTML = `<!DOCTYPE html>
|
|
235
|
+
<html lang="en">
|
|
236
|
+
<head>
|
|
237
|
+
<meta charset="UTF-8">
|
|
238
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
239
|
+
<title>Mail Triage</title>
|
|
240
|
+
<style>
|
|
241
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
242
|
+
:root{
|
|
243
|
+
--bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
|
|
244
|
+
--text:#e6edf3;--text2:#8b949e;
|
|
245
|
+
--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
|
|
246
|
+
}
|
|
247
|
+
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
|
|
248
|
+
#app{display:flex;flex-direction:column;height:100%;overflow-y:auto}
|
|
249
|
+
#toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;background:var(--bg2);flex-shrink:0}
|
|
250
|
+
#toolbar .title{font-size:13px;font-weight:600;flex:1}
|
|
251
|
+
select,input[type=text]{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 8px;border-radius:4px;font-size:12px;font-family:inherit}
|
|
252
|
+
.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}
|
|
253
|
+
.abtn:hover{background:var(--border)}
|
|
254
|
+
.abtn:disabled{opacity:.4;cursor:default}
|
|
255
|
+
.abtn.primary{background:var(--blue);border-color:var(--blue);color:#04101c}
|
|
256
|
+
.abtn.primary:hover{opacity:.9}
|
|
257
|
+
#content{padding:14px;display:flex;flex-direction:column;gap:16px}
|
|
258
|
+
section{background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:12px}
|
|
259
|
+
section h3{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px}
|
|
260
|
+
#summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px}
|
|
261
|
+
.tile{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px;text-align:center}
|
|
262
|
+
.tile .count{font-size:22px;font-weight:700}
|
|
263
|
+
.tile .label{font-size:11px;color:var(--text2);text-transform:capitalize}
|
|
264
|
+
.tile.urgent .count{color:var(--red)}
|
|
265
|
+
.tile.needs-reply .count{color:var(--yellow)}
|
|
266
|
+
.tile.newsletter .count{color:var(--blue)}
|
|
267
|
+
.tile.notification .count{color:var(--purple)}
|
|
268
|
+
#plan-list,#search-results,#log{max-height:260px;overflow-y:auto;font-size:12px}
|
|
269
|
+
.plan-item{display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid var(--border)}
|
|
270
|
+
.plan-item .subj{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
271
|
+
.plan-item .tag{color:var(--text2);font-size:11px;white-space:nowrap}
|
|
272
|
+
#log{font-family:Menlo,Monaco,monospace;white-space:pre-wrap;word-break:break-word;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:8px}
|
|
273
|
+
.searchbar{display:flex;gap:8px;margin-bottom:8px}
|
|
274
|
+
.searchbar input{flex:1}
|
|
275
|
+
.actions{display:flex;gap:8px;margin-top:10px}
|
|
276
|
+
#status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
|
|
277
|
+
.empty{color:var(--text2);padding:8px 0}
|
|
278
|
+
</style>
|
|
279
|
+
</head>
|
|
280
|
+
<body>
|
|
281
|
+
<div id="app">
|
|
282
|
+
<div id="toolbar">
|
|
283
|
+
<span class="title">Mail Triage</span>
|
|
284
|
+
<select id="mailbox-select"></select>
|
|
285
|
+
<button class="abtn" id="refresh-btn">Refresh</button>
|
|
286
|
+
</div>
|
|
287
|
+
<div id="content">
|
|
288
|
+
<section>
|
|
289
|
+
<h3>Inbox summary</h3>
|
|
290
|
+
<div id="summary"></div>
|
|
291
|
+
</section>
|
|
292
|
+
|
|
293
|
+
<section>
|
|
294
|
+
<h3>Search</h3>
|
|
295
|
+
<div class="searchbar">
|
|
296
|
+
<input type="text" id="search-input" placeholder="e.g. is:unread">
|
|
297
|
+
<button class="abtn" id="search-btn">Search</button>
|
|
298
|
+
</div>
|
|
299
|
+
<div id="search-results"><div class="empty">No search run yet.</div></div>
|
|
300
|
+
</section>
|
|
301
|
+
|
|
302
|
+
<section>
|
|
303
|
+
<h3>Triage plan</h3>
|
|
304
|
+
<div class="actions">
|
|
305
|
+
<button class="abtn primary" id="triage-btn">Triage</button>
|
|
306
|
+
<button class="abtn" id="apply-btn" disabled>Apply</button>
|
|
307
|
+
</div>
|
|
308
|
+
<div id="plan-list" style="margin-top:10px"><div class="empty">No plan yet \u2014 click Triage.</div></div>
|
|
309
|
+
</section>
|
|
310
|
+
|
|
311
|
+
<section>
|
|
312
|
+
<h3>Triager agent</h3>
|
|
313
|
+
<div class="actions">
|
|
314
|
+
<button class="abtn" id="run-agent-btn">Run Triager Agent</button>
|
|
315
|
+
</div>
|
|
316
|
+
<div id="log" style="margin-top:10px"><div class="empty">Agent has not run yet.</div></div>
|
|
317
|
+
</section>
|
|
318
|
+
</div>
|
|
319
|
+
<div id="status">Connecting…</div>
|
|
320
|
+
</div>
|
|
321
|
+
<script>
|
|
322
|
+
var APP_ID = ${JSON.stringify(APP_ID)};
|
|
323
|
+
var AGENT_ID = ${JSON.stringify(AGENT_ID)};
|
|
324
|
+
var CATEGORIES = ${JSON.stringify(CATEGORIES)};
|
|
325
|
+
var callTool = null;
|
|
326
|
+
var currentPlan = null;
|
|
327
|
+
var pollHandle = null;
|
|
328
|
+
|
|
329
|
+
function setStatus(msg) {
|
|
330
|
+
document.getElementById("status").textContent = msg;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// See media-viewer/ui.ts for why this loop exists: app_tool_call wraps its
|
|
334
|
+
// dispatch result in at least one more {content:[{type:"text",text:...}]}
|
|
335
|
+
// shell than the underlying tool returned.
|
|
336
|
+
function unwrapText(result) {
|
|
337
|
+
var cur = result;
|
|
338
|
+
for (var i = 0; i < 4; i++) {
|
|
339
|
+
if (typeof cur === "string") {
|
|
340
|
+
try {
|
|
341
|
+
cur = JSON.parse(cur);
|
|
342
|
+
continue;
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return cur;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
|
|
348
|
+
cur = cur.content[0].text;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
return cur;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function callApp(tool, args) {
|
|
357
|
+
return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function asList(value, keys) {
|
|
361
|
+
if (Array.isArray(value)) return value;
|
|
362
|
+
if (value && typeof value === "object") {
|
|
363
|
+
for (var i = 0; i < keys.length; i++) {
|
|
364
|
+
if (Array.isArray(value[keys[i]])) return value[keys[i]];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function renderSummary(counts) {
|
|
371
|
+
var el = document.getElementById("summary");
|
|
372
|
+
el.innerHTML = "";
|
|
373
|
+
CATEGORIES.forEach(function (cat) {
|
|
374
|
+
var tile = document.createElement("div");
|
|
375
|
+
tile.className = "tile " + cat;
|
|
376
|
+
var count = counts && counts[cat] !== undefined ? counts[cat] : "\u2013";
|
|
377
|
+
tile.innerHTML = '<div class="count"></div><div class="label"></div>';
|
|
378
|
+
tile.querySelector(".count").textContent = String(count);
|
|
379
|
+
tile.querySelector(".label").textContent = cat.replace("-", " ");
|
|
380
|
+
el.appendChild(tile);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function loadMailboxes() {
|
|
385
|
+
setStatus("Loading mailboxes\\u2026");
|
|
386
|
+
return callApp("imported:agentpush/mailbox_list", {})
|
|
387
|
+
.then(function (result) {
|
|
388
|
+
var mailboxes = asList(result, ["mailboxes", "items"]);
|
|
389
|
+
var select = document.getElementById("mailbox-select");
|
|
390
|
+
select.innerHTML = "";
|
|
391
|
+
if (mailboxes.length === 0) {
|
|
392
|
+
var opt = document.createElement("option");
|
|
393
|
+
opt.textContent = "inbox";
|
|
394
|
+
opt.value = "inbox";
|
|
395
|
+
select.appendChild(opt);
|
|
396
|
+
} else {
|
|
397
|
+
mailboxes.forEach(function (mb) {
|
|
398
|
+
var name = typeof mb === "string" ? mb : mb.name || mb.id || JSON.stringify(mb);
|
|
399
|
+
var opt = document.createElement("option");
|
|
400
|
+
opt.value = name;
|
|
401
|
+
opt.textContent = name;
|
|
402
|
+
select.appendChild(opt);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
setStatus("Ready");
|
|
406
|
+
})
|
|
407
|
+
.catch(function (err) {
|
|
408
|
+
setStatus("Error loading mailboxes: " + err.message);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function renderSearchResults(items) {
|
|
413
|
+
var el = document.getElementById("search-results");
|
|
414
|
+
el.innerHTML = "";
|
|
415
|
+
if (items.length === 0) {
|
|
416
|
+
el.innerHTML = '<div class="empty">No results.</div>';
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
items.forEach(function (item) {
|
|
420
|
+
var row = document.createElement("div");
|
|
421
|
+
row.className = "plan-item";
|
|
422
|
+
var subj = typeof item === "string" ? item : item.subject || item.title || JSON.stringify(item);
|
|
423
|
+
var from = item && item.from ? item.from : "";
|
|
424
|
+
row.innerHTML = '<span class="subj"></span><span class="tag"></span>';
|
|
425
|
+
row.querySelector(".subj").textContent = subj;
|
|
426
|
+
row.querySelector(".tag").textContent = from;
|
|
427
|
+
el.appendChild(row);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderPlan(plan) {
|
|
432
|
+
var items = asList(plan, ["actions", "items", "plan"]);
|
|
433
|
+
var el = document.getElementById("plan-list");
|
|
434
|
+
el.innerHTML = "";
|
|
435
|
+
if (items.length === 0) {
|
|
436
|
+
el.innerHTML = '<div class="empty">Plan is empty.</div>';
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
var counts = {};
|
|
440
|
+
items.forEach(function (item) {
|
|
441
|
+
var cat = (item && (item.category || item.tag)) || "notification";
|
|
442
|
+
counts[cat] = (counts[cat] || 0) + 1;
|
|
443
|
+
var row = document.createElement("div");
|
|
444
|
+
row.className = "plan-item";
|
|
445
|
+
var subj = (item && (item.subject || item.title)) || JSON.stringify(item);
|
|
446
|
+
var action = (item && (item.action || item.category)) || "";
|
|
447
|
+
row.innerHTML = '<span class="subj"></span><span class="tag"></span>';
|
|
448
|
+
row.querySelector(".subj").textContent = subj;
|
|
449
|
+
row.querySelector(".tag").textContent = action;
|
|
450
|
+
el.appendChild(row);
|
|
451
|
+
});
|
|
452
|
+
renderSummary(counts);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
document.getElementById("refresh-btn").addEventListener("click", loadMailboxes);
|
|
456
|
+
|
|
457
|
+
document.getElementById("search-btn").addEventListener("click", function () {
|
|
458
|
+
var query = document.getElementById("search-input").value;
|
|
459
|
+
var mailbox = document.getElementById("mailbox-select").value;
|
|
460
|
+
setStatus("Searching\\u2026");
|
|
461
|
+
callApp("imported:agentpush/mailbox_search", { mailbox: mailbox, query: query })
|
|
462
|
+
.then(function (result) {
|
|
463
|
+
renderSearchResults(asList(result, ["messages", "items", "results"]));
|
|
464
|
+
setStatus("Ready");
|
|
465
|
+
})
|
|
466
|
+
.catch(function (err) {
|
|
467
|
+
setStatus("Search failed: " + err.message);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
document.getElementById("triage-btn").addEventListener("click", function () {
|
|
472
|
+
var mailbox = document.getElementById("mailbox-select").value;
|
|
473
|
+
setStatus("Building triage plan\\u2026");
|
|
474
|
+
document.getElementById("apply-btn").disabled = true;
|
|
475
|
+
callApp("imported:agentpush/mailbox_triage_plan", { mailbox: mailbox })
|
|
476
|
+
.then(function (plan) {
|
|
477
|
+
currentPlan = plan;
|
|
478
|
+
renderPlan(plan);
|
|
479
|
+
document.getElementById("apply-btn").disabled = false;
|
|
480
|
+
setStatus("Plan ready \\u2014 review, then Apply.");
|
|
481
|
+
})
|
|
482
|
+
.catch(function (err) {
|
|
483
|
+
setStatus("Triage plan failed: " + err.message);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
document.getElementById("apply-btn").addEventListener("click", function () {
|
|
488
|
+
if (!currentPlan) return;
|
|
489
|
+
setStatus("Applying triage plan\\u2026");
|
|
490
|
+
callApp("imported:agentpush/mailbox_triage_apply", { plan: currentPlan })
|
|
491
|
+
.then(function () {
|
|
492
|
+
setStatus("Plan applied.");
|
|
493
|
+
document.getElementById("apply-btn").disabled = true;
|
|
494
|
+
})
|
|
495
|
+
.catch(function (err) {
|
|
496
|
+
setStatus("Apply failed: " + err.message);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
function appendLog(text) {
|
|
501
|
+
var el = document.getElementById("log");
|
|
502
|
+
if (el.querySelector(".empty")) el.innerHTML = "";
|
|
503
|
+
el.textContent += (el.textContent ? "\\n" : "") + text;
|
|
504
|
+
el.scrollTop = el.scrollHeight;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function pollAgentRun(appRunId) {
|
|
508
|
+
if (pollHandle) clearTimeout(pollHandle);
|
|
509
|
+
callApp("app_status", { appRunId: appRunId })
|
|
510
|
+
.then(function (status) {
|
|
511
|
+
var sessions = (status && status.sessions) || [];
|
|
512
|
+
var sessionId = sessions[0] && sessions[0].sessionId;
|
|
513
|
+
var tail = sessionId
|
|
514
|
+
? callApp("agent_output", { sessionId: sessionId, lastN: 200, clean: true })
|
|
515
|
+
: Promise.resolve("");
|
|
516
|
+
return tail.then(function (output) {
|
|
517
|
+
if (output) appendLog(typeof output === "string" ? output : JSON.stringify(output));
|
|
518
|
+
var done = status && (status.status === "ended" || status.status === "done" || status.endedAt);
|
|
519
|
+
if (!done) {
|
|
520
|
+
pollHandle = setTimeout(function () { pollAgentRun(appRunId); }, 2000);
|
|
521
|
+
} else {
|
|
522
|
+
setStatus("Agent run finished.");
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
})
|
|
526
|
+
.catch(function (err) {
|
|
527
|
+
setStatus("Agent status failed: " + err.message);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
document.getElementById("run-agent-btn").addEventListener("click", function () {
|
|
532
|
+
document.getElementById("log").innerHTML = "";
|
|
533
|
+
setStatus("Starting triager agent\\u2026");
|
|
534
|
+
callApp("app_run", { appId: APP_ID, agents: [AGENT_ID], prompt: "Triage my inbox." })
|
|
535
|
+
.then(function (run) {
|
|
536
|
+
appendLog("Started app run " + (run && run.appRunId));
|
|
537
|
+
pollAgentRun(run.appRunId);
|
|
538
|
+
})
|
|
539
|
+
.catch(function (err) {
|
|
540
|
+
setStatus("Run failed: " + err.message);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
window.McpApp.connect()
|
|
545
|
+
.then(function (bridge) {
|
|
546
|
+
callTool = bridge.callTool;
|
|
547
|
+
return loadMailboxes();
|
|
548
|
+
})
|
|
549
|
+
.catch(function (err) {
|
|
550
|
+
setStatus("Standalone mode \\u2014 no host bridge (" + err.message + ")");
|
|
551
|
+
renderSummary(null);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
renderSummary(null);
|
|
555
|
+
</script>
|
|
556
|
+
</body>
|
|
557
|
+
</html>`;
|
|
558
|
+
|
|
559
|
+
// src/mail-triage/index.ts
|
|
560
|
+
var mailTriage = defineApp({
|
|
561
|
+
id: "@agentproto/mail-triage",
|
|
562
|
+
name: "Mail Triage",
|
|
563
|
+
version: "0.1.0",
|
|
564
|
+
description: "Triage your inbox \u2014 categorize, label, archive.",
|
|
565
|
+
agents: [triager],
|
|
566
|
+
workflows: [triageInbox],
|
|
567
|
+
ui: {
|
|
568
|
+
html: MAIL_TRIAGE_HTML,
|
|
569
|
+
title: "Mail Triage",
|
|
570
|
+
tools: [...MAIL_TRIAGE_TOOLS]
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
var cataloger = {
|
|
574
|
+
agent: defineAgent({
|
|
575
|
+
schema: "agent/v1",
|
|
576
|
+
id: "@agentproto/media-cataloger",
|
|
577
|
+
description: "Scans a folder for media files (images, video, audio), collects metadata, and produces a structured catalog.",
|
|
578
|
+
model: "claude-sonnet-5",
|
|
579
|
+
boundaries: [
|
|
580
|
+
"Read-only \u2014 never modify, rename, move, or delete files",
|
|
581
|
+
"Only catalog files with recognized media extensions",
|
|
582
|
+
"Report exact file sizes and paths, never estimate"
|
|
583
|
+
],
|
|
584
|
+
tools: ["list_dir", "read_file", "file_info"],
|
|
585
|
+
workflows: [{ ref: "scan-media" }]
|
|
586
|
+
}),
|
|
587
|
+
body: [
|
|
588
|
+
"You scan a workspace folder for media files and produce a structured JSON catalog.",
|
|
589
|
+
"",
|
|
590
|
+
"Walk the target directory recursively using list_dir. For each file, check the",
|
|
591
|
+
"extension against known media types:",
|
|
592
|
+
"- Images: .jpg, .jpeg, .png, .gif, .webp, .svg, .bmp, .ico, .avif",
|
|
593
|
+
"- Video: .mp4, .webm, .mov, .avi, .mkv",
|
|
594
|
+
"- Audio: .mp3, .wav, .ogg, .flac, .aac, .m4a",
|
|
595
|
+
"",
|
|
596
|
+
"For each media file found, use file_info to get size and modification date.",
|
|
597
|
+
"",
|
|
598
|
+
"Output a JSON object with this shape:",
|
|
599
|
+
"```json",
|
|
600
|
+
"{",
|
|
601
|
+
' "folder": "<scanned path>",',
|
|
602
|
+
' "scannedAt": "<ISO timestamp>",',
|
|
603
|
+
' "summary": { "total": N, "images": N, "video": N, "audio": N, "totalSizeBytes": N },',
|
|
604
|
+
' "files": [',
|
|
605
|
+
" {",
|
|
606
|
+
' "path": "relative/path.jpg",',
|
|
607
|
+
' "name": "file.jpg",',
|
|
608
|
+
' "type": "image",',
|
|
609
|
+
' "extension": ".jpg",',
|
|
610
|
+
' "sizeBytes": 123456,',
|
|
611
|
+
' "modifiedAt": "2026-08-01T12:00:00Z"',
|
|
612
|
+
" }",
|
|
613
|
+
" ]",
|
|
614
|
+
"}",
|
|
615
|
+
"```",
|
|
616
|
+
"",
|
|
617
|
+
"Sort files by type (images first, then video, then audio), then by name."
|
|
618
|
+
].join("\n")
|
|
619
|
+
};
|
|
620
|
+
var scanMedia = defineWorkflow({
|
|
621
|
+
id: "scan-media",
|
|
622
|
+
name: "Scan Media",
|
|
623
|
+
description: "Scan a folder for media files, collect metadata, and produce a structured catalog.",
|
|
624
|
+
version: "0.1.0",
|
|
625
|
+
inputs: {},
|
|
626
|
+
outputs: {},
|
|
627
|
+
steps: [
|
|
628
|
+
{
|
|
629
|
+
id: "catalog",
|
|
630
|
+
kind: "agent",
|
|
631
|
+
agent: { ref: "@agentproto/media-cataloger" },
|
|
632
|
+
prompt: "Scan the workspace root folder for media files (images, video, audio). Walk all subdirectories recursively. For each media file, collect its path, name, type, extension, size, and modification date. Output the full catalog as a single JSON object."
|
|
633
|
+
}
|
|
634
|
+
]
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// src/media-viewer/ui.ts
|
|
638
|
+
var MEDIA_VIEWER_TOOLS = ["directory_list", "file_info", "file_read"];
|
|
639
|
+
var APP_ID2 = "@agentproto/media-viewer";
|
|
640
|
+
var MEDIA_VIEWER_HTML = `<!DOCTYPE html>
|
|
641
|
+
<html lang="en">
|
|
642
|
+
<head>
|
|
643
|
+
<meta charset="UTF-8">
|
|
644
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
645
|
+
<title>Media Viewer</title>
|
|
646
|
+
<style>
|
|
647
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
648
|
+
:root{
|
|
649
|
+
--bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
|
|
650
|
+
--text:#e6edf3;--text2:#8b949e;
|
|
651
|
+
--green:#3fb950;--yellow:#d29922;--red:#f85149;--blue:#58a6ff;--purple:#bc8cff;
|
|
652
|
+
}
|
|
653
|
+
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;background:var(--bg);color:var(--text)}
|
|
654
|
+
#app{display:flex;flex-direction:column;height:100%}
|
|
655
|
+
#toolbar{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;background:var(--bg2);flex-shrink:0}
|
|
656
|
+
#toolbar .title{font-size:13px;font-weight:600;flex:1}
|
|
657
|
+
#breadcrumb{font-size:12px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
658
|
+
.abtn{background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:5px 10px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit}
|
|
659
|
+
.abtn:hover{background:var(--border)}
|
|
660
|
+
#body{flex:1;display:flex;overflow:hidden}
|
|
661
|
+
#grid-wrap{flex:1;overflow-y:auto;padding:14px}
|
|
662
|
+
#grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px}
|
|
663
|
+
.cell{display:flex;flex-direction:column;align-items:center;gap:6px;padding:10px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg2);cursor:pointer;text-align:center}
|
|
664
|
+
.cell:hover{background:var(--bg3);border-color:var(--blue)}
|
|
665
|
+
.cell.selected{border-color:var(--blue);background:var(--bg3)}
|
|
666
|
+
.icon{font-size:28px;line-height:1}
|
|
667
|
+
.name{font-size:11px;color:var(--text);word-break:break-word;max-width:100%;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
|
668
|
+
#empty{color:var(--text2);padding:24px;text-align:center}
|
|
669
|
+
#details{width:280px;border-left:1px solid var(--border);background:var(--bg2);padding:14px;overflow-y:auto;flex-shrink:0}
|
|
670
|
+
#details h3{font-size:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}
|
|
671
|
+
#details .row{display:flex;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--border);font-size:12px}
|
|
672
|
+
#details .row span:first-child{color:var(--text2)}
|
|
673
|
+
#preview{margin-top:10px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-family:Menlo,Monaco,monospace;font-size:11px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow-y:auto}
|
|
674
|
+
#status{padding:6px 14px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
|
|
675
|
+
</style>
|
|
676
|
+
</head>
|
|
677
|
+
<body>
|
|
678
|
+
<div id="app">
|
|
679
|
+
<div id="toolbar">
|
|
680
|
+
<button class="abtn" id="up-btn" title="Up one level">↑</button>
|
|
681
|
+
<span class="title">Media Viewer</span>
|
|
682
|
+
<span id="breadcrumb">.</span>
|
|
683
|
+
<button class="abtn" id="refresh-btn">Refresh</button>
|
|
684
|
+
</div>
|
|
685
|
+
<div id="body">
|
|
686
|
+
<div id="grid-wrap">
|
|
687
|
+
<div id="grid"></div>
|
|
688
|
+
<div id="empty" style="display:none">No files in this folder.</div>
|
|
689
|
+
</div>
|
|
690
|
+
<div id="details" style="display:none"></div>
|
|
691
|
+
</div>
|
|
692
|
+
<div id="status">Connecting…</div>
|
|
693
|
+
</div>
|
|
694
|
+
<script>
|
|
695
|
+
var APP_ID = ${JSON.stringify(APP_ID2)};
|
|
696
|
+
var currentPath = ".";
|
|
697
|
+
var entries = [];
|
|
698
|
+
var callTool = null;
|
|
699
|
+
|
|
700
|
+
function setStatus(msg) {
|
|
701
|
+
document.getElementById("status").textContent = msg;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// app_tool_call wraps its dispatch result at least once (its own
|
|
705
|
+
// text-result body is the JSON-stringified inner MCP tool result); peel
|
|
706
|
+
// back through nested {content:[{type:"text",text:...}]} shells until the
|
|
707
|
+
// payload stops looking like one, so this works whichever layer the host's
|
|
708
|
+
// own bridge already unwrapped.
|
|
709
|
+
function unwrapText(result) {
|
|
710
|
+
var cur = result;
|
|
711
|
+
for (var i = 0; i < 4; i++) {
|
|
712
|
+
if (typeof cur === "string") {
|
|
713
|
+
try {
|
|
714
|
+
cur = JSON.parse(cur);
|
|
715
|
+
continue;
|
|
716
|
+
} catch (e) {
|
|
717
|
+
return cur;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (cur && Array.isArray(cur.content) && cur.content[0] && typeof cur.content[0].text === "string") {
|
|
721
|
+
cur = cur.content[0].text;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
return cur;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function callApp(tool, args) {
|
|
730
|
+
return callTool("app_tool_call", { appId: APP_ID, tool: tool, args: args || {} }).then(unwrapText);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function parseDirectoryListing(raw) {
|
|
734
|
+
var text = typeof raw === "string" ? raw : "";
|
|
735
|
+
return text
|
|
736
|
+
.split("\\n")
|
|
737
|
+
.map(function (line) { return line.trim(); })
|
|
738
|
+
.filter(function (line) { return line.length > 0; })
|
|
739
|
+
.map(function (line) {
|
|
740
|
+
var isDir = line.indexOf("[DIR]") === 0;
|
|
741
|
+
var name = line.replace(/^\\[(FILE|DIR)\\]\\s*/, "");
|
|
742
|
+
return { name: name, isDir: isDir };
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function iconFor(entry) {
|
|
747
|
+
if (entry.isDir) return "\\uD83D\\uDCC1";
|
|
748
|
+
var ext = (entry.name.split(".").pop() || "").toLowerCase();
|
|
749
|
+
if (["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"].indexOf(ext) !== -1) return "\\uD83D\\uDDBC\\uFE0F";
|
|
750
|
+
if (["mp4", "webm", "mov", "avi", "mkv"].indexOf(ext) !== -1) return "\\uD83C\\uDFA5";
|
|
751
|
+
if (["mp3", "wav", "ogg", "flac", "aac", "m4a"].indexOf(ext) !== -1) return "\\uD83C\\uDFB5";
|
|
752
|
+
return "\\uD83D\\uDCC4";
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function joinPath(base, name) {
|
|
756
|
+
if (base === "." || base === "") return name;
|
|
757
|
+
return base.replace(/\\/+$/, "") + "/" + name;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function parentPath(path) {
|
|
761
|
+
if (path === "." || path.indexOf("/") === -1) return ".";
|
|
762
|
+
return path.slice(0, path.lastIndexOf("/")) || ".";
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function renderGrid() {
|
|
766
|
+
document.getElementById("breadcrumb").textContent = currentPath;
|
|
767
|
+
var grid = document.getElementById("grid");
|
|
768
|
+
var empty = document.getElementById("empty");
|
|
769
|
+
grid.innerHTML = "";
|
|
770
|
+
if (entries.length === 0) {
|
|
771
|
+
empty.style.display = "block";
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
empty.style.display = "none";
|
|
775
|
+
entries.forEach(function (entry, idx) {
|
|
776
|
+
var cell = document.createElement("div");
|
|
777
|
+
cell.className = "cell";
|
|
778
|
+
cell.dataset.idx = String(idx);
|
|
779
|
+
cell.innerHTML = '<div class="icon">' + iconFor(entry) + '</div><div class="name"></div>';
|
|
780
|
+
cell.querySelector(".name").textContent = entry.name;
|
|
781
|
+
cell.addEventListener("click", function () { onSelect(entry, cell); });
|
|
782
|
+
grid.appendChild(cell);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function onSelect(entry, cell) {
|
|
787
|
+
Array.prototype.forEach.call(document.querySelectorAll(".cell"), function (c) {
|
|
788
|
+
c.classList.remove("selected");
|
|
789
|
+
});
|
|
790
|
+
cell.classList.add("selected");
|
|
791
|
+
if (entry.isDir) {
|
|
792
|
+
loadDirectory(joinPath(currentPath, entry.name));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
showFileDetails(joinPath(currentPath, entry.name));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function showFileDetails(path) {
|
|
799
|
+
var details = document.getElementById("details");
|
|
800
|
+
details.style.display = "block";
|
|
801
|
+
details.innerHTML = "<h3>Loading…</h3>";
|
|
802
|
+
setStatus("Reading " + path + "\\u2026");
|
|
803
|
+
callApp("file_info", { path: path })
|
|
804
|
+
.then(function (info) {
|
|
805
|
+
var rows = "";
|
|
806
|
+
if (info && typeof info === "object") {
|
|
807
|
+
["name", "type", "size", "modified"].forEach(function (key) {
|
|
808
|
+
if (info[key] !== undefined) {
|
|
809
|
+
rows += '<div class="row"><span>' + key + '</span><span></span></div>';
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
details.innerHTML = "<h3>Details</h3>" + rows + '<div id="preview">Loading preview…</div>';
|
|
814
|
+
var rowEls = details.querySelectorAll(".row span:last-child");
|
|
815
|
+
var keys = ["name", "type", "size", "modified"].filter(function (key) {
|
|
816
|
+
return info && info[key] !== undefined;
|
|
817
|
+
});
|
|
818
|
+
keys.forEach(function (key, i) {
|
|
819
|
+
rowEls[i].textContent = String(info[key]);
|
|
820
|
+
});
|
|
821
|
+
return callApp("file_read", { path: path });
|
|
822
|
+
})
|
|
823
|
+
.then(function (content) {
|
|
824
|
+
var pre = document.getElementById("preview");
|
|
825
|
+
if (!pre) return;
|
|
826
|
+
var text = typeof content === "string" ? content : JSON.stringify(content);
|
|
827
|
+
pre.textContent = text.length > 2000 ? text.slice(0, 2000) + "\\u2026 (truncated)" : (text || "(empty file)");
|
|
828
|
+
setStatus("Ready \\u00b7 " + currentPath);
|
|
829
|
+
})
|
|
830
|
+
.catch(function (err) {
|
|
831
|
+
var pre = document.getElementById("preview");
|
|
832
|
+
if (pre) pre.textContent = "Preview unavailable (" + err.message + ")";
|
|
833
|
+
setStatus("Ready \\u00b7 " + currentPath);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function loadDirectory(path) {
|
|
838
|
+
document.getElementById("details").style.display = "none";
|
|
839
|
+
setStatus("Loading " + path + "\\u2026");
|
|
840
|
+
return callApp("directory_list", { path: path })
|
|
841
|
+
.then(function (raw) {
|
|
842
|
+
currentPath = path;
|
|
843
|
+
entries = parseDirectoryListing(raw).sort(function (a, b) {
|
|
844
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
845
|
+
return a.name.localeCompare(b.name);
|
|
846
|
+
});
|
|
847
|
+
renderGrid();
|
|
848
|
+
setStatus("Ready \\u00b7 " + entries.length + " entries \\u00b7 " + path);
|
|
849
|
+
})
|
|
850
|
+
.catch(function (err) {
|
|
851
|
+
entries = [];
|
|
852
|
+
renderGrid();
|
|
853
|
+
setStatus("Error: " + err.message);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
document.getElementById("up-btn").addEventListener("click", function () {
|
|
858
|
+
loadDirectory(parentPath(currentPath));
|
|
859
|
+
});
|
|
860
|
+
document.getElementById("refresh-btn").addEventListener("click", function () {
|
|
861
|
+
loadDirectory(currentPath);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
window.McpApp.connect()
|
|
865
|
+
.then(function (bridge) {
|
|
866
|
+
callTool = bridge.callTool;
|
|
867
|
+
return loadDirectory(".");
|
|
868
|
+
})
|
|
869
|
+
.catch(function (err) {
|
|
870
|
+
setStatus("Standalone mode \\u2014 no host bridge (" + err.message + ")");
|
|
871
|
+
entries = [];
|
|
872
|
+
renderGrid();
|
|
873
|
+
});
|
|
874
|
+
</script>
|
|
875
|
+
</body>
|
|
876
|
+
</html>`;
|
|
877
|
+
|
|
878
|
+
// src/media-viewer/index.ts
|
|
879
|
+
var mediaViewer = defineApp({
|
|
880
|
+
id: "@agentproto/media-viewer",
|
|
881
|
+
name: "Media Viewer",
|
|
882
|
+
version: "0.1.0",
|
|
883
|
+
description: "Scan a folder for images, video, and audio \u2014 gallery view.",
|
|
884
|
+
agents: [cataloger],
|
|
885
|
+
workflows: [scanMedia],
|
|
886
|
+
ui: {
|
|
887
|
+
html: MEDIA_VIEWER_HTML,
|
|
888
|
+
title: "Media Viewer",
|
|
889
|
+
tools: [...MEDIA_VIEWER_TOOLS]
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// src/bin/sync.ts
|
|
894
|
+
var APPS = [
|
|
895
|
+
{ slug: "code-team", category: "team", handle: codeTeam },
|
|
896
|
+
{ slug: "content-team", category: "team", handle: contentTeam },
|
|
897
|
+
{ slug: "mail-triage", category: "app", handle: mailTriage },
|
|
898
|
+
{ slug: "media-viewer", category: "app", handle: mediaViewer }
|
|
899
|
+
];
|
|
900
|
+
async function syncApps(baseDir) {
|
|
901
|
+
const catalog = [];
|
|
902
|
+
for (const { slug, category, handle } of APPS) {
|
|
903
|
+
const dir = join(baseDir, slug);
|
|
904
|
+
await handle.emit(dir);
|
|
905
|
+
catalog.push({
|
|
906
|
+
appId: handle.id ?? slug,
|
|
907
|
+
name: handle.name ?? slug,
|
|
908
|
+
description: handle.description ?? "",
|
|
909
|
+
dir,
|
|
910
|
+
category
|
|
911
|
+
});
|
|
912
|
+
console.log(`emitted ${handle.id ?? slug} -> ${dir}`);
|
|
913
|
+
}
|
|
914
|
+
return catalog;
|
|
915
|
+
}
|
|
916
|
+
async function writeCatalog(baseDir, catalog) {
|
|
917
|
+
const catalogPath = join(baseDir, "..", "app-catalog.json");
|
|
918
|
+
await mkdir(dirname(catalogPath), { recursive: true });
|
|
919
|
+
await writeFile(catalogPath, JSON.stringify({ apps: catalog }, null, 2) + "\n", "utf8");
|
|
920
|
+
return catalogPath;
|
|
921
|
+
}
|
|
922
|
+
async function main(argv) {
|
|
923
|
+
const { values } = parseArgs({
|
|
924
|
+
args: [...argv],
|
|
925
|
+
allowPositionals: false,
|
|
926
|
+
strict: true,
|
|
927
|
+
options: { "base-dir": { type: "string" } }
|
|
928
|
+
});
|
|
929
|
+
const baseDir = values["base-dir"] ?? join(homedir(), ".agentproto", "apps");
|
|
930
|
+
await mkdir(baseDir, { recursive: true });
|
|
931
|
+
const catalog = await syncApps(baseDir);
|
|
932
|
+
const catalogPath = await writeCatalog(baseDir, catalog);
|
|
933
|
+
console.log(`wrote catalog (${catalog.length} apps) -> ${catalogPath}`);
|
|
934
|
+
}
|
|
935
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
936
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
937
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
938
|
+
process.exitCode = 1;
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export { syncApps, writeCatalog };
|
|
943
|
+
//# sourceMappingURL=sync.mjs.map
|
|
944
|
+
//# sourceMappingURL=sync.mjs.map
|