@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.
Files changed (72) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +144 -0
  3. package/agent/agent.ts +217 -0
  4. package/agent/context-scan.ts +81 -0
  5. package/agent/file-editor-store.ts +27 -0
  6. package/agent/index.ts +31 -0
  7. package/agent/memory-store.ts +404 -0
  8. package/agent/model.ts +14 -0
  9. package/agent/prompt-builder.ts +139 -0
  10. package/agent/prompt-context-files.ts +151 -0
  11. package/agent/sandbox-paths.ts +52 -0
  12. package/agent/session-store.ts +80 -0
  13. package/agent/skill-catalog.ts +25 -0
  14. package/agent/skills/discover.ts +100 -0
  15. package/agent/tool-stream-format.ts +126 -0
  16. package/agent/tool-yaml-like.ts +96 -0
  17. package/agent/tools/bash.ts +100 -0
  18. package/agent/tools/file-editor.ts +293 -0
  19. package/agent/tools/list-dir.ts +58 -0
  20. package/agent/tools/load-skill.ts +40 -0
  21. package/agent/tools/memory.ts +84 -0
  22. package/agent/turn-context.ts +46 -0
  23. package/agent/types.ts +37 -0
  24. package/agent/workspace-bootstrap.ts +98 -0
  25. package/bin/openpaw.cjs +177 -0
  26. package/bundled-skills/find-skills/SKILL.md +163 -0
  27. package/cli/components/chat-app.tsx +759 -0
  28. package/cli/components/onboard-ui.tsx +325 -0
  29. package/cli/components/theme.ts +16 -0
  30. package/cli/configure.tsx +0 -0
  31. package/cli/lib/chat-transcript-types.ts +11 -0
  32. package/cli/lib/markdown-render-node.ts +523 -0
  33. package/cli/lib/onboard-markdown-syntax-style.ts +55 -0
  34. package/cli/lib/ui-messages-to-chat-transcript.ts +157 -0
  35. package/cli/lib/use-auto-copy-selection.ts +38 -0
  36. package/cli/onboard.tsx +248 -0
  37. package/cli/openpaw.tsx +144 -0
  38. package/cli/reset.ts +12 -0
  39. package/cli/tui.tsx +31 -0
  40. package/config/index.ts +3 -0
  41. package/config/paths.ts +71 -0
  42. package/config/personality-copy.ts +68 -0
  43. package/config/storage.ts +80 -0
  44. package/config/types.ts +37 -0
  45. package/gateway/bootstrap.ts +25 -0
  46. package/gateway/channel-adapter.ts +8 -0
  47. package/gateway/daemon-manager.ts +191 -0
  48. package/gateway/index.ts +18 -0
  49. package/gateway/session-key.ts +13 -0
  50. package/gateway/slash-command-tokens.ts +39 -0
  51. package/gateway/start-messaging.ts +40 -0
  52. package/gateway/telegram/active-thread-store.ts +89 -0
  53. package/gateway/telegram/adapter.ts +290 -0
  54. package/gateway/telegram/assistant-markdown.ts +48 -0
  55. package/gateway/telegram/bot-commands.ts +40 -0
  56. package/gateway/telegram/chat-preferences.ts +100 -0
  57. package/gateway/telegram/constants.ts +5 -0
  58. package/gateway/telegram/index.ts +4 -0
  59. package/gateway/telegram/message-html.ts +138 -0
  60. package/gateway/telegram/message-queue.ts +19 -0
  61. package/gateway/telegram/reserved-command-filter.ts +33 -0
  62. package/gateway/telegram/session-file-discovery.ts +62 -0
  63. package/gateway/telegram/session-key.ts +13 -0
  64. package/gateway/telegram/session-label.ts +14 -0
  65. package/gateway/telegram/sessions-list-reply.ts +39 -0
  66. package/gateway/telegram/stream-delivery.ts +618 -0
  67. package/gateway/tui/constants.ts +2 -0
  68. package/gateway/tui/tui-active-thread-store.ts +103 -0
  69. package/gateway/tui/tui-session-discovery.ts +94 -0
  70. package/gateway/tui/tui-session-label.ts +22 -0
  71. package/gateway/tui/tui-sessions-list-message.ts +37 -0
  72. 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 };