agent-changelog 1.0.1
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 +125 -0
- package/SKILL.md +98 -0
- package/bin/agent-changelog.js +59 -0
- package/hooks/agent-changelog-capture/HOOK.md +11 -0
- package/hooks/agent-changelog-capture/handler.ts +35 -0
- package/hooks/agent-changelog-commit/HOOK.md +13 -0
- package/hooks/agent-changelog-commit/handler.ts +97 -0
- package/package.json +16 -0
- package/scripts/commit.sh +174 -0
- package/scripts/diff.sh +40 -0
- package/scripts/log.sh +39 -0
- package/scripts/restore.sh +68 -0
- package/scripts/rollback.sh +80 -0
- package/scripts/status.sh +74 -0
- package/setup.sh +217 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# agent-changelog
|
|
2
|
+
|
|
3
|
+
A versioning skill for OpenClaw that keeps a clear history of workspace changes with sender attribution.
|
|
4
|
+
|
|
5
|
+
Use it to answer questions like:
|
|
6
|
+
|
|
7
|
+
- Who changed this file?
|
|
8
|
+
- What changed between two points in time?
|
|
9
|
+
- Can I roll back to a known good state?
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
- Automatic capture of tracked file changes between turns
|
|
14
|
+
- Batched git commits every 10 minutes with per-sender attribution
|
|
15
|
+
- Chat/CLI commands for status, log, diff, rollback, and restore
|
|
16
|
+
- Optional push to your remote after each batched commit
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
Requirements: `git`, `jq`, Node.js
|
|
21
|
+
|
|
22
|
+
1. Install the skill into your OpenClaw workspace:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx agent-changelog
|
|
26
|
+
```
|
|
27
|
+
Defaults to `~/.openclaw/workspace` (or `OPENCLAW_WORKSPACE` if set).
|
|
28
|
+
2. In your terminal, restart the gateway so the skill is picked up:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
openclaw gateway restart
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. In chat, run:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
/agent-changelog setup
|
|
38
|
+
```
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
4. Restart the gateway again to activate the installed hooks:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
openclaw gateway restart
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
5. Verify:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
/agent-changelog status
|
|
51
|
+
```
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
**Optional — connect to GitHub:**
|
|
55
|
+
After setup, the agent can walk you through linking the workspace to a GitHub repo. Just ask:
|
|
56
|
+
```text
|
|
57
|
+
/agent-changelog ok help me set up github
|
|
58
|
+
```
|
|
59
|
+
It will handle git identity, auth (SSH or HTTPS), remote configuration, and the initial push — no prep work needed on your end.
|
|
60
|
+
|
|
61
|
+
## Example usages
|
|
62
|
+
|
|
63
|
+
Check the latest commit and pending changes:
|
|
64
|
+
```text
|
|
65
|
+
/agent-changelog show me the recent changes
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Browse specific recent history:
|
|
69
|
+
```text
|
|
70
|
+
/agent-changelog show me the last 10 changes made to the SOUL file
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See what is pending before the next batch commit:
|
|
74
|
+
```text
|
|
75
|
+
/agent-changelog what are the uncommitted changes?
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
After setup, `.agent-changelog.json` is created (if missing) and defaults to tracking the entire workspace:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"tracked": [
|
|
85
|
+
"."
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Edit this file to narrow or expand what gets tracked.
|
|
91
|
+
|
|
92
|
+
## A note on secrets
|
|
93
|
+
|
|
94
|
+
By default, agent-changelog tracks your entire workspace. Setup creates a `.gitignore` that excludes common secret patterns — `.env` files, API keys, tokens, credentials, cloud config directories, and more.
|
|
95
|
+
|
|
96
|
+
A few things to be careful about:
|
|
97
|
+
|
|
98
|
+
- **If your workspace already has a `.gitignore`**, setup leaves it untouched. Make sure it excludes anything sensitive before enabling tracking.
|
|
99
|
+
- **If you're pushing to a remote**, audit your workspace for hardcoded secrets in tracked files (SOUL.md, AGENTS.md, etc.) before the first push.
|
|
100
|
+
- **Narrow your tracking** if you're unsure. Edit `.agent-changelog.json` to list only the specific files you want versioned instead of `.`.
|
|
101
|
+
- **Auto-push is on if a remote exists.** If a git remote is configured in your workspace, every batch commit will be pushed automatically. Remove the remote or don't connect to GitHub if you want local-only history.
|
|
102
|
+
|
|
103
|
+
## In one minute: how it behaves
|
|
104
|
+
|
|
105
|
+
- On `message:received`, sender details are captured.
|
|
106
|
+
- On `message:sent`, tracked file changes are staged and queued with attribution.
|
|
107
|
+
- Every 10 minutes, queued entries are committed together with grouped attribution.
|
|
108
|
+
|
|
109
|
+
This gives you low-noise, attributable history without manual git bookkeeping every turn.
|
|
110
|
+
|
|
111
|
+
## FAQ
|
|
112
|
+
|
|
113
|
+
**Does this work without OpenClaw?**
|
|
114
|
+
No. The hooks rely on OpenClaw's event system (`message:received` / `message:sent`), and setup uses the `openclaw` CLI to register crons and enable hooks. It's built specifically for OpenClaw and won't run on another platform without significant rework.
|
|
115
|
+
|
|
116
|
+
**Can I sync history to GitHub?**
|
|
117
|
+
Yes. After setup, ask the agent to help you connect to GitHub and it will handle everything — git identity, auth, remote configuration, and the initial push. Once a remote is configured, every future batch commit is pushed automatically.
|
|
118
|
+
|
|
119
|
+
## Workspace files
|
|
120
|
+
|
|
121
|
+
| File | Purpose |
|
|
122
|
+
| --------------------------- | ----------------------------------------------------------------------------- |
|
|
123
|
+
| `.agent-changelog.json` | Your tracked-files configuration |
|
|
124
|
+
| `.version-context` | Temporary sender handoff between hooks (not committed) |
|
|
125
|
+
| `pending_commits.jsonl` | Pending attribution entries waiting for the next batch commit (not committed) |
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-changelog
|
|
3
|
+
description: Advanced handling for agent-changelog requests (history, diffs, restores, rollbacks, snapshots) using git and OpenClaw scripts with clear, user-focused summaries and outputs.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
metadata: {"openclaw":{"requires":{"bins":["git","jq","node"],"env":["OPENCLAW_WORKSPACE","OPENCLAW_CONFIG"]}}}
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Changelog
|
|
9
|
+
|
|
10
|
+
OpenClaw tracks workspace file changes between turns and attributes them to the user who triggered the change. Use this skill to answer history and diff questions and to apply controlled restores or rollbacks.
|
|
11
|
+
|
|
12
|
+
## When To Use
|
|
13
|
+
|
|
14
|
+
Use this skill when the user asks about:
|
|
15
|
+
- What changed, who changed it, or when it changed
|
|
16
|
+
- Diffs between versions or commits
|
|
17
|
+
- Rolling back or restoring files
|
|
18
|
+
- Taking or inspecting snapshots or status
|
|
19
|
+
- Setting up or verifying auto-versioning
|
|
20
|
+
|
|
21
|
+
## Response Framework
|
|
22
|
+
|
|
23
|
+
1. **Clarify intent and scope.**
|
|
24
|
+
- Determine whether the user wants a quick summary or raw output.
|
|
25
|
+
- Pin down file(s), time range, and commit identifiers if needed.
|
|
26
|
+
|
|
27
|
+
2. **Choose the evidence source.**
|
|
28
|
+
- Casual queries: use git to gather a compact view.
|
|
29
|
+
- Explicit `/agent-changelog` invocations: run the matching script and return stdout verbatim.
|
|
30
|
+
|
|
31
|
+
3. **Present results clearly.**
|
|
32
|
+
- Summarize what changed, who triggered it, and the rough size.
|
|
33
|
+
- Offer the next most likely action (diff, restore, rollback, or log).
|
|
34
|
+
|
|
35
|
+
4. **Handle destructive actions safely.**
|
|
36
|
+
- Always show what will change before a rollback or restore.
|
|
37
|
+
- Prefer `restore` for single-file fixes; use `rollback` only when the user wants to revert everything.
|
|
38
|
+
- If the target commit is ambiguous, ask a clarification question.
|
|
39
|
+
|
|
40
|
+
5. **Guide GitHub onboarding for setup.**
|
|
41
|
+
- After `setup`, proactively ask: "ok do you want help with github?"
|
|
42
|
+
- If yes, walk them through GitHub onboarding with no extra setup steps required on their side.
|
|
43
|
+
- Confirm account status, git identity, auth method, and remote configuration.
|
|
44
|
+
|
|
45
|
+
## Output Style
|
|
46
|
+
|
|
47
|
+
- For summaries, keep it short and conversational.
|
|
48
|
+
- For script-driven output, do not reformat or summarize; if onboarding guidance is needed, provide it after the raw output.
|
|
49
|
+
- If an argument looks like a typo, confirm before running.
|
|
50
|
+
|
|
51
|
+
## File Content Rules
|
|
52
|
+
|
|
53
|
+
**Never embed attribution metadata inside file content.** Do not add inline annotations like `(updated by X on date)`, `# changed by Y`, status footnotes, or any other authorship/timestamp markers into the files you edit. Attribution belongs exclusively in the git commit message, which is handled automatically by the hooks and `commit.sh`. Files should contain only their actual content — clean, annotation-free.
|
|
54
|
+
|
|
55
|
+
## Implementation Notes
|
|
56
|
+
|
|
57
|
+
- **Casual history or diff:** use a small git window (last 5-10 commits) and include stat output.
|
|
58
|
+
- **Slash commands:** use the scripts in `setup.sh` and `scripts/` with the user-provided arguments.
|
|
59
|
+
- **Setup:** run the setup script, then ask "ok do you want help with github?" and proceed if they confirm.
|
|
60
|
+
- **Restore or rollback:** locate the commit via `log`, then perform the change after showing what will be modified.
|
|
61
|
+
- **Semantic summary:** before every commit, run a quick diff and generate a sparse one-line summary of what changed and why (e.g. "added rate-limit rule to AGENTS.md, updated memory skill"). Always pass it via `--summary` and always include it in any history output presented to the user.
|
|
62
|
+
- **Log output:** `log.sh` outputs raw structured data — present it conversationally based on what the user asked. Don't dump raw script output. Format each entry using the `│`-prefixed box style (same as status output), one entry per block.
|
|
63
|
+
|
|
64
|
+
## Command Reference (Compact)
|
|
65
|
+
|
|
66
|
+
Use this only for explicit `/agent-changelog` invocations, and return stdout verbatim.
|
|
67
|
+
|
|
68
|
+
- `setup` -> `bash {baseDir}/setup.sh`
|
|
69
|
+
- `setup` follow-up -> GitHub onboarding guidance
|
|
70
|
+
- `status` -> `bash {baseDir}/scripts/status.sh`
|
|
71
|
+
- `log` -> `bash {baseDir}/scripts/log.sh [count]`
|
|
72
|
+
- `diff` -> `bash {baseDir}/scripts/diff.sh [commit] [commit2]`
|
|
73
|
+
- `rollback` -> `bash {baseDir}/scripts/rollback.sh <commit> ["reason"]`
|
|
74
|
+
- `restore` -> `bash {baseDir}/scripts/restore.sh <file> <commit> ["reason"]`
|
|
75
|
+
- `commit` (user-requested) -> `bash {baseDir}/scripts/commit.sh --manual ["message"] [--summary "one-line semantic summary"]`
|
|
76
|
+
- `commit` (cron-triggered) -> `bash {baseDir}/scripts/commit.sh [--summary "one-line semantic summary"]`
|
|
77
|
+
|
|
78
|
+
## Auto-Versioning Overview
|
|
79
|
+
|
|
80
|
+
Two hooks capture and commit changes between turns and attribute them to the active user. Defaults can be overridden via `.agent-changelog.json`.
|
|
81
|
+
|
|
82
|
+
Tracked by default: `.` (entire workspace). Secrets and runtime files are excluded via the `.gitignore` that setup creates — note that if a `.gitignore` already exists in the workspace, setup leaves it untouched, so ensure it covers secrets before enabling tracking.
|
|
83
|
+
|
|
84
|
+
To track a specific subset instead, edit `<workspace>/.agent-changelog.json` with a `tracked` array (this fully replaces the default):
|
|
85
|
+
```json
|
|
86
|
+
{ "tracked": ["<file-or-folder>", "<file-or-folder>"] }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## GitHub Onboarding (Setup Add-on)
|
|
90
|
+
|
|
91
|
+
Use this flow after setup to help users connect the workspace to GitHub. The user will need to authenticate (SSH key or HTTPS credential) — walk them through it step by step:
|
|
92
|
+
|
|
93
|
+
1. **Account and intent.** Confirm they have a GitHub account and want this repo linked.
|
|
94
|
+
2. **Git identity.** Ensure `user.name` and `user.email` are set for commits.
|
|
95
|
+
3. **Auth method.** Offer SSH or HTTPS; proceed with their preference.
|
|
96
|
+
4. **Remote and verify.** Ensure an `origin` remote exists and verify access.
|
|
97
|
+
5. **Next action.** Create or select the GitHub repo, then push or fetch as needed.
|
|
98
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
|
|
7
|
+
const skillName = "agent-changelog";
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const workspace =
|
|
10
|
+
process.env.OPENCLAW_WORKSPACE ||
|
|
11
|
+
path.join(os.homedir(), ".openclaw", "workspace");
|
|
12
|
+
const skillsDir = path.join(workspace, "skills");
|
|
13
|
+
const targetDir = path.join(skillsDir, skillName);
|
|
14
|
+
|
|
15
|
+
function copyFile(src, dest) {
|
|
16
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
17
|
+
fs.copyFileSync(src, dest);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function copyDir(srcDir, destDir) {
|
|
21
|
+
if (!fs.existsSync(srcDir)) return;
|
|
22
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
23
|
+
|
|
24
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
25
|
+
const src = path.join(srcDir, entry.name);
|
|
26
|
+
const dest = path.join(destDir, entry.name);
|
|
27
|
+
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
copyDir(src, dest);
|
|
30
|
+
} else if (entry.isFile()) {
|
|
31
|
+
copyFile(src, dest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(workspace)) {
|
|
37
|
+
console.error(`OpenClaw workspace not found: ${workspace}`);
|
|
38
|
+
console.error("Set OPENCLAW_WORKSPACE or run OpenClaw once to create it.");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
43
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
for (const file of ["SKILL.md", "setup.sh"]) {
|
|
46
|
+
const src = path.join(packageRoot, file);
|
|
47
|
+
if (!fs.existsSync(src)) {
|
|
48
|
+
console.error(`Missing required file: ${file}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
copyFile(src, path.join(targetDir, file));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const dir of ["hooks", "scripts"]) {
|
|
55
|
+
const src = path.join(packageRoot, dir);
|
|
56
|
+
copyDir(src, path.join(targetDir, dir));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`Installed ${skillName} to ${targetDir}`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-changelog-capture
|
|
3
|
+
description: "Captures sender identity before each agent turn for commit attribution"
|
|
4
|
+
metadata: { "openclaw": { "emoji": "📸", "events": ["message:received"] } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Changelog — Capture
|
|
8
|
+
|
|
9
|
+
Writes sender identity to `.version-context` in the workspace before each agent turn. The companion `agent-changelog-commit` hook reads this to attribute git commits to the correct user.
|
|
10
|
+
|
|
11
|
+
Part of the `agent-changelog` skill. Install via `setup.sh`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const handler = async (event: any) => {
|
|
5
|
+
if (event.type !== "message" || event.action !== "received") return;
|
|
6
|
+
|
|
7
|
+
const workspace =
|
|
8
|
+
process.env.OPENCLAW_WORKSPACE ?? `${process.env.HOME}/.openclaw/workspace`;
|
|
9
|
+
|
|
10
|
+
const ctx = {
|
|
11
|
+
user:
|
|
12
|
+
event.context?.senderName ??
|
|
13
|
+
event.context?.metadata?.senderName ??
|
|
14
|
+
event.context?.from ??
|
|
15
|
+
"unknown",
|
|
16
|
+
userId:
|
|
17
|
+
event.context?.senderId ??
|
|
18
|
+
event.context?.metadata?.senderId ??
|
|
19
|
+
event.context?.from ??
|
|
20
|
+
"unknown",
|
|
21
|
+
channel: event.context?.channelId ?? "unknown",
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
writeFileSync(join(workspace, ".version-context"), JSON.stringify(ctx));
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(
|
|
29
|
+
"[agent-changelog-capture] Failed to write context:",
|
|
30
|
+
err instanceof Error ? err.message : String(err)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default handler;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-changelog-commit
|
|
3
|
+
description: "Auto-commits workspace file changes with sender attribution after each agent turn"
|
|
4
|
+
metadata: { "openclaw": { "emoji": "📝", "events": ["message:sent"], "requires": { "bins": ["git"] } } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Changelog — Commit
|
|
8
|
+
|
|
9
|
+
After each outbound message, stages tracked workspace files and queues sender attribution in the commit message body.
|
|
10
|
+
|
|
11
|
+
Tracked files are read from `.agent-changelog.json` in the workspace, written by `setup.sh` on install.
|
|
12
|
+
|
|
13
|
+
Part of the `agent-changelog` skill. Install via `setup.sh`.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmdirSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
function run(cmd: string, cwd: string): string {
|
|
13
|
+
try {
|
|
14
|
+
return execSync(cmd, { cwd, encoding: "utf-8", timeout: 15_000 }).trim();
|
|
15
|
+
} catch {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getTracked(workspace: string): string[] {
|
|
21
|
+
const cfgPath = join(workspace, ".agent-changelog.json");
|
|
22
|
+
try {
|
|
23
|
+
const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
|
|
24
|
+
if (Array.isArray(cfg.tracked) && cfg.tracked.length > 0) return cfg.tracked;
|
|
25
|
+
} catch {}
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function acquireLock(lockDir: string): Promise<boolean> {
|
|
30
|
+
for (let i = 0; i < 50; i++) {
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(lockDir);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handler = async (event: any) => {
|
|
42
|
+
if (event.type !== "message" || event.action !== "sent") return;
|
|
43
|
+
|
|
44
|
+
const workspace =
|
|
45
|
+
process.env.OPENCLAW_WORKSPACE ?? `${process.env.HOME}/.openclaw/workspace`;
|
|
46
|
+
|
|
47
|
+
if (!existsSync(join(workspace, ".git"))) return;
|
|
48
|
+
|
|
49
|
+
const lockDir = join(workspace, ".version-lock");
|
|
50
|
+
const acquired = await acquireLock(lockDir);
|
|
51
|
+
if (!acquired) {
|
|
52
|
+
console.error("[agent-changelog-commit] Could not acquire lock, skipping");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Detect changes since the last git add (working tree vs index).
|
|
58
|
+
const changed = run("git diff --name-only", workspace)
|
|
59
|
+
.split("\n")
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
|
|
62
|
+
if (changed.length === 0) return;
|
|
63
|
+
|
|
64
|
+
// Read sender identity written by the capture hook
|
|
65
|
+
let user = "unknown";
|
|
66
|
+
let userId = "unknown";
|
|
67
|
+
let channel = "unknown";
|
|
68
|
+
const ctxPath = join(workspace, ".version-context");
|
|
69
|
+
if (existsSync(ctxPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const ctx = JSON.parse(readFileSync(ctxPath, "utf-8"));
|
|
72
|
+
user = ctx.user ?? "unknown";
|
|
73
|
+
userId = ctx.userId ?? "unknown";
|
|
74
|
+
channel = ctx.channel ?? "unknown";
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Append entry to pending log
|
|
79
|
+
const entry = JSON.stringify({ ts: Date.now(), user, userId, channel, files: changed });
|
|
80
|
+
appendFileSync(join(workspace, "pending_commits.jsonl"), entry + "\n");
|
|
81
|
+
|
|
82
|
+
// Stage tracked files
|
|
83
|
+
for (const f of getTracked(workspace)) {
|
|
84
|
+
run(`git add "${f}" 2>/dev/null || true`, workspace);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(
|
|
88
|
+
"[agent-changelog-commit] Error:",
|
|
89
|
+
err instanceof Error ? err.message : String(err)
|
|
90
|
+
);
|
|
91
|
+
} finally {
|
|
92
|
+
try { rmdirSync(lockDir); } catch {}
|
|
93
|
+
try { unlinkSync(join(workspace, ".version-context")); } catch {}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default handler;
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-changelog",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Install the agent-changelog OpenClaw skill into your OpenClaw workspace.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agent-changelog": "bin/agent-changelog.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"SKILL.md",
|
|
11
|
+
"setup.sh",
|
|
12
|
+
"hooks/",
|
|
13
|
+
"scripts/",
|
|
14
|
+
"README.md"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
5
|
+
cd "$WORKSPACE"
|
|
6
|
+
|
|
7
|
+
if [ ! -d .git ]; then
|
|
8
|
+
echo "⚠️ Versioning not initialized"
|
|
9
|
+
echo "Run \`/agent-changelog setup\` to get started."
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
MANUAL=false
|
|
14
|
+
MESSAGE=""
|
|
15
|
+
SUMMARY=""
|
|
16
|
+
for arg in "$@"; do
|
|
17
|
+
case "$arg" in
|
|
18
|
+
--manual) MANUAL=true ;;
|
|
19
|
+
--summary) shift_next=true ;;
|
|
20
|
+
*)
|
|
21
|
+
if [ "${shift_next:-false}" = true ]; then
|
|
22
|
+
SUMMARY="$arg"
|
|
23
|
+
shift_next=false
|
|
24
|
+
elif [ -z "$MESSAGE" ]; then
|
|
25
|
+
MESSAGE="$arg"
|
|
26
|
+
fi
|
|
27
|
+
;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
PENDING="$WORKSPACE/pending_commits.jsonl"
|
|
32
|
+
|
|
33
|
+
# ─── Resolve tracked files ────────────────────────────────────────────
|
|
34
|
+
TRACKED=()
|
|
35
|
+
while IFS= read -r item; do
|
|
36
|
+
TRACKED+=("$item")
|
|
37
|
+
done < <(jq -r '.tracked[]?' "$WORKSPACE/.agent-changelog.json" 2>/dev/null)
|
|
38
|
+
|
|
39
|
+
# ─── Stage any unstaged changes to tracked files ─────────────────────
|
|
40
|
+
for f in "${TRACKED[@]+"${TRACKED[@]}"}"; do
|
|
41
|
+
git add "$f" 2>/dev/null || true
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
# ─── Nothing staged at all → nothing to do ───────────────────────────
|
|
45
|
+
if git diff --cached --quiet 2>/dev/null; then
|
|
46
|
+
[ -f "$PENDING" ] && > "$PENDING"
|
|
47
|
+
echo "✓ No changes to commit"
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ─── Build commit message ─────────────────────────────────────────────
|
|
52
|
+
STAGED_FILES=$(git diff --cached --name-only | tr '\n' ' ' | sed 's/ $//')
|
|
53
|
+
USERS=""
|
|
54
|
+
COUNT=0
|
|
55
|
+
HAS_PENDING=false
|
|
56
|
+
CHANGELOG=""
|
|
57
|
+
|
|
58
|
+
if [ -f "$PENDING" ] && [ -s "$PENDING" ]; then
|
|
59
|
+
HAS_PENDING=true
|
|
60
|
+
while IFS= read -r line; do
|
|
61
|
+
[ -z "$line" ] && continue
|
|
62
|
+
COUNT=$((COUNT + 1))
|
|
63
|
+
|
|
64
|
+
if command -v jq &>/dev/null; then
|
|
65
|
+
user=$(echo "$line" | jq -r '.user // "unknown"' 2>/dev/null || echo "unknown")
|
|
66
|
+
ts=$(echo "$line" | jq -r '.ts // 0' 2>/dev/null || echo "0")
|
|
67
|
+
channel=$(echo "$line" | jq -r '.channel // "unknown"' 2>/dev/null || echo "unknown")
|
|
68
|
+
files=$(echo "$line" | jq -r '(.files // []) | join(", ")' 2>/dev/null || echo "")
|
|
69
|
+
action=$(echo "$line" | jq -r '.action // ""' 2>/dev/null || echo "")
|
|
70
|
+
action_target=$(echo "$line" | jq -r '.target // ""' 2>/dev/null || echo "")
|
|
71
|
+
action_file=$(echo "$line" | jq -r '.file // ""' 2>/dev/null || echo "")
|
|
72
|
+
action_from=$(echo "$line" | jq -r '.from // ""' 2>/dev/null || echo "")
|
|
73
|
+
action_reason=$(echo "$line" | jq -r '.reason // ""' 2>/dev/null || echo "")
|
|
74
|
+
else
|
|
75
|
+
user=$(echo "$line" | grep -o '"user":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
|
|
76
|
+
ts="0"; channel="unknown"; files=""; action=""; action_target=""; action_file=""; action_from=""; action_reason=""
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Format timestamp as readable date
|
|
80
|
+
if [ "$ts" != "0" ] && command -v date &>/dev/null; then
|
|
81
|
+
ts_sec=$((ts / 1000))
|
|
82
|
+
readable=$(date -r "$ts_sec" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "$ts")
|
|
83
|
+
else
|
|
84
|
+
readable="$ts"
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Accumulate unique users
|
|
88
|
+
if [ -n "$user" ] && [ "$user" != "unknown" ]; then
|
|
89
|
+
if [ -z "$USERS" ]; then
|
|
90
|
+
USERS="$user"
|
|
91
|
+
elif ! echo "$USERS" | grep -qF "$user"; then
|
|
92
|
+
USERS="$USERS, $user"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Build per-turn changelog line
|
|
97
|
+
if [ "$action" = "rollback" ]; then
|
|
98
|
+
CHANGELOG="${CHANGELOG} [$readable] $user\n"
|
|
99
|
+
CHANGELOG="${CHANGELOG} action: rollback → $action_target\n"
|
|
100
|
+
[ -n "$action_reason" ] && CHANGELOG="${CHANGELOG} reason: $action_reason\n"
|
|
101
|
+
elif [ "$action" = "restore" ]; then
|
|
102
|
+
CHANGELOG="${CHANGELOG} [$readable] $user\n"
|
|
103
|
+
CHANGELOG="${CHANGELOG} action: restore $action_file from $action_from\n"
|
|
104
|
+
[ -n "$action_reason" ] && CHANGELOG="${CHANGELOG} reason: $action_reason\n"
|
|
105
|
+
else
|
|
106
|
+
CHANGELOG="${CHANGELOG} [$readable] $user ($channel): $files\n"
|
|
107
|
+
fi
|
|
108
|
+
done < "$PENDING"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# ─── Determine prefix ─────────────────────────────────────────────────
|
|
112
|
+
CTX="$WORKSPACE/.version-context"
|
|
113
|
+
|
|
114
|
+
if [ "$MANUAL" = true ]; then
|
|
115
|
+
PREFIX="Manual commit"
|
|
116
|
+
# For manual commits, prefer identity from version-context (set by capture hook)
|
|
117
|
+
if [ -z "$USERS" ] || [ "$USERS" = "unknown" ]; then
|
|
118
|
+
if [ -f "$CTX" ] && command -v jq &>/dev/null; then
|
|
119
|
+
CTX_USER=$(jq -r '.user // ""' "$CTX" 2>/dev/null || true)
|
|
120
|
+
[ -n "$CTX_USER" ] && [ "$CTX_USER" != "unknown" ] && USERS="$CTX_USER"
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
if [ -z "$USERS" ] || [ "$USERS" = "unknown" ]; then USERS="skill invocation"; fi
|
|
124
|
+
elif [ "$HAS_PENDING" = false ] || [ "$COUNT" -eq 0 ]; then
|
|
125
|
+
PREFIX="Auto-commit (cli)"
|
|
126
|
+
USERS="cli"
|
|
127
|
+
else
|
|
128
|
+
PREFIX="Auto-commit"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
if [ -z "$USERS" ]; then USERS="unknown"; fi
|
|
132
|
+
|
|
133
|
+
# ─── Assemble full message ────────────────────────────────────────────
|
|
134
|
+
if [ -n "$MESSAGE" ]; then
|
|
135
|
+
SUBJECT="$MESSAGE"
|
|
136
|
+
else
|
|
137
|
+
SUBJECT="${PREFIX}: $STAGED_FILES"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
MSG="$SUBJECT
|
|
141
|
+
|
|
142
|
+
Triggered by: ${USERS}
|
|
143
|
+
Turns: ${COUNT}"
|
|
144
|
+
|
|
145
|
+
[ -n "$SUMMARY" ] && MSG="${MSG}
|
|
146
|
+
Summary: ${SUMMARY}"
|
|
147
|
+
|
|
148
|
+
if [ -n "$CHANGELOG" ]; then
|
|
149
|
+
MSG="${MSG}
|
|
150
|
+
|
|
151
|
+
--- Change log ---
|
|
152
|
+
$(printf "%b" "$CHANGELOG")"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
git commit -m "$MSG"
|
|
156
|
+
SHORT_HASH=$(git rev-parse --short HEAD)
|
|
157
|
+
|
|
158
|
+
[ -f "$PENDING" ] && > "$PENDING"
|
|
159
|
+
|
|
160
|
+
echo "✅ **Committed** \`$SHORT_HASH\`"
|
|
161
|
+
echo "$SUBJECT"
|
|
162
|
+
echo "_by ${USERS}_"
|
|
163
|
+
[ -n "$SUMMARY" ] && echo "$SUMMARY"
|
|
164
|
+
|
|
165
|
+
# ─── Push if remote is configured ────────────────────────────────────
|
|
166
|
+
GIT_REMOTE=$(git remote 2>/dev/null | head -1)
|
|
167
|
+
|
|
168
|
+
if [ -n "$GIT_REMOTE" ]; then
|
|
169
|
+
if git push "$GIT_REMOTE" 2>/dev/null; then
|
|
170
|
+
echo "↑ Pushed to \`$GIT_REMOTE\`"
|
|
171
|
+
else
|
|
172
|
+
echo "⚠️ Push to \`$GIT_REMOTE\` failed"
|
|
173
|
+
fi
|
|
174
|
+
fi
|
package/scripts/diff.sh
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
5
|
+
cd "$WORKSPACE"
|
|
6
|
+
|
|
7
|
+
if [ ! -d .git ]; then
|
|
8
|
+
echo "⚠️ Versioning not initialized"
|
|
9
|
+
echo "Run \`/agent-changelog setup\` to get started."
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
if [ $# -eq 0 ]; then
|
|
14
|
+
CHANGED=$(git diff HEAD --name-only --no-color 2>/dev/null | tr '\n' ' ' | sed 's/ $//' || true)
|
|
15
|
+
if [ -z "$CHANGED" ]; then
|
|
16
|
+
echo "✓ No uncommitted changes"
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
echo "✏️ **Uncommitted changes**"
|
|
20
|
+
echo "\`$CHANGED\`"
|
|
21
|
+
echo ""
|
|
22
|
+
echo '```diff'
|
|
23
|
+
git diff HEAD --no-color 2>/dev/null || git diff --no-color
|
|
24
|
+
echo '```'
|
|
25
|
+
elif [ $# -eq 1 ]; then
|
|
26
|
+
SUBJECT=$(git log --format="%s" -1 "$1" 2>/dev/null || true)
|
|
27
|
+
DATE=$(git log --format="%ad" --date=format:"%b %d, %H:%M" -1 "$1" 2>/dev/null || true)
|
|
28
|
+
echo "🔍 **\`$1\`** · $DATE"
|
|
29
|
+
echo "_$SUBJECT_"
|
|
30
|
+
echo ""
|
|
31
|
+
echo '```diff'
|
|
32
|
+
git show "$1" --stat --patch --no-color
|
|
33
|
+
echo '```'
|
|
34
|
+
elif [ $# -eq 2 ]; then
|
|
35
|
+
echo "🔍 **Diff** \`$1\` → \`$2\`"
|
|
36
|
+
echo ""
|
|
37
|
+
echo '```diff'
|
|
38
|
+
git diff "$1" "$2" --no-color
|
|
39
|
+
echo '```'
|
|
40
|
+
fi
|
package/scripts/log.sh
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
5
|
+
cd "$WORKSPACE"
|
|
6
|
+
|
|
7
|
+
if [ ! -d .git ]; then
|
|
8
|
+
echo "⚠️ Versioning not initialized"
|
|
9
|
+
echo "Run \`/agent-changelog setup\` to get started."
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
COUNT=10
|
|
14
|
+
for arg in "$@"; do
|
|
15
|
+
case "$arg" in
|
|
16
|
+
[0-9]*) COUNT="$arg" ;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
[ "${COUNT:-0}" -le 0 ] && COUNT=10
|
|
21
|
+
|
|
22
|
+
while IFS= read -r hash; do
|
|
23
|
+
date=$(git log --format="%ad" --date=format:"%b %d, %H:%M" -1 "$hash")
|
|
24
|
+
subject=$(git log --format="%s" -1 "$hash")
|
|
25
|
+
body=$(git log --format="%b" -1 "$hash")
|
|
26
|
+
triggered=$(echo "$body" | grep "^Triggered by:" | sed 's/Triggered by: //' || true)
|
|
27
|
+
turns=$(echo "$body" | grep "^Turns:" | sed 's/Turns: //' || true)
|
|
28
|
+
summary=$(echo "$body" | grep "^Summary:" | sed 's/Summary: //' || true)
|
|
29
|
+
changelog=$(echo "$body" | awk '/^--- Change log ---/{found=1; next} found{print}' || true)
|
|
30
|
+
|
|
31
|
+
echo "commit $hash"
|
|
32
|
+
echo "date: $date"
|
|
33
|
+
echo "subject: $subject"
|
|
34
|
+
[ -n "$triggered" ] && echo "by: $triggered"
|
|
35
|
+
[ -n "$turns" ] && echo "turns: $turns"
|
|
36
|
+
[ -n "$summary" ] && echo "summary: $summary"
|
|
37
|
+
[ -n "$changelog" ] && printf "changelog:\n%s\n" "$changelog"
|
|
38
|
+
echo "---"
|
|
39
|
+
done < <(git log --format="%h" -n "$COUNT")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
5
|
+
cd "$WORKSPACE"
|
|
6
|
+
|
|
7
|
+
if [ ! -d .git ]; then
|
|
8
|
+
echo "⚠️ Versioning not initialized"
|
|
9
|
+
echo "Run \`/agent-changelog setup\` to get started."
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
FILE="${1:-}"
|
|
14
|
+
COMMIT="${2:-}"
|
|
15
|
+
REASON="${3:-}"
|
|
16
|
+
|
|
17
|
+
if [ -z "$FILE" ] || [ -z "$COMMIT" ]; then
|
|
18
|
+
echo "**Usage:** \`/agent-changelog restore <file> <commit> [reason]\`"
|
|
19
|
+
echo ""
|
|
20
|
+
echo "Restores a single file to its state before the given commit."
|
|
21
|
+
echo "Run \`/agent-changelog log\` to find the right commit."
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if ! git cat-file -e "$COMMIT" 2>/dev/null; then
|
|
26
|
+
echo "⚠️ Commit \`$COMMIT\` not found"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if ! git diff-tree --no-commit-id -r --name-only "$COMMIT" | grep -qF "$FILE"; then
|
|
31
|
+
echo "⚠️ \`$FILE\` was not changed in \`$COMMIT\`"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
TARGET_SHORT=$(git rev-parse --short "$COMMIT")
|
|
36
|
+
|
|
37
|
+
# Read sender identity from capture hook context
|
|
38
|
+
USER="unknown"
|
|
39
|
+
USER_ID="unknown"
|
|
40
|
+
CHANNEL="unknown"
|
|
41
|
+
CTX="$WORKSPACE/.version-context"
|
|
42
|
+
if [ -f "$CTX" ] && command -v jq &>/dev/null; then
|
|
43
|
+
USER=$(jq -r '.user // "unknown"' "$CTX" 2>/dev/null || echo "unknown")
|
|
44
|
+
USER_ID=$(jq -r '.userId // "unknown"' "$CTX" 2>/dev/null || echo "unknown")
|
|
45
|
+
CHANNEL=$(jq -r '.channel // "unknown"' "$CTX" 2>/dev/null || echo "unknown")
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Restore file to its state before the target commit and stage it
|
|
49
|
+
git checkout "${COMMIT}^" -- "$FILE"
|
|
50
|
+
git add "$FILE"
|
|
51
|
+
|
|
52
|
+
# Log to pending so the next commit includes restore attribution
|
|
53
|
+
ENTRY=$(jq -n \
|
|
54
|
+
--argjson ts "$(date +%s000)" \
|
|
55
|
+
--arg user "$USER" \
|
|
56
|
+
--arg userId "$USER_ID" \
|
|
57
|
+
--arg channel "$CHANNEL" \
|
|
58
|
+
--arg file "$FILE" \
|
|
59
|
+
--arg from "$TARGET_SHORT" \
|
|
60
|
+
--arg reason "$REASON" \
|
|
61
|
+
'{"ts":$ts,"user":$user,"userId":$userId,"channel":$channel,"action":"restore","file":$file,"from":$from,"reason":$reason,"files":[]}')
|
|
62
|
+
printf '%s\n' "$ENTRY" >> "$WORKSPACE/pending_commits.jsonl"
|
|
63
|
+
|
|
64
|
+
echo "📌 **Staged restore**"
|
|
65
|
+
echo "\`$FILE\` → before \`$TARGET_SHORT\`"
|
|
66
|
+
echo "_by ${USER}_"
|
|
67
|
+
echo ""
|
|
68
|
+
echo "Commit with \`/agent-changelog commit\`"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
5
|
+
cd "$WORKSPACE"
|
|
6
|
+
|
|
7
|
+
if [ ! -d .git ]; then
|
|
8
|
+
echo "⚠️ Versioning not initialized"
|
|
9
|
+
echo "Run \`/agent-changelog setup\` to get started."
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
TARGET="${1:-}"
|
|
14
|
+
REASON="${2:-}"
|
|
15
|
+
|
|
16
|
+
if [ -z "$TARGET" ]; then
|
|
17
|
+
echo "**Usage:** \`/agent-changelog rollback <commit> [reason]\`"
|
|
18
|
+
echo ""
|
|
19
|
+
echo "**Recent commits:**"
|
|
20
|
+
while IFS= read -r hash; do
|
|
21
|
+
date=$(git log --format="%ad" --date=format:"%b %d, %H:%M" -1 "$hash")
|
|
22
|
+
subject=$(git log --format="%s" -1 "$hash")
|
|
23
|
+
echo "• \`$hash\` $date — $subject"
|
|
24
|
+
done < <(git log --format="%h" -10)
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if ! git cat-file -e "$TARGET" 2>/dev/null; then
|
|
29
|
+
echo "⚠️ Commit \`$TARGET\` not found"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
CURRENT_SHORT=$(git rev-parse --short HEAD)
|
|
34
|
+
TARGET_SHORT=$(git rev-parse --short "$TARGET")
|
|
35
|
+
TARGET_MSG=$(git log --format="%s" -1 "$TARGET")
|
|
36
|
+
|
|
37
|
+
# Read identity from version context
|
|
38
|
+
CTX="$WORKSPACE/.version-context"
|
|
39
|
+
ACTOR="unknown"
|
|
40
|
+
ACTOR_ID="unknown"
|
|
41
|
+
CHANNEL="unknown"
|
|
42
|
+
if [ -f "$CTX" ] && command -v jq &>/dev/null; then
|
|
43
|
+
ACTOR=$(jq -r '.user // "unknown"' "$CTX" 2>/dev/null || echo "unknown")
|
|
44
|
+
ACTOR_ID=$(jq -r '.userId // "unknown"' "$CTX" 2>/dev/null || echo "unknown")
|
|
45
|
+
CHANNEL=$(jq -r '.channel // "unknown"' "$CTX" 2>/dev/null || echo "unknown")
|
|
46
|
+
fi
|
|
47
|
+
[ "$ACTOR" = "unknown" ] && ACTOR="skill invocation"
|
|
48
|
+
[ "$ACTOR_ID" = "unknown" ] && ACTOR_ID="skill invocation"
|
|
49
|
+
|
|
50
|
+
# Restore only tracked files
|
|
51
|
+
while IFS= read -r f; do
|
|
52
|
+
git checkout "$TARGET" -- "$f" 2>/dev/null || true
|
|
53
|
+
done < <(jq -r '.tracked[]?' "$WORKSPACE/.agent-changelog.json" 2>/dev/null)
|
|
54
|
+
|
|
55
|
+
# Stage the same tracked files
|
|
56
|
+
while IFS= read -r f; do
|
|
57
|
+
git add "$f" 2>/dev/null || true
|
|
58
|
+
done < <(jq -r '.tracked[]?' "$WORKSPACE/.agent-changelog.json" 2>/dev/null)
|
|
59
|
+
|
|
60
|
+
if ! git diff --cached --quiet; then
|
|
61
|
+
ENTRY=$(jq -n \
|
|
62
|
+
--argjson ts "$(date +%s000)" \
|
|
63
|
+
--arg user "$ACTOR" \
|
|
64
|
+
--arg userId "$ACTOR_ID" \
|
|
65
|
+
--arg channel "$CHANNEL" \
|
|
66
|
+
--arg target "$TARGET_SHORT" \
|
|
67
|
+
--arg reason "$REASON" \
|
|
68
|
+
'{"ts":$ts,"user":$user,"userId":$userId,"channel":$channel,"action":"rollback","target":$target,"reason":$reason,"files":[]}')
|
|
69
|
+
printf '%s\n' "$ENTRY" >> "$WORKSPACE/pending_commits.jsonl"
|
|
70
|
+
|
|
71
|
+
echo "⏪ **Staged rollback**"
|
|
72
|
+
echo "\`$CURRENT_SHORT\` → \`$TARGET_SHORT\`"
|
|
73
|
+
echo "_$TARGET_MSG_"
|
|
74
|
+
echo ""
|
|
75
|
+
echo "_by ${ACTOR}_"
|
|
76
|
+
echo "Commit with \`/agent-changelog commit\`"
|
|
77
|
+
else
|
|
78
|
+
echo "✓ Already at \`$TARGET_SHORT\`"
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
5
|
+
cd "$WORKSPACE"
|
|
6
|
+
|
|
7
|
+
if [ ! -d .git ]; then
|
|
8
|
+
echo "⚠️ Versioning not initialized"
|
|
9
|
+
echo "Run \`/agent-changelog setup\` to get started."
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
HASH=$(git log --format="%h" -1 2>/dev/null || true)
|
|
14
|
+
|
|
15
|
+
if [ -z "$HASH" ]; then
|
|
16
|
+
echo "📸 No commits yet"
|
|
17
|
+
else
|
|
18
|
+
DATE=$(git log --format="%ad" --date=format:"%b %d, %H:%M" -1)
|
|
19
|
+
SUBJECT=$(git log --format="%s" -1)
|
|
20
|
+
BODY=$(git log --format="%b" -1)
|
|
21
|
+
|
|
22
|
+
# Parse commit type from subject prefix
|
|
23
|
+
if echo "$SUBJECT" | grep -qi "^auto-commit"; then
|
|
24
|
+
TYPE="Auto"
|
|
25
|
+
FILES=$(echo "$SUBJECT" | sed 's/^[Aa]uto-commit[^:]*: //')
|
|
26
|
+
elif echo "$SUBJECT" | grep -qi "^manual commit"; then
|
|
27
|
+
TYPE="Manual"
|
|
28
|
+
FILES=$(echo "$SUBJECT" | sed 's/^[Mm]anual commit[^:]*: //')
|
|
29
|
+
elif echo "$SUBJECT" | grep -qi "^rollback"; then
|
|
30
|
+
TYPE="Rollback"
|
|
31
|
+
FILES=""
|
|
32
|
+
else
|
|
33
|
+
TYPE=""
|
|
34
|
+
FILES="$SUBJECT"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Parse identity from commit body
|
|
38
|
+
IDENTITY=$(echo "$BODY" | grep "^Triggered by:" | sed 's/Triggered by: //' | tr -d '\n' | sed 's/cli/CLI/g' || true)
|
|
39
|
+
|
|
40
|
+
# Build output line
|
|
41
|
+
echo "📸 \`$HASH\` · $DATE"
|
|
42
|
+
|
|
43
|
+
META_PARTS=()
|
|
44
|
+
[ -n "$TYPE" ] && META_PARTS+=("$TYPE")
|
|
45
|
+
[ -n "$IDENTITY" ] && META_PARTS+=("by $IDENTITY")
|
|
46
|
+
[ -n "$FILES" ] && META_PARTS+=("$FILES")
|
|
47
|
+
if [ ${#META_PARTS[@]} -gt 0 ]; then
|
|
48
|
+
(IFS=' · '; echo "_${META_PARTS[*]}_")
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo ""
|
|
53
|
+
|
|
54
|
+
MODIFIED=$(git diff HEAD --name-only 2>/dev/null)
|
|
55
|
+
UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null)
|
|
56
|
+
ALL_CHANGES=$(printf '%s\n%s\n' "$MODIFIED" "$UNTRACKED" | grep -v '^$' || true)
|
|
57
|
+
CHANGES=$(printf '%s\n' "$ALL_CHANGES" | grep -c . || true)
|
|
58
|
+
|
|
59
|
+
if [ "$CHANGES" -gt 0 ]; then
|
|
60
|
+
LABEL=$([ "$CHANGES" -eq 1 ] && echo "file" || echo "files")
|
|
61
|
+
echo "✏️ **$CHANGES uncommitted $LABEL:**"
|
|
62
|
+
while IFS= read -r file; do
|
|
63
|
+
[ -z "$file" ] && continue
|
|
64
|
+
if echo "$MODIFIED" | grep -qF "$file"; then
|
|
65
|
+
added=$(git diff HEAD -- "$file" 2>/dev/null | grep -c '^+[^+]' || true)
|
|
66
|
+
removed=$(git diff HEAD -- "$file" 2>/dev/null | grep -c '^-[^-]' || true)
|
|
67
|
+
echo "• \`$file\` +$added/-$removed"
|
|
68
|
+
else
|
|
69
|
+
echo "• \`$file\` new"
|
|
70
|
+
fi
|
|
71
|
+
done <<< "$ALL_CHANGES"
|
|
72
|
+
else
|
|
73
|
+
echo "✓ No uncommitted changes"
|
|
74
|
+
fi
|
package/setup.sh
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
WORKSPACE="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}"
|
|
6
|
+
HOOKS_DEST="$WORKSPACE/hooks"
|
|
7
|
+
|
|
8
|
+
success() { echo "✅ $*"; echo ""; }
|
|
9
|
+
warn() { echo "⚠️ $*"; echo ""; }
|
|
10
|
+
fail() { echo "❌ $*"; exit 1; }
|
|
11
|
+
header() { echo ""; echo "**$***"; echo ""; }
|
|
12
|
+
|
|
13
|
+
echo "> 🗂️ **agent-changelog**"
|
|
14
|
+
echo "> _workspace version control — setup_"
|
|
15
|
+
|
|
16
|
+
# ─── Prerequisites ────────────────────────────────────────────────────
|
|
17
|
+
header "Prerequisites"
|
|
18
|
+
|
|
19
|
+
command -v git &>/dev/null || fail "git not found — install it first"
|
|
20
|
+
success "git $(git --version | awk '{print $3}')"
|
|
21
|
+
|
|
22
|
+
command -v jq &>/dev/null || fail "jq not found — install it first"
|
|
23
|
+
success "jq $(jq --version)"
|
|
24
|
+
|
|
25
|
+
[ -d "$WORKSPACE" ] || fail "Workspace not found: $WORKSPACE"
|
|
26
|
+
success "workspace \`$WORKSPACE\`"
|
|
27
|
+
|
|
28
|
+
if command -v openclaw &>/dev/null; then
|
|
29
|
+
success "openclaw $(openclaw --version 2>/dev/null | head -1 | awk '{print $3}')"
|
|
30
|
+
else
|
|
31
|
+
warn "openclaw CLI not found — you'll need to enable hooks manually"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# ─── Install hooks ────────────────────────────────────────────────────
|
|
35
|
+
header "🪝 Installing hooks"
|
|
36
|
+
|
|
37
|
+
mkdir -p "$HOOKS_DEST"
|
|
38
|
+
|
|
39
|
+
HOOKS=("agent-changelog-capture" "agent-changelog-commit")
|
|
40
|
+
for hook in "${HOOKS[@]}"; do
|
|
41
|
+
src="$SCRIPT_DIR/hooks/$hook"
|
|
42
|
+
dest="$HOOKS_DEST/$hook"
|
|
43
|
+
|
|
44
|
+
if [ ! -d "$src" ]; then
|
|
45
|
+
warn "Hook source not found: \`$src\`"
|
|
46
|
+
continue
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [ -L "$dest" ]; then
|
|
50
|
+
rm "$dest"
|
|
51
|
+
elif [ -d "$dest" ]; then
|
|
52
|
+
rm -rf "$dest"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
cp -r "$src" "$dest"
|
|
56
|
+
success "Installed \`$hook\`"
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
# ─── Enable hooks via config ──────────────────────────────────────────
|
|
60
|
+
header "⚡ Activating hooks"
|
|
61
|
+
|
|
62
|
+
OPENCLAW_CFG="${OPENCLAW_CONFIG:-$HOME/.openclaw/openclaw.json}"
|
|
63
|
+
|
|
64
|
+
if [ -f "$OPENCLAW_CFG" ] && command -v jq &>/dev/null; then
|
|
65
|
+
TMP=$(mktemp)
|
|
66
|
+
jq '
|
|
67
|
+
.hooks.internal.enabled = true |
|
|
68
|
+
.hooks.internal.entries["agent-changelog-capture"].enabled = true |
|
|
69
|
+
.hooks.internal.entries["agent-changelog-commit"].enabled = true
|
|
70
|
+
' "$OPENCLAW_CFG" > "$TMP" && mv "$TMP" "$OPENCLAW_CFG"
|
|
71
|
+
success "Hooks enabled in config"
|
|
72
|
+
else
|
|
73
|
+
warn "Could not update hook config — enable manually after restarting:"
|
|
74
|
+
for hook in "${HOOKS[@]}"; do
|
|
75
|
+
echo " - \`openclaw hooks enable $hook\`"
|
|
76
|
+
done
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# ─── Register cron ────────────────────────────────────────────────────
|
|
80
|
+
header "⏱️ Registering cron"
|
|
81
|
+
|
|
82
|
+
if command -v openclaw &>/dev/null; then
|
|
83
|
+
CRON_NAME="agent-changelog-commit"
|
|
84
|
+
CRON_CMD="bash $SCRIPT_DIR/scripts/commit.sh"
|
|
85
|
+
if openclaw cron list --json 2>/dev/null | jq -e --arg name "$CRON_NAME" '.jobs[] | select(.name == $name)' >/dev/null 2>&1; then
|
|
86
|
+
success "Cron \`$CRON_NAME\` already registered"
|
|
87
|
+
elif openclaw cron add \
|
|
88
|
+
--name "$CRON_NAME" \
|
|
89
|
+
--cron "*/10 * * * *" \
|
|
90
|
+
--message "$CRON_CMD" \
|
|
91
|
+
--session isolated \
|
|
92
|
+
--no-deliver >/dev/null 2>&1; then
|
|
93
|
+
success "Registered \`$CRON_NAME\` _(every 10 min)_"
|
|
94
|
+
else
|
|
95
|
+
warn "Cron registration failed — check with: \`openclaw cron list\`"
|
|
96
|
+
fi
|
|
97
|
+
else
|
|
98
|
+
warn "Register cron manually after gateway starts:"
|
|
99
|
+
echo " \`openclaw cron add --name agent-changelog-commit --cron '*/10 * * * *' --message 'bash $SCRIPT_DIR/scripts/commit.sh' --session isolated --no-deliver\`"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# ─── Initialize git repo ──────────────────────────────────────────────
|
|
103
|
+
header "📦 Git repository"
|
|
104
|
+
|
|
105
|
+
if [ -d "$WORKSPACE/.git" ]; then
|
|
106
|
+
COMMIT_COUNT=$(cd "$WORKSPACE" && git rev-list --count HEAD 2>/dev/null || echo "0")
|
|
107
|
+
success "Already initialized _($COMMIT_COUNT commits)_"
|
|
108
|
+
else
|
|
109
|
+
(cd "$WORKSPACE" && git init -b main) >/dev/null 2>&1
|
|
110
|
+
success "Initialized git repository"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ ! -f "$WORKSPACE/.gitignore" ]; then
|
|
114
|
+
cat > "$WORKSPACE/.gitignore" << 'GITIGNORE'
|
|
115
|
+
# secrets
|
|
116
|
+
.env
|
|
117
|
+
.env.*
|
|
118
|
+
*.env
|
|
119
|
+
.envrc
|
|
120
|
+
**/credentials/
|
|
121
|
+
**/secrets/
|
|
122
|
+
**/.credentials
|
|
123
|
+
**/.secrets
|
|
124
|
+
**/api_key*
|
|
125
|
+
**/apikey*
|
|
126
|
+
**/*_key.txt
|
|
127
|
+
**/*_key.json
|
|
128
|
+
**/*_token*
|
|
129
|
+
**/*_secret*
|
|
130
|
+
**/auth_token*
|
|
131
|
+
**/access_token*
|
|
132
|
+
**/refresh_token*
|
|
133
|
+
*.pem
|
|
134
|
+
*.key
|
|
135
|
+
*.p12
|
|
136
|
+
*.pfx
|
|
137
|
+
id_rsa
|
|
138
|
+
id_ed25519
|
|
139
|
+
*.ppk
|
|
140
|
+
client_secret*.json
|
|
141
|
+
service_account*.json
|
|
142
|
+
*-credentials.json
|
|
143
|
+
.aws/
|
|
144
|
+
.azure/
|
|
145
|
+
.gcloud/
|
|
146
|
+
gcloud*.json
|
|
147
|
+
aws_credentials
|
|
148
|
+
|
|
149
|
+
# runtime
|
|
150
|
+
*.log
|
|
151
|
+
*.tmp
|
|
152
|
+
*.temp
|
|
153
|
+
*.swp
|
|
154
|
+
*.swo
|
|
155
|
+
*~
|
|
156
|
+
*.jsonl
|
|
157
|
+
.version-context
|
|
158
|
+
state/
|
|
159
|
+
state.json
|
|
160
|
+
memory/
|
|
161
|
+
.DS_Store
|
|
162
|
+
Thumbs.db
|
|
163
|
+
desktop.ini
|
|
164
|
+
|
|
165
|
+
# openclaw internal
|
|
166
|
+
.openclaw/
|
|
167
|
+
|
|
168
|
+
# build
|
|
169
|
+
node_modules/
|
|
170
|
+
__pycache__/
|
|
171
|
+
*.pyc
|
|
172
|
+
.cache/
|
|
173
|
+
dist/
|
|
174
|
+
build/
|
|
175
|
+
*.egg-info/
|
|
176
|
+
GITIGNORE
|
|
177
|
+
success "Created \`.gitignore\`"
|
|
178
|
+
else
|
|
179
|
+
warn ".gitignore already exists and was left untouched. Review it before pushing to a remote to make sure secrets are excluded."
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# ─── Seed workspace config ────────────────────────────────────────────
|
|
183
|
+
header "🔧 Workspace config"
|
|
184
|
+
|
|
185
|
+
WORKSPACE_CFG="$WORKSPACE/.agent-changelog.json"
|
|
186
|
+
if [ ! -f "$WORKSPACE_CFG" ]; then
|
|
187
|
+
cat > "$WORKSPACE_CFG" << 'EOF'
|
|
188
|
+
{
|
|
189
|
+
"tracked": [
|
|
190
|
+
"."
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
EOF
|
|
194
|
+
success "Created \`.agent-changelog.json\`"
|
|
195
|
+
else
|
|
196
|
+
success "\`.agent-changelog.json\` already exists — leaving as-is"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# ─── First snapshot ───────────────────────────────────────────────────
|
|
200
|
+
header "📸 First snapshot"
|
|
201
|
+
|
|
202
|
+
cd "$WORKSPACE"
|
|
203
|
+
while IFS= read -r f; do
|
|
204
|
+
git add "$f" 2>/dev/null || true
|
|
205
|
+
done < <(jq -r '.tracked[]?' "$WORKSPACE/.agent-changelog.json" 2>/dev/null)
|
|
206
|
+
|
|
207
|
+
if ! git diff --cached --quiet 2>/dev/null; then
|
|
208
|
+
git commit -m "Initial snapshot — agent versioning setup" >/dev/null 2>&1
|
|
209
|
+
HASH=$(git rev-parse --short HEAD)
|
|
210
|
+
success "Snapshot \`$HASH\` created"
|
|
211
|
+
else
|
|
212
|
+
echo "_No new files to commit_"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# ─── Done ─────────────────────────────────────────────────────────────
|
|
216
|
+
echo ""
|
|
217
|
+
echo "🎉 **Setup complete!**"
|