@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
package/AGENTS.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Try to keep one file limited to one core feature, and include docstrings in all files and functions to explain what they do.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# OpenPaw
|
|
2
|
+
|
|
3
|
+
OpenPaw is a Bun + TypeScript local agent runtime with:
|
|
4
|
+
|
|
5
|
+
- a terminal chat UI (`openpaw tui`) built with OpenTUI
|
|
6
|
+
- a gateway process (`openpaw gateway dev` in foreground, `openpaw gateway start` in background) for messaging channels (Telegram today)
|
|
7
|
+
- persisted sessions and workspace instructions under `~/.openpaw`
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Node.js `>=18.18` (for npm-installed launcher)
|
|
12
|
+
- Bun `>=1.3` runtime (tested with `1.3.11`)
|
|
13
|
+
- A model provider compatible with OpenAI-style chat APIs
|
|
14
|
+
- Optional: Telegram bot token if you want Telegram channel support
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Global install (recommended for end users):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm i -g openpaw
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The first `openpaw` run checks for Bun. If Bun is missing on macOS/Linux, OpenPaw prompts to install it automatically.
|
|
25
|
+
|
|
26
|
+
Windows note: Bun auto-install is not performed yet. Install Bun manually from `https://bun.sh/docs/installation`.
|
|
27
|
+
|
|
28
|
+
From-source install (development):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CLI Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
openpaw --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Main commands:
|
|
41
|
+
|
|
42
|
+
- `openpaw onboard`
|
|
43
|
+
Interactive onboarding (provider URL, API key, model, optional Telegram token, personality)
|
|
44
|
+
- `openpaw tui`
|
|
45
|
+
Start local terminal chat UI
|
|
46
|
+
- `openpaw gateway dev`
|
|
47
|
+
Start configured messaging adapters in foreground mode (blocking)
|
|
48
|
+
- `openpaw gateway start`
|
|
49
|
+
Start gateway daemon in background mode
|
|
50
|
+
- `openpaw gateway status`
|
|
51
|
+
Show background gateway status and log paths
|
|
52
|
+
- `openpaw gateway stop`
|
|
53
|
+
Stop background gateway daemon
|
|
54
|
+
- `openpaw gateway logs [-n 80] [--stderr]`
|
|
55
|
+
Show recent daemon logs
|
|
56
|
+
|
|
57
|
+
## First-Time Setup
|
|
58
|
+
|
|
59
|
+
Run onboarding once:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
openpaw onboard
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This creates:
|
|
66
|
+
|
|
67
|
+
- config file: `~/.openpaw/config.yaml`
|
|
68
|
+
- workspace: `~/.openpaw/workspace`
|
|
69
|
+
- defaults:
|
|
70
|
+
- `~/.openpaw/workspace/agents.md`
|
|
71
|
+
- `~/.openpaw/workspace/soul.md`
|
|
72
|
+
- `~/.openpaw/workspace/user.md` (legacy; prefer the `memory` tool for profile facts)
|
|
73
|
+
- `~/.openpaw/workspace/memories/` (curated `MEMORY.md` / `USER.md` via the `memory` tool)
|
|
74
|
+
- `~/.openpaw/workspace/sessions/*`
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
`~/.openpaw/config.yaml` contains:
|
|
79
|
+
|
|
80
|
+
- `provider.baseUrl`
|
|
81
|
+
- `provider.apiKey`
|
|
82
|
+
- `provider.model`
|
|
83
|
+
- optional `channels.telegram.botToken`
|
|
84
|
+
- `personality` (`Assistant`, `Meowl`, `Coder`)
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
provider:
|
|
90
|
+
baseUrl: "https://api.openai.com/v1"
|
|
91
|
+
apiKey: "sk-..."
|
|
92
|
+
model: "gpt-4o"
|
|
93
|
+
channels:
|
|
94
|
+
telegram:
|
|
95
|
+
botToken: "123456789:ABC..."
|
|
96
|
+
personality: "Assistant"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Sessions and Commands
|
|
100
|
+
|
|
101
|
+
Sessions are persisted as JSON files under:
|
|
102
|
+
|
|
103
|
+
- `~/.openpaw/workspace/sessions`
|
|
104
|
+
|
|
105
|
+
TUI slash commands:
|
|
106
|
+
|
|
107
|
+
- `/new` start a new thread
|
|
108
|
+
- `/sessions` list saved sessions
|
|
109
|
+
- `/resume N` resume by number
|
|
110
|
+
|
|
111
|
+
Telegram bot commands:
|
|
112
|
+
|
|
113
|
+
- `/new`
|
|
114
|
+
- `/sessions`
|
|
115
|
+
- `/resume N`
|
|
116
|
+
|
|
117
|
+
## Architecture
|
|
118
|
+
|
|
119
|
+
Top-level modules:
|
|
120
|
+
|
|
121
|
+
- `agent/`
|
|
122
|
+
Agent runtime, model wiring (`@ai-sdk/openai-compatible`), structured prompt building (identity, channel hints, optional project context from cwd), tools (`bash`, `file_editor`, `list_dir`, `memory`), session persistence
|
|
123
|
+
- `gateway/`
|
|
124
|
+
Shared runtime bootstrap + channel adapters (Telegram + channel orchestration)
|
|
125
|
+
- `cli/`
|
|
126
|
+
Commander CLI entrypoint and OpenTUI screens
|
|
127
|
+
- `config/`
|
|
128
|
+
Config schema, paths, and disk storage
|
|
129
|
+
|
|
130
|
+
Execution flow:
|
|
131
|
+
|
|
132
|
+
1. CLI command starts (`cli/openpaw.tsx`)
|
|
133
|
+
2. Gateway context loads config and ensures workspace layout
|
|
134
|
+
3. Agent runtime is created with tools scoped to workspace root
|
|
135
|
+
4. Channel (TUI or Telegram) forwards user text to `runtime.runTurn(...)`
|
|
136
|
+
5. Assistant output streams back and session history is saved
|
|
137
|
+
|
|
138
|
+
## Notes
|
|
139
|
+
|
|
140
|
+
- Background gateway state files:
|
|
141
|
+
- PID: `~/.openpaw/gateway/gateway.pid`
|
|
142
|
+
- stdout log: `~/.openpaw/gateway/gateway.log`
|
|
143
|
+
- stderr log: `~/.openpaw/gateway/gateway.err.log`
|
|
144
|
+
- For development from source, you can still run: `bun run openpaw ...`
|
package/agent/agent.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAgentUIStream,
|
|
3
|
+
generateId,
|
|
4
|
+
ToolLoopAgent,
|
|
5
|
+
validateUIMessages,
|
|
6
|
+
} from "ai";
|
|
7
|
+
import type { OpenPawConfig } from "../config/types";
|
|
8
|
+
import { buildSystemPrompt } from "./prompt-builder";
|
|
9
|
+
import { MemoryStore } from "./memory-store";
|
|
10
|
+
import { createLanguageModel } from "./model";
|
|
11
|
+
import {
|
|
12
|
+
refreshSkillCatalog,
|
|
13
|
+
type OpenPawSkillCatalog,
|
|
14
|
+
} from "./skill-catalog";
|
|
15
|
+
import { loadSessionMessages, saveSessionMessages } from "./session-store";
|
|
16
|
+
import { createBashTool } from "./tools/bash";
|
|
17
|
+
import { createFileEditorTool } from "./tools/file-editor";
|
|
18
|
+
import { createListDirTool } from "./tools/list-dir";
|
|
19
|
+
import { createLoadSkillTool } from "./tools/load-skill";
|
|
20
|
+
import { createMemoryTool } from "./tools/memory";
|
|
21
|
+
import type { OpenPawSurface, RunTurnParams } from "./types";
|
|
22
|
+
import { getTurnSurface, isSandboxRestricted, runWithTurnContext } from "./turn-context";
|
|
23
|
+
|
|
24
|
+
const STATIC_AGENT_INSTRUCTIONS = [
|
|
25
|
+
"You are OpenPaw, a capable local assistant.",
|
|
26
|
+
"Follow the system prompt: identity, voice with the user, workspace content, tools, and channel hints.",
|
|
27
|
+
"Use tools faithfully: file_editor (view before str_replace; exact single match for old_str), bash, list_dir, load_skill, memory.",
|
|
28
|
+
"Memory tool: add uses content only; replace needs old_text + content; remove needs old_text only.",
|
|
29
|
+
"load_skill: use when a listed skill fits the task; follow loaded instructions and use skillDirectory for bundled file paths.",
|
|
30
|
+
"To the user: sound human; recall context naturally. Do not mention workspace filenames, profile files, or tool names unless they are developers debugging.",
|
|
31
|
+
].join(" ");
|
|
32
|
+
|
|
33
|
+
/** Instantiates all tools, including `load_skill` and filesystem tools that rescan {@link OpenPawSkillCatalog}. */
|
|
34
|
+
function createTools(workspacePath: string, memoryStore: MemoryStore, skillCatalog: OpenPawSkillCatalog) {
|
|
35
|
+
return {
|
|
36
|
+
bash: createBashTool(workspacePath),
|
|
37
|
+
file_editor: createFileEditorTool(workspacePath, skillCatalog),
|
|
38
|
+
list_dir: createListDirTool(workspacePath, skillCatalog),
|
|
39
|
+
load_skill: createLoadSkillTool(skillCatalog),
|
|
40
|
+
memory: createMemoryTool(memoryStore),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type OpenPawTools = ReturnType<typeof createTools>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Derives chat surface from session id when callers do not pass `surface` explicitly.
|
|
48
|
+
*/
|
|
49
|
+
export function surfaceFromSessionId(sessionId: string): OpenPawSurface {
|
|
50
|
+
return sessionId.startsWith("telegram:") ? "telegram" : "cli";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a {@link ToolLoopAgent} with workspace-scoped tools, curated memory, and a dynamic system prompt.
|
|
55
|
+
*
|
|
56
|
+
* @param skillCatalog Mutable skill list for this process; rescanned from disk before each model call and in `load_skill`.
|
|
57
|
+
*/
|
|
58
|
+
export function createOpenPawAgent(
|
|
59
|
+
config: OpenPawConfig,
|
|
60
|
+
workspacePath: string,
|
|
61
|
+
memoryStore: MemoryStore,
|
|
62
|
+
skillCatalog: OpenPawSkillCatalog,
|
|
63
|
+
) {
|
|
64
|
+
const tools = createTools(workspacePath, memoryStore, skillCatalog);
|
|
65
|
+
return new ToolLoopAgent({
|
|
66
|
+
model: createLanguageModel(config),
|
|
67
|
+
instructions: STATIC_AGENT_INSTRUCTIONS,
|
|
68
|
+
tools,
|
|
69
|
+
prepareCall: async (options) => {
|
|
70
|
+
await refreshSkillCatalog(skillCatalog);
|
|
71
|
+
let instructions = await buildSystemPrompt({
|
|
72
|
+
workspacePath,
|
|
73
|
+
personality: config.personality,
|
|
74
|
+
surface: getTurnSurface(),
|
|
75
|
+
memoryUserBlock: memoryStore.formatForSystemPrompt("user"),
|
|
76
|
+
memoryAgentBlock: memoryStore.formatForSystemPrompt("memory"),
|
|
77
|
+
skills: skillCatalog.skills,
|
|
78
|
+
});
|
|
79
|
+
if (!isSandboxRestricted()) {
|
|
80
|
+
instructions +=
|
|
81
|
+
"\n\n## Sandbox (this turn)\nFilesystem sandbox is OFF: file_editor and list_dir may use absolute paths or paths anywhere under the filesystem root; bash runs with cwd set to the user home directory, not the workspace root.";
|
|
82
|
+
}
|
|
83
|
+
return { ...options, instructions };
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type OpenPawAgent = ReturnType<typeof createOpenPawAgent>;
|
|
89
|
+
|
|
90
|
+
export type AgentRuntime = {
|
|
91
|
+
config: OpenPawConfig;
|
|
92
|
+
workspacePath: string;
|
|
93
|
+
/** Curated memory store (frozen snapshot + tool mutations). */
|
|
94
|
+
memoryStore: MemoryStore;
|
|
95
|
+
agent: OpenPawAgent;
|
|
96
|
+
runTurn: (params: RunTurnParams) => Promise<{ text: string }>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Loads memory, discovers skills under the workspace and user config dirs, and builds the shared runtime.
|
|
101
|
+
*/
|
|
102
|
+
export async function createAgentRuntime(
|
|
103
|
+
config: OpenPawConfig,
|
|
104
|
+
workspacePath: string,
|
|
105
|
+
): Promise<AgentRuntime> {
|
|
106
|
+
const memoryStore = new MemoryStore(workspacePath);
|
|
107
|
+
memoryStore.loadFromDisk();
|
|
108
|
+
const skillCatalog: OpenPawSkillCatalog = { workspacePath, skills: [] };
|
|
109
|
+
await refreshSkillCatalog(skillCatalog);
|
|
110
|
+
const agent = createOpenPawAgent(config, workspacePath, memoryStore, skillCatalog);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
config,
|
|
114
|
+
workspacePath,
|
|
115
|
+
memoryStore,
|
|
116
|
+
agent,
|
|
117
|
+
runTurn: async (params) => runTurnWithAgent(agent, params),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runTurnWithAgent(
|
|
122
|
+
agent: OpenPawAgent,
|
|
123
|
+
params: RunTurnParams,
|
|
124
|
+
): Promise<{ text: string }> {
|
|
125
|
+
const {
|
|
126
|
+
sessionId,
|
|
127
|
+
userText,
|
|
128
|
+
sandboxRestricted = true,
|
|
129
|
+
surface = surfaceFromSessionId(sessionId),
|
|
130
|
+
onTextDelta,
|
|
131
|
+
onReasoningDelta,
|
|
132
|
+
onToolStatus,
|
|
133
|
+
} = params;
|
|
134
|
+
|
|
135
|
+
return runWithTurnContext({ sandboxRestricted, surface }, async () => {
|
|
136
|
+
const prior = await loadSessionMessages(sessionId, agent.tools);
|
|
137
|
+
const userMessage = {
|
|
138
|
+
id: generateId(),
|
|
139
|
+
role: "user" as const,
|
|
140
|
+
parts: [{ type: "text" as const, text: userText }],
|
|
141
|
+
};
|
|
142
|
+
const draft = [...prior, userMessage];
|
|
143
|
+
|
|
144
|
+
const uiMessages = await validateUIMessages({
|
|
145
|
+
messages: draft,
|
|
146
|
+
tools: agent.tools as never,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
let accumulated = "";
|
|
150
|
+
const toolNameByCallId = new Map<string, string>();
|
|
151
|
+
|
|
152
|
+
const stream = await createAgentUIStream({
|
|
153
|
+
agent,
|
|
154
|
+
uiMessages,
|
|
155
|
+
onFinish: async ({ messages }) => {
|
|
156
|
+
await saveSessionMessages(sessionId, messages);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
for await (const chunk of stream) {
|
|
161
|
+
if (chunk.type === "text-delta" && "delta" in chunk) {
|
|
162
|
+
const d = chunk.delta;
|
|
163
|
+
accumulated += d;
|
|
164
|
+
onTextDelta?.(d);
|
|
165
|
+
} else if (chunk.type === "error") {
|
|
166
|
+
const errLine = `Error: ${chunk.errorText}`;
|
|
167
|
+
accumulated += errLine;
|
|
168
|
+
onTextDelta?.(errLine);
|
|
169
|
+
} else if (chunk.type === "reasoning-delta" && "delta" in chunk) {
|
|
170
|
+
onReasoningDelta?.(chunk.delta);
|
|
171
|
+
} else if (chunk.type === "tool-input-start") {
|
|
172
|
+
toolNameByCallId.set(chunk.toolCallId, chunk.toolName);
|
|
173
|
+
} else if (chunk.type === "tool-input-available") {
|
|
174
|
+
toolNameByCallId.set(chunk.toolCallId, chunk.toolName);
|
|
175
|
+
onToolStatus?.({
|
|
176
|
+
type: "tool_input",
|
|
177
|
+
toolCallId: chunk.toolCallId,
|
|
178
|
+
toolName: chunk.toolName,
|
|
179
|
+
input: chunk.input,
|
|
180
|
+
});
|
|
181
|
+
} else if (chunk.type === "tool-input-error") {
|
|
182
|
+
toolNameByCallId.set(chunk.toolCallId, chunk.toolName);
|
|
183
|
+
onToolStatus?.({
|
|
184
|
+
type: "tool_error",
|
|
185
|
+
toolCallId: chunk.toolCallId,
|
|
186
|
+
toolName: chunk.toolName,
|
|
187
|
+
errorText: chunk.errorText,
|
|
188
|
+
});
|
|
189
|
+
} else if (chunk.type === "tool-output-available") {
|
|
190
|
+
const toolName = toolNameByCallId.get(chunk.toolCallId) ?? "tool";
|
|
191
|
+
onToolStatus?.({
|
|
192
|
+
type: "tool_output",
|
|
193
|
+
toolCallId: chunk.toolCallId,
|
|
194
|
+
toolName,
|
|
195
|
+
output: chunk.output,
|
|
196
|
+
});
|
|
197
|
+
} else if (chunk.type === "tool-output-error") {
|
|
198
|
+
const toolName = toolNameByCallId.get(chunk.toolCallId) ?? "tool";
|
|
199
|
+
onToolStatus?.({
|
|
200
|
+
type: "tool_error",
|
|
201
|
+
toolCallId: chunk.toolCallId,
|
|
202
|
+
toolName,
|
|
203
|
+
errorText: chunk.errorText,
|
|
204
|
+
});
|
|
205
|
+
} else if (chunk.type === "tool-output-denied") {
|
|
206
|
+
const toolName = toolNameByCallId.get(chunk.toolCallId) ?? "tool";
|
|
207
|
+
onToolStatus?.({
|
|
208
|
+
type: "tool_denied",
|
|
209
|
+
toolCallId: chunk.toolCallId,
|
|
210
|
+
toolName,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { text: accumulated };
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight scanning and truncation for text injected into the system prompt (Hermes-style).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Regex patterns with stable ids for logging / blocked messages. */
|
|
6
|
+
const CONTEXT_THREAT_PATTERNS: [RegExp, string][] = [
|
|
7
|
+
[/ignore\s+(previous|all|above|prior)\s+instructions/i, "prompt_injection"],
|
|
8
|
+
[/do\s+not\s+tell\s+the\s+user/i, "deception_hide"],
|
|
9
|
+
[/system\s+prompt\s+override/i, "sys_prompt_override"],
|
|
10
|
+
[/disregard\s+(your|all|any)\s+(instructions|rules|guidelines)/i, "disregard_rules"],
|
|
11
|
+
[
|
|
12
|
+
/act\s+as\s+(if|though)\s+you\s+(have\s+no|don't\s+have)\s+(restrictions|limits|rules)/i,
|
|
13
|
+
"bypass_restrictions",
|
|
14
|
+
],
|
|
15
|
+
[/<!--[^>]*(?:ignore|override|system|secret|hidden)[^>]*-->/i, "html_comment_injection"],
|
|
16
|
+
[/<\s*div\s+style\s*=\s*["'].*display\s*:\s*none/i, "hidden_div"],
|
|
17
|
+
[/translate\s+.*\s+into\s+.*\s+and\s+(execute|run|eval)/i, "translate_execute"],
|
|
18
|
+
[/curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)/i, "exfil_curl"],
|
|
19
|
+
[/cat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass)/i, "read_secrets"],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const CONTEXT_INVISIBLE_CHARS = new Set([
|
|
23
|
+
"\u200b",
|
|
24
|
+
"\u200c",
|
|
25
|
+
"\u200d",
|
|
26
|
+
"\u2060",
|
|
27
|
+
"\ufeff",
|
|
28
|
+
"\u202a",
|
|
29
|
+
"\u202b",
|
|
30
|
+
"\u202c",
|
|
31
|
+
"\u202d",
|
|
32
|
+
"\u202e",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const CONTEXT_FILE_MAX_CHARS = 20_000;
|
|
36
|
+
const CONTEXT_TRUNCATE_HEAD_RATIO = 0.7;
|
|
37
|
+
const CONTEXT_TRUNCATE_TAIL_RATIO = 0.2;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Scans context file content for suspicious patterns. Returns sanitized replacement if blocked.
|
|
41
|
+
*/
|
|
42
|
+
export function scanContextContent(content: string, filename: string): string {
|
|
43
|
+
const findings: string[] = [];
|
|
44
|
+
|
|
45
|
+
for (const char of CONTEXT_INVISIBLE_CHARS) {
|
|
46
|
+
if (content.includes(char)) {
|
|
47
|
+
findings.push(`invisible unicode U+${char.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [re, pid] of CONTEXT_THREAT_PATTERNS) {
|
|
52
|
+
if (re.test(content)) {
|
|
53
|
+
findings.push(pid);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (findings.length > 0) {
|
|
58
|
+
return `[BLOCKED: ${filename} contained potential prompt injection (${findings.join(", ")}). Content not loaded.]`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Head/tail truncation with a middle marker when content exceeds maxChars.
|
|
66
|
+
*/
|
|
67
|
+
export function truncateContextContent(
|
|
68
|
+
content: string,
|
|
69
|
+
filename: string,
|
|
70
|
+
maxChars: number = CONTEXT_FILE_MAX_CHARS,
|
|
71
|
+
): string {
|
|
72
|
+
if (content.length <= maxChars) {
|
|
73
|
+
return content;
|
|
74
|
+
}
|
|
75
|
+
const headChars = Math.floor(maxChars * CONTEXT_TRUNCATE_HEAD_RATIO);
|
|
76
|
+
const tailChars = Math.floor(maxChars * CONTEXT_TRUNCATE_TAIL_RATIO);
|
|
77
|
+
const head = content.slice(0, headChars);
|
|
78
|
+
const tail = content.slice(-tailChars);
|
|
79
|
+
const marker = `\n\n[...truncated ${filename}: kept ${headChars}+${tailChars} of ${content.length} chars. Use file tools to read the full file.]\n\n`;
|
|
80
|
+
return head + marker + tail;
|
|
81
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory undo stacks for the file editor tool: each absolute file path maps to
|
|
3
|
+
* a stack of prior UTF-8 contents, pushed before mutating operations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const editHistory = new Map<string, string[]>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Records the previous file contents so the next {@link popHistory} can restore it.
|
|
10
|
+
*/
|
|
11
|
+
export function pushHistory(filePath: string, previousContent: string): void {
|
|
12
|
+
if (!editHistory.has(filePath)) {
|
|
13
|
+
editHistory.set(filePath, []);
|
|
14
|
+
}
|
|
15
|
+
editHistory.get(filePath)!.push(previousContent);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Removes and returns the most recent snapshot for {@link filePath}, or null if none.
|
|
20
|
+
*/
|
|
21
|
+
export function popHistory(filePath: string): string | null {
|
|
22
|
+
const stack = editHistory.get(filePath);
|
|
23
|
+
if (!stack || stack.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return stack.pop()!;
|
|
27
|
+
}
|
package/agent/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type { SessionId, RunTurnParams, OpenPawSurface } from "./types";
|
|
2
|
+
export type { BuildSystemPromptOptions } from "./prompt-builder";
|
|
3
|
+
export { MemoryStore } from "./memory-store";
|
|
4
|
+
export { buildSystemPrompt } from "./prompt-builder";
|
|
5
|
+
export { createLanguageModel } from "./model";
|
|
6
|
+
export {
|
|
7
|
+
loadSessionMessages,
|
|
8
|
+
saveSessionMessages,
|
|
9
|
+
getSessionFilePath,
|
|
10
|
+
sessionIdToFilename,
|
|
11
|
+
} from "./session-store";
|
|
12
|
+
export type { SessionFileV1 } from "./session-store";
|
|
13
|
+
export {
|
|
14
|
+
createOpenPawAgent,
|
|
15
|
+
createAgentRuntime,
|
|
16
|
+
surfaceFromSessionId,
|
|
17
|
+
type OpenPawAgent,
|
|
18
|
+
type AgentRuntime,
|
|
19
|
+
type OpenPawTools,
|
|
20
|
+
} from "./agent";
|
|
21
|
+
export type { OpenPawSkillCatalog } from "./skill-catalog";
|
|
22
|
+
export { refreshSkillCatalog, skillScanDirsForWorkspace } from "./skill-catalog";
|
|
23
|
+
export {
|
|
24
|
+
ensureWorkspaceLayout,
|
|
25
|
+
resetWorkspaceToOnboardingDefaults,
|
|
26
|
+
} from "./workspace-bootstrap";
|
|
27
|
+
export {
|
|
28
|
+
DEFAULT_AGENTS_MD,
|
|
29
|
+
DEFAULT_SOUL_MD,
|
|
30
|
+
DEFAULT_USER_MD,
|
|
31
|
+
} from "./workspace-bootstrap";
|