pinokiod 3.22.0 ā 3.24.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.
- package/kernel/plugin.js +57 -10
- package/kernel/shell.js +21 -0
- package/kernel/shell_parser.js +267 -0
- package/kernel/shells.js +2 -0
- package/kernel/util.js +5 -1
- package/package.json +1 -1
- package/server/index.js +202 -25
- package/server/public/style.css +26 -1
- package/server/socket.js +31 -8
- package/server/views/app.ejs +83 -27
- package/server/views/d.ejs +225 -0
- package/server/views/env_editor.ejs +12 -0
- package/server/views/index.ejs +25 -13
- package/server/views/init/index.ejs +8 -2
- package/server/views/net.ejs +13 -7
- package/server/views/network.ejs +7 -1
- package/server/views/partials/dynamic.ejs +3 -0
- package/server/views/partials/menu.ejs +3 -0
- package/server/views/partials/running.ejs +22 -0
- package/server/views/settings.ejs +1 -1
package/kernel/plugin.js
CHANGED
|
@@ -1,26 +1,73 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
|
+
const { glob } = require('glob')
|
|
3
|
+
const Info = require("./info")
|
|
2
4
|
class Plugin {
|
|
3
5
|
constructor(kernel) {
|
|
4
6
|
this.kernel = kernel
|
|
5
7
|
}
|
|
6
8
|
async init() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
9
|
+
let exists = await this.kernel.exists("plugin")
|
|
10
|
+
if (!exists) {
|
|
11
|
+
await fs.promises.mkdir(this.kernel.path("plugin"), { recursive: true }).catch((e) => {})
|
|
12
|
+
if (this.kernel.bin.installed && this.kernel.bin.installed.conda && this.kernel.bin.installed.conda.has("git")) {
|
|
11
13
|
await this.kernel.exec({
|
|
12
14
|
//message: "git clone https://github.com/peanutcocktail/plugin",
|
|
13
|
-
message: "git clone https://github.com/pinokiocomputer/plugin",
|
|
14
|
-
|
|
15
|
+
//message: "git clone https://github.com/pinokiocomputer/plugin",
|
|
16
|
+
message: "git clone https://github.com/pinokiocomputer/code",
|
|
17
|
+
path: this.kernel.path("plugin")
|
|
15
18
|
}, (e) => {
|
|
16
19
|
process.stdout.write(e.raw)
|
|
17
20
|
})
|
|
18
21
|
}
|
|
19
|
-
let plugin_dir = path.resolve(this.kernel.homedir, "plugin")
|
|
20
|
-
let pinokiojs = path.resolve(plugin_dir, "pinokio.js")
|
|
21
|
-
this.config = await this.kernel.require(pinokiojs)
|
|
22
|
-
this.cache = {}
|
|
23
22
|
}
|
|
23
|
+
let plugin_dir = path.resolve(this.kernel.homedir, "plugin")
|
|
24
|
+
// this.config = await this.kernel.require(pinokiojs)
|
|
25
|
+
this.cache = {}
|
|
26
|
+
|
|
27
|
+
let plugin_paths = await glob('**/pinokio.js', { cwd: plugin_dir })
|
|
28
|
+
console.log({ plugin_paths })
|
|
29
|
+
|
|
30
|
+
let plugins = []
|
|
31
|
+
// let info = new Info(this.kernel)
|
|
32
|
+
// info.cwd = () => {
|
|
33
|
+
// return plugin_dir
|
|
34
|
+
// }
|
|
35
|
+
for(let plugin_path of plugin_paths) {
|
|
36
|
+
let config = await this.kernel.require(path.resolve(plugin_dir, plugin_path))
|
|
37
|
+
let cwd = plugin_path.split("/").slice(0, -1).join("/")
|
|
38
|
+
config.image = "/asset/plugin/" + cwd + "/" + config.icon
|
|
39
|
+
plugins.push({
|
|
40
|
+
href: "/run/plugin/" + plugin_path,
|
|
41
|
+
...config
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.config = {
|
|
46
|
+
menu: plugins.map((plugin) => {
|
|
47
|
+
plugin.text = plugin.title
|
|
48
|
+
return plugin
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
console.log("THIS.config", JSON.stringify(this.config, null, 2))
|
|
52
|
+
|
|
53
|
+
// if (this.kernel.bin.installed && this.kernel.bin.installed.conda && this.kernel.bin.installed.conda.has("git")) {
|
|
54
|
+
// // if ~/pinokio/prototype doesn't exist, clone
|
|
55
|
+
// let exists = await this.kernel.exists("plugin")
|
|
56
|
+
// if (!exists) {
|
|
57
|
+
// await this.kernel.exec({
|
|
58
|
+
// //message: "git clone https://github.com/peanutcocktail/plugin",
|
|
59
|
+
// //message: "git clone https://github.com/pinokiocomputer/plugin",
|
|
60
|
+
// message: "git clone https://github.com/pinokiocomputer/code",
|
|
61
|
+
// path: this.kernel.homedir
|
|
62
|
+
// }, (e) => {
|
|
63
|
+
// process.stdout.write(e.raw)
|
|
64
|
+
// })
|
|
65
|
+
// }
|
|
66
|
+
// let plugin_dir = path.resolve(this.kernel.homedir, "plugin")
|
|
67
|
+
// let pinokiojs = path.resolve(plugin_dir, "pinokio.js")
|
|
68
|
+
// this.config = await this.kernel.require(pinokiojs)
|
|
69
|
+
// this.cache = {}
|
|
70
|
+
// }
|
|
24
71
|
}
|
|
25
72
|
async update() {
|
|
26
73
|
if (this.kernel.bin.installed && this.kernel.bin.installed.conda && this.kernel.bin.installed.conda.has("git")) {
|
package/kernel/shell.js
CHANGED
|
@@ -16,6 +16,7 @@ const sudo = require("sudo-prompt-programfiles-x86");
|
|
|
16
16
|
const unparse = require('yargs-unparser-custom-flag');
|
|
17
17
|
const Util = require('./util')
|
|
18
18
|
const Environment = require('./environment')
|
|
19
|
+
const ShellParser = require('./shell_parser')
|
|
19
20
|
const home = os.homedir()
|
|
20
21
|
class Shell {
|
|
21
22
|
/*
|
|
@@ -208,6 +209,7 @@ class Shell {
|
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
async start(params, ondata) {
|
|
212
|
+
console.log("SHELL START", params)
|
|
211
213
|
this.ondata = ondata
|
|
212
214
|
|
|
213
215
|
/*
|
|
@@ -1018,6 +1020,7 @@ class Shell {
|
|
|
1018
1020
|
return params
|
|
1019
1021
|
}
|
|
1020
1022
|
async exec(params) {
|
|
1023
|
+
this.parser = new ShellParser()
|
|
1021
1024
|
params = await this.activate(params)
|
|
1022
1025
|
this.cmd = this.build(params)
|
|
1023
1026
|
let res = await new Promise((resolve, reject) => {
|
|
@@ -1046,6 +1049,24 @@ class Shell {
|
|
|
1046
1049
|
}
|
|
1047
1050
|
this.monitor = this.monitor + data
|
|
1048
1051
|
this.monitor = this.monitor.slice(-300) // last 300
|
|
1052
|
+
|
|
1053
|
+
let notifications = this.parser.processData(data)
|
|
1054
|
+
if (notifications.length > 0) {
|
|
1055
|
+
console.log({ notifications })
|
|
1056
|
+
for(let notif of notifications) {
|
|
1057
|
+
if (notif.type !== "bell") {
|
|
1058
|
+
Util.push({
|
|
1059
|
+
// title: notif.type,
|
|
1060
|
+
// subtitle: notif.title,
|
|
1061
|
+
image: path.resolve(__dirname, "../server/public/pinokio-black.png"),
|
|
1062
|
+
message: notif.title,//`š [${notif.type}] ${notif.title}: ${notif.message}\n`,
|
|
1063
|
+
sound: true,
|
|
1064
|
+
timeout: 30,
|
|
1065
|
+
})
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1049
1070
|
if (!this.done) {
|
|
1050
1071
|
|
|
1051
1072
|
// "request cursor position" handling: https://github.com/microsoft/node-pty/issues/535
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
class ShellParser {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.buffer = '';
|
|
4
|
+
this.bufferMaxSize = 2000;
|
|
5
|
+
this.lastNotificationTime = 0;
|
|
6
|
+
this.debounceMs = 1000; // Prevent spam
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Process terminal data and detect notification patterns
|
|
11
|
+
* @param {string} data - Raw terminal data chunk
|
|
12
|
+
* @returns {Array} Array of notification objects or empty array
|
|
13
|
+
*/
|
|
14
|
+
processData(data) {
|
|
15
|
+
// Add to buffer
|
|
16
|
+
this.buffer += data;
|
|
17
|
+
|
|
18
|
+
// Keep buffer manageable
|
|
19
|
+
if (this.buffer.length > this.bufferMaxSize) {
|
|
20
|
+
this.buffer = this.buffer.slice(-this.bufferMaxSize);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const notifications = [];
|
|
24
|
+
|
|
25
|
+
// Check for each pattern type
|
|
26
|
+
// notifications.push(...this.detectBell(data));
|
|
27
|
+
notifications.push(...this.detectEscapeSequences(data));
|
|
28
|
+
// notifications.push(...this.detectTextPatterns(this.buffer));
|
|
29
|
+
|
|
30
|
+
// Apply debouncing and return
|
|
31
|
+
return this.debounceNotifications(notifications);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect bell character (ASCII 7)
|
|
36
|
+
*/
|
|
37
|
+
detectBell(data) {
|
|
38
|
+
const notifications = [];
|
|
39
|
+
if (data.includes('\x07')) {
|
|
40
|
+
notifications.push({
|
|
41
|
+
type: 'bell',
|
|
42
|
+
title: 'Terminal Bell',
|
|
43
|
+
message: 'Bell character detected',
|
|
44
|
+
timestamp: Date.now()
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return notifications;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect iTerm2-style escape sequences
|
|
52
|
+
* Format: \x1b]9;[title];[message]\x07
|
|
53
|
+
*/
|
|
54
|
+
//detectEscapeSequences(data) {
|
|
55
|
+
// const notifications = [];
|
|
56
|
+
//
|
|
57
|
+
// // iTerm2 notification escape sequence
|
|
58
|
+
// const escapeRegex = /\x1b\]9;([^;]*);?([^\x07]*)\x07/g;
|
|
59
|
+
// let match;
|
|
60
|
+
//
|
|
61
|
+
// while ((match = escapeRegex.exec(data)) !== null) {
|
|
62
|
+
// notifications.push({
|
|
63
|
+
// type: 'escape_sequence',
|
|
64
|
+
// title: match[1] || 'Terminal Notification',
|
|
65
|
+
// message: match[2] || 'Notification from terminal',
|
|
66
|
+
// timestamp: Date.now()
|
|
67
|
+
// });
|
|
68
|
+
// }
|
|
69
|
+
|
|
70
|
+
// // Generic OSC (Operating System Command) sequences
|
|
71
|
+
// const oscRegex = /\x1b\]([0-9]+);([^\x07\x1b]*)\x07/g;
|
|
72
|
+
// while ((match = oscRegex.exec(data)) !== null) {
|
|
73
|
+
// if (match[1] === '9') { // Notification OSC
|
|
74
|
+
// notifications.push({
|
|
75
|
+
// type: 'osc_notification',
|
|
76
|
+
// title: 'System Notification',
|
|
77
|
+
// message: match[2] || 'OSC notification',
|
|
78
|
+
// timestamp: Date.now()
|
|
79
|
+
// });
|
|
80
|
+
// }
|
|
81
|
+
// }
|
|
82
|
+
|
|
83
|
+
// return notifications;
|
|
84
|
+
//}
|
|
85
|
+
detectEscapeSequences(data) {
|
|
86
|
+
const notifications = [];
|
|
87
|
+
|
|
88
|
+
// OSC 9 - Notification sequences (used by iTerm2 and others)
|
|
89
|
+
const notificationRegex = /\x1b\]9;([^;]*);?([^\x07]*)\x07/g;
|
|
90
|
+
let match;
|
|
91
|
+
|
|
92
|
+
while ((match = notificationRegex.exec(data)) !== null) {
|
|
93
|
+
notifications.push({
|
|
94
|
+
type: 'notification_escape',
|
|
95
|
+
title: match[1] || 'Terminal Notification',
|
|
96
|
+
message: match[2] || 'Notification from terminal',
|
|
97
|
+
timestamp: Date.now()
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Other OSC sequences (window title, etc.) - usually not notifications
|
|
102
|
+
const otherOscRegex = /\x1b\]([0-8]|[1-9][0-9]+);([^\x07\x1b]*)\x07/g;
|
|
103
|
+
while ((match = otherOscRegex.exec(data)) !== null) {
|
|
104
|
+
// Only log these for debugging, don't treat as notifications
|
|
105
|
+
console.debug(`OSC ${match[1]}: ${match[2]}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return notifications;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Detect text patterns that might indicate notifications
|
|
113
|
+
*/
|
|
114
|
+
detectTextPatterns(buffer) {
|
|
115
|
+
const notifications = [];
|
|
116
|
+
|
|
117
|
+
const patterns = [
|
|
118
|
+
{
|
|
119
|
+
regex: /\b(?:alert|notification|notify|attention)\b[^\n\r]*$/im,
|
|
120
|
+
type: 'text_alert',
|
|
121
|
+
title: 'Alert Detected',
|
|
122
|
+
extractMessage: true
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
regex: /\berror\b[^\n\r]*$/im,
|
|
126
|
+
type: 'error',
|
|
127
|
+
title: 'Error Detected',
|
|
128
|
+
extractMessage: true
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
regex: /\b(?:completed|finished|done|success)\b[^\n\r]*$/im,
|
|
132
|
+
type: 'completion',
|
|
133
|
+
title: 'Task Complete',
|
|
134
|
+
extractMessage: true
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
regex: /claude\s+code.*?(?:completed|finished|ready|done)[^\n\r]*$/im,
|
|
138
|
+
type: 'claude_code',
|
|
139
|
+
title: 'Claude Code',
|
|
140
|
+
extractMessage: true
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
regex: /ā
[^\n\r]*$/im,
|
|
144
|
+
type: 'success_emoji',
|
|
145
|
+
title: 'Success',
|
|
146
|
+
extractMessage: true
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
regex: /ā[^\n\r]*$/im,
|
|
150
|
+
type: 'error_emoji',
|
|
151
|
+
title: 'Error',
|
|
152
|
+
extractMessage: true
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
regex: /š[^\n\r]*$/im,
|
|
156
|
+
type: 'notification_emoji',
|
|
157
|
+
title: 'Notification',
|
|
158
|
+
extractMessage: true
|
|
159
|
+
}
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
patterns.forEach(pattern => {
|
|
163
|
+
const match = buffer.match(pattern.regex);
|
|
164
|
+
if (match) {
|
|
165
|
+
let message = pattern.extractMessage ?
|
|
166
|
+
match[0].trim().slice(0, 100) :
|
|
167
|
+
`${pattern.title} detected`;
|
|
168
|
+
|
|
169
|
+
// Clean up ANSI escape codes from message
|
|
170
|
+
message = this.stripAnsiCodes(message);
|
|
171
|
+
|
|
172
|
+
notifications.push({
|
|
173
|
+
type: pattern.type,
|
|
174
|
+
title: pattern.title,
|
|
175
|
+
message: message,
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
rawMatch: match[0]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return notifications;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove ANSI escape codes from text
|
|
187
|
+
*/
|
|
188
|
+
stripAnsiCodes(text) {
|
|
189
|
+
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Apply debouncing to prevent notification spam
|
|
194
|
+
*/
|
|
195
|
+
debounceNotifications(notifications) {
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
if (notifications.length > 0 && (now - this.lastNotificationTime) > this.debounceMs) {
|
|
198
|
+
this.lastNotificationTime = now;
|
|
199
|
+
return notifications;
|
|
200
|
+
}
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reset the internal state
|
|
206
|
+
*/
|
|
207
|
+
reset() {
|
|
208
|
+
this.buffer = '';
|
|
209
|
+
this.lastNotificationTime = 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Add custom pattern
|
|
214
|
+
*/
|
|
215
|
+
addCustomPattern(regex, type, title) {
|
|
216
|
+
// This would require refactoring detectTextPatterns to use a dynamic array
|
|
217
|
+
// For now, patterns are hardcoded above
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Usage example:
|
|
222
|
+
/*
|
|
223
|
+
const detector = new TerminalNotificationDetector();
|
|
224
|
+
|
|
225
|
+
// Simulate terminal data chunks
|
|
226
|
+
const testData = [
|
|
227
|
+
'Running command...\n',
|
|
228
|
+
'Alert: Process completed successfully\x07',
|
|
229
|
+
'\x1b]9;Claude Code;File created successfully\x07',
|
|
230
|
+
'Error: File not found\n',
|
|
231
|
+
'ā
Build completed\n'
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
testData.forEach(chunk => {
|
|
235
|
+
const notifications = detector.processData(chunk);
|
|
236
|
+
notifications.forEach(notif => {
|
|
237
|
+
console.log(`[${notif.type}] ${notif.title}: ${notif.message}`);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
*/
|
|
241
|
+
module.exports = ShellParser
|
|
242
|
+
|
|
243
|
+
// Example integration with node-pty:
|
|
244
|
+
/*
|
|
245
|
+
const pty = require('node-pty');
|
|
246
|
+
const detector = new TerminalNotificationDetector();
|
|
247
|
+
|
|
248
|
+
const ptyProcess = pty.spawn('bash', [], {
|
|
249
|
+
name: 'xterm-color',
|
|
250
|
+
cols: 80,
|
|
251
|
+
rows: 30,
|
|
252
|
+
cwd: process.cwd(),
|
|
253
|
+
env: process.env
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
ptyProcess.on('data', (data) => {
|
|
257
|
+
// Forward to stdout
|
|
258
|
+
process.stdout.write(data);
|
|
259
|
+
|
|
260
|
+
// Check for notifications
|
|
261
|
+
const notifications = detector.processData(data);
|
|
262
|
+
notifications.forEach(notif => {
|
|
263
|
+
console.log(`\nš [${notif.type}] ${notif.title}: ${notif.message}\n`);
|
|
264
|
+
// Here you could send to notification system, websocket, etc.
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
*/
|
package/kernel/shells.js
CHANGED
package/kernel/util.js
CHANGED
|
@@ -510,7 +510,11 @@ function p2u(localPath) {
|
|
|
510
510
|
const path = match[2].replace(/\\/g, '/');
|
|
511
511
|
return `/${drive}/${path}`;
|
|
512
512
|
} else {
|
|
513
|
-
|
|
513
|
+
if (localPath.startsWith("/")) {
|
|
514
|
+
return localPath.slice(1)
|
|
515
|
+
} else {
|
|
516
|
+
return localPath
|
|
517
|
+
}
|
|
514
518
|
}
|
|
515
519
|
}
|
|
516
520
|
function u2p(urlPath) {
|