Skip to content

Commit 3237fc8

Browse files
committed
update adding claude.md
1 parent aadca05 commit 3237fc8

13 files changed

Lines changed: 1063 additions & 47 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
if grep -q "caliber" .git/hooks/pre-commit 2>/dev/null; then
3+
exit 0
4+
fi
5+
FLAG="/tmp/caliber-nudge-$(echo "$PWD" | shasum | cut -c1-8)"
6+
find /tmp -maxdepth 1 -name "caliber-nudge-*" -mmin +120 -delete 2>/dev/null
7+
if [ -f "$FLAG" ]; then
8+
exit 0
9+
fi
10+
touch "$FLAG"
11+
printf '{"decision":"block","reason":"Caliber agent config sync is not set up on this machine. Tell the user: This project uses Caliber to keep agent configs in sync — it is not set up yet. Want me to run /setup-caliber? (~30 seconds)"}'

.claude/settings.json

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"hooks": {
3+
"Stop": [
4+
{
5+
"matcher": "",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": ".claude/hooks/caliber-check-sync.sh",
10+
"description": "Caliber: offer setup if not configured"
11+
}
12+
]
13+
}
14+
],
15+
"PostToolUse": [
16+
{
17+
"matcher": "",
18+
"hooks": [
19+
{
20+
"type": "command",
21+
"command": "caliber learn observe",
22+
"description": "Caliber: recording tool usage for session learning"
23+
}
24+
]
25+
}
26+
],
27+
"PostToolUseFailure": [
28+
{
29+
"matcher": "",
30+
"hooks": [
31+
{
32+
"type": "command",
33+
"command": "caliber learn observe --failure",
34+
"description": "Caliber: recording tool failure for session learning"
35+
}
36+
]
37+
}
38+
],
39+
"UserPromptSubmit": [
40+
{
41+
"matcher": "",
42+
"hooks": [
43+
{
44+
"type": "command",
45+
"command": "caliber learn observe --prompt",
46+
"description": "Caliber: recording user prompt for correction detection"
47+
}
48+
]
49+
}
50+
],
51+
"SessionEnd": [
52+
{
53+
"matcher": "",
54+
"hooks": [
55+
{
56+
"type": "command",
57+
"command": "caliber learn finalize --auto",
58+
"description": "Caliber: finalizing session learnings"
59+
}
60+
]
61+
},
62+
{
63+
"matcher": "",
64+
"hooks": [
65+
{
66+
"type": "command",
67+
"command": "caliber refresh --quiet",
68+
"description": "Caliber: auto-refreshing docs based on code changes"
69+
}
70+
]
71+
}
72+
]
73+
},
74+
"permissions": {
75+
"allow": [
76+
"Bash(git *)"
77+
]
78+
}
79+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
name: addon-handler-setup
3+
description: Creates a new AddonHandler registration block inside a `getAddon` method. Use when user says 'add addon', 'register addon', 'new webhosting addon', or adds a new `load_addons` hook handler. Generates `function_requirements('class.AddonHandler')`, fluent `->set_text()`, `->set_cost()`, `->setEnable()`, `->setDisable()`, `->register()` chain followed by `$service->addAddon()`. Do NOT use for editing existing addons or for non-addon hook handlers.
4+
---
5+
# Addon Handler Setup
6+
7+
## Critical
8+
9+
- `function_requirements('class.AddonHandler')` MUST be called before `new \AddonHandler()` — the class is lazy-loaded.
10+
- `$service->addAddon($addon)` MUST be the last line in `getAddon()` — omitting it silently drops the addon.
11+
- The `getAddon` method signature MUST accept `GenericEvent $event` and extract `$service` via `$event->getSubject()`.
12+
- Register the hook in `getHooks()` using `self::$module.'.load_addons'` as the key — never hardcode the module string.
13+
- `doEnable` and `doDisable` MUST have the signature: `(\ServiceHandler $serviceOrder, $repeatInvoiceId, $regexMatch = false)`.
14+
15+
## Instructions
16+
17+
1. **Add the hook to `getHooks()`** in `src/Plugin.php`. The key is `self::$module.'.load_addons'`, value is `[__CLASS__, 'getAddon']`.
18+
```php
19+
public static function getHooks(): array {
20+
return [
21+
self::$module.'.load_addons' => [__CLASS__, 'getAddon'],
22+
self::$module.'.settings' => [__CLASS__, 'getSettings'],
23+
];
24+
}
25+
```
26+
Verify `getHooks()` returns this key before proceeding.
27+
28+
2. **Write the `getAddon` method** immediately after `getHooks()`. Extract `$service` from the event, call `function_requirements`, instantiate `\AddonHandler`, chain all setters, call `register()`, then call `$service->addAddon($addon)`.
29+
```php
30+
public static function getAddon(GenericEvent $event)
31+
{
32+
/** @var \ServiceHandler $service */
33+
$service = $event->getSubject();
34+
function_requirements('class.AddonHandler');
35+
$addon = new \AddonHandler();
36+
$addon->setModule(self::$module)
37+
->set_text('Your Addon Name')
38+
->set_text_match('Your Addon Name (.*)')
39+
->set_cost(YOUR_COST_CONSTANT)
40+
->setEnable([__CLASS__, 'doEnable'])
41+
->setDisable([__CLASS__, 'doDisable'])
42+
->register();
43+
$service->addAddon($addon);
44+
}
45+
```
46+
`set_text_match` must be a regex where `(.*)` captures the variable part (e.g., IP or plan name).
47+
48+
3. **Write `doEnable`**. It receives `\ServiceHandler $serviceOrder`. Retrieve service info and settings first. Skip WHM calls for `WEB_DIRECTADMIN` and `WEB_STORAGE` types. Log every WHM response before acting on it.
49+
```php
50+
public static function doEnable(\ServiceHandler $serviceOrder, $repeatInvoiceId, $regexMatch = false)
51+
{
52+
$serviceInfo = $serviceOrder->getServiceInfo();
53+
$settings = get_module_settings(self::$module);
54+
if ($regexMatch === false) {
55+
function_requirements('get_service_master');
56+
$serverdata = get_service_master($serviceInfo[$settings['PREFIX'].'_server'], self::$module);
57+
if (in_array($serviceInfo[$settings['PREFIX'].'_type'], [get_service_define('WEB_DIRECTADMIN'), get_service_define('WEB_STORAGE')])) {
58+
// skip — unsupported type
59+
} else {
60+
// perform enable logic
61+
myadmin_log(self::$module, 'info', 'Enabling addon for '.$serviceInfo[$settings['PREFIX'].'_id'], __LINE__, __FILE__, self::$module, $serviceInfo[$settings['PREFIX'].'_id']);
62+
}
63+
}
64+
}
65+
```
66+
67+
4. **Write `doDisable`**. Same signature as `doEnable`. Always call `add_output('...')` and send an admin cancellation email via `(new \MyAdmin\Mail())->adminMail()`.
68+
```php
69+
public static function doDisable(\ServiceHandler $serviceOrder, $repeatInvoiceId, $regexMatch = false)
70+
{
71+
$serviceInfo = $serviceOrder->getServiceInfo();
72+
$settings = get_module_settings(self::$module);
73+
$db = get_module_db(self::$module);
74+
// disable logic here
75+
add_output('Your Addon Order Canceled');
76+
$subject = $settings['TBLNAME'].' '.$serviceInfo[$settings['PREFIX'].'_id'].' Canceled Your Addon';
77+
$email = $settings['TBLNAME'].' ID: '.$serviceInfo[$settings['PREFIX'].'_id'].'<br>Description: '.self::$name.'<br>';
78+
(new \MyAdmin\Mail())->adminMail($subject, $email, false, 'admin/website_ip_canceled.tpl');
79+
}
80+
```
81+
82+
5. **Run tests** to verify hook registration and addon wiring:
83+
```bash
84+
vendor/bin/phpunit tests/ -v
85+
```
86+
87+
## Examples
88+
89+
**User says:** "Add a new addon for SSL certificates"
90+
91+
**Actions taken:**
92+
1. Add `self::$module.'.load_addons' => [__CLASS__, 'getAddon']` to `getHooks()` in `src/Plugin.php`.
93+
2. Create `getAddon(GenericEvent $event)` with `set_text('SSL Certificate')`, `set_text_match('SSL Certificate (.*)')`, `set_cost(SSL_CERT_COST)`.
94+
3. Create `doEnable` skipping WHM for DirectAdmin/Storage, logging all API calls.
95+
4. Create `doDisable` calling `add_output('SSL Certificate Canceled')` and `adminMail()`.
96+
97+
**Result:** Addon appears in `webhosting.load_addons` event, is purchasable at `SSL_CERT_COST`, enable/disable callbacks are wired.
98+
99+
## Common Issues
100+
101+
- **`Class 'AddonHandler' not found`**: `function_requirements('class.AddonHandler')` was not called before `new \AddonHandler()`. Add it as the first line inside `getAddon()`.
102+
- **Addon silently missing from the service**: `$service->addAddon($addon)` was omitted. It must follow `->register()``register()` alone does not attach the addon to the service.
103+
- **`doEnable` never fires for DirectAdmin accounts**: This is expected — the `in_array(...WEB_DIRECTADMIN...)` guard intentionally skips those. Do not remove it.
104+
- **`set_text_match` not matching on invoice**: The regex passed to `set_text_match` must include `(.*)` and match the exact invoice line text. Test with the literal string from a real invoice.
105+
- **WHM API returns null response**: `function_requirements('whm_api')` was skipped. Always call it before `new \xmlapi()`; always `myadmin_log` the raw response string before `json_decode()`.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: find-skills
3+
description: Discovers and installs community skills from the public registry. Use when the user mentions a technology, framework, or task that could benefit from specialized skills not yet installed, asks 'how do I do X', 'find a skill for X', or starts work in a new technology area. Proactively suggest when the user's task involves tools or frameworks without existing skills.
4+
---
5+
6+
# Find Skills
7+
8+
Search the public skill registry for community-contributed skills
9+
relevant to the user's current task and install them into this project.
10+
11+
## Instructions
12+
13+
1. Identify the key technologies, frameworks, or task types from the
14+
user's request that might have community skills available
15+
2. Ask the user: "Would you like me to search for community skills
16+
for [identified technologies]?"
17+
3. If the user agrees, run:
18+
```bash
19+
caliber skills --query "<relevant terms>"
20+
```
21+
This outputs the top 5 matching skills with scores and descriptions.
22+
4. Present the results to the user and ask which ones to install
23+
5. Install the selected skills:
24+
```bash
25+
caliber skills --install <slug1>,<slug2>
26+
```
27+
6. Read the installed SKILL.md files to load them into your current
28+
context so you can use them immediately in this session
29+
7. Summarize what was installed and continue with the user's task
30+
31+
## Examples
32+
33+
User: "let's build a web app using React"
34+
-> "I notice you want to work with React. Would you like me to search
35+
for community skills that could help with React development?"
36+
-> If yes: run `caliber skills --query "react frontend"`
37+
-> Show the user the results, ask which to install
38+
-> Run `caliber skills --install <selected-slugs>`
39+
-> Read the installed files and continue
40+
41+
User: "help me set up Docker for this project"
42+
-> "Would you like me to search for Docker-related skills?"
43+
-> If yes: run `caliber skills --query "docker deployment"`
44+
45+
User: "I need to write tests for this Python ML pipeline"
46+
-> "Would you like me to find skills for Python ML testing?"
47+
-> If yes: run `caliber skills --query "python machine-learning testing"`
48+
49+
## When NOT to trigger
50+
51+
- The user is working within an already well-configured area
52+
- You already suggested skills for this technology in this session
53+
- The user is in the middle of urgent debugging or time-sensitive work
54+
- The technology is too generic (e.g. just "code" or "programming")
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
name: module-settings-field
3+
description: Adds a new settings field inside getSettings() using add_text_setting or similar method on the MyAdmin\Settings object. Use when user says 'add setting', 'new config option', 'add cost field', 'register a setting', or extends the webhosting.settings handler. Always wraps label/description in _() for i18n and calls setTarget('module') before and setTarget('global') after. Do NOT use for reading settings at runtime or for non-settings configuration.
4+
---
5+
# Module Settings Field
6+
7+
## Critical
8+
- **Always** call `$settings->setTarget('module')` before adding fields and `$settings->setTarget('global')` after — omitting either corrupts the settings target for other plugins.
9+
- **Always** wrap every human-readable string (group name, label, description) in `_()` for gettext i18n.
10+
- The `getSettings` method receives a `GenericEvent` — extract the subject with `$event->getSubject()` to get the `\MyAdmin\Settings` instance.
11+
- The default value **must** use `(defined(CONST_NAME) ? CONST_NAME : fallback)` — never reference an undefined constant directly, as it causes a fatal error.
12+
- This method is a hook handler; it must be registered in `getHooks()` under `self::$module.'.settings'`.
13+
14+
## Instructions
15+
16+
1. **Verify the hook is registered** in `getHooks()` in `src/Plugin.php`:
17+
```php
18+
self::$module.'.settings' => [__CLASS__, 'getSettings'],
19+
```
20+
If missing, add it to the returned array before proceeding.
21+
22+
2. **Open `src/Plugin.php`** and locate the `getSettings(GenericEvent $event)` method. All field additions happen inside this method only.
23+
24+
3. **Insert the new field** between the existing `setTarget('module')` and `setTarget('global')` calls, following this exact signature:
25+
```php
26+
$settings->add_text_setting(
27+
self::$module, // module slug, always self::$module
28+
_('Group Name'), // settings group label — wrap in _()
29+
'snake_case_key', // config key: lowercase, underscores, no prefix
30+
_('Field Label'), // human label — wrap in _()
31+
_('One sentence description.'), // help text — wrap in _()
32+
(defined('CONST_NAME') ? CONST_NAME : default_value) // safe default
33+
);
34+
```
35+
36+
4. **Define the matching constant** (if introducing a new cost/limit). Add the constant name to the module's constants file or to the relevant `defines.php`. The constant name must be `UPPER_SNAKE_CASE` and match the key used in `add_text_setting`.
37+
38+
5. **Verify** the full `getSettings` method still opens with `setTarget('module')` and closes with `setTarget('global')` with **no other `setTarget` calls** in between.
39+
40+
6. **Run tests** to confirm no fatal errors:
41+
```bash
42+
vendor/bin/phpunit tests/ -v
43+
```
44+
45+
## Examples
46+
47+
**User says:** "Add a setting for the maximum number of dedicated IPs per account."
48+
49+
**Actions taken:**
50+
1. Confirm `self::$module.'.settings' => [__CLASS__, 'getSettings']` exists in `getHooks()` in `src/Plugin.php`.
51+
2. Add constant `WEBSITE_IP_MAX` to the defines file with default `5`.
52+
3. In `getSettings()`, insert after the existing `add_text_setting` call:
53+
```php
54+
$settings->add_text_setting(self::$module, _('Costs & Limits'), 'website_ip_max', _('Max Dedicated IPs'), _('Maximum number of dedicated IPs allowed per webhosting account.'), (defined('WEBSITE_IP_MAX') ? WEBSITE_IP_MAX : 5));
55+
```
56+
4. Confirm `setTarget('module')` is first and `setTarget('global')` is last.
57+
58+
**Result — complete `getSettings` method:**
59+
```php
60+
public static function getSettings(GenericEvent $event)
61+
{
62+
/** @var \MyAdmin\Settings $settings **/
63+
$settings = $event->getSubject();
64+
$settings->setTarget('module');
65+
$settings->add_text_setting(self::$module, _('Costs & Limits'), 'website_ip_cost', _('Dedicated IP Cost'), _('This is the cost for purchasing an additional IP on top of a Website.'), (defined('WEBSITE_IP_COST') ? WEBSITE_IP_COST : 3));
66+
$settings->add_text_setting(self::$module, _('Costs & Limits'), 'website_ip_max', _('Max Dedicated IPs'), _('Maximum number of dedicated IPs allowed per webhosting account.'), (defined('WEBSITE_IP_MAX') ? WEBSITE_IP_MAX : 5));
67+
$settings->setTarget('global');
68+
}
69+
```
70+
71+
## Common Issues
72+
73+
- **`PHP Fatal error: Undefined constant 'WEBSITE_IP_MAX'`**: You referenced the constant directly as the default value. Fix: always guard with `(defined('CONST_NAME') ? CONST_NAME : fallback)`.
74+
- **Settings from other plugins appear under the wrong module**: You forgot `setTarget('global')` at the end of `getSettings()`. The target remains `'module'` for all subsequent plugin settings registrations. Add `$settings->setTarget('global');` as the final line.
75+
- **Label/description appears as raw PHP string (not translated)**: You omitted `_()` around a string. Wrap every human-readable argument in `_()`.
76+
- **`Call to undefined method ... add_text_setting`**: `$event->getSubject()` returned something other than `\MyAdmin\Settings`. Confirm the hook key in `getHooks()` is exactly `self::$module.'.settings'` (not `'webhosting_settings'` or similar).
77+
- **New field not visible in admin UI**: The hook was not registered. Verify `getHooks()` in `src/Plugin.php` returns `self::$module.'.settings' => [__CLASS__, 'getSettings']` and that the plugin is listed in `include/config/plugins.json` in the parent MyAdmin install.

0 commit comments

Comments
 (0)