@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.
- package/dist/apps/chat/agent-profiles.js +4 -1
- package/dist/apps/chat/agent-store.js +196 -3
- package/dist/apps/chat/web-app.js +5 -4
- package/dist/apps/chat-ui/assets/{dist-ZB1-ui2y.js → dist-B6kzRv2_.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-C3PnEkhb.js → dist-Bns-O5iY.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BAXNalar.js → dist-CVcI9eYD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-vKlxFkTa.js → dist-CqkKCn6l.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DnACFKyO.js → dist-D4CEl91a.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CYPL-B2Z.js → dist-D9K0kHT0.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B2BEpL7n.js → dist-DWA7BIr3.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BwUvs6Ph.js → dist-Dj3uUYqF.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-C0zsJ8II.js → dist-DqaKPP5Y.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CiDSXgtg.js → dist-DuM9PL5k.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BAGS_xkV.js → dist-DvPgMX-r.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-0x7tuTNX.js → index-Baxj7Erc.js} +18 -17
- package/dist/apps/chat-ui/index.html +1 -1
- package/dist/apps/chat-vscode-web/assets/{index-C3GTPyDo.js → index-CjOC7zYy.js} +4 -4
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/inkMarkdown.js +8 -3
- package/dist/apps/cli-ui/inkSyntaxHighlighter.js +166 -0
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/{pibo-vscode-ext-1.4.4.vsix → pibo-vscode-ext-1.4.6.vsix} +0 -0
- package/dist/cli.js +20 -0
- package/dist/mcp/agent-context.js +10 -6
- package/dist/mcp/commands/info.js +19 -7
- package/dist/mcp/config.js +98 -65
- package/dist/plugins/builtin.js +15 -1
- package/dist/plugins/codex-compat.js +2 -0
- package/dist/session-ui/terminalRows.js +111 -10
- package/dist/shared/trace-nodes.js +12 -0
- package/dist/skills/cli.js +25 -1
- package/dist/tools/codex-image-generation.js +272 -0
- package/dist/tools/guides.js +71 -0
- package/dist/tools/index.js +7 -3
- package/dist/tools/python-runtime.js +2 -2
- package/dist/tools/registry.js +25 -1
- package/package.json +1 -1
- 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(
|
|
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
|
|
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: `${
|
|
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;
|
package/dist/skills/cli.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/tools/guides.js
CHANGED
|
@@ -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.',
|
package/dist/tools/index.js
CHANGED
|
@@ -166,9 +166,13 @@ function printEnv(name) {
|
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
const binDir = status.executablePath.replace(/\/[^/]+$/, '');
|
|
169
|
-
const wrapperPath = entry.name === '
|
|
170
|
-
|
|
171
|
-
|
|
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,
|
|
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'));
|