Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
SKULK_BASE_URL=
SKULK_TOKEN=
HPL_BASE_URL=
HPL_TOKEN=
4 changes: 4 additions & 0 deletions .github/workflows/carmel-judgment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
- name: 📦 Install dependencies
run: npm ci

- name: 📜 Enforce JSON output contract
id: json
run: npm run json:check

- name: 🧪 Run tests
id: tests
run: npm run test:run
Expand Down
74 changes: 62 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,41 @@

Formerly developed under the codename **Skulk**, HPL is built to work just as well for humans at the keyboard as it does for automation, CI, and agent-driven workflows.

This package is in **active alpha development**. Interfaces are stabilizing, but expect iteration.
This package is in **active alpha development**. Interfaces are stabilizing, but iteration is expected.

---

## What HPL Connects To

HPL is the CLI for the **Human Pattern Lab API**.
HPL is a deterministic bridge between:

By default, it targets a Human Pattern Lab API instance. You can override the API endpoint with `--base-url` to use staging or a self-hosted deployment of the same API.
- the **Human Pattern Lab Content Repository** (source of truth)
- the **Human Pattern Lab API** (runtime index and operations)

Written content lives as Markdown in a dedicated content repository.
The API syncs and indexes that content so it can be rendered by user interfaces.

By default, HPL targets a Human Pattern Lab API instance. You can override the API endpoint with `--base-url` to use staging or a self-hosted deployment of the same API.

> Note: `--base-url` is intended for alternate deployments of the Human Pattern Lab API, not arbitrary third-party APIs.

---

## Authentication

HPL supports token-based authentication via the `SKULK_TOKEN` environment variable.
HPL supports token-based authentication via the `HPL_TOKEN` environment variable.

```bash
export SKULK_TOKEN="your-api-token"
export HPL_TOKEN="your-api-token"
```

(Optional) Override the API endpoint:

```bash
export SKULK_BASE_URL="https://api.thehumanpatternlab.com"
export HPL_BASE_URL="https://api.thehumanpatternlab.com"
```

> `SKULK_BASE_URL` should point to the **root** of a Human Pattern Lab API deployment.
> `HPL_BASE_URL` should point to the **root** of a Human Pattern Lab API deployment.
> Do not include additional path segments.

Some API endpoints may require authentication depending on server configuration.
Expand All @@ -63,20 +69,44 @@ Some API endpoints may require authentication depending on server configuration.
npm install -g @thehumanpatternlab/hpl@alpha
```

### Run a command
### Sync Lab Notes from the content repository

```bash
hpl notes sync --dir ./src/labnotes/en
hpl notes sync --content-repo AdaInTheLab/the-human-pattern-lab-content
```

For machine-readable output:
This pulls structured Markdown content from the repository and synchronizes it into the Human Pattern Lab system.

### Machine-readable output

```bash
hpl --json notes sync
```

---

## Content Source Configuration (Optional)

By default, `notes sync` expects a content repository with the following structure:

```text
labnotes/
en/
*.md
ko/
*.md
```

You may pin a default content repository using an environment variable:

```bash
export HPL_CONTENT_REPO="AdaInTheLab/the-human-pattern-lab-content"
```

This allows `hpl notes sync` to run without explicitly passing `--content-repo`.

---

## Commands

```text
Expand All @@ -87,7 +117,8 @@ hpl <domain> <action> [options]

- `hpl notes list`
- `hpl notes get <slug>`
- `hpl notes sync --dir <path>`
- `hpl notes sync --content-repo <owner/name|url>`
- `hpl notes sync --dir <path>` (advanced / local development)

### health

Expand All @@ -107,12 +138,31 @@ hpl version

Structured output is treated as a **contract**, not a courtesy.

When `--json` is provided:

- stdout contains **only valid JSON**
- stderr is used for logs and diagnostics
- exit codes are deterministic

A verification step is included:

```bash
npm run json:check
```

Fails if any non-JSON output appears on stdout.
This command fails if any non-JSON output appears on stdout.

---

## What HPL Is Not

HPL is not:
- a chatbot interface
- an agent framework
- a memory system
- an inference layer

It is a command-line tool for interacting with Human Pattern Lab systems in a predictable, human-owned way.

---

Expand Down
185 changes: 27 additions & 158 deletions bin/hpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,170 +2,39 @@
/* ===========================================================
🌌 HUMAN PATTERN LAB — CLI ENTRYPOINT
-----------------------------------------------------------
Commands:
- version
- capabilities
- health
- notes list
- notes get <slug>
Contract: --json => JSON only on stdout
Purpose:
- Register top-level commands
- Define global flags (--json)
- Parse argv
Contract:
--json => JSON only on stdout (enforced in command handlers)
Notes:
- Avoid process.exit() inside command handlers (can trip libuv on Windows + tsx).
Avoid process.exit() inside handlers (Windows + tsx stability).
=========================================================== */

import { Command } from "commander";
import { writeHuman, writeJson } from "../src/io";
import { EXIT } from "../src/contract/exitCodes";
import { runVersion } from "../src/commands/version";
import { runCapabilities } from "../src/commands/capabilities";
import { runHealth } from "../src/commands/health";
import { runNotesList } from "../src/commands/notes/list";
import { runNotesGet } from "../src/commands/notes/get";
import { renderTable } from "../src/render/table";
import { formatTags, safeLine, stripHtml } from "../src/render/text";

type GlobalOpts = { json?: boolean };
import { versionCommand } from "../src/commands/version.js";
import { capabilitiesCommand } from "../src/commands/capabilities.js";
import { healthCommand } from "../src/commands/health.js";
import { notesCommand } from "../src/commands/notes/notes.js";

const program = new Command();

program
.name("hpl")
.description("Human Pattern Lab CLI (alpha)")
.option("--json", "Emit contract JSON only on stdout")
.showHelpAfterError();

function setExit(code: number) {
// Let Node exit naturally (important for Windows + tsx stability).
process.exitCode = code;
}

program
.command("version")
.description("Show CLI version (contract: show_version)")
.action(() => {
const opts = program.opts<GlobalOpts>();
const envelope = runVersion("version");
if (opts.json) writeJson(envelope);
else writeHuman(`${envelope.data.name} ${envelope.data.version}`);
setExit(EXIT.OK);
});
import { EXIT } from "../src/contract/exitCodes.js";

program
.command("capabilities")
.description("Show CLI capabilities for agents (contract: show_capabilities)")
.action(() => {
const opts = program.opts<GlobalOpts>();
const envelope = runCapabilities("capabilities");
if (opts.json) writeJson(envelope);
else {
writeHuman(`intentTier: ${envelope.data.intentTier}`);
writeHuman(`schemaVersions: ${envelope.data.schemaVersions.join(", ")}`);
writeHuman(`supportedIntents:`);
for (const i of envelope.data.supportedIntents) writeHuman(` - ${i}`);
}
setExit(EXIT.OK);
});
const program = new Command();

program
.command("health")
.description("Check API health (contract: check_health)")
.action(async () => {
const opts = program.opts<GlobalOpts>();
const result = await runHealth("health");

if (opts.json) {
writeJson(result.envelope);
} else {
if (result.envelope.status === "ok") {
const d: any = (result.envelope as any).data;
const db = d.dbPath ? ` (db: ${d.dbPath})` : "";
writeHuman(`ok${db}`);
} else {
const e: any = (result.envelope as any).error;
writeHuman(`error: ${e.code} — ${e.message}`);
}
}
setExit(result.exitCode);
});

const notes = program.command("notes").description("Lab Notes commands");

notes
.command("list")
.description("List lab notes (contract: render_lab_note)")
.option("--limit <n>", "Limit number of rows (client-side)", (v) => parseInt(v, 10))
.action(async (cmdOpts: { limit?: number }) => {
const opts = program.opts<GlobalOpts>();
const result = await runNotesList("notes list");

if (opts.json) {
writeJson(result.envelope);
setExit(result.exitCode);
return;
}

if (result.envelope.status !== "ok") {
const e: any = (result.envelope as any).error;
writeHuman(`error: ${e.code} — ${e.message}`);
setExit(result.exitCode);
return;
}

const data: any = (result.envelope as any).data;
const rows = (data.notes as any[]) ?? [];
const limit = Number.isFinite(cmdOpts.limit) && (cmdOpts.limit as any) > 0 ? (cmdOpts.limit as any) : rows.length;
const slice = rows.slice(0, limit);

const table = renderTable(slice, [
{ header: "slug", width: 28, value: (n) => safeLine(String((n as any).slug ?? "")) },
{ header: "title", width: 34, value: (n) => safeLine(String((n as any).title ?? "")) },
{ header: "status", width: 10, value: (n) => safeLine(String((n as any).status ?? "-")) },
{ header: "dept", width: 8, value: (n) => safeLine(String((n as any).department_id ?? "-")) },
{ header: "tags", width: 22, value: (n) => formatTags((n as any).tags) },
]);

writeHuman(table);
writeHuman(`\ncount: ${data.count}`);
setExit(result.exitCode);
});

notes
.command("get")
.description("Get a lab note by slug (contract: render_lab_note)")
.argument("<slug>", "Lab Note slug")
.option("--raw", "Print raw contentHtml (no HTML stripping)")
.action(async (slug: string, cmdOpts: { raw?: boolean }) => {
const opts = program.opts<GlobalOpts>();
const result = await runNotesGet(slug, "notes get");

if (opts.json) {
writeJson(result.envelope);
setExit(result.exitCode);
return;
}

if (result.envelope.status !== "ok") {
const e: any = (result.envelope as any).error;
writeHuman(`error: ${e.code} — ${e.message}`);
setExit(result.exitCode);
return;
}

const n: any = (result.envelope as any).data;

writeHuman(`# ${n.title}`);
writeHuman(`slug: ${n.slug}`);
if (n.status) writeHuman(`status: ${n.status}`);
if (n.type) writeHuman(`type: ${n.type}`);
if (n.department_id) writeHuman(`department_id: ${n.department_id}`);
if (n.published) writeHuman(`published: ${n.published}`);
if (Array.isArray(n.tags)) writeHuman(`tags: ${formatTags(n.tags)}`);
writeHuman("");

const body = cmdOpts.raw ? String(n.contentHtml ?? "") : stripHtml(String(n.contentHtml ?? ""));
writeHuman(body || "(no content)");
setExit(result.exitCode);
});

// Let commander handle errors; set exit code without hard exit.
program.parseAsync(process.argv).catch(() => setExit(EXIT.UNKNOWN));
.name("hpl")
.description("Human Pattern Lab CLI (alpha)")
.option("--json", "Emit contract JSON only on stdout")
.showHelpAfterError()
.configureHelp({ helpWidth: 100 });

program.addCommand(versionCommand());
program.addCommand(capabilitiesCommand());
program.addCommand(healthCommand());
program.addCommand(notesCommand());

program.parseAsync(process.argv).catch(() => {
process.exitCode = EXIT.UNKNOWN;
});
Loading
Loading