@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,157 @@
1
+ /**
2
+ * Maps persisted AI SDK UI messages into {@link ChatLine} rows for the terminal chat transcript.
3
+ */
4
+ import type { DynamicToolUIPart, ToolUIPart, UIMessage, UITools } from "ai";
5
+ import {
6
+ getToolName,
7
+ isFileUIPart,
8
+ isReasoningUIPart,
9
+ isTextUIPart,
10
+ isToolUIPart,
11
+ } from "ai";
12
+ import {
13
+ formatTuiToolDeniedMarkdown,
14
+ formatTuiToolErrorMarkdown,
15
+ formatTuiToolInputMarkdown,
16
+ formatTuiToolOutputMarkdown,
17
+ truncateJson,
18
+ } from "../../agent/tool-stream-format";
19
+ import type { AssistantSegment, ChatLine } from "./chat-transcript-types";
20
+
21
+ /**
22
+ * Builds a short summary line for a tool part (plain transcript / non-markdown rows).
23
+ */
24
+ function formatToolPartSummary(part: ToolUIPart<UITools> | DynamicToolUIPart): string {
25
+ const name = getToolName(part);
26
+ switch (part.state) {
27
+ case "input-streaming":
28
+ return `[Tool ${name}] …`;
29
+ case "input-available":
30
+ return `[Tool ${name}] ${truncateJson(part.input)}`;
31
+ case "output-available":
32
+ return `[Tool ${name}] → ${truncateJson(part.output)}`;
33
+ case "output-error":
34
+ return `[Tool ${name}] error: ${part.errorText}`;
35
+ case "output-denied":
36
+ return `[Tool ${name}] (denied)`;
37
+ case "approval-requested":
38
+ return `[Tool ${name}] (approval requested)`;
39
+ case "approval-responded":
40
+ return `[Tool ${name}] (approval ${part.approval.approved ? "ok" : "rejected"})`;
41
+ default:
42
+ return `[Tool ${name}]`;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Same tool display as the live TUI stream: markdown headings + fenced YAML blocks.
48
+ */
49
+ function formatToolPartForAssistantTui(part: ToolUIPart<UITools> | DynamicToolUIPart): string {
50
+ const name = getToolName(part);
51
+ switch (part.state) {
52
+ case "input-streaming":
53
+ return `🔧 **Tool · ${name}**\n\n_(input streaming…)_`;
54
+ case "input-available":
55
+ return formatTuiToolInputMarkdown(name, part.input);
56
+ case "output-available":
57
+ return `${formatTuiToolInputMarkdown(name, part.input)}\n\n${formatTuiToolOutputMarkdown(part.output)}`;
58
+ case "output-error":
59
+ return `${formatTuiToolInputMarkdown(name, part.input)}\n\n${formatTuiToolErrorMarkdown(name, part.errorText)}`;
60
+ case "output-denied":
61
+ return `${formatTuiToolInputMarkdown(name, part.input)}\n\n${formatTuiToolDeniedMarkdown(name)}`;
62
+ case "approval-requested":
63
+ return `🔧 **Tool · ${name}**\n\n_(approval requested)_`;
64
+ case "approval-responded":
65
+ return `🔧 **Tool · ${name}**\n\n_(approval ${part.approval.approved ? "granted" : "rejected"})_`;
66
+ default:
67
+ return `🔧 **Tool · ${name}**`;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Merges a new string into the tail segment when `kind` matches, else appends a segment.
73
+ */
74
+ function mergeAssistantSegment(
75
+ segments: AssistantSegment[],
76
+ kind: AssistantSegment["kind"],
77
+ text: string,
78
+ ): AssistantSegment[] {
79
+ const trimmed = text.trim();
80
+ if (!trimmed) {
81
+ return segments;
82
+ }
83
+ const last = segments[segments.length - 1];
84
+ if (last?.kind === kind) {
85
+ return [...segments.slice(0, -1), { kind, text: `${last.text}\n\n${trimmed}` }];
86
+ }
87
+ return [...segments, { kind, text: trimmed }];
88
+ }
89
+
90
+ /**
91
+ * Converts assistant message parts into ordered reasoning vs text segments for the TUI.
92
+ */
93
+ function partsToAssistantSegments(parts: UIMessage["parts"]): AssistantSegment[] {
94
+ let segments: AssistantSegment[] = [];
95
+ for (const part of parts) {
96
+ if (isTextUIPart(part) && part.text.trim()) {
97
+ segments = mergeAssistantSegment(segments, "text", part.text);
98
+ } else if (isReasoningUIPart(part) && part.text.trim()) {
99
+ segments = mergeAssistantSegment(segments, "reasoning", part.text);
100
+ } else if (isFileUIPart(part)) {
101
+ segments = mergeAssistantSegment(
102
+ segments,
103
+ "text",
104
+ `[file: ${part.filename ?? part.mediaType}]`,
105
+ );
106
+ } else if (isToolUIPart(part)) {
107
+ segments = mergeAssistantSegment(segments, "tool", formatToolPartForAssistantTui(part));
108
+ }
109
+ }
110
+ return segments;
111
+ }
112
+
113
+ /**
114
+ * Joins user/system message parts into a single transcript string (no reasoning label).
115
+ */
116
+ function partsToPlainTranscriptText(parts: UIMessage["parts"]): string {
117
+ const chunks: string[] = [];
118
+ for (const part of parts) {
119
+ if (isTextUIPart(part) && part.text.trim()) {
120
+ chunks.push(part.text);
121
+ } else if (isReasoningUIPart(part) && part.text.trim()) {
122
+ chunks.push(part.text);
123
+ } else if (isFileUIPart(part)) {
124
+ chunks.push(`[file: ${part.filename ?? part.mediaType}]`);
125
+ } else if (isToolUIPart(part)) {
126
+ chunks.push(formatToolPartSummary(part));
127
+ }
128
+ }
129
+ return chunks.join("\n\n").trim();
130
+ }
131
+
132
+ /**
133
+ * Maps validated session `UIMessage[]` into {@link ChatLine} rows for the terminal chat transcript.
134
+ * Skips messages that have no displayable content after stripping empty parts.
135
+ */
136
+ export function uiMessagesToChatLines(messages: UIMessage[]): ChatLine[] {
137
+ const out: ChatLine[] = [];
138
+ for (const msg of messages) {
139
+ if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "system") {
140
+ continue;
141
+ }
142
+ if (msg.role === "assistant") {
143
+ const segments = partsToAssistantSegments(msg.parts);
144
+ if (segments.length === 0) {
145
+ continue;
146
+ }
147
+ out.push({ role: "assistant", segments });
148
+ continue;
149
+ }
150
+ const text = partsToPlainTranscriptText(msg.parts);
151
+ if (!text) {
152
+ continue;
153
+ }
154
+ out.push({ role: msg.role, text });
155
+ }
156
+ return out;
157
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Subscribes to OpenTUI text selection updates and copies the final selection to the
3
+ * system clipboard using OSC 52 when the terminal supports it.
4
+ */
5
+ import { useEffect } from "react";
6
+ import { CliRenderEvents } from "@opentui/core";
7
+ import { useRenderer } from "@opentui/react";
8
+
9
+ /**
10
+ * Registers a renderer listener so that when the user finishes selecting text in the TUI,
11
+ * the selected string is sent to the clipboard (via OSC 52), then the selection is cleared.
12
+ */
13
+ export function useAutoCopySelection(): void {
14
+ const renderer = useRenderer();
15
+
16
+ useEffect(() => {
17
+ const onSelection = (): void => {
18
+ if (!renderer.isOsc52Supported()) {
19
+ return;
20
+ }
21
+ const sel = renderer.getSelection();
22
+ if (!sel || sel.isDragging) {
23
+ return;
24
+ }
25
+ const text = sel.getSelectedText();
26
+ if (!text) {
27
+ return;
28
+ }
29
+ renderer.copyToClipboardOSC52(text);
30
+ renderer.clearSelection();
31
+ };
32
+
33
+ renderer.on(CliRenderEvents.SELECTION, onSelection);
34
+ return () => {
35
+ renderer.off(CliRenderEvents.SELECTION, onSelection);
36
+ };
37
+ }, [renderer]);
38
+ }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env bun
2
+ import { createCliRenderer } from "@opentui/core";
3
+ import { createRoot, useKeyboard, useRenderer } from "@opentui/react";
4
+ import { useCallback, useEffect, useReducer, useState } from "react";
5
+ import {
6
+ configExists,
7
+ deleteConfig,
8
+ saveConfig,
9
+ PERSONALITIES,
10
+ type OpenPawConfig,
11
+ type Personality,
12
+ type ProviderConfig,
13
+ } from "../config";
14
+ import { ensureWorkspaceLayout } from "../agent/workspace-bootstrap";
15
+ import {
16
+ WelcomeScreen,
17
+ InputScreen,
18
+ PersonalityScreen,
19
+ ConfirmScreen,
20
+ StartChatScreen,
21
+ } from "./components/onboard-ui";
22
+
23
+ type Step =
24
+ | "provider-baseUrl"
25
+ | "provider-apiKey"
26
+ | "provider-model"
27
+ | "telegram"
28
+ | "personality"
29
+ | "confirm"
30
+ | "start-chat";
31
+
32
+ type WizardState = {
33
+ step: Step;
34
+ provider: ProviderConfig;
35
+ botToken: string;
36
+ personality: Personality;
37
+ };
38
+
39
+ type WizardAction =
40
+ | { type: "SET_STEP"; step: Step }
41
+ | { type: "PATCH_PROVIDER"; patch: Partial<ProviderConfig> }
42
+ | { type: "SET_BOT_TOKEN"; value: string }
43
+ | { type: "SET_PERSONALITY"; value: Personality };
44
+
45
+ const initialWizardState: WizardState = {
46
+ step: "provider-baseUrl",
47
+ provider: { baseUrl: "", apiKey: "", model: "" },
48
+ botToken: "",
49
+ personality: "Assistant",
50
+ };
51
+
52
+ function wizardReducer(state: WizardState, action: WizardAction): WizardState {
53
+ switch (action.type) {
54
+ case "SET_STEP":
55
+ return { ...state, step: action.step };
56
+ case "PATCH_PROVIDER":
57
+ return { ...state, provider: { ...state.provider, ...action.patch } };
58
+ case "SET_BOT_TOKEN":
59
+ return { ...state, botToken: action.value };
60
+ case "SET_PERSONALITY":
61
+ return { ...state, personality: action.value };
62
+ default: {
63
+ const _exhaustive: never = action;
64
+ return _exhaustive;
65
+ }
66
+ }
67
+ }
68
+
69
+ function OnboardingWizard({
70
+ onComplete,
71
+ }: {
72
+ onComplete: (startChat: boolean) => void;
73
+ }) {
74
+ const [state, dispatch] = useReducer(wizardReducer, initialWizardState);
75
+ const { step, provider, botToken, personality } = state;
76
+
77
+ const handleConfirm = async () => {
78
+ const config: OpenPawConfig = {
79
+ provider,
80
+ channels: botToken ? { telegram: { botToken } } : undefined,
81
+ personality,
82
+ };
83
+ await saveConfig(config);
84
+ ensureWorkspaceLayout();
85
+ dispatch({ type: "SET_STEP", step: "start-chat" });
86
+ };
87
+
88
+ switch (step) {
89
+ case "provider-baseUrl":
90
+ return (
91
+ <InputScreen
92
+ key={step}
93
+ title="Provider Configuration"
94
+ label="Enter the base URL for your LLM provider:"
95
+ value={provider.baseUrl}
96
+ onChange={(v) =>
97
+ dispatch({ type: "PATCH_PROVIDER", patch: { baseUrl: v } })
98
+ }
99
+ onSubmit={() =>
100
+ dispatch({ type: "SET_STEP", step: "provider-apiKey" })
101
+ }
102
+ placeholder="https://api.openai.com/v1"
103
+ />
104
+ );
105
+ case "provider-apiKey":
106
+ return (
107
+ <InputScreen
108
+ key={step}
109
+ title="Provider Configuration"
110
+ label="Enter your API key:"
111
+ value={provider.apiKey}
112
+ onChange={(v) =>
113
+ dispatch({ type: "PATCH_PROVIDER", patch: { apiKey: v } })
114
+ }
115
+ onSubmit={() =>
116
+ dispatch({ type: "SET_STEP", step: "provider-model" })
117
+ }
118
+ onBack={() =>
119
+ dispatch({ type: "SET_STEP", step: "provider-baseUrl" })
120
+ }
121
+ placeholder="sk-..."
122
+ password
123
+ />
124
+ );
125
+ case "provider-model":
126
+ return (
127
+ <InputScreen
128
+ key={step}
129
+ title="Provider Configuration"
130
+ label="Enter the model name:"
131
+ value={provider.model}
132
+ onChange={(v) =>
133
+ dispatch({ type: "PATCH_PROVIDER", patch: { model: v } })
134
+ }
135
+ onSubmit={() => dispatch({ type: "SET_STEP", step: "telegram" })}
136
+ onBack={() =>
137
+ dispatch({ type: "SET_STEP", step: "provider-apiKey" })
138
+ }
139
+ placeholder="gpt-4o"
140
+ />
141
+ );
142
+ case "telegram":
143
+ return (
144
+ <InputScreen
145
+ key={step}
146
+ title="Channel Configuration"
147
+ label="Enter your Telegram bot token:"
148
+ value={botToken}
149
+ onChange={(v) => dispatch({ type: "SET_BOT_TOKEN", value: v })}
150
+ onSubmit={() =>
151
+ dispatch({ type: "SET_STEP", step: "personality" })
152
+ }
153
+ onBack={() =>
154
+ dispatch({ type: "SET_STEP", step: "provider-model" })
155
+ }
156
+ placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz"
157
+ password
158
+ />
159
+ );
160
+ case "personality":
161
+ return (
162
+ <PersonalityScreen
163
+ key={step}
164
+ options={[...PERSONALITIES]}
165
+ onSelect={(index) => {
166
+ dispatch({
167
+ type: "SET_PERSONALITY",
168
+ value: PERSONALITIES[index] ?? "Assistant",
169
+ });
170
+ dispatch({ type: "SET_STEP", step: "confirm" });
171
+ }}
172
+ onBack={() => dispatch({ type: "SET_STEP", step: "telegram" })}
173
+ />
174
+ );
175
+ case "confirm":
176
+ return (
177
+ <ConfirmScreen
178
+ key={step}
179
+ config={{
180
+ provider,
181
+ channels: botToken ? { telegram: { botToken } } : undefined,
182
+ personality,
183
+ }}
184
+ onConfirm={handleConfirm}
185
+ onRestart={() =>
186
+ dispatch({ type: "SET_STEP", step: "provider-baseUrl" })
187
+ }
188
+ onBack={() =>
189
+ dispatch({ type: "SET_STEP", step: "personality" })
190
+ }
191
+ />
192
+ );
193
+ case "start-chat":
194
+ return (
195
+ <StartChatScreen
196
+ key={step}
197
+ onYes={() => onComplete(true)}
198
+ onNo={() => onComplete(false)}
199
+ />
200
+ );
201
+ default: {
202
+ const _exhaustive: never = step;
203
+ return _exhaustive;
204
+ }
205
+ }
206
+ }
207
+
208
+ function App() {
209
+ const [showWelcome, setShowWelcome] = useState(true);
210
+ const renderer = useRenderer();
211
+
212
+ useEffect(() => {
213
+ if (configExists()) {
214
+ deleteConfig();
215
+ }
216
+ }, []);
217
+
218
+ useKeyboard((key) => {
219
+ if (key.name === "escape") {
220
+ renderer.destroy();
221
+ }
222
+ });
223
+
224
+ const handleOnboardingComplete = (startChat: boolean) => {
225
+ renderer.destroy();
226
+ if (startChat) {
227
+ console.log("Starting chat...");
228
+ }
229
+ };
230
+
231
+ const dismissWelcome = useCallback(() => {
232
+ setShowWelcome(false);
233
+ }, []);
234
+
235
+ if (showWelcome) {
236
+ return <WelcomeScreen onComplete={dismissWelcome} />;
237
+ }
238
+
239
+ return <OnboardingWizard onComplete={handleOnboardingComplete} />;
240
+ }
241
+
242
+ export async function handleOnboard(options: {}) {
243
+ const renderer = await createCliRenderer({
244
+ exitOnCtrlC: true,
245
+ });
246
+ const root = createRoot(renderer);
247
+ root.render(<App />);
248
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env bun
2
+ import { program } from "commander";
3
+ import { startGateway } from "../gateway";
4
+ import { runOpenPawTui } from "./tui";
5
+ import { handleOnboard } from "./onboard";
6
+ import { handleReset } from "./reset";
7
+ import {
8
+ getGatewayDaemonStatus,
9
+ readGatewayDaemonLog,
10
+ startGatewayDaemon,
11
+ stopGatewayDaemon,
12
+ } from "../gateway/daemon-manager";
13
+
14
+ program.version("0.1.0").description("OpenPaw");
15
+
16
+ program
17
+ .command("reset")
18
+ .description(
19
+ "Delete ~/.openpaw/workspace (sessions and files) and recreate onboarding default layout",
20
+ )
21
+ .action(() => {
22
+ try {
23
+ handleReset();
24
+ } catch (e) {
25
+ console.error(e instanceof Error ? e.message : e);
26
+ process.exitCode = 1;
27
+ }
28
+ });
29
+
30
+ program
31
+ .command("onboard")
32
+ .description("Go through the onboarding setup")
33
+ .action(handleOnboard);
34
+
35
+ const gateway = program.command("gateway").description("Run messaging channel adapters (shared agent runtime)");
36
+
37
+ gateway
38
+ .command("dev")
39
+ .description("Start all configured channels in foreground mode (blocking)")
40
+ .action(async () => {
41
+ try {
42
+ await startGateway();
43
+ } catch (e) {
44
+ console.error(e instanceof Error ? e.message : e);
45
+ process.exitCode = 1;
46
+ }
47
+ });
48
+
49
+ gateway
50
+ .command("start")
51
+ .description("Start gateway daemon in background mode")
52
+ .action(() => {
53
+ try {
54
+ const result = startGatewayDaemon();
55
+ if (result.status === "already_running") {
56
+ console.log(
57
+ `OpenPaw gateway is already running (pid ${result.pid}). Logs: ${result.paths.logFile}`,
58
+ );
59
+ return;
60
+ }
61
+ console.log(
62
+ `OpenPaw gateway started in background (pid ${result.pid}). Logs: ${result.paths.logFile}`,
63
+ );
64
+ } catch (e) {
65
+ console.error(e instanceof Error ? e.message : e);
66
+ process.exitCode = 1;
67
+ }
68
+ });
69
+
70
+ gateway
71
+ .command("stop")
72
+ .description("Stop gateway daemon")
73
+ .action(() => {
74
+ try {
75
+ const result = stopGatewayDaemon();
76
+ if (result.status === "already_stopped") {
77
+ console.log("OpenPaw gateway is not running.");
78
+ return;
79
+ }
80
+ console.log(`Stopped OpenPaw gateway daemon (pid ${result.pid}).`);
81
+ } catch (e) {
82
+ console.error(e instanceof Error ? e.message : e);
83
+ process.exitCode = 1;
84
+ }
85
+ });
86
+
87
+ gateway
88
+ .command("status")
89
+ .description("Show gateway daemon status")
90
+ .action(() => {
91
+ try {
92
+ const status = getGatewayDaemonStatus();
93
+ if (status.state === "running") {
94
+ console.log(`OpenPaw gateway is running (pid ${status.pid}).`);
95
+ console.log(`stdout log: ${status.paths.logFile}`);
96
+ console.log(`stderr log: ${status.paths.errFile}`);
97
+ return;
98
+ }
99
+ if (status.state === "stale") {
100
+ console.log(`OpenPaw gateway had a stale pid (${status.pid}); pid file was cleaned.`);
101
+ return;
102
+ }
103
+ console.log("OpenPaw gateway is stopped.");
104
+ } catch (e) {
105
+ console.error(e instanceof Error ? e.message : e);
106
+ process.exitCode = 1;
107
+ }
108
+ });
109
+
110
+ gateway
111
+ .command("logs")
112
+ .description("Print recent gateway daemon logs")
113
+ .option("--stderr", "Show stderr log instead of stdout")
114
+ .option("-n, --lines <count>", "Number of lines to show", "80")
115
+ .action((options: { stderr?: boolean; lines?: string }) => {
116
+ try {
117
+ const parsed = Number.parseInt(options.lines ?? "80", 10);
118
+ const lineCount = Number.isFinite(parsed) && parsed > 0 ? parsed : 80;
119
+ const stream = options.stderr ? "stderr" : "stdout";
120
+ const text = readGatewayDaemonLog(lineCount, stream);
121
+ if (!text) {
122
+ console.log("No daemon logs found yet.");
123
+ return;
124
+ }
125
+ console.log(text);
126
+ } catch (e) {
127
+ console.error(e instanceof Error ? e.message : e);
128
+ process.exitCode = 1;
129
+ }
130
+ });
131
+
132
+ program
133
+ .command("tui")
134
+ .description("Terminal chat UI (OpenTUI / local session), separate from the gateway process")
135
+ .action(async () => {
136
+ try {
137
+ await runOpenPawTui();
138
+ } catch (e) {
139
+ console.error(e instanceof Error ? e.message : e);
140
+ process.exitCode = 1;
141
+ }
142
+ });
143
+
144
+ program.parse();
package/cli/reset.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { resetWorkspaceToOnboardingDefaults } from "../agent/workspace-bootstrap";
2
+ import { getWorkspaceRoot } from "../config/paths";
3
+
4
+ /**
5
+ * CLI handler: delete `~/.openpaw/workspace` and recreate the same layout and
6
+ * default markdown files as onboarding (`agents.md`, `soul.md`, `user.md`, empty `sessions/`).
7
+ */
8
+ export function handleReset(): void {
9
+ const root = getWorkspaceRoot();
10
+ resetWorkspaceToOnboardingDefaults();
11
+ console.log(`Reset workspace: removed and recreated ${root} with onboarding defaults.`);
12
+ }
package/cli/tui.tsx ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Entry for the `openpaw tui` subcommand: full-screen terminal chat against
3
+ * the local agent runtime (separate from the gateway process).
4
+ */
5
+ import { createCliRenderer } from "@opentui/core";
6
+ import { createRoot } from "@opentui/react";
7
+ import { loadSessionMessages } from "../agent/session-store";
8
+ import { createGatewayContext } from "../gateway/bootstrap";
9
+ import { getTuiPersistenceSessionId } from "../gateway/tui/tui-active-thread-store";
10
+ import { ChatApp } from "./components/chat-app";
11
+ import { uiMessagesToChatLines } from "./lib/ui-messages-to-chat-transcript";
12
+
13
+ /**
14
+ * Bootstraps config and workspace, then runs the OpenTUI chat until the user exits.
15
+ */
16
+ export async function runOpenPawTui(): Promise<void> {
17
+ const ctx = await createGatewayContext();
18
+ const sessionId = await getTuiPersistenceSessionId();
19
+ const stored = await loadSessionMessages(sessionId, ctx.runtime.agent.tools);
20
+ const initialLines = uiMessagesToChatLines(stored);
21
+ const renderer = await createCliRenderer({
22
+ exitOnCtrlC: true,
23
+ });
24
+ createRoot(renderer).render(
25
+ <ChatApp
26
+ initialSessionId={sessionId}
27
+ initialLines={initialLines}
28
+ runtime={ctx.runtime}
29
+ />,
30
+ );
31
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./paths";
3
+ export * from "./storage";