botholomew 0.15.0 → 0.15.1
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/README.md +2 -0
- package/package.json +3 -2
- package/src/commands/chat.ts +4 -0
- package/src/config/schemas.ts +2 -0
- package/src/context/fetcher.ts +1 -1
- package/src/tui/App.tsx +35 -0
- package/src/tui/components/InputBar.tsx +6 -4
- package/src/tui/components/Logo.tsx +15 -5
- package/src/tui/components/SleepProgress.tsx +5 -2
- package/src/tui/components/StatusBar.tsx +9 -6
- package/src/tui/idle.tsx +68 -0
- package/src/utils/frontmatter.ts +1 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botholomew",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "An autonomous AI agent for knowledge work — works your task queue while you sleep.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"botholomew": "./src/cli.ts"
|
|
7
|
+
"botholomew": "./src/cli.ts",
|
|
8
|
+
"bothy": "./src/cli.ts"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"src",
|
package/src/commands/chat.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
import { loadConfig } from "../config/loader.ts";
|
|
2
3
|
|
|
3
4
|
export function registerChatCommand(program: Command) {
|
|
4
5
|
program
|
|
@@ -29,6 +30,8 @@ export function registerChatCommand(program: Command) {
|
|
|
29
30
|
const React = await import("react");
|
|
30
31
|
const { App } = await import("../tui/App.tsx");
|
|
31
32
|
const dir = program.opts().dir;
|
|
33
|
+
const config = await loadConfig(dir);
|
|
34
|
+
const idleTimeoutMs = config.tui_idle_timeout_seconds * 1000;
|
|
32
35
|
|
|
33
36
|
// VHS/ttyd doesn't fully negotiate the Kitty Keyboard protocol, so
|
|
34
37
|
// Ink's "enabled" mode drops non-text keystrokes (Tab, Escape) under
|
|
@@ -41,6 +44,7 @@ export function registerChatCommand(program: Command) {
|
|
|
41
44
|
projectDir: dir,
|
|
42
45
|
threadId: opts.threadId,
|
|
43
46
|
initialPrompt: opts.prompt,
|
|
47
|
+
idleTimeoutMs,
|
|
44
48
|
}),
|
|
45
49
|
{
|
|
46
50
|
exitOnCtrlC: false,
|
package/src/config/schemas.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface BotholomewConfig {
|
|
|
14
14
|
worker_stopped_retention_seconds?: number;
|
|
15
15
|
schedule_min_interval_seconds?: number;
|
|
16
16
|
schedule_claim_stale_seconds?: number;
|
|
17
|
+
tui_idle_timeout_seconds?: number;
|
|
17
18
|
log_level?: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -33,5 +34,6 @@ export const DEFAULT_CONFIG: Required<BotholomewConfig> = {
|
|
|
33
34
|
worker_stopped_retention_seconds: 3600,
|
|
34
35
|
schedule_min_interval_seconds: 60,
|
|
35
36
|
schedule_claim_stale_seconds: 300,
|
|
37
|
+
tui_idle_timeout_seconds: 180,
|
|
36
38
|
log_level: "",
|
|
37
39
|
};
|
package/src/context/fetcher.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface FetchedContent {
|
|
|
39
39
|
/**
|
|
40
40
|
* MCP server that produced the content (e.g. "google-docs", "github",
|
|
41
41
|
* "firecrawl"), or null when we fell back to a plain HTTP fetch. Useful
|
|
42
|
-
* for `
|
|
42
|
+
* for `botholomew context import` to pick a default destination subdirectory.
|
|
43
43
|
*/
|
|
44
44
|
source: string | null;
|
|
45
45
|
}
|
package/src/tui/App.tsx
CHANGED
|
@@ -33,6 +33,7 @@ import { ThreadPanel } from "./components/ThreadPanel.tsx";
|
|
|
33
33
|
import type { ToolCallData } from "./components/ToolCall.tsx";
|
|
34
34
|
import { ToolPanel } from "./components/ToolPanel.tsx";
|
|
35
35
|
import { WorkerPanel } from "./components/WorkerPanel.tsx";
|
|
36
|
+
import { IdleProvider, useIdle } from "./idle.tsx";
|
|
36
37
|
import { buildSlashCommands, getSlashMatches } from "./slashCompletion.ts";
|
|
37
38
|
import { ansi } from "./theme.ts";
|
|
38
39
|
|
|
@@ -40,6 +41,7 @@ interface AppProps {
|
|
|
40
41
|
projectDir: string;
|
|
41
42
|
threadId?: string;
|
|
42
43
|
initialPrompt?: string;
|
|
44
|
+
idleTimeoutMs: number;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
let nextMsgId = 0;
|
|
@@ -138,8 +140,32 @@ export function App({
|
|
|
138
140
|
projectDir,
|
|
139
141
|
threadId: resumeThreadId,
|
|
140
142
|
initialPrompt,
|
|
143
|
+
idleTimeoutMs,
|
|
141
144
|
}: AppProps) {
|
|
145
|
+
return (
|
|
146
|
+
<IdleProvider timeoutMs={idleTimeoutMs}>
|
|
147
|
+
<AppInner
|
|
148
|
+
projectDir={projectDir}
|
|
149
|
+
threadId={resumeThreadId}
|
|
150
|
+
initialPrompt={initialPrompt}
|
|
151
|
+
/>
|
|
152
|
+
</IdleProvider>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface AppInnerProps {
|
|
157
|
+
projectDir: string;
|
|
158
|
+
threadId?: string;
|
|
159
|
+
initialPrompt?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function AppInner({
|
|
163
|
+
projectDir,
|
|
164
|
+
threadId: resumeThreadId,
|
|
165
|
+
initialPrompt,
|
|
166
|
+
}: AppInnerProps) {
|
|
142
167
|
const { exit } = useApp();
|
|
168
|
+
const { markActivity } = useIdle();
|
|
143
169
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
144
170
|
const [messagesEpoch, setMessagesEpoch] = useState(0);
|
|
145
171
|
const [inputValue, setInputValue] = useState("");
|
|
@@ -265,9 +291,14 @@ export function App({
|
|
|
265
291
|
const slashCommandsRef = useRef<SlashCommand[]>([]);
|
|
266
292
|
const inputValueRef = useRef("");
|
|
267
293
|
|
|
294
|
+
const markActivityRef = useRef(markActivity);
|
|
295
|
+
markActivityRef.current = markActivity;
|
|
296
|
+
|
|
268
297
|
const stableAppHandler = useCallback(
|
|
269
298
|
// biome-ignore lint/suspicious/noExplicitAny: Ink's Key type is not exported
|
|
270
299
|
(input: string, key: any) => {
|
|
300
|
+
markActivityRef.current();
|
|
301
|
+
|
|
271
302
|
// Ctrl+C exits
|
|
272
303
|
if (input === "c" && key.ctrl) {
|
|
273
304
|
exit();
|
|
@@ -407,12 +438,15 @@ export function App({
|
|
|
407
438
|
if (now - lastStreamFlush >= 50) {
|
|
408
439
|
setStreamingText(currentText);
|
|
409
440
|
lastStreamFlush = now;
|
|
441
|
+
markActivityRef.current();
|
|
410
442
|
}
|
|
411
443
|
},
|
|
412
444
|
onToolPreparing: (id, name) => {
|
|
445
|
+
markActivityRef.current();
|
|
413
446
|
setPreparingTool({ id, name });
|
|
414
447
|
},
|
|
415
448
|
onToolStart: (id, name, input) => {
|
|
449
|
+
markActivityRef.current();
|
|
416
450
|
if (currentText) {
|
|
417
451
|
finalizeSegment();
|
|
418
452
|
}
|
|
@@ -428,6 +462,7 @@ export function App({
|
|
|
428
462
|
setPreparingTool(null);
|
|
429
463
|
},
|
|
430
464
|
onToolEnd: (id, _name, output, isError, meta) => {
|
|
465
|
+
markActivityRef.current();
|
|
431
466
|
const tc = pendingToolCalls.find((t) => t.id === id);
|
|
432
467
|
if (tc) {
|
|
433
468
|
tc.running = false;
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useState,
|
|
10
10
|
} from "react";
|
|
11
11
|
import type { SlashCommand } from "../../skills/commands.ts";
|
|
12
|
+
import { useIdle } from "../idle.tsx";
|
|
12
13
|
import { getSlashMatches, shouldSubmitOnEnter } from "../slashCompletion.ts";
|
|
13
14
|
import { SlashCommandPopup } from "./SlashCommandPopup.tsx";
|
|
14
15
|
|
|
@@ -38,6 +39,7 @@ export const InputBar = memo(function InputBar({
|
|
|
38
39
|
const [popupDismissed, setPopupDismissed] = useState(false);
|
|
39
40
|
const savedInput = useRef("");
|
|
40
41
|
const lastActivity = useRef(Date.now());
|
|
42
|
+
const { isIdle } = useIdle();
|
|
41
43
|
|
|
42
44
|
// Refs for values read inside the input handler — eagerly updated so rapid
|
|
43
45
|
// keystrokes that arrive before React re-renders always see fresh state.
|
|
@@ -94,7 +96,7 @@ export const InputBar = memo(function InputBar({
|
|
|
94
96
|
// Blink cursor when input is active — skip ticks while typing so the
|
|
95
97
|
// cursor stays solid and we avoid unnecessary renders during rapid input.
|
|
96
98
|
useEffect(() => {
|
|
97
|
-
if (disabled) {
|
|
99
|
+
if (disabled || isIdle) {
|
|
98
100
|
setCursorVisible(true);
|
|
99
101
|
return;
|
|
100
102
|
}
|
|
@@ -105,7 +107,7 @@ export const InputBar = memo(function InputBar({
|
|
|
105
107
|
setCursorVisible((prev) => (prev === phase ? prev : phase));
|
|
106
108
|
}, 530);
|
|
107
109
|
return () => clearInterval(id);
|
|
108
|
-
}, [disabled]);
|
|
110
|
+
}, [disabled, isIdle]);
|
|
109
111
|
|
|
110
112
|
// Stable input handler — the callback reference never changes, which
|
|
111
113
|
// prevents Ink's useInput from removing/re-adding the stdin listener on
|
|
@@ -337,14 +339,14 @@ export const InputBar = memo(function InputBar({
|
|
|
337
339
|
<Box
|
|
338
340
|
flexDirection="column"
|
|
339
341
|
borderStyle="single"
|
|
340
|
-
borderColor={disabled ? "gray" : "green"}
|
|
342
|
+
borderColor={disabled || isIdle ? "gray" : "green"}
|
|
341
343
|
paddingX={1}
|
|
342
344
|
>
|
|
343
345
|
{header}
|
|
344
346
|
{!disabled && (
|
|
345
347
|
<Box flexDirection="column">
|
|
346
348
|
<Box>
|
|
347
|
-
<Text color="green">{"› "}</Text>
|
|
349
|
+
<Text color={isIdle ? "gray" : "green"}>{"› "}</Text>
|
|
348
350
|
{placeholder ? (
|
|
349
351
|
<Text dimColor>Type a message...</Text>
|
|
350
352
|
) : (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
+
import { useIdle } from "../idle.tsx";
|
|
3
4
|
import { theme } from "../theme.ts";
|
|
4
5
|
|
|
5
6
|
const STARTUP_FRAMES = [
|
|
@@ -23,8 +24,10 @@ const IDLE_MS = 2000;
|
|
|
23
24
|
export function AnimatedLogo() {
|
|
24
25
|
const [frameIndex, setFrameIndex] = useState(0);
|
|
25
26
|
const [startupDone, setStartupDone] = useState(false);
|
|
27
|
+
const { isIdle } = useIdle();
|
|
26
28
|
|
|
27
29
|
useEffect(() => {
|
|
30
|
+
if (isIdle) return;
|
|
28
31
|
const interval = setInterval(
|
|
29
32
|
() => {
|
|
30
33
|
setFrameIndex((prev) => {
|
|
@@ -42,20 +45,21 @@ export function AnimatedLogo() {
|
|
|
42
45
|
startupDone ? IDLE_MS : STARTUP_MS,
|
|
43
46
|
);
|
|
44
47
|
return () => clearInterval(interval);
|
|
45
|
-
}, [startupDone]);
|
|
48
|
+
}, [startupDone, isIdle]);
|
|
46
49
|
|
|
47
50
|
const frames = startupDone ? IDLE_FRAMES : STARTUP_FRAMES;
|
|
48
51
|
// biome-ignore lint: frameIndex is always in bounds
|
|
49
52
|
const frame = frames[frameIndex]!;
|
|
53
|
+
const color = isIdle ? "gray" : theme.accent;
|
|
50
54
|
|
|
51
55
|
return (
|
|
52
56
|
<Box flexDirection="column" alignItems="center" justifyContent="center">
|
|
53
57
|
{frame.map((line) => (
|
|
54
|
-
<Text key={line} color={
|
|
58
|
+
<Text key={line} color={color}>
|
|
55
59
|
{line}
|
|
56
60
|
</Text>
|
|
57
61
|
))}
|
|
58
|
-
<Text bold color={
|
|
62
|
+
<Text bold color={color}>
|
|
59
63
|
Botholomew
|
|
60
64
|
</Text>
|
|
61
65
|
<Text dimColor>Starting chat session...</Text>
|
|
@@ -67,13 +71,19 @@ const CHAR_FRAMES = ["{o,o}", "{o,o}", "{-,-}", "{o,o}"];
|
|
|
67
71
|
|
|
68
72
|
export function LogoChar() {
|
|
69
73
|
const [frameIndex, setFrameIndex] = useState(0);
|
|
74
|
+
const { isIdle } = useIdle();
|
|
70
75
|
|
|
71
76
|
useEffect(() => {
|
|
77
|
+
if (isIdle) return;
|
|
72
78
|
const interval = setInterval(() => {
|
|
73
79
|
setFrameIndex((prev) => (prev + 1) % CHAR_FRAMES.length);
|
|
74
80
|
}, IDLE_MS);
|
|
75
81
|
return () => clearInterval(interval);
|
|
76
|
-
}, []);
|
|
82
|
+
}, [isIdle]);
|
|
77
83
|
|
|
78
|
-
return
|
|
84
|
+
return (
|
|
85
|
+
<Text color={isIdle ? "gray" : theme.accent}>
|
|
86
|
+
{CHAR_FRAMES[frameIndex]}{" "}
|
|
87
|
+
</Text>
|
|
88
|
+
);
|
|
79
89
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Box, Text } from "ink";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
+
import { useIdle } from "../idle.tsx";
|
|
3
4
|
import { theme } from "../theme.ts";
|
|
4
5
|
|
|
5
6
|
interface SleepProgressProps {
|
|
@@ -17,11 +18,13 @@ export function SleepProgress({
|
|
|
17
18
|
reason,
|
|
18
19
|
}: SleepProgressProps) {
|
|
19
20
|
const [now, setNow] = useState(() => Date.now());
|
|
21
|
+
const { isIdle } = useIdle();
|
|
20
22
|
|
|
21
23
|
useEffect(() => {
|
|
24
|
+
if (isIdle) return;
|
|
22
25
|
const id = setInterval(() => setNow(Date.now()), TICK_MS);
|
|
23
26
|
return () => clearInterval(id);
|
|
24
|
-
}, []);
|
|
27
|
+
}, [isIdle]);
|
|
25
28
|
|
|
26
29
|
const totalMs = totalSeconds * 1000;
|
|
27
30
|
const elapsedMs = Math.min(totalMs, Math.max(0, now - startedAt.getTime()));
|
|
@@ -34,7 +37,7 @@ export function SleepProgress({
|
|
|
34
37
|
<Box flexDirection="column">
|
|
35
38
|
<Box>
|
|
36
39
|
<Text dimColor>{" "}</Text>
|
|
37
|
-
<Text color={theme.accent}>{bar}</Text>
|
|
40
|
+
<Text color={isIdle ? "gray" : theme.accent}>{bar}</Text>
|
|
38
41
|
<Text dimColor>
|
|
39
42
|
{" "}
|
|
40
43
|
{elapsedSec}s / {totalSeconds}s
|
|
@@ -2,6 +2,7 @@ import { Box, Text } from "ink";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { listTasks } from "../../tasks/store.ts";
|
|
4
4
|
import { listWorkers } from "../../workers/store.ts";
|
|
5
|
+
import { useIdle } from "../idle.tsx";
|
|
5
6
|
import { LogoChar } from "./Logo.tsx";
|
|
6
7
|
|
|
7
8
|
interface StatusBarProps {
|
|
@@ -32,8 +33,10 @@ export function StatusBar({
|
|
|
32
33
|
pendingCount: 0,
|
|
33
34
|
inProgressCount: 0,
|
|
34
35
|
});
|
|
36
|
+
const { isIdle } = useIdle();
|
|
35
37
|
|
|
36
38
|
useEffect(() => {
|
|
39
|
+
if (isIdle) return;
|
|
37
40
|
let mounted = true;
|
|
38
41
|
|
|
39
42
|
// Errors here (e.g. transient DuckDB lock conflicts while a freshly
|
|
@@ -66,32 +69,32 @@ export function StatusBar({
|
|
|
66
69
|
mounted = false;
|
|
67
70
|
clearInterval(interval);
|
|
68
71
|
};
|
|
69
|
-
}, [projectDir, onWorkerStatusChange]);
|
|
72
|
+
}, [projectDir, onWorkerStatusChange, isIdle]);
|
|
70
73
|
|
|
71
74
|
return (
|
|
72
75
|
<Box paddingX={0}>
|
|
73
76
|
<LogoChar />
|
|
74
|
-
<Text bold color="blue">
|
|
77
|
+
<Text bold color={isIdle ? "gray" : "blue"}>
|
|
75
78
|
Botholomew
|
|
76
79
|
</Text>
|
|
77
80
|
{chatTitle && (
|
|
78
81
|
<>
|
|
79
82
|
<Text dimColor> | </Text>
|
|
80
|
-
<Text color="cyan" bold italic>
|
|
83
|
+
<Text color={isIdle ? "gray" : "cyan"} bold italic>
|
|
81
84
|
{chatTitle.length > 30 ? `${chatTitle.slice(0, 29)}…` : chatTitle}
|
|
82
85
|
</Text>
|
|
83
86
|
</>
|
|
84
87
|
)}
|
|
85
88
|
<Text dimColor> | </Text>
|
|
86
89
|
{status.workerCount > 0 ? (
|
|
87
|
-
<Text color="green">
|
|
90
|
+
<Text color={isIdle ? "gray" : "green"}>
|
|
88
91
|
{status.workerCount} worker{status.workerCount === 1 ? "" : "s"}
|
|
89
92
|
</Text>
|
|
90
93
|
) : (
|
|
91
|
-
<Text color="yellow">no workers</Text>
|
|
94
|
+
<Text color={isIdle ? "gray" : "yellow"}>no workers</Text>
|
|
92
95
|
)}
|
|
93
96
|
<Text dimColor> | </Text>
|
|
94
|
-
<Text>
|
|
97
|
+
<Text dimColor={isIdle}>
|
|
95
98
|
{status.pendingCount} pending, {status.inProgressCount} active
|
|
96
99
|
</Text>
|
|
97
100
|
</Box>
|
package/src/tui/idle.tsx
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
const CHECK_INTERVAL_MS = 10_000;
|
|
12
|
+
|
|
13
|
+
export function shouldBeIdle(
|
|
14
|
+
lastActivity: number,
|
|
15
|
+
now: number,
|
|
16
|
+
timeoutMs: number,
|
|
17
|
+
): boolean {
|
|
18
|
+
if (timeoutMs <= 0) return false;
|
|
19
|
+
return now - lastActivity >= timeoutMs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface IdleContextValue {
|
|
23
|
+
isIdle: boolean;
|
|
24
|
+
markActivity: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const IdleContext = createContext<IdleContextValue>({
|
|
28
|
+
isIdle: false,
|
|
29
|
+
markActivity: () => {},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
interface IdleProviderProps {
|
|
33
|
+
timeoutMs: number;
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function IdleProvider({ timeoutMs, children }: IdleProviderProps) {
|
|
38
|
+
const lastActivityRef = useRef(Date.now());
|
|
39
|
+
const [isIdle, setIsIdle] = useState(false);
|
|
40
|
+
const isIdleRef = useRef(isIdle);
|
|
41
|
+
isIdleRef.current = isIdle;
|
|
42
|
+
|
|
43
|
+
const markActivity = useCallback(() => {
|
|
44
|
+
lastActivityRef.current = Date.now();
|
|
45
|
+
if (isIdleRef.current) {
|
|
46
|
+
setIsIdle(false);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (timeoutMs <= 0) return;
|
|
52
|
+
const id = setInterval(() => {
|
|
53
|
+
const idle = shouldBeIdle(lastActivityRef.current, Date.now(), timeoutMs);
|
|
54
|
+
setIsIdle((prev) => (prev === idle ? prev : idle));
|
|
55
|
+
}, CHECK_INTERVAL_MS);
|
|
56
|
+
return () => clearInterval(id);
|
|
57
|
+
}, [timeoutMs]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<IdleContext.Provider value={{ isIdle, markActivity }}>
|
|
61
|
+
{children}
|
|
62
|
+
</IdleContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useIdle(): IdleContextValue {
|
|
67
|
+
return useContext(IdleContext);
|
|
68
|
+
}
|
package/src/utils/frontmatter.ts
CHANGED
|
@@ -3,7 +3,7 @@ import matter from "gray-matter";
|
|
|
3
3
|
export interface ContextFileMeta {
|
|
4
4
|
loading?: "always" | "contextual";
|
|
5
5
|
"agent-modification"?: boolean;
|
|
6
|
-
// Set by `
|
|
6
|
+
// Set by `botholomew context import <url>` so the saved file remembers
|
|
7
7
|
// where it came from. Optional so files written by other paths
|
|
8
8
|
// (prompts/, beliefs/, agent-authored notes) aren't required to
|
|
9
9
|
// carry import metadata.
|