neoctl 0.2.14 → 0.2.15
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/repl/index.js +36 -5
- package/dist/repl/index.js.map +1 -1
- package/dist/web/index.d.ts +8 -1
- package/dist/web/index.js +174 -12
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
package/dist/web/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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: {
|
|
@@ -508,7 +602,7 @@ export class WebRepl {
|
|
|
508
602
|
this.broadcastSync();
|
|
509
603
|
}
|
|
510
604
|
setStatus(next) {
|
|
511
|
-
this.status = next;
|
|
605
|
+
this.status = next.phase === "running_tools" ? next : { ...next, currentTool: undefined };
|
|
512
606
|
this.broadcastSync();
|
|
513
607
|
}
|
|
514
608
|
finalizeForegroundView() {
|
|
@@ -885,10 +979,14 @@ export class WebRepl {
|
|
|
885
979
|
res.write(`event: ${event}\n`);
|
|
886
980
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
887
981
|
}
|
|
982
|
+
sendSerialized(res, event, data) {
|
|
983
|
+
res.write(`event: ${event}\n`);
|
|
984
|
+
res.write(`data: ${data}\n\n`);
|
|
985
|
+
}
|
|
888
986
|
broadcastSync() {
|
|
889
|
-
const payload = this.snapshot(false);
|
|
987
|
+
const payload = JSON.stringify(this.snapshot(false));
|
|
890
988
|
for (const res of this.subscribers)
|
|
891
|
-
this.
|
|
989
|
+
this.sendSerialized(res, "sync", payload);
|
|
892
990
|
}
|
|
893
991
|
}
|
|
894
992
|
function reqKeepAlive(res) {
|
|
@@ -907,6 +1005,8 @@ async function route(req, res, router) {
|
|
|
907
1005
|
return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
|
|
908
1006
|
if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
|
|
909
1007
|
return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
|
|
1008
|
+
if (req.method === "GET" && url.pathname === "/api/images")
|
|
1009
|
+
return sendImage(res, url.searchParams.get("path"), url.searchParams.get("mime"));
|
|
910
1010
|
const scope = webRuntimeScopeFromUrl(url);
|
|
911
1011
|
const repl = await router.get(scope);
|
|
912
1012
|
if (req.method === "GET" && url.pathname === "/events")
|
|
@@ -970,6 +1070,23 @@ async function sendFile(res, filepath, contentType) {
|
|
|
970
1070
|
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "public, max-age=3600" });
|
|
971
1071
|
res.end(body);
|
|
972
1072
|
}
|
|
1073
|
+
async function sendImage(res, encodedPath, mimeType) {
|
|
1074
|
+
const filepath = decodeImagePath(encodedPath);
|
|
1075
|
+
if (!filepath)
|
|
1076
|
+
return sendJson(res, { error: "missing image path" }, 400);
|
|
1077
|
+
const contentType = safeImageContentType(mimeType);
|
|
1078
|
+
const binaryPath = filepath.endsWith(".base64.txt") ? filepath.slice(0, -".base64.txt".length) : filepath;
|
|
1079
|
+
if (existsSync(binaryPath)) {
|
|
1080
|
+
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
|
|
1081
|
+
createReadStream(binaryPath).pipe(res);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (!existsSync(filepath))
|
|
1085
|
+
return sendJson(res, { error: "image not found" }, 404);
|
|
1086
|
+
const base64 = (await fs.readFile(filepath, "utf8")).trim();
|
|
1087
|
+
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
|
|
1088
|
+
res.end(Buffer.from(stripDataUrlPrefix(base64), "base64"));
|
|
1089
|
+
}
|
|
973
1090
|
function sendJson(res, value, status = 200) {
|
|
974
1091
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
|
|
975
1092
|
res.end(JSON.stringify(value));
|
|
@@ -1074,7 +1191,7 @@ function reduceStatus(status, event) {
|
|
|
1074
1191
|
if (event.type === "state")
|
|
1075
1192
|
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
1193
|
if (event.type === "tool.started")
|
|
1077
|
-
return { ...status, phase: "running_tools", detail: event.toolUse.name, currentTool: { id: event.toolUse.id, name: event.toolUse.name,
|
|
1194
|
+
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
1195
|
if (event.type === "tool.finished")
|
|
1079
1196
|
return { ...status, currentTool: status.currentTool?.id === event.toolUse.id ? undefined : status.currentTool, activityTick: status.activityTick + 1 };
|
|
1080
1197
|
if (event.type === "context.metrics")
|
|
@@ -1095,6 +1212,21 @@ function reduceStatus(status, event) {
|
|
|
1095
1212
|
return { ...status, activityTick: status.activityTick + 1 };
|
|
1096
1213
|
return status;
|
|
1097
1214
|
}
|
|
1215
|
+
function toolKindForToolUse(toolName, input) {
|
|
1216
|
+
if (toolName === "image2")
|
|
1217
|
+
return isRecord(input) && input.mode === "edit" ? "修图" : "作图";
|
|
1218
|
+
if (toolName === "edit" || toolName === "write" || toolName.includes("artifact_editor") || toolName.includes("apply_patch"))
|
|
1219
|
+
return "编辑";
|
|
1220
|
+
if (toolName === "exec" || toolName.includes("shell") || toolName.includes("command"))
|
|
1221
|
+
return "执行";
|
|
1222
|
+
if (toolName.includes("download"))
|
|
1223
|
+
return "下载";
|
|
1224
|
+
if (toolName === "read" || toolName === "list" || toolName === "grep" || toolName === "search" || toolName.includes("query") || toolName.includes("load"))
|
|
1225
|
+
return "查询";
|
|
1226
|
+
if (toolName === "plan")
|
|
1227
|
+
return "计划";
|
|
1228
|
+
return "工具";
|
|
1229
|
+
}
|
|
1098
1230
|
async function handleExportCommand(outputPath, runtime) {
|
|
1099
1231
|
const snapshot = runtime.engine.snapshot();
|
|
1100
1232
|
if (!snapshot.session)
|
|
@@ -1495,20 +1627,43 @@ function imageLineForBlock(role, block) {
|
|
|
1495
1627
|
kind,
|
|
1496
1628
|
text: block.label ?? `[image ${block.mimeType}]`,
|
|
1497
1629
|
image: {
|
|
1498
|
-
src:
|
|
1630
|
+
src: imageBlockToSrc(block),
|
|
1499
1631
|
label: block.label,
|
|
1500
1632
|
mimeType: block.mimeType,
|
|
1501
1633
|
},
|
|
1502
1634
|
};
|
|
1503
1635
|
}
|
|
1504
|
-
function
|
|
1505
|
-
|
|
1636
|
+
function imageBlockToSrc(block) {
|
|
1637
|
+
if (block.storage?.path) {
|
|
1638
|
+
return `/api/images?path=${encodeImagePath(block.storage.path)}&mime=${encodeURIComponent(block.mimeType)}`;
|
|
1639
|
+
}
|
|
1640
|
+
const data = block.data.trim();
|
|
1506
1641
|
if (!data)
|
|
1507
1642
|
return "";
|
|
1508
1643
|
if (data.startsWith("data:"))
|
|
1509
1644
|
return data;
|
|
1510
1645
|
return `data:${block.mimeType};base64,${data}`;
|
|
1511
1646
|
}
|
|
1647
|
+
function encodeImagePath(filepath) {
|
|
1648
|
+
return Buffer.from(filepath, "utf8").toString("base64url");
|
|
1649
|
+
}
|
|
1650
|
+
function decodeImagePath(value) {
|
|
1651
|
+
if (!value)
|
|
1652
|
+
return undefined;
|
|
1653
|
+
try {
|
|
1654
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
1655
|
+
}
|
|
1656
|
+
catch {
|
|
1657
|
+
return undefined;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function safeImageContentType(value) {
|
|
1661
|
+
return value && /^image\/[a-z0-9.+-]+$/i.test(value) ? value : "application/octet-stream";
|
|
1662
|
+
}
|
|
1663
|
+
function stripDataUrlPrefix(value) {
|
|
1664
|
+
const match = /^data:[^;]+;base64,(.*)$/is.exec(value);
|
|
1665
|
+
return match ? match[1] : value;
|
|
1666
|
+
}
|
|
1512
1667
|
function assistantText(message) {
|
|
1513
1668
|
const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1514
1669
|
return text.length > 0 ? text : undefined;
|
|
@@ -1561,14 +1716,21 @@ function thinkingLine(text, live = false) {
|
|
|
1561
1716
|
return { kind: "thinking", title: titleForKind("thinking"), text, previewStyle: "summary", summaryMaxLines: THINKING_SUMMARY_MAX_LINES, live };
|
|
1562
1717
|
}
|
|
1563
1718
|
function formatToolUse(toolUse) {
|
|
1719
|
+
const toolKind = toolKindForToolUse(toolUse.name, toolUse.input);
|
|
1564
1720
|
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 };
|
|
1721
|
+
return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: planToolBodyTitle(toolUse.input), toolKind, text: formatPlanToolPayload(toolUse.input), collapsible: true };
|
|
1566
1722
|
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 };
|
|
1723
|
+
return { kind: "tool", title: toolTitle(toolUse.name, "running"), bodyTitle: description, toolKind, text: formatReplData(toolUse.input, 1200), previewStyle: "summary", collapsible: true };
|
|
1568
1724
|
}
|
|
1569
1725
|
function formatToolResultLine(toolName, output, ok) {
|
|
1570
1726
|
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 };
|
|
1727
|
+
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) };
|
|
1728
|
+
}
|
|
1729
|
+
function xhsArtifactFromToolOutput(toolName, output) {
|
|
1730
|
+
if (toolName !== "open_xhs_artifact_editor" || !isRecord(output))
|
|
1731
|
+
return undefined;
|
|
1732
|
+
const artifact = output.artifact;
|
|
1733
|
+
return isRecord(artifact) && typeof artifact.id === "string" ? artifact : undefined;
|
|
1572
1734
|
}
|
|
1573
1735
|
function toolTitle(toolName, _phase) {
|
|
1574
1736
|
return toolName;
|