pinokiod 7.0.6 → 7.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.0.6",
3
+ "version": "7.0.9",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -40,7 +40,7 @@ Failure handling:
40
40
 
41
41
  Use direct `pterm` commands for control-plane operations:
42
42
 
43
- `pterm search`, `pterm status`, `pterm run`, `pterm logs`, `pterm upload`, `pterm which`, `pterm stars`, `pterm star` / `pterm unstar`, `pterm registry search`, `pterm download`
43
+ `pterm search`, `pterm status`, `pterm run`, `pterm open`, `pterm logs`, `pterm upload`, `pterm which`, `pterm stars`, `pterm star` / `pterm unstar`, `pterm registry search`, `pterm download`
44
44
 
45
45
  Do not run update commands from this skill.
46
46
  Once a Pinokio-managed app is selected, treat `pterm` and the launcher-managed interfaces as the source of truth for lifecycle and execution. Do not switch to repo-local CLIs or bundled app binaries unless the user explicitly asks for CLI mode.
@@ -50,8 +50,8 @@ Once a Pinokio-managed app is selected, treat `pterm` and the launcher-managed i
50
50
  Follow these sections in order:
51
51
  1. Use Search App first.
52
52
  2. Only use Registry Fallback if Search App found no suitable installed app and the user approved it.
53
- 3. Then use Run App.
54
- 4. Then use API Call Strategy if the app exposes an automatable API.
53
+ 3. Then use Launch App.
54
+ 4. Then use Using Apps if the app exposes an automatable API.
55
55
  5. Only use Parallel Mode when the user explicitly asks to use multiple apps or multiple machines in parallel.
56
56
 
57
57
  ### 1. Search App
@@ -88,7 +88,7 @@ Follow these sections in order:
88
88
  - more recent `last_launch_at` (if available)
89
89
  - higher `score`
90
90
  - If the top candidate is not clearly better than alternatives, ask user once with top 3 candidates.
91
- - If a suitable installed app is found, select it and continue to Run App.
91
+ - If a suitable installed app is found, select it and continue to Launch App.
92
92
  - Search results may include apps from other reachable Pinokio machines:
93
93
  - prefer the canonical `ref` field when it exists
94
94
  - `ref` uses the form `pinokio://<host>:<port>/<scope>/<id>`
@@ -108,7 +108,7 @@ Follow these sections in order:
108
108
  - then run the downloaded app with `pterm run <local_app_path_or_name>`
109
109
  - Do not use `pterm run <url>` for the registry flow.
110
110
 
111
- ### 3. Run App
111
+ ### 3. Launch App
112
112
 
113
113
  - Once you have a selected app, use `pterm status`.
114
114
  - Poll every 2s.
@@ -145,6 +145,17 @@ Follow these sections in order:
145
145
  - use `ready_url` by default when it exists and is caller-usable
146
146
  - if `ready_url` is missing, or it fails because the client cannot access loopback, and `external_ready_urls` exists, try those URLs in order
147
147
  - missing `external_ready_urls` is normal; it usually means network sharing is off
148
+ - If the user explicitly wants to open the app UI or open a web page in a browser or popup window:
149
+ - use `pterm open <url>`
150
+ - only do this for explicit viewing/manual interaction requests, not for normal API automation
151
+ - use a caller-usable app URL:
152
+ - `ready_url` when it exists and the current machine can reach it
153
+ - otherwise the first usable entry from `external_ready_urls`
154
+ - default behavior should be popup-preferred:
155
+ - on a desktop Pinokio node, it opens a Pinokio popup window
156
+ - on a server-only or minimal node, it falls back to the system browser automatically
157
+ - use `--surface browser` only when the user explicitly wants the system browser instead of the default popup-preferred behavior
158
+ - if popup sizing matters and the user does not specify one, default to `--preset center-medium`
148
159
  - Failure criteria:
149
160
  - timeout before success
150
161
  - app drops back to `offline` during startup after a run attempt
@@ -152,27 +163,89 @@ Follow these sections in order:
152
163
  - on failure, fetch `pterm logs <ref> --tail 200` when `ref` exists, otherwise `pterm logs <app_id> --tail 200`, and return:
153
164
  - raw log tail
154
165
  - short diagnosis
166
+ - After successful task completion:
167
+ - do not stop or shut down the app unless the user explicitly asks
168
+ - prefer leaving a successfully running app online for reuse
155
169
 
156
- ### 4. API Call Strategy (generated once, reused)
157
- - Resolve path roots before writing agent-owned files:
158
- - prefer the current working directory when it is the active writable task/workspace folder
159
- - resolve `PINOKIO_HOME` with `pterm home` when fallback global storage is needed
160
- - Generated client location:
161
- - local default: `<current_working_directory>/pinokio_agent/clients/<app_id>/<operation>.<ext>`
162
- - fallback: `<PINOKIO_HOME>/agents/clients/<app_id>/<operation>.<ext>`
163
- - Output location:
164
- - local default: `<current_working_directory>/pinokio_agent/output/<app_id>/...`
165
- - fallback: `<PINOKIO_HOME>/agents/output/<app_id>/...`
166
- - First run for `<app_id>/<operation>`:
170
+ ### 4. Using Apps
171
+ - Create or reuse one app-specific skill folder for the selected app:
172
+ - local default: `<current_working_directory>/pinokio_agent/skills/<scope>/<app_id>/`
173
+ - fallback: `<PINOKIO_HOME>/agents/skills/<scope>/<app_id>/`
174
+ - App-specific skill folder structure:
175
+ - `SKILL.md`: short instructions for how to use this app
176
+ - include frontmatter with only:
177
+ - `name`: short stable app-specific skill name using lowercase letters, digits, and hyphens only; derive it from a normalized app identity such as `<scope>-<app_id>` and keep it under 64 characters
178
+ - `description`: one clear sentence describing what this app-specific skill does and when it should be used
179
+ - optional `clients/`: reusable local client files
180
+ - optional `references/`: saved API artifacts such as OpenAPI specs, Gradio config, or concise notes
181
+ - outputs: `<app_skill_folder>/output/<target_host>/...`
182
+
183
+ - Reuse an existing app-specific skill when possible:
184
+ - if `<app_skill_folder>/SKILL.md` exists and still describes the app's current API correctly, read it first and follow it
185
+ - if the folder already contains a reusable client for the needed operation and it still works against the current app API, reuse that client
186
+ - if the folder has no `SKILL.md`, or the saved instructions or saved client no longer match the current API, rediscover the app interface and rewrite the app-specific skill folder
187
+
188
+ - If rediscovery is needed, choose exactly one usage mode:
189
+ - Mode A: use the app directly
190
+ - Mode B: reuse or generate a reusable client
191
+ - Use Mode A only if all of these are true:
192
+ - the running app already exposes a documented HTTP API you can call directly
193
+ - the task is simple enough to complete with one or a few direct requests
194
+ - saving a client file would not make later work meaningfully easier
195
+ - Standard callable API examples:
196
+ - OpenAPI / Swagger endpoints
197
+ - FastAPI docs
198
+ - Gradio API
199
+ - other documented standard HTTP interfaces
200
+ - Otherwise use Mode B.
201
+
202
+ - Shared rules for both modes:
203
+ - prefer documented/public APIs exposed by the running launcher
204
+ - choose a base URL that the current machine can actually reach:
205
+ - use `ready_url` when it exists and the current machine can reach it
206
+ - otherwise use `external_ready_urls` in order
207
+ - if the task needs remote filesystem paths, first run `pterm upload <ref> <file...>` and use the returned remote paths for that target only
208
+ - never reuse a remote uploaded path from one target on another target
209
+ - keep `<app_skill_folder>/SKILL.md` concise and operational
210
+ - put bulky raw artifacts in `references/` instead of bloating `SKILL.md`
211
+
212
+ - Mode A: use the app directly
213
+ - execute the needed requests directly from the current machine
214
+ - update `<app_skill_folder>/SKILL.md` to record:
215
+ - what callable API surface exists
216
+ - how to choose the base URL
217
+ - required request inputs and outputs
218
+ - whether remote upload is needed for path-based tasks
219
+ - do not create a reusable client in this mode unless the workflow later becomes repetitive or multi-step enough to justify Mode B
220
+
221
+ - Mode B: reuse or generate a reusable client
222
+ - if no matching client exists under `<app_skill_folder>/clients/` for the needed operation, generate one
223
+ - if a client exists but the contract no longer matches, regenerate it only for:
224
+ - 404/405 endpoint mismatch
225
+ - 400/422 payload/schema mismatch
226
+ - auth/header mismatch
167
227
  - inspect docs/code to infer endpoint + payload
168
- - generate minimal HTTP client file (`js`/`py`/`sh`)
169
- - Later runs:
170
- - reuse existing generated client file directly
171
- - Regenerate only if request indicates contract mismatch:
172
- - 404/405 endpoint mismatch
173
- - 400/422 payload/schema mismatch
174
- - auth/header mismatch
175
- - Prefer documented/public app APIs exposed by the running launcher.
228
+ - generate a minimal cross-platform HTTP client in `py` or `js`
229
+ - do not use Bash, PowerShell, or other machine-specific shell scripts for reusable clients unless the user explicitly asks for a machine-local one-off script
230
+ - generated clients run on the current machine; do not copy or write them onto the remote machine
231
+ - organize clients by app and operation, not by host
232
+ - example: if local Cropper and remote Cropper use the same endpoint and payload shape for `trim`, reuse one client such as `clients/trim.py`
233
+ - do not create a second client file only because the target host changed
234
+ - pass per-run values into the client at execution time:
235
+ - a base URL that the current machine can actually reach
236
+ - uploaded remote file paths when needed
237
+ - per-run auth headers/cookies if required
238
+ - never hardcode per-run values into the saved client:
239
+ - `ref`
240
+ - base URL / host / port
241
+ - uploaded temp file paths
242
+ - per-run auth tokens or cookies
243
+ - update `<app_skill_folder>/SKILL.md` to record:
244
+ - which client file to use for each operation
245
+ - required runtime arguments
246
+ - expected outputs
247
+ - when the client should be regenerated
248
+
176
249
  - Do not execute the app's internal Python/Node/bundled CLI as a fallback when `pterm` has already selected a launcher-managed app.
177
250
  - If no automatable API exists after the app is running, report that clearly instead of bypassing the launcher with an internal CLI.
178
251
 
@@ -193,9 +266,15 @@ Follow these sections in order:
193
266
  - prefer `ready` apps first
194
267
  - then `running` apps
195
268
  - then offline apps if more targets are still needed
269
+ - If subagents are available, prefer one subagent per selected target.
270
+ - the main agent should do the search, choose the targets, and aggregate the final results
271
+ - each subagent should own exactly one target `ref` when it exists, otherwise one `app_id`
272
+ - each subagent should run status/run/logs/upload only for its own target
196
273
  - Run and monitor each selected target independently.
197
274
  - Keep outputs labeled by target `ref` when it exists, otherwise `app_id`.
198
275
  - For remote path-based tasks, run `pterm upload <ref> <file...>` separately for each remote target when `ref` exists, otherwise fall back to `app_id`.
276
+ - Do not reuse one target's uploaded remote file path for another target.
277
+ - If subagents are unavailable, keep the same per-target separation and run the targets sequentially.
199
278
 
200
279
  ## Behavior Rules
201
280
 
@@ -204,8 +283,8 @@ Follow these sections in order:
204
283
  - Do not rewrite launcher files unless user explicitly asked.
205
284
  - When a task needs a local executable such as `python`, prefer resolving it with `pterm which <command>` before falling back to generic shell discovery.
206
285
  - Prefer returning full logs over brittle deterministic error parsing.
207
- - REST endpoints may be used for diagnostics only when pterm is unavailable; do not claim full install/launch lifecycle completion without compatible pterm commands.
208
- - Do not keep searching after app selection; move to status/run.
286
+ - Pinokio control-plane REST endpoints may be used for diagnostics only when `pterm` is unavailable; do not claim full install/launch lifecycle completion without compatible `pterm` commands.
287
+ - Do not keep searching after app selection; move to Launch App.
209
288
  - Do not assume `external_ready_urls` exists; localhost-only apps are normal.
210
289
  - Do not conflate loopback access failure, sandbox denial, or missing permission with "Pinokio is not running" or "`pterm` is not installed."
211
290
  - On `pterm` permission failure, prefer asking for permission over asking the user to manually run commands.
@@ -215,8 +294,14 @@ Follow these sections in order:
215
294
 
216
295
  User: "Launch FaceFusion"
217
296
 
218
- 1. Use Search App and then Run App as usual.
297
+ 1. Use Search App and then Launch App as usual.
219
298
  2. If launcher menu has no explicit default item, infer ordered selectors from the current launcher menu.
220
299
  3. Run:
221
- `pterm run <app_path> --default 'run.js?mode=Default' --default run.js --default install.js`
222
- 4. Poll `pterm status <app_id>` until ready.
300
+ - if `ref` exists:
301
+ `pterm run <ref> --default 'run.js?mode=Default' --default run.js --default install.js`
302
+ - otherwise:
303
+ `pterm run <app_path> --default 'run.js?mode=Default' --default run.js --default install.js`
304
+ 4. Poll:
305
+ - `pterm status <ref>` when `ref` exists
306
+ - otherwise `pterm status <app_id>`
307
+ until ready.
package/server/index.js CHANGED
@@ -46,6 +46,7 @@ const NOTIFICATION_SOUND_EXTENSIONS = new Set(['.aac', '.flac', '.m4a', '.mp3',
46
46
  const LOG_STREAM_INITIAL_BYTES = 512 * 1024
47
47
  const LOG_STREAM_KEEPALIVE_MS = 25000
48
48
  const DEFAULT_REGISTRY_URL = 'https://beta.pinokio.co'
49
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]'])
49
50
 
50
51
  const ex = fn => (req, res, next) => {
51
52
  Promise.resolve(fn(req, res, next)).catch(next);
@@ -8592,6 +8593,191 @@ class Server {
8592
8593
  Util.openURL(req.body.url)
8593
8594
  res.json({ success: true })
8594
8595
  }))
8596
+ this.app.post("/pinokio/open", ex(async (req, res) => {
8597
+ const url = typeof req.body.url === 'string' ? req.body.url.trim() : ''
8598
+ if (!url) {
8599
+ res.status(400).json({ error: 'Missing url' })
8600
+ return
8601
+ }
8602
+ const normalizeSurface = (value) => {
8603
+ return String(value || '').trim().toLowerCase() === 'browser' ? 'browser' : 'popup'
8604
+ }
8605
+ const normalizePreset = (value) => {
8606
+ const normalized = String(value || '').trim().toLowerCase()
8607
+ if (normalized === 'center-small' || normalized === 'center-medium' || normalized === 'center-large' || normalized === 'fullscreen') {
8608
+ return normalized
8609
+ }
8610
+ return 'center-medium'
8611
+ }
8612
+ const defaultPeerPort = () => {
8613
+ const rawPort = Number.parseInt(String(this.kernel?.peer?.default_port || this.kernel?.server_port || this.port || DEFAULT_PORT), 10)
8614
+ return Number.isFinite(rawPort) && rawPort > 0 ? rawPort : DEFAULT_PORT
8615
+ }
8616
+ const currentPeerHost = () => {
8617
+ return this.kernel && this.kernel.peer && this.kernel.peer.host
8618
+ ? String(this.kernel.peer.host).trim()
8619
+ : ''
8620
+ }
8621
+ const currentPeerName = () => {
8622
+ return this.kernel && this.kernel.peer && this.kernel.peer.name
8623
+ ? String(this.kernel.peer.name).trim()
8624
+ : ''
8625
+ }
8626
+ const isLocalPeerToken = (value) => {
8627
+ const normalized = String(value || '').trim().toLowerCase()
8628
+ return !!normalized && (LOOPBACK_HOSTS.has(normalized) || normalized === currentPeerHost().toLowerCase() || normalized === currentPeerName().toLowerCase())
8629
+ }
8630
+ const resolvePeerTarget = (rawValue) => {
8631
+ const localHost = currentPeerHost() || '127.0.0.1'
8632
+ const localName = currentPeerName() || localHost
8633
+ const fallbackPort = defaultPeerPort()
8634
+ if (rawValue === undefined || rawValue === null || String(rawValue).trim() === '') {
8635
+ return {
8636
+ local: true,
8637
+ host: localHost,
8638
+ port: fallbackPort,
8639
+ name: localName
8640
+ }
8641
+ }
8642
+ const trimmed = String(rawValue).trim()
8643
+ let hostOrName = trimmed
8644
+ let port = fallbackPort
8645
+ const separatorIndex = trimmed.lastIndexOf(':')
8646
+ if (separatorIndex > 0 && separatorIndex < trimmed.length - 1) {
8647
+ const possiblePort = trimmed.slice(separatorIndex + 1).trim()
8648
+ if (/^\d+$/.test(possiblePort)) {
8649
+ hostOrName = trimmed.slice(0, separatorIndex).trim()
8650
+ const explicitPort = Number.parseInt(possiblePort, 10)
8651
+ if (Number.isFinite(explicitPort) && explicitPort > 0) {
8652
+ port = explicitPort
8653
+ }
8654
+ }
8655
+ }
8656
+ if (!hostOrName) {
8657
+ return {
8658
+ error: 'Invalid peer'
8659
+ }
8660
+ }
8661
+ if (isLocalPeerToken(hostOrName)) {
8662
+ return {
8663
+ local: true,
8664
+ host: localHost,
8665
+ port,
8666
+ name: localName
8667
+ }
8668
+ }
8669
+ if (this.kernel && this.kernel.peer && this.kernel.peer.info && typeof this.kernel.peer.info === 'object') {
8670
+ for (const [host, info] of Object.entries(this.kernel.peer.info)) {
8671
+ const peerName = info && info.name ? String(info.name).trim() : ''
8672
+ if (host === hostOrName || (peerName && peerName === hostOrName)) {
8673
+ return {
8674
+ local: host === localHost,
8675
+ host,
8676
+ port,
8677
+ name: peerName || host
8678
+ }
8679
+ }
8680
+ }
8681
+ }
8682
+ if (LOOPBACK_HOSTS.has(hostOrName.toLowerCase())) {
8683
+ return {
8684
+ local: true,
8685
+ host: localHost,
8686
+ port,
8687
+ name: localName
8688
+ }
8689
+ }
8690
+ if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostOrName)) {
8691
+ return {
8692
+ local: false,
8693
+ host: hostOrName,
8694
+ port,
8695
+ name: hostOrName
8696
+ }
8697
+ }
8698
+ return {
8699
+ error: `Unknown peer: ${trimmed}`
8700
+ }
8701
+ }
8702
+ const requestedSurface = normalizeSurface(req.body.surface)
8703
+ const requestedPreset = normalizePreset(req.body.preset)
8704
+ const peerTarget = resolvePeerTarget(req.body.peer)
8705
+ if (peerTarget.error) {
8706
+ res.status(400).json({ error: peerTarget.error })
8707
+ return
8708
+ }
8709
+ if (!peerTarget.local) {
8710
+ try {
8711
+ const response = await axios.post(`http://${peerTarget.host}:${peerTarget.port}/pinokio/open`, {
8712
+ url,
8713
+ surface: requestedSurface,
8714
+ preset: requestedPreset
8715
+ }, {
8716
+ timeout: 5000,
8717
+ headers: {
8718
+ 'x-pinokio-peer': '1'
8719
+ }
8720
+ })
8721
+ res.json({
8722
+ ...(response.data && typeof response.data === 'object' ? response.data : { success: true }),
8723
+ peer: {
8724
+ host: peerTarget.host,
8725
+ port: peerTarget.port,
8726
+ name: peerTarget.name || peerTarget.host,
8727
+ local: false
8728
+ }
8729
+ })
8730
+ return
8731
+ } catch (error) {
8732
+ const remoteMessage = error && error.response && error.response.data && error.response.data.error
8733
+ ? error.response.data.error
8734
+ : (error && error.message ? error.message : 'Peer open failed')
8735
+ res.status(502).json({
8736
+ error: remoteMessage,
8737
+ peer: {
8738
+ host: peerTarget.host,
8739
+ port: peerTarget.port,
8740
+ name: peerTarget.name || peerTarget.host,
8741
+ local: false
8742
+ }
8743
+ })
8744
+ return
8745
+ }
8746
+ }
8747
+ let result
8748
+ if (this.browser && typeof this.browser.open === 'function') {
8749
+ result = await Promise.resolve(this.browser.open({
8750
+ url,
8751
+ surface: requestedSurface,
8752
+ preset: requestedPreset
8753
+ }))
8754
+ }
8755
+ if (!result || result.ok === false) {
8756
+ Util.openURL(url)
8757
+ result = {
8758
+ ok: true,
8759
+ surface_used: 'browser'
8760
+ }
8761
+ }
8762
+ const surfaceUsed = typeof result.surface_used === 'string' && result.surface_used
8763
+ ? result.surface_used
8764
+ : 'browser'
8765
+ res.json({
8766
+ success: true,
8767
+ url,
8768
+ requested_surface: requestedSurface,
8769
+ surface_used: surfaceUsed,
8770
+ preset_used: surfaceUsed === 'popup'
8771
+ ? (typeof result.preset_used === 'string' && result.preset_used ? result.preset_used : requestedPreset)
8772
+ : null,
8773
+ peer: {
8774
+ host: peerTarget.host,
8775
+ port: peerTarget.port,
8776
+ name: peerTarget.name || peerTarget.host,
8777
+ local: true
8778
+ }
8779
+ })
8780
+ }))
8595
8781
  this.app.post("/openfs", ex(async (req, res) => {
8596
8782
  //Util.openfs(req.body.path, req.body.mode)
8597
8783
  if (req.body.name) {