@openape/ape-agent 2.0.2 → 2.2.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.
@@ -0,0 +1,80 @@
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
+ metadata:
5
+ openape:
6
+ requires_tools: [bash]
7
+ ---
8
+
9
+ # Shell access via ape-shell
10
+
11
+ ## What this is
12
+
13
+ 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:
14
+
15
+ - Auto-approved if a YOLO scope matches (owner has pre-approved this command pattern)
16
+ - Otherwise the owner gets a push notification on their phone with the exact command to approve or deny
17
+ - Approval takes ~3–15s typically; budget is 5min before the call times out
18
+
19
+ You run as the agent's hidden macOS user, so the filesystem and network you see are what *that* user sees — already jailed.
20
+
21
+ ## When to prefer bash over a curated tool
22
+
23
+ Curated tools (time.now, http.get, file.read, tasks.list, mail.list) are **always preferred** when they apply:
24
+
25
+ - Faster (no grant cycle)
26
+ - Structured JSON returns instead of stdout parsing
27
+ - Lower risk score (visible to the owner in troop UI)
28
+
29
+ Reach for `bash` when:
30
+
31
+ - The curated tool doesn't cover the case (e.g. `git status`, `iurio cases search`, `o365-cli mail trash <id>`)
32
+ - You need to chain commands with pipes / loops / redirects
33
+ - You need an auth header the deny-list strips (e.g. `curl -H 'Authorization: …'`)
34
+
35
+ ## Patterns
36
+
37
+ Read system info:
38
+
39
+ ```
40
+ bash({ "cmd": "uname -a && uptime" })
41
+ ```
42
+
43
+ Run a CLI:
44
+
45
+ ```
46
+ bash({ "cmd": "ape-tasks list --status open,doing --json" })
47
+ bash({ "cmd": "iurio cases search 'foo'" })
48
+ ```
49
+
50
+ Quote-heavy commands — wrap in single-quotes outside, escape inside:
51
+
52
+ ```
53
+ bash({ "cmd": "find ~/Downloads -name '*.pdf' -mtime -7 -exec ls -lh {} +" })
54
+ ```
55
+
56
+ Long-running command — use the `timeout_ms` param (default 5 min):
57
+
58
+ ```
59
+ bash({ "cmd": "pnpm test", "timeout_ms": 180000 })
60
+ ```
61
+
62
+ ## Anti-patterns
63
+
64
+ - **Don't** use `bash date` for the time — call `time.now` (in-process, no grant).
65
+ - **Don't** use `bash cat ~/notes.md` for a $HOME read — call `file.read` (no grant).
66
+ - **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.
67
+ - **Don't** chain destructive commands in one call (`rm -rf … && …`). One destructive command per approval so the owner can decide each.
68
+
69
+ ## Response shape
70
+
71
+ ```json
72
+ {
73
+ "stdout": "...",
74
+ "stderr": "...",
75
+ "exit_code": 0,
76
+ "timed_out": false
77
+ }
78
+ ```
79
+
80
+ 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,39 @@
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
+ metadata:
5
+ openape:
6
+ requires_tools: [file.read]
7
+ ---
8
+
9
+ # Files in $HOME
10
+
11
+ ## Jail
12
+
13
+ Both tools are restricted to the agent's `$HOME` directory:
14
+
15
+ - `path` is resolved relative to `$HOME` (`~/` prefix accepted, or plain `notes.md`)
16
+ - `..` segments that would escape `$HOME` are rejected
17
+ - 1 MB cap per read/write — anything bigger gets truncated (read) or rejected (write)
18
+
19
+ 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.
20
+
21
+ ## Patterns
22
+
23
+ Read a JSON config:
24
+
25
+ ```
26
+ file.read({ "path": "~/.openape/agent/agent.json" })
27
+ ```
28
+
29
+ Append a note (read-modify-write — there's no `file.append`):
30
+
31
+ ```
32
+ const r = file.read({ "path": "notes.md" })
33
+ file.write({ "path": "notes.md", "content": r.content + "\n- new line\n" })
34
+ ```
35
+
36
+ ## Conventions
37
+
38
+ - Paths in user prompts ("save this to notes.md") → relative to `$HOME`. Don't prefix with `/Users/...`.
39
+ - Binary files don't round-trip via these tools (UTF-8 only). Use `bash` for non-text I/O.
@@ -0,0 +1,49 @@
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
+ metadata:
5
+ openape:
6
+ requires_tools: [http.get]
7
+ ---
8
+
9
+ # HTTP fetch
10
+
11
+ ## When to use
12
+
13
+ - `http.get` — read content from any HTTPS URL (webpages, REST endpoints, JSON APIs)
14
+ - `http.post` — POST JSON to an HTTPS URL
15
+
16
+ Both are bounded:
17
+
18
+ - Response capped at 1 MB (anything longer is truncated)
19
+ - Headers go through a deny-list (no `Authorization` for arbitrary hosts, no `Cookie`)
20
+ - HTTP-only URLs are rejected — HTTPS required
21
+
22
+ For commands that go beyond simple HTTP (auth, mTLS, complex curl flags, multipart upload), use the `bash` tool with `curl` instead.
23
+
24
+ ## Patterns
25
+
26
+ Fetch JSON:
27
+
28
+ ```
29
+ http.get({
30
+ "url": "https://api.example.com/users/42",
31
+ "headers": { "Accept": "application/json" }
32
+ })
33
+ ```
34
+
35
+ POST JSON:
36
+
37
+ ```
38
+ http.post({
39
+ "url": "https://api.example.com/notes",
40
+ "body": { "title": "from agent", "body": "..." },
41
+ "headers": { "Content-Type": "application/json" }
42
+ })
43
+ ```
44
+
45
+ ## Anti-patterns
46
+
47
+ - 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.
48
+ - Don't paginate by manually incrementing offsets without checking the API's actual contract — read response shape first.
49
+ - For auth that needs `Authorization: Bearer …`, the deny-list strips it. Use `bash` with `curl` if you need it.
@@ -0,0 +1,46 @@
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
+ metadata:
5
+ openape:
6
+ requires_tools: [mail.list]
7
+ openclaw:
8
+ # If the o365-cli binary isn't on PATH the underlying tool fails
9
+ # at runtime — skip the skill from the prompt so the LLM doesn't
10
+ # try to use it on a host where it can't run.
11
+ requires:
12
+ bins: [o365-cli]
13
+ ---
14
+
15
+ # Inbox (o365-cli)
16
+
17
+ ## What this is
18
+
19
+ 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.
20
+
21
+ ## When to use
22
+
23
+ - `mail.list` — recent inbox messages. Optional `unread_only: true`. Default limit 20.
24
+ - `mail.search` — keyword/from/subject search. Pass a query string.
25
+
26
+ 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.
27
+
28
+ ## Patterns
29
+
30
+ Latest 10 unread:
31
+
32
+ ```
33
+ mail.list({ "limit": 10, "unread_only": true })
34
+ ```
35
+
36
+ Search by sender:
37
+
38
+ ```
39
+ mail.search({ "q": "from:smaurer@deloitte.at", "limit": 20 })
40
+ ```
41
+
42
+ ## Conventions
43
+
44
+ - Don't draft replies inside the agent if the user just asked "what's in my inbox" — listing is read-only, replies are explicit.
45
+ - When the user wants to triage ("welche kann ich archivieren?"), list first, then offer specific candidates with message IDs — do NOT auto-move anything.
46
+ - 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,55 @@
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
+ metadata:
5
+ openape:
6
+ requires_tools: [tasks.list]
7
+ openclaw:
8
+ # The bash escape-hatch path uses ape-tasks; surface it as a soft
9
+ # dependency so on a host without ape-tasks the LLM is told this
10
+ # skill doesn't apply.
11
+ requires:
12
+ bins: [ape-tasks]
13
+ ---
14
+
15
+ # Personal tasks (ape-tasks)
16
+
17
+ ## What this is
18
+
19
+ 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.
20
+
21
+ ## When to use
22
+
23
+ Use `tasks.create` whenever the user asks for any of:
24
+
25
+ - "Reminder me to X tomorrow"
26
+ - "Wiedervorlage in 2 Tagen"
27
+ - "Add to my todo: …"
28
+ - "Schedule X for next week"
29
+
30
+ Use `tasks.list` when they ask "what's on my list", "any open tasks", "remind me what's due".
31
+
32
+ ## Create — parameters
33
+
34
+ ```
35
+ tasks.create({
36
+ "title": "<short title>",
37
+ "notes": "<optional longer body>",
38
+ "priority": "low" | "med" | "high", // default med
39
+ "due_at": "<ISO 8601 or relative: +2h | +1d | tomorrow 9am>"
40
+ })
41
+ ```
42
+
43
+ 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:
44
+
45
+ ```
46
+ bash({ "cmd": "ape-tasks new --title '...' --remind-at '+2d' --context-summary '...' --context-url '...'" })
47
+ ```
48
+
49
+ …because the CLI has more knobs than the tool exposes (assignee, remind-at, context-url).
50
+
51
+ ## Conventions
52
+
53
+ - Always convert relative dates from the user prompt to an absolute date in your response: "in 2 Tagen" → "am 13. Mai 2026".
54
+ - Don't create duplicate tasks — `tasks.list` first if the user might have one already.
55
+ - 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,35 @@
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
+ metadata:
5
+ openape:
6
+ requires_tools: [time.now]
7
+ ---
8
+
9
+ # Time and date
10
+
11
+ ## When to use
12
+
13
+ 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.
14
+
15
+ ## How to use
16
+
17
+ ```
18
+ time.now({})
19
+ ```
20
+
21
+ No arguments. Response shape:
22
+
23
+ ```json
24
+ {
25
+ "iso": "2026-05-11T07:14:44Z",
26
+ "epoch_seconds": 1778383484,
27
+ "timezone_offset_minutes": 120
28
+ }
29
+ ```
30
+
31
+ ## Conventions
32
+
33
+ - 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)."
34
+ - For relative-time questions ("vor 3 Tagen"), compute from `epoch_seconds` — don't rely on the LLM's internal clock guess.
35
+ - 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 existsSync6, readFileSync as readFileSync7 } from "fs";
1044
- import { homedir as homedir8 } from "os";
1045
- import { join as join8 } from "path";
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,177 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
3799
3799
  return allowlist.has(peer);
3800
3800
  }
3801
3801
 
3802
+ // src/skills.ts
3803
+ import { execFileSync as execFileSync3 } from "child_process";
3804
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "fs";
3805
+ import { homedir as homedir8 } from "os";
3806
+ import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
3807
+ import { fileURLToPath } from "url";
3808
+ import { parse as parseYaml } from "yaml";
3809
+ var SKILLS_SUBDIR = [".openape", "agent", "skills"];
3810
+ var SOUL_PATH_PARTS = [".openape", "agent", "SOUL.md"];
3811
+ function soulPath(home = homedir8()) {
3812
+ return join8(home, ...SOUL_PATH_PARTS);
3813
+ }
3814
+ function skillsDir(home = homedir8()) {
3815
+ return join8(home, ...SKILLS_SUBDIR);
3816
+ }
3817
+ function readSoul(home = homedir8()) {
3818
+ const path = soulPath(home);
3819
+ if (!existsSync6(path)) return null;
3820
+ try {
3821
+ const body = readFileSync7(path, "utf8").trim();
3822
+ return body.length > 0 ? body : null;
3823
+ } catch {
3824
+ return null;
3825
+ }
3826
+ }
3827
+ function parseFrontmatter(content) {
3828
+ const trimmed = content.trimStart();
3829
+ if (!trimmed.startsWith("---")) return null;
3830
+ const closeIdx = trimmed.indexOf("\n---", 3);
3831
+ if (closeIdx < 0) return null;
3832
+ const fmBlock = trimmed.slice(3, closeIdx).trim();
3833
+ let parsed;
3834
+ try {
3835
+ parsed = parseYaml(fmBlock);
3836
+ } catch {
3837
+ return null;
3838
+ }
3839
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3840
+ const fields = parsed;
3841
+ const name = typeof fields.name === "string" ? fields.name.trim() : "";
3842
+ const description = typeof fields.description === "string" ? fields.description.trim() : "";
3843
+ if (!name || !description) return null;
3844
+ function asStringArray(v2) {
3845
+ if (!Array.isArray(v2)) return void 0;
3846
+ const out = v2.map((x2) => typeof x2 === "string" ? x2.trim() : "").filter((s2) => s2.length > 0);
3847
+ return out.length > 0 ? out : void 0;
3848
+ }
3849
+ const meta = fields.metadata && typeof fields.metadata === "object" && !Array.isArray(fields.metadata) ? fields.metadata : {};
3850
+ const openapeMeta = meta.openape && typeof meta.openape === "object" && !Array.isArray(meta.openape) ? meta.openape : {};
3851
+ const openclawMeta = meta.openclaw && typeof meta.openclaw === "object" && !Array.isArray(meta.openclaw) ? meta.openclaw : {};
3852
+ const requiresTools = asStringArray(openapeMeta.requires_tools) ?? asStringArray(fields.requires_tools);
3853
+ function readRequiresBins(scope) {
3854
+ const requires = scope.requires;
3855
+ if (requires && typeof requires === "object" && !Array.isArray(requires)) {
3856
+ return asStringArray(requires.bins);
3857
+ }
3858
+ return void 0;
3859
+ }
3860
+ const requiresBins = readRequiresBins(openclawMeta) ?? readRequiresBins(openapeMeta) ?? asStringArray(fields.requires_bins);
3861
+ return { name, description, requiresTools, requiresBins };
3862
+ }
3863
+ var binCheckCache = /* @__PURE__ */ new Map();
3864
+ function hasBinaryOnPath(bin) {
3865
+ const cached = binCheckCache.get(bin);
3866
+ if (cached !== void 0) return cached;
3867
+ let found = false;
3868
+ try {
3869
+ execFileSync3("/usr/bin/which", [bin], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
3870
+ found = true;
3871
+ } catch {
3872
+ }
3873
+ binCheckCache.set(bin, found);
3874
+ return found;
3875
+ }
3876
+ function scanSkillsDir(dir) {
3877
+ if (!existsSync6(dir)) return [];
3878
+ let entries;
3879
+ try {
3880
+ entries = readdirSync4(dir);
3881
+ } catch {
3882
+ return [];
3883
+ }
3884
+ const out = [];
3885
+ for (const entry of entries) {
3886
+ const skillPath = join8(dir, entry, "SKILL.md");
3887
+ if (!existsSync6(skillPath)) continue;
3888
+ let st;
3889
+ try {
3890
+ st = statSync(skillPath);
3891
+ } catch {
3892
+ continue;
3893
+ }
3894
+ if (!st.isFile()) continue;
3895
+ let body;
3896
+ try {
3897
+ body = readFileSync7(skillPath, "utf8");
3898
+ } catch {
3899
+ continue;
3900
+ }
3901
+ const fm = parseFrontmatter(body);
3902
+ if (!fm) continue;
3903
+ out.push({
3904
+ name: fm.name,
3905
+ description: fm.description,
3906
+ filePath: skillPath,
3907
+ requiresTools: fm.requiresTools,
3908
+ requiresBins: fm.requiresBins
3909
+ });
3910
+ }
3911
+ return out;
3912
+ }
3913
+ function defaultSkillsDir() {
3914
+ const here = dirname2(fileURLToPath(import.meta.url));
3915
+ return resolve2(here, "..", "default-skills");
3916
+ }
3917
+ function composeSkills(home, enabledTools) {
3918
+ const enabled = new Set(enabledTools);
3919
+ const byName = /* @__PURE__ */ new Map();
3920
+ for (const s2 of scanSkillsDir(defaultSkillsDir())) byName.set(s2.name, s2);
3921
+ for (const s2 of scanSkillsDir(skillsDir(home))) byName.set(s2.name, s2);
3922
+ const out = [];
3923
+ for (const s2 of byName.values()) {
3924
+ if (s2.requiresTools && s2.requiresTools.length > 0) {
3925
+ const allPresent = s2.requiresTools.every((t2) => enabled.has(t2));
3926
+ if (!allPresent) continue;
3927
+ }
3928
+ if (s2.requiresBins && s2.requiresBins.length > 0) {
3929
+ const allBinsPresent = s2.requiresBins.every((b2) => hasBinaryOnPath(b2));
3930
+ if (!allBinsPresent) continue;
3931
+ }
3932
+ out.push(s2);
3933
+ }
3934
+ out.sort((a2, b2) => a2.name.localeCompare(b2.name));
3935
+ return out;
3936
+ }
3937
+ function escapeXml(s2) {
3938
+ return s2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3939
+ }
3940
+ function formatSkillsBlock(skills) {
3941
+ if (skills.length === 0) return "";
3942
+ const lines = [
3943
+ "",
3944
+ "The following skills provide specialized instructions for specific tasks.",
3945
+ "Use the file.read tool to load a skill's file when the user's task matches its description.",
3946
+ "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.",
3947
+ "",
3948
+ "<available_skills>"
3949
+ ];
3950
+ for (const s2 of skills) {
3951
+ lines.push(" <skill>");
3952
+ lines.push(` <name>${escapeXml(s2.name)}</name>`);
3953
+ lines.push(` <description>${escapeXml(s2.description)}</description>`);
3954
+ lines.push(` <location>${escapeXml(s2.filePath)}</location>`);
3955
+ lines.push(" </skill>");
3956
+ }
3957
+ lines.push("</available_skills>");
3958
+ return lines.join("\n");
3959
+ }
3960
+ function composeSystemPrompt(input) {
3961
+ const home = input.home ?? homedir8();
3962
+ const parts = [];
3963
+ const soul = readSoul(home);
3964
+ if (soul) parts.push(soul);
3965
+ const skills = composeSkills(home, input.enabledTools);
3966
+ const skillsBlock = formatSkillsBlock(skills);
3967
+ if (skillsBlock) parts.push(skillsBlock);
3968
+ const base = input.base?.trim();
3969
+ if (base) parts.push(base);
3970
+ return parts.join("\n\n");
3971
+ }
3972
+
3802
3973
  // src/throttle.ts
3803
3974
  function createThrottle(fn, intervalMs) {
3804
3975
  let timer;
@@ -3951,20 +4122,20 @@ var ThreadSession = class {
3951
4122
  };
3952
4123
 
3953
4124
  // src/bridge.ts
3954
- var AGENT_CONFIG_PATH2 = join8(homedir8(), ".openape", "agent", "agent.json");
4125
+ var AGENT_CONFIG_PATH2 = join9(homedir9(), ".openape", "agent", "agent.json");
3955
4126
  function resolveSystemPrompt(envFallback) {
3956
- if (!existsSync6(AGENT_CONFIG_PATH2)) return envFallback;
4127
+ if (!existsSync7(AGENT_CONFIG_PATH2)) return envFallback;
3957
4128
  try {
3958
- const parsed = JSON.parse(readFileSync7(AGENT_CONFIG_PATH2, "utf8"));
4129
+ const parsed = JSON.parse(readFileSync8(AGENT_CONFIG_PATH2, "utf8"));
3959
4130
  return typeof parsed.systemPrompt === "string" ? parsed.systemPrompt : envFallback;
3960
4131
  } catch {
3961
4132
  return envFallback;
3962
4133
  }
3963
4134
  }
3964
4135
  function resolveTools(envFallback) {
3965
- if (existsSync6(AGENT_CONFIG_PATH2)) {
4136
+ if (existsSync7(AGENT_CONFIG_PATH2)) {
3966
4137
  try {
3967
- const parsed = JSON.parse(readFileSync7(AGENT_CONFIG_PATH2, "utf8"));
4138
+ const parsed = JSON.parse(readFileSync8(AGENT_CONFIG_PATH2, "utf8"));
3968
4139
  if (Array.isArray(parsed.tools)) {
3969
4140
  return parsed.tools.filter((t2) => typeof t2 === "string");
3970
4141
  }
@@ -3982,10 +4153,10 @@ var RECONNECT_BASE_MS = 1e3;
3982
4153
  var RECONNECT_MAX_MS = 3e4;
3983
4154
  var ALLOWLIST_POLL_INTERVAL_MS = 3e4;
3984
4155
  function loadBridgeEnvFile() {
3985
- const path = join8(homedir8(), "Library", "Application Support", "openape", "bridge", ".env");
3986
- if (!existsSync6(path)) return;
4156
+ const path = join9(homedir9(), "Library", "Application Support", "openape", "bridge", ".env");
4157
+ if (!existsSync7(path)) return;
3987
4158
  try {
3988
- const raw = readFileSync7(path, "utf8");
4159
+ const raw = readFileSync8(path, "utf8");
3989
4160
  for (const line of raw.split(/\r?\n/)) {
3990
4161
  const trimmed = line.trim();
3991
4162
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -4036,7 +4207,7 @@ function log(line) {
4036
4207
  `);
4037
4208
  }
4038
4209
  function sleep(ms) {
4039
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4210
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4040
4211
  }
4041
4212
  function truncate(s2, n2) {
4042
4213
  return s2.length <= n2 ? s2 : `${s2.slice(0, n2 - 1)}\u2026`;
@@ -4139,11 +4310,16 @@ var Bridge = class {
4139
4310
  threadId,
4140
4311
  chat: this.chat,
4141
4312
  runtimeConfig: this.runtimeConfig(),
4142
- systemPrompt: resolveSystemPrompt(this.cfg.systemPrompt),
4143
4313
  // Tools resolve from agent.json (latest sync from troop) on
4144
4314
  // every new thread, so owner edits in the troop UI take
4145
4315
  // effect after the next sync without a bridge restart.
4316
+ // SOUL.md + skills are merged into the system prompt the same
4317
+ // way — picked up per-thread without restart.
4146
4318
  tools: resolveTools(this.cfg.tools),
4319
+ systemPrompt: composeSystemPrompt({
4320
+ base: resolveSystemPrompt(this.cfg.systemPrompt),
4321
+ enabledTools: resolveTools(this.cfg.tools)
4322
+ }),
4147
4323
  maxSteps: this.cfg.maxSteps,
4148
4324
  log
4149
4325
  });
@@ -4154,7 +4330,7 @@ var Bridge = class {
4154
4330
  const bearer = await this.bearer();
4155
4331
  const wsUrl = `${this.cfg.endpoint.replace(/^http/, "ws")}/api/ws?token=${encodeURIComponent(bearer.replace(/^Bearer\s+/i, ""))}`;
4156
4332
  const ws = new WebSocket(wsUrl);
4157
- return new Promise((resolve2, reject) => {
4333
+ return new Promise((resolve3, reject) => {
4158
4334
  let pingTimer;
4159
4335
  let allowlistTimer;
4160
4336
  ws.on("open", () => {
@@ -4187,7 +4363,7 @@ var Bridge = class {
4187
4363
  ws.on("close", () => {
4188
4364
  if (pingTimer) clearInterval(pingTimer);
4189
4365
  if (allowlistTimer) clearInterval(allowlistTimer);
4190
- resolve2();
4366
+ resolve3();
4191
4367
  });
4192
4368
  ws.on("error", (err) => {
4193
4369
  if (pingTimer) clearInterval(pingTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/ape-agent",
3
- "version": "2.0.2",
3
+ "version": "2.2.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,9 @@
20
21
  "jose": "^5.9.0",
21
22
  "ofetch": "^1.4.1",
22
23
  "ws": "^8.18.0",
23
- "@openape/cli-auth": "0.4.0",
24
- "@openape/apes": "1.21.0"
24
+ "yaml": "^2.8.0",
25
+ "@openape/apes": "1.22.0",
26
+ "@openape/cli-auth": "0.4.0"
25
27
  },
26
28
  "devDependencies": {
27
29
  "@antfu/eslint-config": "^7.6.1",