Compare commits
72 Commits
0df8f89c33
...
zhi-2026-0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a8f490cc2 | |||
| 1d34768019 | |||
| 4cca810c15 | |||
| 840707b604 | |||
| 795a710376 | |||
| fd0f84d6b8 | |||
|
|
1b82ac1de8 | ||
| 78a77c642e | |||
| 236fd7cc2f | |||
| f6fe46410e | |||
| b1bd85ae5c | |||
| 11021f7aa9 | |||
| 010e887339 | |||
| 01d52f42e5 | |||
| 240e52a76e | |||
| db3539917c | |||
| bec308fd06 | |||
| 315cf46889 | |||
| 71a15e2eaa | |||
| 8f40f29c65 | |||
| 6b5702fc40 | |||
| f446011bed | |||
| 97a7d89deb | |||
| fb72550eea | |||
| 8d71e68cd2 | |||
| 5ad82369a8 | |||
| 3e342e1162 | |||
| eb168a11a6 | |||
| 95cf7b85fa | |||
| 1e7af09245 | |||
| 7745bc8bbc | |||
| 8fc95aff22 | |||
| 0c5e6b8688 | |||
| 9bd1452042 | |||
| 82e287b265 | |||
| 597282cbd7 | |||
| 060b95f699 | |||
| 436c723a2f | |||
| 0b0f7d2a9f | |||
| d63f187bf2 | |||
| 87e75b77e8 | |||
| a86b80e83a | |||
| 0da1703140 | |||
| f0cb155fa4 | |||
| b14d3acbba | |||
| f0b27d5a1a | |||
| ae90cd7e0b | |||
| 5c405ff5fa | |||
| d20e827a5c | |||
| f3c19ba6e2 | |||
| 7c0b857a2f | |||
| bd84a50e98 | |||
| 5c8d1d3529 | |||
| 50fe0d87d7 | |||
| 39edaf3b83 | |||
| 8fed12b3ab | |||
| 457b989c1f | |||
| c8642368e9 | |||
| 5a5a2498c0 | |||
| fc720ac05a | |||
| 7f2ca44ef6 | |||
| db89b9e88e | |||
| f20bfeea80 | |||
| 87c98b26b8 | |||
| f6b13246d8 | |||
| 2e9be8375c | |||
| 8f3055be62 | |||
| cd88440361 | |||
| f73fd68e49 | |||
| 1d8427422a | |||
| 4d98d83fbb | |||
| dc5ce61bf7 |
3
.mandatory
Normal file
3
.mandatory
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
git-hangman-lab
|
||||||
|
keycloak-hangman-lab
|
||||||
|
claw-skills
|
||||||
34
claw-skills/SKILL.md
Normal file
34
claw-skills/SKILL.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: claw-skills
|
||||||
|
description: ClawSkills management — update, improve, create, and fix skills.
|
||||||
|
---
|
||||||
|
|
||||||
|
> All skill documentation must be written in English.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### update-skills
|
||||||
|
|
||||||
|
Pull latest ClawSkills and install/update skills to the agent workspace.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{baseDir}/scripts/update-skills
|
||||||
|
```
|
||||||
|
|
||||||
|
### promote-improvements
|
||||||
|
|
||||||
|
Create a branch named after the agent and force-push local changes for review.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{baseDir}/scripts/promote-improvements
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
- `{baseDir}/workflows/create-skills.md` — When you notice a reusable pattern with no existing skill covering it.
|
||||||
|
- `{baseDir}/workflows/fix-skills.md` — When a skill or its scripts fail or produce unexpected results.
|
||||||
|
- `{baseDir}/workflows/improve-skills.md` — When a skill has misleading descriptions, missing coverage, or suboptimal behavior.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- `{baseDir}/docs/standard.md` — Skill structure, layer responsibilities, and writing requirements.
|
||||||
104
claw-skills/docs/standard.md
Normal file
104
claw-skills/docs/standard.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# Skill Authoring Standard
|
||||||
|
|
||||||
|
## Anatomy of a Skill
|
||||||
|
|
||||||
|
A skill is a directory with up to four layers:
|
||||||
|
|
||||||
|
```
|
||||||
|
{skill-name}/
|
||||||
|
SKILL.md # Required — the menu
|
||||||
|
scripts/ # Optional — the kitchen tools
|
||||||
|
workflows/ # Optional — the recipes
|
||||||
|
docs/ # Optional — the reference manual
|
||||||
|
```
|
||||||
|
|
||||||
|
Each layer has a single responsibility:
|
||||||
|
|
||||||
|
| Layer | Role | Analogy | Contains | Does NOT contain |
|
||||||
|
|-------|------|---------|----------|------------------|
|
||||||
|
| **SKILL.md** | Router | Menu | What's available + when to use it | Process details, standards, how-tos |
|
||||||
|
| **scripts/** | Automation | Kitchen tools | Fixed-logic executables | Judgment calls, contextual decisions |
|
||||||
|
| **workflows/** | Process guide | Recipes | Step-by-step procedures requiring judgment | Standards, naming rules, structure specs |
|
||||||
|
| **docs/** | Reference | Reference manual | Facts, standards, specifications | Procedures, step-by-step instructions |
|
||||||
|
|
||||||
|
### How they relate
|
||||||
|
|
||||||
|
```
|
||||||
|
Agent reads SKILL.md
|
||||||
|
→ finds the right script or workflow
|
||||||
|
→ workflow references docs/ for standards when needed
|
||||||
|
```
|
||||||
|
|
||||||
|
## SKILL.md — The Menu
|
||||||
|
|
||||||
|
SKILL.md is the first thing an agent reads. It answers two questions: **what's available** and **when to use it**.
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: {skill-name}
|
||||||
|
description: {one-line description of when to use this skill}
|
||||||
|
---
|
||||||
|
|
||||||
|
> Execution notes (e.g., "All scripts must be executed via pcexec")
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
List each script with invocation syntax and a one-line description.
|
||||||
|
Group related scripts under a subheading if needed.
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
List each workflow file path with the trigger condition (when to use it).
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
Point to docs/ if the skill has reference material.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- **Concise.** One-line descriptions per script/workflow. No multi-paragraph explanations.
|
||||||
|
- **No process details.** Don't describe how to do something — that belongs in workflows/.
|
||||||
|
- **No standards or rules.** Don't define naming conventions or structure — that belongs in docs/.
|
||||||
|
- **Warnings and prerequisites** are allowed as short callouts (e.g., "do not run unless explicitly requested").
|
||||||
|
|
||||||
|
## scripts/ — The Kitchen Tools
|
||||||
|
|
||||||
|
Scripts handle operations that are **completely fixed** — the steps are predictable, the logic doesn't change based on context.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Each script has a single responsibility
|
||||||
|
- Must be executable (`chmod +x`)
|
||||||
|
- Must be called via `pcexec` unless documented otherwise
|
||||||
|
- Use `AGENT_ID`, `AGENT_WORKSPACE` environment variables when behavior varies by agent
|
||||||
|
- Use `ego-mgr get <field>` for agent identity info inside scripts
|
||||||
|
|
||||||
|
## workflows/ — The Recipes
|
||||||
|
|
||||||
|
Workflows guide operations that **require judgment** — the steps vary depending on context, or decisions need to be made along the way.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Each workflow is a standalone markdown file
|
||||||
|
- Must be self-contained enough to follow independently
|
||||||
|
- Reference docs/ for standards (e.g., `> See docs/standard.md for naming conventions`) — don't repeat them inline
|
||||||
|
- Focus on **steps and decision points**, not rules
|
||||||
|
|
||||||
|
## docs/ — The Reference Manual
|
||||||
|
|
||||||
|
Docs hold **facts and standards** that don't change based on who's reading or what task they're doing.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- Reference material only — no step-by-step procedures
|
||||||
|
- Standards, specifications, naming conventions, structure definitions
|
||||||
|
- Referenced by workflows and SKILL.md, never duplicated into them
|
||||||
|
|
||||||
|
## General Requirements
|
||||||
|
|
||||||
|
- **Language:** All skill documentation must be written in English
|
||||||
|
- **Naming:** Skill directories use kebab-case (e.g., `git-hangman-lab`)
|
||||||
|
- **Frontmatter:** SKILL.md must start with a YAML frontmatter block containing `name` and `description`
|
||||||
29
claw-skills/scripts/promote-improvements
Executable file
29
claw-skills/scripts/promote-improvements
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WORKSPACE="${AGENT_WORKSPACE:-}"
|
||||||
|
if [[ -z "$WORKSPACE" ]]; then
|
||||||
|
echo "Error: AGENT_WORKSPACE not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$WORKSPACE"
|
||||||
|
|
||||||
|
# Get agent name from ego-mgr
|
||||||
|
AGENT_NAME=$(ego-mgr get name)
|
||||||
|
if [[ -z "$AGENT_NAME" ]]; then
|
||||||
|
echo "Error: failed to get agent name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BRANCH_NAME="$AGENT_NAME"
|
||||||
|
|
||||||
|
# Checkout new branch based on agent name
|
||||||
|
echo "Creating and checking out branch: $BRANCH_NAME"
|
||||||
|
git checkout -b "$BRANCH_NAME"
|
||||||
|
|
||||||
|
# Force push to the new branch
|
||||||
|
echo "Force pushing to origin/$BRANCH_NAME"
|
||||||
|
git push origin "$BRANCH_NAME" --force
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
16
claw-skills/scripts/update-skills
Executable file
16
claw-skills/scripts/update-skills
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CLAW_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
|
GIT_LAB_DIR="${CLAW_DIR}/git-hangman-lab"
|
||||||
|
|
||||||
|
# Pull latest ClawSkills from git-hangman-lab
|
||||||
|
echo "Fetching latest ClawSkills..."
|
||||||
|
"${GIT_LAB_DIR}/scripts/git-ctrl" repo get-latest ClawSkills --force
|
||||||
|
|
||||||
|
# Run learn.sh to install skills
|
||||||
|
echo "Running learn.sh..."
|
||||||
|
"${CLAW_DIR}/ClawSkills/learn.sh"
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
51
claw-skills/workflows/create-skills.md
Normal file
51
claw-skills/workflows/create-skills.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Create Skills
|
||||||
|
|
||||||
|
When you notice a reusable pattern with no existing skill covering it.
|
||||||
|
|
||||||
|
> See `{baseDir}/docs/standard.md` for skill structure and layer responsibilities.
|
||||||
|
|
||||||
|
## Principle
|
||||||
|
|
||||||
|
Not every repetitive task needs a skill. Ask first: is this pattern general enough to warrant abstraction?
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Identify the Pattern
|
||||||
|
|
||||||
|
- Record the repeating sequence of operations
|
||||||
|
- Analyze: what are you doing repeatedly across tasks?
|
||||||
|
- Evaluate: is this general-purpose or a one-off?
|
||||||
|
|
||||||
|
### 2. Decide the Approach
|
||||||
|
|
||||||
|
Should this be a new skill, or a script/workflow added to an existing skill?
|
||||||
|
|
||||||
|
**Add a script when:**
|
||||||
|
- The steps are fixed and results are predictable
|
||||||
|
- The operation recurs across different agents/workspaces
|
||||||
|
- Involves external system calls (git, keycloak, file ops, etc.)
|
||||||
|
|
||||||
|
**Add a workflow when:**
|
||||||
|
- Steps vary depending on context or require judgment
|
||||||
|
- The scenario is too nuanced for rigid logic
|
||||||
|
|
||||||
|
**Not worth abstracting:**
|
||||||
|
- One-off tasks
|
||||||
|
|
||||||
|
### 3. Design
|
||||||
|
|
||||||
|
- **Name:** kebab-case (e.g., `git-clone-repo`)
|
||||||
|
- **SKILL.md:** Router only — list scripts with invocation syntax, workflows with trigger conditions
|
||||||
|
- **Scripts:** Single responsibility each, placed under `scripts/`
|
||||||
|
|
||||||
|
### 4. Implement
|
||||||
|
|
||||||
|
- Write the scripts, verify they work
|
||||||
|
- Write the SKILL.md
|
||||||
|
- Test locally
|
||||||
|
|
||||||
|
### 5. Submit
|
||||||
|
|
||||||
|
Call `{baseDir}/scripts/promote-improvements` to push to your branch.
|
||||||
|
|
||||||
|
Commit message includes: skill name, what it does, usage example.
|
||||||
42
claw-skills/workflows/fix-skills.md
Normal file
42
claw-skills/workflows/fix-skills.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Fix Skills
|
||||||
|
|
||||||
|
When a skill or its scripts fail to produce expected results, or throw errors.
|
||||||
|
|
||||||
|
> See `{baseDir}/docs/standard.md` for skill structure and layer responsibilities.
|
||||||
|
|
||||||
|
## Principle
|
||||||
|
|
||||||
|
**Do not resort to workarounds lightly.** Identify the root cause first, then fix properly.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Gather Information
|
||||||
|
|
||||||
|
- Record the triggering scenario: how it was called, expected result, actual result
|
||||||
|
- Record the error output in full (stack traces, debug output)
|
||||||
|
- Confirm the environment: model version, node, skill version (git log helps)
|
||||||
|
|
||||||
|
### 2. Analyze Root Cause
|
||||||
|
|
||||||
|
Investigate in this order:
|
||||||
|
|
||||||
|
1. **Are inputs/parameters correct?** — Do they match what the script expects?
|
||||||
|
2. **Are dependencies satisfied?** — Required secrets, tokens, config files present?
|
||||||
|
3. **Is there a bug in the script logic?** — Read the source, add `set -x` if needed
|
||||||
|
4. **Is there a design flaw?** — Does the logic actually cover this scenario?
|
||||||
|
|
||||||
|
### 3. Pinpoint the File
|
||||||
|
|
||||||
|
- Identify which file and which line the problem is in
|
||||||
|
- If it's a skill issue → follow the improve-skills workflow
|
||||||
|
- If it's an environment issue → document in memory/
|
||||||
|
|
||||||
|
### 4. Fix
|
||||||
|
|
||||||
|
- Test the fix locally
|
||||||
|
- If skill files need modification → follow improve-skills process
|
||||||
|
- Avoid introducing new side effects
|
||||||
|
|
||||||
|
### 5. Verify
|
||||||
|
|
||||||
|
- Reproduce the original scenario; confirm the issue is resolved
|
||||||
42
claw-skills/workflows/improve-skills.md
Normal file
42
claw-skills/workflows/improve-skills.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Improve Skills
|
||||||
|
|
||||||
|
When a skill has misleading descriptions, missing coverage, or suboptimal behavior.
|
||||||
|
|
||||||
|
> See `{baseDir}/docs/standard.md` for skill structure and layer responsibilities.
|
||||||
|
|
||||||
|
## Principle
|
||||||
|
|
||||||
|
Improvement is better than workaround. Fix problems when you find them, so others don't hit the same pitfall.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Identify the Problem
|
||||||
|
|
||||||
|
- Record which skill and which part has the issue
|
||||||
|
- Describe specifically: what's misleading, what's missing, what's not working well
|
||||||
|
- Record the scenario where you encountered it
|
||||||
|
|
||||||
|
### 2. Assess Impact
|
||||||
|
|
||||||
|
- How many agents will this affect?
|
||||||
|
- Is it a design issue or a documentation issue?
|
||||||
|
- What's the complexity of the fix?
|
||||||
|
|
||||||
|
### 3. Design the Fix
|
||||||
|
|
||||||
|
- **Documentation issue** → update SKILL.md or docs/
|
||||||
|
- **Logic flaw** → modify the script
|
||||||
|
- **Missing coverage** → add a new workflow or script
|
||||||
|
- **Process issue** → redesign the workflow
|
||||||
|
|
||||||
|
### 4. Implement
|
||||||
|
|
||||||
|
- Make changes locally
|
||||||
|
- Update SKILL.md if behavior changed
|
||||||
|
- Test if the change is significant
|
||||||
|
|
||||||
|
### 5. Submit
|
||||||
|
|
||||||
|
Call `{baseDir}/scripts/promote-improvements` to push to your branch.
|
||||||
|
|
||||||
|
Commit message should describe what was changed and why.
|
||||||
12
daily-routine/SKILL.md
Normal file
12
daily-routine/SKILL.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: daily-routine
|
||||||
|
description: Daily work lifecycle — planning, task execution, and slot management. Use when woken up for daily planning or task slot execution.
|
||||||
|
---
|
||||||
|
|
||||||
|
> Scripts interact with HarborForge via `hf` CLI (must run via pcexec).
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
- `{baseDir}/workflows/plan-schedule.md` — When woken up at daily planning time. Plan today's schedule based on suggested workload.
|
||||||
|
- `{baseDir}/workflows/task-handson.md` — When a task slot is due. Execute the assigned task following the appropriate skill workflow.
|
||||||
|
- `{baseDir}/workflows/slot-complete.md` — When finishing a slot. Handle defer decisions and transition to next slot.
|
||||||
71
daily-routine/workflows/plan-schedule.md
Normal file
71
daily-routine/workflows/plan-schedule.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Plan Schedule
|
||||||
|
|
||||||
|
When woken up for daily planning (typically 01:00 UTC). You receive your suggested workload for the day.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Review Suggested Workload
|
||||||
|
|
||||||
|
Read the suggested workload provided in your wakeup message. This includes:
|
||||||
|
- Number of oncall slots to schedule
|
||||||
|
- Number of task slots to schedule
|
||||||
|
|
||||||
|
This was designed by agent-resource-director for your specific role and capacity.
|
||||||
|
|
||||||
|
### 2. Review Existing Schedule
|
||||||
|
|
||||||
|
Check if any slots have already been planned for today:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf calendar show --date today
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Review Pending Tasks
|
||||||
|
|
||||||
|
List tasks assigned to you or that you've claimed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf tasks list --assignee me --status open
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Plan Slots
|
||||||
|
|
||||||
|
Create slots for today. Distribute them within the work period:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf calendar slot create --type work --time HH:MM --duration <min> --event-data '{"task_id": <id>}'
|
||||||
|
hf calendar slot create --type oncall --time HH:MM --duration <min>
|
||||||
|
```
|
||||||
|
|
||||||
|
For each task slot, note which HF task you intend to work on.
|
||||||
|
|
||||||
|
### 5. Assess Collaboration Needs
|
||||||
|
|
||||||
|
Review your planned work — does any task require discussion with other agents?
|
||||||
|
|
||||||
|
If yes:
|
||||||
|
- Check the shared daily planning channel
|
||||||
|
- Propose discussion time with relevant agents
|
||||||
|
- Agree on a time slot
|
||||||
|
|
||||||
|
### 6. Write Daily Plan to Memory
|
||||||
|
|
||||||
|
Append a brief summary to your daily note:
|
||||||
|
|
||||||
|
```
|
||||||
|
memory/YYYY-MM-DD.md:
|
||||||
|
## Daily Plan
|
||||||
|
- N task slots, M oncall slots scheduled
|
||||||
|
- Tasks to work on: [list]
|
||||||
|
- Discussions planned: [list or none]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Confirm Readiness
|
||||||
|
|
||||||
|
Once your schedule is submitted, you're done with planning. Set your status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf agent status --set idle
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for your first slot to be triggered by the wakeup mechanism.
|
||||||
63
daily-routine/workflows/slot-complete.md
Normal file
63
daily-routine/workflows/slot-complete.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Slot Complete
|
||||||
|
|
||||||
|
When you have finished working on the current slot.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Record Results
|
||||||
|
|
||||||
|
Update the task status if applicable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf tasks update --id <task_id> --status <completed|in-progress|blocked>
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the slot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf calendar slot update --id <slot_id> --status finished
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Write to Memory
|
||||||
|
|
||||||
|
Append a brief work summary to your daily note (`memory/YYYY-MM-DD.md`):
|
||||||
|
|
||||||
|
```
|
||||||
|
## Slot <slot_id> — <task summary>
|
||||||
|
- What was done: ...
|
||||||
|
- Key decisions: ...
|
||||||
|
- Output: commit <hash>, PR #<num>, file <path>, etc.
|
||||||
|
- Blockers: ... (if any)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Check for Overdue Slots
|
||||||
|
|
||||||
|
Before going idle, check if there are overdue slots:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf calendar show --date today --status not-started
|
||||||
|
```
|
||||||
|
|
||||||
|
For each overdue slot, decide:
|
||||||
|
- **Defer**: reschedule to a later time today (if work period allows)
|
||||||
|
- **Skip**: if no longer relevant
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf calendar slot update --id <slot_id> --status deferred --time HH:MM
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Decide Next Action
|
||||||
|
|
||||||
|
If there are more ready slots within the work period:
|
||||||
|
- Pick the highest priority one
|
||||||
|
- Follow `task-handson` workflow for it
|
||||||
|
- Do NOT set status to idle yet
|
||||||
|
|
||||||
|
If no more slots, or work period is ending:
|
||||||
|
- Set status to idle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf agent status --set idle
|
||||||
|
```
|
||||||
|
|
||||||
|
Your session can end. You will be woken up again if new slots become due.
|
||||||
55
daily-routine/workflows/task-handson.md
Normal file
55
daily-routine/workflows/task-handson.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Task Handson
|
||||||
|
|
||||||
|
When a task slot is due and you are woken up to work on it.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Acknowledge Slot
|
||||||
|
|
||||||
|
Set your status to busy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf agent status --set busy
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the slot status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf calendar slot update --id <slot_id> --status ongoing
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Identify the Task
|
||||||
|
|
||||||
|
Read the slot's event data to find the associated task:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf calendar slot show --id <slot_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the slot has an associated task_id, load the task details:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hf tasks show --id <task_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Find the Right Workflow
|
||||||
|
|
||||||
|
Based on the task type and your role, find the appropriate skill workflow:
|
||||||
|
- Code implementation → your role's development workflow
|
||||||
|
- Code review → git-hangman-lab PR review workflow
|
||||||
|
- Deployment → operator deployment workflow
|
||||||
|
- Skill maintenance → claw-skills improve/create workflow
|
||||||
|
- Administrative → role-specific administrative workflow
|
||||||
|
|
||||||
|
If no specific workflow applies, work on the task directly using your tools and judgment.
|
||||||
|
|
||||||
|
### 4. Execute
|
||||||
|
|
||||||
|
Follow the identified workflow to completion. During execution:
|
||||||
|
- Use appropriate tools for your task
|
||||||
|
- Commit code changes with meaningful messages
|
||||||
|
- Update task comments with progress if the work is substantial
|
||||||
|
|
||||||
|
### 5. Complete Slot
|
||||||
|
|
||||||
|
When done, follow the `slot-complete` workflow.
|
||||||
@@ -3,98 +3,53 @@ name: git-hangman-lab
|
|||||||
description: Git operations for hangman-lab.top - manage accounts, tokens, repositories, and Gitea settings.
|
description: Git operations for hangman-lab.top - manage accounts, tokens, repositories, and Gitea settings.
|
||||||
---
|
---
|
||||||
|
|
||||||
> ⚠️ **Note**: All scripts must be executed via the `pcexec` tool.
|
> All scripts must be executed via the `pcexec` tool.
|
||||||
|
|
||||||
## Git Operations
|
## Scripts
|
||||||
|
|
||||||
### Check Git Credentials
|
### Account & Credentials
|
||||||
|
|
||||||
Verify git credentials are configured correctly.
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `{baseDir}/scripts/git-ctrl check-git-cred` | Verify git credentials are configured correctly |
|
||||||
|
| `{baseDir}/scripts/git-ctrl create-git-account` | Create a new git account and configure access |
|
||||||
|
| `{baseDir}/scripts/git-ctrl generate-access-token` | Generate an access token for the current user |
|
||||||
|
| `{baseDir}/scripts/git-ctrl reset-password` | Reset password (reads username from secret-mgr) |
|
||||||
|
|
||||||
```bash
|
> **create-git-account**: Do not execute unless explicitly requested. Contact **agent-resource-director** or **hangman** first.
|
||||||
{baseDir}/scripts/git-ctrl check-git-cred
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Git Account
|
### Repository Operations
|
||||||
|
|
||||||
Create a new git account and configure access.
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
> ⚠️ **Warning**: Do not execute this command unless explicitly requested. If you don't have a git account, contact **agent-resource-director** or **hangman** to guide you through the process.
|
| `{baseDir}/scripts/git-ctrl repo create <repo-name>` | Create repo at `${AGENT_WORKSPACE}/<repo-name>` |
|
||||||
|
| `{baseDir}/scripts/git-ctrl repo get-latest <repo-name> [branch] [--recursive] [--force]` | Pull latest or clone if missing |
|
||||||
```bash
|
| `{baseDir}/scripts/git-ctrl repo add-collaborators --user <user> --repo <repo>` | Add a collaborator |
|
||||||
{baseDir}/scripts/git-ctrl create-git-account
|
| `{baseDir}/scripts/git-ctrl repo list-all` | List all visible repositories |
|
||||||
```
|
| `{baseDir}/scripts/git-ctrl repo config --repo-path <path> [--recursive]` | Configure repo credentials |
|
||||||
|
|
||||||
### Generate Access Token
|
|
||||||
|
|
||||||
Generate an access token for the current user.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/git-ctrl generate-access-token
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Repository
|
|
||||||
|
|
||||||
Create a new git repository on git.hangman-lab.top.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/git-ctrl create-repo <repo-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note**: The repository will be created at `${AGENT_WORKSPACE}/${repo-name}` (default: `/root/.openclaw/workspace/workspace-mentor`)
|
|
||||||
|
|
||||||
### Pull Request Operations
|
### Pull Request Operations
|
||||||
|
|
||||||
Manage pull requests on git.hangman-lab.top.
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `{baseDir}/scripts/git-ctrl pr create <repo-path> <head> <base> [title] [body]` | Create a PR |
|
||||||
|
| `{baseDir}/scripts/git-ctrl pr list <repo-path>` | List PRs |
|
||||||
|
| `{baseDir}/scripts/git-ctrl pr show <repo-path> <index>` | Show PR details |
|
||||||
|
| `{baseDir}/scripts/git-ctrl pr commits <repo-path> <index>` | List PR commits |
|
||||||
|
| `{baseDir}/scripts/git-ctrl pr merge <repo-path> <index> <do> [commit-id] [title] [message]` | Merge a PR |
|
||||||
|
|
||||||
```bash
|
> **pr merge `<do>`**: `merge`, `squash`, `rebase`, or `manually-merged`
|
||||||
{baseDir}/scripts/git-ctrl pr create <repo-local-path> <head-branch> <base-branch> [pr-title] [pr-body]
|
> Access token is auto-generated if not found.
|
||||||
{baseDir}/scripts/git-ctrl pr list <repo-local-path>
|
|
||||||
{baseDir}/scripts/git-ctrl pr commits <repo-local-path> <pr-index>
|
|
||||||
{baseDir}/scripts/git-ctrl pr merge <repo-local-path> <pr-index> <do> [commit-id] [title] [message]
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note**: The access token will be automatically generated if not found.
|
### Gitea Settings
|
||||||
|
|
||||||
> **`<do>`** can be: `merge`, `squash`, `rebase`, `manually-merged`
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `{baseDir}/scripts/git-ctrl link-keycloak` | Link Keycloak account with Gitea (OAuth binding) |
|
||||||
|
| `{baseDir}/scripts/git-ctrl external-login-ctrl --enable/--disable` | Enable or disable local login on Gitea |
|
||||||
|
|
||||||
### Link Keycloak Account
|
### Package Publishing
|
||||||
|
|
||||||
Link Keycloak account with Gitea (for OAuth binding).
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
```bash
|
| `{baseDir}/scripts/git-ctrl publish-package <repo-path>` | Publish a package to the Gitea registry |
|
||||||
{baseDir}/scripts/git-ctrl link-keycloak
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add Repository Collaborator
|
|
||||||
|
|
||||||
Add a collaborator to a repository.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/git-ctrl repo-add-collaborators --user <user> --repo <repo>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Repository Config
|
|
||||||
|
|
||||||
When you clone a repository from git.hangman-lab.top and are ready to develop, or after creating a new local repo with git init, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/git-ctrl repo-config --repo-path <path to your repo>
|
|
||||||
```
|
|
||||||
|
|
||||||
### External Login Control
|
|
||||||
|
|
||||||
Enable or disable local login on Gitea.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/git-ctrl external-login-ctrl --enable
|
|
||||||
{baseDir}/scripts/git-ctrl external-login-ctrl --disable
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset Password
|
|
||||||
|
|
||||||
Reset password for the current user (reads username from secret-mgr).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/git-ctrl reset-password
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Get the directory where this script is located
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
|
|
||||||
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
|
||||||
echo "Error: script must be executed by pcexec"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 <repo-name>"
|
|
||||||
echo ""
|
|
||||||
echo "Create a new git repository on git.hangman-lab.top"
|
|
||||||
echo ""
|
|
||||||
echo "Arguments:"
|
|
||||||
echo " repo-name Name of the repository to create"
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ $# -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPO_NAME="$1"
|
|
||||||
|
|
||||||
# Validate repo name (alphanumeric, hyphens, underscores only)
|
|
||||||
if ! [[ "$REPO_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
||||||
echo "Error: Invalid repository name '$REPO_NAME'"
|
|
||||||
echo "Only alphanumeric characters, hyphens, and underscores are allowed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
|
||||||
|
|
||||||
# Step 1: Create directory
|
|
||||||
echo "Creating directory: ${REPO_DIR}"
|
|
||||||
mkdir -p "${REPO_DIR}"
|
|
||||||
|
|
||||||
# Step 2: cd to directory
|
|
||||||
cd "${REPO_DIR}"
|
|
||||||
|
|
||||||
# Step 3: git init
|
|
||||||
echo "Initializing git repository..."
|
|
||||||
git init
|
|
||||||
|
|
||||||
# Step 4: git remote add origin
|
|
||||||
echo "Adding remote origin..."
|
|
||||||
USERNAME="$(secret-mgr get-username --key git)"
|
|
||||||
REMOTE_URL="https://git.hangman-lab.top/${USERNAME}/${REPO_NAME}.git"
|
|
||||||
git remote add origin "${REMOTE_URL}"
|
|
||||||
echo " Remote: ${REMOTE_URL}"
|
|
||||||
|
|
||||||
# Step 5: Run repo-config
|
|
||||||
echo "Configuring repository..."
|
|
||||||
"$SCRIPT_DIR/repo-config" --repo-path "${REPO_DIR}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Done! Repository created at: ${REPO_DIR}"
|
|
||||||
echo "Remote: ${REMOTE_URL}"
|
|
||||||
@@ -33,7 +33,7 @@ if [[ "$enable" == "true" && "$disable" == "true" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REMOTE_HOST="vps.git"
|
REMOTE_HOST="server.t0"
|
||||||
REMOTE_USER="root"
|
REMOTE_USER="root"
|
||||||
GITEA_URL="https://git.hangman-lab.top/user/login"
|
GITEA_URL="https://git.hangman-lab.top/user/login"
|
||||||
MAX_RETRIES=10
|
MAX_RETRIES=10
|
||||||
|
|||||||
@@ -1,17 +1,47 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# Get the directory where this script is located
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
# Verify git credentials first
|
|
||||||
"$SCRIPT_DIR/check-git-cred"
|
"$SCRIPT_DIR/check-git-cred"
|
||||||
|
|
||||||
username=$(secret-mgr get-username --key git)
|
USERNAME=$(ego-mgr get default-username)
|
||||||
token_output=$("$SCRIPT_DIR/gitea" admin user generate-access-token --username "$username" --token-name "$username")
|
PASSWORD=$(secret-mgr get-secret --key git)
|
||||||
|
|
||||||
# Extract token from output (format: "Access token was successfully created: <token>")
|
# Check if token already exists
|
||||||
token=$(echo "$token_output" | awk '{print $NF}')
|
EXISTING=$(curl -s -u "${USERNAME}:${PASSWORD}" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/users/${USERNAME}/tokens")
|
||||||
|
|
||||||
secret-mgr set --key git-access-token --username "$username" --secret "$token"
|
EXISTING_ID=$(echo "$EXISTING" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
tokens = json.load(sys.stdin)
|
||||||
|
name = '$USERNAME'
|
||||||
|
for t in tokens:
|
||||||
|
if t.get('name') == name:
|
||||||
|
print(t.get('id'))
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING_ID" ]]; then
|
||||||
|
curl -s -u "${USERNAME}:${PASSWORD}" -X DELETE \
|
||||||
|
"https://git.hangman-lab.top/api/v1/users/${USERNAME}/tokens/${EXISTING_ID}" > /dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create new token
|
||||||
|
RESP=$(curl -s -u "${USERNAME}:${PASSWORD}" -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"name\": \"${USERNAME}\", \"scopes\": [\"all\"]}" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/users/${USERNAME}/tokens")
|
||||||
|
|
||||||
|
TOKEN=$(echo "$RESP" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
print(d.get('sha1', ''))
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
echo "Failed to generate access token: $RESP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
secret-mgr set --key git-access-token --username "$USERNAME" --secret "$TOKEN"
|
||||||
echo "Access token generated and stored successfully"
|
echo "Access token generated and stored successfully"
|
||||||
|
|||||||
@@ -1,65 +1,32 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Get the directory where this script is located
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
# Check if subcommand is provided
|
|
||||||
if [[ $# -eq 0 ]]; then
|
if [[ $# -eq 0 ]]; then
|
||||||
echo "Usage: $0 <command> [options]"
|
echo "Usage: $0 <command> [options]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Commands:"
|
echo "Commands:"
|
||||||
echo " check-git-cred Verify git credentials"
|
echo " check-git-cred Verify git credentials"
|
||||||
echo " create-git-account Create a new git account"
|
echo " create-git-account Create a new git account"
|
||||||
echo " create-repo Create a new repository"
|
echo " pr Pull request operations (create/list/commits/merge/show)"
|
||||||
echo " pr Pull request operations (create/list/commits/merge)"
|
|
||||||
echo " generate-access-token Generate access token for current user"
|
echo " generate-access-token Generate access token for current user"
|
||||||
echo " link-keycloak Link Keycloak account with Gitea"
|
echo " link-keycloak Link Keycloak account with Gitea"
|
||||||
echo " repo-add-collaborators Add collaborator to repository"
|
echo " repo Repository operations (create/add-collaborators/list-all/config)"
|
||||||
echo " repo-config Configure repository"
|
|
||||||
echo " external-login-ctrl Enable/disable local login"
|
echo " external-login-ctrl Enable/disable local login"
|
||||||
echo " reset-password Reset user password"
|
echo " reset-password Reset user password"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get subcommand
|
subcommand="$1"; shift
|
||||||
subcommand="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
# Route to appropriate script
|
|
||||||
case "$subcommand" in
|
case "$subcommand" in
|
||||||
check-git-cred)
|
check-git-cred) "$SCRIPT_DIR/check-git-cred" "$@" ;;
|
||||||
"$SCRIPT_DIR/check-git-cred" "$@"
|
create-git-account) "$SCRIPT_DIR/create-git-account" "$@" ;;
|
||||||
;;
|
pr) "$SCRIPT_DIR/pr" "$@" ;;
|
||||||
create-git-account)
|
generate-access-token) "$SCRIPT_DIR/generate-access-token" "$@" ;;
|
||||||
"$SCRIPT_DIR/create-git-account" "$@"
|
link-keycloak) "$SCRIPT_DIR/link-keycloak" "$@" ;;
|
||||||
;;
|
repo) "$SCRIPT_DIR/repo" "$@" ;;
|
||||||
create-repo)
|
external-login-ctrl) "$SCRIPT_DIR/external-login-ctrl" "$@" ;;
|
||||||
"$SCRIPT_DIR/create-repo" "$@"
|
reset-password) "$SCRIPT_DIR/reset-password" "$@" ;;
|
||||||
;;
|
*) echo "Unknown command: $subcommand"; exit 1 ;;
|
||||||
pr)
|
esac
|
||||||
"$SCRIPT_DIR/pr" "$@"
|
|
||||||
;;
|
|
||||||
generate-access-token)
|
|
||||||
"$SCRIPT_DIR/generate-access-token" "$@"
|
|
||||||
;;
|
|
||||||
link-keycloak)
|
|
||||||
"$SCRIPT_DIR/link-keycloak" "$@"
|
|
||||||
;;
|
|
||||||
repo-add-collaborators)
|
|
||||||
"$SCRIPT_DIR/repo-add-collaborators" "$@"
|
|
||||||
;;
|
|
||||||
repo-config)
|
|
||||||
"$SCRIPT_DIR/repo-config" "$@"
|
|
||||||
;;
|
|
||||||
external-login-ctrl)
|
|
||||||
"$SCRIPT_DIR/external-login-ctrl" "$@"
|
|
||||||
;;
|
|
||||||
reset-password)
|
|
||||||
"$SCRIPT_DIR/reset-password" "$@"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown command: $subcommand"
|
|
||||||
echo "Run '$0' for usage information"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REMOTE_HOST="vps.git"
|
REMOTE_HOST="server.t0"
|
||||||
REMOTE_USER="root"
|
REMOTE_USER="root"
|
||||||
CONTAINER_NAME="git-kc-gitea"
|
CONTAINER_NAME="git-kc-gitea"
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ ESCAPED_USERNAME=$(sql_escape "$USERNAME")
|
|||||||
cleanup() {
|
cleanup() {
|
||||||
"$SCRIPT_DIR/external-login-ctrl" --disable >/dev/null 2>&1 || true
|
"$SCRIPT_DIR/external-login-ctrl" --disable >/dev/null 2>&1 || true
|
||||||
if [[ -n "${ORIG_LOGIN_TYPE:-}" && -n "${ORIG_LOGIN_SOURCE:-}" ]]; then
|
if [[ -n "${ORIG_LOGIN_TYPE:-}" && -n "${ORIG_LOGIN_SOURCE:-}" ]]; then
|
||||||
ssh root@vps.git "
|
ssh root@server.t0 "
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
. /root/git-kc/.env
|
. /root/git-kc/.env
|
||||||
docker exec -i git-kc-mysql mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"UPDATE user SET login_type=${ORIG_LOGIN_TYPE}, login_source=${ORIG_LOGIN_SOURCE}, login_name=${ORIG_LOGIN_NAME_SQL:-NULL} WHERE name='${ESCAPED_USERNAME}';\"
|
docker exec -i git-kc-mysql mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"UPDATE user SET login_type=${ORIG_LOGIN_TYPE}, login_source=${ORIG_LOGIN_SOURCE}, login_name=${ORIG_LOGIN_NAME_SQL:-NULL} WHERE name='${ESCAPED_USERNAME}';\"
|
||||||
@@ -48,7 +48,7 @@ rm -f "$COOKIES_FILE" "$KC_LOGIN_HTML" "$KC_POST_LOGIN_HTML" "$KC_POST_LOGIN_LOG
|
|||||||
"$GITEA_CALLBACK_HTML" "$GITEA_LINK_HTML" "$GITEA_LINK_RESP_HTML" "$GITEA_LINK_RESP_LOG"
|
"$GITEA_CALLBACK_HTML" "$GITEA_LINK_HTML" "$GITEA_LINK_RESP_HTML" "$GITEA_LINK_RESP_LOG"
|
||||||
|
|
||||||
# Capture original login fields so we can restore them exactly.
|
# Capture original login fields so we can restore them exactly.
|
||||||
ORIG_STATE=$(ssh root@vps.git "
|
ORIG_STATE=$(ssh root@server.t0 "
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
. /root/git-kc/.env
|
. /root/git-kc/.env
|
||||||
docker exec -i git-kc-mysql mysql -N -B -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"SELECT login_type, login_source, COALESCE(login_name, '__NULL__') FROM user WHERE name='${ESCAPED_USERNAME}' LIMIT 1;\"
|
docker exec -i git-kc-mysql mysql -N -B -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"SELECT login_type, login_source, COALESCE(login_name, '__NULL__') FROM user WHERE name='${ESCAPED_USERNAME}' LIMIT 1;\"
|
||||||
@@ -114,7 +114,7 @@ if [[ -z "${CSRF_TOKEN:-}" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[INFO] 临时将 login_type 改为本地登录..."
|
echo "[INFO] 临时将 login_type 改为本地登录..."
|
||||||
ssh root@vps.git "
|
ssh root@server.t0 "
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
. /root/git-kc/.env
|
. /root/git-kc/.env
|
||||||
docker exec -i git-kc-mysql mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"UPDATE user SET login_type=0, login_source=0, login_name=NULL WHERE name='${ESCAPED_USERNAME}';\"
|
docker exec -i git-kc-mysql mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"UPDATE user SET login_type=0, login_source=0, login_name=NULL WHERE name='${ESCAPED_USERNAME}';\"
|
||||||
@@ -130,7 +130,7 @@ curl -v "https://git.hangman-lab.top/user/link_account_signin" \
|
|||||||
2>"$GITEA_LINK_RESP_LOG" || true
|
2>"$GITEA_LINK_RESP_LOG" || true
|
||||||
|
|
||||||
echo "[INFO] 恢复原始登录方式..."
|
echo "[INFO] 恢复原始登录方式..."
|
||||||
ssh root@vps.git "
|
ssh root@server.t0 "
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
. /root/git-kc/.env
|
. /root/git-kc/.env
|
||||||
docker exec -i git-kc-mysql mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"UPDATE user SET login_type=${ORIG_LOGIN_TYPE}, login_source=${ORIG_LOGIN_SOURCE}, login_name=${ORIG_LOGIN_NAME_SQL} WHERE name='${ESCAPED_USERNAME}';\"
|
docker exec -i git-kc-mysql mysql -uroot -p\"\$MYSQL_ROOT_PASSWORD\" giteadb -e \"UPDATE user SET login_type=${ORIG_LOGIN_TYPE}, login_source=${ORIG_LOGIN_SOURCE}, login_name=${ORIG_LOGIN_NAME_SQL} WHERE name='${ESCAPED_USERNAME}';\"
|
||||||
|
|||||||
@@ -134,6 +134,40 @@ cmd_commits() {
|
|||||||
echo "$RESPONSE" | jq -r '.[] | "\(.sha[0:7])\t\(.commit.message | split("\n")[0])"' 2>/dev/null || echo "$RESPONSE"
|
echo "$RESPONSE" | jq -r '.[] | "\(.sha[0:7])\t\(.commit.message | split("\n")[0])"' 2>/dev/null || echo "$RESPONSE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Subcommand: show
|
||||||
|
cmd_show() {
|
||||||
|
local repo_path="$1"
|
||||||
|
local pr_index="$2"
|
||||||
|
|
||||||
|
get_repo_info "$repo_path"
|
||||||
|
local token
|
||||||
|
token="$(ensure_token)"
|
||||||
|
|
||||||
|
echo "Showing PR #$pr_index in: $OWNER/$REPO_NAME"
|
||||||
|
|
||||||
|
RESPONSE=$(curl -s -X GET "https://git.hangman-lab.top/api/v1/repos/${OWNER}/${REPO_NAME}/pulls/${pr_index}" \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H "Authorization: token ${token}")
|
||||||
|
|
||||||
|
if echo "$RESPONSE" | jq -e '.message' >/dev/null 2>&1; then
|
||||||
|
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message')
|
||||||
|
echo "Error: $ERROR_MSG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$RESPONSE" | jq '{
|
||||||
|
id, number, title, body, state, draft, mergeable, merged,
|
||||||
|
additions, deletions, changed_files, comments,
|
||||||
|
html_url, diff_url, patch_url,
|
||||||
|
labels,
|
||||||
|
milestone,
|
||||||
|
base: .base | {label, ref, sha},
|
||||||
|
head: .head | {label, ref, sha},
|
||||||
|
merge_base,
|
||||||
|
created_at, updated_at, closed_at
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
# Subcommand: merge
|
# Subcommand: merge
|
||||||
cmd_merge() {
|
cmd_merge() {
|
||||||
local repo_path="$1"
|
local repo_path="$1"
|
||||||
@@ -180,27 +214,61 @@ cmd_merge() {
|
|||||||
merge_when_checks_succeed: true
|
merge_when_checks_succeed: true
|
||||||
}')
|
}')
|
||||||
|
|
||||||
RESPONSE=$(curl -s -X POST "https://git.hangman-lab.top/api/v1/repos/${OWNER}/${REPO_NAME}/pulls/${pr_index}/merge" \
|
TEMP_FILE=$(mktemp)
|
||||||
|
HTTP_CODE=$(curl -s -o "$TEMP_FILE" -w "%{http_code}" -X POST "https://git.hangman-lab.top/api/v1/repos/${OWNER}/${REPO_NAME}/pulls/${pr_index}/merge" \
|
||||||
-H 'accept: application/json' \
|
-H 'accept: application/json' \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-H "Authorization: token ${token}" \
|
-H "Authorization: token ${token}" \
|
||||||
-d "$json")
|
-d "$json")
|
||||||
|
|
||||||
if echo "$RESPONSE" | jq -e '.message' >/dev/null 2>&1; then
|
RESPONSE=$(cat "$TEMP_FILE")
|
||||||
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message')
|
rm -f "$TEMP_FILE"
|
||||||
echo "Error: $ERROR_MSG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
MERGED=$(echo "$RESPONSE" | jq -r '.merged')
|
case "$HTTP_CODE" in
|
||||||
|
200)
|
||||||
if [[ "$MERGED" == "true" ]]; then
|
MERGED=$(echo "$RESPONSE" | jq -r '.merged')
|
||||||
echo "Pull request merged successfully!"
|
if [[ "$MERGED" == "true" ]]; then
|
||||||
else
|
echo "merge success"
|
||||||
echo "Error: Failed to merge pull request"
|
else
|
||||||
echo "$RESPONSE" | jq '.'
|
echo "merge failed"
|
||||||
exit 1
|
echo "$RESPONSE" | jq '.'
|
||||||
fi
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
409)
|
||||||
|
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Conflicting changes"')
|
||||||
|
echo "Error [$HTTP_CODE]: $ERROR_MSG"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
404)
|
||||||
|
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Not found"')
|
||||||
|
echo "Error [$HTTP_CODE]: $ERROR_MSG"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
423)
|
||||||
|
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Repository is archived"')
|
||||||
|
echo "Error [$HTTP_CODE]: $ERROR_MSG"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
405)
|
||||||
|
echo "merge failed check the pr status"
|
||||||
|
# Fetch PR details to show merge status
|
||||||
|
PR_INFO=$(curl -s -X GET "https://git.hangman-lab.top/api/v1/repos/${OWNER}/${REPO_NAME}/pulls/${pr_index}" \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H "Authorization: token ${token}")
|
||||||
|
echo "$PR_INFO" | jq '{mergeable: .mergeable, head_sha: .head.sha, base_ref: .base.ref, head_ref: .head.ref}'
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if echo "$RESPONSE" | jq -e '.message' >/dev/null 2>&1; then
|
||||||
|
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message')
|
||||||
|
echo "Error [$HTTP_CODE]: $ERROR_MSG"
|
||||||
|
else
|
||||||
|
echo "Error [$HTTP_CODE]: Unknown error"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
@@ -214,6 +282,7 @@ usage() {
|
|||||||
echo " commits <repo-local-path> <pr-index> List commits in a PR"
|
echo " commits <repo-local-path> <pr-index> List commits in a PR"
|
||||||
echo " merge <repo-local-path> <pr-index> <do> [commit-id] [title] [message]"
|
echo " merge <repo-local-path> <pr-index> <do> [commit-id] [title] [message]"
|
||||||
echo " Merge a pull request"
|
echo " Merge a pull request"
|
||||||
|
echo " show <repo-local-path> <pr-index> Show PR details"
|
||||||
echo ""
|
echo ""
|
||||||
echo " <do> can be: merge, squash, rebase, manually-merged"
|
echo " <do> can be: merge, squash, rebase, manually-merged"
|
||||||
exit 2
|
exit 2
|
||||||
@@ -256,6 +325,13 @@ case "$COMMAND" in
|
|||||||
fi
|
fi
|
||||||
cmd_merge "$@"
|
cmd_merge "$@"
|
||||||
;;
|
;;
|
||||||
|
show)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Error: show requires <repo-local-path> <pr-index>"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
cmd_show "$@"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Error: Unknown command: $COMMAND"
|
echo "Error: Unknown command: $COMMAND"
|
||||||
usage
|
usage
|
||||||
|
|||||||
207
git-hangman-lab/scripts/publish-package
Executable file
207
git-hangman-lab/scripts/publish-package
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
COMMAND=""
|
||||||
|
REGISTRY=""
|
||||||
|
IMAGE=""
|
||||||
|
TAG=""
|
||||||
|
PACKAGE_FILE=""
|
||||||
|
REPO=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage:"
|
||||||
|
echo " publish-package docker <registry> <image> <tag> --proj <repo>"
|
||||||
|
echo " publish-package nuget <source> <package-file> --proj <repo>"
|
||||||
|
echo " publish-package pypi <package-file> --proj <repo>"
|
||||||
|
echo " publish-package npm --proj <repo>"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
docker|nuget|pypi|npm)
|
||||||
|
COMMAND="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--proj)
|
||||||
|
REPO="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$COMMAND" ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
case $COMMAND in
|
||||||
|
docker)
|
||||||
|
[[ -z "$REGISTRY" ]] && REGISTRY="$1" && shift && continue
|
||||||
|
[[ -z "$IMAGE" ]] && IMAGE="$1" && shift && continue
|
||||||
|
[[ -z "$TAG" ]] && TAG="$1" && shift && continue
|
||||||
|
;;
|
||||||
|
nuget)
|
||||||
|
[[ -z "$SOURCE" ]] && SOURCE="$1" && shift && continue
|
||||||
|
[[ -z "$PACKAGE_FILE" ]] && PACKAGE_FILE="$1" && shift && continue
|
||||||
|
;;
|
||||||
|
pypi)
|
||||||
|
[[ -z "$PACKAGE_FILE" ]] && PACKAGE_FILE="$1" && shift && continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$COMMAND" ]] || [[ -z "$REPO" ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
do_docker() {
|
||||||
|
if [[ -z "$REGISTRY" ]] || [[ -z "$IMAGE" ]] || [[ -z "$TAG" ]]; then
|
||||||
|
echo "Error: docker requires <registry> <image> <tag>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OWNER=$(ego-mgr get default-username)
|
||||||
|
if [[ -z "$OWNER" ]]; then
|
||||||
|
echo "Error: cannot get username from ego-mgr"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
KEY=$(python3 -c "import uuid; print(uuid.uuid4())")
|
||||||
|
|
||||||
|
LOCKFILE="$HOME/.openclaw/.docker"
|
||||||
|
lock-mgr acquire "$LOCKFILE" "$KEY"
|
||||||
|
|
||||||
|
# Push via SSH tunnel to bypass Cloudflare's 100MB request body limit.
|
||||||
|
# Tunnel forwards 127.0.0.1:$TUNNEL_PORT on this host to Gitea's HTTP port on server.t0.
|
||||||
|
TUNNEL_HOST="root@server.t0"
|
||||||
|
TUNNEL_PORT="5000"
|
||||||
|
TUNNEL_LOCAL="127.0.0.1:${TUNNEL_PORT}"
|
||||||
|
TUNNEL_CTL="$HOME/.openclaw/.docker-tunnel.sock"
|
||||||
|
rm -f "$TUNNEL_CTL"
|
||||||
|
ssh -fN -o ExitOnForwardFailure=yes -o ControlMaster=yes -o ControlPath="$TUNNEL_CTL" \
|
||||||
|
-L "${TUNNEL_LOCAL}:127.0.0.1:3000" "$TUNNEL_HOST"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
ssh -O exit -o ControlPath="$TUNNEL_CTL" "$TUNNEL_HOST" 2>/dev/null || true
|
||||||
|
rm -f "$TUNNEL_CTL"
|
||||||
|
docker logout "$REGISTRY" 2>/dev/null || true
|
||||||
|
docker logout "$TUNNEL_LOCAL" 2>/dev/null || true
|
||||||
|
lock-mgr release "$LOCKFILE" "$KEY" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "Logging in to $REGISTRY (via tunnel $TUNNEL_LOCAL)..."
|
||||||
|
docker login "$TUNNEL_LOCAL" -u "$OWNER" -p "$(secret-mgr get-secret --key git)" 2>/dev/null
|
||||||
|
|
||||||
|
FULL_IMAGE="${REGISTRY}/${OWNER}/${IMAGE}:${TAG}"
|
||||||
|
TUNNEL_IMAGE="${TUNNEL_LOCAL}/${OWNER}/${IMAGE}:${TAG}"
|
||||||
|
echo "Building: $FULL_IMAGE"
|
||||||
|
|
||||||
|
cd "$REPO"
|
||||||
|
docker build -t "$FULL_IMAGE" .
|
||||||
|
docker tag "$FULL_IMAGE" "$TUNNEL_IMAGE"
|
||||||
|
|
||||||
|
echo "Pushing via tunnel: $TUNNEL_IMAGE"
|
||||||
|
docker push "$TUNNEL_IMAGE"
|
||||||
|
docker rmi "$TUNNEL_IMAGE" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Link package to repository
|
||||||
|
TOKEN=$(secret-mgr get-secret --key git-access-token)
|
||||||
|
REPO_NAME=$(basename "$REPO")
|
||||||
|
|
||||||
|
# Determine the actual owner of the git repository
|
||||||
|
REPO_OWNER="$OWNER"
|
||||||
|
if [[ -d "$REPO/.git" ]]; then
|
||||||
|
REMOTE_URL=$(git -C "$REPO" remote get-url origin 2>/dev/null || true)
|
||||||
|
if [[ "$REMOTE_URL" =~ git\.hangman-lab\.top/([^/]+)/ ]]; then
|
||||||
|
REPO_OWNER="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gitea's link/unlink endpoints are not idempotent: POST /-/link/{repo}
|
||||||
|
# returns HTTP 400 "invalid argument" when the package already has ANY link
|
||||||
|
# (even to the same repo), and POST /-/unlink returns 400 when nothing is
|
||||||
|
# linked. So read the current link first and branch accordingly.
|
||||||
|
TARGET_LINK="${REPO_OWNER}/${REPO_NAME}"
|
||||||
|
STATE_RESP=$(curl -s -w "\n%{http_code}" -u "${OWNER}:${TOKEN}" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/${TAG}")
|
||||||
|
STATE_STATUS=$(printf "%s" "$STATE_RESP" | tail -n1)
|
||||||
|
STATE_BODY=$(printf "%s" "$STATE_RESP" | sed '$d')
|
||||||
|
|
||||||
|
CURRENT_LINK=""
|
||||||
|
if [[ "$STATE_STATUS" == "200" ]]; then
|
||||||
|
CURRENT_LINK=$(printf "%s" "$STATE_BODY" | python3 -c \
|
||||||
|
'import sys, json; p=json.load(sys.stdin); r=p.get("repository") or {}; print(r.get("full_name") or "")' \
|
||||||
|
2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CURRENT_LINK" == "$TARGET_LINK" ]]; then
|
||||||
|
echo "Package already linked to ${TARGET_LINK}, skipping link step."
|
||||||
|
else
|
||||||
|
if [[ -n "$CURRENT_LINK" ]]; then
|
||||||
|
echo "Package currently linked to ${CURRENT_LINK}, unlinking first..."
|
||||||
|
UNLINK_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST -u "${OWNER}:${TOKEN}" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/-/unlink")
|
||||||
|
if [[ "$UNLINK_STATUS" != "204" ]]; then
|
||||||
|
echo "Warning: unlink returned HTTP $UNLINK_STATUS, proceeding with link attempt anyway..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
LINK_RESP=$(curl -s -w "%{http_code}" -X POST \
|
||||||
|
-u "${OWNER}:${TOKEN}" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/packages/${OWNER}/container/${IMAGE}/-/link/${REPO_NAME}")
|
||||||
|
LINK_STATUS="${LINK_RESP: -3}"
|
||||||
|
LINK_BODY="${LINK_RESP:0:-3}"
|
||||||
|
|
||||||
|
if [[ "$LINK_STATUS" != "200" && "$LINK_STATUS" != "201" ]]; then
|
||||||
|
if echo "$LINK_BODY" | grep -q '"message".*repository does not exist'; then
|
||||||
|
echo "Warning: repository '$REPO_NAME' is not owned by '$OWNER' — skipping link (requires site admin or matching owner)."
|
||||||
|
else
|
||||||
|
echo "Warning: package link failed (HTTP $LINK_STATUS): $LINK_BODY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Linked package to ${TARGET_LINK}."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
lock-mgr release "$LOCKFILE" "$KEY"
|
||||||
|
echo "Done: $FULL_IMAGE"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_nuget() {
|
||||||
|
echo "publish-package nuget: not yet implemented"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
do_pypi() {
|
||||||
|
echo "publish-package pypi: not yet implemented"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
do_npm() {
|
||||||
|
echo "publish-package npm: not yet implemented"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# For docker, determine the actual repo owner via search API and switch to that owner's agent-id
|
||||||
|
if [[ "$COMMAND" == "docker" ]]; then
|
||||||
|
REPO_NAME=$(basename "$REPO")
|
||||||
|
SCRIPT_DIR_CALLER=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
search_result=$("$SCRIPT_DIR_CALLER/repo" search "$REPO_NAME" 2>&1) || true
|
||||||
|
if [[ -n "$search_result" ]] && echo "$search_result" | python3 -q -c "import sys,json; sys.exit(0 if json.load(sys.stdin) else 1)" 2>/dev/null; then
|
||||||
|
repo_owner=$(echo "$search_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('owner',''))" 2>/dev/null)
|
||||||
|
if [[ -n "$repo_owner" ]]; then
|
||||||
|
owner_agent_id=$(ego-mgr lookup "$repo_owner" 2>/dev/null || echo "")
|
||||||
|
if [[ -n "$owner_agent_id" ]]; then
|
||||||
|
export AGENT_ID="$owner_agent_id"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$COMMAND" in
|
||||||
|
docker) do_docker ;;
|
||||||
|
nuget) do_nuget ;;
|
||||||
|
pypi) do_pypi ;;
|
||||||
|
npm) do_npm ;;
|
||||||
|
esac
|
||||||
468
git-hangman-lab/scripts/repo
Executable file
468
git-hangman-lab/scripts/repo
Executable file
@@ -0,0 +1,468 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
MYSQL_CONTAINER="git-kc-mysql"
|
||||||
|
MYSQL_USER="root"
|
||||||
|
MYSQL_DB="giteadb"
|
||||||
|
MYSQL_ROOT_PASS="K0DprNKJ^vAu3Mx32hMZ%LCzWKElFRfA"
|
||||||
|
GIT_HOST="root@server.t0"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# create
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
do_create() {
|
||||||
|
local REPO_NAME="" VISIBILITY="private"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--visibility)
|
||||||
|
VISIBILITY="$2"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 create <repo-name> [--visibility <public|private>]"
|
||||||
|
exit 0 ;;
|
||||||
|
*)
|
||||||
|
if [[ -z "$REPO_NAME" ]]; then
|
||||||
|
REPO_NAME="$1"; shift
|
||||||
|
else
|
||||||
|
echo "Error: unexpected argument '$1'"
|
||||||
|
exit 1
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$REPO_NAME" ]]; then
|
||||||
|
echo "Usage: $0 create <repo-name> [--visibility <public|private>]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$VISIBILITY" != "public" && "$VISIBILITY" != "private" ]]; then
|
||||||
|
echo "Error: --visibility must be 'public' or 'private' (got '$VISIBILITY')"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
||||||
|
echo "Error: script must be executed by pcexec"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate repo name
|
||||||
|
if ! [[ "$REPO_NAME" =~ ^[a-zA-Z0-9_.-]+$ ]]; then
|
||||||
|
echo "Error: Invalid repository name '$REPO_NAME'"
|
||||||
|
echo "Only alphanumeric, hyphens, underscores, and dots are allowed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
||||||
|
|
||||||
|
# Pre-create the repo on Gitea so --visibility is honoured. Relying on
|
||||||
|
# push-to-create always yields a private repo regardless of flag.
|
||||||
|
if ! secret-mgr list | grep -q "git-access-token"; then
|
||||||
|
echo "Error: git-access-token not found. Generate one first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local USERNAME TOKEN PRIVATE_JSON STATUS BODY
|
||||||
|
USERNAME="$(secret-mgr get-username --key git)"
|
||||||
|
TOKEN="$(secret-mgr get-secret --key git-access-token)"
|
||||||
|
if [[ "$VISIBILITY" == "public" ]]; then
|
||||||
|
PRIVATE_JSON="false"
|
||||||
|
else
|
||||||
|
PRIVATE_JSON="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating ${USERNAME}/${REPO_NAME} on git.hangman-lab.top (visibility: ${VISIBILITY})..."
|
||||||
|
RESP=$(curl -s -w "
|
||||||
|
%{http_code}" -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"name\": \"${REPO_NAME}\", \"private\": ${PRIVATE_JSON}, \"auto_init\": false}" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/user/repos")
|
||||||
|
STATUS=$(printf "%s" "$RESP" | tail -n1)
|
||||||
|
BODY=$(printf "%s" "$RESP" | sed '$d')
|
||||||
|
|
||||||
|
case "$STATUS" in
|
||||||
|
201)
|
||||||
|
echo " Remote repository created."
|
||||||
|
;;
|
||||||
|
409)
|
||||||
|
echo " Remote repository already exists — ensuring visibility..."
|
||||||
|
PATCH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"private\": ${PRIVATE_JSON}}" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/repos/${USERNAME}/${REPO_NAME}")
|
||||||
|
if [[ "$PATCH_STATUS" != "200" ]]; then
|
||||||
|
echo "Warning: failed to update visibility (HTTP $PATCH_STATUS)"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: remote repo creation failed (HTTP $STATUS): $BODY"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "${REPO_DIR}"
|
||||||
|
cd "${REPO_DIR}"
|
||||||
|
if [[ ! -d .git ]]; then
|
||||||
|
git init
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_URL="https://git.hangman-lab.top/${USERNAME}/${REPO_NAME}.git"
|
||||||
|
if ! git remote get-url origin >/dev/null 2>&1; then
|
||||||
|
git remote add origin "${REMOTE_URL}"
|
||||||
|
else
|
||||||
|
git remote set-url origin "${REMOTE_URL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
do_config --repo-path "${REPO_DIR}"
|
||||||
|
|
||||||
|
echo "Done! Repository created at: ${REPO_DIR}"
|
||||||
|
echo "Remote: ${REMOTE_URL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# add-collaborators
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
do_add_collaborators() {
|
||||||
|
local user="" repo=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--user) user="$2"; shift 2 ;;
|
||||||
|
--repo) repo="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$user" || -z "$repo" ]]; then
|
||||||
|
echo "Usage: $0 add-collaborators --user <user> --repo <repo>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! secret-mgr list | grep -q "git-access-token"; then
|
||||||
|
echo "Error: git-access-token not found. Generate one first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
owner=$(secret-mgr get-username --key git)
|
||||||
|
token=$(secret-mgr get-secret --key git-access-token)
|
||||||
|
|
||||||
|
curl -s -X PUT \
|
||||||
|
-H "Authorization: token $token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"permission":"write"}' \
|
||||||
|
"https://git.hangman-lab.top/api/v1/repos/$owner/$repo/collaborators/$user"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# list-all
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
do_list_all() {
|
||||||
|
USERNAME=$(ego-mgr get default-username)
|
||||||
|
if [[ -z "$USERNAME" ]]; then
|
||||||
|
echo "Error: cannot get username from ego-mgr" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
QUERY="
|
||||||
|
SELECT r.name, u.name as owner, r.is_private,
|
||||||
|
COALESCE((r.owner_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||||
|
OR a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||||
|
OR EXISTS (SELECT 1 FROM team_user tu JOIN team t ON t.id = tu.team_id
|
||||||
|
WHERE tu.uid = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||||
|
AND (t.includes_all_repositories = 1
|
||||||
|
OR EXISTS (SELECT 1 FROM team_repo tr WHERE tr.team_id = t.id AND tr.repo_id = r.id)))), 0) as can_write
|
||||||
|
FROM repository r
|
||||||
|
JOIN user u ON r.owner_id = u.id
|
||||||
|
LEFT JOIN access a ON a.repo_id = r.id AND a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||||
|
WHERE r.is_archived = 0
|
||||||
|
AND (r.owner_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||||
|
OR r.is_private = 0
|
||||||
|
OR a.user_id = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci
|
||||||
|
OR EXISTS (SELECT 1 FROM team_user tu WHERE tu.uid = (SELECT id FROM user WHERE lower_name = LOWER('$USERNAME')) COLLATE utf8mb4_unicode_ci))
|
||||||
|
ORDER BY r.name
|
||||||
|
"
|
||||||
|
|
||||||
|
RESULT=$(ssh -o StrictHostKeyChecking=no "$GIT_HOST" \
|
||||||
|
"docker exec $MYSQL_CONTAINER mysql -u $MYSQL_USER -p'$MYSQL_ROOT_PASS' -N -e \"$QUERY\" $MYSQL_DB" 2>/dev/null)
|
||||||
|
|
||||||
|
echo "| proj-name | owner | url | can-write |"
|
||||||
|
echo "|------------|-------|-----|-----------|"
|
||||||
|
|
||||||
|
[[ -z "$RESULT" ]] && exit 0
|
||||||
|
|
||||||
|
echo "$RESULT" | while IFS=$'\t' read -r name owner is_private can_write; do
|
||||||
|
can_write_val=$([[ "$can_write" == "1" ]] && echo "yes" || echo "no")
|
||||||
|
echo "| $name | $owner | https://git.hangman-lab.top/$owner/$name.git | $can_write_val |"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# config
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
do_config() {
|
||||||
|
local REPO_PATH=""
|
||||||
|
local RECURSIVE=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--repo-path) REPO_PATH="${2:-}"; shift 2 ;;
|
||||||
|
--recursive) RECURSIVE=true; shift ;;
|
||||||
|
*) echo "Usage: $0 config --repo-path <path> [--recursive]"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$REPO_PATH" ]]; then
|
||||||
|
echo "Usage: $0 config --repo-path <path> [--recursive]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EMAIL=$(ego-mgr get email)
|
||||||
|
if [[ -z "$EMAIL" ]]; then
|
||||||
|
echo "Error: email not set in ego-mgr"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
is_git_repo() {
|
||||||
|
if [[ -d "$1/.git" ]]; then return 0; fi
|
||||||
|
if [[ -f "$1/.git" ]]; then
|
||||||
|
gitdir=$(grep -m1 "gitdir:" "$1/.git" | cut -d' ' -f2 | tr -d ' ')
|
||||||
|
[[ -n "$gitdir" ]]; return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! is_git_repo "$REPO_PATH"; then
|
||||||
|
echo "Not a git repo: $REPO_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
USER="$(secret-mgr get-username --key git)"
|
||||||
|
PASS="$(secret-mgr get-secret --key git)"
|
||||||
|
ENC_USER="$(U="$USER" python3 - <<'PY'
|
||||||
|
import os, urllib.parse
|
||||||
|
print(urllib.parse.quote(os.environ['U'], safe=''))
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
configure_repo() {
|
||||||
|
local repo="$1" relative="${2:-}"
|
||||||
|
local name="${relative:-$repo}"
|
||||||
|
echo "Configuring: $name"
|
||||||
|
( cd "$repo" && git config user.name "$USER" )
|
||||||
|
( cd "$repo" && git config user.email "$EMAIL" )
|
||||||
|
local git_dir
|
||||||
|
git_dir="$(cd "$repo" && git rev-parse --absolute-git-dir)"
|
||||||
|
local cred_file="${git_dir}/credentials"
|
||||||
|
( cd "$repo" && git config credential.helper "store --file ${cred_file}" )
|
||||||
|
( cd "$repo" && GIT_ASKPASS=true git credential-store --file "${cred_file}" store <<EOF
|
||||||
|
protocol=https
|
||||||
|
host=git.hangman-lab.top
|
||||||
|
username=${ENC_USER}
|
||||||
|
password=${PASS}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_repo "$REPO_PATH"
|
||||||
|
|
||||||
|
if [[ "$RECURSIVE" == "true" ]]; then
|
||||||
|
submodules=$(cd "$REPO_PATH" && git submodule status --recursive 2>/dev/null | awk '{print $2}' || true)
|
||||||
|
for sm in $submodules; do
|
||||||
|
sm_path="$REPO_PATH/$sm"
|
||||||
|
if is_git_repo "$sm_path"; then
|
||||||
|
configure_repo "$sm_path" "$sm"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# get-latest
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
do_get_latest() {
|
||||||
|
local REPO_NAME="" BRANCH="main" RECURSIVE=false FORCE=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--recursive) RECURSIVE=true; shift ;;
|
||||||
|
--force) FORCE=true; shift ;;
|
||||||
|
-*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
echo "Usage: $0 get-latest <repo-name> [branch] [--recursive] [--force]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
[[ -z "$REPO_NAME" ]] && REPO_NAME="$1" && shift && continue
|
||||||
|
[[ -z "$BRANCH" ]] && BRANCH="$1" && shift && continue
|
||||||
|
echo "Unknown argument: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$REPO_NAME" ]]; then
|
||||||
|
echo "Usage: $0 get-latest <repo-name> [branch] [--recursive] [--force]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
||||||
|
echo "Error: script must be executed by pcexec"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local REPO_DIR="${AGENT_WORKSPACE}/${REPO_NAME}"
|
||||||
|
|
||||||
|
if [[ -d "$REPO_DIR" ]]; then
|
||||||
|
# Repo exists locally — update in place
|
||||||
|
if ! git -C "$REPO_DIR" rev-parse --is-inside-work-tree 2>/dev/null; then
|
||||||
|
echo "Error: $REPO_DIR is not a git repository"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local dirty
|
||||||
|
dirty=$(git -C "$REPO_DIR" status --porcelain 2>/dev/null)
|
||||||
|
if [[ -n "$dirty" ]] && [[ "$FORCE" != "true" ]]; then
|
||||||
|
echo "Error: $REPO_DIR has uncommitted changes. Use --force to discard them."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$dirty" ]]; then
|
||||||
|
echo "Discarding uncommitted changes..."
|
||||||
|
git -C "$REPO_DIR" checkout -- . 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
do_config --repo-path "$REPO_DIR"
|
||||||
|
|
||||||
|
echo "Fetching: $REPO_NAME ($BRANCH)"
|
||||||
|
git -C "$REPO_DIR" fetch origin "$BRANCH" 2>/dev/null || true
|
||||||
|
git -C "$REPO_DIR" checkout "$BRANCH" 2>/dev/null || git -C "$REPO_DIR" checkout -b "$BRANCH" "origin/$BRANCH" 2>/dev/null || true
|
||||||
|
if [[ "$FORCE" == "true" ]]; then
|
||||||
|
git -C "$REPO_DIR" reset --hard "origin/$BRANCH" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
git -C "$REPO_DIR" merge --ff-only "origin/$BRANCH" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$RECURSIVE" == "true" ]]; then
|
||||||
|
echo "Updating submodules..."
|
||||||
|
git -C "$REPO_DIR" submodule update --init --recursive 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done: $REPO_DIR updated."
|
||||||
|
else
|
||||||
|
# Repo does not exist locally — search via API then clone
|
||||||
|
echo "Repo not found locally. Searching..."
|
||||||
|
local search_result=""
|
||||||
|
search_result=$(bash "$SCRIPT_DIR/repo" search "$REPO_NAME" 2>&1)
|
||||||
|
local search_exit=$?
|
||||||
|
|
||||||
|
if [[ $search_exit -ne 0 ]]; then
|
||||||
|
echo "Error: repository '$REPO_NAME' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local url=""
|
||||||
|
url=$(echo "$search_result" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
print(data.get('clone-url', ''))
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$url" ]]; then
|
||||||
|
echo "Error: failed to parse clone URL for '$REPO_NAME'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Cloning: $url"
|
||||||
|
git clone "$url" "$REPO_DIR" 2>/dev/null || {
|
||||||
|
echo "Error: git clone failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
do_config --repo-path "$REPO_DIR"
|
||||||
|
|
||||||
|
if [[ "$RECURSIVE" == "true" ]]; then
|
||||||
|
echo "Updating submodules..."
|
||||||
|
git -C "$REPO_DIR" submodule update --init --recursive 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done: $REPO_NAME cloned to $REPO_DIR."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# search
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
do_search() {
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 search <exact-repo-name>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_NAME="$1"
|
||||||
|
|
||||||
|
TOKEN=$(secret-mgr get-secret --key git-access-token 2>/dev/null || secret-mgr get-secret --key git)
|
||||||
|
|
||||||
|
RESP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||||
|
"https://git.hangman-lab.top/api/v1/repos/search?q=${REPO_NAME}")
|
||||||
|
|
||||||
|
RESULT=$(echo "$RESP" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
results = data.get('data', []) if isinstance(data, dict) else []
|
||||||
|
for r in results:
|
||||||
|
if isinstance(r, dict) and r.get('name') == '${REPO_NAME}':
|
||||||
|
owner = r.get('owner', {})
|
||||||
|
login = owner.get('login', '') if isinstance(owner, dict) else ''
|
||||||
|
out = {
|
||||||
|
'name': r.get('name', ''),
|
||||||
|
'id': r.get('id'),
|
||||||
|
'owner': login,
|
||||||
|
'clone-url': r.get('clone_url', '')
|
||||||
|
}
|
||||||
|
print(json.dumps(out))
|
||||||
|
sys.exit(0)
|
||||||
|
print('NOT_FOUND')
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ "$RESULT" == "NOT_FOUND" ]]; then
|
||||||
|
echo "Error: repository '$REPO_NAME' not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$RESULT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Dispatch
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 <create|add-collaborators|list-all|config|search> [args...]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " create <repo-name> Create a new repository"
|
||||||
|
echo " add-collaborators --user <u> --repo <r> Add collaborator"
|
||||||
|
echo " list-all List all visible repositories"
|
||||||
|
echo " config --repo-path <path> [--recursive] Configure repo credentials"
|
||||||
|
echo " get-latest <repo-name> [branch] [--recursive] [--force] Pull latest or clone if missing"
|
||||||
|
echo " search <exact-repo-name> Search for a repository by exact name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
subcommand="$1"; shift
|
||||||
|
|
||||||
|
case "$subcommand" in
|
||||||
|
create) do_create "$@" ;;
|
||||||
|
add-collaborators) do_add_collaborators "$@" ;;
|
||||||
|
list-all) do_list_all "$@" ;;
|
||||||
|
config) do_config "$@" ;;
|
||||||
|
get-latest) do_get_latest "$@" ;;
|
||||||
|
search) do_search "$@" ;;
|
||||||
|
*) echo "Unknown command: $subcommand"; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Get the directory where this script is located
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
|
|
||||||
# Get username from ego-mgr
|
|
||||||
username=$(ego-mgr get default-username)
|
|
||||||
|
|
||||||
# Check if username is provided
|
|
||||||
if [[ -z "$username" ]]; then
|
|
||||||
echo "Error: default-username not set in ego-mgr, please contact ard"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--repo)
|
|
||||||
repo="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--user)
|
|
||||||
user="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check if user and repo are provided
|
|
||||||
if [[ -z "$user" || -z "$repo" ]]; then
|
|
||||||
echo "Usage: $0 --user <user> --repo <repo>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if git-access-token exists
|
|
||||||
if ! secret-mgr list | grep -q "git-access-token"; then
|
|
||||||
echo "generate your access token first"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
owner=$(secret-mgr get-username --key git)
|
|
||||||
token=$(secret-mgr get-secret --key git-access-token)
|
|
||||||
|
|
||||||
# Execute
|
|
||||||
curl -X PUT -H "Authorization: token $token" -H "Content-Type: application/json" -d '{"permission":"write"}' "https://git.hangman-lab.top/api/v1/repos/$owner/$repo/collaborators/$user"
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "Usage: $0 --repo-path <path> [--recursive]"
|
|
||||||
echo " --repo-path: Path to the git repository"
|
|
||||||
echo " --recursive: Also configure all submodules (recursive)"
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
REPO_PATH=""
|
|
||||||
RECURSIVE=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--repo-path)
|
|
||||||
REPO_PATH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--recursive)
|
|
||||||
RECURSIVE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$REPO_PATH" ]]; then
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get email from ego-mgr
|
|
||||||
EMAIL=$(ego-mgr get email)
|
|
||||||
|
|
||||||
# Check if email is provided
|
|
||||||
if [[ -z "$EMAIL" ]]; then
|
|
||||||
echo "Error: email not set in ego-mgr, please contact ard"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if it's a git repo (either .git is a directory or a file with gitdir: reference)
|
|
||||||
is_git_repo() {
|
|
||||||
local repo="$1"
|
|
||||||
if [[ -d "$repo/.git" ]]; then
|
|
||||||
return 0
|
|
||||||
elif [[ -f "$repo/.git" ]]; then
|
|
||||||
local gitdir
|
|
||||||
gitdir=$(grep -m1 "gitdir:" "$repo/.git" | cut -d' ' -f2 | tr -d ' ')
|
|
||||||
if [[ -n "$gitdir" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! is_git_repo "$REPO_PATH"; then
|
|
||||||
echo "Not a git repo: $REPO_PATH"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
USER="$(secret-mgr get-username --key git)"
|
|
||||||
PASS="$(secret-mgr get-secret --key git)"
|
|
||||||
|
|
||||||
if [[ -z "$USER" || -z "$PASS" ]]; then
|
|
||||||
echo "Missing credentials from secret-mgr (key: git)"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# URL-encode username for credential URL
|
|
||||||
ENC_USER="$(U="$USER" python3 - <<'PY'
|
|
||||||
import os, urllib.parse
|
|
||||||
print(urllib.parse.quote(os.environ['U'], safe=''))
|
|
||||||
PY
|
|
||||||
)"
|
|
||||||
|
|
||||||
# Function to configure a single repo
|
|
||||||
configure_repo() {
|
|
||||||
local repo="$1"
|
|
||||||
local relative="${2:-}"
|
|
||||||
|
|
||||||
# Get relative path name for display
|
|
||||||
local name="${relative:-$repo}"
|
|
||||||
|
|
||||||
echo "Configuring: $name"
|
|
||||||
|
|
||||||
# Set local user.name / user.email
|
|
||||||
( cd "$repo" && git config user.name "$USER" )
|
|
||||||
( cd "$repo" && git config user.email "$EMAIL" )
|
|
||||||
|
|
||||||
# Resolve the real git dir (works for normal repos and submodules)
|
|
||||||
local git_dir
|
|
||||||
git_dir="$(cd "$repo" && git rev-parse --absolute-git-dir)"
|
|
||||||
local cred_file="${git_dir}/credentials"
|
|
||||||
|
|
||||||
( cd "$repo" && git config credential.helper "store --file ${cred_file}" )
|
|
||||||
( cd "$repo" && GIT_ASKPASS=true git credential-store --file "${cred_file}" store <<EOF
|
|
||||||
protocol=https
|
|
||||||
host=git.hangman-lab.top
|
|
||||||
username=${ENC_USER}
|
|
||||||
password=${PASS}
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
echo " - user.name: $USER"
|
|
||||||
echo " - user.email: $EMAIL"
|
|
||||||
echo " - credential.helper: configured"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Configure main repo
|
|
||||||
configure_repo "$REPO_PATH"
|
|
||||||
|
|
||||||
# Configure submodules if --recursive is specified
|
|
||||||
if [[ "$RECURSIVE" == "true" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "Configuring submodules..."
|
|
||||||
|
|
||||||
# Get submodules list
|
|
||||||
submodules=$(cd "$REPO_PATH" && git submodule status --recursive 2>/dev/null | awk '{print $2}' || true)
|
|
||||||
|
|
||||||
if [[ -z "$submodules" ]]; then
|
|
||||||
echo "No submodules found"
|
|
||||||
else
|
|
||||||
for sm in $submodules; do
|
|
||||||
sm_path="$REPO_PATH/$sm"
|
|
||||||
if is_git_repo "$sm_path"; then
|
|
||||||
configure_repo "$sm_path" "$sm"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "OK"
|
|
||||||
@@ -3,40 +3,15 @@ name: keycloak-hangman-lab
|
|||||||
description: Keycloak operations for hangman-lab.top - manage accounts, email verification, and passwords.
|
description: Keycloak operations for hangman-lab.top - manage accounts, email verification, and passwords.
|
||||||
---
|
---
|
||||||
|
|
||||||
> ⚠️ **Note**: All scripts must be executed via the `pcexec` tool.
|
> All scripts must be executed via the `pcexec` tool.
|
||||||
|
|
||||||
## Keycloak Operations
|
## Scripts
|
||||||
|
|
||||||
### Create Keycloak Account
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `{baseDir}/scripts/kc-ctrl create-keycloak-account` | Create a new Keycloak account |
|
||||||
|
| `{baseDir}/scripts/kc-ctrl verify-email` | Verify user email in Keycloak |
|
||||||
|
| `{baseDir}/scripts/kc-ctrl set-name` | Set user firstName and lastName |
|
||||||
|
| `{baseDir}/scripts/kc-ctrl reset-password` | Reset password (reads username from secret-mgr) |
|
||||||
|
|
||||||
Create a new Keycloak account.
|
> **create-keycloak-account**: Do not execute unless explicitly requested. Contact **agent-resource-director** or **hangman** first.
|
||||||
|
|
||||||
> ⚠️ **Warning**: Do not execute this command unless explicitly requested. If you don't have a Keycloak account, contact **agent-resource-director** or **hangman** to guide you through the process.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/kc-ctrl create-keycloak-account
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verify Email
|
|
||||||
|
|
||||||
Verify user email in Keycloak.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/kc-ctrl verify-email
|
|
||||||
```
|
|
||||||
|
|
||||||
### Set User Name
|
|
||||||
|
|
||||||
Set user firstName and lastName in Keycloak.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/kc-ctrl set-name
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset Password
|
|
||||||
|
|
||||||
Reset password for the current user (reads username from secret-mgr).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{baseDir}/scripts/kc-ctrl reset-password
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# pcguard || exit 1
|
# pcguard || exit 1
|
||||||
|
|
||||||
REMOTE_HOST="vps.git"
|
REMOTE_HOST="server.t0"
|
||||||
REMOTE_USER="root"
|
REMOTE_USER="root"
|
||||||
CONTAINER_NAME="git-kc-keycloak"
|
CONTAINER_NAME="git-kc-keycloak"
|
||||||
HOST_CONFIG="/root/.keycloak/kcadm.config"
|
HOST_CONFIG="/root/.keycloak/kcadm.config"
|
||||||
|
|||||||
90
learn.sh
Executable file
90
learn.sh
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -z "${AGENT_WORKSPACE:-}" ]]; then
|
||||||
|
echo "Error: script must be executed by pcexec"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TARGET_DIR="${AGENT_WORKSPACE}/skills"
|
||||||
|
CLAW_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
MANDATORY_FILE="${CLAW_DIR}/.mandatory"
|
||||||
|
ROLE_SKILLS_FILE="${CLAW_DIR}/role-skills.json"
|
||||||
|
SKILL_LIST="${AGENT_WORKSPACE}/.skill-list"
|
||||||
|
|
||||||
|
mkdir -p "$TARGET_DIR"
|
||||||
|
|
||||||
|
# Track installed skills to avoid duplicates
|
||||||
|
declare -A INSTALLED
|
||||||
|
|
||||||
|
install_skill() {
|
||||||
|
local skill_name="$1"
|
||||||
|
local source="$2"
|
||||||
|
|
||||||
|
[[ -z "$skill_name" || "$skill_name" == \#* ]] && return
|
||||||
|
|
||||||
|
if [[ -n "${INSTALLED[$skill_name]:-}" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local skill_dir="${CLAW_DIR}/${skill_name}"
|
||||||
|
if [[ -d "$skill_dir" ]]; then
|
||||||
|
echo "Installing ($source): $skill_name"
|
||||||
|
cp -r "$skill_dir" "${TARGET_DIR}/"
|
||||||
|
INSTALLED[$skill_name]=1
|
||||||
|
else
|
||||||
|
echo "Skipping (not found): $skill_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Install mandatory skills
|
||||||
|
if [[ -f "$MANDATORY_FILE" ]]; then
|
||||||
|
while IFS= read -r skill_name || [[ -n "$skill_name" ]]; do
|
||||||
|
install_skill "$skill_name" "mandatory"
|
||||||
|
done < "$MANDATORY_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Install role-based skills from role-skills.json
|
||||||
|
if [[ -f "$ROLE_SKILLS_FILE" ]]; then
|
||||||
|
ROLE=$(ego-mgr get role 2>/dev/null || true)
|
||||||
|
POSITION=$(ego-mgr get position 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -n "$ROLE" ]]; then
|
||||||
|
ROLE_SKILLS=$(python3 -c "
|
||||||
|
import json, sys
|
||||||
|
with open('$ROLE_SKILLS_FILE') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
skills = data.get('roles', {}).get('$ROLE', [])
|
||||||
|
print('\n'.join(skills))
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
|
||||||
|
while IFS= read -r skill_name; do
|
||||||
|
[[ -n "$skill_name" ]] && install_skill "$skill_name" "role:$ROLE"
|
||||||
|
done <<< "$ROLE_SKILLS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$POSITION" ]]; then
|
||||||
|
POS_SKILLS=$(python3 -c "
|
||||||
|
import json, sys
|
||||||
|
with open('$ROLE_SKILLS_FILE') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
skills = data.get('positions', {}).get('$POSITION', [])
|
||||||
|
print('\n'.join(skills))
|
||||||
|
" 2>/dev/null || true)
|
||||||
|
|
||||||
|
while IFS= read -r skill_name; do
|
||||||
|
[[ -n "$skill_name" ]] && install_skill "$skill_name" "position:$POSITION"
|
||||||
|
done <<< "$POS_SKILLS"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Warning: role-skills.json not found, skipping role-based skills"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Install agent-specific skills from .skill-list (if exists)
|
||||||
|
if [[ -f "$SKILL_LIST" ]]; then
|
||||||
|
while IFS= read -r skill_name || [[ -n "$skill_name" ]]; do
|
||||||
|
install_skill "$skill_name" "skill-list"
|
||||||
|
done < "$SKILL_LIST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
21
openclaw-plugin-dev/SKILL.md
Normal file
21
openclaw-plugin-dev/SKILL.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: openclaw-plugin-dev
|
||||||
|
description: OpenClaw plugin development — structure, conventions, hooks, tools, install scripts, and debugging. Use when creating, modifying, or debugging OpenClaw plugins.
|
||||||
|
---
|
||||||
|
|
||||||
|
> Reference docs provide the full specification. Workflows guide specific tasks.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- `{baseDir}/docs/structure.md` — Project layout, directory conventions, file naming
|
||||||
|
- `{baseDir}/docs/entry-point.md` — Plugin entry format, globalThis patterns, lifecycle protection
|
||||||
|
- `{baseDir}/docs/hooks.md` — Hook registration, dedup patterns, available hooks
|
||||||
|
- `{baseDir}/docs/tools.md` — Tool registration interface (inputSchema + execute)
|
||||||
|
- `{baseDir}/docs/state.md` — State management rules (globalThis vs module-level)
|
||||||
|
- `{baseDir}/docs/config.md` — Config schema, install scripts, sensitive fields
|
||||||
|
- `{baseDir}/docs/debugging.md` — Dev loop, log keywords, smoke testing
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
- `{baseDir}/workflows/create-plugin.md` — When creating a new OpenClaw plugin from scratch.
|
||||||
|
- `{baseDir}/workflows/add-hook.md` — When adding a new hook handler to an existing plugin.
|
||||||
61
openclaw-plugin-dev/docs/config.md
Normal file
61
openclaw-plugin-dev/docs/config.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Config Schema & Install Scripts
|
||||||
|
|
||||||
|
## openclaw.plugin.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://openclaw.ai/schemas/plugin-config.json",
|
||||||
|
"configSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"myToken": { "type": "string" },
|
||||||
|
"myFlag": { "type": "boolean", "default": false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- `additionalProperties: false` is mandatory — OpenClaw validates config against schema
|
||||||
|
- Removing a config field requires removing it from schema too (or gateway fails to start)
|
||||||
|
- Sensitive fields (tokens, keys): no `default`, user must configure manually
|
||||||
|
|
||||||
|
## Install Script (scripts/install.mjs)
|
||||||
|
|
||||||
|
### Install does:
|
||||||
|
|
||||||
|
1. Build dist (compile TypeScript)
|
||||||
|
2. Clean and copy to `~/.openclaw/plugins/<id>/`
|
||||||
|
3. Update `openclaw.json`:
|
||||||
|
- Add to `plugins.allow`
|
||||||
|
- Add to `plugins.load.paths`
|
||||||
|
- Set `plugins.entries.<id>.enabled = true`
|
||||||
|
- Set default config via `setIfMissing` (never overwrite existing values)
|
||||||
|
4. Never set sensitive fields (tokens) — add comments for manual configuration
|
||||||
|
|
||||||
|
### Uninstall does:
|
||||||
|
|
||||||
|
1. Remove from `plugins.allow`
|
||||||
|
2. Delete `plugins.entries.<id>`
|
||||||
|
3. Remove from `plugins.load.paths`
|
||||||
|
4. Delete install directory
|
||||||
|
|
||||||
|
### setIfMissing pattern
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function setIfMissing(obj, key, value) {
|
||||||
|
if (obj[key] === undefined || obj[key] === null) {
|
||||||
|
obj[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config field changes
|
||||||
|
|
||||||
|
When renaming or removing config fields:
|
||||||
|
1. Update `openclaw.plugin.json` schema first (source of truth)
|
||||||
|
2. Update `normalizeConfig()` in index.ts
|
||||||
|
3. Update install script `setIfMissing` calls
|
||||||
|
4. Document migration steps for users
|
||||||
46
openclaw-plugin-dev/docs/debugging.md
Normal file
46
openclaw-plugin-dev/docs/debugging.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Development & Debugging
|
||||||
|
|
||||||
|
## Dev Loop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Modify code in plugin/
|
||||||
|
# 2. Reinstall
|
||||||
|
node scripts/install.mjs --install
|
||||||
|
|
||||||
|
# 3. Restart gateway
|
||||||
|
openclaw gateway restart
|
||||||
|
|
||||||
|
# 4. Watch logs
|
||||||
|
openclaw logs --follow
|
||||||
|
|
||||||
|
# 5. Test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Log Keywords
|
||||||
|
|
||||||
|
| Keyword | Meaning |
|
||||||
|
|---------|---------|
|
||||||
|
| `plugin registered` | register() completed |
|
||||||
|
| `startSideCar called` / `already running` | Sidecar status |
|
||||||
|
| `must NOT have additional properties` | Schema vs config mismatch |
|
||||||
|
| `EADDRINUSE` | Port conflict (sidecar or HTTP server) |
|
||||||
|
|
||||||
|
## Checklist Before Deploy
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
- [ ] All hook handlers have event dedup (globalThis, not module-level)
|
||||||
|
- [ ] Gateway lifecycle events protected by globalThis flag
|
||||||
|
- [ ] Business state on globalThis
|
||||||
|
- [ ] `openclaw.plugin.json` schema matches actual config fields
|
||||||
|
- [ ] Install script doesn't set schema-absent fields
|
||||||
|
- [ ] Sensitive fields not set by install script
|
||||||
|
- [ ] Install script cleans old dist before copying
|
||||||
|
|
||||||
|
### Connection-type plugins
|
||||||
|
|
||||||
|
- [ ] Start flag on globalThis (not module-level `let`)
|
||||||
|
- [ ] Runtime reference on globalThis
|
||||||
|
- [ ] Shared registries/callbacks on globalThis, init once
|
||||||
|
- [ ] Cross-plugin API object overwritten every register() but runtime started once
|
||||||
|
- [ ] Consumer plugins defend against provider not loaded
|
||||||
49
openclaw-plugin-dev/docs/entry-point.md
Normal file
49
openclaw-plugin-dev/docs/entry-point.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Plugin Entry Point
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
const _G = globalThis as Record<string, unknown>;
|
||||||
|
const LIFECYCLE_KEY = "_myPluginGatewayLifecycleRegistered";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "my-plugin",
|
||||||
|
name: "My Plugin",
|
||||||
|
register(api: OpenClawPluginApi) {
|
||||||
|
const config = normalizeConfig(api);
|
||||||
|
|
||||||
|
// Gateway lifecycle: only once
|
||||||
|
if (!_G[LIFECYCLE_KEY]) {
|
||||||
|
_G[LIFECYCLE_KEY] = true;
|
||||||
|
// Start sidecars, init global resources
|
||||||
|
api.on("gateway_stop", () => { _G[LIFECYCLE_KEY] = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent session hooks: every register() call (dedup inside handler)
|
||||||
|
registerMyHook(api, config);
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
registerMyTools(api, config);
|
||||||
|
|
||||||
|
api.logger.info("my-plugin: registered");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why globalThis?
|
||||||
|
|
||||||
|
OpenClaw may hot-reload plugins. Module-level variables reset on reload, but `globalThis` persists. All mutable state must be on `globalThis`:
|
||||||
|
|
||||||
|
- Startup flags → prevent double initialization
|
||||||
|
- Dedup sets → prevent double hook execution
|
||||||
|
- Runtime references → keep connections alive across reloads
|
||||||
|
- Shared registries → preserve cross-plugin state
|
||||||
|
|
||||||
|
## Naming Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
_<pluginId>PluginXxx # Internal state (e.g., _prismFacetRouters)
|
||||||
|
__<pluginId> # Cross-plugin public API (e.g., __yonexusClient)
|
||||||
|
```
|
||||||
62
openclaw-plugin-dev/docs/hooks.md
Normal file
62
openclaw-plugin-dev/docs/hooks.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Hook Registration
|
||||||
|
|
||||||
|
## Available Hooks
|
||||||
|
|
||||||
|
| Hook | Purpose | Dedup Method |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `before_model_resolve` | Override model/provider before LLM call | WeakSet on event |
|
||||||
|
| `before_prompt_build` | Inject into system prompt | WeakSet on event |
|
||||||
|
| `before_agent_start` | Legacy prompt injection | WeakSet on event |
|
||||||
|
| `agent_end` | Post-conversation actions | Set on runId |
|
||||||
|
| `message_received` | React to inbound messages | — |
|
||||||
|
| `llm_input` / `llm_output` | Observe LLM traffic | — |
|
||||||
|
| `before_tool_call` / `after_tool_call` | Observe tool execution | — |
|
||||||
|
| `gateway_start` / `gateway_stop` | Gateway lifecycle | globalThis flag |
|
||||||
|
|
||||||
|
## Dedup Patterns
|
||||||
|
|
||||||
|
### WeakSet (for event-object hooks)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const _G = globalThis as Record<string, unknown>;
|
||||||
|
const DEDUP_KEY = "_myPluginHookDedup";
|
||||||
|
if (!(_G[DEDUP_KEY] instanceof WeakSet)) _G[DEDUP_KEY] = new WeakSet<object>();
|
||||||
|
const dedup = _G[DEDUP_KEY] as WeakSet<object>;
|
||||||
|
|
||||||
|
api.on("before_model_resolve", async (event, ctx) => {
|
||||||
|
if (dedup.has(event as object)) return;
|
||||||
|
dedup.add(event as object);
|
||||||
|
// ... handler logic
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set with runId (for agent_end)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const DEDUP_KEY = "_myPluginAgentEndDedup";
|
||||||
|
if (!(_G[DEDUP_KEY] instanceof Set)) _G[DEDUP_KEY] = new Set<string>();
|
||||||
|
const dedup = _G[DEDUP_KEY] as Set<string>;
|
||||||
|
|
||||||
|
api.on("agent_end", async (event, ctx) => {
|
||||||
|
const runId = (event as any).runId as string;
|
||||||
|
if (runId) {
|
||||||
|
if (dedup.has(runId)) return;
|
||||||
|
dedup.add(runId);
|
||||||
|
if (dedup.size > 500) dedup.delete(dedup.values().next().value!);
|
||||||
|
}
|
||||||
|
// ... handler logic
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prompt Injection Hooks
|
||||||
|
|
||||||
|
`before_prompt_build` and `before_agent_start` can return prompt mutation fields:
|
||||||
|
|
||||||
|
| Field | Caching | Use for |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| `prependSystemContext` | Cached | Static role/identity prompts |
|
||||||
|
| `appendSystemContext` | Cached | Static supplementary guidance |
|
||||||
|
| `prependContext` | Not cached | Per-turn dynamic context |
|
||||||
|
| `systemPrompt` | — | Full system prompt replacement (rarely used) |
|
||||||
|
|
||||||
|
Requires `plugins.entries.<id>.hooks.allowPromptInjection: true` in openclaw.json.
|
||||||
48
openclaw-plugin-dev/docs/state.md
Normal file
48
openclaw-plugin-dev/docs/state.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# State Management
|
||||||
|
|
||||||
|
## Where to Store What
|
||||||
|
|
||||||
|
| Data Type | Location | Reason |
|
||||||
|
|-----------|----------|--------|
|
||||||
|
| Business state (maps, sets, caches) | `globalThis` | Module vars reset on hot reload |
|
||||||
|
| Event dedup (WeakSet/Set) | `globalThis` | Same |
|
||||||
|
| Gateway lifecycle flags | `globalThis` | Prevent double init |
|
||||||
|
| Connection flags (WebSocket/TCP) | `globalThis` | Prevent duplicate connections |
|
||||||
|
| Runtime references | `globalThis` | Closures need living instance |
|
||||||
|
| Cross-plugin API objects (`__pluginId`) | `globalThis` | Other plugins access via globalThis |
|
||||||
|
| Pure utility functions | Module-level | No state needed |
|
||||||
|
| Persistent data | File + memory cache | Survives gateway restart |
|
||||||
|
|
||||||
|
## Cross-Plugin API Pattern
|
||||||
|
|
||||||
|
### Provider side
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const _G = globalThis as Record<string, unknown>;
|
||||||
|
|
||||||
|
// Init shared objects once
|
||||||
|
if (!(_G["_myRegistry"] instanceof MyRegistry)) {
|
||||||
|
_G["_myRegistry"] = new MyRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite public API every register() (updates closures)
|
||||||
|
_G["__myPlugin"] = {
|
||||||
|
registry: _G["_myRegistry"],
|
||||||
|
send: (msg) => (_G["_myRuntime"] as Runtime)?.send(msg) ?? false,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Consumer side
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const provider = (globalThis as any)["__myPlugin"];
|
||||||
|
if (!provider) {
|
||||||
|
console.error("[consumer] __myPlugin not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
provider.registry.register("my_rule", handler);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load Order
|
||||||
|
|
||||||
|
Provider must be listed before consumer in `plugins.allow`. Consumer must defend against provider not being loaded.
|
||||||
26
openclaw-plugin-dev/docs/structure.md
Normal file
26
openclaw-plugin-dev/docs/structure.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Plugin Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
proj-root/
|
||||||
|
plugin/ # Installable plugin (copied to ~/.openclaw/plugins/<id>/)
|
||||||
|
index.ts # Entry: export default { id, name, register }
|
||||||
|
openclaw.plugin.json # Config schema declaration
|
||||||
|
package.json # name, version, type: module
|
||||||
|
hooks/ # Hook handlers (one file per hook)
|
||||||
|
tools/ # Tool registrations
|
||||||
|
core/ # Pure business logic (no plugin-sdk imports)
|
||||||
|
web/ # HTTP routes (optional)
|
||||||
|
services/ # Sidecar processes (optional, installed alongside plugin)
|
||||||
|
skills/ # OpenClaw skills provided by this plugin (optional)
|
||||||
|
routers/ # Drop-in modules (plugin-specific, e.g., PrismFacet routers)
|
||||||
|
scripts/
|
||||||
|
install.mjs # --install / --uninstall / --update
|
||||||
|
docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- File names: kebab-case (`before-model-resolve.ts`)
|
||||||
|
- Export names: camelCase (`registerBeforeModelResolve`)
|
||||||
|
- `plugin/core/` must not import from `openclaw/plugin-sdk` — keeps logic unit-testable
|
||||||
|
- Hook registration logic goes in `plugin/hooks/`, not in `index.ts`
|
||||||
40
openclaw-plugin-dev/docs/tools.md
Normal file
40
openclaw-plugin-dev/docs/tools.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Tool Registration
|
||||||
|
|
||||||
|
## Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Without agent context
|
||||||
|
api.registerTool({
|
||||||
|
name: "my-tool",
|
||||||
|
description: "Does something",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
param: { type: "string", description: "..." },
|
||||||
|
},
|
||||||
|
required: ["param"],
|
||||||
|
},
|
||||||
|
execute: async (toolCallId, params) => {
|
||||||
|
const { param } = params as { param: string };
|
||||||
|
return { result: "ok" };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// With agent context (factory function)
|
||||||
|
api.registerTool((ctx) => ({
|
||||||
|
name: "my-contextual-tool",
|
||||||
|
description: "...",
|
||||||
|
inputSchema: { /* ... */ },
|
||||||
|
execute: async (toolCallId, params) => {
|
||||||
|
const agentId = ctx.agentId;
|
||||||
|
return { result: agentId };
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Points
|
||||||
|
|
||||||
|
- Interface is `execute: async (toolCallId, params)`, NOT `handler:`
|
||||||
|
- Use `inputSchema` (JSON Schema), NOT `parameters`
|
||||||
|
- Return `{ result: "..." }` object
|
||||||
|
- Factory form `(ctx) => ({...})` gives access to agent context (agentId, sessionKey, etc.)
|
||||||
40
openclaw-plugin-dev/workflows/add-hook.md
Normal file
40
openclaw-plugin-dev/workflows/add-hook.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Add Hook
|
||||||
|
|
||||||
|
When adding a new hook handler to an existing plugin.
|
||||||
|
|
||||||
|
> See `{baseDir}/docs/hooks.md` for available hooks and dedup patterns.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Create Hook File
|
||||||
|
|
||||||
|
Add `plugin/hooks/<hook-name>.ts`. Export a registration function:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function registerMyHook(api, config) {
|
||||||
|
// Set up dedup on globalThis
|
||||||
|
// api.on("<hook-name>", handler)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add Dedup
|
||||||
|
|
||||||
|
Choose the right dedup pattern:
|
||||||
|
- `before_model_resolve`, `before_prompt_build` → WeakSet on event object
|
||||||
|
- `agent_end` → Set on runId with size cap (500)
|
||||||
|
- `gateway_start/stop` → globalThis flag (in index.ts, not in hook file)
|
||||||
|
|
||||||
|
### 3. Register in index.ts
|
||||||
|
|
||||||
|
Import and call the registration function in the `register()` method.
|
||||||
|
|
||||||
|
Hooks that need to run every register() call (agent-scoped): put outside the lifecycle guard.
|
||||||
|
Hooks that should run once (gateway-scoped): put inside the lifecycle guard.
|
||||||
|
|
||||||
|
### 4. If Prompt Injection
|
||||||
|
|
||||||
|
If the hook returns `prependSystemContext` or `appendSystemContext`, ensure `allowPromptInjection: true` is set in the plugin's config entry and in the install script.
|
||||||
|
|
||||||
|
### 5. Test
|
||||||
|
|
||||||
|
Reinstall, restart gateway, verify via logs.
|
||||||
72
openclaw-plugin-dev/workflows/create-plugin.md
Normal file
72
openclaw-plugin-dev/workflows/create-plugin.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Create Plugin
|
||||||
|
|
||||||
|
When creating a new OpenClaw plugin from scratch.
|
||||||
|
|
||||||
|
> See `{baseDir}/docs/structure.md` for directory layout and conventions.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Scaffold
|
||||||
|
|
||||||
|
Create the project structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-plugin/
|
||||||
|
plugin/
|
||||||
|
index.ts
|
||||||
|
openclaw.plugin.json
|
||||||
|
package.json
|
||||||
|
core/
|
||||||
|
hooks/
|
||||||
|
tools/
|
||||||
|
scripts/
|
||||||
|
install.mjs
|
||||||
|
routers/ # if applicable
|
||||||
|
package.json # dev dependencies (typescript, @types/node)
|
||||||
|
tsconfig.plugin.json
|
||||||
|
.gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Define Config Schema
|
||||||
|
|
||||||
|
Write `plugin/openclaw.plugin.json` with `additionalProperties: false`. Only include fields the plugin actually uses.
|
||||||
|
|
||||||
|
### 3. Implement Entry Point
|
||||||
|
|
||||||
|
Follow the pattern in `{baseDir}/docs/entry-point.md`:
|
||||||
|
- `export default { id, name, register }`
|
||||||
|
- globalThis lifecycle protection
|
||||||
|
- Hooks in separate files under `hooks/`
|
||||||
|
- Tools in separate files under `tools/`
|
||||||
|
- Business logic in `core/` (no plugin-sdk imports)
|
||||||
|
|
||||||
|
### 4. Implement Hooks
|
||||||
|
|
||||||
|
Follow dedup patterns in `{baseDir}/docs/hooks.md`. Each hook gets its own file.
|
||||||
|
|
||||||
|
### 5. Implement Tools
|
||||||
|
|
||||||
|
Follow the interface in `{baseDir}/docs/tools.md`: `inputSchema` + `execute`.
|
||||||
|
|
||||||
|
### 6. Write Install Script
|
||||||
|
|
||||||
|
Follow `{baseDir}/docs/config.md` for install/uninstall conventions.
|
||||||
|
|
||||||
|
### 7. Build & Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
node scripts/install.mjs --install
|
||||||
|
openclaw gateway restart
|
||||||
|
# Test via Discord or openclaw agent CLI
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Push
|
||||||
|
|
||||||
|
Create a git repository and push:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git-ctrl repo create <plugin-name>
|
||||||
|
git add -A && git commit -m "init: <plugin-name>"
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
18
recruitment/SKILL.md
Normal file
18
recruitment/SKILL.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: recruitment
|
||||||
|
description: Onboard new agents into OpenClaw. Triggers on "new agent", "add agent", "create agent", "recruit agent", "onboard agent".
|
||||||
|
---
|
||||||
|
|
||||||
|
> Scripts must be executed via `pcexec`. The onboard script uses `proxy-pcexec`.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `{baseDir}/scripts/new-agent --type openclaw --agent-id <id> [--model <model>]` | Create and register an OpenClaw agent |
|
||||||
|
| `{baseDir}/scripts/new-agent --type contractor --contractor-provider <claude\|gemini> --agent-id <id>` | Create and register a contractor agent |
|
||||||
|
| `{baseDir}/scripts/onboard --name <name> --role <role> --position <pos> --gender <gender> --bot-token <token>` | Set up agent identity and Discord binding (call via `proxy-pcexec` with `proxy-for: <agent-id>`) |
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
- `{baseDir}/workflows/recruitment.md` — Full onboarding flow: gather requirements, create agent, interview, onboard, report.
|
||||||
122
recruitment/scripts/new-agent
Executable file
122
recruitment/scripts/new-agent
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TYPE=""
|
||||||
|
AGENT_ID=""
|
||||||
|
MODEL=""
|
||||||
|
CONTRACTOR_PROVIDER=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: new-agent --type <openclaw|contractor> --agent-id <agent-id> [--model <primary-model>] [--contractor-provider <claude|gemini>]"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--type)
|
||||||
|
TYPE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--agent-id)
|
||||||
|
AGENT_ID="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--model)
|
||||||
|
MODEL="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--contractor-provider)
|
||||||
|
CONTRACTOR_PROVIDER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$TYPE" ]] || [[ -z "$AGENT_ID" ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORKSPACE_DIR="$HOME/.openclaw/workspace/workspace-$AGENT_ID"
|
||||||
|
STATE_DIR="$HOME/.openclaw/states"
|
||||||
|
BINDINGS_LOCK="$STATE_DIR/bindings.lock"
|
||||||
|
|
||||||
|
# Step 1: Register agent
|
||||||
|
if [[ "$TYPE" == "openclaw" ]]; then
|
||||||
|
MODEL_ARG=""
|
||||||
|
if [[ -n "$MODEL" ]]; then
|
||||||
|
MODEL_ARG="--model $MODEL"
|
||||||
|
fi
|
||||||
|
openclaw agents add "$AGENT_ID" $MODEL_ARG --workspace "$WORKSPACE_DIR" --non-interactive
|
||||||
|
elif [[ "$TYPE" == "contractor" ]]; then
|
||||||
|
# Check contractor-agent plugin
|
||||||
|
if ! openclaw plugins list 2>/dev/null | grep -q "contractor-agent"; then
|
||||||
|
echo "Error: contractor-agent plugin is not installed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
openclaw contractor-agents add --agent-id "$AGENT_ID" --workspace "$WORKSPACE_DIR" --contractor "$CONTRACTOR_PROVIDER"
|
||||||
|
else
|
||||||
|
echo "Error: unknown type '$TYPE'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Wait for bindings
|
||||||
|
echo "Checking bindings..."
|
||||||
|
|
||||||
|
wait_for_bindings() {
|
||||||
|
local bindings
|
||||||
|
bindings=$(openclaw config get bindings 2>/dev/null || echo "{}")
|
||||||
|
if echo "$bindings" | grep -q '"interviewee"'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_lock() {
|
||||||
|
if [[ -f "$BINDINGS_LOCK" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while wait_for_bindings; do
|
||||||
|
echo "another interview is ongoing, waiting..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
while wait_for_lock; do
|
||||||
|
echo "bindings locked, waiting..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
# Step 3: Set bindings
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
touch "$BINDINGS_LOCK"
|
||||||
|
|
||||||
|
CURRENT_BINDINGS=$(openclaw config get bindings 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
|
NEW_ENTRY="{\"agentId\": \"$AGENT_ID\", \"match\": {\"channel\": \"discord\", \"accountId\": \"interviewee\"}}"
|
||||||
|
|
||||||
|
if [[ "$CURRENT_BINDINGS" == "{}" ]] || [[ -z "$CURRENT_BINDINGS" ]]; then
|
||||||
|
NEW_BINDINGS="[$NEW_ENTRY]"
|
||||||
|
else
|
||||||
|
NEW_BINDINGS=$(echo "$CURRENT_BINDINGS" | sed 's/\(\[.*\)\]/\1,/' | sed 's/\]$//' | sed 's/\([{,]\)$/\1/')" $NEW_ENTRY]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
openclaw config set bindings "$NEW_BINDINGS"
|
||||||
|
|
||||||
|
rm -f "$BINDINGS_LOCK"
|
||||||
|
|
||||||
|
# Step 4: Configure interviewee account
|
||||||
|
TOKEN=$(secret-mgr get-secret --key interviewee-discord-token --public 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
echo "Error: failed to get interviewee-discord-token from secret-mgr" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
openclaw config set channels.discord.accounts.interviewee "{\"enabled\": true, \"token\": \"$TOKEN\", \"groupPolicy\": \"open\", \"streaming\": {\"mode\": \"off\"}}"
|
||||||
|
|
||||||
|
echo "Agent $AGENT_ID recruited successfully."
|
||||||
130
recruitment/scripts/onboard
Executable file
130
recruitment/scripts/onboard
Executable file
@@ -0,0 +1,130 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
NAME=""
|
||||||
|
ROLE=""
|
||||||
|
POSITION=""
|
||||||
|
GENDER=""
|
||||||
|
BOT_TOKEN=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: onboard --name <name> --role <role> --position <position> --gender <gender> --bot-token <token>"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--name)
|
||||||
|
NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--role)
|
||||||
|
ROLE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--position)
|
||||||
|
POSITION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--gender)
|
||||||
|
GENDER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--bot-token)
|
||||||
|
BOT_TOKEN="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$NAME" ]] || [[ -z "$ROLE" ]] || [[ -z "$POSITION" ]] || [[ -z "$GENDER" ]] || [[ -z "$BOT_TOKEN" ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PCEXEC_PROXIED" != "true" ]]; then
|
||||||
|
echo "Error: this script must be executed with tool proxy-pcexec" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TODAY=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
# Resolve discord-id from bot token
|
||||||
|
DISCORD_ID=$(curl -s "https://discord.com/api/v10/users/@me" \
|
||||||
|
-H "Authorization: Bot $BOT_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('id', ''))" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$DISCORD_ID" ]]; then
|
||||||
|
echo "Error: failed to resolve discord-id from bot token" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
AGENT_ID="agent-$NAME"
|
||||||
|
|
||||||
|
ego-mgr set name "$NAME"
|
||||||
|
ego-mgr set default-username "$NAME"
|
||||||
|
ego-mgr set discord-id "$DISCORD_ID"
|
||||||
|
ego-mgr set role "$ROLE"
|
||||||
|
ego-mgr set position "$POSITION"
|
||||||
|
ego-mgr set gender "$GENDER"
|
||||||
|
ego-mgr set email "${NAME}@${ROLE}.hangman-lab.top"
|
||||||
|
ego-mgr set agent-id "$AGENT_ID"
|
||||||
|
ego-mgr set date-of-birth "$TODAY"
|
||||||
|
|
||||||
|
BINDINGS_LOCK="$HOME/.openclaw/states/bindings.lock"
|
||||||
|
STATE_DIR="$HOME/.openclaw/states"
|
||||||
|
|
||||||
|
# Wait for bindings.lock to disappear
|
||||||
|
while [[ -f "$BINDINGS_LOCK" ]]; do
|
||||||
|
echo "bindings locked, waiting..."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create bindings.lock and update bindings
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
touch "$BINDINGS_LOCK"
|
||||||
|
|
||||||
|
CURRENT_BINDINGS=$(openclaw config get bindings 2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
# Replace accountId "interviewee" -> "{name}" for this agent
|
||||||
|
UPDATED_BINDINGS=$(echo "$CURRENT_BINDINGS" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
bindings = json.load(sys.stdin)
|
||||||
|
for b in bindings:
|
||||||
|
if b.get('agentId') == '$AGENT_ID' and b.get('match', {}).get('accountId') == 'interviewee':
|
||||||
|
b['match']['accountId'] = '$NAME'
|
||||||
|
print(json.dumps(bindings))
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
openclaw config set bindings "$UPDATED_BINDINGS"
|
||||||
|
|
||||||
|
rm -f "$BINDINGS_LOCK"
|
||||||
|
|
||||||
|
# Configure discord account for {name}
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".enabled true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".commands.native true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".commands.nativeSkills true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".token "$BOT_TOKEN"
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".allowBots true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".groupPolicy "open"
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".streaming.mode "off"
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".actions.messages true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".actions.search true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".actions.roles true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".actions.channelInfo true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".actions.events true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".actions.channels true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".dmPolicy "open"
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".allowFrom '["*"]'
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".dm.enabled true
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".execApprovals.enabled false
|
||||||
|
openclaw config set channels.discord.accounts."$NAME".execApprovals.approvers '["*"]'
|
||||||
|
|
||||||
|
~/.openclaw/skills/keycloak-hangman-lab/scripts/create-keycloak-account
|
||||||
|
~/.openclaw/skills/git-hangman-lab/scripts/create-git-account
|
||||||
|
~/.openclaw/skills/git-hangman-lab/scripts/link-keycloak
|
||||||
|
|
||||||
|
echo "Onboarding complete for $NAME."
|
||||||
0
recruitment/workflows/interviewer.md
Normal file
0
recruitment/workflows/interviewer.md
Normal file
48
recruitment/workflows/recruitment.md
Normal file
48
recruitment/workflows/recruitment.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Recruitment Workflow
|
||||||
|
|
||||||
|
Full onboarding flow for new agents. Follow these steps when a request to create a new agent is received.
|
||||||
|
|
||||||
|
> See `claw-skills/docs/standard.md` for skill structure and writing standards.
|
||||||
|
|
||||||
|
## Step 1 — Gather Requirements
|
||||||
|
|
||||||
|
Communicate with the requester to collect:
|
||||||
|
- New agent's `agent-id`
|
||||||
|
- New agent's primary model (`--model`)
|
||||||
|
- New agent's role and position
|
||||||
|
- Whether the agent is a contractor (default: no)
|
||||||
|
|
||||||
|
## Step 2 — Create Agent
|
||||||
|
|
||||||
|
Execute `{baseDir}/scripts/new-agent` with the gathered parameters:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OpenClaw type
|
||||||
|
new-agent --type openclaw --agent-id <agent-id> --model <primary-model>
|
||||||
|
|
||||||
|
# Contractor type
|
||||||
|
new-agent --type contractor --contractor-provider <claude|gemini> --agent-id <agent-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3 — Interview
|
||||||
|
|
||||||
|
Use the `create-discussion-channel` tool to start an interview with the new agent:
|
||||||
|
- Participant: `interviewee`
|
||||||
|
- Discussion guide: `<@YOUR_DISCORD_USER_ID> please refer to {baseDir}/workflows/interviewer.md`
|
||||||
|
|
||||||
|
> Use `ego-mgr get discord-id` (via pcexec) to look up your Discord user ID if unsure.
|
||||||
|
|
||||||
|
After receiving the discussion callback, review the summary:
|
||||||
|
- Contains agent's **name** and **gender** → proceed to Step 4
|
||||||
|
- Missing either field → attempt another `create-discussion-channel` call
|
||||||
|
- Still unavailable → notify the requester and proceed without it
|
||||||
|
|
||||||
|
## Step 4 — Onboard
|
||||||
|
|
||||||
|
Use `proxy-pcexec` to call `{baseDir}/scripts/onboard`:
|
||||||
|
- `proxy-for`: new agent's `agent-id`
|
||||||
|
- Parameters: `--name`, `--role`, `--position`, `--gender`, `--bot-token`
|
||||||
|
|
||||||
|
## Step 5 — Report
|
||||||
|
|
||||||
|
Notify the requester that onboarding is complete.
|
||||||
39
role-skills.json
Normal file
39
role-skills.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"_description": "Maps role and position to skill lists. learn.sh resolves skills by: mandatory → role → position (in that order, deduplicated).",
|
||||||
|
"_extensibility": "Add new roles/positions as needed. Unknown role/position values are silently skipped.",
|
||||||
|
|
||||||
|
"roles": {
|
||||||
|
"developer": [
|
||||||
|
"git-hangman-lab"
|
||||||
|
],
|
||||||
|
"manager": [
|
||||||
|
"git-hangman-lab"
|
||||||
|
],
|
||||||
|
"operator": [
|
||||||
|
"git-hangman-lab"
|
||||||
|
],
|
||||||
|
"mentor": [
|
||||||
|
"git-hangman-lab",
|
||||||
|
"claw-skills"
|
||||||
|
],
|
||||||
|
"secretary": [],
|
||||||
|
"agent-resource-director": [
|
||||||
|
"keycloak-hangman-lab",
|
||||||
|
"recruitment"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"positions": {
|
||||||
|
"tech-leader": [],
|
||||||
|
"delivery-manager": [],
|
||||||
|
"operator": [],
|
||||||
|
"mentor": [
|
||||||
|
"claw-skills"
|
||||||
|
],
|
||||||
|
"administrative-secretary": [],
|
||||||
|
"director": [
|
||||||
|
"keycloak-hangman-lab",
|
||||||
|
"recruitment"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
53
roles/agent-resource-director/ROLE.md
Normal file
53
roles/agent-resource-director/ROLE.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
role: agent-resource-director
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Design and publish suggested workload per agent on a periodic basis
|
||||||
|
- Analyze agent performance: completion rate, efficiency, task complexity
|
||||||
|
- Evaluate ClawSkills branches based on agent performance correlation
|
||||||
|
- Recommend branch merges to mentor based on performance data
|
||||||
|
- Oversee agent recruitment (supervise recruiter)
|
||||||
|
- Maintain role-specific evaluation skills
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- HF Calendar and Task API for workload analysis
|
||||||
|
- Performance metrics: slot completion/defer/abandon rates per agent
|
||||||
|
- Cross-referencing task complexity with agent efficiency
|
||||||
|
- Managing evaluation criteria skills (self-maintained, self-improved)
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Do not merge ClawSkills branches directly — recommend to mentor
|
||||||
|
- Do not assign individual tasks — that is manager's role
|
||||||
|
- Do not write application code
|
||||||
|
|
||||||
|
## Key Processes
|
||||||
|
|
||||||
|
### Workload Design
|
||||||
|
Periodically assigned via HF task. Analyze each agent's recent performance, current project load, and role requirements to produce suggested daily workload (N oncall slots, M task slots per agent).
|
||||||
|
|
||||||
|
### Performance Evaluation
|
||||||
|
- Track per-agent: slots completed vs deferred vs abandoned
|
||||||
|
- Factor in task complexity (not just speed)
|
||||||
|
- Each role has different output quality criteria
|
||||||
|
- Evaluation skills are themselves subject to periodic review and improvement
|
||||||
|
|
||||||
|
### ClawSkills Branch Assessment
|
||||||
|
- Agent performing well → their ClawSkills branch likely contains good improvements
|
||||||
|
- Agent performing poorly → their branch may introduce problems
|
||||||
|
- Provide data-backed recommendations to mentor for merge decisions
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
| Situation | Escalate To |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Skill branch needs technical review | mentor |
|
||||||
|
| Project-level resource allocation | manager |
|
||||||
|
| Infrastructure for new agents | operator |
|
||||||
|
|
||||||
|
## Memory Practice
|
||||||
|
|
||||||
|
After each evaluation cycle, write to `memory/YYYY-MM-DD.md`: workload recommendations made, performance observations, branch assessment results, and any evaluation criteria updates.
|
||||||
36
roles/developer/ROLE.md
Normal file
36
roles/developer/ROLE.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
role: developer
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Implement features, fix bugs, and write code based on assigned HF tasks
|
||||||
|
- Create pull requests via git-ctrl and assist with code reviews
|
||||||
|
- Maintain code quality and write meaningful commit messages
|
||||||
|
- Follow the daily-routine skill for slot planning and task execution
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Git operations: clone, commit, push, create/review PRs (git-hangman-lab skill)
|
||||||
|
- Code editing, debugging, and testing across the workspace
|
||||||
|
- Reading and searching codebases
|
||||||
|
- Running build and test commands
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Do not deploy to production servers — escalate to operator
|
||||||
|
- Do not recruit or onboard new agents — escalate to agent-resource-director
|
||||||
|
- Do not modify ClawSkills main branch directly — use promote-improvements, mentor evaluates
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
| Situation | Escalate To |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Production deployment needed | operator |
|
||||||
|
| Task requirements unclear | manager |
|
||||||
|
| Skill/workflow is broken or missing | mentor |
|
||||||
|
| New agent needed for workload | agent-resource-director |
|
||||||
|
|
||||||
|
## Memory Practice
|
||||||
|
|
||||||
|
After completing each task slot, write a brief summary to `memory/YYYY-MM-DD.md` including what was done, key decisions, and output references (commits, PRs, files).
|
||||||
35
roles/manager/ROLE.md
Normal file
35
roles/manager/ROLE.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
role: manager
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Break down proposals/requirements into actionable HF tasks
|
||||||
|
- Assign tasks to appropriate agents based on their role and capacity
|
||||||
|
- Track project milestones and delivery progress
|
||||||
|
- Coordinate cross-agent work by initiating discussions when needed
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- HF task/project/milestone management via `hf` CLI
|
||||||
|
- Creating and managing discussion channels for cross-agent coordination
|
||||||
|
- Reviewing project status and progress reports
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Do not write production code — assign to developer
|
||||||
|
- Do not deploy — assign to operator
|
||||||
|
- Do not evaluate agent performance — that is agent-resource-director's role
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
| Situation | Escalate To |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Technical implementation question | developer (tech-leader) |
|
||||||
|
| Deployment coordination | operator |
|
||||||
|
| Agent capacity/performance concern | agent-resource-director |
|
||||||
|
| Skill/process improvement needed | mentor |
|
||||||
|
|
||||||
|
## Memory Practice
|
||||||
|
|
||||||
|
After completing each planning or review slot, write a summary to `memory/YYYY-MM-DD.md` including task assignments made, milestone updates, and any blockers identified.
|
||||||
35
roles/mentor/ROLE.md
Normal file
35
roles/mentor/ROLE.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
role: mentor
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Create, maintain, and improve ClawSkills — the shared skill repository
|
||||||
|
- Review skill branches from other agents (via promote-improvements)
|
||||||
|
- Ensure skill quality: clear workflows, working scripts, proper structure
|
||||||
|
- Guide agents on skill usage when they encounter issues
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Full access to ClawSkills repository
|
||||||
|
- Create new skills following claw-skills/docs/standard.md
|
||||||
|
- Evaluate and merge skill branches into main
|
||||||
|
- Review and improve existing workflows and scripts
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Do not merge skill branches based solely on code quality — coordinate with agent-resource-director for performance-based evaluation
|
||||||
|
- Do not deploy applications — escalate to operator
|
||||||
|
- Do not assign tasks — escalate to manager
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
| Situation | Escalate To |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Skill branch performance evaluation data needed | agent-resource-director |
|
||||||
|
| Infrastructure issue blocking skill testing | operator |
|
||||||
|
| New skill request from project requirements | manager |
|
||||||
|
|
||||||
|
## Memory Practice
|
||||||
|
|
||||||
|
After reviewing or creating skills, write to `memory/YYYY-MM-DD.md`: which skills were modified, what changed, which branches were evaluated, and any quality concerns.
|
||||||
36
roles/operator/ROLE.md
Normal file
36
roles/operator/ROLE.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
role: operator
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Deploy applications to production servers (T0, T1, T2, T3)
|
||||||
|
- Manage Docker containers, Nginx configs, and server infrastructure
|
||||||
|
- Monitor server health and respond to operational issues
|
||||||
|
- Maintain deployment scripts and automation
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- SSH to all servers (server.t0 through server.t3)
|
||||||
|
- Docker and docker-compose operations
|
||||||
|
- Server configuration and maintenance
|
||||||
|
- HarborForge Monitor telemetry access
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Do not write application features — escalate to developer
|
||||||
|
- Do not make product decisions — escalate to manager
|
||||||
|
- Infrastructure changes that affect all servers should be documented
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
| Situation | Escalate To |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Code changes needed before deploy | developer |
|
||||||
|
| Deployment priority/scheduling | manager |
|
||||||
|
| Server capacity planning | agent-resource-director |
|
||||||
|
| Deployment skill needs improvement | mentor |
|
||||||
|
|
||||||
|
## Memory Practice
|
||||||
|
|
||||||
|
After each deployment or infrastructure change, write to `memory/YYYY-MM-DD.md`: what was deployed, which servers, any issues encountered, rollback steps if applicable.
|
||||||
32
roles/secretary/ROLE.md
Normal file
32
roles/secretary/ROLE.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
role: secretary
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Record meeting notes and discussion summaries
|
||||||
|
- Manage communications and email correspondence
|
||||||
|
- Maintain team documentation and administrative records
|
||||||
|
- Organize and archive important discussions
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
- Discord channel management and message reading
|
||||||
|
- Document creation and organization
|
||||||
|
- Meeting note formatting and distribution
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Do not make project decisions — record and escalate to manager
|
||||||
|
- Do not modify code or infrastructure
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
| Situation | Escalate To |
|
||||||
|
|-----------|-------------|
|
||||||
|
| Decision needed on meeting outcomes | manager |
|
||||||
|
| Technical questions from communications | developer or operator |
|
||||||
|
|
||||||
|
## Memory Practice
|
||||||
|
|
||||||
|
After each meeting or communication task, write to `memory/YYYY-MM-DD.md`: meeting participants, key decisions, action items, and follow-up needed.
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Sync all skill folders from ClawSkills to ~/.openclaw/skills
|
# Sync skills listed in .mandatory from ClawSkills to ~/.openclaw/skills
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
SKILLS_DIR="$HOME/.openclaw/skills"
|
SKILLS_DIR="$HOME/.openclaw/skills"
|
||||||
|
MANDATORY_FILE="${SCRIPT_DIR}/.mandatory"
|
||||||
|
|
||||||
mkdir -p "$SKILLS_DIR"
|
mkdir -p "$SKILLS_DIR"
|
||||||
|
|
||||||
for folder in "$SCRIPT_DIR"/*/; do
|
if [[ ! -f "$MANDATORY_FILE" ]]; then
|
||||||
folder_name=$(basename "$folder")
|
echo "Error: .mandatory not found at $MANDATORY_FILE"
|
||||||
# Skip this script itself
|
exit 1
|
||||||
if [[ "$folder_name" == "sync-skills.sh" ]]; then
|
fi
|
||||||
continue
|
|
||||||
|
while IFS= read -r skill_name || [[ -n "$skill_name" ]]; do
|
||||||
|
[[ -z "$skill_name" || "$skill_name" == \#* ]] && continue
|
||||||
|
|
||||||
|
skill_dir="${SCRIPT_DIR}/${skill_name}"
|
||||||
|
if [[ -d "$skill_dir" ]]; then
|
||||||
|
echo "Copying $skill_name to $SKILLS_DIR..."
|
||||||
|
cp -rf "$skill_dir" "$SKILLS_DIR/"
|
||||||
|
else
|
||||||
|
echo "Skipping (not found): $skill_name"
|
||||||
fi
|
fi
|
||||||
echo "Copying $folder_name to $SKILLS_DIR..."
|
done < "$MANDATORY_FILE"
|
||||||
cp -rf "$folder" "$SKILLS_DIR/"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
|
|||||||
Reference in New Issue
Block a user