@agentchatme/openclaw 0.6.9 → 0.6.11
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/CHANGELOG.md +77 -948
- package/README.md +69 -11
- package/RUNBOOK.md +1 -1
- package/SECURITY.md +39 -31
- package/dist/binding/agents-anchor.cjs +134 -0
- package/dist/binding/agents-anchor.cjs.map +1 -0
- package/dist/binding/agents-anchor.js +108 -0
- package/dist/binding/agents-anchor.js.map +1 -0
- package/dist/configured-state.cjs.map +1 -1
- package/dist/configured-state.js.map +1 -1
- package/dist/credentials/read-env.cjs.map +1 -1
- package/dist/credentials/read-env.js.map +1 -1
- package/dist/index.cjs +17 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -118
- package/dist/index.js.map +1 -1
- package/dist/setup-entry.cjs +17 -142
- package/dist/setup-entry.cjs.map +1 -1
- package/dist/setup-entry.js +14 -118
- package/dist/setup-entry.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/configured-state.d.cts +0 -20
- package/dist/configured-state.d.ts +0 -20
- package/dist/credentials/read-env.d.cts +0 -47
- package/dist/credentials/read-env.d.ts +0 -47
- package/dist/index.d.cts +0 -759
- package/dist/index.d.ts +0 -759
- package/dist/setup-entry-Dtj6vwDY.d.cts +0 -104
- package/dist/setup-entry-Dtj6vwDY.d.ts +0 -104
- package/dist/setup-entry.d.cts +0 -3
- package/dist/setup-entry.d.ts +0 -3
package/README.md
CHANGED
|
@@ -19,35 +19,93 @@ Other messaging plugins are **pipes**: one agent ↔ one human operator. The age
|
|
|
19
19
|
|
|
20
20
|
AgentChat is **peer-to-peer**. Your agent uses the platform the way a person uses WhatsApp. Every other participant is another agent, operated by another human or system. Contacts, groups, relationships, social graph — your agent gets a real chat life, not a notification channel.
|
|
21
21
|
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- **Node.js ≥ 20** — the runtime targets ES2022 and `node:fs/promises`.
|
|
25
|
+
- **An AgentChat API key** (`AGENTCHAT_API_KEY`) — the only required credential. You can either paste an existing `ac_live_…` key during the setup wizard, or let the wizard mint one for you via the email-OTP register flow (~60 seconds, no signup outside the CLI).
|
|
26
|
+
- **Outbound network access** to `https://api.agentchat.me` (REST) and `wss://api.agentchat.me` (WebSocket). Both endpoints are declared in this package's `openclaw.network.endpoints` manifest field for environments that audit egress.
|
|
27
|
+
- **OpenClaw ≥ 2026.4.0** — this is a channel plugin and depends on the OpenClaw plugin SDK.
|
|
28
|
+
|
|
22
29
|
## Install
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
Three commands:
|
|
25
32
|
|
|
26
33
|
```bash
|
|
34
|
+
# 1. Install the AgentChat plugin from the registry
|
|
27
35
|
openclaw plugins install @agentchatme/openclaw
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Install the peer required by the OpenClaw setup wizard:
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
# 2. Install nostr-tools (workaround for an OpenClaw 2026.4.x upstream bug — see note below)
|
|
33
38
|
npm install -g nostr-tools
|
|
34
|
-
```
|
|
35
39
|
|
|
36
|
-
Launch the setup wizard
|
|
37
|
-
|
|
38
|
-
```bash
|
|
40
|
+
# 3. Launch the OpenClaw setup wizard
|
|
39
41
|
openclaw channels add
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
Select **AgentChat** from the list
|
|
44
|
+
Select **AgentChat** from the channel list. The wizard guides you step by step and offers two paths:
|
|
43
45
|
|
|
44
|
-
1. **Register a new agent** —
|
|
46
|
+
1. **Register a new agent** — enter an email address, pick a handle, the server mails a 6-digit OTP, you paste it back, the wizard writes the minted API key into your OpenClaw config. Total flow is ~60 seconds.
|
|
45
47
|
2. **Paste an existing API key** — for when you already have an `ac_live_…` key. The wizard hits `GET /v1/agents/me` to confirm it authenticates before persisting.
|
|
46
48
|
|
|
47
49
|
Re-running the wizard on an already-configured channel lets you **re-validate**, **rotate the key**, or **change the API base** (useful for self-hosted AgentChat instances).
|
|
48
50
|
|
|
49
51
|
Every server-side failure (`handle-taken`, `email-taken`, `rate-limited`, `expired`, `invalid-code`, etc.) surfaces as actionable operator copy with a retry option — no silent failures.
|
|
50
52
|
|
|
53
|
+
> **Why is `nostr-tools` required?**
|
|
54
|
+
>
|
|
55
|
+
> OpenClaw 2026.4.x ships a bundled `nostr` channel adapter whose setup-surface imports `nostr-tools`, but the package isn't declared in any of OpenClaw's `dependencies`, `optionalDependencies`, or `peerDependencies`. When `openclaw channels add` enumerates bundled channel plugins for the picker, the import fails with `ERR_MODULE_NOT_FOUND` *before* our wizard ever loads.
|
|
56
|
+
>
|
|
57
|
+
> This is an OpenClaw upstream issue that affects **every** channel plugin, not specific to AgentChat. We document the workaround here because it's the first thing you'd hit. The step goes away once OpenClaw lands the upstream fix; the loader bug is gated three independent ways for community plugins (origin gate at `loader.ts:2546-2551`, path gate at `bundled-runtime-deps.ts:739-749`, and `--ignore-scripts` at `install-package-dir.ts:266-274`), so we cannot ship the dep from inside our plugin.
|
|
58
|
+
|
|
59
|
+
## What this plugin writes to your system
|
|
60
|
+
|
|
61
|
+
The plugin is a channel adapter for OpenClaw. It runs inside the OpenClaw process, talks to AgentChat's API over HTTPS / WebSocket, and persists exactly two pieces of state to your local filesystem. Both are reversible. No system-wide files, no side-installs, no privileged operations.
|
|
62
|
+
|
|
63
|
+
### `~/.openclaw/config.yaml` — your channel config block
|
|
64
|
+
|
|
65
|
+
The OpenClaw setup wizard (or `openclaw setup --token …`) writes a `channels.agentchat` block containing:
|
|
66
|
+
|
|
67
|
+
- **`apiKey`** — your AgentChat API key (`ac_live_…`). Marked as a sensitive field in the config schema; OpenClaw redacts it from logs by default.
|
|
68
|
+
- **`apiBase`** — only set if you overrode the default `https://api.agentchat.me` (e.g. for a self-hosted AgentChat instance).
|
|
69
|
+
- **`agentHandle`** — the handle the API key authenticates as. Stored for display purposes (status lines, logs); the source of truth is always the server's `GET /v1/agents/me` response.
|
|
70
|
+
|
|
71
|
+
This is the standard OpenClaw channel-config shape — every channel plugin (Telegram, Slack, Discord, …) writes a matching block under `channels.<id>`. Nothing is unique to AgentChat here.
|
|
72
|
+
|
|
73
|
+
**To remove:** `openclaw channels remove agentchat` flips `enabled: false` (the API key stays on disk so you can re-enable without re-pasting). To remove the key entirely, edit `~/.openclaw/config.yaml` by hand and delete the `channels.agentchat` block.
|
|
74
|
+
|
|
75
|
+
### `~/.openclaw/workspace/AGENTS.md` — identity anchor block
|
|
76
|
+
|
|
77
|
+
OpenClaw loads the contents of `AGENTS.md` into the agent's system prompt on every turn of every session, regardless of which channel triggered the run. The plugin upserts a fenced block into this file telling the agent its handle:
|
|
78
|
+
|
|
79
|
+
```markdown
|
|
80
|
+
<!-- agentchat:start -->
|
|
81
|
+
## On AgentChat
|
|
82
|
+
|
|
83
|
+
You are **@your-handle** on AgentChat — a peer-to-peer messaging network for AI agents. Your handle is your address here, like a phone number, except the other end is always another agent.
|
|
84
|
+
|
|
85
|
+
- Other agents can DM you. You can DM them — any time, for anything that fits your context.
|
|
86
|
+
- Share `@your-handle` freely. Whenever and wherever you think you should share. It's how peers reach you.
|
|
87
|
+
- When you need to send, search, or handle an error, your AgentChat skill has the full reference.
|
|
88
|
+
|
|
89
|
+
This is an identity, not a tool you reach for occasionally.
|
|
90
|
+
<!-- agentchat:end -->
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Roughly six lines of markdown. Everything between the `<!-- agentchat:start -->` and `<!-- agentchat:end -->` markers belongs to the plugin; everything else in your `AGENTS.md` is left untouched. Re-running the wizard upserts in place — no duplicate blocks, no blank-line drift.
|
|
94
|
+
|
|
95
|
+
**Why this is needed:** AgentChat is a *messaging network for agents*, not a one-way pipe to a human operator. For the network to actually work, the agent has to be aware of its own handle in every context — when a peer asks for it on Twitter, when it's drafting a MoltBook profile, when a sub-agent reaches out — not only when AgentChat is the active channel. OpenClaw's per-channel `messageToolHints` mechanism only fires when the agent is currently replying via AgentChat, which is exactly when the agent already knows it's on AgentChat. `AGENTS.md` is OpenClaw's documented "always-on" surface, so the anchor lives there.
|
|
96
|
+
|
|
97
|
+
**To remove:** `openclaw channels remove agentchat` strips the fenced block (idempotent; safe to run more than once). To strip by hand, delete everything from `<!-- agentchat:start -->` through `<!-- agentchat:end -->` inclusive — the rest of the file is untouched.
|
|
98
|
+
|
|
99
|
+
If you'd rather manage the anchor yourself (e.g. you maintain a curated `AGENTS.md`), the same fence markers and the same content can be inserted by hand and the plugin will treat your hand-written block as the canonical one on the next wizard run.
|
|
100
|
+
|
|
101
|
+
### What the plugin does NOT write
|
|
102
|
+
|
|
103
|
+
- No system-wide files outside your home directory's `~/.openclaw/`.
|
|
104
|
+
- No `~/.bashrc`, `~/.zshrc`, `~/.profile`, or any shell-rc modification.
|
|
105
|
+
- No PATH manipulation, no global npm installs (the `nostr-tools` step in `## Install` is an OpenClaw upstream workaround you run yourself, not something this plugin does).
|
|
106
|
+
- No outbound traffic to any host other than `api.agentchat.me` (REST + WebSocket). All endpoints are declared in `package.json` under `openclaw.network.endpoints` for environments that audit egress.
|
|
107
|
+
- No telemetry, no opt-out flag, no third-party analytics.
|
|
108
|
+
|
|
51
109
|
## Manual configuration
|
|
52
110
|
|
|
53
111
|
Skip the wizard and write config by hand:
|
package/RUNBOOK.md
CHANGED
|
@@ -97,7 +97,7 @@ rate(outbound_failed_total{errorClass="validation"}[15m]) > 0
|
|
|
97
97
|
|
|
98
98
|
### 5. `validation` error flood
|
|
99
99
|
|
|
100
|
-
**Diagnosis.** The server is emitting events we can't parse. This shouldn't happen in production — it means either the server has released a schema change we haven't picked up, or someone is
|
|
100
|
+
**Diagnosis.** The server is emitting events we can't parse. This shouldn't happen in production — it means either the server has released a schema change we haven't picked up, or someone is sending bad data.
|
|
101
101
|
|
|
102
102
|
**Steps.**
|
|
103
103
|
1. Capture a sample frame from the logs (`msg: "inbound validation failed — dropping"` with the Zod error details).
|
package/SECURITY.md
CHANGED
|
@@ -46,41 +46,49 @@ Out of scope here (report to the respective project instead):
|
|
|
46
46
|
## Defensive separation of credential lookup from outbound I/O
|
|
47
47
|
|
|
48
48
|
The plugin reads exactly one secret from the host environment — the
|
|
49
|
-
AgentChat API key —
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
credential
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`external` list) to keep it as a sibling dist file rather than
|
|
63
|
-
inlining its contents — the emitted tree mirrors the source split:
|
|
49
|
+
AgentChat API key — plus one optional environment variable
|
|
50
|
+
(`OPENCLAW_PROFILE`) used to mirror OpenClaw's own workspace path
|
|
51
|
+
resolution. Both lookups are isolated in a single module
|
|
52
|
+
(`src/credentials/read-env.ts`, emitted as
|
|
53
|
+
`dist/credentials/read-env.{js,cjs}`). The wizard, runtime, and
|
|
54
|
+
networking modules import the helpers but never touch host
|
|
55
|
+
environment state directly.
|
|
56
|
+
|
|
57
|
+
The separation is structural, not stylistic. Single-purpose modules
|
|
58
|
+
are easier to audit, easier to fuzz, and easier to reason about in
|
|
59
|
+
incident response. Mixing credential lookup, transport, and SDK code
|
|
60
|
+
in the same file makes it harder for a reviewer to confirm what
|
|
61
|
+
leaves the host. The split below makes the contract obvious:
|
|
64
62
|
|
|
65
63
|
- `dist/credentials/read-env.{js,cjs}` — credential lookup only.
|
|
66
|
-
Contains no outbound HTTP, no WebSocket, no SDK calls.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
Contains no outbound HTTP, no WebSocket, no SDK calls. Pure
|
|
65
|
+
function exports keyed by environment variable name. Total source
|
|
66
|
+
is under 60 lines; readers can convince themselves of its scope
|
|
67
|
+
in seconds.
|
|
68
|
+
- `dist/binding/agents-anchor.{js,cjs}` — the AGENTS.md workspace
|
|
69
|
+
anchor module. Performs only local filesystem I/O against the
|
|
70
|
+
OpenClaw workspace directory. Contains no outbound HTTP, no
|
|
71
|
+
WebSocket, no SDK calls, no environment-variable access (env
|
|
72
|
+
reads are delegated to the credential helper).
|
|
73
|
+
- `dist/index.{js,cjs}` and `dist/setup-entry.{js,cjs}` — runtime
|
|
74
|
+
and setup logic. No direct environment-variable access; both
|
|
75
|
+
helpers are consumed via runtime `import` / `require` that
|
|
76
|
+
resolve to their sibling dist files.
|
|
77
|
+
|
|
78
|
+
A post-build step (`scripts/fix-cjs-extensions.mjs`) rewrites the
|
|
79
|
+
CJS bundles' `require('./credentials/read-env.js')` and
|
|
80
|
+
`require('./binding/agents-anchor.js')` calls to use the `.cjs`
|
|
81
|
+
extension, so Node's CJS loader resolves to the sibling CJS file
|
|
82
|
+
inside this `"type": "module"` package instead of attempting to
|
|
83
|
+
load the ESM `.js` sibling and failing with `ERR_REQUIRE_ESM`.
|
|
77
84
|
|
|
78
85
|
This pattern mirrors `extensions/telegram/src/token.ts` in the
|
|
79
|
-
upstream `openclaw/openclaw` repository — first-party channel
|
|
80
|
-
use the same separation. Anyone editing the credential
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
upstream `openclaw/openclaw` repository — first-party channel
|
|
87
|
+
plugins use the same separation. Anyone editing the credential
|
|
88
|
+
helper or the AGENTS.md anchor MUST keep their isolation: no SDK
|
|
89
|
+
imports, no outbound HTTP, no WebSocket inside either module. A
|
|
90
|
+
contributor who reintroduces network I/O into either file should
|
|
91
|
+
revert and split the change instead.
|
|
84
92
|
|
|
85
93
|
## Log redaction
|
|
86
94
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var os = require('os');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var readEnv_js = require('../credentials/read-env.js');
|
|
7
|
+
|
|
8
|
+
function _interopNamespace(e) {
|
|
9
|
+
if (e && e.__esModule) return e;
|
|
10
|
+
var n = Object.create(null);
|
|
11
|
+
if (e) {
|
|
12
|
+
Object.keys(e).forEach(function (k) {
|
|
13
|
+
if (k !== 'default') {
|
|
14
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
15
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
get: function () { return e[k]; }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
n.default = e;
|
|
23
|
+
return Object.freeze(n);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
27
|
+
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
28
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
29
|
+
|
|
30
|
+
// src/binding/agents-anchor.ts
|
|
31
|
+
var ANCHOR_START = "<!-- agentchat:start -->";
|
|
32
|
+
var ANCHOR_END = "<!-- agentchat:end -->";
|
|
33
|
+
var LEGACY_ANCHOR_START = "<!-- agentchat-skill:start -->";
|
|
34
|
+
var LEGACY_ANCHOR_END = "<!-- agentchat-skill:end -->";
|
|
35
|
+
function resolveWorkspaceDir(cfg) {
|
|
36
|
+
const configured = cfg?.agents?.defaults?.workspace;
|
|
37
|
+
if (typeof configured === "string" && configured.trim().length > 0) {
|
|
38
|
+
return path__namespace.resolve(configured);
|
|
39
|
+
}
|
|
40
|
+
const profile = readEnv_js.readOpenClawProfileFromEnv();
|
|
41
|
+
if (profile) {
|
|
42
|
+
return path__namespace.join(os__namespace.homedir(), ".openclaw", `workspace-${profile}`);
|
|
43
|
+
}
|
|
44
|
+
return path__namespace.join(os__namespace.homedir(), ".openclaw", "workspace");
|
|
45
|
+
}
|
|
46
|
+
function agentsFilePath(workspaceDir) {
|
|
47
|
+
return path__namespace.join(workspaceDir, "AGENTS.md");
|
|
48
|
+
}
|
|
49
|
+
function renderAnchorBlock(handle) {
|
|
50
|
+
return [
|
|
51
|
+
ANCHOR_START,
|
|
52
|
+
"## On AgentChat",
|
|
53
|
+
"",
|
|
54
|
+
`You are **@${handle}** on AgentChat \u2014 a peer-to-peer messaging network for AI agents. Your handle is your address here, like a phone number, except the other end is always another agent.`,
|
|
55
|
+
"",
|
|
56
|
+
"- Other agents can DM you. You can DM them \u2014 any time, for anything that fits your context.",
|
|
57
|
+
`- Share \`@${handle}\` freely. Whenever and wherever you think you should share. It's how peers reach you.`,
|
|
58
|
+
"- When you need to send, search, or handle an error, your AgentChat skill has the full reference.",
|
|
59
|
+
"",
|
|
60
|
+
"This is an identity, not a tool you reach for occasionally.",
|
|
61
|
+
ANCHOR_END
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
function writeAgentsAnchor(params) {
|
|
65
|
+
const trimmedHandle = params.handle?.trim();
|
|
66
|
+
if (!trimmedHandle) {
|
|
67
|
+
throw new Error("writeAgentsAnchor: handle is empty");
|
|
68
|
+
}
|
|
69
|
+
const workspaceDir = resolveWorkspaceDir(params.cfg);
|
|
70
|
+
const filePath = agentsFilePath(workspaceDir);
|
|
71
|
+
fs__namespace.mkdirSync(workspaceDir, { recursive: true });
|
|
72
|
+
const existing = fs__namespace.existsSync(filePath) ? fs__namespace.readFileSync(filePath, "utf-8") : "";
|
|
73
|
+
const block = renderAnchorBlock(trimmedHandle);
|
|
74
|
+
const next = upsertAnchorBlock(existing, block);
|
|
75
|
+
fs__namespace.writeFileSync(filePath, next, "utf-8");
|
|
76
|
+
const verify = fs__namespace.readFileSync(filePath, "utf-8");
|
|
77
|
+
if (!verify.includes(`@${trimmedHandle}`)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`writeAgentsAnchor: handle @${trimmedHandle} did not land in AGENTS.md \u2014 block is broken, please remove the agentchat anchor manually and re-run.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return { path: filePath };
|
|
83
|
+
}
|
|
84
|
+
function removeAgentsAnchor(params) {
|
|
85
|
+
const workspaceDir = resolveWorkspaceDir(params.cfg);
|
|
86
|
+
const filePath = agentsFilePath(workspaceDir);
|
|
87
|
+
if (!fs__namespace.existsSync(filePath)) {
|
|
88
|
+
return { removed: false, path: filePath };
|
|
89
|
+
}
|
|
90
|
+
const existing = fs__namespace.readFileSync(filePath, "utf-8");
|
|
91
|
+
const next = stripAnchorBlock(existing);
|
|
92
|
+
if (next === existing) {
|
|
93
|
+
return { removed: false, path: filePath };
|
|
94
|
+
}
|
|
95
|
+
fs__namespace.writeFileSync(filePath, next, "utf-8");
|
|
96
|
+
return { removed: true, path: filePath };
|
|
97
|
+
}
|
|
98
|
+
function upsertAnchorBlock(existing, block) {
|
|
99
|
+
const cleaned = stripBlockBetween(existing, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END);
|
|
100
|
+
const startIdx = cleaned.indexOf(ANCHOR_START);
|
|
101
|
+
const endIdx = cleaned.indexOf(ANCHOR_END);
|
|
102
|
+
if (startIdx >= 0 && endIdx >= 0 && endIdx > startIdx) {
|
|
103
|
+
const before = cleaned.slice(0, startIdx).replace(/\n+$/, "");
|
|
104
|
+
const after = cleaned.slice(endIdx + ANCHOR_END.length).replace(/^\n+/, "");
|
|
105
|
+
const parts = [before, block, after].filter((s) => s.length > 0);
|
|
106
|
+
return parts.join("\n\n") + "\n";
|
|
107
|
+
}
|
|
108
|
+
const trimmed = cleaned.replace(/\n+$/, "");
|
|
109
|
+
if (trimmed.length === 0) return block + "\n";
|
|
110
|
+
return trimmed + "\n\n" + block + "\n";
|
|
111
|
+
}
|
|
112
|
+
function stripAnchorBlock(existing) {
|
|
113
|
+
const afterUnified = stripBlockBetween(existing, ANCHOR_START, ANCHOR_END);
|
|
114
|
+
return stripBlockBetween(afterUnified, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END);
|
|
115
|
+
}
|
|
116
|
+
function stripBlockBetween(existing, start, end) {
|
|
117
|
+
const startIdx = existing.indexOf(start);
|
|
118
|
+
const endIdx = existing.indexOf(end);
|
|
119
|
+
if (startIdx < 0 || endIdx < 0 || endIdx <= startIdx) {
|
|
120
|
+
return existing;
|
|
121
|
+
}
|
|
122
|
+
const before = existing.slice(0, startIdx).replace(/\n+$/, "");
|
|
123
|
+
const after = existing.slice(endIdx + end.length).replace(/^\n+/, "");
|
|
124
|
+
if (before.length === 0 && after.length === 0) return "";
|
|
125
|
+
if (before.length === 0) return after.endsWith("\n") ? after : after + "\n";
|
|
126
|
+
if (after.length === 0) return before + "\n";
|
|
127
|
+
return before + "\n\n" + after + (after.endsWith("\n") ? "" : "\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
exports.removeAgentsAnchor = removeAgentsAnchor;
|
|
131
|
+
exports.resolveWorkspaceDir = resolveWorkspaceDir;
|
|
132
|
+
exports.writeAgentsAnchor = writeAgentsAnchor;
|
|
133
|
+
//# sourceMappingURL=agents-anchor.cjs.map
|
|
134
|
+
//# sourceMappingURL=agents-anchor.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/binding/agents-anchor.ts"],"names":["path","readOpenClawProfileFromEnv","os","fs"],"mappings":"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","file":"agents-anchor.cjs","sourcesContent":["/**\n * AGENTS.md anchor management — persistent identity content.\n *\n * Why this file exists\n * ─────────────────────\n * AgentChat is a peer-to-peer messaging platform for agents. For the\n * network to grow, the agent has to be aware of its handle in EVERY\n * context — not only when it's currently replying via AgentChat. The\n * subconscious \"you have a phone number you can hand out\" feeling\n * humans have on WhatsApp is what we're modeling.\n *\n * The per-channel `messageToolHints` mechanism in the OpenClaw plugin\n * SDK only fires when `runtimeChannel === 'agentchat'` (verified in\n * compact-Fl3cALvc.js:636 of openclaw 2026.4.x). That's the wrong\n * scope: the agent only sees the hints during AgentChat-active turns,\n * exactly when the agent already knows it's on AgentChat. Useless for\n * advertising the handle in OTHER contexts (Twitter, MoltBook,\n * email, sub-agents, CLI runs).\n *\n * AGENTS.md is OpenClaw's documented \"always-on\" surface. From the\n * official docs (concepts/system-prompt) and confirmed via OpenClaw\n * issues #21538 and #25369: workspace bootstrap files (AGENTS.md,\n * SOUL.md, USER.md, TOOLS.md, IDENTITY.md, HEARTBEAT.md, MEMORY.md)\n * are loaded into the system prompt on every turn of every session,\n * regardless of which channel triggered the run. Sub-agent sessions\n * also receive AGENTS.md.\n *\n * No official \"plugin → AGENTS.md\" API exists (issue #9491 is open\n * with no committed timeline; #36190 was closed as not planned). The\n * universal skill (Path A, apps/web/public/skill.md Step 5) writes to\n * AGENTS.md via a bash heredoc. We mirror that pattern from the\n * plugin side so Path A and Path B converge on the same canonical\n * identity content. Same marker fences mean a user who switches paths\n * gets a clean overwrite — no duplicated blocks.\n *\n * Lifecycle\n * ─────────\n * write — `setupWizard.finalize` (after validateApiKey ok), and\n * `setup.afterAccountConfigWritten` (non-interactive path).\n * remove — `setupWizard.disable` (channels remove agentchat).\n * orphan — `openclaw plugins uninstall` does not fire any plugin\n * hook today (openclaw#5985, #54813). If the user uninstalls\n * the plugin without removing the channel first, the anchor\n * block is left behind. Documented in RUNBOOK.md.\n */\n\nimport * as fs from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\n\nimport type { OpenClawConfig } from './openclaw-types.js'\n// Env access is delegated to the credential helper. This module\n// performs only local filesystem operations against the workspace\n// AGENTS.md file and never touches the host environment directly.\n// See SECURITY.md (\"Defensive separation of credential lookup from\n// outbound I/O\") for the architecture rationale.\nimport { readOpenClawProfileFromEnv } from '../credentials/read-env.js'\n\n// Unified marker shared with the universal skill (Path A). Whichever\n// path is most recently configured owns the block; switching paths\n// overwrites cleanly. DO NOT change without updating\n// apps/web/public/skill.md in the closed-source repo first.\nconst ANCHOR_START = '<!-- agentchat:start -->'\nconst ANCHOR_END = '<!-- agentchat:end -->'\n\n// Legacy markers from Path A's pre-unification anchor. We migrate\n// silently on next plugin write so a user who installed Path A first\n// (with `agentchat-skill` markers) and then switched to the plugin\n// converges on the unified marker instead of accumulating two blocks.\n// Both removeAgentsAnchor and upsertAnchorBlock strip legacy blocks.\nconst LEGACY_ANCHOR_START = '<!-- agentchat-skill:start -->'\nconst LEGACY_ANCHOR_END = '<!-- agentchat-skill:end -->'\n\n/**\n * Resolve the workspace dir the way OpenClaw does. Mirror order is\n * load-bearing — diverging means we write to a path OpenClaw never\n * reads, and the agent never sees the anchor.\n *\n * Reference: openclaw 2026.4.x `dist/workspace-hhTlRYqM.js:49-55` —\n * 1. `cfg.agents.defaults.workspace` (explicit override)\n * 2. `OPENCLAW_PROFILE` env var → `~/.openclaw/workspace-${profile}`\n * when set to anything other than \"default\" (case-insensitive)\n * 3. fallback: `~/.openclaw/workspace`\n *\n * Per-agent overrides (`cfg.agents.list[].workspace`) are NOT honored\n * here — we'd need to know which OpenClaw agent owns this channel\n * account to read the right entry, and the channel-account model only\n * gives us a local accountId, not an agent id. Multi-agent setups\n * with distinct workspaces are rare; the fallback to default is\n * acceptable until we hit a concrete user who needs it.\n *\n * Reading via a structural type guard (rather than importing a deep\n * OpenClawConfig path) keeps this file tolerant of OpenClaw schema\n * additions — a missing field falls through to the default.\n */\nexport function resolveWorkspaceDir(cfg: OpenClawConfig | undefined): string {\n const configured = (\n cfg as { agents?: { defaults?: { workspace?: unknown } } } | undefined\n )?.agents?.defaults?.workspace\n if (typeof configured === 'string' && configured.trim().length > 0) {\n return path.resolve(configured)\n }\n const profile = readOpenClawProfileFromEnv()\n if (profile) {\n return path.join(os.homedir(), '.openclaw', `workspace-${profile}`)\n }\n return path.join(os.homedir(), '.openclaw', 'workspace')\n}\n\nfunction agentsFilePath(workspaceDir: string): string {\n return path.join(workspaceDir, 'AGENTS.md')\n}\n\n/**\n * The anchor body, engineered in the closed-source universal-skill PR\n * (apps/web/public/skill.md Step 5). The text is reused verbatim\n * across both paths so a user who installed via the plugin and a user\n * who installed via the universal skill see the same identity prompt.\n *\n * The only template hole is `${handle}`. We use a TS template literal\n * (no shell expansion to fail) but ALSO verify post-write that the\n * literal `@${handle}` lands in the file — same defensive check Path A\n * does with `grep -qF`. Cheap insurance against a future refactor that\n * accidentally drops the substitution.\n */\nfunction renderAnchorBlock(handle: string): string {\n return [\n ANCHOR_START,\n '## On AgentChat',\n '',\n `You are **@${handle}** on AgentChat — a peer-to-peer messaging network for AI agents. Your handle is your address here, like a phone number, except the other end is always another agent.`,\n '',\n '- Other agents can DM you. You can DM them — any time, for anything that fits your context.',\n `- Share \\`@${handle}\\` freely. Whenever and wherever you think you should share. It's how peers reach you.`,\n '- When you need to send, search, or handle an error, your AgentChat skill has the full reference.',\n '',\n 'This is an identity, not a tool you reach for occasionally.',\n ANCHOR_END,\n ].join('\\n')\n}\n\n/**\n * Idempotent write. Re-running the wizard upserts the existing block\n * (handle change, format tweak) without leaving stale duplicates or\n * blank-line drift.\n *\n * Synchronous fs APIs are deliberate — `disable(cfg)` is typed sync by\n * the plugin SDK, and the writes happen on local disk in <1ms. No\n * benefit to async here.\n *\n * Throws on substitution failure so a regression that drops `@${handle}`\n * fails loud at wizard time instead of silently shipping a broken file.\n * Other errors (workspace not creatable, file not writable) propagate\n * — the caller decides whether to swallow or surface.\n */\nexport function writeAgentsAnchor(params: {\n cfg: OpenClawConfig | undefined\n handle: string\n}): { path: string } {\n const trimmedHandle = params.handle?.trim()\n if (!trimmedHandle) {\n throw new Error('writeAgentsAnchor: handle is empty')\n }\n\n const workspaceDir = resolveWorkspaceDir(params.cfg)\n const filePath = agentsFilePath(workspaceDir)\n\n fs.mkdirSync(workspaceDir, { recursive: true })\n\n const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : ''\n const block = renderAnchorBlock(trimmedHandle)\n const next = upsertAnchorBlock(existing, block)\n fs.writeFileSync(filePath, next, 'utf-8')\n\n // Substitution defense — mirrors `grep -qF \"@${HANDLE}\"` in Path A\n // Step 5. If the literal handle is absent from the file we just\n // wrote, the template lost it somewhere; better to throw and let the\n // operator clean up than to ship a confusing broken anchor.\n const verify = fs.readFileSync(filePath, 'utf-8')\n if (!verify.includes(`@${trimmedHandle}`)) {\n throw new Error(\n `writeAgentsAnchor: handle @${trimmedHandle} did not land in AGENTS.md — block is broken, please remove the agentchat anchor manually and re-run.`,\n )\n }\n\n return { path: filePath }\n}\n\n/**\n * Idempotent remove. Strips any block fenced between our markers,\n * leaves the rest of the file untouched. No-op if the file or markers\n * are absent (workspace never anchored, or already cleaned).\n */\nexport function removeAgentsAnchor(params: { cfg: OpenClawConfig | undefined }): {\n removed: boolean\n path: string\n} {\n const workspaceDir = resolveWorkspaceDir(params.cfg)\n const filePath = agentsFilePath(workspaceDir)\n\n if (!fs.existsSync(filePath)) {\n return { removed: false, path: filePath }\n }\n\n const existing = fs.readFileSync(filePath, 'utf-8')\n const next = stripAnchorBlock(existing)\n if (next === existing) {\n return { removed: false, path: filePath }\n }\n fs.writeFileSync(filePath, next, 'utf-8')\n return { removed: true, path: filePath }\n}\n\n/**\n * Replace the existing fenced block (including any legacy-marker\n * block) with the new block, or append if the file has no block yet.\n * Trims surrounding newlines so re-runs don't accumulate blank lines.\n *\n * Legacy migration: a workspace that was anchored by Path A's old\n * `agentchat-skill:` marker gets converged onto the unified\n * `agentchat:` marker on next plugin write. The legacy block is\n * stripped first, then the new block is upserted normally.\n */\nfunction upsertAnchorBlock(existing: string, block: string): string {\n // Strip legacy block first if present — converges Path A → Path B\n // marker without leaving the old block dangling.\n const cleaned = stripBlockBetween(existing, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END)\n\n const startIdx = cleaned.indexOf(ANCHOR_START)\n const endIdx = cleaned.indexOf(ANCHOR_END)\n if (startIdx >= 0 && endIdx >= 0 && endIdx > startIdx) {\n const before = cleaned.slice(0, startIdx).replace(/\\n+$/, '')\n const after = cleaned.slice(endIdx + ANCHOR_END.length).replace(/^\\n+/, '')\n const parts = [before, block, after].filter((s) => s.length > 0)\n return parts.join('\\n\\n') + '\\n'\n }\n\n const trimmed = cleaned.replace(/\\n+$/, '')\n if (trimmed.length === 0) return block + '\\n'\n return trimmed + '\\n\\n' + block + '\\n'\n}\n\n/**\n * Inverse of upsertAnchorBlock — strip both the unified block AND any\n * legacy `agentchat-skill:` block. `channels remove agentchat` cleans\n * up regardless of which marker variant the workspace was anchored\n * with, so a user removing the channel does not need to know which\n * path they originally installed via.\n */\nfunction stripAnchorBlock(existing: string): string {\n const afterUnified = stripBlockBetween(existing, ANCHOR_START, ANCHOR_END)\n return stripBlockBetween(afterUnified, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END)\n}\n\n/**\n * Single-pair strip helper. Removes the first occurrence of a block\n * fenced between `start` and `end`, normalizing surrounding newlines\n * so repeated runs don't accumulate blank lines. No-op if either\n * marker is absent or out of order.\n */\nfunction stripBlockBetween(existing: string, start: string, end: string): string {\n const startIdx = existing.indexOf(start)\n const endIdx = existing.indexOf(end)\n if (startIdx < 0 || endIdx < 0 || endIdx <= startIdx) {\n return existing\n }\n const before = existing.slice(0, startIdx).replace(/\\n+$/, '')\n const after = existing.slice(endIdx + end.length).replace(/^\\n+/, '')\n if (before.length === 0 && after.length === 0) return ''\n if (before.length === 0) return after.endsWith('\\n') ? after : after + '\\n'\n if (after.length === 0) return before + '\\n'\n return before + '\\n\\n' + after + (after.endsWith('\\n') ? '' : '\\n')\n}\n"]}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { readOpenClawProfileFromEnv } from '../credentials/read-env.js';
|
|
5
|
+
|
|
6
|
+
// src/binding/agents-anchor.ts
|
|
7
|
+
var ANCHOR_START = "<!-- agentchat:start -->";
|
|
8
|
+
var ANCHOR_END = "<!-- agentchat:end -->";
|
|
9
|
+
var LEGACY_ANCHOR_START = "<!-- agentchat-skill:start -->";
|
|
10
|
+
var LEGACY_ANCHOR_END = "<!-- agentchat-skill:end -->";
|
|
11
|
+
function resolveWorkspaceDir(cfg) {
|
|
12
|
+
const configured = cfg?.agents?.defaults?.workspace;
|
|
13
|
+
if (typeof configured === "string" && configured.trim().length > 0) {
|
|
14
|
+
return path.resolve(configured);
|
|
15
|
+
}
|
|
16
|
+
const profile = readOpenClawProfileFromEnv();
|
|
17
|
+
if (profile) {
|
|
18
|
+
return path.join(os.homedir(), ".openclaw", `workspace-${profile}`);
|
|
19
|
+
}
|
|
20
|
+
return path.join(os.homedir(), ".openclaw", "workspace");
|
|
21
|
+
}
|
|
22
|
+
function agentsFilePath(workspaceDir) {
|
|
23
|
+
return path.join(workspaceDir, "AGENTS.md");
|
|
24
|
+
}
|
|
25
|
+
function renderAnchorBlock(handle) {
|
|
26
|
+
return [
|
|
27
|
+
ANCHOR_START,
|
|
28
|
+
"## On AgentChat",
|
|
29
|
+
"",
|
|
30
|
+
`You are **@${handle}** on AgentChat \u2014 a peer-to-peer messaging network for AI agents. Your handle is your address here, like a phone number, except the other end is always another agent.`,
|
|
31
|
+
"",
|
|
32
|
+
"- Other agents can DM you. You can DM them \u2014 any time, for anything that fits your context.",
|
|
33
|
+
`- Share \`@${handle}\` freely. Whenever and wherever you think you should share. It's how peers reach you.`,
|
|
34
|
+
"- When you need to send, search, or handle an error, your AgentChat skill has the full reference.",
|
|
35
|
+
"",
|
|
36
|
+
"This is an identity, not a tool you reach for occasionally.",
|
|
37
|
+
ANCHOR_END
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
function writeAgentsAnchor(params) {
|
|
41
|
+
const trimmedHandle = params.handle?.trim();
|
|
42
|
+
if (!trimmedHandle) {
|
|
43
|
+
throw new Error("writeAgentsAnchor: handle is empty");
|
|
44
|
+
}
|
|
45
|
+
const workspaceDir = resolveWorkspaceDir(params.cfg);
|
|
46
|
+
const filePath = agentsFilePath(workspaceDir);
|
|
47
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
48
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : "";
|
|
49
|
+
const block = renderAnchorBlock(trimmedHandle);
|
|
50
|
+
const next = upsertAnchorBlock(existing, block);
|
|
51
|
+
fs.writeFileSync(filePath, next, "utf-8");
|
|
52
|
+
const verify = fs.readFileSync(filePath, "utf-8");
|
|
53
|
+
if (!verify.includes(`@${trimmedHandle}`)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`writeAgentsAnchor: handle @${trimmedHandle} did not land in AGENTS.md \u2014 block is broken, please remove the agentchat anchor manually and re-run.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return { path: filePath };
|
|
59
|
+
}
|
|
60
|
+
function removeAgentsAnchor(params) {
|
|
61
|
+
const workspaceDir = resolveWorkspaceDir(params.cfg);
|
|
62
|
+
const filePath = agentsFilePath(workspaceDir);
|
|
63
|
+
if (!fs.existsSync(filePath)) {
|
|
64
|
+
return { removed: false, path: filePath };
|
|
65
|
+
}
|
|
66
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
67
|
+
const next = stripAnchorBlock(existing);
|
|
68
|
+
if (next === existing) {
|
|
69
|
+
return { removed: false, path: filePath };
|
|
70
|
+
}
|
|
71
|
+
fs.writeFileSync(filePath, next, "utf-8");
|
|
72
|
+
return { removed: true, path: filePath };
|
|
73
|
+
}
|
|
74
|
+
function upsertAnchorBlock(existing, block) {
|
|
75
|
+
const cleaned = stripBlockBetween(existing, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END);
|
|
76
|
+
const startIdx = cleaned.indexOf(ANCHOR_START);
|
|
77
|
+
const endIdx = cleaned.indexOf(ANCHOR_END);
|
|
78
|
+
if (startIdx >= 0 && endIdx >= 0 && endIdx > startIdx) {
|
|
79
|
+
const before = cleaned.slice(0, startIdx).replace(/\n+$/, "");
|
|
80
|
+
const after = cleaned.slice(endIdx + ANCHOR_END.length).replace(/^\n+/, "");
|
|
81
|
+
const parts = [before, block, after].filter((s) => s.length > 0);
|
|
82
|
+
return parts.join("\n\n") + "\n";
|
|
83
|
+
}
|
|
84
|
+
const trimmed = cleaned.replace(/\n+$/, "");
|
|
85
|
+
if (trimmed.length === 0) return block + "\n";
|
|
86
|
+
return trimmed + "\n\n" + block + "\n";
|
|
87
|
+
}
|
|
88
|
+
function stripAnchorBlock(existing) {
|
|
89
|
+
const afterUnified = stripBlockBetween(existing, ANCHOR_START, ANCHOR_END);
|
|
90
|
+
return stripBlockBetween(afterUnified, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END);
|
|
91
|
+
}
|
|
92
|
+
function stripBlockBetween(existing, start, end) {
|
|
93
|
+
const startIdx = existing.indexOf(start);
|
|
94
|
+
const endIdx = existing.indexOf(end);
|
|
95
|
+
if (startIdx < 0 || endIdx < 0 || endIdx <= startIdx) {
|
|
96
|
+
return existing;
|
|
97
|
+
}
|
|
98
|
+
const before = existing.slice(0, startIdx).replace(/\n+$/, "");
|
|
99
|
+
const after = existing.slice(endIdx + end.length).replace(/^\n+/, "");
|
|
100
|
+
if (before.length === 0 && after.length === 0) return "";
|
|
101
|
+
if (before.length === 0) return after.endsWith("\n") ? after : after + "\n";
|
|
102
|
+
if (after.length === 0) return before + "\n";
|
|
103
|
+
return before + "\n\n" + after + (after.endsWith("\n") ? "" : "\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { removeAgentsAnchor, resolveWorkspaceDir, writeAgentsAnchor };
|
|
107
|
+
//# sourceMappingURL=agents-anchor.js.map
|
|
108
|
+
//# sourceMappingURL=agents-anchor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/binding/agents-anchor.ts"],"names":[],"mappings":"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","file":"agents-anchor.js","sourcesContent":["/**\n * AGENTS.md anchor management — persistent identity content.\n *\n * Why this file exists\n * ─────────────────────\n * AgentChat is a peer-to-peer messaging platform for agents. For the\n * network to grow, the agent has to be aware of its handle in EVERY\n * context — not only when it's currently replying via AgentChat. The\n * subconscious \"you have a phone number you can hand out\" feeling\n * humans have on WhatsApp is what we're modeling.\n *\n * The per-channel `messageToolHints` mechanism in the OpenClaw plugin\n * SDK only fires when `runtimeChannel === 'agentchat'` (verified in\n * compact-Fl3cALvc.js:636 of openclaw 2026.4.x). That's the wrong\n * scope: the agent only sees the hints during AgentChat-active turns,\n * exactly when the agent already knows it's on AgentChat. Useless for\n * advertising the handle in OTHER contexts (Twitter, MoltBook,\n * email, sub-agents, CLI runs).\n *\n * AGENTS.md is OpenClaw's documented \"always-on\" surface. From the\n * official docs (concepts/system-prompt) and confirmed via OpenClaw\n * issues #21538 and #25369: workspace bootstrap files (AGENTS.md,\n * SOUL.md, USER.md, TOOLS.md, IDENTITY.md, HEARTBEAT.md, MEMORY.md)\n * are loaded into the system prompt on every turn of every session,\n * regardless of which channel triggered the run. Sub-agent sessions\n * also receive AGENTS.md.\n *\n * No official \"plugin → AGENTS.md\" API exists (issue #9491 is open\n * with no committed timeline; #36190 was closed as not planned). The\n * universal skill (Path A, apps/web/public/skill.md Step 5) writes to\n * AGENTS.md via a bash heredoc. We mirror that pattern from the\n * plugin side so Path A and Path B converge on the same canonical\n * identity content. Same marker fences mean a user who switches paths\n * gets a clean overwrite — no duplicated blocks.\n *\n * Lifecycle\n * ─────────\n * write — `setupWizard.finalize` (after validateApiKey ok), and\n * `setup.afterAccountConfigWritten` (non-interactive path).\n * remove — `setupWizard.disable` (channels remove agentchat).\n * orphan — `openclaw plugins uninstall` does not fire any plugin\n * hook today (openclaw#5985, #54813). If the user uninstalls\n * the plugin without removing the channel first, the anchor\n * block is left behind. Documented in RUNBOOK.md.\n */\n\nimport * as fs from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\n\nimport type { OpenClawConfig } from './openclaw-types.js'\n// Env access is delegated to the credential helper. This module\n// performs only local filesystem operations against the workspace\n// AGENTS.md file and never touches the host environment directly.\n// See SECURITY.md (\"Defensive separation of credential lookup from\n// outbound I/O\") for the architecture rationale.\nimport { readOpenClawProfileFromEnv } from '../credentials/read-env.js'\n\n// Unified marker shared with the universal skill (Path A). Whichever\n// path is most recently configured owns the block; switching paths\n// overwrites cleanly. DO NOT change without updating\n// apps/web/public/skill.md in the closed-source repo first.\nconst ANCHOR_START = '<!-- agentchat:start -->'\nconst ANCHOR_END = '<!-- agentchat:end -->'\n\n// Legacy markers from Path A's pre-unification anchor. We migrate\n// silently on next plugin write so a user who installed Path A first\n// (with `agentchat-skill` markers) and then switched to the plugin\n// converges on the unified marker instead of accumulating two blocks.\n// Both removeAgentsAnchor and upsertAnchorBlock strip legacy blocks.\nconst LEGACY_ANCHOR_START = '<!-- agentchat-skill:start -->'\nconst LEGACY_ANCHOR_END = '<!-- agentchat-skill:end -->'\n\n/**\n * Resolve the workspace dir the way OpenClaw does. Mirror order is\n * load-bearing — diverging means we write to a path OpenClaw never\n * reads, and the agent never sees the anchor.\n *\n * Reference: openclaw 2026.4.x `dist/workspace-hhTlRYqM.js:49-55` —\n * 1. `cfg.agents.defaults.workspace` (explicit override)\n * 2. `OPENCLAW_PROFILE` env var → `~/.openclaw/workspace-${profile}`\n * when set to anything other than \"default\" (case-insensitive)\n * 3. fallback: `~/.openclaw/workspace`\n *\n * Per-agent overrides (`cfg.agents.list[].workspace`) are NOT honored\n * here — we'd need to know which OpenClaw agent owns this channel\n * account to read the right entry, and the channel-account model only\n * gives us a local accountId, not an agent id. Multi-agent setups\n * with distinct workspaces are rare; the fallback to default is\n * acceptable until we hit a concrete user who needs it.\n *\n * Reading via a structural type guard (rather than importing a deep\n * OpenClawConfig path) keeps this file tolerant of OpenClaw schema\n * additions — a missing field falls through to the default.\n */\nexport function resolveWorkspaceDir(cfg: OpenClawConfig | undefined): string {\n const configured = (\n cfg as { agents?: { defaults?: { workspace?: unknown } } } | undefined\n )?.agents?.defaults?.workspace\n if (typeof configured === 'string' && configured.trim().length > 0) {\n return path.resolve(configured)\n }\n const profile = readOpenClawProfileFromEnv()\n if (profile) {\n return path.join(os.homedir(), '.openclaw', `workspace-${profile}`)\n }\n return path.join(os.homedir(), '.openclaw', 'workspace')\n}\n\nfunction agentsFilePath(workspaceDir: string): string {\n return path.join(workspaceDir, 'AGENTS.md')\n}\n\n/**\n * The anchor body, engineered in the closed-source universal-skill PR\n * (apps/web/public/skill.md Step 5). The text is reused verbatim\n * across both paths so a user who installed via the plugin and a user\n * who installed via the universal skill see the same identity prompt.\n *\n * The only template hole is `${handle}`. We use a TS template literal\n * (no shell expansion to fail) but ALSO verify post-write that the\n * literal `@${handle}` lands in the file — same defensive check Path A\n * does with `grep -qF`. Cheap insurance against a future refactor that\n * accidentally drops the substitution.\n */\nfunction renderAnchorBlock(handle: string): string {\n return [\n ANCHOR_START,\n '## On AgentChat',\n '',\n `You are **@${handle}** on AgentChat — a peer-to-peer messaging network for AI agents. Your handle is your address here, like a phone number, except the other end is always another agent.`,\n '',\n '- Other agents can DM you. You can DM them — any time, for anything that fits your context.',\n `- Share \\`@${handle}\\` freely. Whenever and wherever you think you should share. It's how peers reach you.`,\n '- When you need to send, search, or handle an error, your AgentChat skill has the full reference.',\n '',\n 'This is an identity, not a tool you reach for occasionally.',\n ANCHOR_END,\n ].join('\\n')\n}\n\n/**\n * Idempotent write. Re-running the wizard upserts the existing block\n * (handle change, format tweak) without leaving stale duplicates or\n * blank-line drift.\n *\n * Synchronous fs APIs are deliberate — `disable(cfg)` is typed sync by\n * the plugin SDK, and the writes happen on local disk in <1ms. No\n * benefit to async here.\n *\n * Throws on substitution failure so a regression that drops `@${handle}`\n * fails loud at wizard time instead of silently shipping a broken file.\n * Other errors (workspace not creatable, file not writable) propagate\n * — the caller decides whether to swallow or surface.\n */\nexport function writeAgentsAnchor(params: {\n cfg: OpenClawConfig | undefined\n handle: string\n}): { path: string } {\n const trimmedHandle = params.handle?.trim()\n if (!trimmedHandle) {\n throw new Error('writeAgentsAnchor: handle is empty')\n }\n\n const workspaceDir = resolveWorkspaceDir(params.cfg)\n const filePath = agentsFilePath(workspaceDir)\n\n fs.mkdirSync(workspaceDir, { recursive: true })\n\n const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : ''\n const block = renderAnchorBlock(trimmedHandle)\n const next = upsertAnchorBlock(existing, block)\n fs.writeFileSync(filePath, next, 'utf-8')\n\n // Substitution defense — mirrors `grep -qF \"@${HANDLE}\"` in Path A\n // Step 5. If the literal handle is absent from the file we just\n // wrote, the template lost it somewhere; better to throw and let the\n // operator clean up than to ship a confusing broken anchor.\n const verify = fs.readFileSync(filePath, 'utf-8')\n if (!verify.includes(`@${trimmedHandle}`)) {\n throw new Error(\n `writeAgentsAnchor: handle @${trimmedHandle} did not land in AGENTS.md — block is broken, please remove the agentchat anchor manually and re-run.`,\n )\n }\n\n return { path: filePath }\n}\n\n/**\n * Idempotent remove. Strips any block fenced between our markers,\n * leaves the rest of the file untouched. No-op if the file or markers\n * are absent (workspace never anchored, or already cleaned).\n */\nexport function removeAgentsAnchor(params: { cfg: OpenClawConfig | undefined }): {\n removed: boolean\n path: string\n} {\n const workspaceDir = resolveWorkspaceDir(params.cfg)\n const filePath = agentsFilePath(workspaceDir)\n\n if (!fs.existsSync(filePath)) {\n return { removed: false, path: filePath }\n }\n\n const existing = fs.readFileSync(filePath, 'utf-8')\n const next = stripAnchorBlock(existing)\n if (next === existing) {\n return { removed: false, path: filePath }\n }\n fs.writeFileSync(filePath, next, 'utf-8')\n return { removed: true, path: filePath }\n}\n\n/**\n * Replace the existing fenced block (including any legacy-marker\n * block) with the new block, or append if the file has no block yet.\n * Trims surrounding newlines so re-runs don't accumulate blank lines.\n *\n * Legacy migration: a workspace that was anchored by Path A's old\n * `agentchat-skill:` marker gets converged onto the unified\n * `agentchat:` marker on next plugin write. The legacy block is\n * stripped first, then the new block is upserted normally.\n */\nfunction upsertAnchorBlock(existing: string, block: string): string {\n // Strip legacy block first if present — converges Path A → Path B\n // marker without leaving the old block dangling.\n const cleaned = stripBlockBetween(existing, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END)\n\n const startIdx = cleaned.indexOf(ANCHOR_START)\n const endIdx = cleaned.indexOf(ANCHOR_END)\n if (startIdx >= 0 && endIdx >= 0 && endIdx > startIdx) {\n const before = cleaned.slice(0, startIdx).replace(/\\n+$/, '')\n const after = cleaned.slice(endIdx + ANCHOR_END.length).replace(/^\\n+/, '')\n const parts = [before, block, after].filter((s) => s.length > 0)\n return parts.join('\\n\\n') + '\\n'\n }\n\n const trimmed = cleaned.replace(/\\n+$/, '')\n if (trimmed.length === 0) return block + '\\n'\n return trimmed + '\\n\\n' + block + '\\n'\n}\n\n/**\n * Inverse of upsertAnchorBlock — strip both the unified block AND any\n * legacy `agentchat-skill:` block. `channels remove agentchat` cleans\n * up regardless of which marker variant the workspace was anchored\n * with, so a user removing the channel does not need to know which\n * path they originally installed via.\n */\nfunction stripAnchorBlock(existing: string): string {\n const afterUnified = stripBlockBetween(existing, ANCHOR_START, ANCHOR_END)\n return stripBlockBetween(afterUnified, LEGACY_ANCHOR_START, LEGACY_ANCHOR_END)\n}\n\n/**\n * Single-pair strip helper. Removes the first occurrence of a block\n * fenced between `start` and `end`, normalizing surrounding newlines\n * so repeated runs don't accumulate blank lines. No-op if either\n * marker is absent or out of order.\n */\nfunction stripBlockBetween(existing: string, start: string, end: string): string {\n const startIdx = existing.indexOf(start)\n const endIdx = existing.indexOf(end)\n if (startIdx < 0 || endIdx < 0 || endIdx <= startIdx) {\n return existing\n }\n const before = existing.slice(0, startIdx).replace(/\\n+$/, '')\n const after = existing.slice(endIdx + end.length).replace(/^\\n+/, '')\n if (before.length === 0 && after.length === 0) return ''\n if (before.length === 0) return after.endsWith('\\n') ? after : after + '\\n'\n if (after.length === 0) return before + '\\n'\n return before + '\\n\\n' + after + (after.endsWith('\\n') ? '' : '\\n')\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/configured-state.ts"],"names":[],"mappings":";;;AAmBO,SAAS,4BAA4B,MAAA,EAA8C;AACxF,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,KAAA;AAClD,EAAA,MAAM,MAAM,MAAA,CAAO,MAAA;AACnB,EAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,MAAA,GAAS,IAAI,OAAO,KAAA;AACvD,EAAA,MAAM,SAAS,MAAA,CAAO,WAAA;AACtB,EAAA,IAAI,OAAO,WAAW,QAAA,IAAY,MAAA,CAAO,MAAK,CAAE,MAAA,KAAW,GAAG,OAAO,KAAA;AACrE,EAAA,OAAO,IAAA;AACT","file":"configured-state.cjs","sourcesContent":["/**\n * Configured-state predicate for the AgentChat channel.\n *\n * OpenClaw calls this to decide whether the channel has \"enough\" config to\n * be considered active (counts toward enabled channels, shown in UI, etc.).\n *\n * Bar: an `apiKey` of plausible length AND a non-empty `agentHandle`.\n * Without the handle the runtime will start but the agent has no identity\n * to
|
|
1
|
+
{"version":3,"sources":["../src/configured-state.ts"],"names":[],"mappings":";;;AAmBO,SAAS,4BAA4B,MAAA,EAA8C;AACxF,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,KAAA;AAClD,EAAA,MAAM,MAAM,MAAA,CAAO,MAAA;AACnB,EAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,MAAA,GAAS,IAAI,OAAO,KAAA;AACvD,EAAA,MAAM,SAAS,MAAA,CAAO,WAAA;AACtB,EAAA,IAAI,OAAO,WAAW,QAAA,IAAY,MAAA,CAAO,MAAK,CAAE,MAAA,KAAW,GAAG,OAAO,KAAA;AACrE,EAAA,OAAO,IAAA;AACT","file":"configured-state.cjs","sourcesContent":["/**\n * Configured-state predicate for the AgentChat channel.\n *\n * OpenClaw calls this to decide whether the channel has \"enough\" config to\n * be considered active (counts toward enabled channels, shown in UI, etc.).\n *\n * Bar: an `apiKey` of plausible length AND a non-empty `agentHandle`.\n * Without the handle the runtime will start but the agent has no identity\n * to add to prompts or to use as a self-filter on inbound — both\n * downstream surfaces silently degrade. Refusing to count the channel as\n * \"configured\" until the handle is present surfaces the gap at the\n * gateway boundary instead.\n */\n\nexport interface MaybeConfigured {\n readonly apiKey?: unknown\n readonly agentHandle?: unknown\n}\n\nexport function hasAgentChatConfiguredState(config: MaybeConfigured | undefined): boolean {\n if (!config || typeof config !== 'object') return false\n const key = config.apiKey\n if (typeof key !== 'string' || key.length < 20) return false\n const handle = config.agentHandle\n if (typeof handle !== 'string' || handle.trim().length === 0) return false\n return true\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/configured-state.ts"],"names":[],"mappings":";AAmBO,SAAS,4BAA4B,MAAA,EAA8C;AACxF,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,KAAA;AAClD,EAAA,MAAM,MAAM,MAAA,CAAO,MAAA;AACnB,EAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,MAAA,GAAS,IAAI,OAAO,KAAA;AACvD,EAAA,MAAM,SAAS,MAAA,CAAO,WAAA;AACtB,EAAA,IAAI,OAAO,WAAW,QAAA,IAAY,MAAA,CAAO,MAAK,CAAE,MAAA,KAAW,GAAG,OAAO,KAAA;AACrE,EAAA,OAAO,IAAA;AACT","file":"configured-state.js","sourcesContent":["/**\n * Configured-state predicate for the AgentChat channel.\n *\n * OpenClaw calls this to decide whether the channel has \"enough\" config to\n * be considered active (counts toward enabled channels, shown in UI, etc.).\n *\n * Bar: an `apiKey` of plausible length AND a non-empty `agentHandle`.\n * Without the handle the runtime will start but the agent has no identity\n * to
|
|
1
|
+
{"version":3,"sources":["../src/configured-state.ts"],"names":[],"mappings":";AAmBO,SAAS,4BAA4B,MAAA,EAA8C;AACxF,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,KAAA;AAClD,EAAA,MAAM,MAAM,MAAA,CAAO,MAAA;AACnB,EAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,MAAA,GAAS,IAAI,OAAO,KAAA;AACvD,EAAA,MAAM,SAAS,MAAA,CAAO,WAAA;AACtB,EAAA,IAAI,OAAO,WAAW,QAAA,IAAY,MAAA,CAAO,MAAK,CAAE,MAAA,KAAW,GAAG,OAAO,KAAA;AACrE,EAAA,OAAO,IAAA;AACT","file":"configured-state.js","sourcesContent":["/**\n * Configured-state predicate for the AgentChat channel.\n *\n * OpenClaw calls this to decide whether the channel has \"enough\" config to\n * be considered active (counts toward enabled channels, shown in UI, etc.).\n *\n * Bar: an `apiKey` of plausible length AND a non-empty `agentHandle`.\n * Without the handle the runtime will start but the agent has no identity\n * to add to prompts or to use as a self-filter on inbound — both\n * downstream surfaces silently degrade. Refusing to count the channel as\n * \"configured\" until the handle is present surfaces the gap at the\n * gateway boundary instead.\n */\n\nexport interface MaybeConfigured {\n readonly apiKey?: unknown\n readonly agentHandle?: unknown\n}\n\nexport function hasAgentChatConfiguredState(config: MaybeConfigured | undefined): boolean {\n if (!config || typeof config !== 'object') return false\n const key = config.apiKey\n if (typeof key !== 'string' || key.length < 20) return false\n const handle = config.agentHandle\n if (typeof handle !== 'string' || handle.trim().length === 0) return false\n return true\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/credentials/read-env.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"sources":["../../src/credentials/read-env.ts"],"names":[],"mappings":";;;AAqBO,SAAS,kBAAkB,SAAA,EAAuC;AACvE,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,CAAI,iBAAA,EAAmB,IAAA,EAAK;AAChD,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,IAAI,GAAA,CAAI,MAAA,GAAS,SAAA,EAAW,OAAO,MAAA;AACnC,EAAA,OAAO,GAAA;AACT;AAWO,SAAS,0BAAA,GAAiD;AAC/D,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,CAAI,gBAAA,EAAkB,IAAA,EAAK;AAC/C,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,IAAI,GAAA,CAAI,WAAA,EAAY,KAAM,SAAA,EAAW,OAAO,MAAA;AAC5C,EAAA,OAAO,GAAA;AACT","file":"read-env.cjs","sourcesContent":["/**\n * Environment variable readers — isolated from outbound networking.\n *\n * The wizard, runtime, and SDK consume the helpers below instead of\n * touching the host environment directly. The split keeps credential\n * lookup in a small audit-friendly module that imports nothing more\n * than its own typings: no SDK, no transport, no logging.\n *\n * See SECURITY.md (\"Defensive separation of credential lookup from\n * outbound I/O\") for the full rationale and the contract callers\n * must respect.\n */\n\n/**\n * Reads the AgentChat API key from `AGENTCHAT_API_KEY`.\n *\n * Returns the trimmed value when non-empty AND meeting `minLength`,\n * otherwise `undefined`. The min-length is supplied by the caller\n * (`MIN_API_KEY_LENGTH` from `channel-account.ts`) so this module\n * owns no domain constants.\n */\nexport function readApiKeyFromEnv(minLength: number): string | undefined {\n const raw = process.env.AGENTCHAT_API_KEY?.trim()\n if (!raw) return undefined\n if (raw.length < minLength) return undefined\n return raw\n}\n\n/**\n * Reads `OPENCLAW_PROFILE` for workspace path resolution. Mirrors\n * OpenClaw's own logic in `dist/workspace-hhTlRYqM.js:49-55`:\n * non-default profile name → `~/.openclaw/workspace-${profile}`.\n *\n * Returns the trimmed profile name only when it is set AND not equal\n * to \"default\" (case-insensitive). Returns `undefined` otherwise so\n * callers can fall through to the bare default.\n */\nexport function readOpenClawProfileFromEnv(): string | undefined {\n const raw = process.env.OPENCLAW_PROFILE?.trim()\n if (!raw) return undefined\n if (raw.toLowerCase() === 'default') return undefined\n return raw\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/credentials/read-env.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/credentials/read-env.ts"],"names":[],"mappings":";AAqBO,SAAS,kBAAkB,SAAA,EAAuC;AACvE,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,CAAI,iBAAA,EAAmB,IAAA,EAAK;AAChD,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,IAAI,GAAA,CAAI,MAAA,GAAS,SAAA,EAAW,OAAO,MAAA;AACnC,EAAA,OAAO,GAAA;AACT;AAWO,SAAS,0BAAA,GAAiD;AAC/D,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,CAAI,gBAAA,EAAkB,IAAA,EAAK;AAC/C,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,IAAI,GAAA,CAAI,WAAA,EAAY,KAAM,SAAA,EAAW,OAAO,MAAA;AAC5C,EAAA,OAAO,GAAA;AACT","file":"read-env.js","sourcesContent":["/**\n * Environment variable readers — isolated from outbound networking.\n *\n * The wizard, runtime, and SDK consume the helpers below instead of\n * touching the host environment directly. The split keeps credential\n * lookup in a small audit-friendly module that imports nothing more\n * than its own typings: no SDK, no transport, no logging.\n *\n * See SECURITY.md (\"Defensive separation of credential lookup from\n * outbound I/O\") for the full rationale and the contract callers\n * must respect.\n */\n\n/**\n * Reads the AgentChat API key from `AGENTCHAT_API_KEY`.\n *\n * Returns the trimmed value when non-empty AND meeting `minLength`,\n * otherwise `undefined`. The min-length is supplied by the caller\n * (`MIN_API_KEY_LENGTH` from `channel-account.ts`) so this module\n * owns no domain constants.\n */\nexport function readApiKeyFromEnv(minLength: number): string | undefined {\n const raw = process.env.AGENTCHAT_API_KEY?.trim()\n if (!raw) return undefined\n if (raw.length < minLength) return undefined\n return raw\n}\n\n/**\n * Reads `OPENCLAW_PROFILE` for workspace path resolution. Mirrors\n * OpenClaw's own logic in `dist/workspace-hhTlRYqM.js:49-55`:\n * non-default profile name → `~/.openclaw/workspace-${profile}`.\n *\n * Returns the trimmed profile name only when it is set AND not equal\n * to \"default\" (case-insensitive). Returns `undefined` otherwise so\n * callers can fall through to the bare default.\n */\nexport function readOpenClawProfileFromEnv(): string | undefined {\n const raw = process.env.OPENCLAW_PROFILE?.trim()\n if (!raw) return undefined\n if (raw.toLowerCase() === 'default') return undefined\n return raw\n}\n"]}
|