Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e251726
feat(users): add updateBannerUrl mutation to store banner as custom f…
rwhite27 May 21, 2026
bbe759c
fix(users): allow non-admin users to update their own banner URL
rwhite27 May 21, 2026
b18f459
Merge branch 'development' of github.com:bakaphp/kanvas-ecosystem-api…
rwhite27 May 21, 2026
a28076a
feat(agents): fix agent type upsert key, add latestDeployment relatio…
rwhite27 May 25, 2026
1c6f43d
revert: remove updateBannerUrl mutation — no longer needed
rwhite27 May 25, 2026
b113285
revert: restore assertReadyForDeployment validation in AgentChannelIn…
rwhite27 May 25, 2026
f28a0fe
Merge branch 'development' into hermes-agents-feature
rwhite27 May 25, 2026
a2ea1d3
style(agents): remove trailing whitespace in Agent model
rwhite27 May 25, 2026
0cc2d66
feat(intelligence): add OpenTelemetry token usage collection pipeline
rwhite27 Jun 3, 2026
a64a54b
style(console): reorder imports in Kernel alphabetically
rwhite27 Jun 3, 2026
10305e4
fix(intelligence): add missing CollectAllDeploymentsUsageCommand and …
rwhite27 Jun 3, 2026
59896c0
Merge remote-tracking branch 'origin/development' into feat/opentelem…
rwhite27 Jun 3, 2026
c73c8ed
Merge branch 'development' of github.com:bakaphp/kanvas-ecosystem-api…
rwhite27 Jun 8, 2026
080cad7
feat: complete OTel pipeline — forward spans directly to Laravel for …
rwhite27 Jun 9, 2026
2defbf2
feat: add otel-collector service to development docker-compose
rwhite27 Jun 11, 2026
7b14bc8
fix: make Intelligence migrations idempotent with hasColumn guards
rwhite27 Jun 11, 2026
9424a3e
docs: add kanvas-init-env and sync-kanvas-db Claude Code skills
rwhite27 Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions .claude/skills/kanvas-init-env/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
name: kanvas-init-env
description: >
Initializes a fresh Kanvas local environment: creates a new App, registers an
admin user (which auto-creates the Company and Company Branch), and returns
the API key + auth token needed to start testing. Invoke whenever the user
says "init env", "setup local", "create new app", "bootstrap kanvas",
"new local environment", "spin up kanvas", or wants to start from scratch
on a fresh Kanvas install.
---

# kanvas-init-env

Bootstraps a new Kanvas local environment inside the `phpkanvas-ecosystem`
Docker container. Creates: App → registers Admin User (auto-creates Company +
Branch) → returns credentials.

**Prerequisites:** `kanvas:setup-ecosystem` must have already been run (all
migrations applied, base seeders done). If not, run the `sync-kanvas-db`
skill first.

---

## Step 1 — Verify the ecosystem is seeded

Check that the default Kanvas app (id=1) and an owner user (id=1) exist,
since `kanvas:app-create` requires an existing user as owner:

```bash
docker exec phpkanvas-ecosystem php artisan tinker --execute="
echo 'Users: ' . \Kanvas\Users\Models\Users::count() . PHP_EOL;
echo 'Apps: ' . \Kanvas\Apps\Models\Apps::count() . PHP_EOL;
"
```

If counts are 0, the base seeders haven't run. Run:

```bash
docker exec phpkanvas-ecosystem php artisan db:seed
```

---

## Step 2 — Create the App

Ask the user for (or use these defaults if they say "use defaults"):
- **name** e.g. `"Local Test App"`
- **domain** e.g. `"localhost"`
- **url** e.g. `"http://localhost"`
- **owner email** — the existing user who will own this app (default: the
seeded user, usually `admin@kantvasapp.com` or `anonymous@kanvas.dev`)
- **ecosystem_auth** — `true` (share auth with Kanvas ecosystem)
- **payments_active** — `false` for local testing

Create via tinker (non-interactive, scriptable):

```bash
docker exec phpkanvas-ecosystem php artisan tinker --execute="
use Kanvas\Apps\Actions\CreateAppsAction;
use Kanvas\Apps\DataTransferObject\AppInput;
use Kanvas\Users\Models\Users;

\$owner = Users::first();
\$data = AppInput::from([
'name' => 'Local Test App',
'url' => 'http://localhost',
'description' => 'Local development app',
'domain' => 'localhost',
'is_actived' => 1,
'ecosystem_auth' => 1,
'payments_active' => 0,
'is_public' => 1,
'domain_based' => 0,
]);
\$app = (new CreateAppsAction(\$data, \$owner))->execute();
echo 'App created!' . PHP_EOL;
echo 'ID: ' . \$app->id . PHP_EOL;
echo 'Key: ' . \$app->key . PHP_EOL;
" 2>/dev/null
```

Save the **API Key** from the output — every subsequent request needs it as
the `X-Kanvas-Key` header.

Alternatively, run the interactive artisan command:

```bash
docker exec -it phpkanvas-ecosystem php artisan kanvas:app-create
```

---

## Step 3 — Register the Admin User (auto-creates Company + Branch)

The `register` mutation creates the user, a default Company, and a default
Company Branch in one call. The first user registered in an app gets the
`Admin` role by default.

Replace `<APP_KEY>` with the key from Step 2:

```bash
curl -s -X POST http://localhost/graphql \
-H "Content-Type: application/json" \
-H "X-Kanvas-Key: <APP_KEY>" \
-d '{
"query": "mutation Register($data: RegisterInput!) { register(data: $data) { user { id uuid email default_company } token { token refresh_token token_expires } } }",
"variables": {
"data": {
"firstname": "Admin",
"lastname": "User",
"displayname": "admin",
"email": "admin@localtest.com",
"password": "Test1234!",
"password_confirmation": "Test1234!",
"company_name": "Local Test Company"
}
}
}' | python3 -m json.tool 2>/dev/null || \
curl -s -X POST http://localhost/graphql \
-H "Content-Type: application/json" \
-H "X-Kanvas-Key: <APP_KEY>" \
-d '{"query":"mutation { register(data: { firstname: \"Admin\", lastname: \"User\", displayname: \"admin\", email: \"admin@localtest.com\", password: \"Test1234!\", password_confirmation: \"Test1234!\" }) { user { id email } token { token } } }"}'
```

Save from the response:
- `user.id` — the admin user ID
- `token.token` — Bearer token for authenticated requests
- `user.default_company` — the auto-created company ID

---

## Step 4 — Verify entities were created

```bash
docker exec phpkanvas-ecosystem php artisan tinker --execute="
use Kanvas\Apps\Models\Apps;
use Kanvas\Users\Models\Users;
use Kanvas\Companies\Models\Companies;
use Kanvas\Companies\Models\CompaniesBranches;

\$app = Apps::latest()->first();
echo '=== App ===' . PHP_EOL;
echo 'Name: ' . \$app->name . PHP_EOL;
echo 'Key: ' . \$app->key . PHP_EOL;

\$user = Users::getByEmail('admin@localtest.com');
echo PHP_EOL . '=== User ===' . PHP_EOL;
echo 'ID: ' . \$user->id . PHP_EOL;
echo 'Email: ' . \$user->email . PHP_EOL;

\$company = Companies::find(\$user->default_company);
echo PHP_EOL . '=== Company ===' . PHP_EOL;
echo 'Name: ' . \$company->name . PHP_EOL;

\$branch = CompaniesBranches::where('companies_id', \$company->id)->first();
echo PHP_EOL . '=== Branch ===' . PHP_EOL;
echo 'Name: ' . \$branch->name . PHP_EOL;
" 2>/dev/null
```

---

## Step 5 — Report credentials to the user

Present a summary block they can copy into `.env` or pass to the admin UI:

```
Environment ready
─────────────────────────────────────────
App Name: Local Test App
App Key: <key> ← X-Kanvas-Key header / NEXT_PUBLIC_KANVAS_KEY
─────────────────────────────────────────
Admin Email: admin@localtest.com
Admin Password: Test1234!
Bearer Token: <token> ← Authorization: Bearer <token>
─────────────────────────────────────────
Company: Local Test Company (id: N)
Branch: Default Branch (id: N)
─────────────────────────────────────────
```

---

## Troubleshooting

| Error | Cause | Fix |
|---|---|---|
| `No query results for model [Users]` on app create | No users seeded yet | Run `docker exec phpkanvas-ecosystem php artisan db:seed` first |
| `Email has already been taken` | User registered in this app already | Change the email or use a different app key |
| `422 / validation error` on register | `displayname` already taken globally | Change the displayname |
| `Role not found` | Roles not created | Run `docker exec phpkanvas-ecosystem php artisan kanvas:create-role Admin` |
| GraphQL returns `Unauthenticated` | Wrong or missing `X-Kanvas-Key` | Double-check the key from Step 2 |
| `curl: connection refused` | API not running | Make sure Docker containers are up: `docker compose up -d` |
140 changes: 140 additions & 0 deletions .claude/skills/sync-intelligence-db/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
name: sync-kanvas-db
description: >
Runs pending database migrations across ALL Kanvas Laravel API databases on
the local Docker environment. Invoke whenever the user says "sync db",
"run migrations", "sync database", "pull from dev", "run pending migrations",
or when any page returns a 500/GraphQL error caused by a missing table or
column. Also invoke proactively after any `git pull` or `git merge` from
dev/main when migration files have changed.
---

# sync-kanvas-db

Applies pending migrations to every database inside the `phpkanvas-ecosystem`
Docker container. Kanvas uses multiple separate MySQL databases, each with its
own migration subfolder and Laravel connection name.

## Container and paths

- **Container**: `phpkanvas-ecosystem`
- **API repo (host)**: `/Users/mcwhite/Documents/docker_projects/kanvas/kanvas-ecosystem-api`
- **Migrations root (inside container)**: `database/migrations/`

## Database map

Each subfolder uses a different Laravel connection. Run migrations for the
subfolder with the matching `--database` flag.

| Migration path | `--database` flag | Notes |
|---|---|---|
| `database/migrations/` (root) | *(omit — default mysql)* | Core platform tables |
| `database/migrations/ActionEngine/` | `--database=action_engine` | |
| `database/migrations/Event/` | `--database=event` | |
| `database/migrations/Guild/` | `--database=crm` | CRM module |
| `database/migrations/Intelligence/` | `--database=intelligence` | AI agents, tools, deployments |
| `database/migrations/Inventory/` | `--database=inventory` | |
| `database/migrations/Social/` | `--database=social` | |
| `database/migrations/Souk/` | `--database=commerce` | Commerce/orders |
| `database/migrations/Subscription/` | *(omit — default mysql)* | Subscription tables in main DB |
| `database/migrations/Workflow/` | `--database=workflow` | |

## Step 1 — Find pending migrations

Run this for each path you care about (or all of them after a big pull):

```bash
# For a specific path:
docker exec phpkanvas-ecosystem php artisan migrate:status \
--path=database/migrations/Intelligence --database=intelligence 2>/dev/null \
| grep "Pending"

# For the default-connection paths (root, ActionEngine, Subscription):
docker exec phpkanvas-ecosystem php artisan migrate:status \
--path=database/migrations 2>/dev/null | grep "Pending"
```

Filter results to only entries under the path you queried — artisan sometimes
shows migrations from the whole project.

## Step 2 — Run each pending migration individually

Run in chronological order (filename = timestamp order), one at a time.

```bash
docker exec phpkanvas-ecosystem php artisan migrate \
--path=<migration_file_path> \
[--database=<connection>] \
--force 2>&1
```

### Handling failures gracefully

These errors mean the schema already diverged on your local DB — skip, don't
abort:

| Error | Action |
|---|---|
| `Column already exists` (1060) | Schema already present. Fake-mark (Step 3) and continue. |
| `Table already exists` (1050) | Table already present. Fake-mark (Step 3) and continue. |
| `Duplicate key name` (1061) | Index already exists. Fake-mark (Step 3) and continue. |
| `Field 'uuid' doesn't have a default value` (1364) | Seed migration omits `uuid`. Fix: add `'uuid' => \Illuminate\Support\Str::uuid()->toString()` to each insert in the migration file, then re-run. |
| `Column not found` (1054) after `->after(...)` | The referenced column doesn't exist locally. Fix: remove the `->after('...')` call in the migration file, then re-run. |
| Any other error | Log it and continue to the next migration. |

When editing a migration file to fix a local divergence, make the change
minimally (remove `->after()`, add `uuid`, wrap in `hasColumn()` guard). Don't
commit these fixups unless asked.

## Step 3 — Fake-mark already-applied migrations

If the schema change already exists but the migration record doesn't, insert a
fake record so Laravel stops reporting it as Pending.

**IMPORTANT**: you must insert into the correct DB's `migrations` table — match
the `--database` connection from the table above.

```bash
# Example for intelligence DB:
docker exec phpkanvas-ecosystem php artisan tinker --execute="
DB::connection('intelligence')->table('migrations')->insertOrIgnore([
'migration' => '2026_05_15_000000_migration_name_without_php',
'batch' => 99
]);
"

# Example for default mysql DB:
docker exec phpkanvas-ecosystem php artisan tinker --execute="
DB::table('migrations')->insertOrIgnore([
'migration' => '2026_05_15_000000_migration_name_without_php',
'batch' => 99
]);
"
```

Use only when the schema change definitely exists but the migration keeps
showing as Pending.

## Step 4 — Report

After processing all paths:

```
Kanvas DB sync complete
✓ Ran: <list>
⚠ Skipped: <list with reason>
✗ Failed: <list with error>
```

If nothing was pending, say so and exit cleanly.

## Common missing tables (reference)

These are frequently missing on local DBs that haven't been fully migrated:

| Table | DB | Migration file |
|---|---|---|
| `agents_kanvas_modules` | `intelligence` | `2026_05_15_040000_create_agents_kanvas_modules.php` |
| `agent_daily_cycles` | `intelligence` | `2026_05_15_080000_create_agent_daily_cycles.php` |
| `agent_sleep_phases` | `intelligence` | `2026_05_15_090000_create_agent_sleep_phases.php` |
| `nervous_system_tool_categories` | `intelligence` | `2026_05_15_120000_create_nervous_system_tool_categories.php` |
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ class CollectDeploymentUsageCommand extends Command

public function handle(): void
{
if (config('otel.enabled', false)) {
$this->info('OTel is active — token usage is collected via the collector pipeline. Skipping SSH poll.');

return;
}

/** @var Apps $app */
$app = Apps::getById((int) $this->argument('app_id'));
$this->overwriteAppService($app);
Expand Down
Loading
Loading