@clawmem-ai/clawmem 0.1.10 → 0.1.12
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/README.md +31 -179
- package/openclaw.plugin.json +4 -1
- package/package.json +2 -1
- package/skills/clawmem/SKILL.md +79 -0
- package/skills/clawmem/references/collaboration.md +223 -0
- package/skills/clawmem/references/communication.md +60 -0
- package/skills/clawmem/references/manual-ops.md +205 -0
- package/skills/clawmem/references/repair.md +127 -0
- package/skills/clawmem/references/schema.md +63 -0
- package/skills/clawmem/scripts/clawmem_exports.py +54 -0
- package/src/github-client.ts +180 -1
- package/src/service.ts +1043 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# ClawMem Manual Operations And Troubleshooting
|
|
2
|
+
|
|
3
|
+
Use this reference only when:
|
|
4
|
+
- the user explicitly wants raw GitHub-style repo or issue operations
|
|
5
|
+
- you are debugging backend state or labels
|
|
6
|
+
- the plugin memory tools are unavailable
|
|
7
|
+
|
|
8
|
+
ClawMem runs on a GitHub-compatible backend, so repo, issue, label, invitation, and team operations follow GitHub-shaped APIs. That is why `gh` and `curl` are valid fallback tools here.
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
|
|
12
|
+
- Route resolution
|
|
13
|
+
- Preflight
|
|
14
|
+
- Save a memory manually
|
|
15
|
+
- Search memories manually
|
|
16
|
+
- Mark memory as stale manually
|
|
17
|
+
- Link related memories manually
|
|
18
|
+
- `git push` to ClawMem
|
|
19
|
+
- Known pitfalls
|
|
20
|
+
- Autonomy
|
|
21
|
+
|
|
22
|
+
If the plugin tools are available, prefer:
|
|
23
|
+
- `memory_repos` to inspect available repos
|
|
24
|
+
- `memory_list` to inspect the current active-memory index
|
|
25
|
+
- `memory_get` to verify one exact memory
|
|
26
|
+
- `memory_labels` to inspect current schema
|
|
27
|
+
- `memory_repo_create` to create a new memory repo
|
|
28
|
+
- `memory_store` to save
|
|
29
|
+
- `memory_update` to evolve one canonical memory in place
|
|
30
|
+
- `memory_recall` to search
|
|
31
|
+
- `memory_forget` to retire stale memories
|
|
32
|
+
|
|
33
|
+
## Route resolution
|
|
34
|
+
|
|
35
|
+
ClawMem is routed per agent identity, not through one global repo or token.
|
|
36
|
+
|
|
37
|
+
Resolve the current route with the bundled helper:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
eval "$(python3 scripts/clawmem_exports.py)"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That exports:
|
|
44
|
+
- `CLAWMEM_AGENT_ID`
|
|
45
|
+
- `CLAWMEM_BASE_URL`
|
|
46
|
+
- `CLAWMEM_HOST`
|
|
47
|
+
- `CLAWMEM_DEFAULT_REPO`
|
|
48
|
+
- `CLAWMEM_REPO`
|
|
49
|
+
- `CLAWMEM_TOKEN`
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
- Never store tokens in files or chat
|
|
53
|
+
- `CLAWMEM_DEFAULT_REPO` is the fallback memory space for automatic flows
|
|
54
|
+
- `CLAWMEM_REPO` is the repo chosen for the current operation
|
|
55
|
+
- If `CLAWMEM_TOKEN` is empty, this agent identity has not been provisioned yet
|
|
56
|
+
|
|
57
|
+
## Preflight
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
eval "$(python3 scripts/clawmem_exports.py)"
|
|
61
|
+
|
|
62
|
+
test -n "$CLAWMEM_REPO" || { echo "ClawMem repo missing for agent $CLAWMEM_AGENT_ID"; exit 1; }
|
|
63
|
+
test -n "$CLAWMEM_TOKEN" || { echo "ClawMem token missing for agent $CLAWMEM_AGENT_ID"; exit 1; }
|
|
64
|
+
case "$CLAWMEM_REPO" in
|
|
65
|
+
*/*) ;;
|
|
66
|
+
*) echo "Invalid CLAWMEM_REPO='$CLAWMEM_REPO' (expected owner/repo)"; exit 1 ;;
|
|
67
|
+
esac
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For ClawMem, always pass `--repo "$CLAWMEM_REPO"` to `gh` or use `$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/...` with `curl` explicitly.
|
|
71
|
+
|
|
72
|
+
Do not export `GH_HOST` or `GH_ENTERPRISE_TOKEN` globally for unrelated github.com work. Use per-command env prefixes if you need isolation.
|
|
73
|
+
|
|
74
|
+
## Save a memory manually
|
|
75
|
+
|
|
76
|
+
Use the tool path first. If raw issue control is required:
|
|
77
|
+
|
|
78
|
+
### With `gh`
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
for lbl in "type:memory" "kind:core-fact" "kind:convention" "kind:lesson" "kind:skill" "kind:task"; do
|
|
82
|
+
GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
|
|
83
|
+
gh label create "$lbl" --repo "$CLAWMEM_REPO" --color "5319e7" 2>/dev/null || true
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
|
|
87
|
+
gh issue create --repo "$CLAWMEM_REPO" \
|
|
88
|
+
--title "Memory: <concise title>" \
|
|
89
|
+
--body "<durable detail in plain language>" \
|
|
90
|
+
--label "type:memory" \
|
|
91
|
+
--label "kind:lesson"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### With `curl`
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
for lbl in "type:memory" "kind:core-fact" "kind:convention" "kind:lesson" "kind:skill" "kind:task"; do
|
|
98
|
+
curl -sf -X POST -H "Authorization: token $CLAWMEM_TOKEN" \
|
|
99
|
+
-H "Content-Type: application/json" \
|
|
100
|
+
"$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/labels" \
|
|
101
|
+
-d "{\"name\":\"$lbl\",\"color\":\"5319e7\"}" >/dev/null 2>&1 || true
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
curl -sf -X POST -H "Authorization: token $CLAWMEM_TOKEN" \
|
|
105
|
+
-H "Content-Type: application/json" \
|
|
106
|
+
"$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues" \
|
|
107
|
+
-d "{
|
|
108
|
+
\"title\": \"Memory: <concise title>\",
|
|
109
|
+
\"body\": \"<durable detail in plain language>\",
|
|
110
|
+
\"labels\": [\"type:memory\", \"kind:lesson\"]
|
|
111
|
+
}" | jq '{number, title, url: .html_url}'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Search memories manually
|
|
115
|
+
|
|
116
|
+
### With `gh`
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
|
|
120
|
+
gh issue list --repo "$CLAWMEM_REPO" \
|
|
121
|
+
--state open \
|
|
122
|
+
--label "type:memory" \
|
|
123
|
+
--search "<keywords>" \
|
|
124
|
+
--limit 100 \
|
|
125
|
+
--json number,title,body,labels,updatedAt
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### With `curl`
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
curl -sf -H "Authorization: token $CLAWMEM_TOKEN" \
|
|
132
|
+
"$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues?state=open&labels=type:memory&per_page=100&type=issues" | \
|
|
133
|
+
jq --arg q "<keywords>" '
|
|
134
|
+
($q | ascii_downcase) as $needle
|
|
135
|
+
| map(select(
|
|
136
|
+
((.title // "") + "\n" + (.body // "")) | ascii_downcase | contains($needle)
|
|
137
|
+
))
|
|
138
|
+
| map({number, title, body, labels: [.labels[]?.name], updatedAt: .updated_at})
|
|
139
|
+
'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Mark memory as stale manually
|
|
143
|
+
|
|
144
|
+
If this is still the same canonical fact or task, prefer `memory_update` instead of retiring the old node.
|
|
145
|
+
|
|
146
|
+
### With `gh`
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
|
|
150
|
+
gh issue close <number> --repo "$CLAWMEM_REPO"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### With `curl`
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
curl -sf -X PATCH -H "Authorization: token $CLAWMEM_TOKEN" \
|
|
157
|
+
-H "Content-Type: application/json" \
|
|
158
|
+
"$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues/<number>" \
|
|
159
|
+
-d '{"state": "closed"}'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
If a new memory replaces an old one, save the new memory first and mention the old `#ID` in the replacement body so the supersession is explicit.
|
|
163
|
+
|
|
164
|
+
## Link related memories manually
|
|
165
|
+
|
|
166
|
+
When one memory depends on, refines, or supersedes another, mention `#<id>` in the body so the graph keeps an explicit edge.
|
|
167
|
+
|
|
168
|
+
Prefer doing this when you create a curated raw memory, or when you are already rewriting the full issue body intentionally.
|
|
169
|
+
|
|
170
|
+
If you patch an existing plugin-managed memory body by hand, preserve the existing structured body and add the `#<id>` relation into the durable detail instead of overwriting metadata fields blindly.
|
|
171
|
+
|
|
172
|
+
## `git push` to ClawMem
|
|
173
|
+
|
|
174
|
+
`GH_HOST` and `GH_ENTERPRISE_TOKEN` affect `gh`, not `git push`. If you need to push code to a ClawMem git service repo:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
echo "$CLAWMEM_TOKEN" | gh auth login -h "$CLAWMEM_HOST" --with-token
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
After that, `git push` to `https://git.clawmem.ai/...` works normally.
|
|
181
|
+
|
|
182
|
+
## Known pitfalls
|
|
183
|
+
|
|
184
|
+
| Problem | Fix |
|
|
185
|
+
|---|---|
|
|
186
|
+
| Labels do not update reliably via `PATCH` on some backends | Use `PUT /repos/{owner}/{repo}/issues/{n}/labels` when you need exact label replacement |
|
|
187
|
+
| `openclaw config get` returns redacted token values | Read the config file path via `openclaw config file`, then inspect the JSON directly |
|
|
188
|
+
| Conversation mirror returns `404` | The cached conversation issue was deleted; the plugin recreates it on the next session |
|
|
189
|
+
| New session gets `401 Unauthorized` | Re-read the current agent route. If this is first use, trigger one real turn so the plugin can finish provisioning |
|
|
190
|
+
| Agent uses the wrong memory repo | Resolve `config.agents.<agentId>` for the current agent; do not read only top-level legacy repo settings |
|
|
191
|
+
| `gh` is not the official GitHub CLI | Run `gh --version`; if it is the npm `gh` package instead of the official CLI, use `curl` or replace the CLI install |
|
|
192
|
+
|
|
193
|
+
## Autonomy
|
|
194
|
+
|
|
195
|
+
Without confirmation:
|
|
196
|
+
- creating or updating memory nodes
|
|
197
|
+
- adding comments
|
|
198
|
+
- reusing or creating labels
|
|
199
|
+
- closing stale memory nodes
|
|
200
|
+
- creating new memory repos when a new space is clearly needed
|
|
201
|
+
|
|
202
|
+
Requires confirmation:
|
|
203
|
+
- OpenClaw config changes
|
|
204
|
+
- service restarts
|
|
205
|
+
- deletions that go beyond ordinary memory retirement
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# ClawMem Repair And Verification
|
|
2
|
+
|
|
3
|
+
Use this reference when ClawMem is already installed but is not selected as the active memory plugin, is missing per-agent provisioning, has a broken route, or needs verification after setup.
|
|
4
|
+
|
|
5
|
+
The website bootstrap `SKILL.md` is the primary setup guide. This reference is for post-install repair, diagnostics, and compatibility-file reminders.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- Verify activation and provisioning
|
|
10
|
+
- Verify read access without manual login
|
|
11
|
+
- Verify the plugin tool path
|
|
12
|
+
- Compatibility mode for SOUL.md, AGENTS.md, and TOOLS.md
|
|
13
|
+
- Definition of done
|
|
14
|
+
- If ClawMem is still broken
|
|
15
|
+
|
|
16
|
+
## Step 1: Verify activation and provisioning
|
|
17
|
+
|
|
18
|
+
First verify that ClawMem is the active memory plugin.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
openclaw status
|
|
22
|
+
python3 - <<'PY'
|
|
23
|
+
import json, os, subprocess
|
|
24
|
+
cfg_path = subprocess.check_output(["openclaw", "config", "file"], text=True).strip()
|
|
25
|
+
with open(os.path.expanduser(cfg_path)) as f:
|
|
26
|
+
root = json.load(f)
|
|
27
|
+
slots = (root.get("plugins") or {}).get("slots") or {}
|
|
28
|
+
print(f"plugins.slots.memory = {slots.get('memory', 'MISSING')}")
|
|
29
|
+
PY
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Expected:
|
|
33
|
+
- OpenClaw status shows ClawMem as the active memory plugin
|
|
34
|
+
- `plugins.slots.memory = clawmem`
|
|
35
|
+
|
|
36
|
+
Then verify the current agent route. Resolve the current route with the bundled helper:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
eval "$(python3 scripts/clawmem_exports.py)"
|
|
40
|
+
printf 'agent=%s\nbase=%s\ndefaultRepo=%s\ntoken=%s\n' \
|
|
41
|
+
"${CLAWMEM_AGENT_ID}" "${CLAWMEM_BASE_URL}" "${CLAWMEM_DEFAULT_REPO}" \
|
|
42
|
+
"$(test -n "${CLAWMEM_TOKEN}" && printf SET || printf MISSING)"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If `CLAWMEM_DEFAULT_REPO` or `CLAWMEM_TOKEN` is missing, the current agent has not been provisioned yet. Trigger one real turn with that agent so the plugin can finish provisioning and persist credentials, or restart OpenClaw and retry after the agent is first used.
|
|
46
|
+
|
|
47
|
+
## Step 2: Verify read access without manual login
|
|
48
|
+
|
|
49
|
+
This proves that a fresh session can query ClawMem using the current agent's provisioned route.
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
eval "$(python3 scripts/clawmem_exports.py)"
|
|
53
|
+
|
|
54
|
+
test -n "$CLAWMEM_REPO" || { echo "Current agent route has no repo yet"; exit 1; }
|
|
55
|
+
test -n "$CLAWMEM_TOKEN" || { echo "Current agent route has no token yet"; exit 1; }
|
|
56
|
+
|
|
57
|
+
GH_HOST="$CLAWMEM_HOST" GH_ENTERPRISE_TOKEN="$CLAWMEM_TOKEN" \
|
|
58
|
+
gh issue list --repo "$CLAWMEM_REPO" --limit 1 --json number,title
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If `gh` is unavailable or not the official GitHub CLI, use the fallback probe:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
curl -sf -H "Authorization: token $CLAWMEM_TOKEN" \
|
|
65
|
+
"$CLAWMEM_BASE_URL/repos/$CLAWMEM_REPO/issues?state=open&per_page=1&type=issues" | \
|
|
66
|
+
jq 'map({number,title})'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If either command returns JSON, even `[]`, the route is usable.
|
|
70
|
+
|
|
71
|
+
## Step 3: Verify the plugin tool path
|
|
72
|
+
|
|
73
|
+
From a normal ClawMem-enabled session, verify that:
|
|
74
|
+
- `memory_repos` lists accessible repos and marks the default repo
|
|
75
|
+
- `memory_list` returns the active memory index
|
|
76
|
+
- `memory_get` fetches one exact memory by id or issue number
|
|
77
|
+
- `memory_labels` returns the current reusable schema labels
|
|
78
|
+
- `memory_recall` returns either a hit list or a clean miss
|
|
79
|
+
- `memory_store` is available for immediate durable saves
|
|
80
|
+
- `memory_update` updates an existing memory in place
|
|
81
|
+
- `memory_repo_create` creates a new repo when a new memory space is needed
|
|
82
|
+
|
|
83
|
+
Conversation summaries or auto-extracted memories from a just-finished session may appear on the next real request, not necessarily immediately at session close.
|
|
84
|
+
|
|
85
|
+
## Compatibility mode for SOUL.md, AGENTS.md, and TOOLS.md
|
|
86
|
+
|
|
87
|
+
If your OpenClaw environment still relies on file-injected identity or behavior reminders, use these compact compatibility snippets. Do not duplicate the entire skill body into those files.
|
|
88
|
+
|
|
89
|
+
### Optional SOUL.md identity block
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
## Memory System — ClawMem
|
|
93
|
+
I use ClawMem as my memory system.
|
|
94
|
+
When prior context may help, I search ClawMem before answering.
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Optional AGENTS.md reminder
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
Before ending every response, ask: "Did I learn anything durable this turn?"
|
|
101
|
+
If yes or unsure, save it to ClawMem now.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Optional TOOLS.md reminder
|
|
105
|
+
|
|
106
|
+
```markdown
|
|
107
|
+
ClawMem is the primary long-term memory system.
|
|
108
|
+
Use the bundled $clawmem skill for retrieval, saving, routing, schema, and troubleshooting.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
These snippets are compatibility aids, not the primary runtime source of truth.
|
|
112
|
+
|
|
113
|
+
## Definition of done
|
|
114
|
+
|
|
115
|
+
- `openclaw.json` has `plugins.slots.memory = clawmem`
|
|
116
|
+
- The current agent route has a `defaultRepo` or legacy `repo`
|
|
117
|
+
- The current agent route has a `token`
|
|
118
|
+
- Read-only probe works without manual `gh auth login`
|
|
119
|
+
- Plugin memory tools work from a normal session
|
|
120
|
+
- The bundled `$clawmem` skill is available after installation
|
|
121
|
+
|
|
122
|
+
## If ClawMem is still broken
|
|
123
|
+
|
|
124
|
+
- If `plugins.slots.memory` is wrong, set it back to `clawmem`, restart the gateway, and retry.
|
|
125
|
+
- If the route is missing a repo or token, trigger one real turn with that agent and retry provisioning checks.
|
|
126
|
+
- If a new session gets `401 Unauthorized`, re-read the current route instead of assuming the old repo or token is still valid.
|
|
127
|
+
- If your environment still depends on `SOUL.md` or `AGENTS.md`, add the compatibility snippets above rather than pasting large sections of this skill into those files.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# ClawMem Memory Schema
|
|
2
|
+
|
|
3
|
+
Use this reference when deciding how to label or shape a memory, or when you need to explain the ClawMem graph model to another agent or user.
|
|
4
|
+
|
|
5
|
+
## The memory graph
|
|
6
|
+
|
|
7
|
+
Issues are nodes. Labels are schema. `#ID` references are edges.
|
|
8
|
+
|
|
9
|
+
When one memory depends on, refines, supersedes, or generalizes another memory, mention the related `#ID` in the issue body so the relationship stays explicit in the graph.
|
|
10
|
+
|
|
11
|
+
There are two valid memory shapes:
|
|
12
|
+
- Plugin-managed structured memories: created through `memory_store` or `memory_update`; the plugin manages core labels and may also persist agent-selected `kind:*` and `topic:*` labels
|
|
13
|
+
- Curated graph memories: created manually through `gh` or `curl` when you explicitly need raw issue control
|
|
14
|
+
|
|
15
|
+
## Labels
|
|
16
|
+
|
|
17
|
+
Plugin-managed memories always include:
|
|
18
|
+
- `type:memory`
|
|
19
|
+
|
|
20
|
+
Plugin-managed memories may also include:
|
|
21
|
+
- one `kind:*` label
|
|
22
|
+
- optional `topic:*` labels
|
|
23
|
+
|
|
24
|
+
Lifecycle is carried by native issue state:
|
|
25
|
+
- open issue = active memory
|
|
26
|
+
- closed issue = stale or superseded memory
|
|
27
|
+
|
|
28
|
+
If you create a curated memory manually, include:
|
|
29
|
+
- `type:memory`
|
|
30
|
+
- one `kind:*` label
|
|
31
|
+
- optional `topic:*` labels, usually no more than two or three
|
|
32
|
+
|
|
33
|
+
## Kinds
|
|
34
|
+
|
|
35
|
+
| Kind | Label | Use it for |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Core fact | `kind:core-fact` | Stable truths that should update in place as reality changes |
|
|
38
|
+
| Convention | `kind:convention` | Agreed rules or policies |
|
|
39
|
+
| Lesson learned | `kind:lesson` | Corrections, postmortems, or mistakes worth preserving |
|
|
40
|
+
| Skill blueprint | `kind:skill` | Repeatable workflows or playbooks |
|
|
41
|
+
| Active task | `kind:task` | Ongoing work that should stay visible and update over time |
|
|
42
|
+
|
|
43
|
+
## When to create which kind
|
|
44
|
+
|
|
45
|
+
| Trigger | Kind |
|
|
46
|
+
|---|---|
|
|
47
|
+
| User corrects a wrong assumption | `kind:lesson` |
|
|
48
|
+
| You and the user agree on a rule | `kind:convention` |
|
|
49
|
+
| A stable fact about the user or project | `kind:core-fact` |
|
|
50
|
+
| A repeatable workflow you figured out | `kind:skill` |
|
|
51
|
+
| Ongoing work to track | `kind:task` |
|
|
52
|
+
|
|
53
|
+
## Disciplined self-evolution
|
|
54
|
+
|
|
55
|
+
- Before inventing a new `kind` or `topic`, call `memory_labels`.
|
|
56
|
+
- Reuse current schema when it already fits.
|
|
57
|
+
- If the current schema does not fit and a new label would help future retrieval, coordination, or reuse, create it deliberately.
|
|
58
|
+
- New labels should be short, general, and likely to apply again across future memories or agents.
|
|
59
|
+
- Do not invent random label prefixes. Schema evolution must stay within `kind:*` and `topic:*`.
|
|
60
|
+
|
|
61
|
+
## Storage rule
|
|
62
|
+
|
|
63
|
+
If you are writing something so the agent remembers it later, store it in ClawMem. If you are writing something for a tool or human to read directly, write a file instead.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_base_url(raw: str) -> str:
|
|
11
|
+
value = (raw or "https://git.clawmem.ai/api/v3").rstrip("/")
|
|
12
|
+
if not value.endswith("/api/v3"):
|
|
13
|
+
value = f"{value}/api/v3"
|
|
14
|
+
return value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
agent_id = (sys.argv[1].strip() if len(sys.argv) > 1 and sys.argv[1].strip() else os.environ.get("OPENCLAW_AGENT_ID", "main"))
|
|
19
|
+
repo_override = sys.argv[2].strip() if len(sys.argv) > 2 else ""
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
cfg_path = subprocess.check_output(["openclaw", "config", "file"], text=True).strip()
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
print("clawmem_exports.py: openclaw CLI was not found in PATH", file=sys.stderr)
|
|
25
|
+
return 1
|
|
26
|
+
with open(os.path.expanduser(cfg_path), "r", encoding="utf-8") as handle:
|
|
27
|
+
root = json.load(handle)
|
|
28
|
+
|
|
29
|
+
cfg = (((root.get("plugins") or {}).get("entries") or {}).get("clawmem") or {}).get("config") or {}
|
|
30
|
+
agents = cfg.get("agents") or {}
|
|
31
|
+
route = agents.get(agent_id) or {}
|
|
32
|
+
|
|
33
|
+
base_url = normalize_base_url(route.get("baseUrl") or cfg.get("baseUrl") or "")
|
|
34
|
+
default_repo = route.get("defaultRepo") or route.get("repo") or cfg.get("defaultRepo") or cfg.get("repo") or ""
|
|
35
|
+
repo = repo_override or default_repo
|
|
36
|
+
token = route.get("token") or ""
|
|
37
|
+
host = base_url.removesuffix("/api/v3").replace("https://", "").replace("http://", "")
|
|
38
|
+
|
|
39
|
+
pairs = {
|
|
40
|
+
"CLAWMEM_AGENT_ID": agent_id,
|
|
41
|
+
"CLAWMEM_BASE_URL": base_url,
|
|
42
|
+
"CLAWMEM_HOST": host,
|
|
43
|
+
"CLAWMEM_DEFAULT_REPO": default_repo,
|
|
44
|
+
"CLAWMEM_REPO": repo,
|
|
45
|
+
"CLAWMEM_TOKEN": token,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for key, value in pairs.items():
|
|
49
|
+
print(f"export {key}={shlex.quote(value)}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
raise SystemExit(main())
|
package/src/github-client.ts
CHANGED
|
@@ -6,7 +6,62 @@ type IssueResponse = { number: number; title?: string; body?: string; state?: st
|
|
|
6
6
|
type SearchIssuesResponse = { items?: IssueResponse[]; total_count?: number; incomplete_results?: boolean };
|
|
7
7
|
type CommentResponse = { id?: number; body?: string; created_at?: string };
|
|
8
8
|
type LabelResponse = { name?: string; color?: string; description?: string };
|
|
9
|
-
type
|
|
9
|
+
type PermissionMap = Record<string, boolean | undefined>;
|
|
10
|
+
type RepoResponse = {
|
|
11
|
+
name?: string;
|
|
12
|
+
full_name?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
private?: boolean;
|
|
15
|
+
owner?: { login?: string };
|
|
16
|
+
permissions?: PermissionMap;
|
|
17
|
+
role_name?: string;
|
|
18
|
+
};
|
|
19
|
+
type OrgResponse = {
|
|
20
|
+
id?: number;
|
|
21
|
+
login?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
default_repository_permission?: string;
|
|
25
|
+
};
|
|
26
|
+
type TeamResponse = {
|
|
27
|
+
id?: number;
|
|
28
|
+
slug?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
privacy?: string;
|
|
32
|
+
permission?: string;
|
|
33
|
+
role_name?: string;
|
|
34
|
+
permissions?: PermissionMap;
|
|
35
|
+
};
|
|
36
|
+
type CollaboratorResponse = {
|
|
37
|
+
id?: number;
|
|
38
|
+
login?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
permissions?: PermissionMap;
|
|
41
|
+
role_name?: string;
|
|
42
|
+
};
|
|
43
|
+
type RepositoryInvitationResponse = {
|
|
44
|
+
id?: number;
|
|
45
|
+
created_at?: string;
|
|
46
|
+
permissions?: string;
|
|
47
|
+
repository?: RepoResponse;
|
|
48
|
+
invitee?: { login?: string; name?: string };
|
|
49
|
+
inviter?: { login?: string; name?: string };
|
|
50
|
+
};
|
|
51
|
+
type TeamMembershipResponse = { state?: string; role?: string };
|
|
52
|
+
type InvitationResponse = {
|
|
53
|
+
id?: number;
|
|
54
|
+
role?: string;
|
|
55
|
+
created_at?: string;
|
|
56
|
+
expires_at?: string | null;
|
|
57
|
+
email?: string;
|
|
58
|
+
login?: string;
|
|
59
|
+
organization?: OrgResponse;
|
|
60
|
+
invitee?: { login?: string };
|
|
61
|
+
inviter?: { login?: string };
|
|
62
|
+
team_ids?: number[];
|
|
63
|
+
teams?: TeamResponse[];
|
|
64
|
+
};
|
|
10
65
|
type ReqOpts = { allowNotFound?: boolean; allowValidationError?: boolean; omitAuth?: boolean };
|
|
11
66
|
|
|
12
67
|
export class GitHubIssueClient {
|
|
@@ -71,6 +126,130 @@ export class GitHubIssueClient {
|
|
|
71
126
|
}),
|
|
72
127
|
});
|
|
73
128
|
}
|
|
129
|
+
async listUserOrgs(): Promise<OrgResponse[]> {
|
|
130
|
+
return this.req<OrgResponse[]>("user/orgs", { method: "GET" });
|
|
131
|
+
}
|
|
132
|
+
async createUserOrg(params: { login: string; name?: string; defaultRepositoryPermission?: string }): Promise<OrgResponse> {
|
|
133
|
+
return this.req<OrgResponse>("user/orgs", {
|
|
134
|
+
method: "POST",
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
login: params.login,
|
|
137
|
+
...(params.name ? { name: params.name } : {}),
|
|
138
|
+
...(params.defaultRepositoryPermission ? { default_repository_permission: params.defaultRepositoryPermission } : {}),
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async getOrg(org: string): Promise<OrgResponse> {
|
|
143
|
+
return this.req<OrgResponse>(`orgs/${encodeURIComponent(org)}`, { method: "GET" });
|
|
144
|
+
}
|
|
145
|
+
async listOrgTeams(org: string): Promise<TeamResponse[]> {
|
|
146
|
+
return this.req<TeamResponse[]>(`orgs/${encodeURIComponent(org)}/teams`, { method: "GET" });
|
|
147
|
+
}
|
|
148
|
+
async createOrgTeam(org: string, params: { name: string; description?: string; privacy?: "closed" | "secret" }): Promise<TeamResponse> {
|
|
149
|
+
return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
name: params.name,
|
|
153
|
+
...(params.description ? { description: params.description } : {}),
|
|
154
|
+
privacy: params.privacy ?? "closed",
|
|
155
|
+
}),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async setTeamMembership(org: string, teamSlug: string, username: string, role: "member" | "maintainer"): Promise<TeamMembershipResponse> {
|
|
159
|
+
return this.req<TeamMembershipResponse>(
|
|
160
|
+
`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
|
|
161
|
+
{ method: "PUT", body: JSON.stringify({ role }) },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
async removeTeamMembership(org: string, teamSlug: string, username: string): Promise<void> {
|
|
165
|
+
await this.req(
|
|
166
|
+
`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
|
|
167
|
+
{ method: "DELETE" },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
async listTeamRepos(org: string, teamSlug: string): Promise<RepoResponse[]> {
|
|
171
|
+
return this.req<RepoResponse[]>(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos`, { method: "GET" });
|
|
172
|
+
}
|
|
173
|
+
async setTeamRepoAccess(org: string, teamSlug: string, owner: string, repo: string, permission: "read" | "write" | "admin"): Promise<void> {
|
|
174
|
+
await this.req(
|
|
175
|
+
`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
|
|
176
|
+
{ method: "PUT", body: JSON.stringify({ permission }) },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
async removeTeamRepoAccess(org: string, teamSlug: string, owner: string, repo: string): Promise<void> {
|
|
180
|
+
await this.req(
|
|
181
|
+
`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`,
|
|
182
|
+
{ method: "DELETE" },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
async listRepoCollaborators(owner: string, repo: string): Promise<CollaboratorResponse[]> {
|
|
186
|
+
return this.req<CollaboratorResponse[]>(
|
|
187
|
+
`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators`,
|
|
188
|
+
{ method: "GET" },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
async listRepoInvitations(owner: string, repo: string): Promise<RepositoryInvitationResponse[]> {
|
|
192
|
+
return this.req<RepositoryInvitationResponse[]>(
|
|
193
|
+
`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/invitations`,
|
|
194
|
+
{ method: "GET" },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
async setRepoCollaborator(owner: string, repo: string, username: string, permission: "read" | "write" | "admin"): Promise<RepositoryInvitationResponse | undefined> {
|
|
198
|
+
return this.req<RepositoryInvitationResponse>(
|
|
199
|
+
`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
|
|
200
|
+
{ method: "PUT", body: JSON.stringify({ permission }) },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
async removeRepoCollaborator(owner: string, repo: string, username: string): Promise<void> {
|
|
204
|
+
await this.req(
|
|
205
|
+
`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
|
|
206
|
+
{ method: "DELETE" },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
async getRepo(owner: string, repo: string): Promise<RepoResponse> {
|
|
210
|
+
return this.req<RepoResponse>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { method: "GET" });
|
|
211
|
+
}
|
|
212
|
+
async listRepoTeams(owner: string, repo: string): Promise<TeamResponse[]> {
|
|
213
|
+
return this.req<TeamResponse[]>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/teams`, { method: "GET" });
|
|
214
|
+
}
|
|
215
|
+
async listUserRepoInvitations(): Promise<RepositoryInvitationResponse[]> {
|
|
216
|
+
return this.req<RepositoryInvitationResponse[]>("user/repository_invitations", { method: "GET" });
|
|
217
|
+
}
|
|
218
|
+
async acceptUserRepoInvitation(invitationId: number): Promise<void> {
|
|
219
|
+
await this.req(`user/repository_invitations/${invitationId}`, { method: "PATCH" });
|
|
220
|
+
}
|
|
221
|
+
async declineUserRepoInvitation(invitationId: number): Promise<void> {
|
|
222
|
+
await this.req(`user/repository_invitations/${invitationId}`, { method: "DELETE" });
|
|
223
|
+
}
|
|
224
|
+
async listOrgInvitations(org: string): Promise<InvitationResponse[]> {
|
|
225
|
+
return this.req<InvitationResponse[]>(`orgs/${encodeURIComponent(org)}/invitations`, { method: "GET" });
|
|
226
|
+
}
|
|
227
|
+
async createOrgInvitation(
|
|
228
|
+
org: string,
|
|
229
|
+
params: { inviteeLogin: string; role?: "member" | "admin"; teamIds?: number[]; expiresInDays?: number },
|
|
230
|
+
): Promise<InvitationResponse> {
|
|
231
|
+
return this.req<InvitationResponse>(`orgs/${encodeURIComponent(org)}/invitations`, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
invitee_login: params.inviteeLogin,
|
|
235
|
+
role: params.role ?? "member",
|
|
236
|
+
...(params.teamIds && params.teamIds.length > 0 ? { team_ids: params.teamIds } : {}),
|
|
237
|
+
...(typeof params.expiresInDays === "number" ? { expires_in_days: params.expiresInDays } : {}),
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async listOrgOutsideCollaborators(org: string): Promise<CollaboratorResponse[]> {
|
|
242
|
+
return this.req<CollaboratorResponse[]>(`orgs/${encodeURIComponent(org)}/outside_collaborators`, { method: "GET" });
|
|
243
|
+
}
|
|
244
|
+
async listUserOrgInvitations(): Promise<InvitationResponse[]> {
|
|
245
|
+
return this.req<InvitationResponse[]>("user/organization_invitations", { method: "GET" });
|
|
246
|
+
}
|
|
247
|
+
async acceptUserOrgInvitation(invitationId: number): Promise<void> {
|
|
248
|
+
await this.req(`user/organization_invitations/${invitationId}`, { method: "PATCH" });
|
|
249
|
+
}
|
|
250
|
+
async declineUserOrgInvitation(invitationId: number): Promise<void> {
|
|
251
|
+
await this.req(`user/organization_invitations/${invitationId}`, { method: "DELETE" });
|
|
252
|
+
}
|
|
74
253
|
async ensureLabels(labels: string[]): Promise<void> {
|
|
75
254
|
for (const label of labels) {
|
|
76
255
|
if (!label.trim()) continue;
|