[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { TerminalArtifactCard } from './TerminalArtifactCard.tsx';
test('TerminalArtifactCard renders terminal context summary', () => {
const html = renderToStaticMarkup(
<TerminalArtifactCard
artifact={{
kind: 'terminal.context',
sessionId: 'session-1',
label: 'prod',
range: 'tail',
totalLines: 120,
startLine: 100,
endLine: 102,
returnedLines: 3,
hasMoreBefore: true,
hasMoreAfter: false,
source: 'live',
preview: 'alpha\nbeta\ngamma',
}}
/>,
);
assert.match(html, /prod/);
assert.match(html, /lines 101-103 \/ 120/);
assert.doesNotMatch(html, /alpha/);
});
test('TerminalArtifactCard renders terminal read errors', () => {
const html = renderToStaticMarkup(
<TerminalArtifactCard
artifact={{
kind: 'error',
message: 'Terminal context reader is unavailable.',
}}
/>,
);
assert.match(html, /Terminal read failed/);
assert.match(html, /Terminal context reader is unavailable/);
});

View File

@@ -0,0 +1,73 @@
import { AlertCircle, SquareTerminal } from 'lucide-react';
import React from 'react';
import { cn } from '../../../lib/utils';
import type { TerminalToolArtifact } from './terminalToolArtifact';
interface TerminalArtifactCardProps {
artifact: TerminalToolArtifact;
className?: string;
}
function formatLineRange(artifact: Extract<TerminalToolArtifact, { kind: 'terminal.context' }>): string {
if (artifact.returnedLines === 0) {
return `0 / ${artifact.totalLines} lines`;
}
return `lines ${artifact.startLine + 1}-${artifact.endLine + 1} / ${artifact.totalLines}`;
}
function formatSubtitle(artifact: Extract<TerminalToolArtifact, { kind: 'terminal.context' }>): string {
const parts = [
formatLineRange(artifact),
artifact.source,
artifact.hasMoreBefore || artifact.hasMoreAfter ? 'more available' : null,
].filter(Boolean);
return parts.join(' | ');
}
export const TerminalArtifactCard = React.forwardRef<HTMLDivElement, TerminalArtifactCardProps>(({
artifact,
className,
}, ref) => {
if (artifact.kind === 'error') {
return (
<div className={cn(
'flex w-full items-center gap-2.5 rounded-md border border-destructive/25 bg-destructive/5 px-2.5 py-2',
className,
)} ref={ref}>
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-destructive/10 text-destructive">
<AlertCircle size={15} />
</div>
<div className="min-w-0 flex-1 text-left">
<div className="truncate text-[12px] font-medium text-foreground/85">
Terminal read failed
</div>
<div className="truncate text-[11px] text-muted-foreground/60">
{artifact.message}
</div>
</div>
</div>
);
}
const title = artifact.label || artifact.sessionId;
return (
<div className={cn(
'flex w-full items-center gap-2.5 rounded-md border border-border/25 bg-muted/10 px-2.5 py-2',
className,
)} ref={ref}>
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-emerald-500/10 text-emerald-500">
<SquareTerminal size={15} />
</div>
<div className="min-w-0 flex-1 text-left">
<div className="truncate text-[12px] font-medium text-foreground/85">
{title}
</div>
<div className="truncate text-[11px] text-muted-foreground/60">
{formatSubtitle(artifact)}
</div>
</div>
</div>
);
});
TerminalArtifactCard.displayName = 'TerminalArtifactCard';

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip';
import { formatTerminalToolTooltip } from './formatTerminalToolTooltip';
import { TerminalArtifactCard } from './TerminalArtifactCard';
import type { TerminalToolArtifact } from './terminalToolArtifact';
interface TerminalArtifactToolResultProps {
artifact: TerminalToolArtifact;
toolName: string;
args?: Record<string, unknown>;
result?: unknown;
isError?: boolean;
}
export const TerminalArtifactToolResult: React.FC<TerminalArtifactToolResultProps> = ({
artifact,
toolName,
args,
result,
isError,
}) => {
const tooltip = formatTerminalToolTooltip(toolName, args, result, isError);
return (
<Tooltip>
<TooltipTrigger asChild>
<TerminalArtifactCard artifact={artifact} />
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="max-w-md whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed"
>
{tooltip}
</TooltipContent>
</Tooltip>
);
};

View File

@@ -0,0 +1,58 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { I18nProvider } from '../../../application/i18n/I18nProvider.tsx';
import { VaultArtifactCard } from './VaultArtifactCard.tsx';
test('VaultArtifactCard renders note artifact title', () => {
const html = renderToStaticMarkup(
<I18nProvider locale="en">
<VaultArtifactCard
artifact={{
kind: 'vault.note',
noteId: 'note-1',
title: 'Runbook',
group: 'ops/prod',
}}
/>
</I18nProvider>,
);
assert.match(html, /Runbook/);
assert.match(html, /ops\/prod/);
});
test('VaultArtifactCard does not render a clickable note without navigation wiring', () => {
const html = renderToStaticMarkup(
<I18nProvider locale="en">
<VaultArtifactCard
artifact={{
kind: 'vault.note',
noteId: 'note-1',
title: 'Runbook',
}}
/>
</I18nProvider>,
);
assert.doesNotMatch(html, /<button/);
});
test('VaultArtifactCard renders host batch summary', () => {
const html = renderToStaticMarkup(
<I18nProvider locale="en">
<VaultArtifactCard
artifact={{
kind: 'vault.hosts.batch',
addedCount: 2,
preview: [{ label: 'Web', hostname: '10.0.0.1' }],
}}
/>
</I18nProvider>,
);
assert.match(html, /Added 2 hosts/);
assert.match(html, /Web/);
});

View File

@@ -0,0 +1,211 @@
import { ChevronRight } from 'lucide-react';
import React from 'react';
import { useI18n } from '../../../application/i18n/I18nProvider';
import { cn } from '../../../lib/utils';
import type { VaultToolArtifact } from './vaultToolArtifact';
import {
canNavigateVaultArtifact,
navigateVaultArtifact,
useVaultArtifactNavigation,
} from './VaultArtifactNavigationContext';
import { VaultArtifactIcon } from './vaultArtifactPresentation';
interface VaultArtifactCardProps {
artifact: VaultToolArtifact;
toolName?: string;
className?: string;
}
function getArtifactPresentation(
artifact: VaultToolArtifact,
t: (key: string, values?: Record<string, unknown>) => string,
): {
title: string;
subtitle?: string;
clickable: boolean;
} {
switch (artifact.kind) {
case 'vault.note':
return {
title: artifact.title,
subtitle: artifact.group ?? t('ai.chat.artifact.noteFallback'),
clickable: true,
};
case 'vault.host':
return {
title: artifact.label,
subtitle: artifact.port
? `${artifact.hostname}:${artifact.port}`
: artifact.hostname,
clickable: true,
};
case 'vault.hosts.batch': {
const preview = artifact.preview
.slice(0, 2)
.map((host) => host.label || host.hostname)
.filter(Boolean)
.join(', ');
return {
title: artifact.dryRun
? t('ai.chat.artifact.hostsPreview', { count: artifact.addedCount })
: t('ai.chat.artifact.hostsAdded', { count: artifact.addedCount }),
subtitle: preview || t('ai.chat.artifact.openHosts'),
clickable: true,
};
}
case 'vault.summary':
return {
title: artifact.section === 'notes'
? t('ai.chat.artifact.notesSummary', { count: artifact.count })
: artifact.section === 'hosts'
? t('ai.chat.artifact.hostsSummary', { count: artifact.count })
: artifact.section === 'snippets'
? t('ai.chat.artifact.snippetsSummary', { count: artifact.count })
: t('ai.chat.artifact.scriptsSummary', { count: artifact.count }),
subtitle: artifact.section === 'notes'
? t('ai.chat.artifact.openNotes')
: artifact.section === 'hosts'
? t('ai.chat.artifact.openHosts')
: t('ai.chat.artifact.openSnippets'),
clickable: true,
};
case 'vault.snippet':
return {
title: artifact.label,
subtitle: artifact.package || t('ai.chat.artifact.snippetFallback'),
clickable: true,
};
case 'vault.script':
return {
title: artifact.label,
subtitle: artifact.language
? t('ai.chat.artifact.scriptLanguage', { language: artifact.language })
: artifact.package || t('ai.chat.artifact.scriptFallback'),
clickable: true,
};
case 'vault.snippet.deleted':
return {
title: t('ai.chat.artifact.snippetDeleted'),
subtitle: artifact.snippetId,
clickable: false,
};
case 'vault.script.deleted':
return {
title: t('ai.chat.artifact.scriptDeleted'),
subtitle: artifact.scriptId,
clickable: false,
};
case 'vault.snippet.run':
return {
title: t('ai.chat.artifact.snippetRan'),
subtitle: artifact.command || artifact.snippetId,
clickable: true,
};
case 'vault.script.run':
return {
title: artifact.status
? t('ai.chat.artifact.scriptRunStatus', { status: artifact.status })
: t('ai.chat.artifact.scriptStarted'),
subtitle: artifact.runId,
clickable: true,
};
case 'vault.script.runs':
return {
title: t('ai.chat.artifact.scriptRunsSummary', { count: artifact.count }),
subtitle: t('ai.chat.artifact.openSnippets'),
clickable: true,
};
case 'vault.script.action':
return {
title: artifact.action === 'stop'
? t('ai.chat.artifact.scriptRunStopped')
: artifact.action === 'pause'
? t('ai.chat.artifact.scriptRunPaused')
: t('ai.chat.artifact.scriptRunResumed'),
subtitle: artifact.runId,
clickable: false,
};
case 'vault.script.reference':
return {
title: t('ai.chat.artifact.scriptReference'),
subtitle: t('ai.chat.artifact.openSnippets'),
clickable: true,
};
case 'error':
return {
title: t('ai.chat.artifact.failed'),
subtitle: artifact.message,
clickable: false,
};
default:
return {
title: '',
clickable: false,
};
}
}
export const VaultArtifactCard: React.FC<VaultArtifactCardProps> = ({
artifact,
toolName,
className,
}) => {
const { t } = useI18n();
const navigation = useVaultArtifactNavigation();
const presentation = getArtifactPresentation(artifact, t);
const canNavigate = presentation.clickable && canNavigateVaultArtifact(artifact, navigation);
const handleClick = () => {
if (!canNavigate || !navigation) return;
navigateVaultArtifact(artifact, navigation);
};
const content = (
<>
<VaultArtifactIcon artifact={artifact} toolName={toolName} />
<div className="min-w-0 flex-1 text-left">
<div className="truncate text-[12px] font-medium text-foreground/85">
{presentation.title}
</div>
{presentation.subtitle && (
<div className="truncate text-[11px] text-muted-foreground/60">
{presentation.subtitle}
</div>
)}
</div>
{canNavigate && (
<ChevronRight size={12} className="shrink-0 text-muted-foreground/40" />
)}
</>
);
if (!canNavigate) {
return (
<div
className={cn(
presentation.clickable
? 'flex w-full items-center gap-2.5 rounded-md border border-border/25 bg-muted/10 px-2.5 py-2 text-left'
: 'flex items-center gap-2 px-1 py-0.5',
className,
'cursor-default',
)}
>
{content}
</div>
);
}
return (
<button
type="button"
onClick={handleClick}
className={cn(
'flex w-full items-center gap-2.5 rounded-md border border-border/25 bg-muted/10 px-2.5 py-2',
'text-left transition-colors hover:bg-muted/20',
className,
)}
>
{content}
</button>
);
};

View File

@@ -0,0 +1,80 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createVaultArtifactNavigationActions,
navigateVaultArtifact,
} from './VaultArtifactNavigationContext.tsx';
const t = (key: string) => key;
test('clicking an existing note artifact opens that note', () => {
const openedNotes: string[] = [];
const unavailable: Array<{ title: string; message: string }> = [];
const navigation = createVaultArtifactNavigationActions({
notes: [{ id: 'note-1', title: 'Runbook', content: '', createdAt: 1, updatedAt: 1 }],
hosts: [],
snippets: [],
t,
onOpenVaultNote: (noteId) => openedNotes.push(noteId),
onOpenVaultHost: () => {},
onOpenVaultSection: () => {},
onUnavailable: (message, title) => unavailable.push({ title, message }),
});
navigateVaultArtifact({
kind: 'vault.note',
noteId: 'note-1',
title: 'Runbook',
}, navigation);
assert.deepEqual(openedNotes, ['note-1']);
assert.deepEqual(unavailable, []);
});
test('clicking an existing snippet artifact opens that snippet', () => {
const openedSnippets: string[] = [];
const navigation = createVaultArtifactNavigationActions({
notes: [],
hosts: [],
snippets: [{ id: 'snippet-1', label: 'Restart nginx', command: 'sudo systemctl restart nginx' }],
t,
onOpenVaultSnippet: (snippetId) => openedSnippets.push(snippetId),
onUnavailable: (message, title) => { void message; void title; },
});
navigateVaultArtifact({
kind: 'vault.snippet',
snippetId: 'snippet-1',
label: 'Restart nginx',
}, navigation);
assert.deepEqual(openedSnippets, ['snippet-1']);
});
test('clicking a missing note artifact shows an unavailable message instead of opening', () => {
const openedNotes: string[] = [];
const unavailable: Array<{ title: string; message: string }> = [];
const navigation = createVaultArtifactNavigationActions({
notes: [],
hosts: [],
snippets: [],
t,
onOpenVaultNote: (noteId) => openedNotes.push(noteId),
onOpenVaultHost: () => {},
onOpenVaultSection: () => {},
onUnavailable: (message, title) => unavailable.push({ title, message }),
});
navigateVaultArtifact({
kind: 'vault.note',
noteId: 'deleted-note',
title: 'Deleted note',
}, navigation);
assert.deepEqual(openedNotes, []);
assert.deepEqual(unavailable, [{
title: 'ai.chat.artifact.unavailableTitle',
message: 'ai.chat.artifact.noteMissing',
}]);
});

View File

@@ -0,0 +1,206 @@
import React, { createContext, useCallback, useContext, useMemo } from 'react';
import type { Host, Snippet, VaultNote } from '../../../types';
import { useI18n } from '../../../application/i18n/I18nProvider';
import { toast } from '../../ui/toast';
import type { VaultSummarySection, VaultToolArtifact } from './vaultToolArtifact';
export type VaultArtifactNavSection = Extract<VaultSummarySection, 'notes' | 'hosts' | 'snippets'>;
export interface VaultArtifactNavigationActions {
openVaultNote?: (noteId: string) => void;
openVaultHost?: (hostId: string) => void;
openVaultSnippet?: (snippetId: string) => void;
openVaultSection?: (section: VaultArtifactNavSection) => void;
}
interface CreateVaultArtifactNavigationActionsOptions {
notes: VaultNote[];
hosts: Host[];
snippets: Snippet[];
t: (key: string) => string;
onOpenVaultNote?: (noteId: string) => void;
onOpenVaultHost?: (hostId: string) => void;
onOpenVaultSnippet?: (snippetId: string) => void;
onOpenVaultSection?: (section: VaultArtifactNavSection) => void;
onUnavailable: (message: string, title: string) => void;
}
interface VaultArtifactNavigationProviderProps {
notes: VaultNote[];
hosts: Host[];
snippets?: Snippet[];
onOpenVaultNote?: (noteId: string) => void;
onOpenVaultHost?: (hostId: string) => void;
onOpenVaultSnippet?: (snippetId: string) => void;
onOpenVaultSection?: (section: VaultArtifactNavSection) => void;
children: React.ReactNode;
}
const VaultArtifactNavigationContext = createContext<VaultArtifactNavigationActions | null>(null);
export function createVaultArtifactNavigationActions({
notes,
hosts,
snippets,
t,
onOpenVaultNote,
onOpenVaultHost,
onOpenVaultSnippet,
onOpenVaultSection,
onUnavailable,
}: CreateVaultArtifactNavigationActionsOptions): VaultArtifactNavigationActions {
const actions: VaultArtifactNavigationActions = {};
if (onOpenVaultNote) {
actions.openVaultNote = (noteId: string) => {
const exists = notes.some((note) => note.id === noteId);
if (!exists) {
onUnavailable(t('ai.chat.artifact.noteMissing'), t('ai.chat.artifact.unavailableTitle'));
return;
}
onOpenVaultNote(noteId);
};
}
if (onOpenVaultHost) {
actions.openVaultHost = (hostId: string) => {
const exists = hosts.some((host) => host.id === hostId);
if (!exists) {
onUnavailable(t('ai.chat.artifact.hostMissing'), t('ai.chat.artifact.unavailableTitle'));
return;
}
onOpenVaultHost(hostId);
};
}
if (onOpenVaultSnippet) {
actions.openVaultSnippet = (snippetId: string) => {
const exists = snippets.some((snippet) => snippet.id === snippetId);
if (!exists) {
onUnavailable(t('ai.chat.artifact.snippetMissing'), t('ai.chat.artifact.unavailableTitle'));
return;
}
onOpenVaultSnippet(snippetId);
};
}
if (onOpenVaultSection) {
actions.openVaultSection = onOpenVaultSection;
}
return actions;
}
export function VaultArtifactNavigationProvider({
notes,
hosts,
snippets = [],
onOpenVaultNote,
onOpenVaultHost,
onOpenVaultSnippet,
onOpenVaultSection,
children,
}: VaultArtifactNavigationProviderProps) {
const { t } = useI18n();
const onUnavailable = useCallback((message: string, title: string) => {
toast.warning(message, title);
}, []);
const value = useMemo<VaultArtifactNavigationActions>(() => createVaultArtifactNavigationActions({
notes,
hosts,
snippets,
t,
onOpenVaultNote,
onOpenVaultHost,
onOpenVaultSnippet,
onOpenVaultSection,
onUnavailable,
}), [
hosts,
notes,
onOpenVaultHost,
onOpenVaultNote,
onOpenVaultSection,
onOpenVaultSnippet,
onUnavailable,
snippets,
t,
]);
return (
<VaultArtifactNavigationContext.Provider value={value}>
{children}
</VaultArtifactNavigationContext.Provider>
);
}
export function useVaultArtifactNavigation(): VaultArtifactNavigationActions | null {
return useContext(VaultArtifactNavigationContext);
}
export function navigateVaultArtifact(
artifact: VaultToolArtifact,
navigation: VaultArtifactNavigationActions,
): void {
switch (artifact.kind) {
case 'vault.note':
navigation.openVaultNote?.(artifact.noteId);
break;
case 'vault.host':
navigation.openVaultHost?.(artifact.hostId);
break;
case 'vault.hosts.batch':
navigation.openVaultSection?.('hosts');
break;
case 'vault.summary':
if (artifact.section === 'scripts') {
navigation.openVaultSection?.('snippets');
} else {
navigation.openVaultSection?.(artifact.section);
}
break;
case 'vault.snippet':
case 'vault.script':
navigation.openVaultSnippet?.(artifact.kind === 'vault.snippet' ? artifact.snippetId : artifact.scriptId);
break;
case 'vault.snippet.run':
navigation.openVaultSnippet?.(artifact.snippetId);
break;
case 'vault.script.run':
navigation.openVaultSnippet?.(artifact.scriptId);
break;
case 'vault.script.reference':
case 'vault.script.runs':
navigation.openVaultSection?.('snippets');
break;
default:
break;
}
}
export function canNavigateVaultArtifact(
artifact: VaultToolArtifact,
navigation: VaultArtifactNavigationActions | null,
): boolean {
if (!navigation) return false;
switch (artifact.kind) {
case 'vault.note':
return Boolean(navigation.openVaultNote);
case 'vault.host':
return Boolean(navigation.openVaultHost);
case 'vault.hosts.batch':
case 'vault.summary':
case 'vault.script.reference':
case 'vault.script.runs':
return Boolean(navigation.openVaultSection);
case 'vault.snippet':
case 'vault.script':
case 'vault.snippet.run':
case 'vault.script.run':
return Boolean(navigation.openVaultSnippet);
default:
return false;
}
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip';
import { VaultArtifactCard } from './VaultArtifactCard';
import { formatVaultToolTooltip } from './formatVaultToolTooltip';
import type { VaultToolArtifact } from './vaultToolArtifact';
interface VaultArtifactToolResultProps {
artifact: VaultToolArtifact;
toolName: string;
args?: Record<string, unknown>;
result?: unknown;
isError?: boolean;
}
export const VaultArtifactToolResult: React.FC<VaultArtifactToolResultProps> = ({
artifact,
toolName,
args,
result,
isError,
}) => {
const tooltip = formatVaultToolTooltip(toolName, args, result, isError);
return (
<Tooltip>
<TooltipTrigger asChild>
<VaultArtifactCard artifact={artifact} toolName={toolName} className="cursor-pointer" />
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="max-w-md whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed"
>
{tooltip}
</TooltipContent>
</Tooltip>
);
};

View File

@@ -0,0 +1,25 @@
function serialize(value: unknown): string {
if (value == null) return '';
if (typeof value === 'string') return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
export function formatTerminalToolTooltip(
toolName: string,
args?: Record<string, unknown>,
result?: unknown,
isError?: boolean,
): string {
const sections = [
`Tool: ${toolName}`,
args ? `Args:\n${serialize(args)}` : null,
result ? `Result:\n${serialize(result)}` : null,
isError ? 'Status: error' : null,
].filter(Boolean);
return sections.join('\n\n');
}

View File

@@ -0,0 +1,18 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { formatVaultToolTooltip } from './formatVaultToolTooltip.ts';
test('formatVaultToolTooltip joins tool name, args, and result with line breaks', () => {
const text = formatVaultToolTooltip(
'vault_notes_create',
{ title: 'Runbook' },
{ ok: true, note: { id: 'n1', title: 'Runbook' } },
);
assert.match(text, /^vault_notes_create/);
assert.match(text, /Arguments:/);
assert.match(text, /"title": "Runbook"/);
assert.match(text, /Result:/);
assert.match(text, /"ok": true/);
});

View File

@@ -0,0 +1,37 @@
function stringifyToolPayload(value: unknown): string {
if (value == null) return '';
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value) as unknown;
if (parsed && typeof parsed === 'object') {
return JSON.stringify(parsed, null, 2);
}
} catch {
return value;
}
return value;
}
if (typeof value === 'object') {
return JSON.stringify(value, null, 2);
}
return String(value);
}
export function formatVaultToolTooltip(
toolName: string,
args?: Record<string, unknown>,
result?: unknown,
isError?: boolean,
): string {
const lines: string[] = [toolName];
if (args && Object.keys(args).length > 0) {
lines.push('', 'Arguments:', stringifyToolPayload(args));
}
if (result !== undefined) {
lines.push('', isError ? 'Error:' : 'Result:', stringifyToolPayload(result));
}
return lines.join('\n');
}

View File

@@ -0,0 +1,82 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseTerminalToolArtifact } from './terminalToolArtifact.ts';
test('parseTerminalToolArtifact maps terminal context reads', () => {
const artifact = parseTerminalToolArtifact('terminal_read_context', {
ok: true,
sessionId: 'session-1',
label: 'prod',
range: 'tail',
content: 'alpha\nbeta\ngamma',
totalLines: 120,
startLine: 100,
endLine: 102,
returnedLines: 3,
hasMoreBefore: true,
hasMoreAfter: true,
source: 'live',
});
assert.deepEqual(artifact, {
kind: 'terminal.context',
sessionId: 'session-1',
label: 'prod',
range: 'tail',
totalLines: 120,
startLine: 100,
endLine: 102,
returnedLines: 3,
hasMoreBefore: true,
hasMoreAfter: true,
source: 'live',
preview: 'alpha\nbeta\ngamma',
});
});
test('parseTerminalToolArtifact maps errors', () => {
const artifact = parseTerminalToolArtifact('terminal_read_context', {
ok: false,
error: 'Terminal session not found.',
});
assert.deepEqual(artifact, {
kind: 'error',
message: 'Terminal session not found.',
});
});
test('parseTerminalToolArtifact unwraps Claude MCP text result envelopes', () => {
const artifact = parseTerminalToolArtifact('mcp__netcatty-remote-hosts__terminal_read_context', JSON.stringify([
{
type: 'text',
text: JSON.stringify({
ok: true,
sessionId: 'session-1',
label: 'prod',
range: 'tail',
content: 'alpha\nbeta',
totalLines: 20,
startLine: 19,
endLine: 20,
returnedLines: 2,
}),
},
]));
assert.deepEqual(artifact, {
kind: 'terminal.context',
sessionId: 'session-1',
label: 'prod',
range: 'tail',
totalLines: 20,
startLine: 19,
endLine: 20,
returnedLines: 2,
hasMoreBefore: false,
hasMoreAfter: false,
source: undefined,
preview: 'alpha\nbeta',
});
});

View File

@@ -0,0 +1,81 @@
import { normalizeArtifactToolName } from './toolArtifactNames';
import { parseResultPayload } from './toolArtifactResultPayload';
export type TerminalToolArtifact =
| {
kind: 'terminal.context';
sessionId: string;
label?: string;
range: string;
totalLines: number;
startLine: number;
endLine: number;
returnedLines: number;
hasMoreBefore: boolean;
hasMoreAfter: boolean;
source?: string;
preview: string;
}
| {
kind: 'error';
message: string;
};
const TERMINAL_ARTIFACT_TOOL_NAMES = new Set([
'terminal_read_context',
]);
function readString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
function readNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function readBoolean(value: unknown): boolean {
return value === true;
}
export function parseTerminalToolArtifact(
toolName: string,
result: unknown,
): TerminalToolArtifact | null {
const normalizedToolName = normalizeArtifactToolName(toolName);
if (!normalizedToolName || !TERMINAL_ARTIFACT_TOOL_NAMES.has(normalizedToolName)) return null;
const payload = parseResultPayload(result);
if (!payload) return null;
if (payload.ok === false || payload.isError === true || typeof payload.error === 'string') {
return {
kind: 'error',
message: readString(payload.error) ?? 'Terminal context read failed.',
};
}
const sessionId = readString(payload.sessionId);
const content = typeof payload.content === 'string' ? payload.content : '';
const totalLines = readNumber(payload.totalLines);
const startLine = readNumber(payload.startLine);
const endLine = readNumber(payload.endLine);
const returnedLines = readNumber(payload.returnedLines);
if (!sessionId || totalLines == null || startLine == null || endLine == null || returnedLines == null) {
return null;
}
return {
kind: 'terminal.context',
sessionId,
label: readString(payload.label),
range: readString(payload.range) ?? 'viewport',
totalLines,
startLine,
endLine,
returnedLines,
hasMoreBefore: readBoolean(payload.hasMoreBefore),
hasMoreAfter: readBoolean(payload.hasMoreAfter),
source: readString(payload.source),
preview: content.split('\n').slice(0, 6).join('\n'),
};
}

View File

@@ -0,0 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
inferArtifactToolNameFromCliArgs,
normalizeArtifactToolName,
} from './toolArtifactNames.ts';
test('normalizeArtifactToolName unwraps MCP server prefixes', () => {
assert.equal(
normalizeArtifactToolName('mcp__netcatty__vault_notes_create'),
'vault_notes_create',
);
assert.equal(
normalizeArtifactToolName('mcp__netcatty-remote-hosts__terminal_read_context'),
'terminal_read_context',
);
});
test('normalizeArtifactToolName unwraps OpenCode server prefixes', () => {
assert.equal(
normalizeArtifactToolName('netcatty-remote-hosts_vault_notes_get'),
'vault_notes_get',
);
assert.equal(
normalizeArtifactToolName('netcatty-remote-hosts_vault_hosts_list'),
'vault_hosts_list',
);
assert.equal(
normalizeArtifactToolName('netcatty-remote-hosts_terminal_read_context'),
'terminal_read_context',
);
});
test('normalizeArtifactToolName unwraps Copilot server prefixes', () => {
assert.equal(
normalizeArtifactToolName('netcatty-remote-hosts-vault_notes_list'),
'vault_notes_list',
);
assert.equal(
normalizeArtifactToolName('netcatty-remote-hosts-terminal_read_context'),
'terminal_read_context',
);
});
test('inferArtifactToolNameFromCliArgs maps Netcatty CLI artifact commands', () => {
assert.equal(
inferArtifactToolNameFromCliArgs({
command: `/bin/zsh -lc '"/Applications/Netcatty.app/netcatty-tool-cli" vault host get --host-id host_1 --json'`,
}),
'host_get',
);
});

View File

@@ -0,0 +1,105 @@
const MCP_TOOL_NAME_PREFIX = 'mcp__';
const KNOWN_ARTIFACT_TOOL_NAMES = [
'terminal_read_context',
'vault_notes_create',
'vault_notes_update',
'vault_notes_get',
'vault_notes_list',
'vault_hosts_create',
'vault_hosts_import',
'vault_hosts_list',
'host_get',
'snippets_list',
'snippets_get',
'snippets_create',
'snippets_update',
'snippets_delete',
'snippets_run',
'scripts_list',
'scripts_get',
'scripts_create',
'scripts_update',
'scripts_delete',
'scripts_run',
'scripts_reference',
'scripts_runs_list',
'scripts_run_stop',
'scripts_run_pause',
'scripts_run_resume',
'scripts_targets_set',
] as const;
const CLI_ARTIFACT_TOOL_NAMES = new Map<string, string>([
['vault host get', 'host_get'],
]);
function readCommandString(args: Record<string, unknown> | undefined): string | null {
if (!args) return null;
const raw = args.command;
if (typeof raw === 'string') return raw || null;
if (!Array.isArray(raw) || raw.length === 0) return null;
const isShellWrap =
raw.length >= 3 &&
/(?:^|\/)(sh|bash|zsh|fish|ash|dash)$/.test(String(raw[0] ?? '')) &&
/^-l?c$/.test(String(raw[1] ?? ''));
return isShellWrap
? String(raw[raw.length - 1] ?? '') || null
: raw.map((part) => String(part)).join(' ');
}
function unwrapShellCommand(command: string): string {
const strWrap = command.match(
/^(?:\S*\/)?(?:sh|bash|zsh|fish|ash|dash)\s+-l?c\s+(['"])([\s\S]*)\1\s*$/,
);
return strWrap ? strWrap[2] : command;
}
function stripWrappingQuote(value: string): string {
const trimmed = value.trim();
if (trimmed.length < 2) return trimmed;
const first = trimmed[0];
const last = trimmed[trimmed.length - 1];
return (first === last && (first === '"' || first === "'"))
? trimmed.slice(1, -1)
: trimmed;
}
export function normalizeArtifactToolName(toolName: string | undefined): string | undefined {
const trimmed = toolName?.trim();
if (!trimmed) return undefined;
if (trimmed.startsWith(MCP_TOOL_NAME_PREFIX)) {
const segments = trimmed.split('__').filter(Boolean);
return segments[segments.length - 1] || trimmed;
}
const prefixedArtifactToolName = KNOWN_ARTIFACT_TOOL_NAMES.find((candidate) => (
trimmed.endsWith(`_${candidate}`) || trimmed.endsWith(`-${candidate}`)
));
if (prefixedArtifactToolName) return prefixedArtifactToolName;
return trimmed;
}
export function inferArtifactToolNameFromCliArgs(
args: Record<string, unknown> | undefined,
): string | undefined {
const command = readCommandString(args);
if (!command) return undefined;
const unwrapped = unwrapShellCommand(command);
const cliMatch = unwrapped.match(/(?:^|\s|["'])(?:\S*\/)?netcatty-tool-cli(?:\.(?:cjs|cmd))?(?=["'\s]|$)([\s\S]*)$/);
if (!cliMatch) return undefined;
const afterCli = stripWrappingQuote(cliMatch[1] ?? '').replace(/^["']?\s*/, '');
const commandKey = afterCli
.split(/\s+/)
.filter((part) => part && !part.startsWith('-'))
.slice(0, 3)
.join(' ');
return CLI_ARTIFACT_TOOL_NAMES.get(commandKey);
}

View File

@@ -0,0 +1,65 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseResultPayload } from './toolArtifactResultPayload.ts';
test('parseResultPayload unwraps MCP text content arrays', () => {
assert.deepEqual(parseResultPayload(JSON.stringify([
{
type: 'text',
text: JSON.stringify({ ok: true, value: 42 }),
},
])), {
ok: true,
value: 42,
});
});
test('parseResultPayload unwraps MCP content objects', () => {
assert.deepEqual(parseResultPayload({
content: [
{
type: 'text',
text: JSON.stringify({ ok: true, label: 'prod' }),
},
],
}), {
ok: true,
label: 'prod',
});
});
test('parseResultPayload unwraps Copilot content string objects', () => {
assert.deepEqual(parseResultPayload({
content: JSON.stringify({ ok: true, notes: [{ id: 'note-1' }] }),
detailedContent: JSON.stringify({ ok: true, notes: [{ id: 'note-1' }] }),
contents: [
{
type: 'text',
text: JSON.stringify({ ok: true, notes: [{ id: 'note-1' }] }),
},
],
}), {
ok: true,
notes: [{ id: 'note-1' }],
});
});
test('parseResultPayload unwraps Copilot contents arrays', () => {
assert.deepEqual(parseResultPayload({
contents: [
{
type: 'text',
text: JSON.stringify({ ok: true, hosts: [{ id: 'host-1' }] }),
},
],
}), {
ok: true,
hosts: [{ id: 'host-1' }],
});
});
test('parseResultPayload preserves plain result objects', () => {
const payload = { ok: true, name: 'plain' };
assert.equal(parseResultPayload(payload), payload);
});

View File

@@ -0,0 +1,60 @@
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function parseJsonRecord(value: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(value) as unknown;
return parseResultPayload(parsed);
} catch {
return null;
}
}
function looksLikeJson(value: string): boolean {
const trimmed = value.trim();
return trimmed.startsWith('{') || trimmed.startsWith('[');
}
function unwrapMcpTextEnvelope(value: unknown): unknown {
if (Array.isArray(value)) {
const textPart = value.find((entry) => {
const record = asRecord(entry);
return record?.type === 'text' && typeof record.text === 'string';
});
const record = asRecord(textPart);
return typeof record?.text === 'string' ? record.text : value;
}
const record = asRecord(value);
if (record?.type === 'text' && typeof record.text === 'string') {
return record.text;
}
if (Array.isArray(record?.content)) {
return unwrapMcpTextEnvelope(record.content);
}
if (typeof record?.content === 'string' && looksLikeJson(record.content)) {
return record.content;
}
if (typeof record?.detailedContent === 'string' && looksLikeJson(record.detailedContent)) {
return record.detailedContent;
}
if (Array.isArray(record?.contents)) {
return unwrapMcpTextEnvelope(record.contents);
}
return value;
}
export function parseResultPayload(result: unknown): Record<string, unknown> | null {
if (result == null) return null;
const unwrapped = unwrapMcpTextEnvelope(result);
if (typeof unwrapped === 'string') {
return parseJsonRecord(unwrapped);
}
return asRecord(unwrapped);
}

View File

@@ -0,0 +1,45 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { resolveVaultArtifactVisualKind } from './vaultArtifactPresentation.tsx';
test('resolveVaultArtifactVisualKind distinguishes note vs host tools', () => {
assert.equal(
resolveVaultArtifactVisualKind(
{ kind: 'vault.note', noteId: 'n1', title: 'Doc' },
'vault_notes_create',
),
'noteCreate',
);
assert.equal(
resolveVaultArtifactVisualKind(
{ kind: 'vault.host', hostId: 'h1', label: 'Web', hostname: '10.0.0.1' },
'host_get',
),
'host',
);
assert.equal(
resolveVaultArtifactVisualKind(
{
kind: 'vault.hosts.batch',
sourceTool: 'vault_hosts_import',
addedCount: 2,
preview: [],
},
'vault_hosts_import',
),
'hostImport',
);
assert.equal(
resolveVaultArtifactVisualKind(
{
kind: 'vault.hosts.batch',
sourceTool: 'vault_hosts_create',
addedCount: 2,
preview: [],
},
'vault_hosts_create',
),
'hostCreate',
);
});

View File

@@ -0,0 +1,200 @@
import {
AlertCircle,
BookOpen,
FileCode,
FilePenLine,
FileText,
FolderInput,
LayoutGrid,
Library,
ListChecks,
NotebookPen,
Pause,
Play,
Server,
ServerCog,
SquareTerminal,
Trash2,
Zap,
} from 'lucide-react';
import React from 'react';
import { cn } from '../../../lib/utils';
import type { VaultToolArtifact } from './vaultToolArtifact';
export type VaultArtifactVisualKind =
| 'noteCreate'
| 'noteUpdate'
| 'noteRead'
| 'noteList'
| 'host'
| 'hostCreate'
| 'hostImport'
| 'hostList'
| 'snippet'
| 'snippetCreate'
| 'snippetUpdate'
| 'snippetList'
| 'snippetRun'
| 'snippetDeleted'
| 'script'
| 'scriptCreate'
| 'scriptUpdate'
| 'scriptList'
| 'scriptRun'
| 'scriptDeleted'
| 'scriptRuns'
| 'scriptAction'
| 'scriptReference'
| 'error';
const ARTIFACT_ICON_SIZE = 18;
const VISUAL_STYLES: Record<VaultArtifactVisualKind, { wrapper: string; icon: string }> = {
noteCreate: { wrapper: 'bg-violet-500/12', icon: 'text-violet-400' },
noteUpdate: { wrapper: 'bg-violet-500/10', icon: 'text-violet-300/90' },
noteRead: { wrapper: 'bg-violet-500/10', icon: 'text-violet-300/80' },
noteList: { wrapper: 'bg-muted/30', icon: 'text-muted-foreground/70' },
host: { wrapper: 'bg-emerald-500/12', icon: 'text-emerald-400' },
hostCreate: { wrapper: 'bg-sky-500/12', icon: 'text-sky-400' },
hostImport: { wrapper: 'bg-amber-500/12', icon: 'text-amber-400' },
hostList: { wrapper: 'bg-muted/30', icon: 'text-muted-foreground/70' },
snippet: { wrapper: 'bg-sky-500/12', icon: 'text-sky-400' },
snippetCreate: { wrapper: 'bg-sky-500/12', icon: 'text-sky-400' },
snippetUpdate: { wrapper: 'bg-sky-500/10', icon: 'text-sky-300/90' },
snippetList: { wrapper: 'bg-muted/30', icon: 'text-muted-foreground/70' },
snippetRun: { wrapper: 'bg-sky-500/10', icon: 'text-sky-300/90' },
snippetDeleted: { wrapper: 'bg-muted/25', icon: 'text-muted-foreground/60' },
script: { wrapper: 'bg-violet-500/12', icon: 'text-violet-400' },
scriptCreate: { wrapper: 'bg-violet-500/12', icon: 'text-violet-400' },
scriptUpdate: { wrapper: 'bg-violet-500/10', icon: 'text-violet-300/90' },
scriptList: { wrapper: 'bg-muted/30', icon: 'text-muted-foreground/70' },
scriptRun: { wrapper: 'bg-violet-500/10', icon: 'text-violet-300/90' },
scriptDeleted: { wrapper: 'bg-muted/25', icon: 'text-muted-foreground/60' },
scriptRuns: { wrapper: 'bg-muted/30', icon: 'text-muted-foreground/70' },
scriptAction: { wrapper: 'bg-violet-500/10', icon: 'text-violet-300/90' },
scriptReference: { wrapper: 'bg-violet-500/10', icon: 'text-violet-300/90' },
error: { wrapper: 'bg-destructive/10', icon: 'text-destructive/80' },
};
export function resolveVaultArtifactVisualKind(
artifact: VaultToolArtifact,
toolName?: string,
): VaultArtifactVisualKind {
if (artifact.kind === 'error') return 'error';
if (artifact.kind === 'vault.note') {
if (toolName === 'vault_notes_create') return 'noteCreate';
if (toolName === 'vault_notes_update') return 'noteUpdate';
return 'noteRead';
}
if (artifact.kind === 'vault.host') return 'host';
if (artifact.kind === 'vault.hosts.batch') {
if (artifact.sourceTool === 'vault_hosts_import' || toolName === 'vault_hosts_import') {
return 'hostImport';
}
return 'hostCreate';
}
if (artifact.kind === 'vault.summary') {
if (artifact.section === 'notes') return 'noteList';
if (artifact.section === 'hosts') return 'hostList';
if (artifact.section === 'snippets') return 'snippetList';
return 'scriptList';
}
if (artifact.kind === 'vault.snippet') {
if (toolName === 'snippets_create') return 'snippetCreate';
if (toolName === 'snippets_update') return 'snippetUpdate';
return 'snippet';
}
if (artifact.kind === 'vault.snippet.deleted') return 'snippetDeleted';
if (artifact.kind === 'vault.snippet.run') return 'snippetRun';
if (artifact.kind === 'vault.script') {
if (toolName === 'scripts_create') return 'scriptCreate';
if (toolName === 'scripts_update' || toolName === 'scripts_targets_set') return 'scriptUpdate';
return 'script';
}
if (artifact.kind === 'vault.script.deleted') return 'scriptDeleted';
if (artifact.kind === 'vault.script.run') return 'scriptRun';
if (artifact.kind === 'vault.script.runs') return 'scriptRuns';
if (artifact.kind === 'vault.script.action') return 'scriptAction';
if (artifact.kind === 'vault.script.reference') return 'scriptReference';
return 'host';
}
function renderVisualIcon(kind: VaultArtifactVisualKind): React.ReactNode {
const className = VISUAL_STYLES[kind].icon;
switch (kind) {
case 'noteCreate':
return <NotebookPen size={ARTIFACT_ICON_SIZE} className={className} />;
case 'noteUpdate':
return <FilePenLine size={ARTIFACT_ICON_SIZE} className={className} />;
case 'noteRead':
return <FileText size={ARTIFACT_ICON_SIZE} className={className} />;
case 'noteList':
return <Library size={ARTIFACT_ICON_SIZE} className={className} />;
case 'host':
return <Server size={ARTIFACT_ICON_SIZE} className={className} />;
case 'hostCreate':
return <ServerCog size={ARTIFACT_ICON_SIZE} className={className} />;
case 'hostImport':
return <FolderInput size={ARTIFACT_ICON_SIZE} className={className} />;
case 'hostList':
return <LayoutGrid size={ARTIFACT_ICON_SIZE} className={className} />;
case 'snippet':
case 'snippetCreate':
case 'snippetUpdate':
case 'snippetRun':
return <Zap size={ARTIFACT_ICON_SIZE} className={className} />;
case 'snippetList':
return <SquareTerminal size={ARTIFACT_ICON_SIZE} className={className} />;
case 'snippetDeleted':
return <Trash2 size={ARTIFACT_ICON_SIZE} className={className} />;
case 'script':
case 'scriptCreate':
case 'scriptUpdate':
case 'scriptReference':
return <FileCode size={ARTIFACT_ICON_SIZE} className={className} />;
case 'scriptList':
case 'scriptRuns':
return <ListChecks size={ARTIFACT_ICON_SIZE} className={className} />;
case 'scriptRun':
return <Play size={ARTIFACT_ICON_SIZE} className={className} />;
case 'scriptDeleted':
return <Trash2 size={ARTIFACT_ICON_SIZE} className={className} />;
case 'scriptAction':
return <Pause size={ARTIFACT_ICON_SIZE} className={className} />;
case 'error':
return <AlertCircle size={ARTIFACT_ICON_SIZE} className={className} />;
default:
return <BookOpen size={ARTIFACT_ICON_SIZE} className={className} />;
}
}
export function VaultArtifactIcon({
artifact,
toolName,
}: {
artifact: VaultToolArtifact;
toolName?: string;
}) {
const kind = resolveVaultArtifactVisualKind(artifact, toolName);
const styles = VISUAL_STYLES[kind];
return (
<span
className={cn(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-md',
styles.wrapper,
)}
>
{renderVisualIcon(kind)}
</span>
);
}

View File

@@ -0,0 +1,141 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseVaultToolArtifact } from './vaultToolArtifact.ts';
test('parseVaultToolArtifact maps note create results', () => {
const artifact = parseVaultToolArtifact('vault_notes_create', {
ok: true,
note: { id: 'note-1', title: 'Runbook', group: 'ops/prod' },
});
assert.deepEqual(artifact, {
kind: 'vault.note',
noteId: 'note-1',
title: 'Runbook',
group: 'ops/prod',
});
});
test('parseVaultToolArtifact maps single host create to host artifact', () => {
const artifact = parseVaultToolArtifact('vault_hosts_create', {
ok: true,
addedCount: 1,
previewHosts: [
{ id: 'host-1', label: 'Dokploy', hostname: '10.2.0.209' },
],
});
assert.deepEqual(artifact, {
kind: 'vault.host',
hostId: 'host-1',
label: 'Dokploy',
hostname: '10.2.0.209',
});
});
test('parseVaultToolArtifact maps host batch import results', () => {
const artifact = parseVaultToolArtifact('vault_hosts_create', {
ok: true,
addedCount: 2,
previewHosts: [
{ id: 'host-1', label: 'Web', hostname: '10.0.0.1' },
{ id: 'host-2', hostname: 'db.internal' },
],
});
assert.equal(artifact?.kind, 'vault.hosts.batch');
if (artifact?.kind !== 'vault.hosts.batch') return;
assert.equal(artifact.addedCount, 2);
assert.equal(artifact.preview.length, 2);
});
test('parseVaultToolArtifact maps host get results', () => {
const artifact = parseVaultToolArtifact('host_get', {
ok: true,
host: { id: 'host-9', label: 'Prod', hostname: 'prod.example.com', port: 22 },
});
assert.deepEqual(artifact, {
kind: 'vault.host',
hostId: 'host-9',
label: 'Prod',
hostname: 'prod.example.com',
port: 22,
group: undefined,
});
});
test('parseVaultToolArtifact maps errors', () => {
const artifact = parseVaultToolArtifact('vault_notes_get', {
ok: false,
error: 'Vault note "missing" was not found.',
});
assert.deepEqual(artifact, {
kind: 'error',
message: 'Vault note "missing" was not found.',
});
});
test('parseVaultToolArtifact maps script create results', () => {
const artifact = parseVaultToolArtifact('scripts_create', {
ok: true,
script: {
id: 'script-1',
label: 'Disk cleanup',
language: 'javascript',
package: 'maintenance',
},
});
assert.deepEqual(artifact, {
kind: 'vault.script',
scriptId: 'script-1',
label: 'Disk cleanup',
package: 'maintenance',
language: 'javascript',
});
});
test('parseVaultToolArtifact maps snippet list results', () => {
const artifact = parseVaultToolArtifact('snippets_list', {
ok: true,
snippets: [{ id: 's1', label: 'Restart nginx' }],
});
assert.deepEqual(artifact, {
kind: 'vault.summary',
section: 'snippets',
count: 1,
});
});
test('parseVaultToolArtifact maps script run results', () => {
const artifact = parseVaultToolArtifact('scripts_run', {
ok: true,
snippetId: 'script-1',
runId: 'run-9',
kind: 'script',
});
assert.deepEqual(artifact, {
kind: 'vault.script.run',
scriptId: 'script-1',
runId: 'run-9',
status: undefined,
});
});
test('parseVaultToolArtifact unwraps Claude MCP text result envelopes', () => {
const artifact = parseVaultToolArtifact('mcp__netcatty-remote-hosts__vault_notes_list', JSON.stringify([
{
type: 'text',
text: JSON.stringify({
ok: true,
notes: [
{ id: 'note-1', title: 'Docker Compose' },
{ id: 'note-2', title: 'Dokploy' },
],
}),
},
]));
assert.deepEqual(artifact, {
kind: 'vault.summary',
section: 'notes',
count: 2,
});
});

View File

@@ -0,0 +1,330 @@
import { normalizeArtifactToolName } from './toolArtifactNames';
import { parseResultPayload } from './toolArtifactResultPayload';
export type VaultSummarySection = 'notes' | 'hosts' | 'snippets' | 'scripts';
export type VaultToolArtifact =
| {
kind: 'vault.note';
noteId: string;
title: string;
group?: string;
}
| {
kind: 'vault.host';
hostId: string;
label: string;
hostname: string;
port?: number;
group?: string;
}
| {
kind: 'vault.hosts.batch';
sourceTool?: 'vault_hosts_create' | 'vault_hosts_import';
addedCount: number;
dryRun?: boolean;
preview: Array<{ hostId?: string; label?: string; hostname?: string }>;
}
| {
kind: 'vault.summary';
section: VaultSummarySection;
count: number;
}
| {
kind: 'vault.snippet';
snippetId: string;
label: string;
package?: string;
}
| {
kind: 'vault.script';
scriptId: string;
label: string;
package?: string;
language?: string;
}
| {
kind: 'vault.snippet.deleted';
snippetId: string;
}
| {
kind: 'vault.script.deleted';
scriptId: string;
}
| {
kind: 'vault.snippet.run';
snippetId: string;
command?: string;
}
| {
kind: 'vault.script.run';
scriptId: string;
runId: string;
status?: string;
}
| {
kind: 'vault.script.runs';
count: number;
}
| {
kind: 'vault.script.action';
action: 'stop' | 'pause' | 'resume';
runId: string;
}
| {
kind: 'vault.script.reference';
}
| {
kind: 'error';
message: string;
};
const VAULT_ARTIFACT_TOOL_NAMES = new Set([
'vault_notes_create',
'vault_notes_update',
'vault_notes_get',
'vault_notes_list',
'vault_hosts_create',
'vault_hosts_import',
'vault_hosts_list',
'host_get',
'snippets_list',
'snippets_get',
'snippets_create',
'snippets_update',
'snippets_delete',
'snippets_run',
'scripts_list',
'scripts_get',
'scripts_create',
'scripts_update',
'scripts_delete',
'scripts_run',
'scripts_reference',
'scripts_runs_list',
'scripts_run_stop',
'scripts_run_pause',
'scripts_run_resume',
'scripts_targets_set',
]);
function readString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
function readNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function parseNoteArtifact(note: unknown): VaultToolArtifact | null {
if (!note || typeof note !== 'object') return null;
const record = note as Record<string, unknown>;
const noteId = readString(record.id);
const title = readString(record.title);
if (!noteId || !title) return null;
return {
kind: 'vault.note',
noteId,
title,
group: readString(record.group),
};
}
function parseHostArtifact(host: unknown): VaultToolArtifact | null {
if (!host || typeof host !== 'object') return null;
const record = host as Record<string, unknown>;
const hostId = readString(record.id);
const hostname = readString(record.hostname);
if (!hostId || !hostname) return null;
return {
kind: 'vault.host',
hostId,
label: readString(record.label) ?? hostname,
hostname,
port: readNumber(record.port),
group: readString(record.group),
};
}
function parseSnippetArtifact(snippet: unknown): VaultToolArtifact | null {
if (!snippet || typeof snippet !== 'object') return null;
const record = snippet as Record<string, unknown>;
const snippetId = readString(record.id);
const label = readString(record.label);
if (!snippetId || !label) return null;
return {
kind: 'vault.snippet',
snippetId,
label,
package: readString(record.package),
};
}
function parseScriptArtifact(script: unknown): VaultToolArtifact | null {
if (!script || typeof script !== 'object') return null;
const record = script as Record<string, unknown>;
const scriptId = readString(record.id);
const label = readString(record.label);
if (!scriptId || !label) return null;
return {
kind: 'vault.script',
scriptId,
label,
package: readString(record.package),
language: readString(record.language),
};
}
function parsePreviewHosts(value: unknown): Array<{ hostId?: string; label?: string; hostname?: string }> {
if (!Array.isArray(value)) return [];
return value
.map((entry) => {
if (!entry || typeof entry !== 'object') return null;
const record = entry as Record<string, unknown>;
const hostname = readString(record.hostname);
if (!hostname) return null;
return {
hostId: readString(record.id),
label: readString(record.label),
hostname,
};
})
.filter((entry): entry is { hostId?: string; label?: string; hostname: string } => entry !== null);
}
export function isVaultArtifactToolName(toolName: string): boolean {
const normalized = normalizeArtifactToolName(toolName);
return normalized ? VAULT_ARTIFACT_TOOL_NAMES.has(normalized) : false;
}
export function parseVaultToolArtifact(
toolName: string,
result: unknown,
): VaultToolArtifact | null {
const normalizedToolName = normalizeArtifactToolName(toolName);
if (!normalizedToolName || !VAULT_ARTIFACT_TOOL_NAMES.has(normalizedToolName)) return null;
const payload = parseResultPayload(result);
if (!payload) return null;
if (payload.ok === false || payload.isError === true) {
const message = readString(payload.error) ?? 'Operation failed.';
return { kind: 'error', message };
}
switch (normalizedToolName) {
case 'vault_notes_create':
case 'vault_notes_update':
case 'vault_notes_get':
return parseNoteArtifact(payload.note);
case 'vault_notes_list': {
const notes = Array.isArray(payload.notes) ? payload.notes : [];
return { kind: 'vault.summary', section: 'notes', count: notes.length };
}
case 'vault_hosts_create':
case 'vault_hosts_import': {
const preview = parsePreviewHosts(payload.previewHosts);
const addedCount = readNumber(payload.addedCount)
?? (payload.dryRun === true ? readNumber(payload.validCount) : undefined)
?? preview.length;
if (addedCount <= 0 && preview.length === 0) return null;
const dryRun = payload.dryRun === true;
if (!dryRun && addedCount === 1 && preview.length === 1 && preview[0].hostname) {
const single = preview[0];
if (single.hostId) {
return {
kind: 'vault.host',
hostId: single.hostId,
label: single.label ?? single.hostname,
hostname: single.hostname,
};
}
}
return {
kind: 'vault.hosts.batch',
sourceTool: normalizedToolName === 'vault_hosts_import' ? 'vault_hosts_import' : 'vault_hosts_create',
addedCount,
dryRun,
preview,
};
}
case 'vault_hosts_list': {
const hosts = Array.isArray(payload.hosts) ? payload.hosts : [];
return { kind: 'vault.summary', section: 'hosts', count: hosts.length };
}
case 'host_get':
return parseHostArtifact(payload.host);
case 'snippets_list': {
const snippets = Array.isArray(payload.snippets) ? payload.snippets : [];
return { kind: 'vault.summary', section: 'snippets', count: snippets.length };
}
case 'snippets_get':
case 'snippets_create':
case 'snippets_update':
return parseSnippetArtifact(payload.snippet);
case 'snippets_delete': {
const snippetId = readString(payload.snippetId);
if (!snippetId) return null;
return { kind: 'vault.snippet.deleted', snippetId };
}
case 'snippets_run': {
const snippetId = readString(payload.snippetId);
if (!snippetId) return null;
return {
kind: 'vault.snippet.run',
snippetId,
command: readString(payload.command),
};
}
case 'scripts_list': {
const scripts = Array.isArray(payload.scripts) ? payload.scripts : [];
return { kind: 'vault.summary', section: 'scripts', count: scripts.length };
}
case 'scripts_get':
case 'scripts_create':
case 'scripts_update':
case 'scripts_targets_set':
return parseScriptArtifact(payload.script);
case 'scripts_delete': {
const scriptId = readString(payload.scriptId);
if (!scriptId) return null;
return { kind: 'vault.script.deleted', scriptId };
}
case 'scripts_run': {
const scriptId = readString(payload.snippetId) ?? readString(payload.scriptId);
const runId = readString(payload.runId);
if (!scriptId || !runId) return null;
return {
kind: 'vault.script.run',
scriptId,
runId,
status: readString(payload.status),
};
}
case 'scripts_reference':
return { kind: 'vault.script.reference' };
case 'scripts_runs_list': {
const runs = Array.isArray(payload.runs) ? payload.runs : [];
return { kind: 'vault.script.runs', count: runs.length };
}
case 'scripts_run_stop':
return parseScriptRunAction(payload, 'stop');
case 'scripts_run_pause':
return parseScriptRunAction(payload, 'pause');
case 'scripts_run_resume':
return parseScriptRunAction(payload, 'resume');
default:
return null;
}
}
function parseScriptRunAction(
payload: Record<string, unknown>,
action: 'stop' | 'pause' | 'resume',
): VaultToolArtifact | null {
const runId = readString(payload.runId);
if (!runId) return null;
return { kind: 'vault.script.action', action, runId };
}