Perform code optimization and document cleanup (#1)
check / check (ubuntu-latest) (push) Successful in 2m5s
check / check (macos-latest) (push) Has been cancelled
check-online / check-online (ubuntu-latest) (push) Successful in 1m53s

## Summary
- add repository-wide quality tooling and verification scaffolding, including CI workflows, pnpm workspace setup, ESLint/Prettier/markdown checks, and generated-output verification helpers
- reorganize skill sources and generation flow by introducing canonical `_source` variants, generator/manifests, reusable helper abstractions, and shared web-automation/browser utilities
- clean up and expand documentation so the root README flows into docs and skill docs, with clearer development, reviewer, installer, and workflow guidance

## Notable changes
- docs flow and consistency cleanup across `README.md`, `docs/README.md`, and related docs
- new scripts for `check`, docs verification, generated-file verification, shell portability, and safe directory replacement
- refactors in Atlassian and web-automation skill runtimes to reduce duplication and centralize reusable code
- changelog, development documentation, and CI surface updates

## Test Plan
- [ ] `pnpm run check`
- [ ] review generated/manifests and skill sync outputs
- [ ] smoke-check docs flow from `README.md` to `docs/README.md` to skill docs

## Notes
- this branch currently includes tracked `skills/web-automation/shared/node_modules` content that should be reviewed carefully as potentially noisy/accidental committed artifacts

Co-authored-by: Stefano Fiorini <stefano.fiorini@firsthorizon.com>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-05-04 04:41:34 +00:00
parent 2deab1c1b4
commit 251148c3ff
373 changed files with 28504 additions and 1281 deletions
@@ -0,0 +1,103 @@
{
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
"generator": "scripts/generate-skills.mjs",
"generatedRoot": "pi-package/skills/atlassian",
"files": [
{
"path": "scripts/package.json",
"kind": "file",
"mode": "644",
"sha256": "97dd269f922ea83f6abfd29497ca532b94540acd1461b3a012f92853db55e196"
},
{
"path": "scripts/pnpm-lock.yaml",
"kind": "file",
"mode": "644",
"sha256": "15556a6f53e68bb8d92d2710aae0836bc80af7f29be9d63aa1b87fcbd33732c6"
},
{
"path": "scripts/src/adf.ts",
"kind": "file",
"mode": "644",
"sha256": "c7c3b4a78ccd8fb5a8ab99c82e0eab67a0a0d656b3985c1f56817bda199ad20f"
},
{
"path": "scripts/src/cli.ts",
"kind": "file",
"mode": "644",
"sha256": "90dcc029adf0625b86c5eec44c5c1fd11bbf95ffe1185016d139c8a6982d54ff"
},
{
"path": "scripts/src/command-helpers.ts",
"kind": "file",
"mode": "644",
"sha256": "aa03d8d288c8c00485ea10d3b3a60804c1b9ee23ef265004e7912f3242dbcee7"
},
{
"path": "scripts/src/config.ts",
"kind": "file",
"mode": "644",
"sha256": "700dcdce96afab5294426e09f539135ae5432632370260190d6292071422eb3f"
},
{
"path": "scripts/src/confluence.ts",
"kind": "file",
"mode": "644",
"sha256": "28f65f280cd9b6119ce7eab583d0083231525ad6dc04b73389cb5dcbab5bf095"
},
{
"path": "scripts/src/files.ts",
"kind": "file",
"mode": "644",
"sha256": "16296eaa3ae41a4d7c694773036f9bb4bd2baa2db6a9c318078532b713678dba"
},
{
"path": "scripts/src/health.ts",
"kind": "file",
"mode": "644",
"sha256": "1db4b49e05b16a095b7e7ca31cdc4e22ebda19e20e05c40baaaac648eaec0d08"
},
{
"path": "scripts/src/http.ts",
"kind": "file",
"mode": "644",
"sha256": "66444b777d4d9b14d9793eb051c586eb811d2b36815b1018dd9d7517666c7eb2"
},
{
"path": "scripts/src/jira.ts",
"kind": "file",
"mode": "644",
"sha256": "bec0e81a0424dd412c36988cef42c01a95f044ee8346ba626e7eb8bd79379f07"
},
{
"path": "scripts/src/output.ts",
"kind": "file",
"mode": "644",
"sha256": "38e99818582a4962c09a83175634cba2bfead6acf33bd5f43cdca5caed7100a0"
},
{
"path": "scripts/src/raw.ts",
"kind": "file",
"mode": "644",
"sha256": "48fd54bd0cdb421badb58f9be2933a039fe3b9350bbe6191070c9f7bb0054670"
},
{
"path": "scripts/src/types.ts",
"kind": "file",
"mode": "644",
"sha256": "9f92d27ab68604d5abfd0f5dc9552b96fed6d1f9fc7dc6eb30190d8b617628bf"
},
{
"path": "scripts/tsconfig.json",
"kind": "file",
"mode": "644",
"sha256": "3c2eb7ba5c95a16cada153de4787ca7a4bf179609bf3848e12ff15b1b7927a68"
},
{
"path": "SKILL.md",
"kind": "file",
"mode": "644",
"sha256": "69d83441799f3feada7fbf85691bda16fc30718b724871d7e37cfac574db2253"
}
]
}
+2
View File
@@ -3,6 +3,8 @@ name: atlassian
description: Interact with Atlassian Cloud Jira and Confluence through a portable task-oriented CLI for search, issue/page edits, comments, transitions, and bounded raw requests.
---
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/_source/pi/SKILL.md and run `pnpm run sync:pi`. -->
# Atlassian (Pi)
Portable Atlassian workflows for pi using the shared TypeScript CLI in `scripts/`.
@@ -1,5 +1,5 @@
{
"name": "atlassian-skill-scripts",
"name": "@ai-coding-skills/atlassian-pi-mirror",
"version": "1.0.0",
"description": "Shared runtime for the Atlassian skill",
"type": "module",
@@ -16,5 +16,6 @@
"tsx": "^4.20.5",
"typescript": "^5.9.2"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"private": true
}
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
const TEXT_NODE = "text";
function textNode(text: string) {
@@ -1,8 +1,10 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import process from "node:process";
import { pathToFileURL } from "node:url";
import { Command } from "commander";
import { resolveFormat } from "./command-helpers.js";
import { createConfluenceClient } from "./confluence.js";
import { loadConfig } from "./config.js";
import { readWorkspaceFile } from "./files.js";
@@ -10,7 +12,7 @@ import { runHealthCheck } from "./health.js";
import { createJiraClient } from "./jira.js";
import { writeOutput } from "./output.js";
import { runRawCommand } from "./raw.js";
import type { FetchLike, OutputFormat, Writer } from "./types.js";
import type { FetchLike, Writer } from "./types.js";
type CliContext = {
cwd?: string;
@@ -20,10 +22,6 @@ type CliContext = {
stderr?: Writer;
};
function resolveFormat(format: string | undefined): OutputFormat {
return format === "text" ? "text" : "json";
}
function createRuntime(context: CliContext) {
const cwd = context.cwd ?? process.cwd();
const env = context.env ?? process.env;
@@ -0,0 +1,25 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import type { CommandOutput, OutputFormat } from "./types.js";
/**
* Produce the standard dry-run response payload for write operations.
*
* Use this when `--dry-run` is passed to skip the actual API call and
* echo the pending request back to the caller.
*
* @example
* if (input.dryRun) return dryRunResponse(request);
*/
export function dryRunResponse<T>(data: T): CommandOutput<T> {
return { ok: true, dryRun: true, data };
}
/**
* Resolve the `--format` CLI option to a typed OutputFormat.
*
* Returns `"text"` only for the exact string `"text"`;
* all other values (including `undefined`) fall back to `"json"`.
*/
export function resolveFormat(format: string | undefined): OutputFormat {
return format === "text" ? "text" : "json";
}
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import path from "node:path";
import { config as loadDotEnv } from "dotenv";
@@ -1,3 +1,5 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import { dryRunResponse } from "./command-helpers.js";
import { sendJsonRequest } from "./http.js";
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
@@ -177,13 +179,7 @@ export function createConfluenceClient(options: ConfluenceClientOptions) {
},
};
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
const raw = await sendJsonRequest({
config,
@@ -223,13 +219,7 @@ export function createConfluenceClient(options: ConfluenceClientOptions) {
},
};
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
const raw = await sendJsonRequest({
config,
@@ -266,13 +256,7 @@ export function createConfluenceClient(options: ConfluenceClientOptions) {
},
};
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
const raw = await sendJsonRequest({
config,
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import { readFile } from "node:fs/promises";
import path from "node:path";
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import { createJsonHeaders, createStatusError } from "./http.js";
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import { createBasicAuthHeader } from "./config.js";
import type { AtlassianConfig, FetchLike } from "./types.js";
@@ -1,4 +1,6 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import { markdownToAdf } from "./adf.js";
import { dryRunResponse } from "./command-helpers.js";
import { sendJsonRequest } from "./http.js";
import type { AtlassianConfig, CommandOutput, FetchLike, JiraIssueSummary } from "./types.js";
@@ -161,13 +163,7 @@ export function createJiraClient(options: JiraClientOptions) {
},
});
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
const raw = await send("POST", "/rest/api/3/issue", request.body);
return { ok: true, data: raw };
@@ -192,13 +188,7 @@ export function createJiraClient(options: JiraClientOptions) {
fields,
});
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
await send("PUT", `/rest/api/3/issue/${input.issue}`, request.body);
return {
@@ -215,13 +205,7 @@ export function createJiraClient(options: JiraClientOptions) {
body: markdownToAdf(input.body),
});
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
const raw = await send("POST", `/rest/api/3/issue/${input.issue}/comment`, request.body);
return {
@@ -242,13 +226,7 @@ export function createJiraClient(options: JiraClientOptions) {
},
);
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
await send("POST", `/rest/api/3/issue/${input.issue}/transitions`, request.body);
return {
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import type { CommandOutput, OutputFormat, Writer } from "./types.js";
function renderText(payload: CommandOutput<unknown>) {
@@ -1,3 +1,5 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
import { dryRunResponse } from "./command-helpers.js";
import { readWorkspaceFile } from "./files.js";
import { sendJsonRequest } from "./http.js";
import type { AtlassianConfig, CommandOutput, FetchLike } from "./types.js";
@@ -61,13 +63,7 @@ export async function runRawCommand(
...(body === undefined ? {} : { body }),
};
if (input.dryRun) {
return {
ok: true,
dryRun: true,
data: request,
};
}
if (input.dryRun) return dryRunResponse(request);
const data = await sendJsonRequest({
config,
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/atlassian/shared/scripts/ and run `pnpm run sync:pi`.
export type AtlassianConfig = {
baseUrl: string;
jiraBaseUrl: string;
@@ -0,0 +1,31 @@
{
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
"generator": "scripts/generate-skills.mjs",
"generatedRoot": "pi-package/skills/create-plan",
"files": [
{
"path": "SKILL.md",
"kind": "file",
"mode": "644",
"sha256": "8767db25ce6f03e141ce4c48f37e9d7c4958c3cf3d70729f3bd7214b84f6d065"
},
{
"path": "templates/continuation-runbook.md",
"kind": "file",
"mode": "644",
"sha256": "1685cded3d4abaf03122a490175ff03b7da593ce60cbca97ae15fadcb706617f"
},
{
"path": "templates/milestone-plan.md",
"kind": "file",
"mode": "644",
"sha256": "364c08bf0a5ee3738195a0770db72c5a4c9ad7f7fb89eaa064eb8c67f47ad69a"
},
{
"path": "templates/story-tracker.md",
"kind": "file",
"mode": "644",
"sha256": "ecb550ea9dcd9dde6c813e90af9f538bf5a247fc249e8e323f2b7bf583e52196"
}
]
}
+2
View File
@@ -3,6 +3,8 @@ name: create-plan
description: Use when a user asks to create or maintain a structured implementation plan in pi, including milestones, bite-sized stories, and resumable local planning artifacts under ai_plan.
---
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
# Create Plan (Pi)
Create and maintain a local plan workspace under `ai_plan/` at project root.
@@ -1,3 +1,4 @@
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
# Continuation Runbook: [Plan Title]
## Reference Files (START HERE)
@@ -65,15 +66,19 @@ Work from this folder (`ai_plan/YYYY-MM-DD-<short-title>/`) and always follow th
## Full Context Reproduction
### Project Overview
[What this project/feature is about]
### User Requirements
[All gathered requirements]
### Scope
[In scope / out of scope]
### Dependencies
[External dependencies, prerequisites, related systems]
---
@@ -81,16 +86,19 @@ Work from this folder (`ai_plan/YYYY-MM-DD-<short-title>/`) and always follow th
## Key Specifications
### Type Definitions
```typescript
// Copy-paste ready type definitions
```
### Enums & Constants
```typescript
// All enums/constants needed
```
### API Endpoints
```typescript
// Request/response shapes
```
@@ -108,16 +116,19 @@ Work from this folder (`ai_plan/YYYY-MM-DD-<short-title>/`) and always follow th
## Verification Commands
### Lint (changed files first)
```bash
# example: pnpm eslint <changed-file-1> <changed-file-2>
```
### Typecheck
```bash
# example: pnpm tsc --noEmit
```
### Tests (target changed scope first)
```bash
# example: pnpm test -- <related spec/file>
```
@@ -1,6 +1,8 @@
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
# [Plan Title]
## Overview
- **Goal:** [One sentence describing the end state]
- **Created:** YYYY-MM-DD
- **Status:** In Progress | Complete
@@ -8,37 +10,46 @@
## Context
### Requirements
[Gathered requirements from user questions]
### Constraints
[Technical, business, or timeline constraints]
### Success Criteria
[How we know this is complete]
## Architecture
### Design Decisions
[Key architectural choices and rationale]
### Component Relationships
[How pieces fit together]
### Data Flow
[How data moves through the system]
## Milestones
### M1: [Name]
**Description:** [What this milestone achieves]
**Acceptance Criteria:**
- [ ] [Criterion 1]
- [ ] [Criterion 2]
**Stories:** S-101, S-102, S-103...
**Milestone Completion Rule (MANDATORY):**
- Run lint/typecheck/tests for changed files.
- Commit locally (DO NOT push).
- Stop and ask user for feedback.
@@ -48,15 +59,18 @@
---
### M2: [Name]
**Description:** [What this milestone achieves]
**Acceptance Criteria:**
- [ ] [Criterion 1]
- [ ] [Criterion 2]
**Stories:** S-201, S-202, S-203...
**Milestone Completion Rule (MANDATORY):**
- Run lint/typecheck/tests for changed files.
- Commit locally (DO NOT push).
- Stop and ask user for feedback.
@@ -68,16 +82,19 @@
## Technical Specifications
### Types & Interfaces
```typescript
// Key type definitions
```
### API Contracts
```typescript
// Endpoint signatures, request/response shapes
```
### Constants & Enums
```typescript
// Shared constants
```
@@ -94,6 +111,7 @@
## Related Plan Files
This file is part of the plan folder under `ai_plan/`:
- `original-plan.md` - Original approved plan (reference for original intent)
- `final-transcript.md` - Final planning transcript (reference for rationale/context)
- `milestone-plan.md` - This file (full specification)
@@ -1,6 +1,8 @@
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/create-plan/_source/pi/ and run `pnpm run sync:pi`. -->
# Story Tracker: [Plan Title]
## Progress Summary
- **Current Milestone:** M1
- **Stories Complete:** 0/N
- **Milestones Approved:** 0/M
@@ -46,15 +48,18 @@
## Update Instructions (MANDATORY)
Before starting any story:
1. Mark story as `in-dev`
2. Update "Last Updated"
After completing any story:
1. Mark story as `completed`
2. Add local commit hash to Notes
3. Update "Stories Complete" and "Last Updated"
At milestone boundary:
1. Run lint/typecheck/tests for changed files
2. Commit (no push)
3. Request feedback
@@ -63,4 +68,5 @@ At milestone boundary:
6. Continue only after approval
After all milestones approved:
- Ask permission to push and then mark plan completed.
@@ -0,0 +1,19 @@
{
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
"generator": "scripts/generate-skills.mjs",
"generatedRoot": "pi-package/skills/do-task",
"files": [
{
"path": "SKILL.md",
"kind": "file",
"mode": "644",
"sha256": "4920ad0cdeda546b37432c2268159724de54ddb01922308f2df88fcca4db8d31"
},
{
"path": "templates/task-plan.md",
"kind": "file",
"mode": "644",
"sha256": "fd38213fabf350e14b48c5209910d00c16ff74c455101618063835fa8c19e73e"
}
]
}
+2
View File
@@ -3,6 +3,8 @@ name: do-task
description: Execute a single user-supplied prompt end-to-end in pi with plan review, implementation review, verification, and one persistent task-plan artifact.
---
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/do-task/_source/pi/ and run `pnpm run sync:pi`. -->
# Do Task (Pi)
Execute an ad-hoc user prompt end-to-end: parse, clarify, plan, implement, verify, review, commit, and optionally push.
@@ -1,3 +1,4 @@
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/do-task/_source/pi/ and run `pnpm run sync:pi`. -->
# Task Plan: [Short Title]
> **Variant guardrail (pi):** Required workflow skills (`brainstorming`, `test-driven-development`, `verification-before-completion`, `finishing-a-development-branch`, `using-git-worktrees`) must be available to pi as documented in `docs/PI-SUPERPOWERS.md`. Load the relevant workflow skill before entering its matching phase.
@@ -0,0 +1,13 @@
{
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
"generator": "scripts/generate-skills.mjs",
"generatedRoot": "pi-package/skills/implement-plan",
"files": [
{
"path": "SKILL.md",
"kind": "file",
"mode": "644",
"sha256": "578e5b4fbc443ade486e4aa034b0875ebfa7eefd3a1268852ac1c5cbf71f28bf"
}
]
}
@@ -3,6 +3,8 @@ name: implement-plan
description: Use when a plan folder created by create-plan must be executed in pi with milestone verification, reviewer gates, local commits, and resumable tracker updates.
---
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/implement-plan/_source/pi/ and run `pnpm run sync:pi`. -->
# Implement Plan (Pi)
Execute an existing plan under `ai_plan/` milestone by milestone, using verification gates, reviewer approval, and local commits after each approved milestone.
@@ -0,0 +1,103 @@
{
"$schema": "https://ai-coding-skills.dev/schemas/generated-manifest/v1.json",
"generator": "scripts/generate-skills.mjs",
"generatedRoot": "pi-package/skills/web-automation",
"files": [
{
"path": "scripts/auth.ts",
"kind": "file",
"mode": "644",
"sha256": "c0940f452437b05b95e58a9a7ab265fb50aa412bd672e82fedd6a37cbfb3d505"
},
{
"path": "scripts/browse.ts",
"kind": "file",
"mode": "644",
"sha256": "d7e4b4c50116032e5a00f90bca27e069dfc5bbf6eeb06ec8f8edc9e5a9792ab8"
},
{
"path": "scripts/check-install.js",
"kind": "file",
"mode": "644",
"sha256": "e46ee8cbe103794bf1e9c3466bb0fbd21079ceddc60ad9521299e8bc0150e48f"
},
{
"path": "scripts/extract.js",
"kind": "file",
"mode": "644",
"sha256": "6fa2a0589de8afd6501e332e5fa263e1344187ea43a33590b431cdee59d04217"
},
{
"path": "scripts/flow.ts",
"kind": "file",
"mode": "644",
"sha256": "94f3e7987cab253dc3c9e80656a11759fada13b3915608bff7ae08418602f366"
},
{
"path": "scripts/lib/browser.ts",
"kind": "file",
"mode": "644",
"sha256": "879b5f883ff1f888d45ed20be05c2d9bc3d6fe5305a1972b7d49a7e6c0e24934"
},
{
"path": "scripts/package.json",
"kind": "file",
"mode": "644",
"sha256": "1d9226da585c65106dacd874e5e6c7951f5a5b2b0f0cf5902f305a951ca4b44d"
},
{
"path": "scripts/pnpm-lock.yaml",
"kind": "file",
"mode": "644",
"sha256": "17017e15e8b04311f5d53bdd37065b2f5a514a3119f40a0403148440ed181437"
},
{
"path": "scripts/scan-local-app.ts",
"kind": "file",
"mode": "644",
"sha256": "9e1818c254a633e087715609152936dcb3613a0aa724d40a8a13460510691dc7"
},
{
"path": "scripts/scrape.ts",
"kind": "file",
"mode": "644",
"sha256": "a1a3d81d57d9e8ab1854ce3cb230bdd39ae1087ec50c9fe82cc58f5f2663ebeb"
},
{
"path": "scripts/test-full.ts",
"kind": "file",
"mode": "644",
"sha256": "76a647e840753621445c36894bff62e163f6a2e4d0860fa8e64d8df45fe21e08"
},
{
"path": "scripts/test-minimal.ts",
"kind": "file",
"mode": "644",
"sha256": "59e0b2319d3f7521b2a8a4fca2d779afaa157bf2d160160fdec8cb56bea30b4f"
},
{
"path": "scripts/test-profile.ts",
"kind": "file",
"mode": "644",
"sha256": "6cf0141581a9275bfa8a070a36212cef5f6417d64df3df3e614ec682008376b9"
},
{
"path": "scripts/tsconfig.json",
"kind": "file",
"mode": "644",
"sha256": "e5f22d72266068cf410976c880511f2ec1875445256e11739a5e1de6ffedf38d"
},
{
"path": "scripts/turndown-plugin-gfm.d.ts",
"kind": "file",
"mode": "644",
"sha256": "c5001c059b160eff18a4097a8a0a7b96689b4ebc374543c7d5bf6e40b0d8a5ac"
},
{
"path": "SKILL.md",
"kind": "file",
"mode": "644",
"sha256": "7ff56c1c50697439875f4dd0a7f7697962c8ba2105a4f66ab7b170f5dcc762bd"
}
]
}
@@ -3,6 +3,8 @@ name: web-automation
description: Browse and scrape web pages using Playwright-compatible CloakBrowser. Use when automating web workflows, extracting rendered page content, handling authenticated sessions, or running multi-step browser flows.
---
<!-- ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/_source/pi/SKILL.md and run `pnpm run sync:pi`. -->
# Web Automation with CloakBrowser (Pi)
Automated web browsing and scraping for pi using the shared runtime bundle in `scripts/`.
@@ -1,4 +1,5 @@
#!/usr/bin/env npx tsx
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
/**
* Authentication handler for web automation
@@ -10,7 +11,7 @@
* npx tsx auth.ts --url "https://example.com" --type auto
*/
import { getPage, launchBrowser } from './browse.js';
import { getPage, launchBrowser } from './lib/browser.js';
import parseArgs from 'minimist';
import type { Page, BrowserContext } from 'playwright-core';
import { createInterface } from 'readline';
@@ -1,4 +1,5 @@
#!/usr/bin/env npx tsx
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
/**
* Browser launcher using CloakBrowser with persistent profile
@@ -9,12 +10,13 @@
* npx tsx browse.ts --url "https://example.com" --headless false --wait 5000
*/
import { launchPersistentContext } from 'cloakbrowser';
import { homedir } from 'os';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
import parseArgs from 'minimist';
import type { Page, BrowserContext } from 'playwright-core';
import type { BrowserContext } from 'playwright-core';
import { getProfilePath, launchBrowser, getPage } from './lib/browser.js';
// Re-export shared helpers so existing imports of browse.ts continue to work.
export { getProfilePath, launchBrowser, getPage };
interface BrowseOptions {
url: string;
@@ -36,36 +38,6 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const getProfilePath = (): string => {
const customPath = process.env.CLOAKBROWSER_PROFILE_PATH;
if (customPath) return customPath;
const profileDir = join(homedir(), '.cloakbrowser-profile');
if (!existsSync(profileDir)) {
mkdirSync(profileDir, { recursive: true });
}
return profileDir;
};
export async function launchBrowser(options: {
headless?: boolean;
}): Promise<BrowserContext> {
const profilePath = getProfilePath();
const envHeadless = process.env.CLOAKBROWSER_HEADLESS;
const headless = options.headless ?? (envHeadless ? envHeadless === 'true' : true);
console.log(`Using profile: ${profilePath}`);
console.log(`Headless mode: ${headless}`);
const context = await launchPersistentContext({
userDataDir: profilePath,
headless,
humanize: true,
});
return context;
}
export async function browse(options: BrowseOptions): Promise<BrowseResult> {
const browser = await launchBrowser({ headless: options.headless });
const page = browser.pages()[0] || await browser.newPage();
@@ -111,14 +83,6 @@ export async function browse(options: BrowseOptions): Promise<BrowseResult> {
}
}
export async function getPage(options?: {
headless?: boolean;
}): Promise<{ page: Page; browser: BrowserContext }> {
const browser = await launchBrowser({ headless: options?.headless });
const page = browser.pages()[0] || await browser.newPage();
return { page, browser };
}
async function main() {
const args = parseArgs(process.argv.slice(2), {
string: ['url', 'output'],
@@ -1,4 +1,5 @@
#!/usr/bin/env node
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
import fs from "node:fs";
import path from "node:path";
@@ -18,11 +19,9 @@ async function main() {
try {
await import("cloakbrowser");
await import("playwright-core");
await import("better-sqlite3");
await import("esbuild");
} catch (error) {
fail(
"Missing dependency/config: web-automation requires cloakbrowser, playwright-core, better-sqlite3, and esbuild.",
"Missing dependency/config: web-automation requires cloakbrowser and playwright-core.",
error instanceof Error ? error.message : String(error)
);
}
@@ -34,7 +33,6 @@ async function main() {
}
process.stdout.write("OK: cloakbrowser + playwright-core installed\n");
process.stdout.write("OK: better-sqlite3 + esbuild installed\n");
process.stdout.write("OK: CloakBrowser integration detected in browse.ts\n");
}
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
import fs from "node:fs";
import path from "node:path";
@@ -1,8 +1,9 @@
#!/usr/bin/env npx tsx
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
import parseArgs from 'minimist';
import type { Page } from 'playwright-core';
import { launchBrowser } from './browse';
import { launchBrowser } from './lib/browser.js';
type Step =
| { action: 'goto'; url: string }
@@ -0,0 +1,76 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
/**
* Shared browser-launch and profile helpers for web-automation scripts.
*
* Centralises the three reusable primitives that every command entry point
* needs:
* - getProfilePath() — resolve the persistent CloakBrowser profile dir
* - launchBrowser() — launch a CloakBrowser persistent context
* - getPage() — get a ready Page + BrowserContext pair
*
* All command entry points (auth.ts, browse.ts, flow.ts, scan-local-app.ts)
* import from here instead of duplicating these bodies.
*/
import { launchPersistentContext } from 'cloakbrowser';
import { existsSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import type { BrowserContext, Page } from 'playwright-core';
/**
* Return the path to the persistent CloakBrowser profile directory.
*
* Uses `CLOAKBROWSER_PROFILE_PATH` env var when set; otherwise defaults to
* `~/.cloakbrowser-profile/` and creates it if it does not exist.
*/
export function getProfilePath(): string {
const customPath = process.env.CLOAKBROWSER_PROFILE_PATH;
if (customPath) return customPath;
const profileDir = join(homedir(), '.cloakbrowser-profile');
if (!existsSync(profileDir)) {
mkdirSync(profileDir, { recursive: true });
}
return profileDir;
}
/**
* Launch a CloakBrowser persistent context with the shared profile.
*
* Headless mode is resolved in order:
* 1. `options.headless` (explicit caller preference)
* 2. `CLOAKBROWSER_HEADLESS` env var
* 3. `true` (safe default)
*/
export async function launchBrowser(options: {
headless?: boolean;
}): Promise<BrowserContext> {
const profilePath = getProfilePath();
const envHeadless = process.env.CLOAKBROWSER_HEADLESS;
const headless = options.headless ?? (envHeadless ? envHeadless === 'true' : true);
console.log(`Using profile: ${profilePath}`);
console.log(`Headless mode: ${headless}`);
const context = await launchPersistentContext({
userDataDir: profilePath,
headless,
humanize: true,
});
return context;
}
/**
* Return a ready `{ page, browser }` pair using the shared persistent profile.
*
* Re-uses the first existing page or opens a new one if the context is empty.
*/
export async function getPage(options?: {
headless?: boolean;
}): Promise<{ page: Page; browser: BrowserContext }> {
const browser = await launchBrowser({ headless: options?.headless });
const page = browser.pages()[0] || (await browser.newPage());
return { page, browser };
}
@@ -1,5 +1,5 @@
{
"name": "web-automation-scripts",
"name": "@ai-coding-skills/web-automation-pi-mirror",
"version": "1.0.0",
"description": "Web browsing and scraping scripts using CloakBrowser",
"type": "module",
@@ -32,5 +32,6 @@
"tsx": "^4.7.0",
"typescript": "^5.3.0"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"private": true
}
@@ -1,8 +1,10 @@
#!/usr/bin/env npx tsx
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
import { mkdirSync, writeFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { getPage } from './browse.js';
import type { Page } from 'playwright-core';
import { getPage } from './lib/browser.js';
type NavResult = {
requestedUrl: string;
@@ -39,30 +41,34 @@ function getRoutes(baseUrl: string): string[] {
return [baseUrl];
}
async function gotoWithStatus(page: any, url: string): Promise<NavResult> {
type GotoError = { error: unknown };
async function gotoWithStatus(page: Page, url: string): Promise<NavResult> {
const response = await page
.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 })
.catch((error: unknown) => ({ error }));
.catch((error: unknown): GotoError => ({ error }));
if (response?.error) {
if (response !== null && response !== undefined && 'error' in response) {
const gotoError = response as GotoError;
return {
requestedUrl: url,
url: page.url(),
status: null,
title: await page.title().catch(() => ''),
error: String(response.error),
error: String(gotoError.error),
};
}
const httpResponse = response as Awaited<ReturnType<Page['goto']>>;
return {
requestedUrl: url,
url: page.url(),
status: response ? response.status() : null,
status: httpResponse ? httpResponse.status() : null,
title: await page.title().catch(() => ''),
};
}
async function textOrNull(page: any, selector: string): Promise<string | null> {
async function textOrNull(page: Page, selector: string): Promise<string | null> {
const locator = page.locator(selector).first();
try {
if ((await locator.count()) === 0) return null;
@@ -73,7 +79,7 @@ async function textOrNull(page: any, selector: string): Promise<string | null> {
}
}
async function loginIfConfigured(page: any, baseUrl: string, lines: string[]) {
async function loginIfConfigured(page: Page, baseUrl: string, lines: string[]) {
const loginPath = env('SCAN_LOGIN_PATH');
const username = env('SCAN_USERNAME') ?? env('CLOAKBROWSER_USERNAME');
const password = env('SCAN_PASSWORD') ?? env('CLOAKBROWSER_PASSWORD');
@@ -109,7 +115,7 @@ async function loginIfConfigured(page: any, baseUrl: string, lines: string[]) {
lines.push('');
}
async function checkRoutes(page: any, baseUrl: string, lines: string[]) {
async function checkRoutes(page: Page, baseUrl: string, lines: string[]) {
const routes = getRoutes(baseUrl);
const routeChecks: RouteCheck[] = [];
@@ -1,4 +1,5 @@
#!/usr/bin/env npx tsx
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
/**
* Web scraper that extracts content to markdown
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
import { launchPersistentContext } from 'cloakbrowser';
import { homedir } from 'os';
import { join } from 'path';
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
import { launch } from 'cloakbrowser';
async function test() {
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
import { launchPersistentContext } from 'cloakbrowser';
import { homedir } from 'os';
import { join } from 'path';
@@ -11,6 +11,6 @@
"outDir": "./dist",
"rootDir": "."
},
"include": ["*.ts"],
"include": ["*.ts", "lib/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
@@ -1,3 +1,4 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
declare module 'turndown-plugin-gfm' {
import TurndownService from 'turndown';