pinokiod 3.208.0 → 3.210.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.
@@ -136,14 +136,23 @@ class Terminal {
136
136
  return { files: saved, errors: failures }
137
137
  }
138
138
 
139
- const marker = '[attachment] '
139
+ const marker = this.buildAttachmentPayload(saved, {
140
+ shellInstance,
141
+ params
142
+ })
143
+ let shellEmitAttempted = false
144
+ let shellEmitSucceeded = false
140
145
  if (params && params.id && kernel && kernel.shell && typeof kernel.shell.emit === 'function') {
141
146
  try {
142
- kernel.shell.emit({
147
+ shellEmitAttempted = true
148
+ shellEmitSucceeded = kernel.shell.emit({
143
149
  id: params.id,
144
150
  emit: marker,
145
151
  paste: true
146
- })
152
+ }) === true
153
+ if (!shellEmitSucceeded && ondata) {
154
+ ondata({ raw: marker })
155
+ }
147
156
  } catch (error) {
148
157
  if (ondata) {
149
158
  ondata({ raw: marker })
@@ -164,7 +173,100 @@ class Terminal {
164
173
  req.params.buffers = {}
165
174
  }
166
175
 
167
- return { files: saved, errors: failures }
176
+ return { files: saved, errors: failures, shellEmit: shellEmitSucceeded, shellEmitAttempted }
177
+ }
178
+
179
+ buildAttachmentPayload(files, context = {}) {
180
+ const markerPrefix = ''
181
+ if (!Array.isArray(files) || files.length === 0) {
182
+ return markerPrefix
183
+ }
184
+
185
+ const shellName = this.resolveShellName(context)
186
+ const escapedPaths = files
187
+ .map((file) => {
188
+ if (!file || typeof file !== 'object') {
189
+ return null
190
+ }
191
+ const rawPath = this.pickBestPath(file)
192
+ if (!rawPath) {
193
+ return null
194
+ }
195
+ return this.escapePathForShell(shellName, rawPath)
196
+ })
197
+ .filter(Boolean)
198
+
199
+ if (escapedPaths.length === 0) {
200
+ return markerPrefix
201
+ }
202
+
203
+ return escapedPaths.join(' ')
204
+ }
205
+
206
+ resolveShellName(context) {
207
+ if (!context) {
208
+ return null
209
+ }
210
+ const { shellInstance, params } = context
211
+ if (shellInstance && typeof shellInstance.shell === 'string' && shellInstance.shell.trim()) {
212
+ return shellInstance.shell.trim()
213
+ }
214
+ if (params && typeof params.shell === 'string' && params.shell.trim()) {
215
+ return params.shell.trim()
216
+ }
217
+ return null
218
+ }
219
+
220
+ escapePathForShell(shellName, rawPath) {
221
+ if (!rawPath || typeof rawPath !== 'string') {
222
+ return ''
223
+ }
224
+ const normalizedPath = rawPath
225
+ if (!shellName) {
226
+ return this.escapePosixPath(normalizedPath)
227
+ }
228
+ const lowerShell = shellName.toLowerCase()
229
+ if (lowerShell.includes('powershell') || lowerShell.includes('pwsh')) {
230
+ return this.escapePowershellPath(normalizedPath)
231
+ }
232
+ if (lowerShell.includes('cmd.exe') || lowerShell === 'cmd') {
233
+ return this.escapeCmdPath(normalizedPath)
234
+ }
235
+ return this.escapePosixPath(normalizedPath)
236
+ }
237
+
238
+ pickBestPath(file) {
239
+ const candidates = [
240
+ file.cliRelativePath,
241
+ file.cliPath,
242
+ file.path,
243
+ file.displayPath,
244
+ file.homeRelativePath,
245
+ file.storedAs,
246
+ file.originalName,
247
+ typeof file.name === 'string' ? file.name : null
248
+ ]
249
+ for (const candidate of candidates) {
250
+ if (typeof candidate === 'string') {
251
+ const trimmed = candidate.trim()
252
+ if (trimmed.length > 0) {
253
+ return trimmed
254
+ }
255
+ }
256
+ }
257
+ return null
258
+ }
259
+
260
+ escapePosixPath(input) {
261
+ return `'${input.split("'").join("'\\''")}'`
262
+ }
263
+
264
+ escapePowershellPath(input) {
265
+ return `'${input.split("'").join("''")}'`
266
+ }
267
+
268
+ escapeCmdPath(input) {
269
+ return `"${input.replace(/(["^%])/g, '^$1')}"`
168
270
  }
169
271
 
170
272
  resolveShellInstance(params, kernel) {
@@ -0,0 +1,99 @@
1
+ const { spawn } = require('child_process')
2
+ const os = require('os')
3
+
4
+ const cache = new Map()
5
+
6
+ const OK_MARKER = '__PINOKIO_BP_OK__'
7
+ const NO_MARKER = '__PINOKIO_BP_NO__'
8
+
9
+ function normalizeKey(shell, platform) {
10
+ return `${platform || os.platform()}::${(shell || '').toLowerCase()}`
11
+ }
12
+
13
+ function isWindowsShell(shell) {
14
+ const name = (shell || '').toLowerCase()
15
+ return name.includes('cmd.exe') || name === 'cmd' || name.includes('powershell') || name.includes('pwsh')
16
+ }
17
+
18
+ function isBash(shell) {
19
+ return (shell || '').toLowerCase().includes('bash')
20
+ }
21
+
22
+ function isZsh(shell) {
23
+ return (shell || '').toLowerCase().includes('zsh')
24
+ }
25
+
26
+ function runProbe(shell, args, script) {
27
+ return new Promise((resolve) => {
28
+ let stdout = ''
29
+ const child = spawn(shell, [...args, script], {
30
+ stdio: ['ignore', 'pipe', 'ignore']
31
+ })
32
+ child.stdout.setEncoding('utf8')
33
+ child.stdout.on('data', (chunk) => {
34
+ stdout += chunk
35
+ })
36
+ child.on('error', () => resolve(null))
37
+ child.on('close', () => resolve(stdout))
38
+ })
39
+ }
40
+
41
+ async function detect(shell, platform = os.platform()) {
42
+ const key = normalizeKey(shell, platform)
43
+ if (cache.has(key)) {
44
+ return cache.get(key)
45
+ }
46
+
47
+ let result
48
+ try {
49
+ result = await runDetection(shell, platform)
50
+ } catch (_) {
51
+ result = null
52
+ }
53
+ if (result === null) {
54
+ result = true
55
+ }
56
+ cache.set(key, result)
57
+ return result
58
+ }
59
+
60
+ async function runDetection(shell, platform) {
61
+ if (!shell) {
62
+ return true
63
+ }
64
+ if (isWindowsShell(shell)) {
65
+ return false
66
+ }
67
+
68
+ if (isBash(shell)) {
69
+ const script = `if bind -v 2>/dev/null | grep -q 'enable-bracketed-paste'; then bind 'set enable-bracketed-paste on' >/dev/null 2>&1; printf '${OK_MARKER}\n'; else printf '${NO_MARKER}\n'; fi`
70
+ const output = await runProbe(shell, ['--noprofile', '--norc', '-ic'], script)
71
+ return parseProbeOutput(output)
72
+ }
73
+
74
+ if (isZsh(shell)) {
75
+ const script = `if setopt -q bracketed-paste 2>/dev/null; then printf '${OK_MARKER}\n'; exit 0; fi; if setopt bracketed-paste 2>/dev/null; then printf '${OK_MARKER}\n'; exit 0; fi; printf '${NO_MARKER}\n'`
76
+ const output = await runProbe(shell, ['-f', '-c'], script)
77
+ return parseProbeOutput(output)
78
+ }
79
+
80
+ // For other shells assume supported unless clearly Windows-only.
81
+ return true
82
+ }
83
+
84
+ function parseProbeOutput(output) {
85
+ if (typeof output !== 'string') {
86
+ return null
87
+ }
88
+ if (output.includes(OK_MARKER)) {
89
+ return true
90
+ }
91
+ if (output.includes(NO_MARKER)) {
92
+ return false
93
+ }
94
+ return null
95
+ }
96
+
97
+ module.exports = {
98
+ detect
99
+ }
@@ -613,8 +613,8 @@ const init = async (options, kernel) => {
613
613
 
614
614
  const entriesToEnsure = [
615
615
  "ENVIRONMENT",
616
- ".*",
617
- "~*",
616
+ // ".*",
617
+ // "~*",
618
618
  "/logs/",
619
619
  "/cache/",
620
620
  "/AGENTS.md",
package/kernel/shell.js CHANGED
@@ -40,6 +40,13 @@ class Shell {
40
40
  this.platform = os.platform()
41
41
  this.logs = {}
42
42
  this.shell = this.platform === 'win32' ? 'cmd.exe' : 'bash';
43
+ this.supportsBracketedPaste = this.computeBracketedPasteSupport(this.shell)
44
+ if (this.kernel && this.kernel.bracketedPasteSupport) {
45
+ const cached = this.kernel.bracketedPasteSupport[(this.shell || '').toLowerCase()]
46
+ if (typeof cached === 'boolean') {
47
+ this.supportsBracketedPaste = cached
48
+ }
49
+ }
43
50
  this.decsyncBuffer = ''
44
51
 
45
52
  // Windows: /D => ignore AutoRun Registry Key
@@ -276,6 +283,12 @@ class Shell {
276
283
  this.EOL = os.EOL
277
284
  if (this.params.shell) {
278
285
  this.shell = this.params.shell
286
+ const normalizedShell = (this.shell || '').toLowerCase()
287
+ if (this.kernel && this.kernel.bracketedPasteSupport && typeof this.kernel.bracketedPasteSupport[normalizedShell] === 'boolean') {
288
+ this.supportsBracketedPaste = this.kernel.bracketedPasteSupport[normalizedShell]
289
+ } else {
290
+ this.supportsBracketedPaste = this.computeBracketedPasteSupport(this.shell)
291
+ }
279
292
  if (/bash/i.test(this.shell)) {
280
293
  this.args = ["--noprofile", "--norc"]
281
294
  //this.args = [ "--login", "-i"]
@@ -579,6 +592,19 @@ class Shell {
579
592
  const regex = new RegExp(pattern, 'gi')
580
593
  return str.replaceAll(regex, '');
581
594
  }
595
+ computeBracketedPasteSupport(shellName) {
596
+ const name = (shellName || '').toLowerCase()
597
+ if (!name) {
598
+ return true
599
+ }
600
+ if (name.includes('cmd.exe') || name === 'cmd') {
601
+ return false
602
+ }
603
+ if (name.includes('powershell') || name.includes('pwsh')) {
604
+ return false
605
+ }
606
+ return true
607
+ }
582
608
  exists(abspath) {
583
609
  return new Promise(r=>fs.access(abspath, fs.constants.F_OK, e => r(!e)))
584
610
  }
package/kernel/shells.js CHANGED
@@ -9,10 +9,12 @@ const {
9
9
 
10
10
  const path = require('path')
11
11
  const Shell = require("./shell")
12
+ const { detect: detectBracketedPasteSupport } = require('./bracketed_paste_detector')
12
13
  class Shells {
13
14
  constructor(kernel) {
14
15
  this.kernel = kernel
15
16
  this.shells = []
17
+ this.bracketedPasteDetections = new Map()
16
18
 
17
19
  }
18
20
  /*
@@ -40,6 +42,14 @@ class Shells {
40
42
  env: this.kernel.bin.envs({}),
41
43
  })
42
44
 
45
+ await this.ensureBracketedPasteSupport(sh.shell)
46
+ if (this.kernel.bracketedPasteSupport) {
47
+ const cached = this.kernel.bracketedPasteSupport[(sh.shell || '').toLowerCase()]
48
+ if (typeof cached === 'boolean') {
49
+ sh.supportsBracketedPaste = cached
50
+ }
51
+ }
52
+
43
53
  this.kernel.envs = sh.env
44
54
  // also set the uppercase variables if they're not set
45
55
  for(let key in sh.env) {
@@ -67,6 +77,44 @@ class Shells {
67
77
  }
68
78
  })
69
79
  }
80
+ async ensureBracketedPasteSupport(shellName) {
81
+ if (!shellName) {
82
+ return
83
+ }
84
+ const lower = (shellName || '').toLowerCase()
85
+ if (!lower) {
86
+ return
87
+ }
88
+ if (!this.kernel.bracketedPasteSupport) {
89
+ this.kernel.bracketedPasteSupport = {}
90
+ }
91
+ if (Object.prototype.hasOwnProperty.call(this.kernel.bracketedPasteSupport, lower)) {
92
+ return this.kernel.bracketedPasteSupport[lower]
93
+ }
94
+ if (this.bracketedPasteDetections.has(lower)) {
95
+ return this.bracketedPasteDetections.get(lower)
96
+ }
97
+ const fallback = !(lower.includes('cmd.exe') || lower === 'cmd' || lower.includes('powershell') || lower.includes('pwsh'))
98
+ const detectionPromise = detectBracketedPasteSupport(shellName, this.kernel.platform || os.platform())
99
+ .then((support) => {
100
+ const value = typeof support === 'boolean' ? support : fallback
101
+ this.kernel.bracketedPasteSupport[lower] = value
102
+ return value
103
+ })
104
+ .catch((error) => {
105
+ console.warn('[shells.ensureBracketedPasteSupport] detection failed', {
106
+ shell: shellName,
107
+ error: error && error.message ? error.message : error
108
+ })
109
+ this.kernel.bracketedPasteSupport[lower] = fallback
110
+ return fallback
111
+ })
112
+ .finally(() => {
113
+ this.bracketedPasteDetections.delete(lower)
114
+ })
115
+ this.bracketedPasteDetections.set(lower, detectionPromise)
116
+ return detectionPromise
117
+ }
70
118
  async launch(params, options, ondata) {
71
119
  // if array, duplicate the action
72
120
  if (Array.isArray(params.message)) {
@@ -112,6 +160,9 @@ class Shells {
112
160
  let exec_path = (params.path ? params.path : ".") // use the current path if not specified
113
161
  let cwd = (options && options.cwd ? options.cwd : this.kernel.homedir) // if cwd exists, use it. Otherwise the cwd is pinokio home folder (~/pinokio)
114
162
  params.path = this.kernel.api.resolvePath(cwd, exec_path)
163
+
164
+ const plannedShell = params.shell || (this.kernel.platform === 'win32' ? 'cmd.exe' : 'bash')
165
+ await this.ensureBracketedPasteSupport(plannedShell)
115
166
  let sh = new Shell(this.kernel)
116
167
  if (options) {
117
168
  params.group = options.group // set group
@@ -387,18 +438,24 @@ class Shells {
387
438
  let session = this.get(params.id)
388
439
  if (session) {
389
440
  if (params.paste) {
390
- //session.emit("\x1b[?2004h\x1b[200~" + params.emit+ "\x1b[201~")
391
- session.emit("\x1b[200~" + params.emit+ "\x1b[201~")
392
- //session.emit(params.emit)
441
+ const payload = params.emit != null ? String(params.emit) : ''
442
+ if (session.supportsBracketedPaste !== false) {
443
+ //session.emit("\x1b[?2004h\x1b[200~" + params.emit+ "\x1b[201~")
444
+ session.emit("\x1b[200~" + payload + "\x1b[201~")
445
+ } else {
446
+ session.emit(payload)
447
+ }
393
448
  } else {
394
449
  session.emit(params.emit)
395
450
  }
451
+ return true
396
452
  } else {
397
453
  const preview = typeof params.emit === 'string' ? params.emit.slice(0, 64) : ''
398
454
  console.warn('[shell.emit] missing session for id', params.id, {
399
455
  paste: !!params.paste,
400
456
  preview
401
457
  })
458
+ return false
402
459
  }
403
460
  }
404
461
  async send(params, ondata, enter) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.208.0",
3
+ "version": "3.210.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -4868,7 +4868,9 @@ class Server {
4868
4868
  const result = await terminalApi.upload(requestPayload, () => {}, this.kernel)
4869
4869
  const files = result && Array.isArray(result.files) ? result.files : []
4870
4870
  const errors = result && Array.isArray(result.errors) ? result.errors : []
4871
- res.json({ files, errors })
4871
+ const shellEmit = result && typeof result.shellEmit === 'boolean' ? result.shellEmit : false
4872
+ const shellEmitAttempted = result && typeof result.shellEmitAttempted === 'boolean' ? result.shellEmitAttempted : false
4873
+ res.json({ files, errors, shellEmit, shellEmitAttempted })
4872
4874
  } catch (error) {
4873
4875
  res.status(500).json({ error: error && error.message ? error.message : 'remote upload failed' })
4874
4876
  }
@@ -207,7 +207,7 @@ body.dark #status-window b {
207
207
  <script>
208
208
  let shell_id
209
209
  let lastNotifiedShellId = null
210
- const formatUploadNotification = (files, sessionId) => {
210
+ const formatUploadNotification = (files, sessionId, shellEmit, shellEmitAttempted) => {
211
211
  if (!Array.isArray(files) || files.length === 0) {
212
212
  return 'Upload complete (no files reported)'
213
213
  }
@@ -219,7 +219,15 @@ const formatUploadNotification = (files, sessionId) => {
219
219
  const summary = target ? `${name} → ${target}` : name
220
220
  return summary.length > 180 ? `${summary.slice(0, 177)}…` : summary
221
221
  })
222
- return `${countLabel}<br>Session: ${sessionLabel}<br>${summaries.join('<br>')}`
222
+ let emitStatus
223
+ if (shellEmitAttempted) {
224
+ emitStatus = shellEmit ? 'Shell emit: delivered to terminal session' : 'Shell emit: failed – fallback used'
225
+ } else if (shellEmitAttempted === false) {
226
+ emitStatus = 'Shell emit: not attempted'
227
+ } else {
228
+ emitStatus = 'Shell emit: status unknown'
229
+ }
230
+ return `${countLabel}<br>Session: ${sessionLabel}<br>${emitStatus}<br>${summaries.join('<br>')}`
223
231
  }
224
232
  const postMessageToAncestors = (payload) => {
225
233
  if (!payload || typeof payload !== "object") {
@@ -946,8 +954,10 @@ document.addEventListener("DOMContentLoaded", async () => {
946
954
  reloadMemory()
947
955
  }
948
956
  } catch (_) {}
957
+ const shellEmit = typeof result.shellEmit === 'boolean' ? result.shellEmit : undefined
958
+ const shellEmitAttempted = typeof result.shellEmitAttempted === 'boolean' ? result.shellEmitAttempted : undefined
949
959
  n.Noty({
950
- text: formatUploadNotification(files, shell_id),
960
+ text: formatUploadNotification(files, shell_id, shellEmit, shellEmitAttempted),
951
961
  timeout: 6000
952
962
  })
953
963
  }
@@ -182,7 +182,7 @@ body.frozen {
182
182
  <link href="/terminal.css" rel="stylesheet"/>
183
183
  <script>
184
184
  let shell_id
185
- const formatUploadNotification = (files, sessionId) => {
185
+ const formatUploadNotification = (files, sessionId, shellEmit, shellEmitAttempted) => {
186
186
  if (!Array.isArray(files) || files.length === 0) {
187
187
  return 'Upload complete (no files reported)'
188
188
  }
@@ -194,7 +194,15 @@ const formatUploadNotification = (files, sessionId) => {
194
194
  const summary = target ? `${name} → ${target}` : name
195
195
  return summary.length > 180 ? `${summary.slice(0, 177)}…` : summary
196
196
  })
197
- return `${countLabel}<br>Session: ${sessionLabel}<br>${summaries.join('<br>')}`
197
+ let emitStatus
198
+ if (shellEmitAttempted) {
199
+ emitStatus = shellEmit ? 'Shell emit: delivered to terminal session' : 'Shell emit: failed – fallback used'
200
+ } else if (shellEmitAttempted === false) {
201
+ emitStatus = 'Shell emit: not attempted'
202
+ } else {
203
+ emitStatus = 'Shell emit: status unknown'
204
+ }
205
+ return `${countLabel}<br>Session: ${sessionLabel}<br>${emitStatus}<br>${summaries.join('<br>')}`
198
206
  }
199
207
  Dropzone.autoDiscover = false;
200
208
  function postMessageToAncestors(payload) {
@@ -799,8 +807,10 @@ document.addEventListener("DOMContentLoaded", async () => {
799
807
  type: "terminal.upload",
800
808
  files: mappedFiles
801
809
  })
810
+ const shellEmit = packet.data && typeof packet.data.shellEmit === 'boolean' ? packet.data.shellEmit : undefined
811
+ const shellEmitAttempted = packet.data && typeof packet.data.shellEmitAttempted === 'boolean' ? packet.data.shellEmitAttempted : undefined
802
812
  n.Noty({
803
- text: formatUploadNotification(mappedFiles, shell_id),
813
+ text: formatUploadNotification(mappedFiles, shell_id, shellEmit, shellEmitAttempted),
804
814
  timeout: 6000
805
815
  })
806
816
  } else {
@@ -1043,8 +1053,10 @@ document.addEventListener("DOMContentLoaded", async () => {
1043
1053
  reloadMemory()
1044
1054
  }
1045
1055
  } catch (_) {}
1056
+ const shellEmit = typeof result.shellEmit === 'boolean' ? result.shellEmit : undefined
1057
+ const shellEmitAttempted = typeof result.shellEmitAttempted === 'boolean' ? result.shellEmitAttempted : undefined
1046
1058
  n.Noty({
1047
- text: formatUploadNotification(mappedFiles, shell_id),
1059
+ text: formatUploadNotification(mappedFiles, shell_id, shellEmit, shellEmitAttempted),
1048
1060
  timeout: 6000
1049
1061
  })
1050
1062
  }