@pasko70/pibo 3.2.3 → 3.2.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.
Files changed (70) hide show
  1. package/dist/agent-runtime/auth.js +3 -5
  2. package/dist/agent-runtime/portable-history.js +1 -1
  3. package/dist/agent-runtimes/codex-native/client.js +2 -5
  4. package/dist/agent-runtimes/codex-native/redaction.js +3 -27
  5. package/dist/agent-runtimes/omp/client.js +2 -4
  6. package/dist/agent-runtimes/pi/adapter.js +1 -0
  7. package/dist/agent-runtimes/pi/runtime.js +1 -1
  8. package/dist/apps/chat/agent-store.js +1 -1
  9. package/dist/apps/chat/chat-files.js +3 -1
  10. package/dist/apps/chat/chat-request-normalizers.js +1 -1
  11. package/dist/apps/chat/chat-user-skill-routes.js +7 -0
  12. package/dist/apps/chat/data/project-service.js +49 -1
  13. package/dist/apps/chat/data/session-query-service.js +46 -6
  14. package/dist/apps/chat/loop-api.js +10 -1
  15. package/dist/apps/chat/ralph-api.js +10 -1
  16. package/dist/apps/chat/web-app.js +22 -5
  17. package/dist/apps/chat-ui/assets/{dist-CTsyojIh.js → dist-CTC-_3l0.js} +1 -1
  18. package/dist/apps/chat-ui/assets/{dist-BHio-Mzx.js → dist-CftLEFd5.js} +1 -1
  19. package/dist/apps/chat-ui/assets/{dist-DJ6Bhcuk.js → dist-Dq27Wt2q.js} +1 -1
  20. package/dist/apps/chat-ui/assets/{dist-CPKEoSPK.js → dist-Y4fHV5-Z.js} +1 -1
  21. package/dist/apps/chat-ui/assets/{dist-DNHFRMCk.js → dist-dGF-PDGY.js} +1 -1
  22. package/dist/apps/chat-ui/assets/index-C_P0gXvJ.js +228 -0
  23. package/dist/apps/chat-ui/assets/{index-Dc-6Xupt.css → index-DUGVw259.css} +1 -1
  24. package/dist/apps/chat-ui/index.html +2 -2
  25. package/dist/apps/chat-vscode-web/assets/{index-B1TuQNY5.js → index-B5X0NaRo.js} +4 -4
  26. package/dist/apps/chat-vscode-web/index.html +1 -1
  27. package/dist/apps/cli-ui/InkSessionApp.js +38 -28
  28. package/dist/auth/cli.js +4 -6
  29. package/dist/auth/machine-keys.js +20 -3
  30. package/dist/cli-session/localSessionSource.js +2 -4
  31. package/dist/compute/cli.js +16 -13
  32. package/dist/compute/docker.js +96 -48
  33. package/dist/core/base-prompt.js +27 -5
  34. package/dist/core/context-build.js +5 -15
  35. package/dist/core/sensitive-data-redaction.js +2 -2
  36. package/dist/core/session-router.js +15 -4
  37. package/dist/cron/cli.js +42 -10
  38. package/dist/cron/store.js +1 -1
  39. package/dist/data/cli.js +15 -7
  40. package/dist/data/payload-store.js +55 -7
  41. package/dist/data/schema.js +65 -23
  42. package/dist/debug/index.js +5 -0
  43. package/dist/debug/pty.js +45 -10
  44. package/dist/gateway/cli.js +18 -2
  45. package/dist/gateway/server.js +1 -0
  46. package/dist/loops/service.js +32 -16
  47. package/dist/mcp/runtime-session.js +2 -1
  48. package/dist/pi-packages/cli.js +5 -0
  49. package/dist/previews/cli.js +2 -6
  50. package/dist/previews/config.js +10 -0
  51. package/dist/previews/manager.js +2 -9
  52. package/dist/ralph/service.js +21 -10
  53. package/dist/session-ui/statusViewModel.js +2 -4
  54. package/dist/sessions/pibo-data-store.js +18 -0
  55. package/dist/sessions/store.js +9 -0
  56. package/dist/setup/cli.js +14 -7
  57. package/dist/setup/installation-profiles.js +20 -7
  58. package/dist/shared/trace-event-projection.js +32 -1
  59. package/dist/subagents/tool.js +3 -3
  60. package/dist/tools/browser-use-leases.js +11 -3
  61. package/dist/tools/index.js +0 -1
  62. package/dist/tools/mcp-bridge.js +1 -1
  63. package/dist/tools/runtime/node-worker-source.js +39 -5
  64. package/dist/tools/runtime/python-backend.js +6 -2
  65. package/dist/tools/runtime/registry.js +38 -6
  66. package/dist/vscode/cli.js +1 -2
  67. package/dist/vscode/install.js +21 -14
  68. package/npm-shrinkwrap.json +2 -2
  69. package/package.json +3 -1
  70. package/dist/apps/chat-ui/assets/index-Y6jYzsaC.js +0 -228
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-B1TuQNY5.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-B5X0NaRo.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
10
10
  </head>
11
11
  <body>
@@ -1,7 +1,7 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Box, Text, useApp, useInput } from "ink";
2
+ import { Box, Text, useApp, useInput, usePaste } from "ink";
3
3
  import { isCliSourceError } from "../../cli-session/index.js";
4
- import { buildCompactTerminalRows, buildSlashCommandCatalog, buildTerminalStatusViewModel, commandSupportLabel, filterSlashCommands, formatSlashCommand, groupSlashCommandsForHelp, normalizeCommandErrorDescriptor, progressBarText, } from "../../session-ui/index.js";
4
+ import { buildCompactTerminalRows, buildSlashCommandCatalog, buildTerminalStatusViewModel, commandSupportLabel, filterSlashCommands, formatSlashCommand, groupSlashCommandsForHelp, normalizeCommandErrorDescriptor, progressBarText, redactTerminalSecret, } from "../../session-ui/index.js";
5
5
  import { InkTerminalView } from "./InkTerminalView.js";
6
6
  import { isExpandableTerminalRow } from "./InkTerminalRow.js";
7
7
  const IDENTIFIER_PATTERN = /^(?:user|room|ps|pi|entry|evt|run|create|provider|model|agent)[_:]/i;
@@ -32,6 +32,12 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
32
32
  const closedRef = useRef(false);
33
33
  const stateRef = useRef(state);
34
34
  const pickerActivationRef = useRef({ inFlight: false });
35
+ const commitInputState = useCallback((update) => {
36
+ const next = update(stateRef.current);
37
+ stateRef.current = next;
38
+ setState(next);
39
+ return next;
40
+ }, []);
35
41
  useEffect(() => {
36
42
  stateRef.current = state;
37
43
  }, [state]);
@@ -220,82 +226,89 @@ export function InkSessionApp({ source, initialSessionId, maxRows, maxLineChars,
220
226
  cleanup();
221
227
  };
222
228
  }, [cleanup, initialSessionId, openRoomPicker, openSession, source]);
229
+ usePaste((input) => {
230
+ if (closedRef.current || input.length === 0)
231
+ return;
232
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "text", value: input }));
233
+ });
223
234
  useInput((input, key) => {
224
235
  if (closedRef.current)
225
236
  return;
237
+ const returnTerminatedInput = !key.ctrl && !key.meta && !key.return && (input.endsWith("\r") || input.endsWith("\n"));
238
+ if (returnTerminatedInput) {
239
+ const text = input.slice(0, -1);
240
+ if (text.length > 0)
241
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "text", value: text }));
242
+ }
226
243
  if (key.ctrl && input === "c") {
227
244
  requestExit();
228
245
  return;
229
246
  }
230
247
  if (key.escape) {
231
- setState((current) => reduceInkSessionInputState(current, { type: "escape" }));
248
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "escape" }));
232
249
  return;
233
250
  }
234
251
  if (key.upArrow) {
235
- setState((current) => reduceInkSessionInputState(current, { type: "up" }));
252
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "up" }));
236
253
  return;
237
254
  }
238
255
  if (key.downArrow) {
239
- setState((current) => reduceInkSessionInputState(current, { type: "down" }));
256
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "down" }));
240
257
  return;
241
258
  }
242
259
  if (key.leftArrow) {
243
- setState((current) => reduceInkSessionInputState(current, { type: "left" }));
260
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "left" }));
244
261
  return;
245
262
  }
246
263
  if (key.rightArrow) {
247
- setState((current) => reduceInkSessionInputState(current, { type: "right" }));
264
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "right" }));
248
265
  return;
249
266
  }
250
267
  if (key.home) {
251
- setState((current) => reduceInkSessionInputState(current, { type: "home" }));
268
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "home" }));
252
269
  return;
253
270
  }
254
271
  if (key.end) {
255
- setState((current) => reduceInkSessionInputState(current, { type: "end" }));
272
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "end" }));
256
273
  return;
257
274
  }
258
- if (key.return) {
275
+ if (key.return || returnTerminatedInput) {
259
276
  const submitted = stateRef.current.input;
260
277
  if (submitted.length === 0 && !stateRef.current.picker && !stateRef.current.slashSuggestions && canToggleSelectedRowDetails(stateRef.current)) {
261
- setState((current) => reduceInkSessionInputState(current, { type: "toggle-details" }));
278
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "toggle-details" }));
262
279
  return;
263
280
  }
264
281
  if (stateRef.current.slashSuggestions && !stateRef.current.picker) {
265
282
  const accepted = acceptSlashSuggestion(stateRef.current);
266
283
  if (accepted.runInput) {
267
- setState((current) => ({ ...current, input: "", inputCursor: 0, slashSuggestions: undefined, message: undefined, error: undefined }));
284
+ commitInputState((current) => ({ ...current, input: "", inputCursor: 0, slashSuggestions: undefined, message: undefined, error: undefined }));
268
285
  void submitCommandOrMessage(accepted.runInput);
269
286
  }
270
287
  else {
271
- setState((current) => ({ ...current, input: accepted.input, inputCursor: accepted.input.length, slashSuggestions: undefined, message: `Accepted ${accepted.input.trim()}. Press Enter to run or add arguments.`, error: undefined }));
288
+ commitInputState((current) => ({ ...current, input: accepted.input, inputCursor: accepted.input.length, slashSuggestions: undefined, message: `Accepted ${accepted.input.trim()}. Press Enter to run or add arguments.`, error: undefined }));
272
289
  }
273
290
  return;
274
291
  }
275
292
  if (submitted.trimStart().startsWith("/")) {
276
- setState((current) => reduceInkSessionInputState(current, { type: "enter" }));
293
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "enter" }));
277
294
  void submitCommandOrMessage(submitted);
278
295
  return;
279
296
  }
280
297
  if (stateRef.current.picker) {
281
- setState((current) => reduceInkSessionInputState(current, { type: "enter" }));
298
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "enter" }));
282
299
  void selectPickerItem();
283
300
  return;
284
301
  }
285
- setState((current) => reduceInkSessionInputState(current, { type: "enter" }));
302
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "enter" }));
286
303
  void submitCommandOrMessage(submitted);
287
304
  return;
288
305
  }
289
306
  if (key.backspace || key.delete) {
290
- setState((current) => reduceInkSessionInputState(current, { type: "backspace" }));
291
- return;
292
- }
293
- if (input === "d" && !key.ctrl && !key.meta && stateRef.current.input.length === 0 && !stateRef.current.picker && !stateRef.current.slashSuggestions && canToggleSelectedRowDetails(stateRef.current)) {
294
- setState((current) => reduceInkSessionInputState(current, { type: "toggle-details" }));
307
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "backspace" }));
295
308
  return;
296
309
  }
297
- if (input && !key.ctrl && !key.meta) {
298
- setState((current) => reduceInkSessionInputState(current, { type: "text", value: input }));
310
+ if (input && !returnTerminatedInput && !key.ctrl && !key.meta) {
311
+ commitInputState((current) => reduceInkSessionInputState(current, { type: "text", value: input }));
299
312
  }
300
313
  });
301
314
  return React.createElement(InkSessionAppView, { state, maxRows, maxLineChars });
@@ -540,7 +553,7 @@ export function cliCommandSummaryText(catalog = buildSlashCommandCatalog()) {
540
553
  return `Commands: ${commands.join(" ")} (type / for suggestions, /help for catalog)`;
541
554
  }
542
555
  export function cliCommandHintText() {
543
- return "commands: / opens palette · /status runtime · /room /session navigate · /help catalog · ↑↓ focus rows · d/enter details · ctrl-c exit";
556
+ return "commands: / opens palette · /status runtime · /room /session navigate · /help catalog · ↑↓ focus rows · enter details · ctrl-c exit";
544
557
  }
545
558
  export function cliSessionSlashHelpText(catalog = buildSlashCommandCatalog()) {
546
559
  const grouped = groupSlashCommandsForHelp(catalog);
@@ -1307,10 +1320,7 @@ export function formatCliSessionError(error) {
1307
1320
  return `${message}. Recovery: use /status for source state or restart the CLI if the problem persists.`;
1308
1321
  }
1309
1322
  function redactCliSessionStatusText(text) {
1310
- return text
1311
- .replace(/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, "$1=[redacted]")
1312
- .replace(/\b(api[_-]?key|token|secret|password)\s*[:=]\s*([^\s]+)/gi, "$1=[redacted]")
1313
- .replace(/\b(?:sk|pk|pibo|ghp|github_pat)_[A-Za-z0-9_\-]{8,}\b/g, "[redacted]");
1323
+ return redactTerminalSecret(text);
1314
1324
  }
1315
1325
  function errorMessage(error) {
1316
1326
  return error instanceof Error ? error.message : String(error);
package/dist/auth/cli.js CHANGED
@@ -5,7 +5,7 @@ import { Command } from "commander";
5
5
  import { loadPiboConfig } from "../config/config.js";
6
6
  import { piboHomePath } from "../core/pibo-home.js";
7
7
  import { protectPrivateFileSync } from "../core/private-path.js";
8
- import { generateMachineKey, getDefaultMachineKeyStorePath, importMachineKeyRecord, listMachineKeys, parseMachineKeyRecord, revokeMachineKey, } from "./machine-keys.js";
8
+ import { generateMachineKey, getDefaultMachineKeyStorePath, importMachineKeyRecord, listMachineKeys, parseIsoTimestamp, parseMachineKeyRecord, revokeMachineKey, } from "./machine-keys.js";
9
9
  function printJson(value) {
10
10
  console.log(JSON.stringify(value, null, 2));
11
11
  }
@@ -117,12 +117,10 @@ function writePrivateNewFile(path, content) {
117
117
  function parseExpiresAt(value) {
118
118
  if (value === undefined)
119
119
  return undefined;
120
- const timestamp = Date.parse(value);
121
- if (Number.isNaN(timestamp))
122
- throw new Error("--expires-at must be an ISO timestamp");
123
- if (timestamp <= Date.now())
120
+ const expiresAt = parseIsoTimestamp(value, "--expires-at");
121
+ if (Date.parse(expiresAt) <= Date.now())
124
122
  throw new Error("--expires-at must be in the future");
125
- return new Date(timestamp).toISOString();
123
+ return expiresAt;
126
124
  }
127
125
  export async function runAuthCli(argv = process.argv) {
128
126
  if (argv.length <= 2 || argv[2] === "--help" || argv[2] === "-h") {
@@ -15,11 +15,28 @@ export function getDefaultMachineKeyStorePath() {
15
15
  function hashMachineKeyToken(token) {
16
16
  return createHash("sha256").update(token, "utf8").digest();
17
17
  }
18
- function parseIsoTimestamp(value, field) {
19
- if (typeof value !== "string" || value.length === 0 || Number.isNaN(Date.parse(value))) {
18
+ export function parseIsoTimestamp(value, field) {
19
+ if (typeof value !== "string")
20
+ throw new Error(`${field} must be an ISO timestamp`);
21
+ const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/);
22
+ if (!match)
23
+ throw new Error(`${field} must be an ISO timestamp`);
24
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match;
25
+ const year = Number(yearText);
26
+ const month = Number(monthText);
27
+ const day = Number(dayText);
28
+ const hour = Number(hourText);
29
+ const minute = Number(minuteText);
30
+ const second = Number(secondText);
31
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
32
+ const daysByMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
33
+ if (month < 1 || month > 12 || day < 1 || day > daysByMonth[month - 1] || hour > 23 || minute > 59 || second > 59) {
20
34
  throw new Error(`${field} must be an ISO timestamp`);
21
35
  }
22
- return new Date(value).toISOString();
36
+ const timestamp = Date.parse(value);
37
+ if (Number.isNaN(timestamp))
38
+ throw new Error(`${field} must be an ISO timestamp`);
39
+ return new Date(timestamp).toISOString();
23
40
  }
24
41
  function optionalString(value, field) {
25
42
  if (value === undefined)
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { redactSensitiveText } from "../core/sensitive-data-redaction.js";
2
3
  import { DEFAULT_PIBO_PROFILE_NAME, resolvePiboProfileNameFromRegistryOrDefault, } from "../plugins/builtin.js";
3
4
  import { createDefaultPiboUserProfileRegistry } from "../plugins/user-profile-resources.js";
4
5
  import { createDefaultPiboDataSessionStore } from "../sessions/pibo-data-store.js";
@@ -863,10 +864,7 @@ function parseCliOutputPersistenceState(value) {
863
864
  return { version: 1, piboSessionId: candidate.piboSessionId, deliveries };
864
865
  }
865
866
  export function redactCliSecretText(text) {
866
- return text
867
- .replace(/\b([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, "$1=[redacted]")
868
- .replace(/\b(api[_-]?key|token|secret|password)\s*[:=]\s*([^\s]+)/gi, "$1=[redacted]")
869
- .replace(/\b(?:sk|pk|pibo|ghp|github_pat)_[A-Za-z0-9_\-]{8,}\b/g, "[redacted]");
867
+ return redactSensitiveText(text);
870
868
  }
871
869
  function sessionToSummary(session, defaultRoomId) {
872
870
  return {
@@ -13,11 +13,14 @@ function computeImageBuildConfig() {
13
13
  function printJson(value) {
14
14
  console.log(JSON.stringify(value, null, 2));
15
15
  }
16
- function parsePositiveIntegerOption(value) {
17
- if (value === undefined || value.trim() === "")
18
- return undefined;
19
- const parsed = Number(value);
20
- return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
16
+ function positiveIntegerOption(optionName) {
17
+ return (value) => {
18
+ const parsed = Number(value);
19
+ if (value.trim() === "" || !Number.isInteger(parsed) || parsed <= 0) {
20
+ throw new InvalidArgumentError(`${optionName} must be a positive integer`);
21
+ }
22
+ return parsed;
23
+ };
21
24
  }
22
25
  function parseNonNegativeNumber(value) {
23
26
  const parsed = Number(value);
@@ -233,8 +236,8 @@ Next:
233
236
  .description("Create a one-time worker from the current Docker image")
234
237
  .option("--name <name>", "Set the container name")
235
238
  .option("--holder <holder>", "Tag the container holder")
236
- .option("--ttl-seconds <n>", "Tag the worker TTL in seconds")
237
- .option("--idle-seconds <n>", "Tag the worker idle retention in seconds")
239
+ .option("--ttl-seconds <n>", "Tag the worker TTL in seconds", positiveIntegerOption("--ttl-seconds"))
240
+ .option("--idle-seconds <n>", "Tag the worker idle retention in seconds", positiveIntegerOption("--idle-seconds"))
238
241
  .option("--ralph-job-id <id>", "Tag the Ralph job id when this worker is Ralph-owned")
239
242
  .option("--ralph-run-id <id>", "Tag the Ralph run id when this worker is Ralph-owned")
240
243
  .addHelpText("after", `
@@ -262,8 +265,8 @@ Environment:
262
265
  imageName: image.imageName,
263
266
  name: options.name,
264
267
  holder: options.holder,
265
- ttlSeconds: parsePositiveIntegerOption(options.ttlSeconds),
266
- idleSeconds: parsePositiveIntegerOption(options.idleSeconds),
268
+ ttlSeconds: options.ttlSeconds,
269
+ idleSeconds: options.idleSeconds,
267
270
  ralphJobId: options.ralphJobId,
268
271
  ralphRunId: options.ralphRunId,
269
272
  });
@@ -285,8 +288,8 @@ Next:
285
288
  .requiredOption("--worktree <name>", "Name the Git worktree and branch")
286
289
  .option("--repo <path>", "Use this repository", WORKSPACE_DIR)
287
290
  .option("--holder <holder>", "Tag the container holder")
288
- .option("--ttl-seconds <n>", "Tag the worker TTL in seconds")
289
- .option("--idle-seconds <n>", "Tag the worker idle retention in seconds")
291
+ .option("--ttl-seconds <n>", "Tag the worker TTL in seconds", positiveIntegerOption("--ttl-seconds"))
292
+ .option("--idle-seconds <n>", "Tag the worker idle retention in seconds", positiveIntegerOption("--idle-seconds"))
290
293
  .option("--ralph-job-id <id>", "Tag the Ralph job id when this worker is Ralph-owned")
291
294
  .option("--ralph-run-id <id>", "Tag the Ralph run id when this worker is Ralph-owned")
292
295
  .addHelpText("after", `
@@ -316,8 +319,8 @@ Example:
316
319
  worktreeName: options.worktree,
317
320
  imageName: image.imageName,
318
321
  holder: options.holder,
319
- ttlSeconds: parsePositiveIntegerOption(options.ttlSeconds),
320
- idleSeconds: parsePositiveIntegerOption(options.idleSeconds),
322
+ ttlSeconds: options.ttlSeconds,
323
+ idleSeconds: options.idleSeconds,
321
324
  ralphJobId: options.ralphJobId,
322
325
  ralphRunId: options.ralphRunId,
323
326
  });
@@ -401,6 +401,7 @@ function buildComputeWorkerMetadataLabels(options) {
401
401
  `${LABEL_PORT_BLOCK}=${options.portBlock}`,
402
402
  `${LABEL_TTL_SECONDS}=${options.ttlSeconds}`,
403
403
  `${LABEL_IDLE_SECONDS}=${options.idleSeconds}`,
404
+ `${LABEL_LAST_USED_AT}=${options.createdAt}`,
404
405
  ...(options.holder ? [`${LABEL_HOLDER}=${options.holder}`] : []),
405
406
  ...(options.worktree ? [`${LABEL_WORKTREE}=${options.worktree}`] : []),
406
407
  ...(options.worktreePath ? [`${LABEL_WORKTREE_PATH}=${options.worktreePath}`] : []),
@@ -488,60 +489,100 @@ export function buildDevWorkerDockerRunArgs(options) {
488
489
  "ln -sf /workspace/dist/bin/pibo.js /usr/local/bin/pibo && exec tail -f /dev/null",
489
490
  ];
490
491
  }
492
+ export function validateDevWorktreeName(name) {
493
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name)) {
494
+ throw new Error("Dev worktree name must use only letters, numbers, dots, underscores, and hyphens, and must start with a letter or number");
495
+ }
496
+ }
497
+ async function localBranchExists(repoDir, name) {
498
+ try {
499
+ await execFileAsync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${name}`], { cwd: repoDir });
500
+ return true;
501
+ }
502
+ catch (error) {
503
+ if (error instanceof Error && "code" in error && error.code === 1)
504
+ return false;
505
+ throw error;
506
+ }
507
+ }
491
508
  export async function spawnDevWorker(options) {
509
+ validateDevWorktreeName(options.worktreeName);
510
+ const branchExisted = await localBranchExists(options.repoDir, options.worktreeName);
492
511
  const worktreePath = path.join(options.repoDir, ".worktrees", options.worktreeName);
493
512
  await createWorktree(options.repoDir, options.worktreeName);
494
- const id = `pibo-dev-${options.worktreeName}`;
495
- const createdAt = new Date().toISOString();
496
- // Mount host node_modules into the container so dependencies are immediately available
497
- const hostNodeModules = path.join(options.repoDir, "node_modules");
498
- let nodeModulesMount;
499
513
  try {
500
- const { stdout } = await execFileAsync("ls", ["-d", hostNodeModules]);
501
- if (stdout.trim())
502
- nodeModulesMount = hostNodeModules;
514
+ const id = `pibo-dev-${options.worktreeName}`;
515
+ const createdAt = new Date().toISOString();
516
+ // Mount host node_modules into the container so dependencies are immediately available
517
+ const hostNodeModules = path.join(options.repoDir, "node_modules");
518
+ let nodeModulesMount;
519
+ try {
520
+ const { stdout } = await execFileAsync("ls", ["-d", hostNodeModules]);
521
+ if (stdout.trim())
522
+ nodeModulesMount = hostNodeModules;
523
+ }
524
+ catch {
525
+ // host node_modules does not exist; skip mount
526
+ }
527
+ const ports = await withDevPortAllocationLock(async () => {
528
+ const block = await findNextPortBlock();
529
+ const base = DEV_PORT_BASE + block * DEV_PORT_BLOCK_SIZE;
530
+ const gatewayPort = base;
531
+ const cdpPort = base + 1;
532
+ const webPort = base + 2;
533
+ const webUIPortChat = base + 3;
534
+ const webUIPortContext = base + 4;
535
+ const args = buildDevWorkerDockerRunArgs({
536
+ id,
537
+ imageName: options.imageName,
538
+ worktreePath,
539
+ worktreeName: options.worktreeName,
540
+ block,
541
+ gatewayPort,
542
+ cdpPort,
543
+ webPort,
544
+ webUIPortChat,
545
+ webUIPortContext,
546
+ createdAt,
547
+ holder: options.holder,
548
+ ttlSeconds: options.ttlSeconds,
549
+ idleSeconds: options.idleSeconds,
550
+ ralphJobId: options.ralphJobId,
551
+ ralphRunId: options.ralphRunId,
552
+ hostNodeModules: nodeModulesMount,
553
+ });
554
+ await execFileAsync("docker", args, { cwd: options.repoDir });
555
+ return { gatewayPort, cdpPort, webPort, webUIPortChat, webUIPortContext };
556
+ });
557
+ return {
558
+ id,
559
+ image: options.imageName ?? IMAGE_NAME,
560
+ gatewayHost: COMPUTE_PUBLISH_HOST,
561
+ ...ports,
562
+ connect: `docker exec -it ${id} bash`,
563
+ worktree: worktreePath,
564
+ };
503
565
  }
504
- catch {
505
- // host node_modules does not exist; skip mount
566
+ catch (error) {
567
+ const cleanupErrors = [];
568
+ try {
569
+ await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], { cwd: options.repoDir });
570
+ }
571
+ catch (cleanupError) {
572
+ cleanupErrors.push(cleanupError);
573
+ }
574
+ if (!branchExisted) {
575
+ try {
576
+ await execFileAsync("git", ["branch", "-D", options.worktreeName], { cwd: options.repoDir });
577
+ }
578
+ catch (cleanupError) {
579
+ cleanupErrors.push(cleanupError);
580
+ }
581
+ }
582
+ if (cleanupErrors.length > 0)
583
+ throw new AggregateError([error, ...cleanupErrors], `Dev worker spawn failed and cleanup was incomplete for ${options.worktreeName}`);
584
+ throw error;
506
585
  }
507
- const ports = await withDevPortAllocationLock(async () => {
508
- const block = await findNextPortBlock();
509
- const base = DEV_PORT_BASE + block * DEV_PORT_BLOCK_SIZE;
510
- const gatewayPort = base;
511
- const cdpPort = base + 1;
512
- const webPort = base + 2;
513
- const webUIPortChat = base + 3;
514
- const webUIPortContext = base + 4;
515
- const args = buildDevWorkerDockerRunArgs({
516
- id,
517
- imageName: options.imageName,
518
- worktreePath,
519
- worktreeName: options.worktreeName,
520
- block,
521
- gatewayPort,
522
- cdpPort,
523
- webPort,
524
- webUIPortChat,
525
- webUIPortContext,
526
- createdAt,
527
- holder: options.holder,
528
- ttlSeconds: options.ttlSeconds,
529
- idleSeconds: options.idleSeconds,
530
- ralphJobId: options.ralphJobId,
531
- ralphRunId: options.ralphRunId,
532
- hostNodeModules: nodeModulesMount,
533
- });
534
- await execFileAsync("docker", args, { cwd: options.repoDir });
535
- return { gatewayPort, cdpPort, webPort, webUIPortChat, webUIPortContext };
536
- });
537
- return {
538
- id,
539
- image: options.imageName ?? IMAGE_NAME,
540
- gatewayHost: COMPUTE_PUBLISH_HOST,
541
- ...ports,
542
- connect: `docker exec -it ${id} bash`,
543
- worktree: worktreePath,
544
- };
545
586
  }
546
587
  export function buildWorkerDockerRunArgs(options) {
547
588
  const policy = options.policy ?? resolveComputeResourcePolicy();
@@ -727,6 +768,8 @@ function workerFromLabels(base) {
727
768
  hostPid: base.hostPid,
728
769
  createdAt: base.labels[LABEL_CREATED_AT] ?? "unknown",
729
770
  lastUsedAt: base.labels[LABEL_LAST_USED_AT],
771
+ ttlSeconds: numberLabel(base.labels[LABEL_TTL_SECONDS]),
772
+ idleSeconds: numberLabel(base.labels[LABEL_IDLE_SECONDS]),
730
773
  holder: base.labels[LABEL_HOLDER],
731
774
  worktree: base.labels[LABEL_WORKTREE],
732
775
  worktreePath: base.labels[LABEL_WORKTREE_PATH],
@@ -865,6 +908,7 @@ export function buildComputeWorkerReapPlan(workers, options = {}) {
865
908
  const skipReasons = [];
866
909
  const created = new Date(worker.createdAt).getTime();
867
910
  const ageMinutes = Number.isNaN(created) ? undefined : (now.getTime() - created) / 1000 / 60;
911
+ const lastUsed = new Date(worker.lastUsedAt ?? worker.createdAt).getTime();
868
912
  if (worker.role === "dev" && !resolved.includeDev)
869
913
  skipReasons.push("dev-worker-preserved");
870
914
  if (worker.role !== "worker" && worker.role !== "dev")
@@ -875,6 +919,10 @@ export function buildComputeWorkerReapPlan(workers, options = {}) {
875
919
  reasons.push(worker.oomKilled ? "oom-killed" : "dirty");
876
920
  if (ageMinutes !== undefined && ageMinutes > resolved.maxAgeMinutes)
877
921
  reasons.push("old");
922
+ if (ageMinutes !== undefined && worker.ttlSeconds !== undefined && worker.ttlSeconds > 0 && ageMinutes * 60 >= worker.ttlSeconds)
923
+ reasons.push("ttl-expired");
924
+ if (!Number.isNaN(lastUsed) && worker.idleSeconds !== undefined && worker.idleSeconds > 0 && now.getTime() - lastUsed >= worker.idleSeconds * 1000)
925
+ reasons.push("idle-expired");
878
926
  if (reasons.length === 0)
879
927
  skipReasons.push("not-selected");
880
928
  const action = skipReasons.length === 0 ? "remove" : "skip";
@@ -10,6 +10,14 @@ function getBasePromptStatePath(cwd) {
10
10
  function getCustomBasePromptPath(cwd) {
11
11
  return resolve(cwd, ".pibo/base-prompt.md");
12
12
  }
13
+ function getLegacyBasePromptPath(cwd) {
14
+ return resolve(cwd, ".pibo/SYSTEM.md");
15
+ }
16
+ function effectiveMode(state, customExists, legacyExists) {
17
+ if (legacyExists)
18
+ return "legacy";
19
+ return state.mode === "custom" && customExists ? "custom" : "library";
20
+ }
13
21
  function normalizeMode(value) {
14
22
  return value === "custom" ? "custom" : "library";
15
23
  }
@@ -34,8 +42,7 @@ function writeBasePromptState(cwd, state) {
34
42
  writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
35
43
  }
36
44
  export function getActivePiboBasePromptPath(cwd = process.cwd()) {
37
- const legacyOverridePath = resolve(cwd, ".pibo/SYSTEM.md");
38
- if (existsSync(legacyOverridePath))
45
+ if (existsSync(getLegacyBasePromptPath(cwd)))
39
46
  return undefined;
40
47
  const customPath = getCustomBasePromptPath(cwd);
41
48
  const state = readBasePromptState(cwd);
@@ -46,14 +53,17 @@ export function getActivePiboBasePromptPath(cwd = process.cwd()) {
46
53
  export async function readPiboBasePrompt(cwd = process.cwd()) {
47
54
  const state = readBasePromptState(cwd);
48
55
  const customPath = getCustomBasePromptPath(cwd);
56
+ const legacyPath = getLegacyBasePromptPath(cwd);
49
57
  const customExists = existsSync(customPath);
50
- const [libraryMarkdown, customMarkdown] = await Promise.all([
58
+ const legacyExists = existsSync(legacyPath);
59
+ const [libraryMarkdown, customMarkdown, legacyMarkdown] = await Promise.all([
51
60
  readFile(PIBO_LIBRARY_BASE_PROMPT_PATH, "utf-8"),
52
61
  customExists ? readFile(customPath, "utf-8") : Promise.resolve(""),
62
+ legacyExists ? readFile(legacyPath, "utf-8") : Promise.resolve(""),
53
63
  ]);
54
64
  return {
55
65
  mode: state.mode,
56
- effectiveMode: state.mode === "custom" && customExists ? "custom" : "library",
66
+ effectiveMode: effectiveMode(state, customExists, legacyExists),
57
67
  library: {
58
68
  path: PIBO_LIBRARY_BASE_PROMPT_PATH,
59
69
  markdown: libraryMarkdown,
@@ -64,6 +74,11 @@ export async function readPiboBasePrompt(cwd = process.cwd()) {
64
74
  exists: customExists,
65
75
  updatedAt: customExists ? state.updatedAt : undefined,
66
76
  },
77
+ legacy: {
78
+ path: legacyPath,
79
+ markdown: legacyMarkdown,
80
+ exists: legacyExists,
81
+ },
67
82
  };
68
83
  }
69
84
  export async function savePiboCustomBasePrompt(markdown, cwd = process.cwd()) {
@@ -87,9 +102,11 @@ export function setPiboBasePromptMode(mode, cwd = process.cwd()) {
87
102
  });
88
103
  const state = readBasePromptState(cwd);
89
104
  const customExists = existsSync(customPath);
105
+ const legacyPath = getLegacyBasePromptPath(cwd);
106
+ const legacyExists = existsSync(legacyPath);
90
107
  return {
91
108
  mode: state.mode,
92
- effectiveMode: state.mode === "custom" && customExists ? "custom" : "library",
109
+ effectiveMode: effectiveMode(state, customExists, legacyExists),
93
110
  library: {
94
111
  path: PIBO_LIBRARY_BASE_PROMPT_PATH,
95
112
  markdown: readFileSync(PIBO_LIBRARY_BASE_PROMPT_PATH, "utf-8"),
@@ -100,5 +117,10 @@ export function setPiboBasePromptMode(mode, cwd = process.cwd()) {
100
117
  exists: customExists,
101
118
  updatedAt: customExists ? state.updatedAt : undefined,
102
119
  },
120
+ legacy: {
121
+ path: legacyPath,
122
+ markdown: legacyExists ? readFileSync(legacyPath, "utf-8") : "",
123
+ exists: legacyExists,
124
+ },
103
125
  };
104
126
  }
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { createDefaultPiboProfile } from "./default-profile.js";
4
4
  import { loadPiboModelDefaults, selectRequestedModelProfile, selectRequestedThinkingLevel } from "./model-defaults.js";
5
+ import { redactSensitiveText } from "./sensitive-data-redaction.js";
5
6
  import { getMcpAgentContextFile } from "../mcp/agent-context.js";
6
7
  import { PIBO_GOAL_TOOL_NAMES } from "../loops/tools.js";
7
8
  import { getInstalledCliToolContextFile } from "../tools/registry.js";
@@ -38,11 +39,6 @@ export function createPiboRuntimeResolutionManifest(input) {
38
39
  };
39
40
  }
40
41
  const SECRET_KEY_RE = /(api[_-]?key|authorization|bearer|cookie|credential|oauth|password|secret|token)/i;
41
- const SECRET_TEXT_PATTERNS = [
42
- /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi,
43
- /\b(sk|pk|rk)-[A-Za-z0-9_-]{16,}\b/g,
44
- /\b(api[_-]?key|authorization|cookie|password|secret|token)\b\s*[:=]\s*[^\s\n,;]+/gi,
45
- ];
46
42
  function byteLength(text) {
47
43
  return Buffer.byteLength(text, "utf-8");
48
44
  }
@@ -79,15 +75,7 @@ function applyTokenEstimates(node) {
79
75
  return node;
80
76
  }
81
77
  function redactText(text) {
82
- let value = text;
83
- for (const pattern of SECRET_TEXT_PATTERNS) {
84
- value = value.replace(pattern, (match) => {
85
- const separator = match.match(/[:=]/)?.[0];
86
- if (!separator)
87
- return "[REDACTED]";
88
- return `${match.slice(0, match.indexOf(separator) + 1)} [REDACTED]`;
89
- });
90
- }
78
+ const value = redactSensitiveText(text).replaceAll("[redacted]", "[REDACTED]");
91
79
  return { value, redacted: value !== text };
92
80
  }
93
81
  function redactJson(value) {
@@ -340,7 +328,9 @@ export async function inspectPiboContextBuild(options = {}) {
340
328
  const generatedAt = new Date().toISOString();
341
329
  const resourceLoader = runtime.services.resourceLoader;
342
330
  const basePrompt = await readPiboBasePrompt(cwd);
343
- const activePrompt = basePrompt.effectiveMode === "custom" ? basePrompt.custom : basePrompt.library;
331
+ const activePrompt = basePrompt.effectiveMode === "legacy"
332
+ ? basePrompt.legacy
333
+ : basePrompt.effectiveMode === "custom" ? basePrompt.custom : basePrompt.library;
344
334
  const activeToolNames = new Set(runtime.session.getActiveToolNames());
345
335
  const allTools = runtime.session.getAllTools();
346
336
  const toolInfoByName = new Map(allTools.map((tool) => [tool.name, tool]));
@@ -1,9 +1,9 @@
1
1
  const SENSITIVE_KEY_PATTERN = /(?:authorization|cookie|credential|access[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|bearer|oauth|token|secret|password|passwd)/i;
2
2
  const SENSITIVE_TEXT_PATTERNS = [
3
3
  [/\bBearer\s+\S+/gi, "Bearer [redacted]"],
4
- [/\b(?:sk|pk|rk|ghp|gho|ghu|ghs|ghr|github_pat|pibo|xoxb|xoxp|xoxa|xoxr)[-_][A-Za-z0-9_-]{8,}\b/g, "[redacted]"],
4
+ [/\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{16,}|(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{16,}|xox[baprs]-[A-Za-z0-9-]{16,})\b/g, "[redacted]"],
5
5
  [/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, "[redacted]"],
6
- [/\b(access[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|authorization|cookie|credential|oauth|token|secret|password|passwd)\b(\s*["']?\s*[:=]\s*["']?)([^\s"'&,;}]+)/gi, "$1$2[redacted]"],
6
+ [/\b([A-Z0-9_]*(?:ACCESS[_-]?TOKEN|REFRESH[_-]?TOKEN|ID[_-]?TOKEN|API[_-]?KEY|AUTHORIZATION|COOKIE|CREDENTIAL|OAUTH|TOKEN|SECRET|PASSWORD|PASSWD)[A-Z0-9_]*)\b(\s*["']?\s*[:=]\s*["']?)([^\s"'&,;}]+)/gi, "$1$2[redacted]"],
7
7
  ];
8
8
  export function redactSensitiveText(value) {
9
9
  let selected = value;