neoctl 0.2.14 → 0.2.16

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.
@@ -52,17 +52,19 @@ interface UiLine {
52
52
  title?: string;
53
53
  bodyTitle?: string;
54
54
  titleStatus?: "success" | "failure";
55
+ toolKind?: string;
55
56
  format?: "markdown" | "ansi" | "plain" | "diff";
56
57
  previewStyle?: "summary";
57
58
  summaryMaxLines?: number;
58
59
  live?: boolean;
59
60
  collapsible?: boolean;
60
61
  image?: UiLineImage;
62
+ artifact?: unknown;
61
63
  }
62
64
  interface UiActiveTool {
63
65
  id: string;
64
66
  name: string;
65
- input: unknown;
67
+ kind: string;
66
68
  startedAt: number;
67
69
  }
68
70
  interface UiStatus {
@@ -96,6 +98,10 @@ export interface CreateWebRuntimeOptions {
96
98
  cwd?: string;
97
99
  /** Additional tools to register in the web runtime. */
98
100
  externalTools?: readonly Tool[];
101
+ /** Additional shared skill roots. Defaults can also be provided with NEO_SKILL_ROOTS. */
102
+ skillRoots?: readonly string[];
103
+ /** Writable root for skill_create/skill_update. Defaults to NEO_SKILL_CREATE_ROOT or ~/.neoctl/skills. */
104
+ skillCreateRoot?: string;
99
105
  }
100
106
  export interface WebRuntimeScope {
101
107
  /** Browser-tab or client-instance identifier. Omit for the legacy singleton runtime. */
@@ -271,6 +277,7 @@ export declare class WebRepl {
271
277
  private handleCommandOrPrompt;
272
278
  private runCompaction;
273
279
  private send;
280
+ private sendSerialized;
274
281
  private broadcastSync;
275
282
  }
276
283
  export {};
package/dist/web/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import http from "node:http";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { existsSync, readFileSync } from "node:fs";
5
+ import { createReadStream, existsSync, readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createRequire } from "node:module";
8
8
  import { QueryEngine } from "../core/query-engine.js";
@@ -28,9 +28,14 @@ import { createTaskTools } from "../tasks/task-tools.js";
28
28
  import { TaskStore } from "../tasks/task-store.js";
29
29
  import { parseReplCommand, helpText, replCommandDefinitions } from "../repl/commands.js";
30
30
  import { writeSessionMarkdownExport } from "../session/session-export.js";
31
+ import { DefaultContextManager } from "../context/context-manager.js";
32
+ import { buildEffectiveSystemPrompt } from "../context/prompts.js";
31
33
  import { WEB_HTML } from "./html.js";
32
34
  import { openDirectory } from "../open-directory.js";
33
- import { resolveImageBlockDataSync } from "../core/image-storage.js";
35
+ import { getNeoctlHome } from "../paths.js";
36
+ import { FileSystemSkillCatalog } from "../skills/skill-filesystem.js";
37
+ import { createSkillTool } from "../skills/skill-tool.js";
38
+ import { createSkillManagementTools } from "../skills/skill-management-tools.js";
34
39
  const require = createRequire(import.meta.url);
35
40
  const markedPackageDir = path.dirname(require.resolve("marked/package.json"));
36
41
  const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-assets/package.json"));
@@ -109,6 +114,85 @@ function parseWebArgs(argv) {
109
114
  port = 3000;
110
115
  return { host, port: Math.round(port) };
111
116
  }
117
+ class SkillCatalogContextManager {
118
+ catalog;
119
+ base;
120
+ constructor(catalog, base = new DefaultContextManager()) {
121
+ this.catalog = catalog;
122
+ this.base = base;
123
+ }
124
+ async build(input) {
125
+ const runtimeContext = await this.base.build(input);
126
+ const skillSection = await buildSkillCatalogPromptSection(this.catalog);
127
+ if (!skillSection)
128
+ return runtimeContext;
129
+ const promptSections = [...runtimeContext.promptSections, skillSection];
130
+ return {
131
+ ...runtimeContext,
132
+ promptSections,
133
+ systemPrompt: buildEffectiveSystemPrompt(promptSections, input),
134
+ };
135
+ }
136
+ }
137
+ async function buildSkillCatalogPromptSection(catalog) {
138
+ const skills = await catalog.list();
139
+ if (skills.length === 0)
140
+ return undefined;
141
+ const visible = skills.slice(0, 80);
142
+ const lines = visible.map((skill) => {
143
+ const tags = skill.tags?.length ? `; tags=${skill.tags.join(",")}` : "";
144
+ const tools = skill.allowedTools?.length ? `; allowedTools=${skill.allowedTools.join(",")}` : "";
145
+ return `- ${skill.name}: ${skill.description} (execution=${skill.execution}${tags}${tools})`;
146
+ });
147
+ if (skills.length > visible.length)
148
+ lines.push(`- ... ${skills.length - visible.length} more skills available; use skill_list for the full catalog.`);
149
+ return {
150
+ name: "Available Skills",
151
+ cacheStable: false,
152
+ content: [
153
+ "Reusable skills are available through the `skill` tool and the /skill UI command.",
154
+ "When the user's task matches a skill name, description, tags, or domain capability, proactively call the `skill` tool before doing the work directly.",
155
+ "Do not wait for the user to explicitly say 'use skill'. Use skill_list/skill_read if you need to inspect details.",
156
+ "Available skill catalog:",
157
+ ...lines,
158
+ ].join("\n"),
159
+ };
160
+ }
161
+ function resolveSkillCatalogRoots(cwd, extraRoots = [], createRoot) {
162
+ const userRoot = path.resolve(process.env.NEO_SKILL_CREATE_ROOT || createRoot || path.join(getNeoctlHome(), "skills"));
163
+ const configuredRoots = splitPathList(process.env.NEO_SKILL_ROOTS);
164
+ const workspaceRoot = path.resolve(cwd, ".neo", "skills");
165
+ const roots = uniquePaths([
166
+ userRoot,
167
+ ...extraRoots,
168
+ ...configuredRoots,
169
+ workspaceRoot,
170
+ ]).map((root) => ({
171
+ root,
172
+ kind: path.resolve(root) === userRoot ? "user" : "workspace",
173
+ }));
174
+ return { roots, createRoot: userRoot };
175
+ }
176
+ function splitPathList(value) {
177
+ return String(value || "")
178
+ .split(path.delimiter)
179
+ .map((item) => item.trim())
180
+ .filter(Boolean)
181
+ .map((item) => path.resolve(item));
182
+ }
183
+ function uniquePaths(values) {
184
+ const seen = new Set();
185
+ const result = [];
186
+ for (const value of values) {
187
+ const resolved = path.resolve(value);
188
+ const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
189
+ if (seen.has(key))
190
+ continue;
191
+ seen.add(key);
192
+ result.push(resolved);
193
+ }
194
+ return result;
195
+ }
112
196
  export async function createWebRuntime(options = {}) {
113
197
  const envLoad = loadDefaultDotEnvFiles({ override: true });
114
198
  const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
@@ -117,6 +201,11 @@ export async function createWebRuntime(options = {}) {
117
201
  const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
118
202
  const taskStore = new TaskStore();
119
203
  const tools = new ToolRegistry();
204
+ const { roots: skillRoots, createRoot: skillCreateRoot } = resolveSkillCatalogRoots(cwd, options.skillRoots, options.skillCreateRoot);
205
+ const skills = new FileSystemSkillCatalog({
206
+ roots: skillRoots,
207
+ createRoot: skillCreateRoot,
208
+ });
120
209
  tools.register(editTool);
121
210
  tools.register(writeTool);
122
211
  tools.register(createExecTool({ taskStore }));
@@ -129,6 +218,9 @@ export async function createWebRuntime(options = {}) {
129
218
  if (modelConfig?.provider === "openai")
130
219
  tools.register(createOpenAIImageGenerationTool());
131
220
  tools.register(planTool);
221
+ tools.register(createSkillTool(skills));
222
+ for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
223
+ tools.register(tool);
132
224
  for (const tool of options.externalTools ?? [])
133
225
  tools.register(tool);
134
226
  const agentRuntime = { modelGateway, tools, taskStore };
@@ -155,6 +247,8 @@ export async function createWebRuntime(options = {}) {
155
247
  modelGateway,
156
248
  tools,
157
249
  appPromptStore,
250
+ contextManager: new SkillCatalogContextManager(skills),
251
+ skills: (await skills.list()).map((skill) => skill.name),
158
252
  taskNotificationSource: createTaskNotificationSource(taskStore),
159
253
  commands: replCommandDefinitions.map((command) => command.usage),
160
254
  session: {
@@ -266,6 +360,7 @@ export class WebRepl {
266
360
  queuedAttachments;
267
361
  foregroundRun;
268
362
  foregroundRunToken = 0;
363
+ pendingUserImageEchoLabels;
269
364
  backgroundSessionRuns = new Map();
270
365
  suppressReattachedStreaming = new Set();
271
366
  backgroundTaskCount;
@@ -508,7 +603,7 @@ export class WebRepl {
508
603
  this.broadcastSync();
509
604
  }
510
605
  setStatus(next) {
511
- this.status = next;
606
+ this.status = next.phase === "running_tools" ? next : { ...next, currentTool: undefined };
512
607
  this.broadcastSync();
513
608
  }
514
609
  finalizeForegroundView() {
@@ -611,6 +706,11 @@ export class WebRepl {
611
706
  }
612
707
  handleEvent(event) {
613
708
  this.reduce(event);
709
+ if (event.type === "message" && this.matchesPendingUserImageEcho(event.message)) {
710
+ this.pendingUserImageEchoLabels = undefined;
711
+ this.broadcastSync();
712
+ return;
713
+ }
614
714
  if (event.type === "state" || event.type === "context.metrics" || event.type === "usage" || event.type === "tool_call.delta" || event.type === "retrying") {
615
715
  this.broadcastSync();
616
716
  return;
@@ -695,6 +795,15 @@ export class WebRepl {
695
795
  if (event.type === "error")
696
796
  this.append({ kind: "error", text: event.error.message });
697
797
  }
798
+ matchesPendingUserImageEcho(message) {
799
+ const pending = this.pendingUserImageEchoLabels;
800
+ if (!pending?.length || message.role !== "user")
801
+ return false;
802
+ const labels = message.blocks
803
+ .filter((block) => block.type === "image" && typeof block.label === "string")
804
+ .map((block) => block.label);
805
+ return labels.length === pending.length && labels.every((label, index) => label === pending[index]);
806
+ }
698
807
  async handleCommandOrPrompt(text, attachments = []) {
699
808
  const command = parseReplCommand(text);
700
809
  if (command.type === "exit") {
@@ -794,6 +903,10 @@ export class WebRepl {
794
903
  this.append({ kind: "user", text: promptPayload.displayText });
795
904
  for (const line of imageLinesForBlocks("user", promptPayload.blocks))
796
905
  this.append(line);
906
+ const optimisticImageLabels = (promptPayload.blocks ?? [])
907
+ .filter((block) => block.type === "image" && typeof block.label === "string")
908
+ .map((block) => block.label);
909
+ this.pendingUserImageEchoLabels = optimisticImageLabels.length ? optimisticImageLabels : undefined;
797
910
  const runToken = ++this.foregroundRunToken;
798
911
  const abortController = new AbortController();
799
912
  this.activeAbortController = abortController;
@@ -833,6 +946,7 @@ export class WebRepl {
833
946
  this.activeAbortController = undefined;
834
947
  this.interruptArmed = false;
835
948
  this.finalizeForegroundView();
949
+ this.pendingUserImageEchoLabels = undefined;
836
950
  const queuedText = this.queuedInput;
837
951
  const queuedAttach = this.queuedAttachments;
838
952
  this.queuedInput = undefined;
@@ -885,10 +999,14 @@ export class WebRepl {
885
999
  res.write(`event: ${event}\n`);
886
1000
  res.write(`data: ${JSON.stringify(data)}\n\n`);
887
1001
  }
1002
+ sendSerialized(res, event, data) {
1003
+ res.write(`event: ${event}\n`);
1004
+ res.write(`data: ${data}\n\n`);
1005
+ }
888
1006
  broadcastSync() {
889
- const payload = this.snapshot(false);
1007
+ const payload = JSON.stringify(this.snapshot(false));
890
1008
  for (const res of this.subscribers)
891
- this.send(res, "sync", payload);
1009
+ this.sendSerialized(res, "sync", payload);
892
1010
  }
893
1011
  }
894
1012
  function reqKeepAlive(res) {
@@ -907,6 +1025,8 @@ async function route(req, res, router) {
907
1025
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
908
1026
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
909
1027
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
1028
+ if (req.method === "GET" && url.pathname === "/api/images")
1029
+ return sendImage(res, url.searchParams.get("path"), url.searchParams.get("mime"));
910
1030
  const scope = webRuntimeScopeFromUrl(url);
911
1031
  const repl = await router.get(scope);
912
1032
  if (req.method === "GET" && url.pathname === "/events")
@@ -970,6 +1090,23 @@ async function sendFile(res, filepath, contentType) {
970
1090
  res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "public, max-age=3600" });
971
1091
  res.end(body);
972
1092
  }
1093
+ async function sendImage(res, encodedPath, mimeType) {
1094
+ const filepath = decodeImagePath(encodedPath);
1095
+ if (!filepath)
1096
+ return sendJson(res, { error: "missing image path" }, 400);
1097
+ const contentType = safeImageContentType(mimeType);
1098
+ const binaryPath = filepath.endsWith(".base64.txt") ? filepath.slice(0, -".base64.txt".length) : filepath;
1099
+ if (existsSync(binaryPath)) {
1100
+ res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1101
+ createReadStream(binaryPath).pipe(res);
1102
+ return;
1103
+ }
1104
+ if (!existsSync(filepath))
1105
+ return sendJson(res, { error: "image not found" }, 404);
1106
+ const base64 = (await fs.readFile(filepath, "utf8")).trim();
1107
+ res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
1108
+ res.end(Buffer.from(stripDataUrlPrefix(base64), "base64"));
1109
+ }
973
1110
  function sendJson(res, value, status = 200) {
974
1111
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
975
1112
  res.end(JSON.stringify(value));
@@ -1074,7 +1211,7 @@ function reduceStatus(status, event) {
1074
1211
  if (event.type === "state")
1075
1212
  return { ...status, phase: event.phase, detail: event.detail, currentTool: event.phase === "preparing" || event.phase === "calling_model" || event.phase === "ready" ? undefined : status.currentTool, usage: event.phase === "preparing" ? undefined : status.usage, streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens, inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt, outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt, retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil, activityTick: status.activityTick + 1 };
1076
1213
  if (event.type === "tool.started")
1077
- return { ...status, phase: "running_tools", detail: event.toolUse.name, currentTool: { id: event.toolUse.id, name: event.toolUse.name, input: event.toolUse.input, startedAt: Date.now() }, activityTick: status.activityTick + 1 };
1214
+ return { ...status, phase: "running_tools", detail: event.toolUse.name, currentTool: { id: event.toolUse.id, name: event.toolUse.name, kind: toolKindForToolUse(event.toolUse.name, event.toolUse.input), startedAt: Date.now() }, activityTick: status.activityTick + 1 };
1078
1215
  if (event.type === "tool.finished")
1079
1216
  return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
1080
1217
  if (event.type === "context.metrics")
@@ -1095,6 +1232,21 @@ function reduceStatus(status, event) {
1095
1232
  return { ...status, activityTick: status.activityTick + 1 };
1096
1233
  return status;
1097
1234
  }
1235
+ function toolKindForToolUse(toolName, input) {
1236
+ if (toolName === "image2")
1237
+ return isRecord(input) && input.mode === "edit" ? "修图" : "作图";
1238
+ if (toolName === "edit" || toolName === "write" || toolName.includes("artifact_editor") || toolName.includes("apply_patch"))
1239
+ return "编辑";
1240
+ if (toolName === "exec" || toolName.includes("shell") || toolName.includes("command"))
1241
+ return "执行";
1242
+ if (toolName.includes("download"))
1243
+ return "下载";
1244
+ if (toolName === "read" || toolName === "list" || toolName === "grep" || toolName === "search" || toolName.includes("query") || toolName.includes("load"))
1245
+ return "查询";
1246
+ if (toolName === "plan")
1247
+ return "计划";
1248
+ return "工具";
1249
+ }
1098
1250
  async function handleExportCommand(outputPath, runtime) {
1099
1251
  const snapshot = runtime.engine.snapshot();
1100
1252
  if (!snapshot.session)
@@ -1495,20 +1647,43 @@ function imageLineForBlock(role, block) {
1495
1647
  kind,
1496
1648
  text: block.label ?? `[image ${block.mimeType}]`,
1497
1649
  image: {
1498
- src: imageBlockToDataUrl(block),
1650
+ src: imageBlockToSrc(block),
1499
1651
  label: block.label,
1500
1652
  mimeType: block.mimeType,
1501
1653
  },
1502
1654
  };
1503
1655
  }
1504
- function imageBlockToDataUrl(block) {
1505
- const data = resolveImageBlockDataSync(block);
1656
+ function imageBlockToSrc(block) {
1657
+ if (block.storage?.path) {
1658
+ return `/api/images?path=${encodeImagePath(block.storage.path)}&mime=${encodeURIComponent(block.mimeType)}`;
1659
+ }
1660
+ const data = block.data.trim();
1506
1661
  if (!data)
1507
1662
  return "";
1508
1663
  if (data.startsWith("data:"))
1509
1664
  return data;
1510
1665
  return `data:${block.mimeType};base64,${data}`;
1511
1666
  }
1667
+ function encodeImagePath(filepath) {
1668
+ return Buffer.from(filepath, "utf8").toString("base64url");
1669
+ }
1670
+ function decodeImagePath(value) {
1671
+ if (!value)
1672
+ return undefined;
1673
+ try {
1674
+ return Buffer.from(value, "base64url").toString("utf8");
1675
+ }
1676
+ catch {
1677
+ return undefined;
1678
+ }
1679
+ }
1680
+ function safeImageContentType(value) {
1681
+ return value && /^image\/[a-z0-9.+-]+$/i.test(value) ? value : "application/octet-stream";
1682
+ }
1683
+ function stripDataUrlPrefix(value) {
1684
+ const match = /^data:[^;]+;base64,(.*)$/is.exec(value);
1685
+ return match ? match[1] : value;
1686
+ }
1512
1687
  function assistantText(message) {
1513
1688
  const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
1514
1689
  return text.length > 0 ? text : undefined;
@@ -1561,14 +1736,21 @@ function thinkingLine(text, live = false) {
1561
1736
  return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
1562
1737
  }
1563
1738
  function formatToolUse(toolUse) {
1739
+ const toolKind = toolKindForToolUse(toolUse.name, toolUse.input);
1564
1740
  if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input))
1565
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), text: formatPlanToolPayload(toolUse.input), collapsible: true };
1741
+ return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), toolKind, text: formatPlanToolPayload(toolUse.input), collapsible: true };
1566
1742
  const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
1567
- return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: description, text: formatReplData(toolUse.input, 1200), previewStyle: "summary", collapsible: true };
1743
+ return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: description, toolKind, text: formatReplData(toolUse.input, 1200), previewStyle: "summary", collapsible: true };
1568
1744
  }
1569
1745
  function formatToolResultLine(toolName, output, ok) {
1570
1746
  const formatted = formatToolResult(toolName, output, ok);
1571
- return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure", text: formatted.text, format: formatted.format, live: false, previewStyle: formatted.full ? undefined : "summary", summaryMaxLines: formatted.summaryMaxLines, collapsible: true };
1747
+ return { kind: ok ? "tool" : "error", title: toolTitle(toolName, "finished"), bodyTitle: formatted.bodyTitle, titleStatus: ok ? "success" : "failure", toolKind: toolKindForToolUse(toolName, undefined), text: formatted.text, format: formatted.format, live: false, previewStyle: formatted.full ? undefined : "summary", summaryMaxLines: formatted.summaryMaxLines, collapsible: true, artifact: xhsArtifactFromToolOutput(toolName, output) };
1748
+ }
1749
+ function xhsArtifactFromToolOutput(toolName, output) {
1750
+ if (toolName !== "open_xhs_artifact_editor" || !isRecord(output))
1751
+ return undefined;
1752
+ const artifact = output.artifact;
1753
+ return isRecord(artifact) && typeof artifact.id === "string" ? artifact : undefined;
1572
1754
  }
1573
1755
  function toolTitle(toolName, _phase) {
1574
1756
  return toolName;
@@ -1695,6 +1877,17 @@ function formatImageGenerationToolResult(output, ok) {
1695
1877
  const quality = typeof output.quality === "string" ? output.quality : undefined;
1696
1878
  const format = typeof output.outputFormat === "string" ? output.outputFormat : undefined;
1697
1879
  const sourceImages = typeof output.sourceImages === "number" ? output.sourceImages : undefined;
1880
+ const prompt = typeof output.prompt === "string" ? output.prompt.trim() : "";
1881
+ const semanticName = typeof output.semanticName === "string" ? output.semanticName.trim() : "";
1882
+ const background = typeof output.background === "string" ? output.background : undefined;
1883
+ const startedAtIso = typeof output.startedAtIso === "string" ? output.startedAtIso : undefined;
1884
+ const finishedAtIso = typeof output.finishedAtIso === "string" ? output.finishedAtIso : undefined;
1885
+ const revisedPrompt = Array.isArray(output.images)
1886
+ ? output.images
1887
+ .filter(isRecord)
1888
+ .map((image) => typeof image.revisedPrompt === "string" ? image.revisedPrompt.trim() : "")
1889
+ .find((value) => Boolean(value))
1890
+ : undefined;
1698
1891
  const lines = [`${mode === "edit" ? "edited" : "generated"} ${returnedImages ?? 0} image${returnedImages === 1 ? "" : "s"}`];
1699
1892
  const details = [provider, model, size, quality && quality !== "auto" ? quality : undefined, format].filter((value) => Boolean(value));
1700
1893
  if (details.length > 0)
@@ -1704,6 +1897,19 @@ function formatImageGenerationToolResult(output, ok) {
1704
1897
  const duration = imageGenerationDuration(output);
1705
1898
  if (duration !== undefined)
1706
1899
  lines.push(`duration: ${duration}ms`);
1900
+ if (prompt)
1901
+ lines.push(`prompt: ${prompt}`);
1902
+ if (revisedPrompt && revisedPrompt !== prompt)
1903
+ lines.push(`revisedPrompt: ${revisedPrompt}`);
1904
+ lines.push(`mode: ${mode}`);
1905
+ if (semanticName)
1906
+ lines.push(`semanticName: ${semanticName}`);
1907
+ if (background)
1908
+ lines.push(`background: ${background}`);
1909
+ if (startedAtIso)
1910
+ lines.push(`startedAt: ${startedAtIso}`);
1911
+ if (finishedAtIso)
1912
+ lines.push(`finishedAt: ${finishedAtIso}`);
1707
1913
  return lines.join("\n");
1708
1914
  }
1709
1915
  function imageGenerationDuration(output) {
@@ -1916,4 +2122,4 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
1916
2122
  process.exitCode = 1;
1917
2123
  });
1918
2124
  }
1919
- //# sourceMappingURL=index.js.map
2125
+ //# sourceMappingURL=index.js.map