@pasko70/pibo 1.7.7 → 1.7.9

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 (39) hide show
  1. package/dist/apps/chat/chat-user-skill-routes.js +51 -19
  2. package/dist/apps/chat/model-catalog.js +2 -0
  3. package/dist/apps/chat/web-app.js +2 -2
  4. package/dist/apps/chat-ui/assets/{dist-BhCflw5L.js → dist-BLQTYmgi.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-C2wdpulS.js → dist-BLYDOjGT.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-BUfawaFa.js → dist-Bg2k47fY.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-iblHo9vw.js → dist-Bv912fTl.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-NDLYTRqO.js → dist-CCOZbHCt.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-ChMCJ0Xh.js → dist-D6ImmtZ3.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-C5S2MWeq.js → dist-DDQWCHTh.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-XR2wwVeo.js → dist-DTJJ5-iA.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-COMZszUx.js → dist-Daom6uMW.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-CqZsX_X9.js → dist-Dgs6LwMW.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-CJOnkhP1.js → dist-LRUZKp77.js} +1 -1
  15. package/dist/apps/chat-ui/assets/index-BnZ0V5cJ.js +173 -0
  16. package/dist/apps/chat-ui/assets/index-DbRZGRDd.css +1 -0
  17. package/dist/apps/chat-ui/index.html +2 -2
  18. package/dist/apps/chat-vscode-web/assets/index-CiQZTRZH.js +41 -0
  19. package/dist/apps/chat-vscode-web/index.html +1 -1
  20. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  21. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.7.9.vsix +0 -0
  22. package/dist/cli.js +12 -3
  23. package/dist/core/context-guard.js +126 -0
  24. package/dist/core/gateway-resource-guard.js +215 -0
  25. package/dist/core/routed-session.js +76 -1
  26. package/dist/core/runtime.js +40 -2
  27. package/dist/core/session-router.js +2 -0
  28. package/dist/debug/index.js +40 -0
  29. package/dist/mcp/output.js +19 -4
  30. package/dist/providers/openai-gpt56.js +153 -0
  31. package/dist/ralph/cli.js +49 -5
  32. package/dist/session-ui/terminalRows.js +59 -0
  33. package/dist/skills/cli.js +33 -23
  34. package/dist/tools/guides.js +17 -0
  35. package/dist/user-skills/manager.js +106 -8
  36. package/package.json +1 -1
  37. package/dist/apps/chat-ui/assets/index-BStUapSa.css +0 -1
  38. package/dist/apps/chat-ui/assets/index-BYqOi32B.js +0 -173
  39. package/dist/apps/chat-vscode-web/assets/index-B4Jk2P5o.js +0 -41
@@ -52,6 +52,10 @@ export async function runDebugCli(argv = process.argv) {
52
52
  await runDebugRuns(args.slice(1));
53
53
  return;
54
54
  }
55
+ if (args[0] === "resources") {
56
+ await runDebugResources(args.slice(1));
57
+ return;
58
+ }
55
59
  if (args[0] === "signals") {
56
60
  await runDebugSignals(args.slice(1));
57
61
  return;
@@ -77,6 +81,19 @@ export async function runDebugCli(argv = process.argv) {
77
81
  process.exitCode = 1;
78
82
  }
79
83
  }
84
+ async function runDebugResources(args) {
85
+ if (args[0] === "--help" || args[0] === "-h") {
86
+ printDebugResourcesDiscovery();
87
+ return;
88
+ }
89
+ const options = parseOptions(args);
90
+ const { collectGatewayResourceSnapshot, renderGatewayResourceSnapshotText } = await import("../core/gateway-resource-guard.js");
91
+ const snapshot = await collectGatewayResourceSnapshot();
92
+ if (options.json)
93
+ console.log(JSON.stringify(snapshot, null, 2));
94
+ else
95
+ console.log(renderGatewayResourceSnapshotText(snapshot));
96
+ }
80
97
  async function runDebugDb(args) {
81
98
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
82
99
  printDebugDbDiscovery();
@@ -1007,6 +1024,7 @@ Commands:
1007
1024
  failures List failed tool calls and trace/session errors
1008
1025
  jobs Inspect durable Pibo jobs and DLQ
1009
1026
  runs Inspect durable yielded runs
1027
+ resources Show gateway memory reserve, related child processes, and heavy daemons
1010
1028
  signals Inspect live session signal snapshots through Chat Web APIs
1011
1029
  telemetry Inspect runtime observability telemetry
1012
1030
  web Inspect browser render state via CDP
@@ -1019,12 +1037,34 @@ Next:
1019
1037
  pibo debug messages <pibo-session-id> list
1020
1038
  pibo debug trace <pibo-session-id> --running-only
1021
1039
  pibo debug events stream --topic pibo.output
1040
+ pibo debug resources --json
1022
1041
  pibo debug signals tree ps_...
1023
1042
  pibo debug telemetry sessions --active
1024
1043
  pibo debug web targets
1025
1044
  pibo debug pty run -- pibo tui:sessions --demo
1026
1045
  `);
1027
1046
  }
1047
+ function printDebugResourcesDiscovery() {
1048
+ console.log(`pibo debug resources - inspect gateway resource guard state
1049
+
1050
+ Usage:
1051
+ pibo debug resources [--json]
1052
+
1053
+ Reports:
1054
+ Gateway RSS/heap headroom, host free-memory reserve, direct child processes, and known heavy local daemons such as ComfyUI or Unity when process listing is available.
1055
+
1056
+ Environment:
1057
+ PIBO_GATEWAY_RESOURCE_GUARD=warn|block|off
1058
+ PIBO_GATEWAY_MIN_FREE_MEMORY_BYTES=<bytes>
1059
+ PIBO_GATEWAY_MIN_HEAP_AVAILABLE_BYTES=<bytes>
1060
+ PIBO_GATEWAY_MAX_RSS_BYTES=<bytes>
1061
+ PIBO_GATEWAY_KNOWN_DAEMON_WARNING_RSS_BYTES=<bytes>
1062
+
1063
+ Next:
1064
+ pibo debug resources --json
1065
+ pibo compute health --json
1066
+ `);
1067
+ }
1028
1068
  function printDebugSignalsDiscovery() {
1029
1069
  console.log(`pibo debug signals - inspect live Session Signal snapshots
1030
1070
 
@@ -50,6 +50,17 @@ export function truncateToolDescription(description, maxLength = TOOL_DESCRIPTIO
50
50
  return normalized;
51
51
  return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
52
52
  }
53
+ /**
54
+ * Return an info-view summary without generated OpenAPI error-response noise.
55
+ */
56
+ export function summarizeToolDescriptionForInfo(description) {
57
+ if (!description)
58
+ return undefined;
59
+ const withoutErrorResponses = description
60
+ .replace(/\s*Error Responses:\s*[\s\S]*$/i, '')
61
+ .trim();
62
+ return truncateToolDescription(withoutErrorResponses);
63
+ }
53
64
  /**
54
65
  * Format server list for display
55
66
  */
@@ -140,15 +151,19 @@ export function formatServerDetails(serverName, config, tools, withDescriptions
140
151
  lines.push(` ${color(tool.name, colors.green)}`);
141
152
  const toolSummary = withDescriptions
142
153
  ? tool.description
143
- : truncateToolDescription(tool.description);
154
+ : summarizeToolDescriptionForInfo(tool.description);
144
155
  if (toolSummary) {
145
156
  lines.push(` ${color(toolSummary, colors.dim)}`);
146
157
  }
147
158
  // Show parameters from schema
148
159
  const schema = tool.inputSchema;
149
- if (schema.properties) {
150
- lines.push(` ${color('Parameters:', colors.yellow)}`);
151
- for (const [name, prop] of Object.entries(schema.properties)) {
160
+ const parameters = Object.entries(schema.properties ?? {});
161
+ lines.push(` ${color('Parameters:', colors.yellow)}`);
162
+ if (parameters.length === 0) {
163
+ lines.push(` ${color('No parameters', colors.dim)}`);
164
+ }
165
+ else {
166
+ for (const [name, prop] of parameters) {
152
167
  const required = schema.required?.includes(name)
153
168
  ? 'required'
154
169
  : 'optional';
@@ -0,0 +1,153 @@
1
+ import { getModels } from "@mariozechner/pi-ai";
2
+ import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
3
+ export const OPENAI_PROVIDER_ID = "openai";
4
+ export const OPENAI_RESPONSES_API = "openai-responses";
5
+ export const OPENAI_BASE_URL = "https://api.openai.com/v1";
6
+ export const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
7
+ export const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
8
+ export const OPENAI_CODEX_RESPONSES_API = "openai-codex-responses";
9
+ export const OPENAI_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
10
+ const OPENAI_GPT_56_CONTEXT_WINDOW = 1_050_000;
11
+ const OPENAI_CODEX_GPT_56_CONTEXT_WINDOW = 272_000;
12
+ const GPT_56_MAX_TOKENS = 128_000;
13
+ export const OPENAI_GPT_56_MODELS = [
14
+ {
15
+ id: "gpt-5.6",
16
+ name: "GPT-5.6 (Sol alias)",
17
+ cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
18
+ },
19
+ {
20
+ id: "gpt-5.6-sol",
21
+ name: "GPT-5.6 Sol",
22
+ cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
23
+ },
24
+ {
25
+ id: "gpt-5.6-terra",
26
+ name: "GPT-5.6 Terra",
27
+ cost: { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 3.125 },
28
+ },
29
+ {
30
+ id: "gpt-5.6-luna",
31
+ name: "GPT-5.6 Luna",
32
+ cost: { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 1.25 },
33
+ },
34
+ ];
35
+ const OPENAI_GPT_56_MODEL_IDS = new Set(OPENAI_GPT_56_MODELS.map((model) => model.id));
36
+ export function getBuiltInOpenAiModels() {
37
+ return getBuiltInProviderModels(OPENAI_PROVIDER_ID);
38
+ }
39
+ export function getBuiltInOpenAiCodexModels() {
40
+ return getBuiltInProviderModels(OPENAI_CODEX_PROVIDER_ID);
41
+ }
42
+ export function buildOpenAiGpt56Models(baseModels = getBuiltInOpenAiModels()) {
43
+ return buildProviderGpt56Models({
44
+ providerId: OPENAI_PROVIDER_ID,
45
+ api: OPENAI_RESPONSES_API,
46
+ baseUrl: OPENAI_BASE_URL,
47
+ contextWindow: OPENAI_GPT_56_CONTEXT_WINDOW,
48
+ thinkingLevelMap: { off: null, xhigh: "xhigh" },
49
+ baseModels,
50
+ modelCost: (model) => model.cost,
51
+ });
52
+ }
53
+ export function buildOpenAiCodexGpt56Models(baseModels = getBuiltInOpenAiCodexModels()) {
54
+ return buildProviderGpt56Models({
55
+ providerId: OPENAI_CODEX_PROVIDER_ID,
56
+ api: OPENAI_CODEX_RESPONSES_API,
57
+ baseUrl: OPENAI_CODEX_BASE_URL,
58
+ contextWindow: OPENAI_CODEX_GPT_56_CONTEXT_WINDOW,
59
+ thinkingLevelMap: { xhigh: "xhigh", minimal: "low" },
60
+ baseModels,
61
+ modelCost: (model) => ({ ...model.cost, cacheWrite: 0 }),
62
+ });
63
+ }
64
+ export function registerOpenAiGpt56Models(modelRegistry, options = {}) {
65
+ const baseOpenAiModels = options.baseOpenAiModels ?? getBuiltInOpenAiModels();
66
+ const openAiModels = buildOpenAiGpt56Models(baseOpenAiModels);
67
+ const openAiAdded = countMissingGpt56Models(baseOpenAiModels, OPENAI_PROVIDER_ID);
68
+ modelRegistry.registerProvider(OPENAI_PROVIDER_ID, {
69
+ baseUrl: OPENAI_BASE_URL,
70
+ api: OPENAI_RESPONSES_API,
71
+ apiKey: OPENAI_API_KEY_ENV,
72
+ models: openAiModels,
73
+ });
74
+ const baseOpenAiCodexModels = options.baseOpenAiCodexModels ?? getBuiltInOpenAiCodexModels();
75
+ const openAiCodexModels = buildOpenAiCodexGpt56Models(baseOpenAiCodexModels);
76
+ const openAiCodexAdded = countMissingGpt56Models(baseOpenAiCodexModels, OPENAI_CODEX_PROVIDER_ID);
77
+ const openAiCodexOAuth = getOpenAiCodexOAuthConfig();
78
+ modelRegistry.registerProvider(OPENAI_CODEX_PROVIDER_ID, {
79
+ name: openAiCodexOAuth.name,
80
+ baseUrl: OPENAI_CODEX_BASE_URL,
81
+ api: OPENAI_CODEX_RESPONSES_API,
82
+ oauth: openAiCodexOAuth,
83
+ models: openAiCodexModels,
84
+ });
85
+ return {
86
+ registered: true,
87
+ providers: 2,
88
+ models: openAiModels.length + openAiCodexModels.length,
89
+ added: openAiAdded + openAiCodexAdded,
90
+ };
91
+ }
92
+ export function findOpenAiGpt56Model(modelRegistry, model) {
93
+ if (!model?.provider || !model.id)
94
+ return undefined;
95
+ if (model.provider !== OPENAI_PROVIDER_ID && model.provider !== OPENAI_CODEX_PROVIDER_ID)
96
+ return undefined;
97
+ if (!OPENAI_GPT_56_MODEL_IDS.has(model.id))
98
+ return undefined;
99
+ return modelRegistry.find(model.provider, model.id);
100
+ }
101
+ function getBuiltInProviderModels(providerId) {
102
+ try {
103
+ return getModels(providerId);
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ }
109
+ function buildProviderGpt56Models(options) {
110
+ const providerBaseModels = options.baseModels
111
+ .filter((model) => model.provider === options.providerId)
112
+ .map(cloneModel);
113
+ const existingIds = new Set(providerBaseModels.map((model) => model.id));
114
+ const additions = OPENAI_GPT_56_MODELS
115
+ .filter((model) => !existingIds.has(model.id))
116
+ .map((model) => openAiGpt56ModelToRegistryModel(model, options));
117
+ return [...providerBaseModels, ...additions];
118
+ }
119
+ function countMissingGpt56Models(baseModels, providerId) {
120
+ const existingIds = new Set(baseModels.filter((model) => model.provider === providerId).map((model) => model.id));
121
+ return OPENAI_GPT_56_MODELS.filter((model) => !existingIds.has(model.id)).length;
122
+ }
123
+ function cloneModel(model) {
124
+ return {
125
+ ...model,
126
+ input: [...model.input],
127
+ cost: { ...model.cost },
128
+ headers: model.headers ? { ...model.headers } : undefined,
129
+ compat: model.compat ? { ...model.compat } : undefined,
130
+ };
131
+ }
132
+ function openAiGpt56ModelToRegistryModel(model, options) {
133
+ return {
134
+ id: model.id,
135
+ name: model.name,
136
+ api: options.api,
137
+ provider: options.providerId,
138
+ baseUrl: options.baseUrl,
139
+ reasoning: true,
140
+ thinkingLevelMap: options.thinkingLevelMap,
141
+ input: ["text", "image"],
142
+ cost: options.modelCost(model),
143
+ contextWindow: options.contextWindow,
144
+ maxTokens: GPT_56_MAX_TOKENS,
145
+ };
146
+ }
147
+ function getOpenAiCodexOAuthConfig() {
148
+ const provider = getOAuthProvider(OPENAI_CODEX_PROVIDER_ID);
149
+ if (!provider)
150
+ throw new Error("OpenAI Codex OAuth provider is unavailable.");
151
+ const { id: _id, ...oauth } = provider;
152
+ return oauth;
153
+ }
package/dist/ralph/cli.js CHANGED
@@ -4,6 +4,7 @@ import { createDefaultPiboRalphStore } from './store.js';
4
4
  import { createBuiltInRalphStopConditions } from './stopping.js';
5
5
  import { DEFAULT_PIBO_PROFILE_NAME } from '../plugins/builtin.js';
6
6
  import { getRalphJobTemplate, listRalphJobTemplates } from './templates.js';
7
+ import { parsePiboThinkingLevel } from '../core/thinking.js';
7
8
  function printDiscovery() {
8
9
  console.log(`pibo ralph
9
10
 
@@ -34,6 +35,49 @@ function printJson(value) { console.log(JSON.stringify(value, null, 2)); }
34
35
  function maxIterations(value) { if (value === undefined)
35
36
  return undefined; const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 1)
36
37
  throw new Error('--max-iterations must be a positive integer'); return parsed; }
38
+ function parseModelOverride(value) {
39
+ if (value === undefined)
40
+ return undefined;
41
+ const slash = value.indexOf('/');
42
+ if (slash <= 0 || slash === value.length - 1)
43
+ throw new Error('--model must use provider/model syntax, for example openai/gpt-5');
44
+ const provider = value.slice(0, slash).trim();
45
+ const id = value.slice(slash + 1).trim();
46
+ if (!provider || !id)
47
+ throw new Error('--model must use provider/model syntax, for example openai/gpt-5');
48
+ return { provider, id };
49
+ }
50
+ function parseThinkingOverride(value) { return value === undefined ? undefined : parsePiboThinkingLevel(value); }
51
+ function applyRuntimeCreateOptions(input, options) {
52
+ const modelOverride = parseModelOverride(options.model);
53
+ const thinkingLevel = parseThinkingOverride(options.thinking);
54
+ if (modelOverride)
55
+ input.modelOverride = modelOverride;
56
+ if (thinkingLevel)
57
+ input.thinkingLevel = thinkingLevel;
58
+ if (options.fast !== undefined)
59
+ input.fastMode = options.fast;
60
+ }
61
+ function applyRuntimePatchOptions(patch, options) {
62
+ if (options.model !== undefined && options.clearModel)
63
+ throw new Error('Choose either --model or --clear-model, not both');
64
+ if (options.thinking !== undefined && options.clearThinking)
65
+ throw new Error('Choose either --thinking or --clear-thinking, not both');
66
+ if (options.fast !== undefined && options.clearFast)
67
+ throw new Error('Choose either --fast/--no-fast or --clear-fast, not both');
68
+ if (options.clearModel)
69
+ patch.modelOverride = null;
70
+ else if (options.model !== undefined)
71
+ patch.modelOverride = parseModelOverride(options.model);
72
+ if (options.clearThinking)
73
+ patch.thinkingLevel = null;
74
+ else if (options.thinking !== undefined)
75
+ patch.thinkingLevel = parseThinkingOverride(options.thinking);
76
+ if (options.clearFast)
77
+ patch.fastMode = null;
78
+ else if (options.fast !== undefined)
79
+ patch.fastMode = options.fast;
80
+ }
37
81
  function templatePatch(id) {
38
82
  if (!id)
39
83
  return {};
@@ -77,18 +121,18 @@ export async function runRalphCli(argv = process.argv) {
77
121
  else
78
122
  for (const job of jobs)
79
123
  console.log(formatRalphJobLine(job)); store.close(); });
80
- program.command('add').description('Create a Ralph job').option('--template <id>', 'Built-in job template id').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile', DEFAULT_PIBO_PROFILE_NAME).option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts')
124
+ program.command('add').description('Create a Ralph job').option('--template <id>', 'Built-in job template id').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile', DEFAULT_PIBO_PROFILE_NAME).option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--model <provider/model>', 'Runtime model override, for example openai/gpt-5').option('--thinking <level>', 'Runtime thinking level override: off, minimal, low, medium, high, xhigh').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode')
81
125
  .option('--start', 'Start immediately').option('--json', 'Print JSON').action((options) => { const base = templatePatch(options.template); const prompt = options.prompt ?? base.prompt; if (typeof prompt !== 'string' || !prompt.trim())
82
- throw new Error('Choose --template <id> or provide --prompt <text>'); const store = createDefaultPiboRalphStore({ path: program.opts().store }); const job = store.createJob({ name: options.name ?? base.name, description: options.description ?? base.description, enabled: options.start === true, target: targetFromOptions(options), profile: options.profile, prompt, maxIterations: options.maxIterations !== undefined ? maxIterations(options.maxIterations) : typeof base.maxIterations === 'number' ? base.maxIterations : undefined, stopPolicy: base.stopPolicy ?? undefined }); if (options.json)
126
+ throw new Error('Choose --template <id> or provide --prompt <text>'); const input = { name: options.name ?? base.name, description: options.description ?? base.description, enabled: options.start === true, target: targetFromOptions(options), profile: options.profile, prompt, maxIterations: options.maxIterations !== undefined ? maxIterations(options.maxIterations) : typeof base.maxIterations === 'number' ? base.maxIterations : undefined, stopPolicy: base.stopPolicy ?? undefined }; applyRuntimeCreateOptions(input, options); const store = createDefaultPiboRalphStore({ path: program.opts().store }); const job = store.createJob(input); if (options.json)
83
127
  printJson(job);
84
128
  else
85
129
  console.log(`${job.id}\t${job.enabled ? 'running' : 'stopped'}\t${job.name}`); store.close(); });
86
- program.command('edit').argument('<id>', 'Ralph job id').description('Update a Ralph job').option('--template <id>', 'Apply a built-in job template before explicit overrides').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile').option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboRalphStore({ path: program.opts().store }); const patch = { ...templatePatch(options.template) }; if (options.name !== undefined)
130
+ program.command('edit').argument('<id>', 'Ralph job id').description('Update a Ralph job').option('--template <id>', 'Apply a built-in job template before explicit overrides').option('--prompt <text>', 'Task prompt').option('--name <name>', 'Job name').option('--description <text>', 'Job description').option('--profile <profile>', 'Agent profile').option('--room <room-id>', 'Target room id').option('--default-chat', 'Target the shared default chat').option('--max-iterations <n>', 'Stop after n completed run attempts').option('--model <provider/model>', 'Set runtime model override, for example openai/gpt-5').option('--clear-model', 'Clear runtime model override').option('--thinking <level>', 'Set runtime thinking level override: off, minimal, low, medium, high, xhigh').option('--clear-thinking', 'Clear runtime thinking level override').option('--fast', 'Enable runtime fast mode').option('--no-fast', 'Disable runtime fast mode').option('--clear-fast', 'Clear runtime fast mode override').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboRalphStore({ path: program.opts().store }); const patch = { ...templatePatch(options.template) }; if (options.name !== undefined)
87
131
  patch.name = options.name; if (options.description !== undefined)
88
132
  patch.description = options.description; if (options.profile !== undefined)
89
133
  patch.profile = options.profile; if (options.prompt !== undefined)
90
134
  patch.prompt = options.prompt; if (options.maxIterations !== undefined)
91
- patch.maxIterations = maxIterations(options.maxIterations); const target = maybeTargetFromOptions(options); if (target)
135
+ patch.maxIterations = maxIterations(options.maxIterations); applyRuntimePatchOptions(patch, options); const target = maybeTargetFromOptions(options); if (target)
92
136
  patch.target = target; if (Object.keys(patch).length === 0)
93
137
  throw new Error('No Ralph job update fields provided'); const job = store.updateJob(id, patch); if (!job)
94
138
  throw new Error('Ralph job not found'); if (options.json)
@@ -145,7 +189,7 @@ export async function runRalphCli(argv = process.argv) {
145
189
  else
146
190
  for (const run of runs)
147
191
  console.log(formatRalphRunLine(run)); store.close(); });
148
- if (argv.length <= 2 || argv.includes('--help') || argv.includes('-h')) {
192
+ if (argv.length <= 2 || (argv.length === 3 && (argv[2] === '--help' || argv[2] === '-h'))) {
149
193
  printDiscovery();
150
194
  return;
151
195
  }
@@ -129,6 +129,9 @@ function createToolRowCandidate(node, turnId) {
129
129
  const row = createCommandToolRow(node, command);
130
130
  return { row, turnId, exploring: undefined };
131
131
  }
132
+ if (isWebSearchToolName(node.title)) {
133
+ return { row: createWebSearchToolRow(node), turnId };
134
+ }
132
135
  const image = classifyImageTool(node);
133
136
  if (image) {
134
137
  const row = createImageToolRow(node, image);
@@ -161,6 +164,38 @@ function createToolRowCandidate(node, turnId) {
161
164
  const exploring = classifyExploringTool(node);
162
165
  return { row, turnId, exploring };
163
166
  }
167
+ function createWebSearchToolRow(node) {
168
+ const status = mapStatus(node.status);
169
+ const query = webSearchQuery(node);
170
+ const sourceCount = webSearchSourceCount(node.output);
171
+ const lines = [
172
+ {
173
+ prefix: "bullet",
174
+ tokens: [token(webSearchVerb(node.status), toneForStatus(node.status), node.status === "error" ? "bold" : "semibold")],
175
+ },
176
+ ];
177
+ if (query) {
178
+ lines.push({ prefix: "detail", tokens: [token(`query: ${JSON.stringify(query)}`, "cyan")] });
179
+ }
180
+ if (node.status === "done" && sourceCount !== undefined) {
181
+ lines.push({ prefix: "detail", tokens: [token(`sources: ${sourceCount}`, "dim")] });
182
+ }
183
+ if (node.status === "error" && node.error) {
184
+ lines.push({ prefix: "detail", tokens: [token(node.error, "red")] });
185
+ }
186
+ return {
187
+ id: node.id,
188
+ kind: "tool.call",
189
+ status,
190
+ errorKind: node.status === "error" ? "tool" : undefined,
191
+ lines,
192
+ sourceNodeIds: [node.id],
193
+ input: node.input,
194
+ output: node.output,
195
+ error: node.error,
196
+ expandable: node.input !== undefined || node.output !== undefined || Boolean(node.error),
197
+ };
198
+ }
164
199
  function createImageToolRow(node, image) {
165
200
  const status = mapStatus(node.status);
166
201
  const detailLabel = image.path ? `Path: ${image.path}` : image.artifactId ? `Artifact: ${image.artifactId}` : image.query ? `Query: ${image.query}` : image.mimeType ? `Type: ${image.mimeType}` : "Image content returned";
@@ -962,6 +997,30 @@ function isShellToolName(name) {
962
997
  normalized === "bash" ||
963
998
  normalized === "terminal");
964
999
  }
1000
+ function isWebSearchToolName(name) {
1001
+ return (name ?? "").trim().toLowerCase() === "web_search";
1002
+ }
1003
+ function webSearchVerb(status) {
1004
+ if (status === "running")
1005
+ return "Searching web";
1006
+ if (status === "error")
1007
+ return "Web search failed";
1008
+ return "Searched web";
1009
+ }
1010
+ function webSearchQuery(node) {
1011
+ const input = isRecord(node.input) ? node.input : undefined;
1012
+ const output = isRecord(node.output) ? node.output : undefined;
1013
+ return stringValue(input?.query) ?? stringValue(output?.query) ?? stringValue(node.summary);
1014
+ }
1015
+ function webSearchSourceCount(output) {
1016
+ if (!isRecord(output))
1017
+ return undefined;
1018
+ const explicit = output.sourceCount ?? output.sourcesCount;
1019
+ if (typeof explicit === "number" && Number.isFinite(explicit))
1020
+ return explicit;
1021
+ const sources = output.sources ?? output.citations ?? output.results;
1022
+ return Array.isArray(sources) ? sources.length : undefined;
1023
+ }
965
1024
  function shellCommandValue(value) {
966
1025
  if (!isRecord(value))
967
1026
  return undefined;
@@ -1,14 +1,13 @@
1
1
  import { Command } from "commander";
2
2
  import { createDefaultPiboPluginRegistry } from "../plugins/builtin.js";
3
- import { UserSkillManager } from "../user-skills/manager.js";
3
+ import { ScopedUserSkillManager, normalizeUserSkillScope, normalizeWritableUserSkillScope, } from "../user-skills/manager.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
- import os from "node:os";
7
6
  function printJson(value) {
8
7
  console.log(JSON.stringify(value, null, 2));
9
8
  }
10
9
  export async function runSkillsCli(argv) {
11
- const manager = new UserSkillManager(os.homedir());
10
+ const manager = new ScopedUserSkillManager();
12
11
  const program = new Command();
13
12
  program
14
13
  .name("pibo skills")
@@ -37,9 +36,10 @@ export async function runSkillsCli(argv) {
37
36
  program
38
37
  .command("list")
39
38
  .description("List user skills managed by this CLI")
39
+ .option("--scope <scope>", "Skill scope: global, workspace, or all", "all")
40
40
  .option("--json", "Print JSON")
41
41
  .action((options) => {
42
- const skills = manager.list();
42
+ const skills = manager.list(normalizeUserSkillScope(options.scope));
43
43
  if (options.json) {
44
44
  printJson(skills);
45
45
  return;
@@ -48,24 +48,26 @@ export async function runSkillsCli(argv) {
48
48
  console.log("No user skills registered.");
49
49
  return;
50
50
  }
51
- console.log("NAME\t\tENABLED\tSOURCE\t\tDESCRIPTION");
51
+ console.log("NAME\t\tSCOPE\t\tENABLED\tSOURCE\t\tDESCRIPTION");
52
52
  for (const s of skills) {
53
53
  const enabled = s.enabled ? "yes" : "no";
54
- console.log(`${s.name}\t${enabled}\t\t${s.source}\t${s.description}`);
54
+ console.log(`${s.name}\t${s.scope ?? "global"}\t${enabled}\t\t${s.source}\t${s.description}`);
55
55
  }
56
56
  });
57
57
  program
58
58
  .command("show")
59
59
  .description("Show a skill's markdown content")
60
60
  .argument("<name>", "Skill name")
61
- .action((name) => {
62
- const skill = manager.get(name);
61
+ .option("--scope <scope>", "Skill scope: global, workspace, or all", "all")
62
+ .action((name, options) => {
63
+ const scope = normalizeUserSkillScope(options.scope);
64
+ const skill = manager.get(name, scope);
63
65
  if (!skill) {
64
66
  console.error(`Skill "${name}" not found.`);
65
67
  process.exitCode = 1;
66
68
  return;
67
69
  }
68
- console.log(manager.getSkillMarkdown(skill.id));
70
+ console.log(manager.getSkillMarkdown(skill.id, scope));
69
71
  });
70
72
  program
71
73
  .command("add")
@@ -73,6 +75,7 @@ export async function runSkillsCli(argv) {
73
75
  .argument("<name>", "Skill name (kebab-case)")
74
76
  .requiredOption("--file <path>", "Path to markdown file")
75
77
  .option("--description <text>", "Short description")
78
+ .option("--scope <scope>", "Skill scope: global or workspace", "global")
76
79
  .action((name, options) => {
77
80
  const filePath = resolve(options.file);
78
81
  const markdown = readFileSync(filePath, "utf-8");
@@ -80,58 +83,65 @@ export async function runSkillsCli(argv) {
80
83
  name,
81
84
  description: options.description ?? "",
82
85
  markdown,
83
- });
84
- printJson({ id: skill.id, name: skill.name, enabled: skill.enabled });
86
+ }, normalizeWritableUserSkillScope(options.scope));
87
+ printJson({ id: skill.id, name: skill.name, scope: skill.scope, enabled: skill.enabled });
85
88
  });
86
89
  program
87
90
  .command("remove")
88
91
  .description("Remove a user skill")
89
92
  .argument("<name>", "Skill name")
90
- .action((name) => {
91
- const skill = manager.get(name);
93
+ .option("--scope <scope>", "Skill scope: global, workspace, or all", "all")
94
+ .action((name, options) => {
95
+ const scope = normalizeUserSkillScope(options.scope);
96
+ const skill = manager.get(name, scope);
92
97
  if (!skill) {
93
98
  console.error(`Skill "${name}" not found.`);
94
99
  process.exitCode = 1;
95
100
  return;
96
101
  }
97
- manager.remove(skill.id);
102
+ manager.remove(skill.id, scope);
98
103
  console.log(`Removed skill "${skill.name}".`);
99
104
  });
100
105
  program
101
106
  .command("enable")
102
107
  .description("Enable a user skill")
103
108
  .argument("<name>", "Skill name")
104
- .action((name) => {
105
- const skill = manager.get(name);
109
+ .option("--scope <scope>", "Skill scope: global, workspace, or all", "all")
110
+ .action((name, options) => {
111
+ const scope = normalizeUserSkillScope(options.scope);
112
+ const skill = manager.get(name, scope);
106
113
  if (!skill) {
107
114
  console.error(`Skill "${name}" not found.`);
108
115
  process.exitCode = 1;
109
116
  return;
110
117
  }
111
- manager.setEnabled(skill.id, true);
118
+ manager.setEnabled(skill.id, true, scope);
112
119
  console.log(`Enabled skill "${skill.name}".`);
113
120
  });
114
121
  program
115
122
  .command("disable")
116
123
  .description("Disable a user skill")
117
124
  .argument("<name>", "Skill name")
118
- .action((name) => {
119
- const skill = manager.get(name);
125
+ .option("--scope <scope>", "Skill scope: global, workspace, or all", "all")
126
+ .action((name, options) => {
127
+ const scope = normalizeUserSkillScope(options.scope);
128
+ const skill = manager.get(name, scope);
120
129
  if (!skill) {
121
130
  console.error(`Skill "${name}" not found.`);
122
131
  process.exitCode = 1;
123
132
  return;
124
133
  }
125
- manager.setEnabled(skill.id, false);
134
+ manager.setEnabled(skill.id, false, scope);
126
135
  console.log(`Disabled skill "${skill.name}".`);
127
136
  });
128
137
  program
129
138
  .command("install")
130
139
  .description("Install a skill from a URL (GitHub or skills.sh)")
131
140
  .argument("<url>", "Skill URL")
132
- .action(async (url) => {
133
- const skill = await manager.installFromUrl(url);
134
- printJson({ id: skill.id, name: skill.name, source: skill.source });
141
+ .option("--scope <scope>", "Skill scope: global or workspace", "global")
142
+ .action(async (url, options) => {
143
+ const skill = await manager.installFromUrl(url, normalizeWritableUserSkillScope(options.scope));
144
+ printJson({ id: skill.id, name: skill.name, scope: skill.scope, source: skill.source });
135
145
  });
136
146
  if (argv.length <= 2) {
137
147
  program.outputHelp();
@@ -63,6 +63,23 @@ pibo ralph add \\
63
63
 
64
64
  Explicit options override template fields.
65
65
 
66
+ Runtime overrides are optional and apply to sessions created by the job:
67
+
68
+ \`\`\`bash
69
+ pibo ralph add \\
70
+ --room "<room-id>" \\
71
+ --prompt "Use a specific runtime." \\
72
+ --model openai/gpt-5 \\
73
+ --thinking high \\
74
+ --fast \\
75
+ --json
76
+
77
+ pibo ralph edit <job-id> --model openai/gpt-5 --thinking medium --no-fast --json
78
+ pibo ralph edit <job-id> --clear-model --clear-thinking --clear-fast --json
79
+ \`\`\`
80
+
81
+ Use \`--model <provider/model>\`, \`--thinking off|minimal|low|medium|high|xhigh\`, and \`--fast\`/\`--no-fast\` to set runtime options. Use the \`--clear-*\` flags on \`edit\` to return to profile/default runtime behavior.
82
+
66
83
  ## Inspect and Debug
67
84
 
68
85
  \`\`\`bash