@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
@@ -8,7 +8,7 @@ export function buildCompactTerminalRows(traceView, options) {
8
8
  const flatNodes = flattenTraceNodes(traceView.nodes)
9
9
  .sort((left, right) => compareTraceNodes(left.node, right.node))
10
10
  .filter((item) => item.node.type !== "agent.turn" && (options.showThinking || item.node.type !== "model.reasoning"));
11
- const candidates = flatNodes.map((item) => createRowCandidate(item.node, item.turnId));
11
+ const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
12
12
  return groupRelatedToolCandidates(candidates).map((candidate) => candidate.row);
13
13
  }
14
14
  function flattenTraceNodes(nodes, turnId) {
@@ -128,7 +128,7 @@ function createToolRowCandidate(node, turnId) {
128
128
  const image = classifyImageTool(node);
129
129
  if (image) {
130
130
  const row = createImageToolRow(node, image);
131
- return { row, turnId, image: image.detail };
131
+ return { row, turnId, image: image.groupable ? image.detail : undefined };
132
132
  }
133
133
  const preview = previewLines(node.error ?? node.output, COMPACT_TERMINAL_OUTPUT_PREVIEW_LINES, node.error ? "red" : "dim");
134
134
  const row = {
@@ -159,7 +159,7 @@ function createToolRowCandidate(node, turnId) {
159
159
  }
160
160
  function createImageToolRow(node, image) {
161
161
  const status = mapStatus(node.status);
162
- const detailLabel = image.path ? `Path: ${image.path}` : image.query ? `Query: ${image.query}` : image.mimeType ? `Type: ${image.mimeType}` : "Image content returned";
162
+ 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";
163
163
  return {
164
164
  id: node.id,
165
165
  kind: "tool.image",
@@ -168,7 +168,7 @@ function createImageToolRow(node, image) {
168
168
  lines: [
169
169
  {
170
170
  prefix: "bullet",
171
- tokens: [token(imageToolVerb(node.status, image.count), toneForStatus(node.status), "semibold")],
171
+ tokens: [token(image.verb, toneForStatus(node.status), "semibold")],
172
172
  },
173
173
  {
174
174
  prefix: "detail",
@@ -186,7 +186,7 @@ function createToolResultRowCandidate(node, turnId) {
186
186
  const image = classifyImageTool(node);
187
187
  if (image) {
188
188
  const row = createImageToolRow(node, image);
189
- return { row, turnId, image: image.detail };
189
+ return { row, turnId, image: image.groupable ? image.detail : undefined };
190
190
  }
191
191
  return { row: createToolResultRow(node), turnId };
192
192
  }
@@ -311,7 +311,7 @@ function createExecutionCommandRow(node) {
311
311
  return createStatusToolRow(node);
312
312
  }
313
313
  if (node.title === "thinking") {
314
- return createThinkingToolRow(node);
314
+ return isThinkingLevelSetOutput(node.output) ? createThinkingLevelSetRow(node) : createThinkingToolRow(node);
315
315
  }
316
316
  if (node.title === "fast_mode") {
317
317
  return createFastModeToolRow(node);
@@ -369,6 +369,33 @@ function createThinkingToolRow(node) {
369
369
  expandable: false,
370
370
  };
371
371
  }
372
+ function createThinkingLevelSetRow(node) {
373
+ const result = isRecord(node.output) ? node.output : undefined;
374
+ const level = stringValue(result?.level);
375
+ const changed = result?.changed !== false;
376
+ const supported = result?.supported !== false;
377
+ const label = !supported
378
+ ? "Thinking level is not supported by this model."
379
+ : level
380
+ ? changed ? `Thinking level set to ${level}.` : `Thinking level is already ${level}.`
381
+ : "Thinking level updated.";
382
+ return {
383
+ id: node.id,
384
+ kind: "execution.command",
385
+ status: mapStatus(node.status),
386
+ lines: [
387
+ {
388
+ prefix: "bullet",
389
+ tokens: [token(label, supported ? "green" : "dim", "semibold")],
390
+ },
391
+ ],
392
+ sourceNodeIds: [node.id],
393
+ input: node.input,
394
+ output: node.output,
395
+ error: node.error,
396
+ expandable: false,
397
+ };
398
+ }
372
399
  function createFastModeToolRow(node) {
373
400
  const result = isRecord(node.output) ? node.output : undefined;
374
401
  const mode = result?.mode === "fast" ? "fast" : result?.mode === "normal" ? "normal" : undefined;
@@ -495,6 +522,33 @@ function sessionErrorDetailLines(details) {
495
522
  tokens: [token(`${label}: `, "dim"), token(value, "red")],
496
523
  }));
497
524
  }
525
+ function syncThinkingToolRows(candidates) {
526
+ const latest = candidates.map((candidate) => candidate.row.output).filter(isThinkingOutput).at(-1);
527
+ if (!latest)
528
+ return [...candidates];
529
+ return candidates.map((candidate) => {
530
+ if (candidate.row.kind !== "tool.thinking" || !isRecord(candidate.row.output))
531
+ return candidate;
532
+ return {
533
+ ...candidate,
534
+ row: {
535
+ ...candidate.row,
536
+ output: {
537
+ ...candidate.row.output,
538
+ level: latest.level,
539
+ availableLevels: latest.availableLevels,
540
+ supported: latest.supported,
541
+ },
542
+ },
543
+ };
544
+ });
545
+ }
546
+ function isThinkingOutput(value) {
547
+ return isRecord(value) && typeof value.level === "string" && Array.isArray(value.availableLevels);
548
+ }
549
+ function isThinkingLevelSetOutput(value) {
550
+ return isRecord(value) && value.action === "set_thinking_level";
551
+ }
498
552
  function groupRelatedToolCandidates(candidates) {
499
553
  const grouped = [];
500
554
  for (let index = 0; index < candidates.length; index += 1) {
@@ -640,32 +694,48 @@ function detailItemsForGroup(candidates, kind) {
640
694
  function classifyImageTool(node) {
641
695
  const normalized = (node.title ?? "").trim().toLowerCase();
642
696
  const args = isRecord(node.input) ? node.input : undefined;
643
- const path = previewPath(args) ?? previewPath(isRecord(node.output) ? recordField(node.output, "details") : undefined) ?? previewPath(isRecord(node.output) ? node.output : undefined);
697
+ const output = isRecord(node.output) ? node.output : undefined;
698
+ const details = recordField(output, "details");
699
+ const codexOperation = normalized === "codex_image_generation" ? codexImageOperation(details) ?? codexImageOperationFromInput(args) : undefined;
700
+ const path = codexOperation
701
+ ? stringValue(details?.savedPath) ?? previewPath(details)
702
+ : previewPath(args) ?? previewPath(details) ?? previewPath(output);
703
+ const artifactId = codexOperation ? stringValue(details?.artifactId) : undefined;
644
704
  const query = previewQuery(args);
645
705
  const images = collectImagePayloads(node.output);
646
706
  const isImageTool = matchesTool(normalized, ["view_image", "image", "screenshot"]);
647
- if (!images.length && !isImageTool)
707
+ if (!images.length && !isImageTool && !codexOperation)
648
708
  return undefined;
649
709
  const mimeType = images.map((image) => image.mimeType).find((value) => Boolean(value));
650
710
  const count = Math.max(1, images.length);
651
711
  const summary = {
652
712
  type: images.length > 1 ? "images" : images.length === 1 ? "image" : "image_reference",
713
+ toolName: codexOperation ? node.title : undefined,
714
+ operation: codexOperation,
653
715
  path,
716
+ savedPath: codexOperation ? path : undefined,
717
+ artifactId,
718
+ model: codexOperation ? stringValue(details?.model) : undefined,
719
+ referencedImageCount: codexOperation ? numberValue(details?.referencedImageCount) : undefined,
654
720
  query,
655
721
  mimeType,
656
722
  count: count > 1 ? count : undefined,
657
723
  detail: images.length ? "Image data hidden in terminal view." : "Image path only; binary data is hidden in terminal view.",
658
724
  };
659
- const labelTarget = path ?? query ?? mimeType ?? "image";
725
+ const labelTarget = path ?? artifactId ?? query ?? mimeType ?? "image";
726
+ const verb = codexOperation ? codexImageVerb(node.status, codexOperation) : imageToolVerb(node.status, count);
660
727
  return {
661
728
  count,
662
729
  path,
730
+ artifactId,
663
731
  query,
664
732
  mimeType,
733
+ verb,
734
+ groupable: !codexOperation,
665
735
  summary,
666
736
  detail: {
667
737
  id: node.id,
668
- label: `${node.status === "error" ? "Image failed" : "Viewed image"} ${labelTarget}`,
738
+ label: `${verb} ${labelTarget}`,
669
739
  status: mapStatus(node.status),
670
740
  input: sanitizeImagePayload(node.input),
671
741
  output: summary,
@@ -725,6 +795,34 @@ function imageToolVerb(status, count) {
725
795
  return count > 1 ? "Image reads failed" : "Image read failed";
726
796
  return count > 1 ? "Viewed images" : "Viewed image";
727
797
  }
798
+ function codexImageOperation(details) {
799
+ const operation = stringValue(details?.operation);
800
+ return operation === "generate" || operation === "edit" ? operation : undefined;
801
+ }
802
+ function codexImageOperationFromInput(args) {
803
+ if (!args)
804
+ return undefined;
805
+ const referencedPaths = args.referenced_image_paths;
806
+ if (Array.isArray(referencedPaths) && referencedPaths.length > 0)
807
+ return "edit";
808
+ if (numberValue(args.num_last_images_to_include) !== undefined)
809
+ return "edit";
810
+ return "generate";
811
+ }
812
+ function codexImageVerb(status, operation) {
813
+ if (operation === "edit") {
814
+ if (status === "running")
815
+ return "Editing image";
816
+ if (status === "error")
817
+ return "Image edit failed";
818
+ return "Edited image";
819
+ }
820
+ if (status === "running")
821
+ return "Generating image";
822
+ if (status === "error")
823
+ return "Image generation failed";
824
+ return "Generated image";
825
+ }
728
826
  function recordField(record, key) {
729
827
  const value = record?.[key];
730
828
  return isRecord(value) ? value : undefined;
@@ -789,6 +887,9 @@ function previewQuery(value) {
789
887
  }
790
888
  return undefined;
791
889
  }
890
+ function numberValue(value) {
891
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
892
+ }
792
893
  function previewLines(value, maxVisibleLines, tone = "dim", _maxLineLength = 160) {
793
894
  const empty = { lines: [], visibleLineCount: 0, omittedLineCount: 0, totalLineCount: 0, maxVisibleLineCount: maxVisibleLines };
794
895
  const text = previewText(value);
@@ -19,6 +19,9 @@ function areTraceNodesSorted(nodes) {
19
19
  return true;
20
20
  }
21
21
  export function compareTraceNodes(left, right) {
22
+ const bySameTurnPhase = compareSameTurnPhase(left, right);
23
+ if (bySameTurnPhase !== 0)
24
+ return bySameTurnPhase;
22
25
  const byStartTime = compareOptionalIsoTime(left.startedAt, right.startedAt);
23
26
  if (byStartTime !== 0)
24
27
  return byStartTime;
@@ -27,6 +30,15 @@ export function compareTraceNodes(left, right) {
27
30
  return byOrder;
28
31
  return left.id.localeCompare(right.id);
29
32
  }
33
+ function compareSameTurnPhase(left, right) {
34
+ if (!left.eventId || left.eventId !== right.eventId)
35
+ return 0;
36
+ const leftPhase = left.orderKey?.phaseRank;
37
+ const rightPhase = right.orderKey?.phaseRank;
38
+ if (leftPhase === undefined || rightPhase === undefined)
39
+ return 0;
40
+ return leftPhase - rightPhase;
41
+ }
30
42
  function compareOptionalIsoTime(left, right) {
31
43
  if (!left && !right)
32
44
  return 0;
@@ -1,4 +1,5 @@
1
1
  import { Command } from "commander";
2
+ import { createDefaultPiboPluginRegistry } from "../plugins/builtin.js";
2
3
  import { UserSkillManager } from "../user-skills/manager.js";
3
4
  import { readFileSync } from "node:fs";
4
5
  import { resolve } from "node:path";
@@ -9,7 +10,30 @@ function printJson(value) {
9
10
  export async function runSkillsCli(argv) {
10
11
  const manager = new UserSkillManager(os.homedir());
11
12
  const program = new Command();
12
- program.name("pibo skills").description("Manage Pibo user skills (not built-in or plugin skills)");
13
+ program
14
+ .name("pibo skills")
15
+ .description("Manage Pibo user skills and inspect the built-in/plugin skill catalog")
16
+ .addHelpText("after", "\nBuilt-in/plugin skills are selected by agent profiles. Run `pibo skills catalog` to list them.\n");
17
+ program
18
+ .command("catalog")
19
+ .description("List built-in and plugin skills available to profiles")
20
+ .option("--json", "Print JSON")
21
+ .action((options) => {
22
+ const registry = createDefaultPiboPluginRegistry();
23
+ const skills = registry.getCapabilityCatalog().skills.filter((skill) => skill.kind !== "user");
24
+ if (options.json) {
25
+ printJson(skills);
26
+ return;
27
+ }
28
+ if (skills.length === 0) {
29
+ console.log("No built-in or plugin skills registered.");
30
+ return;
31
+ }
32
+ console.log("NAME\tKIND\tPATH");
33
+ for (const skill of skills) {
34
+ console.log(`${skill.name}\t${skill.kind ?? "plugin"}\t${skill.path}`);
35
+ }
36
+ });
13
37
  program
14
38
  .command("list")
15
39
  .description("List user skills managed by this CLI")
@@ -0,0 +1,272 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { platform, release, arch } from "node:os";
3
+ import { dirname, extname, isAbsolute, join, resolve } from "node:path";
4
+ import { AuthStorage } from "@mariozechner/pi-coding-agent";
5
+ import { Type } from "@mariozechner/pi-ai";
6
+ import { defineTool } from "@mariozechner/pi-coding-agent";
7
+ import { getPiboHome } from "../core/pibo-home.js";
8
+ const OPENAI_CODEX_PROVIDER = "openai-codex";
9
+ const DEFAULT_CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api";
10
+ const CODEX_IMAGE_MODEL = "gpt-image-2";
11
+ const MAX_EDIT_IMAGES = 5;
12
+ const OPENAI_JWT_CLAIM_PATH = "https://api.openai.com/auth";
13
+ function trimTrailingSlashes(value) {
14
+ return value.replace(/\/+$/, "");
15
+ }
16
+ export function resolveCodexImageUrl(operation, baseUrl) {
17
+ const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BACKEND_BASE_URL;
18
+ let normalized = trimTrailingSlashes(raw.trim());
19
+ if (normalized.endsWith("/codex/responses"))
20
+ normalized = normalized.slice(0, -"/responses".length);
21
+ if (!normalized.endsWith("/codex"))
22
+ normalized = `${normalized}/codex`;
23
+ return `${normalized}/images/${operation}`;
24
+ }
25
+ function decodeJwtPayload(token) {
26
+ try {
27
+ const payload = token.split(".")[1];
28
+ if (!payload)
29
+ return undefined;
30
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
31
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
32
+ const parsed = JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
33
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ function getOpenAiAccountId(accessToken, storedAccountId) {
40
+ if (typeof storedAccountId === "string" && storedAccountId.trim().length > 0)
41
+ return storedAccountId;
42
+ const payload = decodeJwtPayload(accessToken);
43
+ const auth = payload?.[OPENAI_JWT_CLAIM_PATH];
44
+ if (!auth || typeof auth !== "object" || Array.isArray(auth))
45
+ return undefined;
46
+ const accountId = auth.chatgpt_account_id;
47
+ return typeof accountId === "string" && accountId.trim().length > 0 ? accountId : undefined;
48
+ }
49
+ async function getCodexImageAuth() {
50
+ const authStorage = AuthStorage.create();
51
+ const credential = authStorage.get(OPENAI_CODEX_PROVIDER);
52
+ if (credential?.type !== "oauth") {
53
+ throw new Error("codex_image_generation requires ChatGPT/Codex OAuth login for provider openai-codex. Use the existing OpenAI Codex login flow; API keys are not supported for this tool.");
54
+ }
55
+ const accessToken = await authStorage.getApiKey(OPENAI_CODEX_PROVIDER, { includeFallback: false });
56
+ if (!accessToken) {
57
+ throw new Error("codex_image_generation could not load a ChatGPT/Codex OAuth access token for provider openai-codex. Please log in again with the OpenAI Codex login flow.");
58
+ }
59
+ const accountId = getOpenAiAccountId(accessToken, credential.accountId);
60
+ if (!accountId) {
61
+ throw new Error("codex_image_generation could not resolve the ChatGPT account id from the openai-codex OAuth credential. Please log in again with the OpenAI Codex login flow.");
62
+ }
63
+ return { accessToken, accountId };
64
+ }
65
+ function mimeTypeForPath(path) {
66
+ switch (extname(path).toLowerCase()) {
67
+ case ".jpg":
68
+ case ".jpeg":
69
+ return "image/jpeg";
70
+ case ".webp":
71
+ return "image/webp";
72
+ case ".gif":
73
+ return "image/gif";
74
+ default:
75
+ return "image/png";
76
+ }
77
+ }
78
+ function resolveCwd(baseCwd, path) {
79
+ return isAbsolute(path) ? path : resolve(baseCwd, path);
80
+ }
81
+ function isImageContent(value) {
82
+ return Boolean(value)
83
+ && typeof value === "object"
84
+ && value.type === "image"
85
+ && typeof value.data === "string"
86
+ && typeof value.mimeType === "string";
87
+ }
88
+ function imageContentToDataUrl(image) {
89
+ if (image.data.startsWith("data:"))
90
+ return image.data;
91
+ return `data:${image.mimeType};base64,${image.data}`;
92
+ }
93
+ async function imageFileToDataUrl(cwd, path) {
94
+ const resolved = resolveCwd(cwd, path);
95
+ const data = await readFile(resolved);
96
+ return `data:${mimeTypeForPath(resolved)};base64,${data.toString("base64")}`;
97
+ }
98
+ function contentFromSessionEntry(entry) {
99
+ if (!entry || typeof entry !== "object")
100
+ return undefined;
101
+ const record = entry;
102
+ if (record.type === "message") {
103
+ const message = record.message;
104
+ return message && typeof message === "object" ? message.content : undefined;
105
+ }
106
+ if (record.type === "custom_message")
107
+ return record.content;
108
+ return undefined;
109
+ }
110
+ function recentImagesFromSessionEntries(entries, count) {
111
+ const images = [];
112
+ for (const entry of [...entries].reverse()) {
113
+ const content = contentFromSessionEntry(entry);
114
+ if (!Array.isArray(content))
115
+ continue;
116
+ for (const item of [...content].reverse()) {
117
+ if (!isImageContent(item))
118
+ continue;
119
+ images.push(imageContentToDataUrl(item));
120
+ if (images.length === count)
121
+ break;
122
+ }
123
+ if (images.length === count)
124
+ break;
125
+ }
126
+ images.reverse();
127
+ return images.map((image_url) => ({ image_url }));
128
+ }
129
+ function validateImageArgs(params) {
130
+ if (params.prompt.trim().length === 0)
131
+ throw new Error("`prompt` must not be empty.");
132
+ const pathCount = params.referenced_image_paths?.length ?? 0;
133
+ if (pathCount > MAX_EDIT_IMAGES)
134
+ throw new Error(`\`referenced_image_paths\` must contain at most ${MAX_EDIT_IMAGES} paths.`);
135
+ if (pathCount > 0 && params.num_last_images_to_include !== undefined) {
136
+ throw new Error("Provide only one of `referenced_image_paths` or `num_last_images_to_include`.");
137
+ }
138
+ if (params.num_last_images_to_include !== undefined) {
139
+ if (!Number.isInteger(params.num_last_images_to_include) || params.num_last_images_to_include < 1 || params.num_last_images_to_include > MAX_EDIT_IMAGES) {
140
+ throw new Error(`\`num_last_images_to_include\` must be an integer between 1 and ${MAX_EDIT_IMAGES}.`);
141
+ }
142
+ }
143
+ }
144
+ function sanitizePathPart(value) {
145
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 120) || "unknown";
146
+ }
147
+ function artifactPath(sessionId, toolCallId) {
148
+ const safeSessionId = sanitizePathPart(sessionId?.trim() || "local");
149
+ const safeToolCallId = sanitizePathPart(toolCallId || `image_${Date.now()}`);
150
+ const artifactId = `${safeSessionId}/${safeToolCallId}.png`;
151
+ return {
152
+ artifactId,
153
+ savedPath: join(getPiboHome(), "generated_images", safeSessionId, `${safeToolCallId}.png`),
154
+ };
155
+ }
156
+ async function saveGeneratedImage(sessionId, toolCallId, b64Json) {
157
+ const target = artifactPath(sessionId, toolCallId);
158
+ await mkdir(dirname(target.savedPath), { recursive: true });
159
+ await writeFile(target.savedPath, Buffer.from(b64Json.trim(), "base64"));
160
+ return target;
161
+ }
162
+ function truncateErrorBody(text) {
163
+ const compact = text.replace(/\s+/g, " ").trim();
164
+ return compact.length > 1000 ? `${compact.slice(0, 1000)}…` : compact;
165
+ }
166
+ async function postCodexImageRequest(operation, body, auth, signal, baseUrl) {
167
+ const url = resolveCodexImageUrl(operation, baseUrl);
168
+ const response = await fetch(url, {
169
+ method: "POST",
170
+ signal,
171
+ headers: {
172
+ Authorization: `Bearer ${auth.accessToken}`,
173
+ "chatgpt-account-id": auth.accountId,
174
+ originator: "pi",
175
+ "User-Agent": `pibo (${platform()} ${release()}; ${arch()})`,
176
+ Accept: "application/json",
177
+ "Content-Type": "application/json",
178
+ },
179
+ body: JSON.stringify(body),
180
+ });
181
+ if (!response.ok) {
182
+ const text = await response.text().catch(() => "");
183
+ throw new Error(`Codex image ${operation} request failed: ${response.status}${text ? ` ${truncateErrorBody(text)}` : ""}`);
184
+ }
185
+ return await response.json();
186
+ }
187
+ function firstImageB64(response) {
188
+ const b64 = response.data?.[0]?.b64_json;
189
+ if (!b64 || typeof b64 !== "string")
190
+ throw new Error("Codex image generation returned no image data.");
191
+ return b64;
192
+ }
193
+ function createRequest(prompt, images) {
194
+ return {
195
+ ...(images && images.length > 0 ? { images } : {}),
196
+ prompt,
197
+ background: "auto",
198
+ model: CODEX_IMAGE_MODEL,
199
+ quality: "auto",
200
+ size: "auto",
201
+ };
202
+ }
203
+ async function collectEditImages(params, cwd, sessionEntries) {
204
+ const paths = params.referenced_image_paths ?? [];
205
+ if (paths.length > 0) {
206
+ return await Promise.all(paths.map(async (path) => ({ image_url: await imageFileToDataUrl(cwd, path) })));
207
+ }
208
+ if (params.num_last_images_to_include !== undefined) {
209
+ const images = recentImagesFromSessionEntries(sessionEntries, params.num_last_images_to_include);
210
+ if (images.length !== params.num_last_images_to_include) {
211
+ throw new Error(`Requested the last ${params.num_last_images_to_include} conversation images, but only ${images.length} were available.`);
212
+ }
213
+ return images;
214
+ }
215
+ return [];
216
+ }
217
+ export function createCodexImageGenerationToolDefinition(context = {}, options = {}) {
218
+ return defineTool({
219
+ name: "codex_image_generation",
220
+ label: "Codex Image Generation",
221
+ description: "Generates or edits images through the ChatGPT/Codex backend API using openai-codex OAuth entitlement. Does not use the public OpenAI Images API.",
222
+ promptSnippet: "Use codex_image_generation to create an image from a prompt, or to edit referenced/recent images with the Codex/ChatGPT image backend.",
223
+ promptGuidelines: [
224
+ "Use codex_image_generation for image generation and image edits when the user asks for pictures, visual variants, or edits to existing images.",
225
+ "For edits, pass either referenced_image_paths for local image files or num_last_images_to_include for recent conversation images, but not both.",
226
+ ],
227
+ executionMode: "sequential",
228
+ parameters: Type.Object({
229
+ prompt: Type.String({ description: "Image generation/editing prompt. Be specific about the desired final image." }),
230
+ referenced_image_paths: Type.Optional(Type.Array(Type.String({ description: "Local filesystem image path to edit." }), { description: `Optional local image paths to edit. At most ${MAX_EDIT_IMAGES}.` })),
231
+ num_last_images_to_include: Type.Optional(Type.Number({ description: `Use the last N conversation images as edit references. Must be between 1 and ${MAX_EDIT_IMAGES}.` })),
232
+ }),
233
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
234
+ validateImageArgs(params);
235
+ const images = await collectEditImages(params, ctx.cwd, ctx.sessionManager.getBranch());
236
+ const operation = images.length > 0 ? "edit" : "generate";
237
+ const endpoint = operation === "edit" ? "edits" : "generations";
238
+ const auth = await getCodexImageAuth();
239
+ const response = await postCodexImageRequest(endpoint, createRequest(params.prompt, images), auth, signal, options.baseUrl);
240
+ const result = firstImageB64(response);
241
+ const saved = await saveGeneratedImage(context.piboSessionId, toolCallId, result);
242
+ const details = {
243
+ provider: OPENAI_CODEX_PROVIDER,
244
+ api: "codex-chatgpt-images",
245
+ operation,
246
+ model: CODEX_IMAGE_MODEL,
247
+ savedPath: saved.savedPath,
248
+ artifactId: saved.artifactId,
249
+ referencedImageCount: images.length,
250
+ endpoint,
251
+ created: response.created,
252
+ background: response.background,
253
+ quality: response.quality,
254
+ size: response.size,
255
+ };
256
+ return {
257
+ content: [
258
+ { type: "image", data: result, mimeType: "image/png" },
259
+ { type: "text", text: `Generated image saved to ${saved.savedPath}` },
260
+ ],
261
+ details,
262
+ };
263
+ },
264
+ });
265
+ }
266
+ export function createCodexImageGenerationToolProfile() {
267
+ return {
268
+ name: "codex_image_generation",
269
+ description: "Generate and edit images through the ChatGPT/Codex backend API using openai-codex OAuth.",
270
+ createDefinition: createCodexImageGenerationToolDefinition,
271
+ };
272
+ }
@@ -379,6 +379,77 @@ If \`browser-use --connect\` cannot find Chrome, ask the user whether they want
379
379
  14. If Chrome fails to start with a "SingletonLock" error, the wrapper auto-detects and terminates stale Chrome processes holding the lock. Retry your command.
380
380
  `,
381
381
  };
382
+ export const GRAPHIFY_GUIDE = {
383
+ name: 'graphify',
384
+ description: 'Generate codebase knowledge graphs with the Graphify CLI.',
385
+ content: `---
386
+ name: graphify
387
+ description: Generates interactive codebase knowledge graphs and markdown reports from a workspace folder.
388
+ allowed-tools: Bash(graphify:*)
389
+ ---
390
+
391
+ # Codebase Visualization with Graphify
392
+
393
+ Graphify turns a folder into derived artifacts under \`graphify-out/\`:
394
+
395
+ - \`graphify-out/graph.html\` — an interactive clickable graph;
396
+ - \`graphify-out/graph.json\` — machine-readable graph data;
397
+ - \`graphify-out/GRAPH_REPORT.md\` — a markdown summary with key concepts and suggested questions.
398
+
399
+ Use it when a user asks to graph, map, visualize, or quickly understand a repo/workspace shape.
400
+
401
+ ## Prerequisites
402
+
403
+ Install and apply the Pibo tool environment:
404
+
405
+ \`\`\`bash
406
+ pibo tools install graphify
407
+ eval "$(pibo tools env graphify)"
408
+ graphify --help
409
+ \`\`\`
410
+
411
+ Inside the Pibo source repo, use \`npm run --silent dev -- tools ...\` while testing local changes:
412
+
413
+ \`\`\`bash
414
+ npm run --silent dev -- tools install graphify
415
+ eval "$(npm run --silent dev -- tools env graphify)"
416
+ \`\`\`
417
+
418
+ The installer uses the PyPI package \`graphifyy\` and runs \`graphify install --platform pi\` so the CLI is ready for Pi/Pibo workflows.
419
+
420
+ ## Core Workflow
421
+
422
+ 1. Choose the workspace path. Prefer the active Pibo Room or session workspace boundary when known.
423
+ 2. Run Graphify from that folder or pass the target path explicitly. With \`graphifyy==0.9.x\`, \`graphify .\` writes extraction output under \`graphify-out/\`.
424
+ 3. For a code-only workspace without an LLM key, run \`graphify cluster-only . --no-label\` after extraction to produce the HTML graph and markdown report.
425
+ 4. Put generated artifacts in an ignored room/session artifact directory when possible; do not commit them unless the user explicitly asks.
426
+ 5. Read \`graphify-out/GRAPH_REPORT.md\` first, then open \`graphify-out/graph.html\` when an interactive view is useful.
427
+
428
+ \`\`\`bash
429
+ cd /path/to/workspace
430
+ graphify .
431
+ graphify cluster-only . --no-label
432
+ ls graphify-out/graph.html graphify-out/graph.json graphify-out/GRAPH_REPORT.md
433
+ \`\`\`
434
+
435
+ ## Pibo Usage Notes
436
+
437
+ - For Room-bound work, graph the Room workspace rather than the agent harness checkout unless the user asks otherwise.
438
+ - If generating inside a Git repo, check \`git status --short\` before and after so graph artifacts are not accidentally included in unrelated commits.
439
+ - Code-only extraction can run without an LLM API key. Including docs, README files, papers, images, or semantic-labeling steps may require a configured Graphify backend/API key; if no key is available, graph a code-only subdirectory or skip semantic labels with \`cluster-only . --no-label\`.
440
+ - For large monorepos, start with a subdirectory such as \`src/\`, a code package, or a docs folder only when the needed LLM backend is configured.
441
+ - Treat graph output as derived data. Recompute on demand when the branch or workspace changes.
442
+
443
+ ## Next Commands
444
+
445
+ \`\`\`bash
446
+ pibo tools show graphify
447
+ pibo tools guide graphify graphify
448
+ pibo tools path graphify
449
+ pibo tools doctor graphify
450
+ \`\`\`
451
+ `,
452
+ };
382
453
  export const REMOTE_BROWSER_GUIDE = {
383
454
  name: 'remote-browser',
384
455
  description: 'Browser automation workflow for sandboxed or remote agents.',
@@ -166,9 +166,13 @@ function printEnv(name) {
166
166
  return;
167
167
  }
168
168
  const binDir = status.executablePath.replace(/\/[^/]+$/, '');
169
- const wrapperPath = entry.name === 'agent-browser' ? ensureAgentBrowserWrapper(status) : ensureBrowserUseWrapper(status);
170
- const wrapperBinDir = wrapperPath ? wrapperPath.replace(/\/[^/]+$/, '') : `${status.homeDir}/bin`;
171
- console.log(`export PATH="${wrapperBinDir}:${binDir}:$PATH"`);
169
+ const wrapperPath = entry.name === 'browser-use'
170
+ ? ensureBrowserUseWrapper(status)
171
+ : entry.name === 'agent-browser'
172
+ ? ensureAgentBrowserWrapper(status)
173
+ : undefined;
174
+ const envBinDirs = wrapperPath ? `${wrapperPath.replace(/\/[^/]+$/, '')}:${binDir}` : binDir;
175
+ console.log(`export PATH="${envBinDirs}:$PATH"`);
172
176
  if (entry.runtime.homeEnvVar)
173
177
  console.log(`export ${entry.runtime.homeEnvVar}="${status.homeDir}"`);
174
178
  if (desktop.display)
@@ -123,8 +123,8 @@ export async function printToolPythonRuntimeDoctor(name, spec) {
123
123
  if (name === 'browser-use' && process.platform === 'linux' && !hasDesktopDisplay(detectDesktopEnv())) {
124
124
  printLinuxVirtualDisplayHint(' ');
125
125
  }
126
- if (existsSync(paths.executablePath)) {
127
- const doctor = await runBuffered(paths.executablePath, ['doctor'], getToolPythonRuntimeEnv(paths, spec));
126
+ if (existsSync(paths.executablePath) && spec.doctorArgs?.length) {
127
+ const doctor = await runBuffered(paths.executablePath, spec.doctorArgs, getToolPythonRuntimeEnv(paths, spec));
128
128
  console.log(` tool doctor: ${doctor.ok ? 'ok' : 'failed'}`);
129
129
  if (doctor.output) {
130
130
  console.log(doctor.output.split('\n').map((line) => ` ${line}`).join('\n'));