pinokiod 7.0.3 → 7.0.5
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 +1 -1
- package/prototype/system/SKILL_PINOKIO.md +39 -9
- package/server/routes/apps.js +167 -1
package/package.json
CHANGED
|
@@ -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 which`, `pterm stars`, `pterm star` / `pterm unstar`, `pterm registry search`, `pterm download`
|
|
43
|
+
`pterm search`, `pterm status`, `pterm run`, `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.
|
|
@@ -52,6 +52,7 @@ Follow these sections in order:
|
|
|
52
52
|
2. Only use Registry Fallback if Search App found no suitable installed app and the user approved it.
|
|
53
53
|
3. Then use Run App.
|
|
54
54
|
4. Then use API Call Strategy if the app exposes an automatable API.
|
|
55
|
+
5. Only use Parallel Mode when the user explicitly asks to use multiple apps or multiple machines in parallel.
|
|
55
56
|
|
|
56
57
|
### 1. Search App
|
|
57
58
|
|
|
@@ -76,6 +77,8 @@ Follow these sections in order:
|
|
|
76
77
|
- relevant apps with `ready=true`
|
|
77
78
|
- otherwise relevant apps with `running=true`
|
|
78
79
|
- otherwise relevant offline apps
|
|
80
|
+
- This runtime priority applies across all relevant candidates, not just the same app on different machines.
|
|
81
|
+
- If multiple different apps can satisfy the request, prefer the already-ready one over launching another offline app.
|
|
79
82
|
- Within the selected runtime tier, rank by user preference:
|
|
80
83
|
- exact `app_id`/title match (for explicit app requests)
|
|
81
84
|
- `starred=true`
|
|
@@ -86,7 +89,7 @@ Follow these sections in order:
|
|
|
86
89
|
- higher `score`
|
|
87
90
|
- If the top candidate is not clearly better than alternatives, ask user once with top 3 candidates.
|
|
88
91
|
- If a suitable installed app is found, select it and continue to Run App.
|
|
89
|
-
-
|
|
92
|
+
- Search results may include apps from other reachable Pinokio machines:
|
|
90
93
|
- remote results use `app_id` in the form `<app_id>@<source.host>`
|
|
91
94
|
- `source.local=false` means the result is from another machine
|
|
92
95
|
- treat remote results as separate apps; do not merge them with the local app of the same name
|
|
@@ -106,33 +109,39 @@ Follow these sections in order:
|
|
|
106
109
|
|
|
107
110
|
### 3. Run App
|
|
108
111
|
|
|
109
|
-
- Once you have a selected
|
|
112
|
+
- Once you have a selected app, use `pterm status`.
|
|
110
113
|
- Poll every 2s.
|
|
111
114
|
- Use status fields from pterm output:
|
|
112
115
|
- `path`: absolute app path to use with `pterm run`
|
|
113
116
|
- `running`: script is running
|
|
114
117
|
- `ready`: app is reachable/ready
|
|
115
118
|
- `ready_url`: default base URL for API calls when available
|
|
116
|
-
- `external_ready_urls`: optional ordered non-loopback app URLs for caller-side access; use them only when `ready_url`
|
|
119
|
+
- `external_ready_urls`: optional ordered non-loopback app URLs for caller-side access; use them only when `ready_url` is missing or unusable due to loopback restrictions
|
|
117
120
|
- `state`: `offline | starting | online`
|
|
118
|
-
- `source`: machine identity for
|
|
121
|
+
- `source`: machine identity for results from other reachable Pinokio machines
|
|
119
122
|
- Use `--probe` only for readiness confirmation before first API call (or when status is uncertain).
|
|
120
123
|
- Use `--timeout=<ms>` only when you need a non-default probe timeout.
|
|
121
124
|
- Treat `offline` as expected before first run.
|
|
122
125
|
- If `app_id` contains `@<host>` or `source.local=false`, the app is remote:
|
|
123
126
|
- treat `path` and `ready_url` as source-local fields, not caller-usable local paths/URLs
|
|
124
127
|
- use `external_ready_urls` in order for caller-side API access when available
|
|
125
|
-
-
|
|
128
|
+
- use `pterm run <app_id>` for remote launch; do not use a remote machine's `path` value as a local path
|
|
129
|
+
- for remote path-based tasks:
|
|
130
|
+
- this applies only when the task expects filesystem paths such as `/path/to/file`
|
|
131
|
+
- do not pass local paths from this machine to the remote app
|
|
132
|
+
- first run `pterm upload <app_id> <file...>`
|
|
133
|
+
- then use the returned remote `path` values
|
|
126
134
|
- If app is offline or not ready, run it:
|
|
127
|
-
-
|
|
135
|
+
- For remote apps, run `pterm run <app_id>`.
|
|
136
|
+
- Otherwise run `pterm run <app_path>`.
|
|
128
137
|
- If the launcher has no explicit default item or the launch action depends on current menu state, infer one or more ordered selectors from the launcher's current menu and pass them via repeated `--default`.
|
|
129
138
|
- Prefer stable launcher selectors such as `run.js?mode=Default`, then broader fallbacks like `run.js`, then installation fallback like `install.js`.
|
|
130
139
|
- Continue polling with `pterm status <app_id>`.
|
|
131
140
|
- Default startup timeout: 180s.
|
|
132
141
|
- Success criteria:
|
|
133
142
|
- `state=online` and `ready=true`
|
|
134
|
-
- use `ready_url` by default
|
|
135
|
-
- if `ready_url` fails because the client cannot access loopback and `external_ready_urls` exists, try those URLs in order
|
|
143
|
+
- use `ready_url` by default when it exists and is caller-usable
|
|
144
|
+
- if `ready_url` is missing, or it fails because the client cannot access loopback, and `external_ready_urls` exists, try those URLs in order
|
|
136
145
|
- missing `external_ready_urls` is normal; it usually means network sharing is off
|
|
137
146
|
- Failure criteria:
|
|
138
147
|
- timeout before success
|
|
@@ -165,6 +174,27 @@ Follow these sections in order:
|
|
|
165
174
|
- Do not execute the app's internal Python/Node/bundled CLI as a fallback when `pterm` has already selected a launcher-managed app.
|
|
166
175
|
- If no automatable API exists after the app is running, report that clearly instead of bypassing the launcher with an internal CLI.
|
|
167
176
|
|
|
177
|
+
### 5. Parallel Mode (explicit only)
|
|
178
|
+
|
|
179
|
+
- Use this section only when the user explicitly asks to:
|
|
180
|
+
- run on multiple machines
|
|
181
|
+
- use multiple apps in parallel
|
|
182
|
+
- compare multiple relevant apps side by side
|
|
183
|
+
- generate multiple outputs concurrently
|
|
184
|
+
- Do not use this mode by default.
|
|
185
|
+
- Keep each selected app as a separate target, including remote results like `<app_id>@<source.host>`.
|
|
186
|
+
- Selection rules:
|
|
187
|
+
- if the user asks for all relevant apps, use all relevant search results that can perform the task
|
|
188
|
+
- if the user asks for a specific count, use the top N relevant search results after normal search ranking
|
|
189
|
+
- if the user asks for parallel use but does not specify how many apps or machines to use, ask once
|
|
190
|
+
- Ranking still applies in this mode:
|
|
191
|
+
- prefer `ready` apps first
|
|
192
|
+
- then `running` apps
|
|
193
|
+
- then offline apps if more targets are still needed
|
|
194
|
+
- Run and monitor each selected target independently.
|
|
195
|
+
- Keep outputs labeled by target `app_id`.
|
|
196
|
+
- For remote path-based tasks, run `pterm upload <app_id> <file...>` separately for each remote target and use that target's returned remote `path` values.
|
|
197
|
+
|
|
168
198
|
## Behavior Rules
|
|
169
199
|
|
|
170
200
|
- Do not add app-specific hardcoding when user gave only capability (for example "tts").
|
package/server/routes/apps.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
const express = require('express')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const crypto = require('crypto')
|
|
2
5
|
const axios = require('axios')
|
|
6
|
+
const multer = require('multer')
|
|
7
|
+
const FormData = require('form-data')
|
|
8
|
+
const sanitize = require('sanitize-filename')
|
|
3
9
|
|
|
4
10
|
const DEFAULT_PEER_PORT = 42000
|
|
5
11
|
const DEFAULT_PEER_TIMEOUT_MS = 2500
|
|
12
|
+
const DEFAULT_PEER_UPLOAD_TIMEOUT_MS = 30000
|
|
6
13
|
const IPV4_HOST_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/
|
|
7
14
|
|
|
8
15
|
const isQualifiedHost = (value = '') => {
|
|
@@ -55,6 +62,7 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
const router = express.Router()
|
|
65
|
+
const upload = multer()
|
|
58
66
|
|
|
59
67
|
const asyncHandler = (fn) => (req, res, next) => {
|
|
60
68
|
Promise.resolve(fn(req, res, next)).catch(next)
|
|
@@ -189,6 +197,60 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
189
197
|
}
|
|
190
198
|
return Object.keys(info).filter((host) => host && host !== localHost)
|
|
191
199
|
}
|
|
200
|
+
const uniqueUploadPath = async (directory, originalName) => {
|
|
201
|
+
const parsed = path.parse(originalName)
|
|
202
|
+
const baseName = parsed.name || 'upload'
|
|
203
|
+
const ext = parsed.ext || ''
|
|
204
|
+
let counter = 0
|
|
205
|
+
while (true) {
|
|
206
|
+
const candidateName = counter === 0
|
|
207
|
+
? `${baseName}${ext}`
|
|
208
|
+
: `${baseName}-${counter}${ext}`
|
|
209
|
+
const candidatePath = path.join(directory, candidateName)
|
|
210
|
+
try {
|
|
211
|
+
await fs.promises.access(candidatePath)
|
|
212
|
+
counter += 1
|
|
213
|
+
} catch (_) {
|
|
214
|
+
return {
|
|
215
|
+
name: candidateName,
|
|
216
|
+
path: candidatePath
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const storeAppUploads = async (appPath, files = []) => {
|
|
222
|
+
const token = crypto.randomBytes(16).toString('hex')
|
|
223
|
+
const uploadDir = path.join(appPath, '.pinokio-temp', 'uploads', token)
|
|
224
|
+
await fs.promises.mkdir(uploadDir, { recursive: true })
|
|
225
|
+
const stored = []
|
|
226
|
+
for (const file of files) {
|
|
227
|
+
if (!file || !file.buffer) {
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const originalName = path.basename(typeof file.originalname === 'string' && file.originalname.trim()
|
|
231
|
+
? file.originalname.trim()
|
|
232
|
+
: 'upload')
|
|
233
|
+
const safeName = sanitize(originalName) || `upload-${Date.now()}`
|
|
234
|
+
const target = await uniqueUploadPath(uploadDir, safeName)
|
|
235
|
+
await fs.promises.writeFile(target.path, file.buffer)
|
|
236
|
+
stored.push({
|
|
237
|
+
name: originalName,
|
|
238
|
+
path: target.path,
|
|
239
|
+
size: typeof file.size === 'number' ? file.size : file.buffer.length,
|
|
240
|
+
mimeType: typeof file.mimetype === 'string' ? file.mimetype : ''
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
token,
|
|
245
|
+
files: stored
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const decorateUploadResult = (uploadResult, source, appId) => {
|
|
249
|
+
const next = uploadResult && typeof uploadResult === 'object' ? { ...uploadResult } : {}
|
|
250
|
+
next.app_id = qualifyAppId(appId || next.app_id || '', source.host)
|
|
251
|
+
next.source = source
|
|
252
|
+
return next
|
|
253
|
+
}
|
|
192
254
|
const mergeSearchApps = (localApps, remoteApps, query = '') => {
|
|
193
255
|
const merged = []
|
|
194
256
|
const seen = new Set()
|
|
@@ -393,6 +455,72 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
393
455
|
})
|
|
394
456
|
}))
|
|
395
457
|
|
|
458
|
+
router.post('/apps/:app_id/upload', upload.any(), asyncHandler(async (req, res) => {
|
|
459
|
+
const parsedAppId = parseQualifiedAppId(req.params.app_id)
|
|
460
|
+
const requestedAppId = parsedAppId.app_id || req.params.app_id
|
|
461
|
+
const remoteHost = parsedAppId.qualified ? parsedAppId.host : null
|
|
462
|
+
const files = Array.isArray(req.files) ? req.files : []
|
|
463
|
+
if (files.length === 0) {
|
|
464
|
+
res.status(400).json({ error: 'No files provided' })
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
if (remoteHost && remoteHost !== currentPeerHost()) {
|
|
468
|
+
try {
|
|
469
|
+
const form = new FormData()
|
|
470
|
+
for (const file of files) {
|
|
471
|
+
if (!file || !file.buffer) {
|
|
472
|
+
continue
|
|
473
|
+
}
|
|
474
|
+
form.append('files', file.buffer, {
|
|
475
|
+
filename: path.basename(file.originalname || 'upload'),
|
|
476
|
+
contentType: file.mimetype || 'application/octet-stream',
|
|
477
|
+
knownLength: typeof file.size === 'number' ? file.size : file.buffer.length
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
const response = await axios.post(`http://${remoteHost}:${peerPort()}/apps/${encodeURIComponent(requestedAppId)}/upload`, form, {
|
|
481
|
+
timeout: DEFAULT_PEER_UPLOAD_TIMEOUT_MS,
|
|
482
|
+
maxBodyLength: Infinity,
|
|
483
|
+
maxContentLength: Infinity,
|
|
484
|
+
headers: {
|
|
485
|
+
...peerRequestHeaders(req),
|
|
486
|
+
...form.getHeaders()
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
res.json(decorateUploadResult(response.data, buildSource(remoteHost, false), requestedAppId))
|
|
490
|
+
return
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (error && error.response) {
|
|
493
|
+
res.status(error.response.status).json(error.response.data)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
res.status(502).json({
|
|
497
|
+
error: 'Peer upload unavailable',
|
|
498
|
+
app_id: qualifyAppId(requestedAppId, remoteHost),
|
|
499
|
+
source: buildSource(remoteHost, false)
|
|
500
|
+
})
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const appId = registry.normalizeAppId(requestedAppId)
|
|
505
|
+
if (!appId) {
|
|
506
|
+
res.status(400).json({ error: 'Invalid app_id' })
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
const status = await registry.buildAppStatus(appId, {
|
|
510
|
+
source: req.$source || null
|
|
511
|
+
})
|
|
512
|
+
if (!status || !status.path) {
|
|
513
|
+
res.status(404).json({ error: 'App not found', app_id: appId })
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
const payload = await storeAppUploads(status.path, files)
|
|
517
|
+
if (!Array.isArray(payload.files) || payload.files.length === 0) {
|
|
518
|
+
res.status(400).json({ error: 'No valid files provided', app_id: appId })
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
res.json(decorateUploadResult(payload, buildSource(currentPeerHost(), true), appId))
|
|
522
|
+
}))
|
|
523
|
+
|
|
396
524
|
router.get('/apps/status/:app_id', asyncHandler(async (req, res) => {
|
|
397
525
|
const parsedAppId = parseQualifiedAppId(req.params.app_id)
|
|
398
526
|
const requestedAppId = parsedAppId.app_id || req.params.app_id
|
|
@@ -449,7 +577,45 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
449
577
|
}))
|
|
450
578
|
|
|
451
579
|
router.get('/apps/logs/:app_id', asyncHandler(async (req, res) => {
|
|
452
|
-
const
|
|
580
|
+
const parsedAppId = parseQualifiedAppId(req.params.app_id)
|
|
581
|
+
const requestedAppId = parsedAppId.app_id || req.params.app_id
|
|
582
|
+
const remoteHost = parsedAppId.qualified ? parsedAppId.host : null
|
|
583
|
+
if (remoteHost && remoteHost !== currentPeerHost()) {
|
|
584
|
+
try {
|
|
585
|
+
const params = {}
|
|
586
|
+
if (typeof req.query.script === 'string' && req.query.script.trim()) {
|
|
587
|
+
params.script = req.query.script
|
|
588
|
+
}
|
|
589
|
+
const tail = registry.parseTailCount(req.query.tail, 200)
|
|
590
|
+
if (Number.isFinite(tail) && tail > 0) {
|
|
591
|
+
params.tail = String(tail)
|
|
592
|
+
}
|
|
593
|
+
const response = await axios.get(`http://${remoteHost}:${peerPort()}/apps/logs/${encodeURIComponent(requestedAppId)}`, {
|
|
594
|
+
timeout: DEFAULT_PEER_TIMEOUT_MS,
|
|
595
|
+
headers: peerRequestHeaders(req),
|
|
596
|
+
params
|
|
597
|
+
})
|
|
598
|
+
const payload = response && response.data && typeof response.data === 'object'
|
|
599
|
+
? { ...response.data }
|
|
600
|
+
: {}
|
|
601
|
+
payload.app_id = qualifyAppId(requestedAppId, remoteHost)
|
|
602
|
+
payload.source = buildSource(remoteHost, false)
|
|
603
|
+
res.json(payload)
|
|
604
|
+
return
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (error && error.response) {
|
|
607
|
+
res.status(error.response.status).json(error.response.data)
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
res.status(502).json({
|
|
611
|
+
error: 'Peer logs unavailable',
|
|
612
|
+
app_id: qualifyAppId(requestedAppId, remoteHost),
|
|
613
|
+
source: buildSource(remoteHost, false)
|
|
614
|
+
})
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const appId = registry.normalizeAppId(requestedAppId)
|
|
453
619
|
if (!appId) {
|
|
454
620
|
res.status(400).json({ error: 'Invalid app_id' })
|
|
455
621
|
return
|