@jevitate/cli 0.1.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ai-cli.d.ts +31 -0
  3. package/dist/ai-cli.js +238 -0
  4. package/dist/ai-cli.js.map +1 -0
  5. package/dist/bin.d.ts +2 -0
  6. package/dist/bin.js +50663 -0
  7. package/dist/bin.js.map +1 -0
  8. package/dist/data-dir.d.ts +9 -0
  9. package/dist/data-dir.js +13 -0
  10. package/dist/data-dir.js.map +1 -0
  11. package/dist/envelope.d.ts +11 -0
  12. package/dist/envelope.js +7 -0
  13. package/dist/envelope.js.map +1 -0
  14. package/dist/explore-api.d.ts +187 -0
  15. package/dist/explore-api.js +302 -0
  16. package/dist/explore-api.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +3 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/init-keys.d.ts +14 -0
  21. package/dist/init-keys.js +18 -0
  22. package/dist/init-keys.js.map +1 -0
  23. package/dist/init-mcp.d.ts +87 -0
  24. package/dist/init-mcp.js +198 -0
  25. package/dist/init-mcp.js.map +1 -0
  26. package/dist/init-skills.d.ts +80 -0
  27. package/dist/init-skills.js +256 -0
  28. package/dist/init-skills.js.map +1 -0
  29. package/dist/journey-api.d.ts +37 -0
  30. package/dist/journey-api.js +55 -0
  31. package/dist/journey-api.js.map +1 -0
  32. package/dist/load-api.d.ts +48 -0
  33. package/dist/load-api.js +84 -0
  34. package/dist/load-api.js.map +1 -0
  35. package/dist/mcp-api.d.ts +116 -0
  36. package/dist/mcp-api.js +359 -0
  37. package/dist/mcp-api.js.map +1 -0
  38. package/dist/mission-api.d.ts +56 -0
  39. package/dist/mission-api.js +65 -0
  40. package/dist/mission-api.js.map +1 -0
  41. package/dist/program.d.ts +77 -0
  42. package/dist/program.js +1938 -0
  43. package/dist/program.js.map +1 -0
  44. package/dist/record-api.d.ts +74 -0
  45. package/dist/record-api.js +93 -0
  46. package/dist/record-api.js.map +1 -0
  47. package/dist/regression-api.d.ts +17 -0
  48. package/dist/regression-api.js +16 -0
  49. package/dist/regression-api.js.map +1 -0
  50. package/dist/self-heal-adapter.d.ts +31 -0
  51. package/dist/self-heal-adapter.js +70 -0
  52. package/dist/self-heal-adapter.js.map +1 -0
  53. package/dist/source-api.d.ts +147 -0
  54. package/dist/source-api.js +208 -0
  55. package/dist/source-api.js.map +1 -0
  56. package/dist/source-run-api.d.ts +57 -0
  57. package/dist/source-run-api.js +91 -0
  58. package/dist/source-run-api.js.map +1 -0
  59. package/dist/ui-api.d.ts +35 -0
  60. package/dist/ui-api.js +369 -0
  61. package/dist/ui-api.js.map +1 -0
  62. package/dist/ux-api.d.ts +75 -0
  63. package/dist/ux-api.js +231 -0
  64. package/dist/ux-api.js.map +1 -0
  65. package/dist/version.d.ts +1 -0
  66. package/dist/version.js +37 -0
  67. package/dist/version.js.map +1 -0
  68. package/package.json +85 -0
  69. package/skills/jevitate-explore/SKILL.md +108 -0
  70. package/skills/jevitate-load-test/SKILL.md +42 -0
  71. package/skills/jevitate-mission-scope/SKILL.md +107 -0
  72. package/skills/jevitate-record/SKILL.md +78 -0
  73. package/skills/jevitate-run-journey/SKILL.md +72 -0
  74. package/skills/jevitate-sources/SKILL.md +88 -0
  75. package/skills/jevitate-ux-review/SKILL.md +68 -0
  76. package/ui/app.js +538 -0
  77. package/ui/index.html +546 -0
package/ui/app.js ADDED
@@ -0,0 +1,538 @@
1
+ // Jevitate UI — vanilla-JS SPA for the local `jevitate ui` inbox dashboard.
2
+ // Served same-origin at /app.js (CSP: default-src 'self' — no inline script,
3
+ // no inline event handlers; everything here is wired with addEventListener).
4
+ (function () {
5
+ "use strict";
6
+
7
+ var TOKEN_HEADER = "x-jevitate-token";
8
+ var POLL_MS = 4000;
9
+
10
+ /** Read `?t=` once, then scrub it from the visible URL. The server has
11
+ * already set the auth cookie from this same query param on `GET /`, so
12
+ * same-origin fetches carry it automatically — the header below is sent
13
+ * on every request too, for robustness (belt and suspenders, not the
14
+ * only mechanism). */
15
+ var token = "";
16
+ (function initToken() {
17
+ var params = new URLSearchParams(window.location.search);
18
+ var t = params.get("t");
19
+ if (t) {
20
+ token = t;
21
+ params.delete("t");
22
+ var qs = params.toString();
23
+ var newUrl = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
24
+ window.history.replaceState({}, "", newUrl);
25
+ }
26
+ })();
27
+
28
+ var els = {
29
+ queueScreen: document.getElementById("screen-queue"),
30
+ detailScreen: document.getElementById("screen-detail"),
31
+ queueList: document.getElementById("queue-list"),
32
+ queueCount: document.getElementById("queue-count"),
33
+ detailRoot: document.getElementById("detail-root"),
34
+ backLink: document.getElementById("back-link"),
35
+ toast: document.getElementById("toast"),
36
+ };
37
+
38
+ var state = {
39
+ view: "queue", // "queue" | "detail"
40
+ detailId: null,
41
+ pollTimer: null,
42
+ };
43
+
44
+ // ---------------------------------------------------------------- utils
45
+
46
+ function escapeHtml(value) {
47
+ var s = String(value == null ? "" : value);
48
+ return s
49
+ .replace(/&/g, "&")
50
+ .replace(/</g, "&lt;")
51
+ .replace(/>/g, "&gt;")
52
+ .replace(/"/g, "&quot;")
53
+ .replace(/'/g, "&#39;");
54
+ }
55
+
56
+ function formatTime(iso) {
57
+ if (!iso) return "";
58
+ var d = new Date(iso);
59
+ if (isNaN(d.getTime())) return String(iso);
60
+ return d.toLocaleTimeString(undefined, { hour12: false });
61
+ }
62
+
63
+ var toastTimer = null;
64
+ function showToast(message) {
65
+ if (!els.toast) return;
66
+ els.toast.textContent = message;
67
+ els.toast.hidden = false;
68
+ if (toastTimer) clearTimeout(toastTimer);
69
+ toastTimer = setTimeout(function () {
70
+ els.toast.hidden = true;
71
+ }, 3200);
72
+ }
73
+
74
+ function apiFetch(path, options) {
75
+ options = options || {};
76
+ var headers = Object.assign({}, options.headers || {});
77
+ // Only attach the header when we actually have a token. On first load
78
+ // (URL carries `?t=`) this is always true. On any later reload the `?t=`
79
+ // has already been scrubbed from the URL, so `token` is `""` — sending
80
+ // an empty-string header would otherwise mask the valid auth cookie
81
+ // (the server treats a *present* header as authoritative), 401-ing every
82
+ // /api/* call and rendering a silently-empty queue.
83
+ if (token) headers[TOKEN_HEADER] = token;
84
+ return fetch(path, Object.assign({}, options, { headers: headers, credentials: "same-origin" }));
85
+ }
86
+
87
+ // ------------------------------------------------------------ kind meta
88
+
89
+ var KIND_META = {
90
+ handback: { label: "HANDBACK", color: "var(--cyan)", ledClass: "led-c" },
91
+ approval: { label: "APPROVAL", color: "var(--magenta)", ledClass: "led-m" },
92
+ review: { label: "REVIEW", color: "var(--amber)", ledClass: "led-y" },
93
+ };
94
+
95
+ /** Transition semantics (mirrors `resolveTransition` in @jevitate/inbox):
96
+ * approval -> approve/reject; handback -> resume/reject (+ provide-input,
97
+ * which appends a thread entry rather than resolving); review ->
98
+ * approve/reject. `view` opens the detail screen for every kind. */
99
+ var ACTIONS_BY_KIND = {
100
+ handback: ["resume", "input", "reject", "view"],
101
+ approval: ["approve", "reject", "view"],
102
+ review: ["approve", "reject", "view"],
103
+ };
104
+
105
+ var ACTION_LABEL = {
106
+ approve: "Approve",
107
+ reject: "Reject",
108
+ resume: "Resume",
109
+ input: "Provide input",
110
+ view: "View",
111
+ };
112
+
113
+ var RESOLVED_TOAST = {
114
+ approve: "approved",
115
+ reject: "rejected",
116
+ resume: "resumed",
117
+ };
118
+
119
+ function actionButtonClass(action) {
120
+ if (action === "approve" || action === "resume") return "btn-primary";
121
+ if (action === "reject") return "btn-ghost-w";
122
+ return "btn-ghost-c"; // input, view
123
+ }
124
+
125
+ // ------------------------------------------------------------- queue UI
126
+
127
+ function emptyStateHtml() {
128
+ return (
129
+ '<div class="empty-state">' +
130
+ '<h2 class="disp">NO PENDING ITEMS</h2>' +
131
+ '<div class="mono sub">[ waiting for handbacks&hellip; <span class="cursor-blink" style="color:var(--cyan)">&#9608;</span> ]</div>' +
132
+ '<p class="body-txt">Runs will hand back here when an agent needs you.</p>' +
133
+ "</div>"
134
+ );
135
+ }
136
+
137
+ function cardHtml(item) {
138
+ var meta = KIND_META[item.kind] || KIND_META.approval;
139
+ var actions = ACTIONS_BY_KIND[item.kind] || ["view"];
140
+ var buttons = actions
141
+ .map(function (action) {
142
+ return (
143
+ '<button type="button" class="' +
144
+ actionButtonClass(action) +
145
+ '" data-action="' +
146
+ action +
147
+ '" data-id="' +
148
+ escapeHtml(item.id) +
149
+ '">' +
150
+ ACTION_LABEL[action] +
151
+ "</button>"
152
+ );
153
+ })
154
+ .join("");
155
+
156
+ return (
157
+ '<article class="card" data-id="' +
158
+ escapeHtml(item.id) +
159
+ '">' +
160
+ '<div class="card-top">' +
161
+ '<span class="badge" style="border-color:' +
162
+ meta.color +
163
+ '"><span class="led ' +
164
+ meta.ledClass +
165
+ '"></span><span class="mono" style="color:' +
166
+ meta.color +
167
+ '">[' +
168
+ meta.label +
169
+ "]</span></span>" +
170
+ '<span class="mono eyebrow">[RUN ' +
171
+ escapeHtml(item.run) +
172
+ "]</span>" +
173
+ "</div>" +
174
+ '<h2 class="head card-title">' +
175
+ escapeHtml(item.step) +
176
+ "</h2>" +
177
+ '<div class="card-meta">' +
178
+ "<span>[JOURNEY] <strong>" +
179
+ escapeHtml(item.journey) +
180
+ "</strong></span>" +
181
+ '<span>[AGENT] <span class="agent">' +
182
+ escapeHtml(item.agent) +
183
+ "</span></span>" +
184
+ "</div>" +
185
+ '<div class="card-actions">' +
186
+ buttons +
187
+ "</div>" +
188
+ "</article>"
189
+ );
190
+ }
191
+
192
+ function renderQueue(items) {
193
+ els.queueCount.textContent = String(items.length).padStart(2, "0");
194
+ if (items.length === 0) {
195
+ els.queueList.innerHTML = emptyStateHtml();
196
+ return;
197
+ }
198
+ els.queueList.innerHTML = items.map(cardHtml).join("");
199
+ }
200
+
201
+ function loadQueue() {
202
+ return apiFetch("/api/inbox")
203
+ .then(function (res) {
204
+ if (!res.ok) throw new Error("inbox_fetch_failed");
205
+ return res.json();
206
+ })
207
+ .then(function (data) {
208
+ var items = Array.isArray(data) ? data : data && Array.isArray(data.items) ? data.items : [];
209
+ if (state.view === "queue") renderQueue(items);
210
+ return items;
211
+ })
212
+ .catch(function () {
213
+ // Best-effort poll — a transient failure should not crash the SPA.
214
+ });
215
+ }
216
+
217
+ // ------------------------------------------------------------ detail UI
218
+
219
+ function screenshotPlaceholderHtml() {
220
+ return (
221
+ '<div class="screenshot-placeholder">' +
222
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' +
223
+ '<rect x="3" y="3" width="18" height="18"></rect>' +
224
+ '<circle cx="8.5" cy="8.5" r="1.5"></circle>' +
225
+ '<path d="M21 15l-5-5L5 21"></path>' +
226
+ "</svg>" +
227
+ '<span class="mono">[ NO SNAPSHOT ]</span>' +
228
+ "</div>"
229
+ );
230
+ }
231
+
232
+ function threadHtml(thread) {
233
+ if (!thread || thread.length === 0) return '<div class="mono eyebrow">[ no messages ]</div>';
234
+ return (
235
+ '<div class="thread">' +
236
+ thread
237
+ .map(function (entry) {
238
+ var cls = entry.author === "human" ? "thread-entry human" : "thread-entry";
239
+ return (
240
+ '<div class="' +
241
+ cls +
242
+ '"><div class="who mono">[' +
243
+ escapeHtml(entry.author).toUpperCase() +
244
+ " &middot; " +
245
+ escapeHtml(formatTime(entry.at)) +
246
+ ']</div><div class="body-txt">' +
247
+ escapeHtml(entry.text) +
248
+ "</div></div>"
249
+ );
250
+ })
251
+ .join("") +
252
+ "</div>"
253
+ );
254
+ }
255
+
256
+ function findingsHtml(findings) {
257
+ if (!findings || findings.length === 0) return "";
258
+ return (
259
+ '<div class="section-label mono">[FINDINGS]</div>' +
260
+ '<div class="findings">' +
261
+ findings
262
+ .map(function (f) {
263
+ return (
264
+ '<div class="finding ' +
265
+ escapeHtml(f.severity) +
266
+ '"><div class="title">[' +
267
+ escapeHtml(f.severity).toUpperCase() +
268
+ "] " +
269
+ escapeHtml(f.title) +
270
+ "</div>" +
271
+ (f.evidence ? '<div class="evidence mono">' + escapeHtml(f.evidence) + "</div>" : "") +
272
+ "</div>"
273
+ );
274
+ })
275
+ .join("") +
276
+ "</div>"
277
+ );
278
+ }
279
+
280
+ /** Agent-controlled data — only ever render `targetUrl` as a real link
281
+ * when it's actually http(s). The CSP already blocks a `javascript:` URI
282
+ * from executing, but this is cheap belt-and-braces so a non-http(s)
283
+ * value (or garbage) never even becomes a clickable anchor. */
284
+ function isHttpUrl(url) {
285
+ return typeof url === "string" && /^https?:\/\//i.test(url);
286
+ }
287
+
288
+ function detailActionsHtml(item) {
289
+ var actions = (ACTIONS_BY_KIND[item.kind] || []).filter(function (a) {
290
+ return a !== "view"; // already viewing
291
+ });
292
+ return actions
293
+ .map(function (action) {
294
+ return (
295
+ '<button type="button" class="' +
296
+ actionButtonClass(action) +
297
+ '" data-action="' +
298
+ action +
299
+ '" data-id="' +
300
+ escapeHtml(item.id) +
301
+ '">' +
302
+ ACTION_LABEL[action] +
303
+ "</button>"
304
+ );
305
+ })
306
+ .join("");
307
+ }
308
+
309
+ function renderDetail(item) {
310
+ var meta = KIND_META[item.kind] || KIND_META.approval;
311
+ var glow = item.kind === "handback" ? "glow-c" : "glow-m";
312
+ var kindActions = ACTIONS_BY_KIND[item.kind] || [];
313
+ // Only handbacks have a resume/provide-input action — approval/review
314
+ // items have nothing to do with typed text, so don't show the control.
315
+ var showInput = kindActions.indexOf("input") !== -1 || kindActions.indexOf("resume") !== -1;
316
+
317
+ var left =
318
+ '<div>' +
319
+ '<span class="badge" style="border-color:' +
320
+ meta.color +
321
+ '"><span class="led ' +
322
+ meta.ledClass +
323
+ '"></span><span class="mono" style="color:' +
324
+ meta.color +
325
+ '">[' +
326
+ meta.label +
327
+ "]</span></span>" +
328
+ '<h2 class="disp ' +
329
+ glow +
330
+ ' detail-title">' +
331
+ escapeHtml(item.step) +
332
+ "</h2>" +
333
+ (item.targetUrl
334
+ ? '<div class="mono detail-url">' +
335
+ (isHttpUrl(item.targetUrl)
336
+ ? '<a href="' +
337
+ escapeHtml(item.targetUrl) +
338
+ '" target="_blank" rel="noopener noreferrer">' +
339
+ escapeHtml(item.targetUrl) +
340
+ "</a>"
341
+ : escapeHtml(item.targetUrl)) +
342
+ "</div>"
343
+ : "") +
344
+ '<div class="mono detail-meta">' +
345
+ "<span>[JOURNEY] <strong>" +
346
+ escapeHtml(item.journey) +
347
+ "</strong></span>" +
348
+ "<span>[RUN] <strong>" +
349
+ escapeHtml(item.run) +
350
+ "</strong></span>" +
351
+ '<span>[AGENT] <span class="agent" style="color:var(--cyan)">' +
352
+ escapeHtml(item.agent) +
353
+ "</span></span>" +
354
+ "<span>[CREATED] <strong>" +
355
+ escapeHtml(formatTime(item.createdAt)) +
356
+ "</strong></span>" +
357
+ "</div>" +
358
+ '<p class="body-txt detail-reason">' +
359
+ escapeHtml(item.reason) +
360
+ "</p>" +
361
+ findingsHtml(item.findings) +
362
+ '<div class="section-label mono">[THREAD]</div>' +
363
+ threadHtml(item.thread) +
364
+ (showInput
365
+ ? '<div class="section-label mono">[PROVIDE INPUT]</div>' +
366
+ '<textarea class="ta" id="input-textarea" rows="4" placeholder="type a reply or the value the agent needs…"></textarea>'
367
+ : "") +
368
+ '<div class="detail-actions">' +
369
+ detailActionsHtml(item) +
370
+ "</div>" +
371
+ "</div>";
372
+
373
+ var right =
374
+ '<div>' +
375
+ '<div class="browser-frame">' +
376
+ '<div class="browser-chrome">' +
377
+ '<span class="led" style="background:var(--red)"></span>' +
378
+ '<span class="led" style="background:var(--amber)"></span>' +
379
+ '<span class="led" style="background:var(--green)"></span>' +
380
+ '<span class="mono chrome-url">' +
381
+ escapeHtml(item.targetUrl || "no target") +
382
+ "</span>" +
383
+ "</div>" +
384
+ '<div class="screenshot-body" id="screenshot-body">' +
385
+ '<img id="screenshot-img" src="/api/inbox/' +
386
+ encodeURIComponent(item.id) +
387
+ '/screenshot" alt="Step snapshot" />' +
388
+ "</div>" +
389
+ "</div>" +
390
+ '<div class="mono screenshot-caption">[SNAPSHOT &middot; ' +
391
+ escapeHtml(item.step) +
392
+ "]</div>" +
393
+ "</div>";
394
+
395
+ els.detailRoot.innerHTML = left + right;
396
+
397
+ // Always attempt the fetch and rely on the `error` event (never inline
398
+ // `onerror=`, per CSP) to swap in the placeholder — covers both a
399
+ // missing file (server 404s) and `hasScreenshot: false` alike.
400
+ var img = document.getElementById("screenshot-img");
401
+ if (img) {
402
+ img.addEventListener("error", function onErr() {
403
+ var body = document.getElementById("screenshot-body");
404
+ if (body) body.innerHTML = screenshotPlaceholderHtml();
405
+ });
406
+ }
407
+ }
408
+
409
+ function openDetail(id) {
410
+ return apiFetch("/api/inbox/" + encodeURIComponent(id))
411
+ .then(function (res) {
412
+ if (res.status === 404) {
413
+ showToast("item no longer available");
414
+ return loadQueue().then(showQueueView);
415
+ }
416
+ if (!res.ok) throw new Error("detail_fetch_failed");
417
+ return res.json().then(function (item) {
418
+ state.view = "detail";
419
+ state.detailId = id;
420
+ renderDetail(item);
421
+ els.queueScreen.hidden = true;
422
+ els.detailScreen.hidden = false;
423
+ });
424
+ })
425
+ .catch(function () {
426
+ showToast("could not load item");
427
+ });
428
+ }
429
+
430
+ function showQueueView() {
431
+ state.view = "queue";
432
+ state.detailId = null;
433
+ els.detailScreen.hidden = true;
434
+ els.queueScreen.hidden = false;
435
+ loadQueue();
436
+ }
437
+
438
+ // ------------------------------------------------------------- actions
439
+
440
+ function performAction(id, action) {
441
+ if (action === "view") {
442
+ openDetail(id);
443
+ return;
444
+ }
445
+
446
+ // "Provide input" needs the textarea, which only exists on the detail
447
+ // screen — from a queue card, route there first instead of submitting
448
+ // an empty value.
449
+ if (action === "input" && (state.view !== "detail" || state.detailId !== id)) {
450
+ openDetail(id);
451
+ return;
452
+ }
453
+
454
+ var body = {};
455
+ if (action === "input") {
456
+ var ta = document.getElementById("input-textarea");
457
+ var text = ta ? ta.value.trim() : "";
458
+ if (!text) {
459
+ showToast("type something first");
460
+ return;
461
+ }
462
+ body.input = text;
463
+ } else if (action === "resume") {
464
+ var resumeTa = document.getElementById("input-textarea");
465
+ var resumeText = resumeTa ? resumeTa.value.trim() : "";
466
+ if (resumeText) body.input = resumeText;
467
+ }
468
+
469
+ apiFetch("/api/inbox/" + encodeURIComponent(id) + "/" + action, {
470
+ method: "POST",
471
+ headers: { "Content-Type": "application/json" },
472
+ body: JSON.stringify(body),
473
+ })
474
+ .then(function (res) {
475
+ if (res.status === 409) {
476
+ showToast("already resolved");
477
+ return refreshCurrentView();
478
+ }
479
+ if (res.status === 404) {
480
+ showToast("item no longer available");
481
+ return refreshCurrentView();
482
+ }
483
+ if (!res.ok) {
484
+ showToast("action failed");
485
+ return;
486
+ }
487
+ if (action === "input") {
488
+ showToast("input sent");
489
+ return openDetail(id);
490
+ }
491
+ showToast(RESOLVED_TOAST[action] || "done");
492
+ return showQueueView();
493
+ })
494
+ .catch(function () {
495
+ showToast("network error");
496
+ });
497
+ }
498
+
499
+ function refreshCurrentView() {
500
+ if (state.view === "detail" && state.detailId) return openDetail(state.detailId);
501
+ return showQueueView();
502
+ }
503
+
504
+ // -------------------------------------------------------------- events
505
+
506
+ els.queueList.addEventListener("click", function (ev) {
507
+ var target = ev.target;
508
+ while (target && target !== els.queueList) {
509
+ if (target.tagName === "BUTTON" && target.dataset.action) {
510
+ performAction(target.dataset.id, target.dataset.action);
511
+ return;
512
+ }
513
+ target = target.parentElement;
514
+ }
515
+ });
516
+
517
+ els.detailRoot.addEventListener("click", function (ev) {
518
+ var target = ev.target;
519
+ while (target && target !== els.detailRoot) {
520
+ if (target.tagName === "BUTTON" && target.dataset.action) {
521
+ performAction(target.dataset.id, target.dataset.action);
522
+ return;
523
+ }
524
+ target = target.parentElement;
525
+ }
526
+ });
527
+
528
+ els.backLink.addEventListener("click", function () {
529
+ showQueueView();
530
+ });
531
+
532
+ // --------------------------------------------------------------- init
533
+
534
+ showQueueView();
535
+ state.pollTimer = setInterval(function () {
536
+ loadQueue();
537
+ }, POLL_MS);
538
+ })();