@sentry/junior 0.14.0 → 0.15.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/README.md CHANGED
@@ -25,7 +25,7 @@ export default app;
25
25
 
26
26
  Run `junior init my-bot` to scaffold a complete project including `vercel.json` for Vercel deployment.
27
27
 
28
- Use `createApp({ pluginPackages: [...] })` to declare which plugin packages to load at runtime.
28
+ Use `juniorNitro({ pluginPackages: [...] })` in `nitro.config.ts` to declare which plugin packages to bundle and load at runtime.
29
29
 
30
30
  ## Full docs
31
31
 
package/dist/app.js CHANGED
@@ -95,6 +95,11 @@ async function GET() {
95
95
  });
96
96
  }
97
97
 
98
+ // src/chat/xml.ts
99
+ function escapeXml(value) {
100
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
101
+ }
102
+
98
103
  // src/handlers/health.ts
99
104
  function GET2() {
100
105
  return Response.json({
@@ -104,6 +109,200 @@ function GET2() {
104
109
  });
105
110
  }
106
111
 
112
+ // src/handlers/diagnostics-dashboard.ts
113
+ async function GET3() {
114
+ let health;
115
+ let discovery;
116
+ try {
117
+ const res = await GET2();
118
+ health = {
119
+ ok: res.ok,
120
+ data: await res.json()
121
+ };
122
+ } catch (e) {
123
+ health = { ok: false, error: String(e) };
124
+ }
125
+ try {
126
+ const res = await GET();
127
+ if (res.ok) {
128
+ discovery = {
129
+ ok: true,
130
+ data: await res.json()
131
+ };
132
+ } else {
133
+ discovery = { ok: false, error: `${res.status} ${res.statusText}` };
134
+ }
135
+ } catch (e) {
136
+ discovery = { ok: false, error: String(e) };
137
+ }
138
+ const d = discovery.ok ? discovery.data : null;
139
+ let html = `<!DOCTYPE html>
140
+ <html lang="en">
141
+ <head>
142
+ <meta charset="utf-8" />
143
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
144
+ <title>Junior</title>
145
+ <style>
146
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
147
+ body {
148
+ font-family: "SF Mono", "Cascadia Code", "Fira Code", Menlo, monospace;
149
+ background: #0d1117; color: #c9d1d9; padding: 2rem;
150
+ font-size: 14px; line-height: 1.6;
151
+ }
152
+ h1 { color: #58a6ff; font-size: 1.1rem; margin-bottom: 0.25rem; }
153
+ .subtitle { color: #8b949e; font-size: 0.85rem; margin-bottom: 1.5rem; }
154
+ .section { max-width: 720px; margin-bottom: 1.25rem; }
155
+ .section-title {
156
+ color: #8b949e; font-size: 0.75rem; text-transform: uppercase;
157
+ letter-spacing: 0.08em; margin-bottom: 0.5rem; padding-bottom: 0.25rem;
158
+ border-bottom: 1px solid #21262d;
159
+ }
160
+ .status-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; }
161
+ .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
162
+ .dot-ok { background: #2dd4bf; }
163
+ .dot-err { background: #f87171; }
164
+ .label { color: #8b949e; }
165
+ .value { color: #e6edf3; }
166
+ .detail-row { display: flex; gap: 0.5rem; margin-bottom: 0.25rem; font-size: 0.85rem; }
167
+ .detail-key { color: #8b949e; min-width: 7rem; }
168
+ .detail-val { color: #c9d1d9; }
169
+ .skill-grid { display: flex; flex-wrap: wrap; gap: 0.4rem; }
170
+ .skill-tag {
171
+ background: #161b22; border: 1px solid #30363d; border-radius: 4px;
172
+ padding: 0.2rem 0.5rem; font-size: 0.8rem; color: #c9d1d9;
173
+ }
174
+ .skill-provider { color: #8b949e; font-size: 0.7rem; margin-left: 0.15rem; }
175
+ .provider-list, .package-list { display: flex; flex-wrap: wrap; gap: 0.4rem; }
176
+ .provider-tag {
177
+ background: #1b2332; border: 1px solid #1f3a5f; border-radius: 4px;
178
+ padding: 0.2rem 0.5rem; font-size: 0.8rem; color: #58a6ff;
179
+ }
180
+ .package-tag {
181
+ background: #1a1e2a; border: 1px solid #2d3548; border-radius: 4px;
182
+ padding: 0.2rem 0.5rem; font-size: 0.78rem; color: #a5b4cf;
183
+ }
184
+ .endpoint-list { list-style: none; }
185
+ .endpoint-list li { margin-bottom: 0.2rem; font-size: 0.85rem; }
186
+ .method {
187
+ display: inline-block; font-size: 0.7rem; font-weight: 600;
188
+ padding: 0.1rem 0.35rem; border-radius: 3px; margin-right: 0.4rem;
189
+ min-width: 2.5rem; text-align: center;
190
+ }
191
+ .method-get { background: #1b3a2d; color: #2dd4bf; }
192
+ .method-post { background: #3b2e1a; color: #f0b952; }
193
+ .endpoint-link { color: #c9d1d9; text-decoration: none; }
194
+ .endpoint-link:hover { color: #58a6ff; text-decoration: underline; }
195
+ .error-msg { color: #f87171; font-size: 0.85rem; }
196
+ </style>
197
+ </head>
198
+ <body>
199
+ <h1>&gt; junior</h1>`;
200
+ if (d?.aboutText) {
201
+ html += `
202
+ <div class="subtitle">${escapeXml(String(d.aboutText))}</div>`;
203
+ }
204
+ html += `
205
+ <div class="section">
206
+ <div class="section-title">Status</div>
207
+ <div class="status-row">
208
+ <span class="dot ${health.ok ? "dot-ok" : "dot-err"}"></span>
209
+ <span class="value">${health.ok ? "Healthy" : "Unreachable"}</span>`;
210
+ if (health.ok && health.data?.timestamp) {
211
+ html += `
212
+ <span class="label">&middot; ${escapeXml(new Date(health.data.timestamp).toLocaleTimeString())}</span>`;
213
+ }
214
+ html += `
215
+ </div>`;
216
+ if (d) {
217
+ html += `
218
+ <div class="detail-row"><span class="detail-key">service</span><span class="detail-val">${escapeXml(String(health.data?.service ?? "junior"))}</span></div>`;
219
+ html += `
220
+ <div class="detail-row"><span class="detail-key">cwd</span><span class="detail-val">${escapeXml(String(d.cwd))}</span></div>`;
221
+ html += `
222
+ <div class="detail-row"><span class="detail-key">home</span><span class="detail-val">${escapeXml(String(d.homeDir))}</span></div>`;
223
+ }
224
+ html += `
225
+ </div>`;
226
+ const endpoints = [
227
+ { method: "GET", path: "/api/health" },
228
+ { method: "GET", path: "/api/__junior/discovery" },
229
+ { method: "GET", path: "/api/oauth/callback/mcp/:provider" },
230
+ { method: "GET", path: "/api/oauth/callback/:provider" },
231
+ { method: "POST", path: "/api/webhooks/:platform" }
232
+ ];
233
+ html += `
234
+ <div class="section">
235
+ <div class="section-title">Endpoints</div>
236
+ <ul class="endpoint-list">`;
237
+ for (const ep of endpoints) {
238
+ const cls = ep.method === "GET" ? "method-get" : "method-post";
239
+ const link = ep.path.includes(":") ? `<span>${escapeXml(ep.path)}</span>` : `<a class="endpoint-link" href="${escapeXml(ep.path)}" target="_blank">${escapeXml(ep.path)}</a>`;
240
+ html += `
241
+ <li><span class="method ${cls}">${escapeXml(ep.method)}</span>${link}</li>`;
242
+ }
243
+ html += `
244
+ </ul>
245
+ </div>`;
246
+ if (d) {
247
+ const providers = d.providers;
248
+ const packagedContent = d.packagedContent;
249
+ const skills = d.skills;
250
+ if (providers?.length) {
251
+ html += `
252
+ <div class="section">
253
+ <div class="section-title">Plugins <span class="label">(${providers.length})</span></div>
254
+ <div class="provider-list">`;
255
+ for (const p of providers) {
256
+ html += `
257
+ <span class="provider-tag">${escapeXml(p)}</span>`;
258
+ }
259
+ html += `
260
+ </div>`;
261
+ if (packagedContent?.packageNames?.length) {
262
+ html += `
263
+ <div style="margin-top:0.5rem"><div class="package-list">`;
264
+ for (const pkg of packagedContent.packageNames) {
265
+ html += `
266
+ <span class="package-tag">${escapeXml(pkg)}</span>`;
267
+ }
268
+ html += `
269
+ </div></div>`;
270
+ }
271
+ html += `
272
+ </div>`;
273
+ }
274
+ if (skills?.length) {
275
+ html += `
276
+ <div class="section">
277
+ <div class="section-title">Skills <span class="label">(${skills.length})</span></div>
278
+ <div class="skill-grid">`;
279
+ for (const s of skills) {
280
+ html += `
281
+ <span class="skill-tag">${escapeXml(s.name)}`;
282
+ if (s.pluginProvider) {
283
+ html += ` <span class="skill-provider">${escapeXml(s.pluginProvider)}</span>`;
284
+ }
285
+ html += `</span>`;
286
+ }
287
+ html += `
288
+ </div>
289
+ </div>`;
290
+ }
291
+ } else if (!discovery.ok) {
292
+ html += `
293
+ <div class="section">
294
+ <div class="section-title">Discovery</div>
295
+ <span class="error-msg">unavailable &middot; ${escapeXml(discovery.error ?? "unknown")}</span>
296
+ </div>`;
297
+ }
298
+ html += `
299
+ </body>
300
+ </html>`;
301
+ return new Response(html, {
302
+ headers: { "content-type": "text/html; charset=utf-8" }
303
+ });
304
+ }
305
+
107
306
  // src/handlers/mcp-oauth-callback.ts
108
307
  import { Buffer as Buffer2 } from "buffer";
109
308
 
@@ -2421,13 +2620,6 @@ import { Agent } from "@mariozechner/pi-agent-core";
2421
2620
 
2422
2621
  // src/chat/prompt.ts
2423
2622
  import fs from "fs";
2424
-
2425
- // src/chat/xml.ts
2426
- function escapeXml(value) {
2427
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
2428
- }
2429
-
2430
- // src/chat/prompt.ts
2431
2623
  var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
2432
2624
  function getLoggedMarkdownFiles() {
2433
2625
  const globalState = globalThis;
@@ -9770,7 +9962,7 @@ async function resumeAuthorizedMcpTurn(args) {
9770
9962
  }
9771
9963
  });
9772
9964
  }
9773
- async function GET3(request, provider, waitUntil) {
9965
+ async function GET4(request, provider, waitUntil) {
9774
9966
  const url = new URL(request.url);
9775
9967
  const state = url.searchParams.get("state")?.trim();
9776
9968
  const code = url.searchParams.get("code")?.trim();
@@ -10011,7 +10203,7 @@ async function resumePendingOAuthMessage(stored) {
10011
10203
  }
10012
10204
  });
10013
10205
  }
10014
- async function GET4(request, provider, waitUntil) {
10206
+ async function GET5(request, provider, waitUntil) {
10015
10207
  const providerConfig = getPluginOAuthConfig(provider);
10016
10208
  if (!providerConfig) {
10017
10209
  return htmlErrorResponse(
@@ -12752,8 +12944,25 @@ async function defaultWaitUntil() {
12752
12944
  };
12753
12945
  }
12754
12946
  }
12947
+ async function resolveBuildPluginPackages() {
12948
+ try {
12949
+ const mod = await import("#junior/config");
12950
+ return mod.pluginPackages;
12951
+ } catch {
12952
+ const env = process.env.JUNIOR_PLUGIN_PACKAGES;
12953
+ if (env) {
12954
+ try {
12955
+ return JSON.parse(env);
12956
+ } catch {
12957
+ }
12958
+ }
12959
+ return void 0;
12960
+ }
12961
+ }
12755
12962
  async function createApp(options) {
12756
- setPluginPackages(options?.pluginPackages);
12963
+ setPluginPackages(
12964
+ options?.pluginPackages ?? await resolveBuildPluginPackages()
12965
+ );
12757
12966
  const waitUntil = options?.waitUntil ?? await defaultWaitUntil();
12758
12967
  const app = new Hono().basePath("/api");
12759
12968
  app.onError((err, c) => {
@@ -12762,11 +12971,12 @@ async function createApp(options) {
12762
12971
  });
12763
12972
  app.get("/health", () => GET2());
12764
12973
  app.get("/__junior/discovery", () => GET());
12974
+ app.get("/__junior/dashboard", () => GET3());
12765
12975
  app.get("/oauth/callback/mcp/:provider", (c) => {
12766
- return GET3(c.req.raw, c.req.param("provider"), waitUntil);
12976
+ return GET4(c.req.raw, c.req.param("provider"), waitUntil);
12767
12977
  });
12768
12978
  app.get("/oauth/callback/:provider", (c) => {
12769
- return GET4(c.req.raw, c.req.param("provider"), waitUntil);
12979
+ return GET5(c.req.raw, c.req.param("provider"), waitUntil);
12770
12980
  });
12771
12981
  app.post("/webhooks/:platform", (c) => {
12772
12982
  return POST(c.req.raw, c.req.param("platform"), waitUntil);
package/dist/cli/init.js CHANGED
@@ -88,6 +88,7 @@ async function runInit(dir, log = console.log) {
88
88
  hono: "^4.12.0"
89
89
  },
90
90
  devDependencies: {
91
+ jiti: "^2.6.1",
91
92
  nitro: "3.0.260311-beta",
92
93
  typescript: "^5.9.0",
93
94
  vite: "^8.0.3"
package/dist/nitro.d.ts CHANGED
@@ -2,6 +2,13 @@ interface JuniorNitroOptions {
2
2
  cwd?: string;
3
3
  maxDuration?: number;
4
4
  pluginPackages?: string[];
5
+ /**
6
+ * Extra file patterns to copy into the server output for files that the
7
+ * bundler cannot trace (e.g. dynamically imported providers).
8
+ * Each entry is `"<package-name>/<subpath-glob>"`, resolved via Node
9
+ * module resolution. Example: `"@mariozechner/pi-ai/dist/providers/*.js"`
10
+ */
11
+ includeFiles?: string[];
5
12
  }
6
13
  /** Nitro module that copies app and plugin content into the Vercel build output. */
7
14
  declare function juniorNitro(options?: JuniorNitroOptions): {
package/dist/nitro.js CHANGED
@@ -4,8 +4,9 @@ import {
4
4
  import "./chunk-2KG3PWR4.js";
5
5
 
6
6
  // src/nitro.ts
7
- import { cpSync, existsSync, mkdirSync } from "fs";
7
+ import { cpSync, existsSync, mkdirSync, readdirSync } from "fs";
8
8
  import path from "path";
9
+ import { fileURLToPath } from "url";
9
10
  function juniorNitro(options = {}) {
10
11
  return {
11
12
  nitro: {
@@ -16,12 +17,17 @@ function juniorNitro(options = {}) {
16
17
  nitro.options.vercel ??= {};
17
18
  nitro.options.vercel.functions ??= {};
18
19
  nitro.options.vercel.functions.maxDuration ??= options.maxDuration ?? 800;
20
+ nitro.options.virtual["#junior/config"] = `export const pluginPackages = ${JSON.stringify(options.pluginPackages ?? [])};`;
19
21
  nitro.hooks.hook("compiled", () => {
20
22
  copyAppAndPluginContent(
21
23
  cwd,
22
24
  nitro.options.output.serverDir,
23
25
  options.pluginPackages
24
26
  );
27
+ copyIncludedFiles(
28
+ nitro.options.output.serverDir,
29
+ options.includeFiles
30
+ );
25
31
  });
26
32
  }
27
33
  }
@@ -50,6 +56,45 @@ function copyAppAndPluginContent(cwd, serverRoot, pluginPackages) {
50
56
  copyRootIntoServerOutput(cwd, serverRoot, root);
51
57
  }
52
58
  }
59
+ function resolvePackageDir(pkgName) {
60
+ try {
61
+ const resolved = import.meta.resolve(pkgName);
62
+ const entry = resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
63
+ let dir = path.dirname(entry);
64
+ while (dir !== path.dirname(dir)) {
65
+ if (existsSync(path.join(dir, "package.json"))) return dir;
66
+ dir = path.dirname(dir);
67
+ }
68
+ } catch {
69
+ }
70
+ return void 0;
71
+ }
72
+ function copyIncludedFiles(serverRoot, patterns) {
73
+ if (!patterns?.length) return;
74
+ for (const pattern of patterns) {
75
+ const normalized = pattern.replace(/^node_modules\//, "");
76
+ const parts = normalized.split("/");
77
+ const pkgName = parts[0].startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
78
+ const subpath = parts.slice(pkgName.includes("/") ? 2 : 1).join("/");
79
+ const fileGlob = path.basename(subpath);
80
+ const subDir = path.dirname(subpath);
81
+ const pkgDir = resolvePackageDir(pkgName);
82
+ if (!pkgDir) continue;
83
+ const sourceDir = path.join(pkgDir, subDir);
84
+ if (!existsSync(sourceDir)) continue;
85
+ const entries = readdirSync(sourceDir);
86
+ const re = fileGlob.includes("*") ? new RegExp(
87
+ `^${fileGlob.replace(/[\\^$+?.()|[\]{}]/g, "\\$&").replace(/\*/g, ".*")}$`
88
+ ) : null;
89
+ for (const entry of entries) {
90
+ if (re ? !re.test(entry) : entry !== fileGlob) continue;
91
+ copyIfExists(
92
+ path.join(sourceDir, entry),
93
+ path.join(serverRoot, "node_modules", pkgName, subDir, entry)
94
+ );
95
+ }
96
+ }
97
+ }
53
98
  function copyIfExists(source, target) {
54
99
  if (!existsSync(source)) {
55
100
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"