@amanm/openpaw 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/AGENTS.md +1 -0
- package/README.md +144 -0
- package/agent/agent.ts +217 -0
- package/agent/context-scan.ts +81 -0
- package/agent/file-editor-store.ts +27 -0
- package/agent/index.ts +31 -0
- package/agent/memory-store.ts +404 -0
- package/agent/model.ts +14 -0
- package/agent/prompt-builder.ts +139 -0
- package/agent/prompt-context-files.ts +151 -0
- package/agent/sandbox-paths.ts +52 -0
- package/agent/session-store.ts +80 -0
- package/agent/skill-catalog.ts +25 -0
- package/agent/skills/discover.ts +100 -0
- package/agent/tool-stream-format.ts +126 -0
- package/agent/tool-yaml-like.ts +96 -0
- package/agent/tools/bash.ts +100 -0
- package/agent/tools/file-editor.ts +293 -0
- package/agent/tools/list-dir.ts +58 -0
- package/agent/tools/load-skill.ts +40 -0
- package/agent/tools/memory.ts +84 -0
- package/agent/turn-context.ts +46 -0
- package/agent/types.ts +37 -0
- package/agent/workspace-bootstrap.ts +98 -0
- package/bin/openpaw.cjs +177 -0
- package/bundled-skills/find-skills/SKILL.md +163 -0
- package/cli/components/chat-app.tsx +759 -0
- package/cli/components/onboard-ui.tsx +325 -0
- package/cli/components/theme.ts +16 -0
- package/cli/configure.tsx +0 -0
- package/cli/lib/chat-transcript-types.ts +11 -0
- package/cli/lib/markdown-render-node.ts +523 -0
- package/cli/lib/onboard-markdown-syntax-style.ts +55 -0
- package/cli/lib/ui-messages-to-chat-transcript.ts +157 -0
- package/cli/lib/use-auto-copy-selection.ts +38 -0
- package/cli/onboard.tsx +248 -0
- package/cli/openpaw.tsx +144 -0
- package/cli/reset.ts +12 -0
- package/cli/tui.tsx +31 -0
- package/config/index.ts +3 -0
- package/config/paths.ts +71 -0
- package/config/personality-copy.ts +68 -0
- package/config/storage.ts +80 -0
- package/config/types.ts +37 -0
- package/gateway/bootstrap.ts +25 -0
- package/gateway/channel-adapter.ts +8 -0
- package/gateway/daemon-manager.ts +191 -0
- package/gateway/index.ts +18 -0
- package/gateway/session-key.ts +13 -0
- package/gateway/slash-command-tokens.ts +39 -0
- package/gateway/start-messaging.ts +40 -0
- package/gateway/telegram/active-thread-store.ts +89 -0
- package/gateway/telegram/adapter.ts +290 -0
- package/gateway/telegram/assistant-markdown.ts +48 -0
- package/gateway/telegram/bot-commands.ts +40 -0
- package/gateway/telegram/chat-preferences.ts +100 -0
- package/gateway/telegram/constants.ts +5 -0
- package/gateway/telegram/index.ts +4 -0
- package/gateway/telegram/message-html.ts +138 -0
- package/gateway/telegram/message-queue.ts +19 -0
- package/gateway/telegram/reserved-command-filter.ts +33 -0
- package/gateway/telegram/session-file-discovery.ts +62 -0
- package/gateway/telegram/session-key.ts +13 -0
- package/gateway/telegram/session-label.ts +14 -0
- package/gateway/telegram/sessions-list-reply.ts +39 -0
- package/gateway/telegram/stream-delivery.ts +618 -0
- package/gateway/tui/constants.ts +2 -0
- package/gateway/tui/tui-active-thread-store.ts +103 -0
- package/gateway/tui/tui-session-discovery.ts +94 -0
- package/gateway/tui/tui-session-label.ts +22 -0
- package/gateway/tui/tui-sessions-list-message.ts +37 -0
- package/package.json +52 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { TextAttributes, type SelectOption } from "@opentui/core";
|
|
2
|
+
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
3
|
+
import { type ReactNode, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import type { OpenPawConfig } from "../../config";
|
|
5
|
+
import { ONBOARD } from "./theme";
|
|
6
|
+
|
|
7
|
+
export { ONBOARD };
|
|
8
|
+
|
|
9
|
+
export function OnboardScreenLayout({ children }: { children: ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<box flexGrow={1} alignItems="center" justifyContent="center">
|
|
12
|
+
<box flexDirection="column" gap={1} width={ONBOARD.colWidth}>
|
|
13
|
+
{children}
|
|
14
|
+
</box>
|
|
15
|
+
</box>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function FooterHints({
|
|
20
|
+
variant,
|
|
21
|
+
hasBack,
|
|
22
|
+
}: {
|
|
23
|
+
variant: "input" | "menu";
|
|
24
|
+
hasBack: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
const lines =
|
|
27
|
+
variant === "input"
|
|
28
|
+
? hasBack
|
|
29
|
+
? [
|
|
30
|
+
"Press Enter to continue",
|
|
31
|
+
"Ctrl+Backspace to go back",
|
|
32
|
+
"Esc to exit",
|
|
33
|
+
]
|
|
34
|
+
: ["Press Enter to continue", "Esc to exit"]
|
|
35
|
+
: hasBack
|
|
36
|
+
? [
|
|
37
|
+
"Up/Down to move",
|
|
38
|
+
"Enter to select",
|
|
39
|
+
"Ctrl+Backspace to go back",
|
|
40
|
+
"Esc to exit",
|
|
41
|
+
]
|
|
42
|
+
: ["Up/Down to move", "Enter to select", "Esc to exit"];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<box flexDirection="column">
|
|
46
|
+
{lines.map((line) => (
|
|
47
|
+
<text key={line} fg={ONBOARD.hint}>
|
|
48
|
+
{line}
|
|
49
|
+
</text>
|
|
50
|
+
))}
|
|
51
|
+
</box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Vertical list using built-in `select` (navigation + selection handled by OpenTUI). */
|
|
56
|
+
export function OnboardSelect({
|
|
57
|
+
items,
|
|
58
|
+
onSelect,
|
|
59
|
+
onBack,
|
|
60
|
+
}: {
|
|
61
|
+
items: string[];
|
|
62
|
+
onSelect: (index: number) => void;
|
|
63
|
+
onBack?: () => void;
|
|
64
|
+
}) {
|
|
65
|
+
const renderer = useRenderer();
|
|
66
|
+
const options: SelectOption[] = useMemo(
|
|
67
|
+
() =>
|
|
68
|
+
items.map((name) => ({
|
|
69
|
+
name,
|
|
70
|
+
description: "",
|
|
71
|
+
})),
|
|
72
|
+
[items],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
useKeyboard((key) => {
|
|
76
|
+
if (key.name === "escape") {
|
|
77
|
+
renderer.destroy();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.ctrl && key.name === "backspace" && onBack) {
|
|
81
|
+
onBack();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const rowCount = items.length;
|
|
86
|
+
const height = Math.min(Math.max(rowCount, 2), 14);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<select
|
|
90
|
+
focused
|
|
91
|
+
width={ONBOARD.colWidth}
|
|
92
|
+
height={height}
|
|
93
|
+
options={options}
|
|
94
|
+
showDescription={false}
|
|
95
|
+
wrapSelection={false}
|
|
96
|
+
showScrollIndicator={rowCount > height}
|
|
97
|
+
selectedBackgroundColor="#414868"
|
|
98
|
+
selectedTextColor={ONBOARD.accent}
|
|
99
|
+
textColor={ONBOARD.text}
|
|
100
|
+
backgroundColor="transparent"
|
|
101
|
+
onSelect={(index) => {
|
|
102
|
+
if (index >= 0 && index < items.length) {
|
|
103
|
+
onSelect(index);
|
|
104
|
+
}
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function WelcomeScreen({ onComplete }: { onComplete: () => void }) {
|
|
111
|
+
const [countdown, setCountdown] = useState(1);
|
|
112
|
+
const renderer = useRenderer();
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const timer = setInterval(() => {
|
|
116
|
+
setCountdown((c) => {
|
|
117
|
+
if (c <= 1) {
|
|
118
|
+
clearInterval(timer);
|
|
119
|
+
onComplete();
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
return c - 1;
|
|
123
|
+
});
|
|
124
|
+
}, 1000);
|
|
125
|
+
return () => clearInterval(timer);
|
|
126
|
+
}, [onComplete]);
|
|
127
|
+
|
|
128
|
+
useKeyboard((key) => {
|
|
129
|
+
if (key.name === "escape") {
|
|
130
|
+
renderer.destroy();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<box alignItems="center" justifyContent="center" flexGrow={1}>
|
|
136
|
+
<box flexDirection="column" alignItems="center" gap={1}>
|
|
137
|
+
<ascii-font font="tiny" text="OpenPaw" color={ONBOARD.accent} />
|
|
138
|
+
<text attributes={TextAttributes.DIM}>Welcome to OpenPaw</text>
|
|
139
|
+
<text fg={ONBOARD.hint}>Starting in {countdown}...</text>
|
|
140
|
+
</box>
|
|
141
|
+
</box>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function InputScreen({
|
|
146
|
+
title,
|
|
147
|
+
label,
|
|
148
|
+
value,
|
|
149
|
+
onChange,
|
|
150
|
+
onSubmit,
|
|
151
|
+
onBack,
|
|
152
|
+
placeholder,
|
|
153
|
+
password,
|
|
154
|
+
}: {
|
|
155
|
+
title: string;
|
|
156
|
+
label: string;
|
|
157
|
+
value: string;
|
|
158
|
+
onChange: (v: string) => void;
|
|
159
|
+
onSubmit: () => void;
|
|
160
|
+
onBack?: () => void;
|
|
161
|
+
placeholder?: string;
|
|
162
|
+
password?: boolean;
|
|
163
|
+
}) {
|
|
164
|
+
const renderer = useRenderer();
|
|
165
|
+
const [error, setError] = useState(false);
|
|
166
|
+
|
|
167
|
+
useKeyboard((key) => {
|
|
168
|
+
if (key.name === "escape") {
|
|
169
|
+
renderer.destroy();
|
|
170
|
+
}
|
|
171
|
+
if (key.ctrl && key.name === "backspace" && onBack) {
|
|
172
|
+
onBack();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const handleSubmit = (submitted: string) => {
|
|
177
|
+
if (submitted.trim() === "") {
|
|
178
|
+
setError(true);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setError(false);
|
|
182
|
+
if (submitted !== value) {
|
|
183
|
+
onChange(submitted);
|
|
184
|
+
}
|
|
185
|
+
onSubmit();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<OnboardScreenLayout>
|
|
190
|
+
<text fg={ONBOARD.accent}>
|
|
191
|
+
<strong>{title}</strong>
|
|
192
|
+
</text>
|
|
193
|
+
<text fg={ONBOARD.text}>{label}</text>
|
|
194
|
+
<input
|
|
195
|
+
value={value}
|
|
196
|
+
onChange={(v) => {
|
|
197
|
+
setError(false);
|
|
198
|
+
onChange(v);
|
|
199
|
+
}}
|
|
200
|
+
onSubmit={(payload) =>
|
|
201
|
+
handleSubmit(typeof payload === "string" ? payload : value)
|
|
202
|
+
}
|
|
203
|
+
placeholder={placeholder || ""}
|
|
204
|
+
focused
|
|
205
|
+
width={ONBOARD.inputWidth}
|
|
206
|
+
textColor={ONBOARD.text}
|
|
207
|
+
cursorColor={ONBOARD.accent}
|
|
208
|
+
{...(password ? { password: true } : {})}
|
|
209
|
+
/>
|
|
210
|
+
{error && <text fg={ONBOARD.error}>This field is required</text>}
|
|
211
|
+
<box flexDirection="column" marginTop={1}>
|
|
212
|
+
<FooterHints variant="input" hasBack={!!onBack} />
|
|
213
|
+
</box>
|
|
214
|
+
</OnboardScreenLayout>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function PersonalityScreen({
|
|
219
|
+
options,
|
|
220
|
+
onSelect,
|
|
221
|
+
onBack,
|
|
222
|
+
}: {
|
|
223
|
+
options: string[];
|
|
224
|
+
onSelect: (index: number) => void;
|
|
225
|
+
onBack?: () => void;
|
|
226
|
+
}) {
|
|
227
|
+
return (
|
|
228
|
+
<OnboardScreenLayout>
|
|
229
|
+
<text fg={ONBOARD.accent}>
|
|
230
|
+
<strong>Personality</strong>
|
|
231
|
+
</text>
|
|
232
|
+
<text fg={ONBOARD.text}>Choose a personality:</text>
|
|
233
|
+
<OnboardSelect items={options} onSelect={onSelect} onBack={onBack} />
|
|
234
|
+
<box flexDirection="column" marginTop={1}>
|
|
235
|
+
<FooterHints variant="menu" hasBack={!!onBack} />
|
|
236
|
+
</box>
|
|
237
|
+
</OnboardScreenLayout>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function maskSecret(s: string, maxStars = 20) {
|
|
242
|
+
return "*".repeat(Math.min(s.length, maxStars));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function ConfirmScreen({
|
|
246
|
+
config,
|
|
247
|
+
onConfirm,
|
|
248
|
+
onRestart,
|
|
249
|
+
onBack,
|
|
250
|
+
}: {
|
|
251
|
+
config: OpenPawConfig;
|
|
252
|
+
onConfirm: () => void;
|
|
253
|
+
onRestart: () => void;
|
|
254
|
+
onBack?: () => void;
|
|
255
|
+
}) {
|
|
256
|
+
const menuItems = ["Save and continue", "Start over"];
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<OnboardScreenLayout>
|
|
260
|
+
<text fg={ONBOARD.accent}>
|
|
261
|
+
<strong>Confirm configuration</strong>
|
|
262
|
+
</text>
|
|
263
|
+
<box flexDirection="column" gap={0}>
|
|
264
|
+
<text fg={ONBOARD.text}>
|
|
265
|
+
<strong>Provider</strong>
|
|
266
|
+
</text>
|
|
267
|
+
<text fg={ONBOARD.muted}> Base URL: {config.provider.baseUrl}</text>
|
|
268
|
+
<text fg={ONBOARD.muted}> Model: {config.provider.model}</text>
|
|
269
|
+
<text fg={ONBOARD.muted}>
|
|
270
|
+
{" "}API Key: {maskSecret(config.provider.apiKey)}
|
|
271
|
+
</text>
|
|
272
|
+
</box>
|
|
273
|
+
{config.channels?.telegram && (
|
|
274
|
+
<box flexDirection="column" gap={0}>
|
|
275
|
+
<text fg={ONBOARD.text}>
|
|
276
|
+
<strong>Telegram</strong>
|
|
277
|
+
</text>
|
|
278
|
+
<text fg={ONBOARD.muted}>
|
|
279
|
+
{" "}Bot token: {maskSecret(config.channels.telegram.botToken)}
|
|
280
|
+
</text>
|
|
281
|
+
</box>
|
|
282
|
+
)}
|
|
283
|
+
<box flexDirection="column" gap={0}>
|
|
284
|
+
<text fg={ONBOARD.text}>
|
|
285
|
+
<strong>Personality</strong>
|
|
286
|
+
</text>
|
|
287
|
+
<text fg={ONBOARD.muted}> {config.personality}</text>
|
|
288
|
+
</box>
|
|
289
|
+
<OnboardSelect
|
|
290
|
+
items={menuItems}
|
|
291
|
+
onSelect={(i) => (i === 0 ? onConfirm() : onRestart())}
|
|
292
|
+
onBack={onBack}
|
|
293
|
+
/>
|
|
294
|
+
<box flexDirection="column" marginTop={1}>
|
|
295
|
+
<FooterHints variant="menu" hasBack={!!onBack} />
|
|
296
|
+
</box>
|
|
297
|
+
</OnboardScreenLayout>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function StartChatScreen({
|
|
302
|
+
onYes,
|
|
303
|
+
onNo,
|
|
304
|
+
}: {
|
|
305
|
+
onYes: () => void;
|
|
306
|
+
onNo: () => void;
|
|
307
|
+
}) {
|
|
308
|
+
const items = ["Yes, start chatting", "No, exit for now"];
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<OnboardScreenLayout>
|
|
312
|
+
<text fg={ONBOARD.success}>
|
|
313
|
+
<strong>Configuration saved!</strong>
|
|
314
|
+
</text>
|
|
315
|
+
<text fg={ONBOARD.text}>Start chatting now?</text>
|
|
316
|
+
<OnboardSelect
|
|
317
|
+
items={items}
|
|
318
|
+
onSelect={(i) => (i === 0 ? onYes() : onNo())}
|
|
319
|
+
/>
|
|
320
|
+
<box flexDirection="column" marginTop={1}>
|
|
321
|
+
<FooterHints variant="menu" hasBack={false} />
|
|
322
|
+
</box>
|
|
323
|
+
</OnboardScreenLayout>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared terminal palette and layout constants for OpenPaw CLI screens
|
|
3
|
+
* (onboarding, chat, etc.) so colors stay consistent across TUIs.
|
|
4
|
+
*/
|
|
5
|
+
export const ONBOARD = {
|
|
6
|
+
accent: "#7aa2f7",
|
|
7
|
+
/** Role headers ("You", "Assistant") in chat; same hue as {@link accent}. */
|
|
8
|
+
roleLabel: "#7aa2f7",
|
|
9
|
+
text: "#c0caf5",
|
|
10
|
+
muted: "#a9b1d6",
|
|
11
|
+
hint: "#565f89",
|
|
12
|
+
success: "#73daca",
|
|
13
|
+
error: "#f7768e",
|
|
14
|
+
colWidth: 60,
|
|
15
|
+
inputWidth: 58,
|
|
16
|
+
} as const;
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared row types for the terminal chat transcript (user lines, assistant segments, system).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** One contiguous block of assistant output: extended thinking, tools, or visible reply. */
|
|
6
|
+
export type AssistantSegment = { kind: "reasoning" | "text" | "tool"; text: string };
|
|
7
|
+
|
|
8
|
+
export type ChatLine =
|
|
9
|
+
| { role: "user"; text: string }
|
|
10
|
+
| { role: "assistant"; segments: AssistantSegment[] }
|
|
11
|
+
| { role: "system"; text: string };
|