@openape/ape-agent 2.0.1 → 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 +261 -17
- package/package.json +3 -2
|
@@ -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
|
|
@@ -1422,12 +1422,94 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as read
|
|
|
1422
1422
|
import { homedir as homedir6 } from "os";
|
|
1423
1423
|
import { join as join6 } from "path";
|
|
1424
1424
|
|
|
1425
|
-
// ../../packages/apes/dist/chunk-
|
|
1425
|
+
// ../../packages/apes/dist/chunk-FRCNYDTR.js
|
|
1426
|
+
import { spawn } from "child_process";
|
|
1426
1427
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1427
1428
|
import { homedir as homedir3 } from "os";
|
|
1428
1429
|
import { dirname, normalize, resolve } from "path";
|
|
1429
1430
|
import { execFileSync } from "child_process";
|
|
1430
1431
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1432
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1433
|
+
var MAX_STDIO_BYTES = 64 * 1024;
|
|
1434
|
+
var BIN = "ape-shell";
|
|
1435
|
+
function capStdio(s2) {
|
|
1436
|
+
const buf = Buffer.from(s2, "utf8");
|
|
1437
|
+
if (buf.byteLength <= MAX_STDIO_BYTES) return s2;
|
|
1438
|
+
return `${buf.subarray(0, MAX_STDIO_BYTES).toString("utf8")}
|
|
1439
|
+
[truncated to ${MAX_STDIO_BYTES} bytes]`;
|
|
1440
|
+
}
|
|
1441
|
+
var bashTools = [
|
|
1442
|
+
{
|
|
1443
|
+
name: "bash",
|
|
1444
|
+
description: "Run a shell command on the agent host. Every invocation goes through the OpenApe DDISA grant cycle \u2014 auto-approved if the owner has a matching YOLO scope, otherwise the owner gets a push notification to approve. Runs as the agent's macOS user, so file/network access is limited to what that user can see. Returns stdout, stderr, and exit code. For repeated command patterns ask the owner to set up a YOLO scope so approvals don't pile up.",
|
|
1445
|
+
parameters: {
|
|
1446
|
+
type: "object",
|
|
1447
|
+
properties: {
|
|
1448
|
+
cmd: {
|
|
1449
|
+
type: "string",
|
|
1450
|
+
description: "Shell command to run, e.g. `ls -la ~/Documents`, `git status`, `curl -fsSL https://example.com`. The whole string is passed to `bash -c`; quote internally as needed."
|
|
1451
|
+
},
|
|
1452
|
+
timeout_ms: {
|
|
1453
|
+
type: "number",
|
|
1454
|
+
description: "Wall-clock cap for the whole approval-and-run cycle in milliseconds. Default 300000 (5 min). Approval waits count against this budget."
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
required: ["cmd"]
|
|
1458
|
+
},
|
|
1459
|
+
execute: async (args) => {
|
|
1460
|
+
const a2 = args;
|
|
1461
|
+
if (typeof a2.cmd !== "string" || a2.cmd.trim() === "") {
|
|
1462
|
+
throw new Error("cmd must be a non-empty string");
|
|
1463
|
+
}
|
|
1464
|
+
const timeout = typeof a2.timeout_ms === "number" && a2.timeout_ms > 0 ? a2.timeout_ms : DEFAULT_TIMEOUT_MS;
|
|
1465
|
+
return await new Promise((resolveResult) => {
|
|
1466
|
+
const child = spawn(BIN, ["-c", a2.cmd], {
|
|
1467
|
+
env: { ...process.env, APE_WAIT: "1" },
|
|
1468
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1469
|
+
});
|
|
1470
|
+
let stdout2 = "";
|
|
1471
|
+
let stderr = "";
|
|
1472
|
+
let timedOut = false;
|
|
1473
|
+
let spawnError = null;
|
|
1474
|
+
child.stdout.on("data", (chunk) => {
|
|
1475
|
+
stdout2 += chunk.toString("utf8");
|
|
1476
|
+
});
|
|
1477
|
+
child.stderr.on("data", (chunk) => {
|
|
1478
|
+
stderr += chunk.toString("utf8");
|
|
1479
|
+
});
|
|
1480
|
+
child.on("error", (err) => {
|
|
1481
|
+
spawnError = err;
|
|
1482
|
+
});
|
|
1483
|
+
const timer = setTimeout(() => {
|
|
1484
|
+
timedOut = true;
|
|
1485
|
+
child.kill("SIGTERM");
|
|
1486
|
+
setTimeout(() => {
|
|
1487
|
+
try {
|
|
1488
|
+
child.kill("SIGKILL");
|
|
1489
|
+
} catch {
|
|
1490
|
+
}
|
|
1491
|
+
}, 5e3);
|
|
1492
|
+
}, timeout);
|
|
1493
|
+
child.on("close", (code) => {
|
|
1494
|
+
clearTimeout(timer);
|
|
1495
|
+
if (spawnError) {
|
|
1496
|
+
resolveResult({
|
|
1497
|
+
error: spawnError.message,
|
|
1498
|
+
hint: `Could not exec '${BIN}'. The agent host needs @openape/apes installed globally so ape-shell is on PATH.`
|
|
1499
|
+
});
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
resolveResult({
|
|
1503
|
+
stdout: capStdio(stdout2),
|
|
1504
|
+
stderr: capStdio(stderr),
|
|
1505
|
+
exit_code: code ?? -1,
|
|
1506
|
+
...timedOut ? { timed_out: true } : {}
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
];
|
|
1431
1513
|
var MAX_BYTES = 1024 * 1024;
|
|
1432
1514
|
function jailPath(input) {
|
|
1433
1515
|
if (typeof input !== "string" || input === "") {
|
|
@@ -1718,7 +1800,8 @@ var ALL_TOOLS = [
|
|
|
1718
1800
|
...httpTools,
|
|
1719
1801
|
...fileTools,
|
|
1720
1802
|
...tasksTools,
|
|
1721
|
-
...mailTools
|
|
1803
|
+
...mailTools,
|
|
1804
|
+
...bashTools
|
|
1722
1805
|
];
|
|
1723
1806
|
var TOOLS = Object.fromEntries(
|
|
1724
1807
|
ALL_TOOLS.map((t2) => [t2.name, t2])
|
|
@@ -3716,6 +3799,162 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
|
|
|
3716
3799
|
return allowlist.has(peer);
|
|
3717
3800
|
}
|
|
3718
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
|
+
|
|
3719
3958
|
// src/throttle.ts
|
|
3720
3959
|
function createThrottle(fn, intervalMs) {
|
|
3721
3960
|
let timer;
|
|
@@ -3868,20 +4107,20 @@ var ThreadSession = class {
|
|
|
3868
4107
|
};
|
|
3869
4108
|
|
|
3870
4109
|
// src/bridge.ts
|
|
3871
|
-
var AGENT_CONFIG_PATH2 =
|
|
4110
|
+
var AGENT_CONFIG_PATH2 = join9(homedir9(), ".openape", "agent", "agent.json");
|
|
3872
4111
|
function resolveSystemPrompt(envFallback) {
|
|
3873
|
-
if (!
|
|
4112
|
+
if (!existsSync7(AGENT_CONFIG_PATH2)) return envFallback;
|
|
3874
4113
|
try {
|
|
3875
|
-
const parsed = JSON.parse(
|
|
4114
|
+
const parsed = JSON.parse(readFileSync8(AGENT_CONFIG_PATH2, "utf8"));
|
|
3876
4115
|
return typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : envFallback;
|
|
3877
4116
|
} catch {
|
|
3878
4117
|
return envFallback;
|
|
3879
4118
|
}
|
|
3880
4119
|
}
|
|
3881
4120
|
function resolveTools(envFallback) {
|
|
3882
|
-
if (
|
|
4121
|
+
if (existsSync7(AGENT_CONFIG_PATH2)) {
|
|
3883
4122
|
try {
|
|
3884
|
-
const parsed = JSON.parse(
|
|
4123
|
+
const parsed = JSON.parse(readFileSync8(AGENT_CONFIG_PATH2, "utf8"));
|
|
3885
4124
|
if (Array.isArray(parsed.tools)) {
|
|
3886
4125
|
return parsed.tools.filter((t2) => typeof t2 === "string");
|
|
3887
4126
|
}
|
|
@@ -3899,10 +4138,10 @@ var RECONNECT_BASE_MS = 1e3;
|
|
|
3899
4138
|
var RECONNECT_MAX_MS = 3e4;
|
|
3900
4139
|
var ALLOWLIST_POLL_INTERVAL_MS = 3e4;
|
|
3901
4140
|
function loadBridgeEnvFile() {
|
|
3902
|
-
const path =
|
|
3903
|
-
if (!
|
|
4141
|
+
const path = join9(homedir9(), "Library", "Application Support", "openape", "bridge", ".env");
|
|
4142
|
+
if (!existsSync7(path)) return;
|
|
3904
4143
|
try {
|
|
3905
|
-
const raw =
|
|
4144
|
+
const raw = readFileSync8(path, "utf8");
|
|
3906
4145
|
for (const line of raw.split(/\r?\n/)) {
|
|
3907
4146
|
const trimmed = line.trim();
|
|
3908
4147
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -3953,7 +4192,7 @@ function log(line) {
|
|
|
3953
4192
|
`);
|
|
3954
4193
|
}
|
|
3955
4194
|
function sleep(ms) {
|
|
3956
|
-
return new Promise((
|
|
4195
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3957
4196
|
}
|
|
3958
4197
|
function truncate(s2, n2) {
|
|
3959
4198
|
return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
|
|
@@ -4056,11 +4295,16 @@ var Bridge = class {
|
|
|
4056
4295
|
threadId,
|
|
4057
4296
|
chat: this.chat,
|
|
4058
4297
|
runtimeConfig: this.runtimeConfig(),
|
|
4059
|
-
systemPrompt: resolveSystemPrompt(this.cfg.systemPrompt),
|
|
4060
4298
|
// Tools resolve from agent.json (latest sync from troop) on
|
|
4061
4299
|
// every new thread, so owner edits in the troop UI take
|
|
4062
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.
|
|
4063
4303
|
tools: resolveTools(this.cfg.tools),
|
|
4304
|
+
systemPrompt: composeSystemPrompt({
|
|
4305
|
+
base: resolveSystemPrompt(this.cfg.systemPrompt),
|
|
4306
|
+
enabledTools: resolveTools(this.cfg.tools)
|
|
4307
|
+
}),
|
|
4064
4308
|
maxSteps: this.cfg.maxSteps,
|
|
4065
4309
|
log
|
|
4066
4310
|
});
|
|
@@ -4071,7 +4315,7 @@ var Bridge = class {
|
|
|
4071
4315
|
const bearer = await this.bearer();
|
|
4072
4316
|
const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/api/ws?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
|
|
4073
4317
|
const ws = new WebSocket(wsUrl);
|
|
4074
|
-
return new Promise((
|
|
4318
|
+
return new Promise((resolve3, reject) => {
|
|
4075
4319
|
let pingTimer;
|
|
4076
4320
|
let allowlistTimer;
|
|
4077
4321
|
ws.on("open", () => {
|
|
@@ -4104,7 +4348,7 @@ var Bridge = class {
|
|
|
4104
4348
|
ws.on("close", () => {
|
|
4105
4349
|
if (pingTimer) clearInterval(pingTimer);
|
|
4106
4350
|
if (allowlistTimer) clearInterval(allowlistTimer);
|
|
4107
|
-
|
|
4351
|
+
resolve3();
|
|
4108
4352
|
});
|
|
4109
4353
|
ws.on("error", (err) => {
|
|
4110
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,7 +21,7 @@
|
|
|
20
21
|
"jose": "^5.9.0",
|
|
21
22
|
"ofetch": "^1.4.1",
|
|
22
23
|
"ws": "^8.18.0",
|
|
23
|
-
"@openape/apes": "1.
|
|
24
|
+
"@openape/apes": "1.22.0",
|
|
24
25
|
"@openape/cli-auth": "0.4.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|