@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,103 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getSessionsDir } from "../../config/paths";
4
+ import type { SessionId } from "../../agent/types";
5
+ import { TUI_ACTIVE_THREAD_FILENAME } from "./constants";
6
+
7
+ const LEGACY_TUI_SESSION: SessionId = "tui:main";
8
+
9
+ type TuiActiveThreadState = {
10
+ /** When set, active session is `tui:main:${threadUuid}`; otherwise legacy `tui:main`. */
11
+ threadUuid?: string;
12
+ /** Explicit non-TUI session id (for example `telegram:<chatId>[:threadUuid]`). */
13
+ sessionId?: SessionId;
14
+ };
15
+
16
+ function activeThreadPath(): string {
17
+ return join(getSessionsDir(), TUI_ACTIVE_THREAD_FILENAME);
18
+ }
19
+
20
+ async function readState(): Promise<TuiActiveThreadState> {
21
+ const path = activeThreadPath();
22
+ if (!existsSync(path)) {
23
+ return {};
24
+ }
25
+ try {
26
+ const raw = await Bun.file(path).text();
27
+ const parsed = JSON.parse(raw) as unknown;
28
+ if (typeof parsed !== "object" || parsed === null) {
29
+ return {};
30
+ }
31
+ const sessionId = (parsed as TuiActiveThreadState).sessionId;
32
+ if (typeof sessionId === "string" && sessionId.length > 0) {
33
+ return { sessionId };
34
+ }
35
+ const threadUuid = (parsed as TuiActiveThreadState).threadUuid;
36
+ if (typeof threadUuid === "string" && threadUuid.length > 0) {
37
+ return { threadUuid };
38
+ }
39
+ return {};
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ async function writeState(state: TuiActiveThreadState): Promise<void> {
46
+ const dir = getSessionsDir();
47
+ if (!existsSync(dir)) {
48
+ mkdirSync(dir, { recursive: true });
49
+ }
50
+ const payload =
51
+ state.sessionId && state.sessionId.length > 0
52
+ ? { sessionId: state.sessionId }
53
+ : state.threadUuid && state.threadUuid.length > 0
54
+ ? { threadUuid: state.threadUuid }
55
+ : {};
56
+ await Bun.write(activeThreadPath(), JSON.stringify(payload, null, 2));
57
+ }
58
+
59
+ /**
60
+ * OpenPaw persistence session id for the TUI (legacy `tui:main` or threaded `tui:main:uuid`).
61
+ */
62
+ export async function getTuiPersistenceSessionId(): Promise<SessionId> {
63
+ const state = await readState();
64
+ if (state.sessionId) {
65
+ return state.sessionId;
66
+ }
67
+ if (state.threadUuid) {
68
+ return `tui:main:${state.threadUuid}`;
69
+ }
70
+ return LEGACY_TUI_SESSION;
71
+ }
72
+
73
+ /**
74
+ * Starts a new TUI thread: new uuid in the active store. Returns the new persistence session id.
75
+ */
76
+ export async function startNewTuiThread(): Promise<SessionId> {
77
+ const uuid = crypto.randomUUID();
78
+ await writeState({ threadUuid: uuid });
79
+ return `tui:main:${uuid}`;
80
+ }
81
+
82
+ /**
83
+ * Points the TUI at an existing persistence session (legacy or thread file).
84
+ */
85
+ export async function setActiveTuiSession(persistenceSessionId: SessionId): Promise<void> {
86
+ if (persistenceSessionId === LEGACY_TUI_SESSION) {
87
+ await writeState({});
88
+ return;
89
+ }
90
+ if (!persistenceSessionId.trim()) {
91
+ throw new Error("Invalid session id");
92
+ }
93
+ const prefix = `${LEGACY_TUI_SESSION}:`;
94
+ if (persistenceSessionId.startsWith(prefix)) {
95
+ const uuid = persistenceSessionId.slice(prefix.length);
96
+ if (!uuid) {
97
+ throw new Error("Invalid thread id");
98
+ }
99
+ await writeState({ threadUuid: uuid });
100
+ return;
101
+ }
102
+ await writeState({ sessionId: persistenceSessionId });
103
+ }
@@ -0,0 +1,94 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readdir, stat } from "node:fs/promises";
4
+ import { getSessionsDir } from "../../config/paths";
5
+ import type { SessionId } from "../../agent/types";
6
+ import { TUI_ACTIVE_THREAD_FILENAME } from "./constants";
7
+ import {
8
+ TELEGRAM_ACTIVE_THREADS_FILENAME,
9
+ TELEGRAM_CHAT_PREFERENCES_FILENAME,
10
+ } from "../telegram/constants";
11
+
12
+ export type TuiSessionListEntry = {
13
+ sessionId: SessionId;
14
+ mtimeMs: number;
15
+ };
16
+
17
+ function parseTuiSessionFilename(stem: string): SessionId | null {
18
+ if (stem === "tui_main") {
19
+ return "tui:main";
20
+ }
21
+ if (stem.startsWith("tui_main_")) {
22
+ const suffix = stem.slice("tui_main_".length);
23
+ if (!suffix) {
24
+ return null;
25
+ }
26
+ return `tui:main:${suffix}`;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function parseTelegramSessionFilename(stem: string): SessionId | null {
32
+ const match = /^telegram_(\d+)(?:_(.+))?$/.exec(stem);
33
+ if (!match) {
34
+ return null;
35
+ }
36
+ const chatId = match[1]!;
37
+ const suffix = match[2];
38
+ if (!suffix) {
39
+ return `telegram:${chatId}`;
40
+ }
41
+ return `telegram:${chatId}:${suffix}`;
42
+ }
43
+
44
+ /**
45
+ * Maps a sessions-dir filename stem back to a resumable TUI-visible session id.
46
+ * TUI-native and Telegram sessions are supported.
47
+ */
48
+ function parseResumableSessionFilename(filename: string): SessionId | null {
49
+ if (
50
+ !filename.endsWith(".json") ||
51
+ filename === TUI_ACTIVE_THREAD_FILENAME ||
52
+ filename === TELEGRAM_ACTIVE_THREADS_FILENAME ||
53
+ filename === TELEGRAM_CHAT_PREFERENCES_FILENAME
54
+ ) {
55
+ return null;
56
+ }
57
+ const stem = filename.slice(0, -".json".length);
58
+ const tui = parseTuiSessionFilename(stem);
59
+ if (tui) {
60
+ return tui;
61
+ }
62
+ const telegram = parseTelegramSessionFilename(stem);
63
+ if (telegram) {
64
+ return telegram;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Lists on-disk sessions visible to TUI /sessions and /resume, newest first.
71
+ */
72
+ export async function listTuiSessions(): Promise<TuiSessionListEntry[]> {
73
+ const dir = getSessionsDir();
74
+ if (!existsSync(dir)) {
75
+ return [];
76
+ }
77
+ const names = await readdir(dir);
78
+ const entries: TuiSessionListEntry[] = [];
79
+ for (const name of names) {
80
+ const sessionId = parseResumableSessionFilename(name);
81
+ if (!sessionId) {
82
+ continue;
83
+ }
84
+ const path = join(dir, name);
85
+ try {
86
+ const st = await stat(path);
87
+ entries.push({ sessionId, mtimeMs: st.mtimeMs });
88
+ } catch {
89
+ continue;
90
+ }
91
+ }
92
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
93
+ return entries;
94
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Human-readable label for a TUI /sessions row.
3
+ */
4
+ export function formatTuiSessionLabel(sessionId: string): string {
5
+ const tuiLegacy = "tui:main";
6
+ if (sessionId === tuiLegacy) {
7
+ return "main";
8
+ }
9
+ const tuiPrefix = `${tuiLegacy}:`;
10
+ if (sessionId.startsWith(tuiPrefix)) {
11
+ return sessionId.slice(tuiPrefix.length);
12
+ }
13
+
14
+ const telegramMatch = /^telegram:(\d+)(?::(.+))?$/.exec(sessionId);
15
+ if (telegramMatch) {
16
+ const chatId = telegramMatch[1]!;
17
+ const suffix = telegramMatch[2];
18
+ return suffix ? `telegram ${chatId} / ${suffix}` : `telegram ${chatId} / main`;
19
+ }
20
+
21
+ return sessionId;
22
+ }
@@ -0,0 +1,37 @@
1
+ import type { SessionId } from "../../agent/types";
2
+ import { formatTuiSessionLabel } from "./tui-session-label";
3
+ import type { TuiSessionListEntry } from "./tui-session-discovery";
4
+
5
+ const MAX_LINES = 20;
6
+ const MAX_CHARS = 3500;
7
+
8
+ /**
9
+ * Builds the plaintext body for a TUI /sessions reply (numbered list, active marker, truncation).
10
+ */
11
+ export function formatTuiSessionsListMessage(
12
+ entries: TuiSessionListEntry[],
13
+ activeSessionId: SessionId,
14
+ ): string {
15
+ if (entries.length === 0) {
16
+ return "No saved sessions yet.";
17
+ }
18
+
19
+ const lines: string[] = [];
20
+ const shown = Math.min(entries.length, MAX_LINES);
21
+ for (let i = 0; i < shown; i++) {
22
+ const e = entries[i]!;
23
+ const n = i + 1;
24
+ const mark = e.sessionId === activeSessionId ? " (active)" : "";
25
+ const label = formatTuiSessionLabel(e.sessionId);
26
+ lines.push(`${n}. ${label}${mark}`);
27
+ }
28
+ if (entries.length > MAX_LINES) {
29
+ lines.push(`…and ${entries.length - MAX_LINES} more.`);
30
+ }
31
+
32
+ let body = "Saved sessions (newest first):\n" + lines.join("\n");
33
+ if (body.length > MAX_CHARS) {
34
+ body = `${body.slice(0, MAX_CHARS - 20)}\n…(truncated)`;
35
+ }
36
+ return body;
37
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@amanm/openpaw",
3
+ "version": "0.1.0",
4
+ "description": "Local Bun + TypeScript agent runtime with TUI and messaging gateway adapters.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "openpaw": "bin/openpaw.cjs"
9
+ },
10
+ "scripts": {
11
+ "openpaw": "bun run ./cli/openpaw.tsx",
12
+ "gateway:dev": "bun run ./cli/openpaw.tsx gateway dev",
13
+ "tui": "bun run ./cli/openpaw.tsx tui",
14
+ "onboard": "bun run ./cli/openpaw.tsx onboard",
15
+ "typecheck": "bunx tsc --noEmit"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "latest"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.18.0"
22
+ },
23
+ "files": [
24
+ "agent/",
25
+ "bin/",
26
+ "bundled-skills/",
27
+ "cli/",
28
+ "config/",
29
+ "gateway/",
30
+ "README.md",
31
+ "AGENTS.md"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "peerDependencies": {
37
+ "typescript": "^5"
38
+ },
39
+ "dependencies": {
40
+ "@ai-sdk/openai-compatible": "^2.0.37",
41
+ "@opentui/core": "^0.1.90",
42
+ "@opentui/react": "^0.1.90",
43
+ "ai": "^6.0.138",
44
+ "commander": "^14.0.3",
45
+ "grammy": "^1.41.1",
46
+ "react": "^19.2.4",
47
+ "tg-markdown-converter": "^1.1.1",
48
+ "web-tree-sitter": "0.25.10",
49
+ "yaml": "^2.8.3",
50
+ "zod": "^4.3.6"
51
+ }
52
+ }