@pasko70/pibo 3.0.2 → 3.1.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.
Files changed (69) hide show
  1. package/compute-image/Dockerfile +47 -0
  2. package/compute-image/Dockerfile.dockerignore +5 -0
  3. package/dist/agent-runtime/contract.js +78 -28
  4. package/dist/agent-runtime/portable-history.js +19 -9
  5. package/dist/agent-runtime/registry.js +8 -2
  6. package/dist/agent-runtime/routed-session.js +12 -6
  7. package/dist/agent-runtimes/pi/routed-session.js +10 -4
  8. package/dist/apps/chat/agent-store.js +55 -3
  9. package/dist/apps/chat/data/project-service.js +119 -15
  10. package/dist/apps/chat/data/room-service.js +21 -0
  11. package/dist/apps/chat/loop-api.js +10 -2
  12. package/dist/apps/chat/web-app.js +84 -25
  13. package/dist/apps/chat-ui/assets/{dist-CvaPTBTN.js → dist-CUQJqggN.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-BPotHecb.js → dist-ClUlQWYN.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-BzqQKeVO.js → dist-Djul9BmZ.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-paagejNj.js → dist-GD0JGdCi.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-B6GZgWSj.js → dist-W5HOqHym.js} +1 -1
  18. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  19. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  20. package/dist/apps/chat-ui/index.html +2 -2
  21. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  22. package/dist/apps/chat-vscode-web/index.html +1 -1
  23. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  24. package/dist/compute/cli.js +35 -16
  25. package/dist/compute/docker.js +341 -94
  26. package/dist/config/config.js +3 -0
  27. package/dist/core/session-router.js +33 -105
  28. package/dist/data/pibo-store.js +8 -1
  29. package/dist/data/schema.js +125 -5
  30. package/dist/debug/agents.js +91 -89
  31. package/dist/debug/index.js +2 -0
  32. package/dist/debug/pty.js +127 -48
  33. package/dist/gateway/cli.js +4 -0
  34. package/dist/loops/channel.js +3 -1
  35. package/dist/loops/cli.js +2 -1
  36. package/dist/loops/service.js +90 -11
  37. package/dist/loops/store.js +100 -74
  38. package/dist/mcp/agent-context.js +13 -26
  39. package/dist/mcp/commands/info.js +3 -1
  40. package/dist/mcp/config-command.js +46 -2
  41. package/dist/mcp/config.js +18 -4
  42. package/dist/plugins/context-files-store.js +6 -6
  43. package/dist/previews/base-url.js +34 -0
  44. package/dist/previews/config.js +2 -32
  45. package/dist/ralph/store.js +5 -0
  46. package/dist/reliability/store.js +174 -111
  47. package/dist/session-ui/terminalRows.js +2 -1
  48. package/dist/subagents/observation-query.js +121 -0
  49. package/dist/tools/agent-browser-leases.js +58 -23
  50. package/dist/tools/agent-browser-wrapper.js +1 -1
  51. package/dist/tools/browser-use-leases.js +129 -16
  52. package/dist/tools/browser-use-wrapper.js +1 -1
  53. package/dist/tools/index.js +27 -11
  54. package/dist/tools/python-runtime.js +10 -3
  55. package/dist/tools/registry.js +1 -1
  56. package/dist/tools/runtime/node-backend.js +16 -5
  57. package/dist/tools/runtime/node-worker-source.js +112 -33
  58. package/dist/tools/runtime/python-backend.js +16 -5
  59. package/dist/tools/runtime/python-worker-source.js +167 -16
  60. package/dist/tools/runtime/registry.js +23 -10
  61. package/dist/user-skills/store.js +0 -1
  62. package/npm-shrinkwrap.json +9 -2
  63. package/package.json +8 -2
  64. package/scripts/docker-entrypoint.sh +46 -0
  65. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  66. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  67. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +0 -228
  68. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +0 -1
  69. package/dist/apps/chat-vscode-web/assets/index-U-MSErGa.js +0 -43
@@ -1,13 +1,151 @@
1
1
  export const PYTHON_RUNTIME_WORKER_SOURCE = String.raw `
2
- import contextlib
2
+ import contextvars
3
3
  import inspect
4
- import io
5
4
  import json
5
+ import os
6
6
  import signal
7
+ import subprocess
7
8
  import sys
9
+ import threading
8
10
  import traceback
9
11
 
10
12
  user_globals = {"__name__": "__pibo_runtime__"}
13
+ protocol_stream = os.fdopen(3, "w", encoding="utf-8", buffering=1, closefd=False)
14
+ output_context = contextvars.ContextVar("pibo_runtime_output", default=None)
15
+ original_stdout = sys.stdout
16
+ original_stderr = sys.stderr
17
+ original_popen = subprocess.Popen
18
+
19
+
20
+ def append_output(output, stream, value):
21
+ text = str(value)
22
+ if output is None:
23
+ target = original_stdout if stream == "stdout" else original_stderr
24
+ target.write(text)
25
+ target.flush()
26
+ return
27
+ with output["lock"]:
28
+ output[stream].append(text)
29
+
30
+
31
+ class BinaryOutputProxy:
32
+ def __init__(self, stream, fallback):
33
+ self.stream = stream
34
+ self.fallback = fallback
35
+
36
+ def write(self, value):
37
+ data = bytes(value)
38
+ append_output(output_context.get(), self.stream, data.decode("utf-8", "replace"))
39
+ return len(data)
40
+
41
+ def flush(self):
42
+ self.fallback.flush()
43
+
44
+ def __getattr__(self, name):
45
+ return getattr(self.fallback, name)
46
+
47
+
48
+ class OutputProxy:
49
+ def __init__(self, stream, fallback):
50
+ self.stream = stream
51
+ self.fallback = fallback
52
+ self.encoding = getattr(fallback, "encoding", "utf-8")
53
+ self.errors = getattr(fallback, "errors", "replace")
54
+ self.buffer = BinaryOutputProxy(stream, fallback.buffer)
55
+
56
+ def write(self, value):
57
+ append_output(output_context.get(), self.stream, value)
58
+ return len(str(value))
59
+
60
+ def flush(self):
61
+ self.fallback.flush()
62
+
63
+ def __getattr__(self, name):
64
+ return getattr(self.fallback, name)
65
+
66
+
67
+ sys.stdout = OutputProxy("stdout", original_stdout)
68
+ sys.stderr = OutputProxy("stderr", original_stderr)
69
+
70
+
71
+ def drain_child_output(read_fd, output, stream):
72
+ try:
73
+ with os.fdopen(read_fd, "rb", closefd=True) as source:
74
+ while True:
75
+ chunk = source.read(65536)
76
+ if not chunk:
77
+ return
78
+ append_output(output, stream, chunk.decode("utf-8", "replace"))
79
+ except Exception:
80
+ return
81
+
82
+
83
+ def join_child_output(threads):
84
+ for thread in threads:
85
+ thread.join(0.2)
86
+
87
+
88
+ def routed_popen(*args, **kwargs):
89
+ output = output_context.get()
90
+ if output is None:
91
+ return original_popen(*args, **kwargs)
92
+
93
+ options = dict(kwargs)
94
+ routes = []
95
+ if options.get("stdout") is None:
96
+ read_fd, write_fd = os.pipe()
97
+ options["stdout"] = write_fd
98
+ routes.append((read_fd, write_fd, "stdout"))
99
+ if options.get("stderr") is None:
100
+ read_fd, write_fd = os.pipe()
101
+ options["stderr"] = write_fd
102
+ routes.append((read_fd, write_fd, "stderr"))
103
+
104
+ try:
105
+ process = original_popen(*args, **options)
106
+ except Exception:
107
+ for read_fd, write_fd, _ in routes:
108
+ os.close(read_fd)
109
+ os.close(write_fd)
110
+ raise
111
+
112
+ process_threads = []
113
+ for read_fd, write_fd, stream in routes:
114
+ os.close(write_fd)
115
+ thread = threading.Thread(target=drain_child_output, args=(read_fd, output, stream), daemon=True)
116
+ with output["lock"]:
117
+ output["threads"].append(thread)
118
+ process_threads.append(thread)
119
+ thread.start()
120
+
121
+ if process_threads:
122
+ original_wait = process.wait
123
+ original_poll = process.poll
124
+ original_communicate = process.communicate
125
+
126
+ def wait_with_output(*wait_args, **wait_kwargs):
127
+ result = original_wait(*wait_args, **wait_kwargs)
128
+ join_child_output(process_threads)
129
+ return result
130
+
131
+ def poll_with_output(*poll_args, **poll_kwargs):
132
+ result = original_poll(*poll_args, **poll_kwargs)
133
+ if result is not None:
134
+ join_child_output(process_threads)
135
+ return result
136
+
137
+ def communicate_with_output(*communicate_args, **communicate_kwargs):
138
+ result = original_communicate(*communicate_args, **communicate_kwargs)
139
+ join_child_output(process_threads)
140
+ return result
141
+
142
+ process.wait = wait_with_output
143
+ process.poll = poll_with_output
144
+ process.communicate = communicate_with_output
145
+ return process
146
+
147
+
148
+ subprocess.Popen = routed_popen
11
149
 
12
150
 
13
151
  def bounded(value, max_bytes=8192):
@@ -70,31 +208,44 @@ def error_summary(exc):
70
208
  return out
71
209
 
72
210
 
211
+ def output_snapshot(output):
212
+ with output["lock"]:
213
+ threads = list(output["threads"])
214
+ for thread in threads:
215
+ thread.join(0.05)
216
+ with output["lock"]:
217
+ return "".join(output["stdout"]), "".join(output["stderr"])
218
+
219
+
73
220
  def execute(req):
74
221
  mode = req.get("mode") or "exec"
75
222
  code = req.get("code") or ""
76
223
  if mode == "auto":
77
224
  mode = "exec"
78
- stdout = io.StringIO()
79
- stderr = io.StringIO()
225
+ output = {"stdout": [], "stderr": [], "threads": [], "lock": threading.Lock()}
226
+ token = output_context.set(output)
80
227
  try:
81
- with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
82
- if mode == "eval":
83
- value = eval(compile(code, "<pibo-runtime>", "eval"), user_globals, user_globals)
84
- else:
85
- exec(compile(code, "<pibo-runtime>", "exec"), user_globals, user_globals)
86
- value = None
228
+ if mode == "eval":
229
+ value = eval(compile(code, "<pibo-runtime>", "eval"), user_globals, user_globals)
230
+ else:
231
+ exec(compile(code, "<pibo-runtime>", "exec"), user_globals, user_globals)
232
+ value = None
233
+ stdout, stderr = output_snapshot(output)
87
234
  return {
88
235
  "id": req.get("id"),
89
236
  "status": "ok",
90
- "stdout": stdout.getvalue(),
91
- "stderr": stderr.getvalue(),
237
+ "stdout": stdout,
238
+ "stderr": stderr,
92
239
  "result": summarize(value) if value is not None else None,
93
240
  }
94
241
  except KeyboardInterrupt as exc:
95
- return {"id": req.get("id"), "status": "interrupted", "stdout": stdout.getvalue(), "stderr": stderr.getvalue(), "error": error_summary(exc)}
242
+ stdout, stderr = output_snapshot(output)
243
+ return {"id": req.get("id"), "status": "interrupted", "stdout": stdout, "stderr": stderr, "error": error_summary(exc)}
96
244
  except Exception as exc:
97
- return {"id": req.get("id"), "status": "error", "stdout": stdout.getvalue(), "stderr": stderr.getvalue(), "error": error_summary(exc)}
245
+ stdout, stderr = output_snapshot(output)
246
+ return {"id": req.get("id"), "status": "error", "stdout": stdout, "stderr": stderr, "error": error_summary(exc)}
247
+ finally:
248
+ output_context.reset(token)
98
249
 
99
250
 
100
251
  def inspect_value(req):
@@ -147,8 +298,8 @@ def list_vars(req):
147
298
 
148
299
 
149
300
  def write_response(resp):
150
- sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
151
- sys.stdout.flush()
301
+ protocol_stream.write(json.dumps(resp, ensure_ascii=False) + "\n")
302
+ protocol_stream.flush()
152
303
 
153
304
 
154
305
  def main():
@@ -106,12 +106,7 @@ export class RuntimeSessionRegistry {
106
106
  result.executionCount = session.executionCount;
107
107
  session.lastExecAt = startedAt;
108
108
  session.updatedAt = nowIso();
109
- if (result.status === "failed" && !session.backend.isAlive()) {
110
- session.status = "failed";
111
- }
112
- else {
113
- session.status = "idle";
114
- }
109
+ session.status = session.backend.isAlive() ? "idle" : "failed";
115
110
  this.appendHistory(session, {
116
111
  id: randomUUID(),
117
112
  startedAt,
@@ -131,6 +126,7 @@ export class RuntimeSessionRegistry {
131
126
  if (!session)
132
127
  return notFoundInspect(input.sessionId);
133
128
  const result = await session.backend.inspect(input);
129
+ this.reconcileSession(session);
134
130
  return { ...result, sessionId: session.sessionId };
135
131
  }
136
132
  async vars(controllerPiboSessionId, input) {
@@ -138,6 +134,7 @@ export class RuntimeSessionRegistry {
138
134
  if (!session)
139
135
  return notFoundVars(input.sessionId);
140
136
  const result = await session.backend.vars(input);
137
+ this.reconcileSession(session);
141
138
  return { ...result, sessionId: session.sessionId };
142
139
  }
143
140
  async interrupt(controllerPiboSessionId, input) {
@@ -145,7 +142,12 @@ export class RuntimeSessionRegistry {
145
142
  const sessionId = input.sessionId ?? session?.sessionId ?? "auto";
146
143
  if (!session)
147
144
  return { status: "not_found", sessionId, message: `Runtime session "${sessionId}" was not found.` };
145
+ if (session.status === "closed" || session.status === "failed")
146
+ return { status: "failed", sessionId, message: "Runtime worker is not alive" };
147
+ if (session.status !== "busy")
148
+ return { status: "ok", sessionId, message: "Runtime session is idle; no active execution to interrupt" };
148
149
  const result = await session.backend.interrupt();
150
+ this.reconcileSession(session);
149
151
  return { ...result, sessionId: session.sessionId };
150
152
  }
151
153
  async close(controllerPiboSessionId, input) {
@@ -176,6 +178,7 @@ export class RuntimeSessionRegistry {
176
178
  status: "ok",
177
179
  sessions: [...this.sessions.values()]
178
180
  .filter((session) => session.controllerPiboSessionId === controllerPiboSessionId)
181
+ .map((session) => this.reconcileSession(session))
179
182
  .map(toRecord),
180
183
  };
181
184
  }
@@ -189,6 +192,7 @@ export class RuntimeSessionRegistry {
189
192
  }
190
193
  pruneIdle(now = Date.now(), idleTimeoutMs = 30 * 60 * 1000) {
191
194
  for (const session of this.sessions.values()) {
195
+ this.reconcileSession(session);
192
196
  if (session.status !== "idle")
193
197
  continue;
194
198
  if (now - Date.parse(session.updatedAt) > idleTimeoutMs) {
@@ -208,10 +212,12 @@ export class RuntimeSessionRegistry {
208
212
  };
209
213
  }
210
214
  getDefault(controllerPiboSessionId, runtime) {
211
- return [...this.sessions.values()].find((session) => session.controllerPiboSessionId === controllerPiboSessionId &&
212
- session.runtime === runtime &&
213
- session.status !== "closed" &&
214
- session.status !== "failed");
215
+ return [...this.sessions.values()].find((session) => {
216
+ if (session.controllerPiboSessionId !== controllerPiboSessionId || session.runtime !== runtime)
217
+ return false;
218
+ const status = this.reconcileSession(session).status;
219
+ return status !== "closed" && status !== "failed";
220
+ });
215
221
  }
216
222
  async getOrStartDefault(controllerPiboSessionId, input) {
217
223
  const runtime = input.runtime ?? "python";
@@ -230,6 +236,13 @@ export class RuntimeSessionRegistry {
230
236
  const session = this.sessions.get(sessionId);
231
237
  if (!session || session.controllerPiboSessionId !== controllerPiboSessionId)
232
238
  return undefined;
239
+ return this.reconcileSession(session);
240
+ }
241
+ reconcileSession(session) {
242
+ if (session.status !== "closed" && session.status !== "failed" && !session.backend.isAlive()) {
243
+ session.status = "failed";
244
+ session.updatedAt = nowIso();
245
+ }
233
246
  return session;
234
247
  }
235
248
  appendHistory(session, entry) {
@@ -148,7 +148,6 @@ export function updateUserSkill(id, input, cwd = process.cwd()) {
148
148
  writeFileSync(skillPath, markdown, "utf-8");
149
149
  }
150
150
  else if (name !== existing.name || description !== parsed.description) {
151
- const currentMarkdown = existsSync(existing.path) ? readFileSync(existing.path, "utf-8") : "";
152
151
  const { body } = parseSkillMd(currentMarkdown);
153
152
  writeFileSync(skillPath, buildSkillMd(name, description, body), "utf-8");
154
153
  }
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.0.2",
9
+ "version": "3.1.0",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
13
13
  "dependencies": {
14
+ "@balena/dockerignore": "1.0.2",
14
15
  "@earendil-works/pi-agent-core": "0.84.2",
15
16
  "@earendil-works/pi-ai": "0.84.2",
16
17
  "@earendil-works/pi-coding-agent": "0.84.2",
@@ -968,6 +969,12 @@
968
969
  "node": ">=6.9.0"
969
970
  }
970
971
  },
972
+ "node_modules/@balena/dockerignore": {
973
+ "version": "1.0.2",
974
+ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
975
+ "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
976
+ "license": "Apache-2.0"
977
+ },
971
978
  "node_modules/@better-auth/core": {
972
979
  "version": "1.6.30",
973
980
  "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.30.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"
@@ -14,7 +14,12 @@
14
14
  "docs/ops/**",
15
15
  "README.md",
16
16
  "src/mcp/LICENSE.mcp-cli",
17
- "npm-shrinkwrap.json"
17
+ "npm-shrinkwrap.json",
18
+ "compute-image/Dockerfile",
19
+ "compute-image/Dockerfile.dockerignore",
20
+ "scripts/docker-entrypoint.sh",
21
+ "scripts/prepare-browser-use-wrapper.sh",
22
+ "scripts/prepare-agent-browser-wrapper.sh"
18
23
  ],
19
24
  "bin": {
20
25
  "pibo": "dist/bin/pibo.js",
@@ -58,6 +63,7 @@
58
63
  "postpack": "node scripts/package-shrinkwrap.mjs clean"
59
64
  },
60
65
  "dependencies": {
66
+ "@balena/dockerignore": "1.0.2",
61
67
  "@earendil-works/pi-agent-core": "0.84.2",
62
68
  "@earendil-works/pi-ai": "0.84.2",
63
69
  "@earendil-works/pi-coding-agent": "0.84.2",
@@ -0,0 +1,46 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Xvfb starten (virtueller Display-Server für Browser-Automation)
5
+ if ! pgrep -x Xvfb >/dev/null 2>&1; then
6
+ echo "[docker-entrypoint] Starting Xvfb on DISPLAY=:99 ..."
7
+ Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp &
8
+ sleep 0.5
9
+ fi
10
+
11
+ export DISPLAY=:99
12
+
13
+ # Sicherstellen, dass die Browser-Wrapper existieren
14
+ if [ ! -x "$HOME/.pibo/tools/browser-use/home/bin/browser-use" ]; then
15
+ echo "[docker-entrypoint] Preparing browser-use wrapper ..."
16
+ /app/scripts/prepare-browser-use-wrapper.sh
17
+ fi
18
+
19
+ if [ ! -x "$HOME/.pibo/tools/agent-browser/home/bin/agent-browser" ]; then
20
+ echo "[docker-entrypoint] Preparing agent-browser wrapper ..."
21
+ /app/scripts/prepare-agent-browser-wrapper.sh
22
+ fi
23
+
24
+ # PATH erweitern
25
+ export PATH="$HOME/.pibo/tools/agent-browser/home/bin:$HOME/.pibo/tools/agent-browser/node/node_modules/.bin:$HOME/.pibo/tools/browser-use/home/bin:$HOME/.pibo/tools/browser-use/.venv/bin:$PATH"
26
+ export BROWSER_USE_HOME="$HOME/.pibo/tools/browser-use/home"
27
+ export AGENT_BROWSER_HOME="$HOME/.pibo/tools/agent-browser/home"
28
+
29
+ # Pibo-CLI-Argumente verarbeiten
30
+ case "${1:-gateway}" in
31
+ gateway)
32
+ echo "[docker-entrypoint] Starting Pibo gateway on 0.0.0.0:4789 ..."
33
+ exec node -e "import('./dist/gateway/server.js').then(m => m.runGatewayServer({ host: '0.0.0.0' }))"
34
+ ;;
35
+ gateway:web)
36
+ echo "[docker-entrypoint] Starting Pibo gateway:web with local auth on 0.0.0.0:4789 ..."
37
+ exec node -e "import('./dist/gateway/web.js').then(m => m.runWebGatewayServer({ authMode: 'local', web: { host: '0.0.0.0' } }))"
38
+ ;;
39
+ shell|bash|sh)
40
+ exec /bin/sh
41
+ ;;
42
+ *)
43
+ # Alles andere direkt an Pibo weiterleiten
44
+ exec node dist/bin/pibo.js "$@"
45
+ ;;
46
+ esac
@@ -0,0 +1,70 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ real_agent_browser="$HOME/.pibo/tools/agent-browser/node/node_modules/.bin/agent-browser"
5
+ wrapper_dir="$HOME/.pibo/tools/agent-browser/home/bin"
6
+ wrapper_path="$wrapper_dir/agent-browser"
7
+
8
+ mkdir -p "$wrapper_dir" "$HOME/.pibo/tools/agent-browser/home/profiles"
9
+
10
+ cat > "$wrapper_path" <<'WRAPPER'
11
+ #!/usr/bin/env bash
12
+ set -euo pipefail
13
+
14
+ real_agent_browser="$HOME/.pibo/tools/agent-browser/node/node_modules/.bin/agent-browser"
15
+ default_home="$HOME/.pibo/tools/agent-browser/home"
16
+
17
+ if [ -z "${AGENT_BROWSER_HOME:-}" ]; then
18
+ export AGENT_BROWSER_HOME="$default_home"
19
+ fi
20
+ mkdir -p "$AGENT_BROWSER_HOME" "$AGENT_BROWSER_HOME/profiles"
21
+
22
+ if [ "${PIBO_AGENT_BROWSER_PRESERVE_HOME:-0}" != "1" ]; then
23
+ export HOME="$AGENT_BROWSER_HOME"
24
+ fi
25
+
26
+ explicit_runtime=0
27
+ fresh_profile=0
28
+ show_help=0
29
+ starts_browser=0
30
+ args=()
31
+
32
+ print_wrapper_help() {
33
+ cat <<'HELP'
34
+ Pibo agent-browser wrapper
35
+ - keeps Agent Browser state under AGENT_BROWSER_HOME by default
36
+ - prepends a persistent Pibo profile for browser-launching commands
37
+ - pass --fresh-profile to disable Pibo profile injection
38
+ HELP
39
+ }
40
+
41
+ while [ "$#" -gt 0 ]; do
42
+ case "$1" in
43
+ --help|-h) show_help=1; args+=("$1") ;;
44
+ --fresh-profile) fresh_profile=1 ;;
45
+ --profile|--state|--cdp|--auto-connect|--provider|--engine|--executable-path|--config)
46
+ explicit_runtime=1; args+=("$1"); if [ "$#" -gt 1 ]; then shift; args+=("$1"); fi ;;
47
+ --profile=*|--state=*|--cdp=*|--auto-connect=*|--provider=*|--engine=*|--executable-path=*|--config=*)
48
+ explicit_runtime=1; args+=("$1") ;;
49
+ open|launch|new|goto) starts_browser=1; args+=("$1") ;;
50
+ *) args+=("$1") ;;
51
+ esac
52
+ shift
53
+ done
54
+
55
+ if [ "$show_help" -eq 1 ]; then
56
+ print_wrapper_help
57
+ exec "$real_agent_browser" "${args[@]}"
58
+ fi
59
+
60
+ if [ "$starts_browser" -eq 1 ] && [ "$fresh_profile" -eq 0 ] && [ "$explicit_runtime" -eq 0 ]; then
61
+ profile_dir=${AGENT_BROWSER_PROFILE:-$AGENT_BROWSER_HOME/profiles/PIBo}
62
+ mkdir -p "$profile_dir"
63
+ exec "$real_agent_browser" --profile "$profile_dir" "${args[@]}"
64
+ fi
65
+
66
+ exec "$real_agent_browser" "${args[@]}"
67
+ WRAPPER
68
+
69
+ chmod +x "$wrapper_path"
70
+ printf '%s\n' "$wrapper_path"