@makefinks/daemon 0.1.0
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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli.js +22 -0
- package/package.json +79 -0
- package/src/ai/agent-turn-runner.ts +130 -0
- package/src/ai/daemon-ai.ts +403 -0
- package/src/ai/exa-client.ts +21 -0
- package/src/ai/exa-fetch-cache.ts +104 -0
- package/src/ai/model-config.ts +99 -0
- package/src/ai/sanitize-messages.ts +83 -0
- package/src/ai/system-prompt.ts +363 -0
- package/src/ai/tools/fetch-urls.ts +187 -0
- package/src/ai/tools/grounding-manager.ts +94 -0
- package/src/ai/tools/index.ts +52 -0
- package/src/ai/tools/read-file.ts +100 -0
- package/src/ai/tools/render-url.ts +275 -0
- package/src/ai/tools/run-bash.ts +224 -0
- package/src/ai/tools/subagents.ts +195 -0
- package/src/ai/tools/todo-manager.ts +150 -0
- package/src/ai/tools/web-search.ts +91 -0
- package/src/app/App.tsx +711 -0
- package/src/app/components/AppOverlays.tsx +131 -0
- package/src/app/components/AvatarLayer.tsx +51 -0
- package/src/app/components/ConversationPane.tsx +476 -0
- package/src/avatar/DaemonAvatarRenderable.ts +343 -0
- package/src/avatar/daemon-avatar-rig.ts +1165 -0
- package/src/avatar-preview.ts +186 -0
- package/src/cli.ts +26 -0
- package/src/components/ApiKeyInput.tsx +99 -0
- package/src/components/ApiKeyStep.tsx +95 -0
- package/src/components/ApprovalPicker.tsx +109 -0
- package/src/components/ContentBlockView.tsx +141 -0
- package/src/components/DaemonText.tsx +34 -0
- package/src/components/DeviceMenu.tsx +166 -0
- package/src/components/GroundingBadge.tsx +21 -0
- package/src/components/GroundingMenu.tsx +310 -0
- package/src/components/HotkeysPane.tsx +115 -0
- package/src/components/InlineStatusIndicator.tsx +106 -0
- package/src/components/ModelMenu.tsx +411 -0
- package/src/components/OnboardingOverlay.tsx +446 -0
- package/src/components/ProviderMenu.tsx +177 -0
- package/src/components/SessionMenu.tsx +297 -0
- package/src/components/SettingsMenu.tsx +291 -0
- package/src/components/StatusBar.tsx +126 -0
- package/src/components/TokenUsageDisplay.tsx +92 -0
- package/src/components/ToolCallView.tsx +113 -0
- package/src/components/TypingInputBar.tsx +131 -0
- package/src/components/tool-layouts/components.tsx +120 -0
- package/src/components/tool-layouts/defaults.ts +9 -0
- package/src/components/tool-layouts/index.ts +22 -0
- package/src/components/tool-layouts/layouts/bash.ts +110 -0
- package/src/components/tool-layouts/layouts/grounding.tsx +98 -0
- package/src/components/tool-layouts/layouts/index.ts +8 -0
- package/src/components/tool-layouts/layouts/read-file.ts +59 -0
- package/src/components/tool-layouts/layouts/subagent.tsx +118 -0
- package/src/components/tool-layouts/layouts/system-info.ts +8 -0
- package/src/components/tool-layouts/layouts/todo.tsx +139 -0
- package/src/components/tool-layouts/layouts/url-tools.ts +220 -0
- package/src/components/tool-layouts/layouts/web-search.ts +110 -0
- package/src/components/tool-layouts/registry.ts +17 -0
- package/src/components/tool-layouts/types.ts +94 -0
- package/src/hooks/daemon-event-handlers.ts +944 -0
- package/src/hooks/keyboard-handlers.ts +399 -0
- package/src/hooks/menu-navigation.ts +147 -0
- package/src/hooks/use-app-audio-devices-loader.ts +71 -0
- package/src/hooks/use-app-callbacks.ts +202 -0
- package/src/hooks/use-app-context-builder.ts +159 -0
- package/src/hooks/use-app-display-state.ts +162 -0
- package/src/hooks/use-app-menus.ts +51 -0
- package/src/hooks/use-app-model-pricing-loader.ts +45 -0
- package/src/hooks/use-app-model.ts +123 -0
- package/src/hooks/use-app-openrouter-models-loader.ts +44 -0
- package/src/hooks/use-app-openrouter-provider-loader.ts +35 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +212 -0
- package/src/hooks/use-app-sessions.ts +105 -0
- package/src/hooks/use-app-settings.ts +62 -0
- package/src/hooks/use-conversation-manager.ts +163 -0
- package/src/hooks/use-copy-on-select.ts +50 -0
- package/src/hooks/use-daemon-events.ts +396 -0
- package/src/hooks/use-daemon-keyboard.ts +397 -0
- package/src/hooks/use-grounding.ts +46 -0
- package/src/hooks/use-input-history.ts +92 -0
- package/src/hooks/use-menu-keyboard.ts +93 -0
- package/src/hooks/use-playwright-notification.ts +23 -0
- package/src/hooks/use-reasoning-animation.ts +97 -0
- package/src/hooks/use-response-timer.ts +55 -0
- package/src/hooks/use-tool-approval.tsx +202 -0
- package/src/hooks/use-typing-mode.ts +137 -0
- package/src/hooks/use-voice-dependencies-notification.ts +37 -0
- package/src/index.tsx +48 -0
- package/src/scripts/setup-browsers.ts +42 -0
- package/src/state/app-context.tsx +160 -0
- package/src/state/daemon-events.ts +67 -0
- package/src/state/daemon-state.ts +493 -0
- package/src/state/migrations/001-init.ts +33 -0
- package/src/state/migrations/index.ts +8 -0
- package/src/state/model-history-store.ts +45 -0
- package/src/state/runtime-context.ts +21 -0
- package/src/state/session-store.ts +359 -0
- package/src/types/index.ts +405 -0
- package/src/types/theme.ts +52 -0
- package/src/ui/constants.ts +157 -0
- package/src/utils/clipboard.ts +89 -0
- package/src/utils/debug-logger.ts +69 -0
- package/src/utils/formatters.ts +242 -0
- package/src/utils/js-rendering.ts +77 -0
- package/src/utils/markdown-tables.ts +234 -0
- package/src/utils/model-metadata.ts +191 -0
- package/src/utils/openrouter-endpoints.ts +212 -0
- package/src/utils/openrouter-models.ts +205 -0
- package/src/utils/openrouter-pricing.ts +59 -0
- package/src/utils/openrouter-reported-cost.ts +16 -0
- package/src/utils/paste.ts +33 -0
- package/src/utils/preferences.ts +289 -0
- package/src/utils/text-fragment.ts +39 -0
- package/src/utils/tool-output-preview.ts +250 -0
- package/src/utils/voice-dependencies.ts +107 -0
- package/src/utils/workspace-manager.ts +85 -0
- package/src/voice/audio-recorder.ts +579 -0
- package/src/voice/mic-level.ts +35 -0
- package/src/voice/tts/openai-tts-stream.ts +222 -0
- package/src/voice/tts/speech-controller.ts +64 -0
- package/src/voice/tts/tts-player.ts +257 -0
- package/src/voice/voice-input-controller.ts +96 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ToolLayoutConfig, ToolHeader } from "../types";
|
|
2
|
+
import { registerToolLayout } from "../registry";
|
|
3
|
+
|
|
4
|
+
type UnknownRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function extractPath(input: unknown): string | null {
|
|
11
|
+
if (!isRecord(input)) return null;
|
|
12
|
+
if ("path" in input && typeof input.path === "string") {
|
|
13
|
+
return input.path;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatReadFileResult(result: unknown): string[] | null {
|
|
19
|
+
if (!isRecord(result)) return null;
|
|
20
|
+
if (result.success === false && typeof result.error === "string") {
|
|
21
|
+
return [`error: ${result.error}`];
|
|
22
|
+
}
|
|
23
|
+
if (result.success !== true) return null;
|
|
24
|
+
const path = typeof result.path === "string" ? result.path : "";
|
|
25
|
+
const startLine = typeof result.startLine === "number" ? result.startLine : undefined;
|
|
26
|
+
const endLine = typeof result.endLine === "number" ? result.endLine : undefined;
|
|
27
|
+
const hasMore = typeof result.hasMore === "boolean" ? result.hasMore : undefined;
|
|
28
|
+
const content = typeof result.content === "string" ? result.content : "";
|
|
29
|
+
|
|
30
|
+
const range =
|
|
31
|
+
startLine !== undefined && endLine !== undefined && startLine > 0 && endLine > 0
|
|
32
|
+
? ` (${startLine}-${endLine}${hasMore ? "+" : ""})`
|
|
33
|
+
: "";
|
|
34
|
+
|
|
35
|
+
if (!content.trim()) return path ? [`${path}${range}`] : null;
|
|
36
|
+
|
|
37
|
+
const MAX_LINES = 4;
|
|
38
|
+
const MAX_CHARS = 160;
|
|
39
|
+
const lines = content
|
|
40
|
+
.split("\n")
|
|
41
|
+
.slice(0, MAX_LINES)
|
|
42
|
+
.map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
|
|
43
|
+
|
|
44
|
+
return [`${path}${range}:`, ...lines];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const readFileLayout: ToolLayoutConfig = {
|
|
48
|
+
abbreviation: "read",
|
|
49
|
+
|
|
50
|
+
getHeader: (input): ToolHeader | null => {
|
|
51
|
+
const path = extractPath(input);
|
|
52
|
+
if (!path) return null;
|
|
53
|
+
return { primary: path };
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
formatResult: formatReadFileResult,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
registerToolLayout("readFile", readFileLayout);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ToolLayoutConfig, ToolHeader, ToolLayoutRenderProps } from "../types";
|
|
2
|
+
import type { ToolCallStatus } from "../../../types";
|
|
3
|
+
import { registerToolLayout } from "../registry";
|
|
4
|
+
import { COLORS } from "../../../ui/constants";
|
|
5
|
+
|
|
6
|
+
type UnknownRecord = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function extractSubagentSummary(input: unknown): string | null {
|
|
13
|
+
if (!isRecord(input)) return null;
|
|
14
|
+
if ("summary" in input && typeof input.summary === "string") {
|
|
15
|
+
return input.summary;
|
|
16
|
+
}
|
|
17
|
+
if ("topic" in input && typeof input.topic === "string") {
|
|
18
|
+
return input.topic;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractSearchQuery(input: unknown): string | null {
|
|
24
|
+
if (!isRecord(input)) return null;
|
|
25
|
+
if ("query" in input && typeof input.query === "string") {
|
|
26
|
+
return input.query;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function abbreviateToolName(name: string): string {
|
|
32
|
+
const abbreviations: Record<string, string> = {
|
|
33
|
+
webSearch: "search",
|
|
34
|
+
fetchUrls: "fetch",
|
|
35
|
+
renderUrl: "render",
|
|
36
|
+
getSystemInfo: "sys",
|
|
37
|
+
runBash: "bash",
|
|
38
|
+
todoManager: "todo",
|
|
39
|
+
readFile: "read",
|
|
40
|
+
groundingManager: "grounding",
|
|
41
|
+
};
|
|
42
|
+
return abbreviations[name] ?? name.slice(0, 8);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getStepStatusIcon(status: ToolCallStatus): string {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case "running":
|
|
48
|
+
return "~";
|
|
49
|
+
case "completed":
|
|
50
|
+
return "✓";
|
|
51
|
+
case "failed":
|
|
52
|
+
return "x";
|
|
53
|
+
default:
|
|
54
|
+
return " ";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getStepStatusColor(status: ToolCallStatus): string {
|
|
59
|
+
switch (status) {
|
|
60
|
+
case "running":
|
|
61
|
+
return COLORS.STATUS_RUNNING;
|
|
62
|
+
case "completed":
|
|
63
|
+
return COLORS.STATUS_COMPLETED;
|
|
64
|
+
case "failed":
|
|
65
|
+
return COLORS.STATUS_FAILED;
|
|
66
|
+
default:
|
|
67
|
+
return COLORS.STATUS_PENDING;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function SubagentBody({ call }: ToolLayoutRenderProps) {
|
|
72
|
+
if (!call.subagentSteps || call.subagentSteps.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<box flexDirection="column" paddingLeft={2} marginTop={0}>
|
|
78
|
+
{call.subagentSteps.map((step, idx) => {
|
|
79
|
+
const toolLabel = abbreviateToolName(step.toolName);
|
|
80
|
+
let stepLabel = toolLabel;
|
|
81
|
+
if (step.toolName === "webSearch") {
|
|
82
|
+
const query = extractSearchQuery(step.input);
|
|
83
|
+
if (query) {
|
|
84
|
+
stepLabel = `${toolLabel}: "${query}"`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<box key={`${step.toolName}-${idx}`} flexDirection="row" alignItems="center">
|
|
90
|
+
{step.status === "running" ? (
|
|
91
|
+
<spinner name="dots" color={getStepStatusColor(step.status)} />
|
|
92
|
+
) : (
|
|
93
|
+
<text>
|
|
94
|
+
<span fg={getStepStatusColor(step.status)}>{getStepStatusIcon(step.status)}</span>
|
|
95
|
+
</text>
|
|
96
|
+
)}
|
|
97
|
+
<text marginLeft={1}>
|
|
98
|
+
<span fg={COLORS.TOOL_INPUT_TEXT}>{stepLabel}</span>
|
|
99
|
+
</text>
|
|
100
|
+
</box>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const subagentLayout: ToolLayoutConfig = {
|
|
108
|
+
abbreviation: "agent",
|
|
109
|
+
|
|
110
|
+
getHeader: (input): ToolHeader | null => {
|
|
111
|
+
const summary = extractSubagentSummary(input);
|
|
112
|
+
return summary ? { primary: summary } : null;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
renderBody: SubagentBody,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
registerToolLayout("subagent", subagentLayout);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
import type { ToolLayoutConfig, ToolHeader, ToolLayoutRenderProps } from "../types";
|
|
3
|
+
import { registerToolLayout } from "../registry";
|
|
4
|
+
import { COLORS } from "../../../ui/constants";
|
|
5
|
+
import type { TodoItem } from "../../../types";
|
|
6
|
+
|
|
7
|
+
type UnknownRecord = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface TodoInput {
|
|
14
|
+
action: "write" | "update" | "list";
|
|
15
|
+
todos?: Array<{ content: string; status?: string }>;
|
|
16
|
+
index?: number;
|
|
17
|
+
status?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isTodoInput(input: unknown): input is TodoInput {
|
|
21
|
+
return isRecord(input) && "action" in input && typeof input.action === "string";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractTodoAction(input: unknown): string | null {
|
|
25
|
+
if (!isRecord(input)) return null;
|
|
26
|
+
if ("action" in input && typeof input.action === "string") {
|
|
27
|
+
return input.action;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STATUS_ICON: Record<string, string> = {
|
|
33
|
+
pending: "○",
|
|
34
|
+
in_progress: "◐",
|
|
35
|
+
completed: "●",
|
|
36
|
+
cancelled: "✕",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface FormattedTodoItem {
|
|
40
|
+
text: string;
|
|
41
|
+
status: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatTodoItem(todo: { content: string; status?: string }, _idx: number): FormattedTodoItem {
|
|
45
|
+
const status = todo.status || "pending";
|
|
46
|
+
const icon = STATUS_ICON[status] || "[ ]";
|
|
47
|
+
return {
|
|
48
|
+
text: `${icon} ${todo.content}`,
|
|
49
|
+
status,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatTodoDisplayLines(
|
|
54
|
+
input: TodoInput,
|
|
55
|
+
snapshot?: Array<{ content: string; status: string }>,
|
|
56
|
+
currentTodos: TodoItem[] = []
|
|
57
|
+
): FormattedTodoItem[] {
|
|
58
|
+
if (snapshot && snapshot.length > 0) {
|
|
59
|
+
return snapshot.map((todo, idx) => formatTodoItem(todo, idx));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (input.action === "write" && input.todos) {
|
|
63
|
+
return input.todos.map((todo, idx) => formatTodoItem(todo, idx));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (input.action === "update" || input.action === "list") {
|
|
67
|
+
if (currentTodos.length === 0) {
|
|
68
|
+
return [{ text: "(no todos)", status: "pending" }];
|
|
69
|
+
}
|
|
70
|
+
if (input.action === "update" && input.index !== undefined && input.status) {
|
|
71
|
+
const idx = input.index - 1;
|
|
72
|
+
return currentTodos.map((todo: TodoItem, i: number) => {
|
|
73
|
+
if (i === idx) {
|
|
74
|
+
return formatTodoItem({ content: todo.content, status: input.status }, i);
|
|
75
|
+
}
|
|
76
|
+
return formatTodoItem(todo, i);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return currentTodos.map((todo: TodoItem, idx: number) => formatTodoItem(todo, idx));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [{ text: "(unknown action)", status: "pending" }];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getTodoColor(status: string): string {
|
|
86
|
+
switch (status) {
|
|
87
|
+
case "in_progress":
|
|
88
|
+
return COLORS.STATUS_RUNNING;
|
|
89
|
+
case "completed":
|
|
90
|
+
case "cancelled":
|
|
91
|
+
return COLORS.STATUS_DONE_DIM;
|
|
92
|
+
default:
|
|
93
|
+
return COLORS.STATUS_PENDING;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getTodoAttributes(status: string): number {
|
|
98
|
+
if (status === "completed" || status === "cancelled") {
|
|
99
|
+
return TextAttributes.STRIKETHROUGH;
|
|
100
|
+
}
|
|
101
|
+
return TextAttributes.NONE;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function TodoBody({ call }: ToolLayoutRenderProps) {
|
|
105
|
+
if (!isTodoInput(call.input)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines = formatTodoDisplayLines(call.input, call.todoSnapshot);
|
|
110
|
+
|
|
111
|
+
if (lines.length === 0) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<box flexDirection="column" paddingLeft={2}>
|
|
117
|
+
{lines.map((item: FormattedTodoItem, idx: number) => (
|
|
118
|
+
<text key={idx}>
|
|
119
|
+
<span fg={getTodoColor(item.status)} attributes={getTodoAttributes(item.status)}>
|
|
120
|
+
{item.text}
|
|
121
|
+
</span>
|
|
122
|
+
</text>
|
|
123
|
+
))}
|
|
124
|
+
</box>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const todoLayout: ToolLayoutConfig = {
|
|
129
|
+
abbreviation: "todo",
|
|
130
|
+
|
|
131
|
+
getHeader: (input): ToolHeader | null => {
|
|
132
|
+
const action = extractTodoAction(input);
|
|
133
|
+
return action ? { secondary: action } : null;
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
renderBody: TodoBody,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
registerToolLayout("todoManager", todoLayout);
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { ToolLayoutConfig, ToolHeader } from "../types";
|
|
2
|
+
import { registerToolLayout } from "../registry";
|
|
3
|
+
|
|
4
|
+
type UnknownRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface FetchUrlsInput {
|
|
11
|
+
url: string;
|
|
12
|
+
lineOffset?: number;
|
|
13
|
+
lineLimit?: number;
|
|
14
|
+
highlightQuery?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extractFetchUrlsInput(input: unknown): FetchUrlsInput | null {
|
|
18
|
+
if (!isRecord(input)) return null;
|
|
19
|
+
if (!("url" in input) || typeof input.url !== "string") return null;
|
|
20
|
+
|
|
21
|
+
const lineOffset =
|
|
22
|
+
"lineOffset" in input && typeof input.lineOffset === "number" ? input.lineOffset : undefined;
|
|
23
|
+
const lineLimit = "lineLimit" in input && typeof input.lineLimit === "number" ? input.lineLimit : undefined;
|
|
24
|
+
const highlightQuery =
|
|
25
|
+
"highlightQuery" in input && typeof input.highlightQuery === "string" ? input.highlightQuery : undefined;
|
|
26
|
+
return { url: input.url, lineOffset, lineLimit, highlightQuery };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mergeFetchUrlsDefaults(input: FetchUrlsInput | null, result?: unknown): FetchUrlsInput | null {
|
|
30
|
+
if (!input) return null;
|
|
31
|
+
if (!result || typeof result !== "object") return input;
|
|
32
|
+
|
|
33
|
+
const resultRecord = result as Record<string, unknown>;
|
|
34
|
+
const lineOffset =
|
|
35
|
+
input.lineOffset ?? (typeof resultRecord.lineOffset === "number" ? resultRecord.lineOffset : undefined);
|
|
36
|
+
const lineLimit =
|
|
37
|
+
input.lineLimit ?? (typeof resultRecord.lineLimit === "number" ? resultRecord.lineLimit : undefined);
|
|
38
|
+
|
|
39
|
+
return { ...input, lineOffset, lineLimit };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatFetchUrlsHeader(input: FetchUrlsInput): string {
|
|
43
|
+
if (input.highlightQuery) {
|
|
44
|
+
return `highlight: "${input.highlightQuery}"`;
|
|
45
|
+
}
|
|
46
|
+
const parts: string[] = [];
|
|
47
|
+
if (input.lineOffset !== undefined) {
|
|
48
|
+
parts.push(`lineOffset=${input.lineOffset}`);
|
|
49
|
+
}
|
|
50
|
+
if (input.lineLimit !== undefined) {
|
|
51
|
+
parts.push(`lineLimit=${input.lineLimit}`);
|
|
52
|
+
}
|
|
53
|
+
return parts.length > 0 ? `(${parts.join(", ")})` : "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeWhitespace(text: string): string {
|
|
57
|
+
return text.replace(/\r\n/g, "\n").replace(/\t/g, " ");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type ExaLikeItem = {
|
|
61
|
+
title?: unknown;
|
|
62
|
+
url?: unknown;
|
|
63
|
+
text?: unknown;
|
|
64
|
+
lineOffset?: unknown;
|
|
65
|
+
lineLimit?: unknown;
|
|
66
|
+
remainingLines?: unknown;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function formatExaItemLabel(item: ExaLikeItem): string {
|
|
70
|
+
const title = typeof item.title === "string" ? item.title : "";
|
|
71
|
+
const url = typeof item.url === "string" ? item.url : "";
|
|
72
|
+
return title || url || "(untitled)";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractToolDataContainer(result: UnknownRecord): unknown {
|
|
76
|
+
if ("data" in result) return result.data;
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatFetchUrlsResult(result: unknown): string[] | null {
|
|
81
|
+
if (!isRecord(result)) return null;
|
|
82
|
+
if (result.success === false && typeof result.error === "string") {
|
|
83
|
+
return [`error: ${result.error}`];
|
|
84
|
+
}
|
|
85
|
+
if (result.success !== true) return null;
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(result.highlights)) {
|
|
88
|
+
return formatHighlightsResult(result);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = extractToolDataContainer(result);
|
|
92
|
+
const candidate = isRecord(data) ? (data as ExaLikeItem) : {};
|
|
93
|
+
const label = formatExaItemLabel(candidate);
|
|
94
|
+
const url = typeof candidate.url === "string" ? candidate.url : "";
|
|
95
|
+
const title = typeof candidate.title === "string" ? candidate.title : "";
|
|
96
|
+
const lineOffset = typeof candidate.lineOffset === "number" ? candidate.lineOffset : undefined;
|
|
97
|
+
const lineLimit = typeof candidate.lineLimit === "number" ? candidate.lineLimit : undefined;
|
|
98
|
+
const remainingLines =
|
|
99
|
+
typeof candidate.remainingLines === "number" || candidate.remainingLines === null
|
|
100
|
+
? candidate.remainingLines
|
|
101
|
+
: undefined;
|
|
102
|
+
const rangeParts: string[] = [];
|
|
103
|
+
if (lineOffset !== undefined) rangeParts.push(`lineOffset=${lineOffset}`);
|
|
104
|
+
if (lineLimit !== undefined) rangeParts.push(`lineLimit=${lineLimit}`);
|
|
105
|
+
if (remainingLines !== undefined) {
|
|
106
|
+
rangeParts.push(remainingLines === null ? "remainingLines=unknown" : `remainingLines=${remainingLines}`);
|
|
107
|
+
}
|
|
108
|
+
const remainingSuffix = rangeParts.length > 0 ? ` (${rangeParts.join(", ")})` : "";
|
|
109
|
+
|
|
110
|
+
const headerBase = url && title ? `${label} — ${url}` : label;
|
|
111
|
+
const header = `${headerBase}${remainingSuffix}`;
|
|
112
|
+
|
|
113
|
+
const text = typeof candidate.text === "string" ? candidate.text : "";
|
|
114
|
+
if (!text.trim()) return [header];
|
|
115
|
+
|
|
116
|
+
const MAX_LINES = 4;
|
|
117
|
+
const MAX_CHARS = 160;
|
|
118
|
+
const snippet = normalizeWhitespace(text)
|
|
119
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
120
|
+
.trim()
|
|
121
|
+
.split("\n")
|
|
122
|
+
.slice(0, MAX_LINES)
|
|
123
|
+
.map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
|
|
124
|
+
|
|
125
|
+
return [header, ...snippet];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatHighlightsResult(result: UnknownRecord): string[] {
|
|
129
|
+
const highlights = result.highlights as unknown[];
|
|
130
|
+
const highlightQuery = typeof result.highlightQuery === "string" ? result.highlightQuery : "";
|
|
131
|
+
const count = highlights.length;
|
|
132
|
+
|
|
133
|
+
const lines: string[] = [`${count} highlight${count !== 1 ? "s" : ""} for "${highlightQuery}"`];
|
|
134
|
+
|
|
135
|
+
const MAX_HIGHLIGHTS = 3;
|
|
136
|
+
const MAX_CHARS = 120;
|
|
137
|
+
|
|
138
|
+
highlights.slice(0, MAX_HIGHLIGHTS).forEach((h, idx) => {
|
|
139
|
+
if (typeof h === "string") {
|
|
140
|
+
const clean = h.replace(/\n+/g, " ").trim();
|
|
141
|
+
const truncated = clean.length > MAX_CHARS ? `${clean.slice(0, MAX_CHARS - 1)}…` : clean;
|
|
142
|
+
lines.push(` ${idx + 1}. "${truncated}"`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (highlights.length > MAX_HIGHLIGHTS) {
|
|
147
|
+
lines.push(` ...and ${highlights.length - MAX_HIGHLIGHTS} more`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatRenderUrlResult(result: unknown): string[] | null {
|
|
154
|
+
if (!isRecord(result)) return null;
|
|
155
|
+
if (result.success === false && typeof result.error === "string") {
|
|
156
|
+
return [`error: ${result.error}`];
|
|
157
|
+
}
|
|
158
|
+
if (result.success !== true) return null;
|
|
159
|
+
|
|
160
|
+
const url = typeof result.url === "string" ? result.url : "(unknown url)";
|
|
161
|
+
const lineOffset = typeof result.lineOffset === "number" ? result.lineOffset : undefined;
|
|
162
|
+
const lineLimit = typeof result.lineLimit === "number" ? result.lineLimit : undefined;
|
|
163
|
+
const remainingLines = typeof result.remainingLines === "number" ? result.remainingLines : null;
|
|
164
|
+
const rangeParts: string[] = [];
|
|
165
|
+
if (lineOffset !== undefined) rangeParts.push(`lineOffset=${lineOffset}`);
|
|
166
|
+
if (lineLimit !== undefined) rangeParts.push(`lineLimit=${lineLimit}`);
|
|
167
|
+
rangeParts.push(remainingLines === null ? "remainingLines=unknown" : `remainingLines=${remainingLines}`);
|
|
168
|
+
const remainingSuffix = rangeParts.length > 0 ? ` (${rangeParts.join(", ")})` : "";
|
|
169
|
+
|
|
170
|
+
const header = `${url}${remainingSuffix}`;
|
|
171
|
+
|
|
172
|
+
const text = typeof result.text === "string" ? result.text : "";
|
|
173
|
+
if (!text.trim()) return [header];
|
|
174
|
+
|
|
175
|
+
const MAX_LINES = 4;
|
|
176
|
+
const MAX_CHARS = 160;
|
|
177
|
+
const snippet = normalizeWhitespace(text)
|
|
178
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
179
|
+
.trim()
|
|
180
|
+
.split("\n")
|
|
181
|
+
.slice(0, MAX_LINES)
|
|
182
|
+
.map((l) => (l.length > MAX_CHARS ? `${l.slice(0, MAX_CHARS - 1)}…` : l));
|
|
183
|
+
|
|
184
|
+
return [header, ...snippet];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const fetchUrlsLayout: ToolLayoutConfig = {
|
|
188
|
+
abbreviation: "fetch",
|
|
189
|
+
|
|
190
|
+
getHeader: (input, result): ToolHeader | null => {
|
|
191
|
+
const urlInput = mergeFetchUrlsDefaults(extractFetchUrlsInput(input), result);
|
|
192
|
+
if (!urlInput) return null;
|
|
193
|
+
const headerSuffix = formatFetchUrlsHeader(urlInput);
|
|
194
|
+
return {
|
|
195
|
+
primary: urlInput.url,
|
|
196
|
+
secondary: headerSuffix || undefined,
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
formatResult: formatFetchUrlsResult,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const renderUrlLayout: ToolLayoutConfig = {
|
|
204
|
+
abbreviation: "render",
|
|
205
|
+
|
|
206
|
+
getHeader: (input, result): ToolHeader | null => {
|
|
207
|
+
const urlInput = mergeFetchUrlsDefaults(extractFetchUrlsInput(input), result);
|
|
208
|
+
if (!urlInput) return null;
|
|
209
|
+
const headerSuffix = formatFetchUrlsHeader(urlInput);
|
|
210
|
+
return {
|
|
211
|
+
primary: urlInput.url,
|
|
212
|
+
secondary: headerSuffix || undefined,
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
formatResult: formatRenderUrlResult,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
registerToolLayout("fetchUrls", fetchUrlsLayout);
|
|
220
|
+
registerToolLayout("renderUrl", renderUrlLayout);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ToolLayoutConfig, ToolHeader } from "../types";
|
|
2
|
+
import { registerToolLayout } from "../registry";
|
|
3
|
+
|
|
4
|
+
type UnknownRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SearchInput {
|
|
11
|
+
query: string;
|
|
12
|
+
recency?: string;
|
|
13
|
+
includeDomains?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractSearchInput(input: unknown): SearchInput | null {
|
|
17
|
+
if (!isRecord(input)) return null;
|
|
18
|
+
if (!("query" in input) || typeof input.query !== "string") return null;
|
|
19
|
+
|
|
20
|
+
const recency = "recency" in input && typeof input.recency === "string" ? input.recency : undefined;
|
|
21
|
+
const includeDomains =
|
|
22
|
+
"includeDomains" in input && Array.isArray(input.includeDomains)
|
|
23
|
+
? (input.includeDomains.filter((d) => typeof d === "string") as string[])
|
|
24
|
+
: undefined;
|
|
25
|
+
|
|
26
|
+
return { query: input.query, recency, includeDomains };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ExaLikeItem = {
|
|
30
|
+
title?: unknown;
|
|
31
|
+
url?: unknown;
|
|
32
|
+
text?: unknown;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function extractExaItems(data: unknown): ExaLikeItem[] | null {
|
|
36
|
+
if (!isRecord(data)) return null;
|
|
37
|
+
const direct = data.results;
|
|
38
|
+
if (Array.isArray(direct)) return direct as ExaLikeItem[];
|
|
39
|
+
const contents = data.contents;
|
|
40
|
+
if (Array.isArray(contents)) return contents as ExaLikeItem[];
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractToolDataContainer(result: UnknownRecord): unknown {
|
|
45
|
+
if ("data" in result) return result.data;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatExaItemLabel(item: ExaLikeItem): string {
|
|
50
|
+
const title = typeof item.title === "string" ? item.title : "";
|
|
51
|
+
const url = typeof item.url === "string" ? item.url : "";
|
|
52
|
+
return title || url || "(untitled)";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatWebSearchResult(result: unknown): string[] | null {
|
|
56
|
+
if (!isRecord(result)) return null;
|
|
57
|
+
if (result.success === false && typeof result.error === "string") {
|
|
58
|
+
return [`error: ${result.error}`];
|
|
59
|
+
}
|
|
60
|
+
if (result.success !== true) return null;
|
|
61
|
+
const data = extractToolDataContainer(result);
|
|
62
|
+
const items = extractExaItems(data);
|
|
63
|
+
if (!items) return null;
|
|
64
|
+
|
|
65
|
+
const top = items.slice(0, 4).map((item, idx) => {
|
|
66
|
+
const url = typeof item.url === "string" ? item.url : "";
|
|
67
|
+
const title = typeof item.title === "string" ? item.title : "";
|
|
68
|
+
const label = formatExaItemLabel(item);
|
|
69
|
+
const urlSuffix = url && title ? ` — ${url}` : "";
|
|
70
|
+
return `${idx + 1}) ${label}${urlSuffix}`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return top.length > 0 ? top : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatSearchParams(input: SearchInput): string | undefined {
|
|
77
|
+
const parts: string[] = [];
|
|
78
|
+
if (input.recency) {
|
|
79
|
+
parts.push(`recency: ${input.recency}`);
|
|
80
|
+
}
|
|
81
|
+
if (input.includeDomains && input.includeDomains.length > 0) {
|
|
82
|
+
const domains = input.includeDomains.slice(0, 2).join(", ");
|
|
83
|
+
const suffix = input.includeDomains.length > 2 ? ` +${input.includeDomains.length - 2}` : "";
|
|
84
|
+
parts.push(`domains: ${domains}${suffix}`);
|
|
85
|
+
}
|
|
86
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const MAX_INLINE_CHARS = 60;
|
|
90
|
+
|
|
91
|
+
export const webSearchLayout: ToolLayoutConfig = {
|
|
92
|
+
abbreviation: "search",
|
|
93
|
+
|
|
94
|
+
getHeader: (input): ToolHeader | null => {
|
|
95
|
+
const searchInput = extractSearchInput(input);
|
|
96
|
+
if (!searchInput) return null;
|
|
97
|
+
const truncated =
|
|
98
|
+
searchInput.query.length > MAX_INLINE_CHARS
|
|
99
|
+
? `${searchInput.query.slice(0, MAX_INLINE_CHARS - 1)}…`
|
|
100
|
+
: searchInput.query;
|
|
101
|
+
return {
|
|
102
|
+
primary: `"${truncated}"`,
|
|
103
|
+
secondary: formatSearchParams(searchInput),
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
formatResult: formatWebSearchResult,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
registerToolLayout("webSearch", webSearchLayout);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ToolLayoutConfig, ToolLayoutRegistry } from "./types";
|
|
2
|
+
|
|
3
|
+
const registry: ToolLayoutRegistry = new Map();
|
|
4
|
+
|
|
5
|
+
export function registerToolLayout(toolName: string, config: ToolLayoutConfig): void {
|
|
6
|
+
registry.set(toolName, config);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getToolLayout(toolName: string): ToolLayoutConfig | undefined {
|
|
10
|
+
return registry.get(toolName);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hasToolLayout(toolName: string): boolean {
|
|
14
|
+
return registry.has(toolName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { registry };
|