neoctl 0.2.13 → 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.
@@ -231,6 +231,40 @@ function createTaskNotificationSource(taskStore) {
231
231
  },
232
232
  };
233
233
  }
234
+ function resolveSkillCatalogRoots(cwd) {
235
+ const userRoot = path.resolve(process.env.NEO_SKILL_CREATE_ROOT || path.join(getNeoctlHome(), "skills"));
236
+ const configuredRoots = splitPathList(process.env.NEO_SKILL_ROOTS);
237
+ const workspaceRoot = path.resolve(cwd, ".neo", "skills");
238
+ const roots = uniquePaths([
239
+ userRoot,
240
+ ...configuredRoots,
241
+ workspaceRoot,
242
+ ]).map((root) => ({
243
+ root,
244
+ kind: path.resolve(root) === userRoot ? "user" : "workspace",
245
+ }));
246
+ return { roots, createRoot: userRoot };
247
+ }
248
+ function splitPathList(value) {
249
+ return String(value || "")
250
+ .split(path.delimiter)
251
+ .map((item) => item.trim())
252
+ .filter(Boolean)
253
+ .map((item) => path.resolve(item));
254
+ }
255
+ function uniquePaths(values) {
256
+ const seen = new Set();
257
+ const result = [];
258
+ for (const value of values) {
259
+ const resolved = path.resolve(value);
260
+ const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
261
+ if (seen.has(key))
262
+ continue;
263
+ seen.add(key);
264
+ result.push(resolved);
265
+ }
266
+ return result;
267
+ }
234
268
  async function createRuntime() {
235
269
  const envLoad = loadDefaultDotEnvFiles({ override: true });
236
270
  const modelConfig = readModelProviderConfig(process.env);
@@ -242,12 +276,9 @@ async function createRuntime() {
242
276
  const secretStore = await SecretStore.open();
243
277
  const secretRedactions = new InMemorySecretRedactionRegistry();
244
278
  const tools = new ToolRegistry();
245
- const skillWorkspaceRoot = path.resolve(process.cwd(), ".neo", "skills");
279
+ const { roots: skillRoots, createRoot: skillWorkspaceRoot } = resolveSkillCatalogRoots(process.cwd());
246
280
  const skills = new FileSystemSkillCatalog({
247
- roots: [
248
- { root: skillWorkspaceRoot, kind: "workspace" },
249
- { root: path.resolve(getNeoctlHome(), "skills"), kind: "user" },
250
- ],
281
+ roots: skillRoots,
251
282
  createRoot: skillWorkspaceRoot,
252
283
  });
253
284
  tools.register(editTool);
@@ -260,7 +291,7 @@ async function createRuntime() {
260
291
  tools.register(createLoadImageTool());
261
292
  tools.register(createImageNoteTool());
262
293
  if (modelConfig?.provider === "openai")
263
- tools.register(createOpenAIImageGenerationTool());
294
+ tools.register(createOpenAIImageGenerationTool({ taskStore, foregroundDetachRegistry: foregroundExecDetach }));
264
295
  tools.register(planTool);
265
296
  for (const tool of createSecretTools())
266
297
  tools.register(tool);
@@ -330,7 +361,7 @@ async function createRuntime() {
330
361
  function syncImageGenerationTool(runtime, provider) {
331
362
  runtime.tools.unregister("image2");
332
363
  if (provider === "openai")
333
- runtime.tools.register(createOpenAIImageGenerationTool());
364
+ runtime.tools.register(createOpenAIImageGenerationTool({ taskStore: runtime.taskStore, foregroundDetachRegistry: runtime.foregroundExecDetach }));
334
365
  }
335
366
  function formatCreatedEnvNotice(path) {
336
367
  return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (OPENAI_API_KEY or ANTHROPIC_API_KEY), then restart neo.`;
@@ -1352,7 +1383,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1352
1383
  if (key.ctrl && value.toLowerCase() === "b") {
1353
1384
  const result = runtime.foregroundExecDetach.detachCurrent();
1354
1385
  append(result.ok
1355
- ? systemLine(`Detached foreground exec to background task ${result.taskId ?? "unknown"}.`)
1386
+ ? systemLine(`Detached foreground task to background task ${result.taskId ?? "unknown"}.`)
1356
1387
  : systemLine(result.message));
1357
1388
  return;
1358
1389
  }
@@ -1656,7 +1687,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1656
1687
  return;
1657
1688
  }
1658
1689
  });
1659
- return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: visibleDynamicLines, width, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, skillsBrowser ? e(SkillsBrowser, { state: skillsBrowser, width }) : null, secretsBrowser ? e(SecretsBrowser, { state: secretsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), showForegroundExecDetachHint && foregroundExecDetachHandle ? e(ForegroundExecDetachHintLine, { handle: foregroundExecDetachHandle, width }) : null, agentActivities.length > 0 ? e(SubagentLivePanel, { activities: agentActivities, width, terminalRows: terminalSize.rows, compact: compactLiveLayout, animationTick }) : null, agentActivities.length === 0 && backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, agentActivities.length > 0 && nonAgentBackgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: nonAgentBackgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1690
+ return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: visibleDynamicLines, width, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, skillsBrowser ? e(SkillsBrowser, { state: skillsBrowser, width }) : null, secretsBrowser ? e(SecretsBrowser, { state: secretsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), showForegroundExecDetachHint && foregroundExecDetachHandle ? e(ForegroundExecDetachHintLine, { handle: foregroundExecDetachHandle, width }) : null, agentActivities.length > 0 ? e(SubagentLivePanel, { activities: agentActivities, width, animationTick }) : null, agentActivities.length === 0 && backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, agentActivities.length > 0 && nonAgentBackgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: nonAgentBackgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1660
1691
  }
1661
1692
  const MessageList = React.memo(function MessageList({ lines, width, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1662
1693
  const contentWidth = messageContentWidth(width);
@@ -1994,37 +2025,26 @@ function backgroundTaskStatusRenderRows(taskCount) {
1994
2025
  }
1995
2026
  function ForegroundExecDetachHintLine({ handle, width: terminalWidth }) {
1996
2027
  const width = statusBarWidth(terminalWidth);
2028
+ const toolName = handle.toolName?.trim() || "exec";
1997
2029
  const label = handle.description?.trim() || handle.command;
1998
- const text = `↳ exec still running · Ctrl+B to detach · ${truncateMiddle(label, Math.max(12, width - 38))}`;
2030
+ const text = `↳ ${toolName} still running · Ctrl+B to detach · ${truncateMiddle(label, Math.max(12, width - toolName.length - 33))}`;
1999
2031
  return e(Text, { color: "yellow" }, fitToWidth(text, width));
2000
2032
  }
2001
- function SubagentLivePanel({ activities, width: terminalWidth, terminalRows, compact, animationTick }) {
2033
+ function SubagentLivePanel({ activities, width: terminalWidth, animationTick }) {
2002
2034
  const width = statusBarWidth(terminalWidth);
2003
- const rows = subagentLivePanelRenderRows(activities, terminalRows, compact);
2004
- if (rows <= 0)
2005
- return null;
2006
2035
  const sorted = sortAgentActivitiesForPanel(activities);
2007
2036
  const selected = sorted[0];
2008
2037
  if (!selected)
2009
2038
  return null;
2010
2039
  const activeCount = activities.filter((activity) => activity.status === "running" || activity.status === "pending").length;
2011
- const header = `◆ subagents: ${activeCount} active${activities.length > activeCount ? ` · ${activities.length - activeCount} recent` : ""}`;
2012
- if (rows <= 1) {
2013
- return e(Text, { color: "yellow" }, fitToWidth(`${header} · ${compactAgentSummary(selected, width - header.length - 3)}`, width));
2014
- }
2015
- const detailLines = buildSubagentDetailLines(selected, sorted, animationTick);
2016
- return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(header, width)), ...detailLines.map((line, index) => e(Text, {
2017
- key: `agent-detail-${selected.agentId}-${index}`,
2018
- color: line.color,
2019
- }, fitToWidth(line.text, width))));
2020
- }
2021
- const SUBAGENT_DETAIL_ROWS = 3;
2022
- function subagentLivePanelRenderRows(activities, terminalRows, compact = false) {
2023
- if (activities.length === 0)
2024
- return 0;
2025
- if (compact || terminalRows < 22 || activities.length > 1)
2026
- return 1;
2027
- return 1 + SUBAGENT_DETAIL_ROWS;
2040
+ const recentCount = Math.max(0, activities.length - activeCount);
2041
+ const countText = activeCount > 1
2042
+ ? `${activeCount} active${recentCount ? ` · ${recentCount} recent` : ""}`
2043
+ : recentCount && activeCount === 0
2044
+ ? `${recentCount} recent`
2045
+ : "";
2046
+ const text = `↳ subagent ${subagentStatusText(selected.status, animationTick)}${countText ? ` · ${countText}` : ""} · ${compactAgentSummary(selected, width)}`;
2047
+ return e(Text, { color: statusColor(selected.status) }, fitToWidth(text, width));
2028
2048
  }
2029
2049
  function sortAgentActivitiesForPanel(activities) {
2030
2050
  const rank = (status) => {
@@ -2038,76 +2058,35 @@ function sortAgentActivitiesForPanel(activities) {
2038
2058
  };
2039
2059
  return [...activities].sort((left, right) => rank(left.status) - rank(right.status) || right.updatedAt.localeCompare(left.updatedAt));
2040
2060
  }
2041
- function buildSubagentDetailLines(selected, sorted, animationTick) {
2042
- const spinner = selected.status === "running" ? spinnerFrame(animationTick) : statusGlyph(selected.status);
2043
- const elapsed = formatElapsed(Date.now() - new Date(selected.startedAt).getTime());
2044
- const headerLine = `${spinner} ${selected.description || selected.agentId} · ${elapsed}`;
2045
- const currentLine = selected.currentTool
2046
- ? `→ ${selected.currentTool.name}${selected.currentTool.inputPreview ? ` · ${selected.currentTool.inputPreview}` : ""}`
2047
- : selected.error
2048
- ? `✖ ${selected.error}`
2049
- : selected.resultPreview
2050
- ? `✓ ${selected.resultPreview}`
2051
- : selected.lastText
2052
- ? `• ${selected.lastText}`
2053
- : `• ${selected.prompt}`;
2054
- const recent = selected.timeline.slice(-2).map((entry) => `${timelinePrefix(entry)} ${formatTimelineEntry(entry, 240)}`);
2055
- const otherRunning = sorted
2056
- .filter((activity) => activity.agentId !== selected.agentId && (activity.status === "running" || activity.status === "pending"))
2057
- .slice(0, 2)
2058
- .map((activity) => compactAgentSummary(activity, 180));
2059
- const tail = [...recent, ...otherRunning.map((summary) => `· ${summary}`)].find((line) => line.trim()) ?? `tools:${selected.totalToolUseCount}`;
2060
- return [
2061
- { text: headerLine, color: statusColor(selected.status) },
2062
- { text: currentLine, color: selected.error ? "red" : selected.currentTool ? "#d4b04c" : "yellow" },
2063
- { text: tail, color: "gray" },
2064
- ];
2065
- }
2066
2061
  function compactAgentSummary(activity, maxLength) {
2067
2062
  const current = activity.currentTool
2068
2063
  ? `${activity.currentTool.name}${activity.currentTool.inputPreview ? ` ${activity.currentTool.inputPreview}` : ""}`
2069
- : activity.lastText ?? activity.resultPreview ?? activity.error ?? activity.prompt;
2064
+ : firstSafeSubagentPreview(activity.resultPreview, activity.error, activity.lastText, activity.prompt);
2070
2065
  const elapsed = formatElapsed(Date.now() - new Date(activity.startedAt).getTime());
2071
2066
  return truncateMiddle(`${activity.description || activity.agentId} · ${elapsed} · tools:${activity.totalToolUseCount} · ${current.replace(/\s+/g, " ")}`, Math.max(8, maxLength));
2072
2067
  }
2073
- function formatTimelineEntry(entry, maxLength) {
2074
- const detail = entry.detail ? ` · ${entry.detail.replace(/\s+/g, " ")}` : "";
2075
- return truncateMiddle(`${entry.title}${detail}`, Math.max(8, maxLength));
2076
- }
2077
- function timelinePrefix(entry) {
2078
- if (entry.kind === "tool_start")
2079
- return "";
2080
- if (entry.kind === "tool_result")
2081
- return entry.status === "failed" ? "✖" : "←";
2082
- if (entry.kind === "thinking")
2083
- return "◆";
2084
- if (entry.kind === "error")
2085
- return "✖";
2086
- if (entry.kind === "status")
2087
- return "•";
2088
- return "assistant:";
2089
- }
2090
- function timelineColor(entry) {
2091
- if (entry.status === "failed" || entry.kind === "error")
2092
- return "red";
2093
- if (entry.kind === "tool_start" || entry.kind === "tool_result")
2094
- return "#d4b04c";
2095
- if (entry.kind === "thinking")
2096
- return THINKING_COLOR;
2097
- if (entry.kind === "status")
2098
- return "gray";
2099
- return "green";
2068
+ function firstSafeSubagentPreview(...values) {
2069
+ return values.find((value) => value && !isInternalContinuationPreview(value)) ?? "working";
2070
+ }
2071
+ function isInternalContinuationPreview(value) {
2072
+ const normalized = value.toLowerCase();
2073
+ return normalized.includes("<compact_state>")
2074
+ || normalized.includes("internal continuation state")
2075
+ || normalized.includes("auto compact")
2076
+ || normalized.includes("conversation summary generated by compact");
2100
2077
  }
2101
- function statusGlyph(status) {
2078
+ function subagentStatusText(status, animationTick) {
2079
+ if (status === "running")
2080
+ return `still running ${spinnerFrame(animationTick)}`;
2081
+ if (status === "pending")
2082
+ return "pending";
2102
2083
  if (status === "completed")
2103
- return "";
2084
+ return "completed";
2104
2085
  if (status === "failed")
2105
- return "";
2086
+ return "failed";
2106
2087
  if (status === "killed")
2107
- return "";
2108
- if (status === "pending")
2109
- return "…";
2110
- return "●";
2088
+ return "killed";
2089
+ return status;
2111
2090
  }
2112
2091
  function statusColor(status) {
2113
2092
  if (status === "completed")