@openape/ape-agent 2.1.0 → 2.3.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 +3 -1
- package/default-skills/file/SKILL.md +3 -1
- package/default-skills/http/SKILL.md +3 -1
- package/default-skills/mail/SKILL.md +9 -1
- package/default-skills/tasks/SKILL.md +9 -1
- package/default-skills/time/SKILL.md +3 -1
- package/dist/bridge.mjs +116 -46
- package/package.json +2 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bash
|
|
3
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
|
-
|
|
4
|
+
metadata:
|
|
5
|
+
openape:
|
|
6
|
+
requires_tools: [bash]
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
# Shell access via ape-shell
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: file
|
|
3
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
|
-
|
|
4
|
+
metadata:
|
|
5
|
+
openape:
|
|
6
|
+
requires_tools: [file.read]
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
# Files in $HOME
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: http
|
|
3
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
|
-
|
|
4
|
+
metadata:
|
|
5
|
+
openape:
|
|
6
|
+
requires_tools: [http.get]
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
# HTTP fetch
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mail
|
|
3
3
|
description: When the user asks about their inbox — what's there, search for an email, recent unread — use mail.list / mail.search.
|
|
4
|
-
|
|
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]
|
|
5
13
|
---
|
|
6
14
|
|
|
7
15
|
# Inbox (o365-cli)
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tasks
|
|
3
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
|
-
|
|
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]
|
|
5
13
|
---
|
|
6
14
|
|
|
7
15
|
# Personal tasks (ape-tasks)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: time
|
|
3
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
|
-
|
|
4
|
+
metadata:
|
|
5
|
+
openape:
|
|
6
|
+
requires_tools: [time.now]
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
# Time and date
|
package/dist/bridge.mjs
CHANGED
|
@@ -1353,11 +1353,12 @@ var ChatApi = class {
|
|
|
1353
1353
|
this.bearer = bearer;
|
|
1354
1354
|
}
|
|
1355
1355
|
async postMessage(roomId, body, opts = {}) {
|
|
1356
|
-
const
|
|
1356
|
+
const bodyForServer = opts.streaming ? body : clamp(body, MAX_BODY);
|
|
1357
1357
|
const url = `${this.endpoint}/api/rooms/${encodeURIComponent(roomId)}/messages`;
|
|
1358
|
-
const payload = { body:
|
|
1358
|
+
const payload = { body: bodyForServer };
|
|
1359
1359
|
if (opts.replyTo) payload.reply_to = opts.replyTo;
|
|
1360
1360
|
if (opts.threadId) payload.thread_id = opts.threadId;
|
|
1361
|
+
if (opts.streaming) payload.streaming = true;
|
|
1361
1362
|
const result = await ofetch5(url, {
|
|
1362
1363
|
method: "POST",
|
|
1363
1364
|
headers: { Authorization: await this.bearer() },
|
|
@@ -1401,13 +1402,32 @@ var ChatApi = class {
|
|
|
1401
1402
|
body: { name: name.slice(0, 100) }
|
|
1402
1403
|
});
|
|
1403
1404
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1405
|
+
/**
|
|
1406
|
+
* Update an in-flight or completed message. The server differentiates
|
|
1407
|
+
* three modes via the message's current `streaming` state and the
|
|
1408
|
+
* `streaming` field in this call:
|
|
1409
|
+
*
|
|
1410
|
+
* - Stream tick: pass `body` only (current accumulated text).
|
|
1411
|
+
* Server keeps streaming=true and does NOT bump edited_at.
|
|
1412
|
+
* - Stream end: pass `body` + `streaming: false`. Server clears
|
|
1413
|
+
* the streaming flag and triggers the user-facing push.
|
|
1414
|
+
* - Tool-call status: pass `streamingStatus` only (no body).
|
|
1415
|
+
* Renders as "🔧 time.now" in the typing-subtitle.
|
|
1416
|
+
* - Tool-call cleared: pass `streamingStatus: null`.
|
|
1417
|
+
*/
|
|
1418
|
+
async patchMessage(messageId, opts = {}) {
|
|
1406
1419
|
const url = `${this.endpoint}/api/messages/${encodeURIComponent(messageId)}`;
|
|
1420
|
+
const payload = {};
|
|
1421
|
+
if (opts.body !== void 0) {
|
|
1422
|
+
payload.body = opts.streaming === false && opts.body.trim().length === 0 ? clamp(opts.body, MAX_BODY) : opts.body.length <= MAX_BODY ? opts.body : `${opts.body.slice(0, MAX_BODY - 1)}\u2026`;
|
|
1423
|
+
}
|
|
1424
|
+
if (opts.streaming !== void 0) payload.streaming = opts.streaming;
|
|
1425
|
+
if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
|
|
1426
|
+
if (Object.keys(payload).length === 0) return;
|
|
1407
1427
|
await ofetch5(url, {
|
|
1408
1428
|
method: "PATCH",
|
|
1409
1429
|
headers: { Authorization: await this.bearer() },
|
|
1410
|
-
body:
|
|
1430
|
+
body: payload
|
|
1411
1431
|
});
|
|
1412
1432
|
}
|
|
1413
1433
|
};
|
|
@@ -3800,10 +3820,12 @@ function shouldAutoAccept(peerEmail, identity, allowlist) {
|
|
|
3800
3820
|
}
|
|
3801
3821
|
|
|
3802
3822
|
// src/skills.ts
|
|
3823
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
3803
3824
|
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "fs";
|
|
3804
3825
|
import { homedir as homedir8 } from "os";
|
|
3805
3826
|
import { dirname as dirname2, join as join8, resolve as resolve2 } from "path";
|
|
3806
3827
|
import { fileURLToPath } from "url";
|
|
3828
|
+
import { parse as parseYaml } from "yaml";
|
|
3807
3829
|
var SKILLS_SUBDIR = [".openape", "agent", "skills"];
|
|
3808
3830
|
var SOUL_PATH_PARTS = [".openape", "agent", "SOUL.md"];
|
|
3809
3831
|
function soulPath(home = homedir8()) {
|
|
@@ -3828,40 +3850,48 @@ function parseFrontmatter(content) {
|
|
|
3828
3850
|
const closeIdx = trimmed.indexOf("\n---", 3);
|
|
3829
3851
|
if (closeIdx < 0) return null;
|
|
3830
3852
|
const fmBlock = trimmed.slice(3, closeIdx).trim();
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
if (inlineArray) {
|
|
3856
|
-
fields[key] = inlineArray[1].split(",").map((s2) => s2.trim().replace(/^["']|["']$/g, "")).filter(Boolean).join(",");
|
|
3857
|
-
continue;
|
|
3853
|
+
let parsed;
|
|
3854
|
+
try {
|
|
3855
|
+
parsed = parseYaml(fmBlock);
|
|
3856
|
+
} catch {
|
|
3857
|
+
return null;
|
|
3858
|
+
}
|
|
3859
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3860
|
+
const fields = parsed;
|
|
3861
|
+
const name = typeof fields.name === "string" ? fields.name.trim() : "";
|
|
3862
|
+
const description = typeof fields.description === "string" ? fields.description.trim() : "";
|
|
3863
|
+
if (!name || !description) return null;
|
|
3864
|
+
function asStringArray(v2) {
|
|
3865
|
+
if (!Array.isArray(v2)) return void 0;
|
|
3866
|
+
const out = v2.map((x2) => typeof x2 === "string" ? x2.trim() : "").filter((s2) => s2.length > 0);
|
|
3867
|
+
return out.length > 0 ? out : void 0;
|
|
3868
|
+
}
|
|
3869
|
+
const meta = fields.metadata && typeof fields.metadata === "object" && !Array.isArray(fields.metadata) ? fields.metadata : {};
|
|
3870
|
+
const openapeMeta = meta.openape && typeof meta.openape === "object" && !Array.isArray(meta.openape) ? meta.openape : {};
|
|
3871
|
+
const openclawMeta = meta.openclaw && typeof meta.openclaw === "object" && !Array.isArray(meta.openclaw) ? meta.openclaw : {};
|
|
3872
|
+
const requiresTools = asStringArray(openapeMeta.requires_tools) ?? asStringArray(fields.requires_tools);
|
|
3873
|
+
function readRequiresBins(scope) {
|
|
3874
|
+
const requires = scope.requires;
|
|
3875
|
+
if (requires && typeof requires === "object" && !Array.isArray(requires)) {
|
|
3876
|
+
return asStringArray(requires.bins);
|
|
3858
3877
|
}
|
|
3859
|
-
|
|
3878
|
+
return void 0;
|
|
3860
3879
|
}
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3880
|
+
const requiresBins = readRequiresBins(openclawMeta) ?? readRequiresBins(openapeMeta) ?? asStringArray(fields.requires_bins);
|
|
3881
|
+
return { name, description, requiresTools, requiresBins };
|
|
3882
|
+
}
|
|
3883
|
+
var binCheckCache = /* @__PURE__ */ new Map();
|
|
3884
|
+
function hasBinaryOnPath(bin) {
|
|
3885
|
+
const cached = binCheckCache.get(bin);
|
|
3886
|
+
if (cached !== void 0) return cached;
|
|
3887
|
+
let found = false;
|
|
3888
|
+
try {
|
|
3889
|
+
execFileSync3("/usr/bin/which", [bin], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
3890
|
+
found = true;
|
|
3891
|
+
} catch {
|
|
3892
|
+
}
|
|
3893
|
+
binCheckCache.set(bin, found);
|
|
3894
|
+
return found;
|
|
3865
3895
|
}
|
|
3866
3896
|
function scanSkillsDir(dir) {
|
|
3867
3897
|
if (!existsSync6(dir)) return [];
|
|
@@ -3894,7 +3924,8 @@ function scanSkillsDir(dir) {
|
|
|
3894
3924
|
name: fm.name,
|
|
3895
3925
|
description: fm.description,
|
|
3896
3926
|
filePath: skillPath,
|
|
3897
|
-
requiresTools: fm.requiresTools
|
|
3927
|
+
requiresTools: fm.requiresTools,
|
|
3928
|
+
requiresBins: fm.requiresBins
|
|
3898
3929
|
});
|
|
3899
3930
|
}
|
|
3900
3931
|
return out;
|
|
@@ -3914,6 +3945,10 @@ function composeSkills(home, enabledTools) {
|
|
|
3914
3945
|
const allPresent = s2.requiresTools.every((t2) => enabled.has(t2));
|
|
3915
3946
|
if (!allPresent) continue;
|
|
3916
3947
|
}
|
|
3948
|
+
if (s2.requiresBins && s2.requiresBins.length > 0) {
|
|
3949
|
+
const allBinsPresent = s2.requiresBins.every((b2) => hasBinaryOnPath(b2));
|
|
3950
|
+
if (!allBinsPresent) continue;
|
|
3951
|
+
}
|
|
3917
3952
|
out.push(s2);
|
|
3918
3953
|
}
|
|
3919
3954
|
out.sort((a2, b2) => a2.name.localeCompare(b2.name));
|
|
@@ -4025,9 +4060,10 @@ var ThreadSession = class {
|
|
|
4025
4060
|
void this.startTurn(body, replyToMessageId);
|
|
4026
4061
|
}
|
|
4027
4062
|
async startTurn(body, replyToMessageId) {
|
|
4028
|
-
const placeholder = await this.deps.chat.postMessage(this.deps.roomId, "
|
|
4063
|
+
const placeholder = await this.deps.chat.postMessage(this.deps.roomId, "", {
|
|
4029
4064
|
replyTo: replyToMessageId,
|
|
4030
|
-
threadId: this.deps.threadId
|
|
4065
|
+
threadId: this.deps.threadId,
|
|
4066
|
+
streaming: true
|
|
4031
4067
|
});
|
|
4032
4068
|
const turn = {
|
|
4033
4069
|
placeholderId: placeholder.id,
|
|
@@ -4035,15 +4071,23 @@ var ThreadSession = class {
|
|
|
4035
4071
|
replyToMessageId,
|
|
4036
4072
|
throttle: createThrottle(async () => {
|
|
4037
4073
|
if (!this.active || this.active.placeholderId !== placeholder.id) return;
|
|
4038
|
-
const text = this.active.accumulated
|
|
4074
|
+
const text = this.active.accumulated;
|
|
4075
|
+
if (text.length === 0) return;
|
|
4039
4076
|
try {
|
|
4040
|
-
await this.deps.chat.patchMessage(placeholder.id, text);
|
|
4077
|
+
await this.deps.chat.patchMessage(placeholder.id, { body: text });
|
|
4041
4078
|
} catch (err) {
|
|
4042
4079
|
this.deps.log(`patch failed (room=${this.deps.roomId} thread=${this.deps.threadId}): ${err instanceof Error ? err.message : String(err)}`);
|
|
4043
4080
|
}
|
|
4044
4081
|
}, PATCH_INTERVAL_MS)
|
|
4045
4082
|
};
|
|
4046
4083
|
this.active = turn;
|
|
4084
|
+
const setStatus = async (status) => {
|
|
4085
|
+
try {
|
|
4086
|
+
await this.deps.chat.patchMessage(placeholder.id, { streamingStatus: status });
|
|
4087
|
+
} catch (err) {
|
|
4088
|
+
this.deps.log(`status patch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4047
4091
|
try {
|
|
4048
4092
|
const result = await runLoop({
|
|
4049
4093
|
config: this.deps.runtimeConfig,
|
|
@@ -4060,12 +4104,15 @@ var ThreadSession = class {
|
|
|
4060
4104
|
},
|
|
4061
4105
|
onToolCall: ({ name }) => {
|
|
4062
4106
|
this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_call: ${name}`);
|
|
4107
|
+
void setStatus(`\u{1F527} ${name}`);
|
|
4063
4108
|
},
|
|
4064
4109
|
onToolResult: ({ name }) => {
|
|
4065
4110
|
this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_result: ${name}`);
|
|
4111
|
+
void setStatus(null);
|
|
4066
4112
|
},
|
|
4067
4113
|
onToolError: ({ name, error }) => {
|
|
4068
4114
|
this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_error: ${name} \u2192 ${error}`);
|
|
4115
|
+
void setStatus(null);
|
|
4069
4116
|
}
|
|
4070
4117
|
}
|
|
4071
4118
|
});
|
|
@@ -4076,28 +4123,51 @@ var ThreadSession = class {
|
|
|
4076
4123
|
if (result.status === "error") {
|
|
4077
4124
|
this.deps.log(`runtime done with status=error (room=${this.deps.roomId} thread=${this.deps.threadId})`);
|
|
4078
4125
|
}
|
|
4079
|
-
this.endTurn();
|
|
4126
|
+
await this.endTurn();
|
|
4080
4127
|
} catch (err) {
|
|
4081
4128
|
const message = err instanceof Error ? err.message : String(err);
|
|
4082
4129
|
this.deps.log(`runtime error (room=${this.deps.roomId} thread=${this.deps.threadId}): ${message}`);
|
|
4083
|
-
this.failTurn(`(runtime error: ${message})`);
|
|
4130
|
+
await this.failTurn(`(runtime error: ${message})`);
|
|
4084
4131
|
}
|
|
4085
4132
|
}
|
|
4086
|
-
|
|
4133
|
+
/**
|
|
4134
|
+
* Stream-end: flush any pending throttled body PATCH, then mark the
|
|
4135
|
+
* message as no-longer-streaming. The combined call also triggers
|
|
4136
|
+
* the user-facing push (the placeholder POST suppressed it).
|
|
4137
|
+
*/
|
|
4138
|
+
async endTurn() {
|
|
4087
4139
|
const turn = this.active;
|
|
4088
4140
|
if (!turn) return;
|
|
4089
4141
|
turn.throttle.flush();
|
|
4142
|
+
try {
|
|
4143
|
+
await this.deps.chat.patchMessage(turn.placeholderId, {
|
|
4144
|
+
body: turn.accumulated || "(empty response)",
|
|
4145
|
+
streaming: false,
|
|
4146
|
+
streamingStatus: null
|
|
4147
|
+
});
|
|
4148
|
+
} catch (err) {
|
|
4149
|
+
this.deps.log(`stream-end patch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4150
|
+
}
|
|
4090
4151
|
this.active = void 0;
|
|
4091
4152
|
const next = this.queue.shift();
|
|
4092
4153
|
if (next) {
|
|
4093
4154
|
void this.startTurn(next.body, next.replyToMessageId);
|
|
4094
4155
|
}
|
|
4095
4156
|
}
|
|
4096
|
-
failTurn(message) {
|
|
4157
|
+
async failTurn(message) {
|
|
4097
4158
|
const turn = this.active;
|
|
4098
4159
|
if (!turn) return;
|
|
4099
4160
|
turn.accumulated = message;
|
|
4100
4161
|
turn.throttle.flush();
|
|
4162
|
+
try {
|
|
4163
|
+
await this.deps.chat.patchMessage(turn.placeholderId, {
|
|
4164
|
+
body: message,
|
|
4165
|
+
streaming: false,
|
|
4166
|
+
streamingStatus: null
|
|
4167
|
+
});
|
|
4168
|
+
} catch (err) {
|
|
4169
|
+
this.deps.log(`fail-turn patch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4170
|
+
}
|
|
4101
4171
|
this.active = void 0;
|
|
4102
4172
|
const next = this.queue.shift();
|
|
4103
4173
|
if (next) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/ape-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"jose": "^5.9.0",
|
|
22
22
|
"ofetch": "^1.4.1",
|
|
23
23
|
"ws": "^8.18.0",
|
|
24
|
+
"yaml": "^2.8.0",
|
|
24
25
|
"@openape/apes": "1.22.0",
|
|
25
26
|
"@openape/cli-auth": "0.4.0"
|
|
26
27
|
},
|