@openape/ape-agent 2.0.2 → 2.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/default-skills/bash/SKILL.md +78 -0
- package/default-skills/file/SKILL.md +37 -0
- package/default-skills/http/SKILL.md +47 -0
- package/default-skills/mail/SKILL.md +38 -0
- package/default-skills/tasks/SKILL.md +47 -0
- package/default-skills/time/SKILL.md +33 -0
- package/dist/bridge.mjs +176 -15
- package/package.json +4 -3
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bash
|
|
3
|
+
description: When a task can't be done with the other curated tools (file.read, http.get, tasks.create, mail.list, etc.), use bash — runs any shell command on the agent host through the DDISA grant cycle.
|
|
4
|
+
requires_tools: [bash]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Shell access via ape-shell
|
|
8
|
+
|
|
9
|
+
## What this is
|
|
10
|
+
|
|
11
|
+
The `bash` tool spawns `ape-shell -c '<cmd>'` on the agent host. `ape-shell` is the human owner's interactive shell wrapper — every command goes through the OpenApe DDISA grant cycle:
|
|
12
|
+
|
|
13
|
+
- Auto-approved if a YOLO scope matches (owner has pre-approved this command pattern)
|
|
14
|
+
- Otherwise the owner gets a push notification on their phone with the exact command to approve or deny
|
|
15
|
+
- Approval takes ~3–15s typically; budget is 5min before the call times out
|
|
16
|
+
|
|
17
|
+
You run as the agent's hidden macOS user, so the filesystem and network you see are what *that* user sees — already jailed.
|
|
18
|
+
|
|
19
|
+
## When to prefer bash over a curated tool
|
|
20
|
+
|
|
21
|
+
Curated tools (time.now, http.get, file.read, tasks.list, mail.list) are **always preferred** when they apply:
|
|
22
|
+
|
|
23
|
+
- Faster (no grant cycle)
|
|
24
|
+
- Structured JSON returns instead of stdout parsing
|
|
25
|
+
- Lower risk score (visible to the owner in troop UI)
|
|
26
|
+
|
|
27
|
+
Reach for `bash` when:
|
|
28
|
+
|
|
29
|
+
- The curated tool doesn't cover the case (e.g. `git status`, `iurio cases search`, `o365-cli mail trash <id>`)
|
|
30
|
+
- You need to chain commands with pipes / loops / redirects
|
|
31
|
+
- You need an auth header the deny-list strips (e.g. `curl -H 'Authorization: …'`)
|
|
32
|
+
|
|
33
|
+
## Patterns
|
|
34
|
+
|
|
35
|
+
Read system info:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
bash({ "cmd": "uname -a && uptime" })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Run a CLI:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
bash({ "cmd": "ape-tasks list --status open,doing --json" })
|
|
45
|
+
bash({ "cmd": "iurio cases search 'foo'" })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Quote-heavy commands — wrap in single-quotes outside, escape inside:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
bash({ "cmd": "find ~/Downloads -name '*.pdf' -mtime -7 -exec ls -lh {} +" })
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Long-running command — use the `timeout_ms` param (default 5 min):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
bash({ "cmd": "pnpm test", "timeout_ms": 180000 })
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Anti-patterns
|
|
61
|
+
|
|
62
|
+
- **Don't** use `bash date` for the time — call `time.now` (in-process, no grant).
|
|
63
|
+
- **Don't** use `bash cat ~/notes.md` for a $HOME read — call `file.read` (no grant).
|
|
64
|
+
- **Don't** retry a denied/timed-out approval automatically — the owner saw the prompt and chose. Surface the timeout clearly and ask what they'd like to do.
|
|
65
|
+
- **Don't** chain destructive commands in one call (`rm -rf … && …`). One destructive command per approval so the owner can decide each.
|
|
66
|
+
|
|
67
|
+
## Response shape
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"stdout": "...",
|
|
72
|
+
"stderr": "...",
|
|
73
|
+
"exit_code": 0,
|
|
74
|
+
"timed_out": false
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Non-zero `exit_code` means the command failed — read `stderr` and decide whether to retry with a corrected command, surface the error to the user, or ask for guidance. **Don't** pretend it succeeded.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: file
|
|
3
|
+
description: When the user asks you to read, write, or check a file in your home directory, use the file.read / file.write tools — they're $HOME-jailed and safer than bash cat.
|
|
4
|
+
requires_tools: [file.read]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Files in $HOME
|
|
8
|
+
|
|
9
|
+
## Jail
|
|
10
|
+
|
|
11
|
+
Both tools are restricted to the agent's `$HOME` directory:
|
|
12
|
+
|
|
13
|
+
- `path` is resolved relative to `$HOME` (`~/` prefix accepted, or plain `notes.md`)
|
|
14
|
+
- `..` segments that would escape `$HOME` are rejected
|
|
15
|
+
- 1 MB cap per read/write — anything bigger gets truncated (read) or rejected (write)
|
|
16
|
+
|
|
17
|
+
For files **outside** `$HOME` (e.g. `/etc/...`, another user's directory), use `bash` with `cat`/`tee` — that goes through the DDISA grant cycle so the owner can approve broad-fs reads explicitly.
|
|
18
|
+
|
|
19
|
+
## Patterns
|
|
20
|
+
|
|
21
|
+
Read a JSON config:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
file.read({ "path": "~/.openape/agent/agent.json" })
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Append a note (read-modify-write — there's no `file.append`):
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
const r = file.read({ "path": "notes.md" })
|
|
31
|
+
file.write({ "path": "notes.md", "content": r.content + "\n- new line\n" })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Conventions
|
|
35
|
+
|
|
36
|
+
- Paths in user prompts ("save this to notes.md") → relative to `$HOME`. Don't prefix with `/Users/...`.
|
|
37
|
+
- Binary files don't round-trip via these tools (UTF-8 only). Use `bash` for non-text I/O.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: http
|
|
3
|
+
description: When the user asks to fetch a webpage, hit a REST API, or POST JSON to an endpoint, use the http.get / http.post tools — never invent URLs.
|
|
4
|
+
requires_tools: [http.get]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# HTTP fetch
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- `http.get` — read content from any HTTPS URL (webpages, REST endpoints, JSON APIs)
|
|
12
|
+
- `http.post` — POST JSON to an HTTPS URL
|
|
13
|
+
|
|
14
|
+
Both are bounded:
|
|
15
|
+
|
|
16
|
+
- Response capped at 1 MB (anything longer is truncated)
|
|
17
|
+
- Headers go through a deny-list (no `Authorization` for arbitrary hosts, no `Cookie`)
|
|
18
|
+
- HTTP-only URLs are rejected — HTTPS required
|
|
19
|
+
|
|
20
|
+
For commands that go beyond simple HTTP (auth, mTLS, complex curl flags, multipart upload), use the `bash` tool with `curl` instead.
|
|
21
|
+
|
|
22
|
+
## Patterns
|
|
23
|
+
|
|
24
|
+
Fetch JSON:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
http.get({
|
|
28
|
+
"url": "https://api.example.com/users/42",
|
|
29
|
+
"headers": { "Accept": "application/json" }
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
POST JSON:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
http.post({
|
|
37
|
+
"url": "https://api.example.com/notes",
|
|
38
|
+
"body": { "title": "from agent", "body": "..." },
|
|
39
|
+
"headers": { "Content-Type": "application/json" }
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Anti-patterns
|
|
44
|
+
|
|
45
|
+
- Don't synthesize URLs ("I think it's at /api/v1/foo") — ask the user for the exact endpoint or use `http.get` only after you've seen it in their message or in a tool result.
|
|
46
|
+
- Don't paginate by manually incrementing offsets without checking the API's actual contract — read response shape first.
|
|
47
|
+
- For auth that needs `Authorization: Bearer …`, the deny-list strips it. Use `bash` with `curl` if you need it.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mail
|
|
3
|
+
description: When the user asks about their inbox — what's there, search for an email, recent unread — use mail.list / mail.search.
|
|
4
|
+
requires_tools: [mail.list]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Inbox (o365-cli)
|
|
8
|
+
|
|
9
|
+
## What this is
|
|
10
|
+
|
|
11
|
+
Read access to the owner's Microsoft 365 inbox via the `o365-cli` tool on the agent host. The host must have `o365-cli` installed and authenticated (the owner's CLI session). If it isn't, both tools fail with a clear setup error.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
- `mail.list` — recent inbox messages. Optional `unread_only: true`. Default limit 20.
|
|
16
|
+
- `mail.search` — keyword/from/subject search. Pass a query string.
|
|
17
|
+
|
|
18
|
+
For writing, archiving, replying, or moving messages: use `bash` with explicit `o365-cli` subcommands (see `o365-cli mail --help`). Those go through the DDISA grant cycle so the owner approves each mutation.
|
|
19
|
+
|
|
20
|
+
## Patterns
|
|
21
|
+
|
|
22
|
+
Latest 10 unread:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
mail.list({ "limit": 10, "unread_only": true })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Search by sender:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
mail.search({ "q": "from:smaurer@deloitte.at", "limit": 20 })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Conventions
|
|
35
|
+
|
|
36
|
+
- Don't draft replies inside the agent if the user just asked "what's in my inbox" — listing is read-only, replies are explicit.
|
|
37
|
+
- When the user wants to triage ("welche kann ich archivieren?"), list first, then offer specific candidates with message IDs — do NOT auto-move anything.
|
|
38
|
+
- Account names: there are usually two — owner's primary email (Delta Mind) and a secondary (Legal Tech / DOCPIT). Ask which one if it matters.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tasks
|
|
3
|
+
description: When the user wants to see, create, or schedule a task/reminder/wiedervorlage on their personal task list, use tasks.list / tasks.create.
|
|
4
|
+
requires_tools: [tasks.list]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Personal tasks (ape-tasks)
|
|
8
|
+
|
|
9
|
+
## What this is
|
|
10
|
+
|
|
11
|
+
The owner's personal task list at https://tasks.openape.ai — same one their `ape-tasks` CLI on Mac and the iOS app use. You're allowed to read it and add to it via the `tasks.list` and `tasks.create` tools.
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
Use `tasks.create` whenever the user asks for any of:
|
|
16
|
+
|
|
17
|
+
- "Reminder me to X tomorrow"
|
|
18
|
+
- "Wiedervorlage in 2 Tagen"
|
|
19
|
+
- "Add to my todo: …"
|
|
20
|
+
- "Schedule X for next week"
|
|
21
|
+
|
|
22
|
+
Use `tasks.list` when they ask "what's on my list", "any open tasks", "remind me what's due".
|
|
23
|
+
|
|
24
|
+
## Create — parameters
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
tasks.create({
|
|
28
|
+
"title": "<short title>",
|
|
29
|
+
"notes": "<optional longer body>",
|
|
30
|
+
"priority": "low" | "med" | "high", // default med
|
|
31
|
+
"due_at": "<ISO 8601 or relative: +2h | +1d | tomorrow 9am>"
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For wiedervorlage (mail-eskalation) the owner has a separate flow via `ape-tasks new --remind-at ...` — that path triggers email reminders. If the user wants a *reminder* (not just a todo), prefer:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
bash({ "cmd": "ape-tasks new --title '...' --remind-at '+2d' --context-summary '...' --context-url '...'" })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
…because the CLI has more knobs than the tool exposes (assignee, remind-at, context-url).
|
|
42
|
+
|
|
43
|
+
## Conventions
|
|
44
|
+
|
|
45
|
+
- Always convert relative dates from the user prompt to an absolute date in your response: "in 2 Tagen" → "am 13. Mai 2026".
|
|
46
|
+
- Don't create duplicate tasks — `tasks.list` first if the user might have one already.
|
|
47
|
+
- If the user asks for "remind me on …" and you only have `tasks.create`, set `due_at` and explain that the **list** will surface it, but no push notification fires unless they used `ape-tasks --remind-at`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: time
|
|
3
|
+
description: When the user asks for the current time, date, or wants a sanity-check that the runtime is alive, use the time.now tool — never guess.
|
|
4
|
+
requires_tools: [time.now]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Time and date
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
The `time.now` tool returns the current UTC timestamp as ISO 8601, plus the epoch in seconds and the agent host's timezone offset in minutes. Call it any time the user asks "what time is it", "what day", "how long ago was X", or as a quick "are you alive?" probe.
|
|
12
|
+
|
|
13
|
+
## How to use
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
time.now({})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
No arguments. Response shape:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"iso": "2026-05-11T07:14:44Z",
|
|
24
|
+
"epoch_seconds": 1778383484,
|
|
25
|
+
"timezone_offset_minutes": 120
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Conventions
|
|
30
|
+
|
|
31
|
+
- Always report time in **both** UTC and the user's local clock when the offset is non-zero. Example: "Es ist 07:14 UTC, also 09:14 bei dir (UTC+2)."
|
|
32
|
+
- For relative-time questions ("vor 3 Tagen"), compute from `epoch_seconds` — don't rely on the LLM's internal clock guess.
|
|
33
|
+
- Do NOT call `bash` with `date` for the time — `time.now` is in-process and skips the DDISA grant cycle.
|
package/dist/bridge.mjs
CHANGED
|
@@ -1040,9 +1040,9 @@ var init_chunk_7OCVIDC7 = __esm({
|
|
|
1040
1040
|
});
|
|
1041
1041
|
|
|
1042
1042
|
// src/bridge.ts
|
|
1043
|
-
import { existsSync as
|
|
1044
|
-
import { homedir as
|
|
1045
|
-
import { join as
|
|
1043
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
1044
|
+
import { homedir as homedir9 } from "os";
|
|
1045
|
+
import { join as join9 } from "path";
|
|
1046
1046
|
import process2 from "process";
|
|
1047
1047
|
|
|
1048
1048
|
// ../../packages/cli-auth/dist/index.js
|
|
@@ -3799,6 +3799,162 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
|
|
|
3799
3799
|
return allowlist.has(peer);
|
|
3800
3800
|
}
|
|
3801
3801
|
|
|
3802
|
+
// src/skills.ts
|
|
3803
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "fs";
|
|
3804
|
+
import { homedir as homedir8 } from "os";
|
|
3805
|
+
import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
|
|
3806
|
+
import { fileURLToPath } from "url";
|
|
3807
|
+
var SKILLS_SUBDIR = [".openape", "agent", "skills"];
|
|
3808
|
+
var SOUL_PATH_PARTS = [".openape", "agent", "SOUL.md"];
|
|
3809
|
+
function soulPath(home = homedir8()) {
|
|
3810
|
+
return join8(home, ...SOUL_PATH_PARTS);
|
|
3811
|
+
}
|
|
3812
|
+
function skillsDir(home = homedir8()) {
|
|
3813
|
+
return join8(home, ...SKILLS_SUBDIR);
|
|
3814
|
+
}
|
|
3815
|
+
function readSoul(home = homedir8()) {
|
|
3816
|
+
const path = soulPath(home);
|
|
3817
|
+
if (!existsSync6(path)) return null;
|
|
3818
|
+
try {
|
|
3819
|
+
const body = readFileSync7(path, "utf8").trim();
|
|
3820
|
+
return body.length > 0 ? body : null;
|
|
3821
|
+
} catch {
|
|
3822
|
+
return null;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
function parseFrontmatter(content) {
|
|
3826
|
+
const trimmed = content.trimStart();
|
|
3827
|
+
if (!trimmed.startsWith("---")) return null;
|
|
3828
|
+
const closeIdx = trimmed.indexOf("\n---", 3);
|
|
3829
|
+
if (closeIdx < 0) return null;
|
|
3830
|
+
const fmBlock = trimmed.slice(3, closeIdx).trim();
|
|
3831
|
+
const fields = {};
|
|
3832
|
+
let arrayKey = null;
|
|
3833
|
+
let arrayBuf = [];
|
|
3834
|
+
for (const rawLine of fmBlock.split("\n")) {
|
|
3835
|
+
const line = rawLine.replace(/\r$/, "");
|
|
3836
|
+
if (arrayKey) {
|
|
3837
|
+
const m2 = line.match(/^[\t ]*-[\t ]+(\S.*)$/);
|
|
3838
|
+
if (m2) {
|
|
3839
|
+
arrayBuf.push(m2[1].trim());
|
|
3840
|
+
continue;
|
|
3841
|
+
}
|
|
3842
|
+
fields[arrayKey] = arrayBuf.join(",");
|
|
3843
|
+
arrayKey = null;
|
|
3844
|
+
arrayBuf = [];
|
|
3845
|
+
}
|
|
3846
|
+
const kv = line.match(/^([a-z_]\w*)[\t ]*:[\t ]?(.*)$/i);
|
|
3847
|
+
if (!kv) continue;
|
|
3848
|
+
const [, key, value] = kv;
|
|
3849
|
+
if (value.trim() === "") {
|
|
3850
|
+
arrayKey = key;
|
|
3851
|
+
arrayBuf = [];
|
|
3852
|
+
continue;
|
|
3853
|
+
}
|
|
3854
|
+
const inlineArray = value.match(/^\[(.*)\]$/);
|
|
3855
|
+
if (inlineArray) {
|
|
3856
|
+
fields[key] = inlineArray[1].split(",").map((s2) => s2.trim().replace(/^["']|["']$/g, "")).filter(Boolean).join(",");
|
|
3857
|
+
continue;
|
|
3858
|
+
}
|
|
3859
|
+
fields[key] = value.trim().replace(/^["']|["']$/g, "");
|
|
3860
|
+
}
|
|
3861
|
+
if (arrayKey) fields[arrayKey] = arrayBuf.join(",");
|
|
3862
|
+
if (!fields.name || !fields.description) return null;
|
|
3863
|
+
const requiresTools = fields.requires_tools ? fields.requires_tools.split(",").map((s2) => s2.trim()).filter(Boolean) : void 0;
|
|
3864
|
+
return { name: fields.name, description: fields.description, requiresTools };
|
|
3865
|
+
}
|
|
3866
|
+
function scanSkillsDir(dir) {
|
|
3867
|
+
if (!existsSync6(dir)) return [];
|
|
3868
|
+
let entries;
|
|
3869
|
+
try {
|
|
3870
|
+
entries = readdirSync4(dir);
|
|
3871
|
+
} catch {
|
|
3872
|
+
return [];
|
|
3873
|
+
}
|
|
3874
|
+
const out = [];
|
|
3875
|
+
for (const entry of entries) {
|
|
3876
|
+
const skillPath = join8(dir, entry, "SKILL.md");
|
|
3877
|
+
if (!existsSync6(skillPath)) continue;
|
|
3878
|
+
let st;
|
|
3879
|
+
try {
|
|
3880
|
+
st = statSync(skillPath);
|
|
3881
|
+
} catch {
|
|
3882
|
+
continue;
|
|
3883
|
+
}
|
|
3884
|
+
if (!st.isFile()) continue;
|
|
3885
|
+
let body;
|
|
3886
|
+
try {
|
|
3887
|
+
body = readFileSync7(skillPath, "utf8");
|
|
3888
|
+
} catch {
|
|
3889
|
+
continue;
|
|
3890
|
+
}
|
|
3891
|
+
const fm = parseFrontmatter(body);
|
|
3892
|
+
if (!fm) continue;
|
|
3893
|
+
out.push({
|
|
3894
|
+
name: fm.name,
|
|
3895
|
+
description: fm.description,
|
|
3896
|
+
filePath: skillPath,
|
|
3897
|
+
requiresTools: fm.requiresTools
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
return out;
|
|
3901
|
+
}
|
|
3902
|
+
function defaultSkillsDir() {
|
|
3903
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3904
|
+
return resolve2(here, "..", "default-skills");
|
|
3905
|
+
}
|
|
3906
|
+
function composeSkills(home, enabledTools) {
|
|
3907
|
+
const enabled = new Set(enabledTools);
|
|
3908
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3909
|
+
for (const s2 of scanSkillsDir(defaultSkillsDir())) byName.set(s2.name, s2);
|
|
3910
|
+
for (const s2 of scanSkillsDir(skillsDir(home))) byName.set(s2.name, s2);
|
|
3911
|
+
const out = [];
|
|
3912
|
+
for (const s2 of byName.values()) {
|
|
3913
|
+
if (s2.requiresTools && s2.requiresTools.length > 0) {
|
|
3914
|
+
const allPresent = s2.requiresTools.every((t2) => enabled.has(t2));
|
|
3915
|
+
if (!allPresent) continue;
|
|
3916
|
+
}
|
|
3917
|
+
out.push(s2);
|
|
3918
|
+
}
|
|
3919
|
+
out.sort((a2, b2) => a2.name.localeCompare(b2.name));
|
|
3920
|
+
return out;
|
|
3921
|
+
}
|
|
3922
|
+
function escapeXml(s2) {
|
|
3923
|
+
return s2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3924
|
+
}
|
|
3925
|
+
function formatSkillsBlock(skills) {
|
|
3926
|
+
if (skills.length === 0) return "";
|
|
3927
|
+
const lines = [
|
|
3928
|
+
"",
|
|
3929
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
3930
|
+
"Use the file.read tool to load a skill's file when the user's task matches its description.",
|
|
3931
|
+
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.",
|
|
3932
|
+
"",
|
|
3933
|
+
"<available_skills>"
|
|
3934
|
+
];
|
|
3935
|
+
for (const s2 of skills) {
|
|
3936
|
+
lines.push(" <skill>");
|
|
3937
|
+
lines.push(` <name>${escapeXml(s2.name)}</name>`);
|
|
3938
|
+
lines.push(` <description>${escapeXml(s2.description)}</description>`);
|
|
3939
|
+
lines.push(` <location>${escapeXml(s2.filePath)}</location>`);
|
|
3940
|
+
lines.push(" </skill>");
|
|
3941
|
+
}
|
|
3942
|
+
lines.push("</available_skills>");
|
|
3943
|
+
return lines.join("\n");
|
|
3944
|
+
}
|
|
3945
|
+
function composeSystemPrompt(input) {
|
|
3946
|
+
const home = input.home ?? homedir8();
|
|
3947
|
+
const parts = [];
|
|
3948
|
+
const soul = readSoul(home);
|
|
3949
|
+
if (soul) parts.push(soul);
|
|
3950
|
+
const skills = composeSkills(home, input.enabledTools);
|
|
3951
|
+
const skillsBlock = formatSkillsBlock(skills);
|
|
3952
|
+
if (skillsBlock) parts.push(skillsBlock);
|
|
3953
|
+
const base = input.base?.trim();
|
|
3954
|
+
if (base) parts.push(base);
|
|
3955
|
+
return parts.join("\n\n");
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3802
3958
|
// src/throttle.ts
|
|
3803
3959
|
function createThrottle(fn, intervalMs) {
|
|
3804
3960
|
let timer;
|
|
@@ -3951,20 +4107,20 @@ var ThreadSession = class {
|
|
|
3951
4107
|
};
|
|
3952
4108
|
|
|
3953
4109
|
// src/bridge.ts
|
|
3954
|
-
var AGENT_CONFIG_PATH2 =
|
|
4110
|
+
var AGENT_CONFIG_PATH2 = join9(homedir9(), ".openape", "agent", "agent.json");
|
|
3955
4111
|
function resolveSystemPrompt(envFallback) {
|
|
3956
|
-
if (!
|
|
4112
|
+
if (!existsSync7(AGENT_CONFIG_PATH2)) return envFallback;
|
|
3957
4113
|
try {
|
|
3958
|
-
const parsed = JSON.parse(
|
|
4114
|
+
const parsed = JSON.parse(readFileSync8(AGENT_CONFIG_PATH2, "utf8"));
|
|
3959
4115
|
return typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : envFallback;
|
|
3960
4116
|
} catch {
|
|
3961
4117
|
return envFallback;
|
|
3962
4118
|
}
|
|
3963
4119
|
}
|
|
3964
4120
|
function resolveTools(envFallback) {
|
|
3965
|
-
if (
|
|
4121
|
+
if (existsSync7(AGENT_CONFIG_PATH2)) {
|
|
3966
4122
|
try {
|
|
3967
|
-
const parsed = JSON.parse(
|
|
4123
|
+
const parsed = JSON.parse(readFileSync8(AGENT_CONFIG_PATH2, "utf8"));
|
|
3968
4124
|
if (Array.isArray(parsed.tools)) {
|
|
3969
4125
|
return parsed.tools.filter((t2) => typeof t2 === "string");
|
|
3970
4126
|
}
|
|
@@ -3982,10 +4138,10 @@ var RECONNECT_BASE_MS = 1e3;
|
|
|
3982
4138
|
var RECONNECT_MAX_MS = 3e4;
|
|
3983
4139
|
var ALLOWLIST_POLL_INTERVAL_MS = 3e4;
|
|
3984
4140
|
function loadBridgeEnvFile() {
|
|
3985
|
-
const path =
|
|
3986
|
-
if (!
|
|
4141
|
+
const path = join9(homedir9(), "Library", "Application Support", "openape", "bridge", ".env");
|
|
4142
|
+
if (!existsSync7(path)) return;
|
|
3987
4143
|
try {
|
|
3988
|
-
const raw =
|
|
4144
|
+
const raw = readFileSync8(path, "utf8");
|
|
3989
4145
|
for (const line of raw.split(/\r?\n/)) {
|
|
3990
4146
|
const trimmed = line.trim();
|
|
3991
4147
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -4036,7 +4192,7 @@ function log(line) {
|
|
|
4036
4192
|
`);
|
|
4037
4193
|
}
|
|
4038
4194
|
function sleep(ms) {
|
|
4039
|
-
return new Promise((
|
|
4195
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4040
4196
|
}
|
|
4041
4197
|
function truncate(s2, n2) {
|
|
4042
4198
|
return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
|
|
@@ -4139,11 +4295,16 @@ var Bridge = class {
|
|
|
4139
4295
|
threadId,
|
|
4140
4296
|
chat: this.chat,
|
|
4141
4297
|
runtimeConfig: this.runtimeConfig(),
|
|
4142
|
-
systemPrompt: resolveSystemPrompt(this.cfg.systemPrompt),
|
|
4143
4298
|
// Tools resolve from agent.json (latest sync from troop) on
|
|
4144
4299
|
// every new thread, so owner edits in the troop UI take
|
|
4145
4300
|
// effect after the next sync without a bridge restart.
|
|
4301
|
+
// SOUL.md + skills are merged into the system prompt the same
|
|
4302
|
+
// way — picked up per-thread without restart.
|
|
4146
4303
|
tools: resolveTools(this.cfg.tools),
|
|
4304
|
+
systemPrompt: composeSystemPrompt({
|
|
4305
|
+
base: resolveSystemPrompt(this.cfg.systemPrompt),
|
|
4306
|
+
enabledTools: resolveTools(this.cfg.tools)
|
|
4307
|
+
}),
|
|
4147
4308
|
maxSteps: this.cfg.maxSteps,
|
|
4148
4309
|
log
|
|
4149
4310
|
});
|
|
@@ -4154,7 +4315,7 @@ var Bridge = class {
|
|
|
4154
4315
|
const bearer = await this.bearer();
|
|
4155
4316
|
const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/api/ws?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
|
|
4156
4317
|
const ws = new WebSocket(wsUrl);
|
|
4157
|
-
return new Promise((
|
|
4318
|
+
return new Promise((resolve3, reject) => {
|
|
4158
4319
|
let pingTimer;
|
|
4159
4320
|
let allowlistTimer;
|
|
4160
4321
|
ws.on("open", () => {
|
|
@@ -4187,7 +4348,7 @@ var Bridge = class {
|
|
|
4187
4348
|
ws.on("close", () => {
|
|
4188
4349
|
if (pingTimer) clearInterval(pingTimer);
|
|
4189
4350
|
if (allowlistTimer) clearInterval(allowlistTimer);
|
|
4190
|
-
|
|
4351
|
+
resolve3();
|
|
4191
4352
|
});
|
|
4192
4353
|
ws.on("error", (err) => {
|
|
4193
4354
|
if (pingTimer) clearInterval(pingTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/ape-agent",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "OpenApe agent runtime: per-agent process that connects to chat.openape.ai, runs the LLM loop with tools + cron tasks, and streams replies back to owners.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
|
+
"default-skills",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
16
17
|
"publishConfig": {
|
|
@@ -20,8 +21,8 @@
|
|
|
20
21
|
"jose": "^5.9.0",
|
|
21
22
|
"ofetch": "^1.4.1",
|
|
22
23
|
"ws": "^8.18.0",
|
|
23
|
-
"@openape/
|
|
24
|
-
"@openape/
|
|
24
|
+
"@openape/apes": "1.22.0",
|
|
25
|
+
"@openape/cli-auth": "0.4.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@antfu/eslint-config": "^7.6.1",
|