@pasko70/pibo 1.4.4 → 1.4.6

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 (38) hide show
  1. package/dist/apps/chat/agent-profiles.js +4 -1
  2. package/dist/apps/chat/agent-store.js +196 -3
  3. package/dist/apps/chat/web-app.js +5 -4
  4. package/dist/apps/chat-ui/assets/{dist-ZB1-ui2y.js → dist-B6kzRv2_.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-C3PnEkhb.js → dist-Bns-O5iY.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-BAXNalar.js → dist-CVcI9eYD.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-vKlxFkTa.js → dist-CqkKCn6l.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-DnACFKyO.js → dist-D4CEl91a.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-CYPL-B2Z.js → dist-D9K0kHT0.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-B2BEpL7n.js → dist-DWA7BIr3.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-BwUvs6Ph.js → dist-Dj3uUYqF.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-C0zsJ8II.js → dist-DqaKPP5Y.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-CiDSXgtg.js → dist-DuM9PL5k.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-BAGS_xkV.js → dist-DvPgMX-r.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{index-0x7tuTNX.js → index-Baxj7Erc.js} +18 -17
  16. package/dist/apps/chat-ui/index.html +1 -1
  17. package/dist/apps/chat-vscode-web/assets/{index-C3GTPyDo.js → index-CjOC7zYy.js} +4 -4
  18. package/dist/apps/chat-vscode-web/index.html +1 -1
  19. package/dist/apps/cli-ui/inkMarkdown.js +8 -3
  20. package/dist/apps/cli-ui/inkSyntaxHighlighter.js +166 -0
  21. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  22. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-1.4.4.vsix → pibo-vscode-ext-1.4.6.vsix} +0 -0
  23. package/dist/cli.js +20 -0
  24. package/dist/mcp/agent-context.js +10 -6
  25. package/dist/mcp/commands/info.js +19 -7
  26. package/dist/mcp/config.js +98 -65
  27. package/dist/plugins/builtin.js +15 -1
  28. package/dist/plugins/codex-compat.js +2 -0
  29. package/dist/session-ui/terminalRows.js +111 -10
  30. package/dist/shared/trace-nodes.js +12 -0
  31. package/dist/skills/cli.js +25 -1
  32. package/dist/tools/codex-image-generation.js +272 -0
  33. package/dist/tools/guides.js +71 -0
  34. package/dist/tools/index.js +7 -3
  35. package/dist/tools/python-runtime.js +2 -2
  36. package/dist/tools/registry.js +25 -1
  37. package/package.json +1 -1
  38. package/skills/builtin/graphify/SKILL.md +52 -0
@@ -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-C3GTPyDo.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-CjOC7zYy.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-B5QK07zO.css">
10
10
  </head>
11
11
  <body>
@@ -1,4 +1,5 @@
1
1
  import { sanitizeTerminalText, tokenizeJsonTextLine } from "./inkJson.js";
2
+ import { highlightInkCodeLine, normalizeInkCodeLanguage } from "./inkSyntaxHighlighter.js";
2
3
  export function renderInkMarkdownLines(markdown, options = {}) {
3
4
  return renderInkMarkdownTerminalLines(markdown, options).map((line) => line.tokens.map((token) => token.text).join(""));
4
5
  }
@@ -105,14 +106,18 @@ function inlineMarkdownTokens(text, defaults = {}) {
105
106
  return tokens.filter((token) => token.text.length > 0);
106
107
  }
107
108
  function codeFenceTokens(sourceLine, language) {
108
- if (isBashLanguage(language))
109
+ const normalizedLanguage = normalizeInkCodeLanguage(language);
110
+ if (isBashLanguage(normalizedLanguage))
109
111
  return [{ text: " " }, ...tokenizeInkBashCommand(sourceLine)];
110
- if (language === "json" || language === "jsonc")
112
+ if (normalizedLanguage === "json")
111
113
  return [{ text: " " }, ...tokenizeJsonTextLine(sourceLine)];
114
+ const highlighted = highlightInkCodeLine(sourceLine, normalizedLanguage);
115
+ if (highlighted)
116
+ return [{ text: " " }, ...highlighted];
112
117
  return [{ text: ` ${sourceLine}`, tone: "default" }];
113
118
  }
114
119
  function isBashLanguage(language) {
115
- return ["bash", "sh", "shell", "zsh"].includes(language);
120
+ return language === "bash";
116
121
  }
117
122
  function line(tokens) {
118
123
  return { prefix: "none", tokens };
@@ -0,0 +1,166 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const languageAliases = new Map([
4
+ ["sh", "bash"],
5
+ ["shell", "bash"],
6
+ ["shellscript", "bash"],
7
+ ["zsh", "bash"],
8
+ ["js", "javascript"],
9
+ ["mjs", "javascript"],
10
+ ["cjs", "javascript"],
11
+ ["ts", "typescript"],
12
+ ["mts", "typescript"],
13
+ ["cts", "typescript"],
14
+ ["md", "markdown"],
15
+ ["yml", "yaml"],
16
+ ["jsonc", "json"],
17
+ ["py", "python"],
18
+ ["rs", "rust"],
19
+ ]);
20
+ const grammarComponents = {
21
+ bash: ["bash"],
22
+ css: ["css"],
23
+ go: ["go"],
24
+ html: ["markup"],
25
+ javascript: ["javascript"],
26
+ json: ["json"],
27
+ jsx: ["markup", "javascript", "jsx"],
28
+ markdown: ["markup", "markdown"],
29
+ python: ["python"],
30
+ rust: ["rust"],
31
+ sql: ["sql"],
32
+ tsx: ["markup", "javascript", "jsx", "typescript", "tsx"],
33
+ typescript: ["javascript", "typescript"],
34
+ yaml: ["yaml"],
35
+ };
36
+ const loadedComponents = new Set();
37
+ let prismInstance;
38
+ export function highlightInkCodeLine(sourceLine, language) {
39
+ if (shouldUsePlainCodeTokens())
40
+ return [{ text: sourceLine, tone: "default" }];
41
+ const normalizedLanguage = normalizeInkCodeLanguage(language);
42
+ const prism = loadPrism();
43
+ if (!loadGrammar(prism, normalizedLanguage))
44
+ return undefined;
45
+ const grammar = prism.languages[normalizedLanguage];
46
+ if (!grammar)
47
+ return undefined;
48
+ try {
49
+ return flattenPrismTokens(prism.tokenize(sourceLine, grammar));
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ export function normalizeInkCodeLanguage(language) {
56
+ const normalized = language.trim().toLowerCase();
57
+ return languageAliases.get(normalized) ?? normalized;
58
+ }
59
+ function loadPrism() {
60
+ prismInstance ??= require("prismjs");
61
+ return prismInstance;
62
+ }
63
+ function loadGrammar(prism, language) {
64
+ const components = grammarComponents[language];
65
+ if (!components)
66
+ return false;
67
+ for (const component of components) {
68
+ if (loadedComponents.has(component))
69
+ continue;
70
+ require(`prismjs/components/prism-${component}.js`);
71
+ loadedComponents.add(component);
72
+ }
73
+ return Boolean(prism.languages[language]);
74
+ }
75
+ function flattenPrismTokens(tokens, inheritedTone = "default") {
76
+ const result = [];
77
+ for (const token of tokens) {
78
+ if (typeof token === "string") {
79
+ if (token.length > 0)
80
+ result.push({ text: token, tone: inheritedTone });
81
+ continue;
82
+ }
83
+ const tone = toneForPrismToken(token) ?? inheritedTone;
84
+ appendPrismContent(result, token.content, tone);
85
+ }
86
+ return mergeAdjacentTokens(result);
87
+ }
88
+ function appendPrismContent(result, content, tone) {
89
+ if (typeof content === "string") {
90
+ if (content.length > 0)
91
+ result.push({ text: content, tone });
92
+ return;
93
+ }
94
+ if (Array.isArray(content)) {
95
+ result.push(...flattenPrismTokens(content, tone));
96
+ return;
97
+ }
98
+ const nestedTone = toneForPrismToken(content) ?? tone;
99
+ appendPrismContent(result, content.content, nestedTone);
100
+ }
101
+ function toneForPrismToken(token) {
102
+ const classes = [token.type, ...aliasesForToken(token)];
103
+ for (const className of classes) {
104
+ switch (className) {
105
+ case "comment":
106
+ case "prolog":
107
+ case "doctype":
108
+ case "cdata":
109
+ return "dim";
110
+ case "string":
111
+ case "char":
112
+ case "attr-value":
113
+ case "url":
114
+ return "green";
115
+ case "number":
116
+ case "boolean":
117
+ case "constant":
118
+ return "blue";
119
+ case "keyword":
120
+ case "operator":
121
+ case "punctuation":
122
+ case "important":
123
+ case "atrule":
124
+ return "magenta";
125
+ case "function":
126
+ case "method":
127
+ case "selector":
128
+ case "class-name":
129
+ return "yellow";
130
+ case "tag":
131
+ case "property":
132
+ case "attr-name":
133
+ case "variable":
134
+ case "regex":
135
+ return "cyan";
136
+ case "builtin":
137
+ case "symbol":
138
+ case "deleted":
139
+ return "red";
140
+ default:
141
+ break;
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+ function aliasesForToken(token) {
147
+ if (!token.alias)
148
+ return [];
149
+ return Array.isArray(token.alias) ? token.alias : [token.alias];
150
+ }
151
+ function mergeAdjacentTokens(tokens) {
152
+ const merged = [];
153
+ for (const token of tokens) {
154
+ const previous = merged[merged.length - 1];
155
+ if (previous && previous.tone === token.tone && previous.weight === token.weight && previous.italic === token.italic) {
156
+ previous.text += token.text;
157
+ }
158
+ else {
159
+ merged.push({ ...token });
160
+ }
161
+ }
162
+ return merged;
163
+ }
164
+ function shouldUsePlainCodeTokens() {
165
+ return Boolean(process.env.NO_COLOR) || process.env.TERM === "dumb" || process.env.PIBO_ASCII_PROGRESS === "1";
166
+ }
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { Command } from "commander";
2
3
  import { PIBO_CONFIG_KEYS, getDefaultPiboConfigPath, deletePiboConfigValue, getDisplayPiboConfigValue, loadPiboConfig, redactPiboConfig, savePiboConfig, setPiboConfigValue, } from "./config/config.js";
3
4
  import { parsePiboThinkingLevel } from "./core/thinking.js";
@@ -20,6 +21,16 @@ function printConfigKeys() {
20
21
  function printRootDiscovery() {
21
22
  console.log(printRootDiscoveryText());
22
23
  }
24
+ function getPiboVersion() {
25
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
26
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
27
+ throw new Error("Unable to read Pibo package version");
28
+ }
29
+ return packageJson.version;
30
+ }
31
+ function printPiboVersion() {
32
+ console.log(getPiboVersion());
33
+ }
23
34
  function printConfigDiscovery() {
24
35
  console.log(printConfigDiscoveryText());
25
36
  }
@@ -46,6 +57,10 @@ export async function runPiboCli(argv = process.argv) {
46
57
  printRootDiscovery();
47
58
  return;
48
59
  }
60
+ if (argv[2] === "--version" || argv[2] === "-V") {
61
+ printPiboVersion();
62
+ return;
63
+ }
49
64
  if (argv[2] === "mcp") {
50
65
  const { runMcpCli } = await import("./mcp/index.js");
51
66
  await runMcpCli([argv[0] ?? "node", "pibo mcp", ...argv.slice(3)]);
@@ -348,6 +363,7 @@ export async function runPiboCli(argv = process.argv) {
348
363
  .option("--auth <mode>", "Auth service mode: 'better-auth' (default) or 'local' (loopback-only, no Google OAuth)")
349
364
  .option("--web-host <host>", "Bind the HTTP web host, for example 0.0.0.0 for LAN access")
350
365
  .option("--web-port <port>", "Bind the HTTP web host port", parsePort)
366
+ .option("--gateway-port <port>", "Bind the agent-runtime gateway port", parsePort)
351
367
  .action(async (options) => {
352
368
  const { runWebGatewayServer } = await import("./gateway/web.js");
353
369
  const authMode = options.auth;
@@ -360,6 +376,7 @@ export async function runPiboCli(argv = process.argv) {
360
376
  }
361
377
  await runWebGatewayServer({
362
378
  authMode: authMode,
379
+ port: options.gatewayPort,
363
380
  web: {
364
381
  host: options.webHost,
365
382
  port: options.webPort,
@@ -403,6 +420,9 @@ Commands:
403
420
  gateway Inspect and restart host gateways through safe CLI commands
404
421
  gateway:web Start a web gateway runtime (use --auth=local for loopback-only local auth)
405
422
 
423
+ Options:
424
+ --version Print the Pibo CLI version
425
+
406
426
  Next:
407
427
  pibo <command> --help
408
428
  `;
@@ -1,5 +1,5 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
- import { ensureConfigExists, findConfigPath, isHttpServer, } from './config.js';
2
+ import { ensureConfigExists, isHttpServer, loadConfig, } from './config.js';
3
3
  import { ErrorCode, formatCliError } from './errors.js';
4
4
  export const MCP_SERVER_DESCRIPTION_MAX_LENGTH = 480;
5
5
  export const ENABLED_MCP_SERVERS_CONTEXT_PATH = '.pibo/context/enabled-mcp-servers.md';
@@ -22,11 +22,15 @@ export function normalizeMcpServerDescription(value) {
22
22
  return description;
23
23
  }
24
24
  export async function listMcpServerInfos(configPath) {
25
- const path = findConfigPath(configPath);
26
- if (!path)
27
- return [];
28
- const config = await readMcpConfig(path);
29
- return Object.entries(config.mcpServers).map(([name, server]) => mcpServerInfoFromConfig(name, server));
25
+ try {
26
+ const config = await loadConfig(configPath);
27
+ return Object.entries(config.mcpServers).map(([name, server]) => mcpServerInfoFromConfig(name, server));
28
+ }
29
+ catch (error) {
30
+ if (error.message.includes('CONFIG_NOT_FOUND'))
31
+ return [];
32
+ throw error;
33
+ }
30
34
  }
31
35
  export async function setMcpServerDescription(serverName, descriptionInput, configPath) {
32
36
  const description = normalizeMcpServerDescription(descriptionInput);
@@ -2,7 +2,7 @@
2
2
  * Info command - Show server or tool details
3
3
  */
4
4
  import { getConnection, safeClose } from '../client.js';
5
- import { getServerConfig, loadConfig, } from '../config.js';
5
+ import { formatConfigSourceSummaries, getConfigSourceSummaries, loadConfig, } from '../config.js';
6
6
  import { ErrorCode, formatCliError, serverConnectionError, toolNotFoundError, } from '../errors.js';
7
7
  import { formatServerDetails, formatToolSchema } from '../output.js';
8
8
  /**
@@ -28,12 +28,24 @@ export async function infoCommand(options) {
28
28
  process.exit(ErrorCode.CLIENT_ERROR);
29
29
  }
30
30
  const { server: serverName, tool: toolName } = parseTarget(options.target);
31
- let serverConfig;
32
- try {
33
- serverConfig = getServerConfig(config, serverName);
34
- }
35
- catch (error) {
36
- console.error(error.message);
31
+ const serverConfig = config.mcpServers[serverName];
32
+ if (!serverConfig) {
33
+ const available = Object.keys(config.mcpServers);
34
+ const serverList = available.length > 0 ? available.join(', ') : '(none)';
35
+ const summaries = await getConfigSourceSummaries(options.configPath);
36
+ console.error(formatCliError({
37
+ code: ErrorCode.CLIENT_ERROR,
38
+ type: 'SERVER_NOT_FOUND',
39
+ message: `Server "${serverName}" not found in config`,
40
+ details: [
41
+ `Merged available servers: ${serverList}`,
42
+ 'Config search paths:',
43
+ formatConfigSourceSummaries(summaries),
44
+ ].join('\n'),
45
+ suggestion: available.length > 0
46
+ ? `Use one of: ${available.map((s) => `pibo mcp info ${s}`).join(', ')}`
47
+ : `Add server to mcp_servers.json: { "mcpServers": { "${serverName}": { ... } } }`,
48
+ }));
37
49
  process.exit(ErrorCode.CLIENT_ERROR);
38
50
  }
39
51
  let connection;
@@ -311,6 +311,17 @@ export function getDefaultConfigPaths() {
311
311
  paths.push(join(home, '.config', 'mcp', 'mcp_servers.json'));
312
312
  return paths;
313
313
  }
314
+ export function getConfigSearchPaths(explicitPath) {
315
+ const paths = [];
316
+ if (explicitPath) {
317
+ paths.push(resolve(explicitPath));
318
+ }
319
+ if (process.env.MCP_CONFIG_PATH) {
320
+ paths.push(resolve(process.env.MCP_CONFIG_PATH));
321
+ }
322
+ paths.push(...getDefaultConfigPaths());
323
+ return [...new Set(paths)];
324
+ }
314
325
  export function getPreferredConfigPath(explicitPath) {
315
326
  if (explicitPath) {
316
327
  return resolve(explicitPath);
@@ -341,38 +352,7 @@ export async function ensureConfigExists(explicitPath) {
341
352
  await writeFile(configPath, `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`);
342
353
  return configPath;
343
354
  }
344
- /**
345
- * Load and parse MCP servers configuration
346
- */
347
- export async function loadConfig(explicitPath) {
348
- let configPath;
349
- // Check explicit path from argument or environment
350
- if (explicitPath) {
351
- configPath = resolve(explicitPath);
352
- }
353
- else if (process.env.MCP_CONFIG_PATH) {
354
- configPath = resolve(process.env.MCP_CONFIG_PATH);
355
- }
356
- // If explicit path provided, it must exist
357
- if (configPath) {
358
- if (!existsSync(configPath)) {
359
- throw new Error(formatCliError(configNotFoundError(configPath)));
360
- }
361
- }
362
- else {
363
- // Search default paths
364
- const searchPaths = getDefaultConfigPaths();
365
- for (const path of searchPaths) {
366
- if (existsSync(path)) {
367
- configPath = path;
368
- break;
369
- }
370
- }
371
- if (!configPath) {
372
- throw new Error(formatCliError(configSearchError()));
373
- }
374
- }
375
- // Read and parse config
355
+ async function readRawConfig(configPath) {
376
356
  const content = await readFile(configPath, 'utf-8');
377
357
  let config;
378
358
  try {
@@ -381,45 +361,98 @@ export async function loadConfig(explicitPath) {
381
361
  catch (e) {
382
362
  throw new Error(formatCliError(configInvalidJsonError(configPath, e.message)));
383
363
  }
384
- // Validate structure
385
364
  if (!config.mcpServers || typeof config.mcpServers !== 'object') {
386
365
  throw new Error(formatCliError(configMissingFieldError(configPath)));
387
366
  }
388
- // Validate individual server configs
389
- for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
390
- if (!serverConfig || typeof serverConfig !== 'object') {
391
- throw new Error(formatCliError({
392
- code: ErrorCode.CLIENT_ERROR,
393
- type: 'CONFIG_INVALID_SERVER',
394
- message: `Invalid server configuration for "${serverName}"`,
395
- details: 'Server config must be an object',
396
- suggestion: `Use { "command": "..." } for stdio or { "url": "..." } for HTTP`,
397
- }));
398
- }
399
- const hasCommand = 'command' in serverConfig;
400
- const hasUrl = 'url' in serverConfig;
401
- if (!hasCommand && !hasUrl) {
402
- throw new Error(formatCliError({
403
- code: ErrorCode.CLIENT_ERROR,
404
- type: 'CONFIG_INVALID_SERVER',
405
- message: `Server "${serverName}" missing required field`,
406
- details: `Must have either "command" (for stdio) or "url" (for HTTP)`,
407
- suggestion: `Add "command": "npx ..." for local servers or "url": "https://..." for remote servers`,
408
- }));
367
+ return config;
368
+ }
369
+ function validateServerConfig(serverName, serverConfig) {
370
+ if (!serverConfig || typeof serverConfig !== 'object') {
371
+ throw new Error(formatCliError({
372
+ code: ErrorCode.CLIENT_ERROR,
373
+ type: 'CONFIG_INVALID_SERVER',
374
+ message: `Invalid server configuration for "${serverName}"`,
375
+ details: 'Server config must be an object',
376
+ suggestion: `Use { "command": "..." } for stdio or { "url": "..." } for HTTP`,
377
+ }));
378
+ }
379
+ const hasCommand = 'command' in serverConfig;
380
+ const hasUrl = 'url' in serverConfig;
381
+ if (!hasCommand && !hasUrl) {
382
+ throw new Error(formatCliError({
383
+ code: ErrorCode.CLIENT_ERROR,
384
+ type: 'CONFIG_INVALID_SERVER',
385
+ message: `Server "${serverName}" missing required field`,
386
+ details: `Must have either "command" (for stdio) or "url" (for HTTP)`,
387
+ suggestion: `Add "command": "npx ..." for local servers or "url": "https://..." for remote servers`,
388
+ }));
389
+ }
390
+ if (hasCommand && hasUrl) {
391
+ throw new Error(formatCliError({
392
+ code: ErrorCode.CLIENT_ERROR,
393
+ type: 'CONFIG_INVALID_SERVER',
394
+ message: `Server "${serverName}" has both "command" and "url"`,
395
+ details: 'A server must be either stdio (command) or HTTP (url), not both',
396
+ suggestion: `Remove one of "command" or "url"`,
397
+ }));
398
+ }
399
+ }
400
+ /**
401
+ * Load and merge MCP servers configuration.
402
+ * More specific paths appear first and win server-name conflicts.
403
+ */
404
+ export async function loadConfig(explicitPath) {
405
+ const explicitOrEnvPath = explicitPath
406
+ ? resolve(explicitPath)
407
+ : process.env.MCP_CONFIG_PATH
408
+ ? resolve(process.env.MCP_CONFIG_PATH)
409
+ : undefined;
410
+ if (explicitOrEnvPath && !existsSync(explicitOrEnvPath)) {
411
+ throw new Error(formatCliError(configNotFoundError(explicitOrEnvPath)));
412
+ }
413
+ const existingPaths = getConfigSearchPaths(explicitPath).filter((path) => existsSync(path));
414
+ if (existingPaths.length === 0) {
415
+ throw new Error(formatCliError(configSearchError()));
416
+ }
417
+ const merged = { mcpServers: {} };
418
+ for (const configPath of existingPaths) {
419
+ const config = await readRawConfig(configPath);
420
+ for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
421
+ if (!(serverName in merged.mcpServers)) {
422
+ merged.mcpServers[serverName] = serverConfig;
423
+ }
409
424
  }
410
- if (hasCommand && hasUrl) {
411
- throw new Error(formatCliError({
412
- code: ErrorCode.CLIENT_ERROR,
413
- type: 'CONFIG_INVALID_SERVER',
414
- message: `Server "${serverName}" has both "command" and "url"`,
415
- details: 'A server must be either stdio (command) or HTTP (url), not both',
416
- suggestion: `Remove one of "command" or "url"`,
417
- }));
425
+ }
426
+ for (const [serverName, serverConfig] of Object.entries(merged.mcpServers)) {
427
+ validateServerConfig(serverName, serverConfig);
428
+ }
429
+ return substituteEnvVarsInObject(merged);
430
+ }
431
+ export async function getConfigSourceSummaries(explicitPath) {
432
+ const summaries = [];
433
+ for (const configPath of getConfigSearchPaths(explicitPath)) {
434
+ if (!existsSync(configPath)) {
435
+ summaries.push({ path: configPath, exists: false, servers: [] });
436
+ continue;
418
437
  }
438
+ const config = await readRawConfig(configPath);
439
+ summaries.push({
440
+ path: configPath,
441
+ exists: true,
442
+ servers: Object.keys(config.mcpServers),
443
+ });
419
444
  }
420
- // Substitute environment variables
421
- config = substituteEnvVarsInObject(config);
422
- return config;
445
+ return summaries;
446
+ }
447
+ export function formatConfigSourceSummaries(summaries) {
448
+ return summaries
449
+ .map((summary) => {
450
+ const serverList = summary.exists
451
+ ? summary.servers.join(', ') || '(none)'
452
+ : '(not found)';
453
+ return ` - ${summary.path}: ${serverList}`;
454
+ })
455
+ .join('\n');
423
456
  }
424
457
  /**
425
458
  * Get a specific server config by name
@@ -133,6 +133,11 @@ export const piboCorePlugin = definePiboPlugin({
133
133
  path: builtinSkillPath("pibo-docker-system"),
134
134
  kind: "builtin",
135
135
  });
136
+ api.registerSkill({
137
+ name: "graphify",
138
+ path: builtinSkillPath("graphify"),
139
+ kind: "builtin",
140
+ });
136
141
  api.registerSkill({
137
142
  name: "prd",
138
143
  path: builtinSkillPath("prd"),
@@ -245,7 +250,16 @@ export const piboCorePlugin = definePiboPlugin({
245
250
  slashCommands: ["thinking"],
246
251
  execute(context, event) {
247
252
  const params = getThinkingParams(event);
248
- return params.level ? context.setThinkingLevel(params.level) : context.getThinkingLevel();
253
+ if (!params.level)
254
+ return { ...context.getThinkingLevel(), action: "show_thinking_menu" };
255
+ const previousLevel = context.getThinkingLevel().level;
256
+ const result = context.setThinkingLevel(params.level);
257
+ return {
258
+ ...result,
259
+ action: "set_thinking_level",
260
+ previousLevel,
261
+ changed: previousLevel !== result.level,
262
+ };
249
263
  },
250
264
  });
251
265
  api.registerGatewayAction({
@@ -1,5 +1,6 @@
1
1
  import { dirname, resolve } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { createCodexImageGenerationToolProfile } from "../tools/codex-image-generation.js";
3
4
  import { definePiboPlugin } from "./registry.js";
4
5
  const CODEX_COMPAT_REGISTERED_TOOL_NAMES = [
5
6
  "apply_patch",
@@ -22,6 +23,7 @@ export const piboCodexCompatPlugin = definePiboPlugin({
22
23
  description: toolDescriptions[name],
23
24
  });
24
25
  }
26
+ api.registerTool(createCodexImageGenerationToolProfile());
25
27
  api.registerContextFile({
26
28
  key: CODEX_BASE_PROMPT_CONTEXT_FILE_KEY,
27
29
  label: "Codex Base Prompt",