pinokiod 8.0.31 → 8.0.33

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.
@@ -16,6 +16,8 @@ const FUNCTION_KEYS = new Set([...ACTION_KEYS, ...STATUS_KEYS])
16
16
  const BUILTIN_TOOL_ALIASES = {
17
17
  claude: "pinokio/run/plugin/claude",
18
18
  codex: "pinokio/run/plugin/codex",
19
+ grok: "pinokio/run/plugin/grok",
20
+ "grok-build": "pinokio/run/plugin/grok",
19
21
  antigravity: "pinokio/run/plugin/antigravity-cli",
20
22
  "antigravity-cli": "pinokio/run/plugin/antigravity-cli",
21
23
  "code/claude": "pinokio/run/plugin/claude",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.31",
3
+ "version": "8.0.33",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -4286,6 +4286,8 @@ class Server {
4286
4286
  home_sort: homeSortMode,
4287
4287
  running,
4288
4288
  notRunning,
4289
+ // Keep legacy custom views compatible without loading or rendering README files.
4290
+ readme: undefined,
4289
4291
  filepath,
4290
4292
  mode: null,
4291
4293
  kernel: this.kernel,
@@ -4317,6 +4319,8 @@ class Server {
4317
4319
  ishome: meta,
4318
4320
  running,
4319
4321
  notRunning,
4322
+ // Keep legacy custom views compatible without loading or rendering README files.
4323
+ readme: undefined,
4320
4324
  filepath,
4321
4325
  mode: null,
4322
4326
  kernel: this.kernel,
@@ -1,3 +1,4 @@
1
+ const headerCollapseOnly = document.currentScript?.hasAttribute("data-header-collapse-only");
1
2
  document.addEventListener("DOMContentLoaded", () => {
2
3
  // Logging disabled for production
3
4
  const log = () => {};
@@ -5,7 +6,7 @@ document.addEventListener("DOMContentLoaded", () => {
5
6
 
6
7
  const newWindowButton = document.querySelector("#new-window");
7
8
  const agent = document.body.getAttribute("data-agent");
8
- if (newWindowButton) {
9
+ if (newWindowButton && !headerCollapseOnly) {
9
10
  newWindowButton.addEventListener("click", (event) => {
10
11
  if (agent === "electron") {
11
12
  window.open("/", "_blank", "pinokio");
@@ -15,6 +16,85 @@ document.addEventListener("DOMContentLoaded", () => {
15
16
  });
16
17
  }
17
18
 
19
+ (() => {
20
+ const header = document.querySelector("body > header.navheader");
21
+ if (!header) return;
22
+
23
+ const body = document.body;
24
+ const row = header.querySelector("h1") || header;
25
+ const collapseMinWidth = (Number(window.PinokioAppMobileBreakpoint) || 768) + 1;
26
+ const collapseMedia = matchMedia(`(min-width: ${collapseMinWidth}px) and (hover: hover) and (pointer: fine)`);
27
+ const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
28
+
29
+ const toggle = document.createElement("button");
30
+ toggle.type = "button";
31
+ toggle.className = "btn2 header-collapse-toggle";
32
+ toggle.id = "header-collapse-toggle";
33
+ toggle.setAttribute("aria-label", "Hide toolbar");
34
+ toggle.setAttribute("title", "Hide toolbar");
35
+ toggle.innerHTML = '<div><i class="fa-solid fa-compress" aria-hidden="true"></i></div>';
36
+ row.insertBefore(toggle, row.querySelector("#new-window, #close-window"));
37
+
38
+ const edge = document.createElement("button");
39
+ edge.type = "button";
40
+ edge.className = "header-collapse-edge";
41
+ edge.setAttribute("aria-label", "Show toolbar");
42
+ edge.setAttribute("title", "Show toolbar");
43
+ edge.innerHTML = '<span aria-hidden="true"></span>';
44
+ body.appendChild(edge);
45
+
46
+ let collapseTimer;
47
+ let cueTimer;
48
+ const clearCue = () => {
49
+ clearTimeout(collapseTimer);
50
+ clearTimeout(cueTimer);
51
+ body.classList.remove("header-collapse-cue");
52
+ };
53
+ const setCollapsed = (collapsed) => {
54
+ body.classList.toggle("header-collapse-moving", collapseMedia.matches && !reducedMotion.matches);
55
+ body.classList.toggle("header-collapsed", collapsed);
56
+ header.inert = collapsed;
57
+ header.setAttribute("aria-hidden", collapsed ? "true" : "false");
58
+ document.dispatchEvent(new CustomEvent("pinokio:header-state", { detail: { collapsed, minimized: collapsed } }));
59
+ };
60
+ const finishMotion = (event) => {
61
+ if (event.target === header && event.propertyName === "grid-template-rows") {
62
+ body.classList.remove("header-collapse-moving");
63
+ }
64
+ };
65
+ header.addEventListener("transitionend", finishMotion);
66
+ header.addEventListener("transitioncancel", finishMotion);
67
+
68
+ toggle.addEventListener("click", (event) => {
69
+ if (!collapseMedia.matches) return;
70
+ clearCue();
71
+ body.classList.add("header-collapse-cue");
72
+ const keyboard = event.detail === 0;
73
+ collapseTimer = setTimeout(() => {
74
+ setCollapsed(true);
75
+ if (keyboard) edge.focus({ preventScroll: true });
76
+ cueTimer = setTimeout(() => body.classList.remove("header-collapse-cue"), reducedMotion.matches ? 360 : 520);
77
+ }, reducedMotion.matches ? 0 : 140);
78
+ });
79
+ edge.addEventListener("click", (event) => {
80
+ clearCue();
81
+ setCollapsed(false);
82
+ if (event.detail === 0) toggle.focus({ preventScroll: true });
83
+ });
84
+ const syncMedia = () => {
85
+ body.classList.toggle("header-collapse-enabled", collapseMedia.matches);
86
+ if (!collapseMedia.matches) {
87
+ clearCue();
88
+ if (body.classList.contains("header-collapsed")) setCollapsed(false);
89
+ body.classList.remove("header-collapse-moving");
90
+ }
91
+ };
92
+ collapseMedia.addEventListener("change", syncMedia);
93
+ syncMedia();
94
+ })();
95
+
96
+ if (headerCollapseOnly) return;
97
+
18
98
  const header = document.querySelector("header.navheader");
19
99
  const minimizeButton = document.querySelector("#minimize-header");
20
100
  const homeLink = header ? header.querySelector(".home") : null;
@@ -4650,6 +4650,77 @@ body.dark #dropdown-portal .dropdown-content .btn2 {
4650
4650
  background: white;
4651
4651
  }
4652
4652
 
4653
+ .header-collapse-edge {
4654
+ display: none;
4655
+ }
4656
+
4657
+ @media (min-width: 769px) and (hover: hover) and (pointer: fine) {
4658
+ body.header-collapse-enabled > header.navheader:not(.minimized) {
4659
+ display: grid !important;
4660
+ grid-template-rows: minmax(0, 1fr);
4661
+ overflow: visible;
4662
+ transition: grid-template-rows 240ms cubic-bezier(0.25, 1, 0.5, 1), padding-top 240ms cubic-bezier(0.25, 1, 0.5, 1), padding-bottom 240ms cubic-bezier(0.25, 1, 0.5, 1);
4663
+ }
4664
+ body.header-collapse-enabled > header.navheader:not(.minimized) > h1 {
4665
+ min-height: 0;
4666
+ overflow: visible;
4667
+ }
4668
+ body.header-collapse-moving > header.navheader:not(.minimized) > h1,
4669
+ body.header-collapsed > header.navheader:not(.minimized) > h1 {
4670
+ overflow: hidden;
4671
+ }
4672
+ body.header-collapsed > header.navheader:not(.minimized) {
4673
+ grid-template-rows: minmax(0, 0fr);
4674
+ padding-top: 0 !important;
4675
+ padding-bottom: 0 !important;
4676
+ border-top-width: 0 !important;
4677
+ border-bottom-width: 0 !important;
4678
+ overflow: hidden;
4679
+ box-shadow: none !important;
4680
+ pointer-events: none;
4681
+ }
4682
+ body.header-collapse-cue > .header-collapse-edge,
4683
+ body.header-collapsed > .header-collapse-edge {
4684
+ position: fixed;
4685
+ inset: 0 0 auto;
4686
+ z-index: 10000050;
4687
+ display: block;
4688
+ width: 100%;
4689
+ height: 8px;
4690
+ padding: 0;
4691
+ border: 0;
4692
+ background: transparent;
4693
+ cursor: pointer;
4694
+ -webkit-app-region: no-drag;
4695
+ }
4696
+ .header-collapse-edge > span {
4697
+ position: absolute;
4698
+ inset: 1px 0 auto;
4699
+ height: 3px;
4700
+ background: royalblue;
4701
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7), 0 2px 4px rgba(15, 23, 42, 0.35);
4702
+ opacity: 0;
4703
+ transform: translateY(-4px);
4704
+ transition: opacity 120ms, transform 160ms;
4705
+ }
4706
+ body.header-collapse-cue .header-collapse-edge > span {
4707
+ opacity: 1;
4708
+ transform: none;
4709
+ box-shadow: 0 0 10px 2px rgba(65, 105, 225, 0.7);
4710
+ }
4711
+ body.header-collapsed .header-collapse-edge:is(:hover, :focus-visible) > span {
4712
+ opacity: 0.92;
4713
+ transform: none;
4714
+ }
4715
+ }
4716
+
4717
+ body:not(.header-collapse-enabled) .header-collapse-toggle { display: none !important; }
4718
+
4719
+ @media (prefers-reduced-motion: reduce) {
4720
+ body.header-collapse-enabled > header.navheader:not(.minimized),
4721
+ .header-collapse-edge > span { transition-duration: 0.01ms !important; }
4722
+ }
4723
+
4653
4724
 
4654
4725
  header.navheader.minimized {
4655
4726
  position: fixed;
@@ -186,6 +186,7 @@ pre {
186
186
  <script src="/hotkeys.min.js"></script>
187
187
  <script src="/sweetalert2.js"></script>
188
188
  <script src="/common.js"></script>
189
+ <script src="/nav.js" data-header-collapse-only></script>
189
190
  <script src="/opener.js"></script>
190
191
  <script src="/highlight.js"></script>
191
192
  <script src="/highlight-js.js"></script>
@@ -15,6 +15,7 @@
15
15
  <% if (agent === "electron") { %>
16
16
  <link href="/electron.css" rel="stylesheet" />
17
17
  <% } %>
18
+ <script src="/nav.js" data-header-collapse-only></script>
18
19
  </head>
19
20
  <body class="<%= theme %>" data-agent="<%= agent %>">
20
21
  <header class="navheader grabbable">
@@ -2998,6 +2998,10 @@ const setHomeActionsDrawerOffset = () => {
2998
2998
  const top = header ? Math.max(0, Math.round(header.getBoundingClientRect().bottom)) : 0
2999
2999
  document.body.style.setProperty('--home-actions-drawer-top', `${top}px`)
3000
3000
  }
3001
+ const homeActionsDrawerHeader = document.querySelector('body.is-home > header.navheader')
3002
+ if (homeActionsDrawerHeader && typeof ResizeObserver === 'function') {
3003
+ new ResizeObserver(setHomeActionsDrawerOffset).observe(homeActionsDrawerHeader)
3004
+ }
3001
3005
  const setHomeActionsTriggerExpanded = (dialog, expanded) => {
3002
3006
  if (!dialog || !dialog.id) {
3003
3007
  return
@@ -6,3 +6,4 @@
6
6
  <script src="/notyq.js"></script>
7
7
  <script src="/Socket.js"></script>
8
8
  <script src="/common.js"></script>
9
+ <script src="/nav.js" data-header-collapse-only></script>
Binary file
@@ -0,0 +1,44 @@
1
+ module.exports = {
2
+ title: "Grok Build",
3
+ icon: "grok.png",
4
+ link: "https://github.com/xai-org/grok-build",
5
+ run: [{
6
+ when: "{{platform === 'win32'}}",
7
+ id: "run",
8
+ method: "shell.run",
9
+ params: {
10
+ shell: "{{kernel.path('bin/miniforge/Library/bin/bash.exe')}}",
11
+ conda: {
12
+ skip: true
13
+ },
14
+ message: {
15
+ _: [
16
+ "npx",
17
+ "-y",
18
+ "@xai-official/grok@latest",
19
+ "{{args.prompt || undefined}}"
20
+ ]
21
+ },
22
+ path: "{{args.cwd}}",
23
+ input: true,
24
+ buffer: 1024
25
+ }
26
+ }, {
27
+ when: "{{platform !== 'win32'}}",
28
+ id: "run",
29
+ method: "shell.run",
30
+ params: {
31
+ message: {
32
+ _: [
33
+ "npx",
34
+ "-y",
35
+ "@xai-official/grok@latest",
36
+ "{{args.prompt || undefined}}"
37
+ ]
38
+ },
39
+ path: "{{args.cwd}}",
40
+ input: true,
41
+ buffer: 1024
42
+ }
43
+ }]
44
+ }
Binary file
@@ -0,0 +1,20 @@
1
+ const grok = require("../grok/pinokio")
2
+
3
+ module.exports = {
4
+ ...grok,
5
+ title: "Grok Build Auto",
6
+ description: "Grok Build with tool permission prompts automatically approved unless explicitly denied.",
7
+ run: grok.run.map((step) => ({
8
+ ...step,
9
+ params: {
10
+ ...step.params,
11
+ message: {
12
+ _: [
13
+ ...step.params.message._.slice(0, 3),
14
+ "--always-approve",
15
+ ...step.params.message._.slice(3)
16
+ ]
17
+ }
18
+ }
19
+ }))
20
+ }
@@ -9,7 +9,7 @@ const Server = require('../server')
9
9
 
10
10
  const viewsPath = path.resolve(__dirname, '..', 'server', 'views')
11
11
 
12
- async function createDirectoryRendererFixture() {
12
+ async function createDirectoryRendererFixture({ legacyCustomViews = false } = {}) {
13
13
  const homedir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pinokio-directory-readme-'))
14
14
  const apiRoot = path.resolve(homedir, 'api')
15
15
  const appRoot = path.resolve(apiRoot, 'demo')
@@ -18,6 +18,23 @@ async function createDirectoryRendererFixture() {
18
18
  await fs.promises.writeFile(path.resolve(appRoot, 'README.md'), '# APP README SENTINEL\n')
19
19
  await fs.promises.writeFile(path.resolve(appRoot, 'keep.txt'), 'keep\n')
20
20
 
21
+ const customViewsPath = path.resolve(homedir, 'web', 'views')
22
+ if (legacyCustomViews) {
23
+ await fs.promises.mkdir(customViewsPath, { recursive: true })
24
+ const legacyTemplate = [
25
+ '<% if (readme) { %>',
26
+ '<div class="readme markdown-body"><%- readme %></div>',
27
+ '<% } %>',
28
+ '<% items.forEach((item) => { %>',
29
+ '<div data-name="<%= item.name %>"></div>',
30
+ '<% }) %>'
31
+ ].join('\n')
32
+ await Promise.all([
33
+ fs.promises.writeFile(path.resolve(customViewsPath, 'index.ejs'), legacyTemplate),
34
+ fs.promises.writeFile(path.resolve(customViewsPath, 'file_explorer.ejs'), legacyTemplate)
35
+ ])
36
+ }
37
+
21
38
  const renderer = Object.create(Server.prototype)
22
39
  renderer.kernel = {
23
40
  homedir,
@@ -60,7 +77,7 @@ async function createDirectoryRendererFixture() {
60
77
  renderer.getPeers = () => []
61
78
 
62
79
  const app = express()
63
- app.set('views', viewsPath)
80
+ app.set('views', [customViewsPath, viewsPath])
64
81
  app.set('view engine', 'ejs')
65
82
  app.get('/home', (req, res, next) => {
66
83
  renderer.render(req, res, [], {}).catch(next)
@@ -123,3 +140,27 @@ test('directory pages list README files without auto-rendering their contents',
123
140
  await fixture.close()
124
141
  }
125
142
  })
143
+
144
+ test('legacy custom directory views can reference readme without crashing', async () => {
145
+ const fixture = await createDirectoryRendererFixture({ legacyCustomViews: true })
146
+ try {
147
+ const responses = await Promise.all([
148
+ fetch(`${fixture.baseUrl}/home`),
149
+ fetch(`${fixture.baseUrl}/_api/demo`),
150
+ fetch(`${fixture.baseUrl}/api/demo`)
151
+ ])
152
+ const html = await Promise.all(responses.map((response) => response.text()))
153
+
154
+ responses.forEach((response, index) => {
155
+ assert.equal(response.status, 200, html[index])
156
+ })
157
+ assert.doesNotMatch(html[0], /ROOT README SENTINEL/)
158
+ for (const page of html.slice(1)) {
159
+ assert.doesNotMatch(page, /APP README SENTINEL/)
160
+ assert.match(page, /data-name="README\.md"/)
161
+ assert.match(page, /data-name="keep\.txt"/)
162
+ }
163
+ } finally {
164
+ await fixture.close()
165
+ }
166
+ })
@@ -0,0 +1,78 @@
1
+ const assert = require("node:assert/strict")
2
+ const fs = require("node:fs")
3
+ const path = require("node:path")
4
+ const test = require("node:test")
5
+
6
+ const grok = require("../system/plugin/grok/pinokio")
7
+ const grokAuto = require("../system/plugin/grok-auto/pinokio")
8
+
9
+ test("Grok Build exposes a minimal npx-backed terminal plugin", () => {
10
+ assert.equal(grok.title, "Grok Build")
11
+ assert.equal(grok.icon, "grok.png")
12
+ assert.equal(grok.link, "https://github.com/xai-org/grok-build")
13
+ assert.equal(fs.existsSync(path.join(__dirname, "..", "system", "plugin", "grok", grok.icon)), true)
14
+
15
+ for (const action of ["install", "update", "uninstall", "installed"]) {
16
+ assert.equal(Object.prototype.hasOwnProperty.call(grok, action), false)
17
+ }
18
+
19
+ assert.equal(grok.run.length, 2)
20
+ for (const step of grok.run) {
21
+ assert.equal(step.id, "run")
22
+ assert.equal(step.method, "shell.run")
23
+ assert.deepEqual(step.params.message, {
24
+ _: [
25
+ "npx",
26
+ "-y",
27
+ "@xai-official/grok@latest",
28
+ "{{args.prompt || undefined}}"
29
+ ]
30
+ })
31
+ assert.equal(step.params.path, "{{args.cwd}}")
32
+ assert.equal(step.params.input, true)
33
+ assert.equal(step.params.buffer, 1024)
34
+ }
35
+
36
+ assert.match(grok.run[0].when, /win32/)
37
+ assert.match(grok.run[0].params.shell, /bash\.exe/)
38
+ assert.equal(grok.run[0].params.conda.skip, true)
39
+ assert.equal(Object.prototype.hasOwnProperty.call(grok.run[1].params, "shell"), false)
40
+ })
41
+
42
+ test("Grok Build Auto shares the base wrapper and enables always-approve", () => {
43
+ assert.equal(grokAuto.title, "Grok Build Auto")
44
+ assert.equal(grokAuto.icon, grok.icon)
45
+ assert.equal(grokAuto.link, grok.link)
46
+ assert.match(grokAuto.description, /automatically approved/)
47
+ assert.equal(fs.existsSync(path.join(__dirname, "..", "system", "plugin", "grok-auto", grokAuto.icon)), true)
48
+
49
+ for (const action of ["install", "update", "uninstall", "installed"]) {
50
+ assert.equal(Object.prototype.hasOwnProperty.call(grokAuto, action), false)
51
+ }
52
+
53
+ assert.equal(grokAuto.run.length, grok.run.length)
54
+ for (let index = 0; index < grok.run.length; index += 1) {
55
+ const baseStep = grok.run[index]
56
+ const autoStep = grokAuto.run[index]
57
+
58
+ assert.equal(autoStep.when, baseStep.when)
59
+ assert.equal(autoStep.id, baseStep.id)
60
+ assert.equal(autoStep.method, baseStep.method)
61
+ assert.equal(autoStep.params.path, baseStep.params.path)
62
+ assert.equal(autoStep.params.input, baseStep.params.input)
63
+ assert.equal(autoStep.params.buffer, baseStep.params.buffer)
64
+ assert.equal(autoStep.params.shell, baseStep.params.shell)
65
+ assert.deepEqual(autoStep.params.conda, baseStep.params.conda)
66
+ assert.deepEqual(autoStep.params.message, {
67
+ _: [
68
+ "npx",
69
+ "-y",
70
+ "@xai-official/grok@latest",
71
+ "--always-approve",
72
+ "{{args.prompt || undefined}}"
73
+ ]
74
+ })
75
+ }
76
+
77
+ assert.equal(grok.run.every((step) => !step.params.message._.includes("--always-approve")), true)
78
+ })
@@ -0,0 +1,33 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs/promises')
3
+ const path = require('node:path')
4
+ const test = require('node:test')
5
+
6
+ const root = path.resolve(__dirname, '..')
7
+
8
+ test('header collapse preserves expanded and legacy minimized header styles', async () => {
9
+ const style = await fs.readFile(path.join(root, 'server/public/style.css'), 'utf8')
10
+ const expanded = style.match(/body\.header-collapse-enabled > header\.navheader:not\(\.minimized\) \{[\s\S]*?\n \}/)?.[0] || ''
11
+ const collapsed = style.match(/body\.header-collapsed > header\.navheader:not\(\.minimized\) \{[\s\S]*?\n \}/)?.[0] || ''
12
+
13
+ assert.match(expanded, /grid-template-rows: minmax\(0, 1fr\)/)
14
+ assert.doesNotMatch(expanded, /(?:height|padding|border): .*!important/)
15
+ assert.match(collapsed, /grid-template-rows: minmax\(0, 0fr\)/)
16
+ assert.match(collapsed, /padding-top: 0 !important/)
17
+ assert.match(collapsed, /padding-bottom: 0 !important/)
18
+ })
19
+
20
+ test('collapse-only script includes do not activate legacy navigation behavior', async () => {
21
+ const [nav, connect, create, common] = await Promise.all([
22
+ fs.readFile(path.join(root, 'server/public/nav.js'), 'utf8'),
23
+ fs.readFile(path.join(root, 'server/views/connect/x.ejs'), 'utf8'),
24
+ fs.readFile(path.join(root, 'server/views/create.ejs'), 'utf8'),
25
+ fs.readFile(path.join(root, 'server/views/partials/app_common_scripts.ejs'), 'utf8')
26
+ ])
27
+ const controller = nav.slice(nav.indexOf('(() => {'), nav.indexOf('if (headerCollapseOnly) return;'))
28
+
29
+ assert.ok([connect, create, common].every((source) => /nav\.js" data-header-collapse-only/.test(source)))
30
+ assert.match(nav, /document\.currentScript\?\.hasAttribute\("data-header-collapse-only"\)/)
31
+ assert.match(nav, /if \(headerCollapseOnly\) return;/)
32
+ assert.doesNotMatch(controller, /localStorage|sessionStorage/)
33
+ })
@@ -18,6 +18,14 @@ test("resolveLauncherPluginSelection returns app-dev plugin query paths", () =>
18
18
  PluginSources.resolveLauncherPluginSelection("codex-auto"),
19
19
  "/plugin/codex-auto/pinokio.js"
20
20
  )
21
+ assert.strictEqual(
22
+ PluginSources.resolveLauncherPluginSelection("grok"),
23
+ "/pinokio/run/plugin/grok/pinokio.js"
24
+ )
25
+ assert.strictEqual(
26
+ PluginSources.resolveLauncherPluginSelection("grok-build"),
27
+ "/pinokio/run/plugin/grok/pinokio.js"
28
+ )
21
29
  assert.strictEqual(
22
30
  PluginSources.resolveLauncherPluginSelection("plugin/local-tool"),
23
31
  "/plugin/local-tool/pinokio.js"
@@ -208,10 +216,20 @@ module.exports = {
208
216
  const systemPlugins = menu.filter((item) => item.source === "system")
209
217
  const localPlugins = menu.filter((item) => item.source === "local")
210
218
 
211
- assert.strictEqual(systemPlugins.length, 12)
219
+ assert.strictEqual(systemPlugins.length, 14)
212
220
  assert.ok(systemPlugins.every((item) => item.system === true))
213
221
  assert.ok(systemPlugins.every((item) => item.href.startsWith("/pinokio/run/plugin/")))
214
222
  assert.ok(systemPlugins.every((item) => item.image.startsWith("/pinokio/asset/plugin/")))
223
+ assert.ok(systemPlugins.some((item) => (
224
+ item.title === "Grok Build" &&
225
+ item.href === "/pinokio/run/plugin/grok/pinokio.js" &&
226
+ item.image === "/pinokio/asset/plugin/grok/grok.png"
227
+ )))
228
+ assert.ok(systemPlugins.some((item) => (
229
+ item.title === "Grok Build Auto" &&
230
+ item.href === "/pinokio/run/plugin/grok-auto/pinokio.js" &&
231
+ item.image === "/pinokio/asset/plugin/grok-auto/grok.png"
232
+ )))
215
233
 
216
234
  assert.deepStrictEqual(localPlugins.map((item) => item.title), ["Local Tool"])
217
235
  assert.strictEqual(localPlugins[0].href, "/run/plugin/local-tool/pinokio.js")