pinokiod 3.23.0 → 3.25.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/api/exec/index.js +67 -0
- package/kernel/api/index.js +1 -0
- package/kernel/plugin.js +40 -10
- package/kernel/prototype.js +23 -0
- package/kernel/router/index.js +13 -0
- package/kernel/router/localhost_home_router.js +11 -0
- package/kernel/shell.js +30 -6
- package/kernel/shell_parser.js +267 -0
- package/kernel/shells.js +52 -45
- package/kernel/util.js +5 -1
- package/package.json +2 -1
- package/server/index.js +335 -136
- package/server/public/style.css +26 -1
- package/server/socket.js +13 -9
- package/server/views/app.ejs +122 -32
- package/server/views/d.ejs +284 -0
- package/server/views/env_editor.ejs +12 -0
- package/server/views/index.ejs +94 -14
- package/server/views/init/index.ejs +248 -81
- package/server/views/net.ejs +13 -8
- package/server/views/network.ejs +10 -3
- package/server/views/partials/dynamic.ejs +4 -1
- package/server/views/partials/menu.ejs +3 -0
- package/server/views/partials/running.ejs +22 -0
- package/server/views/settings.ejs +1 -1
- package/server/views/start.ejs +233 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const child_process = require('child_process')
|
|
2
|
+
module.exports = async (req, ondata, kernel) => {
|
|
3
|
+
/*
|
|
4
|
+
req := {
|
|
5
|
+
method: "exec",
|
|
6
|
+
params: {
|
|
7
|
+
|
|
8
|
+
// pinokio params
|
|
9
|
+
message: <message>,
|
|
10
|
+
path: <path>,
|
|
11
|
+
|
|
12
|
+
// node.js child_process.exec params
|
|
13
|
+
|
|
14
|
+
// Working directory
|
|
15
|
+
cwd: '/path/to/working/directory',
|
|
16
|
+
|
|
17
|
+
// Environment variables
|
|
18
|
+
env: { ...process.env, CUSTOM_VAR: 'value' },
|
|
19
|
+
|
|
20
|
+
// Encoding for stdout/stderr (default: 'utf8')
|
|
21
|
+
encoding: 'utf8', // or 'buffer', 'ascii', etc.
|
|
22
|
+
|
|
23
|
+
// Shell to execute the command
|
|
24
|
+
shell: '/bin/bash', // default varies by platform
|
|
25
|
+
|
|
26
|
+
// Timeout in milliseconds
|
|
27
|
+
timeout: 10000, // Kill process after 10 seconds
|
|
28
|
+
|
|
29
|
+
// Maximum buffer size for stdout/stderr
|
|
30
|
+
maxBuffer: 1024 * 1024, // 1MB (default: 1024 * 1024)
|
|
31
|
+
|
|
32
|
+
// Signal to send when killing due to timeout
|
|
33
|
+
killSignal: 'SIGTERM', // default: 'SIGTERM'
|
|
34
|
+
|
|
35
|
+
// User identity (Unix only)
|
|
36
|
+
uid: 1000,
|
|
37
|
+
gid: 1000,
|
|
38
|
+
|
|
39
|
+
// Windows-specific options
|
|
40
|
+
windowsHide: true, // Hide subprocess console window on Windows
|
|
41
|
+
windowsVerbatimArguments: false // Quote handling on Windows
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
*/
|
|
46
|
+
if (req.params && req.params.message) {
|
|
47
|
+
// if cwd exists, use cwd
|
|
48
|
+
// if cwd doesn't exist, set pat
|
|
49
|
+
if (!req.params.cwd && req.params.path) {
|
|
50
|
+
req.params.cwd = req.params.path
|
|
51
|
+
}
|
|
52
|
+
req.params.env = Object.assign({}, kernel.envs, req.params.env)
|
|
53
|
+
console.log("env", JSON.stringify(req.params.env, null, 2))
|
|
54
|
+
ondata({ raw: `██ Exec: ${req.params.message}\r\n` })
|
|
55
|
+
let response = await new Promise((resolve, reject) => {
|
|
56
|
+
child_process.exec(req.params.message, req.params, (error, stdout, stderr) => {
|
|
57
|
+
resolve({
|
|
58
|
+
stdout,
|
|
59
|
+
error,
|
|
60
|
+
stderr
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
console.log({ response })
|
|
65
|
+
return response
|
|
66
|
+
}
|
|
67
|
+
}
|
package/kernel/api/index.js
CHANGED
package/kernel/plugin.js
CHANGED
|
@@ -1,25 +1,55 @@
|
|
|
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
|
}
|
|
8
|
+
async setConfig() {
|
|
9
|
+
let plugin_dir = path.resolve(this.kernel.homedir, "plugin")
|
|
10
|
+
this.cache = {}
|
|
11
|
+
|
|
12
|
+
let plugin_paths = await glob('**/pinokio.js', { cwd: plugin_dir })
|
|
13
|
+
|
|
14
|
+
let plugins = []
|
|
15
|
+
for(let plugin_path of plugin_paths) {
|
|
16
|
+
let config = await this.kernel.require(path.resolve(plugin_dir, plugin_path))
|
|
17
|
+
let cwd = plugin_path.split("/").slice(0, -1).join("/")
|
|
18
|
+
config.image = "/asset/plugin/" + cwd + "/" + config.icon
|
|
19
|
+
plugins.push({
|
|
20
|
+
href: "/run/plugin/" + plugin_path,
|
|
21
|
+
...config
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.config = {
|
|
26
|
+
menu: plugins.map((plugin) => {
|
|
27
|
+
plugin.text = plugin.title
|
|
28
|
+
return plugin
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
6
32
|
async init() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
33
|
+
let exists = await this.kernel.exists("plugin")
|
|
34
|
+
if (!exists) {
|
|
35
|
+
await fs.promises.mkdir(this.kernel.path("plugin"), { recursive: true }).catch((e) => {})
|
|
36
|
+
}
|
|
37
|
+
let code_exists = await this.kernel.exists("plugin/code")
|
|
38
|
+
if (!code_exists) {
|
|
39
|
+
if (this.kernel.bin.installed && this.kernel.bin.installed.conda && this.kernel.bin.installed.conda.has("git")) {
|
|
11
40
|
await this.kernel.exec({
|
|
12
41
|
//message: "git clone https://github.com/peanutcocktail/plugin",
|
|
13
|
-
message: "git clone https://github.com/pinokiocomputer/plugin",
|
|
14
|
-
|
|
42
|
+
//message: "git clone https://github.com/pinokiocomputer/plugin",
|
|
43
|
+
message: "git clone https://github.com/pinokiocomputer/code",
|
|
44
|
+
path: this.kernel.path("plugin")
|
|
15
45
|
}, (e) => {
|
|
16
46
|
process.stdout.write(e.raw)
|
|
17
47
|
})
|
|
48
|
+
await this.setConfig()
|
|
49
|
+
return
|
|
18
50
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.config = await this.kernel.require(pinokiojs)
|
|
22
|
-
this.cache = {}
|
|
51
|
+
} else {
|
|
52
|
+
await this.setConfig()
|
|
23
53
|
}
|
|
24
54
|
}
|
|
25
55
|
async update() {
|
package/kernel/prototype.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const { glob, sync, hasMagic } = require('glob-gitignore')
|
|
4
4
|
const marked = require('marked')
|
|
5
|
+
const matter = require('gray-matter');
|
|
5
6
|
class Proto {
|
|
6
7
|
constructor(kernel) {
|
|
7
8
|
this.kernel = kernel
|
|
@@ -49,6 +50,28 @@ class Proto {
|
|
|
49
50
|
}
|
|
50
51
|
console.log("Proto init done")
|
|
51
52
|
}
|
|
53
|
+
async ai() {
|
|
54
|
+
let ai_path = this.kernel.path("prototype/system/ai/new/static")
|
|
55
|
+
let mds = await fs.promises.readdir(ai_path)
|
|
56
|
+
mds = mds.filter((md) => {
|
|
57
|
+
return md.endsWith(".md")
|
|
58
|
+
})
|
|
59
|
+
const results = []
|
|
60
|
+
for(let md of mds) {
|
|
61
|
+
let mdpath = path.resolve(ai_path, md)
|
|
62
|
+
let mdstr = await fs.promises.readFile(mdpath, "utf8")
|
|
63
|
+
const { data, content } = matter(mdstr)
|
|
64
|
+
// const html = marked.parse(content)
|
|
65
|
+
let { title, description, ...meta } = data
|
|
66
|
+
results.push({
|
|
67
|
+
title,
|
|
68
|
+
description,
|
|
69
|
+
meta,
|
|
70
|
+
content
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
return results
|
|
74
|
+
}
|
|
52
75
|
async reset() {
|
|
53
76
|
await fs.promises.rm(this.kernel.path("prototype"), { recursive: true })
|
|
54
77
|
}
|
package/kernel/router/index.js
CHANGED
|
@@ -216,6 +216,19 @@ class Router {
|
|
|
216
216
|
await this.fill()
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
+
this.config.apps.http.servers.main.routes.push({
|
|
220
|
+
"handle": [
|
|
221
|
+
{
|
|
222
|
+
"handler": "static_response",
|
|
223
|
+
"status_code": 302,
|
|
224
|
+
"headers": {
|
|
225
|
+
"Location": [
|
|
226
|
+
`https://${this.default_match}/launch?url={http.request.scheme}://{http.request.host}{http.request.uri}`
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
})
|
|
219
232
|
this.mapping = this._mapping
|
|
220
233
|
}
|
|
221
234
|
|
|
@@ -6,6 +6,17 @@ class LocalhostHomeRouter {
|
|
|
6
6
|
this.router.add({ host: this.router.kernel.peer.host, dial: this.router.default_host + ":" + this.router.default_port, match: this.router.default_match })
|
|
7
7
|
this.router.config = {
|
|
8
8
|
"apps": {
|
|
9
|
+
"tls": {
|
|
10
|
+
"automation": {
|
|
11
|
+
"policies": [
|
|
12
|
+
{
|
|
13
|
+
"issuers": [{ "module": "internal" }],
|
|
14
|
+
"on_demand": true
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
9
20
|
"http": {
|
|
10
21
|
"servers": {
|
|
11
22
|
"main": {
|
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
|
/*
|
|
@@ -1018,6 +1019,7 @@ class Shell {
|
|
|
1018
1019
|
return params
|
|
1019
1020
|
}
|
|
1020
1021
|
async exec(params) {
|
|
1022
|
+
this.parser = new ShellParser()
|
|
1021
1023
|
params = await this.activate(params)
|
|
1022
1024
|
this.cmd = this.build(params)
|
|
1023
1025
|
let res = await new Promise((resolve, reject) => {
|
|
@@ -1046,6 +1048,22 @@ class Shell {
|
|
|
1046
1048
|
}
|
|
1047
1049
|
this.monitor = this.monitor + data
|
|
1048
1050
|
this.monitor = this.monitor.slice(-300) // last 300
|
|
1051
|
+
|
|
1052
|
+
let notifications = this.parser.processData(data)
|
|
1053
|
+
if (notifications.length > 0) {
|
|
1054
|
+
console.log({ notifications })
|
|
1055
|
+
for(let notif of notifications) {
|
|
1056
|
+
if (notif.type !== "bell") {
|
|
1057
|
+
Util.push({
|
|
1058
|
+
image: path.resolve(__dirname, "../server/public/pinokio-black.png"),
|
|
1059
|
+
message: notif.title,
|
|
1060
|
+
sound: true,
|
|
1061
|
+
timeout: 30,
|
|
1062
|
+
})
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1049
1067
|
if (!this.done) {
|
|
1050
1068
|
|
|
1051
1069
|
// "request cursor position" handling: https://github.com/microsoft/node-pty/issues/535
|
|
@@ -1114,8 +1132,10 @@ class Shell {
|
|
|
1114
1132
|
this.ptyProcess = undefined
|
|
1115
1133
|
// automatically remove the shell from this.kernel.shells
|
|
1116
1134
|
this.kernel.shell.rm(this.id)
|
|
1117
|
-
this.
|
|
1118
|
-
|
|
1135
|
+
if (!this.mute) {
|
|
1136
|
+
this.ondata({ raw: `\r\n\r\n██ Terminated Shell ${this.id}\r\n████\r\n` })
|
|
1137
|
+
this.ondata({ raw: "", type: "shell.kill" })
|
|
1138
|
+
}
|
|
1119
1139
|
cb()
|
|
1120
1140
|
} else {
|
|
1121
1141
|
kill(this.ptyProcess.pid, "SIGKILL", true)
|
|
@@ -1123,13 +1143,17 @@ class Shell {
|
|
|
1123
1143
|
this.ptyProcess = undefined
|
|
1124
1144
|
// automatically remove the shell from this.kernel.shells
|
|
1125
1145
|
this.kernel.shell.rm(this.id)
|
|
1126
|
-
this.
|
|
1127
|
-
|
|
1146
|
+
if (!this.mute) {
|
|
1147
|
+
this.ondata({ raw: `\r\n\r\n██ Terminated Shell ${this.id}\r\n████\r\n` })
|
|
1148
|
+
this.ondata({ raw: "", type: "shell.kill" })
|
|
1149
|
+
}
|
|
1128
1150
|
}
|
|
1129
1151
|
} else {
|
|
1130
1152
|
this.kernel.shell.rm(this.id)
|
|
1131
|
-
this.
|
|
1132
|
-
|
|
1153
|
+
if (!this.mute) {
|
|
1154
|
+
this.ondata({ raw: `\r\n\r\n██ Terminated Shell ${this.id}\r\n████\r\n` })
|
|
1155
|
+
this.ondata({ raw: "", type: "shell.kill" })
|
|
1156
|
+
}
|
|
1133
1157
|
}
|
|
1134
1158
|
|
|
1135
1159
|
|
|
@@ -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
|
@@ -142,59 +142,66 @@ class Shells {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
*/
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
let
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (handler.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
matches[2]
|
|
156
|
-
|
|
157
|
-
let re = new RegExp(matches[1], matches[2])
|
|
158
|
-
let test = re.exec(sh.monitor)
|
|
159
|
-
if (test && test.length > 0) {
|
|
160
|
-
// reset monitor
|
|
161
|
-
sh.monitor = ""
|
|
162
|
-
let params = this.kernel.template.render(handler.notify, { event: test })
|
|
163
|
-
if (params.image) {
|
|
164
|
-
params.contentImage = path.resolve(req.cwd, params.image)
|
|
145
|
+
try {
|
|
146
|
+
if (params.on && Array.isArray(params.on)) {
|
|
147
|
+
for(let i=0; i<params.on.length; i++) {
|
|
148
|
+
let handler = params.on[i]
|
|
149
|
+
// regexify
|
|
150
|
+
//let matches = /^\/([^\/]+)\/([dgimsuy]*)$/.exec(handler.event)
|
|
151
|
+
if (handler.event) {
|
|
152
|
+
if (handler.notify) {
|
|
153
|
+
// notify is a special case. check by line
|
|
154
|
+
let matches = /^\/(.+)\/([dgimsuy]*)$/gs.exec(handler.event)
|
|
155
|
+
if (!/g/.test(matches[2])) {
|
|
156
|
+
matches[2] += "g" // if g option is not included, include it (need it for matchAll)
|
|
165
157
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (stream.cleaned) {
|
|
175
|
-
let line = stream.cleaned.replaceAll(/[\r\n]/g, "")
|
|
176
|
-
let rendered_event = [...line.matchAll(re)]
|
|
177
|
-
// 3. if the rendered expression is truthy, run the "run" script
|
|
178
|
-
if (rendered_event.length > 0) {
|
|
179
|
-
stream.matches = rendered_event
|
|
180
|
-
if (handler.kill) {
|
|
181
|
-
m = rendered_event[0]
|
|
182
|
-
matched_index = i
|
|
183
|
-
sh.kill()
|
|
158
|
+
let re = new RegExp(matches[1], matches[2])
|
|
159
|
+
let test = re.exec(sh.monitor)
|
|
160
|
+
if (test && test.length > 0) {
|
|
161
|
+
// reset monitor
|
|
162
|
+
sh.monitor = ""
|
|
163
|
+
let params = this.kernel.template.render(handler.notify, { event: test })
|
|
164
|
+
if (params.image) {
|
|
165
|
+
params.contentImage = path.resolve(req.cwd, params.image)
|
|
184
166
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
167
|
+
Util.push(params)
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
let matches = /^\/(.+)\/([dgimsuy]*)$/gs.exec(handler.event)
|
|
171
|
+
if (!/g/.test(matches[2])) {
|
|
172
|
+
matches[2] += "g" // if g option is not included, include it (need it for matchAll)
|
|
173
|
+
}
|
|
174
|
+
let re = new RegExp(matches[1], matches[2])
|
|
175
|
+
if (stream.cleaned) {
|
|
176
|
+
let line = stream.cleaned.replaceAll(/[\r\n]/g, "")
|
|
177
|
+
let rendered_event = [...line.matchAll(re)]
|
|
178
|
+
// 3. if the rendered expression is truthy, run the "run" script
|
|
179
|
+
if (rendered_event.length > 0) {
|
|
180
|
+
stream.matches = rendered_event
|
|
181
|
+
if (handler.kill) {
|
|
182
|
+
m = rendered_event[0]
|
|
183
|
+
matched_index = i
|
|
184
|
+
sh.kill()
|
|
185
|
+
}
|
|
186
|
+
if (handler.done) {
|
|
187
|
+
m = rendered_event[0]
|
|
188
|
+
matched_index = i
|
|
189
|
+
sh.continue()
|
|
190
|
+
}
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
if (ondata) {
|
|
198
|
+
ondata(stream)
|
|
199
|
+
}
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.log("Capture error", e)
|
|
202
|
+
ondata({ raw: e.stack })
|
|
203
|
+
sh.mute = true
|
|
204
|
+
sh.kill()
|
|
198
205
|
}
|
|
199
206
|
})
|
|
200
207
|
/*
|
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) {
|