showpane 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +14 -1
  2. package/bundle/meta/scaffold-manifest.json +73 -0
  3. package/bundle/scaffold/VERSION +1 -0
  4. package/bundle/scaffold/__dot__env.example +24 -0
  5. package/bundle/scaffold/__dot__gitignore +41 -0
  6. package/bundle/scaffold/docker/Caddyfile +3 -0
  7. package/bundle/scaffold/docker/Dockerfile +30 -0
  8. package/bundle/scaffold/docker-compose.yml +53 -0
  9. package/bundle/scaffold/next.config.ts +20 -0
  10. package/bundle/scaffold/package-lock.json +5843 -0
  11. package/bundle/scaffold/package.json +42 -0
  12. package/bundle/scaffold/postcss.config.js +6 -0
  13. package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
  14. package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
  15. package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
  16. package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
  17. package/bundle/scaffold/prisma/schema.local.prisma +131 -0
  18. package/bundle/scaffold/prisma/schema.prisma +128 -0
  19. package/bundle/scaffold/prisma/seed.ts +49 -0
  20. package/bundle/scaffold/public/example-avatar.svg +4 -0
  21. package/bundle/scaffold/public/example-logo.svg +4 -0
  22. package/bundle/scaffold/public/robots.txt +2 -0
  23. package/bundle/scaffold/scripts/backup.sh +19 -0
  24. package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
  25. package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
  26. package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
  27. package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
  28. package/bundle/scaffold/scripts/restore.sh +31 -0
  29. package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
  30. package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
  31. package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
  32. package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
  33. package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
  34. package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
  35. package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
  36. package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
  37. package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
  38. package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
  39. package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
  40. package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
  41. package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
  42. package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
  43. package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
  44. package/bundle/scaffold/src/app/api/health/route.ts +19 -0
  45. package/bundle/scaffold/src/app/globals.css +7 -0
  46. package/bundle/scaffold/src/app/layout.tsx +25 -0
  47. package/bundle/scaffold/src/app/page.tsx +171 -0
  48. package/bundle/scaffold/src/components/portal-login.tsx +169 -0
  49. package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
  50. package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
  51. package/bundle/scaffold/src/lib/branding.ts +50 -0
  52. package/bundle/scaffold/src/lib/client-auth.ts +98 -0
  53. package/bundle/scaffold/src/lib/client-portals.ts +134 -0
  54. package/bundle/scaffold/src/lib/control-plane.ts +100 -0
  55. package/bundle/scaffold/src/lib/db.ts +7 -0
  56. package/bundle/scaffold/src/lib/files.ts +124 -0
  57. package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
  58. package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
  59. package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
  60. package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
  61. package/bundle/scaffold/src/lib/storage.ts +204 -0
  62. package/bundle/scaffold/src/lib/token.ts +186 -0
  63. package/bundle/scaffold/src/lib/utils.ts +6 -0
  64. package/bundle/scaffold/src/middleware.ts +61 -0
  65. package/bundle/scaffold/tailwind.config.ts +15 -0
  66. package/bundle/scaffold/tests/__dot__gitkeep +0 -0
  67. package/bundle/scaffold/tsconfig.json +23 -0
  68. package/bundle/scaffold/vitest.config.ts +13 -0
  69. package/bundle/toolchain/VERSION +1 -0
  70. package/bundle/toolchain/bin/check-slug.ts +59 -0
  71. package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
  72. package/bundle/toolchain/bin/create-portal.ts +71 -0
  73. package/bundle/toolchain/bin/delete-portal.ts +48 -0
  74. package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
  75. package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
  76. package/bundle/toolchain/bin/generate-share-link.ts +68 -0
  77. package/bundle/toolchain/bin/list-portals.ts +53 -0
  78. package/bundle/toolchain/bin/materialize-file.ts +35 -0
  79. package/bundle/toolchain/bin/query-analytics.ts +88 -0
  80. package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
  81. package/bundle/toolchain/bin/showpane-config +63 -0
  82. package/bundle/toolchain/bin/tsconfig.json +13 -0
  83. package/bundle/toolchain/skills/VERSION +1 -0
  84. package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
  85. package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
  86. package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
  87. package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
  88. package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
  89. package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
  90. package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
  91. package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
  92. package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
  93. package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
  94. package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
  95. package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
  96. package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
  97. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
  98. package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
  99. package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
  100. package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
  101. package/bundle/toolchain/skills/shared/preamble.md +137 -0
  102. package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
  103. package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
  104. package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
  105. package/dist/index.js +873 -166
  106. package/package.json +3 -2
@@ -0,0 +1,263 @@
1
+ ---
2
+ name: portal-analytics
3
+ description: |
4
+ View portal engagement analytics in the terminal. Use when asked to "show analytics",
5
+ "portal stats", "how is the portal doing", "engagement", or "view counts". (showpane)
6
+ allowed-tools: [Bash, Read, Glob, Grep]
7
+ ---
8
+
9
+ ## Preamble (run first)
10
+
11
+ Before doing anything else, execute this block in a Bash tool call:
12
+
13
+ ```bash
14
+ CONFIG="$HOME/.showpane/config.json"
15
+ if [ ! -f "$CONFIG" ]; then
16
+ echo "Showpane not configured. Run /portal setup first."
17
+ exit 1
18
+ fi
19
+ APP_PATH=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('app_path',''))" 2>/dev/null)
20
+ DEPLOY_MODE=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('deploy_mode','docker'))" 2>/dev/null)
21
+ ORG_SLUG=$(cat "$CONFIG" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
22
+ APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
23
+ if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
24
+ DATABASE_URL="${DATABASE_URL:-}"
25
+ if [ ! -d "$APP_PATH/node_modules/.prisma" ]; then
26
+ echo "App dependencies not installed. Run: cd $APP_PATH && npm install"
27
+ exit 1
28
+ fi
29
+ SKILL_DIR="${SHOWPANE_TOOLCHAIN_DIR:-$HOME/.showpane/current}"
30
+ SKILL_VERSION=$(cat "$SKILL_DIR/VERSION" 2>/dev/null || echo "unknown")
31
+ echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
32
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
33
+ [ -f "$LEARN_FILE" ] && echo "LEARNINGS: $(wc -l < "$LEARN_FILE" | tr -d ' ') loaded" || echo "LEARNINGS: 0"
34
+
35
+ # Predictive next-skill suggestion
36
+ if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
37
+ _RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
38
+ [ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
39
+ fi
40
+
41
+ # Search relevant learnings
42
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
43
+ if [ -f "$LEARN_FILE" ]; then
44
+ _LEARN_COUNT=$(wc -l < "$LEARN_FILE" 2>/dev/null | tr -d ' ')
45
+ echo "LEARNINGS: $_LEARN_COUNT entries"
46
+ if [ "$_LEARN_COUNT" -gt 0 ] 2>/dev/null; then
47
+ echo "RECENT_LEARNINGS:"
48
+ tail -5 "$LEARN_FILE" 2>/dev/null
49
+ fi
50
+ fi
51
+
52
+ # Track skill execution
53
+ SHOWPANE_TIMELINE="$HOME/.showpane/timeline.jsonl"
54
+ mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
55
+ echo '{"skill":"portal-analytics","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
56
+ ```
57
+
58
+ If RECENT_SKILLS is shown, suggest the likely next skill:
59
+ - After portal-create → suggest /portal-preview
60
+ - After portal-preview → suggest /portal-deploy or /portal-share
61
+ - After portal-deploy → suggest /portal-status or /portal-verify
62
+ - After portal-setup → suggest /portal-create
63
+ - After portal-credentials → suggest /portal-share
64
+ - After portal-update → suggest /portal-deploy
65
+
66
+ If RECENT_LEARNINGS is shown, review them before proceeding. Past learnings may contain
67
+ relevant warnings or tips for this operation. Apply them where relevant but don't
68
+ mention them unless they directly affect the current task.
69
+
70
+ ## Overview
71
+
72
+ This skill queries the analytics backend for a specific portal or all portals in the organization and presents engagement data as a formatted ASCII dashboard in the terminal. It uses the `query-analytics.ts` bin script which reads event data from the database via Prisma.
73
+
74
+ Analytics give the portal owner a quick read on whether clients are actually engaging with the content. Low views or stale activity flags portals that need attention -- a nudge to follow up, refresh content, or check that credentials are still working.
75
+
76
+ ## Steps
77
+
78
+ ### Step 1: Determine the target portal
79
+
80
+ Check whether the user specified a portal slug:
81
+
82
+ - If a slug was provided (e.g., "show analytics for whzan"), use that slug directly.
83
+ - If no slug was provided, the skill will query analytics for ALL portals in the organization. Let the user know you are pulling org-wide analytics.
84
+
85
+ Capture the slug (if any) and the ORG_SLUG from the preamble output for the next step.
86
+
87
+ ### Step 2: Query the analytics data
88
+
89
+ Run the analytics query script. For a specific portal:
90
+
91
+ ```bash
92
+ cd $APP_PATH && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig $APP_PATH/tsconfig.json $SKILL_DIR/bin/query-analytics.ts --slug <slug> --org-id <org_id>
93
+ ```
94
+
95
+ For all portals (omit the `--slug` flag):
96
+
97
+ ```bash
98
+ cd $APP_PATH && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig $APP_PATH/tsconfig.json $SKILL_DIR/bin/query-analytics.ts --org-id <org_id>
99
+ ```
100
+
101
+ The script returns JSON on stdout. Expected shape for a single portal:
102
+
103
+ ```json
104
+ {
105
+ "ok": true,
106
+ "portal": {
107
+ "slug": "whzan",
108
+ "companyName": "Whzan Digital Health",
109
+ "views": 45,
110
+ "tabSwitches": 123,
111
+ "lastActivity": "2026-04-07T10:30:00Z",
112
+ "mostViewedTab": "Services overview",
113
+ "previousPeriodViews": 33
114
+ }
115
+ }
116
+ ```
117
+
118
+ For all portals, the response includes a `portals` array with the same fields per entry plus an `orgSummary` object.
119
+
120
+ If the script exits with a non-zero code, read stderr for the error JSON. Common errors:
121
+
122
+ - `portal_not_found`: The slug does not exist in this organization. Suggest running `/portal list` to see available portals.
123
+ - `no_analytics_data`: The portal exists but has zero recorded events. This is normal for brand-new portals.
124
+
125
+ ### Step 3: Format the output as an ASCII table
126
+
127
+ Present the data in a clean, scannable terminal format. For a single portal:
128
+
129
+ ```
130
+ Portal: whzan (Whzan Digital Health)
131
+ Period: Last 30 days
132
+ ════════════════════════════════════════
133
+ Views: 45
134
+ Tab switches: 123
135
+ Last activity: 2026-04-07 10:30
136
+ Most viewed tab: "Services overview"
137
+ ════════════════════════════════════════
138
+ ```
139
+
140
+ For multiple portals, use a summary table:
141
+
142
+ ```
143
+ ANALYTICS (Demo Company) — Last 30 days
144
+ ════════════════════════════════════════════════════════════
145
+ Slug Company Views Tab Switches Last Activity
146
+ ─────────── ──────────────────── ─────── ───────────── ──────────────
147
+ whzan Whzan Digital Health 45 123 7 Apr 10:30
148
+ acme Acme Corp 12 34 5 Apr 14:22
149
+ example Example Portal 0 0 Never
150
+ ════════════════════════════════════════════════════════════
151
+ Total views: 57 | Active portals: 2/3
152
+ ```
153
+
154
+ ### Step 4: Show trends when previous period data is available
155
+
156
+ If the analytics response includes `previousPeriodViews` (or equivalent previous-period fields), calculate and display the delta:
157
+
158
+ ```
159
+ Views: 45 (+12 vs previous 30d)
160
+ ```
161
+
162
+ Use a `+` prefix for increases and `-` for decreases. If the previous period had zero views and the current has some, show it as `(new activity)` rather than a percentage.
163
+
164
+ For the multi-portal table, append a trend column:
165
+
166
+ ```
167
+ Slug Views Trend
168
+ ─────────── ─────── ──────────
169
+ whzan 45 +12 (+36%)
170
+ acme 12 -3 (-20%)
171
+ example 0 --
172
+ ```
173
+
174
+ ### Step 5: Provide actionable commentary
175
+
176
+ After the table, add a brief note if any portals need attention:
177
+
178
+ - **Zero views in 30 days**: "Portal 'example' has had no views. Consider checking credentials or sending a reminder to the client."
179
+ - **Declining trend**: "Portal 'acme' views dropped 20%. The client may need a content refresh or follow-up call."
180
+ - **Strong engagement**: "Portal 'whzan' is your most active portal. Good engagement."
181
+
182
+ Keep commentary to 1-2 sentences maximum. Do not over-explain. The numbers speak for themselves.
183
+
184
+ ## Conventions
185
+
186
+ - Always format dates as `DD Mon YYYY` or `DD Mon HH:MM` for recency (e.g., "7 Apr 10:30").
187
+ - Use double-line box drawing (`═`) for outer borders and single-line (`─`) for internal dividers.
188
+ - Right-align numeric columns in multi-portal tables for scannability.
189
+ - If the user asks for a specific time period (e.g., "last 7 days"), pass `--period 7d` to the script if supported. Default is 30 days.
190
+ - Never expose raw JSON to the user. Always format as the ASCII table described above.
191
+ - If learnings indicate the user prefers a specific portal or checks analytics frequently, mention the most relevant portal first.
192
+
193
+ ## Analytics Events
194
+
195
+ The analytics system tracks the following event types. Understanding what is tracked helps interpret the numbers:
196
+
197
+ - **page_view**: Recorded when a client loads the portal page. One view per page load, not per session. If a client refreshes the page, that counts as a second view.
198
+ - **tab_switch**: Recorded when a client clicks a different tab within the portal. This is the best engagement signal -- it means the client is actively exploring content, not just landing on the page and leaving.
199
+ - **file_download**: Recorded when a client downloads a document from the Documents tab. This indicates high-intent engagement.
200
+ - **share_link_access**: Recorded when someone accesses the portal via a share link rather than username/password login.
201
+
202
+ When reading the analytics output, tab switches relative to views gives you a rough engagement depth. A portal with 10 views and 30 tab switches means clients are exploring (3 tabs per visit on average). A portal with 10 views and 2 tab switches means clients are landing and leaving.
203
+
204
+ ## Time Periods
205
+
206
+ The default analytics period is 30 days. The script supports the following periods via the `--period` flag:
207
+
208
+ - `7d` -- last 7 days (useful for checking recent activity after sharing a portal)
209
+ - `30d` -- last 30 days (default, good general-purpose view)
210
+ - `90d` -- last 90 days (useful for long-running client relationships)
211
+ - `all` -- all time (useful for seeing total engagement since portal creation)
212
+
213
+ When the user asks for a custom period, map their language to the closest supported option:
214
+ - "this week" or "recent" -> `7d`
215
+ - "this month" -> `30d`
216
+ - "this quarter" -> `90d`
217
+ - "total" or "all time" or "since we started" -> `all`
218
+
219
+ ## Interpreting Zero Activity
220
+
221
+ A portal with zero views is not necessarily a problem. Context matters:
222
+
223
+ - **New portal (created in last 7 days)**: Zero views is expected if credentials have not been shared yet. Note: "Portal was created recently. Share credentials with the client to start tracking engagement."
224
+ - **Portal with credentials but no views**: The client may not have received the credentials, or the email landed in spam. Suggest: "Credentials exist but no one has logged in. Consider resending or generating a share link."
225
+ - **Portal that previously had views but now has zero**: The client relationship may have gone cold. This is the most actionable signal -- suggest a follow-up.
226
+ - **Portal with credentials and share link but no views**: The client may not have used the link yet, or it may have been buried in email. Suggest resending the share link with `/portal share`.
227
+
228
+ ## Telemetry
229
+
230
+ If telemetry is enabled, the analytics skill records a minimal event after each query. The telemetry payload does not include any analytics data (view counts, portal slugs, or company names). It only records that the skill was invoked and how long the query took:
231
+
232
+ ```json
233
+ {"skill":"portal-analytics","ts":"2026-04-07T12:00:00Z","duration_s":2,"outcome":"success"}
234
+ ```
235
+
236
+ ## Error Handling
237
+
238
+ - If the preamble fails (no config, no Prisma), stop and show the error. Do not attempt to query analytics.
239
+ - If `query-analytics.ts` returns a non-zero exit code, display the error message from stderr and suggest a fix.
240
+ - If DATABASE_URL is empty, tell the user: "No database configured. Run /portal setup to connect your database."
241
+ - If the analytics query times out (large dataset), suggest narrowing the period: "Query took too long. Try a shorter period: /portal analytics whzan --period 7d"
242
+
243
+ ## Learnings Integration
244
+
245
+ After displaying analytics, check the learnings file for patterns that add context:
246
+
247
+ - If a learning records when credentials were last shared for this portal, note the time gap: "Credentials were shared 3 days ago. Give the client a few more days before following up."
248
+ - If a learning records that the user prefers weekly analytics checks, tailor the output to highlight week-over-week changes rather than 30-day totals.
249
+ - Do not write new learnings from this skill. Analytics is a read-only operation.
250
+
251
+ ## Completion
252
+
253
+ As a final step, log skill completion:
254
+
255
+ ```bash
256
+ echo '{"skill":"portal-analytics","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/timeline.jsonl" 2>/dev/null
257
+ ```
258
+
259
+ ## Related Skills
260
+
261
+ - `/portal status` -- broader health dashboard with scoring (uses analytics as one input)
262
+ - `/portal list` -- see all portals without analytics data
263
+ - `/portal share` -- generate a share link for a portal with strong engagement
@@ -0,0 +1,341 @@
1
+ ---
2
+ name: portal-create
3
+ description: |
4
+ Scaffold a new client portal from a meeting transcript, template, or description.
5
+ Trigger phrases: "create a portal", "new portal", "set up a client page", "make a portal for". (showpane)
6
+ allowed-tools: [Bash, Read, Write, Edit, Glob, Grep]
7
+ ---
8
+
9
+ If the user asks for unsupported hosted behavior, risky upload types, or cloud-specific
10
+ capabilities, read `skills/shared/platform-constraints.md` and apply the relevant limits.
11
+
12
+ ## Preamble (run first)
13
+
14
+ ```bash
15
+ # Read config
16
+ CONFIG="$HOME/.showpane/config.json"
17
+ if [ ! -f "$CONFIG" ]; then
18
+ echo "Showpane not configured. Run /portal setup first."
19
+ exit 1
20
+ fi
21
+ APP_PATH=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
22
+ DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','docker'))" 2>/dev/null)
23
+ ORG_SLUG=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
24
+ APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
25
+ if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
26
+ DATABASE_URL="${DATABASE_URL:-}"
27
+ if [ ! -d "$APP_PATH/node_modules/.prisma" ]; then
28
+ echo "App dependencies not installed. Run: cd $APP_PATH && npm install"
29
+ exit 1
30
+ fi
31
+ SKILL_DIR="${SHOWPANE_TOOLCHAIN_DIR:-$HOME/.showpane/current}"
32
+ SKILL_VERSION=$(cat "$SKILL_DIR/VERSION" 2>/dev/null || echo "unknown")
33
+ echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
34
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
35
+ [ -f "$LEARN_FILE" ] && echo "LEARNINGS: $(wc -l < "$LEARN_FILE" | tr -d ' ') loaded" || echo "LEARNINGS: 0"
36
+
37
+ # Predictive next-skill suggestion
38
+ if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
39
+ _RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
40
+ [ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
41
+ fi
42
+
43
+ # Search relevant learnings
44
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
45
+ if [ -f "$LEARN_FILE" ]; then
46
+ _LEARN_COUNT=$(wc -l < "$LEARN_FILE" 2>/dev/null | tr -d ' ')
47
+ echo "LEARNINGS: $_LEARN_COUNT entries"
48
+ if [ "$_LEARN_COUNT" -gt 0 ] 2>/dev/null; then
49
+ echo "RECENT_LEARNINGS:"
50
+ tail -5 "$LEARN_FILE" 2>/dev/null
51
+ fi
52
+ fi
53
+
54
+ # Track skill execution
55
+ SHOWPANE_TIMELINE="$HOME/.showpane/timeline.jsonl"
56
+ mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
57
+ echo '{"skill":"portal-create","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
58
+ ```
59
+
60
+ If RECENT_SKILLS is shown, suggest the likely next skill:
61
+ - After portal-create → suggest /portal-preview
62
+ - After portal-preview → suggest /portal-deploy or /portal-share
63
+ - After portal-deploy → suggest /portal-status or /portal-verify
64
+ - After portal-setup → suggest /portal-create
65
+ - After portal-credentials → suggest /portal-share
66
+ - After portal-update → suggest /portal-deploy
67
+
68
+ If RECENT_LEARNINGS is shown, review them before proceeding. Past learnings may contain
69
+ relevant warnings or tips for this operation. Apply them where relevant but don't
70
+ mention them unless they directly affect the current task.
71
+
72
+ ## Steps
73
+
74
+ ### Step 1: Determine the portal slug
75
+
76
+ If the user provided a slug (e.g., `/portal create acme-health`), use it. Otherwise, infer from context — the company name mentioned in conversation, a meeting transcript, or ask the user directly.
77
+
78
+ Validate the slug by running:
79
+
80
+ ```bash
81
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/check-slug.ts" --slug <slug> --org-id <org_id>
82
+ ```
83
+
84
+ The script returns `{"valid":true}` or `{"valid":false,"reason":"...","message":"..."}`. If invalid:
85
+ - `reason: "format"` — slug must be lowercase alphanumeric + hyphens, 2-50 chars, no leading/trailing hyphens
86
+ - `reason: "reserved"` — reserved names: api, client, s, admin, static, _next, health, example
87
+ - `reason: "taken"` — a portal with this slug already exists for this org
88
+
89
+ If invalid, explain the issue and ask for a different slug.
90
+
91
+ Also ask for the client's website domain (e.g., "acme-health.com"). This is optional but enables auto-branding:
92
+ - If provided, the client logo will be fetched via `getLogoUrl(domain)` and stored in `ClientPortal.logoUrl`
93
+ - If not provided, an initial-based logo is generated via `getInitialLogo(companyName)` and stored as a data URI
94
+
95
+ ### Step 2: Granola MCP integration (optional)
96
+
97
+ Try to use the Granola MCP `list_meetings` tool to fetch recent meetings. This is a convenience, not a requirement.
98
+
99
+ **If Granola MCP is available:**
100
+ 1. Call `list_meetings` to get recent meetings
101
+ 2. Present the list to the user: date, title, participants
102
+ 3. Ask which meeting to use (or "none — I'll describe the portal manually")
103
+ 4. If a meeting is selected, call `get_meeting_transcript` to retrieve the full transcript
104
+ 5. Store the transcript for content analysis in Step 4
105
+
106
+ **If Granola MCP is NOT available** (tool-not-found error):
107
+ - Skip gracefully. Do not mention Granola or show an error.
108
+ - Ask: "Do you have a meeting transcript to paste, or shall I work from a description?"
109
+ - If the user pastes a transcript, store it for analysis
110
+ - If no transcript, proceed to template selection with manual content
111
+
112
+ Never fail or block because Granola is unavailable. It is purely additive.
113
+
114
+ ### Step 3: Template selection
115
+
116
+ Ask which template to use as a starting point:
117
+
118
+ 1. **sales-followup** — Meeting notes, next steps, documents. Best after a sales call.
119
+ 2. **consulting** — Project overview, deliverables, timeline. Best for ongoing engagements.
120
+ 3. **onboarding** — Welcome, setup steps, resources. Best for new client onboarding.
121
+ 4. **blank** — Start from scratch with just an overview tab.
122
+
123
+ Read the chosen template file from `$SKILL_DIR/templates/` for structural inspiration:
124
+
125
+ ```bash
126
+ cat "$SKILL_DIR/templates/sales-followup/sales-followup-client.tsx"
127
+ ```
128
+
129
+ Always also read the example portal as your quality and style reference:
130
+
131
+ ```bash
132
+ cat "$APP_PATH/src/app/(portal)/client/example/example-client.tsx"
133
+ ```
134
+
135
+ The template provides content structure. The example provides quality and styling. Match the example's patterns: card styles, typography, spacing, responsive breakpoints. Templates are inspiration, not rigid scaffolds. Adapt the structure to fit the actual content.
136
+
137
+ ### Step 4: Analyze transcript (if available)
138
+
139
+ If a transcript was provided (from Granola or pasted), analyze it to extract:
140
+
141
+ | Signal Found | Tab to Generate | Content Pattern |
142
+ |---|---|---|
143
+ | Meeting discussion topics | "Meetings" | Collapsible `<details>` sections per meeting with bullet points |
144
+ | Action items, next steps, follow-ups | "Next Steps" | Numbered timeline with status indicators (done/pending) |
145
+ | Documents mentioned (contracts, NDAs, proposals) | "Documents" | Download cards with file type icons from lucide-react |
146
+ | Service descriptions, capabilities discussed | "Services" | Grid of cards with title and description |
147
+ | Pricing, costs, tiers discussed | "Pricing" | Comparison table or tier cards |
148
+ | Project phases, milestones | "Timeline" | Vertical timeline with phase markers |
149
+
150
+ Always generate at minimum:
151
+ - An **overview/welcome tab** (first tab, always)
152
+ - At least **one additional tab** based on content
153
+
154
+ If the transcript is rich, generate up to 5-6 tabs. Do not exceed 6 tabs total.
155
+
156
+ Extract from the transcript:
157
+ - **Company name** and contact details (for PortalShell props)
158
+ - **Key discussion points** (for meeting notes)
159
+ - **Agreed actions** (for next steps timeline)
160
+ - **Mentioned documents** (for documents tab)
161
+ - **Services or products discussed** (for services/overview content)
162
+
163
+ ### Step 5: Generate the portal files
164
+
165
+ Create two files in `$APP_PATH/src/app/(portal)/client/<slug>/`:
166
+
167
+ #### File 1: `page.tsx` (server component)
168
+
169
+ ```tsx
170
+ import { <SlugName>PortalClient } from "./<slug>-client";
171
+
172
+ export const metadata = {
173
+ title: "<Company Name> | Portal",
174
+ };
175
+
176
+ export default function <SlugName>Portal() {
177
+ return <<SlugName>PortalClient />;
178
+ }
179
+ ```
180
+
181
+ Convert the slug to PascalCase for the component name (e.g., `acme-health` becomes `AcmeHealth`).
182
+
183
+ #### File 2: `<slug>-client.tsx` (client component)
184
+
185
+ This is the main file. Follow these conventions exactly:
186
+
187
+ **Imports:**
188
+ ```tsx
189
+ "use client";
190
+
191
+ import { type ReactNode } from "react";
192
+ import { /* icons from lucide-react */ } from "lucide-react";
193
+ import { cn } from "@/lib/utils";
194
+ import { PortalShell } from "@/components/portal-shell";
195
+ ```
196
+
197
+ **Structure:**
198
+ - Define each tab's content as a separate function component within the file (e.g., `function OverviewTab()`, `function DocumentsTab()`)
199
+ - Export a single named component: `export function <SlugName>PortalClient()`
200
+ - The exported component returns `<PortalShell>` with all required props
201
+
202
+ **PortalShell props (all required):**
203
+ - `companyName` — the org's company name (from config/DB)
204
+ - `companyLogo` — a `<span>` with the first letter of the company name, white text
205
+ - `clientName` — the client's company name (from transcript or user input)
206
+ - `clientLogoSrc` — if client domain was provided: use `getLogoUrl(domain)` from `app/src/lib/branding.ts`. If not: use `getInitialLogo(clientName)` to generate an SVG data URI. Store the chosen URL in the ClientPortal record's `logoUrl` field
207
+ - `clientLogoAlt` — the client company name
208
+ - `lastUpdated` — today's date formatted as "7 April 2026"
209
+ - `contact` — object with `name`, `title`, `avatarSrc`, `email` (from org config)
210
+ - `tabs` — array of tab objects with `id`, `label`, `icon`, `content`, and optional `badge`
211
+ - `hideFooterOnTab` — set to `"overview"` (hides the contact footer on the first tab since it typically has contact info inline)
212
+
213
+ **Styling conventions (match the example portal exactly):**
214
+ - Cards: `rounded-2xl border bg-white shadow-sm`
215
+ - Card padding: `p-5 sm:p-6`
216
+ - Section headings: `text-base font-bold tracking-tight text-gray-900`
217
+ - Body text: `text-sm leading-relaxed text-gray-600`
218
+ - Small text: `text-xs text-gray-500`
219
+ - Bullet points: use `<span>` dots with `h-1.5 w-1.5 rounded-full` for bullet markers
220
+ - Status badges: `rounded-full px-2 py-0.5 text-[11px] font-medium` with color variants
221
+ - Buttons: `rounded-lg bg-gray-900 px-5 py-2 text-xs font-semibold text-white`
222
+ - Grid layouts: `grid gap-3 sm:grid-cols-2` for card grids
223
+ - Spacing between sections: `mt-6` with `mb-4` for section headings
224
+ - Responsive: mobile-first, use `sm:` breakpoints for wider layouts
225
+
226
+ **Icon usage:**
227
+ Import only the icons you need from `lucide-react`. Common choices:
228
+ - `Presentation` for overview/services
229
+ - `CalendarDays` for meetings
230
+ - `FileText` for documents
231
+ - `BarChart3` for analytics/strategy
232
+ - `ListChecks` for next steps
233
+ - `Download` for download buttons
234
+ - `ChevronDown` for collapsible sections
235
+ - `Clock` for timeline
236
+ - `DollarSign` for pricing
237
+
238
+ **For collapsible meeting sections**, use the same `<details>` pattern as the example:
239
+ ```tsx
240
+ <details open={defaultOpen} className="group">
241
+ <summary className="flex cursor-pointer list-none items-center gap-1.5 text-left">
242
+ <ChevronDown className="h-3.5 w-3.5 shrink-0 text-gray-400 transition-transform group-open:rotate-180" />
243
+ <h4 className="text-sm font-semibold text-gray-900">{title}</h4>
244
+ </summary>
245
+ <div className="mt-2 pl-5">{children}</div>
246
+ </details>
247
+ ```
248
+
249
+ ### Step 6: Create database record
250
+
251
+ Run the create-portal script to register the portal in the database:
252
+
253
+ ```bash
254
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id <org_id>
255
+ ```
256
+
257
+ This creates the `ClientPortal` record with the slug, company name, and links it to the Organization. It does NOT create credentials — that is a separate step via `/portal credentials`.
258
+
259
+ ### Step 7: Self-review
260
+
261
+ After generating the files, read them back and verify:
262
+
263
+ 1. **PortalShell used?** The client component must use `<PortalShell>` as its root element.
264
+ 2. **Minimum 2 tabs?** Check the `tabs` array has at least 2 entries.
265
+ 3. **Contact info in props?** The `contact` prop must have `name`, `title`, `avatarSrc`, `email`.
266
+ 4. **"use client" directive?** Must be the first line of the client component.
267
+ 5. **Imports correct?** `cn` from `@/lib/utils`, `PortalShell` from `@/components/portal-shell`.
268
+ 6. **No hardcoded localhost URLs?** Links should be relative or use placeholders.
269
+ 7. **Responsive patterns?** Check for `sm:` breakpoints on grids and padding.
270
+ 8. **Tab content functions?** Each tab should have its own function, not inline JSX.
271
+
272
+ If any check fails, fix the issue before proceeding.
273
+
274
+ ### Step 8: Open preview
275
+
276
+ Check if the dev server is running:
277
+
278
+ ```bash
279
+ lsof -i :3000 -sTCP:LISTEN -t 2>/dev/null
280
+ ```
281
+
282
+ If running, open the portal in the browser:
283
+
284
+ ```bash
285
+ open "http://localhost:3000/client/<slug>"
286
+ ```
287
+
288
+ If not running, suggest:
289
+
290
+ > "Start the dev server with `/portal dev` to preview your portal at http://localhost:3000/client/<slug>"
291
+
292
+ ### Step 9: Summary and next steps
293
+
294
+ Print a summary:
295
+
296
+ ```
297
+ Portal created: <slug>
298
+
299
+ Client: <company_name>
300
+ Tabs: Overview, Next Steps, Documents (3 tabs)
301
+ Files: src/app/(portal)/client/<slug>/page.tsx
302
+ src/app/(portal)/client/<slug>/<slug>-client.tsx
303
+
304
+ Next steps:
305
+ 1. Create login credentials: /portal credentials <slug>
306
+ 2. Preview the portal: /portal preview <slug>
307
+ 3. Edit content: /portal update <slug>
308
+ 4. Deploy: /portal deploy
309
+ ```
310
+
311
+ ### Step 10: Record learning
312
+
313
+ Append a learning about the portal creation for future reference:
314
+
315
+ ```bash
316
+ echo '{"skill":"portal-create","key":"portal-created","insight":"Created portal <slug> for <company>. Template: <template>. Tabs: <tab_list>.","confidence":8,"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/learnings.jsonl"
317
+ ```
318
+
319
+ ## Completion
320
+
321
+ As a final step, log skill completion:
322
+
323
+ ```bash
324
+ echo '{"skill":"portal-create","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/timeline.jsonl" 2>/dev/null
325
+ ```
326
+
327
+ ## Conventions
328
+
329
+ - Always use `"use client"` as the first line of the client component
330
+ - Import `PortalShell` from `@/components/portal-shell`
331
+ - Import `cn` from `@/lib/utils`
332
+ - Import icons from `lucide-react`
333
+ - Minimum 2 tabs, maximum 6
334
+ - First tab is always overview/welcome
335
+ - Tab content as separate function components within the file
336
+ - Cards: `rounded-2xl border bg-white shadow-sm`
337
+ - Text: `text-sm` for body, `text-base font-bold` for headings
338
+ - Spacing: consistent `p-5 sm:p-6` for card padding
339
+ - Scope lock: only create files in `$APP_PATH/src/app/(portal)/client/<slug>/`
340
+ - Never modify shared components, other portals, or lib files during portal creation
341
+ - The example portal at `$APP_PATH/src/app/(portal)/client/example/` is the gold standard — when in doubt, match its patterns